動(dòng)的容錯(cuò)重試機(jī)制與自定義策略實(shí)戰(zhàn))
后端Web框架微服務(wù)RPC框架異步編程【免費(fèi)下載鏈接】hyperf A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.項(xiàng)目地址https://gitcode.com/hyperf/hyperf點(diǎn)擊查看免費(fèi)下載導(dǎo)讀在分布式系統(tǒng)與微服務(wù)架構(gòu)中網(wǎng)絡(luò)通信本質(zhì)上是不穩(wěn)定的任何一個(gè)遠(yuǎn)端調(diào)用都可能因超時(shí)、抖動(dòng)或下游故障而失敗。Hyperf 的hyperf/retry組件提供了一套基于注解Annotation與策略Policy組合的可插拔重試機(jī)制覆蓋重試判定、重試間隔、結(jié)果處理、熔斷與預(yù)算控制等完整環(huán)節(jié)。讀完本文你將掌握#[Retry]注解的完整配置項(xiàng)、如何按業(yè)務(wù)場(chǎng)景定制屬于自己的重試注解以及如何使用鏈?zhǔn)?API 在普通 PHP 代碼中嵌入重試邏輯從而在不引發(fā)雪崩的前提下優(yōu)雅地提升系統(tǒng)容錯(cuò)能力。為什么需要「克制」的重試重試不是簡(jiǎn)單地把失敗的請(qǐng)求再打一遍無(wú)腦重試反而會(huì)放大故障。文檔中明確指出幾個(gè)關(guān)鍵風(fēng)險(xiǎn)放大系統(tǒng)負(fù)載當(dāng)通信出現(xiàn)問(wèn)題時(shí)如果每個(gè)請(qǐng)求都重試一次等價(jià)于系統(tǒng) IO 負(fù)載提升 100%極易觸發(fā)雪崩avalanche。重試無(wú)意義如果錯(cuò)誤本身無(wú)法通過(guò)重試解決重試只是在浪費(fèi)資源。破壞一致性如果被重試的接口不具備冪等性重復(fù)執(zhí)行可能造成數(shù)據(jù)不一致等問(wèn)題。因此一個(gè)合格的重試組件必須同時(shí)回答三個(gè)問(wèn)題什么情況下該重試、重試多少次、兩次重試之間間隔多久并且需要提供預(yù)算Budget與熔斷Circuit Breaker等保護(hù)機(jī)制防止重試本身成為新的故障源。這正是hyperf/retry的設(shè)計(jì)出發(fā)點(diǎn)——它通過(guò)組合多個(gè)職責(zé)單一的重試策略來(lái)覆蓋上述所有維度。安裝在 Hyperf 項(xiàng)目中通過(guò) Composer 安裝composer require hyperf/retry安裝完成后組件會(huì)通過(guò) ConfigProvider.php 自動(dòng)完成注解掃描與 AOP 切面Aspect注冊(cè)無(wú)需額外手動(dòng)配置即可使用。Hello World一行注解開啟重試在需要重試的方法上添加#[Retry]注解即可/** * 發(fā)生異常時(shí)重試該方法 */ #[Retry] public function foo() { // 發(fā)起遠(yuǎn)程調(diào)用 }默認(rèn)的Retry策略組合已經(jīng)能夠滿足大多數(shù)日常重試需求并且由于內(nèi)置了預(yù)算控制BudgetRetryPolicy不會(huì)因?yàn)檫^(guò)度重試而引發(fā)雪崩。從源碼看#[Retry]是一個(gè)#[Attribute(Attribute::TARGET_METHOD)]屬性注解Retry.php其實(shí)際生效依賴 Hyperf 的 AOP 機(jī)制切面 RetryAnnotationAspect.php 監(jiān)聽(tīng)所有繼承自AbstractRetry的注解在方法執(zhí)行時(shí)根據(jù)注解配置構(gòu)造策略并驅(qū)動(dòng)重試循環(huán)。深度定制創(chuàng)建你自己的重試注解組件通過(guò)組合多個(gè)重試策略實(shí)現(xiàn)可插拔性。每個(gè)策略專注于重試流程的一個(gè)方面——重試判定、重試間隔、結(jié)果處理等。通過(guò)調(diào)整注解中使用的策略列表你可以為任何場(chǎng)景定制重試行為。官方強(qiáng)烈建議根據(jù)具體業(yè)務(wù)需求構(gòu)建自己的別名注解alias annotation。下面以「最大嘗試次數(shù)為 3 次」為例演示如何創(chuàng)建新注解。注默認(rèn)的Retry注解本身就能通過(guò)#[Retry(maxAttempts3)]控制最大重試次數(shù)此處僅出于演示目的假設(shè)它不存在。第一步繼承 AbstractRetry創(chuàng)建一個(gè)新的注解類并繼承\(zhòng)Hyperf\Retry\Annotation\AbstractRetry?php declare(strict_types1); namespace App\Annotation; use Doctrine\Common\Annotations\Annotation\Target; #[Attribute(Attribute::TARGET_METHOD)] class MyRetry extends \Hyperf\Retry\Annotation\AbstractRetry { }從源碼結(jié)構(gòu)看AbstractRetry.php 本身繼承自Hyperf\Di\Annotation\AbstractAnnotation并實(shí)現(xiàn)了collectMethod()將注解元數(shù)據(jù)收集到AnnotationCollector中——這是 Hyperf 注解生效的基礎(chǔ)。其$policies屬性默認(rèn)是空數(shù)組因此你的自定義注解需要自己聲明策略組合。第二步用 MaxAttemptsRetryPolicy 限制次數(shù)按需覆寫$policies屬性。要限制重試次數(shù)需要加入MaxAttemptsRetryPolicy它需要一個(gè)參數(shù)——最大嘗試次數(shù)$maxAttempts?php declare(strict_types1); namespace App\Annotation; use Doctrine\Common\Annotations\Annotation\Target; #[Attribute(Attribute::TARGET_METHOD)] class MyRetry extends \Hyperf\Retry\Annotation\AbstractRetry { public $policies [ MaxAttemptsRetryPolicy::class, ]; public $maxAttempts 3; }此時(shí)#[MyRetry]會(huì)讓任意方法最多循環(huán)執(zhí)行 3 次。第三步用 ClassifierRetryPolicy 控制重試對(duì)象還需要加入ClassifierRetryPolicy來(lái)指定哪些錯(cuò)誤可以被重試。加入后默認(rèn)它只會(huì)在拋出Throwable時(shí)進(jìn)行重試?php declare(strict_types1); namespace App\Annotation; use Doctrine\Common\Annotations\Annotation\Target; #[Attribute(Attribute::TARGET_METHOD)] class MyRetry extends \Hyperf\Retry\Annotation\AbstractRetry { public $policies [ MaxAttemptsRetryPolicy::class, ClassifierRetryPolicy::class, ]; public $maxAttempts 3; }第四步組合更多策略定制重試細(xì)節(jié)你可以持續(xù)打磨這個(gè)注解直到滿足你的定制需求。例如只重試自定義的TimeoutException并使用可變間隔——每次重試至少休眠 100 毫秒?php declare(strict_types1); namespace App\Annotation; use Doctrine\Common\Annotations\Annotation\Target; #[Attribute(Attribute::TARGET_METHOD)] class MyRetry extends \Hyperf\Retry\Annotation\Retry { public $policies [ MaxAttemptsRetryPolicy::class, ClassifierRetryPolicy::class, SleepRetryPolicy::class, ]; public $maxAttempts 3; public $base 100; public $strategy \Hyperf\Retry\BackoffStrategy::class; public $retryThrowables [\App\Exception\TimeoutException::class]; }只要該文件被 Hyperf 掃描到你就可以在方法內(nèi)使用#[MyRetry]注解來(lái)重試超時(shí)錯(cuò)誤了。需要注意$policies是「堆疊的中間件」式的策略數(shù)組。切面 RetryAnnotationAspect.php 會(huì)遍歷注解的policies通過(guò)make($policy, $annotation-toArray())將注解上的其他屬性如$maxAttempts、$base等注入到每個(gè)策略構(gòu)造器中再組裝成 HybridRetryPolicy。因此注解屬性名需要與各策略構(gòu)造器的參數(shù)名保持一致才能正確注入。默認(rèn)配置全覽#[Retry]注解的完整默認(rèn)屬性如下/** * 重試策略數(shù)組可將其視為堆疊的中間件。 * var string[] */ public $policies [ FallbackRetryPolicy::class, ClassifierRetryPolicy::class, BudgetRetryPolicy::class, MaxAttemptsRetryPolicy::class, SleepRetryPolicy::class, ]; /** * 重試間隔算法。 */ public string $sleepStrategyClass SleepStrategyInterface::class; /** * 最大嘗試次數(shù)。 */ public int $maxAttempts 10; /** * Retry Budget。 * ttl: token 有效期秒。 * minRetriesPerSec: retry token 的基礎(chǔ)生成速率。 * percentCanRetry: 以請(qǐng)求量的這個(gè)比例生成新 token。 * * var array|RetryBudgetInterface */ public $retryBudget [ ttl 10, minRetriesPerSec 1, percentCanRetry 0.2, ]; /** * 每次嘗試的基礎(chǔ)時(shí)間間隔毫秒。對(duì)于 backoff 策略它是第一次嘗試的間隔 * 對(duì)于 flat 策略它是每次嘗試的間隔。 */ public int $base 0; /** * 配置一個(gè) Predicate用于評(píng)估某個(gè)異常是否應(yīng)該重試。 * 若該異常應(yīng)該重試Predicate 必須返回 true否則返回 false。 * * var callable|string */ public $retryOnThrowablePredicate ; /** * 配置一個(gè) Predicate用于評(píng)估某個(gè)結(jié)果是否應(yīng)該重試。 * 若該結(jié)果應(yīng)該重試Predicate 必須返回 true否則返回 false。 * * var callable|string */ public $retryOnResultPredicate ; /** * 配置被記錄為失敗、因而需要重試的 Throwable 類列表。 * 任何匹配或繼承自列表中某個(gè)類的 Throwable 都會(huì)被重試 * 除非被 ignoreThrowables 忽略。忽略的優(yōu)先級(jí)高于重試。 * * var arraystring|\Throwable */ public $retryThrowables [\Throwable::class]; /** * 配置被忽略、因而不重試的錯(cuò)誤類列表。 * 任何匹配或繼承自列表中某個(gè)類的異常都不會(huì)被重試 * 即使它被標(biāo)記在 retryThrowables 中。 * * var arraystring|\Throwable */ public $ignoreThrowables []; /** * 所有嘗試都耗盡時(shí)的 fallback 回調(diào)。 * * var callable|string */ public $fallback ;這些屬性與 Retry.php 中構(gòu)造函數(shù)參數(shù)一一對(duì)應(yīng)。需要特別說(shuō)明的一點(diǎn)構(gòu)造時(shí)$retryBudget數(shù)組會(huì)被make(RetryBudget::class, $this-retryBudget)實(shí)例化為真正的 RetryBudget 對(duì)象因此直接傳數(shù)組即可組件內(nèi)部會(huì)自動(dòng)完成裝配??蛇x策略詳解MaxAttemptsRetryPolicy最大嘗試次數(shù)參數(shù)類型說(shuō)明maxAttemptsint最大嘗試次數(shù)ClassifierRetryPolicy錯(cuò)誤分類器通過(guò)分類器判斷某個(gè)錯(cuò)誤是否可以重試。參數(shù)類型說(shuō)明ignoreThrowablesarray被忽略的Throwable類名。優(yōu)先級(jí)高于retryThrowablesretryThrowablesarray需要重試的Throwable類名。優(yōu)先級(jí)高于retryOnThrowablePredicateretryOnThrowablePredicatecallable通過(guò)一個(gè)函數(shù)判斷Throwable是否可重試??芍卦嚪祷?true否則返回 falseretryOnResultPredicatecallable通過(guò)一個(gè)函數(shù)判斷返回值是否可重試。可重試返回 true否則返回 false從 ClassifierRetryPolicy.php 的實(shí)現(xiàn)可以印證其判定邏輯先檢查ignoreThrowables是否命中命中即不重試再檢查retryThrowables是否命中命中即重試最后才回退到retryOnThrowablePredicate回調(diào)而結(jié)果判定retryOnResultPredicate只在沒(méi)有拋出異常且結(jié)果為非 null 時(shí)生效。注意其中對(duì) Throwable 的匹配使用的是instanceof語(yǔ)義因此子類異常同樣會(huì)被匹配。FallbackRetryPolicy兜底策略重試資源耗盡后執(zhí)行備選方法。參數(shù)類型說(shuō)明fallbackcallable兜底方法除了能被is_callable識(shí)別的代碼形式外fallback還可以填寫classmethod格式??蚣軙?huì)從Container中獲取對(duì)應(yīng)的class實(shí)例然后執(zhí)行其method方法——這非常適合注入依賴的 Service 方法作為兜底處理。SleepRetryPolicy休眠策略提供兩種重試間隔策略固定間隔FlatStrategy與可變間隔BackoffStrategy。參數(shù)類型說(shuō)明baseint基礎(chǔ)休眠時(shí)間毫秒strategystring任何實(shí)現(xiàn)了Hyperf\Retry\SleepStrategyInterface的類名如Hyperf\Retry\BackoffStrategy對(duì)應(yīng)的具體實(shí)現(xiàn)為 FlatStrategy.php 與 BackoffStrategy.php。文檔中「#[Retry]默認(rèn)的sleepStrategyClass是SleepStrategyInterface::class」其實(shí)是一種空實(shí)現(xiàn)表示默認(rèn)不做額外休眠真正要啟用間隔控制時(shí)應(yīng)顯式指定FlatStrategy或BackoffStrategy。TimeoutRetryPolicy超時(shí)策略當(dāng)總執(zhí)行時(shí)間超過(guò)指定時(shí)間后退出重試會(huì)話。參數(shù)類型說(shuō)明timeoutfloat超時(shí)時(shí)間秒CircuitBreakerRetryPolicy熔斷策略當(dāng)重試失敗并退出重試會(huì)話后在一段時(shí)間內(nèi)直接被標(biāo)記為熔斷狀態(tài)不再進(jìn)行任何嘗試。參數(shù)類型說(shuō)明circuitBreakerState.resetTimeoutfloat恢復(fù)所需時(shí)間秒熔斷狀態(tài)由 CircuitBreakerState.php 維護(hù)。倉(cāng)庫(kù)中還提供了獨(dú)立的#[CircuitBreaker]注解CircuitBreaker.php并有對(duì)應(yīng)的測(cè)試 CircuitBreakerAnotationAspectTest.php 與 CircuitBreakerStateTest.php 驗(yàn)證其行為。BudgetRetryPolicy預(yù)算策略每個(gè)#[Retry]注解會(huì)生成一個(gè)對(duì)應(yīng)的令牌桶token bucket。每次調(diào)用被注解的方法時(shí)會(huì)向桶中放入一個(gè)帶有過(guò)期時(shí)間ttl的 token。當(dāng)發(fā)生可重試的錯(cuò)誤時(shí)需要消耗一定數(shù)量由percentCanRetry決定的 token 才會(huì)真正重試若 token 不足則不再重試錯(cuò)誤繼續(xù)向下傳遞。例如percentCanRetry0.2時(shí)每次重試消耗 5 個(gè) token。這樣一來(lái)當(dāng)某個(gè)對(duì)端崩潰時(shí)最多只會(huì)帶來(lái) 20% 的額外重試消耗對(duì)大多數(shù)系統(tǒng)而言是可接受的。參數(shù)類型說(shuō)明retryBudget.ttlinttoken 過(guò)期時(shí)間秒retryBudget.minRetriesPerSecint每秒保證的最小重試次數(shù)retryBudget.percentCanRetryfloat重試次數(shù)不超過(guò)總請(qǐng)求量的比例注意retry 組件的 token bucket不會(huì)在多個(gè) worker 之間共享因此實(shí)際的重試總量需要乘以 worker 數(shù)量來(lái)估算。RetryBudgetTest.php 中包含了預(yù)算消耗與補(bǔ)充邏輯的單元測(cè)試可供參考。內(nèi)置別名注解由于重試注解的配置相對(duì)復(fù)雜組件內(nèi)置了幾個(gè)預(yù)設(shè)別名注解方便快速書寫#[RetryThrowable]只重試Throwable與默認(rèn)的#[Retry]等價(jià)。#[RetryFalsy]只重試「返回值松散等于 false$result false」的錯(cuò)誤不重試異常。其實(shí)現(xiàn)見(jiàn) RetryFalsy.php內(nèi)部通過(guò)靜態(tài)方法isFalsy()作為retryOnResultPredicate完成判定。#[BackoffRetryThrowable]#[RetryThrowable]的可變間隔版本重試間隔至少 100 毫秒。#[BackoffRetryFalsy]#[RetryFalsy]的可變間隔版本重試間隔至少 100 毫秒。以 BackoffRetryThrowable.php 為例它繼承自RetryThrowable默認(rèn)設(shè)置base 100與sleepStrategyClass BackoffStrategy::class——這正是「間隔至少 100 毫秒、指數(shù)退避」的由來(lái)。測(cè)試文件 RetryFalsyTest.php 與 RetryTest.php 覆蓋了這些別名注解的核心行為。Fluent 鏈?zhǔn)秸{(diào)用除了注解方式你還可以通過(guò)普通 PHP 函數(shù)調(diào)用方式使用該組件?php $result \Hyperf\Retry\Retry::with( new \Hyperf\Retry\Policy\ClassifierRetryPolicy(), // 默認(rèn)重試所有 Throwable new \Hyperf\Retry\Policy\MaxAttemptsRetryPolicy(5) // 最多重試 5 次 )-call(function(){ if (rand(1, 100) 20){ return true; } throw new Exception; });為了提升可讀性還可以使用如下 fluent 語(yǔ)法?php $result \Hyperf\Retry\Retry::whenReturns(false) // 返回 false 時(shí)重試 -max(3) // 最多 3 次 -inSeconds(5) // 最多 5 秒 -sleep(1) // 間隔 1 毫秒 -fallback(function(){return true;}) // 兜底函數(shù) -call(function(){ if (rand(1, 100) 20){ return true; } return false; });從實(shí)現(xiàn)上看Retry.php 通過(guò)__callStatic將靜態(tài)調(diào)用委托給由容器創(chuàng)建make(FluentRetry::class)的 FluentRetry.php 實(shí)例。FluentRetry內(nèi)部把每一步鏈?zhǔn)椒椒ǚg成對(duì)應(yīng)的策略對(duì)象例如whenReturns(false)→ 構(gòu)造ClassifierRetryPolicy([], [], null, fn ($r) $r $when)max(3)→MaxAttemptsRetryPolicy(3)inSeconds(5)→TimeoutRetryPolicy(5)sleep(1)→SleepRetryPolicy(1, FlatStrategy::class)固定間隔backoff(100)→SleepRetryPolicy(100, BackoffStrategy::class)可變間隔。最終所有策略被組裝為HybridRetryPolicy并以與注解切面相同的方式驅(qū)動(dòng)canRetry → attempt → beforeRetry → end的重試循環(huán)FluentRetry.php。若未指定任何策略就調(diào)用call()會(huì)拋出BadMethodCallException提示至少先聲明一個(gè)策略。重試循環(huán)的底層機(jī)制無(wú)論是注解方式還是鏈?zhǔn)椒绞街卦嚨暮诵难h(huán)都在策略層面統(tǒng)一實(shí)現(xiàn)注解路徑見(jiàn) RetryAnnotationAspect.phpstart()初始化RetryContext各策略按順序在此注入初始狀態(tài)如預(yù)算桶、熔斷狀態(tài)、開始時(shí)間等。canRetry()判定當(dāng)前是否還能繼續(xù)嘗試。HybridRetryPolicy 會(huì)同時(shí)詢問(wèn)所有策略只有全部返回 true 才繼續(xù)重試——任何一個(gè)策略如超時(shí)、預(yù)算耗盡、次數(shù)用盡說(shuō)「不」重試即終止。執(zhí)行目標(biāo)方法注解方式為$proceedingJoinPoint-process()捕獲結(jié)果或異常寫入RetryContext。canRetry()再次判定若可重試則調(diào)用beforeRetry()各策略在此執(zhí)行休眠、扣減預(yù)算等動(dòng)作然后回到第 3 步。循環(huán)退出后調(diào)用end()若有遺留異常則重新拋出否則返回最后一次執(zhí)行結(jié)果。RetryContext是貫穿整個(gè)重試會(huì)話的載體承載lastResult、lastThrowable、proceedingJoinPoint等關(guān)鍵狀態(tài)RetryContext.php。理解了這一循環(huán)就能明白為什么policies數(shù)組的順序會(huì)影響行為例如SleepRetryPolicy是否在MaxAttemptsRetryPolicy之前決定了「次數(shù)耗盡」與「最后一段休眠」之間的先后關(guān)系實(shí)踐中通常保持默認(rèn)順序即可??偨Y(jié)hyperf/retry以「策略組合 注解/鏈?zhǔn)诫p入口」的方式把重試這一高風(fēng)險(xiǎn)的容錯(cuò)手段變得可控、可配、可插拔通過(guò)#[Retry]一行注解即可獲得帶預(yù)算保護(hù)的安全重試通過(guò)繼承AbstractRetry組合MaxAttemptsRetryPolicy、ClassifierRetryPolicy、SleepRetryPolicy等策略可以精確刻畫「重試什么、重試幾次、間隔多久」BudgetRetryPolicy與CircuitBreakerRetryPolicy提供了雪崩防護(hù)與熔斷兜底別名注解與Retry::whenReturns(...)-max(...)-call(...)鏈?zhǔn)?API 則讓高頻場(chǎng)景的書寫成本降到最低。在把任何遠(yuǎn)程調(diào)用、數(shù)據(jù)庫(kù)寫入或第三方接口調(diào)用接入重試之前請(qǐng)先確認(rèn)接口的冪等性并結(jié)合業(yè)務(wù)對(duì)延遲與成功率的容忍度選擇合適的策略組合與預(yù)算參數(shù)。贊分享后端Web框架微服務(wù)RPC框架異步編程【免費(fèi)下載鏈接】hyperf A coroutine framework that focuses on hyperspeed and flexibility. Building microservice or middleware with ease.項(xiàng)目地址https://gitcode.com/hyperf/hyperf點(diǎn)擊查看免費(fèi)下載相關(guān)推薦Hyperf Retry 組件實(shí)戰(zhàn)注解式重試、策略組合與熔斷防雪崩指南Hyperf Retry 組件實(shí)戰(zhàn)注解式重試、策略組合與熔斷防雪崩指南 導(dǎo)讀 Hyperf 是高性能的協(xié)程框架而網(wǎng)絡(luò)通信天然不穩(wěn)定微服務(wù)場(chǎng)景下調(diào)用失敗在所后端微服務(wù)Hyperf Retry 組件完全指南基于注解與可插拔策略的高可用重試機(jī)制Hyperf Retry 組件完全指南基于注解與可插拔策略的高可用重試機(jī)制 重試是分布式系統(tǒng)中抵御網(wǎng)絡(luò)抖動(dòng)與瞬時(shí)故障的第一道防線但盲目重試反而會(huì)放大系統(tǒng)負(fù)載后端Web框架微服務(wù)RPC框架異步編程Hyperf 熔斷器Circuit Breaker組件實(shí)戰(zhàn)指南注解驅(qū)動(dòng)、狀態(tài)機(jī)與降級(jí)策略Hyperf 熔斷器Circuit Breaker組件實(shí)戰(zhàn)指南注解驅(qū)動(dòng)、狀態(tài)機(jī)與降級(jí)策略 導(dǎo)讀 在微服務(wù)與分布式系統(tǒng)中一個(gè)基礎(chǔ)服務(wù)不可用往往會(huì)導(dǎo)致調(diào)用鏈后端Web框架微服務(wù)RPC框架異步編程上一篇StreamCap終極指南免費(fèi)開源的多平臺(tái)直播錄制神器下一篇終極解決指南PCL2啟動(dòng)器游戲啟動(dòng)失敗的3個(gè)核心原因與高效修復(fù)方案創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考