一分鐘讓你的程序支持隊列和并發(fā)

作者:董偉明
工作了的開發(fā)同學(xué)想必都會給運營、產(chǎn)品等同事跑過數(shù)據(jù)。在豆瓣,基本每個工程師都在用DPark**,原理就是把任務(wù)拆分,利用DPark集群,在多臺服務(wù)器上同時運行這些任務(wù)可以快速的獲得結(jié)果。但是有些需求不能使用DPark,比如有頻繁的數(shù)據(jù)庫操作,想象一下,一跑起來就會出現(xiàn)大量集群的進(jìn)程連接數(shù)據(jù)庫,讓數(shù)據(jù)庫壓力驟增,甚至影響現(xiàn)有服務(wù);有些需求用DPark有點殺雞用了宰牛刀的感覺,占用了DPark集群資源,但是不用的話,跑一次任務(wù)就得幾十分鐘;如果醬廠外的同學(xué)也想這么爽,或者我們在沒有DPark環(huán)境的地方(如本地)跑,怎么達(dá)到這個目的呢?

有點常識的同學(xué)馬上說,你這機器上是多核CPU么?是的話,多進(jìn)程唄。對,確實這樣會有非常明顯的速度的提高,但是你充分利用了多進(jìn)程的并發(fā)了么?

為啥這么說呢?假設(shè)你有1w個小任務(wù),要在一個16核CPU的服務(wù)器運行,把任務(wù)按某種條件hash之后取16的余數(shù)分配給對應(yīng)的進(jìn)程,這是不是效率可以提升到單進(jìn)程的16倍呢?大部分場景下達(dá)不到,因為任務(wù)不均衡,也就是任務(wù)完成的時間是最后一個完成其全部任務(wù)的進(jìn)程結(jié)束決定的,也就是那個短板,在它沒有完成前,剩下的那15個進(jìn)程都是閑著,看著它,那這段時間就是沒有充分利用。

更有經(jīng)驗的人說,可以使用隊列啊。這也是對的,我們把這1w個任務(wù)都丟給這個隊列,這16個進(jìn)程執(zhí)行完就從隊列中取,這樣就充分利用了吧?確實。我們再深入一下,有些任務(wù)是相對獨立的,比如寫文件,1w個任務(wù)寫1w個文件,大家互不相關(guān),上述解法夠用。但是很多時候執(zhí)行完任務(wù)需要反饋(也就是要執(zhí)行的結(jié)果),這就是還需要額外一個隊列或者帶鎖的全局變量之類的東西存儲這個中間值,不要笑我,之前還用SQLite之類的數(shù)據(jù)庫存過這種中間結(jié)果,等全部完成后再從數(shù)據(jù)庫把這些結(jié)果整理合并。

還要考慮程序的通用性,考慮多進(jìn)程之間如何良好的通信... 無論是新手還是老手都好煩啊。
DuangDuangDuang... 今天「一分鐘讓你的程序支持隊列和并發(fā)」
首先本文的原理可見PYMOTW的Implementing MapReduce with multiprocessing**,也就是利用multiprocessing.Pool(進(jìn)程池)的map方法實現(xiàn)純Python的MapReduce。由于篇幅我把英語注釋去掉:

import collections
import itertools
import multiprocessing
 
 
class SimpleMapReduce(object):
    
    def __init__(self, map_func, reduce_func=None, processes=None):
        self.map_func = map_func
        self.reduce_func = reduce_func
        self.pool = multiprocessing.Pool(processes)
    
    def partition(self, mapped_values):
        partitioned_data = collections.defaultdict(list)
        for key, value in mapped_values:
            partitioned_data[key].append(value)
        return partitioned_data.items()
    
    def __call__(self, inputs, chunksize=1):
        map_responses = self.pool.map(self.map_func, inputs, chunksize=chunksize)
        if self.reduce_func is None:
            return
        partitioned_data = self.partition(itertools.chain(*map_responses))
        reduced_values = self.pool.map(self.reduce_func, partitioned_data)
        return reduced_values

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

  1. processes:源文章叫做num_worker,現(xiàn)在標(biāo)準(zhǔn)庫這個參數(shù)已經(jīng)叫做processes了,如果是None,會賦值成CPU的數(shù)量。 啟動的進(jìn)程數(shù)量要考慮資源競爭,對數(shù)據(jù)庫的訪問壓力等多方面內(nèi)容,有時候多了反而變慢了。
  2. chunksize:是每次取任務(wù)的數(shù)量,任務(wù)小的話可以一次批量的多取點。這個是經(jīng)驗值。
  3. chain表示把可迭代的對象串連起來組成一個新的大的迭代器。
    這個SimpleMapReduce需要傳遞一個map函數(shù)和一個reduce函數(shù),事實上就是執(zhí)行2次self.pool.map,使用者可以忽略隊列的細(xì)節(jié)(但是嚴(yán)重推薦看一下源碼的實現(xiàn)),第二次直接返回結(jié)果而已。當(dāng)然這個例子中reduce函數(shù)可以不設(shè)置,也就是不關(guān)心結(jié)果。
    那怎么用呢?首先你要明確可拆分的單元,比如解析某些目錄下的文件內(nèi)容,那么每個被解析的文件就可以作為一個子任務(wù);想獲得10w用戶的某些數(shù)據(jù),那么每個用戶就是一個子任務(wù)。注意單元也可以按照業(yè)務(wù)特點更集中,比如10w用戶我們可以按某種規(guī)則分組,100人為一個組,也就是一個單元。
    最后我們驗證下這種方式是不是最好,首先這里有一個應(yīng)用日志的目錄,目錄下有多個日志文件:
? du -sh /logs/bran
 5.5G   /logs/bran

需求很簡單,遍歷目錄下的文件,找到符合條件的日志條目數(shù)量。代碼放在Gist**上面就不貼出來了。
我們挨個看看運行效果:

  1. simple.py**。單進(jìn)程方式,結(jié)果如下:
    COST: 249.42918396
    也就是花了249秒!!那設(shè)想下,現(xiàn)在有T級別的日志,更復(fù)雜的處理,你得等多久?
  2. multiprocessing_queue.py**。多進(jìn)程 + 隊列的方式,把每個文件作為任務(wù)放入隊列,啟動X個進(jìn)程去獲取任務(wù),最后放X個None到隊列中,如果獲取的任務(wù)是None,表示任務(wù)都執(zhí)行完了,進(jìn)程就結(jié)束。任務(wù)的執(zhí)行結(jié)果放另外一個隊列,最后獲取全部的執(zhí)行結(jié)果,這次沒有放None,而是捕捉get方法超時來判斷隊列中有沒有待執(zhí)行的任務(wù)。同時,也測試了單倍和雙倍CPU個數(shù)(測試使用的服務(wù)器為24核)的進(jìn)程的執(zhí)行效果的對比:
    CPU個數(shù):COST: 30.0309579372
    CPU個數(shù) * 2:COST: 32.4717597961
    2個結(jié)論:
  3. 速度比單進(jìn)程只提高了8倍,其中一個原因是這個服務(wù)器并不是閑置的,還在完成其他任務(wù)。
  4. 可見進(jìn)程更多并沒有提高運行效率,這個需要在實際工作中注意。
  5. multiprocessing_pool.py**,使用上述的SimpleMapReduce的方式,但是有一點需要注意,SimpleMapReduce的map函數(shù)的返回值的每一項都是鍵、符合數(shù)的元組(或列表),而之前的解析函數(shù)返回的就是符合的結(jié)果,所以我們得簡單封裝一下下:
def map_wrapper(*args, **kwargs):
    matches = file_parser(*args, **kwargs)
    return [(match, 1) for match in matches]

執(zhí)行的結(jié)果如下:
COST: 26.9822928905
看到了吧,你只是額外寫了一個map_wrapper,就實現(xiàn)了multiprocessing_queue.py**里面那一堆代碼的功能。

今年第六屆大會PyConChina2016,由PyChina.org發(fā)起,CPyUG/TopGeek 等社區(qū)協(xié)辦,將在2016年9月10日(上海)9月23日(深圳)10月15日(北京)地舉辦的針對Python開發(fā)者所舉辦的最盛大和權(quán)威的Python相關(guān)技術(shù)會議,由PyChina社區(qū)主辦,致力于推動各類Python相關(guān)的技術(shù)在互聯(lián)網(wǎng)、企業(yè)應(yīng)用等領(lǐng)域的研發(fā)和應(yīng)用。

您可以點擊此處
了解更多詳情,或者掃描下圖二維碼:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容