前言
计算机发展了这么久,业务并发需求和计算需求的膨胀,背后支撑的不止是半导体制程的提升,超大规模的集成电路的诞生,其中分布式架构的发展,节点的横向拓展在其中也是发挥了重大作用,在节点制程的停滞,在未来十年,分布式将会越来越重要。
- 有状态服务“分布式必须显式声明状态;
- 有状态服务不好无感横向扩展;
- 没有魔法,所有行为都有声明,无理的需求是不可能办到的;
- 无状态服务非常丝滑的能够横向拓展;
- 有状态服务需要分布式必须要声明路由分区规则;
无状态服务
大部分web服务,后端和数据库是分离的意味着代码本身应该是无状态的,所以可以很丝滑的做分布式,只需要在前面网关负载均衡就行了,包括loki这种把日志持久化到了对象存储里,这时loki就变成无状态的了,非常方便横向扩展,查询插入节点
有状态服务
有状态服务,就算不分布式迁移起来都很麻烦。
大部分服务包括:对象存储,gitlab,游戏服务器,数据库,都是需要把数据持久化到硬盘里的,想要做分布式就非常复杂了。
分布式方式
有状态服务分布式系统大体上就3个解决思路,下面我列举一下,包括使用对应思路的服务,只不过不同的服务具体的实施还是有区别。
多节点完全一致
核心思路
每个节点都保存完整数据,通过共识算法(如 Raft)保持强一致性。
典型服务
- etcd:分布式元数据存储,用于服务发现和配置管理。
- ClickHouse Keeper:用于 ClickHouse 集群的副本协调。
特点
如果数据量太大,更新频繁,同步的性能和时间,耗时非常大,所以一般是最重要的集群路由元数据才使用这种分布式架构,保证高可用。
分片分区路由(Sharding)
核心思路
数据按规则(如哈希、范围)分散到不同节点,每个节点只存部分数据,显而易见的是如果要把数据分散的必须要有地方路由寻址到对应的节点上。
典型服务
- ClickHouse:按
PARTITION BY和分片键分散数据。 - Kafka:Topic 按 Partition 分散到不同 Broker。
- Redis Cluster:按哈希槽(16384 个槽)分散数据。
- Elasticsearch:索引按分片分散到不同节点。
- MongoDB:按分片键(Shard Key)分散数据。
特点
写入能力随节点数线性扩展,查询需要跨节点聚合。手动声明路由分区规则的复杂度,往往正是分布式系统难以运维的根本原因。路由规则一旦定义,后续的扩缩容、数据重平衡、故障恢复都需要与这套规则联动,因此,分布式系统设计的关键之一就在于如何把这套规则封装成更易维护的抽象层。这也正是一致性哈希、元数据服务、Operator 控制器等技术不断演进的驱动力所在。
单主节点写,多子节点同步读(主从复制)
核心思路
一个主节点接收所有写入,从节点复制数据提供读服务。
典型服务
- MySQL / PostgreSQL(主从模式):传统关系型数据库的读写分离。
- Redis Sentinel:主从复制 + 故障自动切换。
- MongoDB Replica Set:Primary 接收写入,Secondary 提供读。
特点
读能力可扩展,写入能力受限于单节点,适合读多写少的业务,用了这个模式,最终的写能力将会有上限。缺点也显而易见,写入和同步和读取有时间差,如果并发量高的话容易状态不一致。但是这个问题一般都已经有了解决方案了,比如数据库事务完成是需要别的读节点有确认步骤,保证分布式事务原子性和状态一致性
模式对比
| 模式 | 写入扩展性 | 读扩展性 | 一致性 | 典型场景 |
|---|---|---|---|---|
| 共识型复制 | 受限于算法性能 | 可线性扩展 | 强一致性 | 元数据存储、配置管理 |
| 分片分区 | 可线性扩展 | 需跨节点聚合 | 最终一致性或可调一致性 | 日志、时序数据、大数据分析 |
| 主从复制 | 单节点瓶颈 | 可线性扩展 | 最终一致性(存在复制延迟) | 传统数据库读写分离 |
典型服务
我们要根据业务特征使用不同的分布式架构,每个项目组的具体的实施方式还是有不一样的,因为业务场景各有不同,没有统一的最优解。
对象存储
四层架构
| 层级 | 功能 | 存储内容 | 数据量 | 类型 | 分布式模式 | 位置 |
|---|---|---|---|---|---|---|
| 访问层(API Gateway) | 接收客户端请求,解析 bucket/object,路由到正确的元数据分片 | 无持久化存储,缓存部分路由信息 | 无 | 无状态 | 横向拓展 | k8s ingress |
| etcd (强一致集群元数据) | 存储集群的拓扑、元数据分片归属、节点状态、Leader 选举 | my-bucket → Meta Shard 3、节点存活状态、分区分配方案 | 小(KB~MB 级别) | 有状态 | 多节点完全一致 | Kubernetes 控制平面(etcd) |
| 数据元数据数据库 | 存储对象的元数据:文件名、大小、权限、版本、数据块 ID 列表、MD5值 | my-file → 大小 10MB、权限 private、块 ID 列表 [Block-12345, Block-67890] | 大(TB~PB 级别) | 有状态 | 分片路由 | MinIO Service |
| 本地节点数据库(数据块管理层) | 存储数据块在本地硬盘上的物理位置,节点独立管理 | Block-12345 → /dev/sdb1 offset 4096 | 中(GB~TB 级别) | 有状态 | 分片路由 | MinIO Pod |
完整的请求链路
客户端 GET /my-bucket/my-file ↓1. ingress7层网关访问层:解析 bucket="my-bucket",查询这个节点的k8s控制平面本地缓存/etcd ↓2. 集群元数据数据库etcd:返回 "my-bucket" → 的具体minio service的url ↓3. minio service数据元数据数据库:查询 "my-file" ↓ 返回:文件大小、权限、MD5、数据块 ID 列表 [Block-12345, Block-67890] ↓4. minio pod本地节点数据库:分别查询 Block-12345 → Node-A /dev/sdb1 offset 4096 ↓5. 访问层:从 Node-A、Node-B 并行读取数据块,合并后返回客户端特点
其实对象存储把一把一部分状态(存储桶)其实存客户端了,自带了存储桶信息,在创建的时候,etcd是集群的元数据层其实,然后数据的元数据通过存储桶分离来分片路由,然后再通过数据元数据来再分片路由到具体的盘块上。其实最关键的一层路由分片是ingress配合etcd到一个minio service上,这就是一个存储桶也就是一个独立的对象存储服务和别人的桶是隔离的,然后再配合minio service到minio pod数据存储的分片路由,这样做到了分层分布式。可以和云服务商卖的对象存储服务结合起来看。2层的分片路由做到了无限的横向有状态分布式服务可拓展性,这就是现代的分布式架构。
ClickHouse
使用 ClickHouse Keeper 协调(就像minio service clickhouse集群的控制平面);建表的时定义分区机制,分片分区(Sharding)+ 副本复制(Replication),每个分片可以有多个副本。
Kafka
分片分区(Partition)+ 副本复制(Replica),每个 Partition 可以有多个 Replica,Leader 处理读写,Follower 同步。
Elastic Search
主从架构,从节点只读,读并发可以无限横向拓展,按索引分片分区路由多主节点横向拓展。
Gitaly(Gitlab的有状态部分)
分片模式:按仓库分片路由,但没有副本机制保证高可用 集群模式:主从架构。
总结
其实我们追求分布式,多线程,异步来提高并发量都是一个理念,尽可能的把“有状态”的部分限制在尽可能小的范围内,把“无状态”的部分放开,让它尽情并行。有状态的比如写入数据库,这种操作,两条sql之间完全不可以并行,有状态的增删改查之间时序不能乱了,但是无状态的计算是可以横向扩展的。系统的并发上限很大程度上就在有状态服务的增删改查这里的架构。