SpringCloud Alibaba的常用组件
本文介绍了SpringCloud 常用组件:OpenFeign,Gateway,LoadBalancer和SpringCloudAlibaba 常用组件:Nacos,Sentinel,Seata
1 写在前头
1.1 什么是单体架构?
我们传统的项目都是单工程,多模块。一个项目下好几个模块,我们将业务逻辑都编写在一个工程中,然后编译、打包、部署一气呵成,简单粗暴。
而这种架构我们称之为单体架构。在单体架构中,单体应用将所有的业务都集中在同一个工程中,修改或增加业务都可能会对其他业务造成一定的影响,导致测试难度增加。
并且由于代码都集中到一个工程之中,耦合度高,而且服务也只部署到一个服务器上,导致工程并发能力有限。
由于单体架构存在上述的弊端,所以现在一个项目的开发方式也逐渐走向微服务架构
1.2 什么是微服务架构?
单体架构代码耦合度高?好的,那将其拆成多个服务;
单体架构并发能力有限?好的,不同的服务部署集群,一个扛不住就来多个;
单体架构容易引发雪崩?好的,服务之间故障隔离,一个服务挂了我不用你就是了。
上述就是微服务架构相对于单体架构的一些优势,那到底什么是微服务架构?
微服务架构是一种系统架构的设计风格。与传统的单体式架构不同,微服务架构提倡将一个单一的应用程序拆分成多个小型服务,这些小型服务都在各自独立的进程中运行,服务之间使用轻量级通信机制(通常是 HTTP RESTFUL API)进行通讯。
比如一个商城项目,其中不可避免的会有订单模块,库存模块,用户信息模块,在微服务架构中就会将这三个模块拆开,形成三个不同的服务:订单服务,库存服务,用户信息服务,每个服务可以有自己的开发团队,团队可以使用自己的开发语言。这些不同的微小服务,就是微服务。
2 SpringCloudAlibaba
2.1 服务拆分引发的问题
1 不同服务之间怎么发现彼此?
2 不同服务之间怎么通信?
3 多服务调用怎么保证事务的一致性?
4 服务部署到不同的机器上怎么控制并发?
5 某个服务的集群宕机怎么保证整个服务不雪崩?
看到这些问题是不是一阵头大?没关系,SpringCloudAlibaba给我们提供了解决上述问题的方案!
2.2 什么是SpringCloudAlibaba
SpringCloudAlibaba是阿里巴巴提供的微服务开发一站式解决方案,是阿里巴巴开源中间件与 Spring Cloud 体系的融合;
Spring Cloud Alibaba 是国内首个进入 Spring 社区的开源项目。2018 年 7 月,Spring Cloud Alibaba 正式开源,并进入 Spring Cloud 孵化器中孵化;2019 年 7 月,Spring Cloud 官方宣布 Spring Cloud Alibaba 毕业,并将仓库迁移到 Alibaba Github OSS 下。
虽然 Spring Cloud Alibaba 诞生时间不久,但俗话说的好“大树底下好乘凉”,依赖于阿里巴巴强大的技术影响力,Spring Cloud Alibaba 在业界得到了广泛的使用,成功案例也越来越多。
2.3 SpringCloudAlibaba提供的组件
Nacos:服务发现和配置管理
Sentinel:服务降级和服务流控
Seata:分布式事务处理框架
SpringCloud提供的组件:
Loadbalancer:负载均衡
OpenFeign:服务之间的远程调用
Gateway:网关,也是整个微服务的门户
3 Nacos
Nacos是整个微服务项目的注册中心和配置中心,所有的微服务都要来这里注册,以便让其它的服务发现自己,从而互相调用;而某个微服务的配置也可以扔给Nacos管理,启动的时候直接到Nacos拉取即可。
3.1 下载Nacos
Nacos是一个已经写好的项目,我们需要做的是将其下载,然后启动:
官网:
https://nacos.io/zh-cn/
下载:
https://github.com/alibaba/nacos/releases
并不是下载好Nacos就可以直接使用,Nacos还依赖于一些环境:
JDK 1.8+
Maven 3.2+
3.2 启动Nacos
由于我们Nacos没有部署集群,所以此处需要以单机的模式启动,在Nacos的bin目录下进入cmd,执行下述命令:
startup.cmd -m standalone
上述命令会让Nacos以单机模式启动,启动之后在浏览器访问:
localhost:8848/nacos/index.html
看到下述页面说明Nacos启动成功:
4 OpenFeign和Loadbalancer
前置条件:
我们需要在Nacos中注册两个微服务,我此处将其命名为client-test和cilent-account。
client-account:消费者(使用其它服务的接口,即“消费”)
client-test:生产者(给其它服务提供接口,即“生产”)
注意:消费者
和生产者
并不是非黑即白的概念,即某个服务既可能是生产者
,也可能是消费者
。
4.1 对client-account服务做负载均衡(Loadbalancer)
4.1.1 配置
pom.xml:
无,spring-cloud-commons 和 spring-cloud-loadbalancer 已经被引入
application.properties:
无
配置类:
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);
}
}
注意:配置类并不是必须的,如果想要改变Loadbalancer的负载均衡方式就可以配置。
4.1.2 使用
Loadbalancer提供了两种负载均衡方式:随机(RandomLoadBalancer
)和轮询(RoundRobinLoadBalancer
):
轮询:消费者每次调用生产者的接口,依次使用集群中的服务
随机:消费者每次调用生产者的接口,随机使用集群中的服务
其中,轮询是Loadbalancer默认提供的方式,此处我将Loadbalancer的负载均衡方式改为了随机(参见配置类)。
修改client-account的启动类:
@SpringBootApplication
// name和目标微服务的注册名保持一致,configuration引入自定义的配置类
@LoadBalancerClient(name = "client-account", configuration =
CustomLoadBalancerConfiguration.class)
public class SpringCloudTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCloudTestApplication.class, args);
}
}
这样,当client-account去访问client-test时,就会实现随机访问集群中的服务。
4.2 client-account调用client-test服务(OpenFeign)
4.2.1 配置
pom.xml:
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--loadbalancer,springcloud高版本中,OpenFeign使用的负载均衡器,需单独引入-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
application.properties:
无
4.2.2 使用
client-test提供的接口:
@RefreshScope
@RestController
@RequestMapping("/api/test")
public class CountryController {
@Autowired
private CountryService countryService;
/**
* 127.0.0.1:8001/api/test/country/522 ---- get
*/
@GetMapping(value = "/country/{countryId}")
public Country getCountryByCountryId(@PathVariable int countryId) {
return countryService.getCountryByCountryId(countryId);
}
}
client-account中创建一个TestFeign接口,用于远程调用cient-test的接口
// name的值是要调用的微服务在Nacos中注册的名字,Nacos的重要性可见一斑。
@FeignClient(name = "client-test")
public interface TestFeign {
@GetMapping(value = "/api/test/country/{countryId}")
Country getCountryByCountryId(@PathVariable int countryId);
}
client-account在Controller中使用TestFeign接口
@RestController
@RequestMapping("/api/account")
publicc lass AccountController {
@Autowired
private TestFeign testFeign;
/**
* 127.0.0.1:8004/api/account/country/522
* @param id
* @return
*/
@GetMapping("/country/{id}")
public Country getCountry(@PathVariable int id) {
return testFeign.getCountryByCountryId(id);
}
}
在client-account微服务的启动类上添加@EnableFeignClients注解
@EnableFeignClients
@SpringBootApplication
public class SpringCloudAlibabaAccountApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCloudAlibabaAccountApplication.class, args);
}
}
现在启动项目,就可以在client-account服务中调用client-test的接口了!
5 Gateway
Gateway被称为网关,顾名思义,网络的关卡,在一个微服务项目中部署网关,让客户端只能通过网关访问微服务内部的接口,保证了微服务项目内各个接口的安全:
5.1创建Gateway项目
Gateway项目也是一个单独的项目,也就是说Gateway也是一个微服务,它也是需要去Nacos中注册的。
前置问题:
Gateway的组件不能和SpringWeb的组件共存。
为什么?
Gateway构建于Spring 5+,基于Spring boot 2.x响应式的、非阻塞式的API,同时,他支持webSockets和spring框架紧密集成。而当一个项目中有spring-web时,启动项目时默认使用的是spring-boot-starter-web的内置容器,该容器不支持非阻塞,所以gateway就会报错,所以gateway和spring web组件不能共存。
5.1.1 配置
pom.xml:
<!-- gateway -->
<!-- nacos discovery -->
<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>
<!-- loadbalancer(springcloud高版本中,OpenFeign使用的负载均衡器,需单独引入) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<!-- openfeign
(不添加该依赖,网关无法转发客户端的请求,猜测Nacos负载均衡器和Gateway负载均衡器不一致造成)
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
application.properties:
# for server
server.port=8000
# for spring
spring.application.name=client-gateway
# for nacos
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 设置该项目非Web启动(也就是上面说方案二)
spring.main.web-application-type=reactive
# for gateway route
# 默认false,开启后可以通过ip:port/服务名称/接口地址进行服务转发
#spring.cloud.gateway.discovery.locator.enabled=true
# 把服务名转换为小写,Eureka中默认都是大写,但Nacos不会自动转换,所以也可以不写。
#spring.cloud.gateway.discovery.locator.lower-case-service-id=true
spring.cloud.gateway.routes[0].id=account-service
spring.cloud.gateway.routes[0].uri=lb://client-account
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/account/**
spring.cloud.gateway.routes[1].id=common-service
spring.cloud.gateway.routes[1].uri=lb://client-test
spring.cloud.gateway.routes[1].predicates[0]=Path=/api/test/**
5.1.2 使用
启动类上添加下列注解:
@EnableFeignClients
@EnableDiscoveryClient
解决跨域问题:
@Configuration
public class CorsAutoConfiguration {
@Bean
public WebFilter corsFilter() {
/**
* ServerWebExchange:请求上下文,里面可以获取request和response等信息
* WebFilterChain:放行链,将请求放行给下一个过滤器
*/
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if(CorsUtils.isCorsRequest(request)) {
ServerHttpResponse response = ctx.getResponse();
HttpHeaders headers = response.getHeaders();
// 允许跨域访问
headers.set(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
request.getHeaders().getOrigin());
// 允许的header
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
"X-Token,Token,Authorization,x-requested-with,Content-Type");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,
"PUT,POST,GET,OPTIONS,DELETE");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600");
if(request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}
}
至此,Gateway基本功能已经完善,可以将所有微服务的端口都换做是Gateway的端口,让整个Gateway成为整个微服务项目的门户。
6 Sentinel
简介:
Sentinel 翻译过来就是哨兵,那么哨兵的作用自然就是监控。
Sentinel 是由阿里巴巴中间件团队开发的开源项目,是一种面向分布式微服务架构的轻量级高可用流量控制组件。
Sentinel 主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度帮助用户保护服务的稳定性。
基本概念:资源
和规则
资源:我们在项目中想要保护的东西都可以称为资源,可以是某个服务,方法,或者一个代码块
规则:围绕我们想要保护的资源而设定的规则,常见如流控和熔断。
两大组成:核心库
和控制台
Sentinel 核心库:
Sentinel 的核心库不依赖任何框架或库,能够运行于 Java 8 及以上的版本的运行时环境中,同时对 Spring Cloud、Dubbo 等微服务框架提供了很好的支持。
Sentinel 控制台:
Sentinel 提供的一个轻量级的开源控制台,基于SpringBoot开发,它为用户提供了机器自发现、簇点链路自发现、监控、规则配置等功能。
6.1 下载Sentinel控制台
文档地址:https://sentinelguard.io/zh-cn/docs/introduction.html
仓库地址:https://github.com/alibaba/Sentinel
修改控制台的日志输出位置
sentinel默认的输出位置为:${user_home}/logs/csp/${project.name}.properties
。也就是默认位置为c盘。
对于已经使用了sentinel的微服务,可以在其resource
目录下创建一个sentinel.properties
文件,其中定义如下配置即可更改sentinel的日志输出位置:
csp.sentinel.log.dir=G:/file_static/logs/sentinel_logs/client-account/
访问Sentinel的控制台:
sentinel的控制台就是一个jar包,因为是用SpringBoot写的,开发者没有重新定义其端口号,默认就是8080,这是一个常用端口号,可能会被占用,所以我们可以在启动时修改其端口号:
java -jar sentinel-dashboard-1.8.3.jar --server.port=9090
账号和密码都是sentinel
:
6.2 定义资源和规则
6.2.1 定义资源
Sphu定义资源
Sentinel 提供了一个名为 SphU 的类,它包含的 try-catch 风格的 API ,可以帮助我们手动定义资源:
@GetMapping("/string/{name}")
public String getName(@PathVariable String name) {
Entry entry = null;
try {
// 定义资源名称
entry = SphU.entry("getName");
// 业务逻辑开始
return name;
// 业务逻辑结束
} catch (BlockExceptione) {
// 资源被流控时
return"您被流控了";
} finally {
if(entry != null) {
entry.exit();
}
}
}
这样就定义好了一个资源,我们可以为其添加熔断或者流控规则,并且可以在sentinel面板看到该资源:
Spho定义资源
Sentinel 还提供了一个名为 SphO 的类,它包含了 if-else 风格的 API,能帮助我们手动定义资源:
@GetMapping("/integer/{id}")
public int getInt(@PathVariable int id) {
// 定义资源名称
if(SphO.entry("getInt")) {
try {
// 您的业务逻辑
return id;
} finally {
SphO.exit();
}
} else {
// 您的限流或熔断逻辑
return -1;
}
}
这样就定义好了一个资源,我们可以为其添加熔断或者流控规则,并且可以在sentinel面板看到该资源:
@SentinelResource方式定义资源(推荐)
/**
* @author weiyond
* 2022/9/29 13:45
*/
@RestController
@RequestMapping("/api/account")
public class AccountController {
@Autowired
private TestFeign testFeign;
/**
* 127.0.0.1:8004/api/account/country/522
*
* SentinelResource参数解释:
* value:
指定资源名称,可以随便取,按照见名知意,我们一般取当前接口调用的下游服务的方法名作为资源名
* blockHandler:
当资源被限流或降级时,指定一个方法来作为备用逻辑
* 若备用方法和当前方法不在用一个类还需要指定blockHandlerClass参数
* fallback:
当资源被熔断时,指定一个方法来作为备用逻辑
* 若备用方法和当前方法不在用一个类还需要指定fallbackClass参数
* @param id
* @return
*/
@GetMapping("/country/{id}")
@SentinelResource(value = "getCountryByCountryId",
blockHandler = "blockHandler",
fallback = "fallback")
public Country getCountry(@PathVariable int id) {
return testCountryFeignClient.getCountryByCountryId(id);
}
/**
* 发生限流和降级时,就会调用该方法来充当备用处理的逻辑
* FlowException:发生流控时,抛出该异常
* DegradeException:发生降级时,抛出该异常
* @param id
* @param exception
* @return
*/
public Countryb lockHandler(int id, BlockException exception) {
if(exception instanceof FlowException) {
System.out.println("您被限流了.");
} else if(exception instanceof DegradeException) {
System.out.println("您被降级了.");
} else {
System.out.println("当前接口不可用.");
}
return new Country();
}
/**
* 当资源熔断了,就调用该方法来充当备用处理逻辑
* @param id
* @return
*/
public Country fallback(int id) {
System.out.println("当前接口熔断了");
return new Country();
}
}
6.2.2 定义规则
流控规则
对上面通过@SentinelResource
定义的资源getCountryByCountryId
添加流控:
现在,getCountryByCountryId
这个资源已经被Sentinel保护起来了!1秒内多次访问getCountryByCountryId资源,浏览器出现我们自定义的限流信息(接口直接返回空对象,不会去调用下游服务,减轻下游服务的压力):
上述定义流控规则是根据Sentinel控制台
实现的,如果你愿意,还可以使用编码的方式实现流控(这也证明了Sentinel核心库并不依赖于Sentile控制台
):
@GetMapping("/country/{id}")
@SentinelResource(value = "getCountryByCountryId",
blockHandler = "blockHandler",
fallback = "fallback")
public Country getCountry(@PathVariable int id) {
// 调用编码方式的流控规则
initFlowRules();
return testCountryFeignClient.getCountryByCountryId(id);
}
// 资源限流时执行的逻辑
public Country blockHandler(int id, BlockException exception) {
return new Country();
}
private static void initFlowRules() {
List<FlowRule> rules = new ArrayList<>();
// 定义一个限流规则对象
FlowRule rule = new FlowRule();
// 资源名称
rule.setResource("getCountryByCountryId");
// 限流阈值的类型为QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 设置QPS的阈值为2
rule.setCount(1);
rules.add(rule);
// 定义限流规则
FlowRuleManager.loadRules(rules);
}
编码方式也能实现限流:
熔断规则
熔断降级是什么?
在分布式微服务架构中,一个系统往往由多个服务组成,不同服务之间相互调用,组成复杂的调用链路。如果链路上的某一个服务出现故障,那么故障就会沿着调用链路在系统中蔓延,最终导致整个系统瘫痪。Sentinel 提供了熔断降级机制就可以解决这个问题。
Sentinel 的熔断将机制会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),暂时切断对这个资源的调用,以避免局部不稳定因素导致整个系统的雪崩。
熔断降级作为服务保护自身的手段,通常在客户端(调用端)进行配置,资源被熔断降级最直接的表现就是抛出 DegradeException 异常。
Sentinel提供的三种熔断降级策略:
慢调用比例:
选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大响应时间),若请求的响应时间大于该值则统计为慢调用。
当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。
经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则再次被熔断。
异常比例:
当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目且异常的比例大于阈值,则在接下来的熔断时长内请求会自动被熔断。
经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
异常数:
当单位统计时长内的异常数目超过阈值之后会自动进行熔断。
经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
注意:Sentinel 1.8.0 版本对熔断降级特性进行了全新的改进升级,以上熔断策略针对的是Sentinel 1.8.0
及以上版本。
熔断降级的三种状态:
现在开始演示熔断降级,名词解释:
上游服务:处于调用链的上方的服务
下游服务:处于调用链的下方的服务
显然,此处Demo中,我们的client-account就是上游服务,client-test就是下游服务。
我们在哪里做熔断降级?
在上游服务做熔断降级,这样当下游服务发生异常时,可以即时熔断上游服务和下游服务的调用链,避免下游服务的长时间宕机从而引起上游服务阻塞,不断有请求堆积,占用资源,慢慢扩散至整个项目,导致整个项目不可用,这就是雪崩。
client-account服务的熔断降级逻辑:
@RestController
@RequestMapping("/api/account")
public class AccountController {
@Autowired
private TestFeign testFeign;
/**
* 127.0.0.1:8004/api/account/country/522
* SentinelResource参数:
* fallback:当资源被熔断时,指定一个方法来作为备用逻辑
* 若备用方法和当前方法不在用一个类还需要指定fallbackClass参数
*
* @param id
* @return
*/
@GetMapping("/country/{id}")
@SentinelResource(value = "getCountryByCountryId", fallback = "fallback")
public Country getCountry(@PathVariable int id) {
return testCountryFeignClient.getCountryByCountryId(id);
}
/**
* 当资源熔断了,就调用该方法来充当备用处理逻辑
*
* @param id
* @return
*/
public Country fallback(int id) {
monitor();
System.out.println("当前接口熔断了");
return new Country();
}
/**
* 自定义事件监听器,监听熔断器状态转换
* preState:熔断的上一个状态
* newState:当前熔断状态
*/
public void monitor() {
EventObserverRegistry
.getInstance()
.addStateChangeObserver(
"logging",
(prevState, newState, rule, snapshotValue) -> {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-ddHH:mm:ss");
if(newState == CircuitBreaker.State.OPEN) {
// 变换至OPEN state时会携带触发时的值
System.err.println(String.format("%s -> OPEN at%s, 发送请求次数=%.2f",
prevState.name(),
format.format(new Date(TimeUtil.currentTimeMillis())),
snapshotValue));
} else {
System.err.println(String.format("%s -> %s at %s",
prevState.name(),
newState.name(),
format.format(new Date(TimeUtil.currentTimeMillis()))));
}
}
);
}
}
client-test服务随便产生一个异常:
在Sentinel控制台
为getCountryByCountryId
设置熔断降级规则:
上面的规则翻译成白话如下:
1秒内对目标资源的请求次数大于1次,且目标资源发生的异常数量超过1,那么就熔断该资源所使用的下游服务,熔断后的10秒内都使用其备用逻辑。
1秒内多次向资源传递小于0的参数:
idea控制台输出,可以看到根据sentinel控制台定义的熔断规则,此时熔断已经开启:
在熔断时长内(此处为10s),即使传递正常的参数也会直接降级,走备用逻辑:
熔断时长结束,熔断进入half-open状态,此时再次传递-1参数,half-open就会再次进入open:
下游服务异常应答:
idea输出:
熔断时长再次结束,熔断进入half-open状态,此时传递正常参数,half-open就会再次进入closed状态:
下游服务正常应答:
idea输出:
当然,上述是使用Sentinel控制台
设置的规则,我们依然可以使用编码的方式实现熔断规则。
省略,下次和Seata一起补上 ^ ^
参考文章
http://c.biancheng.net/springcloud/micro-service.html
更多推荐
所有评论(0)