操作系統(tǒng)原理

第二個(gè)層面我們談?wù)劜僮飨到y(tǒng)原理。這塊最核心的就是線程和進(jìn)程之間的通訊,這個(gè)通訊包括互斥、同步、消息。大家經(jīng)常會(huì)接觸到互斥。只要有共享變量就一定會(huì)有鎖。在Go語(yǔ)言的服務(wù)器開發(fā),很難避開鎖。為什么呢?因?yàn)榉?wù)器本身是其實(shí)是有很多請(qǐng)求同時(shí)在響應(yīng)的,服務(wù)器本身就是共享資源,既然是共享資源,那么必然是有鎖的。

這里我話外要提一提的是 Erlang。Erlang里面很多人會(huì)說(shuō)它沒有鎖。我一直有個(gè)看法,不是因?yàn)镋rlang是函數(shù)式程序設(shè)計(jì)語(yǔ)言,它沒有變量,所以沒有鎖。只要是服務(wù)器,有很多并發(fā)的請(qǐng)求,那么服務(wù)器就一定是共享資源,這個(gè)是物理事實(shí),是不可改變的。為什么Erlang可以沒有鎖,原因是因?yàn)镋rlang強(qiáng)制讓所有的請(qǐng)求排隊(duì)了。排隊(duì)其實(shí)就是單線程化,那當(dāng)然沒有鎖的,在C里面,在Go里面都可以這么做,所以這并不奇怪。因此,本質(zhì)上來(lái)講,并不是因?yàn)樗呛瘮?shù)式程序設(shè)計(jì)語(yǔ)言,而是因?yàn)樗颜?qǐng)求串行化,也就是說(shuō)不并發(fā)。那怎么并發(fā)呢?Erlang里面想要并發(fā),其實(shí)是用異步消息,也就是將消息發(fā)出去,讓別人做,自己繼續(xù)往下執(zhí)行。這樣就涉及到的異步編程,這些我今天不展開講。但是我認(rèn)為,本質(zhì)上來(lái)講,服務(wù)器編程其實(shí)互斥是難以避免的,因此,Golang服務(wù)器runtim.GOMAXPROCS(1)將程序設(shè)為單線程后,仍然需要鎖,單線程!=所有請(qǐng)求串行化處理。而鎖主要存在以下幾個(gè)問(wèn)題。

1. 鎖最大的問(wèn)題:不易控制

很多人會(huì)因?yàn)槁荛_鎖,其實(shí)這樣做是錯(cuò)誤的。大部分框架想避開鎖并不是因?yàn)殒i慢,而是不易控制,主要表現(xiàn)為如果鎖忘記了Unlock,結(jié)果將是災(zāi)難性的,因?yàn)椴恢故窃撜?qǐng)求掛掉,而是整個(gè)服務(wù)器掛掉了,所有的請(qǐng)求都會(huì)被這個(gè)鎖擋在外面。如果Lock和Unlock不匹配,將因?yàn)橐粋€(gè)請(qǐng)求而導(dǎo)致所有人均受影響。

2. 鎖的次要問(wèn)題:性能殺手

鎖雖然會(huì)導(dǎo)致代碼串行化執(zhí)行,但鎖并不是特別慢。因?yàn)榫€程之間的通訊,它有其他原語(yǔ),如同步、收發(fā)消息,這些都是比鎖慢很多的原語(yǔ)。網(wǎng)絡(luò)上有部分人用Golang的Channel實(shí)現(xiàn)鎖,這很不正確。因?yàn)镃hannel就是線程之間發(fā)消息,成本比鎖高很多。比鎖快的東西,一是沒有鎖,二是原子操作。其中,原子操作并未比鎖快很多,因?yàn)槿绻跊_突不多的情況下,一個(gè)鎖基本上就是一個(gè)原子操作,發(fā)現(xiàn)沒有沖突,直接繼續(xù)執(zhí)行。所以鎖的成本并沒有像大家想象的那么高,尤其是在服務(wù)端,因?yàn)榉?wù)端絕大部分應(yīng)用的程序其實(shí)是IO比較多,更多的時(shí)間是花在IO上面的。

在鎖的最佳實(shí)踐里面,核心是控制鎖的粒度。如果鎖的粒度太大,例如把某一個(gè)IO操作給包進(jìn)去了,那這個(gè)鎖就比較災(zāi)難了。比如這個(gè)IO操作是操作數(shù)據(jù)庫(kù),那么這個(gè)鎖把數(shù)據(jù)庫(kù)的操作,請(qǐng)求和返回結(jié)果這樣一個(gè)操作包進(jìn)去了,那這個(gè)鎖的粒度就很大,就會(huì)導(dǎo)致很多人都會(huì)被擋在外面。這個(gè)是鎖粒度的問(wèn)題。這也是鎖里面比較難控制的一個(gè)點(diǎn)。

