当我们的企业客户描述他们的数据管道挑战时,一个共同的主题浮现出来:他们需要在云端和本地环境中无缝衔接的实时数据处理能力。正因如此,灵活性对于 MinIO 的 AIStor 至关重要。
在本次技术深度探讨中,我将演示 AIStor 如何为需要支持从传统分析到尖端 AI/ML 工作负载的数据工程师创建统一的基础。我们将不介绍理论示例,而是研究一个基于 Kafka 的流式传输管道的实际实现,该管道同时处理两个关键功能:为数据科学家保存原始事件,同时为业务仪表板生成聚合洞察。最棒的是?无论您是在 AWS、Azure 还是您自己的数据中心运行,相同的代码都能无缝运行——无需再为了数据主权要求而牺牲功能。
此实现演示了 AIStor 如何实现基于 Java 的数据管道,无需复杂的基础架构即可处理数千万个 Kafka 事件。我们提供了完整的源代码,展示了如何将其构建为企业级解决方案,该解决方案能够同时保存合规性和数据科学所需的原始事件,并为 KPI 和仪表板生成预聚合数据集。该架构可从开发笔记本电脑线性扩展到生产集群,从而消除了灵活性、性能和部署位置之间的传统权衡。
数据管道的强大功能和灵活性
数据管道的设计方式多种多样,具体取决于您的具体需求。无论您需要流处理、批处理还是混合处理,您选择的架构都应该符合您的业务需求。今天我们将要研究的示例演示了一种简单而强大的 Kafka 事件数据处理方法,同时也适用于其他类似的流处理技术。
了解此示例管道
该管道展示了数据处理中的一种常见模式:从消息系统(Kafka)消费事件,将其以原始形式存储以供将来参考,并同时执行聚合以提取即时见解。
让我们直观地看一下整体架构:
该代码库的主要组成部分如下:
先决条件
- x86 Linux操作系统/mac操作系统
- 运行 Docker 引擎(colima)
- docker cli
- docker-compose 命令行
- JDK 17+
- maven
- git
- curl
配置参数
配置参数在以下文件中:
1、src/main/java/com/minio/training/labs/pipeline/config/AppConfig.java:
eventsToGenerate:要生成的合成事件的数量
maxPollRecords:一次从 Kafka 轮询并写入 MinIO Raw 和 Aggregate Dataset 的最大记录数
2、src/main/java/com/minio/training/labs/pipeline/processor/DirectKafkaEventProcessor.java:
POLL_TIMEOUT_MS:Kafka 消费者轮询的超时时间(以毫秒为单位)
FLUSH_THRESHOLD:在写入 MinIO 聚合数据集之前,聚合器生成的最小数据颗粒数
关键组件说明
数据生成
user_id此管道始于数据生成器,它根据预定义的模式创建合成事件。我们使用一个包含、country和action,等字段的电子商务模式进行演示value。
SchemaDefinition下面是一个事件示例,其中生成了数千万个具有基数的事件。
{
"eventGuid": "67f4dfdd-c4db-4f8d-aa90-24da30f760ac",
"timestamp": 1742199657865,
"country": "country_20",
"is_returning": true,
"user_id": "user_id_694",
"action": "action_3",
"device_type": "device_type_1",
"page": "page_9",
"category": "category_13",
"value": 39.259011259415466,
"session_duration": 243
}
这就是事件产生的方式。
public Event generateEvent() {
Map<String, Object> fields = new HashMap<>();
// Generate values for each column in the schema
for (SchemaDefinition.ColumnDefinition column : schema.getColumns()) {
fields.put(column.getName(), column.generateValue(random));
}
return new Event(fields);
}
Kafka 集成
事件被发送到 Kafka,它作为中央消息系统,负责KafkaEventProducer处理此事。
public void sendEvent(Event event) {
try {
String json = objectMapper.writeValueAsString(event);
ProducerRecord<String, String> record = new ProducerRecord<>(
config.getKafka().getTopic(),
event.getEventGuid(),
json
);
producer.send(record, (metadata, exception) -> {
if (exception != null) {
log.error("Error sending event to Kafka", exception);
} else {
profiler.recordProducerEvent();
}
});
} catch (Exception e) {
log.error("Error serializing event", e);
}
}
事件处理和 MinIO 的 AIStor 集成
该管道的核心是DirectKafkaEventProcessor,它使用来自 Kafka 的事件并执行两个主要操作:
1、原始事件存储:以原始格式存储事件以供将来参考或详细分析
2、实时聚合:创建数据聚合视图以提供即时洞察
事件被消费后,会根据 进行批处理batchSize并存储到 AiStor 中。对于实时聚合,事件也会进行聚合处理,使用eventGuid滑动窗口检查重复数据,将其分组到单维度或多维度,然后写入聚合缓冲区。达到 后FLUSH_THRESHOLD,缓冲区将链接到 AIStor 并清空,以处理下一组事件。
AIStor 集成使此存储过程变得非常简单。以下是我们写入原始事件的方法:
private void writeRawEventsToJson(List<Event> events, String timestamp) {
if (events.isEmpty()) {
return;
}
try {
// Include UUID in filename to ensure uniqueness
String filename = String.format("events_%s_%s.json",
timestamp, UUID.randomUUID().toString().substring(0, 8));
// Partition by year/month/day/hour for efficient querying
String s3Key = String.format("raw-json/%s/%s/%s/%s/%s",
"year=" + timestamp.substring(0, 4),
"month=" + timestamp.substring(5, 7),
"day=" + timestamp.substring(8, 10),
"hour=" + timestamp.substring(11, 13),
filename);
String jsonContent = objectMapper.writeValueAsString(events);
byte[] content = jsonContent.getBytes(StandardCharsets.UTF_8);
// Simple AIStor/S3 PUT operation
s3Client.putObject(PutObjectRequest.builder()
.bucket(config.getS3().getS3Bucket())
.key(s3Key)
.contentType("application/json")
.build(),
RequestBody.fromBytes(content));
log.debug("Wrote {} raw events to MinIO: {}", events.size(), s3Key);
} catch (Exception e) {
log.error("Failed to write raw events to MinIO", e);
}
}
对于聚合事件:
private void writeAggregatedEventsToJson(List<AggregatedEvent> aggregatedEvents, String timestamp) {
// Similar to raw events, but with processed aggregated data
// ...
s3Client.putObject(PutObjectRequest.builder()
.bucket(config.getS3().getS3Bucket())
.key(s3Key)
.contentType("application/json")
.build(),
RequestBody.fromBytes(content));
// ...
}
使用 MinIO 的 AIStor 简化存储
该管道的一大亮点是通过 AIStor 轻松存储数据。这意味着该管道无需更改代码即可处理不断增长的数据量。
连接 AIStor 的配置很简单:
AwsBasicCredentials credentials = AwsBasicCredentials.create(
config.getS3().getS3AccessKey(),
config.getS3().getS3SecretKey());
s3Client = S3Client.builder()
.endpointOverride(URI.create(config.getS3().getS3Endpoint()))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.US_EAST_1)
.httpClient(UrlConnectionHttpClient.builder().build())
.forcePathStyle(true)
.build();
实时聚合
此管道演示了一种简单但功能强大的实时聚合模式。当事件流经系统时,它们会按各种维度进行聚合,time window例如countryactioncategory.
private void processEventForAggregation(Event event) {
// Skip if event was already processed within our deduplication window
if (!processedEventGuids.add(event.getEventGuid())) {
return;
}
LocalDateTime eventTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(event.getTimestamp()),
ZoneOffset.UTC
);
LocalDateTime windowStart = eventTime.truncatedTo(ChronoUnit.HOURS);
String userId = event.getFields().getOrDefault(
"user_id", "unknown").toString();
for (Map.Entry<String, Object> field : event.getFields().entrySet()) {
String fieldName = field.getKey();
if (!fieldName.equals("country") && !fieldName.equals("device_type") &&
!fieldName.equals("action") && !fieldName.equals("page") &&
!fieldName.equals("category")) {
continue;
}
Object fieldValue = field.getValue();
if (fieldValue == null) {
continue;
}
// single dimension
String key = String.format("%s::%s:%s",
windowStart.format(pathFormatter),
fieldName, fieldValue
);
// multi dimension
// String key = String.format("%s::%s:%s::%s:%s::%s:%s::%s:%s::%s:%s",
// windowStart.format(pathFormatter),
// "country", event.getFields().get("country"),
// "device_type", event.getFields().get("device_type"),
// "action", event.getFields().get("action"),
// "page", event.getFields().get("page"),
// "category", event.getFields().get("category")
// );
// Update aggregations...
}
}
这种方法如何运作
1、双重存储策略:通过存储原始数据和汇总数据,您可以获得两全其美的效果,即用于审计或深度分析的详细记录,以及通过聚合快速获取见解。
2、灵活性:无需改变核心架构,代码就可以扩展以支持不同的聚合维度或额外的处理步骤。
3、可扩展性:随着数据量的增长,系统可以通过添加更多 Kafka 分区或增加处理能力进行水平扩展。
虽然此示例展示了一种特定的实现,但相同的架构可以适用于各种工作负载
高速交易数据:通过调整聚合窗口和缓冲策略
- 物联网传感器数据:通过修改模式并添加时间序列特定的聚合
- 用户行为分析:通过实现更复杂的基于会话的聚合
- 支付处理:通过添加交易验证规则和实时聚合来检测欺诈,同时保留合规的原始交易记录和财务报告的汇总数据。
- 日志数据处理:通过实现各种日志格式的解析器,并添加错误率、性能指标和安全事件的实时聚合,并基于时间进行分区,以支持即时警报和历史分析。
在生产环境中运行管道
该数据管道旨在跨各种环境灵活部署,通过容器化可以轻松地从开发转移到生产。
Java 运行时环境
核心管道是用 Java 实现的,具有以下几个优点
- 跨平台兼容性:在不同的操作系统上一致运行
- 丰富的生态系统:利用成熟的库实现 Kafka、AIStor 集成
- 企业就绪:支持监控、指标收集和日志记录集成
容器化部署
整个管道[Code]使用Docker进行容器化,简化了部署并确保了一致性。
./run_pipeline.sh
这个脚本
-
构建 Java 应用程序
-
启动基础设施容器(Kafka、AIStor/MinIO、Prometheus、Grafana)
-
部署数据处理容器
-
设置监控服务
-
Docker Compose 配置句柄
- 容器网络
- 持久存储的卷管理
- 配置的环境变量注入
- 服务依赖关系和启动顺序
凭借一致的行为和性能特征,该管道可以部署在各种环境中,从本地开发机器到生产 Kubernetes 集群。
结论
构建数据管道并不复杂。借助 AIStor 和 Kafka,您可以快速实现一个同时处理原始数据存储和实时聚合的解决方案。本示例仅演示了一种处理 Kafka 事件的方法,但相同的原理可以应用于各种数据处理需求。
Kafka 用于消息传递,AIStor 用于存储,两者的结合为构建可扩展、高弹性的数据管道奠定了坚实的基础,并可随着您的业务需求而增长。通过分离数据提取、处理和存储的关注点,您可以创建一个更易于维护和演进的系统。无论您是刚开始使用数据管道,还是希望优化现有解决方案,都可以将此架构视为一个可根据您的特定需求进行定制的模板。