概述

随着SOA生态的不断完善以及微服务架构思想的落地,服务与服务之间的远程通信需求更多来自服务的解耦。同时,业务规模的不断增长会使得微服务数量增加,那么问题也就随之产生了,比如:

  1. 如何协调线上运行的服务,以及保障服务的高可用性。
  2. 如何根据不同服务的访问情况来合理地调控服务器资源,提高机器的利用率。
  3. 线上出现故障时,如何动态地对故障业务做降级、流量控制等。
  4. 如何动态地更新服务中的配置信息,比如限流阈值、降级开关等。
  5. 如何实现大规模集群所带来的服务地址的管理和服务上下线的动态感知。
    为了解决这些问题,就需要一个统一的服务治理框架对服务进行统一、有效的管控,从而保障服务的高效、健康运行,而Dubbo就是一个这样的框架。
    Dubbo是阿里巴巴内部使用的一个分布式服务治理框架。
    2019年5月,Apache Dubbo 正式从孵化器中毕业,代表着Apache Dubbo正式成为Apache的顶级项目。

如何理解Apache Dubbo

Apache Dubbo是一个分布式服务框架,主要实现多个系统之间的高性能、透明化调用,简单来说它就是一个RPC框架,但是和普通的RPC框架不同的是,它提供了服务治理功能,比如服务注册、监控、路由、容错等。

Apache Dubbo架构图能够清晰的看出,除了基本的RPC框架的职能,它的核心功能便是监控和服务注册。

sofa 微服务治理 微服务治理方案_zookeeper

Apache Dubbo实现远程通信

  1. 生产者和消费者分别添加dubbo-spring-boot-starter 场景启动器。
  2. 新建接口和接口实现类。
  3. 生产者使用apache dubbo的@Service注解接口实现类,启动类添加@DubboComponentScan注解(用来生产dubbo bean)。
  4. 消费者使用apache dubbo的@Reference注解获取一个远程代理对象。

快速上手ZooKeeper

概述

ZooKeeper是一个高性能的分布式协调中间件,所谓的分布式协调中间件的作用类似于多线程环境下通过并发工具包来协调线程的访问控制,只是分布式协调中间件主要解决分布式环境中各个服务进程的访问控制问题,比如顺序控制(分布式锁)。
所以zookeeper不是注册中心,只是基于zookeeper本身的特性可以实现注册中心这个场景而已。

zookeeper的数据结构

  • zookeeper的数据模型和分布式文件系统类似,是一种层次化的属性结构,与文件系统不同的是,zookeeper的数据是结构化存储的,并没有在物理上体现出文件和目录。
    zookeeper的数据结构如下:
  • zookeeper树中的每个节点被称为Znode,Znode维护了一个stat状态信息,其中包含数据变化的时间和版本等。并且每个znode可以设置一个value值,zookeeper并不用于通用的数据库或者大量的对象存储,它只是管理和协调有关的数据,所有value的数据大小不建议设置的非常大,较大的数据会带来更大的网络开销。
    znode节点的stat数据:
[zk: localhost:2181(CONNECTED) 14] stat /locks/lock1
cZxid = 0x3
ctime = Sun Apr 18 10:42:23 CST 2021
mZxid = 0x4
mtime = Sun Apr 18 11:10:43 CST 2021
pZxid = 0x3
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0
  • zookeeper上的每个节点的数据都是允许读和写的,读表示获得指定znode上的value数据,写表示修改指定Znode上的value数据。另外节点的创建规则和文件系统中文件的创建规则类似,必须要按照层级创建(也就是不可跨级创建,必须先有父才能有子,不能一下创建多个层级)。举个简单的例子:
    如果要创建 /locks/lock1,那么必须先创建/locks这个层级节点。

ZooKeeper的特性

zookeeper中的znode在被创建的时候,需要指定节点的类型,节点类型分为:

  1. 持久化节点,节点的数据会持久化到磁盘。
  2. 临时节点,节点的生命周期和创建该节点的客户端的生命周期保持一致,一旦该客户端的会话结束,则该客户端所创建的临时节点会被自动删除。
  3. 有序节点,在创建的节点后面添加一个递增的序列节点,该序列中的节点在同一级父节点下是唯一的。需要注意的是,持久化节点或者临时节点都可以设置为有序节点,也就是持久化有序节点或者临时有序节点。
    在3.5.3版本之后,又增加了两种节点类型:
  4. 容器节点,当容器节点下的最后一个子节点被删除时,容器节点就会被自动删除。
  5. TTL节点,针对持久化节点或者持久化有序节点,我们可以设置一个存活时间,如果存活时间之内该节点没有任何修改并且没有任何子节点,它就会被自动删除。

