diagram-design 重绘图预览 · 亮色主题 · zh

rd-01-single-server

01. Scaling/images/single-server.png · 单服务器架构

单服务器架构客户端向 DNS 查询 api.mysite.com 拿到 IP 地址,再访问唯一一台 Web 服务器 www.mysite.com;Web、应用逻辑与数据都跑在这台机器上。客户端Web 浏览器 · 手机 AppDNSapi.mysite.comWeb 服务器www.mysite.comapi.mysite.comIP 地址www.mysite.com
按 diagram-design 重绘

rd-01-database

01. Scaling/images/database.png · 引入数据库:Web 层与数据层分离

引入数据库:Web 层与数据层分离DNS 把 www.mysite.com 解析成 IP 后,客户端访问 Web 服务器;Web 服务器通过读写/更新操作访问独立部署的数据库,数据不再和代码挤在同一台机器上。客户端Web 浏览器 · 手机 AppDNSwww.mysite.comWeb 服务器数据库www.mysite.comIP 地址www.mysite.com读写 / 更新返回数据
按 diagram-design 重绘

rd-01-load-balancer

01. Scaling/images/load-balancer.png · 负载均衡器与 DNS 解析结果

负载均衡器与 DNS 解析结果DNS 把 mywebsite.com 解析为公网 IP 88.88.88.1;客户端访问该公网 IP 进入负载均衡器,由其按策略转发到内网 10.0.0.1、10.0.0.2 两台服务器。客户端只认识一个公网地址,服务端可以自由增减。DNSmywebsite.com客户端Web 浏览器 · 手机 App负载均衡器服务器 1服务器 2解析结果公网 IP10.0.0.110.0.0.2Domain mywebsite.comIP 88.88.88.1(DNS 解析结果)
按 diagram-design 重绘

rd-01-database-replication

01. Scaling/images/database-replication.png · 数据库复制:主库写入、从库读取

数据库复制:主库写入、从库读取负载均衡器把流量分给 Web 层的两台服务器;两台服务器都写主库、读从库,主库再把变更异步复制到从库。读请求被分流后,数据库层的读能力随从库数量线性扩展。Web 层数据层客户端Web 浏览器 · 手机 AppDNSwww.mysite.com负载均衡器服务器 1服务器 2主库写入从库读取www.mysite.comIP 地址www / api读写读写复制
按 diagram-design 重绘

rd-01-cache

01. Scaling/images/cache.png · 缓存读写路径

缓存读写路径读请求先问缓存:命中就直接返回,不命中才回源数据库,并把结果写回缓存供下次命中。数据库只承担未命中的那部分流量。Web 服务器缓存数据库1 命中即读2.2 返回数据2.1 未命中回源3 写回缓存
按 diagram-design 重绘

rd-01-cdn

01. Scaling/images/cdn.png · CDN 静态资源分发

CDN 静态资源分发用户 A 请求图片:CDN 命中就直接返回;未命中则回源站取图并缓存,用户 B 之后再来就命中同一份缓存。地理上更近的边缘节点同时降低了延迟和源站带宽。用户 A用户 BCDN边缘节点源站服务器1 请求图片4 返回图片5 请求图片6 返回图片2 未命中回源3 缓存到边缘
按 diagram-design 重绘

rd-01-stateless

01. Scaling/images/stateless.png · 无状态 Web 层与会话外置

无状态 Web 层与会话外置把会话数据从 Web 服务器挪到共享存储后,任何一台服务器都能处理任意用户的请求,宕机时直接把流量切走即可,不必在节点之间同步状态。用户 A用户 B用户 CWeb 服务器集群共享存储会话数据会话外置
按 diagram-design 重绘

rd-01-data-center

01. Scaling/images/data-center.png · 多数据中心部署

多数据中心部署地理上分成两个数据中心,各自拥有完整的 Web 服务器、数据库与缓存;静态资源交给 CDN 就近返回。两个中心的数据库都向同一个 NoSQL 汇聚,用于跨地域的数据合并与去重。数据中心 1数据中心 2客户端Web 浏览器 · 手机 AppDNSwww.mysite.com负载均衡器CDN静态资源Web 服务器数据库缓存Web 服务器数据库缓存NoSQL跨地域汇聚www.mysite.comIP 地址API 请求
按 diagram-design 重绘

rd-01-message-queue

01. Scaling/images/message-queue.png · 消息队列解耦耗时任务

消息队列解耦耗时任务Web 服务器只负责把「处理照片」这件事发布到队列就立即返回;消费者按自己的节奏取出任务处理。用户的等待时间不再等于处理时间,消费者也能独立扩容。Web 服务器接收图片上传消息队列待处理照片消费者照片处理发布消费
按 diagram-design 重绘

rd-01-logging

01. Scaling/images/logging.png · 日志、指标与自动化

日志、指标与自动化Web 服务器把埋点先发到消息队列,由 Worker 异步聚合成指标,避免监控写入拖慢在线请求;同一批信号最终汇入日志、指标、监控与自动化工具链。数据库与缓存在这一层并列,NoSQL 承担不适合关系模型的数据。客户端Web 浏览器 · 手机 AppDNSwww.mysite.comCDN静态资源负载均衡器Web 服务器消息队列聚合 Worker数据库缓存NoSQL运维工具链日志 · 指标 · 监控 · 自动化www.mysite.comIP 地址API 请求埋点消费
按 diagram-design 重绘

rd-01-horizontal-scaling

01. Scaling/images/horizontal-scaling.png · 垂直扩容与水平扩容

垂直扩容与水平扩容垂直扩容是把单台机器换得更强:改一个数字就能生效,但硬件有天花板,也无法通过加机器分摊故障。水平扩容是加更多普通机器:容量几乎可以线性增长,代价是要处理负载均衡、状态外置与数据分片。垂直扩容 · 提升单机规格水平扩容 · 增加机器数量更强的单机CPU / 内存 / 磁盘服务器 1服务器 2服务器 3服务器 4对比
按 diagram-design 重绘

rd-02-power-of-two

02. Back Of the Envelope Estimation/images/power-of-two.png · 2 的幂与常见延迟量级

2 的幂与常见延迟量级把容量估算的两张速查表并列。上半组是存储量级:2^10 约千字节(1 KB)、2^20 约百万字节(1 MB)、2^30 约十亿字节(1 GB)、2^40 约万亿字节(1 TB)、2^50 约千万亿字节(1 PB),每加 10 次方跨一个数量级。下半组是常见操作的延迟量级,按从快到慢排列:L1 缓存 0.5ns、L2 缓存 7ns、内存 100ns、SSD 随机读 150µs、数据中心往返 500µs、磁盘寻道 10ms、跨地域机房 150ms。存储量级延迟量级1 KB2^10 ≈ 千1 MB2^20 ≈ 百万1 GB2^30 ≈ 十亿1 TB2^40 ≈ 万亿1 PB2^50 ≈ 千万亿L1 缓存0.5 nsL2 缓存7 ns内存访问100 nsSSD 随机读150 µs数据中心往返500 µs磁盘寻道10 ms跨地域机房150 ms速查:2 的幂给出存储量级,延迟数字给出各操作的耗时量级每 +10 次方跨一个数量级:KB → MB → GB → TB → PB内存快、磁盘慢;跨地域往返最慢,尽量避开磁盘寻道
按 diagram-design 重绘

rd-04-rate-limiter-architecture

04. Rate Limiter/images/rate_limiter_architecture.png · 限流中间件的部署位置

限流中间件的部署位置三种可选部署位置:客户端侧(不可靠)、服务端侧、以及作为中间件/API 网关(最灵活,推荐)。图里画的是推荐的网关方案:客户端请求先经限流中间件,再转发到后端服务器。客户端限流中间件API 网关服务器 1服务器 2请求放行放行三种位置:客户端侧 / 服务端侧 / 中间件(推荐)
按 diagram-design 重绘

rd-04-token-bucket

04. Rate Limiter/images/token-bucket.png · 令牌桶算法

令牌桶算法令牌以固定速率被放进桶里,每个请求消耗一个令牌;桶满则丢弃多余令牌。有令牌时请求被放行,无令牌时拒绝。支持突发流量,内存占用低。客户端令牌生成器令牌桶容量 + 速率服务器按固定速率请求放行
按 diagram-design 重绘

rd-04-leaking-bucket

04. Rate Limiter/images/leaking-bucket.png · 漏桶算法

漏桶算法请求进入 FIFO 队列,以固定速率被处理流出。流出速率恒定,能平滑流量;但突发请求会在队列里排队,可能造成近期请求延迟。请求流入漏桶FIFO 队列处理流出入队固定速率流出
按 diagram-design 重绘

rd-04-fixed-window-counter

04. Rate Limiter/images/fixed-window-counter.png · 固定窗口计数器

固定窗口计数器把时间切成固定长度的窗口,每个窗口内用计数器累计请求数;窗口结束后计数器清零。实现简单、内存开销小,但窗口边界处的突发可能让请求量超过配额。时间窗 [t,t+1)客户端计数器每窗重置请求 +1窗口内
按 diagram-design 重绘

rd-04-fixed-window-issue

04. Rate Limiter/images/fixed-window-issue.png · 固定窗口的边界突发问题

固定窗口的边界突发问题两个相邻窗口各自允许配额内的请求;若突发流量恰好跨在窗口边界上,两个窗口会各放进一批请求,整体实际通过量超过单窗配额。窗口 1窗口 2突发流量跨边界n 请求n 请求
按 diagram-design 重绘

rd-04-sliding-window-log

04. Rate Limiter/images/sliding-window-log.png · 滑动窗口日志

滑动窗口日志记录每个请求的时间戳,只保留滑动窗口内的记录;新请求到达时若窗口内数量未超配额则放行,超窗口的旧记录被丢弃。精确但内存开销高。客户端时间戳日志滑动窗口服务器到达窗内则放行
按 diagram-design 重绘

rd-04-sliding-window-counter

04. Rate Limiter/images/sliding-window-counter.png · 滑动窗口计数器

滑动窗口计数器结合固定窗口与滑动日志:用上一窗口的计数和当前窗口的计数按时间权重加权,估算滑动窗口内的请求数。内存开销低、能平滑突发,只是近似值。上一窗口计数 C1当前窗口计数 C2加权估算放行判定C1×(1−w)C2×w估算请求数
按 diagram-design 重绘

rd-04-architecture

04. Rate Limiter/images/architecture.png · 限流器高层架构

限流器高层架构客户端把请求发到限流中间件;中间件在 Redis 里读写计数器做限流判断,再决定把请求转发给业务 API 还是拒绝。Redis 提供低延迟的集中式计数存储,便于分布式同步。客户端限流中间件API 网关Redis计数器业务 API请求读写计数转发 / 拒绝
按 diagram-design 重绘

rd-05-server-hashing

05. Consistent Hashing/images/server-hashing.png · 传统取模哈希

传统取模哈希用 serverIndex = hash(key) % N 把键映射到服务器:每个键经同一哈希函数算出一个值,再对服务器数量取模得到目标服务器。在服务器数量固定的情况下工作良好。键 Keyhash(key) % N服务器 0服务器 1服务器 2服务器 3hashN 固定时分布均匀
按 diagram-design 重绘

rd-05-server-hashing-miss

05. Consistent Hashing/images/server-hashing-miss.png · 取模哈希在服务器变动时大量未命中

取模哈希在服务器变动时大量未命中一旦服务器数量从 N 变成 N±1,hash(key) % N 的结果对大多数键都变了,缓存几乎全部失效(未命中),需要重新加载数据,造成压力骤增。键 Keyhash(key) % N服务器 0服务器 2服务器 3服务器 1(下线)hashN 变化 → 多数键重映射
按 diagram-design 重绘

rd-05-hash-ring

05. Consistent Hashing/images/hash-ring.png · 哈希空间与环

哈希空间与环哈希函数的输出构成一个连续的哈希空间(如 SHA-1 的 0 到 2^160−1)。把空间首尾相接,就得到一条首尾相连的环;数据与服务都映射到这条环上。哈希空间 0 → 2^160−1首尾相接 → 形成环0 ≡ 2^160−1
按 diagram-design 重绘

rd-05-server-ring

05. Consistent Hashing/images/server-ring.png · 服务器映射到哈希环

服务器映射到哈希环用同一哈希函数把服务器的 IP 或名称哈希到环上,得到服务器在环上的位置。换哈希函数不会影响已有映射的「相对顺序」。哈希环(展开)服务器 0服务器 1服务器 2服务器 3server → hash(IP) → 环上位置
按 diagram-design 重绘

rd-05-server-lookup

05. Consistent Hashing/images/server-lookup.png · 键的服务器查找

键的服务器查找对键做 hash(key) 得到环上的一个点,从该点沿顺时针方向走,遇到的第一个服务器就是这个键的归属服务器。哈希环(展开)键 Key服务器 0服务器 1服务器 2服务器 3hash(key)从映射点顺时针找第一个服务器
按 diagram-design 重绘

rd-05-adding-server

05. Consistent Hashing/images/adding-server.png · 新增服务器只影响附近键

新增服务器只影响附近键新增服务器 s4 时,只有原本落在 s3 与 s4 之间弧段上的键会被重新映射到 s4,其余键的位置不变。相比取模哈希,迁移量从「全部」降到「一小段」。哈希环(展开)服务器 0服务器 1服务器 2服务器 3服务器 4仅 s3 与 s4 之间的键迁移到 s4
按 diagram-design 重绘

rd-05-removing-server

05. Consistent Hashing/images/removing-server.png · 移除服务器只影响它的键

移除服务器只影响它的键服务器 s1 下线后,原本由它负责的键会顺时针迁移到环上的下一个服务器(s2),其余服务器不受影响。哈希环(展开)服务器 0服务器 2服务器 3服务器 1(下线)迁移s1 的键顺时针迁移到下一个服务器
按 diagram-design 重绘

rd-05-virtual-nodes

05. Consistent Hashing/images/virtual-nodes.png · 虚拟节点均衡负载

虚拟节点均衡负载每个物理服务器在环上映射为多个虚拟节点,分布更均匀。虚拟节点越多,各物理服务器分到的键越均衡,避免热点。哈希环(展开)服务器 0服务器 1服务器 21.1虚拟节点1.2虚拟节点2.1虚拟节点2.2虚拟节点3.1虚拟节点3.2虚拟节点
按 diagram-design 重绘

rd-05-server-addition

05. Consistent Hashing/images/server-addition.png · 新增服务器的受影响范围

新增服务器的受影响范围新增 s4 后,受影响的键是「从 s4 逆时针到前一个服务器 s3」之间的那一段:这些键从原服务器迁移到 s4。哈希环(展开)服务器 3服务器 4受影响:s3 与 s4 之间的键
按 diagram-design 重绘

rd-05-server-removed

05. Consistent Hashing/images/server-removed.png · 移除服务器的受影响范围

移除服务器的受影响范围移除 s1 后,受影响的键是「从 s1 逆时针到前一个服务器 s0」之间的那一段:这些键从 s1 迁移到顺时针下一个服务器 s2。哈希环(展开)服务器 0服务器 1(下线)服务器 2受影响:s0 与 s1 之间的键 → s2
按 diagram-design 重绘

rd-06-cap

06. Key-Value Store/images/cap.png · CAP 定理:三者只能取二

CAP 定理:三者只能取二一致性(所有客户端同时看到同一份数据)、可用性(每个请求都有响应)、分区容错(网络分区时仍能运行)。分布式系统里网络分区不可避免,所以只能在一致性和可用性之间二选一:CP 系统在分区时拒绝写入以保一致,AP 系统继续服务但可能返回旧值,CA 系统在现实中不成立。CAP 定理三者只能取二CP 系统牺牲可用性AP 系统牺牲强一致CA 系统现实不成立C + PA + PC + AC 一致性 · A 可用性 · P 分区容错网络分区不可避免 → 只能在 C 与 A 之间取舍
按 diagram-design 重绘

rd-06-server-down

06. Key-Value Store/images/server-down.png · 节点 n3 下线造成的分区

节点 n3 下线造成的分区n3 与 n1、n2 之间发生网络分区:写进 n1/n2 的数据传不到 n3,写进 n3 的数据也传不到 n1/n2。选 CP 就必须阻塞 n1/n2 的写入以避免不一致;选 AP 则继续接受读写,等分区恢复后再把数据同步给 n3。客户端写入节点 n1节点 n2节点 n3(下线)写写分区后:n3 的写入无法传播CP:阻塞 n1/n2 写入 · AP:继续服务,恢复后同步
按 diagram-design 重绘

rd-06-data-replication

06. Key-Value Store/images/data-replication.png · 数据复制:顺时针取前 N 个节点

数据复制:顺时针取前 N 个节点键映射到环上后,从它的位置沿顺时针方向走,遇到的前 N 个服务器就是这份数据的 N 个副本(图中 N=3,即 s0、s1、s2 三个加重的节点)。副本应尽量落在不同数据中心,以提高可靠性。哈希环(展开)服务器 s0服务器 s1服务器 s2服务器 s3N = 3:从键的位置顺时针取前 3 个虚线 = 顺时针方向;首尾相接成环
按 diagram-design 重绘

rd-06-quorum-consensus

06. Key-Value Store/images/quorum-consensus.png · Quorum 共识(N=3, W=2, R=2)

Quorum 共识(N=3, W=2, R=2)N 是副本总数,W 是一次写入需要多少副本确认才算成功,R 是一次读取需要多少副本响应才算成功。只要 W + R > N,读写集合必然重叠,就能保证强一致;配置 W、R 实际是在延迟和一致性之间做取舍。W=1、R=N 是快写慢读,R=1、W=N 是快读慢写。协调器N=3, W=2, R=2副本 1副本 2副本 3写 ✓写 ✓超出 WW + R > N → 读写集合必重叠 → 强一致
按 diagram-design 重绘

rd-06-consistent-server

06. Key-Value Store/images/consistent-server.png · 版本化:顺序复制不产生冲突

版本化:顺序复制不产生冲突每次修改都当作数据的一个新的不可变版本。客户端写入 name 到服务器 1,得到版本 D1([S1,1]);服务器 1 把这份数据复制给服务器 2,向量时钟追加 [S2,1] 变成 D2([S1,1],[S2,1])。后一个版本包含前一个的全部计数,是它的后代,因此没有冲突。写入 name服务器 1D1([S1,1])服务器 2D2([S1,1],[S2,1])put复制版本沿复制链依次推进 → 后代,无冲突
按 diagram-design 重绘

rd-06-inconsistent-server

06. Key-Value Store/images/inconsistent-server.png · 版本化:并发写产生冲突版本

版本化:并发写产生冲突版本两个客户端同时修改同一个 name,服务器 1 记下 D1([S1,1])、服务器 2 记下 D2([S2,1])。两个版本各自都有对方没有的计数,互不为祖先,称为「兄弟版本」——这就是冲突,需要应用层逻辑或客户端介入来合并。写入 A写入 B服务器 1D1([S1,1])服务器 2D2([S2,1])兄弟版本v1 与 v2putput冲突冲突同时发生 → 互不包含 → 需冲突解决
按 diagram-design 重绘

rd-06-vector-clock

06. Key-Value Store/images/vector-clock.png · 向量时钟:祖先与兄弟

向量时钟:祖先与兄弟向量时钟是挂在数据项上的一组 [服务器, 版本] 对:D1 在 S1 上写入得到 [S1,1],再写一次变 [S1,2],S2 写入后追加 [S2,1]。若版本 X 的每个计数都小于等于 Y,则 X 是 Y 的祖先、无冲突;只要有一个计数反过来更小,两者就是兄弟版本,即冲突。缺点是客户端变复杂,且时钟会随更新不断变长,需要裁剪。D1([S1,1])D2([S1,2])D3([S1,2],[S2,1])D([S1,1],[S3,1])写 S1写 S2并发写 S3D1 ⊆ D2 ⊆ D3 为祖先链;D2 与 D([S1,1],[S3,1]) 为兄弟
按 diagram-design 重绘

rd-06-gossip-protocol

06. Key-Value Store/images/gossip-protocol.png · Gossip 协议做故障检测

Gossip 协议做故障检测每个节点维护一张「成员 ID + 心跳计数」表,周期性自增自己的心跳,并把心跳随机发给若干节点。若某个成员的心跳长时间不再增长,才判定它离线——单凭一个节点说「它挂了」不算数,通常需要两个独立信息源。节点 n1成员 ID + 心跳节点 n2节点 n3节点 n4心跳心跳心跳随机挑若干节点发心跳(去中心化)心跳长时间不增长 → 判定该成员离线
按 diagram-design 重绘

rd-06-sloppy-quorum

06. Key-Value Store/images/sloppy-quorum.png · Sloppy quorum:临时借用健康节点

Sloppy quorum:临时借用健康节点节点 s2 下线时,不再硬性要求写入指定的 N 个副本,而是沿环跳过它、继续往后取前 W 个健康节点(这里是 s3)承接写入,并记下「本该写在 s2」的提示;等 s2 恢复后再把变更回补过去,即 hinted handoff。哈希环(展开)服务器 s0服务器 s1服务器 s2(下线)服务器 s3s2 下线 → 顺延取健康节点 s3 承接写入s2 恢复后由 hinted handoff 回补变更
按 diagram-design 重绘

rd-06-key-bucket

06. Key-Value Store/images/key-bucket.png · Merkle 树第 1 步:键空间分桶

Merkle 树第 1 步:键空间分桶先把整个键空间切成若干个桶。桶是同步的最小单位:比对时只需要找出不一致的桶,而不必逐键比对整个数据集。键空间分桶桶 1桶 2桶 3桶 4桶是同步的最小单位
按 diagram-design 重绘

rd-06-hash-key-bucket

06. Key-Value Store/images/hash-key-bucket.png · Merkle 树第 2 步:桶内逐键哈希

Merkle 树第 2 步:桶内逐键哈希用统一的哈希函数给桶里的每个键算出各自的哈希值。这一步得到的是「叶子层」——每个键的哈希就是树的一片叶子。桶key 1key 2key 3哈希函数叶子哈希hashhashhash每个键一个哈希 → 树的叶子层
按 diagram-design 重绘

rd-06-hash-bucket

06. Key-Value Store/images/hash-bucket.png · Merkle 树第 3 步:每桶合成一个哈希

Merkle 树第 3 步:每桶合成一个哈希把桶内所有键的哈希再合并成一个哈希,作为这个桶的哈希值。这样每个桶只用传递一个哈希就能判断内容是否相同。桶key 1key 2key 3桶哈希桶的哈希值合并合并合并一桶一个哈希 → 比对只需传一个值
按 diagram-design 重绘

rd-06-merkel-tree

06. Key-Value Store/images/merkel-tree.png · Merkle 树:自底向上合成根哈希

Merkle 树:自底向上合成根哈希叶子节点存数据块(或键)的哈希,非叶子节点存其子节点哈希的哈希,层层向上合成出根哈希。同步两个副本时先比根哈希:相同则一致;不同则逐层往下比子哈希,定位到不一致的桶,只同步那部分数据,传输量极小。根哈希中间哈希 1中间哈希 2叶子 1叶子 2叶子 3叶子 4根哈希相同 → 一致;不同 → 逐层下钻定位不一致的桶
按 diagram-design 重绘

rd-06-write-path

06. Key-Value Store/images/write-path.png · 写路径(Cassandra 式)

写路径(Cassandra 式)写入先追加到提交日志(顺序写,用于宕机恢复),再写进内存缓存;内存缓存攒满后整体刷盘成 SSTable(有序字符串表,落盘后不可变)。提交日志保证持久性,内存缓存保证低延迟。写请求提交日志顺序写磁盘内存缓存SSTable磁盘追加写入满则刷盘先落日志保证持久,再进内存保证延迟
按 diagram-design 重绘

rd-06-read-path

06. Key-Value Store/images/read-path.png · 读路径:先查内存缓存

读路径:先查内存缓存读请求先查内存缓存:命中就直接返回,最快。未命中则先用布隆过滤器判断目标键大概在哪些 SSTable 里——布隆过滤器说「没有」就一定没有,可以跳过整个文件,避免无谓的磁盘读。读请求内存缓存命中即返回布隆过滤器SSTable查缓存未命中定位命中直接返回;未命中才走磁盘
按 diagram-design 重绘

rd-06-read-path-without-cache

06. Key-Value Store/images/read-path-without-cache.png · 读路径:缓存未命中时的分支

读路径:缓存未命中时的分支缓存未命中时(图中虚线框表示这一步被跳过),请求改走布隆过滤器 → SSTable 的分支:由布隆过滤器筛掉不可能包含该键的文件,只在剩下的 SSTable 里查找,最后把结果返回并可回填缓存。读请求内存缓存(未命中)布隆过滤器SSTable未命中查定位缓存未命中 → 跳过,改走过滤器分支
按 diagram-design 重绘

rd-06-final-architecture

06. Key-Value Store/images/final-architecture.png · 最终架构

最终架构客户端只用 get(key) / put(key, value) 两个接口访问存储;协调器作为客户端与存储之间的代理节点,按一致性哈希把请求路由到环上的节点。节点分布在环上、数据多副本保存,每个节点职责相同,因此没有单点故障,增删节点也可以自动完成。哈希环(展开)客户端get / put协调器代理节点节点 1节点 2节点 3请求路由路由路由无单点故障;节点可自动增删数据在环上多副本保存
按 diagram-design 重绘

rd-07-multi-master

07. Unique-Id Generator/images/multi-master.png · 多主复制方案

多主复制方案每台数据库用 auto_increment 但步长设为 k、起点各不相同(如主库1从1开始每次+2,主库2从2开始每次+2),从而不冲突地生成唯一 ID。缺点:难以跨数据中心扩展,且 ID 不保证随时间严格递增。客户端主库 1 (+2)主库 2 (+2)写写各自自增,ID 不保证跨库递增
按 diagram-design 重绘

rd-07-uuid

07. Unique-Id Generator/images/uuid.png · UUID(128 位)

UUID(128 位)每台服务器独立生成 128 位 UUID,无需彼此协调,扩展性好。缺点是超出 64 位上限、不是数值型、也不能按时间排序。UUID 128 位时间 60位版本 4位变体 2位节点 54位组成组成组成组成
按 diagram-design 重绘

rd-07-ticket-server

07. Unique-Id Generator/images/ticket-server.png · 票据服务器方案

票据服务器方案用一个集中式数据库负责自增并分配 ID:客户端向票据服务器请求,服务器在数据库里自增后把 ID 返回。实现简单、生成的是数值型 ID,但数据库是单点,多机同步也有挑战。客户端票据服务器(集中式)数据库请求 ID自增 + 分配DB 自增后把 ID 返回给客户端
按 diagram-design 重绘

rd-07-twitter-snowflake

07. Unique-Id Generator/images/twitter-snowflake.png · Snowflake 思路

Snowflake 思路把 64 位 ID 拆成几段:时间戳、数据中心 ID、机器 ID、序列号。各段在本地拼接即可生成 ID,无需中心协调,因此是去中心化的、按时间有序的,单机每毫秒可生成数千个。时间戳 41位数据中心 5位机器 5位序列号 12位64 位 ID拼接拼接拼接拼接
按 diagram-design 重绘

rd-07-snowflake-breakdown

07. Unique-Id Generator/images/snowflake-id-breakdown.png · Snowflake ID 位级拆解

Snowflake ID 位级拆解64 位从高到低依次为:1 位符号位(恒为 0)、41 位毫秒时间戳、5 位数据中心 ID、5 位机器 ID、12 位序列号。时间戳保证按时间有序,序列号保证同一毫秒内的并发。1时间戳 41 位机房 5位机器 5位序列号 12 位字段:符号位(恒为 0)· 毫秒时间戳 · 数据中心 · 机器 · 序列号高位 → 低位(相邻字段首尾相接)
按 diagram-design 重绘

rd-08-url-redirection

08. URL Shortener/images/url-redirection.png · URL 重定向:301 与 302

URL 重定向:301 与 302浏览器请求短链,短链服务返回重定向响应并带上长 URL,浏览器再去访问长 URL。301 表示「永久移动」,浏览器会缓存这个映射,之后的同一请求不再打到短链服务(压力最小,但没法统计点击);302 表示「临时移动」,每次请求都会回到短链服务,便于统计点击来源等数据,代价是服务压力更大。浏览器短链服务长 URL 站点301 永久浏览器缓存302 临时每次回源GET 短链跳转301:浏览器缓存映射,后续不再回源
按 diagram-design 重绘

rd-08-url-shortening

08. URL Shortener/images/url-shortening.png · 长 URL 映射为短 URL

长 URL 映射为短 URL用哈希函数把长 URL 映射成短 URL。哈希函数要满足两条:同一个长 URL 必须哈希到同一个 hashValue,并且这个 hashValue 能反查回原来的长 URL。长 URL哈希函数短 URL输入输出要求:同输入同输出,且 hashValue 可反查
按 diagram-design 重绘

rd-08-table-schema

08. URL Shortener/images/table-schema.png · 表结构(关系型数据库)

表结构(关系型数据库)把 <shortURL, longURL> 映射存在关系型数据库里:id 是主键,shortURL 与 longURL 各占一列。选关系库而不是内存哈希表的考量是节省内存——3650 亿条记录放在内存里不现实。url 表id主键shortURLvarcharlongURLvarchar一行一个映射,按 shortURL 查询
按 diagram-design 重绘

rd-08-hash-function

08. URL Shortener/images/hash-function.png · 哈希 + 冲突解决

哈希 + 冲突解决用 CRC32、MD5、SHA-1 之类的哈希函数算出哈希值,取前 7 位作为短链。问题是会碰撞:两个不同的长 URL 可能取到同样的前 7 位。经典解法是检测到冲突后,往原串上再拼一个预定义字符串重新哈希,递归直到不冲突——但每一次重试都是一次额外查询,代价不低,所以实践中会用布隆过滤器加速查重。长 URL哈希函数CRC32 / MD5取前 7 位冲突 → 追加字符串重算输入哈希值冲突重算7 位 Base 62 可容纳约 3.5 万亿个短链
按 diagram-design 重绘

rd-08-url-lookup

08. URL Shortener/images/url-lookup.png · 布隆过滤器加速查重

布隆过滤器加速查重判断一个短链是否已经用过时,先问布隆过滤器:它说「不存在」就肯定不存在,可以直接采用,省掉一次数据库查询;它说「可能存在」才有必要真的去查一次数据库。用极小的内存换来绝大多数查询的短路。短 URL布隆过滤器数据库不存在 → 可用查可能存在一定不存在说「没有」就一定没有;说「可能有」再去查库
按 diagram-design 重绘

rd-08-url-shortening-flow

08. URL Shortener/images/url-shortening-flow.png · URL 缩短流程

URL 缩短流程收到 longURL 后先查库:如果已经 shortened 过就直接返回已有的 shortURL(保证同一个长 URL 永远得到同一个短链)。没有则由分布式 ID 生成器发一个全局唯一 ID,转成 Base 62 得到 shortURL,最后把 <id, shortURL, longURL> 写进数据库。长 URL是否已存在唯一 ID 生成器Base 62 转换数据库longURL未命中转 Base 62保存映射已存在则直接返回旧 shortURL,保证幂等例:ID 2009215674938 → zn9edcu
按 diagram-design 重绘

rd-08-url-redirecting-flow

08. URL Shortener/images/url-redirecting-flow.png · URL 重定向流程

URL 重定向流程用户点击短链后,先查缓存:命中就直接拿到 longURL 返回;未命中才回源查数据库,查到后把结果写回缓存并返回。拿到 longURL 后浏览器跳转到目标站点。读写比约 10:1,所以缓存这一层挡掉了绝大部分数据库压力。用户点击短链缓存数据库长 URL先查缓存未命中命中返回查到返回读写比约 10:1,缓存挡掉大部分读压力未命中回源查库,并回写缓存
按 diagram-design 重绘

rd-09-web-crawler-architecture

09. Web Crawler/images/web-crawler-architecture.png · 网络爬虫高层架构

网络爬虫高层架构上排是下载与解析链:种子 URL 进入 URL Frontier(FIFO 队列),HTML 下载器经 DNS 解析器拿到 IP 后抓取页面,内容解析器校验并解析(畸形页面直接丢弃)。解析出的内容先过「内容已见?」做哈希比对去重,新的才写进内容存储。下排是 URL 支链:从页面里提取链接,过滤掉黑名单和错误 URL,再过「URL 已见?」去重,最后回到 URL Frontier——形成一个闭环的 BFS。种子 URLURL FrontierFIFO 队列HTML 下载器内容解析器内容已见?DNS 解析器内容存储URL 提取器URL 过滤器URL 已见?种子出队解析HTML解析新内容提取链接过滤去重入队上排:抓取与解析;下排:提取与去重,回到 Frontier 形成闭环
按 diagram-design 重绘

rd-09-politeness

09. Web Crawler/images/politeness.png · 礼貌:同一主机串行下载

礼貌:同一主机串行下载队列路由保证每个队列 b1…bn 里只有同一个主机的 URL(靠一张主机名到队列的映射表实现);队列选择器把每个工作线程绑到一个 FIFO 队列上,线程只从自己的队列取 URL。于是同一个主机永远只有一个线程在下载、并且是串行的,两次任务之间还能插入延迟,避免把对方站点打垮。URL 集合队列路由按主机分流队列 b1同一主机队列 b2同一主机队列 b3同一主机队列选择器线程绑定队列工作线程 1-NURL分流取任务一队列一主机;一线程一队列,串行下载两次下载任务之间插入延迟,避免冲击目标站点
按 diagram-design 重绘

rd-09-prioritizer

09. Web Crawler/images/prioritizer.png · 优先级:重要页面先抓

优先级:重要页面先抓优先级计算器按 PageRank、更新频率等给每个 URL 打分,把 URL 投进不同优先级的前队列 f1…fn;队列选择器按优先级加权地随机挑队列——高优先级队列被选中的概率更大,但不会被饿死。前队列负责优先级,后队列负责礼貌,两者串起来才既抓得准又不惹人烦。URL 集合优先级计算器PageRank前队列 f1高优先级前队列 f2中优先级前队列 f3低优先级队列选择器按优先级加权后队列(礼貌)URL算优先级取任务高优先级队列被选中概率更大,但不会饿死前队列管优先级,后队列管礼貌
按 diagram-design 重绘

rd-09-extensibility

09. Web Crawler/images/extensibility.png · 可扩展性:按内容类型插模块

可扩展性:按内容类型插模块把下载与处理逻辑做成可插拔模块挂在爬虫核心上:原来只有 HTML 下载器,要抓图片就插一个 PNG 下载器,要查侵权就插一个网页监控模块。新增内容类型时只加模块、不动核心,改动量最小。爬虫核心可插拔HTML 下载器PNG 下载器新增模块网页监控新增模块网页归档新增模块插入新增内容类型 → 只加模块,不动核心
按 diagram-design 重绘

rd-10-contact-info-gathering

10. Notification System/images/contact-info-gathering.png · 收集联系方式

收集联系方式用户安装 App 或注册时,系统顺手收集三类联系方式:推送用的设备 token、短信用的手机号、邮件用的邮箱地址。设备 token 存进设备 Token 表(一台设备一行),手机号和邮箱存进用户表。之后发通知时就靠这两张表找到投递地址。用户安装 / 注册联系方式收集设备 Token 表推送用用户表邮箱 / 手机号授权存 token存联系方式装 App / 注册时顺带收集,不必单独一步
按 diagram-design 重绘

rd-10-high-level-design

10. Notification System/images/high-level-design.png · 初始设计

初始设计触发服务(微服务、定时任务或分布式系统)产生通知事件,调用通知服务器暴露的 API;通知服务器做基本校验(邮箱、手机号是否合法),从数据库或缓存里取渲染通知所需的元数据,再把通知交给 APNS、FCM、Twilio 这类第三方服务投递给用户。结构简单,但通知服务器是单点:它挂了整个系统就停了,而且数据库、缓存、处理能力都绑在一起,没法各自扩展。触发服务微服务 / 定时通知服务器校验 + 渲染数据库 / 缓存第三方服务APNS / FCM用户设备调用 API取元数据发送投递单点故障:一台通知服务器挂掉 → 全站通知停摆
按 diagram-design 重绘

rd-10-improved-design

10. Notification System/images/improved-design.png · 改进设计:队列解耦 + 水平扩展

改进设计:队列解耦 + 水平扩展三处改动:数据库和缓存从通知服务器里搬出去独立部署;通知服务器变成无状态的多个实例,可以水平扩展;中间加一层消息队列做缓冲,通知服务器只负责把事件投进队列,工作线程再从队列里拉取并调用第三方服务。这样突发流量被队列吸收,各组件也能按自己的节奏扩容。触发服务通知服务器 ×N水平扩展消息队列削峰缓冲数据库 / 缓存工作线程第三方服务调用 API取元数据投递事件拉取发送队列吸收突发;通知服务器无状态,可任意加机器
按 diagram-design 重绘

rd-10-data-loss

10. Notification System/images/data-loss.png · 可靠性:防止数据丢失

可靠性:防止数据丢失通知事件在进队列之前先写进通知日志库持久化,这样即使后续环节崩溃,事件也不会凭空消失。投递给第三方服务失败时,由重试机制重新投递,而不是直接把这条通知丢掉。通知服务器通知日志库持久化消息队列失败重试写日志投递重投先落库、再投递;失败走重试而不是丢弃事件 ID 去重:重复到达的同一事件直接丢弃
按 diagram-design 重绘

rd-10-events-tracking

10. Notification System/images/events-tracking.png · 事件跟踪与附加组件

事件跟踪与附加组件工作线程负责把事件发给第三方服务,第三方再投递给用户。围绕这条主链还有几个配套件:通知模板保证文案格式统一;通知设置记录用户对每个渠道的订阅与退订;限流给单个用户的接收频次封顶;事件跟踪回收打开率、点击率等指标,用来衡量效果并动态调整工作线程数量。通知模板预置格式通知设置订阅 / 退订工作线程第三方服务用户限流频次上限事件跟踪打开率 / 点击率渲染按订阅过滤发送投递回传指标打开率 / 点击率反哺
按 diagram-design 重绘

rd-10-updated-design

10. Notification System/images/updated-design.png · 通知系统完整流程

通知系统完整流程五步走:触发服务调用 API 提交通知;通知服务器校验请求并从缓存或数据库取回渲染所需的元数据;事件被投进消息队列;工作线程从队列拉取事件并调用第三方服务;第三方服务最终把通知投递给用户。整条链路异步、可水平扩展,每一段都能独立扩容和容错。触发服务通知服务器消息队列工作线程第三方服务数据库缓存调用 API入队拉取发送取元数据① 提交 → ② 校验取元数据 → ③ 入队 → ④ 异步发送 → ⑤ 投递每段都可独立扩容;队列负责削峰
按 diagram-design 重绘

rd-11-feed-publishing

11. News Feed System/images/feed-publishing.png · Feed 发布链路

Feed 发布链路用户经发布 API 发帖:负载均衡器把请求分给 Web 服务器,Web 服务器鉴权后转给 Post 服务;Post 服务把帖子写进数据库与缓存,再交给 Fanout 服务把新帖子推送到好友的 feed 缓存,同时由通知服务给好友发提醒。用户发布帖子负载均衡Web 服务器鉴权Post 服务存储帖子Fanout 服务写扩散好友 feed 缓存post ID 列表DB / 缓存通知服务发布 API分发转发扩散写入存帖子提醒写路径:发帖后异步扩散,好友刷新即可见只存 post_id → user_id 映射
按 diagram-design 重绘

rd-11-news-feed-building

11. News Feed System/images/news-feed-building.png · Feed 拉取链路

Feed 拉取链路用户请求自己的 feed:Web 服务器转给 News Feed 服务,服务先从 feed 缓存里取出一串 post ID(已按倒序排好),再按这些 ID 从内容缓存或数据库取回帖子详情,拼装后返回。用户刷新 feed负载均衡Web 服务器News Feed 服务feed 缓存post ID 列表DB / 缓存post 详情拉取 API分发转发取 ID取详情读路径:ID 先取、详情后补,拼装后返回
按 diagram-design 重绘

rd-11-feed-publishing-deep-dive

11. News Feed System/images/feed-publishing-deep-dive.png · 写扩散 vs 读扩散

写扩散 vs 读扩散三种扩散策略:写扩散在发帖时就把帖子推给所有好友的 feed,实时性好、读快,但好友多时写代价高;读扩散在刷新时才去拉好友的帖子,对不活跃用户省资源,但读慢;混合方案对绝大多数用户用写扩散,只对名人这类高连接用户改用读扩散。新帖子写扩散写时推送读扩散读时聚合混合方案普通人推送 / 名人拉取实时、读快;好友多则写代价高对不活跃用户省资源;但读慢绝大多数走推送,名人走拉取
按 diagram-design 重绘

rd-11-fanout-service

11. News Feed System/images/fanout-service.png · Fanout 服务的五步

Fanout 服务的五步Fanout 服务拆成五步:从图数据库取好友 ID;读用户设置过滤掉被屏蔽的好友;把过滤后的好友列表连同新 post ID 投进消息队列;工作线程从队列拉取并更新 feed 缓存;缓存里只追加近期 post ID,用可配置的上限控制内存。好友图取好友 ID缓存过滤排除屏蔽消息队列post + 好友Fanout 工作线程feed 缓存post_id → user_id好友 ID投递拉取追加五步异步串联;缓存上限只保留最近若干条存映射而非整对象,省内存
按 diagram-design 重绘

rd-11-cache-architecture

11. News Feed System/images/cache-architecture.png · 五层缓存架构

五层缓存架构读路径上缓存分五层:News Feed 缓存存 post ID 列表;内容缓存存帖子详情(热帖进热点缓存);社交图缓存存好友关系;行为缓存记点赞、回复、分享;计数缓存维护点赞数、回复数、粉丝数等计数。前两层直接服务 feed 拉取,后三层按需读取。客户端读请求News Feed 服务News Feed 缓存post ID 列表内容缓存post 详情社交图缓存好友关系行为缓存点赞 / 回复计数缓存点赞数 / 粉丝数请求读 ID读详情前两层直接服务拉取;后三层按需读取
按 diagram-design 重绘

rd-12-basic-design

12. Chat System/images/basic-design.png · 聊天基础设计

聊天基础设计发送方通过 HTTP 把消息发给 Chat 服务;Chat 服务先把消息落库保存,再把它转发给接收方。发送走 HTTP,接收侧需要一条能实时推送的通道。发送方发消息Chat 服务存储 + 转发接收方收消息消息消息发送用 HTTP;接收需要实时推送通道
按 diagram-design 重绘

rd-12-polling

12. Chat System/images/polling.png · 短轮询

短轮询客户端每隔固定时间就问一次服务器「有新消息吗」。服务器无论有没有消息都立刻返回,于是产生大量无效请求,浪费带宽与服务器资源。客户端定时轮询服务器立即响应有新消息?无,立即返回有新消息?无,立即返回服务器立即返回,多数轮询是空跑
按 diagram-design 重绘

rd-12-long-polling

12. Chat System/images/long-polling.png · 长轮询

长轮询客户端发一次请求后,服务器把连接挂起,直到真有新消息或超时才返回。相比短轮询少了很多空跑,但对不活跃用户仍会反复超时重连。客户端发起请求服务器挂起等待有新消息?有新消息 / 超时有新消息?连接超时连接挂起直到有新消息或超时
按 diagram-design 重绘

rd-12-websocket

12. Chat System/images/websocket.png · WebSocket

WebSocket客户端用一次 HTTP 握手(GET /ws)请求升级协议,服务器返回 101 确认后,连接升级为双向持久通道。此后消息可在两端随时互发,收发共用同一条连接。客户端发起握手服务器升级协议HTTP 握手101 确认双向消息握手后连接升级为双向持久连接
按 diagram-design 重绘

rd-12-heartbeat-mechanism

12. Chat System/images/heartbeat-mechanism.png · 心跳机制

心跳机制客户端周期性给在线状态服务器发心跳,证明自己还在线。只要持续收到心跳就维持在线;连续 30 秒收不到心跳,就把该用户标记为离线。客户端每 5s 心跳在线状态服务器维护在线状态心跳心跳心跳中断连续 30s 未收到心跳 → 标记离线
按 diagram-design 重绘

rd-12-fanout-presence

12. Chat System/images/fanout-presence.png · 在线状态扩散

在线状态扩散用发布订阅扩散在线状态:每对好友维护一个频道。用户 A 状态变化时,把事件发布到 A-B、A-C、A-D 三个频道,分别由 B、C、D 订阅,从而收到 A 的在线更新。这套设计适合小规模好友关系。在线状态服务器用户 A发布状态频道 A-B订阅频道频道 A-C频道 A-D用户 B用户 C用户 D发布发布发布订阅订阅订阅A 发布状态到 3 个好友频道
按 diagram-design 重绘

rd-12-group-chat-flow

12. Chat System/images/group-chat-flow.png · 群聊消息流程

群聊消息流程群消息在服务端被复制到每个收件人的收件箱(消息同步队列)。用户 A 发出的消息先到 Chat 服务器 1,再分别写入 B、C 的同步队列,各自从队列里拉取。这样同步简单,但群越大写扩散代价越高。用户 A发消息Chat 服务器 1复制到收件箱消息同步队列用户 B 收件箱消息同步队列用户 C 收件箱用户 B用户 C消息拉取拉取消息复制到每个收件人的收件箱
按 diagram-design 重绘

rd-12-group-chat-model

12. Chat System/images/group-chat.png · 群聊数据模型

群聊数据模型群聊消息表 group_message 用 (channel_id, message_id) 作为复合主键:channel_id 定位群,message_id 在群内单调递增,决定消息顺序。其余字段记录发送者、内容与时间。group_message(channel_id, message_id)复合主键:channel_id + message_id字段:user_id · content · created_at局部 ID 保证群内消息有序
按 diagram-design 重绘

rd-12-stateless-arch

12. Chat System/images/high-level-stateless-arch.png · 无状态服务

无状态服务无状态层负责注册、登录、资料管理等与连接无关的事:用户请求先经负载均衡,再分发到服务发现、鉴权、群组管理、用户资料等服务。它们不保存会话状态,因此可以随意水平扩容。无状态用户登录 / 注册负载均衡服务发现鉴权服务群组管理用户资料http负载均衡把请求分发到各无状态服务
按 diagram-design 重绘

rd-12-stateful-arch

12. Chat System/images/high-level-statefull-arch.png · 有状态服务

有状态服务有状态层的 Chat 服务与用户之间维持 WebSocket 长连接,负责消息投递与同步。用户离线时,Chat 服务转而调用第三方推送服务,把新消息通过 APNS / FCM 推给设备。有状态第三方用户 1长连接用户 2长连接Chat 服务维持 ws 连接推送通知wsws离线推送用户离线时改走推送通道
按 diagram-design 重绘

rd-12-high-level-design

12. Chat System/images/high-level-design.png · 聊天系统高层设计

聊天系统高层设计客户端与 Chat 服务器建立 WebSocket 长连接收发实时消息;HTTP 请求走负载均衡到 API 服务器处理登录、资料等;在线状态服务器维护在线状态;通知服务器负责离线推送;KV 存储保存聊天记录,便于水平扩展与低延迟读取。实时服务KV 存储(聊天记录)用户多设备负载均衡Chat 服务器ws 长连接在线状态服务器API 服务器登录 / 资料通知服务器离线推送KV 存储KV 存储KV 存储httpwshttp存储状态读写ws 收实时消息,http 走 API
按 diagram-design 重绘

rd-12-message-synchronization

12. Chat System/images/message-synchronization.png · 多设备消息同步

多设备消息同步同一个用户在手机和笔记本上各有一条会话,每台设备记录自己的 cur_max_message_id。Chat 服务器把「收件人是当前用户」且「消息 ID 大于该设备的 cur_max_message_id」的消息视为新消息,据此补齐各设备缺失的部分。A 的手机cur_max_message_id=653A 的笔记本cur_max_message_id=842Chat 服务器 1维护会话与设备KV 存储聊天记录推送新消息推送新消息读取消息每台设备各自记录已读到的最大消息 ID新消息 = 收件人是我 且 ID 超过设备游标
按 diagram-design 重绘

rd-12-one-to-one-model

12. Chat System/images/one-to-one-chat.png · 一对一数据模型

一对一数据模型一对一消息表 message 以 message_id 为主键,它单调递增,决定消息顺序。message_from、message_to 标明收发双方,content 存正文,created_at 记录时间。message主键 message_id主键 message_id 决定消息顺序字段:message_from · message_tocontent · created_at
按 diagram-design 重绘

rd-12-service-discovery

12. Chat System/images/zookeeper.png · 服务发现

服务发现用户登录(1)经负载均衡(2)到 API 服务器,API 服务器向服务发现(Zookeeper)(3)查询可用的 Chat 服务器;Zookeeper 按地理位置、容量等条件返回最优节点,用户再与该节点建立 WebSocket 长连接(4)。服务发现 (Zookeeper)用户 A登录负载均衡API 服务器Chat 服务器 1Chat 服务器 2Chat 服务器 2被选中1 登录2 转发3 查询4 ws 连接返回最优Zookeeper 按位置与容量推荐最优节点… N 台服务器
按 diagram-design 重绘

rd-13-basic-search-suggestions

13. Search Autocomplete/images/basic-search-suggestions.png · 输入 tw 弹出 top-5 建议

输入 tw 弹出 top-5 建议用户在搜索框里敲下「tw」,系统按查询热度(频次)实时返回最热门的 5 条补全建议:twitter、twitch、twilight、twin peak、twitch prime。这些候选都来自查询服务里预先建好的频次表与 trie,因此响应可以压到 100 毫秒以内。下拉建议 top 5搜索框输入 twtwitter频次 35twitch频次 29twilight频次 25twin peak频次 21twitch prime频次 18实时建议按查询热度排序,最多返回 5 条
按 diagram-design 重绘

rd-13-cached-trie

13. Search Autocomplete/images/cached-trie.png · 缓存版 trie:每个节点存 top-k

缓存版 trie:每个节点存 top-k为了不每次都遍历整棵子树,trie 的每个内部节点都预先缓存好自己子树里最热门的 top-k 列表(浅色节点)。查询命中前缀节点后直接读取缓存即可返回,省掉遍历与排序的开销;叶子节点上标注的是该完整查询的频次。rootb缓存 top-kw缓存 top-kbe缓存 top-kbu缓存 top-kwi缓存 top-kbee: 20bes缓存 top-kbet: 29buy: 14win: 11beer: 10best: 35浅色 = 已缓存 top-k 的内部节点;命中前缀后直接读缓存叶子标注该完整查询的频次,无需再排序
按 diagram-design 重绘

rd-13-data-gathering

13. Search Autocomplete/images/data-gathering.png · 数据采集:频次表随查询累积

数据采集:频次表随查询累积数据采集服务把用户的每一次查询累加进频次表:同样的查询再加一次,就把对应词频 +1;新出现的查询则新增一行。这张不断增长的频次表,正是每周离线重建 trie 的输入。query: —空频次表query: twitchtwitch:1query: twittertwitch:1, twitter:1query: twittertwitch:1, twitter:2query: twillotwitch:1, twitter:2, twillo:1每来一条查询就累加词频时间 → 频次表持续增长,是每周重建 trie 的输入
按 diagram-design 重绘

rd-13-data-gathering-flow

13. Search Autocomplete/images/data-gathering-flow.png · 数据采集流水线

数据采集流水线实时逐条更新 trie 不现实,于是改成离线流水线:分析日志追加写入原始查询;聚合器把日志汇总成频次表;工作线程异步按频次表重建 trie 并落盘到 Trie DB;每周再从 Trie DB 打一份快照灌进内存里的 Trie 缓存,供在线查询快速读取。分析日志原始查询聚合器日志 → 频次表聚合数据频次表工作线程异步重建 trieTrie DB持久化Trie 缓存内存加速采集聚合产出每周更新每周快照离线管道:日志按周聚合、异步重建 trieTrie 缓存在内存里服务在线查询;Trie DB 负责持久化
按 diagram-design 重绘

rd-13-delete-kv

13. Search Autocomplete/images/delete-kv.png · 删除:经过滤层移除不良建议

删除:经过滤层移除不良建议候选建议从 Trie 缓存取出后,先经过一层过滤层:它按可插拔的规则(例如仇恨言论、敏感词)拦掉不该出现的建议,再把干净的结果交给 API 服务器返回。被判定为不良的条目会异步从数据库中物理删除,不阻塞在线查询。API 服务器返回建议过滤层移除不良建议Trie 缓存候选建议候选过滤后过滤规则可插拔;不良建议异步物理删除过滤层让删除策略与查询逻辑解耦
按 diagram-design 重绘

rd-13-frequency-table

13. Search Autocomplete/images/frequency-table.png · 频次表:查询 → 热度

频次表:查询 → 热度频次表记录每条查询字符串被搜索了多少次,按热度降序排列:Twitter 35 次、twitch 29 次、twilight 25 次……查询服务就是拿这张表(以及由它构建的 trie)来回答 top-k 补全的。频次表(Query → Frequency)Twitter频次 35twitch频次 29twilight频次 25twin peak频次 21twitch prime频次 18twitter search频次 14twillo频次 10twin peak sf频次 8按热度降序:越热的查询排在越前
按 diagram-design 重绘

rd-13-sharding

13. Search Autocomplete/images/sharding.png · 分片:前缀范围路由到不同分片

分片:前缀范围路由到不同分片trie 按前缀范围切开分散到多台服务器(例如 a–m、n–z),前缀分布不均时再在内部细分(例如 aa–ag、ah–an)。Web 服务器先问分片映射管理器「这个前缀属于哪个分片」,再直接去对应的数据库分片取数据。数据库Web 服务器查询前缀分片映射管理器前缀 → 分片分片 1分片 2分片 n① 是哪个分片?② 从分片取数据① 先问分片映射管理器 ② 再去对应分片取数据前缀分布不均时,可在分片内继续细分
按 diagram-design 重绘

rd-13-trie-db

13. Search Autocomplete/images/trie-db.png · Trie DB:前缀映射为键值对

Trie DB:前缀映射为键值对除了整树序列化存进文档数据库,trie 还可以拆成键值存储:trie 里的每一个前缀都是一个 key,该节点上的数据(它的 top-k 列表)就是 value。于是「查一个前缀的补全」就退化成一次哈希表查询,非常快。前缀树(trie)键值存储(Key → Value)bbebeebeerbesbestb[be:15, bee:20, beer:10, best:35]be[be:15, bee:20, beer:10, best:35]bes[best:35]bee[bee:20, beer:10]best[best:35]beer[beer:10]每个前缀 = 一个 key;节点数据 = value查前缀 → 一次哈希查询即可拿到 top-k
按 diagram-design 重绘

rd-13-trie-structure

13. Search Autocomplete/images/trie-structure.png · Trie 结构:取 top-k 的三步

Trie 结构:取 top-k 的三步trie 是一棵前缀树,边上的字符拼出查询串,每个叶子节点带一个频次(如 try: 29、win: 50)。取 top-k 分三步:先按输入找到前缀节点,再遍历该节点的子树收集所有合法补全,最后按频次排序取前 k 个。roottwtrtowitretrutry频次 29toy频次 14wiswin频次 50tree频次 10true频次 35wish频次 25① 找前缀节点 → ② 遍历子树收集补全 → ③ 按频次排序取 top-k深色叶子 = 一个完整查询,数字为它的热度
按 diagram-design 重绘

rd-14-high-level-design

14. Youtube/images/high-level-design.png · 高层设计

高层设计客户端(手机、电脑、TV)连到两类后端:视频播放走 CDN,由边缘节点就近分发;上传、改元数据等其余交互走 API 服务器。CDN 只负责流媒体,API 服务器不碰视频流。客户端手机 · 电脑 · TVCDNAPI 服务器流媒体其他请求视频走 CDN,其余交互走 API
按 diagram-design 重绘

rd-14-video-uploading-flow

14. Youtube/images/video-uploading-flow.png · 视频上传流程

视频上传流程上传与元数据更新并行:视频 [1] 直传原始存储,[2] 由转码服务器转成多种格式,完成后分两路——[3a] 转码结果写入转码后存储并 [3a.1] 分发到 CDN;[3b] 完成事件进完成队列,[3b.1] 完成处理器更新元数据缓存与 DB 并通知用户。另一路元数据请求经 [4] 负载均衡到 API 服务器读写元数据。用户上传视频原始存储负载均衡转码服务器API 服务器完成队列转码后存储元数据缓存元数据库完成处理器CDN1 上传4 元数据2 转码3a3b 完成3b.13a.14 分发更新缓存更新 DB上传与元数据更新并行进行完成处理器更新元数据并通知用户
按 diagram-design 重绘

rd-14-metadata-upload

14. Youtube/images/metadata-upload.png · 元数据上传

元数据上传客户端并行提交元数据更新请求(文件名、大小、格式等):请求经负载均衡分给 API 服务器,API 服务器再并行写入元数据缓存与元数据库。用户更新元数据负载均衡API 服务器元数据缓存元数据库更新请求分发写缓存写 DBAPI 服务器并行写缓存与 DB
按 diagram-design 重绘

rd-14-video-streaming-flow

14. Youtube/images/video-streaming-flow.png · 视频播放流程

视频播放流程播放请求直达 CDN:视频从 CDN 的边缘服务器直接流式传输,尽量缩短延迟。常用协议有 MPEG-DASH、Apple HLS 与 Adobe HDS,不同协议支持的编码与播放器各异。客户端手机 · 电脑 · TVCDN流媒体CDN 边缘节点就近分发,降低延迟协议:MPEG-DASH / HLS / HDS
按 diagram-design 重绘

rd-14-dag-video-transcoding

14. Youtube/images/dag-video-transcoding.png · DAG 视频转码

DAG 视频转码原始视频先拆成视频、音频、元数据三路。视频分支进入任务集:检查、视频转码、缩略图、水印等任务并行执行;音频分支做音频编码。视频转码与音频编码的结果汇入「组装」,拼成最终文件。DAG 让各任务高度并行。任务原始视频视频音频元数据检查视频转码缩略图音频编码水印组装转码缩略图编码视频音频原始视频拆成 视频 / 音频 / 元数据任务:检查 / 视频转码 / 缩略图 / 音频编码 / 水印
按 diagram-design 重绘

rd-14-video-transcoding-architecture

14. Youtube/images/video-transcoding-architecture.png · 视频转码架构

