12306将千万级抢票转为追加写 解耦排座消解数据库锁冲突

[Chinese] Case Study: Design 12306 + code deployment system — Scott Shi

Scott Shi

高并发票务的核心挑战在于有限资源的竞态协调而非单纯吞吐量,系统通过将抢票资格核验简化为关系型数据库的单行追加写入、并将耗时的动态规划配座剥离至后台异步批处理,无需依赖易失的内存预分配即可彻底消除行级锁瓶颈并实现水平扩展。

AI-assisted summary of [Chinese] Case Study: Design 12306 + code deployment system — original source. Check the original for full context.

Source published:

Key ideas

  • 针对持续代码部署中 10GB 大文件推送到万台节点的场景,架构采用 ZooKeeper 专职负责版本管理与成员共识,结合 BitTorrent P2P 网络避免跨数据中心骨干网带宽被击穿。 (0:00)
  • 在 12306 订票系统设计中,系统将千万级 QPS 的抢票互斥锁争抢解耦为纯追加写(Append-Only)记录,将耗时的同行人选座等动态规划计算完全剥离至后台异步批处理完成。 (29:45)
  • 铁路各区间的票额在现实中均按历史数据预分配,利用“车次 + 起始站 + 终点站”作为分片键可将并发压力完全打散至独立 Shard,彻底消除跨节点数据竞态与全局自增队列瓶颈。 (56:30)
  • Uber 等现代架构使用 Smart Client 本地端口映射与惰性刷新机制,并在 RPC 失败时向 ZooKeeper 拉取最新拓扑,取代了动辄数十万美元的昂贵硬件负载均衡器。 (70:00)

Detailed notes

## Background 本视频由技术博主 Scott Shi 于 2020 年 11 月 10 日发布在 YouTube 平台。视频简介指出,本次线上技术研讨聚焦于两大核心课题的设计:大规模代码部署系统(超大文件分发)与中国铁路 12306 火车票订票系统,并提供了配套的设计参考文档。

## TL;DR 分享首先探讨了万级节点下 10GB 编译产物的自动化分发方案,提出结合 ZooKeeper 控制面与 BitTorrent P2P 数据面,通过追加合并流与单 Leader 架构抑制写放大和跨机房带宽瓶颈。随后,讲者重点拆解了 12306 千万级峰值抢票系统的架构精髓,主张将高并发“抢票资格锁定”与耗时的“邻座动态规划分配”彻底解耦,将激烈的行锁争抢转化为分片单表上的极速追加写(Append-Only),辅以按车次区间的细粒度 Sharding 和快照隔离。最后,讲者评析了纯内存预分配方案在持久化与虚假售罄上的隐患,并结合微软与 Uber 的工业实践,探讨了去中心化客户端路由对传统昂贵硬件负载均衡的替代演进。

## Atmosphere & style 研讨过程展现出一线大厂架构师之间高密度、重实战、硬核辩论的研讨会质感。讲者表达沉稳自信且善于用直观比喻(如电影院选座、春运分段配额)拆解工程盲点,对观众提出的 Multi-Leader、Redis 抗写和内存预分配等常见误区给予了毫不避讳且逻辑严密的驳斥。现场问答互动频繁,在白板推演、极端宕机容灾推导与大厂踩坑经验的交织中,呈现出兼具学术严谨性与工业现实权衡的深度技术张力。

## Bulletin - [00:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=0s) 针对持续代码部署中 10GB 大文件推送到万台节点的场景,架构采用 ZooKeeper 专职负责版本管理与成员共识,结合 BitTorrent P2P 网络避免跨数据中心骨干网带宽被击穿。 - [29:45](https://www.youtube.com/watch?v=FI8G3VeI94w&t=1785s) 在 12306 订票系统设计中,系统将千万级 QPS 的抢票互斥锁争抢解耦为纯追加写(Append-Only)记录,将耗时的同行人选座等动态规划计算完全剥离至后台异步批处理完成。 - [56:30](https://www.youtube.com/watch?v=FI8G3VeI94w&t=3390s) 铁路各区间的票额在现实中均按历史数据预分配,利用“车次 + 起始站 + 终点站”作为分片键可将并发压力完全打散至独立 Shard,彻底消除跨节点数据竞态与全局自增队列瓶颈。 - [01:10:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=4200s) Uber 等现代架构使用 Smart Client 本地端口映射与惰性刷新机制,并在 RPC 失败时向 ZooKeeper 拉取最新拓扑,取代了动辄数十万美元的昂贵硬件负载均衡器。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=0s) 大规模文件分发不可依赖单节点直连拷贝,应通过 P2P 协议做种互传以保护跨数据中心骨干网资源。 - [05:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=300s) Master 节点严禁频繁广播微小写入,必须对变更日志执行批处理追加(Append Log),并在本地 Merge 合并大文件后一次性上传至 S3 再递增版本。 - [10:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=600s) ZooKeeper 仅适用于低频写入(如约 1 次/分钟)的成员管理与版本共识,不可承接百级 QPS 的高频变更。 - [15:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=900s) 节点下载大文件时需统一使用临时后缀并禁止对外读请求,待分片完整校验 Hash 后原子重命名切换版本。 - [20:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=1200s) 12306 面对日常百万级、春运千万级全站 QPS,平摊到全国上万车次后单车次平均并发仅约 1K,核心瓶颈在于热点车次的行级读写冲突。 - [29:45](https://www.youtube.com/watch?v=FI8G3VeI94w&t=1785s) 车票库存本质上具备预分配特征,抢票系统最核心的解耦是将“购票资格判定(Booking)”与“具体席位分配(Seat Allocation)”彻底剥离。 - [29:45](https://www.youtube.com/watch?v=FI8G3VeI94w&t=1785s) 多人同行选座在数学上属于高开销的动态规划(DP)问题,若置于用户同步链路会导致系统瞬间崩溃,必须由后台工作流异步处理。 - [36:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=2160s) 基于快照隔离(Snapshot Isolation),抢票只需向数据表极速追加插入单行并获取自增 ID,实现写不卡读且读不卡写。 - [36:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=2160s) 防刷与限流必须收敛在上游接入网关或全局旁路服务,若由分布式数据库分片各自计算会被多节点并发穿透打崩。 - [38:30](https://www.youtube.com/watch?v=FI8G3VeI94w&t=2310s) 候补机制通过容忍 1 到 5 分钟超时,接纳前序支付失败与取消的票额,极大平滑了高并发场景下的重试流量与用户体验。 - [45:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=2700s) 跨站多次换乘会导致级联多表写入与补偿事务复杂化,工程上必须对换乘次数设定硬性上限(如最多 2 到 3 次)。 - [49:10](https://www.youtube.com/watch?v=FI8G3VeI94w&t=2950s) Seat Allocation Server 采用单进程串行读取配额表与空位表并跑排座算法,千人级别运算仅需 1 秒左右,且便于失败重试。 - [56:30](https://www.youtube.com/watch?v=FI8G3VeI94w&t=3390s) 基于“车次 + 起止点”进行 Hash 分片,不同区间的票池相互独立,架构中应当竭力避免全局自增 ID 与全局队列带来的性能衰减。 - [01:01:20](https://www.youtube.com/watch?v=FI8G3VeI94w&t=3680s) 关系型数据库通过科学分片可横向支撑海量并发;将未售票全放内存存在单点宕机丢数据与各节点配额超卖/虚假售罄隐患。 - [01:05:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=3900s) 在订单状态正式确认为已预订(Booked)之前,系统允许用户重试并容忍未确认数据的丢失,主从同步可采用异步复制换取低延时。 - [01:10:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=4200s) Uber 采用 RTAPI 统一鉴权,内部服务通过 Smart Client 本地映射直连目标节点,遇 RPC 端口失效才被动向 ZooKeeper 更新路由拓扑。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=0s) 可扩展文件分发系统(Scalable File Distribution System)的背景与核心挑战

讨论首先聚焦于一个在群里引起广泛关注的系统设计题目——大规模可扩展文件分发系统(Scalable File Distribution System)。这个架构场景拥有非常多的实际落地用例。例如群里的王小豪同学曾提出过一个极具代表性的高并发工程痛点:如何构建一个持续代码部署系统(Continuous Code Deployment System)。在该场景下,代码编译(Compile)和链接之后生成的文件体积极其庞大,通常在 10GB 左右;系统每天需要触发数以千计的自动化发布任务,将这 10GB 的文件分发推送到跨越 Multi-Region、Multi-Zone 数据中心的上万台服务器节点(如 10,000 个节点)上。整个分发过程完全是自动化(automatically)执行的,且必须保证即便部分节点出现故障下线(nodes go down),也不能发生任何数据丢失(no data loss)。

讲者表示这个系统的核心思路借鉴了业界成熟的工程实践(包括 YouTube 工程师曾分享过的分发方案)。系统架构的核心思想是采用 ZooKeeper 配合 BitTorrent(P2P 网络)来分发海量大文件: - **ZooKeeper 的职责**:专门用于保存全局配置(Configuration),如 Master 文件所存放的物理位置、文件当前的全局版本号演进(Version Change),并负责广播集群的成员关系管理(Membership Management)。ZooKeeper 本质上就是为成员关系治理和分布式一致性(Consensus)而生的,它能极其精准、强一致地判定并记录哪些节点加入、哪些节点退出了广播组。 - **BitTorrent / P2P 传输的引入**:在跨数据中心分发数十吉字节级别的大文件时,绝不能依赖单点服务器的普通网络拷贝(如简单的 SCP/Node Copy)。如果单纯从单台主节点向成千上万个从节点推数据,单节点的网络带宽瞬间就会被占满卡死。更严重的是,如果在跨数据中心场景下直连拷贝,会极度消耗极其昂贵且宝贵的数据中心间骨干网资源(Inter-Data-Center Network),这是数据中心运维中最极力避免(avoid)的高负载瓶颈。

```mermaid flowchart TD accTitle: Scalable File Distribution Overview accDescr: High level architecture using ZooKeeper for coordination and BitTorrent for peer-to-peer distribution.

subgraph ControlPlane["控制与元数据管理 (Control Plane)"] ZK["ZooKeeper 集群<br/>• 全局文件版本管理<br/>• Membership 管理<br/>• 组内共识保证"] S3["Amazon S3 对象存储<br/>• 持久化存储 10GB 合并文件"] end

subgraph DataPlane["P2P 数据分发层 (Data Plane)"] Seeder["种子节点 (Seeders)<br/>优先下载并对外提供 Upload"] Leecher["下载节点 (Leechers)<br/>从多节点并行拉取 Chunk"] end

ZK -.->|通知新版本及下载地址| DataPlane S3 -.->|首批拉取| Seeder Seeder <-->|P2P 相互拉取/互传| Leecher ```

针对 BitTorrent 的底层细节,讲者坦言自己因为时间限制未做过深钻研,但在设计配置时了解到,Seed Group 可能存在约 2000 个活跃做种节点的限制,超出部分往往以 Leacher 方式只下载不上传。BitTorrent 原生设计就假定网络成员会高频进出(join and exit frequently),因此通过种子群管理或磁力链接(Magnet Link)建立 Peer Group 地址,能够以去中心化方式有效化解数据传输压力。

---

[05:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=300s) 写放大抑制、全序广播与 Master 追加合并流

在数据写入和版本演进的设计上,系统必须严格防范写放大(Write Amplification)问题。由于目标是向 10,000 个节点复制数据,如果在 Master File 上频繁地执行微小更新(例如每写入几个字节就全网广播),这个写操作就会被成倍放大一万倍("你是 proportional to number of nodes 的,每一个 byte 实际上都被增强了 1 万倍")。因此,Master 绝对不能被无休止地频繁写入,必须采用批处理(Batch)或小批次(Mini-batch)方案,在固定时间段或积累到一定大小阈值后再做同步。

为了保证高可用与防范单点故障(Single Point of Failure),写链路的流程规范如下: 1. **复制写保证**:主节点侧维持一组小型机器集群(Small Group Machines)。当写入请求到达时,必须成功写完组内所有副本机器后,才能向客户端返回成功(Return Success to Requester);如果业务场景允许特定程度的数据丢失,才可以降级为单机写入。 2. **变更日志追加(Append Only)**:所有新的写操作都被以增量追加的形式写入一个临时的 Append File(例如命名为 `File_v12345` 的当前未发布版本),不修改已发布的基准文件。 3. **全序广播(Total Broadcast Order)**:日志在副本间复制时必须满足全序广播,确保所有副本节点接收到的变更顺序与 Master 完全一致。讲者在此提醒了一个数据库领域的经典暗坑(Catch):单纯保证变更命令的全序到达,并不等于数据完全一致。例如,如果日志里记录的是非确定性函数调用 `NOW()`,不同节点执行时的机器时钟不一致就会导致数据分叉;正确的做法是 Master 节点先将时间函数求值为确定性数值(如把 `NOW()` 转成明确的 `6点15分`),再将具体数值广播给所有 Replica。

```mermaid flowchart LR accTitle: Append-Merge-Upload Workflow accDescr: The step by step lifecycle of a file version update from writes to ZooKeeper registration.

W["写请求 Writes"] --> A["追加至当前增量<br/>Append Log (vNext)"] A --> T{"达到阈值?<br/>(时间/大小/条数)"} T -- 是 --> M["本地 Merge<br/>旧基准文件 + 增量日志"] M --> U["Master 上传合并后大文件<br/>至 Amazon S3"] U --> R["递增并在 ZooKeeper 注册<br/>Pump Version"] T -- 否 --> A ```

当增量日志的写入达到特定阈值(比如经过了一段固定时间窗口、写操作达到一定数量,或者文件体积达到阈值)时,系统执行版本递增(Pump Version): - 系统立刻初始化一个新的空增量文件(版本号加 1,作为 `version+1`),后续新的写入立即路由到新文件上。 - 原增量版本(如 version 123)与基准文件在 Master 本地进行合并(Merge),拼装成完整的 10GB 镜像大文件。 - 由 Master 节点单点负责将合并后的完整大文件上传至对象存储(如 Amazon S3)。 - 上传成功后,Master 触发在 ZooKeeper 上的全局版本递增注册,正式向外部宣告最新版本已就绪。

---

[10:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=600s) ZooKeeper 的使用陷阱、S3 结合与文件去重策略

在协调服务(Consensus Service)的选择上,像 Uber 内部的类似系统 Flipper 底层也是基于 ZooKeeper 或 etcd 这种强一致性共识服务构建的。ZooKeeper 提供强线性一致性(Linearizability)的全局配置能力,但使用时必须极其谨慎,明确其预期使用场景: - **ZooKeeper 吞吐限制**:ZooKeeper 绝不能被高频写入。它在发生 Leader 选举时服务不可用时间可能长达 200 毫秒,在大规模集群中重新选主代价更高。ZooKeeper 的设计目标是处理低频配置更新,预期的写请求频率通常在 "1 个请求/分钟"(One request per minute)左右。如果系统有每秒数百次(100 QPS)的写操作,绝对不能直接打到 ZooKeeper 上。 - **Master 宕机容灾**:若 Master 在合并或上传过程中挂掉,系统可以通过 Slave 节点接管。因为所有追加日志都已同步至 Slave,数据没有丢失,Slave 同样可以将本地合并后的文件上传到 S3 并向 ZooKeeper 提交 Pump Version。 - **文件命名与多租户疑问**:对于用户可能上传相同文件名但内容不同的冲突问题,讲者澄清本系统属于数据中心内部的大规模文件分发基础设施(用于向选定节点拷贝大文件),并不是对外面向 C 端普通用户("This is not customer facing, it's a data center... So this problem doesn't exist"),调用方均为应用代码(Application Code),路径和标识均由系统规范生成。

