本文已参与「新人创作礼」活动,一起开启掘金创作之路。
OpenTSDB 的 RowKey设计
OpenTSDB 底层存储使用的是 HBase,HBase 将表中的数据按照 RowKey 切分成 HRegion,然后分散到集群的 HRegion Server 中存储并提供查询支持。
OpenTSDB 中可以通过 metric+tag 组合的方式确定唯一一条时序数据,因此 OpenTSDB 在设计 HBase RowKey 的时候就包含了 metric、tag、base_time(以小时为单位的时间戳);表示该行中存储的是该时序在这一小时内的数据。HBase RowKey 的设计
<metric><base_time><tagk1><tagv1><tagk2><tagv2>...<tagkN><tagvN>
OpenTSDB 对 RowKey 设计的优化点:
- 缩短 RowKey 长度:将 metric、tagk、tagv 三部分字符串都被换成UID(UniqueId,全局唯一的id值),字符串与UID是一一对应的关系。RowKey 的长度大大缩短,节省大量空间。
<metric_uid><base_time><tagk1_uid><tagv1_uid>...<tagkN_uid><tagvN_uid>- RowKey 的各个部分的字符串与UID(以字节数组的形式表示)的映射关系
- UID与字符串的映射是可以复用的,下面示例,除了 web02、jvm01 的修改其对应的UID变化了,其余的全部复用。
- RowKey 的各个部分的字符串与UID(以字节数组的形式表示)的映射关系
- 缩短列族:HBase 底层会按照列族创建对应的 MemStore 和 StoreFile(HFile),列族的增加会增加到 RowKey 重复出现次数,所以 OpenTSDB 存放时序数据的核心表中只有一个列族。
- 缩短列名:HBase 底层的 KV 存储中,列名作为 Key 的构成部分之一,也不能设计得过长。OpenTSDB 中的列名设计为相对于 base_time 的时间偏移量,其对应的 value 为该时间戳的点的值。
细节注意:
- 当一个小时内的数据全部写入完成,OPenTSDB 会进行一次优化(压缩),将3600行压缩为一列;
- OpenTSDB 中不能使用大量 tag;tag 过多,即使使用 UID 映射,RowKey 也会变长;默认的最大 tag 数为 8 对,官方推荐的 tag 数为 4~5 对;
- UID 由 UniqueId 组件分配;每种类型的 UID 映射是相互隔离的。
- 分配的 UID 个数存在上限,可以修改相关配置,提升 UID 上限;默认配置中,UID 的长度为 3个字节(2^24个UID)。
- 提升 UID 的上限会导致 UID 所占的字节数会变大,增加全部 RowKey 的长度。
- metric、tagk、tagv 分配 UID,使得这三种类型的 UID 是不通过用的,即每种类型的 UID 映射是相互隔离的。
tsdb-uid 表设计
OpenTSDB 将 metric、tagk、tagv 与 UID 的映射关系记录在 tsdb-uid 表中(tsdb-uid 是默认表名,可通过 tsd.storage.hbase.uid_table 配置来修改)。
由上面的表格可以看出(可以看出不同类型字符串的 UID 映射是相互隔离的)
- tsdb-uid 表中有 id、name 两个 Column Family;
- id family 存储字符串(即 metric、tagk、tagv 原始字符串)与 UID 的关系;
- name family 存储UID 与 对应的字符串,_meta 存储了一些 UID 相关的元数据(JSON结构)。
生成 UID 的两种方式:
- 在 HBase 表中专门维护一个 KeyValue,用于实现自增以生成不重复的 UID;
- 使用 java.security.SecureRandom 随机生成 UID。
UniqueId
UniqueId 这个核心类是实现了 UniqueIdInterface 接口(定义了查询、生成 UID 的基本方法)。
UniqueIdInterface 接口的具体实现代码如下:
public interface UniqueIdInterface {
String kind(); // 该 UniqueIdInterface 对象所管理的 UID 的类型
short width(); // 该 UniqueIdInterface 对象生成的 UID 的长度,单位是 byte
// 根据指定 UID(转换成了 byte[] 数组)查询对应的字符串
String getName(byte[] id) throws NoSuchUniqueId, HBaseException;
// 根据指定字符串查询对应的 UID(转换成了 byte[] 数组)
byte[] getId(String name) throws NoSuchUniqueName, HBaseException;
// 根据指定的字符串查询对应的 UID,如果查询不到,则为该字符串生成 UID(转换成 了byte[])
byte[] getOrCreateId(String name) throws HBaseException, IllegalStateException;
}
UniqueId 是 OpenTSDB 提供的 UniqueIdInterface 接口的唯一实现类。看下 UniqueId 的构造方法:
public UniqueId(final TSDB tsdb, final byte[] table, final String kind, final int width, final boolean randomize_id){
this.client = client;
this.tsdb = tsdb;
this.table = table;
// 若 kind 为空,则抛异常(省略相关代码)
this.kind = toBytes(kind); // 将 kind 字符串转换成 byte[] 数组,默认使用 ISO-8859-1 编码方式
type = stringToUniqueIdType(type);
// 监测 width 是否合法(其取值范围是 [1, 8])(省略相关代码)
this.id_width = (short) width;
this.randomize_id = randomize_id;
}
- 核心字段如下:
- client:HBase 客户端,主要负责与 HBase 进行交互。
- OpenTSDB 使用的 HBase 客户端是 Asynchronous HBase,是一个非阻塞、线程安全、完全异步的客户端。性能是原生 HBase 客户端(HTable)的两倍以上。
- table(byte[] 类型):用于维护 UID 与字符串对应关系的 HBase 表名,默认是 “tsdb-uid”。
- kind(byte[] 类型):TSDB 中会维护三个 UniqueId 对象,每个对象分别管理不同类型的字符串(metric、tagk、tagv)与其 UID 的映射关系;kind 字段可选的取值有:metric、tagk、tagv。
- type(UniqueIdType 类型):该字段与上面介绍的 kind 字段的取值一致,其可选项有:metric、tagk、tagv。
- id_width(short 类型):用于记录当前 UniqueId 对象生成的 UID 的字节长度。
- 在 TSDB 中使用的三个 UniqueId 对象中,该字段的默认值都是 3;
- 可以通过 “tsd.storage.uid.width.metric”、“tsd.storage.uid.width.tagk” 和 “tsd.storage.uid.width.tagv” 三个配置项进行修改。
- randomize_id(boolean 类型):避免 HBase 的热点问题,UniqueId 支持随机生成 metric 对应的 UID,该字段就表示当前 UniqueId 对象是否使用随机方式生成 UID,可以通过 “tsd.core.uid.random_metrics” 配置项来修改其值。
- random_id_collisions(int 类型):UID 在每种类型中要保证唯一性,在使用随机方式生成 metric UID 时就可能出现冲突,若发生冲突,则需要重新生成。该字段主要负责记录出现冲突的次数。
- name_cache(ConcurrentHashMap<String, byte[]> 类型):UniqueId 为了加速查询,会将字符串到 UID 之间的对应关系缓存在改 Map 中,其中的 key 是字符串,value 则是对应的 UID。
- id_cache(ConcurrentHashMap<String, byte[]> 类型):与 name_cache 相似,该字段中缓存的是 UID 到字符串之间的对应关系,其中的 key 是 UID,value 则是对应字符串。
- cache_hite(long 类型)、cache_misses(long类型):用于记录缓存命中的次数及缓存未命中的次数。
- pending_assignments(HashMap<String, Deferred<byte[]>> 类型):当多个线程并发调用 UniqueId 对象为同一个字符串分配 UID 时;若不限制,则会出现同一字符串分配多个 UID 的情况,即会造成 UID 的浪费,也会造成不必要的逻辑错误。pending_assignments 字段记录了正在分配 UID 的字符串,当前线程若检测到已有其他线程正在为该字符串分配,则不再进行分配,而是等待其他线程分配结束即可。
- rejected_assignments(int 类型):在 OpenTSDB 中,可以自定义 UniqueIdFilterPlugin 插件,UniqueIdFilterPlugin 插件拦截不能分配 UID 的字符串,类似黑名单的功能。rejected_assignments 字段是用来记录 UniqueIdFilterPlugin 拦截捏的次数。
- renaming_id_names(Set 类型):UniqueId 除了实现 UniqueIdInterface 接口、提供创建、查询 UID 的功能,还定义了很多重新分配 UID 与字符串对应关系的功能。该renaming_in_names 字段用于记录正在进行重新分配 UID。
- client:HBase 客户端,主要负责与 HBase 进行交互。