资讯详情

资讯详情

Grafana Tempo 依赖探秘:oklog/ulid Go 实现原理与 ULID 实战指南

Grafana Tempo 依赖探秘oklog/ulid Go 实现原理与 ULID 实战指南【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempoULIDUniversally Unique Lexicographically Sortable Identifier是一种 128 位、兼具时间顺序与字典序可排序能力的唯一标识符。本文以 Grafana Tempo 仓库中 vendored 的github.com/oklog/ulid/v2v2.1.2声明于 go.mod为核心完整讲解其设计动机、二进制规范、Go API 用法、熵源与单调性控制、命令行工具与性能特性并结合 ulid.go 源码展开底层实现剖析帮助读者在分布式系统、日志与追踪数据存储等场景中正确选择与使用 ULID。背景为什么需要 ULIDGUID/UUID 在很多场景下并非最优解README 中列举了四个典型痛点字符效率低UUID 不是编码 128 位数据最节省字符的方式UUID v1/v2 依赖环境在多数环境不可用因为它要求访问唯一且稳定的 MAC 地址UUID v3/v5 需要唯一种子生成的 ID 呈随机分布容易在许多数据结构中造成碎片化fragmentationUUID v4 只提供随机性除随机外不携带任何其他信息同样会导致数据结构的碎片化。而 ULID 的特性恰好弥补了这些不足与 UUID/GUID 位数兼容同为 128 位每毫秒可产生1.21e24 个唯一 ULID精确值为 1,208,925,819,614,629,174,706,176字典序可排序lexicographically sortable时间先后与字符串排序一致规范编码为26 字符字符串而 UUID 是 36 字符使用Crockfords Base32编码每字符承载 5 位效率与可读性更佳大小写不敏感无特殊字符URL 安全单调排序monotonic sort order能正确检测并处理同一毫秒内的生成请求。安装与版本该包要求 Go Modules 环境go get github.com/oklog/ulid/v2在 Tempo 仓库中它以一个间接依赖indirect的形式出现在 go.modgithub.com/oklog/ulid/v2 v2.1.2 // indirect虽然 Tempo 自身业务代码modules/、pkg/、tempodb/、cmd/并未直接调用它但它经由仓库 vendored 的 Prometheus TSDB其 block 目录以 ULID 命名见 vendor/github.com/prometheus/prometheus/tsdb/block.go以及 vendor/github.com/go-openapi/strfmt/ulid.go 等组件被间接引入这也印证了 ULID 在时序数据块命名这一经典场景中的实用价值。基本用法两个组成部分ULID 由两部分构造毫秒精度的时间戳与一段随机数据。时间戳建模为uint64表示 Unix 毫秒时间。可以通过ulid.Timestamp(time.Time)转换得到也可以调用time.Time.UnixMilli()后转为uint64随机数据来自调用方提供的io.Reader。这种设计允许在使用时自由权衡性能与安全性但对新手来说可能略显困惑。快速生成ulid.Make如果只是要生成一个 ULID且暂时不关心性能、加密安全性等细节直接使用ulid.Makefmt.Println(ulid.Make()) // 01G65Z755AFWAKHE12NY0CQ9FH从源码看ulid.goMake内部调用MustNew(Now(), defaultEntropy)Now()获取当前 UTC 时间的 Unix 毫秒值等价于Timestamp(time.Now().UTC())defaultEntropy是进程级全局的熵源由math/rand的伪随机数生成器构造并用LockedMonotonicReader包装保证线程安全且单调递增见 ulid.goMake本身对并发安全底层通过sync.Pool分摊锁竞争。进阶构造ulid.New更高级的使用场景应使用ulid.New(ms uint64, entropy io.Reader)entropy : rand.New(rand.NewSource(time.Now().UnixNano())) ms : ulid.Timestamp(time.Now()) fmt.Println(ulid.New(ms, entropy)) // 01G65Z755AFWAKHE12NY0CQ9FHNew的签名ulid.go显示其内部逻辑先调用id.SetTime(ms)写入 48 位时间戳时间超过最大值时返回ErrBigTime然后按熵源类型分发若熵源实现了MonotonicReader接口则调用其MonotonicRead(ms, id[6:])否则用io.ReadFull读取 10 字节填充熵字段。并发安全性完全取决于传入的熵源是否安全。MustNew是New的便捷封装失败时直接 panic 而非返回错误。熵源选择与性能/安全权衡README 明确警告提供熵源时需要格外谨慎。常见熵源选项熵源特点适用场景math/rand.Rand快但不可并发安全多个 goroutine 共用会出问题单线程/每 goroutine 独占golang.org/x/exp/rand.LockedSource加锁保证并发安全多 goroutine 共享crypto/rand密码学安全安全敏感场景sync.Pool池化熵源每 goroutine 独立熵源零锁竞争性能敏感场景性能敏感的并发策略README 给出建议性能敏感场景应避免在生成 ID 时加锁同步。一种方案是为每个并发 goroutine 使用独立的熵源优点完全没有锁竞争缺点无法对随机数据提供强保证且同一毫秒内不提供单调性monotonicity。一种常见的性能优化是用sync.Pool池化多个熵源对象兼顾复用与并发。安全性提示安全敏感的使用场景应始终使用crypto/rand提供的密码学安全熵。此外单调性熵源见下文的inc参数会影响 ID 的“可猜测性”——依赖熵字节安全性的代码应使用默认安全值。单调性同一毫秒内的排序保证单调性意味着每个 ULID 都“大于”前一个。默认情况下 ULID 只具备毫秒精度的自动单调性同一毫秒内生成的 ULID 由其随机分量排序因此默认是无序的。若需要同一毫秒内的严格单调使用ulid.Monotonic(entropy, inc)或ulid.LockedMonotonicEntropy并发安全版本。核心实现见 ulid.gom : Monotonic(entropy, 0) // inc0 表示默认值 math.MaxUint32 locked : LockedMonotonicReader{MonotonicReader: m}关键行为源码注释与实现一致同一 ULID 时间戳内的每次MonotonicRead调用会在前一个熵值上增加一个 1 到inc含之间的随机数inc 0时采用默认值math.MaxUint32inc越小同一毫秒内可产生的单调熵越少但 ID 越容易被“猜中”因此安全敏感代码应保持默认值若递增导致 80 位熵溢出返回ErrMonotonicOverflow底层的uint80类型ulid.go用Hi uint16 Lo uint64表示 80 位熵Add方法实现带进位的大整数加法并检测溢出MonotonicEntropy本身不适合并发使用需用LockedMonotonicReader内部sync.Mutex包装底层熵源必须确实能产生随机字节否则random()可能无法终止——它通过拒绝采样rejection sampling生成[1, inc)区间的均匀随机数并对math/rand.Rand提供快速路径。从源码结构看ulid.gorandom()对普通io.Reader会先计算inc的位宽再按 1/2/3-4/5-8 字节分档读取并做掩码重试属于典型的高效拒绝采样实现。解析与校验 APIParse 系列id, err : ulid.Parse(01G65Z755AFWAKHE12NY0CQ9FH) id, err : ulid.ParseStrict(01G65Z755AFWAKHE12NY0CQ9FH)Parse失败返回错误但非法编码会产生未定义 ULIDParseStrict额外校验所有 26 个字符都属于合法 Base32 字符集稍慢一些MustParse/MustParseStrict是失败即 panic 的便捷版本。底层parseulid.go依次检查长度必须为EncodedSize26严格模式下逐字符查dec表以0xFF为非法哨兵值O(1) 查找首字符大于7时报ErrOverflow因为 Base32 编码了 130 位而 ULID 只有 128 位见 ulid.go最后用展开循环将 26 字符解码为 16 字节。错误类型一览源码中定义的包级错误变量ulid.go错误触发条件ErrDataSize解析/反序列化时数据长度错误SetEntropy长度非 10ErrInvalidCharacters严格解析时出现非法 Base32 字符ErrBufferSize序列化目标缓冲区大小不足二进制需 16 字节、文本需 26 字节ErrBigTime构造时时间戳超过MaxTimeErrOverflow反序列化时首字符大于7超出 128 位容量ErrMonotonicOverflow单调熵递增时溢出 80 位空间ErrScanValueScan收到非字符串/字节切片的值时间相关 APInow : ulid.Now() // 当前 UTC Unix 毫秒 ms : ulid.Timestamp(time.Now()) // time.Time - Unix 毫秒 t : ulid.Time(ms) // Unix 毫秒 - time.Time max : ulid.MaxTime() // 可编码的最大时间戳实现细节ulid.gomaxTime是 6 字节全0xFF的时间值即(148)-1毫秒——据此推算要到公元 10889 年才会耗尽Timestamp为t.Unix()*1000 t.Nanosecond()/1e6源码注释提醒超过 10889 年的时间会产生未定义结果ULID 实例上还有Time()返回uint64毫秒、Timestamp()返回time.Time方法以及SetTime(ms)写入器。与数据库的集成Scan / ValueULID 实现了database/sql的Scanner与driver.Valuer接口ulid.go可直接用于 ORM 与数据库读写Scan(src)接受string或[]byte字节切片同时支持 16 字节二进制形式与 26 字符文本形式按长度自动分派到UnmarshalBinary/UnmarshalText否则返回ErrDataSizenil直接返回Value()默认返回 16 字节二进制MarshalBinary。README 给出包装类型示例若希望以字符串形式写入可自定义stringValuer包装类型并调用String()若希望零值 ULID 报错可自定义invalidZeroValuer。type stringValuer ulid.ULID func (v stringValuer) Value() (driver.Value, error) { return ulid.ULID(v).String(), nil } db.Exec(..., stringValuer(id))序列化与比较ULID 同时实现了encoding.BinaryMarshaler/TextMarshaler/BinaryUnmarshaler/TextUnmarshaler四接口b, _ : id.MarshalBinary() // 16 字节二进制 s, _ : id.MarshalText() // 26 字符文本MarshalBinaryTo/MarshalTextTo支持写入调用方提供的缓冲区零分配缓冲区长度不符返回ErrBufferSizeString()内部复用MarshalTextTo输出 26 字符规范字符串Compare(other ULID) int基于bytes.Compare返回 -1/0/1——这是字典序排序的底层支撑直接作用于 16 字节原始值而非文本IsZero()判断是否为ulid.Zero零值 ULID。规范二进制布局与字符串表示组成部分Timestamp48 位Unix 毫秒时间直到公元 10889 年才会耗尽空间Entropy80 位用户定义的熵源同一毫秒内可借助ulid.Monotonic保持单调编码字母表采用 Crockfords Base32排除了 I、L、O、U 四个字母以避免混淆与滥用0123456789ABCDEFGHJKMNPQRSTVWXYZ二进制布局16 字节网络字节序高位在前0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -------------------------------- | 32_bit_uint_time_high | -------------------------------- | 16_bit_uint_time_low | 16_bit_uint_random | -------------------------------- | 32_bit_uint_random | -------------------------------- | 32_bit_uint_random | --------------------------------即前 6 字节为时间戳后 10 字节为熵。这与源码中type ULID [16]byteulid.go、SetTime写入id[0:6]、熵写入id[6:]的结构完全一致。字符串表示01AN4Z07BY 79KA1307SR9X4MV3 |----------| |----------------| Timestamp Entropy 10 chars 16 chars 48bits 80bits base32 base32前 10 字符为时间戳48 位Base32后 16 字符为熵80 位Base32由于时间戳位于字符串前部字典序排序即时间排序这是 ULID 相对 UUID 最核心的优势MarshalTextToulid.go用展开循环完成逐字符编码String()的基准测试显示单次编码仅 1 次分配。命令行工具仓库同时提供ulid命令行工具可生成与解析 ULIDgo install github.com/oklog/ulid/v2/cmd/ulidlatest用法Usage: ulid [-hlqz] [-f format] [parameters ...] -f, --formatformat when parsing, show times in this format: default, rfc3339, unix, ms -h, --help print this help text -l, --local when parsing, show local time instead of UTC -q, --quick when generating, use non-crypto-grade entropy -z, --zero when generating, fix entropy to all-zeroes示例$ ulid 01D78XYFJ1PRM1WPBCBT3VHMNV $ ulid -z 01D78XZ44G0000000000000000 $ ulid 01D78XZ44G0000000000000000 Sun Mar 31 03:51:23.536 UTC 2019 $ ulid --formatrfc3339 --local 01D78XZ44G0000000000000000 2019-03-31T05:51:23.53602:00其中-z零熵选项非常适合演示与调试输出的 ULID 尾部 16 位为全零便于人工阅读解析结果。注意该命令行工具并未随 Tempo 仓库 vendored需要时请通过go install单独安装。测试与基准测试运行仓库测试go test ./...README 附带了作者在 Intel Core i7 Ivy Bridge 2.7 GHz、MacOS 10.12.1、Go 1.8.0beta1 环境下的基准数据节选关键项BenchmarkNew/WithCryptoEntropy-8 2000000 771 ns/op 20.73 MB/s 16 B/op 1 allocs/op BenchmarkNew/WithEntropy-8 20000000 65.8 ns/op 243.01 MB/s 16 B/op 1 allocs/op BenchmarkNew/WithoutEntropy-8 50000000 30.0 ns/op 534.06 MB/s 16 B/op 1 allocs/op BenchmarkParse-8 50000000 30.0 ns/op 866.16 MB/s 0 B/op 0 allocs/op BenchmarkString-8 20000000 64.9 ns/op 246.40 MB/s 32 B/op 1 allocs/op BenchmarkMarshal/BinaryTo-8 2000000000 1.18 ns/op 13551.75 MB/s 0 B/op 0 allocs/op BenchmarkTimestamp-8 2000000000 0.29 ns/op 27271.59 MB/s 0 B/op 0 allocs/op BenchmarkCompare-8 200000000 7.34 ns/op 4359.23 MB/s 0 B/op 0 allocs/op值得注意的结论熵源成本占主导使用密码学熵~771 ns/op比普通熵~66 ns/op慢约一个数量级解析、比较、二进制编解码均为零分配0 B/opBinaryTo写入预分配缓冲区可达 ns 级充分说明 ULID 是为高吞吐场景设计的。以上数据来自上游 README 的历史环境记录仅用于展示该实现的相对性能特征实际性能请以当前 Go 版本与目标硬件上的基准为准。何时不应使用 ULIDREADME 明确给出边界如果你不关心基于时间的 ID 排序就没有理由使用 ULID——还有很多更简单、更快、更小的 ID 方案例如 UUID。ULID 的价值完全建立在“时间有序 字典序可排序”这一组合之上。典型适用场景包括分布式追踪与日志的 trace/span 标识、时序数据库如 Prometheus TSDB 的 block 命名见 vendor/github.com/prometheus/prometheus/tsdb/block.go、事件流水线中的消息 ID、以及任何需要“按 ID 排序即按时间排序”的存储系统。参考资料Prior ArtULID 规范并非 oklog 原创README 中列出的既有实现包括ulid/javascript原始规范与 JS 参考实现RobThree/NUlid.NET 实现oklog 的 Go 版在解码与编码时直接借鉴了其展开循环技巧见 ulid.go 中多处注释imdario/go-ulid另一个 Go 实现。若需进一步阅读本仓库中的实现源码与变更记录可查看 vendor/github.com/oklog/ulid/v2/ulid.go 与 vendor/github.com/oklog/ulid/v2/CHANGELOG.md以及它在 Tempo 依赖图中的位置 go.mod 与 vendor/modules.txt。【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →