第八章 Nacos服务的注册和发现

685 阅读17分钟

8.1 Spring Cloud Alibaba简介

\

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。

官网地址:spring.io/projects/sp…

github项目地址:github.com/alibaba/spr…

8.1.1 为什么选择 Spring Cloud Alibaba

Spring cloud alibaba在spring cloud家族产品的一个套件,跟Netflix套件一样,涵盖了非常多的实用组件,其中很多跟Netflix一样,内容和功能存在重叠,那我们为什么放弃Netflix,转而选择spring cloud alibaba呢,两个原因如下:

  • Spring Cloud NetFlix项目进入维护模式,维护模式意味着Spring Cloud团队将不再向改团队添加新功能,修复block级别的Bug以及安全问题,会考虑审查社区的小型pull requests,参考文档spring.io/blog/2018/1…
  • 对于中国用户来说,spring cloud alibaba还有一个非常特殊的意义:他将红极一时的Dubbo,以及阿里巴巴强力消息中间件RocketMQ融入到spring cloud体系,同时spring cloud Alibaba中间件产品通过应对阿里的各种实际高并发场景,在实际案例中验证了足够优秀,值得信赖

8.1.2 主要功能

\

  • 服务限流降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
  • 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
  • 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
  • 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
  • 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
  • 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
  • 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

8.1.2 主要组件

\

  • Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
  • Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
  • Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
  • RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
  • Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
  • Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
  • Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

8.1.3 版本说明

\

组件版本关系

Spring Cloud Alibaba VersionSentinelNacosRocketMQDubboSeata
2.2.6.RELEASE1.8.11.4.24.4.02.7.81.3.0
2021.1 or 2.2.5.RELEASE or 2.1.4.RELEASE or 2.0.4.RELEASE1.8.01.4.14.4.02.7.81.3.0
2.2.3.RELEASE or 2.1.3.RELEASE or 2.0.3.RELEASE1.8.01.3.34.4.02.7.81.3.0
2.2.1.RELEASE or 2.1.2.RELEASE or 2.0.2.RELEASE1.7.11.2.14.4.02.7.61.2.0
2.2.0.RELEASE1.7.11.1.44.4.02.7.4.11.0.0
2.1.1.RELEASE or 2.0.1.RELEASE or 1.5.1.RELEASE1.7.01.1.44.4.02.7.30.9.0
2.1.0.RELEASE or 2.0.0.RELEASE or 1.5.0.RELEASE1.6.31.1.14.4.02.7.30.7.1

毕业版本依赖关系(推荐使用)

Spring Cloud VersionSpring Cloud Alibaba VersionSpring Boot Version
Spring Cloud 2020.0.02021.12.4.2
Spring Cloud Hoxton.SR92.2.6.RELEASE2.3.2.RELEASE
Spring Cloud Greenwich.SR62.1.4.RELEASE2.1.13.RELEASE
Spring Cloud Hoxton.SR32.2.1.RELEASE2.2.5.RELEASE
Spring Cloud Hoxton.RELEASE2.2.0.RELEASE2.2.X.RELEASE
Spring Cloud Greenwich2.1.2.RELEASE2.1.X.RELEASE
Spring Cloud Finchley2.0.4.RELEASE(停止维护,建议升级)2.0.X.RELEASE
Spring Cloud Edgware1.5.1.RELEASE(停止维护,建议升级)1.5.X.RELEASE

根据上面版本说明,课程案例中采用的个产品版本是

  • Spring Cloud 2020.0.3
  • Spring Cloud Alibaba 2021.1
  • Spring Boot 2.4.9

8.2 Nacos 简介

\

Nacos是阿里巴巴开源的一款支持服务注册与发现,配置管理以及微服务管理的组件。用来取代以前常用的注册中心(ZooKeeper , Eureka等),以及配置中心(Spring Cloud Config等)。Nacos是集成了注册中心和配置中心的功能。

