SpringCloud中的一些重要组件的学习第一版(包括:Eureka,LoadBalance,Nacos,OpenFeign,Gateway,以及分布式服务部署)

目录

1.Eureka服务发现组件:

问题描述:

解决思路:

注册中心:

​编辑CAP理论:

常见的注册中心(了解为主):

1. Zookeeper

2. Eureka

3. Nacos(功能强大)

1.搭建Eureka Server:

​编辑

2.服务注册:

3.服务发现:

Eureka 和Zookeeper区别:

2.多机部署,负载均衡-LoadBalance:

负载均衡的介绍:

Spring Cloud LoadBalancer:

使⽤Spring Cloud LoadBalancer实现负载均衡:

LoadBalancer 原理:

服务部署(Linux):

3.Nacos注册中心实现

Nacos安装:

修改单机模式:

启动Nacos:

常⻅问题:

Linux:

nacos实现:

Nacos负载均衡:

服务下线:

配置权重

同集群优先访问:

给实例配置集群名称

Nacos 健康检查:

两种健康检查机制:

Nacos服务实例类型:

Nacos环境隔离:

创建Namespace:

Nacos配置中⼼:

为什么需要配置中⼼?

添加配置:

获取配置:

配置中⼼详解

Data Id:

服务部署:

Nacos与Eureka的区别:

4.优雅实现远程调⽤-OpenFeign

RestTemplate存在问题:

OpenFeign介绍

Spring Cloud Feign:

OpenFeign参数传递:

最佳实践:

Feign 继承⽅式:

Feign 抽取⽅式

服务部署:

5.GateWay-统一接口服务:

⽹关介绍:

常⻅⽹关实现

Zuul

Spring Cloud Gateway

Spring Cloud Gateway实现

Route Predicate Factories(路由断⾔⼯⼚):

Gateway Filter Factories(⽹关过滤器⼯⼚)

GatewayFilter:

GatewayFilter说明:

​编辑

Default Filters(默认过滤器):

GlobalFilter(全局过滤器):

Gateway Metrics Filter:

过滤器执⾏顺序:

⾃定义过滤器:

⾃定义GatewayFilter

⾃定义GlobalFilter:

服务部署:

分布式服务部署:

1. 部署前准备

2. 部署操作

MySQL安装

Nacos 安装:

产品服务, 订单服务部署:

⽹关服务部署:


这篇文章已经简单的构建了一个工程,接下来要学习如何使⽤Spring Cloud 来解决微服务中的问题

1.Eureka服务发现组件:

Eureka官方文档

问题描述:

上个章节的例⼦中可以看到, 远程调⽤时, 我们的URL是写死的

当更换机器, 或者新增机器时, 这个URL就需要跟着变更, 就需要去通知所有的相关服务去修改. 随之⽽来的就是各个项⽬的配置⽂件反复更新, 各个项⽬的频繁部署. 这种没有具体意义, 但⼜不得不做的⼯作, 会让⼈⾮常痛苦.

解决思路:

微服务开发时:

服务启动/变更时, 向注册中⼼报道. 注册中⼼记录应⽤和IP的关系.

调⽤⽅调⽤时, 先去注册中⼼获取服务⽅的IP, 再去服务⽅进⾏调⽤

注册中心:

注册中⼼主要有三种⻆⾊:

服务提供者(Server):⼀次业务中, 被其它微服务调⽤的服务. 也就是提供接⼝给其它微服务.

服务消费者(Client):⼀次业务中, 调⽤其它微服务的服务. 也就是调⽤其它微服务提供的接⼝.

服务注册中⼼(Registry): ⽤于保存Server 的注册信息, 当Server 节点发⽣变更时, Registry 会同步变更. 服务与注册中⼼使⽤⼀定机制通信, 如果注册中⼼与某服务⻓时间⽆法通信, 就会注销该实例.他们之间的关系以及⼯作内容, 可以通过两个概念来描述:

服务注册:服务提供者在启动时, 向 Registry 注册⾃⾝服务, 并向 Registry 定期发送⼼跳汇报存活状态.

服务发现: 服务消费者从注册中⼼查询服务提供者的地址,并通过该地址调⽤服务提供者的接⼝. 服务发现的⼀个重要作⽤就是提供给服务消费者⼀个可⽤的服务列表.

CAP理论:

CAP 理论是分布式系统设计中最基础, 也是最为关键的理论.
⼀致性(Consistency) CAP理论中的⼀致性, 指的是强⼀致性. 所有节点在同⼀时间具有相同的数据

可⽤性(Availability) 保证每个请求都有响应(响应结果可能不对)

分区容错性(Partition Tolerance) 当出现⽹络分区后,系统仍然能够对外提供服务

CAP 理论告诉我们: ⼀个分布式系统不可能同时满⾜数据⼀致性, 服务可⽤性和分区容错性这三个基本需求, 最多只能同时满⾜其中的两个.

在分布式系统中, 系统间的⽹络不能100%保证健康, 服务⼜必须对外保证服务. 因此Partition Tolerance不可避免. 那就只能在C和A中选择⼀个. 也就是CP或者AP架构

CP架构: 为了保证分布式系统对外的数据⼀致性, 于是选择不返回任何数据

AP架构: 为了保证分布式系统的可⽤性, 节点2返回V0版本的数据(即使这个数据不正确)

常见的注册中心(了解为主):

1. Zookeeper

Zookeeper的官⽅并没有说它是⼀个注册中⼼, 但是国内Java体系, ⼤部分的集群环境都是依赖Zookeeper来完成注册中⼼的功能.

2. Eureka

Eureka是Netflix开发的基于REST的服务发现框架, 主要⽤于服务注册, 管理,负载均衡和服务故障转移.

官⽅声明在Eureka2.0版本停⽌维护, 不建议使⽤. 但是Eureka是SpringCloud服务注册/发现的默认实现, 所以⽬前还是有很多公司在使⽤.

3. Nacos(功能强大)

Nacos是Spring Cloud Alibaba架构中重要的组件, 除了服务注册, 服务发现功能之外, Nacos还⽀持配置管理, 流量管理, DNS, 动态DNS等多种特性.

CAP理论对⽐:

ZookeeperEurekaNacos
CPAPCP或AP 默认AP

在分布式环境中, 即使拿到⼀个错误的数据, 也胜过⽆法提供实例信息⽽造成请求失败要好(⽐如淘宝11.11, 京东618都是谨遵AP原则)

Eureka主要分为两个部分:

• Eureka Server: 作为注册中⼼Server端, 向微服务应⽤程序提供服务注册, 发现, 健康检查等能⼒.

• Eureka Client: 服务提供者, 服务启动时, 会向Eureka Server 注册⾃⼰的信息(IP,端⼝,服务信息等),Eureka Server 会存储这些信息

1.搭建Eureka Server:

创建Eureka-server ⼦模块:

引⼊eureka-server依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

项⽬构建插件

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

完善启动类

//@EnableEurekaServer标识开启注册中心的功能
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class,args);
    }
}

编写配置⽂件

# Eureka相关配置
# Eureka 服务
server:
  port: 10010
spring:
  application:
    name: eureka-server
eureka:
  instance:
    hostname: localhost
  client:
    fetch-registry: false # 表示是否从Eureka Server获取注册信息,默认为true.因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,这里设置为false
    register-with-eureka: false # 表示是否将自己注册到Eureka Server,默认为true.由于当前应用就是Eureka Server,故而设置为false.
    service-url:
      # 设置与Eureka Server的地址,查询服务和注册服务都需要依赖这个地址
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

2.服务注册:

接下来我们把product-service 注册到eureka-server中
1.引⼊eureka-client依赖

        <!--引⼊eureka-client依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

2.完善配置⽂件

server:
  port: 9090
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/cloud_product?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: "123456"
    driver-class-name: com.mysql.cj.jdbc.Driver
  application:
    name: product-service
mybatis:
  configuration: # 配置打印 MyBatis⽇志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true #配置驼峰⾃动转换
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10010/eureka

3.启动服务
刷新注册中⼼: http://127.0.0.1:10010/


可以看到product-service已经注册到 eureka上了

3.服务发现:

接下来我们修改order-service, 在远程调⽤时, 从eureka-server拉取product-service的服务信息, 实现服务发现.
引⼊依赖:

服务注册和服务发现都封装在eureka-client依赖中, 所以服务发现时, 也是引⼊eureka-client依赖

<!--引⼊eureka-client依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

完善配置⽂件:

服务发现也需要知道eureka地址, 因此配置内容依然与服务注册⼀致,都是配置eureka信息

server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/cloud_order?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: "123456"
    driver-class-name: com.mysql.cj.jdbc.Driver
  application:
    name: order-service
mybatis:
  configuration: # 配置打印 MyBatis⽇志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true #配置驼峰⾃动转换
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10010/eureka

远程调⽤:
远程调⽤时, 我们需要从eureka-server中获取product-service的列表(可能存在多个服务), 并选择其中⼀个进⾏调⽤

@Slf4j
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    //服务发现实体
    @Autowired
    private DiscoveryClient discoveryClient;

    public OrderInfo selectOrderById(Integer orderId){
        OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
//        String url = "http://127.0.0.1:9090/product/"+orderInfo.getProductId();
        //从Eureka中获取服务信息
        List<ServiceInstance> instances = discoveryClient.getInstances("product-service");
        String uri = instances.get(0).getUri().toString();//这里就获取到了服务的IP+Port
        String url = uri+"/product/"+orderInfo.getProductId();
        log.info("远程调用url:{}",url);
        ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
        orderInfo.setProductInfo(productInfo);
        return orderInfo;
    }
}

启动服务:

刷新注册中⼼: http://127.0.0.1:10010/
可以看到order-service已经注册到 eureka上了 访问接⼝: http://127.0.0.1:8080/order/1

可以看到, 远程调⽤也成功了.

Eureka 和Zookeeper区别:

Eureka和Zookeeper都是⽤于服务注册和发现的⼯具,区别如下:

1. Eureka是Netflix开源的项⽬, ⽽Zookeeper是Apache开源的项⽬.

2. Eureka 基于AP原则, 保证⾼可⽤, Zookeeper基于CP原则, 保证数据⼀致性.

3. Eureka 每个节点 都是均等的, Zookeeper的节点区分Leader 和Follower 或 Observer, 也正因为这个原因, 如果Zookeeper的Leader发⽣故障时, 需要重新选举, 选举过程集群会有短暂时间的不可⽤.

2.多机部署,负载均衡-LoadBalance:

问题描述:

List<ServiceInstance> instances = discoveryClient.getInstances("productservice");
//服务可能有多个, 获取第⼀个
EurekaServiceInstance instance = (EurekaServiceInstance) instances.get(0);

由于每次获取服务,只会获取服务列表的第一个,如果一个服务对应多个实例,流量是否可以合理的分配到多个实例?

显然是不能的,通过再启动2个product-service实例,可以验证

解决方案:

请求计数器 % 实例数

请求计数器使用AtomicInteger,是一个多线程的类

