在这篇博文中,我介绍了批处理开发人员或架构师在大规模设计和运行批处理应用程序时可能面临的一些挑战,并展示了 Spring Batch、Spring Boot 和 Kubernetes 如何极大地简化这项任务。
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 用集群的机器玩俄罗斯方块)从而优化账单!
- 使用许多其他有趣的功能
在本节中,我采用了 Spring Batch入门指南中开发的相同作业(这是一个数据摄取作业,将一些个人数据从 CSV 文件加载到关系数据库表中),将其容器化,并将其部署到 Kubernetes 上。如果您想更进一步,将此作业包装在 Spring Cloud Task 中并将其部署在 Spring Cloud Data Flow 服务器中,请参阅使用数据流部署 Spring Batch 应用程序。
1. 设置数据库服务器
我使用 MySQL 数据库来存储 Spring Batch 元数据。数据库位于 Kubernetes 集群之外,这是故意的。原因是为了模仿现实的迁移路径,其中第一步只有无状态工作负载迁移到 Kubernetes。对于许多公司来说,将数据库迁移到 Kubernetes 还不是一个选择(这是一个合理的决定)。要启动数据库服务器,请运行以下命令:
$ git clone git@github.com: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. 创建一个 Bootiful、容器化的 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所需的所有基础设施豆(job repository、job launcher等),以及一些实用工具,如JobBuilderFactory和StepBuilderFactory,以方便创建步骤和作业。在上面的片段中,我使用这些工具来创建一个具有单一的面向块的步骤的作业,定义如下:
- 一个从UrlResource中读取数据的项目阅读器。在一些云环境中,文件系统是只读的,或者甚至不存在,所以不下载数据就能流转数据几乎是一个基本要求。幸运的是,Spring Batch为你提供了保障所有基于文件的项目阅读器(针对平面文件、XML文件和JSON文件)都是针对强大的Spring框架资源抽象工作的,所以任何资源的实现都应该是有效的。在这个例子中,我使用UrlResource直接从GitHub的远程URL sample-data.csv中读取数据,而无需下载。文件名是作为工作参数传入的。
- 一个项目写入器,将个人项目写入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 如何在不需要创建 Dockerfile 的情况下创建 Docker 镜像!令人敬畏的 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
|
你应该看到类似的东西:
. <strong>__ _ </strong> _ _ /\\ / <strong>_'_ </strong> _ _(_)_ <strong> </strong> _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |<strong>__| .</strong>|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: 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 之前,我想展示两件事:
1、防止同一作业实例的重复作业执行
如果您想了解 Spring Batch 如何防止重复作业执行,您可以尝试使用相同的命令重新运行该作业。
Spring Batch 不会让相同的作业实例在成功完成后重新运行。这是设计使然,以防止由于人为错误或平台限制而导致的重复作业执行。
2、防止同一作业实例的并发作业执行
本着同样的精神,Spring Batch 阻止了同一作业实例的并发执行。要对其进行测试,请添加一个项目处理器来Thread.sleep减慢处理速度,并在第一个作业运行时尝试运行第二个作业执行(在单独的终端中)。
更多点击标题