在鎖的最佳實(shí)踐里面,第一點(diǎn)是要懂得善用defer。在Go里面有一點(diǎn)是比較好的,Go語(yǔ)言里面有defer,容易讓你避免鎖的Lock和Unlock不匹配的問(wèn)題,可以大大降低用鎖的心智負(fù)擔(dān)。但濫用defer可能會(huì)導(dǎo)致鎖的粒度變得很大,因?yàn)槟憧赡茉诤瘮?shù)的開始就Lock,然后defer Unlock,這樣整個(gè)函數(shù)的執(zhí)行全都被鎖,函數(shù)里面只要有時(shí)間較長(zhǎng)的IO操作,服務(wù)器的性能就會(huì)下降。這是鎖需要注意的地方。

另外,鎖的最佳實(shí)踐中,第二點(diǎn)是要善用讀寫鎖。絕大部分服務(wù)器里面,尤其是一些請(qǐng)求量比較大的請(qǐng)求,大部分請(qǐng)求的讀操作居多而寫操作較少,這種情況下用讀寫鎖是非常好的方法,可以大大降低鎖的成本。另外一個(gè)降低鎖粒度的方法是鎖數(shù)組。鎖數(shù)組是用于什么場(chǎng)景呢?如果服務(wù)器共享資源本身有很強(qiáng)的分區(qū)特征,那么用鎖數(shù)組比較好。例如你要做一個(gè)網(wǎng)盤服務(wù),不同用戶之間的數(shù)據(jù)沒有關(guān)系,網(wǎng)盤就是一個(gè)文件系統(tǒng),它是樹型結(jié)構(gòu),這個(gè)樹型結(jié)構(gòu)的操作往往需要較高的一致性的要求,不能出現(xiàn)操作到一半被另外一個(gè)操作給中斷,導(dǎo)致文件系統(tǒng)的樹結(jié)構(gòu)被破壞。所以在網(wǎng)盤里面更有可能出現(xiàn)包含了IO操作的大鎖,這種情況下,如果某個(gè)用戶的一次網(wǎng)盤同步操作會(huì)影響其他用戶就會(huì)很難受。因此,在網(wǎng)盤服務(wù)的一個(gè)系統(tǒng)里,用鎖數(shù)組會(huì)比較自然,你可以直接用用戶的ID除以鎖數(shù)組的數(shù)組大小然后取模,數(shù)組的大小決定于服務(wù)的并發(fā)量有多大,選一個(gè)合適的值就好。這樣可以讓不同的用戶相互不干擾,同一個(gè)用戶只影響他自己。

我認(rèn)為,掌握好與鎖相關(guān)的技術(shù),基本上是將服務(wù)器里面很可能最大的一個(gè)坑給解決了。線程間其他的通訊,比如說(shuō)同步、消息相關(guān)的坑相對(duì)少。例如,Go語(yǔ)言的channel實(shí)際上非常好用,既可以作為同步原語(yǔ),也可以作為收發(fā)消息的原語(yǔ)。channel唯一一個(gè)需要注意的,channel是有緩沖區(qū)大小的,所以如果不設(shè)緩沖區(qū)的話,有一個(gè)goroutine發(fā)消息,另一個(gè)goroutine如果沒有及時(shí)接收的話,發(fā)消息的那個(gè)goroutine就阻塞了。但是這個(gè)其實(shí)也很容易就能找到問(wèn)題,所以這個(gè)問(wèn)題不是很大。但是要注意,channel不是唯一的同步原語(yǔ)。Go語(yǔ)言里面其實(shí)同步原語(yǔ)還是蠻多的。比如說(shuō)Group,這是一個(gè)很好用的同步原語(yǔ),它是用來(lái)干嗎的呢?它是讓很多人一起干做某件事情,然后最后在某一個(gè)總控的地方等所有的人干完,然后繼續(xù)往下走的一個(gè)原語(yǔ)。另外一個(gè)就是Cond原語(yǔ),Cond其實(shí)用得不多,原因是channel把大部分Cond適用的場(chǎng)景給滿足了。但是作為操作系統(tǒng)原理中經(jīng)常提的生產(chǎn)者消費(fèi)者模型里面最重要的一個(gè)原語(yǔ),了解它是很重要的。因?yàn)閏hannel這樣一個(gè)通訊設(shè)施,它背后其實(shí)是可以認(rèn)為就是用Cond實(shí)現(xiàn)的。而Cond它要比channel原始很多,應(yīng)用范疇也要廣得多。我今天不展開講Cond了,大家要感興趣,可以翻一翻操作系統(tǒng)原理相關(guān)的書。

