聊聊 Elasticsearch 中的任務管理機制

rockybean發表於2022-12-14

Elasticsearch 對外提供了一個 _tasks 介面,用於獲取當前各個節點正在執行的任務,這裡要避免和 pending_tasks 搞混,後者是用於獲取在 master leader 節點排隊等待修改 cluster state 的處理任務。

WHY

Task 管理功能不是一開始就有的,是從 5.0 開始推出並不斷完善的,如下是相關的 issue 連結。

摘取上面的部分描述如下:

We have identified several potential features of elasticsearch that can spawn long running tasks and therefore require a common management mechanism for this tasks.

This issue will introduce task management API that will provide a mechanism for communicating with and controlling currently running tasks.

The task management API will be based on the top of existing TransportAction framework, which will allow any transport action to become a task.

The tasks will maintain parent/child relationship between tasks running on the coordinating nodes and subtasks that are spawn by the coordinating node on other nodes.

上面這段話把引入 Task 的原因、作用都講解的很清楚了,簡單總結如下:

  • 因為現在 es 裡面會有一些長時間執行的任務,所以需要構建一個通用的管理機制,方便去檢視這些任務的執行狀態、進度,甚至能進行一定的管理操作,比如取消。
  • 該機制會提供一個 task management 的 API 來檢視和獲取當前在執行的所有任務
  • 實現上會基於現有的 TransportAction 框架,這樣就可以將所有的 transport action 請求都變成 task 進行管理。
  • 還會提供父子關係的 Task 型別,主要是由協調節點觸發的父任務和下發到其他節點的子任務組成。

看到這裡,相信大家已經對於 Task 管理的來歷以及要實現的功能有了大致的理解,最後再補充一句。

Task 管理功能也極大提升了 Elasticsearch 系統的可觀測性,對於各個節點當前在執行的任務有了統一觀測的手段,不用再猜了。

接下來我們就看看什麼是 Task。

WHAT

Task 通常包含如下資訊:

  • id,id of the task with the node

    • Node 級別的 id,每個 node 獨立維護的單調遞增的整型
  • task_id ,unique task id

    • Cluster 級別的 id,由 Node Id 和 Node 級別的 Task Id 組成,格式為:{Node Id}:{Task Node Id}
  • node,執行任務的 node
  • parent_task_id,parent task id

    • 如果該任務有父子關係,那麼該 id 不為空
  • type,task type,任務型別,主要有下面幾種:

    • transport: 其他 Node 傳送來的 transport 請求任務
    • direct:直接在本地 node 執行的請求任務
    • persistent:持久化的 task,這些 task 是儲存在 cluster state 裡面的,比如 rollup/transform 等,不會因為 node 掛掉而丟失
  • action,task action

    • 建立該 task 的 transport action 名字
    • 透過 action 可以知道這個 task 在具體做的任務,比如 indices:data/write/bulk 是一個寫請求,cluster:monitor/tasks/lists 是一個獲取 task 列表的請求
    • 有些 action 後面有一些字尾,比如 [n] [s] 等,這些字尾有一定的標識作用,這裡簡單解釋下

      • [n],TransportNodesAction,節點與節點間透過 transport 請求傳送的 action
      • [s],transportShardAction,發生在分片上的 action 操作
      • [p],PrimaryShardAction,發生在主分片上的 action 操作
      • [r],ReplicaShardAction,發生在副本分片上的 action 操作
  • start_time,任務開始的時間戳
  • running_time,任務已經執行的總時間
  • x_opaque_id,client 發起請求時可以設定該 http header,這樣便可以跟蹤該請求發起的所有 task
  • description,任務的詳情描述
  • status,描述任務的狀態
  • cancellable,是否可以取消

Task 的相關介面主要是下面兩個:

  • _cat/tasks,以列表的形式展現所有節點的任務,只展現部分內容,屬於簡潔模式
  • _tasks,以 JSON 的形式展現所有節點的任務詳情,屬於詳情模式

下面是透過 GET _tasks?detailed=true 獲取的一個樣例資料:

{
  "-Tws1PJEQ4WW_GSsRLTSLg:35061634": {
    "node": "-Tws1PJEQ4WW_GSsRLTSLg",
    "id": 35061634,
    "type": "transport",
    "action": "indices:data/write/bulk[s]",
    "status": {
      "phase": "waiting_on_primary"
    },
    "description": "requests[1], index[.monitoring-es-7-2022.12.11][0]",
    "start_time_in_millis": 1670740946462,
    "running_time_in_nanos": 359046,
    "cancellable": false,
    "parent_task_id": "_Z3jXvvvQnqR6pye8zwZtg:69312208",
    "headers": {}
  }
}
  • -Tws1PJEQ4WW_GSsRLTSLg:35061634 是 task id,這個 task 執行在 node id 為 -Tws1PJEQ4WW_GSsRLTSLg 的 node 上,id 為 35061634
  • 這個 task 是一個子任務 subtask,其 parent task id 為 _Z3jXvvvQnqR6pye8zwZtg:69312208
  • task type 為 transport,這說明是其他 node 傳送來的請求
  • action 為 indices:data/write/bulk[s],是在 shard 上執行的一個 task,從 description 上可以看到是在 index .monitoring-es-7-2022.12.11 shard 0 上執行的

其他欄位大家可以自行解釋,這裡不再展開講解。

透過這一小節,我們已經知道了 Task 的組成,那麼接下來我們看看 Task 管理是如何實現的。

HOW

TaskManager 是核心管理類,它提供兩個核心方法供外部呼叫。

  • register,註冊一個新的 task
  • unregister,在 task 執行結束後,登出之前的 task

TaskManager 內部使用一個 Map<Long,Task> 的結構維護當前 Node 的所有 task,即 task node idtask 的對映管理。

Task 建立的時機主要是兩個地方:

  • client 透過 rest api 介面發起請求後,大部分請求都會轉成一個 transport action 請求傳送出去再處理,那麼在這個新的 transport action request 傳送之前會生成一個 task
  • Node 與 Node 之間一直在不停地互相通訊,這個通訊也是透過 transport action 請求完成的,在目標 Node 收到請求後,也會生成一個 task

client rest 請求的處理流程大概如下:

Clipboard - 2022-12-11 19.27.46.png

  1. client 發起 http 請求(e.g. GET _tasks),被路由到相關的 RestAction 中,比如 RestListTasksAction
  2. RestAction 對請求做驗證和處理後,會轉換成對應的 transport action request 傳送出去,轉交本地對應的 TransportAction 處理,比如 TransportListTasksAction
  3. TransportAction 在執行任務之前會透過 TaskManager 註冊一個 task
  4. TransportAction 處理相關的請求
  5. 在第 4 步處理結束後透過 TaskManger 登出之前註冊的 task
  6. 一路返回給 client 相關結果

TransportAction 中的程式碼如下,注 1 為 register,注 2 為 unregister。

Clipboard - 2022-12-11 19.30.43.png

Node 與 Node 之間請求的處理流程大致如下:

Clipboard - 2022-12-11 19.30.54.png

  1. Node1 透過 TransportService 的 sendReqeust 方法向 Node2 傳送請求,這其中使用的 actionName 是相關 TransportAction 中定義的 transportNodeAction,比如 TransportListTasksAction 傳送的 action name 是 cluster:monitor/tasks/lists[n]
  2. Node2 接收到請求,在 InboudHandler 中,透過 action name 從 ReqeustHandlers 中獲取對應的 request handler,然後進行處理
  3. 在 RequestHandler 的 processMessageReceived 方法中,會透過 TaskManager 註冊一個 task
  4. RequestHandler 處理相關的請求
  5. 在第 4 步處理結束後透過 TaskManger 登出之前註冊的 task
  6. 一路返回給 Node1 相關結果

RequestHandlerRegistry 中的程式碼如下,注 1 為 register,注 2 為 unregister。

