第一章:可靠性,可扩展性,可维护性

发布时间 2023-05-17 10:23:29作者: Mr.Tan&

现今很多应用程序都是 数据密集型(data-intensive) 的,而非 计算密集型(compute-intensive) 的。

数据密集型应用标准组件

  • 存储数据,以便自己或其他应用程序之后能再次找到 (数据库(database)
  • 记住开销昂贵操作的结果,加快读取速度(缓存(cache)
  • 允许用户按关键字搜索数据,或以各种方式对数据进行过滤(搜索索引(search indexes)
  • 向其他进程发送消息,进行异步处理(流处理(stream processing)
  • 定期处理累积的大批量数据(批处理(batch processing)

当单个工具解决不了你的问题时,怎么组合使用这些工具,不同工具之间的共性与特性,以及各自的实现原理。

关于数据系统的思考

针对新的数据存储工具与数据处理工具的出现,针对不同应用场景进行优化,不在适合进行传统归类。

类别之间的界限变得越来越模糊,例如:数据存储可以被当成消息队列用(Redis),消息队列则带有类似数据库的持久保证(Apache Kafka)。很多时候,我们需要组合使用各种数据工具。

当你将多个工具组合在一起提供服务时,服务的接口或应用程序编程接口(API, Application Programming Interface)通常向客户端隐藏这些实现细节,这样就使用较小的通用组件创建了一个全新的、专用的数据系统

设计数据系统或服务时可能会遇到很多棘手的问题,例如:当系统出问题时,如何确保数据的正确性和完整性?当部分系统退化降级时,如何为客户提供始终如一的良好性能?当负载增加时,如何扩容应对?什么样的API才是好的API

可靠性(Reliability)

系统在困境(adversity)(硬件故障、软件故障、人为错误)中仍可正常工作(正确完成功能,并能达到期望的性能水准)。

可扩展性(Scalability)

有合理的办法应对系统的增长(数据量、流量、复杂性)

可维护性(Maintainability)

许多不同的人(工程师、运维)在不同的生命周期,都能高效地在系统上工作(使系统保持现有行为,并适应新的应用场景)。

可靠性

人们对可靠软件的典型期望包括(“即使出现问题,也能继续正确工作”):

  • 应用程序表现出用户所期望的功能。
  • 允许用户犯错,允许用户以出乎意料的方式使用软件。
  • 在预期的负载和数据量下,性能满足要求。
  • 系统能防止未经授权的访问和滥用。

造成错误的原因叫做故障(fault),能预料并应对故障的系统特性可称为容错(fault-tolerant)韧性(resilient)。“容错”不是系统可以容忍所有可能的错误,只有针对特定类型的错误才有意义。

注意故障(fault)不同于失效(failure)。

故障通常定义为系统的一部分状态偏离其标准,而失效则是系统作为一个整体停止向用户提供服务。

最好设计容错机制以防因故障而导致失效

硬件故障

硬盘崩溃、内存出错、机房断电、有人拔错网线……一旦你拥有很多机器,这些事情会发生!

硬盘的 平均无故障时间(MTTF mean time to failure) 约为10到50年。因此从数学期望上讲,在拥有10000个磁盘的存储集群上,平均每天会有1个磁盘出故障。

为了减少系统的故障率,可以增加单个硬件的冗余度。

如果在硬件冗余的基础上进一步引入软件容错机制,那么系统在容忍整个(单台)机器故障的道路上就更进一步了。

软件错误

这类错误难以预料,而且因为是跨节点相关的,所以比起不相关的硬件故障往往可能造成更多的系统失效。

虽然软件中的系统性故障没有速效药,但我们还是有很多小办法,例如:

  • 仔细考虑系统中的假设和交互;
  • 彻底的测试;
  • 进程隔离;
  • 允许进程崩溃并重启;
  • 测量、监控并分析生产环境中的系统行为。

如果系统能够提供一些保证(例如在一个消息队列中,进入与发出的消息数量相等),那么系统就可以在运行时不断自检,并在出现差异(discrepancy)时报警。

人为错误

尽管人类不可靠,但怎么做才能让系统变得可靠?最好的系统会组合使用以下几种办法:

  • 以最小化犯错机会的方式设计系统。例如,精心设计的抽象、API和管理后台使做对事情更容易,搞砸事情更困难。但如果接口限制太多,人们就会忽略它们的好处而想办法绕开。很难正确把握这种微妙的平衡。
  • 将人们最容易犯错的地方与可能导致失效的地方解耦(decouple)。特别是提供一个功能齐全的非生产环境沙箱(sandbox),使人们可以在不影响真实用户的情况下,使用真实数据安全地探索和实验。
  • 在各个层次进行彻底的测试【3】,从单元测试、全系统集成测试到手动测试。自动化测试易于理解,已经被广泛使用,特别适合用来覆盖正常情况中少见的边缘场景(corner case)
  • 允许从人为错误中简单快速地恢复,以最大限度地减少失效情况带来的影响。 例如,快速回滚配置变更,分批发布新代码(以便任何意外错误只影响一小部分用户),并提供数据重算工具(以备旧的计算出错)。
  • 配置详细和明确的监控,比如性能指标和错误率。 在其他工程学科中这指的是遥测(telemetry)。 (一旦火箭离开了地面,遥测技术对于跟踪发生的事情和理解失败是至关重要的。)监控可以向我们发出预警信号,并允许我们检查是否有任何地方违反了假设和约束。当出现问题时,指标数据对于问题诊断是非常宝贵的。
  • 良好的管理实践与充分的培训。

可扩展性

服务 降级(degradation) 的一个常见原因是负载增加,例如:系统负载已经从一万个并发用户增长到十万个并发用户,或者从一百万增长到一千万。也许现在处理的数据量级要比过去大得多。

可扩展性(Scalability) 是用来描述系统应对负载增长能力的术语。

讨论可扩展性意味着考虑诸如“如果系统以特定方式增长,有什么选项可以应对增长?”和“如何增加计算资源来处理额外的负载?”等问题。

描述负载

负载可以用一些称为 负载参数(load parameters) 的数字来描述。参数的最佳选择取决于系统架构,它可能是每秒向Web服务器发出的请求、数据库中的读写比率、聊天室中同时活跃的用户数量、缓存命中率或其他东西。

描述性能

一旦系统的负载被描述好,就可以研究当负载增加会发生什么。我们可以从两种角度来看:

  • 增加负载参数并保持系统资源(CPU、内存、网络带宽等)不变时,系统性能将受到什么影响?
  • 增加负载参数并希望保持性能不变时,需要增加多少系统资源?

对于Hadoop这样的批处理系统,通常关心的是吞吐量(throughput),即每秒可以处理的记录数量,或者在特定规模数据集上运行作业的总时间iii。对于在线系统,通常更重要的是服务的响应时间(response time),即客户端发送请求到接收响应之间的时间。

延迟和响应时间

延迟(latency)响应时间(response time) 经常用作同义词,但实际上它们并不一样。响应时间是客户所看到的,除了实际处理请求的时间( 服务时间(service time) )之外,还包括网络延迟和排队延迟。延迟是某个请求等待处理的持续时长,在此期间它处于 休眠(latent) 状态,并等待服务。

通常报表都会展示服务的平均响应时间。它通常被理解为算术平均值(arithmetic mean):给定 n 个值,加起来除以 n 。但是平均值并不是一个非常好的指标,因为它不能告诉你有多少用户实际上经历了这个延迟

通常使用百分位点(percentiles)会更好。如果将响应时间列表按最快到最慢排序,那么中位数(median)就在正中间,意味着有一半的用户在这个响应时间内返回。

响应时间的高百分位点(也称为尾部延迟(tail latencies),比如:99%的响应返回时间)非常重要,因为它们直接影响用户的服务体验。但是优化第99.99百分位点(一万个请求中最慢的一个)被认为太昂贵。

百分位点通常用于服务级别目标(SLO, service level objectives)服务级别协议(SLA, service level agreements),即定义服务预期性能和可用性的合同。

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

应对负载的方法

纵向扩展(scaling up)垂直扩展(vertical scaling),转向更强大的机器)和横向扩展(scaling out)水平扩展(horizontal scaling),将负载分布到多台小机器上)之间的对立。跨多台机器分配负载也称为“无共享(shared-nothing)”架构。

弹性(elastic) ,可以在检测到负载增加时自动增加计算资源,而手动扩展(人工分析容量并决定向系统添加更多的机器)依赖于人工操作。如果负载极难预测(highly unpredictable),则弹性系统可能很有用,但手动扩展系统更简单,并且意外操作可能会更少。

跨多台机器部署无状态服务(stateless services)非常简单,但将带状态的数据系统从单节点变为分布式配置则可能引入许多额外复杂度。出于这个原因,常识告诉我们应该将数据库放在单个节点上(纵向扩展),直到扩展成本或可用性需求迫使其改为分布式。

大规模的系统架构通常是应用特定的—— 没有一招鲜吃遍天的通用可扩展架构。(没有银弹)。

应用的问题可能是读取量、写入量、要存储的数据量、数据的复杂度、响应时间要求、访问模式或者所有问题的大杂烩

一个良好适配应用的可扩展架构,是围绕着假设(assumption)建立的:哪些操作是常见的?哪些操作是罕见的?这就是所谓负载参数。如果假设最终是错误的,最糟糕的是适得其反。早期产品中,支持产品快速迭代的能力,要比可扩展至未来的假想负载要重要的多。

可维护性

设计软件方式:在设计之初就尽量考虑尽可能减少维护期间的痛苦,从而避免自己的软件系统变成遗留系统。

软件系统的三个设计原则:

可操作性(Operability)

便于运维团队保持系统平稳运行。

简单性(Simplicity)

从系统中消除尽可能多的复杂度(complexity),使新工程师也能轻松理解系统。(注意这和用户接口的简单性不一样。)

可演化性(evolability)

使工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配。也称为可扩展性(extensibility)可修改性(modifiability)可塑性(plasticity)

可操作性:人生苦短,关爱运维

一个优秀运维团队的典型职责如下:

  • 监控系统的运行状况,并在服务状态不佳时快速恢复服务
  • 跟踪问题的原因,例如系统故障或性能下降
  • 及时更新软件和平台,比如安全补丁
  • 了解系统间的相互作用,以便在异常变更造成损失前进行规避。
  • 预测未来的问题,并在问题出现之前加以解决(例如,容量规划)
  • 建立部署,配置、管理方面的良好实践,编写相应工具
  • 执行复杂的维护任务,例如将应用程序从一个平台迁移到另一个平台
  • 当配置变更时,维持系统的安全性
  • 定义工作流程,使运维操作可预测,并保持生产环境稳定。
  • 铁打的营盘流水的兵,维持组织对系统的了解。

数据系统可以通过各种方式使日常任务更轻松:

  • 通过良好的监控,提供对系统内部状态和运行时行为的可见性(visibility)
  • 为自动化提供良好支持,将系统与标准化工具相集成
  • 避免依赖单台机器(在整个系统继续不间断运行的情况下允许机器停机维护)
  • 提供良好的文档和易于理解的操作模型(“如果做X,会发生Y”)
  • 提供良好的默认行为,但需要时也允许管理员自由覆盖默认值
  • 有条件时进行自我修复,但需要时也允许管理员手动控制系统状态
  • 行为可预测,最大限度减少意外

简单性:管理复杂度

复杂度(complexity) 有各种可能的症状,例如:状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的Hack、需要绕开的特例等等。

因为复杂度导致维护困难时,预算和时间安排通常会超支。在复杂的软件中进行变更,引入错误的风险也更大:当开发人员难以理解系统时,隐藏的假设、无意的后果和意外的交互就更容易被忽略。相反,降低复杂度能极大地提高软件的可维护性,因此简单性应该是构建系统的一个关键目标。

简化系统并不一定意味着减少功能;它也可以意味着消除额外的(accidental)的复杂度。

额外复杂度 定义为:由具体实现中涌现,而非(从用户视角看,系统所解决的)问题本身固有的复杂度。

消除额外复杂度的最好工具之一是抽象(abstraction)。一个好的抽象可以将大量实现细节隐藏在一个干净,简单易懂的外观下面。一个好的抽象也可以广泛用于各类不同应用。抽象组件的质量改进将使所有使用它的应用受益。

抽象可以帮助我们将系统的复杂度控制在可管理的水平,不过,找到好的抽象是非常困难的。

可演化性:拥抱变化

系统的需求永远不变,基本是不可能的,它们处于常态的变化中。