- 相關(guān)推薦
C語(yǔ)言分布式系統(tǒng)中的進(jìn)程標(biāo)識(shí)
如何為一個(gè)程序每次運(yùn)行 的進(jìn)程取一個(gè)唯一標(biāo)識(shí)符。也就是說(shuō),httpd 程序第一次運(yùn)行,進(jìn)程是 httpd_1,它原地重啟了,進(jìn)程是 httpd_2。下面是小編為大家搜索整理的C語(yǔ)言分布式系統(tǒng)中的進(jìn)程標(biāo)識(shí),希望大家能有所收獲,更多精彩內(nèi)容請(qǐng)及時(shí)關(guān)注我們應(yīng)屆畢業(yè)生考試網(wǎng)!
“進(jìn)程 process”是操作系統(tǒng)的兩大基本概念之一,指的是在內(nèi)存中運(yùn)行的程序。在日常交流中,“進(jìn)程”這個(gè)詞通常不止這一個(gè)意思。有時(shí)候我們會(huì)說(shuō) “httpd 進(jìn)程”或者“mysqld 進(jìn)程”,指的其實(shí)是 program,而不一定是特指某一個(gè)“進(jìn)程”——某一次 fork() 系統(tǒng)調(diào)用的產(chǎn)物。一個(gè)“httpd 進(jìn)程”重啟了,它還是“一個(gè) httpd 進(jìn)程”。本文討論的是,如何為一個(gè)程序每次運(yùn)行 的進(jìn)程取一個(gè)唯一標(biāo)識(shí)符。也就是說(shuō),httpd 程序第一次運(yùn)行,進(jìn)程是 httpd_1,它原地重啟了,進(jìn)程是 httpd_2。
本文所指的“進(jìn)程標(biāo)識(shí)符”是用來(lái)唯一標(biāo)識(shí)一個(gè)程序的“一次運(yùn)行”的。每次啟動(dòng)一個(gè)進(jìn)程,這個(gè)進(jìn)程應(yīng)該被賦予一個(gè)唯一的標(biāo)識(shí)符,與當(dāng)前正在運(yùn)行的所有進(jìn)程都不同;不僅如此,它應(yīng)該與歷史上曾經(jīng)運(yùn)行過(guò),目前已消亡的進(jìn)程也都不同(這兩條的直接推論是,與將來(lái)可能運(yùn)行的進(jìn)程也都不同)。“為每個(gè)進(jìn)程命名”在分布式系統(tǒng)中有相當(dāng)大的實(shí)際意義,特別是在考慮 failover 的時(shí)候。因?yàn)橐粋(gè)程序重啟之后的新進(jìn)程和它的“前世進(jìn)程”的狀態(tài)通常不一樣,凡是與它打交道的其他進(jìn)程(s)最好能通過(guò)它的進(jìn)程標(biāo)識(shí)符變更來(lái)很容易地判斷該程序已經(jīng)重啟,而采取必要的救災(zāi)措施,防止搭錯(cuò)話。
本文先假定每個(gè)服務(wù)端程序的端口是靜態(tài)分配的,在公司內(nèi)部有一個(gè)公用 wiki 來(lái)記錄端口和程序的對(duì)應(yīng)關(guān)系(然后通過(guò) NIS 或 DNS 發(fā)布)。比如端口 11211 始終對(duì)應(yīng) memcached,其他程序不會(huì)使用 11211 端口;3306 始終留給 mysqld;3690 始終留給 svnserve。在分布式系統(tǒng)的初級(jí)階段,這是通常的做法;到了高級(jí)階段,多半會(huì)用動(dòng)態(tài)分配端口號(hào),因?yàn)槎丝谔?hào)只有 6 萬(wàn)多個(gè),是稀缺資源,在公司內(nèi)部也有分配完的一天。本文只考慮 TCP 協(xié)議,不考慮 UDP 協(xié)議,“端口”都指的是 TCP 端口。
另外,我們假定在一臺(tái)機(jī)器上,一個(gè) listening port 同時(shí)只能由一個(gè)進(jìn)程使用,不考慮古老的 listen() + fork() 模型(多個(gè)進(jìn)程可以 accept 同一個(gè)端口上進(jìn)來(lái)的連接),關(guān)于這點(diǎn)陳碩已經(jīng)寫(xiě)的很多,見(jiàn)《Linux 新增系統(tǒng)調(diào)用的啟示 》《多線程服務(wù)器的適用場(chǎng)合 》。
錯(cuò)誤做法
在分布式系統(tǒng)中,如何指涉(refer to)某一個(gè)進(jìn)程呢,或者說(shuō)一個(gè)進(jìn)程如何取得自己的全局標(biāo)識(shí)符 (以下簡(jiǎn)稱 gpid)?容易想到的有兩種做法:
*ip:port (port 是這個(gè)進(jìn)程對(duì)外提供網(wǎng)絡(luò)服務(wù)的端口號(hào),一般就是它的 tcp listening port)
*host:pid
而這兩種做法都有問(wèn)題。為什么?
如果進(jìn)程本身是無(wú)狀態(tài)的,或者重啟了也沒(méi)有關(guān)系,那么用 ip:port 來(lái)標(biāo)識(shí)一個(gè)“服務(wù)”是沒(méi)問(wèn)題的,比如常見(jiàn)的 httpd 和 memcached 都可以用它們的慣用 port (80 和 11211)來(lái)標(biāo)識(shí)。我們可以在其他程序里安全地引用(refer to)“運(yùn)行在 10.0.0.5:80 的那個(gè) http 服務(wù)器”,或者“10.0.0.6:11211 的 memcached”,就算這兩個(gè) service 重啟了,也不會(huì)有太惡劣的后果,大不了客戶端重試一下,或者自動(dòng)切換到備用地址。
如果服務(wù)是有狀態(tài)的,那么 ip:port 這種標(biāo)識(shí)方法就有大問(wèn)題,因?yàn)榭蛻舳藷o(wú)法區(qū)分從頭到尾和自己打交道的是一個(gè)進(jìn)程還是先后多個(gè)進(jìn)程。在開(kāi)發(fā)服務(wù)端程序的時(shí)候,為了能快速重啟,我們一般都會(huì)設(shè)置 SO_REUSEADDR,這樣的結(jié)果是前一秒鐘站在 10.0.0.7:8888 后面的進(jìn)程和后一秒鐘占據(jù) 10.0.0.7:8888 的進(jìn)程可能不相同——服務(wù)端程序快速重啟了。
比方說(shuō),考慮一個(gè)類似 GFS 的分布式文件系統(tǒng)的 master,如果它僅以 ip:port 來(lái)標(biāo)識(shí)自己,然后它向 shadows (不是 chunk server)下達(dá)同步指令,那么 shadows 如何得知 master 是不是已經(jīng)重啟呢?發(fā)指令的是 master 的“前世”還是“今生”?是不是應(yīng)該拒絕“前世”的遺命?
如果考慮改成 host:pid 這種標(biāo)識(shí)方式會(huì)不會(huì)好一點(diǎn)?我認(rèn)為換湯不換藥,因?yàn)?pid 的狀態(tài)空間很小,重復(fù)的概率比較大。比如 Linux 的 pid 的最大值是 32768 (/proc/sys/kernel/pid_max),一個(gè)程序重啟之后,獲得與“前世”相同 pid 的概率是 1/32768;蛟S有讀者不相信重啟之后 pid 會(huì)重復(fù),因?yàn)?pid 是遞增的,遇到上限再回到目前空閑的最小 pid?紤]一個(gè)服務(wù)端程序 A,它的 pid 是 1234,它已經(jīng)穩(wěn)定運(yùn)行了好幾天,這期間,pid 已經(jīng)增長(zhǎng)了幾個(gè)輪回(因?yàn)檫@臺(tái)機(jī)器時(shí)常會(huì)啟動(dòng)一些 scripts 執(zhí)行一些輔助工作)。在 A 崩潰的前一刻,最近被使用的 pid 已經(jīng)回到了 1232,當(dāng) A 崩潰之后,某個(gè)守護(hù)進(jìn)程啟動(dòng)一個(gè)腳本(pid = 1233)來(lái)清理 A 的 log,然后再重啟 A 程序;這樣一來(lái),重啟之后的 A 程序的 pid 碰巧和它的前世相同,都是 1234。也就是說(shuō),用 host:pid 不能唯一標(biāo)識(shí)進(jìn)程。
那么合在一起,用 ip:port:pid 呢?也不能做到唯一。它和 host:pid 面臨的問(wèn)題是一樣的,因?yàn)?ip:port 這部分在重啟之后不會(huì)變,pid 可能輪回。
我猜這時(shí)有人會(huì)想,建一個(gè)中心服務(wù)器,專門分配系統(tǒng)的 gpid 好了,每個(gè)進(jìn)程啟動(dòng)的時(shí)候向它詢問(wèn)自己的 gpid。這錯(cuò)得更遠(yuǎn):這個(gè)全局 pid 分配器的 gpid 由誰(shuí)來(lái)定?如何保證它分配的 gpid 不重復(fù)(考慮這個(gè)程序也可能意外重啟)?它是不是成為系統(tǒng)的 single point of failure?如果要對(duì)該 gpid 分配器做容錯(cuò),是不是面臨分布式系統(tǒng)的基本問(wèn)題:狀態(tài)遷移?
還有一種辦法,用一個(gè)足夠強(qiáng)的隨機(jī)數(shù)做 gpid,這樣一來(lái)確實(shí)不會(huì)重復(fù),但是這個(gè) gpid 本身也沒(méi)有多大額外的意義,不便于管理和維護(hù)(比方說(shuō)根據(jù) gpid 找到是哪個(gè)機(jī)器上運(yùn)行的哪個(gè)進(jìn)程)。
正確做法:以四元組 ip:port:start_time:pid 作為分布式系統(tǒng)中進(jìn)程的 gpid,其中 start_time 是 64-bit 整數(shù),表示進(jìn)程的啟動(dòng)時(shí)刻(UTC 時(shí)區(qū),muduo::Timestamp)。理由如下:
*容易保證唯一性。如果程序短時(shí)間重啟,那么兩個(gè)進(jìn)程的 pid 必定不重復(fù)(還沒(méi)有走完一個(gè)輪回:就算每秒創(chuàng)建 1000 個(gè)進(jìn)程,也要 30 多秒才會(huì)輪回,而以這么高的速度創(chuàng)建進(jìn)程的話,服務(wù)器已基本癱瘓了。);如果程序運(yùn)行了相當(dāng)長(zhǎng)一段時(shí)間再重啟,那么兩次啟動(dòng)的 start_time 必定不重復(fù)。(見(jiàn)下文關(guān)于時(shí)間重復(fù)的解釋)
*產(chǎn)生這種 gpid 的成本很低(幾次低成本系統(tǒng)調(diào)用),沒(méi)有用到全局服務(wù)器,不存在 single point of failure。
*gpid 本身有意義,根據(jù) gpid 立刻就能知道是什么進(jìn)程(port),運(yùn)行在哪臺(tái)機(jī)器(ip),是什么時(shí)間啟動(dòng)的,在 /proc 目錄中的位置 (/proc/pid) 等,進(jìn)程的資源使用情況也可以通過(guò)運(yùn)行在那臺(tái)機(jī)器上的監(jiān)控程序報(bào)告出來(lái)。
*gpid 具有歷史意義,便于將來(lái)追溯。比方說(shuō)進(jìn)程 crash,那么我知道它的 gpid,就可以去歷史記錄中查詢它 crash 之前的 cpu/mem 負(fù)載有多大。
如果僅以 ip:port:start_time 作為 gpid,則不能保證唯一性,如果程序短時(shí)間重啟(間隔一秒或幾秒),start_time 可能會(huì)往回跳變(NTP 在調(diào)時(shí)間)或暫停(正好處于閏秒期間)。關(guān)于時(shí)間跳變的問(wèn)題留給下一篇博客《〈程序中的日期與時(shí)間〉第二章:計(jì)時(shí)與定時(shí)》,簡(jiǎn)單地說(shuō),計(jì)算機(jī)上的時(shí)鐘不一定是單調(diào)遞增的。
沒(méi)有 port 怎么辦?一般來(lái)說(shuō),一個(gè)網(wǎng)絡(luò)服務(wù)程序會(huì)偵聽(tīng)某個(gè)端口來(lái)提供服務(wù),如果它是個(gè)純粹的客戶端,只主動(dòng)發(fā)起連接,沒(méi)有主動(dòng)偵聽(tīng)端口,gpid 該如何分配呢?根據(jù)陳碩在《分布式系統(tǒng)的工程化開(kāi)發(fā)方法 》一文中的觀點(diǎn)“在程序里內(nèi)置 http 服務(wù)器”,分布式系統(tǒng)中的每個(gè)長(zhǎng)期運(yùn)行的、會(huì)與其他機(jī)器打交道的進(jìn)程都應(yīng)該提供一個(gè)管理接口,對(duì)外提供一個(gè)維修探查通道,可以查看進(jìn)程的全部狀態(tài)。這個(gè)管理接口就是一個(gè) TCP server,它會(huì)偵聽(tīng)某個(gè) port。
使用這樣的維修通道的一個(gè)額外好處是,可以自動(dòng)防止重復(fù)啟動(dòng)程序。因?yàn)槿绻貜?fù)啟動(dòng),bind 到那個(gè)運(yùn)維 port 的時(shí)候會(huì)出錯(cuò)(端口已被占用),程序會(huì)立刻退出。更妙的是,不用擔(dān)心進(jìn)程 crash 沒(méi)來(lái)得及清理鎖(如果用跨進(jìn)程的 mutex 就有這個(gè)風(fēng)險(xiǎn)),進(jìn)程關(guān)閉的時(shí)候操作系統(tǒng)會(huì)自動(dòng)把它打開(kāi)的 port 都關(guān)上,下一個(gè)進(jìn)程可以順利啟動(dòng)。
進(jìn)一步,還可以把程序的名稱和版本號(hào)作為 gpid 的一部分,這起到錦上添花的作用。
TCP 協(xié)議的啟示
我在《分布式系統(tǒng)的工程化開(kāi)發(fā)方法 》中提到“從 TCP 協(xié)議能學(xué)到什么?”,今天講的這個(gè) gpid 其實(shí)也是由 TCP 協(xié)議啟發(fā)而來(lái)。TCP 用 ip:port 來(lái)表示 endpoint,兩個(gè) endpoint 構(gòu)成一個(gè) socket。這似乎符合一開(kāi)始提到的以 ip:port 來(lái)標(biāo)識(shí)進(jìn)程的做法。其實(shí)不然。在發(fā)起 TCP 連接的時(shí)候,為了防止前一次同樣地址的連接(相同的 local_ip:local_port:remote_ip:remote_port)的干擾(稱為 wandering duplicates ,即流浪的 packets),TCP 協(xié)議使用 seq 號(hào)碼(這種在 SYN packet 里第一次發(fā)送的 seq 號(hào)碼稱為 initial sequence number, ISN)來(lái)區(qū)分本次連接和以往的連接。TCP 的這種思路與我們防止進(jìn)程的“前世”干擾“今生”很相像。內(nèi)核每次新建 TCP 連接的時(shí)候會(huì)設(shè)法遞增 ISN 以確保與上次連接最后使用的 seq 號(hào)碼不同。相當(dāng)于說(shuō)把 start_time 加入到了 endpoint 之中,這就很接近我們后面提到的“正確的 gpid”做法了。(當(dāng)然,原始 BSD 4.4 的 ISN 生成算法有安全漏洞,會(huì)導(dǎo)致 TCP sequence prediction attack,Linux 內(nèi)核已經(jīng)采用更安全的辦法來(lái)生成 ISN。)
【C語(yǔ)言分布式系統(tǒng)中的進(jìn)程標(biāo)識(shí)】相關(guān)文章:
解析Linux系統(tǒng)中的進(jìn)程調(diào)度06-19
解讀Linux系統(tǒng)中的進(jìn)程調(diào)度08-01
Linux系統(tǒng)中的守護(hù)進(jìn)程講解02-24
C語(yǔ)言入門知識(shí):標(biāo)識(shí)符03-28
c語(yǔ)言調(diào)用系統(tǒng)命令06-13
Linux系統(tǒng)中查殺僵尸進(jìn)程方法介紹03-01
系統(tǒng)進(jìn)程是什么05-22