引入dubbo的实战记录

我们为什么要引入dubbo?

刚进入诺亚那会,java这边差不多十几个项目,其中包含前置系统,订单系统,支付系统,库存系统,基础数据服务系统等等,这些系统之间当时都是采用http请求或者jsonrpc的方式来调用(当然还有mq,后面开单独章节说明),我们有4套环境:一套开发环境,两套测试环境,一套生产环境,现在假设一个请求从php到前置系统,前置系统再调用订单系统,订单系统再调用支付系统,库存系统,基础数据服务系统(假设调用n个系统),那么在订单系统中就必须得配置支付系统,库存系统等的IP地址或者域名,然后还有4套环境,这样在订单系统中至少需要配置4 * n个url,随着项目的不停增加,服务器资源也会不停增加,这些url维护起来会越来越困难,经常碰到的是,项目上生产了,发现url配置错了。后来决心引入dubbo来解决这些问题 。

使用dubbo,引入了注册中心这个角色(我们当时选用的是zookeeper作为注册中心),动态的去注册和发现服务,不需要再通过配置url的方式来调用服务,并且通过http调用,都由nginx来做负载均衡,导致nginx服务器压力过大,使用dubbo,各应用之间的调用都由dubbo来提供负载均衡,减轻了nginx的压力。项目之间的启动也是有先后顺序的,随着系统越来越多,这种顺序也比较难维护,dubbo也提供了可视化的服务依赖关系。

引入dubbo步骤

1. 安装zookeeper(后面简称zk)

我们先安装zk。首先下载zookeeper-3.4.8.tar.gz,解压缩后,进入conf目录,重命名zoo_sample.cfg为zoo.cfg,如下命令:

cd zookeeper-3.4.8/conf
mv zoo_sample.cfg zoo.cfg

然后编辑zoo.cfg,修改dataDir项为:

dataDir=/var/lib/zookeeper(其他目录也可)

按照下面的格式来配置你的zk服务器节点(添加在zoo.cfg中):

server.id=host:port:port

其中id用来标示该机器在集群中的机器序号,同时,在每台zk机器上我们还需要在数据目录(即dataDir参数指定的那个目录)下创建一个myid文件该文件只有一行内容,并且是一个数字,就是server.id中的id(id的范围是1-255),具体操作如下:

vi myid

然后填入序号id,比如我们现在有3台zk服务器,分别为10.3.3.11,10.3.3.12,10.3.3.13 ,那么我们在zoo.cfg中会有如下配置:

server.1=10.3.3.11:2888:3888
server.2=10.3.3.12:2888:3888
server.3=10.3.3.13:2888:3888

假如当前我们所在的是10.3.3.13服务器,那么我们的myid文件中应该为:

3

按照上面的步骤,为其他机器都配置上zoo.cfg和myid文件。进入zk的bin目录下启动服务:

zkServer.sh start

验证服务是否启动成功:

telnet 127.0.0.1 2181

出现如下后,输入stat命令进行服务器启动的验证:

Trying 127.0.0.1...
Connected to localhost (127.0.0.1).
Escape character is '^]'.
stat
Zookeeper version: 3.4.8--1, built on 02/06/2016 03:18 GMT
Clients:
 /10.21.40.61:49209[1](queued=0,recved=13257,sent=13257)
 /10.4.86.19:52858[1](queued=0,recved=14427,sent=14739)
 /10.21.40.55:56897[1](queued=0,recved=716767,sent=717946)
 /10.21.40.61:36759[1](queued=0,recved=1249,sent=1257)
 /10.21.40.61:36379[1](queued=0,recved=14159,sent=14454)
 /10.4.86.100:54715[1](queued=0,recved=0,sent=0)
 /10.21.40.61:45895[1](queued=0,recved=235824,sent=235890)
 /10.4.86.79:54827[1](queued=0,recved=0,sent=0)
 /10.21.40.61:33516[1](queued=0,recved=2684,sent=2732)
 /10.21.40.55:46251[1](queued=0,recved=847196,sent=881416)
 /10.4.164.191:39687[1](queued=0,recved=1603,sent=1603)
 /10.21.40.61:51135[1](queued=0,recved=7123,sent=7235)
 /10.21.40.55:42206[0](queued=0,recved=1,sent=0)
 /10.4.164.191:40234[1](queued=0,recved=281,sent=281)
 /10.4.86.45:55863[1](queued=0,recved=0,sent=0)
 /10.21.40.61:60827[1](queued=0,recved=13439,sent=13687)
 /10.21.40.55:56216[1](queued=0,recved=598291,sent=598292)
 /10.21.40.61:55145[1](queued=0,recved=729,sent=729)
 /10.21.40.61:34212[1](queued=0,recved=14031,sent=14056)

