一个简单可参考的API网关架构设计

2023-05-31 0 752

风险提示:

https://mp.weixin.qq.com/s?__biz=Mzg5NzEyNTkxMA==&mid=2247483872&idx=2&sn=291e04d38d1f7903b9264d11665aac86&chksm=c077da99f700538f542f3ed137047bcf016e81b916189ffac71646258c744d386726c8139d66&token=82196855&lang=zh_CN#rdmp.weixin.qq.com/s?__biz=Mzg5NzEyNTkxMA==&mid=2247483872&idx=2&sn=291e04d38d1f7903b9264d11665aac86&chksm=c077da99f700538f542f3ed137047bcf016e81b916189ffac71646258c744d386726c8139d66&token=82196855&lang=zh_CN#rd

写作第一类两个单纯可参照的API交换机构架设计写作第一类

现代民营企业已经开始做微服务项目构架结构调整的开发者或是构架师,期望责任编辑对您能起著一定的鼓励作用。

API交换机如是说

交换机referring较早出现在计算机系统里头,比如说两个互相分立的以太网段之间透过交换机或是桥终端装置展开通讯,这尾端的路由器或是桥终端装置他们称作交换机。

适当的API交换机将各控制系统对内曝露的服务项目圣皮耶尔县,大部份要初始化这些服务项目的控制系统都须要透过API交换机展开出访,如前所述此种形式交换机可以对API展开标准化控管,比如:证书、身份验证、网络流量控制、协议切换、监视之类。

API交换机的盛行得力于近些年微服务项目构架的蓬勃发展,原先两个巨大的销售业务控制系统被拆分为许多发射率更小的控制系统展开分立布署和保护,此种商业模式势必增添更多的跨控制系统可视化,民营企业API的体量也会成倍增长,API交换机(或是微服务项目交换机)就渐渐成为了微服务项目构架的标准配置模块。

如下表所示是他们重新整理的API交换机的三种众所周知应用领域情景:

一个简单可参考的API网关架构设计
1、面向全国Web或是终端App

这类情景,在力学型态上类似于其间端分立,后端应用领域透过API初始化后端服务项目,须要交换机具有证书、身份验证、内存、服务项目选曲、监视监视系统等机能。

2、面向全国合作方开放API3、民营企业内部控制系统互联互通

对于中大型的民营企业内部往往有几十、甚至上百个控制系统,尤其是微服务项目构架的蓬勃发展控制系统数量更是急剧增加。控制系统之间互相依赖,渐渐形成网状初始化关系不便于管理和保护,须要API交换机展开标准化的证书、身份验证、网络流量控管、超时熔断、监视监视系统管理,从而提高系统的稳定性、降低重复建设、运维管理等成本。

设计目标

1、纯Java实现;

2、支持插件化,方便开发者自定义模块;

3、支持横向扩展,高性能;

4、避免单点故障,稳定性要高,不能因为某个API故障导致整个交换机停止服务项目;

5、管理控制台配置更新可自动生效,不须要重启交换机;

应用领域构架设计

一个简单可参考的API网关架构设计

整个平台拆分为3个子控制系统,Gateway-Core(核心子控制系统)、Gateway-Admin(管理中心)、Gateway-Monitor(监视中心)。

Gateway-Core负责接收客户端请求,调度、加载和执行模块,将请求路由器到上游服务项目端,处理上游服务项目端返回的结果等;Gateway-Admin提供标准化的管理界面,用户可在此展开API、模块、控制系统基础信息的设置和保护;Gateway-Monitor负责收集监视日志、生成各种运维管理报表、自动监视系统等;

控制系统构架设计

一个简单可参考的API网关架构设计

说明:

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(newInetSocketAddress(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 =bui case INVALID: { cmptResult =buildCmptResult(RespErrCode.AUTH_INVALID_TOKEN, “Token无效!”); break; } } return cmptResult; } … }

上面例子看不懂没关系,接下来会详细阐述模块的设计思路。

1、模块接口定义

