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阅读 124,602评论 18 136
  • 最近,接二连三遇到一些事,心里的确有点不痛快,憋住不说,心里难受;倾诉太多,感觉好累,也似乎于事无补,该如何调...
    小萍cyx阅读 90评论 0 2
  • 故园,太文艺,太诗化,太文气,或许属于诗人、作家、成功的人们。 对老百姓来说,故园就是老家,就是亲戚,就是泥土,就...
    艺术与评论阅读 86评论 0 1
  • 我每天的生活几乎固定不变,清早起床后,“骆驼”在我的床前,为我准备好当天要穿的衣物,附带的,还有一份预定的早饭。我...
    时间故事阅读 191评论 0 0
  • 文/阿正 01 距离上次写文已经过去好多天了,请原谅没有做到当初说好的日更一文。我要先给自己找一个能说的通的理由:...
    阿正扯谈阅读 174评论 2 1