分布式系統中的進程標識

來源:互聯網
上載者:User

陳碩 (giantchen_AT_gmail)

Blog.csdn.net/Solstice

昨天跟朋友聊天,談到了分布式系統中如何為進程取標識符(process identifier),寫篇部落格簡單總結一下我的觀點。

本文假定一台機器 (host) 只有一個 IP,不考慮 multihome 的情況。同時假定分布式系統中的每一台機器都正確運行了 NTP,各台機器的時間大體同步。

“進程 process”是作業系統的兩大基本概念之一,指的是在記憶體中啟動並執行程式。在日常交流中,“進程”這個詞通常不止這一個意思。有時候我們會說 “httpd 進程”或者“mysqld 進程”,指的其實是 program,而不一定是特指某一個“進程”——某一次 fork() 系統調用的產物。一個“httpd 進程”重啟了,它還是“一個 httpd 進程”。本文討論的是,如何為一個程式每次運行的進程取一個唯一識別碼。也就是說,httpd 程式第一次運行,進程是 httpd_1,它原地重啟了,進程是 httpd_2。

本文所指的“進程標識符”是用來唯一標識一個程式的“一次運行”的。每次啟動一個進程,這個進程應該被賦予一個唯一的標識符,與當前正在啟動並執行所有進程都不同;不僅如此,它應該與曆史上曾經運行過,目前已消亡的進程也都不同(這兩條的直接推論是,與將來可能啟動並執行進程也都不同)。“為每個進程命名”在分布式系統中有相當大的實際意義,特別是在考慮 failover 的時候。因為一個程式重啟之後的新進程和它的“前世進程”的狀態通常不一樣,凡是與它打交道的其他進程(s)最好能通過它的進程標識符變更來很容易地判斷該程式已經重啟,而採取必要的救災措施,防止搭錯話。

本文先假定每個服務端程式的連接埠是靜態分配的,在公司內部有一個公用 wiki 來記錄連接埠和程式的對應關係(然後通過 NIS 或 DNS 發布)。比如連接埠 11211 始終對應 memcached,其他程式不會使用 11211 連接埠;3306 始終留給 mysqld;3690 始終留給 svnserve。在分布式系統的初級階段,這是通常的做法;到了進階階段,多半會用動態分配連接埠號碼,因為連接埠號碼只有 6 萬多個,是稀缺資源,在公司內部也有分配完的一天。本文只考慮 TCP 協議,不考慮 UDP 協議,“連接埠”都指的是 TCP 通訊埠。

另外,我們假定在一台機器上,一個 listening port 同時只能由一個進程使用,不考慮古老的 listen() + fork() 模型(多個進程可以 accept 同一個連接埠上進來的串連),關於這點陳碩已經寫的很多,見《Linux 新增系統調用的啟示》《多線程伺服器的適用場合》。

錯誤做法

在分布式系統中,如何指涉(refer to)某一個進程呢,或者說一個進程如何取得自己的全域識別碼 (以下簡稱 gpid)?容易想到的有兩種做法:

  • ip:port (port 是這個進程對外提供網路服務的連接埠號碼,一般就是它的 tcp listening port)
  • host:pid

而這兩種做法都有問題。為什嗎?

如果進程本身是無狀態的,或者重啟了也沒有關係,那麼用 ip:port 來標識一個“服務”是沒問題的,比如常見的 httpd 和 memcached 都可以用它們的慣用 port (80 和 11211)來標識。我們可以在其他程式裡安全地引用(refer to)“運行在 10.0.0.5:80 的那個 http 伺服器”,或者“10.0.0.6:11211 的 memcached”,就算這兩個 service 重啟了,也不會有太惡劣的後果,大不了用戶端重試一下,或者自動切換到備用地址。

如果服務是有狀態的,那麼 ip:port 這種標識方法就有大問題,因為用戶端無法區分從頭到尾和自己打交道的是一個進程還是先後多個進程。在開發服務端程式的時候,為了能快速重啟,我們一般都會設定 SO_REUSEADDR,這樣的結果是前一秒鐘站在 10.0.0.7:8888 後面的進程和後一秒鐘佔據 10.0.0.7:8888 的進程可能不相同——服務端程式快速重啟了。

比方說,考慮一個類似 GFS 的Distributed File System的 master,如果它僅以 ip:port 來標識自己,然後它向 shadows (不是 chunk server)下達同步指令,那麼 shadows 如何得知 master 是不是已經重啟呢?髮指令的是 master 的“前世”還是“今生”?是不是應該拒絕“前世”的遺命?

如果考慮改成 host:pid 這種標識方式會不會好一點?我認為換湯不換藥,因為 pid 的狀態空間很小,重複的機率比較大。比如 Linux 的 pid 的最大值是 32768 (/proc/sys/kernel/pid_max),一個程式重啟之後,獲得與“前世”相同 pid 的機率是 1/32768。或許有讀者不相信重啟之後 pid 會重複,因為 pid 是遞增的,遇到上限再回到目前閒置最小 pid。考慮一個服務端程式 A,它的 pid 是 1234,它已經穩定運行了好幾天,這期間,pid 已經增長了幾個輪迴(因為這台機器時常會啟動一些 scripts 執行一些輔助工作)。在 A 崩潰的前一刻,最近被使用的 pid 已經回到了 1232,當 A 崩潰之後,某個守護進程啟動一個指令碼(pid = 1233)來清理 A 的 log,然後再重啟 A 程式;這樣一來,重啟之後的 A 程式的 pid 碰巧和它的前世相同,都是 1234。也就是說,用 host:pid 不能唯一標識進程。

