笔记 02 - 流式架构-数据库技术在应用架构的应用

这篇书评可能有关键情节透露
封面图片来自 Event Sourcing pattern - Cloud Design Patterns
文章内容主要来自
Turning the database inside-out
Materialized View pattern 《Designing Data-Intensive Applications》The Future of Data Systems 这篇文章只是翻译和总结,如果感兴趣,一定去原文,防止被我误导。
传统的架构
首先看下传统 Web 系统架构

最上层是请求方,可能是浏览器或者某种移动客户端;中间的 Backend 接受请求,实现业务逻辑,这层是无状态的,可以同时启动很多实例,并且可以随意的扩展。最下层是数据库,这层保存着数据和状态,是架构上存储获取数据的重要层次。数据库在整个架构是巨大的,全局共享的,可变的,就像程序中的可变的全局变量。我们都知道程序中存在可变的全局变量是一种坏的设计,是一种共享内存的并发。各种技术(Actors,channels)都在避免共享内存的并发带来的死锁,并发修改,race conditions等问题。共享可变的数据库在整架构中一个重大隐患,可以思考下如何解决这个问题。我们先来看下常见的三种架构replication这是一种十分常见的技术,目的是在不同的节点同时维护相同的一份数据。Redis 和 MySQL中 Master Slave 节点有应用,这种 Leaders and Followers 模式最常见的实现,当然也有 Multi-Leader Replication(Tungsten MySQL) 和 Leaderless Replication (Dynamo)两种方式,因为Multi-Leader,Leaderless 涉及比较复杂的冲突处理,整体应用主要以 Leaders and Followers 为主。下文也是基于 Leaders and Followers 模式。如何实现 Replication 呢?一般有三种方式
- Leader 将执行的 command 发送到 Follower 节点,Follower 在自己的数据集上执行这条 command
- 将 Write Ahead Log 发送到 Follower 节点(与同步 command 的区别 Why do SQL databases use a write-ahead log over a command log?)
- Logical log,Leader 将受到 command 影响的数据行同步到 Follower 节点。command 是用来描述状态的改变,而 Logical log 是将每次发生变化的数据进行同步,这样每次数据的变化都形成了不可改变的事实。
secondary indexes二级索引在数据库中很常见,针对特定的数据列建立特定的数据结构进行存储,从而方便查询。需要关注的以下几点
- 索引并没有为添加任何额外的真实数据,只是已有数据的不同表示,索引完全可以通过已有数据进行重建。
- 创建索引是个本质是已有数据的转换过程,数据库已有数据作为输入,索引作为输出。这个过程是数据库内置实现的。
- 当数据库原始数据发生变化时,索引就会自动发生变化与新数据保持一致。建立索引的过程不是一次性的,而是持续性的。
- 索引与原始数据时事务性一致的,不会因为断电,磁盘损坏的故障导致索引与原始数据不一致。
- 有些数据库(PostgreSQL)支持索引的并行建立,不会 block 正常的数据库服务。
- 为了实现索引与高并发变化的数据保持一致,在索引根据快照建立的过程中,数据库必须跟踪快照建立之后的所有数据变化。
caching一般都是先从 cache 里取如果存在直接返回,如果不存在去下层的慢速存储处理获取返回,同时将结果设置在 cache 里,这是一种为了提高性能十分常见的架构。但是其中存在着一些问题
- 如何管理 cache 应该更新失效十分复杂,需要不断根据数据的变化更新 cache,但是这样非常容易遗漏和出错,如果仅仅是 time-to-live 的话,有很多系统又不能接受老旧数据的读取
- race conditions,在高并发的情况下会出现更新数据库的顺序有更新 cache 的顺序不一致,可能造成读取到旧数据,从而引起故障。
- 冷启动,当缓存不存在,突然出现来一波流量,就到打到底层的存储上使系统不稳定
现在回头看数据库的二级索引自动更新,永远保持最新,一致性得到保证,完全没有应用级 cache 的这些问题。 Cache 与 二级索引本质是一样的,都是原始数据的转换成不同的数据展现形式;二级索引是选择了特定的数据字段进行转换,cache 的的转换更加灵活可以应用任意的转换函数,耳机可以涉及多张数据表。materialized views一些数据库(PostgreSQL,Oracle,SQL Server)提供了这种架构的支持。简单说是这是数据库内部支持的自动更新(有的是周期或者手动更新),保持一致的缓存,可以把一些复杂查询的结果存储到数据库内,再有相同的查询执行的时候,直接获取相应的结果不在真正执行。这些都不需要应用层代码考虑。具体可以查看 Materialized view | Wikiwand。但是与 cache 相比 Materialized view 只支持 SQL 语句的组合,cache 则比较灵活支持各种业务逻辑的组合。当然可以使用存储过程来执行业务代码,但是存储过程不是一个好的选择,因为这会带来难以监控,部署,资源隔离等问题。现在我们来回顾一下这四种技术
- replication 同样的一份数据保存在不同的机器上,工作的很好,技术也非常成熟,容易理解和支持。
- secondary indexes 也非常成熟,能自动地保持索引数据的及时更新,并保持与原始数据的一致
- caching 应用级别的 cache 失效逻辑复杂,难以保持一致,高并发容易出现 race condition。整体技术方案比较复杂,混乱
- materialized view 整体来说还可以,是数据层面的 cache,可以组合各种业务逻辑,满足自动更新保持一致。但是它们实现的方式并不是你想要的现代应用程序想要的。 维护 materialized view 会给数据库带来额外的负担,而实际上缓存的目的是减少数据库的负载。
我们可以把这些技术组合起来产生的新的架构,做成一个 application materialized view。把消息流视为系统的一等居民,把实现 replication 时 Leader 和 Follower 之间的 replication stream 抽象成公共接口以供使用。

replication stream 是一种事务日志,所有的写入都是不可变的事实,就像上文提到的逻辑日志一样,可以通过 Kafka 进行实现。随后通过 consumer 进行消费,根据业务业务逻辑提前计算出不同种类的 materialized view,随后所有的读取都在 materialized view 上进行。把读写完全分离是这种架构的核心,所有的写都是事务日志,不需要原地做出数据更新,这样避免了并发修改全局状态的问题,仅仅维护了 append-only log。这样性能也十分好,易于扩展,进行故障修复。application materialized view VS cache

- 在转换过程与应用程序在不同的进程中进行的,与应用程序互不影响,相互独立。 中数据的转换过程与应用程序在不同的进程中进行的,与应用程序互不影响,相互独立,可以独立的监控,调试,扩容。cache 则与应用程序紧密的交缠在一起,相对而言比较容易引入问题。
- cache 会出现 miss 的情况,但是 materialized view 是提前计算的,不会出现 cache miss 的情况
- materialized view 十分灵活,容易创建新的 view 类型,只要把 log 从头重放进行消新逻辑的运算就可以了。新老的 view 的可以并行存在,也容易进行新老版本的替换。
现在使用重新发明的 materialized view,我们完全可以避免开头所提到的全局共享变量,造成系统不稳定的问题了。
这样是不是给了我们新的思考,也许能够避免数据库的繁琐使用。当然这种架构也面临着一些问题
- 流式架构系统由多个组件组成,每种组件的学习与运维都有一定成本。
- 存在数据延迟,要求数据强一致的场景并不适用。
这篇总结抛砖引玉,希望大家不局限于现有的架构,这篇总结带来了新的角度。😁
如果我写得东西给你带来了帮助,或者有什么疑问,欢迎来 《打开引擎盖》继续讨论。