@Lenciel

分布式系统介绍

目录

基础设施团队在招聘工程师的 JD 上一般会有下面几项:

  • 对于分布式系统关键特征和术语能基本掌握
  • 对于分布式领域常见的算法和理论有基本认知
  • 对于分布式系统在生产环境的常见问题有简单了解

那怎么看一个人是不是具备了这方面的能力呢?本座面试时总会从「什么是分布式系统」问起,大多数时候还是会听到各种稀奇古怪的答案,甚至屋子里一下子就安静了。

最怕世界突然安静。

年后又招人了,并且要开始做内部培训。这里梳理一下分布式系统的一些基础知识。参考了aphyr的基础培训课程和 Dan Creswell 的分布式系统必读指南,非常基础,非常必读,no rocket science。

Let’s go。

1.0 什么是「分布式系统」?

Lamport, 1987:

A distributed system is one in which the failure of a computer you didn’t even know existed can render your own computer unusable.

  • 大多数物理服务器都运行着 *nix 系统,通过 TCP 或者 UDP 进行进程间的通信:
    • 可以是云上的虚拟机
    • 也可以是通过InfiniBand通信的系统
    • 可以是局域网里面,隔着几米的距离
    • 也可以是广域网,隔着几千公里距离
  • 大多数移动 App 也是一个分布式系统里的一部分
    • 移动 App 工作的网络环境比服务器端更加恶劣
    • 其他桌面系统的浏览器以此类推的,也属于分布式系统
    • 所以并不是服务器才属于分布式系统,大部分客户端也是
  • 更一般地来说,分布式系统涵盖:
    • 由相互之间需要通信的部件构成的
    • 通信速度「慢」
    • 通信还常常「不稳定」
    • 的任何系统
  • 比如:
    • 飞机上冗余的 CPU
    • ATM 机和 POS 机终端
    • 空间站
    • 支付系统
    • 技术会议里的参会者
    • 喝醉了之后发微信的朋友
    • 写了情书让你帮忙寄出去的基友立

1.1 节点和网络

  • 分布式系统里面的单个组成部分被称为node:「节点
    • 文献里看到的processesagents或者actors,多半也是描述同样的东西

1.1.1 节点

  • 延迟特性
    • 节点内部的操作「很快」
    • 节点之间的操作「很慢」
    • 快和慢是相对的,度量标准取决于系统完成什么样的工作
  • 可靠性
    • 可以失效,但失效的时候作为整体单元失效
    • 失效时是可以被感知的
    • 状态是连贯一致
    • 状态迁移是有序,优雅的
    • 通常是以单线程的状态机模型建模的
  • 一个节点本身也有可能是一个分布式系统
    • 但只要对外作为一个整体,这个节点提供高速、一致的可操作接口,我们就认为它是一个节点
  • 进程的形式化模型
    • Communicating Sequential Processes
    • Pi-calculus
    • Ambient calculus
    • Actor model
  • 节点失效的形式化模型
    • Crash-stop
    • Crash-recover
    • Crash-amnesia
    • Byzantine

1.1.2 以消息流(message flow)的方式来思考网络(network)

  • 节点之间通过网络进行交互
    • 人类通过语音相互沟通
    • 粒子通过场相互沟通
    • 计算机通过 IP、UDP、SCTP 等相互沟通
  • 我们把这些交互建模为节点之间发送的离散的消息
  • 消息需要时间来传播
    • 这就是分布式系统里面「慢」的那部分
    • 我们称其为「延迟」
  • 消息常常都会丢
    • 这是分布式系统「不稳定」的又一个因素
  • 网络不是同质的,资源均匀的
    • 有些网络连接比其他的连接要慢、带宽要窄、稳定性要差

1.1.3 因果图

  • 我们可以把节点和它们之间的交互关系表示为一个视图
    • 时间轴是从左到右,或者从上到下的
    • 节点可以抽象为时间轴方向上的线段(它们在一个时间段内一直存在)
    • 消息是连接这些节点线段的有方向的路径

1.1.4 同步网络

  • 节点执行是按时序来的:每个节点之间距离都是 1
  • 消息延迟是有边界的
  • 有一个完美的全局时钟
  • 很容易被证明的一件事情是
    • 同步网络几乎没法构造出来,你的系统多半不是

1.1.5 半同步网络

  • 和同步网络类似,但是时钟不是固定距离为 1,而是一个范围,比如 [c, 1]

1.1.6 异步网络

  • 独立执行,互不依赖
  • 消息的时延没有可控边界
  • 没有全局时钟
  • 比半同步或者同步网络要脆弱
  • IP 网络肯定是异步的
    • 但在实际操作中并没有发生真正的 pathological stuff
    • 大多数网络都可以在几秒到几周内恢复,不会永远 down 掉
      • 相反的, 人类的时间维度是从几秒到几周的
      • 因此我们不能假装问题不存在

1.2 当网络出问题时

  • 异步网络允许发生
    • Duplicate
    • Delay
    • Drop
    • Reorder
  • 丢包和时延是难以分辨的
  • 拜占庭将军问题
    • 包括对内容重写
    • 在真实网络里面大都不会发生
      • 我只是说大都…

1.3 底层协议

1.3.1 TCP

  • TCP一般就够用了
    • 如不确定,先用 TCP
    • 但不完美,虽然可以对它进行加速
    • 但知道怎么做以及什么时候才需要做很关键
  • 虽然单个 TCP 连接实际上有自带的 duplicate 和 reorder 治理
    • 但很可能会有多个连接打开
    • 而 TCP 连接总会失败
    • 当这种情况发生时,要不就会丢消息,要不就需要重试
    • 所以应该在 TCP 上自定义一个增量的序列号来编码,从而对乱掉的时序进行重建

1.3.2 UDP

  • 和 TCP 相同的寻址规则,但是没有 stream invariants
  • 很多人使用 UDP 为了「更快的速度」
    • 不要认为路由器和节点会被丢弃
    • 不要认为包会重传
    • 或者重新排序
    • “But at least it’s unbiased right?”
      • 并不是
    • 这些都造成一些破坏,比如在收集 metrics 的时候
    • 要 debug 也是非常的困难
    • TCP 提供了流控,并且把 logical messages 重新打包成 packets
      • UDP 下需要自己来实现流控和背压
    • 「TLS over UDP」是可以做的,但是开发难度很高
  • UDP 在 TCP 的 FSM 的 overhead 成本过于高昂的时,是有用的
    • 内存压力
    • 大量的生命周期很短的连接和 socket 的重用
  • 在 best-effort delivery 已经能够很好地满足系统目标的时候特别好用
    • 语音通话:当对方听不清时,用户自己会道歉然后重传
    • 游戏: 延迟和卡顿,但会重新对齐
    • 高层的协议在底层已经乱掉的情况下仍然会继续工作假装什么都没有发生

1.4 时钟

  • 当系统分割成多个独立部分的时候,我们仍然希望事件一定程度上是有序
  • 时钟帮助我们对事件进行排序:先这个,后那个

1.4.1 系统时钟

  • 理论上,操作系统的时钟可以给系统产生的事件带来顺序
    • 然并卵: NTP 可能没有你以为的工作那么理想
    • 然并卵: 在两个节点间的同步完成得并不好
    • 然并卵: 硬件也会有漂移
    • 然并卵: 大数还会带来问题
    • 然并卵: POSIX 时钟从设计上就不是单调的
      • Cloudflare 2017: 因为 leap second 造成的问题
      • 其实就是当时 Go 还不提供 CLOCK_MONOTONIC 的接口
      • 在计算出一个负的 duration 然后把它交给 rand.int63n() 后,就驾崩了
      • 造成了 DNS 解析失败: 1% 的 HTTP 访问被影响了好几个小时
    • 然并卵: The timescales you want to measure may not be attainable
    • 然并卵: 线程会睡
    • 然并卵: 运行时会跪
    • 然并卵: OS 会退
    • 然并卵: 硬件会醉
  • 怀疑人生了吧?

