跳到主要内容

安装与快速上手

环境准备​

本文的示例环境是 Ubuntu 22.04 + Docker + Elasticsearch 8.x。ES 是 Java 应用,对系统参数有硬性要求,不先调参数就启动,第一次踩的坑基本都出在这。

最低可用配置(仅学习/测试):

资源建议值说明
CPU2 核低于 2 核启动就慢,聚合查询会很难受
内存4GB其中至少 1GB 给 JVM 堆,剩下的留给文件缓存
磁盘40GB SSD机械盘能跑,但 merge 期间延迟会很明显
文件系统ext4 / xfs别用 NFS,也别用 CIFS 挂载数据目录

生产环境把内存和磁盘往上加,具体容量规划在第 4 篇算。

三个必须调的系统参数​

第一,vm.max_map_count。ES 用 mmap 方式读取 Lucene 段文件,每个段都要占一个内存映射区,默认的 65530 个小得可怜,段一多就报错。这是官方文档明确要求的前提条件:

# 临时生效
sudo sysctl -w vm.max_map_count=262144

# 永久生效
echo "vm.max_map_count=262144" | sudo tee /etc/sysctl.d/99-elasticsearch.conf
sudo sysctl --system

验证一下:

sysctl vm.max_map_count

第二,文件描述符和锁内存。ES 会打开大量段文件和网络连接,同时要能把堆内存锁在物理内存里(bootstrap.memory_lock),防止被换出:

# /etc/security/limits.conf 追加(需要重新登录生效)
elastic soft nofile 65535
elastic hard nofile 65535
elastic soft memlock unlimited
elastic hard memlock unlimited

用 Docker 部署时,这类限制通过 --ulimit 传给容器,不必改宿主机配置。

第三,关闭 swap。堆被换到磁盘上,GC 时间会从毫秒级涨到秒级,集群直接假死:

sudo swapoff -a
# 注释掉 /etc/fstab 里的 swap 行,或者
sudo sed -i '/ swap / s/^/#/' /etc/fstab
这三个参数不是「优化」而是「前提」

vm.max_map_count 不够会直接启动失败或运行中抛 max virtual memory areas vm.max_map_count [65530] is too low;swap 没关不会立刻报错,但会在压力大时表现为莫名其妙的长时间卡顿。上线前逐条确认,别等出事再补。

Docker 起一个单节点​

单节点模式最简单,一条 docker run 就够。要点有四个:单节点发现模式、显式指定堆内存、数据目录挂出来、权限对得上。

# 1. 准备数据目录(重要:容器内 elastic 用户的 UID 是 1000)
sudo mkdir -p /data/es/data
sudo chown -R 1000:1000 /data/es

# 2. 启动
docker run -d \
--name es01 \
--restart unless-stopped \
-p 9200:9200 \
-p 9300:9300 \
-e "discovery.type=single-node" \
-e "ES_JAVA_OPTS=-Xms1g -Xmx1g" \
-e "xpack.security.enabled=false" \
-v /data/es/data:/usr/share/elasticsearch/data \
--ulimit memlock=-1:-1 \
--ulimit nofile=65535:65535 \
docker.elastic.co/elasticsearch/elasticsearch:8.15.3

逐个参数解释:

  • discovery.type=single-node:单节点发现模式,跳过 master 选举和集群引导。只在单节点测试时用,多节点集群里配了它,节点永远组不成集群。
  • ES_JAVA_OPTS=-Xms1g -Xmx1g:JVM 堆的初始值和最大值。两者必须相等,否则堆在运行中反复伸缩,GC 抖动会很明显。不设置的话 8.x 默认按容器内存的一半自动算,小机器上容易把宿主机吃满。
  • -v /data/es/data:...:数据目录。不挂出来,容器一删数据就没了。
  • --ulimit memlock=-1:-1:允许锁定内存,配合 bootstrap.memory_lock 使用;nofile 给足文件描述符。
  • 端口 9200 是 REST API(我们日常打交道的),9300 是节点间传输端口(集群内部通信)。

启动后看日志确认:

docker logs -f es01

看到这几行就算起来了:

"message": "Node name: [es01]", "ecs.version": "1.2.0"
"message": "Publish address: [172.17.0.2:9300]"
"message": "started", "component": "o.e.n.Node"
"message": "Elasticsearch 8.15.3 started"

再验证一次系统参数(容器内的视角):

docker exec es01 sysctl vm.max_map_count
关闭安全认证只限本地测试

