得物技术网关路由匹配性能优化

为了进一步加强网络安全防范工作,近期对网关服务做了相关的安全升级,其中变化最大的一点是,网关不再提供URI模糊匹配的模式,形如 /api/v1/app/order/** 这样的配置已经不在支持,相信很多小伙伴已经感受到了日常开发上线的不便,但是需要理解的是,随着公司的体量的迅速发展,各方面越来越规范化,安全方面加强管控显然还是非常必要的。
首先看下得物流量传递的基本路径:
APP网关流量路径:四层高防 --> 阿里云SLB --> Gateway --> 业务服务/业务网关(提供协议转化 &接口聚合)
通常来说与业务方打交道的最多的是gateway 服务,很多萌新可能不是很理解网关具体在干啥,这里做个简要说明,网关最大的作用是提供流量分发,同时具备流量管控,防爬,黑白名单,基本鉴权,接口超时,灰度等常见功能;小伙伴们日常开发中最长用到的就是流量转发,比如新起一个服务需要对外网暴露接口,此时就需要在网关的路由管理上进行配置。
所以Spring gateway 的路由匹配就成了一个非常核心的关键功能,这里我们翻阅一下 Spring gateway 的源码。
由于Spring gateway 使用webflux 技术,整体的代码风格较为诡异。
这里简单介绍下webflux的基本概念:
flux 表示 1~N 数据元素
mono ,表示 0~1 个数据元素,
针对数据流的所有操作,在没有订阅之前都不会被触发,只有调用了 subscribe方法后才会实际触发。
得物技术网关路由匹配性能优化
文章图片

图一*
这里我们看下 DispatcherHandler 的handle 方法,该方法会进行webHandler 的适配,对于网关来说这里主要匹配的是RoutePredicateHandlerMapping 这一对象,我们可以从 hadlerMappings 对象中看到:
得物技术网关路由匹配性能优化
文章图片

RoutePredicateHandlerMapping 中的webHandle为 FilteringWebHandler 该handle 中包含了gateway 自带的以及网关自定义的共28个 GolobalFilter
讲到这里很多小伙伴可能会好奇,这个路由匹配到底是在哪儿做的呢,别急,我们慢慢开趴!
按照图一所示,选中mapping后会获取Handler,而获取handler 后优会调用 invokeHandler 方法,那么我么不妨先到 getHandler 方法中看看,点开 RoutePredicateHandlerMapping 源码,我们郁闷的发现并没有 getHandler 方法,而只有getHandlerInternal 方法,仔细看下 RoutePredicateHandlerMapping 的继承关系发现该类继承了 AbstractHandlerMapping, 而AbstractHandlerMapping 中 getHandler 方法早已存在,实现了HandlerMapping 接口同时也做了部分实现 ;废话不多说,源码底下无内鬼!!
得物技术网关路由匹配性能优化
文章图片

原来getHandlerInternal 是在 getHandler 方法中被调用的。这就解释得通了,
仔细观察了 getHandler 中的逻辑,并没有路由匹配的逻辑,此时嫌疑最大的当属 RoutePredicateHandlerMapping 的 getHandlerInternal 了!
得物技术网关路由匹配性能优化
文章图片

不出所料,lookupRoute 没跑了!!
lookupRoute的代码很简单,核心逻辑为简单的匹配,同时添加错误处理,在匹配成功的情况下会把路由信息添加到 ServerWebExchange 中的attributes 中,代码如下:
得物技术网关路由匹配性能优化
文章图片

观察filterWhen,我们会发现这是一个for循环匹配,也就是说,效率为O n, 在路由信息比较多的情况下非常糟糕,当然这不能怪Spring,毕竟gateway 设计之初,是支持各种正则,模糊匹配的,这种要求下,做到O 1的效率并不现实 ,但是结合得物当前的使用场景,我们可以做进一步的优化:
由于新的路由添加为精确模式,也就是每个接口对应一个路由,这种前提下,我们很显然的想到了HASH 算法,由于对于pathVariable模式的path也不再支持(小伙伴们可以思考下,这种接口有什么缺点) ,在收到请求的时候直接提取path部分,通过hash的方式获取到对应的路由信息,改造后的路由查找逻辑如下所示:
得物技术网关路由匹配性能优化
文章图片

findRoute()方法中的逻辑非常简单的:
得物技术网关路由匹配性能优化
文章图片

为了保证并发安全,这里的pathRouteMap 为 ConcurrentHashMap ,其实修改为HashMap 也是可以的,因为路由匹配时,对map是只读操作,更新时候是整体map 引用替换:这里附上刷新路由缓存信息的代码
得物技术网关路由匹配性能优化
文章图片

由于更新路由信息的操作属于高危且核心的操作,对于一个批次的更新最好能够原子性完成,这里我们引入了Copy on write 的思路,修改的时候,先修改bakMap ,等到bakMap中的全部路由信息更新完成后,我们将实际使用的map 引用指向bakMap, 同时将bakMap设置为空。此外更新路由的操作一般来说都是事件触发异步完成,因此对于性能要求并不高,这里加上锁进一步保证路由更新完整性,防止在多个线程调用时,map与bakMap之间出现不匹配的情况!
需要指出的是 gateway 的路由查找逻辑依赖于 CachingRouteLocator, 该类监听路由更新事件,实际的路由刷新通过发布事件的方式完成。观察源码,我们发现处理路由刷新事件时调用了fetch 方法;
得物技术网关路由匹配性能优化
文章图片

同时在初始化阶段以及缓存命中失效阶段时也调用了 fetch方法(这里缓存是gateway自带的缓存机制,而非我们添加的Map缓存)
得物技术网关路由匹配性能优化
文章图片

因此我们可以在fetch 方法中加入 refreshPathRouteMap() 方法;
在lookupRoute 方法中的 this.routeLocator.getRoutes() 实际调用的是CachingRouteLocator#getRoutes()方法。此方法直接返回被缓存的的信息,这里的缓存指的是 gateway 自带的
routes = CacheFlux.lookup(cache, CACHE_KEY, Route.class).onCacheMissResume(this::fetch);
逻辑简单翻译一下,就是如果缓存命中失败会调用fetch方法重新加载路由信息。
至此,路由匹配的逻辑大致分析完成!其实对于之前的 /api/v1/app/order/* 这种模式的路由也可以通过hash方式进行加速,只需要将 * 去掉,作为map的key,在处理请求的时候,尝试获取请求的前缀进行匹配即可!
最终我们实战观察一下改进的实际效果:
【得物技术网关路由匹配性能优化】得物技术网关路由匹配性能优化
文章图片

可以发现,实际的CPU占用从原来的平均24% --> 12% ,比原先下降了一半左右!
文/簌语
关注得物技术,携手走向技术的云端

    推荐阅读