SpringCloud
1. 微服务介绍
微服务架构, 就是将应用拆分成非常小的服务,每个服务都是一个可以独立运行的项目。其主要特点如下:
- 单一职责:每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
- 面向服务:微服务对外暴露业务接口
- 自治:团队独立、技术独立、数据独立、部署独立
- 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
一旦采用微服务系统架构,就势必会遇到这样几个问题:
- 这么多小服务,如何管理他们? 服务治理(电话本)
- 这么多小服务,他们之间如何通讯? http协议(打电话)
- 这么多小服务,客户端怎么访问他们? 服务网关(保安)
对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。
SpringCloud
微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。
目前最知名的就是SpringCloud
和SpringCloud Alibaba
- SpringCloud:Spring基于NetFlix微服务组件进行封装,提供微服务一站式解决方案
- SpringCloud Alibaba:在SpringCloud NetFlix基础上封装了阿里巴巴的微服务解决方案
2. 服务治理 - Eureka
2-1. 服务治理
服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化服务注册与服务发现。
- 服务注册: 在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。
- 服务发现: 服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实例的访问。
服务注册:每个服务实例会把自己的IP地址,服务名称等信息注册到注册中心。
服务发现:当服务实例需要进行服务间的调用时,会查询注册中心获取其它服务的网络位置,找到后就可以进行调用。
服务续约:服务实例在注册完毕后会定期向注册中心发送心跳消息,这个过程被称作服务续约。
服务剔除:注册中心在一定时间内没有收到服务实例的心跳消息,那么就会认为这个服务实例已经失效,进行服务剔除。
2-2. 负载均衡
负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元上进行执行。
根据负载均衡发生位置的不同,一般分为服务端负载均衡和客户端负载均衡。
服务端负载均衡指的是发生在服务提供者一方,比如常见的nginx负载均衡。
客户端负载均衡指的是发生在服务请求的一方,也就是在发送请求之前已经选好了由哪个实例处理请求。
我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供者执行。
开启负载均衡
负载均衡原理总结
- 在RestTemplate上添加了
@LoadBalanced
注解后,会使用LoadBalancerClient
来配置RestTemplate - Spring Cloud Ribbon 的自动配置类LoadBalancerAutoConfiguration中的
@ConditionalOnBean(LoadBalancerClient.class)
条件成立 - 自动配置中添加了LoadBalancerInterceptor,这个拦截器会拦截请求,通过服务ID获取服务的地址列表,然后通过负载均衡策略选出一个地址进行调用
负载均衡策略
负载均衡的规则都定义在IRule接口中,它有很多不同的实现类,分别代表不同规则
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。第一次到8081,第二次就到8082,第三次又到8081,第四次又到8082… |
AvailabilityFilteringRule | 可用过滤规则,其实它功能是先过滤掉不可用的Server实例,再选择并发连接最小的实例。 |
WeightedResponseTimeRule | 为每一个服务器计算一个权重范围区间,权重区间的宽度越大,而权重区间宽度越大被选中的概率就越大。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
RetryRule | 轮询重试(重试采用的默认也是轮询) |
SpringCloud允许通过定义IRule修改负载均衡规则,有两种方式:
- 全局方式:向Spring容器中直接放入想要使用的策略对象
@Bean
public IRule randomRule(){
return new RandomRule();
}
- 局部方式:在配置文件中,针对指定的服务提供者配置策略。
user-service: # 给某个微服务配置负载均衡规则,这里是user-service服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
注意: 负载均衡策略需要配置在服务消费者一方
饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients:
- user-service # 指定饥饿加载的服务名称
3. 服务治理 - Nacos
Nacos是阿里巴巴的产品,也可以作为服务注册中心使用,相比Eureka功能更加丰富,在国内受欢迎程度较高。
3-1. 权重配置
nacos支持权重配置来控制不同实例的访问频率,权重越大则访问频率越高。0 <= 权重值 <= 1
3-2. 临时节点
Nacos的服务实例分为两种类型:
- 临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。
- 持久实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。
配置一个服务实例为永久实例:
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为非临时实例
临时和持久化的区别主要在健康检查失败后的表现,持久化实例健康检查失败后会被标记成不健康,而临时实例会直接从列表中被删除
3-3. Nacos和Eureka的区别
Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异:
模块 | Nacos | Eureka | 说明 |
---|---|---|---|
注册中心 | 是 | 是 | 服务治理基本功能,负责服务中心化注册 |
配置中心 | 是 | 否 | Eureka需要配合Config实现配置中心,且不提供管理界面 |
动态刷新 | 是 | 否 | Eureka需要配合MQ实现配置动态刷新,Nacos采用Netty保持TCP长连接实时推送 |
可用区AZ | 是 | 是 | 对服务集群划分不同区域,实现区域隔离,并提供容灾自动切换 |
分组 | 是 | 否 | Nacos可用根据业务和环境进行分组管理 |
元数据 | 是 | 是 | 提供服务标签数据,例如环境或服务标识 |
权重 | 是 | 否 | Nacos默认提供权重设置功能,调整承载流量压力 |
健康检查 | 是 | 是 | Nacos支持由客户端或服务端发起的健康检查,Eureka是由客户端发起心跳 |
负载均衡 | 是 | 是 | 均提供负责均衡策略 |
管理界面 | 是 | 否 | Nacos支持对服务在线管理,Eureka只是预览服务状态 |
4. 配置管理 - Nacos
Nacos除了可以做注册中心,同样可以做配置管理中心来使用。
3-1. 配置管理介绍
当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。
我们需要一种统一配置管理方案,可以集中管理所有实例的配置。
Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。
3-2. 统一配置管理
在nacos中添加配置
- 点击配置列表,添加新的配置信息
- 然后在弹出的表单中,填写配置信息
注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。
从微服务拉取配置
微服务要拉取nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动。
但如果尚未读取application.yml,又如何得知nacos地址呢?
因此springboot引入了一种新的配置文件:bootstrap.yml
文件,它会在application.yml之前被读取,而且其内容优先级高于application.yml
- 引入nacos-config依赖
在user-service服务中,引入nacos-config的客户端依赖:
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 添加bootstrap.yml
在user-service中添加一个bootstrap.yml文件,内容如下:
spring:
application:
name: user-service
cloud:
nacos:
config:
server-addr: localhost:8848 # nacos 配置中心地址
file-extension: yaml # 文件扩展名
这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据
${spring.application.name}.${spring.cloud.nacos.config.file-extension}
作为文件Data ID,来读取nacos的配置。
- 在代码中读取配置信息
在user-service中的UserController中添加业务逻辑,读取conf对象的相关配置
3-3. 配置热更新
我们最终的目的,是修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新。
局部方式
使用@RefreshScope
注解标注在使用配置信息的类上
全局方式
先使用@ConfigurationProperties
注解将配置读取到一个对象上,然后在需要使用配置的类中注入配置对象
5. 远程调用 - Feign
5-1. Feign介绍
Feign是一个声明式的http客户端,它使得调用远程服务就像调用本地服务一样简单,只需要创建一个接口并添加一个注解即可。
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
而且Feign默认集成了Ribbon,所以使用Feign默认就实现了负载均衡的效果。
5-2. Feign使用
5-3. Feign优化
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
-
URLConnection:默认实现,不支持连接池,每次请求都是新建连接
-
Apache HttpClient :支持连接池
-
OKHttp:支持连接池
因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。
改用Apache的HttpClient
- 在order-service的pom文件中引入Apache的HttpClient依赖
<!--httpClient的依赖内置连接池 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
- 在order-service的application.yml中添加配置
feign:
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 设置最大的连接数
max-connections-per-route: 50 # 并行接收一个服务的请求数量
6. 服务网关 - Gateway
在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?
如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。
这样的架构,会存在着诸多的问题:
- 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
- 认证复杂,每个服务都需要独立认证。
- 存在跨域请求,在一定场景下处理相对复杂。
上面的这些问题可以借助API网关来解决。所谓的API网关,就是指系统的统一入口。它封装了应用程序的内部结构,为客户端提供统一服务。
一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控、路由转发等等。
添加上API网关之后,系统的架构图变成了如下所示:
配置文件
spring:
cloud:
gateway:
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: user-service-route # 当前路由的标识, 要求唯一
uri: lb://user-service # 请求要转发到的地址
predicates: # 断言(就是路由转发要满足的条件)
- Path=/user/** # 当请求路径满足Path指定的规则时,才进行路由转发
6-1. 断言
断言用于进行条件判断(也就是在什么条件下才能进行路由转发),只有断言都返回真,才会真正的执行路由。
SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配。具体如下:
-
基于Datetime类型的断言
# AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期 # BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期 # BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内 - After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]
-
基于远程地址的断言
# RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中 - RemoteAddr=192.168.1.1/24
-
基于Cookie的断言
# CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求cookie是否具有给定名称且值与正则表达式匹配。 - Cookie=chocolate, ch.
-
基于Header的断言
# HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否具有给定名称且值与正则表达式匹配。 - Header=X-Request-Id, \d+
-
基于Host的断言
# HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。 - Host=**.testhost.org
-
基于Method请求方法的断言
# MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。 - Method=GET
-
基于Path请求路径的断言
# PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。 - Path=/foo/{segment}
-
基于Query请求参数的断言
# QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配。 - Query=baz, ba.
内置断言的使用:
spring:
cloud:
gateway:
routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
- id: user-service-route # 当前路由的标识, 要求唯一
uri: lb://user-service # 请求要转发到的地址
predicates: # 断言(就是路由转发要满足的条件)
- Path=/user/** # 当请求路径满足Path指定的规则时,才进行路由转发
- Before=2019-11-28T00:00:00.000+08:00 # 限制请求时间在2019-11-28之前
- Method=POST # 限制请求方式为POST
6-2. 过滤器
Gateway的过滤器会对请求或响应进行拦截,完成一些通用操作。在Gateway中, Filter的生效位置有两个:
- PRE: 这种过滤器在请求被路由之前调用,可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等
- POST:这种过滤器在路由到微服务以后执行,可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等
Gateway的Filter从作用范围可分为两种
- GatewayFilter:应用到单个路由或者一个分组的路由上
- GlobalFilter:应用到所有的路由上
自定义全局过滤器
//自定义SpringCloud gateway网关要求类必须实现两个接口
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//处理拦截逻辑
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1. 获取token (request.getParameter("token"))
String token = exchange.getRequest().getQueryParams().getFirst("token");
//状态码
//200 成功
//400 请求参数格式有误
//401 未认证
//403 权限不足
//404 路径不存在
//405 请求方式不匹配
//500 服务器错误
//2. 校验token,如果失败,直返返回错误提示
if (!"123".equals(token)) {
//设置错误码 401
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); //resp.setStatus(401);
//结束代码执行
return exchange.getResponse().setComplete();
} else {
//3. 如果成功,放行
return chain.filter(exchange);
}
}
//这个方法的返回值用于过滤器排序, 返回的值越小, 优先越高, 越先执行
@Override
public int getOrder() {
return 0;
}
}
当过滤器的order值一样时,局部过滤器 优先级高于 全局过滤器