Kubernetes 上的 Spring Batch:大規模高效批次處理

工程 | Mahmoud Ben Hassine | 2021 年 1 月 28 日 | ...

簡介

自從早期使用打孔卡和磁帶以來,批次處理一直是計算機科學中一個具有挑戰性的領域。如今,現代雲端運算時代為如何在雲端環境中高效地開發和運營批次工作負載帶來了一系列全新的挑戰。在這篇部落格文章中,我將介紹批次開發人員或架構師在設計和執行大規模批次應用程式時可能面臨的一些挑戰,並展示 Spring Batch、Spring Boot 和 Kubernetes 如何極大地簡化這項任務。

在雲端中設計和執行批次工作負載的挑戰

與 Web 應用程式相比,設計雲原生批次應用程式可能看起來很容易,但事實並非如此。批次開發人員面臨許多挑戰。

1. 容錯能力

批次處理通常與其他服務(例如資料庫、訊息代理、Web 服務等)互動,這些服務本質上在雲端環境中是不穩定的。此外,即使執行這些處理程序的節點也可能隨時關閉,並被健康的節點取代。雲原生批次應用程式的設計應具有容錯能力。

2. 穩健性

重複執行批次作業的人為錯誤造成重大財務後果的情況並不少見(例如 WalgreensANZ BankNatWest 等等)。此外,某些平台(例如 Kubernetes)對於重複執行相同作業的偶發性存在一些已知限制。雲原生批次應用程式的設計應準備好處理此類問題。

3. 成本效率

雲端基礎架構根據 cpu/記憶體/頻寬使用量計費。如果發生故障,無法從上次停止的地方重新啟動作業並「失去」先前執行的 cpu/記憶體/頻寬使用量將會非常沒有效率(因此會被收取兩次或更多次的費用!)。

4. 可觀察性

任何現代批次架構都應能夠隨時了解一些關鍵指標,包括

  • 目前正在執行哪些作業?
  • 哪些作業失敗了(如果有的話)?
  • 關於事情進展的其他問題。

能夠在儀表板上一目了然地看到這些 KPI 對於高效運營至關重要。

5. 可擴展性

我們正在處理前所未有的資料量,這已無法在單一機器上處理。正確處理大量分散式資料可能最具挑戰性。雲原生批次應用程式的設計應具有可擴展性。

在設計和開發雲原生批次應用程式時,應考慮所有這些方面。這對開發人員來說是一項相當大的工作。Spring Batch 會處理大部分這些問題。我在下一節中詳細說明。

Spring Batch 如何讓批次開發人員的生活更輕鬆?

Spring Batch 是 JVM 上事實上的批次處理架構。關於 Spring Batch 提供的豐富功能集已經寫了整本書,但我想強調在雲原生開發的背景下,解決先前提到之挑戰的最相關功能

1. 容錯能力

Spring Batch 提供了容錯功能,例如交易管理以及跳過和重試機制,這些功能在批次作業與雲端環境中不穩定的服務互動時非常有用。

2. 穩健性

Spring Batch 使用集中式交易作業儲存庫,可防止重複的作業執行。透過設計,可以避免人為錯誤和平台限制可能導致重複執行相同作業的情況。

3. 成本效益

Spring Batch 作業會將其狀態維護在外部資料庫中,這使得重新啟動失敗的作業成為可能,並從它們停止的地方繼續執行。與其他從頭開始重做工作,因此會被收取兩次或更多次費用的解決方案相比,這更具成本效益!

4. 可觀測性

Spring Batch 提供與 Micrometer 的整合,這在可觀測性方面至關重要。基於 Spring Batch 的批次基礎架構提供關鍵指標,例如目前作用中的作業、讀/寫速率、失敗的作業等。甚至可以使用自訂指標來擴充它。

5. 可擴展性

如前所述,Spring Batch 作業會將其狀態維護在外部資料庫中。因此,從 12 要素 方法論的觀點來看,它們是無狀態的程序。這種無狀態的性質使它們適合於容器化,並以可擴展的方式在雲端環境中執行。此外,Spring Batch 提供多種垂直和水平擴展技術,例如多執行緒步驟和資料的遠端分割/區塊化,以有效擴展批次作業。

