攜程前端自動化任務平臺TaskHub開發實踐
一、前言
本文討論的自動化,是指通過代碼的方式,將原本人工完成的相關操作由機器代為處理,如此達到釋放人力和提升效率等目標。
然而實現自動化的過程不總是那么順利,自動化后的效果未必那么理想。可能有的團隊會發現自動化腳本盡管釋放了業務人員,但成本卻轉移到開發人員身上。他們需要投入大量時間去更新失效的自動化邏輯,出現問題后的排障時間和難度也隨之增加。如果自動化任務的成功率低、問題修復速度慢,那么大量的工作將不得不降級為人工處理,自動化的業務價值無從體現。
我們發現,通過完善自動化任務的相關基建(包括任務調度引擎和任務管理平臺等),上述問題可以得到顯著的控制和緩解。本文將分享攜程旅游研發在這方面的嘗試和經驗,希望能為其他開發者和團隊提供參考。
二、平臺背景
旅游內部不少業務都有自動化需求,部分團隊已經上線了自己的自動化項目,這些項目由不同的團隊維護,但均有相似的痛點:
- 無法及時關閉自動化任務:由于缺乏有效的控制手段,自動化任務一旦重啟就難以中斷。
- 排障效率低下:目前大部分自動化任務是前端自動化任務,前端由于站點更新、網絡波動、代理異常等常見原因導致任務無法完成,出錯的原因很雜,不能復現場景,難以查找根因。
- 日志查找困難:現有的日志系統以時間為錨點記錄日志,沒有劃分任務的邊界。一次自動化任務產生的日志通常是一個普通請求的2到5倍,需要從大量連續的任務執行日志中找到需要的信息,并且隨著任務增加,存儲的日志的文件數量也隨之增長,查找變得更加困難。
- 復現困難:任務的入參通常和日志柔和在一起,導致復現問題時難以分析。
- 日志沒有權限限制:任何人都可以查看日志,可能導致敏感信息泄露。
三、平臺目標
為了解決自動化的共性問題,減少重復開發,我們希望通過創建一個統一的平臺來提升自動化效率和可靠性,具體目標如下:
- 提高排錯效率:通過詳細的日志記錄和輔助工具,低成本快速還原任務場景,便于快速定位和復現。
- 被動感知任務異常:建立實時監控和通知機制,實時檢測并推送任務異常信息,減少人為監控負擔,完全釋放人力。
- 聚焦業務本身:讓開發者專注于核心業務邏輯,不必擔心自動化過程中可能出現的性能問題,通過平臺提供的工具確保自動化任務高效、穩定地運行。
基于以上目標,我們設計開發出前端自動化任務平臺TaskHub,一個能夠幫助自動化提效的解決方案。
四、TaskHub介紹
在TaskHub中,我們把自動化腳本的執行,稱為任務。
任務是無狀態的,任務與任務之間沒有直接關系。任務的設計目標是實現某一特定的自動化功能。
在自動化過程中,核心是確保任務的正確執行。任務執行過程中產生的副作用不應阻礙任務的執行。因此,我們將TaskHub主要分為兩個部分:平臺和引擎。平臺用于記錄和管理任務執行過程中的產物,而引擎專門負責任務的執行。
平臺:可以方便查看、配置、中斷任務。
引擎:負責調度任務執行,是任務執行的核心。
整體設計:

引擎通過NPM的方式安裝,以 SDK 的方式運行在項目中。這樣做有以下幾個好處:
1)引擎和平臺完全解耦。即使平臺不可用也不影響任務運行,確保任務高可用。
2)靈活部署?,F有自動化業務不會因為接入 TaskHub 改變原來的部署方式,業務可根據自身需求按需部署。
接下來分別介紹下平臺和引擎。
4.1 TaskHub平臺

