马蜂窝推荐系统容灾缓存服务的设计与实现

数据库忽然断开链接、第三方接口迟迟不返回结果、高峰期网络发生抖动...... 当程序突发异常时,咱们的应用能够告诉调用方或者用户「对不起,服务器出了点问题」;或者找到更好的方式,达到提高用户体验的目的。html

 

1、背景

用户在马蜂窝 App 上「刷刷刷」时,推荐系统须要持续给用户推荐可能感兴趣的内容,主要分为根据用户特性和业务场景,召回根据各类机器学习算法计算过的内容,而后对这些内容进行排序后返回给前端这几个步骤。前端

推荐的过程涉及到 MySQL 和 Redis 查询、REST 服务调用、数据处理等一系列操做。对于推荐系统来讲,对时延的要求比较高。马蜂窝推荐系统对于请求的平均处理时延要求在 10ms 级别,时延的 99 线保持在 1s 之内。java

当外部或者内部系统出现异常时,推荐系统就没法在限定时间内返回数据给到前端,致使用户刷不出来新内容,影响用户体验。git

因此咱们但愿经过设计一套容灾缓存服务,实如今应用自己或者依赖的服务发生超时等异常状况时,能够返回缓存数据给到前端和用户,来减小空结果数量,而且保证这些数据尽量是用户感兴趣的。github

 

2、设计与实现

设计思路和技术选型

不只仅是推荐系统,缓存技术在不少系统中已经被普遍应用,小到 JVM 中的经常使用整型数,大到网站用户的 session 状态。缓存的目的不尽相同,有些是为了提升效率,有些是为了备份;缓存的要求也高低不一,有些要求一致性,有些则没有要求。咱们须要根据业务场景选择合适的缓存方案。算法

结合到咱们上面提到的业务场景和需求,咱们采用了基于 OHC 堆外缓存和 SpringBoot 的方案,实如今现有推荐系统中增长本地容灾缓存系统。主要是考虑到如下几点因素:spring

1. 避免影响线上服务,将业务逻辑和缓存逻辑隔离数据库

为了避免影响线上服务,咱们将缓存系统封装为一个 CacheService,配置在现有流程的末端,并提供读、写的 API 给外部调用,将业务逻辑和缓存逻辑隔离。后端

2. 异步写入缓存,提升性能缓存

读、写缓存都会带来时间消耗,特别是写入缓存。为了提升性能,咱们考虑将写入缓存作成异步的方式。这部分使用的是 JDK 提供的线程池 ThreadPoolExecutor 来实现,主线程只须要提交任务到线程池,由线程池里的 Worker 线程实现写入缓存。

3. 本地缓存,提升访问速度

在推荐系统中,给用户推荐的内容应该是千人千面的,甚至同一位用户每次刷新看到的内容均可能不一样,这就不要求缓存具备强一致性。所以,咱们只须要进行本地缓存,而不须要采用分布式的方式。这里使用到的是开源缓存工具 OHC,缓存的数据来源于成功处理过的请求。

4. 备份缓存实例,保证可用性

为了保证缓存的可用性,咱们不只在内存中进行缓存,还定时备份到文件系统中,从而保证在能够应用启动时从文件系统加载到内存。具体可使用 SpringBoot 提供的定时任务、ApplicationRunner 来实现。

总体架构

咱们保持了推荐系统的现有逻辑,并在现有流程的末端,配置了 CacheModule 和 CacheService,负责全部和缓存相关的逻辑。

其中,CacheService 是缓存的具体实现,提供读写接口;CacheModule 对本次请求的数据进行处理,并决定是否须要调用 CacheService 对缓存进行操做。

模块解读

1. CacheModule

在完成推荐系统的原有流程处理以后,CacheModule 会对获得的响应报文进行判断,好比是否抛出了异常,响应是否为空等,而后决定是否读取缓存或者提交缓存任务。

CacheModule 的工做流程如图所示,其中橘黄色部分表明对 CacheService 的调用:

  • 提交缓存任务。若是该次请求没有抛出异常,而且响应结果也不为空,则会提交一个缓存任务到 CacheService。任务的 key 值为对应的业务场景,value 为本次响应计算获得的内容。提交的动做是非阻塞的,对接口的耗时影响很小。

  • 读取缓存数据。当应用自己或者依赖应用抛出异常时,系统会根据业务场景的 key 值从 CacheService 中读取缓存并返回给调用方。当出现用户自己已经刷完全部可用数据的状况时,就不须要读取缓存,而是将请求的数据及时反馈给用户。

2. CacheService

在缓存的具体实现上,CacheService 使用到了从 Apache Cassandra 项目中独立出来的 OHC。另外由于咱们整个应用是基于 SpringBoot 的,也用到了 SpringBoot 提供的各类功能。

上文说到对缓存没有强一致性的要求,因此咱们采用的是本地缓存而非分布式缓存,而且抽象出一个 CacheService 类负责对本地缓存进行维护。

