0%

Nacos基础

前言

  1. 介绍Nacos作为注册中心的基本使用
  2. 介绍Nacos作为配置中心的基本使用
  3. 介绍Feign的微服务远程调用(非常实用)
    1. 服务两端统一接口实现
    2. 服务接口统一抽取
  4. 介绍SpringCloudGateway网关的基本使用
    1. 路由断言
    2. 请求过滤器

认识Nacos

微服务注册中心,比Eureka功能更加丰富。

安装

  • 必须配置JAVA_HOME
1
2
export JAVA_HOME=/usr/local/java
export PATH=$PATH:$JAVA_HOME/bin
  • application.properties里面可以修改端口
  • 单机启动: sh startup.sh -m standalone
  • 访问:http://ip:8848/nacos/index.html
    • 用户名密码:nacos

Nacos服务注册

  • 依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 父工程中添加spring-cloud-alibaba管理依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!-- 子工程添加Nacos客户端依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  • 客户端配置Nacos地址
1
spring.cloud.nacos.server-asddr: {ip}:8848 # nacos 服务端地址

Nacos服务分级存储模型

  • 服务跨集群调用问题:尽可能访问本地集群,本地集群不可用再访问外地集群
  • 配置服务提供端集群属性
1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
cluster-name: HZ # 航侦集群(手动配置)
  • 修改负载均衡规则
1
2
3
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 优先选择本地集群,然后在本地集群中用随机
  • 分配权重

    在Nacos控制台可以配置实例的权重

Namesapce环境隔离

Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层的隔离。命名空间下的服务是独立的。(可以用来隔离测试环境啥的)
  • 在nacos控制台创建命名空间
  • 代码中配置命名空间: spring.cloud.nacos.discovery.namespace: ${namespaceId}

  • 总结

    • namespace做环境隔离
    • 每个namespace都有唯一id
    • 不同namespace下的服务不可见

Nacos和Eureka的区别

  • 相同点:
    • 服务提供者注册服务信息
    • 服务消费者定时拉取服务信息
    • 支持服务提供者心跳方式做健康监测
  • 不同点:
    • Nacos会将服务提供者划分为临时实例和非临时实例
      • 临时实例采用心跳检测,心跳停止直接删除服务
      • 非临时实例,nacos主动询问实例,不会删除服务
    • 消费者定时服务拉取,当消息变更,nacos注册中心会主动像消费者进行服务列表同步
      • Eureka只做服务拉取,不做同步
    • Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
  • 配置非临时实例
1
spring.cloud.nacos.discovery.ephemeral=false

Nacos配置管理

统一配置管理

基础知识

  • 配置更改热更新,保证配置变化不用重启服务
  • 配置步骤
    • 登录nacos管理页面
    • 新建配置管理
      • DataId对为配置名称:userservice-dev.yaml
      • 编写配置:一般用来做一些开关,或者ABTest等
  • 启动流程
    • 项目启动 —- 读取nacos配置文件 —- 读取本地配置文件application.yml —- 创建spring容器 —- 加载bean
    • 但是如果nacsos地址在application里面,那么这个流程就不成立了
      • 建立一个bootstrap.yml,供项目启动时读取nacos地址等(bootstrap是引导文件,会在启动后立刻加载)

依赖

1
2
3
4
5
<!--客户端配置 nacos 配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
# bootstrap.yml
spring:
application:
name: userservice # 服务名称
profiles:
active: dev # 注意是开发环境, 在nacos管理页面中的配置文件为 userservice-dev.yaml
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
file-extension: yaml # 配置在nacos管理页面中的配置格式
1
2
3
// 这里的路径就是在nacos中配置的内容(可以理解为,nacos中是在远程的配置文件)
@Value("${pattern.dateformat}")
private String dateformat;

nacos中设置配置总结

  1. 在Nacos管理页面中添加配置文件
  2. 在微服务中引入nacos的config依赖
  3. 在微服务中添加bootstrap.yml,配置nacos地址、当前环境、服务名称、文件后缀名。(这些决定了程序启动时去读取nacos中哪个配置文件)

配置热更新

方式

两种方式实现配置自动更新:

  1. 在用到配置的类上加@RefreshScope注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {

@Value("${pattern.dateformat}")
private String dateformat;

@GetMapping("/now")
public String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
  1. 使用@ConfigurationProperties:不需要RefreshScope也可以自动刷新
1
2
3
4
5
6
7
8
9
10
11
// 定义配置类
@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}

// 其他类中使用上述配置Bean
@Autowired
PatternProperties properties;