@Slf4j
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    //服务发现实体
    @Autowired
    private DiscoveryClient discoveryClient;

    //计数器
    private  AtomicInteger atomicInteger = new AtomicInteger(1);

    private  List<ServiceInstance> instances;

    @PostConstruct
    public void init (){
        instances = discoveryClient.getInstances("product-service");
    }

    public OrderInfo selectOrderById(Integer orderId){
        OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
        //从Eureka中获取服务信息
        //请求次数 % 实例数
        int index = atomicInteger.getAndIncrement() % instances.size();
        ServiceInstance instance = instances.get(index);
        String uri = instance.getUri().toString();//这里就获取到了服务的IP+Port
        String url = uri+"/product/"+orderInfo.getProductId();
        log.info(instance.getInstanceId());
        ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
        orderInfo.setProductInfo(productInfo);
        return orderInfo;
    }
}

重启服务:

通过⽇志可以看到, 请求被均衡的分配在了不同的实例上, 这就是负载均衡.

负载均衡的介绍:

负载均衡(Load Balance,简称 LB) , 是⾼并发, ⾼可⽤系统必不可少的关键组件.
当服务流量增⼤时, 通常会采⽤增加机器的⽅式进⾏扩容, 负载均衡就是⽤来在多个机器或者其他资源中, 按照⼀定的规则合理分配负载.

负载均衡的⼀些实现:

上⾯的例⼦中, 我们只是简单的对实例进⾏了轮询, 但真实的业务场景会更加复杂. ⽐如根据机器的配置进⾏负载分配, 配置⾼的分配的流量⾼, 配置低的分配流量低等.

服务多机部署时, 开发⼈员都需要考虑负载均衡的实现, 所以也出现了⼀些负载均衡器, 来帮助我们实现负载均衡.负载均衡分为服务端负载均衡和客⼾端负载均衡.

服务端负载均衡:

在服务端进⾏负载均衡的算法分配.

⽐较有名的服务端负载均衡器是Nginx. 请求先到达Nginx负载均衡器, 然后通过负载均衡算法, 在多个服务器之间选择⼀个进⾏访问.

客⼾端负载均衡:

在客⼾端进⾏负载均衡的算法分配.

把负载均衡的功能以库的⽅式集成到客⼾端, ⽽不再是由⼀台指定的负载均衡设备集中提供.

⽐如Spring Cloud的Ribbon, 请求发送到客⼾端, 客⼾端从注册中⼼(⽐如Eureka)获取服务列表, 在发送请求前通过负载均衡算法选择⼀个服务器,然后进⾏访问.

Ribbon是Spring Cloud早期的默认实现, 由于不维护了, 所以最新版本的Spring Cloud负载均衡集成的是Spring Cloud LoadBalancer(Spring Cloud官⽅维护)

客⼾端负载均衡和服务端负载均衡最⼤的区别在于服务清单所存储的位置

Spring Cloud LoadBalancer:

Spring Cloud LoadBalancer官网

使⽤Spring Cloud LoadBalancer实现负载均衡:

给 RestTemplate 这个Bean添加 @LoadBalanced 注解

修改IP端⼝号为服务名称

    public OrderInfo selectOrderById(Integer orderId){
        OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
        String url = "http://product-service/product/"+orderInfo.getProductId();
        log.info("远程调用url:{}",url);
        ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
        orderInfo.setProductInfo(productInfo);
        return orderInfo;
    }

测试负载均衡,连续多次发起请求: http://127.0.0.1:8080/order/1

观察product-service的⽇志, 会发现请求被分配到这3个实例上了

负载均衡策略:

负载均衡策略是⼀种思想, ⽆论是哪种负载均衡器, 它们的负载均衡策略都是相似的. Spring Cloud LoadBalancer 仅⽀持两种负载均衡策略: 轮询策略 和 随机策略

1. 轮询(Round Robin)(默认): 轮询策略是指服务器轮流处理⽤⼾的请求. 这是⼀种实现最简单, 也最常⽤的策略.

2. 随机选择(Random): 随机选择策略是指随机选择⼀个后端服务器来处理新的请求.

⾃定义负载均衡策略:

Spring Cloud LoadBalancer 默认负载均衡策略是 轮询策略, 实现是 RoundRobinLoadBalancer,

如果使用随机选择需要:

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

public class CustomLoadBalancerConfiguration {

    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
                                                            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(loadBalancerClientFactory
                .getLazyProvider(name, ServiceInstanceListSupplier.class),
                name);
    }
}

使⽤ @LoadBalancerClient 或者 @LoadBalancerClients 注解

在 RestTemplate 配置类上⽅, 使⽤ @LoadBalancerClient 或 @LoadBalancerClients 注解, 可以对不同的服务提供⽅配置不同的客⼾端负载均衡算法策略.

由于咱们项⽬中只有⼀个服务提供者, 所以使⽤@LoadBalancerClient

@LoadBalancerClient 注解说明

1. name: 该负载均衡策略对哪个服务⽣效(服务提供⽅)

2. configuration : 该负载均衡策略 ⽤哪个负载均衡策略实现.

LoadBalancer 原理:

LoadBalancer 的实现, 主要是 LoadBalancerInterceptor , 这个类会对 RestTemplate 的请求进⾏拦截, 然后从Eureka根据服务id获取服务列表,随后利⽤负载均衡算法得到真实的服务地址信息,替换服务id。我们来看看源码实现:

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

	private LoadBalancerClient loadBalancer;

	private LoadBalancerRequestFactory requestFactory;

	public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
		this.loadBalancer = loadBalancer;
		this.requestFactory = requestFactory;
	}

	public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
		// for backwards compatibility
		this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
	}

	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();
		String serviceName = originalUri.getHost();
		Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
		return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
	}

}

可以看到这⾥的intercept⽅法, 拦截了⽤⼾的HttpRequest请求,然后做了⼏件事:

1. request.getURI() 从请求中获取uri, 也就是 http://product-service/product/1001

2. originalUri.getHost() 从uri中获取路径的主机名, 也就是服务id, product-service

3. loadBalancer.execute 根据服务id, 进⾏负载均衡, 并处理请求

其中execute实现:

@Override
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
		String hint = getHint(serviceId);
		LoadBalancerRequestAdapter<T, TimedRequestContext> lbRequest = new LoadBalancerRequestAdapter<>(request,
				buildRequestContext(request, hint));
		Set<LoadBalancerLifecycle> supportedLifecycleProcessors = getSupportedLifecycleProcessors(serviceId);
		supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
		ServiceInstance serviceInstance = choose(serviceId, lbRequest);
		if (serviceInstance == null) {
			supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
					new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, new EmptyResponse())));
			throw new IllegalStateException("No instances available for " + serviceId);
		}
		return execute(serviceId, serviceInstance, lbRequest);
	}

返回服务的Instance的方法choose:

/**
* 根据serviceId,和负载均衡策略, 选择处理的服务
*
*/
public <T> ServiceInstance choose(String serviceId, Request<T> request) {
//获取负载均衡器
    ReactiveLoadBalancer<ServiceInstance> loadBalancer = this.loadBalancerClientFactory.getInstance(serviceId);
    if (loadBalancer == null) {
        return null;
    } else {
    //根据负载均衡算法, 在列表中选择⼀个服务实例
        Response<ServiceInstance> loadBalancerResponse = (Response)Mono.from(loadBalancer.choose(request)).block();
        return loadBalancerResponse == null ? null :(ServiceInstance)loadBalancerResponse.getServer();
    }
}

这个方法里还有一个choose会有两个实现随机和轮询,然后进行实现

服务部署(Linux):

接下来我们把服务部署在Linux系统上

1.准备数据,安装mysql,数据初始化

2.修改配置⽂件中, 数据库的密码

3.服务构建打包:

采⽤Maven打包, 需要对3个服务分别打包: eureka-service, order-service, product-service

打包⽅式和SpringBoot项⽬⼀致, 依次对三个项⽬打包即可.

4.启动服务

上传Jar包到云服务器,第⼀次上传需要安装lrzsz: apt install lrzsz

直接拖动⽂件到xshell窗⼝, 上传成功.

#后台启动eureka-service, 并设置输出⽇志到logs/eureka.log

nohup java -jar eureka-service.jar >logs/eureka.log &

#后台启动order-service, 并设置输出⽇志到logs/order.log

nohup java -jar order-service.jar >logs/order.log &

#后台启动product-service, 并设置输出⽇志到logs/order.log

nohup java -jar product-service.jar >logs/product-9090.log &

再多启动两台product-service实例

#启动实例, 指定端⼝号为9091

nohup java -jar product-service.jar --server.port=9091 >logs/product-9091.log &

#启动实例, 指定端⼝号为9092

nohup java -jar product-service.jar --server.port=9092 >logs/product-9092.log & 

5.开放端⼝号

根据⾃⼰项⽬设置的情况, 在云服务器上开放对应的端⼝号,不同的服务器⼚商, 开放端⼝号的⼊⼝不同, 需要⾃⾏找⼀找或者咨询对应的客服⼈员.

6. 测试

1. 访问Eureka Server:

2. 访问订单服务接⼝:

远程调⽤成功.

3.Nacos注册中心实现

仓库链接​​​​​​https://github.com/alibaba/nacos                   官网链接https://nacos.io/

2018年6⽉, Eureka 2.0宣布闭源(但是1.X版本仍然为活跃项⽬), 同年7⽉份, 阿⾥Nacos宣布开源. 并快速成为国内最受关注开源产品. 作为Eureka的替代, Nacos已经成为了国内开发者的⾸选, ⽬前Nacos Star 已经突破28K(Eureka 12K)
Nacos (Dynamic Naming and Configuration Service)在最初开源时, Nacos选择进⾏内部三个产品合并统⼀开源(Configserver ⾮持久注册中⼼,VIPServer 持久化注册中⼼,Diamond 配置中⼼). 定位为:⼀个更易于构建云原⽣应⽤的动态服务发现, 配置管理和服务管理平台. 所以Nacos是⼀个注册中⼼组件, 但它⼜不仅仅是注册中⼼组件.截⾄⽬前, Nacos⼏乎⽀持了所有的主流语⾔, ⽐如 Java, Go, C++, Nodejs, Python, Scala等

Nacos安装:

⽬前官⽅推荐的稳定版本为2.2.3

下载地址https://github.com/alibaba/nacos/releases/tag/2.2.3其他版本下载链接

Windows环境下:

⽬录介绍: bin: Nacos启停脚本

• startup.cmd :windows平台的启动脚本

• startup.sh :Linux平台的启动脚本

• shutdown.cmd : windows平台的停⽌脚本

• shutdown.sh : Linux平台的停⽌脚本

conf: Nacos配置⽂件

target: 存放 Nacos 应⽤的 jar 包

修改单机模式:

Nacos 默认启动⽅式为集群, 启动前需要修改配置为单机模式.

1. 使⽤记事本打开 startup.cmd

2. Line 26左右, 修改启动模式

改为set MODE="standalone"

启动Nacos:

启动⾮常简单, 进⼊bin⽬录下, 双击 startup.cmd 即可

访问Nacos主⻚, 出现以下界⾯, 表⽰Nacos启动成功 http://127.0.0.1:8848/nacos

常⻅问题:

Nacos启动后, ⽬录下会多⼀个logs的⽂件夹 报错⽇志在: logs/nacos.log

集群模式启动,报错⽇志:

