1. 为什么要走离线这条路:方案选型与整体设计
docker centos7离线安装ElasticSearch单机版,这件事听起来像是一道普通的部署题,真到了现场才发现它是一整套约束条件下的取舍。我第一次接到这类需求是在一个完全断网的内网机房里,服务器能 ping 通的东西只有同一网段内的几台机器,外网、镜像仓库、软件源一概不通。业务方给的需求很朴素:用一台机器跑一个搜索服务,能存数据、能查询、能重启自愈,最好别给运维增加太多负担。于是"离线 + Docker + 单机"这三个词组合到了一起,成了后来反复复用的一套标准动作。
这篇文章想解决的就是这类场景:你手上有 CentOS 7 的物理机或虚拟机,有 root 权限,但没有外网;你需要在这台机器上把 ElasticSearch 跑起来,并且希望它稳定、可维护、出问题能查。内容会从物料准备讲到参数计算、从启动命令讲到分词插件、从报错排查讲到上线前的压测动作,带完整命令和配置文件。适合刚接手内网部署的运维、需要给业务团队交付搜索能力的后端,以及被"断网环境装个 ES"卡住过的同学。读完之后你至少能拿到一份可以直接照着敲的清单,而不是在几十个论坛帖子里拼凑互相矛盾的片段。
1.1 内网环境下的三个硬约束
离线部署和在线部署最大的差别不是"少了 yum install"这么简单,而是你失去了所有的即时纠错能力。在线环境下缺个依赖,一行命令补上就完事;离线环境下缺一个包,可能要等到下一次进机房窗口期才能补,中间业务就得干等着。所以我把这类项目的约束归纳成三条,后面所有设计都是围绕它们展开的。
第一条是物料必须一次性备齐。镜像、插件包、配置文件模板、甚至验证用的脚本,都得在内网外准备好,用移动介质或内网文件服务器带进去。这里最容易漏的是插件版本对齐——ElasticSearch 的插件对版本号极其敏感,主版本、次版本、修订号三处都得一模一样,差一个数字就会在启动时直接抛异常。
第二条是宿主机内核参数必须先调好。容器不是虚拟机,它共享宿主内核,ES 依赖的 mmap 虚拟内存区域数量、文件句柄上限这些东西在容器里改不了,只能在宿主机上动。这个顺序错了,后面所有调试都是白费力气。
第三条是不能指望在线拉取任何东西。包括镜像、JDK、证书、依赖库。ES 官方镜像自带 JDK,这一点帮了大忙,省掉了一个巨大的坑;但如果你还要装 Kibana、装分词器、装监控插件,那就得提前把所有东西打包好。
1.2 为什么选 Docker 而不是直接解压 tar 包
有人会问,既然要离线,为什么不干脆下载一个 tar.gz 解压运行,何必还套一层 Docker?这个问题的答案在于交付物的一致性和回滚成本。
裸装 tar 包的方式,ES 会直接读到宿主机的 JDK、宿主机的时间、宿主机的环境变量。一旦某台机器的 JDK 版本不一样,行为就可能不同。更麻烦的是升级和回滚:tar 包升级要么解压到新目录做软链切换,要么原地覆盖,前者要维护多份目录,后者一旦失败就回不去了。
Docker 方案把运行时整个打进了镜像里,镜像 ID 就是版本标识。升级就是换一个镜像标签,回滚就是把标签换回去,数据卷独立挂载,切来切去数据都不受影响。而且离线环境下docker save和docker load这一对命令,天然就是"打包带走"的最佳载体——一个几百兆的 tar 文件,拷进机房,两条命令就能还原出完整的运行环境。
当然 Docker 方案也有代价:你得理解容器内的用户权限、目录挂载、内存限制这些概念,比裸装多一层心智负担。但从长期维护看,这层负担是值得的,尤其当你需要管理三五台甚至更多的实例时。
1.3 单机版的能力边界与版本选择
先说清楚单机版能做什么、不能做什么,免得后面业务方期待错位。单节点集群能提供完整的索引、查询、聚合能力,数据落在本地磁盘,能承受中小规模的数据量(我这里说的中小规模,大致是千万级文档、几十 GB 到几百 GB 存储这个量级)。它不能提供高可用:节点一挂服务就断,磁盘一坏数据就可能丢。也不能横向扩展:查询压力上来了只能升配置,没有分片分摊的路子。
如果你的场景对可用性有硬要求,那从一开始就该规划多节点,单机版只是过渡或者开发测试环境。我见过太多团队用单机版跑核心业务,某次机房断电之后数据恢复不了,才回头补集群——那时候历史的索引数据已经很难补全了。
版本选择上有几个实际考量。7.x 系列目前仍然是很多内网环境的主流选择,原因是它自带 JDK、不需要额外的安全证书配置就能起单节点、社区资料也最全。8.x 之后默认开启安全认证,第一次启动会生成 CA 和随机密码,离线环境下操作链路明显变长,如果团队里没有熟悉这块的人,容易卡在证书环节。所以我在内网项目里通常建议:能用 7.x 的最后一个小版本就用它,除非有明确的、必须上 8.x 的功能需求。同一套环境里所有组件的版本号要严格统一,ES、分词插件、Kibana,三者版本号对不齐就会出现莫名其妙的连接问题。
2. 离线物料清单与前期准备
物料准备是整个项目里最没有技术含量、却最容易翻车的环节。我踩过一次坑:镜像、插件、配置文件都带了,唯独没带验证脚本里用到的一个jq,结果在内网里对着一堆 JSON 输出靠肉眼看,排查效率低得离谱。从那以后我都会列一张清单,逐项打勾。
2.1 需要提前打包带走的四类物料
第一类是镜像本体。在三方可以联网的机器上把镜像拉下来,用docker save导出成 tar。注意导出前先确认架构一致,如果源机器是 x86_64、目标机器是 ARM,镜像根本跑不起来,这一点在混合架构的团队里出现频率不低。导出时尽量带上版本号命名,别用 latest,latest在离线环境里是个灾难,你永远不知道自己装的是哪个具体版本。
第二类是插件包。中文场景下最常见的是 IK 分词器,直接从插件发布页面下载对应版本的压缩包即可。注意下载的是 zip 包,不要解压后再带过去,或者解压后保持原始目录结构,别把里面的文件层级搞乱。
第三类是配置模板。包括elasticsearch.yml、jvm.options.d下的自定义堆配置、以及一份启动脚本。配置写在介质里带进去,比在内网里手敲靠谱得多。
第四类是验证与运维小工具。我一般会带一两个静态编译的 curl(有些精简系统里没有)、一份备份脚本、一份健康检查脚本。这些东西不占地方,但关键时刻省事。
# 在能联网的机器上执行 docker pull elasticsearch:7.17.10 docker save -o es-7.17.10.tar elasticsearch:7.17.10 # 打成一个包,方便一次拷走 tar -czf es-offline-bundle.tar.gz es-7.17.10.tar \ elasticsearch-analysis-ik-7.17.10.zip \ config-template/ scripts/打包完成后建议算一下校验值,sha256sum记在纸上或者另存一个文本,拷进内网后先核对一遍。移动介质在拷贝过程中损坏文件的情况我遇到过不止一次,一个几百兆的 tar 包坏在最后一个字节,docker load报错时的提示语往往看不出是文件损坏,容易误判成镜像本身有问题。
2.2 宿主机内核参数调整背后的逻辑
这一步是离线安装里最容易被跳过、又最影响成败的环节。ElasticSearch 底层的 Lucene 用 mmap 的方式读取索引段文件,每映射一个段就要占用一个虚拟内存区域。系统默认的vm.max_map_count是 65530,小数据量时够用,索引一旦多起来就会撞上这个上限,进程直接退出,日志里留下一句max virtual memory areas vm.max_map_count [65530] is too low。
262144 这个数字不是随便定的,它是官方给出的推荐下限值,足够支撑较大的分片数量。设置方式有两种:临时生效用sysctl -w,重启就没了;永久生效写进/etc/sysctl.d/下的配置文件。生产环境当然选后者。
cat > /etc/sysctl.d/99-elasticsearch.conf <<'EOF' vm.max_map_count = 262144 vm.swappiness = 1 vm.overcommit_memory = 1 fs.file-max = 655360 EOF sysctl --systemvm.swappiness = 1是尽量让系统别把 ES 的内存换出去,内存被换到磁盘上对搜索性能是毁灭性的。vm.overcommit_memory = 1允许内存超额分配,避免 JVM 申请大块内存时被内核拒绝。这两项不是必选项,但在内存不算宽裕的机器上很有用。
文件句柄方面,要在/etc/security/limits.conf里放开,同时注意 CentOS 7 的 systemd 服务有自己的限制,光改 limits.conf 对以 systemd 管理的 Docker 服务可能不生效,得在 Docker 的 service 配置里也放一份。
cat >> /etc/security/limits.conf <<'EOF' * soft nofile 65536 * hard nofile 65536 * soft memlock unlimited * hard memlock unlimited EOF mkdir -p /etc/systemd/system/docker.service.d cat > /etc/systemd/system/docker.service.d/limits.conf <<'EOF' [Service] LimitNOFILE=65536 LimitMEMLOCK=infinity EOF systemctl daemon-reload && systemctl restart docker注意:改完 limits 之后必须重新登录会话才生效,用
ulimit -n验证时如果还是旧值,先退出 SSH 再连。重启 Docker 会带走所有正在运行的容器,这一步要挑业务低峰做。
2.3 目录规划与权限设计
目录规划看着琐碎,实际上决定了后面运维时查找文件的效率。我的习惯是在/data下按"应用/实例"两级划分,比如/data/es01/下面放config、data、logs、plugins四个子目录。这样一台机器上跑多个实例也好管理,备份的时候直接打包整个实例目录就行。
权限是这块的关键。ES 官方镜像里的进程是以 UID 1000 的用户身份运行的,挂载进去的数据目录如果属主不对,容器启动时会直接报AccessDeniedException然后退出。很多人第一次遇到这个错误会去查权限位,其实问题在属主 ID 上。宿主机上随便一个用户创建的目录,UID 往往不是 1000,容器里那个 1000 用户就对它没有写权限。
mkdir -p /data/es01/{config,data,logs,plugins} chown -R 1000:1000 /data/es01 chmod -R 755 /data/es01如果宿主机上恰好有个业务用户的 UID 也是 1000,那 ES 容器会对这个用户的目录有完全控制权,这在多租户环境下是需要留意的风险点。稳妥的做法是给 ES 单独准备目录,或者干脆换一个非 1000 的 UID,通过镜像的user参数指定——不过这会让配置复杂度上升,单机场景下用默认 1000 就够了。
磁盘方面,建议数据目录单独挂一块盘,别和系统盘混在一起。ES 对磁盘余量有个保护机制,使用率超过 85% 会把相关索引置为只读,超过 95% 会把整个索引块都锁住。系统盘被日志写满导致 ES 停止写入的事故,我在两三个项目里都见过。
3. 镜像导入与容器编排落地
准备工作做完,就可以进机房操作了。这一段的顺序很关键:先导镜像,再拉配置,再启容器,最后验证。每一步都有可以做的检查,别图快跳过去。
3.1 镜像导入与标签确认
tar -xzf es-offline-bundle.tar.gz docker load -i es-7.17.10.tar docker images | grep elasticsearchdocker load输出的内容里会打印加载进来的镜像名和标签,确认一下版本号和你预期的一致。见过一种情况:镜像包里同时有多个标签指向同一个 ID,加载后列表里出现好几行,容易看错。用docker inspect看一下镜像的创建时间和环境变量更稳妥,docker inspect elasticsearch:7.17.10 --format '{{.Created}}'能确认这是不是你打包的那一份。
加载完成后别急着跑,先确认宿主机的 Docker 版本和存储驱动。老版本 Docker 在 overlay2 上对大量小文件的性能表现不一,而 ES 的索引文件恰恰就是海量小文件。CentOS 7 上 Docker 版本如果低于 19.x,建议先升一升,尤其是内核在 3.10 的老机器,容器内 ES 的稳定性会打折扣。
3.2 配置文件怎么写才不容易出事
配置文件我不建议手写,直接从镜像里导出一份完整的 config 目录来改,这样能保证目录里该有的文件一个不少。做法是用docker create创建一个不启动的容器,把 config 目录拷出来,再删掉这个临时容器。
docker create --name es-tmp elasticsearch:7.17.10 docker cp es-tmp:/usr/share/elasticsearch/config /data/es01/ docker rm es-tmp这样做的好处很实在:镜像里的 config 目录除了elasticsearch.yml,还有jvm.options、log4j2.properties等好几个文件。如果你只挂载一个手工创建的目录进去,那些缺失的配置文件会导致启动失败,报错信息还很间接,容易绕远路。
拿到 config 目录后,改两个地方。先是elasticsearch.yml:
cluster.name: es-single-01 node.name: node-01 path.data: /usr/share/elasticsearch/data path.logs: /usr/share/elasticsearch/logs network.host: 0.0.0.0 http.port: 9200 discovery.type: single-node bootstrap.memory_lock: true action.destructive_requires_name: truecluster.name和node.name虽然单机用不上集群,但起个有意义的名字,出问题看日志时好定位。network.host写0.0.0.0是让服务监听所有网卡,这样同网段的其他机器能访问;如果只想本机访问就写127.0.0.1。discovery.type: single-node是单机模式的关键,不写这个,ES 启动时会尝试发现其他节点,然后因为找不到而反复重试,日志里刷一堆警告。bootstrap.memory_lock: true是锁住内存防止被换出,但这需要宿主机放开 memlock 限制,没放开的话启动检查会直接失败。action.destructive_requires_name: true是我个人习惯加的,防止误操作把_all索引一把删掉。
堆内存的配置单独放到jvm.options.d下,别去改主配置文件,这样升级镜像时你的自定义配置还能保留:
cat > /data/es01/config/jvm.options.d/heap.options <<'EOF' -Xms4g -Xmx4g EOF大小怎么定?两条规则:第一,不超过物理内存的 50%;第二,不超过 32GB(超过之后 JVM 会失去指针压缩优化,反而变慢)。一台 16G 的机器,堆开到 6g 到 8g 都合理,剩下的内存留给 Lucene 的页缓存。堆开太大反而不好,因为页缓存少了,读磁盘的次数就多。我一般按"物理内存一半,且封顶 31g"来定,然后压测一遍看 GC 情况再微调。
3.3 启动命令与编排方式对比
启动方式有两种:docker run一条命令带所有参数,或者写一份 docker-compose 文件。单机版两者都行,我倾向于用 compose,因为参数写进文件里可以版本化,下次换机器直接拷文件,不用回忆当时敲了哪些参数。
version: "2.2" services: es01: image: elasticsearch:7.17.10 container_name: es01 restart: always environment: - discovery.type=single-node - bootstrap.memory_lock=true ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 volumes: - /data/es01/config:/usr/share/elasticsearch/config - /data/es01/data:/usr/share/elasticsearch/data - /data/es01/logs:/usr/share/elasticsearch/logs - /data/es01/plugins:/usr/share/elasticsearch/plugins - /etc/localtime:/etc/localtime:ro ports: - "9200:9200" - "9300:9300"这里有个细节值得说:environment里的discovery.type和bootstrap.memory_lock是带点号的变量名。ES 官方镜像的启动脚本会把所有带点号的环境变量转换成-E参数传给 ES 进程,所以这样写是有效的。但如果你在elasticsearch.yml里也写了同样的配置,两边会冲突,启动时报重复设置的错误。选一处写就行,我习惯把静态配置放文件、动态开关放环境变量。
/etc/localtime那个挂载别嫌多余。容器内默认是 UTC 时间,日志时间戳和你本地时间差八小时,排查问题时对着一堆时间线算差,很容易把因果搞反。挂上宿主机的时区文件之后,日志时间就和你date命令看到的对上了。
不用 compose 的话,等价的docker run长这样:
docker run -d --name es01 --restart=always \ --ulimit memlock=-1:-1 --ulimit nofile=65536:65536 \ -p 9200:9200 -p 9300:9300 \ -e "discovery.type=single-node" \ -e "bootstrap.memory_lock=true" \ -e "ES_JAVA_OPTS=-Xms4g -Xmx4g" \ -v /data/es01/config:/usr/share/elasticsearch/config \ -v /data/es01/data:/usr/share/elasticsearch/data \ -v /data/es01/logs:/usr/share/elasticsearch/logs \ -v /etc/localtime:/etc/localtime:ro \ elasticsearch:7.17.10两种方式有个共同的坑:不要只挂载单个配置文件。比如-v /data/es01/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml这种写法,表面看没问题,但宿主机上如果你用 vi 编辑这个文件,vi 默认是"写临时文件再重命名"的保存方式,文件的 inode 变了,而容器里的挂载点还指着旧 inode,容器重启后读到的还是老内容。这个现象很迷惑人——你明明改了配置,重启了容器,就是不生效。挂目录就没这个问题。
3.4 启动后的验证动作清单
容器起来不代表服务可用,得按顺序验证几件事。
# 1. 容器状态 docker ps -a | grep es01 # 2. 启动日志,看有没有 ERROR 或 bootstrap checks failed docker logs -f es01 # 3. 服务是否响应 curl -s http://127.0.0.1:9200 | head -20 # 4. 集群健康 curl -s "http://127.0.0.1:9200/_cluster/health?pretty" # 5. 堆内存和锁内存是否生效 curl -s "http://127.0.0.1:9200/_nodes/stats/process?pretty" | grep -i lock第三步能返回带版本号、集群名的 JSON,说明服务起来了。第四步返回的健康状态如果是yellow,先别慌——单节点环境下索引默认有 1 个副本分片,副本分片没法分配到自己所在的节点上,所以健康值永远达不到green。这是分布式设计带来的正常现象,不是故障。如果你希望它显示绿色,把索引的副本数设为 0:curl -XPUT 'http://127.0.0.1:9200/_all/_settings' -H 'Content-Type: application/json' -d '{"index":{"number_of_replicas":0}}'。也可以建索引用模板固定,避免每次都要手动改。
第五步验证内存锁定是否真的生效。返回结果里mlockall相关字段如果是 true,说明锁住了;如果是 false,说明宿主机 memlock 限制没放开,虽然服务能跑,但性能会有隐患,还是回头把 ulimit 补上。
4. 中文分词与安全认证的离线落地
基础服务跑起来只是第一步。中文业务场景下,默认的分词器会把整句当做一个词,搜索基本不可用,必须装分词插件。另外越权访问的问题在内网环境下同样存在,尤其是同网段里有其他团队的系统时。
4.1 IK 分词器离线安装的两种方式
第一种方式是把插件目录直接拷进挂载点。因为我们挂载了/data/es01/plugins,所以在宿主机上解压插件包到这个目录,然后重启容器就行。
cd /tmp unzip elasticsearch-analysis-ik-7.17.10.zip -d /data/es01/plugins/ik chown -R 1000:1000 /data/es01/plugins docker restart es01 docker logs es01 | grep -i "loaded plugin"注意 zip 包里解压出来的目录结构,正常的包解压后应该是一个ik目录,里面直接是config、jar 包和plugin-descriptor.properties。如果你解压出来是ik/elasticsearch-analysis-ik-7.17.10/...这种多套一层的结构,ES 会识别不到插件。判断方法很简单:确认plugin-descriptor.properties就在plugins/ik/下一层。
第二种方式是用官方插件命令装,适合你没法重启挂载目录的场景:
docker cp /tmp/elasticsearch-analysis-ik-7.17.10.zip es01:/tmp/ docker exec -it es01 bin/elasticsearch-plugin install file:///tmp/elasticsearch-analysis-ik-7.17.10.zip docker restart es01容器内的/tmp通常是可写的,但装完之后插件是落到了容器层,容器重建就没了。所以推荐第一种方式,插件放在挂载卷里,跟数据一起持久化,换镜像也不丢。
装完之后验证一下,用两个分词器对比效果:
curl -XPOST "http://127.0.0.1:9200/_analyze" -H 'Content-Type: application/json' -d ' {"analyzer":"standard","text":"离线部署搜索引擎"}' curl -XPOST "http://127.0.0.1:9200/_analyze" -H 'Content-Type: application/json' -d ' {"analyzer":"ik_max_word","text":"离线部署搜索引擎"}'第一个会把中文切成单个字,第二个能切出"离线"、"部署"、"搜索引擎"这些有意义的词。看到差异就说明插件生效了。顺便说一句,IK 有两种模式:ik_max_word切得最细,索引时用;ik_smart切得粗,查询时用。常见做法是索引用细粒度、查询用粗粒度,召回率和准确率能兼顾。建索引的时候用映射模板把 analyzer 定死,别指望后面再改,改分词器基本等于重建索引。
4.2 内网环境下的访问控制
ES 的 9200 端口是 HTTP 接口,默认完全没有认证。内网不等于安全,同网段里任何一台机器都能删你的索引。7.x 版本提供了基础的账号密码功能,开启方式是在配置里加一项然后重启。
xpack.security.enabled: true重启之后需要初始化内置账号的密码,官方提供一个交互式命令:
docker exec -it es01 bin/elasticsearch-setup-passwords interactive它会依次让你给elasticsearch、kibana、logstash_system等几个内置用户设置密码。命令执行完,所有 HTTP 请求都要带上认证信息:curl -u elastic:你的密码 'http://127.0.0.1:9200/_cluster/health'。
强化密码本身并不复杂,但两个细节别漏。别把密码写进 shell 历史,用-u参数直接写命令里会被记录,可以用curl -u elastic然后交互输入,或者把密码放进.netrc文件并设成 600 权限。另外建立独立的业务账号,别让业务直接用elastic超级用户,按索引粒度给权限,出了事好追溯。
如果内网环境对 TLS 证书有要求,需要开xpack.security.transport.ssl和http.ssl,这一步在离线环境下比较麻烦,需要自己生成 CA 和节点证书,用elasticsearch-certutil命令能完成,但流程较长。我的建议是单机内网环境下先把账号密码开起来,TLS 分阶段做,别为了"一步到位"把整个上线卡住。
提示:开启安全功能之后,任何一次密码修改都要同步更新到使用方。建议在变更记录里单独列一份"ES 凭据变更"条目,谁在用、什么时候换的、换完通知了谁,这些留痕在出故障时能省很多沟通成本。
4.3 Kibana 的离线接入要点
有可视化界面会让排查效率高很多,Kibana 的离线和 ES 是一套流程:能联网的机器拉镜像,docker save,内网docker load,起容器。版本号必须和 ES 完全一致,这是硬性要求,差一个修订号都可能连不上。
配置文件里主要写三行:server.host: "0.0.0.0"、elasticsearch.hosts: ["http://127.0.0.1:9200"]、i18n.locale: "zh-CN"。最后那行是把界面切成中文,纯属个人偏好,但给业务方演示的时候挺加分。如果 ES 开了认证,Kibana 也得配上账号,推荐单独建一个kibana_system角色的用户,别复用elastic。
Kibana 的端口默认 5601,注意和 ES 的 9200 别冲突。容器启动后第一次访问会有一段时间加载,页面转圈是正常的,看日志里出现Server running at才算真正就绪。
5. 常见问题与排查技巧实录
这一段是我这几年攒下来的错误现场记录,按"现象—原因—处理"整理。我建议你在启动前先扫一眼,很多坑提前两分钟就能避开,事后排查可能要两小时。
5.1 容器起不来:从退出码入手
容器启动失败最主要的信息源不是日志内容,而是退出码。docker ps -a里 STATUS 那一列会显示Exited (xxx),不同的数字指向完全不同的问题。
| 退出码 | 典型含义 | 优先排查方向 |
|---|---|---|
| 137 | 被 SIGKILL 杀掉 | 内存不足被 OOM Killer 干掉,或容器内存限制过小 |
| 143 | 收到 SIGTERM | 有人手动停过,或健康检查失败触发重启 |
| 78 | 配置错误 | 配置文件语法错、文件权限不对、变量冲突 |
| 1 | 通用启动异常 | 看日志最后 50 行,通常是 bootstrap checks 失败 |
137 这个退出码最容易被误判。看到它先跑dmesg -T | grep -i "killed process",如果能看到 ES 进程被内核杀掉,那就是内存超了。常见原因有两种:堆开得太大挤爆了宿主机,或者容器自身设了mem_limit而堆大小超过它。解决办法要么调小堆,要么放宽限制。需要注意的是 JVM 除了堆还要占堆外内存,堆大小设成容器限制的 75% 左右比较安全,不是 100%。
78 对应的场景我遇到最多的是权限问题。日志里会出现AccessDeniedException加一个路径,把那个路径的属主改成 1000:1000 基本就好了。还有一种情况是配置文件里用了 YAML 不支持的缩进,或者中文引号,这类问题日志提示得很模糊,检查方法是用docker exec进容器跑一下bin/elasticsearch --help,看 ES 自己解析配置时报什么。
5.2 启动成功但访问不通的三层排查
服务日志显示启动了,浏览器或者 curl 却连不上,这类问题按网络层次往下查,效率最高。
第一层看端口监听。ss -tlnp | grep 9200在宿主机上执行,如果没有任何输出,说明端口没映射出来,检查-p参数或者 compose 里的ports段。如果监听的是127.0.0.1:9200而不是0.0.0.0:9200,那就只能本机访问,问题出在network.host配置上。
第二层看防火墙。CentOS 7 默认开着 firewalld,firewall-cmd --list-ports能看当前放开的端口。内网环境下如果安全策略允许,可以直接把 9200 加到允许列表:
firewall-cmd --permanent --add-port=9200/tcp firewall-cmd --permanent --add-port=9300/tcp firewall-cmd --reload第三层看容器内是否真的有服务在听。docker exec es01 ss -tlnp或docker exec es01 curl -s localhost:9200。有些精简镜像里没装 curl 和 ss,那就从宿主机用docker inspect看容器的 IP,直接访问容器 IP 的 9200 端口。
还有一类隐蔽的问题:ES 启动过程中会做 bootstrap checks,某些检查不通过时它只监听回环地址,对外网卡上根本不 bind。日志里会有一句 "publish address" 相关的警告,看到这类字样就说明卡在检查阶段了,回去看前面提到的vm.max_map_count和文件句柄有没有配到位。
5.3 内存锁定失败的连锁反应
bootstrap.memory_lock: true这个配置有个特点:它是个"要么全成、要么启动失败"的开关。宿主机没放开 memlock 的话,ES 会报Unable to lock JVM Memory,然后直接退出,连降级运行的机会都不给。
如果你不想折腾宿主机的 ulimit,把这项改成 false 也能跑,只是内存可能被换到 swap 上。折中方案是打开这项,同时确认三处配置都到位:/etc/security/limits.conf里 memlock 是 unlimited、Docker 的 systemd 配置里有LimitMEMLOCK=infinity、容器启动参数里有--ulimit memlock=-1:-1。三处缺一处都会失败。
验证是否真的锁住了,别只看启动日志,用接口查更准:
curl -s "http://127.0.0.1:9200/_nodes/stats/process?pretty" | grep -A2 mlockall返回"mlockall": true才算真锁住了。
5.4 磁盘水位的隐形锁
磁盘用量的水位线是个很多人不知道的机制。ES 会在后台定期检查各数据路径的使用率,超过 85% 时把新写入的索引块标记为只读,超过 90% 时尝试把分片迁走,超过 95% 时把所有索引都锁成只读。表现是:服务好好的,查询正常,就是写不进去,报index read-only / allow delete之类的错误。
内网环境里日志和数据放在同一块盘上很常见,日志涨得快的时候特别容易触发。处理方法是先清理空间,然后手动解锁:
curl -XPUT "http://127.0.0.1:9200/_all/_settings" \ -H 'Content-Type: application/json' \ -d '{"index.blocks.read_only_allow_delete": null}'根治的办法是把水位线调高或者把日志目录分离出去。生产环境我更推荐后者:日志单独挂一个小盘,定期清理,数据盘保持 70% 以下的余量。
5.5 上线前必须做的两件事
第一件是备份恢复演练。单机版最怕的就是数据丢了没地方找。ES 提供了 snapshot 机制,可以备份到本地文件系统。离线环境下配置一个共享目录作为仓库,注册仓库、做一次快照、然后删一个测试索引、再从快照恢复回来。整套流程跑通一次,心里才有底。仓库路径必须在path.repo里声明过,不然注册会失败。
# elasticsearch.yml 里加一行 # path.repo: ["/usr/share/elasticsearch/backup"] curl -XPUT "http://127.0.0.1:9200/_snapshot/local_backup" \ -H 'Content-Type: application/json' \ -d '{"type":"fs","settings":{"location":"/usr/share/elasticsearch/backup"}}' curl -XPUT "http://127.0.0.1:9200/_snapshot/local_backup/snap_001?wait_for_completion=true"第二件是重启验证。很多人装完就交付了,从没验证过重启能不能自己起来。docker restart es01之后等三十秒,看服务是否自动恢复、数据是否还在。这一步能暴露出来一堆问题:restart 策略没设、数据目录权限重启后变了、时区挂载在重启后失效等等。我见过最离谱的一次是宿主机的磁盘挂载顺序在重启后变了,数据目录挂到了空盘上,服务起来了但数据全空——好在是演练,不是真事故。
还有个小技巧值得分享:restart: always和restart: unless-stopped的区别值得留意。前者会在 Docker 服务重启后无条件拉起容器,后者如果容器是被手动停的就不会拉起。生产环境用前者更稳妥,避免宿主机重启后 ES 没起来却没人发现。
我在实际使用中发现,离线部署这件事真正的难点从来不是命令本身,而是信息密度——你必须在一个进不了机房的窗口期里,把所有可能用到的东西想清楚、带齐全。所以我现在做这类项目,都会提前写一份"假设会被问到的问题清单",把镜像版本、端口、目录、账号、参数逐条填上,交付的时候一并给出去。这份清单在后续换人维护、扩机器、迁移的时候,价值比部署脚本本身还高。