问题

  1. 编写代码遇到问题,客户端不停地去请求配置中心,并打印日志,1秒估计得有几十次。

    答:查阅了资料,好像是因为服务端MD5和客户端MD5不一致,但是我不同意,因为功能都能实现。我猜是nacos1.4.1版本在macos上导致的日志打印问题。更新了nacos1.4.3版本后问题解决。

总结

  1. 通过@Value和@RefreshScope实现热更新
  2. 通过@ConfigurationProperties注入
  3. 不是所有配置都适合放在配置中心,维护起来很麻烦

配置共享

一个服务集群可能会共享一些配置。

原理

微服务启动时会从nacos读物多个配置文件:

  • [spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml
  • [spring.application.name].yaml,例如:userservice.yaml

无论profile如何变化,[spring.application.name].yaml这个文件一定会加载,因此在这个配置项中配置共享。

注意【多服务共享配置】

  • 线上配置 > 本地配置
  • 线上当前环境配置 > 线上共享配置

搭建Nacos集群

  1. 搭建数据库集群
  2. 不同机器nacos配置集群和数据库集群的地址,username和pwd
  3. ngnix反向代理

Feign远程调用

RestTemplate的问题

  1. 代码可读性差,编程体验感差
  2. 参数复杂,URL难以维护

很不优雅。

Feign使用

Feign是一个声明式的http客户端,其作用就是帮助我们优雅的实现http请求的发送。

依赖

1
2
3
4
5
<!-- Feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 启动类加注解
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}

// 2. 编写远程接口Client类
@FeignClient("userservice") // 远程调用的服务名称
public interface UserClient {

@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}

// 3. 使用的时候注入就行
@Autowired private UserClient userClient;

注意

Feign中已经集成了Ribbon,实现了负载均衡,非常的优雅。

自定义Feign配置

Feign运行自定义配置来覆盖默认配置,可以修改的配置大致如下:

类型 作用 说明
feign.Logger.Level 修改日志级别 NONE, BASIC(请求基本信息), HEADERS, FULL
feign.codec.Decoder 响应结果的解析器 http远程调用的结果做解析,例如解析json字符串为java对象
feign.codec.Encoder 请求参数编码 指定请求参数的编码格式
feign.Contract 支持的注解格式 默认SpringMVC
feign.Retryer 失败的重试机制 请求失败的重试机制,默认没有,可以使用Ribbon的重试

一般配置日志就行。

实践

  • 配置文件修改
1
2
3
4
5
# 全局
feign.client.config.default.loggerLevel=FULL

# 局部 userservice instead of default
feign.client.config.userservice.loggerLevel=FULL
  • 代码配置
1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 声明配置文件
public class FeignClientConfiguration {
@Bean
public Logger.Level feignLogLevel() {
return Logger.Level.BASIC;
}
}

// 2. 全局配置,放在启动类的注解中
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)

// 3. 局部配置,放在@FeignClient注解中
@FeignClient(value = "userservice", configuration = FeignClientConfiguration.class)

注意:局部配置覆盖全局。

Feign性能优化

底层的客户端实现

  • URLConnection:默认实现,不支持连接池
  • Apache HttpClient:支持连接池
  • OKHttp:支持连接池

性能优化:使用有连接池的实现,以及日志级别为basic和none

实践

  • 依赖
1
2
3
4
5
<!-- httpclient -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
  • 配置连接池
1
2
3
4
5
6
7
8
9
feign:
client:
config:
default:
loggerLevel: BASIC
httpclient:
enabled: true # 使用httpclient
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 每个路径的最大连接数

Feign的最佳实践

方式一:继承

给消费者的FeignClient和提供者的controller定义统一的父接口作为标准。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface UserAPI {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}

// 消费者
@FeignClass(value = "userservice")
public interface UserClient extends UserAPI {}

// 提供者
public class UserController implements UserAPI {
User findById(@PathVariable("id") Long id){
// ...实现业务
}
}

缺点:

  • 服务紧耦合
  • 父接口参数列表中的映射不会被继承

方式二:抽取

将FeignClient抽取为独立模块,并且把接口相关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。

提供者把接口抽取出来,变成jar依赖,给消费者引用。

缺点:

  • 消费者可能引入过多没有必要的依赖

方式二实践!非常重要!

  1. 首先创建一个module,命名为feign-api,然后引入feign的starter依赖
  2. 将pojo,feignConfiguration(之前用来配置日志的),client复制到feign-api中
  3. 在order-service中引用feign-api的依赖
  4. 修改order-service中的import
  • 引入依赖
