网关设计与实现

300 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

网关解决什么问题?

作为微服务网关,以下几个场景迫切需要网关

  1. 请求每个服务都需要认证,需要将认证的逻辑放到网关
  2. 每个应用都是集群部署,如何统一做负载均衡
  3. 灰度路由,to B产品需要根据企业维度灰度

网关的测试用例

  1. 路由转发
  2. 自定义灰度
  3. 针对微服务限流

网关包含了哪些子系统及其作用

  1. 协议层:作为http协议的网关,肯定有http request、http response
  2. hook层:对外提供hook能够自定义开发
  3. 路由层,提供基本的url路由hook、限流hook

网关基本实现原理

协议解析

目前仅实现一个http网关即可,所以我们也只需要解析http的报文协议,可以有以下的方案。

方案一

借助于servlet容器解析http协议,我们只需要聚焦httpServletRequest、httpServletResponse的处理。

优点:不需要处理复杂的报文解析

缺点:性能可能不高,比如tomcat是BIO的线程模型

方案二

自己实现http报文解析

优点:可用自己采用NIO实现,性能更加强大

缺点:协议解析复杂一些

方案对比

图简单可以直接用方案一,比如zuul1,但是演进方向肯定是方案二,比如zuul2。

Handler的api设计

对于这种socket一问一答的需求,责任链必须要安排上。那么Handler的API应该如何设计呢?基于我的一些经验,hook的使用需要能够满足以下几点要求:

  1. 链式执行,这个是基本要求,实现肯定会想到列表循环,但有时你不需要执行后续的hook,所以类似netty的fHandlere机制对自定义更加友好 2.Handler的顺序,这个可以定义个接口
  2. 我可以用Handler在路由之前处理逻辑,比如http header增加一个kv
  3. 我可以用Handler在路由之后处理逻辑,比如拿到路由之后的http response header做一下操作
  4. Handler可以根据request或者response判断是否执行

如果有了解过netty,它的ChannelPipeline非常能够满足我们的要求。

image.png

Handler的生命周期

handler基本不会占用系统资源,所以生命周期中不需要close阶段。

image.png

测试用例功能的实现

上面说了一些整体的设计,下面开始围绕需求进行设计。

路由转发

转发路由之前需要定义路由规则,还有个注意点是需要从注册中心获取微服务的IP地址,然后通过负载均衡策略选择一个IP,建立三次握手直接调用。

image.png

当然每次的三次握手也是十分耗时,你可能会想到使用连接池,没错是个好办法。假设微服务有10个,每个微服务有3个pod,对于每个节点假设连接池缓存5个socket 连接不关闭。此时常态下一个网关pod需要建立。链接数量=10 * 3 * 5=150,emmmm,好像也不是很多,但我感觉如果日后集群扩大,这里会成为日后演进的一个瓶颈。

还有个注意点是需要从注册中心获取微服务的IP列表,这个肯定也是需要缓存到本地,对应的也是需要注册中心提供一个notify的机制,并且也是由注册中心提供服务探活,具体可以参考注册中心设计与实现。

负载均衡策略也是容易被忽略的点,负载均衡主要是解决服务负载不均的问题。轮训、权重、一致性哈希。理论原理你可能深入于心,但是优秀的代码可能需要参考参考(cv大法),遇到面试更是得背诵一番,不同的负载均衡,难点在于如何写出高性能线程安全的代码。

软件架构 4+1 视图

逻辑视图

逻辑视图需要说明整个网关的分层设计。根据实现原理,整个网关分为:

  • 协议层:用于将byte流解析为HTTP请求,将HTTP响应转为byte流发送,具体可以通过Netty实现
  • handler层,这一层分为InboundHandler与OuboundHandler,并且需要管理Handler的整个生命周期
  • 基本的Handler组件:比如路由转发Handler,限流Handler

image.png

过程视图

过程视图中我们通过时序图描述关键测试用例的步骤。在最前面提到过,我们目前网关的测试用例只有3个:

  1. 路由转发
  2. 自定义灰度
  3. 针对微服务限流

