您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

日200亿次调用,喜马拉雅网关的架构设计

时间:2023-09-14 15:21:32  来源:微信公众号  作者:技术老男孩

本文目录

- 说在前面

- 喜马拉雅自研亿级API网关技术实践

- 1、第1版:Tomcat NIO+Async Servlet

- 2、第2版.NETty+全异步

  - 2.1 接入层

  - 2.2 业务逻辑层

  - 2.3 服务调用层

    - 2.3.1 异步 Push

    - 2.3.2 连接池

    - 2.3.3 Connection:close

    - 2.3.4 写超时

- 3、全链路超时机制

- 4、监控报警

- 5、性能优化实践

  - 5.1 对象池技术

  - 5.2 上下文切换

  - 5.3 GC优化

  - 5.4 日志

- 6、未来规划

- 说在最后:有问题可以找老架构取经

- 部分历史案例

喜马拉雅自研亿级API网关技术实践

网关作为一种发展较为完善的产品,各大互联网公司普遍采用它作为中间件,以应对公共业务需求的不断浮现,并能迅速迭代更新。

如果没有网关,要更新一个公共特性,就得推动所有业务方都进行更新和发布,这无疑是效率极低的。然而,有了网关之后,这一切都不再是问题。

喜马拉雅也如此,用户数量已增长到 6 亿级别,Web 服务数量超过 500 个,目前我们的网关每天处理超过 200 亿次的调用,单机 QPS 峰值可达 4w+。

除了实现基本的反向代理功能,网关还具备许多公共特性,如黑白名单、流量控制、身份验证、熔断、API 发布、监控和报警等。根据业务方的需求,我们还实现了流量调度、流量复制、预发布、智能升级、流量预热等相关功能。

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

注意:请点击图像以查看清晰的视图!

本文将介绍在喜马拉雅 API 网关面临亿级流量的情况下,我们如何进行技术演进,以及我们的实践经验总结。

1、第1版:Tomcat NIO+Async Servlet

在架构设计中,网关的关键之处在于接收到请求并调用后端服务时,不能发生阻塞(Block),否则网关的处理能力将受到限制。

这是因为最耗时的操作就是远程调用后端服务这个过程。

如果此处发生阻塞,Tomcat 的工作线程会被全部 block 住了,等待后端服务响应的过程中无法处理其他请求,因此这里必须采用异步处理。

架构图如下

注意:请点击图像以查看清晰的视图!

在这个版本中,我们实现了一个单独的 Push 层,用于在网关接收到响应后,响应客户端,并通过此层实现与后端服务的通信。

该层使用的是 HttpNioClient,支持业务功能包括黑白名单、流量控制、身份验证、API 发布等。

然而,这个版本仅在功能上满足了网关的要求,处理能力很快成为瓶颈。当单机 QPS 达到 5K 时,会频繁发生 Full GC。

通过分析线上堆,我们发现问题在于 Tomcat 缓存了大量 HTTP 请求。因为 Tomcat 默认会缓存 200 个 requestProcessor,每个处理器都关联一个 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 作为接入层和服务调用层,也就是我们的第二版,成功地解决了上述问题,实现了理想的性能。

2、第2版:Netty+全异步

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

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

注意:请点击图像以查看清晰的视图!

2.1 接入层

Netty 的 IO 线程主要负责 HTTP 协议的编解码工作,同时也监控并报警协议层面的异常情况。

我们对 HTTP 协议的编解码进行了优化,并对异常和攻击性请求进行了监控和可视化处理。

例如,我们对 HTTP 请求行和请求头的大小都有限制,而 Tomcat 是将请求行和请求头一起计算,总大小不超过 8K,而 Netty 是分别对两者设置大小限制。

如果客户端发送的请求超过了设定的阀值,带有 cookie 的请求很容易超过这个限制,一般情况下,Netty 会直接响应 400 给客户端。

在优化后,我们只取正常大小的部分,并标记协议解析失败,这样在业务层就可以判断出是哪个服务出现了这类问题。

对于其他攻击性的请求,例如只发送请求头而不发送 body 或者只发送部分内容,都需要进行监控和报警。

2.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 操作时影响业务线程。

2.3 服务调用层

服务调用对于代理网关服务非常关键,这个环节,性能必须很高:必须采用异步方式,

我们利用 Netty 实现了这一目标,同时也充分利用了 Netty 提供的连接池,实现了获取和释放的无锁操作。

2.3.1 异步 Push

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

在这个设计中,我们为每个请求创建一个上下文,发送请求后,将该请求的 context 绑定到相应的连接上,当 Netty 收到服务端响应时,会在连接上执行 read 操作。

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

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

