互联网API网关架构设计.docx

上传人:b****8 文档编号:27648146 上传时间:2023-07-03 格式:DOCX 页数:18 大小:1.22MB
下载 相关 举报
互联网API网关架构设计.docx_第1页
第1页 / 共18页
互联网API网关架构设计.docx_第2页
第2页 / 共18页
互联网API网关架构设计.docx_第3页
第3页 / 共18页
互联网API网关架构设计.docx_第4页
第4页 / 共18页
互联网API网关架构设计.docx_第5页
第5页 / 共18页
点击查看更多>>
下载资源
资源描述

互联网API网关架构设计.docx

《互联网API网关架构设计.docx》由会员分享,可在线阅读,更多相关《互联网API网关架构设计.docx(18页珍藏版)》请在冰豆网上搜索。

互联网API网关架构设计.docx

互联网API网关架构设计

 

互联网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的失效备援,当LBNode1停止服务,Keepalived会将虚拟IP自动飘移到LBNode2,从而避免因为负载均衡器导致单点故障。

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框架(设计优雅,大名鼎鼎的SpringWebflux默认都是使用的Netty,更多的优势就不在此详述了),内部测试在相同的机器上分别通过Tomcat和Netty生成UUID,Netty的性能大约有20%的提升,如果后端服务响应耗时较高的话吞吐量还有更大的提升。

(补充:

Netty4.x的版本即可,不要采用5以上的版本,有严重的缺陷没有解决)

采用Netty作为Http容器首先需要解决的是Http协议的解析和封装,好在Netty本身提供了这样的Handler,具体参考如下代码:

1、构建一个单例的HttpServer,在SpringBoot启动的时候同时加载并启动Netty服务

intsobacklog=Integer.parseInt(AppConfigUtil.getValue("netty.sobacklog"));

ServerBootstrapb=newServerBootstrap();

b.group(bossGroup,workerGroup)

.channel(NioServerSocketChannel.class)

.localAddress(newInetSocketAddress(this.portHTTP))

.option(ChannelOption.SO_BACKLOG,sobacklog)

.childHandler(newChannelHandlerInitializer(null));

//绑定端口

ChannelFuturef=b.bind(this.portHTTP).sync();

logger.info("HttpServernameis"+HttpServer.class.getName()+"startedandlistenon"+f.channel().localAddress());

2、初始化Handler

@Override

