2

长连接网关技术专题(五):喜马拉雅自研亿级API网关技术实践

 2 years ago
source link: http://www.blogjava.net/jb2011/archive/2021/05/31/435882.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Jack Jiang

我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
posts - 232, comments - 13, trackbacks - 0, articles - 0

本文由喜马拉雅技术团队原创分享,原题《喜马拉雅自研网关架构实践》,有改动。

网关是一个比较成熟的产品,基本上各大互联网公司都会有网关这个中间件,来解决一些公有业务的上浮,而且能快速的更新迭代。如果没有网关,要更新一个公有特性,就要推动所有业务方都更新和发布,那是效率极低的事,有网关后,这一切都变得不是问题。

喜马拉雅也是一样,用户数增长达到 6 亿多的级别,Web 服务个数达到500+,目前我们网关日处理 200 亿+次调用,单机 QPS 高峰达到 4w+。

网关除了要实现最基本的功能反向代理外,还有公有特性,比如黑白名单,流控,鉴权,熔断,API 发布,监控和报警等。我们还根据业务方的需求实现了流量调度,流量 Copy,预发布,智能化升降级,流量预热等相关功能。

从技术上来说,喜马拉雅API网关的技术演进路线图大致如下:

本文将分享在喜马拉雅API网关在亿级流量前提下,进行的技术演进发展历程和实践经验总结。

(本文同步发布于:http://www.52im.net/thread-3564-1-1.html

2、专题目录

本文是系列文章的第5篇,总目录如下:

长连接网关技术专题(一):京东京麦的生产级TCP网关技术实践总结

长连接网关技术专题(二):知乎千万级并发的高性能长连接网关技术实践

长连接网关技术专题(三):手淘亿级移动端接入层网关的技术演进之路

长连接网关技术专题(四):爱奇艺WebSocket实时推送网关技术实践

长连接网关技术专题(五):喜马拉雅自研亿级API网关技术实践》(* 本文)

3、第1版:Tomcat NIO+Async Servlet

网关在架构设计时最为关键点,就是网关在接收到请求,调用后端服务时不能阻塞 Block,否则网关的吞吐量很难上去,因为最耗时的就是调用后端服务这个远程调用过程。

如果这里是阻塞的,Tomcat 的工作线程都 block 住了,在等待后端服务响应的过程中,不能去处理其他的请求,这个地方一定要异步。

架构图如下:

这版我们实现单独的 Push 层,作为网关收到响应后,响应客户端时,通过这层实现,和后端服务的通信是 HttpNioClient,对业务的支持黑白名单,流控,鉴权,API 发布等功能。

但是这版只是功能上达到网关的要求,处理能力很快就成了瓶颈,单机 QPS 到 5K 的时候,就会不停的 Full GC。

后面通过 Dump 线上的堆分析,发现全是 Tomcat 缓存了很多 HTTP 的请求,因为 Tomcat 默认会缓存 200 个 requestProcessor,每个 prcessor 都关联了一个 request。

还有就是 Servlet 3.0 Tomcat 的异步实现会出现内存泄漏,后面通过减少这个配置,效果明显。

但性能肯定就下降了,总结了下,基于 Tomcat 做为接入端,有如下几个问题。

Tomcat 自身的问题:

  • 1)缓存太多,Tomcat 用了很多对象池技术,内存有限的情况下,流量一高很容易触发 GC;
  • 2)内存 Copy,Tomcat 的默认是用堆内存,所以数据需要读到堆内,而我们后端服务是 Netty,有堆外内存,需要通过数次 Copy;
  • 3)Tomcat 还有个问题是读 body 是阻塞的, Tomcat 的 NIO 模型和 reactor 模型不一样,读 body 是 block 的。

这里再分享一张 Tomcat buffer 的关系图:

通过上面的图,我们可以看出,Tomcat 对外封装的很好,内部默认的情况下会有三次 copy。

HttpNioClient 的问题:获取和释放连接都需要加锁,对应网关这样的代理服务场景,会频繁的建连和关闭连接,势必会影响性能。

基于 Tomcat 的存在的这些问题,我们后面对接入端做改造,用 Netty 做接入层和服务调用层,也就是我们的第二版,能彻底解决上面的问题,达到理想的性能。