平臺包含三個主要模塊:項目、任務和日志。它們之間是一對多的關系:一個項目包含多個任務,一個任務包含多條日志。每個模塊都有嚴格的權限校驗,確保數據安全和訪問控制。
- 項目模塊:項目是任務的集合。它包含項目的基本信息,并負責管理項目成員和權限,為同一類任務做統一配置。
- 任務模塊:包含任務的輸入、輸出、運行時間等信息。任務流程被結構化展示,使任務的輸入和輸出更加清晰,降低復現難度,提高排錯效率。
- 日志模塊:記錄任務運行的所有日志,分為業務日志和系統日志兩個子模塊,方便快速切換和篩查。除了傳統的文字日志,TaskHub還支持圖片日志。如果在自動化過程中遇到意外錯誤,可以截圖記錄錯誤頁面,留下錯誤快照,方便后期排查時快速了解錯誤發生的場景。
4.2 TaskHub引擎
TaskHub 引擎的核心是調度執行用戶的腳本。用戶只需編寫任務的 .ts 文件,引擎會為每個任務分配一個獨立的 Node 子進程,每個任務都在自己的子進程中運行。
這樣設計有以下幾個好處:
1)數據隔離。借助進程數據隔離的特性,無成本實現業務數據的隔離,帶來了數據的安全性。
2)任務易清理。任務執行完成時,使用 process.exit()退出子進程即可釋放資源和依賴。此前度假部分自動化任務的方案在釋放資源時有較重的心智負擔,需仔細編排以避免影響其他任務。
3)任務可遠程關閉。度假部分自動化場景有中斷執行的需求,獨立的子進程使得中斷更簡單,直接對子進程進行操作即可。
4.2.1 如何初始化項目
在 TaskHub平臺 注冊一個新項目后,使用 TaskHub SDK 提供的 engine 對象來初始化 TaskHub引擎。只需將項目ID傳入 engine.initProject 方法,引擎就會與 TaskHub平臺 上的項目進行綁定。這樣,后續的任務日志、狀態等信息就會與相應的項目關聯起來。
初始化引擎代碼示意如下:

接下來,通過調用 project 的 addTask 方法,即可啟動任務,引擎內部會使用子進程運行用戶的腳本。

以上就是運行TaskHub自動化任務的核心代碼。
4.2.2 引擎內部設計
在 TaskHub 引擎中,每當一個新任務啟動時,引擎會創建一個新的子進程,并在子進程中運行任務,如下圖所示:

在簡單的自動化場景中,通過主進程啟動任務可以滿足大多數需求。然而,在復雜的業務場景中,僅僅依靠主進程啟動一個子進程來運行任務是不夠的。
在任務執行過程中,子進程對于主進程來說是一個黑盒。主進程無法直接了解任務當前運行到哪一步,以及任務的當前狀態。為了便于主進程和子進程之間的數據流轉,TaskHub 引擎建立了主進程和子進程之間的雙向通信機制。
以獲取任務運行結果為例,當子進程運行任務后,我們希望在主進程中獲取任務的返回結果。
我們可以在主進程中使用之前創建的 task 實例來注冊監聽事件,等待子進程發送消息。具體代碼如下所示:


在任務腳本中,根據需要發送任務狀態的更新,如下所示:

需要注意的是,子進程可以向主進程發送消息,而主進程也可以收發子進程的消息。這種雙向通信機制在以下場景中非常有用:
1)狀態監控:主進程可以實時接收子進程的狀態更新,了解任務執行的每一步驟。
2)任務控制:主進程可以向子進程發送指令,例如暫停、繼續或終止任務。
3)異常處理:子進程在遇到問題時,可以立即通知主進程,使得異常情況能夠迅速得到響應和處理。
4)數據傳輸:主進程和子進程之間可以交換數據,確保任務執行所需的信息流暢傳遞。
以上是引擎內部任務調度的實現,接下來介紹引擎如何與TaskHub平臺進行通信。
4.2.3 引擎外部通信
如上面所說,TaskHub 平臺的主要用途是記錄自動化腳本執行過程中產生的日志、任務狀態,以及其他需要被持久化的狀態。所以主要通信的內容包括:任務狀態的變更、日志推送、引擎輪詢獲取需要主動終止的任務。

引擎通信接口 IMessageSender
整體設計中提到 TaskHub 引擎與 TaskHub 平臺是解耦的。引擎內部定義了一份接口 IMessageSender,只要實現了接口就能與引擎共同運行,TaskHub 平臺只是引擎接口的一份實現。
接口定義如下:

我們期望 TaskHub 平臺的可用性等級是穩定的,不期望在更高可用性等級要求的應用接入時被迫提升自己的可用性等級。所以,通信接口中關于日志的部分, 引擎對 原有的日志平臺 也做了一份實現,作為TaskHub日志系統的兜底方案。
當 TaskHub 平臺不可用時,引擎與平臺的通信會降級到兜底方案,此時部分能力是受限的,例如終止任務的能力。但是這并不會影響自動化任務的執行,引擎的調度能力、腳本的日志留痕等能力仍然可用,并且,所有運行日志可以在原有的日志平臺獲取。
以上是引擎設計相關的內容,除此之外,TaskHub還提供了兩個輔助 SDK 以補充 TaskHub 平臺的使用:
- logger:幫助用戶記錄日志到TaskHub平臺,支持文字和圖片日志。
- media:集成了OSS平臺,提供簡單易用的API將本地圖片或base64格式的圖片上傳至服務器,方便任務運行過程中對圖像數據的管理。
五、使用案例
5.1 度假業務自動化數據錄入
度假業務內部有一個需求,業務人員需要定期在某個站點錄入數據。后來,將數據結構化處理后,通過自動化程序定時進行數據錄入,代替之前的人工操作。
雖然這種自動化模式解決了一部分問題,但在實踐中也發現了一些新的問題:
1)排障效率低。在自動化的過程中,由于站點加載的資源很多,涉及出錯的原因很雜,通過現有日志系統排障需要投入大量時間,效率低下,占用了寶貴的開發時間。
2)無法及時關閉單個任務。目前只能通過關閉整個自動化應用來關閉某個正在運行的任務,這個過程不僅會關閉其他正在運行的任務,而且整個關閉的流程很長,無法及時關閉。
在接入TaskHub后,自動化系統的容錯率和排障效率得到了顯著提升。
收益:
及時關閉任務:每個任務運行在不同的子進程上,可以殺掉子進程立即關閉單個任務。
提升日志排障效率:除了可以及時關閉外,TaskHub 顯著提升了查找日志排障的效率,節省了大量時間。
以下圖表對比了傳統日志查找與TaskHub任務日志的差異:
傳統日志查找:開發人員需要從大量日志文件中找到錯誤發生時的日志文件,然后再從文件中定位到異常任務的錯誤日志,費時費力,容易遺漏關鍵細節。
TaskHub任務日志:TaskHub為不同任務劃分邊界,不再需要從大量雜亂的日志中尋找關鍵信息。每個任務的日志被結構化記錄,便于快速查找和定位問題。此外,任務的輸入輸出也清晰可見,可以快速復現場景。

任務詳情

任務日志
5.2 復雜業務場景的自動化改造
在更為復雜的業務場景中,自動化需求往往更加多樣化。例如,當前有一個需求:人工需要定期在某個站點上查看頁面特定元素的狀態,如果滿足某些條件,則去另一個站點錄入數據。
為了將該項目進行自動化改造,可以將場景拆分為以下兩個需求:
需求1:在某個站點上通過查找頁面來獲得業務數據。
需求2:定期在某個站點上錄入數據,錄入數據之前需要判斷【需求1】中的業務結果。
具體實現方案:
需求1:設計成一個接口,通過調用接口返回的結果來獲得業務數據。
需求2:設計成定時任務,定期執行。每次執行之前先請求【需求1】的接口,判斷條件是否滿足。
這是很自然的自動化改造,能夠釋放人力資源,并且任務也被合理地拆分。然而,在實際運行中,可能無法完全達到預期效果:
1)人工并未完全釋放:加入自動化后,人工仍需定時關注自動化任務是否正常運行,無法徹底擺脫手動監控。
2)排障復雜度增加:隨著前置任務的增加,自動化排障的復雜度直線上升。當【需求2】的任務運行出現異常時,如果發現是由于前置的【需求1】出現問題,就需要根據錯誤的發生時間去【需求1】的機器上查找日志。若有多個前置任務,排查成本將大幅增加。
我們來看下加入TaskHub之后,會有哪些改變。
1)任務異常主動通知:從原來的主動查看任務狀態,轉變為被動接收任務異常通知。任務出現異常時,系統會主動提醒,減少了人工監控的負擔。
2)快速排障:通過捕捉錯誤快照,將其記錄到 TaskHub 的圖片日志中,開發人員能夠快速定位并解決問題。
3)清晰的排障鏈路:TaskHub 不僅提升了單一任務日志的查找效率,對于多個串聯任務也同樣適用。通過在下游日志中打印上游任務ID,可以快速查找上游日志,無需在多個機器日志中來回跳轉。
下圖展示了一個三個串聯任務的排障例子。
值得注意的是,TaskHub 并沒有改變原有項目的設計,只是將任務的運行方式從 Node 轉變為 TaskHub 引擎。
TaskHub 將自動化中的任務概念獨立出來,本質上,任務就是腳本的執行。無論任務是通過接口調用、定時函數還是定時器喚起,任務的喚起方式雖然不同,但任務執行的邏輯保持一致。這樣一來,開發者可以更專注于任務的邏輯實現,再根據需求喚起任務即可。
到目前為止,已有 12 個自動化項目使用 TaskHub 運行,累計完成了 48w 次自動化任務,記錄了 1300w 條日志。
六、RPC BFF最佳實踐
在進行 TaskHub BFF 的技術選型時,我們對比了當前多種主流的技術棧。由于 TaskHub BFF 有多個調用方,并且在客戶端和服務端都有消費場景,綜合考慮上手成本和多個消費者之間的同步成本等因素,最終選擇了 RPC BFF。
關于RPC BFF 更多介紹可以查看文章《攜程度假基于 RPC 和 TypeScript 的 BFF 設計與實踐》。
傳統服務開發中,服務提供方需要撰寫服務代碼,然后在第三方平臺同步接口文檔,消費者再根據文檔約定在不同調用環境中使用這些接口。

