4、集群架构基础:主从架构、对等架构、分片与副本的概念、一致性哈希
聊到搜索引擎的集群架构,我脑子里第一个蹦出来的场景,是多年前一次线上事故。
那天凌晨,索引集群的一个节点突然挂了。整个搜索服务直接瘫痪了半小时。为什么?因为当时我们用的是单主架构,主节点一倒,整个集群就没了主心骨。嗯,从那以后,我对集群架构的敬畏心就上来了。
今天咱们就把集群架构的四个核心概念掰开揉碎。说白了,就是解决两个问题:数据怎么放,请求怎么分。
4.1 主从架构:一主多从,读写分离
主从架构,你想想看,就像一个编辑部。主编(Master)负责定稿,编辑们(Slave)负责分发和传阅。
在搜索引擎里,Master节点负责写操作——索引的构建、更新、删除。它把数据变更记录成操作日志(比如MySQL的binlog,或者ES的translog)。Slave节点只负责读,从Master同步数据。
核心特点:
- 写操作单点:所有写请求必须经过Master。好处是避免了写冲突,坏处是Master压力大。
- 读操作水平扩展:Slave可以加很多台,扛住高并发查询。
- 数据同步延迟:Slave从Master拉数据,总有那么几毫秒到几秒的延迟。
我在项目中遇到过一个问题:用户刚发布了一篇文章,立刻搜索,结果搜不到。原因就是写到了Master,但读请求被路由到了还没同步完的Slave。这就是典型的读写不一致。
避坑指南:
我曾经踩过一个坑——Slave节点数量太多,Master的同步带宽被打满。后来我加了一层级联复制:让一部分Slave从Master同步,另一部分Slave从这些“二级Slave”同步。这样Master的负担就降下来了。
4.2 对等架构:人人平等,无中心
对等架构,说白了就是没有“领导”。每个节点都是平等的,都能读写。
这种架构在搜索引擎里不常见,但在一些NoSQL数据库(比如Cassandra)里用得很多。每个节点都保存一部分数据,也都能处理读写请求。
核心特点:
- 无单点故障:任何一个节点挂了,集群照常工作。
- 写操作复杂:因为要保证多个节点之间的数据一致性,需要用到Gossip协议或者Paxos/Raft算法。
- 扩展性好:加节点就能线性提升性能。
我个人习惯,在搜索引擎的实时索引场景里,不太推荐纯对等架构。为什么?因为搜索引擎的写操作(索引构建)非常重,如果每个节点都要参与写,协调成本太高。我一般会把对等架构用在缓存层或者查询聚合层。
注意:
对等架构的“无中心”不代表“无状态”。每个节点依然需要知道集群里有哪些兄弟节点。如果节点发现机制没做好,就会出现“脑裂”——集群分成两半,各自为政。
4.3 分片与副本:数据的分与合
这两个概念,我建议你放在一起理解。分片是横向切分,副本是纵向冗余。
4.3.1 分片(Shard)
分片,就是把一份大索引切成N个小块。每个分片独立存储、独立查询。
举个例子:一个10亿文档的索引,切成10个分片,每个分片只存1亿文档。查询时,10个分片并行搜索,结果再合并。速度能快10倍?理论上是的,但实际受限于网络和合并开销。
分片策略:
- 基于文档ID哈希:简单均匀,但无法范围查询。
- 基于时间范围:适合日志场景,但容易产生热点。
- 自定义路由:灵活,但需要业务配合。
我记得有一次,一个客户把分片数设成了1000个。结果每个分片只有几MB数据,查询时网络开销比搜索本身还大。嗯,分片不是越多越好。我一般建议每个分片控制在10GB-50GB之间。
4.3.2 副本(Replica)
副本,就是分片的备份。每个分片可以有0个或多个副本。
副本的作用有两个:
- 高可用:主分片挂了,副本顶上。
- 提升读性能:查询请求可以分发到副本上,分担压力。
| 特性 | 分片 | 副本 |
|---|---|---|
| 目的 | 分散数据,提升写入和查询吞吐 | 冗余数据,提升可用性和读性能 |
| 数据完整性 | 每个分片是数据的一部分 | 副本和主分片数据完全一样 |
| 写操作 | 只写主分片 | 主分片写完后同步到副本 |
| 读操作 | 可以读主分片或副本 | 可以读任意副本 |
我的经验:
副本数一般设为1-2个就够了。太多副本会导致写放大——每次写操作都要同步到所有副本。我曾经见过一个集群,副本数设了5个,结果写入性能直接腰斩。
4.4 一致性哈希:优雅的扩缩容
好了,现在数据分片了,问题来了:一个文档应该落到哪个分片上?
最简单的办法是取模:hash(文档ID) % 分片数。但这样有个致命问题——分片数一变,所有数据都得重新分布。这就是rehash风暴。
一致性哈希就是来解决这个问题的。它的核心思想是:把哈希值空间组织成一个环。
// 一致性哈希的简化实现
class ConsistentHash {
private final TreeMap<Integer, String> ring = new TreeMap<>();
private final int virtualNodes = 150; // 虚拟节点数
public void addNode(String node) {
for (int i = 0; i < virtualNodes; i++) {
int hash = hash(node + "#" + i);
ring.put(hash, node);
}
}
public String getNode(String key) {
if (ring.isEmpty()) return null;
int hash = hash(key);
// 找到第一个大于等于hash的节点
SortedMap<Integer, String> tailMap = ring.tailMap(hash);
Integer nodeHash = tailMap.isEmpty() ? ring.firstKey() : tailMap.firstKey();
return ring.get(nodeHash);
}
}
这段代码里,我用了虚拟节点。为什么?因为真实节点少的时候,哈希分布不均匀。150个虚拟节点,基本能保证每个物理节点承载的数据量差不多。
一致性哈希的优点:
- 扩缩容影响小:增加或删除一个节点,只影响该节点在环上的邻居,其他节点纹丝不动。
- 负载均衡:虚拟节点让数据分布更均匀。
- 支持动态调整:节点可以随时加入或离开。
我在项目中遇到过一个问题:用一致性哈希做分片路由,结果某个热点文档的查询量特别大,导致它所在的那个节点CPU被打满。后来我加了一层热点检测,把热点文档自动迁移到多个节点上。嗯,这又是另一个故事了。
注意:
一致性哈希不保证数据完全均匀。如果节点配置不同(比如一台机器是128G内存,另一台是32G),你需要给节点加权重。权重高的节点,分配更多的虚拟节点。
4.5 总结:怎么选?
说了这么多,你可能会问:实际项目中到底用哪种?
我个人习惯是这样:
- 搜索引擎核心索引:主从架构 + 分片 + 副本。Master负责写,Slave负责读。分片让数据分散,副本保证高可用。
- 缓存层:对等架构 + 一致性哈希。比如Redis Cluster,每个节点都能读写,扩缩容方便。
- 日志类场景:分片按时间范围,副本设1个。因为日志不需要强一致性,丢了也能接受。
最后说一句:架构没有银弹。主从架构简单但写有瓶颈,对等架构灵活但协调复杂。分片和副本是基础,一致性哈希是工具。理解了这些,你就能根据业务场景,搭出适合自己的集群架构。
嗯,下一章咱们聊聊索引构建的分布式化。到时候你会发现,分片和副本的概念会贯穿始终。