延迟特征 #
- 延迟永远不会为零
- 带宽不断提高,但我们已经接近光和电子的物理极限
- 延迟预估塑造了系统设计
- 您能承受多少网络调用?
不同类型的系统对"慢"有不同的定义
- 不同的目标
- 不同的算法
多核系统 #
-
多核(尤其是NUMA)架构有点像分布式系统
- 节点不会以病态方式失败,但消息交换很慢!
- 同步网络由总线提供(例如Intel QPI)
- 硬件和微指令(microcode)中的一整套协议使内存看起来正常
- 非暂态存储(Non-temporal)指令(例如MOVNTI)
-
它们提供了隐藏分布性的抽象
- MFENCE/SFENCE/LFENCE
- 在加载/存储指令中引入序列化点
- 延迟特征:~100个周期/约30纳秒
- 实际上取决于硬件、缓存、指令等等
- CMPXCHG Compare-and-Swap(对内存的修改满足顺序一致性)
- LOCK
- 锁定整个核心之间的完整内存子系统!
- MFENCE/SFENCE/LFENCE
-
但这些抽象是有代价的
- 硬件锁消除可能有所帮助,但还处于初级阶段
- 博客:Mechanical Sympathy
- 尽可能避免在核心之间进行协调
- 上下文切换(进程或线程!)可能很昂贵
- 处理器绑定可以大大提高性能
- 在编写多线程程序时,尽量将工作划分为独立的块
- 尽量将内存屏障与工作单元边界对齐
- 允许处理器在工作单元内尽可能地进行优化
- 参考Danica Porobic, 2016: High Performance Transaction Processing on Non-Uniform Hardware Topologies
本地网络 #
- 您经常在类似以太网的LAN上部署复制系统
- 消息延迟 ~100us
- 但在任何规模较大的网络(EC2)中,预计延迟可能在 ms 级别
- 有时,数据包可能会延迟五分钟
- 为此最好规划
- 网络与未缓存的磁盘寻道相当
- 或者在EC2中更快
- EC2 磁盘延迟通常达到20ms
- 200ms?
- 20,000ms??
- 因为 EBS 实际上是其他计算机
- 如果您认为EC2中的任何内容都是真实的,那就太搞笑了
- 等等,真实磁盘也会这样?
- 那IO调度程序都是什么鬼?
- 等等,真实磁盘也会这样?
- 20,000ms??
- 200ms?
- EC2 磁盘延迟通常达到20ms
- 或者在EC2中更快
- 但是,网络比内存/计算慢得多
- 如果您的目标是吞吐量,工作单元的时间可能超过 1ms
- 但还有其他分布的原因
- 资源分片
- 隔离故障
地理位置复制 (Geographic replication) #
- 你在全球范围内部署服务的原因有两个
- 终端用户(end-user)延迟
- 人类可以察觉到 ~10ms 的延迟,可以容忍 ~100ms
- SF-丹佛:50ms
- SF-东京:100ms
- SF-马德里:200ms
- 超越光速的唯一方法:将服务移到更近的地方
- 人类可以察觉到 ~10ms 的延迟,可以容忍 ~100ms
- 灾难恢复
- 数据中心的电力状况良好,但并非完美
- 飓风是一种现象
- 整个亚马逊区域可能会发生故障
- 是的,是区域,不是可用区
- 终端用户(end-user)延迟
- 共识需要至少1个往返
- 可能最多需要4个往返
- 如果您的Paxos实现有问题(例如Cassandra),可能始终需要4个往返
- 因此,如果在数据中心之间执行Paxos,请准备好接受这个成本!
- 因为最小延迟高于用户的容忍度
- Cache cache cache
- 写入排队并进行异步传输
- 考虑以降低一致性保证换取较低的延迟
- CRDT始终可以提供安全的本地写入
- 因果一致性和 HAT 在这里可能是好的选择
- 可能最多需要4个往返
- 那么强一致性的事务怎么办?
- 一个地理分布的服务很可能具有自然的分区界面
- 欧盟用户位于欧盟服务器上;美国用户位于美国服务器上
- 使用共识将用户迁移到数据中心
通过共识用户的位置避免对跨分区的事务共识.
- 将更新 pin/proxy 到主数据中心
- 最好是最近的数据中心!
- 但也可能不是!我相信Facebook仍然通过1个数据中心推送所有写入!
- 在可以接受顺序一致性的情况下,本地缓存读取!
- 您可能已经在单个数据中心中利用了缓存
- 一个地理分布的服务很可能具有自然的分区界面
总结 #
我们讨论了分布式系统的三个延迟特征尺度:多核处理器与同步网络耦合、通过局域网连接的计算机以及通过互联网或专用光纤连接的数据中心。CPU的影响主要是性能方面的考虑:了解如何最小化协调。在局域网中,延迟足够短,可以进行多个网络跳转,用户不会注意到延迟。在地理复制的系统中,高延迟推动最终一致性和固定数据中心的解决方案。
常见分布式系统 #
常见的分布式系统包括:
堆内存外包 #
- Redis、memcached等
- 数据存储在内存中,支持复杂数据结构
- 当语言的内置数据结构效率较低时很有用
- 作为缓存非常好用
- 或者作为不同平台之间共享状态的快速临时存储
- 安全性一般
KV 存储 #
- Riak、Couch、Mongo、Cassandra、RethinkDB、HDFS等
- 键通常有1、2、3个维度
- $O(1)$ 的访问性能,有时可以进行基于ID的 $O(range)$ 范围扫描
- 值之间没有强关联
- 对象可以是不透明的或结构化的
- 大规模数据集
- 通常具有线性可扩展性
- 通常不支持事务
- 提供多种一致性模型,通常支持可选的线性化/顺序操作
SQL数据库 #
- Postgres、MySQL、Percona XtraDB、Oracle、MSSQL、VoltDB、CockroachDB等
- 由关系代数定义
- 中等规模的数据集
- 总是支持多记录事务
- 关系和事务需要协调,这降低了可扩展性
- 访问成本根据索引的使用情况而异
- 通常具有强一致性(SI、可串行化、严格可串行化)
搜索 #
- Elasticsearch、SolrCloud等
- 文档通过索引进行引用
- 中等到大规模的数据集
- 通常具有 $O(1)$ 的文档访问性能和类似日志的搜索能力
- 具有良好的可扩展性
- 通常具有较弱的一致性
协调服务 #
- Zookeeper、etcd、Consul等
- 通常具有强(顺序或线性)一致性
- 小规模数据集
- 充当无状态服务的协调原语
流处理系统 #
- Storm、Spark等
- 通常是自定义设计的系统,或者是用于构建自己系统的工具包
- 数据量通常较小且存储在内存中
- 低延迟
- 高吞吐量
- 弱一致性
分布式队列 #
- Kafka、Kestrel、RabbitMQ、IronMQ、ActiveMQ、HornetQ、Beanstalk、SQS、Celery等
- 通过多个节点将日志记录到磁盘以实现冗余
- 当需要立即确认接收到的工作并稍后处理时很有用
- 在无状态服务之间可靠地发送数据
- 我所知道的仅有一个在分区中不会丢失数据的队列是Kafka
- 可能还有SQS?
- 队列不会改善端到端延迟
- 立即执行工作总是更快的
- 队列不会改善平均吞吐量
- 平均吞吐量受消费者限制
- 在并发消费者的情况下,队列无法提供完全的事件顺序
- 但你的消费者几乎肯定是并发的
- 同样地,对于异步消费者,队列不能保证事件顺序
- 因为消费者的副作用 (side effects) 可能会无序发生
- 因此,不要依赖顺序
- 队列可以提供 at-most-once 或者 at-least-once 的消息传递
- 声称可以提供其他保证的人是在向您推销产品
- 要恢复确切的一次性交付需要对副作用进行仔细控制
- 使你排队的操作具有幂等性
- 队列可以提高突发吞吐量
- 平滑处理负载峰值
- 分布式队列还提高了容错性(如果不会丢失数据)
- 如果不需要容错性或大型缓冲区,请直接使用TCP
- 很多人使用一个需要进行六次磁盘写入和十五次网络跳转的队列,而只需一个socket的write()调用就足够了
- 队列可以在选择了不良运行时的情况下帮助你摆脱困境
总结 #
我们使用数据结构存储作为外包堆呢困:它们是分布式系统的"磁带"。KV存储和关系型数据库通常被部署为记录系统;KV存储使用独立的键,不适用于关系型数据,但相对于SQL存储来说具有更好的可扩展性和部分故障容忍性。分布式搜索和协调服务是构建应用程序的基本工具。流处理系统用于对数据集进行持续、低延迟的处理,更像是框架而不是数据库。它们的对应的分布式队列关注的是"消息"而不是"转换"。
模式语言 #
- 构建分布式系统的一般建议
- 艰难获得的经验
- 重复其他专家告诉我的内容
- 在喝啤酒的时候
- 听说而来
- 过度简化
- 盲目模仿
- 我自己编造的东西
- 个人经验可能会有差异
不要分布式 #
- 规则1:在不必要的情况下不要分布式
- 本地系统具有可靠的原语: Locks. Threads. Queues. Txns
- 当您转移到分布式系统时,必须从头开始构建。]
- 这个东西是否足够小,可以放在一个节点上?
- “我有一个大数据问题”
- Softlayer 可以租用一台配备3TB内存的服务器,价格为每月5000美元。
- Supermicro 可以出售一台配备6TB内存的服务器,总价约为11.5万美元。
- “我有一个大数据问题”
- 现代计算机速度很快。
- 我熟悉的生产上的 JVM HTTP 服务推送达到 50K req/sec
- 解析JSON事件,记录到磁盘,推送到 S3
- TCP 上的 proto buffer:每秒处理1000万个事件
- 10-100个事件 batch/messages,在内存中处理
- 我熟悉的生产上的 JVM HTTP 服务推送达到 50K req/sec
- 这个服务是否能够容忍单节点的保证?
- 如果它出现故障,我们能否只需再启动一个?
- 如果分布式算法可以通过手动干预实现,我们是否需要它?
- 本地系统具有可靠的原语: Locks. Threads. Queues. Txns
使用现有的分布式系统 #
- 如果我们必须分布式,能否将工作推到其他软件上?
- 分布式数据库或日志怎么样?
- 我们能否付费让 AWS 为我们处理这些事情?
- 相反,维护的成本是多少?
- 您需要学习多少才能使用/操作该分布式系统?
永不失败 #
- 购买非常昂贵的硬件
- 以受控方式对软件和硬件进行更改
- 对预发布环境进行试运行部署
- 可以构建非常可靠的网络和机器
- 以速度换取稳定性,购买更昂贵的硬件,寻找人才
- 硬件/网络故障仍然会发生,但发生频率低,因此优先级较低
接受故障 #
- 分布式系统不仅仅是延迟,还包括周期性的部分故障
- 我们是否能够接受这种故障并继续服务?
- 我们的服务等级协议是什么?
- 我们是否可以手动恢复?
- 我们是否可以付费请别人修复?
- 保险是否能够承担损失?
- 我们是否可以给客户打电话并道歉?
- 听起来有些愚蠢,但可能更便宜
- 我们永远无法防止100%的系统故障
- 有意选择在系统层面之上进行恢复
- 这就是金融公司和零售商的做法!
恢复优先 #
- 假设刚刚发生了故障:您将如何进行恢复?
- 将这种恢复设为默认执行路径
- 通过首先进行恢复编写代码可以避免对错误处理的犹豫
- 默认情况下执行恢复代码意味着你知道它可以工作
- 默认情况下的恢复意味着你无需担心在真正故障发生时出现不同的语义
- 如果必要,为性能优化引入happy path
- 但会失去部分优势!
协调循环 #
- 您拥有一个复杂的有状态系统,并希望将其移动到其他位置
- 可以制定一个变更计划,并按顺序应用这些变更
- 但如果某个变更出现问题怎么办?如何进行恢复?
- 相反,维护一个目标:对系统期望状态的表示
- 接下来,编写一个函数,检查当前状态,并将其与目标进行差异化
- 使用该差异找到将系统靠近目标的步骤
- 无限循环重复该过程
- 对故障和干扰具有鲁棒性
- 如果管理员手动调整系统会怎样?
- 如果控制系统的两个实例同时运行会怎样?
- 在 Borg和Kubernetes等系统中得到很好的应用
- 也适用于保持系统之间数据同步
- 例如, 确保每个订单都被发货和计费
备份 #
- 备份实质上是顺序一致性,但会丢失一段时间的操作。
- 如果正确执行
- 有些备份程序不会快照状态,这会导致文件系统或数据库损坏
- 破坏外键关系,丢失文件等等…
- 允许你在几分钟到几天内恢复
- 不仅用于故障恢复,还可以让你回到过去的某个时间点
- 用于从逻辑故障中恢复
- 分布式数据库正确执行了其工作,但你告诉它删除了关键数据
- 用于从逻辑故障中恢复
- 如果正确执行
冗余 #
- 好吧,故障是不可接受的选择
- 希望降低故障的概率
- 在多个节点上执行相同的计算,然后具有相同的状态
- 我不太相信主备模式
- 备份可能具有冷缓存、损坏的磁盘、旧版本等问题
- 当备份变为活动状态时,往往会失败
- 尽可能采用主-主模式
- 可预测性胜过效率
- 我也不太喜欢只有2个副本
- 节点故障的概率太高
- 对于不重要的数据可以接受
- 一般希望有三个数据副本
- 对于重要的数据,需要4或5个副本
- 对于Paxos等大多数 quorum 系统,选择奇数个:3、5、7较常见
- 常见的灾难恢复策略:在5个节点上使用Paxos;主数据中心保持3或4个节点
- 操作可以在本地节点确认后完成;延迟较低
- 可以抵御单节点故障(尽管延迟会上升)
- 但在另一个数据中心仍然有顺序一致的备份
- 因此,即使丢失了整个数据中心,情况也并非一片绝望
- 参见 Camille Fournier 关于ZK部署的演讲
- 我不太相信主备模式
分片 #
- 数据集太大或者需要并行计算
- 将问题分解成足够小的部分,以适应单个节点
- 不要太小:太小的部分会导致高开销
- 不要太大:需要逐渐的进行从节点到节点的工作负载平衡
- 大约每个节点有 10-100 个工作单元是理想的,我认为
- 理想情况下:工作单元的大小相等
- 注意热点
- 注意随时间变化的工作负载
- 提前知道你的边界
- 单个部分可以变得多大,什么时候可以超过一个节点的承受能力?
- 我们如何在它在生产环境中压垮一个节点之前强制限制它?
- 然后导致整个系统逐个节点失败,因为系统往往会自动重新平衡
- 为节点分配 shard
- 通常内置于数据库中
- ZK、Etcd等的良好候选者
- 参见Boundary的Ordasity
独立域 #
- 分片是避免协调的一种特殊情况
- 尽可能保持独立性
- 提高故障容错性
- 提高性能
- 减少复杂性
- 通过分片实现可扩展性
- 通过CRDT避免协调
- Flake ID:基本上按时间排序的标识符,无需协调
- 部分可用性:用户仍然可以使用系统的某些部分
- 处理队列:更多的消费者减少了昂贵事件的影响
- 尽可能保持独立性
ID 结构 #
- 我们的世界中的事物必须具有唯一的标识符
- 在规模上,ID结构可能会成就或毁掉您
- 考虑你的访问模式
- 扫描
- 排序
- 分片
- Sequential ID需要协调:你能否避免使用它们?
- Flake ID、UUID, …
- 对于可分片性,你的ID是否可以直接映射到分片?
- SaaS应用程序:对象ID还可以编码客户ID
- Twitter:推文ID可以编码用户ID
不可变值 #
- 永远不会更改的数据很容易存储
- 不需要协调
- 副本和恢复成本低
- 在磁盘上最小化重组
- 适用于Cassandra、Riak和任何LSM树数据库。
- 或者用于像Kafka这样的日志!
- 很容易理解:存在或不存在
- 消除各种令人头疼的事务
- 非常适合缓存
- 具有极高的可用性和可靠性,可调的写入延迟
- 读取延迟低:可以从最近的副本获得响应
- 对地理分布特别有价值
- 需要垃 gc!
- 但是有很好的方法来做到这一点
可变标识 #
- 指向不可变值的指针
- 指针很小!只有元数据!
- 可以在小型数据库上放置大量指针
- 对共识服务或关系数据库友好
- 通常系统中的指针不多
- 整个数据库可以由单个指针表示
- Datomic 只有约5个标识符
- 强一致性的操作可以由不可变的高可用存储支持
- 利用AP存储的延迟和规模优势
- 利用共识系统提供的对小数据集的强一致性
- 写入的可用性受标识的存储限制
- 但是,如果你只需要顺序一致性,则读取可被缓存
- 如果您只需要可串行化,甚至可以更便宜
- 参见Rich Hickey关于Datomic架构的演讲
- 参见Pat Helland在2013年RICON West上的主题演讲,介绍Salesforce的存储
Confluence (合流) #
- 不依赖顺序的系统更易于构建和推理
- 还可以帮助我们避免协调
- CRDT(Convergent and Commutative Replicated Data Types,收敛且可交换的复制数据类型)是一种具有 confluence 的数据类型,这意味着我们可以在不等待的情况下应用更新
- 不可变值通常是 confluence 的:一旦存在,就不会改变
- 流式系统也可以利用 confluence:
- 缓冲事件,并在确认已经接收到所有事件后进行计算和刷新
- 发送部分结果,以便立即采取行动,例如用于监控
- 在完整数据可用时,与 “+ “或 “max “进行合并
- 银行账本(大多数情况下)具有 confluence:交易顺序不影响余额
- 但是,当您需要强制执行最低余额时,就不再具有一致性
- 可以通过添加封存事件(例如当天结束)来恢复一致性
- 参考 Aiken、Widom 和 Hellerstein 在1992年的 “Behavior of Database Production Rules”(数据库生成规则的行为)一文
回压(Backpressure) #
- 相互通信的服务通常通过队列连接
- 服务和队列容量是有限的
- 当下游服务无法处理负载时,您如何处理?
- 消耗资源并崩溃
- 丢弃负载。开始丢弃请求。
- 拒绝请求。忽略工作并告知客户端请求失败。
- 对客户端应用回压,要求它们减慢速度。
- 2-4 允许系统赶上并恢复
- 但回压会减少需要重试的工作量
- 回压将选择权交给生产者:具有组合性
- 减载系统的客户被锁定在减载中
- 他们无法知道系统已经崩溃
- 回压系统的客户端可以对它们的客户端施加回压
- 或者选择丢弃负载
- 如果您正在构建一个异步系统,一定要包含回压机制
- 您的用户以后会感谢您的
- 减载系统的客户被锁定在减载中
- 从根本上说:资源有界限
- 请求超时(有界时间)
- 指数回退(有界使用)
- 有界队列
- 有界并发性
- 参考 Zach Tellman 的 “Everything Will Flow”(一切将会流动)
面向领域模型的服务(Services for domain models) #
- 问题由相互作用的逻辑组件组成
- 组件具有不同的代码、性能和存储需求
- 单体应用程序本质上是多租户系统
- 多租户很困难
- 但同一个进程中运行多个逻辑 “服务 “通常是可行的
- 单体应用程序本质上是多租户系统
- 将系统划分为领域模型的离散部分的逻辑服务
- 面向对象方法:每个名词是一个服务
- 用户服务
- 视频服务
- 索引服务
- 函数式方法:每个动词是一个服务
- 鉴权服务
- 搜索服务
- 调度/路由服务
- 我所知道的大多数大型系统都使用混合方法
- 名词服务是强制执行数据类型不变性的一种好方法
- 动词服务是强制执行转换不变性的一种好方法
- 因此,有一个基本的用户服务,由一个鉴权服务使用
- 在哪里划分边界…这很棘手
- 服务会带来开销:应尽可能少地使用
- 考虑工作单元
- 分离需要独立扩展的服务
- 将延迟依赖紧密的服务放在一起
- 将使用互补资源(例如磁盘和 CPU)的服务放在一起
- 手动方式:在渲染节点上运行内存缓存
- 较新的方法:Google Borg、Mesos、Kubernetes
- 服务应该封装和抽象
- 尽量构建树状结构而不是网状结构
- 避免外部操作服务的数据存储
- 面向对象方法:每个名词是一个服务
结构遵循社会空间(Structure Follows Social Spaces) #
-
生产软件是一种基本上是社会性的产物
-
自然的对齐方式:一个团队或个人负责特定的服务
- Jo Freeman,The Tyranny of Structurelessness
- 责权利应该清晰
- 轮流担任角色以防专治
- 促进信息共享
- 但不要轮换得太频繁
- 软件的适应成本非常高
- Jo Freeman,The Tyranny of Structurelessness
-
随着团队的增长,其使命和思维方式将得到规范化
- 同样,服务及其边界也将得到规范化
- 逐渐积累有关服务与世界之间关系的假设体
-
由于外部压力的变化,需要进行重写
- Tushman 和 Romanelli,在1985年的 “Organizational Transformation as Punctuated Equilibrium”(组织变革的阶段性平衡)中提出
-
服务可以是库
- 最初,所有服务都应该是库
- 完全可以依赖于用户库在多个服务中使用
- 具有明确定义边界的库很容易后续转化为服务
-
社会结构统治库/服务边界
- 对于很少用户或紧密协调的用户,更改很容易
- 但是在许多团队之间,用户的优先级各不相同,必须说服他们
- 为什么用户要付出工作升级到新的库版本?
- 通过定义的 API 弃用生命周期来强制用户协调
- 也可以通过代码审查和工具来强制执行这一点,即使是对库也是如此
-
服务使得集中控制成为可能
- 你的性能改进立即影响所有人
- 逐渐过渡到新的磁盘格式或后备数据库
- 在一个地方对服务的使用进行仪表化
- 通过库很难做到这些事情
-
服务有成本
- 网络调用的故障复杂性和延迟开销
- 服务依赖的复杂的食物链
- 难以静态分析代码路径
- 你认为库的 API 版本控制很困难
- 需要额外的仪表化/部署
-
服务可以使用良好的客户端库
- 该库可以是 “打开一个套接字 “或 HTTP 客户端
- 利用 HTTP header !
- 用于版本控制的 Accept header
- 对于缓存和代理有很多支持
- Haproxy 是非常出色的 HTTP 和 TCP 服务路由器
- 利用 HTTP header !
- 最终,该库可能包括模拟 I/O
- 服务团队负责测试服务提供 API 的功能
- 当 API 已知稳定时,每个客户端可以假设它正常工作
- 不需要在测试套件中进行网络调用
- 大大减少了测试运行时间和开发环境的复杂性
- 该库可以是 “打开一个套接字 “或 HTTP 客户端
跨服务协调(Cross-service coordination) #
- 服务之间的协调需要特殊的协议
- 必须重新发明事务
- 尽可能采用可交换的方式
- Saga
- 最初是为单节点编写的:在分布式环境中需要巧妙处理
- 事务必须具有幂等性,或者与回滚具有交换性
-
Typhon/Cerberus
- 用于为多个数据存储因果一致性的协议
- 例如,如果 Lupita 拉黑了 Miss Angela,然后发布了一篇帖子,Miss Angela 就不能看到
- Typhon:单个逻辑实体在不同数据存储中具有数据项表示
- 假设数据存储是可串行化的,或者提供对数据项的原子读/写操作
- 访问同一实体并且 T1 发生在 T2 之前的事务具有因果依赖关系边 T1 -> T2
- Cerberus:涉及单个实体 x 的事务的协议
- 写操作只能影响 x 的一个表示
- 可以在 x 的不同表示之间进行任意数量的读取
- 全局元数据:每个实体的版本向量(GVV)
- 每个表示的元数据:
- 更新版本向量(UVV):上次更新时已知的版本
- 读取版本向量(RVV):上次读取时已知的版本
- 当 GVV < UVV/RVV 时,检测到冲突
- 两个阶段:
- 读取阶段
- 检查实体 x 的 GVV
- 在每个表示上对 x 进行读取(所要求的)
- 在每个表示上:检查 RVV <= GVV,更新 RVV
- 写入阶段
- 发送写入到表示
- 检查 UVV <= GVV & RVV <= GVV
- 提交
- 更新表示和 RVV/UVV,确保 RVV/UVV 未更改
- 通过增加现有 UVV 的第 i 个条目构建新的 UVV
- 读取阶段
- 用于为多个数据存储因果一致性的协议
- 通用事务
-
Calvin
- 可串行化(或严格 1SR)事务
- 确定性事务入队到分片的全局日志
- 日志确保事务顺序
- 在副本/分片上的应用不需要进一步的协调
- 日志窗口的最小延迟
-
CockroachDB
- 可串行化
- 假设具有线性一致性的存储
- 假设半同步时钟
- 类似于更容易操作的 Spanner
-
Calvin
迁移(Migrations) #
- 迁移很困难
- 没有万能的解决方法
- 但是有些技术可以使你的生活更轻松
- 硬切换
- 编写新系统和迁移,将旧数据复制到新系统中
- 依赖服务同时与两个系统通信,但实际上只与一个通信
- 关闭旧系统
- 复制数据
- 启动新系统
- 折衷!
- 不必担心正在进行的数据
- 简单的迁移脚本:只需读取所有数据并写入新的数据存储
- 需要与迁移脚本成比例的停机时间
- 有时可以将其范围限制为单个分片/用户/域
- 渐进式
- 编写新系统 B
- 与原始系统 A 并行部署
- 依赖服务同时与两者通信
- 理想情况:找到所有读取者,让每个读取者与 A 和 B 都通信
- 然后开始向 B 写入
- 这样就不必担心只知道 A 的读取者
- 一致性的噩梦;需要跟踪所有数据依赖关系
- 折衷!
- 减少或无停机时间
- 但是需要对数据依赖关系进行复杂推理
- 封装服务
- 从操作角度来看,找到并更改所有使用 A 的用户可能很棘手
- 那就别找了。引入一个代理服务 W,代理到 A
- 引入 B,并进行更改,使得 W 也与 B 通信
- 当 A 被淘汰时,删除 W 并直接与 B 通信
- 允许集中度量、错误和行为比较等
- 最终的原子性
- 假设您将每个更新都写入旧服务 A,然后写入新服务 B
- 在某个时刻,您对 A 的写入将成功,而 B 的写入将失败。那么怎么办?
- 可以使用读取修复:读取 A 和 B,填充丢失的更新
- 但这需要可合并性:只适用于像 CRDT 这样的东西
- 可以使用协调过程
- 迭代整个数据库,查找更改,应用到两者
- 也需要类似于 CRDT 的东西
- 可以使用 Saga
- 所有更新都发送到持久化队列
- 队列工作者重试,直到更新应用到 A 和 B
- 可能需要对更新进行排序以避免状态发散
- 可能需要全局序列化
- 注意数据库的一致性模型
- 隔离性
- 假设 Jane 将 w1 写入 A,然后 B
- 与此同时,Naomi 将 w2 写入 B,然后 A
- 结果:A = w2,B = w1
- 最终的原子性无法防止发散
- 可以通过选择一个标准顺序来减少问题:始终是 A 然后 B(或 B 然后 A)
- 但是想象一下
- Jane 写入 A = w1
- Naomi 写入 A = w2
- Naomi 写入 B = w2
- Jane 写入 B = w1
- 我们再次得到混合结果。糟糕。
- 但是想象一下
- 可以使用 CRDT 来缓解
- 或者,如果 A 和 B 是顺序一致的,则可以使用 CaS 操作确保顺序一致
- “当且仅当最后一个写入是 w1 时,才写入 w2”
- 如果操作影响多个键,则必须在该级别上应用 CaS 逻辑
- 渐进式迁移的有用属性
- 确定性
- 避免让数据库生成随机数、自动 ID、时间戳等
- 更容易将更新应用于两个数据存储并得到相同的结果
- 幂等性
- 允许您自由重试更新
- 可交换性
- 消除了序列化更新的需求
- CRDT:结合律、交换律、幂等性
- 不变性:平凡的 CRDT
- 无状态性:无需担心状态!
- 只需确保以相同的方式与外部有状态的内容进行交互
- 确定性
- 那交换队列怎么办?
- 正如我们所说,队列系统应该已经设计为具有幂等性,并且理想情况下具有交换性
- 如果是这样,那么这是(相对)容易的
- 工作者从两个队列中消费
- 将生产者切换为仅发送消息到新队列
- 等待旧队列耗尽
- 停止使用旧队列
- 但是我们想要顺序吗?
- 你将需要重建它
- 一个选项:单个生产者紧密耦合到队列
- 将每个消息 m 写入 A 和 B;只有在两者都确认后才继续
- 这确保了 A 和 B 对顺序的一致性
- 消费者可以将 A 和 B 视为相同:只从 A 或 B 消费
- 再次假设幂等性!
- 正如我们所说,队列系统应该已经设计为具有幂等性,并且理想情况下具有交换性
回顾 #
在可能的情况下,尽量使用单节点而不是分布式系统。接受一些无法避免的故障:服务级别协议和道歉可能是成本有效的。为了处理灾难性故障,我们使用备份。为了提高可靠性,我们引入冗余。为了解决大规模问题,我们将问题划分为分片。不可变值易于存储和缓存,并且可以被可变标识引用,使我们能够构建大规模的强一致性系统。随着软件的发展,不同的组件必须独立扩展,我们将库拆分为独立的服务。服务结构与团队紧密相关。
生产关注事项 #
- 不仅仅是设计上的考虑
- 证明很重要,但真实系统会进行 IO 操作
分布式系统需要文化支持 #
- 在生产环境中理解分布式系统需要不同角色的人紧密合作
- 开发人员
- 质量保证(QA)
- 运维
- 具备共情能力很重要
- 开发人员需要关注生产环境
- 运维人员需要关注实现细节
- 良好的沟通能够加快故障诊断速度
测试一切 #
- 类型系统非常适合预防逻辑错误
- 减轻了测试的负担
- 但是,类型系统不擅长预测或控制运行时性能
- 因此,您需要一个可靠的测试套件 (test suite)
- 理想情况下,您希望有一个严谨的的 “滑块 (slider)”
- 基于 example 的快速测试,在几秒内运行
- 基于 property 的更详细测试,可以在夜间运行
- 能够在进程内模拟整个集群
- 使用模拟网络并控制并发交错
- 可以自动化硬件故障
- 测试分布式系统比测试本地系统要困难得多
- 存在大量您从未听说过的故障模式
- 组合状态空间
- Bug 可能仅在小型/大型/中间时间/空间/并发下表现出来
“它很慢” #
- Jeff Hodges:你可能听到的最糟糕的错误是 “它很慢”
- 这种情况经常发生,定位问题非常困难
- 由于系统是分布式的,必须对多个节点进行性能分析
- 很少有性能分析工具是为此而构建的
- Sigelman 等人,2010年:Dapper, a Large-Scale Distributed
- Zipkin
- 需要大量的工具投入
- Profilers 工具擅长查找 CPU 问题
- 但高延迟通常是 IO 问题,而不是 CPU 问题的标志
- 磁盘延迟
- 网络延迟
- 垃圾回收延迟
- 队列延迟
- 尝试使用应用程序级别的指标定位问题
- 然后深入研究进程和操作系统的性能
- 不同节点之间的延迟变化是一个重要信号
- 1/3 节点慢:可能是节点硬件问题,重新路由
- 3/3 节点慢:可能是逻辑故障:查看分片大小、工作负载、查询等
- 尾延迟在扇出工作负载下会被放大
监控一切 #
- 生产中的缓慢(甚至是错误)来源于系统之间的交互
- 为什么?因为你的测试套件可能只验证了单个系统的正确性
- 所以我们需要一种方法来了解系统在生产环境中的运行方式
- 与其依赖关系相关联
- 这反过来可以驱动新的测试
- 在某种程度上,良好的监控就像持续的测试
- 但不是替代品:这是不同的领域
- 两者都可以确保你的更改没有问题
- 希望高频监控
- 生产行为可能在1ms的时间尺度上发生
- TCP incast
- 理想情况下,约为1ms的分辨率
- 运维响应时间在极限情况下与观察延迟成正比
- 大约1s的端到端延迟
- 理想情况下,毫秒级延迟,可能还有毫秒级分辨率
- 通常代价较高;降低到1s或10s
- 有时可以容忍60s
- 生产行为可能在1ms的时间尺度上发生
- 对于容量规划,每小时/每日的季节性更加有用
- 仪表化应该与应用程序紧密耦合
- 仅测量重要的内容
- 响应请求很重要
- 节点 CPU 的重要性不如前者
- 大多数系统的关键指标
- Apdex:在延迟 SLA 内成功响应
- 延迟分布:0、0.5、0.95、0.99、1
- 百分位数,而不是平均值
- 顺便说一句,你不能计算百分位数的平均值
- 总吞吐量
- 队列统计信息
- 其他系统延迟/吞吐量的主观体验
- 数据库可能认为它很健康,但客户端可能认为它很慢
- 组合爆炸——在排查故障时最好使用此功能
- 你可能需要自己编写这些仪表代码
- 投资一个监控库
- 仅测量重要的内容
- 开箱即用的监控通常无法测量真正重要的内容:你的应用程序行为
- 但在查找问题原因时它可以非常有用
- 主机指标,如 CPU、磁盘等
- 在应用程序执行常见操作(例如 Rails 应用程序)时,New Relic 等工具效果良好
- 按客户端划分指标
- 当用户具有不同的工作负载时很有用
- 可以根据客户端调整阈值,使其适用于该客户端
- 为主要客户设置一些指标,为 “其他” 客户设置另一个指标桶
- 超能力:分布式跟踪基础设施(Zipkin、Dapper 等)
- 需要投入大量时间
-
Mystery Machine
- 从跟踪数据中自动推断服务之间的因果关系
- 识别关键路径
- 在实施之前对新算法进行性能建模
日志记录 #
- 在规模上,日志记录的用处较小
- 问题可能不仅局限于一个节点
- 当请求涉及更多服务时,必须追踪多个日志文件
- 投资日志收集基础设施
- ELK、Splunk 等
- 非结构化信息更难以聚合
- 日志结构化事件
- 问题可能不仅局限于一个节点
影子流量 #
- 负载测试仅在模拟负载与实际负载相匹配时才有用
- 考虑镜像生产流量
- 可以通过使用 SIGUSR1 终止进程来转储五分钟的请求负载,非常方便
- tcpdump/tcpreplay 用于请求的工具
- 在演练/质量保证(QA)节点上跟踪实时生产流量
- 参考 Lyft 的 Envoy
版本控制 #
- 协议版本控制是一个尚未解决的问题
- 在所有消息中包含版本标签
- 包含兼容性逻辑
- 当无法满足客户端请求时通知客户端
- 并记录此信息,以便知道哪些系统需要升级
滚动升级 (Rollouts) #
- 部署通常是解决问题的方式
- 花时间进行自动化、可靠的部署
- 放大你进行的所有其他工作
- 使节点顺利过渡,避免流量中断
- 这意味着你将同时运行多个版本的软件
- 版本控制变得复杂
- 这意味着你将同时运行多个版本的软件
- 通知负载均衡器,将节点从轮询中移出
- 协调以防止级联故障
- 仅将新软件部署到一部分负载或用户
- 逐渐增加新软件上的用户数量
- 在出现错误时回滚或继续前进
- 考虑在生产中跟踪流量,比较旧版本和新版本
- 判断新代码是否更快且正确
自动化控制 #
- 自动化故障处理是有益的
- 但不要太多
- “自动化的讽刺”,Bainbridge 1983年
功能开关 (Feature flags) #
- 我们希望在部署后逐步推出更改集的增量式发布
- 逐个引入功能,观察它们对指标的影响
- 逐渐将负载从一个数据库转移到另一个数据库
- 当发布出现问题时禁用功能
- 当某些服务降级时,我们希望获得部分可用性
- 禁用昂贵的功能以加快故障恢复速度
- 使用高度可用的协调服务来决定启用哪些代码路径或多久启用一次
- 该服务应该具有最少的依赖关系
- 不要使用主要数据库
- 该服务应该具有最少的依赖关系
- 当出现问题时,你可以"调整"系统的行为
- 当协调服务不可用时,进行"安全"的故障处理!
混沌工程 #
- 在生产环境中故意进行爆破
- 强迫工程师立即适当地处理故障,而不是等到事故发生后再处理
- 识别关键路径上的意外依赖项
- “当新的统计服务宕机时,API 也会宕机。你确定这是必要的吗?”
- 需要良好的仪表化和报警系统,以便可以测量事件的影响
- 有限的爆炸范围
- 不要每五分钟摧毁整个数据中心
- 但每个季度尝试一次也可以
- 不要破坏复制组中的太多节点
- 一次只破坏一小部分请求/用户
- 不要每五分钟摧毁整个数据中心
哦,不,队列 #
- 每个队列都是发生严重错误的地方
- 没有节点具有无限的内存。你的队列必须有界限
- 但有多大?没有人知道
- 在生产环境中仪表化您的队列以找出答案
- Little 定律:mean queue depth = mean arrival rate * mean latency
- 这与分布式无关!
- 队列的存在是为了平滑处理负载波动
- 以牺牲延迟为代价提高吞吐量
- 如果负载高于容量,没有队列能够拯救你
- 当队列已满时,丢弃负载或回压(backpressure)
- 进行仪表化
- 发生负载丢弃时,应触发警报
- 回压可通过上游延迟可见
- 仪表化队列深度
- 高深度提示需要增加节点容量
- 端到端队列延迟应小于波动时间尺度
- 提高队列大小可能很诱人,但这是一个恶性循环
- 高深度提示需要增加节点容量
- 所有这些都很困难,我没有好的答案
- 问问 Jeff Hodges 为什么这很困难:参考他的 2013 年 RICON West
- 看一下 Zach Tellman - Everything Will Flow
回顾 #
运行分布式系统需要开发人员、质量保证(QA)人员和运维工程师之间的合作。静态分析和包含示例和基于属性的测试的测试套件可以帮助确保程序的正确性,但要了解生产行为,需要全面的监控和报警。成熟的分布式系统团队通常会在工具方面进行投资,包括流量镜像、版本控制、渐进式部署和功能开关。最后,队列需要特别注意。
进一步阅读 #
在线资源 #
- Mixu有一本详细介绍分布式系统的精彩书籍:http://book.mixu.net/distsys/
- Jeff Hodges提供了一些非常实用的面向生产的建议:https://www.somethingsimilar.com/2013/01/14/notes-on-distributed-systems-for-young-bloods/
- 《分布式计算的错误观点》是关于我们在设计分布式系统时犯的错误假设的经典著作:http://www.rgoarchitects.com/Files/fallacies.pdf
- Christopher Meiklejohn列出了分布式系统的关键论文清单:http://christophermeiklejohn.com/distributed/systems/2013/07/12/readings-in-distributed-systems.html
- Dan Creswell有一个很棒的阅读列表:https://dancres.github.io/Pages/
书籍 #
- Martin Kleppmann的 Designing Data-Intensive Applications 为实践者提供了对分布式系统的全面介绍。
- Nancy Lynch的 “Distributed Algorithms” 是对该领域的全面概述,从更理论的角度进行阐述。