一个最小的 trace 收集器要做哪几件事?自己用 SQLite 存 span 能撑多少吞吐?索引对按 trace_id 的查询有多大帮助?怎么发现丢了 span 的不完整 trace?这篇用 57 行 Python 和 OpenTelemetry SDK 逐项量出来。
检查项 | 结果 ----------------------------------------+---------- SDK 里的 span 总数(真值) | 12194 收集器里存下的 span 数 | 12194 parent_id 与真值不同的 | 0 开始时间或耗时与真值不同的 | 0 服务名或 span 名与真值不同的 | 0 随机抽 100 个 trace:还原出的 span 数等于真值且恰好 1 个根 | 100 / 100 每个 trace 的 span 个数:最少 4、最多 8、平均 6.1;收集器的代码(parse 函数加 MiniCollector 类,含空行和注释)共 57 行 收集器只需要做三件事:把 OTLP 请求解析成行、存下来、查询时按 parent_id 把行拼成树;和 SDK 里的内存 span 逐个比较,没有丢也没有错
解析 OTLP 请求、存下来、查询时按 parent_id 拼成树;和 SDK 里的真值逐个比较没有丢也没有错。
写法 | 存下的 span 数 | 全部请求都被应答用的时间(s) | 按这个时间算的 span/秒 | 全部落盘用的时间(s) | 按落盘时间算的 span/秒 -----------------+------------+-----------------+----------------+-------------+--------------- (a)每个 span 单独提交 | 4000 | 1.41 | 2,833 | 1.72 | 2,325 (b)每个请求一个事务,默认日志 | 100000 | 0.60 | 166,775 | 1.10 | 91,174 (c)每个请求一个事务,WAL | 100000 | 0.58 | 173,029 | 1.08 | 92,791 (d)内存队列 + 后台批量写 | 100000 | 0.45 | 222,738 | 0.95 | 105,363 每个 span 单独提交的吞吐只有每秒两千多个;(b)(c)(d) 三种批量写法每秒都是几万到十几万个,彼此差别不大(都远高于单个提交),说明关键是把很多 span 合并成一次提交;内存队列加后台批量写把应答和落盘分开,应答最快,代价是崩溃时队列里还没写的 span 会丢(我没有测崩溃)。(a)只发了 8 个请求(4000 个 span),因为太慢;解析 protobuf 和 SQLite 写入在同一个进程里,这里的吞吐受 Python 的 GIL 限制
把很多 span 合并成一次提交是关键;逐个提交被落盘次数限制。
配置 | 数据库文件大小 | 平均每个 span 字节数 | 按 trace_id 取一个 trace(ms) | 耗时最长的 10 个根 span(ms) | 按服务统计 span 数(ms) -----+---------+---------------+--------------------------+----------------------+----------------- 有索引 | 96 MB | 192 | 0.009 | 0.0 | 108 没有索引 | 62 MB | 125 | 22.2 | 27 | 117 灌入 500,000 个 span 的耗时:无索引 0.8 s;有索引的库在灌入之后再建两个索引用了 0.4 s(灌入本身 0.9 s)。按 trace_id 查一个 trace,索引让它从扫描整张表变成毫秒以内;统计类的查询(group by)用不上这两个索引,仍然要扫描全表
trace_id 索引让按 trace 查询从扫表变成毫秒以内,代价是更大的存储(96 MB 对 62 MB)。
每个 span 被丢弃的概率 | 收集器里有记录的 trace 数 | 判定不完整的 trace 数 | 比例
---------------+------------------+----------------+------
0% | 1000 | 0 | 0.0%
5% | 1000 | 211 | 21.1%
20% | 1000 | 632 | 63.2%
每个 trace 有 6 个 span,最里层的叶子 span(query)被丢不会让 trace 变成“残缺的树”(只是少了一个叶子,这种情形判定不出来),所以不完整的比例比“有 span 被丢的 trace 的比例”低;丢任何一个非叶子 span 都会让它的子 span 变成孤儿,丢根 span 则找不到根;收集器在查询时(或者等一个超时之后)检查这两个条件,就能给这类 trace 打上标记
ERRORS {}按父 span 缺失或无根判定,丢任何非叶子 span 都会让子 span 变孤儿。