[诡异问题] C# 中用了 MysqlConnector 后,出现无法捕获的异常


作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!


我封装了一个 Mysql 连接池,目的是始终维持着一个 mysql connector 对象,然后把使用过的语句都进行预编译。
然后,重复执行 sql 的时候,始终能够使用预编译的 sql。
具体的实现请看:https://github.com/ahfuzhang/QiWa.framework/blob/v0.9.1/src/Mysql/DbConnectionPool.cs

然后网站总是会重现一个问题:一段时间无法再访问后,第一个请求会出现 500 错误。
我抓到的堆栈信息如下:

at System.Net.Security.SslStream.<<WriteSingleChunk>g__CompleteWriteAsync|166_1>d`1.MoveNext() + 0x88
 --- End of stack trace from previous location
 --- at System.Net.Security.SslStream.<WriteAsyncInternal>d__173`1.MoveNext() + 0x2e1
 --- End of stack trace from previous location 
 --- at MySqlConnector.Protocol.Serialization.StreamByteHandler.<<WriteBytesAsync>g__DoWriteBytesAsync|7_0>d.MoveNext() + 0x90 --- End of stack trace from previous location 
 --- at MySqlConnector.Protocol.Serialization.ProtocolUtility.<WritePayloadAsync>d__2.MoveNext() + 0xfe 
 --- End of stack trace from previous location 
 --- at MySqlConnector.Core.ServerSession.<SendReplyAsync>d__122.MoveNext() + 0x200 
 --- End of stack trace from previous location 
 --- at MySqlConnector.Core.CommandExecutor.<ExecuteReaderAsync>d__0.MoveNext() + 0x585 
 --- End of stack trace from previous location 
 --- at MySqlConnector.MySqlCommand.<ExecuteReaderAsync>d__87.MoveNext() + 0x1d5 
 --- End of stack trace from previous location 
 --- at QiWa.Mysql.MySqlCommandWrapper.<ExecuteReaderAsync>d__12.MoveNext() + 0x5f 
 --- End of stack trace from previous location 
 --- at QiWa.Mysql.DbConnection`3.<ExecuteReaderAsync>d__16.MoveNext() + 0x499 
 --- End of stack trace from previous location 
 --- at Generated.CSharpBackend.API.V1.Menu.Rpc.LoadMenuTreeContext.<RunAsync>d__3.MoveNext() + 0x495 
 --- End of stack trace from previous location 
 --- at Generated.CSharpBackend.API.V1.Menu.Rpc.MenuService.<CallMethod>d__11`2.MoveNext() + 0x4bb

QiWa.Mysql.MySqlCommandWrapper. 这个位置是我的框架的代码。
这部分代码的写法如下:

        try
        {
            var reader = await cmd!.ExecuteReaderAsync(ct).ConfigureAwait(false);
            await using (reader.ConfigureAwait(false))
            {
                long rowCount = 0;
                while (await reader.ReadAsync(ct).ConfigureAwait(true))
                {
                    var rowErr = eachRowFunc(reader);
                    if (rowErr.Err())
                    {
                        return (rowCount, rowErr);
                    }
                    rowCount++;
                }
                return (rowCount, default);
            }
        }
        catch (MySqlException ex)
        {
            return (0,
                Error.WithLoc(
                    (int)ErrorCodes.ExecuteMySqlExceptionError,
                    $"[MySqlException]ExecuteReaderAsync: {ex.Message}"
                )
            );
        }
        catch (OperationCanceledException)
        {
            _disableReuse = true;  // 如果发生超时,则这条连接可能一直在使用之中,所以不再重用这个连接对象
            return (0, Error.WithLoc((int)ErrorCodes.ExecuteTimeoutError, "[OperationCanceledException]cmd.ExecuteReaderAsync timeout"));
        }
        catch (IOException exIO)
        {
            _disableReuse = true;
            return (0, Error.WithLoc((int)ErrorCodes.ExecuteIOExceptionError, $"[IOException]cmd.ExecuteReaderAsync,ex={exIO.Message}"));
        }
        catch (Exception exUnknown)
        {
            _disableReuse = true;
            return (0, Error.WithLoc((int)ErrorCodes.ExecuteUnknownExceptionError, $"[Exception]cmd.ExecuteReaderAsync,ex={exUnknown.Message}"));
        }

可以看到,最后一个 catch 中已经加上了 catch (Exception),理论上无论抛出什么异常都会被抓到。

神奇的地方是,http 入口的 try catch 又抓住了这个异常。这部分代码是:

        // 调用业务
        try
        {
            long start = Stopwatch.GetTimestamp();
            err = await ctx.RunAsync().ConfigureAwait(true);
            counters.Latency.ReportLatency(start);
        }
        catch (Exception ex)
        {
            // 打日志
            ctx.L!.Warn(
                Field.Int64("error_code"u8, 65535),
                Field.String("message"u8, ex.Message),
                Field.String("stack_trace"u8, ex.StackTrace ?? "")
            );
            // 数据上报
            Interlocked.Increment(ref counters.ExceptionsTotal);
            context.Response.StatusCode = 500;
            return (null, Error.WithLoc(65535, "exception"));
        }

** 为什么内层的 try-catch 抓不住异常,而外层的又抓到了 **

我的 AOT 编译参数如下:

# 保留 NativeAOT 调试元数据,方便分析 k8s 中的 core dump。
AOT_CORE_DUMP_FLAGS = \
	-p:DebugSymbols=true \
	-p:DebugType=portable \
	-p:DebuggerSupport=true \
	-p:StackTraceSupport=true \
	-p:IlcGenerateMapFile=true

RID = linux-x64
TFM = net10.0

PRJ=csharp_backend
BUILD_DIR=../../build/Release/linux/amd64/
AOT_OBJ_DIR=./obj/Release/$(TFM)/$(RID)
AOT_NATIVE_OBJ_DIR=$(AOT_OBJ_DIR)/native

build_server:
	cd server/csharp_backend && \
	dotnet restore $(PRJ).csproj -r $(RID) && \
	dotnet clean $(PRJ).csproj -r $(RID) -c Release && \
	rm -fdr $(BUILD_DIR)/* && \
	dotnet publish $(PRJ).csproj \
	  -r $(RID) \
	  -p:DefineConstants=UNIX \
	  -p:AllowUnsafeBlocks=true \
	  -p:PublishAot=true \
	  -p:StripSymbols=false \
	  -p:StaticLinkedRuntime=true \
	  -p:StaticExecutable=true \
	  -p:PositionIndependentExecutable=false \
	  -p:InvariantGlobalization=true \
	  -p:OptimizationPreference=Size \
	  -p:CppCompilerAndLinker=clang \
	  -p:EventSourceSupport=true \
	  -p:EmbedAllSources=true \
	  $(AOT_CORE_DUMP_FLAGS) \
	  --self-contained true \
	  -c Release -o $(BUILD_DIR) && \
	cp -f $(AOT_NATIVE_OBJ_DIR)/$(PRJ).map.xml $(BUILD_DIR) && \
	cp -f $(AOT_OBJ_DIR)/$(PRJ).pdb $(BUILD_DIR) && \
	cp csharp_backend.yaml $(BUILD_DIR)

已经过去两星期了,一个简单的 mysql 连接池,仍然还是会在 AOT 编译模式下发生 coredump 或者抛异常。

文章摘自:https://www.cnblogs.com/ahfuzhang/p/21920545