1. 程式人生 > >Netflix OSS、Spring Cloud還是Kubernetes? 都要吧!

Netflix OSS、Spring Cloud還是Kubernetes? 都要吧!

Netflix OSS是由Netflix公司主持開發的一套程式碼框架和庫,目的是解決上了規模之後的分散式系統可能出現的一些有趣問題。對於當今時代的Java開發者們來說,Netflix OSS簡直就是在雲端開發微服務的代名詞服務發現負載均衡容錯等對於可擴充套件的分散式系統來說都是非常非常重要的概念,Netflix對這些問題都給出了很好的解決方案。在這裡Netflix要對那些在廣大的開源社群中為這些程式碼框架和庫做出過貢獻的人們簡單地說聲“謝謝”,還有許多網際網路公司也做出了貢獻,所以在這裡一併謝過。可是有一些比較大的網際網路公司卻為自己做的一些東西申請了專利,還把程式碼都保留起來沒有開源,這實在不太好。

不過,Netflix OSS的許多內容都是在一個已經過去的年代寫出來的,那時所有東西都只能執行在AWS雲上而沒有其它選擇。關於那個年代的許多寶貴遺產和前提假設都已經被封裝到了Netflix的庫裡面,對於現在你執行的環境(比如Linux容器)已經不適用了。在Linux容器Docker容器管理系統等等出現之後,我們越來越看到把我們的微服務執行在Linux容器(公有云、私有云,或者都要,等等)裡的巨大價值。另外,因為這些容器都是直接把這些服務打包起來、對外不透明的,所以我們傾向於不要過多關心在容器裡面執行的到底是什麼技術(是Java?還是Node.js?或者Go?)。Netflix OSS主要是為Java開發者服務的,它是許多的庫、框架和配置的集合,你需要把它們包含在你的Java程式或服務程式碼裡面。

這就帶來了第一個問題。

種類眾多的微服務是可以用各種不同的框架或語言實現的,但像服務發現、負載均衡、容錯等等功能還是非常重要而且必不可少的。如果我們在容器裡面執行這些服務,我們可以藉助於強大的語言無關的架構來做各種事情,比如構建打包部署健康檢查滾動升級藍綠髮布安全、還有其它等等各種事情。作為一種型別的KubernetesOpenShift專注於為企業使用者服務,它把所有事情都幫你做完了:沒有什麼東西必須要你的應用層程式去知道或者處理的了。你只要簡簡單單的實現你的應用程式和服務就好,只要專注於它們應該做的功能就好。

這樣是不是說這些架構可以幫忙把大家從服務發現、負載均衡、容錯等功能中解放出來?那為什麼這些還要是應用層的事?

如果你使用Kubernets(或者某些變種),那麼答案就是:是的。

Kubernetes方式的服務發現

用Netflix OSS時通常你要建立一臺服務發現伺服器,讓所有可以被各種客戶端發現的服務註冊在這裡。比如你用Netflix Ribbon來與各種其它服務互動,就需要能找出來它們都執行在哪裡。各種服務可能下線,可能按它們自己的執行需要退出,也有可能我們要向叢集中加入更多服務來做橫向擴充套件。這種集中式的服務發現註冊機制通常可以幫助我們跟蹤叢集中有哪些服務是可用的。

問題之一是,做為一個開發人員你需要做這些事情:

  • 決定我到底是想要一個AP系統(ConsulEureka等)還是CP系統(ZooKeeperetcd等等)。
  • 想明白在上了規模時如何執行、管理和監控這些系統(這可不是一個小小的演示系統)。

而且,你要找到對應你使用的程式語言的客戶端庫,才能與服務發現機制通訊。回到我們剛才討論的問題,微服務可能是用許多不同種語言實現的,所以可能一個成熟的Java客戶端庫好找,但相應的Go或Node.js庫就沒有了,那你只好自己寫一個。可對每一種語言和每一個程式設計師,他們都可能對怎麼實現這樣的客戶端庫有自己的想法,這樣你就會忙於維護各種不同的客戶端庫,做的事情功能都是一樣的可是實現邏輯卻各自不同。也有可能每種語言都會使用它自己的服務發現伺服器而且也有它自己現成的客戶端庫呢?所以你就要為每一種語言都管理和維護不同的服務發現伺服器嗎?不管哪種方式都是非常令人討厭的。

如果我們使用DNS能解決問題嗎?

