「转载」喜马拉雅自研网关架构演进过程

时光斑驳了记忆 2021-02-22 17:51:56 ⋅ 574 阅读

背景

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

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

网关除了要实现最基本的功能反向代理外,还有公有特性,比如黑白名单,流控,鉴权,熔断,API 发布,监控和报警等,我们还根据业务方的需求实现了流量调度,流量 Copy,预发布,智能化升降级,流量预热等相关功能,下面就我们网关在这些方便的一些实践经验以及发展历程,下面是喜马拉雅网关的演化过程:

「转载」喜马拉雅自研网关架构演进过程

 

第一版 Tomcat nio + AsyncServlet

网关在架构设计时最为关键点,就是网关在接收到请求,调用后端服务时不能阻塞 Block,否则网关的吞吐量很难上去,因为最耗时的就是调用后端服务这个远程调用过程,如果这里是阻塞的,Tomcat 的工作线程都 block 主了,在等待后端服务响应的过程中,不能去处理其他的请求,这个地方一定要异步

架构图如下:

「转载」喜马拉雅自研网关架构演进过程

 

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

但是这版只是功能上达到网关的要求,处理能力很快就成了瓶颈,单机 qps 到 5k 的时候,就会不停的 full gc,后面通过 dump 线上的堆分析,发现全是 Tomcat 缓存了很多 HTTP 的请求,因为 Tomcat 默认会缓存 200 个 requestProcessor,每个 prcessor 都关联了一个request,还有就是 Servlet 3.0 Tomcat 的异步实现会出现内存泄漏,后面通过减少这个配置,效果明显。但性能肯定就下降了,总结了下,基于 Tomcat 做为接入端,有如下几个问题:

Tomcat 自身的问题

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

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

「转载」喜马拉雅自研网关架构演进过程

 

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

HttpNioClient的问题

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

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

第二版 Netty + 全异步

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

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

「转载」喜马拉雅自研网关架构演进过程

 

接入层

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

对 HTTP 协议的编解码做了优化,对异常,攻击性请求监控可视化。比如我们对 HTTP 的请求行和请求头大小是有限制的,Tomcat 是请求行和请求加在一起,不超过 8k,Netty 是分别有大小限制。假如客户端发送了超过阀值的请求,带 cookie 的请求很容易超过,正常情况下,Netty 就直接响应 400 给客户端。

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

业务逻辑层

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

在业界和一些大厂的网关设计中,业务逻辑层基本都是设计成责任链模式,公有的业务逻辑也在这层实现,我们在这层也是相同的套路,支持了:

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

上面提到的这么多都是对流量的治理,我们每个功能都是一个 filter,处理失败都不影响转发流程,而且所有的这些规则的元数据在网关启动时就会全部初始化好。在执行的过程中,不会有 IO 操作,目前有些设计会对多个 filter 做并发执行,由于我们的都是内存操作,开销并不大,所以我们目前并没有支持并发执行。

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

服务调用层

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

异步 Push

网关在发起服务调用后,让工作线程继续处理其他的请求,而不需要等待服务端返回,这里的设计是我们为每个请求都会创建一个上下文,我们在发完请求后,把该请求的 context 绑定到对应的连接上,等 Netty 收到服务端响应时,就会在给连接上执行 read 操作。

解码完后,再从给连接上获取对应的 context,通过 context 可以获取到接入端的 session,这样 push 就通过 session 把响应写回客户端了,这样设计也是基于 HTTP 的连接是独占的,即连接和请求上下文绑定。

连接池

连接池的原理如下图:

「转载」喜马拉雅自研网关架构演进过程

 

服务调用层除了异步发起远程调用外,还需要对后端服务的连接进行管理,HTTP 不同于 RPC,HTTP 的连接是独占的,所以在释放的时候要特别小心,一定要等服务端响应完了才能释放,还有就是连接关闭的处理也要小心,总结如下几点:

  • Connection:close
  • 空闲超时,关闭连接
  • 读超时关闭连接
  • 写超时,关闭连接
  • Fin,Reset

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

Connection:close

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