Caused by: java.net.UnknownHostException: jmenv.tbsite.net
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:572)
at java.base/java.net.Socket.connect(Socket.java:633)
at java.base/sun.net.NetworkClient.doConnect(NetworkClient.java:178)
at
java.base/sun.net.www.http.HttpClient.openServer(HttpClient.java:534)
at
java.base/sun.net.www.http.HttpClient.openServer(HttpClient.java:639)
at java.base/sun.net.www.http.HttpClient.<init>(HttpClient.java:282)
at java.base/sun.net.www.http.HttpClient.New(HttpClient.java:387)
at java.base/sun.net.www.http.HttpClient.New(HttpClient.java:409)
at
java.base/sun.net.www.protocol.http.HttpURLConnection.getNewHttpClient(HttpURLC
onnection.java:1309)
at
java.base/sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConn
ection.java:1242)
at
java.base/sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConne
ction.java:1128)
at
java.base/sun.net.www.protocol.http.HttpURLConnection.connect(HttpURLConnection
.java:1057)
at
com.alibaba.nacos.common.http.client.request.JdkHttpClientRequest.execute(JdkHt
tpClientRequest.java:114)
at
com.alibaba.nacos.common.http.client.NacosRestTemplate.execute(NacosRestTemplat
e.java:482)
at
com.alibaba.nacos.common.http.client.NacosRestTemplate.get(NacosRestTemplate.ja
va:72)
at
com.alibaba.nacos.core.cluster.lookup.AddressServerMemberLookup.syncFromAddress
Url(AddressServerMemberLookup.java:175)
at
com.alibaba.nacos.core.cluster.lookup.AddressServerMemberLookup.run(AddressServ
erMemberLookup.java:143)
... 126 common frames omitted
2023-12-25 12:14:54,260 WARN [HttpClientBeanHolder] Start destroying common
HttpClient
2023-12-25 12:14:54,260 WARN [ThreadPoolManager] Start destroying ThreadPool

端⼝号冲突,Nacos 默认端⼝号是8848, 如果该端⼝号被其他应⽤占⽤, 启动会报错:

Caused by: java.net.BindException: Address already in use: bind
at java.base/sun.nio.ch.Net.bind0(Native Method)
at java.base/sun.nio.ch.Net.bind(Net.java:555)
at
java.base/sun.nio.ch.ServerSocketChannelImpl.netBind(ServerSocketChannelImpl.ja
va:337)
at
java.base/sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:
294)
at
io.grpc.netty.shaded.io.netty.channel.socket.nio.NioServerSocketChannel.doBind(
NioServerSocketChannel.java:141)
at
io.grpc.netty.shaded.io.netty.channel.AbstractChannel$AbstractUnsafe.bind(Abstr
actChannel.java:562)
at
io.grpc.netty.shaded.io.netty.channel.DefaultChannelPipeline$HeadContext.bind(D
efaultChannelPipeline.java:1334)
at
io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.invokeBind(
AbstractChannelHandlerContext.java:506)
at
io.grpc.netty.shaded.io.netty.channel.AbstractChannelHandlerContext.bind(Abstra
ctChannelHandlerContext.java:491)
at
io.grpc.netty.shaded.io.netty.channel.DefaultChannelPipeline.bind(DefaultChanne
lPipeline.java:973)
at
io.grpc.netty.shaded.io.netty.channel.AbstractChannel.bind(AbstractChannel.java
:260)
at
io.grpc.netty.shaded.io.netty.bootstrap.AbstractBootstrap$2.run(AbstractBootstr
ap.java:356)
at
io.grpc.netty.shaded.io.netty.util.concurrent.AbstractEventExecutor.runTask(Abs
tractEventExecutor.java:174)
at
io.grpc.netty.shaded.io.netty.util.concurrent.AbstractEventExecutor.safeExecute
(AbstractEventExecutor.java:167)
at
io.grpc.netty.shaded.io.netty.util.concurrent.SingleThreadEventExecutor.runAllT
asks(SingleThreadEventExecutor.java:470)
at
io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:50
3)
at
io.grpc.netty.shaded.io.netty.util.concurrent.SingleThreadEventExecutor$4.run(S
ingleThreadEventExecutor.java:997)
at
io.grpc.netty.shaded.io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecu
torMap.java:74)
at
io.grpc.netty.shaded.io.netty.util.concurrent.FastThreadLocalRunnable.run(FastT
hreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:842)
2023-12-25 12:22:05,267 WARN [HttpClientBeanHolder] Start destroying common
HttpClient

解决方式:

1.关闭该进程

netstat -ano|findstr "8848";  taskkill /pid 4968 -f

2.修改Nacos端⼝号

修改⽂件: ${Nacos⽬录}/conf/application.properties 23⾏左右,修改8848为期望的端⼝号即可.

Linux:

上传提前下载好的安装包到服务器上某个⽬录

解压安装包

unzip nacos-server-2.2.3.zip

如果第⼀次使⽤, 未安装unzip命令, 需要安装⼀下

apt-get install unzip

解压后⽬录如下:

root@hcss-ecs-0bb1:/usr/local/src/nacos# pwd
/usr/local/src/nacos
root@hcss-ecs-0bb1:/usr/local/src/nacos# ll
total 44
drwxr-xr-x 5 root root 4096 May 25 2023 ./
drwxr-xr-x 3 root root 4096 Dec 25 15:07 ../
drwxr-xr-x 2 root root 4096 May 25 2023 bin/
drwxr-xr-x 2 root root 4096 May 25 2023 conf/
-rw-r--r-- 1 root root 16583 Mar 6 2023 LICENSE
-rw-r--r-- 1 root root 1305 May 14 2020 NOTICE
drwxr-xr-x 2 root root 4096 May 25 2023 target/

单机模式启动:

可以进入到conf中使用 vim application.properties 编辑文件 将端口号改为10020,:wq保存修改

进⼊nacos/bin⽬录, 输⼊命令:

bash startup.sh -m standalone

上述命令为Ubuntu系统的命令

启动成功后, 访问Nacos链接: http://IP:port/nacos

10020为修改后的端⼝号, 需要在服务器上开放对应的端⼝号

另外, 再开放 Nacos端⼝号 +1000 和 Nacos端⼝号+1001 的端⼝

⽐如端⼝号为10020, 则需要开放端⼝号为: 10020, 11020, 11021

端⼝号为8848, 则需要开放端⼝号为: 9848, 9849

nacos实现:

Nacos是Spring Cloud Alibaba的组件, Spring Cloud Alibaba遵循Spring Cloud中定义的服务注册, 服务发现规范. 因此使⽤Nacos和使⽤Eureka对于微服务来说,并没有太⼤区别.

主要差异在于:

• Eureka需要⾃⼰搭建⼀个服务, Nacos不⽤⾃⼰搭建服务, 组件已经准备好了, 只需启动即可.

• 对应依赖和配置不同

操作参考链接https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-discovery

服务注册/服务发现:

Nacos的服务注册和服务发现代码⼀样

引⼊Spring Cloud Alibaba依赖:

在⽗⼯程的pom⽂件中的 <dependencyManagement> 中引⼊Spring Cloud Alibaba的依赖:

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

