分布式演进严禁复用Tag与Required 规避RPC雪崩

[Chinese] Chapter 5: Encoding and Evolution — Scott Shi

Scott Shi

数据生命周期远长于代码,工业级RPC演进必须彻底废弃Required字段并将校验移至应用层,否则版本不兼容将直接引发反序列化雪崩与静默数据丢失。

AI-assisted summary of [Chinese] Chapter 5: Encoding and Evolution — original source. Check the original for full context.

Source published:

Key ideas

  • 分布式架构中“Data Outlives Code”是一条核心铁律,代码版本迭代频繁,而底层遗留数据可能存活数年甚至数十年,兼容性设计重心必须放在数据而非单纯代码上。 (5:00)
  • Thrift Compact Protocol 与 Protobuf 依靠去除显式字段名、引入 Tag 寻址及变长整数差值编码,可将报文体积压缩至 JSON/MessagePack 的一半左右。 (34:10)
  • 在 Schema 演进中随意新增 `required` 字段或复用已废弃字段 Tag 是引发线上崩溃的最常见原因,业界最佳实践是将必填校验移至应用层处理并保持字段均为 `optional`。 (36:50)
  • RPC 试图将网络调用伪装成本地函数调用存在本质缺陷,实际工程中必须显式处理网络超时、重试风暴与接口幂等性。 (74:00)

Detailed notes

## Background 该视频由 Scott Shi 于 2020 年 11 月 24 日发布在 YouTube 平台上。视频内容围绕 Martin Kleppmann 所著《Designing Data-Intensive Applications》(DDIA)的第 4 章展开,深入讲解了编码技术(Protobuf、Thrift、Avro)与模式演化(Schema Evolution)及数据流动的工程实践。

## TL;DR 本讲座深入探讨了分布式系统中数据编码与模式演化的核心挑战,直指“数据寿命远长于代码”的工业界铁律。主讲人通过新人改动一行代码引发线上崩溃等真实生产故障,拆解了语言内置序列化与文本格式的严重缺陷,对比了 Thrift、Protobuf 及 Avro 等二进制协议在元数据压缩与向后/向前兼容性上的设计权衡。随后,讲座系统剖析了共享数据库、RPC/REST 接口与异步消息队列三种数据流动范式,详细论述了网络调用的非确定性、幂等性保障、版本升级演进以及 GraphQL 针对冗余查询的优化思路。

## Atmosphere & style 整场讲座洋溢着资深工业界工程师面对面传授经验的务实与沉稳感,语气诚恳、接地气且充满警示意味。主讲人频繁穿插“血和泪的教训”、新人踩坑干崩集群以及大厂真实架构案例,将原本晦涩的序列化字节布局与兼容性理论讲得生动且极具代入感。互动环节讨论热烈,不仅有对技术底层原理的深度推演,还流露出系统设计工程师在面对分布式不可靠性时特有的严谨与悲观防御哲学。

## Bulletin - [05:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=300s) 分布式架构中“Data Outlives Code”是一条核心铁律,代码版本迭代频繁,而底层遗留数据可能存活数年甚至数十年,兼容性设计重心必须放在数据而非单纯代码上。 - [34:10](https://www.youtube.com/watch?v=9R-R8ePV22s&t=2050s) Thrift Compact Protocol 与 Protobuf 依靠去除显式字段名、引入 Tag 寻址及变长整数差值编码,可将报文体积压缩至 JSON/MessagePack 的一半左右。 - [36:50](https://www.youtube.com/watch?v=9R-R8ePV22s&t=2210s) 在 Schema 演进中随意新增 `required` 字段或复用已废弃字段 Tag 是引发线上崩溃的最常见原因,业界最佳实践是将必填校验移至应用层处理并保持字段均为 `optional`。 - [01:14:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=4440s) RPC 试图将网络调用伪装成本地函数调用存在本质缺陷,实际工程中必须显式处理网络超时、重试风暴与接口幂等性。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=0s) 工业界微服务系统普遍采用滚动递增发布(Incremental Update),这意味着生产环境中必然长期并存多个代码与协议版本。 - [05:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=300s) Schema-on-Write(写时模式)在写入时进行严格格式校验以杜绝读取端意外,而 Schema-on-Read(读时模式)将防御性判空负担全部推迟到读端业务逻辑中。 - [10:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=600s) 向后兼容(新代码读旧数据)与向前兼容(旧代码读新数据)是维持分布式系统多版本共存平稳运行的两大铁律。 - [15:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=900s) 语言内置序列化(如 Python pickle、Java 内置序列化)存在语言强绑定、安全漏洞(RCE 与解压炸弹)、缺乏版本演进及性能低下等致命缺陷,工业界严禁用于生产通信。 - [15:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=900s) 文本格式(JSON/XML/CSV)存在数字类型歧义(如 Twitter Snowflake 溢出)、二进制支持差(Base64 膨胀 33%)以及边界字符转义复杂易错等问题。 - [20:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=1200s) MessagePack 虽然实现了二进制封装,但由于仍保留完整的明文字符串字段名,在高并发场景下仍会造成大量的带宽与元数据冗余。 - [25:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=1500s) Thrift 与 Protobuf 等 RPC 框架通过 IDL 代码生成器统一各语言调用契约,底层使用数值标记(Field Tag)取代明文字段名以最大化传输效率。 - [29:45](https://www.youtube.com/watch?v=9R-R8ePV22s&t=1785s) Field Tag 是二进制通信中的唯一寻址依据,字段废弃后对应的 Tag 必须永久冻结为“坟墓”,严禁任何形式的编号复用。 - [41:40](https://www.youtube.com/watch?v=9R-R8ePV22s&t=2500s) Apache Avro 专为海量离线或近实时数据流转(如 Kafka 数据管道及数仓导入)设计,通过动态生成 Schema 或外部 Schema Registry 避免每条记录附带 Tag 或字段名。 - [52:30](https://www.youtube.com/watch?v=9R-R8ePV22s&t=3150s) 通过共享数据库在微服务间传递数据是典型的架构反模式,易因不同版本代码反序列化忽略未知字段而在写回时造成数据静默丢失。 - [01:04:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=3840s) 微服务本质是面向服务架构(SOA)的现代重塑,核心在于通过清晰的 API 契约实现跨团队独立维护与部署,以减少人为协同故障。 - [01:14:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=4440s) 网络通信与本地函数调用的本质差异在于不可预测性,因此所有的网络 API 和 RPC 接口必须设计为幂等操作以容忍重试。 - [01:17:30](https://www.youtube.com/watch?v=9R-R8ePV22s&t=4650s) 消息队列(MQ)相比同步 RPC 提供了原生异步解耦、弹性伸缩、故障容忍暂存、寻址透明度以及天然的扇出广播(Fan-out)能力。 - [01:21:45](https://www.youtube.com/watch?v=9R-R8ePV22s&t=4905s) 工业界通常将面向公网的接口设计为普适的 REST API,而在受信任的内部微服务之间采用性能更高、支持代码生成的二进制 RPC 方案。 - [01:21:45](https://www.youtube.com/watch?v=9R-R8ePV22s&t=4905s) GraphQL 通过客户端按需查询(Selective Query)解决了传统 REST API 在庞大业务实体下全量返回导致的网络带宽与性能浪费问题。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=0s) 真实生产事故的教训:为什么我们要深入理解 Encoding 与 Evolution

这一讲探讨的是 Martin Kleppmann 所著《Designing Data-Intensive Applications》(DDIA)的第 4 章:**Encoding and Evolution**(编码与演化)。虽然很多人在阅读 DDIA 时往往会跳过这一章,但它在工业界实际工程中却至关重要。主讲人分享了一个来自自己团队的真实案例:前不久在 Safety 团队,有一位刚毕业入职的新人工程师(new grad)修改了一行看似普通的代码,结果导致线上服务严重损坏、崩了很长一段时间。在分布式系统和 RPC(远程过程调用)的世界里,缺乏经验的工程师最容易在 Schema Evolution(模式演化)上摔跟头,这也几乎是所有新手必经的“极其惨痛的血泪教训”。

本章内容主要分为两大核心板块: 1. **Encoding(编码)**:亦称为 Serialization(序列化)与 Deserialization(反序列化),本质是一种数据在网络间传输的方式。在单机的内存中,数据以特定的内存数据结构存在(例如在 JVM 中,有堆 Heap、栈 Stack,对象被 Class 封装);但当面对分布式系统、无共享架构(Share-Nothing Architecture)时,数据必须跨网络从一个节点传输到另一个节点,这就强制要求必须对数据进行序列化与反序列化。常见方案包括 Python 的 `pickle`、Java 内置序列化、JSON、XML 以及二进制方案(如 Protobuf、Thrift、Avro 等)。 2. **Data Flow(数据流动形式)**:探讨数据在不同节点与系统间流转的几种范式,包括 REST API 与 SOAP 的对比、微服务(Microservices)、RPC 以及基于消息传递的系统。

在学术界或学校做项目时,环境往往很理想,通常全局只有一个代码版本(Version 1),如果写得不好或要改进就直接改。但工业界真实生产环境(Production)截然不同:在任何给定的时间段内,系统里必然存在多个版本(Version 1、Version 2 等)并行运行。现在的分布式系统普遍追求零停机时间(Zero Downtime)和极高的高可用性(High Availability)。升级系统时,绝不可能像银行那样把所有 Version 1 机器全量停机下线,然后再全量部署 Version 2。主流的做法是通过 Kubernetes 等编排工具进行滚动递增发布(Incremental Update)——比如原先有 10 台机器跑 Version 1,先下线 1 台换上 Version 2,此时线上就有 9 台 V1 和 1 台 V2 在同时承载流量。

```mermaid flowchart TD accTitle: Rolling Deployment Version Coexistence accDescr: Diagram showing how multiple versions coexist during an incremental rolling update in production. subgraph UpdateStage["滚动更新中间态(两版本并存)"] V1_Cluster["Version 1 实例群 (9台机器)"] V2_Cluster["Version 2 实例群 (1台机器)"] end Client["客户端请求"] --> V1_Cluster Client --> V2_Cluster V1_Cluster <--> |RPC 交叉交互| V2_Cluster ```

这种多版本共存的常态在微服务架构下会呈指数级放大:系统内有几十上百个微服务,每个服务都在独立演化并跑着两到三个不同的版本。当不同版本的服务相互调用时,彼此对数据结构的假设(Assumption)一旦产生分歧,就会瞬间触发严重的线上 Bug。这也是为什么微服务体系极度强调严苛的 SLA(Service Level Agreement)和接口文档契约——明确界定什么字段允许变更、什么严格禁止,发生故障时也能清晰界定到底是哪个服务的责任。

---

[05:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=300s) “Data Outlives Code” 与 Schema 读写哲学的碰撞

刚入行的初级工程师往往把全副身心都扑在代码(Code)上,但经验丰富的老工程师则更加看重数据(Data)。在分布式与存储系统的漫长生命周期中,**“Data Outlives Code”**(数据比代码活得长)是一条不可动摇的金科玉律: - 生产环境里的代码,任何时刻可能最多就新旧两到三个版本在跑; - 然而底层存储的数据,可能包含 5 年前、甚至 10 年前老代码写入的遗留数据,以及刚才刚刚写入的新数据。如果你没有投入巨大的精力专门去做全局数据迁移(Data Migration)和统一清洗,这些五花八门历史格式的数据就会永久驻留在存储介质中。无论你的业务算法用 A 还是 B,影响的无非是计算性能;但如果现在的代码无法兼容解析 5 年前的数据,那就是灾难性的数据破坏事故。

这也引申出关系型数据库与 NoSQL 数据库在演化模式上的核心差异: - **Relational Database(关系型数据库)—— Schema-on-Write(写时模式)**:数据在写入时就会严格按照预定义的 Table Schema 进行校验,如果不满足格式,写入直接被强行拒绝(Reject)。这样虽然写的时候比较严格,但读的时候非常轻松愉快,不需要工程师在代码里写一大堆防御性的边界兼容逻辑。 - **NoSQL(如 MongoDB)—— Schema-on-Read(读时模式)**:写的时候极其顺畅,只要文档大小不超过限制(例如 16MB),任何奇形怪状的 JSON 都可以直接落盘。然而,这种设计的代价是将复杂性全部转移给了读端(Read)。读取数据时,业务代码必须具备高度的防御性逻辑去判断各种字段是否存在。

> “在所有的 System Design 里面,Surprise 都不是一个 Good Thing,Surprise 都是 Bad Thing…… 成功的一个 Engineer,设计的目标是减少 Surprise,是非常悲观的。Engineer 设计应该非常悲观,尽量考虑一些 Worst Case Scenario。”

使用 MongoDB 写入时虽然很开心,读取时却会遭遇大量“Big Surprise”;而系统设计工程师的终极目标正是彻底消除意外。

```mermaid flowchart LR accTitle: Schema on Write vs Schema on Read accDescr: Comparison between Schema-on-Write rejected early vs Schema-on-Read failing late with surprise. subgraph Relational["Schema-on-Write (RDBMS)"] W1["写入数据"] --> C1{"验证 Schema"} C1 -->|不合规| R1["立即 Reject"] C1 -->|合规| D1[("数据库")] D1 --> Read1["读取:平稳无惊吓 (No Surprise)"] end subgraph Mongo["Schema-on-Read (NoSQL)"] W2["写入数据"] --> D2[("直接入库 (Smooth)")] D2 --> Read2["读取:需大量条件分支判空"] Read2 -.-> S2["线上异常 (Big Surprise!)"] end ```

在互联网业务中,除了服务端微服务多版本之间的复杂拓扑,客户端(Client Side)的版本生态更是一场噩梦: - iOS 生态由于系统更新相对集中,客户端版本离散度还算可控,必要时还能采用“强制更新”这一终极手段(但这属于影响用户体验的“大杀器”,非极端情况不敢轻易使用); - Android 生态则高度碎片化(Fragmented),市面上并存着无数的操作系统版本与应用版本。

---

[10:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=600s) 兼容性两大铁律与跨节点数据传输的底层挑战

当几十个不同版本的 iOS/Android 客户端向后端发起请求,后端数以百计的微服务自身又并存着多个不同版本,互相调用,简单排列组合就会产生成百上千种运行时交互形态。用户在客户端遇到 Bug(例如经常有人抱怨 UberEats 出现故障),排查起来极其困难,因为不同用户命中的是完全不同的客户端版本与服务端微服务版本组合链路。

为了在如此混乱的环境下维持系统的稳定运行,必须严格遵守两个核心兼容性定义: - **Backward Compatibility(向后兼容)**:新代码能够正确读取由旧代码写出的旧数据(New code can read old data)。 - **Forward Compatibility(向前兼容)**:旧代码能够正确读取由新代码写出的新数据(Old code can read new data)。这要求工程师在最初写旧代码时就必须具有前瞻性,能够优雅地忽略或容忍未知的新字段,其实现难度往往远高于向后兼容。

在单机内存中,数据的组织是围绕语言特定的内存结构优化的。例如在 JVM 内部,数据保存在堆(Heap)和栈(Stack)中,代码位于专有的 Code Section,数据结构以结构体、数组、哈希表、树、链表等形态存在,内部充斥着指针操作(Pointer Operation)。借用内存访问速度的比喻:访问内存取数据就好比散步去 200 公里外取东西;而一旦访问磁盘,慢得就像乘坐旅行者号航天器飞到了土星,需要漫长的时间。

但在分布式无共享架构(Share-Nothing Architecture)中,内存中的数据结构无法直接跨机器传输。节点必须通过网络将数据发送给另一台机器,因此必须将内存中的指针和数据结构转换(Encode/Serialize/Marshal)成可在网络中传输的连续文本(Text Representation)或二进制字节流(Binary Representation)。

```mermaid flowchart LR accTitle: In-Memory to Wire Encoding accDescr: Process of transforming in-memory pointer based data structures into network transportable formats. subgraph NodeA["Node A (发送端)"] Mem["内存数据结构<br>(Heap/Pointers/Objects)"] -->|Encode / Serialize| Wire["网络传输格式<br>(JSON / Binary Stream)"] end Wire -->|TCP / Network| NodeB subgraph NodeB["Node B (接收端)"] NodeB_Wire["网络传输格式"] -->|Decode / Deserialize| NodeB_Mem["重建内存对象<br>(Language Objects)"] end ```

不同技术栈对这一转换过程有不同的称谓: - Java/通用领域:**Serialization & Deserialization**(序列化与反序列化) - DDIA 书中:**Encoding & Decoding**(编码与解码) - Go / Ruby 领域:**Marshalling & Unmarshalling**

许多编程语言都自带开箱即用的序列化类库,例如 Java 的 `java.io.Serializable`、Python 的 `pickle`、Ruby 的 `Marshal`。初学者在学校写小作业时觉得只要 `pickle.dump()` 和 `pickle.load()` 一行搞定非常方便,但在严肃的工业级生产环境中,**几乎严禁使用这些语言内置的序列化库**。

---

[15:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=900s) 语言内置序列化的大坑与文本格式(JSON/XML/CSV)的缺陷