8.2.1 Nacos的优势

\

Nacos 作为微服务核心的服务注册与发现中心,对比Eureka。

  • eureka 2.0闭源码了。
  • 从官网来看nacos 的注册的实例数是大于eureka的。
  • 因为nacos使用的raft协议,nacos集群的一致性要远大于eureka集群。

Raft 协议强依赖 Leader 节点来确保集群数据一致性。即 client 发送过来的数据均先到达 Leader 节点,Leader 接收到数据后,先将数据标记为 uncommitted 状态,随后 Leader 开始向所有 Follower 复制数据并等待响应,在获得集群中大于 N/2 个 Follower 的已成功接收数据完毕的响应后,Leader 将数据的状态标记为 committed,随后向 client 发送数据已接收确认,在向 client 发送出已数据接收后,再向所有 Follower 节点发送通知表明该数据状态为committed。

Nacos作为微服务配置中心对比Spring Cloud Config

  • Spring Cloud Config 大部分场景结合git 使用, 动态变更还需要依赖Spring Cloud Bus 消息总线来通过所有的客户端变化。
  • Spring Cloud Config 不提供可视化界面。
  • Nacos Config 使用长连接更新配置, 一旦配置有变动后,通知Provider的过程非常的迅速, 从速度上秒杀Spring Cloud Config。

\

8.2.2 Nacos Server安装启动

\

在使用Nacos之前需要先下载Nacos Server,下载地址:github.com/alibaba/nac…

Nacos Server有两种运行模式

  • standanlone:单节点模式
  • cluster:集群模式
  1. standalone模式

此模式一般用于demo和测试。命令如下。

bin/startup.sh -m standalone # linux
bin/startup.cmd -m standalone # windows

或者修改配置文件startup.cmd,代码如下,默认为cluster,然后直接运行startup.cmd

set MODE="standalone"

然后访问http://localhost:8848/nacos,进入nacos管控台,默认账号密码为nacos/nacos,如图8-1所示。

图8-1 Nacos管控台

  1. cluster模式

cluster 模式需要依赖 MySQL,然后改两个配置文件:

  • conf/cluster.conf
  • conf/application.properties

\

cluster模式配置如下。

(1)cluster.conf,填入要运行 Nacos Server 机器的 ip

\

192.168.100.155
192.168.100.156

\

(2)修改NACOS_PATH/conf/application.properties,加入 MySQL 配置

\

db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=root

\

创建一个名为nacos_config的 database,将NACOS_PATH/conf/nacos-mysql.sql中的表结构导入刚才创建的库中。

8.2.3 Nacos Server数据库

\

问题来了: Nacos Server 的配置数据是存在哪里呢?我们没有对 Nacos Server 做任何配置,那么数据只有两个位置可以存储:

\

  • 内存
  • 本地数据库

\

随便创建一个配置文件,重启nacos,配置文件还在,说明不是内存存储的。

\

这时候我们打开NACOS_PATH/data,会发现里边有个derby-data目录,我们的配置数据现在就存储在这个库中。

\

Derby 是 Java 编写的数据库,属于 Apache 的一个开源项目

\

Nacos Server 的数据源是用 Derby 还是 MySQL 完全是由其运行模式决定的:

  • standalone 的话仅会使用 Derby,即使在 application.properties 里边配置 MySQL 也照样无视;
  • cluster 模式会自动使用 MySQL,这时候如果没有 MySQL 的配置,是会报错的。

注意:不支持 MySQL 8.0 版本

\

\

8.3 Nacos实战

\

使用订单和支付服务,在订单微服务中调用支付微服务,演示Nacos作为注册中心的用法,如图8-2所示

  • Nacos Server:Nacos注册中心(Eureka)。
  • 支付服务:服务提供者。
  • 订单服务:服务消费者。

图8-2 Nacos实战项目结构

\

8.3.1 父工程

\

