这两年做性能测试,我越来越觉得“慢查询日志”这四个字有迷惑性。很多团队一遇到接口响应慢,第一反应就是打开MySQL的慢查询日志,结果翻了大半天,日志干净得像刚擦过的黑板,一条超过阈值的SQL都没有。可接口就是慢,慢得离谱。后来我换了个思路,不在数据库层面死磕,而是把探针插到应用代码里,用插桩技术重新做慢查询测试,整个定位路径一下子就通了。这篇文章把这条思路的来龙去脉、落地代码和踩过的坑完整分享一下,适合正在被“接口慢但SQL不慢”这类问题折磨的测试开发、后端开发和性能测试工程师。
1. 慢查询日志的边界:数据库侧看不到的盲区远比你想象的多
先说清楚一个前提,慢查询日志不是没用,它是数据库侧的诊断工具,负责回答“哪条SQL在数据库里执行得久”。可现实中的慢接口,大量问题根本不出在SQL执行本身。
1.1 单条SQL不慢,但调用次数爆炸
最典型的场景就是N+1查询。前端请求一个订单列表接口,接口先查一次订单主表,拿到100条订单,然后在一个for循环里逐条去查订单明细表。单看每条明细SQL,执行时间都是0.5毫秒,在慢查询日志里连影子都见不到。但100条加起来就是50毫秒,再加上网络往返、ORM映射、事务开销,一个接口轻松突破300毫秒。这种问题,慢查询日志是永远发现不了的,因为它的统计单位是“单条SQL”,而不是“一次完整调用里所有SQL的累计耗时”。
我自己就遇到过这样的线上事故。接口平时30毫秒,某天突然变成800毫秒,慢查询日志零记录。最后是抓应用线程dump才发现,线程全部卡在循环查询明细表的代码上。那次之后我就明白了一个道理:慢查询测试不能只看数据库这一层,必须能看到“业务方法调用了多少次SQL、每一次各花了多久”。
1.2 连接池等待耗时不会进入慢查询日志
第二个盲区是连接池。假设数据库连接池最大连接数是20,某个瞬间有30个线程同时在请求连接,剩下10个线程就得排队。排队时间是纯等待,压根没有SQL在执行,自然不会有任何慢SQL记录。但用户感知到的接口耗时是“排队时间 + 执行时间”,可能已经超过1秒了。
连接池的问题,数据库侧诊断工具很难发现,因为从数据库的视角看,连接一旦建立,SQL执行速度都正常。只有从应用侧出发,统计“从租借连接开始到归还连接结束的整体耗时”,才能暴露这个瓶颈。
1.3 锁等待与事务相互干扰
这时候有人会说,InnoDB的行锁信息、锁等待超时日志,总能看出来吧?能,但那是另一个层面的事了。比如一个事务A更新了某行但不提交,事务B也去更新同一行,B会被阻塞到事务A提交为止。B阻塞期间没有新增慢SQL,数据库整体负载可能也不高,但是接口响应时间已经飙升。
更隐蔽的是死锁检测后的事务回滚,或者一个长事务里多个普通SQL相互拖累。这些问题的共同点是:慢的根源在“事务边界”和“锁的竞争关系”,不在单条SQL。你只有站在应用代码的调用层往下看,才能把事务里所有SQL拧成一个整体来分析。
1.4 业务代码耗时是“隐形大户”
还有一类更气人的场景,SQL慢查询日志看起来人畜无害,业务代码却在疯狂消耗CPU。我见过一个导出报表接口,慢查询日志没有任何异常,线程dump一抓,全在Java的字符串拼接和容器JSON序列化上。插桩统计显示,这个接口里SQL部分只占100毫秒,但数据加工、格式转换、内存拷贝花了700毫秒。
这类问题跟数据库完全无关,但用户感知到的就是“接口慢”。慢查询日志作为数据库层的工具,对这个场景是绝缘的。所以测试慢查询,本质上测的是“一个业务请求端到端的耗时分布”,而不是仅仅测数据库。
1.5 缓存穿透与多级缓存的“伪慢查询”
最后提一个跟“缓存”相爱相杀的场景。Redis缓存命中率本来挺高,接口很快。结果某个热点Key过期,或者布隆过滤器误判增多,大量请求直接穿透缓存打到底层DB。DB压力上升后,一些原本1毫秒的SQL膨胀到50毫秒,从而触发慢查询日志告警。日志告诉你有慢SQL,但真正的根因是缓存设计,修复方式是加缓存预热或改造缓存策略,而不是去优化SQL。
这类问题的定位路径往往是:先看到慢SQL告警,然后追查请求链路的完整耗时分布,最后才发现缓存的锅。而完整的请求链路耗时分布,恰恰是应用层插桩最擅长输出的东西。
慢查询日志不是不需要,而是它只能回答“数据库内部发生了什么”。想回答“用户的请求时间到底花在哪了”,必须引入应用层的观察手段。插桩技术解决的正是这个问题。
2. 应用层插桩:为什么说它是慢查询测试的“全景显微镜”
既然要看“一次请求的完整耗时分布”,就得在代码执行的各个关键节点打上探针,这就叫插桩。插桩技术的基本原理不复杂:在不侵入或尽量少侵入业务代码的前提下,往方法执行前、执行后注入统计逻辑,拿到每次调用的耗时、参数、调用来源,然后聚合成测试结论。
2.1 三个插桩粒度的分工
我习惯把插桩分成三个粒度,测试慢查询时三个粒度配合着看,才能还原全貌。
- 接口层插桩:统计整个HTTP/RPC接口的耗时、入参、出参大小,回答“慢不慢”。
- 服务方法层插桩:统计Service、核心业务方法内部执行耗时,回答“慢在业务的哪一段”。
- 数据访问层插桩:统计SQL的执行耗时、参数、批量大小,回答“SQL本身和调用次数是否有问题”。
从下往上,数据访问层的耗时最准,服务方法层的耗时可以定位到代码位置,接口层的耗时直接对应测试指标。三层数据对不上号的地方,往往就是最有问题的地方。例如SQL所有整体耗时加起来只有200毫秒,但Service方法测出来是800毫秒,那中间600毫秒肯定消耗在非数据库操作上,顺着这个线索就能逮到那个在循环里做复杂计算的坏味道方法。
2.2 三种主流实现方式的取舍
插桩具体怎么做,业界常见的路线有三条,按侵入性和灵活度排队。
| 实现方式 | 侵入性 | 粒度 | 学习成本 | 适合场景 |
|---|---|---|---|---|
| Java Agent字节码增强(Byte Buddy/ASM) | 无侵入,运行时修改字节码 | 任意方法,最细 | 高,需要对JVM和字节码有认识 | 全链路追踪、生产环境灰度观察、通用性能测试平台 |
| Spring AOP注解切面 | 低侵入,只需加注解 | 方法级及自定义切入点 | 中等,懂Spring即可 | Service层、Controller层耗时统计,日常测试环境首选 |
| MyBatis/ORM拦截器 | 低侵入,无需改业务代码 | SQL执行前后 | 中等,了解拦截器机制即可 | SQL耗时、参数统计、N+1问题排查 |
三条路线不是互斥的。我在测试环境最常做的组合是:MyBatis拦截器查SQL耗时,Spring AOP切面查Service方法耗时,再加个HandlerInterceptor查接口层耗时。这套组合几乎能覆盖60%到70%的慢查询测试场景,而且完全不需要上线Agent,不用改现有业务代码。
2.3 什么时候才需要上Java Agent
Java Agent是字节码层面的“终极形态”,在JVM加载类的时候直接改字节码,可以在不改源码、不接受Spring容器管理的类上插桩。如果被测系统包含大量工具类、静态方法、非Spring组件,或者要在生产环境对任意方法做采样,那就只有Java Agent能做到。
但代价很实在:类加载机制复杂,JVM参数要调整,对CRUD后测试人员的排查不友好,而且误改字节码可能导致线上类加载失败。我的建议是,测试阶段先别急着上Agent,把AOP和拦截器的方案跑通,拿到你需要的数据;如果后面发现确实需要对JDK类库或第三方SDK内部方法插桩,再考虑Agent。没必要一上来就把复杂度拉满。
3. 一套可落地的慢查询插桩统计:从SQL到接口的完整链路
理论说再多,不如直接上能跑的代码。下面这套是我在项目中沉淀下来的基础版本,测试环境直接可以复用。它分成三层,每一层解决一个具体问题。
3.1 MyBatis拦截器:统计SQL耗时、参数与最外层调用入口
MyBatis拦截器实现起来非常顺手,核心是实现Interceptor接口,在invoke方法里拿到StatementHandler,就能读取SQL语句和参数,再统计执行前后的耗时。
@Component @Intercepts({ @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}), @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}) }) public class SlowSqlInterceptor implements Interceptor { private static final long SLOW_THRESHOLD_MS = 200L; @Override public Object intercept(Invocation invocation) throws Throwable { long start = System.nanoTime(); try { return invocation.proceed(); } finally { long costMs = (System.nanoTime() - start) / 1_000_000; String sql = extractSql(invocation); String stackTrace = buildCompactStack(); if (costMs >= SLOW_THRESHOLD_MS || isSqlCalledTooManyTimes()) { log.warn("slow-sql|cost={}ms|sql={}|stack={}", costMs, sql, stackTrace); } else { log.debug("normal-sql|cost={}ms|sql={}", costMs, sql); } } } private String extractSql(Invocation invocation) { StatementHandler handler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = handler.getBoundSql(); return boundSql.getSql().replaceAll("\\s+", " ").trim(); } private String buildCompactStack() { StackTraceElement[] stack = Thread.currentThread().getStackTrace(); StringBuilder sb = new StringBuilder(); int depth = 0; for (StackTraceElement element : stack) { String className = element.getClassName(); if (className.startsWith("com.yourcompany.biz") && depth < 10) { sb.append(className).append("#").append(element.getMethodName()).append("<-"); depth++; } } return sb.length() > 0 ? sb.toString() : "unknown"; } }有几个细节值得说。打印SQL的时候一定要把参数打出来,否则你只看到“查询很慢”,却不知道查的是哪个ID的明细。推荐在日志里输出实际参数,比如where order_id = ?要变成where order_id = 123456。留意上面extractSql只拿了个原始的BoundSql,想输出完整可执行SQL还得自己遍历ParameterHandler拿到参数,建议大家在工程里把这一步补全。两个关键地方,请大家留意其中的重要性。
调用栈需要做截断处理。我上面设置成只抓
com.yourcompany.biz包下的调用栈,且最多10层。不然每条SQL都把完整线程栈打出来,日志量巨大,而且大量无关的框架栈帧会淹没真实原因。只保留业务包路径,定位时一眼就能看到入口Service。慢SQL的阈值不建议统一写死。200毫秒是我在单体应用里的默认值,如果你的系统本身很快,可以降到50毫秒;如果你的系统接口基准就要500毫秒,太低的阈值只会刷屏,反而没人看日志。
3.2 Spring AOP切面:统计Service方法耗时并聚合定位
有了SQL耗时,下一个问题是“这些SQL是从哪个方法发起的”。用Spring AOP切面统统计所有带@ApiOperation或自定义@SlowQueryProbe注解的方法,把方法名、耗时、创建子调用次数串联起来。
@Aspect @Component public class ServiceCostAspect { private final ConcurrentHashMap<String, MethodMetric> metricMap = new ConcurrentHashMap<>(); @Around("@annotation(com.yourcompany.framework.annotation.SlowQueryProbe)") public Object around(ProceedingJoinPoint pjp) throws Throwable { String methodKey = pjp.getSignature().getDeclaringType().getSimpleName() + "#" + pjp.getSignature().getName(); long start = System.nanoTime(); try { return pjp.proceed(); } finally { long costMs = (System.nanoTime() - start) / 1_000_000; MethodMetric metric = metricMap.computeIfAbsent(methodKey, k -> new MethodMetric()); metric.add(costMs); if (costMs >= 500) { log.warn("slow-method|method={}|cost={}ms|params={}", methodKey, costMs, truncateParams(pjp.getArgs())); } } } static class MethodMetric { AtomicLong count = new AtomicLong(); AtomicLong totalCost = new AtomicLong(); void add(long cost) { count.incrementAndGet(); totalCost.addAndGet(cost); } } // 通过一个HTTP接口或定时任务输出聚合结果 public Map<String, Object> snapshot() { Map<String, Object> result = new HashMap<>(); metricMap.forEach((method, metric) -> { long count = metric.count.get(); long total = metric.totalCost.get(); result.put(method, Map.of("count", count, "avg", total / Math.max(count, 1), "total", total)); }); return result; } }这个切面最大的用处不是“抓到一次慢方法”,而是metricMap里积累的聚合数据。当测试跑完一轮回归,你能直接输出一张方法耗时排行榜:哪个Service方法调用次数最多、平均耗时多少、总耗时多少。配合上面的SQL拦截器,把methodKey和SQL日志里的栈链对上,就组成了“每个方法的SQL/业务耗时总和”,这一步已经能覆盖大多数慢查询测试场景了。
3.3 接口层拦截器:串起完整请求维度
最后再加一层接口耗时统计。如果你用Spring MVC,用HandlerInterceptor的preHandle和afterCompletion统计整个HTTP请求耗时即可;如果你内部走Dubbo或者gRPC,就在RPC Filter层做同样的事。接口层数据主要用于对外输出测试报告,比如“接口login耗时分布:总体780ms,Service层660ms,DB层430ms”,产品经理和开发负责人看这种数据最直观。
这一层还要顺便统计出参大小。接口慢有时候是因为返回了超大的JSON结构,前端解析慢,但后端CPU和SQL都还好,这种“响应体胖慢”在接口层插桩里会原形毕露。
@Component public class ApiCostInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { request.setAttribute("_api_start", System.nanoTime()); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { Long start = (Long) request.getAttribute("_api_start"); if (start == null) { return; } long costMs = (System.nanoTime() - start) / 1_000_000; String path = request.getRequestURI(); int respSize = response.getContentLength(); log.info("api-cost|path={}|cost={}ms|respSize={}", path, costMs, respSize); } }3.4 与自动化测试框架的联动
这一层插桩做好了,可以把统计结果暴露成一个专门的HTTP端点,比如/internal/metrics/snapshot,测试脚本里直接断言。如果你用pytest做自动化测试,可以这样写:
def test_order_detail_slow_query(): # 先调用被测接口 client.get("/order/detail?orderId=123456") # 再拉取插桩聚合数据 metrics = client.get("/internal/metrics/snapshot").json() order_service_avg = metrics["OrderServiceImpl#queryDetail"]["avg"] assert order_service_avg < 300, f"OrderServiceImpl#queryDetail avg cost too high: {order_service_avg}"这种“先跑业务再查插桩数据”的闭环方式,比单纯断言HTTP响应时间更稳定。因为再好的性能测试也躲不开宿主机偶发的资源抖动和第二轮垃圾回收Stop-The-World,但插桩聚合数据看的是多次调用的平均分布,对偶发抖动天然免疫。
4. 一个真实案例:接口慢得像蜗牛,慢查询日志却“很干净”
下面用一条实际排查链路,完整演示插桩技术怎么定位慢查询问题。案例背景是一个用户中心的“消息列表接口”,测试环境响应时间基线是150毫秒,某天突然掉到1.2秒,开发第一反应是查MySQL慢查询日志,结果一条记录都没有。
4.1 第一轮排查为什么扑空
一开始大家顺着SQL优化思路走了一遍:检查索引、看执行计划、看锁等待,数据库侧一切正常。然后怀疑网络抖动,ping、tcpdump都做了一遍,也没有异常。最后是压上插桩工具,才把这个问题带到阳光下。
4.2 测试脚本侧的压力回放
我先用一条测试脚本把线上读流量回放到测试环境,跑了两分钟,立刻去看插桩聚合数据。AOP切面的方法排行榜上,第一行也不是什么SQL相关方法,而是一个看起来不起眼的UserMsgUrlDecorator#batchDecorate,总耗时占了这个接口端到端耗时的62%。进一步观察,发现这个方法的调用次数达到846次,平均耗时0.9毫秒。单看0.9毫秒肯定不慢,但846次累计就是761毫秒。
4.3 插桩数据带来的关键转机
这个batchDecorate方法名太普通了,没有插桩的话根本不会有人注意到它。顺着SQL拦截器输出的调用栈往下一看,最外层调用入口是MessageServiceImpl#queryPage,栈链下面跟着846次单条SELECT ... FROM message_extra WHERE msg_id = ?。我截取了一段日志,长这样:
slow-sql|cost=2ms|sql=SELECT * FROM message_extra WHERE msg_id = 234567|stack=MessageServiceImpl#queryPage<-UserMsgUrlDecorator#batchDecorate<-MessageController#pageList ... slow-sql|cost=1ms|sql=SELECT * FROM message_extra WHERE msg_id = 234568|stack=MessageServiceImpl#queryPage<-UserMsgUrlDecorator#batchDecorate<-MessageController#pageList第二次调用栈虽然长,但一眼能定位到调用关系。这就是插桩比慢查询日志狠的地方:不只告诉你“这条SQL花了几毫秒”,还告诉你“这段SQL是被谁调起来的、同一条链路里调了多少次”。
4.4 根因确认与修复
真相大白了,这是一个教科书级的N+1查询。列表接口查出当前页的20条消息,MessageController调MessageServiceImpl#queryPage只执行1条分页SQL,但随后在装饰器batchDecorate里,为了拼上每条消息的扩展字段,逐条执行了SELECT * FROM message_extra WHERE msg_id = ?。如果当前页是20条消息,就是一个SQL执行20次;我这边压了846次,说明测试脚本分页翻了好几十页,每一页都在重复这个循环。
修复方案也很简单,把装饰器的单查改成批量:传入20个msg_id,执行一次WHERE msg_id IN (...) OR查询,然后把结果按msg_id做本地Map映射。改完之后重新跑同一轮压测,MessageServiceImpl#queryPage的耗时从761毫秒降到12毫秒,接口总耗时回到160毫秒附近。如果没有插桩技术,没有那行精确到业务方法名的调用栈,这轮排查大概率还要耗上一整天。
4.5 这个案例给测试工作的启示
案例里最值得记住的一个点:单条慢SQL阈值再低,也逮不住0.9毫秒的SQL。真正制造问题的是“调用模式”,是代码写了一个糟糕的循环。插桩技术恰恰能在方法级查看调用关系和累计耗时,这是慢查询日志、APM工具、线程dump都无法单独替代的能力。
5. 插桩技术在慢查询测试中的进阶玩法
除了“发现问题”,插桩技术在慢查询测试里还有几个很强的玩法,能让测试更主动,而不是被动等故障发生。
5.1 注入延迟:主动制造“慢查询故障”
插桩除了统计耗时之外,还可以加入一个过滤器:当某个方法名或某个SQL的WHERE条件命中测试规则时,主动在方法内Sleep一段指定时间。我在故障演练里用过这个方案,配合Chaos工具,可以模拟一个SQL突然从1毫秒退化到500毫秒的效果,然后观察接口熔断、降级、连接池放量这些行为是否正常。
实现方式就是在拦截器的intercept方法里加一段判断逻辑,比如通过配置中心拿到规则Map,命中规则就Thread.sleep(500)。这样做的好处是故障可控、可回放、可递增,比等着线上自然出问题高效太多了。
5.2 与全链路追踪打通:把插桩日志挂到traceId下
插桩统计的数据要落地到排障流程里,最好和全链路追踪产品做一次关联。做法很简单:在入口拦截器里拿到traceId,放到ThreadLocal或MDC里,然后MyBatis拦截器、AOP切面打印日志时,把traceId一起带上。这样以后上游调接口变慢,测试同学在链路追踪平台上点开一次完整Trace,每个SQL、每个方法耗时全都按时间轴排列,排查效率提升不是一星半点。
5.3 把插桩统计做成性能测试平台的“雷达图”
我在团队里把/internal/metrics/snapshot接口的JSON输出接入了内部的性能测试平台,每次回归都自动拉一次数据,然后生成“方法耗时分布雷达图”。它会标记出TOP10方法、累计调用次数、平均耗时和90分位耗时。测试负责人打开平台就能看到:到底是哪个方法本次迭代引入了额外SQL,还是某个上游服务超时导致下游调用次数暴增。这套机制跑了几轮迭代之后,我们甚至能在代码评审阶段就发现潜在的N+1问题,因为在评审前,冒烟测试的插桩数据已经把关键路径方法扫描过一遍了。
5.4 动态开关与采样率:避免插桩自身成为瓶颈
任何时候都要意识到,插桩本身是有开销的。每次拦截都会多几次方法调用、日志输出、字符串拼接,吞吐量一高,这点开销就可能被放大。所以我的工程里会做一个全局开关和一个采样开关:
slow-query: enabled: true sample-rate: 1.0 slow-threshold-ms: 200 max-stack-depth: 10 exclude-packages: - "com.alibaba.fastjson" - "org.apache.commons"压测冒烟时采样率1.0,全量采集;长时间稳定性测试时采样率调到0.1或0.01,只取一部分数据评估趋势。开关用@ConditionalOnProperty控制,测试环境打开,生产环境默认关闭,需要时才打开。
6. 插桩使用的避坑经验:这些坑我踩过,你就不用踩了
最后按老规矩,把实操中踩过的坑集中说一遍。这些细节看起来小,但能直接影响插桩方案能不能真正落地。
6.1 日志输出会反向拖垮被测系统
刚开始做SQL插桩时,我理所当然地每条SQL都打一行日志,结果一轮压测下来,被测接口比原来慢了30%还多。罪魁祸首就是日志刷得太猛,同步磁盘IO都被打满了。后来我改成两层日志策略:慢SQL日志走WARN级别,正常SQL的耗时统计只更新内存指标,不落盘。等一轮测试结束后再按需输出聚合报告。性能测试是拿数据喂给被测系统,前提是探针本身不能喧宾夺主。
6.2 调用栈聚合别设太深
有的系统入口在Controller层,中间调了3个Service,再往下走MyBatis执行SQL。如果你把插桩的调用栈打印设成20层、30层,每一层都是框架代码,看起来密密麻麻,实际有用的就那几行业务包路径。做测试的同学不要追求完整的线程栈,我建议把max-stack-depth设成5到10,并且只保留你自己的业务包。否则两分钟压测下来,日志文件能膨胀到GB级。
6.3 批量SQL与异步线程的统计偏差
插桩统计有一个天然偏差,要心里有数。MyBatis批量执行SQL(比如ExecutorType.BATCH)或者配合分页插件插入LIMIT 10,20,拦截器统计的时间往往不是一个单条SQL的真实耗时,而是批量操作的总耗时。如果你拿着批量SQL的单次耗时去和一些单查SQL对比,容易得出错误的优化结论,把原本正常的批量逻辑误判成慢查询。
另外异步线程和消息队列消费链路,耗时统计不能和请求线程直接挂钩。我在项目里用@Async的线程池处理过一条定时任务链路,AOP切面统计到的耗时是任务的总耗时,不是单次用户请求的耗时。所以看插桩数据时,先搞清楚这段代码是在同步请求链路里执行,还是在异步线程里执行,两者的优化目标和评价标准并不一样。
6.4 耗时阈值不要拍脑袋定
慢SQL阈值设多少,慢方法阈值设多少,一定要从被测系统的基线数据里反推。我一般的做法是:先用插桩方案跑一轮无压力基线(比如50TPS小流量压10分钟),把各方法P90、P99耗时的数据收集出来,然后以P90耗时的1.5倍作为告警阈值。没有基线数据就直接定一个“0.5毫秒压测阈值”,只会让日志天天刷屏,最后谁也不看。
6.5 环境隔离:插桩代码从测试环境到生产的风险边界
插桩代码能不能进生产环境,这个话题很敏感。我的原则是:统计型插桩可以做,但要能满足两个条件。第一,必须有动态开关,默认关闭;第二,日志写异步,绝不能干扰正常业务线程。而故障注入型插桩,只在独立的生产演练环境或者熔断演习时才打开,常规生产流量一律关闭。最稳妥的做法是抽一个独立分支,把插桩代码用独立的包层级隔离,测试环境用这个分支部署,主分支不合并插桩逻辑,从物理上把风险隔离开。
最后再分享一个我自己的习惯。不管用哪种插桩方案,我都在测试环境默认开启,同时只对关键业务接口打开完整调用栈,其余接口只做聚合统计。这样既能保证日常回归有数据可看,又不至于被海量日志淹没。慢查询测试这件事,本质上不是找一个“银弹工具”,而是建立一套能穿透应用层、数据库层、中间件层的观察能力。插桩技术是这套观察能力的地基,希望能给同样被困于慢查询日志盲区的你一点启发。