一分鐘讓你的程式支援佇列和併發

發表於2016-09-19

工作了的開發同學想必都會給運營、產品等同事跑過資料。在豆瓣,基本每個工程師都在用DPark,原理就是把任務拆分,利用DPark叢集,在多臺伺服器上同時執行這些任務可以快速的獲得結果。但是有些需求不能使用DPark,比如有頻繁的資料庫操作,想象一下,一跑起來就會出現大量叢集的程式連線資料庫,讓資料庫壓力驟增,甚至影響現有服務;

有些需求用DPark有點殺雞用了宰牛刀的感覺,佔用了DPark叢集資源,但是不用的話,跑一次任務就得幾十分鐘;如果醬廠外的同學也想這麼爽,或者我們在沒有DPark環境的地方(如本地)跑,怎麼達到這個目的呢?有點常識的同學馬上說,你這機器上是多核CPU麼?是的話,多程式唄。對,確實這樣會有非常明顯的速度的提高,但是你充分利用了多程式的併發了麼?

為啥這麼說呢?假設你有1w個小任務,要在一個16核CPU的伺服器執行,把任務按某種條件hash之後取16的餘數分配給對應的程式,這是不是效率可以提升到單程式的16倍呢?大部分場景下達不到,因為任務不均衡,也就是任務完成的時間是最後一個完成其全部任務的程式結束決定的,也就是那個短板,在它沒有完成前,剩下的那15個程式都是閒著,看著它,那這段時間就是沒有充分利用。

更有經驗的人說,可以使用佇列啊。這也是對的,我們把這1w個任務都丟給這個佇列,這16個程式執行完就從佇列中取,這樣就充分利用了吧?確實。我們再深入一下,有些任務是相對獨立的,比如寫檔案,1w個任務寫1w個檔案,大家互不相關,上述解法夠用。但是很多時候執行完任務需要反饋(也就是要執行的結果),這就是還需要額外一個佇列或者帶鎖的全域性變數之類的東西儲存這個中間值,不要笑我,之前還用SQLite之類的資料庫存過這種中間結果,等全部完成後再從資料庫把這些結果整理合並。

還要考慮程式的通用性,考慮多程式之間如何良好的通訊… 無論是新手還是老手都好煩啊。

DuangDuangDuang… 今天「一分鐘讓你的程式支援佇列和併發」

首先本文的原理可見PYMOTW的Implementing MapReduce with multiprocessing,也就是利用multiprocessing.Pool(程式池)的map方法實現純Python的MapReduce。由於篇幅我把英語註釋去掉:

為了幫助新手理解,我解釋下其中幾個點:

1. processes:源文章叫做num_worker,現在標準庫這個引數已經叫做processes了,如果是None,會賦值成CPU的數量。 啟動的程式數量要考慮資源競爭,對資料庫的訪問壓力等多方面內容,有時候多了反而變慢了。

2. chunksize:是每次取任務的數量,任務小的話可以一次批量的多取點。這個是經驗值。

3. chain表示把可迭代的物件串連起來組成一個新的大的迭代器。

這個SimpleMapReduce需要傳遞一個map函式和一個reduce函式,事實上就是執行2次self.pool.map,使用者可以忽略佇列的細節(但是嚴重推薦看一下原始碼的實現),第二次直接返回結果而已。當然這個例子中reduce函式可以不設定,也就是不關心結果。

那怎麼用呢?首先你要明確可拆分的單元,比如解析某些目錄下的檔案內容,那麼每個被解析的檔案就可以作為一個子任務;想獲得10w使用者的某些資料,那麼每個使用者就是一個子任務。注意單元也可以按照業務特點更集中,比如10w使用者我們可以按某種規則分組,100人為一個組,也就是一個單元。

最後我們驗證下這種方式是不是最好,首先這裡有一個應用日誌的目錄,目錄下有多個日誌檔案:

需求很簡單,遍歷目錄下的檔案,找到符合條件的日誌條目數量。程式碼放在Gist上面就不貼出來了。

我們挨個看看執行效果:

1. simple.py。單程式方式,結果如下:

COST: 249.42918396

也就是花了249秒!!那設想下,現在有T級別的日誌,更復雜的處理,你得等多久?

2. multiprocessing_queue.py。多程式 + 佇列的方式,把每個檔案作為任務放入佇列,啟動X個程式去獲取任務,最後放X個None到佇列中,如果獲取的任務是None,表示任務都執行完了,程式就結束。任務的執行結果放另外一個佇列,最後獲取全部的執行結果,這次沒有放None,而是捕捉get方法超時來判斷佇列中有沒有待執行的任務。同時,也測試了單倍和雙倍CPU個數(測試使用的伺服器為24核)的程式的執行效果的對比:

CPU個數:COST: 30.0309579372
CPU個數 * 2:COST: 32.4717597961

2個結論:

1. 速度比單程式只提高了8倍,其中一個原因是這個伺服器並不是閒置的,還在完成其他任務。

2. 可見程式更多並沒有提高執行效率,這個需要在實際工作中注意。

3. multiprocessing_pool.py,使用上述的SimpleMapReduce的方式,但是有一點需要注意,SimpleMapReduce的map函式的返回值的每一項都是鍵、符合數的元組(或列表),而之前的解析函式返回的就是符合的結果,所以我們得簡單封裝一下下:

執行的結果如下:

COST: 26.9822928905

看到了吧,你只是額外寫了一個map_wrapper,就實現了multiprocessing_queue.py裡面那一堆程式碼的功能。

相關文章