一、分布式項目開發(fā)與聯(lián)調(diào)
接口暴露與引用
暴露接口的通常做法是 接口與實現(xiàn)分離,服務(wù)端將 接口蠢琳、模型、異常 等統(tǒng)一放置于一個模塊咬最,實現(xiàn)置于另一個模塊圈驼。調(diào)用方通過Maven進行引用玉锌。
注意:在分布式項目中,不會把整個服務(wù)提供方打包成JAR并提供給消費端憔维,而是選擇單獨把接口與實體類打包成JAR提供給消費端章鲤。
自動化構(gòu)建與協(xié)作
當(dāng)項目越來越多,服務(wù)依懶關(guān)系越發(fā)復(fù)雜的時候枯途,為了提高協(xié)作效率灰瞻,必須采用自動化工具 完成 接口從編寫到構(gòu)建成JAR包构回,最后到引用的整個過程。
流程描述:
- 服務(wù)提供者項目發(fā)人員編寫Client 接口
- push 至遠程倉庫
- jenkins 構(gòu)建指定版本
- jenkins Deploye 至私服倉庫 nexus
- 服務(wù)消費者項目開發(fā)人員基于maven 從私服務(wù)倉庫下載
接口平滑升級
在項目迭代過程當(dāng)中京腥, 經(jīng)常會有多個項目依懶同一個接口,如下圖 項目B、C都依懶了項目A當(dāng)中的接口1鸿市,此時項目B業(yè)務(wù)需要验游,需要接口1多增加一個參數(shù),升級完成后砸王。項目B能正確構(gòu)建上線,項目C卻不行默穴。
解決辦法與原則:
- 接口要做到向下兼容:接口參數(shù)盡量以對象形式進行封裝膊爪。Model屬性只增不刪叫确,如果需要作廢,可以添加@Deprecated 標識杏慰。
- 如果出現(xiàn)了不可兼容的變更凹耙,則必須通知調(diào)用方整改意述,并制定上線計劃瓣戚。
-- 不推薦仓技,兼容性差
public interface UserService {
User getUser(Integer id, string name);
}
-- 推薦
public interface UserService {
User getUser(UserParam param);
public static class UserParam
{
Integer id;
string name;
}
}
二诉濒、Dubbo控制管理后臺使用
Dubbo 控制后臺的安裝
#從github 中下載dubbo 項目
git clone https://github.com/apache/incubator-dubbo.git
#更新項目
git fetch
#臨時切換至 dubbo-2.5.8 版本
git checkout dubbo-2.5.8
#進入 dubbo-admin 目錄
cd dubbo-admin
#mvn 構(gòu)建admin war 包
mvn clean pakcage -DskipTests
#得到 dubbo-admin-2.5.8.war 即可直接部署至Tomcat
#修改 dubbo.properties 配置文件
dubbo.registry.address=zookeeper://127.0.0.1:2181
三迫卢、Dubbo注冊中心詳解
為了到達服務(wù)集群動態(tài)擴容的目的上荡,注冊中心存儲了服務(wù)的地址信息與可用狀態(tài)信息塞祈,并實時推送給訂閱了相關(guān)服務(wù)的客戶端哼御。
一個完整的注冊中心需要實現(xiàn)以下功能:
- 接收服務(wù)端的注冊與客戶端的引用嗦哆,即將引用與消費建立關(guān)聯(lián),并支持多對多鬓梅。
- 當(dāng)服務(wù)非正常關(guān)閉時能即時清除其狀態(tài)
- 當(dāng)注冊中心重啟時物遇,能自動恢復(fù)注冊數(shù)據(jù)眶根,以及訂閱請求
- 注冊中心本身的集群
Zookeeper 注冊中心
關(guān)于Zookeeper 注冊中心同樣需要了解其存儲結(jié)構(gòu)和更新機制。
Zookeper是一個樹型的目錄服務(wù),本身支持變更推送相比redis的實現(xiàn)Publish/Subscribe功能更穩(wěn)定厚骗。
注意:其中葉子節(jié)點為臨時節(jié)點刨仑。
源碼解析
注意:UserService是一個代理對象士八,由ReferenceConfig引用對象生成,并把ClusterInvoker阻课、RegistryDirectory賦予給它。
四、Dubbo調(diào)用模塊
dubbo調(diào)用模塊核心功能是發(fā)起一個遠程方法的調(diào)用并順利拿到返回結(jié)果垒棋,其體系組成如下:
- 透明代理:通過動態(tài)代理技術(shù)甜无,屏蔽遠程調(diào)用細節(jié)以提高編程友好性。這里dubbo 使用了 javassist作為代理實現(xiàn)评姨。
- 負載均衡:當(dāng)有多個提供者是,如何選擇哪個進行調(diào)用的負載算法阻星。
- 容錯機制:當(dāng)服務(wù)調(diào)用失敗時采取的策略
- 調(diào)用方式:支持同步調(diào)用、異步調(diào)用
負載均衡
Dubbo 目前官方支持以下負載均衡策略:
- 隨機(random):按權(quán)重設(shè)置隨機概率印颤。此為默認算法.
- 輪循 (roundrobin):按公約后的權(quán)重設(shè)置輪循比率。
- 最少活躍調(diào)用數(shù)(leastactive):相同活躍數(shù)的隨機衣迷,活躍數(shù)指調(diào)用前后計數(shù)差陨界。
-
一致性Hash(consistenthash ):相同的參數(shù)總是發(fā)到同一臺機器录淡,默認初始化160個虛擬點窍帝,相對hash取余的方式,一致性Hash避免了單一服務(wù)過熱以及節(jié)點數(shù)量變化后全局亂套的缺點突梦。
image.png
容錯
Dubbo 官方目前支持以下容錯策略:
- 失敗自動切換:調(diào)用失敗后基于retries=“2” 屬性重試其它服務(wù)器
- 快速失敗:快速失敗礼预,只發(fā)起一次調(diào)用,失敗立即報錯扮休。
- 勿略失敗:失敗后勿略凳兵,不拋出異常給客戶端。
- 失敗重試:失敗自動恢復(fù),后臺記錄失敗請求,定時重發(fā)丈探。通常用于消息通知操作
- 并行調(diào)用: 只要一個成功即返回,并行調(diào)用指定數(shù)量機器,可通過 forks="2" 來設(shè)置最大并行數(shù)侣监。
- 廣播調(diào)用:廣播調(diào)用所有提供者遮咖,逐個調(diào)用诀蓉,任意一臺報錯則報錯
異步調(diào)用
異步調(diào)用是指發(fā)起遠程調(diào)用之后獲取結(jié)果的方式鲤孵。
- 同步等待結(jié)果返回(默認)
- 異步等待結(jié)果返回
-
不需要返回結(jié)果
Dubbo 中關(guān)于異步等待結(jié)果返回的實現(xiàn)流程如下圖:
image.png
demoService.sayHello1("han");
Future<Object> future1 = RpcContext.getContext().getFuture(); //底層ThreadLocal
demoService.sayHello2("han2");
Future<Object> future2 = RpcContext.getContext().getFuture();
Object r1 = null, r2 = null;
// wait 直到拿到結(jié)果 獲超時
r1 = future1.get(); //同步
// wait 直到拿到結(jié)果 獲超時
r2 = future2.get(); //同步
注意:有返回值的時候,future.get()方法執(zhí)行的是同步,提升效率的關(guān)鍵在于多個future.get()方法同時執(zhí)行時,全部完成的時間是future.get()方法最慢的那一個,而不是多個future.get()方法的總和。
注意:同步調(diào)用底層也使用了Future阱穗,與異步不同的是鲁僚,同步對異步多了一層包裝冰沙,里面使用了future.get()方法科展,一個線程死循環(huán)在訪問結(jié)果是否有值逗抑,一個線程在檢測線程是否超時。
五鳄梅、Dubbo 調(diào)用非典型使用場景
泛化提供
是指不通過接口的方式直接將服務(wù)暴露出去叠国。通常用于Mock框架或服務(wù)降級框架實現(xiàn)。
模擬出來的通用服務(wù)提供方
public class MockService implements GenericService {
private String target;
public MockService(String target) {
this.target = target;
}
// 通用方法
@Override
public Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException {
if (target.equals("com.lin.client.UserService") && method.equals("getUser")) {
HashMap<Object, Object> map = new HashMap<>();
map.put("id", 1);
map.put("name", "克里斯");
return map;
}
return null;
}
}
public class DubboServer {
public static void main(String[] args) throws IOException {
ApplicationConfig applicationConfig = new ApplicationConfig("sample-app");
ProtocolConfig protocolConfig = new ProtocolConfig();
protocolConfig.setName("dubbo");
protocolConfig.setSerialization("fastjson");
protocolConfig.setPort(-1);//20880
RegistryConfig registryConfig = new RegistryConfig("zookeeper://192.168.0.147:2181");
ServiceConfig serviceConfig = new ServiceConfig();
serviceConfig.setInterface("com.tuling.client.UserService");
//serviceConfig.setRef(new UserServiceImpl());
setMock("com.lin.client.UserService");
serviceConfig.setRegistry(registryConfig);
serviceConfig.setProtocol(protocolConfig);
serviceConfig.setApplication(applicationConfig);
serviceConfig.export();
System.out.println("服務(wù)已暴露");
System.in.read();
}
public static void setMock(ServiceConfig serviceConfig, String server) {
serviceConfig.setRef(new MockService(server));
}
}
隱示傳參
是指通過非常方法參數(shù)傳遞參數(shù)戴尸,類似于http 調(diào)用當(dāng)中添加cookie值粟焊。通常用于分布式追蹤框架的實現(xiàn)。使用方式如下 :
//客戶端隱示設(shè)置值
RpcContext.getContext().setAttachment("index", "1"); // 隱式傳參,后面的遠程調(diào)用都會隱
//服務(wù)端隱示獲取值
String index = RpcContext.getContext().getAttachment("index");
令牌驗證
通過令牌驗證在注冊中心控制權(quán)限吆玖,以決定要不要下發(fā)令牌給消費者筒溃,可以防止消費者繞過注冊中心訪問提供者,另外通過注冊中心可靈活改變授權(quán)方式沾乘,而不需修改或升級提供者
使用:
<!--隨機token令牌怜奖,使用UUID生成-->
<dubbo:provider interface="com.foo.BarService" token="true" />
過濾器
類似于 WEB 中的Filter ,Dubbo本身提供了Filter 功能用于攔截遠程方法的調(diào)用翅阵。其支持自定義過濾器與官方的過濾器使用:
<dubbo:provider filter="accesslog" accesslog="logs/dubbo.log"/>
以上配置 就是 為 服務(wù)提供者 添加 日志記錄過濾器歪玲, 所有訪問日志將會集中打印至 accesslog 當(dāng)中。
自定義過濾器:
- 編寫過濾器
package com.tuling.dubbo;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
@Activate(group = {CommonConstants.PROVIDER})
public class ProviderHelloFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
System.out.println("log========>");
return invoker.invoke(invocation);
}
}
- 添加擴展點
# 文件路徑
META-INF/dubbo/org.apache.dubbo.rpc.Filter
#內(nèi)容:
helloFilter=com.tuling.dubbo.ProviderHelloFilter
六掷匠、調(diào)用內(nèi)部實現(xiàn)源碼分析
分析代理類
在調(diào)用服務(wù)端時滥崩,是接口的形式進行調(diào)用,該接口是Duboo 動態(tài)代理之后的實現(xiàn)讹语,通過反編譯工具可以查看到其具體實現(xiàn):
因為類是代理生成钙皮,所以采用arthas工具來反編譯,具體操作如下:
#運行 arthas
java -jar arthas-boot.jar
#掃描類
sc *.proxy0
#反編譯代理類
jad com.alibaba.dubbo.common.bytecode.proxy0
反編譯的代碼如下:
package org.apache.dubbo.common.bytecode;
import com.alibaba.dubbo.rpc.service.EchoService;
import com.tuling.client.User;
import com.tuling.client.UserService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.List;
import org.apache.dubbo.common.bytecode.ClassGenerator;
public class proxy0 implements ClassGenerator.DC, EchoService, UserService {
public static Method[] methods;
private InvocationHandler handler;
public List findUser(String string, String string2) {
Object[] arrobject = new Object[]{string, string2};
Object object = this.handler.invoke(this, methods[0], arrobject);
return (List)object;
}
public User getUser(Integer n) {
Object[] arrobject = new Object[]{n};
Object object = this.handler.invoke(this, methods[1], arrobject);
return (User)object;
}
@Override
public Object $echo(Object object) {
Object[] arrobject = new Object[]{object};
Object object2 = this.handler.invoke(this, methods[2], arrobject);
return object2;
}
public proxy0() {
}
public proxy0(InvocationHandler invocationHandler) {
this.handler = invocationHandler;
}
}
可看出其代理實現(xiàn)了 UserService 接口顽决。并且基于InvocationHandler 進行代理短条。實際類是 InvokerInvocationHandler 并且其中之屬性為Invoker.。也就是說最終會調(diào)用Invoker進行遠程調(diào)用才菠。
Dubbo調(diào)用流程
//------7協(xié)議 調(diào)用
doInvoke:77, DubboInvoker {org.apache.dubbo.rpc.protocol.dubbo}
invoke:155, AbstractInvoker {org.apache.dubbo.rpc.protocol}
//------6異步轉(zhuǎn)同步
invoke:52, AsyncToSyncInvoker {org.apache.dubbo.rpc.protocol} // 異步轉(zhuǎn)同步 ,返回結(jié)果之前進行阻塞調(diào)用線程
//----- 5過濾器鏈
invoke:92, MonitorFilter {org.apache.dubbo.monitor.support} // 過濾鏈-> 監(jiān)控器
invoke:54, FutureFilter {org.apache.dubbo.rpc.protocol.dubbo.filter} //過濾鏈-> 回調(diào)參數(shù)
invoke:14, ProviderHelloFilter {com.tuling.dubbo} // 過濾鏈-> 自定義過濾器
invoke:60, ConsumerContextFilter {org.apache.dubbo.rpc.filter} // 過濾鏈-> 消費者環(huán)境初始化
//------4集群處理
doInvoke:82, FailoverClusterInvoker {org.apache.dubbo.rpc.cluster.support} // 集服-失敗重試
invoke:248, AbstractClusterInvoker {org.apache.dubbo.rpc.cluster.support} //
//----- 3Mock服務(wù)
invoke:78, MockClusterInvoker {org.apache.dubbo.rpc.cluster.support.wrapper} // mock 服務(wù)
//----- 2動態(tài)代理 --透明化
invoke:55, InvokerInvocationHandler {org.apache.dubbo.rpc.proxy}// 代理的中間接口
getUser:-1, proxy0 {org.apache.dubbo.common.bytecode} // 代理對象
//----- 1調(diào)用客戶端
main:53, DubboClient {com.tuling.dubbo} // 客戶端
協(xié)議-->注冊協(xié)議--->MockClusterInvoker--->ClusterInvoker--->RegistryDirectory--->DubboProtcol->FilterChain-->DubboInvoker
注意:核心在于運用了責(zé)任鏈模式與spi擴展點的技術(shù)茸时。
七、RPC 協(xié)議
在一個典型RPC的使用場景中赋访,包含了服務(wù)發(fā)現(xiàn)可都、負載、容錯蚓耽、網(wǎng)絡(luò)傳輸渠牲、序列化等組件,其中RPC協(xié)議就指明了程序如何進行網(wǎng)絡(luò)傳輸和序列化 田晚。也就是說一個RPC協(xié)議的實現(xiàn)就等于一個非透明的遠程調(diào)用實現(xiàn)嘱兼,如何做到的的呢?
dubbo 支持的RPC協(xié)議列表
dubbo協(xié)議結(jié)構(gòu)
- magic:類似java字節(jié)碼文件里的魔數(shù)贤徒,用來判斷是不是dubbo協(xié)議的數(shù)據(jù)包芹壕。魔數(shù)是常量0xdabb,用于判斷報文的開始。
- flag:標志位, 一共8個地址位接奈。低四位用來表示消息體數(shù)據(jù)用的序列化工具的類型(默認hessian)踢涌,高四位中,第一位為1表示是request請求序宦,第二位為1表示雙向傳輸(即有返回response)睁壁,第三位為1表示是心跳ping事件。
- status:狀態(tài)位, 設(shè)置請求響應(yīng)狀態(tài),dubbo定義了一些響應(yīng)的類型潘明。具體類型見 com.alibaba.dubbo.remoting.exchange.Response
- invoke id:消息id, long 類型行剂。每一個請求的唯一識別id(由于采用異步通訊的方式,用來把請求request和返回的response對應(yīng)上)
-
body length:消息體 body 長度, int 類型钳降,即記錄Body Content有多少個字節(jié)厚宰。
image.png