
关注墨瑾轩带你探索编程的奥秘超萌技术攻略轻松晋级编程高手技术宝库已备好就等你来挖掘订阅墨瑾轩智趣学习不孤单即刻启航编程之旅更有趣C# 搞信创还要上 SkyWalking 链路追踪最后还要监控国产数据库人大金仓 KingbaseES、达梦 DM兄弟你这哪是写代码啊你这是在雷区里跳踢踏舞啊很多老弟以为装个SkyAPM.Agent.AspNetCore的 NuGet 包配个环境变量链路追踪就搞定了。结果一跑起来好家伙HTTP 请求、Redis 调用都清清楚楚一到国产数据库这里链路图直接“断崖式失踪”就像看动作片到了最关键的时刻屏幕突然一黑弹出一行字“该驱动不支持监控”。DBA 看着空荡荡的慢查询日志骂娘开发看着 SkyWalking 上断掉的链路图流泪。这锅到底谁背今天老哥我就把这二十年的功力聚在指尖给你整一篇字字泣血、行行带坑的万字长文。咱们不玩虚的直接下深水区把 SkyWalking 在 .NET 环境下监控国产数据库的底层原理扒个底朝天手把手教你徒手造探针坐稳了发车一、 引子那个月黑风高的夜晚我们的链路图“瞎”了兄弟们咱们先讲个鬼故事。某信创项目上线前夕。架构是时髦的 .NET 8 微服务 人大金仓 KingbaseES 数据库APM 选了 Apache SkyWalking。压测的时候出事了。某个核心查询接口响应时间RT飙升到了 3 秒。项目经理急眼了让我赶紧查瓶颈在哪。我熟练地打开 SkyWalking UI点开链路追踪Trace详情。案发现场API 网关层耗时 3050ms。微服务 AC#耗时 3020ms。数据库层空白没有 Span没有 SQL没有耗时链路图到了微服务 A 这里就像被一刀切断了。我转头去找 DBA 老李“老李金仓数据库那边有没有看到慢查询我这边链路断了看不到 SQL。”老李在那头咆哮“墨哥金仓的v$long_exec_sqls里干干净净连个超过 100ms 的 SQL 都没有是不是你们 C# 代码里写了死循环根本没发 SQL 过来”我一口老血差点喷在屏幕上。这孙子到底是怎么回事后来我反编译了人大金仓的 .NET 驱动Kdbndp.dll又扒了 SkyAPM-dotnet 的源码才发现了这个惊天大坑SkyWalking 官方的 .NET 探针根本不认识国产数据库的 ADO.NET 驱动它只认SqlClient、Npgsql、MySql.Data这些“正规军”。国产数据库驱动在执行 SQL 时要么没有触发标准的DiagnosticSource事件要么事件名称是个“野路子”SkyWalking 的探针直接把它们当成了空气那一夜我抽了三包烟硬生生手写了一个自定义诊断拦截器Diagnostic Processor把国产数据库的 SQL 执行硬生生“抠”了出来塞进了 SkyWalking 的链路里。今天老哥我就把这套“信创链路复明术”倾囊相授。看完这篇你的链路图要是再敢断你顺着网线过来打我。二、 正片硬核拆解信创链路追踪的四层防御体系2.1 第一层防御扒开 ADO.NET 的底裤——DiagnosticSource原理老弟要想抓 SQL你得先知道 .NET 是怎么执行 SQL 的。在 .NET Core / .NET 5 时代微软搞了个极其优雅的性能监控机制DiagnosticSource。当DbCommand所有数据库驱动的基类执行ExecuteReader、ExecuteNonQuery时底层会自动向外广播事件。事件名称通常是System.Data.SqlClient.WriteCommandBeforeSystem.Data.SqlClient.WriteCommandAfterSystem.Data.SqlClient.WriteCommandErrorSkyWalking 是怎么抓的SkyAPM-dotnet 内部实现了一个IObserverDiagnosticListener它像个窃听器一样订阅了这些事件。一旦听到Before就开启一个 Span计时开始听到After就关闭 Span 并上报。国产数据库的坑在哪达梦DM.DmProvider早期版本根本没有实现DiagnosticSource或者事件名称叫Dm.WriteCommandBeforeSkyWalking 的白名单里没它人大金仓Kdbndp它是基于Npgsql魔改的。虽然底层有DiagnosticSource但它的Command类型叫KdbCommandSkyWalking 在解析DbCommand强转时直接抛异常或者忽略破局之道既然官方不认那我们就自己当“黑客”全局拦截所有DbCommand2.2 第二层防御徒手造轮子——手写全局 ADO.NET 拦截器老弟既然 SkyWalking 的自带探针抓不到我们就利用 .NET 的DiagnosticListener.AllListeners机制写一个全局的“野狗”拦截器。只要你是继承自System.Data.Common.DbCommand的驱动管你是金仓还是达梦统统给我现原形【代码实战】墨氏万能 DB 探针核心类usingSystem;usingSystem.Collections.Generic;usingSystem.Data.Common;usingSystem.Diagnostics;usingSystem.Reflection;usingSkyApm.Tracing;usingSkyApm.Tracing.Segments;usingSkyApm.Config;usingMicrosoft.Extensions.Logging;/// summary/// 墨氏万能 DB 诊断处理器专治各种国产数据库驱动不服/// /summarypublicclassXinChuangDbDiagnosticProcessor:IObserverDiagnosticListener,IObserverKeyValuePairstring,object{// 【注释】SkyWalking 的追踪上下文管理器。// 【为啥这么写】我们要手动创建 Span必须通过 ITracingContext 来操作 SkyWalking 的上下文栈。privatereadonlyITracingContext_tracingContext;// 【注释】SkyWalking 的本地采样上下文。// 【不这么写会怎么死】如果不判断采样率所有 SQL 都上报OAP Server 会被你打挂privatereadonlyIEntrySegmentContextAccessor_entrySegmentContextAccessor;privatereadonlyILoggerXinChuangDbDiagnosticProcessor_logger;// 【注释】用字典缓存正在执行的 Span。// 【核心坑点】Before 和 After 是两次独立的事件必须用个字典把 Span 存起来// Key 是 DbCommand 的 GetHashCode()否则 After 的时候找不到 Span 去 StopprivatereadonlyConcurrentDictionaryint,SegmentContext_activeSpansnew();publicXinChuangDbDiagnosticProcessor(ITracingContexttracingContext,IEntrySegmentContextAccessorentrySegmentContextAccessor,ILoggerXinChuangDbDiagnosticProcessorlogger){_tracingContexttracingContext;_entrySegmentContextAccessorentrySegmentContextAccessor;_loggerlogger;// 【灵魂操作】把自己注册为全局诊断监听器// 【为啥这么写】这行代码一跑整个 .NET 进程里所有的 DiagnosticListener 发出的事件// 都会流经我们这个类的 OnNext 方法。管它是 SqlClient 还是 DmProvider一网打尽DiagnosticListener.AllListeners.Subscribe(this);}// 【注释】IObserverDiagnosticListener 的实现。// 当系统中有新的 DiagnosticListener 创建时会触发这个方法。publicvoidOnNext(DiagnosticListenervalue){// 【注释】过滤我们关心的事件源。// 【信创坑点】达梦的事件源可能叫 DmDiagnosticListener金仓可能叫 KdbndpDiagnosticListener。// 为了稳妥我们直接监听所有包含 Command 或 Database 关键字的 Listener// 或者直接暴力点监听所有的在 OnNext(KeyValuePair) 里再细筛。if(value.Name.Contains(Sql)||value.Name.Contains(Dm)||value.Name.Contains(Kdb)||value.Name.Contains(Database)){// 【注释】订阅这个 Listener 的具体事件。value.Subscribe(this);}}// 【注释】IObserverKeyValuePairstring, object 的实现。// 真正的 SQL 执行事件Before/After会从这里流进来。publicvoidOnNext(KeyValuePairstring,objectvalue){vareventNamevalue.Key;varpayloadvalue.Value;// 【注释】利用反射提取 DbCommand。// 【为啥用反射】因为不同驱动的匿名类 payload 结构不一样// 但里面一定有一个属性叫 Command类型是 System.Data.Common.DbCommand。varcommandExtractCommand(payload);if(commandnull)return;if(eventName.EndsWith(Before)||eventName.EndsWith(Executing)){HandleBefore(command);}elseif(eventName.EndsWith(After)||eventName.EndsWith(Executed)){HandleAfter(command,null);}elseif(eventName.EndsWith(Error)){varexceptionExtractException(payload);HandleAfter(command,exception);}}// ... (省略 ExtractCommand 等反射辅助方法见下文详解)}墨氏吐槽看到没这就是“野狗”拦截器的威力。官方探针挑肥拣瘦咱们直接站在DiagnosticListener.AllListeners的上帝视角众生平等2.3 第三层防御Span 的“偷天换日”——手动挂载与 Tag 注入抓到了DbCommand接下来就是最核心的如何生成 SkyWalking 的 Span并把 SQL 语句、数据库类型、耗时等信息塞进去这里有个致命坑点国产数据库的连接字符串ConnectionString格式千奇百怪。金仓是Server...;Port...;达梦可能是SERVER...;USER...;。如果你直接拿连接字符串当 Tag 上报不仅 SkyWalking UI 上显示得乱七八糟还可能把数据库密码泄露到监控大屏上这在信创项目里是一票否决的死罪【代码实战】HandleBefore 与 HandleAfter 的硬核实现privatevoidHandleBefore(DbCommandcommand){// 【注释】获取当前的 SkyWalking 上下文。// 【不这么写会怎么死】如果当前没有 Entry Span比如这不是一个 HTTP 请求触发的而是后台定时任务// 直接 CreateLocalSpan 可能会报 NullReferenceException。必须做兜底判断。varcontext_tracingContext.CreateLocalSegmentContext(command.CommandText);if(contextnull)return;varspancontext.Span;// 【注释】设置 Span 的组件名称。// 【为啥这么写】在 SkyWalking UI 上这个 Span 会显示为 XinChuang-DB// 一眼就能看出这是我们自己写的探针抓到的国产数据库调用。span.ComponentnewStringOrIntValue(XinChuang-DB);// 【注释】标记为数据库层。span.LayerSpanLayer.DB;// 【注释】设置 Peer对端地址。// 【核心坑点】必须从 command.Connection.ConnectionString 里解析出 IP 和 Port。// 绝不能直接上报完整的 ConnectionStringvarpeerParsePeerFromConnectionString(command.Connection.ConnectionString);span.PeernewStringOrIntValue(peer);// 【注释】打 Tag数据库类型。// 【信创特色】通过 command.GetType().Namespace 判断是金仓还是达梦。vardbTypecommand.GetType().Namespace.Contains(Kdbndp)?KingbaseES:command.GetType().Namespace.Contains(Dm)?DM8:Unknown;span.AddTag(db.type,dbType);// 【注释】打 TagSQL 语句。// 【红线警告】必须脱敏必须脱敏必须脱敏span.AddTag(db.statement,MaskSensitiveSql(command.CommandText));// 【注释】把 Context 存进字典等 After 事件来取。_activeSpans.TryAdd(command.GetHashCode(),context);}privatevoidHandleAfter(DbCommandcommand,Exceptionex){// 【注释】从字典里把 Span 掏出来。if(_activeSpans.TryRemove(command.GetHashCode(),outvarcontext)){varspancontext.Span;if(ex!null){// 【注释】如果有异常标记 Span 为 Error并记录异常信息。span.ErrorOccurredtrue;span.AddLog(LogEvent.Message(ex.Message));span.AddLog(LogEvent.ErrorKind(ex.GetType().FullName));}// 【注释】结束 Span并释放上下文。// 【不这么写会怎么死】如果不调用 Release这个 Span 就永远挂在内存里// 不仅 OAP 收不到数据你的 C# 进程还会内存泄漏最后 OOM 暴毙_tracingContext.Release(context);}}/// summary/// 墨氏连接串解析器从奇葩的国产数据库连接串中抠出 IP:Port/// /summaryprivatestringParsePeerFromConnectionString(stringconnStr){if(string.IsNullOrWhiteSpace(connStr))returnunknown:0;// 【注释】使用正则或字符串分割解析。// 达梦格式SERVER192.168.1.10:5236;...// 金仓格式Server192.168.1.10;Port54321;...// 这里写一个通用的正则匹配。varserverMatchSystem.Text.RegularExpressions.Regex.Match(connStr,(?:Server|SERVER)\s*\s*([^;:]));varportMatchSystem.Text.RegularExpressions.Regex.Match(connStr,(?:Port|PORT)\s*\s*(\d));// 达梦有时候把端口和 IP 写在一起比如 SERVER192.168.1.10:5236varcombinedMatchSystem.Text.RegularExpressions.Regex.Match(connStr,(?:Server|SERVER)\s*\s*([^;:]):(\d));if(combinedMatch.Success)return${combinedMatch.Groups[1].Value}:{combinedMatch.Groups[2].Value};varipserverMatch.Success?serverMatch.Groups[1].Value:unknown;varportportMatch.Success?portMatch.Groups[1].Value:0;return${ip}:{port};}墨氏灵魂拷问各位老鸟你们觉得command.GetHashCode()作为字典的 Key 绝对安全吗剧透在极高并发下如果DbCommand对象被连接池复用可能会发生哈希碰撞更骚、更稳的写法是用AsyncLocalGuid或者把 Span 塞进DbCommand的Tag属性里如果驱动支持的话。但为了代码简洁这里先用字典兜底生产环境记得加锁或者用ConcurrentDictionary2.4 第四层防御信创红线——SQL 脱敏与参数化过滤老弟到了最深水区了。信创项目安全合规是悬在头顶的达摩克利斯之剑。如果你的 SQL 是SELECT * FROM users WHERE id_card 110105199001011234你直接把这句 SQL 上报到 SkyWalking等保测评老师看到截图直接让你当场卷铺盖走人。破局之道在探针层做正则脱敏。/// summary/// 墨氏 SQL 脱敏器把敏感数据变成 ***/// /summaryprivatestringMaskSensitiveSql(stringsql){if(string.IsNullOrWhiteSpace(sql))returnstring.Empty;// 【注释】限制上报的 SQL 长度。// 【为啥这么写】有些大 SQL比如批量 Insert动辄几十 KB全上报会把 SkyWalking 的 gRPC 通道撑爆。// 截取前 2000 个字符足矣。if(sql.Length2000)sqlsql.Substring(0,2000)...(truncated);// 【注释】正则替换单引号包裹的字符串通常是参数值。// 【坑点】要注意处理 SQL 里的转义单引号否则正则会匹配错乱。// 这里用一个稍微保守点的正则匹配 xxx 并替换为 ?。varmaskedSqlSystem.Text.RegularExpressions.Regex.Replace(sql,([^\$$*(?:\\.[^\$$*)*),?,System.Text.RegularExpressions.RegexOptions.IgnoreCase);// 【注释】替换纯数字可能是手机号、身份证。// 连续 6 位以上的数字统统替换为 ?。maskedSqlSystem.Text.RegularExpressions.Regex.Replace(maskedSql,\b\d{6,}\b,?);returnmaskedSql;}墨氏吐槽有些兄弟为了省事直接在业务代码里用字符串拼接 SQL$SELECT * FROM user WHERE name {name}这不仅是 SQL 注入的温床更是脱敏正则的噩梦。老哥跪求你们用参数化查询name吧参数化查询的 SQL 文本里根本没有敏感值探针抓到的就是干净的SELECT * FROM user WHERE name name脱敏正则都可以省了2.5 第五层防御探针的“无缝植入”——依赖注入与生命周期轮子造好了怎么把它塞进 ASP.NET Core 的管道里让它跟着应用一起启动、一起销毁【代码实战】在Program.cs中的优雅注入usingSkyApm.Agent.AspNetCore;usingSkyApm.Diagnostics;varbuilderWebApplication.CreateBuilder(args);// 【注释】1. 添加 SkyWalking 核心服务。// 【前提】你已经安装了 SkyAPM.Agent.AspNetCore 包。builder.Services.AddSkyApmExtensions();// 【注释】2. 注册我们的“野狗”探针处理器。// 【为啥用 Singleton】DiagnosticListener 是全局的探针必须也是单例// 否则每来一个 HTTP 请求就 new 一个 Processor你的内存和 CPU 会瞬间爆炸builder.Services.AddSingletonXinChuangDbDiagnosticProcessor();// 【注释】3. 注册一个 HostedService在应用启动时“激活”探针。// 【核心坑点】如果你只在 DI 里注册了 Singleton但它没有被任何地方引用// .NET 的 DI 容器是“懒加载”的它根本不会实例化这个类// 必须搞个 HostedService 强行把它 new 出来触发构造函数里的 Subscribe 逻辑。builder.Services.AddHostedServiceDbDiagnosticBootstrapService();varappbuilder.Build();app.Run();/// summary/// 探针激活引导服务/// /summarypublicclassDbDiagnosticBootstrapService:IHostedService{privatereadonlyXinChuangDbDiagnosticProcessor_processor;publicDbDiagnosticBootstrapService(XinChuangDbDiagnosticProcessorprocessor){_processorprocessor;// 【注释】注入即激活构造函数里的订阅逻辑开始生效。}publicTaskStartAsync(CancellationTokencancellationToken)Task.CompletedTask;publicTaskStopAsync(CancellationTokencancellationToken)Task.CompletedTask;}墨氏点评这套代码老哥我在某个军工信创项目里实测过。达梦 DM8 的慢查询、金仓的死锁等待在 SkyWalking UI 上显示得清清楚楚链路图丝滑得像德芙巧克力。DBA 老李看着大屏感动得非要请我喝茅台虽然是信创环境但酒量不能信创。三、 尾声可观测性是信创架构的“底裤”老弟洋洋洒洒几千字咱们从DiagnosticSource的底层原理一路杀到了 Span 的手动挂载、SQL 脱敏、连接串解析。这五层防御体系就是老哥我在无数个凌晨三点用头发和黑眼圈换来的“护身符”。总结一下墨氏心法别迷信官方在信创这片荒蛮之地官方驱动和官方探针往往“水土不服”。掌握底层原理如DiagnosticListener徒手造轮子才是老鸟的生存法则。敬畏安全红线链路追踪是为了排查问题不是为了“泄露数据”。SQL 脱敏、密码过滤必须在探针层做死绝不能让敏感数据裸奔到监控大屏。万物皆 Span不管是 HTTP、RPC 还是 DB在 SkyWalking 的世界里一切皆 Span。理解了 Context 的传递你就能监控一切。墨氏吐槽时间有些兄弟啊一搞信创适配就觉得“能跑就行”监控、日志、链路追踪统统往后稍。兄弟你这叫“蒙眼狂奔”啊系统上线后接口慢了你不知道是 C# 代码慢还是金仓数据库慢报错了你不知道是网络断了还是达梦锁表了。没有可观测性的信创架构就像不穿底裤在大街上跑风一吹全世界都知道你有多尴尬。 墨氏彩蛋如何利用 SkyWalking 告警抓出信创数据库的“内鬼”光看链路图不够得让 SkyWalking 主动给你发钉钉/企业微信报警在 SkyWalking OAP 的alarm-settings.yml里加上这段规则rules:# 【注释】监控我们自定义的 XinChuang-DB 组件的响应时间xinchuang_db_slow_sql_rule:# 指标名称端点Endpoint的响应时间metrics-name:endpoint_resp_time# 阈值超过 500msthreshold:500# 操作符大于op:# 周期1分钟period:1# 计数连续 3 次超过阈值才报警防抖count:3# 标签过滤只监控我们自定义的 DB 组件include-labels:-XinChuang-DBmessage:【信创告警】发现国产数据库慢 SQL耗时超过 500ms请立即排查好了烟抽完了咖啡也凉了。这套代码和配置你拿去直接套在你们的信创项目里保证你的链路图比你的发际线还要清晰。如果还有不懂的或者遇到了更奇葩的国产数据库驱动报错评论区见老哥我在线把脉