存儲(chǔ)系統(tǒng)原理

七牛就是做存儲(chǔ)的。我覺得存儲(chǔ)這個(gè)東西對(duì)服務(wù)端開發(fā)來(lái)說(shuō)很重要。為什么呢?因?yàn)閷?shí)際上服務(wù)器端開發(fā)的難度原理上比大家想象得要大,之所以今天大家不會(huì)覺得特別特別累,就是因?yàn)橛写鎯?chǔ)中間件。存儲(chǔ)是什么東西呢?存儲(chǔ)其實(shí)是狀態(tài)的維持者,存儲(chǔ)它本身不是問(wèn)題,但是有了服務(wù)器之后,它就是問(wèn)題。因?yàn)榇蠹以谧烂娑?,大家知道存?chǔ)的要求不高的,文件系統(tǒng)就是一個(gè)存儲(chǔ),那它放圖片或者放什么,丟了就丟了,也沒有多少操作系統(tǒng)關(guān)心它丟了會(huì)怎么樣。但是在服務(wù)器端大家都知道,服務(wù)必須邏輯上是不宕機(jī)的。也就意味著狀態(tài)維持的人是不能掛掉的。物理的服務(wù)器肯定是會(huì)掛掉的,但是哪怕物理服務(wù)器掛掉了,你的邏輯的服務(wù)或者說(shuō)服務(wù)器本身不應(yīng)該被掛掉的。因此,它的狀態(tài)繼續(xù)要維持,那誰(shuí)維持呢?就是存儲(chǔ)。如果這個(gè)世界上沒有存儲(chǔ)中間件的話,大家可以想象,寫服務(wù)器是非常非常累的,你每做一件事情,做這件事情的每一步,都要想一想,中間需要把狀態(tài)存下來(lái),以便萬(wàn)一掛掉之后我該怎么辦這樣一個(gè)問(wèn)題。

因此,存儲(chǔ)中間件是大家最重要的生存基礎(chǔ)。對(duì)于服務(wù)器程序員來(lái)講,它是真正革命性的,它是讓你能夠今天這么輕松的寫代碼的基礎(chǔ)。這也是我們需要理解存儲(chǔ)系統(tǒng)為什么重要,它是大家賴以生存的最重要的一個(gè)外部條件。存儲(chǔ)我蠻早的時(shí)候提過(guò)一個(gè)觀點(diǎn),存儲(chǔ)就是數(shù)據(jù)結(jié)構(gòu)。這個(gè)世界上存儲(chǔ)中間件是寫不完的,很多很多,消息隊(duì)列這些是存儲(chǔ),文件系統(tǒng)、數(shù)據(jù)庫(kù)、搜索引擎的倒排檔等等,這些其實(shí)都是存儲(chǔ)。為什么說(shuō)存儲(chǔ)就是數(shù)據(jù)結(jié)構(gòu)呢?因?yàn)樵谧烂娑碎_發(fā)的時(shí)候,大家都知道數(shù)據(jù)結(jié)構(gòu)通常都是自己寫的,或者說(shuō)某個(gè)語(yǔ)言的標(biāo)準(zhǔn)庫(kù)寫的。但是在服務(wù)端里面,因?yàn)闋顟B(tài)通常是持久化的,所以數(shù)據(jù)結(jié)構(gòu)很難寫。而存儲(chǔ)其實(shí)就是一個(gè)中間件服務(wù),是讓你把狀態(tài)維持這樣一件事情,從業(yè)務(wù)里面剝離出來(lái)??梢韵胂?,存儲(chǔ)是非常多樣化的,并且會(huì)和大家熟知的各種各樣的數(shù)據(jù)結(jié)構(gòu)對(duì)應(yīng)起來(lái)(參考文檔)。

靠譜的服務(wù)器是怎么構(gòu)建的呢?很核心的一個(gè)原理,叫Fail Fast,也就是速錯(cuò)。我認(rèn)為,速錯(cuò)思想對(duì)于服務(wù)端開發(fā)來(lái)說(shuō)非常非常重要。但是速錯(cuò)理念的基礎(chǔ)是靠譜的存儲(chǔ)。因?yàn)樗馘e(cuò)的意思是說(shuō),系統(tǒng)萬(wàn)一有問(wèn)題,就掛掉了,掛要之后要重啟重新做。但是重新做,你得知道它剛才在干什么,它的基礎(chǔ)就是要有人維持狀態(tài),也就是存儲(chǔ)。速錯(cuò)的思想最早是在硬件領(lǐng)域,后來(lái)Erlang語(yǔ)言中首先提出將速錯(cuò)這樣一個(gè)思想運(yùn)用在軟件開發(fā)里面,以構(gòu)建高可靠的軟件系統(tǒng)。這是一篇Erlang作者的博士論文。這篇文章對(duì)于我的影響是非常大的,是我個(gè)人在服務(wù)端開發(fā)里面的啟蒙的一個(gè)著作。大家知道軟件是偏實(shí)踐的科學(xué),比較少有體系化的理念出現(xiàn),這個(gè)是我見過(guò)的很棒的一個(gè)服務(wù)端開發(fā)或者分布式系統(tǒng)相關(guān)的理論,個(gè)人受益匪淺。