Latency min/avg/max: 0/0/1247
Received: 14681449
Sent: 14763271
Connections: 19
Outstanding: 0
Zxid: 0x6cafa
Mode: standalone
Node count: 362
Connection closed by foreign host.

出现如上面的信息,就说明服务正常启动了。

2. 再来下载运行dubbo

首先我们去github上面down下来dubbo或者dubbox的代码,这里我们选用dubbox。找到dubbo-admin模块下面的dubbo.properties配置文件,其中有3项如下:

dubbo.registry.address=10.3.3.11:2181,10.3.3.12:2181,10.3.3.13:2181
dubbo.admin.root.password=root
dubbo.admin.guest.password=guest

其中第一项,即是注册中心的地址,第二项和第三项分别是管理界面两个账户的密码(用户名和密码相同),这两项我们不做修改,仅将注册中心修改为我们自己的地址即可,然后用maven打包运行起来即可。至此应用之间的中间件已经运行起来,开始提供服务。

3. 应用中服务提供者和服务消费者的配置

先来看服务提供者配置,首先引入需要的jar包:


    com.github.sgroschupf
    zkclient
    0.1



    org.apache.zookeeper
    zookeeper
    3.4.6



com.alibaba
dubbo
2.8.4

    
        org.springframework
        spring
    


在spring-dubbo.xml(名字随便取)中配置如下:

















    
    
    
    





服务消费方所需jar包和服务提供方一样,配置如下:










    
    

至此,消费方和提供方配置完成。当然,这里只是dubbo非常基本的一些配置项,更多的配置项可以去官网dubbo.io上面查阅,由于以上配置是我做测试的时候用的,所以一些注释和命名略显不太规范
接下来我们看下服务提供方的接口和实现类,先看接口类:

package com.dubbox.provider.api;

import com.dubbox.provider.api.exception.StockException;

/**
 * @author 吴镇
 * @description:服务提供方接口
 * @createdate 2016/8/12
 */
public interface ProviderService {
    String providerSomething1();

    String providerSomething2() throws StockException;
}

再来看实现:

package com.dubbox.provider.service;

import com.dubbox.provider.api.ProviderService;
import com.dubbox.provider.api.exception.ResponseCode;
import com.dubbox.provider.api.exception.StockException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

/**
 * @author 吴镇
 * @description:服务提供方实现
 * @createdate 2016/8/12
 */
@Service(value = "providerServiceImpl")
public class ProviderServiceImpl implements ProviderService {

    private static final Logger LOGGER = LoggerFactory.getLogger(ProviderServiceImpl.class);
    public String providerSomething1(){
        return "I am a service provider,prvider 1~~";
    }

    public String providerSomething2() throws StockException{
        throw new StockException(ResponseCode.COMMUNICATION_ERROR);
    }

}

这里提供了两个方法来做测试,其中providerSomething1没什么好说的,在providerSomething2中比较特殊的是我们抛出了一个异常,StockException是我自定义的一个异常类,不去深究它,由于dubbo在方法抛出异常的时候需要做一些特殊的处理,这里先提一下,待会我们在原理讲解中再来说。其中的接口类ProviderService我们是将它放在一个单独的模块中(maven项目分模块),这里我们叫dubbox-api。

再来看服务消费方如何调用提供方提供的服务,首先,我们需要将服务提供方的dubbox-api模块打成jar包,如果是maven项目的话先进入dubbox-api目录下,然后使用以下命令来打包:

mvn clean
mvn compile
mvn package

以上命令执行后,我们可以看到target目录下生成一个dubbox-api-1.1.jar,如果公司有私服的话,将这个包上传到私服中:

mvn deploy

然后在服务消费方引入这个jar包:


    com.ifa.noah
    web-api
    1.1

现在我们可以像调用本地方法一样来调用服务提供方的方法了,服务消费方代码如下:

package com.dubbox.service;

