SpringCloudAlibaba常用操作
微服务概念
1.1 单体、分布式、集群
单体
⼀个系统业务量很⼩的时候所有的代码都放在⼀个项⽬中就好了,然后这个项⽬部署在⼀台服务器上就 好了。整个项⽬所有的服务都由这台服务器提供。这就是单机结构。
单体应⽤开发简单,部署测试简单.但是存在⼀些问题,⽐如:单点问题,单机处理能⼒有限,当你的业务增⻓到 ⼀定程度的时候,单机的硬件资源将⽆法满⾜你的业务需求。
分布式
由于整个系统运⾏需要使⽤到Tomcat和MySQL,单台服务器处理的能⼒有限,2G的内存需要分配给 Tomcat和MySQL使⽤,,随着业务越来越复杂,请求越来越多. 内存越来越不够⽤了,所以这时候我们 就需要进⾏分布式的部署。
我们进⾏⼀个评论的请求,这个请求是需要依赖分布在两台不同的服务器的组件[Tomat和MySQL],才能 完成的. 所以叫做分布式的系统.
集群
在上⾯的图解中其实是存在问题的,⽐如Tomcat存在单点故障问题,⼀旦Tomcat所在的服务器宕机不 可⽤了,我们就⽆法提供服务了,所以针对单点故障问题,我们会使⽤集群来解决.那什么是集群模式呢?
单机处理到达瓶颈的时候,你就把单机复制⼏份,这样就构成了⼀个“集群”。集群中每台服务器就叫做 这个集群的⼀个“节点”,所有节点构成了⼀个集群。每个节点都提供相同的服务,那么这样系统的处理 能⼒就相当于提升了好⼏倍(有⼏个节点就相当于提升了这么多倍)。
但问题是⽤户的请求究竟由哪个节点来处理呢?最好能够让此时此刻负载较⼩的节点来处理,这样使得 每个节点的压⼒都⽐较平均。要实现这个功能,就需要在所有节点之前增加⼀个“调度者”的⻆⾊,⽤户 的所有请求都先交给它,然后它根据当前所有节点的负载情况,决定将这个请求交给哪个节点处理。这 个“调度者”有个⽜逼了名字——负载均衡服务器。
我们在上⾯的图中仅展示了Tomcat的集群,如果MySQL压⼒⽐较⼤的情况下,我们也是可以对MySQL 进⾏集群的.
1.2 系统架构演变
随着互联⽹的发展,⽹站应⽤的规模也不断的扩⼤,进⽽导致系统架构也在不断的变化。
从互联⽹早起到现在,系统架构⼤体经历了下⾯⼏个过程: 单体应⽤架构—>垂直应⽤架构—>分布 式架构—>SOA架构—>微服务架构。
接下来我们就来了解⼀下每种系统架构是什么样⼦的, 以及各有什么优缺点。
1.2.1 单体应⽤架构
互联⽹早期,⼀般的⽹站应⽤流量较⼩,只需⼀个应⽤,将所有功能代码都部署在⼀起就可以,这 样可以减少开发、部署和维护的成本。
⽐如说⼀个电商系统,⾥⾯会包含很多⽤户管理,商品管理,订单管理,物流管理等等很多模块, 我们会把它们做成⼀个web项⽬,然后部署到⼀台tomcat服务器上。
优点:
-
项⽬架构简单,⼩型项⽬的话, 开发成本低
-
项⽬部署在⼀个节点上, 维护⽅便
缺点:
-
全部功能集成在⼀个⼯程中,对于⼤型项⽬来讲不易开发和维护
-
项⽬模块之间紧密耦合,单点容错率低
-
⽆法针对不同模块进⾏针对性优化和⽔平扩展
1.2.2 垂直应⽤架构
随着访问量的逐渐增⼤,单⼀应⽤只能依靠增加节点来应对,但是这时候会发现并不是所有的模块 都会有⽐较⼤的访问量.
还是以上⾯的电商为例⼦, ⽤户访问量的增加可能影响的只是⽤户和订单模块, 但是对消息模块 的影响就⽐较⼩. 那么此时我们希望只多增加⼏个订单模块, ⽽不增加消息模块. 此时单体应⽤就做不 到了, 垂直应⽤就应运⽽⽣了.
所谓的垂直应⽤架构,就是将原来的⼀个应⽤拆成互不相⼲的⼏个应⽤,以提升效率。⽐如我们可 以将上⾯电商的单体应⽤拆分成:
-
电商系统(⽤户管理 商品管理 订单管理)
-
后台系统(⽤户管理 订单管理 客户管理)
-
CMS系统(⼴告管理 营销管理)
这样拆分完毕之后,⼀旦⽤户访问量变⼤,只需要增加电商系统的节点就可以了,⽽⽆需增加后台 和CMS的节点。
优点:
-
系统拆分实现了流量分担,解决了并发问题,⽽且可以针对不同模块进⾏优化和⽔平扩展
-
⼀个系统的问题不会影响到其他系统,提⾼容错率
缺点:
-
系统之间相互独⽴, ⽆法进⾏相互调⽤
-
系统之间相互独⽴, 会有重复的开发任务
1.2.3 分布式架构
当垂直应⽤越来越多,重复的业务代码就会越来越多。这时候,我们就思考可不可以将重复的代码 抽取出来,做成统⼀的业务层作为独⽴的服务,然后由前端控制层调⽤不同的业务层服务呢?
这就产⽣了新的分布式系统架构。它将把⼯程拆分成表现层和服务层两个部分,服务层中包含业务 逻辑。表现层只需要处理和⻚⾯的交互,业务逻辑都是调⽤服务层的服务来实现。
优点:
- 抽取公共的功能为服务层,提⾼代码复⽤性
缺点:
- 系统间耦合度变⾼,调⽤关系错综复杂,难以维护
1.2.4 SOA架构
在分布式架构下,当服务越来越多,容量的评估,⼩服务资源的浪费等问题逐渐显现,此时需增加⼀个调度中⼼对集群进⾏实时管理。此时,⽤于资源调度和治理中⼼(SOA Service OrientedArchitecture,⾯向服务的架构)是关键。
优点:
- 使⽤注册中⼼解决了服务间调⽤关系的⾃动调节
缺点:
-
服务间会有依赖关系,⼀旦某个环节出错会影响较⼤( 服务雪崩 )
-
服务关系复杂,运维、测试部署困难
1.2.5 微服务架构
微服务架构在某种程度上是⾯向服务的架构SOA继续发展的下⼀步,它更加强调服务的"彻底拆分"。
优点:
-
服务原⼦化拆分,独⽴打包、部署和升级,保证每个微服务清晰的任务划分,利于扩展
-
微服务之间采⽤RESTful等轻量级Http协议相互调⽤
缺点:
- 分布式系统开发的技术成本⾼(容错、分布式事务等)
1.3 微服务架构介绍
微服务架构, 简单的说就是将单体应⽤进⼀步拆分,拆分成更⼩的服务,每个服务都是⼀个可以独⽴运⾏的项⽬。
微服务架构的常⻅问题
⼀旦采⽤微服务系统架构,就势必会遇到这样⼏个问题:
-
这么多⼩服务,如何管理他们?
-
这么多⼩服务,他们之间如何通讯?
-
这么多⼩服务,客户端怎么访问他们?
-
这么多⼩服务,⼀旦出现问题了,应该如何⾃处理?
-
这么多⼩服务,⼀旦出现问题了,应该如何排错?
对于上⾯的问题,是任何⼀个微服务设计者都不能绕过去的,因此⼤部分的微服务产品都针对每⼀个问题提供了相应的组件来解决它们。
1.4 SpringCloud介绍
Spring Cloud是⼀系列框架的集合。它利⽤Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中⼼、消息总线、负载均衡、断路器、数据监控等,都可以⽤SpringBoot的开发⻛格做到⼀键启动和部署。
Spring Cloud并没有重复制造轮⼦,它只是将⽬前各家公司开发的⽐较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot⻛格进⾏再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了⼀套简单易懂、易部署和易维护的分布式系统开发⼯具包。
1.4.1 SpringBoot和SpringCloud有啥关系?
-
SpringBoot专注于快速⽅便的开发单个个体微服务。
-
SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的⼀个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、断路器、路由、事件总线、分布式事务、等等集成服务。
总结: SpringBoot专注于快速、⽅便的开发单个微服务个体,SpringCloud关注全局的服务治理组件的集合。
1.4.2 SpringCloud版本名称?
因为Spring Cloud不同其他独⽴项⽬,它是拥有很多⼦项⽬的⼤项⽬。所以它是的版本是 版本名+版本号 (如Greenwich.SR6)。
版本名:是伦敦的地铁名
版本号:SR(Service Releases)是固定的 ,⼤概意思是稳定版本。后⾯会有⼀个递增的数字。所以 Greenwich.SR6就是Greenwich的第6个Release版本。
1.4.3 为什么选择SpringCloud Alibaba?
我们这⾥为什么选择SpringCloud Alibaba呢,主要因为SpringCloud Netflix的组件:服务注册与发现的Eureka、服务限流降级的 Hystrix、⽹关 Zuul都已经停⽌更新了,当然继续使⽤是没问题的,只是出现问题,官⽅不维护,需要⾃⾏解决.
微服务环境搭建
商品微服务
- 查询商品列表
订单微服务
- 创建订单
2.1 技术选型
持久层: SpingData Jpa
数据库: MySQL5.7
其他: SpringCloud Alibaba 技术栈
2.2 模块设计
— shop-parent ⽗⼯程
— shop-product-api 商品微服务api 【存放商品实体】
— shop-product-server 商品微服务 【端⼝:808x】
— shop-order-api 订单微服务api 【存放订单实体】
— shop-order-server 订单微服务 【端⼝:809x】
2.3 微服务调用
在微服务架构中,最常⻅的场景就是微服务之间的相互调⽤。我们以电商系统中常⻅的⽤户下单为例来演示微服务的调⽤:客户向订单微服务发起⼀个下单的请求,在进⾏保存订单之前需要调⽤商品微服务查询商品的信息。
我们⼀般把服务的主动调⽤⽅称为服务消费者,把服务的被调⽤⽅称为服务提供者。
在这种场景下,订单微服务就是⼀个服务消费者, 商品微服务就是⼀个服务提供者。
2.4 版本说明
https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明
2.5 创建⽗⼯程
创建⼀个maven⼯程,然后在pom.xml⽂件中添加下⾯内容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xiaoge</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<!--⽗⼯程-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
</parent>
<!--依赖版本的锁定-->
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.3.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>
</dependencies>
</dependencyManagement>
</project>
2.6 创建商品微服务
1.创建shop-product-api项⽬,然后在pom.xml⽂件中添加下⾯内容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xiaoge</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-product-api</artifactId>
<!--依赖-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
2 创建实体类
package com.xiaoge.entity;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
//商品
@Entity(name = "t_shop_product")
@Data
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long pid;//主键
private String pname;//商品名称
private Double pprice;//商品价格
private Integer stock;//库存
}
3.创建shop-product-server项⽬,然后在pom.xml⽂件中添加下⾯内容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xiaoge</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-product-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<dependency>
<groupId>com.xiaoge</groupId>
<artifactId>shop-product-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
4.编写启动类ProductServer.java
package com.xiaoge;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ProductServer {
public static void main(String[] args) {
SpringApplication.run(ProductServer.class, args);
}
}
5.编写配置⽂件application.yml
server:
port: 8081
spring:
application:
name: product-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop-product?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: root
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
6.在数据库中创建shop-product的数据库
7.创建ProductDao
package com.xiaoge.dao;
import com.xiaoge.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductDao extends JpaRepository<Product, Long> {
}
8.创建ProductService接⼝和实现类
package com.xiaoge.service.impl;
import com.xiaoge.dao.ProductDao;
import com.xiaoge.entity.Product;
import com.xiaoge.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductDao productDao;
@Override
public Product findByPid(Long pid) {
return productDao.findById(pid).get();
}
}
9.创建Controller
package com.xiaoge.controller;
import com.alibaba.fastjson.JSON;
import com.xiaoge.entity.Product;
import com.xiaoge.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class ProductController {
@Autowired
private ProductService productService;
//商品信息查询
@RequestMapping("/product/{pid}")
public Product findByPid(@PathVariable("pid") Long pid) {
log.info("接下来要进⾏{}号商品信息的查询", pid);
Product product = productService.findByPid(pid);
log.info("商品信息查询成功,内容为{}", JSON.toJSONString(product));
return product;
}
}
10.启动⼯程,等到数据库表创建完毕之后,加⼊测试数据
INSERT INTO t_shop_product VALUE(NULL,'⼩⽶','1000','5000');
INSERT INTO t_shop_product VALUE(NULL,'华为','2000','5000');
INSERT INTO t_shop_product VALUE(NULL,'苹果','3000','5000');
INSERT INTO t_shop_product VALUE(NULL,'OPPO','4000','5000');
11.通过浏览器访问服务
2.7 创建订单微服务
1.创建shop-order-api项⽬,然后在pom.xml⽂件中添加下⾯内容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xiaoge</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-order-api</artifactId>
<!--依赖-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
2 创建实体类
package com.xiaoge.entity;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
//订单
@Entity(name = "t_shop_order")
@Data
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long oid;//订单id
//⽤户
private Long uid;//⽤户id
private String username;//⽤户名
//商品
private Long pid;//商品id
private String pname;//商品名称
private Double pprice;//商品单价
//数量
private Integer number;//购买数量
}
3.创建shop-order-server项⽬,然后在pom.xml⽂件中添加下⾯内容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xiaoge</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-order-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.56</version>
</dependency>
<dependency>
<groupId>com.xiaoge</groupId>
<artifactId>shop-order-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
4.编写启动类OrderServer.java
package com.xiaoge;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderServer {
public static void main(String[] args) {
SpringApplication.run(OrderServer.class, args);
}
}
5.编写配置⽂件application.yml
server:
port: 8091
# tomcat:
# threads:
# max: 10
spring:
application:
name: order-service
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop-order?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: root
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
6.在数据库中创建shop-order的数据库
7.创建OrderDao
package com.xiaoge.dao;
import com.xiaoge.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderDao extends JpaRepository<Order, Long> {
}
8.创建OrderService接⼝和实现类
package com.xiaoge.service.impl;
import com.alibaba.fastjson.JSON;
import com.xiaoge.dao.OrderDao;
import com.xiaoge.entity.Order;
import com.xiaoge.entity.Product;
import com.xiaoge.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Override
public Order createOrder(Long productId,Long userId) {
log.info("接收到{}号商品的下单请求,接下来调⽤商品微服务查询此商品信息",productId);
//远程调⽤商品微服务,查询商品信息
Product product = null;
log.info("查询到{}号商品的信息,内容是:{}", productId,JSON.toJSONString(product));
//创建订单并保存
Order order = new Order();
order.setUid(userId);
order.setUsername("叩丁狼教育");
order.setPid(productId);
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderDao.save(order);
log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
return order;
}
}
9.创建Controller
package com.xiaoge.controller;
import com.xiaoge.entity.Order;
import com.xiaoge.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping("/save")
public Order order(Long pid, Long uid) {
return orderService.createOrder(pid, uid);
}
}
2.8 服务间如何进⾏远程调⽤
商品微服务已经提供了数据接⼝了,订单微服务应该如何去调⽤呢?
其实就是如何通过Java代码去调⽤⼀个http的接⼝地址,我们可以使⽤RestTemplate来进⾏调⽤.
1.在启动类上添加RestTemplate的bean配置
package com.xiaoge;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class OrderServer {
public static void main(String[] args) {
SpringApplication.run(OrderServer.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
2.在OrderServiceImpl中注⼊RestTemplate并实现远程调⽤
package com.xiaoge.service.impl;
import com.alibaba.fastjson.JSON;
import com.xiaoge.dao.OrderDao;
import com.xiaoge.entity.Order;
import com.xiaoge.entity.Product;
import com.xiaoge.feign.ProductFeignClientApi;
import com.xiaoge.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private RestTemplate restTemplate;
@Override
public Order createOrder(Long productId, Long userId) {
log.info("接收到{}号商品的下单请求,接下来调⽤商品微服务查询此商品信息",productId);
Product product = restTemplate.getForObject("http://localhost:8081/product/" + productId, Product.class);
log.info("查询到{}号商品的信息,内容是:{}", productId, JSON.toJSONString(product));
//创建订单并保存
Order order = new Order();
order.setUid(userId);
order.setUsername("music啸");
order.setPid(productId);
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderDao.save(order);
log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
return order;
}
}
虽然我们已经可以实现微服务之间的调⽤。但是我们把服务提供者的⽹络地址(ip,端⼝)等硬编码到了代码中,这种做法存在许多问题:
-
⼀旦服务提供者地址变化,就需要⼿⼯修改代码
-
⼀旦是多个服务提供者,⽆法实现负载均衡功能
-
⼀旦服务变得越来越多,⼈⼯维护调⽤关系困难
那么应该怎么解决呢, 这时候就需要通过注册中⼼动态的实现服务治理。
服务治理 Nacos Discovery
3.1 什么是服务治理
服务治理是微服务架构中最核⼼最基本的模块。⽤于实现各个微服务的⾃动化注册与发现。
服务注册: 在服务治理框架中,都会构建⼀个注册中⼼,每个服务单元向注册中⼼登记⾃⼰提供服务的详细信息。并在注册中⼼形成⼀张服务的清单,服务注册中⼼需要以⼼跳的⽅式去监测清单中的服务是否可⽤,如果不可⽤,需要在服务清单中剔除不可⽤的服务。
服务发现: 服务调⽤⽅向服务注册中⼼咨询服务,并获取所有服务的实例清单,实现对具体服务实例的访问。
通过上⾯的调⽤图会发现,除了微服务,还有⼀个组件是服务注册中⼼,它是微服务架构⾮常重要的⼀个组件,在微服务架构⾥主要起到了协调者的⼀个作⽤。注册中⼼⼀般包含如下⼏个功能:
- 服务发现:
服务注册:保存服务提供者和服务调⽤者的信息
服务订阅:服务调⽤者订阅服务提供者的信息,注册中⼼向订阅者推送提供者的信息
- 服务健康检测
检测服务提供者的健康情况,如果发现异常,执⾏服务剔除
3.2 常⻅注册中⼼
-
Zookeeper
Zookeeper是⼀个分布式服务框架,是Apache Hadoop 的⼀个⼦项⽬,它主要是⽤来解决分布式应⽤中经常遇到的⼀些数据管理问题,如:统⼀命名服务、状态同步服务、集群管理、分布式应⽤配置项的管理等。
-
Eureka
Eureka是Springcloud Netflflix中的重要组件,主要作⽤就是做服务注册和发现。但是现在已经闭源
-
Consul
Consul是基于GO语⾔开发的开源⼯具,主要⾯向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能。Consul的功能都很实⽤,其中包括:服务注册/发现、健康检查、Key/Value存储、多数据中⼼和分布式⼀致性保证等特性。Consul本身只是⼀个⼆进制的可执⾏⽂件,所以安装和部署都⾮常简单,只需要从官⽹下载后,在执⾏对应的启动脚本即可。
-
Nacos
Nacos是⼀个更易于构建云原⽣应⽤的动态服务发现、配置管理和服务管理平台。它是 SpringCloud Alibaba 组件之⼀,负责服务注册发现和服务配置。
3.3 Nacos 简介
Nacos 致⼒于帮助您发现、配置和管理微服务。Nacos 提供了⼀组简单易⽤的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
从上⾯的介绍就可以看出,Nacos的作⽤就是⼀个注册中⼼,⽤来管理注册上来的各个微服务。
核⼼功能点:
-
服务注册: Nacos Client会通过发送REST请求想Nacos Server注册⾃⼰的服务,提供⾃身的元数据,⽐如IP地址,端⼝等信息。Nacos Server接收到注册请求后,就会把这些元数据存储到⼀个双层的内存Map中。
-
服务⼼跳: 在服务注册后,Nacos Client会维护⼀个定时⼼跳来维持统治Nacos Server,说明服务⼀致处于可⽤状态,防⽌被剔除,默认5s发送⼀次⼼跳
-
服务同步: Nacos Server集群之间会相互同步服务实例,⽤来保证服务信息的⼀致性。
-
服务发现: 服务消费者(Nacos Client)在调⽤服务提供的服务时,会发送⼀个REST请求给NacosServer,获取上⾯注册的服务清单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启⼀个定时任务拉取服务最新的注册表信息更新到本地缓存。
-
服务健康检查: Nacos Server 会开启⼀个定时任务来检查注册服务实例的健康情况,对于超过15s没有收到客户端⼼跳的实例会将他的healthy属性设置为false(客户端服务发现时不会发现),如果某个实例超过30s没有收到⼼跳,直接剔除该实例(被剔除的实例如果恢复发送⼼跳则会重新注册)
3.4 Nacos实战入门
接下来,我们就在现有的环境中加⼊nacos,并将我们的两个微服务注册上去。
3.4.1 搭建Nacos环境
-
安装Nacos
下载地址: https://github.com/alibaba/nacos/releases 下载zip格式的安装包,然后进⾏解压缩操作,上课使⽤的Nacos Server版本是1.3.2
-
启动Nacos
#切换⽬录 cd nacos/bin #命令启动 startup.cmd -m standalone
-
访问Nacos
打开浏览器输⼊http://localhost:8848/nacos,即可访问服务, 默认密码是nacos/nacos
3.4.2 将商品服务注册到Nacos
接下来开始修改 shop-product-server 模块的代码, 将其注册到nacos服务上
-
在pom.xml中添加Nacos的依赖
<!--nacos客户端--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
-
在主类上添加**@EnableDiscoveryClient**注解
@SpringBootApplication @EnableDiscoveryClient public class ProductServer { public static void main(String[] args) { SpringApplication.run(ProductServer.class,args); } }
-
在application.yml中添加Nacos服务的地址
spring: cloud: nacos: discovery: server-addr: localhost:8848
-
启动服务, 观察Nacos的控制⾯板中是否有注册上来的商品微服务
3.4.3 将订单服务注册到Nacos
接下来开始修改 shop-order-server 模块的代码, 将其注册到nacos服务上
-
在pom.xml中添加Nacos的依赖
<!--nacos客户端--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
-
在主类上添加**@EnableDiscoveryClient**注解
package com.xiaoge; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication public class OrderServer { public static void main(String[] args) { SpringApplication.run(OrderServer.class, args); } }
-
在application.yml中添加Nacos服务的地址
spring: cloud: nacos: discovery: server-addr: localhost:8848
-
启动服务, 观察Nacos的控制⾯板中是否有注册上来的订单微服务
-
修改OrderServiceImpl, 实现微服务调⽤
package com.xiaoge.service.impl; import com.alibaba.fastjson.JSON; import com.xiaoge.dao.OrderDao; import com.xiaoge.entity.Order; import com.xiaoge.entity.Product; import com.xiaoge.feign.ProductFeignClientApi; import com.xiaoge.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; import java.util.List; @Service @Slf4j public class OrderServiceImpl implements OrderService { @Autowired private OrderDao orderDao; @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; @Transactional @Override public Order createOrder(Long productId, Long userId) { log.info("接收到{}号商品的下单请求,接下来调⽤商品微服务查询此商品信息", productId); //从nacos中获取服务地址 ServiceInstance instance = discoveryClient.getInstances("product-service").get(0); String url = instance.getHost()+":"+instance.getPort(); //远程调⽤商品微服务,查询商品信息 Product product = restTemplate.getForObject("http://"+url+"/product/"+productId,Product.class); log.info("查询到{}号商品的信息,内容是:{}", productId, JSON.toJSONString(product)); //创建订单并保存 Order order = new Order(); order.setUid(userId); order.setUsername("music啸"); order.setPid(productId); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1); orderDao.save(order); log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order)); return order; } }
远程调⽤负载均衡 Ribbon
4.1 什么是负载均衡
通俗的讲, 负载均衡就是将负载(⼯作任务,访问请求)进⾏分摊到多个操作单元(服务器,组件)上进⾏执⾏。
根据负载均衡发⽣位置的不同,⼀般分为服务端负载均衡和客户端负载均衡。
服务端负载均衡指的是发⽣在服务提供者⼀⽅,⽐如常⻅的Nginx负载均衡
⽽客户端负载均衡指的是发⽣在服务请求的⼀⽅,也就是在发送请求之前已经选好了由哪个实例处理请求
我们在微服务调⽤关系中⼀般会选择客户端负载均衡,也就是在服务调⽤的⼀⽅来决定服务由哪个提供者执⾏。
4.2 ⾃定义负载均衡
-
通过idea再启动⼀个 shop-product 微服务,设置其端⼝为8082
-
通过nacos查看微服务的启动情况
-
修改 OrderServiceImpl 的代码,实现负载均衡
package com.xiaoge.service.impl; import com.alibaba.fastjson.JSON; import com.xiaoge.dao.OrderDao; import com.xiaoge.entity.Order; import com.xiaoge.entity.Product; import com.xiaoge.feign.ProductFeignClientApi; import com.xiaoge.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; import java.util.List; @Service @Slf4j public class OrderServiceImpl implements OrderService { @Autowired private OrderDao orderDao; @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; @Transactional @Override public Order createOrder(Long productId, Long userId) { log.info("接收到{}号商品的下单请求,接下来调⽤商品微服务查询此商品信息", productId); //从nacos中获取服务地址 //⾃定义规则实现随机挑选服务 List<ServiceInstance> instances = discoveryClient.getInstances("product-service"); int index = new Random().nextInt(instances.size()); ServiceInstance instance = instances.get(index); String url = instance.getHost()+":"+instance.getPort(); //远程调⽤商品微服务,查询商品信息 Product product = restTemplate.getForObject("http://"+url+"/product/"+productId,Product.class); log.info("查询到{}号商品的信息,内容是:{}", productId, JSON.toJSONString(product)); //创建订单并保存 Order order = new Order(); order.setUid(userId); order.setUsername("music啸"); order.setPid(productId); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1); orderDao.save(order); log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order)); return order; } }
-
启动两个服务提供者和⼀个服务消费者,多访问⼏次消费者测试效果
4.3 基于Ribbon实现负载均衡
Ribbon是Spring Cloud的⼀个组件, 它可以让我们使⽤⼀个注解就能轻松的搞定负载均衡
-
在RestTemplate 的⽣成⽅法上添加@LoadBalanced注解
@Bean @LoadBalanced // todo 这个注解使用了Ribbon中的负载均衡, 它会去读取本地缓存中的服务信息去调用(注意:前提是该服务要能拉取到nacos服务列表) public RestTemplate restTemplate() { return new RestTemplate(); }
-
修改OrderServiceImpl服务调⽤的⽅法
package com.xiaoge.service.impl; import com.alibaba.fastjson.JSON; import com.xiaoge.dao.OrderDao; import com.xiaoge.entity.Order; import com.xiaoge.entity.Product; import com.xiaoge.feign.ProductFeignClientApi; import com.xiaoge.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; import java.util.List; @Service @Slf4j public class OrderServiceImpl implements OrderService { @Autowired private OrderDao orderDao; @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; @Transactional @Override public Order createOrder(Long productId, Long userId) { log.info("接收到{}号商品的下单请求,接下来调⽤商品微服务查询此商品信息", productId); //远程调⽤商品微服务,查询商品信息 Product product = restTemplate.getForObject("http://product-service/product/"+productId,Product.class); log.info("查询到{}号商品的信息,内容是:{}", productId, JSON.toJSONString(product)); //创建订单并保存 Order order = new Order(); order.setUid(userId); order.setUsername("music啸"); order.setPid(productId); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1); orderDao.save(order); log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order)); return order; } }
-
为了更直观看到请求是进⾏负载均衡了,我们修改⼀下ProductController代码
package com.xiaoge.controller; import com.alibaba.fastjson.JSON; import com.xiaoge.entity.Product; import com.xiaoge.service.ProductService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Slf4j public class ProductController { @Autowired private ProductService productService; @Value("${server.port}") private String port; //商品信息查询 @RequestMapping("/product/{pid}") public Product findByPid(@PathVariable("pid") Long pid) { log.info("接下来要进⾏{}号商品信息的查询", pid); Product product = productService.findByPid(pid); product.setPname(product.getPname() + "data, from " + port); log.info("商品信息查询成功,内容为{}", JSON.toJSONString(product)); return product; } }
-
调⽤订单保存的⽅法,查看⽇志.
默认情况下,采取的是ZoneAvoidanceRule的策略,复合判断server所在区域的性能和server的可⽤性选择server
Ribbon⽀持的负载均衡策略
Ribbon内置了多种负载均衡策略,内部负载均衡的顶级接⼝为
com.netflix.loadbalancer.IRule , 具体的负载策略如下图所示:
策略名 策略描述 实现说明 BestAvailableRule 选择⼀个最⼩的并发请求的server 逐个考察Server,如果Server被tripped了,则忽略,在选择其中ActiveRequestsCount最⼩的server AvailabilityFilteringRule 先过滤掉故障实例,再选择并发较⼩的实例; 使⽤⼀个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查status⾥记录的各个server的运⾏状态 WeightedResponseTimeRule 根据相应时间分配⼀个weight,相应时间越⻓,weight越⼩,被选中的可能性越低。 ⼀个后台线程定期的从status⾥⾯读取评价响应时间,为每个server计算⼀个weight。Weight的计算也⽐较简单responsetime 减去每个server⾃⼰平均的responsetime是server的权 RetryRule 对选定的负载均衡策略机上重试机制。 在⼀个配置时间段内当选择server不成功,则⼀直尝试使⽤subRule的⽅式选择⼀个可⽤的server RoundRobinRule 轮询⽅式轮询选择server 轮询index,选择index对应位置的server RandomRule 随机选择⼀个server 在index上随机,选择index对应位置的server ZoneAvoidanceRule(默认) 复合判断server所在区域的性能和server的可⽤性选择server 使⽤ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前⼀个判断判定⼀个zone的运⾏性能是否可⽤,剔除不可⽤的zone(的所有server),AvailabilityPredicate⽤于过滤掉连接数过多的Server。 我们可以通过修改配置来调整Ribbon的负载均衡策略,在order-server项⽬的application.yml中增加如下配置:
product-service: # 调⽤的提供者的名称 ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 使用随机算法
远程调用Feign
5.1 什么是Feign
Feign是Spring Cloud提供的⼀个声明式的伪Http客户端, 它使得调⽤远程服务就像调⽤本地服务⼀样简单, 只需要创建⼀个接⼝并添加⼀个注解即可。
Nacos很好的兼容了Feign, Feign默认集成了 Ribbon, 所以在Nacos下使⽤Fegin默认就实现了负载均衡的效果。
Feign处理流程
5.2 订单微服务集成Feign
-
在shop-order-server项⽬的pom⽂件加⼊Fegin的依赖
<!--fegin组件--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
在启动类OrderServer.java上添加Fegin的扫描注解,注意扫描路径(默认扫描当前包及其⼦包)
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class OrderServer { public static void main(String[] args) { SpringApplication.run(OrderServer.class,args); } }
-
在shop-order-server项⽬中新增接⼝ProductFeignApi
package com.xiaoge.feign; import com.xiaoge.entity.Product; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; //name的名称⼀定要和订单服务的服务名保持⼀致 @FeignClient(value = "product-service") public interface ProductFeignClientApi { @RequestMapping("/product/{pid}") Product findByPid(@PathVariable("pid") Long pid); }
-
修改OrderServiceImpl.java的远程调⽤⽅法
package com.xiaoge.service.impl; import com.alibaba.fastjson.JSON; import com.xiaoge.dao.OrderDao; import com.xiaoge.entity.Order; import com.xiaoge.entity.Product; import com.xiaoge.feign.ProductFeignClientApi; import com.xiaoge.service.OrderService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service @Slf4j public class OrderServiceImpl implements OrderService { @Autowired private OrderDao orderDao; @Autowired private ProductFeignClientApi productFeignClientApi; @Transactional @Override public Order createOrder(Long productId, Long userId) { log.info("接收到{}号商品的下单请求,接下来调⽤商品微服务查询此商品信息", productId); // todo feign 远程调⽤商品微服务,查询商品信息 Product product = productFeignClientApi.findByPid(productId); log.info("查询到{}号商品的信息,内容是:{}", productId, JSON.toJSONString(product)); //创建订单并保存 Order order = new Order(); order.setUid(userId); order.setUsername("music啸"); order.setPid(productId); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1); orderDao.save(order); log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order)); return order; } }
-
重启订单服务,并验证.
服务熔断降级 Sentinel
6.1 高并发带来的问题
在微服务架构中,我们将业务拆分成⼀个个的服务,服务与服务之间可以相互调⽤,但是由于⽹络原因或者⾃身的原因,服务并不能保证服务的100%可⽤,如果单个服务出现问题,调⽤这个服务就会出现⽹络延迟,此时若有⼤量的⽹络涌⼊,会形成任务堆积,最终导致服务瘫痪。
接下来,模拟⼀个高并发的场景
-
在订单服务中新建SentinelController.java
@RestController public class SentinelController { @RequestMapping("/sentinel1") public String sentinel1() { //模拟⼀次⽹络延时 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return "sentinel1"; } @RequestMapping("/sentinel2") public String sentinel2() { return "测试⾼并发下的问题"; } }
-
修改配置⽂件中tomcat的并发数
server: port: 8091 tomcat: threads: max: 10 #tomcat的最⼤并发值修改为10
-
接下来使⽤压测⼯具,对请求进⾏压⼒测试
下载地址https://jmeter.apache.org/
-
第⼀步:修改配置,并启动软件
进⼊bin⽬录,修改jmeter.properties⽂件中的语⾔⽀持为language=zh_CN,然后点击jmeter.bat启动软件。
-
第⼆步:添加线程组
-
第三步:配置线程并发数
-
第四步:添加Http请求
-
第五步:配置取样,并启动测试
-
第六步:访问 http://localhost:8091/sentinel2 观察结果
结论:此时会发现, 由于sentinel1⽅法囤积了⼤量请求, 导致sentinel2⽅法的访问出现了问题,这就是服务雪崩的雏形。
-
6.2 服务器雪崩效应
在分布式系统中,由于⽹络原因或⾃身的原因,服务⼀般⽆法保证 100% 可⽤。如果⼀个服务出现了问题,调⽤这个服务就会出现线程阻塞的情况,此时若有⼤量的请求涌⼊,就会出现多条线程阻塞等待,进⽽导致服务瘫痪。
由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩效应” 。
-
情景1: 微服务之间相互调⽤,关系复杂,正常情况如下图所示:
-
情景2:某个时刻,服务A挂了,服务B和服务C依然在调⽤服务A
-
情景3:由于服务A挂了,导致服务C和服务B⽆法得到服务A的响应,这时候服务C和服务B由于⼤量线程积压,最终导致服务C和服务B挂掉.
-
情景4: 相同道理,由于服务之间有关联,所以会导致整个调⽤链上的所有服务都挂掉.
服务器的雪崩效应其实就是由于某个微⼩的服务挂了,导致整⼀⼤⽚的服务都不可⽤.类似⽣活中的雪崩效应,由于落下的最后⼀⽚雪花引发了雪崩的情况.
雪崩发⽣的原因多种多样,有不合理的容量设计,或者是⾼并发下某⼀个⽅法响应变慢,亦或是某台机器的资源耗尽。我们⽆法完全杜绝雪崩源头的发⽣,只有做好⾜够的容错,保证在⼀个服务发⽣问题,不会影响到其它服务的正常运⾏。
6.3 常见容方案
要防⽌雪崩的扩散,我们就要做好服务的容错,容错说⽩了就是保护⾃⼰不被猪队友拖垮的⼀些措施, 下⾯介绍常⻅的服务容错思路和组件。
常⻅的容错思路
常⻅的容错思路有隔离、超时、限流、熔断、降级这⼏种,下⾯分别介绍⼀下。
-
隔离机制: ⽐如服务A内总共有100个线程, 现在服务A可能会调⽤服务B,服务C,服务D.我们在服务A进⾏远程调⽤的时候,给不同的服务分配固定的线程,不会把所有线程都分配给某个微服务. ⽐如调⽤服务B分配30个线程,调⽤服务C分配30个线程,调⽤服务D分配40个线程. 这样进⾏资源的隔离,保证即使下游某个服务挂了,也不⾄于把服务A的线程消耗完。⽐如服务B挂了,这时候最多只会占⽤服务A的30个线程,服务A还有70个线程可以调⽤服务C和服务D.
-
超时机制: 在上游服务调⽤下游服务的时候,设置⼀个最⼤响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。
-
限流机制: 限流就是限制系统的输⼊和输出流量已达到保护系统的⽬的。为了保证系统的稳固运⾏,⼀旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的⽬的。
-
熔断机制: 在互联⽹系统中,当下游服务因访问压⼒过⼤⽽响应变慢或失败,上游服务为了保护系统整体的可⽤性,可以暂时切断对下游服务的调⽤。这种牺牲局部,保全整体的措施就叫做熔断。
服务熔断⼀般有三种状态:
-
熔断关闭状态(Closed)
服务没有故障时,熔断器所处的状态,对调⽤⽅的调⽤不做任何限制
-
熔断开启状态(Open)
后续对该服务接⼝的调⽤不再经过⽹络,直接执⾏本地的fallback⽅法
-
半熔断状态(Half-Open)
尝试恢复服务调⽤,允许有限的流量调⽤该服务,并监控调⽤成功率。如果成功率达到预期,则说明服务已恢复,进⼊熔断关闭状态;如果成功率仍旧很低,则重新进⼊熔断关闭状态。
-
-
降级机制: 降级其实就是为服务提供⼀个兜底⽅案,⼀旦服务⽆法正常调⽤,就使⽤兜底⽅案。
6.4 常见的容错组件
-
Hystrix
Hystrix是由Netflflix开源的⼀个延迟和容错库,⽤于隔离访问远程系统、服务或者第三⽅库,防⽌级联失败,从⽽提升系统的可⽤性与容错性。
-
Resilience4J
Resilicence4J⼀款⾮常轻量、简单,并且⽂档⾮常清晰、丰富的熔断⼯具,这也是Hystrix官⽅推荐的替代产品。不仅如此,Resilicence4j还原⽣⽀持Spring Boot 1.x/2.x,⽽且监控也⽀持和prometheus等多款主流产品进⾏整合。
-
Sentinel
Sentinel 是阿⾥巴巴开源的⼀款断路器实现,本身在阿⾥内部已经被⼤规模采⽤,⾮常稳定。
6.5 Sentinel ⼊⻔
6.5.1 什么是 Sentinel
Sentinel (分布式系统的流量防卫兵) 是阿⾥开源的⼀套⽤于服务容错的综合性解决⽅案。它以流量为切⼊点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性
Sentinel 具有以下特征:
-
丰富的应⽤场景:Sentinel 承接了阿⾥巴巴近 10 年的双⼗⼀⼤促流量的核⼼场景, 例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填⾕、集群流量控制、实时熔断下游不可⽤应⽤等。
-
完备的实时监控:Sentinel 提供了实时的监控功能。通过控制台可以看到接⼊应⽤的单台机器秒级数据, 甚⾄ 500 台以下规模的集群的汇总运⾏情况。
-
⼴泛的开源⽣态:Sentinel 提供开箱即⽤的与其它开源框架/库的整合模块, 例如与 SpringCloud、Dubbo、gRPC 的整合。只需要引⼊相应的依赖并进⾏简单的配置即可快速地接⼊Sentinel。
Sentinel 分为两个部分:
-
核⼼库(Java 客户端)不依赖任何框架/库,能够运⾏于所有 Java 运⾏时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的⽀持。
-
控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运⾏,不需要额外的 Tomcat 等应⽤容器。
6.5.2 订单微服务集成 Sentinel
为微服务集成Sentinel⾮常简单, 只需要加⼊Sentinel的依赖即可在shop-order-server项⽬的pom⽂件中添加如下依赖
<!--sentinel组件-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
6.5.3 安装 Sentinel 控制台
Sentinel 提供⼀个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。
-
下载jar包 https://github.com/alibaba/Sentinel/releases
-
启动控制台
# 直接使⽤jar命令启动项⽬(控制台本身是⼀个SpringBoot项⽬) java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.0.jar
-
修改shop-order-server项⽬中的配置⽂件application.yml,新增如下配置:
spring: cloud: sentinel: transport: port: 9999 #跟控制台交流的端⼝,随意指定⼀个未使⽤的端⼝即可 dashboard: localhost:8080 # 指定控制台服务的地址
-
通过浏览器访问localhost:8080 进⼊控制台 ( 默认⽤户名密码是 sentinel/sentinel )注意: 默认是没显示order-service的,需要访问⼏次接⼝,然后再刷新sentinel管控台才可以看到.
6.5.4 实现⼀个接口的限流
第⼀步: 簇点链路—>流控
第⼆步: 在单机阈值填写⼀个数值,表示每秒上限的请求数
第三步:通过控制台快速频繁访问, 观察效果
6.5.5 Sentinel 容错的维度
流量控制:流量控制在⽹络传输中是⼀个常⽤的概念,它⽤于调整⽹络包的数据。任意时间到来的请求往往是随机不可控的,⽽系统的处理能⼒是有限的。我们需要根据系统的处理能⼒对流量进⾏控制。
熔断降级:当检测到调⽤链路中某个资源出现不稳定的表现,例如请求响应时间⻓或异常⽐例升⾼的时候,则对这个资源的调⽤进⾏限制,让请求快速失败,避免影响到其它的资源⽽导致级联故障。
系统负载保护:Sentinel 同时提供系统维度的⾃适应保护能⼒。当系统负载较⾼的时候,如果还持续让请求进⼊可能会导致系统崩溃,⽆法响应。在集群环境下,会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在⼀个边缘状态的时候,Sentinel 提供了对应的保护机制,让系统的⼊⼝流量和系统的负载达到⼀个平衡,保证系统在能⼒范围之内处理最多的请求。
6.5.6 Sentinel 规则种类
Sentinel主要提供了这五种的流量控制,接下来我们每种演示⼀下
6.6 Sentinel 规则 - 流控
6.6.1 流控规则
流量控制,其原理是监控应⽤流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时对流量进⾏控制,以避免被瞬时的流量⾼峰冲垮,从⽽保障应⽤的⾼可⽤性。
资源名:唯⼀名称,默认是请求路径,可⾃定义
针对来源:指定对哪个微服务进⾏限流,默认指default,意思是不区分来源,全部限制
阈值类型 / 单机阈值:
-
QPS(每秒请求数量): 当调⽤该接⼝的QPS达到阈值的时候,进⾏限流
-
线程数:当调⽤该接⼝的线程数达到阈值的时候,进⾏限流
是否集群:暂不需要集群
6.6.1.1 QPS 流控
前⾯6.4.4案例就是演示的QPS流控
6.6.1.2 线程数流控
-
删除掉之前的QPS流控,新增线程数流控
-
在Jmeter中新增线程
-
访问 http://localhost:8091/sentinel2 会发现已经被限流
6.6.2 流控模式
点击上⾯设置流控规则的编辑按钮,然后在编辑⻚⾯点击高级选项,会看到有流控模式⼀栏。
sentinel共有三种流控模式,分别是:
-
直接(默认):接⼝达到限流条件时,开启限流
-
关联:当关联的资源达到限流条件时,开启限流 [适合做应⽤让步]
-
链路:当从某个接⼝过来的资源达到限流条件时,开启限流
6.6.2.1 直接流控模式
前⾯演示的案例就是这种.
6.6.2.2 关联流控模式
关联流控模式指的是,当指定接⼝关联的接⼝达到限流条件时,开启对指定接⼝开启限流。
场景:当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。⽐如对数据库同⼀个字段的读操作和写操作存在争抢,读的速度过⾼会影响写得速度,写的速度过⾼会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使⽤关联限流来避免具有关联关系的资源之间过度的争抢.
-
在SentinelController.java中增加⼀个⽅法,重启订单服务
@RequestMapping("/sentinel3") public String sentinel3(){ return "sentinel3"; }
-
配置限流规则, 将流控模式设置为关联,关联资源设置为的 /sentinel2
-
通过postman软件向/sentinel2连续发送请求,注意QPS⼀定要⼤于3
-
访问/sentinel3,会发现已经被限流
6.5.2.3 链路流控模式
链路流控模式指的是,当从某个接⼝过来的资源达到限流条件时,开启限流。
-
在shop-order-server项⽬的application.yml⽂件中新增如下配置:
spring: cloud: sentinel: web-context-unify: false #关闭链路折叠 比如说(/trace1/tranceService /trace2/tranceService它就会把它当成/tranceService, 所以这里要把它的折叠给关了)
-
在shop-order-server项⽬中新增TraceServiceImpl.java
package com.xiaoge.service.impl; import com.alibaba.csp.sentinel.annotation.SentinelResource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Service @Slf4j public class TraceServiceImpl { @SentinelResource(value = "tranceService") public void tranceService() { log.info("调⽤tranceService⽅法"); } }
-
在shop-order-server项⽬中新增TraceController.java
package com.xiaoge.controller; import com.xiaoge.service.impl.TraceServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TraceController { @Autowired private TraceServiceImpl traceService; @RequestMapping("/trace1") public String trace1() { traceService.tranceService(); return "trace1"; } @RequestMapping("/trace2") public String trace2() { traceService.tranceService(); return "trace2"; } }
-
重新启动订单服务并添加链路流控规则
-
分别通过 /trace1 和 /trace2 访问, 发现/trace1没问题, /trace2的被限流了
6.6.3 流控效果
-
快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果
-
Warm Up:它从开始阈值到最⼤QPS阈值会有⼀个缓冲阶段,⼀开始的阈值是最⼤QPS阈值的1/3,然后慢慢增⻓,直到最⼤阈值,适⽤于将突然增⼤的流量转换为缓步增⻓的场景。
-
排队等待:让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设置⼀个超时时间,当请求超过超时间时间还未处理,则会被丢弃。
6.7 Sentinel 规则 - 降级
降级规则就是设置当满⾜什么条件的时候,对服务进⾏降级。Sentinel提供了三个衡量条件:
-
慢调用比例: 选择以慢调⽤⽐例作为阈值,需要设置允许的慢调⽤ RT(即最⼤的响应时间),请求的响应时间⼤于该值则统计为慢调⽤。当单位统计时⻓内请求数⽬⼤于设置的最⼩请求数⽬,并且慢调⽤的⽐例⼤于阈值,则接下来的熔断时⻓内请求会⾃动被熔断。经过熔断时⻓后熔断器会进⼊探测恢复状态(HALF-OPEN 状态),若接下来的⼀个请求响应时间⼩于设置的慢调⽤ RT 则结束熔断,若⼤于设置的慢调⽤ RT 则会再次被熔断。
-
异常比例: 当单位统计时⻓内请求数⽬⼤于设置的最⼩请求数⽬,并且异常的⽐例⼤于阈值,则接下来的熔断时⻓内请求会⾃动被熔断。经过熔断时⻓后熔断器会进⼊探测恢复状态(HALF-OPEN状态),若接下来的⼀个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常⽐率的阈值范围是 [0.0, 1.0] ,代表 0% - 100%。
-
异常数:当单位统计时⻓内的异常数⽬超过阈值之后会⾃动进⾏熔断。经过熔断时⻓后熔断器会进⼊探测恢复状态(HALF-OPEN 状态),若接下来的⼀个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
6.7.1 慢调⽤⽐例案例
-
在shop-order-server项⽬中新增FallBackController.java类,代码如下:
@RestController @Slf4j public class FallBackController { @RequestMapping("/fallBack1") public String fallBack1(){ try { log.info("fallBack1执⾏业务逻辑"); //模拟业务耗时 TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } return "fallBack1"; } }
-
新增降级规则:
上⾯配置表示,如果在1S之内,有【超过1个的请求】且这些请求中【响应时间>最⼤RT】的【请求数量⽐例>10%】,就会触发熔断,在接下来的10s之内都不会调⽤真实⽅法,直接⾛降级⽅法。
⽐如: 最⼤RT=900,⽐例阈值=0.1,熔断时⻓=10,最⼩请求数=10
-
情况1: 1秒内的有20个请求,只有10个请求响应时间>900ms, 那慢调⽤⽐例=0.5,这种情况就会触发熔断
-
情况2: 1秒内的有20个请求,只有1个请求响应时间>900ms, 那慢调⽤⽐例=0.05,这种情况不会触发熔断
-
情况3: 1秒内的有8个请求,只有6个请求响应时间>900ms, 那慢调⽤⽐例=0.75,这种情况不会触发熔断,因为最⼩请求数这个条件没有满⾜.
注意: 我们做实验的时候把最⼩请求数设置为1,因为在1秒内,⼿动操作很难在1s内发两个请求过去,所以要做出效果,最好把最⼩请求数设置为1。
-
6.7.2 异常比例案例
-
在shop-order-server项⽬的FallBackController.java类新增fallBack2⽅法:
int i=0; @RequestMapping("/fallBack2") public String fallBack2(){ log.info("fallBack2执⾏业务逻辑"); //模拟出现异常,异常⽐例为33% if(++i%3==0){ throw new RuntimeException(); } return "fallBack2"; }
-
新增降级规则:
上⾯配置表示,在1s之内,,有【超过3个的请求】,异常⽐例30%的情况下,触发熔断,熔断时⻓为10s.
6.7.3 异常数案例
-
在shop-order-server项⽬的FallBackController.java类新增fallBack3⽅法:
@RequestMapping("/fallBack3") public String fallBack3(String name){ log.info("fallBack3执⾏业务逻辑"); if("xiaogeCode".equals(name)){ throw new RuntimeException(); } return "fallBack3"; }
-
新增降级规则
上⾯配置表示,在1s之内,,有【超过3个的请求】,请求中超过2个请求出现异常就会触发熔断,
熔断时⻓为10s
6.8 Sentinel 规则 - 热点
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最⾼的 Top K 数据,并对其访问进⾏限制。⽐如:
-
商品 ID 为参数,统计⼀段时间内最常购买的商品 ID 并进⾏限制
-
⽤户 ID 为参数,针对⼀段时间内频繁访问的⽤户 ID 进⾏限制
热点参数限流会统计传⼊参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调⽤进⾏限流。热点参数限流可以看做是⼀种特殊的流量控制,仅对包含热点参数的资源调⽤⽣效。
-
在shop-order-server项⽬中新增HotSpotController.java,代码如下:
package com.xiaoge.controller; import com.alibaba.csp.sentinel.annotation.SentinelResource; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.TimeUnit; @RestController @Slf4j public class HotSpotController { @RequestMapping("/hotSpot1") @SentinelResource(value = "hotSpot1") public String hotSpot1(Long productId) throws InterruptedException { log.info("访问编号为:{}的商品", productId); TimeUnit.SECONDS.sleep(1); return "hotSpot1"; } }
注意:⼀定需要在请求⽅法上贴@SentinelResource注解,否则热点规则⽆效
-
新增热点规则:
-
在热点规则中编辑规则,在编辑之前⼀定要先访问⼀下/hotSpot1,不然参数规则⽆法新增.
-
新增参数规则:
-
点击保存,可以看到已经新增了参数规则.
-
访问http://localhost:8091/hotSpot?productId=1 访问会降级
访问http://localhost:8091/hotSpot?productId=2 访问不会降级
6.9 Sentinel 规则 - 授权
很多时候,我们需要根据调⽤来源来判断该次请求是否允许放⾏,这时候可以使⽤ Sentinel 的来源访问控制(⿊⽩名单控制)的功能。来源访问控制根据资源的请求来源( origin )限制资源是否通过,若配置⽩名单则只有请求来源位于⽩名单内时才可通过;若配置⿊名单则请求来源位于⿊名单时不通过,其余的请求通过。
-
在shop-order-server中新建RequestOriginParserDefinition.java,定义请求来源如何获取
package com.xiaoge.controller; import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; @Component public class RequestOriginParserDefinition implements RequestOriginParser { @Override public String parseOrigin(HttpServletRequest request) { /** * 定义从请求的什么地⽅获取来源信息 * ⽐如我们可以要求所有的客户端需要在请求头中携带来源信息 */ String serviceName = request.getParameter("serviceName"); return serviceName; } }
-
在shop-order-server中新建AuthController.java,代码如下:
package com.xiaoge.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Slf4j public class AuthController { @RequestMapping("/auth1") public String auth1(String serviceName) { log.info("应⽤:{},访问接⼝", serviceName); return "auth1"; } }
-
新增授权规则
-
访问测试
访问http://localhost:8091/auth1?serviceName=pc 不能访问
访问http://localhost:8091/auth1?serviceName=app 可以访问
6.10 Sentinel 规则 - 系统规则
系统保护规则是从应⽤级别的⼊⼝流量进⾏控制,从单台机器的 load、CPU 使⽤率、平均 RT、⼊⼝QPS 和并发线程数等⼏个维度监控应⽤指标,让系统尽可能跑在最⼤吞吐量的同时保证系统整体的稳定性。
系统保护规则是应⽤整体维度的,⽽不是资源维度的,并且仅对入口流量⽣效。⼊⼝流量指的是进⼊应⽤的流量( EntryType.IN ),⽐如 Web 服务或 Dubbo 服务端接收的请求,都属于⼊⼝流量。
系统规则⽀持以下的模式:
-
Load ⾃适应(仅对 Linux/Unix-like 机器⽣效):系统的 load1 作为启发指标,进⾏⾃适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值⼀般是 CPUcores * 2.5 。
-
CPU usage(1.5.0+ 版本):当系统 CPU 使⽤率超过阈值即触发系统保护(取值范围 0.0-1.0),⽐较灵敏。
-
平均 RT:当单台机器上所有⼊⼝流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
-
并发线程数:当单台机器上所有⼊⼝流量的并发线程数达到阈值即触发系统保护。
-
入口 QPS:当单台机器上所有⼊⼝流量的 QPS 达到阈值即触发系统保护。
6.11 Sentinel ⾃定义异常返回
当前⾯设定的规则没有满⾜是,我们可以⾃定义异常返回.
-
FlowException 限流异常
-
DegradeException 降级异常
-
ParamFlowException 参数限流异常
-
AuthorityException 授权异常SystemBlockException 系统负载异常
在shop-order-server项⽬中定义异常返回处理类
package com.xiaoge.controller;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.alibaba.fastjson.JSON;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class ExceptionHandlerPage implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
response.setContentType("application/json;charset=utf-8");
ResultData data = null;
if (e instanceof FlowException) {
data = new ResultData(-1, "接⼝被限流了");
} else if (e instanceof DegradeException) {
data = new ResultData(-2, "接⼝被降级了");
} else if (e instanceof ParamFlowException) {
data = new ResultData(-3, "参数限流异常");
} else if (e instanceof AuthorityException) {
data = new ResultData(-4, "授权异常");
} else if (e instanceof SystemBlockException) {
data = new ResultData(-5, "接⼝被降级了...");
}
response.getWriter().write(JSON.toJSONString(data));
}
}
@Data
@AllArgsConstructor//全参构造
@NoArgsConstructor//⽆参构造
class ResultData {
private int code;
private String message;
}
6.12 @SentinelResource 的使⽤
在定义了资源点之后,我们可以通过Dashboard来设置限流和降级策略来对资源点进⾏保护。同时还能通过@SentinelResource来指定出现异常时的处理策略。
@SentinelResource ⽤于定义资源,并提供可选的异常处理和 fallback 配置项。
其主要参数如下:
属性 | 作⽤ |
---|---|
value | 资源名称,必需项(不能为空) |
entryType | entry 类型,可选项(默认为 EntryType.OUT ) |
blockHandler / blockHandlerClass | blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是public ,返回类型需要与原⽅法相匹配,参数类型需要和原⽅法相匹配并且最后加⼀个额外的参数,类型为BlockException 。blockHandler 函数默认需要和原⽅法在同⼀个类中。若希望使⽤其他类的函数,则可以指定blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则⽆法解析。 |
fallback / fallbackClass | fallback 函数名称,可选项,⽤于在抛出异常的时候提供fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore ⾥⾯排除掉的异常类型)进⾏处理。fallback 函数签名和位置要求:1. 返回值类型必须与原函数返回值类型⼀致;2.⽅法参数列表需要和原函数⼀致,或者可以额外多⼀个Throwable 类型的参数⽤于接收对应的异常。3.fallback 函数默认需要和原⽅法在同⼀个类中。若希望使⽤其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则⽆法解析。 |
defaultFallback | 默认的 fallback 函数名称,可选项,通常⽤于通⽤的fallback 逻辑(即可以⽤于很多服务或⽅法)。默认fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore ⾥⾯排除掉的异常类型)进⾏处理。若同时配置了 fallback 和 defaultFallback,则只有fallback 会⽣效。defaultFallback 函数签名要求:1. 返回值类型必须与原函数返回值类型⼀致;2. ⽅法参数列表需要为空,或者可以额外多⼀个Throwable 类型的参数⽤于接收对应的异常。3.defaultFallback 函数默认需要和原⽅法在同⼀个类中。若希望使⽤其他类的函数,则可以指定 fallbackClass为对应的类的 Class 对象,注意对应的函数必需为 static函数,否则⽆法解析。 |
exceptionsToIgnore | ⽤于指定哪些异常被排除掉,不会计⼊异常统计中,也不会进⼊ fallback 逻辑中,⽽是会原样抛出。 |
定义限流和降级后的处理⽅法
直接将限流和降级⽅法定义在⽅法中
package com.xiaoge.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class AnnoController {
@RequestMapping("/anno1")
@SentinelResource(value = "anno1",
blockHandler = "anno1BlockHandler", // todo 当前方法如果被限流或者降级会调用这个字符串对应的方法
fallback = "anno1Fallback" // todo 当方法报错之后, 会调用这个字符串对应方法
)
public String anno1(String name) {
if ("xiaogeCode".equals(name)) {
throw new RuntimeException();
}
return "anno1";
}
public String anno1BlockHandler(String name, BlockException ex) {
log.error("{}", ex);
return "接⼝被限流或者降级了";
}
//Throwable时进⼊的⽅法
public String anno1Fallback(String name, Throwable throwable) {
log.error("{}", throwable);
return "接⼝发⽣异常了";
}
}
6.13 Feign 整合 Sentinel
-
在shop-order-server项⽬的配置⽂件中开启feign对Sentinel的⽀持
# 开启feign整合sentinel feign: sentinel: enabled: true
-
创建容错类
package com.xiaoge.feign; import com.xiaoge.entity.Product; import org.springframework.stereotype.Component; /** * TODO * * @author <a href="mailto:1330137071@qq.com">Zhang Xiao</a> * @since */ @Component public class ProductFeignFallback implements ProductFeignClientApi { @Override public Product findByPid(Long pid) { Product product = new Product(); product.setPname("兜底商品"); return product; } }
-
在feign接⼝中定义容错类
package com.xiaoge.feign; import com.xiaoge.entity.Product; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; /** * TODO * * @author <a href="mailto:1330137071@qq.com">Zhang Xiao</a> * @since */ @FeignClient(value = "product-service", fallback = ProductFeignFallback.class) public interface ProductFeignClientApi { @RequestMapping("/product/{pid}") Product findByPid(@PathVariable("pid") Long pid); }
-
停⽌所有 商品服务,重启 shop-order 服务,访问请求,观察容错效果
可能上⾯的案例并不是特别恰当,我们只是通过案例来演示Feign集成Sentinel实现降级的效果. 接下来我们具体更贴切的案例来讲解Feign降级的作⽤.
⽐如我们在购物的时候,查看商品详情⻚⾯的时候,⾥⾯包含库存信息,商品详情信息,评论信息,这个需求包含的微服务如下:
假设现在评论服务宕机了,那是不是意味⽤户发出查看商品请求也⽆法正常显示了,商品都看不到了,那⽤户也⽆法进⾏下单的操作了. 但是对于⽤户来说,评论看不到并不影响他购物,所以这时候我们应该对评论服务进⾏及·降级处理,返回⼀个兜底数据(空数据),这样⽤户的查看商品请求能正常显示,只是评论数据看不到⽽已,这样的话,⽤户的下单请求也不会受到影响.
服务网关 Gateway
⼤家都都知道在微服务架构中,⼀个系统会被拆分为很多个微服务。那么作为客户端要如何去调⽤这么多的微服务呢?如果没有⽹关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调⽤。
这样的架构,会存在着诸多的问题:
-
客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
-
认证复杂,每个服务都需要独⽴认证。
-
微服务做集群的情况下,客户端并没有负责均衡的功能
上⾯的这些问题可以借助API 网关来解决。
所谓的API⽹关,就是指系统的统⼀⼊⼝,它封装了应⽤程序的内部结构,为客户端提供统⼀服务,⼀些与业务本身功能⽆关的公共逻辑可以在这⾥实现,诸如认证、鉴权、监控、路由转发等等。
添加上API⽹关之后,系统的架构图变成了如下所示:
⽹关是如何知道微服务的地址?⽹关如何进⾏负载均衡呢?
⽹关需要将⾃⼰的信息注册到注册中⼼上并且拉取其他微服务的信息,然后再调⽤的时候基于Ribbon实现负载均衡
7.1 常见网关介绍
-
Nginx+lua
使⽤nginx的反向代理和负载均衡可实现对api服务器的负载均衡及⾼可⽤,lua是⼀种脚本语⾔,可以来编
写⼀些简单的逻辑, nginx⽀持lua脚本
-
Kong
基于Nginx+Lua开发,性能⾼,稳定,有多个可⽤的插件(限流、鉴权等等)可以开箱即⽤。 问题:只⽀持Http协议;⼆次开发,⾃由扩展困难;提供管理API,缺乏更易⽤的管控、配置⽅式。
-
Zuul
Netflflix开源的⽹关,功能丰富,使⽤JAVA开发,易于⼆次开发 问题:缺乏管控,⽆法动态配置;依赖组件较多;处理Http请求依赖的是Web容器,性能不如Nginx,Spring Cloud Gateway
Spring公司为了替换Zuul⽽开发的⽹关服务,将在下⾯具体介绍。
注意:SpringCloud alibaba技术栈中并没有提供⾃⼰的⽹关,我们可以采⽤Spring Cloud Gateway来做⽹关
7.2 Gateway 简介
Spring Cloud Gateway是Spring公司基于Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的⽹关,它旨在为微服务架构提供⼀种简单有效的统⼀的 API 路由管理⽅式。它的⽬标是替代Netflflix Zuul,其不仅提供统⼀的路由⽅式,并且基于 Filter 链的⽅式提供了⽹关基本的功能,例如:安全,监控和限流。
优点:
-
性能强劲:是第⼀代⽹关Zuul的1.6倍
-
功能强⼤:内置了很多实⽤的功能,例如转发、监控、限流等
-
设计优雅,容易扩展
缺点:
-
其实现依赖Netty与WebFlux,不是传统的Servlet编程模型,学习成本⾼
-
不能将其部署在Tomcat、Jetty等Servlet容器⾥,只能打成jar包执⾏
-
需要Spring Boot 2.0及以上的版本,才⽀持
7.3 Gateway 快速⼊⻔
-
创建⼀个 api-gateway 的模块,导⼊相关依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.xiaoge</groupId> <artifactId>shop-parent</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>api-gateway</artifactId> <dependencies> <!--gateway⽹关--> <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.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> </project>
-
编写启动类
package com.xiaoge; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; /** * TODO * * @author <a href="mailto:1330137071@qq.com">Zhang Xiao</a> * @since */ @EnableDiscoveryClient @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
-
编写配置⽂件
server: port: 9000 spring: application: name: api-gateway cloud: nacos: discovery: server-addr: localhost:8848 gateway: discovery: locator: enabled: true # 让gateway可以发现nacos中的微服务 # 注意网关不配置路由规则, 它有默认的规则, 就是你访问的服务名必须是对应的服务的应用名, 它就会转发到对应的服务下, 即使我们配置了规则, 默认规则还是存在
-
启动测试
7.4 ⾃定义路由规则
- 在application.yml中添加新的路由规则
spring:
cloud:
# 注意网关不配置路由规则, 它有默认的规则, 就是你访问的服务名必须是对应的服务的应用名, 它就会转发到对应的服务下, 即使我们配置了规则, 默认规则还是存在
routes:
- id: product_route # 路由名称保持唯一
uri: lb://product-service # 符合条件的请求转发到那个服务, lb表示对服务进行负载均衡
predicates: # 拦截那些请求
- Path=/product-serv/**
filters: # 在转发请求之前, 将拦截到的路径的第一层路径删除掉
- StripPrefix=1
- id: order_route
uri: lb://order-service
predicates:
- Path=/order-serv/**
filters:
- StripPrefix=1
-
启动测试
7.5 Gateway 概念
路由(Route) 是 gateway 中最基本的组件之⼀,表示⼀个具体的路由信息载体。主要定义了下⾯的⼏个信息:
-
id,路由标识符,区别于其他 Route。
-
uri,路由指向的⽬的地 uri,即客户端请求最终被转发到的微服务。
-
order,⽤于多个 Route 之间的排序,数值越⼩排序越靠前,匹配优先级越⾼。
-
predicate,断⾔的作⽤是进⾏条件判断,只有断⾔都返回真,才会真正的执⾏路由。
-
filter,过滤器⽤于修改请求和响应信息。
执⾏流程图:
7.6 过滤器 Filter
过滤器就是在请求的传递过程中,对请求和响应做⼀些⼿脚.
在Gateway中, Filter的⽣命周期只有两个:“pre” 和 “post”。
-
PRE: 这种过滤器在请求被路由之前调⽤。我们可利⽤这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
-
POST:这种过滤器在路由到微服务以后执⾏。这种过滤器可⽤来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
在Gateway中,Filter的作⽤范围两种:
-
GatewayFilter:应⽤到单个路由或者⼀个分组的路由上。
-
GlobalFilter:应⽤到所有的路由上
7.6.1 局部路由过滤器
局部过滤器是针对单个路由的过滤器,在SpringCloud Gateway中内置了很多不同类型的⽹关路由过滤器,有兴趣同学可以⾃⾏了解,这⾥太多了,我们就不⼀⼀讲解,我们主要来讲⼀下⾃定义路由过滤器。
需求: 统计订单服务调⽤耗时.
-
编写Filter类,注意名称是有固定格式xxxGatewayFilterFactory
package com.xiaoge.filter; import lombok.Getter; import lombok.Setter; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.Arrays; import java.util.List; /** * todo 自定义局部过滤器, 注意一定要请求我们定义了过滤器的那个路由负责, 该过滤器才会生效. * todo * gateway: * discovery: * locator: * enabled: true * 我们定义了这个配置文件, 它会让gateway可以发现nacos中的微服务, 就会有默认的规则, 就是你访问的服务名必须是对应的服务的应用名, 它就会转发到对应的服务下, 即使我们配置了规则, 默认规则还是存在 */ @Component public class TimeGatewayFilterFactory extends AbstractGatewayFilterFactory<TimeGatewayFilterFactory.Config> { private static final String BEGIN_TIME = "beginTime"; //构造函数 public TimeGatewayFilterFactory() { super(TimeGatewayFilterFactory.Config.class); } // 读取配置⽂件中的参数 赋值到 配置类中 @Override public List<String> shortcutFieldOrder() { /** todo application.yml 中配置的事 - Time=true 所以只有一个值, 所以这里Arrays.asList("show")也只是一个值 这里的值要跟application.yml中对应的值对应, 假设 - Time=true,false, 那么这里Arrays.asList("show","abc") 值随便写(可以不对应, 但是asList里面的值必须比, application.yml中的值多) 下面静态类Config里面属性的数量要跟Arrays.asList对应 */ return Arrays.asList("show"); } // todo 拦截到之后就会调用apply方法, 把创建对象时候反射创建出来的config传入进来 @Override public GatewayFilter apply(Config config) { return new GatewayFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { if (!config.show) { return chain.filter(exchange); } exchange.getAttributes().put(BEGIN_TIME, System.currentTimeMillis()); /** * pre的逻辑 * chain.filter().then(Mono.fromRunable(()->{ * post的逻辑 * })) */ return chain.filter(exchange).then(Mono.fromRunnable(() -> { Long startTime = exchange.getAttribute(BEGIN_TIME); if (startTime != null) { System.out.println(exchange.getRequest().getURI() + "请求耗时: " + (System.currentTimeMillis() - startTime) + "ms"); } })); } }; } @Setter @Getter static class Config { private boolean show; } }
-
在指定的路由中添加路由规则
server: port: 9000 spring: application: name: api-gateway cloud: nacos: discovery: server-addr: localhost:8848 gateway: discovery: locator: enabled: true # 让gateway可以发现nacos中的微服务 # 注意网关不配置路由规则, 它有默认的规则, 就是你访问的服务名必须是对应的服务的应用名, 它就会转发到对应的服务下, 即使我们配置了规则, 默认规则还是存在 routes: - id: product_route # 路由名称保持唯一 uri: lb://product-service # 符合条件的请求转发到那个服务, lb表示对服务进行负载均衡 predicates: # 拦截那些请求 - Path=/product-serv/** filters: # 在转发请求之前, 将拦截到的路径的第一层路径删除掉 - StripPrefix=1 - id: order_route uri: lb://order-service predicates: - Path=/order-serv/** filters: - StripPrefix=1 - Time=true # 自定义过滤器
-
访问商品服务的时候是没有打印⽇志的,访问订单服务的时候打印⼊职如下:
7.6.2 全局路由过滤器
全局过滤器作⽤于所有路由, ⽆需配置。通过全局过滤器可以实现对权限的统⼀校验,安全性验证等功能。
SpringCloud Gateway内部也是通过⼀系列的内置全局过滤器对整个路由转发进⾏处理如下:
需求: 实现统⼀鉴权的功能,我们需要在⽹关判断请求中是否包含token且,如果没有则不转发路由,有则执⾏正常逻辑。
-
编写全局过滤类
package com.xiaoge.filter; import org.apache.commons.lang3.StringUtils; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** * 自定义全局过滤器 */ @Component public class AuthGlobalFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getQueryParams().getFirst("token"); if (StringUtils.isBlank(token)) { System.out.println("鉴权失败"); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // 不让它继续访问, 直接截断返回对应的response return exchange.getResponse().setComplete(); } return chain.filter(exchange); } }
-
启动并测试
7.7 集成 Sentinel 实现网关限流
⽹关是所有请求的公共⼊⼝,所以可以在⽹关进⾏限流,⽽且限流的⽅式也很多,我们本次采⽤前⾯学过的Sentinel组件来实现⽹关的限流。Sentinel⽀持对SpringCloud Gateway、Zuul等主流⽹关进⾏限流。
从1.6.0版本开始,Sentinel提供了SpringCloud Gateway的适配模块,可以提供两种资源维度的限流:
-
route维度:即在Spring配置⽂件中配置的路由条⽬,资源名为对应的routeId
-
⾃定义API维度:⽤户可以利⽤Sentinel提供的API来⾃定义⼀些API分组
7.7.1 网关集成 Sentinel
https://github.com/alibaba/Sentinel/wiki/⽹关限流
-
添加依赖
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> </dependency>
-
添加配置
spring: cloud: sentinel: transport: port: 9999 dashboard: localhost:8080
-
重启⽹关服务并测试.
7.7.2 API 分组
Sentinel中⽀持按照API分组进⾏限流,就是我们可以按照特定规则进⾏限流.
在管控台⻚⾯中提供了三种⽅式的API分组管理
-
精准匹配
-
前缀匹配
-
正则匹配
现在我们定义了如下的接⼝地址
package com.xiaoge.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* TODO
*
* @author <a href="mailto:1330137071@qq.com">Zhang Xiao</a>
* @since
*/
@RestController
@RequestMapping(value = "v1")
@Slf4j
public class TestController {
@RequestMapping("/test1")
public String test1() {
return "test1";
}
@RequestMapping("/test2")
public String test2() {
return "test2";
}
@RequestMapping("/test3/test")
public String test3() {
return "test3";
}
}
精准匹配
-
在API管理中新建API分组,匹配模式选择精准匹配,匹配串写请求URL地址
-
在流控规则中,API类型中选择API分组,然后在API名称中选择我们刚刚定义的V1限流
-
此时上⾯三个请求中,只有 /product-service/v1/test1会被限流
前缀匹配
-
在API管理中新建API分组,匹配模式选择前缀匹配,匹配串写请求URL地址
此时 /product-service/v1/test1 和 /product-service/v1/test2 会被限流
注意: 如果路径为/*表示匹配⼀级路径,如果路径为/**表示多级路径
正则匹配
-
在API管理中新建API分组,匹配模式选择正则匹配,匹配串写请求URL地址
7.7.3 修改限流默认返回格式
-
在配置类GatewayConfiguration.java中添加如下配置
package com.xiaoge.config; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.annotation.PostConstruct; import java.util.HashMap; import java.util.Map; /** * todo 自定义网关集成sentinel限流返回格式 */ @Configuration public class GatewayConfiguration { @PostConstruct public void initBlockHandlers() { BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { Map map = new HashMap<>(); map.put("code", 0); map.put("message", "接⼝被限流了"); return ServerResponse.status(HttpStatus.OK). contentType(MediaType.APPLICATION_JSON). body(BodyInserters.fromValue(map)); } }; GatewayCallbackManager.setBlockHandler(blockRequestHandler); } }
-
重启并测试
链路追踪 Sleuth&Zipkin
微服务架构是⼀个分布式架构,它按业务划分服务单元,⼀个分布式系统往往有很多个服务单元。由于服务单元数量众多,业务的复杂性,如果出现了错误和异常,很难去定位。主要体现在,⼀个请求可能需要调⽤很多个服务,⽽内部服务的调⽤复杂性,决定了问题难以定位。所以微服务架构中,必须实现分布式链路追踪,去跟进⼀个请求到底有哪些服务参与,参与的顺序⼜是怎样的,从⽽达到每个请求的步骤清晰可⻅,出了问题,很快定位。
分布式链路追踪(Distributed Tracing),就是将⼀次分布式请求还原成调⽤链路,进⾏⽇志记录,性能监控并将⼀次分布式请求的调⽤情况集中展示。⽐如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
8.1 常见的链路追踪技术
-
cat :由⼤众点评开源,基于Java开发的实时应⽤监控平台,包括实时应⽤监控,业务监控 。 集成⽅案是通过代码埋点的⽅式来实现监控,⽐如: 拦截器,过滤器等。 对代码的侵⼊性很⼤,集成成本较⾼。⻛险较⼤。
-
zipkin :由Twitter公司开源,开放源代码分布式的跟踪系统,⽤于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。该产品结合spring-cloud-sleuth使⽤较为简单, 集成很⽅便, 但是功能较简单。
-
pinpoint: Pinpoint是韩国⼈开源的基于字节码注⼊的调⽤链分析,以及应⽤监控分析⼯具。特点是⽀持多种插件,UI功能强⼤,接⼊端⽆代码侵⼊。
-
skywalking:SkyWalking是本⼟开源的基于字节码注⼊的调⽤链分析,以及应⽤监控分析⼯具。特点是⽀持多种插件,UI功能较强,接⼊端⽆代码侵⼊。⽬前已加⼊Apache孵化器。
-
Sleuth: SpringCloud 提供的分布式系统中链路追踪解决⽅案。
8.2 集成链路追踪组件 Sleuth
-
在product-server和order-server中添加sleuth依赖
<!-- 日志链路追踪 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency>
-
订单微服务调⽤商品微服务,这个流程中通过@Slfj打印⽇志.重启服务并访问测试订单服务⽇志结果:
商品服务⽇志结果:
8.3 ⽇志参数解释
⽇志格式:
[order-server,c323c72e7009c077,fba72d9c65745e60,false]
1、第⼀个值,spring.application.name的值
2、第⼆个值,c323c72e7009c077 ,sleuth⽣成的⼀个ID,叫Trace ID,⽤来标识⼀条请求链路,⼀条请求链路中包含⼀个Trace ID,多个Span ID
3、第三个值,fba72d9c65745e60、spanID 基本的⼯作单元,获取元数据,如发送⼀个http
4、第四个值:true,是否要将该信息输出到zipkin服务中来收集和展示。
8.4 Zipkin+Sleuth 整合
zipkin是Twitter基于google的分布式监控系统Dapper(论⽂)的开发源实现,zipkin⽤于跟踪分布式服务之间的应⽤数据链路,分析处理延时,帮助我们改进系统的性能和定位故障。
官⽹:https://zipkin.io/
-
下载Zipkin的jar包,在官⽹可以下载.
-
通过命令⾏,输⼊下⾯的命令启动ZipKin Server
java -jar zipkin-server-2.22.1-exec.jar
-
通过浏览器访问 http://localhost:9411访问
-
在订单微服务和商品微服务中添加zipkin依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
-
在订单微服务和商品微服务中添加如下配置:
spring: zipkin: base-url: http://127.0.0.1:9411/ #zipkin server的请求地址 discoveryClientEnabled: false #让nacos把它当成⼀个URL,⽽不要当做服务名, 这样127.0.0.1:9411这个地址就不会去nacos中找对应服务 sleuth: sampler: probability: 1.0 #采样的百分⽐
-
重启订单微服务和商品微服务,访问 http://localhost:8091/save?uid=1&pid=1
-
访问zipkin的UI界⾯,观察效果
配置中心 Nacos Config
9.1 服务配置中心介绍
⾸先我们来看⼀下,微服务架构下关于配置⽂件的⼀些问题:
-
配置⽂件相对分散。在⼀个微服务架构下,配置⽂件会随着微服务的增多变的越来越多,⽽且分散在各个微服务中,不好统⼀配置和管理。
-
配置⽂件⽆法区分环境。微服务项⽬可能会有多个环境,例如:测试环境、预发布环境、⽣产环境。每⼀个环境所使⽤的配置理论上都是不同的,⼀旦需要修改,就需要我们去各个微服务下⼿动维护,这⽐较困难。
-
配置⽂件⽆法实时更新。我们修改了配置⽂件之后,必须重新启动微服务才能使配置⽣效,这对⼀个正在运⾏的项⽬来说是⾮常不友好的。
基于上⾯这些问题,我们就需要配置中心的加⼊来解决这些问题。
配置中心的思路是:
⾸先把项⽬中各种配置全部都放到⼀个集中的地⽅进⾏统⼀管理,并提供⼀套标准的接⼝。
当各个服务需要获取配置的时候,就来配置中⼼的接⼝拉取⾃⼰的配置。
当配置中⼼中的各种参数有更新的时候,也能通知到各个服务实时的过来同步最新的信息,使之动态更新。
当加⼊了服务配置中⼼之后,我们的系统架构图会变成下⾯这样:
9.2 常⻅的服务配置中心
-
Apollo
Apollo是由携程开源的分布式配置中⼼。特点有很多,⽐如:配置更新之后可以实时⽣效,⽀持灰度发布功能,并且能对所有的配置进⾏版本管理、操作审计等功能,提供开放平台API。并且资料也写的很详细。
-
Disconf
Disconf是由百度开源的分布式配置中⼼。它是基于Zookeeper来实现配置变更后实时通知和⽣效的。
-
SpringCloud Config
这是Spring Cloud中带的配置中⼼组件。它和Spring是⽆缝集成,使⽤起来⾮常⽅便,并且它的配置存储⽀持Git。不过它没有可视化的操作界⾯,配置的⽣效也不是实时的,需要重启或去刷新。
-
Nacos
这是SpingCloud alibaba技术栈中的⼀个组件,前⾯我们已经使⽤它做过服务注册中⼼。其实它也集成了服务配置的功能,我们可以直接使⽤它作为服务配置中⼼。
9.3 Nacos Config ⼊⻔
使⽤nacos作为配置中⼼,其实就是将nacos当做⼀个服务端,将各个微服务看成是客户端,我们将各个微服务的配置⽂件统⼀存放在nacos上,然后各个微服务从nacos上拉取配置即可。
接下来我们以商品微服务为例,学习nacos config的使⽤。
-
搭建nacos环境【使⽤现有的nacos环境即可】
-
在商品微服务中引⼊nacos的依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
-
在微服务中添加nacos config的配置
注意:不能使⽤原来的 application.yml 作为配置⽂件,⽽是新建⼀个 bootstrap.yml 作为配置⽂件
配置⽂件优先级(由⾼到低): bootstrap.properties -> bootstrap.yml -> application.properties -> application.yml
spring: application: name: product-service cloud: nacos: config: server-addr: 127.0.0.1:8848 #nacos中⼼地址 file-extension: yaml # 配置⽂件格式 profiles: active: dev # 环境标识 # 使用了nacos配置中心, 它会自动去根据服务名称(order-service)和指定的环境信息(dev)和指定后置(yaml), 去nacos配置中心找到对应的配置(order-service-dev.yaml)加载到内存中, 使用了nacos配置中心, 本地application.yml也还在 # 相同配置nacos中的配置负载application.yml中的, application.yml中的覆盖bootstrap.yml中的
-
在nacos中添加配置,然后把商品微服务application.yml配置复制到配置内容中.
-
注释本地的application.yam中的内容, 启动程序进⾏测试
-
如果依旧可以成功访问程序,说明我们nacos的配置中⼼功能已经实现
9.4 配置动态刷新
在⼊⻔案例中,我们实现了配置的远程存放,但是此时如果修改了配置,我们的程序是⽆法读取到的,因此,我们需要开启配置的动态刷新功能.
-
在nacos中的product-service-dev.yaml配置项中添加下⾯配置:
appConfig: name: product2020
-
在商品微服务中新增NacosConfigControlller.java
package com.xiaoge.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RefreshScope public class NacosConfigController { @Value("${appConfig.name}") private String appConfigName; @RequestMapping("/nacosConfig1") public String nacosConfig() { return "远程信息:" + appConfigName; } }
9.5 配置共享
当配置越来越多的时候,我们就发现有很多配置是重复的,这时候就考虑可不可以将公共配置⽂件提取出来,然后实现共享呢?当然是可以的。接下来我们就来探讨如何实现这⼀功能。
- 同⼀个微服务的不同环境之间共享配置
如果想在同⼀个微服务的不同环境之间实现配置共享,其实很简单。只需要提取⼀个以spring.application.name 命名的配置⽂件,然后将其所有环境的公共配置放在⾥⾯即可。
-
新建⼀个名为product-service.yaml配置存放商品微服务的公共配置,把之前的公共配置都存放进去.
-
新建⼀个名为product-service-test.yaml配置存放测试环境的配置
-
新建⼀个名为product-service-dev.yaml配置存放测试环境的配置
-
需要的配置信息具体如下:
-
在NacosConfigController.java中新增如下逻辑
@RestController @RefreshScope public class NacosConfigController { @Value("${appConfig.name}") private String appConfigName; @Value("${env}") private String env; @RequestMapping("/nacosConfig1") public String nacosConfig(){ return "远程信息:"+appConfigName; } @RequestMapping("/nacosConfig2") public String nacosConfig2(){ return "公共配置:"+appConfigName+",环境配置信息:"+env; } }
-
通过修改环境,参看是否可以读取公共配置和环境独有配置
- 不同微服务中间共享配置
不同为服务之间实现配置共享的原理类似于⽂件引⼊,就是定义⼀个公共配置,然后在当前配置中引⼊。
-
在nacos中定义⼀个DataID为global-config.yaml的配置,⽤于所有微服务共享
globalConfig: global
-
修改bootstrap.yaml
spring: application: name: order-service cloud: nacos: config: server-addr: 127.0.0.1:8848 #nacos中⼼地址 file-extension: yaml # 配置⽂件格式 shared-configs: - data-id: global-config.yaml # 配置要引⼊的配置, 引入nacos中定义的配置 (这里什么配置文件都可以引入, 通过data-id, 需要引入多个全局配置就写多个data-id) refresh: true # 动态刷新 profiles: active: dev # 环境标识 # 使用了nacos配置中心, 它会自动去根据服务名称(order-service)和指定的环境信息(dev)和指定后置(yaml), 去nacos配置中心找到对应的配置(order-service-dev.yaml)加载到内存中, 使用了nacos配置中心, 本地application.yml也还在 # 相同配置nacos中的配置负载application.yml中的, application.yml中的覆盖bootstrap.yml中的
-
在NacosConfigController.java中新增⼀个⽅法
@RestController @RefreshScope public class NacosConfigController { @Value("${appConfig.name}") private String appConfigName; @Value("${env}") private String env; @Value("${globalConfig}") private String globalConfig; @RequestMapping("/nacosConfig1") public String nacosConfig(){ return "远程信息:"+appConfigName; } @RequestMapping("/nacosConfig2") public String nacosConfig2(){ return "公共配置:"+appConfigName+",环境配置信息:"+env; } @RequestMapping("/nacosConfig3") public String nacosConfig3(){ return "全局配置:"+globalConfig+",公共配置:"+appConfigName+",环境配置信 息:"+env; } }
-
重启服务并测试.