腾讯目前有几十万台量级的服务器,TMP 系统按 1 分钟粒度采集监控数据,平均每天采集 1200 多亿的数据点。
本文将从当前存储架构存在的问题出发,介绍从尝试使用 Opentsdb 到自行设计 Hbase 存储方案来存储 TMP 服务器海量监控数据的实践历程。
TMP 当前存储架构分析
我们首先看下当前的 TMP 的 1 分钟粒度数据存储架构。Agent 上报的数据,通过 Collector 从 MySQL 数据表中查询索引和路由规则 a,分发到不同的数据存储结点上。
数据节点 Datacache 收到数据后先缓冲到内存中,内存的数据定期 DUMP 到文件系统中。
这套架构优点很明显,设计简洁、有最新数据缓存、数据分布式存储、可横向扩展,同时完全自研,各自实现细节可控。
但同样存在一些问题:
数据节点 Cache 程序异常,会导致内存缓存数据丢失,进而丢失监控数据,需要从 Agent 端或者对等集群恢复。
数据节点磁盘故障或机器故障,持久化的 FILE 也会丢失,数据同样需要从对等集群中恢复,数据访问入口需要人工介入切换集群。
数据格式和占用空间固定,不具备监控粒度扩展性,空的数据点也要占据存储空间,数据不支持压缩。
索引和路由规则这类依赖外部 DB 系统,这些 metadata 的可用性影响整个系统。
Hbase 存储引擎优势
Hbase 是 Hadoop 生态栈中的分布式列存储数据库,基于 Bigtable 模型和 LSM-Tree 存储引擎,用于海量数据的随机读写,在业界的大规模监控系统的时序数据存储中已有成熟应用案例,如某度和某宝 。
图1:Hbase 的存储原理
我们看下使用 Hbase 存储有何优势:
数据高可靠,高可用。数据在写内存的同时,会写一份 HLog 到 HDFS 中,如果某台 RegionServer 异常,内存中的数据会从 Hlog 中自动恢复。持久化数据保存在 HDFS 中,默认持有 3 个副本,无单点丢失数据风险。
高性能。LSM-Tree 存储引擎保证了 Hbase 超高的写性能,实际上前面介绍的 TMP 自研存储系统也是一种简化版的 LSM-Tree 存储引擎,因而同样有如此高的性能。
天然的水平伸缩,高可扩展性。存储层 DataNode,数据服务层 RegionServer 均支持自由伸缩扩容。
数据表支持压缩,空列不占存储空间。
Opentsdb 尝试及瓶颈分析
在准备使用 Hbase 存储 TMP 监控数据之初,我们曾尝试使用基于 Hbase 的开源时序数据库 Opentsdb 来直接存储服务器监控数据。但 Opentsdb 到 70w/s 的时候整个 Hbase 集群就已超负荷运转、响应缓慢了,完全无法满足如此大规模的存储需求。
我们仔细分析了 Opentsdb 在超大规模时序数据存储上存在的主要瓶颈:
所有 metric 跟 tag 都要经过 tsdb-uid 表的转换,此设计本意是为了压缩 rowkey 大小,但引入较大的计算资源开销;
数据写入的 Append 机制及原始 compaction 设计存在较大的性能问题,这在后面部分会详细分析;
所有的数据都放在同一张表里,不利于基于时间对数据进行维护操作,比如对一个月前非热点数据进行抽样存储,且无法控制 Region 数,也就无法控制 split,对性能影响较大;
基于这些原因,我们最终决定直接使用 Hbase 进行 TMP 服务器监控数据存储。
TMP 监控存储设计实践
Hbase 的使用在整个 hadoop 生态栈中属于较为复杂的一个类别。TMP 监控存储设计结合了业界使用 Hbase 的一些成熟的实践经验,同时参考和改进了 OpenTSDB 在使用 HBase 时的比较好的设计思想,以支撑 TMP 监控数据的大规模读写。
Region 预切分
Hbase 中的数据会按 rowkey 所处范围分布在各个 Region 中,新建表的时候只有一个 Region,并随着 Region 变大开始分裂。这在过程中会消耗大量在网络、磁盘 IO 等资源,对集群会有较大性能影响。
同时由于开始期间只有少量 Region,数据的读写很容易全落在单台 RegionServer 上,造成 HotSpot 现象,严重影响读写性能。因此对存储表进行 Region 预切分处理是 Hbase 使用中十分重要的一步。
这里我们将每天的数据表预切分为 100 个 Region, 以{0x01, 0x01 … 0x63},即二进制的 1~99 为 splitKeys,第一个 Region Rowkey 范围为 [0x00…~0x01],第二个 Region Rowkey 范围为 [0x01…~0x02],以此内推。
结合接下来这节中的 Rowkey salt 设计就可以均匀地将数据分散在各 Region 中。
Rowkey 和列设计
Rowkey 设计由 salt(1 byte) 服务器 ID(4 byte) timestamp(4 byte) 监控特性 ID(4 byte) 的方式组成:
Salt 是使用服务器 id 进行 hash 后对单表初始 Region 数进行求余所得的一位字节,用来将不同服务器的监控数据均匀分布在表的各个 Region 中。
Rowkey 第二部分为服务器 ID,服务器监控数据查询通常是查询指定服务器的某些特征,因而将服务器 ID 放在第二部分可以大幅提高查询效率。
timestamp 实际上是一个 time-base,用于将一段时间内的数据存放在同一行。
attr_id 为特性 id,区分具体监控指标。
这里使用一个字节 t 作为列族,列族名称本身并没什么含义,主要强调只使用一个列族存储数据,尽量小的名称作为名字。使用多列族每个 Region 会对应有多个 Memstore,会加重内存消耗,在此场景下不适用。
列名(在 Hbase 中称 Qualifier)为时间偏移,与 Rowkey 中的 time-base 一起组成 timestamp 标识数据点的精确时间。
基于列的 Compaction
在介绍列 Compaction 之前,我们先看下 Hbase 数据的具体存储结构:
图2:表结构与存储结构
如图 2 所示,为表结构以及对应的存储结构示例,在 Hbase 或表格存储这类底层采用 LSM-tree 的数据库中,表数据会按列存储。
每行中的每一列在存储文件中都会以 Key-value 的形式存在于文件中。其中 Key 的结构为:行主键 列族 列名,Value 为列的值。
该种存储结构的特点是:
可以注意到,在 Hbase 的物理存储中,每一列都会存储该列的 rowkey 和列族信息,在列很多的情况下这些重复的信息将占用大量的存储空间。
因此这里参考 Opentsdb 的做法,将同一 time-base 内的所有列合并压缩为一列(注意这里说的列 Compaction 与 HBase 本身的 Compation 是完全不同的,Hbase 的 Compation 是指将多个小的 HFile 合并为一个大的 HFile)。
Opentsdb 的列 Compaction 由数据量大小和时间间隔共同触发,在并发写操作巨大的时候会对 Hbase 产生很大的读写压力,并且会阻塞写操作,性能表现较差。
2.2 版本加入的 append 机制更是每一次写操作产生一次读操作,对 Hbase 利用很不经济,写入量大时会对整个集群的读压力造成巨大影响。
基于这些原因,TMP 监控数据在每天凌晨对前一天的数据表进行全表扫描,并对每行数据的列名(Qualifier)和 Value 进行合并,压缩为一列。在现网实际环境中可以看到,列压缩后的数据表比压缩前占用存储空间减少接近90%,如下图:
Hbase 性能调优
Hbase 性能调优是个比较复杂的事情,网上可以看到很多专门讲 Hbase 调优的文章。
这里仅挑出几个比较立竿见影的点来分享:
Heap 和 Memstore 大小。尽量调大 RegionServer 的 heap 大小,如写入量远大于查询量,可以增大 Memstore 与 BlockCache 的比例,如 0.5 : 0.3 。原因是 HBase 基于 LSM Tree 的存储引擎,数据会先缓存至 Memstore 再刷入磁盘。
Snappy 压缩。对数据表启用 Snappy 压缩,可以减少磁盘 IO,增大写入性能。
Hbase 自身的 Compation 线程数。Hbase 在 flush 和 compaction 的时候会对 rowkey 进行排序操作,适当增大 compaction 线程,可更加充分利用 CPU,增加性能。具体可将 hbase.regionserver.thread.compaction.small/large 调节为 5。
GC 调优。GC 参数设置不当会触发 Stop the world 这类严重影响性能的问题。
总结
基于上述设计和优化后, TMP 监控数据存储方案比直接使用 Opentsdb 存储性能提高了 3~5 倍,8 台 RegionServer 峰值写入速率可达 400w/s ,Opentsdb 则到 70w/s 的时候整个 Hbase 集群就已经无法正常工作了。
目前此套基于 Hbase 的监控数据存储系统已经上线提供服务,后续计划在入库 Hbase 之前加入缓冲层进行列的预 Compaction,可进一步成倍提升整体性能。
HBase 发展至今已是个比较成熟的开源分布式数据库,其高性能,高可用性及高可扩展的特性,可为海量数据的存取提供强大动力。
CIO之家 www.ciozj.com 公众号:imciow