2.1.3 存储层设计和选型
在了解了与接口、表现层相关的通信策略后,我们再来介绍应用程序的另一个重要组成:存储层。冯·诺伊曼提出的“计算-存储”体系在微服务架构下仍然成立:系统中大部分的组件都是无状态的或易失状态的计算单元,少部分组件负责将数据持久化,也就是“落盘”。存储层通常位于系统架构的底层,可以称之为“后端的后端”。
1.数据库
设计微服务的存储层时,首先要回答的问题是,应该使用何种数据库。稍有规模的软件公司都会专门设置数据库管理者这一职位,数据库的重要性和复杂性可见一斑。
我们可以从以下几个维度来分析业务对于数据库的需求。
● 数据模型:需求包括实体属性是多是少、关联关系是否复杂、查询和写入的粒度是否一致等。
● 数据量:需求包括存量和增量是否分片、是否冗余存储等。
● 读写场景:需求包括读写速率和速率变化率、并发度如何,对延迟是否敏感,读写数据分布是否集中(是否有热区)等。
为了满足这些需求,目前常用的数据库架构和技术可以按以下几个维度进行归类。
● OLTP与OLAP:OLTP(在线事务处理)面向广大客户,数据输入和输出量级较小、计算负担较小,但是对延迟敏感,对数据的准确性和时效性要求高;OLAP(在线分析处理)面向数据专家客户和公司管理者,数据输入和输出量级大、计算负担大,可能会对PB级别的输入数据进行全表扫描和聚合计算,对延迟相对不敏感。
● SQL与NoSQL:SQL是指以MySQL为代表的支持ACID事务的传统关系数据库。NoSQL最初是对传统SQL数据库及数据库设计范式的反抗,但是随着它的发展壮大,NoSQL一直在借鉴与融合SQL的设计,甚至对SQL数据库的设计产生了反哺,二者有殊途同归的趋势。目前关于NoSQL的定义,比较中肯的解读是Not only SQL(不只是SQL)。NoSQL在使用场景上又可以细分为key-value(读写实体的粒度完全一致,零关联)、搜索引擎(支持基于文本分析的全文检索、复合查询)和图数据库(专精于查询复杂的关系,使用场景包括社交网络、知识图谱等)几类。
结合上面介绍的需求维度和技术维度,微服务在存储层设计和选型时可以参考以下决策步骤。
● 如果数据模型、数据量、读写场景的需求都不复杂,优先考虑SQL数据库。
● 在OLTP典型场景下(对延迟敏感,数据量不大),可优先考虑通过SQL数据库进行数据切分(分库分表);如果SQL数据库的读写性能无法满足要求,可以考虑引入缓存、队列等中间件。
● 在OLAP典型场景下(对延迟不敏感,数据量超出单机承载范围、模型复杂),可以选择时序数据库、Hive、列存储数据库等。
● 在更加复杂的混合场景下,可以让不同的微服务根据需求采用不同的数据库和存储中间件。例如使用搜索引擎来支持全文搜索,使用内存key-value数据库来存储用户登录信息,使用时序数据库来记录和查询用户交互事件和数据变更历史记录,等等。
2.缓存
缓存是计算机软硬件系统广泛存在的一类组件和设计模式,存储层的典型代表是内存key-value型数据库,它比读写硬盘的数据库快,可以作为中间件挡在主数据库的前面,产生以小硬件撬动大流量的杠杆效应。完全在内存中处理数据虽然快,但代价是机器停电或进程重启会导致数据丢失。为了解决易失性问题,有些缓存(如Redis)也提供了数据持久化方案。
缓存的主要设计指标之一是命中率,命中率越高说明缓存的作用越大,另一方面也预示着缓存被击穿时对数据库的影响越大。缓存的另一个设计指标是流量分布的均衡性。如果某个key承载了过大的流量(热点),容易造成存储硬件(内存、网卡)的损坏,热点击穿甚至可能引发新的热点,造成系统雪崩。
应用要使用缓存,需要允许缓存中的数据和数据库的数据出现短暂的不一致。应用可以选择伴随写数据库的操作(之前、之后均可)更新缓存,也可以选择伴随读数据库的操作来更新缓存并设置缓存的过期时间,甚至可以选择离线定期更新缓存,在线只读取缓存,未命中也不穿透到数据库。如果应用设置了缓存过期时间,还要小心处理在缓存过期到缓存更新的间隙中数据库的流量尖峰。
3.队列
队列作为先进先出(FIFO)的数据结构,是各种数据库系统的持久化、跨节点同步和共识机制的实现基础,保证了进入数据库的数据不丢失、不乱序。例如MySQL的Binlog、Lucene的Transaction Log,事实上都是一种队列。
作为中间件,队列可以挡在主数据库前面,解决数据高并发(化并为串)和流速剧烈变化(削峰填谷)的问题。而在消息驱动的架构(Event-driven Architecture)设计中,队列的重要性再度提升,作为系统的事实来源(source of truth),解决了多个数据库系统的同步问题。
队列的核心设计指标是吞吐量和队列长度。生产能力大于消费能力时,队列长度增加,反之减少。如果生产能力长期大于消费能力,队列会被击穿,造成系统不可用,这时需要考虑队列的扩容(提升消费能力)。相反,如果生产能力长期小于消费能力,队列资源会闲置,这时候需要考虑队列的复用(提升生产能力)。
由于队列本身具有串行的特性,应用需要识别和处理少量数据以限制队列吞吐量(类比交通事故导致道路拥堵)。解决办法通常是在消费端设置合理的超时重试机制,将重试超过一定次数的数据从拥堵队列中移除并存储到其他地方,比如另一个“死信”队列(dead letter queue)中。
微服务在存储层设计和选型的灵活度上比传统的单体应用更有优势,不同的微服务可以使用不同的数据库和存储中间件,但微服务架构同时也引入了许多分布式系统特有的问题,比如跨服务保持事务性约束,处理网络延迟和中断造成的数据丢失、乱序、陈旧和冲突,等等。
云原生应用存储层的设计选型更是一个方兴未艾的领域,涉及很多的开放性问题。例如在技术选型上,为实现某种业务需求,应该直接使用云商的SaaS数据库,还是基于PaaS托管开源的数据库,抑或基于IaaS搭建和运维数据库,甚至选择存储层不上云确保对敏感数据的管控?在运维方面,如何快速、正确地对存储层进行扩容和缩容,能否像无状态的计算组件那样使用容器和容器编排?在产品设计上,如何避免或减少跨区域的数据读写和同步,弱化网络延迟对用户体验的影响?在安全性方面,如何设计和贯彻云上数据的访问权限和内容解析,特别是当公司和云商在业务上构成竞争关系时。
总而言之,数据密集型应用的存储层设计和选型应该遵循简单、可靠、可扩展原则:不要用“牛刀”杀“鸡”,不要盲目求新求变,不要过度依赖ACID事务、存储过程、外键等不可扩展的约束。