import com.dubbox.provider.api.ProviderService;
import com.dubbox.provider.api.exception.StockException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * @author 吴镇
 * @description:服务消费方
 * @createdate 2016/8/12
 */
@Service
public class TestService {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestService.class);

    @Autowired
    private ProviderService provider;

    public String getDateFromDubbo(){
        String str = "";
        try{
            str = provider.ProviderSomething2();
            System.out.println(str);
        }catch (Exception e){
            if(e instanceof StockException){
                StockException stockException = (StockException) e;
                LOGGER.info("调用服务异常,异常码为:{},异常信息为:{}", stockException.getResponseCode().getCode(),stockException.getResponseCode().getMsg());
            }
        }
        return str;
    }
}

注意到,我们直接像注入本地的bean一样来注入提供方提供的服务。我们运行单元测试来调用getDateFromDubbo方法即可。到这里,我们的一个dubbo中间件的例子就完成了。

dubbo原理

1. dubbo服务注册发现的实现

a. dubbo是如何将配置(上文中的spring-dubbo.xml文件)转变为spring上下文中的bean的

首先,dubbo定义了名称空间:dubbo,查看源码我们在META-INF找到了如下文件:dubbo.xsd,spring.handlers,spring.schemas。其中dubbo.xsd是名称空间定义文件,spring.handlers指定了dubbo名称空间节点解析器,spring.schemas配置告诉名称空间xsd文件在哪。dubbo所有的配置项均在名称空间中,我们看下spring.schemas中的内容:

--指定了dubbo.xsd文件的位置
http\://code.alibabatech.com/schema/dubbo/dubbo.xsd=META-INF/dubbo.xsd

再来看spring.handlers中的内容:

http\://code.alibabatech.com/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler

指定了名称空间解析器为DubboNamespaceHandler类,我们看下DubboNamespaceHandler类的内容:

package com.alibaba.dubbo.config.spring.schema;

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

import com.alibaba.dubbo.common.Version;
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ConsumerConfig;
import com.alibaba.dubbo.config.ModuleConfig;
import com.alibaba.dubbo.config.MonitorConfig;
import com.alibaba.dubbo.config.ProtocolConfig;
import com.alibaba.dubbo.config.ProviderConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.spring.AnnotationBean;
import com.alibaba.dubbo.config.spring.ReferenceBean;
import com.alibaba.dubbo.config.spring.ServiceBean;

/**
 * DubboNamespaceHandler
 * 
 * @author william.liangf
 * @export
 */
public class DubboNamespaceHandler extends NamespaceHandlerSupport {

    static {
        Version.checkDuplicate(DubboNamespaceHandler.class);
    }

    public void init() {
        registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
        registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
        registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
        registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
        registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
        registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
        registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
        registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
        registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
        registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
    }

}

我们看到这个类继承自spring的NamespaceHandlerSupport类,在子类中调用registerBeanDefinitionParser方法, 注册解析器, 如registerBeanDefinitionParser("service",new DubboBeanDefinitionParser(ServiceBean.class,true)),spring会将对应的配置文件(上文中的spring-dubbo.xml文件)转换成bean,重点来看ServiceBean和ReferenceBean,从ServiceBean的继承关系和实现接口来看:

public class ServiceBean extends ServiceConfig implements InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware
  • 继承自ServiceConfig将得到配置属性和暴露服务等相关方法。
  • 实现spring的InitializingBean接口,spring容器在初始化完成后,将会调用afterPropertiesSet方法。
  • 实现spring的DisposableBean接口,spring容器在销毁时,会调用destroy方法。
  • 实现spring的ApplicationContextAware接口,实现setApplicationContext方法,spring会通过这个方法给这个bean注入ApplicationContext,在serviceBean中通过ApplicationContext拿到了很多的bean。
  • 实现了ApplicationListener接口,会监听spring容器生命周期事件onApplicationEvent,serviceBean监听其ContextRefreshedEvent事件,在spring容器初始化完成之后,如果服务为暴露(export)再暴露一下
  • 实现了spring的BeanNameAware接口,将bean name设置为bean id

