9 存储
任何一个系统,从单核 CPU 到分布式,从前端到后台,要实现各式各样的功能和逻辑,只有读和写两种操作。而每个系统的业务特性可能都不一样,有的侧重读、有的侧重写,有的两者兼备,本节主要探讨在不同业务场景下存储读写的一些方法论。
9.1 读写分离
大多数业务都是读多写少,为了提高系统处理能力,可以采用读写分离的方式将主节点用于写,从节点用于读,如下图所示。
读写分离架构
读写分离架构有以下几个特点:1)数据库服务为主从架构,可以为一主一从或者一主多从;2)主节点负责写操作,从节点负责读操作;3)主节点将数据复制到从节点;基于基本架构,可以变种出多种读写分离的架构,如主-主-从、主-从-从。主从节点也可以是不同的存储,如 mysql+redis。
读写分离的主从架构一般采用异步复制,会存在数据复制延迟的问题,适用于对数据一致性要求不高的业务。可采用以下几个方式尽量避免复制滞后带来的问题。
1)写后读一致性:即读自己的写,适用于用户写操作后要求实时看到更新。典型的场景是,用户注册账号或者修改账户密码后,紧接着登录,此时如果读请求发送到从节点,由于数据可能还没同步完成,用户登录失败,这是不可接受的。针对这种情况,可以将自己的读请求发送到主节点上,查看其他用户信息的请求依然发送到从节点。
2)二次读取:优先读取从节点,如果读取失败或者跟踪的更新时间小于某个阀值,则再从主节点读取。
3)关键业务读写主节点,非关键业务读写分离。
4)单调读:保证用户的读请求都发到同一个从节点,避免出现回滚的现象。如用户在 M 主节点更新信息后,数据很快同步到了从节点 S1,用户查询时请求发往 S1,看到了更新的信息。接着用户再一次查询,此时请求发到数据同步没有完成的从节点 S2,用户看到的现象是刚才的更新的信息又消失了,即以为数据回滚了。
9.2 动静分离
动静分离将经常更新的数据和更新频率低的数据进行分离。最常见于 CDN,一个网页通常分为静态资源(图片/js/css 等)和动态资源(JSP、PHP 等),采取动静分离的方式将静态资源缓存在 CDN 边缘节点上,只需请求动态资源即可,减少网络传输和服务负载。
在数据库和 KV 存储上也可以采取动态分离的方式,如 7.6 提到的点播视频缓存的动静分离。在数据库中,动静分离更像是一种垂直切分,将动态和静态的字段分别存储在不同的库表中,减小数据库锁的粒度,同时可以分配不同的数据库资源来合理提升利用率。
9.3 冷热分离
冷热分离可以说是每个存储产品和海量业务的必备功能,Mysql、ElasticSearch、CMEM、Grocery 等都直接或间接支持冷热分离。将热数据放到性能更好的存储设备上,冷数据下沉到廉价的磁盘,从而节约成本。企鹅电竞为了节省在腾讯云成本,直播回放按照主播粉丝数和时间等条件也采用了冷热分离,下图是 ES 冷热分离的一个实现架构图。
ES冷热分离架构图
9.4 重写轻读
重写轻度个人理解可能有两个含义:1)关键写,降低读的关键性,如异步复制,保证主节点写成功即可,从节点的读可容忍同步延迟。2)写重逻辑,读轻逻辑,将计算的逻辑从读转移到写。适用于读请求的时候还要进行计算的场景,常见的如排行榜是在写的时候构建而不是在读请求的时候再构建。
在微博、朋友圈等社交产品场景中都有类似关注或朋友的功能。以朋友圈模拟为例(具体我也不知道朋友圈是怎么做的),如果用户进入朋友圈时看到的朋友消息列表是在请求的时候遍历其朋友的新消息再按时间排序组装出来的,这显然很难满足朋友圈这么大的海量请求。可以采取重写轻读的方式,在发朋友圈的时候就把列表构造好,然后直接读就可以了。
仿照 Actor 模型,为用户建立一个信箱,用户发朋友圈后写完自己的信箱就返回,然后异步的将消息推送到其朋友的信箱,这样朋友读取他的信箱时就是其朋友圈的消息列表,如下图所示:
重写轻读流程
上图仅仅是为了展示重写轻度的思路,在实际应用中还有些其他问题。如:1)写扩散:这是个写扩散的行为,如果一个大户的朋友很多,这写扩散的代价也是很大的,而且可能有些人万年不看朋友圈甚至屏蔽了朋友。需要采取一些其他的策略,如朋友数在某个范围内是才采取这种方式,数量太多采取推拉结合和分析一些活跃指标等。2)信箱容量:一般来说查看朋友圈不会不断的往下翻页查看,这时候应该限制信箱存储条目数,超出的条目从其他存储查询。
9.5 数据异构
数据异构主要是按照不同的维度建立索引关系以加速查询。如京东、天猫等网上商城,一般按照订单号进行了分库分表。由于订单号不在同一个表中,要查询一个买家或者商家的订单列表,就需要查询所有分库然后进行数据聚合。可以采取构建异构索引,在生成订单的时同时创建买家和商家到订单的索引表,这个表可以按照用户 id 进行分库分表。
10 队列
在系统应用中,不是所有的任务和请求必须实时处理,很多时候数据也不需要强一致性而只需保持最终一致性,有时候我们也不需要知道系统模块间的依赖,在这些场景下队列技术大有可为。
10.1 应用场景
队列的应用场景很广泛,总结起来主要有以下几个方面:
- 异步处理:业务请求的处理流程通常很多,有些流程并不需要在本次请求中立即处理,这时就可以采用异步处理。如直播平台中,主播开播后需要给粉丝发送开播通知,可以将开播事件写入到消息队列中,然后由专门的 daemon 来处理发送开播通知,从而提高开播的响应速度。
- 流量削峰:高并发系统的性能瓶颈一般在 I/O 操作上,如读写数据库。面对突发的流量,可以使用消息队列进行排队缓冲。以企鹅电竞为例,每隔一段时间就会有大主播入驻,如梦泪等。这个时候会有大量用户的订阅主播,订阅的流程需要进行多个写操作,这时先只写用户关注了哪个主播存储。然后在进入消息队列暂存,后续再写主播被谁关注和其他存储。
- 系统解耦:有些基础服务被很多其他服务依赖,如企鹅电竞的搜索、推荐等系统需要开播事件。而开播服务本身并不关心谁需要这些数据,只需处理开播的事情就行了,依赖服务(包括第一点说的发送开播通知的 daemon)可以订阅开播事件的消息队列进行解耦。
- 数据同步:消息队列可以起到数据总线的作用,特别是在跨系统进行数据同步时。拿我以前参与过开发的一个分布式缓存系统为例,通过 RabbitMQ 在写 Mysql 时将数据同步到 Redis,从而实现一个最终一致性的分布式缓存。
- 柔性事务:传统的分布式事务采用两阶段协议或者其优化变种实现,当事务执行时都需要争抢锁资源和等待,在高并发场景下会严重降低系统的性能和吞吐量,甚至出现死锁。互联网的核心是高并发和高可用,一般将传统的事务问题转换为柔性事务。下图是阿里基于消息队列的一种分布式事务实现(详情查看:企业 IT 架构转型之道 阿里巴巴中台战略思想与架构实战,微信读书有电子版):
基于MQ的分布式柔性事务
其核心原理和流程是:
1)分布式事务发起方在执行第一个本地事务前,向 MQ 发送一条事务消息并保存到服务端,MQ 消费者无法感知和消费该消息 ①②。
2)事务消息发送成功后开始进行单机事务操作 ③:
a)如果本地事务执行成功,则将 MQ 服务端的事务消息更新为正常状态 ④;
b)如果本地事务执行时因为宕机或者网络问题没有及时向 MQ 服务端反馈,则之前的事务消息会一直保存在 MQ。MQ 服务端会对事务消息进行定期扫描,如果发现有消息保存时间超过了一定的时间阀值,则向 MQ 生产端发送检查事务执行状态的请求 ⑤;
c)检查本地事务结果后 ⑥,如果事务执行成功,则将之前保存的事务消息更新为正常状态,否则告知 MQ 服务端进行丢弃;
3)消费者获取到事务消息设置为正常状态后,则执行第二个本地事务 ⑧。如果执行失败则通知 MQ 发送方对第一个本地事务进行回滚或正向补偿。
10.2 应用分类
- 缓冲队列:队列的基本功能就是缓冲排队,如 TCP 的发送缓冲区,网络框架通常还会再加上应用层的缓冲区。使用缓冲队列应对突发流量时,使处理更加平滑,从而保护系统,上过 12306 买票的都懂。
缓冲队列
在大数据日志系统中,通常需要在日志采集系统和日志解析系统之间增加日志缓冲队列,以防止解析系统高负载时阻塞采集系统甚至造成日志丢弃,同时便于各自升级维护。下图天机阁数据采集系统中,就采用 Kafka 作为日志缓冲队列。
天机阁数据采集系统
- 请求队列:对用户的请求进行排队,网络框架一般都有请求队列,如 spp 在 proxy 进程和 work 进程之间有共享内存队列,taf 在网络线程和 Servant 线程之间也有队列,主要用于流量控制、过载保护和超时丢弃等。
TAF请求接收队列 - 任务队列:将任务提交到队列中异步执行,最常见的就是线程池的任务队列。
- 消息队列
用于消息投递,主要有点对点和发布订阅两种模式,常见的有 RabbitMQ、RocketMQ、Kafka 等,下图是常用消息队列的对比:
常用消息队列
总结
本文探讨和总结了后台开发设计高性能服务的常用方法和技术,并通过思维导图总结了成一套方法论。当然这不是高性能的全部,甚至只是凤毛菱角。每个具体的领域都有自己的高性能之道,如网络编程的 I/O 模型和 C10K 问题,业务逻辑的数据结构和算法设计,各种中间件的参数调优等。文中也描述了一些项目的实践,如有不合理的地方或者有更好的解决方案,请各位同仁赐教。