资讯动态

Pingora 错误处理指南:pingora-error 的错误建模、传播与重试机制

发布时间:2026/9/11 2:11:11 来源:尧图企业网站定制
Pingora 错误处理指南pingora-error 的错误建模、传播与重试机制【免费下载链接】pingoraA library for building fast, reliable and evolvable network services.项目地址: https://gitcode.com/GitHub_Trending/pi/pingora在 Pingora 这类面向高并发网络服务的框架中错误处理的质量直接决定服务的可观测性与可用性连接被对端断开、读超时、无效 HTTP 头……这些网络场景下的异常需要被统一建模、逐层包装、完整记录并在可恢复时驱动上游重试。Pingora 为此提供了独立的pingora-errorcrate它导出一个贯穿整个框架的自定义Result类型与一套简洁的错误构建 API。本文基于 errors.md 官方指南结合 pingora-error 源码 与 pingora-proxy 的重试/错误处理实现系统讲解如何创建、包装、传播 Pingora 错误以及如何利用可重试错误实现上游故障转移。读完本文你将能在自己的 Pingora 服务中写出结构清晰、日志可读、支持重试的错误处理代码。从 pingora-error 开始统一的Result与ErrorPingora 的各个 cratepingora-proxy、pingora-core、pingora-cache等统一通过pingora-error导出的Result传递错误。该 crate 在 Cargo.toml 中描述为 Error types and error handling APIs for Pingora其核心定义在 lib.rs 中/// The boxed [Error], the desired way to pass [Error] pub type BError BoxError; /// Syntax sugar for std::ResultT, BError pub type ResultT, E BError StdResultT, E;也就是说Pingora 全框架通用的ResultT实际是std::result::ResultT, BoxError。BErrorboxed error是推荐的传递形态因为Error内部包含一条可以无限延伸的cause链必须通过堆上装箱才能保持Sized。Error本身是一个对任意错误类型的包装器lib.rs 定义pub struct Error { /// the type of error pub etype: ErrorType, /// the source of error: from upstream, downstream or internal pub esource: ErrorSource, /// if the error is retry-able pub retry: RetryType, /// chain to the cause of this error pub cause: OptionBoxdyn ErrorTrait Send Sync, /// an arbitrary string that explains the context when the error happens pub context: OptionImmutStr, }与标准库的dyn std::error::Error相比Pingora 的Error多携带了etype错误类型、esource错误来源和retry是否可重试三个维度这让错误不仅是什么还让框架知道来自哪里以及能否重试——后者正是pingora-proxy实现上游故障转移的依据。Error 的四个维度类型、来源、原因与上下文官方指南 errors.md 明确指出一个错误有type如ConnectionClosed、source如Upstream、Downstream、Internal以及可选的cause被包装的另一个错误和context用户提供的任意字符串细节。这四个维度在源码中一一对应Error的四个字段。ErrorType预定义错误类型与自定义扩展ErrorType枚举lib.rs#L102-L148按业务场景分成了几大类下面按类梳理类别变体连接类ConnectTimedout、ConnectRefused、ConnectNoRoute、ConnectProxyFailure、TLSWantX509Lookup、TLSHandshakeFailure、TLSHandshakeTimedout、InvalidCert、HandshakeError、ConnectError兜底、BindError、AcceptError、SocketError协议类InvalidHTTPHeader、H1Error兜底、H2Error兜底、H2Downgrade、InvalidH2已建连后的 IO 类ReadError、WriteError、ReadTimedout、WriteTimedout、ConnectionClosed应用类HTTPStatus(u16)直接映射为要返回给下游的 HTTP 状态码文件类FileOpenError、FileCreateError、FileReadError、FileWriteError其他InternalError、UnknownError兜底自定义Custom(static str)、CustomCode(static str, u16)当预置类型不够用时用户可以通过两个构造器扩展自定义类型// 自定义一个带名称的错误类型 pub const fn new(name: static str) - Self { ErrorType::Custom(name) } // 自定义一个带名称和错误码的类型 pub const fn new_code(name: static str, code: u16) - Self { ErrorType::CustomCode(name, code) }源码注释特别提醒Custom只接受static str如果需要运行时生成字符串更合适的做法是把它当作上下文context而非类型。ErrorSource错误来自哪里ErrorSourcelib.rs#L47-L57有四个取值Upstream由远端服务器引起Downstream由远端客户端引起Internal由内部逻辑引起Unset来源未知或待设置。Error还提供了一组运行时修改来源的辅助方法as_up()/as_down()/as_in()原地修改以及into_up()/into_down()/into_in()消费self并返回自身便于链式调用。来源信息在pingora-proxy的默认fail_to_proxy中直接决定了最终返回给下游的 HTTP 状态码下文详述因此务必选对。context 与 cause上下文与原因链context使用专门的ImmutStr类型immut_str.rs存储它内部是Static(static str)或Owned(Boxstr)二选一当传入字符串字面量时零分配传入String时才装箱兼顾性能与灵活性。cause则是一条指向底层错误的可选链。Error同时实现了Display和std::error::Errortrait其Display会调用内部的chain_display把整条来源 → 类型 → 上下文 → 原因链完整打印出来。创建错误的函数家族new / explain / because官方指南将创建错误的方式归纳为三类。这三类在源码中一一对应先看完整签名函数行为源码位置Error::new(e)仅指定类型来源置为Unsetlib.rs#L234-L236Error::new_up(e)/new_down(e)/new_in(e)指定类型 来源Upstream/Downstream/Internal无上下文无原因lib.rs#L294-L306Error::explain(e, context)新错误无直接原因但带更多上下文lib.rs#L281-L283Error::because(e, context, cause)包装一个导致错误的原因并附加上下文lib.rs#L256-L267Error::new_str(s)用Custom(s)静态字符串创建自定义错误lib.rs#L310-L312对应地还有一批直接返回ResultT的快捷函数err(e)/err_up(e)/err_down(e)/err_in(e)等价于Err(Error::new_*(e))以及e_explain/e_because。使用准则来自官方指南需要新建一个错误、没有直接原因、但想补充上下文时用Error::explain如果是对Result中的已有错误做替换可用explain_err需要把导致错误的原因包进一个新错误、并补充上下文时用Error::because如果是对Result中的已有错误做包装可用or_err。源码对because的使用还有一条补充建议lib.rs#L252-L254只有当新上下文无法从导致错误本身捕获时才使用because否则应直接透传原始错误?避免无意义地拉长原因链。此外还有两个实用的增强方法more_context(context)从自身派生出类型和来源不变、把自身作为 cause 的新错误等价于更简洁的because变体仅适用于Error类型because则适用于所有实现std::error::Error的类型set_cause/set_context对已存在的错误原地补上原因与上下文。实战示例校验 Host 头并向上游传播官方指南给出了一个非常典型的用法——在请求过滤阶段校验必需头缺失则生成错误并传播。我们将其拆解并补充完整上下文fn validate_req_header(req: RequestHeader) - Result() { // validate that the host header exists req.headers() .get(http::header::HOST) .ok_or_else(|| Error::explain(InvalidHTTPHeader, No host header detected)) } impl MyServer { pub async fn handle_request_filter( self, http_session: mut Session, ctx: mut CTX, ) - Resultbool { validate_req_header(session.req_header()?).or_err(HTTPStatus(400), Missing required headers)?; Ok(true) } }整个流程分两步validate_req_header中Option::ok_or_else在host头缺失时调用Error::explain(InvalidHTTPHeader, No host header detected)产生一个类型为InvalidHTTPHeader、带上下文说明的错误该错误在handle_request_filter里被or_err包装成新的HTTPStatus(400)错误context为 Missing required headers而原来的InvalidHTTPHeader错误自动成为新错误的cause。当这个错误沿Result向上传播、最终被pingora-proxy捕获时会进入fail_to_proxy阶段proxy_trait.rs#L620-L658。其默认实现会把错误翻译成对下游的响应错误情形返回给下游的状态码etype是HTTPStatus(code)直接用codeesource为Upstream502 Bad Gatewayesource为Downstream且etype是WriteError/ReadError/ConnectionClosed0连接已死无法写响应esource为Downstream的其他情况400 Bad Requestesource为Internal/Unset500 Internal Server Error所以示例中的HTTPStatus(400)错误最终会以400 Bad Request响应给下游同时被记录到错误日志——日志输出格式可以从 pingora-proxy/lib.rs 看到Fail to proxy: {error}, status: {code}, tries: {n}, retry: {bool}, {request_summary}需要注意的是原始导致错误也仍然可见。因为or_err只是把原始错误包进新错误而Error的Display实现会打印整条 cause 链。这一点在 pingora-error 单元测试 中有直接验证let e3 Error::new(ErrorType::InternalError); let e4 Error::because(ErrorType::HTTPStatus(400), test, e3); assert_eq!( format!({}, e4), HTTPStatus context: test cause: InternalError ); assert_eq!(e4.root_etype().as_str(), InternalError);Display输出 HTTPStatus context: test cause: InternalError 即完整呈现了包装关系而root_etype()/root_cause()则用于穿透整条链直接取出最底层的原因类型与原因对象如判断是否为连接被对端关闭。在 Result 上链式处理错误的便捷 trait除了Error自身的构建函数pingora-error还提供了一组基于 trait 的链式方法全部通过map_err实现见 lib.rs#L474-L585OrErrT, E针对ResultT, Eor_err(et, context)map_errbecause把任意错误包装成带新类型与静态上下文的 Pingora 错误or_err_with(et, || context)同or_err但接受闭包便于构造String型上下文explain_err(et, |e| context)map_errexplain替换而非包装原错误适用于原错误无法移出作用域的场景or_fail()把非 Pingora 的错误如str、标准库 IO 错误包装为InternalError后透传——just to surface errors that are not Error (where?cannot be used directly)也就是让那些不能直接?的错误也能纳入统一错误链。OkOrErrT针对OptionTor_err(et, context)ok_or(Error::explain(et, context))的简写or_err_with(et, || context)接受闭包构造上下文的版本。ContextT针对ResultT, BErrorerr_context(|| context)map_errmore_context保持原类型与来源不变、把自身作为 cause仅追加上下文。源码注释对or_fail的定位说得很清楚or_err/or_err_with仍被优先推荐因为它们让错误更可读、可追溯or_fail只是在不便构造新类型时兜底使用。可重试错误驱动上游故障转移的关键官方指南的 Retry 一节指出错误可以被标记为 retry-able。如果错误可重试pingora-proxy将允许重试该上游请求某些错误只在连接被复用reused connections时才允许重试——典型场景是远端已把我们要复用的连接关闭了此时重试是安全的。关于连接复用与失败连接不可复用的约定可参阅 连接池与复用指南。底层的 RetryType 状态机Error.retry字段的类型是RetryTypelib.rs#L60-L81pub enum RetryType { Decided(bool), // 重试与否已被明确决定 ReusedOnly, // 仅当错误来自复用连接时才重试 } impl RetryType { pub fn decide_reuse(mut self, reused: bool) { if matches!(self, RetryType::ReusedOnly) { *self RetryType::Decided(reused); } } pub fn retry(self) - bool { /* Decided(b) bReusedOnly 会 panic */ } }ReusedOnly是一个待定状态必须等框架拿到reused事实后通过decide_reuse落地。在pingora-proxy的error_while_proxy过滤器中proxy_trait.rs#L579-L592这个落地过程可见let mut e e.more_context(format!(Peer: {}, peer)); // only reused client connections where retry buffer is not truncated e.retry.decide_reuse(client_reused !session.as_ref().retry_buffer_truncated());即仅当客户端连接是复用的且重试缓冲未被截断时才允许这类错误重试。默认重试状态如何继承Error::createlib.rs#L203-L225定义了默认的重试继承规则创建时若提供了cause且该 cause 能下转型为BError则新错误继承 cause 的 retry 状态若没有 cause 或 cause 不是 Pingora 的Error则 retry 置为false不可重试。这与官方指南的表述完全一致默认情况下新建的Error要么继承其直接 cause 的重试状态要么未指定时被视为不可重试。要显式修改可用set_retry(bool)。pingora-proxy 的重试循环重试决策发生在 pingora-proxy/src/lib.rs 的上游代理主循环中let (reuse, e) self.proxy_to_upstream(mut session, mut ctx).await; match e { Some(error) { let retry error.retry(); // 若可重试先记一条 warn 日志然后继续循环重新选 peer、重发请求 if retry { /* warn log */ } proxy_error Some(error); if !retry { break; // 不可重试则立即终止循环 } } ... }也就是说错误被判定为可重试时请求不会立刻失败而是重新走一遍upstream_peer()→ 建立/复用连接 → 发送请求的流程只有不可重试或重试耗尽时才调用fail_to_proxy终结请求。重试过程中的每次失败都以warn级别记录最终失败才以error级别记录避免日志噪音见 lib.rs#L1041-L1056。用户控制重试行为有两个天然钩子fail_to_connectproxy_trait.rs#L602-L610在连接上游失败时被调用注释明确在此过滤器中用户可以通过给错误e打标记来决定该错误是否可重试若判定可重试upstream_peer()会被再次调用用户可借此把请求转到另一个可能可用的上游error_while_proxy在连接建立/复用之后发生错误时被调用默认实现即上面提到的more_contextdecide_reuse。完整示例带指数退避的重试仓库中的 backoff_retry.rs 示例 给出了可重试错误 退避策略的完整落地对应运行命令RUST_LOGINFO cargo run --example backoff_retry -- --conf examples/conf.yamlfn fail_to_connect( self, _session: mut Session, _peer: HttpPeer, ctx: mut Self::CTX, e: BoxError, ) - BoxError { ctx.retries 1; let mut retry_e e; retry_e.set_retry(true); // 标记为可重试 retry_e } async fn upstream_peer( self, _session: mut Session, ctx: mut Self::CTX, ) - ResultBoxHttpPeer { const MAX_SLEEP: Duration Duration::from_secs(10); if ctx.retries 0 { // 指数退避示例10^n 毫秒上限 10 秒 let sleep_ms std::cmp::min(Duration::from_millis(u64::pow(10, ctx.retries)), MAX_SLEEP); tokio::time::sleep(sleep_ms).await; } let mut peer HttpPeer::new((10.0.0.1, 80), false, .into()); peer.options.connection_timeout Some(Duration::from_millis(100)); Ok(Box::new(peer)) }核心思路在fail_to_connect里用set_retry(true)把所有连接类错误标记为可重试在upstream_peer里借助上下文CTX.retries计数实现 10ms、100ms、1s……上限 10s 的指数退避由于可重试错误会重新触发upstream_peer()退避逻辑得以在每次重试前执行。这也印证了错误处理与 连接池指南 中请求出错后连接不可复用之间的配合关系。附错误日志级别约定Pingora 认为断连、超时、非法输入是网络的常态应通过错误日志STDERR 或日志文件来记录。官方 error_log.md 指南 给出五个级别的使用建议与错误处理直接相关error错误导致请求无法被正确处理如要连接的上游服务器离线warning出错但系统已自行恢复如主 DNS 超时但备用 DNS 查询成功info服务启动 / 关闭等事件debug / trace内部细节且这两个级别在release构建中不会编译进去。pingora-proxy已内置了常见的错误日志接口如上面见到的 Fail to proxy 日志、重试 warn 日志普通代理错误无需用户手工记录suppress_error_log/suppress_proxy_warn_log钩子则允许用户按需抑制日志但源码注释提醒抑制重试 warn 日志会移除每次重试的唯一审计记录应辅以指标等其他可观测手段。小结Pingora 的错误处理以pingora-error的ResultT, BoxError为统一载体用etype类型、esource来源、retry可重试性、causecontext原因链与上下文四类信息完整刻画一次失败。日常使用只需记住三条主线新建错误Error::explain(e, context)无原因或new_in/new_up/new_down指定来源包装传播Error::because(e, context, cause)在Result上对应or_err/or_err_with日志中会完整打印整条 cause 链驱动重试通过set_retry(true)或让错误继承 cause 的重试状态配合fail_to_connect、upstream_peer等钩子实现上游故障转移连接复用场景下的重试则依赖ReusedOnly与decide_reuse的协作。理解这套机制后你可以让 Pingora 服务的每一次上游失败都有迹可循、可分类、可重试把错误从需要手动埋点排查的麻烦变成框架自动记录与恢复的常态。【免费下载链接】pingoraA library for building fast, reliable and evolvable network services.项目地址: https://gitcode.com/GitHub_Trending/pi/pingora创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价