分布式数据缓存系统的设计与实现

随着现代企业互联网应用的规模越来越大,系统面对大量并发请求的处理能力越来越重要,在整个应用系统架构中,数据库层的访问速度会成为整个应用系统的瓶颈。数据缓存技术能够缓存数据库的查询结果,当下次访问同样数据时,可以直接从缓存中取,有效地降低了数据库层的访问负载量,提高了系统性能。但是,随着网络用户的不断增加,一台服务器的性能根本无法满足大量的并发请求,这种情况下就要使用服务器集群,而在集群环境下的缓存就是分布式缓存。

近年来出现了不少缓存产品,如 Memcached,JBoss Cache,OSCache,Ehcache 等,但是这些分布式缓存产品在数据冗余备份和失败恢复方面存在一定的不足。Memcached 并不支持冗余备份,服务器端所有节点的缓存内容是互异的,如果一个节点出现故障宕机,那么该节点保存的所有数据也就没有了,所以 Memcached 存在单点失效问题。JBoss Cache 支持两种冗余策略:全局复制和 Buddy 复制,全局复制将数据复制给集群中所有节点,保证在失败转移时可以转移到集群中任何一个节点,但它限制了系统的伸缩性;Buddy 复制则挑选特定节点担当备份数据节点,但作为冗余备份的节点是通过其 xml 文件设置的,当备份节点失效时,就无法启动新的节点作为失效节点。OSCache 提供的集群功能是非常有限的,无法让缓存中数据在各个节点间复制。Ehcache 是一个 Java 语言开发的简单缓存系统,没有提供冗余备份和失败恢复功能。

因此,设计并实现一种具有数据冗余备份和失败恢复功能的分布式数据缓存系统就很有意义了。该研究课题来源于中航信信天游电子客票验真官网项目。

1 系统应用架构

在现代企业应用分层架构中,数据缓存层通常以中间件的形式存在于数据库层的前面。信天游电子客票验真官网的分层架构见图 1 所示。本缓存系统部署在应用服务器集群上,即在应用程序层缓存数据,第一次请求时访问数据库层查询数据,同时将数据缓存起来,将来有相同请求时,直接从缓存系统中返回数据,减小了数据库层的压力,有效提高了系统的整体性能。

分布式数据缓存系统的设计与实现

图 1 应用架构图

2 缓存框架设计

本文设计的分布式缓存系统采用 Peer-To-Peer 的拓扑结构,集群中每个缓存节点都知道其它所有节点的存在,每个节点可以与任一节点进行通信,这种结构具有单跳数据分发,低延时的特点。本缓存系统作为信天游电子客票验真官网的数据缓存层,功能是完成对数据库查询结果的缓存,减少客户请求的处理时间,降低数据库端的负载,同时还要具有数据冗余备份和失败恢复的功能。缓存系统主要由以下模块构成:缓存管理模块、复制缓存模块、分布式缓存模块、数据分布模块、替换算法模块、缓存同步模块、缓存通信及可靠性服务模块。缓存系统设计框架见图 2。

分布式数据缓存系统的设计与实现

图 2 缓存系统设计框架图

3 系统主要模块设计

本系统使用 Java 语言开发,采用模块化设计思想和基于面向对象的设计方法,具有较好的可扩展性。下面介绍主要模块的设计及实现。

3.1 缓存管理模块

缓存管理模块在本系统中由 CacheManager 类来实现,它是整个缓存系统的入口点,负责初始化系统的所有配置信息,建立和管理缓存对象,以及协调各模块间工作。初始化过程如下:首先构造 CacheManager 对象,调用 init()方法,该方法先调用 ConfigurationFactory类的 parseConfiguration()方法构造一个 Configuration 类对象,然后用 Configuration 对象构造一 个 ConfigurationHelper 类 对 象 , 最 后 以 该 ConfigurationHelper 对 象 为 参 数 , 调 用CacheManager 类的 configure()方法完成所有配置信息的初始化。

3.2 缓存数据分布与同步

本文设计的是分布式数据缓存系统,必然缓存要工作在集群中的多台机器上,那么缓存数据如何在集群中的机器节点间分布就很重要,它保证了缓存数据在集群上的正确性。当集群中某个节点的缓存数据过期或有数据更新时,如何保证所有节点间的缓存数据同步,这就需要有效的同步机制来保证缓存节点间的数据相同。本文设计的缓存系统实现了两种分布式缓存:复制缓存和分布式缓存。

