一、小分片过多带来的烦恼

用过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来瘦身。不过要注意几个前提条件:

  1. 索引必须是只读的
  2. 必须有足够的磁盘空间
  3. 目标分片数必须是原分片数的约数
# 首先把索引设为只读
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%
  • 维护成本大幅降低

未来还可以考虑:

  1. 结合Tiered Storage功能
  2. 试用新的时序索引模式
  3. 探索Searchable Snapshots

记住,Elasticsearch就像个挑剔的美食家 - 给它太大块的牛排会噎着,太小块的又嫌麻烦。找到合适的分片大小,才能让它高效运转。