需要注意的是,在同一层级目录下,节点的名称必须是唯一的,就像我们在同一个目录下不能创建两个有相同名字的文件一样。

Watcher机制

zookeeper提供了一种针对znode的订阅/通知机制,也就是当znode节点状态发生变化时或者zookeeper客户端连接状态发生变化时,会触发事件通知。这个机制在服务注册与发现中,针对服务调用者及时感知服务提供者的变化提供了非常好的解决方案。
在zookeeper提供的java api中,提供了三种机制来针对znode进行注册监听,分别是:

  • getData():用于获取指定节点的value信息,并且可以注册监听,当监听的节点进行创建、修改、删除操作是,会触发相应的事件通知。
  • getChildren():用于获取指定节点的所有子节点,并且允许注册监听,当监听节点的子节点进行创建、修改、删除操作时,触发相应的事件通知。
  • exists():用于判断指定节点是否存在,同样可以注册针对指定节点的监听,监听的事件类型和getData()相同。

注意:watcher事件的触发是一次性的(也就是读清除的),比如客户端通过getData(’/node’,true)注册监听,如果/node节点发生数据修改,那么该客户就会收到一个修改通知,但是/node再次发生变化时,客户端无法收到Watcher事件,为了解决这个问题,客户端必须在收到的事件回调中再次注册事件。

常见应用场景分析

分布式锁

锁从范围级别来说主要分成两种,线程级别的和进程级别的。
比如Synchronize或者Lock,它们主要用于解决多线程环境下(单机部署)共享资源的数据安全性问题,但是它们所处理的范围是线程级别的。
在分布式架构中多个进程对同一个共享资源进行访问时,也存在数据安全性问题,因此也需要使用锁的形式来解决这类问题,而解决分布式环境下多进程对于共享资源访问带来的安全性问题的方案就是使用分布式锁。锁的本质是排他的,也就是避免同一时刻多个进程同时访问某一个共享资源。
如果使用zookeeper实现分布式锁达到排他的目的,只需要用到节点的特性:临时节点及同级节点的唯一性

  • 获取锁的过程:
    在获得排他锁时,所有客户端可以去zookeeper服务器上 /Exclusive_Locks 节点下创建一个临时节点 /lock。ZooKeeper基于同级节点的唯一性,会保证所有客户端中只有一个客户端能创建成功,创建成功的客户端获得了排他锁,没有获得锁的客户端就需要通过Watcher机制监听 /Exclusive_Locks 节点下子节点的变更事件,用于实时监听 /lock 节点的变化情况以做出反应。
  • 释放锁的过程:
    在获得锁的过程中,我们定义的锁节点 /lock 为临时节点,那么在以下两种情况下会触发锁的释放。
  1. 获得锁的客户端因为异常断开了和服务端的连接,基于临时节点的特性,/lock 节点会被自动删除。
  2. 获得锁的客户端执行完业务逻辑后,主动删除了创建的 /lock 节点。

当 /lock 节点被删除之后,zookeeper服务器会通知所要监听了 /Exclusive_Locks 子节点变化的客户端。这些客户端受到通知后,再次发起创建 /lock 节点的操作来获得排他锁。

Master选举

