OpenTSDB 的UID的相关方法

141 阅读11分钟

分配 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); // 返回相应字符串
	}
}