缓存与高可用

概述

我们研发的项目在使用服务注册发现后,可以在请求过程中自动匹配到最合适的节点,并能通过服务注册中心灵活的调整访问路由规则、限流策略、熔断策略,实现灰度发布、失败熔断等复杂的业务运维场景。北极星SDK大幅减轻了业务研发的负担,使业务代码不需要直接参与服务注册中心的交互,只要调用SDK提供的基础API就可以获得最终结果,SDK能够实现上述抽象统合能力,缓存的设计和实现是基础。 使用北极星的业务研发理解SDK缓存实现机制,有助于编写更安全的代码逻辑,设计合理的高可用方案和相关配置

本文试图回答的场景问题:

  • 北极星SDK产生了哪些缓存,各自有什么用
  • 怎样调节和观测这些缓存的内容
  • 缓存对业务高可用机制产生哪些影响
  • 我们需要关注哪些缓存相关的配置项,应该在哪些场景进行调整

缓存构成

内存缓存

为什么需要内存缓存

性能

在使用服务注册发现的业务场景中,为了保障访问的目标后端地址符合预期,访问端(Consumer)每次连接被访问端(Provider)时,都需要经过基于服务注册发现的一系列查询和过滤逻辑,包括:查询当前有哪些健康的被访问端实例、经过路由策略筛选掉哪些、经过熔断策略筛选掉哪些、最终经过负载均衡策略筛选出唯一的被访问端地址

在这一系列的服务发现过程中,都需要从服务注册中心(Server)获取各类实时数据,包括:被访问端的全部实例列表、路由策略配置、熔断策略配置等。如果每次业务请求都需要经过一系列的服务信息查询,势必会大幅降低业务性能,同时对服务注册中心造成巨大压力 SDK需要提供上述信息的动态缓存能力,让业务可以直接通过内存中获取到需要的数据信息

可靠性

同样的,缓存手段也保障了在服务注册中心不可用情况下的业务连续性,详细请见 典型场景 高可用 章节

缓存哪些内容

缓存数据格式

每个SDKContex维护一个全局的sync.Map内存缓存表,各类缓存数据均保存在这个缓存表中。其中Key标识数据类型和服务名,Value为数据报文。以服务实例的缓存表内容为例:

SDK使用 sync.Map的Load()、Store()、Delete()等原子动作对缓存表进行维护

缓存类型

实例信息

  • 类型:Instances
  • 说明:单个服务的所有实例信息,包含所有健康或异常的实例
  • 使用场景:调用GetInstances场景使用,查询所有实例

路由信息

  • 类型:Routing
  • 说明:单个服务的所有路由规则数据
  • 使用场景:调用ProcessRouter场景使用,查询服务关联的路由规则

熔断信息

  • 类型:CircuitBreaker
  • 说明:单个服务的所有熔断规则信息
  • 使用场景:调用GetInstances场景使用,用于判断返回的实例是否被熔断

限流信息

  • 类型:RateLimiting
  • 说明:单个服务的所有限流规则数据

服务信息

  • 类型:Services
  • 说明:根据输入的标签批量查询服务
  • 使用场景:调用WatchServices/GetServices场景使用,查询所有服务信息

怎样产生和更新

SDK的Cache模块对上层模块提供Get/Load/Report等原语方法

以获取服务实例信息为例:

  • 获取缓存:上层模块优先调用Get原语,Get尝试从内存缓存表中获取对应的服务数据,如果Get数据为空或以失效,则发起远程调用的Load流程
  • 远程调用:上层模块调用Load会触发生成查询任务,并由任务调度队列轮询发往服务注册中心,由SDK维护的固定长连接发送和接收数据报文,详见 网络连接 。查询任务产生后,SDK就开启了针对这条服务信息的Watch流程
  • Watch机制:SDK的连接模块持续监听服务注册中心的回包,并根据回包类型产生缓存数据,更新到缓存表中,Watch流程包含两个关键点:
    • 差异化更新:为了避免不必要的缓存表更新,对回包revision和缓存数据revision进行对比,有差异再更新
    • Watch频率:为了控制每个客户端SDK与服务注册中心的通信频率,每条任务轮询的最小间隔时长由配置.consumer.localCache.serviceRefreshInterval 确定,默认为2秒
  • 更新缓存状态:上层模块通过调用Report原语更新缓存表中服务实例的熔断状态,用于下一次查询获取服务实例的熔断信息

缓存多久