4、第2版:Netty+全异步

基于 Netty 的优势,我们实现了全异步,无锁,分层的架构。

先看下我们基于 Netty 做接入端的架构图:

PS:如果你对Netty和Java NIO了解太少,下面几篇资料请务必阅读:

少啰嗦!一分钟带你读懂Java的NIO和经典IO的区别

Java的BIO和NIO很难懂?用代码实践给你看,再不懂我转行!

史上最强Java NIO入门:担心从入门到放弃的,请读这篇!

写给初学者:Java高性能NIO框架Netty的学习方法和进阶策略

新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析

史上最通俗Netty框架入门长文:基本介绍、环境搭建、动手实战

4.1 接入层

Netty 的 IO 线程,负责 HTTP 协议的编解码工作,同时对协议层面的异常做监控报警。

对 HTTP 协议的编解码做了优化,对异常,攻击性请求监控可视化。比如我们对 HTTP 的请求行和请求头大小是有限制的,Tomcat 是请求行和请求加在一起,不超过 8K,Netty 是分别有大小限制。

假如客户端发送了超过阀值的请求,带 cookie 的请求很容易超过,正常情况下,Netty 就直接响应 400 给客户端。

经过改造后,我们只取正常大小的部分,同时标记协议解析失败,到业务层后,就可以判断出是那个服务出现这类问题,其他的一些攻击性的请求,比如只发请求头,不发 body 或者发部分这些都需要监控和报警。

4.2 业务逻辑层

负责对 API 路由,流量调度等一序列的支持业务的公有逻辑,都在这层实现,采样责任链模式,这层不会有 IO 操作。

在业界和一些大厂的网关设计中,业务逻辑层基本都是设计成责任链模式,公有的业务逻辑也在这层实现。

我们在这层也是相同的套路,支持了:

  • 1)用户鉴权和登陆校验,支持接口级别配置;
  • 2)黑白名单:分全局和应用,以及 IP 维度,参数级别;
  • 3)流量控制:支持自动和手动,自动是对超大流量自动拦截,通过令牌桶算法实现;
  • 4)智能熔断:在 Histrix 的基础上做了改进,支持自动升降级,我们是全部自动的,也支持手动配置立即熔断,就是发现服务异常比例达到阀值,就自动触发熔断;
  • 5)灰度发布:我对新启动的机器的流量支持类似 TCP 的慢启动机制,给机器一个预热的时间窗口;
  • 6)统一降级:我们对所有转发失败的请求都会找统一降级的逻辑,只要业务方配了降级规则,都会降级,我们对降级规则是支持到参数级别的,包含请求头里的值,是非常细粒度的,另外我们还会和 varnish 打通,支持 varnish 的优雅降级;
  • 7)流量调度:支持业务根据筛选规则,对流量筛选到对应的机器,也支持只让筛选的流量访问这台机器,这在查问题/新功能发布验证时非常用,可以先通过小部分流量验证再大面积发布上线;
  • 8)流量 copy:我们支持对线上的原始请求根据规则 copy 一份,写入到 MQ 或者其他的 upstream,来做线上跨机房验证和压力测试;
  • 9)请求日志采样:我们对所有的失败的请求都会采样落盘,提供业务方排查问题支持,也支持业务方根据规则进行个性化采样,我们采样了整个生命周期的数据,包含请求和响应相关的所有数据。

上面提到的这么多都是对流量的治理,我们每个功能都是一个 filter,处理失败都不影响转发流程,而且所有的这些规则的元数据在网关启动时就会全部初始化好。

在执行的过程中,不会有 IO 操作,目前有些设计会对多个 filter 做并发执行,由于我们的都是内存操作,开销并不大,所以我们目前并没有支持并发执行。

还有个就是规则会修改,我们修改规则时,会通知网关服务,做实时刷新,我们对内部自己的这种元数据更新的请求,通过独立的线程处理,防止 IO 在操作时影响业务线程。

4.3 服务调用层

服务调用对于代理网关服务是关键的地方,一定需要异步,我们通过 Netty 实现,同时也很好的利用了 Netty 提供的连接池,做到了获取和释放都是无锁操作。

4.3.1)异步 Push:

网关在发起服务调用后,让工作线程继续处理其他的请求,而不需要等待服务端返回。

