导语
随着车金融业务的快速发展,单体架构的系统已经不能满足业务的快速发展的需要,在这种情况下, 本文主要介绍微服务网关在金融的实践与演进过程。
随着车金融业务的快速发展,单体架构的系统已经不能满足业务的快速发展的需要,因此在2018年初,我们对车金融业务进行了微服务架构的升级改造, 整个系统拆分出40多个微服务。在重构过程中我们发现以下几个问题:
每一个访问微服务系统的客户端都需要维护一份服务路由关系;
一些通用的如身份鉴权、权限控制等功能,微服务中重复开发。
为了解决上述的痛点,方便统一调用微服务接口,所以在架构上引入了服务网关。
网关又称为API网关,是微服务系统的唯一流量入口。所有的客户端都通过网关访问微服务,API网关封装了系统的内部访问,同时提供了部分通用的功能,比如:身份验证、权限、负载均衡、限流、熔断、灰度发布等。
以电影场景举例来说:
顾客1观看3D电影,由检票员检票通过之后发放3D眼镜,并指引顾客进入3D观影厅;顾客2和顾客3观看2D电影,由检票员检票通过之后,指引顾客进入2D观影厅;在互联网领域中,顾客为流量,检票为身份鉴权,发放3D眼镜为对请求的扩展,指引顾客进入不同的观影厅为对请求的路由。
API网关优势
在不引入网关系统的情况下:
1.客户端会请求不同的微服务,会增加客户端复杂性
2.每个服务需要独立开发相同的非业务功能(身份认证)
引入网关系统后:
1.降低客户端访问微服务的复杂度,对路由配置统一管理
2.提供公共通用功能(如:权限控制,身份认证)
技术选型
业界网关解决方案有很多,包括商业的、开源的。例如Tyk(Tyk 是一个基于Go实现的网关服务)、Kong、Orange(和Kong类似,中国人开发,有比较有好的UI界面)、api-umbrella(Ruby实现的一个 API 网关)、apiaxle(Nodejs 实现的一个 网关)、Netflix zuul、nginx+lua等;最终,由于金融的java生态,并且基于spring体系的java架构,决定技术选型为Netflix zuul作为金融的网关服务。
ZUUL简介
zuul 是什么?zuul是API网关的开源实现方案,主要包含了对请求的路由和过滤两个功能。zuul是由Netflix开源的微服务网关,是基于JVM的路由器和服务端负载均衡器,可以和Consul、Ribbon、Hystrix等组件配合使用,并且Spring Cloud对Zuul进行了整合,使我们可以非常简洁方便的构建我们的API网关。
zuul的核心是一系列的filters, 其作用可以类比Servlet框架的Filter,或者AOP;zuul中定义了四种标准过滤器类型,这些过滤器类型对应于请求的典型生命周期。
1.PRE:这种过滤器在请求被路由之前调用。可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
2.ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
3.POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTPHeader、收集统计信息和指标、将响应从微服务发送给客户端等。
4.ERROR:在其他阶段发生错误时执行该过滤器。
下图为过滤器的生命周期:
金融网关实践
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);
}
这样完整的动态路由就实现完成了。
引入consul作为注册中心
经过上面的改造后,发现在应用过程中新接入服务必须要经过人工配置,并且新服务都需要为接入网关而申请内网域名,为了解决人工配置和申请域名的人工介入,注册中心就粉墨登场了。
Consul是一种 服务网络解决方案,可跨任何运行时平台以及公共或私有云连接和保护服务。
金融网关借助于集团的云平台,在每一个业务实例所在的docker中,启动一个consul的agent进程(即consul client),这个agent会收集业务实例进程的相关信息(如:容器ip、业务进程端口等)上报给consul server集群,该agent也负责做服务的健康检查相关的工作,并且随服务一起启动,一起销毁;然后网关会通过consulserver获取服务路由信息进行路由。通过引入consul彻底解决了服务的人工配置,做到了自动化的服务发现与路由。
网关内部线程模型
目前我们使用的zuul版本为1.x,该版本中对一次请求的拦截与路由使用的是同步阻塞线程;
1.优势
首先在设计层面上架构设计简单,其次源码阅读上代码易于理解,最后是链路追踪比较方便,出现问题时易于排查。
2. 缺点
zuul内部本质上是一个同步的servlet,这样每一个请求servlet都会为其分配一个线程来处理这个请求,但是容器中的线程是有限的,一般会使用线程池,当后端服务响应缓慢时,线程资源会被持续占用,当线程被大量占用导致连接池满之后,新请求会被拒绝。
未来展望
对于网关目前存在的问题,首先在未来会基于Netty去改造金融网关;同时网关也是所有服务的入口,也会对服务的性能分析以及健康指标做一些相关的分析工作。
参考文献:
1.zuul github(https://github.com/Netflix/zuul/wiki)
2.zuul源码(https://github.com/Netflix/zuul/tree/1.x)
作者简介:
彭海滨,金融公司车贷技术部开发工程师,负责金融公司网关建设和开发。
END
相关推荐:
独家|Linux进程内存用量分析之堆内存篇
独家| rocksdb compaction限速实践与源码分析
独家|一文了解58安全画像系统演进之路