服务的治理-nacos

什么是服务治理

服务治理是对通过注册中心来对服务们进行管理,里面包括服务的注册和发现等功能,在一个微服务架构的系统中会有很多服务提供者和服务消费者,消费者是如何找到服务的提供者呢?此时服务注册中心的作用就凸显出来了,服务提供者将自己注册到这个注册中心上,服务消费者需要消费的时候从注册中心查找,相当于是将服务提供者和服务消费者做了解耦的操作。

服务注册中心通常会具备下面功能

  • 注册每个服务提供者都将自己注册到注册中心,注册中心记录微服务的名称,ip,端口号等信息。
  • 发现消费者从注册中心中找到服务提供者的注册信息,然后根据注册信息找到服务提供者进行调用。
  • 服务检查以心跳的方式检测服务提供者是否可用,如果不可用则由注册中心将其移除。

这里做一个比喻,注册中心好比就是淘宝,当你(消费者)想买一双篮球鞋的时候会从淘宝上搜索,然后找到合适店铺(提供者)购买心仪的鞋子即可,淘宝(注册中心)需要将那些挂掉的店铺(提供者)移除,由此可见注册中心的重要性了。

注册中心相关技术

  • Zookeeper
    zookeeper是一个使用java实现的分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。遵循CAP原理中的CP。
  • Eureka
    Eureka是Springcloud Netflix中的重要组件,由java实现,主要作用就是做服务注册和发现。但是现在已经闭源。遵循CAP原理中的AP。
  • Consul
    Consul是HashiCorp 公司基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能。遵循CAP原理中的CP。
  • Nacos
    Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是 Spring Cloud Alibaba 组件之一,由java实现,负责服务注册发现和服务配置,可以这样认为nacos=eureka+config。可以通过配置切换Nacos中的CP和AP。

nacos简介

nacos是spring cloud alibaba组件之一,主要作用就是注册中心和服务配置,nacos是使用spring boot开发的。接下来先看下如何使用nacos作为注册中心。

nacos官网:https://nacos.io/zh-cn/docs/quick-start.html

下载nacos安装包:https://github.com/alibaba/nacos/releases

上面如果打开的话可以从这里下载: https://pan.baidu.com/s/14QK3R50oGHvCRVI1Ono6ng 提取码: yegk

下载之后解压(解压目录中不要带有中文),进入到nacos的bin目录下(这里用的是2.0.3版本),该版本nacos默认是集群启动的,因此需要先改成单机模式,windows操作系统下用文本编辑器打开startup.cmd,将set MODE=”cluster”中cluster改成standalone即可(linux同理),修改之后双击startup.cmd即可启动nacos服务器了,默认端口是8848,启动之后浏览器中输入:http://localhost:8848/nacos ,默认的用户名和密码都是nacos

服务的注册

nacos注册中心配置好之后,接下来就是要进行服务的注册了,接下来将pay模块和buy模块都注册到nacos中。

1.添加依赖

将下面依赖分别添加到pay模块和buy模块中,需要注意的是分别要在dependencyManagement和dependencies中添加:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2020.0.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2021.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

下面将nacos起步依赖和loadbalancer直接添加到dependencies中:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
            </exclusion>
        </exclusions>
</dependency>
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

2.添加注解

如果你用的是比较旧的cloud版本,那么需要分别在两个模块的启动类上面添加@EnableDiscoveryClient注解用来开启服务的发现。本文使用的版本无需添加。

3.修改配置文件

分别在两个模块的appliction.properties或application.yml配置文件中设置服务的名字和注册中心的地址,通过这些配置就可以将服务注册到注册中心上面了。

pay模块:

#tomcat端口号
server.port=8081
#给微服务命名
spring.application.name=pay-service
#注册中心nacos的地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

buy模块

#tomcat端口号
server.port=8082
#给微服务命名
spring.application.name=buy-service
#注册中心nacos的地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

4.从服务注册中心里面查找服务

上面的操作已经将服务都注册到了nacos服务注册中心上,此时想要调用服务的时候只需从nacos服务注册中心中查找即可。这里修改buy模块中的controller为从注册中心中获取服务。

先注入DiscoveryClient,然后调用getInstances方法传入要获取的微服务的名字即可,这个名字就是之前在配置文件中spring.application.name设置的名字。

getInstances方法会返回一个List对象,里面存放的是获取到的微服务实例对象,这里为什么是返回List呢?因为实际应用中往往会对微服务进行集群配置,此时一个名字会有多个微服务对象,所以会返回List,里面存放的这些对象。

拿到微服务ServiceInstance对象之后,通过当前微服务的ip和端口号就可以进行调用了。

package com.monkey1024.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

/*
    购买
 */
@RestController
public class BuyController {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("buy")
    public String buy() {

        /*
            从注册中心获取微服务的实例对象,getInstances中传入要获取的微服务的名字
         */
        List<ServiceInstance> instances = discoveryClient.getInstances("pay-service");
        ServiceInstance serviceInstance = instances.get(0);

        //通过微服务实例对象获取其地址和端口号进行调用
        String msg = restTemplate.getForObject("http://"+ serviceInstance.getHost()+":"+serviceInstance.getPort()+"/pay", String.class);

        return "购买成功:" + msg;
    }
}