1.4.2 Lamport时钟

  • Lamport 1977: “Time, Clocks, and the Ordering of Events in a Distributed System
    • 每个进程一个逻辑时钟
    • 每次状态迁移的时候单调递增: t' = t + 1
    • 每次消息发送的操作时间戳+1 并带上改时间戳
    • 每次接收消息的操作:t' = max(t, t_msg + 1)
  • 只要对所有的进程进行排序,我们就可以知道消息的顺序
    • 但是这个序列号看起来非常不直观
    • 并且无法表示同时发生的消息

1.4.3 向量时钟

  • 把所有进程的时钟放到一个向量里
  • t_i' = max(t_i, t_msg_i)
  • 每次处理完内部事件,把向量里自己进程的逻辑时钟加 1
  • 向量时钟记录了逻辑时钟没有的因果关系,也就是偏序
    • 两个事件的因果关系为:
      • A < B 意味着 A 在 B 后发生
      • B < A 意味着 B 在 A 后发生
      • 否则两者就是没有因果关系的
    • 如果所有的 A_i <= B_i,且至少有一个 A_i < B_i,那么 A < B
    • 如果 A 和 B 比起来有的分量大,有的分量小,则可能是同时发生
  • 棒棒棒: the past is shared; the present is independent
    • 当下各个节点的状态需要被保留
    • 过去的状态可以被丢弃
    • 如何对过去的状态做 GC
  • 向量空间里面有 n 个进程
    • GC 的时候需要协调顺序
    • 或者不做 GC,牺牲掉正确性,直接删掉旧的状态
  • 变种:分布式系统中数据一般存在多个副本,多个副本的时钟可能被同时更新,引起副本间数据不一致

1.4.4 GPS和原子钟

  • 比 NTP 要好很多
    • 全局分发,毫秒级精度,统一排序
    • 可以把一个异步的网络提升为半同步的
    • 解锁了很多更有效率的算法
  • 目前只有 Google 有
    • Spanner: 全局强一致性的事务
    • 没有公开分享实现方式
  • 比你想象的贵很多
    • 每个 GPS 接收器就要好一百多刀
    • 原子钟得多少钱?
    • 需要各种类型的 GPS:制造商可能会搞出问题
    • 但未来的数据中心一定参考 Google,通过硬件接口来解决 bounded-accuracy time 的供给问题

1.4.5 小结

分布式系统的基础,是它的底层构件:

  • 节点之间通过网络进行消息传递
  • 节点本身以及节点间的网络都有可能会崩坏
  • TCP/UDP 这样的协议使得跨进程的通信成为可能
  • 通过时钟我们可以对事件进行排序

接下来看看分布式系统经常被讨论的一些上层特性

1.5 Availabiliy

  • 什么是可用性?就是对所有尝试过的操作里成功部分的比例衡量。
  • 文献和报表里面常见的可用性表示什么?

1.5.1 Total availability

  • 别幼稚: 分布式系统可以做到完全可用吗?
  • 最多是所有还活着的节点上操作成功了
    • 失效的节点啥操作都不会成功

1.5.2 Sticky availability

  • 比较懂分布式的人发明的词。
  • 指对所有活着的节点进行的操作都成功了
    • 在特定的客户端都访问特定的节点的前提下(所以微信分 set 做高可用,还有大量别的系统通过 session 来让特定的客户端「粘住」特定的后端节点)

1.5.3 High availability

  • 比较懂分布式的人发明的遮羞的词
  • 高可用系统比不高可用系统总要好一些吧
  • 衡量标准是系统最多在多长时间内不可用
  • 意味着有一些操作失败了

1.5.4 Majority available

  • 当支撑主要功能的节点运行良好时,由它们完成的操作是成功的
  • 意味着有一些功能被降级了,用户可能在 UI 上根本看不到它们的入口了

1.5.5 如何量化可用性

  • 业界标准 “uptime” 究竟对不对?
    • 系统在没有人用的时候一直 up 着真的好吗?
    • 高峰期该 down 的系统坚挺地 up 着真的好吗?
    • 能否通过测量某个时间窗口中请求数量?
    • 然后在多个时段对这样的窗口进行采样得到趋势图?
    • 注意这样来衡量可用性,uptime 受时间轴单位的影响很大

1.6 Consistency

  • 一致性模型其实就是对系统里面的事件进行「历史现场保护」的模型

1.6.1 Monotonic Reads

  • 一旦本座读过,接下来所有的读操作返回的状态或者值都和本座读的一致

1.6.2 Monotonic Writes

  • 一旦本座写过,接下来所有本座的写操作都会在这次写之后才发生

1.6.3 Read Your Writes

  • 一旦本座写过,接下来所有本座的读操作都返回本座最后一次写的值

1.6.4 Writes Follow Reads

  • 一旦本座读过,接下来所有的写操作都在这次读之后才发生

1.6.5 Serializability

  • 所有的操作(事务)看起来都是原子的
  • 虽然也会有顺序
    • 但顺序本身是什么样的没有约束
    • 所以读到了之前的状态也不会有问题

1.6.6 Causal consistency

  • 所有的操作可以用一个 DAG 表示出因果关系
    • 无法在同一个 DAG 里面表示的操作就是concurrent
  • 局限性: 要完成一个操作前,节点上所有的其他操作必须完成
  • 但是 concurrent 的操作可以自由排序

1.6.7 Sequential consistency

1.6.8 Linearizability

  • 有时候被称为 atomic consistency 或者 strong consistency
  • 所有的操作(事务)看起来都是原子的
  • 所有进程都遵从一个排序
  • 所有操作都有触发时间和结束时间来规约
  • 看起来只是在 sequential consistency 只关心所有进程或者节点的偏序关系基础上, 加上了时间顺序
  • 但实际上非常强大:Sequential Consistency versus Linearizabiltiy

1.6.9 ACID isolation levels

  • ANSI SQL 标准里定义的 ACID 隔离等级比较古怪
    • 主要是拿现有厂商的实现倒推出来的
    • 标准里很多地方定义模糊
  • Weak Consistency: A Generalized Theory and Optimistic Implementations for Distributed Transactions
    • 每个 ANSI SQL 的隔离等级都是为了防止出现特定状况设定的,要能对应起来
    • Read Uncommitted
      • 防止 脏写 的状况发生
        • w1(x) … w2(x)
        • 在另外一个事务提交之前是不能覆盖其再进行写操作的
      • 当另外一个事务还在修改时能够读数据
      • 也能读取被事务回滚以后的数据
    • Read Committed
      • 防止脏读的状况发生
        • w1(x) … r2(x)
        • 不能读一个事务还没有提交的数据,不管这个事务在修改或其他写操作
    • Repeatable Read
      • 防止模糊读或者叫不可重入读的状况发生
        • r1(x) … w2(x)
        • 一旦一个事务读数据,直至这个事务提交之前,这个数据一直不会改变
    • Serializable
      • 防止幻读的状况发生
        • 给定预期 P
        • r1(P) … w2(y in P)
        • 一旦一个事务读取满足查询的元素集合,这个集合在事务提交前都不会改变
    • Cursor Stability
      • 事务有一个游标集合
        • 一个游标指向被事务访问的一个对象
      • 读锁一直保留到游标被移除或提交
        • 在提交时间,游标被升级为写锁
      • 能阻止 lost-update
    • Snapshot Isolation
      • 事务总是能读取提交后的数据的一个快照,这些数据可以是在事务开始之前抓取准备好
      • 只有没有任何其他提交的事务被写入到任何我们想写的对象时,新的提交才能发生
        • first-committer-wins

1.6.10 这些有什么鸟用?

  • 现实世界是不是没有那么「分布式」?
  • 大多数公司用用 Read Committed 也就够了
  • 但是网络攻击的人经常就是利用并发访问来找到系统漏洞

