OpenTSDB 的唯一ID(UniqueId)

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变化了,其余的全部复用。
      在这里插入图片描述
  • 缩短列族: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 的两种方式:

  1. 在 HBase 表中专门维护一个 KeyValue,用于实现自增以生成不重复的 UID;
  2. 使用 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。

分配 UID

getOrCreateId()

  • getOrCreateId() 方法:先查询指定字符串对应的 UID,成功则返回 UID,失败则为字符串分配 UID 并返回。
  • 方法的流程:
    在这里插入图片描述
  • 具体实现代码(getOrCreateId() 方法是同步阻塞的,getOrCreateIdAsync() 方法是异步版本):
public byte[] getOrCreateId(final String name) throws HBaseException {
	try {
		return getIdAsync(name).joinUninterruptibly(); // 调用 getIdAsync() 方法查询 name 对应的 UID
	} catch(NoSuchUniqueName e) { // 查询失败时会抛出 NoSuchUniqueName 异常
		if(tsdb != null && tsdb.getUidFilter() != null && tsdb.getUidFilter().fillterUIDAssignments()) { // 是否配置了 UniqueIdFilterPlugin
			try {
				// 检查 name 是否允许分配 UID
				if(!tsdb.getUidFilter().allowUIDAssignment(type, name, null,null).join()) {
					rejected_assignments++; // 若不允许,则递增 rejected_assignments,并抛出异常
					throw new FailedToAssignUniqueIdException(new String(kind), name, 0, "...");	
				}
			} catch(Exception e1) { // 简化异常处理代码
				throw new RuntimeException("...", e1);
			}
		}
		
		Deferred<byte[]> assignment = null;
		boolean pending = false;
		synchronized (pending_assignments) { // 加锁同步
			assignment = pending_assignments.get(name); // 检查是否有其他线程并发,并为 name 分配 UID
			if(assignment == null) { // 无并发。则向 pending_assignments 添加相应键值对
				assignment = new Deferred<byte[]>();
				pending_assignments.put(name, assignment);
			} else{
				pending = true; // 存在并发
			}
		}

		if(pending) {
			// 等待并发线程完成 UID 的分配,并返回其为 name 分配的 UID,这里省略 try/catch 代码
			return assignment.joinUninterruptibly();
		}

		byte[] uid = null;
		try {
			// 由于当前线程完成 UID 的分配,创建 UniqueIdAllocator 对象,并调用其 tryAllocate() 方法
			uid = new UniqueIdAllocator(name, assignment).tryAllocate().joinUninterruptibly();
		} catch(Exception e1) {
			... // 省略异常处理代码			
		} finally {
			synchronized (pending_assignments) { // 当前线程完成 UID 分配后,清理 pending_assignments
				if(pending_assignments.remove(name) != null) {
					LOG.info("Completed pending assignment for: " + name);
				}
			}
		}
		return uid; // 返回 name 对应的 UID
	} catch(Exception e) {
		throw new RuntimeException("Should never br here", e);
	}
}

查询 UID

UniqueId.getId() 方法、getOrCreateIdAsync() 方法、getOrCreateId() 方法在查询指定字符串对应的 UID 时,都是通过调用 UniqueId.getIdAsync() 方法完成。

  • getId() 方法:
public byte[] getId(final String name) throws NoSuchUniqueName, HBaseException {
	try {
		return getIdAsync(name).joinUninterruptibly(); // 阻塞等待 getIdAsync() 方法执行完成
	} catch(Exception e) { // 简化异常处理代码
		throw new RuntimeException("Should never be here", e);
	}
}
  • 其中 getIdAsync() 方法:首先会查询 name_cache 缓存,如果缓存未命中,才会继续调用 getIdFromHBase() 方法查询 HBase 表
public Deferred<byte[]> getIdAsync(final String name) {
	final byte[] id = getIdFromCache(name); // 从 name_cache 缓存查询 name 对应的 UID
	if(id != null) { // 缓存命中,则将 cache_hits 加1,并返回 UID
		incrementCacheHits();
		return Deferred.fromResult(id);
	}
	
	class GetIdCB implements Callback<byte[], byte[]> {
		// 为便于理解,将 GetIdCB 这个 Callback 的具体实现下面分析
	}
	incrementCacheMiss(); // 缓存未命中,则将 cache_misses 加 1
	Deferred<byte[]> d = getIdFromHBase(name).addCallback(new GetIdCB());
	return d;
}
  • GetIdCB(主要做校验操作,并将查询结果添加到缓存中) 这个内部类实现了 Callback 接口。Asynchronous HBase 客户端是异步的,其操作返回的 Deferred 可以添加 Callback 对象执行回调操作。
