《極簡微服務》的原始動機是爲了總結本身所學,但後來發現不少同窗在學習微服務後仍然一頭霧水,處於一種知道可是還沒有深入的階段。我但願可以經過《極簡微服務》這篇文章,用盡可能少的語言將各部分的知識串聯起來,幫助剛入門的同窗們從新梳理一遍,但願可以讓你們對微服務造成一個更清晰的認識。由於是「極簡」,因此本文不會涉及到任何代碼的具體實現。java
我想要爲你們帶來的思路是:linux
首先要說明的是,我入門微服務走的是Spring Cloud相關技術棧,因此我可能很難作到徹底的脫離這個技術棧去總結。我對於微服務的瞭解可能也是膚淺的,某些方面上的理解可能也會存在一些誤差,我更多的把這看做是一種記錄和交流,由於我也不過是經過閱讀書籍和一些博文後作了一些梳理,因此但願讀者朋友們可以有批判地閱讀個人文字。數據庫
當你們閱讀個人文字,而後再提起微服務的時候,若是能說出更多本身想法,那這恐怕就是對我莫大的鼓勵了。windows
由於叫作 「極簡」,因此其實我已經儘可能想要控制好篇幅不要太長了,可是很無奈到最後仍是超過了本身所認爲的最理想篇幅,由於我以爲有些東西是不得不去了解的,望讀者朋友可以見諒。安全
其實就是微服務的目標啦。服務器
終極目標網絡
經過將大的項目拆分紅不一樣子服務,不一樣子服務相互協做,創造出一個分佈式的、穩定的、高可用的、可控制的系統。
管理對象架構
服務。爲了實現終極目標管理好各個服務,讓他們之間的溝通無障礙。
所以,接下來我將針對咱們的服務與服務之間的關係來描述微服務。負載均衡
微服務是一種架構風格 ,就是將業務系統拆分爲不一樣的服務,而後每一個服務是獨立的。不一樣的服務經過輕量級的協議通訊(如HTTP)。部署的時候,一個服務可能會有多個實例。服務與服務的合做,構成了一個完整的系統。框架
經過微服務咱們能夠達到一些理想的效果。
...
有一種說法是,開發人員是被迫寫配置的。由於開發者得讓本身的軟件去適應不一樣的變化。
一開始是寫配置文件,可是太零散,因而將配置都寫在一個大的集合裏面,好比數據庫或者一個文件。
可是存在幾個問題:
因此,後面提出了 配置中心 ,但願可以解決上述的問題。
對於程序而言,它但願:
對於配置中心而言,它但願:
既然配置中心存了全部服務的配置,當服務們要加載配置就至少要和配置中心通訊一次。若是說服務要硬編碼配置中心的地址,假如配置中心換個地址,那麼就必須手動修改全部服務的這個配置。當服務數量達到幾十個的時候,這簡直是個噩耗。
因此硬編碼地址顯然不是一個好的方案。
微服務架構裏面,配置中心也是一個服務。發散的思考一下:不一樣服務之間的通訊是怎樣的?總不可能都是硬編碼吧?
回顧上一個章節的問題,服務之間應該如何通訊。簡單來講就是:服務太多了,不知道怎麼才能找到對方。
其實很簡單,咱們找一個第三者,讓第三者告訴咱們就行了。就像是出去旅行同樣,不認識路的話,你查百度地圖就行了,讓地圖告訴你具體的位置。
因此咱們要抽象出一箇中間服務,專門用來作這個事情,告訴你其餘服務的位置,這個過程就叫作服務發現。而其餘服務告訴這個中間服務本身的位置的過程,就叫作服務註冊(相似於把本身的位置告訴百度地圖,讓他記錄你的位置)。
服務註冊/發現中心:
其餘服務(客戶端):
不一樣的服務註冊、發現中心實現,流程可能存在差別。我在這裏只拿Eureka來看一下(目前我也只瞭解這個 = = )。
說明:
服務地址
,這個服務地址就像是一本地圖小手冊,上面記錄了全部服務的地址。這個客戶端只須要定時更新這個小手冊,保證它是最新的就行了。服務註冊於發現機制,解決了不一樣服務之間的位置問題。可是僅僅是這樣是否足夠呢?
來聽一聽客戶端的疑問:
我就想去吃鴨脖,你這個地圖小手冊,鴨脖店的地址怎麼有兩個啊?那我到底要去哪個店家呢?
選擇鴨脖子店的問題,嚴肅一點來講,就是當擁有多個服務實例的時候,選擇哪一個實例去調用的問題。
這個過程實際上就叫作負載均衡。
負載均衡能夠分兩種類型:服務端的負載均衡和客戶端的負載均衡。他們之間存在什麼樣的差別呢?
服務端負載均衡
顧名思義,就是把負載均衡(選擇哪一個鴨脖子店的選擇權)在服務端完成。在這種方案下,其實對於客戶端來講,就不存在選擇的問題了。當客戶端說 「要吃鴨脖」 的時候,服務端查看本身的地址列表,發現有兩個鴨脖店,那麼服務端會挑一個店的地址告訴客戶端。
怎麼挑選呢?一般能夠根據目標服務實例的網絡情況,負載的請求數量,甚至是隨機分配等,通常都是事先制定好一套規則。
客戶端負載均衡
相對於服務端的負載均衡,客戶端負載均衡就是把所謂的選擇權交給客戶端。也就是說,有多少個可用的服務實例的地址,服務端就返回多少個地址。客戶端要調用哪個服務實例,徹底由本身去選擇。
一樣,客戶端怎麼去選擇,通常也都是預先設定好規則的。
如今,服務與服務之間的通訊,服務註冊與發現解決了 「在哪裏」 的問題;負載均衡解決了 「若是有多個位置應該去哪裏」 的問題。服務註冊/發現 和 負載均衡是屬於相互合做的關係,共同解決了 「去哪裏」 的問題。
可是咱們要如何保證客戶端真的順利到達目的地呢?
嚴肅點說就是:要如何保證客戶端對目標服務的調用是正常的?
接過上一小節的問題,你以爲咱們應該如何保障咱們服務之間的調用是正常的呢?
爲了回答這個問題,咱們先拋出一個概念,叫作 「彈性」。
何謂彈性呢?彈性對於不一樣的對象來講略有不一樣。
對於客戶端來講:
彈性就是是否有響應。任何一個請求都應該有響應,且響應時間不該太長。
對於被調用的服務端來講:
彈性就是自我保護。爲了保護正常的運做,我可能會拒絕一些請求,以保障服務沒有由於過多的請求而崩潰。
咱們在這裏打個比方。服務之間的調用是正常 就像是 去店裏買蛋糕,這就是一個 「是否能順利抵達目標地點並買到蛋糕」 的問題。
在這裏咱們首先要明確,確定不能保證每一個人都可以順利抵達商店,由於目標商店隨時均可能會 「關門不幹」;其次,就算抵達了店裏,也不必定可以買到蛋糕,由於可能賣完了,或者機器出故障了。
因此咱們要換個思路,若是店家關門了,我就提早告訴它關門了,你最好別去了;或者你已經到了店家但發現蛋糕都賣完了,因而你改爲買餅乾,或者是你乾脆直接去別的店裏買。
維持彈性的方案有很多,其實以前也有碰到過,讓咱們來總結一下。
以前咱們學習到的有:
再來看看其餘的方案吧:
因爲服務發現與註冊機制、負載均衡在上面的章節已經有介紹了,故在此就再也不贅述。下面會圍繞其餘的方案來進行分析。
斷路器的流程
微服務中最多見的就是斷路器了,下面來看看斷路器的流程(不一樣的斷路器實現可能會有所差別):
如上圖所示,有四個對象,分別是用戶、客戶端、斷路器、目標服務。
由用戶向客戶端發起請求,客戶端調用目標服務來完成用戶的指望。斷路器在這其中承擔了客戶端與目標服務之間的 「中間人」 的角色。
通訊能夠劃分爲三個階段:
後備模式的流程
其實很簡單,用僞碼來表示就是:
boolean success = callService(); if (success) { doSomething(); } else { excutePlanB(); }
其實後備模式是能夠和斷路器結合起來使用的,當在斷路器流程中,判斷目標服務出現問題後的第二階段,客戶端的調用能夠所有走Plan B。
艙壁模式的流程
其實就是隔離,隔離的方式有不少種,好比線程的隔離,模塊的隔離,地域隔離等。這裏咱們拿線程隔離的舉個例子:
上圖中,本來客戶端對全部服務的調用都使用同一個線程池。
當某個目標服務A出現故障的時候,堆積在這個服務的調用越積越多,一直到線程池中的線程全被佔用了,此時客戶端新來一個須要對服務B的調用,卻沒有線程池能夠用了,只能排隊等待。
解決方案很簡單,事先將不一樣的服務劃分到不一樣的線程池中去,這樣就不會出現一個服務佔用資源,致使其餘服務不可用的狀況了。
如今咱們不只解決了 「去哪裏」 的問題,還解決了 「是否能抵達並買到東西」 的問題。
讓咱們好好想一想,咱們如今解決的都是服務與服務之間的通訊問題。若是是外部服務(咱們微服務體系外的客戶端)要與咱們的服務通訊,如何解決呢?
接回上一小節的問題。
問題描述:
內部服務與服務之間,是經過一本字典來查找對方的位置,可是若是是一個「外國人」(外部服務),一方面我不能把字典給他由於字典是機密,其次,就算給了他他也看不懂畢竟文字不通。
分析:
所謂內部服務,通常對外都是不可見的。咱們經過服務之間的合做給外部提供一個總體,因此對外而言咱們應該是一個總體。基於安全性考慮咱們也不能講內部服務暴露給外部服務使用。
解決方案
選舉出一個中間人,專門用來和外部服務通訊。由這個中間人來判斷對方是否是合法的請求,而後作相應轉發工做。
這個中間人又像是一個守門人,只接納有「令牌」的人,並「通風報信」。
其實沒有太多想要說的。咱們看一下網關所處的位置,恰好就是處於對外來請求的「守門人」的位置。咱們只須要明白網關的做用便可。
很明顯,網關的做用有:
想必到這裏,你已經基本瞭解服務註冊與發現、配置中心、負載均衡、斷路器、網關之間是如何共同工做的了。
如今惟一的問題是,相對於以前的單體架構系統來講,服務的數量變多了,服務之間的通訊也變多了。系統反而一會兒變得複雜了。若是服務與服務之間的調用出了問題,我應該怎麼排查問題呢?
接回上一小節問題。
問題描述
相對於單體架構系統而言,微服務架構犧牲了原有的簡單,換取了系統的靈活性。因此微服務看起來反而變得更復雜了。一個請求,可能要通過網關、服務與服務之間來回的調用,因此作的日誌也都十分零散,怎麼監控系統、排查問題,變成了使人頭疼的問題。
來自風箏的啓發
不管風箏怎麼飛,只要我手裏握着一條線,我就能知道風箏在哪裏。咱們能夠借鑑這個思路,爲咱們的請求創造出一根看不見的 「線」 。
TraceID
如上圖所示,當請求進來的時候,我只須要爲這個請求添加上一個標識(traceID),無論後續這個請求被如何轉發,只要帶有這個traceID,那他們確定是同一請求。
這個traceID不只僅要可以被轉發,作日誌的時候,也要將traceID一併記錄下來,這樣無論日誌分散在哪一個服務,我均可以知道他們是否是同一個請求產生的。
經過traceID咱們能夠實現請求的鏈路跟蹤問題。
日誌蒐集
咱們知道,不一樣的服務多是部署在不一樣的服務器上的,因此纔會顯得零散。若是咱們要查日誌,確定不可以來回登陸到不一樣服務器上去查。因此,咱們須要將全部的服務的日誌統一搜集起來,統一管理。這叫作日誌聚合。
經過將全部服務的日誌統一搜集起來,統一存儲,統一分析。再經過TraceID作鏈路的跟蹤,咱們就能夠搭建出一個日誌管理和分析平臺。
如此,在微服務架構下,監控和查找問題,變得一目瞭然。
如今來回顧一下,微服務的組件,都還記得有哪些嗎?
來看一下他們是如何相互協做的吧!
(這只是從一種維度上去劃分,劃分的維度能夠有不少)
是函數級別的。一般咱們在這裏捕獲到大部分的錯誤。
服務級別的集成測試。測試某個服務的功能。
系統測試,多個服務的測試。一般須要在頁面上用鼠標點點點。
對於單元測試而言,有時候一個函數依賴於另一個函數,也就是說,另一個的正確與否,會直接影響到我這個測試的結果。因此有時候咱們須要製造必定的隔離空間。
對於服務測試來講,也存在這樣的問題。
如此來講,測試的隔離性咱們須要重視。
爲了創造這種隔離性,咱們能夠用Mock、或者打樁技術。
所謂打樁,最簡單的理解就是你能夠認爲是「寫死」,a函數,須要依賴b函數的返回值。那麼咱們能夠將b函數的返回值「寫死」。這就保證了b的返回確定不會出錯了。
看似簡單,可是你不能直接修改函數b,由於很容易出錯,如果你忘了改回來,就把你「寫死」的代碼一併提交了。
Mock和打樁很相似,區別在於,打樁是不關心你函數b(即你這個測試要依賴的那個函數)執行了多少次的。而Mock不只僅能夠模擬調用函數b不少次,還會不少其餘的問題,好比,調用是否成功等。
咱們一般藉助一些測試框架來打樁和Mock,好比Java最經常使用的JUnit。
如上圖所示,不一樣的測試,會帶來不一樣的效果。
隨着服務愈來愈大,測試愈來愈多,人工的測試變得愈來愈困難。微服務離不開自動化測試。因此咱們須要藉助CI/CD來讓咱們的測試變得自動化。
持續集成對於微服務來講真的很重要。微服務組件解決的是技術上的問題,而持續集成所解決的是流程和效率上的問題。
持續集成可以加快微服務架構下的開發、測試、部署,可以幫助咱們更快的發現和解決問題。
初聞持續集成,讓人摸不着頭腦,怎麼會有如此晦澀的詞語呢!
可是且慢,先來看看哪些典型的場景:
這些場景都使人頭疼,他們的共同特徵是:要麼都是重複的工做,要麼是常常出現的問題,對項目工期、團隊協做產生了較大的影響。
咱們須要解決這些問題,用的手段就是持續集成。接下來咱們就來解釋什麼叫持續集成。
所謂持續集成(Continuous Integration)。
先來看一下什麼叫作集成。什麼是集成呢?集成就是將代碼提交到主幹,經過一系列自動化的工具來構建(你能夠簡單理解爲打包)和測試,驗證是否有錯誤,是否會對其餘人的代碼產生影響。
因此持續集成,就是不斷的進行集成,即常常提交代碼以集成。持續集成的目的就是儘早集成,早點發現錯誤,早點解決問題。
咱們談到CI的時候,每每涉及到幾個方面:
這其實也就是技術和流程問題。
一個好的持續集成環境是怎樣的?
代碼提交後自動(或者按期手動)觸發持續集成:自動跑單元測試,自動構建代碼,自動上傳部署。不須要過多的人工參與。中間每一步出現問題的時候,都會立刻經過一些手段進行反饋,防止出現更多的錯誤。
咱們談論CI的時候,不少時候也包含了持續部署(Continuous Delivery)的含義在裏面。
何謂持續部署?
是CI的下一步。CI經過自動化的流程,確保軟件語法和單元測試上是沒有問題的。可是還不夠,因此下一步須要將軟件的新版本交給質量團隊或者用戶。讓他們去繼續測試、評審。若評審經過則說明代碼就能夠進入生產階段了。
容器也是微服務架構不得不去思考的一個問題。它解決的是部署的問題。
部署有哪些問題呢?
能不能減小服務器的數量,在一臺服務器部署多個服務,而且能作到能夠支持多種服務的部署?
要支持不一樣服務,確定要支持不一樣的環境,好比windows,好比linux。
其實能夠的,用虛擬機嘛。
虛擬機的大概思路是:
將整個環境先搭建好,而後打包成爲虛擬機鏡像,每次須要增長一個新環境的時候,直接實例化作好的鏡像便可
一方面,虛擬機技術解決了環境的問題,部署比之前快了很多;
另外一方面,構建鏡像耗費時間長(一旦環境須要改變的時候,或者須要多個不一樣環境的時候),鏡像文件大;而且,虛擬機的管理也須要佔用額外的資源和空間;
虛擬機技術的特色是:
標準的虛擬機中存在一個Hypervisor,它會幫助咱們管理運行在Hypervisor之上的其餘虛擬機,包括資源的分配,外部的請求路由管理,內存的映射等;虛擬機越多,Hypervisor佔用的資源越多;
即Linux container,簡稱LXC。
LXC的原理是,建立一個隔離的進程空間,在這個空間中運行其餘的進程。而且由物理機的內核來完成資源的分配工做。
以下圖所示:
與傳統虛擬機不同的地方是:
如今你們都知道Docker了
LXC是操做系統級別的虛擬化方案,畢竟是Linux下的容器。是否存在應用級別的容器技術呢?
那就是Docker了。
2013年,Docker橫空出世的時候,在底層上也藉助了LXC來管理容器,而後本身在上層作其餘的管理工做。可是過了幾年,Docker 0.9的時候有了新歡libcontainer,LXC就變成了一個「備胎」,此時Docker能夠選擇再也不依賴Linux部件了。再Docker 1.8的時候,LXC被認定爲「前妻」。
再到後來,libcontainer上位。Docker致力於去實現容器化的標準,你只須要實現libcontainer提供的標準接口,那麼你就可以運行Docker,這爲Docker的全面跨平臺提供了可能。
分層
一個鏡像能夠分爲任意多層,好比:
若是幾個鏡像擁有相同的層。好比,鏡像A和鏡像B用的是相同的操做系統,那麼下載A時已經下載好了這個操做系統,下載B的時候,就不須要從新下載了,只須要下載第一層之上的數據便可。
增量更新/版本管理
對於一個公共鏡像A來講,有許多鏡像可能都是根據它來製做的,而鏡像A製做好了便不能夠改變。但對於其餘鏡像來講,有時候一些環境變量不免須要改變,這時應該怎麼辦呢?
Docker中約定:
這樣,只須要在最上層添加一個讀寫層,將須要改變的配置都集中到這個層來進行改寫。在寫的時候同時拷貝出一個只讀屬性的文件備份,應用實際讀取的時候讀取的是這個只讀屬性的拷貝。
如上圖所示,底層中環境變量A是1,可是在讀寫層,咱們將A設置爲3,那麼應用程序讀取的時候,讀取到的是A=3。而讀寫層不只僅能夠修改配置,還能夠將移除某些組件。每新加一層發佈出去的時候,就至關於發佈了一個更新。這樣的一個好處就是,對於以前發佈出去的鏡像,咱們擁有了 增量更新 和 版本管理 的能力。
Docker採用了C/S架構。分爲Client和Server,而後還借鑑了Git的思想,能夠搭建本身的中央鏡像倉庫。大致的架構以下圖所示(圖片來自於網絡):
須要解釋一下幾個名詞:
咱們所說的Docker,一般是指Docker Engine,它包括了:
其中,REST API和CLI都是用來跟server交互的。
咱們原先所謂的部署,對於Java來講,都是將代碼打包成jar或者war,也就是說,jar和war就是咱們CI的構建物。
採用了容器技術之後,CI每次集成的結果是一個容器鏡像,那麼咱們就只須要在服務器將這個鏡像實例化就能夠了,從而不須要關心環境的差別問題。
微服務經過將系統拆分爲不一樣的服務,經過服務與服務之間的相互協做,構成一個總體。
在微服務架構下,咱們關心服務與服務之間如何通訊。因此咱們會單獨構建一個服務中心,專用用於幫助服務與服務之間更好的溝通;再經過斷路器的方式,爲咱們的服務提供保障,讓服務間的調用具有彈性;此外,咱們還會獨立出一個服務,專門管理服務的配置;考慮到咱們須要支持外部的訪問,因此咱們獨立出一個網關,用來承擔「看門人」的角色。上述的種種爲系統帶來了巨大的複雜性,爲此,咱們還單首創造創一套日誌聚合和鏈路跟蹤機制,用於監控微服務的狀態。
除了這些,咱們還須要經過持續集成和容器技術來幫助咱們達成更高效的開發、測試、部署效率,讓服務具有更好的伸縮性,要增長新的服務實例的時候,只須要再實例化一次鏡像便可。
彷彿一切都變得美好了,這就是微服務。
(這是個人原創文章,若要轉載,但願你們可以註明出處,謝謝你們~)