⼀个简单可参考的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.Value("netty.sobacklog"));
ServerBootstrap b = new ServerBootstrap();
.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 " + Name() + " started and listen on " + f.channel().localAddress());
2、初始化 Handler
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpRequestDecoder());
p.addLast(new HttpResponseEncoder());
int maxContentLength = 2000;
try {
maxContentLength = Integer.Value("netty.maxContentLength"));
} catch (Exception e) {
logger.warn("netty.maxContentLength 配置异常,系统默认为:2000KB");
微服务网关和注册中心区别}
p.addLast(new HttpObjectAggregator(maxContentLength * 1024));// HTTP 消息的合并处理
p.addLast(new HttpServerInboundHandler());
}
HttpRequestDecoder 和 HttpResponseEncoder 分别实现 Http 协议的解析和封装,Http Post 内容超过⼀个数据包⼤⼩会⾃动分组,通过HttpObjectAggregator 可以⾃动将这些数据粘合在⼀起,对于上层收到是⼀个完整的 Http 请求。
3、通过 HttpServerInboundHandler 将⽹络请求转发给⽹关执⾏器
@Override
public void channelRead0(ChannelHandlerContext ctx, Object msg)
throws Exception {
try {
if (msg instanceof HttpRequest && msg instanceof HttpContent) {
CmptRequest cmptRequest = vert(ctx, msg);
CmptResult cmptResult = ute(cmptRequest);
FullHttpResponse response = encapsulateResponse(cmptResult);
ctx.write(response);
ctx.flush();
}
} catch (Exception e) {
<("⽹关⼊⼝异常," \+ 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 = Logger(WeixinAuthTokenCmpt.class);
private final CmptResult SUCCESS_RESULT;
public WeixinAuthTokenCmpt() {
SUCCESS_RESULT = buildSuccessResult();
}
@Override
public CmptResult execute(CmptRequest request, Map<String, FieldDTO> config) {
if (logger.isDebugEnabled()) {
logger.debug("WeixinTokenCmpt ......");
}
CmptResult cmptResult = null;
//Token 认证超时间 (传⼊单位: 分)
long authTokenExpireTime = getAuthTokenExpireTime(config);
WeixinTokenDTO authTokenDTO = AuthTokenDTO(request);
logger.debug("Token=" + authTokenDTO);
AuthTokenState authTokenState = validateToken(authTokenDTO, authTokenExpireTime);
switch (authTokenState) {
case ACCESS: {
cmptResult = SUCCESS_RESULT;
Map<String, String> header = new HashMap<>();
header.put(HeaderKeyConstants.HEADER\_APP\_ID_KEY, AppID());
header.put(CmptHeaderKeyConstants.HEADER\_WEIXIN\_OPENID_KEY, OpenID());
header.put(CmptHeaderKeyConstants.HEADER\_WEIXIN\_SESSION_KEY, SessionKey());
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;
}
...
}
上⾯例⼦看不懂没关系,接下来会详细阐述组件的设计思路。
1、组件接⼝定义
public interface ICmpt {
/**
* 组件执⾏⼊⼝
*
* @param request
* @param config,组件实例的参数配置
* @return
*/
CmptResult execute(CmptRequest request, Map<String, FieldDTO> config);
/**
* 销毁组件持有的特殊资源,⽐如线程。
*/
void destroy();
}
execute 是组件执⾏的⼊⼝⽅法,request 前⾯提到过是 http 请求的封装,config 是组件的特殊配置,⽐如上⾯例⼦提到的认证组件就有⼀个⾃定义配置 -Token 的有效期,不同的 API 使⽤该组件可以设置不同的有效期。
FieldDTO 定义如下:
public class FieldDTO {
private String title;
private String name;
private FieldType fieldType = FieldType.STRING;
private String defaultValue;
private boolean required;
private String regExp;
private String description;
}
CmptResult 为组件执⾏后的返回结果,其定义如下:
public class CmptResult {
RespErrMsg respErrMsg;// 组件返回错误信息
private boolean passed;// 组件过滤是否通过
private byte\[\] data;// 组件返回数据
private Map<String, String> header = new HashMap<String, String>();// 透传后端服务响应头信息
private MediaType mediaType;// 返回响应数据类型
private Integer statusCode = 200;// 默认返回状态码为 200
}
2、组件类型定义
执⾏器需要根据组件类型和组件执⾏结果判断是要直接返回客户端还是继续往下⾯执⾏,⽐如认证类型的组件,如果认证失败是不能继续往下执⾏的,但缓存类型的组件没有命中才继续往下执⾏。当然这样设计存在⼀些缺陷,⽐如新增组件类型需要执⾏器配合调整处理逻辑。(Kong 也提供了⼤量的功能组件,没有研究过其⽹关框架是如何跟组件配合的,是否⽀持⽤户⾃定义组件类型,知道的朋友详细交流下。)
初步定义如下组件类型:
认证、鉴权、流量管控、缓存、路由、⽇志等。
其中路由类型的组件涵盖了协议转换的功能,其负责调⽤上游系统提供的服务,可以根据上游系统提供 API 的协议定制不同的路由组件,⽐如:Restful、WebService、Dubbo、EJB 等等。
3、组件执⾏位置和优先级设定
执⾏位置:Pre、Routing、After,分别代表后端服务调⽤前、后端服务调⽤中和后端服务调⽤完成后,相同位置的组件根据优先级决定执⾏的先后顺序。
4、组件发布形式
组件打包成标准的 Jar 包,通过 Admin 管理界⾯上传发布。
附 - 组件可视化选择 UI 设计
组件热插拔设计和实现
JVM 中 Class 是通过类加载器 + 全限定名来唯⼀标识的,上⾯章节谈到组件是以 Jar 包的形式发布的,
但相同组件的多个版本的⼊⼝类名需要保持不变,因此要实现组件的热插拔和多版本并存就需要⾃定义类加载器来实现。
⼤致思路如下:
⽹关接收到 API 调⽤请求后根据请求参数从缓存⾥拿到 API 配置的组件列表,然后再逐⼀参数从缓存⾥获取组件对应的类实例,如果不到则尝试通过⾃定义类加载器载⼊ Jar 包,并初始化组件实例及缓存。
附 - 参考⽰例
public static ICmpt newInstance(final CmptDef cmptDef) {
ICmpt cmpt = null;
try {
final String jarPath = getJarPath(cmptDef);
if (logger.isDebugEnabled()) {
logger.debug("尝试载⼊ jar 包,jar 包路径: " + jarPath);
}
// 加载依赖 jar
CmptClassLoader cmptClassLoader = CmptClassLoaderManager.loadJar(jarPath, true);
// 创建实例
if (null != cmptClassLoader) {
cmpt = FullQualifiedName(), ICmpt.class, cmptClassLoader);
} else {
<("加载组件 jar 包失败! jarPath: " + jarPath);
}
} catch (Exception e) {
<("组件类加载失败,请检查类名和版本是否正确。ClassName=" + FullQualifiedName() + ", Version=" + Version());
e.printStackTrace();
}
return cmpt;
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论