( 注意: Spring Boot 和Spring Cloud的版本是有⼀定对应关系的. Spring Cloud Alibaba也遵循Spring Cloud 的标准, 在引⼊依赖时, ⼀定要确认各个版本的对应关系.Spring Cloud Alibaba 和SpringCloud版本对应关系, 参考官⽅⽂档: 、https://sca.aliyun.com/zhcn/docs/2022.0.0.0/overview/version-explain/   版本在⼀定范围内可以⾃由选择. )

引⼊Nacos 依赖:

在order-service和product-service中引⼊nacos依赖

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

引⼊Load Balance依赖

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

配置Nacos地址

spring:
    application:
        name: product-service
    cloud:
        nacos:
            discovery:
                server-addr: IP:Port

远程调⽤:

修改IP为项⽬名:

public OrderInfo selectOrderById(Integer orderId){
        OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
        String url = "http://product-service/product/"+orderInfo.getProductId();
        log.info("远程调用url:{}",url);
        ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
        orderInfo.setProductInfo(productInfo);
        return orderInfo;
    }

为restTemplate添加负载均衡注解 @LoadBalanced

@Configuration
public class BeanConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

启动服务

启动两个服务, 观察Nacos的管理界⾯, 发现order-service 和product-service 都注册在Nacos上了
测试接⼝: http://127.0.0.1:8080/order/1

启动多个服务, 测试负载均衡

启动三个product-service服务 多次访问接⼝, 观察⽇志 http://127.0.0.1:8080/order/1

常⻅问题:

java.net.UnknownHostException  检查是否添加 LoadBalance 依赖
服务注册失败 检查Spring Cloud Alibaba版本是否正确

参考:
版本发布说明 | Spring Cloud Alibaba

Nacos负载均衡:

⽣产环境相对是⽐较恶劣的, 我们需要对服务的流量进⾏更加精细的控制. Nacos⽀持多种负载均衡策略, 包括权重, 同机房, 同地域, 同环境等.

服务下线:

当某⼀个节点上接⼝的性能较差时, 我们可以第⼀时间对该节点进⾏下线. 操作步骤: 服务详情 -> 下线

点击下线后, 再次请求接⼝, 会发现该服务没有请求进来了 再次单击上线, 该节点会继续收到请求.
权重配置.除了下线之外, 我们也可以配置这个节点的流量权重

配置权重

操作步骤: 找到对应节点 ->编辑 -> 在弹出的窗⼝修改权重值

每个节点默认权重为1, 修改为0.1

开启Nacos负载均衡策略

由于Spring Cloud LoadBalance组件⾃⾝有负载均衡配置⽅式, 所以不⽀持Nacos的权重属性配置. 我们需要开启Nacos的负载均衡策略, 让权重配置⽣效

如何解决MSE Nacos上修改服务实例的权重不⽣效问题_微服务引擎(MSE)-阿⾥云帮助中⼼

#开启nacos的负载均衡策略
spring.cloud.loadbalancer.nacos.enabled=true
spring:
    cloud:
        loadbalancer:
            nacos:
                enabled: true

测试权重配置,启动服务, 访问多次接⼝, 观察结果, 会发现9091端⼝号的实例接收的请求明显⽐另外两个实例少,整体流量⽣效, 局部流量不是严格按照设置的⽐例进⾏分配的

常⻅问题

修改权重时, 可能会报错:

原因: Nacos 采⽤ raft 算法来计算 Leader, 并且会记录前⼀次启动的集群地址, 当服务器 IP 改变时会导致 raft 记录的集群地址失效, 导致选 Leader 出现问题. (⽹络环境发⽣变化时, IP地址也会发⽣变化)

解决办法: 删除 Nacos 根⽬录下 data ⽂件夹下的 protocol ⽂件夹后重启即可.

同集群优先访问:

一般在部署的时候,至少部署两个实例,分别在两个不同的机房上,防止机房不能正常运行

Nacos把同⼀个机房内的实例, 划分为⼀个集群. 所以同集群优先访问, 在⼀定程度上也可以理解为同机房优先访问.

微服务架构中, ⼀个服务通常有多个实例共同提供服务, 这些实例可以部署在不同的机器上, 这些机器可以分布在不同的机房, ⽐如product-service:

实例1: 分布在上海机房 实例2: 分布在上海机房 实例3: 分布在北京机房 实例4: 分布在北京机房

微服务访问时, 应尽量访问同机房的实例. 当本机房内实例不可⽤时, 才访问其他机房的实例.

⽐如order-service 在上海机房, product-service 在北京和上海机房都有实例, 那我们希望可以优先访问上海机房, 如果上海机房没有实例, 或者实例不可⽤, 再访问北京机房的实例. 通常情况下, 因为同⼀个机房的机器属于⼀个局域⽹, 局域⽹访问速度更快⼀点

给实例配置集群名称

1. 为product-service配置集群名称

spring:
    cloud:
        nacos:
            discovery:
                server-addr: 110.41.51.65:10020
                cluster-name: SH #集群名称: 上海集群

重启服务, 观察Nacos控制台, SH集群下多了⼀个实例

复制product-service启动配置, 添加VM Option 设置9091端⼝号的实例, 机房为BJ

-Dserver.port=9091 -Dspring.cloud.nacos.discovery.cluster-name=BJ

设置9092端⼝号的实例, 机房为BJ

-Dserver.port=9091 -Dspring.cloud.nacos.discovery.cluster-name=BJ

观察Nacos, BJ集群下多了⼀个实例

2. 为order-service配置集群名称: SH

开启Nacos负载均衡策略

#开启nacos的负载均衡策略
spring.cloud.loadbalancer.nacos.enabled=true
spring:
    cloud:
        loadbalancer:
            nacos:
                enabled: true

启动服务

对接⼝访问多次, 观察⽇志, 会发现只有9090端⼝的实例收到了请求(同集群)

把9090端⼝的实例进⾏下线(SH集群), 再次访问接⼝, 观察⽇志, 发现9091端⼝和9092端⼝的实例收到了请求

Nacos 健康检查:

两种健康检查机制:

Nacos作为注册中⼼, 需要感知服务的健康状态, 才能为服务调⽤⽅提供良好的服务. Nacos 中提供了两种健康检查机制:

客⼾端主动上报机制:

• 客⼾端通过⼼跳上报⽅式告知服务端(nacos注册中⼼)健康状态, 默认⼼跳间隔5秒;

• nacos会在超过15秒未收到⼼跳后将实例设置为不健康状态, 超过30秒将实例删除

服务器端反向探测机制:

• nacos主动探知客⼾端健康状态, 默认间隔为20秒.

• 健康检查失败后实例会被标记为不健康, 不会被⽴即删除.

Nacos 中的健康检查机制不能主动设置,健康检查机制是和 Nacos 的服务实例类型强相关的.

Nacos服务实例类型:

Nacos的服务实例(注册的节点)分为临时实例和⾮临时实例.

• 临时实例: 如果实例宕机超过⼀定时间, 会从服务列表剔除, 默认类型.

• ⾮临时实例: 如果实例宕机, 不会从服务列表剔除, 也可以叫永久实例

Nacos对临时实例, 采取的是 客⼾端主动上报机制, 对⾮临时实例, 采取服务器端反向探测机制.

配置⼀个服务实例为永久实例:

spring:
    cloud:
        nacos:
            discovery:
                ephemeral: false # 设置为⾮临时实例

重启服务, 观察Nacos控制台

停⽌服务, 再观察控制台,节点依然不会消失.

常⻅问题:

Nacos服务实例类型不允许改变

设置服务实例类型, 重新启动Nacos可能会报错

原因: Nacos会记录每个服务实例的IP和端⼝号, 当发现IP和端⼝都没有发⽣变化时, Nacos不允许⼀个服务实例类型发⽣变化, ⽐如从临时实例,变为⾮临时实例, 或者从⾮临时实例, 变成临时实例.

解决办法:

1. 停掉nacos

2. 删除nacos ⽬录下 /data/protocol/raft 信息, ⾥⾯会保存应⽤实例的元数据信息.

服务正常, Nacos健康检查失败

现象:   服务正常, 但是Nacos显⽰健康状态为false

原因和解决办法:

如何解决Nacos持久化实例HTTP/TCP的健康检查不通过问题_微服务引擎(MSE)-阿⾥云帮助中⼼

Nacos环境隔离:

企业开发中, ⼀个服务会分为开发环境, 测试环境和⽣产环境(预发布环境和发布环境).

1. 开发环境:开发⼈员⽤于开发的服务器, 是最基础的环境. ⼀般⽇志级别设置较低, 可能会开启⼀些调试信息.

2. 测试环境:测试⼈员⽤来进⾏测试的服务器, 是开发环境到⽣产环境的过渡环境.

3. ⽣产环境:正式提供对外服务的环境, 通常关掉调试信息.

通常情况下, 这⼏个环境是不能互相通信的. Nacos提供了namespace(命名空间)来实现环境的隔离. 不同的namaspace的服务不可⻅.

创建Namespace:

默认情况下,所有服务都在同⼀个namespace,名为public

点击左侧命名空间, 可以对namespace进⾏操作

新增命名空间

配置namespace

namespace创建完成后, 对服务进⾏配置

配置项Key默认值说明
命名空间spring.cloud.nacos.discovery.nam espace常⽤场景之⼀是不同环境的注册的区分隔离,例如开发测试环境和⽣产环境的资源(如配置,服务)隔离等

修改order-service的命名空间:

spring:
    cloud:
        nacos:
            discovery:
                namespace: 51152a13-7911-49e3-bbdc-16fd5670a257

测试远程调⽤

1. 启动服务, 观察Nacos控制台

public 命名空间下只有product-service服务

order-service在dev命名空间下

2. 访问接⼝, 测试远程调⽤

测试时会发现服务报错,因为不在同一个环境下

修改product-service的其中⼀个实例, 命名空间改为dev

启动服务,观察Nacos控制台

Nacos配置中⼼:

除了注册中⼼和负载均衡之外, Nacos还是⼀个配置中⼼, 具备配置管理的功能.

Namespace 的常⽤场景之⼀是不同环境的配置区分隔离. 例如开发测试环境和⽣产环境的配置隔离.

为什么需要配置中⼼?

当前项⽬的配置都在代码中, 会存在以下问题:

1. 配置⽂件修改时, 服务需要重新部署. 微服务架构中, ⼀个服务可能有成百个实例, 挨个部署⽐较⿇烦, 且容易出错.

2. 多⼈开发时, 配置⽂件可能需要经常修改, 使⽤同⼀个配置⽂件容易冲突.

配置中⼼就是对这些配置项进⾏统⼀管理. 通过配置中⼼, 可以集中查看, 修改和删除配置, ⽆需再逐个修改配置⽂件. 提⾼效率的同时, 也降低了出错的⻛险.

1. 服务启动时, 从配置中⼼读取配置项的内容, 进⾏初始化.

2. 配置项修改时, 通知微服务, 实现配置的更新加载.

快速上⼿

通过以下操作, 我们先来感受下Nacos 配置中⼼的使⽤:

参考⽂档: Nacos Spring Cloud 快速开始 Nacos config

添加配置:

在Nacos控制台添加配置项

💡 注意: 配置管理的命名空间和服务列表的命名空间是隔离的, 两个是分别设置的. 默认是public 也就是服务管理命名空间配置 ≠ 配置管理的命名空间

新建配置项

配置内容:

nacos.test.num=5

说明:

1. Data ID 设置为项⽬名称

2. 配置内容的数据格式, ⽬前只⽀持 properties 和 yaml 类型

3. 设置配置内容

获取配置:

1. 引⼊Nacos Config依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- SpringCloud 2020.*之后版本需要引⼊bootstrap-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

2. 配置bootstrap.properties

微服务启动前, 需要先获取nacos中配置, 并与application.yml配置合并. 在微服务运⾏之前, Nacos要求必须使⽤ bootstrap.properties 配置⽂件来配置Nacos Server 地址.

spring.application.name=product-service
spring.cloud.nacos.config.server-addr=110.41.51.65:10020

或者使⽤bootstrap.yml

spring:
    application:
        name: product-service
    cloud:
        nacos:
            config:
                server-addr: 110.41.51.65:10020

spring.application.name 需要和nacos配置管理的Data ID⼀致

spring.cloud.nacos.config.server-addr 为Nacos Server的地址

💡 配置中⼼和注册中⼼的配置是隔离的

Nacos 配置中⼼: spring.cloud.nacos.config.server-addr

Nacos 注册中⼼: spring.cloud.nacos.discovery.server-addr

3. 编写程序

@RefreshScope
@RestController
public class NacosController {
    @Value("${nacos.config}")
    private String nacosConfig;

    @RequestMapping("/getConfig")
    public String getConfig(){
        return "从Nacos中获取配置项nacos.config:"+nacosConfig;
    }

}

• @Value 读取配置

• @RefreshScope 配置进⾏热更新

4. 测试

启动程序, 访问接⼝: http://127.0.0.1:9090/getConfig

常⻅问题:

1. 读取不到配置项

可能原因:

◦ 配置错误: 检查配置Data ID, 配置格式, 配置空间等

◦ 未引⼊依赖

2. No spring.config.import property has been defined

启动报错⽇志:

***************************
APPLICATION FAILED TO START
***************************
Description:
No spring.config.import property has been defined
Action:
Add a spring.config.import=nacos: property to your configuration.
If configuration is not required add
spring.config.import=optional:nacos: instead.
To disable this check, set spring.cloud.nacos.config.importcheck.enabled=false

原因: bootstrap.properties 是系统级的资源配置⽂件, ⽤于程序执⾏更加早期配置信息读取. 但是SpringCloud 2020.* 之后的版本把bootstrap禁⽤了, 导致在读取⽂件的时候读取不到⽽报错, 所以需要重新导⼊bootstrap 包进来就可以了

3. Nacos Server地址配置错误

报错信息如下:

2023-12-28T17:12:53.070+08:00 ERROR 14356 --- [t.remote.worker]
c.a.n.c.remote.client.grpc.GrpcClient : Server check fail, please check
server 127.0.0.1 ,port 11020 is available , error ={}
java.util.concurrent.ExecutionException:
com.alibaba.nacos.shaded.io.grpc.StatusRuntimeException: UNAVAILABLE: io
exception
at
com.alibaba.nacos.shaded.com.google.common.util.concurrent.AbstractFuture.ge
tDoneValue(AbstractFuture.java:566) ~[nacos-client-2.2.1.jar:na]
at
com.alibaba.nacos.shaded.com.google.common.util.concurrent.AbstractFuture.ge
t(AbstractFuture.java:445) ~[nacos-client-2.2.1.jar:na]
// ...
Caused by: com.alibaba.nacos.shaded.io.grpc.StatusRuntimeException:
UNAVAILABLE: io exception
at
com.alibaba.nacos.shaded.io.grpc.Status.asRuntimeException(Status.java:539)
~[nacos-client-2.2.1.jar:na]
at
com.alibaba.nacos.shaded.io.grpc.stub.ClientCalls$UnaryStreamToFuture.onClos
e(ClientCalls.java:544) ~[nacos-client-2.2.1.jar:na]
at
com.alibaba.nacos.shaded.io.grpc.internal.DelayedClientCall$DelayedListener$
3.run(DelayedClientCall.java:471) ~[nacos-client-2.2.1.jar:na]
// ...
Caused by:
com.alibaba.nacos.shaded.io.grpc.netty.shaded.io.netty.channel.AbstractChann
el$AnnotatedConnectException: Connection refused: no further information:
/127.0.0.1:11020
Caused by: java.net.ConnectException: Connection refused: no further
information
at java.base/sun.nio.ch.Net.pollConnect(Native Method) ~[na:na]
at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:672) ~[na:na]
at
java.base/sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:
946) ~[na:na]

