随着移动互联网的兴起、开放合作思维的盛行,不同终端和第三方开发者都需要大量的接入企业核心业务能力,此时各业务系统将会面临同一系列的问题,例如:如何让调用方快速接入、如何让业务方安全地对外开放能力,如何应对和控制业务洪峰调用等等。于是就诞生了一个隔离企业内部业务系统和外部系统调用的屏障 - API网关,它负责在上层抽象出各业务系统需要的通用功能,例如:鉴权、限流、ACL、降级等。另外随着近年来微服务的流行,API网关已经成为一个微服务架构中的标配组件。
有赞API网关目前承载着微商城、零售、微小店、餐饮、美业、AppSDK、部分PC、三方开发者等多个业务的调用,每天有着亿级别的流量。
有赞后端服务最开始是由PHP搭建,随着整个技术体系的升级,后面逐步从PHP迁移到Java体系。在API网关设计之初主要支持Dubbo、Http两种协议。迁移过程中,我们发现部分服务需要通过RPC方式调用PHP服务,于是我们(公司)基于Dubbo开发了一个新的框架Nova,兼容Dubbo调用,同时支持调用PHP服务。于是网关也支持了新的Nova协议,这样就有Dubbo、Http、Nova三种协议。
随着业务的不断发展,业务服务化速度加快,网关面临各类新的需求。例如回调类型的API接入,这种API不需要鉴权,只需要一个限流服务,路由到后端服务即可;另外还有参数、返回值的转换需求也不断到来,这期间我们快速迭代满足新的需求。而在这个过程中我们也走了很多弯路,例如API的规范,在最开始规范意识比较笼统,导致返回值在对外暴露时出现了不统一的情况,后续做SDK自动化的时候比较棘手,经过不断的约束开发者,最终做到了统一。
部署架构图
网关的调用方主要包括微商城、微小店、零售等App应用,以及三方开发者和部分PC业务。通过LVS做负载均衡,后端Tengine实现反向代理,网关应用调用到实际的业务集群
应用架构图
网关核心由Pipe链构成,每个Pipe负责一块功能,同时使用缓存、异步等特性提升并发及性能
线程模型图
网关采用Jetty部署,调用采用Http协议,请求由容器线程池处理(容器开启了Servlet3.0异步,提升了较大的吞吐量),之后分发到应用线程池异步处理。应用线程池在设计之初考虑不同的任务执行可能会出现耗时不一的情况,所以将任务分别拆分到不同的线程池,以提高不同类型任务的并发度,如图分为CommonGroup, ExecutionGroup, ResultGroup
CommonGroup执行通用任务,ExecutionGroup执行多协议路由及调用任务,ResultGroup执行结果处理任务(包含异常)
网关业务生态图
网关生态主要包含控制台、网关核心、网关统计与监控
控制台主要对API生命周期进行管理,以及ACL、流量管控等功能;
网关核心主要处理API调用,包含鉴权、限流、路由、协议转换等功能;
统计与监控模块主要完成API调用的统计以及对店铺、三方的一些报表统计,同时提供监控功能和报警功能
我们使用Jetty容器来部署应用,并开启Servlet3.0的异步特性,由于网关业务本身就是调用大量业务接口,因此IO操作会比较频繁,使用该特性能较大提升网关整体并发能力及吞吐量。另外我们在内部处理开启多组线程池进行异步处理,以异步回调的方式通知任务完成,进一步提升并发量
为了进一步提升网关的性能,我们增加了一层分布式缓存(借用Codis实现),将一些不经常变更的API元数据缓存下来,这样不仅减少了应用和DB的交互次数,还加快了读取效率。我们同时考虑到Codis在极端情况下存在不稳定因素,因此我们在本地再次做了本地缓存,这样的读取可以从ms级别降低到ns级别。为了实现多台机器的本地缓存一致性,我们使用了ZK监听节点变化来更新各机器本地缓存
在设计网关的时候,我们采用责任链模式来实现网关的核心处理流程,将每个处理逻辑看成一个Pipe,每个Pipe按照预先设定的顺序先后执行,与开源的Zuul 1.x类似,我们也采用了PRPE模式(Pre、Routing、Post、Error),在我们这里Pre分为PrePipe、RateLimitPipe、AuthPipe、AclPipe、FlowSepPipe,这些Pipe对数据进行预处理、限流、鉴权、访问控制、分流,并将过滤后的Context向下传递;Routing分为DubboPipe、HttpPipe,这些Pipe分别处理Dubbo协议、Http协议路由及调用;Post为ResultPipe,处理正常返回值以及统计打点,Error为ErrorPipe,处理异常场景
Jetty容器线程池(QTP)负责接收Http请求,之后交由应用线程池CommonGroup,ExecutionGroup, ResultGroup,通用的操作将会被放到CommonGroup线程池执行,执行真实调用的被放到ExecutionGroup,结果处理放到ResultGroup。这样部分Pipe之间线程隔离,通常前置Pipe处理都比较快,所以共享线程池即可,真实调用通常比较耗时,因此我们放到独立的线程池,同时结果处理也存在一些运算,因此也放到独立线程池
最早我们采用了简单的分布式缓存(Codis)计数实现限流,以IP、API维度构建Key进行累加,这种限流方式实现简单,但是不能做到连续时间段内平滑限流。例如针对某个API每分钟限流100次,第1秒发起20次,第二秒发起30次,第3秒发起40次,这样的限流波动比较大,因此我们决定将其改进。经过调研我们最终选择了令牌桶限流,令牌桶限流相比于漏桶限流能适应闲置较长时段后的尖峰调用,同时消除了简单计数器限流带来的短时间内流量不均的问题。目前网关支持IP、店铺、API、应用ID和三方ID等多个维度的限流,也支持各维度的自由组合限流,可以很容易扩展出新的维度
由于我们经常遇到调用后端接口超时,或者异常的情况,后端服务无法立即恢复,这种情况下再将请求发到后端已没有意义。于是我们使用Hystrix进行熔断降级处理。Hystrix支持线程池和信号量2种模式的隔离方案,网关的业务场景是多API和API分组,每个API都可能路由到不同后端服务,如果我们对API或者API分组做线程池隔离,就会产生大量的线程,所以我们选择了信号量做隔离。我们为每个API提供一个降级配置,用户可以选择自己配置的API在达到多少错误率时进行熔断降级。
引入Hystrix后,Hystrix会对每个API做统计,包括总量、正确率、QPS等指标,同时会产生大量事件,当API很多的时候,这些指标和事件会占用大量内存,导致更加频繁的YoungGC,这对应用性能产生了一定的影响,不过整体的收益还是不错的
另外有赞内部也开发了一个基于Hystrix的服务熔断平台(Tesla),平台在可视化、易用性、扩展性上面均有较大程度的提升;后续网关会考虑熔断模块的实现基于服务熔断平台,以提供更好的服务
有赞内部存在多种协议类型的后端服务,最原始的服务是PHP开发,后面逐渐迁移到Java,很早一部分API是由PHP暴露的,后续为了能做灰度迁移到Java,我们做了分流,将老的PHP接口的流量按照一定的比例分发到新的Java接口上
除了核心功能的调用外,网关还需要支持内部用户(下称业务方)快速配置接口暴露给开发者。 控制台主要职责包括:快速配置API、一站式测试API、一键发布API,自动化文档生成,自动化SDK生成
为了让业务方能在上线后了解自己的接口的运行状况,我们做了API相关的统计。我们通过在核心模块里面打日志,利用rsyslog采集数据到Kafka,然后从Kafka消费进行统计,之后回流到数据库供在线查询
除此之外,我们为每个商家做了他们授权的服务商调用接口的统计。这块功能的实现,我们通过Storm从Kafka实时消费,并实时统计落HBase,每天凌晨将前一天的数据同步到Hive进行统计并回流到数据库
业务方API上线后,除了查看统计外,当API出问题时,还需要及时发现。我们针对这块做了API报警功能。用户在平台配置自己的API的报警,这里我们主要支持基于错误数或RT维度的报警。
我们实时地从Kafka消费API调用日志,如果发现某个API的RT或者错误次数超过配置的报警阈值,则会立即触发报警
在网关上暴露的API很多,如何让这些API按照统一的标准对外暴露,让开发者能够低门槛快速接入是网关需要思考的问题
网关规范主要是对API的命名、入参(公用入参、业务入参)、内部服务返回值、错误码(公用错误码、业务错误码)、出参(公用出参、业务出参),进行规范
在我们的实践过程中,总结了以下规范:
现象:应用hung死,调用接口返回503,无法服务
排查过程:现场dump了内存,GC记录,以及线程运行快照。首先看了GC发现是Full GC,但是不清楚是哪里发生的,看线程运行快照也没发现什么问题。于是在本地用HeapAnalysis分析,堆区没看出什么问题,大对象都是应该占用的;于是查看方法区,通过ClassLoader Analysis发现Fastjson相关的类较多,因此怀疑是class泄露,进一步通过MAT的OQL语法分析,发现是Fastjson在序列化Jetty容器的HttpServletRequest时,为了加快速度于是创建新的类时抛了异常,导致动态创建的类在方法区堆积从而引发Full GC,后续我们也向Fastjson提了相关bug
解决方案:将序列化HttpServletRequest的代码移除
现象:在有赞双11全链路压测期间,某个业务调用API,导致我们的应用CPU几乎接近100%
排查过程:经过日志分析,发现该接口存在大量超时,但是从代码没看出特别有问题的地方。于是我们将接口在QA环境模拟调用,用VisualVM连上去,通过抽样器抽样CPU,发现某个方法消耗CPU较高,因此我们迅速定位到源码,发现这段代码主要是执行轮询任务是否完成,如果完成则调用完成回调,如果未完成继续放到队列。再结合之前的环境观察发现大量超时的任务被放到队列,导致任务被取出后,任务仍然是未完成状态,这样会将任务放回队列,这样其实构成了一个死循环
解决方案:将主动轮询改为异步通知,我们这里是Dubbo调用,Dubbo调用返回的Future实际是一个FutureAdapter,可以获取到里面的ResponseFuture(DefaultFuture),这个类型的Future支持设置Callback,任务完成时会通知到设置的回调
有赞网关目前归属有赞共享技术-基础服务中心团队开发和维护;
该团队目前主要分为商品中心、库存中心、物流中心、消息沟通平台、云生态5个小组;
商品/库存/物流中心:通过不断抽象上层业务,完成通用的模型建设;为上层业务方提供高可用的服务,并快速响应多变的业务需求;针对秒杀、洪峰调用、及上层业务多变等需求,三个小组还齐力开发和持续完善着 对比引擎、服务熔断、热点探测等三个通用系统;
消息沟通平台:提供几乎一切消息沟通相关的能力及一套帮助商家与用户联系的多客服系统,每天承载着上亿次调用(短信、apppush、语音、微信、微博、多客服、邮件等通道);
云生态:承担着核心网关的建设和发展(上面的网关应用系统)、三方推送系统、有赞云后台、商业化订购以及App Engine的预研和开发;
目前该团队HC开放,期待有机会与各位共事;(内推邮箱:huangtao@youzan.com)
注,本文作者:有赞网关(黄涛、尹铁夫、叮咚)
欢迎关注我们的公众号
Original url: Access
Created at: 2019-09-26 17:37:28
Category: default
Tags: none
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论