3.2.1 复制缓存(Replicated Cache

复制缓存就是集群中每一台机器节点的缓存内容都是一致的,每个节点都有一份缓存数据的完整备份。当复制缓存中有数据更新时,并不需要数据分布算法,直接把数据更新到本地机器的复制缓存中,然后调用通信模块把更新消息发送给集群中其它节点,保证所有节点缓存数据同步。更新过程见图 3。

分布式数据缓存系统的设计与实现

图 3 复制缓存更新过程

当从复制缓存中查询数据时,直接从本地机器的缓存中查找数据,如果存在,直接返回;如果不存在,即从本地数据库中查询数据,把查询结果放入本地缓存,返回查询结果,同时调用缓存通信模块把更新消息发送给集群中其它节点。查询过程见图 4。

分布式数据缓存系统的设计与实现

图 4 复制缓存查询过程

复制缓存采用的同步机制[3]是 TTL 模式和客户端失效模式。客户端失效模式是指当本地复制缓存有数据更新时,调用通信模块向集群中其它节点发送更新消息,保证所有节点缓存数据同步,客户端失效模式是通过给复制缓存注册监听器的方式实现的。TTL 模式是指,在向缓存中插入数据对象时,给该对象一个生存周期(TTL),当从缓存中取对象时,首先查看它的 TTL 是否过期,如果过期,便删除,再从本地数据库中查询数据,把数据重新放入缓存;后台会启动一个线程不断地检测本地缓存中的对象是否过期,如果过期,就从缓存中删除数据,最后还要调用通信模块把更新消息或失效消息发送给集群中其它节点。复制缓存由 ReplicatedCache 类实现,该类提供了操作复制缓存的所有 API 接口。

3.2.2 分布式缓存(Distributed Cache

分布式缓存使用强大的数据分布算法把缓存数据分布到集群中的各个节点上,每个节点都保存一部分数据,整个缓存数据分散到了集群中的每个节点上,同时把备份数据分布到不同的节点上,保证系统的可靠性。首先讨论分布式缓存使用的分布式算法:一致性哈希算法(Consistent Hashing)[4]。它是一种哈希算法,将一个所给数据映射为一个 32 位的哈希值,也就是 0 至 2^32 的数值空间,将这个空间看作为一个首尾相接的圆,见图 5 所示。缓存数据分布过程:首先求出集群中所有机器节点的哈希值,分布式缓存使用每个机器的 IP 地址作为哈希函数的键,将计算出的所有哈希值映射到 0 至 2^32 的圆上;然后使用相同的哈希函数求缓存数据键的哈希值,并映射到圆上,从键映射到的位置开始顺时针查找,将数据保存在找到的第一个缓存节点上;如果超过 2^32 还找不到节点,就把缓存数据保存在第一个节点上。同时,将缓存节点顺时针方向的下一个节点作为数据的备份节点。见图 5所示,object1 映射到的位置超过 2^32 还未找到缓存节点,就把 object1 保存在第一个节点nodeA 上,nodeB 作为备份节点。

分布式数据缓存系统的设计与实现

图 5 一致性哈希算法(1)

分布式数据缓存系统的设计与实现

图 6 一致性哈希算法(2)

如果分布式缓存系统中添加了一台新机器,假设为 nodeD,并映射到图 5 中 nodeC 和nodeA 之间,见图 6 所示,则原来映射到 nodeA 的 object1 现在要重新分布在 nodeD 上,只有 nodeC 和 nodeD 之间的数据要重新分布。从上面可以看出,一致性哈希算法最大限度地抑制了键的重新分布,增加了缓存系统的可靠性。向分布式缓存中插入数据时,先根据一致性哈希算法找到保存该数据的节点及其备份节点,再调用通信模块将该数据保存到机器节点及备份节点中。更新和删除的处理流程同数据插入的过程。从分布式缓存中查询数据时,先调用一致性哈希算法找到存储该数据的节点,然后调用缓存通信模块去读取数据,如果节点缓存中存在数据,调用通信模块直接返回结果;如果不存在,则从本地数据库中查询数据,把查询结果更新给节点缓存及备份缓存中,返回结果。查询处理过程见图 7 所示。

分布式数据缓存系统的设计与实现

图 7 分布式缓存查询过程

分布式缓存采用的同步机制也是 TTL 模式和客户端失效模式,但不完全同于复制缓存的同步机制。TTL 模式,亦即在向缓存中插入数据对象时,给对象一个生存周期(TTL),当查询对象时,首先查看它的 TTL 是否过期,如果过期,就从本地数据库中查询数据,更新缓存及备份节点中的数据对象;同样后台会启动一个线程来检测本地缓存中的对象是否过期,如果过期,直接从缓存中删除,把这个失效消息发送给备份节点。客户端失效模式是指,当分布式缓存中有数据更新或删除时,先根据一致性哈希算法找到保存该数据的节点及备份节点,然后调用通信模块把该消息发送给数据节点及备份节点,实现缓存数据更新或删除。分布式缓存由 DistributedCache 类实现,该类提供了操作分布式缓存的所有 API 接口。

3.3 替换算法模块

缓存替换算法指出当缓存空间满时如何选择缓存中被替换对象。本缓存系统实现了三种传统的替换算法[5]:先进先出算法(FIFO)、最近最少使用算法(LRU)、最不经常使用算法(LFU)。FIFO,该算法进行数据替换时,选择最早放入缓存的对象替换。该算法由类 FIFOPolicy实现,每、次替换时,从缓存中选择更新时间或者创建时间最早的对象换出缓存。LRU,该算法进行数据替换时,选择最近最少使用的对象替换出缓存,很适合具有高局部性的数据访问模式,但对顺序访问模式表现的很糟糕。该算法由类 LRUPolicy 实现,每次从缓存中选择上一次被访问时间最早的对象换出缓存。LFU,该算法选择缓存中被最少访问的对象进行替换,适合具有不相关访问模型的应用。

该算法由类 LFUPolicy 实现,每次从缓存中选择被访问次数最少的对象换出缓存。

3.4 缓存通信模块

一套通信协议,用于缓存节点间的通信。通信模块的调用是通过给复制缓存和分布式缓存注册监听器的方式实现的。通信模块主要由类JGroupsManager实现,复制缓存发送消息过程如下:

1) 假设本地缓存中有数据更新了,这时产生一个数据更新的事件,将该事件告知监听器 JgroupsReplicator 类;

2) 以更新的对象为参数,调用 JgroupsReplicator 类的 notifyElementUpdated()方法,该方法将更新消息封装成一个 EventMessage 类对象;

