DDIA 第一章 可靠、可扩展、可维护的应用

数据密集型应用

现在的很多应用是数据密集型的,数据是这些应用的主要挑战-数据的总量、数据的复杂度和数据变化的速度。

很多数据密集型的应用都是基于已有的数据系统提供的常用功能来构建的。例如:

  • 存储数据,以便自己或其他应用能够找到(基于数据库实现此功能)
  • 记住一个昂贵操作的结果,来加速读取(缓存)
  • 允许用户通过关键词搜索数据,或多种方式过滤数据(搜索索引)
  • 发送消息到另一个进程进行异步处理(流式处理)
  • 定时处理大量累积的数据(批处理)

由于数据系统的抽象,这些功能都看似很简单。但是,数据系统在逐渐变得相似,不同的数据系统可能同时具有多种特性,例如:Redis的cache和消息队列功能;应用程序越来越宽泛的需求使得单一数据系统无法完成,需要使用程序代码来组合不同的数据系统;同一需求,可能有多种方式和数据系统来实现。因此选择合适的数据系统和权衡架构的设计是一个值得思考的问题。

设计数据系统的原则

  • 可靠性(Reliability) 就算出现问题的时候(硬件或软件错误,人为错误),系统都应该应该持续的正确工作(在期望的水平上提供正确的功能)。
  • 可扩展性(Scalability) 随着系统的增长(数据量、流量或数据复杂度),应该有合理的方法来应对这些增长。
  • 可维护性(Maintainability) 随着时间的推移,许多人都会参与系统相关的工作(开发和运维,他们保证系统现有行为正常、并使系统能够应对新的情况),他们应该高效的工作。

可靠性

系统具有容错性(fault-tolerant或resilient)。系统在遇到特定错误的时候,也能按预期正常工作。

这里原文特别区别了fault和failure。fault指的是系统组件的运作偏离了预期,而failure指的是系统整体无法提供服务。设计一个完全没有fault的系统是不可能的,但是可以通过设计fault-tolerant机制来避免fault导致failure。 下面,failure会写为系统故障。

硬件故障 每个硬件都有预期的寿命,系统使用的硬件规模够大、运行时间够长时,硬件总会出故障。

通过软件和硬件层面冗余的方式,可以避免出现硬件故障时无法提供服务。

软件错误 硬件错误的发生相对独立,但是软件错误更加难以预期,往往会导致很多的系统错误。

通过完善的考虑系统的假设和交互、测试、进程隔离、监控、允许进程挂了后重启等方式来避免软件错误。

人为错误 系统的设计、构建和运维是由人来进行的,但人是不可靠的。

通过如下方式来减少人为错误: * 以最小化错误机会的方式来设计系统。例如:设计良好的抽象、API和管理界面来避免“做错误的事”。 * 解耦人们最容易犯的错和犯错的地方。例如:提供完整功能的非生产环境sanbox。 * 完善的测试。例如:从单元测试到整个系统集成测试。 * 提供快速和简单的错误恢复机制。例如:快速的回滚配置,上线新代码应逐步从小范围内到大范围,提供重新计算数据的工具来修复老数据错误。 * 详尽和清晰的监控。例如:性能计数和错误率。

可扩展性

即便一个系统现在能够可靠的工作,这不意味着未来也能。一个常见的原因就是负载的增加。当讨论可扩展性时,常常需要考虑,“如果系统以一个特定的方式增长,应该怎么办?”,“如何增加计算资源来应对额外的负载?”。

描述负载 首先需要简洁的描述负载,负载可以使用几个数字来描述,叫做负载参数(load parameters),参数的选择依赖于系统的架构,例如: * web服务器每秒的请求个数。 * 数据库的读写比例。 * 聊天室同时活跃的用户数。 * cache命中率。

描述性能 一旦有了系统负载的描述,那么可以讨论负载增加时会发生什么,可以有两个角度: * 负载增加时,如果保持系统资源不变,系统的性能会怎样? * 负载增加时,为了保持系统性能不变,需要增加多少资源?

讨论这两个问题需要描述性能, 1. 吞吐量(批处理系统) 2. 响应时间(在线系统) * 平均响应时间。 * 百分比响应时间。 例如:p50,取time的中位数,如果是200ms,那么代表50%的响应时间小于200ms。类似的还有p90,p99。 * 高百分比响应时间,又叫做尾部延迟(tail latencies)。

性能与可用性一起被用在SLOs(service level objectives)和SLAs(service level agreements)中,规约定义了服务期望的性能和可用性。

高百分比响应时间常常受队列延迟(queueing delay)的影响,少量慢请求会阻塞后续请求的处理,这个现象又叫做head-of-line blocking。

如果一个请求需要进一步使用更多的后端调用来完成,那么一个较慢的调用就会拖慢整个请求,这叫做尾部延迟放大(tail latency amplification)。

应对负载的方法 1. scaling up 垂直缩放,迁移到性能更强的机器。 2. scaling out 水平缩放,把负载分布到多个小机器上。也叫做share-nothing architecture。

实际工程上可能会混用两种方法,几个性能较强的机器可能比非常多的小机器来的划算。把无状态的服务分布到多个机器较为容易,但是把一个有状态的数据系统分布式化会引入很多额外的复杂性。

大规模分布式系统架构往往是针对应用高度定制化的,并不存在一个通用的分布式架构。因为要解决的问题可能是读为主、写为主、大量数据的存储为主、数据的复杂度、响应时间、访问方式,或者是前面各种因素的混合。

可维护性

软件开发的主要成本并不在最初的开发,而是持续的维护-修bug、运维、排错、兼容性的平台等。主要关注以下方面, 1. 可操作性,便于运维团队的维护。 需要提供: * 完善的监控 * 自动化和集成工具 * 避免依赖特定的机器 * 文档 * 良好的默认行为,并提供修改默认行为的方法 * 自我恢复,并提供手动操作的方法 * 可预测的行为 2. 简单,管理复杂性,便于其他开发者理解系统。 好的抽象可以隐藏复杂的细节。 3. 可进化,便于修改和增加系统功能,又叫做可扩展性(extensibility)、可修改性(modifiability)、可塑性(plasticity)。 增加数据系统的敏捷性。