还有就是如果端上的请求带了 connection:close,那 Tomcat 就不等这个连接重用到 100 次,即一次就关闭,通过在响应头里添加 Connection:close,即成了短连接,这个在和 Tomcat 保持长连接时,需要注意的,如果要利用,就要主动 remove 掉这个 close 头。

写超时

首先网关什么时候开始计算服务的超时时间,如果从调用 writeAndFlush 开始就计算,这其实是包含了 Netty 对 HTTP 的 encode 时间和从队列里把请求发出去即 flush 的时间,这样是对后端服务不公平的,所以需要在真正 flush 成功后开始计时,这样是和服务端最接近的,当然还包含了网络往返时间和内核协议栈处理的时间,这个不可避免,但基本不变。

所以我们是 flush 成功回调后开始启动超时任务,这里就有个注意的地方,如果 flush 不能快速回调,比如来了一个大的 post 请求,body 部分比较大,而 Netty 发送的时候第一次默认是发1k的大小,如果还没有发完,则增大发送的大小继续发,如果在 Netty 在 16 次后还没有发送完成,则不会再继续发送,而是提交一个 flushTask 到任务队列,待下次执行到后再发送。

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

全链路超时机制

下面是我们在整个链路超时处理的机制。

「转载」喜马拉雅自研网关架构演进过程

 

  • 协议解析超时
  • 等待队列超时
  • 建连超时
  • 等待连接超时
  • 写前检查是否超时
  • 写超时
  • 响应超时

监控报警

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

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

协议层

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

应用层

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

性能优化实践

对象池技术

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

上下文切换

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

「转载」喜马拉雅自研网关架构演进过程

 

我们整个网关没有涉及到 IO 操作,但我们在业务逻辑这块还是和 Netty 的 IO 编解码线程异步,是有两个原因,1)是防止开发写的代码有阻塞,2)是业务逻辑打日志可能会比较多,在突发的情况下,在 push 线程时,支持用 Netty 的 IO 线程替代,这里做的工作比较少,这里有异步修改为同步后(通过修改配置调整),CPU 的上下文切换减少 20%,进而提高了整体的吞吐量,就是不能为了异步而异步,zull2 的设计和我们的类似,

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 时,才能把该对象回收,导致创建连接的这些对象在年轻代不能立即回收,从而进入了老年代,这也是为啥老年代会一直缓慢增长的问题。

日志

高并发下,特别是 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 住,所以我们在打日志时,也要注意精简。

未来规划

现在我们都是基于 HTTP/1,现在 HTTP/2 相对于 HTTP/1 关键实现了在连接层面的服务,即一个连接上可以发送多个 HTTP 请求,即 HTTP 连接也能和 rpc 连接一样,建几个连接就可以了,彻底解决了 HTTP/1 连接不能复用导致每次都建连和慢启动的开销。

我们也在基于 Netty 升级到 HTTP/2, 除了技术升级外,我们对监控报警也一直在持续优化,怎么提供给业务方准确无误的报警,也是一直在努力,还有一个就是降级,作为统一接入网关,和业务方做好全方位的降级措施,也是一直在完善的点,保证全站任何故障都能通过网关第一时间降级,也是我们的重点。

总结

网关已经是一个互联网公司的标配,这里总结实践过程中的一些心得和体会,希望给大家一些参考以及一些问题的解决思路,欢迎交流,我们也还在不断完善中,同时我们也在做多活,云原生,稳定性平台等项目,喜马拉雅平台架构有机会有挑战,目前正在大力招揽人才,感兴趣的同学可以加入我们。目前正在招揽 Java 相关的人才,资深和架构都有,感兴趣的同学可以加入我们,请微信联系 @yzb1102