這樣就解決了客戶端庫的問題嗎?DNS是所有使用TCP/UDP的系統自帶的,不管你是部署在單機、雲、容器、Windows或是Solaris等等哪種系統上。你的客戶端程式只需要指向一個域名(比如http://awesomefooservice/),然後底層框架就知道該把服務路由到DNS指向的地方了(也可能是用VIP加負載均衡,或者輪詢DNS等等)。好!現在我們不必再關心我需要使用什麼樣的客戶端程式來發現一個服務了,我只需要使用任意一種TCP客戶端就好。我也不必關心如何管理DNS叢集,它是網路路由器自帶的,大家都用得很熟。

但DNS對彈性發現的情況可能表現很糟糕

缺點:DNS對於彈性的、動態的服務叢集就表現不好了。要向叢集中加入新服務時該怎麼辦?要下線服務呢?服務的IP地址可能快取在DNS伺服器或路由器中(也許並不屬於你的管轄範圍),也可能在你自己的IP棧中。還有如果你的程式或者服務要偵聽非標準的80埠呢?DNS是預設使用標準的80埠的。要讓DNS使用非標準埠,你就要用到DNS SRV記錄,這樣就又回到了最初的問題,你需要在應用程式層有特殊的客戶端庫來發現這些記錄。

Kubernetes服務

咱們就用Kubernetes吧。反正我們要在Docker或者Linux容器裡面執行東西,那就最好是把Docker容器(或者Rocket容器,或者Hyper.sh容器等等)執行在Kubernetes裡面了。

(看,我的技術其實很差,尤其是對於那些看起來好象很“簡單”的技術——因為你不可能用很複雜的元件來搭建出一個很複雜的系統。你會想要簡單的元件。但寫出簡單的元件這事本身其實很複雜。要我理解象Google或Red Hat做的和Kubernetes相關的、來簡化分散式系統部署、管理等等的事對於我來說實在是象是天方夜譚。:) )

使用Kubernetes我們只要建立並使用Kubernetes服務就好了。我們不必浪費時間去搭建服務發現伺服器、寫定製化的客戶端庫、調校DNS等等。它直接就可用,我們只要直接進入我們微服務的下一個主題即可,即提供業務價值。

它是怎麼工作的呢?

下面這些簡單的抽象都是Kubernetes實現了來支援這些功能的:

Pod很簡單,它們基本上就是你的Linux容器。標籤也很簡單,它們基本上就是一些用於標記你的Pod的鍵-值型字串(比如Pod A有如下標籤:app=cassandra、tier=backend、version=1.0、language=java)。這些標籤可以是任何你想起的名字。

最後一個概念是服務。也很簡單,一個服務就是一個固定叢集IP地址。這個IP地址是虛擬的,可以用來指向真正提供服務的Pod或者容器。那這個IP地址怎麼知道該找哪一個Pod或者容器呢?它是用“標籤選擇器”來找到所有符合你要找的標籤的Pod的。比如,假如我們需要一個有“app=cassandra AND tier=backend”標籤的Kubernetes服務,它就會幫我們找到一個VIP,指向任何一個同時有這兩個標籤的Pod。這個選擇器是動態工作的,任何加入叢集的Pods都會根據它所帶有的標籤立刻自動參與服務發現。

另一個用Kubernetes作為Pod選擇服務的好處是Kubernetes非常智慧,它知道哪個Pod是屬於哪個服務的,還會考慮它的存活和健康情況。Kubernetes會使用內建的存活和健康檢查機制來判斷一個Pod是否應該被加入叢集,依據是它是否存活和它是否在正常工作。對於那些不符合條件的它會直接剔除掉。

要注意的是,一個Kubernetes服務的例項不是一個什麼“東西”,也不是應用、Docker容器等等任何東西,它是個虛擬的東西,所以自然也不會有單點故障。它就是一個由Kubernetes路由的IP地址。

這對於程式設計師來說實在是令人難以置信的強大和簡單。現在如果一個應用想要使用一個Cassandra服務,它會直接使用固定IP地址來找到Cassandra資料庫。但寫死一個固定IP地址肯定不是什麼好做法,因為當你想要把你的程式或者服務挪個地方時就會遇到麻煩。所以一般做法是改IP(或者加個配置項),可這樣又加重了程式配置功能的負擔,最終解決方案通常就是DNS。

使用Kubernetes內的DNS叢集就可以解決上述問題。因為對於一個特定的執行環境(開發、QA等)IP地址是固定的,那我們就不介意直接使用固定IP,反正它也永遠不會變。但如果我們是使用DNS的話,舉個例子,就可以把程式配置成與http://awesomefooservice上的服務互動,這樣不管我們的執行環境怎麼變,從開發改到QA再改到生產,我們都會部署那些Kubernetes服務,業務程式就不需要改了。

我們不需要做額外的配置,也不用費心DNS快取或SRV記錄、定製客戶端庫或管理額外的服務發現框架等等。Pod可以自動加入叢集或從叢集中剔除掉,Kubernetes服務的標籤選擇器會動態的依據標籤分組。業務程式只需要與http://awesomefooservice/互動即可,隨便你是個Java應用,或者是Python、Node.js、Perl、Go、.NET、Ruby、C++、Scala、Groovy等什麼語言寫的都行。這種服務發現機制就不強求必須使用什麼特定的客戶端,你隨便用就好了。