Spring Batch 提供其他功能,但上述功能在設計和開發雲原生批次流程時非常有幫助。

Kubernetes 如何簡化批次管理員的生活?

Kubernetes 是雲端的事實容器編排平台。大規模運作批次基礎架構絕非易事,而 Kubernetes 在這個領域確實改變了遊戲規則。在雲端時代之前,在我之前的一份工作中,我扮演了批次管理員的角色,我必須管理一個專用於批次作業的 4 台機器叢集。以下是我必須手動執行或找到使用 (bash!) 腳本自動化的一些任務

  • ssh 進入每台機器以檢查目前正在執行的作業
  • ssh 進入每台機器以收集失敗作業的日誌
  • ssh 進入每台機器以升級作業版本或更新其配置
  • ssh 進入每台機器以終止掛起的作業並重新啟動它們
  • ssh 進入每台機器以編輯/更新用於作業排程的 crontab 檔案
  • 許多其他類似的任務..

所有這些任務顯然效率低下且容易出錯,由於資源管理不善,導致四台專用機器未得到充分利用。如果您在 2021 年仍在執行此類任務(無論是手動還是透過腳本),我相信現在是考慮將您的批次基礎架構遷移到 Kubernetes 的好時機。原因是 Kubernetes 讓您可以使用 **單一** 命令針對 **整個** 叢集執行所有這些任務,從運營的角度來看,這是一個 **巨大** 的差異。遷移到 Kubernetes 讓您可以

  • 使用單一命令詢問整個叢集目前正在執行的作業
  • 提交/排程作業,而無需知道它們將在哪個節點上執行
  • 透明地更新作業定義
  • 自動執行作業以完成(Kubernetes 作業建立一個或多個 pod,並確保指定數量的 pod 成功終止)
  • 最佳化叢集資源的使用(Kubernetes 在叢集機器上玩 Tetris),從而最佳化帳單!
  • 使用許多其他有趣的功能

Spring Batch on Kubernetes:完美的搭配,實際操作

在本節中,我採用 Spring Batch 入門指南 中開發的相同作業(這是一個資料擷取作業,可將一些人員資料從 CSV 檔案載入到關係資料庫表中),將其容器化,並將其部署在 Kubernetes 上。如果您想更進一步,將此作業包裝在 Spring Cloud Task 中,並將其部署在 Spring Cloud Data Flow 伺服器中,請參閱 使用 Data Flow 部署 Spring Batch 應用程式

1. 設置資料庫伺服器

我使用 MySQL 資料庫來儲存 Spring Batch 元數據。資料庫位於 Kubernetes 叢集之外,這是故意的。原因是模仿一個實際的遷移路徑,其中只有無狀態的工作負載在第一步遷移到 Kubernetes。對於許多公司來說,將資料庫遷移到 Kubernetes 還不是一個選項(這是一個合理的決定)。要啟動資料庫伺服器,請執行以下命令

$ git clone [email protected]:benas/spring-batch-lab.git
$ cd blog/spring-batch-kubernetes
$ docker-compose -f src/docker/docker-compose.yml up

這將建立一個預先填充了 Spring Batch 的技術表 以及 業務表 PEOPLE 的 MySQL 容器。我們可以按如下方式檢查這一點

$ docker exec -it mysql bash
root@0a6596feb06d:/# mysql -u root test -p # the root password is "root"
Enter password:
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.21 MySQL Community Server - GPL

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show tables;
+------------------------------+
| Tables_in_test               |
+------------------------------+
| BATCH_JOB_EXECUTION          |
| BATCH_JOB_EXECUTION_CONTEXT  |
| BATCH_JOB_EXECUTION_PARAMS   |
| BATCH_JOB_EXECUTION_SEQ      |
| BATCH_JOB_INSTANCE           |
| BATCH_JOB_SEQ                |
| BATCH_STEP_EXECUTION         |
| BATCH_STEP_EXECUTION_CONTEXT |
| BATCH_STEP_EXECUTION_SEQ     |
| PEOPLE                       |
+------------------------------+
10 rows in set (0.01 sec)

mysql> select * from PEOPLE;
Empty set (0.00 sec)

2. 建立一個可愛的、容器化的 Spring Batch 作業

