1. 前言
服务的质量对于企业来说至关重要。因为,服务故障可能会有很多潜在的影响,包括用户的不满、伤害,或丧失信任;直接或者间接的收入损失;品牌以及口碑上的影响;不良的新闻报道等。为了提高服务质量,减少服务故障时间我们需要额外关注计划外停机这个指标。
对于大多数服务而言,最直接的能够代表风险承受能力的指标就是对于计划外停机时间的可接受水平。计划外停机时间是由服务预期的可用性水平所体现的,通常我们愿意用提供的“9”系列的数字来体现,比如可用性为99.9%(3个9)、99.99%(4个9)。
服务可用性可以基于时间计算也可以基于产量的指标计算。
- 基于时间的可用性: 可用性=系统正常运行时间/(系统正常运行时间+计划停机时间)
基于时间的可用性计算方式对于Google或者Alibaba这样的公司来说性通常是毫无意义的,因为他们通常部署的是全球的分布式服务,他们所采用的故障隔离手段使得他们能够保证在任何时候、任何地方对于一个给定的服务,总是可以处理一定的用户流量。
- 基于产量的可用性:可用性=成功请求数/总的请求数
根据服务类型的不同,对服务风险容忍度的要求也是不同的。在构建和运维基础设施服务的要求在许多方面是不同于消费者服务的。一个根本的区别是,基础设施组件有多个客户,而他们通常有很多不同的需求。在成本效益条件下满足这些竞争性约束的方式就是将基础设施分割成多个服务,在多个独立的服务水平上提供该服务。
服务质量是 MTTR(平均故障修复时间)、MTBF(平均无故障时间)、MTTF(平均失败时间)的函数(参考系统故障的度量指标:MTTR,MTBF,MTTF)。评价一个团队将系统恢复到正常情况的最有效指标,就是MTTR。
在保障系统可靠性方面并没有什么万能药,有的只是极强的务实态度。 在这里主要结合《SRE:Google运维解密》(强烈推荐阅读)和一些我在生产中实践,谈一谈服务质量和服务的可观测体系建设。
2. 服务质量术语
2.1 服务质量指标(SLI)
服务质量指标(SLI)是指服务的某项服务质量的一个具体量化指标。 理想情况下,SLI应该直接度量某一个具体的服务质量,常见的SLI包括:
- 错误率(Fail-rate)- 请求处理失败的百分比
- 系统吞吐量(Throughput)- 每秒请求数量
- 请求延迟(Latency)- 处理请求所消耗的时间
- 可用性(Availability)- 服务可用时间的百分比
- 持久性(Durability)- 对数据存储系统来说,数据能够完整保存的时间
2.2 服务质量目标(SLO)
服务质量目标(SLO)指服务的某个 SLI 的目标值,或者目标范围。 SLO 的定义是 SLI≤目标值,或者范围下限≤ SLI ≤ 范围上限。
SLO 的选择不是一个纯粹的技术活动,这里还涉及产品和业务层面的决策,SLI 和 SLO(甚至SLA)的选择都应该直接反映该决策。同样的,有时候可能可以牺牲某些产品特性,以便满足人员、上线时间(time to market)、硬件可用性,以及资金的限制。
2.3 服务质量协议(SLA)
服务质量协议(SLA)指服务与用户之间的一个明确的,或者不明确的协议,描述了在达到或者没有达到SLO之后的后果。 这些后果可以是财务方面的—退款或者罚款—也可能是其他类型的。区别SLO和SLA的一个简单方法是问“如果SLO没有达到时,有什么后果?”如果没有定义明确的后果,那么我们就肯定是在讨论一个SLO,而不是SLA。
3. SLI 在实践中的应用
我们不应该将监控系统中的所有指标都定义为SLI;只有理解用户对系统的真实需求才能真正决定哪些指标是否有用。指标过多会影响对那些真正重要的指标的关注,而选择指标过少则会导致某些重要的系统行为被忽略。一般来说,四五个具有代表性的指标对系统健康程度的评估和关注就足够了。
根据我们的实践,大部分指标都应该以“分布”,而不是平均值来定义。例如,针对某个延迟SLI,某些请求可能很快,其他的可能会很慢,有时候会非常慢。简单平均可能会掩盖长尾延迟和其中的变化。而利用百分位指标可以帮助我们关注该指标的分布性:高百分位,如99.9%,99%体现了指标最差的情况,而 50% 则体现了普遍情况。响应时间的分布越分散,意味着普通用户受到长尾请求延迟的影响就越明显。
3.1 请求时长分布最佳实践
在 prometheus 中展示请求时长分布的数据结构有两种:histograms 和 summaries。
histogram 根据配置会预先划分若干个 bucket,它将每个采样点打到划分好的 bucket 中,并对每个观测点的值进行累计(sum)和次数进行累计(count),所以一个名为 的 histogram 会产生三个类别的指标:
- 观测桶的累计计数器,_bucket{le="上边界"}, 这个值为小于等于上边界的所有采样点数量。
- 观测值的总和,_sum。
- 已观测到事件的总数,_count(等价于 _bucket{le="+Inf"})。
一个 histogram 的例子:
如上表,histogram 的 buckets 设置为 buckets=[1,5,10],observerd value 为实际的观测值, Observe 表示观测值该 bucket 中的数量,即落在 [-,1] 观测值的数量为 2,落在 [1,5] 的观测值为数量为 3,落在 [5,10] 的观测值的数量为1,Write 为得到的最终结果(histogram 的最终结果中 bucket 计数是向下包含的):
[basename]_bucket{le="1"} = 2
[basename]_bucket{le="5"} = 5
[basename]_bucket{le="10"} = 6
[basename]_bucket{le="+Inf"} = 6
[basename]_count = 6
[basename]_sum = 17
例如,使用如下的 PromQL,通过 histogram 我们可以计算过去 1 分钟内平均请求时长:
rate(http_request_duration_seconds_sum[1m])
/
rate(http_request_duration_seconds_count[1m])
我们可能定义一个 SLO:一个服务 95% 的请求应该在300ms内完成并返回。 在这个场景下我们将 histogram 的一个桶的上限定义为 0.3s,然后,在这个服务在 300ms 内的请求的相对数量小于 95%时就发出告警。下面的 PromQL 计算最近5分钟内服务的请求的 SLO:
sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m])) by (job)
/
sum(rate(http_request_duration_seconds_count[5m])) by (job)
我们还可以一种非常相似的方式计算 Apdex 分数,以请求时长为例,如果服务请求时长的 SLO 为 300ms,那么请求时长的容忍上限为 1.2s(通常是 SLO 的 4 倍)。那个么根据 **Apdex=(满意样本+可容忍样本*0.5)/样本总数 **的计算公式,可是使用下面的 PromQL 计算过去 5 分钟内每个 job 的 Apdex 分数:
(
sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m])) by (job)
+
sum(rate(http_request_duration_seconds_bucket{le="1.2"}[5m])) by (job)
) / 2 / sum(rate(http_request_duration_seconds_count[5m])) by (job)
该计算与传统的 Apdex 分数不完全匹配,因为它包括计算中满意和可忍受的部分中的误差。
我们还可以使用 histogram 和 summary 来计算 φ-分位数(Quantiles),就是我们通常描述的TP99(99% 分位数)、TP50(中位数)。summary 和 histogram 的本质区别是:
- summary 在客户端计算分位数,然后直接暴露它们。
- histogram 使用在客户端暴露桶的技术,然后在服务端使用 histogram_quantitle() 函数来计算分位数。
这两种方法有许多不同的影响:
Histogram | Summary | |
---|---|---|
配置 | 需要根据观测的值合理划分桶的范围 | 选择所需的 φ 分位数和滑动窗口。其他 φ 分位数和滑动窗口无法稍后计算 |
客户端性能 | 几乎没有影响,因为只需要增加计数器 | 比较昂贵,需要使用计算资源,因为需要在客户端计算分位数,对CPU 敏感型的服务影响比较大。 |
服务端性能 | 服务端必须计算分位数,如果临时计算花费的时间太长(例如在大型仪表板中),可以使用记录规则进行优化。 | 服务端几乎没有影响。 |
时间序列的个数(除了_sum 和 _count) | 每个桶一个时间序列。 | 每个分位数一个时间序列。 |
分位数误差 | 误差在观测值的维度上受限于相关桶的宽度。 | 误差在 φ 的维度上受到可配置值的限制。 |
聚合 | 使用 histogram_quantitle() 函数 | 一般不可聚合 |
在实践中我们更多的时候使用的是 histogram 因为它对客户端的几乎没有影响,使用 histogram 最重要的是如何分配桶, 桶的分配会直接影响分位数的误差。在 micrometer 中提供了构建带有 histogram 的 Timer :
Timer.builder("my.timer")
.publishPercentileHistogram() // (1)
.minimumExpectedValue(Duration.ofMillis(1)) // (2)
.maximumExpectedValue(Duration.ofSeconds(60)) // (3)
对于 prometheus,histogram 中的桶由 Mmcrometer 根据生成器(PercentileHistogramBuckets)预设,这个生成器是由 Netflix 根据经验确定的,可以在大多数真实世界的计时器和分布摘要上产生一个合理的误差边界。默认情况下,生成器生成 276 个存储桶,但 micrometer 只包括那些在 minimumExpectedValue 和 maximumExpectedValue 设置范围内的桶。默认情况下,micrometer 将 Timer 固定为 1ms 到 1min 的范围,每个计时器维度产生 73 个桶。
4. SLO 在实践中的应用
SLO 的制定并不是一个技术问题,而是一个产品问题。在制定 SLO 时我们必须考虑以下几个方面的问题:
- 基于用户的使用习惯,服务的可靠性达到什么样的程度用户才会满意。
- 服务可靠程度不够,用户是否有其他代替选项。
- 服务的可靠程度是否会影响用户对这项服务的使用模式?
要求 SLO 能够被 100% 满足是不正确,也是不现实的,过于强调这个会降低创新和服务部署的速度,增加运营成本。达不到 SLO 的现象的发生频率对用户可见的服务健康度来说是一个非常有用的指标,每日(或者每周)对 SLO 达标程度进行监控可以展示一个趋势,这样就可以在重大问题发生之前得到预警,开发者和管理者应该按月或者按季度对 SLO 达标程序进行评估。当然 SLO 的选择不是一个纯粹的技术活动,因为这里还涉及产品和业务层面的决策 SLI 和 SLO(甚至SLA)的选择都应该直接反映该决策。同样的,有时候可能可以牺牲某些产品特性,以便满足上线时间(time to market)、硬件可用性,以及资金的限制。
在企业中,最主要的矛盾就是迭代创新的速度与产品稳定程度之间的矛盾。 在 google 中面对这种矛盾的工具是**错误预算。**错误预算源于这样一个理念:任何产品都不是,也不应该做到100% 可靠(显然这并不适用于心脏起搏器和防抱死刹车系统等)。一般来说,任何软件系统都不应该一味地追求100% 可靠。因为对最终用户来说,99.999% 和 100% 的可用性是没有实质区别的。从最终用户到服务器之间有很多中间系统,这些系统综合起来可靠性要远低于 99.999%。所以,在99.999% 和 100%之间的区别基本上成为其他系统的噪声。就算我们花费巨大精力将系统变为100% 可靠也并不能给用户带来任何实质意义上的好处。
5. 可观测体系建设
如下图,云原生下的可观测性建设包含三个方面:指标监控(Metrics)、日志事件(Logging)、链路追踪(Tracing)。
5.1 如何采集数据
数据的采集应该尽量使用 instrumentation 或者 ebpf 等无侵入的方案。例如使用 opentelemetry-java-instrumentation。下面是 Opentelemetry 可观测性采集架构:
5.2 采集哪些指标
5.2.1 4个黄金指标
延迟
服务处理某个请求所需要的时间。这里区分成功请求和失败请求很重要。例如,某个由于数据库连接丢失或者其他后端问题造成的HTTP 500错误可能延迟很低。计算总体延迟时,如果将500回复的延迟也计算在内,可能会产生误导性的结果。但是,“慢”错误要比“快”错误更糟!因此,监控错误回复的延迟是很重要的。
流量
使用系统中的某个高层次的指标针对系统负载需求所进行的度量。
- 对Web服务器来说,该指标通常是每秒HTTP请求数量,同时可能按请求类型分类(静态请求与动态请求)。
- 对音频流媒体系统来说,这个指标可能是网络I/O速率,或者并发会话数量。
- 对键值对存储系统来说,指标可能是每秒交易数量,或每秒的读取操作数量。
错误率
请求失败的速率。
饱和度
评估服务容量有多“满”。通常是系统中目前最为受限的某种资源的某个具体指标的度量。(在内存受限的系统中,即为内存;在I/O受限的系统中,即为I/O)。这里要注意,很多系统在达到100% 利用率之前性能会严重下降,增加一个利用率目标也是很重要的。像 Tomcat、Web Reactive(Netty)一般以线程多少来评估服务的饱和度。
5.2.2 应用指标
应用的指标可以查看 SpringBoot Production-ready Features。一般来说应用监控大盘应该包含以下几个方面:
- 应用程序启动指标
- 应用程序启动的时间点
- 应用程序启动时长(从启动到现在)
- 应用程序准备好为请求提供服务所需的时时间
- 启动应用程序所需的时间
- JVM
- 堆内存使用指标、非堆内存使用指标、装载和卸载类数量等
- GC 指标(GC次数、GC耗时、内存晋升情况)
- 线程利用率指标(线程数量、各线程状态)
- Runtime(Go语言)
- GC 指标(GC次数、GC耗时)
- 内存使用指标
- 缓存指标(缓存命中比率)
- 日志事件指标
5.2.3 中间件组件指标
- 熔断器指标
- 限流器指标
- 壁仓指标
- 重试指标
- 注册中心指标(不同类型注册中心关注的指标可能不太相同)
- 推送指标(推送延迟、推送次数、推送包体积)
- 订阅指标(长连接数量、心跳处理速率)
- 分布式共识系统指标
- 每个共识组成员指标(成员数量、成员的状态(健康或不健康)、数据同步情况)
- Leader 指标(Leader是否存在、Leader角色变化次数、任期)
- 吞吐和延迟指标(系统每秒处理的字节数、提议(日志记录)接收时间的延迟分布、接收者在持久化日志上花费的时间、系统不同部分之间观察到的网络延迟)
- 提议(日志记录)指标(提议数量,被接收提议的数量)
分布式共识系统是什么? 架构师不能通过牺牲正确性来满足可靠性或者性能的要求,尤其是在处理关键数据时。例如,假设某个系统处理财务交易:可靠性和性能在最终结果不正确的情况下一文不值。系统必须能够可靠地在多个进程中同步关键状态。分布式共识算法就提供了这种功能。如 Zookeeper、Etcd。
5.3 关于告警
最普遍的和传统的报警策略是针对某个特定的情况或者指标,一旦出现情况或者指标超过阈值就触发l警报(钉钉、微信、Email、短信)。但是这样的报警策略并不是非常有效:一个需要人工阅读邮件和分析警报来决定目前是否需要采取某种行动的系统从本质上就是错误的。监控系统不应该依赖人来分析警报信息,而是应该由系统自动分析,仅当需要用户执行某种操作时,才需要通知用户。
一个监控系统应该只有三类输出。
5.3.1 紧急警报(alert)
意味着收到警报的用户需要立即执行某种操作,目标是解决某种已经发生的问题,或者是避免即将发生的问题。
5.3.2 工单(ticket)
意味着接受工单的用户应该执行某种操作,但是并非立即执行。系统并不能自动解决目前的情况,但是如果一个用户在几天内执行这项操作,系统不会受到任何影响。
5.3.3 日志(logging)
平时没有人需要关注日志信息,但是日志信息依然被收集起来以备调试和事后分析时使用。正确的做法是平时没人会去主动阅读日志,除非有特殊需要。
6. 写在最后
服务质量和服务稳定性建设是一个非常复杂的问题,可观测性只是保证服务稳定性和提升服务质量的一个方面,后面我会和大家分享如何利用Opentelemetry 构建端到端的可观测性体系。最后再次向大家推荐 《SRE:Google运维解密》。