書接上文:一文詳解微服務架構(一)
然而……
沒有銀彈
春天來了睛驳,萬物復蘇烙心,又到了一年一度的購物狂歡節(jié)膜廊。眼看著日訂單數(shù)量蹭蹭地上漲,小皮小明小紅喜笑顏開弃理±B郏可惜好景不長,樂極生悲痘昌,突然嘣的一下钥勋,系統(tǒng)掛了。
以往單體應用辆苔,排查問題通常是看一下日志算灸,研究錯誤信息和調用堆棧。而微服務架構整個應用分散成多個服務驻啤,定位故障點非常困難菲驴。小明一個臺機器一臺機器地查看日志,一個服務一個服務地手工調用骑冗。經過十幾分鐘的查找赊瞬,小明終于定位到故障點:促銷服務由于接收的請求量太大而停止響應了。其他服務都直接或間接地會調用促銷服務贼涩,于是也跟著宕機了巧涧。在微服務架構中,一個服務故障可能會產生雪崩效用遥倦,導致整個系統(tǒng)故障谤绳。其實在節(jié)前,小明和小紅是有做過請求量評估的袒哥。按照預計缩筛,服務器資源是足以支持節(jié)日的請求量的,所以肯定是哪里出了問題堡称。不過形勢緊急瞎抛,隨著每一分每一秒流逝的都是白花花的銀子,因此小明也沒時間排查問題却紧,當機立斷在云上新建了幾臺虛擬機婿失,然后一臺一臺地部署新的促銷服務節(jié)點。幾分鐘的操作后啄寡,系統(tǒng)總算是勉強恢復正常了。整個故障時間內估計損失了幾十萬的銷售額哩照,三人的心在滴血……
事后挺物,小明簡單寫了個日志分析工具(量太大了,文本編輯器幾乎打不開飘弧,打開了肉眼也看不過來)识藤,統(tǒng)計了促銷服務的訪問日志砚著,發(fā)現(xiàn)在故障期間,商品服務由于代碼問題痴昧,在某些場景下會對促銷服務發(fā)起大量請求稽穆。這個問題并不復雜,小明手指抖一抖赶撰,修復了這個價值幾十萬的 Bug舌镶。
問題是解決了,但誰也無法保證不會再發(fā)生類似的其他問題豪娜。微服務架構雖然邏輯設計上看是完美的餐胀,但就像積木搭建的華麗宮殿一樣,經不起風吹草動瘤载。微服務架構雖然解決了舊問題否灾,也引入了新的問題:
- 微服務架構整個應用分散成多個服務,定位故障點非常困難鸣奔。
- 穩(wěn)定性下降墨技。服務數(shù)量變多導致其中一個服務出現(xiàn)故障的概率增大,并且一個服務故障可能導致整個系統(tǒng)掛掉挎狸。事實上扣汪,在大訪問量的生產場景下,故障總是會出現(xiàn)的伟叛。
- 服務數(shù)量非常多私痹,部署、管理的工作量很大统刮。
- 開發(fā)方面:如何保證各個服務在持續(xù)開發(fā)的情況下仍然保持協(xié)同合作紊遵。
- 測試方面:服務拆分后,幾乎所有功能都會涉及多個服務侥蒙。原本單個程序的測試變?yōu)榉臻g調用的測試暗膜。測試變得更加復雜。
小明小紅痛定思痛鞭衩,決心好好解決這些問題学搜。對故障的處理一般從兩方面入手,一方面盡量減少故障發(fā)生的概率论衍,另一方面降低故障造成的影響瑞佩。
監(jiān)控 - 發(fā)現(xiàn)故障的征兆
在高并發(fā)分布式的場景下,故障經常是突然間就雪崩式爆發(fā)坯台。所以必須建立完善的監(jiān)控體系炬丸,盡可能發(fā)現(xiàn)故障的征兆。
微服務架構中組件繁多蜒蕾,各個組件所需要監(jiān)控的指標不同稠炬。比如 Redis 緩存一般監(jiān)控占用內存值焕阿、網絡流量,數(shù)據(jù)庫監(jiān)控連接數(shù)首启、磁盤空間暮屡,業(yè)務服務監(jiān)控并發(fā)數(shù)、響應延遲毅桃、錯誤率等褒纲。因此如果做一個大而全的監(jiān)控系統(tǒng)來監(jiān)控各個組件是不大現(xiàn)實的,而且擴展性會很差疾嗅。一般的做法是讓各個組件提供報告自己當前狀態(tài)的接口(metrics 接口)外厂,這個接口輸出的數(shù)據(jù)格式應該是一致的。然后部署一個指標采集器組件代承,定時從這些接口獲取并保持組件狀態(tài)汁蝶,同時提供查詢服務。最后還需要一個 UI论悴,從指標采集器查詢各項指標掖棉,繪制監(jiān)控界面或者根據(jù)閾值發(fā)出告警。
大部分組件都不需要自己動手開發(fā)膀估,網絡上有開源組件幔亥。小明下載了 RedisExporter 和 MySQLExporter,這兩個組件分別提供了 Redis 緩存和 MySQL 數(shù)據(jù)庫的指標接口察纯。微服務則根據(jù)各個服務的業(yè)務邏輯實現(xiàn)自定義的指標接口帕棉。然后小明采用 Prometheus 作為指標采集器,Grafana 配置監(jiān)控界面和郵件告警饼记。這樣一套微服務監(jiān)控系統(tǒng)就搭建起來了:
定位問題 - 鏈路跟蹤
在微服務架構下香伴,一個用戶的請求往往涉及多個內部服務調用。為了方便定位問題具则,需要能夠記錄每個用戶請求時即纲,微服務內部產生了多少服務調用,及其調用關系博肋。這個叫做鏈路跟蹤低斋。
我們用一個 Istio 文檔里的鏈路跟蹤例子來看看效果:
從圖中可以看到,這是一個用戶訪問 productpage 頁面的請求匪凡。在請求過程中膊畴,productpage 服務順序調用了 details 和 reviews 服務的接口。而 reviews 服務在響應過程中又調用了 ratings 的接口病游。整個鏈路跟蹤的記錄是一棵樹:
要實現(xiàn)鏈路跟蹤巴比,每次服務調用會在 HTTP 的 HEADERS 中記錄至少記錄四項數(shù)據(jù):
- traceId:traceId 標識一個用戶請求的調用鏈路。具有相同 traceId 的調用屬于同一條鏈路。
- spanId:標識一次服務調用的 ID轻绞,即鏈路跟蹤的節(jié)點 ID。
- parentId:父節(jié)點的 spanId佣耐。
- requestTime & responseTime:請求時間和響應時間政勃。
另外,還需要調用日志收集與存儲的組件兼砖,以及展示鏈路調用的 UI 組件奸远。
以上只是一個極簡的說明,關于鏈路跟蹤的理論依據(jù)可詳見 Google 的 Dapper
了解了理論基礎后讽挟,小明選用了 Dapper 的一個開源實現(xiàn) Zipkin懒叛。然后手指一抖,寫了個 HTTP 請求的攔截器耽梅,在每次 HTTP 請求時生成這些數(shù)據(jù)注入到 HEADERS眼姐,同時異步發(fā)送調用日志到 Zipkin 的日志收集器中。這里額外提一下罢杉,HTTP 請求的攔截器滩租,可以在微服務的代碼中實現(xiàn)律想,也可以使用一個網絡代理組件來實現(xiàn)(不過這樣子每個微服務都需要加一層代理)蜘欲。
鏈路跟蹤只能定位到哪個服務出現(xiàn)問題,不能提供具體的錯誤信息晌柬。查找具體的錯誤信息的能力則需要由日志分析組件來提供姥份。
分析問題 - 日志分析
日志分析組件應該在微服務興起之前就被廣泛使用了。即使單體應用架構年碘,當訪問數(shù)變大澈歉、或服務器規(guī)模增多時,日志文件的大小會膨脹到難以用文本編輯器進行訪問屿衅,更糟的是它們分散在多臺服務器上面埃难。排查一個問題,需要登錄到各臺服務器去獲取日志文件,一個一個地查找(而且打開涡尘、查找都很慢)想要的日志信息忍弛。
因此,在應用規(guī)模變大時考抄,我們需要一個日志的 “搜索引擎”细疚。以便于能準確的找到想要的日志。另外川梅,數(shù)據(jù)源一側還需要收集日志的組件和展示結果的 UI 組件:
小明調查了一下疯兼,使用了大名鼎鼎地 ELK 日志分析組件贫途。ELK 是 Elasticsearch姨裸、Logstash 和 Kibana 三個組件的縮寫。
- Elasticsearch:搜索引擎扑毡,同時也是日志的存儲苦掘。
- Logstash:日志采集器惯驼,它接收日志輸入,對日志進行一些預處理,然后輸出到 Elasticsearch乡恕。
- Kibana:UI 組件运杭,通過 Elasticsearch 的 API 查找數(shù)據(jù)并展示給用戶。
最后還有一個小問題是如何將日志發(fā)送到 Logstash叛本。一種方案是在日志輸出的時候直接調用 Logstash 接口將日志發(fā)送過去跷叉。這樣一來又(咦,為啥要用 “又”)要修改代碼…… 于是小明選用了另一種方案:日志仍然輸出到文件园欣,每個服務里再部署個 Agent 掃描日志文件然后輸出給 Logstash。
網關 - 權限控制赂弓,服務治理
拆分成微服務后翔怎,出現(xiàn)大量的服務按脚,大量的接口唯沮,使得整個調用關系亂糟糟的萌庆。經常在開發(fā)過程中,寫著寫著,忽然想不起某個數(shù)據(jù)應該調用哪個服務占遥。或者寫歪了,調用了不該調用的服務负芋,本來一個只讀的功能結果修改了數(shù)據(jù)……
為了應對這些情況,微服務的調用需要一個把關的東西,也就是網關绍绘。在調用者和被調用者中間加一層網關,每次調用時進行權限校驗左刽。另外迄靠,網關也可以作為一個提供服務接口文檔的平臺。
使用網關有一個問題就是要決定在多大粒度上使用:最粗粒度的方案是整個微服務一個網關吠式,微服務外部通過網關訪問微服務,微服務內部則直接調用;最細粒度則是所有調用,不管是微服務內部調用或者來自外部的調用宜岛,都必須通過網關。折中的方案是按照業(yè)務領域將微服務分成幾個區(qū),區(qū)內直接調用戴而,區(qū)間通過網關調用。
由于整個網上超市的服務數(shù)量還不算特別多扶踊,小明采用的最粗粒度的方案:
服務注冊于發(fā)現(xiàn) - 動態(tài)擴容
前面的組件,都是旨在降低故障發(fā)生的可能性分井。然而故障總是會發(fā)生的杂抽,所以另一個需要研究的是如何降低故障產生的影響铸磅。
最粗暴的(也是最常用的)故障處理策略就是冗余。一般來說,一個服務都會部署多個實例羞迷,這樣一來能夠分擔壓力提高性能,二來即使一個實例掛了其他實例還能響應热鞍。
冗余的一個問題是使用幾個冗余?這個問題在時間軸上并沒有一個切確的答案澄港。根據(jù)服務功能、時間段的不同,需要不同數(shù)量的實例髓涯。比如在平日里蚓再,可能 4 個實例已經夠用问畅;而在促銷活動時,流量大增秩铆,可能需要 40 個實例添祸。因此冗余數(shù)量并不是一個固定的值,而是根據(jù)需要實時調整的。
一般來說新增實例的操作為:
- 部署新實例
- 將新實例注冊到負載均衡或 DNS 上
操作只有兩步林艘,但如果注冊到負載均衡或 DNS 的操作為人工操作的話究孕,那事情就不簡單了。想想新增 40 個實例后绘趋,要手工輸入 40 個 IP 的感覺……
解決這個問題的方案是服務自動注冊與發(fā)現(xiàn)。首先,需要部署一個服務發(fā)現(xiàn)服務,它提供所有已注冊服務的地址信息的服務绣溜。DNS 也算是一種服務發(fā)現(xiàn)服務怖喻。然后各個應用服務在啟動時自動將自己注冊到服務發(fā)現(xiàn)服務上锚沸。并且應用服務啟動后會實時(定期)從服務發(fā)現(xiàn)服務同步各個應用服務的地址列表到本地坠韩。服務發(fā)現(xiàn)服務也會定期檢查應用服務的健康狀態(tài)只搁,去掉不健康的實例地址音比。這樣新增實例時只需要部署新實例,實例下線時直接關停服務即可氢惋,服務發(fā)現(xiàn)會自動檢查服務實例的增減洞翩。
服務發(fā)現(xiàn)還會跟客戶端負載均衡配合使用。由于應用服務已經同步服務地址列表在本地了焰望,所以訪問微服務時,可以自己決定負載策略娇未。甚至可以在服務注冊時加入一些元數(shù)據(jù)(服務版本等信息)忽妒,客戶端負載則根據(jù)這些元數(shù)據(jù)進行流量控制,實現(xiàn) A/B 測試、藍綠發(fā)布等功能旭等。
服務發(fā)現(xiàn)有很多組件可以選擇鲸睛,比如說 Zookeeper 肺魁、Eureka、Consul狗唉、Etcd 等。不過小明覺得自己水平不錯蹋半,想炫技降宅,于是基于 Redis 自己寫了一個……
熔斷球拦、服務降級福稳、限流
熔斷
當一個服務因為各種原因停止響應時,調用方通常會等待一段時間带饱,然后超時或者收到錯誤返回却盘。如果調用鏈路比較長,可能會導致請求堆積,整條鏈路占用大量資源一直在等待下游響應。所以當多次訪問一個服務失敗時椰弊,應熔斷,標記該服務已停止工作饵婆,直接返回錯誤轴总。直至該服務恢復正常后再重新建立連接。
服務降級
當下游服務停止工作后博个,如果該服務并非核心業(yè)務怀樟,則上游服務應該降級,以保證核心業(yè)務不中斷盆佣。比如網上超市下單界面有一個推薦商品湊單的功能往堡,當推薦模塊掛了后械荷,下單功能不能一起掛掉,只需要暫時關閉推薦功能即可虑灰。
限流
一個服務掛掉后吨瞎,上游服務或者用戶一般會習慣性地重試訪問。這導致一旦服務恢復正常穆咐,很可能因為瞬間網絡流量過大又立刻掛掉颤诀,在棺材里重復著仰臥起坐。因此服務需要能夠自我保護——限流对湃。限流策略有很多崖叫,最簡單的比如當單位時間內請求數(shù)過多時,丟棄多余的請求拍柒。另外心傀,也可以考慮分區(qū)限流。僅拒絕來自產生大量請求的服務的請求拆讯。例如商品服務和訂單服務都需要訪問促銷服務脂男,商品服務由于代碼問題發(fā)起了大量請求,促銷服務則只限制來自商品服務的請求种呐,來自訂單服務的請求則正常響應疆液。
測試
微服務架構下,測試分為三個層次:
- 端到端測試:覆蓋整個系統(tǒng)陕贮,一般在用戶界面機型測試棋弥。
- 服務測試:針對服務接口進行測試岗憋。
- 單元測試:針對代碼單元進行測試。
三種測試從上到下實施的容易程度遞增,但是測試效果遞減荠瘪。端到端測試最費時費力少办,但是通過測試后我們對系統(tǒng)最有信心券躁。單元測試最容易實施艾扮,效率也最高,但是測試后不能保證整個系統(tǒng)沒有問題筐高。
由于端到端測試實施難度較大搜囱,一般只對核心功能做端到端測試。一旦端到端測試失敗柑土,則需要將其分解到單元測試:則分析失敗原因蜀肘,然后編寫單元測試來重現(xiàn)這個問題,這樣未來我們便可以更快地捕獲同樣的錯誤稽屏。
服務測試的難度在于服務會經常依賴一些其他服務扮宠。這個問題可以通過 Mock Server 解決:
單元測試大家都很熟悉了。我們一般會編寫大量的單元測試(包括回歸測試)盡量覆蓋所有代碼狐榔。
微服務框架
指標接口坛增、鏈路跟蹤注入获雕、日志引流、服務注冊發(fā)現(xiàn)收捣、路由規(guī)則等組件以及熔斷届案、限流等功能都需要在應用服務上添加一些對接代碼。如果讓每個應用服務自己實現(xiàn)是非常耗時耗力的罢艾÷茜瑁基于 DRY 的原則,小明開發(fā)了一套微服務框架昆婿,將與各個組件對接的代碼和另外一些公共代碼抽離到框架中,所有的應用服務都統(tǒng)一使用這套框架進行開發(fā)蜓斧。
使用微服務框架可以實現(xiàn)很多自定義的功能仓蛆。甚至可以將程序調用堆棧信息注入到鏈路跟蹤,實現(xiàn)代碼級別的鏈路跟蹤挎春】锤恚或者輸出線程池、連接池的狀態(tài)信息直奋,實時監(jiān)控服務底層狀態(tài)能庆。
使用統(tǒng)一的微服務框架有一個比較嚴重的問題:框架更新成本很高。每次框架升級脚线,都需要所有應用服務配合升級搁胆。當然,一般會使用兼容方案邮绿,留出一段并行時間等待所有應用服務升級渠旁。但是如果應用服務非常多時,升級時間可能會非常漫長船逮。并且有一些很穩(wěn)定幾乎不更新的應用服務顾腊,其負責人可能會拒絕升級…… 因此,使用統(tǒng)一微服務框架需要完善的版本管理方法和開發(fā)管理規(guī)范挖胃。
另一條路 - Service Mesh
另一種抽象公共代碼的方法是直接將這些代碼抽象到一個反向代理組件杂靶。每個服務都額外部署這個代理組件,所有出站入站的流量都通過該組件進行處理和轉發(fā)酱鸭。這個組件被稱為 Sidecar吗垮。
Sidecar 不會產生額外網絡成本。Sidecar 會和微服務節(jié)點部署在同一臺主機上并且共用相同的虛擬網卡凹髓。所以 sidecar 和微服務節(jié)點的通信實際上都只是通過內存拷貝實現(xiàn)的抱既。
Sidecar 只負責網絡通信。還需要有個組件來統(tǒng)一管理所有 sidecar 的配置扁誓。在 Service Mesh 中防泵,負責網絡通信的部分叫數(shù)據(jù)平面(data plane)蚀之,負責配置管理的部分叫控制平面(control plane)。數(shù)據(jù)平面和控制平面構成了 Service Mesh 的基本架構捷泞。
Sevice Mesh 相比于微服務框架的優(yōu)點在于它不侵入代碼足删,升級和維護更方便。它經常被詬病的則是性能問題锁右。即使回環(huán)網絡不會產生實際的網絡請求失受,但仍然有內存拷貝的額外成本。另外有一些集中式的流量處理也會影響性能咏瑟。
結束拂到、也是開始
微服務不是架構演變的終點。往細走還有 Serverless码泞、FaaS 等方向兄旬。另一方面也有人在唱合久必分分久必合,重新發(fā)現(xiàn)單體架構……
不管怎樣余寥,微服務架構的改造暫時告一段落了领铐。小明滿足地摸了摸日益光滑的腦袋,打算這個周末休息一下約小紅喝杯咖啡宋舷。
“不積跬步绪撵,無以至千里”,希望未來的你能:有夢為馬 隨處可棲祝蝠!加油音诈,少年!
關注公眾號:「Java 知己」绎狭,每天更新Java知識哦改艇,期待你的到來!
- 發(fā)送「Group」坟岔,與 10 萬程序員一起進步谒兄。
- 發(fā)送「面試」,領取BATJ面試資料社付、面試視頻攻略承疲。
- 發(fā)送「玩轉算法」,領取《玩轉算法》系列視頻教程鸥咖。
- 千萬不要發(fā)送「1024」...