# 第14章 服務發現
服務發現是一種容許計算機網絡上的任意服務均可以找到它所需要通信的其他服務的機制。它是大多數分布式系統的核心組件。如果用戶的基礎設施是運行或者遵循的面向服務的[架構](https://en.wikipedia.org/wiki/Service-oriented_architecture)(Service Oriented Architecture,SOA),那么毫無疑問,用戶需要部署某種服務發現方案。同時,服務發現也是軟件應用設計方面的一個新興概念,這和傳統的SOA有許多的相似之處,而如今它有一個更廣為人知的名字叫做[微服務](http://martinfowler.com/articles/microservices.html)。
服務發現的定義相當簡單(如圖14-1所示):**一個客戶端如何才能找到它想要與之通信的IP地址和服務端口**?

圖14-1
針對這個問題,不同的解決方案往往隱藏了很多用戶不知道的微妙之處。為了能更好地理解服務發現的理念,我們首先至少需要為它約定一些基本的要求。一個服務發現方案必須遵循的最低要求如下。
- **服務注冊/服務聲明**:即服務在它所屬的網絡上聲明自己存在的過程。其通常做法是在某種服務數據庫里加上這行記錄,該數據庫通常也被稱為**服務中心**或**服務目錄**。服務中心里的每行條目必須至少包含服務的IP地址和端口信息,最好還包含服務的一些元數據,如使用的協議、具體的環境、版本等。
- **服務查找/服務發現**:即從網絡上找出想要與之通信的服務的具體連接信息的過程。該過程具體可以歸結為查詢服務目錄數據庫然后找出給定服務的具體IP地址和端口信息。理想情況下,應該可以通過不同的維度如上述所提到的幾個元數據,來查詢服務目錄里的數據。
服務注冊的過程其實比我們上述所提到的情況要復雜許多。一般來說,有以下兩種實現方式。
- 把服務注冊的模塊直接嵌入到應用服務的源代碼里。
- 使用一個伙伴進程(或者說是協同進程)來幫忙處理注冊的任務。
將服務注冊嵌入到應用源代碼中,這會對客戶端的庫有一定的要求。一般來說,一個特定編程語言可用的客戶端的庫也是有限的,因此為了實現這一點,用戶可能會寫很多額外的代碼,這可能會給用戶的代碼庫引入很多不必要的復雜度。在應用的源代碼里嵌入服務發現的代碼常常會導致產生所謂的**胖客戶端**,這部分代碼不易編寫,并且常常會造成調試方面的困擾。這一方案會將用戶綁定到一個特定的服務發現方案上,這可能會導致用戶的應用和服務缺乏一定的可移植性。最后,如果用戶想要在自己的基礎設施上運行和集成一些第三方的服務,如`redis`,那么可能需要費些力氣。但是,如果能夠建立一套穩定的、積極維護的方案,用戶可以從這里面得到很多好處,如客戶端服務的負載均衡、連接的池化、自動的服務心跳檢測和故障遷移等。
另一種被廣泛采用的服務注冊的方案是在應用服務一側運行一個伙伴進程,然后用它來代表應用服務的注冊。這一方案的好處也是顯而易見的。它非常實用的一點在于不需要應用程序的作者為此編寫任何額外的代碼。通過把伙伴進程集成到init系統[\[1\]](part0020.xhtml#anchor141)里,可以確保(一定程度上來說)服務只有當它完全啟動并且正常運行的時候方才注冊。采用這種方案來注冊服務同樣也使它可以輕松地和第三方服務整合到一起。但是,它的缺點在于用戶不得不為每個需要注冊的服務運行一個單獨的進程。此外,使用伙伴進程也會對實際的服務注冊過程提出額外的要求:你怎么為自己的伙伴進程提供你想要注冊的服務的配置呢?由于服務注冊經常需要更新應用服務的一些元數據,因此了解應用的配置是非常有必要的。顯然,伙伴進程這個方法會帶來一些好處,但是它也同樣會引入一些運維方面的挑戰。
基礎設施越復雜,我們要求放到服務目錄數據庫里的數據也會越多。一個更加全面的解決方案理應該可以提供更多的內容(與之前的兩種方案相比),特別是在以下幾個方面:
- 服務目錄數據庫的高可用性;
- 能夠輕松將服務目錄數據庫擴展到多臺主機,同時保證一定級別的數據一致性;
- 告知相關服務的可用性,以便在服務不可用時及時地將它從服務目錄中刪除;
- 告知一些特定服務目錄條目的變更,如當一些服務的元數據變更時。
什么,使用Docker是不是這些事情都得做?好吧,實際上Docker會使服務發現的問題更加明顯。特別是在開始將容器基礎設施擴展到多臺宿主機的時候這一點更為關鍵。一旦用戶開始使用Docker來打包和運行應用,用戶會發現自己在不停地查找運行在Docker容器里的特定服務正在監聽的端口和IP地址。幸運的是,Docker使得查找服務連接信息變得很簡單:用戶需要做的只是通過Docker守護進程去查詢一下遠程API暴露給自己的信息。很多時候用戶最終選擇的是編寫一些簡單的shell腳本,然后將服務連接的具體信息以環境變量的形式通過Docker API傳入新容器里。雖然對于本地環境而言這是一個不錯的便捷方法,但是它不是一個易于擴展和可持續的方案。這不僅在于定制腳本的維護的艱難,更主要的是當應用或服務實例分布到多臺宿主機或者多個數據中心時情況會變得更為糟糕。這個問題在云環境這樣的服務更替頻繁的場景下會暴露得更加明顯。
在本章中,我們會盡力覆蓋到服務發現這個話題的方方面面,并且從實際情況出發,探討在運行Docker容器時可以采用的多種開源方案。那么,打起精神來,讓我們開始吧。
DNS是支撐起[萬維網](https://en.wikipedia.org/wiki/World_Wide_Web)的核心技術之一。從整體上來說,DNS是一個主要用于將人類可讀的名稱解析為機器可讀的IP地址的一致性的分布式數據庫。眾所周知,它也被用于查找負責一個指定域的電子郵件服務器。用DNS做服務發現似乎是一個很自然的選擇,因為它一個已經被充分檢驗過、被廣泛部署并且非常易于理解的技術。對此,業界有豐富的可供選擇的開源服務的實現,與此同時幾乎能想到的任何一門編程語言都會提供相當不錯的全套客戶端庫。DNS支持客戶端緩存和基礎的域名代理,這也使得它成為一個可擴展性很強的解決方案。
DNS最易于理解的部分莫過于早前所提到的通過DNS的[A記錄](https://en.wikipedia.org/wiki/List_of_DNS_record_types#A)實現從域名解析到IP地址的過程。然而只使用A記錄做服務發現是遠遠不夠的,因為它們不會提供指定服務所監聽端口的任何信息。此外,A記錄無法提供關于運行在用戶的基礎設施里的服務的任何元數據信息。為了能夠用DNS實現全功能的服務發現,我們至少需要用到以下兩個額外的DNS記錄:
- [**SRV**](https://en.wikipedia.org/wiki/SRV_record)——通常用來提供網絡上的服務位置信息,如端口號等;
- [**TXT**](https://en.wikipedia.org/wiki/TXT_Record)——通常用于提供多個服務的元數據信息,如環境,版本等。
這些資源記錄可以說是使用DNS作為服務發現的解決方案的最低要求。當然也可以根據國際慣例在公知端口號[\[2\]](part0020.xhtml#anchor142)下運行服務,這樣一來便可以簡單地只使用A記錄來實現服務發現,然而,這樣一來用戶將無法完成一些自己本來需要的復雜查詢操作。
注冊和注銷服務時需要在DNS服務器配置里添加或刪除指定的DNS記錄,并且常常需要重新加載一次服務以使得變動生效。用戶必須實現一定程度的自動化,以保持DNS服務端的配置和線上自己基礎設施里正在運行的應用服務的配置是一致的。DNS誕生于一個相對“靜態”的互聯網時代,與如今新的“云時代”下服務器和服務的更替都非常頻繁的情況相比,當時它并不需要頻繁地去更新DNS記錄。由于DNS基礎設施采取的是多層緩存機制,因此DNS記錄的修改也需要一定的傳播時間才能使之完全生效。用戶常常試圖采取把[TTL](https://en.wikipedia.org/wiki/Time_to_live)值降低到最小的方法來解決這個傳播時間的問題,但這樣做的話可能會產生太多不必要的網絡流量,進而拖慢通信的速度,并且會因為服務的查找動作過于頻繁而給DNS服務器增加額外的不必要的負載。
與經過實戰檢驗的[bind](https://www.isc.org/downloads/bind/)服務相比,雖然業界一些著名的[DNS服務器實現](http://bind-dlz.sourceforge.net)在配置方面采取了更加靈活的方式,但是用戶常常不愿意運行他們自己的DNS服務器,因為除了原有方案已經實現的大量的自動化工作以外,他們還需要花費相當大的運營和維護成本來維護它們。云服務商們還提供了很豐富的帶有簡單API控制的DNS服務,如AWS的[Route53](http://aws.amazon.com/cn/route53/),使用它們的話可能需要花費額外的精力來編寫和維護代碼,這樣看來這些方案也不是非常可取的,并且它們始終沒有解決服務繁雜的問題。的確,天上可不會掉餡餅。
隨著Docker和**微服務**架構的興起,DNS服務器也從現有模式中衍生出了新品種。這些DNS服務器解決了部分之前討論過的問題,而且常常可以被用來提供簡單的服務發現,并且它的服務對象還不只是運行在Docker容器里的服務。在下一節里,我們將介紹一些最廣為人知的實現方案,并且探討下應該如何在基礎設施里使用它們。
最廣為人知的“新生代”DNS服務實現之一是一款名為[SkyDNS](https://github.com/skynetservices/skydns)的軟件,它可以被用在基礎設施里實現服務發現。可以從源碼編譯它也可以將它打包成Docker容器部署。可以從[Docker Hub](https://hub.docker.com/r/skynetservices/skydns/)上找到它的Docker鏡像。SkyDNS的最新版本采用的是`etcd`服務來存儲它的DNS記錄。我們會在本章的稍后部分詳細介紹`etcd`。現在,讓我們姑且認為`etcd`是一個分布式的鍵值對存儲吧。
SkyDNS提供了一個遠程的JSON API,它允許用戶通過發送`HTTP POST`請求到指定的API端點的方式來動態地完成服務的注冊。這樣的話它會自動創建一個`SRV`DNS記錄,正如我們之前所了解的,它可以被用于發現在網絡上運行的服務的連接端口信息。自己也可以將`TTL`設置成任意值,如此一來,一旦`TTL`過期了它會自動注銷該服務。SkyDNS也提供了[DNSSEC](https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions)的功能。要想設置它的話自己需要做一些額外的配置。可以在GitHub上閱讀該項目的[文檔](https://github.com/skynetservices/skydns/blob/master/README.md)。
如果想把SkyDNS和Docker一起使用,就得寫一個專門的SkyDNS客戶端庫來幫忙完成服務的注冊。幸運的是,正如前面提到的,多虧SkyDNS本身提供遠程API,這應該不會是一個大問題。一旦服務完成了注冊,便可以用任一DNS庫來查詢它的具體信息。豐富的開源DNS庫在這時體現出了巨大的價值。一個更簡單的使用SkyDNS集成到Docker的方案便是使用Docker公司的[Michael Crosby](https://twitter.com/crosbymichael)開發的[skydock](https://github.com/crosbymichael/skydock),雖然目前來說它的可擴展性還不是很強。Skydock將會監聽Docker API的事件,然后自動地為用戶處理所有Docker容器的服務注冊工作:會幫用戶自動完成服務的注冊和注銷。Skydock的唯一問題便是它目前只能用在一臺Docker宿主機上,然而,這一點可能在以后會得到改善。Skydock的確是一款值得密切關注的工具。如果想找到更多關于Skydock以及它是如何應用到Docker容器的內容,這里有一個非常棒的由Michael貢獻的[YouTube視頻](https://www.youtube.com/watch?v=Nw42q1ofrV0),他會帶你領略所有的Skydock特性。
Docker DNS服務器領域的另外一員便是[`weave-dns`](https://github.com/weaveworks/weave/tree/master/weavedns)。`weave-dns`最大的好處在于用戶不必花費太多精力就能使用它提供的一些開箱即用的功能。和Skydock一樣,它會監聽Docker API的事件然后通過自動添加和刪除特定的DNS記錄來完成容器服務的注冊。與Skydock只能在一臺Docker宿主機上使用不同,`weave-dns`允許跨多臺Docker宿主機工作。然而,如果想要充分發揮它的優勢,就必須在專有的`weave`覆蓋網絡上使用它,這對于一些用戶而言可能是一個不太好的消息。`weave`網絡是一個**軟件定義網絡**([SDN](https://en.wikipedia.org/wiki/Software-defined_networking)),它可以為用戶提供一個簡單而安全的跨多臺Docker宿主機的覆蓋網絡,然而,如果用戶已經使用了其他的SDN方案,那么`weave-dns`也許不是一個最佳選擇。此外,目前`weave-dns`依賴于用戶使用公知端口號來提供服務,而不是查詢`SRV`或`TXT`記錄。
隨著Docker生態系統的快速擴張和成長,以后可能還會有更多可供使用的DNS服務器實現,有的可能作為獨立的服務,有的可能是作為成熟的SDN產品的一部分提供。由于篇幅的限制,本書不可能覆蓋到所有可供選擇的方案,其他的備選方案就留待讀者自己繼續挖掘和探索。接下來我們將介紹一些已經成為分布式系統領域事實標準的服務發現方案。我們將從著名的Zookeeper項目開始。
Zookeeper是一個[Apache基金會](http://www.apache.org)的項目,它為分布式系統提供分布式的協同服務。它還提供了**簡單的基本授權**功能,允許客戶端建立更加復雜的協同功能。Zookeeper旨在成為一個分布式服務的核心協調組件或者是搭建強大的分布式應用時的一個基礎組件。關于Zookeeper的介紹我們甚至可以寫一整本書來講解,而且實際上這樣的[書](http://www.amazon.com/ZooKeeper-Distributed-Coordination-Flavio-Junqueira/dp/1449361307)已經有了。在這里我們將不再探討那些基礎的概念了,取而代之的是,我們將會把重點放在該如何在基礎設施里采用Zookeeper作為用戶的服務發現方案。
從整體上來說,Zookeeper提供了一個名為`znode`的分布式*內存*數據存儲寄存器。它們以類似標準文件系統的組織方式存放在分級的命名空間里。znode這樣的層次結構通常被稱為“數據樹”。`znode`通常有兩種類型:
- **普通的**——可以被客戶端顯式創建和刪除;
- **臨時的**——與普通的一樣,此外客戶端還可以選擇委托,一旦客戶端會話終止便會自動把它們從集群里刪除。
客戶端可以在集群里的任意`znode`上設置**監聽**,它可以讓Zookeeper自動地通知客戶端任何數據上的更改或刪除。可以說,Zookeeper最大的優勢之一在于它提供的API的簡潔性:它只提供了7種[`znode`操作](http://zookeeper.apache.org/doc/r3.2.1/zookeeperOver.html#Simple+API)。借助于[Zookeeper原子廣播](http://web.stanford.edu/class/cs347/reading/zab.pdf)(ZAB)一致性算法的實現,Zookeeper提供了健壯的數據一致性保證和分區容忍性。簡單來說,ZAB定義了一個領導者和一群追隨者(他們可以共同選舉出領導者)。所有的寫請求都會轉發到領導者這邊,隨后領導者會將它們應用到系統中。讀請求則可以被追隨者們消費。Zookeeper只能在服務器的法定人數(大多數)都正常的情況下正常工作,因此用戶必須保證Zookeeper集群的部署數量總是保持在3、5等這樣的奇數單位。或者更官方地說,用戶必須保證運行的集群有`2n+1`個節點(*n*是一個代表服務器數量的正整數)。這樣規模的集群可以容忍*n*個節點的故障。前面所提到的屬性對Zookeeper的可擴展性有一定的影響。增加新節點可以提高讀的吞吐量,但是會降低寫的吞吐能力。此外,當仲裁發生時,它必須等待遠程站點選舉領導者的投票結束,這樣也就導致寫的速度會有所下降,因此,如果想跨多個數據中心運行Zookeeper集群,用戶應該事先考慮好這些問題的應對措施。
可以通過利用Zookeeper原生提供的臨時`znode`特性來實現服務發現。服務在注冊時會在集群里的一個在其啟動時給定的命名空間下創建一個臨時`znode`,然后將它在網絡上的位置信息(IP地址和端口)填充進去。Zookeeper層級式的命名空間為同類服務的集合提供了一個簡單的實現機制,當基礎設施里運行了多個同種服務的實例時,這個實現機制相當有用。
服務注冊必須要嵌入到相關服務的源代碼中,或者用戶也可以編寫一個簡單的伙伴服務,它將使用Zookeeper協議并處理服務本身的注冊工作。事實上,無論選擇哪種方式都無可避免地需要編寫一些額外的代碼。客戶端可以通過檢索特定的Zookeeper `znode`的命名空間里的信息來發現已注冊的服務數據。和之前提到的一樣,只要被注冊的服務創建的`TCP`會話仍然是活動狀態,那么臨時`znode`便不會被刪除。而一旦服務從Zookeeper斷開連接,`znode`就會被刪除并且該服務馬上會被注銷。客戶端可以在他們想要被告知相關情況的[`znode`端設置監聽](http://zookeeper.apache.org/doc/r3.2.1/zookeeperOver.html#Conditional+updates+and+watches)。當`znode`發生變化時監聽會被**馬上觸發然后刪除**。
Zookeeper是完全使用Java編寫的。該項目也提供了一個完備的Java客戶端庫來完成和Zookeeper集群的交互。雖然也有通過其他[編程語言實現](https://cwiki.apache.org/confluence/display/ZOOKEEPER/ZKClientBindings)的客戶端,但是并不是所有的客戶端都會提供全面的功能特性的支持,而且他們的實現也各有差異,這有時候會使終端用戶產生一些困擾。客戶端必須處理服務發現和自動服務故障遷移兩方面的負載平衡,以防出現客戶端查找時一些發現的服務不再響應或它們的Zookeeper會話已經關閉的情況。如果用的是Java編程語言,那么這里有一個很棒的庫,它在Zookeeper客戶端庫的基礎上進行了一下包裝,并且提供了很多額外的開箱即用的功能。它叫做[Curatorr](http://curator.apache.org)。同Zookeeper一樣,它也是一個Apache基金會項目。關于如何使用Curator,可以查看Curator的[入門文檔](http://tomaszdziurko.pl/2014/07/zookeeper-curator-and-microservices-load-balancing/)。
Zookeeper的確能夠提供一個健壯的、經過實戰檢驗的服務發現方案。然而,在用戶的基礎設施里運行Zookeeper集群會引入一定程度的復雜性,它要求用戶具備一些基本的Zookeeper運維經驗并且會帶來一些額外的維護成本。由于Zookeeper提供了一個強一致性的保證,當網絡出現阻斷時位于非仲裁方的服務即使仍然正常工作也將無法完成注冊或尋找已經注冊的服務。就服務更替非常頻繁的寫負載較重的環境而言,Zookeeper也許不是一個最佳選擇。使用Zookeeper來完成服務發現的最棘手的問題之一便是它依賴已注冊服務創建的TCP會話的持久性來保證服務發現的可用性。而僅僅只存在TCP會話也不能保證服務一定是健康的。應用服務可以執行的任務非常多元化。只檢查TCP會話是否存活的話很難驗證這些任務的健康性。因此用戶不能**只靠TCP連接的活躍度**來判斷服務是否健康!很多用戶低估了這一點并且因此引發了很多意想不到的事情。
如果把Zookeeper當做IT架構里的一個基礎組件,它也許會讓用戶眼前一亮。雖然用戶無法直接將其運用到Docker基礎設施里,但是它可以通過其他構建在它之上的系統“悄然”貢獻自己的一份力。經典案例莫過于[Apache Mesos](http://mesos.apache.org),用戶可以使用它的一個插件然后借助Zookeeper來完成Docker宿主機上Mesos集群的容器之間的調度。如果想把Zookeeper作為一個獨立的服務發現方案來使用,可能需要編寫一個簡單的伙伴客戶端,它會和“Docker化”的應用服務一起運行并且處理服務自身的注冊工作。然而,還有更簡單的方法。用戶可以使用一些基于Zookeeper之上實現的一些解決方案,如Smartstack,關于這塊內容我們將在本章的稍后部分詳細介紹。
接下來,我們將涉足的是分布式鍵值存儲領域,相對于新人而言,它可以說是比Zookeeper更容易的、用作Docker基礎設施里服務發現的方案。我們討論的第一個主題即是這其中一款名為`etcd`的工具。
`etcd`是[CoreOS](https://coreos.com)團隊使用[Go語言](https://golang.org)編寫的一款分布式鍵值存儲軟件。它和Zookeeper有許多的相似之處。我將先介紹它的一些基本特性然后再講述如何將它用于服務發現。
同Zookeeper類似,`etcd`將數據存放在層級的命名空間里。它定義了目錄和鍵的概念(這并不是`etcd`獨創的)。任何目錄都可以包含多個鍵,實際上它們是用來查找存儲在`etcd`中的數據的唯一標識符。存儲在`etcd`中的數據可以是臨時性的也可以是持久化的。`etcd`和Zookeeper主要不同之處在于針對臨時數據的實現方式。在Zookeeper里,臨時數據的生命周期等同于客戶端創建的TCP會話的壽命,而`etcd`采取的是和DNS的做法有些相似的一個方案。任何一個存放在`etcd`里的鍵都會設置一個`TTL`(Time to Live,存活時間)值。該TTL值定義了對應鍵在被設置值以后多長時間會過期,過期的同時該鍵值即被永久刪除。客戶端可以在任意時刻刷新TTL從而延長存儲數據的壽命。客戶端還可以在任意鍵或者目錄上設置監聽,這樣一來當這些數據發生變更時它們便能獲取到相應的通知。`etcd`的監聽機制是通過[HTTP長輪詢](https://en.wikipedia.org/wiki/Push_technology#Long_polling)來實現的。
使用`etcd`的最大的好處之一在于它通過提供一個遠程的JSON API抽象了底層的數據操作。這對于應用開發人員來說是一個天大的喜訊,他們不再需要使用特定的編程語言客戶端來實現這些操作。所有需要與`etcd`交互的操作都可以通過一個簡單的HTTP客戶端來實現(另外,每個`etcd`發行版都默認自帶一個名為**etcdctl**的命令行客戶端工具)。正如[官方文檔](https://github.com/coreos/etcd/blob/master/Documentation/api.md)中的很多例子中展示的,用戶甚至可以簡單地使用`curl`命令來和`etcd`集群進行交互。`etcd`使用[Raft](https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf)一致性算法,通過在集群之間同步日志來管理數據。和Zookeeper使用的ZAB類似,Raft定義了領導者和追隨者節點。所有的寫請求都必須經由通過領導者使之生效,隨后它會將操作以日志形式同步/重演到其余的追隨者節點。
為了使`etcd`能夠正常工作,集群里必須部署2*n*+1個節點,即3、5、7等。可以通過https://coreos.com/ os/docs/latest/cluster-architectures.html來查看推薦的生產環境集群設置方案。同Zookeeper一樣,`etcd`也提供了強大的數據一致性保證和分區容忍。此外,`etcd`還建立了一套強大的[安全模型](https://github.com/coreos/etcd/blob/master/Documentation/security.md),它使得客戶端與集群之間以及集群中各節點之間的通信都可以采用SSL/TLS作為認證方式來完成客戶端的授權。雖然說在`etcd`集群的管理方面可能會遇到一些小小的挑戰。但值得慶幸的是,`etcd`官方提供了很棒的[管理](https://github.com/coreos/etcd/blob/master/Documentation/admin_guide.md)和[集群部署](https://github.com/coreos/etcd/blob/master/Documentation/clustering.md)指導手冊,在將`etcd`集群部署到基礎設施之前,**必須得去讀一讀**這些手冊。當然,`etcd`不只限于這里簡要介紹的內容,因此我建議如果感興趣的讀者不妨去看看它的官方文檔。接下來,我們將討論該如何將`etcd`用于服務發現。
可以通過利用`etcd`的TTL特性來實現服務發現的功能。注冊服務會在`etcd`集群里創建一個新的鍵值對,然后把連接的詳細信息填到里面。這里,可以創建一個單獨的鍵也可以插入到一個現有的鍵的目錄里。`etcd`的目錄提供了一個很好的分組方式,它可以將運行在基礎設施里的相同服務的多個實例歸類到一起(目錄本身也是以一個鍵的形式存在)。該服務隨后可以在一個給定的鍵上設置一個TTL,這樣一來用戶無需對它做任何進一步的操作,存放在其中的數據便會在達到TTL的值后自動過期。借助于TTL,`etcd`實現了一個簡單的自動注銷服務的機制。用戶還可以通過更新TTL的方式來延遲數據的過期時間,這也是一些長期運行的服務必須做的,它們可以借助這一手段來避免不斷的注冊/注銷。客戶端則可以通過查詢`etcd`集群里的特定鍵或者目錄來找出已注冊服務的連接信息。就像之前提到的那樣,客戶端可以在任意一個鍵上設置監聽,然后可以在該鍵存儲的數據發生變化時收到通知。
多虧了有`etcd`提供的遠程JSON API,服務的注冊甚至還可以嵌入到服務的源代碼里,或者也可以使用一個伙伴進程,通過實現一個簡單的HTTP客戶端——`etcdctl`或者`curl`就能辦到——來和`etcd`集群交互。由于使用簡單的命令行工具便能完成服務的注冊,這使得集成第三方服務也變得相當簡單:可以在服務啟動時在`etcd`里添加一個新條目,然后在服務關閉時刪掉該鍵。借助于[SystemD](http://www.freedesktop.org/wiki/Software/systemd/),可以很容易地實現伙伴進程同主服務進程一起啟動、停止。
在使用Docker時用戶也可以輕松地采取相同的策略來實現服務的注冊。伙伴進程會去檢索服務容器的信息,然后將解析后的IP地址和端口數據填充到一個特定的`etcd`鍵里。這個鍵隨后可以被其他Docker容器通過查詢`etcd`集群的方式來讀取。
如果采用伙伴進程來實現服務注冊,最終可能在維護方面會比較傷腦筋,因為用戶需要不斷地更新`TTL`值并監控已注冊服務的健康狀況。用戶一般是通過一個簡單的shell腳本來實現這一點,它會運行一個無限循環,并且以一定的時間間隔不斷檢查正在運行的服務的健康性,然后據此更新特定的`etcd`鍵對應的值。可以通過一個簡單的[實例](https://coreos.com/fleet/docs/latest/launching-containers-fleet.html)來了解這個方案。
至于客戶端,業界已經有大量現成的、不同的編程語言實現的工具和[庫可供選擇](https://github.com/coreos/etcd/blob/master/Documentation/libraries-and-tools.md),這里面的大多數工具的社區仍然相當活躍。再者說,同其他工具一樣,`etcd`原生的[Go語言庫](https://github.com/coreos/go-etcd)應該會提供所有功能特性的支持。遺憾的是,它仍然無法提供一些服務負載均衡或者故障轉移方面的功能支持,因此用戶需要自己去解決這些問題。
`etcd`提供了一個非常不錯的針對服務發現的解決方案。因此,盡管它仍然還在不斷的迭代完善,很多企業已經開始將它應用到他們的生產環境中。具備編程語言無關性的遠程API接口對于應用開發者而言有很大的推動作用,因為它給了他們更多的選擇空間。然而,同Zookeeper一樣,采用`etcd`的話會給用戶的基礎設施引入額外的管理復雜度。用戶需要了解如何操作`etcd`集群,而這一點并不是那么容易就能辦到。通常來說,缺乏對`etcd`內部原理的理解,往往可能導致一些意想不到的事情發生,有些情況下甚至可能會丟失數據。
因此,對于一個初學者而言,根據用戶存儲在集群里的數據容量來完成etcd的擴容工作可能會是一個不小的挑戰。服務目錄里的記錄必須有它們各自的TTL值,然后用戶需要通過已注冊的服務不斷地刷新該值,這需要開發者投入一些額外的精力。如果所處環境里運行的服務本身生命周期很短,那么頻繁更新TTL值會產生相當大的網絡流量。我們這里所提到的`etcd`實際上是許多其他開源項目實現的基石,像之前提到過的SkyDNS或者是[Kubernetes](http://kubernetes.io),并且它已經默認被內置到了CoreOS Linux發行版里。該項目很有可能會得到進一步的發展和顯著的提高。
`etcd`已然成為Docker基礎設施里一個非常流行的、用來實現服務發現解決方案的基本構建組件。一些新的、全套的解決方案都受到了它的啟發。值得一提的是,這里面有一個項目是[Jason Wilder](https://twitter.com/jaswilder)本人創建的。它把`etcd`和另外一款非常流行的名為HAProxy的開源軟件結合了起來。它采取伙伴進程的方式來實現服務發現并且利用Docker API發送相應的事件,隨后用它來生成HAproxy的配置,借此完成Docker容器里運行的服務與其他服務之間請求的路由和負載均衡工作。關于這部分內容,讀者可以在http://jasonwilder.com/blog/2014/07/15/docker-service-discovery/了解更多詳細內容。再強調一次,業界可能已經有很多方案是基于`etcd`實現的服務發現,然而Jason的這個項目把服務發現本身的一些基礎概念講解得非常透徹,并且定義了一個已經在Docker社區里被反復驗證的服務發現模型。圖14-2對該項目進行了簡單地講解說明。

圖14-2
在以上設定中,HAproxy直接運行在宿主機上(即不是運行在Docker容器里)并且為所有運行在Docker容器里的服務提供了一個單一的入口。運行在Docker容器里的服務會在服務啟動時通過在`etcd`里創建一個特定的鍵條目來完成注冊。我們可以利用一個特殊的服務進程(如[conf.d](https://github.com/kelseyhightower/confd))來監控`etcd`集群里的鍵命名空間,一旦發生變化的話它會馬上為之生成新的HAproxy配置并隨后重新加載HAproxy服務使之生效。針對該服務的請求會自動被路由和負載均衡到運行在Docker容器里的其他服務。這有點像Smartstack所推崇的模式,它是另外一款服務發現的解決方案,我們將在本章的后面部分詳細介紹。
由于篇幅有限,關于Docker生態圈里其他那些圍繞`etcd`建立的服務發現方案便留待讀者朋友探索。接下來,我們將介紹一款比`etcd`更加年輕也可以說更加強大的兄弟軟件:`consul`。
consul是一款由[HashiCorp公司](https://www.hashicorp.com)編寫的多功能分布式系統工具。同之前介紹過的`etcd`一樣,consul也是使用Go語言實現。consul很好地將它所有的特性集成為一個可定制化軟件,并易于使用和運維。在這里,我們不會花太多篇幅去介紹什么是consul,關于這一點讀者可以在它的[官方網站](https://www.consul.io/docs/index.html)上找到一個非常全面的文檔,里面包含了大量的實際案例。取而代之的是,我們將會去總結它的主要特性并且探討在Docker基礎設施里如何借助它來實現服務發現。最后,在本章的末尾我們將會介紹一個實際案例,它利consul提供的功能特性,將consul作為一個插件式的后端服務,為運行在Docker容器里的應用提供了即插即用的服務發現功能。
我們選擇用**多功能**一詞來描述consul的目的正是在于consul的確可以無需花費其他任何額外的精力,作為一款單獨運行的工具提供下述任意一項功能:
- 分布式鍵值存儲;
- 分布式監控工具;
- DNS服務器。
上述功能及其易用性使得consul成為DevOps社區里一款非常強大和流行的工具。讓我們快速過目一下,看看consul的背后究竟隱藏了些什么奧秘使得它可以用在如此多的場合。
consul,同`etcd`類似,也是基于Raft一致性算法實現的,這也就是說,它所在的集群里的節點部署數量同樣應該滿足2*n*+1(*n*表示一個正整數)以保證其正常工作。和`etcd`一樣,consul也提供了一個遠程的JSON API,這使得各種不同的編程語言實現的客戶端訪問該服務更加簡單。通過提供遠程API的支持,consul允許用戶自行在其之上構建新服務或直接使用它原生提供的開箱即用的功能。
就部署而言,consul定義了一個代理(agent)的概念。該代理能夠在以下兩種模式運行:
- **服務端**——提供分布式鍵值存儲和DNS服務器;
- **客戶端**——提供服務的注冊、運行健康監測以及轉發請求給服務器。
服務端和客戶端代理共同組成一個完整的集群。consul通過利用HashiCorp編寫的另外一款名為[serf](https://www.serfdom.io)的工具來實現集群成員身份和節點發現。serf是基于[SWIM](http://www.cs.cornell.edu/~asdas/research/dsn02-swim.pdf)一致性協議實現的,并且在一些性能方面做了優化。您可以通過[consul官網](https://www.consul.io/docs/internals/gossip.html)來了解consul中的gossip詳細的內部實現原理。利用gossip協議并通過將它和本地服務的健康檢測結合到一起,這使得consul可以實現一個簡單但是異常強大的分布式故障檢測機制。這對于開發者和運維人員而言實在是一個巨大的福音。開發人員可以在他們的應用程序里公開健康檢測的端點然后輕松地將應用服務添加到consul的分布式服務集合里。運維人員也可以編寫簡單的工具,使用consul的API來監控服務的健康性,或者他們只是使用consul原生提供的[Web UI](https://github.com/hashicorp/consul/blob/master/ui/README.md)去操作。
這里討論到的只是consul一些基本的內容,實際上它所提供的還遠不止這些,因此**強烈建議**讀者去細讀一下consul強大的官方文檔。如果想知道consul和市面上其他工具的差異,不要猶豫,趕緊去看一下專門討論這一話題的[官方文檔](https://www.consul.io/intro/vs/index.html)吧。接下來,讓我們一起來看看我們該如何將consul用于基礎設施里的服務發現。
迄今為止,在我們介紹過的工具中,作為一款可定制的服務發現解決方案,consul無疑是最容易上手的一個。用戶可以通過以下幾種方式將自己的服務注冊到consul的服務目錄里:
- 利用consul的遠程API將服務注冊嵌入到用戶的應用代碼里;
- 使用一個簡單的伙伴腳本/客戶端工具,在應用服務啟動時通過遠程API來完成注冊;
- 創建一個簡單的聲明服務的[配置文件](https://www.consul.io/intro/getting-started/services.html),consul代理可以在服務啟動或重新加載服務后讀取該配置。
已注冊的服務可以通過consul的遠程API直接去查找,當然用戶也可以使用consul提供的開箱即用的DNS服務來檢索它們的信息。這一點尤其方便,因為用戶不必再受限于一個特定的服務查找方案,而且甚至可以不費任何力氣地同時使用這兩套方案。此外,consul還允許用戶為自己的應用服務設計一些自定義的健康檢測機制。如此一來,用戶不必再像之前的Zookeeper那樣和TCP會話周期綁定到一起,也不必像`etcd`那樣和TTL值掛鉤。consul代理程序會持續不斷地在本地監控已注冊服務的健康性并且一旦健康檢測失敗它會立馬自動將其從服務目錄中抹除。
Consul提供了一個非常完備的服務發現解決方案,并且令人意外的是它的成本其實非常低。在consul中,應用服務可以通過遠程API或DNS來定位和檢索。為了完成服務的注冊,需要避免使用遠程API而可以采用更簡單的基于JSON的配置文件來實現這一點。這使得consul能夠很方便地和傳統配置管理工具集成在一起。使用consul會給用戶的基礎設施引入一些額外的復雜度,但是作為回報,用戶也從中獲得了大量的收益。與`etcd`相比,consul集群無疑是更易于維護和管理的。Consul在多數據中心方面也具備很好的擴展能力,事實上,consul提供了一些額外的工具專門負責多數據中心的擴容工作。Consul可以作為一個單獨的工具使用,也可以作為構建一個復雜的分布式系統的基本組件。如今,業界圍繞它已然形成了一個新的完整的工具生態圈。在下一節里,我們將會介紹一款名為`registrator`的工具,它使用consul作為它的一個可插拔的后端服務,它為運行在Docker容器里的應用提供了一個非常易于上手的、自動服務注冊的解決方案。
從整體上來說,`registrator`會去監聽Docker的Unix套接字來獲取Docker容器啟動和消亡時的事件,并且它會通過在事先配置好的一個可插拔的后端服務中創建新記錄的形式自動完成容器的服務注冊。這就意味著它必須以一個Docker容器的身份來運行。讀者可以在[Docker Hub](https://registry.hub.docker.com/u/gliderlabs/registrator/)上找到`registrator`的Docker鏡像。`registrator`提供了相當多的配置參數選擇,因此,盡情去[GitHub項目頁面](https://github.com/gliderlabs/registrator)的文檔庫里去查找關于它們的詳細解釋吧。
下面,讓我們一起來看一個簡短的實際案例。我們將會使用`registrator`把`redis`內存數據庫打包到Docker容器里運行,用戶可以很方便地通過consul發現它在網絡上所提供的服務。當然,用戶也可以使用類似的方法在Docker基礎設施里運行任意的應用服務。
回到這個例子,首先我們需要啟動一個consul容器。這里,需要用到`registrator`之父[Jeff Lindsay](https://twitter.com/progrium)創建的鏡像來實例化具體的容器:
```
# docker run -d -p 8400:8400 -p 8500:8500 -p 8600:53/udp -h
node1 progrium/consul -server -bootstrap
37c136e493a60a2f5cef4220f0b38fa9ace76e2c332dbe49b1b9bb596e3ead39
#
```
現在,后端的發現服務已經開始運行,接下來我們將會啟動一個`registrator`容器,并且同時傳給它一個consul的連接URL作為參數:
```
# docker run -d -v /var/run/docker.sock:/tmp/docker.sock -h
$HOSTNAME gliderlabs/registrator consul://$CONSUL_IP:8500
e2452c138dfa9414e907a9aef0eb8a473e8f6e28d303e8a374245ea6cd0e9cdd
```
如下所示,我們可以看到容器均已成功啟動,并且我們假定所有容器都是注冊的`redis`服務:
```
docker ps
CONTAINER ID IMAGE COM-
MAND CREATED STATUS
PORTS
NAMES
e2452c138dfa gliderlabs/registrator:latest "/bin/regis-
trator co 3 seconds ago Up 2 sec-
onds
distracted_sammet
37c136e493a6 progrium/consul:latest "/bin/start
-server 2 minutes ago Up 2 minutes 53/tcp,
0.0.0.0:8400->8400/tcp, 8300-8302/tcp, 8301-8302/udp,
0.0.0.0:8500->8500/tcp, 0.0.0.0:8600->53/udp furious_kirch
```
考慮到整個例子的完整性,我們不妨介紹一下最初的情況,下列命令展示了我們正在運行的只有一個節點的consul集群并且在該時刻沒有任何已注冊的服務運行:
```
# curl $CONSUL_IP:8500/v1/catalog/nodes
[{"Node":"consul1","Address":"172.17.0.2"}]
# curl $CONSUL_IP:8500:8500/v1/catalog/services
{"consul":[]}
```
現在,讓我們先啟動一個`redis`容器,然后公開它所有需要對外提供服務的端口:
```
# docker run -d -P redis
55136c98150ac7c44179da035be1705a8c295cd82cd452fb30267d2f1e0830d6
```
如果一切順利,我們應該可以在consul的服務目錄里找到該`redis`服務的信息:
```
# curl -s localhost:8500/v1/catalog/service/redis |python -
mjson.tool
[
{
"Address": "172.17.0.6",
"Node": "node1",
"ServiceAddress": "",
"ServiceID": "docker-hacks:hungry_archimedes:6379",
"ServiceName": "redis",
"ServicePort": 32769,
"ServiceTags": null
}
]
```
從上面的輸出可以看到`registrator`定義服務所采用的格式。關于這一點可以轉到[項目文檔](https://github.com/gliderlabs/registrator#how-it-works)了解更多的細節。正如我們在前面章節所了解到的,consul提供了一個原生的開箱即用的DNS服務的支持,因此所有已注冊的服務可以很輕松地通過DNS來查找和定位。要驗證這一點也非常簡單。首先,我們需要找出consul提供的DNS服務器將哪些端口映射到了宿主機上:
```
# docker port 37c136e493a6
53/udp -> 0.0.0.0:8600
8400/tcp -> 0.0.0.0:8400
8500/tcp -> 0.0.0.0:8500
```
太棒了,我們可以看到容器的DNS服務被映射到了宿主機的所有網絡接口上,并且監聽了8600端口。現在,我們可以使用Linux上著名的`dig`工具來完成一些DNS的查詢操作。從consul的官方文檔中我們可以了解到,consul里已注冊服務對應的默認的DNS記錄會以NAME.service.consul的格式命名。因此,在這個例子中,當注冊一個新服務時`registrator`使用的Docker鏡像名便會是`redis.service.consul`(當然,必要的話也可以修改這個設置)。
那么,現在讓我們來試著運行一下DNS的查詢吧:
```
# dig @172.17.42.1 -p 8600 redis.service.consul +short
172.17.0.6
```
如今我們已經獲得了`redis`服務器的IP地址,但是同該服務通信所需的信息還遠不止這些。我們還需要找出該服務器監聽的TCP端口。幸運的是,這一點很容易辦到。我們需要做的只是通過查詢查詢consul的DNS來尋找對應的使用相同的DNS名稱的`SRV`記錄。如果一切順利,我們應該可以看到返回的端口號是32769,當然我們也可以通過它提供的遠程API以檢索consul服務目錄的方式來獲取這個信息:
```
# dig @172.17.42.1 -p 8600 -t SRV redis.service.consul +short
1 1 32769 node1.node.dc1.consul.
```
真的是太棒了!借助consul,我們成功地為我們的Docker容器實施了一整套完備的服務發現方案,而且所有我們需要做的配置只是運行兩個簡單的命令而已!我們甚至無需編寫任何代碼。
如果我們現在停止`redis`容器,consul會將它標記為已停止的狀態,如此一來,它將不會再響應我們的任何請求。這一點同樣也非常容易驗證:
```
# docker stop 55136c98150a
55136c98150a
# dig @172.17.42.1 -p 8600 -t SRV redis.service.consul +short
# dig @172.17.42.1 -p 8600 -t SRV redis.service.consul
; <<>> DiG 9.9.5-3ubuntu0.1-Ubuntu <<>> @172.17.42.1 -p 8600 -t
SRV redis.service.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 56543
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDI-
TIONAL: 0
;; QUESTION SECTION:
;redis.service.consul. IN SRV
;; Query time: 3 msec
;; SERVER: 172.17.42.1#8600(172.17.42.1)
;; WHEN: Tue May 05 17:59:35 EDT 2015
;; MSG SIZE rcvd: 38
```
如果正在尋找一個簡單而容易上手的服務發現的解決方案,`registrator`無疑是一個非常省力的選擇,盡管它仍然需要用戶運行一些像`consul`或者`etcd`這樣的存儲后端。然而,由于其本身具備簡單部署的優勢以及它提供的對Docker的原生集成支持,選用它無疑是利大于弊的。
現在,我們將結束這一節,接著介紹一個新的不依賴任何一致性算法來保證數據的強一致性的解決方案,然而它仍然提供了一些有趣的特性,因此不妨將它當作實現用戶的基礎設施里的服務發現的一個備選方案。
在過去的幾年里,[Netflix的工程師](http://techblog.netflix.com)團隊開發了大量的開源工具來幫助他們管理微服務云基礎架構,以方便那些以前必須創建他們自己的[開源軟件中心](http://netflix.github.io)心才能消費他們的用戶快速上手。大部分的工具都是用Java編程語言編寫的,如果用戶沒有復用其OSS工具箱里的一些其他工具往往會很難將其集成到私有的基礎設施里。在這一節里,我們將會一起來看看[Eureka](https://github.com/Netflix/eureka)究竟提供了怎樣的一個有意思的服務發現的選擇。
Eureka是一個基于REST的服務,它在設計之初最主要的目的是用來提供“中間層”的負載均衡、服務發現和服務的故障轉移。官方推薦的用例場景是,如果用戶在AWS云中運行自己的基礎設施,AWS云是Eureka經過實戰檢驗的地方,而用戶的基礎設施里又存在大量的內部服務,同時又不希望注冊到AWS ELB中或者對外公開這些服務的話,這時候可以考慮采用Eureka實現服務發現。可以說,促成Eureka的最主要的動力便是在于ELB不支持內部服務的負載均衡。理想情況下,由于Eureka本身不提供會話保持的功能,因此通過Eureka發現的服務本身應該都是無狀態的。從架構上來說Eureka主要有兩個組件:
- **服務端**——提供服務的注冊;
- **客戶端**——處理服務的注冊,并且提供基本的、輪換式的負載均衡和故障轉移功能。
官方推薦的部署方案是每個[AWS](http://aws.amazon.com/cn/)地理區域部署一個Eureka的服務集群,或者至少每一個AWS可用區域部署一臺服務器。實際上,Eureka服務端根本不知道其他AWS區域有哪些服務器。它保存信息最主要的目的是為了保證**一個AWS區域內**的負載均衡。Eureka集群里的服務器會以異步的形式同步他們彼此間的服務注冊信息。這一同步操作可能需要花一些時間才能完全生效。由于服務端可能有緩存,因此該操作有時候甚至要耗費數分鐘的時間才能完成。與Zookeeper、etcd或consul相比,**Eureka更偏重于服務的可用性而不是數據的強一致性**,因此客戶端往往可能需要處理臟數據的讀取。沒辦法,Eureka本身便是這樣設計的。它關注的是經常發生故障的云環境下的彈性伸縮問題。采用這種方案就意味著Eureka甚至可以在集群因為某種故障出現網絡分區時仍能工作,不過這樣會犧牲數據的一致性。
Eureka客戶端會將應用服務的信息注冊到服務端,并且隨后每30秒更新一次他們的“租約”信息。如果客戶端沒有在90秒內更新它的租約,那么它會自動從服務端的注冊中心里刪除,然后必須重新注冊。這跟`etcd`里面通過`TTL`實現的機制有些類似,但是如果使用Eureka,用戶無法設置注冊中心里的記錄的生命周期。Eureka客戶端對于服務端故障具備一定的彈性耐受能力。它們會將本地的注冊信息緩存起來,因此即使注冊服務器發生故障,它們依舊可以正常工作,這顯然要求客戶端這一方可以處理一些服務的故障轉移工作。一旦網絡分區的故障排除,客戶端本地的狀態會被合并到服務端。而為了易于排障,Netflix OSS庫里提供了另外一款名為\[Ribbon\]{(https://github.com/Netflix/ribbon)的工具專門用來解決這個問題。
Eureka舊版本的客戶端主要是基于pull模式的,然而最新發布的版本已經加入了大量對于服務端和客戶端兩方面的改進。它將集群的[讀寫](https://github.com/Netflix/eureka/wiki/Eureka-2.0-Architecture-Overview)分離開來,從而改善了性能并提高了可擴展性。Eureka客戶端可以訂閱一組特定的服務,隨后,就像之前介紹過的工具一樣,他們便可以在服務端發生任何更改的時候收到通知。Eureka官方還提供了一個簡單的儀表盤,使其更容易部署到其他的云服務上。您可以轉到如下[鏈接](https://github.com/Netflix/eureka/wiki/Eureka-2.0-Motivations)來了解新版本設計理念方面的更多內容。
就像之前所提到的那樣,Eureka的設計目標是客戶端的中間層負載均衡。要實現服務之間的負載均衡,必須首先看看它們是否在服務端的注冊中心里。由于Eureka是使用Java編程語言實現的,因此用戶可以相當容易地將Java客戶端集成到自己的應用代碼里。原生的客戶端提供了非常全面的功能特性,包括一個簡單的基于輪換式的負載均衡的支持。每個應用服務可以在它啟動的時候到Eureka服務端注冊自己的信息,然后每隔30秒發送一個心跳包到服務端。如果超過90秒該服務仍然沒有動作,Eureka會自動將其注銷。
Eureka還對外開放了一個REST API,如此一來用戶便可以輕松實現自己自己的客戶端。雖然開源界目前已經有幾個不同編程語言實現的客戶端庫,但是沒有哪個客戶端能夠真正在功能覆蓋面及質量上勝過原生客戶端的實現。另外一個選擇是用戶可以實現一個小型的Java伙伴程序,和自己的服務一起運行,然后由它來調用原生的客戶端庫幫忙處理服務的注冊和心跳匯報工作。這將帶來額外的工作量以及不必要的維護復雜度,當然用戶將因此獲得原生客戶端庫的全部功能支持。
使用Eureka來實現服務發現的最大優點在于它對故障方面具備一定的容忍能力,這使得它成為云環境下的一個非常不錯的選擇,當然,前提是用戶可以在客戶端這一邊處理服務的故障轉移以及臟數據的讀取。事實上,云環境的部署需求正是它設計最主要的動力所在。然而,遺憾的是,用戶無法控制服務注冊中心里記錄的生命周期,而且必須不斷地發送心跳包,這樣做的話可能會為自己的基礎設施帶來不小的流量壓力,并且會給注冊服務器帶來一些額外的負載。客戶端查詢的往往也是全量的服務列表數據,在查找服務時也沒有過濾或者搜索的細粒度之分等。關于這一點可能會在2.0版本得到改善,而這一版本也會引入一個服務訂閱的概念,即用戶可以收到有關服務的通知信息。
Eureka在最開始設計的時候便考慮到了自動擴展,因此它的可擴展性相當不錯。此外,Eureka的最新版本對它的擴展能力做了進一步的改善。它的致命要害在于,如果想充分利用Eureka,就必須要用到Netflix的一些其他OSS組件,如之前提到的Ribbon庫或Archaius配置服務,該服務又依賴于Zookeeper。這一點也許讀者早就意識到了,它可能會為基礎設施引入大量不必要的復雜度。
接下來,我們將把視角從Netflix的OSS深淵里挪開,轉而討論一個已經非常流行的不同的變種方案,借助它用戶可以非常便捷地實現服務發現,而且它還有一個非常有趣的名字。那么,讓我們一起來見識一下Smartstack吧!
Smartstack是一個由[AirBnb](http://nerds.airbnb.com)的工程師團隊創建的服務發現解決方案。Smartstack在整個服務發現的生態圈里的地位非常特殊,原因在于它的設計理念啟發了其他的解決方案。正如它的名字所暗示的,smartstack真的是一組智能服務組成的**技術棧**,其中包括[Nerve](https://github.com/airbnb/nerve)和[Synapse](https://github.com/airbnb/synapse)兩個部分。
Nerve和Synapse都是使用Ruby編程語言編寫的,并且以Ruby gem的形式發布。他們可以和[HAproxy](http://www.haproxy.org)以及之前介紹過的Zookeeper交互。http://www.haproxy.org上有一篇很棒的入門性質的博客文章,讀者有興趣的話不妨去讀一讀,在這里可以了解到更多關于Smartstack背后的一些創造動機。在本節里,我們將介紹它的一些主要特性,并在最后做一個簡短的總結。請記住,在打算將Smartstack部署到自己的基礎設施之前,千萬不要猶豫,多看一些它的在線文檔會有很大幫助的。
Smartstack在服務發現的注冊和發現方面是伙伴進程模型的擁護者:`synapse`和`nerve`均是以獨立進程和應用服務一起運行的,并且代表應用服務自動處理服務的注冊及查找工作。在生產環境里,一臺宿主機上運行一個Synapse實例應該就夠了。Smartstack會利用Zookeeper作為自己服務目錄的的后端并且采用HAproxy作為已發現的服務的唯一入口和負載均衡器。Smartstack可以非常輕松地集成到用戶的Docker基礎設施里。圖14-3所示就是采用Smartstack實現服務發現的一個簡單架構。

圖14-3
應用開發人員無需編寫任何服務發現的代碼并且他們還可以得到免費的開箱即用的負載均衡和服務故障自動轉移的功能支持。接下來,讓我們進一步看看Smartstack的這兩個核心服務,從而更好地理解Smartstack究竟是怎樣完成服務發現工作的。
Nerve是一款簡單的用來監控機器和服務的健康狀況的工具。它將服務的健康信息保存在一些分布式存儲里。后端方面目前只完全支持Zookeeper,但是官方也正在努力做針對`etcd`的完全適配工作。Nerve負責服務發現里的**服務注冊**部分,它會根據服務的健康狀態添加和刪除Zookeeper集群里的`znode`。如果使用`etcd`作為后端存儲,Nerve將會在`etcd`集群里添加一個鍵值記錄然后設置它的`TTL`為30秒。之后,它會根據服務的健康狀況不斷地更新該記錄。
Nerve給服務部署方面指明了一條道路,即要求應用開發人員提供一些**合適**的機制來監控服務的健康狀態。這一點很重要,而這不僅是為了提高服務發現實現的可靠性。Nerve利用應用服務提供的健康檢測方案來驅動服務注冊流程。最后,Nerve還可以從服務發現解決方案的整體中分離出來,作為單獨的監控服務的看門狗來使用。
如果想了解Nerve更多的內容,不妨去GitHub上讀一讀它的[官方文檔](https://github.com/airbnb/nerve)。
Synapse是一款簡單的服務發現的實現方案,它定義了服務`watcher`的概念,讓用戶可以從指定的后端中監聽相應的事件。Synapse會根據收到的事件生成相應的HAproxy配置文件。Synapse中提供了一些可用的服務`watcher`:
- **stub**——沒有監聽的概念,用戶只能手動指定服務的列表;
- **zookeeper**——zookeeper會在集群里的特定`znode`節點上注冊監聽;
- **docker**——監聽Docker API的事件;
- **EC2**——根據AWS EC2的標簽來監聽服務器。
每當監聽的服務不可用時,Synapse會重寫HAproxy的配置,隨后重新加載HAproxy服務使之生效。所有客戶端的請求都是通過HAproxy代理的,它負責將請求路由到真正的特定應用服務。這對于應用開發人員和運維人員來說都是一個共贏的局面:
- 開發人員不必編寫任何的服務發現代碼;
- 運維人員也可以通過一些經過充分驗證的解決方案來實現服務的負載均衡以及故障遷移。
再強調一下,這些內容均可以在GitHub上的[官方文檔](https://github.com/airbnb/synapse)里找到。
Smartstack是一個具備技術無關性的絕佳方案,借助它,用戶可以在基礎設施里實現服務發現。它不需要用戶編寫任何額外的應用代碼并且可以輕松地被部署到裸機、虛擬機或Docker容器里。Smartstack本身相當簡單,但是整個套件至少需要維護4個不同的部分:Zookeeper、HAproxy、Synapse和Nerve。例如,如果沒有在基礎設施中事先運行Zookeeper,用戶可能覺得運行全套的Smartstack方案會很難。此外,雖然在用戶的服務之前運行HAproxy可以為用戶提供一個不錯的服務層抽象以及負載均衡和服務的故障遷移功能,然而用戶需要在每臺宿主機上至少管理一個HAproxy實例,這會引入一定的復雜度,并且往往需要一定的維護成本。
在本章的最后,將簡單介紹一款由[bitly](http://word.bitly.com)的工程師團隊開發的名為`nsqlookupd`的工具。[`nsqlookupd`](http://nsq.io/components/nsqlookupd.html)**并不是一個完整的服務發現解決方案**,它只是提供了一種發現[`nsqd`](http://nsq.io/components/nsqd.html)實例的創新方式,或是在應用運行時跑在基礎設施里的一個分布式消息隊列。
實際上,`nsqd`守護進程在向那些`nsqlookupd`實例聲明自己啟動的時候就已經完成了服務注冊,隨后`nsqd`會定期發送帶有它們狀態信息的心跳包給每個`nslookupd`實例。
`nsqlookupd`實例擔任的角色是直接為客戶端提供查詢的服務注冊中心。它們提供的只是一個網絡上周期性同步的`nsqd`實例的數據庫。客戶端通常需要檢索每個可用的實例信息,然后合并這些結果。
如果要找的是一個可以在大規模基礎設施這樣的拓撲架構里運行的分布式消息隊列解決方案,不妨去看一看`nsqd`和`nsqlookupd`項目的官方文檔,了解更多詳細內容。
在本章里,我們介紹了一系列的服務發現解決方案。服務發現沒有捷徑可尋,只能根據任務的類型和它必須滿足的需求來選擇一款合適的工具。和傳統的推薦一個特定解決方案的形式不同,我們給出一個包含多種解決方案的概述表(見表14-1),對本章的內容做出總結,希望這能夠幫助讀者做出正確的選擇,選出最適用于自己的基礎設施的服務發現工具。
表14-1
名稱
注冊機制
數據一致性
語言
SkyDNS
客戶端
強一致
Go
weave-dns
自動注冊
強一致
Go
ZooKeeper
客戶端
強一致
Java
etcd
伙伴程序 + 客戶端
強一致
Go
consul
客戶端 + 配置 + 自動注冊
強一致
Go
eureka
客戶端
終端一致
Java
nslookupd
客戶端
終端一致
Go
在第15章里,我們將介紹Docker的日志采集和監控。
- - - - - -
[\[1\]](part0020.xhtml#ac141) 即像systemd這樣的服務管理程序,你可以將自己的應用和伙伴進程關聯起來,設定伙伴進程在應用啟動之后方才啟動。——譯者注
[\[2\]](part0020.xhtml#ac142) IANA機構設定了一個Linux下常見服務端口注冊列表(https://zh.wikipedia.org/wiki/TCP/UDP%E7%AB%AF%E5%8F%A3%E5%88%97%E8%A1%A8),它指定了這些著名服務的默認注冊端口號。——譯者注
- 版權信息
- 版權聲明
- 內容提要
- 對本書的贊譽
- 譯者介紹
- 前言
- 本書面向的讀者
- 誰真的在生產環境中使用Docker
- 為什么使用Docker
- 開發環境與生產環境
- 我們所說的“生產環境”
- 功能內置與組合工具
- 哪些東西不要Docker化
- 技術審稿人
- 第1章 入門
- 1.1 術語
- 1.1.1 鏡像與容器
- 1.1.2 容器與虛擬機
- 1.1.3 持續集成/持續交付
- 1.1.4 宿主機管理
- 1.1.5 編排
- 1.1.6 調度
- 1.1.7 發現
- 1.1.8 配置管理
- 1.2 從開發環境到生產環境
- 1.3 使用Docker的多種方式
- 1.4 可預期的情況
- 為什么Docker在生產環境如此困難
- 第2章 技術棧
- 2.1 構建系統
- 2.2 鏡像倉庫
- 2.3 宿主機管理
- 2.4 配置管理
- 2.5 部署
- 2.6 編排
- 第3章 示例:極簡環境
- 3.1 保持各部分的簡單
- 3.2 保持流程的簡單
- 3.3 系統細節
- 利用systemd
- 3.4 集群范圍的配置、通用配置及本地配置
- 3.5 部署服務
- 3.6 支撐服務
- 3.7 討論
- 3.8 未來
- 3.9 小結
- 第4章 示例:Web環境
- 4.1 編排
- 4.1.1 讓服務器上的Docker進入準備運行容器的狀態
- 4.1.2 讓容器運行
- 4.2 連網
- 4.3 數據存儲
- 4.4 日志
- 4.5 監控
- 4.6 無須擔心新依賴
- 4.7 零停機時間
- 4.8 服務回滾
- 4.9 小結
- 第5章 示例:Beanstalk環境
- 5.1 構建容器的過程
- 部署/更新容器的過程
- 5.2 日志
- 5.3 監控
- 5.4 安全
- 5.5 小結
- 第6章 安全
- 6.1 威脅模型
- 6.2 容器與安全性
- 6.3 內核更新
- 6.4 容器更新
- 6.5 suid及guid二進制文件
- 6.6 容器內的root
- 6.7 權能
- 6.8 seccomp
- 6.9 內核安全框架
- 6.10 資源限制及cgroup
- 6.11 ulimit
- 6.12 用戶命名空間
- 6.13 鏡像驗證
- 6.14 安全地運行Docker守護進程
- 6.15 監控
- 6.16 設備
- 6.17 掛載點
- 6.18 ssh
- 6.19 私鑰分發
- 6.20 位置
- 第7章 構建鏡像
- 7.1 此鏡像非彼鏡像
- 7.1.1 寫時復制與高效的鏡像存儲與分發
- 7.1.2 Docker對寫時復制的使用
- 7.2 鏡像構建基本原理
- 7.2.1 分層的文件系統和空間控管
- 7.2.2 保持鏡像小巧
- 7.2.3 讓鏡像可重用
- 7.2.4 在進程無法被配置時,通過環境變量讓鏡像可配置
- 7.2.5 讓鏡像在Docker變化時對自身進行重新配置
- 7.2.6 信任與鏡像
- 7.2.7 讓鏡像不可變
- 7.3 小結
- 第8章 存儲Docker鏡像
- 8.1 啟動并運行存儲的Docker鏡像
- 8.2 自動化構建
- 8.3 私有倉庫
- 8.4 私有registry的擴展
- 8.4.1 S3
- 8.4.2 本地存儲
- 8.4.3 對registry進行負載均衡
- 8.5 維護
- 8.6 對私有倉庫進行加固
- 8.6.1 SSL
- 8.6.2 認證
- 8.7 保存/載入
- 8.8 最大限度地減小鏡像體積
- 8.9 其他鏡像倉庫方案
- 第9章 CI/CD
- 9.1 讓所有人都進行鏡像構建與推送
- 9.2 在一個構建系統中構建所有鏡像
- 9.3 不要使用或禁止使用非標準做法
- 9.4 使用標準基礎鏡像
- 9.5 使用Docker進行集成測試
- 9.6 小結
- 第10章 配置管理
- 10.1 配置管理與容器
- 10.2 面向容器的配置管理
- 10.2.1 Chef
- 10.2.2 Ansible
- 10.2.3 Salt Stack
- 10.2.4 Puppet
- 10.3 小結
- 第11章 Docker存儲引擎
- 11.1 AUFS
- 11.2 DeviceMapper
- 11.3 BTRFS
- 11.4 OverlayFS
- 11.5 VFS
- 11.6 小結
- 第12章 Docker 網絡實現
- 12.1 網絡基礎知識
- 12.2 IP地址的分配
- 端口的分配
- 12.3 域名解析
- 12.4 服務發現
- 12.5 Docker高級網絡
- 12.6 IPv6
- 12.7 小結
- 第13章 調度
- 13.1 什么是調度
- 13.2 調度策略
- 13.3 Mesos
- 13.4 Kubernetes
- 13.5 OpenShift
- Red Hat公司首席工程師Clayton Coleman的想法
- 第14章 服務發現
- 14.1 DNS服務發現
- DNS服務器的重新發明
- 14.2 Zookeeper
- 14.3 基于Zookeeper的服務發現
- 14.4 etcd
- 基于etcd的服務發現
- 14.5 consul
- 14.5.1 基于consul的服務發現
- 14.5.2 registrator
- 14.6 Eureka
- 基于Eureka的服務發現
- 14.7 Smartstack
- 14.7.1 基于Smartstack的服務發現
- 14.7.2 Nerve
- 14.7.3 Synapse
- 14.8 nsqlookupd
- 14.9 小結
- 第15章 日志和監控
- 15.1 日志
- 15.1.1 Docker原生的日志支持
- 15.1.2 連接到Docker容器
- 15.1.3 將日志導出到宿主機
- 15.1.4 發送日志到集中式的日志平臺
- 15.1.5 在其他容器一側收集日志
- 15.2 監控
- 15.2.1 基于宿主機的監控
- 15.2.2 基于Docker守護進程的監控
- 15.2.3 基于容器的監控
- 15.3 小結
- DockOne社區簡介
- 看完了