描述负载之后,我们需要进一步了解负载增加系统将会发生什么变化,此时我们就需要对系统性能进行定量描述。与负载的描述类似,在不同的系统中关心的性能指标有所不同:
- 在批处理系统如 Hadoop 中,通常关心「吞吐量」(throughput),即每秒可处理的记录条数,或在固定大小数据集上运行作业所需的总时间
- 在线上系统中,更看重服务的「响应时间」(response time),即客户端从发送请求到接收响应之间的间隔。
对于响应时间来说,其包括处理请求时间、以及各种延迟(网络延迟、排队延迟)。即使反复发送、处理相同的请求,每次都可能会产生略微不同的响应时间。因此我们最好将响应时间视为一种可度量的数据分布。
如下图所示,每个灰色条代表一个服务请求,高度表示该请求的响应时间。我们可以考察服务请求的「平均响应时间」(一般为算术平均值),但其有时会掩盖一些信息;因此更好的选择是使用「百分位数」,包括中位数、95百分位数、99百分位数等。百分位数常用于描述、定义服务质量指标和服务质量协议,较高的响应时间百分位数会直接影响用户的总体服务体验(请求最慢的客户往往购买了更多的商品)。
1.2.3 应对负载增加的方法
在描述完负载与性能的相关参数后,我们关心的问题是:当负载参数增加时,应如何保持良好性能?这里原书给出了一系列的 tips,现归纳如下:
- 针对特定级别负载而设计的架构不太可能应付超出预设目标 10 倍的实际负载
- 扩展一般分为「垂直扩展」(升级到更强大的机器)与「水平扩展」(将负载分布到多个更小的机器)
- 水平扩展不可避免,不过单机性能同样不能忽视
- 如果负载高度不可预测,自动「弹性」系统会更加高效,其可以自动检测负载增减,然后自动添加更多计算资源
- 将无状态服务扩展的至多机器相对容易,而将有状态服务从单个节点扩展到分布式多机环境的复杂性会大大增加
- 随着分布式系统的发展,未来分布式数据系统将成为标配
- 超大规模的系统往往针对特定应用而高度定制,很难有一种通用的架构
1.3 可维护性
软件的大部分成本在于开发完成后的持续维护,包括 bug 修复、保证系统正常运行、故障排查、新平台适配、增加新功能等。本节将关注软件系统中保持高可维护性的三个设计原则:
- 「可操作性」(Operability):方便运维团队来保持系统平稳运行
- 「简单性」(Simplicity):简化系统复杂性,使新工程师能够轻松理解系统(注意与用户界面的简单性区分)
- 「可演化性」(Evolvability):使贡献是能够轻松地对系统进行更改,根据需求变化将其适配到非典型场景(也称为可延伸性、易修改性或可塑性)
1.3.1 可操作性
良好的操作性可以使得日常的工作变得简单,使运维团队能够专注于高附加值的任务。数据系统设计可以在这方面做很多事情,例如:
- 提供对系统运行时行为和内部的可观测性,方便监控
- 提供良好的自动化支持与标准工具集成
- 避免依赖特定的机器(允许机器停机维护而系统不间断运行)
- 提供良好的文档和易于理解的操作模式
- 提供良好的默认配置,允许管理员方便地修改默认值
1.3.2 简单性
系统的复杂性有各种各样的表现方式:状态空间的膨胀、模块的紧耦合、不一致的命名和术语、特殊处理等。复杂性会使得维护变得越来越困难,降低复杂性可以大大提高软件的可维护性。
简化系统设计并不意味着减少系统功能,而主要意味着消除「意外」(accidental)方面的复杂性,其并非软件固有,而是实现本身所衍生出来的问题。
消除意外复杂性的最好方法之一就是「抽象」(abstraction)。一个好的设计抽象可以隐藏大量的实现细节,并对外提供干净、易懂的接口;一个好的抽象也可以用于不同的应用程序,提升开发效率与软件质量。
1.3.3 可演化性
一成不变的系统需求几乎不存在,我们使用可演化性来指代「数据系统层面的敏捷性」。可演化性的目标在于可以轻松地修改数据系统,使其适应不断变化的需求。这一目标与简单性密切相关:简单易懂的系统通常比复杂的系统更容易修改。
在组织流程方面,「敏捷」(Agile)开发模式为适应变化提供了很好的参考。不过当前大部分的敏捷开发技术还只是针对小规模、本地模式环境。在更大的数据系统层面上提高敏捷性的方法还有待探索。