Google Borgmon

监控是生产环境必需的一部分,是稳定服务的基础。Borg出现于2003年,是Google的调度服务;而Borgmon是Google的监控服务,是对Borg的补充。

监控的挑战和需求:

  • Google体量下,要分析大量系统组件
  • 需要合理的维护成本
  • 既要对单个组件,也要对整体区域等多个维度的测量、分析和报警

在过去的十年里,Google的监控系统从自定义的脚本进化到了Borgmon,提供了新的基于时间序列(time-series)的监控系统。

应用与接口

Google使用varz方法展示内部状态,为了进行大规模的数据采集,varz被标准化,可以通过一个HTTP请求来获取所有的测量值。比如:

% curl http://webserver:80/varz
http_requests 37  
errors_total 12  

还可以通过变量映射获取更多信息,下面代码是对HTTP返回码的统计:

http_responses map:code 200:25 404:0 500:12  

Google在它的应用里都内置了HTTP Server用来展示内部状态,并且在主要的编程语言里都有输出统计信息的接口。Go语言的expvar库和JSON输出是类似的API接口,示例参考这篇文章

收集数据

Borgmon通过配置好的target list,获取每一个target的 /varz URI内容,解析结果,并且存储在自己的内存里。除了定义好的变量,Borgmon还会收集一些别的信息:

  • target对应的host和port
  • target是否响应数据收集请求
  • target是否响应健康检查请求
  • 数据收集完成时间

这些收集的数据可以用来检查监控任务是否正常运作。

时间序列数据的存储

一些概念定义:

A service is typically made up of many binaries running as many tasks, on many machines, in many clusters.  

Borgmon把所有的数据存储在内存数据库里,定时checkpoint到磁盘上,并且会周期性的打包到外部的系统TSDB(Time-Series Database,开源)。对于一个周期性1min的检测任务,每次产生24bytes数据,那么对于100w个时间序列数据,有:

24bytes * 12hour * 60min * 100w = 17GB

即,存储12小时的时间序列数据,大概需要17GB的空间。

Borgmon的数据模型是一个矩阵。

Matrix

每个点的格式是(timestamp, value),表示时间点和具体值;纵列串起来的是一个time-series,由一个label集合定义(上图中的host1, host2等分别表示每一个time-series)

实际上,这个结构是一块固定大小的内存,称为time-series arena;当这块内存满了之后,由一个garbage collector过期掉最老的数据。通常情况下,DC(data center)和全局的Borgmon server一般会存储12个小时的数据,底层的server存储的时间会少的多。

labels and vectors

如上图所示,time-series被存储为(timestamp, value)的序列,通常称之为vector,时间节点一般间隔是固定的而被忽略掉。

Vector

time-series的名字是label的集合,KV格式;有几个label是比较重要的:

  • var: 变量名字
  • job: 监控的类型
  • service: 为用户提供的服务
  • zone: server位置,通常指DC

这些变量组合起来称为variable expression,一个示例如下

 {var=http_requests,job=webserver,instance=host0:80,service=web,zone=us-west}

一个查询可以指定一部分条件:

{var=http_requests,job=webserver,service=web,zone=us-west}

得到结果:

{var=http_requests,job=webserver,instance=host0:80,service=web,zone=us-west} 10
{var=http_requests,job=webserver,instance=host1:80,service=web,zone=us-west} 9
{var=http_requests,job=webserver,instance=host2:80,service=web,zone=us-west} 11
{var=http_requests,job=webserver,instance=host3:80,service=web,zone=us-west} 0
{var=http_requests,job=webserver,instance=host4:80,service=web,zone=us-west} 10

还可以查询指定时间范围的数据:

{var=http_requests,job=webserver,service=web,zone=us-west}[10m]
{var=http_requests,job=webserver,instance=host0:80, ...} 0 1 2 3 4 5 6 7 8 9 10
{var=http_requests,job=webserver,instance=host1:80, ...} 0 1 2 3 4 4 5 6 7 8 9
{var=http_requests,job=webserver,instance=host2:80, ...} 0 1 2 3 5 6 7 8 9 9 11
{var=http_requests,job=webserver,instance=host3:80, ...} 0 0 0 0 0 0 0 0 0 0 0
{var=http_requests,job=webserver,instance=host4:80, ...} 0 1 2 3 4 5 6 7 8 9 10