Master选举是分布式系统中非常常见的场景,在分布式架构中,为了保证服务的可用性,通常会采用集群模式,也就是当其中一个机器宕机后,集群中的其他节点会接替节点继续工作。在这种场景中,就需要从集群中选举一个节点作为Master节点,剩余的节点都作为备份节点随时待命。当原有的Master节点出现故障之后,还需要从集群中的其他备份节点选举一个节点作为Master节点继续提供服务。
zookeeper就可以帮助集群中的节点实现Master选举。具体而言,zookeeper中有两种方式来实现Master选举这一场景:

  • 利用临时节点、同级节点唯一性、watcher机制(exist()监听是否存活事件)。
    同一级节点不能重复创建一个已经存在的节点,这个有点类似分布式锁的实现场景,其实Master选举的场景也是如此。假设集群中有3个节点,需要选举出Master,那么这三个节点会同时去zookeeper服务器上创建一个临时节点 /master-election,由于节点的特性,只会有一个客户端创建成功,创建成功的客户端所在的机器就成了Master。同时,其他没有创建成功的客户端,针对该节点注册Watcher事件,用于监控当前的Master机器是否存活,一旦发现Master “挂了”,也就是 /master-election节点被删除了,那么其他的客户端将会重新发现Master选举操作。
  • 利用临时有序节点的特性+watcher机制(后一个监听前一个,而不是fllower都监听master,应该是为了避免事件广播带来的网络开销,同时也减少了选举复杂度)。
    利用临时有序节点的特性来实现。所有参与选举的客户端在zookeeper服务器的 /master 节点下创建一个临时有序节点,编号最小的节点表示Master,后续的节点可以监听前一个节点的删除事件,用于触发重新选举。
    如下图所示:client01,client02,client03三个节点去zookeeper server的 /master 节点下创建临时有序节点,编号最小的节点client01表示Master节点,client02和client03会分别通过watcher机制监听比自己编号小的一个节点,比如client03会监听client01-000000001节点的删除事件、client02会监听client03-000000002节点的删除事件,一旦最小的节点被删除,client-03就会被选举为Master。

Apache Dubbo 集成ZooKeeper实现服务注册

概述

  • 大规模服务化后,在远程RPC调用的过程中,会遇到两个比较尖锐的问题:
  1. 服务动态上下线感知。
  2. 负载均衡。
  • 服务动态上下线感知。
    就是服务调用者要感知到服务提供者上下线的变化。
    对于服务提供者是一个集群的场景,一旦服务提供者的IP故障或者集群中某个节点下线了,服务调用者需要同步更新这些地址,但是这个操作如果人工来做是不现实的,所以需要一个第三方软件来统一管理服务提供者的URL地址,服务调用者可以从这个软件中获得目标服务的相关地址,并且第三方软件需要动态感知服务提供者状态的变化来维护管理的URL,进而使得服务调用者能够及时感知到变化而做出相应的处理。
  • 负载均衡
    当服务提供者是由多个节点组成的集群环境时,服务调用者需要通过负载均衡算法来动态选择一台目标服务器进行远程通信。负载均衡的主要目的是通过多个节点的集群来均衡服务器的访问压力,提升整体性能。实现负载均衡的前提是,要得到目标服务集群的所有地址,在服务调用者端进行计算,而地址的获取也同样依赖于第三方软件。
第三方软件的主要功能其实就是服务注册与发现,如下图,可以看到引人服务注册中心后服务调用者服务提供者之间的访问变化。Apache Dubbo 支持多种注册中心,比如zookeeper、Nacos、Redis等。在开源版本中,官方推荐使用的注册中心是zookeeper,所以使用Apache Dubbo的功能大部分都使用zookeeper来实现服务注册与发现。

sofa 微服务治理 微服务治理方案_sofa 微服务治理_02

ZooKeeper注册中心的实现原理

sofa 微服务治理 微服务治理方案_zookeeper_03


Dubbo服务注册到ZooKeeper上之后,可以在zookeeper服务器上看到上图所示的树形数据结构。

  • 当Dubbo服务启动时,会去zookeeper服务商的/dubbo/com.practice.dubbo.IHelloService/providers目录下创建当前服务的URL,其中com.practice.dubbo.IHelloService是发布服务的接口的全路径名称,providers表示服务提供者的类型,dubbo://ip:port表示该服务发布的协议类型及访问地址。其中,URL是临时节点,其他皆为持久化节点。使用临时节点的好处在于,如果该节点的服务器下线了,那边这个服务器的URL地址就会从ZooKeeper服务器上被移除。
  • 当Dubbo服务消费者启动时,会对/dubbo/com.practice.dubbo.IHelloService 节点下的子节点注册Watcher监听,这样便可以感知服务提供方节点的上下线变化,从而防止请求发送到已经下线的服务器造成访问失败。同时,服务消费者会在/dubbo/com.practice.dubbo.IHelloService/consumers 下写入自己的URL,这样做的目的是可以在监控平台上看到某个Dubbo服务正在被哪些服务调用。最重要的是,Dubbo服务的消费者如果需要调用IHelloService服务,那边它会先去/dubbo/com.pratcie.dubbo.IHelloService/providers 路径下获得所有该服务的提供方URL列表,然后通过负载均衡算法计算出一个地址进行远程访问。
  • 整体来看,服务注册和动态感知的功能用到了ZooKeeper中临时节点、持久化节点、Watcher等,回过头看前面分析的ZooKeeper的应用场景发现,几乎所有的场景都是基于这些来完成的。另外,不得不提的是,Dubbo还可以针对不同的情况实现以下功能:
  1. 基于临时节点的特性,当服务提供者宕机或者下线时,注册中心会自动删除该服务提供者的信息。
  2. 注册中心重启时,Dubbo能够自动恢复注册数据及订阅请求。
  3. 为了保证节点操作的安全性,ZooKeeper提供了ACL(access limit)权限控制,在Dubbo中可以通过dubbo.registry.username/dubbo.registry.password设置节点的验证信息。
  4. 注册中心默认的根节点是 /dubbo,如果需要针对不同环境设置不同的根节点,可以使用dubbo.registry.group修改根节点名称。

实战Dubbo Spring Cloud

概述

Dubbo Spring Cloud是Spring Cloud Alibaba的核心组件,它构建在原生的Spring Cloud标准之上,不仅覆盖了Spring Cloud原生特性,还提供了更加稳定和成熟的实现。

实现Dubbo服务提供方

创建一个普通的Maven工程,并在该工程下创建两个模块:sampleapi和samplecloudprovider。其中sampleapi是一个普通的Maven工程,samplecloudprovider是一个Spring boot工程。

  • 在sampleapi中声明接口,并执行mvn install将jar安装到本地(如果其他工程打包无法依赖到sampleapi,删除jar同级目录中的_remote.repositories文件,表示重本地仓库获取)。
public interface IHelloService {
    String sayHello(String name);
}
  • 在samplecloudprovider中添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.practice.springcloud.dubbo</groupId>
    <artifactId>samplecloudprovider</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>samplecloudprovider</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
    </properties>
    <!--各种jar包版本仲裁管理-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.11.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.1.1.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-dubbo</artifactId>
        </dependency>
        <dependency>
            <groupId>com.practice.springboot</groupId>
            <artifactId>sampleapi</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <!--解决Jar中清单文件(MANIFEST.MF)没有设置启动类(Start-Class:)的问题-->
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                    <mainClass>com.practice.springcloud.dubbo.samplecloudprovider.SamplecloudproviderApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

依赖说明如下:

  1. spring-cloud-starter:Spring Cloud 核心包。
  2. spring-cloud-starter-dubbo:引人Spring Cloud Alibaba Dubbo。
  3. spring-cloud-starter-zookeeper-discovery:基于zookeeper实现服务注册发现的artifactId。
  • 在samplecloudprovider中创建接口的实现类HelloServiceImpl,其中@Service是Dubbo服务的注解,表示当前服务会发布一个远程服务。
@Service
public class HelloServiceImpl implements IHelloService {

    @Value("${spring.application.name}")
    private String serviceName;
    @Override
    public String sayHello(String name) {
        return String.format("[%s]: Hello, %s", serviceName, name);
    }
}
  • 在application.properties中配置dubbo相关的信息。
dubbo.protocol.port=20880
dubbo.protocol.name=dubbo

spring.application.name=spring-cloud-dubbo-provider
# 表示服务是否注册到注册中心
spring.cloud.zookeeper.discovery.register=true
# 表示连接zookeeper服务器的地址
spring.cloud.zookeeper.connect-string=localhost:2181
  • 在启动类中声明@DubboComponentScan注解,启动服务
@DubboComponentScan
@SpringBootApplication
public class SamplecloudproviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(SamplecloudproviderApplication.class, args);
    }

}

@DubboComponentScan扫描当前注解所在的包及其子包路径下的@org.apache.dubbo.config.annotation.Service注解,实现服务的发布。发布完成后,就可以在zookeeper服务器上看到一个 /services/${project-name} 节点,这个节点保存了服务提供方相关的地址信息。