而在 RPC BFF 模式的開發中,服務提供方撰寫服務代碼之后,消費者只需通過一個命令就可以獲得接口的調用文檔和調用函數。這種方式不僅簡化了開發流程,還提高了接口調用的一致性和可靠性。
在使用RPC BFF的過程中,我們也總結了一些最佳實踐,希望能夠給大家帶來一些啟發。
6.1 收斂類型
在設計 RPC 接口的返回值時,有些開發者可能會沿用樸素函數的設計思路,即當接口返回成功時,返回一個成功標識和數據;當失敗時,則返回一個失敗狀態和錯誤碼,讓消費端根據不同的錯誤碼進行相應操作,如下所示:

這種設計存在以下幾個問題:
1)邏輯不夠清晰:簡單地將返回結果分為成功和失敗,實際開發中可能還有“可接受的錯誤”或其他復雜的狀態,擴展性不佳。
2)錯誤碼維護復雜:前后端都需要維護同一套錯誤碼,增加了開發和維護成本。
3)錯誤處理遺漏:編碼時容易遺漏未處理的錯誤狀態,增加了系統不穩定性。
其實,可以使用 RPC BFF 的 Union 類型,將所有可能返回的狀態聯合起來,合并為最終返回的類型。

通過聚合所有返回結果,不再需要維護同一套錯誤碼,并且通過類型系統就能確保所有可能的狀態都被處理。如下圖所示,在消費返回結果時,代碼編輯器中有代碼提示,不會遺漏任何狀態。
如果接口或類型有更新,只需要一個命令就可以同步更新接口和接口類型。


6.2 原子化過程
RPC BFF 的核心理念是面向函數的接口編程,得益于它對底層通信細節的封裝,開發者只需考慮函數的功能即可。
與 RESTful 優先資源的理念不同,RPC BFF 的原子是一個過程。REST 將資源暴露出來,而 RPC 是將過程暴露出來。
所以,可以通過分析服務流程(過程),將其拆解為最小不可分割的流程,再將這些流程通過函數實現。
只需保證所有原子化的過程都被很好地處理。在原子過程之上的過程調用,只需按需組合這些原子過程,從而形成一個過程流,最終形成一個有向無環圖(Directed Acyclic Graph)。
例如,有一個接口接收一個 ProjectId ,并返回該 ID 對應的項目信息。如果未獲取到項目,則返回未找到。

getProjectById既可以對外暴露,也可以在其他函數中調用。
假設有一個業務流程,需要首先檢查項目是否存在,然后根據項目狀態執行不同的操作。如下圖所示,在 getProjectSetting 中,我們調用接口的方式就像調用本地函數一樣,并且利用前面說的返回值類型組合,清晰地完成代碼邏輯。

通過這種方式,有以下幾個收益:
1)高復用性:原子化的函數可以在不同的業務場景中復用,減少代碼重復。
2)清晰明確:每個函數只負責一個具體的操作,邏輯清晰,易于維護和測試。
3)組合靈活:可以根據業務需求,靈活組合原子化過程,構建復雜的業務邏輯。
七、結語
通過重新梳理整個自動化流程,我們拆分出了運行自動化任務的核心部分和輔助模塊。在保證自動化任務高可用的基礎上,提高了自動化的整體效率和容錯率??紤]到自動化場景的復雜性,我們特別設計了主進程與子進程的雙向通信機制,以應對各種自動化場景的挑戰。
此外,我們還提供了兩個自動化場景案例,詳細分析TaskHub如何幫助提升自動化效率。
最后,我們結合團隊在RPC BFF的實踐,分享了一些使用經驗。
未來,我們將繼續探索更多的自動化場景,并不斷完善TaskHub。



























