文章目录
1 微服务
1.1 微服务概念
概念
Microservice architectures are the ‘new normal’. Building small, self-contained, ready to run applications can bring great flexibility and added resilience to your code. Spring Boot’s many purpose-built features make it easy to build and run your microservices in production at scale. And don’t forget, no microservice architecture is complete without Spring Cloud ‒ easing administration and boosting your fault-tolerance.
微服务是一种架构模式,提倡将单一应用程序划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务之间采用轻量级的通信机制互相协作。每个服务都围绕具体业务构建,并且能够独立的部署到生产环境、类生产环境等。
1.2 SpringCloud
SpringCloud:
Spring Cloud can help with service discovery, load-balancing, circuit-breaking, distributed tracing, and monitoring. It can even act as an API gateway.
分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体
(截至2020年份) Cloud升级
服务名 | 使用状况 |
---|---|
服务注册中心 | Eureka(×)、Zookeeper、Consul、Nacos |
服务调用 | Ribbon、LoadBalancer | Feign(×)、OpenFeign |
服务降级 | Hystrix(×)、resilience4j、sentinel |
服务网关 | Zuul(×)、gateway |
服务配置 | Config(×)、Nacos |
服务总线 | Bus(×)、Nacos |
1.3 工程搭建
约定 > 配置 > 编码
1.新建maven工程
2.检查字符编码是否正确
3.注解生效激活
4.检查jdk配置
5.配置依赖
<?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.cyan</groupId>
<artifactId>cloud</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<!-- 统一管理jar包版本 -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<mysql.version>5.1.47</mysql.version>
<druid.version>1.1.16</druid.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>
<!-- 子模块继承后:锁定版本+子模块无需写组名和版本号 -->
<dependencyManagement>
<dependencies>
<!-- springboot 2.2.2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- SpringCloud Hoxton.SR1-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- SpringCloud Alibaba 2.1.0 RELEASE -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<!-- druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<!-- log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
</project>
知识补充
dependencyManagement
maven使用dependencyManagement来提供一种管理依赖版本号的方式
通常在一个组织或项目的最顶层的父POM看到dependencyManagement元素
使用pom.xml中的dependencyManagement元素能让所有在子项目中引用一个依赖而不用显示列出版本号,maven会沿着父子层次向上走,直到找到一个拥有dependencyManagement元素的项目,然后它就会使用dependencyManagement元素指定的版本号
优势: 多个子项目引用同样依赖,可以避免在每个子项目中声明一个版本号,若升级或切换其他版本号,仅需要在顶层父容器更新,不需要一个一个子项目的修改;另外若某子项目需要另外版本,仅需要声明version
- dependencyManagement只是声明依赖,并不实现引入,因此项目需要显示声明需要的依赖
- 若不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取父pom
- 如果子项目中指定了版本号,那么会使用子项目中指定的jar版本
父工程创建完成执行mvn:install
将父工程发布到仓库方便子工程继承
1.4 支付模块构建
微服务模块构建
- 建module
- 改POM
- YML
- 主启动
- 业务类
cloud-provider-payment-8001
1.建cloud-provider-payment-8001
2.修改pom文件
<?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">
<parent>
<artifactId>cloud</artifactId>
<groupId>com.cyan</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-provider-payment-8001</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3.配置YML
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.gjt.mm.mysql.Driver
url: jdbc:mysql://localhost:3306/db2019?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.cyan.springcloud.entities
4.主启动搭建
@SpringBootApplication
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}
5.数据库搭建
CREATE TABLE `payment` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT'ID',
`serial` varchar(200) DEFAULT'',
PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
6.业务逻辑
实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
private Long id;
private String serial;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message) {
this(code, message, null);
}
}
数据访问层
@Mapper
public interface PaymentDao {
int create(Payment payment);
Payment getPaymentById(@Param("id") Long id);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cyan.springcloud.dao.PaymentDao">
<insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
INSERT INTO payment(serial) VALUES(#{serial});
</insert>
<resultMap id="BaseResultMap" type="com.cyan.springcloud.entities.Payment">
<id column="id" property="id" jdbcType="BIGINT"/>
<id column="serial" property="serial" jdbcType="VARCHAR"/>
</resultMap>
<select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
SELECT * FROM payment WHERE id = #{id};
</select>
</mapper>
业务逻辑层
public interface PaymentService {
int create(Payment payment);
Payment getPaymentById(Long id);
}
@Service
public class PaymentServiceImpl implements PaymentService {
@Resource
private PaymentDao paymentDao;
@Override
public int create(Payment payment) {
return paymentDao.create(payment);
}
@Override
public Payment getPaymentById(Long id) {
return paymentDao.getPaymentById(id);
}
}
控制器层
@Slf4j
@RestController
@RequestMapping(value = "/payment")
public class PaymentController {
@Resource
private PaymentService paymentService;
@PostMapping(value = "/create")
public CommonResult create(Payment payment) {
int result = paymentService.create(payment);
log.info("插入结果:{}", result);
if (result > 0) {
return new CommonResult(200, "Insert Success", result);
} else {
return new CommonResult(500, "Insert Failed", null);
}
}
@GetMapping(value = "/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id) {
Payment payment = paymentService.getPaymentById(id);
log.info("查询结果:{}", payment);
if (payment != null) {
return new CommonResult(200, "Query Success", payment);
} else {
return new CommonResult(404, "Query Failed" + id, null);
}
}
}
结论 测试成功
1.5 消费者订单模块构建
cloud-consumer-order-80
1.cloud-consumer-order-80
2.修改pom文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.YML
server:
port: 80
4.主启动
@SpringBootApplication
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
5.业务逻辑
实体类
- Payment
- CommonResult
RestTemplate
RestTemplate提供多种便捷访问远程Http服务的方法,是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集
- 使用RestTemplate访问restful接口比较简单
url
表示REST请求地址requestMap
表示请求参数ResponseBean.class
表示HTTP响应转换成的对象类型
配置类
@Configuration
public class ApplicationContextConfig {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
控制器
@Slf4j
@RestController
@RequestMapping("/consumer")
public class OrderController {
public static final String PAYMENT_URL = "http://localhost:8001";
@Resource
private RestTemplate restTemplate;
@GetMapping("/payment/create")
public CommonResult<Payment> create(Payment payment) {
return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
}
@GetMapping("/payment/get/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id") Long id) {
return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
}
}
1.6 工程重构
发现问题:系统存在重复部分,需要进行重构
新建模块:cloud-api-commons
添加依赖
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
</dependencies>
将公共部分封装进该模块
maven命令clean install
订单模块80和支付模块8001分别改造
-
删除各自原先的
entities
文件夹 -
添加依赖
<dependency> <groupId>com.cyan</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency>
结论 测试成功
2 服务注册与发现
2.1 Eureka
基础知识
服务治理
Spring Cloud封装 了Netfilx开发的Eureka模块实现服务治理
传统RPC远程调用框架中,管理服务之间依赖关系较为复杂,因此使用服务治理管理该关系,可以实现服务调用、负载军和、容错等,实现服务发现与注册
服务注册与发现
Eureka采用C/S设计架构,Eureka Server作为服务注册功能的服务器,属于服务注册中心,而系统中其他微服务,使用Eureka的客户端连接到Eureka Server并维持心跳连接,这样系统的维护人员可以通过Eureka Server来监控系统中各个微服务是否正常运行
在服务注册发现中,有一个注册中心。当服务器启动时,会把当前主机服务器的信息(如服务器地址、通讯地址等)以别名方式注册到注册中心上,另一方(消费者|服务提供者),以该别名方式去注册中心获取实际的服务通讯地址,然后实现本地RPC调用
Eureka两个组件:Eureka Server和Eureka Client
Eureka Server 提供服务注册
- 每个微服务节点启动后,会在Eureka Server中进行注册,Eureka Server的服务注册表将会存储所有可用服务节点的信息
Eureka Client 通过注册中心进行访问
- 用于简化Eureka Server的交互,客户端同时具备内置的、使用轮询负载算法的负载均衡器。应用启动后,将向Eureka Server发送心跳(默认周期30s)
- 若Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90s)
2.2 单机 Eureka 构建
Eureka Server
1.创建模块cloud-eureka-server-7001
2.引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>com.cyan</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
3.YML
server:
port: 7001
eureka:
instance:
hostname: localhost # eureka服务端实例名称
client:
register-with-eureka: false # false表示不向注册中心注册自己
fetch-registry: false # false表示自己为注册中心,维护服务实例,无需检索服务
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 设置与Eureka Server交互的地址,注册服务和查询服务
4.主启动
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7001 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7001.class, args);
}
}
访问http://localhost:7001/,测试成功
支付微服务8001注册进Eureka Server
备注:
将cloud-provider-payment-8001入驻Eureka Server成为服务提供者 provider
1.修改pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.YML
eureka:
client:
register-with-eureka: true # 将自己注册进Eureka Server
fetch-registry: true # 是否从Eureka Server抓取已有的注册信息,默认true,集群必须设置为itrue才能配合ribbon使用负载均衡
service-url:
defaultZone: http://localhost:7001/eureka
3.主启动
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}
4.测试
- 先启动Eureka Server
- http://localhost:7001
订单微服务80注册进 Eureka Server
备注:
将cloud-consumer-order-80入驻Eureka Server成为服务消费者consumer
1.修改pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.YML
server:
port: 80
spring:
application:
name: cloud-order-service
eureka:
client:
register-with-eureka: true # 将自己注册进Eureka Server
fetch-registry: true # 是否从Eureka Server抓取已有的注册信息,默认true,集群必须设置为itrue才能配合ribbon使用负载均衡
service-url:
defaultZone: http://localhost:7001/eureka
3.主启动
@SpringBootApplication
@EnableEurekaClient
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
4.测试
- 先启动Eureka Server 7001服务
- http://localhost:7001
- 再启动服务提供者provider 8001服务
2.3 集群 Eureka 构建
Eureka集群原理
微服务RPC远程服务调用的最核心是什么?
A: 高可用,若注册中心只有一个,出现单点故障,会导致整个服务环境不可使用
解决方案:搭建Eureka注册中心集群,实现负载均衡+故障容错
互相注册,相互守望
集群环境构建
1.新建cloud-eureka-server-7002
2.引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>com.cyan</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
3.修改映射配置
- 找到
C:\Windows\System32\drivers\etc\hosts
文件 - 修改映射配置,并添加进hosts文件
- 127.0.0.1 eureka7001.com
- 127.0.0.1 eureka7002.com
4.YML
7001
server: port: 7001 eureka: instance: hostname: eureka7001.com # eureka服务端实例名称 client: register-with-eureka: false # false表示不向注册中心注册自己 fetch-registry: false # false表示自己为注册中心,维护服务实例,无需检索服务 service-url: defaultZone: http://eureka7002.com:7002/eureka/
7002
server: port: 7002 eureka: instance: hostname: eureka7002.com # eureka服务端实例名称 client: register-with-eureka: false # false表示不向注册中心注册自己 fetch-registry: false # false表示自己为注册中心,维护服务实例,无需检索服务 service-url: defaultZone: http://eureka7001.com:7001/eureka/
5.主启动
@SpringBootApplication
@EnableEurekaClient
public class EurekaMain7002 {
public static void main(String[] args) {
SpringApplication.run(EurekaMain7002.class, args);
}
}
6.测试
访问http://eureka7001.com:7001/
访问http://eureka7002.com:7002/
2.4 订单支付微服务注册进Eureka集群
支付服务8001微服务注册进集群
YML
eureka:
client:
register-with-eureka: true # 将自己注册进Eureka Server
fetch-registry: true # 是否从Eureka Server抓取已有的注册信息,默认true,集群必须设置为itrue才能配合ribbon使用负载均衡
service-url:
# defaultZone: http://localhost:7001/eureka # 单机Eureka
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
订单服务80微服务注册进集群
YML
eureka:
client:
register-with-eureka: true # 将自己注册进Eureka Server
fetch-registry: true # 是否从Eureka Server抓取已有的注册信息,默认true,集群必须设置为itrue才能配合ribbon使用负载均衡
service-url:
# defaultZone: http://localhost:7001/eureka # 单机Eureka
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
2.5 支付微服务集群构建
1.新建工程cloud-provider-payment-8002
2.配置依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.cyan</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
3.YML
server:
port: 8002
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.gjt.mm.mysql.Driver
url: jdbc:mysql://localhost:3306/db2019?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: root
eureka:
client:
register-with-eureka: true # 将自己注册进Eureka Server
fetch-registry: true # 是否从Eureka Server抓取已有的注册信息,默认true,集群必须设置为itrue才能配合ribbon使用负载均衡
service-url:
# defaultZone: http://localhost:7001/eureka # 单机Eureka
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.cyan.springcloud.entities
4.主启动
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8002 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8002.class, args);
}
}
5.业务
[COPY]
6.修改8001和8002的控制器
@Value("${server.port}")
private String serverPort;
7.测试
http://localhost:7001/
http://localhost:7002/
8.负载均衡
-
订单服务访问地址不能写死
// public static final String PAYMENT_URL = "http://localhost:8001"; // 通过在Eureka上注册过的微服务名称调用 public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
-
使用
@LoadBalanced
注解赋予RestTemplate负载均衡能力@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } }
9.再次测试
a. 启动EurekaServer(7001、7002)
b. 再启动服务提供者(8001、8002)
c. http://localhost/consumer/payment/get/1
d. 负载均衡达到:8001和8002交替出现
Ribbon和Eureka整合后Consumer可以直接调用服务,无需关心地址和端口号
2.6 actuator微服务信息完善
主机名称:服务名称修改
修改cloud-provider-payment-8001的YML
eureka:
client:
register-with-eureka: true # 将自己注册进Eureka Server
fetch-registry: true # 是否从Eureka Server抓取已有的注册信息,默认true,集群必须设置为itrue才能配合ribbon使用负载均衡
service-url:
# defaultZone: http://localhost:7001/eureka # 单机Eureka
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
instance-id: payment8001
访问信息有IP提示信息
eureka:
client:
register-with-eureka: true # 将自己注册进Eureka Server
fetch-registry: true # 是否从Eureka Server抓取已有的注册信息,默认true,集群必须设置为itrue才能配合ribbon使用负载均衡
service-url:
# defaultZone: http://localhost:7001/eureka # 单机Eureka
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
instance-id: payment8001
prefer-ip-address: true # 访问路径显示IP
2.7 服务发现
对于注册进Eureka里的微服务,可以通过服务发现来获得该服务的信息
1.在8001中注入依赖
@Resource
private DiscoveryClient discoveryClient;
2.测试代码
@GetMapping("/discovery")
public Object discovery()
{
List<String> services = discoveryClient.getServices();
for (String element : services) {
log.info("*****element: "+element);
}
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
log.info(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());
}
return this.discoveryClient;
}
3.开启支持
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}
4.测试
- 启动EurekaServer
- 启动8001主启动类
- 访问http://localhost:8001/payment/discovery
{"services":["cloud-payment-service","cloud-order-service"],"order":0}
2.8 Eureka自我保护
保护模式
保护模式主要用于一组客户端和Eureka Server之间存在网格分区场景下的保护,一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就不会注销任何微服务
自我保护机制原因
为了EurekaClient可以正常运行,防止与EurekaServer网络不通情况下,EurekaServer不会立刻将EurekaClient服务剔除
自我保护模式
默认情况,如果Eureka‘Server在一定时间内没有接收到某个微服务实例的心跳,EurekaServer会注销该实例(默认90s)。但当网络分区故障发生时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险(微服务本身正常的,无需注销微服务)。Eureka通过“自我保护“模式解决–当EurekaServer节点在短时间内丢失过多客户端时(网络分区故障等),那么该节点进入自我保护模式
在自我保护模式下,EurekaServer会保护服务注册表中的信息,不再注销任何服务实例
禁止自我保护
默认情况,自我保护机制开启的,使用下述配置关闭自我保护
eureka.server.enable-self-preservation=false
3 服务注册与发现Zookeeper
3.1 注册中心 Zookeeper
介绍
zookeeper是分布式协调工具,可以实现注册中心功能
关闭Linux服务器防火墙后启动zookeeper服务器
systemctl stop firewalld
systemctl status firewalld
3.2 服务提供者
1.新建cloud-provider-payment-8003
**2.依赖配置 **[其他依赖一样]
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
3.YML
server:
port: 8003
spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
connect-string: 192.168.111:2181
4.主启动
@SpringBootApplication
@EnableDiscoveryClient // 用于使用Consul或Zookeeper作为注册中心时注册服务
public class PaymentMain8004 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8004.class, args);
}
}
5.控制器
@Slf4j
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@RequestMapping("/zk")
public String paymentzk() {
return "SpringCloud with Zookeeper: " + serverPort + "\t" + UUID.randomUUID().toString();
}
}
3.3 服务消费者
1.新建cloud-consumerzk-order-80
2.配置依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<!-- 排除自带的zookeeper -->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 添加zookeeper3.4.9版本 -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.YML
server:
port: 80
spring:
application:
name: cloud-consumer-order
cloud:
zookeeper:
connect-string: 192.168.111.144:2181
4.主启动
@SpringBootApplication
@EnableDiscoveryClient
public class OrderZKMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderZKMain80.class, args);
}
}
5.业务
@Slf4j
@RestController
@RequestMapping("/consumer")
public class OrderZKController {
public static final String INVOKE_URL = "http://cloud-provider-payment";
@Resource
private RestTemplate restTemplate;
@GetMapping("/payment/zk")
public String paymentInfo() {
String result = restTemplate.getForObject(INVOKE_URL + "/payment/zk", String.class);
return result;
}
}
4 服务注册与发现Consul
4.1 Consul简介
概念
Consul is a service networking solution that enables teams to manage secure network connectivity between services and across multi-cloud environments and runtimes. Consul offers service discovery, identity-based authorization, L7 traffic management, and service-to-service encryption.
Consul是一套开源的分布式服务发现和配置管理系统,提供了微服务系统中的服务治理、配置中心、控制总线等功能,这些功能可以单独使用,可以一起使用构建服务网格,它提供了一种完整的服务网格解决方案
优点
- 基于raft协议,比较简洁
- 支持健康检查,同时支持HTTP和DNS协议
- 支持数据中心的WAN集群
- 提供图形化界面,并且跨平台
Consul功能
- 服务发现:提供HTTP和DNS两种发现方式
- 健康监测:支持多种方式(HTTP、TCP、Docker、Shell脚本定制化)
- KV存储:Key、Value的存储方式
- 多数据中心:Consul支持多数据中心
- 可视化界面
Spring Cloud Consul
- 使用开发者模式启动
consul agent -dev
- 访问Cosnul首页 http://localhost:8500
4.2 服务提供者
1.新建支付服务cloud-provider-consul-payment-8006
2.pom
<dependencies>
<dependency>
<groupId>com.cyan</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--SpringCloud consul-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
3.YML
server:
port: 8006
spring:
application:
name: consul-provider-payment
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
4.主启动
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain8006 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8006.class, args);
}
}
5.业务
@Slf4j
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@RequestMapping("/consul")
public String paymentConsul() {
return "Spring Cloud with Consul: " + serverPort + "\t" + UUID.randomUUID().toString();
}
}
6.测试
4.3 服务消费者
1.新建订单服务cloud-consumer-consul-order-80
2.pom
<dependencies>
<!--SpringCloud consul-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.YML
server:
port: 80
spring:
application:
name: cloud-consumer-order
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
4.主启动
@SpringBootApplication
@EnableDiscoveryClient
public class OrderConsulMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderConsulMain80.class, args);
}
}
5.配置
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced // 赋予RestTemplate负载均衡能力
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
6.业务
@Slf4j
@RestController
@RequestMapping("/consumer")
public class OrderConsulController {
public static final String INVOKE_URL = "http://consul-provider-payment";
@Resource
private RestTemplate restTemplate;
@GetMapping("/payment/consul")
public String paymentInfo() {
String result = restTemplate.getForObject(INVOKE_URL + "/payment/consul", String.class);
return result;
}
}
7.测试
注册中心总结
组件名 | 语言 | CAP | 服务健康检查 | 对外接口暴露 | SpringCloud集成 |
---|---|---|---|---|---|
Eureka | Java | AP | 可配支持 | HTTP | 已集成 |
Consul | Go | CP | 支持 | HTTP/DNS | 已集成 |
Zookeeper | Java | CP | 支持 | 客户端 | 已集成 |
CAP
C:Consistency强一致性
A:Availability可用性
P:Partition tolerance分区容错性
CAP理论关注粒度是数据
经典CAP图
最多只能同时较好满足两个
CAP理论核心:一个分布式系统不可能同时很好满足一致性,可用性和分区容错性三个需求
根据CAP原理将NoSQL数据库分成满足CA原则、P原则、AP原则:
CA:单点集群,满足一致性、可用性的分布式系统,通常在可扩展性上不太强大
CP:满足一致性,分区容忍性的系统,通常性能不是特别高
AP:满足可用性,分区容忍性的系统,通常可能对一致性要求低一些
5 负载均衡服务调用Ribbon
5.1 Ribbon介绍
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端 负载均衡的工具,Ribbon主要提供客户端的软件负载均衡算法和服务调用Ribbon客户端组件提供一系列完善的配置项(如连接超时、重试)
在配置文件中列出Load Balancer
所有的机器,Ribbon自动帮你基于某种规则,连接这些机器
Ribbon功能
- 负载均衡:将用户的请求平摊分配到多个服务器上,从而达到高可用(常见的负载均衡软件有Nginx、LVS、硬件F5)
- 集中式LB:在服务的消费方和提供方之间使用独立的LB设施,由该设施负责把访问请求通过某种策略转发至服务的提供方
- 进程内LB:将LB逻辑集成到消费方,消费方从服务注册中心获知哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器
- Ribbon属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它获取到服务提供方的地址
- 负载均衡+RestTemplate调用
补充:
Ribbon本地负载均衡客户端VS. Nginx服务端负载均衡
- Nginx是服务器负载均衡,客户端所有请求都会交给Nginx,然后有Nginx实现转发请求。负载均衡是由服务端实现的
- Ribbon本地负载均衡,在调用微服务接口时,会在注册中心获取注册信息服务列表后缓存到JVM本地,从而在本地实现RPC远程服务调用技术
5.2 Ribbon的负载均衡和Rest调用
Ribbon其实就是软负载均衡的客户端组件,可以和其他所需请求的客户端结合使用
-
先选择EurekaServer,它优先选择在同一个区域内负载较少的server
-
再根据用户指定的策略,从server取到的服务注册列表中选择一个地址
其中Ribbon提供多种策略(轮询、随机和根据响应时间加权等)
RestTemplate
getForObject方法/getForEntity方法
getForObject 返回对象为响应体中数据转换成的对象,基本可以理解为Json
getForEntity 返回对象为ResponseEntity对象,包含响应中的一些重要信息(如响应头、响应状态码、响应体)
5.3 Ribbon核心组件IRule
Ribbon默认自带的负载规则
IRule:根据特定算法从服务列表中选取一个要访问的服务
所属包名 | 说明 |
---|---|
com.netflix.loadbalancer.RoundRobinRule | 轮询 |
com.netflix.loadbalancer.RandomRule | 随机 |
com.netflix.loadbalancer.RetryRule | 先按照RoundRobinRule的策略获取服务。如果获取服务失败,则在指定时间内会进行重试,获取可用的服务 |
WeightedResponseTimeRule | 对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择 |
BestAvailableRule | 先过滤由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务 |
AvailabilityFilteringRule | 先过滤掉故障实例,再选择并发较小的实例 |
ZoneAvoidanceRule | 默认规则,复合判断server所在区域的性能和server的可用性选择服务器 |
Ribbon负载规则替换
1.修改cloud-consumer-order-80
2.配置细节
官网文档明确给出警告:该自定义配置类不能放在@ComponentScan
所扫描的当前包以及子包下i,否则自定义的配置类会被所有的Ribbon客户端共享,从而达不到特殊化定制目的
3.新建包com.cyan.myrule
4.新建类添加规则
@Configuration
public class MyselfRule {
@Bean
public IRule myRule() {
return new RandomRule();
}
}
5.主启动添加RibbonClient
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MyselfRule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
5.4 Ribbon均衡算法
Ribbon默认负载轮询算法原理
负载均衡算法:
rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标
每次服务重启动后rest接口计数从1开始
手写本地负载均衡器
1.7001/7002集群启动
2.8001/8002微服务改造
@GetMapping("/lb")
public String getPaymentLB() {
return serverPort;
}
3.80订单微服务改造
ApplicationContextConfig去掉
LoadBalanced
@Configuration
public class ApplicationContextConfig {
@Bean
// @LoadBalanced // 赋予RestTemplate负载均衡能力
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
LoadBalancer接口
public interface LoadBalancer {
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
MyLB
@Component
public class MyLB implements LoadBalancer{
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement() {
int current;
int next;
do {
current = this.atomicInteger.get();
next = current >= Integer.MAX_VALUE ? 0 : current + 1;
} while (!this.atomicInteger.compareAndSet(current, next));
System.out.println("(第几次访问)next = " + next);
return next;
}
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
OrderController
@GetMapping("payment/lb")
public String getPaymentLB() {
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (instances == null || instances.size() <= 0) {
return null;
}
ServiceInstance serviceInstance = loadBalancer.instances(instances);
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri + "/payment/lb", String.class);
}
测试
http://localhost/consumer/payment/lb
6 服务接口调用OpenFeign
6.1 OpenFeign介绍
Feign是一个声明式WebService客户端,使用Feign能让编写的WebService客户端更加简单。它的使用方法是定义一个服务接口然后添加注解。Feign也支持可拔插式的编码器和解码器。SpringCloud对Feign进行封装,使其支持SpringMVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用可以支持负载均衡
Feign作用
Feign旨在使Java Http客户端编写更容易。前面使用Riibbon+RestTempate时,利用RestTemplate对http请求的封装处理,形成模板化的调用方法。但实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,通常都会针对每个微服务自行封装一些客户端来包装这些依赖服务的调用。
所以Feign再此基础上进一步封装,由它帮我们定义和实现依赖服务接口的定义。在Feign的实现下,仅需要创建一个接口并使用注解的方式来配置它(以前是Dao接口标注Mapper注解,现在一个微服务接口上面标注Feign即可),即可完成对服务提供方的接口绑定,简化使用SpringCloud Ribbon时,自动封装服务调用客户端的开发量
Feign集成了Ribbon
利用Ribbon维护Payment的服务列表信息,并且通过轮询实现客户端的负载均衡,而与Ribbon不同的是,通过Feiign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现服务的调用
Feign与OpenFeign的区别
Feign | OpenFeign |
---|---|
Feign是SpringCloud组件中的一个轻量级RESTful的HTTP服务客户端Feign内置Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式:使用Feign的注解定义接口,调用该接口,就可以调用服务注册中心的服务 | OpenFeign是SpringCloud在Feign的基础上支持SpringMVC注解。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务 |
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId></dependency> | <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-open-feign</artifactId></dependency> |
6.2 OpenFeign服务调用
接口+注解:
微服务调用接口+@FeignClient
1.新建cloud-consumer-feign-order-80
Feign在消费端使用
2.pom
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.cyan</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.YML
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
4.主启动类
@SpringBootApplication
@EnableFeignClients
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
6.业务
业务逻辑接口+@FeignClient配置调用provider服务
新建PaymentFeignService接口并新增注解@FeignClient
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
@GetMapping("/payment/get/{id}")
CommonResult getPaymentById(@PathVariable("id") Long id);
}
控制层Controller
@Slf4j
@RestController
@RequestMapping("/consumer")
public class OrderFeignController {
@Resource
private PaymentFeignService paymentFeignService;
@GetMapping("/payment/get/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
return paymentFeignService.getPaymentById(id);
}
}
7.测试
-
先启动eureka集群7001 7002
-
再启动两个微服务8001 8002
-
启动OpenFeign
-
访问http://localhost/consumer/payment/get/1
Feign自带负载均衡配置项
6.3 OpenFeign超时控制
默认Feign客户端只等待一秒钟,但是服务端处理需要超过一秒钟,导致Feign客户端不想再等待,直接返回报错,为了避免该情况,我们需要设置Feign客户端的超时控制
# 设置Feign客户端超时时间
ribbon:
# 建立连接后从服务器读取可用资源所用的时间
ReadTimeOut: 5000
# 建立连接所用时间
ConnectTimeOut: 5000
6.4 OpenFeign日志增强
日志打印功能
Feign提供日志打印功能,可以通过配置来调整日志级别,从而了解Feign中Http请求的细节 (对Feign接口的调用情况进行监控和输出)
日志级别
日志级别 | 说明 |
---|---|
NONE | 默认,不显示任何日志 |
BASIC | 仅记录请求方法、URL、响应状态码以及执行时间 |
HEADERS | 除了BASIC中定义的信息之外,还有请求和响应的头信息 |
FULL | 除了HEADERS中定义的信息之外,还有请求和响应的正文以及元数据 |
配置
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
开启日志的Feign客户端
logging:
level:
# feign日志级别 以及监控监控
com.cyan.springcloud.service.Payment: debug
7 服务降级Hystrix
7.1 Hystrix概述
分布式系统面临的问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败
服务雪崩
多个微服务之间调用地时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他地微服务,称为扇出
如果扇出地链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,即**“雪崩效应**”
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒内出现饱和。比失败更糟糕的是,这些应用程序可能导致服务之间的延迟增加,备份队列、线程和其他系统资源紧张,导致整个系统发生更多的级联故障需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统
当发现一个模块下的某个实例失败后,该模块仍然接受流量,然后有问题的模块调用其他的模块,就会发生级联故障(雪崩)
Hystrix
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统中,许多依赖不可避免地调用失败(超时、异常)。Hystrix能保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性
"断路器"本身是一种开关装置,当某个服务单元发生故障后,通过断路器的故障监控(类似熔断保险丝),向调用方法返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或抛出调用方无法处理的异常,这样保证服务调用方的线程不会被长时间、不必要的占用,从而避免故障在分布式系统中的蔓延,乃至雪崩
Hystrix作用
- 服务降级
- 服务熔断
- 接近实时的监控
使用教程7.2
7.2 服务降级/熔断/限流
服务降级
服务降级一般是指在服务器压力剧增的时候,根据实际业务使用情况以及流量,对一些服务和页面有策略的不处理或者用一种简单的方式进行处理,从而释放服务器资源的资源以保证核心业务的正常高效运行。
通常原因为服务器的资源是有限的,而请求是无限的。在用户使用即并发高峰期,会影响整体服务的性能,严重的话会导致宕机,以至于某些重要服务不可用。故高峰期为了保证核心功能服务的可用性,就需要对某些服务降级处理。可以理解为舍小保大,通常处理为不让客户端等待而是立即返回一个友好的提示。
服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(兜底处理)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。
服务降级场景
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满也会导致服务降级
服务熔断
熔断机制是应对雪崩效应的一种微服务链路保护机制。 高压电路中,如果某个地方的电压过高,熔断器就会熔断,对电路进行保护。股票交易中,如果股票指数过高,也会采用熔断机制,暂停股票的交易。同样,在微服务架构中,熔断机制也是起着类似的作用。当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
服务限流
有些场景并不能用熔断和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页),因此需有一种手段来限制这些场景的并发/请求量,即限流。
限流的目的是通过对并发请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)
7.3 Hystrix支付服务构建
1.新建cloud-provider-hystrix-payment-8001
2.pom
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.cyan</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.YML
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
#defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
defaultZone: http://eureka7001.com:7001/eureka
4.主启动
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
5.业务
业务
public class PaymentServiceImpl implements PaymentService {
@Override
public String paymentInfo_Success(Integer id) {
return "ThreadPool: " + Thread.currentThread().getName() + " paymentInfo_Success, id: " + id + "Success!";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
int time = 3;
try {
TimeUnit.SECONDS.sleep(time);
} catch (Exception e) {
e.printStackTrace();
}
return "ThreadPool: " + Thread.currentThread().getName() + " paymentInfo_TimeOut, id: " + id + "Success!";
}
}
控制器
@Slf4j
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/hystrix/success/{id}")
public String paymentInfo_Success(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfo_Success(id);
log.info("result = " + result);
return result;
}
@GetMapping("/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id)
{
String result = paymentService.paymentInfo_TimeOut(id);
log.info("*result = " + result);
return result;
}
}
6.测试
- 启动eurak7001
- 启动cloud-provider-hystrix-payment-8001
- 访问
- success:http://localhost:8001/payment/hystrix/success/1
- timeout:http://localhost:8001/payment/hystrix/timeout/1
7.4 高并发测试
Tomcat默认的工作线程被打满,没有多余线程来分解压力和处理,因此造成卡死
7.5 Hystrix订单服务构建
1.新建cloud-consumer-feign-hystrix-order-80
2.pom
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.cyan</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3.yml
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
4.主启动
@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class, args);
}
}
5.业务
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/success/{id}")
String paymentInfo_Success(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
@Slf4j
@RestController
@RequestMapping("/consumer")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/payment/hystrix/success/{id}")
public String paymentInfo_Success(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_Success(id);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
}
6.测试
-
正常访问测试
- http://localhost:8001/payment/hystrix/timeout/1
- http://localhost:8001/payment/hystrix/success/1
-
高并发测试
- 用两万线程去访问8001
- 消费端80服务再去访问正常的Success微服务8001地址
结论 出现故障
原因
- Tomcat线程池的工作现场被挤占完毕,8001同一层次的其他接口服务被困死
- 80此时调用8001,客户端访问响应缓慢
7.6 降级容错解决的维护要求
超时导致服务器变慢(转圈)
超时不再等待
出错(宕机或程序运行出错)
出错有兜底
解决方案
- 对方服务(8001)超时,调用者(80)不能一直卡死等待,必须有服务降级
- 对方服务(8001)宕机,调用者(80)不能一直卡死等待,必须有服务降级
- 对方服务(8001)Succuss,调用者(80)自己出故障或者有自我要求(自己的等待事件小于服务提供者),自己处理降级
7.7 服务降级
支付降级
降级配置
@HystrixCommand
服务提供者8001从自身找出问题
设置自身调用超时时间的峰值,峰值内可以正常运行,超过需要有兜底的方法处理,做服务降级fallback
服务提供者8001fallback
业务类启用
@Service
public class PaymentServiceImpl implements PaymentService {
@Override
public String paymentInfo_Success(Integer id) {
return "ThreadPool: " + Thread.currentThread().getName() + " paymentInfo_Success, id: " + id + "Success!";
}
@Override
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000") // 3s内正常业务逻辑
})
public String paymentInfo_TimeOut(Integer id) {
// time超过峰值
int time = 5;
try {
TimeUnit.SECONDS.sleep(time);
} catch (Exception e) {
e.printStackTrace();
}
return "ThreadPool: " + Thread.currentThread().getName() + " paymentInfo_TimeOut, id: " + id + "TimeOut!";
}
public String paymentInfo_TimeOutHandler(Integer id) {
return "ThreadPool: " + Thread.currentThread().getName() + " 系统繁忙,请稍后再试, id: " + id + "TimeOutHandler!";
}
}
HystrixCommand
报异常后如何处理
- 一旦调用服务方法失败并抛出错误信息后,会自动调用
@HystrixCommand
标注好的fallbackMethod调用类中的指定方法
主启动激活
添加新注解@EnableCircuitBreaker
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
订单降级
1.YML
feign:
hystrix:
enabled: true
2.主启动
@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class, args);
}
}
3.业务
@Slf4j
@RestController
@RequestMapping("/consumer")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/payment/hystrix/success/{id}")
public String paymentInfo_Success(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_Success(id);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "消费者80:对方支付系统繁忙,请稍后再试(或者运行出错)";
}
}
存在问题
上述解决方案:每个业务方法对应一个兜底方法,代码膨胀;
解决方案(全局服务降级)
目的:解决代码膨胀
@DefaultProperties(defaultFallback="")
- 普通业务通过该注解统一跳转到处理结果页面
- 在OrderHystrixController中编码方法
public String paymentGlobalFallbackMethod() {
return "Global Exception, please try again~";
}
- 添加注解
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
@Slf4j
@RestController
@RequestMapping("/consumer")
@DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/payment/hystrix/success/{id}")
public String paymentInfo_Success(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_Success(id);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
// @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
// @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
// })
@HystrixCommand
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "消费者80:对方支付系统繁忙,请稍后再试(或者运行出错)";
}
public String paymentGlobalFallbackMethod() {
return "Global Exception, please try again~";
}
}
解决方案(通配服务降级)
目的:解决和业务逻辑混在一起,耦合度较高
服务降级,客户端去调用服务端,碰上服务端宕机或关闭
本次案例服务降级处理在客户端80实现,与服务端8001无关,只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦
常见异常
- 运行
- 超时
- 宕机
根据cloud-consumer-feign-hystrix-order-80已有的PaymentHystrixService接口重新新建一个类PaymentFallbackService
实现该接口,统一为接口的方法进行异常处理
public class PaymentFallbackService implements PaymentHystrixService{
@Override
public String paymentInfo_Success(Integer id) {
return "PaymentFallbackService: paymentInfo_Success fallback";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
return "PaymentFallbackService: paymentInfo_TimeOut fallback";
}
}
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/success/{id}")
String paymentInfo_Success(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
测试
-
启动7001和PaymentHystrixMain8001
-
访问http://localhost/consumer/payment/hystrix/success/111
-
故意关闭8001
-
客户端自己调用提示
此时服务端宕机,但我们做出服务降级处理,客户端在服务端不可用时会获得提示信息而不是挂起耗死服务器
7.8 服务熔断
服务熔断理论
熔断机制
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某微服务出错不可用或响应事件太长时,会进行服务降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当监测到该节点微服务调用响应正常后,恢复调用链路
在SpringCloud框架中,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5s内20次调用失败,就会启用熔断机制。熔断机制的注解是@HystrixCommand
服务熔断案例
修改cloud-provider-hystrix-payment-8001
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"),// 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),// 请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),// 失败率达到多少后跳闸
})
@Override
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
if(id < 0) {
throw new RuntimeException("******id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " +id;
}
@GetMapping("/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
String result = paymentService.paymentCircuitBreaker(id);
log.info("result = " + result);
return result;
}
服务熔断总结
熔断类型
- 熔断打开
- 请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开事件达到所设置时钟则进入半熔断状态
- 熔断关闭
- 熔断关闭不会对服务进行熔断
- 熔断半开
- 部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常呢个,关闭熔断
断路器何时生效
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled",value = "true"),// 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),// 请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),// 失败率达到多少后跳闸
})
快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10s
请求总数阈值:在快照时间窗内必须满足请求总数阈值才有资格熔断,默认20s,意味在10s,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或者其他原因失败,断路器都不会打开
错误百分比阈值:当请求总数在快照时间窗内超过阈值,断路器会打开
断路器开启或关闭条件
-
1.当满足一定阈值时(默认10s内超过20个请求次数)
-
2.当失败率达到一定时(默认10s内超过50%的请求失败)
-
3.到达以上阈值,断路器将会开启
-
4.当开启时,所有请求都不会进行转发
-
5.一段时间后(默认是5s),此时断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,如果失败,继续开启。重复4和5
断路器打开之后
再有请求调用时,将不会调用主逻辑,而是直接调用降级fallback。通过断路器,实现自动发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果
原来的主逻辑如何恢复
当断路器打开,对主逻辑进行熔断后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的主要逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此请求正常返回,那么断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续打开状态,休眠时间窗重新计时
7.9 图形化监控搭建
除了隔离依赖服务外,Hystrix提供准实时调用监控Hystrix Dashboard,SpringCloud提供了相关整合
1.新建cloud-consumer-hystrix-dashboard-9001
2.pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
3.yml
server:
port: 9001
4.主启动
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class, args);
}
}
所有Provider微服务提供类都需要配置监控依赖配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
监控8001.因此修改8001启动类
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001
{
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
/**
*此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
*ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
*只要在自己的项目里配置上下面的servlet就可以了
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
8 服务网关 Gateway
8.1 概述
Gateway是什么
SpringCloud Gateway是SpringCloud全新项目,基于Spring5.0+SpringBoot2.0和Project Reactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的API的路由管理方式。
SpringCloud Gateway作为SpringCloud生态系统中的网关,目标是代替Zuul,为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层使用高性能的Reactor模式通信框架Netty
SpringCloud Gateway的模板提供统一的路由方式且基于Filter链的方式提供网关基本的功能,如:安全、监控/指标、限流
Gateway能干嘛
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
- …
架构
8.2 异步非阻塞模型
SpringCloud Gateway特性
-
基于Spring Framework5, Project Reactor和SpringBoot2.0进行构建
-
动态路由:能够匹配任何请求属性
-
可以对路由指定Predicate(断言)和Filter(过滤器)
-
集成Hystrix的断路器功能
-
集成SpringCloud服务发现功能
-
易于编写的Predicate(断言)和Filter(过滤器)
-
请求限流功能
-
支持路径重写
SpringCloud Gateway与Zuul的区别
在SpringCloud Finchley正式版之前,SpringCloud推荐的网关是Netflix提供的Zuul
- Zuul 1.x是一个基于阻塞I/O的API Gateway
- Zuul 1.x基于Servlet2.5使用阻塞架构,它不支持任何长连接(如WebSocket)。Zuul类似Nginx设计,每次I/O操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是Nginx以C++实现,Zuul以Java实现,JVM本身有第一次加载较慢的情况,使得Zuul的性能相对较差
- Zuul 2.x理念想基于Neety非阻塞和支持长连接,但SpringCloud目前还未整合
- SpringCloud Gateway建立在SpringFramework5、Project Reactor和SpringBoot2上,使用非阻塞API
- SpringCloud Gateway支持WebSocket,并且与Spring紧密集成
Zuul 1.x 模型
SpringCloud中所集成的Zuul版本,采用的是Tomcat容器,使用传统的Servlet IO处理模型
Servlet是一个简单的网络IO模型,当请求进入servlet container时,servlet container为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是高并发场景下,线程数量会上涨,线程资源有限,影响处理时间。在一些简单业务场景。不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,该场景下servlet模型没有优势。
Zuul 1.x 基于servlet上的一个阻塞处理模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet)并由该servlet阻塞式处理,具有弊端。
8.3 工作流程
路由 Route
路由是构建网关的基本模块,由ID、目标URI,一系列的断言和过滤器组成,如果断言为true,则匹配该路由
断言Predicate
开发人员可以匹配HTTP请求中所有内容,如果请求与断言相匹配则进行路由
过滤Filter
Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改
客户端向SpringCloud Gateway发出请求,然后在Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler。Handler再通过指定的过滤器链将请求发送到实际服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或之后(post)执行业务逻辑
Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量控制、日志输出、协议转换等,在"post"类型的过滤器可以做响应内容、响应头的修改、日志的输出、流量监控等
8.4 快速入门
1.新建cloud-gateway-gateway9527
2.pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
3.yml(新增网关配置)
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
#- After=2020-02-21T15:51:37.485+08:00[Asia/Shanghai]
#- Cookie=username,zzyy
#- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
4.主启动
@SpringBootApplication
@EnableEurekaClient
public class GatewayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GatewayMain9527.class, args);
}
}
5.测试
- 启动7001
- 启动8001
- 启动9527
- 访问http://localhost:9527/payment/get/1
8.5 配置动态路由
默认情况Gateway会根据注册中心注册的服务列表,以
注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能
启动
eureka7001、8001、8002
pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
yml
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route # 路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/p
ayment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
注意:
uri的协议为lb,表示启用Gateway的负载均衡功能
lb://serviceName是SpringCloud Gateway在微服务中自动为我们创建的负载均衡uri
测试
访问:http://localhost:9527/payment/lb 8001/8002两个端口切换
8.6 常用Predicate
8.7 Filter
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。SpringCloud中内置了多种路由过滤器,都由GatewayFilter的工厂类产生
生命周期
pre和post
种类
GatewayFilter和GlobalFilter
常用的GatewayFilter
自定义过滤器
自定义全局过滤器
1.实现接口GlobalFilter,Ordered
2.案例代码
@Component
public class MyLogGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("*******************来到全局日志过滤器 MyLogGatewayFilter " + new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if (uname == null) {
log.info("!!!!!用户名为空");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
9 服务配置 Config
9.1 分布式配置中心介绍
分布式系统面临的问题–配置问题
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,因此一套集中式的、动态的配置管理设施必不可少。SpringCloud提供了ConfigServer解决该问题
配置中心
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供一个中心化的外部配置
SpringCloud Config
SpringCloud Config分为服务端和客户端两部分。
服务端称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口
客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动时从配置中心获取和加载配置信息,配置服务器默认采用git存储配置信息,这样有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便管理和访问配置内容
SpringCloud Config能干什么
- 集中管理配置文件
- 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
- 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
- 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
- 将配置信息以REST接口的形式暴露
9.2 配置总控中心搭建
1.在Gitee上搭建"springcloud-config"仓库
2.获取git地址
git@gitee.com:xxxx.git
3.在本地硬盘目录上新建git仓库并clone
git clone git@gitee.com:xxx.git:
4.新建cloud-config-center-3344(Cloud的配置中心模块)
5.pom
6.yml
7.主启动
@EnableConfigServer
8.Windows下修改host文件,增加映射
127.0.0.1 config-3344.com
9.测试通过Config微服务是否可以从Github上获取配置内容
启动微服务3344,访问http://config-3344.com:3344/master/config-dev.yml
配置读取规则
/{label}/{application}-{profile}.yml
- master
- http://config-3344.com:3344/master/config-dev.yml
- http://config-3344.com:3344/master/config-test.yml
- http://config-3344.com:3344/master/config-prod.yml
- dev
- http://config-3344.com:3344/dev/config-dev.yml
- http://config-3344.com:3344/dev/config-test.yml
- http://config-3344.com:3344/dev/config-prod.yml
/{application}-{profile}.yml
/{application}/{profile}[/{label}]
label:分支(branch)
name :服务名
profiles:环境(dev/test/prod)
9.3 客户端配置与测试
1.新建cloud-config-client-3355
2.pom
3 .bootstrap.yml
application.yml 属于用户级别的资源配置
bootstrap.yml 属于吸引级别的资源配置,优先级更高
SpringCloud会创建一个Bootstrap Context,作为Spring应用的Application Context
的父上下文。初始化时,Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment
Bootstrap
属性有高优先级,默认情况下,它们不会被本地配置覆盖。Bootstrap context
和Application Context
有不同约定,因此需要bootstrap.yml 保证两者配置分离
server:
port: 3355
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址k
4.主启动
@SpringBootApplication
@EnableEurekaClient
public class ConfigClientMain3355 {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3355.class, args);
}
}
5.业务
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo()
{
return configInfo;
}
}
6.测试
-
启动配置中心3344微服务自测
- http://config-3344.com:3344/master/config-prod.yml
- http://config-3344.com:3344/master/config-dev.yml
-
启动3355作为Client准备访问
http://localhost:3355/configInfo
9.4 分布式配置的动态刷新
场景模拟
Linux运维修改Github配置文件内容
刷新3344,发现ConfigServer配置中心立刻响应
刷新3355,发现ConfigClient客户端没有任何响应
3355没有变化除非自己重启或者重新加载
手动版动态刷新
- 修改3355,引入
actuator
监控 - 修改YML,暴露监控端口
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
@RefreshScope
业务类Controller修改- 运维人员发送POST请求
curl -X POST "http://localhost:3355/actuator/refresh"
10 消息总线 Bus
10.1 概述
需求
- 分布式自动刷新配置功能
- SpringCloud Bus配合SpringCloud Config使用可以实现配置的动态刷新
总线是什么
SpringCloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。SpringCloud Bus目前支持RabbitMQ和Kafka
SpringCloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送,也可以当作微服务间的通信通道
总线 在微服务架构系统中,通常会使用轻量级的消息代理来构建共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便的广播一些需要让其他连接在该主题上的实例都知道的消息
基本原理
ConfigClient实例都监听MQ中间一个topic(默认SpringCloud Bus)。当一个服务刷新数据时,它会把信息放入到Topic中,这样其他监听同一Topic的服务就得到通知,然后更新自身的配置
10.2 动态刷新全局广播
cloud-config-client-3366
0.准备RabibitMQ环境并运行
1.新建cloud-config-client-3366
2.pom
3.yml
4.主启动
5.业务
设计思想
- 利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端的配置【不合适】
- 利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置【合适】
原因
- 方式一打破微服务职责的单一性,微服务本身是业务模块,本不应该承担配置刷新的职责
- 方式一打破微服务节点的对等性
- 有一定局限性。如:微服务在迁移时,它的网络地址常常发生变化,此时要想做到自动刷新,就会增加更多的修改
给cloud-config-center-3344配置中心服务端添加消息总线支持
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
##rabbitmq相关配置,暴露bus刷新配置的端点
management:
endpoints: #暴露bus刷新配置的端点
web:
exposure:
include: 'bus-refresh'
给cloud-config-center-3355/66配置中心客户端添加消息总线支持
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
#rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
测试
运维
修改Github上配置文件,增加版本号
发送POST请求curl -X POST "http://localhost:3344/actuator/bus-refresh"
,一次发送处处生效
配置中心
http://config-3344.com:3344/config-dev.yml
客户端
http://localhost:3355/configInfo
http://localhost:3366/configInfo
获取配置信息,发现都已经刷新
总结:一次修改,广播通知,处处生效
10.3 动态刷新定点通知
需求:不想全部通知,只想定点通知(只通知3355,不通知3366)
指定具体某一个实例生效而不是全部
http://localhost:[配置中心端口号]/actuator/bus-refresh/{destination}
/bus/refresh请求不再发送到具体的服务实例上,而是发给config server并通过destination参数指定需要更新配置的服务或实例
curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355"
10.4 总结
11 消息驱动 Stream
11.1 概述
消息驱动
屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
SpringCloud Stream
SpringCloud Stream是一个构建消息驱动微服务的框架。应用程序通过inputs或者outputs来与SpringCloudStream中binder对象交互。通过配置来binding,而SpringCloudStream的binder对象负责与消息中间件交互。
通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。SpringCloudStream为一些供应商的消息中间件产品提供个性化的自动配置实现,引用了发布-订阅、消费组、分区三个核心概念。目前仅RabbitMQ和Kafka
11.2 设计思想
标准MQ
- 生产者/消费者之间靠消息媒介传递信息内容(Message)
- 消息必须走特定的通道(MessageChannel)
- 消息通道里的消息谁负责发谁负责处理(消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅)
为什么使用CloudStream
这些中间件的差异性导致实际项目开发造成困扰,如果使用两个消息队列的其中一种,后面的业务需求,想往另外一种消息队列进行迁移,此时一大堆东西要重写做,因为它跟系统耦合,而Cloud Stream给我们提供了一种解耦的方式
stream如何统一底层差异
**通过定义绑定器作为中间层,实现应用程序与消息中间件细节之间的隔离。**通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现
11.3 Cloud Stream标准流程套路
Binder:很方便的连接中间件,屏蔽差异
Channel:通道,在消息通讯系统中实现存储和转发的媒介,通过Channel对队列进行配置
Souce和Sink:从Stream发布消息就是输出,接受消息就是输入
11.4 编码API和常用注解
组成 | 说明 |
---|---|
Middleware | 中间件,目前仅支持RabbitMQ和Kafka |
Binder | Binder是应用与消息中间件之间的封装,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(对应Kafka的topic、RabbitMQ的exchange) |
@Input | 注解标识输入通道,通过该输入通道接受到的消息进入应用程序 |
@Output | 注解标识输出通道,发布的消息将通过该通道离开应用程序 |
@StreamListener | 监听队列,用于消费者的队列的消息接收 |
@EanableBinding | 指信道channel和exchange绑定在一起 |
11.5 消息驱动–生产者
1.新建cloud-stream-rabbitmq-provider-8801
2.pom
3.yml
4.主启动
5.业务类
发送消息接口
public interface IMessageProvider {
String send();
}
发送消息接口实现
@EnableBinding(Source.class) // 消息推送管道
public class MessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output; // 消息发送管道
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("************serial = " + serial);
return null;
}
}
控制层
@RestController
public class SendMessageController {
@Resource
private IMessageProvider messageProvider;
@GetMapping("/sendMessage")
public String sendMessage() {
return messageProvider.send();
}
}
6.测试
- 启动7001
- 启动rabbitmq(http://localhost:15672/)
- 启动8801(http://localhost:8801/sendMessage)
11.6 消息驱动–消费者
1.新建cloud-stream-rabbitmq-consumer-8802
2.pom
3.yml
4.主启动
5.业务
@Component
@EnableBinding(Sink.class)
public class ReceiveMessage {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("消费者1号,接收到的消息 " + message.getPayload() + "\tport\t" + serverPort);
}
}
6.测试
测试8801发送,8802接收消息
11.7 分组消费与持久化
拷贝一份8802作为新的消费者,8801发送消息,8802和8803接收,出现重复消费和消息持久化问题
重复消费问题
原因:默认分组group是不同的,不同分组可以重复消费
解决方案:自定义配置分组,分同一个组,解决重复消费问题
使用分组和持久化属性group解决
在上述场景下,订单系统做集群部署,都会从RabbitMQ中获取订单信息,如果一个订单同时被两个服务获取,那么就会造成数据错误,为了避免该情况,可以使用Stream中的消息分组解决。在Stream中处于同一个组中的多个消费者是竞争关系,能保证消息只会被其中一个应用消费一次,不同组可以全面消费(重复消息),同一个组内会发生竞争关系,只有一个可以消费
解决
原理 : 微服务应用宝放置于同一个group中,就能保证消息只会被其中一个应用消费一次。不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费
多数情况,生产者发生消息给某具体微服务时只希望被消费一次,上述启动的两个应用实例,虽然同属一个应用,但该消息出现被重复消费两次的情况。为了解决该问题,在SpringCloud中提出消费组的概念
8802/8803实现轮询分组,每次只有一个消费者。8801模块的发的消息只能被两者之一接收到,这样避免重复消费
因此将8802和8803归为相同组group: groupA
消息持久化
12 分布式请求链路跟踪Sleuth
12.1 概述
在微服务框架中,一个由客户端发起的请求在后端系统中会经过等多个不同的服务节点调用来协同产生最后的请求结果,每一个前端请求都会形成一个复杂的分布式服务调用链路,链路中任何一环出现高延时或错误都会导致整个请求最后失败
SpringCloud Sleuth提供了一套完整的服务跟踪解决方案
在分布式系统中提供追踪解决方案并且兼容支持zipkin
12.2 zipkin搭建
下载zipkin
运行jar
java -jar zipkin-server-2.12.9.jar
运行控制台
http://localhost:9411/zipkin/
完整的调用链路
表示一请求链路,一条链路通过Trace id唯一标识,Span标识发起的请求信息,各span通过parent id关联起来
精简版
整个链路依赖关系
Trace:类似树结构的Span集合,表示一条调用链路,存在唯一标识
Span:调用链路来源(一次请求信息 )
搭建步骤
1.修改cloud-provider-payment-8001
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
spring:
application:
name: cloud-payment-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1 # 1 表示全部采集
@GetMapping("/zipkin")
public String paymentZipkin() {
return "Hello, I am paymentZipkin server fallback";
}
2.修改cloud-consumer-order-80
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1 # 1 表示全部采集
@GetMapping("/payment/zipkin")
public String paymentZipkin() {
String result = restTemplate.getForObject("http://localhost:8001" + "/payment/zipkin/", String.class);
return result;
}
3.测试
-
启动7001/8001/80,80调用8001
-
访问http://localhost:9411