稳定性治理的常见方法

avatar
FE @字节跳动

服务稳定性建设是一项长期和持续的任务,需要我们在日常开发工作中投入更多的精力,才能将服务稳定性的事情做好。最近我也在学习稳定性相关的知识,本文就是在学习中的一些总结和思考,希望通过本篇文章让大家对服务稳定性有一个全面的了解。

一、基本概念

1.1 服务等级协议(SLA)

当我们提到服务稳定性时,经常会提到一个重要的概念 SLA,也就是服务等级协议。这是服务提供商与客户之间定义的一个正式承诺。服务提供商与受服务用户之间具体达成了承诺的服务指标——质量、可用性,责任。在我们的视角,SLA 主要包括一下两个方面的内容,服务可用性故障恢复时间。

  • 服务可用性:全年中服务稳定的时间

    • 365 * 24 = 8760 小时
    • 系统可用性年故障时间日故障时间
      99%(两个九)3.65天(87.6小时)14.4分钟
      99.9%(三个九)8小时1.44分钟
      99.99%(四个九)52分钟8.6秒
      99.999%(五个九)5分钟0.86秒
      99.9999%(六个九)32秒86毫秒
  • 服务故障恢复时间

    •   MTBF(Mean Time Between Failure) 是平均故障间隔的意思,代表两次故障的间隔时间,也就是系统正常运转的平均时间。这个时间越长,系统稳定性越高。
    •   MTTR Mean Time To Repair 表示故障的平均恢复时间,也可以理解为平均故障时间。这个值越小,故障对于用户的影响越小。
    • 提升 MTBF 缩短 MTTR,做到 MTTR 5-15-30 (5分钟发现,15分钟定位,30分钟恢复)。

      24 小时进行复盘

1.2 服务器性能

并发数:同一时刻处理的请求数,受限于 CPU、进程数量