父工程统一管理spring boot,spring cloud和spring cloud alibaba版本号,参考上面版本对应关系

\

  • Spring Cloud 2020.0.3
  • Spring Cloud Alibaba 2021.1
  • Spring Boot 2.4.9

注意:spring-cloud-starter-bootstrap组件,在Spring Boot 2.4.X版本中,不在加载bootstrap.yml,需要spring-cloud-starter-bootstrap组件,才能加载bootstrap.yml配置文件。

\

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lxs.demo</groupId>
    <artifactId>cloud-alibaba-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>payment</module>
        <module>order</module>
    </modules>

    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.9</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <alibaba-cloud.version>2021.1</alibaba-cloud.version>
        <springcloud.version>2020.0.3</springcloud.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${springcloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${alibaba-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>


</project>

\

8.3.2 支付微服务-服务提供者

\

  1. pom.xml

\

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-alibaba-demo</artifactId>
        <groupId>com.lxs.demo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>payment</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

    </dependencies>


</project>

\

  1. application.yml

\

server:
  port: ${port:9001}

spring:
  application:
    name: payment-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #配置Nacos地址

\

  1. PaymentApplication

\

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentApplication
{
    public static void main(String[] args) {
        SpringApplication.run(PaymentApplication.class, args);
    }
}

\

  1. PaymentController

\

@RestController
@RequestMapping("/payment")
public class PaymentController {

    @Value("${server.port}")
    private String serverPort;

    @GetMapping("	/{id}")
    public ResponseEntity<String> payment(@PathVariable("id") Long id) {
        return ResponseEntity.ok("订单号 = " + id + ",支付成功,server.port" + serverPort);
    }



}

\

5) 启动支付服务

\

启动两个服务实例端口号分别为9001和9002,注册到nacos中

\

配置文件port: ${port:9001} 表示,没有port参数,使用9001端口,有port参数则使用port参数指定的端口,使用9002端口的支付服务,如图8-2所示。

\

图8-3 配置9002支付服务

\

使用9001端口支付服务,如图8-3所示。

图8-4 配置9001支付服务

\

访问nacos查看服务列表,如图8-4所示。

\

图8-5 支付服务列表

8.3.3 订单微服务-服务消费者

\

  1. pom.xml

注意:必须依赖spring-cloud-starter-loadbalancer组件,spring-cloud-starter-alibaba-nacos-discovery,不在默认继承ribbon,而是使用Spring Cloud Common总的loadbalancer组件,实现负载均衡

\

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-alibaba-demo</artifactId>
        <groupId>com.lxs.demo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>order</artifactId>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--open feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
       

    </dependencies>


</project>

\

  1. application.yml

\

server:
  port: 84

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

\

  1. 启动器

\

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderApplication
{
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

\

  1. ApplicationContextConfig

\

Nacos底层使用Spring Cloud Common中的Spring Cloud LoadBalancer组件实现负载均衡,注入RestTemplate,使用注解@LoadBalanced开启负载均衡功能。

\

@Configuration
public class ApplicationContextConfig {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

  1. FeignClient

\

(1)PaymentFallbackService

\

@Component
public class PaymentFallbackService implements PaymentService {

    @Override
    public ResponseEntity<String> payment(Long id) {
        return new ResponseEntity<String>("feign调用,异常降级方法", HttpStatus.INTERNAL_SERVER_ERROR);
    }


}

\

(2)PaymentService

\

@FeignClient(value = "payment-service", fallback = PaymentFallbackService.class)
public interface PaymentService {

    @GetMapping("/payment/{id}")
    public ResponseEntity<String> payment(@PathVariable("id") Long id);

}

\

  1. OrderController

\

@RestController
@RequestMapping("/order")
public class OrderController {
    
    public static final String SERVICE_URL = "http://payment-service";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/lb/{id}")
    public CommonResult consumer_ribbon(@PathVariable("id") Integer id){
        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
        return result
    }
    
    //OpenFeign
    @Resource
    private PaymentService paymentService;

    @GetMapping(value = "/feign/{id}")
    public CommonResult<Payment> consumer_feign(@PathVariable("id") Long id) {
        return paymentService.paymentSQL(id);
    }    
    
}

\

5) 启动

\

访问http://localhost:9003/order/lb/2地址,可以看到9001和9002端口交替执行,因为nacos底层也是采用Spring Cloud Loadbalancer进行负载均衡处理,如图8-6所示。

\

图8-6 nacos使用loadbalancer实现负载均衡

8.4 Nacos实现原理分析

\

8.4.1 Nacos架构图

\

分析Nacos的组成,如图8-7所示。

  • Provider APP:服务提供者
  • Consumer APP:服务消费者
  • Name Server:通过VIP(vritual ip)或者DNS实现Nacos高可用集群的服务路由
  • Nacos Server:Nacos服务提供者,里面包含的Open API是功能访问入口,Config Service、Naming Service是Nacos提供的配置服务、名字服务模块。Consistency Protocol是一致性协议,用来实现Nacos集群节点的数据同步,这里使用的是Raft算法
  • Nacos Console:Nacos控制台

整体来说,服务通过VIP(Virtual IP)访问Nacos Server高可用集群,基于Open API完成服务的注册和服务的查询。Nacos Server本事可以支持主备模式,所以底层会采用数据一致性算法来完成从节点的数据同步。服务消费者也是如此,基于Open API从Nacos Server中查询服务列表。

图8-7 Nacos架构图

8.4.2 注册中心原理

\

服务注册功能主要体现在:

  • 服务实例在启动时注册到服务注册表,并在关闭时注销。
  • 服务消费者查询服务注册表,获得可用实例。
  • 服务注册中心需要调用服务实例的监控检查API来验证它是否能够处理请求。

Nacos服务注册与发现的实现原理如图8-8所示

\

图8-8 Nacos注册中心原理

8.5 Nacos源码分析

\

Nacos源码部分,我们主要阅读三部分。

  • 服务注册。
  • 服务地址的获取。
  • 服务地址变化的感知。

下面基于这三方面分析Nacos是如何实现的。

8.5.1 Spring Cloud什么时候完成服务注册

\

在Spring Cloud Common包中有一个类org.springframework.cloud.client.serviceregistry.ServiceRegistry,他是Spring Cloud提供的服务注册的标准,集成到Spring Cloud中实现服务注册组件会实现改接口

public interface ServiceRegistry<R extends Registration> {
	void register(R registration);
	void deregister(R registration);
	void close();
	void setStatus(R registration, String status);
	<T> T getStatus(R registration);
}

Nacos集成Spring Cloud服务注册有一个实现类com.alibaba.cloud.nacos.registry.NacosServiceRegistry,它是什么时候触发服务注册动作的呢?

在Spring Cloud Commons包的META-INF/spring.factories中包含自动装配的配置信息如下。

# AutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.client.CommonsClientAutoConfiguration,\
org.springframework.cloud.client.ReactiveCommonsClientAutoConfiguration,\
org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationAutoConfiguration,\

其中AutoServiceRegistrationAutoConfiguration就是服务注册相关的配置类,代码如下。

@Configuration(proxyBeanMethods = false)
@Import(AutoServiceRegistrationConfiguration.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
public class AutoServiceRegistrationAutoConfiguration {

	@Autowired(required = false)
	private AutoServiceRegistration autoServiceRegistration;

	@Autowired
	private AutoServiceRegistrationProperties properties;

	@PostConstruct
	protected void init() {
		if (this.autoServiceRegistration == null && this.properties.isFailFast()) {
			throw new IllegalStateException(
					"Auto Service Registration has " + "been requested, but there is no AutoServiceRegistration bean");
		}
	}

}

在AutoServiceRegistrationAutoConfiguration配置类中,可以看到注入了一个AutoServiceRegistration实例,该类的类图如图8-9所示。可以看出AutoServiceRegistrationAutoConfiguration抽象类实现了该接口,并且最重要的是

图8-9 AutoServiceRegistration类图

\

我们重点关注ApplicationListener,熟悉Spring框架都知道它是一种事件监听机制,该类声明如下。

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

	/**
	 * Handle an application event.
	 * @param event the event to respond to
	 */
	void onApplicationEvent(E event);
    
	static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
		return event -> consumer.accept(event.getPayload());
	}

}

其中方法的作用是监听某个指定的事件,而AbstractAutoServiceRegistration实现了该抽象的方法,并且监听WebServerInitializedEvent事件,调用this.bind(event)方法。

public abstract class AbstractAutoServiceRegistration<R extends Registration>
		implements AutoServiceRegistration, ApplicationContextAware, ApplicationListener<WebServerInitializedEvent> {

	public void onApplicationEvent(WebServerInitializedEvent event) {
		bind(event);
	}
    ....
}

继续跟进this.bind方法,可以发现最终调用com.alibaba.cloud.nacos.registry.NacosServiceRegistry.register方法完成注册

8.5.2 NacosServiceRegistry的实现

\

在NacosServiceRegistry.register方法中,调用Nacos Client SDK中的nameService.registerInstance完成注册。代码如下。

	@Override
	public void register(Registration registration) {

		if (StringUtils.isEmpty(registration.getServiceId())) {
			log.warn("No service to register for nacos client...");
			return;
		}

		NamingService namingService = namingService();
		String serviceId = registration.getServiceId();
		String group = nacosDiscoveryProperties.getGroup();

		Instance instance = getNacosInstanceFromRegistration(registration);
        
		...
	}

再来看一下namingService.registerInstance()方法的实现,主要逻辑如下

  • 通过beatReator.addBeatInfo创建心跳信息实现健康检查,Nacos Server必须要确保注册的服务实例是健康的,而心跳检测就是服务健康检测的手段。
  • serverProxy.registerService实现服务注册。
    public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        
        NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", namespaceId, serviceName,
                instance);
        
        final Map<String, String> params = new HashMap<String, String>(16);
        params.put(CommonParams.NAMESPACE_ID, namespaceId);
        params.put(CommonParams.SERVICE_NAME, serviceName);
        params.put(CommonParams.GROUP_NAME, groupName);
        params.put(CommonParams.CLUSTER_NAME, instance.getClusterName());
        params.put("ip", instance.getIp());
        params.put("port", String.valueOf(instance.getPort()));
        params.put("weight", String.valueOf(instance.getWeight()));
        params.put("enable", String.valueOf(instance.isEnabled()));
        params.put("healthy", String.valueOf(instance.isHealthy()));
        params.put("ephemeral", String.valueOf(instance.isEphemeral()));
        params.put("metadata", JacksonUtils.toJson(instance.getMetadata()));
        
        reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST);
        
    }

