使用 Dubbo 搭建一个简单的分布式系统

前言


随着阿里巴巴开源的高性能分布式 RPC 框架 Dubbo 正式进入 Apache 孵化器,Dubbo 又火了一把。本文作为 Dubbo 系列开端,先教大家使用 Dubbo 搭建一个简单的分布式系统,因为要研究一个东西的原理,必须先能把环境搭建起来,并且会使用它。


在这个系统里面会包含服务提供者,服务消费者,服务注册中心(本 Chat 使用 ZooKeeper),管理控制台(Dubbo-Admin),监控平台(Dubbo-Monitor),麻雀虽小,却五脏俱全。


通过本 Chat 你将能学到(文章内有 Demo 源码):

  • 五大组件的关系。

  • 如何基于 Spring 配置搭建一个简单的分布式系统。

  • 如何基于 Dubbo API 搭建一个简单的分布式系统。

  • 何为服务端异步调用,如何使用异步调用,使用异步调用好处是什么。

  • 何为泛化调用,如何使用泛化调用,什么时候使用泛化调用。


五大组件关系


  • 服务提供方在启动时候会注册自己提供的服务到服务注册中心。

  • 服务消费方在启动时候会去服务注册中心订阅自己需要的服务,然后服务注册中心异步把消费方需要的服务接口的提供者的地址列表返回给服务消费方,服务消费方根据路由规则和设置的负载均衡算法选择一个服务提供者 ip 进行调用。

  • 监控平台主要用来统计服务的调用次数和调用耗时,服务消费者和提供者,在内存中累计调用次数和调用耗时,并定时每分钟发送一次统计数据到监控中心,监控中心则使用数据绘制图表来显示,监控平台不是分布式系统必须的,但是这些数据有助于系统运维和调优。服务提供者和消费者可以直接配置监控平台的地址,也可以通过服务注册中心来获取。

  • 管理控制台主要提供路由规则,动态配置,服务降级,访问控制,权重调整,负载均衡,等管理功能。管理控制台直接与服务注册中心打交道,从服务注册中心获取所有注册的服务和消费方法;并且可以通过管理台界面设置服务消费端的路由规则,动态配置等信息并注册到服务管理台,这些信息会被通知到服务消费端。管理控制台也不是分布式系统必备的组件,但是有了他我们可以对服务进行很好的治理和监控。

服务注册中心的搭建


本文我们讲解 Apache ZooKeeper 的搭建。

  • 首先你需要到 http://zookeeper.apache.org/releases.html 下载一个 zk 的包,本文作者使用的是 zookeeper-3.4.11 这个版本,如下图:

解压该包后,如下图:

  • 然后修改 zookeeper-3.4.11/conf 文件夹里面的 zoo.cfg 文件。

    • 设置配置项 dataDir 为一个存在的以 data 结尾的目录;

    • 设置 zk 的监听端口 clientPort=2181;

    • 设置 zk 心跳检查间隔 tickTime = 2000;

    • 设置 Follower 服务器启动时候从 Leader 同步完毕数据能忍受多少个心跳时间间隔数 initLimit=5。

    • 设置运行过程中 Leader 同步数据到 Follower 后,Follower 回复信息到 Leader 的超时时间 syncLimit=2,如果 Leader 超过 syncLimit 个 tickTime 的时间长度,还没有收到 Follower 响应,那么就认为这个 Follower 已经不在线了:

  • 最后在 zookeeper-3.4.11/bin 下运行 sh zkServer.sh start-foreground 就会启动 zk,会有下面输出:


可知 zk 在端口 2181 进行监听,至此服务注册中心搭建完毕。


服务提供方与服务消费方搭建


本文 Demo 结构介绍


首先讲解下本文使用的 Demo 的结构,Demo 使用 Maven 聚合功能,里面有三个模块,目录如下:


  • 其中 Consumer 模块为服务消费者,里面 TestConsumer 和 consumer.xml 组成了基于 Spring 配置方式的服务调用,TestConsumerApi 是基于 Dubbo API 方式的服务调用,TestConsumerApiGeneric 是泛化方式的服务调用,TestConsumerAsync 是异步调用的方式。

  • 其中 Provider 模块为服务提供者,里面 TestProvider 和 provider.xml 组成了基于 Spring 配置方式的服务提供,TestProviderApi 是基于 Dubbo API 的服务提供,UserServiceImpl 为服务实现类。

  • 其中 SDK 模块是一个二方包,用来存放服务提供者所有的接口,是为了代码复用使用,在服务提供者和消费者的模块里面都需要引入这个二方包。