class GetIdCB implements Callback<byte[]. byte[]> {
	public byte[] call(final byte[] id) {
		if(id == null) { // HBase 查询结果为空,则抛出 NoSuchUniqueName 异常
			throw new NoSuchUniqueName(kind(), name);
		}
		// 检测 UID 长度是否合法,若不合法则抛出异常(略)
		
		// addIdToCache() 方法会将 name 字符串到 UID 的映射关系保存到 name_cache 缓存中
		// addNameToCache() 方法会将 UID 到 name 字符串的映射关系保存到 id_cache 缓存中
		addIdToCache(name, id);
		addNameToCache(id, name);
		return id;
	}
}
  • getIdFromHBase() 方法是通过调用 hbaseGet() 方法完成 HBase 查询的(getNameFromHBase() 方法同理),hbaseGet() 方法如下:
private Deferred<byte[]> hbaseGet(final byte[] key, final byte[] family) {
	final GetRequest get = new GetRequest(table, key); // 创建 GetRequest
	get.family(family).qualifier(kind); // 指定查询的 Family 和 qualifier
	class GetCB implements Callback<byte[], ArrayList<KeyValue>> { // 定义 Callback 回调
		public byte[] call(final ArrayList<KeyValue> row){
			// 如果 HBase 表查询结果为空,则返回 null(略)
			return row.get(0).value(); // 获取查询结果的第一个KeyValue 的 value 值	
		}
	}
	
	// 异步执行 GetRequest 查询 HBase 表,并将查询结果传递到 GetCB 回调中进行处理
	return client.get(get).addCallback(new GetCB());
}

UniqueIdAllocator

在 UniqueId.getOrCreateId() 方法中,最终是通过创建 UniqueIdAllocator 对象并调用其 tryAllocate() 方法完成 UID 分配;分析下 UniqueIdAllocator 的核心字段:

  • name(String 类型):记录了当前 UniqueIdAllocator 对象负责分配 UID 的字符串;
  • id(long 类型):记录了 name 分配得到的 UID;
  • row(byte[] 类型):id 字段的 byte[] 数组版本;
  • state(byte[] 类型):当前 UniqueIdAllocator 对象的状态;
    • ALLOCATE_UID 阶段:通过递增或是随机方式获取 UID;
    • CREATE_REVERSE_MAPPING 阶段:创建 UID 到 name 的映射,并保存到 tsdb-uid 表中。
    • CREATE_FORWARD_MAPPING 阶段:创建 name 到 UID 的映射,并保存到 tsdb-uid 表中。
    • DONE 阶段:返回新分配的 UID。
  • attempt(short 类型):当分配 UID 出现异常时会进行重试,该字段会记录此次分配剩余的重试次数。若是随机生成方式,默认重试次数为 10,否则其默认值为 3;
  • assignment(Deferred<byte[]> 类型):当前 UniqueIdAllocator 对象关联的 Deferred 对象。

在 UniqueIdAllocator 的构造方法中会初始化 name字段、assignment字段。调用 tryAllocate() 方法时,会初始化 state 字段并调用其 call() 方法开始 UID 分配。UniqueIdAllocator.tryAllocate() 方法实现如下:

Deferred<byte[]> tryAllocate() {
	attempt--;
	state = ALLOCATE_UID; // 初始化 state 字段
	call(null);
	return assignment;
}

UniqueIdAllocator.call() 方法实现如下:

public Object call(final Object arg) {
	if(attempt == 0) { // 检测 attempt 决定是否能继续重试,若不能重试,则抛出异常(略)
		if(hbe == null && !randomize_id) {
			throw new IllegalStateException("Should never happen!");
		}
		if(hbw == null) {
			throw new FailedToAssignUniqueIdException(...);
		}
		throw hbe;
	}
	
	if(arg instanceof Exception) {
		if(arg instanceof HBaseException) { // 出现异常
			LOG.error(msg, (Exception) arg);
			hbe = (HBaseException) arg;
			attempt--; // 递减 attempt 并重试 state 字段,开始新一轮重试操作
			state = ALLOCATE_UID;
		} else {
			return arg; // 非 HBASEException 异常,则不仅重试,直接抛给上层
		}
	}
	
	class ErrBack implements Callback<Object, Exception> {
		// 定义 ErrBack 回调,其具体试下最后介绍
	}
	
	final Deferred d;
	switch(state) { // 根据 state 状态决定执行的具体操作
		case ALLOCATE_UID:
			d = allocateUid(); // ALLOCATE_UID 阶段,生成 UID
			break;
		case CREATE_REVERSE_MAPPING:
			d = createReverseMapping(arg); // CREATE_REVERSE_MAPPING 阶段,保存 UID 到 name 的映射关系
			break;
		case CREATE_FORWARD_MAPPING:
			d = createForwardMapping(arg); // CREATE_FORWARD_MAPPING 阶段,保存 name 到 UID 的映射关系
			break;
		case DONE:
			return done(arg); // DONE 阶段,将分配完成的 UID 返回
		default:
			throw new AssertionError("Should never be here!");
	}
	// 这里的 addBoth() 方法添加的 Callback 是当前的 UniqueIdAllocator 对象
	return d.addBoth(this).addErrback(new ErrBack());
}

上面实现中存在4中阶段,现在对着4中阶段进行一一分析:

  • ALLOCATE_UID 阶段:使用 UniqueIdAllocator.allocateUid() 方法 生成UID
private Deferred<Long> allocateUid() {
	state = CREATE_REVERSE_MAPPING; // 推进 state 状态
	if(randomize_id) { // 随机生成 UID 的方式
		return Deferred.fromResult(RandomUniqueId.getRandomUID());
	} else {
		// 增加方式生成 UID,在 tsdb-uid 表中维护了一个特殊行,该行中的 KV 是用来生成递增 UID 的
		// 这里的 AtomicIncrementRequest 请求就是原子加一操作,返回值即为新生成的 UID
		return client.atomicIncrement(new AtomicIncrementRequest(table, MAXID_ROW, ID_FAMILY, kind));
	}
}

RandomUniqueId 中维护了 java.security.SecureRandom 对象用于生成随机数(通过系统收集了一些随机事件,例如鼠标、键盘单击等,使用这些随机事件作为种子,从而确保产生非确定的输出)。

private static SecureRandom random_generator = new SecureRandom(Bytes.fromLong(System.currentTimeMillis()));
public static long getRandomUID(final int width) {
	if(width > MAX_WIDTH) { // 检测需要生成的 UID 的字节数
		throw new IllegalArgumentException("...");
	}
	
	final byte[] bytes = new byte[width];
	random_generator.nextBytes(bytes);
	long value = 0;
	for(int i=0; i<bytes.length; i++) {
		value <<= 8;
		value |= bytes[i] & 0xFF;
	}
	return value !=0 ? value : value + 1; // 保证生成的 UID 不为0
}
  • CREATE_REVERSE_MAPPING 阶段:使用 UniqueIdAllocator.createReverseMapping() 方法将 UID 保存到 name 的映射关系
// 如果 ALLOCATE_UID 阶段正常,则该方法的参数 arg 应该是生成的 UID
private Deferred<Boolean> createReverseMapping(final Object arg) {
	/**
	 * 检测 UID 是否为 long 类型、UID 的值是否合法(大于0)及 UID 所占字节数
	 * 是否合法(大于等于 id_width),若检测失败则表示 ALLOCATE_UID 阶段异常,
	 * 这里会报出异常(略)
	 */
	id = (Long) arg; // 生成的 UID
	row = Bytes.fromLong(id); // 将 UID 转换成对应的 byte[] 数组
	// 在 ALLOCATE_UID 阶段生成的 UID 为8个字节,这里会检查超过 id_width 字节是都为 0
	for(int i=0; i< row.length - id_width; i++) {
		if(row[i] != 0) {
			throw new IllegalStateException(...);
		}
	}
	// 将8字节的 row 整理为 id_width 个字节
	row = Arrays.copyOfRange(row, row.length - id_width, row.length);
	state = CREATE_FORWARD_MAPPING; // 推进 state 状态

	/**
	 * 通过 CAS 操作将 UID 到 name 字符串的映射关系保存到 tsdb-uid 表中。这里的
	 * compareAndSet() 操作也是个原子操作,tsdb-uid 表中对应 value 为空时,才能
	 * 写入成功。
	 * 两次为不同字符串随机生成 UID 产生了相同的 UID,则会写入失败,触发重试。
	 */ 
	 return client.compareAndSet(reverseMapping(), HBaseClient.EMPTY_ARRAY);
}
private PutRequest reverseMapping() {
	// 该 PutRequest 操作的 RowKey 是 UID,Family 是 name,qualifier 是 kind,value 是 name 字符串
	return new PutRequest(table, row, NAME_FAMILY, kind, toBytes(name));
}
  • CREATE_FORWARD_MAPPING 阶段:UniqueIdAllocator.createForwardMapping() 方法将 name 保存到 UID 的映射关系