视频转码架构转码流水线:预处理器按 GOP 拆分并生成 DAG,同时把 GOP 与元数据写入临时存储以便失败重试;DAG 调度器把任务分阶段入队;资源管理器分配最优任务与 worker;任务 worker 执行转码;最终产出编码后视频。预处理器DAG 调度器资源管理器任务 worker编码后视频临时存储调度入队分配输出GOP 与元数据中间数据预处理器按 GOP 拆分并生成 DAG,中间数据存临时存储
按 diagram-design 重绘

rd-14-dag-config

14. Youtube/images/dag-config.png · DAG 配置

DAG 配置DAG 由客户端程序员写的配置文件生成。每个任务声明名称、类型、输入与输出:第一个任务 download-input 从 config.url 拉取输入并写入 context;第二个任务 transcode 读取上游输出、按配置转码,再用 next 串到下一个任务。任务配置download-input任务配置transcodenexttype=Download,输入取 config.url输出写入 context.inputVideotype=Transcode,读上游输出输出写入 context.file
按 diagram-design 重绘

rd-14-dag-scheduler

14. Youtube/images/dag-scheduler.png · DAG 调度器

DAG 调度器DAG 调度器把 DAG 拆成分阶段的任务并放入资源管理器的任务队列。Stage 1 拆出视频、音频、元数据;Stage 2 把视频进一步拆成视频编码与缩略图,音频走音频编码。同一阶段内的任务可并行。DAG 调度器原始视频视频音频元数据视频编码缩略图音频编码拆分编码编码Stage 1Stage 2
按 diagram-design 重绘

rd-14-resource-manager

14. Youtube/images/resource-manager.png · 资源管理器

资源管理器资源管理器管理资源分配效率,含三个队列与一个任务调度器:任务队列(优先级队列,待执行任务)、Worker 队列(优先级队列,worker 利用率)、运行队列(正在运行的任务与 worker)。调度器从队列取最优任务与 worker,再指令选中的 task worker 执行。资源管理器任务 worker任务队列优先级Worker 队列利用率运行队列运行中任务调度器水印编码器缩略图合并器取任务取 worker回写状态执行调度器从三个队列中挑选任务与 worker…
按 diagram-design 重绘

rd-14-task-worker

14. Youtube/images/task-worker.png · 任务 worker

任务 worker任务 worker 负责执行转码及其他操作。不同类型的 worker 跑不同任务:水印、编码、缩略图、合并等,同类 worker 可水平扩展以提升并行度。任务 worker水印编码器缩略图合并器…
按 diagram-design 重绘

rd-14-video-split

14. Youtube/images/video-split.png · 视频切分

视频切分为加速上传并支持断点续传,长视频按 GOP 对齐切成小块:原始视频被切成 GOP 1、GOP 2 … GOP N,各块可独立并行上传,失败只需重传对应分片。GOP 分片原始视频GOP 1GOP 2GOP N按 GOP 对齐切分长视频按 GOP 对齐切成小块,便于并行与续传…
按 diagram-design 重绘

rd-14-message-queue1

14. Youtube/images/message-queue1.png · 消息队列(一)

消息队列(一)用消息队列把模块解耦以获得高并行度:原始存储的数据入队后由下载模块拉取,下载完成再入队由编码模块拉取,编码完成继续入队交给下游。各模块独立扩缩容。原始存储消息队列下载模块消息队列编码模块消息队列入队拉取入队拉取入队模块间用消息队列解耦,实现高并行
按 diagram-design 重绘

rd-14-message-queue2

14. Youtube/images/message-queue2.png · 消息队列(二)

消息队列(二)上传模块把任务入队,编码侧从消息队列拉取,编码后的视频写入编码后存储,再上传到 CDN。队列把上传与编码两段流水线解耦,各自独立扩展。上传模块消息队列编码后存储CDN入队拉取上传到 CDN编码完成后经消息队列写入存储并上传 CDN
按 diagram-design 重绘

rd-14-pres-signed-urls

14. Youtube/images/pres-signed-urls.png · 预签名 URL

预签名 URL用预签名 URL 限制上传只对授权用户开放:[1] 客户端先 POST /upload 请求上传;[2] API 服务器返回一个预签名 URL;[3] 客户端凭该 URL 直接把视频上传到原始存储,全程不经 API 服务器转发。原始存储用户API 服务器1 POST /upload2 预签名 URL3 上传视频1 请求上传 → 2 返回预签名 URL → 3 直传原始存储预签名 URL 限制上传仅授权用户可用
按 diagram-design 重绘

rd-15-namespaces

15. Google Drive/images/namespaces.png · 命名空间与目录展开

命名空间与目录展开根目录 drive/ 下为每个用户建一个命名空间,命名空间内是各自上传的文件与文件夹。文件或文件夹由「命名空间 + 相对路径」唯一确定,例如 user1 命名空间下的 recipes/chicken_soup.txt。命名空间drive/命名空间根目录user1recipes/chicken_soup.txtuser2football.mov / sports.txtuser3best_pic_ever.png展开根目录 drive/ 下按用户划分命名空间文件 = 命名空间 + 相对路径,全局唯一
按 diagram-design 重绘

rd-15-replication

15. Google Drive/images/replication.png · 同区域与跨区域复制

同区域与跨区域复制对象存储的副本策略分两种:同区域复制把多个副本放在同一地域,写入延迟低、可用性高,但整个地域故障时一起丢失;跨区域复制把副本放到另一个地域,可抗地域级故障,代价是复制延迟更高。同区域复制区域 A区域 BBucket副本副本副本Bucket副本副本副本复制复制同区域:多副本同地域,延迟低跨区域:副本跨地域,可容灾但延迟高
按 diagram-design 重绘

rd-15-sync-conflicts

15. Google Drive/images/sync-conflicts.png · 同步冲突的三种结局

同步冲突的三种结局两个用户同时修改同一文件时:先被处理的版本生效并回传;落后的那一端收到冲突。系统会把本地副本与服务端最新版本都摆出来,由用户选择合并,或用其中一个覆盖另一个。① 同时编辑② 先到者生效③ 冲突提示User 1User 2我们的系统User 1User 2我们的系统User 1User 2我们的系统编辑编辑已同步已同步冲突user1、user2 同时改同一文件user1 先到,更新生效user2 收到冲突,可合并或覆盖
按 diagram-design 重绘

rd-15-high-level-design

15. Google Drive/images/high-level-design.png · Google Drive 高层架构

Google Drive 高层架构用户经浏览器或移动端接入:文件走块服务器切成 4 MB 块后存入云存储,冷数据下沉到冷存储;非上传类流程走负载均衡器到 API 服务器。API 服务器读写元数据 DB 与缓存,并通过通知服务把变更推给客户端,离线客户端由备份队列暂存。用户块服务器4 MB 分块负载均衡器云存储块存储API 服务器非上传流程通知服务发布订阅离线备份队列离线补同步冷存储归档元数据 DB / 缓存用户 / 文件 / 块上传 / 下载文件上传长轮询存储块冷归档分发读写块元数据变更通知离线订阅文件切成 4 MB 块,按哈希去重存储非上传流程由 API 服务器处理
按 diagram-design 重绘

rd-15-metadata-database

15. Google Drive/images/metadata-database.png · 元数据数据库表关系

元数据数据库表关系高度简化的四类核心表:user 拥有多个 workspace,workspace 下挂多个 file;file 有多个 file_version 记录修订历史,file_version 再拆成多个 block;device 记录登录设备,其上传的版本也指向 file_version。user 表user_id · user_nameworkspace 表id · owner_id · is_sharedfile 表id · file_name · checksumblock 表block_id · file_version_idfile_version 表id · file_id · versiondevice 表device_id · user_id拥有包含版本分块设备上传高度简化:仅列关键表与字段file_version 记每次修订,block 指向其版本
按 diagram-design 重绘

rd-15-upload-flow

15. Google Drive/images/upload-flow.png · 文件上传流程

文件上传流程上传分两步:客户端先把元数据交给 API 服务器,落库为 pending 状态;再把文件块经块服务器存入云存储。云存储完成后回调 API 服务器把状态改为 uploaded,并由通知服务把变更推给相关客户端。客户端上传方API 服务器写元数据元数据 DBpending 状态块服务器切块 + 压缩云存储S3 存储块通知服务长轮询元数据pending上传文件存块回调通知变更先传元数据(pending),再传块云存储回调后状态改为 uploaded
按 diagram-design 重绘

rd-15-download-flow

15. Google Drive/images/download-flow.png · 文件下载流程

文件下载流程文件在别处被改动时,在线客户端由通知服务经长轮询得知,离线客户端上线后自行拉取。客户端先经 API 服务器取回最新元数据,再向块服务器下载变更的块,块由块服务器从云存储取回,客户端按序重建文件。通知服务长轮询客户端重建文件API 服务器查元数据元数据 DB变更记录块服务器取块云存储块存储通知变更拉取变更查元数据下载块取块被通知后:先拉元数据,再下载变更块块由云存储返回,客户端按序重建文件
按 diagram-design 重绘

rd-15-delta-sync

15. Google Drive/images/delta-sync.png · 增量同步(Delta Sync)

增量同步(Delta Sync)文件切成等长块后各自算哈希。同步时逐块比对,只传输哈希发生变化的块,其余块直接跳过,从而大幅节省带宽。图中 Block 2 与 Block 5 变化,只有它们被上传到云存储。块服务器云存储Block 1Block 2Block 3Block 4Block 5Block 6Block 7Block 8Block 9Block 10Block 2Block 5仅传变更块文件切成等长块,各块独立哈希仅上传哈希变化的块Block 2 与 Block 5 变更,其余块跳过
按 diagram-design 重绘

rd-15-file-sync

15. Google Drive/images/file-sync.png · 文件切块同步链路

文件切块同步链路客户端把文件切分成块,每块依次经过压缩与加密,再上传到云存储。压缩按文件类型选用不同算法,加密保证传输与静态数据安全;所有块上传后按特定顺序拼回原文件。文件原始文件Block 1Block 2Block N压缩压缩压缩加密加密加密云存储块存储压缩压缩压缩加密加密加密文件 → 切块 → 压缩 → 加密 → 云存储压缩按文件类型选算法加密后的块上传到云存储
按 diagram-design 重绘

rd-16-high-level-design

16. Proximity Service/images/high-level-design.png · 高层设计:LBS 与 Business 服务

高层设计:LBS 与 Business 服务系统分成两部分:LBS 处理「按位置找附近商家」,是纯读、无状态、可水平扩展的服务;Business 服务处理商家主的增删改和顾客查看详情。负载均衡把两类请求分派到对应服务,两者共用一套主从数据库集群——读走副本、写走主库。商家信息不要求实时,副本的轻微延迟可以接受。数据库集群用户负载均衡LBS 服务搜索附近Business 服务增删改查ReplicaReplicaPrimary搜索请求转发商家接口读读写复制复制两部分:LBS 做定位搜索(读多),Business 服务做商家增删改查(写少)主从复制:读副本、写主库
按 diagram-design 重绘

rd-16-2d-search

16. Proximity Service/images/2d-search.png · 朴素做法:二维圆范围搜索

朴素做法:二维圆范围搜索最直观的做法是以用户位置为圆心、给定半径画一个圆,取圆内所有商家。SQL 上用 latitude / longitude 各加一个 BETWEEN 条件即可,但数据库的一维索引只能加速一个维度,实际仍要扫描大量行。半径越大、数据越多,代价越高。搜索半径内的商家我餐厅加油站咖啡馆酒店以我为中心画半径 R 的圆,圆内即候选结果问题:lat / long 各建一维索引仍不够,圆范围查询要扫大量行
按 diagram-design 重绘

rd-16-geospatial-index-types

16. Proximity Service/images/geospatial-index-types.png · 空间索引的两大类

空间索引的两大类把二维数据压成一维索引有两条路:哈希类(均匀网格、GeoHash、笛卡尔分层)把空间切成固定或半固定的格子并编码;树类(四叉树、Google S2、R 树)按数据分布递归划分空间。本章重点讲 GeoHash 与四叉树两种。索引哈希树均匀网格Geohash笛卡尔分层四叉树Google S2R 树哈希类切固定格子,树类按密度递归划分
按 diagram-design 重绘

rd-16-even-grid

16. Proximity Service/images/even-grid.png · 均匀网格:固定边长

均匀网格:固定边长把世界切成固定边长的格子,每个格子存落在其中的商家。做法简单,但商家分布并不均匀:城市中心一个格子塞不下,乡村一个格子大片空置。格子大小无法随人口密度自适应,这是均匀网格最大的短板。低密度中密度低密度中密度高密度格子过载中密度低密度中密度低密度均匀网格:全球切成固定边长的格子问题:城市格子过载、乡村格子空置,无法按密度自适应
按 diagram-design 重绘

rd-16-geohash

16. Proximity Service/images/geohash.png · Geohash:四象限编码

Geohash:四象限编码先沿本初子午线和赤道把行星切成四个象限,每格再四分为四,如此递归。每个格子用交替的经度位、纬度位编码成字符串:左上为 01、右上为 11、左下为 00、右下为 10。前缀相同即代表落在同一个大格内。01西北11东北00西南10东南沿本初子午线与赤道切成四象限,每格再四分(-180, 90)(180, 90)(-180, -90)(180, -90)
按 diagram-design 重绘

rd-16-geohash-1

16. Proximity Service/images/geohash-1.png · Geohash:继续四分的 4×4 网格

Geohash:继续四分的 4×4 网格把每个象限再四分成四,就得到 4×4 的 16 个格子,每格用两位编码(纬度位 + 经度位)。继续细分下去,编码串越来越长、格子越来越小。Geohash 的核心保证是:两个 geohash 共享的前缀越长,两点越近。01 0101 1111 0111 1101 0001 1011 0011 1000 0100 1110 0110 1100 0000 1010 0010 10每格再四分,用两位编码(纬度位 + 经度位)共享前缀越长 → 两点越近,这是 geohash 的核心保证
按 diagram-design 重绘

rd-16-geohash-radius-mapping

16. Proximity Service/images/geohash-radius-mapping.png · 半径与 geohash 长度对照

半径与 geohash 长度对照geohash 有 12 级精度,搜索时按半径选最短够用的编码长度。半径越小,需要的精度越高、编码越长:0.5 km 用长度 6,1~2 km 用 5,5~20 km 用 4。查表选长度可以避免精度过高带来的多余格子。搜索半径Geohash 长度0.5 km61 km52 km55 km420 km4按半径选最短够用的 geohash 长度半径越小,编码越长
按 diagram-design 重绘

rd-16-boundary-issue

16. Proximity Service/images/boundary-issue.png · 边界问题与相邻格

边界问题与相邻格紧贴格边的两家店可能被分到不同 geohash,前缀完全不同却近在咫尺;反之共享长前缀的两点也可能分属不同格。解决办法是把本格连同周围 8 个相邻格一起查,再按真实距离过滤,避免漏掉边界上的商家。西北北东北西我的格子搜索中心东西南南东南边界问题:紧贴格边的店可能落到相邻格解法:连同 8 个相邻格一起查,再按真实距离过滤
按 diagram-design 重绘

rd-16-quadtree

16. Proximity Service/images/quadtree.png · 四叉树:递归四分

四叉树:递归四分四叉树把一块方形区域分成四个象限,哪个象限里的商家数超过阈值,就继续把它四分,直到每格都不超过上限(本例 100 家)。左图是根格 200m,右图是商家最密的 NE 象限继续细分。密度高的地方格子自然更小。根节点 · 200m 见方NE 继续四分NW 40mNE 30mSW 70mSE 60m10m11m4m5m再四分四叉树:格子内商家数超过阈值就继续四分递归直到每格不超过 100 家
按 diagram-design 重绘

rd-16-building-quadtree

16. Proximity Service/images/building-quadtree.png · 构建四叉树:自顶向下分裂

构建四叉树:自顶向下分裂四叉树是一棵树:每个内部节点恰好有四个孩子,对应空间的四个子区域(nw / ne / sw / se),节点上的数字是该区域的商家数。根 200m 分成 40m / 30m / 70m / 60m 四个象限;30m 象限再分成 12m / 4m / 5m / 9m;最密的 4m 象限继续分,直到叶子节点(50 / 90 / 60 / 60 / 80 家)都不超过 100。图中只展开了通向 140 的那条分支,其余分支省略。200m40m30m70m60m12m4m5m9m5090601406080每个内部节点恰好有 4 个孩子,对应 nw / ne / sw / se 四个象限节点数字是该区域的商家数;白底为内部节点(继续分裂),浅底为叶子(最终格子)
按 diagram-design 重绘

rd-16-realworld-quadtree

16. Proximity Service/images/realworld-quadtree.png · 真实世界:自适应网格

真实世界:自适应网格真实城市里,四叉树长成密度自适应的样子:Boulder、Aurora、Parker 这些郊区格子很大,而 Denver 市中心被切成许多小格。切分只到「每格 ≤ 100 家」为止,因此密集处自然更深、稀疏处更浅,索引规模也不会失控。科罗拉多 · 自适应网格Denver 市中心 · 继续细分BoulderAuroraGoldenParker格 1格 2格 3格 4格子随密度自适应:郊区格子大,市中心格子小只切到每格 ≤ 100 家,密集处自然更深
按 diagram-design 重绘

rd-16-hilbert-curve

16. Proximity Service/images/hilbert-curve.png · Hilbert 曲线:二维映射到一维

Hilbert 曲线:二维映射到一维Google S2 用 Hilbert 曲线把球面网格铺成一条一维曲线,给每个格子一个序号。曲线的关键性质是保局部性:在二维上彼此靠近的店(店 B、店 C),映射到一维后序号也相邻(0.50、0.58)。于是二维的「找附近」问题可以当成一维的范围查询来做。二维平面(店铺位置)一维 Hilbert 序号店 A店 B店 C店 D0.420.500.580.66Hilbert 曲线把二维网格铺成一条线二维相邻的点,一维序号也相邻 → 可用一维范围查询找附近商家
按 diagram-design 重绘

rd-16-geofence

16. Proximity Service/images/geofence.png · 地理围栏与 S2 覆盖

地理围栏与 S2 覆盖地理围栏是一块任意形状的区域。S2 用 Hilbert 曲线把球面切成可变精度的格子,用若干粗细不同的格子拼出这块围栏——边缘处用细格贴合形状,内部用粗格节省数量。还能指定 min / max level 和最大格子数来控制覆盖精度与开销。覆盖围栏的 S2 单元格S2 格 · 粗S2 格 · 粗S2 格 · 粗S2 格 · 粗S2 格 · 细S2 格 · 细地理围栏区域S2 用 Hilbert 曲线把球面切成可变精度的格子能覆盖任意形状;可指定 min / max level 与最大格数
按 diagram-design 重绘

rd-16-final-design

16. Proximity Service/images/final-design.png · 最终架构:检索附近商家的六步

最终架构:检索附近商家的六步用户把经纬度和半径发给负载均衡器,LB 转给 LBS;LBS 按半径算出 geohash 长度(500m → 6),再算出本格与 8 个相邻格,并行查 Geohash Redis 拿到商家 ID 列表,再查 Business Info Redis 取详情,按距离排序后返回。Business 服务只负责商家增删改,写入主库后异步复制到副本。Redis 集群数据库集群 · 主从复制用户搜索附近负载均衡LBS 服务算 geohashBusiness 服务商家详情Geohash RedisBusiness InfoReplicaReplicaPrimary① 请求② 转发/businesses⑤ 取 ID⑥ 取详情写入复制复制① 用户请求 → ② 转发给 LBS③④ 算 geohash 与 8 个邻居 → ⑤⑥ 并行查 Redis → 按距离排序返回
按 diagram-design 重绘

rd-17-fan-out-backend

17. Nearby Friends/images/fan-out-backend.png · 后端作为扇出中枢

后端作为扇出中枢移动端不直连好友(P2P 在弱网与省电约束下不现实),而是把位置更新发给共享后端;后端收到后找出所有应收到该更新的在线好友,把位置转发给他们。转发前先比较距离,超出配置阈值的直接丢弃。用户上报位置后端扇出好友 A好友 B好友 C位置更新后端把位置更新扇出给距离阈值内的好友
按 diagram-design 重绘

rd-17-simple-high-level-design

17. Nearby Friends/images/simple-high-level-design.png · 简单高层设计

简单高层设计移动用户经负载均衡器接入:位置类实时流量走 WebSocket 长连接,好友与资料管理等辅助请求走 HTTP 的 API 服务器。WebSocket 服务器把位置写入 Redis 位置缓存(带 TTL)与位置历史库,并通过 Redis Pub/Sub 广播;API 服务器读写用户库。移动用户位置更新负载均衡WebSocket 服务器双向位置流API 服务器好友 / 资料Redis Pub/Sub频道广播位置缓存最新位置位置历史库写入历史用户库资料 / 好友WS / http转发http发布写缓存写历史读资料读写位置走 WS 长连接,管理走 httpTTL 过期即视为离线
按 diagram-design 重绘

rd-17-redis-pubsub-usage

17. Nearby Friends/images/redis-pubsub-usage.png · Redis Pub/Sub 的用法

Redis Pub/Sub 的用法发布者把位置更新写进对应用户的频道,Redis Pub/Sub 再把消息广播给该频道的所有订阅者——即负责这些好友的 WebSocket 服务器。一个用户一个频道,频道在无人订阅时不占内存。用户发布位置更新Redis Pub/Sub好友发布者 1发布者 2用户 1 的频道用户 2 的频道订阅者 1订阅者 2订阅者 3发布发布推送推送每个用户一个频道,发布后广播给所有订阅者
按 diagram-design 重绘

rd-17-periodic-location-update

17. Nearby Friends/images/periodic-location-update.png · 周期性位置更新的写路径

周期性位置更新的写路径① 移动端把位置更新发给负载均衡器;② 负载均衡器转给该客户端的 WebSocket 长连接;③ WebSocket 服务器把位置写入位置历史库;④ 更新位置缓存并在内存中留一份用于后续距离计算;⑤ 通过 Redis Pub/Sub 把位置发布到该用户的频道。移动用户位置更新负载均衡WebSocket 服务器处理与扩散位置历史库位置缓存Redis Pub/Sub① 上报② 转发③ 写历史④ 写缓存⑤ 发布⑥ Pub/Sub 广播回订阅的 WS 服务器,⑦ 再推给附近好友
按 diagram-design 重绘

rd-17-detailed-periodic-location-update

17. Nearby Friends/images/detailed-periodic-location-update.png · 详细版:发布 / 订阅与好友推送

详细版:发布 / 订阅与好友推送① 用户 1 与用户 5 把位置上报到各自的 WS 连接;② WS 连接把位置发布到各自在 Redis Pub/Sub 里的频道;③ 频道把消息广播给订阅了它的 WS 连接(用户 1 的频道被用户 2/3/4 的连接订阅,用户 5 的频道被用户 4/6 的连接订阅);④ 各连接再把好友位置更新推给对应的移动端。WebSocket 服务器Redis Pub/SubWebSocket 服务器用户 1上报位置用户 5用户 1 的 WS 连接用户 5 的 WS 连接用户 1 的频道用户 5 的频道用户 2 的 WS 连接用户 3 的 WS 连接用户 4 的 WS 连接用户 6 的 WS 连接用户 2用户 3用户 4用户 6① 上报位置② 发布③ 订阅④ 推给好友
按 diagram-design 重绘

rd-17-channel-distribution-data

17. Nearby Friends/images/channel-distribution-data.png · 频道分布数据

频道分布数据服务发现组件(zookeeper / etcd)里存着一份哈希环数据:它记录每个 Redis Pub/Sub 服务器负责哪一段哈希区间。WebSocket 服务器把这份数据拉到本地缓存,就能直接算出某个频道落在哪台服务器上。哈希环(展开)p_4p_1p_2p_3频道名hash(key)hash(channel)服务器 p_1 负责该区间的哈希值p_x: Redis Pub/Sub 服务器 X
按 diagram-design 重绘

rd-17-consistent-hashing

17. Nearby Friends/images/consistent-hashing.png · 一致性哈希:扩容只迁移一小段

一致性哈希:扩容只迁移一小段给 Redis Pub/Sub 集群加一台服务器 p_1_new 时,用一致性哈希只需把「p_4 与 p_1_new 之间」那一段区间的频道重新映射过去,其余频道原地不动。相比取模,扩容触发的重新订阅请求被压到最小。扩容前扩容后p_4p_1p_2p_3p_4p_1_newp_2p_3新增 →受影响:p_4 与 p_1 之间的频道迁移到新节点
按 diagram-design 重绘

rd-17-geohash-pubsub

17. Nearby Friends/images/geohash-pubsub.png · 按 geohash 建频道

按 geohash 建频道为了支持「附近随机的人」,把地图按 geohash 切成网格,每个格子对应一个 Redis Pub/Sub 频道。落在同一格里的用户订阅同一个频道,就能收到格内其他人的位置更新。地图网格Redis Pub/Sub9q8zn61 用户9q8znd3 用户9q8znf1 用户9q8zn31 用户Geohash: 9q8zn6Geohash: 9q8zndGeohash: 9q8znfGeohash: 9q8zn3地图按 geohash 切格,每格对应一个频道→
按 diagram-design 重绘

rd-17-location-updates-geohash

17. Nearby Friends/images/location-updates-geohash.png · geohash 频道的位置更新流

geohash 频道的位置更新流用户 2 的位置更新经 WebSocket 连接上报(①),发布到它所在 geohash(9q8znd)对应的频道;该频道再把更新广播给所有订阅者(②)——即 geohash 9q8znd 内其他用户的连接,由它们推给各自的移动端。WebSocket 连接Redis Pub/Subgeohash 9q8znd 内的用户用户 2Geohash: 9q8zn6Geohash: 9q8zndGeohash: 9q8znfGeohash: 9q8zn3用户 1用户 2用户 3① 位置更新② 订阅按 geohash 建频道,边界附近多订阅相邻格
按 diagram-design 重绘

rd-17-geohash-borders

17. Nearby Friends/images/geohash-borders.png · geohash 边界:多订阅相邻格

geohash 边界:多订阅相邻格用户若正好处在格子边界附近,只订阅本格频道会漏掉紧邻格里的好友。做法是同时订阅相邻的 geohash 频道,最多覆盖周围 3×3 个格子,从而补齐跨格的好友位置。geohash 网格9q8zn69q8znd9q8znf9q8zn39q8zn99q8znc9q8zn29q8zn89q8znb用户靠近边界时,同时订阅相邻 geohash 频道最多覆盖周围 3×3 个格子
按 diagram-design 重绘

rd-18-high-level-design

18. Google Maps/images/high-level-design.png · Google Maps 高层设计

Google Maps 高层设计移动端把请求交给负载均衡器,再分流到导航服务与位置服务:导航服务读地理编码 DB 与对象存储里的路由瓦片,位置服务把用户位置写进位置 DB;地图瓦片则由移动端直接从 CDN 拉取,CDN 回源到预计算瓦片存储。移动端发起请求CDN边缘缓存预计算瓦片预计算瓦片负载均衡导航服务路径规划位置服务位置上报地理编码 DB地理编码路由瓦片对象存储用户位置 DBCassandra请求下载瓦片回源路由分流地址写入地图瓦片静态分发,导航与位置走服务端回源生成,长期缓存
按 diagram-design 重绘

rd-18-location-service

18. Google Maps/images/location-service.png · 位置服务链路

位置服务链路位置服务负责记录用户的位置更新:移动端每隔 t 秒上报一次位置,经负载均衡器进入位置服务,最终写进为高写入量优化的位置数据库。移动端上报位置负载均衡位置服务用户位置 DBCassandra上报转发写入优先可用性:位置数据很快过期,允许最终一致
按 diagram-design 重绘

rd-18-location-service-diagram

18. Google Maps/images/location-service-diagram.png · 位置服务数据写入

位置服务数据写入深挖位置服务的数据库设计:移动端批量上报的位置更新经负载均衡器进入位置服务,由位置服务写入 NoSQL 位置库;用 user_id 分区、timestamp 聚簇,天然适配写多读少的负载。移动端批量上报负载均衡位置服务用户位置 DBCassandra上报转发写入user_id 为分区键,timestamp 为聚簇键
按 diagram-design 重绘

rd-18-static-map-tiles

18. Google Maps/images/static-map-tiles.png · 静态地图瓦片分发

静态地图瓦片分发把整个世界存成一张大图不可行,瓦片按需拉取。瓦片不动态生成,而是按 geohash 预先算好、静态存放在对象存储里,再由 CDN 就近分发给移动端。移动端请求瓦片CDN预计算瓦片对象存储拉取回源按 geohash 静态寻址,客户端可自行计算 URL
按 diagram-design 重绘

rd-18-cdn-vs-no-cdn

18. Google Maps/images/cdn-vs-no-cdn.png · 有无 CDN 的延迟对比

有无 CDN 的延迟对比没有 CDN 时,用户直接连源站,往返延迟约 300 ms。有了 CDN,源站把瓦片同步到各地 POP(接入点),用户从最近的 POP 取瓦片,延迟降到约 10 ms。无 CDN有 CDN源站用户源站POPPOPPOP用户300 ms10 ms就近接入:POP 越近,首屏瓦片越快
按 diagram-design 重绘

rd-18-location-update-batches

18. Google Maps/images/location-update-batches.png · 客户端批量上报位置

