第一章:可靠性,可扩展性,可维护性
很多应用程序都是 数据密集型(data-intensive) 的,而非 计算密集型(compute-intensive) 的。
CPU 很难成为这类应用的瓶颈。
数据密集型应用通常的组成:
- 数据库(database)
- 缓存(cache)
- 搜索索引(search indexes)
- 流处理(stream processing)
- 批处理(batch processing)
当单个工具不能解决问题的时候,组合这些工具将会成为难题。
数据系统:数据库、消息队列、缓存等,为什么要混为一谈?类别之间的界限变得越来越模糊,例如 Kafka 也会持久化数据,Redis 也能当消息队列。
在一个多组件的数据系统中,有如下重要的问题:
可靠性(Reliability):系统在 困境(adversity)(故障等)中还可以正常运行,也可以叫做高可用?
- 能预料并且应对 故障(fault) 的系统特性叫做 容错(fault-tolerant)或韧性(resilient)
- 容错也是有范围的,不是极端的容忍所有的错误
- 故障(fault) 不是 失效(failure),后者是完全不可用了
- 最好的容错是防止因为故障而导致失效
- 故意触发故障来提供容错能力是有意义的
- 比起 阻止错误(prevent error),我们通常更倾向于容忍错误,但是一些特殊情况除外,例如安全问题,敏感数据被盗,这样还是要防患于未然
- 硬件故障:云平台的设计就是优先考虑 灵活性(flexibility) 和 弹性(elasticity),而不是单机可靠性。毕竟出错在数学期望上来讲,在所难免(毕竟机器多了,就成了必然)
- 软件错误:硬件故障是相互独立的,系统性错误(systematic error) 往往会造成更多的 系统失效,一些小办法:
- 彻底的测试
- 进程隔离
- 允许进程、服务崩溃之后重启
- 提供一些监控手段,出现差异报警
- 人为错误 办法:
- 以最小化犯错机会的方式设计系统:设计抽象、API和管理后台让事情变得容易,同时要注意平衡,过于复杂人们会想办法绕开
- 容易出错的地方 解耦(decouple),沙箱(sandbox)
- 单元测试、手动测试、自动化测试帮助测试 边缘场景(corner case)
- 允许简单的恢复:回滚,灰度,数据重推脚本之类的
- 配置详细和明确的监控,遥测(telemetry),指标数据,监控数据,指标数据也容易诊断数据
- 好的管理
- 当你想偷工减料来牺牲可靠性的时候,希望你知道成本,这意味着什么。
可扩展性(Scalability):有办法应对系统的增长,水平扩容,垂直扩容
- 服务 降级(degradation) 的一个常见原因是负载增加
- 描述负载,负载参数(load parameters) 数字描述,例如 QPS、数据库 IOPS、缓存命中、业务相关的指标等,少数极端场景
- 描述性能,接下来就可以研究增加负载会发生什么:
- 描述系统性能:
- 如果你想知道“典型(typical)”响应时间,那么平均值并不是一个非常好的指标
- 使用 百分位点(percentiles) 会更好:如果将响应时间列表按最快到最慢排序,那么 中位数(median) 就在正中间:举个例子,如果你的响应时间中位数是 200 毫秒,这意味着一半请求的返回时间少于 200 毫秒,另一半比这个要长。
- 中位数也被称为第 50 百分位点,有时缩写为 p50
- 为了弄清异常值有多糟糕,可以看看更高的百分位点,例如第 95、99 和 99.9 百分位点
- 响应时间的高百分位点(也称为 尾部延迟(tail latencies))非常重要,往往响应最慢的用户数据也最多,他们花的钱也就越多,价值越大
- 优化第 99.99 百分位点(一万个请求中最慢的一个)被认为太昂贵了,随机事件已经占了上风,收益不大
- 百分位点通常用于 服务级别目标(SLO, service level objectives) 和 服务级别协议(SLA, service level agreements)
- SLA 可能会声明,如果服务响应时间的 p50 小于 200 毫秒,且 p999 低于 1 秒,则认为服务工作正常,否则则不正常
- 排队延迟(queueing delay) 通常是影响高百分位的原因,服务器 CPU 只能并行处理少量事务(例如 CPU 核心数量),这时只要有少量的慢请求就会影响到后续请求,这种也被叫做 头部阻塞(head-of-line blocking),所以测量客户端响应时间非常重要
- 在多重调用的后端服务里,高百分位数变得特别重要。木桶原理,用户需要等待最慢的那个后端请求
- 百分位点计算方法:简单方法:保存所有请求的响应时间并排序;节约 CPU 内存方法:前向衰减,t-digest 或 HdrHistogram 计算近似值。(这让我想起了 HyperLogLog,基于概率的基数统计算法,还有 bitmap)
- 应对负载的方法:
- 适应某个级别负载的架构不太可能应付10倍于此的负载。
- 纵向扩展(scaling up)(垂直扩展(vertical scaling),转向更强大的机器)和 横向扩展(scaling out)(水平扩展(horizontal scaling),将负载分布到多台小机器上)
- 跨多台机器分配负载也称为“无共享(shared-nothing)”架构
- 优秀的架构是这两种方式结合,几台强大的机器可能比很多小型虚拟机更便宜
- 弹性(elastic) 扩容:在检测到负载增加时自动增加计算资源
- 如果负载 极难预测(highly unpredictable),则弹性系统可能很有用,但手动扩展系统更简单
- 跨多台机器部署 无状态服务(stateless services) 非常简单,但将带状态的数据系统从单节点变为分布式配置则可能引入许多额外复杂度
- 常识是数据库一般为纵向扩展,直到扩展成本昂贵或者已经到达瓶颈,这时候再把数据库改为分布式
- 分布式数据库越来越好,常识逐渐被改变,预见将来分布式数据库将会成为默认配置,即使是处理数据量不大的场景
- 没有 万金油(magic scaling sauce) 的可扩展架构
- 一个良好的可扩展架构是围绕 假设(assumption) 建立的,例如哪些是常见操作(负载参数)?但是早期的创业公司基本上是先支持业务快速迭代优先,没有时间给你设计和假设
可维护性(Maintainability):我所理解的,所谓功能的扩充,不同的人都可以良好的合作
- 软件的大部分开销并不在最初的开发阶段,而是在持续的维护阶段:bug 修复,偿还技术债,新功能,保证正常运行等
- 设计之初尽可能的为后面着想
- 可操作性(Operability):便于运维团队操作
- 简单性(Simplicity):便于新人入伙
- 可演化性(evolability):便于新增功能
- 可操作性:人生苦短,关爱运维
- 总计如下:良好的监控(可见性(visibility))、跟踪问题、及时打补丁、了解系统之间相互作用、预测(加机器,加硬盘)、自动化工具(原文有好的总结,这里简单总结下)
- 简单性:管理复杂度
- 项目越大,代码越容易变得复杂难以理解:状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的Hack、需要绕开的特例
- 消除 额外的(accidental) 的复杂度:由具体实现中涌现,而非(从用户视角看,系统所解决的)问题本身固有的复杂度
- 用于消除额外复杂度的最好工具之一是 抽象(abstraction):高级语言是对机器码、寄存器等的抽象
- 抽象虽然可以帮我们管理复杂度,但是一个好的抽象是困难的
- 可演化性:拥抱变化
- 敏捷(agile) 工作模式为适应变化提供了一个框架
- 修改数据系统并使其适应不断变化需求的容易程度,是与简单性和抽象性密切相关的
使应用可靠、可扩展或可维护并不容易,另外本章也让我认识到了监控的重要性,无论是业务维度的监控还是底层机器维度的监控。这种可见性的监控展示也会让人心里有底,当出现异常的时候也会报警通知,也能发现一些难以察觉到的小问题。