作者:fredalxin
地址:https://fredal.xin/talking-msa-a-msa-request
在我們對微服務架構有了整體的認識,并且具備了服務化的前提后,一個完整的微服務請求需要涉及到哪些內容呢?
這其中包括了微服務框架所具備的三個基本功能:
- 服務的發布與參考
- 服務的注冊與發現
- 服務的遠程通信
服務的發布與參考
首先我們面臨的第一個問題是,如何發布服務和參考服務,具體一點就是,這個服務的介面名是啥,有哪些引數,回傳值是什么型別等等,通常也就是介面描述資訊,
常見的發布和參考的方式包括:
- RESTful API / 宣告式Restful API
- XML
- IDL
一般來講,不管使用哪種方式,服務端定義介面與實作介面都是必要的,例如:
@exa(id = "xxx")
public interface testApi {
@PostMapping(value = "https://www.cnblogs.com/soatest/{id}")
String getResponse(@PathVariable(value = "https://www.cnblogs.com/javastack/p/id") final Integer index, @RequestParam(value = "https://www.cnblogs.com/javastack/p/str") final String Data);
}
}
具體實作如下:
public class testApiImpl implements testApi{
@Override
String getResponse(final Integer index, final String Data){
return "ok";
}
}
宣告式Restful API
這種常使用HTTP或者HTTPS協議呼叫服務,相對來說,性能稍差,
首先服務端如上定義介面并實作介面,隨后服務提供者可以使用類似restEasy這樣的框架通過servlet的方式發布服務,而服務消費者直接參考定義的介面呼叫,
除此之外還有一種類似feign的方式,即服務端的發布依賴于springmvc controller,框架只基于客戶端模板化http請求呼叫,這種情況下需介面定義與服務端controller協商一致,這樣客戶端直接參考介面發起呼叫即可,
XML
使用私有rpc協議的都會選擇xml配置的方式來描述介面,比較高效,例如dubbo、motan等,
同樣服務端如上定義介面并實作介面,服務端通過server.xml將檔案介面暴露出去,服務消費者則通過client.xml參考需要呼叫的介面,
但這種方式對業務代碼入侵較高,xml配置有變更時候,服務消費者和服務提供者都需要更新,
IDL
IDL是介面描述語言,常用于跨語言之間的呼叫,最常用的IDL包括Thrift協議以及gRpc協議,例如gRpc協議使用Protobuf來定義介面,寫好一個proto檔案后,利用語言對應的protoc插件生成對應server端與client端的代碼,便可直接使用,
但是如果引數欄位非常多,proto檔案會顯得非常大難以維護,并且如果欄位經常需要變更,例如洗掉欄位,PB就無法做到向前兼容,
一些tips
不管哪種方式,在介面變更的時候都需要通知服務消費者,消費者對api的強依賴性是很難避免的,介面變更引起的各種呼叫失敗也十分常見,所以如果有變更,盡量使用新增介面的方式,或者給每個介面定義好版本號吧,
在使用上,大多數人的選擇是對外Restful,對內Xml,跨語言IDL,
一些問題
在實際的服務發布與參考的落地上,還會存在很多問題,大多和配置資訊相關,例如一個簡單的介面呼叫超時時間配置,這個配置應該配在服務級別還是介面級別?是放在服務提供者這邊還是服務消費者這邊?
在實踐中,大多數服務消費者會忽略這些配置,所以服務提供者自身提供默認的配置模板是有必要的,相當于一個預定義的程序,每個服務消費者在繼承服務提供者預定義好的配置后,還需要能夠進行自定義的配置覆寫,
但是,比方說一個服務有100個介面,每個介面都有自身的超時配置,而這個服務又有100個消費者,當服務節點發生變更的時候,就會發生100*100次注冊中心的訊息通知,這是比較可怕的,就有可能引起網路風暴,
服務的注冊與發現
假設你已經發布了服務,并在一臺機器上部署了服務,那么消費者該怎樣找到你的服務的地址呢?
也許有人會說是DNS,但DNS有許多缺陷:
- 維護麻煩,更新延遲
- 無法在客戶端做負載均衡
- 不能做到埠級別的服務發現
其實在分布式系統中,有個很重要的角色,叫注冊中心,便是用于解決該問題,