3) 以 EventMessage 类对象为参数,调用 JGroupsManager 的 send()方法,该方法将EventMessage 类对象封装成 JGroupsMessage 类对象;

4) 最后调用 JGroupsManager 的成员变量 NotificationBus 类对象的 sendNotification()方法将消息发送给集群中的其它节点。

复制缓存接收消息过程:

1) 缓存系统初始化后,生成的 JGroupsManager 类对象根据配置文件信息,会监听本地机器的某端口号,JGroupsManager 类实现了 org.jgroups.blocks.NotificationBus.

Consumer 接口;

2) 当此端口号有消息传来时,调用 JGroupsManager 类的 handleNotification()方法,将收到的消息转化为 JGroupsMessage 类对象;

3) 以 JGroupsMessage 类对象为参数,再调用 JGroupsManager 类的 handleJGroupNotification()方法,该方法从 JGroupsMessage 类对象中提取出复制缓存相关信息,

包括名称,事件的类型,数据对象的键和值;

4) 根据名称找到复制缓存对象,执行消息传递过来的事件。分布式缓存与复制缓存的通信过程稍有不同,但是底层实现上都调用 JGroupsManager类,这里不予讨论。

3.5 可靠性服务模块

可靠性服务模块功能是提供缓存节点间的数据迁移和失败恢复,当有新缓存节点加入集群或有节点退出集群时,可靠性服务模块会被动态的调用,可靠性服务由 Coordinator 类实现。先讨论复制缓存的可靠性服务,复制缓存集群中有一个缓存节点退出或失效时,不必做任何处理,因为集群中的所有节点都保存了整个缓存的一份副本。复制缓存集群中有一个新缓存节点加入时,调用 Coordinator 类的处理流程:

1) 新加入节点的 CacheManager 类初始化工作完成后,启动协调者 Coordinator,使Coordinator 调用通信模块 JGroupsManager 类向集群中的其它所有节点发送一条消

息,该消息通知其它节点我是新加入的节点,新加入节点是知道集群中其它节点的;

2) 所有节点收到这条消息后,先把新节点的信息加入到配置信息中,然后回复新节点一条同类型的消息,新节点通信模块 JGroupsManager 类收到这条消息后,解析该

消息,发现是回复消息,把该消息传给 Coordinator,以后再收到同类型消息,不作处理;

3) Coordinator 解析回复消息,向第一个回复消息的节点发送迁移数据的请求;

4) 收到迁移数据请求的节点启动协调者 Coordinator 类准备向新节点迁移缓存中的数据,协调者间隔性的向新节点发送缓存中的数据,直到所有缓存数据都迁移为止,

最后发送一条迁移数据完成的消息;

