一、小分片过多带来的烦恼
用过Elasticsearch的朋友都知道,分片(Shard)是个好东西。它让数据可以水平扩展,提高查询吞吐量。但是好东西吃多了也会消化不良,当你的集群里塞满了几千个小分片时,问题就来了。
想象一下这样的场景:你负责维护一个电商平台的搜索服务,每天有数百万商品信息需要索引。最初设计时,你给每个商品类别都创建了独立索引,想着这样查询时可以精准定位。三个月后,系统开始频繁报警,CPU使用率居高不下,节点内存吃紧,查询延迟飙升。
# 查看集群分片状态的API请求示例
GET _cat/shards?v=true&h=index,shard,prirep,state,docs,store,node
# 返回结果示例
index shard prirep state docs store node
products_books 0 p STARTED 12345 45.2mb node-1
products_books 1 p STARTED 12567 46.1mb node-2
products_books 0 r STARTED 12345 45.2mb node-3
...
# 注释:这里可以看到每个分片都很小,但总数可能有上千个
这种情况我见过太多次了。每个分片虽然只有几十MB,但架不住数量多啊!每个分片都要占用:
- 独立的Lucene索引结构
- 文件描述符
- 内存中的各种缓存
- 后台合并任务
二、合并策略的救赎之道
解决这个问题的核心思路很简单:把小分片合并成大分片。Elasticsearch提供了几种武器来帮我们完成这个任务。
2.1 冷热数据分层
这是最优雅的方案。把新数据放在"热"节点上,使用较小的分片保证写入性能;旧数据迁移到"冷"节点,合并成大分片节省资源。
PUT _ilm/policy/hot_warm_policy
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": {
"max_size": "50gb",
"max_age": "7d"
},
"set_priority": {
"priority": 100
}
}
},
"warm": {
"min_age": "30d",
"actions": {
"shrink": {
"number_of_shards": 1
},
"forcemerge": {
"max_num_segments": 1
}
}
}
}
}
}
# 注释:这个ILM策略会在数据30天后自动缩容为1个分片
2.2 手动合并索引
对于已经存在的小索引,我们可以用_shrink API来瘦身。不过要注意几个前提条件:
- 索引必须是只读的
- 必须有足够的磁盘空间
- 目标分片数必须是原分片数的约数
# 首先把索引设为只读
PUT my_small_index/_settings
{
"settings": {
"index.blocks.write": true
}
}
# 然后执行shrink操作
POST my_small_index/_shrink/my_big_index
{
"settings": {
"index.number_of_replicas": 1,
"index.number_of_shards": 2, # 原分片数是4
"index.codec": "best_compression"
},
"aliases": {
"my_search_alias": {}
}
}
# 注释:将4个分片合并为2个,同时启用最佳压缩
三、实战中的经验之谈
在实际操作中,我总结出几个关键点:
3.1 最佳分片大小
根据官方建议和实战经验,分片大小在10GB-50GB之间比较理想。太大会影响查询性能,太小则浪费资源。
# 查看索引大小的API
GET _cat/indices?v=true&h=index,pri.store.size
# 理想情况下应该看到这样的分布
index pri.store.size
logs-2023.01 32gb
logs-2023.02 28gb
...
3.2 合并时机的选择
千万别在业务高峰期做合并!这个操作非常消耗IO和CPU。建议:
- 设置维护窗口期
- 使用ILM自动在低峰期执行
- 监控系统负载,动态调整
# 监控合并进度的方式
GET _tasks?detailed=true&actions=*shrink*
# 返回示例
{
"task": {
"description": "shrink [my_small_index] to [my_big_index]",
"status": {
"total": 100,
"completed": 42,
"percent": 42
}
}
}
四、避坑指南
4.1 副本数的陷阱
合并后记得调整副本数。我见过有人合并了主分片却忘了副本,结果集群状态还是黄的。
# 正确的姿势
PUT my_big_index/_settings
{
"index.number_of_replicas": 2
}
4.2 映射冲突
合并不同索引时,要确保它们的mapping兼容。特别是字段类型必须一致。
# 先检查mapping
GET my_index1/_mapping
GET my_index2/_mapping
# 发现冲突时的处理方式
PUT my_index2/_mapping
{
"properties": {
"price": {
"type": "float" # 与index1保持一致
}
}
}
4.3 别名的重要性
永远通过别名访问索引!这样在合并时可以无缝切换。
# 创建别名
POST _aliases
{
"actions": [
{
"add": {
"index": "my_big_index",
"alias": "products"
}
},
{
"remove": {
"index": "my_small_index",
"alias": "products"
}
}
]
}
五、总结与展望
经过合理合并后,我们的电商平台集群从原来的3000多个小分片缩减到200个左右的大分片。效果立竿见影:
- 查询延迟降低40%
- 节点内存使用下降60%
- 维护成本大幅降低
未来还可以考虑:
- 结合Tiered Storage功能
- 试用新的时序索引模式
- 探索Searchable Snapshots
记住,Elasticsearch就像个挑剔的美食家 - 给它太大块的牛排会噎着,太小块的又嫌麻烦。找到合适的分片大小,才能让它高效运转。
评论