监控平台设计之Graphite&Prometheus存储

Posted 魏小言

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了监控平台设计之Graphite&Prometheus存储相关的知识,希望对你有一定的参考价值。

监控平台

监控诉求

监控平台在各个公司及部门都很普及,什么是监控,我们做监控的诉求是什么/?

监控对象

人、物、行为…归根结底是数据。

针对业务平台监控,数据可以是在线数据流、日志、数据库元数据…

OLAP

监控不应仅仅是对数据的收集展示,需要提供OLAP支持「行数据分析、挖掘及建模拆分/上下钻/排序/特定逻辑…」等处理,完成监控的智能化「观察动态数据趋势辅助业务做相关决策、定位问题、场景快照及复原…」,从而满足监控诉求,这样才是一个成熟的监控。

监控常见问题

  • 1、数据如何采集/?
  • 推or拉or…
  • 2、监控系统架构是否可扩展伸缩/?高可用/?吞吐量如何/?
  • 包含降级策略
  • 3、原始/加工/半加工数据如何存储/?
  • 存储产品选型
  • 4、数据对象关系如何扩展
  • 数据模式化
  • 5、…

系统架构整体选型

有了idea,轮子重复造意义不大。

成熟的监控组合:ES+Kibana+Logstash、Graphite+Collectd、Grafana+Influxdb. 等等…

存储细分选型

监控类一般是时序数据库,按照底层技术不同可以划分为三类:

  • 1、直接基于文件的简单存储:RRD Tool,Graphite Whisper。这类工具附属于监控告警工具,底层没有一个正规的数据库引擎。只是简单的有一个二进制的文件结构。
  • 2、基于K/V数据库构建:opentsdb(基于hbase),blueflood,kairosDB(基于cassandra),influxdb,prometheus(基于leveldb)
  • 3、基于关系型数据库构建:mysql,postgresql 都可以用来保存时间序列数据
  • 偏业务监控选型一般为第3类,偏数据监控一般使用1-2类。

业界监控架构选型

JD\\ALibaba\\Tencent\\Baidu…各个公司/部分/业务不同,轮子也不少,暂不介绍

OOPS

OOPS:weibo广告监控平台「暂且叫这个吧…OOPS,后续可以考虑叫个高级的名字」

地址:XXXXX

目的

业务指标监控的对象是Trace日志,针对日志做离线/实时分析,从多个业务维度进行数据加工,进而达到加速问题定位、处理、及时止损;辅助支撑业务侧相关决策的目的。

数据链路

业务将生产日志落本地,之后写入Kafka,离线分析模块进行数据的落库【Hive】;实时分析模块进行Kafka数据消费,数据业务逻辑计算、复合等加工,至Graphite/Prometheus展示。

监控框架链路

实时分析模块支持两种监控框架链路:一是Graphite;二是Prometheus。Graphite日志持久化时间长,Prometheus指标维度丰富且占用资源少,设计及扩展简单,PromQL功能强大。总体来说也包含业务场景特殊及历史原因的存在,两个监控框架并用。

Graphite & Prometheus架构

Graphite原生无监控指标收集模块,数据存储为Whisper,含Web-ui,架构图如下:

在这里插入图片描述
Prometheus原生含收集、存储、Web-iu、AlterManage、explore;
架构图如下:
在这里插入图片描述

Metric

Graphite:metric名称以点".“分割组件,这种方式是一种维度的编码方式,通过”.“来潜在的提供分割数据标识。

Prometheus:在提供metric名称之外,明确的通过标签键值对标识metric不同的维度,更易于通过查询语句来过滤、分组、匹配metrics。

举个例子:

使用Graphite/StatsD存储状态码为500、方法是POST、路径是”/tracks"、服务名称为api-server(api-server有多个实例)的http请求数,这样一个监控指标:

stats.api-server.tracks.post.500 -> 93

使用Prometheus存储同样的监控指标:

api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="<sample1>"} -> 34
api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="<sample2>"} -> 28
api_server_http_requests_total{method="POST",handler="/tracks",status="500",instance="<sample3>"} -> 31

存储规则

Graphite

常用Whisper数据库[python],是一款类RRD(round-robin-database)[C]型数据库。特点是,把高精度的指标数据转换成低精度的指标数据以满足存储长期的历史数据的需求。比如说把按秒采集的指标转换成按分钟采集的指标,以减少数据量,进行长期存储。

RRD

数据库在存储的时候在存储的时候,按照“循环”round-robin的方式进行存储,也就是说,自己定义一个周期。在过了一个周期之后,后面的数据将覆盖前面的数据。

Whisper
数据点(监控指标)

一般是双精度类型,且饱含时间戳、业务指标。

数据存储规则

conf/storage-schemas.conf

[case_name]
Pattern =  ^fe_idx.*
retention = 15s:7d,1m:21d,15m:5y

模版
[name]                
#存储标识
pattern = regex       
#用来匹配具体指标名的正则表达式。如果配置文件里面定义了多个策略,那么收到一个指标数据的时候,会从上到下使用每个策略里面的pattern对指标名称进行正则表达式匹配,最先匹配到的策略将会被使用。
retentions = timePerPoint:timeToStore, timePerPoint:timeToStore, …        
#定义了数据采集精度和存储时长。timePerPoint就是多长时间采集一个数据点,timeToStore就是采集的数据最长存储多长时间。每个retentions后面可以定义多个timePerPoint:timeToStore对。每个timePerPoint:timeToStore对按高精度短时长到低精度长时长进行排序。上面的retentions包含了三个timePerPoint:timeToStore对,分别是15秒采集一个数据点,保存7天的数据,1分钟采集一个数据点,保存21天的数据和15分钟采集一个数据点,保存5年的数据。
指标精度转换规则

conf/storage-aggregation.conf

[case_name]
Pattern =  ^fe_idx.*
xFilesFactor = 0.2
aggregationMethod = average

模版
[name]
#规则标识
pattern = <regex>
#用来匹配具体指标名的正则表达式。如果配置文件里面定义了多个聚合规则,那么收到一个指标数据的时候,会从上到下使用每个规则里面的pattern对指标名称进行正则表达式匹配,最先匹配到的规则将会被使用。
aggregationMethod = <average|sum|last|max|min>
#数据聚合方法
xFilesFactor = <float between 0 and 1>
#必须是一个0到1之间的浮点型数值。这个值规定了要把高精度的数据转换成一个低精度的数据,高精度的数据必须有几个。
以15s:7d,1m:21d这个定义为例子,高精度的定义是15秒采集一个数据,而低精度的定义是1分钟采集一个数据。那么在高低精度数据转换的时候,正常情况下就是把4个数据点转换成一个数据点。
但是实际可能存在这样的情况,就是1分钟内的数据点没有4个,只有一个,两个,或者三个,就是有的时间点他没有采集到数据。那么xFilesFactor的意思就是在这种数据缺少的情况下,数据点数必须满足多少百分比,才能做数据聚合操作。如果定义成0.5,那么就是说,至少要有2个点才能做数据聚合操作,如果定义成0.1,那就是说只要有1个点就可以做数据聚合操作。
这个值定义成多少,还跟具体的数据聚合策略有关系。如果数据聚合策略是sum(求和),这种策略下就算没有数据点,也是可以做求和操作的,那么xFilesFactor就可以定义成0。如果数据聚合策略是min(求最小值),这种策略下,没有数据点肯定就没法取最小值,那么xFilesFactor就可以定义成0.1,就是说至少要有一个数据点,才能做聚合操作,等等。
行为及空间效率

数据库文件生成时,就把所有的数据点都创建出来了。比如说1m:1d这个定义,在收到第一个数据点的时候,这个数据库文件就被创建了,数据库文件里面总共1440个数据点。不管这些数据点有没有收集到值,数据点都会提前创建好,如果没有值,就是None。
以retentions = 15s:7d,1m:21d,15m:5y这个多精度定义为例子,当数据写入这个数据库时,数据会被同时写多份。数据会首先被写入到最高精度的数据点中,然后当满足数据聚合条件后,再把多个高精度的数据聚合,写到低精度的数据点中。

Prometheus

Prometheus存储是KV存储,使用的是LevelDb的引擎。特点是顺序读写性能非常高,与其存储机制有关。

K-V

Prometheus的监控指标格式是:metric_name{lable_name:label_val,……} timestamp sample,整体可划分为:Key:metric_name{lable_name:label_val,……} timestamp;value:ample 。「注:所有的数值都是64bit的。每条时间序列里面记录的其实就是64bit timestamp(时间戳) + 64bit value(采样值)」

其哈希值计算为:FNV(sort(metric_name,lable_name:label_val……)) 。

FNV哈希算法全名为Fowler-Noll-Vo算法,是以三位发明人Glenn Fowler,Landon Curt Noll,Phong Vo的名字来命名的,最早在1991年提出。
FNV能快速hash大量数据并保持较小的冲突率,它的高度分散使它适用于hash一些非常相近的字符串,比如URL,hostname,文件名,text,IP地址等。
LevelDb设计思路

采用LSM-Tree结构实现,将磁盘的随机写转换成有序写,从而提高写速度。

读性能

有序存储:比如 B-Tree/B+Tree,但是 B-Tree/B+Tree 会导致随机写。
哈希存储:不支持有序遍历。

写性能

Append by:所有写操作都是将数据添加到文件末尾,大约等于磁盘的理论速度;不支持有序遍历,且需定期清存,故适合于WAL

LSM-Tree「The Log-Structured Merge Tree」

顾名思义,日志结构+Merge「合并」,写入速度快,利用了磁盘的顺序写,在读写性能中折中,适合主put\\get场景,主要针对的场景是写密集、少量查询的场景;主在 key-value 存储系统应用,如 LevelDB,RocksDB,还有分布式行式存储数据库 Cassandra 也用了 LSM-tree 的存储架构。


LSM-Tree模型:


是一个多层结构,就更一个树一样,上小下大。首先是内存的 C0 层,保存了所有最近写入的 (k,v),这个内存结构是有序的,并且可以随时原地更新,同时支持随时查询。剩下的 C1 到 Ck 层都在磁盘上,每一层都是一个在 key 上有序的结构。

Put

首先存在WAL机制「Write Ahead Log,写入之前记录的日志」中,接下来加到memory中 C0 层。
当 C0 层的数据达到一定大小,就把 C0 层 和 C1 层合并,类似归并排序,这个过程就是Compaction(合并)。
合并出来的新的 new-C1 会顺序写磁盘,替换掉原来的 old-C1。当 C1 层达到一定大小,会继续和下层合并。合并之后所有旧文件都可以删掉,留下新的。

「注意数据的写入可能重复,新版本需要覆盖老版本。什么叫新版本,我先写(a=1),再写(a=233),233 就是新版本了。假如 a 老版本已经到 Ck 层了,这时候 C0 层来了个新版本,这个时候不会去管底下的文件有没有老版本,老版本的清理是在合并的时候做的。」

「写入过程基本只用到了内存结构,Compaction 可以后台异步完成,不阻塞写入。」

Get

最新的数据在 C0 层,最老的数据在 Ck 层,所以查询也是先查 C0 层,如果没有要查的 k,再查 C1,逐层查。

LevelDb模型

LSM是一种思想模型,而LevelDB是现实实现,基于LSM,但又不同于LSM,更具实操意义。
LSM-tree 被分成三种文件,第一种是内存中的两个 memtable,一个是正常的接收写入请求的 memtable,一个是不可修改的immutable memtable。另外一部分是磁盘上的 SStable (Sorted String Table),有序字符串表,这个有序的字符串就是数据的 key。SStable 一共有七层(L0 到 L6)。下一层的总大小限制是上一层的 10 倍。
在这里插入图片描述

Put

首先将写入操作加到写前日志中,接下来把数据写到 memtable中,当 memtable 满了,就将这个 memtable 切换为不可更改的 immutable memtable,并新开一个 memtable 接收新的写入请求。而这个 immutable memtable 就可以刷磁盘了。这里刷磁盘是直接刷成 L0 层的 SSTable 文件,并不直接跟 L0 层的文件合并。
每一层的所有文件总大小是有限制的,每下一层大十倍。一旦某一层的总大小超过阈值了,就选择一个文件和下一层的文件合并。

「数据重复场景:L0 层的多个文件在同一层,有先后关系的,后面的同个 key 的数据也会覆盖前面的。会为每个key-value加个版本号。在 Compaction 时候应该只会留下最新的版本」

Get

先查memtable,再查 immutable memtable,然后查 L0 层的所有文件,最后一层一层往下查。

索引

prometheus的存储层使用了全文检索中的倒排索引概念,将每个时间序列视为一个小文档。而metric和label对应的是文档中的单词。

例如,requests_total{path="/status", method=“GET”, instance=“10.0.0.1:80”}是包含以下单词的文档:

* name="requests_total"
* path="/status"
* method="GET"
* instance="10.0.0.1:80"

倒排索引

https://nlp.stanford.edu/IR-book/html/htmledition/a-first-take-at-building-an-inverted-index-1.html
https://blog.csdn.net/zhaoliwen/article/details/106736495

读写放大问题

读写放大(read and write amplification)是 LSM-tree 的主要问题,这么定义的:读写放大 = 磁盘上实际读写的数据量 / 用户需要的数据量。注意是和磁盘交互的数据量才算,这份数据在内存里计算了多少次是不关心的。比如用户本来要写 1KB 数据,结果你在内存里计算了1个小时,最后往磁盘写了 10KB 的数据,写放大就是 10,读也类似。

写放大:我们以 RocksDB 的 Level Style Compaction 机制为例,这种合并机制每次拿上一层的所有文件和下一层合并,下一层大小是上一层的 r 倍。这样单次合并的写放大就是 r 倍,这里是 r 倍还是 r+1 倍跟具体实现有关,我们举个例子。

假如现在有三层,文件大小分别是:9,90,900,r=10。又写了个 1,这时候就会不断合并,1+9=10,10+90=100,100+900=1000。总共写了 10+100+1000。按理来说写放大应该为 1110/1,但是各种论文里不是这么说的,论文里说的是等号右边的比上加号左边的和,也就是10/1 + 100/10 + 1000/100 = 30 = r * level。个人感觉写放大是一个过程,用一个数字衡量不太准确,而且这也只是最坏情况。

读放大:为了查询一个 1KB 的数据。最坏需要读 L0 层的 8 个文件,再读 L1 到 L6 的每一个文件,一共 14 个文件。而每一个文件内部需要读 16KB 的索引,4KB的布隆过滤器,4KB的数据块(看不懂不重要,只要知道从一个SSTable里查一个key,需要读这么多东西就可以了)。一共 24*14/1=336倍。key-value 越小读放大越大。

「而写放大、读放大、空间放大,三者就像 CAP 定理一样,需要做好权衡和取舍。」

附录

当一个人找不到出路的时候,最好的办法就是将当前能做好的事情做到极致,做到无人能及。

以上是关于监控平台设计之Graphite&Prometheus存储的主要内容,如果未能解决你的问题,请参考以下文章

监控平台设计 之 GraphitePrometheus 竞对

Grafana & Graphite & Collectd监控系统

广告业务系统 之 数据中转站 —— “日志中心-实时服务监控”

终于有人把Prometheus入门讲明白了 | 留言送书

#yyds干货盘点#Prometheus 之 Exporter 详解

监控平台设计 之 Prometheus 存储 LevelDB