前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Prometheus TSDB

Prometheus TSDB

原创
作者头像
王磊-字节跳动
修改2020-01-30 11:46:27
3.4K0
修改2020-01-30 11:46:27
举报
文章被收录于专栏:01ZOO

Gorilla TSDB

架构

类似方案对比

OpenTSDB:

  • 基于 Hbase
  • 不做  time roll up aggregation for older data(对比较老对数据进行精度压缩,比如按小时为粒度存储)
  • a richer data model for identifying time series

Whisper (Graphite):

  • stores time series data on local disk in the Whisper format, a Round Robin Database (RRD) style database
  • 需要时序数据固定 interval

InfluxDB:

  • A even richer data model than OpenTSDB. 由于其灵活性,存储磁盘消耗更大。

作者认为以上方案的通用问题:从磁盘查不够快

数据压缩

时间戳压缩:

  1. 计算  delta of delta: D = (t3 - t2) - (t2 - t1)
  2. 根据 D 的分布范围进行类似霍夫曼编码,比如 D 的访问可以分成5类 0,-63~64, -255~256, -2047~2048, 其他
  3. 5个类别的出现概览由高到底,这样就可以进行编码压缩,压缩实验显示 96% 的时间戳都可以压缩到 1 bit 即 0

数值(double)压缩:

  1. 存第一个 value,后面对value 对前面对value 进行 xor 操作
  2. 如果 xor 为0 则存 0 (1bit)(这种情况占了 59% )
  3. 如果 xor 不为0,则看 xor 的结果里面前面的 0 的数量 和末尾的 0 的数量哪个多,根据 两种 condition 由两种存储方式。【 这种情况的压缩效率并不高,有优化空间。但是考虑到整体的 point 的存储空间已经从 16 bytes (int64 + double) 压缩到了 1.37 byte,这部分优化就不是那么重要了 】。

存储和查询

内存中的数据结构

  • 里面比较重要的是 data block 的概念,closed 的datablock 不可变,而 open 的 datablock 只会 append 压缩后的数据。
  • 由于特殊的压缩方式,查询时需要把整个 block 拷贝出来,返回给 client,解压缩步骤可以在 client 端完成。
  • 由于特殊的压缩方式,查询数据需要查整个 block,那么 block 越小越好,而同时 block 越大,压缩效率则越高,最终选定 2 小时的为 block 的存储时常,这个压缩效率为 1.37 byte / 8 byte

磁盘上的数据结构

为了保证Gorilla 能从 crash 中恢复

  • 有四种数据:Key lists(索引), append-only logs(日志), complete block files, and checkpoint files (保证写入的 block file 的可靠性)
  • 要把 closed block 数据存储到 disk
  • 同时保存一个 log,这个 log 是当前写入的数据,从这个 log 里面可以恢复 open data block 
  • 当保存了closed block 之后,closed block 相关的 log 就可以删除,节约磁盘

高可用

  • 磁盘数据结构保证 crash 能够恢复,基本不丢失数据,磁盘数据存在 GlusterFS,存 3 副本
  • 所有 Gorilla  都有两个副本,当一个 down 了之后可以从另一个读取数据
  • Gorilla node 前面有一个 ShardManager 进行数据分片管理 (每个 node 只存一个或者多个 shard,数据用 unique string keys 进行分片,所以每个时序数据都可以对应一个 Gorilla host)
  • Unhealthy node 能自动转发请求到 healthy node
  • 长期存储用 Hbase 存储

Prometheus TSDB

问题

时序数据的格式

{__name__="requests_total", path="/status", method="GET", instance=”10.0.0.1:80”}{__name__="requests_total", path="/status", method="POST", instance=”10.0.0.3:80”}{__name__="requests_total", path="/", method="GET", instance=”10.0.0.2:80”}

读写的问题

写一般是对比如1000个series 同时进行采集,所以他是纵向的。磁盘随机写效率低、SSD 写放大问题  →  顺序写 和 批量写 是必然选择 (sequential and batched writes are the ideal write pattern for spinning disks and SSDs alike. A simple rule to stick)

读的问题更加复杂:可能是读一段时间一个 series 的数据,也可能是 读一个时间点 10000 个 series 的数据,所以读 是在 two-dimensional plane, 读既不是完全横向也不是纵向的,而是一个两者组合的矩形.

