数据密集型应用的模块
当今许多应用大多是数据数据密集(data-intensive)而不是计算密集型(compute-intensive)的。所以CPU的处理能力往往不是应用程序的瓶颈。关键在于数据的量、数据的复杂度以及数据的快速多变性。
应用往往包含以下模块:
- 数据库:用于存储数据
- 高速缓存:缓存复杂或者操作代价昂贵的结果,加快下一次访问
- 索引:用户可以按照关键字搜索数据并支持各种过滤
- 流式处理:持续发送消息到另一个进程,处理采用异步方式
- 批处理:定期处理大量积累的数据
数据系统的架构
核心设计目标
可靠性
出现意外情况,比如硬件、软件故障、人为失误等,系统可以继续正常运转,至少确保功能正确。
硬件故障
比较容易出现的,硬盘崩溃,内存故障,电网停电。
第一反应是为硬件冗余来减少系统故障率。例如磁盘RAID,服务器双电源,甚至热插拔CPU,数据中心添加备用电源、发电机。
这样当一个组件发生故障时,冗余组件可以快速接管,之后运维人员可以修复或者更换坏掉的组件。
直到最近,采用硬件冗余方案对于大多数应用场景还是足够的,它让单机完全失效的概率降到最低。只要可以把备份迅速恢复到新的机器上,故障的停机时间在大多数应用中并不是灾难性的。
现在,通过软件容错的方式来容易多机失效成为新的手段,或者成为硬件容错方案的有力补充。例如滚动升级。
软件错误
这类故障更难预料。各个节点直接是由软件关联的,可能会导致更多的系统故障。
例如,
- 由于软件错误,导致特定的输入引发应用的崩溃。例如Linux内核bug,在2012年6月30的闰秒时候触发,导致很多应用程序被挂掉。
- 失控的进程把系统的资源耗尽,导致这些共享资源不能被释放。
- 系统的Dependency出了问题,返回值异常。
- 组件中的小故障触发另一个组件中的故障,进而触发更多的故障。
没有快速的解决方法。只能仔细考虑很多细节。
- 检查系统的假设条件和系统之间的交互
- 进行全面的测试
- 进程隔离,
- 允许进程崩溃后自动重启
- 反复评估、监控并分析生产环境中的行为表现。
例如消息队列中,输出消息的数量应等于输入消息的数量。如果发现不一致,则立即告警。
人为失误
人无法做到万无一失。运维人员的配置错误可能是系统下线的第一大原因。
要保证系统可靠,如何减少人为错误对它的影响?
- 用最小出错的方式来设计系统。让做错事更难。
- 想办法分离最容易出错的地方,容易引发故障的接口。使用Sandbox隔离真正的生产和测试环境。
- 充分的测试。单元测试,集成测试,手动测试。边界条件的考虑。
- 当出现人为失误时,有快速回滚或者回复的机制。滚动发布新代码。
- 监控子系统需要详细和清晰。
- 推行管理流程和相关培训。
可扩展性
随着规模的增长,例如数据量、流量和复杂性,系统应该可以用合理的方式进行应对,满足这种增长。
当应用负载增加的时候,比如用户从1w到100w,从100w到1000w,应用程序如何应对增长的负载。
相关参数:Web服务的QPS,数据库的写入比例,DAU,缓存命中率。有时候平均值很重要,有时候短时间内的峰值会成为系统瓶颈。
Twitter的Fan-out结构,对数据量提出了挑战。当一个人发Tweet时候,怎么处理Timeline这个请求。根据粉丝的数量,区别处理。
如何描述性能
系统负载增加后,会发生什么,两种思考方式
- 系统资源不变(CPU,内存,带宽),系统的性能会发生什么变化?
- 如果要保持性能不变,需要增加多少资源?
不同类型的系统关心的性能指标不同
- 批处理系统通常关心吞吐量(throughput),例如Hadoop,每秒可以处理多少条数据或者完成一个作业总共需要多少时间。
- Online系统中,更看重服务的响应时间(response time),即客户端从发出请求到得到回复的总时间。
对于响应时间,如下图,有一些很长的,算异常请求,可能是由于数据大很多。但也有可能是其他因素造成的,例如上下文切换、进程调度、网络丢包、TCP重传、垃圾回收STW,缺页中断、磁盘IO。
最好使用百分位数,中位数(50%)来评估系统的响应时间。
采用较高的响应时间百分位数很重要,因为直接影响用户的总体服务体验。例如亚马逊采用99.9百分位来定义服务响应时间。优化99.9%的目标可能成本很高。能不能带来收益很关键。
排队延迟(queueing delay)通常占了高百分位点处响应时间的很大一部分。由于服务器只能并行处理少量的事务(如受其CPU核数的限制),所以只要有少量缓慢的请求就能阻碍后续请求的处理,这种效应有时被称为头部阻塞(head-of-line blocking)
应对负载增加
垂直扩展和水平扩展。
好的系统有弹性特征,可以自动检测负载的变化,来自动添加更多的计算资源。
可扩展架构通常都是从通用模块逐步构建出来的。
可维护性
项目会随着时间的推移,项目会需要新的人员参与到开发和运维工作中,来满足系统的稳定和新场景的适应。系统应该高效的变化。
软件的大部分开销并不在最初的开发阶段,而是在持续的维护阶段,包括修复漏洞、保持系统正常运行、调查失效、适配新的平台、为新的场景进行修改、偿还技术债、添加新的功能等等。
为此,我们将特别关注软件系统的三个设计原则:
可操作性(Operability)
便于运维团队保持系统平稳运行。
良好的可操作性意味着更轻松的日常工作,进而运维团队能专注于高价值的事情。数据系统可以通过各种方式使日常任务更轻松:
简单性(Simplicity)
从系统中消除尽可能多的复杂度(complexity),使新工程师也能轻松理解系统。(注意这和用户接口的简单性不一样。)
复杂度(complexity)有各种可能的症状,例如:状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的Hack、需要绕开的特例等等,
可演化性(evolability)
使工程师在未来能轻松地对系统进行更改,当需求变化时为新应用场景做适配。也称为可扩展性(extensibility),可修改性(modifiability)或可塑性(plasticity)。
组织流程方面,敏捷开发,TDD,重构。
修改数据系统并使其适应不断变化需求的容易程度,是与简单性和抽象性密切相关的:简单易懂的系统通常比复杂系统更容易修改