上面的 xpack.security.enabled=false 关掉了 ES 8.x 默认开启的账号密码 + TLS。8.x 第一次启动会自动生成 elastic 用户密码、配置 HTTPS 证书,那是生产默认该有的姿态。关掉认证等于把 9200 端口完全裸奔:任何人能读能写能删索引。

只在本机学习时关;哪怕是内网测试环境,也建议保留认证,或者至少用防火墙限制来源 IP。生产环境按官方安全文档重新开启认证、生成证书并设置密码;本地想恢复默认的安全配置,最省事是清空数据目录重启,初始 elastic 密码会打印在首次启动日志里(搜索 Password for the elastic user)。

如果是长期维护的环境,用 compose 管起来更省事:

services:
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:8.15.3
container_name: es01
restart: unless-stopped
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms1g -Xmx1g
- xpack.security.enabled=false
ports:
- "9200:9200"
volumes:
- /data/es/data:/usr/share/elasticsearch/data
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65535
hard: 65535

REST API 第一课​

ES 的交互方式非常统一:HTTP 方法 + 路径 + JSON body。方法语义固定——GET 查、PUT 建、POST 写、DELETE 删。下面这组命令覆盖了日常 80% 的基本操作,全部可以直接复制执行。

看看集群长什么样​

# 根路径:版本、集群名、节点名、Lucene 版本
curl -s "http://localhost:9200/?pretty"
{
"name": "es01",
"cluster_name": "docker-cluster",
"version": {
"number": "8.15.3",
"build_flavor": "default",
"lucene_version": "9.11.1"
},
"tagline": "You Know, for Search"
}
# 集群健康
curl -s "http://localhost:9200/_cluster/health?pretty"

# 节点列表:看版本、堆内存、负载
curl -s "http://localhost:9200/_cat/nodes?v&h=name,ip,heap.percent,ram.percent,cpu,load_1m,master"

单节点 + 1 副本的索引,健康状态必然是 yellow——副本没地方放。这是正常现象,不是故障,第 6 篇会讲怎么判断。

建索引​

curl -X PUT "http://localhost:9200/books" \
-H 'Content-Type: application/json' \
-d '
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"title": { "type": "text" },
"author": { "type": "keyword" },
"price": { "type": "double" },
"publish_date": { "type": "date" },
"tags": { "type": "keyword" }
}
}
}
'

单节点测试把副本设成 0,健康状态就是 green,看着舒服。mappings 里显式声明字段类型,比让 ES 自动猜要靠得住得多。

写文档​

# 指定 _id 写入(幂等,重复执行是覆盖)
curl -X PUT "http://localhost:9200/books/_doc/1?refresh=true" \
-H 'Content-Type: application/json' \
-d '
{
"title": "Elasticsearch 权威指南",
"author": "Zachary Tong",
"price": 59.9,
"publish_date": "2024-05-01",
"tags": ["search", "es"]
}
'

# 不指定 _id,让 ES 生成一个随机 ID
curl -X POST "http://localhost:9200/books/_doc?refresh=true" \
-H 'Content-Type: application/json' \
-d '
{
"title": "深入理解 Elasticsearch",
"author": "张三",
"price": 79.0,
"publish_date": "2025-03-15",
"tags": ["es", "ops"]
}
'

返回里的 result 字段是 created 或 updated,_version 每次写递增,_shards 里能看到成功写了几个分片。

refresh=true 是干什么的

ES 是近实时系统,写完的数据默认要等下一次 refresh(约 1 秒)才能被搜到。命令行里加 ?refresh=true 可以强制立即刷新,方便边写边查。但它是测试用的手段:每次强制刷新都会生成新段,批量写入时用等于自残。业务代码写完立刻要读,正确做法是前端接受 1 秒延迟,或者用 GET /_doc/1 按 ID 实时读取(按 ID 读不走 refresh)。

查一下​

# 按 ID 精确读(实时,不受 refresh 影响)
curl -s "http://localhost:9200/books/_doc/1?pretty"

# 搜索全部
curl -s "http://localhost:9200/books/_search?pretty"

# match 查询:会走分词,能搜到包含关键词的文档
curl -s "http://localhost:9200/books/_search?pretty" \
-H 'Content-Type: application/json' \
-d '
{
"query": {
"match": { "title": "Elasticsearch" }
},
"size": 5
}
'

