缓存系统
缓存系统设计
常见的缓存系统会采取 Key-Value 的非结构化存储,其需要具备的标准特性是:高性能、高扩展与高可靠。其高性能往往采取高速缓存、内存或 SSD 等存储方案,高扩展会综合应用多种存储引擎,并通过中间件或者负载均衡等方式保证扩展能力,而高可用则类似于我们在高可用架构中讨论的,需要有多种容灾与隔离方案。
数据引擎
Memcache
Memcache 是基于内存的数据库,仅支持 KV 类型数据,不支持持久化。它适用于数据小而简单,读多(QPS 万以上)写少,且偶尔数据丢失不应该对业务产生较大影响;例如访问量显示,session manager 等功能。
Redis
Redis 是基于内存的数据库,除了标准的 KV 类型,还支持 String,List,Hash,Set,SortedSet 等复杂类型。它适用于数据形式复杂,偶尔数据丢失不应该对业务产生较大影响;例如排行榜、最新项目检索、地理位置存储及 range 查询。
LevelDB
LevelDB 是基于 SSD 硬盘,仅支持 KV 类型数,支持持久化。它适用于数据简单,有持久化需求,且读写 QPS 较高(万级别)但存储数据较简单的应用场景;例如订单计数,库存记录等功能,更新非常频繁。
系统架构
-
Client——在初始化时,会从 Config server 处请求数据的分布信息,根据获取到的 key 和 Data server 的对照表,和 Data server 交互完成用户的请求;
-
Config Server——管理 Data Server 节点、维护 Data Server 的状态信息,为了保证高可用性质,采用了一主(master)一备(slave)的方式保证可靠性;
-
Data Server——负责数据存储,按照 Config Server 的指示完成数据复制和迁移工作,并定时给 Config Server 发送心跳信息。
路由负载均衡
缓存系统中基本的路由对照表是由 Config Server 生成的提供给 Client 使用 key 来寻找对应 Data Server 的路由表。主 Config Server 会利用心跳机制检测 Data Server 的存活情况,并更新路由对照表,待客户端请求时返回新的路由表。备 Config Server 也会通过心跳,检测各自的存活情况;当主 Config Server 失活,备 Config Server 及时切换成主 Config Server 提供服务。Config Server 会每次为对照表维护一个版本号,每次将对照表与版本号也会发给 Data Server。客户端请求数据的时候,Data Server 每次都将自己的数据对照表版本号放入 response 中,客户端收到 response 后,将对照表 version 与本地比较,如果不相同,则再次向 Config Server 请求最新对照表。
负载均衡采用的是一致性哈希算法,简单来说,一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数 H 的值空间为 0 - 232-1(即哈希值是一个 32 位无符号整形),将各个服务器使用 H 进行一个哈希,具体可以选择服务器的 ip 或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将三台服务器使用 ip 地址哈希后在环空间的位置。
与直接对 key 进行 hash 的一致性哈希算法不同的是,config server 首先通过 hash 将所有的 key 分到 Q 个 bucket 中,在缓存系统里 bucket 是负载均衡和数据迁移的基本单位,config server 根据一定的策略把每个桶指派到不同的 data server 上,因为数据按照 key 做 hash 算法,保证了桶分布的均衡性,从而保证了数据分布的均衡性,如图所示:
高可用部署
- 双机房单集群单份
缓存系统部署在两个机房中,数据存储份数为 1,其优点在于服务器存在于双机房,任一机房宕机服务保持可用,并且单份数据,无论应用在哪个机房,看到的都是同一个数据。不过应用服务器会跨机房访问,并且当一边机房出现故障时,缓存系统中的数据会失效一半。
- 双机房单集群双份
相较于单份存储这里会存储两份数据,每个机房拥有独立集群,应用在哪个机房就访问相同机房的集群,不会出现跨机房调用和流量,并且单边机房故障,不会影响业务访问命中率。不过这种架构对于后端数据源就有了特别的要求,多集群间一致性保证依赖于后端的数据源。当后端数据源数据发生更新后,业务不能直接把数据 put 到缓存系统,而是先需要调用 invalid 接口来失效这些对等集群中的数据(来保持各集群的数据和后端数据源的一致性)。之后业务可以把数据 put 到当前集群(注意:只会 put 到本机房的集群,不会 put 到对端集群)或者在读缓存系统时发生 not exist 的时候从后端数据源取来放入缓存系统。
- 双机房独立集群
在两个机房中各自部署 2 个独立的集群,这两个集群没有直接关系。还多了一个 invalidserver 的角色,invalidserver 接收客户端的 invalid 或者 hide 请求后,会对各机房内的集群进行 delete 或者 hide 操作,以此保障整个缓存系统中的数据和后端数据源保持一致的。这种部署方式同样避免了跨机房的调用和流量,在单边机房故障时候不会影响到业务访问命中率。其不足同样在后端数据源发生数据变更后,需要手动调用 Invalid 接口来失效数据然后再添加。
- 双机房主备集群
这种部署方式中,存在一个主集群和一个备份集群,分别在两个机房中。正常情况下,用户只使用主集群,读写数据都与主集群交互。主备集群会自动同步数据(不需要业务去更新两边),保证两个机房数据的最终一致性。当一个机房发生故障后,备集群会自动切换成主集群,提供服务,保证系统可用性。这种部署方式的数据安全和服务可用性都比较高,不过在读多写少的情况下会造成负载不均衡。
Memcache 存储引擎适用于双机房单集群单份,双机房独立集群,双机房单集群双份。Redis 存储引擎适用于双机房单集群单份。LevelDB 存储引擎适用于双机房主备集群,双机房单集群单份。
热点与限流
单台 Data Server 限流策略有两种:
-
要首先触发单机的最大阈值,再根据 namespace 配置的阈值判断是否已经超出限制。
-
namespace 配置的阈值超出单机的最大阈值限制即触发限流。超出部分的请求将返回限流返回码。
我们应该避免热点 key。所谓热点 key,就是个别会被大量访问的 key,由于 key 根据 hash 值落到单台 Data Server 上,因此很容易触发限流。同时还需避免单次批量请求过多的 key。对于热点 Key,应该划出专门的 Hot Zone 来进行存储。各个接收线程单独计数,使用 threadLocal + LRU HashMap,然后由后台统计线程进行汇总计算,从而识别得到热点数据。
对于读热点,在 Data Server 中划分出 Hot Zone,该区域会存储热点数据。Client 初始化时会获取到哈希机器配置,然后会随机选取一台 Data Server 作为热点数据读写固定的 Hot Zone。Data Server 会向 Client 反馈的热点数据,当数据被识别为热点时,会先到固定的 Hot Zone 进行读取,获取失败则按原先路由方式到源 Data Server 进行取数,后面再通过异步的方式将数据更新到 Hot Zone,也就是 Hot Zone 和源数据的 Data Server 形成了二级缓存。通过这种方式就可以将热点数据查询压力进行分摊到各个 Hot Zone,水平扩展能力得到提升。
对于写热点,如果使用多级缓存的方式会有数据一致性的问题,写热点是通过在服务端写合并的方式,以减少数据实际写次数。当 Data Server 识别出热点数据后,不会立即进行操作,而是将该数据交给热点线程处理,热点线程会将一定时间内对同一个 key 的操作进行合并,随后由定时线程按照预设的合并周期将合并后的请求提交到引擎层,处理完成后再返回结果给客户端。