一、什麼是MapReduce
首先,將這個單詞分解為Map、Reduce。
- Map階段:在這個階段,輸入資料集被分割成小塊,並由多個Map任務處理。每個Map任務將輸入資料對映為一系列(key, value)對,並生成中間結果。
- Reduce階段:在這個階段,中間結果被重新分組和排序,以便相同key的中間結果被傳遞到同一個Reduce任務。每個Reduce任務將具有相同key的中間結果合併、計算,並生成最終的輸出。
舉個例子,在一個很長的字串中統計某個字元出現的次數。
from collections import defaultdict
def mapper(word):
return word, 1
def reducer(key_value_pair):
key, values = key_value_pair
return key, sum(values)
def map_reduce_function(input_list, mapper, reducer):
'''
- input_list: 字元列表
- mapper: 對映函式,將輸入列表中的每個元素對映到一個鍵值對
- reducer: 聚合函式,將對映結果中的每個鍵值對聚合到一個鍵值對
- return: 聚合結果
'''
map_results = map(mapper, input_list)
shuffler = defaultdict(list)
for key, value in map_results:
shuffler[key].append(value)
return map(reducer, shuffler.items())
if __name__ == "__main__":
words = "python best language".split(" ")
result = list(map_reduce_function(words, mapper, reducer))
print(result)
輸出結果為
[('python', 1), ('best', 1), ('language', 1)]
但是這裡並沒有體現出MapReduce的特點。只是展示了MapReduce的執行原理。
二、基於多執行緒實現MapReduce
from collections import defaultdict
import threading
class MapReduceThread(threading.Thread):
def __init__(self, input_list, mapper, shuffler):
super(MapReduceThread, self).__init__()
self.input_list = input_list
self.mapper = mapper
self.shuffler = shuffler
def run(self):
map_results = map(self.mapper, self.input_list)
for key, value in map_results:
self.shuffler[key].append(value)
def reducer(key_value_pair):
key, values = key_value_pair
return key, sum(values)
def mapper(word):
return word, 1
def map_reduce_function(input_list, num_threads):
shuffler = defaultdict(list)
threads = []
chunk_size = len(input_list) // num_threads
for i in range(0, len(input_list), chunk_size):
chunk = input_list[i:i+chunk_size]
thread = MapReduceThread(chunk, mapper, shuffler)
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
return map(reducer, shuffler.items())
if __name__ == "__main__":
words = "python is the best language for programming and python is easy to learn".split(" ")
result = list(map_reduce_function(words, num_threads=4))
for i in result:
print(i)
這裡的本質一模一樣,將字串分割為四份,並且分發這四個字串到不同的執行緒執行,最後將執行結果歸約。只不過由於Python的GIL機制,導致Python中的執行緒是併發執行的,而不能實現並行,所以在python中使用執行緒來實現MapReduce是不合理的。(GIL機制:搶佔式執行緒,不能在同一時間執行多個執行緒)。
三、基於多程序實現MapReduce
由於Python中GIL機制的存在,無法實現真正的並行。這裡有兩種解決方案,一種是使用其他語言,例如C語言,這裡我們不考慮;另一種就是利用多核,CPU的多工處理能力。
from collections import defaultdict
import multiprocessing
def mapper(chunk):
word_count = defaultdict(int)
for word in chunk.split():
word_count[word] += 1
return word_count
def reducer(word_counts):
result = defaultdict(int)
for word_count in word_counts:
for word, count in word_count.items():
result[word] += count
return result
def chunks(lst, n):
for i in range(0, len(lst), n):
yield lst[i:i + n]
def map_reduce_function(text, num_processes):
chunk_size = (len(text) + num_processes - 1) // num_processes
chunks_list = list(chunks(text, chunk_size))
with multiprocessing.Pool(processes=num_processes) as pool:
word_counts = pool.map(mapper, chunks_list)
result = reducer(word_counts)
return result
if __name__ == "__main__":
text = "python is the best language for programming and python is easy to learn"
num_processes = 4
result = map_reduce_function(text, num_processes)
for i in result:
print(i, result[i])
這裡使用多程序來實現MapReduce,這裡就是真正意義上的並行,依然是將資料切分,採用並行處理這些資料,這樣才可以體現出MapReduce的高效特點。但是在這個例子中可能看不出來很大的差異,因為資料量太小。在實際應用中,如果資料集太小,是不適用的,可能無法帶來任何收益,甚至產生更大的開銷導致效能的下降。
四、在100GB的檔案中檢索資料
這裡依然使用MapReduce的思想,但是有兩個問題
- 檔案太大,讀取速度慢
解決方法:
使用分塊讀取,但是在分割槽時不宜過小。因為在建立分割槽時會被序列化到程序,在程序中又需要將其解開,這樣反覆的序列化和反序列化會佔用大量時間。不宜過大,因為這樣建立的程序會變少,可能無法充分利用CPU的多核能力。
- 檔案太大,記憶體消耗特別大
解決方法:
使用生成器和迭代器,但需獲取。例如分塊為8塊,生成器會一次讀取一塊的內容並且返回對應的迭代器,以此類推,這樣就避免了讀取記憶體過大的問題。
from datetime import datetime
import multiprocessing
def chunked_file_reader(file_path:str, chunk_size:int):
"""
生成器函式:分塊讀取檔案內容
- file_path: 檔案路徑
- chunk_size: 塊大小,預設為1MB
"""
with open(file_path, 'r', encoding='utf-8') as file:
while True:
chunk = file.read(chunk_size)
if not chunk:
break
yield chunk
def search_in_chunk(chunk:str, keyword:str):
"""在檔案塊中搜尋關鍵字
- chunk: 檔案塊
- keyword: 要搜尋的關鍵字
"""
lines = chunk.split('\n')
for line in lines:
if keyword in line:
print(f"找到了:", line)
def search_in_file(file_path:str, keyword:str, chunk_size=1024*1024):
"""在檔案中搜尋關鍵字
file_path: 檔案路徑
keyword: 要搜尋的關鍵字
chunk_size: 檔案塊大小,為1MB
"""
with multiprocessing.Pool() as pool:
for chunk in chunked_file_reader(file_path, chunk_size):
pool.apply_async(search_in_chunk, args=(chunk, keyword))
if __name__ == "__main__":
start = datetime.now()
file_path = "file.txt"
keyword = "張三"
search_in_file(file_path, keyword)
end = datetime.now()
print(f"搜尋完成,耗時 {end - start}")
最後程式執行時間為兩分鐘左右。