全部评论: 0

    我有话说:

    阿里技术:聊一聊从单机至亿级流量大型网站系统架构演进过程

    网站的初期,我们经常会在单机上跑我们所有的程序和软件。此时我们使用一个容器,如tomcat、jetty、jboos,然后直接使用JSP/servlet技术......

    「轻阅读」使用 Angular 打造微前端架构的 ToB 企业级应用

    转载:https://www.cnblogs.com/worktile/p/11940244.html

    「轻阅读」图文并茂带你了解分布式架构演进

    初始阶段架构初始阶段 的小型系统 应用程序、数据库、文件等所有的资源都在一台服务器上通俗称LAMP

    「推荐」通过API实现微服务管控-限流,熔断和降级

      今天准备谈下基于API来实现微服务治理管控中的服务限流,熔断和降级方面的内容。在前面谈微服务架构的时候也谈到过类似通过Hystrix,Sentinel来是服务限流熔断。包括也不断

    kongx v2.0.0 发布, kong 可视化管理平台

    kongx v2.0.0 已经发布。kongx是 kong 的可视化界面管理平台(参考 konga 的部分界面布局方式),能够集中化管理应用不同环境的配置,提供同步各环境的配置功能,并且

    腾讯高吞吐消息队列组件TubeMQ升级为 TubeHub

    TubeMQ简介 TubeMQ 项目始于 2013 年,是腾讯的高吞吐消息队列组件。项目团队于 2019 年将 TubeMQ 捐赠给 Apache 基金会,成为腾讯首个被 Apache 基金会

    国产 servlet 容器,smart-servlet 体验版发布

    smart-servlet 是一款实现了Servlet 3.1规范,支持多应用隔离部署的的 Web 容器。除此之外,smart-servlet 还是一款插件化容器,用户可以通过开发定义插件扩展容器

    转载」蘑菇街消息系统上云实践

    小编又来啦~本周要推荐给大家的是一篇跟中间件上云相关的技术文章,这里面详细的记录了,蘑菇街消息系统上云的全过程,也是市面上开放出来为数不多的企业组件上云实践。有相关需求的同学可以好好学习下

    Apache APISIX 2.4 发布,云原生的微服务 API

    Apache APISIX 2.4 已经发布。Apache APISIX 是一个云原生微服务 API ,它提供了高性能、安全、开源和可扩展的平台,基于 Nginx 和 etcd,支持动态路由和插

    微服务架构:搭建网站扫码登录的功能设计

    微信扫码登录大家都是应用比较多的登录方式了,现在大的购物网站像京东、淘宝等都支持使用APP扫码登录网站了。今天就用APP扫码登录网站的实例来举例说明微服务架构的搭建过程

    「轻阅读」微博推荐架构演进

    微博两个核心基础点:一是用户关系构建,二是内容传播,微博推荐一直致力于优化这两点,促进微博发展。

    京东技术:京东系统架构师如何让笨重的架构变得灵巧

    京东系统架构师,从事架构设计与开发工作,熟悉各种开源软件架构。在Web开发、架构优化上有较丰富实战经历。

    WeCube 2.7.1 发布,一站式 IT 架构管理和运维管理工具

    WeCube简介 微众银行在分布式架构实践的过程中,发现将银行核心系统构建于分布式架构之上,会遇到一些与传统单体应用不同的痛点(例如,服务器增多,部署难度大;调用链长,全链路跟踪困难; 系统复杂

    架构实战篇:认识一下微服务架构

    微服务是一个新兴的软件架构,就是把一个大型的单个应用程序和服务拆分为数十个的支持微服务。

    微服务架构下的若干常用设计模式

    在我们选择了用微服务架构来设计、交付数字化应用后,因微服务架构本身所带来的一些共性问题。

    阿里技术:底向上构建知识图谱全过程

    知识图谱的构建技术主要有自顶向下和底向上两种。其中自顶向下构建是指借助百科类网站等结构化数据源,从高质量数据中提取本体和模式信息,加入到知识库里。

    架构实战篇:一个可供中小团队参考的微服务架构技术栈

    作者近年一直在一线互联网公司(携程,拍拍贷等)开展微服务架构实践,根据我个人的一线实践经验和我平时对Spring Cloud的调研,我认为Spring Cloud技术栈中的有些组件离生产级开发尚有

    《CSS 揭秘》译者,百姓前端架构 TL 带来更多干货,等你来撩~~~

    春节小长假已经过完了,《IT实战联盟》 也开始准备更新更多优质的实战资源帮助大家解决工作中的实际问题。

    创业团队如何设计支撑百万并发的数据库架构

    我们来聊一下对于一个支撑日活百万用户的高并系统,他的数据库架构应该如何设计?