创建配置类将RestTemple对象放入到spring容器中

/*
    配置类
 */
@Configuration
public class BeanConfig {
    /*
        将RestTemple对象放入到spring容器中
     */
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

5.启动测试

先启动nacos服务注册中心,然后分别启动pay模块和buy模块,然后我们可以在服务注册中心的页面中的服务列表下看到注册成功的微服务。之后从浏览器中访问buy模块,buy模块作为服务消费者会从注册中心nacos中找到服务提供者pay,这样就可以成功调用了。

负载均衡

负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行。微服务架构应用实际部署的时候往往会进行集群部署,此时就需要负载均衡控制将请求分发到哪个微服务上了。负载均衡分为服务端负载均衡和客户端负载均衡。

  • 服务端负载均衡:nginx
  • 客户端负载均衡:ribbon和spring-cloud-loadbalancer

在之前的版本中,spring cloud使用了Netflix 公司发布Ribbon作为负载均衡器,但是由于Ribbon进入维护模式,spring cloud在Spring Cloud Commons中添加了spring-cloud-loadbalancer作为新的负载均衡器用来替换Ribbon。在之前的示例中我们已经引入spring-cloud-loadbalancer依赖,因此我们可以直接使用loadbalancer实现负载均衡了。spring-cloud-loadbalancer主要功能是提供客户端的软件负载均衡算法,使用起来非常方便。接下来将之前的代码修改为负载均衡。

1.创建服务提供者

为了便于测试,再创建一个pay模块,端口号设置为8083,保证其不被占用即可,PayController中的pay方法的返回值修改一下,使两个pay模块中的返回值产生差异,便于测试观察。

2.修改配置文件

之前我们将服务名pay-service写死到代码中了,这样不利于维护,所以现在将该部分写到配置文件中,通过读取配置文件来获取,添加如下内容,等号左边随意命名,等号右边是要调用的服务名:

#要调用微服务的名字
service-url.pay-service=pay-service

3.修改BuyController

这里只需要通过服务名调用即可

/*
    购买
 */
@RestController
public class BuyController {

    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    //从配置文件中读取调用服务的地址
    @Value("${service-url.pay-service}")
    private String payUrl;

    @GetMapping("buy")
    public String buy() {
        /*
            从注册中心获取微服务的实例对象,getInstances中传入要获取的微服务的名字
         */
        ServiceInstance serviceInstance = loadBalancerClient.choose(payUrl);

        //通过微服务实例对象获取其地址和端口号进行调用
        String msg = restTemplate.getForObject("http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/pay", String.class);

        return "购买成功:" + msg;
    }
}

4.启动测试

分别启动nacos服务器,两个pay模块,一个buy模块,多次访问buy模块可以看到会调用不同的pay模块提供的服务。

使用Ribbon

上面使用的是loadBalancerClient实现的负载均衡,下面将代码修改为使用Ribbon,buy方法如下:

@GetMapping("buy")
    public String buy() {
        String msg = restTemplate.getForObject("http://" + payUrl + "/pay", String.class);
        return "购买成功:" + msg;
    }

在BeanConfig类的restTemplate方法上添加@LoadBalanced注解即可

@Configuration
public class BeanConfig {
    /*
        将RestTemple对象放入到spring容器中
     */
    @Bean
    @LoadBalanced//添加负载均衡
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

nacos缓存

在spring cloud starter下的nacos discovery里面的NacosServer类中有个Map类型的成员变量metadata,它的作用是将服务缓存,当某个服务第一次被访问之后,该服务会加入到这个Map中,之后再次访问的时候会直接从Map中获取,提高效率。我们可以通过下面实验来验证下nacos缓存。

启动nacos,消费者,提供者,先让消费者调用一下提供者,关闭nacos之后消费者仍然是可以访问提供者,这就是nacos缓存的作用了。在消费者没有调用提供者的前提下,关闭nacos,缓存是无效的。

public class NacosServer extends Server {

	private final MetaInfo metaInfo;
	private final Instance instance;
	private final Map<String, String> metadata;

	public NacosServer(final Instance instance) {
		super(instance.getIp(), instance.getPort());
		this.instance = instance;
		this.metaInfo = new MetaInfo() {
			@Override
			public String getAppName() {
				return instance.getServiceName();
			}

			@Override
			public String getServerGroup() {
				return null;
			}

			@Override
			public String getServiceIdForDiscovery() {
				return null;
			}

			@Override
			public String getInstanceId() {
				return instance.getInstanceId();
			}
		};
		this.metadata = instance.getMetadata();
	}

	@Override
	public MetaInfo getMetaInfo() {
		return metaInfo;
	}

	public Instance getInstance() {
		return instance;
	}

	public Map<String, String> getMetadata() {
		return metadata;
	}
}