Series Churn 

为什么一个 series 一个文件不行:在微服务环境中 series 会非常多,和存在多时间可能很短 (作者命名为 series churn )

  • 同时读写几万个文件效率低,频繁打开关闭几万个文件效率低
  • 文件系统 inodes 不够用
  • 清理过期数据成本高

筛选查询问题

series churn 带来另一个问题:由于 series 数量总是在不停膨胀的,所以从千万个 series 查询出满足条件的 series 的查询效率需要优化

资源消耗问题

series churn increases the usage of memory, CPU, and disk IO every time we scale an application or do a rolling update. 如果 单个文件存储一个 series 这个消耗非常高。

总结

总结一下,主要要解决的几个问题:

  • 读写效率问题
  • series churn 如何存储的问题
  • 快速筛选查询 series 的问题
  • 资源消耗问题

如何解决

文件结构设计

./data ├── 01DYXN0KX8B50SX1CAPCXXWJFN │ ├── chunks │ │ ├── 000001 │ │ ├── 000002 │ │ └── 000003 │ ├── index │ ├── tombstones │ └── meta.json ├── 01DYXVWB57MYNTD4XHCSC0A2JX │ ├── chunks │ │ ├── 000001 │ │ └── 000002 │ ├── index │ ├── tombstones │ └── meta.json └── wal ├── 000001 ├── 001227 └── 001228

  1. 把文件存储按照时间划分成多个 “little database” (block,每个 block 2小时),这样的好处是:
    1. 查询时序数据一般为一个时间范围,这样就可以忽略时间访问不在查询时间内的 block,同时也解决了 series churn 查询 的问题,因为需要检索的数据从一开始减少了
    2. 写完一个 block 之后,从内存持久化到文件中,这样就是批量顺序写,提高了写效率
    3. 最近的 chunks 是被查询最多的,始终在内存中,提高热数据查询效率
    4. 删除旧数据变得非常简单 → 只要删除一个文件夹就可以了
    5. 同时每个 block 中的同一个 series 的数据是 连续一段存储空间,提高查询效率
  2. 使用 mmap 提高读写效率 (缓存的工作交给操作系统),这意味着我可以把整个 database 里面的文件都当成在内存里面进行操作,并且在适当的时候缓存和清理缓存。
  3. Compaction: 由于单个 block 比较小(2小时),所以需要定期将小 block 定期合并成大 block 以方便后续的合并或者其他操作。 同时可以修改数据:dropping deleted data, or restructuring our sample chunks for improved query performance.

# Compaction t0             t1            t2             t3             t4             now ┌────────────┐  ┌──────────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐ │ 1          │  │ 2        │  │ 3         │  │ 4         │  │ 5 mutable │    before └────────────┘  └──────────┘  └───────────┘  └───────────┘  └───────────┘ ┌─────────────────────────────────────────┐  ┌───────────┐  ┌───────────┐ │ 1              compacted                │  │ 4         │  │ 5 mutable │    after (option A) └─────────────────────────────────────────┘  └───────────┘  └───────────┘ ┌──────────────────────────┐  ┌──────────────────────────┐  ┌───────────┐ │ 1       compacted        │  │ 3      compacted         │  │ 5 mutable │    after (option B) └──────────────────────────┘  └──────────────────────────┘  └───────────┘  # Retention ┌────────────┐  ┌────┼─────┐  ┌───────────┐  ┌───────────┐  ┌───────────┐ │ 1          │  │ 2  |     │  │ 3         │  │ 4         │  │ 5         │   . . . └────────────┘  └────┼─────┘  └───────────┘  └───────────┘  └───────────┘                      |                      |             retention boundary

索引

使用倒序索引,series ID 是 forward index. 对每种 label name/value 对都建立倒序索引,比如 app=nginx → ID 1,4,5,ID保持有序,这样多个 label 组合的时候查询效率也是 O(n)

__name__="requests_total"   ->   [ 9999, 1000, 1001, 2000000, 2000001, 2000002, 2000003 ]     app="foo"              ->   [ 1, 3, 10, 11, 12, 100, 311, 320, 1000, 1001, 10002 ]              intersection   =>   [ 1000, 1001 ]

