一、前言
部門去年年中開始各種改造,第一步是模塊服務化,這邊初選dubbo試用在一些非重要模塊上,慢慢引入到一些稍微重要的功能上,半年時間,學習過程及線上使用遇到的些問題在此總結下。
整理這篇文章差不多花了兩天半時間,請尊重勞動成果,如轉載請註明出處http://blog.csdn.net/hzzhoushaoyu/article/details/43273099
二、什麼是dubbo
Dubbo是阿里巴巴提供的開源的SOA服務化治理的技術框架,據說只是剖出來的一部分開源的,但一些基本的需求已經可以滿足的,而且擴展性也非常好(至今沒領悟到擴展性怎麼做到的),通過spring bean的方式管理配置及實例,較容易上手且對應用無侵入。更多介紹可戳http://alibaba.github.io/dubbo-doc-static/Home-zh.htm。
三、如何使用dubbo
1.服務化應用基本框架
如上圖所示,一個抽象出來的基本框架,consumer和provider是框架中必然存在的,Registry做爲全局配置信息管理模塊,推薦生產環境使用Registry,可實時推送現存活的服務提供者,Monitor一般用於監控和統計RPC調用情況、成功率、失敗率等情況,讓開發及運維瞭解線上運行情況。
應用執行過程大致如下:
- 服務提供者啓動,根據協議信息綁定到配置的IP和端口上,如果已有服務綁定過相同IP和端口的則跳過
- 註冊服務信息至註冊中心
- 客戶端啓動,根據接口和協議信息訂閱註冊中心中註冊的服務,註冊中心將存活的服務地址通知到客戶端,當有服務信息變更時客戶端可以通過定時通知得到變更信息
- 在客戶端需要調用服務時,從內存中拿到上次通知的所有存活服務地址,根據路由信息和負載均衡機制選擇最終調用的服務地址,發起調用
- 通過filter分別在客戶端發送請求前和服務端接收請求後,通過異步記錄一些需要的信息傳遞到monitor做監控或者統計
2.服務接口定義
一般單獨有一個jar包,維護服務接口定義、RPC參數類型、RPC返回類型、接口異常、接口用到的常量,該jar包中不處理任何業務邏輯。
比如命名api-0.1.jar,在api-0.1.jar中定義接口
- public interface UserService
- {
- public RpcResponseDto isValidUser(RpcAccountRequestDto requestDto) throws new RpcBusinessException, RpcSystemException;
- }
public interface UserService
{
public RpcResponseDto isValidUser(RpcAccountRequestDto requestDto) throws new RpcBusinessException, RpcSystemException;
}
並在api-0.1.jar中定義RpcResponseDto,RpcAccountRequestDto,RpcBusinessException,RpcSystemException。
服務端通過引用該jar包實現接口並暴露服務,客戶端引用該jar包引用接口的代理實例。
3.註冊中心
開源的dubbo已支持4種組件作爲註冊中心,我們部門使用推薦的zookeeper做爲註冊中心,由於就瓶頸來說不會出現在註冊中心,風險較低,未做特別的研究或比較。
- zookeeper,推薦集羣中部署奇數個節點,由於zookeeper掛掉一半的機器集羣就不可用,所以部署4臺和3臺的集羣都是在掛掉2臺後集羣不可用
- redis
- multicast,廣播受到網絡結構的影響,一般本地不想搭註冊中心的話使用這種調用
- dubbo簡易註冊中心
對於zookeeper客戶端,dubbo在2.2.0之後默認使用zkclient,2.3.0之後提供可選配置Curator,提到這個點的原因主要是因爲zkclient發現一些問題:①服務器在修改服務器時間後zkClient會拋出日誌錯誤之類的異常然後容器(我們使用resin)掛掉了,也不能確定就是zkClient的問題,接入dubbo之前無該問題②dubbo使用zkclient不傳入連接zookeeper等待超時時間,使用默認的Integer.MAX_VALUE,這樣在zookeeper連不上的情況下不報錯也無法啓動;目前我們準備尋找其他解決方案,比如使用curator試下,還沒正式投入。
4.服務端
配置應用名
- <dubbo:application name=“test”/>
<dubbo:application name="test"/>
配置dubbo註解識別處理器,不指定包名的話會在spring bean中查找對應實例的類配置了dubbo註解的
- <dubbo:annotation/>
<dubbo:annotation/>
配置註冊中心,通過group指定註冊中心分組,可通過register配置是否註冊到該註冊中心以及subscribe配置是否從該註冊中心訂閱
- <dubbo:registry address=“zookeeper://127.0.0.1:2181/” group=“test”/>
<dubbo:registry address="zookeeper://127.0.0.1:2181/" group="test"/>
配置服務協議,多網卡可通過IP指定綁定的IP地址,不指定或者指定非法IP的情況下會綁定在0.0.0.0,使用Dubbo協議的服務會在初始化時建立長連接
- <dubbo:protocol name=“dubbo” port=“20880” accesslog=“d:/access.log”></dubbo:protocol>
<dubbo:protocol name="dubbo" port="20880" accesslog="d:/access.log"></dubbo:protocol>
通過xml配置文件配置服務暴露,首先要有個spring bean實例(無論是註解配置的還是配置文件配置的),在下面ref中指定bean實例ID,作爲服務實現類
- <dubbo:service interface=“com.web.foo.service.FirstDubboService” ref=“firstDubboServiceImpl” version=“1.0”></dubbo:service>
<dubbo:service interface="com.web.foo.service.FirstDubboService" ref="firstDubboServiceImpl" version="1.0"></dubbo:service>
通過註解方式配置服務暴露,Component是Spring bean註解,Service是dubbo的註解(不要和spring bean的service註解弄混),如前文所述,dubbo註解只會在spring bean中被識別- @Component
- @Service(version=“1.0”)
- public class FirstDubboServiceImpl implements FirstDubboService
- {
- @Override
- public void sayHello(TestDto test)
- {
- System.out.println(”Hello World!”);
- }
- }
@Component
@Service(version="1.0")
public class FirstDubboServiceImpl implements FirstDubboService
{
@Override
public void sayHello(TestDto test)
{
System.out.println("Hello World!");
}
}
5.客戶端
配置客戶端reference bean。客戶端跟服務端不同的是客戶端這邊沒有實際的實現類的,所以配置的dubbo:reference實際會生成一個spring bean實例,作爲代理處理Dubbo請求,然後其他要調用處直接使用spring bean的方式使用這個實例即可。
xml配置文件配置方式,id即爲spring bean的id,之後無論是在spring配置中使用ref=”firstDubboService”還是通過@Autowired註解都OK
- <dubbo:reference interface=“com.web.foo.service.FirstDubboService”
- version=“1.0” id=“firstDubboService” ></dubbo:reference>
<dubbo:reference interface="com.web.foo.service.FirstDubboService"
version="1.0" id="firstDubboService" ></dubbo:reference>
另外開發、測試環境可通過指定Url方式繞過註冊中心直連指定的服務地址,避免註冊中心中服務過多,啓動建立連接時間過長,如
- <dubbo:reference interface=“com.web.foo.service.FirstDubboService”
- version=“1.0” id=“firstDubboService” url=“dubbo://127.0.0.1:20880/”></dubbo:reference>
<dubbo:reference interface="com.web.foo.service.FirstDubboService"
version="1.0" id="firstDubboService" url="dubbo://127.0.0.1:20880/"></dubbo:reference>
註解配置方式引用,
- @Component
- public class Consumer
- {
- @Reference(version=“1.0”)
- private FirstDubboService service;
- public void test()
- {
- TestDto test = new TestDto();
- test.setList(Arrays.asList(new String[]{“a”, “b”}));
- test.setTest(”t”);
- service.sayHello(test);
- }
- }
@Component
public class Consumer
{
@Reference(version="1.0")
private FirstDubboService service;
public void test()
{
TestDto test = new TestDto();
test.setList(Arrays.asList(new String[]{"a", "b"}));
test.setTest("t");
service.sayHello(test);
}
}
Reference被識別的條件是spring bean實例對應的當前類中的field,如上是直接修飾spring bean當前類中的屬性
這個地方看了下源碼,本應該支持當前類和父類中的public set方法,但是看起來是個BUG,Dubbo處理reference處部分源碼如下
- Method[] methods = bean.getClass().getMethods();
- for (Method method : methods) {
- String name = method.getName();
- if (name.length() > 3 && name.startsWith(“set”)
- && method.getParameterTypes().length == 1
- && Modifier.isPublic(method.getModifiers())
- && ! Modifier.isStatic(method.getModifiers())) {
- try {
- Reference reference = method.getAnnotation(Reference.class);
- if (reference != null) {
- Object value = refer(reference, method.getParameterTypes()[0]);
- if (value != null) {
- method.invoke(bean, new Object[] { });//??這裏不是應該把value作爲參數調用麼,而且爲什麼上面if條件判斷參數爲1這裏不傳參數
- }
- }
- } catch (Throwable e) {
- logger.error(”Failed to init remote service reference at method ” + name + “ in class ” + bean.getClass().getName() + “, cause: ” + e.getMessage(), e);
- }
- }
- }
Method[] methods = bean.getClass().getMethods();
for (Method method : methods) {
String name = method.getName();
if (name.length() > 3 && name.startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())
&& ! Modifier.isStatic(method.getModifiers())) {
try {
Reference reference = method.getAnnotation(Reference.class);
if (reference != null) {
Object value = refer(reference, method.getParameterTypes()[0]);
if (value != null) {
method.invoke(bean, new Object[] { });//??這裏不是應該把value作爲參數調用麼,而且爲什麼上面if條件判斷參數爲1這裏不傳參數
}
}
} catch (Throwable e) {
logger.error("Failed to init remote service reference at method " + name + " in class " + bean.getClass().getName() + ", cause: " + e.getMessage(), e);
}
}
}
6.監控中心
- <dubbo:monitor protocol=“registry” /> <!–通過註冊中心獲取monitor地址後建立連接–>
<dubbo:monitor protocol="registry" /> <!--通過註冊中心獲取monitor地址後建立連接-->
- <dubbo:monitor address=“dubbo://127.0.0.1:7070/com.alibaba.dubbo.monitor.MonitorService” /> <!–繞過註冊中心直連monitor,同consumer直連–>
<dubbo:monitor address="dubbo://127.0.0.1:7070/com.alibaba.dubbo.monitor.MonitorService" /> <!--繞過註冊中心直連monitor,同consumer直連-->
7.服務路由
8.負載均衡
- Random,隨機,按權重配置隨機概率,調用量越大分佈越均勻,默認是這種方式
- RoundRobin,輪詢,按權重設置輪詢比例,如果存在比較慢的機器容易在這臺機器的請求阻塞較多
- LeastActive,最少活躍調用數,不支持權重,只能根據自動識別的活躍數分配,不能靈活調配
- ConsistentHash,一致性hash,對相同參數的請求路由到一個服務提供者上,如果有類似灰度發佈需求可採用
9.dubbo過濾器
- dubbo初始化過程加載META-INF/dubbo/internal/,META-INF/dubbo/,META-INF/services/三個路徑(classloaderresource)下面的com.alibaba.dubbo.rpc.Filter文件
-
文件配置每行Name=FullClassName,必須是實現Filter接口
-
@Activate標註擴展能被自動激活
-
@Activate如果group(provider|consumer)匹配才被加載
-
@Activate的value字段標明過濾條件,不寫則所有條件下都會被加載,寫了則只有dubbo URL中包含該參數名且參數值不爲空才被加載
http://alibaba.github.io/dubbo-doc-static/User+Guide-zh.htm#UserGuide-zh-%3Cdubbo%3Amonitor%2F%3E
可關注以上鍊接內容,dubbo提供較多的輔助功能特性,大多目前我們暫時未使用到,後續我們這邊關注到的兩個特性可能會再引進來使用:- 結果緩存,省得自己再去寫一個緩存,對緩存沒有特殊要求的話直接使用dubbo的好了
- 分組合並,對RPC接口不同的實現方式分別調用然後合併結果的一種調用模式,比如我們要查用戶是否合法,一種我們要查是否在黑名單,同時我們還要關注登錄信息是否異常,然後合併結果
四、前車之鑑
1.服務版本號
- 引用只會找相應版本的服務
- <dubbo:serviceinterface=“com.xxx.XxxService” ref=“xxxService” version=“1.0” />
- <dubbo:referenceid=“xxxService” interface=“com.xxx.XxxService” version=“1.0”/>
<dubbo:serviceinterface=“com.xxx.XxxService” ref=“xxxService” version=“1.0” />
<dubbo:referenceid=“xxxService” interface=“com.xxx.XxxService” version=“1.0”/>
- 爲了今後更換接口定義發佈在線時,可不停機發布,使用版本號
2.暴露一個內網一個外網IP問題
爲了在測試環境提供一個內網訪問的地址和一個辦公區訪問的地址。
- 服務不配置ip,綁定到0.0.0.0,自動獲取保證獲取到是內網IP註冊到註冊中心即可,如果不是想要的IP,可以在/etc/hosts中通過綁定Hostname指定IP
- 內網訪問方式通過註冊中心或者直連指定內網IP和端口
- 外網訪問方式通過直連指定外網IP和端口
3.dubbo reference註解問題
4.服務超時問題
- 客戶端耗時大,也就是超時異常時的client elapsed xxx,這個是從創建Future對象開始到使用channel發出請求的這段時間,中間沒有複雜操作,只要CPU沒問題基本不會出現大耗時,頂多1ms屬於正常
- IOThread繁忙,默認情況下,dubbo協議一個客戶端與一個服務提供者會建立一個共享長連接,如果某個客戶端處於特別繁忙而且一直往一個服務提供者塞請求,可能造成IOThread阻塞,一般非常特殊的情況纔會出現
- 服務端工作線程池中線程全部繁忙,接收消息後塞入隊列等待,如果等待時間比預想長會引起超時
- 網絡抖動,如果上述情況都排除了,還出現在請求發出後,服務接收請求前超過預想時間,只能歸類到網絡抖動了,需要SA一起查看問題
- 服務自身耗時大,這個需要應用自身做好耗時統計,當出現這種情況的時候需要用數據來說明問題及規劃優化方案,建議採用緩存埋點的方式統計服務中各個執行階段的耗時情況,最終如果超過預想時間則把緩存統計的耗時情況打日誌,減少日誌量,且能夠得到更明確的信息
5.服務保護
-
考慮服務的dubbo線程池類型(fix線程池的話考慮線程池大小)、數據庫連接池、dubbo連接數限制是否都合適
-
考慮服務超時時間和重試的關係,設置合適的值
-
一定時間內服務異常數較大,則可考慮使用failfast讓客戶端請求直接返回或者讓客戶端不再請求
6.zkclient的問題
7.註冊中心的分組group和服務的不同實現group
五、dubbo如何工作的
1.如何跟進源碼
2.服務提供者
- ServiceBean
- ProtocolFilterWrapper
- RegistryProtocol
- DubboProtocol
- DubboProtocol$ExchangeHandler
3.客戶端
- ReferenceBean
- InvokerInvocationHandler
- ProtocolFIlterWrapper
- RegistryProtocol
- DubboProtocol
- ClusterInvoker
- DubboInvoker