高可用实践之服务隔离(四)

隔离方案

对于服务隔离,常见的隔离方案有:

  1. 线程隔离:
    1. 在同一个进程里,可以区分不同的服务方法,对于核心的服务方法,用单独的线程池执行核心的服务方法,确保在特定线程池出现故障时,不会影响到其他线程池资源。
    2. 除此之外,可以有另外一个隔离纬度,如对特定服务方法的执行时间进行统计,对于执行时间长的服务方法,放在一个独立的慢线程池处理,其他放在默认线程池处理。
  2. 进程隔离/读写服务隔离:读服务流量大,和读服务进行隔离,用不同的进程部署,核心服务与非核心服务也可以进行隔离,避免相互影响。
  3. 分组隔离,对于一条完整的调用链路,对每一个链路节点划分出一个个分组,未划分的都归为默认分组。这样,特定分组的上游服务会调用特定分组的下游服务,不同分组的服务实现逻辑可能一致,但调用互不干扰,达到了调用链路的隔离,以便在不同的链路完成不同的需求,如全链路压测、beta测试,灰度测试等。
  4. 集群隔离。又或者出于安全或其他因素考虑,限制服务只能在同机房或同网域下访问。
  5. 资源隔离:如隔离核心服务和非核心服务在不同的网络带宽,避免非核心服务的带宽占用影响核心服务访问

对于除线程隔离外的其他隔离方案,笔者没有太多的实践经验,这里主要看看基于线程隔离和后续的分组隔离的相关实现。

线程隔离

在RPC服务中,可以根据服务方法的不同特性或调用情况,为特定的服务方法配置指定配置的线程池,如根据服务方法是否核心、读或写操作,方法执行快慢等特性,用不同线程池进行隔离。

参考Pigeon比较好的一个实践,是将服务方法和线程池配置的对应关系放到配置中心。在程序中监听配置变更,来为不同服务方法动态创建相关配置的线程池,来达到对线上服务流量的动态隔离。

具体配置示例如下:

  1. 配置特定参数的线程池:
    [ {
     "poolName" : "pool1",
     "corePoolSize" : 50,
     "maxPoolSize" : 100,
     "workQueueSize" : 101
    }, {
     "poolName" : "pool2",
     "corePoolSize" : 1,
     "maxPoolSize" : 2,
     "workQueueSize" : 33
    }, {
     "poolName" : "pool3",
     "corePoolSize" : 0,
     "maxPoolSize" : 1,
     "workQueueSize" : 1
    } ]
    
  2. 配置特定服务方法使用指定线程池:
    {
     "com.dianping.pigeon.benchmark.service.HelloService#statistics" : "pool1",
     "com.dianping.pigeon.benchmark.service.EchoService" : "pool2",
     "com.dianping.pigeon.benchmark.service.TestService" : "pool3"
    }
    

基于上面配置,我们可以在程序中实时监听配置变化,来为指定服务方法初始化相关的线程池,具体可能涉及以下相关数据结构:

// poolName --> poolConfig
private volatile static ConcurrentMap<String, PoolConfig> poolConfigs = Maps.newConcurrentMap();
// api --> poolName
private volatile static ConcurrentMap<String, String> apiPoolNameMapping = Maps.newConcurrentMap();
// poolConfig --> threadPool
private static final ConcurrentMap<PoolConfig, DynamicThreadPool> dynamicThreadPools = Maps.newConcurrentMap();

假设调用方调用服务方指定服务方法,在服务方记为api字段,可通过:api->poolName->poolName->poolConfig->threadPool找到对应的线程池配置,来执行相应的服务方法,实现不同api方法的线程隔离。

泳道和动态分组隔离

泳道隔离

泳道可以理解为是配在一台机器上的一个环境变量,以此标识当前机器的所属泳道。

泳道往往具有以下特性:

  1. 如果一个service的机器定义了swimlane为tg,那么这个机器只能是客户端同样为tg泳道的机器能够调用。
  2. 对于客户端来说,假设配置了泳道为tg,那么这个客户端机器调用远程服务时,会优先选择服务端泳道配置同样为tg的机器,如果tg泳道的机器不可用或不存在,才会调用其他未配置泳道的机器。

泳道不仅仅是 RPC 领域的概念,为了完成从上到下的隔离,其他中间件也要能识别泳道,并做相应的支持。如对于独立的配置中心中,同一个 key 的配置,在不同的泳道中读取到的值是可以不一样的。

动态分组隔离

在RPC领域,我们往往通过动态分组来实现更灵活、更细粒度的服务隔离功能。

和泳道定义在机器的环境变量不同,分组属性往往是和RPC进程挂钩的,作为一个服务进程的动态配置存在,因此分组可以将粒度细化到单个机器的单个服务级别,而它的动态性也意味着可以随着配置变更而实时刷新。在RPC服务中,一个服务往往定义在注册中心如zk中作为一个zk节点存在,在需要变更分组时,只需要从删除原分组节点数据,创建新分组节点数据,以此完成动态变更。

另一方面,泳道不具备调用者和服务者的区分功能。而基于分组,我们可以定义一个服务,本身作为服务提供者在分组1,而作为服务调用者在分组2,来满足更复杂的调用场景。

在服务高可用领域,我们还常常会提到一个降级的概念,对于动态分组,以服务调用方为例,在获取不到指定分组的服务提供方节点时,可以配置降级到默认分组或基于泳道来获取服务节点。


版权声明:本文为qwe6112071原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。