然而存儲(chǔ)為什么難呢?是因?yàn)閯e人都可以Fail Fast,但是存儲(chǔ)系統(tǒng)不行。存儲(chǔ)系統(tǒng)必須遵守頂層設(shè)計(jì)理念,其實(shí)是和Fail Fast相反的,它需要達(dá)到的結(jié)果是,無(wú)論怎么錯(cuò)都應(yīng)該有正確的結(jié)果。當(dāng)然如果說(shuō)存儲(chǔ)系統(tǒng)完全和Fail Fast相反倒也不至于,因?yàn)榇鎯?chǔ)系統(tǒng)的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)本身,還是會(huì)用到很多速錯(cuò)相關(guān)的原理。但是存儲(chǔ)系統(tǒng)對(duì)外表現(xiàn)出來(lái)的、所呈現(xiàn)的使用界面,和速錯(cuò)原理會(huì)有反過(guò)來(lái)的感覺。因?yàn)闊o(wú)論發(fā)生什么樣的錯(cuò)誤,包括軟件、網(wǎng)絡(luò)、磁盤、服務(wù)器斷電、內(nèi)存,甚至是IDC故障等等,對(duì)于一個(gè)存儲(chǔ)系統(tǒng)來(lái)講,它都認(rèn)為,這必須是能承受的,必須有合理的結(jié)果。當(dāng)然這個(gè)能承受的范圍,不同的存儲(chǔ)系統(tǒng)是不一樣的,代價(jià)也不一樣。比如說(shuō)MemCache這樣的存儲(chǔ)系統(tǒng),它就不考慮斷電這樣的問(wèn)題。對(duì)于MySQL這樣的東西,如果說(shuō)在最早的時(shí)候,它是不考慮宕機(jī)這樣的故障的,后來(lái)引入了主從之后,你就可以想象,它就能夠解決服務(wù)器掛掉、硬盤掛掉等問(wèn)題。不同的存儲(chǔ)系統(tǒng),因?yàn)閷?duì)可靠性要求不一樣,它的實(shí)現(xiàn)難度也有非常大的差別(參考文檔)。

那么現(xiàn)實(shí)中的存儲(chǔ),好吧,第一個(gè)我提了七牛云存儲(chǔ),我這是打廣告了。第二像MongoDB、MySQL等這些都是存儲(chǔ)。大家經(jīng)常接觸的也主要是這一些。

模塊的設(shè)計(jì)

我一般講模塊設(shè)計(jì)的時(shí)候,都會(huì)先講架構(gòu)相關(guān)的一些東西。當(dāng)然架構(gòu)這個(gè)話題,要完整的講,可以講很長(zhǎng)很長(zhǎng)時(shí)間。因?yàn)榧軜?gòu)的話題真的很復(fù)雜。如果只是用一兩頁(yè)描述架構(gòu)的話,我會(huì)談這么一些點(diǎn)。首先架構(gòu)師必須重視的第一件事情是需求,因?yàn)榧軜?gòu)的目的是為了滿足需求,這一點(diǎn)千萬(wàn)不能搞錯(cuò)。談到架構(gòu),很多人都會(huì)喜歡說(shuō),我設(shè)計(jì)了一個(gè)牛逼的框架。但是我長(zhǎng)期以來(lái)在強(qiáng)調(diào)的一個(gè)觀點(diǎn)是說(shuō),框架這種事情其實(shí)在架構(gòu)哲學(xué)里面一點(diǎn)都不重要,框架其實(shí)是實(shí)踐層面的事情,架構(gòu)真正需要關(guān)心的其實(shí)是需求的正交分解,怎么樣把需求分解得足夠的正交。所謂的正交就是兩個(gè)模塊之間沒有什么太復(fù)雜的關(guān)系。當(dāng)然正交是數(shù)學(xué)里面的詞,我不知道其他人有沒有會(huì)把它用到這個(gè)領(lǐng)域。但是我覺得正交這個(gè)詞很符合需求分解的這個(gè)概念。