這樣服務發現功能就大大簡化了。

客戶端側的負載均衡怎麼辦?

這事很有趣。Netflix提供了Eureka和Ribbon兩個用於客戶端側的負載均衡,你也可以組合使用。基本實現就是服務註冊(Eureka/Consul/ZooKeeper等等)功能在跟蹤叢集中都有什麼服務,並且會向關心這些資訊的客戶端更新這些資訊。這樣客戶端就知道了叢集中都有哪些節點,它只需要選一個(隨機,或者固定,或者任何它自己定製的演算法)然後呼叫就好。等下一次再呼叫時,它想的話它也可以換另一個來呼叫。這樣的好處是我們不需要那些可能會迅速成為系統瓶頸的軟/硬負載均衡器。另一個重要方面是,當客戶端知道服務在哪裡之後,它直接與服務互動即可,中間不需要再經過中轉。

但依我拙見,客戶端側的負載均衡案例只能佔實際情況的5%。我來解釋一下。

我們想要的是一種理想的、可擴充套件的負載均衡機制,而且不要有額外的裝置和客戶端庫等。大多數情況下我們並不介意處理過程中請求會多一跳到負載均衡器(想想看,可能你99%的應用都是這麼做的)。我們可能會碰到這樣的情況:服務A要呼叫B,B還要呼叫C,然後D、E,等等,想像一下那條呼叫鏈。這種情況下如果每次呼叫都要增加額外的一跳的話,我們的整體延遲就會變得很大。所以可能的解決方案就是要“減掉這多餘的一跳”,但這一跳也可能不止是到負載均衡器的,更好的辦法是要減少那條呼叫鏈的層級。請參考我部落格上事件驅動系統專題中關於“自治與集中”的討論,我已經考慮過這樣的問題了。

根據上文將Kubernetes服務用作服務發現一節所描述的辦法 ,我們可以有不錯的負載均衡機制(也不會有各種服務註冊、定製客戶端、DNS缺點等額外開銷)。當我們通過DNS或IP來與Kubernetes服務互動時,Kubernetes會預設地就在叢集中的Pod之間做負載均衡(注意叢集是由標籤和標籤選擇器定義的)。如果你不想有負載均衡的額外一跳也不用擔心,虛擬IP是直接指向Pod的,不會經過實際的物理網路中轉

那95%的案例就輕鬆搞定了!所以不必過度設計,簡單就好。

那剩下的5%的情況怎麼辦?可能你會有這樣的情況,你的程式要在執行時決定呼叫叢集中的哪個具體終端節點。一般來說你可能會想用些複雜的定製演算法,而不是常用的輪詢、隨機、固定某一個等等。這時就可以使用客戶端側的負載均衡機制。你仍然可以用Kubernetes的服務發現機制來找出叢集中有哪些Pod是可用的,再根據標籤來決定呼叫哪個。fabric8.io社群的Kubeflix專案為Ribbon提供了發現外掛,比如通過Kubernetes的REST API獲得一個服務的所有Pod,然後呼叫者可以用程式碼來根據業務選擇具體呼叫哪個,不限程式語言。對於這些情況,花些精力來實現根據不同客戶端情況定製的發現機制程式碼庫是值得的,更好的做法是把這些定製的邏輯模組化,把依賴關係從業務程式中獨立出去。這樣使用Kubernetes時,就可以把這些獨立的演算法模組也隨著你的程式和服務部署上去,就可以方便的使用定製的負載均衡演算法了。

我還是那句話,這是5%的需要有額外複雜處理的情況。對於95%的情況,使用內建的機制就夠了。

容錯又怎麼辦?

在搭建有依賴關係的系統時要時刻記得每個模組要對別人提供什麼服務,就是說即使呼叫方不存在或者崩潰了,它也要記得自己的義務。Kubernetes在容錯方面又有什麼功能呢?

Kubernetes的確是有自愈功能的。如果一個Pod或者Pod中的一個容器掛掉了,Kubernetes可以把它再拉起來以維持ReplicaSet的不變性。比如你配置想要有10個叫“foo”的Pod,那Kubernetes就會幫你一直維持這個數量。即使某個Pod掛了,它也會再拉起一個來保持住10這個總數。

自愈功能太強大了,而且是隨著Kubernetes原生提供的,但我們討論這個的原因是如果被依賴物(資料庫或其他服務)掛掉了,依賴它的業務程式該怎麼樣?這完全要靠業務程式自己決定怎麼處理了。舉例來說,如果你想在Netflix上看個電影,就會有個請求傳送到授權服務上,校驗你是否有許可權去看那個電影。可如果授權服務掛了該怎麼辦呢?就不給使用者看那個電影嗎?或者把出錯日誌打給使用者看?Netflix的做法是允許使用者看。當授權服務出錯時,允許一個無許可權的使用者看某個電影,這種體驗比直接拒絕要好得多,也許人家有這個許可權呢?

