前言
mapreduce在我的理解裡一直都是java等語言的專利,介於python乃至於pypy的效能局限, 一直沒想過用python寫分布式任務,最多就是多workers從訊息佇列取任務執行這樣,但是最近一件事真的顛覆 了我對python的認識.
先說說起因
某天分享sed和awk,領導突發奇想讓我用一些顧問的實際工作需要去我們的大量資料裡面擷取想要的資料的需求作為一些示範的例子.其中有這樣一個需求(我去掉實際一些專業晦澀的用語,用實際的內容來表達):
需求
1. 有大量的gz壓縮檔, 找到其中某2天的資料, 每一行都是一條實際資料
2. 需要解壓縮每個檔案,遍曆每行找到用逗號隔開的第21列為16233,第23列為27188的行. 以第2列為鍵計算符合的數量
3. 在全部統計結果裡面根據值計算符合的鍵的數量: 比如{'a':2, 'b':1, 'c':1},結果就是{1:2, 2:1},也就是2次的有2個,1次的只有一個
分析
一上來真的想用awk來搞.但是和其他同事一聊,有幾個痛點:
1. 2天資料總量在400G以上,用awk還要保留2次雜湊結果-不可能用awk
2. 用python,據同事經驗說:只是解壓縮這些小檔案後讀取什麼都不做也大概1天多的時間,完全不能忍
3. 資料還沒有放到hadoop, 沒有其他更好更快的方法
解題思路:
最初我想做成這樣:
把需要處理的這些壓縮檔放到隊列裡面
啟動多進程出隊列裡面擷取要處理的檔案,執行,把符合的結果放到共用變數疊加
計算完成後從共用變數裡面或者資料在產生上面第三條的結果
但是今天講的是python得mapreduce,也就是我後續的版本,它源於偉大的Doug Hellmann的Implementing MapReduce with multiprocessing
#!/usr/bin/env python
#coding=utf-8
# python mapreduce 跑數實現
# Author: Dongweiming
import gzip
import time
import os
import glob
import collections
import itertools
import operator
import multiprocessing
class AdMapReduce(object):
def __init__(self, map_func, reduce_func, num_workers=None):
'''
num_workers: 不指定就是預設可用cpu的核心數
map_func: map函數: 要求返回格式類似:[(a, 1), (b, 3)]
reduce_func: reduce函數: 要求返回格式類似: (c, 10)
'''
self.map_func = map_func
self.reduce_func = reduce_func
self.pool = multiprocessing.Pool(num_workers)
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):
'''調用類的時候被觸發'''
# 其實都是借用multiprocessing.Pool.map這個函數, inputs是一個需要處理的列表,想想map函數
# chunksize表示每次給mapper的量, 這個根據需求調整效率
map_responses = self.pool.map(self.map_func, inputs, chunksize=chunksize)
# itertools.chain是把mapper的結果連結起來為一個可迭代的對象
partitioned_data = self.partition(itertools.chain(*map_responses))
# 大家想,上面的就是[(a, [1,2]), (b, [2,3]),列表中的數就是當時符合的次數,reduce就是吧列表符合項sum
reduced_values = self.pool.map(self.reduce_func, partitioned_data)
return reduced_values
def mapper_match(one_file):
'''第一次的map函數,從每個檔案裡面擷取符合的條目'''
output = []
for line in gzip.open(one_file).readlines():
l = line.rstrip().split(',')
if int(l[20]) == 16309 and int(l[22]) == 2656:
cookie = l[1]
output.append((cookie, 1))
return output
def reduce_match(item):
'''第一次的reduce函數,給相同的key做統計'''
cookie, occurances = item
return (cookie, sum(occurances))
def mapper_count(item):
'''第二次mapper函數,其實就是把某key的總數做鍵,但是值標1'''
_, count = item
return [(count, 1)]
def reduce_count(item):
'''第二次reduce函數'''
freq, occurances = item
return (freq, sum(occurances))
if __name__ == '__main__':
start = time.time()
input_files = glob.glob('/datacenter/input/2013-12-1[01]/*')
mapper = AdMapReduce(mapper_match, reduce_match)
cookie_feq = mapper(input_files)
mapper = AdMapReduce(mapper_count, reduce_count)
cookie_feq = mapper(cookie_feq)
cookie_feq.sort(key=operator.itemgetter(1))
for freq, count in cookie_feq:
print '{0}t{1}t{2}'.format(freq, count, freq*count)
#cookie_feq.reverse()
end = time.time()
print 'cost:', end - start
後話
哇,看python做mapreduce也是可以這樣優雅的, 我是用pypy跑下來,竟然只有了61分鐘….
但是其實他只是藉助mapreduce思想和多核的硬體基礎,其實pool做的還是檔案層級的處理.假如是少量的大檔案,就未必有這樣好的效果了.
我想很多時候這樣的工作都可以交給這個Admapreduce類來做