java服務中間件之旅(二):dubbo實戰

男神們鎮樓

前言

前段時間發布了一篇java服務中間件之旅(一):dubbo入門 , 在那之后有閱讀了曾憲杰大大的<<大型網站系統與java中間件實踐>>一書,并在公司項目中實際應用了dubbo,于是今天就打算對dubbo在實際項目中的應用做一個簡單的分享,歡迎大家拍磚評論。

今天的文章主要包括三個部分:

  • dubbo核心技術簡介
  • dubbo實際中常用配置
  • dubbo應用過程中踩到的坑

一、dubbo核心技術簡介

上一篇博客中有提到dubbo的服務架構,在這里我先講一下遠程服務的調用流程,dubbo本質上就是解決了此問題。

遠程服務器調用流程

  1. 客戶端發起接口調用
  2. 服務中間件進行路由選址:找到具體接口實現的服務地址
  3. 客戶端將請求信息進行編碼(序列化: 方法名,接口名,參數,版本號等)
  4. 建立與服務端的通訊(不是調度中心,而是客戶端與服務端直連)
  5. 服務端將接收到的信息進行反編碼(反序列化)
  6. 根據信息找到服務端的接口實現類
  7. 將執行結果反饋給客戶端

針對上面的調用流程,結合dubbo的服務架構,是不是對dubbo的了解又深入了一些?同學們有空也可以根據上述的流程自己實現一遍簡單的遠程調用,下面為dubbo核心模塊用到的一些技術,可以提前做一些知識儲備。

核心技術

  1. java多線程
  2. JVM
  3. 網絡通訊(NIO)
  4. 動態代理
  5. 反射
  6. 序列化
  7. 路由節點管理(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項目上,同學們可以直接下載:

Paste_Image.png

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自動分配未占用的端口。

在這里還有兩個非常坑的問題。

  1. 同一個容器中同時啟動多個服務,由于是同時注冊的服務,dubbo有時候會報出端口被占用的問題。解決方案,根據業務把服務注冊時間給delay,進行錯開

  2. ipv6與ipv4占用同一個端口問題。。。
    這個問題當初讓博主苦苦尋找了三個小時,還debug到socket源碼中。。。
    問題場景是這樣的。博主啟動了兩個項目A(通過jboss)和B(通過main方法),注冊了不同的dubbo服務,無論是接口名,版本號,還是分組,兩個服務都沒有共同性,但是B的客戶端(消費者)的遠程調用還是進入了A的項目,博主一臉懵逼的情況下請教了網易和阿里的朋友,都表示不可思議,找不到答案(仍然感謝不愿意透露姓名的CY和XK同學熱情幫助與提供解決思路),最終通過lsof -i tcp:20880指令看到了如下一幕:

20880端口占用情況
如圖所示,20880端口被兩個進程同時占用

仔細一看,type不一樣。。。
原來博主很早之前自己折騰過ipv6,通過jboss容器啟動的使用的是ipv4,而main方法啟動的使用的是ipv6。。。
解決方法,在jvm啟動參數中指定為ipv4:

-Djava.net.preferIPv4Stack=true

dubbo 注解配置的不足之處

用慣了spring的注解方式,在使用dubbo的時候自然也優先想用注解的形式進行配置,在跑demo的時候倒也沒出啥問題,但是隨著業務場景的復雜,發現注解的功能太過單薄。只能配置到接口層,無法細化到方法層。


dubbo源碼中注解只有兩個

比如一個接口下有三個方法A,B,C。A方法需要異步,B方法需要同步,并將超時時間設定為5秒,C方法需要使用回調,通過注解的方式就無法實現,還得老老實實地使用配置的方式,雖然麻煩,但功能強大

結尾

博主的java服務中間件之旅的中間章就到此為止了,在這篇文章中,主要以應用為主,簡單的介紹了一些dubbo原理。鑒于水平問題和實踐經驗問題,文中可能存在很多不足之處甚至是錯誤觀點,歡迎大家留言拍磚,來深入交流(來啊,互相傷害啊~~)。

demo代碼已經上傳至git: git@git.oschina.net:jianli/dubbo-demo.git 寫得比較隨意,是實踐的時候拿來練手的,僅作參考。。。

java服務中間件之旅(三):手寫自己的服務中間件 預計幾個月之后發布吧,最近公司事情實在太多,目前能力也不足,雖然有大概的思路,但還得做很多準備工作。

對分布式、java感興趣的同學可以關注我,也可以加我好友進行交流(聯系方式簡書個人資料中有)。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,837評論 18 139
  • 最近,接二連三遇到一些事,心里的確有點不痛快,憋住不說,心里難受;傾訴太多,感覺好累,也似乎于事無補,該如何調...
    小萍cyx閱讀 208評論 0 2
  • 故園,太文藝,太詩化,太文氣,或許屬于詩人、作家、成功的人們。 對老百姓來說,故園就是老家,就是親戚,就是泥土,就...
    藝術與評論閱讀 255評論 0 1
  • 我每天的生活幾乎固定不變,清早起床后,“駱駝”在我的床前,為我準備好當天要穿的衣物,附帶的,還有一份預定的早飯。我...
    時間故事閱讀 370評論 0 0
  • 文/阿正 01 距離上次寫文已經過去好多天了,請原諒沒有做到當初說好的日更一文。我要先給自己找一個能說的通的理由:...
    阿正扯談閱讀 361評論 2 1