一、Otel的traceId规范

如图,Otel生成的traceId分为高低位,各占16位,每一位都是十六进制的字符.即traceId大小为16字节,128位.下一章节会详细介绍高低位都是如何生成的.

二、ThreadLocalRandom的生成规则

2.1 mix64混淆算法

public String generateTraceId() {  
    //安卓JVM or 默认JVM
    Random random = (Random)randomSupplier.get();  
    long idHi = random.nextLong();  
  
    long idLo;  
    do {  
        idLo = random.nextLong();  
    } while(idLo == 0L);  
    //1.long -> 十六进制字符
    //2.toString
    return TraceId.fromLongs(idHi, idLo);  
}

生成高位和低位调用的方法是同一个mix64算法,这里采用的randomSupplier是ThreadLocalRandom,我们直接看内部是如何实现的

public long nextLong() {  
    return mix64(nextSeed());  
}

需要拿到一个随机种子再传入mix64方法,我们先看这个混淆算法是怎么实现的

private static long mix64(long z) {  
    z = (z ^ (z >>> 33)) * 0xff51afd7ed558ccdL;  
    z = (z ^ (z >>> 33)) * 0xc4ceb9fe1a85ec53L;  
    return z ^ (z >>> 33);  
}

首先可以看到实现很简单,只有位移,异或和大质数乘法.最终的目的是混淆输入数据,确保输出的均匀性和无偏性(指输出的伪随机数的均匀分布特性不受seed的影响).常用于分布式系统下高性能,高质量的伪随机数生成.

感兴趣的可查看SplitMix64 伪随机数生成算法,此处不做展开.

2.2 种子的生成

混淆算法有了,我们再来看种子是如何生成的.就不再一层一层往下追了,直接把所有和seed相关的操作摘出来放在一起学习:

//根据系统时间 和 JVM时间 初始化seeder
private static final AtomicLong seeder  = 
new AtomicLong(mix64(System.currentTimeMillis()) ^  mix64(System.nanoTime()));

//用于seed递增的大质数,说明了种子是线性递增的
private static final long GAMMA = 0x9e3779b97f4a7c15L;

// at end of <clinit> to survive static initialization circularity  
static {  
    String sec = VM.getSavedProperty("java.util.secureRandomSeed");  
    if (Boolean.parseBoolean(sec)) {  
        byte[] seedBytes = java.security.SecureRandom.getSeed(8);  
        long s = (long)seedBytes[0] & 0xffL;  
        for (int i = 1; i < 8; ++i)  
            s = (s << 8) | ((long)seedBytes[i] & 0xffL);  
        seeder.set(s);  
    }  
}

//使用Unsafe类直接操作内存
final long nextSeed() {  
    Thread t; long r; // read and update per-thread seed  
    U.putLong(t = Thread.currentThread(), SEED,  
              r = U.getLong(t, SEED) + GAMMA);  
    return r;  
}

ThreadLocalRandom中根据系统时间 和 JVM启动后的相对时间初始化一个seeder,以及一个大质数作为seed递增常量,这也说明了seed的生成实际上就是一个简单的线性加法.

这里有一类特殊情况,如果系统属性设置了secureRandomSeed且为true,会使用安全性更高的SecureRandom来生成seeder的初始值

nextSeed()通过Unsafe类直接操作当前线程内存,取出SEED的值加上递增常量再写回内存,并把新生成的seed返回给混淆算法

细心观察这份代码的人应该可以发现,nextSeed()中会取当前线程内存中的SEED,那第一次取会发生什么呢,初始化相关的逻辑在哪?接下来的代码就展示了这一部分:

//ThreadLocalRandom没有同名的构造函数,而是提供了名为current的静态方法来返回实例
public static ThreadLocalRandom current() {  
    if (U.getInt(Thread.currentThread(), PROBE) == 0)  
        localInit();  
    return instance;  
}

static final void localInit() {  
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);  
    int probe = (p == 0) ? 1 : p; // skip 0  
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));  
    Thread t = Thread.currentThread();  
    U.putLong(t, SEED, seed);  
    U.putInt(t, PROBE, probe);  
}    

PROBE的初始值就是0,此时调用localInit()方法,对seeder线性增长再混淆后的结果就是seed的初始值

seeder和seed的区别就是每个线程的seed初始值都来自于seeder,线程内的seed根据这个初始值递增.

三、JDK自带的UUID

我在自己的电脑上简单测试对比了Otel和UUID的性能,各自生成10000次并统计时间,交换测试的位置前后差距都在几十倍以上,当然即便差距很大生成10000次UUID的总耗时不过也就一百来毫秒.给出我的测试代码,可以尝试在本地运行查看结果(前提是有Otel的依赖)

package org.example;  

import io.opentelemetry.api.internal.OtelEncodingUtils;  
import io.opentelemetry.api.internal.TemporaryBuffers;  
import io.opentelemetry.api.trace.TraceId;  
  
import java.util.UUID;  
import java.util.concurrent.ThreadLocalRandom;  
  
public class Main {  

    private static final int cnt = 10000;  
  
    public static void main(String[] args) {  
        //testUUID();  
        testTraceId();  
        testUUID();  
    }  
  
    static void testUUID(){  
        long start = System.currentTimeMillis();  
        String traceId = " ";  
        for(int i=0;i<cnt;i++){  
            traceId = UUID.randomUUID().toString();  
        }  
        System.out.println("UUID生成时长: " + traceId);  
        System.out.println(System.currentTimeMillis()-start);  
    }  
  
    static void testTraceId(){  
        long start = System.currentTimeMillis();  
        for(int i=0;i<cnt;i++){  
            String traceId = generateTraceId();  
        }  
        System.out.println("traceId生成时长: ");  
        System.out.println(System.currentTimeMillis()-start);  
    }  
  
    static String generateTraceId(){  
        ThreadLocalRandom random = ThreadLocalRandom.current();  
        long idHi = random.nextLong();  
  
        long idLo;  
        do {  
            idLo = random.nextLong();  
        } while(idLo == 0L);  
  
        return fromLongs(idHi, idLo);  
    }  
  
    public static String fromLongs(long traceIdLongHighPart, long traceIdLongLowPart) {  
        if (traceIdLongHighPart == 0L && traceIdLongLowPart == 0L) {  
            return " ";  
        } else {  
            char[] chars = TemporaryBuffers.chars(32);  
            OtelEncodingUtils.longToBase16String(traceIdLongHighPart, chars, 0);  
            OtelEncodingUtils.longToBase16String(traceIdLongLowPart, chars, 16);  
            return new String(chars, 0, 32);  
        }  
    }  
 }   

traceId大致的生成逻辑已经在上一章梳理过了,UUID之所以慢是因为内部采用的随机数生成器是SecureRandom,它的好处是尽管性能不如ThreadLocalRandom,却能提供更好的安全性.具体怎么选择取决于你的使用场景.


Andy_Shawshank
1 声望0 粉丝