[zk: localhost:2181(CONNECTED) 1] ls -R /services
/services
/services/spring-cloud-dubbo-provider
/services/spring-cloud-dubbo-provider/e86f28c5-1109-4351-a9a5-3d1eb1483019
[zk: localhost:2181(CONNECTED) 2] get /services/spring-cloud-dubbo-provider/e86f28c5-1109-4351-a9a5-3d1eb1483019
{"name":"spring-cloud-dubbo-provider","id":"e86f28c5-1109-4351-a9a5-3d1eb1483019","address":"192.168.174.128","port":20880,"sslPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"application-1","name":"spring-cloud-dubbo-provider","metadata":{"dubbo.metadata-service.urls":"[ \"dubbo://192.168.174.128:20880/com.alibaba.cloud.dubbo.service.DubboMetadataService?anyhost=true&application=spring-cloud-dubbo-provider&bind.ip=192.168.174.128&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&group=spring-cloud-dubbo-provider&interface=com.alibaba.cloud.dubbo.service.DubboMetadataService&methods=getAllServiceKeys,getServiceRestMetadata,getExportedURLs,getAllExportedURLs&pid=13713&qos.enable=false®ister=true&release=2.7.3&revision=2.1.1.RELEASE&side=provider×tamp=1618841633590&version=1.0.0\" ]","dubbo.protocols.dubbo.port":"20880"}},"registrationTimeUTC":1618841630936,"serviceType":"DYNAMIC","uriSpec":{"parts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":":","variable":false},{"value":"port","variable":true}]}}

实现Dubbo服务调用方

创建一个名为springcloudconsumer的spring boot项目,就可以实现dubbo服务调用了。

  • 添加如下依赖,为了演示需要,增加了spring-boot-starter-web组件,表示这是一个web项目。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.practice.springcloud</groupId>
    <artifactId>springcloudconsumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springcloudconsumer</name>
    <description>springcloudconsumer</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-dubbo</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
            <version>2.1.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.practice.springboot</groupId>
            <artifactId>sampleapi</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                    <mainClass>com.practice.springcloud.consumer.ConsumerApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  • 在application.propeties文件中添加Dubbo相关的配置信息。
dubbo.cloud.subscribed-services=spring-cloud-dubbo-provider

server.port=8081
spring.application.name=spring-cloud-dubbo-consumer
# 表示当前服务不需要注册到zookeeper服务器上,默认为true
spring.cloud.zookeeper.discovery.register=false
spring.cloud.zookeeper.connect-string=localhost:2181
  • 创建HelloController类,暴露一个 /say 服务,来消费Dubbo服务提供者的IHelloService服务。
@RestController
public class HelloController {
    @Reference
    private IHelloService helloService;

    @GetMapping("/say")
    public String sayHello() {
        return helloService.sayHello("Test Dubbo Function");
    }
}
  • 启动Spring Boot服务
@SpringBootApplication
public class ConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}
  • 测试结果
[root@localhost ~]$ curl http://127.0.0.1:8081/say
[spring-cloud-dubbo-provider]: Hello, Test Dubbo Function

Apache Dubbo的高级应用

概述

Apache Dubbo更像一个生态,它提供了很多比较主流框架的集成,比如:

  • 支持多种协议的服务发布,默认是dubbo://,还可以支持rest://、webservice://、thrift://等。
  • 支持多种不同的注册中心,如Nacos、ZooKeeper、Redis,未来还将会支持Consul、Eureka、Etcd等。
  • 支持多种序列化技术,如avro、fst、fastjson、hessians2、kryo等。
    除此之外,Apache Dubbo在服务治理方面的功能非常完善,比如集群容错、服务路由、负载均衡、服务降级、服务限流、服务监控、安全验证等。

集群容错

在分布式架构的网络通信中,容错能力是必须要具备的。
什么是容错呢?从字面来看,就是服务容忍错误的能力。我们都知道网络通信中会存在很多不确定的因素导致请求失败,比如网络延迟、网络中断、服务异常等。当服务调用者(消费者)调用服务提供者的接口时,如果因为上述原因出现请求失败,那对于服务调用者来说,需要一种机制来应对。Dubbo 中提供了集群容错的机制来优雅的处理这种错误。

容错模式

