vivo亿级微服务 API 网关架构实践
扫描二维码
随时随地手机看文章
网关作为微服务生态中的重要一环,由于历史原因,中间件团队没有统一的微服务API网关,为此准备技术预研打造一个功能齐全、可用性高的业务网关。
二、技术选型
常见的开源网关按照语言分类有如下几类:
- Nginx Lua:OpenResty、Kong 等;
- Java:Zuul1/Zuul2、Spring Cloud Gateway、gravitee-gateway、Dromara Soul 等;
- Go:janus、GoKu API Gateway 等;
- Node.js:Express Gateway、MicroGateway 等。
由于团队内成员基本上为Java技术栈,因此并不打算深入研究非Java语言的网关。接下来我们主要调研了Zuul1、Zuul2、Spring Cloud Gateway、Dromara Soul。
业界主流的网关基本上可以分为下面三种:
- Servlet 线程池
- NIO(Tomcat / Jetty) Servlet 3.0 异步
- NettyServer NettyClient
在进行技术选型的时候,主要考虑功能丰富度、性能、稳定性。在反复对比之后,决定选择基于Netty框架进行网关开发;但是考虑到时间的紧迫性,最终选择为针对 Zuul2 进行定制化开发,在 Zuul2 的代码骨架之上去完善网关的整个体系。
三、Zuul2 介绍
接下来我们简要介绍一下 Zuul2 关键知识点。
Zuul2 的架构图:
为了解释上面这张图,接下来会分别介绍几个点
- 如何解析 HTTP 协议
- Zuul2 的数据流转
- 两个责任链:Netty ChannelPipeline责任链 Filter责任链
3.1 如何解析 HTTP 协议
学习Zuul2需要一定的铺垫知识,比如:Google Guice、RxJava、Netflix archaius等,但是更关键的应该是:如何解析HTTP协议,会影响到后续Filter责任链的原理解析,为此先分析这个关键点。
首先我们介绍官方文档中的一段话:By default Zuul doesn't buffer body content, meaning it streams the received headers to the origin before the body has been received.This streaming behavior is very efficient and desirable, as long as your filter logic depends on header data.
翻译成中文:默认情况下Zuul2并不会缓存请求体,也就意味着它可能会先发送接收到的请求Headers到后端服务,之后接收到请求体再继续发送到后端服务,发送请求体的时候,也不是组装为一个完整数据之后才发,而是接收到一部分,就转发一部分。
这个流式行为是高效的,只要Filter过滤的时候只依赖Headers的数据进行逻辑处理,而不需要解析RequestBody。
上面这段话映射到Netty Handler中,则意味着Zuul2并没有使用HttpObjectAggregator。
我们先看一下常规的Netty Server处理HTTP协议的样例:
NettyServer样例
@Slf4j
public class ConfigServerBootstrap {
public static final int WORKER_THREAD_COUNT = Runtime.getRuntime().availableProcessors();
public void start(){
int port = 8080;
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(WORKER_THREAD_COUNT);
final BizServerHandler bizServerHandler = new BizServerHandler();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(10, 10, 0));
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(500 * 1024 * 1024));
pipeline.addLast(bizServerHandler);
}
});
log.info("start netty server, port:{}", port);
serverBootstrap.bind(port).sync();
} catch (InterruptedException e) {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
log.error(String.format("start netty server error, port:%s", port), e);
}
}
}
这个例子中的两个关键类为:HttpServerCodec、HttpObjectAggregator。
HttpServerCodec是HttpRequestDecoder、HttpResponseEncoder的组合器。
- HttpRequestDecoder职责:将输入的ByteBuf解析成HttpRequest、HttpContent对象。
- HttpResponseEncoder职责:将HttpResponse、HttpContent对象转换为ByteBuf,进行网络二进制流的输出。
HttpObjectAggregator的作用:组装HttpMessage、HttpContent为一个完整的FullHttpRequest或者FullHttpResponse。
当你不想关心chunked分块传输的时候,使用HttpObjectAggregator是非常有用的。
HTTP协议通常使用Content-Length来标识body的长度,在服务器端,需要先申请对应长度的buffer,然后再赋值。如果需要一边生产数据一边发送数据,就需要使用"Transfer-Encoding: chunked" 来代替Content-Length,也就是对数据进行分块传输。
接下来我们看一下Zuul2为了解析HTTP协议做了哪些处理。
Zuul的源码:https://github.com/Netflix/zuul,基于v2.1.5。
// com.netflix.zuul.netty.server.BaseZuulChannelInitializer#addHttp1Handlers
protected void addHttp1Handlers(ChannelPipeline pipeline) {
pipeline.addLast(HTTP_CODEC_HANDLER_NAME, createHttpServerCodec());
pipeline.addLast(new Http1ConnectionCloseHandler(connCloseDelay));
pipeline.addLast("conn_expiry_handler",
new Http1ConnectionExpiryHandler(maxRequestsPerConnection, maxRequestsPerConnectionInBrownout, connectionExpiry));
}
// com.netflix.zuul.netty.server.BaseZuulChannelInitializer#createHttpServerCodec
protected HttpServerCodec createHttpServerCodec() {
return new HttpServerCodec(
MAX_INITIAL_LINE_LENGTH.get(),
MAX_HEADER_SIZE.get(),
MAX_CHUNK_SIZE.get(),
false
);
}
通过对比上面的样例发现,Zuul2并没有添加HttpObjectAggregator,也就是需要自行去处理chunked分块传输问题、自行组装请求体数据。
为了解决上面说的chunked分块传输问题,Zuul2通过判断是否LastHttpContent,来判断是否接收完成。
3.2 Zuul2 数据流转
如上图所示,Netty自带的HttpServerCodec会将网络二进制流转换为Netty的HttpRequest对象,再通过ClientRequestReceiver编解码器将HttpRequest转换为Zuul的请求对象HttpRequestMessageImpl;
请求体RequestBody在Netty自带的HttpServerCodec中被映射为HttpContent对象,ClientRequestReceiver编解码器依次接收HttpContent对象。
完成了上述数据的转换之后,就流转到了最重要的编解码ZuulFilterChainHandler,里面会执行Filter链,也会发起网络请求到真正的后端服务,这一切都是在ZuulFilterChainHandler中完成的。
得到了后端服务的响应结果之后,也经过了Outbound Filter的过滤,接下来就是通过ClientResponseWriter把Zuul自定义的响应对象HttpResponseMessageImpl转换为Netty的HttpResponse对象,然后通过HttpServerCodec转换为ByteBuf对象,发送网络二进制流,完成响应结果的输出。
这里需要特别说明的是:由于Zuul2默认不组装一个完整的请求对象/响应对象,所以Zuul2是分别针对请求头 请求Headers、请求体进行Filter过滤拦截的,也就是说对于请求,会走两遍前置Filter链,对于响应结果,也是会走两遍后置Filter链拦截。
3.3 两个责任链
3.3.1 Netty ChannelPipeline责任链
Netty的ChannelPipeline设计,通过往ChannelPipeline中动态增减Handler进行定制扩展。
接下来看一下Zuul2 Netty Server中的pipeline有哪些Handler?
接着继续看一下Zuul2 Netty Client的Handler有哪些?
本文不针对具体的Handler进行详细解释,主要是给大家一个整体的视图。
3.3.2 Filter责任链
请求发送到Netty Server中,先进行Inbound Filters的拦截处理,接着会调用Endpoint Filter,这里默认为ProxyEndPoint(里面封装了Netty Client),发送请求到真实后端服务,获取到响应结果之后,再执行Outbound Filters,最终返回响应结果。
三种类型的Filter之间是通过nextStage属性来衔接的。
Zuul2存在一个定时任务线程GroovyFilterFileManagerPoller,定期扫描特定的目录,通过比对文件的更新时间戳,来判断是否发生变化,如果有变化,则重新编译并放入到内存中。
通过定位任务实现了Filter的动态加载。
四、功能介绍
上面介绍了Zuul2的部分知识点,接下来介绍网关的整体功能。
4.1 服务注册发现
网关承担了请求转发的功能,需要一定的方法用于动态发现后端服务的机器列表。
这里提供两种方式进行服务的注册发现:
集成网关SDK
- 网关SDK会在服务启动之后,监听ContextRefreshedEvent事件,主动操作zk登记信息到zookeeper注册中心,这样网关服务、网关管理后台就可以订阅节点信息。
- 网关SDK添加了ShutdownHook,在服务下线的时候,会删除登记在zk的节点信息,用于通知网关服务、网关管理后台,节点已下线。
手工配置服务的机器节点信息
- 在网关管理后台,手工添加、删除机器节点。
- 在网关管理后台,手工设置节点上线、节点下线操。
为了防止zookeeper故障,网关管理后台已提供HTTP接口用于注册、取消注册作为兜底措施。
4.2 动态路由
动态路由分为:机房就近路由、灰度路由(类似于Dubbo的标签路由功能)。
- 机房就近路由:请求最好是不要跨机房,比如请求打到网关服务的X机房,那么也应该是将请求转发给X机房的后端服务节点,如果后端服务不存在X机房的节点,则请求到其他机房的节点。
- 灰度路由:类似于Dubbo的标签路由功能,如果希望对后端服务节点进行分组隔离,则需要给后端服务一个标签名,建立"标签名→节点列表"的映射关系,请求方携带这个标签名,请求到相应的后端服务节点。
网关管理后台支持动态配置路由信息,动态开启/关闭路由功能。
4.3 负载均衡
当前支持的负载均衡策略:加权随机算法、加权轮询算法、一致性哈希算法。
可以通过网关管理后台动态调整负载均衡策略,支持API接口级别、应用级别的配置。
负载均衡机制并未采用Netflix Ribbon,而是仿造Dubbo负载均衡的算法实现的。
4.4 动态配置
API网关支持一套自洽的动态配置功能,在不依赖第三方配置中心的条件下,仍然支持实时调整配置项,并且配置项分为全局配置、应用级别治理配置、API接口级别治理配置。
在自洽的动态配置功能之外,网关服务也与公司级别的配置中心进行打通,支持公司级配置中心配置相应的配置项。
4.5 API管理
API管理支持网关SDK自动扫描上报,也支持在管理后台手工配置。
4.6 协议转换
后端的服务有很多是基于Dubbo框架的,网关服务支持HTTP→HTTP的请求转发,也支持HTTP→Dubbo的协议转换。
同时C 技术栈,采用了tars框架,网关服务也支持HTTP → tras协议转换。
4.7 安全机制
API网关提供了IP黑白名单、OAuth认证授权、appKey