1.7 Tradeoffs

  • 如果可以的话,我们当然都想要系统同时具备 total availability 和 linearizability
  • 但保持一致意味着需要协调状态
    • 如果事件是顺序不敏感的,就不要过分追求
    • 如果事件是顺序敏感的,我们就需要协调状态
  • 协调状态一般就得发消息进行通信,这是有成本的
    • 一致性越高的系统越慢
    • 一致性越高的系统越不直观
    • 一致性越高的系统越难高可用

1.7.1 Availability/Consistency

  • CAP 讲的就是 linearizability 或 total availability 只能要一个
  • 仅仅了解 CAP 理论还不够
    • Highly Available Transactions: Virtues and Limitations
    • 有些人完全不喜欢 sticky available 或者 totally available
      • Strong serializable
      • Serializable
      • Repeatable Read
      • Cursor Stability
      • Snapshot Isolation
    • 有些人接受 sticky available
      • Causal
      • PRAM
      • Read Your Writes
    • 有些人接受 totally available
      • Read Uncommitted
      • Read Committed
      • Monotonic Atomic View
      • Writes Follow Reads
      • Monotonic Reads
      • Monotonic Writes

1.7.2 Harvest/Yield

  • Harvest, Yield, and Scalable Tolerant Systems
    • Yield: 完成一个请求的可能性
    • Harvest: 代表不完整的响应数据
    • 例子
      • 搜索引擎一个节点失败后,让一些结果丢失
      • 数据更新反映在一些节点上,而其他节点没有更新
        • 在分区 partition 情况下考虑 AP
        • 能写一些其他人不能读的数据
      • 流式视频会降级服务,比如降低分辨率以保障更低的延时
    • 取决于负载、硬件、网络拓扑等因素
    • 最好能够按每个请求为单位对 harvest/yield 进行调整

1.7.3 组合拳

  • 一致性模型很多,怎么选?
  • 并不是所有的数据都用一个方案去处理
    • 「大数据」反而优先级较低
    • 「小」数据常常更加关键
    • 比如:微信上用户的各种操作有强一致性保证, 像朋友圈这种东西有最终一致性就可以了

1.7.4 小结

  • 可用性就是度量操作的成功率
  • 各种一致性模型就是对什么操作在什么时候能够发生的规定
  • 一致性要求越高,通常就意味着可用性或者性能的下降

在有了这些理论基础之后,接下来我们看看如何进行工程实现。

1.8 可能的情况下尽量不要Consensus

1.8.1 CALM conjecture

  • CALM: consistency as logical monotonicity
  • 单调就是不会被推翻重来的意思
    • 从已知的局部信息演绎出的结果从来不会因为新信息的引入失效
    • 无论是关系代数和 Datalog 没有 negation 将会是单调的
  • Relational transducers for declarative networking
    • T 理论说明在 Datalog 中可以不在意网络程度情况下节点处理服务器的协调能只以单调计算
      • This is not an easy read
    • 无需协调不意味着不用通信
      • Algo succeeds even in face of arbitrary horizontal partitions
  • 使用不严谨的实践词语表达
    • 在思考问题的时候,看看它能不能被建模成只需要「增加」事实即可表达的
    • 当根据目前已知的事实计算出新的事实时,之前的是否会有需要被推翻的情况?
    • 有没有特别的事实可以作为「上封条」的事实,一旦出现,就意味着整个事实链条完整了
    • 这种「单调」的算法通常来说要好实现得多
    • Likely tradeoff: incomplete reads
  • Bloom语言
    • 非序列化的编程语言
    • 自带流程分析
    • 能够看出哪里需要进行「协调」

1.8.2 Gossip

  • 消息广播系统
  • 在集群管理、服务发现、CDN 等方面得到了广泛使用
  • 非常弱的一致性
  • 非常高的可用性
  • 全局广播
    • 发送一个消息到其他每个节点
    • O(nodes)
  • 网格化
    • 传染病模型
    • 与邻居玩击鼓传花
    • 按 max-free-path 进行传播
  • 或者是使用 spanning tree
    • 一个节点连接到其他另外节点
    • 减少多余信息
    • 降低延迟
    • Plumtree

1.8.3 CRDTs

  • 顺序无关的收敛数据类型
    • 如计数器,set,map 等
  • 对 dupes/delays/reorders 有较好容忍度
  • 不像顺序一致的系统,顺序不依赖「单一来源」
  • 但也不同于最终一致的系统,从来不会丢失信息
    • 除非显式地丢弃信息
  • 在高可用系统中工作良好,比如
    • Web 客户端/手机客户端
    • Dynamo
    • Gossip
  • A comprehensive study of Convergent and Commutative Replicated Data Types
    • 假设有一个已经组合的数据类型 x 和 merge 函数 m, 那么就有:
      • 可聚合: m(x1, m(x2, x3)) = m(m(x1, x2), x3)
      • 可交换: m(x1, x2) = m(x2, x1)
      • 幂等: m(x1, x1) = m(x1)
  • 易于构建,易于检验,解决了各种常见的让人头疼的问题
    • 通讯失败?再试一次,反正会聚集收敛
    • 消息乱了次序也没有关系
    • 两个副本需要同步? 直接 merge 了事
  • 缺点
    • 有些算法 依赖 顺序,不能用 CRDTs 进行表达
    • 读操作可能会读到脏数据
    • 空间成本更高

1.8.4 HATs

  • Highly Available Transactions, Virtues and Limitations
    • 任何副本都会保证响应
    • 低延时 (比串行化协议速度快 1-3 个数量级)
    • Read Committed
    • Monotonic Atomic View
    • Excellent for commutative/monotonic systems
    • 多条目更新时有外键约束
    • 有限的唯一性约束
    • 对于给定任意有限时滞,可以保证收敛(所谓「最终一致性」)
    • 是地理上广阔分布系统的优秀候选方案
    • 最适合解决需要强事务机制的系统
    • 也不要忘了: COPS, Swift, Eiger, Calvin 等等

1.9 Consensus躲不开

  • 共识问题:
    • 三种进程类型
      • Proposers: 提议一个值
      • Acceptors: 通过相互协调,选择一个唯一的值
      • Learners: 读取被选中的值
    • acceptor 的分类
      • N 总共存在的 acceptor
      • F 可能进入失效状态的正常 acceptor
      • M 失效的 acceptor
    • 三条不变性定律:
      • Nontriviality: 只有被提议的值能被学习
      • Safety: 最多只有一个值能被学习
      • Liveness: 假设有一个 P, 一个 L, 和一个 N-F 的集合(即不包括失效状态的所有 acceptor),并且它们都是无故障能彼此通讯的,那么如果 P 提议了一个值,就一定可以被 L 学习到。
  • 大量的分布式系统其实都等效为共识问题
    • 各种锁
    • 有序日志
    • 可复制状态机
  • FLP 理论告诉我们在异步网络中实现共识一致性是不可能的
    • 只要在特定的时间杀死一个特定进程,就可以破坏任何共识算法
    • 但也不至于有这么糟糕因为现实情况下网络里的节点大都可以完成共识
    • 并且,FLP 是基于确定性的
  • Tight Bounds for Asynchronous Consensus
    • 异步共识的严格边界要求

1.9.1 Paxos

  • Paxos 是共识算法的金线
  • 在独立 proposal 基础上实现最终的共识
  • 一些优化后的变种
    • Multi-Paxos
    • Fast Paxos
    • Generalized Paxos
    • 大多数时候很难一下说清用哪个好,或者用哪些来组合是安全的
    • 每种算法的实现上也有细微的「口味不同」
    • 所以把 Paxos 描述成一个算法「系列」而不是一个算法可能合适一些
  • 在大量的产品中用到
    • Chubby
    • Cassandra
    • Riak
    • FoundationDB
    • WANdisco SVN servers
  • 新研究成果: Flexible Paxos: Quorum intersection revisited

1.9.2 ZAB

  • ZAB:Zookeeper Atomic Broadcast
  • 提供顺序一致性 (线性写, 滞后的有序读)
    • 支撑了 ZK 客户端需要快速本地读的特性
    • 但是也有一个 SYNC 命令用来保持实时更新可知
    • (SYNC + op) 组合可以实现线性读
  • 和 Paxos 一样,majority quorum, 5 个或者 7 个节点