隨著大需求(比如說(shuō)一個(gè)應(yīng)用程序,或者一個(gè)服務(wù)器)逐漸被切成很多個(gè)小需求,小的需求繼續(xù)分解變成一個(gè)個(gè)類和函數(shù)。這一層層的需求分解的單元,本質(zhì)上來(lái)講,都是同樣的東西,都是模塊,只是粒度問(wèn)題。所有這些app、service、package、class、func等,統(tǒng)一都可以稱之為模塊。那所有的模塊,第一重要的是什么呢?是它的規(guī)格。模塊里面最核心的,任何一個(gè)模塊的規(guī)格是要體現(xiàn)需求。為什么我會(huì)說(shuō)我是反框架的,因?yàn)榭蚣芷鋵?shí)就是模塊的連接方式,不同的模塊如何連接這個(gè)框架。那這種連接方式通常是易變的、不穩(wěn)定的,因?yàn)榭蚣苁切枰葸M(jìn)的。隨著需求的增加、修改,它會(huì)不斷演進(jìn),肯定后面會(huì)發(fā)現(xiàn),之前搭的框架不太好了,需要重構(gòu)。框架需要變的時(shí)候,通常很痛苦,所以也是很多人為什么重視框架的原因。但是不應(yīng)該因?yàn)檫@一點(diǎn)兒把框架看得太重。因?yàn)椴环€(wěn)定的東西,通常是最不重要的東西。你要抓住的是穩(wěn)定的東西。因此,框架只是實(shí)踐程度可依賴的東西,但是從架構(gòu)來(lái)講不要太強(qiáng)調(diào)。

模塊,剛才我講了,模塊其實(shí)最重要的是規(guī)格,也就是使用界面,或者叫interface(接口)。對(duì)于一個(gè)應(yīng)用程序來(lái)說(shuō),interface就是用戶交互。對(duì)于一個(gè)service來(lái)說(shuō),interface就是api。對(duì)于一個(gè)package來(lái)說(shuō),就是包的導(dǎo)出的函數(shù)或者類。對(duì)于一個(gè)class來(lái)說(shuō),就是公開的方法。對(duì)于函數(shù)來(lái)說(shuō)就是函數(shù)的原型。這些都是interface。模塊的interface必須體現(xiàn)需求,否則這個(gè)interface就不是一個(gè)好interface。

總結(jié)一下,如果要提煉模塊的最佳實(shí)踐的話,我會(huì)提煉這樣三點(diǎn)。

第一,模塊的需求一定要是單一職責(zé)。就是這個(gè)模塊不能做太多的事情,如果有太多的事情,它就要進(jìn)一步的分解。

第二,模塊的使用界面要體現(xiàn)需求。大家一看這個(gè)模塊的界面,就知道這個(gè)模塊是干什么的。比如一個(gè)軟件,你下載下來(lái)玩的時(shí)候,一看就應(yīng)該知道這個(gè)軟件目的是什么,而不是看了好幾眼都分不清楚這個(gè)軟件到底是財(cái)務(wù)軟件還是什么軟件,那這個(gè)interface就太糟糕了。所以其實(shí)所有的interface都是一樣的,都要體現(xiàn)需求。

第三是模塊的可測(cè)試性。任何一個(gè)模塊,如果提煉得好的話,它應(yīng)該很容易測(cè)試。為什么這一點(diǎn)很重要呢?因?yàn)闇y(cè)試在軟件系統(tǒng)里面其實(shí)非常重要,尤其是在服務(wù)端開發(fā),尤其是像七牛這樣一個(gè)做基礎(chǔ)服務(wù)的,一個(gè)bug或者一個(gè)故障會(huì)導(dǎo)致成千上萬(wàn)甚至上百萬(wàn)的公司受影響,那么這個(gè)測(cè)試非常非常重要??蓽y(cè)試性包括什么呢?它包括把模塊跑起來(lái)的最小的環(huán)境。如果一個(gè)模塊耦合小,意味著外部環(huán)境依賴少,這個(gè)模塊就很容易測(cè)試。反過(guò)來(lái),很容易測(cè)試意味著這個(gè)模塊的耦合很低。因此,模塊的可測(cè)試性,其實(shí)能夠反向來(lái)推導(dǎo)這個(gè)模塊設(shè)計(jì)得好與不好。