请求计数:SDK的Cache模块对上层模块提供Get原语获取缓存数据,每次调用Get获取缓存表中的一条数据后,就会对应更新这条数据的最后访问时间(lastVisitTime

缓存GC:为了避免缓存表占用空间越来越大,查询任务队列越来越多,SDK设计支持缓存的GC机制。通过配置 .consumer.localCache.serviceExpireTime(默认24小时)定义过期时长,当一条缓存数据大于过期时长没有被访问后,将会被从缓存表中删除,并会连带删除这条缓存数据对应的同步任务和持久化文件缓存。缓存数据被GC后,意味着对应服务的Watch流程也终结,将由下一次Get查询获取结果为空时重新发起Watch和缓存

判断缓存是否过期的公式可简化为:

time.Now() - lastVisitime > serviceExpiretime

持久化缓存

为什么需要持久化缓存

可靠性

服务注册中心故障后,业务依靠内存缓存表可以继续对其他服务寻址,但是如果业务碰巧也重启了,内存缓存表就会丢失,这时就需要文件缓存来顶上

可维护性

通常业务研发对SDK产生的数据会经过再加工使用,文件缓存增加了服务发现数据的可视性,我们可以通过观测缓存文件来分析SDK的内存数据内容,进而在遇到服务发现数据不准时,方便判断问题的归属区域

存在哪里

由配置项 .consumer.localCache.persistDir 确定,默认为 ./polaris/backup

什么格式

以服务default/demo的实例缓存和路由缓存数据为例:

  • 文件名称:svc#$命名空间$服务名$数据类型.json
  • 文件内容:数据类型返回的请求报文

读写机制

读取

服务启动时由配置项 .consumer.localCache.startUseFileCache (默认false)决定是否由持久化缓存产生内存缓存表,如果持久化缓存被读取到内存缓存表中,由配置项.consumer.localCache.persistAvailableInterval(默认5分钟)决定这份数据是否有效,文件缓存在内存缓存表中的有效范围可简化表示为:

startUseFileCache && (time.Now() - file.ModTime() < persistAvailableInterval)

更新 当内存缓存Watch流程判定缓存发生新增、更新或删除时,均会发起持久化缓存更新调度任务,调度任务每100毫秒轮询一次,对待执行的任务进行持久化操作,既写入到文件

删除 当内存缓存表被GC时,对应的持久化缓存文件同时也会被删除

典型场景

高可用

故障场景

1.注册中心故障

服务注册中心发生故障时,SDK缓存使业务能够继续保持通信和路由选择,直到缓存失效

2.访问端与注册中心网络断开

对于访问端服务A,与服务注册中心发生故障效果相同,SDK缓存使业务能够继续保持通信和路由选择,直到缓存失效

3.被访问端与注册中心网络断开

被访问端服务B与注册中心连接断开时,因为B无法向注册中心更新心跳信息,访问端服务A获取到服务B的所有实例均为下线状态。SDK会执行默认的兜底路由逻辑,认为所有的服务实例均为健康状态,不影响服务A向服务B访问

4.上述故障场景+服务A重启

服务A配置开启文件缓存并且允许初始化读取时,重启后会读取持久化缓存到内存缓存表中,这样业务能够继续保持与服务B的通信和路由选择

边界条件

上述故障场景生效期间,当以下条件同时触发时,缓存机制无法保障业务的通信或路由选择符合预期

1.被访端服务实例下线

被访问端B的服务实例下线后,因为服务A内的缓存无法通过Watch注册中心动态更新,A有可能继续访问到异常的服务实例

2.访问端缓存过期

访问端缓存过期后(具体触发条件参考 缓存构成 章节),因为前述故障条件,服务A不再能够重新生成缓存,A访问B必现失败

可用性矩阵

注册中心故障 访问端与注册中心断连 被访端与注册中心断连 访问端重启 被访端实例下线 被访端缓存失效 访问端业务是否可用
N N Y N N N 可用
N Y N N N N 可用
Y Y Y N N N 可用
Y Y Y Y N N 可用
Y Y Y Y Y N 部分可用
Y Y Y Y Y Y 不可用

多Context用法

因为部分历史原因,业务研发有在同一个进程内开启多个SDKContext的用法,每个SDKContext实例会产生独立的长连接与服务注册中心交互,并各自维护独立的内存缓存表,但默认共享使用同一份持久化缓存

这种用法可能导致缓存同步异常和问题排查的困难,强烈建议改为单个业务进程使用全局共享一个SDKContext。如果由于特殊原因无法改造,需要对使用方式做如下约束:

// 初始化配置X
cfgX := config.NewDefaultConfigurationWithDomain()
// 开启文件缓存、设置独立的文件缓存路径
cfgX.GetConsumer().GetLocalCache().SetPersistDir("./polaris/ctx-x/backup")
cfgX.GetConsumer().GetLocalCache().SetPersistEnable(true)
// 使用指定的配置初始化SDKContextX
sdkCtxFoo, _ := polaris.NewSDKContextByConfig(cfgX)

// 初始化配置Y
cfgY := config.NewDefaultConfigurationWithDomain()
// 开启文件缓存、设置独立的文件缓存路径
cfgY.GetConsumer().GetLocalCache().SetPersistDir("./polaris/ctx-y/backup")
cfgY.GetConsumer().GetLocalCache().SetPersistEnable(true)
// 使用指定的配置初始化SDKContextY
sdkCtxY, _ := polaris.NewSDKContextByConfig(cfgY)

配置汇总

  • 配置项consumer.localCache.serviceExpireTime
  • 说明:内存缓存过期时间
  • 默认值:24小时



  • 配置项consumer.localCache.serviceRefreshInterval
  • 说明:内存缓存任务最小发送间隔
  • 默认值:2秒



  • 配置项consumer.localCache.persitEnable
  • 说明:是否开启持久化缓存
  • 默认值:true



  • 配置项consumer.localCache.persistDir
  • 说明:持久化缓存存放路径
  • 默认值:./polaris/config



  • 配置项consumer.localCache.startUseFileCache
  • 说明:启动读取持久化缓存初始化内存缓存表
  • 默认值:false



  • 配置项consumer.localCache.persistAvailableInterval
  • 说明:启动读取持久化缓存数据有效时间
  • 默认值:60秒