攜程前端自動化任務(wù)平臺TaskHub開發(fā)實踐
一、前言
本文討論的自動化,是指通過代碼的方式,將原本人工完成的相關(guān)操作由機器代為處理,如此達到釋放人力和提升效率等目標。
然而實現(xiàn)自動化的過程不總是那么順利,自動化后的效果未必那么理想??赡苡械膱F隊會發(fā)現(xiàn)自動化腳本盡管釋放了業(yè)務(wù)人員,但成本卻轉(zhuǎn)移到開發(fā)人員身上。他們需要投入大量時間去更新失效的自動化邏輯,出現(xiàn)問題后的排障時間和難度也隨之增加。如果自動化任務(wù)的成功率低、問題修復(fù)速度慢,那么大量的工作將不得不降級為人工處理,自動化的業(yè)務(wù)價值無從體現(xiàn)。
我們發(fā)現(xiàn),通過完善自動化任務(wù)的相關(guān)基建(包括任務(wù)調(diào)度引擎和任務(wù)管理平臺等),上述問題可以得到顯著的控制和緩解。本文將分享攜程旅游研發(fā)在這方面的嘗試和經(jīng)驗,希望能為其他開發(fā)者和團隊提供參考。
二、平臺背景
旅游內(nèi)部不少業(yè)務(wù)都有自動化需求,部分團隊已經(jīng)上線了自己的自動化項目,這些項目由不同的團隊維護,但均有相似的痛點:
- 無法及時關(guān)閉自動化任務(wù):由于缺乏有效的控制手段,自動化任務(wù)一旦重啟就難以中斷。
- 排障效率低下:目前大部分自動化任務(wù)是前端自動化任務(wù),前端由于站點更新、網(wǎng)絡(luò)波動、代理異常等常見原因?qū)е氯蝿?wù)無法完成,出錯的原因很雜,不能復(fù)現(xiàn)場景,難以查找根因。
- 日志查找困難:現(xiàn)有的日志系統(tǒng)以時間為錨點記錄日志,沒有劃分任務(wù)的邊界。一次自動化任務(wù)產(chǎn)生的日志通常是一個普通請求的2到5倍,需要從大量連續(xù)的任務(wù)執(zhí)行日志中找到需要的信息,并且隨著任務(wù)增加,存儲的日志的文件數(shù)量也隨之增長,查找變得更加困難。
- 復(fù)現(xiàn)困難:任務(wù)的入?yún)⑼ǔ:腿罩救岷驮谝黄穑瑢?dǎo)致復(fù)現(xiàn)問題時難以分析。
- 日志沒有權(quán)限限制:任何人都可以查看日志,可能導(dǎo)致敏感信息泄露。
三、平臺目標
為了解決自動化的共性問題,減少重復(fù)開發(fā),我們希望通過創(chuàng)建一個統(tǒng)一的平臺來提升自動化效率和可靠性,具體目標如下:
- 提高排錯效率:通過詳細的日志記錄和輔助工具,低成本快速還原任務(wù)場景,便于快速定位和復(fù)現(xiàn)。
- 被動感知任務(wù)異常:建立實時監(jiān)控和通知機制,實時檢測并推送任務(wù)異常信息,減少人為監(jiān)控負擔,完全釋放人力。
- 聚焦業(yè)務(wù)本身:讓開發(fā)者專注于核心業(yè)務(wù)邏輯,不必擔心自動化過程中可能出現(xiàn)的性能問題,通過平臺提供的工具確保自動化任務(wù)高效、穩(wěn)定地運行。
基于以上目標,我們設(shè)計開發(fā)出前端自動化任務(wù)平臺TaskHub,一個能夠幫助自動化提效的解決方案。
四、TaskHub介紹
在TaskHub中,我們把自動化腳本的執(zhí)行,稱為任務(wù)。
任務(wù)是無狀態(tài)的,任務(wù)與任務(wù)之間沒有直接關(guān)系。任務(wù)的設(shè)計目標是實現(xiàn)某一特定的自動化功能。
在自動化過程中,核心是確保任務(wù)的正確執(zhí)行。任務(wù)執(zhí)行過程中產(chǎn)生的副作用不應(yīng)阻礙任務(wù)的執(zhí)行。因此,我們將TaskHub主要分為兩個部分:平臺和引擎。平臺用于記錄和管理任務(wù)執(zhí)行過程中的產(chǎn)物,而引擎專門負責任務(wù)的執(zhí)行。
平臺:可以方便查看、配置、中斷任務(wù)。
引擎:負責調(diào)度任務(wù)執(zhí)行,是任務(wù)執(zhí)行的核心。
整體設(shè)計:
引擎通過NPM的方式安裝,以 SDK 的方式運行在項目中。這樣做有以下幾個好處:
1)引擎和平臺完全解耦。即使平臺不可用也不影響任務(wù)運行,確保任務(wù)高可用。
2)靈活部署?,F(xiàn)有自動化業(yè)務(wù)不會因為接入 TaskHub 改變原來的部署方式,業(yè)務(wù)可根據(jù)自身需求按需部署。
接下來分別介紹下平臺和引擎。
4.1 TaskHub平臺
平臺包含三個主要模塊:項目、任務(wù)和日志。它們之間是一對多的關(guān)系:一個項目包含多個任務(wù),一個任務(wù)包含多條日志。每個模塊都有嚴格的權(quán)限校驗,確保數(shù)據(jù)安全和訪問控制。
- 項目模塊:項目是任務(wù)的集合。它包含項目的基本信息,并負責管理項目成員和權(quán)限,為同一類任務(wù)做統(tǒng)一配置。
- 任務(wù)模塊:包含任務(wù)的輸入、輸出、運行時間等信息。任務(wù)流程被結(jié)構(gòu)化展示,使任務(wù)的輸入和輸出更加清晰,降低復(fù)現(xiàn)難度,提高排錯效率。
- 日志模塊:記錄任務(wù)運行的所有日志,分為業(yè)務(wù)日志和系統(tǒng)日志兩個子模塊,方便快速切換和篩查。除了傳統(tǒng)的文字日志,TaskHub還支持圖片日志。如果在自動化過程中遇到意外錯誤,可以截圖記錄錯誤頁面,留下錯誤快照,方便后期排查時快速了解錯誤發(fā)生的場景。
4.2 TaskHub引擎
TaskHub 引擎的核心是調(diào)度執(zhí)行用戶的腳本。用戶只需編寫任務(wù)的 .ts 文件,引擎會為每個任務(wù)分配一個獨立的 Node 子進程,每個任務(wù)都在自己的子進程中運行。
這樣設(shè)計有以下幾個好處:
1)數(shù)據(jù)隔離。借助進程數(shù)據(jù)隔離的特性,無成本實現(xiàn)業(yè)務(wù)數(shù)據(jù)的隔離,帶來了數(shù)據(jù)的安全性。
2)任務(wù)易清理。任務(wù)執(zhí)行完成時,使用 process.exit()退出子進程即可釋放資源和依賴。此前度假部分自動化任務(wù)的方案在釋放資源時有較重的心智負擔,需仔細編排以避免影響其他任務(wù)。
3)任務(wù)可遠程關(guān)閉。度假部分自動化場景有中斷執(zhí)行的需求,獨立的子進程使得中斷更簡單,直接對子進程進行操作即可。
4.2.1 如何初始化項目
在 TaskHub平臺 注冊一個新項目后,使用 TaskHub SDK 提供的 engine 對象來初始化 TaskHub引擎。只需將項目ID傳入 engine.initProject 方法,引擎就會與 TaskHub平臺 上的項目進行綁定。這樣,后續(xù)的任務(wù)日志、狀態(tài)等信息就會與相應(yīng)的項目關(guān)聯(lián)起來。
初始化引擎代碼示意如下:
接下來,通過調(diào)用 project 的 addTask 方法,即可啟動任務(wù),引擎內(nèi)部會使用子進程運行用戶的腳本。
以上就是運行TaskHub自動化任務(wù)的核心代碼。
4.2.2 引擎內(nèi)部設(shè)計
在 TaskHub 引擎中,每當一個新任務(wù)啟動時,引擎會創(chuàng)建一個新的子進程,并在子進程中運行任務(wù),如下圖所示:
在簡單的自動化場景中,通過主進程啟動任務(wù)可以滿足大多數(shù)需求。然而,在復(fù)雜的業(yè)務(wù)場景中,僅僅依靠主進程啟動一個子進程來運行任務(wù)是不夠的。
在任務(wù)執(zhí)行過程中,子進程對于主進程來說是一個黑盒。主進程無法直接了解任務(wù)當前運行到哪一步,以及任務(wù)的當前狀態(tài)。為了便于主進程和子進程之間的數(shù)據(jù)流轉(zhuǎn),TaskHub 引擎建立了主進程和子進程之間的雙向通信機制。
以獲取任務(wù)運行結(jié)果為例,當子進程運行任務(wù)后,我們希望在主進程中獲取任務(wù)的返回結(jié)果。
我們可以在主進程中使用之前創(chuàng)建的 task 實例來注冊監(jiān)聽事件,等待子進程發(fā)送消息。具體代碼如下所示:
在任務(wù)腳本中,根據(jù)需要發(fā)送任務(wù)狀態(tài)的更新,如下所示:
需要注意的是,子進程可以向主進程發(fā)送消息,而主進程也可以收發(fā)子進程的消息。這種雙向通信機制在以下場景中非常有用:
1)狀態(tài)監(jiān)控:主進程可以實時接收子進程的狀態(tài)更新,了解任務(wù)執(zhí)行的每一步驟。
2)任務(wù)控制:主進程可以向子進程發(fā)送指令,例如暫停、繼續(xù)或終止任務(wù)。
3)異常處理:子進程在遇到問題時,可以立即通知主進程,使得異常情況能夠迅速得到響應(yīng)和處理。
4)數(shù)據(jù)傳輸:主進程和子進程之間可以交換數(shù)據(jù),確保任務(wù)執(zhí)行所需的信息流暢傳遞。
以上是引擎內(nèi)部任務(wù)調(diào)度的實現(xiàn),接下來介紹引擎如何與TaskHub平臺進行通信。
4.2.3 引擎外部通信
如上面所說,TaskHub 平臺的主要用途是記錄自動化腳本執(zhí)行過程中產(chǎn)生的日志、任務(wù)狀態(tài),以及其他需要被持久化的狀態(tài)。所以主要通信的內(nèi)容包括:任務(wù)狀態(tài)的變更、日志推送、引擎輪詢獲取需要主動終止的任務(wù)。
引擎通信接口 IMessageSender
整體設(shè)計中提到 TaskHub 引擎與 TaskHub 平臺是解耦的。引擎內(nèi)部定義了一份接口 IMessageSender,只要實現(xiàn)了接口就能與引擎共同運行,TaskHub 平臺只是引擎接口的一份實現(xiàn)。
接口定義如下:
我們期望 TaskHub 平臺的可用性等級是穩(wěn)定的,不期望在更高可用性等級要求的應(yīng)用接入時被迫提升自己的可用性等級。所以,通信接口中關(guān)于日志的部分, 引擎對 原有的日志平臺 也做了一份實現(xiàn),作為TaskHub日志系統(tǒng)的兜底方案。
當 TaskHub 平臺不可用時,引擎與平臺的通信會降級到兜底方案,此時部分能力是受限的,例如終止任務(wù)的能力。但是這并不會影響自動化任務(wù)的執(zhí)行,引擎的調(diào)度能力、腳本的日志留痕等能力仍然可用,并且,所有運行日志可以在原有的日志平臺獲取。
以上是引擎設(shè)計相關(guān)的內(nèi)容,除此之外,TaskHub還提供了兩個輔助 SDK 以補充 TaskHub 平臺的使用:
- logger:幫助用戶記錄日志到TaskHub平臺,支持文字和圖片日志。
- media:集成了OSS平臺,提供簡單易用的API將本地圖片或base64格式的圖片上傳至服務(wù)器,方便任務(wù)運行過程中對圖像數(shù)據(jù)的管理。
五、使用案例
5.1 度假業(yè)務(wù)自動化數(shù)據(jù)錄入
度假業(yè)務(wù)內(nèi)部有一個需求,業(yè)務(wù)人員需要定期在某個站點錄入數(shù)據(jù)。后來,將數(shù)據(jù)結(jié)構(gòu)化處理后,通過自動化程序定時進行數(shù)據(jù)錄入,代替之前的人工操作。
雖然這種自動化模式解決了一部分問題,但在實踐中也發(fā)現(xiàn)了一些新的問題:
1)排障效率低。在自動化的過程中,由于站點加載的資源很多,涉及出錯的原因很雜,通過現(xiàn)有日志系統(tǒng)排障需要投入大量時間,效率低下,占用了寶貴的開發(fā)時間。
2)無法及時關(guān)閉單個任務(wù)。目前只能通過關(guān)閉整個自動化應(yīng)用來關(guān)閉某個正在運行的任務(wù),這個過程不僅會關(guān)閉其他正在運行的任務(wù),而且整個關(guān)閉的流程很長,無法及時關(guān)閉。
在接入TaskHub后,自動化系統(tǒng)的容錯率和排障效率得到了顯著提升。
收益:
及時關(guān)閉任務(wù):每個任務(wù)運行在不同的子進程上,可以殺掉子進程立即關(guān)閉單個任務(wù)。
提升日志排障效率:除了可以及時關(guān)閉外,TaskHub 顯著提升了查找日志排障的效率,節(jié)省了大量時間。
以下圖表對比了傳統(tǒng)日志查找與TaskHub任務(wù)日志的差異:
傳統(tǒng)日志查找:開發(fā)人員需要從大量日志文件中找到錯誤發(fā)生時的日志文件,然后再從文件中定位到異常任務(wù)的錯誤日志,費時費力,容易遺漏關(guān)鍵細節(jié)。
TaskHub任務(wù)日志:TaskHub為不同任務(wù)劃分邊界,不再需要從大量雜亂的日志中尋找關(guān)鍵信息。每個任務(wù)的日志被結(jié)構(gòu)化記錄,便于快速查找和定位問題。此外,任務(wù)的輸入輸出也清晰可見,可以快速復(fù)現(xiàn)場景。
任務(wù)詳情
任務(wù)日志
5.2 復(fù)雜業(yè)務(wù)場景的自動化改造
在更為復(fù)雜的業(yè)務(wù)場景中,自動化需求往往更加多樣化。例如,當前有一個需求:人工需要定期在某個站點上查看頁面特定元素的狀態(tài),如果滿足某些條件,則去另一個站點錄入數(shù)據(jù)。
為了將該項目進行自動化改造,可以將場景拆分為以下兩個需求:
需求1:在某個站點上通過查找頁面來獲得業(yè)務(wù)數(shù)據(jù)。
需求2:定期在某個站點上錄入數(shù)據(jù),錄入數(shù)據(jù)之前需要判斷【需求1】中的業(yè)務(wù)結(jié)果。
具體實現(xiàn)方案:
需求1:設(shè)計成一個接口,通過調(diào)用接口返回的結(jié)果來獲得業(yè)務(wù)數(shù)據(jù)。
需求2:設(shè)計成定時任務(wù),定期執(zhí)行。每次執(zhí)行之前先請求【需求1】的接口,判斷條件是否滿足。
這是很自然的自動化改造,能夠釋放人力資源,并且任務(wù)也被合理地拆分。然而,在實際運行中,可能無法完全達到預(yù)期效果:
1)人工并未完全釋放:加入自動化后,人工仍需定時關(guān)注自動化任務(wù)是否正常運行,無法徹底擺脫手動監(jiān)控。
2)排障復(fù)雜度增加:隨著前置任務(wù)的增加,自動化排障的復(fù)雜度直線上升。當【需求2】的任務(wù)運行出現(xiàn)異常時,如果發(fā)現(xiàn)是由于前置的【需求1】出現(xiàn)問題,就需要根據(jù)錯誤的發(fā)生時間去【需求1】的機器上查找日志。若有多個前置任務(wù),排查成本將大幅增加。
我們來看下加入TaskHub之后,會有哪些改變。
1)任務(wù)異常主動通知:從原來的主動查看任務(wù)狀態(tài),轉(zhuǎn)變?yōu)楸粍咏邮杖蝿?wù)異常通知。任務(wù)出現(xiàn)異常時,系統(tǒng)會主動提醒,減少了人工監(jiān)控的負擔。
2)快速排障:通過捕捉錯誤快照,將其記錄到 TaskHub 的圖片日志中,開發(fā)人員能夠快速定位并解決問題。
3)清晰的排障鏈路:TaskHub 不僅提升了單一任務(wù)日志的查找效率,對于多個串聯(lián)任務(wù)也同樣適用。通過在下游日志中打印上游任務(wù)ID,可以快速查找上游日志,無需在多個機器日志中來回跳轉(zhuǎn)。
下圖展示了一個三個串聯(lián)任務(wù)的排障例子。
值得注意的是,TaskHub 并沒有改變原有項目的設(shè)計,只是將任務(wù)的運行方式從 Node 轉(zhuǎn)變?yōu)?TaskHub 引擎。
TaskHub 將自動化中的任務(wù)概念獨立出來,本質(zhì)上,任務(wù)就是腳本的執(zhí)行。無論任務(wù)是通過接口調(diào)用、定時函數(shù)還是定時器喚起,任務(wù)的喚起方式雖然不同,但任務(wù)執(zhí)行的邏輯保持一致。這樣一來,開發(fā)者可以更專注于任務(wù)的邏輯實現(xiàn),再根據(jù)需求喚起任務(wù)即可。
到目前為止,已有 12 個自動化項目使用 TaskHub 運行,累計完成了 48w 次自動化任務(wù),記錄了 1300w 條日志。
六、RPC BFF最佳實踐
在進行 TaskHub BFF 的技術(shù)選型時,我們對比了當前多種主流的技術(shù)棧。由于 TaskHub BFF 有多個調(diào)用方,并且在客戶端和服務(wù)端都有消費場景,綜合考慮上手成本和多個消費者之間的同步成本等因素,最終選擇了 RPC BFF。
關(guān)于RPC BFF 更多介紹可以查看文章《攜程度假基于 RPC 和 TypeScript 的 BFF 設(shè)計與實踐》。
傳統(tǒng)服務(wù)開發(fā)中,服務(wù)提供方需要撰寫服務(wù)代碼,然后在第三方平臺同步接口文檔,消費者再根據(jù)文檔約定在不同調(diào)用環(huán)境中使用這些接口。
而在 RPC BFF 模式的開發(fā)中,服務(wù)提供方撰寫服務(wù)代碼之后,消費者只需通過一個命令就可以獲得接口的調(diào)用文檔和調(diào)用函數(shù)。這種方式不僅簡化了開發(fā)流程,還提高了接口調(diào)用的一致性和可靠性。
在使用RPC BFF的過程中,我們也總結(jié)了一些最佳實踐,希望能夠給大家?guī)硪恍﹩l(fā)。
6.1 收斂類型
在設(shè)計 RPC 接口的返回值時,有些開發(fā)者可能會沿用樸素函數(shù)的設(shè)計思路,即當接口返回成功時,返回一個成功標識和數(shù)據(jù);當失敗時,則返回一個失敗狀態(tài)和錯誤碼,讓消費端根據(jù)不同的錯誤碼進行相應(yīng)操作,如下所示:
這種設(shè)計存在以下幾個問題:
1)邏輯不夠清晰:簡單地將返回結(jié)果分為成功和失敗,實際開發(fā)中可能還有“可接受的錯誤”或其他復(fù)雜的狀態(tài),擴展性不佳。
2)錯誤碼維護復(fù)雜:前后端都需要維護同一套錯誤碼,增加了開發(fā)和維護成本。
3)錯誤處理遺漏:編碼時容易遺漏未處理的錯誤狀態(tài),增加了系統(tǒng)不穩(wěn)定性。
其實,可以使用 RPC BFF 的 Union 類型,將所有可能返回的狀態(tài)聯(lián)合起來,合并為最終返回的類型。
通過聚合所有返回結(jié)果,不再需要維護同一套錯誤碼,并且通過類型系統(tǒng)就能確保所有可能的狀態(tài)都被處理。如下圖所示,在消費返回結(jié)果時,代碼編輯器中有代碼提示,不會遺漏任何狀態(tài)。
如果接口或類型有更新,只需要一個命令就可以同步更新接口和接口類型。
6.2 原子化過程
RPC BFF 的核心理念是面向函數(shù)的接口編程,得益于它對底層通信細節(jié)的封裝,開發(fā)者只需考慮函數(shù)的功能即可。
與 RESTful 優(yōu)先資源的理念不同,RPC BFF 的原子是一個過程。REST 將資源暴露出來,而 RPC 是將過程暴露出來。
所以,可以通過分析服務(wù)流程(過程),將其拆解為最小不可分割的流程,再將這些流程通過函數(shù)實現(xiàn)。
只需保證所有原子化的過程都被很好地處理。在原子過程之上的過程調(diào)用,只需按需組合這些原子過程,從而形成一個過程流,最終形成一個有向無環(huán)圖(Directed Acyclic Graph)。
例如,有一個接口接收一個 ProjectId ,并返回該 ID 對應(yīng)的項目信息。如果未獲取到項目,則返回未找到。
getProjectById既可以對外暴露,也可以在其他函數(shù)中調(diào)用。
假設(shè)有一個業(yè)務(wù)流程,需要首先檢查項目是否存在,然后根據(jù)項目狀態(tài)執(zhí)行不同的操作。如下圖所示,在 getProjectSetting 中,我們調(diào)用接口的方式就像調(diào)用本地函數(shù)一樣,并且利用前面說的返回值類型組合,清晰地完成代碼邏輯。
通過這種方式,有以下幾個收益:
1)高復(fù)用性:原子化的函數(shù)可以在不同的業(yè)務(wù)場景中復(fù)用,減少代碼重復(fù)。
2)清晰明確:每個函數(shù)只負責一個具體的操作,邏輯清晰,易于維護和測試。
3)組合靈活:可以根據(jù)業(yè)務(wù)需求,靈活組合原子化過程,構(gòu)建復(fù)雜的業(yè)務(wù)邏輯。
七、結(jié)語
通過重新梳理整個自動化流程,我們拆分出了運行自動化任務(wù)的核心部分和輔助模塊。在保證自動化任務(wù)高可用的基礎(chǔ)上,提高了自動化的整體效率和容錯率??紤]到自動化場景的復(fù)雜性,我們特別設(shè)計了主進程與子進程的雙向通信機制,以應(yīng)對各種自動化場景的挑戰(zhàn)。
此外,我們還提供了兩個自動化場景案例,詳細分析TaskHub如何幫助提升自動化效率。
最后,我們結(jié)合團隊在RPC BFF的實踐,分享了一些使用經(jīng)驗。
未來,我們將繼續(xù)探索更多的自動化場景,并不斷完善TaskHub。