客户端批量上报位置与其每次定位都单独发请求,不如在客户端把位置按 15 秒打包成一批,再整体上报。批次越小越实时,越大越省电省流量,15 秒是常见的折中。客户端批次 1批次 2批次 3loc 15…loc 2loc 1loc 30…loc 17loc 16loc 45…loc 32loc 31客户端每 15s 把位置打包成一批上报
按 diagram-design 重绘

rd-18-location-update-streaming

18. Google Maps/images/location-update-streaming.png · 位置更新的流式下游

位置更新的流式下游位置服务除了写位置库,还把更新投进 Kafka。多个下游各自消费:路况更新服务维护路况 DB、机器学习服务做个性化、瓦片处理服务更新路由瓦片、分析服务写入分析 DB。下游消费方移动端上报位置负载均衡位置服务用户位置 DBCassandraKafka流式缓冲路况更新服务路况 DBML 个性化服务个性化 DB瓦片处理服务路由瓦片分析服务分析 DB投递一路写入、一路流式外送,互不阻塞
按 diagram-design 重绘

rd-18-navigation-service

18. Google Maps/images/navigation-service.png · 导航服务内部组件

导航服务内部组件导航服务先把地址交给地理编码服务解析成经纬度,再转给路线规划服务。规划服务并行调用排序服务(按用户偏好过滤,如避开收费路)、最短路服务(在对象存储的路由瓦片上跑 A* 变体)和 ETA 服务(读路况 DB 预测时间);路况更新服务异步刷新路况 DB 与位置 DB。移动端发起导航导航服务地理编码服务地理编码 DB地理编码路线规划服务排序服务过滤服务最短路服务路由瓦片对象存储ETA 服务路况 DB路况更新服务位置 DBCassandra请求规划地址查询排序路径ETA过滤取瓦片路况更新写入地址 → 坐标 → 路线,三段式;允许少量延迟但要求准确
按 diagram-design 重绘

rd-18-final-design

18. Google Maps/images/final-design.png · 最终设计:自适应 ETA 与重规划

最终设计:自适应 ETA 与重规划在导航服务的基础上加入「自适应 ETA 与重规划」:它订阅路况 DB 的变化,并维护一张活跃用户表(记录每位导航中用户将要经过的各级瓦片)。一旦某块瓦片发生事故,就能立刻找出受影响的用户并下发新路线。移动端发起导航导航服务地理编码服务地理编码 DB地理编码路线规划服务排序服务过滤服务最短路服务路由瓦片对象存储ETA 服务路况 DB路况更新服务位置 DBCassandra自适应 ETA / 重规划自适应重规划活跃用户活跃用户请求规划订阅地址查询排序路径ETA过滤取瓦片路况更新写入读路况重规划新增:按瓦片索引导航中的用户,事故即重规划只存各级祖先瓦片,检查终点瓦片即可
按 diagram-design 重绘

rd-18-partitioning-system

18. Google Maps/images/partitioning-system.png · 球面定位系统

球面定位系统地球是一个自转的球体,位置由两个角度定义:纬度表示南北(±90°),经度表示东西(±180°)。两者合起来给出球面上唯一的一个点。地球球体经纬度lat / long定位纬度:南北 ±90°;经度:东西 ±180°例:40°N, 70°E
按 diagram-design 重绘

rd-18-map-projections

18. Google Maps/images/map-projections.png · 常见地图投影

常见地图投影把 3D 球面摊到 2D 平面叫地图投影,几乎没有不失真的做法,差别只在牺牲什么。墨卡托保角(放大两极)、高尔-彼得斯保面积、皮尔斯五方与温克尔三倍则试图折中,各有取舍。墨卡托投影皮尔斯五方投影高尔-彼得斯投影温克尔三倍投影保角 / 保面积 / 折中,几乎都会产生形变Google 地图采用 Web Mercator(墨卡托改良版)
按 diagram-design 重绘

rd-18-road-representation

18. Google Maps/images/road-representation.png · 路网抽象成图

路网抽象成图导航算法把交叉口看作节点、道路看作边,整张路网就变成一张加权图。绝大多数导航算法都是 Dijkstra 或 A* 的变体,在图上跑最短路。路网图交叉口交叉口交叉口交叉口交叉口交叉口交叉口 = 节点,道路 = 边在图上跑 Dijkstra / A* 的变体求最短路
按 diagram-design 重绘

rd-18-routing-tiles

18. Google Maps/images/routing-tiles.png · 路由瓦片与邻居引用

路由瓦片与邻居引用整张世界路网无法一次性载入,于是像切地图一样把路网切成一块块路由瓦片。每块瓦片只保存自己那一小片路网,并持有相邻瓦片的引用;算法可以按需把瓦片拼成更大的路网。路由瓦片 1路由瓦片 2路由瓦片 3节点节点节点节点节点节点节点节点节点节点节点节点每块瓦片是一小张路网图瓦片持有邻居引用,算法按需拼接成更大路网
按 diagram-design 重绘

rd-18-map-routing-hierarchical

18. Google Maps/images/map-routing-hierarchical.png · 分级路由瓦片

分级路由瓦片长途路线如果全用最细的瓦片拼接,依然很耗时间和内存。因此路由瓦片也分不同细节层级:离目的地越远用越粗的瓦片,越近用越细的瓦片,算法按需逐级细化。粗粒度瓦片:跨城中粒度瓦片:城区细粒度瓦片:街区当前瓦片目的地越近,使用的瓦片越细按需逐级细化,显著降低内存带宽
按 diagram-design 重绘

rd-18-shortest-path-service

18. Google Maps/images/shortest-path-service.png · 最短路服务遍历瓦片

最短路服务遍历瓦片最短路服务接收起点/终点,转成经纬度后推出 geohash,定位到对应的路由瓦片。算法从起点瓦片开始,沿邻居引用一块块遍历,直到找到一条通往终点瓦片的足够好的路径。路由瓦片瓦片 1瓦片 2瓦片 3瓦片 4起点瓦片 → 沿邻居引用逐块遍历 → 终点瓦片A* 变体在拼接出的大图上求最短路起点终点
按 diagram-design 重绘

rd-18-precomputed-map-tile-image

18. Google Maps/images/precomputed-map-tile-image.png · 预计算的地图瓦片

预计算的地图瓦片地图瓦片不动态生成,而是离线批量渲染好、切成一张张 256×256 的图片,再推送到 CDN 静态存放。客户端只要按 geohash 拼出 URL,就能就近拉到对应瓦片。离线渲染批量渲染地图瓦片256 × 256CDN静态分发渲染上传离线算好、静态分发,避免动态生成的服务器压力相似的瓦片(如沙漠)可大幅压缩
按 diagram-design 重绘

rd-18-zoom-level-increases

18. Google Maps/images/zoom-level-increases.png · 缩放级与瓦片数量

缩放级与瓦片数量地图瓦片按缩放级分层存放。最低缩放级用一张 256×256 瓦片表示整个世界;缩放级每升一级,瓦片数就翻两番(×4):1 → 4 → 16。客户端根据当前缩放级挑对应的瓦片。缩放级 0 · 1 张缩放级 1 · 4 张缩放级 2 · 16 张缩放级每升一级,瓦片数 ×41 张 · 4 张 · 16 张,按当前缩放级选择
按 diagram-design 重绘

rd-18-geohashing

18. Google Maps/images/geohashing.png · geohash 递归四分

geohash 递归四分geohash 把世界展开成平面后递归地切成四个象限,每个象限用两个字符(01 / 11 / 00 / 10)标记。再往下一级,就对选中象限继续四分,并把这两位追加到字符串后面,于是越精确的格子字符串越长。第 1 级:世界 → 4 象限第 2 级:象限 11 再分0111001011 0111 1111 0011 10世界展开成平面,递归切成四象限每分一级,geohash 字符串追加 2 位
按 diagram-design 重绘

rd-18-map-tile-url-calculation

18. Google Maps/images/map-tile-url-calculation.png · 地图瓦片 URL 的计算方式

地图瓦片 URL 的计算方式确定瓦片有两种做法:客户端自己按 geohash 算 URL(省一次请求,但一旦定下格式就很难改),或让地图瓦片服务代算(多一次 API 调用,但格式可随时调整)。拿到 URL 后,客户端再从 CDN 下载瓦片。移动端请求瓦片负载均衡地图瓦片服务③ 构造 URLCDN边缘缓存① 取 URL② 转发④ 下载瓦片两种做法:客户端自算,或服务端代算客户端自算省一次请求,但要长期兼容旧格式
按 diagram-design 重绘

rd-18-user-location-data-torw

18. Google Maps/images/user-location-data-torw.png · 位置更新的行结构

位置更新的行结构用户位置数据写多读少,适合 Cassandra。一行记录一次位置更新:user_id、timestamp、用户状态、出行方式与经纬度。user_id 做分区键,timestamp 做聚簇键,同一用户的位置按时间有序存放。user_idtimestampuser_modedriving_modelocation1011635740977activedriving(20.0, 30.5)一行 = 一次位置更新user_id 分区键 + timestamp 聚簇键
按 diagram-design 重绘

rd-18-user-location-row-example

18. Google Maps/images/user-location-row-example.png · 位置行示例(含导航模式)

位置行示例(含导航模式)实际存下来的行还会带上导航模式:key(user_id)、timestamp、lat、long、user_mode 与 navigation_mode。这样既知道用户在哪儿,也知道他正在开车、走路还是骑行。key (user_id)timestamplatlonguser_modenavigation_mode5113205300021.989.8activedriving一行 = 一次位置更新user_id 分区键 + timestamp 聚簇键,写多读少
按 diagram-design 重绘

rd-18-adaptive-eta-data-storage

18. Google Maps/images/adaptive-eta-data-storage.png · 自适应 ETA 的瓦片存储

自适应 ETA 的瓦片存储为了在事故发生时快速找出受影响的用户,系统按瓦片索引导航中的用户,而不是存下整条路线。每个用户只存起点瓦片以及它的各级祖先瓦片(范围每级扩大 4 倍),一路存到包含终点的层级;只要检查最后那块瓦片是否命中事故瓦片即可。层级 0层级 1层级 2起点起点起点终点层级 0:只含起点瓦片层级 1:范围扩大 4 倍层级 2:同时含起点与终点只存各级祖先瓦片,检查终点瓦片即可
按 diagram-design 重绘

rd-19-message-queue-components

19. Distributed Message Queue/images/message-queue-components.png · 消息队列的核心组件

消息队列的核心组件生产者把消息发到消息队列,消费者订阅队列并消费消息。队列居中解耦两端,让生产者和消费者可以各自独立伸缩——两者都是客户端,消息队列是服务端。生产者消息队列消费者生产消费订阅生产者与消费者互不感知,只与队列交互
按 diagram-design 重绘

rd-19-point-to-point-model

19. Distributed Message Queue/images/point-to-point-model.png · 点对点模型

点对点模型消息发到队列后只会被一个消费者消费:即使有多个消费者,同一条消息也仅投递给其中一个;一旦被确认消费即从队列移除,因此传统点对点模型没有数据保留。生产者消息队列消费者 1消费者 2生产消息 A同一条消息只投递给一个消费者确认消费后即从队列移除
按 diagram-design 重绘

rd-19-publish-subscribe-model

19. Distributed Message Queue/images/publish-subscribe-model.png · 发布订阅模型

发布订阅模型消息关联到某个 topic,订阅该 topic 的每个消费者都会收到全部消息。与点对点不同,同一条消息会被广播给所有订阅者,这是事件流平台更常见的模型。生产者消息队列消费者 1消费者 2发布消息 A消息 A订阅同一 topic 的消费者都收到消息
按 diagram-design 重绘

rd-19-partitions

19. Distributed Message Queue/images/partitions.png · Topic 拆分为分区

Topic 拆分为分区当单个 topic 的数据量过大时,可以把它拆成多个分区来横向扩展。发往 topic 的消息被均匀分到各分区,每个分区像一条 FIFO 队列,分区内保持消息顺序;承载分区的服务器称为 broker。Topic-A分区 1Partition-1Topic-APartition-2Topic-A均匀分布分区内 FIFO;消息位置即 offset
按 diagram-design 重绘

rd-19-consumer-groups

19. Distributed Message Queue/images/consumer-groups.png · 消费组

消费组消费组是一组协同消费同一 topic 的消费者。消息按消费组(而非按消费者)复制,每个组各自维护 offset;组内每个分区只允许一个消费者订阅,因此并行读能提升吞吐,同时保留分区内的顺序。消费组 1消费组 2Partition-1Topic-APartition-2Topic-APartition-1Topic-B消费者 1消费者 2消费者 3消费者 4订阅组内一个分区只被一个消费者订阅
按 diagram-design 重绘

rd-19-high-level-architecture

19. Distributed Message Queue/images/high-level-architecture.png · 消息队列高层架构

消息队列高层架构生产者把消息推给指定 topic,消费组订阅并消费。Broker 持有多个分区,消息持久化在数据存储里,消费状态单独存放;元数据存储保存配置与 topic 属性,协调服务负责服务发现(哪些 broker 存活)与 leader 选举。生产者Brokers分区 + 副本消费者数据存储消息分段状态存储消费偏移元数据存储配置 / Topic协调服务发现 / 选举生产消费写消息存偏移协调服务负责发现与选举
按 diagram-design 重绘

rd-19-wal-example

19. Distributed Message Queue/images/wal-example.png · WAL 分段存储

WAL 分段存储分区用只追加的 write-ahead log 落盘,并拆成多个 segment 以免单个文件过大。旧 segment 只读,只有最新的 segment 接收写入;顺序追加对机械硬盘极其友好,再叠加操作系统页缓存即可获得可观吞吐。segment-1(只读)segment-2(可写)0123456789Topic-A Partition-1:offset 从 0 顺序追加写满后滚动出新 segment,旧段转只读
按 diagram-design 重绘

rd-19-message-structure

19. Distributed Message Queue/images/message-structure.png · 消息结构

消息结构生产、队列、消费者三方共用同一套 schema 才能避免额外拷贝。key 决定消息落到哪个分区(如 hash(key) % numPartitions),value 是负载;topic、partition、offset 三者可唯一定位一条消息,timestamp、size、crc 则用于时序、计量与完整性校验。keybyte[]valuebyte[]topicstringpartitionintegeroffsetlongtimestamplongsizeintegercrcintegerkey 决定分区;key 无需唯一,也可缺省topic + partition + offset 唯一定位一条消息
按 diagram-design 重绘

rd-19-routing-layer

19. Distributed Message Queue/images/routing-layer.png · 独立路由层

独立路由层路由层从元数据存储读取副本分布计划并缓存,生产者把消息发给它,它再转发给该分区的 leader 副本。从副本向 leader 拉取新消息,确认足够多后 leader 提交并回 ack;代价是多了一跳网络往返,且不利于批量发送。生产者路由层Broker-1Topic-A P1 · leaderBroker-2Topic-A P1 · follower生产转发给 leader复制拉取leader 提交后向生产者返回 ack
按 diagram-design 重绘

rd-19-routing-layer-producer

19. Distributed Message Queue/images/routing-layer-producer.png · 路由层内嵌到生产者

路由层内嵌到生产者把路由层搬进生产者可以省掉一跳、降低延迟,也让生产者自己决定消息进哪个分区。生产者内的缓冲区把消息在内存里攒成更大的批次再一次性发出,从而提升吞吐。生产者缓冲区批量攒消息路由层按分区路由Broker-1Topic-A P1 · leaderBroker-2Topic-A P1 · follower路由到 leader复制少一跳、可自行控分区、可攒批
按 diagram-design 重绘

rd-19-batch-size-throughput-vs-latency

19. Distributed Message Queue/images/batch-size-throughput-vs-latency.png · 批量大小的取舍

批量大小的取舍批量是吞吐与延迟之间的经典权衡:批量越大,攒批等待越久、延迟越高,但单次请求摊薄了网络往返、吞吐越高;批量越小,请求发得越早、延迟越低,吞吐也随之下降。按传统消息队列场景调小批量,按日志聚合场景调大批量。小批量低延迟 · 低吞吐大批量高延迟 · 高吞吐批量增大批量是吞吐与延迟的经典权衡追求吞吐时需更多分区来补偿顺序写
按 diagram-design 重绘

rd-19-consumer-example

19. Distributed Message Queue/images/consumer-example.png · 消费者按 offset 拉取

消费者按 offset 拉取消费者在分区里指定自己的 offset,从该位置开始接收一批消息。示例中消费者 1 已消费到 offset 6、消费者 2 已消费到 offset 13;读取位置由消费者自己维护,因此可以回放或跳过。Topic-A Partition-1offset 0–15消费者 1last offset = 6消费者 2last offset = 13拉取拉取offset 由消费者自己维护,可回放
按 diagram-design 重绘

rd-19-consumer-flow

19. Distributed Message Queue/images/consumer-flow.png · 消费者接入流程

消费者接入流程新消费者订阅 topic A 并加入 group-1;协调者由 group 名哈希定位,因此同组消费者都连到同一个 broker。协调者确认加入并把分区 2 分配给该消费者,消费者从上次 offset 拉取、处理,再把 offset 提交回 broker——这两步的先后决定了投递语义。消费者新加入Broker-1group-1 协调者Broker-2Topic-A Partition-21. 加入组2. 分配分区3. 拉取消息4. 处理消息后再提交 offset
按 diagram-design 重绘

rd-19-consumer-rebalancing

19. Distributed Message Queue/images/consumer-rebalancing.png · 消费者重平衡

消费者重平衡重平衡决定哪个消费者负责哪些分区,在消费者加入/离开或分区增删时触发。同组消费者都连到同一个协调者(按 group 名哈希得到);成员变化时协调者选出新的组 leader,由 leader 算出分区分配方案,经协调者广播给其余消费者。消费者 1Group-1消费者 2Group-1消费者 3Group-1消费者 4Group-2Broker-1Group-1 协调者Broker-2Group-2 协调者心跳协调者 = 按 group 名哈希选出的 broker
按 diagram-design 重绘

rd-19-consumer-rebalance-example

19. Distributed Message Queue/images/consumer-rebalance-example.png · 分区减少触发重平衡

分区减少触发重平衡分区数量变化会触发重平衡:左边 4 个分区对应组内 4 个消费者,分区减少到 3 个后,消费者列表也要相应收敛,每个分区仍只被一个消费者读取。消费组消费组P1P2P3P4消费者 1消费者 2消费者 3消费者 4P1P2P3消费者 1消费者 2消费者 3分区数 4 → 3重平衡收敛消费者
按 diagram-design 重绘

rd-19-consumer-join-group-usecase

19. Distributed Message Queue/images/consumer-join-group-usecase.png · 消费者加入组的时序

消费者加入组的时序起初只有消费者 A 在组内、消费全部分区。B 请求加入后,协调者借心跳通知组内成员被动重平衡;成员重新加入后协调者选出 leader 并广播结果,leader 生成分区分配方案经协调者下发,随后各消费者开始消费新分到的分区。消费者 ACoordinator消费者 B1a 心跳3b 请求重平衡4a 重新加入2 加入组4b 成为 leader5 分发方案同一组消费者都连到同一个协调者
按 diagram-design 重绘

rd-19-consumer-leaves-group-usecase

19. Distributed Message Queue/images/consumer-leaves-group-usecase.png · 消费者离开组的时序

消费者离开组的时序消费者 A、B 同组。B 请求退出后,协调者在收到 A 的心跳时告知需要重平衡;其余步骤与加入组一致——重新加入、选 leader、下发新的分区方案。消费者 ACoordinator消费者 B1a 心跳2a 退出组2b 确认退出3a 心跳3b 请求重平衡5 分发方案离开组同样经心跳触发被动重平衡
按 diagram-design 重绘

rd-19-consumer-no-heartbeat-usecase

19. Distributed Message Queue/images/consumer-no-heartbeat-usecase.png · 消费者失联的时序

消费者失联的时序协调者长时间收不到某消费者的心跳,就判定它已失联并触发重平衡,把它的分区交给组内其他消费者。处理流程与离开组一致。消费者 ACoordinator消费者 B1a 心跳1b 心跳确认2a 心跳2b 心跳确认3b 请求重平衡6 分发方案A 长时间无心跳 → 判定失联,触发重平衡
按 diagram-design 重绘

rd-19-state-storage

19. Distributed Message Queue/images/state-storage.png · 状态存储

状态存储状态存储保存分区与消费者的映射,以及每个分区最后消费到的 offset。示例中 group-1 停在 offset 6、group-2 停在 offset 13;消费者崩溃后,新消费者就从该位置继续,不会漏读也不会重读已提交的部分。Topic-A Partition-1offset 0–15Group-1offset = 6Group-2offset = 13读到 6读到 13读写频繁但量小,一致性要求高
按 diagram-design 重绘

rd-19-zookeeper

19. Distributed Message Queue/images/zookeeper.png · ZooKeeper 统一承载元数据与状态

ZooKeeper 统一承载元数据与状态ZooKeeper 是分层键值存储,常被用作分布式配置、同步服务与命名注册(服务发现)。把元数据存储、状态存储与协调服务一并交给它之后,broker 只需维护消息数据本身,副本的 leader 选举也由 ZooKeeper 协助完成。ZooKeeper元数据存储Metadata状态存储State协调服务CoordinationBrokers只存消息数据生产者消费者偏移状态选举生产消费配置、状态、协调统一托管
按 diagram-design 重绘

rd-19-replication-example

19. Distributed Message Queue/images/replication-example.png · 分区复制

分区复制每个分区被复制到多个 broker,但只有一个 leader 副本。生产者把消息发给 leader,副本向 leader 拉取;当足够多的副本同步完成后 leader 才向生产者返回确认。副本的分布方案称为副本分布计划,由 leader 生成并写入 ZooKeeper。生产者Broker-1Topic-A P1 · leaderBroker-2副本 · followerBroker-3副本 · follower生产复制复制每个分区只有一个 leader,其余为副本
按 diagram-design 重绘

rd-19-in-sync-replicas-example

19. Distributed Message Queue/images/in-sync-replicas-example.png · 同步副本(ISR)

同步副本(ISR)已提交 offset 为 13,leader 又写入两条尚未提交的消息。一条消息只有在 ISR 内所有副本都同步后才算提交;replica-2、replica-3 已完全追上 leader 属于 ISR,replica-4 落后太多暂时被移出 ISR。ISR 是性能与持久性之间的权衡。replica-1leader · 10–15replica-2follower · 10–14replica-3follower · 10–13replica-4follower · 10–11 落后fetchfetch未追上committed offset = 13ISR: {replica-1, replica-2, replica-3}
按 diagram-design 重绘

rd-19-ack-all

19. Distributed Message Queue/images/ack-all.png · ACK=all

ACK=allACK=all 要求 ISR 内所有副本都同步该消息后 leader 才回 ack:发送慢,但持久性最高。示例中 ISR 为 {replica-1, replica-2},replica-3 尚未追上。生产者Broker-1replica-1 · followerBroker-2replica-2 · leaderBroker-3replica-3 · 落后1. 生产2. fetch4. ack未追上ISR: {replica-1, replica-2}, ack=all
按 diagram-design 重绘

rd-19-ack-1

19. Distributed Message Queue/images/ack-1.png · ACK=1

ACK=1ACK=1 下 leader 一收到消息就回 ack:发送快,但持久性偏低——若 leader 在副本同步前宕机,消息可能丢失。示例 ISR 为 {replica-1, replica-2},replica-3 未追上。生产者Broker-1replica-1 · followerBroker-2replica-2 · leaderBroker-3replica-3 · 落后1. 生产fetch2. ack未追上ISR: {replica-1, replica-2}, ack=1
按 diagram-design 重绘

rd-19-ack-0

19. Distributed Message Queue/images/ack-0.png · ACK=0

ACK=0ACK=0 下生产者发出消息后完全不等待 leader 的确认:发送最快,持久性最低,消息可能在网络或 leader 侧丢失而生产者无感知。生产者Broker-1replica-1 · followerBroker-2replica-2 · leaderBroker-3replica-3 · 落后生产(不等 ack)fetch未追上ISR: {replica-1, replica-2}, ack=0
按 diagram-design 重绘

rd-19-broker-failure-recovery

19. Distributed Message Queue/images/broker-failure-recovery.png · Broker 故障恢复

Broker 故障恢复某个 broker 故障后,只要还有足够副本就不会丢分区数据:协调者选出新的 leader,并把故障 broker 上的分区重分配给其余副本;这些副本先以 follower 身份追赶,追上后再进入 ISR。Broker-1接管 P2Broker-2接管 P1Broker-3故障下线Broker-4接管 P3迁移迁移故障 broker 的分区重分配到其余副本新 leader 由协调者选举产生
按 diagram-design 重绘

rd-19-broker-replica-redistribution

19. Distributed Message Queue/images/broker-replica-redistribution.png · 新增 Broker 后的副本重分配

新增 Broker 后的副本重分配加入新 broker 时可以临时容忍副本数超过配置,等新 broker 追上后再删掉多余的副本。示例中 Topic-A Partition-2 从 Broker-1 复制到新增的 Broker-4,追平后移除 Broker-1 上的冗余副本。Broker-1P1, P2Broker-2P1, P2Broker-3P1, P2Broker-4新增 · 追平中副本迁移追平后移除不再需要的冗余副本临时允许副本数超过配置
按 diagram-design 重绘

rd-19-partition-example

19. Distributed Message Queue/images/partition-exmaple.png · 新增分区

新增分区新增分区时,旧分区的历史消息不迁移,只有新消息会写入新分区:生产者收到通知、消费者触发重平衡后即可感知新分区。这样避免了拷贝全量历史数据的高昂代价。partition-1持久消息partition-2持久消息partition-1旧 + 新消息partition-2旧 + 新消息partition-3仅新消息原样保留原样保留历史数据不迁移,只写入新分区生产者收到通知,消费者触发重平衡
按 diagram-design 重绘

rd-19-partition-decrease

19. Distributed Message Queue/images/partition-decrease.png · 减少分区

减少分区分区退役后,新消息只发往剩余分区,但退役分区不会立刻删除,消费者仍可继续读;等预配置的保留期过去再截断数据、释放存储。过渡期内生产者只写活跃分区,消费者则从全部分区读取。partition-1保留partition-2保留partition-3退役partition-1旧 + 新partition-2旧 + 新partition-3只读 · 待截断停止写入保留期到期后截断并释放存储
按 diagram-design 重绘

rd-19-at-most-once

19. Distributed Message Queue/images/at-most-once.png · 至多一次(at-most-once)

至多一次(at-most-once)消息至多被投递一次,也可能完全丢失。生产者异步发送、失败不重试;消费者取到消息后立即提交 offset,若在真正处理前崩溃,这条消息就再也不会被处理。生产者消息队列消费者可能丢消息可能丢消息不重试;消费后立即提交 offset
按 diagram-design 重绘

rd-19-at-least-once

19. Distributed Message Queue/images/at-least-once.png · 至少一次(at-least-once)

至少一次(at-least-once)消息不会漏处理,但可能重复投递。生产者用 ack=1 或 ack=all 发送、出错就重试;消费者处理完才提交 offset,因此若在处理后、提交前崩溃,这条消息会被再次投递——适合可去重或允许重复的场景。生产者消息队列消费者可能重复可能重复失败重试;处理完才提交 offset
按 diagram-design 重绘

rd-19-exactly-once

19. Distributed Message Queue/images/exactly-once.png · 恰好一次(exactly-once)

恰好一次(exactly-once)消息有且仅有一次,对使用者最友好,但实现代价极高:需要生产者幂等、事务提交与消费者侧的精确一次语义配合,通常只在关键链路才开启。生产者消息队列消费者恰好一次恰好一次实现代价高,通常按需开启
按 diagram-design 重绘

rd-19-message-filtering

19. Distributed Message Queue/images/message-filtering.png · 消息过滤

消息过滤同一个分区里的消息可能只有一部分是某消费者关心的。与其为每种需求单独建 topic(浪费存储、还让生产者与消费者耦合),不如给消息打 tag,消费者只订阅自己关心的 tag,由 broker 侧过滤,避免把无关消息推给消费者。BrokerM1tag:AM2tag:BM3tag:AM4tag:B标签过滤Tag filter消费者拉取订阅 tag:A按 tag 过滤,减少消费者无效流量
按 diagram-design 重绘

rd-19-delayed-message-implementation

19. Distributed Message Queue/images/delayed-message-implementation.png · 延迟消息实现

延迟消息实现延迟或定时消息先写进 broker 内的临时存储,到点再搬进目标分区供消费者读取。临时存储可以是一个或多个特殊 topic,计时则由专用延迟队列或分层时间轮实现。Broker生产者临时存储延迟消息Topic-A Partition-1即时消息延迟消息即时消息到时投递临时存储可为特殊 topic;计时用时间轮
按 diagram-design 重绘

rd-20-core-components

20. Metrics Monitoring and Alerting System/images/metrics-monitoring-core-components.png · 五大核心组件

五大核心组件指标监控与告警系统由五个核心部分组成:数据采集、数据传输、数据存储、告警、可视化。前两者负责把指标从各处汇集进来,数据存储按时间序列组织落盘,告警分析数据并触发通知,可视化把结果画成图表。指标监控与告警系统五大核心组件① 数据采集② 数据传输③ 数据存储④ 告警⑤ 可视化五个环节串成一条链路:采集 → 传输 → 存储 → 告警 / 可视化
按 diagram-design 重绘