展開來(lái)講,第一,模塊的使用界面,它應(yīng)該符合需求,而不應(yīng)該符合某種框架的需要。這一點(diǎn),我為什么強(qiáng)調(diào)呢?而且是反復(fù)強(qiáng)調(diào)呢?是因?yàn)槲艺J(rèn)為很多剛剛踏入這個(gè)行業(yè)的人會(huì)違背這一點(diǎn),包括我自己。最早做office軟件的時(shí)候,我很清楚自己犯了無(wú)數(shù)次這樣的錯(cuò)誤,所以我后來(lái)把這一條作為非常重要的告誡自己的點(diǎn)。因?yàn)椴惑w現(xiàn)需求的話,意味著這個(gè)模塊的使用界面是不穩(wěn)定的。最自然體現(xiàn)需求的使用界面是最穩(wěn)定的。第二,我認(rèn)為模塊應(yīng)該是可完成的。也就是說(shuō)它的需求是穩(wěn)定的可預(yù)期的,或者是說(shuō)模塊的目標(biāo)是單一的,只做一件事情。只有這樣才能做到模塊可完成。但是反例很多很多。比如C++里面有Boost、MFC、QT這些庫(kù)。其實(shí)你知道,它們都是大而統(tǒng),包含很多的東西,你不知道這個(gè)庫(kù)是干嘛的。這種我個(gè)人是非常反對(duì)。我早期也是這樣的,早期自己寫了一些通用庫(kù),都是很含糊,想到一個(gè)很好的東西,就把它扔到通用庫(kù)里面,最后這個(gè)通用庫(kù)就變成垃圾筒,什么東西都有。任何一個(gè)模塊,都有一個(gè)你對(duì)它的邊界的界定,邊界界定好之后,這個(gè)模塊總歸有一天,它逐步趨于穩(wěn)定,最終幾乎不必去修改(就算修改也只是實(shí)現(xiàn)上的優(yōu)化)。

剛才我也講了模塊應(yīng)該是可測(cè)試的??蓽y(cè)試性可以表征一個(gè)模塊的耦合度。耦合越低越容易測(cè)試。所謂的耦合就是環(huán)境依賴,我依賴外部的東西越少越容易測(cè)試。一個(gè)模塊要測(cè)試的話,必須要模擬整個(gè)環(huán)境,讓它跑起來(lái)。

服務(wù)器的設(shè)計(jì)

服務(wù)器的設(shè)計(jì)首先要遵循模塊的設(shè)計(jì),其次是服務(wù)器有服務(wù)器特有的一些東西。第一是服務(wù)器的測(cè)試。七牛對(duì)于測(cè)試非常看重,參加過(guò)上次Gopher China大會(huì)的都知道我講的內(nèi)容,就是HTTP服務(wù)器如何測(cè)試。七牛為此自己發(fā)明了一個(gè)DSL語(yǔ)言,就是領(lǐng)域?qū)S谜Z(yǔ)言,專門用于測(cè)試。現(xiàn)在這個(gè)DSL在我們團(tuán)隊(duì)用得非常廣泛,基本上所有新增的模塊都會(huì)用這個(gè)方法進(jìn)行單元測(cè)試。第二個(gè)是服務(wù)器的可維護(hù)性。我沒有講服務(wù)器本身應(yīng)該怎么設(shè)計(jì),因?yàn)檫@個(gè)其實(shí)跟領(lǐng)域是有關(guān)系的,也就是你做什么事情,本身是很具化的,我沒辦法告訴你應(yīng)該怎么樣設(shè)計(jì)。服務(wù)器的設(shè)計(jì),無(wú)非遵循我剛剛講的模塊設(shè)計(jì)的一些準(zhǔn)則,但是服務(wù)器有它自己的特征,因?yàn)樗鳛橐粋€(gè)互聯(lián)網(wǎng),或者作為一個(gè)C/S結(jié)構(gòu)的東西,它有一些通用的需求。剛才我們講模塊需要做需求的正交分解,那作為一個(gè)Web服務(wù)器的話,除了業(yè)務(wù)相關(guān)的東西,會(huì)不會(huì)有一些通用的需求?其實(shí)通用的需求是非常多的,我這里列了很多,但是肯定不完整,當(dāng)然列這一些,已經(jīng)有更多細(xì)節(jié)的話題可以展開來(lái)講。