那麼合在一起,用 ip:port:pid 呢?也不能做到唯一。它和 host:pid 面臨的問題是一樣的,因為 ip:port 這部分在重啟之後不會變,pid 可能輪迴。

我猜這時有人會想,建一個中心伺服器,專門分配系統的 gpid 好了,每個進程啟動的時候向它詢問自己的 gpid。這錯得更遠:這個全域 pid 分配器的 gpid 由誰來定?如何保證它分配的 gpid 不重複(考慮這個程式也可能意外重啟)?它是不是成為系統的 single point of failure?如果要對該 gpid 分配器做容錯,是不是面臨分布式系統的基本問題:狀態遷移?

還有一種辦法,用一個足夠強的隨機數做 gpid,這樣一來確實不會重複,但是這個 gpid 本身也沒有多大額外的意義,不便於管理和維護(比方說根據 gpid 找到是哪個機器上啟動並執行哪個進程)。

正確做法

正確做法:以四元組 ip:port:start_time:pid 作為分布式系統中進程的 gpid,其中 start_time 是 64-bit 整數,表示進程的啟動時刻(UTC 時區,muduo::Timestamp)。理由如下:

  • 容易保證唯一性。如果程式短時間重啟,那麼兩個進程的 pid 必定不重複(還沒有走完一個輪迴:就算每秒建立 1000 個進程,也要 30 多秒才會輪迴,而以這麼高的速度建立進程的話,伺服器已基本癱瘓了。);如果程式運行了相當長一段時間再重啟,那麼兩次啟動的 start_time 必定不重複。(見下文關於時間重複的解釋)
  • 產生這種 gpid 的成本很低(幾次低成本系統調用),沒有用到全域伺服器,不存在 single point of failure。
  • gpid 本身有意義,根據 gpid 立刻就能知道是什麼進程(port),運行在哪台機器(ip),是什麼時間啟動的,在 /proc 目錄中的位置 (/proc/pid) 等,進程的資源使用方式也可以通過運行在那台機器上的監控程式報告出來。
  • gpid 具有曆史意義,便於將來追溯。比方說進程 crash,那麼我知道它的 gpid,就可以去記錄中查詢它 crash 之前的 cpu/mem 負載有多大。

如果僅以 ip:port:start_time 作為 gpid,則不能保證唯一性,如果程式短時間重啟(間隔一秒或幾秒),start_time 可能會往回跳變(NTP 在調時間)或暫停(正好處於閏秒期間)。關於時間跳變的問題留給下一篇部落格《〈程式中的日期與時間〉第二章:計時與定時》,簡單地說,電腦上的時鐘不一定是單調遞增的。

沒有 port 怎麼辦?一般來說,一個網路服務程式會偵聽某個連接埠來提供服務,如果它是個純粹的用戶端,只主動發起串連,沒有主動偵聽連接埠,gpid 該如何分配呢?根據陳碩在《分布式系統的工程化開發方法》一文中的觀點“在程式裡內建 http 伺服器”,分布式系統中的每個長期啟動並執行、會與其他機器打交道的進程都應該提供一個管理介面,對外提供一個維修探查通道,可以查看進程的全部狀態。這個管理介面就是一個 TCP server,它會偵聽某個 port。

使用這樣的維修通道的一個額外好處是,可以自動防止重複啟動程式。因為如果重複啟動,bind 到那個營運 port 的時候會出錯(連接埠已被佔用),程式會立刻退出。更妙的是,不用擔心進程 crash 沒來得及清理鎖(如果用跨進程的 mutex 就有這個風險),進程關閉的時候作業系統會自動把它開啟的 port 都關上,下一個進程可以順利啟動。

進一步,還可以把程式的名稱和版本號碼作為 gpid 的一部分,這起到錦上添花的作用。

TCP 協議的啟示

我在《分布式系統的工程化開發方法》中提到“從 TCP 協議能學到什嗎?”,今天講的這個 gpid 其實也是由 TCP 協議啟發而來。TCP 用 ip:port 來表示 endpoint,兩個 endpoint 構成一個 socket。這似乎符合一開始提到的以 ip:port 來標識進程的做法。其實不然。在發起 TCP 串連的時候,為了防止前一次同樣地址的串連(相同的 local_ip:local_port:remote_ip:remote_port)的幹擾(稱為 wandering duplicates,即流浪的 packets),TCP 協議使用 seq 號碼(這種在 SYN packet 裡第一次發送的 seq 號碼稱為 initial sequence number, ISN)來區分本次串連和以往的串連。TCP 的這種思路與我們防止進程的“前世”幹擾“今生”很相像。核心每次建立 TCP 串連的時候會設法遞增 ISN 以確保與上次串連最後使用的 seq 號碼不同。相當於說把 start_time 加入到了 endpoint 之中,這就很接近我們後面提到的“正確的 gpid”做法了。(當然,原始 BSD 4.4 的 ISN 產生演算法有安全性漏洞,會導致 TCP sequence prediction attack,Linux 核心已經採用更安全的辦法來產生 ISN。)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.