1.9.3 Humming Consensus

1.9.4 Viewstamped Replication

  • 看起来是复制协议,其实也是共识算法
  • 事务处理中加入了视图改变算法
  • 大多数节点知道的值能保证活到将来
  • 没有产品化
  • 和 Paxos 一起对 Raft 产生了影响

1.9.5 Raft

  • In Search of an Understandable Consensus Algorithm
  • Lamport 说 Paxos 很简单但是对大多数人来说并不是那样
    • 有没有一个大家都懂的共识算法?
  • Paxos 要解决独立决策的问题,但大多数时候我们只要处理好状态机就够了
    • 维持一个状态机切换事件副本到日志
  • 同时提供了一个集群类成员身份转换的实现,这是大多数系统的核心问题
  • 能够实现线性化的状态机
    • RethinkDB
    • etcd
    • Consul

1.9.6 小结

  • 构建分布式系统的时候,首先考虑它是不是只增加而不推翻事实的系统,不要来就用共识算法
  • 我们可以使用 gossip 来广播消息到其他进程,使用 CRDT 来 merge 节点的更新,使用 HAT 来处理弱隔离事务
  • 有 serializability/linearizability 要求的系统需要共识处理,Paxos/ZAB/VR/Raft,选择哪个算法要看实际情况,特别是系统的规模,因为这常常和系统里的延迟有关

1.10 系统的延迟特性

  • 延迟总是存在的:想想 Grace Hopper 手里那根 11.78 英寸的电缆
    • 带宽再怎么加还是有物理上限的
    • 延迟特性是系统设计时最重要的考量指标
      • 能承受多少网络请求其实也是这个起决定性作用
      • 不同的系统有不同的延迟
      • 不同的系统有不同的任务
      • 不同的算法有不同的特点

1.10.1 多核系统

1.10.2 局域网内

  • 一般来说我们使用的云服务商提供的服务都类似于服务器就在一个 LAN 里面
  • 延迟基本上可以控制到 100ms 以内
    • 但是一旦跨区就有可能时延显著增大
    • 时延还会有抖动,要提前准备好应对措施
  • 和磁盘 I/O 比起来,网络延迟是一种怎样的延迟?
    • 云服务上的「磁盘」可能是其他的服务器在提供
    • 不是可能是,一定是吧
    • 就算不是,插在服务器上的物理硬盘也会有时延,不然 I/O 为什么需要「调度」?
  • 但是和内存访问相比,网络的时延就相当可观了
    • 如果考虑的是吞吐量?
    • 但是使用分布式还可以是其他原因
      • 资源的 sharding
      • 问题的 isolating

1.10.3 多地多中心

  • 做多地多中心的原因有
    • 终端用户感知的时延
      • 10ms 的延迟人类就可以感知,最多可以忍受 100ms 的延迟
        • SH-BJ: 50ms
        • SH–CD: 100 ms
        • SH–GY: 200 ms
      • 如何打败光速:就近服务用户
    • 灾难恢复
  • 很多人以为一个 round 就完成共识
    • 实际上可能需要是 4 个
      • 最坏情况下每次都需要 4 个如果对 Paxos 的实现不是很好(比如 Cassandra)
    • 所以如果在数据中心之间搞 Paxos,要考虑代价
    • 跨数据中心的时延是用户可以感知的
      • 缓存
      • 消息队列
      • 一致性不一定要那么强
      • CRDT 来保障本地写
      • Causal consistency/HATs
  • 非要强一致性?
    • 分区也许是个选择
      • 用户可能不会像货车帮的用户而是像滴滴的用户,不怎么跨省移动
      • 当用户跨区时,再在数据中心之间实现跨区访问
    • 当数据有更新时,放到哪个数据中心?
      • 最近的?
      • Facebook 仍然所有的写操作先经过一个作为网关的数据中心
    • 当顺序一致是够用的,缓存本地读
      • 多地多中心时怎么做?

1.10.4 小结

这里讨论了三个不同维度的分布式系统:多核系统,在一个 LAN 里面部署的服务,以及通过专线互联的数个数据中心。如果说有什么可以总结的那就是,能够避免协调和共识就尽量避免,小到在一个 CPU 的核进行某个计算任务的闭环,大到当业务横跨广阔地域时对用户进行分区来做 datacenter-pinned 的方案,都是这个标准的体现。

1.11 常见分布式系统

1.11.1 Outsourced heaps

  • Redis, memcached, …
  • 一般是内存里的数据结构,不会是很大规模的数据
  • 如果选用的开发语言自己提供了很强的能力好像并不太需要
  • 用来做缓存很棒
  • 或者用来给多个平台之间进行一些「粗鲁」的数据分享
  • 不是非常的安全

1.11.2 KV stores

  • Riak, Couch, Mongo, Cassandra, RethinkDB, HDFS, …
  • 1 维/2 维/3 维的 key
  • O(1)读
  • 可存储大规模的数据集
  • 可以认为有线性的 scalability
  • 可以认为没有事务机制
  • 通常是 linearizable/sequential 的

1.11.3 SQL databases

  • Postgres, MySQL, Percona XtraDB, Oracle, MSSQL, VoltDB, CockroachDB, …
  • 通过关系代数进行建模
  • 可存储中等规模的数据集
  • 处理事务特别是多条记录的事务
  • 关系的维护和事务的完成需要协调机制,scalability 一般
  • 大多数系统提供了主从的 failover 切换
  • 读操作的成本很大程度上受索引质量的影响
  • 强一致性为主 (SI, serializable, strict serializable)
  • Elasticsearch, SolrCloud, …
  • 可存储中等到大型规模的数据集
  • O(1)读, log-ish 的搜索
  • scalability 很好
  • 弱一致性为主

1.11.5 Coordination services

  • Zookeeper, etcd, Consul, …
  • 可存储较小规模的数据集
  • Useful as a coordination primitive for stateless services
  • 强一致性为主 (sequential 或者 linearizable)

1.11.6 Streaming systems

  • Storm, Spark…
  • 通常是根据自己的需要自建的工具集来实现
  • in-memory 的数据卷,规模不会大
  • 低延迟
  • 高吞吐
  • 弱一致性

1.11.7 Distributed queues

  • Kafka, Kestrel, Rabbit, IronMQ, ActiveMQ, HornetQ, Beanstalk, SQS, Celery, …
  • 多节点上做持久化来确保有 redundancy
  • 对于可以分段执行的任务很有用
  • 在无状态的服务之间进行数据传递很有用
  • 唯一不会丢消息的是?Kafka?
    • 还是 SQS?
  • 消息队列并不会改善端到端的时延
    • 有任务立即处理掉总是比丢消息队列要快的
  • 消息队列并不会改善平均吞吐量
    • 平均吞吐量总是和用户数正相关的
  • 消息队列在用户有并发操作的时候并不能保证事件的顺序
    • 用户绝对会有并发操作
  • 消息队列即使在用户的操作是异步的时候也不一定能保证事件的顺序
    • 比如网络问题导致事件到达顺序乱了
    • 所以不要依赖消息队列的事件顺序
  • 消息队列提供了 at-most-once 或者 at-least-once 的消息投递机制
    • 任何号称做到了其他 feature 的消息队列(比如 exactly-once)多半揣着别的东西
    • exactly-once 的投递在消息被确认了之后,持久化之前接收者驾崩了会是什么情况?
    • 所以保证操作是冥等的很重要
  • 消息队列对改善「尖峰时刻」的吞吐量是很有效的
    • 可以用来削峰
  • 分布式消息队列在不丢数据的前提下还可以改善系统的容错性
    • 很可能只需要个 TCP 就够了
    • 很多人使用消息队列干的事情只是简单的 IPC 通信,用一个 socket 的 write()就够
  • 当 runtime 很挫的时候,消息队列可以帮忙

