在前面的文章中,介绍过 dubbo-go 如何实现路由策略功能。路由策略功能是 dubbo-go 中的重要功能之一,可在控制面对服务的路由进行精细控制。同时,在实现之初,路由策略配置与其他配置是串行加载,在获得服务端的实例列表后,与处理好的路由规则进行匹配,从而过滤出可使用的候选实例。因为处理其中匹配逻辑较为复杂,所以导致启动服务的时间增加。网络请求通过健康实例优先路由规则与路由实例匹配时,通过双循环匹配(可能大家会有个小疑问,为什么是双循环呢?后面会慢慢解释)列表,导致查找时间增加。那针对以上问题,是否会有解决方法呢?答案是肯定的,所以本文中会介绍在路由策略的基础上,如何进行路由策略优化的两种手段展开分析,其分别是:异步解析路由规则、路由规则快速匹配。
图 1 将本次修改点具象化,在改造前,路由配置通过读取、解析、过滤、匹配四大步骤,将用户配置的路由规则,转换成可匹配请求的处理逻辑,最终得出请求可请求的实例。而本次改造最主要的目的是:网络请求通过健康实例优先路由规则与路由实例匹配时,需要将两个列表相交,分别是【健康的实例列表】与【符合路由规则的实例列表】,从而得出可访问的实例列表。如果使用双循环,算法复杂度随着两个列表的数据量增加而增加。所以本次改造时,选择了 压缩位图 作为底层存储,让匹配更高效。
在改造时,发现主要在存储方式变更后,会影响到 过滤与匹配 两大部分。在启动过滤时,改造成压缩位图,需要执行比较多构造逻辑。如果启动时同步执行,需时较长。所以在启动时改造成异步执行。而在匹配时,需要将入参的实例列表改造成压缩位图,以便后续进行匹配。
异步解析路由规则
接下来,先分析如何将串行解析路由规则改造成异步解析,从而降低启动服务的时间。
在使用路由策略时,为了提高程序匹配路由规则的效率,路由规则的读取和解析皆在应用启动时完成。而随着需要读取的路由配置增加,启动时间则会越来越长。将解析路由规则中的过滤实例列表操作修改成了异步,从而减少启动需要处理的信息,达到降低启动时间的效果。到这里大家可能会有个小疑问:究竟改造优化了什么呢?
在构造路由链的构造方法中,将同步构建过滤器并过滤实例列表,修改成异步构建。在构造方法中,会调用一个异步定时器与监听修改方法:chain.loop。
// 构造路由链func NewRouterChain(url *common.URL) (*RouterChain, error) { ...... go chain.Loop() <---- 异步定时器与监听修改方法 return chain, nil}
深入 chain.loop 内部,能看到其中包含定时与通知刷新缓存,两步的操作。
-
定时刷新:用于初始化缓存。
-
通知刷新:用于接收更新通知,更新路由的缓存,其结构是 router -> invokers。通过配置中心动态增加路由规则、实例列表更新后,都会触发该动作。
func (c *RouterChain) Loop() { ticker := time.NewTicker(timeInterval) for { select { case <-ticker.C: // 定时刷新缓存 if protocol.GetAndRefreshState() { c.buildCache() } case <-c.notify: // 收到通知之后,构建缓存 c.buildCache() } }}
经过优化之后,有效降低服务的启动时间。
存储方式
本次的修改是将存储的数组,改成位图,而使用的位图为:RoaringBitmap。
主要的思路是:将32位无符号整数按照高 16 位分桶,最多可能有 216 个桶,每一个桶中有一个 container 用于存储数据。存储数据时,按照数据的高 16 位(k % 2^16)找到需要存储的桶,如果不存在则新建一个新的桶,再将低 16 位(k mod 2^16)放入桶的 container 中。换句话说:一个位图就是很多桶的集合。
图 2
图 2 中,分别代表 3 个数据集:
-
第一个桶,存储有前1000个62的倍数。
-
第二个桶,存储有[216, 216+100)区间的数。
-
第三个桶,存储有[2×216, 3×216)区间内的所有偶数。
如有兴趣,可访问:roaringbitmap.org/ 获取更多信息。
匹配实例列表
在路由的具体实现中,包含条件路由(condition)、连接健康路由(conncheck)、实例健康优先路由(healthcheck)、标签路由(tag) 四个主要实现。其中本次优化只适用于连接健康路由(conncheck)、实例健康优先路由(healthcheck)、标签路由(tag)三个实现,而不适用于条件路由(condition)。
以条件路由(condition)为例,需要将路由配置的内容与元数据(包含 Provider 与 Consumer)的内容相匹配(如图 2 所示),且路由配置里的内容为表达式形式,所以并不能进行位图匹配。
图 2
以实例健康优先路由(healthcheck)为例,通过判断【健康的实例列表】与【符合路由规则的实例列表】的交点,从而快速的得出可访问的实例列表(如图 3 所示)。以便降低双循环带来的算法复杂度,提升程序的性能。在【健康的实例列表】或者【符合路由规则的实例列表】数据量增长时,性能不会带来明显的下降。
图 3
连接健康路由(conncheck)与标签路由(tag)的实现,在实现思想层面上是一致的,我就不一一累赘了。
总结
相信你已经知道了在优化路由策略时, Dubbo-go 是通过哪些方式进行优化。而关于路由策略,还有很多深层的知识值得我们一一探索和学习,比如:
-
Dubbo-go 条件路由是如何实现?
-
Dubbo-go 标签路由是如何实现,如何通过池化优化其逻辑?
这一系列问题的答案,大家可以从代码中,慢慢寻找答案。
欢迎加入社区
在公众号【部长技术之路】后台回复关键字【dubbogo】加入社区。