一,為什麼寫JMicro
印象中初次接觸微服務大概是2011年,那會做Eclpise外掛開發,網上檢視好多關於OSGI的技術文章,發現Spring新出了一個叫Spring-boot的框架,那會沒太上心,只是瞭解了點皮毛,工作又太忙,之後就沒下文了。
直到大概2015年的某天,碰到一個小專案,沒什麼難度,都用老套路去玩,沒什麼意思,得玩點新東西才行,也不枉一翻付出,於是選擇用GO語言實現,選擇GO主要是想體驗一下GO,看是不是真如傳說中的那樣無敵。經過一翻折騰,最終確定GOGIN+GOMICRO實現。是的,從那會開始,通過學心和使用GOMICRO,從此迷上微服務。後來因為工作需要,再沒什麼機會在專案中接觸GO。
後面也曾試圖去用Dubbo和Spring-Cloud做專案,但也止於淺嘗則止,沒能深入。一方面專案時間太緊,折騰不起。另一方面,也是最重要的,專案組成員根本不願意去學新東西,在很多成員心中,微服務,Spring-Cloud太深奧,玩不起,沒時間,至於Dubbo,寫個服務,做個RPC也是從百度複製下來的,跑起來就完事了,沒人關心箇中的原理,經常碰到問題就抓狂!
從那時候起,就一直琢磨著能不能用Java寫一個像GoMicro一樣簡單的微服務框架,這個框架要確保足夠簡單,入門和使用成本要底,就像寫HelloWord一樣,但微服務的基本功能要全。因此專案依賴不能多,打出來的執行包也要小,能不用的第三方包堅決不用,最好使用JDK庫就行!
二.從RPC開始
微服務的基礎是RPC,而反過來單單有RPC卻不能叫微服務,微服務是在RPC基礎上,實現一系列的基礎性功能支援後才能叫微服務。沒有超時,限流,熔斷這些基礎支援,服務“微”了之後,隨時都可能掛機,這就是為什麼微服務之前存在很多RPC框架,但卻沒人說那是微服務。
堅持從JDK庫入手,使用原生NIO SOCKET做了個簡單的RPC,並且相容了HTTP方式,完後做壓力測試發現效能太差,然後退一步吧,選擇Mina做底層通訊重新再折騰一遍,結果還是不盡如人間,效能還可以,就是自覺得太繁瑣臃腫,還是不滿意,最終又改為Netty才完成RPC基本功能開發。很多時候,路就是一步一個腳印走出來的,如果前面碰到大山大河,實在跨不過去,回頭繞個灣也是必須的,否則一步沒跨過去就OVER了。最優的不是選擇最好的,而是選擇合適的。
接著做IOC,超時,熔斷,限速,配置管理,服務注理,監控服務,負載均衡,服務路由,API閘道器,HTTP,WEB Socket,全域性ID,鏈路追蹤,訊息服務,非同步RPC,基於VUE做的後臺管理,服務編排。。。。,到現下圖功能基本上都有實現,並且正常可用,後期肯定會不斷優化。
圖中最底層是JMicro依賴服務,沒做任何修改,Redis用於做高速一至性ID生成及訊息快取,Zookeeper做配置管理及服務註冊中心,應用可以通過JMicro IOC容器直接獲得Redis和Zookeeper例項元件並使用,非常方便;Netty做RPC底層資料通訊,同時支援HTTP及Websocket,應此支援Web端直接訪問微服務;Mongodb是一個可選依賴,用於儲存RPC監控日誌和事件資訊;Vue+IView實現後臺管理的前端頁面。
智慧控制:目前沒有實現,之所以列出來,是因為一直把他當作JMicro的核心功能,目的是通過監控獲取到的系統資料訓練一個人工智慧大腦,讓其自動完成JMicro系統的日常維護。
IOC容器:之所以沒用現成的Spring等容器實現,就如一開始所說的,他們太複雜,不夠輕量(即使他們一直堅持認為他們是輕量級容器),對於JMicro來說,他們還是大重了,也不利於JMicro對微服務的量身定製。JMIcro IOC使用非常簡單,在99%情況,通過Component註解元件,通過Inject註解欄位就夠了,就這麼簡單。
編碼解碼:如果說RPC是微服務的基礎,那麼編碼解碼則是微服務的基石,基石不牢,服務的大廈隨時都可能倒塌。我將JMicro實現的編碼解碼實命名為字首型別編碼解碼器,首先對類對進行編碼,然後將類的編碼資訊和要傳輸的資料一起序列化到二進位制資料中,解碼時則將編碼轉為類資訊,然後反序列化二進位制資料為類物件,當然,編碼考慮了高頻使用的類並確保其編碼最短,從而達到序列化的資料冗餘最底。測試效果還是相當不錯的,對比ProtoBuffer效能不相上下,某此情況差此,但某些情況比ProtoBuffer還好,甩JDK原生序列化及Kryo序列化幾條街。
別的功能待以後再細說。
實現以上功能的全部第三方依賴如下:
其中
javapoet是編譯時做程式碼生成用的,在最終執行包中並不需要
guava前期實現限流試驗性用到,最後限速並不理想,實際上已經排除
Curator後期打算去除,只保留Zookeeper.
三,目前最滿意的一個特性:非同步RPC
大家都知道,NodeJS單執行緒,但其效能卻很強勁,因為非同步;Redis效能也很強勁,因為非同步;無數的庫或服務都強調通過非同步提高效能。
在對JMIcro做壓力測試過程中,我深深地體會到一個同步的代價(效能底還不說,高併發時還會死鎖)而非同步的必要性。在JMicro中,非同步體現在很多模組中。其中主要以下幾個:
- 訊息服務,一個針對JMicro量身定製的非同步訊息中介軟體,服務可以通過其實現訊息釋出/訊息訂閱; 還可以做非同步RPC,比如釋出一個RPC服務方法,使其自動訂閱特定主題的訊息,訊息中介軟體能識別此種型別的服務方法,並將匹配的訊息傳送給此服務的服務方法。
- 以1作為基礎,可以在客戶端及服務端做非同步RPC,比如在客戶端過直接將訊息傳送到訊息中介軟體,讓訊息中介軟體轉發給目標服務;也可以在服務端收到訊息後(比如API閘道器,同步轉非同步,綃峰限流),將訊息傳送到訊息伺服器,讓訊息伺服器轉發給目標方法。
- 程式碼級別的非同步RPC呼叫,讓Java程式碼像Scala,NodeJS等語言一樣做非同步程式設計,以下對此做詳細的Demo
下面片段程式碼摘自
ISimpleRpcAsyncClient src = (ISimpleRpcAsyncClient)of.get(ISimpleRpc.class);
//最外層非同步RPC呼叫
src.helloAsync("Hello JMicro").then((rst, fail)->{
//第一次非同步返回結果
System.out.println(rst);
//做一次同步呼叫
String r = src.hello("Hello two");
//同步返回結果
System.out.println(r);
//再做一次非同步呼叫
src.helloAsync("Hello two").then((rst1, fail1)->{
//非同步返回結果
System.out.println(rst);
});
});
我覺得這種非同步RPC風格是最有殺傷力的,這使得我們的遠端RPC呼叫不再阻塞執行緒,包括工作執行緒和網路IO執行緒,只要我們的執行緒佔用CPU,他都是在做有用的工作!
四,體驗JMiro非同步RPC
前面吹了這麼多,不來點實際可見的操作,肯定不能讓人信服,現在手把手教你體驗一把JMicro是不是真的做了遠端RPC呼叫,是不是真的是非同步呼叫。
1. 環境準備
首先確保本機安裝了Java,Maven,Zookeeper監聽在2181埠,Redis工作在6379埠,保持預設,所有配置都省了。
2. 下載JMicro程式碼
到https://github.com/mynewworldyyl/jmicro下載程式碼到特定目錄,下面以${basedir}指代此目錄
3. 開啟一個命令列視窗,cd進入到${basedir}\codegenerator目錄,執行
mvn clean install -Dmaven.test.skip=true
4. 上面命令執行成功後,cd進入到${basedir}目錄,執行mvn clean install -Dmaven.test.skip=true
5. 確保3和4成功後,cd進入到${basedir}\example目錄, 執行mvn clean install-Dmaven.test.skip=true
6. 通過3,4,5步確保相關依賴包都已經安裝在Maven本地倉庫中,打包可執行的服務Jar包,cd進入到${basedir}\example\example.provider目錄, 執行mvn mvn clean install -Pbuild-main -Dmaven.test.skip=true
7. 執行服務端
java -jar target/jmicro-example.provider-0.0.1-SNAPSHOT-jar-with-dependencies.jar -javaagent: ${basedir}\target\jmicro-agent-0.0.1-SNAPSHOT.jar
看到以下輸出說明服務啟動成功
8. 打包可執行的測試客戶端Jar包,cd進入到${basedir}\example\example.comsumer目錄, 執行mvn clean install -Pbuild-main -Dmaven.test.skip=true
9. 執行客戶端
java -jar target/jmicro-example.comsumer-0.0.1-SNAPSHOT-jar-with-dependencies.jar -javaagent: ${basedir}\target\jmicro-agent-0.0.1-SNAPSHOT.jar
以下是客戶端兩次非同步呼叫,一次同步呼叫返回的結果
對應伺服器的3個被呼叫的輸出
以上樣例對應的服務介面及實現類分別為:
@Service //指定此介面是一個遠端介面,對外提供RPC服務
@AsyncClientProxy //編譯時註解,Javac編譯器呼叫特定註解處理器生成客戶端訪問程式碼
public interface ISimpleRpc {
//我們測試用的方法
String hello(String name);
//IPromise<String> helloAsync(String name);
//POJO物件為引數的RPC方法
String hi(Person p);
//測試RPC鏈路
String linkRpc(String msg);
}
服務實現類程式碼有點多,在此只貼我們呼叫的方法,別的請檢視原始碼
public String hello(String name) {
if(SF.isLoggable(MC.LOG_DEBUG)) {
//向監控服務傳送一條日誌事件,請放心,呼叫此方法只是把日誌存於本地並立即返回,不影響正常的效能
SF.eventLog(MC.MT_PLATFORM_LOG,MC.LOG_DEBUG,SimpleRpcImpl.class, name);
}
System.out.println("Server hello: " +name);
//返回一條資訊給客戶端
return "Server say hello to: "+name;
}
客戶端測試類
完整類圖
簡化類圖
五,實現原理
建議適當修改上面程式碼然後編譯執行測試,比如修改服務端返回值,修改客戶端呼叫引數,服務端返回改為同步,或再呼叫別的服務(可以同步呼叫,也可非同步呼叫)。
啟動多個example.provider例項(理論上,你可以啟動無數個服務例項,那怕都在同一臺機器上)多次執行客戶端呼叫看看呼叫了那個服務例項(服務路由及負載均衡)
如果你對微服務有興趣,並且有足夠的耐心,你應該能從中獲得樂趣。
如果認真檢視了原始碼,應該看到以下幾個關鍵的註解,而使用JMIcro,基本上使用這幾個註解就夠了:
1. Service:如果註解在介面上,意思是告訴JMicro容器,這個一個服務接實口,客戶端使用介面類為引數get例項時,請返回服務代理例項;如果註解在實現類上,告訴JMicro容器,這是一個RPC服務,請將服務資訊釋出到服務註冊中心,讓客戶端知道我的存在。
2. AsyncClientProxy:註解服務介面,告訴編譯器在編譯全部原始碼前,請呼叫註解處理器生成服務客戶端代理介面及實現類,供客戶端直接使用,讓客戶端覺得自己就好像獲得了服務實現類的直接引用一樣,完全不知道是跨JVM的遠端呼叫。
3. Component:JMIcro容器元件,JMicro容器啟動時例項化,供容器中的其他元件使用,是的,服務本身也是一個元件,只不過其實現了服務介面,並向註冊中心註冊自己才變成了遠端服務,同時其本身也是一個元件,可以被同一個容器中的其他元件依賴並被自動注入。
4. Reference:註解在元件的欄位上,告訴JMicro容器,我需要一個遠端服務代理例項,請將這個例項值賦給我註解的欄位。
5. SMethod註解在方法上,告訴JMicro容器,我是一個服務方法,並指定服務引數,比如超時時間,熔斷策略,是否可監控,日誌級別等。
6. 每個服務都由三個值唯一確定,分別是服務介面全稱(serviceName),名稱空間(namespace),版本(version),並使用在Service及Reference註解上。
後面有時間會對JMicro的細節做更多介紹,以實戰為主,微服務概念為鋪。
附一個後臺管理的前端鏈路日誌查詢頁面
大家有任何問題歡迎評論。
如果你對這個專案感興趣,也歡迎參與開發,共同完善。
後面資金允許後,買個伺服器在公網上部署一套,讓大家體驗!