其中 SDK 里面的接口定义源码如下:

public interface UserServiceBo {    String sayHello(String name);  
    String sayHello2(String name); 
    String testPojo(Person person);

}

在 SDK 模块执行 mvn clean install 命令会安装该模块的 Jar 到本地仓库,要想在其他模块引入该 Jar,必须要先执行这个安装步骤。


基于 Spring 配置的服务提供方与消费方搭建


基于 Spring 配置的服务提供方搭建


Provider 模块为服务提供者,作用是注册提供的服务到 zk,并使用 Netty 服务监听服务消费端的链接。里面 TestProvider 和 provider.xml 组成了基于 XML 方式的服务提供,UserServiceImpl 为服务实现类。


  • 首先需要在 Provider 模块里面引入 SDK 模块,因为 Provider 模块需要用到 UserServiceBo 接口(需要在 SDK 模块执行 mvn clean install 命令会安装该模块的 Jar 到本地仓库)。

  • 然后实现 UserServiceBo 接口为 UserServiceImpl,代码如下:

public class UserServiceImpl implements UserServiceBo{    @Override
    public String sayHello(String name) {        //让当前当前线程休眠2s
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }        return name; 
    }    @Override
    public String sayHello2(String name) {        //让当前当前线程休眠2s
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }        return name;
    }        @Override
    public String testPojo(Person person) {        return JSON.toJSONString(person);
    }  
}
  • 然后 provider.xml 的内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.0.xsd 
    http://code.alibabatech.com/schema/dubbo
    http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <!-- 提供方应用信息,用于计算依赖关系 -->
    <dubbo:application name="dubboProvider" />

    <!-- 使用zookeeper注册中心暴露服务地址 -->
    <dubbo:registry address="zookeeper://127.0.0.1:2181" />

    <!-- 用dubbo协议在20880端口暴露服务 -->
    <dubbo:protocol name="dubbo" port="20880" />
    <!-- 启用monitor模块 -->
    <dubbo:monitor protocol="registry" />

    <bean id="userService" class="com.test.UserServiceImpl" />

    <!-- 声明需要暴露的服务接口 -->
    <dubbo:service interface="com.test.UserServiceBo" ref="userService"
        group="dubbo"  version="1.0.0" timeout="3000"/>

</beans>
  • 然后日志文件 log4j.properties 内容如下:

log4j.rootLogger=INFO,A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
  • 然后,编写服务发布测试类 TestProvider,代码如下:

public class TestProvider {   

    public static void main(String[] arg) throws InterruptedException {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:provider.xml");        //挂起当前线程,如果没有改行代码,服务提供者进程会消亡,服务消费者就发现不了提供者了
        Thread.currentThread().join();
    }
}
  • 最后,运行 TestProvider 类,输出如下:

说明当前服务已经注册了 ZooKeeper 了。


4.3 基于 Dubbo API 方式的服务提供方与消费方搭建



 Consumer 模块为服务消费方,服务消费端主要是从 zk 获取自己需要的服务提供者的 ip 列表,然后根据路由规则选择一个 ip 进行远程调用。里面 TestConsumer 和 consumer.xml 组成了基于 XML 方式的服务调用。

  • 首先需要在 Consumer 模块里面引入 SDK 模块,因为 Consumer 模块需要用到 UserServiceBo 接口(泛化调用时候不需要这个步骤)。

  • 然后 consumer.xml 内容如下:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    
     xmlns:context="http://www.springframework.org/schema/context"
     xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.0.xsd 
    http://code.alibabatech.com/schema/dubbo
    http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

  <!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->    
    <dubbo:application name="dubboConsumer" />  

      <!-- 使用multicast广播注册中心暴露发现服务地址 -->    
    <dubbo:registry  protocol="zookeeper" address="zookeeper://127.0.0.1:2181" />  
       <!-- 启动monitor-->
    <dubbo:monitor protocol="registry" />        
      <!-- 生成远程服务代理,可以和本地bean一样使用demoService -->    
    <dubbo:reference id="userService" interface="com.test.UserServiceBo" group="dubbo" version="1.0.0" timeout="3000"/>    

</beans>
  • 然后测试服务消费类 TestConsumer 代码如下:

public class TestConsumer {    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(                new String[] { "classpath:consumer.xml" });        final UserServiceBo demoService = (UserServiceBo) context.getBean("userService");

        System.out.println(demoService.sayHello("哈哈哈"));
    }
}
  • 最后运行 TestConsumer,会输出:

说明服务消费端已经正常调用了服务提供方的服务了。

注:至此一个经典的含有服务提供者,服务消费者,服务注册中心的简单分布式系统搭建完毕了。


基于 Dubbo API 方式的服务提供方与消费方搭建



基于Dubbo API 方式的服务提供方搭建

其中 Provider 模块为服务提供者,里面 TestProviderApi 是基于 Dubbo API 的服务提供,UserServiceImpl 为服务实现类。

  • 首先需要在 Provider 模块里面引入 SDK 模块,这个不变。

  • 然后实现 UserServiceBo 接口为 UserServiceImpl,这个也不变。

  • 然后编写 Dubbo API 服务提供测试代码 TestProviderApi 代码如下:

public class TestProviderApi {    public static void main(String[] arg) throws InterruptedException {        //(4.3.1-1)等价于  <bean id="userService" class="com.test.UserServiceImpl" />
        UserServiceBo userService = new UserServiceImpl();        //(4.3.1-2)等价于  <dubbo:application name="dubboProvider" />
        ApplicationConfig application = new ApplicationConfig();
        application.setName("dubboProvider");        //(4.3.1-3)等价于  <dubbo:registry address="zookeeper://127.0.0.1:2181" />
        RegistryConfig registry = new RegistryConfig();
        registry.setAddress("127.0.0.1:2181");
        registry.setProtocol("zookeeper");        // (4.3.1-4)等价于 <dubbo:protocol name="dubbo" port="20880" />
        ProtocolConfig protocol = new ProtocolConfig();
        protocol.setName("dubbo");
        protocol.setPort(20880);        //4.3.1-5)等价于     <dubbo:monitor protocol="registry" />
        MonitorConfig monitorConfig = new MonitorConfig();
        monitorConfig.setProtocol("registry");        //4.3.1-6)等价于 <dubbo:service interface="com.test.UserServiceBo" ref="userService"
        //group="dubbo"  version="1.0.0" timeout="3000"/>
        ServiceConfig<UserServiceBo> service = new ServiceConfig<UserServiceBo>(); // 此实例很重,封装了与注册中心的连接,请自行缓存,否则可能造成内存和连接泄漏
        service.setApplication(application);
        service.setMonitor(monitorConfig);
        service.setRegistry(registry); // 多个注册中心可以用setRegistries()
        service.setProtocol(protocol); // 多个协议可以用setProtocols()
        service.setInterface(UserServiceBo.class);
        service.setRef(userService);
        service.setVersion("1.0.0");
        service.setGroup("dubbo");
        service.setTimeout(3000);
        service.export();       //4.3.1-8) 挂起当前线程
        Thread.currentThread().join();
    }
}

基于Dubbo API 方式的服务消费方搭建

其中 Consumer 模块为服务消费者,里面 TestConsumerApi 是基于 Dubbo API 方式的服务调用。

  • 首先需要在 Consumer 模块里面引入 SDK 模块,这个不变。

  • 编写基于 Dubbo API 消费服务的测试类 TestConsumerApi 代码如下:

public class TestConsumerApi {    public static void main(String[] args) throws InterruptedException {        // 等价于  <dubbo:application name="dubboConsumer" />  
        ApplicationConfig application = new ApplicationConfig();
        application.setName("dubboConsumer");        // 等价于     <dubbo:registry  protocol="zookeeper" address="zookeeper://127.0.0.1:2181" />  
        RegistryConfig registry = new RegistryConfig();
        registry.setAddress("127.0.0.1:2181");
        registry.setProtocol("zookeeper");        //等价于   <dubbo:monitor protocol="registry" />
        MonitorConfig monitorConfig = new MonitorConfig();
        monitorConfig.setProtocol("registry");        //等价于<dubbo:reference id="userService" interface="com.test.UserServiceBo"
        //group="dubbo" version="1.0.0" timeout="3000" />
        ReferenceConfig<UserServiceBo> reference = new ReferenceConfig<UserServiceBo>(); // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
        reference.setApplication(application);
        reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
        reference.setInterface(UserServiceBo.class);
        reference.setVersion("1.0.0");
        reference.setGroup("dubbo");
        reference.setTimeout(3000);
        reference.setInjvm(false);
        reference.setMonitor(monitorConfig);

