Choerodon 的微服务之路(二):Choerodon 的微服务网关
本文是 Choerodon 猪齿鱼微服务系列文章的第二篇。在《Choerodon的微服务之路(一):如何迈出关键的第一步》中,我们了解到在微服务架构中,一个完整的单体应用被拆分成多个有着独立部署能力的业务服务,每个服务可以使用不同的编程语言,不同的存储介质,来保持最低限度的集中式管理。本篇将介绍Choerodon在搭建微服务网关时考虑的一些问题以及两种常见的微服务网关。
▌文章的主要内容包括:
- 为什么要使用API Gateway
- 两种Gateway 模式
- Choerodon 的网关
对于Choerodon 而言,前端通过ReactJs实现,后端服务则通过Java,GoLang等多种语言实现。我们通过将后端拆分成许多个单独的业务服务,选择不同的语言切实地帮助我们来实现系统功能,这种面向服务的模式给我们带来了开发的便捷性,但是也带来了新的问题。服务之间如何做到相互通信,前端与后端又是如何进行通信的,是我们需要去解决的问题。
回到微服务架构的领域,如果要解决基本的通信问题,基本上只要解决下面三个问题就可以了。
- 服务网络通信能力
- 服务间的数据交互格式
- 服务间如何相互发现与调用
除了这些基本的问题以外,因为整个Choerodon是一个分布式的系统,开始时看似清晰的服务拆分,实则杂乱无章,有时候完成一个业务逻辑需要到不同的服务区调取接口,这是一件很痛苦的事,同时我们又不得不面对分布式的一些问题。包括负载均衡,链路追踪,限流,熔断,链路加密,服务鉴权等等一大堆的问题。于是一个面向服务治理、服务编排的组件——微服务网关,是我们首要考虑的解决方案。
为什么要使用API Gateway
我们回到文章开始时的三个问题,我们来考虑如何解决服务间的通信。
▌为什么使用HTTP?
HTTP是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),无论在哪种语言中几乎都有原生的支持,即使没有,也有第三方库来支持。通过HTTP 减少网络传输,来解决服务间网络的通信问题。
▌为什么选择JSON 作为数据交互格式?
因为 JSON 本身轻量、简洁,不管是编写,传输,还是解析都更加高效,而且相对来说,每个语言支持地都比较好。通过对JSON 的序列化和反序列化来实现网络请求中的数据交互。
▌为什么使用K8S 来进行服务发现?
对于负载均衡而言,业内已经有多种成熟的解决方案了,也大多是通过DNS 的方式去发现服务。不论是使用硬件F5 来解决,或者软件nginx,甚至Spring Cloud 也提供了对Eureka、Consul 等多种服务发现的支持。不过由于Choerodon 使用K8s 来作为服务编排引擎,基于K8s Client 来实现服务发现则更符合我们的切实需求。
当然对于Choerodon 而言,我们需要的不仅仅是一个简单的通信方式,而是一个完整的微服务解决方案。
API Gateway(API 网关)作为微服务体系里面的一部分,其需要解决的问题和 Choerodon 需要解决的问题非常类似。顾名思义,是企业 IT 在系统边界上提供给外部访问内部接口服务的统一入口。在微服务概念的流行之前,API网关的实体就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。
API 网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与 Facade 模式类似。API 网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。
如果没有 API 网关,大家可能想到的一贯做法是通过前端客户端与后端服务直接通信。这样会存在以下一些问题:
- 客户端直连各个服务,耦合度太高
- 所有的服务接口都需要暴露给客户端,会存在安全隐患
- 当服务增多时,后端服务对于客户端而言则是一个噩梦
- 多次访问不同的服务,请求数量过多,影响效率
- 权限、身份校验等需要每个服务都实现,重复造轮子
Choerodon 通过使用 Spring Cloud Zuul,将所有的后端都通过统一的网关接入微服务体系中,并在网关层处理所有的非业务功能,同时提供统一的REST/HTTP 方式对外提供API。
单节点Gateway 模式
所谓的单节点Gateway 模式,也就是提供一个单一的Gateway 来支持不同的客户端访问。
这种模式下,大家会使用一个自定义 API 网关服务面对多个不同客户端应用程序。其中最大的好处就是所有的请求都受统一网关的控制,实现简单。对于请求的身份认证、负载均衡、监控等都可以在统一的网关中实现。
伴随这一好处的同时,也会带来一定的风险。因为随着后端服务的增多,网关的API 将针对不同客户端发展,越来越多。同时,由于接口权限、身份验证等都在网关中实现,统一网关也会变得越来越庞大,类似于一个单独的应用程序或者单体应用。
除此之外,也会引入一个新的问题,即资源隔离的问题。假设后端的一个服务突然变慢,由于所有的请求都使用同一个网关入口,可能会将网关拖垮,进而影响到其他服务接口的访问。
要解决这个问题,有两种方式可以去解决,一种是做线程池的隔离,可以给一些重要的业务一些单独的线程池,不重要的业务再放到一个大的单独的线程池里面。另一种就是给不同的业务设置不同的网关。
Spring Cloud 可以通过修改 ZUUL 和 hystrix 的配置,将信号量隔离修改为线程池隔离,提高性能。
zuul: ribbonIsolationStrategy: THREAD hystrix: command: default: execution: isolation: strategy: THREAD #hystrix隔离策略,默认为THREAD thread: timeoutInMilliseconds: 20000 #hystrix超时时间 threadpool: default: coreSize: 100 #并发执行的最大线程数 maximumSize: 5000 #最大线程池大小 allowMaximumSizeToDivergeFromCoreSize: true #允许maximumSize 配置生效 maxQueueSize: -1 #设置最大队列大小,为-1时,使用SynchronousQueue
线程池隔离仅仅做到了线程池的隔离,但是 CPU 和 Memory 之类资源的隔离其实并没有做。如果想要更加彻底的隔离方式,可以采用和线程池隔离类似的方式,给重要的服务用独立的网关来为其服务,不重要的服务,再给一个独立的网关来服务。这也就是多节点的Gateway 模式。
多节点Gateway 模式
多节点的Gateway 模式本身是一种BFF架构,即为不同的设备提供不同的API接口,引申而来,也可以按照不同的业务类型划分为多种业务场景下的网关。
上面这张图显示了一个简化版本的多API 网关。在这种情况下,每个API 的边界是基于BFF 模式,因此只提供每个客户端应用所有要的API。
这种模式带来的好处是根据不同颗粒度的API网关,在性能上能够做到更精确的控制。但是在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,则需要在每个网关中重复实现。代码开发比较冗余。
可以说,这两种模式各有利弊,并不能单纯的比较其好坏,而应该根据实际的业务场景来选择适合自己的解决方案。
Choerodon 的网关
结合Choerodon 自身的核心业务,我们在不考虑多终端的情况下,最终选择了单一网关,并在此基础上,做了插件化的开发。
Choerodon 认为,一个网关应该包含两部分。
服务网关 = 路由转发 + 过滤器
路由转发:将外部的请求,转发到对应的微服务上 过滤器:包含一系列非功能的横切需求。例如权限校验、限流、监控等
我们在API 网关中保留了Spring Cloud Zuul 的路由转发,然后将权限校验等抽离到一个叫做gateway-helper
的服务中。如下图所示。
请求到达api-gateway
后,根据当前的HttpContext
上下文封装一个RibbonCommandContext
对象,该对象将包含了请求转发的gateway-helper
对应的信息。再由RibbonCommandFactory
根据RibbonCommandContext
对象生成一个RibbonCommand
,由RibbonCommand
完成HTTP 请求的发送并的得到响应结果ClientHttpResponse
。核心代码如下:
// GateWayHelperFilter.java public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; RibbonCommandContext commandContext = buildCommandContext(req); try (ClientHttpResponse clientHttpResponse = forward(commandContext)) { if (clientHttpResponse.getStatusCode().is2xxSuccessful()) { request.setAttribute(HEADER_JWT, clientHttpResponse.getHeaders().getFirst(HEADER_JWT)); chain.doFilter(request, res); } else { setGatewayHelperFailureResponse(clientHttpResponse, res); } } catch (ZuulException e) { res.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); res.setCharacterEncoding("utf-8"); try (PrintWriter out = res.getWriter()) { out.println(e.getMessage()); out.flush(); } } } private RibbonCommandContext buildCommandContext(HttpServletRequest req) { Boolean retryable = gatewayHelperProperties.isRetryable(); String verb = getVerb(req); MultiValueMap<String, String> headers = buildZuulRequestHeaders(req); MultiValueMap<String, String> params = buildZuulRequestQueryParams(req); InputStream requestEntity; long contentLength; String requestService = gatewayHelperProperties.getServiceId(); requestEntity = new ByteArrayInputStream("".getBytes()); contentLength = 0L; return new RibbonCommandContext(requestService, verb, req.getRequestURI(), retryable, headers, params, requestEntity, this.requestCustomizers, contentLength); } private ClientHttpResponse forward(RibbonCommandContext context) throws ZuulException { RibbonCommand command = this.ribbonCommandFactory.create(context); try { return command.execute(); } catch (HystrixRuntimeException ex) { throw new ZuulException(ex, "Forwarding gateway helper error", 500, ex.getMessage()); } }
可以看到,我们在api-gateway
服务中完成了对请求的首次转发。请求到达gateway-helper
。在gateway-helper
中,针对配置进行判断,如果有自定义的helper,则会重定向到自定义的helper 上进行后续的处理。否则的话按照默认的逻辑进行权限校验。核心代码如下:
// RequestRootFilter.java public boolean filter(final HttpServletRequest request) { String uri = RequestRibbonForwardUtils.buildZuulRequestUri(request); String service = RequestRibbonForwardUtils.getHelperServiceByUri(helperZuulRoutesProperties, uri); if (StringUtils.isEmpty(service)) { return requestPermissionFilter.permission(request) && requestRatelimitFilter.through(request); } return customGatewayHelperFilter(request, service, uri); } private boolean customGatewayHelperFilter(final HttpServletRequest request, final String service, final String uri) { ClientHttpResponse clientHttpResponse = null; try { RibbonCommandContext commandContext = RequestRibbonForwardUtils.buildCommandContext(request, requestCustomizers, service, uri); clientHttpResponse = RequestRibbonForwardUtils.forward(commandContext, ribbonCommandFactory); return clientHttpResponse.getStatusCode().is2xxSuccessful(); } catch (Exception e) { LOGGER.warn("error.customGatewayHelperFilter"); return false; } finally { if (clientHttpResponse != null) { clientHttpResponse.close(); } } }
在RequestPermissionFilter
中,我们对请求进行权限校验,来判断该用户是否有对应资源的操作权限。核心代码如下:
//RequestPermissionFilterImpl.java public boolean permission(final HttpServletRequest request) { if (!permissionProperties.isEnabled()) { return true; } //如果是文件上传的url,以/zuul/开否,则去除了/zuul再进行校验权限 String requestURI = request.getRequestURI(); if (requestURI.startsWith(ZUUL_SERVLET_PATH)) { requestURI = requestURI.substring(5, requestURI.length()); } //skipPath直接返回true for (String skipPath : permissionProperties.getSkipPaths()) { if (matcher.match(skipPath, requestURI)) { return true; } } //如果获取不到该服务的路由信息,则不允许通过 ZuulRoute route = ZuulPathUtils.getRoute(requestURI, helperZuulRoutesProperties.getRoutes()); if (route == null) { LOGGER.info("error.permissionVerifier.permission, can't find request service route, " + "request uri {}, zuulRoutes {}", request.getRequestURI(), helperZuulRoutesProperties.getRoutes()); return false; } String requestTruePath = ZuulPathUtils.getRequestTruePath(requestURI, route.getPath()); final RequestInfo requestInfo = new RequestInfo(requestURI, requestTruePath, route.getServiceId(), request.getMethod()); final CustomUserDetails details = DetailsHelper.getUserDetails(); //如果是超级管理员用户,且接口非内部接口,则跳过权限校验 if (details != null && details.getAdmin() != null && details.getAdmin()) { return passWithinPermissionBySql(requestInfo); } //判断是不是public接口获取loginAccess接口 if (passPublicOrLoginAccessPermissionByMap(requestInfo, details) || passPublicOrLoginAccessPermissionBySql(requestInfo, details)) { return true; } if (details == null || details.getUserId() == null) { LOGGER.info("error.permissionVerifier.permission, can't find userDetail {}", requestInfo); return false; } //其他接口权限权限审查 if (passSourcePermission(requestInfo, details.getUserId())) { return true; } LOGGER.info("error.permissionVerifier.permission when passSourcePermission {}", requestInfo); return false; }
通过上述的代码片段可以看到。在Choerodon 中,可以自主实现自己的geteway-helper
,来对请求进行更复杂的控制。
Choerodon 支持在页面上对路由信息进行配置和修改,控制路由的动态调整。如下图所示。
以看到,通过页面对路由进行修改后,路由动态更新到 api-gateway及gateway-heler。通过配置中心实时生效,避免了修改代码重新部署带来的麻烦。
总结
回顾一下这篇文章,我们介绍了Choerodon 在搭建微服务网关时考虑的一些问题以及两种常见的微服务网关,并且通过代码介绍了Choerodon 的网关时如何实现的。这些都是我们实践过程中的一些做法和体会,希望大家可以结合自己的业务来参考。
更多关于微服务系列的文章,欢迎点击阅读 ▼
关于Choerodon猪齿鱼
Choerodon猪齿鱼是一个开源企业服务平台,是基于Kubernetes的容器编排和管理能力,整合DevOps工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理的开源平台,同时提供IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。
大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:
- 官网:http://choerodon.io
- 论坛:http://forum.choerodon.io
- Github:https://github.com/choerodon
- 微信:Choerodon猪齿鱼
- 微博:Choerodon猪齿鱼
欢迎加入Choerodon猪齿鱼社区,共同为企业数字化服务打造一个开放的生态平台。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
HTTPS通信的C++实现
HTTPS是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。Nebula是一个为开发者提供一个快速开发高并发网络服务程序或搭建高并发分布式服务集群的高性能事件驱动网络框架。Nebula作为通用网络框架提供HTTPS支持十分重要,Nebula既可用作https服务器,又可用作https客户端。本文将结合Nebula框架的https实现详细讲述基于openssl的SSL编程。如果觉得本文对你有用,帮忙到Nebula的Github或码云给个star,谢谢。Nebula不仅是一个框架,还提供了一系列基于这个框架的应用,目标是打造一个高性能分布式服务集群解决方案。Nebula的主要应用领域:即时通讯(成功应用于一款IM)、消息推送平台、数据实时分析计算(成功案例)等,Bwar还计划基于Nebula开发爬虫应用。 1. SSL加密通信 HTTPS通信是在TCP通信层与HTTP应用层之间增加了SSL层,如果应用层不是HTTP协议也是可以使用SSL加密通信的,比如WebSocket协议WS的加上SSL...
- 下一篇
一个GO语言性能问题的发现和解决
本文是大 U 同事的一篇实操性经验贴,是发现问题、分析问题到解决问题的完整案例,借此分享,希望对各位有所帮助。 事件起因 事情起因于公司一位同事在内部邮件组中 post 了一个问题,一个使用了 go1.8.3 写的业务程序跑了一段时间后出现部分 goroutine 卡在等待一个锁 ForkLock 的现象,同事认为这是 go1.8.3 的 bug,升级到 go1.10 后没有再重现。为了搞清楚这个事情,同事在 github 上发了 issue: https://github.com/golang/go/issues/26836,期间也做了很多重现的尝试,但并未重现。 我浏览了一下出现该问题的业务代码,大概的使用方式是父进程调用 os/exec 下的 Command 开子进程执行 shell 命令。Command 后面会调用 golang 封装的 forkExec 来开子进程并执行命令,forkExec 使用了 ForkLock。 问题分析 ForkLock 的存在是为了避免下面的情况:在有多个 goroutine 同时 fork exec 的情况下, 为了子进程只继承它需要的文件描述符...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- 设置Eclipse缩进为4个空格,增强代码规范
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- 2048小游戏-低调大师作品