隔离方案
对于服务隔离,常见的隔离方案有:
- 线程隔离:
- 在同一个进程里,可以区分不同的服务方法,对于核心的服务方法,用单独的线程池执行核心的服务方法,确保在特定线程池出现故障时,不会影响到其他线程池资源。
- 除此之外,可以有另外一个隔离纬度,如对特定服务方法的执行时间进行统计,对于执行时间长的服务方法,放在一个独立的慢线程池处理,其他放在默认线程池处理。
- 进程隔离/读写服务隔离:读服务流量大,和读服务进行隔离,用不同的进程部署,核心服务与非核心服务也可以进行隔离,避免相互影响。
- 分组隔离,对于一条完整的调用链路,对每一个链路节点划分出一个个分组,未划分的都归为默认分组。这样,特定分组的上游服务会调用特定分组的下游服务,不同分组的服务实现逻辑可能一致,但调用互不干扰,达到了调用链路的隔离,以便在不同的链路完成不同的需求,如全链路压测、beta测试,灰度测试等。
- 集群隔离。又或者出于安全或其他因素考虑,限制服务只能在同机房或同网域下访问。
- 资源隔离:如隔离核心服务和非核心服务在不同的网络带宽,避免非核心服务的带宽占用影响核心服务访问
对于除线程隔离外的其他隔离方案,笔者没有太多的实践经验,这里主要看看基于线程隔离和后续的分组隔离的相关实现。
线程隔离
在RPC服务中,可以根据服务方法的不同特性或调用情况,为特定的服务方法配置指定配置的线程池,如根据服务方法是否核心、读或写操作,方法执行快慢等特性,用不同线程池进行隔离。
参考Pigeon比较好的一个实践,是将服务方法和线程池配置的对应关系放到配置中心。在程序中监听配置变更,来为不同服务方法动态创建相关配置的线程池,来达到对线上服务流量的动态隔离。
具体配置示例如下:
- 配置特定参数的线程池:
[ { "poolName" : "pool1", "corePoolSize" : 50, "maxPoolSize" : 100, "workQueueSize" : 101 }, { "poolName" : "pool2", "corePoolSize" : 1, "maxPoolSize" : 2, "workQueueSize" : 33 }, { "poolName" : "pool3", "corePoolSize" : 0, "maxPoolSize" : 1, "workQueueSize" : 1 } ] - 配置特定服务方法使用指定线程池:
{ "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方法的线程隔离。
泳道和动态分组隔离
泳道隔离
泳道可以理解为是配在一台机器上的一个环境变量,以此标识当前机器的所属泳道。
泳道往往具有以下特性:
- 如果一个service的机器定义了swimlane为tg,那么这个机器只能是客户端同样为tg泳道的机器能够调用。
- 对于客户端来说,假设配置了泳道为tg,那么这个客户端机器调用远程服务时,会优先选择服务端泳道配置同样为tg的机器,如果tg泳道的机器不可用或不存在,才会调用其他未配置泳道的机器。
泳道不仅仅是 RPC 领域的概念,为了完成从上到下的隔离,其他中间件也要能识别泳道,并做相应的支持。如对于独立的配置中心中,同一个 key 的配置,在不同的泳道中读取到的值是可以不一样的。
动态分组隔离
在RPC领域,我们往往通过动态分组来实现更灵活、更细粒度的服务隔离功能。
和泳道定义在机器的环境变量不同,分组属性往往是和RPC进程挂钩的,作为一个服务进程的动态配置存在,因此分组可以将粒度细化到单个机器的单个服务级别,而它的动态性也意味着可以随着配置变更而实时刷新。在RPC服务中,一个服务往往定义在注册中心如zk中作为一个zk节点存在,在需要变更分组时,只需要从删除原分组节点数据,创建新分组节点数据,以此完成动态变更。
另一方面,泳道不具备调用者和服务者的区分功能。而基于分组,我们可以定义一个服务,本身作为服务提供者在分组1,而作为服务调用者在分组2,来满足更复杂的调用场景。
在服务高可用领域,我们还常常会提到一个降级的概念,对于动态分组,以服务调用方为例,在获取不到指定分组的服务提供方节点时,可以配置降级到默认分组或基于泳道来获取服务节点。