public interface ICmpt { /** *模块执行入口 * * @param request * @param config,模块实例的参数配置 * @return */ CmptResult execute(CmptRequestrequest, Map 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 header = new HashMap();//透传后端服务项目响应头信息 private MediaType mediaType;//返回响应数据类型 private Integer statusCode =200;//默认返回状态码为200 }

2、模块类型定义

执行器须要根据模块类型和模块执行结果判断是要直接返回客户端还是继续往下面执行,比如说证书类型的模块,如果证书失败是不能继续往下执行的,但内存类型的模块没有命中才继续往下执行。当然这样设计存在一些缺陷,比如说新增模块类型须要执行器配合调整处理逻辑。(Kong也提供了大量的机能模块,没有研究过其交换机框架是如何跟模块配合的,是否支持用户自定义模块类型,知道的朋友详细交流下。)

初步定义如下表所示模块类型:证书、身份验证、网络流量控管、内存、路由器、日志等。

其中路由器类型的模块涵盖了协议切换的机能,其负责初始化上游控制系统提供的服务项目,可以根据上游控制系统提供API的协议定制不同的路由器模块,比如说:Restful、WebService、Dubbo、EJB之类。

3、模块执行位置和优先级设定

执行位置:Pre、Routing、After,分别代表后端服务项目初始化前、后端服务项目初始化中和后端服务项目初始化完成后,相同位置的模块根据优先级决定执行的先后顺序。

4、模块发布形式

模块打包成标准的Jar包,透过Admin管理界面上传发布。

附-模块可视化选择UI设计

一个简单可参考的API网关架构设计

模块热插拔设计和实现

JVM中Class是透过类加载器+全限定名来唯一标识的,上面章节谈到模块是以Jar包的形式发布的,但相同模块的多个版本的入口类名须要保持不变,因此要实现模块的热插拔和多版本并存就须要自定义类加载器来实现。

大致思路如下表所示:

模块对应的类实例,如果找不到则尝试透过自定义类加载器载入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 = LoadClassUtil.newObject(cmptDef.getFullQualifiedName(), ICmpt.class, cmptClassLoader); } else { logger.error(“加载模块jar包失败! jarPath: ” + jarPath); } } catch (Exception e) { logger.error(“模块类加载失败,请检查类名和版本是否正确。ClassName=” + cmptDef.getFullQualifiedName() + “, Version=” + cmptDef.getVersion()); e.printStackTrace(); } return cmpt; }

补充说明:

自定义类加载器可直接须要继承至URLClassLoader,另外必须指定其父类加载器为执行器的加载器,否则模块没法引用交换机的其它类。

API故障隔离及超时、熔断处理

在详细阐述设计前先讲个实际的案例,大概12年的时候某公司自研了一款ESB的尾端件(民营企业服务项目总线跟API交换机很类似于,当年SOA理念大行其道的时候都推崇的是ESB,侧重服务项目的选曲和异构控制系统的整合。),刚开始用的还行,但随着接入控制系统的增多,突然某天运维发现大量API出现缓慢甚至超时,初步检查发现ESB每个节点的线程几乎消耗殆尽,起初判断是资源不够,紧急扩容后还是很快线程占满,最终导致上百个控制系统瘫痪。

最终找到问题的症结是某个销售业务控制系统自身的原因导致服务项目不可用,下游销售业务控制系统请求大量堆积到ESB中,从而导致大量线程堵塞。

以上案例说明了两个在民营企业应用领域构架设计里头的经典原则-故障隔离,由于大部份的API请求都要经过交换机,必须隔离API之间的互相影响,尤其是个别API故障导致整个交换机集群服务项目中断。

接下来分别如是说故障隔离、超时控管、熔断的实现思路。

1、故障隔离

有两种形式可以实现,一是为每个API创建两个线程池,每个线程分配10~20个线程,这也是常用的隔离策略,但此种形式有几个明显的缺点:

1)线程数会随着API接入数量递增,1000个API就须要2万个线程,光线程切换对CPU就是不小的开销,而其线程还须要占用一定的内存资源;

2)平均分配线程池大小导致个别出访量较大且响应时间相对较长的API吞吐量上不去;

3)本身就有工作线程池了,再增加API的线程池,导致某些须要ThreadLocal特性的编程变得困难。

二是用信号量隔离,直接复用Netty的工作线程,上面线程池隔离提到的3个缺点都可以基本避免, 建议设置单个API的信号量个数小于等于Netty工作线程池数量的1/3,这样既兼顾了单个API的性能又不至于单个API的问题导致整个交换机堵塞。具体实现可以考虑直接引用成熟的开源框架,推荐Hystrix,可以同时解决超时控制和熔断。

