Api-gateway-assist
# Api-gateway-assist
# 1.服务发现gateway-assist
# 网关算力服务注册发现
核心:通过设计SpringBoot Starter,让gateway-engine引擎启动SpringBoot程序时,自动读取core算力节点配置信息(yml),并根据该信息发送HTTP请求,将gateway-core注册到网关注册中心。

GatewayServiceProperties:通过注解@ConfigurationProperties实现对yml配置读取。
RegisterGatewayService#doRegister:根据yml读取的算力节点信息,通过HttpUtil.post向注册中心的网关注册接口发送HTTP请求。
GatewayApplication:实现ApplicationListener,利用消息监听发布机制,当refresh结束后触发doRegister流程。
# @Configuration
①在@Configuration注解配置类中,通过@Bean注解在返回new实例的方法上来实现注册自定义bean。
②在@Configuration注解配置类中,通过@ComponentScan指定自定义bean所在的包,实现注册自定义bean。
- 原理
AnnotationConfigApplicationContext类中会初始化ConfigurationClassPostProcessor,他会在refresh中invokeBeanFactoryPostProcessors执行。
ConfigurationClassPostProcessor解析过程中,首先检查BeanDefinition是否@Configuration注解标记,然后再扫描并构建新的BeanDefinition,初始化容器。
# SPI机制
- SPI概念
全称Service Provider Interface,服务提供者接口。调用方可以自定义实现服务提供方的服务接口,并替换其默认实现。也就是说通过SPI机制,我们可以自定义修改覆盖外部Jar包(服务提供方)里的接口实现。
而API(Application Provider Interface)的使用完全依赖于所提供的Jar包。
- JDK原生SPI
java.util.ServiceLoader:JDK原生SPI的核心类,可以通过类名获取在"META-INF/services/"下的多个配置实现文件。
缺点:无法确认具体加载哪一个实现,仅靠ClassPath的顺序决定。同时不能按需加载,需要遍历所有内容并实例化,耗时。
- Dubbo SPI
核心在于支持按”名“读取SPI服务实现类。
在服务接口添加@SPI注解(可以指定默认实现)。在 META-INF/dubbo路径下支持别名配置(键值对配置,key为别名,value为实现类名),从而解决了SPI服务具体加载的实现类。
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
2
内部框架获取服务通过ExtensionLoader实现(Dubbo内部也继承了轻量级的AOP和IOC):
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<Robot> extensionLoader =
ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
}
}
2
3
4
5
6
7
8
9
10
11
- Spring SPI
JDK和Dubbo的SPI机制每个扩展点单独一个文件,而对于Spring框架来说,所有扩展点都存放在META-INF/spring.factories一个文件当中,指定全限定名的接口+自定义实现类。
org.springframework.boot.context.config.ConfigDataLocationResolver=\
com.example.LocationResolver
2
核心是通过SpringFactoriesLoader获取对应的服务。

# 2.重构:Netty服务端和配置类缓存
- Netty通信服务和配置类的初始化
核心:将整个网关的Netty服务启动和配置初始化全部交给gateway-assist自动配置类实现。
①在GatewayApplication监听器中引入Configuration(交给Spring管理),在拉取RPC注册信息后,根据聚合信息实现配置缓存初始化。
②在GatewayAutoConfig启动配置类中引入initGateway方法,根据网关IP端口配置进行网关通信服务初始化。
GatewayAutoConfig#initGateway:把启动网关通信服务(gateway-core)交给Spring进行。在SpringBoot应用程序启动后,spring主线程在执行@Bean注解方法初始化Bean时,会从线程池中获取新的线程,异步执行Netty服务端的所有流程。
- 大体流程
①启动zookeeper和真正的服务提供方api-gateway-test-provider,暴露RPC服务
②启动网关注册中心api-gateway-center
③启动api-gateway-assist00,而因为其中内嵌了assist和core网关算力,所以整个网关助手测试工程此时可以充当一个具有自动配置(服务拉取,注册,初始化)的算力节点。(相当于assist+core的一个胖jar)
当用户访问网关监听地址时,HTTP请求会打到assist0中的Netty服务端线程,触发监听事件,并根据uri从缓存中取出对应的Dubbo配置,向test-provider请求并响应给用户。
当网关服务启动后,如果前端操作增添接口,后台拿到接口数据需要①向注册中心center(相当于数据库)的注册接口发送请求,完成数据库中接口信息的写入②获取到gateway-assist00(gateway-core)里的交给Spring管理的Configuration Bean实例(@Autowired),然后通过手动式编程将当前接口信息加入配置缓存,完成注册。

