一些好的規則

作者: Peter Neumark  來源: infoq  發布時間: 2014-09-02 09:01  閱讀: 5428 次  推薦: 1   原文鏈接   [收藏]  

  英文原文:A Few Good Rules

  什么是明智的標準化?

  想象一下第一次和特別的人約會。當你到達最喜歡的餐館時,所有的燈都熄滅了,你身處黑暗之中。奇怪的是,從廚房傳來的聲音又表明這里像往常一樣正在營業中。你聽到一位女服務員走來,等待著引導你到沒有燈光照射到的座位上。你的同伴不知所措,并且有一點害怕。你是打算留下,還是找個正常點的地方吃飯?

  Web應用就像餐館一樣,人們通過其所提供的體驗對其進行評價。即使是短暫的中斷也會影響服務提供商的口碑或服務水平。政策和指導方針在防止代價高昂的服務中斷中扮演著重要的角色。不幸的是,它們也能導致不理智決策的產生,從而造成更大的損害。比如公司內“DevOps團隊”的建立。這將導致所有的運維知識都被隔離在一個單獨的團隊中。盡管這樣一個管理層指令可能預示著DevOps的到來,但它什么都不是。

  工程師鄙視無邏輯的、官僚主義的規則。這些規則是前進的障礙物。然而,每家公司至少都會有一些這樣的規則。在過去,可能有好的理由在一些問題上制定這樣的規則。漸漸地,這些規則過時了。但是,規則制定者不能(或不敢)取消它們。當使用C++代碼庫時,由于歷史原因,被告知不能使用STL;參與的Java項目被堅定地拒絕從1.4遷移到新版本。任何有過這樣經歷的人都明白有些措施可能會對生產力產生消極的影響。

  我們應該忘記這些規則嗎?

  面對這些像障礙物一樣的規則,我們都很想將它們廢除。不幸的是,“無為”的公司通常都沒有成功地廢除它們。好的規則是一種重要的交流形式。這種形式關乎到長期策略、從過去吸取到慘痛教訓、以及來自用戶需求中的發現。理想情況下,一個組織制定的與時俱進的規則,可以幫助個人增強做出正確決定的信心。在實踐中, 這樣的情況真的發生過嗎?

  一家公司是否擁有真正有效的指導方針,Netflix就是一個很好的例子。至少通過閱讀他們的博客開源的代碼會給人留下這樣的印象。比如,即使沒有和Netflix的任何員工聊過,我也能確定,“構建它,運行它”是一個他們如何把開發和運維結合起來的不錯的想法。另一個明確的原則是:寫代碼是為了構建一個可靠的、可擴展的服務,而不是為了其他目的。他們開源了所寫的大部分后臺軟件。這個事實比任何事都更具有說服力。

  Netflix已經構建了Netflix內部Web服務框架(NIWS)。這是一個自定義的軟件棧,用于創建可靠地運行在云上的內部Web服務。NIWS采用了一些不太流行的技術和不太常用的方法。使用這種與最佳實踐背道而馳的方法需要有相當強的自信。毫無疑問,部分可以歸因于落實的政策。這些政策讓工程師可以不受限制地考慮問題。

  Netflix的負載均衡

  在Netflix如何挑戰常規的例子中,我最喜歡的是他們是如何在NIWS中實現負載均衡的。面向客戶的流量仍使用傳統的負載均衡處理器(一個標準的Amazon EC2 ELB),但對于Netflix服務器之間的流量,他們選擇了一個完全不同的方案,稱作客戶端負載均衡(client-side loadbalancing)。基本思想很簡單:取消專門用于負載均衡的節點。這些節點用于在Netflix服務器間轉發流量。客戶端本身維持著一張列表,記錄了可用的后臺節點。當客戶端發送請求時,直接與所選的后臺實例交互。而這樣就沒有必要使用專門的負載均衡器。

  客戶端負載均衡并不是Netflix發明的。但是, 它是有名的公司里第一個在基礎架構中完全使用這種技術的(公平地說,同一時期內,Twitter和Yahoo也在做基于相同概念的實驗)。在多個后臺服務器上做均衡的標準方法是:通過一個負載均衡器,如Amazon EC2 ELB,或者在服務器上運行類似HAProxy的軟件。對于這么關鍵的組件,使用保守的方法和一種大多數工程師都熟悉的技術是很有意義的。但是,幾乎沒有公司在Netflix之前試驗客戶端負載均衡的方法。其真正原因是,他們甚至都沒有考慮到這種方法。

  對于從事大規模應用程序開發的軟件工程師,每天都要和各種庫和組件打交道。這有點像魚和水的關系。在能使用一種特定方法成功地構建系統這么多年后(也許幾十年) ,對已經經過考驗的方法或者系統的構建模塊提出疑問,這看起來是在浪費時間。在許多公司里,這些決定已經被寫進政策中。這些政策基本上是不可變的。但是,Netflix采用了客戶端負載均衡的方法,并因此取得了顯著的成功。首先,他們從系統中移除了一個單點故障點(對于頻繁地在沒有警告下就停止服務的EC2實例,這是一個重大的勝利)。其次,通過將負載均衡的邏輯集成進客戶端,負載均衡的策略可以參考客戶端提供的信息。比如,考慮以下的負載均衡規則:

向客戶端的EC2可訪問區域(EC2 Availability Zone)中的可用節點發送請求。如果這樣的實例不存在,則在當前區域中找一個運行已超過一天的實例替代。

  傳統的負載均衡器并沒有被設計成用來執行這種自定義邏輯。它們也無法獲取太多關于客戶端的信息(比如一個客戶端所屬的EC2可訪問區域)。自定義負載均衡邏輯變成了應用的一部分,使用相同的語言編寫。這意味著編寫代碼的單元測試用例變得容易。而在傳統上,這被認為是“基礎設施的東西”。因此,這不僅讓制定更復雜、更智能的決策成為可能;也使得人們對工作能如期完成更有信心。從某方面看,NIWS將DevOps帶入了下一個層次:開發人員和運維工程師不僅坐在一起,在同一個團隊中工作;而且他們使用相同的開發語言,向相同的代碼庫提交更新。

  Prezi加入客戶端負載均衡俱樂部

  用一個內部的客戶端負載均衡實現替代標準的負載均衡器,這種讓Netflix受益的技術只適用于Netflix嗎?不一定。在prezi.com,我們對內部流量也采用了這種技術。我們的一些應用服務器運行著若干個服務。當這些服務通信時,我們希望它們優先選擇本地的服務實例,而不是向網絡中發送請求。然而,如果需要訪問的服務沒有運行在同一臺服務器上,那么就可以訪問任何一個該服務的實例。對于Prezi,獲得的好處是,盡可能地避免了網絡流量、減少了在AWS上的支出和響應時間。目前運行于prezi.com產品上的負載均衡邏輯由以下的這段Scala代碼實現:

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的工程師可以設計出NIWS,而不用擔心質疑當前技術所帶來的后果。因為公司的規則授權他們這么做。即使任何人都可以獲得NIWS的技術,只有那些有著類似思維的公司才能夠使用這種技術去搭建產品。具體來講,期望工程師基于技術價值做出決定的公司和完美主義的公司是無法利用這樣的技術的。

  Netflix驗證(Netflix test)

  期望所有的工程師在做決定時不受辦公室政治、流行技術和害怕改變的制約,這是不可能的。然而,減少這些方面的影響,對確保開發不會誤入歧途有很大的幫助。一堆武斷的限制規定會讓工程師的設計缺乏創新和效用。相比之下,一些好的規則限制了問題空間、明確了約束、改善了產品的質量。

  基于NIWS棧的源代碼,Netflix在決定如何實現一個組件時會考慮兩件事:

  1. 這個組件發生故障的可能性及后果是什么?
  2. 當這個組件的設想場景發生改變時,是否容易修改這個組件的行為?

  我將這些問題成為Netflix驗證。這兩個問題是緊密關聯的。甚至可以說,第二個問題包含了第一個問題。這兩個問題之所以意義重大,是在于他們如實地鏡射出了Netflix的商業目標。這個目標就是提供可靠的、可擴展的服務。其他也有相同目標的公司也能從這個驗證中受益。但是,這個驗證的真正力量在于它沒有提到的東西。它沒有提到任何具體的技術或者供應商

  不適用于完美主義者

  真正讓我驚訝的是,Netflix的代碼只專注于足夠好即可,而無過之。別誤解,目前我所看到的代碼容易閱讀,并且有很高的單元測試覆蓋率。即便如此,我也沒有預料到Netflix能專注在足夠好這個級別。比如,在代碼的許多地方,當后臺線程啟動之后,就再沒有任何操作停止它們。這看起來有很大的問題,直到你意識到Netflix不在節點上進行軟件升級。他們通過啟動一個新的EC2實例集群來部署新版本的應用。當通過監控驗證新版本應用運行正常后,就將老集群關閉。如果有人使用了這些部署工具(也是開源的),那么就沒有僵尸線程的問題。然而,如果有人在一個像Glassfish的應用服務器上使用Netflix的庫,那么每次重新部署都將會觸發內存泄漏。

  代碼中包含大量單例模式的類,也是我未預料到的。當我們以一種Netflix未預見的方式使用一個NIWS庫時,我們很快會發現自己在不斷掙扎地使用錯綜復雜的技術來處理問題。包括使用多個類加載器。

  最后,盡管wiki頁面上關于代碼的文檔有很大的幫助,但是這樣的文檔太少了,很多細節都沒有描述。通常,代碼就是文檔。有幾次,我在github issue tracker上找到了一些解決NIWS相關問題的最佳建議。

  我的許多同事,在第一次接觸Netflix生態圈時都有點不知所措。他們的第一反應是譴責那些寫代碼的工程師未經訓練或者太懶惰。“應該有一些規則關閉這些沒用的線程”,我聽他們這樣說著。然而,對于Netflix,我們所列出的NIWS的缺點,都不算是一個真正的問題。用于處理線程關閉的時間被用在了其他更需要的地方。如果有人想要以不支持的方式重用代碼,那么單例模式的類只是其中一個會遇到的問題。最后,盡管寫文檔是一件好事。但是,可讀性高的代碼和大量內部專業知識讓文檔成為了一個可選項。Netflix建立了關于線程管理、惱人的設計模式和最小化文檔量的規則。通過建立這些規則,讓工程師可以專注于其他主要問題。

  事實上,我已經意識到Netflix的軟件棧之所以成功,是因為它有著旺盛的精力。這不僅讓Netflix“砍掉了軟件棧的一些邊角”的事實可以被接受,而且實際上也催生了一個更好的產品。編寫了大量描述代碼的文檔還得保證文檔不會過期,因為代碼總是在不斷的演進。編寫不會用到的特性會使開發者失去動力、且難已為團隊證明自己,對社區也沒有什么好處,因為這部分代碼不會在產品中被驗證到。在Prezi,我們有一些一直想開源的項目。但由于缺乏時間加入一些我們希望的改進,目前還不能將它們開源。Netflix成功地開源了大量的代碼卻沒有破產。因為它們一直專注于代碼的可讀性和單元測試,而不是加入過多的亮點,以及保證其不會過時。Netflix實施的這些合理規則,使得它的設計開發可以應對不斷快速增長的用戶;甚至是不斷開源所寫的代碼。

  因此所有的特定規則都不好嗎?

  如果用Netflix驗證再形成一些指導方針,那么這些方針是相當通用的。例如,通過努力獲得成功的名言,像“花10%的時間用于償還技術債”,或者技術信息,如“0.6.1版的NodeJS使我們的Web應用變得不穩定,別使用它”。如果把從過去失敗中總結獲取的教訓忘了,這難道不是一種浪費嗎?

  這樣的建議,和最佳實踐、知名的組件一樣,是非常有價值的。在加速開發和簡化系統的運維方面,通過多年的驗證,這些建議已經獲得了工程師的信任。比如在Prezi,大多數后臺系統都是用Python寫的,并使用了gunicorn web服務器、Django web框架和MySQL數據庫。在公司的初期,這個軟件棧使得開發者能夠專注于新產品的特性上。多年來,“使用Django和MySQL開發服務”就如同“不要在周五下午3點后部署”一樣明確。這些都不是Prezi成文的規則,但卻早已在實行中。

  隨著注冊用戶數從0攀升到4000萬,許多當初采用這個平臺的實際情況都已時過境遷。比如,當所有的網站流量都由一個應用處理時,將所有用戶數據存入一個MySQL數據庫中是有意義的。如今,Prezi擁有許多獨立的服務。這些服務對響應延時、可靠性和一致性上都有著不同的需求。許多服務運行在EC2上,將數據庫當做鍵-值存儲的容器,通過主鍵訪問數據。第一年制定的技術指導方針,盡管在那時有用,但沒有一條能幫助我們應對目前工程上的挑戰。

  只要標準的技術和特定的規則沒有過時,就能夠激發工程師的產出。問題在于,當這些特定的規則不再適用時,仍然被強制實施。

  固定的接口集

  對于已經過時的規范而言,一個問題(而且很常見)是軟件接口的過時。我最喜歡的例子是Java Servlet API。即使它并沒有真的過時!實際上,它是一個優秀的接口:直觀、穩定、有完整的文檔、很多不同應用服務器都是使用它實現的。

  當Prezi決定探索JVM,將其作為我們可靠的Django棧的一個可選平臺時,我們選擇了一個輕量級的代理應用作為我們的試用項目。我強烈地表明應該使用Jetty和Servlet API,而不是團隊考慮的另一個可選方案。這個方案使用一個不知名的Scala Web服務器。6個月之后,我們關閉了原有的代理程序,而用一個基于Spray(這個技術我是投反對票的)寫的程序取而代之。部分原因是:對于我們的用例,使用它可以獲得更高的效率。因為在我們的用例中,響應時間主要受發出的HTTP請求的響應延時的影響。我開始從代碼層面思考:我們想要什么樣的目標,想使用什么樣的接口。我們如何寫單元測試。開發者社區有多大。這正是Servlet API在抽象層面解決的問題。我本應該考慮(或談論)關于它是如何利用硬件資源的。具體來說,瓶頸在于:處理請求時是否需要大量的CPU或者IO資源。由于在我們的用例中,大部分時間都花費在等待發出的HTTP請求的響應上,所以沒有這樣的資源要求。這就是代理程序的本質。鑒于我們的用例,使用Servlet的方法對每一個請求都創建一個專門的線程,不僅毫無必要地限制了處理請求的并行數,而且也無法高效地利用內存。

  Servlet API不適用于這個問題的事實,并不能說明那些常用的接口或Java編程語言不好。數以千計的公司使用Servlet構建了令人驚嘆的產品。其他編程語言也具有相似語義的Web服務器接口。這個故事想表明的是,我在使用特定的指導方針時脫離了實際的場景。接口是用來解決某一個特定問題的。當問題不再是你嘗試解決的問題時,使用給定的接口不是一個好的選擇(無論這個接口有多流行或者多新穎)。

  DevOps的規則

  DevOps能量來自于合作中的人有著完全不同的技能。相比于成員技能單一的團隊;一個擁有各種不同技能的團隊,包括長滿胡子的系統管理員、函數式編程的狂熱粉絲,更有可能構建出可靠和可擴展的服務。

  成員技術背景的不同使得團隊更加需要明確的規則。開發者不需要知道為什么使用的自定義Linux內核有著一大串的編譯參數。類似地,不是所有人都需要擔心代碼中有多少單例模式對象的存在。“寫shell腳本時必須添加shebang行”,或者“解析用戶數據的代碼要有單元測試”。像這樣的標準適用于團隊中的每一個人,并且會幫助到那些在特定領域內沒有足夠經驗做好事情的人。特定規則只有被適當的使用,才會對團隊產生積極的作用。

  更通用的規則,像這些Netflix驗證只適用于制定高層級決策,但是能夠應用地更久。管理團隊既需要通用的規則也需要高層級的規則。訣竅是要及時發現我們制定的規則是否已不再發揮期望的作用。

  如果我們回到文章開頭的那個餐館,打開冰箱門,不同盒子上有著不同的保質期時間。有的可能幾個月,比如番茄醬;有的可能幾個小時,比如魚。做飯要用到不同的原料,而每種原料有自己的保質期。保持原料的新鮮,使得最終做出的食物可口,這是一個廚師的責任。同樣,不僅在我們決定要將什么進行標準化這件事上需要智慧,在及時發現我們的標準是否已失去意義這件事上也需要真正的智慧。

1
0
 
標簽:程序員
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()