原文链接:https://dzone.com/articles/spring-boot-autoscaler

作者:Piotr Mińkowski

译者:helloworldtang

自动伸缩是每个人都想要的,尤其是在微服务领域。让我们看看如何在基于Spring Boot的应用程序中实现。

我们决定使用 Kubernetes、 PivotalCloudFoundry或 HashiCorp's Nomad等工具的一个更重要的原因是为了让系统可以自动伸缩。当然,这些工具也提供了许多其他有用的功能,在这里,我们只是用它们来实现系统的自动伸缩。乍一看,这似乎很困难,但是,如果我们使用 SpringBoot来构建应用程序,并使用 Jenkins来实现 CI,那么就用不了太多工作。

今天,我将向您展示如何使用以下框架/工具实现这样的解决方案:

  • Spring Boot

  • Spring Boot Actuator

  • Spring Cloud Netflix Eureka

  • Jenkins CI

它是如何工作的

每一个包含 SpringBootActuator库的 SpringBoot应用程序都可以在 /actuator/metrics端点下公开 metric。许多有价值的 metric都可以提供应用程序运行状态的详细信息。在讨论自动伸缩时,其中一些 metric可能特别重要: JVM、CPU metric、正在运行的线程数和HTTP请求数。有专门的 Jenkins流水线通过按一定频率轮询 /actuator/metrics 端点来获取应用程序的指标。如果监控的任何 metric【指标】低于或高于目标范围,则它会启动新实例或使用另一个 Actuator端点 /actuator/shutdown来关闭一些正在运行的实例。在此之前,我们需要知道当前有那些实践在提供服务,只有这样我们才能在需要的时候关闭空闲的实例或启动新的新例。 在讨论了系统架构之后,我们就可以继续开发了。这个应用程序需要满足以下要求:它必须有公开的可以优雅地关闭应用程序和用来获取应用程序运行状态 metric【指标】的端点,它需要在启动完成的同时就完成在Eureka的注册,在关闭时取消注册,最后,它还应该能够从空闲端口池中随机获取一个可用的端口。感谢 SpringBoot,只需要约五分钟,我们可以轻松地实现所有这些机制。

动态端口分配

由于可以在一台机器上运行多个应用程序实例,所以我们必须保证端口号不冲突。幸运的是, SpringBoot为应用程序提供了这样的机制。我们只需要将 application.yml中的 server.port属性设置为 0。因为我们的应用程序会在 Eureka中注册,并且发送唯一的标识 instanceId,默认情况下这个唯一标识是将字段 spring.cloud.client.hostnamespring.application.name和 server.port拼接而成的。

示例应用程序的当前配置如下所示。 可以看到,我通过将端口号替换为随机生成的数字来改变了生成 instanceId字段值的模板。

  1. spring:

  2.  application:

  3.    name: example-service

  4. server:

  5.  port: ${PORT:0}

  6. eureka:

  7.  instance:

  8.    instanceId: ${spring.cloud.client.hostname}:${spring.application.name}:${random.int[1,999999]}

启用 Actuator的 Metric

为了启用 SpringBootActuator,我们需要将下面的依赖添加到 pom.xml

  1. <dependency>

  2.    <groupId>org.springframework.boot</groupId>

  3.    <artifactId>spring-boot-starter-actuator</artifactId>

  4. </dependency>

我们还必须通过HTTP API将属性 management.endpoints.web.exposure.include设置为 '*'来暴露 Actuator的端点。现在,所有可用的指标名称列表都可以在 /actuator/metrics端点中找到,每个指标的详细信息可以通过 /actuator/metrics/{metricName}端点查看。

优雅地停止应用程序

除了查看 metric端点外, SpringBootActuator还提供了停止应用程序的端点。然而,与其他端点不同的是,缺省情况下,此端点是不可用的。我们必须把 management.endpoint.shutdown.enabled设为 true。在那之后,我们就可以通过发送一个 POST请求到 /actuator/shutdown端点来停止应用程序了。