- 问题
网关算力真正与RPC服务建立远程连接,获取泛化实例过程还是在触发Netty监听事件中的openSession进行。
①注册添加配置缓存中,键值对key由name改为对应application和interface的Id
②zookeeper在虚拟机,采用host模式启动(bridge模式通信失败)只能访问虚拟机IP访问,127.0.0.1失败。
# 3.Maven三大打包插件
# maven-jar-plugin
默认的打包插件,用来打普通的project JAR包;
# maven-assembly-plugin
支持自定义的打包结构,也可以定制依赖项等。
# maven-shade-plugin
①将依赖的jar包打包到当前jar包(常规打包是不会将所依赖jar包打进来的),也就是说其他地方引用gateway-assist插件时,不需要再导入gateway-core依赖。
②对依赖的jar包进行重命名。
使用:将项目打成一个可执行jar包时,configuration下增加artifactSet,includes添加需要增加的第三方maven依赖,excludes排除不需要打包进来的第三方依赖,
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<artifactSet>
<includes>
<include>com.panhai.gateway:api-gateway-core:jar:</include>
</includes>
</artifactSet>
</configuration>
</plugin>
2
3
4
5
6
7
8
9
10
11
# 普通jar包和Fat jar
普通的jar只包含当前 jar的信息,不含有第三方 jar,因此当内部依赖第三方jar时直接运行则会报错,这时候需要将第三方jar内嵌到可执行jar里。
Fatjar:将一个jar及其依赖的三方jar全部打到一个包中。胖包到哪里都能用,而要使用瘦包必须引用的工程中自带依赖才行。
spring-boot-maven-plugin和maven-shade-plugin(指定)打的包都是胖jar。
# 4.容器关闭监听与异常管理
- 功能
①添加一个容器关闭的监听器,当容器关闭时需要把网关通信core下的Netty服务也一起关闭。
GatewayApplication监听ContextClosedEvent上下文关闭事件,引入Channel作为成员属性,用于关闭Netty服务。
②将网关的注册和RPC服务拉取这两个操作,放入到上下文接口的setApplicationContext方法,这样可以在注册服务与拉取配置失败时,直接抛异常关闭容器。
- ApplicationContextAware扩展点
实现上下文容器感知接口的对象的方法setApplicationContext,在beanPostProcessorsBeforeInitialization阶段中,通过调用ApplicationContextAwareProcessor(实现了BeanPostProcessor)的增强方法实现。
# 5.配置Dockerfile构建镜像
将打包好的Jar包传入服务器,通过构建镜像文件运行在docker上
- 步骤
①将打包好的可执行jar包与Dockerfile传入服务器
②编写Dockerfile文件,其中常用指令:
ENV:指定容器启动后,所要执行指令的运行环境,配合外部传入
FROM:指定基础镜像,必须为第一个命令
ADD:将本地文件(jar包)添加到容器中
WORKDIR:配置指定当前工作目录,后续所有指令(CMD)和操作都是把该目录作为相对路径。
EXPOSE:配置镜像暴露的服务端口,一般配合host网络模式使用。(不配置host模式启动容器,会被-p接口映射覆盖)
ENTRYPOINT / CMD:配置容器启动后,调用执行的命令。
# 基础镜像
FROM openjdk:8-jdk-alpine
# 作者
MAINTAINER "panhai"
# 时区
ENV TZ=PRC
WORKDIR /usr/local/dockerfile
# 添加应用
ADD api-gateway-engine.jar /api-gateway-engine.jar
# 执行镜像
ENTRYPOINT ["java","-jar","/api-gateway-engine.jar"]
2
3
4
5
6
7
8
9
10
11
③编译Dockerfile文件,生成Docker镜像。其中后面的点表示从当前上下文相对路径获取。
docker build -f ./Dockerfile -t api-gateway-engine:1.0.1 .
④执行镜像,并暴露对应的端口给外部访问
docker run -p 7397:7397 -p 8002:8002 --name api-gateway-engine -d api-gateway-engine:1.0.1
不适用容器与宿主机自动映射,只当宿主机网络模式,宿主机EXPOSE暴露的端口会直接使用宿主机对应的端口
docker run --network host --name api-gateway-engine -d api-gateway-engine:1.0.1
# 6.NettyServer#bind与Docker虚拟网卡
核心:在主机启动Netty服务端时,绑定IP地址设置为0.0.0.0,代表监听所有发往本地主机的请求。
# ServerBootstrap#bind
①一台机器上可能会有多张网卡,通过ifconfig查看当前机器的所有网卡配置。
②在程序中Netty服务端绑定的IP只能是其所在机器中(ifconfig所能感知到)的某个网卡的IP地址。
③Netty通过bind绑定的地址,是指服务端能够监听到**目的地IP为所绑定网卡地址的IP包**。比如你的主机有网卡A和B,程序中bind(A),那么操作系统就会把所有发往A网卡地址的IP包数据,从内核态复制到用户态,转发给程序使用。
④如果bind绑定0.0.0.0,那么Netty服务端可以监听并收到外部发给你主机上任意一张网卡的请求。
# Docker上启动Netty服务
- 环境
虚拟机(宿主机)的IP为192.168.200.200
- 问题排查与实验
①bind绑定192.168.200.200:网关引擎在宿主机直接java -jar可以正常运行,并且外部可通过192.168.200.200访问RPC服务。但是放进宿主机的docker后网关引擎启动失败。
②bind绑定127.0.0.1:网关引擎在宿主机和docker都可以正常启动,但是外部通过宿主机IP或是127.0.0.1都无法访问网关通信组件,获取RPC服务。(-p和host模式两种容器启动方法都尝试过,均访问不到)
- 结论
①实验一说明容器内部感知不到宿主机的网卡IP,进入容器内部通过ifconfig查看也验证了这个想法。docker内部只能感知到自己的虚拟网卡(eth0),因此Netty服务端不能正常启动。
docker exec -it api-gateway-engine sh

