介绍 reliability, scalability, 和 maintainability 的含义,以及实现它们的手段。
这里将最常见的数据处理需求列出来了:
A data-intensive application is typically built from standard building blocks that provide commonly needed functionality. For example, many applications need to:
- Store data so that they, or another application, can find it again later (databases)
- Remember the result of an expensive operation, to speed up reads (caches)
- Allow users to search data by keyword or filter it in various ways (search indexes)
- Send a message to another process, to be handled asynchronously (stream processing)
- Periodically crunch a large amount of accumulated data (batch processing)
不同的数据系统(主要指软件层面,如 MySQL、ElasticSearch 等)会有自己的特性,来满足上述一些场景的需求。
Thinking About Data Systems
背景:
Data system 分类的 边界模糊:
- Redis 即可以做 cache,也有人使用它做消息队列
- Apache Kafka 即是消息队列,也提供了类似数据库的持久化
一个工具往往不能满足全部业务需求,比如下面是一个可能的业务架构图:
这个系统是由一些通用组件构建出来的特殊系统,需要考虑:
- 如何保证缓存数据的一致性?
- 如何保证出错时数据保持正确和完整?
- 当部分组件不工作、系统降级时,如何仍然保持系统整体性能?
- 负载增加时如何扩容?
- 服务提供的 API 应该是怎样的?
问题:
如何判断使用怎样的系统?各个系统在设计上有什么共通点?
解答:
不同系统间一般有 3 个共同要考虑的点:
- 可靠性(Reliability):即使出现异常情况,软件也能正常运行
- 可伸缩性(Scalability):可以应对负载的增加
- 可维护性(Maintainability):系统可以容易地进行变更来使其顺利运行,比如可以容易地打补丁、发布修复 bug 后的应用等
这一章下面的内容在阐述这几点具体的含义。
Reliability
定义:
「可靠性」,
- 简单地说:continuing to work correctly, even when things go wrong.
- 复杂地说:
- The application performs the function that the user expected.
- It can tolerate the user making mistakes or using the software in unexpected ways.
- Its performance is good enough for the required use case, under the expected load and data volume.
- The system prevents any unauthorized access and abuse.
错误相关的概念:
- Faults: things go wrong
- Cope with (certain kinds of) faults: fault-tolerant or resilient.
Faults v.s. failure:
- Faults 一般指单个组件的功能没有满足要求
- Failure 一般是系统整体出现错误,无法满足要求。如果没有做好错误处理,fault 可能会演变为 failure
Netflix Chaos Monkey: 有意地破坏系统中一些组件,产生 fault,以演习和测试整个系统的可靠性。
并不是全部 fault 都是可以被 tolerant 的,比如有安全漏洞导致数据库信息被黑客盗取。
下面描述各种不同类型的 fault。
Hardware Faults
现状:
硬件错误是非常常见的:机房断电、运维插错电缆造成网络配置错误、内存或者硬盘坏掉。一个硬盘平均 10-50 年会挂掉,如果像数据中心那样多的硬盘,几乎每天都会有硬盘挂掉。
解决方法:
Add redundancy(增加冗余):硬盘上 Raid,机房多路供电,上 UPS。
但是……
并不是任何一种应用都需要强调单机的可用性。比如一个互联网应用的逻辑层 server,往往会有数十台甚至上千台机器,依赖于前端的负载均衡和高可用机制,一台机器挂掉了也有其他机器继续服务,影响并不大。比较依赖于单机可用性的,可能是一些关键的数据库服务等。
Software Error
关联性:
硬件错误往往不是相关联的,比如一台机器硬盘挂了,并不代表其他机器也会同时挂掉。但是软件错误往往是「系统性的」,比如 Linux Kernel 的一个 bug 可能会影响非常多的服务;一个服务可能返回了错误的信息给到上游,导致上游也出现异常。
潜伏性:
软件中的 bug 经常在于代码对它的运行环境有一些预设的认识。一般情况下环境的情况符合这个认识,但是一旦不符合了,bug 就会引起错误。
解决办法:
编程时多思考;详尽的测试;进程隔离;出错时 crash 并重启;监控及分析线上系统等。
Human Errors
现状:
人不可靠。事实上互联网中的服务不可用,绝大多数是来自运维操作或者配置出问题。硬件故障引起的问题只占很小一部分。
解决办法:
- 设计一个不容易犯错的系统。比如 API、管理界面上,把容易出错的部分抽象或者屏蔽掉,只留下容易理解的、好用的接口供用户操作。
- 提供机制,使用户即使犯了错也不会引起问题。比如提供沙箱环境,让开发可以拿线上数据做分析,但是又不会影响到用户环境
- 做仔细的测试,覆盖系统的各个层级;做自动化测试
- 提供快速恢复的机制;比如发布配置或者发布程序的平台,提供快速回滚的机制
- 建立详细且清晰的监控机制,使得出错时能快速定位问题
- 通过管理和培训的手段来减少误操作的发生
How Important Is Reliability?
通常都非常非常重要。如果是做原型,可以为了开发速度适当牺牲可靠性。
Scalability
指如何去应对负载(load)增加。
Describing Load
<<.s "问题:"" >>
如果无法清晰描述 负载(load),那么也无法知道什么是负载增加,以及如何应对。
解决过程中引入的概念:
负载,一般拆分成多个 负载因素(load parameters)来描述,根据你的系统架构而有无不同,比如:
- requests per second to a web server
- the ratio of reads to writes in a database
- the number of simultaneously active users in a chat room
- the hit rate on a cache
Twitter 例子:
Twitter 的场景是读多(300k rps)写少(4.6k rps)。一般有两种方案:
- 发推时写帐号自身的存储,读 timeline 时从关注的全部帐号中拉取帖子并进行合并和排序
- 发推时写帐号自身的存储,同时更新到关注者的缓存中;这样读 timeline 时不用去拉关注的帐号的内容进行合并
在读多写少的场景下,第二种方案的性能事实上比第一种好。但是 Twitter 有个特点时,部分名人有些非常多的关注量(上百万甚至千万),用第二种时会导致名人发帖时要写入的数据非常多,无法及时送达到全部关注者。最终 Twitter 结合了这两种方案来解决这个问题。
在这个场景中,Twitter 的 关键负载因素,是用户的粉丝数分布(以及他们发推的频率)。你的系统根据自身的特点,也可能有它独特的负载因素。
Describing Performance
从实际问题出发……
- 如果把一个负载因素增加(比如请求量增长一倍),保持系统资源不变(如 CPU、带宽、机器数等),你的系统的性能会受到怎样的影响?
- 接上,如果你希望系统性能不变,需要增加多少资源?
应该如何描述性能?
不同类型的系统,用来描述性能的指标不一样。比如 Hadoop 类批处理系统(batch processing system),一般以 吞吐量(throughput,即单位时间内处理的纪录数)来衡量。线上系统则一般以 响应时间(response time)来衡量。
不同请求的响应时间各不一样。一般观察响应时间的 分布情况 来做判断:
一般来说各请求的处理时间都应该是一致的,但是一些因素会引起的额外的延时,导致各请求响应时间不一致。比如进程切换、TCP 丢包重传、GC 引起进程中断等等。
从分布情况中,一般考虑这些点:
- 使用 percentiles 来衡量大部分请求的时延情况,比如 p95(即最快的 95% 的请求落在什么范围内),以及 p99、p999 等;这个方法能得出绝大部分用户的情况
- 不考虑 使用平均数(mean / average),因为对于延时变化很大的场景,它并不能体现什么
有时候 p999 会显著大于 p95。High percentiles of response time,被称为 tail latencies。有些场景需要着重考虑 tail latency。比如亚马逊中,处理得最慢的请求往往是下单量很大的客户,他们的数据多导致了处理过慢。到了 p9999 时,优化最慢的请求往往已经变得性价比不高,因此对于 tail latency 的处理应该有合理的评估。
非核心内容
service level objectives (SLOs) 和 service level agreements (SLAs),往往是以 percentiles 的形式描述的。比如 p50 的响应时间是 200ms,p99 的响应时间在 1s 内。服务的当机时间少于 0.1% 等等。
Head-of-line blocking(队头阻塞),是指后台程序在按顺序处理请求时,有部分请求执行时间过长,导致其后的请求在队列中等待了太长时间,从而引起整个系统性能降低的问题。
如果场景是用户需要发多个并行请求(比如动态加载内容的网页),并在全部请求都返回后才能处理(例如渲染出整个网页),那么最慢的请求会最终影响性能。这叫 tail latency amplification,放大了 tail latency 带来的影响。
- Note
- Latency(延时)与 response time 并不一样。Response time 是指客户端观察到的延时,包含了网络传输、请求在队列中等待被处理的时间等。Latency 则仅包含请求被程序实际执行的时间。
Approaches for Coping with Load
应对负载增加的方法
常见的两种说法:
- Scaling up: 换更好的硬件,让单台机器更强(Stack Overflow 在很长一段时间是 这样做 的)
- Scaling out: 堆更多的机器,是现在互联网服务常见的模式
有些系统是弹性的(elastic),可以根据负载变化自动扩缩机器。缺点是系统变复杂、可能应对不了一些异常的流量增长等。
无状态服务容易做 scaling out。有状态服务难。以前的惯常做法是,对数据库使用 scaling up 模式。但是未来 分布式数据库应该是标配。
Scaling Architecture 的考虑点
没有一个适配任何场景的 scaling architecture。考虑点有:数据存储的大小,读写数据的量,数据的复杂度,响应时间要求等等。比如一个 100,000 RPS,每次请求处理 1kB 数据的系统,与每分钟 3 次请求,每次处理 2GB 数据的系统,他们处理的数据量是一样的,但是架构完全不一样。
非核心内容
对于创业团队,如果想法未被市场验证,应该优先开发速度而不是架构设计。因为设计出来的 scaling architecture 可能跟后面业务的模式不匹配,导致无用功。
Maintainability
可维护性中的「维护」,包含了修复 bug、维护系统正常运转、增加新功能等等。考虑 Operability、Simplicity 及 Evolvability。
Operability: Making Life Easy for Operations
主要讲运维应该做什么事情来维护系统顺利运转。如监控、问题定位、打安全补丁、容量预估、运维工具开发、文档维护等等。
Simplicity: Managing Complexity
复杂的软件系统出 bug 机率高、维护成本高。
复杂的功能可以通过 抽象,使得软件实现上简单直观。
Evolvability: Making Change Easy
应对需求变化、外界环境变化、商业模式变化等等情况,你的系统需要是可随时进化的。这往往构建在 simplicity 的基础上。但是作者说这个很重要,要单独列出来。