1.11.8 小结

分布式系统里面有各种各样的结构化的数据存储,它们是系统之间的胶水。KV 和关系数据库一般用来存各种记录,KV 虽然使用独立的 key,用来存关系数据看起来不是那么合适,但是它提供了更好的 scalability 并且对 partial failure 有较好的承受力;相比之下,关系数据库提供了更丰富的查询功能和更强的事务机制。流式数据处理系统则主要用来进行低延迟,持续性的数据集的处理,架构上看起来更像是一套 framework 而不是一个数据存储。消息队列则顾名思义偏重在对消息的处理而不是对变化的处理。

1.12 A Pattern Language

  • 开发分布式系统的一些要点
    • 一手经验
    • 他人的经验/最佳实践
    • 江湖传言
    • 只言片语
    • Cargo-culting
    • Stuff I just made up
    • YMMV

1.12.1 是不是可以不「分布式」

  • 如果不是非搞不可,最好不搞
    • primitives/锁/线程/队列,凡此种种,没有分布式作业之前都是靠得住的
      • 在分布式系统里达到同样的功能要如何实现?
    • 什么样的计算任务需要分布式?
      • “这是大数据”
        • 现在云服务的机器有几个 T 的内存?
        • 现在云服务的机器有多「快」?
      • JVM 可以处理什么量级的请求?50000 每秒?
      • TCP 上的 PB 呢?10000000 每秒?
    • 是不是一个「可以挂掉」的服务?
    • 是不是可以做 failover?
    • 是不是可以进行一些人工干预?

1.12.2 使用现有的系统

  • 如果非分布式不可,是否需要自己来?
    • 是不是一个分布式数据库或者日志系统可以解决的?
    • 是不是用阿里云/腾讯云提供的基础设施能解决的?
    • 知道怎么开发/测试/运维/升级一个分布式系统吗?

1.12.3 用尽办法减少failure

  • 都决定做了就买那些看起来贵得要死的机器吧
  • 对软硬件的改动都要做足够的测试并且跟踪起来
    • 有 staging 环境吗?
  • 硬件的问题总会有,但是通过花钱和找靠谱的人,可以把它发生的概率降到很低,从而使它成为一个「低优先级」的问题

1.12.4 但仍然要接受failure

  • 分布式系统两个最大的难题之一就是partial failure
  • 多大点儿事,对吧
    • SLA 是?
    • 可以恢复吗?
    • 需要打电话给用户道歉赔钱吗?
    • 不要以为你是个伟大的程序员,你是提供服务的

1.12.5 备份

  • 备份其实就是一个 equential 的一致性但
    • 只有在正确地备份了之后,才有备份
      • 有些备份程序没有保存状态,造成文件系统或者数据库崩溃
      • 外键失效/文件丢失,多搞过几次恢复你总能遇得上
      • 也不是使用分布式数据库就没事了,手一抖关键数据就被删除了
    • 但备份不仅仅是进行错误恢复,它可以进行场景重构
      • 对于理解和解决逻辑错误很有用

1.12.6 冗余

  • 系统一定会挂
  • 如何降低挂的概率?
  • 在多个节点上尽量保持状态和数据的一致
    • 主备的 failover 是不够的
      • 备用系统不一定切得过去:缓存失效,磁盘坏的,软件版本不一致等等问题
      • 几乎没有见过大型生产系统在失效的时候能够切到备用系统
      • 尽量多活
        • 建设起来非常有难度
    • 有 2 份备份数据也是不够的
      • 除非这数据不重要
      • 所有备份都是 3(数据中心,机房,甚至柴油发电机)
        • 特别重要的做个 4/5 份也没有什么关系
    • 常见的策略有哪些?
  • 只要错误是「不互相关联」的,冗余就可以增强系统的可用性
    • 所以下面的问题就算是冗余也解决不了
      • 如果同一个批次上线使用的磁盘集体驾崩了
      • 整个机架因为交换机一起挂了驾崩了
      • 停电后 UPS 不工作整个数据中心里的节点一起驾崩了
      • 地震这类天灾
      • 以及,如果在节点上运行的任务就是有问题的,这些节点也会一起驾崩
      • 级联失效/雪崩

1.12.7 分片

  • 计算任务太大时做分片
  • 一个节点上可以处理分片后的任务
    • 别太小: 太分散整体协调的 overhead 会增大
    • 别太大: 在各个节点之间会出现负载不均衡,又得进行调度
    • 10-100 比较好
  • 理想情况:计算任务是均匀的
    • 但会有 hotspots
    • 但 workload 会因时而异
  • 设计时要给出系统上限
    • 一个节点计算任务超过上限了,涌出来的任务会不会把其他的节点一个个淹死
  • 分片如何进行分配?

1.12.8 ID

  • 你打造的分布式系统里还有东西没有唯一 ID 吗?
    • 考虑不同的 access 模式
      • Scans
      • Sorts
      • Shards
    • 如何避免协调:全局唯一,但本地生成
    • ID 可以 map 到一个分片吗?
    • ID 里要不要编码用户 ID?

1.12.9 不可变值

  • 永远不变的数据易于存储
    • 从不需要协调更改
    • 复制和恢复成本低
  • Cassandra, Riak, 任何使用里 LSM-tree 的 DB
    • 以及像 Kafka 这样的系统
  • 易于验证:要不就是要不就不是
    • 传统的让人头疼的问题都没有了
    • 很容易做缓存
  • 超高的可用性和持久性,可以对写操作的延时做调整和控制
    • 读操作延迟很低:可以从最近的一个副本直接读
    • 这对地理上分布广的系统非常有价值
  • 需要 GC
    • 但是有很多成熟的好方案了

1.12.10 可变的标识

  • 指向不可变的值
  • 指针很小,仅仅是元数据
    • 一个很小的数据库就可以放入大量的指针
    • 对于共识服务或关系数据库是好的数据结构
  • 大多数时候系统里面也没有那么多指针
    • 整个数据库可能就分配一个指针来指向
    • Datomic
  • 基于标识的强一致性操作能够用不可变的 HA 高可用存储来实现
    • 利用 CAP 中 AP 存储的优点:低延时和可扩容
    • 利用有共识系统提供的基于小数据集的强一致性
    • 写操作可用性受存储的限制
      • 但是如果只需要读操作顺序一致,非常好做缓存
      • 如果只需要 serializability 还能做得更便宜
    • Rich Hickey的演讲
    • Pat Helland讲解Salesforce的存储

1.12.11 无冲突

  • 文献里面的 Conflict-free/Convergent/Commutative/Confluent 是一个意思
  • 顺序无关的系统总是更好构建和维护的
  • 同时也减少了协调的工作量
  • CRDTs
  • Immutable Value
  • Streaming:
    • 先 buffer,当知道可以 compute+flush 的时候再操作
    • 如果需要,对当前结果可以 emit 出去并且根据结果进行相应操作(监控体系)
    • 全量数据拿到后呢?
    • 银行交易系统怎么做的?
  • Behavior of Database Production Rules

1.12.12 背压与流控

  • 服务之间很多时候是通过队列来交互的
  • 服务和队列的容量是有限的
  • 当负载无法被消化的时候怎么处理?
    1. 对请求进行分片,或者丢了不处理
    2. Reject,并且告知客户端系统在「维护」
    3. 采用背压:也就是告诉客户端缓些来
  • 三种方式都是可以的
    • 但是背压可以大大减少需要重试的请求:根本没有发出来
  • 背压把决定权交给了生产者,很 compositional
    • 流控的设计模式,客户端永远不知道服务器咋了
    • 背压的设计模式,要流控也可以选
    • 如果异步系统最好实现背压机制,用户会少一些机会来羞辱你
  • 核心:对资源划分领域边界
    • bounded time (比如简单的,给请求加上超时)
    • bounded use
    • Bounded queues
    • Bounded concurrency
  • Everything Will Flow

