数据密集型应用系统设计-1-可靠、可扩展与可维护的应用系统

认识数据系统

数据密集型应用(data-intensive)指计算压力小而数据量大,数据复杂度高,数据快速多变的应用。比如网站,游戏等网络服务。这类应用的瓶颈不是CPU的处理能力,而是数据的读写。

计算密集型应用(compute-intensive)指计算压力大的应用。比如机器学习模型训练等,瓶颈常常在CPU,GPU算力上。

数据密集型应用常常由标准模块构成,常见模块如下:

  • 数据库:用来长期存储数据,保证对数据可以反复访问
  • 高速缓存:用来存储那些查询复杂缓慢的数据,加快下一次访问
  • 索引:加快数据查询的方式,支持关键字搜索和各种过滤
  • 流式处理:持续发送消息至另一个进程,处理采用异步方式
  • 批处理:定期处理大量的累积数据
    数据系统设计便是根据需求,找到多个不同的数据处理模块,将其组合,使其协作,并对外界隐藏内部的实现细节,低成本高质量的满足需求。

在满足需求的前提下,衡量一个数据系统的好坏一般可以从下面三个方面进行。

可靠性

当出现意外情况如硬件,软件故障,人为失误时,系统应该可以正常运转。虽然性能可能有所降低,但保证功能正确。

可能出错的事情称为错误或故障,系统可应对错误则称为容错或弹性。

硬件故障

应对硬件故障的方式就是硬件冗余,增加服务节点的数量,增加数据的备份数量等等。当一个组件出现故障的时候,冗余组件可以快速接管。要做到这一点,其实是软件层面的考量,通过软件控制多台服务器共同工作,动态分配负载等等。软件升级时同样可以通过控制多个节点轮流升级来保证服务不停用。

软件错误

软件错误所导致的故障更加难以预料,影响范围也会更广。软件错误没有快速解决的方法,只能通过对细节的把控来尽量避免。比如认真检查服务间的依赖关系,假设某环节不可用是否会导致大面积雪崩。认真进行测试,进程隔离,允许进程崩溃并自动重启,加强监控等等。

人为失误

人是不可靠的,经常会犯错误,为了避免人为失误造成的故障,可以

  • 以最小出错的方式设计系统,比如精心设计抽象层,API和管理界面,使整个开发流程规范化,使做对很容易
  • 分离出最容易出错的地方,容易引发故障的接口。提供一个类似生产环境的沙箱,让人可以放心尝试
  • 充分测试,单元测试,集成测试,手动测试,回归测试等等。
  • 当出现了人为失误的时候,提供快速恢复机制。如快速回滚,滚动发布,灰度发布等等。
  • 设置清晰的监控系统,包括性能指标和错误率。
    另外需要注意的是,可靠性是一个相对的概念,在有限的成本下设计出尽可能可靠的系统才是常态。完全可靠的系统并不存在,为了可靠性而使成本陡增也要考虑是否值得。

可扩展性

随着规模的增长,比如数据量的增加,流量或复杂性的提升。系统应该以合理的方式来匹配这种增长,让扩展所需的成本尽量小。

可扩展性是用来描述系统应对负载增加能力的术语。扩展性通常要考虑类似当系统使用增加是有何种措施应对,如何添加计算资源等问题。

描述负载

需要用一些参数来简洁的描述出当前系统的负载情况。比如服务器的每秒请求处理次数(qps)。数据库的写入比例。产品的活跃用户数量,缓存命中率等等。

推特的例子:

  • 发布推特:平均4.6k request/sec,峰值12k request/sec

  • 浏览:平均 300k request/sec
    处理难点不在峰值流量,而在巨大的扇出结构:每个人会关注很多人,也会被很多人关注。解决方案有两种:

  • 将新发送的消息写入全局集合中,每个用户读取时根据自己的关注列表从全局集合中查找,然后按时间排序

  • 为每个用户维护一个关注缓存队列,当一条消息发送之后,将这个消息推送到每个关注了这个人的关注队列中。
    用第一个计划的问题在于,查询消耗过大,随着用户量的不断上涨,数据库逐渐不堪重负。

第二个方案也存在明显的缺点,发布消息这个动作的工作量变得很大。一个典型的问题是,当一个明星(关注者非常多)的人发送推特时,这个方案会十分耗时。

