英文原文:A Few Good Rules
什么是明智的標(biāo)準(zhǔn)化?
想象一下第一次和特別的人約會。當(dāng)你到達(dá)最喜歡的餐館時,所有的燈都熄滅了,你身處黑暗之中。奇怪的是,從廚房傳來的聲音又表明這里像往常一樣正在營業(yè)中。你聽到一位女服務(wù)員走來,等待著引導(dǎo)你到?jīng)]有燈光照射到的座位上。你的同伴不知所措,并且有一點害怕。你是打算留下,還是找個正常點的地方吃飯?
Web應(yīng)用就像餐館一樣,人們通過其所提供的體驗對其進(jìn)行評價。即使是短暫的中斷也會影響服務(wù)提供商的口碑或服務(wù)水平。政策和指導(dǎo)方針在防止代價高昂的服務(wù)中斷中扮演著重要的角色。不幸的是,它們也能導(dǎo)致不理智決策的產(chǎn)生,從而造成更大的損害。比如公司內(nèi)“DevOps團隊”的建立。這將導(dǎo)致所有的運維知識都被隔離在一個單獨的團隊中。盡管這樣一個管理層指令可能預(yù)示著DevOps的到來,但它什么都不是。
工程師鄙視無邏輯的、官僚主義的規(guī)則。這些規(guī)則是前進(jìn)的障礙物。然而,每家公司至少都會有一些這樣的規(guī)則。在過去,可能有好的理由在一些問題上制定這樣的規(guī)則。漸漸地,這些規(guī)則過時了。但是,規(guī)則制定者不能(或不敢)取消它們。當(dāng)使用C++代碼庫時,由于歷史原因,被告知不能使用STL;參與的Java項目被堅定地拒絕從1.4遷移到新版本。任何有過這樣經(jīng)歷的人都明白有些措施可能會對生產(chǎn)力產(chǎn)生消極的影響。
我們應(yīng)該忘記這些規(guī)則嗎?
面對這些像障礙物一樣的規(guī)則,我們都很想將它們廢除。不幸的是,“無為”的公司通常都沒有成功地廢除它們。好的規(guī)則是一種重要的交流形式。這種形式關(guān)乎到長期策略、從過去吸取到慘痛教訓(xùn)、以及來自用戶需求中的發(fā)現(xiàn)。理想情況下,一個組織制定的與時俱進(jìn)的規(guī)則,可以幫助個人增強做出正確決定的信心。在實踐中, 這樣的情況真的發(fā)生過嗎?
一家公司是否擁有真正有效的指導(dǎo)方針,Netflix就是一個很好的例子。至少通過閱讀他們的博客和開源的代碼會給人留下這樣的印象。比如,即使沒有和Netflix的任何員工聊過,我也能確定,“構(gòu)建它,運行它”是一個他們?nèi)绾伟验_發(fā)和運維結(jié)合起來的不錯的想法。另一個明確的原則是:寫代碼是為了構(gòu)建一個可靠的、可擴展的服務(wù),而不是為了其他目的。他們開源了所寫的大部分后臺軟件。這個事實比任何事都更具有說服力。
Netflix已經(jīng)構(gòu)建了Netflix內(nèi)部Web服務(wù)框架(NIWS)。這是一個自定義的軟件棧,用于創(chuàng)建可靠地運行在云上的內(nèi)部Web服務(wù)。NIWS采用了一些不太流行的技術(shù)和不太常用的方法。使用這種與最佳實踐背道而馳的方法需要有相當(dāng)強的自信。毫無疑問,部分可以歸因于落實的政策。這些政策讓工程師可以不受限制地考慮問題。
Netflix的負(fù)載均衡
在Netflix如何挑戰(zhàn)常規(guī)的例子中,我最喜歡的是他們是如何在NIWS中實現(xiàn)負(fù)載均衡的。面向客戶的流量仍使用傳統(tǒng)的負(fù)載均衡處理器(一個標(biāo)準(zhǔn)的Amazon EC2 ELB),但對于Netflix服務(wù)器之間的流量,他們選擇了一個完全不同的方案,稱作客戶端負(fù)載均衡(client-side loadbalancing)?;舅枷牒芎唵危喝∠麑iT用于負(fù)載均衡的節(jié)點。這些節(jié)點用于在Netflix服務(wù)器間轉(zhuǎn)發(fā)流量??蛻舳吮旧砭S持著一張列表,記錄了可用的后臺節(jié)點。當(dāng)客戶端發(fā)送請求時,直接與所選的后臺實例交互。而這樣就沒有必要使用專門的負(fù)載均衡器。
客戶端負(fù)載均衡并不是Netflix發(fā)明的。但是, 它是有名的公司里第一個在基礎(chǔ)架構(gòu)中完全使用這種技術(shù)的(公平地說,同一時期內(nèi),Twitter和Yahoo也在做基于相同概念的實驗)。在多個后臺服務(wù)器上做均衡的標(biāo)準(zhǔn)方法是:通過一個負(fù)載均衡器,如Amazon EC2 ELB,或者在服務(wù)器上運行類似HAProxy的軟件。對于這么關(guān)鍵的組件,使用保守的方法和一種大多數(shù)工程師都熟悉的技術(shù)是很有意義的。但是,幾乎沒有公司在Netflix之前試驗客戶端負(fù)載均衡的方法。其真正原因是,他們甚至都沒有考慮到這種方法。
對于從事大規(guī)模應(yīng)用程序開發(fā)的軟件工程師,每天都要和各種庫和組件打交道。這有點像魚和水的關(guān)系。在能使用一種特定方法成功地構(gòu)建系統(tǒng)這么多年后(也許幾十年) ,對已經(jīng)經(jīng)過考驗的方法或者系統(tǒng)的構(gòu)建模塊提出疑問,這看起來是在浪費時間。在許多公司里,這些決定已經(jīng)被寫進(jìn)政策中。這些政策基本上是不可變的。但是,Netflix采用了客戶端負(fù)載均衡的方法,并因此取得了顯著的成功。首先,他們從系統(tǒng)中移除了一個單點故障點(對于頻繁地在沒有警告下就停止服務(wù)的EC2實例,這是一個重大的勝利)。其次,通過將負(fù)載均衡的邏輯集成進(jìn)客戶端,負(fù)載均衡的策略可以參考客戶端提供的信息。比如,考慮以下的負(fù)載均衡規(guī)則:
向客戶端的EC2可訪問區(qū)域(EC2 Availability Zone)中的可用節(jié)點發(fā)送請求。如果這樣的實例不存在,則在當(dāng)前區(qū)域中找一個運行已超過一天的實例替代。
傳統(tǒng)的負(fù)載均衡器并沒有被設(shè)計成用來執(zhí)行這種自定義邏輯。它們也無法獲取太多關(guān)于客戶端的信息(比如一個客戶端所屬的EC2可訪問區(qū)域)。自定義負(fù)載均衡邏輯變成了應(yīng)用的一部分,使用相同的語言編寫。這意味著編寫代碼的單元測試用例變得容易。而在傳統(tǒng)上,這被認(rèn)為是“基礎(chǔ)設(shè)施的東西”。因此,這不僅讓制定更復(fù)雜、更智能的決策成為可能;也使得人們對工作能如期完成更有信心。從某方面看,NIWS將DevOps帶入了下一個層次:開發(fā)人員和運維工程師不僅坐在一起,在同一個團隊中工作;而且他們使用相同的開發(fā)語言,向相同的代碼庫提交更新。
Prezi加入客戶端負(fù)載均衡俱樂部
用一個內(nèi)部的客戶端負(fù)載均衡實現(xiàn)替代標(biāo)準(zhǔn)的負(fù)載均衡器,這種讓Netflix受益的技術(shù)只適用于Netflix嗎?不一定。在prezi.com,我們對內(nèi)部流量也采用了這種技術(shù)。我們的一些應(yīng)用服務(wù)器運行著若干個服務(wù)。當(dāng)這些服務(wù)通信時,我們希望它們優(yōu)先選擇本地的服務(wù)實例,而不是向網(wǎng)絡(luò)中發(fā)送請求。然而,如果需要訪問的服務(wù)沒有運行在同一臺服務(wù)器上,那么就可以訪問任何一個該服務(wù)的實例。對于Prezi,獲得的好處是,盡可能地避免了網(wǎng)絡(luò)流量、減少了在AWS上的支出和響應(yīng)時間。目前運行于prezi.com產(chǎn)品上的負(fù)載均衡邏輯由以下的這段Scala代碼實現(xiàn):
override def choose(key: scala.Any): Server = Option(getLoadBalancer).map(lb => lb.getServerList(true).filter{server => server.getHost == config.getHostname && serverIsAvailable(lb, server) }).getOrElse(Seq()) match { case Seq() => super.choose(key) case matchedServers => matchedServers(0) }
Netflix的工程師可以設(shè)計出NIWS,而不用擔(dān)心質(zhì)疑當(dāng)前技術(shù)所帶來的后果。因為公司的規(guī)則授權(quán)他們這么做。即使任何人都可以獲得NIWS的技術(shù),只有那些有著類似思維的公司才能夠使用這種技術(shù)去搭建產(chǎn)品。具體來講,期望工程師基于技術(shù)價值做出決定的公司和完美主義的公司是無法利用這樣的技術(shù)的。
Netflix驗證(Netflix test)
期望所有的工程師在做決定時不受辦公室政治、流行技術(shù)和害怕改變的制約,這是不可能的。然而,減少這些方面的影響,對確保開發(fā)不會誤入歧途有很大的幫助。一堆武斷的限制規(guī)定會讓工程師的設(shè)計缺乏創(chuàng)新和效用。相比之下,一些好的規(guī)則限制了問題空間、明確了約束、改善了產(chǎn)品的質(zhì)量。
基于NIWS棧的源代碼,Netflix在決定如何實現(xiàn)一個組件時會考慮兩件事:
我將這些問題成為Netflix驗證。這兩個問題是緊密關(guān)聯(lián)的。甚至可以說,第二個問題包含了第一個問題。這兩個問題之所以意義重大,是在于他們?nèi)鐚嵉冂R射出了Netflix的商業(yè)目標(biāo)。這個目標(biāo)就是提供可靠的、可擴展的服務(wù)。其他也有相同目標(biāo)的公司也能從這個驗證中受益。但是,這個驗證的真正力量在于它沒有提到的東西。它沒有提到任何具體的技術(shù)或者供應(yīng)商。
不適用于完美主義者
真正讓我驚訝的是,Netflix的代碼只專注于足夠好即可,而無過之。別誤解,目前我所看到的代碼容易閱讀,并且有很高的單元測試覆蓋率。即便如此,我也沒有預(yù)料到Netflix能專注在足夠好這個級別。比如,在代碼的許多地方,當(dāng)后臺線程啟動之后,就再沒有任何操作停止它們。這看起來有很大的問題,直到你意識到Netflix不在節(jié)點上進(jìn)行軟件升級。他們通過啟動一個新的EC2實例集群來部署新版本的應(yīng)用。當(dāng)通過監(jiān)控驗證新版本應(yīng)用運行正常后,就將老集群關(guān)閉。如果有人使用了這些部署工具(也是開源的),那么就沒有僵尸線程的問題。然而,如果有人在一個像Glassfish的應(yīng)用服務(wù)器上使用Netflix的庫,那么每次重新部署都將會觸發(fā)內(nèi)存泄漏。
代碼中包含大量單例模式的類,也是我未預(yù)料到的。當(dāng)我們以一種Netflix未預(yù)見的方式使用一個NIWS庫時,我們很快會發(fā)現(xiàn)自己在不斷掙扎地使用錯綜復(fù)雜的技術(shù)來處理問題。包括使用多個類加載器。
最后,盡管wiki頁面上關(guān)于代碼的文檔有很大的幫助,但是這樣的文檔太少了,很多細(xì)節(jié)都沒有描述。通常,代碼就是文檔。有幾次,我在github issue tracker上找到了一些解決NIWS相關(guān)問題的最佳建議。
我的許多同事,在第一次接觸Netflix生態(tài)圈時都有點不知所措。他們的第一反應(yīng)是譴責(zé)那些寫代碼的工程師未經(jīng)訓(xùn)練或者太懶惰。“應(yīng)該有一些規(guī)則關(guān)閉這些沒用的線程”,我聽他們這樣說著。然而,對于Netflix,我們所列出的NIWS的缺點,都不算是一個真正的問題。用于處理線程關(guān)閉的時間被用在了其他更需要的地方。如果有人想要以不支持的方式重用代碼,那么單例模式的類只是其中一個會遇到的問題。最后,盡管寫文檔是一件好事。但是,可讀性高的代碼和大量內(nèi)部專業(yè)知識讓文檔成為了一個可選項。Netflix建立了關(guān)于線程管理、惱人的設(shè)計模式和最小化文檔量的規(guī)則。通過建立這些規(guī)則,讓工程師可以專注于其他主要問題。
事實上,我已經(jīng)意識到Netflix的軟件棧之所以成功,是因為它有著旺盛的精力。這不僅讓Netflix“砍掉了軟件棧的一些邊角”的事實可以被接受,而且實際上也催生了一個更好的產(chǎn)品。編寫了大量描述代碼的文檔還得保證文檔不會過期,因為代碼總是在不斷的演進(jìn)。編寫不會用到的特性會使開發(fā)者失去動力、且難已為團隊證明自己,對社區(qū)也沒有什么好處,因為這部分代碼不會在產(chǎn)品中被驗證到。在Prezi,我們有一些一直想開源的項目。但由于缺乏時間加入一些我們希望的改進(jìn),目前還不能將它們開源。Netflix成功地開源了大量的代碼卻沒有破產(chǎn)。因為它們一直專注于代碼的可讀性和單元測試,而不是加入過多的亮點,以及保證其不會過時。Netflix實施的這些合理規(guī)則,使得它的設(shè)計開發(fā)可以應(yīng)對不斷快速增長的用戶;甚至是不斷開源所寫的代碼。
因此所有的特定規(guī)則都不好嗎?
如果用Netflix驗證再形成一些指導(dǎo)方針,那么這些方針是相當(dāng)通用的。例如,通過努力獲得成功的名言,像“花10%的時間用于償還技術(shù)債”,或者技術(shù)信息,如“0.6.1版的NodeJS使我們的Web應(yīng)用變得不穩(wěn)定,別使用它”。如果把從過去失敗中總結(jié)獲取的教訓(xùn)忘了,這難道不是一種浪費嗎?
這樣的建議,和最佳實踐、知名的組件一樣,是非常有價值的。在加速開發(fā)和簡化系統(tǒng)的運維方面,通過多年的驗證,這些建議已經(jīng)獲得了工程師的信任。比如在Prezi,大多數(shù)后臺系統(tǒng)都是用Python寫的,并使用了gunicorn web服務(wù)器、Django web框架和MySQL數(shù)據(jù)庫。在公司的初期,這個軟件棧使得開發(fā)者能夠?qū)W⒂谛庐a(chǎn)品的特性上。多年來,“使用Django和MySQL開發(fā)服務(wù)”就如同“不要在周五下午3點后部署”一樣明確。這些都不是Prezi成文的規(guī)則,但卻早已在實行中。
隨著注冊用戶數(shù)從0攀升到4000萬,許多當(dāng)初采用這個平臺的實際情況都已時過境遷。比如,當(dāng)所有的網(wǎng)站流量都由一個應(yīng)用處理時,將所有用戶數(shù)據(jù)存入一個MySQL數(shù)據(jù)庫中是有意義的。如今,Prezi擁有許多獨立的服務(wù)。這些服務(wù)對響應(yīng)延時、可靠性和一致性上都有著不同的需求。許多服務(wù)運行在EC2上,將數(shù)據(jù)庫當(dāng)做鍵-值存儲的容器,通過主鍵訪問數(shù)據(jù)。第一年制定的技術(shù)指導(dǎo)方針,盡管在那時有用,但沒有一條能幫助我們應(yīng)對目前工程上的挑戰(zhàn)。
只要標(biāo)準(zhǔn)的技術(shù)和特定的規(guī)則沒有過時,就能夠激發(fā)工程師的產(chǎn)出。問題在于,當(dāng)這些特定的規(guī)則不再適用時,仍然被強制實施。
固定的接口集
對于已經(jīng)過時的規(guī)范而言,一個問題(而且很常見)是軟件接口的過時。我最喜歡的例子是Java Servlet API。即使它并沒有真的過時!實際上,它是一個優(yōu)秀的接口:直觀、穩(wěn)定、有完整的文檔、很多不同應(yīng)用服務(wù)器都是使用它實現(xiàn)的。
當(dāng)Prezi決定探索JVM,將其作為我們可靠的Django棧的一個可選平臺時,我們選擇了一個輕量級的代理應(yīng)用作為我們的試用項目。我強烈地表明應(yīng)該使用Jetty和Servlet API,而不是團隊考慮的另一個可選方案。這個方案使用一個不知名的Scala Web服務(wù)器。6個月之后,我們關(guān)閉了原有的代理程序,而用一個基于Spray(這個技術(shù)我是投反對票的)寫的程序取而代之。部分原因是:對于我們的用例,使用它可以獲得更高的效率。因為在我們的用例中,響應(yīng)時間主要受發(fā)出的HTTP請求的響應(yīng)延時的影響。我開始從代碼層面思考:我們想要什么樣的目標(biāo),想使用什么樣的接口。我們?nèi)绾螌憜卧獪y試。開發(fā)者社區(qū)有多大。這正是Servlet API在抽象層面解決的問題。我本應(yīng)該考慮(或談?wù)摚╆P(guān)于它是如何利用硬件資源的。具體來說,瓶頸在于:處理請求時是否需要大量的CPU或者IO資源。由于在我們的用例中,大部分時間都花費在等待發(fā)出的HTTP請求的響應(yīng)上,所以沒有這樣的資源要求。這就是代理程序的本質(zhì)。鑒于我們的用例,使用Servlet的方法對每一個請求都創(chuàng)建一個專門的線程,不僅毫無必要地限制了處理請求的并行數(shù),而且也無法高效地利用內(nèi)存。
Servlet API不適用于這個問題的事實,并不能說明那些常用的接口或Java編程語言不好。數(shù)以千計的公司使用Servlet構(gòu)建了令人驚嘆的產(chǎn)品。其他編程語言也具有相似語義的Web服務(wù)器接口。這個故事想表明的是,我在使用特定的指導(dǎo)方針時脫離了實際的場景。接口是用來解決某一個特定問題的。當(dāng)問題不再是你嘗試解決的問題時,使用給定的接口不是一個好的選擇(無論這個接口有多流行或者多新穎)。
DevOps的規(guī)則
DevOps能量來自于合作中的人有著完全不同的技能。相比于成員技能單一的團隊;一個擁有各種不同技能的團隊,包括長滿胡子的系統(tǒng)管理員、函數(shù)式編程的狂熱粉絲,更有可能構(gòu)建出可靠和可擴展的服務(wù)。
成員技術(shù)背景的不同使得團隊更加需要明確的規(guī)則。開發(fā)者不需要知道為什么使用的自定義Linux內(nèi)核有著一大串的編譯參數(shù)。類似地,不是所有人都需要擔(dān)心代碼中有多少單例模式對象的存在。“寫shell腳本時必須添加shebang行”,或者“解析用戶數(shù)據(jù)的代碼要有單元測試”。像這樣的標(biāo)準(zhǔn)適用于團隊中的每一個人,并且會幫助到那些在特定領(lǐng)域內(nèi)沒有足夠經(jīng)驗做好事情的人。特定規(guī)則只有被適當(dāng)?shù)氖褂茫艜F隊產(chǎn)生積極的作用。
更通用的規(guī)則,像這些Netflix驗證只適用于制定高層級決策,但是能夠應(yīng)用地更久。管理團隊既需要通用的規(guī)則也需要高層級的規(guī)則。訣竅是要及時發(fā)現(xiàn)我們制定的規(guī)則是否已不再發(fā)揮期望的作用。
如果我們回到文章開頭的那個餐館,打開冰箱門,不同盒子上有著不同的保質(zhì)期時間。有的可能幾個月,比如番茄醬;有的可能幾個小時,比如魚。做飯要用到不同的原料,而每種原料有自己的保質(zhì)期。保持原料的新鮮,使得最終做出的食物可口,這是一個廚師的責(zé)任。同樣,不僅在我們決定要將什么進(jìn)行標(biāo)準(zhǔn)化這件事上需要智慧,在及時發(fā)現(xiàn)我們的標(biāo)準(zhǔn)是否已失去意義這件事上也需要真正的智慧。