rd-20-metrics-example-1

20. Metrics Monitoring and Alerting System/images/metrics-example-1.png · 指标查询示例:单点取值

指标查询示例:单点取值查询「生产机 i631 在 20:00 的 CPU 负载」会返回一条时间序列。曲线在 0.313 附近波动,其中 20:00 这个采样点的值是 0.29,图中用气泡标注出来。avg of cpu.load.i631折线图18:00≈0.3119:00≈0.3120:000.2921:00≈0.3100:30≈0.63该点 0.29曲线示意:数值在 0.313 附近波动y 轴 Percent:0 → 0.626x 轴:时间 18:00 → 次日 00:30
按 diagram-design 重绘

rd-20-metrics-example-1-data

20. Metrics Monitoring and Alerting System/images/metrics-example-1-data.png · 一个数据点的四个字段

一个数据点的四个字段上例取到的那个点可以拆成四个字段:指标名 cpu.load、标签 host:i631 与 env:prod、Unix 时间戳 1613707265、以及该时刻的值 0.29。指标名 + 标签确定是哪条时序,时间戳 + 值确定是哪个采样点。指标名cpu.load标签host:i631, env:prod时间戳1613707265值0.29指标名 + 标签 → 定位一条时序标签用于切分与聚合Unix 时间戳(秒)该时刻的采样值
按 diagram-design 重绘

rd-20-time-series-data-example

20. Metrics Monitoring and Alerting System/images/time-series-data-example.png · 一条时序由什么组成

一条时序由什么组成每条时间序列包含三部分:一个指标名(字符串);一组标签(key:value 列表);以及一串值与时间戳(value, timestamp 对)。这三者共同定义了一条时序及其历史数据。指标名String标签集合List of <key:value>值与时间戳数组Array of <value, timestamp>一条时序 = 指标名 + 标签 + 值序列唯一标识一条时序决定查询与聚合的维度按时间排列的历史采样
按 diagram-design 重绘

rd-20-time-series-data-viz

20. Metrics Monitoring and Alerting System/images/time-series-data-viz.png · 时序数据的可视化方式

时序数据的可视化方式把时序数据画成散点:x 轴是时间,y 轴是要查询的维度(指标名、标签等)。每个点是一次采样;高亮区域是查询「servicepool=s1 且 method=GET、跨所有机器、时间 t4 到 t7」命中的点集。散点示意:x 轴时间 t1…t9,y 轴时序名 / 标签时序 m2machinename:m2时序 m1machinename:m1查询结果t4 – t7每条时序 = 一组标签的唯一组合查询:servicepool=s1 且 method=GET,跨所有机器,时间 t4–t7
按 diagram-design 重绘

rd-20-influxdb-scale

20. Metrics Monitoring and Alerting System/images/influxdb-scale.png · InfluxDB 规模参考

InfluxDB 规模参考时序库的容量随规格变化:2–4 核 2–4 GB 可支撑每秒不到 5,000 次写入;4–6 核 8–32 GB 约 25 万次;8 核 32 GB 以上可超过每秒 25 万次写入、100 万条唯一序列。2–4 核 / 2–4 GB500 IOPS · <5,000 写入/秒 · <5 查询/秒 · <100,000 序列4–6 核 / 8–32 GB500–1,000 IOPS · <250,000 写入/秒 · <25 查询/秒 · <1,000,000 序列8+ 核 / 32+ GB1,000+ IOPS · >250,000 写入/秒 · >25 查询/秒 · >1,000,000 序列不同规格下的写入 / 查询能力(InfluxDB 实测)小规模中等规模8 核 32GB 可达 25 万次写入/秒
按 diagram-design 重绘

rd-20-high-level-design

20. Metrics Monitoring and Alerting System/images/high-level-design.png · 高层设计

高层设计指标源把数据交给采集器,采集器写入时序数据库;查询服务封装对数据库的读取,向上同时服务告警系统和可视化系统;告警系统按规则产生告警并投递到邮件、短信、PagerDuty、HTTPS 回调等通道。指标源服务器 / DB / 队列指标采集器采集并写入时序数据库按标签索引查询服务查询接口告警系统规则与去重可视化系统仪表盘邮件短信PagerDutyHTTPS 回调发送查询发送查询采集 → 存储 → 查询:告警与可视化都经查询服务读取数据
按 diagram-design 重绘

rd-20-metrics-collection

20. Metrics Monitoring and Alerting System/images/metrics-collection.png · 采集阶段:客户端发了就不管

采集阶段:客户端发了就不管采集阶段对偶发的数据丢失并不敏感,客户端可以「发了就不管」。图中虚线框标出的是采集范围:指标源到采集器这一段,采集器把数据写进时序库后交给查询服务。采集范围指标源服务器 / DB / 队列指标采集器采集并写入时序数据库按标签索引查询服务查询接口告警系统规则与去重可视化系统仪表盘邮件短信PagerDutyHTTPS 回调发送查询发送查询采集容忍少量丢失:客户端可以「发了就不管」
按 diagram-design 重绘

rd-20-transmission-pipeline

20. Metrics Monitoring and Alerting System/images/metrics-transmission-pipeline.png · 扩展指标传输管线

扩展指标传输管线无论推模型还是拉模型,采集器都部署在自动扩缩容组里。图中虚线框标出的是传输段:采集器把指标写进时序库;若时序库故障则可能丢数据,因此后面还要引入排队机制兜底。传输段(自动扩缩容)指标源服务器 / DB / 队列指标采集器采集并写入时序数据库按标签索引查询服务查询接口告警系统规则与去重可视化系统仪表盘邮件短信PagerDutyHTTPS 回调发送查询发送查询采集器放进自动扩缩容组;时序库故障时需要兜底
按 diagram-design 重绘

rd-20-pull-model-example

20. Metrics Monitoring and Alerting System/images/pull-model-example.png · 拉模型:采集器主动拉取

拉模型:采集器主动拉取拉模型下,采集器按固定间隔主动向各类指标源(Web 服务器、DB 集群、队列集群、缓存集群)发起 HTTP 拉取;它需要一份随时更新的目标清单,因此要先从服务发现(etcd / Zookeeper)拿到地址与拉取参数。指标源服务发现Web 服务器DB 集群队列集群缓存集群etcdZookeeper指标采集器采集并写入拉取指标拉取指标拉取指标拉取指标获取目标列表先取目标清单,再按间隔逐个拉取
按 diagram-design 重绘

rd-20-push-model-example

20. Metrics Monitoring and Alerting System/images/push-model-example.png · 推模型:服务主动上报

推模型:服务主动上报推模型下由各类指标源主动把指标推给采集器;采集器同样从服务发现读取配置。因为推请求可能被采集器拒绝,采集器需要放到负载均衡器后面的自动扩缩容组里,才能扛住突发流量。指标源服务发现Web 服务器DB 集群队列集群缓存集群etcdZookeeper指标采集器采集并写入推送指标推送指标推送指标推送指标获取目标列表采集器需置于负载均衡后的自动扩缩容组
按 diagram-design 重绘

rd-20-service-discovery-example

20. Metrics Monitoring and Alerting System/images/service-discovery-example.png · 服务发现里的采集配置

服务发现里的采集配置服务发现(etcd / Zookeeper)保存着「从哪采、何时采」的规则。每条服务注册信息包含区域、类型与主机地址等字段,采集器据此决定拉取哪些端点、间隔多久、超时与重试怎么设。服务发现etcdZookeeperregion: us-westregion: us-southregion: us-easttype: mysql · host: 10.10.11.1配置规则规则含拉取间隔、IP、超时与重试参数每条注册信息:region / type / host
按 diagram-design 重绘

rd-20-collection-flow

20. Metrics Monitoring and Alerting System/images/metrics-collection-flow.png · 采集流程:发现目标 + HTTP 拉取

采集流程:发现目标 + HTTP 拉取采集分两步:① 采集器从服务发现获取目标配置,拿到拉取间隔、IP、超时与重试参数;② 采集器按配置向应用服务器的 /metrics 端点发起 HTTP 请求,取回指标数据。端点变化时也可以注册变更通知或定期轮询。服务发现Web 服务器/metrics 端点指标采集器etcdZookeeper① 发现目标② HTTP 请求先拿配置,再按配置拉取 /metrics 端点
按 diagram-design 重绘

rd-20-consistent-hash-ring

20. Metrics Monitoring and Alerting System/images/consistent-hash-ring.png · 采集器与服务器落在同一个哈希环上

采集器与服务器落在同一个哈希环上单机采集器扛不住规模,需要多实例,但多个采集器不能重复采集同一台机器。把采集器和服务器放到同一个一致性哈希环上,每个采集器只负责环上一段连续区间内的服务器,从而天然完成分工。图中把环展开成直线,虚线表示顺时针相邻。哈希环(展开,顺时针)Collector 1Collector 2Collector 3Collector 4S1S2S3S4S5S6高亮:Collector 2 负责 S3、S4 所在区间每个采集器只负责环上一段连续区间,互不重叠
按 diagram-design 重绘

rd-20-collector-agent

20. Metrics Monitoring and Alerting System/images/metrics-collector-agent.png · 推模型:采集代理 + 负载均衡

推模型:采集代理 + 负载均衡推模型通常在服务实例旁装一个采集代理:代理从本机收集若干类指标,先做简单聚合再统一推送,从而减少采集器要处理的数据量。推送请求经负载均衡器分发到一组可自动扩缩容的采集器。指标源采集代理Web 服务器Metrics 1Metrics 2Metrics 3负载均衡指标采集器自动扩缩容推送消息代理先做本地聚合,再统一推送采集器置于负载均衡后的自动扩缩容组
按 diagram-design 重绘

rd-20-double-delta-encoding

20. Metrics Monitoring and Alerting System/images/double-delta-encoding.png · 双差分编码

双差分编码时间戳序列 1610087371、1610087381、1610087391… 往往等间隔。与其存完整时间戳,不如存相邻两个时间戳的差值(delta),再对差值序列本身做一次编码——因为差值大多相同,只需记录少数变化点,压缩率很高。1610087371161008738116100873911610087400161008741110差值10差值9差值11差值存时间戳差值,而不是完整时间戳差值序列再编码:多数相同,只需记录变化点
按 diagram-design 重绘

rd-20-queuing-mechanism

20. Metrics Monitoring and Alerting System/images/queuing-mechanism.png · 排队机制:Kafka 削峰与解耦

排队机制:Kafka 削峰与解耦为防时序库故障时丢数据,在采集器和数据库之间加一层队列:采集器把指标写进 Kafka,消费者(Storm / Flink / Spark)拉取、处理后再写库。Kafka 高可靠可扩展,把采集与处理解耦,并靠保留数据避免丢失。排队缓冲与流处理指标源指标采集器Kafka排队缓冲消费者流处理时序数据库查询服务告警系统规则与去重邮件短信PagerDutyHTTPS 回调可视化系统仪表盘发送查询发送查询Kafka 按指标名分区,消费者可按指标聚合
按 diagram-design 重绘

rd-20-kafka-partitions

20. Metrics Monitoring and Alerting System/images/metrics-collection-kafka.png · Kafka 按指标名分区

Kafka 按指标名分区Kafka 可以配置成「每个指标名一个分区」:metric 1 进 Partition 0,metric 2 进 Partition 1,metric 3 进 Partition 2,以此类推。这样消费者可以按指标名各自聚合;要再扩展,可以继续按标签分区,并给指标排优先级。Kafka指标采集器Partition 0metric 1Partition 1metric 2Partition 2metric 3按指标名分区一个指标名一个分区,消费者按指标聚合再扩展:按标签分区并给指标排优先级
按 diagram-design 重绘

rd-20-cache-layer-query-service

20. Metrics Monitoring and Alerting System/images/cache-layer-query-service.png · 查询服务加缓存层

查询服务加缓存层在时序库和可视化 / 告警之间加一层查询服务,能解耦客户端与数据库,让我们随时换库。再在查询服务下挂一层缓存,把热点查询结果存起来,显著降低时序库的读压力。查询服务与缓存层指标源指标采集器Kafka排队缓冲消费者流处理时序数据库查询服务查询接口告警系统规则与去重缓存缓存最近查询可视化系统仪表盘邮件短信PagerDutyHTTPS 回调发送查询读写缓存发送查询查询服务解耦客户端与数据库,缓存降低读压力
按 diagram-design 重绘

rd-20-alerting-system

20. Metrics Monitoring and Alerting System/images/alerting-system.png · 告警系统的七步链路

告警系统的七步链路① 规则配置(YAML)加载进缓存;② 告警管理器从缓存读取规则;③ 按固定间隔调用查询服务;④ 命中的告警写入 Alert store(KV 库,保证至少通知一次);⑤ 告警发布到 Kafka;⑥ 告警消费者从 Kafka 拉取;⑦ 按通道发出邮件、短信、PagerDuty、HTTPS 回调。规则配置文件YAML 规则缓存Alert store告警状态告警管理器规则与去重Kafka告警消费者查询服务查询接口邮件短信PagerDutyHTTPS 回调① 读取② 加载规则③ 调用查询④ 存状态⑤ 发布⑥ 拉取⑦ 通知告警管理器负责过滤、合并、去重,并保证至少通知一次
按 diagram-design 重绘

rd-20-grafana-dashboard

20. Metrics Monitoring and Alerting System/images/grafana-dashboard.png · Grafana 仪表盘示例

Grafana 仪表盘示例可视化系统把一段时间内的指标画成图表。这是一个 Grafana 面板:左上折线看各 Web 服务器的请求量,右上双轴折线对比内存与 CPU,左中柱状图给出前端加载耗时的分位值,右中折线看登录数,底部折线看流量进出。Grafana 仪表盘(示例)server requests折线 · 请求量Memory / CPU双轴折线client side full page load柱状 · 分位耗时logins折线 · 登录数Traffic In / Out折线 · 流量面板按时间轴展示指标与告警
按 diagram-design 重绘

rd-20-final-design

20. Metrics Monitoring and Alerting System/images/final-design.png · 最终设计

最终设计把前面的选择拼在一起:指标源 → 采集器 → Kafka → 消费者 → 时序库;查询服务封装读写并挂一层缓存,向上服务告警系统与可视化系统;告警管理器读规则、写状态、发布到 Kafka,由消费者投递到各通道。查询服务与缓存指标源指标采集器Kafka排队缓冲消费者流处理时序数据库查询服务查询接口告警系统规则与去重缓存缓存最近查询可视化系统仪表盘邮件短信PagerDutyHTTPS 回调发送查询读写缓存发送查询采集与处理解耦;查询服务 + 缓存支撑告警与可视化
按 diagram-design 重绘

rd-21-rtb-ecosystem

21. Ad Click Event Aggregation/images/digital-advertising-example.png · 实时竞价(RTB)生态

实时竞价(RTB)生态广告主经需求方平台(DSP)出价,发布商经供给方平台(SSP)提供广告位,双方在广告交易平台(Ad Exchange)上撮合。整条链路通常在一秒内完成,且计费依赖点击数据的准确性。需求侧供给侧广告主DSP 需求方平台Ad ExchangeSSP 供给方平台发布商竞价请求广告位请求RTB 在 1 秒内完成撮合点击数据准确性直接影响计费
按 diagram-design 重绘

rd-21-high-level-1

21. Ad Click Event Aggregation/images/high-level-design-1.png · 高层设计:同步链路

高层设计:同步链路日志采集器把点击事件推给数据聚合服务,服务每分钟算出「ad 点击计数」和「Top 100 广告」写入数据库;查询服务(仪表盘)再从数据库读聚合结果。数据在输入和输出两端都是无界流。日志采集器数据聚合服务数据库查询服务推送数据点击计数Top 100 广告查询输入处理输出展示
按 diagram-design 重绘

rd-21-high-level-2

21. Ad Click Event Aggregation/images/high-level-design-2.png · 高层设计:异步解耦

高层设计:异步解耦为避开同步 sink(消费者崩溃会拖垮全链路),用两条 Kafka 队列解耦:第一条存原始点击事件,分发给聚合服务和数据库写入器落原始库;第二条存每分钟的聚合结果,由数据库写入器落聚合库,供查询服务读取。日志采集器消息队列数据聚合服务消息队列数据库写入器原始数据库数据库写入器聚合数据库查询服务点击计数Top 100 广告拉取结果查询结果
按 diagram-design 重绘

rd-21-atomic-commit

21. Ad Click Event Aggregation/images/atomic-commit.png · 第二条队列与原子提交

第二条队列与原子提交第二条消息队列存在的意义,是把「写聚合结果」和「提交消费 offset」放进同一个原子提交里:要么两者都成功,要么都回滚,从而达成端到端的 exactly-once 语义。原子提交范围日志采集器消息队列数据聚合服务消息队列数据库写入器原始数据库数据库写入器聚合数据库查询服务点击计数Top 100 广告拉取结果查询结果
按 diagram-design 重绘

rd-21-ad-count-map-reduce

21. Ad Click Event Aggregation/images/ad-count-map-reduce.png · MapReduce:点击计数

MapReduce:点击计数聚合服务按 MapReduce 组织:Map 节点读入数据后分流给多个 Aggregate 节点,每个节点每分钟独立汇总自己那份 ad 的点击次数,各自输出一份点击计数。聚合服务数据输入MapAggregateAggregate点击计数每分钟点击计数每分钟数据输入点击计数点击计数每分钟聚合一次
按 diagram-design 重绘

rd-21-top-100-map-reduce

21. Ad Click Event Aggregation/images/top-100-map-reduce.png · MapReduce:Top 100

MapReduce:Top 100算 Top 100 时链路更长:Map 分流给多个 Aggregate,各节点汇总后把结果交给 Reduce,Reduce 归并所有分片算出全局 Top 100 广告。聚合服务数据输入MapAggregateAggregateReduceTop 100 广告数据输入Top 100 广告Top 100 聚合
按 diagram-design 重绘

rd-21-map-node

21. Ad Click Event Aggregation/images/map-node.png · Map 节点按 ad_id 分流

Map 节点按 ad_id 分流Map 节点读入数据后按 ad_id 取模决定去向:ad_id % 2 = 0 的事件交给聚合节点 1,ad_id % 2 = 1 的交给聚合节点 2。这样同一广告的事件一定落到同一节点。数据输入Map聚合节点 1聚合节点 2数据输入ad_id % 2 = 0ad_id % 2 = 1
按 diagram-design 重绘

rd-21-reduce-node

21. Ad Click Event Aggregation/images/reduce-node.png · Reduce 节点归并 Top 3

Reduce 节点归并 Top 3三个 Aggregate 节点各自维护一份 ad → 点击数的局部 Top 3,Reduce 节点把它们归并成全局结果。示例中最近一分钟点击最多的三支广告是 ad1、ad7 和 ad13。ad1: 12ad3: 5 · ad2: 3ad7: 9ad10: 4 · ad8: 3ad13: 8ad11: 4 · ad15: 3Reduce取 Top 3Top 3 广告ad1 · ad7 · ad13合并任务:取 Top 3
按 diagram-design 重绘

rd-21-use-case-1

21. Ad Click Event Aggregation/images/use-case-1.png · 用例 1:按 ad_id 聚合点击

用例 1:按 ad_id 聚合点击全部事件先经 Map 按 ad_id 分组,同一广告的事件落到同一聚合节点;各节点在内存里按分钟累计,输出该广告在过去一分钟内的点击次数。全部事件Map按 ad_id 分组聚合 ad3点击 4 次聚合 ad1点击 3 次聚合 ad2点击 5 次事件流同一 ad_id 的事件分到同一聚合节点
按 diagram-design 重绘

rd-21-use-case-2

21. Ad Click Event Aggregation/images/use-case-2.png · 用例 2:Top N 点击广告

用例 2:Top N 点击广告Top N 的链路多一层 Reduce:Map 把事件分成若干组交给 Aggregate,每个节点维护一个堆以快速取出局部 Top 3,Reduce 再归并出全局 Top 3;把 3 换成 N 即可扩展到 Top N。全部事件Map按 ad_id 分组聚合组 1ad3 · ad6 · ad9聚合组 2ad1 · ad4 · ad7聚合组 3ad2 · ad5 · ad8Reduce取 Top 3Top 3 广告ad3 · ad1 · ad2事件流Top 3 广告
按 diagram-design 重绘

rd-21-lambda-architecture

21. Ad Click Event Aggregation/images/lambda-architecture.png · Lambda 架构

Lambda 架构Lambda 同时保留两条处理路径:事件既进批处理层由批处理引擎离线计算,也进流处理层由实时引擎近实时计算,两者结果汇总到服务后端供查询;代价是两套代码库要各自维护。批处理层流处理层服务层数据存储事件源消息队列批处理引擎流处理引擎服务后端查询原始数据结果数据事件查询
按 diagram-design 重绘

rd-21-kappa-architecture

21. Ad Click Event Aggregation/images/kappa-architecture.png · Kappa 架构

Kappa 架构Kappa 把批与流合并成单一路径:事件只经流处理引擎近实时计算,结果直接进服务后端。历史数据重算也走同一个流处理引擎,因此只需维护一套代码——本章的高层设计即采用 Kappa。流处理层服务层数据存储事件源消息队列流处理引擎服务后端查询原始数据结果数据事件分区查询
按 diagram-design 重绘

rd-21-recalculation

21. Ad Click Event Aggregation/images/recalculation-example.png · 历史数据重算

历史数据重算当聚合逻辑出现重大 bug 需要重算时:重算服务①从原始库读出全量事件(批作业),②交给一个只做重算的聚合服务,③把结果写回第二条消息队列,再落聚合库。专用服务隔离了重算,避免影响实时链路。日志采集器消息队列数据聚合服务每分钟消息队列数据库写入器数据库写入器原始数据库数据聚合服务仅重算重算服务聚合数据库查询服务③ 写入① 读取② 重算查询批作业:从原始库重算聚合结果
按 diagram-design 重绘

rd-21-watermark-technique

21. Ad Click Event Aggregation/images/watermark-technique.png · 延迟事件漏出窗口

延迟事件漏出窗口用事件时间做聚合时,事件时间与处理时间因异步和网络延迟而错位。事件 2 的事件时间落在窗口 1 内,但处理时间 85 秒已晚于窗口 1 关闭的 60 秒,于是被漏掉。1234512345窗口 1含事件 1窗口 2含事件 3窗口 3含事件 4事件时间处理时间0306090120150180秒事件 2 处理时间 85 秒 > 窗口 1 关闭的 60 秒 → 漏聚合
按 diagram-design 重绘

rd-21-watermark-extended

21. Ad Click Event Aggregation/images/watermark-2.png · 扩展窗口:水位线

扩展窗口:水位线把窗口故意延长一段,这段延长就是「水位线」。窗口 1 加上水位线后覆盖到事件 2 的处理时间,漏聚合被补回。水位线越短漏事件越多但延迟越低,越长越不容易漏但延迟越高。1234512345窗口 1含事件 1水位线窗口 2含事件 3水位线窗口 3含事件 4水位线事件时间处理时间0306090120150180秒延长部分即水位线;越短漏事件越多但延迟越低
按 diagram-design 重绘

rd-21-tumbling-window

21. Ad Click Event Aggregation/images/tumbling-window.png · 翻滚窗口(1 分钟)

翻滚窗口(1 分钟)翻滚窗口把时间切成固定长度、互不重叠的段,本章用它做点击计数:每 1 分钟一段,段内累加点击次数,段结束时输出该分钟的计数。聚合点击第 1 分钟聚合点击第 2 分钟聚合点击第 3 分钟聚合点击第 4 分钟翻滚窗口(1 分钟窗口)12345分钟
按 diagram-design 重绘

rd-21-sliding-window

21. Ad Click Event Aggregation/images/sliding-window.png · 滑动窗口(3 分钟)

滑动窗口(3 分钟)滑动窗口长度固定但相邻窗口重叠,本章用它算「最近 M 分钟 Top N 广告」:窗口长 3 分钟、每分钟滑动一次,于是每 1 分钟就产出一份覆盖过去 3 分钟的 Top 广告。最近 3 分钟 Top 广告每分钟运行最近 3 分钟 Top 广告每分钟运行滑动窗口(3 分钟)12345分钟
按 diagram-design 重绘

rd-21-data-duplication-1

21. Ad Click Event Aggregation/images/data-duplication-example.png · 数据重复:确认前失败

数据重复:确认前失败上游 Kafka 消费到 offset 100 的事件交给聚合器,聚合器算完把结果发给下游,等下游确认后再回执上游。若在最后一步回执前崩溃,上游没收到确认就会从 offset 100 重新消费,同一批数据被重复处理。上游 Kafkaoffset 100聚合器下游 Kafka聚合状态offset 110① 消费② 聚合③ 发送步骤 6 回执前失败 → offset 100 被重复消费
按 diagram-design 重绘

rd-21-data-duplication-2

21. Ad Click Event Aggregation/images/data-duplication-example-2.png · 存 offset 到 HDFS/S3

存 offset 到 HDFS/S3一种缓解做法是把已消费的 offset 存到 HDFS/S3:聚合器先校验并保存 offset,再聚合、再发送结果。但「保存 offset」与「发送结果」不是原子的,若保存后崩溃,消息就可能永远到不了下游。上游 Kafkaoffset 100聚合器HDFS / S3存 offset下游 Kafka① 消费③ 校验保存 offset④ 发送结果先存 offset 再发送 → 崩溃则丢消息
按 diagram-design 重绘

rd-21-data-duplication-3

21. Ad Click Event Aggregation/images/data-duplication-example-3.png · 分布式事务保证原子性

分布式事务保证原子性彻底的做法是「存 offset」与「发送结果」放进同一个分布式事务:校验 offset、聚合、发送结果、保存 offset、下游回执要么整体成功要么整体失败,从而避免重复与丢失。分布式事务上游 Kafkaoffset 100聚合器HDFS / S3存 offset下游 Kafka① 消费③ 校验保存 offset④ 发送结果存 offset 与发送结果同一事务
按 diagram-design 重绘

rd-21-scale-consumers

21. Ad Click Event Aggregation/images/scale-consumers.png · 消费者组扩容

消费者组扩容消息队列靠消费者组水平扩展:把消费者加进同一个组,Kafka 会重平衡把分区重新分配。扩容前 4 个分区由 2 个消费者分担,扩容后每个消费者各拿一个分区;前提是提前建好足够的分区。扩展前:2 个消费者扩展后:4 个消费者分区 0分区 1分区 2分区 3消费者 0消费者 1分区 0分区 1分区 2分区 3消费者 0消费者 1消费者 2消费者 3重平衡
按 diagram-design 重绘

rd-21-aggregation-scaling

21. Ad Click Event Aggregation/images/aggregation-service-scaling.png · 聚合服务水平扩展

聚合服务水平扩展聚合服务由多个服务节点组成,每个节点各跑一个 Map 进程和一个 Reduce 进程。加节点即可提升吞吐;Map 输出按 ad_id 重分区(shuffle)后交叉送到各 Reduce 节点,再汇成最终聚合结果。聚合服务(水平扩展)KafkaMap 进程Map 进程Map 进程Reduce 进程Reduce 进程Reduce 进程聚合结果Map 输出按 ad_id 重分区shuffle 到各 Reduce 节点
按 diagram-design 重绘

rd-21-multi-threading

21. Ad Click Event Aggregation/images/multi-threading-example.png · 聚合服务多线程

聚合服务多线程提升单节点吞吐的另一种办法是多线程:Kafka 的事件按 ad_id 区间拆给不同服务节点,节点内再把各 ad_id 分给不同线程并行聚合,线程之间互不干扰。聚合服务节点 1聚合服务节点 2Kafka事件分组ad_id 1–3 事件事件分组ad_id 4–6 事件线程 1ad_id = 1线程 2ad_id = 2线程 3ad_id = 3线程 1ad_id = 4线程 2ad_id = 5线程 3ad_id = 6分区
按 diagram-design 重绘

rd-21-cassandra-scalability

21. Ad Click Event Aggregation/images/cassandra-scalability.png · Cassandra 一致性哈希扩展

Cassandra 一致性哈希扩展Cassandra 用一致性哈希水平扩展:节点加入或离开时,数据只在相邻节点间自动重平衡,无需手工分片。只用单 token 时负载容易不均;引入虚拟节点(每个物理节点持有多个 token)后,负载被摊得更平。哈希环(无虚拟节点)哈希环(有虚拟节点)节点 A节点 B节点 C节点 D节点 E节点 F节点 1节点 2节点 3节点 4节点 5节点 6新增节点自动重平衡,无需手工分片
按 diagram-design 重绘

rd-21-hotspot-issue

21. Ad Click Event Aggregation/images/hotspot-issue.png · 热点广告与资源申请

热点广告与资源申请若某支广告点击远超其他(热点),对应聚合节点会过载。它可向资源管理器申请额外资源,拿到资源后把 300 个事件拆成 3 组各处理 100 个,再把结果写回原节点,避免单点被压垮。数据输入Map热点聚合节点ad1 300 事件Reduce聚合输出资源管理器热点事件输出① 申请资源② 分配资源热点:单支广告点击远超其他申请资源后拆成 3 组,各处理 100 个事件
按 diagram-design 重绘

rd-21-fault-tolerance

21. Ad Click Event Aggregation/images/fault-tolerance-example.png · 容错:周期性快照

容错:周期性快照聚合在内存中完成,节点崩溃会丢数据。除了 Kafka offset,还需为「最近 M 分钟 Top N」这类中间状态定期打快照:按分钟记录每支广告在各分钟的点击数,恢复时据此重建。快照:最近 5 分钟 Top 3消息队列数据聚合服务ad1: 121 3 2 3 3ad3: 51 1 3 0 0ad2: 30 2 0 1 0每分钟为进行中的聚合打快照横轴:分钟 1 2 3 4 5
按 diagram-design 重绘

rd-21-fault-recovery

21. Ad Click Event Aggregation/images/fault-tolerance-recovery-example.png · 故障恢复:接棒继续

故障恢复:接棒继续主聚合节点故障后,新节点接手:它读取最新提交的消费 offset,再从快照存储拉取最近一次增量快照,据此重建中间状态并继续聚合,无需从头重算。外部节点主聚合节点快照存储新聚合节点增量快照拉取快照✗ 主节点故障节点停止服务
按 diagram-design 重绘

rd-21-reconciliation-flow

21. Ad Click Event Aggregation/images/reconciliation-flow.png · 每日对账流程

每日对账流程水位线再长也仍可能漏掉极少数事件,可用日终对账兜底:批作业从原始库重算聚合结果,与线上聚合库比对,差异写进差异报告交人工复核,必要时回补。消息队列数据聚合服务聚合数据库线上结果原始数据库全量事件对账任务每日批处理差异报告事件聚合结果存原始读原始比对差异
按 diagram-design 重绘

rd-21-alternative-design

21. Ad Click Event Aggregation/images/alternative-design.png · 替代设计:现成大数据组件

替代设计:现成大数据组件若不想自建,可用现成组件:点击数据经风控引擎后写入 Hive 做冷存,再建 ElasticSearch 层供数据科学家快速查询;聚合则交给 ClickHouse 等 OLAP 库,供商家侧分析。日志采集器消息队列风控引擎HiveClickHouseElasticSearch商家分析数据科学家聚合查询查询
按 diagram-design 重绘

rd-22-qps-estimation

22. Hotel Reservation System/images/qps-estimation.png · QPS 估算:三步漏斗

QPS 估算:三步漏斗预订要经过三步,每步约 10% 的转化率。若每天约 3 笔预订,反推上去:预订页 QPS≈30,酒店 / 房型详情页 QPS≈300。读多写少,且真正的写只有最后一步。浏览酒店 / 房型详情QPS = 300进入预订页QPS = 30完成预订QPS = 3转化 10%转化 10%每步约 10% 转化率,逐层收窄
按 diagram-design 重绘

rd-22-schema-design

22. Hotel Reservation System/images/schema-design.png · 初始表结构(按具体房间预订)

初始表结构(按具体房间预订)按服务切分关系库:酒店服务管 hotel / room,费率服务管 room_type_rate,客人服务管 guest,预订服务管 reservation。reservation 里带 room_id,说明这条模型假设「预订一个具体房间」,适合 Airbnb 却不太贴合酒店「订房型、入住时才分房」的业务。酒店服务费率服务客人服务预订服务hotelroomroom_type_rateguestreservationhotel_idPKnameaddresslocationroom_idPKroom_type_idfloornumberhotel_idnameis_availablehotel_idPKdatePKrateguest_idPKfirst_namelast_nameemailreservation_idPKhotel_idroom_idstart_dateend_datestatusguest_id
按 diagram-design 重绘

rd-22-status-state-machine

22. Hotel Reservation System/images/status-state-machine.png · 订单状态机

订单状态机status 字段是一台状态机:订单从「待支付」出发,支付成功进入「已支付」,超时未付或主动取消进入「已取消」,支付被拒进入「已拒绝」;只有「已支付」能再走到「已退款」。已取消 / 已拒绝 / 已退款都是终态。待支付已取消已支付已拒绝已退款取消支付成功拒付退款已取消 / 已拒绝 / 已退款为终态
按 diagram-design 重绘

rd-22-updated-schema

22. Hotel Reservation System/images/updated-schema.png · 改进后的表结构(按房型预订)

改进后的表结构(按房型预订)预订改为订「房型」而非具体房间:新增 room_type_inventory 表,按 (hotel_id, room_type_id, date) 一行一格记录 total_inventory 与 total_reserved,行由每日 CRON 预生成;reservation 的 hotel_id / room_type_id 变为外键。这样库存与预订同库,能借关系库的事务保证一致性。酒店服务费率服务客人服务预订服务hotelroomroom_type_rateguestroom_type_inventoryreservationhotel_idPKnameaddresslocationroom_idPKroom_type_idfloornumberhotel_idnameis_availablehotel_idPKdatePKrateguest_idPKfirst_namelast_nameemailhotel_idPKroom_type_idPKdatePKtotal_inventorytotal_reservedreservation_idPKhotel_idFKroom_type_idFKstart_dateend_datestatusguest_id
按 diagram-design 重绘

rd-22-high-level-design

22. Hotel Reservation System/images/high-level-design.png · 高层设计:微服务架构

高层设计:微服务架构用户经 CDN 拿静态资源,预订请求走公有 API 网关(限流、鉴权),网关再分发到酒店、费率、预订、支付四个服务,各自带缓存或数据库;酒店数据静态、可激进缓存。运维人员走私有内网 API 到酒店管理服务。服务间用 gRPC 之类的 RPC 通信。用户预订 / 取消CDN运维公有 API 网关限流 / 鉴权内网 API酒店管理服务酒店服务费率服务预订服务支付服务酒店缓存静态缓存酒店库费率库预订库支付库静态资源预订管理调用公有区走网关,私有区走内网 API
按 diagram-design 重绘

rd-22-microservices-vs-monolith

22. Hotel Reservation System/images/microservices-vs-monolith.png · 单体架构 vs 微服务架构

单体架构 vs 微服务架构单体架构里预订、库存、支付三个服务共享同一个酒店库,可以直接用关系库的事务保证一致性;纯微服务架构里每个服务各有一个专属数据库,服务之间没有共享事务,一致性要靠分布式手段解决。本章采用混合方案:把预订与库存放进同一个服务、共用一个库。单体架构微服务架构预订服务库存服务支付服务酒店库预订服务库存服务支付服务预订库库存库支付库专属专属专属共享一个库 → 事务保证一致性各库各管 → 无共享事务
按 diagram-design 重绘

rd-22-atomicity-monolith

22. Hotel Reservation System/images/atomicity-monolith.png · 单体:一个事务内的原子操作

单体:一个事务内的原子操作单体里「管理房态」与「预订房间」两步写操作落在同一个关系库里,可以放进一个事务:要么全部提交,要么全部回滚,不会出现房态改了但订单没落库的中间状态。单一事务用户下单管理房态预订房间数据库请求请求写入写入同一事务提交,要么全成功要么全回滚
按 diagram-design 重绘

rd-22-microservice-non-atomic-operation

22. Hotel Reservation System/images/microservice-non-atomic-operation.png · 微服务:跨服务没有共享事务

微服务:跨服务没有共享事务拆成微服务后,「管理房态」在库存服务、「预订房间」在预订服务,各自只在自己的单服务事务里保证原子性。两个事务彼此独立,一步失败无法回滚另一步,只能靠 2PC 或 Saga 之类的分布式手段补偿。事务 1事务 2用户下单库存服务预订服务步骤失败无回滚管理房态预订房间失败跨服务无共享事务,一步失败无法回滚另一步
按 diagram-design 重绘

rd-22-double-booking-single-user

22. Hotel Reservation System/images/double-booking-single-user.png · 重复提交:同一用户点两次

重复提交:同一用户点两次用户手抖点了两次「预订」,前端发出两条一模一样的 INSERT,用的还是同一个 reservation_id,结果数据库里落下两行重复记录——这就是需要解决的第一个并发问题。用户INSERT 121第 1 次INSERT 121第 2 次两条重复记录第 1 行 · 第 2 行点击点击两次点击共用同一 reservation_id → 落下两行
按 diagram-design 重绘

rd-22-idempotency

22. Hotel Reservation System/images/idempotency.png · 幂等 API:同一 reservation_id 只生效一次

幂等 API:同一 reservation_id 只生效一次进入预订流程时先生成一个全局唯一的 reservation_id,之后每次提交都带上它。第一次提交正常落库;第二次提交时后端发现是重复的 reservation_id,直接拦下,动作只执行一次。用户① 生成订单生成全局唯一 ID② 返回预订页返回预订页③a 提交预订提交预订③b 再次提交再次提交 → 被拦下预订服务reservation_idreservation_id 全局唯一 → 重复提交被识别并拦下
按 diagram-design 重绘

rd-22-unique-constraint-violation

22. Hotel Reservation System/images/unique-constraint-violation.png · 唯一约束拦下重复写入

唯一约束拦下重复写入把 reservation_id 设成唯一约束:第一次 INSERT 成功落下一行;第二次带着同一个 id 再来时触发唯一约束冲突,写入被拒绝,库里仍只有一行。用户INSERT 121第 1 次INSERT 121第 2 次写入成功写入成功唯一约束冲突唯一约束冲突点击点击通过冲突同一 IDreservation_id 唯一 → 只留一行
按 diagram-design 重绘

rd-22-double-booking-multiple-users

22. Hotel Reservation System/images/double-booking-multiple-users.png · 多用户并发:都读到有房 → 超卖

多用户并发:都读到有房 → 超卖库存 100 / 已订 99,只剩 1 间。事务隔离级别非串行时,User 1 和 User 2 同时查库存,都看到「还有 1 间」,各自把 total_reserved 加 1 并提交,最终变成 101 / 100——超卖。库存100 / 99 · 剩 1 间User 1 查库存有房User 2 查库存有房User 1 下单已订 +1User 2 下单已订 +1超卖 101 / 100both commit查库存查库存预订预订两个事务都读到 99 → 都提交 → 超卖
按 diagram-design 重绘

rd-22-pessimistic-locking

22. Hotel Reservation System/images/pessimistic-locking.png · 方案一:悲观锁

方案一:悲观锁悲观锁在更新期间直接锁住记录:事务 1 用 SELECT … FOR UPDATE 拿到行锁,直到提交才释放;事务 2 想查同一行只能排队等待,等事务 1 提交后它看到的已订数是 100、无房可订,只能回滚。串行化更新避免了冲突,但锁持有时长直接影响其他事务,伸缩性差。事务 1事务 2BEGINSELECT … FOR UPDATE加锁UPDATE 库存COMMITBEGINSELECT … FOR UPDATE等待锁UPDATE 库存已满 → 失败ROLLBACK等待 T1事务 1 持锁到提交,事务 2 只能排队等待
按 diagram-design 重绘

rd-22-optimistic-locking

22. Hotel Reservation System/images/optimistic-locking.png · 方案二:乐观锁(版本号)

方案二:乐观锁(版本号)表里加一列 version:读记录时带上版本号,写回时把版本号 +1。左图顺序更新:User 1 读 v1 写 v2,User 2 再读 v2 写 v3,链条一致、无冲突。右图并发更新:User 1 和 User 2 都读到 v1,各自写回 v2,第二次写入发现版本没推进,判定冲突并回滚。无冲突冲突v1v2v3User 1User 2v1v2v2写 v2读 v2写 v2写 v2✓ 顺序更新:版本单调递增,无冲突✗ 并发写回同一版本 → 冲突回滚
按 diagram-design 重绘

rd-22-database-constraint

22. Hotel Reservation System/images/database-constraint.png · 方案三:数据库约束

方案三:数据库约束思路和乐观锁相近,但把兜底交给数据库:加一条 CHECK 约束「total_inventory - total_reserved ≥ 0」。User 1 先提交、库存变 100 / 100;User 2 再想加 1 就会越界,约束直接拒绝这次写入。实现最简单,但数据竞争激烈时性能同样会下降。库存100 / 99 · 剩 1 间User 1 查库存有房User 2 查库存有房User 1 下单成功User 2 下单被拒CHECK 约束总量 - 已订 ≥ 0查库存查库存预订预订越界写入被 CHECK 约束直接拒绝
按 diagram-design 重绘

rd-22-database-sharding

22. Hotel Reservation System/images/database-sharding.png · 数据库分片:按 hotel_id 路由

数据库分片:按 hotel_id 路由所有查询都带 hotel_id,所以用 hash(hotel_id) % 16 把数据切成 16 个分片,每个分片各持一部分数据。QPS 三万时分到 16 片,每片约 1875 QPS,落在单台 MySQL 集群的承受范围内。预订服务hotel_id % 16分片路由Shard 0Shard 1Shard 2Shard 3Shard 4Shard 5Shard 6Shard 7按 hotel_id 取模,共 16 个分片(图中示意 8 个)
按 diagram-design 重绘

rd-22-inventory-cache

22. Hotel Reservation System/images/inventory-cache.png · 库存缓存:查询走缓存,更新走库

库存缓存:查询走缓存,更新走库用 Redis 缓存房态,key 为 hotelID_roomTypeID_date,value 是该日可用房数,并设 TTL 让过期日期自动淘汰。查询走缓存降低库压力;更新仍然落库,再由 CDC(如 Debezium)异步同步到缓存。缓存与库短时间不一致是可以接受的,因为越界预订最终会被数据库挡住。预订服务库存缓存Redis · TTL库存库分片 15查询库存更新库存异步同步缓存与库异步一致(CDC),短暂不一致可接受
按 diagram-design 重绘

rd-23-dns-lookup

23. Distributed Email Service/images/dns-lookup.png · MX 记录查询

MX 记录查询发件服务器要投递到 gmail.com,先向 DNS 解析器查询该域的 MX 记录。解析器返回一串带优先级的邮件交换主机,数值越小优先级越高,发件服务器按序尝试投递。发件服务器发出邮件DNS 解析器查询 MXMX 记录按优先级查询 MX返回列表MX 记录带优先级,数值越小越优先20 alt2.gmail-smtp-in.l.google.com30 alt3.gmail-smtp-in.l.google.com40 alt4.gmail-smtp-in.l.google.com5 gmail-smtp-in.l.google.com10 alt1.gmail-smtp-in.l.google.com
按 diagram-design 重绘

rd-23-traditional-mail-server

23. Distributed Email Service/images/traditional-mail-server.png · 传统邮件服务器

传统邮件服务器Alice 用 Outlook 发信:邮件经 SMTP 送到 outlook.com 服务器并落盘;服务器查 DNS 找到 gmail.com 的 MX 记录后经 SMTP 转发到 gmail 服务器;Bob 再用 IMAP/POP 从 gmail 服务器拉取。收发与存储都绑定在单台服务器上。outlook.com 邮件服务器gmail.com 邮件服务器AliceIMAP/POPSMTP 服务器本地存储BobSMTP 服务器IMAP/POP本地存储1 发送2 转发存储拉取4 获取存储拉取单机架构:SMTP + IMAP/POP + 本地磁盘
按 diagram-design 重绘

rd-23-local-dir-storage

23. Distributed Email Service/images/local-dir-storage.png · 本地目录存储

本地目录存储传统邮件服务器把邮件存成本地文件:每个用户一个 Maildir 目录,目录下再按状态分成 cur(已读)、new(新到)、tmp(投递中)三个子目录。每封邮件是一个独立文件,磁盘 I/O 很快成为瓶颈。homeuser1user2MaildirMaildircurnewtmpcurnewtmp每个用户一个 Maildir,邮件按状态分目录cur 已读 · new 新到 · tmp 投递中
按 diagram-design 重绘

rd-23-high-level-architecture

23. Distributed Email Service/images/high-level-architecture.png · 分布式邮件高层架构

分布式邮件高层架构Webmail 通过 https 访问 Web 服务器(登录、资料等请求/响应),通过 WebSocket 连接实时服务器接收新邮件推送;两者都读写底层的存储层——元数据 DB、附件存储、分布式缓存与搜索库。存储层WebmailWeb 服务器实时服务器元数据库附件存储分布式缓存搜索库httpsWebSocket读写推送https 走请求/响应,WebSocket 走实时推送
按 diagram-design 重绘

rd-23-email-sending-flow

23. Distributed Email Service/images/email-sending-flow.png · 发信流程

发信流程Webmail 经 HTTPS 到负载均衡器,再分给 Web 服务器做基本校验:通过则入待发队列,失败则入错误队列。SMTP 出站线程从待发队列拉取、做垃圾/病毒检查与重试后投递到 Internet;发件同时把元数据、附件与缓存写进存储层,并在 Sent 文件夹留一份。出站检查存储层Webmail负载均衡Web 服务器校验错误队列待发队列SMTP 出站Internet垃圾检查病毒检查重试元数据库搜索库附件存储缓存1 HTTPS2 分发4.b 失败4.a 通过5 拉取7 投递3 存储6 存 Sent校验失败走错误队列,可指数退避重试
按 diagram-design 重绘

rd-23-email-receiving-flkow

23. Distributed Email Service/images/email-receiving-flkow.png · 收信流程

收信流程来信先到 SMTP 负载均衡器,分发到 SMTP 服务器执行接收策略(无效邮件直接丢弃);过大的附件转存对象存储。邮件处理线程做初步垃圾/病毒检查后,写入存储层、缓存与实时服务器;在线用户经 WebSocket 立即收到,离线用户回线后走 HTTPS 从 Web 服务器拉取。来信检查存储层来信负载均衡SMTP 服务器接收策略来信队列邮件处理垃圾检查病毒检查实时服务器WebmailWeb 服务器附件存储元数据库搜索库缓存1 来信2 分发4 入队5 处理7 推送8 WS9 HTTPS3 存附件6 存储10 拉取在线走 WebSocket 推送,离线回线后 HTTPS 拉取
按 diagram-design 重绘

rd-23-legend

23. Distributed Email Service/images/legend.png · 表结构图例

表结构图例三张表都用同一套键标记:K 表示分区键(Partition Key),决定数据分布到哪个分片;C↑ 表示升序聚类键,C↓ 表示降序聚类键,决定同一分区内的排序。KPartition KeyC↑Clustering Key ↑C↓Clustering Key ↓分区键定分布,聚类键定排序↑ 升序 · ↓ 降序
按 diagram-design 重绘

rd-23-folders-table

23. Distributed Email Service/images/folders-table.png · folders_by_user 表

folders_by_user 表folders_by_user 以 user_id 为分区键,folder_id 为 UUID,folder_name 为 TEXT。用 user_id 作分区键,可让同一用户的所有文件夹落在同一分片,从而一条查询取回该用户的全部文件夹。folders_by_user字段类型键user_idUUIDKfolder_idUUID—folder_nameTEXT—分区键 user_id 让同一用户的文件夹聚在一起
按 diagram-design 重绘

rd-23-emails-table

23. Distributed Email Service/images/emails-table.png · emails_by_folder 表

emails_by_folder 表emails_by_folder 以 (user_id, folder_id) 为分区键、email_id 为降序聚类键。email_id 是 timeuuid,天然按创建时间排序,于是「取某文件夹下最新邮件」就是一次顺序读。emails_by_folder字段类型键user_idUUIDKfolder_idUUIDKemail_idTIMEUUIDC↓fromTEXT—subjectTEXT—previewTEXT—is_readBOOLEAN—email_id 是 timeuuid,天然按时间倒序
按 diagram-design 重绘

rd-23-attachments

23. Distributed Email Service/images/attachments.png · 邮件与附件表

邮件与附件表emails_by_user 以 user_id 为分区键、email_id 为降序聚类键,存发件人、收件人、主题、正文与附件清单;附件实体拆到独立的 attachments 表,用 (email_id, filename) 定位,url 指向对象存储中的文件。emails_by_userattachments字段类型键user_idUUIDKemail_idTIMEUUIDC↓fromTEXT—toLIST<TEXT>—subjectTEXT—bodyTEXT—attachmentsLIST<filename|size>—字段类型键email_idTIMEUUIDCfilenameTEXTKurlTEXT—附件清单只存文件名与大小,实体放对象存储
按 diagram-design 重绘

rd-23-read-unread-emails

23. Distributed Email Service/images/read-unread-emails.png · 已读 / 未读表拆分

已读 / 未读表拆分Cassandra 禁止按非主键列过滤,无法直接查「未读邮件」。于是把 emails 表反范式化成 read_emails 与 unread_emails 两张表:标记已读时在两张表间搬一行,查询未读就只需扫 unread_emails。read_emailsunread_emails字段类型键user_idUUIDKfolder_idUUIDKemail_idTIMEUUIDC↓fromTEXT—subjectTEXT—previewTEXT—字段类型键user_idUUIDKfolder_idUUIDKemail_idTIMEUUIDC↓fromTEXT—subjectTEXT—previewTEXT—反范式化:标记已读时在两张表间搬行
按 diagram-design 重绘

rd-23-elasticsearch

23. Distributed Email Service/images/elasticsearch.png · Elasticsearch 搜索

Elasticsearch 搜索搜索请求同步走 RESTful API 直接查 Elasticsearch;发信、收信、删信这类会改动数据的操作则异步投进 Kafka,由消费者解耦后调用 RESTful API 重建索引。写入远多于读取,因此索引要专为写密集优化。Elasticsearch 集群搜索请求同步发邮件收邮件删邮件Kafka异步解耦Kafka 消费者RESTful API分片 1分片 2分片 3同步发 / 收 / 删邮件经 Kafka 异步解耦按 user_id 分区,分片落在不同节点
按 diagram-design 重绘

rd-23-lsm-tree

23. Distributed Email Service/images/lsm-tree.png · LSM-tree 分层

LSM-tree 分层LSM-tree 把写优化为纯顺序写:新数据先进内存的 Level 0(memtable),达到阈值后整体刷到磁盘的 Level 1;各层再按序合并,越往下层数据越多、单个 SSTable 越大、数量越少。Level 4磁盘Level 3磁盘Level 2磁盘Level 1磁盘Level 0内存写入先进内存,达阈值后合并到下层磁盘每个小方块是一个 SSTable,越往下越大越少时间 →
按 diagram-design 重绘

rd-23-multi-dc-example

23. Distributed Email Service/images/multi-dc-example.png · 多数据中心与故障切换

多数据中心与故障切换上:美国用户就近接入美国数据中心,数据经复制同步到欧洲数据中心做备份。下:美国数据中心故障时,用户请求切换到欧洲数据中心,由副本继续提供服务——这就是用多副本换取高可用。正常:跨数据中心复制故障:切换到备数据中心美国数据中心欧洲数据中心美国数据中心欧洲数据中心美国用户服务器数据库服务器数据库美国用户服务器数据库服务器数据库复制连接失败切换复制用户就近接入,副本异地复制主中心不可用 → 切到副本中心
按 diagram-design 重绘

rd-24-storage-comparison

24. S3-like Object Storage/images/storage-comparison.png · 存储三分类

存储三分类存储分三类:块存储是裸块设备(HDD/SSD),性能高、成本高;文件存储建在块存储之上,提供目录层级,便于通用文件访问;对象存储牺牲性能换取高持久、海量规模与低成本,数据以扁平结构存为对象,每个对象由数据与元数据组成,通过 RESTful API 访问。块存储文件存储对象存储块块块块目录文件文件对象元数据载荷存储三分类:块 / 文件 / 对象可变 · 成本高 · 性能中高可变 · 通用文件访问不可变 · 低成本 · 海量扩展
按 diagram-design 重绘

rd-24-object-store-vs-unix

24. S3-like Object Storage/images/object-store-vs-unix.png · 对象存储与 Unix 文件系统

对象存储与 Unix 文件系统两者设计哲学一致:保存文件时先在一个数据结构里记录文件名,真正的数据放在别处。Unix 用 inode 保存文件名与块指针,通过本地磁盘访问读取块;对象存储用元数据服务保存「对象名 → 对象 ID」,再经网络请求从数据服务按 ID 取回对象内容。Unix 文件系统对象存储系统inode文件名 / 块指针硬盘本地磁盘MetaStore对象名 → 对象 IDDataStoreObject + ID磁盘访问网络请求设计哲学一致:元数据与内容分离先查元数据,再读本地磁盘块先查元数据,再经网络取对象
按 diagram-design 重绘

rd-24-bucket-and-object

24. S3-like Object Storage/images/bucket-and-object.png · 桶与对象

桶与对象桶是对象的逻辑容器,名称全局唯一,自身带一份元数据(ID、名称、策略、生命周期等)。对象是桶里的一份数据,由数据(对象内容)和对象元数据(ID、名称、版本、过期时间、访问控制等)组成;元数据与数据分开存储,可各自独立扩展。桶元数据ID / 名称 / 策略 / 生命周期桶全局唯一名称对象数据对象内容对象元数据ID / 名称 / 版本 / 过期描述桶是逻辑容器;对象 = 数据 + 元数据元数据与数据分开存储,可各自扩展按 UUID 定位对象数据
按 diagram-design 重绘

rd-24-high-level-design

24. S3-like Object Storage/images/high-level-design.png · 对象存储高层设计

对象存储高层设计客户端请求先过负载均衡器,分给无状态的 API 服务;API 服务统一编排:向左调用 IAM 做鉴权与授权,向下写元数据存储,向右把对象读写到数据存储。数据存储内部由主节点与两个从节点组成,主节点把数据复制到从节点以保证持久性。数据存储客户端负载均衡API 服务IAM 服务鉴权 / 授权元数据存储主存储节点从存储节点从存储节点HTTP转发鉴权 / 授权元数据读写对象复制复制无状态 API 服务统一编排IAM 集中鉴权与访问控制
按 diagram-design 重绘

rd-24-uploading-object

24. S3-like Object Storage/images/uploading-object.png · 上传对象

上传对象上传分七步:① 客户端 PUT 建桶;②⑤ API 服务调 IAM 校验身份与写权限;③ 校验通过后向元数据存储写入桶记录;④ 再 PUT 上传对象 script.txt;⑥ 对象载荷经数据存储落盘并返回 UUID;⑦ API 服务用 object_id、bucket_id、bucket_name 等新建对象元数据。数据存储客户端负载均衡API 服务IAM 服务鉴权 / 授权元数据存储主存储节点从存储节点从存储节点① PUT① 转发②⑤ 鉴权③⑦ 元数据⑥ 上传对象复制复制① 建桶 → ②⑤ 鉴权 → ③ 桶元数据④ 上传 → ⑥ 落盘 → ⑦ 对象元数据
按 diagram-design 重绘

rd-24-download-object

24. S3-like Object Storage/images/download-object.png · 下载对象

下载对象下载分五步:① 客户端发送 HTTP GET,如 GET /bucket-to-share/script.txt;② API 服务调 IAM 校验读权限;③ 校验通过后从元数据存储取回对象 UUID;④ 用 UUID 从数据存储取回对象载荷;⑤ 数据沿原路返回客户端。数据存储客户端负载均衡API 服务IAM 服务鉴权 / 授权元数据存储主存储节点从存储节点从存储节点① GET① 转发② 鉴权③ 取 UUID④ 取对象复制复制① GET → ② 鉴权 → ③ 取 UUID → ④ 取对象⑤ 对象数据沿原路返回客户端
按 diagram-design 重绘

rd-24-data-store-interactions

24. S3-like Object Storage/images/data-store-interactions.png · API 服务与数据存储的交互

API 服务与数据存储的交互上传:API 服务向数据存储发请求,携带 file_content 载荷,数据存储落盘后返回 ObjectID。下载:API 服务带上 ObjectID 请求,数据存储按 ID 取回对象数据返回。请求与响应是同一对盒子之间的往返,图中各画单向,响应由注释说明。上传对象下载对象API 服务数据存储API 服务数据存储请求:上传响应:对象数据请求携带 file_content,响应返回 ObjectID按 ObjectID 取回对象数据
按 diagram-design 重绘

rd-24-data-store-main-components

24. S3-like Object Storage/images/data-store-main-components.png · 数据存储的主要组件

数据存储的主要组件数据路由服务提供 RESTful / gRPC 接口访问数据节点集群,本身无状态,靠加机器扩展;它向放置服务查询最佳数据节点,并负责读写数据。放置服务维护虚拟集群映射(物理拓扑),接收所有数据节点的心跳,用 5 或 7 个副本经 Paxos / Raft 同步。数据节点存实际数据,靠复制到多个节点保证持久性。数据路由服务放置服务主节点从节点从节点查询数据流量心跳心跳心跳复制复制放置服务维护虚拟集群映射5 或 7 副本,经 Paxos / Raft 同步数据节点守护进程上报心跳
按 diagram-design 重绘

rd-24-virtual-cluster-map

24. S3-like Object Storage/images/virtual-cluster-map.png · 虚拟集群映射

虚拟集群映射虚拟集群映射描述集群的物理拓扑:根节点下按数据中心(DC)划分,每个 DC 下按主机(Host)划分,每台主机再划分出若干分区(Partition)。放置服务据此决定对象应该落到哪些数据节点上。默认DC-1DC-2Host-1Host-2Host-3Host-4分区 1分区 2分区 3分区 4分区 5分区 6分区 7分区 8根数据中心主机分区
按 diagram-design 重绘

rd-24-data-persistence-flow

24. S3-like Object Storage/images/data-persistence-flow.png · 数据持久化流程

数据持久化流程写路径:① API 服务把对象数据转给数据路由服务;② 数据路由服务向放置服务查询,按一致性哈希确定复制组;③ 把数据发给主节点;④ 主节点本地落盘并复制到两个从节点,复制成功后才返回;⑤ 数据路由服务把对象 UUID 返回 API 服务。主节点等待复制完成,偏向强一致而非低延迟。数据存储API 服务数据路由服务放置服务主节点从节点从节点① 写入② 选主③ 发送数据④ 复制④ 复制⑤ 返回对象 ID放置服务按一致性哈希确定复制组主节点复制成功后才返回,偏向强一致
按 diagram-design 重绘

rd-24-consistency-vs-latency

24. S3-like Object Storage/images/consistency-vs-latency.png · 一致性 vs 延迟

一致性 vs 延迟主节点复制时机的三种选择:方案一等两个从节点都复制成功才返回,一致性最强、延迟最高;方案二只等一个从节点,一致性与延迟都居中;方案三主节点写完立即返回,一致性最弱、延迟最低。等待的副本越少,写延迟越低,但一致性越弱。方案一方案二方案三数据路由服务主节点从节点 ×2数据路由服务主节点从节点 ×2数据路由服务主节点从节点 ×2同步等待复制两副本同步等待复制一副本立即返回最强一致 · 最高延迟:等待两个从节点确认中等一致 · 中等延迟:只等一个从节点确认最弱一致 · 最低延迟:主节点写完即返回
按 diagram-design 重绘

rd-24-wal-optimization

24. S3-like Object Storage/images/wal-optimization.png · WAL 合并小对象

WAL 合并小对象每个对象单独存一个文件会浪费磁盘块、并撑爆 inode 数量。用预写日志(WAL)把多个小对象顺序追加合并进一个大文件:当前读写文件(如 /data/c)持续追加 object 1..4,写满后转为只读文件,再新建一个读写文件。代价是同一文件的写入需串行化,可把文件绑定到特定核心以避免锁竞争。本地文件系统只读文件只读文件读写文件object 1..4API 服务对象 4写入追加到末尾已写满的只读文件当前读写文件小对象顺序追加合并;写满后转只读并新建文件绑定核心以避免锁竞争
按 diagram-design 重绘

rd-24-updated-data-persistence-flow

24. S3-like Object Storage/images/updated-data-persistence-flow.png · 更新后的数据持久化流程

更新后的数据持久化流程① API 服务发来保存新对象的请求;② 数据节点服务把对象追加到当前读写文件(如 /data/c)末尾;③ 同时在本地对象映射表里插入一条记录,记录 obj_id、文件名、偏移与大小。映射表随数据节点本地部署,用 SQLite 这类轻量文件型关系库,避免跨网络访问与额外扩展。本地机器只读文件/data/a只读文件/data/b读写文件/data/cobject_mapping 表obj_id / 文件 / 偏移 / 大小API 服务数据节点服务① 保存② 追加③ 插入记录映射表随数据节点本地部署SQLite:轻量文件型关系库
按 diagram-design 重绘

rd-24-failure-domain-isolation

24. S3-like Object Storage/images/failure-domain-isolation.png · 故障域隔离

故障域隔离仅在同一域内复制还不够:一次关键事件(断电、网络故障)可能同时击垮同域的多台机器。因此要把副本跨故障域分布——跨机架、跨数据中心、跨独立网络。图中同一 AZ 内的机架相互独立,副本再跨 AZ 复制,从而在整座 AZ 被摧毁时数据仍然可用。AZ 1(独立供电与网络)AZ 2(独立供电与网络)AZ 3(独立供电与网络)机架 1机架 2机架 1机架 2机架 1机架 2节点 1节点 2节点 1节点 2节点 1节点 2跨 AZ 复制跨 AZ 复制跨 AZ 复制跨 AZ 复制故障域:机架 / 数据中心 / 独立网络关键事件可能同时击垮同域多台机器跨域复制把副本分散到独立供电与网络
按 diagram-design 重绘

rd-24-erasure-coding

24. S3-like Object Storage/images/erasure-coding.png · 纠删码原理

纠删码原理纠删码把原始数据切成等长的数据块,再算出若干校验块。把每块看作一个数据节点:只要丢失的块数不超过校验块数,就能用剩下的块重建丢失的数据。这里 4 份数据 + 2 份校验,可容忍任意 2 块丢失。数据块校验块原始数据d1d2d3d4计算校验p1p2① 切成等长数据块② 计算校验块③ 任意 2 块丢失可重建4 份数据 + 2 份校验,可容忍 2 块丢失
按 diagram-design 重绘

rd-24-erasure-coding-across-failure-domains

24. S3-like Object Storage/images/erasure-coding-across-failure-domains.png · 跨故障域的纠删码

跨故障域的纠删码采用 8+4 纠删码:原始对象切成 8 个数据块,再算出 4 个校验块,共 12 块。把这 12 块分散到多个故障域,每个域只放其中几块,从而在某个域整体失效时仍能重建对象,最大化可靠性。故障域 1故障域 2故障域 3故障域 4原始对象数学计算d1d2d3d4d5d6d7d8p1p2p3p48+4 纠删码:8 个数据块 + 4 个校验块跨故障域分布,最大化可靠性
按 diagram-design 重绘

rd-24-erasure-coding-vs-replication

24. S3-like Object Storage/images/erasure-coding-vs-replication.png · 纠删码 vs 三副本

纠删码 vs 三副本存 1GB 数据:三副本把整份数据复制到 3 个节点,占用 3GB,存储开销 200%,读快、实现简单,但成本高;纠删码 4+2 把数据切成 6 片各 0.25GB 分散到 6 个节点,占用 1.5GB,开销仅 50%,任意 2 片丢失可重建,代价是计算更多、读取要跨多个位置。三副本复制(200% 开销)纠删码 4+2(50% 开销)1 GB1 GB1 GB0.25 GB0.25 GB0.25 GB0.25 GB0.25 GB0.25 GB副本:整份复制,读快、成本高纠删码:6 片,任意 2 片丢失可重建
按 diagram-design 重绘

rd-24-checksums-for-correctness

24. S3-like Object Storage/images/checksums-for-correctness.png · 校验和保证正确性

校验和保证正确性整盘损坏容易发现,局部数据被悄悄改坏却很难察觉。为此为每个文件与每个对象都存一份校验和——文件内容的哈希,读取时重新计算并比对即可发现损坏。采用 8+4 纠删码时,要分别取回 8 个数据片并逐一校验各自的校验和。本地文件系统只读文件文件级校验和只读文件文件级校验和读写文件对象级校验和读取时校验哈希,检测静默损坏损坏则用副本或纠删码重建8+4 时分别取 8 片逐一校验
按 diagram-design 重绘

rd-24-metadata-data-model

24. S3-like Object Storage/images/metadata-data-model.png · 元数据数据模型

元数据数据模型元数据主要两张表:bucket 表记录 bucket_name、bucket_id、owner_id、enable_versioning;object 表记录 bucket_name、object_name、object_version、object_id。桶数量有限,单库即可容纳;对象表规模巨大,按 hash(bucket_name, object_name) 分片,因为绝大多数查询都基于对象 / 桶名。bucket 表object 表bucket桶元数据object对象元数据bucket_namebucket_idowner_idenable_versioningbucket_nameobject_nameobject_versionobject_idobject 表按 hash(bucket_name, object_name) 分片
按 diagram-design 重绘

rd-24-object-versioning

24. S3-like Object Storage/images/object-versioning.png · 对象版本管理

对象版本管理用一个 TIMEUUID 类型的 object_version 列即可按时间排序记录。每次覆盖写入都产生一个新的 object_id:同一对象名 script.txt 下依次出现 version = fas3(当前)、bn31、1ag1(历史),各自指向数据存储里不同的对象 ID。元数据存储数据存储version = fas3script.txtid 0xM13nversion = bn31script.txtid 0x12Haversion = 1ag1script.txtid 0x91b4对象id 0xM13n对象id 0x12Ha对象id 0x91b4当前版本历史版本
按 diagram-design 重绘

rd-24-deleting-versioned-object

24. S3-like Object Storage/images/deleting-versioned-object.png · 删除带版本的对象

删除带版本的对象删除对象并不真的抹掉数据,而是新建一个带特殊 object_id 的版本,标记为 Delete Marker(version = kk1h),表示对象已删除。此后按名查询该对象会返回 404,而历史版本(fas3、bn31、1ag1)与其对象 ID 仍然保留在数据存储里,可按版本号访问。元数据存储数据存储Delete Markerversion = kk1hversion = fas3script.txtid 0xM13nversion = bn31script.txtid 0x12Haversion = 1ag1script.txtid 0x91b4对象id 0xM13n对象id 0x12Ha对象id 0x91b4当前版本:Delete Marker(查询返回 404)历史版本仍保留,可按版本号访问
按 diagram-design 重绘

rd-24-multipart-upload

24. S3-like Object Storage/images/multipart-upload.png · 分片上传大文件

分片上传大文件大文件用分片上传:① 客户端先向数据存储发起上传;② 数据存储返回唯一标识 upload_id;③ 客户端把大文件拆成多个分片,用 upload_id 各自独立上传;每个分片上传成功后数据存储返回一个 etag(该分片的 MD5 校验和);全部上传完再发起 complete,携带 upload_id、分片号与所有 etag,数据存储据此重组对象。客户端数据存储分片 1分片 2分片 8ETag 1ETag 2ETag 8① 初始化② 上传大文件拆成多个分片独立上传③ complete 携带 upload_id 与各 ETag,重组对象ETag = 分片的 MD5
按 diagram-design 重绘

rd-24-compaction

24. S3-like Object Storage/images/compaction.png · 压缩回收空间

压缩回收空间垃圾回收负责回收不再使用的空间:惰性删除标记过的对象、上传中断留下的孤儿数据、校验失败被判定损坏的数据,以及副本上的冗余。为方便删除,用压缩过程整理文件:① 把未被删除的对象从 data/b 复制到 data/d;② 复制完成后用一次数据库事务更新 object_mapping 表;为避免产生过多小文件,压缩只在文件增长超过阈值时进行。压缩前压缩后object_mappingobj_id / 文件 / 偏移 / 大小只读文件 /data/bObject 2 / 3 / 5object_mappingobj_id / 文件 / 偏移 / 大小只读文件 /data/dObject 3① 复制存活对象② 更新映射Object 2 / 5 已删除存活对象被复制过来压缩在文件增长超过阈值时进行
按 diagram-design 重绘

rd-25-leaderboard

25. Real-time Gaming Leaderboard/images/leaderboard.png · 排行榜界面

排行榜界面排行榜展示名次、玩家与积分三列,默认取前 10 名;前三名带星标。数据需实时刷新,不接受批量历史结果。名次玩家积分1Aquaboys9762B team9563Berlin's Angels8904GrendelTeam878前三名带星标;实时刷新,不用批量历史5mil DAU / 25mil MAU,每赢一局得 1 分
按 diagram-design 重绘

rd-25-high-level-architecture

25. Real-time Gaming Leaderboard/images/high-level-architecture.png · 排行榜高层架构

排行榜高层架构玩家赢一局后,客户端把结果报给游戏服务;游戏服务校验后调用排行榜服务更新分数,排行榜服务再写入排行榜存储。玩家查询榜单与个人排名时,直接走排行榜服务。客户端游戏服务排行榜服务排行榜存储① 赢一局② 更新分数③ 更新分数④ 取榜单写路径:赢局 → 游戏服务 → 排行榜服务 → 存储读路径:客户端直接向排行榜服务取榜单与排名
按 diagram-design 重绘

rd-25-alternative-design

25. Real-time Gaming Leaderboard/images/alternative-design.png · 当前方案 vs 备选方案

当前方案 vs 备选方案当前方案里客户端只报给游戏服务,由服务端校验赢局后再更新分数;备选方案让客户端直接向排行榜服务写分数,省一跳但可被中间人代理篡改,不安全。当前方案备选方案客户端游戏服务排行榜服务客户端排行榜服务① 赢一局② 更新分数直接上报服务端校验赢局,客户端改不了分客户端直连排行榜:可被中间人篡改
按 diagram-design 重绘

rd-25-message-queue

25. Real-time Gaming Leaderboard/images/message-queue-based-comm.png · 基于消息队列的通信

基于消息队列的通信在游戏服务与排行榜服务之间插入 Kafka:游戏服务只写一次主题,排行榜服务、分析服务、推送服务各自订阅。只有当其他服务也关心对局结果时才值得引入。游戏服务Kafka排行榜服务分析服务推送服务写入只有其他服务也关心对局结果时,才加这一层队列三个下游各自订阅同一主题
按 diagram-design 重绘

rd-25-leaderboard-table

25. Real-time Gaming Leaderboard/images/leaderboard-table.png · leaderboard 表结构

leaderboard 表结构最简方案:一张 leaderboard 表,user_id 为主键,score 为整数分数。原书按月建表,更省事的做法是加一个 month 列。leaderboarduser_idvarcharscoreint每月一张表,或加 month 列免去维护负担user_id 为键,score 是待排序字段
按 diagram-design 重绘

rd-25-user-wins-point

25. Real-time Gaming Leaderboard/images/user-wins-point.png · 用户得分时写库

用户得分时写库用户赢分时,排行榜服务向数据库写入:用户不存在则 INSERT 一行,已存在则把 score 加 1。排行榜服务DB插入或更新不存在则 INSERT,已存在则 score = score + 1每赢一局 +1 分
按 diagram-design 重绘

rd-25-find-position

25. Real-time Gaming Leaderboard/images/find-leaderboard-position.png · 查询榜单名次

查询榜单名次取前 N 名要按 score 倒序排序:直接全表排序会做一次表扫描,代价高;给 score 加索引再用 LIMIT 10 可避免扫描全部记录。但查询非头部用户的排名仍会退化成全表扫描。排行榜服务DB排序取前 N全表排序代价高;score 加索引 + LIMIT 10 优化非头部用户排名仍要扫描,扩展性差
按 diagram-design 重绘

rd-25-sorted-set

25. Real-time Gaming Leaderboard/images/sorted-set.png · Redis 有序集合

Redis 有序集合一个键对应一个有序集合:member 是 user_id,按 score 升序维护。底层用哈希表维护 user_id→score 映射,再用跳表按分数排序,因此插入与查找都是 O(logN)。leaderboard_feb_2021scoremember99user1097user2094user10592user4590user786user10183user982user30279user20072user309有序集合member = user_id(24 字符)25mil 用户 ≈ 650MB,单机即可容纳
按 diagram-design 重绘

rd-25-skip-list

25. Real-time Gaming Leaderboard/images/skip-list.png · 跳表结构

跳表结构跳表 = 有序基础链表 + 多级索引。查找时从最高层索引出发,能跳就跳,跳到不能再跳就下降一层,最终在基础链表里定位目标,避免逐节点遍历。115601815366014810152636456068L2 索引L1 索引基础链表从 L2 跳到 15,再逐层下降,最终在基础链表定位 45
按 diagram-design 重绘

rd-25-skip-list-performance

25. Real-time Gaming Leaderboard/images/skip-list-performance.png · 跳表的查找性能

跳表的查找性能以 64 个节点为例:在基础链表上逐节点查找需要遍历 62 个节点;跳表借助多级索引只需 11 跳,橙色路径即实际查找轨迹。1234567891011121357911159基础链表逐节点遍历:62 步跳表逐层下降:只需 11 跳
按 diagram-design 重绘

rd-25-position-of-user

25. Real-time Gaming Leaderboard/images/leaderboard-position-of-user.png · 展示用户附近的排名

展示用户附近的排名除前 10 名外,还要展示指定用户前后各 4 名。已知用户排名后,用 ZREVRANGE 取一段区间即可一次拿回这一窗口,无需全表排序。名次玩家积分267Aquaboys876258B team845259Berlin's Angels832360GrendelTeam799361Mallow007785362Woo78743363milan~114732364G3^^^^2726365Mailso_91_712高亮行为目标玩家,展示前后各 4 名ZREVRANGE 取 [357, 365] 区间一次拿回
按 diagram-design 重绘

rd-25-manage-services

25. Real-time Gaming Leaderboard/images/manage-services-ourselves.png · 自建服务方案

自建服务方案自建方案:客户端经负载均衡打到 Web 服务器;榜单用 Redis 有序集合,用户资料与积分放 MySQL,另用一层 Redis 缓存常被访问的 Top 10 用户资料。客户端负载均衡Web 服务器Redis榜单有序集合MySQL资料 + 积分RedisTop10 资料缓存请求分发榜单读写Top10 缓存自建:Redis 存榜单,MySQL 存资料,再加一层缓存Top10 资料常被访问,单独缓存
按 diagram-design 重绘

rd-25-api-gateway-mapping

25. Real-time Gaming Leaderboard/images/api-gateway-mapping.png · API 与 Lambda 的映射

API 与 Lambda 的映射API 网关按路由把请求分派给不同 Lambda:取 Top 10 走 LeaderboardFetchTop10,取个人排名走 LeaderboardFetchPlayerRank,更新分数走 LeaderboardUpdateScore。APIsLambda 函数GET /v1/scoresLeaderboardFetchTop10GET /v1/scores/{:user_id}LeaderboardFetchPlayerRankPOST /v1/scoresLeaderboardUpdateScoreAPI 网关按路由把请求映射到 Lambda每个 Lambda 只处理一种榜单操作
按 diagram-design 重绘

rd-25-scoring-lambda

25. Real-time Gaming Leaderboard/images/user-scoring-point-lambda.png · 得分链路(Serverless)

得分链路(Serverless)赢一局的写路径:排行榜服务把请求交给 API 网关,网关触发 Lambda;Lambda 用 ZINCRBY 更新 Redis 里的榜单分数,同时把这一局记进 MySQL 的积分表。排行榜服务API 网关LambdaRedis排行榜MySQL积分表赢一局转发ZINCRBY插入积分无需管理服务器:按需运行、自动扩缩
按 diagram-design 重绘

rd-25-retrieve-leaderboard

25. Real-time Gaming Leaderboard/images/user-retrieve-leaderboard.png · 取榜单链路(Serverless)

取榜单链路(Serverless)读路径:Lambda 用 ZREVRANGE 从 Redis 取回榜单与排名,再从 MySQL 取用户资料,结果经 API 网关返回排行榜服务。请求方向相反但走同一走廊,此处只画返回方向。排行榜服务API 网关LambdaRedis排行榜MySQL用户资料① 返回榜单返回ZREVRANGE② 取资料读路径:先取 Top10 与排名,再补用户资料请求方向相反、走同一走廊,此处只画返回路径
按 diagram-design 重绘

rd-25-range-partition

25. Real-time Gaming Leaderboard/images/range-partition.png · 按分数范围分区

按分数范围分区按分数区间把数据切成多个 Redis 有序集合分片,分片映射存在应用或 MySQL 里。取 Top 10 只需查最高分片;取某用户排名则要算本分片内排名,再累加更高分片的用户数(O(1))。[1, 100]Sorted set[101, 200]Sorted set[201, 300]Sorted set[901, 1000]Sorted set……按分数区间分片,分片映射存应用或 MySQL取 Top10 只查最高分片 [901, 1000]取用户排名需累加更高分片的用户数
按 diagram-design 重绘

rd-25-hash-partition

25. Real-time Gaming Leaderboard/images/hash-partition.png · 哈希分区(Redis Cluster)

哈希分区(Redis Cluster)Redis Cluster 用代理按槽位分布数据:槽位 = CRC16(key) % 16384。每个分片一主两从,主节点处理写入、从节点提供副本;分片内再按分数有序排列。Redis ClusterShard 1Shard 2Shard 3客户端代理主节点[0, 5500]副本副本主节点[5501, 11000]副本副本主节点[11001, 16383]副本副本Slot = CRC16(key) % 16384,代理按槽位路由每分片一主两从:写入主节点,读可走从节点
按 diagram-design 重绘

rd-25-top10-calculation

25. Real-time Gaming Leaderboard/images/top-10-players-calculation.png · 哈希分区下算 Top 10

哈希分区下算 Top 10哈希分区下无法直接定位最高分:必须让每个分片各返回自己的 Top 10,再在应用层归并排序。K 越大、分片越多,取回的数据量与延迟越高。Shard 0Top 10Shard 1Top 10Shard 2Top 10归并排序scatter-gatherTop 10 榜单合并每个分片各取 Top10,再在应用层归并K 越大、分片越多,延迟越高示例:99 user10 / 97 user20 / 94 user105
按 diagram-design 重绘

rd-25-dynamo-db

25. Real-time Gaming Leaderboard/images/dynamo-db.png · NoSQL 方案:DynamoDB

NoSQL 方案:DynamoDB备选的 NoSQL 方案用 DynamoDB 这类全托管数据库:适合高写入,并能在同一分区内按分数排序;需要查非主键字段时用全局二级索引。排行榜服务API 网关LambdaDynamoDB请求调用读写全托管 NoSQL:高写入,同分区内按分数排序用全局二级索引查非主键字段
按 diagram-design 重绘

rd-25-chess-table-1

25. Real-time Gaming Leaderboard/images/chess-game-leaderboard-table-1.png · 国际象棋榜表结构(主键)

国际象棋榜表结构(主键)DynamoDB 主表:主键是 user_id + score,属性列放邮箱、头像、榜单名。这样按用户查很方便,但按分数查询无法扩展。主键属性user_idscoreemailprofile_picleaderboard_namelovelove309love@test.comhttps://cdn.example/3.pngchess#2020-02i_love_tofu209test@test.comhttps://cdn.example/p.pngchess#2020-02golden_gate103gold@test.comhttps://cdn.example/2.pngchess#2020-03pizza_or_bread203piz@test.comhttps://cdn.example/31.pngchess#2021-05ocean10oce@test.comhttps://cdn.example/32.pngchess#2020-02……………主键 = user_id + score;属性列可扩展
按 diagram-design 重绘

rd-25-chess-table-2

25. Real-time Gaming Leaderboard/images/chess-game-leaderboard-table-2.png · 国际象棋榜表结构(二级索引)

国际象棋榜表结构(二级索引)为了按分数查询,把 score 放进排序键:分区键用 chess#月份,排序键用 score,就能在分区内按分数区间查。但按月分区会让最新月份成为热点。全局二级索引属性分区键 (PK)排序键 (score)user_idemailprofile_picchess#2020-02309lovelovelove@test.comhttps://cdn.example/3.pngchess#2020-02209i_love_tofutest@test.comhttps://cdn.example/p.pngchess#2020-03103golden_gategold@test.comhttps://cdn.example/2.pngchess#2020-02203pizza_or_breadpiz@test.comhttps://cdn.example/31.pngchess#2020-0210oceanoce@test.comhttps://cdn.example/32.png……………把 score 放进排序键,即可按分数区间查询按月分区 → 最新月份成热点
按 diagram-design 重绘

rd-25-chess-table-3

25. Real-time Gaming Leaderboard/images/chess-game-leaderboard-table-3.png · 国际象棋榜表结构(写分片)

国际象棋榜表结构(写分片)用写分片打散热点:在分区键后追加 user_id % N 得到的分区号。分区越多写入扩展性越好,但读取要查更多分区做 scatter-gather,读扩展性变差。全局二级索引属性分区键 (PK)排序键 (score)user_idemailprofile_picchess#2020-02#p0309lovelovelove@test.comhttps://cdn.example/3.pngchess#2020-02#p1209i_love_tofutest@test.comhttps://cdn.example/p.pngchess#2020-03#p2103golden_gategold@test.comhttps://cdn.example/2.pngchess#2020-02#p1203pizza_or_breadpiz@test.comhttps://cdn.example/31.pngchess#2020-02#p210oceanoce@test.comhttps://cdn.example/32.png……………写分片:分区键追加 user_id % N 的分区号分区越多写越快,读越慢(需 scatter-gather)
按 diagram-design 重绘

rd-25-scatter-gather

25. Real-time Gaming Leaderboard/images/scatter-gather-2.png · Scatter-gather 取全局 Top 10

Scatter-gather 取全局 Top 10写分片后,每个分区键各存一段榜单。取全局 Top 10 时要向所有分区散开(scatter)各取前 10,再汇总归并(gather)成一个全局榜单;分区越多,查询越慢。Partition 0chess#2020-02#p0Partition 1chess#2020-02#p1Partition 2chess#2020-02#p2汇总归并scatter-gatherTop 10 榜单gathergather合并散开:各分区各取 Top10汇总:合并后得到全局 Top10
按 diagram-design 重绘

rd-26-high-level-flow

26. Payment System/images/high-level-flow.png · 三方资金流转

三方资金流转资金流转只有三方参与:买方用信用卡付款(Pay-in)进电商网站的银行账户,电商再把钱结算(Pay-out)给卖方的银行账户。中间的电商网站是资金的中转枢纽。买方付款电商网站收款卖方结算信用卡银行账户电商银行账户卖方付款 Pay-in结算 Pay-out三方参与资金流转:买方 → 电商 → 卖方买方信用卡扣款进平台账户,平台再结算给卖方
按 diagram-design 重绘

rd-26-payin-flow-high-level

26. Payment System/images/payin-flow-high-level.png · Pay-in 高层流程

Pay-in 高层流程用户点击下单,支付事件进入支付服务;支付服务落库后把支付单交给支付执行器,执行器落库并调用外部 PSP,PSP 再经卡组织完成信用卡扣款。随后支付服务更新钱包、登记账本,各服务把余额与流水写入各自的库。左侧为支付系统内部,右侧 PSP 与卡组织属外部。支付系统(内部)用户支付事件支付服务协调支付支付库支付执行器执行支付单订单库账本钱包账本库钱包库支付服务商卡组织①支付事件②存事件③支付单④存单⑤调用 PSP⑥更新钱包⑦存余额⑧记账⑨存账内部服务全部落在支付系统边界内外部:PSP 与卡组织
按 diagram-design 重绘

rd-26-hosted-payment-page

26. Payment System/images/hosted-payment-page.png · PSP 托管支付页

PSP 托管支付页为避免存储卡号与繁重的合规负担,商户直接使用 PSP 提供的托管支付页:页面由 PSP 域名承载,用户在其中填写邮箱、密码等凭据并登录,卡片数据只提交给 PSP,商户系统完全不接触卡号。PSP 托管支付页PayPal邮箱 / 手机号密码保持登录登录并支付卡片数据只提交到 PSP 域名,商户系统不接触卡号登录后由 PSP 直接向发卡行发起扣款
按 diagram-design 重绘

rd-26-hosted-payment-page-workflow

26. Payment System/images/hosted-payment-page-workflow.png · 托管支付页工作流

托管支付页工作流用户在浏览器结算页点击结算,请求打到支付服务;支付服务带 nonce 向 PSP 建单,PSP 返回支付 token 并存入令牌库;随后用户被展示托管支付页并开始支付。支付完成后回跳完成页。异步的 token 返回、支付结果与 webhook 通知见下方说明。客户端浏览器支付系统结算页点击结算托管支付页PSP 页面完成页回跳结果支付服务协调流程令牌库PSP支付服务商①结算④存令牌②建单⑤展示托管页⑥发起支付⑧跳转托管页由 PSP 提供,商户不接触卡号③ PSP 返回支付 token⑦ 支付结果回传托管页⑨ PSP webhook 通知结果
按 diagram-design 重绘

rd-26-settlement-report

26. Payment System/images/settlement-report.png · 结算文件对账

结算文件对账每晚 PSP 下发结算文件,对账任务用它比对内部与外部状态:支付服务的支付库、支付执行器的订单库、账本库与钱包库的流水都要与 PSP 的结算记录核对,发现的差异进入对账环节,由财务团队按可分类与不可分类分别处理。支付系统用户支付事件支付服务支付库支付执行器订单库账本钱包账本库钱包库对账核对差异支付服务商卡组织①支付事件②存事件③支付单④存单⑤调用 PSP⑥记账⑦更新钱包⑧存账⑨存余额核对结算文件每晚对账:以 PSP 结算文件为准不可分类的差异由财务人工核查
按 diagram-design 重绘

rd-26-single-receiver

26. Payment System/images/single-receiver.png · 单接收者模式

单接收者模式多个服务订阅同一个 topic,但每条消息只被处理一次:消息 m1 投递给服务 A,消息 m2 投递给服务 B,各服务按自己的节奏消费,互不重复。消息队列同一 topic服务 A订阅者服务 B订阅者m1m2同一 topic 多订阅者,每条消息只投递给一个服务消息 m4→m3→m2→m1 依次出队
按 diagram-design 重绘

rd-26-multiple-receiver

26. Payment System/images/multiple-receiver.png · 多接收者模式

多接收者模式多个服务订阅同一个 topic,同一条消息被转发给所有订阅者:支付事件经消息队列广播后,支付系统、分析服务与计费服务各收到一份 m1,各自触发自己的副作用。事件源支付事件消息队列同一 topic支付系统分析服务计费服务发布m1m1m1同一 topic 广播给所有订阅者,每条消息投递多份一次支付触发多个副作用:支付、分析、计费
按 diagram-design 重绘

rd-26-failed-payments

26. Payment System/images/failed-payments.png · 失败支付处理

失败支付处理失败支付先判定是否可重试:可重试的进入重试队列,由支付系统再次执行;再次失败后继续判定,仍可重试则回到重试队列,终态失败则进死信队列等待调试。不可重试的支付直接落库,交给人工排查。失败可重试?重试队列支付系统仍失败可重试?死信队列数据库是否重试仍失败否是失败支付先判定可否重试不可重试:落库待人工排查终态失败:进死信队列
按 diagram-design 重绘

