大型Rust项目的错误处理——深入探讨GreptimeDB的实践

💡 原文英文,约2700词,阅读约需10分钟。
📝

内容提要

本文讨论了Rust错误处理的实践,并分享了GreptimeDB中可能的未来工作。主题包括:(1)如何构建更便宜但更准确的错误堆栈以替代系统回溯;(2)如何在大型项目中组织错误;(3)如何以不同的方案将错误打印到日志和最终用户。GreptimeDB中的错误可能如下所示:0:Foo错误,位于src/common/catalog/src/error.rs:80:10;1:Bar错误,位于src/common/function/src/error.rs:90:10;2:根本原因,无效的表名,位于src/common/catalog/src/error.rs:100:10。Rust的错误处理以Result<T, E>枚举为中心,其中E通常(但不必要地)扩展std::error::Error。文章还介绍了如何在GreptimeDB这样的复杂系统中组织不同类型的错误,从错误的定义到如何记录错误或向最终用户呈现错误。

🔎

延伸解读

为何选择 snafu 而非 thiserror 或 anyhow

在 GreptimeDB 这类由多个子 crate 组成的大型项目中,每个 crate 需要独立的错误类型,同时又要能方便地组合。thiserror 和 anyhow 难以同时满足这一需求:thiserror 为每个错误类型实现 From,导致同一来源类型无法定义多个变体,上下文信息在类型层面被模糊;anyhow 则更适合二进制程序。snafu 结合了两者优点,既提供便捷的宏来定义错误类型,又支持上下文转换,因此成为 GreptimeDB 错误系统的基石。

虚拟用户堆栈如何替代系统回溯

系统回溯虽然能提供调用栈,但会消耗大量 CPU 和内存,且包含许多无关的标准库和运行时堆栈。在 GreptimeDB 中,启用 debuginfo 会使二进制体积增加超过 700MB。虚拟用户堆栈由用户代码定义和构建,只记录错误传播路径,每一层包含消息和代码位置,格式清晰。它还能在批处理等场景中延迟捕获,等到拥有更多上下文时再生成,从而提供更准确的错误信息。

面向最终用户的错误信息呈现策略

最终用户通常不关心代码行号和完整堆栈。GreptimeDB 从错误堆栈中提取关键部分:最外层的简要描述、最内层(叶子)错误的消息以及外部依赖的根因。最终格式为“KIND - REASON ([EXTERNAL CAUSE])”,例如“Failed to handle protocol - Failed to decode ... (serde_json(invalid character at position 1))”。这种方案既保留了用户能理解的上下文,又避免了技术细节的干扰。

❓

Q&A

Rust中的错误处理是如何工作的?

Rust的错误处理以Result<T, E>枚举为中心,E通常扩展std::error::Error。

GreptimeDB是如何组织错误的?

GreptimeDB通过定义自定义错误类型,并使用snafu库来构建错误系统,以便于定义和组合错误类型。

在GreptimeDB中,如何记录和呈现错误信息?

GreptimeDB采用虚拟用户堆栈来记录错误信息,提供清晰的错误传播路径,并以用户友好的格式呈现错误。

为什么系统回溯在错误处理上存在局限性?

系统回溯消耗大量CPU和内存,并包含许多无关信息,难以快速定位错误源。

GreptimeDB中错误消息的最终格式是什么?

最终的错误消息格式为:KIND - REASON ([EXTERNAL CAUSE]),以便于用户理解。

如何在Rust项目中定义自定义错误?

开发者通常会为项目定义自定义错误,以表达特定的应用错误信息,通常使用thiserror或anyhow库。

🏷️

标签

➡️

继续阅读