DDIA-1-可靠可扩展可维护的应用系统

数据密集型应用的模块

当今许多应用大多是数据数据密集(data-intensive)而不是计算密集型(compute-intensive)的。所以CPU的处理能力往往不是应用程序的瓶颈。关键在于数据的量、数据的复杂度以及数据的快速多变性。

应用往往包含以下模块:

  1. 数据库:用于存储数据
  2. 高速缓存:缓存复杂或者操作代价昂贵的结果,加快下一次访问
  3. 索引:用户可以按照关键字搜索数据并支持各种过滤
  4. 流式处理:持续发送消息到另一个进程,处理采用异步方式
  5. 批处理:定期处理大量积累的数据

数据系统的架构

核心设计目标

可靠性

出现意外情况,比如硬件、软件故障、人为失误等,系统可以继续正常运转,至少确保功能正确。

硬件故障

比较容易出现的,硬盘崩溃,内存故障,电网停电。

第一反应是为硬件冗余来减少系统故障率。例如磁盘RAID,服务器双电源,甚至热插拔CPU,数据中心添加备用电源、发电机。

这样当一个组件发生故障时,冗余组件可以快速接管,之后运维人员可以修复或者更换坏掉的组件。

直到最近,采用硬件冗余方案对于大多数应用场景还是足够的,它让单机完全失效的概率降到最低。只要可以把备份迅速恢复到新的机器上,故障的停机时间在大多数应用中并不是灾难性的。

现在,通过软件容错的方式来容易多机失效成为新的手段,或者成为硬件容错方案的有力补充。例如滚动升级。

软件错误

这类故障更难预料。各个节点直接是由软件关联的,可能会导致更多的系统故障。

例如,

  1. 由于软件错误,导致特定的输入引发应用的崩溃。例如Linux内核bug,在2012年6月30的闰秒时候触发,导致很多应用程序被挂掉。
  2. 失控的进程把系统的资源耗尽,导致这些共享资源不能被释放。
  3. 系统的Dependency出了问题,返回值异常。
  4. 组件中的小故障触发另一个组件中的故障,进而触发更多的故障。

没有快速的解决方法。只能仔细考虑很多细节。

  1. 检查系统的假设条件和系统之间的交互
  2. 进行全面的测试
  3. 进程隔离,
  4. 允许进程崩溃后自动重启
  5. 反复评估、监控并分析生产环境中的行为表现。

例如消息队列中,输出消息的数量应等于输入消息的数量。如果发现不一致,则立即告警。

人为失误

人无法做到万无一失。运维人员的配置错误可能是系统下线的第一大原因。

要保证系统可靠,如何减少人为错误对它的影响?

  1. 用最小出错的方式来设计系统。让做错事更难。
  2. 想办法分离最容易出错的地方,容易引发故障的接口。使用Sandbox隔离真正的生产和测试环境。
  3. 充分的测试。单元测试,集成测试,手动测试。边界条件的考虑。
  4. 当出现人为失误时,有快速回滚或者回复的机制。滚动发布新代码。
  5. 监控子系统需要详细和清晰。
  6. 推行管理流程和相关培训。

可扩展性

随着规模的增长,例如数据量、流量和复杂性,系统应该可以用合理的方式进行应对,满足这种增长。

当应用负载增加的时候,比如用户从1w到100w,从100w到1000w,应用程序如何应对增长的负载。

相关参数:Web服务的QPS,数据库的写入比例,DAU,缓存命中率。有时候平均值很重要,有时候短时间内的峰值会成为系统瓶颈。

Twitter的Fan-out结构,对数据量提出了挑战。当一个人发Tweet时候,怎么处理Timeline这个请求。根据粉丝的数量,区别处理。

如何描述性能

系统负载增加后,会发生什么,两种思考方式

  1. 系统资源不变(CPU,内存,带宽),系统的性能会发生什么变化?
  2. 如果要保持性能不变,需要增加多少资源?

不同类型的系统关心的性能指标不同

  1. 批处理系统通常关心吞吐量(throughput),例如Hadoop,每秒可以处理多少条数据或者完成一个作业总共需要多少时间。
  2. Online系统中,更看重服务的响应时间(response time),即客户端从发出请求到得到回复的总时间。

对于响应时间,如下图,有一些很长的,算异常请求,可能是由于数据大很多。但也有可能是其他因素造成的,例如上下文切换、进程调度、网络丢包、TCP重传、垃圾回收STW,缺页中断、磁盘IO。

最好使用百分位数,中位数(50%)来评估系统的响应时间。

采用较高的响应时间百分位数很重要,因为直接影响用户的总体服务体验。例如亚马逊采用99.9百分位来定义服务响应时间。优化99.9%的目标可能成本很高。能不能带来收益很关键。

​排队延迟(queueing delay)通常占了高百分位点处响应时间的很大一部分。由于服务器只能并行处理少量的事务(如受其CPU核数的限制),所以只要有少量缓慢的请求就能阻碍后续请求的处理,这种效应有时被称为头部阻塞(head-of-line blocking)

应对负载增加

垂直扩展和水平扩展。

好的系统有弹性特征,可以自动检测负载的变化,来自动添加更多的计算资源。

可扩展架构通常都是从通用模块逐步构建出来的。

可维护性

项目会随着时间的推移,项目会需要新的人员参与到开发和运维工作中,来满足系统的稳定和新场景的适应。系统应该高效的变化。

​软件的大部分开销并不在最初的开发阶段,而是在持续的维护阶段,包括修复漏洞、保持系统正常运行、调查失效、适配新的平台、为新的场景进行修改、偿还技术债、添加新的功能等等。

为此,我们将特别关注软件系统的三个设计原则:

可操作性(Operability)

​ 便于运维团队保持系统平稳运行。
良好的可操作性意味着更轻松的日常工作,进而运维团队能专注于高价值的事情。数据系统可以通过各种方式使日常任务更轻松:

简单性(Simplicity)

​ 从系统中消除尽可能多的复杂度(complexity),使新工程师也能轻松理解系统。(注意这和用户接口的简单性不一样。)
复杂度(complexity)有各种可能的症状,例如:状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的Hack、需要绕开的特例等等,

可演化性(evolability)

​ 使工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配。也称为可扩展性(extensibility),可修改性(modifiability)或可塑性(plasticity)。
组织流程方面,敏捷开发,TDD,重构。
修改数据系统并使其适应不断变化需求的容易程度,是与简单性和抽象性密切相关的:简单易懂的系统通常比复杂系统更容易修改