# term 查询:不分词,精确匹配 keyword 字段
curl -s "http://localhost:9200/books/_search?pretty" \
-H 'Content-Type: application/json' \
-d '
{
"query": {
"term": { "author": "张三" }
}
}
'

搜索结果要看三个地方:hits.total.value 是命中文档数,hits.max_score 是最高相关性得分,每条 _source 是原始文档(所以别把大字段塞进 ES,_source 也是要存盘的)。match 对 text 字段分词后匹配、按相关性打分;term 对 keyword 字段原样匹配、不分词——这两个查询写反了,结果就是「明明有数据却搜不到」。

改和删​

# 局部更新:只改 price 字段
curl -X POST "http://localhost:9200/books/_update/1?refresh=true" \
-H 'Content-Type: application/json' \
-d '
{
"doc": { "price": 49.9 }
}
'

# 删除文档
curl -X DELETE "http://localhost:9200/books/_doc/1"

# 删除索引(数据全没,慎用)
curl -X DELETE "http://localhost:9200/books"

_update 在底层并不是「原地改一个字段」,而是取出 _source、合并、写一条新文档、把旧的标记删除。所以更新频繁的索引,磁盘占用和 merge 压力都会明显高于只追加的索引。

几个顺手的 _cat 接口​

curl -s "http://localhost:9200/_cat/indices?v&s=store.size:desc"
curl -s "http://localhost:9200/_cat/shards?v"
curl -s "http://localhost:9200/_cat/health?v"

_cat 系列是纯文本表格,?v 带表头,?h= 选列,?s= 排序,特别适合终端里快速扫一眼。完整清单见附录的《ES 查询与运维命令速查》。

Kibana 一句带过​

Kibana 是 Elastic 官方的可视化配套,最常用的功能是 Dev Tools——一个带语法高亮和自动补全的请求控制台,比在终端里敲 curl 舒服得多。版本必须和 ES 严格一致,起一个容器接上就行:

docker run -d --name kibana \
-p 5601:5601 \
--add-host=host.docker.internal:host-gateway \
-e "ELASTICSEARCH_HOSTS=http://host.docker.internal:9200" \
docker.elastic.co/kibana/kibana:8.15.3

Linux 上容器里的 host.docker.internal 不会自动解析,所以要加 --add-host 把它指向宿主机网关;macOS / Windows 的 Docker Desktop 自带这个域名,可以省略。

浏览器打开 5601,进入 Dev Tools 就能写请求了。本文所有命令都能在里面直接跑(省掉 curl 和 URL 前缀)。集群监控、日志分析、仪表盘这些能力是 Kibana 的主场,本系列不展开。

常见问题​

启动就退出,日志报 max virtual memory areas​

前面没调 vm.max_map_count。宿主机调完记得重启容器;容器内不能改这个值(它是内核参数,和宿主机共享)。

启动报 permission denied,路径是 data 目录​

挂载目录的属主不是 UID 1000。容器里的 ES 以 elastic 用户(UID 1000)运行,宿主机目录如果属于 root,写不进去:

sudo chown -R 1000:1000 /data/es

容器被 OOM Killer 干掉,退出码 137​

要么堆设得太大(宿主机内存不够),要么没限制堆、ES 把内存吃满。记住公式:堆不超过物理内存的 50%,且不超过 31GB(超过 31GB 会失去指针压缩,堆越大反而越慢)。

curl 返回 connection refused​

先看容器状态 docker ps -a,再 docker logs es01 看启动日志。常见原因:内存不足启动失败、端口被占用、vm.max_map_count 不满足。ES 启动比一般中间件慢,8.x 冷启动 20-40 秒很正常,别急着判定失败。

小结​

  • 三个系统前提:vm.max_map_count=262144、文件描述符/锁内存给足、关闭 swap。这是启动条件,不是性能优化。
  • Docker 单节点四要素:discovery.type=single-node、-Xms 等于 -Xmx、数据目录挂出来、目录属主 UID 1000。
  • xpack.security.enabled=false 只配出现在本地测试里,8.x 默认的认证和 TLS 是生产底线。
  • REST API 三板斧:建索引定 Mapping、写文档带 _id、搜索分 match 和 term;写完想立刻搜到用 refresh=true,但别在批量写入里用。
  • 单节点 + 1 副本必然是 yellow,这是数学问题不是故障;把副本设为 0 就是 green。

环境跑通了,下一篇进入正题:Mapping 怎么设计、查询 DSL 怎么写、聚合怎么做,以及批量写入的正确姿势。