生产环境中不能直接使用语言内置序列化库(如 Python `pickle`、Java 内置序列化)的致命原因包括: 1. **语言强绑定(Language-specific lock-in)**:用 Python `pickle` 导出的数据,下游的 Java 服务几乎无法直接读取和反序列化,彻底锁死了技术选型。 2. **严重的安全性风险(Security Vulnerabilities)**:反序列化过程本质上是在接收端根据字节流恢复并执行一段不可控的代码,攻击者可以轻易构造恶意载荷注入远程代码执行(RCE)。这与在线算法评测系统(LeetCode)必须防范未知代码沙箱逃逸同理。另一种常见攻击是**解压炸弹(Decompress Bomb / Zip Bomb)**:攻击者发送一个极小的(例如 1KB)压缩数据包,一旦在服务器内存中被反序列化展开,瞬间膨胀成 2TB 的海量数据,直接撑爆服务器的内存和磁盘,导致整个集群崩溃。 3. **缺乏版本控制机制(No Schema Evolution)**:完全无法应对多版本字段迭代与兼容性协调。如果必须升级服务,只能效仿旧时代的传统大银行,把系统完全停机(Go offline)好几个小时来更新,这在现代互联网服务中是完全不可接受的。 4. **性能极其低下(Low Efficiency)**:编解码开销巨大,输出的体积过于冗余膨胀。

主讲人在此顺带提到了 Java 语言中另一个类似的隐藏陷阱——`hashCode()`:理论上哈希值应当纯粹由输入的字符串内容决定,但 Java 对象的默认 `hashCode` 在不同的线程、CPU 或特定实现环境下生成的哈希值可能完全无法复现(Not reproducible),因此绝不能在分布式或需要数据持久化的场景下依赖它做一致性计算。

为了解决多语言通信,工业界广泛采用了基于文本的标准格式:**JSON、XML 和 CSV**。 - **JSON 的流行背景**:近年来随着前端 JavaScript 的火爆而成为事实标准,同时 MongoDB 等文档数据库也原生支持 JSON 存储。 - **文本格式的核心缺陷与工业踩坑**: - **数字与类型歧义**:在 XML 和 CSV 中,纯文本无法直接区分 `"15"` 究竟是一个数字还是一个字符串;JSON 虽然区分了字符串和数值,但它无法精准区分大整数和双精度浮点数。主讲人特别提到了 **Twitter 著名的 Snowflake ID 踩坑事件**:Twitter 的推文 Snowflake ID 是一个极大的 64 位整型数字,在通过 JSON 传输给前端 JavaScript 时,因为 JS 将数字统一视为 IEEE 754 双精度浮点数,直接引发了精度截断丢失和歧义(区分不出数字 A 与数字 B),最终迫使 Twitter 必须把 ID 强转为 String 格式进行传输。 - **二进制数据支持极其糟糕**:JSON/XML 无法原生承载 Binary Stream。若要传输二进制内容,必须先用 Base64 进行编码。Base64 编码会导致数据体积直接膨胀约 33%,白白损耗了大量的带宽和网络传输效率。 - **Schema 规范沦为摆设**:虽然 XML 拥有 XML Schema,JSON 也推出了 JSON Schema,它们甚至支持强大的正则表达式校验,但由于设计得过于复杂晦涩,在实际工业界几乎没有开发者真正落地使用它们。 - **CSV 复杂的边界情况(Corner Cases)**:CSV 默认使用逗号(`,`)分隔,如果业务字段值本身包含逗号、换行符或引号,就必须做转义(Skip / Escape)。主讲人曾经负责过一个解析大量自然对话文本的工程项目,解析结果导出为 CSV,结果遭遇了无穷无尽的边界问题,稍微有一处转义错误,就会导致整行数据严重错位(Mismatched / Misaligned rows),毁掉整个数据集。

---

[20:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=1200s) B2B 数据交换现实与二进制编码启蒙:MessagePack 案例深度拆解

尽管 CSV 存在诸多缺陷,但在 B2B(企业对企业)的数据交互场景中,它依然是使用极其广泛的主力格式。虽然理想状态下企业间应该通过开放 API 相互拉取数据(比如 Uber 给 Lyft 提供带完整 Auth 鉴权的 API),但很多企业缺乏足够的工程资源来维护复杂的 API 架构。

主讲人以 Uber 内部真实的保险业务(Insurance)为例:Uber 与 Farmers、State Farm、Progressive 等传统第三方商业保险公司打交道时,并无高大上的实时 API。工程团队为此专门写了一个 File Server,每个月定期以 CSV 格式下载这些保险公司的月度原始数据,然后将文件丢到 File Server 上。随后,Uber 的精算师(Actuary)与数据分析师(Data Analyst)会把这些巨大的 CSV 导入到类似 Google Sheets / Excel 中,花费整整一周的时间进行数据清洗、运算校验和月度财报汇总。

为了解决文本格式体积过大的问题,第二类解决方案是**二进制编码(Binary Encoding)**。主讲人以 **MessagePack**(一种通用的 JSON 二进制包装方案)为例进行了具体的数据结构拆解。

假设需要编码如下 JSON 数据对象: ```json { "userName": "Martin", "favoriteNumber": 1337, "interests": ["daydreaming", "hacking"] } ```

使用 MessagePack 编码后,文本 JSON 被压缩成了紧凑的 66 字节十六进制二进制数据流。其编码细节十分典型: - **容器与元数据声明**:首字节 `0x83`(二进制以 8 开头表示它是一个包含 3 个 Entry 的 Object 映射)。 - **字段键值对布局**: - 第 1 个 Entry:Key 是字符串且长度为 8(`userName`),Value 是字符串且长度为 6(`Martin`),其字符底层依然使用标准的 ASCII 码表示。 - 第 2 个 Entry:Key 是长度为 14 的字符串(`favoriteNumber`),Value 被编码为一个 16 位的无符号整型(Unsigned int 16),存储具体数值 `1337`。 - 第 3 个 Entry:Key 是长度为 9 的字符串(`interests`),Value 则是一个 Array;标识字节声明它是一个包含 2 个 Entry 的数组,紧接着依次存放两个 String 项:第一个是长度为 11(十六进制 `0x0B`)的 `"daydreaming"`,第二个是长度为 7 的 `"hacking"`。

虽然 MessagePack 成功将数据体积压缩到了 66 字节,但主讲人敏锐地指出了它与后续更先进的 Protobuf/Thrift 的根本区别与劣势所在:**MessagePack 依然完整地编码了所有的字段名称(Field Name)**,如 `userName`、`favoriteNumber` 和 `interests`。在典型的 RPC 通信中,如果 Client 端与 Server 端运行的是同一套预先约定好的代码,双方完全可以在协议层就协商一致第 1 列是 `userName`、第 2 列是 `favoriteNumber`,根本没有必要在每一次网络传输中都反复、冗余地把这些长字符串字段名重新编码并发送一遍。

[25:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=1500s) Thrift 的诞生与 RPC 架构原理解析

在面对高并发、请求极其频繁的系统时,文本格式或携带冗余元数据的格式会造成巨大的网络开销。如果一个接口每秒承受上百万次请求(million requests per second),每一秒钟所有的字段名——例如 `username`、`favorite_number`、`interests`——都会在网络上被机械地重复传输一百万次。这完全是没有必要的浪费。当客户端和服务端运行同一套代码时,真正需要传输的只有具体的数据值,像列名(Field Name)和字段类型(Field Type)这类 Schema 信息根本没必要塞在数据包里到处跑。为了解决这个问题,Uber 等大厂在实践中广泛采用了 Thrift。Thrift 提供了两种核心编码方式,分别是 Binary Protocol(二进制协议)与 Compact Protocol(紧凑协议)。

对于没有接触过 Thrift 或 Protocol Buffers(Protobuf)的人,Scott 特意花时间解释了这类 RPC 框架的底层工作机制。RPC(Remote Procedure Call)的核心目标是像调用本地函数一样去调用远端函数,把参数传过去并把返回值带回来。开发人员需要先在一种 IDL(接口定义语言,Scott 调侃自己一度把 IDL 记成了 "Data Description Language" 或者 "Interactive Data Language")文件中定义数据的 Schema。有了这个 `.thrift` 或 `.proto` 文件后,编译工具会根据目标语言自动生成客户端和服务端代码。无论你用的是 Python、Java 还是 Go,代码生成器都能编译出对应的结构体与调用桩。

```mermaid flowchart LR accTitle: RPC IDL Code Generation and Wire Protocol accDescr: Shows how Thrift or Protobuf IDL files are compiled into different languages and talk across the wire using binary protocols. subgraph Def["Schema Definition"] IDL["IDL File\n.thrift /.proto"] end subgraph ClientSide["Client (e.g. Go)"] CG["Generated Go Code"] CD["Go Struct Data"] end subgraph Wire["Network Transfer"] BP["Binary / Compact Protocol\n(Packed Bytes)"] end subgraph ServerSide["Server (e.g. Java)"] SG["Generated Java Code"] SD["Java Object / Data"] end

IDL --> CG IDL --> SG CD --> CG CG --> BP BP --> SG SG --> SD ```

这种机制保证了客户端和服务端能够 "speak the same language"(使用相同的语言交流)。即便客户端是用 Go 写的,服务端是用 Java 写的,Go 端生成的 Thrift 代码会把内存中的结构体塞进 Binary Protocol 或 Compact Protocol 中压缩成一串纯粹的二进制字节流发往远端;Java 服务端接收到这串字节流后再解码成 Java 的数据结构。在 Thrift 的二进制编码下,编码器直接去除了冗余的字段名字,取而代之的是用紧凑的数值标记(Field Tag)与类型(Field Type)来定位字段,例如用 001 标记第一个字段,后面附带长度与真实数据。

---