rd-26-retry-mechanism

26. Payment System/images/retry-mechanism.png · 重试机制

重试机制为实现至少一次投递,客户端在支付失败后按策略重试:前三次每次支付 10 元都被支付系统拒绝,客户端按指数退避的间隔再次发起,第四次请求终于成功。重试间隔可采用立即、固定、递增或指数退避等策略。客户端发起支付支付系统处理扣款Pay $10Pay $10Pay $10Pay $10失败失败失败成功重试重试重试
按 diagram-design 重绘

rd-26-idempotency-example

26. Payment System/images/idempotency-example.png · 幂等示例

幂等示例首次请求携带 idempotency-key 建单,服务端扣款成功后返回结果。客户端超时后重试,携带同一个 key;服务端发现该 key 已处理过,直接返回上次的结果而不再扣款,从而避免重复收费。首次请求重试客户端发起支付支付系统幂等校验客户端支付系统已见过该 keyPOST 建单扣款成功POST 相同 key返回上次结果Retry重试携带相同 idempotency-key:服务端识别后直接返回上次结果,不再扣款
按 diagram-design 重绘

rd-27-digital-wallet

27. Digital Wallet/images/digital-wallet.png · 数字钱包总览

数字钱包总览用户在支付平台内开一个数字钱包:从银行卡充值(Deposit)把钱存进钱包,之后可以直接用钱包在电商网站支付,比走普通支付通道更快、更便宜。银行卡银行数字钱包支付系统电商网站充值支付钱包支持充值、提现、转账与支付
按 diagram-design 重绘

rd-27-wallet-service

27. Digital Wallet/images/wallet-service.png · 钱包服务与内存分片

钱包服务与内存分片无状态的钱包服务接收转账命令,按 accountID 哈希把账户分到多个 Redis 分片;分片数量与地址存在 Zookeeper 里。服务可水平扩展,但跨分片的转账无法原子执行。转账命令A −$1 → B转账命令钱包服务钱包服务Redis{ A: 余额 }Redis{ B: 余额 }Redis{ C: 余额 }Zookeeper分片信息提交提交读分片A −$1B +$1C +$1按 accountID 哈希分片;跨分片转账无法原子完成
按 diagram-design 重绘

rd-27-distributed-transactions-relational-dbs

27. Digital Wallet/images/distributed-transactions-relational-dbs.png · 分片关系数据库上的分布式事务

分片关系数据库上的分布式事务把内存 Redis 换成可持久化的关系数据库分片,每个分片保存 ID / Balance 表。钱包服务仍按哈希路由,但跨分片转账需要在多个数据库上执行事务。转账命令A −$1 → B转账命令钱包服务钱包服务关系数据库ID: A · 余额: 1关系数据库ID: B · 余额: 1关系数据库ID: C · 余额: 1Zookeeper分片信息提交提交读分片A −$1B +$1C +$1事务要跨多个数据库,需要 2PC / TC-C 之类的协调
按 diagram-design 重绘

rd-27-2pc-protocol

27. Digital Wallet/images/2pc-protocol.png · 两阶段提交(2PC)

两阶段提交(2PC)协调者(钱包服务)依次向每个数据库发 canCommit?;只有全部回复 yes 后才发出 doCommit,再等所有数据库回 haveCommitted 才算成功;任一阶段失败则全部回滚释放锁。第二库走完全相同的 4 步。协调者钱包服务数据库 A4 步协议:① 协调者 canCommit? → ② DB yes → ③ 协调者 doCommit → ④ DB haveCommitted数据库 C 走完全相同的 4 步;任一阶段失败 → 全部回滚释放锁
按 diagram-design 重绘

rd-27-try-phase

27. Digital Wallet/images/try-phase.png · TC/C 第一阶段:Try

TC/C 第一阶段:Try协调者在 A 的数据库上开启本地事务,把 A 的余额减 $1 并加锁;C 的数据库收到 NOP 指令,什么都不做。协调者钱包服务数据库 A余额 $1 → $0数据库 C余额 $0Try: A−$1NOPA 扣款 $1 并加锁;C 收到 NOP扣款先于加款,保证中间态不会凭空多钱
按 diagram-design 重绘

rd-27-confirm-phase

27. Digital Wallet/images/confirm-phase.png · TC/C 第二阶段:Confirm

TC/C 第二阶段:Confirm若第一阶段两个数据库都回复 yes,进入确认阶段:A 的数据库收到 NOP,C 的数据库被要求把余额加 $1(本地事务)。协调者第一阶段 Try第二阶段 Confirm数据库 A余额 $1数据库 C余额 $0Try: A−$1Confirm: C+$1A=$1C=$1两个独立事务:先扣 A,再确认给 C 加钱
按 diagram-design 重绘

rd-27-cancel-phase

27. Digital Wallet/images/cancel-phase.png · TC/C 第二阶段:Cancel

TC/C 第二阶段:Cancel若第一阶段任一步失败,进入取消阶段:A 的数据库被要求把余额加回 $1(补偿操作),C 的数据库收到 NOP。协调者第一阶段 Try第二阶段 Cancel数据库 A余额 $0数据库 C余额 $0Try: A−$1Cancel: A+$1A=$1回滚 A=$1用补偿事务撤销已提交的扣款;C 收到 NOP
按 diagram-design 重绘

rd-27-phase-status-tables

27. Digital Wallet/images/phase-status-tables.png · 用阶段状态表恢复中间态

用阶段状态表恢复中间态协调者若中途宕机,需要恢复自己的中间状态。办法是在每个数据库分片内原子维护一张阶段状态表(记录事务内容、try 状态、第二阶段名与状态、乱序标志)。转账命令A −$1 → B转账命令钱包服务钱包服务数据库分片阶段状态表 + 余额表数据库分片阶段状态表 + 余额表数据库分片阶段状态表 + 余额表Zookeeper分片信息提交提交读分片A −$1B +$1C +$1表在分片内原子更新,供协调者重启后恢复
按 diagram-design 重绘

rd-27-unbalanced-state

27. Digital Wallet/images/unbalanced-state.png · 分布式事务的中间不平衡态

分布式事务的中间不平衡态事务在途时,各账户状态会短暂不一致:TC/C 开始时 A+C=$1,第一阶段之后变成 $0(少了 $1),第二阶段完成后又回到 $1。只要总能恢复、且用户无法动用中间态,就是可接受的。TC/C 开始前A + C = $1第一阶段之后A + C = $0第二阶段之后A + C = $1损失 $1恢复 $1中间态总额少 $1,会被下一阶段补回
按 diagram-design 重绘

rd-27-out-of-order-execution

27. Digital Wallet/images/out-of-order-execution.png · 乱序执行的处理

乱序执行的处理数据库可能先收到 Cancel 再收到 Try。此时在阶段状态表里置上乱序标志;之后 Try 到达时先检查该标志,若已置位就直接返回失败。协调者数据库 ATry 失败数据库 C先收到 Cancel阶段状态表out-of-order 标志TryCancel 先到置位标志Try 到达时若乱序标志已置位,直接返回失败
按 diagram-design 重绘

rd-27-saga

27. Digital Wallet/images/saga.png · Saga 事务

Saga 事务所有操作排成一条链、依次执行;一旦某步失败,就从当前步开始向前逐步执行补偿操作回滚。每一步都在自己的数据库里,失败时执行相反方向的补偿。开始A −$1C +$1成功A +$1C −$1A +$1成功完成失败失败失败正向顺序执行;失败则逐步补偿回滚
按 diagram-design 重绘

rd-27-event-sourcing

27. Digital Wallet/images/event-sourcing.png · 事件溯源的四要素

事件溯源的四要素命令是来自现实世界的意图(可失败、含随机性);状态机校验命令后产生事件;事件是系统里已经发生的事实;另一个状态机把事件应用到状态上,读写出账户余额。命令可失败 · 含随机状态机校验命令事件已发生的事实状态机应用事件状态余额输入校验产出读取应用命令可失败;事件是事实,状态机须确定性
按 diagram-design 重绘

rd-27-dynamic-event-sourcing

27. Digital Wallet/images/dynamic-event-sourcing.png · 事件溯源的动态视图

事件溯源的动态视图命令按时间顺序到达,经状态机校验后生成事件;事件再被应用到状态上,状态随之推进。如此循环,形成一条随时间前进的链路。命令事件状态命令事件状态命令事件状态校验应用校验应用校验应用命令 → 校验 → 事件 → 应用 → 状态,循环推进
按 diagram-design 重绘

rd-27-command-queue

27. Digital Wallet/images/command-queue.png · 命令队列

命令队列命令需要全局顺序,因此放进 FIFO 队列(如 Kafka)。转账命令从一端入队、按序从另一端出队,交给状态机处理。命令队列A −$1→ CA −$1→ CA −$1→ BA −$1→ Doutinout命令按全局顺序入队,保证先后一致
按 diagram-design 重绘

rd-27-wallet-service-state-machine

27. Digital Wallet/images/wallet-service-state-macghine.png · 钱包服务状态机全景

钱包服务状态机全景状态机从命令队列读取命令,从数据库读取余额;命令通过校验后为两个账户各生成一个事件并写入事件队列;事件再被应用,更新数据库里的余额状态。客户端命令队列Kafka状态机校验命令事件队列余额状态A/B/C/D状态机应用事件提交命令读取生成事件读取余额读取事件更新余额命令保序入队;校验后生成事件;事件再更新余额
按 diagram-design 重绘

rd-27-historical-states

27. Digital Wallet/images/historical-states.png · 重放事件重建历史状态

重放事件重建历史状态命令产生的事件写进事件队列,再按序应用到状态上;从事件列表开头重放,就能重建任意时刻的历史余额(如图所示逐次累积)。命令队列事件队列A −$1→ CA −$1→ BA −$1→ DA:−$1 C:+$1A:−$1 B:+$1A:−$1 D:+$1历史状态 1A:$5 B:$4 C:$3 D:$2历史状态 2A:$4 B:$4 C:$4 D:$2历史状态 3A:$3 B:$5 C:$4 D:$2事件不可变,从头重放即可重建任意时刻
按 diagram-design 重绘

rd-27-cqrs-architecture

27. Digital Wallet/images/cqrs-architecture.png · CQRS 读写分离架构

CQRS 读写分离架构写路径:客户端提交命令,经命令队列与状态机校验后产生事件并写入事件队列,状态随之更新。读路径:事件被发布给查询服务,由它重建历史状态来回答客户端查询。写路径读路径客户端命令队列状态机事件队列状态查询服务历史状态提交读取生成事件读取发布事件查询读取写路径产出事件;读路径消费事件并重建历史状态
按 diagram-design 重绘

rd-27-mmap-optimization

27. Digital Wallet/images/mmap-optimization.png · mmap:内存与磁盘合一

mmap:内存与磁盘合一用 mmap 把本地磁盘文件映射进内存:命令列表与事件列表既在内存里快速读写,又落盘持久化;只做追加写,对 HDD 也很快。内存磁盘命令列表状态机事件列表命令文件事件文件处理产出mmapmmap内存与磁盘同一份数据;追加写免网络往返
按 diagram-design 重绘

rd-27-rocks-db-approach

27. Digital Wallet/images/rocks-db-approach.png · RocksDB 存储状态

RocksDB 存储状态除了命令与事件,状态也存进本地文件系统。选用 RocksDB:它用 LSM-tree 优化写入,读性能则靠内存缓存来补。内存磁盘命令列表状态机事件列表状态RocksDB 内存缓存命令文件RocksDB 文件事件文件处理产出读写mmapmmapmmapLSM-tree 优化写;缓存优化读
按 diagram-design 重绘

rd-27-snapshot-approach

27. Digital Wallet/images/snapshot-approach.png · 用快照加速重建

用快照加速重建定期把状态快照写成大二进制文件存进分布式文件存储(如 HDFS);只读状态机重建时先加载最近一份快照,再从快照之后重放事件,无需每次都从头重放。对象存储事件列表不可变只读状态机重建历史状态客户端查询快照 2020-03-03快照 2020-03-02读取事件查询响应加载快照先加载最近快照,再重放之后的事件快照定期写入对象存储
按 diagram-design 重绘

rd-27-raft-replication

27. Digital Wallet/images/raft-replication.png · Raft 复制事件列表

Raft 复制事件列表为事件列表做高可靠,需要跨节点复制。用 Raft:一个 Leader 主动、其余 Follower 被动;Leader 把事件复制给 Follower,只要过半节点存活系统就能继续。状态与快照可从事件重放得到,因此只需保证事件列表可靠。Raft 节点组客户端写入FollowerEvents 副本LeaderCommands → EventsFollowerEvents 副本只读状态机只读状态机只读状态机查询写入Raft 复制Raft 复制同步同步同步只需保证事件列表可靠;过半存活即可继续
按 diagram-design 重绘

rd-27-polling-approach

27. Digital Wallet/images/polling-approach.png · 轮询获取最新状态

轮询获取最新状态CQRS 的请求 / 响应流较慢,客户端需要周期性轮询才能得知钱包已更新。轮询不是实时的,频率过高还会压垮查询服务。客户端Raft Leader写入只读状态机写入同步事件周期性轮询轮询非实时;频率过高会压垮查询服务
按 diagram-design 重绘

rd-27-reverse-proxy

27. Digital Wallet/images/reverse-proxy.png · 反向代理代发命令与轮询

反向代理代发命令与轮询引入反向代理:它代替用户发送命令,并代替用户轮询响应,可以用一次请求为多个用户取数,从而降低系统压力;但仍未解决实时性的问题。用户反向代理代发命令Raft Leader写入只读状态机命令转发写入轮询响应批量代用户查询,降低系统压力
按 diagram-design 重绘

rd-27-push-state-machines

27. Digital Wallet/images/push-state-machines.png · 只读状态机主动推送响应

只读状态机主动推送响应让只读状态机在响应就绪后主动把结果推回反向代理,而不是等代理轮询,从而让用户感觉更新是实时发生的。用户反向代理代发命令Raft Leader写入只读状态机命令转发写入实时推送响应就绪即推送,用户感知为实时
按 diagram-design 重绘

rd-27-sharded-raft-groups

27. Digital Wallet/images/sharded-raft-groups.png · 分片 Raft 组 + 协调者

分片 Raft 组 + 协调者为继续扩展,把系统拆成多个 Raft 组(分片),组内各自用 Raft 复制、组间用 TC/C 或 Saga 编排分布式事务。协调者把每个操作路由到对应分片,分片的只读状态机把响应推回反向代理。分片 1(账户 A)分片 2(账户 C)TC/C 协调者Saga阶段状态表反向代理Raft 组分片 1只读状态机反向代理Raft 组分片 2只读状态机命令 A−1命令 C+1写入事件推送响应写入事件推送响应记录状态每个操作路由到对应分片,跨分片用 TC/C 或 Saga 编排
按 diagram-design 重绘

rd-28-world-exchanges

28. Stock Exchange/images/world-stock-exchanges.png · 全球主要交易所

全球主要交易所按市值划分的全球主要交易所:美洲有 NYSE、NASDAQ、TMX;欧洲/中东/非洲有伦敦证交所、泛欧交易所、德意志交易所、SIX 瑞士交易所;亚太有上交所、深交所、日本交易所集团、港交所、韩国交易所。气泡越大代表市值越高。美洲欧洲 / 中东 / 非洲亚太NYSENASDAQTMX 集团伦敦证交所泛欧交易所德意志交易所SIX 瑞士交易所上交所深交所日本交易所集团港交所韩国交易所气泡越大代表市值越高交易所的基本职能:高效撮合买卖双方
按 diagram-design 重绘

rd-28-l1-price

28. Stock Exchange/images/l1-price.png · L1 行情

L1 行情L1 行情只包含最优买价与最优卖价及其数量。以 APPLE 为例,最优卖价(ask)为 100.10、数量 1800;最优买价(bid)为 100.08、数量 2000。卖价始终高于买价,二者之差即买卖价差。APPLE 股票最优卖价100.10 · 1800最优买价100.08 · 2000L1 只含最优买卖价与数量卖价高于买价,价差为 0.02
按 diagram-design 重绘

rd-28-l2-price

28. Stock Exchange/images/l2-price.png · L2 行情

L2 行情L2 行情在 L1 基础上展开多档价格:卖盘从最优卖价 100.10 向上到 100.13,买盘从最优买价 100.08 向下到 100.05,每档给出挂单数量。价差仍是 0.02,但可以看到更深的挂单分布。卖盘(卖价由高到低)买盘(买价由高到低)100.13300100.121500100.112000100.10 最优卖价1800100.08 最优买价2000100.07800100.062000100.05600L2 展示多档价格与每档数量
按 diagram-design 重绘

rd-28-l3-price

28. Stock Exchange/images/l3-price.png · L3 行情

L3 行情L3 行情在每个价位上进一步列出排队中的每一笔挂单数量。以卖盘 100.11 为例,队列为 900、700、400;买盘 100.06 的队列为 1100、400、300、200。它揭示了同一价位内部的挂单顺序。卖盘(卖价由高到低)买盘(买价由高到低)100.12600 · 900100.11900 · 700 · 400100.10 最优卖价200 · 400 · 1100 · 100100.08 最优买价500 · 600 · 900100.07100 · 700100.061100 · 400 · 300 · 200L3 列出每个价位上排队中的每一笔挂单
按 diagram-design 重绘

rd-28-candlestick

28. Stock Exchange/images/candlestick.png · 蜡烛图的构成

蜡烛图的构成一根蜡烛图记录区间内的四个价格:最高价与开盘价之间是上影线,开盘价与收盘价之间是实体,收盘价与最低价之间是下影线。图中用竖排的价格节点示意这一结构。最高价High开盘价Open收盘价Close最低价Low上影线实体下影线蜡烛图四价影线=最高/最低,实体=开盘/收盘
按 diagram-design 重绘

rd-28-high-level-design

28. Stock Exchange/images/high-level-design.png · 交易所高层设计

交易所高层设计交易流(关键路径):客户端经经纪商下单,客户端网关做校验/限流/鉴权后转给订单管理器;订单管理器做风控校验、核对钱包,再把订单交给定序器,定序器定序后送撮合引擎撮合。撮合引擎把成交流交给行情发布器,行情发布器生成行情给数据服务;订单管理器把订单与成交交给报告器落库。客户端下单经纪商客户端网关限流/鉴权订单管理器钱包定序器撮合引擎订单簿聚合风控行情发布器数据服务报告器数据库订单 / 成交下单转发校验订单定序风控放行成交流行情回报落库交易流在关键路径上,其余流程非关键
按 diagram-design 重绘

rd-28-sequencer

28. Stock Exchange/images/sequencer.png · 定序器

定序器定序器是让撮合引擎具备确定性的关键组件:订单管理器把入站订单交给入站定序器,入站定序器打上序列号后送撮合引擎;撮合产生的成交由出站定序器打号后回到订单管理器。两端各有一个定序器,保证入站与出站的事件顺序一致。订单管理器入站定序器撮合引擎出站定序器订单定序成交回报入站与出站各一个定序器,保证事件顺序一致
按 diagram-design 重绘

rd-28-client-gateway

28. Stock Exchange/images/client-gateway.png · 客户端网关的职责

客户端网关的职责客户端网关接收用户的订单并转交给订单管理器,自身承担鉴权、校验、限流、规范化与 FIXT 协议支持等职责。由于它处在关键路径上,必须保持轻量。网关鉴权校验限流规范化FIXT 支持网关在关键路径上,须保持轻量职责:鉴权、校验、限流、规范化、FIXT
按 diagram-design 重绘

rd-28-client-gateways

28. Stock Exchange/images/client-gateways.png · 多客户端网关

多客户端网关交易所针对不同客户提供不同的接入网关:交易所网站/App 经 HTTP 走 App/Web 网关;经纪商/自营商走支持 FIX/非 FIX 的 API 网关;其他 API 用户可走托管引擎(colo engine,券商租用的交易服务器)。各网关再汇入后端的共享服务。交易所交易所网站 / App经纪商 / 自营商其他 API 用户App/Web 网关API 网关FIX / 非 FIX托管引擎分片服务HTTPFIX其他转发转发不同客户走不同网关托管引擎:券商租用的交易服务器
按 diagram-design 重绘

rd-28-market-data

28. Stock Exchange/images/market-data.png · 行情数据流

行情数据流撮合引擎把订单与撮合结果送给行情发布器(MDP)。MDP 为每个标的维护订单簿,并从成交流重建蜡烛图;蜡烛图再写入持久化存储,最终由数据服务对外展示聚合后的行情。行情发布器 MDP撮合引擎订单簿订单簿订单簿蜡烛图按时间窗聚合持久化数据服务订单 / 成交聚合发布订单簿按标的维护
按 diagram-design 重绘

rd-28-reporting-flow

28. Stock Exchange/images/reporting-flow.png · 报告流

报告流报告器从订单管理器收集订单与成交信息,生成执行报告,分发给结算与清算、账簿与记录两条下游路径,最终汇总为监管报送并落库。报告流不在关键路径上,准确与合规比延迟更重要。撮合管理器订单管理器报告器ExecutionReport结算与清算账簿与记录监管报送数据库成交订单订单 / 成交结算记账报送报送报告流不在关键路径,重准确与合规结算/记账汇总到监管报送后落库
按 diagram-design 重绘

rd-28-data-model

28. Stock Exchange/images/product-order-execution-data-model.png · 订单、成交与产品数据模型

订单、成交与产品数据模型交易所的三类核心数据:Product 描述标的属性,Order 表示买卖指令,Execution 是撮合产出的成交结果。一笔订单可产生多笔成交(1 : 0..n),而每笔订单关联一个产品(1 : 1)。OrderorderID, price, qty, sideExecutionexecID, orderID, price, qtyProductproductID, symbol, lotSize1 : 0..n1 : 1订单、成交、产品三类数据一笔订单可产生多笔成交
按 diagram-design 重绘

rd-28-order-book-execution

28. Stock Exchange/images/order-book-execution.png · 订单簿撮合示例

订单簿撮合示例当一笔买入 2700 股的市价单进入时,撮合引擎会逐档吃掉卖盘:200 + 400 + 1100 + 100 + 900 = 2700。大单吃穿多档后,最优卖价被推高、买卖价差走阔。卖盘 Ask买盘 Bid100.12600 · 900100.11900 · 700 · 400100.10 最优卖价200 · 400 · 1100 · 100100.08 最优买价500 · 600 · 900100.07100 · 700100.061100 · 400 · 300 · 200吃 2700 股买入 2700 股逐档吃掉卖盘,价差走阔
按 diagram-design 重绘

rd-28-order-book-impl

28. Stock Exchange/images/order-book-impl.png · 订单簿实现

订单簿实现订单簿用 limitMap 把价格映射到价位(PriceLevel),每个价位内部用双向链表保存该价位的订单;orderMap 则把 orderID 映射到订单,使撤单可 O(1) 定位。挂单加到链表尾、撮合从链表头删除,都是 O(1)。买盘档位limitMap价格 → 档位100.08Order 500,600,900100.07Order 100,700,200100.06Order 1100,300,200orderMaporderID → OrderlimitMap:价格映射到价位每个价位内部是双向链表orderMap 让撤单可 O(1) 定位
按 diagram-design 重绘

rd-28-mmap-bus

28. Stock Exchange/images/mmap-bus.png · 单机 mmap 总线

单机 mmap 总线现代交易所常把一切放在一台服务器上:订单管理器、撮合引擎、行情发布器各自跑在应用循环里,通过 mmap 事件总线通信;报告器、日志、聚合风控、持仓管理等下游进程同样订阅这条总线。进程间没有网络与磁盘访问,从而把端到端延迟压到微秒级。单台服务器订单管理器应用循环撮合引擎应用循环行情发布器应用循环mmap 事件总线报告器日志聚合风控持仓管理写入写入写入订阅订阅订阅订阅所有进程通过 mmap 共享内存通信,无网络与磁盘
按 diagram-design 重绘

rd-28-application-loop

28. Stock Exchange/images/application-loop.png · 应用循环

应用循环订单管理器内部用一个应用循环(while 循环执行关键任务)串起处理:输入线程收到订单后 dispatch 给应用循环线程,循环更新订单状态并把指令 dispatch 给输出线程。循环被绑定到同一个 CPU 上,避免上下文切换,也就没有锁竞争。输入线程Netloop应用循环线程绑定 CPU 1输出线程Netloop订单状态CPU 1避免上下文切换dispatchdispatchupdate绑定应用循环绑定同一 CPU,避免上下文切换没有锁竞争:只有一个线程操作状态
按 diagram-design 重绘

rd-28-event-sourcing

28. Stock Exchange/images/event-sourcing.png · 事件溯源

事件溯源传统方案只保存当前状态:订单表里 Order 从 V1/New 变成 V2/Filled,中间过程丢失。事件溯源改为保存不可变的状态迁移:事件表记录 100/NewOrderEvent、101/OrderFilledEvent,任何时刻都能重放事件重建出当前状态。Order V1NewOrder V2FilledOrderVersion / StatusEventSequence / TypeOrderFilled非事件溯源:只存当前状态事件溯源:存不可变事件
按 diagram-design 重绘

rd-28-design-so-far

28. Stock Exchange/images/design-so-far.png · 设计雏形

设计雏形外部域通过 FIX 协议与网关交互,网关把 NewOrderEvent 写入 mmap 事件存储;撮合引擎内的订单管理器校验事件、更新订单状态并交给应用循环与撮合核心,撮合核心产生 OrderFilledEvent 写回事件存储。行情发布器与报告器各自订阅事件存储做后续处理。撮合引擎网关FIX订单管理器撮合核心应用循环事件存储客户端事件存储 mmap行情发布器报告器NewOrderEvent校验/更新发送撮合NewOrderEventEventsEvents设计雏形外部域 (FIX)行情 / 报告订阅事件
按 diagram-design 重绘

rd-28-sequencer-deep-dive

28. Stock Exchange/images/sequencer-deep-dive.png · 定序器深挖

定序器深挖在这个设计里定序器不再充当事件存储,而是作为单一写入者:网关与撮合引擎各自把数据写入环形缓冲,定序器从环形缓冲拉取、定序后写入 mmap 事件存储。这样事件存储只接受定序器的写入,顺序完全确定。网关撮合引擎环形缓冲环形缓冲定序器事件存储 mmap写入写入写入定序器从环形缓冲拉取数据
按 diagram-design 重绘

rd-28-leader-election

28. Stock Exchange/images/leader-election.png · 主备选举

主备选举为实现高可用,撮合引擎部署热(Hot)与温(Warm)两个实例。热实例正常处理读写:从事件存储消费 NewOrderEvent、写回 OrderFilledEvent。温实例处于待命,只消费 NewOrderEvent 而不对外发布事件,一旦热实例失联即可接管。撮合引擎(热)撮合引擎(温)事件存储 mmapNewOrderEventOrderFilledNewOrderEvent热实例处理读写,温实例仅接收订单心跳检测主实例是否存活
按 diagram-design 重绘

rd-28-leader-election-terms

28. Stock Exchange/images/leader-election-terms.png · 选举任期

选举任期Raft 把时间切成一个个任期:任期 1、2 以选举开始;任期 3 是稳定的正常运行期;任期 4 因选票分裂没有选出领导者,很快进入任期 5 重新选举。任期编号单调递增,是判断新旧信息的依据。任期 1选举任期 2选举任期 3正常运行任期 4选票分裂任期 5正常运行时间轴:任期与选举任期 4 因选票分裂重新选举
按 diagram-design 重绘

rd-28-replication

28. Stock Exchange/images/replication-across-servers.png · 跨服务器复制

跨服务器复制事件存储跨多台服务器复制:主事件存储接收热撮合引擎的读写,并通过 AppendEntries RPC 把日志同步到各个副本;温撮合引擎只从事件存储消费 NewOrderEvent。多副本保证了即使主实例故障数据也不丢失。撮合引擎(热)撮合引擎(温)事件存储 mmap(主)事件存储 mmap(副本 1)事件存储 mmap(副本 2)事件存储 mmap(副本 3)NewOrderEventOrderFilledNewOrderEventAppendEntries主副本通过 AppendEntries RPC 同步到其余副本
按 diagram-design 重绘

rd-28-market-data-publisher

28. Stock Exchange/images/market-data-publisher.png · 行情发布器优化

行情发布器优化行情发布器为每个标的维护订单簿,并基于成交流重建蜡烛图。蜡烛图按时间分辨率(1 分钟、1 小时、1 天)分别保存,用预分配的环形缓冲只保留最近 100 笔,避免频繁分配内存;再写入持久化并交给数据服务。行情发布器 MDP蜡烛图撮合引擎订单簿订单簿订单簿1 分钟1 小时1 天持久化数据服务订单 / 成交聚合发布环形缓冲只保留最近 100 笔
按 diagram-design 重绘

rd-28-determinism

28. Stock Exchange/images/determinism.png · 确定性

确定性功能确定性由定序器保证:事件真实发生的时刻并不重要,重要的是定序器赋予它们的顺序。上排事件在真实时间里间隔不均,下排经定序器编号后间隔均匀,只要顺序一致,重放就能得到完全相同的结果。事件 1事件 2事件 3事件 4事件 5事件 6事件 1事件 2事件 3事件 4事件 5事件 6真实发生时刻定序器编号后顺序由定序器决定,与真实时刻无关
按 diagram-design 重绘