前往 start.spring.io 並使用以下依賴項產生一個專案:Spring Batch 和 MySQL 驅動程式。您可以使用此 連結 建立專案。解壓縮專案並將其載入到您最喜歡的 IDE 後,您可以按如下方式更改主類別

package com.example.demo;

import java.net.MalformedURLException;

import javax.sql.DataSource;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;

@SpringBootApplication
@EnableBatchProcessing
public class DemoApplication {

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

	@Bean
	@StepScope
	public Resource resource(@Value("#{jobParameters['fileName']}") String fileName) throws MalformedURLException {
		return new UrlResource(fileName);
	}

	@Bean
	public FlatFileItemReader<Person> itemReader(Resource resource)  {
		return new FlatFileItemReaderBuilder<Person>()
				.name("personItemReader")
				.resource(resource)
				.delimited()
				.names("firstName", "lastName")
				.targetType(Person.class)
				.build();
	}

	@Bean
	public JdbcBatchItemWriter<Person> itemWriter(DataSource dataSource) {
		return new JdbcBatchItemWriterBuilder<Person>()
				.dataSource(dataSource)
				.sql("INSERT INTO PEOPLE (FIRST_NAME, LAST_NAME) VALUES (:firstName, :lastName)")
				.beanMapped()
				.build();
	}

	@Bean
	public Job job(JobBuilderFactory jobs, StepBuilderFactory steps,
				   DataSource dataSource, Resource resource) {
		return jobs.get("job")
				.start(steps.get("step")
						.<Person, Person>chunk(3)
						.reader(itemReader(resource))
						.writer(itemWriter(dataSource))
						.build())
				.build();
	}

	public static class Person {
		private String firstName;
		private String lastName;
                // default constructor + getters/setters omitted for brevity
	}

}

@EnableBatchProcessing 註釋設置 Spring Batch 所需的所有基礎架構 bean(作業儲存庫、作業啟動器等)以及一些實用程式,例如 JobBuilderFactoryStepBuilderFactory,以方便建立步驟和作業。在上面的程式碼片段中,我使用這些實用程式建立了一個具有單一區塊導向步驟的作業,定義如下

  • 一個從 UrlResource 讀取資料的項目讀取器。在某些雲端環境中,檔案系統是唯讀的,甚至不存在,因此能夠在不下載的情況下串流傳輸資料幾乎是一項基本要求。幸運的是,Spring Batch 已經涵蓋了這一點!所有基於檔案的項目讀取器(對於平面檔案、XML 檔案和 JSON 檔案)都針對強大的 Spring Framework Resource 抽象工作,因此 Resource 的任何實作都應該有效。在本範例中,我使用 UrlResource 直接從 GitHub 上的 sample-data.csv 的遠端 URL 讀取資料,而無需下載。檔名作為作業參數傳入。
  • 一個將 Person 項目寫入 MySQL 中 PEOPLE 表的項目寫入器。

就是這樣。讓我們打包作業並使用 Spring Boot 的 maven 外掛程式為其建立一個 docker 映像

$ mvn package
...
$ mvn spring-boot:build-image -Dspring-boot.build-image.imageName=benas/bootiful-job
[INFO] Scanning for projects...
[INFO]
…
[INFO] --- spring-boot-maven-plugin:2.4.1:build-image (default-cli) @ demo ---
[INFO] Building image 'docker.io/benas/bootiful-job:latest'
…
[INFO] Successfully built image 'docker.io/benas/bootiful-job:latest'
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

映像現在應該已正確建立,但讓我們檢查一下

$ docker images
REPOSITORY             TAG           IMAGE ID               CREATED             SIZE
benas/bootiful-job     latest        52244b284f08    41 seconds ago   242MB

請注意 Spring Boot 如何建立 Docker 映像,而無需建立 Dockerfile!Josh Long 編寫了一篇關於此驚人功能的完整部落格文章:YMNNALFT:使用 Spring Boot Maven 外掛程式和 Buildpacks 輕鬆建立 Docker 映像。現在讓我們在 Docker 容器中執行此作業,以檢查一切是否按預期工作