[29:45](https://www.youtube.com/watch?v=9R-R8ePV22s&t=1785s) Thrift Binary Protocol 的结构瓶颈与 Tag 坟墓法则

在 Thrift Binary Protocol 中,一个包含用户名、幸运数字和兴趣列表的 Person 结构体被压缩到了 59 个字节,相比 MessagePack 的 66 字节略有缩减。它省掉的绝大多数空间来自于抹去了明文的字段名字(Field Name)。然而,仔细观察这种二进制结构就会发现它的效率依然极其低下:它所记录的元数据——特别是长度信息(Length)——占用了夸张的字节数。例如,为了表达一个长度仅为 6 的字符串,Binary Protocol 竟然用了多达 8 个乃至 16 个字节的十六进制空间去存长度,"为了表示 Lens 是 6,它整个这个数量都比这个 6 大了",造成了严重的元数据浪费。

在这种编码体系下,真实的数据内容本身(例如具体的字符串 `"Martin"`、`"daydreaming"`、`"hiking"`)是绝对不可能被凭空省掉的;能优化的只有包裹它们的元数据(Metadata)。与此同时,既然网络数据包中已经完全抹掉了字段名,客户端与服务端之所以能正确理解彼此,完全仰仗于每个字段头部的 Field Tag。如果 Tag 是 1,双方都通过本地编译好的代码查表,认定 1 代表 `username`。

一旦理解了这套依赖 Tag 的寻址逻辑,就会明白为什么在 Thrift 和 Protobuf 的世界里绝对不能复用字段编号。如果把原本编号为 1 的 `username` 废弃掉,转而把 1 改成 `credit_card`,或者把编号为 2 的 `favorite_number` 替换成同样为整数类型的 `phone_number`,系统将遭遇灾难性后果。因为在庞大的分布式系统中,你根本无法预估外部某个客户端使用的是几年前未升级的 SDK 版本;老版本客户端误以为 2 是幸运数字,服务端却以为收到了电话号码,整个数据流就会彻底错乱。

- 字段一旦废弃(Deprecated),其对应的 Field Tag 必须原地封死,"变成一个 tomb 了,就变成一个坟墓了,没人敢用这个 2 了"。 - Field Tag 的数值分配必须保持单调递增(growing),永远不要进行 Tag 的复用(Never reuse tag)。

---

[34:10](https://www.youtube.com/watch?v=9R-R8ePV22s&t=2050s) Thrift Compact Protocol 与 Protobuf 的极限压缩

为了解决 Binary Protocol 中元数据极其低效的缺陷,Thrift 演进出了 Compact Protocol(紧凑协议),而 Google 的 Protobuf(常配合 gRPC 使用)也采取了高度类似的极致压缩思路。在这类紧凑格式中,序列化体积从 Binary Protocol 的 59 字节骤降到了 34 字节,体积缩减了超过 40%,几乎是最初 JSON/MessagePack 的一半大小。

Compact Protocol 之所以能达成这种压缩比,其核心刀刃全部砍在了数据类型(Data Type)与长度(Length)的编码优化上: 1. **Delta 差值编码与字段合并**:在 IDL 中定义的字段通常是连续递增的(字段 1、字段 2、字段 3)。Compact Protocol 在存储 Tag 时,直接存与上一个字段的差值(Offset),如果两个字段连续,差值仅为 1;同时,它将字段的 Tag 差值与类型编号巧妙地压缩在同一个字节的高低位里(例如用高位存 Tag 差值,低位存类型),用极小代价一次性描述两个关键信息。 2. **变长整数与极简长度**:原本 Binary Protocol 中占用数个字节的冗余长度字段被直接压缩,长度较短的字符串只用最少量的字节表达长度数值,只有在超长情况下才会扩展额外字节。

| 编码协议 | 序列化体积 (示例数据) | 是否保留字段名 (Field Name) | 元数据 (Metadata) 效率 | |:--- |:--- |:--- |:--- | | **MessagePack** | 66 bytes | 是 (保留完整明文字符串) | 较低,键名占用大量重复空间 | | **Thrift Binary Protocol** | 59 bytes | 否 (转换为数字 Tag) | 很低,Length 和 Type 占用了过多冗余字节 | | **Thrift Compact Protocol / Protobuf** | 34 bytes | 否 (使用差值 Tag + 紧凑类型) | 极高,高低位复用字节并压缩变长长度 |

这种压缩思路确立了二进制序列化的演进方向:真实业务数据无法压缩,但包裹数据的元数据框架可以无限压榨。这也使得 Protobuf 和 Thrift Compact Protocol 成为了现代高吞吐、低延迟 RPC 通信的事实标准。

---

[36:50](https://www.youtube.com/watch?v=9R-R8ePV22s&t=2210s) 兼容性血泪史:为什么新人加一行 Required 就能干崩系统

在 Schema 演进(Schema Evolution)与向后/向前兼容性(Backward and Forward Compatibility)的设计上,Scott 直言这是一部由 "血和泪的教训" 堆出来的规范指南。在工业界团队中,新人(New Guy)入职后最容易犯的低级错误,往往就是改了一行看似无伤大雅的 Schema 定义,直接导致下游整个生产系统雪崩故障(breakdown),"我自己至少见过两三次了,就是这个一行 code 把系统 breakdown 了"。

各种变更操作的危险系数截然不同: - **修改字段名称(Change Field Name)**:这是绝对安全(OK)的。因为底层在网络传输时完全依靠 Tag 寻址,字段名在序列化后根本不存在,改名对二进制传输没有任何影响。 - **复用废弃字段 Tag(Reuse Field Tag)**:绝对禁止(Not OK),如前所述,老版本客户端会让数据完全混乱。 - **修改字段类型(Change Field Type)**:大部分情况下绝对不行(Not OK)。把 `array` 改成 `string`,或者把 `int` 改成 `string`,会直接导致反序列化失败,除非两种底层类型之间有着极特殊的二进制兼容规范,否则严禁随意变更。 - **新增 Required 字段**:这是摧毁分布式系统最常见的元凶。序列化与反序列化框架在处理声明为 `required` 的字段时,会在底层强制校验字段是否存在,如果解包时找不到对应数据,会直接抛出反序列化异常(Exception: Invalid Request)。如果服务端更新后将某个新增字段设为必填,而使用旧版客户端的用户发起请求时根本不知道这个字段的存在,服务端就会将线上所有的旧客户端请求统统判定为非法并抛出异常,顷刻间干翻服务。

反向操作同样充满陷阱:如果服务端移除了一个客户端原本标记为 `required` 的字段,当服务端返回结果缺失该字段时,客户端本地同样会抛异常拒绝解析。正因如此,前向与后向兼容性组合枚举起来非常繁复。业界目前最成熟的实践是:**原则上永远不要给字段加 `required`**。要么仅在系统的 `v0` 或 `v1` 初始阶段定义极少数永远不可能变更的必填字段,此后所有演进版本全部将其视作 `optional`。若业务层面确实需要校验字段完整性,应当把这一逻辑挪到应用层代码(Application Level Logic)去处理,缺少字段时打印一条系统报警日志并优雅返回兜底响应,而不是让底层的 RPC 框架直接抛异常中断服务。

---

[41:40](https://www.youtube.com/watch?v=9R-R8ePV22s&t=2500s) Apache Avro:面向海量数据传输与数仓导入的演进设计

在对比完 Thrift 与 Protobuf 之后,Scott 转向了第三种截然不同的二进制格式:Apache Avro。与前两者的设计哲学不同,Thrift 和 Protobuf 是纯粹为了高频在线事务请求(Transactional Requests / High QPS)而生的,服务于面向用户的实时流量;而 Avro 则是为了海量离线或近实时的数据流转(Data Transfer)量身打造的。

在 Uber 的数据架构中,Avro 被重度应用于将数据从在线事务型数据库(Transactional Database)搬运同步到大数据分析平台 Hadoop(HDFS)的链路上。Uber 内部开发了一个基于 Kafka 的核心管道服务叫 Watchtower(瞭望塔),系统内的在线事务产生的变更会被转化成基于事件驱动(Event-Driven)的 Kafka 消息。这套 Kafka 数据管道的消息 Schema 就是用 Avro 定义的,数据也是通过 Avro 进行序列化传输的。

Avro 最大的特色在于它处理元数据的方式与 RPC 完全不同。在数据库批量导出或向数仓倾倒数据时,数据记录可能成千上万,但每行记录如果都打上 Tag 反而成了累赘;更重要的是,数据库的字段名(Field Name)在数据导入时具有至关重要的映射意义,而 Field Tag 此时毫无价值。Avro 支持根据数据库的表结构动态生成 Schema(Dynamic Generate Schema),在转储数据文件(Data Dump)时,Avro 会将 Schema 写入数据文件的头部或通过外部 Schema Registry 统一管理,正文中的大量数据记录本身就可以彻底裸奔,既不需要附带字段名也不需要附带 Tag,从而完美适配了数据库与大数据分析系统之间动态、批量的数据流转场景。

[45:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=2700s) 二进制编码优势、类型系统与 Facebook 改造 PHP 的启示

二进制编码(如 Protobuf 和 Thrift 的 Compact 编码)相较于 CSV、JSON 以及特定语言内置的编码方式,具有压倒性的优势。首先是极其紧凑的体积(Compact),能使数据利用率接近翻倍;特别是它彻底省略了字段名(field name)的传输。举例来说,若系统承受每秒一百万次请求(1 million RPS),使用 JSON 传输意味着每秒都要冗余传输一百万次字段名字符串,而 Thrift 或 Protobuf 仅凭字段标识符(tag)便能将这每秒上百万次字段名的开销省去。其次,Schema 本身具备极高的工程价值。Protobuf 和 Thrift 的 IDL 定义文件是作为代码的一部分 Check-in 到 Git 或 SVN 等版本控制系统中的,团队可以借助 `git log` 清晰追踪 Schema 在两年前、几个月前乃至当下的演进历史。

更为关键的是,这种基于代码的 Schema 提供了一份全局的“数据库”,便于在部署前进行前向与后向兼容性检查。例如当服务端计划将接口从 V1 升级到 V2 时,工程师能够对照历史上所有客户端版本,推演 V2 是否包含破坏性改动(Breaking changes),避免部署后旧版客户端无法请求新版服务端。最后,在 Go、Java 等静态类型(Statically Typed)语言中,基于 Protobuf 生成的代码能在编译期进行严格的类型检查(Type Checking)与类型推导,把低级错误掐死在上线之前。业界大厂极度重视强类型,最典型的例子莫过于 Facebook:由于早期原生 PHP 无类型检查,线上屡屡出现微小却致命的低级错误导致宕机(Outage)。为此,Facebook 耗费巨资打造了 HHVM,推行带类型的 Hack(Typed PHP),并编写了将 Hack 代码转化为 C++ 代码的高性能编译器,直接在生产环境中跑转换后的 C++ 代码,彻底消除了无类型代码带来的无谓麻烦。

- **体积与传输效率**:Protobuf 与 Thrift Compact 编码省去字段名开销,高吞吐场景下每秒省去百万级冗余元数据传输。 - **版本可追溯性**:IDL 作为代码纳入 Git 纳管,每一次 Schema 变更都具备清晰的变更日志。 - **静态类型保障**:强类型语言结合 IDL 生成代码,在编译期完成类型校验;Facebook 曾专门打造 HHVM 将 Typed PHP 编译为 C++ 运行以杜绝低级停机故障。

---

[50:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=3000s) 互联网与自动驾驶的场景差异:拥抱 Shared-Nothing 架构

现代互联网系统设计的核心基石是无共享架构(Shared-Nothing Architecture)。这与自动驾驶(Autonomous Driving)等嵌入式边缘场景有着本质区别。以车载 128 线激光雷达(LiDAR)为例,其每秒产生的数据高达几百兆甚至 180 MB/s,根本不可能全量实时传回互联网云端处理。更致命的是网络的非确定性——正如分布式系统理论中所揭示的网络阴暗面(The Trouble with Distributed Systems),网络延迟极其不可靠,原本 5 毫秒即可返回的请求,可能因并发拥堵突然暴增至一两分钟毫无响应。在互联网场景下这只是页面加载变慢,但在自动驾驶场景下,几秒钟的网络假死就意味着严重的机毁人亡事故。

结合这一点,Scott 分享了针对求职者的职业建议:找工作时应优先选择互联网/服务端背景的公司,而非传统单机软件公司(如早期的 Adobe 或单机版 Microsoft Word)。传统本地客户端软件迭代缓慢,技术成长空间有限;互联网公司遵循敏捷迭代,全面采用 Shared-Nothing 架构,不仅薪酬待遇更具竞争力,技术体系也能让工程师积累高并发与分布式演进的核心能力。在无共享架构下,服务间的数据流动(Data Flow)通常可归纳为三种典型途径:通过数据库共享、通过 API(REST/RPC)调用,以及通过异步消息队列传递。

```mermaid flowchart TD accTitle: 三种典型数据流动途径 accDescr: 架构中数据在不同组件间流动的三种主要方式

subgraph Approaches["Shared-Nothing 架构下的数据流动途径"] direction LR A["1. 共享数据库<br/>(Database)"] B["2. 远程调用<br/>(REST API / RPC)"] C["3. 异步消息<br/>(Message Passing)"] end

A -.->|高风险/反模式| E["多服务直连共享 DB<br/>易致数据污染与丢失"] B -->|最普遍/解耦| F["契约隔离<br/>独立演进部署"] C -->|解耦/削峰| G["事件驱动<br/>异步解耦缓冲"] ```

---

[52:30](https://www.youtube.com/watch?v=9R-R8ePV22s&t=3150s) 模式一的反思:通过共享数据库流转数据的隐患

通过共享数据库来流转数据被普遍视作一种架构反模式(Anti-pattern),在实际生产中极易产生问题。当两个独立的微服务(如 Service 1 与 Service 2)同时访问并修改底层的同一个数据库甚至同一张数据表时,两者所承载的业务逻辑假定与 ORM(对象关系映射)框架往往并不一致。例如,Service 1 在向支付表写入数据时,为了规范展示,通过自身业务逻辑将信用卡号每四位切分并添加连字符(Dash);而 Service 2 在读取或写入时并没有这种假设与处理,读出的数据格式便完全混乱损坏(Corrupted)。

这种架构在面临版本迭代时会彻底失控。首先,数据库中可能存在不同版本的代码写入的数据;其次,多个服务以不同的业务理解操作同一份存储;再者,写入数据的服务与读取数据的服务可能处于不同的生命周期版本。多种变数叠加在一起,使得共享数据库模式变得极其脆弱且难以维护。

---

[55:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=3300s) 代码版本不一致导致的数据静默丢失与微服务兼容性噩梦

通过数据库交互时,即便只是单一服务的滚动升级,也极易发生隐蔽的数据丢失问题。以 LinkedIn 的用户资料系统为例,新版本代码新增了头像字段,包含 `username`、`favorite_number`、`interests` 以及 `photo_url` 四个字段。当客户端使用新版本代码将包含这四个字段的数据写入时,如果读取端运行的是尚未更新的旧版本代码,其 ORM 在反序列化解码时根本不认识 `photo_url`,往往会直接将其丢弃(Discard);一旦该旧版服务稍后执行更新并把对象重新写回数据库,原本存储的 `photo_url` 就会被彻底冲掉,造成不可逆的数据静默丢失(Data Loss)。

```mermaid flowchart LR accTitle: 旧版本反序列化引发数据丢失流程 accDescr: 展示新数据写入后被旧版服务反序列化并写回导致的字段丢失

subgraph Write1["新版代码写入"] A["客户端 (新版)<br/>写入 4 字段:<br/>username, number,<br/>interests, photo_url"] end

subgraph Storage["数据层"] DB["数据库"] end

subgraph ReadOverwrite["旧版服务处理"] B["服务端 (旧版 ORM)<br/>解码时忽略未知字段<br/>丢弃 photo_url"] C["服务端 (旧版)<br/>更新写回数据库<br/>photo_url 永久丢失!"] end

A -->|"1. 写入完整数据"| DB DB -->|"2. 旧版读取"| B B -->|"3. 重新编码写回"| C C -->|"4. 覆盖写入"| DB ```

要避免此类问题,必须通过极其严谨的代码防御手段,但这也揭示了无共享微服务架构下最具挑战性的难题——版本兼容性。如果一个核心服务需要与下游 20 个微服务进行交互,发布新版本时就必须逐一推演是否与这 20 个服务的旧版本保持向后兼容。在大型分布式系统中,由于各业务线发布节奏不一,下游服务可能依然运行着数月前的极旧 Client 版本。这就需要跨团队协调升级节奏,甚至必须强制要求对方先将版本提升(Bump)到某个安全基线后,己方服务才能正式切流。

业界因版本升级兼容性引发的惨痛故事不胜枚举。Scott 谈到自己曾在换工作空档期参加 Kafka 技术峰会,会上各家公司分享了海量关于版本不兼容导致发布事故的“悲惨故事(Miserable Stories)”。例如 Kafka 社区推进移除 ZooKeeper 依赖(KIP-500 / KRaft)的架构演变:为了稳妥升级,官方设计了复杂的平滑过渡版本。用户必须先升级到同时兼容 ZooKeeper 和 KRaft 的中间过渡版本(Interim Version),在过渡版本中通过修改配置关闭对 ZooKeeper 的强依赖;待集群稳定运行无误后,才能最终 Bump 到彻底剔除 ZooKeeper 支持的全新版本。整个演进历程展现了工业级系统管理版本升级时所面对的极端复杂性。

---

[01:00:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=3600s) “Data Outlives Code”:数据生命周期与 NoSQL 快照机制

在系统设计中存在一条核心铁律:"Data Outlives Code"(数据寿命远长于代码)。服务端的业务代码通常一至两个月迭代一次,旧版本代码在生产环境中顶多存活一两年;然而数据库里的数据一旦上线,往往需要留存五年、十年甚至永久。因此,在架构评审与系统设计面试中,“永远是 Data 大于一切,Data 永远比 Code 重要”。面试中若一时遗忘某个特定算法细节(如 Geohash 的具体编码,或 MD5 与 SHA-1 的底层哈希差异)往往无伤大雅,但如果对数据如何流转、数据模型如何兼容、生命周期如何管理缺乏全局清晰的把控,则是严重的架构缺陷。

```mermaid flowchart TD accTitle: 代码生命周期与数据生命周期的差异 accDescr: 对比代码版本快速更迭与数据长期存留的生命周期差异

subgraph CodeLifespan["Code 变更频繁 (周期短)"] C1["当前在线版本: 1~2 个月"] C2["归档旧版本: 最多 1~2 年"] end

subgraph DataLifespan["Data 长期留存 (周期长)"] D1["历史数据: 5~10 年前的数据持续存在"] D2["要求持续兼容历史 Schema 与多态结构"] end

CodeLifespan -.->|"代码快速更迭"| System["业务系统运转"] DataLifespan ==>|"决定系统核心契约"| System ```

这种数据持久性差异在不同数据库类型中表现各异: - **Schema-on-write(关系型数据库,如 MySQL)**:数据在写入时强制遵循预定义 Schema,整张数据表的结构全局一致。 - **Schema-on-read(文档型数据库,如 MongoDB)**:同一个集合(Collection)内天然并存着过去数年间各个时期写入、具有不同字段结构的历史文档。

面对 NoSQL 多版本数据并存的特性,当系统为了容灾备份、防止“删库跑路”而导出数据快照(Snapshot Dump)时,导出的 Dump 数据往往会依据当前的最新 Schema 重新编码转换。因此,快照导出的数据形态与 MongoDB 原生存储(Native Data)在物理上并不完全相同。LinkedIn 自研的分布式文档数据库 Espresso 同样印证了这一逻辑:虽然底层依托 MySQL 存储,但其上层采用文档存储模型,在执行全量 Dump 数据时,同样需要基于最新 Schema 进行转换规整以确保版本向前兼容。

---

[01:04:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=3840s) 模式二:基于 REST API 与 RPC 的服务交互及 Salesforce 的版本实践

第二种常见的数据流转方式是借助 REST API 或 RPC 进行客户端与服务端间的交互。在现代微服务拓扑中,“客户端”与“服务端”的角色完全是相对的。例如 Uber 的行程服务(Trip Service),面对发起叫车的用户端时它是服务端,而为了结算扣款调用底层支付服务(Payment Service)时,它自身又变成了客户端。

谈及架构名词的演化,当下流行的微服务架构(Microservices Architecture)本质上是对十多年前 Java/REST 时代面向服务架构(SOA, Service-Oriented Architecture)的重塑与再包装(Rebranding)。虽然概念包装更加时髦,但两者的核心哲学完全一致——都是为了服务解耦。通过清晰的契约(API Contract),Trip 团队与 Payment 团队可以由不同的组织独立维护、独立演进与独立部署。只要对外接口的 Agreement 保持稳定,内部技术栈即便从 Node.js 重构为 Go,或者从 Java 改写为 C++,外部调用方都完全无感。DevOps 运维中绝大多数故障根源都在于人为错误(Human Error),规范的接口协议本质上是为了降低多团队协同中的沟通损耗与失误概率。

在接口版本演进方面,原生的 REST API 规范并未对版本控制(Versioning)给出强制标准。工业界通常有两种主流实践: 1. **URL 显式版本号(如 Salesforce)**:Salesforce 将 API 版本号直接嵌入 URI 路径中(如 `/services/data/v1/user`、`/v2/user`)。其维护策略极其严格,生产环境通常仅维持向后兼容最近 3 至 4 个版本(大约对应过去一年的版本周期);一旦某个版本逾期,平台会提前发出日落(Sunset)通知,强制要求调用方升级,逾期则彻底废弃旧接口。 2. **面向大客户(2B)的定制化路由**:针对企业级服务商(如一家向 Uber 和 Facebook 提供基础服务的 2B 公司),通常会在系统内部针对不同签约客户维护专有的版本映射与兼容网关,以确保各巨头客户的调用平稳过渡。

- **角色相对性**:服务往往兼具 Server 与 Client 双重身份(如业务层对上暴露服务、对下调用支撑服务)。 - **架构同源性**:微服务架构本质上是 SOA 的现代化演进,核心目标始终是通过接口契约降低系统耦合与人员协作成本。 - **REST 版本策略**:REST 原生缺乏版本规范,Salesforce 采用 URL 路径嵌入版本并实施 1 年(3~4 个版本)的严格 Sunset 机制;2B 场景则常结合租户身份定制版本支持策略。

[01:10:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=4200s) 传统 REST API 的版本管理痛点与 REST 的本质

在跨公司或微服务协作中,传统 REST API 的版本维护往往非常痛苦。以 Salesforce 的服务为例,服务方在内部需要维护很多个版本:Uber 可能还在用 Version 15,Cisco 还在用 Version 1,而 Lyft 则在用 Version 13。一旦服务方需要推行版本升级,就必须专门找 Uber 的团队谈判,说明新版本带来的好处,催促对方排期升级。等到对方准备就绪,服务方才能把客户端的版本号从 1 推进(bump)到 15。这种管理方式极其繁琐(tedious),也是传统 REST API 相比现代 RPC 和 Schema Evolution 机制的一大劣势——它在协议层面上缺乏内置的版本演进支持。

从根源上来看,REST 并不是一个硬性的技术标准或协议规范,而源于 Roy Fielding 的博士毕业论文。它本质上是一种架构哲学(philosophy),定义了包括无状态、统一定义在内的六条设计约束。这套理念因为定义得好,所以在工业界得到了极其广泛的应用。这与分布式系统中的 CAP 定理类似——很多新人常常把 CAP 挂在嘴边,但 CAP 本质上也是指导权衡的哲学工具,并非具体的工程协议。与面向资源的 REST 相对的是 SOAP,SOAP 的设计更偏向于 Action 导向,但如今基本上只剩一些传统的 2B 企业还在使用,工程师即使不了解 SOAP 也没关系,关键是要深刻理解 REST 的设计理念。

当数据流通过 RPC 或 REST API 进行跨网络传输时,存在很多根本性的缺陷(fundamentally flawed)。网络调用与本地函数调用(Local Function Call)有着本质的区别: - 本地函数调用是高度可预测的(predictable),除因 CPU 上下文切换导致执行被其他进程抢占外,结果非成即败(要么 success,要么 fail)。 - 网络调用则充满了不可预测性:请求在发出前可能会丢包,响应在返回途中也可能会丢失。因此,使用 RPC 或 REST API 时必须显式处理超时(Timeout)机制。

```mermaid flowchart TD accTitle: 本地调用与网络 RPC 调用的可靠性对比 accDescr: 对比本地内存函数调用与跨网络 RPC 调用的不可靠因素,突出网络丢失与超时对调用的影响

subgraph LOCAL["本地函数调用 (Local Call)"] L_Caller["调用方 Client"] -->|直接内存寻址| L_Func["本地函数 Function"] L_Func -->|明确的 Success / Failure| L_Caller end

subgraph REMOTE["网络远程调用 (RPC / REST)"] R_Caller["客户端 Client"] -->|网络传输 Request| NET_REQ{是否丢包?} NET_REQ -->|丢失| LOSS1["请求丢失 (Timeout)"] NET_REQ -->|成功送达| R_Server["服务端 Server 执行"] R_Server -->|网络回传 Response| NET_RES{是否丢包?} NET_RES -->|丢失| LOSS2["响应丢失 (Timeout)"] NET_RES -->|成功返回| R_Caller end ```

[01:14:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=4440s) 网络调用的核心挑战:超时重试、幂等性与复杂系统边界

当网络调用发生超时(Timeout)时,客户端实际上处于一种“黑盒”状态,根本无法判断服务器到底执行了请求没有——可能是请求根本没到达服务端就丢失了,也可能是服务端已经成功处理并扣费完毕,但在回传响应时网络中断了。为了保证请求最终生效,客户端必须进行重试(Retry)。而一旦引入重试,服务端就可能收到重复的请求。

正因如此,所有的 REST API 和 RPC 接口在设计上几乎都必须满足幂等性(Idempotence)。所谓幂等,即同一个请求被执行多次,不会产生额外的副作用。例如在直播或讲座打赏的支付场景中: - 客户端发起打赏扣款后发生网络超时,用户并不清楚扣款是否成功。 - 客户端发起重试,如果第一次请求实际上已经扣款成功,第二次重试就应该被识别为重复操作,变成一个空操作(no-op),避免重复扣钱。

处理这些边缘情况(corner cases)会极大增加系统的设计复杂度。Scott 回忆自己刚从学校毕业进入互联网公司时,曾担心过程序员会不会失业;但真正接触工程后发现,即便是调用一个简单的 API,背后也要处理负载均衡(Load Balancing)、分片路由(Sharding / Routing)、超时重试、幂等机制以及各种异常捕获。正是这些处理不完的复杂边界情况,让“程序员永远不会丢饭碗”。

此外,本地函数调用与网络 RPC 还存在以下三点显著差异: - **执行时间不可控**:本地调用耗时是完全确定的;而网络调用受链路抖动影响,延迟可能从 1 毫秒剧变到数秒。 - **内存指针 vs 编解码**:本地函数直接传递指针(Pointer)访问局部内存,开销几乎为零;网络请求则必须经历序列化与反序列化(Encoding & Decoding),将数据结构转换为二进制流或文本流再通过网络发送,带来显著的 CPU 和时间开销。 - **多语言交互复杂度($N^2$ 难题)**:本地调用通常运行在单一语言的 Runtime 中;而微服务架构下客户端与服务端可能采用不同的编程语言(例如 Java 调 Go)。如果客户端有 10 种语言支持,服务端也有 10 种实现,多语言交互的复杂边界将达到 $N^2$(即 100 种组合),隐藏着难以彻底解决的底层兼容问题。

[01:17:30](https://www.youtube.com/watch?v=9R-R8ePV22s&t=4650s) 三种数据流模式对比:数据库、RPC 与消息队列(Message Queue)

在分布式系统中,常见的数据流向主要有三种模式: 1. **通过数据库传递(Dataflow through Database)**:这在系统设计中通常是被明确划红线禁止的,绝不能把数据库作为微服务之间日常数据流转和通信的介质。 2. **通过 REST API / RPC 传递**:虽然具备网络不可预测性和各种设计缺陷,但同步交互场景下完全可以接受。 3. **通过消息队列传递(Message passing via MQ / Kafka)**:通过引入 Message Queue 或 Worker Pool,从架构根源上实现了真正的关注点分离与解耦(Decoupling)。

与同步的 RPC/REST 相比,消息队列通信具备多重核心优势: - **原生异步与弹性伸缩**:消息的发送方和处理方解耦,任务被异步化。当消费端 Worker 不足时可以随时扩容,空闲时可以缩容,系统的维护与排错(Debug)变得清晰独立。 - **高可用与崩溃容忍**:在 RPC 模式下,如果服务端宕机,客户端请求会立即报错(Unavailable);但在消息队列模式下,服务端挂掉后消息依然安全地暂存在队列中,等服务端或 Worker 重启恢复后可以继续消费,业务请求不会直接丢失。 - **寻址透明度(Location Transparency)**:客户端发送消息时,完全不需要知道具体是哪一台服务器在消费,不需要关心后端服务器的真实 IP 地址和端口号(Port number)。 - **天然支持扇出广播(Fan-out)**:一条消息可以被多个不同的业务服务同时消费处理。

```mermaid flowchart TD accTitle: 消息队列在 YouTube 场景下的单消息多服务消费机制 accDescr: 展示用户观看视频的行为生成一条消息后,通过消息队列分发给历史记录服务和机器学习推荐服务异步消费

User["用户行为: 观看视频"] -->|生成事件消息| MQ["Message Queue / Kafka"]

subgraph CONSUMERS["多服务异步消费 (Fan-out)"] MQ -->|消费事件| Svc_History["播放历史服务 (User History)<br/>记录登录与观看记录"] MQ -->|消费事件| Svc_ML["机器学习服务 (Machine Learning)<br/>训练偏好并实时生成视频推荐"] end ```

在实际工业界中,以 YouTube 的系统设计为例:用户观看完一个视频后,该行为会转化为一条 User Activity 消息投递到消息队列。后端多个下游微服务订阅该消息:一个服务专门负责登录验证并记录用户播放历史(User History),另一个机器学习推荐服务(Machine Learning)根据该活动即时分析用户兴趣,向其推荐可能喜欢的相关视频。类似地,在 Uber 请求行程时,发单消息也可以同时触发支付(Payment)与派单等多项服务协同处理。消息队列属于典型的“发送即忘”(Send-and-forget)异步通信模式,而 RPC 和 REST 则通常要求同步等待对端返回响应。

[01:21:45](https://www.youtube.com/watch?v=9R-R8ePV22s&t=4905s) 问答与讨论:RPC 异常处理、REST vs RPC 的选型与 GraphQL 优化

在问答环节,有听众提出实际工程问题:Thrift 具备显式的异常定义机制(如 `throws` 语法),而 Protobuf 在跨语言支持时并没有原生的语言级 Exception 语法,该如何处理异常? - 讨论指出,Protobuf 本质上是一个共享的数据编解码格式,并不负责模拟各语言运行时的异常抛出。 - 常见的工业界做法是将错误(Error)本身作为 Protobuf 的一个普通字段(如 `error_code` 或 `error_message`)进行封装传递,调用方根据该字段在各语言层面自行抛出或处理错误。

针对 REST 与 RPC 的对比与选型,核心结论如下: - **执行效率**:REST 基于 HTTP 传输文本或通用 JSON,传输开销较大;RPC 采用二进制编解码(Binary encoding),报文体积更小、解析速度更快,通常效率比 REST 高出约 30%(具体取决于数据模式)。 - **代码生成(Code Generation)**:RPC 框架(如 Thrift、gRPC/Protobuf)通常自带代码生成层,使得远程调用在代码层面的体验与调用本地函数(Remote Function Call)无异;而 REST 更多是一种架构哲学,没有内置的代码生成机制。 - **业界选型惯例**:通常对外的公网接口(Public API)采用普适性强、无客户端绑定的 REST API;企业内部微服务通信(Internal Services)或公司间专线协作,则优先选用效率更高的 RPC。 - **安全性与鉴权**:RPC 通常运行在受信的内网环境中,往往默认调用方身份已通过验证,内部函数调用通常不再像面向公网的 REST 接口那样对每一次调用做复杂的 Authentication 和 Authorization。

```mermaid flowchart LR accTitle: 互联网架构中 REST 与 RPC 的常见部署边界 accDescr: 公网客户端通过 REST API 访问网关,内部微服务之间通过高性能二进制 RPC 相互通信

Client["公网客户端 (Public Client)"] -->|REST API / HTTP<br/>通用友好| Gateway["API Gateway 网关"]

subgraph INTRANET["内网受信任微服务集群"] Gateway -->|RPC / Protobuf / Thrift<br/>高性能、二进制、代码生成| SvcA["微服务 A"] SvcA <-->|内部 RPC 通信| SvcB["微服务 B"] SvcA <-->|内部 RPC 通信| SvcC["微服务 C"] end ```

关于新兴的 GraphQL: - GraphQL 是对传统 REST API 痛点的一种重要优化。在类似 Airbnb 或 Uber 的复杂业务中,单个核心实体(Entity)往往极其庞大。例如一个 User 实体,用户既可能充当司机的角色(Driver,包含出车记录 Driving Record、最后上线时间 Last Online Time 等),又可能充当乘客的角色(Rider,包含乘车偏好等字段)。一个庞大的 User 实体可能包含几十个顶级字段、上千个子字段。 - 如果采用传统的 REST API 调用 `getUser`,服务端往往会将上千个字段全量序列化并返回,造成巨大的网络和性能浪费。 - GraphQL 的核心设计哲学就是“按需查询”(Selective Query):它不负责底层的认证鉴权,而是允许客户端在发起请求时明确指定自己只需要哪几个字段,服务端按需返回裁剪后的数据,这是目前非常火热的新型接口设计方案。

对于《Designing Data-Intensive Applications》(DDIA) 书中对 RPC 的批判态度: - 书中详尽阐述了 RPC 试图将网络调用伪装成本地调用的根本缺陷——不可预测的网络延迟、必须手动处理的幂等性与重试、强制的编解码开销等。 - 但在当前的工业实践中,RPC 并没有更好的替代品。虽然存在这些物理限制,但在未来,随着网络硬件和极低延迟技术(例如 5G 极低时延通信支撑远程自动驾驶,或者硬件总线级互联)的发展,远程通信的可靠性与速度可能会产生质的跃升。 - 目前系统工程师仍然必须清楚意识到 RPC 的局限性,在软件架构层面上严谨地做好容错与幂等设计。

## Notable quotes > [05:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=300s) 在所有的 System Design 里面,Surprise 都不是一个 Good Thing,Surprise 都是 Bad Thing…… 成功的一个 Engineer,设计的目标是减少 Surprise,是非常悲观的。Engineer 设计应该非常悲观,尽量考虑一些 Worst Case Scenario。 — Scott Shi

> [05:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=300s) Data Outlives Code — Scott Shi

> [29:45](https://www.youtube.com/watch?v=9R-R8ePV22s&t=1785s) 为了表示 Lens 是 6,它整个这个数量都比这个 6 大了 — Scott Shi

> [29:45](https://www.youtube.com/watch?v=9R-R8ePV22s&t=1785s) 变成一个 tomb 了,就变成一个坟墓了,没人敢用这个 2 了 — Scott Shi

> [36:50](https://www.youtube.com/watch?v=9R-R8ePV22s&t=2210s) 我自己至少见过两三次了,就是这个一行 code 把系统 breakdown 了 — Scott Shi

> [01:00:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=3600s) 永远是 Data 大于一切,Data 永远比 Code 重要 — Scott Shi

> [01:14:00](https://www.youtube.com/watch?v=9R-R8ePV22s&t=4440s) 程序员永远不会丢饭碗 — Scott Shi

## Mentioned Martin Kleppmann, Designing Data-Intensive Applications (DDIA), Kubernetes, MongoDB, JVM, Python, Java, Go, Ruby, pickle, JSON, XML, CSV, Twitter, Snowflake ID, MessagePack, Apache Thrift, Protocol Buffers (Protobuf), gRPC, Apache Avro, Apache Kafka, Watchtower, Hadoop (HDFS), Facebook, HHVM, Hack, C++, Adobe, Microsoft Word, LinkedIn, Espresso, ZooKeeper, KRaft, MySQL, Salesforce, Uber, Lyft, Cisco, Farmers, State Farm, Progressive, Roy Fielding, YouTube, Airbnb, GraphQL, SOAP, REST

## Video description Please subscribe to my channel!

in this session, Scott covered the session of encoding(protobuf, thrift or avro) and evolution doc: https://docs.google.com/presentation/d/1D0OTKXtYvOxzGR0qBnStKWpOSxNGu1CzZ9fAfhnWOes/edit?usp=sharing

[Source](https://www.youtube.com/watch?v=9R-R8ePV22s)

中文简报

分布式演进严禁复用 Tag 与 Required,规避 RPC 雪崩 数据生命周期远长于代码,工业级 RPC 演进必须彻底废弃 Required 字段并将校验移至应用层,否则版本不兼容将直接引发反序列化雪崩与静默数据丢失。 本讲座深入探讨了分布式系统中数据编码与模式演化的核心挑战,直指“数据寿命远长于代码”的工业界铁律。主讲人通过新人改动一行代码引发线上崩溃等真实生产故障,拆解了语言内置序列化与文本格式的严重缺陷,对比了 Thrift、Protobuf 及 Avro 等二进制协议在元数据压缩与向后/向前兼容性上的设计权衡。随后,讲座系统剖析了共享数据库、RPC/REST 接口与异步消息队列三种数据流动范式,详细论述了网络调用的非确定性、幂等性保障、版本升级演进以及 GraphQL 针对冗余查询的优化思路。 分布式架构中“数据寿命长于代码”(Data Outlives Code)是一条核心铁律,代码版本迭代频繁,而底层遗留数据可能存活数年甚至数十年,兼容性设计重心必须放在数据而非单纯代码上。 (5:00) Thrift Compact Protocol 与 Protobuf 依靠去除显式字段名、引入 Tag 寻址及变长整数差值编码,可将报文体积压缩至 JSON/MessagePack 的一半左右。 (34:10) 在 Schema 演进中随意新增 required 字段或复用已废弃字段 Tag 是引发线上崩溃的最常见原因,业界最佳实践是将必填校验移至应用层处理并保持字段均为 optional。 (36:50) RPC 试图将网络调用伪装成本地函数调用存在本质缺陷,实际工程中必须显式处理网络超时、重试风暴与接口幂等性。 (74:00)

Reading summary

主讲人 Scott Shi 围绕《数据密集型应用系统设计》(DDIA)指出,在分布式系统的滚动发布与微服务架构中,多版本共存是必然常态且“数据寿命远长于代码”;工程师必须依靠严谨的 Schema 演进规范与悲观容错设计,在序列化编码与数据流转范式中平衡系统稳定性与执行效率。 数据寿命长于代码与兼容性铁律 (5:00) 在追求零停机与高可用的生产环境中,系统升级通常依赖滚动递增发布,导致不同版本的代码必然交叉调用。Scott 强调“Data Outlives Code”是系统设计的核心原则:线上业务代码通常数月即可更新淘汰,但存储的历史数据往往留存数年乃至永久。若缺乏向后兼容(新代码读旧数据)与向前兼容(旧代码读新数据)设计,系统将面临灾难性破坏。 在字段演进中,主讲人强调绝对禁止复用已废弃的字段编号(Field Tag 必须永久封存为 Tomb),同时原则上严禁新增 Required 必填字段。若服务端在新版本中添加 Required 字段,未升级的旧版客户端请求会因缺失字段直接触发反序列化异常并中断服务。业界成熟方案是将新增字段均设为 Optional,并将校验收敛至应用层防御逻辑中。 从文本冗余到二进制极致压缩 (15:00) 跨节点传输需要将内存对象序列化,但 Python pickle 或 Java 原生序列化因语言锁定、缺乏版本控制以及反序列化代码执行(RCE)与解压炸弹等严重安全隐患,在工业级生产中被明确禁用。通用的文本格式亦存在缺陷:CSV 易因特殊字符转义错位导致数据损坏;JSON 缺乏精细数值类型,曾导致 Twitter 因 JavaScript 浮点数截断 Snowflake ID 精度而被迫改用字符串传输,且无法高效承载二进制流。 面对高频并发调用,文本与 MessagePack 每次均重复发送字段名,造成网络带宽极度浪费。Thrift Compact Protocol 与 Protobuf 抹去字段名,采用数值 Tag、Delta 差值编码与变长整数压缩元数据,将示例数据体积从 MessagePack 的 66 字节压榨至 34 字节,成为微服务 RPC 的事实标准。相比之下,Apache Avro 将 Schema 剥离至文件头或注册表,适配于数仓批量转储与 Kafka 事件流等海量数据导入场景。 三种数据流转范式的可靠性权衡 (52:30) 分布式架构中主要存在三种数据流动方式。通过共享数据库传递数据被普遍视作架构反模式,不仅破坏服务隔离,还极易引发数据静默丢失:例如旧版服务读取包含新字段(如新增头像 URL)的数据时会忽略该字段,更新后写回数据库便会将新数据永久冲掉。 相比之下,基于 REST API 或 RPC 的同步调用实现了服务解耦与契约隔离,但面临网络不可预测性,必须显式设计超时重试与服务端幂等机制。第三种模式则是基于消息队列(如 Kafka)的异步流转,具备原生弹性伸缩、节点故障容忍、位置透明以及天然的扇出广播(Fan-out)能力(如 YouTube 将单个用户观看事件异步分发给播放历史服务与机器学习推荐引擎)。 RPC 选型边界与按需查询演进 (81:45) 针对接口协议选型,工业界惯例是对外公网接口采用跨语言通用友好的 REST API,企业内网服务间则采用具备静态类型检查、自带代码生成且效率高约 30% 的二进制 RPC 框架。针对复杂业务中单一实体字段膨胀(如包含数百个司机与乘客属性的 User 对象)导致的带宽浪费,GraphQL 提供了客户端“按需查询”(Selective Query)的有效优化。 尽管 DDIA 批判 RPC 试图将不可靠的网络调用伪装成本地调用存在根本性缺陷,但 Scott 认为工业界当前仍缺乏替代方案;系统工程师必须始终保持悲观的防御思维,妥善处理重试与幂等设计。 主讲人认为,尽管未来高带宽、极低延迟通信硬件的发展有望缩小远程网络调用与本地调用的物理鸿沟,但在当前的无共享架构下,消除意外(No Surprise)依然是架构设计的首要目标;工程师应当把对数据模型演进和生命周期的把控置于算法实现细节之上。

Source quotes

[5:00] Scott Shi: 在所有的 System Design 里面,Surprise 都不是一个 Good Thing,Surprise 都是 Bad Thing…… 成功的一个 Engineer,设计的目标是减少 Surprise,是非常悲观的。Engineer 设计应该非常悲观,尽量考虑一些 Worst Case Scenario。 [5:00] Scott Shi: Data Outlives Code [29:45] Scott Shi: 为了表示 Lens 是 6,它整个这个数量都比这个 6 大了 [29:45] Scott Shi: 变成一个 tomb 了,就变成一个坟墓了,没人敢用这个 2 了 [36:50] Scott Shi: 我自己至少见过两三次了,就是这个一行 code 把系统 breakdown 了 [60:00] Scott Shi: 永远是 Data 大于一切,Data 永远比 Code 重要 [74:00] Scott Shi: 程序员永远不会丢饭碗

Original transcript

[0:00] 今天讲那个encoding and evolution这一张呢就是这是DDIA的那个Design Data Intensive Application第四张然后这张其实很多时候就是很多部分可以跳过的但是这张其实还是挺重要的一张因为它这个就是像我们Safety组前两天前前期的时间就有一个engineer一个new class然后就改了一行code然后就broke了很长的一段时间代码这是一个真实的例子然后就是和这块这个evolution就是schemaevolution这块没有理解和比较相关如果你用那个就是RPC的话这块作为一个new grids来说是最容易blow up的地方就是最容易new grids或者是比较没有经验的话其实不一定是new grids如果你是对RPC比较新的话也非常容易blow up的地方这个是基本上new grids都是有非常惨痛的教训的在这块常见的一个比较容易出事的地方,就是工作中。然后这张主要也是讲了两大部分吧,一大部分是这个encoding,encoding也叫Serialization和Desserialization,它其实是一种data transfer 的一种方式,你Data在Memory里面你是有Data structure,各种各样Data structure在Memory里面然后你是,比如说加Memore 设定里面的话,你有GVM里面,你有HIP,有Stack管理的那个层,然后你class就是incapture起来,然后你这样子但是你如果比如说你在两个不同的机器,像我们这话说的讨论都是Internet都是在intelign,share nothing architecture在你需要从一台node传到另一台node的时候那你就需要有一个encoding然后或者叫serialization把这个data能传过去所以是serialization和desserialization是一定要做的所以这块的话就很容易出事的地方常见的比如说java的话像python的话不是有个plpico你可以就pico dump然后pico load然后就可以做这个serialization和desserialization就是Serialization和Desserialization这块然后另一块的话讲了一下DataFlow几种不同的DataFlow的形式然后还是非常有趣的所以我们这块也会看到然后这块的话我们会讲一些REST API和SOAP的区别然后也讲一下Micro Service listProtobuf和这个Protobuf和Shift好,然后在我们这个Code中,我觉得有一点和就是不同的地方,比如说你在做学术的时候,做student做Project的时候有一点不同的是,你在做Project的时候,在学术的时候,一般只有一个VersionCode你比如说你在写一个version code然后它不work的话或者improve的话就有一个version code但是在所有的工作情况下是不一样的在工作情况下往往有多个version code你比如说你现在是在production中可能有多个version你可能是version 1,version 2并且在任何一个时间段在任何一个时间段你可能有多个version并行的在run假设你在有version 1假设你在有version 1然后你要更新你的code现在的话,现在所有的distribute system都讲究的是0 downtime,你的availability一定要高所以如果你想把version 1 upgrade到version 2的话version 1 upgrade到version 2的话,你不会是一下就整个把version 1全部bring offline,只有银行会这么干不会把所有version 1bring offline,然后再deploy,全部deploy version 2它现在大部分都是用一些比如说用kubernetes这种就是你update是incremental update就是比如说你version 1有10台machine你先bring down一台然后把9台变成version 1,1台变成version 2是吧然后在任何一个时间段你可能有多个version同时在run的所以说这个这种情况下的话你其实是在任何情况下你的任何一个service在production里面可能有多个version这个事情会变得很复杂如果你是像做Micro Service的话如果你做Micro Service的话你其实是有多个Service然后每个Service有多个Version那多个Version之间相互交互的时候你的Assumption可能就不一样了所以Assumption不一样就很容易出生Bug所以这也是为什么做Micro Service的时候你Service Level的Agreement一定要做得非常好就是说你这个Agreement什么是允许的什么是不允许的你整个这个Doc要做得非常好就是这样你跟其他组合作的时候它会有非常好的合作的一个pattern然后如果出问题的话你也可以说这个我们这个是这个agreement是哪一个组的fault是吧这个是非常重要的然后其次的话这个code是一方面如果做的经验多的话就是我觉得New Glass是非常在乎code的但是做的经验多是不在乎code是非常在乎data的 [5:00] 谢谢data是远远重于code的,为什么呢?因为有几种常见的方式,你的data一般都是outleave code的你的code可能在任何一个时间段只有version 1和version 2两个version,version 1是old version,version 2是新的version这是你的code 是吧,任何情况下就两个version,就是旧的version和新的version code,但是data的话,你data可能是有5年前输入的data,10年前输入的data和刚才输入的data每个data的format如果你没有专门整理过,你没有做data migration的话,没有把它统一整理过的话都是不一样的。所以这个data呢Version data是远远大于code的,任何时候的code产生了data,任何一个version产生了data,都会leave在那里。所以经常有一个常见的说法就是data outleave code。Data outlive code,这个也会反复提起,在今天的这个speak中也会反复提起。所以说做的经验多的话,就是Data往往是最重要的,比Code越远重要。所以这个,不管你是用什么algorithm实践的,但是如果你API这个实践好的话呢,你不管是algorithm A,algorithm B,它只有performance的不同。但是你Data,你五年前的Data,如果和现在Data不compatible的话,那就是很大的问题了,就非常非常大的问题了。和这个code schema version的进化是吧,version是怎么进化的,不同之间的version是怎么样compatible的。这个的话是非常重要的一章,就是它很多这个字里面讲的东西都是就是工作中比较学与类的教训。好的。所以我们看一下relational database,因为它是我们像之前大概两三周前讲过,relational database是schema on write,就是当你写进去的时候,你就根据你的data和relational database的table进行verify,一旦你不满足relational database的schema的话,你的write会被reject,但是所有的这个NoSQL database,比如说Mongo,你是Schema on RAID,就是什么意思?就是你写的时候可以任何Schema,Mongo可以把任何Json写到它的Database里面,只要它那个size,比如说它size没有超过什么16MB,你可以写任何Json。但是你读的时候,你需要有一些intelligence,是吧?你读的时候,你知道它哪些schema有,哪些schema没有。所以这个Mongo这个database,你经常写的时候是很smooth的,但是你读的时候有surprise,是吧?然后,记住,所有的system design里面,surprise都不是一个good thing,surprise都是bad thing,就是,surprise就是,所有的system design engineer,你设计的goal是减少surprise的,就是,成功的一个Engineer,是减少Surprise,是非常悲观的,所以Engineer设计应该非常悲观的,尽量考虑一些worst case scenario,减少Surprise所以你Mongo写进去的时候,你写的时候很开心,你读的时候有big surprise 是吧?但是你如果用那个relational database的话,你写的时候如果出问题的话,写的时候就reject了,但你读的时候很happy,不需要处理很多connor case所以,就是不同的 Schema on Read 和 Schema on Write然后其次的话,这块也是我之前谈的,就是当你在任何情况下的你server side的这个就是internet company,是吧?所有现在热门的都是internet company,热门的company都是internet company,你server side application的话,肯定是有多个version在轮,再结合你的Micro Service,Gbag Service再加几十个version,Gbag Service每个service再加两个version,那这个交互就非常interaction,非常非常复杂了。好好,其次的话其次的话,Client side的这个version就更复杂了,你比如说Apple的话这个还好一点,至少Apple的operating system是version是比较固定的是吧,就是它几个version是相对来说是比较少的,就是iOS,Android的那块就非常fragmented,operating system就非常fragmented,你application的这个version更为复杂。你真的发了三个request,还是我们这个version,这个code的问题,还是什么,确认不错,我们必须把data purge一下,是吧,把data clean up一下,然后我们还得强制这个client的端,update一下version,我知道iOS好像是可以有一个强制update, [10:00] 就是你必须update以后才可以使用,但是这个其实是大杀器嘛,就是你不希望使用太多是吧然后这个就很难控制,所以Client application version就更多了。我们再反回一下,再想一下这个问题啊。你Client可能有几十个version,iOS有十几个version,你Android有几十个version application。然后靠到你这个server段,你server段有几百个micro service然后每个Micro Service的Cell多个version共同存在,你就可以看到,其实每个人使用的时候,他的Experience都是不一样的。这个又 depends on 它到底靠哪一个client version 是吧然后再看到哪一个server version然后这个server version又和另一个server的哪一个version进行进行交互了,所以这个Experience是非常不一样的。很多时候这个很容易出出bag,经常有人抱怨UberEats这个APP出bag,那你实际上它这个,你到底是哪一个version出bag,你的version出bag version,可能跟别人出bag version不一样,你简单相乘一下就有几百种可能。基本上千种可能的就是即使你用一个很简单的feature简单相当下基本上千种可能的哈然后这块我定义了两个非常重要的概念,一个叫backward compatibility就是backward compatibility,就是你写一个新的code,可以读旧的data新的code 可以读旧的data新的code 可以读旧的 data 好然后 forward compatibility 是什么意思呢 是你旧的code可以读旧的data同时也可以读新的data就是你旧的code设计的时候,你有前瞻性是吧,这个forward compatibility更难做,就是你需要有前瞻性,所以你这个旧的code不但能读旧的data,也可以读新的data,就是这两个compatibility的问题。好,然后我们再说这个程序的时候,同时你data大部分是有两种形式,一种是在memory的,比如说你在jvm,jama virtual machine 里面,你可能有HIP 是吧 就是有HIP stack 然后你code 其实是专门存在这个 GBM 有个code section 里面是专门存code 的然后你处理的时候呢 你这个data 实际上可能是 不管是structure, list, array, hash table, trace 这些可能dependent on language 是吧 有的是在stack上 有的是在HIP上 是吧但是这些总体来说呢 它是用pointer 来进行operation的然后它是被optimize过的,如果我们回想一下上周的那个类比是吧,就是这个访问的类比的话,你在memory中取一个数据就相当于就走200公里取一个数据还是很快的,到disk的话你可能就到土星那块了是吧,得走好几天,你如果按旅行者的话得走好几天,就到土星那块起一个出去就很慢了。但是当你想share的这个data和另一个node的时候,因为所有的都是shareNasi architecture,当你想share这个data和另一个node的时候,那么你必须把这个data通过network或者通过其他的方式,send 是吧,那你必须把这个data,in memory data structure,转化成一种可以接受的,可以transfer,从network transfer 的这个一种方式,那么的话你可能就需要,不管是把它变成一种Json, encode成一种Json,或者 encode成一种Binary format,你必须把它encode一下。这种的话,如果在上学的话,大家写的时候也经常见,比如说你这个写了一个python,然后把python这个program,或者把这个program加一些data,然后这个pico dump一下,然后pico load一下,这也是一样的。也是一种 Serialization and Deserialization,它就是两种 Presentation.OK然后它这个有多种叫法,叫Serialization/Desserialization,然后这本书里面叫Coding和Encoding,Coding和Encoding和Decoding,然后这个,像这个,Go里面还有什么叫Marshalling和Marshalling有不同的叫法,但是它其实只是同一个东西,就是说白了是把in-memory的一种Data structure,变成一种可以在这个Network之间传的Text Representation,或者是一种Binary Representation。然后最常见的一种就是language,比如说这个java的serialization,ruby的marshall,然后python的pico这个就是你用的时候非常简单,但是它有很多的问题,你是很难在production中,你是很难在工作中用到这些,你可以在学术中用到这些,可以在工作中用到这些。为什么呢?首先这个encoding呢,是和某个language相保定的,是吧?你如果是用python的pcode的话,你不可能用java的serializeboard来decode。然后其次的话,这个它有security问题,如果因为你作为一个server端,你要run这个的时候,你实际上是把,比如说你serialize,你其实run一个deserialize的code。在你desserilize code里面可以做任何code就像说白了这个问题就很像这个Lead Code,我们前两天讲了Design Lead Code的问题。你别人Send你一个Code,这个Code可以做很多事情。最常见的一个攻击 [15:00] 这个叫decompress bomb这个它把一个data一个data非常小的1k的data传给你然后做decompress然后直接就把你的整个memorymemory和disk直接crash掉了因为它1k的datadecompress可能变成两个t或多个t的data你整个这个系统就crash掉了所以你有security问题因为你要执行一段你不知道是什么的code然后其次的话有version的问题这个是完全没有version的是吧你dump这个data不管dumpdatadump code是完全没有version是完全没有做没法做不同version之间相互coordinate的你要update的系统假设你做这个你用这个作为一个productiondeploy到一个production系统你这个version你怎么样upgrade可能你是要把整个system什么go offline然后花几个小时update跟这个chase啊这种大银行做的一样其实非常awkward不应该有dontime然后最后的话efficiency是非常低的然后这块的话我想额外提一下就是Java像这种language很多这个非常好用的building library其实非常非常有很多坑的比如说这个还有另一个常见的就是Java hashcode你Java hashcode的理论上来说一个hashcode是给一个stringhash完以后的HashStream是仅跟你的那个InputStream相关的,但加上Hashcode不一样加上Hashcode是不reproduceable的,就是你这个ThreadHashcode和另一个ThreadHashcode可能是不一样的所以你其实它不是一个简单的跟这个Hashcode最后产生的HashStream不仅仅和你的Input有关还和你具体实践的时候是在哪一个side执行的和哪一个,我记得哪个side, 还是哪一个cpu执行的相关所以这个就完全没法 reuseable所以这个看起来有些很好用的东西其实不能用完你看hashcode的这个,如果你是为了能reproduce的话就是任何人都不应该用这个hashcode来加完的hashcode好,然后其次的话另一种方式就是另一种方式,这叫language specific encoding and decoding另一种常见的encoding和decoding方式就是用JSON,XML和CSVJSON近几年比较火,多半原因是因为这个JavaScript相对比较火一点所以JSON相对来说比较好用一点,并且像比如说Mongo也非常native的支持JSON,就直接start到database里面所以这个JSON这几年比较火的原因然后但是这几种方式呢,就是不管你把DataDOM成一个JSON还是DOCSV 还是 dump 成一个 XML它是有它的优势比刚才那个要好很多但是它有劣势,比如它劣势什么呢?在 XML 和 CSV 里面你不能区分它到底是一个 number 还是一个 string它可能比如说 15 你是不能区分它的 data type 是 number 还是 string 的然后 JSON 里面是不能区分很大的那个数和浮点数的所以在这个Json里面,Twitter有一个坑,就是Twitter我忘了它有个什么ID的,然后它那个数是非常大的一个数,非常大的一个big number,然后导致这个Json,它incrept那个数的时候,encoding和decoding那个数的时候会产生歧义,所以它区分不了它对应的到底是数A还是数B,所以它必须用另外一个data帮它去encoding,所以这个Json有很多坑,Twitter反正之前采过一种坑。然后其次的话这个JSON还是不管是JSON XML还是CSV它是不支持BinaryStream的如果你想把它变成Binary的话比如说你想把它变成BinaryStream那你能做的事情只能是把BinaryStream先变成Base64Encode一下然后传Base64它的Efficiency会减少很多他们说我记得他提了一个数据说efficiency会减少33%它浪费了一些efficiency其次的话有一些schema support对于Json和XML也不是太好的XML和Json其实设计的时候设计者是加了一个schema support但是没有人用没有人用Json和XMLschema support他其实加了很多很不错的东西但是它中间可能太复杂了,没有人用它比如说还加了一些这个Regular expression什么的,还是挺powerful的但是还是没有人用就是很尴尬的一点,就是你设计的时候你是这么想的,但你的user不一定这么使用然后最后一点的话就是csv其实是有很多counter case的比如说你这个csv是用comma来separate的话那你如果这个value里面content一个comma怎么办你可以skip string你可以skip string来就是skip这个comma但是它还有很多其他的countercase就导致很复杂这个是我是知道的因为这个我当时有个project是parse所有的conversation然后这个data parse出来是csv的然后导致中间有很多countercase非常复杂然后这个csv经常容易被mass up掉mass up掉的话就整个这个row就是你看到这个csv [20:00] 所以这个肉就卖散不掉了,这个data就非常missaligned有很多 counter case 非常复杂 你要把所有 counter case cover 一遍 其实挺复杂的所以这个CSV 如果你Data Transfer 通过这个JSON,CSV,XML 是可以接受的一种模式大部分公司之间,就是B2B之间,Data sharing的话是通过这个方式来进行share的就是有的是通过API,有API当然好了,就比如说我是一个Uber,是个公司给这个假设给Lift,跟Lift share一些某些Data,那我给Lift一个API那Lift靠这个Uber API就可以拿到Data,这当然是最好的,但是很多时候很多公司之间相互交流的时候没有那么多Engineering,API毕竟authorization, authentication然后这个复杂度还是挺高的,但是你如果做CSV的话就是每个月我download的一遍CSV,每个月我去monstay把这个data,用CSV格式download了下来,然后把data出来发给你,然后你再把它处理一下,然后就可以了。这个其实很常见的,我们insurance,uber的insurance,包括所有的insurance公司都是这么搞的,就是他跟第三方insurance,不管是那个farmers,state farm,还是什么progressive,这种insurance公司,他就是从,我们自己写了一个什么fileserver,然后从这些公司里面把Data以CSV的格式download下来,然后放到我们fileserver上,然后我们很多insurance的accurate,就是那个精算师。还有一些data analyst就把那个CSV处理下来,然后upload到一个Google Excel里面,然后进行一大堆处理,然后运算,然后data validation,然后再把这个Month类的Report的Pause一遍这个其实他们基本上每月花一周时间做这个事情你可以看到这个CSV用的还是挺广泛的除了刚才我们说的一个是Language Encoding,Language就是Native Encoding,CSV和JSON的encoding,还有一个是Banary Encoding这块我们拿了一个Banner Encoding是用了一个Message Pack是一种Banner Encoding的一种方式这块举了一个例子这款像左边的话就是你要encoding的这个Json,还有一个username,有favorite number,还有interest然后我们注意到它encoding的时候,encode从右面这个数,右面这个data,就是binary data注意到这款encoding有一些细节还挺有趣的,它所有的data,比如说username,Username,然后Martin然后这个Ferrith number都是用ASKMA来表示的然后它那个然后它有一些metadata比如说这个object有多少个entread83表示这个object是有三个entread嗯object和三个entray这个8可能是表示object,3可能有三个enteryanyway,反正它就表示object是有三个entery的然后第一个entery是它表示string,然后它lance是8然后第2个就是 然后就是 第1个untray 是吧? 然后它那个K是Stream,然后Lens是8,它Value也是Stream,Lens是6,所以就是UserMartin,然后第2个untray它的K是什么Stream,然后Lens是14然后它value是一个on int是吧onsigned int,然后是16位的,然后是他给了1337,然后第三个的话的key是interest,是个A9是吧,然后他那个value的话是arrays,是两个的array,有两个entery是吧,9表是array二的话表示两个entray然后它是Type还是Stream,A表示TypeStream,然后Lens是B是吧Lens B表示11,16进制的话B表示11,A表示10B是11嘛,然后加上这个K是,就是,就是,就是,Dedreaming和这个hacking是吧然后这个的话实际上看的话,这个incony还是比较明显的,就是先表示多好个object,然后表示有type和lens然后就把它一个一个写过来,这个incondy还是比较明显的。比较简单的,它encoding了以后呢,它这个byte变成66byte,就是Bets减小了,整个数量是比之前的小一点这里面有几个东西我想说一下这个Message Pack和其他的后面几个encoding的不一样第一个不一样是它所有的Field name全部就也encode过来了,比如说usernameFavorite number, Interested,这个是Field name,也Encode过来了这个Fuel Name,如果你 client 端和 server 端 run 是同一个东西的话,你其实可以不 encode比如说第一个column约定书层的就是你client端和server端达层一致第二 column 就是 username 你可以不硬 code 是吧不英课的这个column name然后第二column就是fuel number,那你就不可以不incode这个column内,就没必要传送 [25:00] 我们可以思考一下如果是这个是个request 的非常frequent的endpoint的话,你前面这个全部都是重复的,你这个username,favorite number of interest,你要是 million request per second的话,你每秒钟你这个就duplicate了一个 million time所以这个全部是没必要的,你只要把后面的data传输过就行了。你这个前面这个schema,就是你到底column到底是什么fill name吗?你的FieldType其实没必要传送的,你其实两边client端和server端如果run同样代码的话是没必要去encode这方面的所以就是看了第二个encoding,就是我们Uber之前用过的叫Shift,Shift是有两种encoding的,我们先看它的banner encoding。这个是一个常见的Strift的一个Structure,就是有叫Structure of Person然后它有个username然后有数字数然后有interest,我们看这个就是跟刚才那个是完全一样的,但是它client端和server端,都是swift的这个code,所以它其实generate的时候这个schema是你看端端和色彩端是达成一致的,就是说第一个Field就是UserName,第二个Field就是FieldNumber,然后如果你没有用过Strift或者Protobuf的话,我这块简单的花一分钟解释一下Swift的Protobuf是RPC的一种一种 framework它提供两种东西,high level 说它提供两种,一种是你定义的时候,你定义一个叫IDL的东西,IDL叫哦一定要DataDescriptionLanguage 我忘了idea是什么了反正它提供一种data description language 你只描述你自己的data它是为了 因为RPC 你是为了transfer remote procedure call 你是为了靠remote一个function 靠一个remote function然后你需要把那个parameter 传进去 它可能要把data transfer回来所以你需要在这个shift和protobuf里面,你需要定义的data schema你需要定义Data Schema,然后你用Safe的时候它会帮你generate这个,根据你不同的language, generate一些code你比如说在这块的时候我们可以说这个service叫叫Lookup Person 是吧 你这个Service叫Lookup Person然后用Swift 不管用Swift的Protobuf不管是用thrift和protobuf,你实际上是把这个,你把这个,就是你定义的这个,.proto或者是.srfile把它 compile成,编译成, compile成不同language,比如说你用python的话,它可以compile成python,你用java的话可以compile成generate.java code,你用Going的话可以给你 generate 一大堆Go code但是你的Client端和Server端同时做这件事情也就是说假设我们有个Client有一个Server的话,当你把这个PersonStractorPersonStrike不管在Clend的Server端同时有的话,当你使用Swift的时候,当你使用Swift Compile的时候,它会把他所有支持的语言大概支持五六种语言,可能近十种语言,然后把所有的语言对于每一种语言generate成一个Clend。Client code 就是比如说对于go的话就generate从一个person的点go code generated code,然后你其他的code就可以直接使用它然后把这个这个data就是就是client和server端相互交流我不知道没有说清啊,就是你client端和server端是帮你generate code的,然后generate code之间保证你client的server端是speak same language,这个language就是swift的binary protocol或者swift的那个compact protocol,然后所以你不管你client端用什么code,比如说你client端用的是go,你server端用的是java,那它会把go靠你go generate the script file,然后把你的那个data呢放到这个go,就是把这个data呢放到这个compact,就是把这个data呢放到这个compact,变成一个Binary的Data,然后通过这个Swift Binary Protocol或Swift Compact Protocol然后转到那个server端,然后server端因为你用的是java,所以你decode的时候你decode的时候会把它变成java的一种Java的那种表现,就是Java的那种Data structure,然后读出来,然后用Java的形式使用。然后这个IDL是Interactive Data Language, 我忘了前程是什么了,就是Interactive Data Language。好,然后这边的话,所以这个Binary encoding的时候,其实我们看到一下这个swift其实和刚才那个没有本质的区别。我们看的话就是它唯一的不同就是什么呢?它去掉了这个FieldName,你没有这个FieldName是UserName,还是FieldNumber,还是Interest,但它Encode的时候是一致的它第一个就首先是这个FieldType 比如说是String然后它FieldTag是第几个Field 是吧这块Field1还是Field2然后它是Field1的话就是说是001所以它这个支持Field是大概支持2的4次方 它后面可以extended应该是2^4层次所以他这个还是挺好的 [30:00] 然后它后面还可以extended是吧应该是256个还是挺大的后面还可以extended然后它每个field比如说field1的话它会定义一下它lens是多少然后它这块像这种情况下username.lens.16然后它后面跟上具体的data然后第二个field的话因为它是i64的就是64个的integer然后它给上具体的这个数是什么然后这个第三个field的话它就说的这个是list是吧 type是list然后field type是3然后这个stream,type是stream,然后有多少个list item,是两个list item,然后第一个lens是多少,然后给上daystreaming,第二个lens是7,给上hacking。所以这个里面我们看一下,这个其实它是59个bats,是比刚才66个bats要小的。它省略的,其实大部分省的地方就是这个fuel name,你这个fuel name不管是username,favorite number和interest就省掉了。你增加的地方是type和lens特别它这个lens用起来是非常不高效的你比如说这个lens是6这个lens是6的情况下它这块其实用了几个bit呢用了几个这个应该是几个byte呢用1 2 3 4 5 6 7 8个byte所以这个效率非常低并且它这个一个应该是两个byte因为它是16进制的所以实际上是16个byte这个效率是非常非常低的效率是非常非常低的所以它有进一步优化的空间这是第一点是吧就是它这个比刚才那个好一点的原因是它省略了field name但是它这个明显还是非常低效的因为它这个lance使用的空间太多了它有进一步优化的空间我们注意到这里面我们看到这两个encoding也可以看到有一部分是没办法进一步优化的比如说这个marting比如说这个daydreaming比如说这个hiking这个具体的data是没有办法进一步优化的是吧但是它这个metadata是可以优化的所以这也是它们优化的方向优化这个metadata然后具体的data是没法优化的然后最后一点注意点是你这个data transfer的时候因为你没有transfer这个data field所以怎么样保证client端和server讲的是同一个language呢实际上是根据这个 field tag来的所以这个 field tag是1的时候那我client端假设就是usernameclient端我读啊我是usernameserver端读的也是username是吧所以它是根据user tag来的如果你说我不想换user field了是吧我这个user这个person这个datafield tag是1的时候,我不想是UserName,我想用它是叫Credit Card那你Client端,你发给Server端的时候你Client端以为它是Credit Card,Server端接受的时候以为是UserName,就出现大问题了所以不管是在Shrift,还是在Protobuf里面你这个Field是没法从用的你这个Field是没法从用的假设你这块有个field,你deprecated了,那你也不应该用那你这个field,这个field tag,就是比如说这个r,这个fielit number,觉得以后没有用这个field变成deprecated,那这个field number要放在这里,变成一个tom了,就变成一个坟墓了,没人敢用这个r了因为你如果再用r的话,比如说你在新的version里面把它变成这个不是fielit number了,是变成一个比如说是full number,是吧这两个都是integer,是可以共用的,它之间是没有冲突的但是,你client端以为它是favorite number,但server端以为是full number,这是会出问题的,是吧?特别是你在以后的version里面,你不知道你的client端使用什么version,client端可能几年没有更新过protobuf和sifth的client了,所以它以为是favorite number,但你server端一直assume它是full number,那就出了大问题了,所以在这个protobuf 和 Swift 里面是永远不要用重用 永远不要 reuse tag 的所以你的 tag 是 growing 的如果这个 field deprecated 的话你这个是一直 growing 的问题就是不停在 grow好 我们看到刚才那个叫 Swift Binary Protocol我们现在看一个 compact protocol就是还是一样的 field 还是一样的 field但是我们看到这块进行了几个优化第一个优化就是他把刚才的16个bytes就是二乘以八16个bytes变成了多少呢变成四个bytes变成四个bytes就优化了大概优化了很多是吧就优化了很多刚才16个bytes那个Lens就变成了四个bytes然后其次的话他这个我们可以看一下他这个Field tag是吧他这个Field tag呢就是第一个表示是他第一个Field表示Field tag是1它后面的field是表示和第一个之间的差值因为这个field定义的时候你一般都是连续定义第一个field 第二个数是第三个field所以第一个field是1的话第二个field1表示在上面的基础上加1第三个field1表示在上面的基础上加1所以field变成123然后第二个这个byte data表示什么呢是表示它type 是吧 [35:00] 如果是String的话就表示是80表示String, 6的话表示是int64位的,然后这个9的话表示List,这样的话我们可以看一下就是这个其实省了很多很多的空间,省了大概压缩了四倍嘛,大概感觉。然后这个Lens也是压缩了很多的,像我们刚才看这个Lens就非常,这前一页的这个Lens就非常,为了表示Lens是6,它整个这个数量都比这个6大了是吧,这个Lens的利用率很低,这块的话就直接减制成6,如果它这个Lens比这个6强的话,它会有其他的方式来就是把它,增加这个Lens 是吧,就是其他方式增加这个Lens,所以它压缩的比更高一点是吧。所以之前的话是59个Bet,所以现在压缩了一下,就是Compact Protocol压缩成34个Bet。所以它这个focus就是这个压缩呢就是在datatype上和lance上进行压缩我们看一下DataType和Lens上进行了压缩我们再回顾一下Message Pack是有Full Name的所以它这个Lens比较长是66个byteBinary Protocol是把Full Name取掉了,所以它这个Lens 从66减到59但是它这个metadata占用很多空间,特别这个lens占用很多空间,所以它只有减少了7个byte,减少了fuel name,减少了7个byte。这个里面不仅把FillName取消了,就是这个Compact Protocol,不仅把FillName取消掉了,并且把这个MetaData也取消了很多。这就是额外的减了40%多的这个40%多的这个bites 是吧 就变成34个bites了然后我们看一下ProBuff,ProBuff是JRPC的一个就是用来做这个transfer的protocol 是吧,serialized or deserialized protocol。它实际上也是做跟那个compact protocol是比较相似的。也是做了这个就是也是就是对MetaZeta进行优化,然后没有Fuel Name谢谢大家我们看一下这块我这块简单列了几个OK和NotOK的大家就是思考一下,这个也是一个血和泪的教型啊,就是我觉得NewBuy是最常见的犯的就是你去公司最常见的犯的一个code,就是一行code,然后把整个系统breakdown,这是最常见的,我自己至少见过两三次了,就是这个一行code把fieldbreakdown了好所以为了你保证你这个backward compatible和forward compatible,什么更改是允许的?比如说你改field name是允许的,就是因为它我们想一下这个Protobuf和Strift的进行DataTransfer的时候,它不用Field,它不用Field name,它是根据tag来的,就是你到底是在第几个Field,第一个Field还是第二个Field还是第三个Field,它是根据这个Offset来的,所以你Field name是可以随便更改的。这个是不会出现问题的,但是你不能reuse field,我们之前也讲过了是不能reuse field的,因为reuse field,你不知道你有哪一个client是几年前的版本,你以为是field number,结果人家sign的是fold number,那你整个data就mass up了,所以你是不能reuse field的,因为你不知道你client用的什么version,是吧?然后,change type是也许ok,但一般情况下,大部分情况下是不ok的。比如说你这个type之前是个array 是吧,你要把它变成一个string,多半是不ok的。你之前是个int,你要把它变成string,多半也是不ok的。所以change type的话你要非常非常小心,多半是不ok的。这个问题会很大,就是你比如说你这块定义的client端定义的favorite number,然后server端想的是string,然后你传过去的时候这个就不compatible,很有问题的。然后最后的话是加requireField是非常非常不ok的,这也是最常见的你broker系统的一个方式,就是我们可以思考一下,这个protobuf还是这个,不管是protobuf还是shift,他做的时候就是把这个field就是把这个dataCompress,就是Encode和Decode以后,它会check这个field在不在是吧,你如果是定义required field,它接受这个data的时候,如果你定义required field,如果它那个field不存在,或者那个data没有,那就会zero exception,就是你的invalid request,是吧?就server段会说exception invalid request,或者client段会说exception,我不知道你server段返回什么,反正我这块没有。是吧?如果你有一个,加了一个necessary field,如果你加了一个NancyField,我们可以思考一下,假设你client端是version1,是有两个field都是optionField,你 server 端突然加了一个 necessary field 就是 field 3 叫 necessary field任何client端用的旧的client发给server的request全部是invalid的因为你只有两个field,field 1和field 2 which is optional,当你,当你,但你server don't expect,你有第三个field,是required field没有这个 field 那你过去的时候就会fill 掉那我们反过来想一下 [40:00] 假设你remove了一个field 是吧在你server端 remove了一个field你client端是有field1 field2和field3的其中 field 3 是 required field然后你server端是remove了一个field是吧然后你send a request过去,然后他给你返回数的时候返回数的时候,他最后只有Fu1和Fu2,没有Fu3那你也可以是被rejected 然后或者是你这个另一种情况是什么呢Client的端是有Field1,Field2,和第三个Client端是有Field1Field2的然后呃是是是Field 3是Required Field 是一个最是old version 是吧Server 端也是Field 1,Field 2和Field 3,然后Field 3是Required Field,也是old version,但你看端更新那个新的version,把Field 3给remove掉了,which is required field,那你这样的话算过去的时候,server的话还是不认是吧,因为你缺少一个required field。这个forward compatible和backward compatible中间有很多,你可以把很多这个列举一下,眉举一下,你就会发现整个这个required field就是一个坑。所以现在很多时候这个一个非常好的这个习惯就是不要加required field。除非你确定这个Field加上去以后永远能存在并且你只在第一个version加required field,是吧,你在version 0或version 1的时候加required field,以后就不加required field,这个是比较好的一个practice,就是如果你加required field,你可以在第一个version加以后就不更改它了。不管是增加required field 还是remove required field 都不应该有然后如果你想有一个field变成就是你希望在application level希望有一个field是required的话,那你可以在application level加logic,就是说这个field应该是required的,如果不是required的话,我可以log一下是吧,我可以systemlog一下说这个field应该required的,你这个client有问题,然后呢,然后你再返回一个你sugar过的一个,你专门处理过的这个response。所以这个backward和forward compatibility的点,大家可以,就是有很多文章写的挺不错的,这款我只是一个非常简化版的version,你可以搜一下这个比如说thrift和protobuf backward compatible有很多有很多业界的同僚写的那个总结文章写的非常不错特别是你改SRIFT的时候要非常小心读一遍这个文章大概有个概念然后最基本的雷不要踩是吧不要突然加一个required field这种最基本的雷不要踩是吧reuse field这种雷不要踩是吧这个还是很重要的不过,NewGuy采来也正常,因为这个是最常见的这个一行扣的整个系统到达最常见的空好然后我们就看一下最后一种是Avro encoding,这个Avro encoding和之前的encoding不一样,我们看之前的两种encoding,不管是Protobuf还是Drift,它更多的是为了QPS,就是Transactional Request,是吧?它实际上是,你实际上就是有点像,比如说我们之前讲的这个Transactional和Analytic,就是嗯上周次讲的Transactional Analytics,它是425这个User Traffic所以,用者是有所在的但是Averill这个protocol不一样,Averill这个protocol更多是为了data transfer的在Uber 里面我们也用这个Avro Protocol,在Avro Protocol用的什么呢?是把这个Data从这个Transactional Database然后Pot到这个Analytic Hadoop上,Hadoop File System上,就是用这个File System是吧,它实际上是用一个Uber有一个kafka的一个service叫watch tower,就是你所有的analytic transaction它会同时会把它变成一个就是那个event driven的一个kafka message,然后这个kafka呢然后卡夫卡这个schema是用Avro来定义的然后他是用Avzor来transfer的,他是为了database先transferdata的。我这块具体怎么样compress,encode和decode我就跳过了。因为Avro设计的时候是数据的数据,比如说数据的数据,所以它这个Full name很重要,Full tag就不重要了。这个是不一样的,因为这个database transfer,你fuel name是非常重要的,但是你fuel tag就不是很重要。好,所以这个Avro呢,它优点是什么呢?就是你用来,比如说你slot file有很多records啊,然后或者是你Send records over network connection,或者是你这个database dynamic generate schema,比如说你这个不管是sift还是put a buffer,它是费了transactional needs需求的,所以它没有这个needs说我这块有个database,那块有个database,我把这个database从这个database传到那个database是没有的但是这个Avro是这样定义的它会dynamically根据你的database的generate schema然后dump你的那个data然后在另一个schema [45:00] 接受就是Avril设计的这个这个优点是吧那我们看到之前这个Binary Coding和这个CSVJSON还有更早的这个Language Encoding有什么区别呢其实它的区别是这样子,首先它更Compact我们可以看到这个Protobuf不管是Protobuf的这个encoding还是Swift的Compact encoding它可以使这个data使用率增加一倍,进一倍是吧?并且你这个metadata越多你的使用率就越高我们这个data使用率是非常高的这个这个特别是可以减少 emit field name假设你这个request是一个 million request per second,如果你不用这个的话,你用的是比如说是这个JSON来传的话,那你这个Field可能要重复一个million time 是吧,你说每个request里面都要fill name,但是用这个swift或者protobuf的话,你这fill name就省略了,每秒就省略了一个million time的fill name的传输。其次的话这个schema也是非常valuable的,你不管是protobuf还是swift,我们可以看一下你所有的这个protobuf和swift,你其实都是作为code来check-in的你作为Code来Check-in,就可以用到Git了,GitHub这种CodeVersion Control System,是吧,CodeVersion Control Management System,就是不管是这个Git还是这个SVN是吧,它实际上就把你的所有的Version全部列出来,这样的话你就可以看到你这个Protobuf这个Schema是怎么样involved的了,是之前比如说两年前什么Version,然后几个月前是什么version,现在是什么version是吧?你说的version其实都全部index了,就是通过这个GitHub,还有这个当你check in code的时候就相当于帮你保存了。你想lookup旧的version,你可以看Git的这个history,你可以看Gitlog就知道它version是怎么样演变的。但是如果是比如说你用这个CSV或JSON,那你这个是完全没有的是吧,包括你用Language的这个,Language的这个encoding的话更没有了,因为Language encoding根本没有考虑version的问题,所以你用Protobuf和Swift,因为它是code的一部分,你在写code,你写Protobuf,写Swift,写code的一部分,所以它相当于是用Github就帮你version过了。好其次的话,因为你这个schema是相对于你有一个database, backward forward,你可以check的是吧?所以在你任何deploy,比如说你server端想把这个version从V1 bump到V2,那你可以看,根据把说的historical client version看一下,这个V2会不会有breaking effort 是吧?会不会你deploy V2了以后,之前有个版本的V1,这个Client就没法抗你这个V2的server了,因为你有说过去的history,所以这个schema evolvability会更好一点。然后最后一点的话就是你Strong Type的就是Statistical Type的Language,比如说Go,比如说Java,你Generate Code的这个Portograph和Swift的Generate Code是可以帮你发现一些type mismatch,在Compile的时候就可以发现一些type checking,做一些type checking的。如果大家写过Compiler的话,Type Checking和Type Reference是非常重要的一部分。啊一般来说,稍微有一点经验的公司或大公司都会用一些strong type language,这也是为什么Facebook花了那么长时间,把那个PHP改成了它那个非常特殊的一个PHP,Facebook的PHP是Tapped PHP,因为PHP本身是没法没有Tapped的,它会造成很多这种非常trivial的Error,然后就没有被人发现,然后就造成outage了。所以Facebook花了很长时间写了一个HHVMApplicationDeveloper以为他写的是一个PHP,但是实际上他会把这PHP这个type的php 转换成C++ code,然后在productions上跑到C++ code,他专门写了一个php,就是typephp.c++的一个compiler也是因为这个type code其实会减少很多这个 unnecessary headache unnecessary outage好,encoding这块和decoding这块就讲完了,大家有什么问题吗?有问题的话回复一下我觉得可以直接问如果有问题的话可以直接提问谢谢大家谢谢大家谢谢大家谢谢大家谢谢大家没有话我们就到下一个小章节啊,就是这个Data Flow,Data Flow的不同方式。我们讲过之前 [50:00] 我们所有的这个system design都是一个互联网环境下的 是吧?不是你不是design一个比如说autonomous driving自动驾驶这个其实不是一个互联网的环境,因为它这个数据量太大了,autonomous driving你要是有一个128LiDAR每秒钟的数据量高达几百个Mbps180MB/s 你这个是没法用互联网来搞定的 并且就autonomous driving 任何通过互联网 我们之前讲那个在第一次讲的互联网的DarkSizeTruth, Knowledge and the Lives in the Internet 的时候,就是讲这个这个SharonArcy Architecture 的时候讲过。这个Network 它的 performance 是非常不稀疏的。你很多时候一个request之前可能要花5毫秒就解决了,但是你下一次的话,因为多个用户同时在使用,本来是5毫秒的request,你突然就变成了两分钟都没有回应,一两分钟都没有回应。所以你autonomous driving,如果是几秒都没有回应,可能就出车祸了。所以它那个环境下,它是不能用这个,就是不能用这个,就是它不是一个互联网公司,它不能用这种server端的这个来处理。但是我们大部分就是现在的比较热门的工作都是互联网公司,他这个其实面对的问题不要,所以都是用share nothing architecture。然后这方面我也想给大家一个找工作的建议,如果你找工作的话,我觉得第一点就是你先看他是不是一个server,就是互联网公司。Client Server不是那种非常传统的那种像特别早的什么比如说这个Adobe啊,特别早的这个Microsoft words啊,是吧,不要这种工资,它这个迭代数据很慢,学到东西也很少。互联网公司的话它讲就是fast situation,它整个这个工作模式和这个工作环境和这个都是不一样的,因为我两个公司都工作过,我之前在那个EA做过那个游戏,不管是开发模式什么都不一样,然后所以找互联网公司就是这个就是它不仅是给的钱多啊,它更多的是对你职业发贷有好处,其次的话,如果你是在一个互联网公司工作的话,它基本上都是share nothing architecture所以它就是我们之前讲的说的design是围绕这个Sharnazi architecture来的所以这个share nursing architecture 里面几种 data flow 的话一般是有三种,一种是 data flow 通过 database 来share第二种是通过 API 来share 的, 第三种的话是通过 message 来share 的, 所以我们一个一个看。从 database 来share 的话, 首先这是一个错的模式, 这是一个非常不 work 的模式。为什么不work呢?就是你现在是用database来扩你这个database的时候,访问的时候可能是来自于两个service 是吧?可能是来自于Service也可能是来自于service1,也可能是来自于service2,同时对database进行修改那你这个service 两个service 之间你可能中间的 business logic 处理是不一样的比如Service 1的时候,它可能会通过Logic Business Code 1来处理,然后它可能是这个ORM可能是用的ORM Framework 1是吧,然后把它写进去那你另一个Service 2可能用了不同的Business logic然后用了不同的ORM写进去,你写进去是同一个database,同一个table,但你读出来的时候,你不一定compatible,就是你series2读series1的data是不一定compatible的,因为它可能出现了一个,就是它可能用的logic不一样,是吧,它可能是一种,就是你assume 的assumption 不一样,是吧也可能他处理的不一样是不compatible的是吧即使你现在compatible以后也不一定compatible是吧你比如说servicee说我现在是在这个payment里面写一个新的roll,我要去payment里面写一个新的roll,我为了保证我这个数据更好一点,我会把payment里面这个credit card进行处理一下,把每四位poss一下,就是四位加一个dash,四位加一个dash这种举个例子。但是你service 2可能没有这么做,所以你service 2读的时候,你四位加一个dash,这个data读出来就非常crofted了。所以这样的话,这个DataFlow通过Database是非常run的方式,那因为你这个,首先你有可能有不同的version在写入,其次,你可能有不同的Service,首先你可能有不同的Service在写入同一个Database里面,其次不同的Service有不同的Version,这就变得非常不maintainable然后再其次,你写的时候和读的时候不一定是同一个service,也不一定是同一个version这个就整个乱层一船了就完全没法用了好好,然后这块的话就给了一个例子,这块给了一个例子就是这个还是,就是这个是很常见的一个例子,就这也不完全是跟这个database [55:00] 就是Data Pass from Database相关的比如说这个例子里面在你这个写的时候你是用了一个新的version写的这是一个link in 的例子不一定是linky嘛 就是一个profile例子你写的时候是用一个新的version code写的所以你有username, favorite number, interest,还有个photo url 是吧也是四个field然后你写进去这个database的时候当你做这个ORM,Object Relationship Mapping的时候,就是Encode和Decode的时候你就把它变成了什么,就是ORM变成了什么,就变成了这个这个old version 因为它是old version 所以它没有photo.uil所以他要把photo url discarded了是吧,我们看一下这个,这个code就是在你写code的时候,它是old version code,Client是新的version你的server端是old version,所以拿到这个时候就把photo url discarded你写进database的时候,这个photo.us就彻底丢失了这就是Data Loss了,看一下就是你写的时候就是你data written的时候是new version code有一个photo url你就是你把它decode的时候,read decode的时候,你code是old version你是不认识这个photo URL的,是Java code的,然后你写进database的时候,这个photo URL就彻底丢失了,所以这样就落死我data了。好,这个是可以通过carefully coding能解决的,这个是可以仔细的coding解决的,但是这个是,所以说在Coding比较在Syrnaz新architecture,coding比较难的部分就是你想一下,每次都要想一下你的新的version和旧的version是否compatible的。如果你这个Service和Service ABCD都有interaction的话,那你当前的Service新的Version和旧的ABCD是不是也是compatible的?就是都需要想一遍的,所以如果Micro Service有一点不好的地方就是如果你这个Service跟20个Service做interaction,你做update的时候,你需要想一下你的新的version是否和旧的这20个Service全部 compatible,是不是全部 backward compatible这是非常重要的一点谢谢大家并且 service 比如说你这个 service A B C D E他的Client的version,他可能用一个非常旧的Client version,那如果你这个Compatible不只是跟上一个版本Compatible,你可能要跟非常旧的版本也要Compatible这个就非常非常复杂了,所以这个你可能到时候跟另一个组是吧,比如说Service ABC是用的是特别旧的version,你还得跟它单独聊说,你必须把这个你的那个version bump到20以后,否则的话我们这个data就crafted了,你首先得check in,说保证它这个version bump到20以后,然后你才能把你的version再check in,是吧,这个rollout是非常非常复杂的。业界上关于这个miserable story非常非常多的,这个Misterable Story,这个version,upgradeMisterable Story非常非常多的我记得我去年失业的时候参加了一个Kafka Conference它这个 Kafka,这个topic,不是有version吗?它这个 Kafka Conference就是他这个version也是讲了很多这种非常miserable的story,就是这个version不compatible,然后deploy 出问题了然后我前两天读了一件Hacker News的文章,就是卡夫卡是把root paper 给remove掉了Kafka之前是在用Rootkeeper 来管它那个replication 和这个Partition, 我接下来会讲replication, partition, 这一块, 卡夫卡用这个rootkeeper 来管它replication和partition的, 它最新的版本是它起了一个非常酷的名字叫, 卡夫卡need a nodekeeperCafcar要把这些Rulekeeper给remove掉,dependency remove掉,然后它那个version update很有趣,就是它有一个过渡的version,就是说如果你用Cafcar过渡version的话,那你这个它同时支持有Rulekeeper和没有Rulekeeper的如果你想 update 到最新的没有 rulekeeper 的 version,他推荐你先 update 到其中, upgrade 到其中一个过渡的 version 上。就是在intermedium,interim version上,然后呢,通过设计一个configuration通过设置这个configuration,把rootkeeper的dependency取消掉,那你就用没有rootkeeper的过渡vership然后如果没有问题的话,你再可以bump到最新的version,最新的version是完全没有rootkeeper,就不支持有rootkeeper的version了。它是通过这个方式来管这个version的,它花了很久的篇幅就介绍一下怎么样怎么upgrade,这也可以看到这个业界里面整个version,不仅是client server 端的version,不仅是swift 之间的version,不仅是data 之间的version,你 software upgrade 的时候,你这个 library update 的时候,version也是非常头疼的一件事情,有很多 miserable story [60:00] 好最后的话就是我看应该没有几个slides了就是Data Outview Code,这个我之前也讲过一遍了,就是你Code可能总体来说就几个version是吧,如果你这个可能server 端一般就一到两个version,client端如果你没有要求你的client upgrade version的话,可能要多个version比较复杂一点,client端毕竟不是你直接用的,很多时候不是你直接用的一个service,所以你的client version相对多一点。但是你的data不是这样子, data是从你这个server general available以后,所有的data都在里面,所以你code可能就有一年旧的version,两年旧的version,或者是两个月的更多是很新的。但你data的话经常有五年旧的data,十年旧的data,很常见的一件事情。所以这个data呢是Otlio扣着的,我再调调一下这个你包括跟System Design的时候就说你始终focus是focus on data这部分的System Design里面永远是Data是大于一切的,Data是永远比Code重要的,知道吧?就是特别你很多时候System Design的时候,如果你突然有个Algorithm想不起来,无所谓,就说有一个Algorithm能处理,能记住名字就可以了或者是你有一个具体的方式,有一个具体的小的technic,比如GioHash这种小的想着技巧忘了就忘了无所谓但是你这个data怎么处理,这个data是怎么样flow过去的这个是不能出问题的System Design里面具体一个小technic忘了无所谓的没有人car的你到底是用MD5还是用sharewarehash这两个之间有什么区别没有人car知道最好,但是不知道无所谓,但是你这个data怎么处理,这个data这个data怎么handle,这个data是怎么flow过去的这个是最重要的这个是需要很多经验积累,然后也需要一些理解,然后读一些paper,读一些article能理解更深的。好,所以说这个这块的话就是这块讲的XPrescial也是这个LinkedIn的这个Database,XPrescial这个LinkedIn的database 我看起到哪儿了对谢谢大家好,如果你是用MySQL,我们之前讲的MySQL或者SQL的话,你是Schema on write,所以你的整个table schema是一致的,但你如果用的不是这个SQL,是用的NoSQL,比如说Mongo的话,你其实同一个table里面有多个schema的存在。你有多个schema的存在当你take a snapshot的时候,比如说你要这个database take a snapshot,防止有人丢裤跑路这种是吧,你要take a snapshot,防止有人丢裤跑路,你要take a snapshot,然后那你的话,你其实如果是用mongo的话,你其实是你这个data可能是过去五年data都有,然后各种version都有,这种情况下的话,你dump的时候,其实你要把这个dump呢,用最新的scheme啊,所以你take a snapshot database,take a snapshot data,和这个database自己原生态存的,就是native存的data可能不是同一个data你这个dump的data可能是经过这个latest schema encoded过的,所以不是同一个data。所以这个每次database dump出来可能version都不一样,多少有点不一样。但是它的存在这个mongo里面这个data,就以document database的作举例,在mongo里面的original data可能是从来没有变过的,但它每次dump的时候会根据最近的schema来dump一遍。这个data两个data还是非常compatible的好,就是这一点。然后之前讲过不是讲了,有人问了这个LinkedIn用什么存Data,LinkedIn是用Expatial存Data,这个Expatial是在MySQL上实践的,有点像Document Database,但是它在MySQL上实践的,它Dump的时候也是遵循这个,就是你实际上有很多种不同version data,所以你Dump的时候也要用最新的schema进行处理,所以处理出来也是不一样的。就是之前讲的第一个就是database是通过 data flows through database这是第一个然后这是不对的,所有DataFlow through database,这个都是有问题的,都会出现各种各样的问题。然后第二个的话,DataFlow可以through REST API或 RPC,这个是比较常见的一个,就REST API和RPC,这也是这个说的Internet互联网公司最常见的一种形式。RPC不管是REST 还有RPC都有Client和Server你Client可能是webbrowser,你Server可能是一个webserver,很多时候如果是RPC,internal RPC,你Client也可能是一个webserver,你Server还是一个server。同时呢,你这个你这个看着 [65:00] 和server是相互的相对的你一个service可能同时是client的同时也是server就是你一个service比如说你这个Uber Trip Service你是user的server但你有可能是payment的client你会request payment做些事情user request你做些事情然后其次的话这个service呢你可能是server request也有可能是为了transfer data你可能是为了transfer data 是吧然后对,然后那天有人问了这个SOA就是service-oriented architecture和这个micro-service architecture有什么区别其实它的区别不大这个micro-service architecture其实是被redefined了rebranded了,它其实和这个十几年前就是Java比较火的时候那个service-oriented architectureREST的Java比较火的时候比较火的时候,那个service-oriented architecture是同一个东西其实这个核心部分是差距不大的但是我们谈的时候,你要谈micro service architecture,感觉是感觉更时髦一点,service-oriented architecture,感觉是java,rest API,那种比较十几年前比较老的东西,就是2B2b的那种东西但其实本质上是没有区别的本质上实际上是没有很大区别然后最后一点的话就是不管你是用micro service还是service-oriented architecture你实际上都是为了decouple你是为了把client和service之间decouple出来那你这样的话你client和server可以独立的进行deploy和evolve比如说你这个trip service和payment service你就可以有不同的组去维护然后他们之间可以相互的进行更新你trip更新的时候你不需要depend on payment更新你payment更新的时候不需要trip更新你们之间是通过API定义好的那你API定义好了以后你在里面怎么搞都可以你不管是把这个重新全部架构一边把node.js改成go是吧或者把java改成c++你可以重新架构一点只要你这个API没有变你这个agreement没有变你可以随便搞是吧然后这样的话这个系统总体来说是更稳定一点因为毕竟我们会发现这个code里面System Design里面很多部分都是怎么样就是让几个组之间更好协作的因为你毕竟这个code不是从天降的也不不是凭空生存的,你毕竟是用人来维护的。而这个系统设计中,系统维护中,DevOps里面最大的Error就来自于HumanError,所以这很多系统设计约定俗成的这种,不管是Micro Service Architecture, Service Oriented Architecture,它其实是为了更好的Coordinate Developer,更好的就是产生一种Againment,像它这种架构,它这种agreement的出错率是最低的好,这块我简单列了个REST API这个我列了一下这个REST API因为我觉得可能有的同学对REST API这个理解不够深入我列了个REST API,我今天可能就跳过了,没有时间讲就是我推荐你去,就是推荐大家如果你没有懂什么是REST API的话应该去读一下什么是REST API这个是非常fundamental的东西,不复杂不复杂,它的核心点就是一个uniform interface它是把所有的东西都是resource你通过action来get resource,或通过action来进行对这个resource进行create, create, update的操作和delete的操作这个REST API不复杂这块的话,我想额外说两点,REST API是没有对versioning进行考量的我之前讲这个你schema evolve 的时候schema evolve 的时候REST API是没有对Version进行更改的那像Salesforce用REST APISalesforce怎么样管理REST API呢Salesforce是直接把Version是放到那个UIL里面的就是你比如说这个就他比如说这个Resource假设这个Resource叫User他使用的时候会叫Version1或者user version 1然后user version 2它实际上把这个 version是直接放到这个 URL 里面所以你这个它并且Southforce是只 maintain过去的4到5个 version3到4个 version就是它只 maintain过去一年的 version如果你要你要是那个什么你用的 versionoutdated的话它会通知你这个 version要 sunset了你要更新成更新的 version所以这个Southforce是直接把这个 version放到 REST API 里面的Wrest API URL 里面还有一些2B的公司呢它是每个Client 它专门自己存一个version有的2B公司这么做比如说比如说这个假设有存在一个叫System Design Service的公司假设我成立一个叫System Design Service的公司我有Uber作为我的客户我有Facebook作为我的客户 [70:00] 我的Salesforce作为客户是吧我自己internally存一个version,所以Uber可能用我的第15个version,version 15,是吧,Susco可能用我的version 1,是吧,Lift可能用version 13,是吧,当我去,当我说我需要你upgrade version的时候,我是专门找你谈,就是我专门找Uber说,啊,这个我需要upgrade version了, version upgrade 以后降降的好处是吧update 以后这样这样的好处那你是不是花点时间把它update成最新的version当你准备好说我这个version update 好了那我就把那version bump一下 是吧bump 从1 bump到15这就是非常 tedious 的 management这是为什么我们说这个传统的REST API可能相比于最新的RPC version和这个schema involvement 的不好的一点,它没有version support。实际上如果我们看那个REST的话, REST其实是一个博士的毕业论文,是一个博士的 thesis,他实际上写REST的原因,他是提供一个是一个philosophy,是一个价值观,是一个philosophy,它不是说是一个standard,Rex是一个philosophy他写了六条 philosophy,只不过这个 philosophy 定义的很好,别人用的很多。相同例子很多,我们会讲那个 CAP 的时候。CAP的时候也会讲,CAP也是一个非常就是经常感觉 new guys 会说CAP,CAP也是一个 philosophy它也是一个帮助你做的东西,它其实不是一个很好的一个protocol,也不是一个很好的一个technical的一个东西,它是一个philosophy。但是unfortunately,所有的这个REST,CAP都比较火一点,但是它其实更多是一种philosophy。和REST相对应的是一个叫SOAP的东西,它这个,Design 是不一样的,SOAP是定义是Action更多一点,但是这个SOAP基本上只有一些传统2B公司在用的,所以你不知道无所谓的,SOAP不知道无所谓的,但是REST要理解,这个REST的Philosophy是怎么定义的,你要找几个例子来看,这个不太复杂,大家可以自己花点时间就可以搞懂了。好,我们讲这个Data Flows 入RPC 或Data Flows 入API,那它这个 Flows 是什么呢?有很多floor,这个RPC和API是有fundamentally flawed的但是它是过不去的卡吧,就有很多flow。我们一项项读一读,这块课想多花点时间,让大家理解RPC和API的flow的部分。当你使用RPC和API的时候,它就不像Local Function了Local function是非常predictable你最多有就是你的CPU有些context-switch的话,可能当前这个执行的command会被其他的这个process 被占掉了,但是总体来说是非常predictable你要不就 success,你要不就 feel.但你用 REST API 的时候,它是不是很predictable,可能这个request loss,就是Network出发前,这个request loss,有可能这个response loss所以很多时候当你用RPC的时候,你必须要去Timeout因为你这个request可能permanent loss 是吧?你这个request permanent loss,quare response permanent loss,所以你必须timeout所以这个当你用这个REST API或者是这个RPC的时候,我们统一叫RPC好了,当你用RPC是Remote Procedure Cloud, API Cloud的时候,你一定要handle一个就是timeout 你要处理timeout的问题这是一点是吧谢谢大家其次的话,如果你timeout的话,我们看第三个步骤,如果你timeout的话你其实是没法区分 你从看着端你是没法区分到底这个request成功了没有你可能是在request的时候timeout了,request的时候丢失了,你有可能是整个人SA server 端处理完response的时候丢失了所以当你timeout的时候你必须得retry,否则你这个request就没有go through 嘛。那你retry的话,那你server端有可能会接到同一个request多次。那这时候你所有的Rest API,所有的RPC设计,所有的API设计,一般都是要idempotent的。idempotent的意思就是你同一个request,request多次没有side effect你比如说我举个例子比如说你是个payment你payment比如说你server client 端说我手机端说我要对这个比如说你听我课就讲得很好,要打嗓是吧?那你打嗓的时候,你突然timeout了,所以你不知道你到底有pay没有pay我钱是吧?那你可能,因为你不知道pay没有pay我钱,可能再重复一下这个action当你重复这个action,假设第一次其实已经勾诉只是return response的时候timeout的话,那你第二次进行pay的时候,这个pay应该变成一个no-op,不做任何事情那你这个就对你邀请 [75:00] 你设计的时候你这个service就是ad important还在 important是一种的一种的一种的一种谢谢大家所以它会增加你这个设计逻辑的复杂度这也是我觉得我第一次到互联网工作公司工作的这个感觉就是你这个因为你有timeout,然后各种corner case处理,你会感觉这个程序员永远不会丢饭碗的,因为这个,我感觉我刚毕业的时候还会挺担心会丢饭碗,但是后来去互联网公司工作一下就发现这个corner case处理太多了,其实是一个非常简单的API call包括你怎么样做什么load balancing 是吧怎么样charge request 你怎么样处理这个timeout 啊怎么样处理各种counter case exception你会发现程序医师永远不会丢饭碗的?好然后其次的话就是当你靠local function的时候,你的这个execution time是predictable的,但是你靠network call的时候,你这个execution time可能从一毫秒到几秒之间变化非常大,depends on network,所以这个variable是比较大的。谢谢大家你这个就完全不predictable是吧然后最后的话是当你靠local function的时候你其实是一个pinter到local memory 你不需要把那个data你不需要把那个Data Structure全部copy一下,但是你做Network Request的时候,你必须做Encoding和Decoding,你必须把这个Data变成,就是Encoding成一个,不管是Binary还是一个TextStream,然后把它Transfer到另一个Service,然后从那个Service处理完的时候,也必须Encoding过去,然后传输给你,然后在Decode以后才能进行处理。所以你其实花了很多时间做encoding和decoding的就是RPCs里面最后一点是这个client server可能是在不同language 是吧?你比如说你client可以加完server可以是go,但是这个不同language之间,你比如说你有client支持10个language,server支持10个language,那你不同language的interaction是nsquared的。就可能有不同100个language之间相互 interact 那你中间会有各种各样复杂的问题各种各样复杂的问题啊,这个问题很多时候是这个fundamentally很难被解决的。好,最后一点就是我们讲过三种Data Flow的模式,一种是Data Flow,是Database是划签掉的,就是任何时候Data Flow都不应该通过Database来进行Transpass,然后一种是Data Flow通过REST API通过API或者通过RPC,这个是可以接受的,虽然它有各样的unpredictable,各样的问题,但是它是可以接受的。最后一种就是Data Flow通过Message,这款Message是Message Queue或者是Kafka,就是Kafka或者Message Queue都可以啊。它的好处是什么呢?首先是decouple,我们之前讨过了好多次,当你加一个message queue,或者当你加一个worker pool的时候,你就fundamentally natively decouple了你就是把这两个问题就分开了,separate concern,decouple这两个client server了然后你可以就是asynchronize处理是吧,asynchronize处理的话,你这个所有端比如说worker少的时候,你可以多加点worker,worker多的时候可以减少点worker是吧总体来说这两个问题就从逻辑上就分开了。你maintain的时候和那个debug的时候更好debug是吧?然后所以说这个message passing data flow 首先就是比如说你server突然down掉了,message也可以在那里等你serverup的时候可以这个处理,因为是Asynchronized处理的,然后你Server也可以就是Asynchronized skill 是吧,这是第一点,非常好的一点,然后 decouple 也是非常好的一点,然后其次的话,你如果这个Servor单挂掉的话,你Servor单也可以等等重启了以后接着处理Message,是吧?但是如果是RPC或者API的话,你Servor单挂掉,你直接就unavailable了。是吧,就是你如果收入单挂掉RPC,不管是RPC API,你收入单挂掉你的request直接on available,但是在这个message里面,你这个client request的中指就是产生了一个message你server有空的时候,server有空,worker有空的时候,处理的message就可以处理,所以你不是unavailable,是吧?你不会说是这个就挂掉了然后其次的话还有一个其次的就是你这个Client团不需要知道具体是哪一个server处理你的,你不需要知道你server的具体信息,你只要有一个worker在处理你就行了,有一个server在处理就行了,处理有块钱,你不需要知道这个server的IP address和这个paw number其次的话还有一点就是你这个Client端同一个message可以sign了多个server是吧这个常见的就是我记得在Design YouTube里面就是把user activity变成一个就是 [80:00] message passing data flow. So, you watch YouTube videos, Watch YouTube video and then it's gone Then the activity, user activity, can become a message. Then you can have several services to make this message. There is a service that can log in. Then you can log in. You can log in. This video will watch. There is a service called user history. User activity. And another service is machine learning. According to user activity, it should be recommended to you do what? For you to do some videos, machine learning. UseActivity will help you recommend, you may like watch videos, right? There are several services to handle. So we use a message to do a service and handle it. And then another one, You request a trip, Uber request a trip, you can do payment. That is also a request for more services to handle. How? And the last thing is the message passing communication and RPC. It's different. The message passing is asynchronous. sender is send and forget, sender sender is forgot, and you need to fix this worker after you need to call sender, it is asynchronous to fix it. But the request is usually synchronized. You hope that when you have server-side and you will have to fix your request. That's all for today's presentation. Do you have any questions? uh 还是 I have a question. "I have a real question in the work of the work. Shift is called "Create, throw, accept" Protobufs no exception is no other language. I will tell you how to do this. Protobufs can also be an exception. And then, the protocol buff is how to fix the system. Cocoa Buff for Exception? Cocoa Buff is a shared thing, it's not an exception. No, it should be a solution. What kind of exception is it? I was thinking about the problem. - I don't know how to use the message. What is the message? I forgot the detail. I still use the Prodweb. I used it with Swift, but I didn't use it with Perlva. um That's right. That's not a problem. It's a problem. It's a problem. them. It's not a protobuf. There is a lot of protobufs that I don't want to do this exception. Because it might not be able to do this exception. Thank you. Your exception is not wrapped in protobuf, it is always the end of the process. It is not a protobuf. I have a problem. I have done this. I have a problem. I have done this. I have a problem. You can set the arrow to a protobuf. You can see the error as a prototype field. This is okay. Thank you. This is probably a kind of a common method, just to put a field in a field. I'll just look at it. And it says REST and RRPC are the same. Rest is a philosophy REST is a way through HTTP cloud, and efficiency is lower. It is through HTTP transfer. RPC is through Binary encoding or decoding. So it can reduce the transfer of the transfer. transfer of the data. efficiency, so it's the effect. And this is the first thing. Efficiency RPC is more than rest. It's a lot. It's a lot. It's a 30% percent. I forgot to say it. and also according to your data pattern. And the second thing is RPC, the usual way of using the RPC. If you have API, you can support RPC. Because it is remote procedure com, remote function com. You actually do it when you do it. You actually have to do this RPC. The design of the system is more like the Car function. The heavy carrying function is where to operate. You just know that it is in a server. [85:00] 你不看它到底是本地执行的还是在remote执行的但实际的生活中我们现在讲了RPCN很多 limitation,我们讲了RTCN很多 limitation你实际上还是要知道这个RPC在哪执行呢?就是你作为一个合格的系统工程师,你要知道RPC在哪执行呢?但是它设计理念就是,你就知道它在一个node上执行就可以了,不像在哪个node执行。然后其次的话,另一点的话是,REST的话一般都是就是你set一个REST这个server的话就是他有一套自己的 philosophy 然后它是不帮你generate code的 是吧,你这个像RPC的话,RPC framework,不管是这个,不管是这个thrift,protobuf,它其实额外更Generate code,你实际上有code generation layer所以你使用RPC的手机像靠一个remote function一样这个还是有点不一样的但是其实主要的区别还是Efficient的区别,就是这两个区别的主要区别是Efficient的区别。我觉得其他区别都是相对来说就是就是这个 API 区别啊,包括使用的 best practice 区别啊,这都是次项区别,主要区别还是这个 efficient 区别现在的话现有的话一般说对public的都是REST API对public的REST API,然后internal的,还有不同的cooperation之间的,或者是不同的这个API 的这种有RPC 比较多,比如说你设计一个 service 你说对外的对于 public 的 REST API 比较多一点,然后你 internal 的可能是 RPC 多一点谢谢大家是这样子的,因为它这样Internal RPC效率更高,然后这个另一方面我不知道这个RPC是不是这个Security问题可能有一些,你RPC的话是不是你这个Authentication Authorization这一块,Security是不是可能有一些额外的问题?但是一般来说你写RPC因为Remote Function Call,你可能就不验证Color,就是你已经验证过Color了。然后这块说这个 REST is implementation or definition of API pushup,Rest是个Philosophy就是这个 philosophy 我举个例子就是说谢谢大家就像NASI architecture是一个philosophy 是一个指导性的一个东西它没有任何实际的,就是没有任何实际的这个 implementation,REST是个Philosophy 就是Philosophy嘛哲学 哲学一个定义 philosophy, principle, theory, rest is philosophy, it's not that它不是那个implementation,它没有任何实际的指导。啊但是API是Application Programming Interface,这是非常广阔的一个问题,它是更多的时候是这两个就是完全是两个Abstract Context不一样的然后GraphQL,GraphQL是什么呢?GraphQL如果读一下的话,它其实是对REST的一种优化了。我记得我参加过一个RBNB的一个关于GraphQL的一个Seminar,两年前参加过一个Seminar。GraphQL是一个非常special的一个optimization,你如果看GraphQL的话,它的design其实也写的挺有趣的,它是一个非常lightweight,它不做authentication,也不做authorization,它针对的case是什么呢?它针对的case就比如说不管是Uber还是Airbnb这种,你一个user里面有太多东西了你比如说你这个user里面,你可能是一个user,比如说user,我这个user,我可能既是driver也可能是 Rider 是吧?那我是 Driver 里面,我可能有 Drive Record, Driving Record, 有一些 Last Online Time, 是吧? 我作为 Driver 这个 role 里面。我作为 Rider 里面有可能有各种各样的 field 是吧?你现在这个就是传统的互联网公司, 你一个 Entity 里面, 为了更好的去设计,Entity 很大, 比如说 User Entity 里面。可能有几百个field有几十个大的Field,然后几十个大Field有上千个小Field当你进行REST API Code的时候你就不能简单的通过getUser来读了你比如说GateUser你不可能把这1000个Fu的全部读出来吧所以这时候GraphQL就说我到底是根据什么request,就是你getUser的时候我到底根据什么request的来读我只要读其中的几个field就可以了,选择性的读几个field所以它是基于这个philosophy进行设计的这个也是也是这个也是新的一种设计方式吧大家可以看一下这个GraphQL,这也是非常活动的 [90:00] a very火力 It's a very hot one. It's like next version of REST API. It's a bit more of a new framework. And then the book of Design Data Intensive Application is a lot more than the way to explain the RPC problem. I feel like I'm not a good idea of RPC. And these questions are no better alternative. I think that RPC is no better alternative. This problem is definitely going to exist. That you do the same as the autonomous driving. The server and request are local. This problem is very small. This problem will be in 5G. After 5G, it will be solved by the solution. Because 5G's length is not very low. If 5G's length is very low, it can be connected to autonomous driving and 5G. Then you can use 5G remote driving to autonomous driving. This is a technology that can be solved. I think RPC has a lot of problems. The problem is unpredictable. And visible resources. You say data should be encoding and decoding. RESTOR RESULTS UNPREDICTABLE, and you have to do the server and add important. If you have to do the same thing, you can do the same thing. So you have to do the same thing. There is this problem, but the RPCI as a future, it can be used to through other ways to solve. I think we can go back to the PCIe. You have to do the software with a disk. This is very difficult. So if you have a better alternative, RPCI as a future. Okay, I think there's no other problem. Thank you. Well, today is here. Thank you. Thank you. Thank you.