这里的设计是我们为每个请求都会创建一个上下文,我们在发完请求后,把该请求的 context 绑定到对应的连接上,等 Netty 收到服务端响应时,就会在给连接上执行 read 操作。

解码完后,再从给连接上获取对应的 context,通过 context 可以获取到接入端的 session。

这样 push 就通过 session 把响应写回客户端了,这样设计也是基于 HTTP 的连接是独占的,即连接和请求上下文绑定。

4.3.2)连接池:

连接池的原理如下图:

服务调用层除了异步发起远程调用外,还需要对后端服务的连接进行管理。

HTTP 不同于 RPC,HTTP 的连接是独占的,所以在释放的时候要特别小心,一定要等服务端响应完了才能释放,还有就是连接关闭的处理也要小心。

总结如下几点:

  • 1)Connection:close;
  • 2)空闲超时,关闭连接;
  • 3)读超时关闭连接;
  • 4)写超时,关闭连接;
  • 5)Fin、Reset。

上面几种需要关闭连接的场景,下面主要说下 Connection:close 和空闲写超时两种,其他的应该是比较常见的比如读超时,连接空闲超时,收到 fin,reset 码这几个。

4.3.3)Connection:close:

后端服务是 Tomcat,Tomcat 对连接重用的次数是有限制的,默认是 100 次。

当达到 100 次后,Tomcat 会通过在响应头里添加 Connection:close,让客户端关闭该连接,否则如果再用该连接发送的话,会出现 400。

还有就是如果端上的请求带了 connection:close,那 Tomcat 就不等这个连接重用到 100 次,即一次就关闭。

通过在响应头里添加 Connection:close,即成了短连接,这个在和 Tomcat 保持长连接时,需要注意的,如果要利用,就要主动 remove 掉这个 close 头。

4.3.4)写超时:

首先网关什么时候开始计算服务的超时时间,如果从调用 writeAndFlush 开始就计算,这其实是包含了 Netty 对 HTTP 的 encode 时间和从队列里把请求发出去即 flush 的时间,这样是对后端服务不公平的。

所以需要在真正 flush 成功后开始计时,这样是和服务端最接近的,当然还包含了网络往返时间和内核协议栈处理的时间,这个不可避免,但基本不变。

所以我们是 flush 成功回调后开始启动超时任务。

这里就有个注意的地方:如果 flush 不能快速回调,比如来了一个大的 post 请求,body 部分比较大,而 Netty 发送的时候第一次默认是发 1k 的大小。

如果还没有发完,则增大发送的大小继续发,如果在 Netty 在 16 次后还没有发送完成,则不会再继续发送,而是提交一个 flushTask 到任务队列,待下次执行到后再发送。

这时 flush 回调的时间就比较大,导致这样的请求不能及时关闭,而且后端服务 Tomcat 会一直阻塞在读 body 的地方,基于上面的分析,所以我们需要一个写超时,对大的 body 请求,通过写超时来及时关闭。

5、全链路超时机制

上图是我们在整个链路超时处理的机制:

  • 1)协议解析超时;
  • 2)等待队列超时;
  • 3)建连超时;
  • 4)等待连接超时;
  • 5)写前检查是否超时;
  • 6)写超时;
  • 7)响应超时。

6、监控报警

网关业务方能看到的是监控和报警,我们是实现秒级别报警和秒级别的监控,监控数据定时上报给我们的管理系统,由管理系统负责聚合统计,落盘到 influxdb。

我们对 HTTP 协议做了全面的监控和报警,无论是协议层的还是服务层的。

协议层:

  • 1)攻击性请求,只发头,不发/发部分 body,采样落盘,还原现场,并报警;
  • 2)Line or Head or Body 过大的请求,采样落盘,还原现场,并报警。

应用层:

  • 1)耗时监控:有慢请求,超时请求,以及 tp99,tp999 等;
  • 2)OPS 监控和报警;
  • 3)带宽监控和报警:支持对请求和响应的行,头,body 单独监控;
  • 4)响应码监控:特别是 400,和 404;
  • 5)连接监控:我们对接入端的连接,以及和后端服务的连接,后端服务连接上待发送字节大小也都做了监控;
  • 6)失败请求监控;
  • 7)流量抖动报警:这是非常有必要的,流量抖动要么是出了问题,要么就是出问题的前兆。