配置中⼼详解

设置命名空间:

Nacos配置管理的命名空间和服务列表的命名空间是分别设置的. 默认是public

Nacos命名空间配置依然在bootstrap.properties中进⾏配置

spring.cloud.nacos.config.namespace=51152a13-7911-49e3-bbdc-16fd5670a257

对应bootstrap.yml配置

spring:
    cloud:
        nacos:
          config:
            server-addr: 51152a13-7911-49e3-bbdc-16fd5670a257

如果设置命名空间后, 项⽬启动时, 会从该命名空间下找对应的配置项.

重新访问接⼝, 观察结果: http://127.0.0.1:9090/getConfig

Data Id:

Data Id 格式介绍

在 Nacos Spring Cloud 中, dataId 的完整格式如下:

${prefix}-${spring.profiles.active}.${file-extension}

• prefix 默认为 spring.application.name 的值, 也可以通过配置项spring.cloud.nacos.config.prefix 来配置.

spring.profiles.active 即为当前环境对应的 profile. 当 spring.profiles.active为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${fileextension}

• file-exetension 为配置内容的数据格式,可以通过配置项spring.cloud.nacos.config.file-extension 来配置。⽬前只⽀持 properties和 yaml 类型. 默认为properties.

微服务启动时, 会从Nacos读取多个配置⽂件:

1. ${prefix}-${spring.profiles.active}.${file-extension} 如: product-servicedev.properties

2. ${prefix}.${file-extension} , 如: product-service.properties

3. ${prefix} 如product-service

💡 ${spring.application.name}, ${spring.profiles.active} 等通过配置⽂件来指定时, 必须放在 bootstrap.properties ⽂件中

观察⽇志:

在bootstrap.yml中添加 spring.profiles.active 值

spring:
    profiles:
        active: dev

启动服务, 观察⽇志

三个⽂件的优先级为: product-service-dev.properties > product-service.properties >product-service

测试:

配置项如下:

访问接⼝: http://127.0.0.1:9090/config/get

服务获取到了 product-service-dev.properties 的值

删除 product-service-dev.properties 配置, 再次访问接⼝

💡 注意:

1. bootstrap.yml 设置的配置格式必须和nacos控制台配置的数据格式保持⼀致.

2. 不设置配置格式(spring.cloud.nacos.config.file-extension)时, 默认为properties

服务部署:

1. 修改数据库, Nacos等相关配置

2. 对两个服务进⾏打包

3. 上传jar到Linux服务器

4. 启动Nacos

启动前最好把data数据删除掉.

5. 启动服务

#后台启动order-service, 并设置输出⽇志到logs/order.log

nohup java -jar order-service.jar >logs/order.log &

#后台启动product-service, 并设置输出⽇志到logs/order.log

nohup java -jar product-service.jar >logs/product-9090.log &

#启动实例, 指定端⼝号为9091

nohup java -jar product-service.jar --server.port=9091 >logs/product-9091.log &

观察Nacos控制台

6. 测试

访问接⼝: http://110.41.51.65:8080/order/1

Nacos与Eureka的区别:

共同点:

• 都⽀持服务注册和服务拉取

区别:

1. 功能

Nacos除了服务发现和注册之外, 还提供了配置中⼼, 流量管理和DNS服务等功能.

2. CAP理论

Eureka遵循AP原则, Nacos可以切换AP和CP模式,默认AP.

Nacos 根据配置识别CP或者AP模式. 如果注册Nacos的Client的节点是临时节点, 那么Nacos对这个Client节点的效果就是AP, 反之是CP. AP和CP可以同时混合存在.

3. 服务发现

Eureka:基于拉模式. Eureka Client会定期从Server拉取服务信息, 有缓存, 默认每30秒拉取⼀次. Nacos:基于推送模式. 服务列表有变化时实时推送给订阅者, 服务端和客⼾端保持⼼跳连接.

服务启动,服务停⽌:

4.优雅实现远程调⽤-OpenFeign

RestTemplate存在问题:

观察咱们远程调⽤的代码

public OrderInfo selectOrderById(Integer orderId) {
    OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
    String url = "http://product-service/product/"+ orderInfo.getProductId();
    ProductInfo productInfo = restTemplate.getForObject(url,ProductInfo.class);
    orderInfo.setProductInfo(productInfo);
    return orderInfo;
}

虽说RestTemplate 对HTTP封装后, 已经⽐直接使⽤HTTPClient简单⽅便很多, 但是还存在⼀些问题:

1. 需要拼接URL, 灵活性⾼, 但是封装臃肿, URL复杂时, 容易出错.

2. 代码可读性差, ⻛格不统⼀.

微服务之间的通信⽅式, 通常有两种: RPC 和 HTTP.

在SpringCloud中, 默认是使⽤HTTP来进⾏微服务的通信, 最常⽤的实现形式有两种:

• RestTemplate

• OpenFeign

RPC(Remote Procedure Call)远程过程调⽤,是⼀种通过⽹络从远程计算机上请求服务,⽽不需要了解底层⽹络通信细节。RPC可以使⽤多种⽹络协议进⾏通信, 如HTTP、TCP、UDP等, 并且在TCP/IP⽹络四层模型中跨越了传输层和应⽤层。简⾔之RPC就是像调⽤本地⽅法⼀样调⽤远程⽅法。

常⻅的RPC框架有:

1. Dubbo: Apache Dubbo 中⽂

2. Thrift : Apache Thrift - Home

3. gRPC: gRPC

OpenFeign介绍

OpenFeign 是⼀个声明式的 Web Service 客⼾端. 它让微服务之间的调⽤变得更简单, 类似controller调⽤service, 只需要创建⼀个接⼝,然后添加注解即可使⽤OpenFeign.

OpenFeign 的前⾝

Feign 是 Netflix 公司开源的⼀个组件.

• 2013年6⽉, Netflix发布 Feign的第⼀个版本 1.0.0

• 2016年7⽉, Netflix发布Feign的最后⼀个版本 8.18.0

2016年,Netflix 将 Feign 捐献给社区

• 2016年7⽉ OpenFeign 的⾸个版本 9.0.0 发布,之后⼀直持续发布到现在.

可以简单理解为 Netflix Feign 是OpenFeign的祖先, 或者说OpenFeign 是Netflix Feign的升级版. OpenFeign 是Feign的⼀个更强⼤更灵活的实现.

💡 我们现在⽹络上看到的⽂章, 或者公司使⽤的Feign, ⼤多都是OpenFeign.

Spring Cloud Feign:

Spring Cloud Feign 是 Spring 对 Feign 的封装, 将 Feign 项⽬集成到 Spring Cloud ⽣态系统中. 受 Feign 更名影响,Spring Cloud Feign 也有两个 starter

• spring-cloud-starter-feign

• spring-cloud-starter-openfeign

由于Feign的停更维护, 对应的, 我们使⽤的依赖是 spring-cloud-starter-openfeign

OpenFeign 官⽅⽂档: GitHub - OpenFeign/feign: Feign makes writing java http clients easier Spring Cloud Feign⽂档:Spring Cloud OpenFeign

快速上⼿

引⼊依赖

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

添加注解

在order-service的启动类添加注解 @EnableFeignClients , 开启OpenFeign的功能.

//@EnableFeignClients开启Feigng功能
@EnableFeignClients
@SpringBootApplication
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class,args);
    }
}

编写OpenFeign的客⼾端

@FeignClient(value = "product-service")
//@FeignClient(value = "product-service",path = "/product")
public interface ProductApi {
    @RequestMapping("/product/{productId}")
    ProductInfo getProductInfo(@PathVariable("productId") Integer productId);
}

基于SpringMVC的注解来声明远程调⽤的信息

@FeignClient 注解作⽤在接⼝上, 参数说明:

• name/value:指定FeignClient的名称, 也就是微服务的名称, ⽤于服务发现, Feign底层会使Spring Cloud LoadBalance进⾏负载均衡. 也可以使⽤ url 属性指定⼀个具体的url.

• path: 定义当前FeignClient的统⼀前缀.

远程调⽤

修改远程调⽤的⽅法:

@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private ProductApi productApi;

    public OrderInfo selectOrderById(Integer orderId){
        OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
//        String url = "http://product-service/product/"+orderInfo.getProductId();
//        ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
        ProductInfo productInfo = productApi.getProductInfo(orderInfo.getProductId());
        orderInfo.setProductInfo(productInfo);
        return orderInfo;
    }
}

测试

启动服务, 访问接⼝, 测试远程调⽤: http://127.0.0.1:8080/order/1

可以看出来, 使⽤Feign也可以实现远程调⽤.

Feign 简化了与HTTP服务交互的过程, 把REST客⼾端的定义转换为Java接⼝, 并通过注解的⽅式来声明请求参数,请求⽅式等信息, 使远程调⽤更加⽅便和间接.

OpenFeign参数传递:

通过观察, 我们也可以发现, Feign的客⼾端和服务提供者的接⼝声明⾮常相似

上⾯例⼦中, 演⽰了Feign 从URL中获取参数, 接下来演⽰下Feign参数传递的其他⽅式

只做代码演⽰, 不做功能

服务提供⽅ product-service

@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private ProductService productService;

    //url中的参数
    @RequestMapping("/{productId}")
    public ProductInfo getProduct(@PathVariable("productId") Integer productId){
        return  productService.selectProductById(productId);
    }

    //单个参数
    @RequestMapping("/p1")
    public String p1(Integer id){
        return "p1接收到参数:"+id;
    }

    //多个参数
    @RequestMapping("/p2")
    public String p2(Integer id,String name){
        return "p2接收到参数,id:"+id +",name:"+name;
    }

    //对象
    @RequestMapping("/p3")
    public String p3(ProductInfo productInfo){
        return "接收到对象, productInfo:"+productInfo;
    }

    //json格式
    @RequestMapping("/p4")
    public String p4(@RequestBody ProductInfo productInfo){
        return "接收到对象, productInfo:"+productInfo;
    }
}

Feign客⼾端

@FeignClient(value = "product-service",path = "/product")
//@FeignClient(value = "product-service",path = "/product")
public interface ProductApi {
    @RequestMapping("/{productId}")
    ProductInfo getProductInfo(@PathVariable("productId") Integer productId);

    //单个参数中这里@RequestParam 做参数绑定, 不能省略
    @RequestMapping("/p1")
    String p1(@RequestParam("id") Integer id);

    @RequestMapping("/p2")
    String p2(@RequestParam("id")Integer id,@RequestParam("name")String name);

    @RequestMapping("/p3")
    String p3(@SpringQueryMap ProductInfo productInfo);

    @RequestMapping("/p4")
    String p4(@RequestBody ProductInfo productInfo);

}

💡 注意: @RequestParam 做参数绑定, 不能省略

服务消费⽅order-service

