🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

Trace Sql:打通全鏈路日志最後一公里

image

背景介紹

曾經,我遭遇了一個荒謬的Bug。那是一個看似平凡的字段,由我和同事共同維護著,如同兩個陌生人在同一片土地上耕作。最終,我們發現這個字段背叛了現實,它所記錄的與真實情況南轅北轍。

生產環境是沉默的,它從不打印SQL語句,就像西西弗斯推石的山坡,永遠不會告訴你石頭為何滾落。我們各自負責的模組如同兩座孤島,彼此的業務邏輯互不相通,因此很難判斷問題的根源在哪裡。

後來,我借助阿里雲的SQL洞察,如同考古學家挖掘古跡一般,獲取到了那條記錄的更新歷史,這才為我洗刷了冤屈。原來是同事後續的操作將字段更新錯誤了。幸運的是,兩個操作之間存在時間差,這成了唯一的線索,讓真相得以浮出水面。

那一刻我想,如果SQL語句也能攜帶TraceId,就像每個人都有自己的身份證一樣,那麼回溯問題將變得簡單而確鑿,鐵證如山。這就是我們今天要探討的——在資料庫的荒原中,為每一條SQL語句刻上它的身份標記。

在微服務的迷宮中,全鏈路追蹤本應是我們的阿里阿德涅之線。我們在應用層、網關層、服務間調用等環節都小心翼翼地添加著TraceId,卻往往忽略了資料庫這個沉默的見證者。為了實現真正意義上的全鏈路追蹤,我們必須將TraceId的印記延伸到SQL語句中,讓每一次資料庫的低語都能被準確地追溯到它的源頭。

解決思路

恆等條件

筆者最開始想到的辦法是使用 WHERE子句,比如查詢的時候我在WHERE的最後加一個恆等表達式,形如:#{traceId} = #{traceId}

SELECT
    id, age, name 
FROM 
    tbl_user
WHERE
    id = 1 
AND 
    '004b0307-2d71-466e-aedf-cb8009893881' = '004b0307-2d71-466e-aedf-cb8009893881';

這樣我們就可以把鏈路中的TraceId帶入SQL,可是這麼搞局限性非常大:

  • SELECT、UPDATE、DELET都可以帶WHERE字句,但是INSERT不可以
  • 需要處理的case複雜,比如子句前需不需要AND,比如有的SQL是ORDER或者LIMIT子句結尾,那你還要找到WHERE子句的位置然後添加恆等式,等等
  • 即使各種case全部都能覆蓋,但是為了加上trace浪費挺多資源

綜上所述,筆者最終放棄了這個方案。

SQL註解

我們必須要找到一種足夠簡單,不需要應對各種複雜case,程式碼好寫好維護的方案。於是我想到可以把SQL都帶上註解,然後註解裡帶上Trace資訊。

/*X-ld:0002a4de-400f-40ae-ba46-9402f0eb46f4*/
SELECT
    id, age, name
FROM
    tbl_user
WHERE
    id = 1;

這個方案有很多好處:

  • 支援所有類型的SQL
  • 足夠簡單,不需要分析原SQL,僅僅只需要給原來的語句頭部加上註解資訊
  • 程式碼好寫,基本上也沒啥資源消耗

具體實現

使用MyBatis的 Interceptor 攔截 StatementHandler,修改SQL。MyBatis 允許開發者通過實現 org.apache.ibatis.plugin.Interceptor接口,攔截四大對象之一:

  • Executor
  • ParameterHandler
  • ResultSetHandler
  • StatementHandler我們重點要用這個!

我們要攔截的是:StatementHandler.prepare()方法,在這個方法執行之前或之後,可以拿到即將發送到資料庫的原始 SQL,然後我們在 SQL 的頭部或尾部拼接上 /* traceId = xxx */這種註解(筆者最終選擇頭部,因為一眼就能看到)。

import com.baomidou.mybatisplus.core.toolkit.StringPool;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;

import java.lang.reflect.Field;
import java.sql.Connection;

@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})
public class TraceSqlInterceptor implements Interceptor {

    /** 
     * 這裡沒叫traceId是因為阿里雲ARMS已經使用該名詞
     * 換成別的以示區分
     */
    private static final String TRACE_NAME = "X-Id:";

    /** 
     * @see BoundSql#sql 
     */
    private static final String SQL = "sql";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        if (target instanceof StatementHandler) {
            String sqlMark = buildMark();
            if (StringUtils.isNotBlank(sqlMark)) {
                StatementHandler stat = (StatementHandler) target;
                BoundSql boundSql = stat.getBoundSql();
                /* 原始SQL語句 */
                String sql = boundSql.getSql();
                setField(boundSql, sqlMark + sql);
            }
        }
        return invocation.proceed();
    }

    private void setField(BoundSql boundSql, String newSql) {
        try {
            Field field = BoundSql.class.getDeclaredField(SQL);
            field.setAccessible(true);
            field.set(boundSql, newSql);
        } catch (Exception e) {
            /* 忽略,因為失敗不會有任何影響,無非SQL無法成功著色 */
        }
    }

    /** 
     * 構造標記 
     */
    private static String buildMark() {
        /* 這一行是獲取業務系統的鏈路Id */
        String traceId = TraceContext.getTraceId();

        if (StringUtils.isBlank(traceId)) {
            return StringUtils.EMPTY;
        }

        return StringPool.SLASH
            + StringPool.ASTERISK
            + StringPool.SPACE
            + TRACE_NAME
            + traceId
            + StringPool.SPACE
            + StringPool.ASTERISK
            + StringPool.SLASH;
    }
}

同時不要忘了往Spring容器中注入攔截器實例

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class TraceSqlConfiguration {

    @Bean
    public TraceSqlInterceptor traceSqlInterceptor() {
        return new TraceSqlInterceptor();
    }
}

看看成果

image

可以看到阿里雲ARMS採集到的SQL已經全部帶上了業務系統的TraceId,目的達成......


後記
有朋友反饋我的文字功底不行,因此開頭的背景介紹是用AI改寫的,有那味兒了......
image


原文出處:https://juejin.cn/post/7553952804924801058


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝12   💬9   ❤️4
557
🥈
我愛JS
📝3   💬9   ❤️7
190
🥉
AppleLily
📝1   💬4   ❤️1
68
#4
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付