1.12.13 领域模型的服务

  • 核心问题是每个 domain 在逻辑上都是碎片,它们之间如何完成交互
  • 各个碎片有自己的技术栈,性能表现,存储需求
    • 单体应用很多采用里多租户方式
      • 正确实现多租户很难
      • 拆成多个逻辑服务
  • 如何拆分
    • 面向对象编程: 每个名词是个服务
      • 用户
      • 活动
      • 广告
    • 函数式编程: 每个动词是个服务
      • 认证
      • 搜索
      • 排序
      • 分发
    • 大型系统一般都会混合使用两种拆分方式
      • 以名词为核心构建的服务易于保障datatype invariants
      • 以动词为核心构建的服务易于保障transformation invariants
      • 所以我们经常看到一个用户服务被认证服务调用
    • 但是具体如何划分边界仍然是很 tricky 的
      • 服务开发和维护的成本不小,能少一个就少一个
      • 以是否构成一个完整的工作单元来衡量
      • 需要进行不同维度扩展的也分到不同的 service 为妙
      • 依赖项和延时特性比较一致的服务部署到一起
      • 资源(如 CPU 或者磁盘)互补的服务部署到一起
        • 比如: 在进行渲染的节点上使用 memcache
        • 比如: Google Borg, Mesos, Kubernetes
    • 服务需要有较好的封装和抽象
      • 最好是树状结构不是网状结构
      • 暴露接口而不是暴露数据
    • 服务间进行协调需要新的协议
      • 对事务要重新进行定义
      • Sagas
        • 注意它是在单节点场景下被提出的: 针对分布式环境要小心
        • 事务必须是冥等的
      • Typhon/Cerberus
        • 多个 data store 上达到最终一致性的协议

1.12.14 康威定律

  • 产品化的软件系统本质上是一个社会化的产物
  • 一个团队来主导一个服务的开发
    • The Tyranny of Structurelessness
      • 权利与责任应该明确
      • 成员要进行任务的轮换
        • 增强彼此间的信息分享
      • 轮换不宜过于频繁
        • 软件研发的上下文切换成本很高
  • 随着团队的不断壮大,它承担的任务和团队成员的思维会固化
  • 服务还是类库?
    • 不要轻易做服务,一开始先考虑类库
    • 边界良好划分的类库很容易就可以在需要的时候升级成一个服务
  • 康威定律:组织边界决定了交付物的边界
    • 当类库的用户数较少或者团队的协作比较紧密时,变动较容易
    • 但是当跨多个团队时,各个用户的优先级都不一样,需要进行协调
    • 而且类库的更新需要用户自己来做(即使用 Maven)
    • 服务可以使得团队间通过定义的 API 来进行协作,并且给这种协作加上强制约束
      • 如果是用类库的方式,需要通过很多的代码交叉 review 以及相应的工具才能有类似效果
  • 服务提供中心化的控制力
    • 性能改进每个人都会立刻感知到
    • 技术选型更加独立可控
    • 可以先在单点进行服务的试用
    • 这些都是使用类库很难达到的
  • 但服务有较大成本
    • 失效的情况复杂度高,还要考虑调用的网络延迟
    • 服务以来弄不好就是一锅粥
    • 很难进行静态的代码路径扫描
    • 测试和运维成本更高
    • 版本化是必须的但还不是技术而是艺术
  • 但不要纠结服务还是类库:优秀的服务后面跑着的可以是优秀的类库

1.12.15 小结

  • 如无必要,不要搞分布式,用一个 node
  • 如果要搞,弄好 SLA,准备好天天下跪
  • 当灾难性的问题发生时,唯一可以依靠的是备份,所以请备份
  • 要提高可靠性,可以增加冗余性
  • 要解决大规模的问题,可以分片
  • 不可变值和可变的标识符结合使用,来构建大型的分布式系统
  • 当系统越来越大时,每个模块必须独立 scale,有些库可能升级成服务,要做好与此同时团队进行 scale 的工作

1.13 生产环境常见问题

  • 大量「设计上」想不到的事情会发生
  • 数学证明很重要,但真实系统还得 I/O

1.13.1 分布式系统是需要企业文化作为支撑的

  • 要构建/部署/运维分布式系统需要组织内各种角色相互紧密配合
    • 开发
    • 测试
    • 运维
  • 同理心很重要
    • 开发需要关注环境和运维
    • 运维需要关注设计和实现
    • 好的沟通可以让问题解决得更快(我们自己推广微服务过程的经验)

1.13.2 测试

  • 强类型语言可以很好的防止一些逻辑错误
    • 可以减少一些测试的工作量
  • 但是并不能预测和控制运行时的 performance
  • 所以需要全面的测试能力和完备的测试集
    • 可以迅速执行完毕的基于用例的测试(example-based testing)
    • 可以在晚上跑的生成式的测试(property-based testing)
    • 可以模拟整个集群的行为进行测试
    • 可以模拟网络交互进行concurrent interleaving的控制
    • 可以自动化注入一些硬件方面的失效情况
  • 测试分布式系统比测试一个本地的,单体的应用要难得多

1.13.3 When “It’s Slow”

  • Jeff Hodges: The worst bug you’ll ever hear is “it’s slow”
    • 总会发生,很难定位
    • 因为系统是分布式的,需要对各个节点都做 profile
      • 大多数的 profiler 都不支持这样的用法
      • Dapper
      • Zipkin
      • 其他工具方面的投资
    • Profiler 的功能即使完备,也主要是发现 CPU 相关的问题
      • 但是出现「很慢」的情况,多数瓶颈在 I/O 而不是 CPU
      • 存储
      • 网络
      • 消息队列
      • GC
    • 通过应用的 metrics 来找到问题
      • 然后深入到进程和 OS 层面来查看具体原因
    • 如果在同一个集群下面:
      • 1/3 的节点: 多半是硬件或者配置问题
      • 大量的节点: 多半是逻辑问题,要看看分片大小/负载等关键指标
    • 尾延迟问题

1.13.4 监控系统

  • 某种程度上监控就是一个持续的测试过程
    • 监控的性能要求高
      • 1ms 量级的响应速度
        • TCP incast
      • 运维处理的响应速度:10 秒内?1 分钟内?能不能自动化?
    • And for capacity planning, hourly/daily seasonality is more useful
    • 按需进行设计
      • 哪些是重要的?
        • 对请求的响应是重要的
        • 节点的 CPU 没那么重要
      • 关键指标
        • Apdex: SLA 范围内的成功响应次数
        • Latency: 0, 0.5, 0.95, 0.99, 1
          • 百分位,不是平均
          • 也不是百分位的平均
        • 负载
        • 消息队列的指标
        • 其他
          • 监控看起来没问题,用户说慢?
    • 普通的监控手段是无效的
      • trace
      • host 机器的 cpu/disk 等 metrics
      • APM
      • 很可能需要自己开发
        • 至少是 metrics 要自己开发
    • 分布式日志基础设施 (Zipkin, Dapper)
      • 需要极大的投入
      • Mystery Machine
        • 通过日志自动关联事件
        • 找出关键路径
        • 对性能进行更好的预估和建模

1.13.5 日志

  • 日志记录的是单个子系统的情况,在大规模系统里用处不大
    • 问题一般不是出在单个节点
      • 而一个请求往往是跨很多服务的,信息散布在多个日志文件里面
      • 需要对日志进行聚合
        • ELK, Splunk
    • 非结构化的信息很难聚合
      • 结构化所有的信息

1.13.6 流量投影

  • 压力测试只有当模拟出来的流量和真实的流量特征一致时才有意义
  • 如何从生产环境 dump 流量?
    • 招数 1: 使用 SIGUSR1 杀掉进程来 dump 一个时间段的流量
    • 招数 2: tcpdump/tcpreplay 套件
    • 招数 3: 从生产环境直接投影到测试环境
  • Envoy有一些相关的功能

1.13.7 版本管理

  • 是艺术,不是技术
    • 意味着没有人真正知道是怎么做,但是做得好的就让你感觉美
    • 比如所有消息都有版本这个标签
    • 兼容好之前版本的逻辑
    • 通知客户的 API 版本过老
      • 如何进行强制升级?

1.13.8 发布机制

  • 发布机制实际上决定了修复问题的速度
  • 所以为自动化的,可控的发布做足够多的投入是值得的
    • 它是一个杠杆:做好了可以把其他各个部分的投入整体加成,反之亦然
    • 逐个节点发布,不要造成中断
      • 这意味着其实会有多个版本的应用同时在跑
        • 「版本管理」一定要做好
    • 做好雪崩的防范
  • 科学的进行灰度
    • 选取热心用户先灰度
    • 迅速甚至是高速的迭代
    • 考虑进行流量投影来比较新旧版本
      • 这是真正有效的检验新老版本性能的办法

1.13.9 功能开关

  • 在一次变更之后,所有的更新如何可以比较受控的灰度到用户?
    • 把功能一个个打开,看看它们对 metrics 的影响
    • 逐渐把流量一个库一个库的加上去
    • 一旦出现问题可以动态关闭功能开关
  • 当有些服务降级的时候,我们希望对用户而言关键的功能是可用的,所以需要动态关掉一些别的功能的能力
    • 在故障恢复的时候,先关掉一些功能也可以加快恢复的过程
  • 进行功能开关设定的服务要足够健壮
    • 必须依赖足够少
      • 特别是要和主库解耦
    • 当这个服务失效时,如何做到fail safe

1.13.10 混沌测试

  • 在生产环境进行错误注入时
    • 及时处理出现的问题,不要把演习搞成事故
    • 对关键路径的依赖进行识别
      • 关掉哪些通过 API 对外提供功能的服务要特别小心
    • 需要基础设施特别是监控和告警很健全,可以迅速衡量出影响并应对
    • 控制好影响范围
      • 不要动不动就想把整个数据中心弄没了看看是什么情况
        • 但也许每个季度来一次演习?
      • 不要把副本里面的太多节点一次性搞挂
      • 不要把大量的用户/请求一次性搞挂

1.13.11 消息队列通常是麻烦的源头

  • 每个消息队列都有可能带来灾难性的问题
    • 没有一个节点的内存是无上限的,所以队列必须有「边界」
    • 边界是多少要通过埋点获取数据来进行规划,不要猜
  • 消息队列主要是用来进行削峰的
    • 提高了吞吐量但是牺牲了响应时间
    • 一旦负载过大,超过了队列容量,就还是会挂,怎么办?
      • 分片
      • 背压
      • 注意埋点和监控
        • 当分片发生时,应该有告警
        • 当背压发生时,应该有告警
    • 注意消息队列的深度
      • 通常看深度就知道是不是应该扩容了
        • E2E 的队列时延应该是小于平均波动
      • 增加队列的 size 往往是一个治标不治本的方案
    • 其实是特别复杂的工程难题:

1.13.12 小结

  • 一个分布式系统需要开发、测试和运维工程师通力合作
  • 传统的测试手段,以及现在兴起的 formal verification 或者 property-based 的测试,可以对系统的正确性有不错的验证。但是要理解生产环境特别是生产环境的错误,一定要做好生产环境的埋点和端到端监控
  • 成熟的分布式系统研发团队会投入很大的成本和精力进行工具打磨:流量投影,增量部署,功能开关化等等。

1.14 延伸阅读材料

35-02 概率

35故事集

1

好久不见。

这次要说的事情,发生在差不多十年前。

那时我在张江一家外企上班。每周三下午四点,我会乘二号线然后转四号线,到上海体育馆附近一家医院理疗。

这家医院很难得的开设了专门的「疼痛科」,处理使用电脑过度的脖子和肩背,在程序员圈子里颇有些名气。

做完理疗一般是六点左右,我会去美罗城一家人很少的咖啡店,要一杯焦糖玛奇朵,一块羊角面包,看 40 分钟自己带去的书,然后到体育馆外场踢一场球。

一次理疗,一杯焦麻,一块羊角面包,一本书,一场球,每周三的固定模式。

四点下班虽然过分,理疗对于已经年满三十的我来说用处不大心里也很清楚,但除开固定模式带来的仪式感,要在生活里创造点别的乐趣也很难。

那个星期三傍晚,我照常来到咖啡店。这天带的是《刀锋》。

很早之前我就看完了所有能够找到的毛姆的书,但每次再看,仍然觉得有趣。

单论文笔,毛姆不算伟大,但在我看来,这其实是他的优点:没有追求戏剧性刻意制造的巧合,瑰丽斑斓的手法也不多, 特别难得的是,很少跳到台上讲道理,对不待见的角色嘲讽时带着怜悯,对喜欢的人物无论多么倾心也只是点到为止。

打开第一页,我整个人就掉了进去,直到有人在我对面坐了下来,轻轻敲了一下桌面。

「不好意思,打断你一下,问个问题好吗?」

我抬头打量了一下对方。齐肩短发,长相甜美,笑得温柔又干净。年纪应该在二十五岁上下,衣着休闲,虽然不是什么昂贵的品牌,但搭配得很有格调。

她平常应该也很少有这么大胆的举动,羞得满脸通红,在咖啡厅黯淡的灯光中显得异常娇媚,一时间我竟看得有点呆了。

「如果是觉得这样太不礼貌了就算了」,她一边说一边站起来要逃。

「没有,没有打扰。你想问什么?」,我连忙说。

「你看的是毛姆的《刀锋》吗?」

「是啊」,我有些悻悻然地把书立起来指着封面,「我还以为你要我的电话号码呢。」

她的脸更红了,顿了几秒才像是重新鼓足了勇气般继续说道,「你进来拿书出来的时候瞄到一下,就想没有这么巧的事情吧」。

然后就在自己的挎包里掏出本一模一样的书来。

我们两个人互相看了一眼,都忍不住笑出声来。

顾客稀少的狭小咖啡厅里,居然有这样好看的女孩和我都在看并不热门的《刀锋》,真的是好巧。

虽然是两个陌生人,一下子也亲近了很多。

她说她是本地人,因为还没结婚,和家人一起住徐汇中学附近,目前在离家不远的幼儿园里当老师。工作性质的关系,很难找到读书的时间,只能在每周三下班后,挤点时间翻翻。

「要好的姐妹们要看书也是追追网络小说,男朋友一般都在电脑上打游戏,很难有这方面的共同话题。」

我也对自己做了乏善可陈的介绍,四川人,程序员,住浦东,工作十年,还在租房。喜欢踢球,但跑得越来越慢,书算看过不少,不过基本都是小说:仅仅是消磨时间。

「不如一起吃个晚饭吧」,我看了一眼手机,「如果你没有别的安排。」

「好的呀。」

我们边吃边聊着自己觉得有趣的电影和小说,不知不觉一个多小时就过去了。约好了一起踢球的人给我来了通电话,我告诉他们今天要加班。

分开的时候,她说,「真不好意思耽误了你踢球,但和你聊天很高兴。」

「我也一样,很开心」,然后在是否能大胆要她联系方式的心理斗争中,我目送她离开。

2

第二周,我继续着自己的模式,到了差不多同样的时刻,她又走进了咖啡厅。平底鞋,白色丝绸连衣裙,搭了珍珠耳环和俏皮的金属坠子项链。头发束了起来,不是那种一丝不苟地弄法,慵懒且有恰当好处的自然。

整个人就算放到一堆电影明星里,也会闪闪发光。

我们四目相对时,她冲我微微一笑,然后在离我不远的桌子坐下,打开书看起来。我也埋头翻起自己的这本,却一页都看不进去,只觉得被人放进了烤箱一般,口干舌燥。

「这外面有家不错的西班牙海鲜饭」,她总算听到了老房失火的噼啪声,款款走过来柔声说道,「吃完我去看你踢会儿球吧。」

「七点半开球,会不会有些来不及?」

「没关系,今天我开车了。」

踢完球她送我回家,在花木路上她靠边停下来,握住了我的手。