总体架构:

7、性能优化实践

7.1 对象池技术

对于高并发系统,频繁的创建对象不仅有分配内存的开销外,还有对gc会造成压力,我们在实现时会对频繁使用的比如线程池的任务task,StringBuffer等会做写重用,减少频繁的申请内存的开销。

7.2 上下文切换

高并发系统,通常都采用异步设计,异步化后,不得不考虑线程上下文切换的问题。

我们的线程模型如下:

我们整个网关没有涉及到io操作,但我们在业务逻辑这块还是和netty的io编解码线程异步。

是有两个原因:

  • 1)是防止开发写的代码有阻塞;
  • 2)是业务逻辑打日志可能会比较多,在突发的情况下,但是我们在push线程时,支持用netty的io线程替代,这里做的工作比较少,这里有异步修改为同步后(通过修改配置调整),cpu的上下文切换减少20%,进而提高了整体的吞吐量,就是不能为了异步而异步,zull2的设计和我们的类似。

7.3 GC优化

在高并发系统,gc的优化不可避免,我们在用了对象池技术和堆外内存时,对象很少进入老年代,另外我们年轻代会设置的比较大,而且SurvivorRatio=2,晋升年龄设置最大15,尽量对象在年轻代就回收掉, 但监控发现老年代的内存还是会缓慢增长,通过dump分析,我们每个后端服务创建一个链接,都时有一个socket,socket的AbstractPlainSocketImpl,而AbstractPlainSocketImpl就重写了Object类的finalize方法。

实现如下:

 * Cleans up if the user forgets to close it.

