PostgreSQL的KPI

管数据库和管人差不多,都需要定KPI(关键性能指标)。那么数据库的KPI是什么?本文介绍了一种衡量PostgreSQL负载的方式:使用一种单一横向可比,与负载类型和机器类型基本无关的指标,名曰PG Load(PG负载)

管数据库和管人差不多,都需要定KPI(关键性能指标)。那么数据库的KPI是什么?本文介绍了一种衡量PostgreSQL负载的方式:使用一种单一横向可比,与负载类型和机器类型基本无关的指标,名曰PG Load(PG负载)

0x01 Introduction

在现实生产中,经常会有衡量数据库性能与负载,评估数据库水位的需求。一种最朴素的形式就是,能不能有一个类似于KPI的单一指标,能直接了当地告诉用户他心爱的数据库负载有没有超过警戒线?工作量到底饱和不饱和?

当然这里其实隐含着一个重要信息,即用户期待的负载指标是一个饱和度(Saturation)指标,所谓饱和度,即服务容量有多”满“,通常是系统中目前最为受限的某种资源的某个具体指标的度量。通常来说,0%的饱和度意味着系统完全空闲,100%的饱和度意味着满载,系统在达到100%利用率前就会出现性能的严重下降,因此设定指标时还需要包括一个利用率目标,或者说水位红线、黄线,当系统瞬时负载超过红线时应当触发告警,长期负载超过黄线时应当进行扩容。

不幸的是,定义系统有多”饱和“并不是一件容易的事情,往往需要借助某些间接指标。评估一个数据库的负载程度,传统上通常会基于这样几类指标进行综合评估:

  • 流量:每秒查询数量QPS,或每秒事务数量TPS。

  • 延迟:查询平均响应时间 Query RT,或事务平均响应时间Xact RT

  • 饱和度:机器负载(Load),CPU使用率,磁盘读写带宽饱和度,网卡IO带宽饱和度

  • 错误:数据库客户端连接排队

这些指标对于数据库性能评估都很有参考意义,但它们也都存在各式各样的问题。

0x02 常用评估指标的问题

让我们来看一看,这些现有的常用指标都有哪些问题。

第一个Pass的当然是错误类指标,譬如连接池排队。错误类指标最大的问题就是,当错误出现时,饱和度可能已经没有意义了。评估饱和度的一个重要原因就是用于预防系统过载,如果系统已经过载大量报错,那么使用错误现象反过来定义饱和度是没有意义的。此外,错误类指标难以精确量化。我们只能说:当连接池出现排队时,数据库负载比较大;队列越长,负载越大;没有排队时,数据库负载不怎么大,仅此而已。这样的定义当然也无法让人满意。

第二个Pass的则是系统层(机器级别)指标,数据库运行在机器上,CPU使用率,IO使用率这样的指标与数据库负载程度密切相关,如果CPU和IO是瓶颈,理论上当然是可以直接使用瓶颈资源的饱和度指标作为数据库的饱和指标,但这一点并非总是成立的,有可能系统瓶颈在于数据库本身。而且严格来说它们是机器的KPI而不是DB的KPI,评估数据库负载时当然可以参照系统层的指标,但DB层也应该有本层的评估指标。要先有数据库本身的饱和度指标,才可以去比较底层资源和数据库本身到底谁先饱和谁是瓶颈。这条原则同样适用于应用层观察到的指标。

流量类的指标很有潜力,特别是QPS,TPS这样的指标相当具有代表性。但这些指标也存在问题。一个数据库实例上的查询往往是五花八门各式各样的,一个耗时10微秒的查询和一个10秒的查询在统计时都被算为一个Q,类似于QPS这样的指标无法进行横向比较,只有比较粗略的参考意义,甚至当查询类型发生变化时,都无法和自己的历史数据进行纵向比较。此外也很难针对QPS、TPS这样的指标设置利用率目标,同一个数据库执行SELECT 1可以打到几十万的QPS,但执行复杂SQL时可能就只能打到几千的QPS。不同负载类型和机器硬件会对数据库的QPS上限产生显著影响,只有当一个数据库上的查询都是高度单一同质且没有复杂变化的条件下,QPS才有参考意义,在这种苛刻条件下倒是可以通过压力测试设定一个QPS的水位目标。