参照配置如下表所示:

Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey)) .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey )) .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() //舱壁隔离策略-信号量 .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE) //设置每组command可以申请的信号量最大数 .withExecutionIsolationSemaphoreMaxConcurrentRequests(CmptInvoker.maxSemaphore) /*开启超时设置*/ .withExecutionIsolationThreadInterruptOnTimeout(true) /*超时时间设置*/ .withExecutionIsolationThreadTimeoutInMilliseconds(timeout) .withCircuitBreakerEnabled(true)//开启熔断 .withCircuitBreakerSleepWindowInMilliseconds(Constants.DEFAULT_CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS)//5秒后会尝试闭合回路

2、 超时控管

API的超时控制是必须要做的,否则上游服务项目即便是间歇性响应缓慢也会堵塞大量线程(虽然透过信号量隔离后不会导致整个交换机线程堵塞)。

其次,每个API最好可以单独配置超时时间,但不建议可以让用户随意设置,还是要有个最大阈值。(API交换机不适合须要长时间传输数据的情景,比如说大文件上传或是下载、DB数据同步等)

cket响应的超时时间,Hystrix可以对整个初始化展开超时控制等。

3、熔断

熔断类似于电路中的保险丝,当超过负荷或是电阻被击穿的时候自动断开对设备起著保护作用。在API交换机中设置熔断的目的是快速响应请求,避免不必要的等待,比如说某个API后端服务项目正常情况下1s以内响应,但现在因为各种原因出现堵塞大部分请求20s才能响应,虽然设置了10s的超时控制,但让请求线程等待10s超时不仅没有意义,反而会增加服务项目提供方的负担。

为此他们可以设置单位时间内超过多少比例的请求超时或是异常,则直接熔断链路,等待一段时间后再次尝试恢复链路。

实现层面可以直接复用Hystrix。

运行时配置更新机制

前面章节提到过出于性能考虑交换机在运行时要尽可能减小对DB的出访,设计上可以将API、模块等关键内容展开内存,这样一来性能是提升了,但也增添了新的问题,比如说Admin对API或是模块展开配置调整后如何及时更新到集群的各个交换机节点。

解决方案很多,比如说引入消息尾端件,当Admin调整配置后就往消息中心发布一条消息,各交换机节点订阅消息,收到消息后刷新内存数据。

他们在具体实现过程中采用的是Zookeeper集群数据同步机制,其实现原理跟消息尾端件很类似于,只不过交换机在启动的时候就会向ZK节点展开注册,也是被动更新机制。

性能考虑

性能是交换机一项非常重要的衡量指标,尤其是响应时间,客户端本来可以直连服务项目端的,现在增加了两个交换机层,对于一个本身耗时几百毫秒的服务项目接入交换机后增加几毫秒,影响倒是可以忽略不计;但如果服务项目本身只须要几毫秒,因为接入交换机再增加一倍的延时,用户感受就会比较明显。

建议在设计上须要遵循如下表所示原则:

1、核心交换机子控制系统必须是无状态的,便于横向扩展。

2、运行时不依赖本地存储,尽量在内存里头完成服务项目的处理和中转。

3、减小对线程的依赖,采用非阻塞式IO和异步事件响应机制。

4、后端服务项目如果是HTTP协议,尽量采用连接池或是Http2,测试连接复用和不复用性能有几倍的差距。(TCP建立连接成本很高)

附-HttpClient连接池设置

PoolingHttpClientConnectionManager cmOfHttp = new PoolingHttpClientConnectionManager(); cmOfHttp.setMaxTotal(maxConn); cmOfHttp.setDefaultMaxPerRoute(maxPerRoute); httpClient = HttpClients.custom() .setConnectionManager(cmOfHttp).setConnectionManagerShared(true) .build();

说明:httpClient第一类可以作为类的成员变量长期驻留内存,这个是连接池复用的前提。

结语

API交换机作为民营企业API服务项目的汇聚中心,其良好的性能、稳定性和可扩展性是基础,只有这个基础打扎实了,他们才能在上面扩展更多的特性。这篇文章主要如是说交换机的总体构架设计, 后面的篇幅在详细探讨下各种模块的具体设计和实现。

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务