总结

  • 在内存中 batching,使用 wal 跟踪,定期 flush 到磁盘,这是当今数据库类程序常见的操作
  • 使用在横向划分多个 block 定期 Compaction/Retention 的方式提高读写/删除效率
  • 使用倒序索引提高筛选查询效率

文件结构详细

基于 2.10 

meta.json

meta.json 例子

在下面这个例子里面:

  • 存储了 54 小时的数据 maxTime-minTime/ 3600,000; 时间单位一般都是 ms 
  • 27 个 block 合并而成,原始的 block 都是 2小时,level 为 4表示合并了多次,原始的 27 个block id 都在 sources 里面
  • 共计 1050682376 个数据点,351207 个 series,9083900 个 chunk (大概是 series * 54/2 )

Index

【索引这块看上去有不小的优化空间,观察一个 100 M 左右的 block (kubernetes 环境的 prometheus),index 文件24 M左右,占比很高】

【另:按照时间分片的文件结构也带来不少问题,一个问题是构建 cluster 变得很困难(同时因为prometheus 是主动 scrape ),如何在多节点上进行分片是一个问题,参考 Gorilla 的做一个 shard manager (按照 series id/name 进行分片 )是一个办法,前端抓取之后,后端分片存储到多个prometheus instance,查询的时候再做 merge】

label 中用的strings,集中存储能减少 index 大小series 数据,按照 label set 排序,同时 id = offset/16一个 series 数据的索引信息,包括 labels, chunks的开始结束时间,以及真实chuck 索引一个 label对 的所有倒序索引,即这个 label 对匹配的series idsPosting 的索引,对一个 label 对,其对应的posting 位置,这里比较奇怪的是 name 和 value 直接存储并没有索引到 Symbol table整体布局的索引

Chunks

chunks/ 文件夹下面的内容,最大单文件大小512 M

代码语言:javascript
复制
┌──────────────────────────────┐
│  magic(0x85BD40DD) <4 byte>  │
├──────────────────────────────┤
│    version(1) <1 byte>       │
├──────────────────────────────┤
│    padding(0) <3 byte>       │
├──────────────────────────────┤
│ ┌──────────────────────────┐ │
│ │         Chunk 1          │ │
│ ├──────────────────────────┤ │
│ │          ...             │ │
│ ├──────────────────────────┤ │
│ │         Chunk N          │ │
│ └──────────────────────────┘ │
└──────────────────────────────┘

chuck
代码语言:javascript
复制
┌───────────────┬───────────────────┬──────────────┬────────────────┐
│ len <uvarint> │ encoding <1 byte> │ data <bytes> │ CRC32 <4 byte> │
└───────────────┴───────────────────┴──────────────┴────────────────┘

Tombstones

被删除的 数据

代码语言:javascript
复制
┌────────────────────────────┬─────────────────────┐
│ magic(0x0130BA30) <4b>     │ version(1) <1 byte> │
├────────────────────────────┴─────────────────────┤
│ ┌──────────────────────────────────────────────┐ │
│ │                Tombstone 1                   │ │
│ ├──────────────────────────────────────────────┤ │
│ │                      ...                     │ │
│ ├──────────────────────────────────────────────┤ │
│ │                Tombstone N                   │ │
│ ├──────────────────────────────────────────────┤ │
│ │                  CRC<4b>                     │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
代码语言:javascript
复制
chuck
代码语言:javascript
复制
┌────────────────┬─────────────────┬────────────────┐
│ref <uvarint64> │ mint <varint64> │ maxt <varint64>│
└────────────────┴─────────────────┴────────────────┘

Wal

默认单文件大小限制为 128 M

代码语言:javascript
复制
Series records encode the labels that identifies a series and its unique ID.
┌────────────────────────────────────────────┐
│ type = 1 <1b>                              │
├────────────────────────────────────────────┤
│ ┌─────────┬──────────────────────────────┐ │
│ │ id <8b> │ n = len(labels) <uvarint>    │ │
│ ├─────────┴────────────┬─────────────────┤ │
│ │ len(str_1) <uvarint> │ str_1 <bytes>   │ │
│ ├──────────────────────┴─────────────────┤ │
│ │  ...                                   │ │
│ ├───────────────────────┬────────────────┤ │
│ │ len(str_2n) <uvarint> │ str_2n <bytes> │ │
│ └───────────────────────┴────────────────┘ │
│                  . . .                     │
└────────────────────────────────────────────┘
代码语言:javascript
复制
Sample records encode samples as a list of triples (series_id, timestamp, value).
代码语言:javascript
复制
┌──────────────────────────────────────────────────────────────────┐
│ type = 2 <1b>                                                    │
├──────────────────────────────────────────────────────────────────┤
│ ┌────────────────────┬───────────────────────────┐               │
│ │ id <8b>            │ timestamp <8b>            │               │
│ └────────────────────┴───────────────────────────┘               │
│ ┌────────────────────┬───────────────────────────┬─────────────┐ │
│ │ id_delta <uvarint> │ timestamp_delta <uvarint> │ value <8b>  │ │
│ └────────────────────┴───────────────────────────┴─────────────┘ │
│                              . . .                               │
└──────────────────────────────────────────────────────────────────┘