@RequestMapping("/feign")
@RestController
public class TestFeignController {
    @Autowired
    private ProductApi productApi;

    @RequestMapping("/o1")
    public String o1(Integer id){
        return productApi.p1(id);
    }

    @RequestMapping("/o2")
    public String o2(@RequestParam("id")Integer id, @RequestParam("name")String
            name){
        return productApi.p2(id,name);
    }

    @RequestMapping("/o3")
    public String o3(ProductInfo productInfo){
        return productApi.p3(productInfo);
    }

    @RequestMapping("/o4")
    public String o4(@RequestBody ProductInfo productInfo){
        System.out.println(productInfo.toString());
        return productApi.p4(productInfo);
    }

}

最佳实践:

最佳实践, 其实也就是经过历史的迭代, 在项⽬中的实践过程中, 总结出来的最好的使⽤⽅式.

通过观察, 我们也能看出来, Feign的客⼾端与服务提供者的controller代码⾮常相似 Feign 客⼾端

Feign 继承⽅式:

Feign ⽀持继承的⽅式, 我们可以把⼀些常⻅的操作封装到接⼝⾥.

我们可以定义好⼀个接⼝, 服务提供⽅实现这个接⼝, 服务消费⽅编写Feign 接⼝的时候, 直接继承这个接⼝

具体参考:Spring Cloud OpenFeign Features :: Spring Cloud Openfeign

创建⼀个Module:

接⼝可以放在⼀个公共的Jar包⾥, 供服务提供⽅和服务消费⽅使⽤

引⼊依赖

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

编写接⼝

复制 ProductApi, ProductInfo 到product-api模块中

public interface ProductInterface {
    @RequestMapping("/{productId}")
    ProductInfo getProduct(@PathVariable("productId") Integer productId);

    //单个参数中这里@RequestParam 做参数绑定, 不能省略
    @RequestMapping("/p1")
    String p1(@RequestParam("id") Integer id);

    @RequestMapping("/p2")
    String p2(@RequestParam("id")Integer id,@RequestParam("name")String name);

    @RequestMapping("/p3")
    String p3(@SpringQueryMap ProductInfo productInfo);

    @RequestMapping("/p4")
    String p4(@RequestBody ProductInfo productInfo);

}

⽬录结构如下:

打Jar包

通过Maven打包

观察Maven本地仓库, Jar包是否打成功

服务提供⽅

服务提供⽅实现接⼝ ProductInterface

public class ProductController implements ProductInterface {
 ....

}

服务消费⽅

服务消费⽅继承ProductInterface

@FeignClient(value = "product-service", path = "/product")
public interface ProductApi extends ProductInterface {
}

测试,试远程调⽤,http://127.0.0.1:8080/order/1

Feign 抽取⽅式

官⽅推荐Feign的使⽤⽅式为继承的⽅式, 但是企业开发中, 更多是把Feign接⼝抽取为⼀个独⽴的模块(做法和继承相似, 但理念不同).

操作⽅法:

将Feign的Client抽取为⼀个独⽴的模块, 并把涉及到的实体类等都放在这个模块中, 打成⼀个Jar. 服务消费⽅只需要依赖该Jar包即可. 这种⽅式在企业中⽐较常⻅, Jar包通常由服务提供⽅来实现.

创建⼀个module

引⼊依赖

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

编写API

复制 ProductApi, ProductInfo 到product-api模块中

@FeignClient(value = "product-service",path = "/product")
//@FeignClient(value = "product-service",path = "/product")
public interface ProductApi {
    @RequestMapping("/{productId}")
    ProductInfo getProductInfo(@PathVariable("productId") Integer productId);

    //单个参数中这里@RequestParam 做参数绑定, 不能省略
    @RequestMapping("/p1")
    String p1(@RequestParam("id") Integer id);

    @RequestMapping("/p2")
    String p2(@RequestParam("id")Integer id,@RequestParam("name")String name);

    @RequestMapping("/p3")
    String p3(@SpringQueryMap ProductInfo productInfo);

    @RequestMapping("/p4")
    String p4(@RequestBody ProductInfo productInfo);

}

打Jar包

通过Maven打包

观察Maven本地仓库, Jar包是否打成功

服务消费⽅使⽤product-api

1. 删除 ProductApi, ProductInfo

2. 引⼊依赖

<dependency>
        <groupId>org.example</groupId>
        <artifactId>product-api</artifactId>
        <version>1.0-SNAPSHOT</version>
        <scope>compile</scope>
</dependency>

修改项⽬中ProductApi, ProductInfo的路径为product-api中的路径

3. 指定扫描类: ProductApi

在启动类添加扫描路径

@EnableFeignClients(basePackages = {"com.bite.api"})
@SpringBootApplication
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

也可以指定需要加载的Feign客⼾端

@EnableFeignClients(clients = {ProductApi.class})

测试远程调⽤ http://127.0.0.1:8080/order/1

服务部署:

1. 修改数据库, Nacos等相关配置

2. 对两个服务进⾏打包

Maven打包默认是从远程仓库下载的, product-api 这个包在本地, 有以下解决⽅案:

◦ 上传到Maven中央仓库(参考: 如何发布Jar包到Maven中央仓库, ⽐较⿇烦)[不推荐]

◦ 搭建Maven私服, 上传Jar包到私服[企业推荐]

◦ 从本地读取Jar包[个⼈学习阶段推荐]

前两种⽅法⽐较复杂, 咱们使⽤第三种⽅式

修改pom⽂件

        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                </configuration>
            </plugin>
        </plugins>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>product-api</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>system</scope>
            <systemPath>
                D:/JAVA/JAVAEE/Maven/.m2/repository/org/example/product-api/1.0-SNAPSHOT/product-api-1.0-SNAPSHOT.jar
            </systemPath>
        </dependency>

把D:/Maven/.m2/repository 改为本地仓库的路径

3. 上传jar到Linux服务器

4. 启动Nacos

启动前最好把data数据删除掉.

5. 启动服务

#后台启动order-service, 并设置输出⽇志到logs/order.log

nohup java -jar order-service.jar >logs/order.log &

#后台启动product-service, 并设置输出⽇志到logs/order.log

nohup java -jar product-service.jar >logs/product-9090.log &

#启动实例, 指定端⼝号为9091

nohup java -jar product-service.jar --server.port=9091 >logs/product-9091.log & 

观察Nacos控制台

6. 测试

访问接⼝: http://110.41.51.65:8080/order/1

观察远程调⽤的结果:

5.GateWay-统一接口服务:

⽹关介绍:

问题

前⾯, 我们通过Eureka, Nacos解决了服务注册, 服务发现的问题, 使⽤Spring Cloud LoadBalance解决了负载均衡的问题, 使⽤OpenFeign解决了远程调⽤的问题.

但是当前所有微服务的接⼝都是直接对外暴露的, 可以直接通过外部访问. 为了保证对外服务的安全性, 服务端实现的微服务接⼝通常都带有⼀定的权限校验机制. 由于使⽤了微服务, 原本⼀个应⽤的的多个模块拆分成了多个应⽤, 我们不得不实现多次校验逻辑. 当这套逻辑需要修改时, 我们需要修改多个应⽤, 加重了开发⼈员的负担.

针对以上问题, ⼀个常⽤的解决⽅案是使⽤API⽹关.

什么是API⽹关?

API⽹关(简称⽹关)也是⼀个服务, 通常是后端服务的唯⼀⼊⼝. 它的定义类似设计模式中的Facade模式(⻔⾯模式, 也称外观模式). 它就类似整个微服务架构的⻔⾯, 所有的外部客⼾端访问, 都需要经过它来进⾏调度和过滤.

⽹关核⼼功能:

权限控制: 作为微服务的⼊⼝, 对⽤⼾进⾏权限校验, 如果校验失败则进⾏拦截

动态路由: ⼀切请求先经过⽹关, 但⽹关不处理业务, ⽽是根据某种规则, 把请求转发到某个微服务

负载均衡: 当路由的⽬标服务有多个时, 还需要做负载均衡

限流: 请求流量过⾼时, 按照⽹关中配置微服务能够接受的流量进⾏放⾏, 避免服务压⼒过⼤.

常⻅⽹关实现

业界常⽤的⽹关⽅式有很多, 技术⽅案也较成熟, 其中不乏很多开源产品, ⽐如Nginx, Kong, Zuul, Spring Cloud Gateway等. 下⾯介绍两种常⻅的⽹关⽅案.

Zuul

Zuul 是 Netflix 公司开源的⼀个API⽹关组件, 是Spring Cloud Netflix ⼦项⽬的核⼼组件之⼀,它可以和 Eureka、Ribbon、Hystrix 等组件配合使⽤.

在Spring Cloud Finchley正式版之前, Spring Cloud推荐的⽹关是Netflix提供的Zuul(此处指Zuul 1.X). 然⽽Netflix在2018年宣布⼀部分组件进⼊维护状态, 不再进⾏新特性的开发. 这部分组件中就包含Zuul.

Spring Cloud Gateway

Spring Cloud Gateway 是Spring Cloud的⼀个全新的API⽹关项⽬, 基于Spring + SpringBoot等技术开发, ⽬的是为了替换掉Zuul. 旨在为微服务架构提供⼀种简单⽽有效的途径来转发请求, 并为他们提供横切关注点, ⽐如: 安全性, 监控/指标和弹性.

在性能⽅⾯, 根据官⽅提供的测试报告, Spring Cloud Gateway的RPS(每秒请求数)是Zuul的1.6倍. 测试报告参考: https://github.com/spencergibb/spring-cloud-gateway-bench

Spring Cloud Gateway实现

快速上⼿

我们通过以下的演⽰, 先来了解⽹关的基本功能

创建⽹关项⽬

API⽹关也是⼀个服务

引⼊⽹关依赖

    <dependencies>
        <!--⽹关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--基于nacos实现服务发现依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--负载均衡-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
    </dependencies>

编写启动类:

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

添加Gateway的路由配置:

创建application.yml⽂件, 添加如下配置:

server:
  port: 10030
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: 152.136.172.144:10020
    gateway:
      routes:
        - id: order-service #路由规则ID,随便起,不重复就行
          uri: lb://order-service/
          predicates:
            - Path=/order/** , /feign/**
        - id: product-service
          uri: lb://product-service/
          predicates:
            - Path=/product/**

配置字段说明:

• id : ⾃定义路由ID, 保持唯⼀

• uri: ⽬标服务地址, ⽀持普通URI 及 lb://应⽤注册服务名称 . lb表⽰负载均衡, 使⽤ lb:// ⽅式表⽰从注册中⼼获取服务地址.

• predicates: 路由条件, 根据匹配结果决定是否执⾏该请求路由, 上述代码中, 我们把符合Path规则的⼀切请求, 都代理到uri参数指定的地址.

测试,启动API⽹关服务:

1. 通过⽹关服务访问product-service

http://127.0.0.1:10030/product/1001

url符合 yml⽂件中配置的 /product/** 规则, 路由转发到product-service: http://productservice/product/1001

访问时, 观察⽹关⽇志, 可以看到⽹关服务从Nacos时获取服务列表;

2. 通过⽹关服务访问order-service

http://127.0.0.1:10030/order/1

url符合 yml⽂件中配置的 /order/** 规则, 路由转发到product-service: http://orderservice/product/1001

Route Predicate Factories(路由断⾔⼯⼚):

Predicate

Predicate是Java 8提供的⼀个函数式编程接⼝, 它接收⼀个参数并返回⼀个布尔值, ⽤于条件过滤, 请求参数的校验.

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
    //...
}

代码演⽰:

定义⼀个Predicate

/**
 * 接受一个字符串判断是否为空
 */
