自動伸縮是每一個人都想要的,尤爲是在微服務領域。讓咱們看看如何在基於Spring Boot的應用程序中實現。java
咱們決定使用 Kubernetes 、 Pivotal Cloud Foundry 或 HashiCorp's Nomad 等工具的一個更重要的緣由是爲了讓系統能夠自動伸縮。固然,這些工具也提供了許多其餘有用的功能,在這裏,咱們只是用它們來實現系統的自動伸縮。乍一看,這彷佛很困難,可是,若是咱們使用 Spring Boot 來構建應用程序,並使用 Jenkins 來實現 CI ,那麼就用不了太多工做。git
今天,我將向您展現如何使用如下框架/工具實現這樣的解決方案:github
它是如何工做的web
每個包含 Spring Boot Actuator 庫的 Spring Boot 應用程序均可以在 /actuator/metrics端點下公開 metric 。許多有價值的 metric 均可以提供應用程序運行狀態的詳細信息。在討論自動伸縮時,其中一些 metric 可能特別重要: JVM 、CPU metric 、正在運行的線程數和HTTP請求數。有專門的 Jenkins 流水線經過按必定頻率輪詢 /actuator/metrics 端點來獲取應用程序的指標。若是監控的任何 metric 【指標】低於或高於目標範圍,則它會啓動新實例或使用另外一個 Actuator 端點 /actuator/shutdown 來關閉一些正在運行的實例。在此以前,咱們須要知道當前有那些實踐在提供服務,只有這樣咱們才能在須要的時候關閉空閒的實例或啓動新的新例。算法
在討論了系統架構以後,咱們就能夠繼續開發了。這個應用程序須要知足如下要求:它必須有公開的能夠優雅地關閉應用程序和用來獲取應用程序運行狀態 metric 【指標】的端點,它須要在啓動完成的同時就完成在Eureka的註冊,在關閉時取消註冊,最後,它還應該可以從空閒端口池中隨機獲取一個可用的端口。感謝 Spring Boot ,只須要約五分鐘,咱們能夠輕鬆地實現全部這些機制。spring
動態端口分配sql
因爲能夠在一臺機器上運行多個應用程序實例,因此咱們必須保證端口號不衝突。幸運的是, Spring Boot 爲應用程序提供了這樣的機制。咱們只須要將 application.yml 中的 server.port屬性設置爲 0 。由於咱們的應用程序會在 Eureka 中註冊,而且發送惟一的標識 instanceId ,默認狀況下這個惟一標識是將字段 spring.cloud.client.hostname , spring.application.name和 server.port 拼接而成的。docker
示例應用程序的當前配置以下所示。tomcat
能夠看到,我經過將端口號替換爲隨機生成的數字來改變了生成 instanceId 字段值的模板。服務器
spring: application: name: example-service server: port: ${PORT:0} eureka: instance: instanceId: ${spring.cloud.client.hostname}:${spring.application.name}:${random.int[1,999999]}
啓用Actuator的Metric
爲了啓用 Spring Boot Actuator ,咱們須要將下面的依賴添加到 pom.xml 。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
咱們還必須經過HTTP API將屬性 management.endpoints.web.exposure.include 設置爲 '*' 來暴露 Actuator 的端點。如今,全部可用的指標名稱列表均可以在 /actuator/metrics 端點中找到,每一個指標的詳細信息能夠經過 /actuator/metrics/{metricName} 端點查看。
優雅地中止應用程序
除了查看 metric 端點外, Spring Boot Actuator 還提供了中止應用程序的端點。然而,與其餘端點不一樣的是,缺省狀況下,此端點是不可用的。咱們必須把 management.endpoint.shutdown.enabled 設爲 true 。在那以後,咱們就能夠經過發送一個 POST 請求到 /actuator/shutdown 端點來中止應用程序了。
這種中止應用程序的方法保證了服務在中止以前從 Eureka 服務器註銷。
啓用Eureka自動發現
Eureka 是最受歡迎的發現服務器,特別是使用 Spring Cloud 來構建微服務的架構。因此,若是你已經有了微服務,而且想要爲他們提供自動伸縮機制,那麼 Eureka 將是一個天然的選擇。它包含每一個應用程序註冊實例的IP地址和端口號。爲了啓用 Eureka 客戶端,您只須要將下面的依賴項添加到 pom.xml 中。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
正如以前提到的,咱們還必須保證經過客戶端應用程序發送到 Eureka 服務器的 instanceId 的惟一性。在「動態端口分配」中已經描述了它。
下一步須要建立一個包含內嵌 Eureka 服務器的應用程序。爲了實現這個功能,首先咱們須要在 pom.xml 中添加下面這個依賴:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
這個 main類 須要添加 @EnableEurekaServer 註解。
@SpringBootApplication @EnableEurekaServer public class DiscoveryApp { public static void main(String[] args) { new SpringApplicationBuilder(DiscoveryApp.class).run(args); } }
默認狀況下,客戶端應用程序嘗試使用 8761 端口鏈接 Eureka 服務器。咱們只須要單獨的、獨立的 Eureka 節點,所以咱們將禁用註冊,並嘗試從另外一個 Eureka 服務器實例中獲取服務列表。
spring: application: name: discovery-service server: port: ${PORT:8761} eureka: instance: hostname: localhost client: registerWithEureka: false fetchRegistry: false serviceUrl: defaultZone: http://localhost:8761/eureka/
咱們將使用 Docker 容器來測試上面的自動伸縮系統,所以須要使用 Eureka 服務器來準備和構建 image 。
Dockerfile 和 image 的定義以下所示。
咱們可使用命令 docker build -t piomin/discovery-server:2.0 來進行構建。
FROM openjdk:8-jre-alpine ENV APP_FILE discovery-service-1.0-SNAPSHOT.jar ENV APP_HOME /usr/apps EXPOSE 8761 COPY target/$APP_FILE $APP_HOME/ WORKDIR $APP_HOME ENTRYPOINT ["sh", "-c"] CMD ["exec java -jar $APP_FILE"]
爲彈性伸縮構建一個Jenkins流水線
第一步是準備 Jenkins 流水線,負責自動伸縮。咱們將建立 Jenkins 聲明式流水線,它每分鐘運行一次。可使用 triggers 指令配置執行週期,它定義了自動化觸發流水線的方法。咱們的流水線將與 Eureka 服務器和每一個使用 Spring Boot Actuator 的微服務中公開的 metric 端點進行通訊。
測試服務的名稱是 EXAMPLE-SERVICE ,它和定義在 application.yml 文件 spring.application.name 的屬性值(大寫字母)相同。被監控的 metric 是運行在Tomcat容器中的HTTP listener線程數。這些線程負責處理客戶端的HTTP請求。
pipeline { agent any triggers { cron('* * * * *') } environment { SERVICE_NAME = "EXAMPLE-SERVICE" METRICS_ENDPOINT = "/actuator/metrics/tomcat.threads.busy?tag=name:http-nio-auto-1" SHUTDOWN_ENDPOINT = "/actuator/shutdown" } stages { ... } }
使用Eureka整合Jenkins流水線
流水線的第一個階段負責獲取在 discovery 服務器上註冊的服務列表。 Eureka 發現了幾個HTTP API端點。其中一個是 GET /eureka/apps/{serviceName} ,它返回一個給定服務名稱的全部活動實例列表。咱們正在保存運行實例的數量和每一個實例 metric 端點的URL。這些值將在流水線的下一個階段中被訪問。
下面的流水線片斷能夠用來獲取活動應用程序實例列表。 stage 名稱是 Calculate 。咱們使用 HTTP請求插件 來發起HTTP鏈接。
stage('Calculate') { steps { script { def response = httpRequest "http://192.168.99.100:8761/eureka/apps/${env.SERVICE_NAME}" def app = printXml(response.content) def index = 0 env["INSTANCE_COUNT"] = app.instance.size() app.instance.each { if (it.status == 'UP') { def address = "http://${it.ipAddr}:${it.port}" env["INSTANCE_${index++}"] = address } } } } } @NonCPS def printXml(String text) { return new XmlSlurper(false, false).parseText(text) }
下面是 Eureka API對咱們的微服務的示例響應。響應 content-type 是 XML 。
使用Spring Boot Actuator整合Jenkins流水線
Spring Boot Actuator 使用 metric 來公開端點,這使得咱們能夠經過名稱和選擇性地使用標籤找到 metric 。在下面可見的流水線片斷中,我試圖找到 metric 低於或高於閾值的實例。若是有這樣的實例,咱們就中止循環,以便進入下一個階段,它執行向下或向上的伸縮。應用程序的IP地址是從帶有 INSTANCE_ 前綴的流水線環境變量獲取的,這是在前一階段中被保存了下來的。
stage('Metrics') { steps { script { def count = env.INSTANCE_COUNT for(def i=0;i 100) return "UP" else if (value.toInteger() < 20) return "DOWN" else return "NONE" }
關閉應用程序實例
在流水線的最後一個階段,咱們將關閉運行的實例,或者根據在前一階段保存的結果啓動新的實例。經過調用 Spring Boot Actuator 端點能夠很容易執行中止操做。在接下來的流水線片斷中,首先選擇了 Eureka 實例。而後咱們將發送 POST 請求到那個ip地址。
若是須要擴展應用程序,咱們將調用另外一個流水線,它負責構建 fat JAR 並讓這個應用程序在機器上跑起來。
stage('Scaling') { steps { script { if (env.SCALE_TYPE == 'DOWN') { def ip = env["INSTANCE_0"] + env.SHUTDOWN_ENDPOINT httpRequest url: ip, contentType: 'APPLICATION_JSON', httpMode: 'POST' } else if (env.SCALE_TYPE == 'UP') { build job: 'spring-boot-run-pipeline' } currentBuild.description = env.SCALE_TYPE } } }
下面是 spring-boot-run-pipeline 流水線的完整定義,它負責啓動應用程序的新實例。它先從 git 倉庫中拉取源代碼,而後使用 Maven 命令編譯並構建二進制的jar文件,最後經過在 java -jar 命令中添加 Eureka 服務器地址來運行應用程序。
pipeline { agent any tools { maven 'M3' } stages { stage('Checkout') { steps { git url: 'https://github.com/piomin/sample-spring-boot-autoscaler.git', credentialsId: 'github-piomin', branch: 'master' } } stage('Build') { steps { dir('example-service') { sh 'mvn clean package' } } } stage('Run') { steps { dir('example-service') { 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 &' } } } } }
擴展到多個機器
在前幾節中討論的算法只適用於在單個機器上啓動的微服務。若是但願將它擴展到更多的機器上,咱們將不得不修改咱們的架構,以下所示。每臺機器都有 Jenkins 代理運行並與 Jenkins master通訊。若是想在選定的機器上啓動一個微服務的新實例,咱們就必須使用運行在該機器上的代理來運行流水線。此代理僅負責從源代碼構建應用程序並將其啓動到目標機器上。這個實例的關閉仍然是經過調用HTTP端點來完成。
假設咱們已經成功地在目標機器上啓動了一些代理,咱們須要對流水線進行參數化,以便可以動態地選擇代理(以及目標機器)。
當擴容應用程序時,咱們必須將代理標籤傳遞給下游流水線。
build job:'spring-boot-run-pipeline', parameters:[string(name: 'agent', value:"slave-1")]
調用 流水線具體由那個標籤下的代理運行,是由" ${params.agent} "決定的。
pipeline { agent { label "${params.agent}" } stages { ... } }
若是有一個以上的代理鏈接到主節點,咱們就能夠將它們的地址映射到標籤中。因爲這一點,咱們可以將從 Eureka 服務器獲取的微服務實例的IP地址映射到與 Jenkins 代理的目標機器上。
pipeline { agent any triggers { cron('* * * * *') } environment { SERVICE_NAME = "EXAMPLE-SERVICE" METRICS_ENDPOINT = "/actuator/metrics/tomcat.threads.busy?tag=name:http-nio-auto-1" SHUTDOWN_ENDPOINT = "/actuator/shutdown" AGENT_192.168.99.102 = "slave-1" AGENT_192.168.99.103 = "slave-2" } stages { ... } }
總結
在本文中,我演示瞭如何使用 Spring Boot Actuato metric 來自動伸縮 Spring Boot 應用程序。使用 Spring Boot 提供的特性以及 Spring Cloud Netflix Eureka 和 Jenkins ,您就能夠實現系統的自動伸縮,而無需藉助於任何其餘第三方工具。本文也假設遠程服務器上也是使用 Jenkins 代理來啓動新的實例,可是您也可使用 Ansible 這樣的工具來啓動。若是您決定從 Jenkins 運行 Ansible 腳本,那麼將不須要在遠程機器上啓動 Jenkins 代理。
歡迎工做一到五年的Java工程師朋友們加入Java架構開發: 855835163 羣內提供免費的Java架構學習資料(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用本身每一分每一秒的時間來學習提高本身,不要再用"沒有時間「來掩飾本身思想上的懶惰!趁年輕,使勁拼,給將來的本身一個交代!