💡 本文介绍了微服务优雅高低线的实际办法,包含实用于 Spring 利用的优雅高低线逻辑,以及应用 Docker 实现无损下线的 demo,以及服务预热。同时,本文还总结了优雅高低线的价值和挑战。
前言
微服务优雅高低线的原理是指在微服务的公布过程中,保障服务的稳定性和可用性,防止因为服务的变更而造成流量的中断或谬误。
微服务优雅高低线的原理能够从三个角度来思考:
- 服务端的优雅上线,即在服务启动后,期待服务齐全就绪后再对外提供服务,或者有一个服务预热的过程。
- 服务端的无损下线,即在服务进行前,先从注册核心登记,回绝新的申请,期待旧的申请处理完毕后再下线服务。
- 客户端的容灾策略,即在调用服务时,通过负载平衡、重试、黑名单等机制,抉择衰弱的服务实例,防止调用不可用的服务实例。
微服务优雅高低线能够进步微服务的稳定性和可靠性,缩小公布过程中的危险和损失。
优雅上线
优雅上线,也叫无损上线,或者提早公布,或者提早裸露,或者服务预热。
优雅上线的目标是为了进步公布的稳定性和可靠性,防止因为利用的变更而造成流量的中断或谬误。
优雅上线的办法
优雅上线的办法有以下几种:
- 提早公布:即提早裸露应用服务,比方利用须要一些初始化操作后能力对外提供服务,如初始化缓存,数据库连接池等相干资源就位,能够通过配置或代码来实现提早裸露。
- QoS 命令:即通过命令行或 HTTP 申请来管制应用服务的上线和下线,比方在利用启动时不向注册核心注册服务,而是在服务健康检查完之后再手动注册服务。
- 服务注册与发现:即通过注册核心来治理应用服务的状态和路由信息,比方在利用启动时向注册核心注册服务,并监听服务状态变动事件,在利用进行时向注册核心登记服务,并告诉其余服务更新路由信息。
- 灰度公布:即通过分流策略来管制应用服务的流量调配,比方在公布新版本的利用时,先将局部流量导入到新版本的利用上,察看其运行状况,如果没有问题再逐渐减少流量比例,直到全副切换到新版本的利用上。
下面的办法核心思想都是一个,就是等服务做好了筹备再把申请放行过来。
优雅上线的实现
大部分优雅上线都是通过注册核心和服务治理能力来实现的。
对于初始化过流程较长的利用,因为注册通常与利用初始化过程同步进行,因而可能呈现利用还未齐全初始化就曾经被注册到注册核心供内部消费者调用,此时间接调用可能会导致申请报错。
所以,通过服务注册与发现来做优雅上线的基本思路是:
- 在利用启动时,提供一个健康检查接口,用于反馈服务的状态和可用性。
-
利用启动后,能够采纳下列办法来使新的申请临时不进入新版的服务实例。
- 临时不向注册核心注册服务。
- 隔离服务,有些注册核心反对隔离服务实例,比方北极星。
- 将权重配置为 0,比方 nacos。
- 将服务实例的 enable 改为 false,比方 nacos。
- 让健康检查接口返回不衰弱的状态。
- 在新版本的利用实例实现初始化操作后,确保了可用性后,再对应的将上述的办法勾销,这样就能够让新的申请被路由到新版本的利用实例上。
- 如果须要预热,就让流量进入新版本的利用实例时按比例的一点点减少。
这样,就能够实现优雅上线的过程,保障申请进来的时候,不会因为新版本的利用实例没有筹备好而导致申请失败。
优雅上线的代码 demo
咱们以 Spring Cloud 和 Nacos 为例,讲一下如何通过服务注册与发现来做优雅上线的过程。
首先,咱们须要创立一个 Spring Cloud 我的项目,并增加 Nacos 的依赖。
而后,咱们须要在 application.properties 文件中配置 Nacos 的相干信息,如注册核心地址,服务名,分组名等,例如:
spring.application.name=provider
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.cloud.nacos.discovery.group=DEFAULT_GROUP
接下来,咱们须要在启动类上增加 @EnableDiscoveryClient
注解,示意开启服务注册与发现性能,例如:
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {public static void main(String[] args) {SpringApplication.run(ProviderApplication.class, args);
}
}
而后,咱们须要创立一个 Controller 类,提供一个简略的接口,用于返回服务的信息,例如:
@RestController
public class ProviderController {@Value("${server.port}")
private int port;
@GetMapping("/hello")
public String hello() {return "Hello, I am provider, port:" + port;}
}
最初,如果须要咱们能够重写健康检查接口,用于反馈服务的状态和可用性。这里咱们须要引入Actuator
。
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Override
public Health health() {if (isDatabaseConnectionOK()) {return Health.up().build();} else {return Health.down().withDetail("Error Code", "DB-001").build();}
}
private boolean isDatabaseConnectionOK() {
// 查看数据库连贯、缓存等
return true;
}
}
这样,咱们就实现了一个简略的服务提供者利用,并且能够通过 Nacos 来实现服务注册与发现。
接下来,咱们须要创立一个服务消费者利用,并且也增加 Nacos 的依赖和配置信息。
而后,咱们须要在启动类上增加 @EnableDiscoveryClient
注解,示意开启服务注册与发现性能,并且应用 RestTemplate 来调用服务提供者的接口,例如:
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerApplication {public static void main(String[] args) {SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
@LoadBalanced // 开启负载平衡
public RestTemplate restTemplate() {return new RestTemplate();
}
@RestController
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/hello")
public String hello() {
// 应用服务名来调用服务提供者的接口
return restTemplate.getForObject("<http://provider/hello>", String.class);
}
}
}
这里咱们应用了 @LoadBalanced 注解来开启负载平衡性能,并且应用服务名 provider 来调用服务提供者的接口。
这样,咱们就实现了一个简略的服务消费者利用,并且能够通过 Nacos 来实现服务注册与发现。
接下来,咱们就能够通过以下步骤来实现优雅上线的过程:
- 在公布新版本的服务提供者利用时,先启动新版本的利用实例,然而不向注册核心注册服务,或者让健康检查接口返回不衰弱的状态,这样就不会有新的申请进入新版本的利用实例。这能够通过配置或代码来实现,例如:
# 不向注册核心注册服务
spring.cloud.nacos.discovery.register-enabled=false
// 让健康检查接口返回不衰弱的状态
this.isHealthy = false;
- 在新版本的利用实例实现初始化操作后,再向注册核心注册服务,或者让健康检查接口返回衰弱的状态,这样就能够让新的申请被路由到新版本的利用实例上。这能够通过配置或代码来实现,例如:
# 向注册核心注册服务
spring.cloud.nacos.discovery.register-enabled=true
// 让健康检查接口返回衰弱的状态
this.isHealthy = true;
这样,就能够实现优雅上线的过程,保障正在解决的申请不会被中断,而新的申请会被路由到新版本的利用上。
服务预热
服务预热是指在服务上线之前,先让服务处于一个运行状态,让其加载必要的资源、建设连贯等,以便在服务上线后可能疾速响应申请。如下图所示。
在流量较大状况下,刚启动的服务间接解决大量申请可能因为利用外部资源初始化不彻底从而呈现申请阻塞、报错等问题。此时通过服务预热,在服务刚启动阶段通过小流量帮忙服务在解决大量申请前实现初始化,能够帮忙发现服务上线后可能存在的问题,例如资源有余、连接数过多等,从而及时进行调整和优化,确保服务的稳定性和可靠性。
Spring Boot 实现服务预热
咱们能够通过应用 Spring Boot Actuator 来实现服务预热。
- 增加 Spring Boot Actuator 依赖。
- 配置了将所有
Actuator
端点裸露进去,并启用了预热端点。
management.endpoints.web.exposure.include=*
management.endpoint.warmup.enabled=true
- 这时咱们就能够调用 warmup 接口来实现预热了。默认的接口如下:http://localhost:8080/actuator/warmup
这里 spring 的 warmup 端点会做以下几件事件:
- 加载 Spring 上下文
- 初始化连接池
- 加载缓存数据
- 发送测试申请
如果咱们想自定义预热逻辑,咱们也能够通过实现 warmup
接口来自定义预热的逻辑。代码如下:
@Component
public class MyWarmup implements Warmup {
@Override
public void warmup() {// 实现预热逻辑}
}
优雅下线
无损下线、优雅下线都是同一个意思。都是为了防止服务下线的时候因为申请没有解决完导致申请失败的状况。
优雅下线的办法
无损下线的一些罕用的工具或框架有:
- Dubbo-go:反对多种注册核心、负载平衡、容灾策略等,能够实现优雅高低线的设计与实际。
- Spring Cloud:提供了多种组件来实现服务的配置、路由、监控、熔断等,能够通过监听
ContextClosedEvent
事件来实现优雅下线的逻辑。 - Docker:能够通过
docker stop
或docker kill
命令来进行容器,前者会发送SIGTERM
信号给容器的 PID1 过程,后者会发送SIGKILL
信号。如果程序能响应SIGTERM
信号,就能够实现优雅下线的操作。
Spring Cloud 优雅下线的原理
ContextClosedEvent
是 Spring 容器在敞开时公布的一个事件,能够通过实现 ApplicationListener 接口来监听这个事件,并在 onApplicationEvent
办法中执行一些自定义的逻辑。
对于 Spring Cloud 中的微服务来说,当收到 ContextClosedEvent
事件时,能够做以下几件事件:
- 从注册核心登记以后服务,这样就不会再有新的申请进入。
- 回绝或者提早新的申请,这样就能够保障正在解决的申请不会被中断。
- 期待一段时间,让旧的申请处理完毕,或者超时。
- 敞开服务,开释资源。
这样就能够实现优雅下线的逻辑,防止因为服务的变更而造成流量的中断或谬误。
Spring boot 优雅下线的 demo
在旧版本外面,咱们须要实现 TomcatConnectorCustomizer
和 ApplicationListener<ContextClosedEvent>
接口,而后就能够在 customize
办法中获取到 Tomcat 的 Connector
对象,并在 onApplicationEvent
办法中监听到 Spring 容器的敞开事件。
在 2.3 及当前版本,咱们只须要在 application.yml
中增加几个配置就能启用优雅关停了。
# 开启优雅进行 Web 容器,默认为 IMMEDIATE:立刻进行
server:
shutdown: graceful
# 最大等待时间
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
这个开关的具体实现逻辑在咱们在 GracefulShutdown
里。
而后咱们须要增加 actuator 依赖,而后在配置中裸露 actuator
的shutdown
接口。
# 裸露 shutdown 接口
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: shutdown
这个时候,咱们调用 http://localhost:8080/actuator/shutdown 就能够执行优雅关停了,它会返回如下内容:
{"message": "Shutting down, bye..."}
优缺点
我感觉这种办法有以下的长处和毛病:
长处:
- 简略易用,只须要简略的配置,就能够实现优雅下线的逻辑。
- 实用于 Tomcat 作为内嵌容器的 Spring Boot 利用,不须要额定的配置或依赖。
- 能够保障正在解决的申请不会被中断,而新的申请不会进入,防止了服务的变更造成流量的中断或谬误。
毛病:
- 只实用于 Tomcat 作为内嵌容器的 Spring Boot 利用,如果应用其余的容器或部署形式,可能须要另外的实现。
- 须要期待肯定的工夫,让正在解决的申请实现或超时,这可能会影响服务的进行速度和资源的开释。
- 如果正在解决的申请过多或过慢,可能会导致线程池无奈优雅地敞开,或者超过零碎的终止工夫,造成强制敞开。
Docker 优雅下线的 demo
这里用一个简略的 JS 利用来演示 docker 实现无损下线的过程。
首先,咱们须要创立一个 Dockerfile 文件,用于定义一个简略的利用容器,代码如下:
# 基于 node:14-alpine 镜像
FROM node:14-alpine
# 设置工作目录
WORKDIR /app
# 复制 package.json 和 package-lock.json 文件
COPY package*.json ./
# 装置依赖
RUN npm install
# 复制源代码
COPY . .
# 裸露 3000 端口
EXPOSE 3000
# 启动利用
CMD ["node", "app.js"]
而后,咱们须要创立一个 app.js 文件,用于定义一个简略的 web 利用,代码如下:
// 引入 express 模块
const express = require('express');
// 创立 express 利用
const app = express();
// 定义一个响应 /hello 门路的接口
app.get('/hello', (req, res) => {
// 返回 "Hello, I am app" 字符串
res.send('Hello, I am app');
});
// 监听 3000 端口
app.listen(3000, () => {
// 打印日志信息
console.log('App listening on port 3000');
});
接下来,咱们须要在终端中执行以下命令,来构建和运行咱们的利用容器,并查看页面后果。
# 构建镜像,命名为 app:1.0.0
docker build -t app:1.0.0 .
# 运行容器,命名为 app-1,映射端口为 3001:3000
docker run -d --name app-1 -p 3001:3000 app:1.0.0
# 查看容器运行状态和端口映射信息
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a8a9f9f7c6c4 app:1.0.0 "docker-entrypoint.s…" 10 seconds ago Up 9 seconds 0.0.0.0:3001->3000/tcp app-1
# 在浏览器中拜访 <http://localhost:3001/hello>,能够看到返回 "Hello, I am app" 字符串
这个时候假如咱们要公布一个新版本的利用,咱们须要批改 app.js 文件中的代码,把返回的字符串批改为“Hello, I am app v2”。
而后,咱们须要在终端中执行以下命令,来构建和运行新版本的利用容器:
# 构建镜像,命名为 app:2.0.0
docker build -t app:2.0.0 .
# 运行容器,命名为 app-2,映射端口为 3002:3000
docker run -d --name app-2 -p 3002:3000 app:2.0.0
# 查看容器运行状态和端口映射信息
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b7b8f8f7c6c4 app:2.0.0 "docker-entrypoint.s…" 10 seconds ago Up 9 seconds 0.0.0.0:3002->3000/tcp app-2
a8a9f9f7c6c4 app:1.0.0 "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 0.0.0.0:3001->3000/tcp app-1
# 在浏览器中拜访 <http://localhost:3002/hello>,能够看到返回 "Hello, I am app v2" 字符串
接下来,须要优雅公开线旧版本的利用容器,让它实现正在解决的申请,而后进行接管新的申请,最初退出过程。
# 向旧版本的利用容器发送 SIGTERM 信号,让它优雅地终止
docker stop app-1
# 查看容器运行状态和端口映射信息
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b7b8f8f7c6c4 app:2.0.0 "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 0.0.0.0:3002->3000/tcp app-2
# 在浏览器中拜访 <http://localhost:3001/hello>,能够看到无奈连贯到服务器的谬误
这样,咱们就实现了通过 Docker 来做优雅下线的过程,保障正在解决的申请不会被中断,而新的申请会被路由到新版本的利用上。
这里次要用到了 docker stop
命令。docker stop
命令会向容器发送 SIGTERM
信号,这是一种优雅终止过程的形式,它会给指标过程一个清理善后工作的机会,比方实现正在解决的申请,开释资源等。如果指标过程在肯定工夫内(默认为 10 秒)没有退出,docker stop
命令会再发送 SIGKILL
信号,强制终止过程。
所以,应用 docker stop
命令能实现优雅下线的前提是,容器中的利用可能正确地响应 SIGTERM
信号,并在收到该信号后执行清理工作。如果容器中的利用疏忽了 SIGTERM
信号,或者在清理工作过程中出现异常,那么 docker stop
命令就无奈实现优雅下线的成果。
让容器中的利用正确地响应 SIGTERM
信号的办法,次要取决于容器中的 1 号过程是什么,以及它如何解决信号。如果容器中的 1 号过程就是利用自身,那么利用只须要在代码中为 SIGTERM 信号注册一个处理函数,用于执行清理工作和退出过程。例如,在 Node.js 中,能够这样写:
// 定义一个解决 SIGTERM 信号的函数
function termHandler() {
// 执行清理工作
console.log('Cleaning up...');
// 退出过程
process.exit(0);
}
// 为 SIGTERM 信号注册处理函数
process.on('SIGTERM', termHandler);
总结
优雅高低线的价值
在微服务实际中,实现优雅高低线能给咱们带来以下益处:
- 最小化服务中断:通过优雅高低线,能够最小化服务中断的工夫和影响范畴,从而确保服务的可用性和稳定性。
- 防止数据失落:优雅下线能够确保正在解决的申请可能实现,防止数据失落和申请失败。
- 进步用户体验:优雅高低线能够确保用户在应用服务时不会遇到任何中断或谬误,从而进步用户体验和满意度。
- 简化部署流程:通过应用自动化工具和流程,能够简化部署流程,缩小人工干预和谬误,进步部署效率和品质。
- 进步可维护性:通过应用监控和日志记录工具,能够及时发现和解决问题,进步服务的可维护性和可靠性。
这些益处能够帮忙企业进步服务质量和效率,晋升用户满意度和竞争力。
优雅高低线的挑战
但同时,优雅高低线也面临一些挑战:
- 复杂性减少:微服务架构通常由多个服务组成,每个服务都有本人的生命周期和依赖关系,因而优雅高低线须要思考多个服务之间的交互和协调,减少了零碎的复杂性。
- 部署流程简单:优雅高低线须要应用自动化工具和流程,这须要投入大量的工夫和资源来构建和保护,减少了部署流程的复杂性。
- 数据一致性问题:优雅下线须要确保正在解决的申请可能实现,但这可能会导致数据一致性问题,须要采取措施来解决这个问题。
- 人员技能要求高:微服务架构须要具备更高的技术水平和技能,须要领有更多的开发和运维教训,这对企业的人员要求较高。
综上所述,企业须要认真思考这些挑战,并采取相应的措施来解决这些问题,以确保在微服务实际中更好的落地优雅高低线。