真实场景:凌晨3点,集群挂了
2023年双11大促前一周,我负责的ES集群(3节点,Elasticsearch 7.17.9)在晚上10点突然写入超时。监控显示:CPU 100%,GC次数每分钟200+,索引写入拒绝率30%。原因是业务方临时增加了日志采集量(从5000条/秒涨到15000条/秒),分片分配不均导致节点过热。最终花了4小时扩容、调优才恢复。这次事故让我彻底重构了集群部署方案。
问题:小集群扛不住高并发写入
核心痛点:
- 单节点写入瓶颈:3节点集群,每个节点16核32G,写入吞吐仅8000条/秒
- 查询延迟高:聚合查询平均耗时2.3秒,用户反馈页面加载慢
- GC频繁:老年代GC间隔<30秒,导致节点频繁暂停
- 分片分配不均:索引默认5分片1副本,热点节点磁盘使用率90%
方案对比:3节点 vs 5节点集群
| 维度 | 3节点方案 | 5节点方案(推荐) |
|---|---|---|
| 节点配置 | 16核32G,SSD 500G | 8核16G,SSD 1T(成本更低) |
| 分片策略 | 默认5分片1副本 | 按索引调整:日志类3分片2副本,业务类5分片1副本 |
| JVM堆大小 | 16G(超过物理内存50%) | 8G(物理内存50%) |
| 写入吞吐 | 8000条/秒 | 15000条/秒(提升87.5%) |
| 查询延迟(P99) | 2.3秒 | 0.9秒(降低60.8%) |
| GC频率 | 老年代GC每30秒一次 | 老年代GC每120秒一次 |
| 成本 | 3台高配机器 | 5台中配机器(总成本降低20%) |
结论:5节点方案通过分散负载、优化分片、降低JVM堆大小,显著提升性能且成本更低。
完整实现:从零搭建5节点ES集群
1. 环境准备
操作系统:Ubuntu 22.04 LTS,Elasticsearch 7.17.9,JDK 17.0.6
节点规划:
- node1: 192.168.1.10 (master+data)
- node2: 192.168.1.11 (master+data)
- node3: 192.168.1.12 (master+data)
- node4: 192.168.1.13 (data)
- node5: 192.168.1.14 (data)
2. 安装JDK与ES
# 所有节点执行
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.17.9-linux-x86_64.tar.gz
tar -xzf elasticsearch-7.17.9-linux-x86_64.tar.gz -C /usr/local/
ln -s /usr/local/elasticsearch-7.17.9 /usr/local/elasticsearch
# 创建用户
useradd -m -s /bin/bash elasticsearch
chown -R elasticsearch:elasticsearch /usr/local/elasticsearch
# 配置系统参数
cat >> /etc/security/limits.conf <> /etc/sysctl.conf <
3. 核心配置文件 elasticsearch.yml
# node1 配置示例,其他节点修改 node.name 和 network.host
cluster.name: production-cluster
node.name: node-1
path.data: /data/elasticsearch/data
path.logs: /var/log/elasticsearch
network.host: 192.168.1.10
http.port: 9200
transport.port: 9300
discovery.seed_hosts: ["192.168.1.10:9300", "192.168.1.11:9300", "192.168.1.12:9300", "192.168.1.13:9300", "192.168.1.14:9300"]
cluster.initial_master_nodes: ["node-1", "node-2", "node-3"]
node.master: true
node.data: true
node.ingest: true
bootstrap.memory_lock: true
indices.fielddata.cache.size: 20%
indices.memory.index_buffer_size: 10%
indices.queries.cache.size: 10%
indices.breaker.fielddata.limit: 40%
indices.breaker.request.limit: 40%
indices.breaker.total.limit: 70%
action.auto_create_index: true
xpack.security.enabled: false
xpack.monitoring.enabled: true
4. JVM调优参数 jvm.options
# 关键参数,堆大小设为物理内存50%
-Xms8g
-Xmx8g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=2
-XX:+AlwaysPreTouch
-XX:+DisableExplicitGC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/elasticsearch/heapdump.hprof
5. 启动集群
# 所有节点启动
su - elasticsearch -c "/usr/local/elasticsearch/bin/elasticsearch -d -p /var/run/elasticsearch.pid"
# 检查集群状态
curl -X GET "http://192.168.1.10:9200/_cluster/health?pretty"
# 期望输出:status: green, number_of_nodes: 5
6. 索引分片策略配置
# 创建日志索引模板,3分片2副本
PUT _index_template/logs_template
{
"index_patterns": ["logs-*"],
"template": {
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2,
"refresh_interval": "30s",
"translog.durability": "async",
"translog.sync_interval": "5s",
"index.routing.allocation.total_shards_per_node": 2
},
"mappings": {
"dynamic": false,
"properties": {
"timestamp": {"type": "date"},
"level": {"type": "keyword"},
"message": {"type": "text", "analyzer": "standard"}
}
}
}
}
# 创建业务索引,5分片1副本
PUT /business_index
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1,
"refresh_interval": "10s"
},
"mappings": {
"properties": {
"user_id": {"type": "keyword"},
"action": {"type": "keyword"},
"timestamp": {"type": "date"}
}
}
}
7. 写入性能压测脚本
# 使用 esrally 压测,安装后执行
esrally --pipeline=benchmark-only --target-hosts=192.168.1.10:9200,192.168.1.11:9200,192.168.1.12:9200 --track=geonames --challenge=append-no-conflicts --client-options="timeout:60" --report-file=/tmp/benchmark.json
# 自定义压测脚本(Python)
cat > /tmp/es_benchmark.py <<'EOF'
import requests
import json
import time
from concurrent.futures import ThreadPoolExecutor
urls = ["http://192.168.1.10:9200", "http://192.168.1.11:9200", "http://192.168.1.12:9200"]
index_name = "benchmark_index"
doc = {"field1": "value1", "field2": 123, "timestamp": "2023-01-01T00:00:00"}
def write_doc(i):
try:
r = requests.post(f"{urls[i%3]}/{index_name}/_doc", json=doc, timeout=5)
return r.status_code
except:
return 0
start = time.time()
with ThreadPoolExecutor(max_workers=50) as executor:
results = list(executor.map(write_doc, range(10000)))
end = time.time()
success = sum(1 for r in results if r == 201)
print(f"写入10000条,成功{success}条,耗时{end-start:.2f}秒,吞吐{success/(end-start):.0f}条/秒")
EOF
python3 /tmp/es_benchmark.py
效果数据
| 指标 | 优化前(3节点) | 优化后(5节点) | 提升 |
|---|---|---|---|
| 写入吞吐(条/秒) | 8000 | 15000 | +87.5% |
| 查询延迟P99(秒) | 2.3 | 0.9 | -60.8% |
| 老年代GC间隔(秒) | 30 | 120 | +300% |
| CPU使用率(%) | 85 | 55 | -35.3% |
| 磁盘IO等待(%) | 40 | 15 | -62.5% |
压测环境:esrally geonames track,100并发,持续10分钟。数据来自生产环境实际监控(Prometheus + Grafana)。
避坑指南
以下是我踩过的5个坑,每个都导致过线上事故:
- 坑1:JVM堆大小超过物理内存50% 堆设16G(物理32G),导致GC暂停长达5秒。解决方案:堆设为物理内存50%(16G物理设8G),预留内存给OS缓存。
- 坑2:分片数过多 日志索引设了20分片,每个分片只有几百MB,查询时协调节点开销巨大。解决方案:分片大小控制在10-50GB,日志类索引3分片足够。
- 坑3:未设置total_shards_per_node 导致分片集中到某几个节点,磁盘使用率不均。解决方案:设置index.routing.allocation.total_shards_per_node为2。
- 坑4:translog同步模式为request 每次写入都fsync,写入吞吐下降80%。解决方案:改为async,设置sync_interval为5s。
- 坑5:未开启bootstrap.memory_lock 导致JVM堆被swap,性能骤降。解决方案:设置bootstrap.memory_lock: true,并确保系统配置允许锁内存。
总结
ES集群部署不是简单的多节点堆叠。分片策略、JVM调优、索引模板、系统参数缺一不可。5节点方案在成本和性能上优于3节点高配方案,但需要精细配置。建议生产环境至少5节点,日志类索引3分片2副本,业务类5分片1副本,JVM堆设为物理内存50%。