架構設計:多進程還是多線程

就像莎士比亞的“To be, or not to be, that is the question”始終困擾着哈姆雷特,對於“進程還是線程?”這個問題,也經常困擾着那些進行軟件架構設計的傢伙。所以今天打算聊一下我對這個問題的體會。假如你還搞不清楚線程和進程的區別,請先找本操作系統原理的書好好拜讀一下,再回來看帖。

  由於這個問題很容易引發口水戰,事先聲明如下:多進程和多線程,無法一概而論地說誰比誰好。因此本帖主要描述特定場景(與我所負責的產品相關)下,進程和線程的權衡經驗,僅供大夥兒參考。

  由於特定場景是本帖討論的前提,先說說我目前負責的產品的特點:業務邏輯比較複雜、業務數據量比較大、對數據實時處理的性能要求比較高、對健壯性和安全性要求比較高、要求跨平臺(包括操作系統、數據庫)、某些情況下需要分佈部署。

  上面說了一大堆,其實有不少的應用系統符合上述特點,比如:某些網絡遊戲服務器、某些金融行業的業務系統、某些電子商務的交易系統等等。如果你正在從事的是類似的應用系統的設計,希望我下面介紹的經驗對你有幫助。

   進程顆粒度問題

  大夥兒應該明白,進程和線程都是處理併發(concurrency)的手段。對於上述這種比較複雜的系統,如果你企圖全部用進程(見注1)或者全部用線程(見注2)來處理併發,估計會死得很難看。所以,關鍵問題就是如何在進程和線程之間進行平衡(也就是確定進程顆粒度的問題)。

  我個人建議,儘量以業務邏輯的單元來劃分進程。這樣做的好處有如下幾點:

  1、避免扯皮

  一般來說,某個固定業務邏輯的開發人員也是相對固定的。如果業務邏輯對應的某個進程崩潰了,測試人員容易快速定位肇事者,然後直接提交Bug給他/她。

  反之,一個進程搞得太龐大,N多人摻和在裏面,一旦進程崩潰了,相關編程人員之間很容易互相扯皮,不利於維護安定團結的局面;另外,由於測試人員經常搞不清楚Bug屬於誰,經常給錯Bug,也容易製造人民內部矛盾。

  從上面可以看出來,相對細的進程顆粒度能夠避免一些管理上的麻煩。由於XXX經常教導我們:“穩定壓倒一切”,所以該優點列第一條。

  2、健壯性、容錯性

  一般來說,開發人員的水平參差不齊,優秀的畢竟是少數(具體參見“二八原理系列”的帖子)。所以難免會有菜鳥程序員搞出低級錯誤,而有些低級錯誤是致命的,會導致進程的崩潰。

  如果你是以業務邏輯劃分進程,一個業務邏輯的進程崩潰,對其它業務邏輯的影響不大(除非是該業務邏輯的依賴方);因此就不會出現“注2”提到的問題。

  3、分佈式

  我常碰見的分佈式部署需求,一般都是按照業務邏輯的維度來劃分。比如系統中有一個認證模塊,裏面包含有敏感的用戶認證信息。這時候客戶就會要求把該模塊單獨部署在一臺經過安全加固的主機中(以防階級敵人搞破壞)。

  如果是以業務邏輯爲單位劃分進程,要滿足上述的部署需求就相對容易了(只要再配合恰當的進程間通訊機制,下面會提到)。

  另外,支持分佈式部署還可以順帶解決性能問題。比如某個業務邏輯模塊特別消耗硬件資源(比如內存、CPU、硬盤、帶寬),就可以把它拿出去單獨放一臺機器上跑。

  4、跨編程語言

  這個好處可能很多人容易忽略。一般來說,每個編程語言都有各自的優缺點。如果你通過業務邏輯劃分進程,就可以根據不同的業務邏輯的特點來選擇合適的編程語言。

  比如:對於性能敏感的模塊,我就使用C++搞定;而對於一些業務邏輯密集型的模塊,則使用Java或Python開發。

   進程間通訊(以下簡稱IPC)問題

  既然不可能把整個系統放入一個進程,那就必然會碰到IPC的問題。下面就來說一下該如何選擇IPC。

  各種操作系統裏面,有很多稀奇古怪的IPC類型。由於要考慮跨平臺,首先砍掉一批(關於IPC的跨平臺問題,我在“跨平臺開發”系列中會提到)。剩下的IPC類型中,能夠進行數據傳輸的IPC就不多了,主要有如下幾種:套接字(以下簡稱Socket)、共享內存、管道、文件。

  其中Socket是我強烈推薦的IPC方式,理由如下:使用Socket可以天然地支持分佈式部署;使用Socket可以比較容易地實現多種編程語言的混合(比如C++、Java、Python、Flex都支持Socket);使用Socket還可以省掉了一大坨“鎖操作”的代碼。

  列位看官中,或許有人在擔心Socket的性能問題,其實大可不必多慮。當兩個進程在本機上進行Socket通訊時,由於可以使用localhost環回地址,數據不用經過物理網卡,操作系統內核還可以進行某些優化。這種情況下,Socket相對其它幾種IPC機制,不會有太大的性能偏差。

  最後再補充一下,Socket方式也可以有效防止扯皮問題。舉個例子:張三寫了一個進程A,李四寫了一個進程B,進程A通過Socket方式發數據給進程B。突然有一天,兩個進程的通訊出故障了。然後張三就說是李四接收數據出錯;李四就說張三發送數據出錯。這時候怎麼辦捏?很簡單,隨便找個Sniffer軟件當場抓一下數據包並Dump出來看,問題就水落石出了。

   爲啥還要線程?

  上面說了這麼多進程的好處,有同學要問了:“那線程有什麼用捏?”總的來說,使用線程出於兩方面的考慮:性能因素和編碼方便。

  1、性能因素

  由於某些操作系統(比如Windows)中的進程比較重型,如果頻繁創建進程或者創建大量進程,會導致操作系統的負載過高。舉例如下:

  假設你要開發一個類似Web Server的應用。你針對每一個客戶端請求創建一個對應的進程用於進行數據交互(是不是想起了古老的CGI :-)。一旦這個系統擴容,用戶的併發連接數一增加,你的應用立馬死翹翹。

  上面的例子表明,跨平臺軟件系統的進程數要保持相對穩定。如果你的進程數會隨着某些環境因素呈線性增長,那就相當不妙了(順帶說一下,如果線程數會隨着環境因素呈線性增長,也相當不妙)。而根據業務邏輯的單元劃分進程,順便能達到“進程數的相對穩定”的效果。

  2、編碼方面

  由於業務邏輯內部的數據耦合比較緊密。如果業務邏輯內部的併發也用進程來實現,可能會導致大量的IPC編碼(任意兩個進程之間只要有數據交互,就得寫一坨IPC代碼)。這或許會讓相關的編程人員怨聲載道。

  當然,編碼方面的問題也不是絕對的。假如你的系統有很成熟且方便易用的IPC庫,可以比較透明地封裝IPC相關操作,那這方面的問題也就不存在了。

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章