resilience4j不夠用?自制分散式斷路器來幫忙 -Nicolas

banq發表於2020-04-30

當服務的多個例項可以呼叫指定的外部服務,在這些服務例項中都要定製斷路策略很浪費,比如呼叫外部服務一段時間後進行關閉處理邏輯等。他們可以統一共享呼叫同一個外部服務的統計資訊,這樣一個呼叫失敗以後,其他服務例項就不要再重試一遍,這是使用分散式斷路器的地方。

由於找不到現有解決方案,我們決定自行嘗試一下。簡而言之,我們從ratelimitj的啟發中快速構建了一個分散式斷路器。通話統計資訊是使用Redis共享的,它就像一個超級按鈕。我們計劃將其開源,但這將是另一篇文章的主題;-) 這裡分享思路。

1. 共享的統計資訊:指標,跟蹤和監視

我們對指標的需求非常簡單:對於每個佇列和每種命令,我們都希望跟蹤,並瞭解其中有多少成功了,或失敗和重試或最終移入隔離區。這將使我們能夠了解流量並在需要時調整引數。

鑑於我們在Java / Kotlin應用程式中使用的是Spring Boot,因此這裡沒有做任何決定:我們將照常使用Micrometer來發布帶有適當標籤的量規,然後在Datadog中遵循這些指標,這是一個(好)監控SAAS。

2. 視覺化

儘管有日誌,指標和警報是很明顯的,但我們希望有一種方法可以隨時視覺化計劃或隔離的任務及其嘗試次數和任何可能的錯誤,以及採取行動來執行這些任務(刪除它們) ,重新安排時間,等等)。

3.總體設計思路:

第一個設計決策是關於如何表示命令,執行請求和隔離的。以下是做出的主要決定和最終決定:

  • 命令是一塊程式碼,可以通過名稱找到,並且通過提供一個其(Java)的地圖的引數執行。由於我們使用的是Spring,因此命令是Spring元件,並且Spring上下文充當命令登錄檔。
  • 命令執行請求包括一個命令的名稱和引數,例如:(名稱= pushTransactionForInvoice,ARGS = {invoiceId,的transactionId})
  • 命令與“邏輯”佇列相關聯,每個佇列對應於一個外部服務。因此,該“邏輯”佇列是跟蹤呼叫並應用速率限制和斷路邏輯的單元。
  • 計劃後,命令執行請求將作為任務儲存在PostgreSQL表(佇列表)中,並具有以下核心詳細資訊:任務ID,計劃執行日期/時間,“邏輯”佇列名稱,命令名稱,命令引數,命令重量(限制wrt速率,請參閱“演算法”部分
  • 還儲存了與命令的執行相關的其他詳細資訊:任務狀態(PENDING或LOCKED),到目前為止的執行次數,最新的執行錯誤(如果有)。
  • 最後,儲存“ 下一個任務 s” 的列表,這些列表是僅在成功完成當前任務後才執行的命令。稍後對此有更多詳細資訊。
  • 命令的引數和要執行的下一個任務的列表儲存為JSONB,以說明它們的可變性。
  • 該隔離被表示為第二表,儲存幾乎快要任務,加上上次執行嘗試的時間相同的細節。

resilience4j不夠用?自制分散式斷路器來幫忙 -Nicolas

4.為指定服務配置新的命令佇列

//將請求定義為命令 放入佇列中
const val MY_QUEUE_NAME: String = "myServiceQueue"

@Configuration
class MyServiceQueueConfiguration {

    @Bean(MY_QUEUE_NAME)
    fun myServiceQueue(commandExecutionQueueFactory: CommandExecutionQueueFactory) =
        commandExecutionQueueFactory.createQueue(
                MY_QUEUE_NAME,

                // optionally redefine part or totality of the default policy
                DEFAULT_EXECUTION_POLICY.copy(
                        concurrency = 4,
                        delayBeforeConsideringTask = Duration.ofSeconds(5),
                        maxRetriesBeforeQuarantine = 10,

                        // optional, none by default
                        rateLimits = RateLimits(
                                2300 executionsOver Duration.ofMinutes(15),
                                4500 executionsOver Duration.ofMinutes(30),
                                8800 executionsOver Duration.ofHours(1)
                        ),

                        // optional, none by default
                        circuitBreaking = CircuitBreaking(
                                failureRateThreshold = 0.5,
                                windowDuration = Duration.ofMinutes(10),
                                // will tell that some exceptions are to be considered as provider failures
                                considerExceptionAsFailureIf = someExceptionPredicate()
                        )
                ),

                // optional, a probe that will be queried to know whether to pause task consumption
                // (may query a feature flag, a state defined via some UI, etc.)
                somePauseProbe()
        )
}

您可以在此處找到所有可用的佇列選項

將命令執行請求定義為一個簡單的物件,其中包含要執行的命令的名稱和一個(Java)引數對映。

5. 命令執行佇列的核心邏輯

class CommandQueue(...) {
    // ...
    
    override fun schedule(command: CommandSpecification) {
        // add task to queue, log details, emit metrics
        schedule(ScheduledTask(
                command,
                queueName,
                clock,
                // this is the important part for deduplication to work
                scheduledExecutionDate = executionPolicy.computeNextExecutionDate(clock, tries = 0)
        ), command.deduplicate)
    }

    // ...

    override fun processCommands(): Boolean {
        val circuitBreaker = circuitBreaker()
        if (circuitBreaker.isOpen()) {
            return false
        }

        val task = taskRepository.tryLockingTaskWithEarliestScheduleOlderThan(queueName, LocalDateTime.now(clock))
                ?: return false

        val command = commandRegistry.get(task.commandName)

        // each case: decides what to do with task, log details, emit metrics
        val executionResult = when {
            command == null -> commandNotFound(task)
            violatesRateLimit(task.weight) -> rateLimited(task)
            !running -> aborted(task)
            else -> executeCommand(command, task)
        }

        registerCall(circuitBreaker, executionResult)
        // remove task, or move it to quarantine, or update number of tries
        handleExecutionResult(executionResult)
        return executionResult.commandExecuted
    }
    
    // ...
}

6.缺陷

輪詢PostgreSQL表也不是一個好主意。但是,每個“邏輯”佇列每秒最多隻能輪詢一次。

談到佇列,它們都在同一張表中進行管理,考慮到更多的使用情況,這可能是效能問題。如果發生這種情況,我們可以將表專用於每個佇列,而當要新增佇列時,我們需要付出更多配置的代價(現在我們需要建立表)。

我們系統的一個更實際的限制是它目前僅處理同步操作,但是我們可以對其進行調整,以便以非同步方式接收命令執行的結果。

更多點選標題見原文

相關文章