从ServiceBean的afterPropertiesSet的逻辑可以看出,在读取配置到ServiceConfig后(将配置转为实体), 在上下文中,根据ServiceConfig配置属性找到对应的bean注入,完了调用ServiceConfig的export()方法暴露服务,export方法中做了一些属性的初始化,并判断是否是延迟加载,如果延迟加载,则新起一个线程来进行暴露服务,在这个新的线程中sleep delay时间再start

if (delay != null && delay > 0) {
    Thread thread = new Thread(new Runnable() {
        public void run() {
            try {
                Thread.sleep(delay);
            } catch (Throwable e) {
            }
            doExport();
        }
    });
    thread.setDaemon(true);
    thread.setName("DelayExportServiceThread");
    thread.start();
} else {
    doExport();
}

接着做了一些属性的初始化,重点再来看doExportUrls(),开始则获取所有注册中心地址,然后获取协议,如果没有配置任何协议的话,默认协议是dubbo协议:

String name = protocolConfig.getName();
if (name == null || name.length() == 0) {
    name = "dubbo";
}

其中在配置协议的时候,我们一般是这样配置的:


这里面没有配置host属性,如果没有配置的话,dubbo会默认去获取暴露服务的主机IP,如果还是拿不到host,则获取注册中心地址发起socket连接,然后通过socket.getLocalAddress().getHostAddress()得到主机地址,如果还是获取不到host,则通过遍历本地网卡获取一个host,代码如下:

if (NetUtils.isInvalidLocalHost(host)) {
    anyhost = true;
    try {
        host = InetAddress.getLocalHost().getHostAddress();
    } catch (UnknownHostException e) {
        logger.warn(e.getMessage(), e);
    }
    if (NetUtils.isInvalidLocalHost(host)) {
        if (registryURLs != null && registryURLs.size() > 0) {
            for (URL registryURL : registryURLs) {
                try {
                    Socket socket = new Socket();
                    try {
                        SocketAddress addr = new InetSocketAddress(registryURL.getHost(), registryURL.getPort());
                        socket.connect(addr, 1000);
                        host = socket.getLocalAddress().getHostAddress();
                        break;
                    } finally {
                        try {
                            socket.close();
                        } catch (Throwable e) {}
                    }
                } catch (Exception e) {
                    logger.warn(e.getMessage(), e);
                }
            }
        }
        if (NetUtils.isInvalidLocalHost(host)) {
            host = NetUtils.getLocalHost();
        }
    }
}

从网卡获取host的代码不深究,这里我们不配置host,让dubbo自动获取,然后获取暴露服务的端口。后面再来获取method级别的配置,判断service节点下面是否有method标签,这个method是针对方法级别的配置,配置如下:


    
     
    
    

针对方法级别的配置我们生产上没有使用,这里说明下,比如对于timeout属性来说,如果针对接口级别的设置时40s,但是针对方法级别的设置时20s,那么接口调用超时时长限制应该是20s。接着做了一些属性设置的判断,拼接URL,根据不同的协议(protocol)来暴露服务,比如我们选用了默认的dubbo协议,那么就是使用DubboProtocol来暴露,底层使用Netty框架来进行远程连接通信(Netty是一个nio框架,更加方便的使用socket),最终往zookeeper上面注册一个节点,这个节点是什么类型的节点,节点路径是什么样的,请看下面zookeeper原理。

以上就是暴露服务的过程,总结一下,利用spring框架将dubbo配置转换为内存中的bean,初始化协议,host,regist url等属性,找到注册中心,找到对应的protocol来暴露服务。其中有一点不得不提的是dubbo是插件化的,其中利用到了JDK提供的SPI协议,SPI规定在META-INF下面来定义具体的服务实现,比如dubbo可供选择的协议有:dubbo,http,rmi,hession等等,在META-INF下面定义的文件中以key-value的形式来描述服务:

registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol

这些实现类都会实现一个带有@SPI注解的接口,在dubbo启动的时候会去META-INF下面加载这些服务的实现类(这些加载方式是框架来实现的,而不是JDK来规定的),在暴露服务的过程中根据客户端配置文件中定义的不同协议来选择不同的服务实现类来暴露服务。

b.dubbo的线程模型

provide端提供的是一个默认200大小的线程池。代码如下

public class FixedThreadPool implements ThreadPool {

