如何让快递更快?菜鸟自研定时任务调度引擎首次公开
阿里妹導讀:網上購物的普及化帶動了物流行業的迅猛發展,同時也帶來了極大的壓力和嚴峻的考驗,特別是在電商大促的時節。如何有效提高整個物流鏈路的時效體驗,給消費者更好的體驗,這是菜鳥物流一直奮斗的目標。
今天,我們來深入了解菜鳥的輕量級定時任務調度引擎設計系統,學習如何在億級別包裹中快速定位運輸超時的包裹。
在中國物流快速發展的今天,日均包裹量已經突破1億,如何確保1億包裹在合理的時間之內送達收件人,并且能夠在收件人反饋之前,及時處理那些沒有在合理時間內運輸的包裹,從而提高物流整個鏈路的時效體驗,已經成為亟待解決的關鍵問題。
要想解決問題,首先要發現問題!有效、及時地發現問題離不開對這1億包裹運輸過程中每個環節實時監控,而要實現這個場景,就需要一個能夠支撐起如此量級的實時定時調度系統。這就是本文的主題:輕量級定時任務調度引擎的設計。
傳統方案
針對上文提到的問題場景,一般的做法是將定時任務寫入數據庫,通過一個線程定時查詢出將要到期的任務,再執行任務相關邏輯。該方案的優點是實現簡單,尤其適合單機或者業務量比較小的場景來。但是缺點也很明顯:在分布式且業務量較大的場景中會引入很多復雜性。首先,需要設計一套合理的分庫分表邏輯,以及集群任務負載邏輯。其次,即使做到這些,也會由于某些場景定時任務時間集中在某個時間點,導致集群單節點壓力過大。再次,需要合理的預估容量,否則后續線性存儲擴容將會非常復雜。
我們發現,上述方案的主要問題其實是定時任務時間和任務存儲的耦合。如果能夠將時間和存儲解耦,任務的存儲就等于是無狀態的,這樣對存儲的可選擇性范圍會大很多,對存儲的約束也大大降低!
下文將會介紹如何通過時間輪思想將定時任務的時間和任務本身進行解耦,從而在設計和性能上得到很大的提升。
時間輪基本介紹
時間輪方案將現實生活中的時鐘概念引入到軟件設計中,主要思路是定義一個時鐘周期(比如時鐘的12小時)和步長(比如時鐘的一秒走一次),當指針每走一步的時候,會獲取當前時鐘刻度上掛載的任務并執行,整體結構如圖1。
從上圖可以看到,對于時間的計算是交給一個類似時鐘的組件來做,而任務是通過一個指針或者引用去關聯某個刻度上到期的定時任務,這樣就能夠將定時任務的存儲和時間進行解耦,時鐘組件難度不大,以何種方式存儲這些任務數據,是時間輪方案的關鍵。
時間輪定時任務的存儲
我們發現,阿里巴巴MQ(RocketMQ商業化版本,后文統稱為阿里巴巴MQ)對其定時消息的改造[1]很有借鑒意義,老方案基于定時輪詢的方式check消息是否到期,這種方案對于離散的時間比較受限,所以也就導致老版本只能支持幾種延遲級別的定時消息。為了解決這個問題,阿里巴巴MQ團隊將原方案改造為基于時間輪+鏈表的方案,從而既能支持離散的定時消息,也能夠解決傳統時間輪每個刻度需要管理各自任務列表的復雜性。
圖2簡明的描述了方案的關鍵點,其設計思路就是將任務列表寫入到磁盤,并且在磁盤中采用鏈表的方式將任務列表串起來,要達到這種串起來的效果,需要每個任務中都有一個指向下一個任務的磁盤offset,只需要拿到鏈表的頭便可以獲取整個任務鏈表,于是在該方案中,時間輪的時間刻度不需要存儲所有的任務列表,只需要存儲鏈表的頭即可,從而將內存的壓力釋放出來。
該方案對于中間件這樣定位的系統來說是可以接受的,但對于一個定位在普通應用的系統來說,對部署的要求就顯得過高了,因為你需要一塊固定的硬盤。在當前容器化以及容量動態管理的趨勢下,一個普通應用需要依賴一塊固定的磁盤,對系統的運維和部署都會帶來額外的復雜度。于是在此基礎上形成本文重點介紹的輕量級定時任務調度引擎。
輕量級定時任務調度引擎
輕量級定時任務調度引擎借鑒了時間輪方案的核心思想,同時去除了系統對磁盤的依賴。既然不能將數據直接存儲在磁盤,那只能依托專門的存儲服務(比如Hbase,Redis等)來進行存儲。于是就將下層的存儲替換成了一種存儲服務能夠滿足的結構:
將任務設計成一種結構化的表,并且將上面的offset替換成了一個任務ID(圖2是文件的offset),并且通過ID將整個任務鏈表串起來,時間輪上只關聯鏈表頭的ID。這里對MQ方案改造的點只是將磁盤的offset替換成一個ID,從而解耦對磁盤的依賴。
方案到現在感覺可以行得通,但是又引出了另外兩個問題:
問題1:單一鏈表無法并行提取,從而影響提取效率,對于某個時刻有大量定時任務的時候,定時任務處理的延遲會比較嚴重;
問題2:既然任務不是存儲在本機磁盤了,表明整個集群的定時任務是集中存儲,而集群中各個節點都擁有自己的時間輪,那么集群里面每個節點重啟之后如何恢復?集群擴容&縮容如何自動管理?
任務鏈表分區——加速單一鏈表提取
鏈表的好處是在內存中不用存儲整個任務列表,而只需要存儲一個簡單的ID,這樣減少了內存的消耗,但是卻帶來了另一個問題。眾所周知,鏈表的特性是對寫友好,但讀的效率卻并不高,如果某個時刻需要掛載很長的任務鏈表,那鏈表的方式是完全不能利用并發來提高讀的效率。
如何能夠提高某個時間的任務隊列提取的效率呢?這里利用分區的原理,將某個時刻的單一鏈表通過分區的方式拆分成多個鏈表,當將某個時間點的任務提取的時候,可以根據鏈表集合大小來并行處理,從而可以加速整個任務提取的速度。所以方案調整成圖4:
集群管理——集群節點的自我識別
解決了定時任務的存儲問題和單一鏈表提取任務的效率問題,好像整個方案都已經ready了,但是還有另一個重要的問題前面沒有考慮進去,就是集群部署后節點重啟后如何進行恢復?比如需要知道重啟之前的時間輪刻度,需要知道重啟之間時間輪刻度上的定時任務鏈表數據等,后面我統一將這種數據稱之為時間輪元數據。如果任務寫入磁盤,某個機器的重啟,可以從本地磁盤加載當前節點的時間輪元數據來進行恢復;而如果不是通過磁盤來實現,那就帶來問題2-1:一臺機器在重啟之后怎么獲取它重啟之前的時間輪元數據呢?
通過上面的解決定時任務存儲問題,對于元數據的存儲也可以借助專門的存儲服務,但是由于集群的各個節點是無狀態的,所以各個節點啟動的時候,并不知道如何從存儲服務中讀取屬于自己的元數據(也就是查詢的條件),這便引出了問題2-2:集群節點怎么獲取屬于它的元數據呢?
可能第一想到的就是將元數據和節點ip或者mac地址關聯上,但是在現在動態容量調度的情況下,一個機器擁有一個固定的ip也是很奢侈的,因為你不能保證你的應用會運行在哪臺機器上。既然物理環境不能依賴,就只能依賴邏輯環境來對每個節點的時間輪進行區分了,于是就通過對集群每個節點分配一個在集群中唯一的邏輯ID,每臺機器通過自己拿到的ID去獲取時間輪數據。于是又引出了問題2-3:這個ID由誰來分配? 這就是Master節點。
在整個集群中需要選舉出一個節點為Master,所有的其他節點會將自己注冊到Master節點中,然后再由Master節點分配一個ID給各個節點,從而通過這個ID到存儲服務中獲取之前時間輪的元數據信息,最后初始化時間輪。圖5和圖6是該過程的描述圖:
圖5展示了節點注冊和獲取ID的過程,注意, Master節點自身也注冊并且分配了ID,這是因為對一個普通應用來說,并沒有Master和非Master之分,Master也是會參與整個業務的計算的,只不過它除了參與業務計算之外,額外還要進行集群的管理。
集群自動化管理——自動感知擴容&縮容
上文引出了Master節點,并且所有節點都會將自己注冊給Master(包括Master自己),于是基于Master就可以構建出一套集群管理的機制。對于普通應用來說,集群的擴容縮容是很正常的操作,在動態調度的場景下,擴縮容操作甚至連應用owner都不感知,那么集群能夠感知自己被擴容了和縮容了么?答案是肯定的,因為存在一個Master節點,而Master可以感知到集群的其他節點的存活狀態,Master發現集群的容量變化,從而做出集群的負載調整。
定時任務提取集群化——集群壓力軟負載
上文通過將時間輪上的某個時間關聯單一鏈表拆分成多個鏈表來提高提取任務的并發度,同時也已構建出了一套集群管理方案。如果這個并發度只是在單節點,只讓該節點自己提取,將會因為某個時刻的當前節點到期的任務數量很大,從而導致集群中某些節點的壓力會很大。為了能夠更加合理的利用集群計算能力,那么可以基于上面的集群管理能力,對方案再進一步優化:將提取的到期任務鏈表集合通過軟負載的方式分發給集群其他節點,同時由于任務的數據是集中存儲的,只要其他節點能夠拿到任務鏈表頭的ID,便可以提取得到該鏈表的所有任務,從而集群的壓力也將被平攤,圖7是該過程的交互過程:
總結
上面對整個方案的演進,以及各個階段遇到的問題進行了詳細的介紹,可能還有一些點沒有太深入,大家可以回復留言中詳細討論。下面結合上面的內容,做了一個簡單的流程梳理,以方便大家對全文的理解。
引用:[1] 消息的處理方法、裝置和電子設備:中國,201710071742.7[P].2017-10-07.
總結
以上是生活随笔為你收集整理的如何让快递更快?菜鸟自研定时任务调度引擎首次公开的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用开源项目的正确姿势,都是血和泪的总结
- 下一篇: 在阿里,我们如何管理代码分支?