这种停止应用程序的方法保证了服务在停止之前从 Eureka服务器注销。

启用 Eureka自动发现

Eureka是最受欢迎的发现服务器,特别是使用 SpringCloud来构建微服务的架构。所以,如果你已经有了微服务,并且想要为他们提供自动伸缩机制,那么 Eureka将是一个自然的选择。它包含每个应用程序注册实例的IP地址和端口号。为了启用 Eureka客户端,您只需要将下面的依赖项添加到 pom.xml中。

  1. <dependency>

  2.    <groupId>org.springframework.cloud</groupId>

  3.    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>

  4. </dependency>

正如之前提到的,我们还必须保证通过客户端应用程序发送到 Eureka服务器的 instanceId的唯一性。在“动态端口分配”中已经描述了它。

下一步需要创建一个包含内嵌 Eureka服务器的应用程序。为了实现这个功能,首先我们需要在 pom.xml中添加下面这个依赖:

  1. <dependency>

  2.    <groupId>org.springframework.cloud</groupId>

  3.    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>

  4. </dependency>

这个 main类需要添加 @EnableEurekaServer注解。

  1. @SpringBootApplication

  2. @EnableEurekaServer

  3. public class DiscoveryApp {

  4.    public static void main(String[] args) {

  5.        new SpringApplicationBuilder(DiscoveryApp.class).run(args);

  6.    }

  7. }

默认情况下,客户端应用程序尝试使用 8761端口连接 Eureka服务器。我们只需要单独的、独立的 Eureka节点,因此我们将禁用注册,并尝试从另一个 Eureka服务器实例中获取服务列表。

  1. spring:

  2.  application:

  3.    name: discovery-service

  4. server:

  5.  port: ${PORT:8761}

  6. eureka:

  7.  instance:

  8.    hostname: localhost

  9.  client:

  10.    registerWithEureka: false

  11.    fetchRegistry: false

  12.    serviceUrl:

  13.      defaultZone: http://localhost:8761/eureka/

我们将使用 Docker容器来测试上面的自动伸缩系统,因此需要使用 Eureka服务器来准备和构建 image。 Dockerfile和 image的定义如下所示。 我们可以使用命令 docker build-t piomin/discovery-server:2.0来进行构建。

  1. FROM openjdk:8-jre-alpine

  2. ENV APP_FILE discovery-service-1.0-SNAPSHOT.jar

  3. ENV APP_HOME /usr/apps

  4. EXPOSE 8761

  5. COPY target/$APP_FILE $APP_HOME/

  6. WORKDIR $APP_HOME

  7. ENTRYPOINT ["sh", "-c"]

  8. CMD ["exec java -jar $APP_FILE"]

为弹性伸缩构建一个 Jenkins流水线

第一步是准备 Jenkins流水线,负责自动伸缩。我们将创建 Jenkins声明式流水线,它每分钟运行一次。可以使用 triggers指令配置执行周期,它定义了自动化触发流水线的方法。我们的流水线将与 Eureka服务器和每个使用 SpringBootActuator的微服务中公开的 metric端点进行通信。 测试服务的名称是 EXAMPLE-SERVICE,它和定义在 application.yml文件 spring.application.name的属性值(大写字母)相同。被监控的 metric是运行在Tomcat容器中的HTTP listener线程数。这些线程负责处理客户端的HTTP请求。

  1. pipeline {

  2.    agent any

  3.    triggers {

  4.        cron('* * * * *')

  5.    }

  6.    environment {

  7.        SERVICE_NAME = "EXAMPLE-SERVICE"

  8.        METRICS_ENDPOINT = "/actuator/metrics/tomcat.threads.busy?tag=name:http-nio-auto-1"

  9.        SHUTDOWN_ENDPOINT = "/actuator/shutdown"

  10.    }

  11.    stages { ... }

  12. }

使用 Eureka整合 Jenkins流水线