服务注册的逻辑下一小节单独分析,这里重点关注beatReactor.addBeatInfo实现心跳机制。代码如下。

    public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
        BeatInfo existBeat = null;
        //fix #1733
        if ((existBeat = dom2Beat.remove(key)) != null) {
            existBeat.setStopped(true);
        }
        dom2Beat.put(key, beatInfo);
        executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size());
    }

从上述代码看,所谓心跳机制就是客户端通过schedule定时享服务端发送一个数据包,然后启动一个线程不断检测服务端的回应,如果在设定时间内没有收到服务端的回应,则任务服务器出现了故障。Nacos服务端会根据客户端的心跳包不断更新服务的状态。

8.5.3 Nacos服务注册原理

\

Nacos提供了SDK及Open API的形式来实现服务注册。

  1. Java SDK服务注册
void registerInstance(String serviceName, String ip, int port) throws NacosException;
void registerInstance(String serviceName, String ip, int port, String clusterName) throws NacosException;
void registerInstance(String serviceName, Instance instance) throws NacosException;

\

请求参数

名称类型描述
serviceName字符串服务名
ip字符串服务实例IP
portint服务实例port
clusterName字符串集群名
instance参见代码注释实例属性
  1. Open API服务注册

使用post请求/nacos/v1/ns/instance,实例代码。

curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?port=8848&healthy=true&ip=11.11.11.11&weight=1.0&serviceName=nacos.test.3&encoding=GBK&namespaceId=n1'

请求参数

名称类型是否必选描述
ip字符串服务实例IP
portint服务实例port
namespaceId字符串命名空间ID
weightdouble权重
enabledboolean是否上线
healthyboolean是否健康
metadata字符串扩展信息
clusterName字符串集群名
serviceName字符串服务名
groupName字符串分组名
ephemeralboolean是否临时实例

对于服务注册,对外提供的服务接口请求地址为nacos/v1/ns/instance,实现代码在nacos-naming模块下的InstanceController类中。

  • 从请求中获得serviceName(服务名)和namespaceId(命名空间Id)。
  • 调用registerInstance注册实例
@RestController
@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT + "/instance")
public class InstanceController {
    @CanDistro
    @PostMapping
    @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
    public String register(HttpServletRequest request) throws Exception {
        
        final String namespaceId = WebUtils
                .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
        final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
        NamingUtils.checkServiceNameFormat(serviceName);
        
        final Instance instance = parseInstance(request);
        
        serviceManager.registerInstance(namespaceId, serviceName, instance);
        return "ok";
    }

上述代码namespaceId默认为public,registerInstance方法的主要逻辑。

