很多應(yīng)用拆分成微服務(wù),,是為了承載高并發(fā),,往往一個進程扛不住這么大的量,因而需要拆分成多組進程,,每組進程承載特定的工作,,根據(jù)并發(fā)的壓力用多個副本公共承擔流量。(如果覺得疲累,,可以點擊下面的伴奏音樂,,而且猜對歌名,直接送書?。?/span> 將一個進程變成多組進程,,每組進程多個副本,需要程序的修改支撐這種分布式的架構(gòu),,如果架構(gòu)不支持,,僅僅在資源層創(chuàng)建多個副本是解決不了問題的。 很多人說,,支撐雙十一是靠堆機器,,誰不會?真正經(jīng)歷過的會覺得,,能夠靠堆機器堆出來的,,都不是問題,怕的是機器堆上去了,,因為架構(gòu)的問題,,并發(fā)量仍然上不去。 阻礙單體架構(gòu)變?yōu)榉植际郊軜?gòu)的關(guān)鍵點就在于狀態(tài)的處理,。如果狀態(tài)全部保存在本地,,無論是本地的內(nèi)存,還是本地的硬盤,,都會給架構(gòu)的橫向擴展帶來瓶頸,。 狀態(tài)分為分發(fā)、處理,、存儲幾個過程,,如果對于一個用戶的所有的信息都保存在一個進程中,,則從分發(fā)階段,就必須將這個用戶分發(fā)到這個進程,,否則無法對這個用戶進行處理,,然而當一個進程壓力很大的時候,根本無法擴容,,新啟動的進程根本無法處理那些保存在原來進程的用戶的數(shù)據(jù),,不能分擔壓力。 所以要講整個架構(gòu)分成兩個部分,,無狀態(tài)部分和有狀態(tài)部分,,而業(yè)務(wù)邏輯的部分往往作為無狀態(tài)的部分,而將狀態(tài)保存在有狀態(tài)的中間件中,,如緩存,、數(shù)據(jù)庫、對象存儲,、大數(shù)據(jù)平臺、消息隊列等,。 這樣無狀態(tài)的部分可以很容易的橫向擴展,,在用戶分發(fā)的時候,可以很容易分發(fā)到新的進程進行處理,,而狀態(tài)保存到后端,。而后端的中間件是有狀態(tài)的,這些中間件設(shè)計之初,,就考慮了擴容的時候,,狀態(tài)的遷移,復(fù)制,,同步等機制,,不用業(yè)務(wù)層關(guān)心。 如圖所示,,將架構(gòu)分為兩層,,無狀態(tài)和有狀態(tài)。 容器和微服務(wù)是雙胞胎,,因為微服務(wù)會將單體應(yīng)用拆分成很多小的應(yīng)用,,因而運維和持續(xù)集成會工作量變大,而容器技術(shù)能很好的解決這個問題,。然而在微服務(wù)化之前,,建議先進行容器化,在容器化之前,,建議先無狀態(tài)化,,當整個流程容器化了,,以后的微服務(wù)拆分才會水到渠成。 前面說對于任何狀態(tài),,需要考慮它的分發(fā)、處理,、存儲,。 對于數(shù)據(jù)的存儲,主要包含幾類數(shù)據(jù):
如果這些數(shù)據(jù)都保存在本地,,和業(yè)務(wù)邏輯耦合在一起,,就需要在數(shù)據(jù)分發(fā)的時候,將同一個用戶分到同一個進程,,這樣就會影響架構(gòu)的橫向擴展,。 對于保存在內(nèi)存里的數(shù)據(jù),例如Session,,可以放在外部統(tǒng)一的緩存中,。 對于業(yè)務(wù)相關(guān)的數(shù)據(jù),則應(yīng)該保存在統(tǒng)一的數(shù)據(jù)庫中,,如果性能扛不住,,可以進行讀寫分離,如文章《微服務(wù)化的數(shù)據(jù)庫設(shè)計與讀寫分離》,。 如果性能還是抗住不,,則可以使用分布式數(shù)據(jù)庫。 對于文件,,照片之類的數(shù)據(jù),,應(yīng)該存放在統(tǒng)一的對象存儲里面,通過CDN進行預(yù)加載,,如文章《微服務(wù)的接入層設(shè)計與動靜資源隔離》,。 對于非結(jié)構(gòu)化數(shù)據(jù),可以存在在統(tǒng)一的搜索引擎里面,,例如ElasticSearch,。 如果所有的數(shù)據(jù)都放在外部的統(tǒng)一存儲上,則應(yīng)用就成了僅僅包含業(yè)務(wù)邏輯的無狀態(tài)應(yīng)用,,可以進行平滑的橫向擴展,。 而所有的外部統(tǒng)一存儲,,無論是緩存、數(shù)據(jù)庫,、對象存儲,、搜索引擎、都有自身的分布式橫向擴展機制,。 在實行了無狀態(tài)化之后,,就可以將有狀態(tài)的集群集中到一起,進行跨機房的部署,,實現(xiàn)跨機房的高可用性,。而無狀態(tài)的部分可以通過Dubbo自動發(fā)現(xiàn),當進程掛掉的時候,,自動重啟,,自動修復(fù),也可以進行多機房的部署,。 但是還有一個遺留的問題,就是已經(jīng)分發(fā),,正在處理,,但是尚未存儲的數(shù)據(jù),肯定會在內(nèi)存中有一些,,在進程重啟的時候,數(shù)據(jù)還是會丟一些的,,那這部分數(shù)據(jù)怎么辦呢,? 這部分就需要通過重試進行解決,當本次調(diào)用過程中失敗之后,,前序的進程會進行重試,,例如Dubbo就有重試機制。既然重試,,就需要接口是冪等的,,也即同一次交易,調(diào)用兩次轉(zhuǎn)賬1元,,不能最終轉(zhuǎn)走2元,。 接口分為查詢、插入,、更新,、刪除等操作。 對于查詢接口來講,,本身就是冪等的,,不用做特殊的判斷,。 對于插入接口來講,如果每一個數(shù)據(jù)都有唯一的主鍵,,也能保證插入的唯一性,,一旦不唯一,則會報錯,。 對于更新操作來講,,則比較復(fù)雜,分幾種情況,。 一種情況是同一個接口,,前后調(diào)用多次的冪等性。另一種情況是同一個接口,,并發(fā)環(huán)境下調(diào)用多次的正確性,。 為了保持冪等性,往往要有一個冪等表,,通過傳入冪等參數(shù)匹配冪等表中ID的方式,,保證每個操作只被執(zhí)行一次,而且在實行最終一致性的時候,,可以通過不斷重試,,保證最終接口調(diào)用的成功。 對于并發(fā)條件下,,誰先調(diào)用,,誰后調(diào)用,需要通過分布式鎖如Redis,,ZooKeeper等來實現(xiàn)同一個時刻只有一個請求被執(zhí)行,,如何保證多次執(zhí)行結(jié)果仍然一致呢?則往往需要通過狀態(tài)機,,每個狀態(tài)只流轉(zhuǎn)一次,。還有就是樂觀鎖,也即分布式的CAS操作,,將狀態(tài)的判斷,、更新整合在一條語句中,可以保證狀態(tài)流轉(zhuǎn)的原子性,。樂觀鎖并不保證更新一定成功,,需要有對應(yīng)的機制來應(yīng)對更新失敗。 無狀態(tài)化之后,,實行容器化就十分順暢了,容器的不可改變基礎(chǔ)設(shè)施,以及容器基于容器平臺的掛掉自動重啟,,自動修復(fù),,都因為無狀態(tài)順暢無比。 關(guān)鍵技術(shù)一:Dockerfile 例如下面的Dockerfile,。 為什么一定要用Dockerfile,,而不建議通過保存鏡像的方式來生成鏡像呢? 這樣才能實現(xiàn)環(huán)境配置和環(huán)境部署代碼化 ,,將Dockerfile維護在Git里面,,有版本控制,并且通過自動化的build的過程來生成鏡像,,而鏡像中就是環(huán)境的配置和環(huán)境的部署,,要修改環(huán)境應(yīng)先通過Git上面修改Dockerfile的方式進行,這就是IaC,。 關(guān)鍵技術(shù)二:容器鏡像 通過Dockerfile可以生成容器鏡像,,容器的鏡像是分層保存,對于Dockerfile中的每一個語句,,生成一層容器鏡像,,如此疊加,每一層都有UUID,。 容器鏡像可以打一個版本號,,放入統(tǒng)一的鏡像倉庫。 關(guān)鍵技術(shù)三:容器運行時 容器運行時,,是將容器鏡像之上加一層可寫入層,,為容器運行時所看到的文件系統(tǒng)。 容器運行時使用了兩種隔離的技術(shù),。 一種是看起來是隔離的技術(shù),,稱為namespace,也即每個namespace中的應(yīng)用看到的是不同的IP地址,、用戶空間、程號等,。 另一種是用起來是隔離的技術(shù),,稱為CGroup,也即明明整臺機器有很多的CPU,、內(nèi)存,,而一個應(yīng)用只能用其中的一部分。 CGroup 很多人會將容器當成虛擬機來用,,這是非常不正確的,而且容器所做的事情虛擬機都能做到。 如果部署的是一個傳統(tǒng)的應(yīng)用,,這個應(yīng)用啟動速度慢,,進程數(shù)量少,基本不更新,,那么虛擬機完全能夠滿足需求,。
如果是一個傳統(tǒng)應(yīng)用,,根本沒有必要花費精去容器化,,因為白花了力氣,享受不到好處,。 什么情況下,,才應(yīng)該考慮做一些改變呢? 傳統(tǒng)業(yè)務(wù)突然被互聯(lián)網(wǎng)業(yè)務(wù)沖擊了,,應(yīng)用老是變,,三天兩頭要更新,而且流量增大了,,原來支付系統(tǒng)是取錢刷卡的,,現(xiàn)在要互聯(lián)網(wǎng)支付了,流量擴大了N倍,。 沒辦法,,一個字:拆,! 拆開了,每個子模塊獨自變化,,少相互影響,。 拆開了,原來一個進程扛流量,,現(xiàn)在多個進程一起扛,。 所以稱為微服務(wù)。 微服務(wù)場景下,,進程多,,更新快,于是出現(xiàn)100個進程,,每天一個鏡像,。 容器樂了,每個容器鏡像小,,沒啥問題,,虛擬機哭了,因為虛擬機每個鏡像太大了,。 所以微服務(wù)場景下,,可以開始考慮用容器了。 虛擬機怒了,,老子不用容器了,,微服務(wù)拆分之后,用Ansible自動部署是一樣的,。 這樣說從技術(shù)角度來講沒有任何問題,。 然而問題是從組織角度出現(xiàn)的。 一般的公司,,開發(fā)會比運維多的多,,開發(fā)寫完代碼就不用管了,環(huán)境的部署完全是運維負責,,運維為了自動化,,寫Ansible腳本來解決問題。 然而這么多進程,,又拆又合并的,,更新這么快,配置總是變,,Ansible腳本也要常改,每天都上線,,不得累死運維,。 所以這如此大的工作量情況下,運維很容易出錯,哪怕通過自動化腳本,。 這個時候,,容器就可以作為一個非常好的工具運用起來。 除了容器從技術(shù)角度,,能夠使得大部分的內(nèi)部配置可以放在鏡像里面之外,,更重要的是從流程角度,將環(huán)境配置這件事情,,往前推了,,推到了開發(fā)這里,要求開發(fā)完畢之后,,就需要考慮環(huán)境部署的問題,,而不能當甩手掌柜。 這樣做的好處就是,,雖然進程多,,配置變化多,更新頻繁,,但是對于某個模塊的開發(fā)團隊來講,,這個量是很小的,因為5-10個人專門維護這個模塊的配置和更新,,不容易出錯,。 如果這些工作量全交給少數(shù)的運維團隊,不但信息傳遞會使得環(huán)境配置不一致,,部署量會大非常多,。 容器是一個非常好的工具,就是讓每個開發(fā)僅僅多做5%的工作,,就能夠節(jié)約運維200%的工作,,并且不容易出錯。 然而本來原來運維該做的事情開發(fā)做了,,開發(fā)的老大愿意么,?開發(fā)的老大會投訴運維的老大么? 這就不是技術(shù)問題了,,其實這就是DevOps,,DevOps不是不區(qū)分開發(fā)和運維,而是公司從組織到流程,,能夠打通,,看如何合作,邊界如何劃分,,對系統(tǒng)的穩(wěn)定性更有好處,。 所以微服務(wù),,DevOps,容器是相輔相成,,不可分割的,。 不是微服務(wù),根本不需要容器,,虛擬機就能搞定,,不需要DevOps,一年部署一次,,開發(fā)和運維溝通再慢都能搞定,。 所以,容器的本質(zhì)是基于鏡像的跨環(huán)境遷移,。 鏡像是容器的根本性發(fā)明,,是封裝和運行的標準,其他什么Namespace,、CGroup,,早就有了。這是技術(shù)方面,。 在流程方面,,鏡像是DevOps的良好工具。 容器是為了跨環(huán)境遷移的,,第一種遷移的場景是開發(fā),,測試,生產(chǎn)環(huán)境之間的遷移,。如果不需要遷移,,或者遷移不頻繁,虛擬機鏡像也行,,但是總是要遷移,,帶著幾百G的虛擬機鏡像,太大了,。 第二種遷移的場景是跨云遷移,,跨公有云,跨Region,,跨兩個OpenStack的虛擬機遷移都是非常麻煩,,甚至不可能的,因為公有云不提供虛擬機鏡像的下載和上傳功能,,而且虛擬機鏡像太大了,,一傳傳一天。 所以如圖為將容器融入持續(xù)集成的過程中,,形成DevOps的流程,。 通過這一章,,再加上第一章《微服務(wù)化的基石——持續(xù)集成》就構(gòu)成了微服務(wù),DevOps,,容器化三位一體的統(tǒng)一。 對于容器鏡像,,我們應(yīng)該充分利用容器鏡像分層的優(yōu)勢,,將容器鏡像分層構(gòu)建,在最里面的OS和系統(tǒng)工具層,,由運維來構(gòu)建,,中間層的JDK和運行環(huán)境,由核心開發(fā)人員構(gòu)建,,而最外層的Dockerfile就會非常簡單,,只要將jar或者war放到指定位置就可以了。 這樣可以降低Dockerfile和容器化的門檻,,促進DevOps的進度,。 容器化好了,,應(yīng)該交給容器平臺進行管理,,從而實現(xiàn)對于容器的自動化管理和編排。 例如一個應(yīng)用包含四個服務(wù)A,、B,、C、D,,她們相互引用,,相互依賴,如果使用了容器平臺,,則服務(wù)之間的服務(wù)發(fā)現(xiàn)就可以通過服務(wù)名進行了,。例如A服務(wù)調(diào)用B服務(wù),不需要知道B服務(wù)的IP地址,,只需要在配置文件里面寫入B服務(wù)服務(wù)名就可以了,。如果中間的節(jié)點宕機了,容器平臺會自動將上面的服務(wù)在另外的機器上啟動起來,。容器啟動之后,,容器的IP地址就變了,但是不用擔心,,容器平臺會自動將服務(wù)名B和新的IP地址映射好,,A服務(wù)并無感知。這個過程叫做自修復(fù)和自發(fā)現(xiàn),。如果服務(wù)B遭遇了性能瓶頸,,三個B服務(wù)才能支撐一個A服務(wù),,也不需要特殊配置,只需要將服務(wù)B的數(shù)量設(shè)置為3,,A還是只需要訪問服務(wù)B,,容器平臺會自動選擇其中一個進行訪問,這個過程稱為彈性擴展和負載均衡,。 當容器平臺規(guī)模不是很大的時候,,Docker Swarm Mode還是比較好用的:
總之Docker幫你料理好了一切,你不用太關(guān)心細節(jié),,很容易就能夠?qū)⒓哼\行起來,。 而且可以通過Docker命令,像在一臺機器上使用容器一樣使用集群上的容器,,可以隨時將容器當虛擬機來使用,,這樣對于中等規(guī)模集群,以及運維人員還是比較友好的,。 當然內(nèi)置的太多了也有缺點,,就是不好定制化,不好Debug,,不好干預(yù),。當你發(fā)現(xiàn)有一部分性能不行的時候,你需要改整個代碼,,全部重新編譯,,當社區(qū)更新了,合并分支是很頭疼的事情,。當出現(xiàn)了問題的時候,,由于Manager大包大攬干了很多活,不知道哪一步出錯了,,反正就是沒有返回,,停在那里,如果重啟整個Manager,,影響面又很大,。 當規(guī)模比較大,應(yīng)用比較復(fù)雜的時候,,則推薦Kubernetes,。 Kubernetes模塊劃分得更細,模塊比較多,而且模塊之間完全的松耦合,,可以非常方便地進行定制化,。 而且Kubernetes的數(shù)據(jù)結(jié)構(gòu)的設(shè)計層次比較細,非常符合微服務(wù)的設(shè)計思想,。例如從容器->Pods->Deployment->Service,,本來簡單運行一個容器,被封裝為這么多的層次,,每次層有自己的作用,,每一層都可以拆分和組合,這樣帶來一個很大的缺點,,就是學習門檻高,為了簡單運行一個容器,,需要先學習一大堆的概念和編排規(guī)則,。 但是當需要部署的業(yè)務(wù)越來越復(fù)雜時,場景越來越多時,,你會發(fā)現(xiàn)Kubernetes這種細粒度設(shè)計的優(yōu)雅,,使得你能夠根據(jù)自己的需要靈活的組合,而不會因為某個組件被封裝好了,,從而導致很難定制,。例如對于Service來講,除了提供內(nèi)部服務(wù)之間的發(fā)現(xiàn)和相互訪問外,,還靈活設(shè)計了headless service,,這使得很多游戲需要有狀態(tài)的保持長連接有了很好的方式,另外訪問外部服務(wù)時,,例如數(shù)據(jù)庫,、緩存、headless service相當于一個DNS,,使得配置外部服務(wù)簡單很多,。很多配置復(fù)雜的大型應(yīng)用,更復(fù)雜的不在于服務(wù)之間的相互配置,,可以有Spring Cloud或者Dubbo去解決,,復(fù)雜的反而是外部服務(wù)的配置,不同的環(huán)境依賴不同的外部應(yīng)用,,External Name這個提供和很好的機制,。 包括統(tǒng)一的監(jiān)控cAdvisor,統(tǒng)一的配置ConfigMap,,都是構(gòu)建一個微服務(wù)所必須的,。 然而Kubernetes當前也有一個瓶頸——集群規(guī)模還不是多么大,官方說法是幾千個節(jié)點,,所以超大規(guī)模的集群,,還是需要有很強的IT能力進行定制化,。但是對于中等規(guī)模的集群也足夠了。 而且Kubernetes社區(qū)的熱度,,可以使得使用開源Kubernetes的公司能夠很快地找到幫助,,等待到新功能的開發(fā)和Bug的解決。 |
|