网关系统
# 网关系统
项目核心模块分为三块内容:
- Center注册中心
- Assist-Engine-Core网关算力
- Provider-SDK服务提供方
# 网关算力Core
整个网关流程(启动服务器、监听连接、请求处理)如下:
- 创建线程任务:在调用的线程任务当中,启动Netty服务器
- 服务器请求Handler:根据 uri 从 map 获取对应 httpstatement 映射规则,并存入通道会话
- 鉴权Handler:请求头拿到 uid 和 token 进行鉴权
- 方法调用Handler:解析请求参数,根据 uri 拿到泛化调用,执行、封装、返回调用结果
- ①获取数据源,建立连接。默认实现以下两种数据源:
- HTTP:由网关向另一个url发送HTTP请求,调用接口
- Dubbo:根据方法名获取对应的泛化调用实例,执行RPC方法
- ②连接对象根据配置信息,包括应用配置+配置中心配置+服务引用配置,获取泛化调用实例
- ③执行器根据映射规则,封装被调用方法名+参数类型+参数,并通过泛化实例执行。
- ①获取数据源,建立连接。默认实现以下两种数据源:
关键配置对象:
Configuration
Netty服务端参数,包括IP端口、boss线程、worker线程。
三个Dubbo重实例对象,并配置有缓存Map,key为对应应用ID和接口ID。
网关映射HttpStatement缓存集合,key设置为uri。
泛化调用IGenericReference实例对象缓存,key设置为uri。
HttpStatement
- 应用ID和接口ID
- 方法名、入参类型、鉴权、uri
# Center注册中心
数据库表设计:
- 方法信息表;应用ID+接口ID+方法信息(uri,请求方式,参数类型)
- 接口信息表:应用ID+接口名称+接口版本
- 应用信息表:应用ID+注册中心
- 网关服务组表:网关服务ID+地址,对应一个Core实例
- 网关分配表:网关服务ID <= 绑定 = > 应用ID,一个applicationID下所有接口和方法都交给一个Core核心进行处理
docker挂载和刷新Nginx服务:
- 在Center程序运行的容器内部,重写一个conf配置文件,该文件地址挂载在容器外部(阿里云服务器)Nginx配置文件的地址。因此只要容器内部的文件发生变动,那么外部Nginx配置文件也会同步更新。
- Nginx刷新reload使配置生效。通过docker-java包,可以在当前容器调用其它容器的服务。具体来说,可以通过容器名拿到该容器的ID,从而exec进入容器内部,执行reload -s指令。
注册节点流程:
- 插表:向网关算力表插入记录
- 查表:查出所有运行的算力节点,组成代理上游服务器
- 更新刷新Nginx配置
Redis事件通知机制:
- 通道名称为网关地址ID,发送的消息内容为应用名称ID。
- 每次应用-接口-方法注册成功后,通过redis向通道发送事件。
# Assist
ContextClosedEvent上下文关闭扩展点:
- 容器关闭时,需要将core的Netty服务也一并关闭
ApplicationContextAware上下文扩展点:
- 首先根据yml配置的网关信息,向“注册中心”注册网关节点。
- 查出该网关ID下的所有的接口和方法,加入configuration中的缓存:
- Dubbo三大变量:applicaiton+registry+reference
- 方法映射HttpStatement,key为对应请求的uri
通过spring.factories文件,指定GatewayAutoConfig作为外部Bean导入容器管理,通过注解@Configuration与@Bean创建更多Bean对象,其中对于方法@Bean:
- 方法形参会从Bean容器中找
- Bean注解的方法返回的对象会作为Bean纳入容器管理
通过Bean方法自动创建的Bean对象如下:
- Configuration:全局配置信息对象
- Channel:初始化Netty服务器通道,监听网络请求
- GatewayApplication:spring拓展点的对象,用于初始化configuration对象
# SDK
核心流程如下:
- 通过BeanPost增强方法,获取每个带有自定义注解的接口和方法,并插入数据库。
- 向Center发送Redis事件
# Q&A
# 问题一
Netty启动服务端绑定监听的IP地址时,试了两个IP地址都启动失败:
- 服务器公网IP:直接在服务器上运行正常,但是通过docker运行失败
- 127.0.0.1:可以正常启动,但是外部无法通过公网IP进行网关通信
经排查了解到,Netty的bind只能绑定本机能感知到的网卡IP。通过docker exec进入容器内部进行ifconfig,发现确实无法感知到外部的公网IP,只能查到虚拟网卡以及127.0.0.1这两个地址。
而对于127.0.0.1外网不能访问,是因为本地环回地址只能在本地访问,只能在容器内通过127.0.0.1进行访问,而在外部其它机器上通过该地址请求不能打到该机器上。
最终解决方案是绑定地址是0.0.0.0,相当于监听该机器上的所有网卡,无论是docker的虚拟网卡还是机器上的网卡,外部发送的数据包都可以监听到。
# 问题二
如何解决网关启动后,后启动的RPC服务加入到Assist网关的缓存?Redis的事件发布通知机制如何进行设计?
- Assist首先向center注册中心拉取Redis配置,创建Jedis客户端监听通道消息;创建监听器适配器,指定消息处理函数;将Topic通道与消息处理函数进行绑定。
- Center中心提供发送消息的接口
整体流程如下:
- Provider:启动时,通过spring拓展点BeanPostProcessor,获取每个注解的Bean对象方法名,将当前服务的“应用、接口、方法”向注册中心注册,最后调用注册中心的Redis事件发送。
- Assist:Jedis客户端接收到事件后,获取应用ID,重新拉取该应用ID的所有接口和方法加入缓存。
所有向Center注册中心发送的接口请求,都是通过Hutool的HttpUtil工具发送请求。
# 面试问题
# 1、网关数据库表的QPS问题?性能瓶颈在哪儿?
网关的数据库表主要功能是进行服务注册,以及接口分配,因此一般来说并发量并不会很大,除非真的同时有很多个接口进行注册,那么可以采用分库分表,或者进行分布式部署。
性能瓶颈:网关包含多条链路,比如服务拉取,服务注册,用户请求。其中性能瓶颈主要在这一条链路:用户请求->协议转换->接口获取->泛化调用。包括几个方面:
- 网络IO:大量请求堆积,超时会导致对应的网络性能问题。可以通过修改Netty网络模型,零拷贝技术提高系统IO性能。
- Dubbo服务性能:Dubbo服务性能也会影响到API网关整体的性能。提高每个RPC接口的服务性能方法,比如Dubbo的异步调用、负载均衡等
- 线程池:网关需要处理大量的请求,因此如何设置好线程池参数是关键。也就是Netty的EventLoop线程对象,主要分为两大类,分别是boss和worker线程,每个线程都会维护多个任务队列。
- boss线程主要处理连接accept事件,一般bind几个IP端口则设置几个boss线程。boss线程会将连接的客户端注册到worker的注册队列中。
- worker线程主要负责处理用户请求。每个worker线程会不断select查看是否有客户端读写事件,如果没有则不会运行。因此worker线程并不是一直工作的,为了提高CPU的使用效率,一般设置为CPU核心数*2
- 每个客户端的所有事件都会交由同一个worker进行处理,worker会串行执行所有handler,从而避免线程上下文切换。
- 内存使用:Netty线程池需要维护多个客户端的队列,因此使用不当可能出现内存溢出、内存泄漏等问题。可以采用减少对象创建等方法进行优化,同时对于网关而言,朝生夕灭的对象比较多,因此JVM的初始新生代需要设置大一些,让大部分对象都直接在新生代存储和消亡,不需要进入老年代。
- Handler线程池分离:线程池默认使用NIO的线程池。如果自定义的Handler业务处理开销过大,那么可以自定义一个线程池,为Handler指定特定的线程池进行处理,防止业务任务与IO任务同时抢占资源。
# 2、Netty如何实现断线重连?
一般是在worker添加一个定时任务,重新执行客户端连接的逻辑即可。
该网关中,客户端并不需要处理数据,因此无需编写客户端代码,客户端仅通过网络连接与服务端进行通信。
# 3、网关的高可用可以做哪些处理?
方案如下:
- 异地多机房拓展部署多个网关算力实例,本质上只是负责协议转发,失败了需要配置超时重试。
- 负载均衡:LVS配置前端负载均衡+ F5硬件负载均衡+ Nginx
# 4、注册中心负责什么部分?
注册中心主要负责服务注册、服务拉取、算力分配等功能,提供给网关操作数据库的功能。而其它网关指的是用于RPC服务的注册中心。
# 5、该网关与SpringCloud网关有什么区别?
网关这个名词实际上叫的比较大。个人理解的话,SpringCloud主要是侧重于不同微服务之间的HTTP通信问题,包括超时、限流、负载。
而API网关主要是侧重于不同RPC的调用管理,以及协议转换。
# 6、系统想要使用该网关,需要做哪些步骤?
如果需要对RPC接口到HTTP协议进行转换,统一管理,可以使用这套网关。本质上是通过编程式控制,向不同RPC接口以及HTTP接口发送请求。看是哪套RPC接口,如果是Dubbo正好本系统进行了开发,将外部的HTTP请求通过网关,打到Dubbo接口,将调用结果通过HTTP协议返回。
- Dubbo协议:将RPC接口导入SDK,使用相应的注解对接口和方法进行标注,然后启动程序暴露接口,并进行服务注册。另外需要统一外部的HTTP请求路径与对应的RPC接口。
- HTTP协议:实现一个http到http的协议转换,Spring服务端接口不需要修改。只需注意对应的映射规则。
# 7、网关为什么需要自研?和市面上的区别在哪儿?
市面上有一些网关,个别大厂也有在自研,经过调研主要是一下几个核心问题:
- 兼容性:市面上开源的网关,系统升级时比较麻烦,可能会出现网关服务依赖包的版本不兼容问题,使用自研的可以减少维护成本。此外,使用的依赖包全程可控,可以减少安全问题。
- 拓展性:自研最大的好处,在于可以自定义扩展功能,接入处理不同内部自研的协议,同时在调用过程拓展对应的功能。
# 8、同一套网关系统怎么映射到不同的后端Dubbo服务上面?
每个HTTP请求路径,都映射一个Dubbo服务接口方法。
每个网关实例启动时,会向数据库拉取对应的映射关系,并加入缓存。
# 9、部署多套网关,如何进行区分?
网关系统配置了网关<==>Dubbo服务的映射,每个网关ID与服务的映射关系会存入数据库。一个服务下的所有接口对应由同一个网关进行协议转换和转发。
网关启动时,会拉取网关与接口的映射关系,存入映射表缓存当中。
# 10、RPC服务上报过后,协议变了,网关应该如何处理?
协议改变后,需要改动如下:
- 修改注册中心上报信息,包括RPC服务协议类型
- 网关需要拓展从HTTP协议到该协议的转换方法,保证该RPC服务能够正确被网关转发和调用
# 11、服务降级方案怎么设计
服务降级主要包括:限流、熔断、降级,可以通过插件实现。在RPC服务启动注册的时候,同时启动服务治理配置,配置相应的操作,比如降级之后直接返回错误码。