2.3.2 连接池

连接池的原理如下图

注意:请点击图像以查看清晰的视图!

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

HTTP 与 RPC 不同,HTTP 连接是独占的,所以在释放连接时需要特别小心,必须等待服务端响应完成后才能释放,此外,连接关闭的处理也需要谨慎。

总结如下几点

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

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

2.3.3 Connection:close

后端服务采用的是 Tomcat,它对连接的重用次数有规定,默认为 100 次。

当达到 100 次限制时,Tomcat 会在响应头中添加 Connection:close,要求客户端关闭该连接,否则再次使用该连接发送请求会出现 400 错误。

还有就是如果前端的请求带了 connection:close,那 Tomcat 就不会等待该连接重用满 100 次,即一次就关闭连接。

在响应头中添加 Connection:close 后,连接变为短连接。

在与 Tomcat 保持长连接时,需要注意这一点,如果要利用该连接,需要主动移除 close 头。

2.3.4 写超时

首先,网关在何时开始计算服务的超时时间?

如果从调用 writeAndFlush 开始计算,实际上包含了 Netty 对 HTTP 的编码时间和从队列中发送请求即 flush 的时间,这样对后端服务不公平。

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

因此,我们在 flush 成功回调后启动超时任务。

需要注意的是:如果 flush 不能快速回调,例如遇到一个大的 POST 请求,body 部分较大,而 Netty 发送时默认第一次只发送 1k 大小。

如果尚未发送完毕,会增大发送大小继续发送,如果在 Netty 发送 16 次后仍未发送完成,将不再继续发送,而是提交一个 flushTask 到任务队列,待下次执行后再发送。

此时,flush 回调时间较长,导致此类请求无法及时关闭,后端服务 Tomcat 会一直阻塞在读取 body 部分,基于上述分析,我们需要设置写超时,对于大的 body 请求,通过写超时及时关闭连接。

3、全链路超时机制

注意:请点击图像以查看清晰的视图!

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

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

4、监控报警

对于网关的业务方来说,他们能看到的是监控和警报功能,我们能够实现秒级的报警和监控,将监控数据定时上传到我们的管理系统,由管理系统负责汇总统计并存储到 InfluxDB 中。

我们对 HTTP 协议进行了全面的监控和警报,涵盖了协议层和服务层的问题。

协议层

  • 1)针对攻击性请求,只发送头部,不发送或只发送部分 body,我们会进行采样并记录,还原现场,并触发警报;
  • 2)对于 Line 或 Head 或 Body 过大的请求,我们会进行采样记录,还原现场,并及时发出警报。

应用层

  • 1)监控耗时:包括慢请求,超时请求,以及 tp99,tp999 等;

  • 2)监控 OPS:并及时发出警报;

  • 3)带宽监控和报警:支持对请求和响应的行,头,body 单独监控;

  • 4)响应码监控:特别是 400,和 404;

  • 5)连接监控:我们对接入端的连接,以及与后端服务的连接,以及后端服务连接上待发送字节大小都进行了监控;

  • 6)失败请求监控

  • 7)流量抖动报警:这是非常必要的,流量抖动可能是出现问题,或者是问题即将出现的预兆。

总体架构

注意:请点击图像以查看清晰的视图!

5、性能优化实践

5.1 对象池技术

针对高并发系统,不断地创建对象不仅会占用内存资源,还会对垃圾回收过程产生压力。

为了解决这个问题,我们在实现过程中会对诸如线程池的任务、StringBuffer 等频繁使用的对象进行重用,从而降低内存分配的开销。

5.2 上下文切换

在高并发系统中,通常会采用异步设计。异步化后,线程上下文切换的问题必须得到关注。

我们的线程模型如下

注意:请点击图像以查看清晰的视图!

我们的网关没有涉及 I/O 操作,但在业务逻辑处理方面仍然采用了 Netty 的 I/O 编解码线程异步方式。

这主要有两个原因

  • 1)防止开发人员编写的代码出现阻塞现象;

  • 2)在突发情况下,业务逻辑可能会产生大量的日志记录,我们允许在推送线程时使用 Netty 的 I/O 线程作为替代。这种做法可以减少 CPU 上下文切换的次数,从而提高整体吞吐量。我们不能仅仅为了异步而异步,Zuul2 的设计理念与我们的做法相似。

5.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 线程处理的,其优先级不高,默认为 8。它需要等待 Finalizer 线程把 ReferenceQueue 的对象对应的 finalize 方法执行完,并等到下次垃圾回收时,才能回收该对象。这导致创建链接的这些对象在年轻代不能立即回收,从而进入了老年代,这也是老年代持续缓慢增长的原因。

