bigywater.tech
微服务 · 78

数据库调用的追踪:慢查询怎么定位到调用链

慢查询日志里的 SQL 怎么对应到是哪次请求?SQL 注释里放 traceparent 有多大帮助?N+1 在 trace 里长什么样?db.statement 该写字面值还是占位符?连接池的等待算在数据库头上吗?这篇用真实的 MySQL 和 OpenTelemetry 逐项量出来。

120 vs 14
120 行慢日志里,带 traceparent 注释和不带注释分别能唯一对应到客户端 span 的行数
21.0 vs 1.0
N+1 写法与一次 JOIN 写法每个请求的 db span 个数
507 vs 203 ms
连接池只有 3 个连接、12 个并发时,包住取连接和执行的 db span 与只含执行的 db.execute span 的耗时中位数

慢日志与调用链

实测 · 本机执行输出
  慢日志表里 start_time 相对客户端 db.query span 的偏移(用带注释的行精确对应后算出,中位数):相对 span 的结束时刻 -0.1 ms,相对 span 的开始时刻 +179.2 ms;因此没有注释时用 “慢日志时间 ≈ span 的结束时刻 ± 30 ms” 且规整后的 SQL 文本相同来找候选 span
  配置                  | 慢日志行数 | 唯一对应到一个 span | 有多个候选 span(对应不上) | 没有候选 | 平均候选 span 数 | 最多候选 span 数
  --------------------+-------+--------------+------------------+------+-------------+------------
  (a)带 traceparent 注释 | 120   | 120          | 0                | 0    | 1.0         | 1          
  (b)不带注释             | 120   | 14           | 106              | 0    | 3.0         | 5          
  有注释时,慢日志里的 SQL 文本带着 trace_id,直接对应到那一次调用;没有注释时只能靠 SQL 文本和时间推断,并发请求执行同一条 SQL(只有字面值不同)时,同一时刻有多个候选,分不出是哪一个请求

SQL 注释里放 traceparent,服务端的慢日志和 performance_schema 里就有 trace_id,可以精确对应到那一次调用。

performance_schema

实测 · 本机执行输出
  30 次快查询里能按 trace_id 在 performance_schema 里找到的有 30 次;其中一条:digest_text = 'SELECT * FROM `orders` WHERE `id` = ?',TIMER_WAIT = 0.067 ms,SQL_TEXT 开头 "/*traceparent='00-3fe56ef50d43c718a408ebffac030568-798450faa80ce43d-03'*/ select";这些快查询不在慢日志里(慢日志表里现有 120 行,都是上一节的慢查询)
  慢日志只记录超过 long_query_time 的语句;performance_schema 的语句历史能记录所有语句(受表大小限制,会被覆盖),SQL 注释里的 trace_id 让两者都能和调用链对上

语句历史记录所有语句,不受慢日志阈值限制。

N+1

实测 · 本机执行输出
  写法         | 每个请求的 db span 个数 | 请求耗时 p50(ms) | p99(ms) | 每个请求 db span 耗时合计(ms)
  -----------+------------------+--------------+---------+----------------------
  (a)N+1     | 21.0             | 2.19         | 2.53    | 1.88                 
  (b)一次 JOIN | 1.0              | 0.79         | 0.88    | 0.77                 
  N+1 在 trace 里是一串等宽的短 db span 紧挨着排开,单个都不慢,所以慢日志抓不到;发现它靠看一个请求下 db span 的个数

N+1 的每个 db span 都很短,靠看一个请求下 db span 的个数发现。

db.statement

实测 · 本机执行输出
  写法     | 不同的 db.statement 个数 | 含邮箱地址的 span 数 | db.statement 总字节数
  -------+---------------------+---------------+------------------
  (a)字面值 | 1000                | 1000          | 55890            
  (b)占位符 | 1                   | 0             | 37000            
  字面值把每个请求的参数都带进 span:不同语句的个数和请求数一样多,按语句聚合(比如统计各 SQL 的耗时)就没有意义,用户的邮箱也进了追踪后端;占位符形式的语句只有一种,聚合有意义也不含用户数据

用占位符形式,语句个数才有限,聚合有意义,也不会把用户数据带进追踪后端。

连接池等待

实测 · 本机执行输出
  span           | 耗时中位数(ms) | 最小(ms) | 最大(ms)
  ---------------+-----------+--------+-------
  db.query.total | 507       | 202    | 814   
  pool.acquire   | 305       | 0      | 609   
  db.execute     | 203       | 200    | 206   
  只用一个 span 把“取连接 + 执行”包在一起时,看到的是“数据库慢”;其实一部分是排队等连接——拆成 pool.acquire 和 db.execute 才能看出数据库本身只用了约 200 ms,其余是等连接
ERRORS {}

把取连接和执行拆成两个 span,才能看出数据库本身和排队各占多少。

分步演示

数据库追踪
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。MySQL 26.7.0(自己起的,performance_schema 打开,慢日志写表),pymysql,OpenTelemetry Python SDK 1.45.1(手写的 db span,不是自动插桩);慢查询是 items 表(3 万行)的自连接,单次约 100~150 ms(为让慢日志有内容把阈值设成 0.08 s);不带注释的匹配用的是 “规整后的 SQL + 结束时刻 ± 30 ms” 这个我选的规则,换更聪明的规则(例如利用字面值)候选会更少;每节只跑了一次
    • 没有验证:自动插桩库(opentelemetry-instrumentation-pymysql / sqlcommenter)对注释的写法和对预编译语句的处理;注释对语句缓存、查询摘要(digest)的影响(performance_schema 的 digest_text 里注释被去掉了);PostgreSQL 的 auto_explain 和 pg_stat_statements;分库分表中间件后面的慢查询;MySQL 的 EXPLAIN ANALYZE 与 span 的关联
    京ICP备2026067502号-1京公网安备11010502063487号