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


我觉得我都写了 20 多年程序了,一个简单的 API 服务器不应该出现什么难以解决的问题。
然而,这个 C# 程序莫名奇妙崩溃的问题已经一周多了,仍然在继续……

挖坑 1:出现全局未捕获的异常时,退出进程

我基于 Kestrel 写了一个自己的后端框架:https://github.com/ahfuzhang/QiWa
一开始:我担心程序中存在未捕获的异常,于是我实现了一个捕获全局的线程异常的功能:
see: 《C#: 如何从全局捕获所有线程的异常》 https://www.cnblogs.com/ahfuzhang/p/19563733

核心代码是:

        AppDomain.CurrentDomain.UnhandledException += (_, eventArgs) =>
        {
            PrintUnhandledException("AppDomain.CurrentDomain.UnhandledException", eventArgs.ExceptionObject as Exception);
        };

        TaskScheduler.UnobservedTaskException += (_, eventArgs) =>
        {
            PrintUnhandledException("TaskScheduler.UnobservedTaskException", eventArgs.Exception);
        };

因为我担心未捕获的异常会导致隐含的问题,所以未捕获的异常一律退出:

    private static void PrintUnhandledException(string source, Exception? exception)
    {
        // 保证只输出一次,防止多线程重入
        if (Interlocked.Exchange(ref _hasPrinted, 1) == 1)
        {
            return;
        }
        // todo: 在这里输出日志

        const int exitCode = 99;
        Environment.Exit(exitCode);
    }

挖坑2: 主动 push metric 来快速采集性能数据,并采用 fire and forgot 的 Task 模式。

因为框架还在开发中,所以我希望框架自身可以快速 push metrics 数据到 VictoriaMetrics 中,便于后续实现秒级的性能监控。
做法就是自己读取 OpenTelemetry 的 metrics 对象,然后拼装为 prometheus 格式的文本,使用 http post 发送到 VictoriaMetrics 服务器端。

其中一段代码是这样的:

        _ = _client!.PostAsync("", content).ContinueWith(t =>
        {
            buf.Dispose();
            var statusCode = (int)t.Result.StatusCode;
            if (t.IsFaulted)
            {

                ThreadLocalLogger.Current.Warn(
                    Field.Utf8String("_msg"u8, "post metrics fail"u8),
                    Field.String("url"u8, _client!.BaseAddress!.ToString()),
                    Field.Int64("status_code"u8, statusCode)
                );
                return;
            }
            ThreadLocalLogger.Current.Debug(
                Field.Utf8String("_msg"u8, "post metrics success"u8),
                Field.Int64("status_code"u8, statusCode),
                Field.Int64("bytes"u8, bytes)
            );
        });

上面的代码想得很美好,一切皆异步,fire and forgot!
但是,当某次 http 访问失败时,就会抛出异常。特别是,测试环境机器资源紧张,特别不稳定。
于是,上面的代码就会抛出未捕获的异常。

教训

  • 全局捕获 Unhandled Exception,是否因为这样的异常退出进程,这里应该做成一个明确的配置。
    • 正式环境中,通过仅打日志而不退出进程来提升稳定性。
  • 牢记: C# 不是 golang, C# 不是golang.
    • 只要调用了 System 下面的库,就要明确的看文档了解这个库到底会抛出什么异常。然后老老实实的用 try-catch 包裹起来。
    • 一点懒都不能偷
  • 一个容易出问题的测试环境是好事
    • 我之前在本地调试了两周,一切都很完美;好多问题部署到测试环境才暴露出来。
    • 希望未来有一种混沌工程的容器,程序部署到里面就会自动模拟各种环境不稳定问题。