5.4 日志

在高并发系统中,尤其是 Netty 的 I/O 线程,除了执行 I/O 读写操作外,还需执行异步任务和定时任务。如果 I/O 线程处理不过队列中的任务,可能会导致新进来的异步任务被拒绝。

在什么情况下可能会出现这种情况呢?异步读写问题不大,主要是多耗点 CPU。最有可能阻塞 I/O 线程的是日志记录。目前 Log4j 的 ConsoleAppender 日志 immediateFlush 属性默认为 true,即每次记录日志都是同步写入磁盘,这对于内存操作来说,速度较慢。

同时,AsyncAppender 的日志队列满了也会阻塞线程。Log4j 默认的 buffer 大小是 128,而且是阻塞的。即当 buffer 大小达到 128 时,会阻塞写日志的线程。在并发写日志量较大且堆栈较深的情况下,Log4j 的 Dispatcher 线程可能会变慢,需要刷盘。这样 buffer 就不能快速消费,很容易写满日志事件,导致 Netty I/O 线程被阻塞。因此,在记录日志时,我们需要注意精简。

6、未来规划

目前,我们都在使用基于 HTTP/1 的协议。

相对于 HTTP/1,HTTP/2 在连接层面实现了服务,即在一个连接上可以发送多个 HTTP 请求。

这就意味着 HTTP 连接可以像 RPC 连接一样,建立几个连接即可,完全解决了 HTTP/1 连接无法复用导致的重复建连和慢启动的开销。

我们正在基于 Netty 升级到 HTTP/2,除了技术升级外,我们还在不断优化监控报警,以便为业务方提供准确无误的报警。此外,我们还在作为统一接入网关与业务方实施全面的降级措施,以确保全站任何故障都能通过网关第一时间降级,这也是我们的重点工作。