对于2、3需求比较简单,就不做时序图描述了,仅描述路由转发时序图。

image.png

物理视图

网关的部署通常比较简单,网关的前面可能是一个硬件负载均衡(F5)或者软件负载均衡(Nginx),部署时需要配置对应的路由策略。

另外一个在部署时需要注意的就是高可用和灰度怎么做。

高可用

什么?你说不用做高可用? 没有高可用的服务你敢上线?

网关的高可用还是比较简单,毕竟是一个无状态的服务。而无状态的服务高可用通常套路都一样:

  1. 冗余部署:比如主备部署,部署一个Master网关,再部署一个Slave
  2. 异常检测:监控Master是否正常,主要是服务是否正常
  3. 节点切换:比如Master节点网络不好或者节点挂了,需要将对Master节点的请求,转为对Slave机器的请求

这个三板斧可以解决任何无状态的服务高可用,高可用具体可以看看我的其它文章。 对于网关来说,我提一个主备方案。

方案一:脚本检测

  1. 冗余部署:网关服务搞两台服务器A,B,均部署网关服务,Nginx只路由到服务器A
  2. 异常检测:自己写一个shell脚本部署到Nginx所在服务器,定时执行,ping服务器A
  3. 如果ping不通,则立即修改Nginx的配置,将对服务器A的路由IP切换到服务器B的IP,然后重新加载Nginx配置文件

优点:很简单,完全不需要再引入其它软件,实施起来很快就能上线。 缺点:只能应对 Nginx -> 服务器A网络异常,或者服务器A挂掉的情况。并且最大故障时间=定时任务执行间隔 + nginx reload时间

灰度

什么?你说不用做灰度? 没有灰度的服务你敢上线?你敢保证的代码没有bug?我相信世界上的任何一个coder都不能保证,毕竟只要是人,都会犯错。

网关的灰度发布基本是根据来源IP进行灰度,比如仅允许公司办公网的IP可以路由到灰度机器。具体实现也是依赖于Nginx或者硬件的负载均衡。

开发视图

在开发视图中,我们通过类图描述具体类依赖关系与结构,协议层负责编解码http消息,HandlerManager具体执行Handler,他们两个都依赖HttReqeust、HttResponse,这俩类可以直接使用Netty的http模块提供的类。但如果要支持不同的网络通信框架,这俩类也得自己封装一下。 image.png

场景(测试用例)

场景就是测试用例,我们当前的一切设计都是为了实现测试用例,一定不要偏移主题,如果最终写出的代码连测试用例都无法完成,做方案设计的人一定要背这个锅。

测试用例:

  1. 协议层:作为http协议的网关,肯定有http request、http response
  2. hook层:对外提供hook能够自定义开发
  3. 路由层,提供基本的url路由hook、限流hook

测试

到这一步,设计阶段基本完成了,下一步不是考虑安排人手开发,而是考虑开发后、上线后,如何验证网关的正确性(能否正确路由)、高性能(支持的最大并发量多少)、高可用(有故障还能正常工作?),当然笼统的说网关也不对,因为正常情况下没有资源对网关的每个用例都做验证,需要针对具体的用例来验证对应的,正确性、高性能、高可用。

对于网关来说,路由转发是核心功能,需要全面验证。正确性验证就是配置下路由,看tomcat.log能不能收到转发的请求,高性能就是找个路由转发的接口,拿jmeter跑一跑,等cpu达到70%算是并发峰值。高可用就是把网关给挂掉(比如缩容道0个),手动请求下是否在tomcat.log还是能收到请求日志。

可观测性

当用了一段时间后,看某个服务的请求量多少也是尤为的重要,所以可观测性的建设是一个重要但不紧急的事情,不必在开发阶段思考。这个时候需要列出监控的指标,比如某个服务的请求量,http status=5xx的数量等等

总结

到这个里网关的设计基本就结束了,你可以看到每个点都需要考虑。