流水线的第一个阶段负责获取在 discovery服务器上注册的服务列表。 Eureka发现了几个HTTP API端点。其中一个是 GET/eureka/apps/{serviceName},它返回一个给定服务名称的所有活动实例列表。我们正在保存运行实例的数量和每个实例 metric端点的URL。这些值将在流水线的下一个阶段中被访问。 下面的流水线片段可以用来获取活动应用程序实例列表。 stage名称是 Calculate。我们使用HTTP请求插件 来发起HTTP连接。

  1. stage('Calculate') {

  2. steps {

  3.  script {

  4.   def response = httpRequest "http://192.168.99.100:8761/eureka/apps/${env.SERVICE_NAME}"

  5.   def app = printXml(response.content)

  6.   def index = 0

  7.   env["INSTANCE_COUNT"] = app.instance.size()

  8.   app.instance.each {

  9.    if (it.status == 'UP') {

  10.     def address = "http://${it.ipAddr}:${it.port}"

  11.     env["INSTANCE_${index++}"] = address

  12.    }

  13.   }

  14.  }

  15. }

  16. }

  17. @NonCPS

  18. def printXml(String text) {

  19. return new XmlSlurper(false, false).parseText(text)

  20. }

下面是 Eureka API对我们的微服务的示例响应。响应 content-type是 XML。 

使用 SpringBootActuator整合 Jenkins流水线

