阅读对象
传统企业正在做微服务架构转型的开发人员或者架构师,希望本文对您能起到一定的引导作用。
API网关介绍
网关一词较早出现在网络设备里面,比如两个相互独立的局域网段之间通过路由器或者桥接设备进行通信,这中间的路由或者桥接设备我们称之为网关。
相应的API网关将各系统对外暴露的服务聚合起来,所有要调用这些服务的系统都需要通过API网关进行访问,基于这种方式网关可以对API进行统一管控,例如:认证、鉴权、流量控制、协议转换、监控等等。
API网关的流行得益于近几年微服务架构的兴起,原本一个庞大的业务系统被拆分成许多粒度更小的系统进行独立部署和维护,这种模式势必会带来更多的跨系统交互,企业API的规模也会成倍增加,API网关(或者微服务网关)就逐渐成为了微服务架构的标配组件。
如下是我们整理的API网关的几种典型应用场景:
1、面向Web或者移动App
这类场景,在物理形态上类似前后端分离,前端应用通过API调用后端服务,需要网关具有认证、鉴权、缓存、服务编排、监控告警等功能。
2、面向合作伙伴开放API
这类场景,主要为了满足业务形态对外开放,与企业外部合作伙伴建立生态圈,此时的API 网关注重安全认证、权限分级、流量管控、缓存等功能的建设。
3、企业内部系统互联互通
对于中大型的企业内部往往有几十、甚至上百个系统,尤其是微服务架构的兴起系统数量更是急剧增加。系统之间相互依赖,逐渐形成网状调用关系不便于管理和维护,需要API网关进行统一的认证、鉴权、流量管控、超时熔断、监控告警管理,从而提高系统的稳定性、降低重复建设、运维管理等成本。
设计目标
1、纯Java实现;
2、支持插件化,方便开发人员自定义组件;
3、支持横向扩展,高性能;
4、避免单点故障,稳定性要高,不能因为某个API故障导致整个网关停止服务;
5、管理控制台配置更新可自动生效,不需要重启网关;
应用架构设计
整个平台拆分成3个子系统,Gateway-Core(核心子系统)、Gateway-Admin(管理中心)、Gateway-Monitor(监控中心)。
Gateway-Core负责接收客户端请求,调度、加载和执行组件,将请求路由到上游服务端,处理上游服务端返回的结果等;
Gateway-Admin提供统一的管理界面,用户可在此进行API、组件、系统基础信息的设置和维护;
Gateway-Monitor负责收集监控日志、生成各种运维管理报表、自动告警等;
系统架构设计
说明:
1、网关核心子系统通过HAProxy或者Nginx进行负载均衡,为避免正好路由的LB节点服务不可用,可以考虑在此基础上增加Keepalived来实现LB的失效备援,当LB Node1停止服务,Keepalived会将虚拟IP自动飘移到LB Node2,从而避免因为负载均衡器导致单点故障。DNS可以直接指向Keepalived的虚拟IP。
2、网关除了对性能要求很高外,对稳定性也有很高的要求,引入Zookeeper及时将Admin对API的配置更改同步刷新到各网关节点。
3、管理中心和监控中心可以采用类似网关子系统的高可用策略,如果嫌麻烦管理中心可以省去Keepalived,相对来说管理中心没有这么高的可用性要求。
4、理论上监控中心需要承载很大的数据量,比如有1000个API,平均每个API一天调用10万次,对于很多互联网公司单个API的量远远大于10万,如果将每次调用的信息都存储起来太浪费,也没有太大的必要。可以考虑将API每分钟的调用情况汇总后进行存储,比如1分钟的平均响应时间、调用次数、流量、正确率等等。
5、数据库选型可以灵活考虑,原则上网关在运行时要尽可能减少对DB的依赖,否则IO延时会严重影响网关性能。可以考虑首次访问后将API配置信息缓存,Admin对API配置更改后通过Zookeeper通知网关刷新,这样一来DB的访问量可以忽略不计,团队可根据自身偏好灵活选型。
非阻塞式HTTP服务
管理和监控中心可以根据团队的情况采用自己熟悉的Servlet容器部署,网关核心子系统对性能的要求非常高,考虑采用NIO的网络模型,实现纯HTTP服务即可,不需要实现Servlet容器,推荐Netty框架(设计优雅,大名鼎鼎的Spring Webflux默认都是使用的Netty,更多的优势就不在此详述了),内部测试在相同的机器上分别通过Tomcat和Netty生成UUID,Netty的性能大约有20%的提升,如果后端服务响应耗时较高的话吞吐量还有更大的提升。(补充:Netty4.x的版本即可,不要采用5以上的版本,有严重的缺陷没有解决)
采用Netty作为Http容器首先需要解决的是Http协议的解析和封装,好在Netty本身提供了这样的Handler,具体参考如下代码:
1、构建一个单例的HttpServer,在SpringBoot启动的时候同时加载并启动Netty服务
int sobacklog =Integer.parseInt(AppConfigUtil.getValue("netty.sobacklog"));
ServerBootstrap b = newServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(this.portHTTP))
.option(ChannelOption.SO_BACKLOG, sobacklog)
.childHandler(new ChannelHandlerInitializer(null));
//绑定端口
ChannelFuture f =b.bind(this.portHTTP).sync();
logger.info("HttpServer name is " + HttpServer.class.getName()+ " started and listen on " + f.channel().localAddress());
2、初始化Handler
@Override
protected voidinitChannel(SocketChannel ch) throws Exception {
ChannelPipeline p =ch.pipeline();
p.addLast(newHttpRequestDecoder());
p.addLast(newHttpResponseEncoder());
int maxContentLength = 2000;
try {
maxContentLength =Integer.parseInt(AppConfigUtil.getValue("netty.maxContentLength"));
} catch (Exception e) {
logger.warn("netty.maxContentLength配置异常,系统默认为:2000KB");
}
p.addLast(new
HttpObjectAggregator(maxContentLength * 1024));// HTTP 消息的合并处理
p.addLast(newHttpServerInboundHandler());
}
HttpRequestDecoder和HttpResponseEncoder分别实现Http协议的解析和封装,Http Post内容超过一个数据包大小会自动分组,通过HttpObjectAggregator可以自动将这些数据粘合在一起,对于上层收到是一个完整的Http请求。
3、通过HttpServerInboundHandler将网络请求转发给网关执行器
@Override
public voidchannelRead0(ChannelHandlerContext ctx, Object msg)
throws Exception {
try {
if (msg instanceofHttpRequest && msg instanceof HttpContent) {
CmptRequestcmptRequest = CmptRequestUtil.convert(ctx, msg);
CmptResultcmptResult = this.gatewayExecutor.execute(cmptRequest);
FullHttpResponseresponse = encapsulateResponse(cmptResult);
ctx.write(response);
ctx.flush();
}
} catch (Exception e) {
logger.error("网关入口异常," + e.getMessage());
e.printStackTrace();
}
}
设计上建议将Netty接入层代码跟网关核心逻辑代码分离,不要将Netty收到HttpRequest和HttpContent直接给到网关执行器,可以考虑做一层转换封装成自己的Request给到执行器,方便后续可以很容易的将Netty替换成其它Http容器。(如上代码所示,CmptRequest即为自定义的Http请求封装类,CmptResult为网关执行结果类)
组件化及自定义组件支持
组件是网关的核心,大部分功能特性都可以基于组件的形式提供,组件化可以有效提高网关的扩展性。
先来看一个简单的微信认证组件的例子:
如下实现的功能是对API请求传入的Token进行校验,其结果分别是认证通过、Token过期和无效Token,认证通过后再将微信OpenID携带给上游服务系统。
/**
*微信token认证,token格式:
*{appID:'',openID:'',timestamp:132525144172,sessionKey: ''}
*/
public class WeixinAuthTokenCmpt extends AbstractCmpt {
private static Logger logger =LoggerFactory.getLogger(WeixinAuthTokenCmpt.class);
private final CmptResultSUCCESS_RESULT;
public WeixinAuthTokenCmpt() {
SUCCESS_RESULT =buildSuccessResult();
}
@Override
public CmptResult execute(CmptRequest request,Map config) {
if (logger.isDebugEnabled()){
logger.debug("WeixinTokenCmpt ......");
}
CmptResult cmptResult =null;
//Token认证超时间(传入单位:分)
long authTokenExpireTime =getAuthTokenExpireTime(config);
WeixinTokenDTO authTokenDTO= this.getAuthTokenDTO(request);
logger.debug("Token=" + authTokenDTO);
AuthTokenStateauthTokenState = validateToken(authTokenDTO, authTokenExpireTime);
switch (authTokenState) {
case ACCESS: {
cmptResult =SUCCESS_RESULT;
Map header = new HashMap<>();
header.put(HeaderKeyConstants.HEADER_APP_ID_KEY,authTokenDTO.getAppID());
header.put(CmptHeaderKeyConstants.HEADER_WEIXIN_OPENID_KEY,authTokenDTO.getOpenID());
header.put(CmptHeaderKeyConstants.HEADER_WEIXIN_SESSION_KEY,authTokenDTO.getSessionKey());
cmptResult.setHeader(header);
break;
}
case EXPIRED: {
cmptResult =buildCmptResult(RespErrCode.AUTH_TOKEN_EXPIRED, "token过期,请重新获取Token!");
break;
}
case INVALID: {
cmptResult =buildCmptResult(RespErrCode.AUTH_INVALID_TOKEN, "Token无效!");
break;
}
}
return cmptResult;
}
...
}
作者:王苏龙
链接:https://www.jianshu.com/p/fb815d5fc287