使用注冊中心尋址并呼叫的程序如下:
- 服務啟動時,向注冊中心注冊自身,并定期發送心跳匯報存活狀態,
- 客戶端呼叫服務時,向注冊中心訂閱服務,并將節點串列快取至本地,再與服務端建立連接(當然這兒可以lazy load),發起呼叫時,在本地快取節點串列中,基于負載均衡演算法選取一臺服務端發起呼叫,
- 當服務端節點發生變更,注冊中心能感知到后通知到客戶端,
注冊中心的實作主要需要考慮以下這些問題:
- 自身一致性與可用性
- 注冊方式
- 存盤結構
- 服務健康監測
- 狀態變更通知
一致性與可用性
一個老舊的命題,即分布式系統中的CAP(一致性、可用性、磁區容錯性),我們知道同時滿足CAP是不可能的,那么便需要有取舍,常見的注冊中心大致分為CP注冊中心以及AP注冊中心,
CP注冊中心
比較典型的就是zookeeper、etcd以及consul了,犧牲可用性來保證了一致性,通過zab協議或者raft協議來保證一致性,
AP注冊中心
犧牲一致性來保證可用性,感覺只能列出eureka了,eureka每個服務器單獨保存節點串列,可能會出現不一致的情況,
從理論上來說,僅用于注冊中心,AP型是遠比CP型合適的,可用性的需求遠遠高于一致性,一致性只要保證最終一致即可,而不一致的時候還可以使用各種容錯策略進行彌補,
保障高可用性其實還有很多辦法,例如集群部署或者多IDC部署等,Consul就是多IDC部署保障可用性的典型例子,它使用了wan gossip來保持跨機房狀態同步,
注冊方式
有兩種與注冊中心互動的方式,一種是通過應用內集成sdk,另一種則是通過其他方式在應用外間接與注冊中心互動,
應用內
這應該就是最常見的方式了,客戶端與服務端都集成相關sdk與注冊中心進行互動,例如選擇zookeeper作為注冊中心,那么就可以使用curator sdk進行服務的注冊與發現,
應用外
consul提供了應用外注冊的解決方案,consul agent或者第三方Registrator可以監聽服務狀態,從而負責服務提供者的注冊或銷毀,而Consul Template則可以做到定時從注冊中心拉取節點串列,并重繪LB配置(例如通過Nginx的upstream),這樣就相當于完成了服務消費者端的負載均衡,
存盤結構
注冊中心存盤相關資訊一般采取目錄化的層次結構,一般分為服務-介面-節點資訊,
同時注冊中心一般還會進行分組,分組的概念很廣,可以是根據機房劃分也可以根據環境劃分,
節點資訊主要會包括節點的地址(ip和埠號),還有一些節點的其他資訊,比如請求失敗的重試次數、超時時間的設定等等,
當然很多時候,其實可能會把介面這一層給去掉,因為考慮到介面數量很多的情況下,過多的節點會造成很多問題,比如之前說的網路風暴,
服務健康監測
服務存活狀態監測也是注冊中心的一個必要功能,在zookeeper中,每個客戶端都會與服務端保持一個長連接,并生成一個session,在session過期周期內,通過客戶端定時向服務端發送心跳包來檢測鏈路是否正常,服務端則重置下次session的過期時間,如果session過期周期內都沒有檢測到客戶端的心跳包,那么就會認為它已經不可用了,將其從節點串列中移除,
狀態變更通知
在注冊中心具備服務健康檢測能力后,還需要將狀態變更通知到客戶端,在zookeeper中,可以通過監聽器watcher的process方法來獲取服務變更,
服務的遠程通信
在上面,服務消費者已經正確參考了服務,并發現了該服務的地址,那么如何向這個地址發起請求呢?要解決服務間的遠程通信問題,我們需要考慮一些問題:
- 網路I/O的處理
- 傳輸協議
- 序列化方式
網路I/O的處理
簡單來說,就是客戶端是怎么處理請求?服務端又是怎么處理請求的?
先從客戶端來說,我們創建連接的時機可以是從注冊中心獲取到節點資訊的時候,但更多時候,我們會選擇在第一次請求發起呼叫的時候去創建連接,此外,我們往往會為該節點維護一個連接池,進行連接復用,
如果是異步的情況下,我們還需要為每一個請求編號,并維護一個請求池,從而在回應回傳時找到對應的請求,當然這并不是必須的,很多框架會幫我們干好這些事情,比如rxNetty,
從服務端來說,處理請求的方式就可以追溯到unix的5種IO模型了,我們可以直接使用Netty、MINA等網路框架來處理服務端請求,或者如果你有十分的興趣,可以自己實作一個通信框架,
傳輸協議
最常見的當然是直接使用Http協議,使用雙方無需關注和了解協議內容,方便直接,但自然性能上會有所折損,
還有就是目前比較火熱的http2協議,擁有二進制資料、頭部壓縮、多路復用等許多優良特性,但從自身的實踐上看,http2要走到生產仍有一段距離,一個最簡單的例子,升級到http2后所有的header names都變成小寫,同時不是case-insenstive了,這時候就會有兼容性問題,
當然如果追求更高效與可控的傳輸,可以定制私有協議并基于tcp進行傳輸,私有協議的定制需要通信雙方都了解其特性,設計上還需要注意預留好擴展欄位,以及處理好粘包分包等問題,
序列化方式
在網路傳輸的前后,往往都需要在發送端進行編碼,在服務端進行解碼,這樣主要是為了在網路傳輸時候減少資料傳輸量,
常用的序列化方式包括文本類的,例如XML/JSON,還有二進制型別的,例如Protobuf/Thrift等,在選擇序列化的考慮上,一是性能,Protobuf的壓縮大小和壓縮速度都會比JSON快很多,性能也更好,二是兼容性上,相對來說,JSON的前后兼容性會強一些,可以用于介面經常變化的場景,
在此還是需要強調,使用每一種序列化都需要了解過其特性,并在介面變更的時候拿捏好邊界,例如jackson的FAIL_ON_UNKNOW_PROPERTIES屬性、kryo的CompatibleFieldSerializer、jdk序列化會嚴格比較serialVersionUID等等,
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2021最新版)
2.終于靠開源專案弄到 IntelliJ IDEA 激活碼了,真香!
3.阿里 Mock 工具正式開源,干掉市面上所有 Mock 工具!
4.Spring Cloud 2020.0.0 正式發布,全新顛覆性版本!
5.《Java開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/285563.html
標籤:Java