SpringBootActuator使用 metric来公开端点,这使得我们可以通过名称和选择性地使用标签找到 metric。在下面可见的流水线片段中,我试图找到 metric低于或高于阈值的实例。如果有这样的实例,我们就停止循环,以便进入下一个阶段,它执行向下或向上的伸缩。应用程序的IP地址是从带有 INSTANCE_前缀的流水线环境变量获取的,这是在前一阶段中被保存了下来的。

  1. stage('Metrics') {

  2. steps {

  3. script {

  4. def count = env.INSTANCE_COUNT

  5. for(def i=0;i 100)

  6. return "UP"

  7. else if (value.toInteger() < 20)

  8. return "DOWN"

  9. else

  10. return "NONE"

  11. }

关闭应用程序实例

在流水线的最后一个阶段,我们将关闭运行的实例,或者根据在前一阶段保存的结果启动新的实例。通过调用 SpringBootActuator端点可以很容易执行停止操作。在接下来的流水线片段中,首先选择了 Eureka实例。然后我们将发送 POST请求到那个ip地址。 如果需要扩展应用程序,我们将调用另一个流水线,它负责构建 fat JAR并让这个应用程序在机器上跑起来。

  1. stage('Scaling') {

  2. steps {

  3.  script {

  4.   if (env.SCALE_TYPE == 'DOWN') {

  5.    def ip = env["INSTANCE_0"] + env.SHUTDOWN_ENDPOINT

  6.    httpRequest url: ip, contentType: 'APPLICATION_JSON', httpMode: 'POST'

  7.   } else if (env.SCALE_TYPE == 'UP') {

  8.    build job: 'spring-boot-run-pipeline'

  9.   }

  10.   currentBuild.description = env.SCALE_TYPE

  11.  }

  12. }

  13. }

下面是 spring-boot-run-pipeline流水线的完整定义,它负责启动应用程序的新实例。它先从 git仓库中拉取源代码,然后使用 Maven命令编译并构建二进制的jar文件,最后通过在 java-jar命令中添加 Eureka服务器地址来运行应用程序。

  1. pipeline {

  2.    agent any

  3.    tools {

  4.        maven 'M3'

  5.    }

  6.    stages {

  7.        stage('Checkout') {

  8.            steps {

  9.                git url: 'https://github.com/piomin/sample-spring-boot-autoscaler.git', credentialsId: 'github-piomin', branch: 'master'

  10.            }

  11.        }

  12.        stage('Build') {

  13.            steps {

  14.                dir('example-service') {

  15.                    sh 'mvn clean package'

  16.                }

  17.            }

  18.        }

  19.        stage('Run') {

  20.            steps {

  21.                dir('example-service') {

  22.                    sh 'nohup java -jar -DEUREKA_URL=http://192.168.99.100:8761/eureka target/example-service-1.0-SNAPSHOT.jar 1>/dev/null 2>logs/runlog &'

  23.                }

  24.            }

  25.        }

  26.    }

  27. }

扩展到多个机器

在前几节中讨论的算法只适用于在单个机器上启动的微服务。如果希望将它扩展到更多的机器上,我们将不得不修改我们的架构,如下所示。每台机器都有 Jenkins代理运行并与 Jenkins master通信。如果想在选定的机器上启动一个微服务的新实例,我们就必须使用运行在该机器上的代理来运行流水线。此代理仅负责从源代码构建应用程序并将其启动到目标机器上。这个实例的关闭仍然是通过调用HTTP端点来完成。 

你可以在我的文章在Docker容器上的运行Jenkins节点中找到更多关于运行 Jenkins代理的信息,并通过 JNLP协议将它们与 Jenkins master联系起来。假设我们已经成功地在目标机器上启动了一些代理,我们需要对流水线进行参数化,以便能够动态地选择代理(以及目标机器)。 当扩容应用程序时,我们必须将代理标签传递给下游流水线。

  1. build job:'spring-boot-run-pipeline', parameters:[string(name: 'agent', value:"slave-1")]

调用流水线具体由那个标签下的代理运行,是由" ${params.agent}"决定的。

  1. pipeline {

  2.    agent {

  3.        label "${params.agent}"

  4.    }

  5.    stages { ... }

  6. }

如果有一个以上的代理连接到主节点,我们就可以将它们的地址映射到标签中。由于这一点,我们能够将从 Eureka服务器获取的微服务实例的IP地址映射到与 Jenkins代理的目标机器上。

  1. pipeline {

  2.    agent any

  3.    triggers {

  4.        cron('* * * * *')

  5.    }

  6.    environment {

  7.        SERVICE_NAME = "EXAMPLE-SERVICE"

  8.        METRICS_ENDPOINT = "/actuator/metrics/tomcat.threads.busy?tag=name:http-nio-auto-1"

  9.        SHUTDOWN_ENDPOINT = "/actuator/shutdown"

  10.        AGENT_192.168.99.102 = "slave-1"

  11.        AGENT_192.168.99.103 = "slave-2"

  12.    }

  13.    stages { ... }

  14. }

总结

在本文中,我演示了如何使用 SpringBootActuato metric来自动伸缩 SpringBoot应用程序。使用 SpringBoot提供的特性以及 SpringCloudNetflixEureka和 Jenkins,您就可以实现系统的自动伸缩,而无需借助于任何其他第三方工具。本文也假设远程服务器上也是使用 Jenkins代理来启动新的实例,但是您也可以使用 Ansible这样的工具来启动。如果您决定从 Jenkins运行 Ansible脚本,那么将不需要在远程机器上启动 Jenkins代理。示例中用到的代码可以在 GitHub上找到。

译:Spring Boot 自动伸缩的更多相关文章

  1. Springboot 系列(三)Spring Boot 自动配置原理

    注意:本 Spring Boot 系列文章基于 Spring Boot 版本 v2.1.1.RELEASE 进行学习分析,版本不同可能会有细微差别. 前言 关于配置文件可以配置的内容,在 Spring ...

  2. Spring Boot自动配置与Spring 条件化配置

    SpringBoot自动配置 SpringBoot的自动配置是一个运行时(应用程序启动时)的过程,简化开发时间,无需浪费时间讨论具体的Spring配置,只需考虑如何利用SpringBoot的自动配置即 ...

  3. Spring Boot自动配置原理、实战

    Spring Boot自动配置原理 Spring Boot的自动配置注解是@EnableAutoConfiguration, 从上面的@Import的类可以找到下面自动加载自动配置的映射. org.s ...

  4. Spring Boot自动配置

    Spring Boot自动配置原理 Spring Boot的自动配置注解是@EnableAutoConfiguration, 从上面的@Import的类可以找到下面自动加载自动配置的映射. org.s ...

  5. Spring boot 自动配置自定义配置文件

    示例如下: 1.   新建 Maven 项目 properties 2.   pom.xml <project xmlns="http://maven.apache.org/POM/4 ...

  6. Spring Boot自动配置原理与实践(一)

    前言 Spring Boot众所周知是为了简化Spring的配置,省去XML的复杂化配置(虽然Spring官方推荐也使用Java配置)采用Java+Annotation方式配置.如下几个问题是我刚开始 ...

  7. Spring Boot 自动装配(二)

    目录 目录 前言 1.起源 2.Spring Boot 自动装配实现 2.1.@EnableAutoConfiguration 实现 2.1.1. 获取默认包扫描路径 2.1.2.获取自动装配的组件 ...

  8. Spring Boot自动配置原理(转)

    第3章 Spring Boot自动配置原理 3.1 SpringBoot的核心组件模块 首先,我们来简单统计一下SpringBoot核心工程的源码java文件数量: 我们cd到spring-boot- ...

  9. Spring Boot自动装配

    前言 一些朋友问我怎么读源码,这篇文章结合我看源码时候一些思路给大家聊聊,我主要从这三个方向出发: 确定目标,这个目标要是一个具体,不要一上来我要看懂Spring,这是不可能的,目标要这么来定,比如看 ...

随机推荐

  1. Day 5-8 自定义元类控制类的实例化行为

    __call__方法: 对象后面加括号,触发执行. 注:构造方法的执行是由创建对象触发的,即:对象 = 类名() :而对于 __call__ 方法的执行是由对象后加括号触发的,即:对象() 或者 类( ...

  2. C# Note28: Dispatcher类

    在项目中也是经常用到: 刚见到它时,你会想:为什么不直接使用System.Windows命名空间下的MessageBox类,何必要这么麻烦?(认真分析看它做了什么,具体原因下面解释) 主要介绍的方法: ...

  3. 剑指offer(17)层次遍历树

    题目: 从上往下打印出二叉树的每个节点,同层节点从左至右打印. public class Solution { ArrayList<Integer> list = new ArrayLis ...

  4. 保存后自动格式化代码(vscode)

    痛点: 写项目的时候, 我们经常会拷贝一些代码, 每当拷贝过来都需要重新调整, 如果可以实现保存自动调整代码, 将会给我们带来很多的便利! 解决: 其实对于vscode来说, 实现这一点很容易. 我们 ...

  5. 在python中定义二维数组

    发表于 http://liamchzh.0fees.net/?p=234&i=1 一次偶然的机会,发现python中list非常有意思. 先看一段代码 [py]array = [0, 0, 0 ...

  6. GIT的前世今生

    在重点介绍GIT的一些操作之前,我们首先来说一说GIT的前世今生,了解整个版本控制的变迁能够让我们知道该如何去选择这些工具,另外通过这些技术的变迁也能够让我们对现在的技术有着更加深入的理解,在正式介绍 ...

  7. Python:matplotlib绘制条形图

    条形图,也称柱状图,看起来像直方图,但完是两码事.条形图根据不同的x值,为每个x指定一个高度y,画一个一定宽度的条形:而直方图是对数据集进行区间划分,为每个区间画条形.     将上面的代码稍微修改一 ...

  8. PLSQL 错误问题:Datebase character set (AL32UTF-8) and Client character set (ZHS16GBK) are different.

    (解决不了,网上用的是Orecal,我用的只是客户端.) 网上找到解决方法 打开注册表(ctr+R,输入regedit),根据报错提示找到注册表位置,但本机是win10 64位系统,根据以上路径找不到 ...

  9. How to create ISO on macOS

    hdiutil makehybrid -iso -joliet -o test1.iso /users/test/test1

  10. How to enable usb on vbox

    Device-->Install Guest Addition Shared Folders Settings-->Advanced-->Shared Clipboard--> ...