比較好的做法是優雅的降級,或者找出替代方案來持續提供服務。Netflix Hystrix就是一個非常好的Java解決方案,它實現了機制來做隔離熔斷回滾。每一種都是針對不同業務的具體實現,所以在這種情況下,針對不同的程式語言有定製的客戶端庫也是非常合理的。

Kubernetes也有類似功能嗎?當然!

再看看強大的Kubeflix專案,你可以用Netflix Turbine專案來累積並且將你的叢集中執行的所有斷路器視覺化。Hystrix可以把所有的伺服器事件以資料流的形式傳送出去,由Turbine消費掉。那Turbine又怎麼知道哪些Pod裡面有Hystrix呢?好問題,這個可以用Kubernetes的標籤解決。如果給所有有Hystrix的Pod全都打上個“hystrix.enabled=true”的標籤,Kubeflix Turbine引擎就可以自動發現每個Hystrix斷路器的SSE流,並且把它們展現在Turbine的網頁上。太感謝你了,Kubernetes!

配置管理怎麼辦?

Netflix Archaius是用於處理雲上系統的分散式配置管理的。用法與Eureka和Ribbon一樣,搭起一個配置伺服器,再用一個Java庫去查出配置項的值就可以了,它也支援動態更改配置等。請記住Netflix是為了在AWS上構建系統實現的這個功能,但是屬於Netflix的。作為CI/CD管道的一部分,他們要構建AMI並且部署,可構建AMI或任何VM映象都是非常耗時的,並且大多數時候都要有很多前置工作。有了Docker或Linux容器,事情就容易多了,接下來我將從配置的角度解釋一下。

還是先說95%的情況。我們希望把環境相關的配置資訊儲存在我們的業務程式之外,再在執行時從執行環境(開發、QA、生產等)中獲得它們。這裡有個非常重要的區別,不是每個配置都和環境相關,要隨著執行環境來改的。而且我們也非常想要有與程式語言不相關的辦法來查詢配置,避免強迫大家用Java,然後又要配一堆的Java庫和路徑等。

我們可以用Kubernetes來提供基於環境的配置管理:

我們可以把配置資訊通過環境變數提供給Linux容器,這樣不管Java、Node.js、GO、Ruby還是Python等等,大多數程式語言都可以很容易獲取。也可以把配置資訊儲存在Git上,再把Git Repo和Pod捆綁起來,對映成Pod本地檔案系統的檔案,這樣任何程式設計框架都可以以獲取本地檔案的方式來獲取配置資訊了,這是個好方案。最後,也可以通過Kubernetes的ConfigMap來把版本化的配置資訊儲存在ConfigMap中,它也是做為檔案系統載入到Pod上,這樣就可以將Git Repo解耦出去了。獲取配置資訊的方法仍是從檔案系統的配置檔案中讀資料,你自己喜歡用什麼程式語言或者框架都好。

另外5%的情況呢?

在剩下的5%的情況下你可能想要在程式執行時動態更改配置資訊。這一點Kubernetes可以幫忙。你只需要在ConfigMap中更改配置檔案,然後把那些改變動態的推送到載入了它的Pod上就好了。在這個方案裡,你要使用客戶端的庫來幫助你感知到這些配置的改動,並且把它們提交到業務程式中。Netflix Archais就提供了有這樣功能的客戶端。Java版的Spring Cloud Kubernetes在用了ConfigMap時處理這樣的事情更容易。

Spring Cloud怎麼樣?

使用Java的程式設計師們在Spring下開發微服務時常常把Spring Cloud和Netflix OSS等同起來,因為它很大一部分就是基於Netflix OSS實現的。fabric8.io社群中也有很多用Kubernetes執行Spring Cloud的好東西,請檢視https://github.com/fabric8io/spring-cloud-kubernetes。包括配置、日誌等在內的很多模式都可以用Kubernetes執行得非常好,不用藉助服務發現引擎、配置管理引擎等額外的、複雜的框架。

小結

如果你正在構建自己的微服務,而且你也對Netflix OSS/Java/Spring/Spring Cloud等方案很感興趣,請一定提醒自己你們不是Netflix,所以不必直接呼叫AWS EC2的原語來把自己的程式搞得非常複雜。如果你在調查Docker的方案,那採用Kubernetes是個非常明智的選擇,它本身就自帶許多這樣的分散式系統功能。請在合適的時候將業務級程式庫分層,這樣可以從一開始就避免把你的服務搞的太複雜,因為Netflix在5年前就非常明智的開始這樣做了。事實上他們也是不得不這樣的,但想想如果5年前他們有Kubernetes又會是怎樣?他們的Netflix OSS棧會看起來完全不同的! :)