  • 创建一个空服务(在Nacos控制台“服务列表”中展示的服务信息),实际上是初始化一个serviceMap,它是一个ConcurrentHashMap集合。
  • getService,从serviceMap中,根据namespaceId和serviceName得到一个服务对象。
  • 调用addInstance添加服务实例。
    public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException {
        
        createEmptyService(namespaceId, serviceName, instance.isEphemeral());
        
        Service service = getService(namespaceId, serviceName);
        
        if (service == null) {
            throw new NacosException(NacosException.INVALID_PARAM,
                    "service not found, namespace: " + namespaceId + ", service: " + serviceName);
        }
        
        addInstance(namespaceId, serviceName, instance.isEphemeral(), instance);
    }

下面简单分析一下createEmptyService创建空服务的代码,最终调用的是createServiceIfAbsent

  • 根据namespaceId,serviceName从缓存中获取Service实例。
  • 如果service为空,则创建并保存到缓存中。
    public void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster)
            throws NacosException {
        Service service = getService(namespaceId, serviceName);
        if (service == null) {
            
            Loggers.SRV_LOG.info("creating empty service {}:{}", namespaceId, serviceName);
            service = new Service();
            service.setName(serviceName);
            service.setNamespaceId(namespaceId);
            service.setGroupName(NamingUtils.getGroupName(serviceName));
            // now validate the service. if failed, exception will be thrown
            service.setLastModifiedMillis(System.currentTimeMillis());
            service.recalculateChecksum();
            if (cluster != null) {
                cluster.setService(service);
                service.getClusterMap().put(cluster.getName(), cluster);
            }
            service.validate();
            
            putServiceAndInit(service);
            if (!local) {
                addOrReplaceService(service);
            }
        }
    }

主要关注putServiceAndInit方法。

  • 通过putService方法将服务缓存到内存。
  • service.init()简历心跳检测机制。
  • consistencyService.listen实现数据一致性的监听。
    private void putServiceAndInit(Service service) throws NacosException {
        putService(service);
        service.init();
        consistencyService
                .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service);
        consistencyService
                .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service);
        Loggers.SRV_LOG.info("[NEW-SERVICE] {}", service.toJson());
    }