$ docker run \
   -e SPRING_DATASOURCE_URL=jdbc:mysql://192.168.1.53:3306/test \
   -e SPRING_DATASOURCE_USERNAME=root \
   -e SPRING_DATASOURCE_PASSWORD=root \
   -e SPRING_DATASOURCE_DRIVER-CLASS-NAME=com.mysql.cj.jdbc.Driver \
   benas/bootiful-job \
   fileName=https://raw.githubusercontent.com/benas/spring-batch-lab/master/blog/spring-batch-kubernetes/data/sample1.csv

您應該會看到類似以下的內容

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.1)

2021-01-08 17:03:15.009  INFO 1 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication v0.0.1-SNAPSHOT using Java 1.8.0_275 on 876da4a1cfe0 with PID 1 (/workspace/BOOT-INF/classes started by cnb in /workspace)
2021-01-08 17:03:15.012  INFO 1 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2021-01-08 17:03:15.899  INFO 1 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-01-08 17:03:16.085  INFO 1 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2021-01-08 17:03:16.139  INFO 1 --- [           main] o.s.b.c.r.s.JobRepositoryFactoryBean     : No database type set, using meta data indicating: MYSQL
2021-01-08 17:03:16.292  INFO 1 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : No TaskExecutor has been set, defaulting to synchronous executor.
2021-01-08 17:03:16.411  INFO 1 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 1.754 seconds (JVM running for 2.383)
2021-01-08 17:03:16.414  INFO 1 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [fileName=https://raw.githubusercontent.com/benas/spring-batch-lab/master/blog/spring-batch-kubernetes/data/sample1.csv]
2021-01-08 17:03:16.536  INFO 1 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] launched with the following parameters: [{fileName=https://raw.githubusercontent.com/benas/spring-batch-lab/master/blog/spring-batch-kubernetes/data/sample1.csv}]
2021-01-08 17:03:16.596  INFO 1 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step]
2021-01-08 17:03:17.481  INFO 1 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [step] executed in 884ms
2021-01-08 17:03:17.501  INFO 1 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] completed with the following parameters: [{fileName=https://raw.githubusercontent.com/benas/spring-batch-lab/master/blog/spring-batch-kubernetes/data/sample1.csv}] and the following status: [COMPLETED] in 934ms
2021-01-08 17:03:17.513  INFO 1 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2021-01-08 17:03:17.534  INFO 1 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

作業現在已完成,我們可以檢查資料是否已成功載入到資料庫中

mysql> select * from PEOPLE;
+----+------------+-----------+
| ID | FIRST_NAME | LAST_NAME |
+----+------------+-----------+
|  1 | Jill       | Doe       |
|  2 | Joe        | Doe       |
|  3 | Justin     | Doe       |
|  4 | Jane       | Doe       |
|  5 | John       | Doe       |
+----+------------+-----------+
5 rows in set (0.00 sec)

就這樣!現在讓我們把這個任務部署到 Kubernetes 上。不過,在繼續並將這個任務部署到 Kubernetes 之前,我想展示兩件事:

防止相同任務實例的重複執行

如果您想了解 Spring Batch 如何防止重複的任務執行,您可以嘗試使用相同的命令重新執行該任務。應用程式應該會啟動失敗,並顯示以下錯誤:

2021-01-08 20:21:20.752 ERROR 1 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: Failed to execute ApplicationRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:798) [spring-boot-2.4.1.jar:2.4.1]
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:785) [spring-boot-2.4.1.jar:2.4.1]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:333) [spring-boot-2.4.1.jar:2.4.1]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1309) [spring-boot-2.4.1.jar:2.4.1]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1298) [spring-boot-2.4.1.jar:2.4.1]
	at com.example.demo.DemoApplication.main(DemoApplication.java:30) [classes/:0.0.1-SNAPSHOT]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_275]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_275]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_275]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_275]
	at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49) [workspace/:na]
	at org.springframework.boot.loader.Launcher.launch(Launcher.java:107) [workspace/:na]
	at org.springframework.boot.loader.Launcher.launch(Launcher.java:58) [workspace/:na]
	at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88) [workspace/:na]
