独家|微服务网关组件在金融的实践
导语
随着车金融业务的快速发展,单体架构的系统已经不能满足业务的快速发展的需要,在这种情况下,本文主要介绍微服务网关在金融的实践与演进过程。
背景
随着车金融业务的快速发展,单体架构的系统已经不能满足业务的快速发展的需要,因此在2018年初,我们对车金融业务进行了微服务架构的升级改造, 整个系统拆分出40多个微服务。在重构过程中我们发现以下几个问题:每一个访问微服务系统的客户端都需要维护一份服务路由关系; 一些通用的如身份鉴权、权限控制等功能,微服务中重复开发。
什么是网关
网关又称为API网关,是微服务系统的唯一流量入口。所有的客户端都通过网关访问微服务,API网关封装了系统的内部访问,同时提供了部分通用的功能,比如:身份验证、权限、负载均衡、限流、熔断、灰度发布等。以电影场景举例来说:顾客1观看3D电影,由检票员检票通过之后发放3D眼镜,并指引顾客进入3D观影厅;顾客2和顾客3观看2D电影,由检票员检票通过之后,指引顾客进入2D观影厅;在互联网领域中,顾客为流量,检票为身份鉴权,发放3D眼镜为对请求的扩展,指引顾客进入不同的观影厅为对请求的路由。API网关优势
在不引入网关系统的情况下:
1.客户端会请求不同的微服务,会增加客户端复杂性
2.每个服务需要独立开发相同的非业务功能(身份认证)
引入网关系统后:
1.降低客户端访问微服务的复杂度,对路由配置统一管理
2.提供公共通用功能(如:权限控制,身份认证)
金融网关实践1.网关建设初期随着金融多业务线的不断发展,网关需要提供更多的功能,比如:灰度,白名单标签等。同时,不同的业务也需要搭建网关服务。所以网关面临下面三个问题:1)新接入业务必须要修改静态路由配置文件,熟悉spring的同学都知道就是yml文件,这样势必会引入线上重启的风险;2)随着服务接入的增多,各个服务也会有各种拦截功能的调整,比如首页不需要登录拦截,基础数据不需要权限功能等,这时候需要修改网关中的源码来做到这种适配;3)伴随着金融各个业务线微服务架构调整,每个业务线都需要建设自己的网关,各个业务线的网关有许多相同的功能相互重叠,并且得不到复用,每个业务线也需要投入人力去开发与维护相关的工作;基于上面的三个问题,我们对金融网关也进行了改造升级。2.网关云演进过程
为了改造原有各个业务线重复建设导致的资源浪费,首先整合所有业务网关到单集群中,然后依托于集团云平台的流量分组能力,在网关内部对不同业务线做了流量隔离。引入数据库作为网关配置,把服务注册、路由配置以及功能组件作为动态配置项,提供可视化界面增加、修改配置信息,配置的修改会通过消息队列通知网关集群,网关修改相应的内部配置缓存;以此来支持网关功能组件的可插拔式配置;目前网关的内部架构可以灵活的支持不同业务线的业务拦截需求,对内部新业务的扩展也可以做到通过配置的形式支持。下面将详细介绍网关功能组件的动态配置及动态路由的改造过程。
(1)网关动态配置演进
网关对于不同的请求做不同的功能拦截操作,需要修改相关代码做一些适配工作。随着网关集群的业务线增加,每个业务线都需要一些需求调整,这时候会带来一些网关功能的调整,为了节省修改代码的人力成本和消除不必要的上线;因此,我们就思考如何才能把这些静态配置化操作转为动态化呢?
为了做动态化拦截功能配置。首先把拦截功能模块基于责任链模式做了拆分,拼接链的环节通过配置中心加载到内存中的配置,对不同的服务进行不同的责任链拼接,这样配置中心修改配置网关实时感知配置的变动,进行动态拦截功能模块的动态配置化改造。那么对于动态路由的改造呢?
(2)动态路由
由于zuul在不引入注册中心的情况下只支持通过yml、properties获取路由信息,对于接入新服务非常的不友好,因为要修改静态配置文件然后进行上线升级操作。在第一版的演进过程中希望通过db暂时作为配置中心,而不引入注册中心。因此通过对相关的源码进行了查看(本文内相关源码及配置均有删减,代码出处见参考文献)
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
...
// 路由预处理(pre阶段)
preRoute();
...
// 路由阶段(route阶段)
route();
...
// 请求响应阶段(post阶段)
postRoute();
}
在路由阶段(route阶段)请求会先经过RibbonRoutingFilter,然后经过SimpleHostRoutingFilter以下代码分别是两个filter的执行条件//RibbonRoutingFilter
ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null&& ctx.sendZuulResponse());
// SimpleHostRoutingFilter
RequestContext.getCurrentContext().getRouteHost() != null
&& RequestContext.getCurrentContext().sendZuulResponse();
通过以上代码,结合application.yml配置文件
zuul:
routes:
service1:
path: /service1/**
url: http://127.0.0.1:8080
service2:
path: /service2/**
serviceId: service2
当调用到RibbonRoutingFilter时会去判断serviceId是否为空(执行路由条件),当调用到SimpleHostRoutingFilter时会校验host是否为空。
由此推断路由信息是在pre阶段确定下来的,然后定位到PreDecorationFilter会根据请求URI匹配相应的路由信息,然后获取静态配置中的路由信息解析出相应的RouteHost和serviceId。其源码(由于源码过长,请同学们自行查看)中RouteLocator即为我们的路由定位器,也就是我们要重写的部分。
(3) 路由定位器
PreDecorationFilter通过RouteLocator根据URI获取Route,因此可以通过对RouteLocator的扩展来完成动态路由工作。Spring Cloud默认的路由定位器由SimpleRouteLocator来实现。
主要功能包含:
通过properties获取所有路由;
根据请求URI获取路由信息;
代码如下:
public class SimpleRouteLocator implementsRouteLocator, Ordered {
// routes 用于存储路由信息
private AtomicReference<Map<String,ZuulRoute>> routes = new AtomicReference<>();
// 查找路由信息
protected Map<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute>routesMap = new LinkedHashMap<>();
// 提取ZuulProperties中的ZuulRoute
for (ZuulRoute route :this.properties.getRoutes().values()) {
routesMap.put(route.getPath(), route);
}
return routesMap;
}
// 根据请求匹配路由
protected Route getSimpleMatchingRoute(final Stringpath) {
// 确认初始化路由map完成
getRoutesMap();
// 对URI处理
String adjustedPath = adjustPath(path);
// 获取匹配路由
ZuulRoute route = getZuulRoute(adjustedPath);
return getRoute(route, adjustedPath);
}
}
所以这里继承SimpleRouteLocator并重写了locateRoutes函数,由properties获取路由信息改为通过DB获取我们的路由信息。
@Override
public Map<String, ZuulRoute>loadLocateRoute() {
List<ZuulRouteDto> zuulRouteDtos =getZuulRoutes();
// 把DB获取的路由信息转为Map
Map<String, ZuulRoute> handle =handle(zuulRouteDtos);
return handle;
}
/**
* @authorpenghb
* @description 获取所有路由
* @date 8:37PM 2019/6/3
* @return 路由列表
**/
private List<ZuulRouteDto> getZuulRoutes() {
String cloudClusterGroup =System.getenv(SYSTEM_CLOUD_GROUP);
APIResponse<List<ZuulRouteDto>> all = zuulRouteService.findByCloudGroupCode(cloudClusterGroup);
return APIResponseUtils.getResultData(all);
}
(4) 路由动态刷新
由于Spring Cloud默认的SimpleRouteLocator是不支持路由刷新的,但是自定义的动态路由是要支持路由的刷新功能的(当配置中心路由信息修改后,网关要实时的刷新路由信息),因此在继承SimpleRouteLocator的基础上,还要实现Zuul提供的RefreshableRouteLocator来支持动态路由刷新能力。
zuul内部提供了ZuulRefreshListener,它会监听ApplicationEventPublisher发布的事件,如果事件为RoutesRefreshedEvent,则会调用routeLocator的refresh函数,在自定义的路由定位器中可以直接调用SimpleRouteLocator的doRefresh函数:
protected void doRefresh() {
this.routes.set(locateRoutes());
}
当路由信息在配置中心发生变化的时候,就通过ApplicationEventPublisher发布一个RoutesRefreshedEvent事件:RoutesRefreshedEventroutesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
publisher.publishEvent(routesRefreshedEvent);
这样动态刷新路由也实现了。
最后向IOC容器中注入自定义的路由定位器,去替换Spring Cloud的路由定位器。
@Bean
@ConditionalOnMissingBean(ZuulRouteDatabaseLocator.class)
public ZuulRouteDatabaseLocator zuulRouteDatabaseLocator() {
return newZuulRouteDatabaseLocator(this.server.getServletPrefix(), this.zuulProperties);
}
这样完整的动态路由就实现完成了。
参考文献: 1.zuul github(https://github.com/Netflix/zuul/wiki)2.zuul源码(https://github.com/Netflix/zuul/tree/1.x)
作者简介:彭海滨,金融公司车贷技术部开发工程师,负责金融公司网关建设和开发。
END
相关推荐:独家|Linux进程内存用量分析之堆内存篇
独家| rocksdb compaction限速实践与源码分析
独家|一文了解58安全画像系统演进之路