推特最后的处理方案是将1和2做了结合。采取方案2的每人一个队列的做法,但是对一些特殊用户(关注者多)进行方案一的处理。即每个人的消息队列中维护了大部分其关注者发送的消息,对少数名人发送的消息则需要刷新的时候查询。

描述性能

需要通过一些数据来描述系统或者机器的性能,再结合负载数据,得出该系统能否胜任,或者该系统应该配置多少台机器。

吞吐量是其中的一个指标,常被批处理系统如hadoop运用。其定义是每秒钟可处理的记录条数。

响应时间是另一个指标,常被用在线性系统中。定义是客户端从发送请求到接到响应的间隔。响应时间通常不是一个确定的数字,因为每次请求的时间都不同,而是一组数值分布。

由于响应时间的不确定性,要想衡量一个系统的性能,需要对一组响应时间进行分析。此时,平均值并不是一个很好的选择,会丢失细节。百分位数更好。

比如中位数为200ms则表示又一半的用户响应时间大于200ms。更加常用的指标是p95,p99,p999等,分别表示95%,99%,99.9%的请求时间小于200ms。

在制定服务性能标准时,可以用p999来制定,即保证99.9%的请求要快于200ms。如果采用p9999则过高,付出的成本小于收益。

应对负载增加的方法

首先可以将响应时间百分比数据加入监控,比如计算10分钟这个滚动窗口中的p99,以此分析当前负载是否达到了系统的性能上限。

如果负载快速达到了系统的预期性能上限,则要考虑现有架构是否支持快速的垂直或者水平扩展。结合成本,给出合理的设计和扩展方案。

更高级的系统具有弹性特征,可以自动检测负载增加,然后自动加入更多计算资源。如果负载高 度不可预测,则自动弹性系统会更加高效。

由于无状态服务直接水平扩展比较容易,而有状态服务水平扩展的复杂度很大。因此之前的通用做法是将数据库运行在一个节点上,进行垂直扩展,直到不得不做水平扩展。

超大规模的系统由于用途和压力不同,很难有通用架构,需要考虑数据读取量、写入量、待存储的数据量、数据的复杂程度、 晌应时间要求、访问模式等因素进行定制化的架构设计。

扩展性好的架构会做出某些假设,然后有针对性地优化设 计,如哪些操作是最频繁的,哪些负载是少数情况。

可维护性

随着时间的推移,许多新的人员参与到系统的开发与运维中,以维护现有功能或适配新场景等。系统都应该高速运转。

通过软件设计的三个原则,可以更好的设计出便于后续维护的软件系统。

可运维性

优秀的软件应该在以下方面提供便利的方法,以便软件的日常运维工作。

  • 提供对系统运行时行为和内部的可观测性,方便监控。
  • 支持自动化, 与标准工具集成。
  • 避免绑定特定的机器,这样在整个系统不间断运行的同时,允许 机器停机维护。
  • 提供良好的文档和易于理解的操作模式,诸如“如果我做了X,会发生Y”。
  • 提供良好的默认配置,且允许管理员在需要时方便地修改默认值。
  • 尝试自我修复,在需要时让管理员手动控制系统状态。
  • 行为可预测,减少意外发生。

    简单性

随着系统功能的逐渐丰富,应该尽力避免系统变得复杂,其中包括状态空间的膨胀,模块紧搞合,令人纠结的相互依赖关系, 不一致的命名和术语,为了性能而采取的特殊处理,为解决某特定 问题而引人的特殊框架等。

消除意外复杂性最好手段之一是抽象。 一个好的设计抽象可以隐藏大量的实现细节, 并对外提供干净、易懂的接口。例如,高级编程语言作为一种抽象,可以隐藏机器汇编代码、 CPU寄存器和系统调用等细节和复杂性。 SQL作为一种抽象,隐藏了内部复杂的磁盘和内存数据结构 ,以及来自多客户端的并发请求,系统崩愤之后的不一致等问题。

可演化性

一成不变的系统几乎不存在,可演化的目标是可以轻松地修改数据系统,使其适应不断变化的需求,这和简单性与抽象性密切相关: 简单易懂的系统往往比复杂的系统更容易修改。


数据密集型应用系统设计-1-可靠、可扩展与可维护的应用系统
http://www.bake-data.com/2022/03/26/数据密集型应用系统设计-1-可靠、可扩展与可维护的应用系统/
Author
shuchen
Posted on
March 26, 2022
Licensed under