比起QPS/TPS,RT(响应时间 Response Time)这样的指标反而更具有参考价值。因为响应时间增加往往是系统饱和的前兆。根据经验法则,数据库的负载越大,查询与事务的平均响应时间也会越高。RT相比QPS的一个优势是**,RT是可以设置一个利用率目标的**,比如可以为RT设定一个绝对阈值:不允许生产OLTP库上出现RT超过1ms的慢查询。但QPS这样的指标就很难画出红线来。不过,RT也有自己的问题。第一个问题是它依然是定性而非定量的,延迟增加只是系统饱和的预警,但没法用来精确衡量系统的饱和度。第二个问题通常能从数据库与中间件获取到的RT统计指标都是平均值,但真正起到预警效果的有可能是诸如P99,P999这样的统计量。

这里把常用指标都批判了一番,到底什么样的指标适合作为数据库本身的饱和度呢?

0x03 衡量PG的负载

我们不妨参考一下**机器负载(Node Load)CPU利用率(CPU Utilization)**的评估指标是如何设计的。

机器负载(Node Load)

想要看到机器的负载水平,可以在Linux系统中使用top命令。top命令的第一行输出就醒目地打印出当前机器1分钟,5分钟,15分钟的平均负载水平

$ top -b1
top - 19:27:38 up 18:49,  1 user,  load average: 1.15, 0.72, 0.71

这里load average后面的三个数字分别表示最近1分钟,5分钟,15分钟系统的平均负载水平。

那么这个数字到底是什么意思呢?简单的解释是,这个数字越大机器越忙。

在单核CPU的场景下,Node Load(以下简称负载)是一个非常标准的饱和度指标。对于单核CPU,负载为0时CPU处于完全空闲的状态,负载为1(100%)时,CPU正好处于满载工作的状态。负载大于100%时,超出100%部分比例的任务正在排队。

Node Load也有自己的利用率目标,通常的经验是在单核情况下:0.7(70%)是黄线,意味着系统有问题,需要尽快检查;1.0(100%)是红线,负载大于1意味着进程开始堆积,需要立即着手处理。5.0(500%)是死线,意味着系统基本上已经堵死了。

对于多核CPU,事情稍微有点不一样。假设有n个核,那么当系统负载为n时,所有CPU都处于满载工作的状态;而当系统负载为n/2时,姑且可以认为一半CPU核正在满载运行。因而48核CPU的机器满载时的负载为48。总的来说,如果我们把机器负载除以机器的CPU核数,得到的指标就与单核场景下保持一致了(0%空载,100%满载)。

CPU利用率(CPU Utilization)

另一个很有借鉴意义的指标是CPU利用率(CPU Utilization)。CPU利用率其实是通过一个简单的公式计算出来的,对于单核CPU:

1 - irate(node_cpu_seconds_total{mode="idle"}[1m]

这里node_cpu_seconds_total{mode="idle"}是一个计数器指标,表示CPU处于空闲状态的总时长。irate函数会用该指标对时间进行求导,得出的结果是,每秒CPU处于空闲状态的时长,换句话说也就是CPU空闲率。用1减去该值就得到了CPU的利用率。

对于多核CPU来说,只需要把每个CPU核的利用率加起来,除以CPU的核数,就可以得到CPU的整体利用率。

那么这两个指标对于PG的负载又有什么借鉴意义呢?

数据库负载(PG Load)

PG的负载是不是也可以采用类似于CPU利用率和机器负载的方式来定义?当然可以,而且这是一个极棒的主意。

让我们先来考虑单进程情况下的PG负载,假设我们需要这样一个指标,当该PG进程完全空闲时负载因子为0,当该进程处于满载状态时负载为1(100%)。类比CPU利用率的定义,我们可以使用“单个PG进程处于活跃状态的时长占比”来表示“单个PG后端进程的利用率”。

如图1所示,在一秒的统计周期内,PG处于活跃(执行查询或者执行事务)状态的时长为0.6秒,那么这一秒内的PG负载就是60%。如果这个唯一的PG进程在整个统计周期中都处于忙碌状态,而且还有0.4秒的任务在排队,如那么就可以认为PG的负载为140%。

对于并行场景,计算方法与多核CPU的利用率类似,首先把所有PG进程在统计周期(1s)内处于活跃状态的时长累加,然后除以“可用的PG进程/连接数”,或者说“可用并行数”,即可得到PG本身的利用率指标,如图3所示。两个PG后端进程分别有200ms+400ms与800ms的活跃时长,那么整体的负载水平为:(0.2s + 0.4s + 0.8s) / 1s / 2 = 70%

总结一下,某一段时间内PG的负载可以定义为:

pg_load = pg_active_seconds / time_peroid / parallel

  • pg_active_seconds是该时间段内所有PG进程处于活跃状态的时长之和。

  • time_peroid是负载计算的统计周期,通常为1分钟,5分钟,15分钟,以及实时(小于10秒)。

  • parallel 是PostgreSQL的可用并行数,后面会详细解释。

因为前两项之商实际上就是一段时间内的每秒活跃时长总数,因此这个公式进一步可以简化为活跃时长对时间的导数除以可用并行数,即:

rate(pg_active_seconds[time_peroid]) / parallel

time_peroid通常是固定的常量(1,5,15分钟),所以问题就是如何获取PG进程活跃总时长pg_active_seconds这个指标,以及如何评估计算数据库可用并行数max_parallel 了。

0x04 计算PG的负载饱和度

事务还是查询?

当我们说数据库进程 活跃/空闲 时,究竟在说什么? PG处于活跃状态,到底是什么意思?如果PG后端进程正在执行查询,那么当然可以认为PG正处于忙碌状态。但如果如上图4所示,PG进程正在执行一个交互式事务,但没有实际执行查询,即所谓的“Idle in Transaction”状态,又应该怎么计算“活跃时长”呢?图4中两个查询中空闲的那200ms时间。那么这段时间应该算作“活跃”,还是算作“空闲”呢?

这里的核心问题是怎么定义活跃状态:数据库进程位于事务中算活跃,还是只有当实际执行查询时才算活跃。对于没有交互式事务的场景,一个查询就是一个事务,用哪种方式都一样,但对于多语句,特别是交互式的多语句事务,这两者就有比较明显的区别了。从资源使用的角度看,没有执行查询也就意味着没有消耗数据库本身的资源。但空闲着的事务本身会占用连接导致连接无法复用,Idle In Transaction本身也应当是一种极力避免的情况。总的来说,这两种定义方式都可以,使用事务的方式会略微高估应用负载,但从负载评估的角度可能会更为合适。

如何获取活跃时长

决定了数据库后端进程的活跃定义后,第二个问题就是,如何获取一段时间的数据库活跃时长?不幸的是在PG中,用户很难通过数据库本身获取这一性能指标。PG提供了一个系统视图:pg_stat_activity,可以看到当前运行着的Postgres进程里列表,但这是一个时间点快照,只能大致告诉在当前时刻,数据库的后端进程中有多少个处于活跃状态,有多少个处于空闲状态。统计一段时间内数据库处于活跃状态的时长,就成了一个难题。一种解决方案是使用类似于Load的计算方式,通过周期性地采样PG中活跃进程的数量,计算出一个负载指标来。不过,这里有更好的办法,但是需要中间件的协助参与。

数据库中间件对于性能监控非常重要,因为很多指标数据库本身并没有提供,只有通过中间件才能暴露出来。以Pgbouncer为例,Pgbouncer在内部维护了一系列统计计数器,使用SHOW STATS可以打印出这些指标,诸如:

  • total_xact_count:总共执行了多少个事务
  • total_query_count:总共执行了多少个查询
  • total_xact_time:总共花费在事务执行的时长
  • total_query_time:总共花费在查询执行上的时长

这里total_xact_time就是我们需要的数据,它记录了Pgbouncer中间件中花费在某个数据库上的事务总耗时。我们只需要用这个指标对时间求导,就可以得到想要的数据:每秒活跃时长占比。

这里使用Prometheus的PromQL表达计算逻辑,首先对事务耗时计数器求导,分别算出其1分钟,5分钟,15分钟,以及实时粒度(最近两次采样点之间)上的每秒活跃时长。再上卷求和,将数据库层次的指标上卷为实例级别的指标。(连接池SHOW STATS这里的统计指标是以数据库为单位的,因此在计算实例级别的总活跃时长时,应当上卷求和,消除数据库维度的标签:sum without(datname)

- record: pg:ins:xact_time_realtime
expr: sum without (datname) (irate(pgbouncer_stat_total_xact_time{}[1m]))
- record: pg:ins:xact_time_rate1m
expr: sum without (datname) (rate(pgbouncer_stat_total_xact_time{}[1m]))
- record: pg:ins:xact_time_rate5m
expr: sum without (datname) (rate(pgbouncer_stat_total_xact_time{}[5m]))
- record: pg:ins:xact_time_rate15m
expr: sum without (datname) (rate(pgbouncer_stat_total_xact_time{}[15m]))

这样计算得到的结果指标已经可以相对本身进行纵向比较,并在同样规格的实例间进行横向比较了。而且无论数据库的负载类型怎样,都可以使用这个指标。

不过不同规格的实例,仍然没法使用这个指标进行对比。比如对于单核单连接PG,满载时每秒活跃时长可能是1秒,也就是100%利用率。而对于64核64连接的PG,满载时每秒活跃时长是64秒,那么就是6400%的利用率。因此,还需要一个归一化的处理,那么问题又来了。

可用并行数如何定义?

不同于CPU利用率,PG的可用并行数并没有一个清晰的定义,而且跟负载类型有一些微妙的关系。但能够确定的是,在一定范围内,最大可用并行与CPU的核数呈粗略的线性关系。当然这个结论的前提是数据库最大连接数显著超过CPU核数,如果在64核的CPU上只允许数据库建立30条连接,那么可以肯定最大可用并行就是30而不是CPU核数64。软件的并行最终还是要由硬件的并行度来支撑,因此我们可以简单的使用实例的CPU核数作为可用并行数。

在64核的CPU上运行64个活跃PG进程,则其负载为(6400% / 64 = 100%)。同理运行128个活跃PG进程,负载就是(12800% / 64 = 200%)。

那么利用上面计算得到的每秒活跃时长指标,就可以计算出实例级别的PG负载指数了。

- record: pg:ins:load0
expr:  pg:ins:xact_time_realtime / on (ip) group_left()  node:ins:cpu_count
- record: pg:ins:load1
expr: pg:ins:xact_time_rate1m  / on (ip) group_left()  node:ins:cpu_count
- record: pg:ins:load5
expr: pg:ins:xact_time_rate5m  / on (ip) group_left()  node:ins:cpu_count
- record: pg:ins:load15
expr: pg:ins:xact_time_rate15m  / on (ip) group_left()  node:ins:cpu_count

PG LOAD的另一种解释

如果我们仔细审视PG Load的定义,其实可以发现每秒活跃时长这个指标,其实可以粗略等价于:TPS x XactRT,或者QPS x Query RT。这个也很好理解,假设我QPS为1000,每个查询RT为1ms,则每秒花费在查询上的时间为 1000 * 1ms = 1s。

因此,PG Load可以视为一个由三个核心指标复合而成的衍生指标:tps * xact_rt / cpu_count

TPS,RT用于负载评估都有各自的问题,但它们通过简单的乘法结合成一个新的复合指标,一下子就显示出了神奇的力量。(尽管实际上是通过其他更准确的方式计算出来的)

0x05 PG Load的实际效果

接下来,我们来看一下PG Load用于实际生产环境的表现。

PG Load最直接的作用有两个,告警以及容量评估。

Case 1: 用于报警:慢查询堆积导致的服务不可用

下图是一次生产事故的现场,由于某业务上线了一个慢查询,瞬间导致连接池被慢查询占据,发生堆积。可以看出PG Load和RT都很及时地反映出了故障的情况,而TPS看上去则是掉了一个坑,并不是特别显眼。

从效果上看,PG Load1与PG Load0(实时负载)是一个相当灵敏的指标,对于大多数与压力负载有关的故障都能及时准确作出反应。所以被我们采纳为核心报警指标。

PG Load的利用率目标有一些经验值:黄线通常为50%,即需要引起关注的阈值;红线通常为70%,即报警线,需要立刻采取行动的阈值;500%或更高通常意味着这个实例已经被打崩了。

Case 2:用于水位评估与容量规划

比起报警,水位评估与容量规划更像是PG Load的核心用途。毕竟报警之类的的需求还是可以通过延迟,排队连接等指标来满足的。

这里,PG集群的15分钟负载是一个很好的参考值。通过这个指标的历史均值,峰值,以及其他一些统计量,我们可以很轻松地看出哪些集群处于高负载状态需要扩容,哪些集群处于低资源利用率状态需要缩容。

CPU利用率是另一个很重要的容量评估指标。我们可以看出,PG Load与CPU Usage有着很密切的关系。不过相比CPU使用率,PG Load更为纯粹地反映了数据库本身的负载水平,滤除了机器上的无关负载,也可以滤除掉数据库维护工作(备份,清理,垃圾回收)产生的杂音,更为丝滑平顺。因此非常适合用于容量评估。

当系统负载长期位于30%~50%时,就应该考虑进行扩容了。

0x06 结论

本文介绍了一种定量衡量PG负载的方式,即PG Load指标

该指标可以简单直观地反映数据库实例的负载水平

该指标非常适合作容量评估之用,也可以作为核心报警指标。

该指标可以基本无视负载类型与机器类型,进行纵向历史比较与横向水位比较。

该指标可以通过简单的方式计算得出,即每秒后端进程活跃总时长除以可用并发数。

该指标所需数据需要从数据库中间件获取

PG Load的0代表空载,100%代表满载。黄线经验值为50%,红线经验值为70%,

PG Load是一个好指标👍

WeChat Column

Last modified 2024-03-01: update blog images (ba0fa2b)