Tags:架构设计   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
京东小程序数据中心架构设计与最佳实践
一、京东小程序是什么京东小程序平台能够提供开放、安全的产品,成为品牌开发者链接京东内部核心产品的桥梁,致力于服务每一个信任我们的外部开发者,为不同开发能力的品牌商家提...【详细内容】
2024-03-27  Search: 架构设计  点击:(9)  评论:(0)  加入收藏
高并发架构设计(三大利器:缓存、限流和降级)
软件系统有三个追求:高性能、高并发、高可用,俗称三高。本篇讨论高并发,从高并发是什么到高并发应对的策略、缓存、限流、降级等。引言1.高并发背景互联网行业迅速发展,用户量剧...【详细内容】
2024-03-13  Search: 架构设计  点击:(6)  评论:(0)  加入收藏
如何判断架构设计的优劣?
架构设计的基本准则是非常重要的,它们指导着我们如何构建可靠、可维护、可测试的系统。下面是这些准则的转换表达方式:简单即美(KISS):KISS原则的核心思想是保持简单。在设计系统...【详细内容】
2024-02-20  Search: 架构设计  点击:(36)  评论:(0)  加入收藏
听说你会架构设计?
一个漂泊江湖多年的 985 非科班程序员,曾混迹于国企、互联网大厂和创业公司的后台开发攻城狮。1. 引言当我那天拿着手机,正在和朋友们的微信群里畅聊着八卦新闻和即将到来的周...【详细内容】
2023-12-15  Search: 架构设计  点击:(150)  评论:(0)  加入收藏
云原生数据库 GaiaDB 架构设计解析:高性能、多级高可用
百度智能云团队在今年 11-12 月特别推出了四期《百度智能云数据库》系列云智公开课,为大家全面地介绍了以云原生数据库 GaiaDB 和分布式数据库 GaiaDB-X 为代表的百度智能云...【详细内容】
2023-12-13  Search: 架构设计  点击:(119)  评论:(0)  加入收藏
架构设计的目的
关于架构设计的目的,存在一些常见的误解。首先,仅仅因为架构很重要,并不意味着必须进行架构设计。重要的是理解架构为何重要。例如,有人可能认为没有架构设计,系统就无法运行,但这...【详细内容】
2023-12-06  Search: 架构设计  点击:(195)  评论:(0)  加入收藏
10个必知必会的云原生架构设计模式
在构建云原生应用程序时,采用了一些不同的软件架构方法。云原生应用程序通常采用微服务架构,以最大程度地利用云计算模型的优势。这些应用程序需要能够在动态编排和容器化环境...【详细内容】
2023-12-04  Search: 架构设计  点击:(153)  评论:(0)  加入收藏
如何做好架构设计,架构设计有章可循吗?
设计一个系统的过程,就是建造一座大厦的过程,架构设计的质量直接决定了大厦的质量。在我们进行系统的架构设计时,总是会遇到一系列的问题,比如一个大型系统的架构应该如何起步,从...【详细内容】
2023-11-24  Search: 架构设计  点击:(300)  评论:(0)  加入收藏
云计算时代下的系统技术架构设计与实践
随着云计算技术的快速发展,越来越多的企业和组织将其业务和应用迁移到云平台上。云计算为用户提供了弹性的计算资源、灵活的服务模式和高效的运维管理,成为了当今信息技术领域...【详细内容】
2023-11-16  Search: 架构设计  点击:(127)  评论:(0)  加入收藏
听说你会架构设计?来,弄一个群聊系统
群聊系统是社交应用的核心功能之一,每个社交产品几乎都有着群聊系统的身影:包括但不限于 QQ、微信、抖音、小红书等。上述介绍的技术细节可能只是群聊系统的冰山一角,像常见的...【详细内容】
2023-11-08  Search: 架构设计  点击:(212)  评论:(0)  加入收藏
▌简易百科推荐
对于微服务架构监控应该遵守的原则
随着软件交付方式的变革,微服务架构的兴起使得软件开发变得更加快速和灵活。在这种情况下,监控系统成为了微服务控制系统的核心组成部分。随着软件的复杂性不断增加,了解系统的...【详细内容】
2024-04-03  步步运维步步坑    Tags:架构   点击:(5)  评论:(0)  加入收藏
大模型应用的 10 种架构模式
作者 | 曹洪伟在塑造新领域的过程中,我们往往依赖于一些经过实践验证的策略、方法和模式。这种观念对于软件工程领域的专业人士来说,已经司空见惯,设计模式已成为程序员们的重...【详细内容】
2024-03-27    InfoQ  Tags:架构模式   点击:(13)  评论:(0)  加入收藏
哈啰云原生架构落地实践
一、弹性伸缩技术实践1.全网容器化后一线研发的使用问题全网容器化后一线研发会面临一系列使用问题,包括时机、容量、效率和成本问题,弹性伸缩是云原生容器化后的必然技术选择...【详细内容】
2024-03-27  哈啰技术  微信公众号  Tags:架构   点击:(10)  评论:(0)  加入收藏
DDD 与 CQRS 才是黄金组合
在日常工作中,你是否也遇到过下面几种情况: 使用一个已有接口进行业务开发,上线后出现严重的性能问题,被老板当众质疑:“你为什么不使用缓存接口,这个接口全部走数据库,这怎么能扛...【详细内容】
2024-03-27  dbaplus社群    Tags:DDD   点击:(11)  评论:(0)  加入收藏
高并发架构设计(三大利器:缓存、限流和降级)
软件系统有三个追求:高性能、高并发、高可用,俗称三高。本篇讨论高并发,从高并发是什么到高并发应对的策略、缓存、限流、降级等。引言1.高并发背景互联网行业迅速发展,用户量剧...【详细内容】
2024-03-13    阿里云开发者  Tags:高并发   点击:(6)  评论:(0)  加入收藏
如何判断架构设计的优劣?
架构设计的基本准则是非常重要的,它们指导着我们如何构建可靠、可维护、可测试的系统。下面是这些准则的转换表达方式:简单即美(KISS):KISS原则的核心思想是保持简单。在设计系统...【详细内容】
2024-02-20  二进制跳动  微信公众号  Tags:架构设计   点击:(36)  评论:(0)  加入收藏
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  ijunfu  今日头条  Tags:SpringBoot   点击:(10)  评论:(0)  加入收藏
PHP+Go 开发仿简书,实战高并发高可用微服务架构
来百度APP畅享高清图片//下栽のke:chaoxingit.com/2105/PHP和Go语言结合,可以开发出高效且稳定的仿简书应用。在实现高并发和高可用微服务架构时,我们可以采用一些关键技术。首...【详细内容】
2024-01-14  547蓝色星球    Tags:架构   点击:(115)  评论:(0)  加入收藏
GraalVM与Spring Boot 3.0:加速应用性能的完美融合
在2023年,SpringBoot3.0的发布标志着Spring框架对GraalVM的全面支持,这一支持是对Spring技术栈的重要补充。GraalVM是一个高性能的多语言虚拟机,它提供了Ahead-of-Time(AOT)编...【详细内容】
2024-01-11    王建立  Tags:Spring Boot   点击:(124)  评论:(0)  加入收藏
Spring Boot虚拟线程的性能还不如Webflux?
早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。测试场景作者采用了一个尽可...【详细内容】
2024-01-10  互联网架构小马哥    Tags:Spring Boot   点击:(115)  评论:(0)  加入收藏
站内最新
站内热门
站内头条