其他 tsdb 相关参考

tsdb 部分源码分析

compact 流程

  • db.go 后台线程 db.run()
    • db.Compact(): chan notify with backoff
      • db.compactor.Write(db.dir, rangeHead:head, ...)
        • LeveledCompactor.write(dest, meta, BlockReader:rangeHead:b)
        • LeveledCompactor.populateBlock(BlockReader:rangeHead:blocks, meta, index.Writer, chucks.Writer)
          • index.Writer.AddSymbol
          • index.Writer.ensureStage(idxStageSymbols): startSymbols
          • chucks.Writer.WriteChunks: level=1, writer 为 instrumentedChunkWriter(只是多记录了一些 metrics,其他一样)
          • index.Writer.AddSeries
          • index.Writer.ensureStage(idxStageSeries): finishSymbols
          • index.Writer.Close()
          • index.Writer.ensureStage(idxStageDone)
          • writePostingsToTmpFiles
          • writeLabelIndices
          • writePostings
          • writeLabelIndexesOffsetTable
          • writePostingsOffsetTable
          • writeTOC
        • writeMetaFile
        • tombstones.WriteFile
      • db.reload(): reload blocks and trigger head truncation if new blocks appeared.Blocks that are obsolete due to replacement or retention will be deleted.
        • openBlocks(db.dir, db.blocks...)
          • readMetaFile
          • getBlock, if not open, OpenBlock
          • readMetaFile
          • return Block with index.Reader, chunks.Reader, tombstones.Reader
        • db.deleteBlocks(deletable)
      • db.compactor.Plan(): Plan returns a set of directories that can be compacted concurrently.
      • db.compactor.Compact(): Compact runs compaction against the provided directories.
        • compactBlockMetas
        • db.compactor(LeveledCompactor).write(dest, meta, blocks...) 和保存head里面的数据到文件的不同之处只是 BlockMeta.Compaction.Level 不同
      • db.reload()
      • runtime.GC(): 多次运行

query 流程

  • DB.Querier(mint, maxt int64): append(rangeHeader, blocks)
  • tsdb.querier.Select(ms ...*labels.Matcher)
    • for b in querier.blocks: b.Select(ms...)
      • blockQuerier.LookupChunkSeries
        • PostingsForMatchers
          • for m in labels.Matchers: postingsForMatcher
          • IndexReader.Postings(labelName, labelValue)
        • return baseChunkSeries{index.Postings, IndexReader, MemTombstones}

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Gorilla TSDB
    • 架构
      • 类似方案对比
        • 数据压缩
          • 存储和查询
            • 高可用
            • Prometheus TSDB
              • 问题
                • 时序数据的格式
                • 读写的问题
                • Series Churn 
                • 筛选查询问题
                • 资源消耗问题
              • 如何解决
                • 文件结构设计
                • 索引
              • 文件结构详细
                • meta.json
                • Index
                • Chunks
                • Tombstones
                • Wal
              • 其他 tsdb 相关参考
                • tsdb 部分源码分析
                相关产品与服务
                前端性能监控
                前端性能监控(Real User Monitoring,RUM)是一站式前端监控解决方案,专注于 Web、小程序等场景监控。前端性能监控聚焦用户页面性能(页面测速,接口测速,CDN 测速等)和质量(JS 错误,Ajax 错误等),并且联动腾讯云应用性能监控实现前后端一体化监控。用户只需要安装 SDK 到自己的项目中,通过简单配置化,即可实现对用户页面质量的全方位守护,真正做到低成本使用和无侵入监控。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档