前言
前段时间发布了一篇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感兴趣的同学可以关注我,也可以加我好友进行交流(联系方式简书个人资料中有)。