public class StringPredicate implements Predicate<String> {
    @Override
    public boolean test(String o) {
        return o==null||o.isEmpty();
    }
}

使⽤这个Predicate

public class PredicateTest {
    @Test
    public void test(){
        Predicate<String> predicate = new StringPredicate();
        System.out.println(predicate.test(""));
        System.out.println(predicate.test("123"));
    }
}

Predicate的其他写法

1) 内置函数

2) lambda写法

    /**
     * 匿名内部类
     */
    @Test
    public void test1(){
        Predicate<String> predicate = new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return s==null||s.isEmpty();
            }
        };
        System.out.println(predicate.test(""));
        System.out.println(predicate.test("123"));

    }

    /**
     * lambda表达式
     */
    @Test
    public void test2(){
        Predicate<String> predicate = s -> s==null||s.isEmpty();
        System.out.println(predicate.test(""));
        System.out.println(predicate.test("123"));
    }

    /**
     * negate的使用
     */
    @Test
    public void test3(){
        Predicate<String> predicate = s -> s==null||s.isEmpty();
        System.out.println(predicate.negate().test(""));
        System.out.println(predicate.negate().test("123"));
    }
    /**
     * or的使用
     * 判断字符串是aa或者bb
     */
    @Test
    public void test4(){
        Predicate<String> predicate1 = s -> s.equals("aa");
        Predicate<String> predicate2 = s -> s.equals("bb");
        System.out.println(predicate1.or(predicate2).test("aa"));
        System.out.println(predicate1.or(predicate2).test(""));

    }
    /**
     * and的使用
     * 字符串不为空且由数字组成
     */
    @Test
    public void test5(){
        Predicate<String> predicate1 = s -> s!=null&&!s.isEmpty();
        Predicate<String> predicate2 = s -> s!=null&&s.chars().allMatch(Character::isDigit);
        System.out.println(predicate1.and(predicate2).test("aa"));
        System.out.println(predicate1.and(predicate2).test("123"));
    }

Predicate 的其他⽅法:

• isEqual(Object targetRef) :⽐较两个对象是否相等,参数可以为Null

• and(Predicate other): 短路与操作,返回⼀个组成Predicate

• or(Predicate other) :短路或操作,返回⼀个组成Predicate

• test(T t) :传⼊⼀个Predicate参数,⽤来做判断

• negate() : 返回表⽰此Predicate逻辑否定的Predicate

⽅法说明
boolean test(T t)判断条件, 可以理解为 条件A 根据逻辑返回布尔值
Predicate<T> and(Predicate<? super T> other)条件A && 条件B当前Predicate的test⽅法 && other的test⽅法, 相当于进⾏两次判断
default Predicate<T> negate()! 条件A 对当前判断进⾏"!"操作,即取⾮操作
default Predicate<T> or(Predicate<? super T> other)条件A ||条件B 当前Predicate的test⽅法 || other的test⽅法

Route Predicate Factories (路由断⾔⼯⼚, 也称为路由谓词⼯⼚, 此处谓词表⽰⼀个函数), 在Spring Cloud Gateway中, Predicate提供了路由规则的匹配机制.

我们在配置⽂件中写的断⾔规则只是字符串, 这些字符串会被Route Predicate Factory读取并处理, 转变为路由判断的条件.

⽐如前⾯章节配置的 Path=/product/** , 就是通过Path属性来匹配URL前缀是 /product 的请求.

这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory来实现的

Spring Cloud Gateway 默认提供了很多Route Predicate Factory, 这些Predicate会分别匹配HTTP请求的不同属性, 并且多个Predicate可以通过and逻辑进⾏组合.

名称说明⽰例
After这个⼯⼚需要⼀个⽇期时间(Java的 ZonedDateTime对象), 匹配指定⽇期之后的请求

predicates:

- After=2017-01-20T17:42:47.789- 07:00[America/Denver]

Before匹配指定⽇期之前的请求

predicates:

- Before=2017-01-20T17:42:47.789- 07:00[America/Denver]

Betwee n匹配两个指定时间之间的请求 datetime2 的参数必须在 datetime1 之后

predicates:

- Between=2017-01-20T17:42:47.789- 07:00[America/Denver], 2017-01- 21T17:42:47.789-07:00[America/Denver]

Cookie请求中包含指定Cookie, 且该Cookie值符合指定的正则表达式

predicates:

- Cookie=chocolate, ch.p 

Header请求中包含指定Header, 且该Header值符合指定的正则表达式

predicates:

- Header=X-Request-Id, \d+ 

Host请求必须是访问某个host(根据请求中的Host 字段进⾏匹配)

predicates:

- Host=**.somehost.org,**.anotherhost.or g

Method匹配指定的请求⽅式

predicates:

- Method=GET,POST 

Path匹配指定规则的路径

predicates:

- Path=/red/{segment},/blue/{segment}

RemoteAddr请求者的IP必须为指定范围

predicates:

- RemoteAddr=192.168.1.1/24 

更多请参考:

https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway-server-webflux/request-predicates-factories.html

代码演⽰

1. 添加Predicate规则

在application.yml中添加如下规则

server:
  port: 10030
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: 152.136.172.144:10020
    gateway:
      routes:
        - id: order-service #路由规则ID,随便起,不重复就行
          uri: lb://order-service/
          predicates:
            - Path=/order/** , /feign/**
            - After=2017-01-20T17:42:47.789-07:00[America/Denver]
        - id: product-service
          uri: lb://product-service/
          predicates:
            - Path=/product/**

2. 测试

访问: http://127.0.0.1:10030/order/1

3. 修改时间, 再次访问

Gateway Filter Factories(⽹关过滤器⼯⼚)

Predicate决定了请求由哪⼀个路由处理, 如果在请求处理前后需要加⼀些逻辑, 这就是Filter(过滤器)的作⽤范围了.

Filter分为两种类型: Pre类型和Post类型.

Pre类型过滤器: 路由处理之前执⾏(请求转发到后端服务之前执⾏), 在Pre 类型过滤器中可以做鉴权, 限流等.

Post类型过滤器: 请求执⾏完成后, 将结果返回给客⼾端之前执⾏.

Spring Cloud Gateway 中内置了很多Filter, ⽤于拦截和链式处理web请求. ⽐如权限校验, 访问超时等设定.

Spring Cloud Gateway从作⽤范围上, 把Filter可分为GatewayFilter 和GlobalFilter.

GatewayFilter: 应⽤到单个路由或者⼀个分组的路由上. GlobalFilter: 应⽤到所有的路由上, 也就是对所有的请求⽣效.

GatewayFilter:

GatewayFilter 同 Predicate 类似, 都是在配置⽂件 application.yml 中配置,每个过滤器的逻辑都是固定的. ⽐如 AddRequestParameterGatewayFilterFactory 只需要在配置⽂件中写 AddRequestParameter , 就可以为所有的请求添加⼀个参数, 我们先通过⼀个例⼦来演⽰

GatewayFilter如何使⽤.

快速上⼿

1. 在application.yml中添加filter

server:
  port: 10030
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: 152.136.172.144:10020
    gateway:
      routes:
        - id: order-service #路由规则ID,随便起,不重复就行
          uri: lb://order-service/
          predicates:
            - Path=/order/** , /feign/**
            - After=2025-10-13T18:58:51.751454700+08:00[Asia/Shanghai]
          filters:
            - AddRequestParameter=userName, bite
        - id: product-service
          uri: lb://product-service/
          predicates:
            - Path=/product/**

该filter只添加在了 order-service 路由下, 因此只对order-service 路由⽣效,

2. 接收参数并打印

log.info("过滤器传过来的参数,userName:{}",userName);

3. 测试

重启服务,访问请求, 观察⽇志 http://127.0.0.1:10030/order/1

GatewayFilter说明:

 Spring Cloud Gateway提供了的Filter⾮常多, 下⾯列出⼀些常⻅过滤器的说明. 详细可参考官⽅⽂档:GatewayFilter Factories

名称说明⽰例
AddRequestHead er为当前请求添加Header- AddRequestHeader=X-Request-red, blue 参数: Header的名称及值
AddRequestPara meter为当前请求添加请求参数- AddRequestParameter=red, blue 参数: 参数的名称及值
AddResponseHea der为响应结果添加Header- AddResponseHeader=X-Response-Red, Blue 参数: Header的名称及值
RemoveRequestHeader从当前请求删除某个Header- RemoveRequestHeader=X-Request-Foo 参数: Header的名称
RemoveRespons eHeader从响应结果删除某个Header- RemoveResponseHeader=X-Response-Foo 参数: Header的名称
RequestRateLimi ter为当前⽹关的所有请求执⾏限流过滤, 如果被限流, 默认会响应HTTP 429-Too ManyRequests 默认提供了RedisRateLimiter的限流实现, 采⽤令牌桶算法实现限流功能. 此处不做具体介绍
filters:
    - name: RequestRateLimiter
        args:
            redis-ratelimiter.replenishRate: 10
            redis-ratelimiter.burstCapacity: 20
            redis-ratelimiter.requestedTokens: 1

redis-rate-limiter.replenishRate : 令牌填充速度, 即每秒钟允许多少个请求(不丢弃任何请求) redis-rate-limiter.burstCapacity : 令牌桶容量, 即每秒⽤⼾最⼤能够执⾏的请求数量(不丢弃任何请求). 将此值设置为零将阻⽌所有请求

redis-rate-limiter.requestedTokens  : 每次请求占⽤
⼏个令牌, 默认为  1 。

Retry针对不同的响应进⾏重试. 当后端服务不可⽤时, ⽹关会根据配置参数来发起重试请求.
filters:
    - name: Retry
        args:
            retries: 3
            statuses: BAD_REQUEST

retries: 重试次数, 默认为3 status:HTTP请求返回的状态码, 针对指定状态码进⾏重试. 对应org.springframework.http.HttpStatus

RequestSize设置允许接收最⼤请求包的⼤⼩. 如果请求包⼤⼩超过设置的值, 则返回 413 Payload Too Large. 请求包⼤⼩, 单位为字节, 默认值为5M.
filters:
    - name: RequestSize
        args:
            maxSize: 5000000

默认过滤器添加⼀个filter并将其应⽤于所有路由, 这个属性需要⼀个filter的列表, 详细参考下⾯章节
Default Filters(默认过滤器):

前⾯的filter添加在指定路由下, 所以只对当前路由⽣效, 若需要对全部路由⽣效, 可以使⽤ spring.cloud.gateway.default-filters 这个属性需要⼀个filter的列表.

配置举例:

spring:
    cloud:
        gateway:
            default-filters:
              - name: Retry
                args:
                    retries: 3
                    statuses: BAD_GATEWAY
@RequestMapping("/o1")
    public String o1(Integer id, String userName, HttpServletResponse response){
        log.info("过滤器传过来的参数,userName:{}",userName);
        response.setStatus(502);
        return productApi.p1(id);
    }

运行结果:重试三次

GlobalFilter(全局过滤器):

GlobalFilter是Spring Cloud Gateway中的全局过滤器, 它和GatewayFilter的作⽤是相同的. GlobalFilter 会应⽤到所有的路由请求上, 全局过滤器通常⽤于实现与安全性, 性能监控和⽇志记录等相关的全局功能.

Spring Cloud Gateway 内置的全局过滤器也有很多, ⽐如:

• Gateway Metrics Filter: ⽹关指标, 提供监控指标

• Forward Routing Filter: ⽤于本地forword, 请求不转发到下游服务器

• LoadBalancer Client Filter: 针对下游服务, 实现负载均衡.

• ...

更多过滤器参考: Global Filters

Gateway Metrics Filter:

快速上⼿

1. 添加依赖

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

2. 添加配置

spring:
    cloud:
        gateway:
            metrics:
                enabled: true
management:
    endpoints:
      web:
        exposure:
          include: "*"
    endpoint:
      health:
        show-details: always
      shutdown:
        enabled: true

3. 测试

http://127.0.0.1:10030/actuator, 显⽰所有监控的信息链接

过滤器执⾏顺序:

⼀个项⽬中, 既有GatewayFilter, ⼜有 GlobalFilter时, 执⾏的先后顺序是什么呢?

请求路由后, ⽹关会把当前项⽬中的GatewayFilter和GlobalFilter合并到⼀个过滤器链(集合)中, 并进⾏排序, 依次执⾏过滤器.

每⼀个过滤器都必须指定⼀个int类型的order值, 默认值为0, 表⽰该过滤的优先级. order值越⼩,优先级越⾼,执⾏顺序越靠前.

• Filter通过实现Order接⼝或者添加@Order注解来指定order值.

• Spring Cloud Gateway提供的Filter由Spring指定. ⽤⼾也可以⾃定义Filter, 由⽤⼾指定.

当过滤器的order值⼀样时, 会按照 defaultFilter > GatewayFilter > GlobalFilter的顺序执⾏.

⾃定义过滤器:

Spring Cloud Gateway提供了过滤器的扩展功能, 开发者可以根据实际业务来⾃定义过滤器, 同样⾃定义过滤器也⽀持GatewayFilter 和 GlobalFilter两种.

⾃定义GatewayFilter

⾃定义GatewayFilter, 需要去实现对应的接⼝ GatewayFilterFactory , Spring Boot 默认帮我们实现的抽象类是 AbstractGatewayFilterFactory , 我们可以直接使⽤.

定义GatewayFilter

@Slf4j
@Component
//CustomGatewayFilterFactory自定义过滤器的名称默认为GatewayFilterFactory前面的部分
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomConfig> implements Ordered {

    public CustomGatewayFilterFactory() {
        super(CustomConfig.class);
    }

    @Override
    public int getOrder() {
        //配置优先级, order越⼤, 优先级越低
        return Ordered.LOWEST_PRECEDENCE;
    }

    @Override
    public GatewayFilter apply(CustomConfig config) {
        return new GatewayFilter() {
            /**
             * ServerWebExchange HTTP请求和响应的交互契约,提供了对HTTP的请求和相应的访问
             * GatewayFilterChain 过滤器链
             * Mono: Reactor的核心类,数据流的发布者,Mono最多只能触发一个事件,可以把Mono用在异步完成任务时,发出通知
             * chain.filter(exchange) 执行请求
             * Mono.fromRunnable(),创建一个包含Runnable元素的数据流
             */
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                //Pre类型  执行请求  Post类型
                log.info("Pre Filter,config:{}",config);//Pre类型过滤器代码逻辑
                return chain.filter(exchange).then(Mono.fromRunnable(()->{
                    log.info("Post Filter...");//Post类型的过滤器代码逻辑
                }));
            }
        };
    }
}