Dubbo默认提供了6种容错模式,默认为Failover Cluster。
如果这6中模式不能满足你的实际需求,还可以自行扩展。这也是Dubbo的强大之处,几乎所有的功能都提供了插拔式的扩展。

  • Failover Cluster(故障转移集群):失败自动切换。当服务调用失败后,会切换到集群中的其他机器进行重试,默认重试次数为2,通过属性retries=2可以修改次数,但是重试次数增加会带来更长的响应延迟。这种容错模式通常用于读操作,因为事务型操作会带来数据重复问题。
  • Failfast Cluster:快速失败。当服务调用失败后,立即报错,也就是发起一次调用。通常用于一些幂等的写操作,比如新增数据,因为当服务调用失败时,很可能这个请求已经在服务端处理成功,只是因为网络延迟导致响应失败,为了避免在结果不确定的情况下导致数据重复插入的问题,可以使用这种容错机制。
  • Failsafe Cluster:失败安全。也就是出现异常是,直接忽略异常。
  • Failback Cluster:失败后自动回复。服务调用出现异常时,在后台记录这条失败的请求定时重发。这种模式适合用于消息通知操作,保证这个请求一定发送成功。
  • Forking Cluster:并行调用。并行调用集群中的多个服务,只要其中一个成功就返回。可以通过forks=2来设置最大并行数。
  • Broadcast Cluster:广播调用。广播调用所有的服务提供者,任意一个服务报错则表示服务调用失败。这种机制通常用于通知所有的服务提供者更新缓存或者本地资源信息。

配置方式

配置非常简单,只需要在指定服务的@Service注解上增加一个参数即可。在@Service注解中增加cluster="failfast"参数,表示当前服务的容错方式是快速失败。

@Service(cluster = "failfast")
public class HelloServiceImpl implements IHelloService {
    @Value("${spring.application.name}")
    private String serviceName;

    @Override
    public String sayHello(String name) {
        return String.format("[%s]: Hello, %s", serviceName, name);
    }
}

在实际应用中,查询语句容错策略建议使用默认的Failover Cluster,而增删改操作建议使用Failfast Cluster或者使用Failover Cluster(retries=0)策略,防止出现数据重复添加等其他问题。
建议在设计接口的时候把查询接口方法单独做出一个接口提供查询。

负载均衡

  • 负载均衡是指在访问量较大的情况下,我们会通过水平扩容的方式增加多个节点来平衡请求的流量,从而提升服务的整体性能。简单来说,如果一个服务节点的TPS是100,那么如果增加到5个节点的集群,意味着整个集群的TPS可以达到500。
  • 当服务调用者面对5个节点组成的服务提供集群时,请求应该分发到哪个节点,取决于负载均衡算法,通过该算法可以让每个服务器节点获得适合自己处理能力的负载。负载均衡可以分为硬件负载均衡和软件负载均衡,硬件负载均衡比较常见的是F5,软件负载均衡目前比较主流的是Nginx。
  • 在Dubbo中提供了4中负载均衡策略,默认负载均衡策略是random。同样,如果这4种策略不能满足实际需求,我们可以基于Dubbo中的SPI机制来扩展。
  1. Random LoadBalance:随机算法。可以针对性能好的服务器设置较大的权重值,权重值越大,随机的概率也会越大。
  2. RoundRobin LoadBalance:轮询。按照公约后的权重设置轮询比例。
  3. LeastActive LoadBalance:最少活跃调用。处理较慢的节点将会收到更小的请求。
  4. ConsistentHash LoadBalance:一致性Hash。相同参数的请求总是发生到同一个服务提供者。
  • 配置方式
    在@Service注解上添加loadbalance参数:
@Service(cluster = "failfast", loadbalance = "roundrobin")

服务降级

服务降级是一种系统保护策略,当服务器访问压力较大时,可以根据当前业务情况对不重要的服务进行降级,以保证核心服务的正常运行。所谓的降级,就是把一些非必要的功能在流量较大的时间段暂时关闭,比如在双11大促时,淘宝会把查看历史订单、商品评论等功能关闭,从而释放更多的资源来保障大部分用户能够正常完成交易。

降级有多个层面的分类:

  • 按照是否自动化分为自动降级和人工降级。
  • 按照功能可分为读服务降级和写服务降级。

自动降级

