把 function 當作參數,我們可以作更多的想像跟操作。而且是在編譯的階段就可以決定驗證,而不用等到 runtime 注入,例如 spring boot。甚至可以讓我們的 function 更有還原能力(Resilience)
今天就來談談 Arrow KT 的 Resilience 模組,是在 Cloud Native 時代很重要的 pattern
當遇到某些不利的情況時,我們常常需要對 function 進行重試或重複。通常,重試或重複並不是立即進行的,而是基於某種策略進行的。例如,從網路請求中提取內容時,如果失敗,我們可能希望使用指數後退演算法(exponential backoff algorithm)重試,最長15秒或5次嘗試,以先到者為主。
Schedule 允許你定義和組合強大而簡單的策略。使用 Schedule 有兩個步驟
suspend fun download(url: Url): ByteArray =
Schedule.recurs(5).retry{
url.readBytes()
}
同理我們也可利用 Schedule 作到 repeat
最簡單的策略是重複十次。如果我們調用 repeat,則相同的操作將被執行十次,如果我們調用 retry,則最多嘗試十次直到成功。
fun <A> recurTenTimes() = Schedule.recurs<A>(10)
指數後退演算法(exponential backoff algorithm)是一種標準的重試與外部服務通信的操作演算法,例如網絡請求。大致上,這表示著嘗試之間的延遲按給定因子增加。
@ExperimentalTime
val exponential = Schedule.exponential<Unit>(250.milliseconds)
當服務過載時,額外的互動可能只會使其過載狀態惡化。當與如 Schedule 這樣的重試機制結合時,可能變成滾雪球的放大。有時,在高峰流量期間,僅使用 backoff 重試策略可能不夠。為了防止這些過載的資源過度載入,電路斷路器(Circuit Breaker)通過快速失敗來保護該服務。這有助於我們實現穩定性並防止分散式系統中的滾雪球的錯誤放大。
這是電路斷路器開始的狀態
在此狀態下正常進行請求:
當出現異常時,它將增加失敗計數器
當失敗計數器達到給定的 maxFailures 閾值時,斷路器移至打開狀態。
成功的請求將將失敗計數器重置為零
在此狀態下,電路斷路器會快速短路/失敗所有請求。
如果在配置的 resetTimeout 之後發出請求,斷路器將移至半打開狀態,允許一個請求作為測試通過。
當允許一個請求作為測試請求通過時,電路斷路器處於此狀態。
在測試請求仍在運行時所做的所有其他請求都將短路/快速失敗。
如果測試請求成功,電路斷路器將跳回關閉,並將resetTimeout和failures計數也重置為初始值。
如果測試請求失敗,電路斷路器將返回打開,並將resetTimeout乘以exponentialBackoffFactor,直至設置的maxResetTimeout
如此一來,我們就可以利用 Circuit Breaker 來保護我們的服務
@ExperimentalTime
suspend fun main(): Unit {
suspend fun apiCall(): Unit {
println("apiCall . . .")
throw RuntimeException("Overloaded service")
}
val circuitBreaker = CircuitBreaker(
openingStrategy = OpeningStrategy.Count(2),
resetTimeout = 2.seconds,
exponentialBackoffFactor = 1.2,
maxResetTimeout = 60.seconds,
)
suspend fun <A> resilient(schedule: Schedule<Throwable, *>, f: suspend () -> A): A =
schedule.retry { circuitBreaker.protectOrThrow(f) }
// simulate getting overloaded
Either.catch {
resilient(Schedule.recurs(5), ::apiCall)
}.let { println("recurs(5) apiCall twice and 4x short-circuit result from CircuitBreaker: $it") }
// simulate reset timeout
delay(2000)
println("CircuitBreaker ready to half-open")
// retry once,
// and when the CircuitBreaker opens after 2 failures
// retry with exponential back-off with same time as CircuitBreaker's resetTimeout
val fiveTimesWithBackOff = Schedule.recurs<Throwable>(1) andThen
Schedule.exponential(2.seconds) and Schedule.recurs(5)
Either.catch {
resilient(fiveTimesWithBackOff, ::apiCall)
}.let { println("exponential(2.seconds) and recurs(5) always retries with actual apiCall: $it") }
}
在 JVM 的世界裡,有 Resillane4j 這樣的函式庫提供類似的功能。但 Arrow KT 的能夠更 Kotlin 而且是能跟著 KMM