导语
本文介绍了微服务优雅高低线的实际办法及原理,包含实用于 Spring 利用的优雅高低线逻辑和服务预热,以及应用 Docker 实现无损下线的 Demo。同时,本文还总结了优雅高低线的价值和挑战。
作者简介
颜松柏
腾讯云微服务架构师
领有超过 10 年的 IT 从业教训,精通软件架构设计,微服务架构,云架构设计等多个畛域,在泛互、金融、教育、出行等多个行业领有丰盛的微服务架构教训。
前言
微服务优雅高低线的原理是指在微服务的公布过程中,保障服务的稳定性和可用性,防止因为服务的变更而造成流量的中断或谬误。
微服务优雅高低线的原理能够从三个角度来思考:
- 服务端的优雅上线,即在服务启动后,期待服务齐全就绪后再对外提供服务,或者有一个服务预热的过程。
- 服务端的无损下线,即在服务进行前,先从注册核心登记,回绝新的申请,期待旧的申请处理完毕后再下线服务。
- 客户端的容灾策略,即在调用服务时,通过负载平衡、重试、黑名单等机制,抉择衰弱的服务实例,防止调用不可用的服务实例。
微服务优雅高低线能够进步微服务的稳定性和可靠性,缩小公布过程中的危险和损失。
优雅上线
优雅上线,也叫无损上线,或者提早公布,或者提早裸露,或者服务预热。
优雅上线的目标是为了进步公布的稳定性和可靠性,防止因为利用的变更而造成流量的中断或谬误。
优雅上线的办法
优雅上线的办法有以下几种:
- 提早公布 :即提早裸露应用服务,比方利用须要一些初始化操作后能力对外提供服务,如初始化缓存,数据库连接池等相干资源就位,能够通过配置或代码来实现提早裸露。
- QoS 命令 :即通过命令行或 HTTP 申请来管制应用服务的上线和下线,比方在利用启动时不向注册核心注册服务,而是在服务健康检查完之后再手动注册服务。
- 服务注册与发现 :即通过注册核心来治理应用服务的状态和路由信息,比方在利用启动时向注册核心注册服务,并监听服务状态变动事件,在利用进行时向注册核心登记服务,并告诉其余服务更新路由信息。
- 灰度公布 :即通过分流策略来管制应用服务的流量调配,比方在公布新版本的利用时,先将局部流量导入到新版本的利用上,察看其运行状况,如果没有问题再逐渐减少流量比例,直到全副切换到新版本的利用上。
下面的办法核心思想都是一个,就是等服务做好了筹备再把申请放行过来。
优雅上线的实现
大部分优雅上线都是通过注册核心和服务治理能力来实现的。
对于初始化过流程较长的利用,因为注册通常与利用初始化过程同步进行,因而可能呈现利用还未齐全初始化就曾经被注册到注册核心供内部消费者调用,此时间接调用可能会导致申请报错。
所以,通过服务注册与发现来做优雅上线的基本思路是:
- 在利用启动时,提供一个健康检查接口,用于反馈服务的状态和可用性。
-
利用启动后,能够采纳下列办法来使新的申请临时不进入新版的服务实例。
- 临时不向注册核心注册服务。
- 隔离服务,有些注册核心反对隔离服务实例,比方北极星。
- 将权重配置为 0。
- 将服务实例的 Enable 改为 False。
- 让健康检查接口返回不衰弱的状态。
- 在新版本的利用实例实现初始化操作后,确保了可用性后,再对应的将上述的办法勾销,这样就能够让新的申请被路由到新版本的利用实例上。
- 如果须要预热,就让流量进入新版本的利用实例时按比例的一点点减少。
这样,就能够实现优雅上线的过程,保障申请进来的时候,不会因为新版本的利用实例没有筹备好而导致申请失败。
优雅上线的北极星代码 Demo
咱们以 Spring Cloud 和 北极星 为例,讲一下如何通过服务注册与发现来做优雅上线的过程。
首先,咱们须要创立一个 Spring Cloud 我的项目,并增加北极星的依赖。
而后,咱们须要在 application.properties 文件中配置北极星的相干信息,如注册核心地址,服务名,分组名等,例如:
spring:
application:
name: ${application.name}
cloud:
polaris:
address: grpc://${批改为第一步部署的 Polaris 服务地址}:8091
namespace: default
而后,咱们须要创立一个 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;
}
}
这样,咱们就实现了一个简略的服务提供者利用,并且能够通过北极星来实现服务注册与发现。
接下来,咱们须要创立一个服务消费者利用,并且也增加北极星的依赖和配置信息。
而后,应用 RestTemplate 来调用服务提供者的接口,例如:
@SpringBootApplication
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 来调用服务提供者的接口。
这样,咱们就实现了一个简略的服务消费者利用,并且能够通过北极星来实现服务注册与发现。
接下来,咱们就能够通过以下步骤来实现优雅上线的过程:
- 在公布新版本的服务提供者利用时,先启动新版本的利用实例,然而不向注册核心注册服务,或者让健康检查接口返回不衰弱的状态,这样就不会有新的申请进入新版本的利用实例。这能够通过配置或代码来实现,例如:
# 不向注册核心注册服务
spring.cloud.polaris.discovery.register=false
// 让健康检查接口返回不衰弱的状态
this.isHealthy = false;
- 在新版本的利用实例实现初始化操作后,再向注册核心注册服务,或者让健康检查接口返回衰弱的状态,这样就能够让新的申请被路由到新版本的利用实例上。这能够通过配置或代码来实现,例如:
# 向注册核心注册服务
spring.cloud.polaris.discovery.register=true
// 让健康检查接口返回衰弱的状态
this.isHealthy = true;
这样,就能够实现优雅上线的过程,保障正在解决的申请不会被中断,而新的申请会被路由到新版本的利用上。
不过,如果对优雅上线的极致要求不高,北极星自身就是反对优雅上线的,毋庸做额定的操作。因为北极星的逻辑是,当 Spring 的 Bean 全副加载实现后,Controller 能拜访后才会去注册服务。所以,在绝大多数的场景下,它曾经满足了优雅上线的要求。
服务预热
服务预热是指在服务上线之前,先让服务处于一个运行状态,让其加载必要的资源、建设连贯等,以便在服务上线后可能疾速响应申请。如下图所示。
在流量较大状况下,刚启动的服务间接解决大量申请可能因为利用外部资源初始化不彻底从而呈现申请阻塞、报错等问题。此时通过服务预热,在服务刚启动阶段通过小流量帮忙服务在解决大量申请前实现初始化,能够帮忙发现服务上线后可能存在的问题,例如资源有余、连接数过多等,从而及时进行调整和优化,确保服务的稳定性和可靠性。
云原生 API 网关实现服务预热
云原生 API 网关是腾讯云基于开源微服务网关推出的一款高性能高可用的云上网关托管产品。咱们能够通过简略的几个配置就能实现服务预热。
首先咱们在网关新建后端服务的时候,能够关上下图中的慢启动开关。同时能够设置慢启动的工夫。
开启后,服务端有新的服务节点上线后,会在设置的慢启动的工夫内,将新节点的权重从 1 逐渐减少到目标值。这个新节点的流量会缓缓减少。
如果有多个新增节点,那所有新增的节点都会慢启动。
针对后端起源是 K8S 服务、注册核心、IP 列表的服务都能够实现慢启动,也就是服务预热。
优雅下线
无损下线、优雅下线都是同一个意思。都是为了防止服务下线的时候因为申请没有解决完导致申请失败的状况。
优雅下线的办法
无损下线的一些罕用的工具或框架有:
- 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);
北极星的优雅下线
北极星的心跳默认是 5 秒维持一次,客户端的缓存默认是 2 秒刷新一次。实践上,在极致状况下,服务下线会有 2 秒的不可用工夫。但客户端都有重试机制,且大部分客户端的超时工夫都是大于 2 秒的。因而大部分状况下,服务在北极星下线是不会造成业务感知的。
北极星的优雅下线有多种形式。其中下面的 Spring Boot 与 Docker 的形式是其中两种。
另外一种是能够在服务下线的时候,在 PreStop 的时候去做服务隔离与反注册。
这样的隔离操作能够手动做,也能够通过脚本来主动做。
如上图,被隔离的实例将不会被主调方发现,这样就不会有新的需要进来,在解决实现现有的申请后,就能够执行下线操作了。
总结
优雅高低线的价值
在微服务实际中,实现优雅高低线能给咱们带来以下益处:
- 最小化服务中断:通过优雅高低线,能够最小化服务中断的工夫和影响范畴,从而确保服务的可用性和稳定性。
- 防止数据失落:优雅下线能够确保正在解决的申请可能实现,防止数据失落和申请失败。
- 进步用户体验:优雅高低线能够确保用户在应用服务时不会遇到任何中断或谬误,从而进步用户体验和满意度。
- 简化部署流程:通过应用自动化工具和流程,能够简化部署流程,缩小人工干预和谬误,进步部署效率和品质。
- 进步可维护性:通过应用监控和日志记录工具,能够及时发现和解决问题,进步服务的可维护性和可靠性。
这些益处能够帮忙企业进步服务质量和效率,晋升用户满意度和竞争力。
优雅高低线的挑战
但同时,优雅高低线也面临一些挑战:
- 复杂性减少:微服务架构通常由多个服务组成,每个服务都有本人的生命周期和依赖关系,因而优雅高低线须要思考多个服务之间的交互和协调,减少了零碎的复杂性。
- 部署流程简单:优雅高低线须要应用自动化工具和流程,这须要投入大量的工夫和资源来构建和保护,减少了部署流程的复杂性。
- 数据一致性问题:优雅下线须要确保正在解决的申请可能实现,但这可能会导致数据一致性问题,须要采取措施来解决这个问题。
- 人员技能要求高:微服务架构须要具备更高的技术水平和技能,须要领有更多的开发和运维教训,这对企业的人员要求较高。
综上所述,企业须要认真思考这些挑战,并采取相应的措施来解决这些问题,以确保在微服务实际中更好的落地优雅高低线。