第一個(gè)比如路由,這個(gè)就不用說(shuō)了。大家看到大部分的Web框架都會(huì)解決路由相關(guān)的問(wèn)題。第二個(gè)是協(xié)議,通常大家看到比較多的協(xié)議,如果用HTTP的話,會(huì)比較多的見到form、json或者xml。第三個(gè)是授權(quán),授權(quán)我就不展開了。會(huì)話,其實(shí)跟授權(quán)類似。第四個(gè)是問(wèn)題的跟蹤和定位。這個(gè)我等一下再講。第五個(gè)是審計(jì)。審計(jì)有兩個(gè)用處,一個(gè)是計(jì)費(fèi),像七牛這樣的服務(wù)需要審計(jì),因?yàn)槊恳淮蜛PI請(qǐng)求,會(huì)有計(jì)費(fèi)相關(guān)的東西;另一個(gè)做對(duì)賬,你說(shuō)有我說(shuō)沒有,那最終是有還是沒有,看服務(wù)器的日志來(lái)說(shuō)了算。第六個(gè)是性能的調(diào)優(yōu),然后是測(cè)試和監(jiān)控。為什么會(huì)有這么多的需求呢?原因是因?yàn)榉?wù)器開發(fā),我覺得大家可能關(guān)注了開發(fā)兩個(gè)字,但是可能忘了服務(wù)器開發(fā)完了是干什么的。服務(wù)器開發(fā)完了,它后面還要在線上跑很長(zhǎng)時(shí)間,而且絕大部分的時(shí)間是在線上。所以服務(wù)器的開發(fā)其實(shí)和在線上運(yùn)維的過(guò)程是不能脫節(jié)的。正因?yàn)椴荒苊摴?jié),所以我們才會(huì)關(guān)注像監(jiān)控、性能調(diào)優(yōu)、問(wèn)題跟蹤定位、審計(jì)相關(guān)的一些需求。

這一些其實(shí)都是和具體的業(yè)務(wù)無(wú)關(guān)的,所有的服務(wù)器都需要。那么其實(shí)這些需求,可以被分解出來(lái),由一些基礎(chǔ)組件去實(shí)現(xiàn)。當(dāng)然因?yàn)檫@些東西,很多都是和七牛內(nèi)部的組件相關(guān),所以我沒有展開。

1. 服務(wù)器的測(cè)試

我大概的講一下服務(wù)器的測(cè)試方法,在七牛這邊怎么用的,還有服務(wù)器可維護(hù)性相關(guān)的東西。第一個(gè)是七牛用的兩個(gè)東西,一個(gè)是叫mockhttp,當(dāng)然這個(gè)不一定要用,因?yàn)槲抑繥o其實(shí)有標(biāo)準(zhǔn)的httptest模塊,它能夠監(jiān)聽隨機(jī)端口的服務(wù)。七牛也用這種方式起測(cè)試服務(wù)器,但是我自己個(gè)人更喜歡用mockhttp。因?yàn)樗槐O(jiān)聽物理的端口,所以它沒有端口沖突問(wèn)題,心智相對(duì)負(fù)擔(dān)比較低,而且相比那種監(jiān)聽真正物理端口的程序跑得會(huì)更快一些。這個(gè)mockhttp已經(jīng)開源了,在github.com/qiniu上可以找到。

第二個(gè)是基于七牛的httptest,暫時(shí)還沒有開源。今天我沒有辦法完整地講,因?yàn)槲抑坝袀€(gè)完整的講座,在網(wǎng)上可以搜索得到。它最核心的思想是什么呢?你不用寫client端sdk,直接就可以基于http協(xié)議寫測(cè)試案例。如果沒有這樣的工具,寫一個(gè)服務(wù)器的測(cè)試,顯然第一件事情就寫一個(gè)sdk,把你的服務(wù)器的interface,也就是網(wǎng)絡(luò)api包裝一下,包裝成一個(gè)類,里面有很多函數(shù)。然后通過(guò)這個(gè)類去測(cè)你的服務(wù)。這種模式有什么不好的地方呢?最大的問(wèn)題是這個(gè)sdk,其實(shí)很多時(shí)候是不穩(wěn)定的。不穩(wěn)定會(huì)帶來(lái)一個(gè)問(wèn)題,就是這個(gè)sdk你改改改,有可能改sdk的人忘了服務(wù)器的測(cè)試案例在用它,改了后會(huì)導(dǎo)致編不過(guò),從而導(dǎo)致測(cè)試案例失敗。當(dāng)然也有另外一種做法,就是我為服務(wù)器的測(cè)試專門寫一個(gè)sdk,但是我覺得這個(gè)成本是比較高昂的,因?yàn)槟阆喈?dāng)于只為某一個(gè)具體的場(chǎng)景專門做一個(gè)事情,而這個(gè)事情,可能工作量不一定非常巨大,但是很繁雜。因此,七牛的httptest最本質(zhì)的點(diǎn)是,可以直接寫一個(gè)看起來(lái)像直接發(fā)網(wǎng)絡(luò)包的方式去做測(cè)試。然后盡可能把網(wǎng)絡(luò)協(xié)議的文本描述讓它看起來(lái)更人性化一些,讓人一看就知道發(fā)過(guò)去的是什么。這樣也可以認(rèn)為是寫了一個(gè)sdk,但是這個(gè)sdk非常通用,所有的HTTP服務(wù)器都能去用它。這樣所有的HTTP服務(wù)測(cè)試,包括單元測(cè)試和集成測(cè)試,都可以用這種方式測(cè)。