「还没有洗澡」,我有些不情愿她捏着汗津津的手。

「没关系的」,她害羞地低头看着汽车仪表盘,好像对自己说话一般说道,「我们见过之后,我每天都过得兴冲冲的。以前我上班时常常想,那些孩子究竟在高兴什么,现在我终于明白了那是什么样的感觉。」

我轻轻捏了一下她的手。

「还特地为今天见面,换了一套新的内衣。」

事情进展得真快,但好像并无不妥。

我来不及告诉她自己这一个星期也和她一样过得既紧张又充满了希望,连一向嫌弃我病怏怏模样的老板都说我干起活来神采飞扬,就和她再次发动汽车,飞快地冲到我租住的小区,手拉着手窜进房间,拉上窗帘,没有开灯,在沙发上忘情地吻了起来。

我用舌尖感受着她充满弹性的嘴唇,她贝壳般光滑的牙齿,她冰凉的耳垂,她白皙的脖颈。在我的攻击下她呼吸渐渐急促起来,我就缓缓地脱掉了她的连衣裙。

她穿着蕾丝内衣温暖湿润的身体,比连衣裙还要光滑白净。我一只手轻轻抚弄着她柔软的乳房,另一只手伸到她背后去解搭扣。

这时我摸到了一颗挺大的痣,我把她翻过来抱在怀里,她看起来就像点了一滴酱油的寿司般让人嘴馋,于是我用舌尖舔了一下那粒黑痣。

她的身体突然变得很僵硬,猛地一下从我怀里坐起来,一边拿衣服遮住自己一边说,「对不起,能不能停下来。」

「是我有什么地方做得不好吗?」,我有些不知所措,「还是刚踢完球太难闻了。」

「都不是,我很喜欢你的汗味,你的嘴唇也温柔极了。但是,我有些害怕。」

我把她拉过来抱回怀里,用手轻轻抚摸着她头发,让她平静下来。

「没关系的,我们停下来,你不要怕。」

她的头在我下巴和胸前摩挲着,没有说话。房间里充满了令人窒息的沉默,让我觉得喘不过气。

过了很久,她说,「你转过去,我要穿衣服了。」

「还怕我看啊」,我嘟囔着。

「对不起,你不要觉得扫兴」,在得到她允许转过头来之后,她带着撒娇的微笑说道。

「怎么会,还专门买了性感内衣」,我故作轻松地说。

「不会性感的,我的胸部不够大」,她脸红到了耳根,「但是我确实希望你看到的时候自己漂亮一点。」

「我只是想开个玩笑,让我们都缓和一下。」

「我当然知道」,她脸上浮起心领神会的笑容,温柔地看着我的眼睛。

我抱了抱她,「把你的名字和联系方式给我吧,漂亮姑娘」。

「是要认真地追我吗?」,她歪着头,表情变得调皮起来,「其实,我就是怕没有这个,你就不会好好对待我。那就从下周三开始吧,你要好好追我。」

3

接下来到这个周三,我没有带踢球的东西。理了发,刮了胡子,衣服也是自己觉得最精神的一套,还抱了一大捧百合:聊天的时候她说过,这是她最喜欢的花。

地铁到世纪大道换乘的时候,我突然发现,她从门口上车了。

她穿得也很庄重,事后想起来,未免庄重得过了头,有些不像轻松直率的她了。

而且,头发也染成了不那么精神的浅栗色,这都是命运之神给我的提示,但当时我内心充满了盲目的喜悦,把这些线索都一一忽视了。

「送给你」,我挤过去来到她上车的门前。

「给我的?」,她显得很惊讶,「但是我还真是很喜欢百合,谢谢了。」

「当然是真的喜欢,你说过嘛。今天我带你去吃个法国餐厅吧?」,我一边说,一边感觉到她身上有着奇怪的距离感,那种近在咫尺却不知道相隔着多少个光年的距离感。

「你是那种在地铁上送一捧花得逞了,就要骚扰个不停的大叔是吧?」,她把花塞回我的手里。

我看着她的眼睛,希望她是在和我开玩笑而已。但她的眼神里,除开纯粹的愤怒,就只有纯粹的平静。

我伸出手想要去摸一下她的手背,缓和一下场面,结果她一把推开了我,旁边的人都开始注意起我们。

「你不记得我们上周在一起说的了吗?」,我低声说道。

「不知道你记得什么,但是我不记得了。」

记忆这东西就怪在这里,唯有大家都记得且记得一致,不然就毫无意义。

我把花往地上一扔,转身往别的车厢走去。她好像在我身后喊了些什么,但我很好地控制了自己最后的好奇心,头也不回的在最近一站下了车。

这点脸面我还是要留给自己的。

不用说,我的周三模式再也无法进行下去,上海体育馆的球场也再没有踏入过,甚至整个浦西,我都尽量不去。

4

大概三年之后,因为要到美国出差,我去南京西路签证。地铁到了世纪大道,她穿着同一身职业套装上车了。

时间没有在她身上留下太多痕迹,虽然严肃了些,但还是一如既往的好看。

我甚至无端地感觉车厢也是同一节,谁知道呢,地铁的每节车厢都那么像。

一切仿佛就发生在昨天。

「你是在张江做程序员的是吧」,她看到我目不转睛地盯着,主动走过来说道。

「你是在徐汇教幼儿园的是吧?」,我没好气地回答到。

「那是我的妹妹,双胞胎妹妹。」

「双胞胎妹妹…..」,我的声音空洞又干瘪,像落入深井的人发出的毫无希望的求救声。

「我们是同卵双胞胎,唯一的差别只有身上的痣,但一般人怎么可能接触得到。所以从小到大,就给对方添了不少麻烦。遇到你那回应该是最严重的一次,可是当时并不知道。」

「她应该没有告诉你。」

「妹妹虽然从小就比我热情,但自己心里面还是藏着不少事情。我倒是当天回去就告诉了她自己遇到了变态,问她是不是她认识的人。她表情虽然很复杂,但是并没有承认。后来才听她说了。」

「后来听她说了。」

「是啊,她每晚都去美罗城那个咖啡厅等你。」

「每晚?」

「每晚,坚持了一年多。认为如果是心意相通的人,就一定会回到上一次两个人说好的地方,出国之前这么告诉我的。但说实话,作为姐姐,你们没有在一起我为她感到幸运。」

「为什么呢?」

「她太喜欢你了,但你照顾不好她的。你看看你这个年纪了,还在租房住。」

我想了一秒,有些不服气地想要争论如果有她自己会多么努力,但嘴上只是说,「她现在过得怎么样,幸福吗?」

「应该挺幸福吧,她说得不多。可能幸福的人都是沉默的,因为他们很满足,没有那么多话要说。你呢?」

「我一直觉得并不是自己有什么地方没有做对,只是输给了概率。要和她这样的女孩在一起,概率很重要,就像我们踢球时天上划过的飞机。也许你觉得声音很大,就在头顶,但怎么也找不到。也许你觉得飘渺不定,但随便一看,就锁住它了。」

看到她的表情有些不以为然,我补充了一句:「同卵双胞胎的概率只有 1/250,百分之零点四,知道自己输给这样的概率我释然多了。」

「概率吗?我觉得主要是意愿的问题。你难道没有想过她为什么突然变了样?没有想过一个在徐汇上班的人,怎么可能在世纪大道上车?而且如果你那天不是跑那么快,就能听到我在后面喊着问你,你是不是要见的是我妹妹」,她仿佛不需要喘气地说道,「相比之下我妹妹的做事情的意愿要强烈得多。在决定不等你了之后,很怕痛一直不敢做手术的她,还去把背上那颗痣给去了。」

她一边说,地铁一边在隧道里继续穿行着。我感觉一帧帧交替映入的光亮和黑暗好像把现实完全切碎了,她在说什么我完全没法听见,只觉着整辆车载着我们一起,滑向无比遥远的过去,又或者是无比遥远的未来。

一切就像是某部电影里的一个镜头。