一种多业务下多环境的dubbo隔离方案

1,730 阅读6分钟

一、背景

1.初见端倪

今天是我来到这片工地的第一天,是日天朗气清,阳光明媚,作为一个新时代农名工+萌新,正在津津有味地看着新人须知。
闲暇之余,点开了业务的环境部署平台,卧槽,好家伙,6套测试环境,每套有8个服务,每个服务约两个实例,大约6x8x2=96个实例,这就是资本的力量吗[狗头保命]
图例如下: image.png 惊叹之余,让人不禁产生疑问,为什么一个业务要用这么多套测试环境呢?
了解后发现,原来对外使用的测试环境只有一套(app-lucy:fat),其余5套业务内部使用,那么问题来了,为什么内部要使用这么多套环境呢?
原来是同个业务内分了不同的开发线,大家送测上线的时间和周期是不一样的,为了送测期间环境的稳定性,所以都是各自开发送测然后再合并代码上线!
图例如下: image.png

2.问题乍现

有那么一天,接到了一个小需求,需要在dubbo接口的返回值中加个字段。当我修改、编译、部署一气呵成之后,调用发现返回值中并没有这个字段!
1a89f7ba-a94b-4d5b-93ec-22fbd84b7189.jpg

查日志发后现调用到了另一套环境的旧逻辑。情况是酱紫的:

image.png

随后打开dubbo的管理后台一看,果然,里边有十几个provider,核对了下ip,发现每套环境的服务ip都有在provider列表中,并且没有相应的标识😂,也就是6套测试环境的dubbo共用一个zk注册中心,provider注册如下

image.png

换言之,修改了某个dubbo接口,就需要在6套测试环境全量部署接口所在的服务才能保证调用生效😀
作为一个萌新,这不得挣扎一下?

image.png

二、开始解决

在确认了测试环境之间dubbo隔离的必要性之后,开始着手寻找解决的方法

方案1.基于dubbo LoadBalance 的隔离方案

前同事实现的一种方案,利用了dubbo的Consumer在有多个Provider的情况下,可以使用自定义LoadBalance的特性,其简要实现如下:

a.测试环境的分组配置如下图:provider设置特定分组,consumer统一设置为"*"

image.png

b.大致原理:

由于consumer的分组使用了"*",所以可以获取所有的provider(包括分组为空和特殊设置的分组)然后再通过自定义的LoadBalance对provider进行分组的识别和选择

c.局限性

一个api的provider需要有两个或以上才会进入LoadBalance的逻辑,也就是单台实例时候,隔离不生效

方案2.dubbo2.7的新特性Tag

上一个方案缺陷比较致命,然后又找了dubbo的资料来看了下,发现dubbo在2.7有个新特性Tag可以实现隔离的效果,喜出望外!准备实现的时候,仔细看了下业务使用的dubbo依赖,好家伙,是基于2.6定制的,推动所有业务升级dubbo显然不现实,所以方案不予考虑。远水救不了近火呀!

812c3312-7144-4c80-b09e-95268c1148c5.jpg

方案3.dubbo的group

在寻找资料过程中,发现很多都有提到使用group来进行隔离的方法,只要针对Consumer和provider设置相同的group,那就只能调用到该group啦!
image.png

说干就干,直接把一套测试环境下所有服务的dubbo分组改成了同一个值,如图: image.png

还没有等我开心完,前端小伙伴旧找上门了,页面直接报错了, 赶紧看了下报错接口的日志,发现调用其它业务的dubbo接口都报错了,找不到provider,想想才恍然大明白, 当前的业务间调用图如下: 0000222200.png

理想的业务间调用图如下:

image.png

可见,在app-lucy的业务中,不管哪一套测试环境,调用app-bob业务时,都应该调用app-bob:fat环境(fat是稳定的共用环境,fat-x是业务自己搭建的私有非稳定环境),而在app-lucy的某套环境中,如app-lucy:fat-1环境,其业务本身的dubbo接口应该只在本套环境中提供与调用。
所以,报错是因为刚刚直接针对整个实例内的dubbo接口进行配置的(不管是Provider还是Consumer,不管是其它业务还是自己所在的业务),也就是其它业务(app-bob)的接口(Consumer)也配置了这个分组,但是别的业务没有这么多套环境,也就没有配置这个分组,所以会出现调用其它业务接口时找不到Provider。

方案4.改进版的group配置【选用】

理清思路后再想想,只要对我自己所在业务的dubbo接口设置分组应该就可以实现隔离,其它业务的dubbo接口,还是保持原封不动就可以啦!

108b9c94-a0a1-433a-9399-40c1aecd6988.gif

具体的dubbo api的分组设置是:

a. 对于 fat 环境,统一使用默认分组(不设置分组),这样 app-lucy:fat 就一定会调用 app-bob:fat,例如 app-lucy:fat 的分组信息应该是:

image.png

b. 对于 fat-x 环境,对其业务本身的api统一设置分组,其它业务的使用默认分组,例如 app-lucy:fat-1 的分组信息应该是:

image.png

为了少做无用功,去跟导师同事讨论了下,确认理论可行后,再开始动手。

三、问题的关键

