DDIA-chap1-可靠性、可伸缩性和可维护性

Posted by raftale on September 16, 2023

很多应用程序都是数据密集型,而非计算密集型的。

数据密集型应用通常由标准组件构成,标准组件提供了许多通用的功能:

  1. 存储数据,即数据库
  2. 记住开销昂贵操作的结果,加快读取速度:缓存
  3. 允许用户按关键字搜索/过滤数据:搜索引擎
  4. 向其他进程发送消息,进行异步处理:流处理
  5. 定期处理累积的大批量数据:批处理

目标:可靠、可伸缩、可维护的数据系统

关于数据系统的思考

数据库、消息队列、缓存、搜索引擎

许多新的数据存储工具与数据处理 之间的界限越来越模糊,数据存储可以被当成消息队列使用(redis),消息队列则带有类似数据库的持久性保证(kafka)。

可靠性:系统在困境下仍可以正常工作

可伸缩性:有合理的办法应对系统的增长

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

可靠性

造成错误的原因叫做故障(fault),故障不等同失败(failure),失败或者说失效说明系统完全停止向用户提供服务,而故障是部分的。

能预料并应对故障的系统特性可称为 容错(fault-tolerant)

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

硬件故障

硬件冗余的基础上引入软件容错机制,就提高了系统可靠性;

软件错误

进程隔离、监控、允许重启是应对软件错误比较好的方法。

人为错误

如何避免:

  1. 以最小化犯错机会的方式设计系统:精心设计的抽象
  2. 最容易犯错的地方进行解耦;
  3. 彻底的测试
  4. 出错后快速恢复
  5. 良好的监控
  6. 良好的管理

可靠性的重要性

软件失效会对现实世界造成损失

可伸缩性

描述负载

负载参数:qps,数据库的读写比率,活跃用户、缓存命中率等

以twitter举例:

  1. 发布推文:用户可以向其粉丝发布新消息(平均4.6k请求/秒,峰值超过12k请求/秒);
  2. 主页时间线:用户可以查阅他们关注的人发布的推文(300k请求/秒)

处理每秒12000次写入不难,但推特的伸缩性挑战不是来自推特量,而是来自于fan-out,每个用户关注了很多人,也被很多人关注。

一张用户表user,包含用户id、昵称、头像;

关注表follows,写了哪一个关注了哪一个;

推文表,记录每个用户发的推。

如果要查询用户关注的人发的推,大体上,有两种实现方式:

第一个是写这样一个查询SQL:

1
2
3
4
5
select tweets.*, users.*
from tweets 
join users on tweets.id = users.id
join follows on follows.followee_id = users.id
where follows.follower_id = current_user

followee_id为被关注者的ID

推特早期就是使用的这个方法,但系统很难跟上主页时间线查询的负载。所以提出了第二个方法。

第二个是方法是,为每个用户的主页时间线维护一个缓存,存储推文的ID,就像每个用户的推文收件箱。当一个用户发布推文时,查找所有关注该用户的人,并将新的推文插入到每个主页时间线缓存中。因为写频率比读多了2个数量级,所以写先做完读的工作。

但方法二有个缺点,就是发推需要大量的额外工作。平均来说,一条推文会发往75个关注者,所以每秒4.6k的发推写入,变成了对主页时间线缓存每秒345k的写入。这个平均值也隐藏了用户粉丝数差异巨大的现实,一些超过3000w的粉丝的用户的一条推文就会导致主页时间线缓存的3000万次写入。

每个用户粉丝数的分布是探索可伸缩性的一个关键负载因素,因为它决定了扇出负载。

目前推特是结合了两种方法,大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中,但是少数拥有海量粉丝的用户会被排出在外。当用户读取主页时间线时,分别地获取出该用户所关注的每位名流的推文,再与用户的主页时间线缓存合并。

描述性能

一旦系统的负载被描述好,就可以研究负载增加时会发生什么。

  1. 增加负载参数并保持系统资源不变时,系统性能会收到什么影响?
  2. 增加负载参数并系统保持性能不变时,需要增加多少系统资源?

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

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

响应时间的平均值不是一个好的指标,因为它不能告诉你有多少用户实际上经历了这个延迟。

通过使用百分位点会更好,中位数是一个好的度量标准。

响应时间的高百分位点也很重要,它直接影响用户的服务体验。因为响应慢的客户往往是数据最多或者说最有价值的客户。

百分位点通常用于服务级别目标(SLO,service level objectives)和服务级别协议(SLA,service level agreements),即定义服务预期性能和可用性的合同。SLA可能会声明,如果服务响应时间的中位数小于200ms,且99.9百分位点低于1秒,则认为服务工作正常。

排队延迟通常占用了高百分位点处响应时间的 很大一部分,由于服务器只能并行处理少量的事务(如受CPU核数的限制),所以只要有少量缓慢的请求就能阻碍后续请求的处理,这种效应有时被称为头部阻塞。即使后续请求在服务器上处理的非常迅速,由于需要等待先前请求完成,客户端最终看到的是缓慢的总体响应时间。因为这种效应,使得测试时的响应时间非常重要。

为测试系统的可伸缩性而人为产生负载时,产生负载的客户端要独立于响应时间不断发送请求。如果客户端在发送下一个请求之前等待先前的请求完成,这种行为会产生人为排队的效果,使得测试时的队列比现实情况更短,使测量结果产生偏差。

应对负载的方法

当负载参数增加时,如果保持良好的性能。

垂直伸缩、水平伸缩。

水平伸缩 跨多台机器分配负载也被称为无共享架构。

有些系统是弹性的,可以检测到负载增加时自动增加资源,如果负载难以预测,弹性系统很有用,其余情况手动更加稳妥。

跨多台机器部署 无状态服务非常简单,但将带状态的数据系统从单节点变为分布式配置则可能引入许多额外复杂度。处于这个原因,常识告诉我们应该将数据库放在单个节点上,直到达到瓶颈。

分布式数据库越来越好用,有一天会成为常用的选项。

大规模的系统架构通常是应用特定的,没有一招通用的可伸缩架构。

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

举个例子,处理每秒十万个请求(每个大小为1KB)的系统与用于处理每分钟3个请求的系统会非常不一样,尽管两个系统有同样的数据吞吐量。

一个良好适配应用的可伸缩性架构,是围绕着假设建立的:哪些操作是常见的?哪些操作是罕见的?

如果假设是错误的,那么为伸缩所做的工程投入就是白费的。

可维护性

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

  1. 可操作性:便于运维团队保持系统平稳运行
  2. 简单性:从系统中消除尽可能多的复杂度,使新工程师也能轻松理解系统;
  3. 可演化性:未来能轻松的对系统进行更改,当需求变化时为新应用场景做适配,也称为可扩展性、可修改性、可塑性。

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

简单性:管理复杂度

复杂度:状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的hack、需要绕开的特例等等

消除额外复杂度最好的工具就是抽象,一个好的抽象可以把大量实现细节隐藏在一个干净、简单易懂的外观下面。一个好的抽象可以广泛用于各类不同应用。

可演化性:拥抱变化

组织流程方面:敏捷

测试驱动开发、重构

总结