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个。因为日志不需要强一致性,丢了也能接受。

最后说一句:架构没有银弹。主从架构简单但写有瓶颈,对等架构灵活但协调复杂。分片和副本是基础,一致性哈希是工具。理解了这些,你就能根据业务场景,搭出适合自己的集群架构。

嗯,下一章咱们聊聊索引构建的分布式化。到时候你会发现,分片和副本的概念会贯穿始终。