* ServerWebExchange: HTTP请求-响应交互的契约, 提供对HTTP请求和响应的访问, 服务器端请求属性, 请求实例,响应实例等, 类似Context⻆⾊

* GatewayFilterChain: 过滤器链

* Mono: Reactor核⼼类, 数据流发布者, Mono最多只触发⼀个事件, 所以可以把Mono ⽤于在异步任务完成时发出通知.

* Mono.fromRunnable: 创建⼀个包含Runnable元素的数据流

配置优先级, order越⼤, 优先级越低}

针对这个Filter的配置, 使⽤CustomConfig 定义

@Data
public class CustomConfig {
    private String name;

}

代码说明:

1. 类名统⼀以GatewayFilterFactory结尾, 因为默认情况下, 过滤器的name会采⽤该定义类的前缀. 这⾥的name=Custom(yml配置中使⽤)

2. apply⽅法中, 同时包含Pre和Post过滤, then⽅法中是请求执⾏结束之后处理的

3. CustomConfig 是⼀个配置类, 该类只有⼀个属性name, 和yml的配置对应

4. 该类需要交给Spring管理, 所以需要加 @Service或者@Component 注解

5. getOrder表⽰该过滤器的优先级, 值越⼤, 优先级越低.

配置过滤器:

            filters:
              - name: Custom #过滤器名称
                args:
                  name: test_custom

测试,重启服务, 访问接⼝, 观察⽇志 http://127.0.0.1:10030/order/1

⾃定义GlobalFilter:

GlobalFilter的实现⽐较简单, 它不需要额外的配置, 只需要实现GlobalFilter接⼝, ⾃动会过滤所有的Filter.

定义GlobalFilter

@Slf4j
@Component
public class CustomGlobalFilter implements GlobalFilter , Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("Pre Global Filter...");
        return chain.filter(exchange).then(Mono.fromRunnable(()->{
            log.info("Post Global Filter...");
        }));
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

测试,重启服务, 访问接⼝, 观察⽇志 http://127.0.0.1:10030/order/1

从⽇志中,也可以看出来, Pre过滤器:当GatewayFilter 和GlobalFilter 过滤器order⼀样时, 会先执⾏GatewayFilter,Post则反之

服务部署:

1. 修改数据库, Nacos等相关配置

2. 对三个服务进⾏打包: product-service, order-service, gateway

3. 上传jar到Linux服务器

4. 启动Nacos

启动前最好把data数据删除掉.

5. 启动服务

#后台启动order-service, 并设置输出⽇志到logs/order.log
nohup java -jar order-service.jar >logs/order.log &
#后台启动product-service, 并设置输出⽇志到logs/order.log
nohup java -jar product-service.jar >logs/product-9090.log &
#启动⽹关
nohup java -jar gateway.jar >logs/gateway.log &

观察Nacos控制台

6. 测试

访问接⼝: 观察远程调⽤的结果:

观察⽇志:

分布式服务部署:

1. 部署前准备

前⾯的服务, 咱们都是在单机上部署的, 接下来我们使⽤多台服务器, 演⽰下分布式服务部署. 在部署之前, 我们先整理下需要部署的内容:

1. MySQL

2. Nacos

3. ⽹关服务

4. 产品服务

5. 订单服务

由于服务器受限, 我们把MySQL, Nacos, 理论上这两个服务是集群的模式,⽹关服务部署为单例的, 产品服务和订单服务部署为多例的.

环境要求

机器个数: 1-N台均可.

如果为1台,所有的项⽬均部署在⼀台机器即可. 如果为多台, 把上⾯5个服务, 根据机器性能分摊, 分开部署即可. 

机器环境:

Linux环境(CentOS, Ubuntu均可)

机器个数: 2台 服务部署分配如下

服务器计划部署内容
服务器1产品服务实例1, 订单服务实例1
服务器2产品服务实例2, 订单服务实例2

剩下的服务根据机器的性能分摊即可

机器个数: 3台 服务部署分配如下

服务器计划部署内容
服务器1

产品服务实例1, 订单服务实例1

服务器2产品服务实例2, 订单服务实例2
服务器3

Nacos MySQL,网关服务

2. 部署操作

MySQL安装

1.MySQL 安装  

2. 数据初始化

3. 对其他服务器授权

MySQL默认情况下, 只允许本地连接, 即localhost, 如果其他服务器需要连接到MySQL, 需要MySQL对这个服务器授权

语法格式:

grant 权限 on 数据库对象 to ⽤⼾

使⽤下⾯SQL, 创建⽤⼾, 并授权

-- 创建⽤⼾jqq, 并设置密码, 此步可省略

CREATE USER 'jqq'@'%' IDENTIFIED BY 'BITE@yyds.666';

-- 对bite⽤⼾授权

-- *.* 表⽰所有库的所有表, 也可以指定库和表-- %表⽰IP, %表⽰允许所有IP访问, 也可以指定IP

GRANT ALL ON *.* TO 'jqq'@'%';

-- 让修改⽣效

FLUSH PRIVILEGES; 

2. 修改bind-address

修改⽂件路径: /etc/mysql/mysql.conf.d/mysqld.cnf 把bind-address = 127.0.0.1改为 bind-address = 0.0.0.0

vim进入后按 i 进入编辑按esc以后 :wq 推出保存

3. 重启MySQL服务器(一定要重启

sudo systemctl restart mysql

4. 开放3306端⼝号

5. 测试授权结果

使⽤CMD客⼾端, 连接服务器MySQL, 如果可以正确连接, 则授权成功

#把110.41.51.65改成⾃⼰服务器的IP -u改成设置的账号名 -p后是对应的密码

mysql -h152.136.172.144 -P3306 -ujqq -pJqq200506,

Nacos 安装:

参考 注册中⼼的其他实现-Nacos 中的Nacos安装章节 安装前需要安装JDK

产品服务, 订单服务部署:

两台服务器操作步骤⼀样,要修改配置文件中MySQL和Nacos的相关配置

1. 安装JDK.

2. 登录服务器, 上传Jar包

3. 启动服务

#后台启动product-service, 并设置输出⽇志到logs/order.log nohup java -jar product-service-1.0-SNAPSHOT.jar --spring.cloud.nacos.discovery.ip=162.14.71.227 >logs/product.log &

#后台启动order-service, 并设置输出⽇志到logs/order.log nohup java -jar order-service-1.0-SNAPSHOT.jar --spring.cloud.nacos.discovery.ip=162.14.71.227 >logs/order.log &

Nacos 默认注册地址为内⽹IP, 如果各个服务器不在⼀个局域⽹时, 根据内⽹IP去访问时, 会出现⽹络不通, 接⼝调⽤失败, 需要通过spring.cloud.nacos.discovery.ip 设置注册的地址

4. 开放对应端⼝号

5. 测试

产品服务: http://162.14.71.227:10030/product/1001

订单服务: http://162.14.71.227:10030/order/1

⽹关服务部署:

1. 安装JDK.

2. 登录服务器, 上传Jar包

3. 启动服务

#后台启动gateway, 并设置输出⽇志到logs/gateway.log

nohup java -jar gateway-1.0-SNAPSHOT.jar --server.port=8080 >logs/gateway.log &

3. 开放对应端⼝号

4. 测试

产品服务: http://120.78.126.148:8080/product/1001

订单服务: http://120.78.126.148:8080/order/1

多访问几次最后可以看出来有负载均衡

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值