protected void finalize() throws IOException {

    close();

是为了我们没有主动关闭链接,做的一个兜底,在gc回收的时候,先把对应的链接资源给释放了。

由于finalize的机制是通过jvm的Finalizer线程来处理的,而且Finalizer线程的优先级不高,默认是8,需要等到Finalizer线程把ReferenceQueue的对象对于的finalize方法执行完,还要等到下次gc时,才能把该对象回收,导致创建链接的这些对象在年轻代不能立即回收,从而进入了老年代,这也是为啥老年代会一直缓慢增长的问题。

7.4 日志

高并发下,特别是 Netty 的 IO 线程除了要执行该线程上的 IO 读写操作,还有执行异步任务和定时任务,如果 IO 线程处理不过来队列里的任务,很有可能导致新进来异步任务出现被拒绝的情况。

那什么情况下可能呢?IO 是异步读写的问题不大,就是多耗点 CPU,最有可能 block 住 IO 线程的是我们打的日志。

目前 Log4j 的 ConsoleAppender 日志 immediateFlush 属性默认为 true,即每次打 log 都是同步写 flush 到磁盘的,这个对于内存操作来说,慢了很多。

同时 AsyncAppender 的日志队列满了也会 block 住线程,log4j 默认的 buffer 大小是 128,而且是 block 的。

即如果 buffer 的大小达到 128,就阻塞了写日志的线程,在并发写日志量大的的情况下,特别是堆栈很多时,log4j 的 Dispatcher 线程会出现变慢要刷盘。

这样 buffer 就不能快速消费,很容易写满日志事件,导致 Netty IO 线程 block 住,所以我们在打日志时,也要注意精简。

8、未来规划

现在我们都是基于 HTTP/1,现在 HTTP/2 相对于 HTTP/1 关键实现了在连接层面的服务,即一个连接上可以发送多个 HTTP 请求。

即 HTTP 连接也能和 RPC 连接一样,建几个连接就可以了,彻底解决了 HTTP/1 连接不能复用导致每次都建连和慢启动的开销。

我们也在基于 Netty 升级到 HTTP/2,除了技术升级外,我们对监控报警也一直在持续优化,怎么提供给业务方准确无误的报警,也是一直在努力。

还有一个就是降级,作为统一接入网关,和业务方做好全方位的降级措施,也是一直在完善的点,保证全站任何故障都能通过网关第一时间降级,也是我们的重点。

9、写在最后

网关已经是一个互联网公司的标配,这里总结实践过程中的一些心得和体会,希望给大家一些参考以及一些问题的解决思路,我们也还在不断完善中,同时我们也在做多活的项目,欢迎交流。

附录:更多相关资料

[1] NIO异步网络编程资料:

Java新一代网络编程模型AIO原理及Linux系统AIO介绍

有关“为何选择Netty”的11个疑问及解答

MINA、Netty的源代码(在线阅读版)已整理发布

详解Netty的安全性:原理介绍、代码演示(上篇)

详解Netty的安全性:原理介绍、代码演示(下篇)

详解Netty的优雅退出机制和原理

NIO框架详解:Netty的高性能之道

Twitter:如何使用Netty 4来减少JVM的GC开销(译文)

绝对干货:基于Netty实现海量接入的推送服务技术要点

新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析

写给初学者:Java高性能NIO框架Netty的学习方法和进阶策略

少啰嗦!一分钟带你读懂Java的NIO和经典IO的区别

史上最强Java NIO入门:担心从入门到放弃的,请读这篇!

手把手教你用Netty实现网络通信程序的心跳机制、断线重连机制

Java的BIO和NIO很难懂?用代码实践给你看,再不懂我转行!

史上最通俗Netty框架入门长文:基本介绍、环境搭建、动手实战

长连接网关技术专题(一):京东京麦的生产级TCP网关技术实践总结

长连接网关技术专题(五):喜马拉雅自研亿级API网关技术实践

>> 更多同类文章 ……

[2] 有关IM架构设计的文章:

浅谈IM系统的架构设计

简述移动端IM开发的那些坑:架构设计、通信协议和客户端

一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

一套原创分布式即时通讯(IM)系统理论架构方案

从零到卓越:京东客服即时通讯系统的技术架构演进历程

蘑菇街即时通讯/IM服务器开发之架构选择

腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT

如何解读《微信技术总监谈架构:微信之道——大道至简》

快速裂变:见证微信强大后台架构从0到1的演进历程(一)

移动端IM中大规模群消息的推送如何保证效率、实时性?

现代IM系统中聊天消息的同步和存储方案探讨

微信朋友圈千亿访问量背后的技术挑战和实践总结

腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面

以微博类应用场景为例,总结海量社交系统的架构设计步骤

子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践

一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

社交软件红包技术解密(一):全面解密QQ红包技术方案——架构、技术实现等

即时通讯新手入门:一文读懂什么是Nginx?它能否实现IM的负载均衡?

从游击队到正规军(一):马蜂窝旅游网的IM系统架构演进之路

从游击队到正规军(二):马蜂窝旅游网的IM客户端架构演进和实践总结

从游击队到正规军(三):基于Go的马蜂窝旅游网分布式IM系统技术实践

瓜子IM智能客服系统的数据架构设计(整理自现场演讲,有配套PPT)

阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处

微信后台基于时间序的新一代海量数据存储架构的设计实践

IM开发基础知识补课(九):想开发IM集群?先搞懂什么是RPC!

阿里技术分享:电商IM消息平台,在群聊、直播场景下的技术实践

一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等

一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等

从新手到专家:如何设计一套亿级消息量的分布式IM系统

>> 更多同类文章 ……

[3] 更多其它架构设计相关文章:

腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面

快速理解高性能HTTP服务端的负载均衡技术原理

子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践

知乎技术分享:从单机到2000万QPS并发的Redis高性能缓存实践之路

新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践

阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史

阿里技术分享:阿里自研金融级数据库OceanBase的艰辛成长之路

达达O2O后台架构演进实践:从0到4000高并发请求背后的努力

优秀后端架构师必会知识:史上最全MySQL大表优化方案总结

小米技术分享:解密小米抢购系统千万高并发架构的演进和实践

一篇读懂分布式架构下的负载均衡技术:分类、原理、算法、常见方案等

通俗易懂:如何设计能支撑百万并发的数据库架构?

多维度对比5款主流分布式MQ消息队列,妈妈再也不担心我的技术选型了

从新手到架构师,一篇就够:从100到1000万高并发的架构演进之路

美团技术分享:深度解密美团的分布式ID生成算法

12306抢票带来的启示:看我如何用Go实现百万QPS的秒杀系统(含源码)

>> 更多同类文章 ……

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入。同步发布链接是:http://www.52im.net/thread-3564-1-1.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK