可靠性、可扩展性和可维护性

date
Oct 8, 2021
slug
DDIA-chapter1
status
Published
tags
DDIA
summary
DDIA第一章读书笔记
type
Post

TOC

现在很多应用都是数据密集型应用(data-intensive),而不是计算密集型应用(compute-intensive)
数据密集型应用的标准组件及其通用功能:
  • 存储数据(database)
  • 加快读取速度(cache)
  • 按关键词搜索数据,或对数据进行过滤(search indexes)
  • 向其他进程发送消息,进行异步处理(stream processing)
  • 定期处理大批量数据(batch processing)
基础目标:
  • 可靠性(Reliability)
  • 可扩展性(Scalability)
  • 可维护性(Maintainability)

可靠性

  • 系统在困境中仍可以正常工作
故障(fault):造成错误的原因,通常定义为系统的一部分状态偏离其标准
容错(fault-tolerant):能预料并应对故障的系统特性
失效(failure): 系统作为一个整体停止向用户提供服务。
硬件故障的解决方式:增加单个硬件的冗余度、软件容错机制
软件错误:内部的系统性错误(systematic error),比如失控进程占用共享资源等,还有比如级联故障等。解决方法:仔细考虑假设和交互,测试,进程隔离,测量监控等等……
人为错误:运维配置错误是导致服务中断的首要原因。解决方法:以最小化犯错机会的方式设计系统(比如抽象、设计api等等),将人们最容易犯错的地方和可能导致失效的地方解耦(decouple),提供一个功能齐全的非生产环境沙箱(sandbox),在各个层次进行彻底的测试,允许从人为错误中简单快速地恢复,配置详细和明确的监控,良好的管理实践与充分的培训。

可扩展性

“如果系统以特定方式增长,有什么选项可以应对增长?”
“如何增加计算资源来处理额外的负载?”

描述负载

负载参数的最佳选择取决于系统架构,可能是每秒向web服务器发出的请求、数据库中的读写比率、聊天室中同时活跃的用户数量、缓存命中率或其他东西

推特的例子

推特发布推文:用户可以向其粉丝发布新消息(平均 4.6k请求/秒,峰值超过 12k请求/秒)。
主页时间线:用户可以查阅他们关注的人发布的推文(300k请求/秒)。
处理每秒12,000次写入(发推文的速率峰值)还是很简单的。然而推特的扩展性挑战并不是主要来自推特量,而是来自扇出(fan-out)——每个用户关注了很多人,也被很多人关注。
有两种实现方式。
方法1:
发布推文时,只需要将新推文推入全局推文集合即可,当请求自己的主页时间线时,首先查找他关注的所有人,查询这些被关注用户发布的推文并按时间顺序合并。
notion image
可以利用这样子的sql查询
SELECT tweets.*, users.*

FROM tweets

JOIN users ON tweets.sender_id = users.id

JOIN follows ON follows.followee_id = users.id

WHERE follows.follower_id = current_user
方法2:
为每个用户的主页时间线维护一个缓存,当用户发布推文时就将新的推文插入到每一个关注者的缓存中。所以读取主页时间线的请求开销很小,因为已经提前计算好了。
notion image
推特一开始使用方法1,但是系统跟不上查询的负载,所以用了方法2
  • 因为发推特的频率比查询主页的频率几乎低了两个数量级,所以这种情况下最好在写入时做更多的工作,在读取时做更少的工作
然而方法2的缺点是,发推特需要额外工作,尤其是对于一些粉丝众多的用户,发推特就会有大量的写入工作。
  • 所以在推特的例子中,每个用户粉丝数的分布(可能按照用户的发推频率来加权)是探讨可扩展性的一个关键负载参数,因为它决定了扇出负载
推特最终的解决方法:两种方法的混合,对于大多数用户采用方法2,对于少量具有海量粉丝的用户使用方法1,当用户读取主页时间线的时候,分别获取关注的明星的推文和自己的主页时间线缓存进行合并。
  • 妙啊

描述性能

“当增加负载参数并保持系统资源不变时,系统性能将受到什么影响?”
“当增加负载参数并希望性能保持不变时,需要增加多少系统资源?”
对于Hadoop这样的批处理系统,通常关心的是吞吐量(throughput),即每秒可以处理的记录数量。对于在线系统,更重要的是响应时间(response time),即客户端发送请求到接受响应之间的时间。
延迟(lantency):某个请求等待处理的持续时长,在此期间处于休眠状态,并等待服务。
响应时间(respense time):用户看到的,除了实际处理请求的时间,还包括网络延迟和排队延迟。
我们需要把响应时间视为一个可以测量的数值分布,而不是单个数值。
通常报表会展示平均响应时间(但是当你想知道典型响应时间,平均值不好)
通常使用百分位点(percentiles)会更好,比如中位数就是p50,一半服务时间小于中位数,一半大于中位数,为了弄清楚异常值有多糟糕,可以看更高的百分位点比如p95,p99,p999(以为着95%,99%,99.9%的请求响应时间比该阈值快)
响应时间的高百分位点(也叫做尾部延迟(tail latencies))非常重要,因为它们直接影响用户服务体验,比如亚马逊描述内部响应时间用p999,即使只影响1000个中的1个,但是因为请求响应最慢的客户也是数据最多的客户,也可以说是最有价值的用户。
排队延迟:由于服务器只能并行处理少量的事务(受CPU核数的限制),所以有少量缓慢的请求就阻碍后续,称为头部阻塞(head-of-line blocking)。
当负载参数增加时,如何保持良好的性能?
纵向扩展(scaling up)(垂直扩展(vertical scaling),转到更强大的机器)和横向扩展(scaling out)(水平扩展(horizontal scaling),将负载分布到多台小机器上)
跨多台机器部署无状态服务(stateless services)非常简单,但将带状态的数据系统从单节点变为分布式配置则可能引入许多额外复杂度。
随着分布式系统的工具和抽象越来越好,可以预见分布式数据系统将成为未来的默认设置。
大规模的系统架构通常是应用特定的,没有可以一招鲜吃遍天的通用可扩展架构,应用的问题可能是读取量、写入量、要存储的数据量、数据的复杂度、响应时间要求、访问模式或者所有问题的大杂烩。
举个例子,,用于处理每秒十万个请求(每个大小为1 kB)的系统与用于处理每分钟3个请求(每个大小为2GB)的系统看上去会非常不一样,尽管两个系统有同样的数据吞吐量。
一个良好适配应用的可扩展架构,是围绕着假设建立的,哪些操作是常见的?哪些操作是罕见的?这就是所谓负载参数。

可维护性

可维护性又分为
  • 可操作性(Operability)
  • 简单性(Simplicity)
  • 可演化性(evolability)

可操作性

便于运维团队保持系统平稳运行
比如通过良好的监控,提供对系统内部状态的可视性,提供良好的文档和易于理解的操作模型等等等等

简单性:管理复杂度

复杂度(complexity)有各种可能的症状,例如:状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的Hack、需要绕开的特例等等
消除额外复杂度的最好工具之一是**抽象**
比如SQL就是一种抽象,隐藏了复杂的磁盘/内存数据结构、来自其他客户端的并发请求、崩溃后的不一致性。

可演化性:拥抱变化

敏捷工作模式,敏捷技术:如测试驱动开发(TDD,test-driven development)和重构(refactoring)
修改数据系统并使其适应不断变化需求的容易程度,是与简单性和抽象性密切相关的:简单易懂的系统通常比复杂系统更容易修改。但由于这是一个非常重要的概念,我们将用一个不同的词来指代数据系统层面的敏捷性:可演化性(evolvability)

© Jeremy Yang 2021 - 2022