②实验二的结论就很好体现了对bind的理解,docker内部bind监听环回地址相当于禁止外部访问,除非请求也是在容器内部进行或者配置响应的host映射,否则Netty都监听不到外部的请求。
③综上,此处给出的方案是算力节点的启动Netty服务时,监听的IP地址设置为0.0.0.0。
④虚拟机内部gateway-assist向外部windows环境下的gateway-center拉取服务时,ip不能够为localhost。宿主机与外部环境进行通信时需要访问外部ip,也就是注册中心的IP需要改为虚拟机IP。
- 关于Docker与Vmware
虚拟机端口转发:访问本机端口时,配置所要转发给虚拟机的IP端口。从而实现外网(访问本体某个端口)访问内网虚拟机。
-it:docker run的参数,表示交互式运行,配合/bin/bash进行命令行输入
容器之间进行访问通过docker0网球进行:docker 查看虚拟网卡 (opens new window)
虚拟机三种连接方式:虚拟机三种网络连接方式 (opens new window)
# 7.Redis发布订阅实现算力自动注册RPC
- 功能实现
目前整个网关系统启动需要遵循以下顺序:①注册中心②RPC应用提供③网关算力引擎。也就是说后续有新的应用接口暴露服务后,不会被算力引擎存入缓存,也就使用不了整个网关调用服务。解决的关键点是engine如何感知到每个provider提供的服务。
长轮询、长链接方案:①网关引擎在Spring生命周期初始化中,另起一个线程,在后台while(true)不断重复拉取所有服务,并存入缓存。显然不论是对于网关引擎还是注册中心都消耗占用不少资源。 ②网关引擎再启动一个Netty服务端(监听端口不能和已有网关通信服务监听端口相同),注册中心启动一个Netty客户端和engine建立连接,每次有新的服务接口注册进来后,通过管道writeAndFlush(systemId)通知服务端,拉取注册新的接口。但维持这样的长链接也会占用不少资源。
最佳实践:使用事件发布订阅机制异步进行,比如redis、MQ,这里网关引擎与注册中心采用redis的发布订阅模式进行通信。新的应用接口启动注册后,触发注册中心的事件发布机制,gateway-center向网关引擎推送新注册接口的信息,gateway-assist收到后进行更新。
此处每启动一个新的provider注册进数据库时,都是以systemId为单位,assist注册时需要保存应用下的所有接口+所有方法。因此事件推送时仅需要传递systemId即可。而后续如果细化到只注册某个方法时,addmapper也需要细化拆分注册不同的模块。

- 细节
GatewayApplication#addMappers:因为向Configuration注册需要复用,所以可以抽离注册模块成一个方法。
queryApplicationSystemRichInfo(String gatewayId, String systemId):此处在方法复用上的设计十分巧妙。因为第一次注册时需要拉取网关下注册的所有应用接口,而第二次仅需要拉取指定应用下的所有接口方法。因此传参时systemId为空,多加一次网关应用分配信息的查询;而第二次注册直接指定变化的systemId。
Center-RpcRegisterManage#registerEvent:消息事件发布,由sdk触发调用。
Assist-GatewayApplication#receiveMessage:指定的消息监听方处理器方法,入参为推送的消息内容。
- redis消息订阅发布
①redis事件发布端:
redistemplate:通过配置方式注入Bean,设置默认序列化器fastjsonredisserializer,入参自动注入RedisConnectionFactory根据yml配置的redis端口建立链接。
RedisTemplate#convertAndSend:发布消息,指明接收方Topic通信信道,和消息内容。
②redis监听器:
RedisConnectionFactory:负责设置连接参数,redis服务地址。
注入连接工厂Bean并修改配置,从注册中心拉取redis的端口IP信息(properties),创建Jedis客户端连接(不需要在assist重新配置redis服务地址)。
RedisMessageListenerContainer:注入消息监听器容器,需要设置连接工厂和监听器适配器。并将消息通信Topic与监听器适配器绑定。
MessageListenerAdapter:指明消息处理委托对象,以及消息处理方法(最终发布方的消息会被该方法接收)。