(1) 数据格式

推荐系统返回数据时,根据业务场景和用户特征设定以「屏」为单位返回数据,每屏能够包含多个内容项,因此采起 key-set 的数据格式:key 值为业务场景,好比首页的「视频」频道;缓存内容则为「屏」的集合。

(2) 存储位置

对于 Java 应用,缓存能够存放在内存中或者硬盘文件中。而内存空间又分为 heap(堆内存)和 off-heap(堆外内存)。咱们对这几种方式进行了对比:

为了保证较快的读写速度,避免缓存 GC 影响线上服务,因此选择 off-heap 做为缓存空间。OHC 最先包含在 Apache Cassandra 项目中,以后独立出来,成为了基于 off-heap 的开源缓存工具。它既能够维护大量的 off-heap 内存空间,同时也使用于低开销的小型缓存实体。因此咱们使用 OHC 做为 off-heap 的缓存实现。

(3) 文件备份

在应用重启时,off-heap 中的缓存为空。为了尽快载入缓存,咱们使用 SpringBoot 的 Scheduling Tasks 功能,按期将缓存从 off-heap 备份到文件系统;经过继承 SpringBoot 的 ApplicationRunner 监听应用启动的过程,启动完成后将硬盘中的备份文件加载到 off-heap,保证缓存数据的可用性。

CacheService 维护一个任务队列,队列中保存着 CacheModule 经过非阻塞的方式提交的缓存任务,由 CacheService 决定是否要执行这些缓存任务。

(4) 对 CacheModule 提供的 API

  • 读取缓存时,传入 key 值,缓存模块随机从 set 中读取数据返回。

  • 写入缓存时,将 key 和 value 封装为一个任务,提交到任务队列,由任务队列负责异步写入缓存。

(5) 任务队列与异步写入

这里咱们使用了 JDK 中的线程池来实现。在构造线程池时,使用 LinkedBlockingQueue 做为任务队列,能够实现快速增删元素;由于应用的 QPS 在 100 之内,因此工做线程数目固定为 1;队列写满以后,则执行 DiscardPolicy,放弃插入队列。

(6) 缓存数量控制

若是缓存占用内存空间过大,会影响线上应用,咱们能够采用为不一样的业务场景配置最大缓存数量来控制缓存数量。没有达到配置值时,将成功处理过的数据写入缓存;达到配置值时能够随机抽样覆盖原有缓存项,来保证缓存的实时性。

综合考虑以上各个方面,CacheService 的设计以下:

线上表现

为了验证容灾缓存的效果,咱们在命中缓存时进行了埋点,并经过 Kibana 查看每小时缓存的命中数量。如图所示,在 18:00 到 19:00 系统存在必定的超时,而这段时间因为缓存服务发挥了做用,使系统的可用性获得提高。

咱们还对 OHC 的读取和写入速度进行了监控。写入缓存的时延在毫秒级别,而且是异步写入;读取缓存的时延在微秒级别。基本没有给系统增长额外的时间消耗。

踩过的坑

在将缓存写入 OHC 以前,须要进行序列化,咱们使用了开源的 kryo 做为序列化工具。以前在使用 kyro 时,发现对于没有实现 Serializable 的类,反序列化时可能失败,好比使用 List#subList 方法返回的内部类 java.util.ArrayList$SubList。这里能够手动注册 Serializer 来解决这个问题,在 Github 上开源的 kryo-serializers 仓库提供了各类类型的 serializers。

另一点,须要注意根据具体使用场景,来配置 OHC 中的 capacity 和 maxEntrySize。若是配置的值过小的话,会致使写入缓存失败。能够在上线以前测算缓存的空间占用,合理设置整个缓存空间的大小和每一个缓存 entry 的大小。

 

3、优化方向

基于 SpringBoot 和 OHC,咱们在现有的推荐系统中增长了一个本地容灾缓存系统,当依赖服务或者应用自己突发异常时能够返回缓存的数据。

该缓存系统还存在一些不足,咱们近期会针对如下几点进行重点优化:

  • 缓存数目写满以后,目前应用会随机覆写已经存在的缓存。将来能够进行优化,将最老的缓存项替换。

  • 在某些场景下缓存的粒度不够精细,好比目的地页推荐共用一个缓存的 key 值。将来能够根据目的地的 ID,为每一个目的地配置一份缓存。

  • 如今推荐系统还有部分配置依赖于 MySQL,将来会考虑将在本地进行文件缓存。

 

[参考资料]

1. Java Caching Benchmarks 2016 - Part 1

2. On Heap vs Off Heap Memory Usage

3. OHC - An off-heap-cache

4. kryo-serializers

5. scheduling-tasks

 

本文做者:孙兴斌,马蜂窝推荐和搜索后端研发工程师。

(马蜂窝技术原创内容,转载务必注明出处保存文末二维码图片,谢谢配合。)

关注马蜂窝技术公众号,找到更多你须要的内容

相关文章
相关标签/搜索