关于文件去重(File Deduplication),系统可根据数据类型灵活处理: - **通用大文件/二进制**:直接使用文件大小(File Size)结合哈希指纹(Hash Signature / SHA256)完成校验和秒传去重。 - **图像与媒体数据(Image DNA)**:讲者延伸介绍了 Facebook 等安全防风控场景中的 Image DNA 技术。在社交平台上,黑产用户经常通过改动图片分辨率、裁切尺寸、轻微调整色调滤镜或改变视频格式来绕过普通 Hash 校验;Image DNA 能够提取出图像的视觉特征向量,即使文件格式、尺寸或色彩发生细微变动,依然能够通过图像特征指纹识别出内容实质相同,从而实现高维度的去重和安全审核。

---

[15:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=900s) BitTorrent 下载状态流转、单 Leader 抉择与 12306 订票系统引入

深入到各个从节点(Replica)从 BitTorrent 下载更新的具体工作流,整体状态迁移如下图所示:

```mermaid flowchart TD accTitle: Replica Node File Lifecycle accDescr: State management of file versions on an individual replica node.

subgraph ReplicaNode["单台 Replica 节点本地磁盘状态"] VOld["File_v1 (历史版本)"] VCur["File_v2 (当前正在提供读取的版本)"] VProg["File_v3.crdownload<br/>(BitTorrent 正在并行拉取中)"] end

VProg -->|下载完成并校验 Hash| VDone["重命名为 File_v3<br/>(变为最新可用状态)"] VDone -->|激活新版本| GC["后台垃圾回收<br/>清理 File_v1 等陈旧过期文件"] GC -.->|释放磁盘空间| VOld ```

在节点拉取文件的生命周期中: - 当 ZooKeeper 推送了版本 Pump 事件后,节点通过 Magnet Link 加入分发拓扑,系统需有常驻进程调度 Seeder(做种上传者)与 Leecher(下载者)的角色分配。 - 节点在下载过程中,未完成的文件被统一赋予临时后缀(如类似 Chrome 的 `.crdownload` 格式),处于该状态的文件绝对不会对外暴露承接任何读请求(Read Request)。 - 一旦 BitTorrent 将分片下载完毕并通过校验,系统立即将其原子重命名(Rename)为正式名称(如 `File_A_version_3`)。 - 节点上可能短暂停留多个版本(如旧的 Version 1 和提供服务的 Version 2),新版本就绪后,陈旧版本会被异步清理,以防 10GB 级别的大文件占满磁盘。

在互动环节,听众欣妖提问:"刚才那个地方是可以用 Multi-Leader 吗?" 讲者明确否定了 Multi-Leader 方案:引入 Multi-Leader 会导致多点写入时出现极其严重的文件冲突(File Conflict),解决大文件冲突的成本极高且复杂,完全没有必要,在分发体系中采用 Single Leader 更加稳健清晰。

随后,分享转入当天的第二个核心题目——中国铁路 12306 火车票订票系统(12306 Ticket Booking System)。讲者坦言自己因为出国较早,出国时中国还没全面普及高铁,因此自己甚至没有真正用过 12306 网站,但这道系统设计题十分经典有趣。其核心指标与业务特征极其严苛: - **流量峰值**:系统的日常高峰 QPS 在 100 万(1 Million)左右,但在春运抢票等极端峰值下,QPS 会爆发式激增到 1000 万(10 Million)甚至更高。 - **复杂分段行程(Multi-Segment Routing)**:旅客预订 A 地到 B 地的票,一趟车次沿途停靠多个经停站。例如一趟列车可以拆分为多个阶段行程:用户可以购买"北京到上海"整程,也可以只预订"北京到杭州"区段,或者在广州中转继续前往香港。这要求票务库存必须具备跨区段的动态复用与锁定能力。 - **业务规则与容量**:用户允许在任意给定时间退票(Revoke a booking at any given time);同时,标准高铁列车单列通常约有 1,000 个乘客座位,高频并发下的区间余票扣减与一致性构成了系统的核心难点。

[20:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=1200s) 业务规模估算与数据库瓶颈:从电影票冲突到两阶段锁(2PL)

在估算 12306 的系统体量时,Scott 列出了一组基础运行数据:2018 年全国铁路客运量为 33 亿人次,年增长率约为 9.4%,而 2019 年春运期间创下的单日历史峰值达到了 1300 万人次(13 million)。按未来 10 年的发展冗余预估,系统设计应当具备每年承载 50 亿人次的能力。将 50 亿年客流分摊到 365 天,平均每天约为 1400 万人次;若按 85% 的平均上座率计算,全国每天需开行约 16,500 列客车。针对春运等极端高峰日,系统指标被设定为能够支撑每天 2800 万人次。在 QPS 层面,假设日常全站 QPS 在 100 万左右,春运高峰期全站 QPS 按 1000 万(10 million)测算。若将这 1000 万 QPS 平均摊到上万辆列车上,单一车次(例如“北京到上海上午 12:00 出发”的班次)所承受的平均 QPS 实际上只有 1,000 左右。

系统的真正瓶颈必然落在数据库层面。抢票场景中最典型的痛点是多人同时争抢同一班列车时发生的“读-修改-写冲突”(Read-Modify-Write Conflict)。Scott 以去电影院选座买票打比方:“比如我和景祥同时看这个座位 A 有没有人,我们两个事务看的时候都发现没人,然后我去订、景祥也去订,如果系统给两个人都返回成功,那就出现了典型的冲突。”传统关系型数据库解决并发写冲突的标准模型是两阶段锁(Two-Phase Locking, 2PL)。在 2PL 机制下,事务读取数据时必须获取非排他的共享读锁(Shared Read Lock),允许多个用户同时并发读取;但当其中一人试图抢占座位发起写入时,就必须将读锁升级为排他的独占写锁(Exclusive Write Lock)。此时,该写操作必须将其他所有并发读取的事务驱逐或拒绝(Reject/Abort),直至独占写锁持有完毕并完成写入,才能彻底避免超卖。

- **年客运量与增长**:2018 年客流量 33 亿人次,年增速 9.4%;设计目标按年承载 50 亿人次规划(可满足未来约 10 年需求)。 - **日均与峰值指标**:日常平均每天 1400 万人次;春运极端高峰按每天 2800 万人次设计;全站峰值 QPS 预估为 1000 万。 - **单车次并发压力**:按全国运行 10,000+ 列客车平摊,单一车次抢票峰值 QPS 约为 1K。 - **并发锁机制**:两阶段锁(2PL)通过“读共享锁 + 写独占锁”规避冲突,写入时需排他并终止冲突事务。

---

[25:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=1500s) 分库分表策略与讨论:两阶段锁还是 MVCC 混合并发控制?

面对千万级并发峰值,数据库分库分表(Sharding)势在必行。Scott 补充道,若考虑放票并非全天均匀分布,而是每天固定整点集中放票,局部单车次的瞬间 QPS 可能会从 1K 冲到 10K 左右。针对核心分片键(Sharding Key),Scott 提议直接按“具体车次”(例如“G1234 次列车 12:30 发车”)进行分片,将属于同一列车的所有车票读写请求路由到同一个数据库物理节点上。在现代数据中心高配硬件环境下,单一 SQL 节点承载两阶段锁下的写入吞吐量一般在每秒 100K 左右,足以轻松消化单一车次分摊到的并发竞争。

```mermaid flowchart TD accTitle: 数据库分片与并发控制讨论 accDescr: 展示两阶段锁与多版本并发控制在行级锁上的分片与锁争用机制

subgraph ClientReq["客户端抢票请求"] R1["用户 A 请求 (北京-上海 G1234)"] R2["用户 B 请求 (北京-上海 G1234)"] end

subgraph ShardingRouter["按车次分片 (Sharded by Train ID)"] Router{"Hash(车次 + 发车时间)"} end

subgraph DBShard["单车次专用 DB 节点"] direction TB subgraph ConcurrencyControl["并发控制策略讨论"] LockModel["2PL: 谁先拿到独占写锁谁胜出"] MVCCModel["MVCC: Row 级别标记 Insert/Delete Tx ID"] end RowLock["锁定目标数据行 (Single-Row Lock)"] end

R1 --> Router R2 --> Router Router --> DBShard DBShard --> ConcurrencyControl ConcurrencyControl --> RowLock ```

讨论期间,景祥提出疑问:在传统 2PL 规则下,试图获取独占写锁的操作是否会被已存在的共享读锁长期阻塞?Scott 确认了这一点,指出这正是上世纪 70 年代末诞生并沿用至今的经典关系型数据库特性。景祥随后补充说明,现代主流事务型数据库实际上采用了混合架构,即“两阶段锁 + 多版本并发控制(MVCC)”,而且锁粒度早已不再锁整张表,而是细化到了单一行(Row-Level Locking)。在 MVCC 下,每当数据行更新时,引擎会在该行记录其 `insert_transaction_id` 与 `delete_transaction_id`,借此版本链判断当前事务的数据可见性;写操作只针对目标单行加锁,谁先抢到锁谁提交,其余后到冲突更新的事务直接被系统中止(Abort)。Scott 赞同这一补充,并指出虽然行级锁机制细节略有不同,但本质上都是围绕事务隔离级别展开的权衡,后续可以结合快照隔离(Snapshot Isolation)进一步展开。

- **Sharding Key 设定**:以“车次 + 发车日期时间”作为分片键,同车次强一致性约束完全收敛在单分片内。 - **单分片吞吐能力**:现代数据中心单 SQL 节点性能可达约 100K QPS。 - **并发锁演进机制**:传统 2PL 读写互斥;现代混合引擎基于 MVCC + 行级写锁(First-committer wins,冲突事务直接 Abort 提升吞吐)。

---

