专注于Java领域优质技术,欢迎关注
前言
假设你正在开发一个电商网站,那么这里会涉及到很多后端的微服务,比如会员、商品、推荐服务等等。那么这里就会遇到一个问题,APP/Browser怎么去访问这些后端的服务? 如果业务比较简单的话,可以给每个业务都分配一个独立的域名(https://service.api.company.com),但这种方式会有几个问题:
每个业务都会需要鉴权、限流、许可权校验等逻辑,如果每个业务都各自为战,自己造轮子实现一遍,会很蛋疼,完全可以抽出来,放到一个统一的地方去做。如果业务量比较简单的话,这种方式前期不会有什么问题,但随着业务越来越复杂,比如淘宝、亚马逊开启一个页面可能会涉及到数百个微服务协同工作,如果每一个微服务都分配一个域名的话,一方面客户端程式很难维护,涉及到数百个域名,另一方面是连线数的瓶颈,想象一下你开启一个APP,通过抓包发现涉及到了数百个远端呼叫,这在移动端下会显得非常低效。每上线一个新的服务,都需要运维参与,申请域名、配置Nginx等,当上线、下线服务器时,同样也需要运维参与,另外采用域名这种方式,对于环境的隔离也不太友好,呼叫者需要自己根据域名自己进行判断。另外还有一个问题,后端每个微服务可能是由不同语言编写的、采用了不同的协议,比如HTTP、Dubbo、GRPC等,但是你不可能要求客户端去适配这么多种协议,这是一项非常有挑战的工作,专案会变的非常复杂且很难维护。后期如果需要对微服务进行重构的话,也会变的非常麻烦,需要客户端配合你一起进行改造,比如商品服务,随着业务变的越来越复杂,后期需要进行拆分成多个微服务,这个时候对外提供的服务也需要拆分成多个,同时需要客户端配合你进行改造,非常蛋疼。
API Gateway

通过引入API闸道器,客户端只需要与API闸道器互动,而不用与各个业务方的界面分别通讯,但多引入一个元件就多引入了一个潜在的故障点,因此要实现一个高效能、稳定的闸道器,也会涉及到很多点。
API注册
业务方如何接入闸道器?一般来说有几种方式。
第一种采用外挂扫描业务方的API,比如Spring MVC的注解,并结合Swagger的注解,从而实现引数校验、文件&&SDK生成等功能,扫描完成之后,需要上报到闸道器的储存服务。手动录入。比如界面的路径、请求引数、响应引数、呼叫方式等资讯,但这种方式相对来说会麻烦一些,如果引数过多的话,前期录入会很费时费力。配置档案汇入。比如通过SwaggerOpenAPI等,比如阿里云的闸道器:
协议转换
内部的API可能是由很多种不同的协议实现的,比如HTTP、Dubbo、GRPC等,但对于使用者来说其中很多都不是很友好,或者根本没法对外暴露,比如Dubbo服务,因此需要在闸道器层做一次协议转换,将使用者的HTTP协议请求,在闸道器层转换成底层对应的协议,比如HTTP -> Dubbo, 但这里需要注意很多问题,比如引数型别,如果型别搞错了,导致转换出问题,而日志又不够详细的话,问题会很难定位。
服务发现
闸道器作为流量的入口,负责请求的转发,但首先需要知道转发给谁,如何定址,这里有几种方式:
写死在程式码/配置档案里,这种方式虽然比较挫,但也能使用,比如线上仍然使用的是物理机,IP变动不会很频繁,但扩缩容、包括应用上下线都会很麻烦,闸道器自身甚至需要实现一套健康监测机制。域名。采用域名也是一种不错的方案,对于所有的语言都适用,但对于内部的服务,走域名会很低效,另外环境隔离也不太友好,比如预发、线上通常是同一个数据库,因此闸道器读取到的可能是同一个域名,这时候预发的闸道器呼叫的就是线上的服务。注册中心。采用注册中心就不会有上述的这些问题,即使是在容器环境下,节点的IP变更比较频繁,但节点列表的实时维护会由注册中心搞定,对闸道器是透明的,另外应用的正常上下线、包括异常宕机等情况,也会由注册中心的健康检查机制检测到,并实时反馈给闸道器。并且采用注册中心效能也没有额外的效能损耗,采用域名的方式,额外需要走一次DNS解析、Nginx转发等,中间多了很多跳,效能会有很大的下降,但采用注册中心,闸道器是和业务方直接点对点的通讯,不会有额外的损耗。服务呼叫
闸道器由于对接很多种不同的协议,因此可能需要实现很多种呼叫方式,比如HTTP、Dubbo等,基于效能原因,最好都采用异步的方式,而Http、Dubbo都是支援异步的,比如apache就提供了基于NIO实现的异步HTTP客户端。
因为闸道器会涉及到很多异步呼叫,比如、HTTP客户端、dubbo、redis等,因此需要考虑下异步呼叫的方式,如果基于回拨或者future的话,程式码巢状会很深,可读性很差,可以参考zuul和spring cloud gateway的方案,基于响应式进行改造。
优雅下线
优雅下线也是闸道器需要关注的一个问题,闸道器底层会涉及到很多种协议,比如HTTP、Dubbo,而HTTP又可以继续细分,比如域名、注册中心等,有些自身就支援优雅下线,比如Nginx自身是支援健康监测机制的,如果检测到某一个节点已经挂掉了,就会把这个节点摘掉,对于应用正常下线,需要结合释出系统,首先进行逻辑下线,然后对后续Nginx的健康监测请求直接返回失败(比如直接返回500),然后等待一段时间(根据Nginx配置决定),然后再将应用实际下线掉。另外对于注册中心的其实也类似,一般注册中心是只支援手动下线的,可以在逻辑下线阶段呼叫注册中心的界面将节点下线掉,而有些不支援主动下线的,需要结合快取的配置,让应用延迟下线。另外对于其他比如Dubbo等原理也是类似。
效能
闸道器作为所有流量的入口,效能是重中之重,早期大部分闸道器都是基于同步阻塞模型构建的,比如Zuul 1.x。但这种同步的模型我们都知道,每个请求/连线都会占用一个执行绪,而执行绪在JVM中是一个很重的资源,比如Tomcat预设就是200个执行绪,如果闸道器隔离没有做好的话,当发生网络延迟、FullGC、第三方服务慢等情况造成上游服务延迟时,执行绪池很容易会被打满,造成新的请求被拒绝,但这个时候其实执行绪都阻塞在IO上,系统的资源被没有得到充分的利用。另外一点,容易受网络、磁盘IO等延迟影响。需要谨慎设定超时时间,如果设定不当,且服务隔离做的不是很完善的话,闸道器很容易被一个慢界面拖垮。
而异步化的方式则完全不同,通常情况下一个CPU核启动一个执行绪即可处理所有的请求、响应。一个请求的生命周期不再固定于一个执行绪,而是会分成不同的阶段交由不同的执行绪池处理,系统的资源能够得到更充分的利用。而且因为执行绪不再被某一个连线独占,一个连线所占用的系统资源也会低得多,只是一个档案描述符加上几个器等,而在阻塞模型中,每条连线都会独占一个执行绪,而执行绪是一个非常重的资源。对于上游服务的延迟情况,也能够得到很大的缓解,因为在阻塞模型中,慢请求会独占一个执行绪资源,而异步化之后,因为单条连线所占用的资源变的非常低,系统可以同时处理大量的请求。
如果是JVM平台,Zuul 2、Spring Cloud gateway等都是不错的异步闸道器选型,另外也可以基于Netty、Spring Boot2.x的webflux、vert.x或者servlet3.1的异步支援进行自研。
快取
对于一些幂等的get请求,可以在闸道器层面根据业务方指定的快取头做一层快取,储存到Redis等二级快取中,这样一些重复的请求,可以在闸道器层直接处理,而不用打到业务线,降低业务方的压力,另外如果业务方节点挂掉,闸道器也能够返回自身的快取。
限流
限流对于每个业务元件来说,可以说都是一个必须的元件,如果限流做不好的话,当请求量突增时,很容易导致业务方的服务挂掉,比如双11、双12等大促时,界面的请求量是平时的数倍,如果没有评估好容量,又没有做限流的话,很容易服务整个不可用,因此需要根据业务方界面的处理能力,做好限流策略,相信大家都见过淘宝、百度抢红包时的降级页面。
因此一定要在接入层做好限流策略,对于非核心界面可以直接将降级掉,保障核心服务的可用性,对于核心界面,需要根据压测时得到的界面容量,制定对应的限流策略。限流又分为几种:
单机。单机效能比较高,不涉及远端呼叫,只是本地计数,对界面RT影响最小。但需要考虑下限流数的设定,比如是针对单台网关、还是整个闸道器丛集,如果是整个丛集的话,需要考虑到闸道器缩容、扩容时修改对应的限流数。分散式。分散式的就需要一个储存节点维护当前界面的呼叫数,比如redis、sentinel等,这种方式由于涉及到远端呼叫,会有些效能损耗,另外也需要考虑到储存挂掉的问题,比如redis如果挂掉,闸道器需要考虑降级方案,是降级到本地限流,还是直接将限流功能本身降级掉。另外还有不同的策略:简单计数、令牌桶等,大部分场景下其实简单计数已经够用了,但如果需要支援突发流量等场景时,可以采用令牌桶等方案。还需要考虑根据什么限流,比如是IP、界面、使用者维度、还是请求引数中的某些值,这里可以采用表示式,相对比较灵活。稳定性
稳定性是闸道器非常重要的一环,监控、告警需要做的很完善才可以,比如界面呼叫量、响应时间、异常、错误码、成功率等相关的监控告警,还有执行绪池相关的一些,比如活跃执行绪数、伫列积压等,还有些系统层面的,比如CPU、内存、FullGC这些基本的。
闸道器是所有服务的入口,对于闸道器的稳定性的要求相对于其他服务会更高,最好能够一直稳定的执行,尽量少重启,但当新增功能、或者加日志排查问题时,不可避免的需要重新发布,因此可以参考zuul的方式,将所有的核心功能都基于不同的实现,的程式码采用Groovy编写,储存到数据库中,支援动态载入、编译、执行,这样在出了问题的时候能够第一时间定位并解决,并且如果闸道器需要开发新功能,只需要增加新的,并动态新增到闸道器即可,不需要重新发布。
熔断降级
熔断机制也是非常重要的一项。若某一个服务挂掉、界面响应严重超时等发生,则可能整个闸道器都被一个界面拖垮,因此需要增加熔断降级,当发生特定异常的时候,对界面降级由闸道器直接返回,可以基于Hystrix或者Resilience4j实现。
日志
由于所有的请求都是由闸道器处理的,因此日志也需要相对比较完善,比如界面的耗时、请求方式、请求IP、请求引数、响应引数(注意脱敏)等,另外由于可能涉及到很多微服务,因此需要提供一个统一的traceId方便关联所有的日志,可以将这个traceId置于响应头中,方便排查问题。
隔离
比如执行绪池、http连线池、redis等应用层面的隔离,另外也可以根据业务场景,将核心业务部署带单独的闸道器丛集,与其他非核心业务隔离开。
闸道器管控平台
这块也是非常重要的一环,需要考虑好整个流程的使用者体验,比如接入到闸道器的这个流程,能不能尽量简化、智慧,比如如果是dubbo界面,我们可以通过到git仓库中获取源代码、解析对应的类、方法,从而实现自动填充,尽量帮使用者减少操作;另外界面一般是从测试->预发->线上,如果每次都要填写一遍表单会非常麻烦,我们能不能自动把这个事情做掉,另外如果闸道器部署到了多个可用区、甚至不同的国家,那这个时候,我们还需要界面资料同步功能,不然使用者需要到每个后台都操作一遍,非常麻烦。
这块个人的建议是直接参考阿里云、aws等提供的闸道器服务即可,功能非常全面。
其他
其他还有些需要考虑到的点,比如界面mock,文件生成、sdk程式码生成、错误码统一、服务治理相关的等,这里就不累述了。
总结
目前的闸道器还是中心化的架构,所有的请求都需要走一次闸道器,因此当大促或者流量突增时,闸道器可能会成为效能的瓶颈,而且当闸道器接入的大量界面的时候,做好流量评估也不是一项容易的工作,每次大促前都需要跟业务方一起针对界面做压测,评估出大致的容量,并对闸道器进行扩容,而且闸道器是所有流量的入口,所有的请求都是由闸道器处理,要想准确的评估出容量很复杂。可以参考目前比较流行的ServiceMesh,采用去中心化的方案,将闸道器的逻辑下沉到sidecar中,sidecar和应用部署到同一个节点,并接管应用流入、流出的流量,这样大促时,只需要对相关的业务压测,并针对性扩容即可,另外升级也会更平滑,中心化的闸道器,即使灰度释出,但是理论上所有业务方的流量都会流入到新版本的闸道器,如果出了问题,会影响到所有的业务,但这种去中心化的方式,可以先针对非核心业务升级,观察一段时间没问题后,再全量推上线。另外ServiceMesh的方案,对于多语言支援也更友好。
来自:https://github.com/aCoder2013/blog/issues/35