前言
前段時間發布了一篇java服務中間件之旅(一):dubbo入門 , 在那之后有閱讀了曾憲杰大大的<<大型網站系統與java中間件實踐>>一書,并在公司項目中實際應用了dubbo,于是今天就打算對dubbo在實際項目中的應用做一個簡單的分享,歡迎大家拍磚評論。
今天的文章主要包括三個部分:
- dubbo核心技術簡介
- dubbo實際中常用配置
- dubbo應用過程中踩到的坑
一、dubbo核心技術簡介
上一篇博客中有提到dubbo的服務架構,在這里我先講一下遠程服務的調用流程,dubbo本質上就是解決了此問題。
遠程服務器調用流程
- 客戶端發起接口調用
- 服務中間件進行路由選址:找到具體接口實現的服務地址
- 客戶端將請求信息進行編碼(序列化: 方法名,接口名,參數,版本號等)
- 建立與服務端的通訊(不是調度中心,而是客戶端與服務端直連)
- 服務端將接收到的信息進行反編碼(反序列化)
- 根據信息找到服務端的接口實現類
- 將執行結果反饋給客戶端
針對上面的調用流程,結合dubbo的服務架構,是不是對dubbo的了解又深入了一些?同學們有空也可以根據上述的流程自己實現一遍簡單的遠程調用,下面為dubbo核心模塊用到的一些技術,可以提前做一些知識儲備。
核心技術
- java多線程
- JVM
- 網絡通訊(NIO)
- 動態代理
- 反射
- 序列化
- 路由節點管理(zookeeper)
二、dubbo實際中常用配置
第一篇博客中有個dubbo的demo,dubbo與spring集成得非常好,單純使用dubbo服務非常簡單,但是在實際操作中,著實有不少地方值得注意,下面我就列舉下常見的一些業務場景和dubbo配置。
最佳實踐(此章節摘自dubbo用戶手冊)
分包
建議將服務接口,服務模型,服務異常等均放在API包中,因為服務模型及異常也是API的一部分,同時,這樣做也符合分包原則:重用發布等價原則(REP),共同重用原則(CRP)
如果需要,也可以考慮在API包中放置一份spring的引用配置,這樣使用方,只需在Spring加載過程中引用此配置即可,配置建議放在模塊的包目錄下,以免沖突,如:com/alibaba/china/xxx/dubbo-reference.xml
粒度
服務接口盡可能大粒度,每個服務方法應代表一個功能,而不是某功能的一個步驟,否則將面臨分布式事務問題,Dubbo暫未提供分布式事務支持。
服務接口建議以業務場景為單位劃分,并對相近業務做抽象,防止接口數量爆炸
不建議使用過于抽象的通用接口,如:Map query(Map),這樣的接口沒有明確語義,會給后期維護帶來不便。
版本
每個接口都應定義版本號,為后續不兼容升級提供可能,如:<dubbo:service interface="com.xxx.XxxService" version="1.0" />
建議使用兩位版本號,因為第三位版本號通常表示兼容升級,只有不兼容時才需要變更服務版本。
當不兼容時,先升級一半提供者為新版本,再將消費者全部升為新版本,然后將剩下的一半提供者升為新版本。
兼容性
服務接口增加方法,或服務模型增加字段,可向后兼容,刪除方法或刪除字段,將不兼容,枚舉類型新增字段也不兼容,需通過變更版本號升級。
各協議的兼容性不同,參見: 服務協議
枚舉值
如果是完備集,可以用Enum,比如:ENABLE, DISABLE。
如果是業務種類,以后明顯會有類型增加,不建議用Enum,可以用String代替。
如果是在返回值中用了Enum,并新增了Enum值,建議先升級服務消費方,這樣服務提供方不會返回新值。
如果是在傳入參數中用了Enum,并新增了Enum值,建議先升級服務提供方,這樣服務消費方不會傳入新值。
序列化
服務參數及返回值建議使用POJO對象,即通過set,get方法表示屬性的對象。
服務參數及返回值不建議使用接口,因為數據模型抽象的意義不大,并且序列化需要接口實現類的元信息,并不能起到隱藏實現的意圖。
服務參數及返回值都必需是byValue的,而不能是byRef的,消費方和提供方的參數或返回值引用并不是同一個,只是值相同,Dubbo不支持引用遠程對象。
異常
建議使用異常匯報錯誤,而不是返回錯誤碼,異常信息能攜帶更多信息,以及語義更友好,
如果擔心性能問題,在必要時,可以通過override掉異常類的fillInStackTrace()方法為空方法,使其不拷貝棧信息,
查詢方法不建議拋出checked異常,否則調用方在查詢時將過多的try...catch,并且不能進行有效處理,
服務提供方不應將DAO或SQL等異常拋給消費方,應在服務實現中對消費方不關心的異常進行包裝,否則可能出現消費方無法反序列化相應異常。
調用
不要只是因為是Dubbo調用,而把調用Try-Catch起來。Try-Catch應該加上合適的回滾邊界上。
對于輸入參數的校驗邏輯在Provider端要有。如有性能上的考慮,服務實現者可以考慮在API包上加上服務Stub類來完成檢驗。
版本控制
在dubbo最佳實踐中有提到,所有接口都應定義版本,在這里有幾點需要注意下,接口服務如果更新頻繁,并且兼容老版本的,不建議更改版本號,因為dubbo這邊對除 * 以外的版本號,都是采用完全匹配的方式進行匹配。即服務端的版本號如果從1.0升級為1.1,并且未保留原有的1.0的服務,那么客戶端必須同時也將服務版本號升級為1.1,否則將無法匹配到遠處服務。
博主在自己項目中的版本使用規則如下,僅供參考:
- 版本號采用兩位,x.x 第一位表示需要非兼容升級,第二位表示兼容升級
- bug fix程度的升級不改版本號
- 版本升級的時候,保證老版本服務的繼續使用,同時部署新老版本,等客戶端全部升級完成后,再考慮下架老版本服務
- 版本可以細化配置到具體的接口 ,但是我們建議以通用配置來控制版本號
<dubbo:provider version="1.0"/>
<dubbo:consumer version="1.0"/>
對服務進行調優
dubbo的服務調用有很多默認配置,這些配置可能會引起服務調用業務上的錯誤,需要特別注意的有以下幾點:
- timeout,調用超時時間,默認為1000毫秒,即超過1000毫秒沒有返回數據,就會執行重試機制
- retries,失敗重試次數,默認為2,即失敗(超時)之后的重試次數
- connections,對每個提供者的最大鏈接數,默認為100,建議根據服務器配置進行調整
- loadbalance,負載均衡策略,默認為random
- async, 是否異步執行,默認為false
- delay, 延遲注冊服務時間,默認為0,建議不同的接口把暴露服務時間錯開,避免dubbo爆端口被占用錯誤(博主曾深受其害)
以上的幾點,如果服務端與客戶端都同時進行了配置,則客戶端優先級更高。
以下是根據我們服務器性能與業務需求的部分通用配置.
<!--服務端口自動分配-->
<dubbo:protocol name="dubbo" port="-1" />
<!-- 輪詢的機制,版本號為1.0,超時時間定為兩秒,不重試(避免出現業務上的錯誤),最大鏈接數配置為200,服務提供者lijian -->
<dubbo:provider loadbalance="roundrobin" version="1.0" timeout="2000" retries="0" connections="200" owner="lijian"/>
當某接口執行時間非常長的時候,常見的有三種方式去處理:
- 忽略返回值,配置return為true
<dubbo:service interface="com.lijian.dubbo.service.SlowService" ref="slowService" retries="1">
<dubbo:method name="slow" return="false"></dubbo:method>
</dubbo:service>
- 配置為異步
<dubbo:service interface="com.lijian.dubbo.service.SlowService" ref="slowService" retries="1">
<dubbo:method name="slow" async="true"></dubbo:method>
</dubbo:service>
- 配置為回調的方式
服務端配置:
<bean id="callbackService" class="com.lijian.dubbo.service.impl.CallbackServiceImpl" />
<dubbo:service interface="com.lijian.dubbo.service.CallbackService" ref="callbackService" connections="1" callbacks="1000">
<dubbo:method name="addListener">
<dubbo:argument type="com.lijian.dubbo.listener.MyListener" callback="true" />
</dubbo:method>
</dubbo:service>
接口實現:
package com.lijian.dubbo.service.impl;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.lijian.dubbo.listener.MyListener;
import com.lijian.dubbo.service.CallbackService;
public class CallbackServiceImpl implements CallbackService {
private final Map<String, MyListener> listeners = new ConcurrentHashMap<String, MyListener>();
public CallbackServiceImpl() {
Thread t = new Thread(new Runnable() {
public void run() {
while (true) {
try {
for (Map.Entry<String, MyListener> entry : listeners
.entrySet()) {
try {
entry.getValue().changed(
getChanged(entry.getKey()));
} catch (Throwable t) {
listeners.remove(entry.getKey());
}
}
Thread.sleep(5000); // 定時觸發變更通知
} catch (Throwable t) { // 防御容錯
t.printStackTrace();
}
}
}
});
t.setDaemon(true);
t.start();
}
public void addListener(String key, MyListener listener) {
listeners.put(key, listener);
listener.changed(getChanged(key)); // 發送變更通知
}
private String getChanged(String key) {
return "Changed: "
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
.format(new Date());
}
}
對參數進行校驗
使用spring的同學對@Validated 注解肯定不會感到陌生,可以對請求參數進行格式校驗:
controller代碼:
@RequestMapping(value = {""},method = RequestMethod.POST)
@ResponseBody
public GeneralResult addAdvertising(@Validated @RequestBody AdvertisingForm form){
return GeneralResult.newBuilder().setResult(advertisingService.addAdvertising(form));
}
AdvertisingForm部分代碼:
public class AdvertisingForm {
@NotEmpty(message = "標題不能為空")
private String title;
@NotEmpty(message = "照片不能為空")
private String photo;
...
}
在dubbo中,同樣可以使用validate功能進行格式校驗.
需要被校驗的User類:
package com.lijian.dubbo.beans;
import java.io.Serializable;
import javax.validation.constraints.Min;
import org.hibernate.validator.constraints.NotEmpty;
public class User implements Serializable{
private static final long serialVersionUID = 8332069385305414629L;
@NotEmpty(message="姓名不可為空")
private String name;
@Min(value=18,message="年齡必須大于18歲")
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
dubbo 的validate配置:
<bean id="validateService" class="com.lijian.dubbo.service.impl.ValidateServiceImpl" />
<dubbo:service interface="com.lijian.dubbo.service.ValidateService" ref="validateService" validation="true"/>
接口服務的調用如下:
package com.lijian.dubbo.consumer.main;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.lijian.dubbo.beans.User;
import com.lijian.dubbo.consumer.action.UserAction;
public class ValidateMainClass {
@SuppressWarnings("resource")
public static void main(String[] args){
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
context.start();
UserAction userAction = context.getBean(UserAction.class);
User user = new User();
// 如果年齡小于18,會報出 Caused by: javax.validation.ConstraintViolationException: Failed to validate service: com.lijian.dubbo.service.ValidateService, method: insert, cause: [ConstraintViolationImpl{interpolatedMessage='年齡必須大于18歲', propertyPath=age, rootBeanClass=class com.lijian.dubbo.beans.User, messageTemplate='年齡必須大于18歲'}]
// user.setAge(19);
user.setAge(17);
// 如果name為空,會報出 Caused by: javax.validation.ConstraintViolationException: Failed to validate service: com.lijian.dubbo.service.ValidateService, method: insert, cause: [ConstraintViolationImpl{interpolatedMessage='姓名不可為空', propertyPath=name, rootBeanClass=class com.lijian.dubbo.beans.User, messageTemplate='姓名不可為空'}]
user.setName("李健大帥哥");
System.out.println(userAction.addUser(user));
}
}
dubbo的常用實踐場景就介紹到這,還有更多的場景博主會在后續陸續更新~
三、dubbo應用過程中踩到的坑
雖然dubbo是個偉大的服務中間件開源框架,但博主確實在使用過程中踩了不少坑,在這里也分享下,避免同樣的問題走彎路。。
eclipse找不到dubbo的xsd文件
在配置dubbo服務的過程中,經常會遇到雖然程序能夠跑起來,但是配置文件一堆紅叉,雖然不影響功能,但是確實很讓人惡心。
報錯信息如下:
Multiple annotations found at this line: – cvc-complex-type.2.4.c: The matching wildcard is strict, but no declaration can be found for element ‘dubbo:application’. – schema_reference.4: Failed to read schema document ‘http://code.alibabatech.com/schema/dubbo/dubbo.xsd’, because 1) could not find the document; 2) the document could not be read; 3) the root element of the document is not <xsd:schema>.
廢話少說直接上解決方案: 下載一個dubbo.xsd文件windows->preferrence->xml->xmlcatalog add->catalog entry ->file system 選擇剛剛下載的文件路徑 修改key值和配置文件的http://code.alibabatech.com/schema/dubbo/dubbo.xsd 相同 保存。。在xml文件右鍵validate ok解決了。
我把文件傳到了demo的git項目上,同學們可以直接下載:
dubbo與spring、netty版本沖突
我們項目中的spring與netty版本都比dubbo依賴的版本要高,需要將老版本的jar包給移除掉。
pom.xml中對dubbo的引用 如下:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.3</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
</exclusion>
<exclusion>
<artifactId>netty</artifactId>
<groupId>org.jboss.netty</groupId>
</exclusion>
</exclusions>
</dependency>
端口20880被綁定
dubbo服務端默認占用的端口為20880(運行服務器上的端口),建議改為-1,讓dubbo自動分配未占用的端口。
在這里還有兩個非常坑的問題。
同一個容器中同時啟動多個服務,由于是同時注冊的服務,dubbo有時候會報出端口被占用的問題。解決方案,根據業務把服務注冊時間給delay,進行錯開
ipv6與ipv4占用同一個端口問題。。。
這個問題當初讓博主苦苦尋找了三個小時,還debug到socket源碼中。。。
問題場景是這樣的。博主啟動了兩個項目A(通過jboss)和B(通過main方法),注冊了不同的dubbo服務,無論是接口名,版本號,還是分組,兩個服務都沒有共同性,但是B的客戶端(消費者)的遠程調用還是進入了A的項目,博主一臉懵逼的情況下請教了網易和阿里的朋友,都表示不可思議,找不到答案(仍然感謝不愿意透露姓名的CY和XK同學熱情幫助與提供解決思路),最終通過lsof -i tcp:20880
指令看到了如下一幕:
仔細一看,type不一樣。。。
原來博主很早之前自己折騰過ipv6,通過jboss容器啟動的使用的是ipv4,而main方法啟動的使用的是ipv6。。。
解決方法,在jvm啟動參數中指定為ipv4:
-Djava.net.preferIPv4Stack=true
dubbo 注解配置的不足之處
用慣了spring的注解方式,在使用dubbo的時候自然也優先想用注解的形式進行配置,在跑demo的時候倒也沒出啥問題,但是隨著業務場景的復雜,發現注解的功能太過單薄。只能配置到接口層,無法細化到方法層。
比如一個接口下有三個方法A,B,C。A方法需要異步,B方法需要同步,并將超時時間設定為5秒,C方法需要使用回調,通過注解的方式就無法實現,還得老老實實地使用配置的方式,雖然麻煩,但功能強大
結尾
博主的java服務中間件之旅的中間章就到此為止了,在這篇文章中,主要以應用為主,簡單的介紹了一些dubbo原理。鑒于水平問題和實踐經驗問題,文中可能存在很多不足之處甚至是錯誤觀點,歡迎大家留言拍磚,來深入交流(來啊,互相傷害啊~~)。
demo代碼已經上傳至git: git@git.oschina.net:jianli/dubbo-demo.git 寫得比較隨意,是實踐的時候拿來練手的,僅作參考。。。
java服務中間件之旅(三):手寫自己的服務中間件 預計幾個月之后發布吧,最近公司事情實在太多,目前能力也不足,雖然有大概的思路,但還得做很多準備工作。
對分布式、java感興趣的同學可以關注我,也可以加我好友進行交流(聯系方式簡書個人資料中有)。