5) 新节点的协调者不断地接收来自迁移节点的缓存数据,放入本地缓存中,这一过程中不向其它节点发送更新缓存数据消息,直到缓存数据迁移完成。分布式缓存的数据分布方法完全不同于复制缓存,所以它们的数据迁移和失败恢复的处理过程不同。分布式缓存集群中加入一个新缓存节点时,调用 Coordinator 类的处理过程如

下:

1) 新加入节点的 CacheManager 类初始化后,调用一致性哈希算法找到自己映射在圆上的位置,设新节点用 nodeE 表示,映射在圆上的位置见图 8 所示,只需迁移 nodeB上映射在 nodeA 和 nodeE 之间的缓存数据到新节点。启动协调者 Coordinator,调用通信模块 JGroupsManager 类向集群中的其它所有节点发送一条消息,该消息通

知其它节点我是新加入的节点,然后再向 nodeB 节点发送一条迁移数据的请求;

2) 所有节点收到这条消息后,都把新节点的信息加入到配置信息中,nodeB 节点收到迁移数据的请求后,启动 Coordinator,准备向 nodeE 迁移数据;

3) nodeB 节点 Coordinator 到自己缓存中取数据,先判断数据映射在圆上的位置,如果是在 nodeE 节点的范围,就把该数据对象封装成消息发给 nodeE 节点,同时把

该数据放入自己的备份缓存中,并从本地缓存中删除(B 节点的备份节点保存的这部分数据也被删除),直至所有缓存数据检测完,这一过程间歇性的完成,防止造成网络拥塞;

4) nodeB 节点 Coordinator 下一步取本地备份缓存中的数据,检测数据应该保存的位置,如果是 nodeE 节点的数据,则不作处理,否则将备份数据发给 nodeE 节点,通知它这是备份数据,nodeE 节点把数据放入自己的备份缓存中,nodeB 节点同时从本地备份缓存中删除这些数据。该过程也要间歇地完成,直到所有备份数据检测完,发送一条迁移完成的消息;

5) 至此,整个的数据迁移工作就完成了。

分布式数据缓存系统的设计与实现

图 8 分布式缓存数据迁移

分布式数据缓存系统的设计与实现

图 9 分布式缓存失败恢复

见图 9 所示,假设分布式缓存集群中有 5 个节点,现在 nodeB 节点突然宕机了,失败恢复的处理过程如下:

1) 假设 nodeA 节点查询一条数据,该数据位于 nodeB 节点上,nodeA 向 nodeB 发送一条请求消息等待一段时间没有收到任何回复,重复向 nodeB 发送两次请求,如果仍收不到回复,便向备份节点 nodeC 请求数据,nodeC 发送数据给 nodeA 节点,nodeA 收到数据后通知 nodeC 节点 nodeB 节点好像已失效;

2) nodeC 节点向 nodeB 节点发送一条询问消息,如果收到回复消息,说明 nodeB 节点正常;如果等待一段时间后,又重复发了几次询问消息,都收不到回复消息,就

确定 nodeB 节点失效了;

3) nodeC 节点向所有其它节点发送 nodeB 失效消息,所有节点从配置信息中删除nodeB 节点的信息;

4) nodeC 节点协调者 Coordinator 取本地备份缓存中的数据,放入本地缓存中保存,同时更新 nodeC 节点的备份节点缓存,并从本地备份缓存中删除数据,直至处理

完毕;

5) nodeC 节点协调者 Coordinator 向逆时针方向的 nodeE 节点发送备份数据的请求,nodeE 收到消息后,查看自己的备份节点是否是 nodeC,如果是,就启动 Coordinator

向 nodeC 节点发送备份数据,否则不做处理;

6) nodeE 节点向 nodeC 节点备份数据完成后,整个的失败恢复工作就完成了。

4 结论

经实验测试,本文实现的两种不同分布模式的缓存:复制缓存和分布式缓存,都具有数据冗余备份和失败恢复功能,并有着良好的稳定性。复制缓存适于在节点较少的集群中使用,而分布式缓存适于应用在大规模的集群服务器中。目前,该缓存系统已经应用在信天游电子客票验真官网的数据缓存层,系统运行稳定,达到了实际使用的需求。但是,系统实现的分布式缓存存在一个明显缺点,就是当系统执行数据冗余备份或失败恢复功能时,网络中的数据传输量明显增大,对网络带宽的要求很高。本文下一步的改进工作包括:

1) 研究策略使系统在不繁忙时或网络带宽充裕时,执行数据冗余备份和失败恢复。

2) 考虑在分布式缓存的本地加入 Near Cache,减小网络传输时间。

3) 考虑把 AOP 功能加入本缓存系统中,实现智能缓存系统,同时抽象出缓存框架供其他应用程序使用。


分享到:


相關文章: