ES 集群扩建:Hot/Warm 分层架构实践
背景
随着日志数据持续增长,原有 ES 集群存储容量逼近瓶颈。本次扩建目标是:新增节点扩充容量,同时借机引入 Hot/Warm 分层架构,充分利用新机器上 NVMe SSD 与 HDD 并存的硬件特性,并在扩建完成后下线部分旧节点。
一、新节点的硬件特点
新增机器每台配有两种存储介质:
- NVMe SSD × 4
- HDD × 2(其中一块有存量数据不纳入 ES)
NVMe 适合承载高频查询的热数据,HDD 适合存储低频访问的冷数据。问题在于:怎么让 ES 感知到这个差异。
二、存储架构选型
核心问题是:怎么让 ES 感知到介质差异。
方案一:单进程内 path.data 混合 SSD+HDD(不推荐)
最直觉的做法是把所有磁盘路径全部写进一个 ES 实例的 path.data:
/ssd1 ┐
/ssd2 ├─ SSD
/ssd3 │
/ssd4 ┘
/home/disk2 ┐─ HDD(容量约为 SSD 的两倍)
/home/disk3 ┘
这个方案有一个根本性的问题:ES 的 shard 分配策略(DiskThresholdDecider)基于磁盘剩余空间比例,完全感知不到介质差异。
HDD 容量是 SSD 的两倍,剩余空间更大,ES 会优先把约 2/3 的 shard 分配到 HDD——包括最新写入的热数据。
查询时还会出现典型的木桶效应:
一次查询
├── shard A → SSD → 5ms
├── shard B → HDD → 80ms
└── shard C → HDD → 75ms
最终响应 = 80ms(SSD 的价值被完全抵消)
HDD 随机 IO 上限约 100 IOPS,大量聚合扫描时延迟不可控。混合部署等于让最慢的设备拖垮整体性能。
方案二:单机双 ES 实例,Hot/Warm 分层独占介质(推荐)
每台机器运行两个 ES 进程,通过 node role 区分冷热,让每类介质独占一个实例:
单台机器
├── ES 实例 1(Hot 节点,端口 9200/9300)
│ └── path.data: <nvme挂载点1>, <nvme挂载点2>, ... (NVMe,独占)
│
└── ES 实例 2(Warm 节点,端口 9201/9301)
└── path.data: <hdd挂载点1>, <hdd挂载点2> (HDD,独占)
两个实例的 IO 通道天然隔离:NVMe 走 PCIe,HDD 走 SATA,互不竞争。通过 ILM 策略控制数据生命周期:热数据写入 Hot 节点(SSD),超过时间阈值后自动迁移到 Warm 节点(HDD),再经由 Cold 阶段最终删除。
三、实施步骤
总体顺序
Step 1:系统参数配置
Step 2:部署 Hot 节点
Step 3:部署 Warm 节点
Step 4:配置 ILM 策略
Step 5:验证集群状态
Step 6:下线旧节点(逐台)
Step 1:系统参数配置
每台新机器均需执行。
# 系统级参数
cat >> /etc/sysctl.conf << 'EOF'
fs.file-max = 655360
vm.max_map_count = 262144
vm.swappiness = 1
EOF
sysctl -p
# 用户级文件句柄限制
cat >> /etc/security/limits.conf << 'EOF'
elasticsearch soft nofile 65536
elasticsearch hard nofile 65536
elasticsearch soft nproc 65536
elasticsearch hard nproc 65536
EOF
创建数据目录并授权:
# Hot 节点目录(NVMe,按实际挂载点替换)
mkdir -p /nvme1/es-hot /nvme2/es-hot /nvme3/es-hot /nvme4/es-hot
mkdir -p /nvme1/es-hot-logs
chown -R elasticsearch:elasticsearch /nvme{1,2,3,4}/es-hot
chown -R elasticsearch:elasticsearch /nvme1/es-hot-logs
# Warm 节点目录(HDD,按实际挂载点替换)
mkdir -p /hdd1/es-warm /hdd2/es-warm
mkdir -p /hdd1/es-warm-logs
chown -R elasticsearch:elasticsearch /hdd{1,2}/es-warm
chown -R elasticsearch:elasticsearch /hdd1/es-warm-logs
Step 2:部署 Hot 节点
配置文件(/etc/elasticsearch/es-hot/elasticsearch.yml):
cluster.name: your-cluster-name
node.name: ${HOSTNAME}-hot
node.roles: [data_hot, data_content]
path.data:
- /nvme1/es-hot
- /nvme2/es-hot
- /nvme3/es-hot
- /nvme4/es-hot
path.logs: /nvme1/es-hot-logs
network.host: 0.0.0.0
http.port: 9200
transport.port: 9300
discovery.seed_hosts:
- <existing-node-1>:9300
- <existing-node-2>:9300
- <new-node-1>:9300
- <new-node-2>:9300
cluster.initial_master_nodes:
- <master-node-name>
node.attr.data_tier: hot
JVM 配置(/etc/elasticsearch/es-hot/jvm.options):
-Xms<N>g
-Xmx<N>g
heap 不要超过 31G。超过后 JVM 会关闭 CompressedOops,内存利用率反而下降。单机运行两个实例时,建议每个实例分配物理内存的 25% 左右,剩余内存留给 OS page cache,对 ES 查询同样重要。
启动并验证:
ES_PATH_CONF=/etc/elasticsearch/es-hot systemctl start elasticsearch-hot
# 确认节点已加入集群
curl -s 'http://localhost:9200/_cat/nodes?v&h=name,node.role,disk.used_percent'
Step 3:部署 Warm 节点
配置文件(/etc/elasticsearch/es-warm/elasticsearch.yml):
cluster.name: your-cluster-name
node.name: ${HOSTNAME}-warm
node.roles: [data_warm, data_cold]
path.data:
- /hdd1/es-warm
- /hdd2/es-warm
path.logs: /hdd1/es-warm-logs
network.host: 0.0.0.0
http.port: 9201
transport.port: 9301
discovery.seed_hosts:
- <existing-node-1>:9300
- <existing-node-2>:9300
- <new-node-1>:9300
- <new-node-2>:9300
cluster.initial_master_nodes:
- <master-node-name>
node.attr.data_tier: warm
# HDD 性能优化:限制 merge 并发,避免 IO 打满
index.merge.scheduler.max_thread_count: 1
JVM 配置同 Hot 节点,-Xms<N>g / -Xmx<N>g,两个实例保持一致。
启动并验证:
ES_PATH_CONF=/etc/elasticsearch/es-warm systemctl start elasticsearch-warm
curl -s 'http://localhost:9201/_cat/nodes?v&h=name,node.role,disk.used_percent'
Step 4:配置 ILM 策略
PUT _ilm/policy/logs_hot_warm_policy
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_size": "50GB",
"max_age": "1d"
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "7d",
"actions": {
"migrate": {},
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 },
"set_priority": { "priority": 50 }
}
},
"cold": {
"min_age": "30d",
"actions": {
"migrate": {},
"set_priority": { "priority": 0 }
}
},
"delete": {
"min_age": "90d",
"actions": { "delete": {} }
}
}
}
}
各阶段时间阈值(7d / 30d / 90d)根据实际日志保留需求调整。
Step 5:验证集群状态
# 集群整体健康,确认为 green
curl -s 'http://localhost:9200/_cluster/health?pretty'
# 查看所有节点角色与资源使用
curl -s 'http://localhost:9200/_cat/nodes?v&h=name,node.role,disk.used_percent,heap.percent,ram.percent'
# 检查 shard 分布是否符合预期
curl -s 'http://localhost:9200/_cat/shards?v&h=index,shard,prirep,state,node,store' | head -40
# 确认 ILM 策略正在执行
curl -s 'http://localhost:9200/_ilm/explain/logs-*?pretty' | head -60
# 查看各节点磁盘实际分配情况
curl -s 'http://localhost:9200/_cat/allocation?v'
Step 6:下线旧节点(逐台)
必须逐台操作,不可同时下线多台。
下线第一台
# 将该节点的 shard 全部驱逐
curl -X PUT 'http://localhost:9200/_cluster/settings' \
-H 'Content-Type: application/json' -d '{
"transient": {
"cluster.routing.allocation.exclude._name": "<node-name-1>"
}
}'
# 等待该节点 shard 归零(可能需要数十分钟)
watch -n 10 "curl -s 'http://localhost:9200/_cat/shards?v' | grep <node-name-1>"
# 确认集群仍为 green
curl -s 'http://localhost:9200/_cluster/health?pretty' | grep status
# 安全关闭
systemctl stop elasticsearch
下线第二台
第一台确认下线、集群回绿后,再对第二台执行同样操作:
curl -X PUT 'http://localhost:9200/_cluster/settings' \
-H 'Content-Type: application/json' -d '{
"transient": {
"cluster.routing.allocation.exclude._name": "<node-name-2>"
}
}'
watch -n 10 "curl -s 'http://localhost:9200/_cat/shards?v' | grep <node-name-2>"
curl -s 'http://localhost:9200/_cluster/health?pretty' | grep status
systemctl stop elasticsearch
清除排除配置
curl -X PUT 'http://localhost:9200/_cluster/settings' \
-H 'Content-Type: application/json' -d '{
"transient": {
"cluster.routing.allocation.exclude._name": null
}
}'
# 最终确认
curl -s 'http://localhost:9200/_cat/nodes?v&h=name,node.role,disk.used_percent'
curl -s 'http://localhost:9200/_cluster/health?pretty'
四、几点补充说明
关于已有节点的 node role:原有保留节点建议同步修改 node.roles 为 [data_hot, data_content],与新 Hot 节点统一 tier,避免 ILM 迁移逻辑混乱。
关于 heap 上限:31G 是 JVM CompressedOops 的临界点,超过后对象指针从 4 字节膨胀为 8 字节,堆内存利用率下降。单机多实例时更要注意总量控制,建议每个实例不超过物理内存的 25%。