Clipboard - 2022-12-11 19.31.02.png

關於 register 和 unregister 的實現邏輯,這裡就不展開講了,感興趣的同學可以自行去檢視相關程式碼。

其他

持久化結果:.tasks 索引

細心的同學可能會發現在 elasticsearch 中有一個系統索引 .tasks,如果去查詢這個索引的內容,會得到類似如下的文件內容。

GET .tasks/_search
{
        "_index" : ".tasks",
        "_type" : "task",
        "_id" : "9m1T5Qx6RnaRXER7Z:1715505780",
        "_score" : 2.8134105,
        "_source" : {
          "completed" : true,
          "task" : {
            "node" : "9m1T5Qx6RnaRXER7Z",
            "id" : 1715505780,
            "type" : "transport",
            "action" : "indices:data/write/reindex",
            "status" : {
              "total" : 34556,
              "updated" : 0,
              "created" : 34556,
              "deleted" : 0,
              "batches" : 35,
              "version_conflicts" : 0,
              "noops" : 0,
              "retries" : {
                "bulk" : 0,
                "search" : 0
              },
              "throttled_millis" : 0,
              "requests_per_second" : -1.0,
              "throttled_until_millis" : 0
            },
            "description" : "reindex from [.indexA] to [.indexA_reindex][_doc]",
            "start_time_in_millis" : 1657627161222,
            "running_time_in_nanos" : 7259281625,
            "cancellable" : true,
            "headers" : { }
          },
          "response" : {
            "took" : 7239,
            "timed_out" : false,
            "total" : 34556,
            "updated" : 0,
            "created" : 34556,
            "deleted" : 0,
            "batches" : 35,
            "version_conflicts" : 0,
            "noops" : 0,
            "retries" : {
              "bulk" : 0,
              "search" : 0
            },
            "throttled" : "0s",
            "throttled_millis" : 0,
            "requests_per_second" : -1.0,
            "throttled_until" : "0s",
            "throttled_until_millis" : 0,
            "failures" : [ ]
          }
        }
      }

這個返回的文件記錄了一個 reindex task 的詳情和結果。

需要注意的是,並非所有的 task 都可以持久化結果到 .tasks 索引中,這隻支援某些 long running task ,如下:

  • DeleteByQuery
  • Reindex
  • UpdateByQuery

在發起相關請求時,只要加上一個引數 wait_for_completion=true,請求會返回一個 task id,然後該 task 的結果會被記錄到 .tasks 索引中。如果不加該引數,則不會記錄。

另外 .tasks 索引是按需建立的,只有在需要記錄結果時才會建立該索引,如果你的 cluster 裡面沒有,也沒有什麼問題。

取消任務 Cancel Task

部分 Task 在執行過程中可以被取消(Cancel),相關介面為 POST _tasks/[task_id/_cancel 。但不是所有 Task 都可以被取消,只有 Cancellable 為 true 的才可以。

可以取消的任務主要是一些 long running 的task,比如 reindex、update by query、delete by query、search 等,它們的 task 都繼承了 CancellableTask

另外 ES 還引入了自動 cancel search 任務的機制,如下是相關 issue:

當 ES 發現 client 主動斷開連線時,會主動 cancel 當前正在執行的 search 任務,以便減輕叢集負載。

Persistent Task

Persistent Task 是一類比較特殊的任務,一般的 Task 在 Node 停止或者 Crash 後就結束了,即便 Node 重啟也無法繼續之前在執行的 Task,但是 Persistent Task 透過將自身持久化到 Cluster State 中,即便相關 Node 停止,它依然可以被重新分配到其他 Node 上繼續執行。

這部分使用的不多,主要是 x-pack 增加的一些如 ml、rollup、transform 等功能在用,瞭解下即可。

總結

本文主要講解了 Elasticsearch 中 Task 的來歷、組成和實現,希望能對大家有所幫助,以後可以正確的使用 Task Management API 來解決使用中的問題。

引用

相關文章