一、背景
1.初见端倪
今天是我来到这片工地的第一天,是日天朗气清,阳光明媚,作为一个新时代农名工+萌新,正在津津有味地看着新人须知。
闲暇之余,点开了业务的环境部署平台,卧槽,好家伙,6套测试环境,每套有8个服务,每个服务约两个实例,大约6x8x2=96个实例,这就是资本的力量吗[狗头保命]
图例如下:
惊叹之余,让人不禁产生疑问,为什么一个业务要用这么多套测试环境呢?
了解后发现,原来对外使用的测试环境只有一套(app-lucy:fat),其余5套业务内部使用,那么问题来了,为什么内部要使用这么多套环境呢?
原来是同个业务内分了不同的开发线,大家送测上线的时间和周期是不一样的,为了送测期间环境的稳定性,所以都是各自开发送测然后再合并代码上线!
图例如下:
2.问题乍现
有那么一天,接到了一个小需求,需要在dubbo接口的返回值中加个字段。当我修改、编译、部署一气呵成之后,调用发现返回值中并没有这个字段!
查日志发后现调用到了另一套环境的旧逻辑。情况是酱紫的:
随后打开dubbo的管理后台一看,果然,里边有十几个provider,核对了下ip,发现每套环境的服务ip都有在provider列表中,并且没有相应的标识😂,也就是6套测试环境的dubbo共用一个zk注册中心,provider注册如下
换言之,修改了某个dubbo接口,就需要在6套测试环境全量部署接口所在的服务才能保证调用生效😀
作为一个萌新,这不得挣扎一下?
二、开始解决
在确认了测试环境之间dubbo隔离的必要性之后,开始着手寻找解决的方法
方案1.基于dubbo LoadBalance 的隔离方案
前同事实现的一种方案,利用了dubbo的Consumer在有多个Provider的情况下,可以使用自定义LoadBalance的特性,其简要实现如下:
a.测试环境的分组配置如下图:provider设置特定分组,consumer统一设置为"*"
b.大致原理:
由于consumer的分组使用了"*",所以可以获取所有的provider(包括分组为空和特殊设置的分组)然后再通过自定义的LoadBalance对provider进行分组的识别和选择
c.局限性
一个api的provider需要有两个或以上才会进入LoadBalance的逻辑,也就是单台实例时候,隔离不生效
方案2.dubbo2.7的新特性Tag
上一个方案缺陷比较致命,然后又找了dubbo的资料来看了下,发现dubbo在2.7有个新特性Tag可以实现隔离的效果,喜出望外!准备实现的时候,仔细看了下业务使用的dubbo依赖,好家伙,是基于2.6定制的,推动所有业务升级dubbo显然不现实,所以方案不予考虑。远水救不了近火呀!
方案3.dubbo的group
在寻找资料过程中,发现很多都有提到使用group来进行隔离的方法,只要针对Consumer和provider设置相同的group,那就只能调用到该group啦!
说干就干,直接把一套测试环境下所有服务的dubbo分组改成了同一个值,如图:
还没有等我开心完,前端小伙伴旧找上门了,页面直接报错了,
赶紧看了下报错接口的日志,发现调用其它业务的dubbo接口都报错了,找不到provider,想想才恍然大明白,
当前的业务间调用图如下:
理想的业务间调用图如下:
可见,在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接口,还是保持原封不动就可以啦!
具体的dubbo api的分组设置是:
a. 对于 fat 环境,统一使用默认分组(不设置分组),这样 app-lucy:fat 就一定会调用 app-bob:fat,例如 app-lucy:fat 的分组信息应该是:
b. 对于 fat-x 环境,对其业务本身的api统一设置分组,其它业务的使用默认分组,例如 app-lucy:fat-1 的分组信息应该是:
为了少做无用功,去跟导师同事讨论了下,确认理论可行后,再开始动手。
三、问题的关键
经过上面的尝试,问题关键点变成了如何识别出本业务的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的管理后台,调用正常
注册中心provider注册情况如下:
五、局限性
- 要求 app-lucy 的dubbo接口存放有规律,因为要通过正则进行匹配
例如lucy业务的接口都放在 com.wingli.lucy.dubbo.api 下,否则的话,正则写起来很复杂,如果新增一个接口在别的包下,那么就需要修改匹配的正则。
- dubbo 接口的 beanId 使用了默认值,暂不支持指定beanId
在代码示例中可以看到,我们配置分组是通过设置properties,即dubbo.reference.$beanId.group=xxx,而其中的{beanId}我们使用了api的全限定名,因为如果不加特殊配置,beanId是等于全限定名的,但如果通过其它方式指定了beanId,而又使用全限定名来设置的话,那就不生效了
- 使用反射扫描字段,会有一定的耗时
相对于直接声明所有接口并设置properties来说,反射获取是相对较慢的,但是dubbo的注入也是通过这个反射做的,所以时间也还好,耗时跟运行的环境关联性比较大,有兴趣的小伙伴可以自测一下
六、参考
- bean加载:juejin.cn/post/697794…
- 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
}
}