Ch1#
开篇老生常谈的可靠性,可伸缩性,可维护性
大部分应用是数据密集型应用。
需要的数据组件包括但不限于:
- 数据库
- 缓存
- 搜索索引
- 异步处理
- 流处理
- 批处理
存储数据,以便自己或其他应用程序之后能再次找到 (数据库(database))记住开销昂贵操作的结果,
加快读取速度(缓存(cache))允许用户按关键字搜索数据,或以各种方式对数据进行过滤(搜索索引(search indexes))向其他进程发送消息,进行异步处理(流处理(stream processing))定期处理累积的大批量数据(批处理(batch processing))
可靠性:
故障(fault)
失败(failure)
故障通常定义为系统的一部分状态偏离其标准,而失效则是系统作为一个整体停止向用户提供服务。故障的概率不可能降到零,因此最好设计容错机制以防因故障而导致失效。本书中我们将介绍几种用不可靠的部件构建可靠系统的技术。
硬件故障
软件错误
人为错误
CH2#
关系模型
问题:与应用代码层的 OOP 不匹配,需要 ORM
SQL
文档模型
天然 OOP。
但是对于多对多关系不好处理。
在需要连接的情况下,可能得需要应用层手动连接
图数据模型
Cypher 查询语言
RDF 数据模型
存储引擎
日志式
页面式
以日志式的 KV 为例
- 通过索引快速定位数据
- 执行压缩和分段合并(所以如何避免最终用完磁盘空间?一种好的解决方案是,将日志分为特定大小的段,当日志增长到特定尺寸时关闭当前段文件,并开始写入一个新的段文件。然后,我们就可以对这些段进行压缩(compaction))
Hash Index
SSTable (对于不存在的键,性能低下,存储引擎通常使用额外的 Bloom 过滤器)
B 树索引
基于 page
问题:在更新时,更新一个页面可能会导致页面拆分,这导致除了写两个新页外,还需要修改父页面的引用。这个过程并非是原子性的。
这导致崩溃时,整个存储不一致,因此需要 WAL,来做一个 redo log
在对外的并发时,多个线程看到的页面不一致,需要做特殊处理
优化:
写时复制(CopyOnWrite):修改的页面被写入到不同的位置,并且树中的父页面的新版本被创建,指向新的位置。
数据模型与查询方式#
数据存储与索引#
数据 Schema 格式及其演化#
数据序列化、反序列化(编码格式)#
文本、人类可读、二进制
Self Contained 如 JSON,XML,CSV
Schema First 如 Thrift、Proto Buffer、Avro
数据类型
- 如何编解码
- 格式的可读性、空间效率
- Schema 的变动处理
向前、向后兼容性#
面对变化的处理、向前、向后兼容性
向前:用新 Schema 去解释以前的值,(对于服务端而言,旧客户端会提供旧的值)
向后:用旧 Schema 去解释新增的值,(对于客户端而言,服务端会提供新的值)
此外,同一时刻,系统内部可能存在多个不同 Schema 版本,有的是 5 年前写入的,有的是一年前写入的。
数据的流动模式#
数据库中的数据流动#
服务中的数据流动#
REST/RPC#
REST 提倡:
- 使用 URL 来标识资源
- 使用 HTTP 功能进行缓存控制
- 使用 HTTP 身份验证和内容类型协商
- 采用 OpenAPI 描述 API,人类可读
SOAP :
- 应当传输层无关(通常使用 HTTP,但不一定需要 HTTP)
- 数据格式通常为 XML
- 通过 WSDL 进行 API 描述
- 并且内置了一系列,WS-* 的标准来实现诸如缓存、身份验证的业务需求
RPC 相比于本地的函数调用,十分不可靠,需要更多的手段来保障其可靠性。比如延迟高,网络失败(请求丢失,响应丢失),因此为了保障恰好执行一次,需要做好错误处理、幂等、重试等保障机制。
也就是需要明确的是,RPC,不是本地函数调用,非常不同,因此需要完全不同的处理手段。
通常会使用 Future / Promise 这样的手段进行封装。
部分框架还提供服务发现机制。
服务定义与服务实现的编写的耦合度
RPC 演化#
一个系统有多个 RPC 服务,现实的需求必然是每个 RPC 的 Schema 能够独立更改。而且更新的模式期望是:先更新 RPC 服务端,后更新 RPC 客户端。
因此,可以简化兼容需求:只需要在请求上向后兼容,在响应上向前兼容。
服务兼容性变得更加困难,因为 RPC 通常用于跨组织边界的通信,因此服务提供者通常无法控制其客户端,也无法强制它们升级。因此,兼容性需要保持很长时间,也许是无限期的。如果需要破坏兼容性的更改,服务提供者通常最终会并行维护服务 API 的多个版本。
持久执行和工作流#
事件驱动数据流#
多服务工作流协调
持久化执行
基于 MQ 的 EventDriven 架构/ 消息代理
- 如果接收者不可用或过载,MQ 可以充当缓冲区,从而提高系统可靠性。
- MQ 可以自动将消息重新传递给已崩溃的进程,从而防止消息丢失。
- MQ 避免了服务发现的需要,因为发送者不需要直接连接到接收者的 IP 地址。
- MQ 允许将相同的消息发送给多个接收者。
- MQ 在逻辑上将发送者与接收者解耦(发送者只是发布消息,不关心谁使用它们)。
Actor 模式