吞吐量( QPS :每秒钟处理的请求数

响应时间:处理一个请求的所需的时间,比如我们常说的接口响应时间 200ms

指标描述
avg平均相应时间
pct50数据集升序排列,第 50 分位置大的数据
pct90数据集升序排列,第 90 分位置大的数据
pct99数据集升序排列,第 99 分位置大的数据

QPS = 并发数 / 平均响应时间

例如:10 / 0.1 = 100

1.3 为什么要做稳定性建设?

服务的稳定性是产品品牌和影响力的支撑,尤其对于 ToD、ToB 产品来说,稳定性是至关重要的因素,如果我们的服务出现问题,会直接影响到上游产品的使用,给公司和客户带来经济损失。

尤其是在服务产品的体量增大以后,如果出现稳定性问题会造成更大面积的影响。此外,随着系统复杂的增高,我们越来越难以去梳理清楚服务之间的依赖关系,可能我们平时不注意的一个小问题,造成了更大的意外影响。

2021年 10 月 4 日,美国社交媒体 Facebook、Instagram 和即时通讯软件 WhatsApp 出现大规模宕机,此次宕机长达近 7 个小时,刷新了 Facebook 自 2008 年以来的最长宕机时长。

Facebook 事后发表了故障报告,表示在一项日常维护工作中,工程师们发出一条用于评估全球骨干网容量可用性的指令,但意外切断了骨干网络中的所有连接,这实质上就是断开了 Facebook 全球数据中心之间的连接。服务中断之后,Facebook 的工程师们因无法通过正常方式访问 Facebook 数据中心进行修复,导致故障持续了 7 个小时之久。

这次事故让脸书一夜之间市值蒸发约 473 亿美元。

二、服务器稳定性

2.1 依赖治理

服务依赖是稳定性治理中的一个重要概念,它本质上是对系统中内聚和耦合性问题的梳理。将我们服务中各种依赖关系梳理清晰,逐一建立稳定性保障,是提高服务整体稳定性的关键。

  • 依据服务依赖的强弱,我们可以将依赖进行分类:

    • 强依赖:当依赖系统不可用时,服务不可用,但是系统不会崩溃。服务可以强依赖同级别或者更高级别的服务。(服务器、数据库)
    • 弱依赖:当依赖系统不可用时,服务可用,但是会损失一些次级功能(或性能)。服务可以弱依赖于低于自己级别的业务。(MQ、第三方API)
  • 在没有定义的情况下,可以有一些基础约定:

    • 中间件基础设施 高于 业务系统
    • 前台系统 高于 后台系统(比如配置管理系统)

处理方式:

  • 强依赖:需要对于强依赖服务不可用时候产生的资损等有明确的计算评估和恢复手段,需要在强依赖系统恢复之后的短时间内完成业务的恢复

  • 弱依赖:对于弱依赖系统,可以通过限流、降级预案等进行管理,确保自动、手动的解除依赖,保护系统的主要功能逻辑。

  • 减少强服务依赖,或通过中间件方式,将强依赖解耦

启动依赖治理:

  • 重要性在于两个方面:

    • 扩容、服务不可用回滚时候的及时性,启动越快恢复越快;
    • 是否在某些依赖有问题情况下的启动,只有启动了才有机会加载应急策略。
  • 原则上应用启动时,只能依赖数据库、本地资源、服务化架构中间件,不允许依赖其他基础服务、内部服务或者外部服务。

  • 服务启动时长需要进行优化,原则上不能超过 1 - 2 min。

存储依赖治理:

  • 核心业务需要确认所依赖但存储是否做到隔离(提升被依赖方重要性)、是否做到主备等容灾方案、是否存在读写分离、是否存在只读不写等降级方案。

  • 在线应用要弱依赖偏离线服务能力的存储系统(如hbase、hive)。

  • 理论上,DB 需要能够扛住全量的压力(但通常比较难)。

中间件依赖治理:

  • 同步系统的 SLA 不能低于应用 SLA

  • 异步系统(如消息系统)在考虑服务可用性的基础上,需要关注消息延迟带来的业务问题。

  • 配置中心类中间件的依赖,需要保留降级方案(避免应急预案不能实施)

在上面的依赖治理中有提到过,我们可以通过消息队列来解耦依赖,另外比如同步流程,异步任务这种类型的操作也适合使用消息队列来进行开发。

  • 用途:解耦、异步任务、削峰填谷

  • 长耗时任务、同步任务、异步任务场景适合使用消息队列进行处理

2.2 数据库治理

数据库是服务中最重要的基础设施,如果数据库出现问题,那么我们的服务基本也就处于不可用状态了,如果保护好数据库,不让它承受过大的访问压力,是我们在数据库优化中需要考虑的问题。

其中,我们在开发中 数据库使用中最常遇到的问题就是慢查询,通常来说我们规定一个查询的时间要小于 100 ms,大量的慢查询会增加数据库压力,使其可用性下降,如果这是个大量请求,会让请求堆积阻塞,最终导致服务不可用,数据库宕机。合理的数据表设计、索引建立以及查询语句的优化可以帮助解决这个问题。

  • 慢查询监控

    • 一般云服务的数据库有提供慢查询通知,可以帮助我们及时发现慢查询问题。
  • 数据表设计

    • 合理的表设计是重要的一环,虽然 MongoDB 并没有表结构的概念,但合理规划数据间的关系仍可以减少慢查询数量
    • 比如尽量避免或者减少在 MySQL 中使用 in 这样的语句
    • 根据实际情况使用第三范式:列的原子性、非主键列直接依赖主键列
    • 合理的数据赘余:比如阅读 PV、评分等数据(也可以选择将此类计算数据存储在缓存中)
  • 数据库索引

    • 数据库索引是一项很重要的保证,正确的使用好索引可以提高数据库的访问速度
    • Mongodb 使用 B-tree 索引,这个和 MySQL 不一样 www.cnblogs.com/rjzheng/p/1…
    • 虽然索引的数据结构不一样,但 MongoDB 也和 MySQL 一样使用最左索引。其中第一索引列可以被单独使用
    • 通过 explain 语句检查索引的有效性
  • 读写分离

    • 在多请求环境下,建议使用读写分离的主从模式,减少主库的压力
    • 读写分离一般存在读延时的问题,这个需要在开发时考虑进去
  • 数据库备份

2.3 缓存治理

在低吞吐量下,缓存的作用不是特别明显,一般数据库的读写分离就可以应付,此时缓存的主要作用更偏向与提高服务速度和缓存计算结果。但当吞吐量很大时,缓存的作用会变得十分关键,或者流量激增的瞬间。

比如在 1s 内重复访问同一资源多次(>1000),在增加缓存的情况下可以防止数据库被打挂。

  • 缓存命中率(对于 Redis 来说最重要的是命中率)

    • 我们可以通过字节云Reids平台查看缓存的使用情况
  • 数据一致性

    • 评估使用场景,一般可以使用: 在更新后删除 key (这里有什么问题?)
    • TypeORM 的缓存功能 typeorm.io/caching#cac…
  • 防止大 key

    • 大 key 的定义:

      • String 类型:value 大于 10kb
      • Hash/Set/List:个数大于 5000 个或超过 10mb
    • 禁止大key情况:拆分或压缩(gzip、snappy)

    • 可以通过平台查看大key情况

  • 缓存穿透

    • 排查服务调用,看看是哪里出现的缓存穿透问题。

    • 缓存空值,低 TTL

    • 在获取数据时使用锁来限制打到数据库的请求。

      • Node.js 是单进程,不存在多线程资源争抢问题。但一般我们会同时部署多个 Node.js 进程,这时可以使用 redis 分布式锁进行处理。(谨慎使用,增加开销和风险)
    • 可以使用布隆过滤器

  • 缓存雪崩

    • 缓存随机值
    • 数据预热
  • 连续请求可以使用 pipeline,降低请求传输的损耗

  • 不使用慢查询命令,比如 SCAN

    • 不使用持久化 AOF,这个会导致 redis 阻塞

2.4 多机房和负载均衡

多机房部署可以防止单一机房挂断后出现服务不可用的情况,在物理级别提供高可用保证。在使用多机房部署后,负载均衡是其中关键的部分,将影响我们的流量如何划分。

  • 负载均衡

DNS 是当今互联网领域最常见的流量调度方式,一般用来实现地理级别的流量负载均衡。DNS 负载均衡的本质是 DNS 解析同一个域名可以返回不同的 IP 地址。优点是非常简单、低成本,但是也存在明显缺点:DNS更新不及时、DNS劫持问题、流量调度不均衡等。所以客户端也会通过 HTTPDNS 的方式进行负载均衡。

  • 常见的负载均衡场景

通常常见互联网分布式架构主要有客户端层、接入层、Web层、服务层、数据层。可以看到,每一个下游都有多个上游调用,如果可以做到每一个上游都均匀访问每一个下游,就可以实现将请求/数据流量均匀分摊到多个操作单元上执行。

使用场景架构层次作用效果典型案例
全局负载均衡客户端层客户端请求流量打到各机房按照权重及容量相对均衡HTTPDNS、GSLB
网络层负载均衡四层接入实现外网域名的相对收敛保证分发路由到后端集群的RS服务器流量相对均衡LVS、TGW
Web层负载均衡七层接入实现客户端HTTP请求流量均衡的分发路由到逻辑层服务器Nginx、TLB
RPC请求负载均衡逻辑层实现RPC客户端请求流量均衡的分发路由到RPC服务端Kite、Dubbo
存储访问负载均衡数据层实现存储访问流量均衡的分发路由到基础存储组件,保障存储的请求和数据做到均衡处理,提高存储系统的可靠性和可扩展性Redis Alchemy ProxyMySQL DbatmanByteKV Proxy
容器负载均衡基础设施层实现分发路由到最基础的容器中Pod对象的流量均衡K8S ServiceK8S KubeProxy

2.5 故障预防

在开发中我们需要时刻保持一颗对故障的敬畏之心,任何服务都是不可信任的。进行合理的设计可以帮助我们在服务出现问题时,能够自动进行重试、恢复和降级,尽可能避免或者减小故障影响。这里罗列了一些我们一般用到的故障预防方法

  • 超时预防

    • 原因:网络抖动,下游异常
    • 处理方法:增加接口超时设置,比如 node-fetch 的 defaultTimeout
    • 影响:如果我们对依赖接口不进行限制,会在网络发生抖动,或下游异常时导致我们的服务一直在等待结果反馈,会占用一定的计算资源。
  • 请求重试

    • 对于核心依赖,建议添加重试逻辑
    • 处理方法:重试次数和重试间隔(指数退避算法)
    • 可能引发的问题:大量重试会导致底层依赖服务压力激增,使其无法恢复
  • 服务降级

    • 当服务器压力过大,无法继续提供服务时,可以考虑通过服务降级的方式减少非必要服务的提供,将计算资源集中到主要服务中。比如 双 11 中无法退货的方案。
    • 可以使用动态配置服务降级策略
  • 服务熔断

    • 当出现问题时,部分功能暂停服务或直接返回预置结果
    • 防止无效重试和等待,占用系统资源
    • 保护下游资源

在框架层我们也有很多方法可以提高稳定性,合理的使用这些能力,能够帮助我们减少出问题的情况。推荐使用内部框架,有提供更多的开箱即用功能可以使用

预防方法插件备注
安全中间件CORS(针对前端请求环境)、Helmet 等nodejs.bytedance.net/docs/gulu/e…
接口限流rate-limitenodejs.bytedance.net/docs/gulu/e…基于令牌桶
权限校验User Guard / 鉴权nodejs.bytedance.net/docs/gulu/p…
异常捕获Error Handlernodejs.bytedance.net/docs/gulu/a…

2.6 日志和监控

系统日志是定位问题的重要方法。在日常开发中添加详细的日志记录,可以帮助在发生问题时快速定位问题原因。

等级用途报警阈值
Trace开发调试日志
Info重要业务日志
Warn存在问题,但仍可继续>100 时进行通知
Error服务不可用,需要马上处理>0 时经行报警

在日志信息中,有一些必要的数据应该全部提供。比如 log_id,request_id,error_code,下游请求信息和错误码等,方便在排查问题时进行精准定位。

另外需要注意,请根据产品的合规要求,在日志中对用户数据进行加密或者删除,比如 jwt、secret、password 等,防止出现通过日志暴露用户隐私的风险。

服务监控

监控与报警是发现问题的最重要方式,建立全面的监控和报警机制,可以帮助我们能够及时在服务出现状况时快速接入,在服务监控中,主要分为一下几种监控类型:

  • 物理资源监控(服务器)

物理资源监控的报警工作,主要依赖于观测平台注入的云平台服务器的报警。

类型参数截图报警
CPU 监控CPU Load CPU UsageCPU Load > 80%CPU Usage > 80%
内存监控RSS UsageRSS Usage > 80%

对于 Node.js 应用,可以进行更精细的监控。

类型参数截图介绍
内存监控Heap UsedHeap TotalExternalV8 Heap 区分了 Used 和 Total,这里是主要是因为 V8 的内存回机制,在进程中有一些内存是可回收并且没有马上被回收的,Total - Used 实际上是指当前可以回收但没有回收的内存。在 Node.js 中使用 Buffer 时,其内存占用量会被记录到 External 中。虽然还会有一些其他的内容被保存在 External 中,我们一般可以认为 External 就是 Buffer 的占用量。
libuvLibuv Handles对于常见的 web 应用来说, libuv handles 较高通常意味着当前请求量较大或者有 tcp 连接等未被正确释放。这里需要观察下异常增高的场景。
  • 业务服务监控:这部分主要是对服务内容进行监控,比如服务对外提供的接口以及下游依赖的第三方服务。

    • 接口时延、错误率、流量
    • 依赖时延、错误
    • 业务正确性(稳定性打点)

2.7 流程规范

制定合理的 CI、CD 发布流程,进行合规发布。在发布中请格外注意灰度、观察和回归三个流程,能够及时发现上线问题并进行处理。

2.8 故障处理

当发生故障时,一定要及时止损,防止影响进一步扩大,然后再进行定位和排查。

  • 预处理

    • 代码回滚
    • 服务扩容
    • 兜底策略
    • 服务降级、熔断
  • 定位修复

    • 通过监控报警、日志等信息定位问题,经行修复

三、前端稳定性

优秀的前端稳定性可以提高网站的用户体验,通过各种手段和措施保障在出现异常时也能够有正确的提示和引导,网站功能依旧完整,不会出现白屏、无响应等严重问题。

完善的稳定性方案可以让网站更加健壮,提高整体性能;通过节流、缓存等方法能够减轻服务器压力,通过监控和报警机制,能够快速发现和定位问题,进行及时止损。

3.1 常见技术方案

技术方案描述截图示例
加载中在加载资源时,给用户提示加载中状态骨架屏和 loading 态
状态提示当接口调用出现问题时,进行提示当出现服务不可用或者网络波动时,一个优秀的兜底方案可以提高服务的整体观感
懒加载图片懒加载可以加快页面首次打开时的速度,也能减轻服务器的压力在滚动到加载上方时再加载资源
异常捕获在 React 中可能会因为意外的未捕获错误导致整个渲染树销毁可以使用 ErrorBoundary 组件进行逐级处理,在发生意外时进行兜底显示。

3.2 接口管理

请求接口数据是我们在前端开发中经常用到的操作,一个网页中通常存在着很多的接口数据。而接口请求可能因为各种情况发生错误,比如网络波动,服务器繁忙,所以在调用接口时添加缓存、重试功能,可以更好的提高成功率。我们可以使用诸如 SWR 这类的接口工具,方便的添加这些能力。

类型描述文档地址
接口缓存对已经调用过的接口进行缓存,短时间内防止重复调用相同的接口swr.vercel.app/zh-CN/docs/…
超时重试接口调用失败时进行重试swr.vercel.app/zh-CN/docs/…
聚焦时重新验证当页面重新聚焦时,重新获取接口数据swr.vercel.app/zh-CN/docs/…

3.3 CDN 容灾

在目前的开发模式中,CDN 资源是前端网站的重要依赖项,如果 CDN 资源请求发生问题基本会导致网站不可用状态,所以对 CDN 资源进行容灾方案,可以当单 CDN 节点发生问题时,经行节点切换。

3.4 PWA 技术(渐进式 Web 应用)

PWA 技术的出现可以让前端网站以应用的形式为用户提供服务,在一定程度上提高了这个网站的可用性指标。

PWA 技术主要包括两个内容:

其中 Manifest 是用来配置应用信息的配置文件,当我们在网页中注入 Manifest 后相当于告知浏览器我们要开启 PWA 功能。而 Service Worker 是 PWA 技术的重要保证,可以将它简单理解为一个浏览器上的代理服务器。它会拦截你发起的请求,然后根据网络是否可用来采取适当的动作。通过它可以加快页面访问速度,能够在网络不可用的情况下提供部分功能,此外也能像应用一样进行消息通知。

3.5 监控和报警

前端监控是我们观测网站异常的重要方法,通过添加监控和报警逻辑,能够帮助我们收集前端页面发生问题时的数据,定位问题来源进行排查和修复。

页面性能分为首屏性能和非首屏性能。首屏性能指的是页面首次加载的时候的性能,可以通过FP、FCP、TTI、LCP 等指标来衡量( W3C Paint Timing 规范草案 )。

SPA 应用则需要额外衡量非首屏性能,即路由切换时的性能。

FP、FCP:FP 是指首次渲染的时间点、FCP 是指首次渲染内容的时间点。

TTI:表示网页第一次 完全达到可交互状态 的时间点。一般是指在连续的 5s 内没有 Long Task 出现

LCP:最大可视内容渲染时间。我们一般使用 LCP 最为页面主要内容渲染完成的时间点

以上图为例,绿色方块的区域是内容最大的元素,所以在这个例子中,LCP等于这个元素开始渲染的时间。

除了上述前端页面性能监控指标,我们通常也需要监控页面的异常情况:

类型内容处理方法
JS异常代码问题、浏览器兼容性问题、安全限制、资源加载问题、IO异常、用户侧原因等监控处理
ajax异常错误请求(HTTP 4XX, 5XX等)、慢请求使用 SWR 管理
静态资源异常CDN 加载错误TNC 方案
白屏大部分是有 React 中的报错导致监控处理、ErrorBoundary
安全问题XSS、CSRF、CSP输入内容过滤、开启 CSP、同源检测、SameSite

以上就是本次分享的全部内容,如果大家有稳定性相关的内容,欢迎在评论区交流~