总阅读量:次

摘要: 原创出处 http://www.iocoder.cn/Spring-Cloud/Jenkins/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

本文在提供完整代码示例,可见 https://github.com/YunaiV/SpringBoot-Labslabx-16 目录。

原创不易,给点个 Star 嘿,一起冲鸭!

1. 概述

《芋道 Jenkins 极简入门》中,我们已经学习了如何使用 Jenkins 部署一个 Java 项目,通过启动 jar 包的形式。如果没有看过的胖友,需要先把该文的「2. 快速入门」小节给看啦。因为该小节,已经介绍如何使用 Jenkins 部署 Spring Cloud 打成的 jar 包,部署到服务器上。

本文,我们会在《芋道 Jenkins 极简入门》的基础之上,额外学习两块内容:

  • 在本文的「2. 项目打包」小节中,我们会学习如何将 Spring Cloud 项目打包成 jar 包。这样,我们后续就可以使用 Jenkins 进行部署。
  • 在本文的「3. 优雅下线」小节中,我们会分享在 Spring Cloud 项目关闭时,如何优雅下线,避免服务消费者继续请求当前项目。

2. 项目打包

Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以方便的将 Spring Boot 项目打成 jar 包或者 war 包。

友情提示:因为 Spring Cloud 项目也是基于 Spring Boot 的,因此一样可以使用 spring-boot-maven-plugin 插件来打包。

考虑到部署的便利性,我们绝大多数 99.99% 的场景下,我们会选择打成 jar 包。这样,我们就无需在部署项目的服务器上,配置相应的 Tomcat、Jetty 等 Servlet 容器。所以本小节,我们也是将 Spring Boot 项目,打成 jar 包。

下面,我们新建 labx-16-demo-01 项目,作为本小节的示例,最终项目如下图:项目结构

2.1 引入依赖

pom.xml 文件中,引入相关依赖。

<?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>labx-16</artifactId>
<groupId>cn.iocoder.springboot.labs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>labx-16-demo-01</artifactId>
<!-- <1> 配置打成 jar 包 -->
<packaging>jar</packaging>

<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<spring.boot.version>2.2.4.RELEASE</spring.boot.version>
<spring.cloud.version>Hoxton.SR1</spring.cloud.version>
<spring.cloud.alibaba.version>2.2.0.RELEASE</spring.cloud.alibaba.version>
</properties>

<!--
引入 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- 实现对 SpringMVC 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- <2> 实现对 Actuator 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- 引入 Spring Cloud Alibaba Nacos Discovery 相关依赖,将 Nacos 作为注册中心,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>

<build>
<!-- <3> 设置构建的 jar 包名 -->
<finalName>${project.artifactId}</finalName>
<!-- <4> 使用 spring-boot-maven-plugin 插件打包 -->
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>

  • <1> 处,通过 <packaging /> 设置为 jar,表示该项目构建打包成 jar 包。😈 如果胖友想要打成 war 包,可以将此处修改成 war
  • <2> 处,引入 spring-boot-starter-actuator 依赖,实现对 Spring Boot Actuator 的自动化配置。这样,我们就可以使用 Actuator 提供的 health 端点,获得健康检查所需的 HTTP API。
  • <3> 处,设置构建的 jar 包的名字。因为我们在《芋道 Jenkins 极简入门》「2. 快速入门」小节中,提供的 deploy.sh 部署脚本,暂时不支持 jar 包的名字带有版本号,所以这里我们需要进行下设置。
  • <4> 处,引入 spring-boot-maven-plugin 插件,因为我们要使用它构建打包 Spring Cloud 项目。

2.2 配置文件

resources 目录下,创建 5 个配置文件,对应不同的环境。如下:

嘿嘿,现在暂时偷懒,所以每个配置文件的内容基本是一样的。如下:

server:
port: 8079

management:
server:
port: 8078 # 自定义端口,避免 Nginx 暴露出去

endpoint:
web:
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。

spring:
application:
name: demo-service

cloud:
nacos:
# Nacos 作为注册中心的配置项,对应 NacosDiscoveryProperties 配置类
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址

  • server.port 配置项,设置 SpringMVC 的服务器端口。

  • management.server.port 配置项,设置 Spring Boot Actuator 的服务器端口。

  • management.endpoint.web.exposure.include 配置项,设置 Spring Boot Actuator 的开放的端点。

  • spring.application.name 配置项,应用(服务)名。

  • spring.cloud.nacos.discovery.server-addr 配置项,设置 Nacos 注册中心的地址。

    友情提示:如果对使用 Nacos 作为 Spring Cloud 注册中心的胖友,可以后续阅读《芋道 Spring Cloud Alibaba 注册中心 Nacos 入门》文章。