[29:45](https://www.youtube.com/watch?v=FI8G3VeI94w&t=1785s) 配额预分配与“抢票(Booking)”与“配座(Seat Allocation)”解耦

关于车票的席位调度,Scott 强调席位库存必须是“预先确定与预先分配的”(Predetermined and Pre-allocated)。这种设计有着充分的现实依据:一方面,铁路系统至今仍保留有线下实体售票窗口的配额;另一方面,长途列车必须为沿途经停站预留乘车区间配额。例如一列从北京开往上海、总定员 1000 人的列车,可能事先划分出 800 张全程直达票,剩下 200 张拆解为“北京至石家庄”以及“石家庄至上海”各 100 张。Scott 笑着分享道,自己在现实中认识父母在铁路局工作的业内人士,证实我国铁路系统实际调度中也正是采取这种预分配机制。

在此业务背景下,Scott 提出了全系统最核心的设计理念:**将“抢票判定(Booking)”与“具体席位分配(Seat Allocation)”两个动作彻底解耦开来**。抢票阶段只做一件事,就是判断用户“抢没抢到票的资格”,而绝不当场计算到底坐在几车几号。因为如果单次购票请求包含多人(例如家庭出行一次预订 10 张票),系统为了保证同行人坐在一起,在数学本质上是一个必须进行全局优化的动态规划(DP, Dynamic Programming)问题。若在抢购读写的高频核心链路中同步计算 DP 选座,系统性能会瞬间崩溃。因此,管理员通过后台管理系统将直达与区间配额预先写入配置库或内存数据库,抢票主流程仅以此配额做计数核验,选座计算则完全剥离到后端异步流水线中。

- **区间配额预分配**:始发终到直达客流与沿途区间客流物理拆分(如 1000 席划分为 800 直达 + 200 沿途经停配额)。 - **抢票与排座解耦**:Booking 仅分配“购票资格号”,Seat Allocation 异步计算“实体座位”。 - **规避同步计算陷阱**:多人同行邻座分配属于动态规划(DP)优化问题,严禁置于用户并发抢票的核心同步链路上。

---

[33:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=1980s) 快照隔离(Snapshot Isolation)与 Booking 核心表结构设计

为了彻底解放抢票时的写性能,数据库应采用快照隔离级别(Snapshot Isolation),主流的 MySQL 和 PostgreSQL 均天然支持此配置。快照隔离的核心优势在于“读写互不阻塞”(Readers do not block writers, and writers do not block readers)。当事务 T1、T2、T3 陆续发生时,读事务读取的是与其开启时间戳对应的历史快照数据。虽然此时用户查询到的余票数字可能是稍有延迟的旧数据(Stale Data),但在秒杀抢票场景下,终端用户完全能够理解并接受短延时的数据状态,关键在于系统保证读不会返回脏数据,同时海量只读请求绝不会拖慢抢购写入。

基于该理念,Scott 提出了 Booking 核心表的数据结构方案: 1. **自增主键(Auto-incremental ID)**:在逻辑上等同于一个持久化的队列(Queue)。ID 数值越小代表进入时间越早,天然具备排队判定的功能。 2. **UID(分片与列车 Hash ID)**:根据车次及日期时间进行 Hash 计算,确保请求精确路由至对应分片;若单一车次并发依然极端庞大,还可进一步细拆(例如将整列车均分为 3 至 4 个车厢分组,Hash 键后追加 `1/2/3/4` 区分)。 3. **Train Number 与 Start Time**:记录车次名称与发车具体时间。 4. **Passenger ID**:购票人身份标识(通常采用身份证号)。 5. **Ticket Count**:单笔订单订购的车票数量。 6. **Request UID**:由于抢票资格确认和排座皆为异步处理,系统生成一个唯一 Request ID 返回给前端,用于客户端后续主动轮询(Polling)查询排队与出票结果。 7. **Route**:乘车路线编号(例如 `A1` 代表北京至上海直达,`B2` 代表石家庄至上海)。 8. **State**:状态流转标识,主要包含三种状态——`empty`(处理中/排队中,涵盖未分配及候补中)、`booked`(成功锁定票额)和`cancelled`(取消/失败)。 9. **索引设计**:该表在 Primary Key 自带索引之外,重点对 `Passenger ID`(方便用户快速检索个人购票记录)、`Request ID`(支撑高频轮询)以及 `Route`(供系统扫描当前线路已预订总数)建立二级索引。

- **快照隔离优势**:写不卡读、读不卡写,读操作容忍微量数据延迟以保障极端写入性能。 - **逻辑排队机制**:利用单表 Auto-incremental ID 充当自然队列,序号在配额阈值内即视为抢票成功。 - **异步查询凭证**:核心表下发唯一 Request UID,将长耗时处理转为异步 Pull 机制。

---

[36:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=2160s) 极速 Append 写入、异步轮询与全局限流(Rate Limiting)

在这种架构下,抢票写入被简化到了极致——在数据库层面,用户的抢购行为仅仅是**向 Booking 表中追加插入单行记录(Append a row)**。在写入的一瞬间,用户根本不需要去和任何竞争者实时争抢具体的物理行锁,也不去即时扣减席位计数器,仅仅是完成一次速度极快的极简插入。若按读写比 10:1 测算,即便单个车次的抢购 QPS 达到 10K 乃至 100K 的极端峰值,单台 SQL 节点承载纯追加插入也完全游刃有余。

```mermaid flowchart LR accTitle: 抢票核心写入与异步轮询架构 accDescr: 展示用户抢票请求通过网关全局限流后极速写入分片表,并生成 Request ID 供前端异步轮询的过程

subgraph ClientLayer["客户端 (Client)"] UserReq["提交抢票请求"] PollReq["轮询查票 (带 Request ID)"] end

subgraph Gateway["接入与限流层"] LB["负载均衡器 (LB)"] RateLimiter["旁路限流 (Lookaside Rate Limiter)\n按 IP / User 限制每秒请求"] end

subgraph ShardedDB["车次专用数据库分片"] AppendWrite["单表极速插入 (Append a Row)\n获得自增排队 ID"] ReadIndex["读取快照与索引\n(无排他锁阻塞)"] end

UserReq --> LB LB --> RateLimiter RateLimiter --> AppendWrite AppendWrite -.->|"立即返回 Request ID"| UserReq PollReq --> LB LB --> ReadIndex ```

在客户端交互侧,用户提交后系统立即返回 Request ID,客户端随后以异步轮询方式(Asynchronous Pulling)拉取最新状态。Scott 强调,由于客户端应用是完全可控的,必须严控轮询频率,同时必须在前端接入层(Load Balancer)或旁路服务(Look-aside Service)实施强制的限流与节流(Rate Limiting and Throttling)。Scott 阐述了一个关键的架构陷阱:限流绝不能放在底层的 Sharding DB 节点去算,因为即便分散到 1000 个后台服务节点时,每个节点单 IP 的并发只有 1 次,但上游汇总起来该 IP 已经发起了超过 1000 QPS 的轰炸。因此,必须依赖集中式限流器跨节点全局追踪单 IP 请求频次。Scott 还半开玩笑地吐槽道:“已有的 12306 Rate Limiting 好像做的不太好,我看到好多程序员可以写脚本非常快地抢票;如果能用程序抢票,肯定它的 rate limit 没有做好。”

- **写入复杂度骤降**:剥离资源互斥争抢逻辑,将复杂的抢票事务转化为数据库单行极速追加(Append-only)。 - **限流防护层级**:限流必须收敛在上层负载均衡(LB)或全局旁路缓存服务,杜绝分布式分片被穿透聚合打崩。 - **非阻塞式余票查询**:查询余票仅读取数据快照,读操作绝不成为抢票并发写入的性能瓶颈。

---

[38:30](https://www.youtube.com/watch?v=FI8G3VeI94w&t=2310s) 候补机制(Waiting List)与后台批处理(Background Process)实现

针对购票结果与取消订单,系统设计了精细的状态判定与候补名单(Waiting List)机制。当一条抢票记录写入后,系统根据配置的限额判定其命运: 1. **名额以内(Success)**:自增序号落在直达配额(如前 800 名)之内,直接判定购票成功,交由后台进一步计算具体座席分配。 2. **彻底超额(Fail)**:如果列车仅有 1000 个总定员,而该请求的自增序号已经排到了 2000 名开外,明显超出了系统设定的候补上限,系统无需挂起,直接返回抢票失败。 3. **候补区间(Waiting List)**:若序号落在临界溢出范围内,请求会进入候补等待队列,容许等待 1 到 5 分钟(时间可配置)。在此窗口期内,若排在前面的用户主动取消订票、或者因后续支付超时/信用卡扣款失败被系统剔除,候补用户即可被自动顺延晋级(Promote)为成功订票。

```mermaid flowchart TD accTitle: 候补队列与后台异步配座处理流 accDescr: 展示用户追加记录后的三种判定路径以及后台批处理调度器分配座位的整体业务流程

subgraph EvaluateQueue["序号与配额对比评估"] OrderRow["用户排队记录 (自增 ID)"] Check{"比对车次配额阈值"} end

subgraph Branch["判定分支"] DirectSuccess["进入配额内 (成功)\n标记为 Booked"] InWaitList["进入候补队列 (1-5 min 超时)\n监控前序 Cancel / Pay Fail"] DirectFail["超出候补上限\n直接返回失败 (Fail)"] end

subgraph BatchWorker["后台调度器 (Background Process 每 10s 一次)"] Timer["分布式锁定时器 (Distributed Lock Timer)\nMaster 周期调度"] ScanQuota["扫描线路剩余配额与有效订单"] RunDP["执行邻座/同行人分配 (DP 算法)"] UpdateDB["回写数据库座位号与最终状态"] end

OrderRow --> Check Check -->|"序号 <= 800"| DirectSuccess Check -->|"800 < 序号 <= 候补上限"| InWaitList Check -->|"序号 > 候补上限"| DirectFail InWaitList -.->|"前序退票/超时晋级"| DirectSuccess DirectSuccess --> ScanQuota Timer --> BatchWorker ScanQuota --> RunDP RunDP --> UpdateDB ```

有参与者提问是否可以取消候补机制直接判定失败?Scott 回应称直接失败在技术上当然可行,但候补机制提供了极佳的用户体验优化。因为现实中联程转乘失败、支付超时取消以及误购退订的比例相当高,候补提供了关键的缓冲带。如果候补全部清空后仍有余票剩余,系统可以在配置中心重新把该车次标记为可用(Available)并重新放票。

对于后台座席分配与动态配额调整(Dynamic Allocation),系统不随 QPS 实时触发,而是采用后台常驻批处理进程(Background Process)每隔 10 秒左右批量扫表一次。批处理进程有序读取未分座的有效订单,根据有限的列车座位拓扑执行动态规划配座算法,分配完毕后统一批量回写数据库更新记录。关于 Mike 针对“后台进程如何落地”的提问,Scott 和参会者(Brand)展开了技术探讨:单机场景下可采用类似 Chronos 的单机定时器应用;但在分布式高可用环境下,必须依赖分布式锁与定时调度协调器保证主节点选举(Master Failover)。业界常用的方案包括 Uber 开源的 Cadence 编排引擎,或是 Salesforce 内部的分布式调度服务等,确保任何一台机器宕机时集群都能无缝接管并在固定周期内可靠分发处理任务。

- **三级状态分流**:阈值内锁定资格、溢出安全线直接报错、临界区挂起候补(1-5 分钟超时)。 - **后台周期批处理**:后台 Worker 每 10 秒批处理一次,解耦实时并发压力,统一执行座席拓扑计算并回写。 - **分布式调度底座**:分布式任务调度依赖具备高可用主从切换能力的协调器(如 Uber 的 Cadence 或带分布式锁的定时服务)。

[45:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=2700s) 后台任务与车次/旅程的 Booking 状态管理

在设计 12306 抢票的核心架构时,后台批处理是关键的一环。如果需要 background process,业界通常可以使用 Cadence 等工作流调度框架稍作定制即可搭建。后台进程本身在逻辑上并不复杂,真正的难点在于容灾:一旦出现单点故障(Single Point of Failure),必须具备可靠的重启机制。至于如何判定整趟列车“不可订票”(unbookable),成本其实并不高——当整辆车所有的 seats 全部被订满且没有发生 cancellation 时,状态即可置为 unbookable;而一旦有人退票(release/cancel),空闲座位就会重新变为 available。

更细致的粒度是将 unbookable 做到“具体旅程”(车次 + 起始点与终点)级别。例如一趟列车总共有 1000 个座位,其中北京直达上海的额度规划了 800 个。一旦这 800 个直达座位被抢光,北京到上海这一段就标记为 unbookable,但北京到石家庄的中途区间依然可以正常购票。此外,跨站换乘(cross-transit)场景在落库时可能需要向多张表插入数据,例如中转两次就需要写入三张表。为了保证原子性,一旦其中某程 insert 失败,其余行程必须自动回滚取消。因此系统中必须对换乘次数设定硬性上限(如最多允许 2 到 3 次中转)。最后,对于生命周期结束的列车,当车次发车开走后,其实时数据应迅速归档迁移至 historical log table,确保主库保持非常轻量(lean),以便持续高效地支撑读写请求。

- **状态管理**:车票可用性可划分为全局车次或“车次 + 起止点”的双重粒度。 - **换乘限制**:换乘过多会导致级联写入与补偿回滚成本过高,工程上应硬性限制换乘次数(如 $\le 2\sim 3$ 次)。 - **冷热数据分离**:发车后数据迅速切入历史日志表,保证在线库处于最小负载。

---

[47:15](https://www.youtube.com/watch?v=FI8G3VeI94w&t=2835s) 抢票的本质:高并发争抢转化为 Append-Only 追加写

传统做法中,每个车次面临每秒万级(如 10k~14k write/s)的资源争抢(resource competing),极易引发严重的行级锁竞争与数据库瓶颈。本设计的精髓在于将复杂的资源竞争转化为**纯粹的 Append-Only 追加写入问题**。前端请求涌入时,系统仅需在数据库中快速 append 用户请求,随后由 background process 定期扫描这些追加记录,批量裁决哪些用户成功抢到凭证。

裁决完毕后,后台进程将成功购票的用户集合(例如本批次判定的 600 人或 200 人)打包,通过直接调用或 Message Queue 发送给 Seat Allocation Server(座位分配服务器)。这种解耦对用户体验非常友好:抢票时用户最迫切关心的核心是“到底买没买到票”;只要确认拿到票,具体被分在哪个车厢、哪排座位,用户完全容忍系统多花 20 到 30 秒进行异步计算。如果不把这一步拆开,用户未获知结果前会频繁刷新重试,进一步成倍放大系统的写入洪峰。

```mermaid flowchart TD accTitle: High Concurrency Booking Flow accDescr: Diagram illustrating how concurrent user booking requests are converted into an append-only log and processed asynchronously by background batch and allocation servers.

subgraph Ingestion ["请求写入阶段"] U1["User Requests (10k+ QPS)"] --> DB[("Append-Only Log Table")] end

subgraph Decision ["抢票裁决阶段"] DB --> BP["Background Process\n(每 5~10 秒扫描一次)"] BP -->|"批量裁定成功名单\n(如 600 人)"| MQ["Message Queue / Direct Call"] end

subgraph Allocation ["座位分配与结算"] MQ --> SAS["Seat Allocation Server\n(单进程 DP / 贪心算法)"] SAS <--> ST[("Seating Table")] SAS --> PS["Payment Service"] end ```

---

[49:10](https://www.youtube.com/watch?v=FI8G3VeI94w&t=2950s) 动态规划(DP)座位分配与车次水平分片

Seat Allocation Server 拿到 600 个配额后,使用单进程(single process)即可轻松完成分配。由于是单进程串行计算,服务先读取已分配的座位表(allocated seats table),明确当前可用座位的空位分布;接着使用动态规划(DP)或贪心算法进行排座,计算量在千人级别时耗时仅需 1 秒左右,算完后再统一批量落库。即使排座服务意外崩溃,系统也可以直接从持久化的状态表中捞出“已判定买到票但尚未分配座位”的用户列表重新触发计算,具备良好的容错性(fault tolerance)。

在可扩展性方面,列车车次数量是有限且固定的,写入 QPS 与车次容量正相关,因此天然具备水平扩展(horizontally scalable)的特性。即便未来出现载客量高达 10 万座、单车并发冲到 1M QPS 的“超级列车”,也只需将分片粒度进一步下沉——例如以“车次 + 车厢区间”(如 1 号车厢 120 座、2 号车厢 120 座)作为 shard key,总能找到均匀合理的切分维度。

关于支付环节与座位分配的时序探讨,两者完全可以并行处理,甚至可以“先扣款,再排座”。因为大部分购票用户的支付都是合理可通行的(pass through),支付被拒的概率极低;一旦极少数支付最终失败,只需反向调用 revoke 将已占座位释放即可。在排座优化方面,一笔订单往往包含同行人的多张车票(如 1 到 15 张),通过 DP 算法在长方形的车厢布局中进行矩阵优化,可以从算法层面近乎完美地保证同行乘客能够坐在一起。

---

[52:30](https://www.youtube.com/watch?v=FI8G3VeI94w&t=3150s) 轮询周期评估与“无后台轮询”方案辩论

针对观众“12306 是否需要直接返回座位、后台任务引入延迟是否过大”的疑问,其实延迟完全处于可控范围。后台进程的执行间隔是可配置的(configurable):若设为 10 秒轮询一次,用户写入请求耗时约 1 秒,后台扫描加批量排座耗时约 1 秒,整体约 12 秒返回;若将周期压缩至 5 秒甚至更短,7 到 8 秒即可向用户返回终态结果。

随后,景祥提出了一种替代思路:能否彻底去掉独立的后台常驻进程,改为由用户请求驱动?即当积攒了一批(如 100 个)请求后,由其中某个请求扮演“临时 leader”,在写入时顺带触发执行,把当前尚未处理的座位一并分配给所有人。

讲师对此指出,两者的本质非常接近,核心区别仅在于触发源(trigger)是独立进程还是请求内逻辑。但让客户端请求充当 leader 存在现实缺陷:并发极端密集时,100、200、1000 等各批次请求几乎在同一毫秒涌入,很难协调哪一个请求该处理 1~100 还是 901~1000,容易引发并发处理权冲突。若为了防并发而引入消息队列(Message Queue)异步消费,为了保证排座一致性,最终依然需要退化为单机单进程消费,其实际效果与单进程 Background Process 殊途同归。

---

[56:30](https://www.youtube.com/watch?v=FI8G3VeI94w&t=3390s) 区间车票配额预分配与起止点 Hash Sharding

讨论转向了长途多站点线路的流量倾斜问题(如上海—苏州—武汉—济南—北京)。短途乘车请求量极大,这种站点间的斜率分布对系统架构到底有何冲击?

从工程角度来看,铁道部门早已基于历史数据(historical data)为各区间预先划定了车票配额(例如武汉到济南的 B1 区间固定分配 500 张)。因此,不同区间的票池彼此独立: 1. 北京到上海的直达票售罄,并不会抢占武汉到北京的票额; 2. 反之短途售罄也不会挤占长途配额。

这意味着该问题天然支持极细粒度的 Sharding: - **组合 Hash Key**:系统可以直接使用 `车次 + 起始站 + 终点站` 作为 Hash Key 进行分片; - **消除竞争**:不同区间的请求落入完全不同的 Shard,各自在其独立资源池(resource pool)内竞价,不存在跨机器的数据竞争; - **分片数量评估**:假设一趟列车停靠 10 个站点,站点的两两排列组合在平方级别(约 100 个区间组合)。将其分散到 100 个 Shard 毫无压力。即便是 Uber 的 Schemeless 架构,其分片数量也达到了 4096 个,百级分片在工程上非常轻松且能使负载更加均衡。

对于观众询问的“Auto-Incremental ID 会不会出现跨机重复”,讲师明确指出:竞争只发生在同一物理 Shard(相同车次与区间)内部,不同机器承载的是不同资源池,根本不需要引入全局自增 ID,更应当竭力避免全局队列(Avoid Global Queue),以杜绝全局协调导致的性能衰减。

---

[01:01:20](https://www.youtube.com/watch?v=FI8G3VeI94w&t=3680s) 关系型数据库的可扩展性与预分配内存方案评驳

针对观众李方远提出的方案——“因关系型数据库(RDBMS)扩展性存在瓶颈,故不宜将其置于关键路径,应将未售车票预先加载到内存服务器(In-Memory Database)中处理并异步回写”,讲师明确表达了不同看法。

讲师指出,关系型数据库的可扩展性上限极高,完全可以做到近似无限扩展(Infinite Scale)。以 Uber 的实践为例,采用 4096 个分片,若每台机器承载 10k QPS,整个集群理论上能支撑超过 4 亿(400 Million)的总 QPS,且分片层(Layer System)还可以支持多层嵌套。12306 真正的瓶颈从来不是单纯的高并发 QPS,而是在特定资源热点下的并发分配控制。

针对内存服务器预分配方案,存在两大严重隐患: 1. **持久化与数据丢失(Durability)**:在内存中极速应答并异步刷盘,一旦单机宕机,尚未落盘的售出凭单将会直接丢失; 2. **超卖与虚假售罄(Under-sell)**:各节点预分配配额极易导致部分节点票已卖空、另一些节点却票源闲置。即便引入客流预测(Demand Prediction)或 L4/L7 复杂负载均衡,也无法根本解决分配不均的问题。在真实的底层存储中,数据必须带有持久状态(stateful),前端再多的代理节点最终依然要汇聚到持有特定车次区间状态的数据库实体上。

- **高并发瓶颈本质**:12306 这类大型票务系统的核心挑战是多方对有限资源的竞态协调,而非无状态请求的高 QPS。 - **关系型数据库能力**:通过科学的业务主键分片,单机处理 10k/s 的行写入绰绰有余。

---

[01:05:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=3900s) 数据一致性容忍边界、Redis 单线程瓶颈与硬件负载均衡演进

关于节点宕机导致车票锁死或数据丢失的问题,讲师给出了关键的业务状态容忍度分界线:**“在用户购票状态尚未确认为 booked 之前,系统是完全允许丢失数据的(lost data)。”** 如果用户还没拿到票机器就挂了,直接对用户返回订票失败、让其重试即可;真正绝对不能丢数据的,只有已经向用户明确承诺“订票成功”的数据。因此在主从复制(Replication)策略上,完全可以采用异步复制来换取极低延迟;如果追求极致安全性,也可以在后台判定出票后,由 Application Server 确认成功写入两个以上的副本(Replication > 1)后再向前端返回成功信号。

对于“能否使用 Redis 扛写入”的提议,讲师认为虽然行得通但并非优秀设计。尽管 Redis 读取可以支持多线程,但其写操作与核心事务执行受限于单线程(Single Thread)。虽然单台 Redis 执行简单写命令可以达到 $10^5$(10 万)QPS,足以应付当前峰值,但把瞬时写洪峰全部压在一个单线程上,在容灾、故障转移(Fault Tolerance)和持久性(Durability)上都会面临诸多妥协。

最后,讲师延伸分享了一篇关于负载均衡(Load Balancer)演进的技术论文。微软在工程实践中探索使用商用通用服务器(Commercial off-the-shelf machines)全面替代传统专用硬件负载均衡器。传统硬件负载均衡器负责路由管理(Route Management)、服务健康检查(Health Monitoring)以及入站出站报文的目的地决策。这类设备高度依赖专业硬件,售价极其昂贵,动辄 8 万到 20 多万美元一台,推动了现代数据中心走向软件定义与集群化替代的浪潮。

[01:10:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=4200s) 廉价机器替代昂贵负载均衡与 Uber 的服务路由设计

讲者回顾了大约博士第三年期间学术界和工业界的研究趋势,当时研究所涌现出许多旨在用廉价商用机器(commodity hardware)取代极其昂贵的硬件负载均衡器(Hardware Load Balancer)的方案。早期的商用硬件负载均衡器价格极其昂贵,“一个就20多万”,因此研究人员尝试设计多层软件架构来实现平替。其中一种方案采用多层结构,首层是类似 Equal-Cost 的 Multiplex 机制,对请求直接进行均分而不深入检查 packet 细节;后续的 Multiplex 再将流量分发到底层的 Virtual Machine(当时尚未普及 Docker 和 Kubernetes)。为了保证属于同一 transaction 或 connection 的请求落到同一台后端机器上,系统通过一致的 Header 进行匹配识别,即便请求经过不同的 Multiplex,最终也会被分流到同一台目标服务器(如服务器 1)进行处理。同时,该方案还专门设计让出站数据包(outbound packet)绕过 Multiplex,用软件切分负载均衡的功能。

讲者认为这种 2013 年左右的论文设计固然有其可取之处,但技术演进至今已有“过期”之感,相比之下 Uber 的现代内部设计更为优雅和实用。在 Uber 的架构中,所有的出站/外部请求都会汇聚到 RTAPI(Real-Time API)网关,先由 RTAPI 读取用户的身份 ID 信息并完成身份认证与鉴权(Authentication & Authorization)。验证通过后,系统再根据 ServiceName,通过 gRPC 或 Thrift 的 Service Lookup 机制将请求路由到具体的后端服务。

Uber 的服务发现采用了类似 Smart Client(肥客户端)的设计思想。每个 Client 端在本地维护了一张路由映射表(Mapping),发请求时并不直接穿透昂贵的外置代理,而是先在本地进行端口转发或查找。例如要调用 Service A,客户端直接把请求发送到对应的本地虚拟端口(如 Port 12345),再由底层的路由映射表查出该端口对应哪台机器的真实 IP 和 Port,进而直接建立连接。如果目标节点不可用,客户端再去尝试本地缓存的下一个可用节点。当本地映射彻底失效(outdated)时,客户端便会向集群的全局配置中心 ZooKeeper 请求拉取最新的服务 IP 与端口列表。通过这种方式,Uber 内部专有的路由组件(如 Martin 等)与 ZooKeeper 配合,实现了轻量级、高可用的去中心化路由与状态维护。

```mermaid flowchart TD accTitle: Client-Side Routing and Lookup Architecture accDescr: Flow diagram showing how client requests are verified via RTAPI, resolved through local mapping, and updated via ZooKeeper.

subgraph Ingress["入口鉴权层"] Req["客户端请求"] --> RTAPI["RTAPI 网关"] RTAPI --> Auth["认证与鉴权 (AuthN/AuthZ)"] end

subgraph ClientSide["客户端路由 (Smart Client / Martin)"] Auth --> LocalMap["本地端口映射 (如 Port 12345)"] LocalMap --> RouteCheck{"本地缓存是否有效?"} RouteCheck -- "有效" --> TargetNode["目标后端服务节点 (IP:Port)"] RouteCheck -- "失效/不可用" --> FetchZK["向 ZooKeeper 请求最新拓扑"] end

subgraph ConfigPlane["配置中心控制面"] FetchZK --> ZK["ZooKeeper 集群 (Consensus / Membership)"] ZK -. "更新本地映射".-> LocalMap end ```

- 早期的商用硬件负载均衡设备成本极高,动辄数十万元,催生了使用普通商用 PC 搭建软件分流架构的浪潮。 - 旧论文方案通过固定的 Connection Header 进行会话保持,确保同一连接的不同请求被正确打散到对应的计算节点上。 - Uber 的架构由 RTAPI 统一承载 API 鉴权,服务间调用则重度依赖 gRPC/Thrift 与本地 Client Mapping 路由。

---

[01:14:48](https://www.youtube.com/watch?v=FI8G3VeI94w&t=4488s) 基于 ZooKeeper 的节点一致性与过期检测

在问答环节,观众 Harry(哈雷)提问:“所有的负载均衡客户端怎么保证 Mapping 的一致性?”讲者解释道,Mapping 的共识(Consensus)本质上正是通过 ZooKeeper 来实现的。ZooKeeper 在分布式架构中核心负责两项核心工作:全局配置中心(Configuration)与成员资格管理(Membership Management)。当集群中有一个新节点启动并声明“我负责 Service A,我的 IP 和 Port 分别是多少”时,该节点就绪后会主动向 ZooKeeper 注册;旧节点离线或更换时,同样会由 ZooKeeper 感知并维护成员变更状态。

而在客户端一侧,Uber 内部将路由层(Routing Layer)与 Internal Client 紧密结合在一起。客户端优先读取本地缓存路由去请求特定目标节点(如 Node 2)。客户端如何知道自己的本地映射已经过期(outdated)?机制非常直观:当向预分配的端口(如 12345)发起 gRPC 或 Thrift 请求时,如果服务端对应的 Docker 容器已经迁移或销毁,目标机器上已不再有监听该端口的容器进程,该次 RPC 调用便会立刻抛出连接失败(Service is not available)。客户端在遭遇调用失败后,便能立即确定当前缓存已失效,并自动触发对 ZooKeeper 的更新拉取,从而始终保持以 ZooKeeper 作为 Single Source of Truth(唯一信任源)。讲者补充道,在 Multi-tenant(多租户)或 Infra 基础架构组的视角下,这个流程的底层逻辑非常纯粹,就是一个清晰的数据面(Data Plane)与控制面(Control Plane/Secret Plan)解耦,并不需要过度复杂的协议。

- ZooKeeper 承担分布式共识基石,专精于全局服务发现拓扑(Configuration)与节点存活监测(Membership)。 - 路由过期不需要复杂的探测心跳,直接依据 RPC 请求失败(Connection Refused / Docker 容器下线)即可被动识别并触发增量更新。 - 内部网络环境下节点故障率相对受控,本地缓存加惰性刷新的方案能够在保证高性能的同时维持足够的稳定性。

---

[01:16:30](https://www.youtube.com/watch?v=FI8G3VeI94w&t=4590s) 下期预告:YouTube、Twitch 与斗鱼的流媒体架构差异

在讨论结尾,讲者对视频流媒体系统的设计做了一个简要的前瞻,提到设计像 YouTube 这样的大型视频系统在 CDN 分发策略与视频切片编码上具有相当高的复杂度。通过对比市面上主流视频与直播平台,会发现它们在技术选型与实现细节上各具特色且截然不同:

- **Twitch**:主要针对直播场景,其切片策略倾向于采用固定时长的小片段(如约 30 秒一个 Clip/切片)。 - **斗鱼**:在 CDN 侧的分发更偏向于维护同一个流式文件,随着推流持续向文件末端追加(append)写入新的音视频数据。 - **YouTube**:采用了自研的专用 Encoding 编码格式与流水线,视频会被切分为极短的时长单位(大约每 3 秒到 5 秒一个短切片),客户端根据带宽自适应拉取更新。

讲者表示直接打开这些平台的网页控制台或网络抓包,就能直观看出它们流媒体传输机制的显著区别。由于这部分内容涉及大量的 CDN 策略与格式编解码原理,讲者计划在收集更详尽的对比调研后,于下周组织一次专门的“Design YouTube”系统设计分享,逐一剖析这些工程实现的权衡取舍。最后,讲者向参会者致谢并结束了本场讨论。

## Notable quotes > [05:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=300s) 你是 proportional to number of nodes 的,每一个 byte 实际上都被增强了 1 万倍 — Scott Shi

> [10:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=600s) This is not customer facing, it's a data center... So this problem doesn't exist — Scott Shi

> [20:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=1200s) 比如我和景祥同时看这个座位 A 有没有人,我们两个事务看的时候都发现没人,然后我去订、景祥也去订,如果系统给两个人都返回成功,那就出现了典型的冲突。 — Scott Shi

> [33:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=1980s) Readers do not block writers, and writers do not block readers — Scott Shi

> [36:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=2160s) 已有的 12306 Rate Limiting 好像做的不太好,我看到好多程序员可以写脚本非常快地抢票;如果能用程序抢票,肯定它的 rate limit 没有做好。 — Scott Shi

> [01:05:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=3900s) 在用户购票状态尚未确认为 booked 之前,系统是完全允许丢失数据的(lost data)。 — Scott Shi

> [01:10:00](https://www.youtube.com/watch?v=FI8G3VeI94w&t=4200s) 一个就20多万 — Scott Shi

> [01:14:48](https://www.youtube.com/watch?v=FI8G3VeI94w&t=4488s) Service is not available

## Mentioned 12306, Scott Shi, 王小豪, 欣妖, 景祥, Mike, Brand, 李方远, Harry, ZooKeeper, BitTorrent, Amazon S3, YouTube, Uber, Flipper, Facebook, MySQL, PostgreSQL, Chronos, Cadence, Salesforce, Redis, Microsoft, RTAPI, gRPC, Thrift, Martin, Docker, Kubernetes, Twitch, Douyu

## Video description in this video, we talked about the design of 12306:

https://docs.google.com/document/d/1SxnY_qXui7OClJxgCYzAa2cXVIAseRPUOlhrccughGU/edit

and a code deployment system:

https://docs.google.com/document/d/1X0WBrqalFCCtc3U_pGWnsLPpSIfGvswXY-4U7ekGLb4/edit

[Source](https://www.youtube.com/watch?v=FI8G3VeI94w)

中文简报

12306将千万级抢票转为追加写 解耦排座消解数据库锁冲突 高并发票务的核心挑战在于有限资源的竞态协调而非单纯吞吐量,系统通过将抢票资格核验简化为关系型数据库的单行追加写入、并将耗时的动态规划配座剥离至后台异步批处理,无需依赖易失的内存预分配即可彻底消除行级锁瓶颈并实现水平扩展。 分享首先探讨了万级节点下 10GB 编译产物的自动化分发方案,提出结合 ZooKeeper 控制面与 BitTorrent P2P 数据面,通过追加合并流与单 Leader 架构抑制写放大和跨机房带宽瓶颈。随后,讲者重点拆解了 12306 千万级峰值抢票系统的架构精髓,主张将高并发“抢票资格锁定”与耗时的“邻座动态规划分配”彻底解耦,将激烈的行锁争抢转化为分片单表上的极速追加写(Append-Only),辅以按车次区间的细粒度 Sharding 和快照隔离。最后,讲者评析了纯内存预分配方案在持久化与虚假售罄上的隐患,并结合微软与 Uber 的工业实践,探讨了去中心化客户端路由对传统昂贵硬件负载均衡的替代演进。 针对持续代码部署中 10GB 大文件推送到万台节点的场景,架构采用 ZooKeeper 专职负责版本管理与成员共识,结合 BitTorrent P2P 网络避免跨数据中心骨干网带宽被击穿。 (0:00) 在 12306 订票系统设计中,系统将千万级 QPS 的抢票互斥锁争抢解耦为纯追加写(Append-Only)记录,将耗时的同行人选座等动态规划计算完全剥离至后台异步批处理完成。 (29:45) 铁路各区间的票额在现实中均按历史数据预分配,利用“车次 + 起始站 + 终点站”作为分片键可将并发压力完全打散至独立 Shard,彻底消除跨节点数据竞态与全局自增队列瓶颈。 (56:30) Uber 等现代架构使用 Smart Client 本地端口映射与惰性刷新机制,并在 RPC 失败时向 ZooKeeper 拉取最新拓扑,取代了动辄数十万美元的昂贵硬件负载均衡器。 (70:00)

Reading summary

讲者 Scott Shi 探讨了两个极端并发场景的架构设计:利用 ZooKeeper 与 BitTorrent 协同解决跨数据中心大文件分发的写放大与带宽瓶颈,以及通过“抢票判定与席位分配解耦”和 Append-only 追加写模型攻克 12306 春运千万级 QPS 的库存争抢难题。 大文件分发架构:P2P 传输与写放大抑制 (0:00) 在面对 10GB 级别构建产物向多区域上万台服务器自动化分发的场景中,传统单点网络直连或简单跨机房拷贝会迅速挤占昂贵的数据中心骨干网。系统采用 ZooKeeper 负责低频配置变更和强一致性成员管理,实际数据传输则交由 BitTorrent 构建去中心化 P2P 拓扑。为避免向万台节点广播微小写操作引发的万倍“写放大”,主节点通过 Append-only 增量日志记录变更,且非确定性函数(如时钟调用)必须先在 Master 上固化为确定性取值再进行全序广播。 增量累积到时间或体积阈值后,系统才触发版本递增(Pump Version),在本地将增量日志与基准文件 Merge 为完整镜像上传至 Amazon S3,最后在 ZooKeeper 注册最新版本。从节点下载中采用临时后缀隔离读流量,校验完成后原子替换。讲者指出该系统采用 Single Leader 架构,避免 Multi-Leader 带来的复杂大文件冲突;ZooKeeper 写频率应严格压低在约 1 次/分钟,高频写入(如 100 QPS)严禁直接落入共识集群。 12306 核心设计:排座解耦与追加写转化 (20:00) 12306 面对日常百万级与春运千万级 QPS,核心瓶颈在于多方争抢有限席位的“读-修改-写冲突”。讲者指出,多人购票的邻座优化在数学上属于动态规划(DP)问题,若在抢购链路中同步排座会直接导致性能崩溃。核心应对策略是将“抢购资格确认(Booking)”与“实体座位分配(Seat Allocation)”彻底解耦:始发与沿途各区间的定额库存预先离线切分,在线抢票被极致简化为向车次分片表中极速追加单行记录(Append-only),直接避开昂贵的物理行锁与即时计数器扣减。 写入完成后系统立即下发 Request ID 供前端异步轮询,底层利用自增 ID 充当自然排队队列。随后由独立的后台常驻进程(每 5 至 10 秒)批量扫表裁决抢票成功名单,再交由 Seat Allocation Server 在单进程内串行执行 DP 贪心排座并批量落库。针对临界用户,系统提供 1 至 5 分钟的候补队列(Waiting List)承接前序退票或支付超时名额,既优化了用户体验,又将高并发资源竞争转化为稳定的批处理流水线。 分片治理与数据库选型争鸣 (56:30) 针对列车多经停站的斜率分布,系统以“车次 + 起始站 + 终点站”作为分片键(Sharding Key),将不同区间的票池物理隔离在独立资源池内,完全规避了跨机竞争与全局自增队列带来的性能衰退。针对将车票预置于内存数据库(如 Redis)的提议,讲者认为并不合适:Redis 核心写操作受单线程约束,且在内存中应答极易发生机器宕机丢单,多节点预分配更会导致部分节点售罄、部分节点闲置的“虚假售罄”问题。 讲者坚持关系型数据库通过科学的分库分表具备极高的扩展上限,单节点足以承载约 10k 至 100k 的行追加。系统在“确认 booked”之前容忍节点宕机带来的重试数据丢失,只有出票成功记录才需要多副本保证。限流与节流防线必须整体前置于负载均衡或集中式旁路层,防止分布式 Shard 被跨节点穿透的恶意脚本压垮。 服务路由演进与去中心化客户端发现 (70:00) 在接入层与负载均衡的演变上,讲者回顾了早期学术界以廉价 PC 替代数十万元商用硬件负载均衡器的分流设计,并对比了现代微服务架构(如 Uber)的解耦实践。Uber 在入口处通过 RTAPI 网关统一收敛身份认证与鉴权,服务间调用则采用 Smart Client(肥客户端)模型,在本地维持虚拟端口映射以实现去中心化路由。 客户端根据本地缓存直连目标服务节点,当容器销毁或迁移导致连接抛错时,客户端即被动判定缓存失效,进而向 ZooKeeper 增量拉取最新拓扑。这种“控制面(ZooKeeper 维护拓扑一致性)与数据面(本地端口映射直连)解耦”配合“故障惰性刷新”的设计,省去了高频的主动心跳探测,在复杂网络拓扑中兼顾了低延迟与稳定性。 无论是海量大文件分发还是极端并发票务,核心思路均在于规避全局锁与细粒度同步协调,转而通过版本化批处理、异步解耦及控制面与数据面分离化解瓶颈。讲者最后预告下期将聚焦流媒体架构,深入对比 YouTube(自研短切片)、Twitch(30 秒切片)与斗鱼(流式追加)在 CDN 策略与编码流水线上的工程取舍。

Source quotes

[5:00] Scott Shi: 你是 proportional to number of nodes 的,每一个 byte 实际上都被增强了 1 万倍 [10:00] Scott Shi: This is not customer facing, it's a data center... So this problem doesn't exist [20:00] Scott Shi: 比如我和景祥同时看这个座位 A 有没有人,我们两个事务看的时候都发现没人,然后我去订、景祥也去订,如果系统给两个人都返回成功,那就出现了典型的冲突。 [33:00] Scott Shi: Readers do not block writers, and writers do not block readers [36:00] Scott Shi: 已有的 12306 Rate Limiting 好像做的不太好,我看到好多程序员可以写脚本非常快地抢票;如果能用程序抢票,肯定它的 rate limit 没有做好。 [65:00] Scott Shi: 在用户购票状态尚未确认为 booked 之前,系统是完全允许丢失数据的(lost data)。 [70:00] Scott Shi: 一个就20多万 [74:48] Service is not available

Original transcript

[0:00] 好,我们今天开始聊两个就是之前在群里面讨论过的Design。第一个Design就可以认为是Scalable File Distribution System。嗯这个其实还是有挺多use case的,我记得王小豪同学也在那个群里面说了一个就是有一个design的问题这个很像他当时问的问题是怎么样就是build a continuous code deployment system,然后这个code compile 以后可能很大,可能10GB然后你需要每天可能要上千个Deploy然后你要做的就是把这个code compile然后link compile,然后compile完了以后把这个code push out上千个那个server上,然后他是每天都在就是automatically做的。这个的话其实和这个很像,他那个设计我也是借鉴了一下那个设计吧。然后这块的话这个问题也是类似的,就是你有一个multi-region,multi-zone data center。然后你需要一个不停的记录一个就是你需要不停的 replicate 一个 changed file然后这个file呢可能是大概10GB,然后你需要去crow到比如说100sign nodes,或者10sign nodes,然后每个nodes可能会go down并且不能有任何 data loss 然后应该怎么设计然后我想了一些solution,然后借鉴了一下,也是借鉴了一下那个就是他当时YouTube的视频里面那个engineer solution核心思想就是用Rulekeeper来keep一些configuration,比如说你的location or masterfile在哪里。然后你的这个MasterFile的VersionChange在哪里,就是是什么样子的。然后你MembershipManagement,SharingGroup也可以。这个MembershipManagement,这个就本来就是ZooKipper做的,ZooKipper是专门做MembershipManagement的。因为它中间有一些Consensus,比如说哪一些,有一些Consensus是比较重要的。比如说哪个Member是退出Group了,哪些Member是在Group里面,这个Consensus是比较重要。还有的话就是transfer的时候是用bTorrent或者是 peer-to-peer network transfer large file 的。我这块由于时间限制,我没有对 peer-to-peer network 进行深入的谣解。然后大概就是有个种子,有个Seed Group Management,然后每个的话就是我看他Seed group有个limitation是大概有2000所以如果你这个Seed过多的话,那剩下的可能只是download的,可能就不upload了,还是可能有个这样的限制。我这块没有了解太多,我只是简单读了一下这个就是写的时候的configuration,然后有可能可以用 BitTorrent 去管这个Membership 因为这个 BitTorrent 设计的时候就是 peer-to-peer network 他就assume 这个Members 可以 join 然后 exit frequently所以你可能也能用 peer to peer network 去管理 membership可能也是够用的但是这样的话你还是要有一些还是要有一些那个,还是要有需要有一些啊就是管一些比如说你一开始的时候他是有一个address能找见这个peergroup在哪里应该有这种,之前有一些比较老的设计有什么super user,butolerant好像比那个稍微先进一点。所以我不是很清楚他具体怎么设计的,但是已有了这个设计应该是能满足这个要求的。谢谢大家谢谢大家好,大家有对Bittorrent比较了解的share一下knowledge吗?有人说加不进来这个room meeting谢谢大家好吧,那我看到还有人的交流,应该没问题,我们接着聊这个。所以这块的话,首先就是设计思想就是用这个Rulekeeper加上这个Bitorrent来分享File。Bitorrent分享File的主要原因是不想就是havveload一个Node,比如说你如果只有一个Node去这个,去share resource的话,你用node copy events,node network就被卡死了。一般来说,特别是如果你要copy,node要copy到另一个node,另一个node是在另一个datacenter里面,你要再用最宝贵的,就是inter data center network,这个是最宝贵的,一般的datacenter里面最宝贵的资源,所以这个一般是不,就是除非你有很好,一般来说你是想avoid, [5:00] 这个resource占用的所以说 我这块想的想法就是 首先Master File的话不应该change too frequently这个MasterCell不应该change too frequently那么否则的话你的read amplification就是你这块写一遍然后你replicate 要replicate一万遍你是 proportional to number of nodes 的所以说如果你每一个byte都update的话,你实际上每个byte都被增强了1万倍所以这块这个MasterFile不应该去写更多所以你可能需要一个是那种batch,mini batch,或者是这种看一个固定的时间段来做的,这是一个想法其次的话这个File的话,在一开始写的时候,在MasterFile就有一个,应该有一个single point failure,防止有single point failure,所以应该就replicate,但如果你说MasterFile可以丢失的话,那其实就不需要replicate然后再其次的话,你在append这个change的时候,就比如说你现在file是在有ABCD,你append的EFD的时候,你这个不应该有data loss的。然后最后的话是如果你 read from replica 的话,你可能会读到 still data,但是不应该有 corrupted data,就是你如果从 replica 读的话,你可能需要一些过时的 data,不应该有 replicate data这块想法的flow也是比较straightforward的首先有一个small group machine,这块是主要做replicate,如果你没有replicate的需求,就是它允许certain degree or data loss的话,你就一个也可以。然后你这个machine是keep to up to date version of files,然后Each of the write,就是你比如说我现在有个write request,我要写这个file,有个write request,我必须write到所有的machine然后才能说 return success to requester如果只 write 其中一个或两个的话,我不能 return success to requester然后 new write 是 append到 change logs 的,比如说你现在这个file name 可能是current version然后你现在在append的这个version 还没有publish version 可以叫new version number 我们可以叫做 version 12345那么当你replicate的时候,这个write order,replicate order是total broadcast orderTotal Broadcast Order就是首先没有Write Loss就是你没有Write Loss,你没有Loss of Data,其次的话Order Replica得到那个change是exactly the same order as the master node.就是Total Broadcast Order,就是说它是一种replication的一个,就是一种order,说明什么呢?你所有的replica拿到的data和master上拿到的data应该是一致的但是这块有个catch是什么呢?因为在database里面,这是个file,就是说写的都是string了就无所谓了但是在database里面即使你guarantee total broadcast order,你data可能还是不一致,因为你在database里面你可能比如说靠的是timelow,那你在这块这个master machine的timelow和replica的timelow就不一致是吧,那你这种情况下的话,你这个master machine的靠timelow的时候可能要把这个就是这种function转化成具体的数值,比如说你在master上写timelow的时候,首先把timelow转化成6点15分,然后你replicate的时候你replicate6点15分,而不是你说你replicate的时候,你replicate6点15分,而不是你说你replicate的时候,你也可以说replicate time now,那你这样的话在master在那个replica上的timestamp最后只是不一致的但是最后的话,如果没有data loss的话,允许data loss的话,可以不做replication。然后我们看到这个新的request呢,其实是不停的append到这个file的上面的。是不停的append到file的这个,比如说fileA12345上的,然后当你的certain threshold到达的时候,比如说你是过了一段时间了,或者是你这个write已经超过某个数量,或者是你这个size到达每个数量,你可以pump version了。那么你新加的append file,就是你首先新建一个新的append file,就是version+1。那这样的话所有的新的request write request要写到这个version+1这个上面然后就是之前那个version比如说现在新的version是124那123这个version和之前的file就merge起来了你就把那个file就是123D version和这个incremental update和这个旧的file merge 起来然后当你merge起来以后,你可以用这个master node,只用master node来upload,把这个uploaded merge file呢,to Amazon Street,我们说过M1 Street是object storage,你可以认为是dakabase,也会叫object storage这些,然后当这个file upload成功的时候,你就去register入keeper,入keeper的话,Uber里面有个叫flipper,我不知道他是用rootkeeper,底下是用rootkeeper还是用,ETCD的 反正它肯定是用一个consensus service来实践的RuleCable上它是一个global configuration所以他 [10:00] Guarantee you, basically guarantee you. when you write it, it has linearity, You can read the book you can read the book you can read. uh, You will read the most of the first time, it is a global configuration service. But when you use rulekeeper, you should be careful. Rulekeeper expected use case is you don't write too often. Because every time rulekeeper is going to be a... Leader election, I saw Uber said that the stimulus is a leader election. It's available to 200ms. This is still a bit longer. But for a long time, the leader election is a little longer. This time is quite a long time. So performance is not good. So QPS, rulekeeper expected QPS is one request per minute. If you have 100 per second, you may not be able to use rulekeeper. Rulekeeper can ensure replica The data is consistent. This is the question, how uniquely identify a file? Different users can upload the same file name, but the content is not the same. This is what you can do through application level to solve. If you file is the same, the content is the same. This is not customer facing, it's a data center. Just wrap, quite sure to copy the data by a large file to distribute it to a selected node. This is not customer facing. So this user is application code. So this problem doesn't exist. The design of the design is Master node, and then Upload the file to the S3. And when it was done, when it was done, it was done. The root keeper version pumped. It was pumped from the new version. It was pumped from the new version. It was pumped from the new version. It was pumped from the new version. If master is done, you can use slave file to do the same thing. Because we said before, he wrote it. Master and slave. Master is done. Master is done. Slave is done. Slave is done. If it is done, it will be done. So you slave is not lost. So you can also use slave file and then upload to S3 and then pump the rootcaper version. Rootkeeper is also tracking group membership or broadcast group. If it has a membership change, it will modify the rootkeeper. So it will keep track latest and up-to-date group members. This is the rootkeeper's main usage. It is tracking accurate group membership. as a download command. Group member is a million. So the download command of group members is using BitTorrent to share this file. There is a seeder, which is to select some to download. For example, you first download 10 people, then you will go to seed group, and then you will go to seed group, and then you will go to seed group, and then you will go to seed group, and then you will go to seed group, and then you will go to seed group. And this is the file that says that there is a file. deduplication, then use file size and hash signature to do. This file deduplication has an image deduplication, and an image DNA. This is a very simple way to use file deduplication. um Some of the images are in the safety of Facebook. I'm talking a little bit about imageDNA. Facebook is what is happening. There are many people who have published a false video, a red video, a red video, or a red video. That it must be done. If you just done this format, it can reformat. It can change, make it simple changes. It can be uploaded. It's like image DNA is the same thing, it's like from the image in the image, you can change the color, or the size of the size, and the size of the image. Then you can do the DDo. Depending on the use case, if you have a file file, you can use file signature, hash signature, or image. Then you can use image DNA to do the DDo. How? So we have some details Um, maybe, [15:00] 要做这个project还是要去掌握的,就是去做deep dive的比如说这个 BitTorrent 我记得 BitTorrent 是有一个 magnet link 磁力连接 然后你可以通过磁力连接去你可以通过这个磁力连接去那个去download的这个就是就是加入这个这个share group,然后你可以作为leacher或者是seater,seater的话就upload的然后Literature就是download所以这块可能还需要一个process去Manager就是当这个Rulekeeper Pump新 version 需要有一个stable的process 然后去不停地去manage谁是seater 谁是seater 谁是leater 然后保证这个file distribute 过去对于任何一个reply卡来说,你可能会有同一个file在不同的version比如说你可能有file version 1,file version 2,这是download好的。然后你可能有个file CR download,就是表示它在download的过程中。那么因为你在download过程中说你会不会显示的,不会受read request的。然后这个file 1这种是old version,still version,可能就是在过一段时间后会被在这个replica上清除掉。然后这样的话保证你这个机器没有多余的file。因为是就是version最高但是没有这个没有in progress一种然后download的时候如果用过Bitcoin的话就这样他也会有个判的一个什么CR download的当他download完的时候他会把这个CR download的给remove掉就是 rename 成 File A version 3这样的话是可以满足这个需求的。我觉得这个设计是work的,但是这个bit torrent这块还有一些细节的问题。可能就是如果真的要做的话需要去研究一下这个的话,我觉得大家有什么讨论的吗?就是有什么想法吗?对于这个设计。可以直接发言如果没有什么讨论想法的话,这个设计可以用在多处的,你可以想一下你要copy一个file,快速的copy一个version的file去10k nodes的话,这个设计其实还是可以用在很多处的,比如说你这个code deployment,比如说你这个Broadcasting,其实你就是Broadcasting large file,在Data Center里Broadcasting large file嘛所以这个Rootkeeper加这个BitTorrent应该是能解决的。中间还有一些细节可能需要figure out,但是这个应该是可以解决的。特别这个BitTorrent这块,我没找见一个很好的一个Design Dock,我觉得中间还有一些细节需要解决。我们在讨论这个12306这个Ticket Booking System,这个相对我可能比较熟一点。所以我之前也想过这个问题,所以我们可能这个我不确定的相对比较小一点。然后欣妖问这块可以用刚才那个地方是可以用Multi Leader吗用 multi leaderMate Nieder如果有multi-leader的话你的file会有conflict,这块result conflict很麻烦,我觉得没必要,还是single leader 挺好的。好好,我们现在就是讨论一下这个12306的Ticket Booking System。这个问题我也想过,之前想过一段时间,但是这次讨论的时候,具体的把一些很多Solutions写下来的话,我觉得还是有一些挺有趣的地方的。就是12306是国内的一个火车订票系统,它的peak QPS是在一个million左右然后他但是他这个QPS可以grow到more than 1 million,就是可能grow到那个什么10个million,甚至更高。那么在这个里面我还加了一些额外的feature,因为我其实没有用过12306这个网站,因为我出国的时候还没有高铁,所以我没有用过12306这个网站,所以他说有一些,我想的他就是我见了一下,就是可能说您可以比如说您可以book一个tickets从CityA到CityB那可能会经过多个旅程,比如说你可以从北京到杭州,可以变成北京到Hong Kong,可以从北京到杭州。然后你book 比如说从北京到上海的那个那个那个就是从北上那条线book从北京到杭州,然后从杭州到广州你可以比如说从那个杭州到广州的线进杭州到广州,然后road3的话可能说从广州到香港,就是你其实可以把一个tripbook成几个小的阶段。然后你的客户应该也可以revoque a book at any given time,这个我觉得应该是support,应该可以支持的。然后我查了下就是大概高铁的每个set是有一千个左右的座位然后18 [20:00] 电源的traffic是33亿人18年的traffic是33亿人次Gross Rate 是 9.4 那最高的Day record 是13个million人次 per day,这个是在19年春运的时候我简单的estimate 了一下,它实际上是expect的是一天可以handle50亿人次,就是一年可以handle50亿的traffic,差不多就是能用应付大概可能10年左右差不多够的然后其次的话一天可能一天的话如果是50亿人次每年的话呢每天平均下来就是1400个Million1400万仁慈那么如果是假设85%的占坐率的话,那实际上需要一天发车是1600016500册次然后我就是这块假设的话是春运这种高峰时刻的话应该能handle多少呢?应该能handle28个million每天,28个million每天的话,用简单的QPS来算的话,就是因为现在的QPS是一个million嘛,假设它这个QPS是10个million的话,那么10个million一万辆车的话,实际上我们车数应该是高于一万辆的,10个million每天是一万辆车的话,所以它其实每辆车的人次定做的 2PS 是 1K就是他这个每辆车就是你可能一天是有10个million的QPS,就是高峰值就是10个million QPS,但你平均到每辆车上的QPS大概是1K,就平均到每个车次上,比如说从北京到上海上午12点开出发这个车的QPS大概是1K左右然后我这块设计主要想的是它这个Botonex在database上呢所以最常见的就是比如说你很多人同时就是做同事book同一辆车的时候,同一辆火车的时候,那么你很有可能有发生这种read, modify, write conflict,就比如说这read, modify, write, conflict是什么呢?比如说你在订那个theater票的时候就说,我和,比如说我和景祥同事看这个这个座位A有没有人,然后然后我们看都没有,我们这个transaction看的都没有,然后我去book然后紧想去book,然后我们都返回成功的话,那我们就出现conflict的话,就是read,modify,write,conflict这是一种常见的conflict, read, modify, write conflict是有一种常见的 concurrency control model是可以解决的就是那个 two-phase lockingtwo-phase locking是怎么解决的呢就是你的当你RAID的时候,当你进行TransactionRAID的时候,你是拿一个读锁,就是拿了一个读锁,读锁是RAID Lock是Non-Exclusive Lock然后当你write的时候你拿到是一个exclusive lock的邪手就是exclusive write lock所以当我们俩同时读的时候,我们是同时,就是我,比如说我和景祥同时在读这个座位A,那我们其实两个同时拿到了读锁,那都是non-exclusive,说我们两个可以同时读。但我们其中有一个想去站这个座位的时候,他其实就必须把这个读锁变成写锁,那我必须把这个,就是不管这个lock granularity是在这个table上,还是在这个row上,我必须把这个row或者这个table lock住。当我去尝试去lock它的时候,那我就必须,因为它是exclusive的写所,所以我这个另一个那个景祥的读,必须被我reject掉,必须被我踢出去,是吧?然后我必须把这个景祥的transaction reject掉以后,我才能就是也没有其他的读的时候,我才能拿到这个exclusive的这个写所,那我才能把这个座位站上这是 two-phase locking这样的话我是可以把这个座位占到的就是two-phase locking可以做到谢谢大家但是我们这块讨论的时候就是这个最常见的问题就是re-modify-write conflict,这是很麻烦的一个问题,这也是很多时候你看起,比如说12306很大的一个很高的QPS有时候能handle,但是你去电影院book一个tickets有时候会fuel的原因其次的话,我这块设计的时候,我想的就是relational database因为你需要 strong transaction,并且这个 strong transaction 不只是这块,还有什么需求呢?你 book tickets 的时候,你可能先占了这个座位,说先占了这个 tickets,你很快要做的是 transaction,表示你有 money 去 pay 这个座位。那这块的话肯定也是要follow up做一个就是transaction的时候肯定是要就是financial transaction 肯定要用relational database 比较好,用noSQL的话可能会有各种各样不必要的麻烦然后再其次的话这个database是一定要sharded的这个肯定很明显的这个如果你有平均到一天有10个million人QPS的话,这个database是一定要 [25:00] 一定要被sharded 10 billion QPS 的话你这个10个million QPS的话,这个是一定要被sharded的然后我们这块的话因为他这个QPS可能也没有这么多train,所以这个QPS可能还是要比这个EK要大一点可能还是要比1k大一点,我记得我有不同的,就是用不同的方式来evaluate,可能是个10k左右。这个是假设,你就是比如说一天一开始的话,就比如说开始预定明天的车次,那我明天1万辆测试同时开始预定,但是很多时候可能好像是分开预定的,所以你这个QPS可能还是要比这个EQ要大一点的,每个测试。然后计划请向问了一个问题说你如果acquire这个write lock不会被shared read lock block吗会的会的,所以你这个write lock是exclusive lock的时候,你必须把所有的read lock给踢出去,这是非常传统的就是你所有看最早的relational database都是用two-phase locking时间的,这个叫two-phase locking,包括你上database的课都是用two-phase locking时间的,这也是从上世纪70年代末到,反正到最近还在用这个two-phase locking这个是会的好然后这款Database我们要shared的,那我常见的用一个shared的key是什么呢?就是我想的对这个测词,就比如说这1234在12.30分出发作为一个测词,然后把这个作为sharedkey然后把data删着过去好Sycle里面,Two-Face Locking QPS 多少?我记得几天,下个一个数据是QPS是100K每秒,最新的Data Center里面比较好一点,这么时间是100K每秒。100k每秒锦翔说:现在database用hybrid是用Two-Face Locking+Multi-Version Concurrent ControlMarpy version concurrent control 简向你试一下我意思是说,现在其实,如果是做于Transaction Possession的DB的话他locking已经不在table本身,他已经lock down那个每一个Low上面去所以我每次更改一次Database都会在每一个Database上的Low加一个他的insert transaction id 跟他delete transaction id然后通过这个insert跟delete的全数来判断你当前的陈先生能不能看得见?所以他就不需要lock整张表格,只需要lock一个lock就好了这是MultiVersion的部分,2-phase locking就是刚才Scott说的。现在主流Database一般都是谁先拿到Lock谁赢,剩下Lock都会被abort掉。就是比如说两个层层更改同一个肉然后第一个 transitionupdate 它就会在那一行上面lock住那张表 如果下一个进来也要更改这个的话会直接被uplo掉这是为了提高那个throughput做的一个选择你说的是不是就是这种啊,比如说这个Case,比如说我去读这个balance啊就是这个transaction 12说我去先读这个balance 然后呢其次的话嗯等一下这个case怎么会上我看一下Transaction level is the transaction level.isolation, that's called isolation.这个怎么说,你这个是语欲上的一个东西哦,就是对一个,对于两个全身的更改,就是,Rate跟Rate同一个set的数据的话是用这个东西来表达的但是如果是具体到一个表格里面不同的数据的话是用我说这种方法的它其实是single row的locking是吧?嗯,ok,ok,这个不直接相关,不过我后面会简单谈一下snapshot isolation 的问题,好好的好的 谢谢Shar然后我们接着谈这块就是这个shardingKan的话我propose这个shardingKan是车次就是按车次来shard那么其次的话这个says应该是pre-determined pre-allocatedPredetermined and preallocated的原因是什么呢?一方面是有很多offline [30:00] 比如说你还是有一些售票亭去上,并且比如说你这个从北京到上海的车次,你石家庄站也是要reserve 一部分从石家庄的车次测票的,所以你这个还是要去这个就是先分好的,就比如说你从北京到上海是1000个测座,有800个测座是从北京到上海直达,剩下200个的话,其中100个是从北京到石家庄,石家庄到上海,这种先分好的。然后其次的话好像现实生活中我看到有人在铁路局工作就是他父母在铁路局工作也说铁路局也是这么做的然后我这块这个想法呢比较简单就是我这个想法比较简单,就是把booking和 set allocation 这两个动作分开booking就表示你有没有book到你有没有book到,比如说这个是pre-located,比如说北京到上海有1000个车票北京上海有1000个车票,其中有800个是直达那从这个这个这个这个肉特里面从北京到上海这个直达的肉特里面有800个那我book的时候就很快写一个record,然后它有个sequential number,就相当于是在用sequel里面时间一个queue,如果在前800个的话,我肯定是拿到测票的,如果在后800个的话,可能会没有测票,所以这个booking就表示你有没有测票,但是你book到车票以后呢,你还需要去做SETA location,这个是为什么呢?就是因为book车票上,你可能book多个嘛,比如说你一个人book了10个那我肯定还是优先把你10个人安排到相近临近的做,并且这个allocation呢,实际想一下它只要数字不是1的话,你都有一个DP的问题。就是你都需要可能,你要optimize的话可能都是要DP的问题。所以这个现在location是分开的,就是original想法就是分开的。所以说,比如说我们做北京到上海这123412:30出发这个为例子。首先这个管理员就决定了有多少个从北京上海直达的,然后有多少个是从北京到石家庄上海到石家庄的。然后把它就写入这个database了,就是它通过一些管理的后台就把这个就写了database了,所以你可能很快的需要读,比如说你放了一个configuration db,或是放了一个in-memory db,你可能不需要不停的要读这个来check是不是到了capacity了。然后其次的话呃shard的话用车次来shard的然后这个database是用snapshot isolationsnapshot isolation就是跟我们之前说的two-phase locking不一样snapshot isolation什么的你读的可能是still data你会读snapshot isolation说白了就是你写的时候不被读给block你读的时候不被写给block当你读的时候,你可能读的是比如说你现在transaction12345,我们举个例子比如说你Transaction是T1,T2,T3,你读的时候,你可能就是当你读的时候,你可能已经T2 T3已经写好了,但是你读的这个timestamp可能读的是T1,所以你只能读到transaction1之前的,就是这个write1之前的东西。你读不到T2和T3的东西,就是你读的data可能是有点still的,但你不会读一个wrong data,同时你写的时候你是不被读。给lock的 不像2-face lock的 你不会被读给block掉所以它这块可以用snapshot isolation来做这个然后MySQL和Postgres都有snapshot isolation就是你可以configure你database来做snapshot isolation然后这个proposed data schema是什么呢?就是首先有个Ultra incremental ID这个AlterIncreate的ID说白了是一个Visualize的一个Q,就是你实际上把这个作为Q的功能,谁的指晓,谁就最后先抢到票,是一个满足Q的Functionality。然后有个UID,这个UID是用车次来hash的,这个UID是为什么有呢?你当前有一个million的request来booking system你需要很快的找见在哪个database是存你这个booking的,所以用hashableUID呢能很快的allocate在哪个database上如果每个车次抢的人也很多的话你可能要把每个车次还得break出来,你比如说每个车次分成3到4节,平均分成3到4节,也可能要这么做,也可能需要这么做。那这样的话就可能就是hash车次再加后面再加一个数,比如说等分的话,4等分的话就加个1234。然后呢你有就是有一些train number啊 start time啊然后就是user 比如说我是user12345就可能拿身份证来做user然后我book了多少个tickets,我有个request UOID,因为你这个设计中,你知道你拿到车票是需要时间的,需要去系统来看到你是不是在前段。你写了 [35:00] 的时候你其实不知道自己拿不到哪测调其次你拿到测票的座位在哪,是另一个process处理的,所以这个需要有个request uid当你request的时候,你会不停的po request UID,然后知道你到底有没有拿了测票。然后最后这块是个rote,rote的话比如说用A1,A1就表示北京到上海,B2就表示石家庄到上海这种加个timestamp,然后再加个state。这个state的话,我这块想的是比较简单,就是三种情况: empty,book和cancel的AMTEK可能有两种情况,一种就是系统还没有决定你有没有分配测票另一种情况可能就是你在一个waiting list里面,就是你前面有够100个人了,但是你不到waiting list之外,waiting list number是可以configured的。然后这个booking table需要index上passengerID,requestID,root和primarykey这个PrimaryKey自己有代index,所以你主要index就是passengerID,requestID和wrote,requestID比较好理解就是你不停的requestPort他说这个不停的去扑他说这个这个不停的需要去扑他说看他什么情况了passenger ID 的话你可能就是另一种 request 就说我想搜一下我作为这个passenger 我的那个车票有没有 book 成功Route 是为了系统System scan你之前有多少个人book了,然后决定你能不能book到。然后这块有人问,静怡问说这个UID是hash什么?UID是hash车次啊?就是比如说G1234号列车6:09分,6月9号12点开车,hash车次作为这个,最后guarantee所有车次就是这个车次的这个request 最后到同一个database好就到同一个database然后所以呢,你做Book的时候你做book的时候实际上你只做了一件事就是在这个这个table里面碰到了一个肉你做就是在这个table里append的一个roll所以这个操作是很快的,就是append的一个roll,就是很快的一个操作你没有去看你跟谁在抢Resource你没有做任何事情,你就是在这个table里append的一个row然后假设这个read/write ratio是10:1的话,就是一个写十个读的话,那么这个QPS呢可能是10K,之前按1K来160K,不过即使是100K的话,100k 的话其实这个SQL Server是 handle过的, handle住的,一个单个的SQL Server是 handle住的,是吧?所以它这个一个SQL的Sharz是可以很轻松的 handle一个车次的,就是 resource competing的。然后当User book了 tickets的时候,我们就 return request id 了。那它可以asynchronize的去Pull这个request, 当然我们不想,因为这个client是我们可以自己设计的,就不能让它too frequent, 然后也可以加rate limiting和throttling, 然后我想说这个rate limiting和throttling一定要加在这个load balancer上,因为这个load balancer或者是lookaside的server上因为当你把这个request sharded到1000个server的时候,虽然你当前这个server可能来自于IP的request只有1,但1000个aggregate起来可能有1000多个request per second所以你这个load 那个就是sortle和这个rate limiting一定要在上面那层,或者是你单独有个专门做rate limiting的这个专门做 rate limiting, keep track of IPrequest per second的这种一个service,就是look-aside service,这样的话可以做regnancy meeting因为这个rate limit 长电操作,slotolent是长电用操作,我这块没有单独提。好像已有的12306 Rate Limiting做的不太好,因为我看到好多人就是程序员可以就是hack他那个system,然后能非常快的抢票,那如果他用程序来抢票的话,那肯定他Rate Limiting没有做好如果能用长序简报, 肯定 rate limit 没有做好。好,所以它这个,因为这个读和写不 block each other, 所以follow的读的话是不会, 就是follow up读是不会 slow down 写的。然后其次的话显示还有多少个 tickets available呢?就是一个简单的读,你不会影响写,所以不是book的 bottleneck,但是你读的可能是still data,但是我觉得现在user都expect你读的data可能不是最新的data,他们知道这个抢得很快。然后我觉得这个tricky part是有的user可以cancel booking出现多种情况看似我不停这种情况下的话你在 rating list 里面可能会被promoted比如说这种情况下的话当我book的时候我在这个book table 里面写了一个roll的时候可能会发生以下这种情况一种情况是你在这个stretchhold 之内那这种情况的话你就相当于这个ticket被你book掉了,你就可以return success [40:00] 另一种的话是你在waiting list 之外,比如说是800个直达车次,你在前800个直达车次,就是说你book到车票了,但是你在哪个座位可能需要计算下来返回给你。但是如果说你在1000个座位,你在2000个以外,那你就可以直接return fill了,因为你不可能有车票给他,就fill掉了,然后waiting list number可以configure的。那如果你在waiting list 之内的话,你可能等个几分钟,等到1到5分钟之间,这个number也可以configure。如果之前的就是user cancel booking的话,你可以return success,就是轮到你来可以return success,否则的话timeout的时候return field,这样的话是可以保证这个。如果你说的wait list都清楚了,然后还是有一些empty ticket的话,你可以把这个车次再open up出来。这个好像也是常见的操作,不是经常就是你提早去book ticket的时候发现他之前有一些book很满的额外的空位了,这也是可能常见的。但是你在你在你在open up的时候,这个QPS就不会很高了,因为不可能所有人都盯着看,然后瞬间去make requests好然后这个座位分配呢我这块想的是用一个backend process, background process这个原因是,首先不能根据QPS来,因为这个QPS可能这个上线和下线都很高,你这个QPS如果是10万个QPS per second的话,肯定是不skill的。所以这个background process可以比如说每10秒处理一次当他去的时候就实际上出一次的时候他可以读到他首先可以读到configuration说从北京到上海有多少个测票在这个车次里面有多少个车票多少个 available然后他按顺序读,读到所有的book的,知道有多少个车票已经被book了然后再读哪些人在Q的前面还没有book到测票,然后他就把那个先说这几个人可以book掉了,那就把他这个write back of the table说book掉了。那哪些人是clearly out of the waiting list 了,直接fail掉。这个是可以做的,就是用background process来做这个decision。然后有人问为什么需要waiting list,可以直接fill吗?我觉得可以直接fill,我觉得我做waiting list的原因就是嗯提供一个稍微好一点的user experience,因为我觉得这个failure rate应该是挺高的,就特别直接book,然后fail的rate可能很高,fail的原因可能有多个,比如说你request就是从北京到上海,上海到广州是一个转程车票,然后其中一个fail的话另一个也应该fail,其次的话,当你request的时候,你可能刚requestbook完了以后,你可能要去刷信用卡,让你信用卡fill的时候,或者你的payment field的时候,你可能也需要mark field,其次的话,你book的时候有可能book出来,所以一开始的failure rate应该很高,所以我觉得这块设计一个waiting list,但我承认是optimization,可以把waiting list取掉,应该没有任何影响。如果一个车被Fully Book的话,它可以就直接在Configuration Server就说这个Unvailable,那你在看的时候就不显示它了,也不给你提供Book的选项了。然后它如果有新的空位release出来的话,你可以把它变成再available,然后再这个引起它book从技术的角度来讲,Technical就是Dynamic Allocation就是整个这个车票动态划分,就比如说有多少个册票是从北京到上海,从多少个册票北京到石家庄,这个其实是可以做到的。你就是不停的insert 然后用 background process background process 然后做已有的 limited information 的optimization是可以做到的好但是这个的话就是可能需要一个更longer的window,其次的话,我觉得并不复杂,其实应该可以做到的。然后有人问说,有人问说这个Background process怎么做? Mike问Background process怎么做?这个Background process可以这么做就是你就做一个那个你只要做一个distributed locker timer就可以了这种的话就是distributed timer,我还真没做这个,就是你做一个保证任何一台机器,就你可有个group machine然后Grooper Machine 任何一台机器就是在任何情况下如果有group machine其中有一个fill掉的话,其他的机器也是可以起来作为master,然后在每个固定的时间段issue一个request,然后如果是单台机器的话是Kronos,然后Brand说Cadence,Cadence也是可以的,Cadence就Uber开源那个也是可以的,然后如果你只是做这个的话,Kronos是单机的嘛,就是有一些Chrono这种application单机的,但是你在一个distribute system的话可能需要coordinate一下,像Salesforce有自己的service,Uber的话,Uber的 [45:00] 如果需要这种background process的话,我一般就靠那个cadence,或者什么这种这种,稍微craft一下就可以做background process了然后应该是这样子的我觉得这个 background process 并不复杂但是你需要应对的就是如果你有 single point of failure 的话你怎么样 restart backgroundProcess 这个稍微有一点点复杂,你自己写一个应该也不复杂的。然后如何确认整个车unbookable,这个cost会比较大不会啊,当你所有的说了这个sits全部book掉,没有人cancel的时候,这个车就unbook包了然后当你release的时候,有人cancel的时候,他不就那个什么吗?就是有available的并且你可以做这个车的onbook包,你也可以做旅程的onbook包,比如说你从北京到上海直达有800个,你虽然这个车有一千个座位,但从北京到上海直达是800个座位。这个全部book完了你就把变成就是unbookbook然后从北京到石下庄你还是可以book的你可以把它就是细分到车次加旅程,就是车次加这个其实点和终点这样做,我觉得也是可以做的。然后再一点的话就是你要book的话,cross-transit可能需要insert到很多肉到所有的table里面,比如说你要从就是你要去就是转成两次的话,你可能要insert到三个table里面。这样的话,如果有一个insert field的话,其他的chip应该automatically cancelled或deleted。所以这块的话,你转成的次数应该是有限制的,比如说最大是两次或三次。我没有用过12306,我不知道它是不是这个feature,然后最后的话这个data呢,当你这个车器开走的话,你应该把它move到一个historical log table然后使当前这个database非常令,非常非常非常瘦,这样的话你可以不停的就是server-in-request好,之前的booking呢,其实就是说,我觉得这个booking呢,就是把这个resource competing,变成了一个append problem。就是把一个非常就是10k的right每秒的这个resource computing对于每个车次就是对于每个测试,14000的Request Write 每秒,就是10000的Competing变成了一个single append的problem,我只是append的一个request,append的一个request,然后用一个background process去scan这个到底谁book到了谁没有book到然后用background process做decision就是book到了以后就是你book到以后然后再去分配座位所以这个background process,比如说我现在是个background process,我这一次判定有600个人book到了,或者200个人book到了,X number of people,是吧?我就把这个通过一个直接的request,或者是一个message queue都可以,就是发到这个Sate Allocation Server说好,这600个人book到Tickets了,然后他们分别book的是什么形状的Tickets那么您给它分配座位吧,然后它就可以怎么分配座位呢?首先这个set location可以花时间更长一点,因为作为一个user来说,你更多看法的是有没有book到tickets?如果book到tickets,我具体做哪,其实关系的不是那么多了,可以比如说再花额外的20秒30秒应该是没有问题的。但是有没有book到tickets很重要,因为没有book到tickets,可能需要去refresh你的request,就会额外的增加这个write。所以当他判断这个booked tickets以后呢,他就是把这个计划呢,你600个user可以用一个进程,用single process去allocate,那你就可以用简单的用dp啊或者graded都可以是吧,你首先你要做什么呢,就是因为你是可以single process嘛,你首先去读,已经allocate好的table,比如说哪些sits已经被占了,已经allocate好的table,那如果这个sitsallocate好的话,那你就是说在EU的基础上知道哪些sitsavailable,那你根据available sets,然后你一个single process,然后做一些简单DP和grading,那实际上这个,如果做DP的话,其实就是一秒左右。几百个的话,1000个的话,其实疫苗左右就处理完了。处理完了以后你再把它写进去,然后你这个sit就allocate好了。如果这个sit tablecrush掉的话,那么你可以就是从start的一个sitting table,那你这个从start的sitting table,你就可以读一下这个,这个,你就可以读一下这个,呃,可以读一下说我哪些人已经book好了,但我还没有allocate set,然后重新allocate set这个是可以只要放tolerant就可以的因为每个就是train的number是fixed的PenHive QPS呢相对来说也是proportional to Sys的所以相对来说 [50:00] 人是可以Infinently Scaled然后其次的话,如果这个SIT真的很高,比如说你发明了一个什么SuperTrain然后一个train里面有10万个seat,然后QPS在这个train上达到1M,那你还是可以把这个train车次加上后面加一个固定的数,比如说车次第一车向作为120,车次第二车向作为120,车次第三车向作为120。你还是可以就是把车次再break下来,然后按那个来shard,嗯,就是总是能找见一个很好的合理的sharding的一个granularity然后Alex问Payment那块可以和SET Allocation Server并列吗?我觉得这块是可以并列的,就是甚至可以先做payment,然后再做Sate Allocation,就是你book好鲜味饭的payment,然后再做Sate Allocation,我觉得这个是根据Use Case的使用情况了,因为我觉得你book tickets 的时候大部分 payment 应该都是 reasonable 就是属于能 pass through 的可能reject payment不是太多你可以allocate好了以后然后payment的就是付款确认了以后然后再返回处,如果payment fill的话,你就可以把seed allocation 给revoke 掉也是可以的,然后这块SATA location,因为它是个DP问题,你想一下它每个车厢这种长方形的,然后book 的测票数量可能是任何一个数但是不可能,很大也不太可能,就是比如说1到15都是可能的,那你确实需要一个DP去optimize,也没必要。我觉得因为它这个request,其实看那个request,如果是同一个人订票的话,这个number of tickets是一下reserve 很多的。所以你在optimize的时候,你用dpa optimize的时候,你能guarantee,almost guarantee,这个tickets买到一起的人能坐在一起。这是我的一个设计想法然后大家有什么comments吗或者是有什么其他的想法觉得这个能解决这个其他不一样的想法觉得解决这个也挺不错的有什么comments我觉得这个问题啊好,近话有杨阳说,12306的话是直接返回座位结果吧,如果要background process要延迟一段时间才返回结果结果。如果是这个的话,你实际上差不多要,我们可以算一下。如果是这个background process,首先这个是configurable的,如果这个background process是10秒run一次的话。你request过去是一秒钟,写的话就是四手五路一秒钟这个background process is configurable 10秒钟 run一次然后它run好了以后然后这个batch去allocate作备再花一秒钟,差不多12秒那这个12秒其实就纯粹看这个Bagrand process都就run一次了,如果这个Bagrand process5秒钟run一次的话就可以比较7-8秒左右反汇。所以这个好像也没有很很有问题,我觉得还是skill的。我不知道已有的12306怎么做的我没有读到这个他已有的怎么做的还有一种做法还有一种做法是我想到还有一种做法是什么呢?我想想我还有一种做法是小金 我记得我们讨论的时候 我之前讨论的时候还有一种做法 你记得是怎么做吗有没有什么想法好然后陈肖问说,AuterIncrementalID是在SingleDatabaseShare上吗?如果Global怎么实践?嗯你这个Single Incremental ID 其实没有Global的问题, 因为你实际上来说, 你对于同一, 这块其实你想的是, 你有M个人对N个资源进行,CompetingM个人就是所有想从北京到上海的人对于北京到上海的测票进行competing那么他的所有的request呢实际上到具体的测试上是到同一个database的到同一个database,所以当你这么share的时候,你其实是不需要global coordination的所以这个AlterIncreaseID相当于是个QNumber,相当于是个RequestNumber景祥,我看你这块说,可以把batch变成每个request会把当前还没有处理的座位一起处理而能解释的观念 [55:00] 我们听不到你说啊啊 听到了吧现在听得到我现在不是有一个背光 process 不断的批准那些座位的问题吗?好其实可以让我觉得可以让就是比如说你现在有100个人在已经summit了这些allocation然后让这100个人第一个就是他比如说,他100个人已经在数据库里面填完了然后第一个其中的随便一个进来然后就把这100个全部处理了然后一起放给所有了啊 ok 但是这样的话呃随便说办法Event就变成了就是一个鼓一个鼓的处理,然后每一个鼓有自己的leader,有一个leader的一个Leader的一个task 去把剩下所有task一起处理谢谢大家我懂你的意思就是每个insert的时候比如说就是insert然后你看到你的ID是多少比如说你ID是一个固定的值100然后你就把之前的全部处理掉是,每一个就是把它之前全部除了?但是我觉得这个还是有问题的,因为我直观想到的问题就是因为这个request太多了,所以100,200,1000可能是同一时间出现的。那100这个是处理1到100的吗?还是1000也要处理1到1000的?还是处理901到1000?主要是每次都有一个人出来把没有处理的全部处理掉就好了。就不需要一定是神 也不一定是比他大比他小好那是你自己的其实和Background Process是一样的其实是很像,但是trigger的不一样在配光Police独立的一个技能做这个是每一个技能进来都来做这件事情然后就那个这么说这其实并不需要一个我想一下好其实这话也可以做一个Message Queue这种冲例,就是说,就是弄个Message Queue,然后Message Queue Worker的话就是,啊好但是Message or Asynchronized 可能同时有多个处理说白了这个就是这样子就是你只有一台机器顺利它处理然后只有一个进场进行处理就可以了其实两个意思差不多啊,我说这个东西可以不用那一筹外光窝口谢谢大家谢谢大家嗯,行,我觉得意思是一致的,就是其实意思还是一致的,没有本质的区别好,最快有 CWCW,我们。多个分段的车次比如说从上海到书州到武汉到济南到北京请求比较多,短阶段请求比较多,这种协力分布对于系统最大影响在哪?如果我理解对的话,就是这个其实这个从比如说从武汉到几年有多少测票,它是根据历史数据来分的,就是它一上来已经分好了。所以它是不属于工程问题,从工程上来,比如说从从engineering 角度,从武汉到几年,比如说它的车程是B1,B1就是500个票,这个票已经之前按历史数据,historical data已经分好了。所以,在这种情况下,其实对系统,我觉得影响其实其实影响就是说你把这个问题可以更细分化,你比如说你可以把车次加车层作为hash key来hash,所以说你这个比如说你从北京到上海那你从北京到上海直达的和从北京到武汉中间一半这个就是从武汉到北京这块是其实在可以分到两个完全不同的事了因为反正他们数量是固定死的他们在自己的pool里面进行competing也不直接进行competing他不会说北京到上海这个直达的book完了,我就book北京到武汉,也不会说北京到武汉book完了,就暂用北京到上海的这个直达测票,这个是就是那个之间book competing的。所以你把它细分的很细的时候,你可以share的时候,也可以share的很细。其实我觉得如果你按车次然后再加那个起始点中止点来shard的可能会shard的更细一点,可能会让这个load更balance一点那么每辆车的SAD数是在脸数的平方级别,那应该没问题啊,你这个像Uber的事儿的,scheme list 1000个, [60:00] 其实也不是太夸张4096个,UberSherz4096个,一辆车站点就10个的话,你按平方级别就100个,就是放到100个Sherz上,我不觉得是个问题啊,一个车次的话Sherz放到100个上面,不觉得是个问题。谢谢大家如果按照AltreIncrease ID的话来决定是否买到票那么很多台机器都相同的number这个不是问题啊,因为你其实只有在箱头的机器上才有competing的那个才有competing的那个什么,如果你在不同机器上的话你是不compete的,因为你不是同一个资源铺,一个resource铺的。你在不同机器上说明你要不是同一台车次,如果按这个设计的话不是同一个车次,或者是你是同一个车次,但你其实点综织点不一样,如果你稍微的更细分的话那你这样的话你本身也不直接compete,所以没有必要有Global Queue。然后其次,这块设计就是Avoid Global Queue,因为Global Queue的performance很糟糕,这块有Global Queue的话,也没有说performance很糟糕吧,但是我就没有必要有global queue然后要问放 background process 的话一直跑是不是比较 costly?毕竟不是一直 peak,相对 message queue 有什么优点吗?Message queue 是做 find out 的,就是你一下 queue 一大堆 request 然后然后那个然后不停的有这个你可以比较elastic的调节有多少个worker去处理这个request是比较方向的过去的Background process的话这种情况下你肯定要放Background process 因为你需要用一个process来做DP吗你需要去读到你需要做resource allocation 你需要一个process做DP吗谢谢大家然后可以用一些,HX说可以用一些Buffer的WriteTrick,然后像Schemeless一样,to increaseWriteAvailability。如果按这个设计的话,其实每辆车Write其实就是10k每秒。10k/s其实非常小,按一个扇的处理能力其实非常小,还是能处理的好,然后李方远这块提了一个alternative solution,方远在吗?谢谢大家方言不在啊 提了一个alternative solution我没有读懂这个alternative solution是怎么回事,我觉得是不work的solution,就比如说说是不在relational database上做critical bus,因为relational database scalability是有上限的Reallitional Database,我个人觉得是可以InfiniteSkill,因为你像Uber的话是分成4096分如果你做一个简单计算,4096,4096乘以每一台机器可以做10k的transaction per second的话QPS的话你实际上是有400个Million的QPS可以支持,基本上算InfiniteSkill,并且你这个LayerSystem可以做多层你这个sharding是可以做多层的,所以这个relational database skill 其实不是一个问题,其次的话这个skill 12306需求也不是到这个skill的,就是没有到它skill的这个波投Nike,其实一个测词上波投Nike还是没有到的然后 pre-allocated tickets把没有卖的 tickets的portion放到一个server上server从内存里server request,然后一步update回去给database。那就用一个in-memory database update了。你memory database update 性能是可以满足的,但 durability 会有问题,然后你返回的时候,你返回的时候,Deerability会有问题请不吝点赞 订阅 转发 打赏支持明镜与点点栏目你返回的时候 Durability 会有问题你如果是在写到 In-Memory 的话request 应该是没问题的,就是你这个10k的request per secondsecure request per second的同一个机器你看多久返回了对,这块其实12306这个设计就是Large Scale Ticket Booking System,其实QPS不是关键,我觉得说的很好,HX说的很好,QPS不是关键。你需要管的是多个人对于resource进行competing的时候怎么样分配这个resource这个是观点QPS其实当你shard以后,它不是一个很好很很重要的问题好Pre-allocate的话容易Under-sell,那么减少Under-sell,Allocate的时候可以做Demand Prediction但是 heuristic 最好像就是火车站的时候做的如果解决不均匀,那就各种load balancing好了Client side加L4 load balancing,L7 load balancing,这样所有的票都满了,有的收入都没了 [65:00] 这块的话我觉得这个data其实你data是在database上作为stated,你load balancer只是charging,就是request,你不管有多少个load balancer,有多少个application server最后到Datapace上你同一个车次的同一个起始点中止点的request还是最后到同一个database上的host 上有 agent 解决 local tickets 快慢玩玩是 request 或者很久没留下release 一下我还是不懂什么意思啊,好,还有Available的问题,如果有一个server挂了,分到它的ticket被站着,别人没办法买怎么办?这块这个server你要看,按我们这个设计的话,所有的application server的stiteless挂就挂了然后database是不能挂的但是Database的话应该是,首先Database挂的概率相对低一点,其次的话如果你真的很担心挂,你可以在那个就是synchronize say,replicate一份。但是default的话都是asynchronized replicate,因为这样的话request时间快但是你可以synchronize the replicative 。我想说一下这个设计里面,其实你只有当你返回,就是如果你这个用户这个state没有book的话,你是可以Lost data的你是可以Lost data的 就是你没有变成booked的State 你是可以Lost data的你什么时候不可以loss data,就是在book确认了以后你是不可以loss data的但是你没有book的话,你就可以lost data,无所谓,在request过来的时候,我没有request,我就是说没有book成功就可以了嘛。但是你book了以后是不能lost data的所以这块你replicate,就是你这个server replicate甚至可以这么做你Background process scan 的时候说我觉得这个人不考 tickets 了然后你这个ApplicationServer去几个Replica看一下它这个book有没有写入,如果book写入了再返回也可以。就是你先去确定这个book写入了,就是这个number of replication 大于1,再返回,yes也可以。如果你不担心这个就是非常small chance,就是比如说1/1/11的这个loss data的话,你其实Asynchronized Replicate也没问题所以这个high availability也不是问题,fought tolerance也不是问题谢谢大家谢谢大家如果一个server挂了,卖出去的票没有来得及及时update怎么办?人可以问问PM我觉得这是Engineer问题不是PM问题然后可以后面加一个memory-based database,比如Redis这个用Redis是可以搞定的,但不是很好,因为Redis所有的Write是到Single Thread上的所有的Redis Thread是在一个Single Thread上解决的。也就是说你RAID的时候,Redis是有多个thread处理你的RAID的问题,但是你说的Redis thread是在single thread进行的。虽然它guarantee serialized transaction,但是你这种情况下,你瞬间给Redis single thread加了很多写。虽然它这个数量其实很能够的,Redis每秒钟大概能处理简单的write是10的5次方的write,10的5次方每秒钟,Redis是没有任何问题的。虽然能处理掉,但我总感觉不是一个很好的设计。其次,你用Redis写的话,你本身就没法guarantee fault tolerance问题,没法guarantee durability问题。谢谢大家然后我读了下这个paper,我读了下这个load balancer paper,我觉得还挺有趣的,那个load balancer paper 还挺有趣的,它是是微软又嗯微软用这个commercial machine 来替代传统的load balancing的一个过程,我觉得这个paper写的还是挺有趣的,像这个传统的load balancing呢是有rote management,就是你管怎么样去charge road,然后有health monitoring,看说哪些application还活着然后你对于inbound package的话你要决定它的destination,然后你对于outbound的话你要决定你要决定这个怎么样就是怎么样把它就是做request返回是吧,就比如说我现在outbound的话,我要决定怎么返回给哪一个requester然后传统的这个load balancer呢其实他是有一个非常自己专门做load balancer的hardware做的他们这个hardware非常贵啊他这块写了8万我记得我上研究上的时候说20多万就是一个standard的那个commercial的load balancer是20多万美元的是非常非常贵的所以它后面有很多就是这个是 [70:00] 第三年左右嘛,就是研究所那会有很多那个Solution是用commercial非常便宜的机器来取代这个Expensive Load Balancer这是他们其中一个努力之一然后它这个设计也很有趣它这个设计很有趣,它设计的时候是,它是多层的,第一个Tier就是这个叫EcoCost说白了就是对于他所有的注册这个MutexMultiplex就是评分,就按这个评分,他也不考虑什么package,就评分然后这个Multiplex呢是会把你这个request分到你的service上,然后它这块是用Virtual Machine的,没有用什么Kubernetes啊,Docker啊,他会把你的request分到你的server上然后这样的话你这个request比如说是同一个transaction或者同一个connection的,那你第一个request的,进行multiplex可能是分到1的,第二个request可能分到这个multiplex,但是它也会分到1。因为它这个就是有一个header是一致的,就是同一个connection然后配到一起,然后它在里面再处理,然后它处理的时候呢,它实际上是把EU的load balancer就把它分开了,用新的一些series来,说白了是用software来解决hardware load balancer的功能,或者解决那种commercial load balancer machine 的功能然后它的outbound ticket package 是是避开这个Multiplex的我觉得这个设计是有可取之促的,但是我个人觉得Uber设计更好,因为这个其实我个人觉得SRE这块理解不是太深容但我读这个paper 我感觉这个是替代了最早的那个就是load balancer就是那种commercial的load balancer,非常贵的load balancer,一个GPG20多万然后他用这个用software来替代了那个commercial load balancer。但是像Uber这种,我觉得Uber这个设计还是挺好的,就是Ober有个叫RTAPI 所有的outbound全部通过RTAPI来进行你这个verification,然后从RTAPI里面读取你的ID信息,然后对你进行verify然后读取ID信息然后验证就是authorization authentication然后验证是你以后,然后根据你的ServiceName来lookup这个Service wrote到哪儿。然后这时候就是用JRPC或Sriff的那个Service Lookup Service然后Uber Lookup Service好像是应该是SIG Client什么意思呢?就是他每个Client其实有个自己的Lookup,就是从Client端就有个Lookup Knowledge知道去哪里找你的service如果这个他那个autodate的话Client autodate的话他会去Rulekeeper寻找最新的Uber内部专门有个Service叫Martin, 底层的实践的话, 应该就是每个Client的端是有一个Mapping的, 比如说我要靠ServiceA, 我就把我的Request全部Pot forward到ServiceA对应的是Pot12345,12345这个POT呢,然后我就lookup说,这个POT request到12345,12345又forward到哪一个IP的地址,然后哪一个POT number然后进行request forward 如果这个不work了 那我就需要去读我下一个available的node如果这些都AutoData的话,我要去Rootkeeper上拿到最新的,因为Rootkeeper是个Configuration,Global Configuration,去Rootkeeper拿到最新的这个Service IP address location,然后我就 forward or require 最新的那个Service就用那个Rootkeeper来keep最新的configuration就说了这么多,我就说这个感觉是一个load balancer的一个work,但并且这个work好像感觉已经有点过期了我读了一下这个Payment我觉得写的还是不错的,从2013年的贝利塔写的还是可以的。好,我今天就讨论这些好了,我没有什么额外的要讨论,大家有什么问题吗?或者有什么额外的想贡献呢哈雷问说所有的load balancing的client怎么保证mapping的一致性,mapping的consensus实际上是用root cable这个consensus进行实践的。你入kaper就是专门做consensus的比如说当你有一个新的node,我们说Roopkeeper,就是刚才讲过就是Roopkeeper擅长做两件事嘛,一个是configuration,也做membership management,当你有个新的node加入的时候,说这个node说我负责service嗯我负责service,就是service A,然后我这块有我的IP是多少是吧,我的那个Pol number是吧, [75:00] 那么我看一下这块有没有个例子OK就这个例子这个例子,比如说这个我现在是有个roting layer 我说要去拿那个然后我就有个routing layer,这个说白了在Uber里面这个routing layer和这个client server是同一个,internal service是同一个,这个routing layer就是multi他会说我先去Node2去读是吧我去这个Note 2去读因为这个data在Node2上然后但是如果这个Node2没有或者它dataAutoData的话他去入Kaper拿最新的data那如果这个Node有新的Node加入,会有旧的Node退出,会有Node更换的话,那Node会更换。就是inform rootkeeper 然后当他就是当他 ready 的时候会 inform rootkeeper所以这样的话你永远能从最新的Rootcabular拿来的Source or Choose然后因为这个commercial所以你的这个commercial database,所以你这个是自己internal的嘛,所以它这个fill的概率很低,怎么知道autodate很简单,因为你Uber内部不是请求的时候,你就是请求pot12345然后进行request 你是个grpc call 或striple call你如果AutoDate的话,你这个server已经没有这个docker了,你就不可能去服务你这个你这server已经没有这个docker去听着这个port number了你就不可能服了就直接就Service is not available,不就AutoData了吗?当它AutoData的时候,这Marketing应该自动trigger一个update就可以了好如果在Multi-Late组或者是Infra组工作的话,你可以share一些更多的insights,但我直观对我Multi-Late理解就是这样子的,并不用很复杂,就是一个rich plan,比较一个secret plan。好好 如果没有问题的话 今天就讲到这里吧我还想说这个Design YouTube这块还是有些复杂的嗯我就是做了一些这个CDN的research,我觉得还是挺有趣的,比如说Twitch的话就是30秒一个Cliff啊,豆瑜的CDN就是同一个file,然后它会不停的喷着新的东西啊然后YouTube的话,它是用自己的encoding啊,然后它自己用自己的encoding,我也没看过它format是什么,但是它好像大概每三秒还是五秒就是code一个claps,然后你去那个去那个update我这个可能还需要点时间再写啊然后我下周可能跟大家分享一下Design YouTube然后并且你直接打开YouTube,或者打开斗鱼,或者打开Twitch,你会发现他们实践方式是不一样的他们时间方式不一样的,直接看就发现他们时间方式不一样的。可能多个时间方式,到时候我要有空的话也把多个时间方式列一下,然后讨论一下。好谢谢大家好今天就说到这里吧谢谢大家