1. 程式人生 > >Python筆記——分散式程序

Python筆記——分散式程序

在Thread和Process中,應當優選Process,因為Process更穩定,而且,Process可以分佈到多臺機器上,而Thread最多隻能分佈到同一臺機器的多個CPU上。

Python的multiprocessing模組不但支援多程序,其中managers子模組還支援把多程序分佈到多臺機器上。一個服務程序可以作為排程者,將任務分佈到其他多個程序中,依靠網路通訊。由於managers模組封裝很好,不必瞭解網路通訊的細節,就可以很容易地編寫分散式多程序程式。

舉個例子:如果我們已經有一個通過Queue通訊的多程序程式在同一臺機器上執行,現在,由於處理任務的程序任務繁重,希望把傳送任務的程序和處理任務的程序分佈到兩臺機器上。怎麼用分散式程序實現?

原有的Queue可以繼續使用,但是,通過managers模組把Queue通過網路暴露出去,就可以讓其他機器的程序訪問Queue了。

我們先看服務程序,服務程序負責啟動Queue,把Queue註冊到網路上,然後往Queue裡面寫入任務:

# task_master.py

import random, time, queue
from multiprocessing.managers import BaseManager

# 傳送任務的佇列:
task_queue = queue.Queue()
# 接收結果的佇列:
result_queue = queue.Queue()

# 從BaseManager繼承的QueueManager:
class QueueManager(BaseManager):
    pass

# 把兩個Queue都註冊到網路上, callable引數關聯了Queue物件:
QueueManager.register('get_task_queue', callable=lambda: task_queue)
QueueManager.register('get_result_queue', callable=lambda: result_queue)
# 繫結埠5000, 設定驗證碼'abc':
manager = QueueManager(address=('', 5000), authkey=b'abc')
# 啟動Queue:
manager.start()
# 獲得通過網路訪問的Queue物件:
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放幾個任務進去:
for i in range(10):
    n = random.randint(0, 10000)
    print('Put task %d...' % n)
    task.put(n)
# 從result佇列讀取結果:
print('Try get results...')
for i in range(10):
    r = result.get(timeout=10)
    print('Result: %s' % r)
# 關閉:
manager.shutdown()
print('master exit.')

請注意,當我們在一臺機器上寫多程序程式時,建立的Queue可以直接拿來用,但是,在分散式多程序環境下,新增任務到Queue不可以直接對原始的task_queue進行操作,那樣就繞過了QueueManager的封裝,必須通過manager.get_task_queue()獲得的Queue介面新增。

然後,在另一臺機器上啟動任務程序(本機上啟動也可以):

# task_worker.py

import time, sys, queue
from multiprocessing.managers import BaseManager

# 建立類似的QueueManager:
class QueueManager(BaseManager):
    pass

# 由於這個QueueManager只從網路上獲取Queue,所以註冊時只提供名字:
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')

# 連線到伺服器,也就是執行task_master.py的機器:
server_addr = '127.0.0.1'
print('Connect to server %s...' % server_addr)
# 埠和驗證碼注意保持與task_master.py設定的完全一致:
m = QueueManager(address=(server_addr, 5000), authkey=b'abc')
# 從網路連線:
m.connect()
# 獲取Queue的物件:
task = m.get_task_queue()
result = m.get_result_queue()
# 從task佇列取任務,並把結果寫入result佇列:
for i in range(10):
    try:
        n = task.get(timeout=1)
        print('run task %d * %d...' % (n, n))
        r = '%d * %d = %d' % (n, n, n*n)
        time.sleep(1)
        result.put(r)
    except Queue.Empty:
        print('task queue is empty.')
# 處理結束:
print('worker exit.')

任務程序要通過網路連線到服務程序,所以要指定服務程序的IP。

現在,可以試試分散式程序的工作效果了。先啟動task_master.py服務程序:

$ python3 task_master.py 
Put task 3411...
Put task 1605...
Put task 1398...
Put task 4729...
Put task 5300...
Put task 7471...
Put task 68...
Put task 4219...
Put task 339...
Put task 7866...
Try get results...

task_master.py程序傳送完任務後,開始等待result佇列的結果。現在啟動task_worker.py程序:

$ python3 task_worker.py
Connect to server 127.0.0.1...
run task 3411 * 3411...
run task 1605 * 1605...
run task 1398 * 1398...
run task 4729 * 4729...
run task 5300 * 5300...
run task 7471 * 7471...
run task 68 * 68...
run task 4219 * 4219...
run task 339 * 339...
run task 7866 * 7866...
worker exit.

task_worker.py程序結束,在task_master.py程序中會繼續打印出結果:

Result: 3411 * 3411 = 11634921
Result: 1605 * 1605 = 2576025
Result: 1398 * 1398 = 1954404
Result: 4729 * 4729 = 22363441
Result: 5300 * 5300 = 28090000
Result: 7471 * 7471 = 55815841
Result: 68 * 68 = 4624
Result: 4219 * 4219 = 17799961
Result: 339 * 339 = 114921
Result: 7866 * 7866 = 61873956

這個簡單的Master/Worker模型有什麼用?其實這就是一個簡單但真正的分散式計算,把程式碼稍加改造,啟動多個worker,就可以把任務分佈到幾臺甚至幾十臺機器上,比如把計算n*n的程式碼換成傳送郵件,就實現了郵件佇列的非同步傳送。

Queue物件儲存在哪?注意到task_worker.py中根本沒有建立Queue的程式碼,所以,Queue物件儲存在task_master.py程序中:

                                             │
┌─────────────────────────────────────────┐     ┌──────────────────────────────────────┐
│task_master.py                           │  │  │task_worker.py                        │
│                                         │     │                                      │
│  task = manager.get_task_queue()        │  │  │  task = manager.get_task_queue()     │
│  result = manager.get_result_queue()    │     │  result = manager.get_result_queue() │
│              │                          │  │  │              │                       │
│              │                          │     │              │                       │
│              ▼                          │  │  │              │                       │
│  ┌─────────────────────────────────┐    │     │              │                       │
│  │QueueManager                     │    │  │  │              │                       │
│  │ ┌────────────┐ ┌──────────────┐ │    │     │              │                       │
│  │ │ task_queue │ │ result_queue │ │<───┼──┼──┼──────────────┘                       │
│  │ └────────────┘ └──────────────┘ │    │     │                                      │
│  └─────────────────────────────────┘    │  │  │                                      │
└─────────────────────────────────────────┘     └──────────────────────────────────────┘
                                             │

                                          Network

Queue之所以能通過網路訪問,就是通過QueueManager實現的。由於QueueManager管理的不止一個Queue,所以,要給每個Queue的網路呼叫介面起個名字,比如get_task_queue

authkey有什麼用?這是為了保證兩臺機器正常通訊,不被其他機器惡意干擾。如果task_worker.pyauthkeytask_master.pyauthkey不一致,肯定連線不上。

小結

Python的分散式程序介面簡單,封裝良好,適合需要把繁重任務分佈到多臺機器的環境下。

注意Queue的作用是用來傳遞任務和接收結果,每個任務的描述資料量要儘量小。比如傳送一個處理日誌檔案的任務,就不要傳送幾百兆的日誌檔案本身,而是傳送日誌檔案存放的完整路徑,由Worker程序再去共享的磁碟上讀取檔案。