Spring Cloud开发实践: 基于Consul和Spring Cloud 2021.0的演示项目
Posted Milton
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud开发实践: 基于Consul和Spring Cloud 2021.0的演示项目相关的知识,希望对你有一定的参考价值。
目录
- Spring Cloud开发实践(一): 简介和根模块
- Spring Cloud开发实践(二): Eureka服务和接口定义
- Spring Cloud开发实践(三): 接口实现和下游调用
- Spring Cloud开发实践(四): Docker部署
- Spring Cloud开发实践(五): Consul - 服务注册的另一个选择
- Spring Cloud开发实践(六): 基于Consul和Spring Cloud 2021.0的演示项目
Consul 服务
启动Consul服务, 在Win10下可以执行以下命令, 或者存成bat文件运行, 保持窗口打开
consul agent -dev -client=0.0.0.0 -data-dir .\\ -advertise 127.0.0.1 -ui -config-dir .\\
浏览器访问 http://127.0.0.1:8500 , 用于观察后面注册的Node和Health情况
Spring Cloud 项目
这个演示项目使用的 Spring Boot 和 Spring Cloud 都不是最新版本, 因为最新版本最低要求 JDK17. 这里选择的是对应 JDK11 可用的最高版本, 各组件版本明细为
- Consul 1.15
- JDK 11
- Spring Boot 2.7.11
- Spring Cloud 2021.0.6
整体结构
这个用于演示的项目名称为 Dummy, 包含3个子模块, 分别是 dummy-common-api, dummy-common-impl 和 dummy-admin, 其中
- dummy-common-api 和 dummy-common-impl 逻辑上属于同一个模块 dummy-common. api 是对外输出的接口, impl是对应的实现
- dummy-admin 依赖 dummy-common-api , 使用其提供的接口
打包后, 需要部署的是两个jar: dummy-common.jar 和 dummy-admin.jar, 前者提供服务接口, 后者消费前者提供的接口, 并对外(例如前端, 小程序, APP)提供接口
项目的整体结构如下
│ pom.xml
├───dummy-admin
│ │ pom.xml
│ ├───src
│ │ ├───main
│ │ │ ├───java
│ │ │ └───resources
│ │ │ application.yml
│ │ └───test
│ └───target
├───dummy-common-api
│ │ pom.xml
│ ├───src
│ │ ├───main
│ │ │ ├───java
│ │ │ └───resources
│ │ └───test
│ └───target
└───dummy-common-impl
│ pom.xml
├───src
│ ├───main
│ │ ├───java
│ │ └───resources
│ │ application.yml
│ └───test
└───target
根模块 Dummy
根模块的 pom.xml 中,
- 定义了子模块, module标签中的内容, 要和子模块目录名一致.
- 设置JDK版本 11
- 引入全局 Spring Boot Dependencies, 版本 2.7.11
- 引入全局 Spring Cloud Dependencies, 版本 2021.0.6
- 还有一些是Plugin相关的版本, 略
<?xml version="1.0" encoding="UTF-8"?>
...
<name>Dummy: Root</name>
<modules>
<module>dummy-common-api</module>
<module>dummy-common-impl</module>
<module>dummy-admin</module>
</modules>
<properties>
<!-- Global encoding -->
<project.jdk.version>11</project.jdk.version>
<project.source.encoding>UTF-8</project.source.encoding>
<!-- Global dependency versions -->
<spring-boot.version>2.7.11</spring-boot.version>
<spring-cloud.version>2021.0.6</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Boot Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>$spring-boot.version</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud Dependencies -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>$spring-cloud.version</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
...
</build>
</project>
Dummy Common API 模块
这个模块用于生成依赖的jar包, 作用非常重要. 以下详细说明
pom.xml 中除了定义和父模块的关系, 需要引入 openfeign
<?xml version="1.0" encoding="UTF-8"?>
...
<parent>
<groupId>com.rockbb.test</groupId>
<artifactId>dummy</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>dummy-common-api</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>Dummy: Commons API</name>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
...
</dependencies>
<build>
...
</build>
</project>
定义一个 UserDTO, 这个是用于传输的数据对象
@Data
public class UserDTO implements Serializable
private Long id;
private String name;
对应的服务接口. 这里用到了 @FeignClient 注解
- @FeignClient 是给 dummy-admin 模块用的
- name= CommonConstant.SERVICE_NAME 就是 "dummy-common", 因为这个API模块中所有Service接口都使用同样的名称, 这边做成常量
- contextId = "userDTOService" 如果不加这个参数, 多个 FeignClient 使用同样的 name 时, 就会冲突. 这个一般直接定义为这个 service 的bean名称
- path = "/userDTOService" 用于指定当前类中所有接口的请求前缀. 在更早的版本中, 可以将 @RequestMapping 和 @FeignClient 联用, 这个是定义在 @RequestMapping 中的, 后来不允许了, 因为有安全风险.
- @GetMapping 和 @PostMapping 同时用于 dummy-admin 和 dummy-common
- 对于 dummy-admin, 这就是 FeignClient 的请求路径
- 对于 dummy-common, 这就是 Contoller 方法的服务路径
- 需要注意 @GetMapping 请求的接口形式, 必须显式添加 @RequestParam("id") 这类 GET 模式的参数注解, 否则使用 @GetMapping 的 Feign 请求也会被转为 POST 而导致请求错误.
@FeignClient(name = CommonConstant.SERVICE_NAME, contextId = "userDTOService", path = "/userDTOService")
public interface UserDTOService
@GetMapping("/get")
UserDTO get(@RequestParam("id") long id);
@PostMapping("/add")
int add(@RequestBody UserDTO dto);
在 dummy-admin 中, 这个接口会被实例化为 feign 代理, 在模块中可以像普通 service 一样调用, 而在 dummy-common 中, 不引入 feign 依赖, 或者在 @EnableFeignClients 的 basePackages 中避开本包路径, 就会忽略这个注解, 从而实现模块间接口的关联.
与现在很多 Spring Cloud 项目中单独拆出一个 Service 模块的做法, 这种实现有很多的优点
- 开发过程友好. 与单机开发几乎一样的代码量, 唯一区别是要注意 Get 和 Post 对请求参数的格式和个数的约束
- 易重构易扩展. 可以借助 IDE 的代码分析能力, 改动自动标红, 避免人为错误和遗漏
- 性能开销小, 如果 DTO 直接映射到数据库字段, 可以全程使用一个类.
Dummy Common Impl 模块
模块的 pom.xml
- 引入 spring-boot-starter-web, 因为要提供 RestController 的能力
- 引入 spring-cloud-starter-consul-discovery 或 spring-cloud-starter-consul-all, 因为要接 Consul
- 引入 dummy-common-api 依赖, 因为 Controller 请求定义在 API 中
- 打包使用 spring-boot-maven-plugin 的 repackage, 因为要打 fat jar, 在服务器上实现单包部署
<?xml version="1.0" encoding="UTF-8"?>
...
<name>Dummy: Common Implementation</name>
<dependencies>
<!-- Spring Boot Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Cloud Dependencies consul-discovery 和 consul-all 二选一 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-all</artifactId>
</dependency>
...
<dependency>
<groupId>com.rockbb.test</groupId>
<artifactId>dummy-common-api</artifactId>
<version>$project.version</version>
</dependency>
</dependencies>
<build>
<finalName>dummy-common</finalName>
<resources>
...
</resources>
<plugins>
...
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
配置部分 application.yml
- 定义服务端口 8762
- 定义 servlet 路径, 必须定义, 否则不会配置 Controller 请求
- spring.application.name: dummy-common 定义了本服务的名称, 这个名称就是在 FeignClient 中引用的服务名称, 需要与 FeignClient 中的值一致
- spring.config.import 如果使用这个设置, 依赖要使用 consul-all, 因为 consul-discovery 中不带 consul-config. 使用这个设置后, 会自动使用默认的 Consul 地址和端口
- cloud.consul.host 和 port 如果使用了config.import, 在这里可以修改默认的值, 如果不使用config.import, 则必须配置 host 和 port, 依赖可以换成 consul-discovery
- cloud.consul.discovery.health-check-path 用于更改默认的 health 检查请求路径, 默认的是 /actuator/health, 这里改为 /health
- cloud.consul.discovery.instance-id 用于定义当前实例在 Consul 里的实例ID. 默认使用 application.name-port, 如果正好这个服务在两个服务器上分别跑了一个实例, 且实例端口一样, 就会产生冲突, 可以改为 application.name-[随机串] 的形式避免冲突
server:
port: 8762
tomcat:
uri-encoding: UTF-8
servlet:
context-path: /
spring:
application:
name: dummy-common
config:
import: \'optional:consul:\' #This will connect to the Consul Agent at the default location of "http://localhost:8500"
# cloud:
# consul:
# host: 127.0.0.1
# port: 8500
# discovery:
# health-check-path: /health # replace the default /actuator/health
# instance-id: $spring.application.name:$random.value
代码部分, 首先是实现 health 检查的处理方法, 这部分是普通的 RestController 方法. 返回字符串可以任意指定, 只要返回的 code 是 200 就可以
@RestController
public class HealthCheckServiceImpl
@GetMapping("/health")
public String get()
return "SUCCESS";
服务接口的实现类, 这里实现了两个接口方法 get 和 add
- 使用 @RestController 注解, 与 API Service 中方法上的 @GetMapping 和 @PostMapping 配合, 将 Service 方法映射为 Controller 方法
- 在类上的 @RequestMapping("userDTOService") 方法是必须的, 因为在 API Service 中与 @FeignClient 冲突无法定义, 只能在这里定义
- 方法和参数上除了 @Override 不需要任何注解, 因为都在 API Service 上定义过了. 这里加上注解也没问题, 但是要手工保持一致.
@RestController
@RequestMapping("userDTOService")
public class UserDTOServiceImpl implements UserDTOService
@Autowired
private UserRepo userRepo;
@Override
public UserDTO get(long id)
log.debug("Get user: ", id);
UserDTO user = new UserDTO();
user.setId(id);
user.setName("dummy");
return user;
@Override
public int add(UserDTO dto)
log.debug("Add user: ", dto.getName());
return 0;
dummy-common 模块运行后会将接口注册到 Consul, 启动后注意观察两部分:
- Consul 的日志输出和控制面板显示, 在-dev模式下, 节点注册后 Consul 日志会显示模块的名称和心跳检测记录, 面板上会显示新的 Node
- Consul 控制面板中显示的 Health Checks 是否正常, 如果不正常, 需要检查 /health 路径为什么访问失败
Dummy Admin 模块
dummy-admin 是调用接口, 并对外提供服务的模块
pom.xml 和 dummy-common 基本一样, 因为都要连接 Consul, 都要提供 Controller 方法
<?xml version="1.0" encoding="UTF-8"?>
...
<name>Dummy: Admin API</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.rockbb.test</groupId>
<artifactId>dummy-common-api</artifactId>
<version>$project.version</version>
</dependency>
</dependencies>
<build>
<finalName>dummy-admin</finalName>
<resources>
...
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
</build>
</project>
在主应用入口, 除了 @SpringBootApplication 以外, 还需要增加两个注解
- @EnableDiscoveryClient(autoRegister=false) 连接到 Consul 并使用服务发现, 默认会将当前节点也注册到 Consul 作为服务. 对于纯消费节点, 不对其它节点提供接口的, 使用 autoRegister=false 可以避免将自己注册到 Consul
- @EnableFeignClients(basePackages = "com.rockbb.test.dummy.common.api") 扫描对应的包, 对 @FeignClient 注解实例化接口代理
/* Attach to discovery service without registering itself */
@EnableDiscoveryClient(autoRegister=false)
@EnableFeignClients(basePackages = "com.rockbb.test.dummy.common.api")
@SpringBootApplication
public class AdminApp
public static void main(String[] args)
SpringApplication.run(AdminApp.class, args);
在调用方法的地方, 按普通 Service 注入和调用
@Slf4j
@RestController
public class IndexController
@Autowired
private UserDTOService userDTOService;
@GetMapping(value = "/user_get")
public String doGetUser()
UserDTO user = userDTOService.get(100L);
return user.getId() + ":" + user.getName();
@GetMapping(value = "/user_add")
public String doAddUser()
UserDTO user = new UserDTO();
user.setName("foobar");
int result = userDTOService.add(user);
return String.valueOf(result);
可以通过注入的 DiscoveryClient 对象, 查看对应服务的服务地址(一般不需要)
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/services")
public Optional<URI> serviceURL()
return discoveryClient.getInstances(CommonConstant.SERVICE_NAME)
.stream()
.map(ServiceInstance::getUri)
.findFirst();
参考
- Consul和Spring Boot非常详细的配置教程 https://medium.com/javarevisited/hands-on-consul-with-spring-boot-1ebf2918165c
爱油科技基于Docker和Spring Cloud的微服务实践
本文介绍了爱油科技——一家成品油行业的初创公司如何以Spring Cloud为框架,以Rancher为容器调度平台完成整体业务的微服务化。
从单体应用到微服务
单体应用
对于单体应用来说,优点很多,例如:
小而美,结构简单易于开发实现
部署门槛低,单个Jar包或者网站打包即可部署
可快速实现多实例部署
然而随着业务复杂性的上升,业务规模的扩大,缺点也显现出来,例如:
随着业务发展更多的需求被塞进系统,体系结构逐渐被侵蚀反应堆林立
被技术绑架,难以为特定业务选择平台或框架,尽管可能有更适宜的技术做这件事
协作困难,不同业务的团队在一个系统上进行开发相互冲突
难以扩展,为了热点业务而不得不同时扩容全部业务,或者难以继续扩容
因此微服务技术作为一项对分布式服务治理的架构模式,逐渐被大家认识了。
架构拆分
实施微服务,首先对我们的架构进行了拆分:按行分层,按列分业务。
在我们的微服务体系中,所有的服务被划分为了三个层次:
基础设施层:为所有业务提供基础设施,包括服务注册、数据库和NoSQL、对象存储、消息队列等基础设施服务,这一层通常是由成熟组件、第三方服务组成。
业务服务层:业务微服务,根据业务领域每个子域单独一个微服务,分而治之。
接入层:直接对外提供服务,例如网站、API接口等。接入层不包含复杂的业务逻辑,只做呈现和转换。
实践中我们主要关注业务服务层和接入层,对于没有足够运维力量的我们,基础设施使用云服务是省事省力的选择。
业务服务层我们给他起名叫作Epic,接入层我们起名Rune,建立之初便订立了如下原则:
业务逻辑层内所有服务完全对等,可相互调用
业务逻辑层所有服务必须是无状态的
接入层所有服务可调用业务逻辑层所有服务,但接入层内部同层服务之间不可调用
接入层不能包含业务逻辑代码
所有微服务必须运行在Docker容器里
业务逻辑层我们主要使用使用Java,接入层我们主要使用PHP或Node。后来随着团队的成长,逐步将接入层全部迁移至Node。
框架选型
爱油科技作为一家成品油行业的初创型公司,需要面对非常复杂的业务场景,而且随着业务的发展,变化的可能性非常高。所以在微服务架构设计之初,我们就期望我们的微服务体系能:
不绑定到特定的框架、语言
服务最好是Restful风格
足够简单,容易落地,将来能扩展
和Docker相容性好
目前常见的微服务相关框架:
Dubbo、DubboX
Spring Cloud
Motan
Thrift、gRPC
这些常见的框架中,Dubbo几乎是唯一能被称作全栈微服务框架的“框架”,它包含了微服务所需的几乎所有内容,而DubboX作为它的增强,增加了REST支持。
它优点很多,例如:
全栈,服务治理的所有问题几乎都有现成答案
可靠,经过阿里实践检验的产品
实践多,社区有许多成功应用Dubbo的经验
不过遗憾的是:
已经停止维护
不利于裁剪使用
“过于Java”,与其他语言相容性一般
Motan是微博平台微服务框架,承载了微博平台千亿次调用业务。
优点是:
性能好,源自于微博对高并发和实时性的要求
模块化,结构简单,易于使用
与其他语言相容性好
不过:
为“短平快”业务而生,即业务简单,追求高性能高并发。
Apache Thrift、gRPC等虽然优秀,并不能算作微服务框架,自身并不包括服务发现等必要特性。
如果说微服务少不了Java,那么一定少不了Spring,如果说少不了Spring,那么微服务“官配”Spring Cloud当然是值得斟酌的选择。
Spring Cloud优点:
“不做生产者,只做搬运工”
简单方便,几乎零配置
模块化,松散耦合,按需取用
社区背靠Spring大树
当然它有很多不足之处,例如:
轻量并非全栈
没解决RPC的问题
实践案例少
根据我们的目标,我们最终选择了Spring Cloud作为我们的微服务框架,原因有4点:
虽然Dubbo基础设施更加完善,但结构复杂,我们很难吃得下,容易出坑;
基于Apache Thrift和gRPC自研,投入产出比很差;
不想过早引入RPC以防滥用,Restful风格本身就是一种约束;
做选择时,Motan还没有发布。
因此Spring Cloud成为了理性的选择。
Spring Cloud
Spring Cloud是一个集成框架,将开源社区中的框架集成到Spring体系下,几个重要的家族项目:
spring-boot,一改Java应用程序运行难、部署难,甚至无需Web容器,只依赖JRE即可
spring-cloud-netflix,集成Netflix优秀的组件Eureka、HystrixRibbon、Zuul,提供服务发现、限流、客户端负载均衡和API网关等特性支持
spring-cloud-config,微服务配置管理
spring-cloud-consul,集成Consul支持
当然,SpringCloud下子项目非常多,这里就不一一列出介绍了。
服务发现和配置管理
Spring Cloud Netflix提供了Eureka服务注册的集成支持,不过没选它是因为:
更适合纯Java平台的服务注册和发现
架构中仍然需要其他分布式KV服务,没解决我们的核心问题
Docker作为支撑平台的重要技术之一,Consul几乎也是我们的必选服务。因此我们觉得一事不烦二主,理所应当的Consul成为我们的服务注册中心。
Consul的优势:
使用Raft一致性算法,能保证分布式集群内各节点状态一致
提供服务注册、服务发现、服务状态检查
支持HTTP、DNS等协议
提供分布式一致性KV存储
也就是说,Consul可以一次性解决我们对服务注册发现、配置管理的需求,而且长期来看也更适合跟不同平台的系统,包括和Docker调度系统进行整合。
最初打算自己开发一个Consul和Spring Cloud整合的组件,不过幸运的是,我们做出这个决定的时候,spring-cloud-consul刚刚发布了,我们可以拿来即用,这节约了很多的工作量。
因此借助Consul和spring-cloud-consul,我们实现了:
服务注册,引用了srping-cloud-consul的项目可以自动注册服务,也可以通过HTTP接口手动注册,Docker容器也可以自动注册
服务健康状态检查,Consul可以自动维护健康的服务列表
异构系统可以直接通过Consul的HTTP接口拉取并监视服务列表,或者直接使用DNS解析服务
通过分布式一致性KV存储进行微服务的配置下发
为一些业务提供选主和分布式锁服务
当然也踩到了一些坑:
#!/usr/bin/env bash
set -e
if [ -n "$RUN_IN_RANCHER" ]; then
echo "Waiting for ip address..."
sleep 5
RANCHER_MS_BASE=http://rancher-metadata/2015-12-19
PRIMARY_IP=`curl -sSL $RANCHER_MS_BASE/self/container/primary_ip`
SERVICE_INDEX=`curl -sSL $RANCHER_MS_BASE/self/container/service_index`
if [ -n "$PRIMARY_IP" ]; then
export SPRING_CLOUD_CONSUL_DISCOVERY_HOSTNAME=$PRIMARY_IP
fi
echo "Starting service #${SERVICE_INDEX-1} at $PRIMARY_IP."
fi
exec "$@"
服务集成
为了方便开发人员使用,微服务框架应当简单容易使用。对于很多微服务框架和RPC框架来说,都提供了很好的机制。在Spring Cloud中通过OpenFeign实现微服务之间的快速集成:
服务方声明一个Restful的服务接口,和普通的Spring MVC控制器几乎别无二致:
@RestController
@RequestMapping("/users")
public class UserResource {
@RequestMapping(value = "{id}", method = RequestMethod.GET, produces = "application/json")
public UserRepresentation findOne(@PathVariable("id") String id) {
User user = this.userRepository.findByUserId(new UserId(id));
if (user == null || user.getDeleted()) {
throw new NotFoundException("指定ID的用户不存在或者已被删除。");
}
return new UserRepresentation(user);
}
}
客户方使用一个微服务接口,只需要定义一个接口:
@FeignClient("epic-member-microservice")
public interface UserClient {
@Override
@RequestMapping(value = "/users/{id}", method = RequestMethod.GET, produces = "application/json")
User findOne(@PathVariable("id") String id);
}
在需要使用UserClient的Bean中,直接注入UserClient类型即可。事实上,UserClient和相关VO类,可以直接作为公共接口封装在公共项目中,供任意需要使用的微服务引用,服务方Restful Controller直接实现这一接口即可。
OpenFeign提供了这种简单的方式来使用Restful服务,这大大降低了进行接口调用的复杂程度。
对于错误的处理,我们使用HTTP状态码作为错误标识,并做了如下规定:
4xx用来表示由于客户方参数错误、状态不正确、没有权限、操作冲突等种种原因导致的业务错误。
5xx用来表示由于服务方系统异常、无法服务等原因服务不可用的错误。
对于服务器端,只需要在一个异常类上添加注解,即可指定该异常的HTTP响应状态码,例如:
@ResponseStatus(HttpStatus.NOT_FOUND)
public class NotFoundException extends RuntimeException {
public NotFoundException() {
super("查找的资源不存在或者已被删除。");
}
public NotFoundException(String message) {
super(message);
}
public NotFoundException(String message, Throwable cause) {
super(message, cause);
}
对于客户端我们实现了自己的FeignClientExceptionErrorDecoder来将请求异常转换为对于的异常类,示例如下:
@Component
public class FeignClientExceptionErrorDecoder implements ErrorDecoder {
private final ErrorDecoder delegate = new ErrorDecoder.Default();
@Override
public Exception decode(String methodKey, Response response) {
// Only decode 4xx errors.
if (response.status() >= 500) {
return delegate.decode(methodKey, response);
}
// Response content type must be json
if (response.headers().getOrDefault("Content-Type", Lists.newArrayList()).stream()
.filter(s -> s.toLowerCase().contains("json")).count() > 0) {
try {
String body = Util.toString(response.body().asReader());
// 转换并返回异常对象
...
} catch (IOException ex) {
throw new RuntimeException("Failed to process response body.", ex);
}
}
return delegate.decode(methodKey, response);
}
}
需要注意的是,decode方法返回的4xx状态码异常应当是HystrixBadRequestException的子类对象,原因在于,我们把4xx异常视作业务异常,而不是由于故障导致的异常,所以不应当被Hystrix计算为失败请求,并引发断路器动作,这一点非常重要。
在UserClient.findOne方法的调用代码中,即可直接捕获相应的异常了:
try {
User user = this.userClient.findOne(new UserId(id));
} catch(NotFoundException ex) {
...
}
通过OpenFeign,我们大大降低了Restful接口进行服务集成的难度,几乎做到了无额外工作量的服务集成。
服务质量保证
微服务架构下,由于调用需要跨系统进行远程操作,各微服务独立运维,所以在设计架构时还必须考虑伸缩性和容错性,具体地说主要包括以下几点要求:
服务实例可以平滑地加入、移除
流量可以均匀地分布在不同的实例上
接口应当资源隔离,防止因为个别接口调用时间过长导致线程池被占满而导致整个服务不可用
能支持接口降级并隔离故障节点,防止集群雪崩
服务能进行平滑升级
spring-cloud-netflix和相关组件为我们提供了很好的解决方案:
Hystrix——实现了断路器模式,帮助控流和降级,防止集群雪崩,就像汽车的避震器
Ribbon——提供了客户端负载均衡器
Zuul——API网关模式,帮助实现接口的路由、认证等
下面主要介绍一下,各个组件在进行服务质量保证中是如何发挥作用的。
Consul
Consul中注册了一致性的可用的服务列表,并通过健康检查保证这些实例都是存活的,服务注册和检查的过程如下:
服务启动完成,服务端口开始监听时,spring-cloud-consul通过Consul接口发起服务注册,将服务的/health作为健康检查端点;
Consul每隔5秒访问/health,检查当前微服务是否为UP状态;
/health将会收集微服务内各个仪表收集上来的状态数据,主要包括数据库、消息队列是否连通等;
如果为UP状态,则微服务实例被标记为健康可用,否则被标记成失败;
当服务关闭时,先从Consul中取消服务注册,再优雅停机。
这样能够保证Consul中列出的所有微服务状态都是健康可用的,各个微服务会监视微服务实例列表,自动同步更新他们。
Hystrix
Hystrix提供了断路器模式的实现,主要在三个方面可以说明:
首先Hystrix提供了降级方法,断路器开启时,操作请求会快速失败不再向后投递,直接调用fallback方法来返回操作;当操作失败、被拒或者超时后,也会直接调用fallback方法返回操作。这可以保证在系统过载时,能有后备方案来返回一个操作,或者优雅的提示错误信息。断路器的存在能让故障业务被隔离,防止过载的流量涌入打死后端数据库等。
然后是基于请求数据统计的断路开关,在Hystrix中维护一个请求统计了列表(默认最多10条),列表中的每一项是一个桶。每个桶记录了在这个桶的时间范围内(默认是1秒),请求的成功数、失败数、超时数、被拒数。其中当失败请求的比例高于某一值时,将会触发断路器工作。
最后是不同的请求命令(HystrixCommand)可以使用彼此隔离的资源池,不会发生相互的挤占。在Hystrix中提供了两种隔离机制,包括线程池和信号量。线程池模式下,通过线程池的大小来限制同时占用资源的请求命令数目;信号量模式下通过控制进入临界区的操作数目来达到限流的目的。
这里包括了Hystrix的一些重要参数的配置项:
Ribbon
Ribbon使用Consul提供的服务实例列表,可以通过服务名选取一个后端服务实例连接,并保证后端流量均匀分布。spring-cloud-netflix整合了OpenFeign、Hystrix和Ribbon的负载均衡器,整个调用过程如下(返回值路径已经省略):
在这个过程中,各个组件扮演的角色如下:
Feign作为客户端工厂,负责生成客户端对象,请求和应答的编解码
Hystrix提供限流、断路器、降级、数据统计
Ribbon提供负载均衡器
Feign负责提供客户端接口收调用,把发起请求操作(包括编码、解码和请求数据)封装成一个Hystrix命令,这个命令包裹的请求对象,会被Ribbon的负载均衡器处理,按照负载均衡策略选择一个主机,然后交给请求对象绑定的HTTP客户端对象发请求,响应成功或者不成功的结果,返回给Hystrix。
spring-cloud-netflix中默认使用了Ribbon的ZoneAwareLoadBalancer负载均衡器,它的负载均衡策略的核心指标是平均活跃请求数(Average Active Requests)。ZoneAwareLoadBalancer会拉取所有当前可用的服务器列表,然后将目前由于种种原因(比如网络异常)响应过慢的实例暂时从可用服务实例列表中移除,这样的机制可以保证故障实例被隔离,以免继续向其发送流量导致集群状态进一步恶化。不过由于目前spring-cloud-consul还不支持通过consul来指定服务实例的所在区,我们正在努力将这一功能完善。除了选区策略外,Ribbon中还提供了其他的负载均衡器,也可以自定义合适的负载均衡器。
关于区域的支持,我提交的PR(https://github.com/spring-cloud/spring-cloud-consul/pull/251)已经Merge到spring-cloud-consul项目中,预计下个版本将会包含这项特性。
总的来看,spring-cloud-netflix和Ribbon中提供了基本的负载均衡策略,对于我们来说已经足够用了。但实践中,如果需要进行灰度发布或者需要进行流量压测,目前来看还很难直接实现。而这些特性在Dubbo则开箱即用。
Zuul
Zuul为使用Java语言的接入层服务提供API网关服务,既可以根据配置反向代理指定的接口,也可以根据服务发现自动配置。Zuul提供了类似于iptables的处理机制,来帮助我们实现验证权鉴、日志等,请求工作流如下所示:
使用Zuul进行反向代理时,同样会走与OpenFeign类似的请求过程,确保API的调用过程也能通过Hystrix、Ribbon提供的降级、控流机制。
Hystrix Dashboard
Hystrix会统计每个请求操作的情况来帮助控制断路器,这些数据是可以暴露出来供监控系统热点。Hystrix Dashboard可以将当前接口调用的情况以图形形式展示出来:
Hystrix Dashboard既可以集成在其他项目中,也可以独立运行。我们直接使用Docker启动一个Hystrix Dashboard服务即可:
docker run --rm -ti -p 7979:7979 kennedyoliveira/hystrix-dashboard
为了实现能对整个微服务集群的接口调用情况汇总,可以使用spring-cloud-netflix-turbine来将整个集群的调用情况汇集起来,供Hystrix Dashboard展示。
日志监控
微服务的日志直接输出到标准输出/标准错误中,再由Docker通过syslog日志驱动将日志写入至节点机器机的rsyslog中。rsyslog在本地暂存并转发至日志中心节点的Logstash中,既归档存储,又通过ElasticSearch进行索引,日志可以通过Kibana展示报表。
在rsyslog的日志收集时,需要将容器信息和镜像信息加入到tag中,通过Docker启动参数来进行配置:
--log-driver syslog --log-opt tag="{{.ImageName}}/{{.Name}}/{{.ID}}"
不过rsyslog默认只允许tag不超过32个字符,这显然是不够用的,所以我们自定义了日志模板:
template (name="LongTagForwardFormat" type="string" string="<%PRI%>%TIMESTAMP:::date-rfc3339% %HOSTNAME% %syslogtag%%msg:::sp-if-no-1st-sp%%msg%")
在实际的使用过程中发现,当主机内存负载比较高时,rsyslog会发生日志无法收集的情况,报日志数据文件损坏。后来在Redhat官方找到了相关的问题,确认是rsyslog中的一个Bug导致的,当开启日志压缩时会出现这个问题,我们选择暂时把它禁用掉。
领域驱动设计
领域驱动设计能够很大程度上帮助我们享用微服务带来的优势,所以我们使用领域驱动设计(DDD)的方法来构建微服务,因为微服务架构和DDD有一种天然的契合。把所有业务划分成若干个子领域,有强内在关联关系的领域(界限上下文)应当被放在一起作为一个微服务。最后形成了界限上下文-工作团队-微服务一一对应的关系:
身份与访问 – 团队A – 成员微服务
商品与促销 – 团队B – 商品微服务
订单交易 – 团队C – 交易微服务
…
微服务设计
在设计单个微服务(Epic层的微服务)时,我们这样做:
使用OOD方法对业务进行领域建模,领域模型应当是充血模型
领域服务帮助完成多个领域对象协作
事件驱动,提供领域事件,供内部或者其他微服务使用
依赖倒置,在适配器接口中实现和框架、组件、SDK的整合
这给我们带来了显著的好处:
服务开发时关注于业务,边界合理清晰
容易直接对领域模型进行单元测试
不依赖特定组件或者平台
事务问题
从单体应用迁移到微服务架构时,不得不面临的问题之一就是事务。在单体应用时代,所有业务共享同一个数据库,一次请求操作可放置在同一个数据库事务中;在微服务架构下,这件事变得非常困难。然而事务问题不可避免,非常关键。
解决事务问题时,最先想到的解决方法通常是分布式事务。分布式事务在传统系统中应用的比较广泛,主要基于两阶段提交的方式实现。然而分布式事务在微服务架构中可行性并不高,主要基于这些考虑:
分布式事务需要事务管理器,对于不同语言平台来说,几乎没有有一致的实现来进行事务管理;
并非所有的持久化基施都提供完整ACID的事务,比如现在广泛使用的NoSQL;
分布式事务存在性能问题。
根据CAP理论,分布式系统不可兼得一致性、可用性、分区容错性(可靠性)三者,对于微服务架构来讲,我们通常会保证可用性、容错性,牺牲一部分一致性,追求最终一致性。所以对于微服务架构来说,使用分布式事务来解决事务问题无论是从成本还是收益上来看,都不划算。
对微服务系统来说解决事务问题,CQRS Event Sourcing是更好的选择。
CQRS是命令和查询职责分离的缩写。CQRS的核心观点是,把操作分为修改状态的命令(Command),和返回数据的查询(Query),前者对应于“写”的操作,不能返回数据,后者对应于“读”的操作,不造成任何影响,由此领域模型被一分为二,分而治之。
Event Sourcing通常被翻译成事件溯源,简单的来说就是某一对象的当前状态,是由一系列的事件叠加后产生的,存储这些事件即可通过重放获得对象在任一时间节点上的状态。
通过CQRS Event Sourcing,我们很容易获得最终一致性,例如对于一个跨系统的交易过程而言:
用户在交易微服务提交下单命令,产生领域事件PlaceOrderEvent,订单状态PENDING;
支付微服务收到领域事件进行扣款,扣款成功产生领域事件PaidEvent;
交易微服务收到领域事件PaidEvent,将订单标记为CREATED;
若支付微服务发现额度不足扣款失败,产生领域事
InsufficientEvent,交易微服务消费将订单标记为CANCELED。
我们只要保证领域事件能被持久化,那么即使出现网络延迟或部分系统失效,我们也能保证最终一致性。
实践上,我们利用Spring从4.2版本开始支持的自定义应用事件机制将本地事务和事件投递结合起来进行:
领域内业务过程会产生领域事件,通过Spring的应用事件机制进行应用内投递;
监听相应的领域事件,在事务提交前投递至消息队列;
以上全都没有异常发生,则本地事务提交,如果出现异常,本地事务回滚。
一些小经验
使用Spring Configured实现非Spring Bean的依赖注入(自己new的对象也可以注入了,对充血模型非常有用)
使用Swagger UI实现自文档的微服务,写好接口即有文档,即可调试
DevOps
到目前为止我们已经有数十个微服务运行于线上了,微服务数目甚至多过了团队人数。如果没有DevOps支持,运维这些微服务将是一场灾难。
我们使用Docker镜像作为微服务交付的标准件:
Gitlab管理团队项目代码
Gitlab-CI提供构建打包,大家提交的项目都要构建并跑通测试
使用Rancher作为Docker调度平台,Merge后RC分支自动部署
测试通过后统一上线发布
由于时间所限,这里就不展开赘述了。
永不完美
基于spring-cloud-consul的配置管理仍然需要完善,对于大规模应用的环境中,配置的版本控制、灰度、回滚等非常重要。SpringCloud提供了一个核,但是具体的使用还要结合场景、需求和环境等,再做一些工作。
对于非JVM语言的微服务和基于SpringCloud的微服务如何协同治理,这一问题仍然值得探索。包括像与Docker编排平台,特别是与Mesos协同进行伸缩的服务治理,还需要更多的实践来支持。
总结
是否选用微服务架构,应当根据业务实际情况进行判断,切勿跟风为了微服务而微服务;
目前来看还没有微服务全栈框架,Spring Cloud也未必是最优方案,技术选型还是应当务实;
微服务架构下,对于业务的理解拆分、领域建模等提出了更高的要求,相比框架,它们才是微服务架构的基石;
DevOps是微服务实践中的重要一环,不容小视。
Q&A
Q:你们是部署在公有云,还是托管机房?
A:我们部署在阿里云上,使用了很多阿里云服务作为基础设施,这一点是为了节约运维成本。
Q:怎么解决服务过多依赖问题?开发也会有麻烦,因为要开发一个功能,为了把服务跑起来,可能要跑很多服务。
A:在我们的实际开发过程中,也遇到了这个问题。主要的是通过部署几个不同的仿真环境,一组开发者可以共用这组环境。本地开发也很简单,只需要把Consul指向到这个集群的Consul上即可。
Q:你们微服务业务调用最深有几层?restful接口调用链的效率如何?比单体结构慢多少?
A:一般不超过3层,这是领域驱动设计给我们带来的优势,单个服务几乎自己就能完成职责范围内的任务,没有出现RPC灾难,一个业务我们也不倾向于拆分成若干个远程操作进行。
Q:你好,我们单位从6月份 开始实施微服务化(O2O业务),使用的是Dubbo,使用事务型消息来做最终一致性,请问CQRS+Event Sourcing相对于事务型消息队列来处理一致性问题 有什么优势么?
A:其实CQRS+Event Sourcing是一种观念的转变,落地还是需要靠存储和消息队列,优势在于可以消除系统中的锁点,性能会更好。
Q:关于领域事件,如果本地事务提交后,下游的服务报错,是否只能在业务层面再发起一个补偿的事件,让本地事务达到最终一致性呢?
A:如果下游服务报错,那么事件不会被消费。会以退避重试的方式重发事件。
Q:分享很棒,请问你们的Docker的部署是基于原生的Docker和Swarm,还是Kubernetes来做的?
A:我们使用Rancher来管理集群。没选Kubernetes的原因是因为团队资源有限,Swarm最初试过,调度不够完善。后来Docker 1.12以后的Swarmkit应该是更好的选择。
Q:微服务开发测试用例相比于单体应用是不是更复杂一些?你们是怎样保证测试覆盖率的?
A:事实上对于单元测试来讲,反而更容易进行了。因为系统拆分之后,把原来很难测试的一些节点给疏通了。
Q:你好请教一下,当微服务之间的依赖关系比较多,且层次比较深时,服务的启动,停止,以及升级之间的关系如何处理?
A:目前还几乎没出现过需要彻底冷启动的情况。而且启动服务时并不需要依赖服务也启动,只需要发生业务时,依赖服务启动即可。
官 网:cnrancher.com
长按二维码,加入技术群
以上是关于Spring Cloud开发实践: 基于Consul和Spring Cloud 2021.0的演示项目的主要内容,如果未能解决你的问题,请参考以下文章
爱油科技基于Docker和Spring Cloud的微服务实践
Spring Boot 实践3 --基于spring cloud 实现微服务的简单调用
基于spring boot 2.x 的 spring-cloud-admin 实践
贝壳金控赵文乐:基于 Spring Cloud 的服务治理实践