service.init()方法代码就不看了,如图8-10所示,他主要通过定时任务不断检测当前服务下的所有实例最后发送的心跳包的时间,如果超时,则设置healthy为fase,表示服务不健康,并且发送服务变更事件。

图8-10 心跳检测机制

下面再看putService方法,他的功能将Service保存到serviceMap中。

    public void putService(Service service) {
        if (!serviceMap.containsKey(service.getNamespaceId())) {
            synchronized (putServiceLock) {
                if (!serviceMap.containsKey(service.getNamespaceId())) {
                    serviceMap.put(service.getNamespaceId(), new ConcurrentSkipListMap<>());
                }
            }
        }
        serviceMap.get(service.getNamespaceId()).put(service.getName(), service);
    }

最后简单总结下,服务注册的完成过程。

  • Nacos客户端通过Open SDK的形式发送服务注册的请求。
  • Nacos服务端收到请求后,做了一下3件事。
  • 构建一个Service对象保存到ConcurrentHashMap中。
  • 使用定时任务对当前服务下的所有势力简历心跳检测机制。
  • 基于数据一致性协议将服务数据进行同步。

8.5.4 服务地址的查询

\

服务地址查询Open API。

curl -X GET '127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=nacos.test.1'

使用SDK方式如下,healf表示服务的健康状态。

List<Instance> selectInstances(String serviceName, boolean healthy) throws NacosException;

nacos-naming模块下的InstanceController类。

  • 解析请求参数。
  • 通过doSrvIpxt返回服务列表数据。
    @GetMapping("/list")
    @Secured(parser = NamingResourceParser.class, action = ActionTypes.READ)
    public ObjectNode list(HttpServletRequest request) throws Exception {
        
        String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID);
        String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME);
        NamingUtils.checkServiceNameFormat(serviceName);
        
        String agent = WebUtils.getUserAgent(request);
        String clusters = WebUtils.optional(request, "clusters", StringUtils.EMPTY);
        String clientIP = WebUtils.optional(request, "clientIP", StringUtils.EMPTY);
        int udpPort = Integer.parseInt(WebUtils.optional(request, "udpPort", "0"));
        String env = WebUtils.optional(request, "env", StringUtils.EMPTY);
        boolean isCheck = Boolean.parseBoolean(WebUtils.optional(request, "isCheck", "false"));
        
        String app = WebUtils.optional(request, "app", StringUtils.EMPTY);
        
        String tenant = WebUtils.optional(request, "tid", StringUtils.EMPTY);
        
        boolean healthyOnly = Boolean.parseBoolean(WebUtils.optional(request, "healthyOnly", "false"));
        
        return doSrvIpxt(namespaceId, serviceName, agent, clusters, clientIP, udpPort, env, isCheck, app, tenant,
                healthyOnly);
    }

doSrvIpxt方法,大致逻辑如下。

  • 根据namespaceId、serviceName获得service实例。
  • 从Service实例中基于srvIps得到所有服务提供者的实例信息。
  • 遍历组装JSON字符串并返回。

8.5.5 Nacos服务地址动态感知

\

服务消费者不仅需要获得服务提供者的地址列表,还需要服务实例出现异常时监听服务地址的变化,服务动态感知的基本原理如图8-11所示,Nacos客户端有一个HostReactor类,它的功能是实现服务的动态更新,基本原理如下。

  • 客户端发起事件订阅后,在HostReactor中有一个UpdateTask线程,每个10秒发送一次Pull请求,获得服务端最新的地址列表。
  • 对于服务端,他和服务提供者的实例之间维持了心跳检测,一旦服务提供者出现异常,则会发送一个Pull消息给Nacos客户端,也就是服务消费者。
  • 服务消费者受到请求后,使用HostReactor中提供的processServiceJSON解析消息,并更新本地服务地址列表。

图8-11服务动态感知