1
2
3
4
5
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
  • 问题

    当定义的FeignClients不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。有两种解决方案。

    • 指定FeignClient所在包@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
    • 定向指定字节码@EnableFeignClients(clients = {UserClient.class})

补充

maven安装外部jar包!!

1
2
3
4
5
6
7
mvn install:install-file -Dfile=${jar包绝对路径} -DgroupId=com.memoforward -DartifactId=xxx-sdk -Dversion=1.1 -Dpackaging=jar -DgeneratePom=true


-Dfile的后面输入的为你下载的第三方jar包的本地文件路径。
-DgroupId的后面输入的为你转maven jar包后groupId的标签内容<groupId>com.memoforward</groupId>。
-DartifactId的后面输入的为你转maven jar包后artifactId的标签内容<artifactId>xxx-sdk</artifactId>。
-Dversion的后面输入的为你转maven jar包后version的标签内容版本号<version>1.1</version>。
1
2
3
4
5
<dependency>
<groupId>com.memoforward</groupId>
<artifactId>xxx-sdk</artifactId>
<version>1.1</version>
</dependency>

Gateway服务网关

官网地址:

为什么需要网关

痛点:不是所有微服务接口都是可以被访问的,需要对请求进行校验

网关功能:

  • 进行身份认证和权限校验
  • 服务路由、负载均衡
  • 请求限流

网关的技术实现

SpringCloud中网关的实现包括两种:

  • gateway
  • zuul

Zuul基于Servlet的实现,属于阻塞式变成。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。

搭建网关的步骤

  • 依赖
1
2
3
4
5
6
7
8
9
10
<!-- nacos服务注册,网关自己就是一个服务 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 网关gateway服务 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
  • 基础配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 127.0.0.1:8848
gateway:
routes: # 网关路由配置
- id: user-service # 唯一路由id,自定义,每个服务不一样
# uri: http://127.0.0.1:8081
uri: lb://userservice # loadbalance://....
predicates: # 路由断言,判断请求是否符合路由规则
- Path=/user/** # 路径匹配,以/user/开头的都会路由到userservice
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**

路由断言工厂(配置)

  • 我们在配置文件中写的断言规则只是字符串,这些字符串会被断言工厂读取并处理,转变为路由判断的条件
  • 例如Path=/user/**是按照路径匹配,这个规则是由PathRoutePredicateFactory类来处理的
  • 像上述的断言工厂Spring中还有11个

Spring的官网中有各种断言示例:SpringGateway断言

过滤器

路由过滤器

GateWayFilter是网关中提供一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。

可以形成过滤器链,用来对请求进行操作,比如添加请求头、限制请求流量。

全局过滤器

  • 自定义全局过滤器:实现GlobalFilter接口
1
2
3
4
5
6
7
8
9
10
public interface GlobalFilter {
/**
* @author memoforward
* @param exchange 请求上下文,里面可以获取request, response,用来写过滤逻辑
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono<void> 返回标示当前过滤器业务结束}
* @date 2022/11/14 下午8:15
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

简单案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
@Order(-1) // 过滤优先级,越小越高;也可以实现Springframework的ordered接口,都一样
public class MyGlobalFilter implements GlobalFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> queryParams = request.getQueryParams();
// 2. 获取参数中的参数
String authorization = queryParams.getFirst("authorization");
// 3. 判断参数是否等于admin
if ("admin".equals(authorization)) {
// 4. 是,放行
return chain.filter(exchange);
}
// 5. 否,拦截
// 5.1 设置状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 5.2 拦截
return exchange.getResponse().setComplete();
}
}

过滤器执行顺序

三类过滤器:路由过滤器、DefaultFilter、GlobalFilter

请求路由后,会将当前的这个三种过滤器合并到一个链中,依次执行每个过滤器。

顺序:

  • 路由过滤器和defaultFilter的order由Spring指定,默认从1开始递增
  • 当order一样,会按照defaultFilter > 路由过滤器 > GlobalFilter的顺序执行

跨域问题处理

跨域问题:浏览器禁止请求的发起者与服务端发生跨域的ajax请求,请求被浏览器拦截

解决方案:CORS

  • Gateway配置跨域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 127.0.0.1:8848
gateway:
globalcors:
add-to-simple-url-handler-mapping: true # 解决options请求被拦截
cors-configurations:
'[/**]': # 拦截一切请求
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
- "http://www.baidu.com"
allowedMethods:
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许携带的请求头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 一次跨域请求的有效期