Caused by: org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException: A job instance already exists and is complete for parameters={fileName=https://raw.githubusercontent.com/benas/spring-batch-lab/master/blog/spring-batch-kubernetes/data/sample1.csv}.  If you want to run this job again, change the parameters.
…

Spring Batch 不允許在成功完成後重新執行相同的任務實例。這是經過設計的,目的是防止由於人為錯誤或平台限制而導致的重複任務執行,如前一節所述。

防止相同任務實例的並行執行

同樣地,Spring Batch 也會防止相同任務實例的並行執行。為了測試這一點,您可以添加一個 Thread.sleep 的 item processor 來減慢處理速度,然後在第一個任務執行正在運行時(在另一個終端機中)嘗試運行第二個任務執行。第二次(並行)嘗試會失敗,並顯示:

2021-01-08 20:59:04.201 ERROR 1 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: Failed to execute ApplicationRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:798) [spring-boot-2.4.1.jar:2.4.1]
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:785) [spring-boot-2.4.1.jar:2.4.1]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:333) [spring-boot-2.4.1.jar:2.4.1]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1309) [spring-boot-2.4.1.jar:2.4.1]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1298) [spring-boot-2.4.1.jar:2.4.1]
	at com.example.demo.DemoApplication.main(DemoApplication.java:31) [classes/:0.0.1-SNAPSHOT]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_275]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_275]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_275]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_275]
	at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49) [workspace/:na]
	at org.springframework.boot.loader.Launcher.launch(Launcher.java:107) [workspace/:na]
	at org.springframework.boot.loader.Launcher.launch(Launcher.java:58) [workspace/:na]
	at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88) [workspace/:na]