Rule Evaluation

Borgmon在上述数据收集和存储基础设施的基础上,通过规则计算得到进一步的数据。一些特点如下:

  • 可以使用简单代数表达式,通过time-series定义另一个time-series
  • 在可能的地方并行执行
  • Borgmon内部也有监控,以进行debug和自我检测
  • 可以进行聚合,如sum

例如,我们想在web server报错超过一定比例的时候报警,或者说在非200返回码,占总请求的比例超过某个值的时候报警。

rules <<<  
      # Compute the rate of requests for each task from the count of requests
      {var=task:http_requests:rate10m,job=webserver} =
        rate({var=http_requests,job=webserver}[10m]);
      # Sum the rates to get the aggregate rate of queries for the cluster;
      # ‘without instance’ instructs Borgmon to remove the instance label
      # from the right hand side.
      {var=dc:http_requests:rate10m,job=webserver} =
        sum without instance({var=task:http_requests:rate10m,job=webserver})
    >>>

task:http_requests:rate10m的中间结果:

{var=task:http_requests:rate10m,job=webserver,instance=host0:80, ...} 1
{var=task:http_requests:rate10m,job=webserver,instance=host2:80, ...} 0.9
{var=task:http_requests:rate10m,job=webserver,instance=host3:80, ...} 1.1
{var=task:http_requests:rate10m,job=webserver,instance=host4:80, ...} 0
{var=task:http_requests:rate10m,job=webserver,instance=host5:80, ...} 1

dc:http_requests:rate10m的中间结果:

{var=dc:http_requests:rate10m,job=webserver,service=web,zone=us-west} 4

计算整体比例:

rules <<<  
       # Compute a rate pertask and per ‘code’ label
       {var=task:http_responses:rate10m,job=webserver} =
         rate by code({var=http_responses,job=webserver}[10m]);
       # Compute a cluster level response rate per ‘code’ label
       {var=dc:http_responses:rate10m,job=webserver} =
         sum without instance({var=task:http_responses:rate10m,job=webserver});
       # Compute a new cluster level rate summing all non 200 codes
       {var=dc:http_errors:rate10m,job=webserver} = sum without code(
         {var=dc:http_responses:rate10m,jobwebserver,code=!/200/};
       # Compute the ratio of the rate of errors to the rate of requests
       {var=dc:http_errors:ratio_rate10m,job=webserver} =
         {var=dc:http_errors:rate10m,job=webserver}
           /
         {var=dc:http_requests:rate10m,job=webserver};
     >>>

中间结果:

{var=task:http_responses:rate10m,job=webserver}

{var=task:http_responses:rate10m,job=webserver,code=200,instance=host0:80, ...} 1
{var=task:http_responses:rate10m,job=webserver,code=500,instance=host0:80, ...} 0
{var=task:http_responses:rate10m,job=webserver,code=200,instance=host1:80, ...} 0.5
{var=task:http_responses:rate10m,job=webserver,code=500,instance=host1:80, ...} 0.4
{var=task:http_responses:rate10m,job=webserver,code=200,instance=host2:80, ...} 1
{var=task:http_responses:rate10m,job=webserver,code=500,instance=host2:80, ...} 0.1
{var=task:http_responses:rate10m,job=webserver,code=200,instance=host3:80, ...} 0
{var=task:http_responses:rate10m,job=webserver,code=500,instance=host3:80, ...} 0
{var=task:http_responses:rate10m,job=webserver,code=200,instance=host4:80, ...} 0.9
{var=task:http_responses:rate10m,job=webserver,code=500,instance=host4:80, ...} 0.1

{var=dc:http_responses:rate10m,job=webserver}

{var=dc:http_responses:rate10m,job=webserver,code=200, ...} 3.4
{var=dc:http_responses:rate10m,job=webserver,code=500, ...} 0.6

{var=dc:http_responses:rate10m,jobwebserver,code=!/200/} 

{var=dc:http_responses:rate10m,job=webserver,code=500, ...} 0.6

{var=dc:http_errors:rate10m,job=webserver} 
{var=dc:http_errors:rate10m,job=webserver, ...} 0.6

{var=dc:http_errors:ratio_rate10m,job=webserver} {var=dc:http_errors:ratio_rate10m,job=webserver} 0.15

Alerting

报警规则是类似的语法,此外还可以指定持续时间以避免状态快速变化带来的false alert。

rules <<<  
      {var=dc:http_errors:ratio_rate10m,job=webserver} > 0.01
        and by job, error
      {var=dc:http_errors:rate10m,job=webserver} > 1
        for 2m
        => ErrorRatioTooHigh
          details "webserver error ratio at [[trigger_value]]"
          labels {severity=page};
    >>>

在上面这个例子里,指定错误率大于1%,错误数大于1,并且持续2分钟的情况下,才会报警;报警可以指定信息和数值。

Borgmon会通过AlertRPC发送请求给AlertManager,AlertManager会将报警信息发送给正确的地址。

Sharding the Monitoring Topology

Borgmon可以从其他的Borgmon导入数据,并且为了提升效率,使用流式接口而不是基于varz的文本接口。使用一个Borgmon收集数据、计算rule很快就会成为瓶颈,并且是单点,所以一个典型的部署如下图所示:

Topology

在这个部署里,最底层的Borgmon只用于收集数据,每个DC的Borgmon做一些聚集和rule计算,顶层的Borgmon可能会用于rule计算和展示。这样每一层的Borgmon都只处理和存储自己感兴趣的数据,数据不断流转到上层去。

Black-Box Monitoring

Borgmon是一个白盒监控系统,意味着它可以看到系统的内部状态,各种统计数据、queue是否已满、组件fail等等信息。但是这并不是系统全景,它不能看到用户看到的情况。你只能看到到达的query信息,用户到达不了的请求是看不到的,比如DNS问题;用户丢失的query是看不到的,比如server宕机了。

Google内部使用Prober解决这个问题,它可以给服务发送请求,并且解析payload协议(比如HTTP和HTML),甚至直接把内容生成time-series。Prober可以直接发送报警给AlertManager,或者开放本身的varz接口,供Borgmon收集。Prober经常被用来生成payload size/operation和RT的直方图,用来分析用户视角的性能。

Maintaining the Configuration

Borgmon分离了rule和被监控的目标,那么同一组rule可以同时用于多个目标,而不用为每个目标设置相同的配置。这看起来微不足道,但是极大的减小了维护重复配置的的代价。

Borgmon也支持语言模板,这个类似宏的系统使工程师可以重复利用写好的规则,这个功能也减少了重复的工作。

Borgmon也提供了单元测试和回归测试的功能,以保证rule执行和作者意图相符。Production Monitoring团队持续执行着集成测试任务,运行着这些测试,以在上线前验证这些配置。

在已经出现的公用模板里,有两种类型。一种是代码库输出的信息,比如HTTP Server库,内存分配,RPC服务等。这些代码库的使用者就可以复用这些代码库的varz模板。第二种模板用来聚集数据,比如来自单个server的任务数据,来自全局服务的数据等。这些模板包含一些常用的聚集规则,可以让工程师为他们的服务建模。

十年

Borgmon把check-and-alert的模型转化成了大规模变量收集和集中对time-series进行rule计算的模式。这种解耦让系统的规模增长可以独立于rule计算,并且更容易维护。新的应用使用的所有组件和库都有统计信息;而聚集模板又减少了实现的代价。

十年是很长的时间,随着不断的实验和变化,Google内部的监控系统也在不断进化,随着公司规模增长不断提升。

虽然Borgmon只在Google内部使用,但是这种处理time-series数据的思想已经体现在了一些开源的工具里,比如Prometheus,Riemann,Heka,Bosun等等。

flacro

Read more posts by this author.