人工降级一般具有一定的前置性,比如在电商大促之前,暂时关闭某些非核心服务,如评价、推荐等。
自动降级更多的来自于系统出现某些异常的时候自动触发的“兜底策略”,比如:

  • 故障降级,调用的远程服务“挂了”,网络故障或者RPC服务返回异常。这类情况在业务允许的情况下可以通过设置兜底数据响应给客户端。
  • 限流降级,不管是什么类型的系统,它所支撑的流量是有限的,为了保护系统不被压垮,在系统中会针对核心业务进行限流。当请求流量达到阈值时,后续的请求会被拦截,这类请求可以进入排队系统,比如12306。也可以直接返回降级页面,比如返回“活动太火爆,请稍后在来”页面。

兜底策略实例

Dubbo提供了一种Mock配置来实现服务降级,也就是说当服务提供方出现网络异常无法访问时,客户端不抛出异常,而是通过降级配置返回兜底数据,步骤如下:

  • 在springcloudconsumer项目中创建MockHelloService类,这个类只需要实现自动降级的接口即可,然后重写接口中的抽象方法实现本地数据的返回。
public class MockHelloService implements IHelloService {
    @Override
    public String sayHello(String name) {
        return "Sorry, 服务无法访问,返回降级数据";
    }
}
  • 在HelloController类中修改@Reference注解增加Mock参数。其中设置了属性cluster=“failfast”,把集群容错设置成快速失败,因为默认的容错策略会发起两次重试,等待的时间较长。
@RestController
public class HelloController {
    @Reference(mock = "com.practice.springcloud.consumer.service.MockHelloService", cluster = "failfast")
    private IHelloService helloService;
    @GetMapping("/say")
    public String sayHello() {
        return helloService.sayHello("Test Dubbo Function");
    }
}
  • 在不起的Dubbo服务端或者服务端的返回值超过默认的超时时间时,访问/say接口得到的结果就是MockHelloService中返回的数据。
[root@localhost]# curl http://localhost:8090/say
Sorry, 服务无法访问,返回降级数据

主机绑定规则

dubbo服务对外发布的IP地址如何获取

主机绑定表示的是Dubbo服务对外发布的ip地址。
默认情况下Dubbo会按照以下顺序(优先级由高到低,只要其中一步获取到ip地址就作为对外发布地址)来查找并绑定主机IP地址:

  • 查找环境变量中DUBBO_IP_TO_BIND 属性配置的IP地址。
  • 查找dubbo.protocol.host 属性配置的IP地址,默认为空,如果没有配置或者IP地址不合法,则继续往下查找。
  • 通过LocalHost.getHostAddress获取本机IP地址,如果获取失败,则继续往下查找。
  • 如果配置了注册中心的地址,则使用socket通信连接到注册中心的地址后,使用for循环通过socket.getLocalHostAddress().getHostAddress()扫描各个网卡获取网卡IP地址。
    上述过程中,任意一个步骤检测到合法的IP地址,便会将其返回作为对外暴露的服务IP地址。需要注意的是,获取的IP地址并不是写入注册中心的地址,默认情况下,写入注册中心的IP地址优先选择环境变量中DUBBO_IP_TO_REGISTRY 属性配置的IP地址。在这个属性没有配置的情况下,才会选取前面获得的IP地址并写入注册中心。

对外发布的IP地址如何正确配置

使用默认的主机绑定规则,可能会存在获取的IP地址不正确的情况。
因为Dubbo检查本地IP地址的策略是先调用LocalHost.getHostAddress(),这个方法的原理是通过获取本机的hostname映射IP地址,如果它指向的是一个错误的IP地址,那么这个错误的地址将会作为服务发布的地址注册到zookeeper节点上,虽然dubbo服务能够正常启动,但是服务消费者却无法正常调用。按照Dubbo中IP地址的查找规则,如果遇到发布的ip地址不正确的情况,可以实现下面多种方案来解决:

  • 在 /etc/hosts 中配置机器名对应正确的IP地址映射。
  • 在环境变量中添加DUBBO_IP_TO_BIND或者DUBBO_IP_TO_REGISTRY 属性,Value值为绑定的主机地址。
  • 通过dubbo.protocol.host 设置主机地址。
    除获取绑定主机IP地址外,对外发布的端口也是需要注意的,Dubbo框架中针对不同的协议都提供了默认的端口:
  • Dubbo协议的默认端口是:20880。
  • Webservice协议的默认端口号是:80。
    在实际使用过程中,建议指定一个端口号,避免和其他Dubbo服务的端口产生冲突。