原文來自微信公眾號:聊聊架構
在互聯網應用中,各式各樣的定時任務存於系統各個角落。我們希望由一個平台統一將這些作業管理起來。通過這個系統,作業的宕機、崩潰等狀態就可收入運維同學掌控,直接對接報警系統,將發現的掛掉作業再啟動就好。但一旦平台中運行大量的作業,發現異常作業並手動處理難免會感到繁瑣,而且人工處理帶來的誤操作以及時間差和7*24小時在線支持的要求都帶來了額外的成本。
什么是分布式定時任務中間件
最大限度的減少人工干預不僅是分布式定時任務中間件,也是所有的分布式中間件的核心價值所在。解決這個問題的方式就是高可用,讓作業在被系統發現宕機之后能自動切換。高可用和馬上要聊的彈性化可以一起看,彈性化可以認為是高可用的進階版本,在高可用的同時還能夠提升效率和充分利用資源。
如果作業處理的數據較多或計算量較大,單一實例效率不足的時候,最好可以做到增加一台服務器就能提升一部分的處理能力,也就是動態擴容。而一旦有運行中的服務器宕機,也不會影響整體的作業運行,無非是由於運行資源的減少而導致運行效率下降而已,這就是動態縮容。這通常采用分片的方式實現。它需要將一個任務拆分為多個獨立的任務項,然后由分布式的服務器分別執行某一個或幾個分片項。例如:有一個遍歷數據庫某張表的作業,現有2台服務器。為了快速的執行作業,那么每台服務器應執行作業的50%。 為滿足此需求,可將作業分成2片,每台服務器執行1片。作業遍歷數據的邏輯應為:服務器A遍歷ID以奇數結尾的數據;服務器B遍歷ID以偶數結尾的數據。 如果分成10片,則作業遍歷數據的邏輯應為:每片分到的分片項應為ID%10,而服務器A被分配到分片項0,1,2,3,4;服務器B被分配到分片項5,6,7,8,9,直接的結果就是服務器A遍歷ID以0-4結尾的數據;服務器B遍歷ID以5-9結尾的數據。而將將分片總數設置為1,並使用多於1台的服務器執行作業,作業將會以1主n從的方式執行。一旦執行作業的服務器崩潰,等待執行的服務器將會在下次作業啟動時替補執行。
另外還有一些作業完整性的需求,如:當作業執行時間過長造成下次該執行的作業的時間被錯過時,需要提供被錯過作業重執行機制;當一個作業分片失效時,需要立即在其他可運行節點運行,即失效轉移功能等。
無論是高可用、分片還是錯過作業重執行、失效轉移,都需要依托於一個注冊中心,用於記錄作業的狀態並且能夠進行分布式組件的協調,一般使用ZooKeeper居多。使用ZooKeeper的臨時節點和監聽功能可以有效的做到分布式的協調,用於發現新上線節點和處理已下線節點。
總結一下,分布式定時任務中間件的關注點從易到難是:集中化 -> 高可用 –> 彈性化。
去中心化和中心化
介紹完了關鍵功能點,接下來我們聊一下實現分布式定時任務中間件的兩種架構方案,去中心化和中心化。
去中心化架構是指所有的作業節點都是對等的。每個作業從注冊中心拉取自己的執行時間並且各自定時執行,執行時均使用作業服務器的本地時鍾,在作業無需分片調整時並不會對注冊中心產生寫操作,進而不會導致注冊中心更新緩存,因此執行效率很高,對注冊中心產生的壓力非常小。每個作業執行實例在執行時拉取僅屬於自己實例的作業分片(通過本地緩存即可,並不直接讀取注冊中心),並傳遞給業務代碼,供其根據所得分片項編寫業務邏輯。去中心化架構需要一個被選舉出來的主節點處理分片行為,僅分片行為不是去中心化的,需要集中處理。主節點是通過選舉獲得的非永久節點,一旦主節點的服務器宕機,則需要重新選舉主節點。當作業服務器的在線狀態發生變化時,則觸發ZooKeeper監聽,並設置需要重新分片的標記。在下次作業運行時,主節點將根據這個標記確定是否重分片並在需要時分片,分片時其他從節點一律處於阻塞狀態。主節點的唯一特別之處就是負責作業運行之前的分片,其他方面的運行節點別無二致,主節點也是一個作業運行節點。
去中心化架構的優點是輕量級,僅提供一個lib就可以與業務代碼一同工作,部署成本低,只需搭建注冊中心即可。缺點是如果各作業服務器時鍾不一致會產生同一作業的不同分片運行有先有后,缺乏統一調度。並且不能跨語言。
中心化架構將系統分為調度節點和執行節點。由調度節點發起作業的分片和執行,然后通過RPC發布給作業執行節點,或者通過寫注冊中心讓監聽注冊中心的作業執行節點主動觸發。同樣可以采用注冊中心來協調作業調度節點和執行節點的狀態,也可以將注冊中心退化為配置中心,專門用於存儲作業配置元數據,轉而由調度中心負責監聽各個執行節點的狀態。
中心化架構模式可以解決服務器時間差以及跨語言的問題(如果采用跨語言RPC或REST發送執行信令的方式)。缺點是部署和運維稍復雜,需要單獨部署調度節點並需要維護其高可用,這也會造成一定的資源浪費。
無論是中心化還是去中心化,在分布式的場景下由於網絡重試造成的順序不一致等原因,可能導致ZooKeeper的數據與真實運行的作業產生不一致,這種不一致通過正向的校驗無法完全避免。需要另外啟動一個線程定時校驗注冊中心數據與真實作業狀態的一致性,即通過檢測的方式維持最終一致性。
分布式定時任務中間件——Elastic-Job
Elastic-Job最初的版本分離於當當內部的應用框架ddframe,是一個純Java實現的分布式方案,參照dubbo的方式,提供無中心化解決方案。它采用all in jar的理念,使用時無需區分主從或調度、執行節點,一切都采用自我協調。Elastic-Job采用ZooKeeper作為注冊中心,用於處理作業的高可用、分片、失效轉移等分布式協調功能。每個使用Elastic-Job的應用都需要與ZooKeeper建立連接,這樣會造成ZooKeeper的連接過多,容易成為分布式ZooKeeper的瓶頸。它的分片邏輯根據IP地址抓取所在服務器的分片,IP地址不沖突是前提條件。Elastic-Job的架構圖如下:
此時,每個團隊自己搭建和管理各自的注冊中心和服務器,因此使用Elastic-Job的項目都散落在各處。
隨着私有雲的技術棧越來越普及,公司決定將業務平台逐漸遷入私有雲。作為入雲的第一步,選擇合理、風險易控的方向則很重要。經過多方面權衡,我們選擇了基於Mesos + 自研Framework的作業雲方案。而之前公司大量的使用Elastic-Job的項目都需要遷移,因此我們希望完全兼容Elastic-Job的原有API,所以Chronos等作業調度框架並不能滿足我們的需求。我們要做的是將Elastic-Job結合到雲平台,並增加結合硬件資源的掌控,以及部署發布自動化等功能。Elastic-Job本身包含Quartz,可以直接進行Job定時調度。因此,我們的最初想法是將Elastic-Job作為常駐服務啟動,用Kubernetes或Mesos + Marathon運行。
Elastic-Job當時並未考慮Cloud Native,設計本身存在一個問題,那就是分片是基於IP地址的。如果運行在私有雲,每個Job實例一旦分配到同一台Server,同樣的IP地址會導致Job分片沖突。雖然用CNI等網絡解決方案可以處理這方面的問題,但我們希望提供更加輕量級的解決方案。原因是我們不希望僅搭建公司級別的私有雲,更希望遵循公司的一貫理念,將整個體系開源。
涉及到中心調度的需求,就不能僅使用Mesos + Marathon,或者Kubernetes做簡單的治理了,需要自研一套框架來解決這些事情。當時的Kubernetes剛剛推出Multi-Scheduler,並未經過太多的驗證。因此自研的靈活度上,采用Tow-Level調度體系的Mesos比較容易滿足我們的需求。我們最終選定Mesos + 自定義Framework的方案搭建作業雲平台。為了與Elastic-Job兼容,作業雲沿用了Elastic-Job的API,新的基於Mesos的framework叫做Elastic-Job-Cloud,原Elastic-Job更名為Elastic-Job-Lite。
既然需要對Elastic-Job進行修改,那么其他需求也如雨后春筍一般冒了出來。最關鍵的需求是作業雲平台應該可以同時支持常駐和瞬時兩種作業。常駐作業就是之前提到的將Elastic-Job放到Marathon運行的形態。它由作業jar本身負責定時任務的調度,無論作業是否在執行,都會一直占有資源。常駐作業適合執行間隔短或初始化時間長的作業,以業務作業居多。比如訂單拉取作業,需要初始化Spring容器,建立各種資源的連接,然后執行復雜的業務邏輯,並且訂單拉取的時間間隔不會太長。另一種瞬時作業就像Chronos處理的方式,由中心調度,每次調度執行結束后都釋放占用的資源。它適合間隔時間長,資源占用高的作業,以報表作業居多。
Elastic-Job-Cloud包括Mesos Framework的Scheduler和Customized Executor兩部分。
Elastic-Job-Cloud放棄了采用ZooKeeper作為注冊中心的方案,轉而采用Mesos framework API提供的statusUpdate方法處理。statusUpdate的治理能力還是非常不錯的,通過對Executor回傳狀態的解讀來處理高可用、重新分片以及失效轉移等功能。ZooKeeper在Elastic-Job-Cloud中徹底退化為存儲媒介,僅用於存儲Job的Metadata和待運行隊列等狀態數據。之所以仍然采用ZooKeeper是為了避免采用過多的第三方依賴,保持和Mesos使用統一的技術棧。作業運行的實時狀態,由於大量的讀寫請求,放在ZooKeeper會極大的影響整個作業雲的性能,因此直接放在內存中。未來我們希望將Job Metadata以及隊列狀態遷移至etcd,希望Mesos支持etcd作為配置中心的版本盡快出現。
Elastic-Job-Cloud采用中心節點分片,直接將分片任務轉化為Mesos的TaskInfo,這樣就屏蔽了IP地址的限制。資源分配在Lite中是缺失的,Mesos這樣的平台可以將硬件資源和業務應用有機結合。我們最初開發的資源分配策略十分不穩定,無論是優先向一台Server分配,還是整個集群平均分配,算法都比較復雜,而且分配的維度不止一個。目前我們對CPU和Meomry資源進行管理,Disk,GPU資源等暫時未關注。經歷了幾次壓測,整個系統的資源利用率極不穩定,直到采用了由netflix開源的Fenzo,一個專注於Mesos資源分配的框架,一切問題迎刃而解。吐槽一下,這是用Java開發Mesos framework的福利,Fenzo至今還未有其他語言的版本。Fenzo除了能夠提供便利的資源分配策略之外,還能提供彈性資源伸縮分配等功能。但Fenzo使用不當會造成內存泄漏,關鍵點在於Fenzo會在內存中存儲當前已分配的Task,Task運行結束后需要釋放。但添加Task時,直接使用Fenzo生成的TaskAssigner對象,釋放時卻需要提供TaskID。Elastic-Job-Cloud的業務邏輯中TaskID中會包含Mesos的slaveID,用於通過TaskID回溯運行時狀態。但Fenzo需要分配Server前即提供TaskID,因此我們的解決方案是先提供一個偽造的SlaveID,在分配完成后替換為真實的SlaveID。這樣在記錄Task時,由於Fenzo API的關系,不太容易注意到TaskID的作用,注銷Task時使用真正的TaskID是無法清除的,而Fenzo未提供任何報錯或日志,非常容易造成內存泄漏。
Elastic-Job-Cloud的作業調度采用兩個隊列,Offer隊列用於收集Mesos分配的資源,Job隊列用於堆積待執行作業。當待執行作業可以從資源隊列中匹配到合適的資源時,才會分配並生成TaskInfo執行。這種方式對於運行在同一Mesos集群中的其他framework不太友好,可能在作業需求不多的情況下造成Offer的囤積。但對於Offer收取和Job執行同為異步的情況下,也沒什么其他更好的方案了。
Scheduler開發是自研Mesos framework的必需品,而Customized Executor則不然,很多Mesos framework直接使用Default Executor,已經能滿足基本需求。我們采用Customized Executor主要是兩方面原因。第一個原因是為了兼容Elastic-Job-Lite的API,另一個原因是為了做到更合理的資源利用。這兩個原因的根源在於Java。公司的很多項目都是Java開發的,Elastic-Job的API也是基於java,它根據Mesos分配好的分片處理相應的業務邏輯。像下面這樣:
更合理的資源利用說起來比較復雜。Java的生態圈非常繁雜,用於支撐業務的作業框架不能只做到簡單的能夠觸發調用,還需要和Spring這樣的容器深度融合,至少需要通過依賴注入獲取Spring容器中的bean。每次Job啟動都初始化Spring容器是很大的浪費。Elastic-Job-Cloud的Executor在第一次啟動時即實例化Spring容器,以后每次Job調度都復用Spring容器即可。以何種維度復用Executor是個值得探討的問題,一開始我們采用每個Job的維度復用Executor,如果一個Job分為10片,分配到2台Mesos的Agent,那么每個Executor會開啟多線程執行被分配到的分片項,而不同的Job則開啟不同的Executor。但是應用的需求是無止境的,這種方式針對某些需求就不合適。如果發生很多Job只是名字以及配置不同,但Jar相同,而這種Job又很多的情況下,為每個Job創建獨立的Executor就會造成資源過度浪費。舉個具體的例子會更容易理解:系統監控作業,監控各種RESTFul API的可用性,有的每分鍾監控一次,有的每5分鍾監控一次,而各API的URL又不同。每個監控作業都是一個獨立的Job,有不同的name和cron,但它們均來源於同一個Jar。針對這種情況,我們采用以Jar的App URL為維度復用Executor。成百上千個類似的Job匯聚成線程而不是進程,進一步節省資源。
Elastic-Job-Cloud的架構圖如下:
為了使開源的Elastic-Job系列更加完善,Elastic-Job-Lite仍舊是必不可少的組成部分。我們看到了Elastic-Job-Cloud的強大功能,也看到了Elastic-Job-Lite的巨大潛力,因此我們再度對Elastic-Job-Lite出手,並於近日發布了2.1.0的支持Cloud Native 里程碑版本。
新版本支持單服務器跑任意多的相同作業實例,原作業實例標識由IP地址替換為作業啟動瞬時產生的UUID。在新的Cloud Native架構下,作業物理服務器概念大幅弱化。為了向前兼容,作業物理服務器僅包含控制服務器是否可以禁用這一功能。為了更加純粹的實現作業核心,作業物理服務器統計和操作功能未來可能刪除,可下放至容器治理部署系統。
使用新增加的運行實例概念全面替換原服務器概念,Elastic-Job-Lite與容器治理系統的對接由原來的服務器維度轉變為運行實例維度,每個運行實例都是動態的,會隨着作業下線而消失。
習慣根據服務器靜態分配作業的使用者也不用太過擔心新版本帶來的變化,服務器信息以另外一個維度可選的存在於作業管理信息中,使用者仍然可以繼續使用服務器靜態部署的方式。
通過這次修改,Elastic-Job-Lite已經可以非常容易的對接Mesos + Marathon以及Kubernetes,相信使用者可以非常輕松的搭建一個深度定制版的作業私有雲。
並且在這次更新中,我們實現了自修復能力使穩定性進一步提升。剛才聊過,在分布式場景由於網絡重試導致的順序不一致,很難完全通過正向的方式保證每個節點間的狀態完全同步。而Elastic-Job原來的版本在網絡不穩定的情況下,可能發生主節點選舉卡死,或某個分片不運行的情況,可以通過重啟應用修復。在新版本中使用異步線程,定期(可配置時間間隔)檢測集群中不正確的狀態,以反向檢查的方式查找並自動修復分布式的不一致,為分布式穩定性的完善增加了最后一塊拼圖。
雖然從功能上看Elastic-Job-Cloud更加完善,但由於依托於Mesos,使用復雜度較高,不易整合進公司現有系統,對新手來說使用成本較高,因而更加輕量級的Elastic-Job-Lite在實際使用中更受青睞。Elastic-Job-Lite只需在pom.xml中引入Elastic-Job的maven坐標,並且參照在GitHub上的example編寫幾行代碼即可,唯一的外部依賴是ZooKeeper。
Elastic-Job已開源接近2年,目前為止已更新發布17次,提交千余次,GitHub的star數量也逐漸接近2000。它已在分布式作業領域占有一席之地,明確采納的公司已超過50家,未收錄的采納公司不計其數,更有多個開源產品衍生自Elastic-Job。
Elastic-Job項目的開源地址:https://github.com/dangdangdotcom/elastic-job
嘉賓簡介
張亮,目前是當當架構部負責人。主要負責分布式中間件以及私有雲平台的搭建。致力於開源,目前主導兩個開源項目elastic-job和sharding-jdbc。擅長以java為主分布式架構以及以Mesos為主的雲平台方向,推崇優雅代碼,對如何寫出具有展現力的代碼有較多研究。