如何轻松打造百亿流量API网关?看这一篇就够了(上)
上篇整体描述了网关的背景,涉及职能、分类、定位环节,本篇进入本文的重点,将会具体谈下百亿级流量API网关的演进过程。
准备好瓜子花生小板凳开始积累技能点吧~
一个每天百亿级流量的API网关实现过程是不断演进的,其中驱动的内核是流量的增长和业务接入的多样性,下面带大家来看下百亿级流量API网关在不同阶段能力建设的取舍与权衡之道。
青铜期
青铜期是网关夯实基础的阶段,这一阶段网关流量规模并不大,增长趋势不是很明显。基础能力建设是本阶段的主题。
如果剖析一个API网关的所有功能,割舍掉不必要的功能,只留3个核心功能,你会如何取舍呢?
作为笔者,相信只会留下这个三个功能:协议适配、服务编排、服务注册,它们代表着一个请求从上游到下游的整个生命周期。
协议适配
网关会透传下游上千个服务,而这些服务一定不是每一个都需要网关去开发配置的。我们通过一个协议适配的转换,让下游各种服务通过我们统一指定的协议,通过网关开放出去。
网关屏蔽了下游协议的差异性,下游可以专注于提供服务,而无需考虑协议的转换。
服务编排
业务在接入API网关后,在将请求透传到下游服务之前,中间请求的路由部分就涉及到服务编排。服务编排是网关能力的核心,一般会涉及如下场景
- API聚合能力。一次API请求可能会将下游多个服务聚合后才能得到完整的结果;
- 流量打标与灰度分发。常用于上游业务灰度场景,比如上游业务前期需要在某几个城市灰度进行功能验证,通过网关编排中的DSL能力对上流量ab标记一起传递给后端,或者以规则方式有选择的过滤流量;
- 插件化。网关设计一般会考虑采用pipeline架构解耦,像日志、业务限流、缓存、熔断等功能,应该以插件化的方式动态织入。
Robert Kowalski在一篇论文中提到
任何算法都会有两个部分,一个是Logic部分,这是用来解决实际问题的。另一个是 Control部分,这是用来决定用什么策略来解决问题。Logic部分是真正意义上的解决问题的算法,而Control部分只是影响解决这个问题的效率。程序运行的效率问题和程序的逻辑其实是没有关系的。我们认为,如果将Logic和Control部分有效地分开,那么代码就会变得更容易改进和维护。
”
注意,最后一句话是重点,如果将逻辑和控制部分有效分开,那么代码就变得更容易改进和维护。
评价一个服务编排能力的好坏等同于DSL能力的抽象程度,DSL的描述就是“Logic”。
服务注册
下游的API接口需要通过网关注册后对外进行发布,业务初期下游接入的服务并不多,可以采用纯手工的方式hard code维护下游API。
业务数量发展到一定程度,就要考虑API接入自动化的过程,需要一个管理平台能提供API完整的生命周期管理,包括API的创建、维护、发布、下线等,通过配置的方式即可开放内部服务API,方便上游业务高效接入。
白银期
白银期迎来流量和接入业务大规模的增长,安全稳定和性能是这一阶段的主题。
网关是如何保障接入业务安全稳定的?核心是流量治理,分上下游流控和业务流量隔离,具体怎么理解呢?
- 上下游流控涉及系统架构中的容错设计。针对上游的是全局性限流,针对下游的是业务粒度限流、重试、熔断、降级兜底等;
- 业务流量隔离,包含API维度的业务线程池隔离、KA业务集群隔离,应用层服务编排采用Pipline/Reactive架构是业务线程池隔离的前提,而在服务资源调度层通过单元化支持可以做到业务OWT维度独立集群部署。
那如何压榨单机性能,提升服务资源利用率呢?
- 在服务资源调用层引入弹性伸缩。一般网关流量请求分布符合高峰低谷的现象,在低峰期资源会空置,资源浪费较严重,通过弹性伸缩提升空置资源利用率。
- 应用层技术改造就涉及到全异步化和缓存建设
- 网关是IO密集型服务,全异步化的实施不但能提升服务的吞吐,进一步压榨单机性能,尤其涉及多IO任务编排场景,还能进一步降低RT,满足对RT高敏感业务方的诉求。另外在安全保障方面可以优雅的支持端到端线程池隔离,提升服务的稳定性。具体实施的手段是 Tomcat NIO + Servlet 3.0 + 服务Pipline架构 + 异步RPC;
- 只要谈到服务性能提升,引入缓存可能是大家的首选策略,毕竟像网关对RT要求比较高的场景,缓存的实施会带来可观的收益。在安全保障方面,服务触发降级兜底时还可以通过缓存做到数据降级。但是缓存的介入,需要考虑缓存数据淘汰策略及命中率提升手段。
安全稳定
流量治理
流量隔离
流量隔离前面简单提过,分应用层业务线程池级隔离和资源调度层KA业务集群隔离。
关于线程池流量隔离,以工程师视角来看待技术选型,可能反应式编程方式对这方面的支持会更优雅一些,毕竟像“回调地域”的复杂性对编码来说确实很痛苦,这也是引入Pipline/Reactive架构的要性之一。
那如何做好线程池隔离呢?
- 业务线程池隔离
- 一般存在几种场景会实施业务线程池隔离,如业务主导的需求、业务请求具备不定时大流量跑批行为、KA业务且对RT敏感等,与业务方沟通后,针对性的配置独立API粒度的线程池;
- 快慢线程池隔离,应用会对流量的执行时间采样统计,如果存在执行变慢的行为,会将其隔离到慢线程池。
关于KA业务独立集群隔离,这是单元化涉及的事情,单元化一方面具备业务请求就近接入能力,尽量减少跨单元调用,还具备独立集群容灾部署,帮助业务解决异地多活的能力,避免次要业务流量出现问题影响到核心链路流量。
入口端流控
入口端流控主要谈的是限流的概念,这地方具体指的是接入层的全局性限流,也就是前面提到的WAF的概念,其关注点是保护网关自身服务。
如果出现限流行为,这时候一般采取哪些决策处置呢?
- 拒绝服务。在系统还未分配充足资源时,把多出来的请求拒绝掉。
- 弹性伸缩。根据配置的规则,自动触发弹性扩容。除了弹性伸缩,还需要一个自动化发布系统,发布越快越好,否则,机器还没弹出几个,系统早已被压垮了。
- 如果扩容后资源还不足,就需要考虑资源分配策略
- 服务降级,让服务有足够的资源处理更多请求,降级可能会涉及到功能降级或者数据降级;
- 存在特权请求,比如大客户或核心链路业务等,优先保证特权请求正常服务。
出口端流控
出口端流控存在两个维度的概念,一是容错设计,保护下游,涉及业务粒度限流、降级、重试、熔断、缓存等能力建设,二是满足业务需求,涉及多API聚合、数据编排、分流等能力建设。满足业务需求维度更多体现在“黄金期”,后面会详细介绍,这里不过多赘述。
一个请求通过网关服务编排路由到下游,如果出现错误,可能会触发限流、重试、降级或者熔断,至于会触发哪种行为呢?其实有前后依赖关系的。
如果系统出现流量暴增,达到下游限制的速率,会触发网关内业务粒度限流行为,比如下游配额/负载不足。
限流可能跟重试相关联。但这地方需要明确好哪些情况可以重试。如调用超时、下游服务返回了某种可以重试的错误(如繁忙中、流控中、维护中、资源不足等)。
对于某些错误,则最好不要重试,比如:业务级的错误(如没有权限、或是非法数据等错误),技术上的错误(如HTTP的503等,这种原因可能是触发了代码的bug,重试下去没有意义)。
如果下游服务触发限流或者超时,重试几次后还未恢复则打开网关熔断。如果确认下游服务已挂掉,则直接打开熔断即可。
熔断时期,网关熔断器切成断开状态,对上游请求立即返回错误响应,而不调用下游服务。在一定时钟周期后或者探活程序感知下游服务已恢复,则网关熔断器切换成半开状态,允许上游部分流量去调用下游服务。如果这些请求对下游服务调用成功,那么可以认为之前导致调用失败的错误已经修正,此时熔断器切换到闭合状态,同时将错误计数器重置,这时候允许流量正常请求下游服务。
除了熔断,在等待下游服务恢复时期,网关还需做好降级兜底机制建设,一般采用数据降级方式,将流量降级到兜底数据源,兜底数据源一般是缓存或者其他下游服务提供方。
安全
安全,在这里侧重于接入层请求的安全,也就是前面提到的流量网关的概念,这地方不过多赘述。
再简单补充一下,关于鉴权,除了安全维度的概念,其实也跟网关服务治理能力相关,在无网关接入之前,服务治理工作会分散在各自服务,重复性高且效能较低,网关接入之后,一些服务治理工作推进统一交由网关收敛,反哺工程效能提升。
立体化监控
服务的健康状态是一个整体的概念,涉及到立体化监控体系的建设。
一个立体化的监控体系分不同层次指标的监控,包含服务调用链层、应用层、系统资源层,不同维度的指标侧重又不一样
- 系统资源层,侧重于使用率、饱和度以及错误数这三个维度的指标,也就是经常会谈到的USE法(Utilization Saturation and Errors)的概念,这三个维度的指标,涵盖了系统资源的常见性能瓶颈,所以常被用来快速定位系统资源的性能瓶颈;
- 使用率,表示资源用于服务的时间或容量百分比。100%的使用率,表示容量已经用尽或者全部时间都用于服务;
- 饱和度,表示资源的繁忙程度,通常与等待队列的长度相关。100%的饱和度,表示资源无法接受更多的请求;
- 错误数,表示发生错误的事件个数。错误数越多,表明系统的问题越严重。
- 应用层,侧重于请求数、错误率和响应时间这三个维度的指标,也就是RED法(Rate Errors Duration),这些指标不仅直接关系到用户的使用体验,还反映应用整体的可用性和可靠性。有了请求数、错误率和响应时间这三个黄金指标之后,我们就可以快速知道,应用是否发生了性能问题;
- 服务链路层。在一个分布式服务体系中,服务的健康状态不仅仅关注于服务自身的状态,还受限于下游调用链的影响。排查问题的思路也是自顶向下的策略,首先判断服务链路中是自身服务还是其他关联依赖服务引起的故障,如果判断是自身服务故障,接下来才进行应用层及系统资源层指标判断。至于如何定位调用链路拓扑root cause,那就是调用链跟踪做的事情。
那如何做一个好的监控系统呢?其实就是前面三层的指标串联
- 服务调用链跟踪。关于调用链跟踪,源于Google公开的Dapper paper,对应的开源实现是Zipkin;
- 服务调用时长分布。像Zipkin或者一些trace工具就可以看到一个服务调用链上的时间分布,有助于我们知道最耗时的服务是什么;
- 服务的Top N视图,主要关注服务错误率的Top N。一个系统请求的排名情况,一般按调用量排名、按请求最耗时排名、按热点排名(热点是一个时间段内请求次数的响应时间和);
- 依赖资源或者中间件操作关联。对于Java应用,可以通过JavaAgent字节码注入技术拿到JDBC执行数据库操作的执行时间;
- 服务资源关联。服务与运行服务机器节点的数据进行关联,节点数据一般包括CPU、MEM、I/O、DESK、NETWORK。关于这一维度的监控系统像Falcon、Zabbix等。
笔者前面也写过一篇文章,关于在大规模流量下如何实现一个实时智能监控平台,那是更一步的治理手段了。
DevOps
一个系统的可用性,工业界一般使用一个公式:
其中
MTTF 是 Mean Time To Failure,平均故障前的时间,即系统平均能够正常运行多长时间才发生一次故障。系统的可靠性越高,MTTF 越长。(注意:从字面上来说,看上去有 Failure 的字样,但其实是正常运行的时间。)
MTTR 是 Mean Time To Recovery,平均修复时间,即从故障出现到故障修复的这段时间,这段时间越短越好。
这个公式就是计算系统可用性的,也就是我们常说的,多少个9。
根据上面的这个公式,为了提高可用性,我们要么提高系统的无故障时间,要么减少系统的故障恢复时间。但是业内经过长期的实践发现
故障是正常的,而且是常见的。
故障是不可预测突发的,而且相当难缠。
也就是不要尝试去避免故障,而我们要干的事情是尽一切手段来降低MTTR——故障的修复时间。这也就是前面监控系统建设主要解决的点——快速解决问题降低资损。
但面对系统的复杂性和不确定性,系统的稳定性保障已不仅仅满足于降低MTTR,还需引入一系列手段提前感知风险,进一步提升服务的SLA。
具体采取的手段有哪些呢?全链路压测、故障演练和日常巡检,按照一般系统实施频率划分:全链路压测
全链路压测侧重于整体链路或者某一业务单元的可靠性,其最大的实施难点不单纯是技术问题,而是组织上下游推动的成本。主要应用有如下场景
- 峰值业务稳定性。一般涉及秒杀类活动业务场景,通过全链路压测对秒杀活动峰值业务稳定性进行考验,保障峰值业务不受损;
- 服务容量规划。通过全链路压测技术对成本进行优化,对服务进行精细化的容量规划;
- 性能瓶颈探测。全链路压测还可以用于探测服务的性能瓶颈,提升服务的整体能力和吞吐量;
- 新系统上线。全链路压测用于新系统上线,准确地探知服务能力,防止一上线就被用户流量打垮。
相信大家听过”Chaos Monkey”,就是对线上系统主动注入故障来验证线上系统的可靠性,听起来很像一个放置游戏。
如今业内已形成一套完备的方法论体系——混沌工程,各家厂商这几年也不断建设或开放自家的混沌工具,比如阿里开源的ChaosBlade。
故障演练实施过程需要梳理现在业务中的故障点,包括自身故障和依赖资源故障,针对故障点,建立完善的故障演练机制,最终实现故障演练常态化。
这里简单提一下日常巡检。一般巡检可能涵盖应用巡检、资源巡检、核心指标巡检,初期主要采用人工方式,比如团队值班人工巡检,后续可以通过建立自动化的巡检系统,健康脚本周期性的对系统各层指标巡检,并有系统健康度报告产出。
性能
谈到性能,就不得不提到性能工程这个话题,涉及数据模型、性能测试、性能分析、性能优化。
关键路径是通过性能测试设置不同性能场景对线上服务进行全方位的诊断,得到服务性能指标的基准,以基准预设目标尺度,后续通过性能分析找出服务的瓶颈点,对瓶颈点以收益视角评估多种优化方案,选出最优方案,进而实施优化,最终以性能测试反馈结果评估是否达到预设目标尺度。
准备好数据模型是整个性能工程中的前提,如果性能测试场景选取的数据与线上业务真实流量分布不一致,那不管采用哪种性能测试手段,最终得出的结果都是错的。
那你可能会说我直接录制线上流量不就好了么?确实可以,但是需要对录制流量的时间点进行多次采样测试,保证测试结果准确性。像峰值压测场景,可能会考虑某个时间区间峰值时间段的流量数据分布进行压测,还要考虑未来一段时间不同业务的流量分布增长趋势,这一般会涉及到容量预估的场景。其最终目的是保证准备压测场景的流量数据与线上真实流量数据分布相贴合。
可能性能测试一些书籍对测试场景的概念描述相对模糊,边界不清晰,什么容量测试、压力测试、负载测试,说实话确实很难区分这三个测试场景有啥区别,如果做好分类的话,一般不超过这几个场景
- 基准性能场景。一般指的是单业务单API或者多业务单API基准测试,为后续混合容量做准备;
- 容量性能场景。这是最高频使用的场景,比如混合容量测试,拿到单机在真实流量分布下的性能指标基线,评估预期服务容量;
- 稳定性性能场景。稳定性测试侧重的是服务在某个压力位能稳定运行多久的能力,在稳定性测试中,显然最核心的元素是时间。像秒杀场景,会采用稳定性测试场景压测服务在瞬间峰值QPS下持续运行的时间周期;
- 异常性能场景。要做异常性能场景,前提就是要有压力。在压力流量之下,模拟异常。
准备好数据模型,选好测试场景,在性能测试实施的过程中,还要考虑一些干扰项对结果的真实性产生影响,比如测试环境是否具有可重复性——现场环境快速复位的能力、测试数据是否可循环——缓存干预结果准确性等。
性能分析以立体化监控体系为基础,拿到各层指标,追溯因果关系,形成证据链,找到最终的的root cause。
可能性能分析的瓶颈点有多个,针对不同瓶颈点优化方案也有多个,至于选取哪个瓶颈点及优化方案进行实施呢?只有一个考虑维度,ROI。
可能前面会花很长的篇幅赘述性能工程方面的概念,没具体谈一下网关性能优化的细节,但其实上面谈的概念指导网关性能优化的整体方向,也就是所谓的道,至于优化手段是术。“道为体,术为用。精于术而乏道者亦不能长久,精于术而明道者乃高人也”。
全异步化
提到网关的性能优化,第一顺位是全异步化,大家应该还记得网关的定位,核心是做好安全保障。
网关是网络IO密集型场景,全异步化将端到端的线程池解耦,释放网络等待引起的线程占用,线程数不再成为网关的瓶颈,彻底隔离API请求之间的影响,慢API不会引起网关的不稳定性。
提升服务的吞吐,高效利用单机资源是全异步化的另一优势,也就涉及到网关的另一定位,降本。
全异步化实施之前的考虑点是比较多的,像线程模型方案的选择,协议、容器异步化是否支持,比如HTTP协议如果采用Tomcat容器需要依赖 Servlet 3.0+ 的支持。
Tomcat 6.X 提供了对NIO的支持,通过指定Connector的protocol=“org.apache.coyote.http11.Http11NioProtocol”,就可以开启NIO模式。Tomcat 8.X 之后提供了对 NIO2.0的支持,默认开启NIO通信模式。
事实上,Tomcat支持NIO,与Tomcat的HTTP服务是否是异步,没有必然关系。即便Tomcat启用NIO,HTTP请求和响应的消息处理仍然可能是同步阻塞的。
那如何做到做到异步非阻塞?那就需要确认Servlet是否支持异步,如果Servlet是3.X之前的版本,则HTTP协议的处理仍然是同步的。
将前置依赖方案对齐之后,最关键的考虑点是端到端的异步化。端到端的异步化具体实施环节,第一阶段涉及出入端异步化。
关于入口端,前面的协议容器基本都已提到,针对入口请求,除了将 Tomcat的Connector配置成NIO模式之外,还需要Tomcat配套的Servlet版本支持异步化(3.0+),同时还需要在业务 Servlet 的代码中开启异步模式,HTTP服务端才能够实现真正的异步化:I/O异步以及业务逻辑处理的异步化。
关于出口端,就需要考虑调用下游服务,协议是否提供异步化支持,如果是HTTP调用的话,会依赖AsyncHttpClient,如果是RPC调用,则需要依赖异步RPC。
如果调用下游服务存在同步场景,需要跟下游服务方同步推动提供异步API支持,否则的话整个调用链路还是同步阻塞,在大流量下服务可能因同步阻塞请求使整个服务hang住,有潜在雪崩的风险,这是需要注意的点。
出入端异步化之后,进入第二阶段,服务编排异步化。pipeline/reactive架构是服务编排异步化的基石,因为这地方可能会涉及到多IO任务编排场景,比如多API聚合场景,存在一个上游服务会调用下游十几个服务,还要做业务逻辑的情况。
反应式编程方式对这类场景(Amdahl加速定律是这一场景的理论基石)的支持会更优雅一些,一是可以将多IO任务重新编排,降低RT,二是可以将服务内部业务逻辑解耦,进一步压榨单机资源利用率,提升服务吞吐。
弹性伸缩
网关服务线上流量分布一般符合高峰低谷的现象,在低峰期资源空置浪费较严重,因此需要引入弹性伸缩在服务资源治理角度提升服务资源利用率,进一步降低资源成本。
服务一定要预估好固定容量!不要为了盲目的引入弹性伸缩而大量释放过多机器!这是引入弹性伸缩后唯一须注意的点,否则后续可能引发很多线上风险问题
- 服务发布期间,弹性伸缩会被禁止,如果采用同机房流量调度,某单机房容量不足在发版时突增流量,服务算力不足引发雪崩;
- 弹性伸缩扩容配置策略不完备,在突增流量场景,原始机器较少,触发弹性扩容时机相对滞后,弹出机器被压垮,进而触发雪崩。
容量预估跟混合容量压测场景有关,如果服务采用同城流量调度策略,容量预估可能相对更简单一些,但如果服务采用同机房流量调度策略,不同机房的流量分布模型可能都不一致,所以每个机房负载峰值也不一样,容量预估的难度会加大。当然,弹性伸缩规则配置和维护的难度也会变大。
最后一个可能需要注意的点是,针对弹出的机器做好稳定包标记,防止弹出的机器与线上部署版本不一致存在风险。
黄金期
黄金期是是API网关角色转换的阶段,从API网关到开放平台,聚焦于开放、安全与高效接入。
对外(前端)开放平台提供多租户与配置化门户。
多租户与配置化门户包含开发者门户与开发者控制台,开发者门户需涵盖产品与解决方案、接口文档、支持中心、开发者社区建设,开发者控制台则需做好开发者管理和应用管理,比如开发者管理会涉及申请入驻、API收费、身份管理、信息维护等功能,而应用管理会涉及应用创建、权限设置、应用设置、SDK下载功能。
对内(后端)开放平台具备定制化开放与垂直化管理能力。作为服务提供方,应具备API开放和运营管理的能力。
API开放主要涉及API全生命周期管理、API文档、测试工具、灰度、API定制化等能力建设。运营管理则涵盖开发者管理、应用管理、文档管理、工单管理、通知中心、数据统计&用户画像等功能。
开放会加速安全能力更进一步建设,对安全的要求更高且安全控制的粒度会更加细腻。开放代表易用与灵活,而安全代表着难用与限制,如何把握开放与安全的边界?这是个很难说清楚的话题。
前面谈过安全的话题,但仅局限于网关自身与上下游服务安全。开放后,安全的边界引入了数据安全和应用安全。
数据安全主要涵盖对外数据披露的审批流程、数据分级、数据脱敏、数据混淆与加密、数据监控。应用安全则涵盖开发者入驻规范、应用创建申请、API权限申请、应用安全配置、应用安全扫描、环境托管安全等功能。
API全生命周期管理
API全生命周期管理,包括API的创建、维护、发布、下线、灰度等,通过配置的方式即可开放内部服务API,供上游业务高效接入。
API定制化能力
API聚合和数据编排能力是开放平台具备API定制化能力的前提,而API聚合&数据编排能力又依赖API网关DSL能力的建设。
API聚合和数据编排能力做到对API的定制化、复用,对外固化成某一领域的解决方案,供开发者使用,但对内来说“富API”可能会与内部上游服务的资源边界产生冲突,这也是个需要考量的事情。
灰度
灰度对于下游API提供方,是保证新版本API正确性的手段,而对于上游API使用方是验证新业务准确性的策略,常见于AB场景。
API网关灰度能力建设有两个视角的考量
- 服务编排DSL具备流量分发的能力;
- 在流量调度层,API网关通过灰度流量支持,独立集群部署,具备灰度链路流量隔离的能力。
尾声
好了,文到尾声,对本文的知识点林林总总进行梳理,我们可以重新回顾下本文上篇开头的思维导图,涉及API网关的整体介绍,以及一个百亿级API网关在不同阶段的定位和能力建设取舍的权衡,以全局的视角贯穿一个百亿级流量API网关的设计。
后面如果有时间的话会细谈下关键知识点的具体实现细节,比如全异步化、DSL能力建设、单元化建设、立体化监控体系等等。
一个厂里搬砖工的瞎比比
作者微信:crackxb
点个「关注+在看」 再走吧