导语
每年,网上都会报道XXX系统异常不可用,给客户带来巨大的经济损失。云服务的客户基数更大,一旦出现问题,都将给客户和服务自身带来极大影响。本文将基于华为云FunctionGraph自身的实践,详细介绍如何构建高可用的Serverless计算平台,实现客户和平台双赢。
高可用介绍
高可用性[1](英语:high availability,缩写为 HA),IT术语,指系统无中断地执行其功能的能力,代表系统的可用性程度。是进行系统设计时的准则之一。
业界一般使用 SLA指标来衡量系统的可用性。
服务级别协议[2](英语:service-level agreement,缩写SLA)也称服务等级协议、服务水平协议,是服务提供商与客户之间定义的正式承诺。服务提供商与受服务客户之间具体达成了承诺的服务指标——质量、可用性,责任。例如,服务提供商对外承诺99.99%的SLA,则全年服务失效时间最大为 5.26 分钟(365*24*60*0.001%)。
FunctionGraph直观度量系统可用性的两个黄金指标,SLI和时延,SLI是系统的请求成功率指标,时延是系统处理的性能。
高可用挑战
FunctionGraph作为华为云中的子服务,在构建自身能力的同时,不仅要考虑系统本身的健壮性,也要考虑周边依赖服务的健壮性(例如依赖的身份认证服务不可用了,进行流量转发的网关服务服务宕机了,存储对象的服务访问失败了等等)。除此之外,系统依赖的硬件资源故障或者系统突然遭到流量攻击等,面临这些不可控的异常场景,系统如何构建自己的能力来保持业务高可用是一个很大的挑战。图一展示了FunctionGraph的周边交互。
图1 FunctionGraph的周边交互
针对常见的问题,梳理出了4个大类,如表1所示。
故障大类 | 一级分类 | 二级分类 |
流量突变 | 正常流量突增 | 同步流量增加,底层资源不足 |
异步流量增加,消息队列资源不足 | ||
异常流量突增 | 函数时延突增,请求堆积飙升 | |
函数失败率飙升,重试请求飙升 | ||
系统服务异常 | 系统资源监控异常 | CPU飙升,无法处理新请求 |
内存飙升,无法处理新请求 | ||
客户流量飙升,客户互相干扰 | ||
系统进程监控异常 | 进程频繁重启,请求处理异常 | |
连接数飙升,无法处理新请求 | ||
系统时延飙升,请求大量堆积 | ||
中间件监控异常 | KAFKA消息堆积飙升,异步请求堆积 | |
REDIS内存使用率飙升,指标异常 | ||
ETCD连接数飙升,请求处理异常 | ||
系统依赖服务异常 | 基础依赖性服务异常 | 网关服务异常,无法接收请求 |
负载均衡服务异常,无法接收请求 | ||
VPC服务异常,无法转发请求 | ||
DNS解析服务异常,无法转发请求 | ||
逻辑依赖性服务异常 | 鉴权服务异常,鉴权失败 | |
日志服务异常,无法存取日志 | ||
容器服务异常,系统异常 | ||
对象存储服务异常,系统异常 | ||
底层硬件资源异常 | 网络中断、丢包、时延突增 | |
机房因不可控因素受损,系统异常 | ||
节点故障,系统异常 | ||
变更引起 | 软件版本变更 | 引入bug,功能不兼容 |
部署架构变更 | 引入bug,功能不兼容 | |
配置参数变更 | 引入bug,功能不兼容 | |
依赖服务变更 | 引入bug,功能不兼容 |
表1 FunctionGraph常见问题总结
针对这些问题,我们总结了如下几类通用的治理办法。
- 流量突变治理:过载保护+弹性扩缩容+熔断+异步削峰+监控告警,基于防御式的设计思想,通过过载保护+熔断确保系统所有资源受控,然后在此基础上通过提供极致的扩容能力来满足大流量,合适的客户场景推荐异步削峰来减缓系统压力,监控告警用来及时发现过载问题。
- 系统服务异常治理:容灾架构+重试+隔离+监控告警,通过容灾架构避免系统整个宕机,通过重试来减少系统异常对客户业务的影响,通过隔离快速剥离系统异常点,防止故障扩散,通过监控告警快速发现系统服务异常问题。
- 系统依赖服务异常治理:容灾架构+缓存降级+监控告警,通过容灾架构减少依赖服务单点故障,通过缓存降级确保依赖服务故障后系统仍能正常运行,通过监控告警快速发现依赖服务异常问题。
- 变更引起治理:灰度升级+流程管控+监控告警,通过灰度升级避免正式客户由于系统升级异常而造成的全局故障,通过流程管控将人为变更的风险降到最低,通过监控告警快速发现变更后的故障。
FunctionGraph系统设计实践
为了解决表1出现的问题,FunctionGraph在架构容灾、流控、重试、缓存、灰度升级、监控告警、管理流程上等多方面做了优化,可用性大幅提升。下面主要介绍一些FunctionGraph面向异常的设计实践,在弹性能力、系统功能等暂不展开。
- 容灾架构
实现华为云容灾1.1架构(例:服务AZ级故障域、集群跨AZ自愈能力、AZ级服务依赖隔离),FunctionGraph管理面和数据面集群部署多套,每套集群AZ隔离,实现同region内的AZ容灾。如图2所示,FunctionGraph部署多套数据面集群(承担FunctionGraph函数运行业务)和dispatcher调度集群(承担FunctionGraph的流量集群调度任务),用来提升系容量以及容灾。当前其中某个元戎集群异常时,dispatcher调度组件能及时摘除故障集群,并将流量分发至其他几个集群。
图2 FunctionGraph简略架构图
- 分布式无中心化架构设计,支持灵活的横向扩缩容
这个策略是逻辑多租服务设计的关键,需要解决无中心化后,组件扩缩容后的重均衡问题。
- 静态数据管理的无中心化:逻辑多租服务的元数据,初期由于量少,可以全部存储到同一套中间件中。随着客户上量,需要设计数据的拆分方案,支持数据的分片,应对后续海量数据读写,以及可靠性压力。
- 流量调度功能的无中心化:组件功能设计,支持无中心化(常见中心化依赖:锁、流控值、调度任务等),流量上量后,可扩展组件副本数量,组件通过自均衡策略,完成流量的重新负载。
- 多维度的流控策略
FunctionGraph上的客户函数流量最终达到runtime运行时之前,会经过多个链路,每个链路都有可能出现超过其承载阈值的流量。因此,为了确保各个链路的稳定性,FunctionGraph在每条链路上,防御性的追加了不同的流控策略。基本原则解决计算(cpu)、存储(磁盘、磁盘I/0)、网络(http连接、带宽)上的函数粒度的资源隔离。
函数流量从客户侧触发,最终运行起来的链路流控如图3所示。
图3 FunctionGraph流控
- 网关APIG流控
APIG是FunctionGraph的流量入口,支持Region级别总的流量控制,可以根据region的业务繁忙程度进行弹性扩容。同时APIG支持客户级别的流量控制,当检测到客户流量异常时,可以快速通过APIG侧限制客户流量,减少个别客户对系统稳定性的影响。
2.系统业务流控
-
- 针对api级别的流控
客户流量通过APIG后,走到FunctionGraph的系统侧。基于APIG流控失效的场景,FunctionGraph构建了自身的流控策略。当前支持节点级别流控、客户api总流控、函数级别流控。当客户流量超过FunctionGraph的承载能力时,系统直接拒绝,并返回429给客户。
- 系统资源流控
FunctionGraph是逻辑多租服务,控制面和数据面的资源是客户共享的,当非法客户恶意攻击时,会造成系统不稳定。FunctionGraph针对共享资源实现基于请求并发数的客户流控,严格限制客户可用的资源。另外对共享资源的资源池化来保证共享资源的总量可控制,进而保证系统的可用性。例如:http连接池、内存池、协程池。
- 并发数控制:构建基于请求并发数的FunctionGraph函数粒度的流控策略,FunctionGraph的客户函数执行时间有毫秒、秒、分钟、小时等多种类型,常规的QPS每秒请求数的流控策略在处理超长执行的请求时有先天不足,无法限制同一时刻客户占用的系统共享资源。基于并发数的控制策略,严格限制了同一时刻的请求量,超过并发数直接拒绝,保护系统共享资源。
- http连接池:构建高并发的服务时,合理的维护http的长连接数量,能最大限度减少http连接的资源开销时间,同时保证http连接数资源的可控,确保系统安全性的同时提升系统性能。业界可以参考http2的连接复用,以及fasthttp内部的连接池实现,其原理都是尽量减少http的数量,复用已有的资源。
- 内存池:客户的请求和响应报文特别大,同时并发特别高的场景下,单位时间占用系统的内存较大,当超过阈值后,会轻松造成系统内存溢出,导致系统重启。基于此场景,FunctionGraph新增了内存池的统一控制,在请求入口和响应出口,校验客户请求报文是否超过阈值,保护系统内存可控。
- 协程池:FunctionGraph构建于云原生平台上,采用的go语言。如果每一个请求都使用一个协程来进行日志和指标的处理,大并发请求来临时,导致有海量的协程在并发执行,造成系统的整体性能大幅下降。FunctionGraph引入go的协程池,通过将日志和指标的处理任务改造成一个个的job任务,提交到协程池中,然协程池统一处理,大幅缓解了协程爆炸的问题。
- 异步消费速率控制:异步函数调用时,会优先放到FunctionGraph的kafka中,通过合理设置客户的kafka消费速率,确保函数实例始终够用,同时防止过量的函数调用,导致底层资源被迅速耗光。
- 函数实例控制
- 客户实例配额:通过限制客户总配额,防止恶意客户将底层资源耗光,来保障系统的稳定性。当客户业务确实有需要,可以通过申请工单的方式快速扩充客户配额。
- 函数实例配额:通过限制函数配额,防止单个客户的函数将客户的实例耗光,同时也能防止客户配额失效,短时间内造成大量的资源消耗。另外,客户业务如果涉及数据库、redis等中间件的使用,通过函数实例配额限制,可以保护客户的中间件连接数在可控范围内。
- 高效的资源弹性能力
流控属于防御式设计思想,通过提前封堵的方式减少系统过载的风险。客户正常业务突发上量需要大量的资源时,首先应该解决的是资源弹性问题,保证客户业务成功的前提下,通过流控策略兜底系统出现异常,防止爆炸面扩散。FunctionGraph支持集群节点快速弹性、支持客户函数实例快速弹性、支持客户函数实例的智能预测弹性等多种弹性能力,保证客户业务突增时依然能正常使用FunctionGraph。
- 重试策略
FunctionGraph通过设计恰当好处的重试策略,使系统在发生异常的时候,也可以保障客户的请求最终执行成功。如图4所示,重试的策略一定要有终止条件,否则会造成重试风暴,更轻松的击穿系统的承载上限。
图4 重试策略
- 函数请求失败重试
- 同步请求:当客户请求执行时,遇到系统错误时,FunctionGraph会将请求转发至其他集群,最多重试3次,确保客户的请求,在遇到偶现的集群异常,也可以在其他集群执行成功。
- 异步请求:由于异步函数对实时性要求不高,客户函数执行失败后,系统可以针对失败请求做更为精细的重试策略。当前FunctionGraph支持二进制指数退避的重试,当函数由于系统错误异常终止后,函数会按2,4,8,16指数退避的方法,当间隔退避到20分钟时,后续重试均按照20分的间隔进行,函数请求重试时间最大支持6小时,当超过后,会按失败请求处理,返回给客户。通过二进制指数退避的方式,可以最大程度保障客户业务的稳定性。
- 依赖服务间的重试
- 中间件的重试机制:以redis为例,当系统读写redis偶现失败时,会sleep一段时间,再重复执行redis的读写操作,最大重试次数3次。
- http请求重试机制:当http请求由于网络波动,发生eof、io timeout之类的错误时,会sleep一段时间,在重复http的发送操作,最大重试次数3次。
- 缓存
缓存不仅可以加速数据的访问,而且当依赖的服务故障时,仍然可以使用缓存数据,保障系统的可用性。从功能类别划分,FunctionGraph需要进行缓存的组件有两类,1是中间件,2是依赖的云服务,系统优先访问缓存数据,同时定期从中间件和依赖的云服务刷新本地缓存数据。方式如图5所示。
- 缓存中间件数据:FunctionGraph通过发布订阅的方式,监听中间件数据的变化及时更新到本地缓存,当中间件异常时,本地缓存可以继续使用,维持系统的稳定性。
- 缓存关键依赖服务数据:以华为云的身份认证服务IAM为例,FunctionGraph会强依赖IAM,当客户发起首次请求,系统会将token缓存到本地,过期时间24小时,当IAM挂掉后,不影响老的请求。FunctionGraph系统的使用。其他关键的云服务依赖做法一直,都是把关键的数据临时缓存到本地内存。
图5 FunctionGraph的缓存措施
- 熔断
上面的种种措施,可以保障客户业务平稳运行,但当客户业务出现异常一直无法恢复或者有恶意客户持续攻击FunctionGraph平台,系统资源会一直浪费在异常流量上,挤占正常客户的资源,同时系统可能会在持续高负荷运行异常流量后出现不可预期的错误。针对这种场景,FunctionGraph基于函数调用量模型构建了自身的断路器策略。具体如图6所示,根据调用量的失败率进行多级熔断,保证客户业务的平滑以及系统的稳定。
图6 熔断策略模型
- 隔离
- 异步函数业务隔离:按照异步请求的类别,FunctionGraph将Kafka的消费组划分为定时触发器消费组、专享消费组、通用消费组、异步消息重试消费组,topic同理也划分为对等类别。通过细分consumer消费组和topic,定时触发器业务和大流量业务隔离,正常业务和重试请求业务隔离,客户的业务请求得到最高优先级的保障。
- 安全容器隔离:传统cce容器基于cgroup进行隔离,当客户增多,客户调用量变大时,会偶现客户间的互相干扰。通过安全容器可以做到虚拟机级别的隔离,客户业务互不干扰。
- 灰度升级
逻辑多租服务,一旦升级出问题,造成的影响不可控。FunctionGraph支持按ring环升级(根据region上业务的风险度进行划分)、蓝绿发布、金丝雀发布策略,升级动作简要描述成三个步骤:
- 升级前集群的流量隔离:当前FunctionGraph升级时,优先将升级集群的流量隔离,确保新流量不在进入升级集群;
- 升级前集群的流量迁移、优雅退出:将流量迁移到其他集群,同时升级集群的请求彻底优雅退出后,执行升级操作;
- 升级后的集群支持流量按客户迁入:升级完成后,将拨测客户的流量转发到升级集群,待拨测用例全部执行成功后,在将正式客户的流量迁进来。
- 监控告警
当FunctionGraph出现系统无法兜住的错误后,我们给出的解决措施就是构建监控告警能力,快速发现异常点,在分钟级别恢复故障,最大程度减少系统的中断时间。作为系统高可用的最后一道防线,快速发现问题的能力至关重要,FunctionGraph围绕着业务关键路径,构建了多个告警点。如表2所示。
监控告警 | 一级分类 | 二级分类 | 三级分类 |
依赖服务监控 | 中间件负载 | kafka负载 | 消息堆积量 |
broker磁盘使用率 | |||
redis负载 | 内存使用率 | ||
cpu使用率 | |||
依赖服务状态 | 中间件状态 | kafka连通性 | |
redis连通性 | |||
etcd连通性 | |||
云服务状态 | 认证服务连通性 | ||
对象存储连通性 | |||
日志服务连通性 | |||
…… | |||
系统服务监控 | 底层资源状态 | 集群状态 | cpu负载 |
内存负载 | |||
集群健康状态 | |||
节点状态 | cpu负载 | ||
内存负载 | |||
节点健康状态 | |||
公网流量负载 | 流量过大告警 | ||
服务进程状态 | 进程实时负载 | cpu负载 | |
内存负载 | |||
进程健康状态 | 进程异常告警 | ||
业务指标监控 | 系统可用性监控 | 拨测用例 | 用例失败告警 |
系统SLI执行成功率 | SLI下降告警 | ||
客户业务状态 | 失败告警 | ||
系统时延 | 时延增大告警 |
表2: FunctionGraph构建的监控告警
- 流程规范
上面的一些措施从技术设计层面解决系统可用性的问题,FunctionGraph从流程上也形成了一套规章制度,当技术短期无法解决问题后,可以通过人为介入快速消除风险。具体有如下团队运作规范:
- 内部war room流程:遇到现网紧急问题,团队内部快速组织起关键角色,第一时间恢复现网故障;
- 内部变更评审流程:系统版本在测试环境浸泡验证没问题后,在正式变更现网前,需要编写变更指导书,识别变更功能点和风险点,经团队关键角色评估后,才准许上现网,通过标准的流程管理减少人为变更导致异常;
- 定期现网问题分析复盘:例行每周现网风险评估、告警分析复盘,通过问题看系统设计的不足之处,举一反三,优化系统。
- 客户端容灾
业界最先进的云服务,对外也无法承诺100%的SLA。所以,当系统自身甚至人力介入都无法在急短时间内快速恢复系统状态,这时候和客户共同设计的容灾方案就显得至关重要。一般,FunctionGraph会和客户一同设计客户端的容灾方案,当系统持续出现异常,客户端需要针对返回进行重试,当失败次数达到一定程度,需要考虑在客户端侧触发熔断,限制对下游系统的访问,同时及时切换到逃生方案。
总结
FunctionGraph在做高可用设计时,整体遵循如下原则“冗余+故障转移”,在满足业务基本需求的情况下,保证系统稳定后在逐步完善架构。
“冗余+故障转移”包括以下能力:
容灾架构:多集群模式、主备模式
过载保护:流控、异步削峰、资源池化
故障治理:重试、缓存、隔离、降级、熔断
灰度发布:灰度切流、优雅退出
客户端容灾:重试、熔断、逃生
未来,FunctionGraph会持续从系统设计、监控、流程几个维度持续构建更高可用的服务。如图7所示,通过构建监测能力快速发现问题,通过可靠性设计快速解决问题,通过流程规范来减少问题,持续提升系统的可用性能力,为客户提供SLA更高的服务。
图7: FunctionGraph高可用迭代实践
参考文献
[1]高可用定义:https://zh.wikipedia.org/zh-hans/%E9%AB%98%E5%8F%AF%E7%94%A8%E6%80%A7
[2]SLA定义:https://zh.wikipedia.org/zh-hans/%E6%9C%8D%E5%8A%A1%E7%BA%A7%E5%88%AB%E5%8D%8F%E8%AE%AE
作者:安 校稿:旧浪、闻若