2. 服務(wù)器的可維護(hù)性

我剛才在講服務(wù)器的需求時(shí)提過(guò)這一點(diǎn),也是我覺得非常非常需要去強(qiáng)調(diào)的一點(diǎn),就是服務(wù)器的可維護(hù)性。這一點(diǎn)是極其極其重要的,因?yàn)榉?wù)器的開發(fā)和運(yùn)維并不能分割的,服務(wù)器本身的設(shè)計(jì)需要為運(yùn)維做好準(zhǔn)備。這個(gè)系統(tǒng)跑到線上會(huì)發(fā)生很多問(wèn)題,發(fā)生這些問(wèn)題之后,如何快速地解決,需要在開發(fā)階段就去思考。正因?yàn)槿绱?,所以才?huì)有很多出于可維護(hù)性上的一些基礎(chǔ)的需求,包括日志。日志其實(shí)是最基礎(chǔ)的。沒有日志怎么排除這種故障呢?但是對(duì)于經(jīng)常要發(fā)生的情況,服務(wù)器設(shè)計(jì)本身就需要避免,最最基本的不能有單點(diǎn),因?yàn)橛袉吸c(diǎn),一個(gè)服務(wù)器掛掉了,線上就完蛋了,運(yùn)維就要立刻跟上。但是這種事情必然會(huì)發(fā)生。對(duì)于必然會(huì)發(fā)生的事情,必然是需要在開發(fā)階段就去避免。所以某種意義上來(lái)說(shuō),高可用是為了可維護(hù)性,如果不是為了可維護(hù)性,就不用考慮高可用的問(wèn)題。

服務(wù)器的可維護(hù)性,我覺得大概分為這樣幾個(gè)類別的需求。第一個(gè)是性能瓶頸。性能瓶頸,比如說(shuō)你發(fā)現(xiàn)業(yè)務(wù)支撐的并發(fā)量不夠,經(jīng)常需要加機(jī)器,這時(shí)候要發(fā)現(xiàn)慢到底慢在哪里。也許你認(rèn)為網(wǎng)站剛剛上線的時(shí)候,不用考慮這個(gè)問(wèn)題,但是如果這個(gè)網(wǎng)站能做大的話,總有一天會(huì)碰到瓶頸問(wèn)題。因此,最早的時(shí)候,就要為瓶頸問(wèn)題考慮好,如果萬(wàn)一發(fā)生瓶頸,如何能盡快發(fā)現(xiàn)瓶頸在哪里。此外,我認(rèn)為非常非常關(guān)鍵的是異常情況的預(yù)警。很多時(shí)候如果存在瓶頸,那么等它發(fā)生的時(shí)候就已經(jīng)是災(zāi)難了。最好的情況下,在達(dá)到災(zāi)難的臨界點(diǎn)之前,最好有個(gè)預(yù)警線,在那個(gè)預(yù)警線上開放排除問(wèn)題就比較好一點(diǎn)。

第二個(gè)是故障發(fā)現(xiàn)和處理。當(dāng)線上真的發(fā)現(xiàn)故障了,雖然我們極力去避免,但是肯定避免不了了,一定會(huì)發(fā)生故障,沒有一個(gè)公司不會(huì)發(fā)生故障。發(fā)生故障的時(shí)候,如何去快速地響應(yīng),這個(gè)就是快速地定位故障源。這個(gè)其實(shí)也是服務(wù)器開發(fā)里面,我覺得需要深度去考慮的一個(gè)問(wèn)題。對(duì)于經(jīng)常發(fā)生的故障,必須要實(shí)現(xiàn)自我恢復(fù)。也就是我剛剛第一個(gè)講的。一旦發(fā)生這個(gè)事情,不是偶然,是經(jīng)常的。那么你必須要在開發(fā)階段解決,而不是到線上運(yùn)維階段解決這個(gè)問(wèn)題。

第三個(gè)是用戶問(wèn)題排查,一個(gè)用戶,提供了一個(gè)非常個(gè)例化的問(wèn)題,不是服務(wù)總體的一個(gè)問(wèn)題,可能就是一個(gè)客服的個(gè)例問(wèn)題,那么就需要有所謂的reqid。每一個(gè)用戶請(qǐng)求都有一個(gè)唯一的reqid,一旦是個(gè)例的問(wèn)題需要跟進(jìn),客戶要告訴我你的reqid是多少,輸入這個(gè)ID,就能把所有和該請(qǐng)求相關(guān)的東西都能找到。整個(gè)服務(wù)端的請(qǐng)求鏈都能找到之后,這個(gè)問(wèn)題的排查就更容易。

分享到

崔歡歡

相關(guān)推薦