protectedvoidinitChannel(SocketChannelch)throwsException{

ChannelPipelinep=ch.pipeline();

p.addLast(newHttpRequestDecoder());

p.addLast(newHttpResponseEncoder());

intmaxContentLength=2000;

try{

maxContentLength=Integer.parseInt(AppConfigUtil.getValue("netty.maxContentLength"));

}catch(Exceptione){

logger.warn("netty.maxContentLength配置异常,系统默认为:

2000KB");

}

p.addLast(newHttpObjectAggregator(maxContentLength*1024));//HTTP消息的合并处理

p.addLast(newHttpServerInboundHandler());

}

HttpRequestDecoder和HttpResponseEncoder分别实现Http协议的解析和封装,HttpPost内容超过一个数据包大小会自动分组,通过HttpObjectAggregator可以自动将这些数据粘合在一起,对于上层收到是一个完整的Http请求。

3、通过HttpServerInboundHandler将网络请求转发给网关执行器

@Override

publicvoidchannelRead0(ChannelHandlerContextctx,Objectmsg)

throwsException{

try{

if(msginstanceofHttpRequest&&msginstanceofHttpContent){

CmptRequestcmptRequest=CmptRequestUtil.convert(ctx,msg);

CmptResultcmptResult=this.gatewayExecutor.execute(cmptRequest);

FullHttpResponseresponse=encapsulateResponse(cmptResult);

ctx.write(response);

ctx.flush();

}

}catch(Exceptione){

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:

''}

*

publicclassWeixinAuthTokenCmptextendsAbstractCmpt{

privatestaticLoggerlogger=LoggerFactory.getLogger(WeixinAuthTokenCmpt.class);

privatefinalCmptResultSUCCESS_RESULT;

publicWeixinAuthTokenCmpt(){

SUCCESS_RESULT=buildSuccessResult();

}

@Override

publicCmptResultexecute(CmptRequestrequest,Mapconfig){

if(logger.isDebugEnabled()){

logger.debug("WeixinTokenCmpt......");

}

CmptResultcmptResult=null;

//Token认证超时间(传入单位:

分)

longauthTokenExpireTime=getAuthTokenExpireTime(config);

WeixinTokenDTOauthTokenDTO=this.getAuthTokenDTO(request);

logger.debug("Token="+authTokenDTO);

AuthTokenStateauthTokenState=validateToken(authTokenDTO,authTokenExpireTime);

switch(authTokenState){

caseACCESS:

{

cmptResult=SUCCESS_RESULT;

Mapheader=newHashMap<>();

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;

}

caseEXPIRED:

{

cmptResult=buildCmptResult(RespErrCode.AUTH\_TOKEN\_EXPIRED,"token过期,请重新获取Token!

");

break;

}

caseINVALID:

{

cmptResult=buildCmptResult(RespErrCode.AUTH\_INVALID\_TOKEN,"Token无效!

");

break;

}

}

returncmptResult;

}

...

}

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

1、组件接口定义

publicinterfaceICmpt{

  /**    

  *组件执行入口

  *

  *@paramrequest

  *@paramconfig,组件实例的参数配置

  *@return

  */  

  CmptResultexecute(CmptRequestrequest,Mapconfig);

  /**

  *销毁组件持有的特殊资源,比如线程。

  */

  voiddestroy();

}

execute是组件执行的入口方法,request前面提到过是http请求的封装,config是组件的特殊配置,比如上面例子提到的微信认证组件就有一个自定义配置-Token的有效期,不同的API使用该组件可以设置不同的有效期。

FieldDTO定义如下:

publicclassFieldDTO{

privateStringtitle;

privateStringname;

privateFieldTypefieldType=FieldType.STRING;

privateStringdefaultValue;

privatebooleanrequired;

privateStringregExp;

privateStringdescription;

}

CmptResult为组件执行后的返回结果,其定义如下:

publicclassCmptResult{

RespErrMsgrespErrMsg;//组件返回错误信息

privatebooleanpassed;//组件过滤是否通过

privatebyte\[\]data;//组件返回数据

privateMapheader=newHashMap();//透传后端服务响应头信息

privateMediaTypemediaType;//返回响应数据类型

privateIntegerstatusCode=200;//默认返回状态码为200

}

2、组件类型定义

执行器需要根据组件类型和组件执行结果判断是要直接返回客户端还是继续往下面执行,比如认证类型的组件,如果认证失败是不能继续往下执行的,但缓存类型的组件没有命中才继续往下执行。

当然这样设计存在一些缺陷,比如新增组件类型需要执行器配合调整处理逻辑。

(Kong也提供了大量的功能组件,没有研究过其网关框架是如何跟组件配合的,是否支持用户自定义组件类型,知道的朋友详细交流下。

初步定义如下组件类型:

认证、鉴权、流量管控、缓存、路由、日志等。

其中路由类型的组件涵盖了协议转换的功能,其负责调用上游系统提供的服务,可以根据上游系统提供API的协议定制不同的路由组件,比如:

Restful、WebService、Dubbo、EJB等等。

3、组件执行位置和优先级设定

执行位置:

Pre、Routing、After,分别代表后端服务调用前、后端服务调用中和后端服务调用完成后,相同位置的组件根据优先级决定执行的先后顺序。

4、组件发布形式

组件打包成标准的Jar包,通过Admin管理界面上传发布。

附-组件可视化选择UI设计

组件热插拔设计和实现

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

大致思路如下:

网关接收到API调用请求后根据请求参数从缓存里拿到API配置的组件列表,然后再逐一参数从缓存里获取组件对应的类实例,如果找不到则尝试通过自定义类加载器载入Jar包,并初始化组件实例及缓存。

附-参考示例

publicstaticICmptnewInstance(finalCmptDefcmptDef){

  ICmptcmpt=null;

  try{

    finalStringjarPath=getJarPath(cmptDef);

    if(logger.isDebugEnabled()){

      logger.debug("尝试载入jar包,jar包路径:

"+jarPath);

    }

    //加载依赖jar

    CmptClassLoadercmptClassLoader=CmptClassLoaderManager.loadJar(jarPath,true);

    //创建实例

    if(null!

=cmptClassLoader){

      cmpt=LoadClassUtil.newObject(cmptDef.getFullQualifiedName(),ICmpt.class,cmptClassLoader);

    }else{

      logger.error("加载组件jar包失败!

jarPath:

"+jarPath);

    }

  }catch(Exceptione){

    logger.error("组件类加载失败,请检查类名和版本是否正确。

ClassName="+cmptDef.getFullQualifiedName()+",Version="+cmptDef.getVersion());

    e.printStackTrace();

  }

  returncmpt;

}

补充说明:

自定义类加载器可直接需要继承至URLClassLoader,另外必须指定其父类加载器为执行器的加载器,否则组件没法引用网关的其它类。

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

在详细阐述设计前先讲个实际的案例,大概12年的时候某公司自研了一款ESB的中间件(企业服务总线跟API网关很类似,当年SOA理念大行其道的时候都推崇的是ESB,侧重服务的编排和异构系统的整合。

),刚开始用的还行,但随着接入系统的增多,突然某天运维发现大量API出现缓慢甚至超时,初步检查发现ESB每个节点的线程几乎消耗殆尽,起初判断是资源不够,紧急扩容后还是很快线程占满,最终导致上百个系统瘫痪。

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

以上案例说明了一个在企业应用架构设计里面的经典原则-故障隔离,由于所有的API请求都要经过网关,必须隔离API之间的相互影响,尤其是个别API故障导致整个网关集群服务中断。

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

1、故障隔离

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

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

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

∙Netty本身就有工作线程池了,再增加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)

 

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高中教育 > 数学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1