内网机房里那台 CentOS 7 服务器,网线插着但出不了公网,yum 源是本地镜像,连pip install都得走内部源。项目要做全文检索,技术选型会上定的是 ElasticSearch,要求单机部署、数据不能丢、重启后能自己起来。我第一次接这类活的时候想得很简单:下个 tar 包,解压,改配置,nohup一挂,完事。结果在那台机器上折腾了大半天,从 JDK 版本到vm.max_map_count,从目录权限到启动用户,一个接一个地冒出来。后来换成 Docker 方案,把镜像在能上网的机器上拉好、导出、拷进去导入,容器一条命令起来,反而清爽很多。
这篇就围绕docker + CentOS 7 + 离线 + ElasticSearch 单机版这个组合,把我实际趟过的流程、踩过的坑、以及每一步为什么要这么做的理由写清楚。适合手上正好有一台不通外网的内网机器、又需要快速搭一个可用 ES 实例的同行参考;对 Docker 刚上手、但已经能看懂基本命令的读者也友好,关键的坑我都会展开讲原因,而不是只丢一条命令给你抄。
1. 内网机器上装搜索服务,为什么我最后把 tar 包方案换成了容器
1.1 一次"解压即用"的幻想是怎么破灭的
ElasticSearch 官方确实提供 tar.gz 发行包,文档里也写着解压后bin/elasticsearch就能启动。听起来很美,但真正在内网机器上执行时,第一个问题就来了:发行包自带 JDK,但自带的 JDK 版本和你系统里已经装的那套大概率不是一个东西。CentOS 7 上很多人习惯性先yum install java-1.8.0-openjdk,然后发现 ES 7.x 之后的版本要求 JDK 11 及以上,环境变量JAVA_HOME一旦被系统那套抢走,启动直接报版本不兼容。你得去改jvm.options或者显式指定ES_JAVA_HOME,而这台机器上可能还有别的 Java 应用在跑,改环境变量又会牵连别人。
第二个问题是启动用户。ElasticSearch 出于安全考虑,禁止以 root 身份运行。内网机器上你登进去十有八九就是 root,于是得useradd一个专用账号,把整个安装目录chown过去,再用su - esuser切过去启动。看着也不算大事,但如果你还要配 systemd 开机自启,service 文件里的User=、Group=、LimitNOFILE、LimitMEMLOCK一个都不能少,写错了就是启动失败、日志里还看不出原因。
第三个问题是清理和回滚。tar 包方案是"散装"的:程序目录、配置目录、数据目录、日志目录、PID 文件,散落在你当时随手定的几个路径上。三个月后要升级版本,你得记住当初每一个路径,还得处理配置文件的手工合并。容器方案在这些事情上是有明确边界的——镜像里是只读的程序和默认配置,你只需要管好挂出来的那几个目录。
我并不是说 tar 包方案不能用,它在某些极端受限的环境(比如连 Docker 都不让装的机器)里仍是唯一选择。但只要机器上允许跑容器,我就会优先选容器,原因在下一小节展开。
1.2 容器方案在离线现场的三个实际好处
第一个好处是依赖被一次性封进镜像。ES 需要什么 JDK、需要哪些系统库、默认配置长什么样,全在镜像里定死了。你在能上网的机器上把镜像拉下来验证过能跑,导出成 tar 文件,拷到内网机器导入,跑起来的行为和验证时一模一样。这中间不存在"内网机器的 glibc 版本低了半档"这类问题,因为容器用的是镜像里的用户态环境。
第二个好处是启动参数即配置。离线环境最怕的是"配置散落各处、出了事找不到是谁改的"。用容器,端口映射、堆内存、单机模式、数据目录,全部集中在一条docker run命令或者一个 compose 文件里。这条命令本身就可以当成部署文档存档,下次重建的时候照着敲一遍就行。
第三个好处是资源边界清晰。单机版 ES 最怕的是把整台机器的内存吃干,导致同机器上其他服务被 OOM Killer 干掉。容器可以用-m限制内存上限,用ES_JAVA_OPTS限制 JVM 堆,两层限制叠起来,至少不会出现"ES 一启动,隔壁服务全躺下"的局面。
需要提前说清楚一点:容器化不会帮你解决内核参数问题。vm.max_map_count这类参数是宿主机层面的,容器共享宿主内核,该调还得调。这一点很多人第一次做会误解,以为进了容器就跟宿主机隔离了,实际上网络、内核参数、部分资源限制都是共享或半共享的。第 3 节会专门讲这块。
2. 断网环境下真正要搬的东西:镜像、运行时与系统基线
2.1 Docker 引擎本身的离线落地顺序
很多人默认内网机器上已经有 Docker,实际去一看,docker -v报 command not found。所以第一件事是确认宿主机有没有容器运行时。CentOS 7 上装 Docker 有两条路:走官方仓库的docker-ce,或者走系统自带的docker(版本很老,1.13 那种,不建议)。离线情况下只能走 rpm 包本地安装。
需要准备的最小包集合大概是这几个(版本号以你实际下载的为准,这里只列依赖关系):
| 包名 | 作用 | 备注 |
|---|---|---|
| containerd.io | 容器运行时底座 | 必须先装,版本要满足 docker-ce 的最低要求 |
| docker-ce-cli | docker 命令行 | 和 docker-ce 版本保持一致 |
| docker-ce | 引擎主体 | 主包 |
| docker-ce-rootless-extras | 可选 | 不用 rootless 模式可以不装 |
| docker-compose-plugin | 可选 | 想用 compose 就得带上 |
在能上网的机器上把这些 rpm 下齐,可以用yumdownloader --resolve --destdir=./rpms docker-ce docker-ce-cli containerd.io,--resolve会把依赖一起拉下来。拷到内网机器后,进目录执行:
# 一次性装完当前目录下所有 rpm,--nodeps 谨慎使用,先不加 yum localinstall -y ./*.rpm # 装完确认 systemctl enable docker systemctl start docker docker versionyum localinstall比rpm -ivh好在它会尝试从已有的本地源里补齐缺失依赖。如果它报缺依赖而你又确定那些依赖已经装了,再考虑加--nodeps,但加之前一定先rpm -qa | grep 包名确认一遍,别图省事把依赖顺序搞乱。
装完之后还有一个容易忽略的点:CentOS 7 默认的存储驱动选择。如果/var/lib/docker所在分区是 xfs,要确认挂载参数里带ftype=1,否则 overlay2 驱动起不来,Docker 会退回到devicemapper,性能和稳定性都差一截。用xfs_info /var/lib/docker | grep ftype看一眼,输出ftype=1就是对的。这个参数是格式化时定的,事后改不了,只能重新格式化分区或者换一个目录挂载。所以在规划磁盘的时候就得多问一句。
2.2 镜像的导出、校验与导入
镜像这一步是整个离线流程里最"物理"的环节。我的习惯是在一台能出公网的机器上操作,尽量让这台机器的 Docker 版本和内网机器接近,减少意外。
# 1. 拉取指定版本,别用 latest,生产环境锁版本 docker pull elasticsearch:7.17.9 # 2. 看一眼镜像大小,心里有个数 docker images | grep elasticsearch # 3. 导出成 tar,加 gzip 压缩一下,体积能小不少 docker save elasticsearch:7.17.9 | gzip > es-7.17.9.tar.gz # 4. 算个校验值,拷贝过程中损坏能第一时间发现 sha256sum es-7.17.9.tar.gz > es-7.17.9.tar.gz.sha256到了内网机器上,先校验再导入:
# 校验,输出 OK 才继续 sha256sum -c es-7.17.9.tar.gz.sha256 # 导入 gunzip -c es-7.17.9.tar.gz | docker load # 确认镜像在本地 docker images | grep elasticsearch这里有两个实践细节值得说。一是为什么不用docker export。export导的是容器文件系统快照,会丢掉镜像的分层信息和元数据(比如ENTRYPOINT、环境变量、暴露端口),导进去之后你得到的只是一个"裸文件系统",还得自己拼启动命令。save导的是完整镜像,包括所有层和配置,导入后和原镜像等价。
二是版本锁定。elasticsearch:latest会随上游更新漂移,今天拉的 tomorrow 可能就变了。离线环境里你没法随时重拉,所以一开始就写死具体版本号,把版本号记在部署文档里。同理,如果后面还要装 Kibana 做可视化,Kibana 的版本必须和 ES 主版本严格一致,7.17.9 的 ES 配 7.17.x 的 Kibana,跨大版本基本连不上。
2.3 别忘了一起搬过去的小工具
纯离线环境下,很多平时觉得理所当然的工具其实没有。我在一次部署里就吃过亏:ES 起来了,想curl验证一下,结果那台最小化安装的 CentOS 7 上压根没有 curl。所以搬镜像的时候顺手把这几样也带上:
curl或者wget,验证接口用;jq,解析 JSON 返回结果,比肉眼数括号强太多;netstat(来自 net-tools)或者ss(来自 iproute),确认端口有没有监听;vim或nano,总得有个能改配置的编辑器;- 如果打算用 compose 编排,
docker-compose的二进制文件也一并拷过去,放到/usr/local/bin并chmod +x。
这些东西单个都不大,但临时找不到的时候会非常耽误事。我的做法是在内网机器上建一个/opt/offline-tools目录,把这次部署用到的东西全丢进去,写个 README 记下每个文件干什么用的。半年后别人接手,或者自己回来做升级,打开这个目录就一目了然。
3. 容器起来之前的系统层准备:内核参数与资源限制
3.1 vm.max_map_count 到底管什么
这是 ES 部署里被问得最多的一个参数。容器启动后日志里出现:
max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]然后容器退出。原因不复杂:ElasticSearch 底层用 Lucene,Lucene 把索引数据放在内存映射文件(mmap)里,一个索引分片的段文件会占用多个内存映射区域。Linux 内核对单个进程能创建的 vm area 数量有上限,默认 65530,索引一多就不够用了。vm.max_map_count调的就是这个上限。
临时改(重启失效):
sysctl -w vm.max_map_count=262144永久改,写进配置文件,别直接改/etc/sysctl.conf(那个文件容易被别人覆盖),单独建一个:
cat > /etc/sysctl.d/99-elasticsearch.conf <<'EOF' vm.max_map_count = 262144 vm.overcommit_memory = 1 EOF sysctl --system sysctl vm.max_map_countvm.overcommit_memory = 1是另一条常被一起加上的。它让内核在分配内存时更宽松,避免 ES 在堆外内存申请时被拒绝。这个参数有没有必要,取决于机器上其他负载的情况,如果你这台机器只跑 ES,加上没坏处;如果机器上还有别的内存敏感服务,得评估一下。
需要强调:这两个参数改的是宿主机,不是容器。容器共享宿主内核,你在容器里执行sysctl -w通常会报Read-only file system,因为/proc/sys在容器里默认是只读挂载的。有些教程会让你给容器加--privileged然后进去改,这是错误做法,改也只影响容器命名空间内的视图,宿主机层面该报错还是报错。正确的位置就是宿主机。
3.2 内存锁定、swap 与文件句柄
bootstrap.memory_lock: true这个配置的意思是让 ES 把自己的堆内存锁定在物理内存里,不被换出到 swap。为什么要锁?因为 JVM 一旦发生堆内存换页,GC 停顿时间会从毫秒级跳到秒级,对搜索服务来说这是灾难性的。
要让它生效,需要两处配合。宿主机上要关掉 swap,或者至少保证 ES 进程的内存不会被换出:
# 临时关闭 swapoff -a # 永久关闭,注释掉 /etc/fstab 里的 swap 行 sed -i '/swap/s/^/#/' /etc/fstab容器侧要给它足够的 memlock 限额:
--ulimit memlock=-1:-1同时如果启用了 memory_lock,/usr/share/elasticsearch/config/elasticsearch.yml里对应的配置项要打开。7.x 之后的镜像支持用环境变量覆盖一部分配置,但bootstrap.memory_lock这种嵌套键用环境变量写起来别扭,我更倾向于把配置文件挂进去,或者用-e传 JVM 参数、用挂载覆盖 yml。
文件句柄数也不能少。ES 每个分片都要打开一堆文件,默认的 1024 肯定不够:
--ulimit nofile=65536:65536这个在docker run里给就行,宿主机的/etc/security/limits.conf也需要配一份,因为 Docker 守护进程自身的限制会传递下来:
* soft nofile 65536 * hard nofile 65536改完limits.conf要重新登录才生效,验证用ulimit -n。
3.3 存储驱动与磁盘空间的最低调要求
ES 对磁盘的写入是比较重的,单机版尤其如此。规划的时候留足空间,我的经验值是:索引数据本身的 3 倍。一份给数据,一份给段合并过程中的临时空间,一份给快照备份。
磁盘水位线是 ES 自己的一套保护机制,默认配置是:
| 水位线 | 默认阈值 | 触发行为 |
|---|---|---|
| low | 85% | 不再往该节点分配新分片 |
| high | 90% | 尝试把分片迁走(单机版无处可迁,只能告警) |
| flood_stage | 95% | 对索引加只读锁,写入直接失败 |
单机版没有别的节点可以迁,所以一旦碰到 high 水位线,你会看到集群一直 yellow 或者 red,写入报blocked by: [FORBIDDEN/12/index read-only / allow delete (api)]。这时候就算你删了数据、磁盘降下来了,索引的只读锁也不会自动解除,得手工执行:
curl -XPUT "http://localhost:9200/_all/_settings" -H 'Content-Type: application/json' -d' { "index.blocks.read_only_allow_delete": null }'这个坑我在一次日志量突增的场景里踩过,当时磁盘从 70% 一路冲到 96%,索引被锁,业务方那边写入全挂。事后我们把水位线调低了一点(比如 low 80、high 85),给自己留出反应时间,同时加了磁盘监控告警。阈值不是越低越好,调太低会导致正常的段合并也被限制,反而影响写入性能。
4. 启动参数的逐条拆解:单机模式、堆内存与数据落盘
4.1 discovery.type=single-node 背后的选举逻辑
ES 是一个分布式系统,即使你只跑一个节点,它内部也在走集群的发现和选举流程。默认配置下,节点会尝试去找cluster.initial_master_nodes或者组播发现其他节点,找不到就会反复重试,日志里刷一堆连接超时的报错,启动很慢,集群状态也可能一直不是 green。
discovery.type=single-node这个设置告诉 ES:跳过发现流程,我就是唯一节点,直接自举成 master。加上它之后,启动时间是秒级的,日志干净很多。
7.x 版本里如果不加这个参数,单纯跑单节点,通常会看到:
the default discovery settings are unsuitable for production use; at least one of [discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes] must be configured然后容器退出。8.x 的行为类似,只是报错文案略有不同。所以单机版部署,这个参数基本是必填项,省不了。
顺带说一句,single-node模式下的集群健康状态通常是yellow而不是 green。原因不是配置错了,而是 ES 默认给每个索引建 1 个主分片 + 1 个副本分片,单节点上副本无处分配,状态就是 yellow。解决办法见第 6 节,把副本数改成 0 就绿了。这个 yellow 在很多监控系统里会触发告警,提前跟运维打个招呼,或者直接把索引模板里的副本数改掉。
4.2 堆内存怎么给,给多少
JVM 堆的设置有两个常见的错误方向:给太小,GC 频繁,查询慢;给太大,堆外内存和文件系统缓存被挤压,反而更慢。
基本规则是这样的:堆内存不超过物理内存的 50%,并且不超过 31GB。前半句是因为 Lucene 依赖操作系统的文件缓存来加速段文件读取,你把内存全给了堆,文件缓存就没了,查询性能会掉。后半句是因为 JVM 在堆小于约 32GB 时可以使用压缩对象指针(compressed oops),超过之后指针宽度翻倍,同样的堆能装的对象反而变少,是纯粹的负收益。
常见的配置对照:
| 物理内存 | 建议堆大小 | 说明 |
|---|---|---|
| 4 GB | 1g ~ 2g | 只适合测试和小数据量 |
| 8 GB | 2g ~ 4g | 小规模生产可用 |
| 16 GB | 6g ~ 8g | 中等规模 |
| 32 GB | 12g ~ 16g | 比较舒服的配置 |
| 64 GB | 24g ~ 31g | 上限卡在 31g |
设置方式是通过环境变量传 JVM 参数:
-e "ES_JAVA_OPTS=-Xms4g -Xmx4g"-Xms和-Xmx一定要设成一样大。设成不一样,JVM 会做堆的伸缩,伸缩过程本身有开销,而且容易在低峰期缩下来、高峰期再扩上去,触发 Full GC。固定大小是最省心的。
另外容器本身也限制一下内存上限,比如-m 8g,这样即使 JVM 那边算错了,也不会把宿主机吃穿。注意-m的值要大于堆大小加上堆外开销,堆 4g 的话,容器给 8g 是合理的,给 5g 就很容易被 OOM Killer 干掉。
4.3 目录挂载与权限:uid 1000 这个坎
ES 镜像内部默认用elasticsearch用户运行,这个用户的 uid 是 1000。你挂载宿主目录进去的时候,容器内进程要对这个目录有读写权限,这就要求宿主目录的属主是 uid 1000(或者权限足够宽)。
常见的错误做法是chmod 777一把梭,能用,但不安全也不优雅。正确的做法是:
# 建目录 mkdir -p /data/es/{data,logs,config} # 把属主改成 1000,和容器内用户对齐 chown -R 1000:1000 /data/es # 看一眼 ls -ln /data/es如果宿主机上恰好已经有一个 uid 1000 的账号,你chown 1000:1000之后ls -l会显示成那个用户名,这个不影响使用,uid 对得上就行。
要挂载的目录一般是三个:
| 宿主机路径 | 容器内路径 | 用途 |
|---|---|---|
| /data/es/data | /usr/share/elasticsearch/data | 索引数据,最重要的一个 |
| /data/es/logs | /usr/share/elasticsearch/logs | 日志,排查问题靠它 |
| /data/es/config | /usr/share/elasticsearch/config | 配置覆盖 |
data目录是命根子,绝对不能只放在容器里。容器的可写层在容器删除后就没了,docker rm一下数据全丢。这个错误我见过不止一次,尤其是用docker run --rm习惯了的同学。数据目录一定要挂出来,而且这个目录要纳入日常备份。
配置文件目录挂载有个小坑:如果你把一个空目录挂到/usr/share/elasticsearch/config,容器内原有的默认配置文件会被"遮盖"掉,ES 找不到elasticsearch.yml、jvm.options、log4j2.properties,启动直接失败。正确做法是先把镜像里的默认配置拷出来再改:
# 先起一个临时容器把配置拷出来 docker run --rm --entrypoint cat elasticsearch:7.17.9 \ /usr/share/elasticsearch/config/elasticsearch.yml > /data/es/config/elasticsearch.yml # 其它几个文件同理 docker run --rm --entrypoint cat elasticsearch:7.17.9 \ /usr/share/elasticsearch/config/jvm.options > /data/es/config/jvm.options或者更省事的办法:不挂整个 config 目录,只挂单个文件:
-v /data/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro单文件挂载不会遮盖目录里其他文件,出问题的概率小很多。:ro表示只读,防止容器内进程意外改配置。
4.4 一份可以直接抄的启动脚本
把前面这些串起来,一份完整的启动命令长这样:
docker run -d \ --name es-single \ --restart=always \ -p 9200:9200 \ -p 9300:9300 \ -e "discovery.type=single-node" \ -e "ES_JAVA_OPTS=-Xms4g -Xmx4g" \ -e "TZ=Asia/Shanghai" \ -v /data/es/data:/usr/share/elasticsearch/data \ -v /data/es/logs:/usr/share/elasticsearch/logs \ -v /data/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro \ --ulimit nofile=65536:65536 \ --ulimit memlock=-1:-1 \ -m 8g \ elasticsearch:7.17.9逐条解释一下关键项的理由:
--restart=always:机器重启后容器自动起来。内网机器很少做精细的进程管理,这个参数能省掉很多"重启后服务没了"的投诉。-p 9300:9300:传输端口。单机版其实用不到,但如果后面要接 Kibana 或者别的节点,提前开出来省得改。安全考虑的话,单机版可以只开 9200。-e TZ=Asia/Shanghai:容器默认是 UTC,日志时间对不上本地时间,排查问题时换算很烦。设时区能直接解决。--ulimit两项对应前面讲的文件句柄和内存锁定。-m 8g容器内存上限,前面解释过要和堆大小留出余量。
如果不想每次都敲这么长的命令,可以写成docker-compose.yml,但离线环境下需要先确认 compose 插件已经装好。我个人的偏好是:单节点就用docker run,把命令存成一个start-es.sh脚本放在/opt/es/下面,简单直接;要编排多个服务(ES + Kibana + 采集器)再用 compose。
5. 起不来看日志:离线现场最常卡住的几个环节
5.1 容器反复重启的排查顺序
容器起不来的时候,docker ps看不到它,docker ps -a能看到状态是Restarting或者Exited (1)。这时候按下面的顺序查,效率最高:
第一步,看日志:
docker logs --tail 100 es-single--tail 100只看最后 100 行,ES 的启动日志很长,全刷出来反而不好找重点。想持续观察就加-f。
第二步,判断是配置类错误还是资源类错误。配置类错误日志里通常有明确的关键词,比如max virtual memory areas、unable to lock JVM memory、failed to obtain node locks、Permission denied。资源类错误则可能是 OOM,用dmesg | grep -i oom能看到内核的 OOM Killer 记录。
第三步,如果日志里什么都没有,容器秒退,大概率是入口脚本就挂了。用覆盖入口的方式进容器里看:
docker run --rm -it --entrypoint bash elasticsearch:7.17.9进去之后手动执行bin/elasticsearch,前台跑,报错直接打屏幕上,比翻日志快。
第四步,如果是数据目录的问题,常见的是failed to obtain node locks,意思是上一个进程的锁文件还在。这个锁是data/nodes/0/node.lock,正常情况下进程退出会释放,异常退出会残留。确认没有 ES 进程在跑之后,删掉这个 lock 文件即可。但要注意,删之前一定确认进程真的没了,ps aux | grep elasticsearch看一眼,别把正在写的节点搞坏。
5.2 bootstrap checks 报错与对应关系表
ES 启动时会做一系列自检,叫 bootstrap checks,不通过就退出。这是刻意设计的,宁可起不来也不让你在错误配置下跑生产。常见的几条对照如下:
| 报错关键词 | 含义 | 处理方式 |
|---|---|---|
| max virtual memory areas ... too low | vm.max_map_count 太小 | 宿主机 sysctl 调到 262144 |
| max file descriptors [4096] ... too low | 文件句柄太少 | ulimit nofile 调到 65536 |
| memory locking requested ... cannot lock | 内存锁定失败 | 关 swap,加 ulimit memlock=-1:-1 |
| max number of threads ... too low | 线程数不够 | ulimit nproc 调到 4096 |
| system call filters failed | 系统调用过滤失败 | 较老内核上偶发,检查 seccomp 配置 |
| max size virtual memory ... too low | 虚拟内存限制 | ulimit as 设为 unlimited |
这张表建议直接存进部署文档。离线现场没法上网搜,有个对照表能省掉大量试错时间。
需要单独说一下system call filters failed这条。在某些定制化内核或者容器环境下,seccomp 的默认策略会导致 ES 的系统调用过滤自检失败。有两种处理方向:调内核/运行时配置让它支持,或者在确认环境可控的前提下,通过bootstrap.system_call_filter=false关掉这项检查。后者是有安全折衷的,只建议在完全内网、风险可控的单机测试环境里用,生产环境还是想办法把底层配好。
5.3 8.x 版本的安全配置差异
如果你用的不是 7.17 而是 8.x 系列,有一个大变化必须知道:8.x 默认开启安全功能。具体表现是首次启动会生成证书、要求密码、HTTP 接口走 HTTPS,curl http://localhost:9200直接拒绝。
如果你只是要一个内网单机版的搜索服务,不需要这套安全机制,可以在启动时关掉:
-e "xpack.security.enabled=false" \ -e "xpack.security.http.ssl.enabled=false" \ -e "xpack.security.transport.ssl.enabled=false"这三个环境变量一起给,8.x 的行为就接近 7.x 了。如果确实需要开启安全(比如这个服务要暴露给多个部门用),那就得走完整的证书生成流程,离线环境下没网下载工具,需要用镜像里自带的elasticsearch-certutil来做:
# 生成一个基础证书包 docker exec -it es-single bin/elasticsearch-certutil ca docker exec -it es-single bin/elasticsearch-certutil cert --ca elastic-stack-ca.p12生成的文件在容器内的/usr/share/elasticsearch/config下,记得它挂载出来的是宿主机目录,拷出来备份好。这一步在离线环境里比较繁琐,如果对安全没有硬性要求,我的建议是先用关掉安全的方式把服务跑通,验证业务逻辑,安全加固作为第二阶段单独做。别一上来就卡在证书上,把整个部署节奏拖住。
另外 8.x 还有一个变化:默认不再自带 JDK 之外的兼容层,某些老版本客户端可能连不上。如果你的业务方用的是很老的 Java 客户端,升级前先确认客户端版本。
6. 服务跑起来之后:健康校验、分片设置与日常维护
6.1 三层验证:进程、端口、集群健康
容器状态是Up不代表服务真的可用。我有一次遇到容器一直 Up,但业务方连不上,最后发现是 ES 进程还在初始化,端口监听了但集群状态是 red,所有查询都超时。所以验证要分层做。
第一层,看容器和进程:
docker ps | grep es-single docker exec es-single ps -ef | grep elasticsearch第二层,看端口:
ss -lntp | grep 9200宿主机上能看到 9200 在监听,说明端口映射是通的。
第三层,看接口和集群状态:
# 基础信息,能返回就说明 HTTP 服务正常 curl -s http://localhost:9200 | jq . # 集群健康 curl -s "http://localhost:9200/_cluster/health?pretty" # 节点列表 curl -s "http://localhost:9200/_cat/nodes?v"_cluster/health返回的status有三个值:green 全部正常,yellow 主分片可用但副本未分配,red 有主分片不可用。单机版看到 yellow 是正常的(副本没地方放),看到 red 就说明有分片出问题了,得去看_cat/indices?v找出是哪个索引。
再往下一层,做一次真实的写入和查询验证。这一步很多人省掉,结果接口通了但索引写不进去,业务上线才发现:
# 写一条 curl -XPOST "http://localhost:9200/test-index/_doc" \ -H 'Content-Type: application/json' \ -d '{"msg":"hello offline es","ts":"2024-01-01T00:00:00"}' # 查一下 curl -s "http://localhost:9200/test-index/_search?pretty" # 验证完删掉 curl -XDELETE "http://localhost:9200/test-index"这条链路走通,才算部署完成。
6.2 单机版必须改的副本数
前面提过,默认副本数是 1,单节点上副本分不到地方,集群一直 yellow。改法有两种,看你的使用场景。
如果只是临时验证,可以直接改现有索引:
curl -XPUT "http://localhost:9200/_all/_settings" \ -H 'Content-Type: application/json' \ -d '{"index":{"number_of_replicas":0}}'如果是长期部署,建议建一个索引模板,让以后新建的索引自动是 0 副本:
curl -XPUT "http://localhost:9200/_index_template/single-node-template" \ -H 'Content-Type: application/json' \ -d' { "index_patterns": ["*"], "template": { "settings": { "number_of_replicas": 0, "number_of_shards": 1 } } }'number_of_shards也建议显式设成 1。ES 7.x 之后默认是 1 个主分片(6.x 是 5 个),但显式写出来能避免版本差异带来的意外。单机版分片多了没有好处,每个分片都有固定开销,分片太多反而拖慢查询。
还有一点:分片数一旦创建就不能改(只能 reindex),副本数可以随时改。所以分片数在建模阶段就要想清楚。单机版、数据量在千万级以下的,1 个主分片通常够用;如果预计数据量很大,可以按"单分片 10GB~50GB"的经验值来估。
6.3 备份、日志轮转与升级路径
离线环境里,备份的重要性比在线环境还高,因为出了问题你没法临时下载工具救场。ES 的备份有几种做法,单机版最实用的是文件系统级冷备:
# 停容器,保证数据一致 docker stop es-single # 打包数据目录 tar -czf /backup/es-data-$(date +%Y%m%d).tar.gz -C /data/es data # 起容器 docker start es-single停容器再备份是为了保证数据文件的一致性,热备需要走 snapshot API,配置仓库路径,相对复杂。如果业务不能停,那就得用 snapshot:
# 先在配置里注册一个备份仓库路径 # path.repo: ["/usr/share/elasticsearch/backup"] curl -XPUT "http://localhost:9200/_snapshot/backup_repo" \ -H 'Content-Type: application/json' \ -d '{"type":"fs","settings":{"location":"/usr/share/elasticsearch/backup"}}' # 创建快照 curl -XPUT "http://localhost:9200/_snapshot/backup_repo/snapshot_20240101?wait_for_completion=true"日志方面,ES 自己的日志写在logs/目录下,按天滚动,默认保留 7 天。这个可以通过挂载的log4j2.properties调整。容器层面还有docker logs的输出,默认是 json-file 驱动,会一直增长,需要配一下限制:
# 在 /etc/docker/daemon.json 里加 { "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }改完systemctl restart docker。这个配置对之后新建的容器生效,已有容器需要重建。
升级路径这块,离线环境的升级比在线麻烦,因为你得重复一遍"导出 - 拷贝 - 导入"的流程。ES 的跨大版本升级不能直接换镜像了事,7.x 升 8.x 需要走升级助手检查、重新索引等步骤。我的建议是:单机版尽量锁死版本,把升级当成一个独立项目来做,而不是日常维护的一部分。升级前先用 snapshot 做完整备份,在新版本上用同样的数据验证查询结果一致,再切换流量。
最后分享一个我在内网部署时养成的小习惯:每次部署完,把这次用到的镜像 tag、启动命令、sysctl 配置、目录结构、验证命令,全部写进一个DEPLOY.md放在/opt/es/下。离线环境最怕的是"当时那个人走了,机器还在跑,没人知道它是怎么装起来的"。一份几十行的文档,能省掉后来者几天的摸索。我见过太多内网服务因为缺少这份文档,最后只能推倒重来,数据还得从备份里捞,代价远比写文档大得多。