Caused by: org.springframework.batch.core.repository.JobExecutionAlreadyRunningException: A job execution for this job is already running: JobExecution: id=1, version=1, startTime=2021-01-08 20:58:46.434, endTime=null, lastUpdated=2021-01-08 20:58:46.435, status=STARTED, exitStatus=exitCode=UNKNOWN;exitDescription=, job=[JobInstance: id=1, version=0, Job=[job]], jobParameters=[{fileName=https://raw.githubusercontent.com/benas/spring-batch-lab/master/blog/spring-batch-kubernetes/data/sample1.csv}]
…

由於集中式的任務儲存庫,Spring Batch 可以檢測目前正在運行的執行(基於資料庫中的任務狀態),並通過拋出 JobExecutionAlreadyRunningException 來防止在同一節點或叢集的任何其他節點上並行執行。

3. 在 Kubernetes 上部署任務

設置 Kubernetes 叢集不在本文的範圍內,所以我假設您已經有一個正在運行並可以通過使用 kubectl 與之交互的 Kubernetes 叢集。在本文中,我使用 Docker Desktop 應用程式提供的單節點本地 Kubernetes 叢集。

首先,我為外部資料庫創建一個服務,如 Kubernetes 最佳實務:映射外部服務 中的「情境 1:叢集外部具有 IP 位址的資料庫」中所述。以下是服務定義:

kind: Service
apiVersion: v1
metadata:
  name: mysql
spec:
    type: ClusterIP
    ports:
      - port: 3306
        targetPort: 3306
---
kind: Endpoints
apiVersion: v1
metadata:
  name: mysql
subsets:
  - addresses:
      - ip: 192.168.1.53 # This is my local IP, you might need to change it if needed
    ports:
      - port: 3306
---
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  # base64 of "root" ($>echo -n "root" | base64)
  db.username: cm9vdA==
  db.password: cm9vdA==

這個服務可以應用於 Kubernetes,如下所示:

$ kubectl apply -f src/kubernetes/database-service.yaml

現在,由於我們已經為我們的任務創建了一個 Docker 映像,將其部署到 Kubernetes 只是定義一個具有以下清單的 Job 資源的問題:

apiVersion: batch/v1
kind: Job
metadata:
  name: bootiful-job-$JOB_NAME
spec:
  template:
    spec:
      restartPolicy: OnFailure
      containers:
        - name: bootiful-job
          image: benas/bootiful-job
          imagePullPolicy: Never
          args: ["fileName=$FILE_NAME"]
          env:
            - name: SPRING_DATASOURCE_DRIVER-CLASS-NAME
              value: com.mysql.cj.jdbc.Driver
            - name: SPRING_DATASOURCE_URL
              value: jdbc:mysql://mysql/test
            - name: SPRING_DATASOURCE_USERNAME
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: db.username
            - name: SPRING_DATASOURCE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: db.password

這個清單遵循與 基於範本創建任務 相同的方法,這是 Kubernetes 文件建議的。這個任務範本作為一個基礎,用於為每個要載入的輸入檔案創建一個任務。我已經載入了 sample1.csv 檔案,所以我使用以下命令為另一個名為 sample2.csv 的遠端檔案創建一個任務:

$ JOB_NAME=sample2 \
  FILE_NAME="https://raw.githubusercontent.com/benas/spring-batch-lab/master/blog/spring-batch-kubernetes/data/sample2.csv" \
  envsubst < src/k8s/job.yaml | kubectl apply -f -

這個命令替換任務範本中的變數,為給定的檔案創建一個任務定義,然後將其提交到 Kubernetes。讓我們檢查 Kubernetes 中的任務和 Pod 資源:

$ kubectl get jobs
NAME                  COMPLETIONS   DURATION   AGE
bootiful-job-sample2   0/1           97s        97s

$ kubectl get pods
NAME                             READY   STATUS      RESTARTS   AGE
bootiful-job-sample2-n8mlb   0/1     Completed   0          7s

$ kubectl logs bootiful-job-sample2-n8mlb
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.4.1)

2021-01-08 17:48:42.053  INFO 1 --- [           main] com.example.demo.BootifulJobApplication  : Starting BootifulJobApplication v0.1 using Java 1.8.0_275 on bootiful-job-person-n8mlb with PID 1 (/workspace/BOOT-INF/classes started by cnb in /workspace)
2021-01-08 17:48:42.056  INFO 1 --- [           main] com.example.demo.BootifulJobApplication  : No active profile set, falling back to default profiles: default
2021-01-08 17:48:43.028  INFO 1 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2021-01-08 17:48:43.180  INFO 1 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2021-01-08 17:48:43.231  INFO 1 --- [           main] o.s.b.c.r.s.JobRepositoryFactoryBean     : No database type set, using meta data indicating: MYSQL
2021-01-08 17:48:43.394  INFO 1 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : No TaskExecutor has been set, defaulting to synchronous executor.
2021-01-08 17:48:43.541  INFO 1 --- [           main] com.example.demo.BootifulJobApplication  : Started BootifulJobApplication in 1.877 seconds (JVM running for 2.338)
2021-01-08 17:48:43.544  INFO 1 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [fileName=https://raw.githubusercontent.com/benas/spring-batch-lab/master/blog/spring-batch-kubernetes/data/sample2.csv]
2021-01-08 17:48:43.677  INFO 1 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] launched with the following parameters: [{fileName=https://raw.githubusercontent.com/benas/spring-batch-lab/master/blog/spring-batch-kubernetes/data/sample2.csv}]
2021-01-08 17:48:43.758  INFO 1 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [step]
2021-01-08 17:48:44.632  INFO 1 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [step] executed in 873ms
2021-01-08 17:48:44.653  INFO 1 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=job]] completed with the following parameters: [{fileName=https://raw.githubusercontent.com/benas/spring-batch-lab/master/blog/spring-batch-kubernetes/data/sample2.csv}] and the following status: [COMPLETED] in 922ms
2021-01-08 17:48:44.662  INFO 1 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2021-01-08 17:48:44.693  INFO 1 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

然後,您可以檢查 PEOPLE 表中新添加的人員:

mysql> select * from PEOPLE;
+----+------------+-----------+
| ID | FIRST_NAME | LAST_NAME |
+----+------------+-----------+
|  1 | Jill       | Doe       |
|  2 | Joe        | Doe       |
|  3 | Justin     | Doe       |
|  4 | Jane       | Doe       |
|  5 | John       | Doe       |
|  6 | David      | Doe       |
|  7 | Damien     | Doe       |
|  8 | Danny      | Doe       |
|  9 | Dorothy    | Doe       |
|  10 | Daniel    | Doe       |

+----+------------+-----------+
10 rows in set (0.00 sec)

就是這樣,我們的任務已成功在 Kubernetes 中運行!

提示與技巧

在結束本文之前,我想分享一些在將 Spring Batch 任務遷移到 Kubernetes 上的雲端時值得考慮的提示與技巧。

1. 任務封裝和部署

在單個容器或 Pod 中運行多個 Spring Batch 任務不是一個好主意。這不符合雲原生開發的最佳實務,也不符合一般的 Unix 哲學。每個容器或 Pod 運行一個任務具有以下優點:

  • 分離的日誌
  • 獨立的生命週期(錯誤、功能、部署等)
  • 分離的參數和退出代碼
  • 可重新啟動性(如果失敗,僅重新啟動失敗的任務)

2. 選擇正確的 Spring Batch 任務參數

一個成功的 Spring Batch 任務實例無法重新啟動。同樣地,一個成功的 Kubernetes 任務也無法重新啟動。這使得為每個 Spring Batch 任務實例設計一個 Kubernetes 任務成為一個完美的匹配!因此,正確選擇 Spring Batch 中的識別任務參數變得至關重要,因為這樣做決定了任務實例的身份,並因此決定了 Kubernetes 任務的設計(請參閱第 3 點)。框架的兩個重要方面受到此選擇的影響:

  • 任務識別:Spring Batch 基於任務實例的身份來防止重複和並行的任務執行。
  • 失敗情境:Spring Batch 依賴於任務實例的身份來啟動一個新的任務執行,從上一個任務執行停止的地方開始。

批次處理是關於處理**固定、不可變**的資料集。如果輸入資料不是固定的,那麼使用流處理工具更合適。Spring Batch 中識別任務參數應該代表一個**唯一可識別的不可變**資料集。正確選擇一組識別任務參數的一個好提示是計算它們的雜湊值(或更準確地說是它們所代表的資料的雜湊值),並確保該雜湊值是穩定的。以下是一些例子:

任務參數 好/壞 評論
fileName=log.txt 一個不斷增長的日誌檔案不是一個固定的資料集
fileName=transactions-2020-08-20.csv 只要檔案內容是固定的
folderName=/in/data 一個具有可變內容的資料夾不是一個固定的資料集
folderName=/in/data/2020/12/20 一個包含給定日期收到的所有訂單檔案的資料夾
jmsQueueName=events 項目從佇列中移除,因此這不是一個固定的資料集
orderDate=2020-08-20 例如,如果用於 D+1 上的資料庫 select 查詢中

不幸的是,許多人在設計良好的識別任務參數方面失敗,最終添加一個時間戳或一個隨機數作為一個額外的識別任務參數,作為任務實例鑑別器。使用一個不斷增長的 "run.id" 參數是這種失敗的徵兆。

3. 選擇正確的 Kubernetes 任務部署模式

Kubernetes 的文件提供了一個完整的章節,稱為 任務模式,其中描述了如何選擇正確的任務部署模式。在本文中,我遵循了 使用擴展進行並行處理 的方法,從範本中為每個檔案創建一個任務。雖然這種方法允許並行處理多個檔案,但當有很多檔案要載入時,它會對 Kubernetes 造成壓力,因為這會導致創建許多 Kubernetes 任務對象。如果您的所有檔案都具有相似的結構,並且您想創建一個單一任務來一次性載入它們,您可以使用 Spring Batch 提供的 MultiResourceItemReader 並創建一個單一的 Kubernetes 任務。另一個選擇是使用一個具有分區步驟的單一任務,其中每個 worker 步驟處理一個檔案(這可以通過使用內置的 MultiResourcePartitioner 來實現)。

4. 優雅/突然關閉的影響

當一個 Spring Batch 任務執行失敗時,如果任務實例是可重新啟動的,您可以重新啟動它。您可以自動執行此操作,只要任務執行被優雅地關閉,因為這讓 Spring Batch 有機會正確地將任務執行的狀態設置為 FAILED 並將其 END_TIME 設置為非空值。但是,如果任務執行突然失敗,任務執行的狀態仍然會設置為 STARTED 並且其 END_TIMEnull。當您嘗試重新啟動這樣的任務執行時,Spring Batch 會認為(因為它只查看資料庫狀態)目前有一個任務執行正在為這個實例運行,並且會失敗並顯示 JobExecutionAlreadyRunningException。在這種情況下,應該更新元數據表以允許重新啟動這樣的失敗執行 -- 類似於:

> update BATCH_JOB_EXECUTION set status = 'FAILED', END_TIME = '2020-01-15 10:10:28.235' where job_execution_id = X;
> update BATCH_STEP_EXECUTION set status = 'FAILED' where job_execution_id = X and step_name='failed step name';

Spring Batch Job 的正常/突然關閉與 Kubernetes Job 的重新啟動策略直接相關。例如,當 restartPolicy=OnFailure 時,如果 Pod 突然故障,且 Job 控制器立即建立一個新的 Pod,您將無法及時更新資料庫,且新的 Spring Batch Job 執行會因 JobExecutionAlreadyRunningException 而失敗。同樣的情況也會發生在第三個 Pod,依此類推,直到 Pod 達到 CrashLoopBackOff 狀態,並且在超過 backoffLimit 後被刪除。

現在,如果您遵循最佳實踐,使用 System.exit(SpringApplication.exit(SpringApplication.run(MyBatchApplication.class, args))); 執行您的 Spring Boot Batch 應用程式(如上面的程式碼片段所示),Spring Boot(進而 Spring Batch)可以正確處理 SIGTERM 訊號,並在 Kubernetes 啟動 Pod 終止流程時,正常關閉您的應用程式。如此一來,當 Pod 正常關閉時,Spring Batch Job 實例可以自動重新啟動直到完成。不幸的是,Kubernetes Pod 的正常關閉並非總是能保證,因此在設定重新啟動策略和 backoffLimit 值時,您應該考慮這一點,以確保您有足夠的時間來更新 Job 儲存庫,以處理失敗的 Job。

應該注意的是,Docker 的 ENTRYPOINTshell 形式不會將 Unix 訊號傳送到容器中執行的子進程。因此,為了讓在容器中執行的 Spring Batch Job 正確攔截 Unix 訊號,ENTRYPOINT 形式應該是 exec。這也與上面提到的 Kubernetes 的 Pod 終止流程直接相關。關於此事的更多詳細資訊,可以在Kubernetes 最佳實踐:優雅終止部落格文章中找到。

5. 選擇正確的 Kubernetes Job 並行策略

正如我先前指出的,Spring Batch 會阻止相同 Job 實例的並行 Job 執行。因此,如果您遵循 "每個 Spring Batch Job 實例一個 Kubernetes Job" 的部署模式,將 Job 的 spec.parallelism 設定為大於 1 的值是沒有意義的,因為這會並行啟動兩個 Pod,並且其中一個肯定會因 JobExecutionAlreadyRunningException 而失敗。然而,對於分割的 Job,將 spec.parallelism 設定為大於 1 的值就非常有意義。在這種情況下,可以並行 Pod 中執行分區。正確選擇並行策略與選擇哪種 Job 模式密切相關(如第 3 點中所述)。

6. Job 元數據整理

刪除 Kubernetes Job 會刪除其對應的 Pod。Kubernetes 提供了一種使用 ttlSecondsAfterFinished 參數自動清除已完成 Job 的方法。然而,在 Spring Batch 中沒有等效的方法:您應該手動清除 Job 儲存庫。對於任何嚴肅的生產批次基礎架構,您應該考慮這一點,因為 Job 實例和執行可能會非常快速地增長,具體取決於部署的 Job 的頻率和數量。我認為這裡有一個很好的機會來建立一個 Kubernetes 自定義資源定義,以便在刪除對應的 Kubernetes Job 時刪除 Spring Batch 的元數據。

結論

我希望這篇文章能闡明在雲端設計、開發和執行批次應用程式的挑戰,以及 Spring Batch、Spring Boot 和 Kubernetes 如何極大地簡化這項任務。這篇文章展示了如何透過 Spring 生態系統的生產力,在三個簡單的步驟中從 start.spring.io 到 Kubernetes,但這只是冰山一角。這篇文章是一個部落格系列的開端,我將在其中涵蓋在 Kubernetes 上執行 Spring Batch Job 的其他方面。在接下來的文章中,我將解決使用 MicrometerWavefront 進行 Job 可觀察性的問題,以及如何在 Kubernetes 上擴展 Spring Batch Job。敬請關注!

獲取 Spring 電子報

透過 Spring 電子報保持聯繫

訂閱

搶先一步

VMware 提供培訓和認證,以加速您的進度。

了解更多

獲得支持

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

查看全部