    public Executor getExecutor(URL url) {
        String name = url.getParameter(Constants.THREAD_NAME_KEY, Constants.DEFAULT_THREAD_NAME);
        int threads = url.getParameter(Constants.THREADS_KEY, Constants.DEFAULT_THREADS);
        int queues = url.getParameter(Constants.QUEUES_KEY, Constants.DEFAULT_QUEUES);
        return new ThreadPoolExecutor(threads, threads, 0, TimeUnit.MILLISECONDS, 
                queues == 0 ? new SynchronousQueue() : 
                    (queues < 0 ? new LinkedBlockingQueue() 
                            : new LinkedBlockingQueue(queues)),
                new NamedThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }

}

可以看到corePoolSize和maximumPoolSize的大小是一样的大的,且keepAliveTime时长是0,所以不会扩展。其workQueue默认使用SynchronousQueue,根据配置queues的参数不同来选择一个默认大小的LinkedBlockingQueue或者一个queues大小的LinkedBlockingQueue(各种Queue),参数threadFactory为dubbo生成线程的工厂,其策略是使用一个线程安全的AtomicInteger来生成线程标示号,再拼接上一个前缀,所以,我们在日志中看见的dubbo的线程号就是这个样子的:

DubboServerHandler-10.21.40.60:20890-thread-143

最后一个参数是当任务超出线程池(maximumPoolSize+Queue)容量时的处理策略,查看AbortPolicyWithReport源码如下:

public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
    
    protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);
    
    private final String threadName;
    
    private final URL url;
    
    public AbortPolicyWithReport(String threadName, URL url) {
        this.threadName = threadName;
        this.url = url;
    }
    
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        String msg = String.format("Thread pool is EXHAUSTED!" +
                " Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
                " Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!" ,
                threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
                e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
                url.getProtocol(), url.getIp(), url.getPort());
        logger.warn(msg);
        throw new RejectedExecutionException(msg);
    }

}

可以看到,拒绝策略很简单,打个日志,抛个异常。有幸我们在生产环境中碰到了这个异常,当时情况是这样的,前端一个请求调用我们的系统,我们又http去调用了PHP的一个接口,结果php的接口请求花了近100秒,导致dubbo线程一直没有释放,当时访问量比较高,线程池很快就满了,然后就看见了这个异常。后来的解决方案是:1,加大dubbo线程池大小;2,减小dubbo timeout的时长;3,减小http timeout的时长

zookeeper原理

ZAB协议

ZAB全称zookeeper原子消息广播协议
所有事务请求必须由一个唯一的服务器来协调处理,这样的服务器被称为leader服务器,而余下其他服务器则成为follower服务器(以上过程为master选举过程),leader服务器负责将一个客户端事务请求转换成一个事务Proposal(提议),并将该提议分发给集群中所有follower服务器。之后leader服务器需要等待所有follower服务器的反馈,一旦超过半数以上的follower服务器进行了正确的反馈后,那么leader就会再次向所有follower服务器分发Commit消息,要求其将前一个提议进行提交。(以上就是为什么建议zk集群为奇数的原因,比如对于5台服务器和6台,其实都只支持宕机两台,那还不如只要5台服务器呢)

dubbo对注册中心进行了抽象封装,所以底层我们选择zk或者redis都行。而zk的这种类似unix文件系统结构的目录服务,且支持变更的推送,因此非常适合作为dubbo的注册中心。
项目启动时,dubbo将配置中需要生产和消费的服务的类的包路径作为zk的文件路径。假设我们现在有一个服务:com.foo.BarService,服务启动的时候,会首先在zk的/dubbo/com.foo.BarService/providers节点下面创建一个子节点,并写入自己的URL地址,这就代表了这个额服务的一个提供者。
消费者在启动的时候,读取并订阅zk上/dubbo/com.foo.BarService/providers节点下面的所有子节点,并解析出所有提供者的URL地址来作为该服务地址列表然后发起正常的调用。同时还会在/dubbo/com.foo.BarService/consumers节点下创建一个临时节点,并写入自己的URL地址,这句代表“com.foo.BarService”的一个消费者。

需要注意的是,所有提供者在zk上面创捷的节点都是临时节点(zk上面有永久节点,临时节点,顺序节点,永久顺序阶段,临时顺序节点这几种节点类型),一旦某一个服务提供者或者消费者节点挂掉了,该节点就会自动从zk上被删除(通过心跳来检测是否挂掉)。

你可能感兴趣的:(引入dubbo的实战记录)