// 如果 CREATE_REVERSE_MAPPING 阶段正常,则该方法的参数 arg 应该为 true
private Deferred<?> createForwardMapping(final Object arg) {
	// 检测 arg 是否为 Boolean 类型,若不是 Boolean 类型,则表示 CREATE_REVERSE_MAPPING 阶段异常,这里会继续抛出异常(略)
	if(!((Boolean) arg)) { // 检测CREATE_REVERSE_MAPPING 阶段的 CAS 操作是否执行成功
		if(randomize_id) {
			random_id_collisions++; // 随机生成的 UID 发生冲突,递增 random_id_collisions
		} else {
			// 日志输出(略)
		}
		attempt--; // 可重试次数减少
		state = ALLOCATE_UID; // 重置 state 字段,开始一次尝试
		return Deferred.fromResult(false); 
	}
	state = DONE; // 推进 state 状态
	// 同样是 CAS 操作,将 name 字符串到 UID 的映射关系保存到 tsdb-uid 表中
	return client.compareAndSet(forwardMapping(), HBaseClient.EMPTY_ARRAY);
}
private PutRequest forwardMapping() {
	// 该 PutRequest 操作的 RowKey 是 name 字符串,Family 是 id,qualifier 是 kind,value 是 UID
	return new PutRequest(table, toBytes(name), ID_FAMILY, kind, row);
}
  • DONE 阶段:UniqueIdAllocator.done() 方法将保存相关 UIDMeta 信息,将 name 和 UID 之间的映射关系添加到 name_cache 和 id_cache 中,并最终返回刚刚为 name 字符串分配的 UID
// 如果 CREATE_FORWARD_MAPPING 阶段正常,则该方法的参数 arg 应该为 true
private Deferred<byte[]> done(final Object arg) {
	// 检测 arg 是否为 Boolean 类型,若不是 Boolean 类型,则表示 CREATE_REVERSE_MAPPING 阶段异常,这里会继续抛出异常(略)
	if(!((Boolean) arg)) { // 检测 CREATE_FORWARD_MAPPING 阶段的 CAS 操作是否执行成功
		if(randomize_id) {
			random_id_collisions++; // 随机生成的 UID 发生冲突,递增 random_id_collisions
		}
		class GetIdCB implements Callback<Object, byte[]> {
			public Object call(final byte[] row) throws Exception {
				assignment.callback(row);
				return null;
			}
		}
		getIdAsync(name).addCallback(new GetIdCB()); // 查询 name 字符串对应 UID,并返回
		return assignment;
	}
	/** 
	 * cacheMapping() 方法调用了 addIdToCache() 方法和 addNameToCache() 方法
	 * 将 UID 和 name 之间的映射关系保存到 name_cache 和 id_cache 中
	 */
	 cacheMapping(name, row);
	 // 根据配置决定是否保存 UIDMeta 信息
	 if(tsdb != null  && tsdb.getConfig().enable_realtime_uid()) {
		final UIDMeta meta = new UIDMeta(type, row, name);
		meta.storeNew(tsdb);
		tsdb.indexUIDMeta(meta);
	}
	// 为 name 分配 UID 的过程结束,清理其在 pending_assignment 中的对应记录
	synchronized(pending_assignments) {
		if(pending_assignments.remove(name) != null) {
			LOG.info("Completed pending assignment for: " + name);
		}
	}
	assignment.callback(row); // 返回生成的 UID
	return assignment;
}

UniqueIdFilterPlugin

在 UniqueId.getOrCreateId() 方法中,在为 name 字符串分配 UID 之前,先要通过 UniqueIdFilterPlugin 的监测。

  1. 首先监测 TSDB 中是否配置了 uid_filter 字段(UniqueIdFilterPlugin 类型);
  2. 之后检测该 UniqueIdFilterPlugin 对象是否会拦截 UID 的分配;
  3. 最后调用 UniqueIdFilterPlugin.allowUIDAssignment() 方法检测该 name 字符串是否可以分配 UID。

UniqueIdFilterPlugin 接口定义如下:

public abstract class UniqueIdFilterPlugin {
	// 当 UniqueIdFilterPlugin 对象初始化时会首先调用该方法,如果不能正确初始化,则该方法会抛出异常
	public abstract void initialize(final TSDB tsdb);

	// 当系统关闭,会调用 shutdown() 方法释放该 UniqueIdFilterPlugin 对象的相关资源
	public abstract Deferred<Object> shutdown();
	
	// 返回版本信息
	public abstract String version();
	
	// 收集监控信息
	public abstract void collectStats(final StatsCollector collector);
	
	// 指定的字符串 value 是否可以分配 UID,其中参数 metric 和 tags 用于辅助判断,可以为 null
	public abstract void Deferred<Boolean> allowUIDAssignment(final UniqueIdType type, final String value, final String metric, final Map<String, String> tags);
	
	// 判断当前 UniqueIdFilterPlugin 对象是否拦截 UID 的分配
	public abstract boolean fillterUIDAssignments(); 
}

UniqueIdWhitelistPlugin 是 OpenTSDB 提供的 UniqueIdFilterPlugin 接口的唯一实现
在这里插入图片描述
UniqueIdWhitelistFilter 实现的是白名单的功能,其核心字段含义如下:

  • metric_patterns(List< Pattern > 类型):当前 UniqueIdWhitelistFilter 对象的 metric 白名单,只有 metric 符合该 List 中的所有正则表达式之后,才能进行 UID 的分配。UniqueIdWhitelistFilter.tagk_patterns 和 tagv_pattrens 字段的功能与 metric_patterns 类似。
  • metrics_rejected(AtomicLong 类型):记录被过滤掉的 metric 的个数,tagks_rejected 和 tagvs_rejected 字段含义类似。
  • metrics_allowed(AtomicLong 类型):记录通过过滤、能够进行 UID 分配的 metric 的个数,tagks_allowed 和 tagvs_allowed 字段含义类似。

filterUIDAssignments() 方法始终会返回 true。
allowUIDAssignment() 方法中会根据字符串所属的不同类型,应用不同的正则表达式进行检查,具体如下:

public Deferred<Boolean> allowUIDAssignment(final UniqueIdType type, final String value, final String metric, final Map<String, String> tags) {
	switch(type) { // 根据 type 类型,使用不同的正则表达式进行过滤
		case METRIC:
			if(metric_patterns != null) {
				for(final Pattern pattern : metric_patterns) {
					if(!pattern.matcher(value).find()) { // value 必须匹配全部的正则表达式
						metrics_rejected.incrementAndGet();
						return Deferred.fromResult(false);
					}
				}
			}
			metrics_allowed.incrementAndGet(); // value 通过检查
			break;
		case TAGK:
			// 与处理 metric 类型字符串逻辑类似
			break;
		case TAGV:
			// 与处理 metric 类型字符串逻辑类似
			break;
	}
	return Deferred.fromResult(true);
}

异步分配 UID — getOrCreateIdAsync() 方法

由于 getOrCreateIdAsync() 方法是 getOrCreateId() 方法的异步版本,所以两者功能及处理非常相似,但是还是存在差异的,下面来分析下具体方法:

public Deferred<byte[]> getOrCreateIdAsync(final String name, final String metric, final Map<String, String> tags) {
	final byte[] id = getIdFromCache(name); // 首先查询 name_cache 缓存
	if(id != null) {
		incrementCacheHits(); // 缓存命中,递增 cache_hits
		return Deferred.fromResult(id);
	}
	
	class AssignmentAllowedCB implements Callback(Deferred<byte[]>, Boolean) {
		// AssignmentAllowedCB 这个 Callback 实现是创建 UniqueIdAllocator 对象并调用
		// 其 tryAllocate() 方法完成 UID 分配的地方,后面具体分析
	}
	
	class HandleNoSuchUniqueNameCB implements Callback<Object, Exception> {
		// 下面 getIdAsync() 方法查询 HBase 表的结果将会在该 Callback 对象中处理,后面具体分析
	}
	
	// 调用 getIdAsync() 方法查询 HBase 表,这里添加 Callback 是上面定义的 HandleNoSuchUniqueNameCB 对象
	return getIdAsync(name).addErrback(new HandleNoSuchUniquenNameCB);
}

getIdAsync() 方法已经分析过了,这里分析下 HandleNoSuchUniqueNameCB 的实现,其中会处理 HBase 表的查询结果:

class HandleNoSuchUniqueNameCB implements Callback<Object, Exception> {
	public Object call(final Exception e) {
		// 在 HBase 的 tsdb-uid 表中查询不到指定的字符串时,会抛出 NoSuchUniqueName 异常
		if(e instanceof NoSuchUniqueName) {
			if(tsdb != null && tsdb.getUidFilter() != null && tsdb.getUidFilter().fillterUIDAssignments()) { // UniqueIdFilterPlugin 是否拦截 UID 的分配调用 UniqueIdFilter Plugin.allowUIDAssignment() 方法判断是否为该字符串分配 UID,这里添加的回调为 AssignmentAllowedCB 对象
				return tsdb.getUidFilter().allowUIDAssignment(type, name, metric, tags).addCallbackDeferring(new AssignmentAllowedCB());
			} else { // 直接回调 AssignmentAllowedCB 分配 UID
				return Deferred.fromResult(true).addCallbackDeferring(new AssignmentAllowedCB());
			}
		}
		return e; // 若没有异常或不是 NoSuchUniqueName 类型的异常,则不会触发 UID 分配的逻辑
	}
}

HandleNaSuchUniqueNameCB 执行完成后,会回调前面定义的 AssignmentAllowedCB,其中会根据 UniqueIdFilterPlugin 的拦截结果决定是否为 name 字符串分配 UID,具体如下:

class AssignmentAllowedCB implements Callback<Deferred<byte[]>, Boolean> {
	@Override
	public Deferred<byte[]> call(final Boolean allowed) throws Exception {
		if(!allowed) { // name 字符串被 UniqueIdFilterPlugin 拦截,无法分配 UID
			rejected_assignments++; // 增加 rejected_assignments
			return Deferred.fromError(new FailedToAssignUniqueIdException(new String(kind), name, 0, "Blocked by UID filter.")); // 返回异常
		}
		Deferred<byte[]> assignment = null;
		synchronized(pending_assignments) { // 加锁检测是否存在其他线程并发为该 name 字符串分配 UID
			assignment = pending_assignments.get(name);
			if(assignment == null) { // 不存在其他线程并发为该 name 字符串分配 UID
				assignment = new Deferred<byte[]>();
				pending_assignments.put(name, assignment);		
			}else { // 存在其他线程并发为该 name 字符串分配 UID
				LOG.info("Already waiting for UID assignment: " + name);
				return assignment;
			}
		}
		// 创建 UniqueIdAlloctor 对象,并调用其 tryAllocate() 方法完成 UID 分配,前面已经分析过了
		return new UniqueIdAllocator(name, assignment).tryAllocate();;
	}
}

getOrCreateIdAsync() 的所有过程都是异步非阻塞的,其中涉及到 HBase 表查询,UniqueIdFilterPlugin 拦截,UniqueIdAllocator 分配 UID 等。

getOrCreateId() 的上述步骤中,都加了 join() 或 joinUninterruptibly() 方法等阻塞调用。

查询字符串

通过指定的 UID 查询相应的字符串,该功能是在 UniqueId.getNameAsync() 方法中完成的;是 getIdAsync() 方法的逆过程。
UniqueId 实现的 getName() 方法(该方法定义在 UniqueIdInterface 接口)的底层也是通过调用 getNameAsync() 方法实现的。具体如下:

public Deferred<String> getNameAsync(final byte[] id) {
	// 检测参数 id 长度是否合法(略)
	// 首先查询 id_cache 缓存中是否存在指定 UID 到字符串的映射关系
	final String name = getNameFromCache(id);
	if(name != null) {
		incrementCacheHits(); // 缓存命中,递增 cache_hits
		return Deferred.fromResult(name); // 返回相应字符串
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值