通过多个不同的配置文件,搭配上我们在《芋道 Jenkins 极简入门》「2. 快速入门」小节中,提供的 deploy.sh 部署脚本,设置使用 jar 包中不同的环境配置。

不过可能胖友会吐槽,一个 jar 包包含所有环境的配置,会不会不安全的问题?!确实存在,所以我们一般会做几个事情:

  • 1、不同环境处于不同的网络环境中。例如说,我们测试环境使用一个[阿里云 VPC](专有网络 VPC),正式环境使用另一个阿里云 VPC。这样,在测试环境下,即使使用 application-prod.yaml 配置文件,因为不同网络环境,也是无法连接正式环境的服务。
  • 2、将 application-pre.yamlapplication-prod.yaml 配置文件,放在对应环境中,避免泄露。又或者考虑采用配置中心。
  • 3、敏感配置,进行加密处理。

😈 上述三个方案,可以一起采用,目前我们就是这么干的。

2.3 DemoController

创建 DemoController 类,提供示例 API 接口。代码如下:

@RestController
@RequestMapping("/demo")
public class DemoController {

@GetMapping("/echo")
public String echo() {
return "echo";
}

}

2.4 Application

创建 Application 类,应用启动类。代码如下:

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Component
public class Listener implements ApplicationListener<ApplicationEvent> {

@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextClosedEvent) {
this.sleep(10);
}
}

private void sleep(int seconds) {
try {
Thread.sleep(seconds * 1000L);
} catch (InterruptedException ignore) {
}
}

}

}

这里创建的 Listener 监听器,主要是为了监听 ContextClosedEvent 事件,在 Spring 容器关闭之前,sleep 10 秒。这样,我们就能模拟 Spring Boot 应用,关闭时需要一段时间的过程。😈 毕竟,咱这个示例基本是一个"空"项目,关闭速度非常快,无法完整测试 deploy.sh 部署脚本的关闭流程。

2.5 简单测试

如果我们使用 IDEA,可以直接通过界面,使用 spring-boot-maven-plugin 插件打包。如下图所示:IDEA 打包

当然,我们也可以通过执行 mvn clean package -pl labx-16/labx-16-demo-01 -Dmaven.test.skip=true 命令,构建 labx-16/labx-16-demo-01 子 Maven 模块

友情提示:如果胖友要构建整个项目,可以考虑使用 mvn clean package -Dmaven.test.skip=true 命令。

打包完成后,我们可以使用 java -jar 命令,进行启动。操作命令如下:

# 进入 jar 包所在目录,即指定项目的 target 目录下
$ cd labx-16/labx-16-demo-01/target

# 启动 Java 服务。这里,我们使用 local 环境
$ java -jar labx-16-demo-01.jar --spring.profiles.active=local
... 一堆 Spring Cloud 项目的启动日志。

后续,我们就可以参考《芋道 Jenkins 极简入门》「2.4 Jenkins 部署任务配置」小节,部署 Spring Cloud 项目。

3. 优雅下线

在 Spring Cloud 项目的微服务实例关闭时,需要首先从注册中心设置为下线,避免该服务的消费者继续请求该服务实例,导致请求失败。

虽然说,在 Spring 容器关闭时,Spring Cloud 内置的 AbstractAutoServiceRegistration 会将当前实例从注册中心取消注册,但是并不能保证这个步骤最先执行,在所有 Bean 的销毁之前。同时,虽然当前服务实例已经从注册中心取消注册,但是服务消费者从注册中心中拉取到最新的注册信息,是存在延迟的,只是长短的差别。例如说:

  • Eureka 注册中心贼慢,最长可达 3 分钟:

    【可忽略,服务异常宕机】90s(微服务在Eureka Server上租约到期)+

    30s(Eureka Server服务列表刷新到只读缓存ReadOnlyMap的时间,Eureka Client默认读此缓存)+
    30s(Zuul作为Eureka Client默认每30秒拉取一次服务列表) +
    30s(Ribbon默认动态刷新其ServerList的时间间隔)

    = 180s,即 3分钟

  • Nacos 注册中心贼快,略微有秒级的延迟。

如果我们在服务实例从注册中心取消注册后,立即销毁其它 Spring Bean 的话,会导致当前在处理的请求,又或者服务消费者因为注册中心延迟期间继续打进来的请求,产生处理失败的情况。

面对这样的问题,我们需要实现两件事情:

  • 在任何 Spring Bean 的销毁之前,先将当前服务实例从注册中心取消注册。
  • 取消注册之后,sleep 一段时间,保证服务消费者能够从注册中心拉取到最新实例列表。

