我們決定使用Kubernetes
、Pivotal Cloud Foundry
或HashiCorp`s Nomad
等工具的一個更重要的原因是為了讓系統可以自動伸縮。當然,這些工具也提供了許多其他有用的功能,在這裡,我們只是用它們來實現系統的自動伸縮。乍一看,這似乎很困難,但是,如果我們使用Spring Boot
來構建應用程式,並使用Jenkins
來實現CI
,那麼就用不了太多工作。
今天,我將向您展示如何使用以下框架/工具實現這樣的解決方案:
- Spring Boot
- Spring Boot Actuator
- Spring Cloud Netflix Eureka
- Jenkins CI
它是如何工作的
每一個包含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 Boot
為應用程式提供了這樣的機制。我們只需要將application.yml
中的server.port
屬性設定為0
。因為我們的應用程式會在Eureka
中註冊,並且傳送唯一的標識instanceId
,預設情況下這個唯一標識是將欄位spring.cloud.client.hostname
, spring.application.name
和server.port
拼接而成的。
示例應用程式的當前配置如下所示。
可以看到,我通過將埠號替換為隨機生成的數字來改變了生成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進階之路:878249276,群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用”沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!
原文連結:http://www.spring4all.com/article/1594