经过上面的尝试,问题关键点变成了如何识别出本业务的dubbo接口,然后进行分组配置

方案1.手动列出所有本业务的dubbo接口,然后进行设置

往后新增接口啥的都要手动维护,不够优雅!

方案2.声明接口所在的包名(正则),扫描ClassPath上该包名下的接口

一般而言,业务的dubbo接口都会放在一个包名内,因此可以通过对包名下类的扫描来获取该业务的dubbo接口, 一开始使用的是Guava中的ClassPath类,在本机的Idea中跑起来还可以,但是部署到容器中就不行了,排查后发现使用的是spring-boot的打包,会有jar in jar的问题,通过修改源码解决后,发现这样扫描jar耗时也是比较长的!

方案3.声明接口所在的包名(正则),在Bean加载的过程中加以识别【选用】

由于我们需要在dubbo接口向zk注册中心注册前把分组设置好,所以我们选择的扫描点是 postProcessBeforeInstantiation

a.provider扫描方式:获取所加载bean的接口列表,然后进行正则匹配

b.Consumer扫描方式:用反射收集字段(@Reference修饰)的接口,然后进行正则匹配

四、结果

可以,运行后观察调用情况和dubbo的管理后台,调用正常
image.png

注册中心provider注册情况如下:

image.png

五、局限性

  1. 要求 app-lucy 的dubbo接口存放有规律,因为要通过正则进行匹配

例如lucy业务的接口都放在 com.wingli.lucy.dubbo.api 下,否则的话,正则写起来很复杂,如果新增一个接口在别的包下,那么就需要修改匹配的正则。

  1. dubbo 接口的 beanId 使用了默认值,暂不支持指定beanId

在代码示例中可以看到,我们配置分组是通过设置properties,即dubbo.reference.$beanId.group=xxx,而其中的{beanId}我们使用了api的全限定名,因为如果不加特殊配置,beanId是等于全限定名的,但如果通过其它方式指定了beanId,而又使用全限定名来设置的话,那就不生效了

  1. 使用反射扫描字段,会有一定的耗时

相对于直接声明所有接口并设置properties来说,反射获取是相对较慢的,但是dubbo的注入也是通过这个反射做的,所以时间也还好,耗时跟运行的环境关联性比较大,有兴趣的小伙伴可以自测一下

六、参考

  1. bean加载:juejin.cn/post/697794…
  2. dubbo:dubbo.apache.org/zh/docs/qui…

七、代码示例

import com.alibaba.dubbo.common.utils.ConcurrentHashSet
import com.alibaba.dubbo.config.annotation.Reference
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.BeanFactory
import org.springframework.beans.factory.BeanFactoryAware
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter
import org.springframework.context.annotation.Conditional
import org.springframework.core.annotation.AnnotationUtils
import org.springframework.stereotype.Component
import org.springframework.util.ReflectionUtils
import org.springframework.util.ReflectionUtils.FieldCallback
import java.lang.reflect.Modifier
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.system.measureTimeMillis

@Component
class DubboGroupIsolation : InstantiationAwareBeanPostProcessorAdapter(), BeanFactoryAware {

    private val packageRegex = System.getProperty("app-lucy.dubbo.isolation.package", "")
    private val groupName = System.getProperty("app-lucy.dubbo.isolation.group", "")
    private val isEnable = packageRegex.isNotBlank() && groupName.isNotBlank()
    private val logger = LoggerFactory.getLogger(DubboGroupIsolation::class.java)
    private val setted = ConcurrentHashSet<String>()
    private lateinit var beanFactory: BeanFactory
    private var totalTakeTimeAtm = AtomicInteger(0)

    init {
        if (!isEnable) {
            logger.warn("DubboGroupIsolation bean is Enable but package=$packageRegex or group=$groupName not set!!!")
        }
    }

    override fun postProcessBeforeInstantiation(beanClass: Class<*>, beanName: String): Any? {
        if (!isEnable) return null

        measureTimeMillis {
            val interfaceList = LinkedList<String>()
            //consumer
            ReflectionUtils.doWithFields(beanClass, FieldCallback { field ->
                val reference = AnnotationUtils.getAnnotation(
                    field,
                    Reference::class.java
                )
                if (reference != null) {
                    if (Modifier.isStatic(field.modifiers)) {
                        return@FieldCallback
                    }
                    //match
                    interfaceList.add(field.type.name)
                }
            })

            //provider
            interfaceList.addAll(beanClass.interfaces.map { it.name })

            interfaceList.filterNot {
                setted.contains(it)
            }.filter {
                it.substringBeforeLast(".").matches(Regex(packageRegex))
            }.map {
                listOf<String>("dubbo.reference.$it.group", "dubbo.service.$it.group").map {
                    System.setProperty(it, groupName)
                    logger.info("set dubbo properties key=$it value=$groupName")
                }
                setted.add(it)
            }
        }.let {
            logger.info("deal dubbo group beanName=$beanName currentTakeTime=$it totalTakeTime=${totalTakeTimeAtm.addAndGet(it.toInt())}")
        }

        return null
    }

    override fun setBeanFactory(beanFactory: BeanFactory) {
        this.beanFactory = beanFactory
    }
}