        UserServiceBo userService = reference.get(); 
        System.out.println(userService.sayHello("哈哈哈"));
        Thread.currentThread().join();

    }
}


服务消费端泛化调用



前面我们讲解基于 Spring 和基于 Dubbo API 方式搭建一个简单的分布式系统时候服务消费端是引入了一个 SDK 二方包的,里面存放了服务提供端提供的所有接口类,之所以需要引入接口类是因为服务消费端一般是基于接口使用 JDK 代理实现远程调用的。

泛化接口调用方式主要用于服务消费端没有 API 接口类及模型类元(比如入参和出参的 POJO 类)的情况下使用;这时候参数及返回值中由于没有对应的 POJO 类,所以所有 POJO 均转换为 Map 表示。使用泛化调用时候服务消费模块不在需要引入 SDK 二方包。

下面基于 Dubbo API 来实现异步调用,在 Consumer 模块里面 TestConsumerApiGeneric 是泛化调用的方式,代码如下:

public class TestConsumerApiGeneric {    public static void main(String[] args) throws IOException {        // 当前应用配置
        ApplicationConfig application = new ApplicationConfig();
        application.setName("dubboConsumer");        // 连接注册中心配置
        RegistryConfig registry = new RegistryConfig();
        registry.setAddress("127.0.0.1:2181");
        registry.setProtocol("zookeeper");        // 泛型参数设置为GenericService
        ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>(); 
        reference.setApplication(application);
        reference.setRegistry(registry); 
        reference.setVersion("1.0.0");
        reference.setGroup("dubbo");
        reference.setTimeout(3000);        //设置为泛化
        reference.setInterface("com.test.UserServiceBo");
        reference.setGeneric(true);        //用com.alibaba.dubbo.rpc.service.GenericService替代所有接口引用
        GenericService userService = reference.get(); // 

        // 基本类型以及Date,List,Map等不需要转换,直接调用,如果返回值为POJO也将自动转成Map
        Object result = userService.$invoke("sayHello", new String[] {"java.lang.String"},        new Object[] {"哈哈哈"});

        System.out.println(JSON.json(result));//POJO参数转换为mapMap<String, Object> map = new HashMap<String, Object>();
        map.put("class", "com.test.PersonImpl");
        map.put("name", "jiaduo");
        map.put("password", "password");

        result = userService.$invoke("testPojo", new String[] { "com.test.Person" }, new Object[] { map });
        System.out.println((result));
    }
}

这里由于 sayHello 的参数是 String,没有很好的体现参数转换为 Map,下面我们具体来说下 POJO 参数转换 Map 的含义。

比如服务提供者提供的一个接口的 testPojo(Person person) 方法的参数为如下 POJO:

package com.test;public class PersonImpl implements Person {private String name;private String password;public String getName() {return name;
}public void setName(String name) {this.name = name;
}public String getPassword() {return password;
}public void setPassword(String password) {this.password = password;
}
}

则 POJO 数据:

Person person = new PersonImpl();
person.setName("jiaduo");
person.setPassword("password");

正常情况下调用接口是使用:

servicePerson.testPojo(person);

泛化调用下需要首先转换 person 为 Map 后如下表示:

Map<String, Object> map = new HashMap<String, Object>();// 注意:如果参数类型是接口,或者List等丢失泛型,可通过class属性指定类型。map.put("class", "com.test.PersonImpl");
map.put("name", "jiaduo");
map.put("password", "password");

然后使用下面方法进行泛化调用:

 servicePerson.$invoke("testPojo", new String[]
{"com.test.Person"}, new Object[]{map});

泛化调用通常用于框架集成,比如:实现一个通用的服务测试框架,可通过 GenericService 调用所有服务实现,而不需要依赖服务实现方提供的接口类以及接口的入参和出参的 POJO 类。


 服务消费端异步调用



无论前面我们讲解的正常调用还是泛化调用也好,都是进行同步调用的,也就是服务消费方发起一个远程调用后,调用线程要被阻塞挂起,直到服务提供方返回。

本节来讲解下服务消费端异步调用,异步调用是指服务消费方发起一个远程调用后,不等服务提供方返回结果,调用方法就返回了,也就是当前线程不会被阻塞,这就允许调用方同时调用多个远程方法。

猜你喜欢

转载自blog.csdn.net/u010682330/article/details/79914342