具体的解决方案,可以参考《Spring Cloud 微服务如何优雅停机及源码分析》文章。这里,艿艿倾向采用方式四:service-registry 端点

3.1 service-registry 端点

我们先来简单了解下 service-registry 端点,我们可以设置当前服务在注册中心的状态

一起来简单测试一波,直接使用「2. 项目打包」labx-16-demo-01 项目:

1 因为我们已经设置 management.endpoints.web.exposure.include 配置项为 *,所以 service-registry 端点已经开放。如下图所示:配置文件

2 启动 Spring Cloud 项目示例,操作命令如下:

# 进入 jar 包所在目录,即指定项目的 target 目录下
$ cd labx-16/labx-16-demo-01/target

# 启动 Java 服务。这里,我们使用 local 环境
$ java -jar labx-16-demo-01.jar --spring.profiles.active=prod
... 一堆 Spring Cloud 项目的启动日志。

启动完成后,我们可以在 Nacos 控制台看到该服务实例处于上线状态。如下图所示:服务实例 - 上线

3 使用 Postman 请求 service-registry 端点,设置服务实例为下线。如下图所示:Postman 请求 `service-registry 端点

请完成后,我们可以在 Nacos 控制台看到该服务实例处于下线状态。如下图所示:服务实例 - 下线

3.2 集成到部署脚本

复制出新的 depoly.sh 脚本,将 service-registry 端点集成进来。最终如下图所示:脚本

  • 重点红框部分,记得看下说明哟。

一起来简单测试一波,整体步骤如下:

1 先将部署目录初始化如下图所示:部署目录

2 执行 deploy 脚本,进行部署。打印日志如下:

$ sh deploy.sh 
[backup] 开始备份 labx-16-demo-01 ...
[backup] 备份 labx-16-demo-01 完成
[stop] 开始停止 /work/projects/labx-16-demo-01/labx-16-demo-01
[stop] 从注册中心下线当前实例,并 sleep 20
[stop] /work/projects/labx-16-demo-01/labx-16-demo-01 运行中,开始 kill [43671]
-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .[stop] 停止 $BASE_PATH/$SERVER_NAME 成功
[transfer] 开始转移 labx-16-demo-01.jar
[transfer] 移除 /work/projects/labx-16-demo-01/labx-16-demo-01.jar 完成
[transfer] 从 /work/projects/labx-16-demo-01/build 中获取 labx-16-demo-01.jar 并迁移至 /work/projects/labx-16-demo-01 ....
[transfer] 转移 labx-16-demo-01.jar 完成
[start] 开始启动 /work/projects/labx-16-demo-01/labx-16-demo-01
[start] JAVA_OPS: -Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/work/projects/labx-16-demo-01/heapError
[start] JAVA_AGENT:
[start] PROFILES: prod
[start] 启动 /work/projects/labx-16-demo-01/labx-16-demo-01 完成
[healthCheck] 开始通过 http://127.0.0.1:8078/actuator/health/ 地址,进行健康检查
appending output to nohup.out
-e .-e .-e .-e .[healthCheck] 健康检查通过
2020-03-30 20:48:02.790 INFO 46564 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-03-30 20:48:02.790 INFO 46564 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.30]
2020-03-30 20:48:02.793 INFO 46564 --- [ main] o.a.c.c.C.[Tomcat-1].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-03-30 20:48:02.793 INFO 46564 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 84 ms
2020-03-30 20:48:02.804 INFO 46564 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 18 endpoint(s) beneath base path '/actuator'
2020-03-30 20:48:02.856 INFO 46564 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8078 (http) with context path ''
2020年03月30日 20:48:02.860 INFO 46564 --- [ main] c.i.s.lab16.jenkinsdemo.Application : Started Application in 3.157 seconds (JVM running for 3.525)
2020年03月30日 20:48:03.480 INFO 46564 --- [nio-8078-exec-1] o.a.c.c.C.[Tomcat-1].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020年03月30日 20:48:03.480 INFO 46564 --- [nio-8078-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020年03月30日 20:48:03.484 INFO 46564 --- [nio-8078-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms

后续,胖友可以再额外启动一个该 Spring Cloud 项目的服务实例,并再自己搭建一个 Spring Cloud Gateway 网关,转发请求到该服务的两个实例。同时,不断高频请求网关,不断逐个使用 deploy.sh 脚本重启服务实例,看看是否会出现请求失败的情况!

666. 彩蛋

好像暂时没有什么可写的彩蛋,用就完事了,妥妥的。

如果想要使用 Jenkins 部署 Spring Boot 项目,可以参考《芋道 Spring Boot 持续交付 Jenkins 入门》文章。

AltStyle によって変換されたページ (->オリジナル) /