PTSDB
PTSDB的核心包括:倒排索引+窗口存储Block。数据的写入按照两个小时为一个时间窗口,将两小时内产生的数据存储在一个Head Block中,每一个块中包含该时间窗口内的所有样本数据(chunks),元数据文件(meta.json)以及索引文件(index)。
最新写入数据保存在内存block中,2小时后写入磁盘。后台线程把2小时的数据最终合并成更大的数据块,一般的数据库在固定一个内存大小后,系统的写入和读取性能会受限于这个配置的内存大小。而PTSDB的内存大小是由最小时间周期,采集周期以及时间线数量来决定的。为防止内存数据丢失,实现wal机制。删除记录在独立的tombstone文件中。
存储引擎
PTSDB的核心数据结构是HeadAppender,Appender commit时wal日志编码落盘,同时写入head block中。
PTSDB本地存储使用自定义的文件结构。主要包含:WAL,元数据文件,索引,chunks,tombstones。
乱序处理
PTSDB对于乱序的处理采用了最小时间窗口的方式,指定合法的最小时间戳,小于这一时间戳的数据会丢弃不再处理。
合法最小时间戳取决于当前head block里面最早的时间戳和可存储的chunk范围。
这种对于数据行为的限定极大的简化了设计的灵活性,对于compaction的高效处理以及数据完整性提供了基础。
内存的管理
使用mmap读取压缩合并后的大文件(不占用太多句柄),
建立进程虚拟地址和文件偏移的映射关系,只有在查询读取对应的位置时才将数据真正读到物理内存。
绕过文件系统page cache,减少了一次数据拷贝。
查询结束后,对应内存由Linux系统根据内存压力情况自动进行回收,在回收之前可用于下一次查询命中。
因此使用mmap自动管理查询所需的的内存缓存,具有管理简单,处理高效的优势。
Compaction
Compaction主要操作包括合并block、删除过期数据、重构chunk数据。
合并多个block成为更大的block,可以有效减少block个,当查询覆盖的时间范围较长时,避免需要合并很多block的查询结果。
为提高删除效率,删除时序数据时,会记录删除的位置,只有block所有数据都需要删除时,才将block整个目录删除。
block合并的大小也需要进行限制,避免保留了过多已删除空间(额外的空间占用)。
比较好的方法是根据数据保留时长,按百分比(如10%)计算block的最大时长,当block的最小和最大时长超过2/3blok范围时,执行compaction
快照
PTSDB提供了快照备份数据的功能,用户通过admin/snapshot协议可以生成快照,快照数据存储于data/snapshots/-目录。
存储格式
Write Ahead Log
WAL有3种编码格式:时间线,数据点,以及删除点。总体策略是基于文件大小滚动,并且根据最小内存时间执行清除。
-
当日志写入时,以segment为单位存储,每个segment默认128M,记录数大小达到32KB页时刷新一次。当剩余空间小于新的记录数大小时,创建新的Segment。
-
当compation时WAL基于时间执行清除策略,小于内存中block的最小时间的wal日志会被删除。
-
重启时,首先打开最新的Segment,从日志中恢复加载数据到内存。
元数据文件
meta.json文件记录了Chunks的具体信息,比如新的compactin chunk来自哪几个小的chunk。这个chunk的统计信息,比如:最小最大时间范围,时间线,数据点个数等等。
compaction线程根据统计信息判断该blocks是否可以做compact:(maxTime-minTime)占整体压缩时间范围的50%,删除的时间线数量占总体数量的5%。
索引
索引一部分先写入Head Block中,随着compaction的触发落盘。
索引采用的是倒排的方式,posting list里面的id是局部自增的,作为reference id表示时间线。索引compact时分为6步完成索引的落盘:Symbols->Series->LabelIndex->Posting->OffsetTable->TOC
- Symbols存储的是tagk, tagv按照字母序递增的字符串表。比如name,go_gc_duration_seconds, instance, localhost:9090等等。字符串按照utf8统一编码。
- Series存储了两部分信息,一部分是标签键值对的符号表引用;另外一部分是时间线到数据文件的索引,按照时间窗口切割存储数据块记录的具体位置信息,因此在查询时可以快速跳过大量非查询窗口的记录数据,
为了节省空间,时间戳范围和数据块的位置信息的存储采用差值编码。
- LabelIndex存储标签键以及每一个标签键对应的所有标签值,当然具体存储的数据也是符号表里面的引用值。
- Posting存储倒排的每个label对所对应的posting refid
- OffsetTable加速查找做的一层映射,将这部分数据加载到内存。OffsetTable主要关联了LabelIndex和Posting数据块。TOC是各个数据块部分的位置偏移量,如果没有数据就可以跳过查找。
Chunks
数据点存放在chunks目录下,每个data默认512M,数据的编码方式支持XOR,chunk按照refid来索引,refid由segmentid和文件内部偏移量两个部分组成。
Tombstones
记录删除通过mark的方式,数据的物理清除发生在compaction和reload的时候。以时间窗口为单位存储被删除记录的信息。
最佳实践
在一般情况下,Prometheus中存储的每一个样本大概占用1-2字节大小。如果需要对Prometheus Server的本地磁盘空间做容量规划时,可以通过以下公式计算:
neededdisk_space = retention_time_seconds * ingestedsamples_per_second * bytes_per_sample
保留时间(retention_time_seconds)和样本大小(bytes_per_sample)不变的情况下,如果想减少本地磁盘的容量需求,
只能通过减少每秒获取样本数(ingested_samples_per_second)的方式。
因此有两种手段,一是减少时间序列的数量,二是增加采集样本的时间间隔。
考虑到Prometheus会对时间序列进行压缩,因此减少时间序列的数量效果更明显。
PTSDB的限制在于集群和复制。因此当一个node宕机时,会导致一定窗口的数据丢失。当然,如果业务要求的数据可靠性不是特别苛刻,本地盘也可以存储几年的持久化数据。当PTSDB Corruption时,可以通过移除磁盘目录或者某个时间窗口的目录恢复。
PTSDB的高可用,集群和历史数据的保存可以借助于外部解决方案,不在本文讨论范围。
历史方案的局限性,PTSDB在早期采用的是单条时间线一个文件的存储方式。这中方案有非常多的弊端,比如:
Snapshot的刷盘压力:定期清理文件的负担;低基数和长周期查询查询,需要打开大量文件;时间线膨胀可能导致inode耗尽。