序言
第1章 并行和分布式計算介紹
第2章 異步編程
第3章 Python的并行計算
第4章 Celery分布式應用
第5章 云平臺部署Python
第6章 超級計算機群使用Python
第7章 測試和調試分布式應用
第8章 繼續學習
無論大小的分布式應用,測試和調試的難度都非常大。因為是分布在網絡中的,各臺機器可能十分不同,地理位置也可能不同。
進一步的,使用的電腦可能有不同的用戶賬戶、不同的硬盤、不同的軟件包、不同的硬件、不同的性能。還可能在不同的時區。對于錯誤,分布式應用的開發者需要考慮所有這些。查錯的人需要面對所有的這些挑戰。
目前為止,本書沒有花多少時間處理錯誤,而是關注于開發和部署應用的工具。
在本章,我們會學習開發者可能會碰到的錯誤。我們還會學習一些解決方案和工具。
概述
測試和調試一個單體應用并不簡單,但是有許多工具可以使其變得簡單,包括pdb調試器,各種分析工具(有cProfile和line_profile),糾錯器(linter),靜態代碼分析工具,和許多測試框架,其中許多都包括于Python 3.3及更高版本的標準庫。
調試分布式應用的困難是,單進程應用調試的工具處理多進程時就失去了一部分功能,特別是當進程運行在不同的機器上時。
調試、分析用C、C++、Fortran語言寫成的分布式應用可以用工具,例如Intel VTune、Allinea MAP和DDT。但是Python開發者可用的工具極少,甚至沒有。
編寫小型和中型的分布式應用并不難。與單線程應用相比,寫多線程應用的難點是后者有許多依賴間組件,組件通常運行在不同的硬件上,必須要協調網絡。這就是為什么監控和調試分布代碼如此困難。
幸運的是,還是可以在Python分布式應用上使用熟悉的調試工具和代碼分析工具。但是,這些工具的作用有限,我們必須使用登錄和打印語句,以搞清錯誤在哪里。
常見錯誤——時鐘和時間
時間是一個易用的變量。例如,當將不同的數據流整合、數據庫排序、重建事件的時間線,使用時間戳是非常自然的。另外,一些工具(比如GMU make
)單純的依賴文件修改的時間,很容易被不同機器的錯誤時間搞混。
因為這些原因,在所有的機器進行時間同步是非常重要的。如果機器位于不同的時區,不僅要同步時間,還要根據UTC時間進行校準。當不能將時間調整為UTC時間時,建議代碼內部都是按照UTC來運行,只是在屏幕顯示的時候再轉化為本地時間。
通常,在分布式系統中進行時間同步是一個復雜的課題,超出了本書的范疇。大多數讀者,可以使用網絡時間協議(NTP),這是一個完美的同步解決方案。大多數操作系統都支持NTP。
關于時間,另一個需要考慮的是周期動作的計時,例如輪詢循環和定時任務。許多應用需要每隔一段時間就產生進程或進行動作(例如,發送email確認或檢查新的數據是否可用)。
常用的方法是使用定時器(使用代碼或使用OS工具),在某一時刻讓所有定時器啟動,通常是在某刻和一定時間段之后。這種方法的危險之處是,進程同一時刻開始工作,可能使系統過載。
一個常見的例子是啟動許多進程,這些進程都需要從一個共享硬盤讀取配置或數據。這種情況下,所有一切正常,知道進程的數量變得太大,以至于共享硬盤無法處理數據傳輸,就會導致應用變慢。
常見的解決方法是把計時器延遲,讓計時器分布在一個范圍之內。通常,因為我們不總是控制所有使用的代碼,讓計時器隨機延遲幾分鐘是可行的。
另一個例子是圖片處理服務,需要給隔一段時間就檢測新的數據。當發現新的圖片,就復制這些圖片、重命名、縮放、并轉換成常見的格式,最后存檔。如果不小心,同一時間上傳過多圖片,就會很容易使系統過載。
更好的方法是限制應用(使用隊列架構),只加載合理數量的圖片,而不使系統過載。
常見錯誤——軟件環境
另一個常見的問題是所有機器上安裝的軟件是一致的,升級也是一致的。
不過,往往用幾小時調試一個分布式系統,最后發現因為一些未知的原因,一些電腦上的代碼或軟件是舊版的。有時,還會發現該有的代碼反而沒有。
軟件存在差異的原因很多:可能是加載失敗,或部署過程中的錯誤,或者僅僅是人為的錯誤。
HPC中常用的解決方法是,在啟動應用之前,將代碼安裝在虛擬環境里。一些項目傾向于靜態的依賴鏈接,以免從動態庫加載出現錯誤。
當和安裝完整環境、軟件依賴和應用本身相比,這種方法適用于運行時較長的應用。實際意義不大。
幸好,Python可以創建虛擬環境??梢允褂脙蓚€工具pyvenv
(Python 3.5以上的標準庫支持)和virtualenv
(PyPI支持)。另外,使用pip
命令,可以指定包的版本。聯合使用這些工具,就可以控制執行環境。
但是,錯誤往往在細節,不同的節點可能有相同的虛擬環境,但是有不兼容的第三方庫。
對于這些問題,可以使用容器技術,例如Docker,或有版本控制的虛擬環境。
如果不能使用容器技術,就想到了HPC機群,最好的方法不是依賴系統軟件,而是自己管理環境和軟件棧。
常見問題——許可和環境
不同的電腦可能是在不同的用戶賬戶下運行我們的代碼,我們的應用可能想在一個特定的目錄下讀取文件或寫入數據,然后碰到了一個許可錯誤。即使我們的代碼使用的賬戶都是相同的,它們的環境可能是不同的。因此,設定的環境變量的值可能是錯誤的。
當我們的代碼使用特殊的低級用戶賬號運行時,這種問題就很常見。防御性的代碼,尤其是訪問環境碰到未定義值時,能返回默認設置是十分必要的。
一個常見的方法是,只在特定的用戶賬號下運行,這個賬號由自己控制,指定環境變量,和應用啟動文件(它的版本也是受控的)。
但是,一些系統不僅是在極度受限的賬戶下運行任務,而且還是限制在沙盒內。大多數時候,連接外網也是禁止的。此時,唯一的辦法就是本地設置完整環境,并復制到共享硬盤。其它的數據可以來自用戶搭建的,運行小任務的服務器。
通常來說,許可錯誤和用戶環境問題與軟件環境問題類似,應該協同處理。開發者往往想讓代碼盡可能獨立于環境,用虛擬環境裝下代碼和環境變量。
常見問題——硬件資源可用性
在給定的時間,我們的應用需要的硬件資源可能,也可能不可用。即使可用,也不能保證在相當長的時間內都可用。當網絡出現故障時,就容易碰到這個問題,并且很常見(尤其是對于移動app)。在實際中,很難將這種錯誤和機器或應用崩潰進行區分。
使用分布式框架和任務規劃器的應用經常需要依靠框架處理常見的錯誤。當發生錯誤或機器不可用時,一些任務規劃器還會重新提交任務。
但是,復雜的應用需要特別的策略應對硬件問題。有時,最好的方法是當資源可用時,再次運行應用。
其它時候,重啟的代價很大。此時,常用的方法是從檢查點重啟。也就是說,應用會周期的記錄狀態,所以可以從檢查點重啟。
如果從檢查點重啟,你需要平衡從中途重啟和記錄狀態造成的性能損失。另一個要考慮的是,增加了代碼的復雜性,尤其是使用多個進程或線程讀寫狀態信息。
好的方法是,可以快速重新創建的數據和結果不要寫入檢查點。或者,一些進程需要花費大量時間,此時使用檢查點非常合適。
例如,氣象模擬可能運行數周或數月。此時,每隔幾個小時就寫入檢查點是非常重要的,因為從頭開始成本太高。另外,上傳圖片和創建縮略圖的進程,它的運行特別快,就不需要檢查點。
安全起見,狀態的寫入和更新應該是不可分割的(例如,寫入臨時文件,只有在寫入完全的時候才能取代原先的文件)。
與HPC和AWS競價實例很相似,進程中的一部分會被從運行的機器驅趕出來。當這種情況發生時,通常會發送一個警告(信號SIGQUIT
),幾秒之后,這些進程就會被銷毀(信號SIGKILL
)。對于AWS競價實例,可以通過實例元數據的服務確定銷毀的時間。無論哪種情況,我們的應用都有時間來記錄狀態。
Python有強大的功能捕獲和處理信號(參考signal
模塊)。例如,下面的示例代碼展示了一個檢查點策略:
#!/usr/bin/env python3.5
"""
Simple example showing how to catch signals in Python
"""
import json
import os
import signal
import sys
# Path to the file we use to store state. Note that we assume
# $HOME to be defined, which is far from being an obvious
# assumption!
STATE_FILE = os.path.join(os.environ['HOME'],
'.checkpoint.json')
class Checkpointer:
def __init__(self, state_path=STATE_FILE):
"""
Read the state file, if present, and initialize from that.
"""
self.state = {}
self.state_path = state_path
if os.path.exists(self.state_path):
with open(self.state_path) as f:
self.state.update(json.load(f))
return
def save(self):
print('Saving state: {}'.format(self.state))
with open(self.state_path, 'w') as f:
json.dump(self.state, f)
return
def eviction_handler(self, signum, frame):
"""
This is the function that gets called when a signal is trapped.
"""
self.save()
# Of course, using sys.exit is a bit brutal. We can do better.
print('Quitting')
sys.exit(0)
return
if __name__ == '__main__':
import time
print('This is process {}'.format(os.getpid()))
ckp = Checkpointer()
print('Initial state: {}'.format(ckp.state))
# Catch SIGQUIT
signal.signal(signal.SIGQUIT, ckp.eviction_handler)
# Get a value from the state.
i = ckp.state.get('i', 0)
try:
while True:
i += 1
ckp.state['i'] = i
print('Updated in-memory state: {}'.format(ckp.state))
time.sleep(1)
except KeyboardInterrupt:
ckp.save()
我們可以在一個終端運行這段代碼,然后在另一個終端,我們發送一個信號SIGQUIT
(例如,-s SIGQUIT <process id>
)。如果這么做的話,我們可以看到檢查點的動作,如下圖所示:
筆記:使用分布式應用通常需要在性能不同、硬件不同、軟件不同的機器上運行。
即使有任務規劃器,幫助我們廁何時的軟件和硬件環境,我們必須記錄各臺機器的環境和性能。在高級的架構中,這些性能指標可以提高任務規劃的效率。
例如,PBS Pro,再次執行提交任務時就考慮了歷史性能。HTCondor持續給每臺機器打分,用于選擇節點和排名。
最讓人沒有辦法的情況是網絡問題或服務器過載,網絡請求的時間太長,就會導致代碼超時。這可能會導致我們認為服務使不可用的。這些暫時性的問題,是很難調試的。
困難——開發環境
另一個分布式系統常見的困難是搭建一個有代表性的開發和測試環境,尤其是對于個人小型團隊。開發環境最好能代表最糟糕的開發環境,可以讓開發者測試常見的錯誤,例如硬盤溢出、網絡延遲、間歇性網絡斷開,硬件、軟件失效等實際中會發生的故障。
大型團隊擁有開發和測試集群的資源,他們總是有專門的軟件質量團隊對我們的代碼進行壓力測試。
不幸的是,小團隊常常被迫在筆記本電腦上編寫代碼,并使用非常簡單(最好的情況?。┑挠蓛膳_或三臺虛擬機組成環境,它們運行在筆記本電腦上以模擬真實系統。
這種務實的方案是可行的,絕對比什么都沒有要好。然而,我們應該記住,虛擬機運行在同一主機上表現出不切實際的高可用性和較低的網絡延遲。此外,沒有人會意外升級它們,而不通知我們或使用錯誤的操作系統。這個環境太易于控制和穩定,不夠真實。
更接近現實的設置是創建一個小型開發集群,比如AWS,使用相同的VM鏡像,使用生產環境中相同的軟件棧和用戶帳戶。
簡而言之,很難找到替代品。對于基于云平臺的應用,我們至少應該在部署版本的小型版本上測試我們的代碼。對于HPC應用程序,我們應該使用測試集群、或集群的一部分,用于測試和開發。
理想情況下,我們最好在操作系統的一個克隆版本上進行開發。但是,考慮成本和簡易性,我們還是會使用虛擬機:因為它夠簡單,基本上是免費的,不用網絡連接,這一點很重要。
然而,我們應該記住分布式應用并不是很難編寫的,只是它們的故障模式比單機模式多的多。其中一些故障(特別是與數據訪問相關的),所以需要仔細地選擇架構。
在開發階段后期,糾正由錯誤假設所導致的架構選擇代價高昂。說服管理者盡早給我們提供所需的硬件資源通常是困難的。最后,這是一種微妙的平衡。
有效策略——日志
通常情況下,日志就像備份或吃蔬菜,我們都知道應該這樣做,但大多數人都忘記了。在分布式應用程序中,我們沒有其他選擇,日志是必不可少的。不僅如此,記錄一切都是必要的。
由于有許多不同的進程在遠程資源上運行,理解發生了什么的唯一方法是獲得日志信息并使其隨時可用,并且以易于檢索的格式/系統存儲。
在最低限度,我們應該記錄進程的啟動和退出時間、退出代碼和異常(如果有的話),所有的輸入參數,輸出,完整執行環境、執行主機名和IP,當前工作目錄,用戶帳戶以及完整應用配置,和所有的軟件版本。
如果出了問題,我們應該能夠使用這些信息登錄到同一臺機器(如果仍然可用),轉到同一目錄,并復制我們的代碼,重新運行。當然,完全復制執行環境可能做不到(通常是因為需要管理員權限)。
然而,我們應該始終努力模擬實際環境。這是任務規劃器的優點所在,它允許我們選擇指定的機器,并指定完整的任務環境,這使得復制錯誤更少。
記錄軟件版本(不僅是Python版本,還有使用的所有包的版本)可以診斷遠程機器上過時的軟件棧。Python包管理器,pip
,可以容易的獲取安裝的包:import pip; pip.main(['list'])
。import sys; print(sys.executable, sys.version_info)
可以顯示Python的位置和版本。
創建一個系統,使所有的類和函數調用發出具有相同等級的日志,而且是在對象生命周期的同一位置。常見的方法包括使用裝飾器、元類。這正是Python模塊autologging
(PyPI上有)的作用。
一旦日志就位,我們面臨的問題是在哪里存儲這些日志,對于大型應用,傳輸日志占用資源很多。簡單的應用可以將日志寫入硬盤的文本文件。更復雜的應用程序可能需要在數據庫中存儲這些信息(可以通過創建一個Python日志模塊的自定義處理程序完成)或專門的日志聚合器,如Sentry(https://getsentry.com)。
與日志密切相關的是監督。分布式應用程序可以有許多可移動組件,并且需要知道哪些機器處于繁忙狀態,以及哪些進程或任務當前正在運行、等待,或處于錯誤狀態。知道哪些進程比平時花費更長的時間,往往是一個重要的警告信號,表明可能有錯誤。
Python有一些監督方案(經常與日志系統集成)。比如Celery,推薦使用flower(http://flower.readthedocs.org)作為監督和控制。另外,HPC任務規劃器,往往缺少通用的監督方案。
在潛在問題變嚴重之前,最好就監測出來。實際上,監視資源(如可用硬盤空間和觸發器動作),甚至是簡單的email警告,當它們低于閾值時,監督是有用的。許多部門監督硬件性能和硬盤智能數據,以發現潛在問題。
這些問題更可能是運營而不是開發者感興趣的,但最好記住。監督也可以集成在我們的應用程序以執行適當的策略,來處理性能下降的問題。
有效策略——模擬組件
一個好的,雖然可能耗費時間和精力,測試策略是模擬系統的一些或全部組件。原因是很多:一方面,模擬軟件組件使我們能夠更直接地測試接口。此時,mock測試庫,如unittest.mock
(Python 3.5的標準庫),是非常有用的。
另一個模擬軟件組件的原因是,使組件發生錯誤以觀察應用的響應。例如,我們可以將增加REST API或數據庫的服務的響應時間,看看會發生什么。有時,超時會讓應用誤以為服務器崩潰。
特別是在設計和開發復雜分布式應用的早期階段,人們可能對網絡可用性、性能或服務響應時間(如數據庫或服務器)做出過于樂觀的假設。因此,使一個服務完全失效或修改它的功能,可以檢測出代碼中的錯誤。
Netflix Chaos Monkey (https://github.com/Netflix/SimianArmy)可以隨機使系統中的組件失效,用于測試應用的反應。
總結
用Python編寫或運行小型或中型分布式應用程序并不困難。我們可以利用許多高質量框架,例如,Celery、Pyro、各種任務規劃期,Twisted、,MPI綁定(本書中沒有討論),或標準庫的模塊multiprocessing
。
然而,真正的困難在于監視和調試應用,特別是因為大部分代碼并行運行在許多不同的、通常是遠程的計算機上。
潛藏最深的bug是那些最終產生錯誤結果的bug(例如,由于數據在過程中被污染),而不是引發一個異常,大多數框架都能捕獲并拋出。
遺憾的是,Python的監視和調試工具不像用來開發相同代碼的框架和庫那么功能完備。其結果是,大型團隊可以使用自己開發的、通常是非常專業的分布式調試系統,小團隊主要依賴日志和打印語句。
分布式應用和動態語言(尤其是Python)需要更多的關于調試方面的工作。
序言
第1章 并行和分布式計算介紹
第2章 異步編程
第3章 Python的并行計算
第4章 Celery分布式應用
第5章 云平臺部署Python
第6章 超級計算機群使用Python
第7章 測試和調試分布式應用
第8章 繼續學習