本文介绍了微服务高雅上下线的实践办法,包含适用于 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);
总结
高雅上下线的价值
在微服务实践中,完结高雅上下线能给咱们带来以下优点:
- 最小化服务中止:经过高雅上下线,能够最小化服务中止的时刻和影响规模,然后保证服务的可用性和稳定性。
- 防止数据丢失:高雅下线能够保证正在处理的恳求能够完结,防止数据丢失和恳求失利。
- 进步用户体会:高雅上下线能够保证用户在运用服务时不会遇到任何中止或过错,然后进步用户体会和满意度。
- 简化布置流程:经过运用自动化东西和流程,能够简化布置流程,减少人工干预和过错,进步布置功率和质量。
- 进步可保护性:经过运用监控和日志记载东西,能够及时发现和处理问题,进步服务的可保护性和可靠性。
这些优点能够协助企业进步服务质量和功率,提升用户满意度和竞争力。
高雅上下线的挑战
但一起,高雅上下线也面对一些挑战:
- 杂乱性增加:微服务架构通常由多个服务组成,每个服务都有自己的生命周期和依靠关系,因而高雅上下线需求考虑多个服务之间的交互和协调,增加了体系的杂乱性。
- 布置流程杂乱:高雅上下线需求运用自动化东西和流程,这需求投入很多的时刻和资源来构建和保护,增加了布置流程的杂乱性。
- 数据一致性问题:高雅下线需求保证正在处理的恳求能够完结,但这或许会导致数据一致性问题,需求采取办法来处理这个问题。
- 人员技术要求高:微服务架构需求具有更高的技术水平和技术,需求具有更多的开发和运维经历,这对企业的人员要求较高。
综上所述,企业需求认真考虑这些挑战,并采取相应的办法来处理这些问题,以保证在微服务实践中更好的落地高雅上下线。