公司内网有一台 2C4G 的机器准备做日志检索,需要上一套 Elasticsearch,我的第一反应就是 docker-compose。原因很简单:二进制包加 JDK 再加 IK 插件,在离线环境里来回折腾太累,用 docker 把 ES 带起来只需要一个 yaml 文件,以后升级、迁移、克隆环境都方便。这篇文章就写一下我用 docker-compose 部署 Elasticsearch 7.17.10 并离线安装 IK 中文分词器的完整过程,顺带把启动过程中踩过的坑一次性说清楚。
全文默认你已经有 docker 和 docker-compose 环境,Compose 用 v2 语法,也就是 docker compose 而不是 docker-compose。如果你的版本还习惯用连字符,所有命令里的空格换成连字符即可。
1. 方案选型与整体设计
1.1 为什么我最后选了 docker-compose
之前我用过二进制 tar 包部署 ES,最大的痛点是 JDK 版本、系统参数、目录权限、服务脚本这些东西都要自己维护。ES 本身是 Java 应用,虽然 7.x 以后发行包里内置了 OpenJDK,但换了机器还得重新准备环境,麻烦。后来尝试裸跑 docker run,单容器启动是快,但参数一多,命令长到不忍直视,而且每次还得手动加 --restart、--network、--volume,稍微漏一个就能出问题。
docker-compose 的价值不在于它多神秘,而是把容器的启动参数固化成一份声明式配置。端口、目录、环境变量、健康检查、网络全部写在 yaml 里,一条 docker compose up -d 就能复现整个环境。对于团队协作来说,新人拿到仓库后不需要问“ES 当时怎么装的”,一份文件就够了。如果后面想加 Kibana 或者 Logstash,也是在同一个文件里追加 service。
单机部署 ES 用 compose 理由非常充分:配置可版本化、可审计、可回滚。生产环境当然也可以用 Kubernetes 做编排,但多数中小团队、个人开发者、内网工具站用它已经足够。
1.2 版本怎么固定:ES 和 IK 的版本对应关系
版本对应是部署 ES 系列组件最容易栽跟头的地方。ES 的版本号非常严格,大版本、小版本、补丁版本都不能乱配。我选的是 7.17.10,这是 7.x 系列比较后期的稳定版,修复了不少问题,也是目前很多公司还在用的主力版本。我没有直接上 8.x,原因很简单:8.x 默认开启安全认证,集群通信和客户端访问都要带证书和账号密码,对于内网日志检索这种场景,反而增加了不必要的复杂度;7.17 可以关掉 xpack 安全模块直接跑,对快速搭建非常友好。
IK 分词器的版本必须和 ES 严格匹配。GitHub 上 medcl/elasticsearch-analysis-ik 的 release 版本号基本跟随 ES 版本,但注意它不一定每个 ES 补丁版本都跟着发。比如我用的 ES 是 7.17.10,IK 的 release 可能有 7.17.0,这个没关系,只要主次版本一致(都是 7.17),一般就能正常加载。真正不能忍的是把 IK 8.x 的包装到 ES 7.x 上,那启动时直接报 incompatible,插件加载失败,ES 甚至可能起不来。
镜像也是一样,建议固定具体 tag,不要用 latest。镜像仓库里 latest 指向的版本会随时变化,今天跑着没问题,明天重新拉镜像可能就换版本了,这在生产环境是事故隐患。我习惯把所有组件版本都钉死在 yaml 里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 部署配置拆解:目录、JVM 与系统参数
2.1 docker-compose.yml 逐段说明
先贴一份我实际使用的 compose 文件,注释比较详细,后面再逐段拆:
yaml复制version: "3.8"
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.10
container_name: es
restart: unless-stopped
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms1g -Xmx1g
- bootstrap.memory_lock=true
- xpack.security.enabled=false
ports:
- "9200:9200"
volumes:
- ./es-data:/usr/share/elasticsearch/data
- ./es-logs:/usr/share/elasticsearch/logs
- ./ik/elasticsearch-analysis-ik-7.17.0.zip:/tmp/ik.zip
ulimits:
memlock:
soft: -1
hard: -1
healthcheck:
test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health >/dev/null || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 40s
networks:
- esnet
networks:
esnet:
driver: bridge
这里有几个关键点。discovery.type=single-node 必须加,不加的话 ES 启动后会尝试和其他节点发现通信,单节点环境下大概率报 bootstrap checks failed 或者长时间处于 red 状态。ES_JAVA_OPTS 用来控制 JVM 堆大小,ES 官方建议堆内存不要超过物理内存的 50%,所以 4G 内存的机器我给了 1g,留 2g 给操作系统页缓存和其他服务。bootstrap.memory_lock=true 配合 ulimits.memlock 的 -1,意思是禁止 ES 内存交换到 swap,日志检索场景对响应时间敏感,内存换出会把性能拖垮。
端口方面,7.x 以后节点间通信也默认走 9200,9300 不再需要对外暴露,所以我只映射了 9200。如果你确实需要多节点集群,那要单独配置 transport.host 和节点发现,单机场景不用管。
挂载目录需要说明一下。./es-data 是数据目录,./es-logs 是日志目录。这两个目录如果不存在,Docker 会自动创建,但自动创建的目录属主是 root,而 ES 容器内默认以 elasticsearch 用户运行,UID 是 1000,所以大概率会因为权限问题启动失败。这坑我踩过一次,后面排错章节细说。./ik/elasticsearch-analysis-ik-7.17.0.zip 是我提前下载的 IK 插件包,临时挂载到容器里,用于离线安装。
2.2 JVM 内存参数和健康检查
JVM 堆大小是 ES 最重要的一个参数。-Xms1g -Xmx1g 把最小堆和最大堆设成一样,避免运行期堆扩容导致性能抖动。给多大合适?我个人的经验公式是:堆内存不要超过宿主机总内存的一半,剩下的一半留给文件系统缓存,因为 Lucene 重度依赖 OS page cache 来提速,堆分配太狠反而害了它。比如我有 4G,就给 ES 1g 到 1.5g,保守一点给了 1g。如果机器是 8G,可以给 2g 到 3g,但前提是容器分配的内存也要有对应上限,否则容器可能先把宿主机内存打满。
健康检查这里我给的是最简单的 HTTP 探测,只要 /_cluster/health 能返回 200 就算存活。注意这个检查不能替代业务层面的监控,它只能告诉你 ES 进程活着,不代表所有分片都是绿色。如果想让检查更严格,可以把测试命令改成检查返回体里的 status 字段,例如 grep "green"。但我的建议是本地部署阶段用简单版本就够了,生产环境再配合 Prometheus 那套指标去监控。
2.3 宿主机准备:最大虚拟内存和目录权限
在 docker compose up 之前,有两件系统层面的准备工作必须做,否则 ES 启动必挂。
第一件是设置 vm.max_map_count。ES 底层使用 Lucene,需要大量内存映射区域,Linux 默认值 65530 不够用,至少需要 262144。不改的话启动会报:
text复制bootstrap check failure [1] of [2]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
解决方法是在宿主机执行:
bash复制sudo sysctl -w vm.max_map_count=262144
这只对当前内核生效,重启后会失效,所以还要写进 /etc/sysctl.conf:
bash复制echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf
第二件是目录属主。ES 镜像里默认用户是 elasticsearch,UID 1000。如果宿主机上数据目录的属主不是它,进程就没法写入。我通常在启动前手动建目录并调整权限:
bash复制mkdir -p ./es-data ./es-logs ./ik
chown -R 1000:1000 ./es-data ./es-logs
注意不要对 ./ik 执行 chown,因为那个目录是宿主机下载文件用的,容器只需要读取挂载进去的 zip,不需要写入。
3. 离线安装 IK 分词器:两种落地方式
3.1 准备工作:把 IK 的安装包拿到手
离线环境最麻烦的就是“怎么把包搞进去”。IK 的 zip 包在 GitHub Releases 页面下载,文件名一般是 elasticsearch-analysis-ik-<version>.zip。你得找一台能上网的机器,下载对应版本的 zip,然后通过 U 盘、内网传输工具或者运维通道拷进目标服务器。
这里有一个细节:如果你连 Docker Hub 也访问不了,镜像也需要提前导进去。在有网机器上执行:
bash复制docker pull docker.elastic.co/elasticsearch/elasticsearch:7.17.10
docker save docker.elastic.co/elasticsearch/elasticsearch:7.17.10 -o es-7.17.10.tar
把 es-7.17.10.tar 拷到目标机器后执行:
bash复制docker load -i es-7.17.10.tar
IK 的包可以和镜像一起拷过去,整个离线部署就齐了。
3.2 方式 A:临时挂载 zip 后进容器安装
我推荐优先用这种方式,因为不需要额外构建镜像,改动最小。假设你已经把 IK 的 zip 放到宿主机 ./ik 目录,并且在 compose 文件里加了挂载:
yaml复制- ./ik/elasticsearch-analysis-ik-7.17.0.zip:/tmp/ik.zip
先把服务启动起来:
bash复制docker compose up -d
等容器进入运行状态后,进入容器安装插件:
bash复制docker compose exec elasticsearch bin/elasticsearch-plugin install -b file:///tmp/ik.zip
这里的 -b 参数非常关键,它表示 batch mode,跳过安装过程中的交互式确认。如果不加,es 会弹一个确认提示等待输入 y/N,在脚本里执行的时候直接就卡住了。
安装完成后,重启容器让插件真正加载:
bash复制docker compose restart elasticsearch
重启后确认插件已经注册:
bash复制docker compose exec elasticsearch bin/elasticsearch-plugin list
正常会输出 analysis-ik。这样离线安装就完成了。
3.3 方式 B:Dockerfile 自定义镜像
方式 A 虽然方便,但有一个问题:容器重建后插件会丢失。如果哪天 docker compose down 后重新 up,你得重新进容器安装一遍。对于需要反复重建环境的场景,不如直接用 Dockerfile 把插件打进镜像。
在项目目录下建一个 Dockerfile:
dockerfile复制FROM docker.elastic.co/elasticsearch/elasticsearch:7.17.10
COPY ./ik/elasticsearch-analysis-ik-7.17.0.zip /tmp/ik.zip
RUN bin/elasticsearch-plugin install -b file:///tmp/ik.zip
然后在 compose 文件里,把 image 换成 build: .:
yaml复制services:
elasticsearch:
build: .
# image: docker.elastic.co/elasticsearch/elasticsearch:7.17.10
首次启动执行:
bash复制docker compose up -d --build
构建过程中,RUN 会把插件安装到镜像层里,之后每次启动都自带 IK。这种方式适合生产环境容器化部署,也适合你要分发给其他团队的情况。
我实际项目里的偏好是:快速验证用方式 A,运维规范化用方式 B。
3.4 验证插件加载和自定义词典扩展
安装好 IK 之后,可以用 _analyze 接口快速验证分词器是否生效:
bash复制curl -X POST "localhost:9200/_analyze?pretty" -H 'Content-Type: application/json' -d'
{
"analyzer": "ik_max_word",
"text": "中华人民共和国国歌"
}'
如果 IK 没装好,这里会直接报错提示找不到 analyzer。装好的话,会返回一组分词结果,ik_max_word 会尽量细分出更多词项,比如“中华人民共和国”“中华人民”“中华”“华人”“人民共和国”等。
IK 还支持自定义词典,适合把公司名、产品名、人名等专有名词加进词库。插件的数据目录在容器内是 /usr/share/elasticsearch/config/analysis-ik/,里面有几个 .dic 文件。你可以把自定义词放到宿主机,再挂载进去实现热更新。
4. 启动服务与分词检索验证
4.1 启动容器并确认健康状态
准备工作做完后,正式启动:
bash复制docker compose up -d
启动过程中要观察日志:
bash复制docker compose logs -f elasticsearch
看到类似这种日志说明启动成功:
text复制[INFO ][o.e.n.Node] [xxx] started
[INFO ][o.e.g.GatewayService] [xxx] recovered [0] indices into cluster_state
然后确认 HTTP 接口:
bash复制curl -s "localhost:9200/_cluster/health?pretty"
返回体里 status 是 green 或者 yellow 都算正常。单节点没有副本分片时,yellow 是因为主分片存在但副本未分配,不是故障。如果你比较介意,可以设置 number_of_replicas 为 0,或者直接忽略,因为单节点本来就没有副本可分配。
4.2 创建索引、写入数据、跑一次中文搜索
ES 安装完成不等于能开箱即用,你还需要建索引、指定分词器,不然默认的 standard 分词器会把中文按字切分,搜索体验非常差。
先创建一个带 IK 分词器的索引:
bash复制curl -X PUT "localhost:9200/test_index" -H 'Content-Type: application/json' -d'
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
}
}'
这里解释一下:analyzer 是写入文档时使用的分词器,用 ik_max_word 做最细粒度切分,索引里存尽可能多的词项;search_analyzer 是查询时使用的分词器,用 ik_smart 做粗粒度切分,提高查准率。这个组合是中文搜索最常见的配置。
写入一条测试数据:
bash复制curl -X PUT "localhost:9200/test_index/_doc/1" -H 'Content-Type: application/json' -d'
{
"title": "中华人民共和国国歌在首都北京奏响"
}'
然后搜索:
bash复制curl -X POST "localhost:9200/test_index/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query": {
"match": {
"title": "首都"
}
}
}'
如果 IK 分词器生效,这条查询能正常命中。如果用默认分词器,搜“首都”大概率只能匹配到包含“首”或“都”的文档,语义完全不对。
4.3 ik_max_word 与 ik_smart 怎么选
这两个模式的取舍是 IK 使用里最核心的问题。简单说,ik_max_word 倾向于“切碎”,ik_smart 倾向于“切准”。
用“中华人民共和国国歌”做例子:ik_max_word 可能切出“中华人民共和国”“中华人民”“中华”“华人”“人民共和国”“人民”“共和”“国歌”等;ik_smart 通常只切出“中华人民共和国”“国歌”。从索引角度看,ik_max_word 会让倒排索引更大,但召回率更高;ik_smart 索引更小,精度更高。
我的建议是索引和查询分离:写入时用 ik_max_word,查询时用 ik_smart。这也是官方文档和社区的主流做法。如果你对某类业务有很强的分类意图,比如商品名搜索,可以两个都用,然后根据评分结果做二次排序。
5. 常见问题与排查技巧实录
5.1 启动报错:max virtual memory areas 不足
这个问题在 2.3 节提到了,但它是新手最常见的错误,我再展开说一下。ES 的 Lucene 需要大量内存映射区域,内核参数 vm.max_map_count 默认值不够。启动日志会明确告诉你:
text复制bootstrap check failure [1] of [2]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
不要试图在容器内部改这个参数,容器没法改宿主机的内核参数。只能在宿主机执行 sysctl -w vm.max_map_count=262144,并且写入 sysctl.conf 持久化。如果是通过 systemd 管理的机器,还可以用 sysctl.d 下的配置文件。
5.2 数据目录权限导致 AccessDenied
如果你没有提前 chown 1000:1000 数据目录,启动日志会报类似下面的错误:
text复制java.nio.file.AccessDeniedException: /usr/share/elasticsearch/data
这是因为 Docker 在创建宿主机空目录时,属主是 root,而容器内进程是 UID 1000 的 elasticsearch 用户。解决办法很简单,启动前先 chown -R 1000:1000 ./es-data ./es-logs。
如果已经启动过且目录被创建了,也没关系,先把容器停掉,执行 chown,再启动就行。这里有个小技巧:如果你记不住 UID,可以进容器查一下 id elasticsearch,然后在宿主机用数字 UID 做 chown,不要按用户名 chown,因为宿主机不一定有这个用户。
5.3 “elasticsearch health check failed”刷屏
有时候 Spring Boot 或者其他客户端会一直报错:
text复制e.elasticsearchrestclienthealthindicator : elasticsearch health check failed
看到这个日志先别慌。最常见的原因是 ES 还没有完全启动完成,或者是客户端用错了端口。ES 从进程启动到真正对外提供服务中间有分片恢复等过程,如果客户端在 start_period 内探测失败,就会反复报这个错误。解决办法是等待一段时间,或者用 docker compose logs -f elasticsearch 确认 ES 真的起来了。另一种情况是客户端配置连接了 9300,但 9300 在容器环境没有对外开放,导致探测失败,这个要把端口改成 9200。
5.4 IK 插件版本不兼容
插件安装成功但不代表万事大吉。如果 IK 版本和 ES 版本不匹配,启动时会报:
text复制Plugin [analysis-ik] is incompatible with version [7.17.10], was designed for version [8.2.0]
ES 对插件版本校验非常严格,不会容忍主次版本不一致。解决方法是下载正确版本的 IK。GitHub 上 medcl/elasticsearch-analysis-ik 的 release 里,一般会有明确标注,比如 v7.17.0 的包对应 ES 7.17.x。下载时多看一眼命名,别贪图顺手拿最新版。
5.5 内存不足导致容器被 kill
ES 是吃内存大户。如果你没有设置 ES_JAVA_OPTS,默认堆大小是 1g,在 2G 内存的小机器上,加上 Lucene 的文件缓存,很容易把宿主机内存打满,容器被系统的 OOM killer 直接杀掉。表现就是 docker ps 看不到容器,docker logs 可能没有明确报错,但 docker inspect 里会看到 OOMKilled: true。
解决办法:
- 明确设置
ES_JAVA_OPTS=-Xms512m -Xmx512m调整堆大小。 - 给容器加
mem_limit限制,防止 ES 无限制吃内存。比如mem_limit: 2g。
生产环境如果条件允许,还是建议宿主机内存不要低于 4G。
5.6 Windows 上部署的几个注意点
在内网测试环境或者开发机上,很多人用的是 Windows + Docker Desktop。Windows 下部署 ES 有一些细节不太一样。
首先是文件路径,yaml 里的相对路径和容器内路径都不变,但 PowerShell 执行 chown 不方便,Windows 文件系统没有 POSIX 权限,反而很少遇到 AccessDenied 的问题,因为 Docker Desktop 的虚拟文件系统层做了一层转换。
其次是在 Docker Desktop 里挂载速度比 Linux 慢,ES 数据量大的话性能会打折。开发测试无所谓,正式生产不建议在 Windows 上跑 ES。
命令方面,PowerShell 里用 curl 需要留意,它是 Invoke-WebRequest 的别名,语法不一样。建议直接用 curl.exe,或者在容器内部测试接口。ES 官方镜像自带 curl,所以最省事的做法是 docker compose exec elasticsearch curl -s localhost:9200。
6. 最后一点生产建议
整套环境跑通之后,我一般还会做几件收尾的事。一是把 compose 文件里的 container_name 去掉,避免多个项目冲突;二是把 IK 插件包连同镜像文件一起归档到内网的制品仓库,下次部署不用再到处找。三是如果在公司环境共享使用,建议在 ES 前面加一层权限校验,不要裸奔在公网,否则很容易被扫描器盯上。
另外,ES 的监控不能省。docker-compose 能把 ES 拉起来,但运行期的健康状态还是要依靠外部监控。我常用的组合是用 Prometheus 抓 ES 指标,Grafana 出面板,compose 里再加一个 exporter 的服务就行。这些属于后续扩展,先把 ES 稳定跑起来再说。
从零开始到中文搜索跑通,整个过程最耗时间的不是起服务,而是确认版本匹配和目录权限。只要把这几个关键点想清楚,docker-compose 部署 ES 加离线 IK 分词器就是一个非常舒服的体验。希望这篇记录能帮你少踩几个坑。
