使用Kafka分割槽擴充套件Spring Batch大資料排程批處理 – Arnold
假設有一個您需要定期執行的流程,例如一天結束 (EOD)。假設這個流程中需要處理的資料量在不斷增加。
最初,你可以做一個非常簡單的 Spring 排程(或者 Quartz 或者你有什麼),它只執行一個方法,一次載入所有資料,處理所有資料並將結果寫回資料庫。
如果讀取的行數(例如從資料庫中)是 10,000 行,它可能工作得很好,但如果突然有 10 000 000 行怎麼辦?執行可能會失敗,因為記憶體不足錯誤
或者需要很長的時間才能完成。
遠端分割槽
獲取初始資料集時,例如,如果我們從資料庫中讀取事務(或任何域物件),我們只獲取事務 ID。
將它們劃分為分割槽(不是塊;塊在 Spring Batch 世界中具有不同的含義)並將分割槽傳送給可以處理它們並執行實際業務邏輯的工作人員。
常規分割槽和遠端分割槽的主要區別在於工作者的位置。
- 在常規分割槽的情況下,作為工作者的程式是與正在進行資料分割槽的程式在同一JVM中的本地執行緒。
- 但在遠端分割槽的情況下,工作者不是在同一個JVM中執行,而是完全不同的JVM。當有一些工作需要處理時,會透過訊息傳遞系統通知各個工作者。
侷限性
Kafka是基於主題執行的。主題可以有分割槽。你可以擁有的消費者數量(對於同一個消費者組)取決於你對主題的分割槽數量。這意味著,你的分割槽批處理作業的併發係數與主題分割槽的數量直接相關。
一個主題所使用的分割槽數量應在建立該主題時設定。後來,我們可以改變現有主題的分割槽數量,但是你必須注意到某些副作用。
這意味著Kafka不可能根據資料量來動態地擴充套件工作者的數量。我所說的動態是指,有時你需要10個工人,但假設在聖誕節期間資料量大增,你就需要50個。這就需要一些自定義的指令碼了。
畢竟,我認為一個好的經驗法則--在Kafka的情況下--是過度擴大主題分割槽的數量。比方說,如果你在非高峰期需要10個消費者,而在高峰期需要20個,我認為你可以選擇兩倍/三倍的數量,以確保你有增長的空間,而不會有太多的頭痛。因此,我認為60是一個很好的分割槽數字,最多可以支援60個同時進行的消費者。當然,這取決於你的資料量的增長速度,但你應該明白這個道理。
技術棧
- Spring Batch
- Spring Integration
- Spring for Apache Kafka
- MySQL
- Liquibase
Manager
我們將從管理器和它的配置開始。讓我們有一個ManagerConfiguration類。我們將需要幾個配置的依賴項和兩個註釋。
@Configuration @Profile("manager") public class ManagerConfiguration { @Autowired private JobBuilderFactory jobBuilderFactory; @Autowired private RemotePartitioningManagerStepBuilderFactory stepBuilderFactory; @Autowired private KafkaTemplate kafkaTemplate; } |
@Profile註解是至關重要的,因為我們只想讓這個配置在我們試圖執行管理器時啟動,我們將用Spring的profile來控制它。
JobBuilderFactory將被用來建立我們的分割槽作業。RemotePartitioningManagerStepBuilderFactory將用於為我們的工作建立步驟,使用這個類而不是普通的StepBuilderFactory非常重要。另外,請注意,有一個非常類似的StepBuilderFactory,叫做RemotePartitioningWorkerStepBuilderFactory,它是用來給工人而不是經理使用的。我們很快就會到那裡。
KafkaTemplate是自動為我們配置的,我們將需要它來配置管理器和Kafka之間的通道。
現在,讓我們上新增一個通道,我們將把它作為從應用程式到Kafka的輸出通道。
@Configuration @Profile("manager") public class ManagerConfiguration { // previous content is omitted for simplicity @Bean public DirectChannel outboundRequests() { return new DirectChannel(); } } |
DirectChannel只是Spring Integration中對訊息通道的一個抽象。
接下來,讓我們建立一個Partitioner,對我們的資料集進行分割槽。這將是一個新的類,我把它叫做ExamplePartitioner。
public class ExamplePartitioner implements Partitioner { public static final String PARTITION_PREFIX = "partition"; @Override public Map<String, ExecutionContext> partition(int gridSize) { int partitionCount = 50; Map<String, ExecutionContext> partitions = new HashMap<>(); for (int i = 0; i < partitionCount; i++) { ExecutionContext executionContext = new ExecutionContext(); executionContext.put("data", new ArrayList<Integer>()); partitions.put(PARTITION_PREFIX + i, executionContext); } for (int i = 0; i < 1000; i++) { String key = PARTITION_PREFIX + (i % partitionCount); ExecutionContext executionContext = partitions.get(key); List<Integer> data = (List<Integer>) executionContext.get("data"); data.add(i + 1); } return partitions; } } |
這個分割槽器的實現沒有做任何有趣的事情。它建立了50個分割槽,對於每個分割槽,它把一些數字放入一個列表中,在關鍵資料下可以訪問。
這意味著,每個分割槽在列表中會有20個數字。
這就是你可以想象獲得交易或任何你想處理的ID的地方,稍後下線,工作者將從資料庫中載入相應的行。
很好,讓我們建立工作步驟job steps並從分割槽器中建立一個bean。
@Configuration @Profile("manager") public class ManagerConfiguration { // previous content is omitted for simplicity @Bean public ExamplePartitioner partitioner() { return new ExamplePartitioner(); } @Bean public Step partitionerStep() { return stepBuilderFactory.get("partitionerStep") .partitioner(Constants.WORKER_STEP_NAME, partitioner()) .outputChannel(outboundRequests()) .build(); } } |
沒有什麼特別的,我們建立了呼叫分割槽器的步驟,以及我們想把分割槽傳送到的輸出通道。
另外,這裡有一個對常量類的引用,讓我給你看看它的內容。
public class Constants { public static final String TOPIC_NAME = "work"; public static final String WORKER_STEP_NAME = "simpleStep"; public static final int TOPIC_PARTITION_COUNT = 3; } |
這就是全部。我們將呼叫Kafka主題工作,它將有3個主題分割槽,我們想在分割槽資料集上呼叫的工作步驟被稱為simpleStep。
很好,現在我們來建立分割槽器工作。
@Configuration @Profile("manager") public class ManagerConfiguration { // previous content is omitted for simplicity @Bean(name = "partitionerJob") public Job partitionerJob() { return jobBuilderFactory.get("partitioningJob") .start(partitionerStep()) .incrementer(new RunIdIncrementer()) .build(); } } |
同樣,沒有什麼特別的,只是引用了我們之前建立的分割槽器步驟,並在作業中新增了RunIdIncrementer,這樣我們就可以輕鬆地重新執行作業。
很好。現在,我想說的是最複雜的東西,如何將通道接入Kafka,並確保主題分割槽被正確利用。
我們也會用Spring Integration來做這個:
@Configuration @Profile("manager") public class ManagerConfiguration { // previous content is omitted for simplicity @Bean public IntegrationFlow outboundFlow() { KafkaProducerMessageHandler messageHandler = new KafkaProducerMessageHandler(kafkaTemplate); messageHandler.setTopicExpression(new LiteralExpression(Constants.TOPIC_NAME)); return IntegrationFlows .from(outboundRequests()) .log() .handle(messageHandler) .get(); } } |
首先,我們需要一個KafkaProducerMessageHandler,它將接收到的訊息並將其釋出到Kafka主題中。
該主題由setTopicExpression方法呼叫來標記,最後,我們只需將所有東西作為一個整合流來連線。
然而,這還不會利用主題分割槽,訊息將被髮布到同一個分割槽。
讓我們透過setPartitionIdExpression方法為其新增一個自定義表示式。
@Configuration @Profile("manager") public class ManagerConfiguration { // previous content is omitted for simplicity @Bean public IntegrationFlow outboundFlow() { KafkaProducerMessageHandler messageHandler = new KafkaProducerMessageHandler(kafkaTemplate); messageHandler.setTopicExpression(new LiteralExpression(Constants.TOPIC_NAME)); Function<Message<?>, Long> partitionIdFn = (m) -> { StepExecutionRequest executionRequest = (StepExecutionRequest) m.getPayload(); return executionRequest.getStepExecutionId() % Constants.TOPIC_PARTITION_COUNT; }; messageHandler.setPartitionIdExpression(new FunctionExpression<>(partitionIdFn)); return IntegrationFlows .from(outboundRequests()) .log() .handle(messageHandler) .get(); } } |
我們提供一個FunctionExpression,它將動態地解開訊息,獲得stepExecutionId屬性並與modulo運算子相結合。
分割槽計數的當前值是3。這意味著分割槽ID表示式將從[0, 1, 2]範圍內返回一個值,這將表示目標主題分割槽。
這算是在分割槽之間提供了一種平均分配,但不是100%。
如果你需要一個複雜的分割槽ID決定器,你肯定可以調整實現。
另外,你也可以同樣使用setMessageKeyExpression方法來提供一個類似的FunctionExpression來計算訊息key,而不是直接告訴Kafka要使用哪個分割槽。
還有一點需要注意的是,我在整合流程中加入了log(),所以傳送出去的訊息會被記錄下來;只是為了除錯的目的。
這就是管理器的配置。
Worker
工作者的配置將是類似的。讓我們建立一個WorkerConfiguration類。
@Configuration @Profile("worker") public class WorkerConfiguration { @Autowired private JobBuilderFactory jobBuilderFactory; @Autowired private RemotePartitioningWorkerStepBuilderFactory stepBuilderFactory; @Autowired private DataSource dataSource; @Bean public IntegrationFlow inboundFlow(ConsumerFactory<String, String> cf) { return IntegrationFlows .from(Kafka.messageDrivenChannelAdapter(cf, Constants.TOPIC_NAME)) .channel(inboundRequests()) .get(); } @Bean public QueueChannel inboundRequests() { return new QueueChannel(); } } |
幾個依賴關係,一個用於入站訊息的訊息通道,並將其與Spring Integration連線起來。
讓我們來建立一個工作步驟。
@Configuration @Profile("worker") public class WorkerConfiguration { // previous content is omitted for simplicity @Bean public Step simpleStep() { return stepBuilderFactory.get(Constants.WORKER_STEP_NAME) .inputChannel(inboundRequests()) .<Integer, Customer>chunk(100) .reader(itemReader(null)) .processor(itemProcessor()) .writer(itemWriter()) .build(); } } |
這將建立步驟定義,將其與入站訊息通道相連,並引用 ItemReader、ItemProcessor 和 ItemWriter 例項。這些看起來如下。
@Configuration @Profile("worker") public class WorkerConfiguration { // previous content is omitted for simplicity @Bean @StepScope public ItemReader<Integer> itemReader(@Value("#{stepExecutionContext['data']}") List<Integer> data) { List<Integer> remainingData = new ArrayList<>(data); return new ItemReader<>() { @Override public Integer read() { if (remainingData.size() > 0) { return remainingData.remove(0); } return null; } }; } } |
ItemReader是一個Bean,它將在Spring Batch執行上下文中的資料鍵下接收分割槽資料作為一個引數。請注意,必須在Bean定義上使用@StepScope,以便為該步驟啟用後期繫結。
實現很簡單。我們將把收到的ID儲存在一個本地列表中,在每個ItemReader呼叫期間,我們將從列表中刪除一個專案,直到沒有剩餘。
@Configuration @Profile("worker") public class WorkerConfiguration { // previous content is omitted for simplicity @Bean public ItemWriter<Customer> itemWriter() { return new JdbcBatchItemWriterBuilder<Customer>() .beanMapped() .dataSource(dataSource) .sql("INSERT INTO customers (id) VALUES (:id)") .build(); } @Bean public ItemProcessor<Integer, Customer> itemProcessor() { return new ItemProcessor<>() { @Override public Customer process(Integer item) { return new Customer(item); } }; } } |
ItemProcessor和ItemWriter則更簡單。ItemProcessor只是將ID轉換為Customer物件,模擬對DTO的某種處理,ItemWriter只是將Customers寫入資料庫。
客戶類是一個簡單的POJO,沒有什麼特別的。
public class Customer { private int id; public Customer(int id) { this.id = id; } public int getId() { return id; } } |
最後的配置步驟
接下來我們需要做的是用所需的分割槽數量建立Kafka主題,所以讓我們建立一個新的KafkaConfiguration類。
@Configuration public class KafkaConfiguration { @Bean public NewTopic topic() { return TopicBuilder.name(Constants.TOPIC_NAME) .partitions(Constants.TOPIC_PARTITION_COUNT) .build(); } } |
如果分割槽計數還不存在,這將自動建立一個主題。
接下來,我們需要建立資料庫結構來儲存我們的客戶,並允許Spring管理其狀態。讓我們在 src/main/resources/db/changelog 資料夾下建立一個 db.changelog-master.xml 檔案,內容如下。
<?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd"> <changeSet id="0001-initial" author="Arnold Galovics"> <createTable tableName="customers"> <column name="id" type="number"> </column> </createTable> <sqlFile path="classpath:/org/springframework/batch/core/schema-mysql.sql" relativeToChangelogFile="false"/> </changeSet> </databaseChangeLog> |
createTable很簡單,SQL檔案的匯入是由Spring Batch的核心模組提供的東西。
讓我們在application.properties中新增一些配置。
spring.datasource.url=jdbc:mysql://localhost:3306/db_example?createDatabaseIfNotExist=true spring.datasource.username=root spring.datasource.password=mysql spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml |
用Liquibase配置DataSource。然後是Kafka生產者的配置。
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.LongSerializer spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer spring.kafka.producer.group-id=producer-g |
這裡最重要的是使用JsonSerializer,這樣Spring Batch要傳送的訊息就會被編碼成JSON。
同樣地,消費者:
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.LongDeserializer spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer spring.kafka.consumer.group-id=consumer-g |
還有一件事:
spring.kafka.consumer.properties.spring.json.trusted.packages=*
執行
建立用於啟動應用程式的資訊庫。我將建立一個docker-compose.yml。
version: "3" services: zookeeper: image: 'bitnami/zookeeper:latest' ports: - '2181:2181' environment: - ALLOW_ANONYMOUS_LOGIN=yes kafka: image: 'bitnami/kafka:latest' ports: - '9092:9092' environment: - KAFKA_BROKER_ID=1 - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181 - ALLOW_PLAINTEXT_LISTENER=yes - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT,EXTERNAL:PLAINTEXT - KAFKA_CFG_LISTENERS=CLIENT://:9093,EXTERNAL://:9092 - KAFKA_CFG_ADVERTISED_LISTENERS=CLIENT://kafka:9093,EXTERNAL://localhost:9092 - KAFKA_CFG_INTER_BROKER_LISTENER_NAME=CLIENT depends_on: - zookeeper kafka-ui: image: provectuslabs/kafka-ui:latest ports: - '8080:8080' environment: - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9093 depends_on: - kafka mysql: image: mysql ports: - '3306:3306' command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: mysql |
我就不多說了。它啟動了一個Kafka代理,一個位於8080埠的Kafka UI例項,如果你想檢視主題的狀態,還有一個MySQL伺服器。
docker-compose up啟動一切。
完整程式碼可在GitHub 上獲得。
相關文章
- Ubunut擴充套件分割槽套件
- 擴充套件aix交換分割槽套件AI
- Java後端開發中的任務排程:使用Spring Batch實現批處理Java後端SpringBAT
- Linux主分割槽,擴充套件分割槽,邏輯分割槽Linux套件
- Hash分割槽表的使用及擴充套件套件
- linux下線上擴大擴充套件分割槽的方法Linux套件
- Linux主分割槽,擴充套件分割槽,邏輯分割槽[final]Linux套件
- 帶default分割槽的列表分割槽表的擴充套件套件
- 在 Linux 下使用 fdisk 擴充套件分割槽容量Linux套件
- Windows 8.1怎麼建立擴充套件分割槽?Windows套件
- centos 擴充套件root根分割槽的大小CentOS套件
- Linux LVM 擴充套件磁碟分割槽LinuxLVM套件
- Spring Boot 之 Spring Batch 批處理實踐Spring BootBAT
- 大資料——Scala擴充套件大資料套件
- .NET 開源 EF Core 批處理擴充套件工具,真好用套件
- 03 Windows批處理的作用域和延遲擴充套件Windows套件
- Linux 格式化擴充套件分割槽(Extended)Linux套件
- 擴充套件redhat linux as 5 的swap分割槽套件RedhatLinux
- Linux 擴充套件磁碟分割槽(命令列操作)Linux套件命令列
- 《Spring Batch 權威指南》之“批處理和 Spring”SpringBAT
- 海量資料處理_表分割槽
- PostgreSQL 原始碼解讀(98)- 分割槽表#4(資料查詢路由#1-“擴充套件”分割槽表)SQL原始碼路由套件
- Spring Batch 基本的批處理指導原則SpringBAT
- 批處理作業排程問題
- 如何在 Linux 中擴充套件 XFS 根分割槽Linux套件
- linux建立新分割槽擴充套件磁碟空間Linux套件
- DoorDash使用 Kafka 和 Flink 構建可擴充套件的實時事件處理Kafka套件事件
- SQL Server大分割槽表沒有空分割槽的情況下如何擴充套件分割槽的方法SQLServer套件
- 配置 Spring Batch 批處理失敗重試機制SpringBAT
- 一文輕鬆搞定批處理框架 Spring Batch框架SpringBAT
- 使用Kotlin擴充套件函式擴充套件Spring Data案例Kotlin套件函式Spring
- redhat linux swap分割槽擴充套件的三種方法RedhatLinux套件
- 批處理作業排程-分支界限法
- 圖片處理擴充套件 Grafika 的簡單使用套件
- 運維實戰:Linux系統擴充套件oracle資料庫所在的分割槽運維Linux套件Oracle資料庫
- 使用分割槽助手擴充C盤空間
- 使用 .NET Core 構建可擴充套件的實時資料處理系統套件
- 資料共享-spring batch(9)上下文處理SpringBAT