实时数据流处理:水印、触发器与数据存储选择
1. 水印和触发器
在实时数据处理中,Beam 编程模型能隐式处理滑动窗口内的乱序记录,并默认处理延迟到达的记录。这里引入了“水印”的概念,它代表管道中未处理的最旧记录,是实时数据处理系统的固有属性,体现了管道中的延迟情况,Cloud Dataflow 会随时间跟踪并学习这种延迟。
1.1 水印的类型
- 基于 Cloud Pub/Sub 插入时间的水印 :若以记录插入 Cloud Pub/Sub 的时间作为事件时间,水印能严格保证管道不会观察到事件时间更早的数据。
- 用户指定事件时间的水印 :当用户通过指定时间戳标签来指定事件时间时,无法阻止发布程序向 Cloud Pub/Sub 插入旧记录,此时水印是基于观察到的历史延迟得出的经验值。水印的概念不仅适用于 Cloud Pub/Sub,对于间歇性的流数据源(如低功耗物联网设备),水印也有助于处理延迟问题。
1.2 触发器的作用
聚合统计的计算由“触发器”驱动,触发器触发时,管道会进行计算。管道中可以有多个触发器,通常每个触发器都与水印相关。默认触发器为
Repeatedly.forever(AfterWatermark.pastEndOfWindow())
,意味着当水印超过窗口结束时间,且有延迟数据到达时,触发器就会触发,即每个延迟到达的记录都会单独处理,这种方式优先保证计算的正确性,但可能会牺牲一定的性能。
1.3 抖动对水印的影响
在模拟中添加均匀分布的抖动时,由于均匀延迟范围在 90 到 120 秒之间,最早和最晚到达记录的延迟差为 30 秒,因此 Cloud Dataflow 需要将窗口多保持 30 秒。通过 Cloud Platform Console 上的 Cloud Dataflow 作业监控网页可以查看学习到的水印值,点击任何转换步骤即可查看该值。
而对于指数分布的抖动,小延迟比大延迟更常见。在 Cloud Pub/Sub 管道的模拟数据中添加指数分布的抖动后,学习到的水印值为 21 秒。
1.4 调整触发器的权衡
默认触发器优先考虑正确性,逐个处理延迟记录并更新聚合结果。不过,我们可以轻松改变这种权衡。例如:
.triggering(Repeatedly.forever(
AfterWatermark.pastEndOfWindow()
.withLateFirings(
AfterPane.elementCountAtLeast(10))
.orFinally(AfterProcessingTime.pastFirstElementInPane()
.plusDelayOf(Duration.standardMinutes(30)))))
在这个新的触发器设置中,计算仍在水印处触发。延迟记录每 10 条一批进行处理,但前提是它们在窗口开始后的 30 分钟内到达,超过这个时间的延迟记录将被丢弃。
1.5 水印和触发器的关系流程图
graph LR
A[水印推进] --> B{是否超过窗口结束时间}
B -- 是 --> C{是否有延迟数据到达}
C -- 是 --> D[默认触发器触发]
C -- 否 --> E[等待延迟数据]
B -- 否 --> E
F[新触发器设置] --> G[水印触发计算]
G --> H{延迟记录是否在30分钟内到达}
H -- 是 --> I[每10条一批处理延迟记录]
H -- 否 --> J[丢弃延迟记录]
2. 事务、吞吐量和延迟
将输出的航班记录流式传输到 BigQuery 适用于航班延迟场景,但对于其他数据管道可能并非最佳选择。选择输出接收器时,应考虑四个因素:访问模式、事务、吞吐量和延迟。
2.1 选择输出接收器的因素
- 访问模式 :若主要是长期存储和延迟访问数据,可将数据流式传输到 Cloud Storage 上的分片文件,这些文件可作为后续导入 Cloud SQL 或 BigQuery 进行数据分析的临时存储。在这里,假设需要近实时查询数据。
- 事务性 :对于每个航班,会收到多个事件(如起飞、离地等)。需要考虑是否为每个航班创建一行以反映其最新状态,还是仅进行追加存储;读者是否可以接受稍微过时的记录,还是系统必须有单一的真实源和一致的行为。这些问题的答案决定了航班更新是否需要事务处理,还是可以在仅提供最终一致性保证的环境中进行。
- 吞吐量 :需要了解每秒有多少航班事件,以及这个速率是否恒定,是否有高峰活动期。这些因素决定了系统需要处理的吞吐量。
- 延迟 :如果提供最终一致性,需要确定可接受的延迟时间,即航班数据添加到数据库后,所有读者应在多长时间内看到新数据。目前,流式传输到 BigQuery 支持每秒最多 100,000 个事件,延迟约为几秒。对于吞吐量需求更高或延迟要求更低的情况,需要考虑其他解决方案。
2.2 可能的流式输出接收器
| 输出接收器 | 适用场景 | 特点 |
|---|---|---|
| BigQuery | 每秒处理数万条记录,延迟为几秒的场景,适用于许多仪表盘应用 | 完全托管的数据仓库,支持 SQL 查询 |
| Cloud Storage 上的文本文件 | 主要用于数据保存,或可接受每小时批量更新到 BigQuery 的场景 | 可按小时分片,成本低于直接流式传输到 BigQuery |
| Cloud Bigtable | 处理涉及数百 PB 数据、每秒数百万读写操作、延迟为毫秒级的工作负载 | 可大规模扩展的 NoSQL 数据库服务,吞吐量与节点数量线性相关,能自动重新平衡数据 |
| Cloud SQL | 频繁更新、低吞吐量、中等规模的数据,需要从多种工具和编程语言近实时访问的场景 | 由 MySQL 或 PostgreSQL 支持,关系型技术普遍,工具生态系统强大 |
| Cloud Datastore | 需要处理大规模数据集(数 TB 数据),避免层次对象与扁平关系表转换问题的场景 | NoSQL 对象存储,设计为最终一致性,在某些查询中可实现强一致性 |
| Cloud Spanner | 需要强一致性、事务处理、SQL 查询能力,且数据可全球访问、能扩展到极大规模的场景 | 提供毫秒级延迟、极高的可用性,完全托管,无需手动复制或维护 |
在航班延迟场景中,不需要事务处理,每秒输入流少于 1000 个事件,插入数据库到应用程序获取航班延迟信息的延迟为几秒是可以接受的。BigQuery 完全托管,有许多数据可视化和报表创建工具支持,且成本相对较低,因此是合适的选择。
3. Cloud Bigtable 介绍
假设每秒有数十万条航班事件,且延迟要求为毫秒级(如飞机在飞行中提供最新坐标信息用于空中交通管制),Cloud Bigtable 是更好的选择。
3.1 Cloud Bigtable 的存储结构
Cloud Bigtable 将计算和存储分离,表被分片为连续行的块,称为“表片”。Cloud Bigtable 实例不直接存储表片,而是存储指向一组表片的指针,表片本身持久存储在 Cloud Storage 中。这样,即使节点出现故障,数据仍存储在 Cloud Storage 中,工作可以重新分配到其他节点,只需复制元数据。
3.2 Cloud Bigtable 的数据特点
- 数据结构 :数据是排序的键值对映射,每行有一个唯一键。与 BigQuery 不同,Cloud Bigtable 按行存储,行按键值排序。
- 列族 :相关的列被分组到“列族”中,不同的列族通常由不同的应用程序管理。在列族内,列有唯一的名称。
- 时间序列记录 :特定行的特定列值可以在不同时间戳包含多个单元格,表是追加式的,所有值都存储在表中,这样可以维护单元格值随时间的时间序列记录。对于大多数情况,Cloud Bigtable 不关心数据类型,所有数据都被视为原始字节字符串。
3.3 Cloud Bigtable 的性能优化
Cloud Bigtable 表的性能与表片中的行排列有关,行按键排序。为了优化写入性能,希望多个写入操作并行进行,让每个 Cloud Bigtable 实例写入自己的表片集合,这要求行键不遵循可预测的顺序。而读取性能方面,若能同时读取多行则更高效。因此,在设计 Cloud Bigtable 模式时,关键是要在写入分布和读取集中之间找到平衡。
3.4 Cloud Bigtable 的表设计
3.4.1 短宽表设计
短宽表利用了 Cloud Bigtable 的稀疏性,通过列的存在或缺失来表示是否有对应的数据。例如,在汽车工厂场景中,若要查询特定汽车的零部件属性(如零件 ID、供应商、制造地点等),可以使用汽车序列号作为行键,每个独特的零件(如火花塞)对应一个列。由于无值的单元格不占用空间,不用担心随着新车型的引入列的数量会无限增加。为了提高写入效率,汽车序列号应从生产线编号开始,这样写入操作可以并行在不同表片上进行。同时,诊断应用程序在查询特定生产线特定日期生产的所有车辆时,能高效地读取连续行;服务中心查询特定车辆的所有零件时,只需读取一行,读取性能也很高。
3.4.2 高窄表设计
高窄表通常每行只存储一个事件。每个航班事件可以流式传输到 Cloud Bigtable 的新行中,这样可以记录每个航班的多个状态(如起飞、离地等)及其历史记录。每个航班约有 20 个字段,都可以属于同一个列族,使得流式更新简单直观。
3.5 行键设计
为了使高窄表的读写都高效,行键设计很关键。常见的查询可能涉及特定航空公司在特定机场之间的近期航班(如今天 AS 航空公司在 SEA 和 SJC 之间的航班状态)。为了实现高效查询,行键应包含出发地、目的地机场和航空公司信息,即
ORIGIN#DEST#CARRIER
。这样的设计有助于优化写入性能,虽然繁忙机场(如亚特兰大)对应的表片可能会出现热点问题,但大量以字母 A 开头的小机场可以平衡负载。
同时,为了提高扫描性能,应将最新数据存储在表的“顶部”。Timestamp 不能直接按常规方式使用,应将其转换为自 1970 年以来的毫秒数,然后用最大可能的长整型值减去该毫秒数,即
LONG_MAX - millisecondsSinceEpoch
。时间戳应放在行键的末尾,避免写入集中在一个表片上。最终行键格式为
ORIGIN#DEST#CARRIER#ReverseTimeStamp
,这里使用航班的预定出发时间作为时间戳,以避免因出发延迟导致键不同的问题。
3.6 流式传输到 Cloud Bigtable
以下是将航班事件流式传输到 Cloud Bigtable 的具体步骤:
3.6.1 创建 Cloud Bigtable 实例
使用
gcloud
命令创建实例:
gcloud beta bigtable \
instances create flights \
--cluster=datascienceongcp --cluster-zone=us-central1-b \
--description="Chapter 10" --instance-type=DEVELOPMENT
这里创建了名为
flights
的实例,集群名为
datascienceongcp
,选择开发实例类型可以降低成本,因为集群不进行复制或全球可用。
3.6.2 在代码中引用实例和表
在 Cloud Dataflow 代码中,需要在项目 ID 的上下文中引用实例和表:
private static String INSTANCE_ID = "flights";
private String getInstanceName(MyOptions options) {
return String.format("projects/%s/instances/%s", options.getProject(), INSTANCE_ID);
}
private static String TABLE_ID = "predictions";
private String getTableName(MyOptions options) {
return String.format("%s/tables/%s", getInstanceName(options), TABLE_ID);
}
private static final String CF_FAMILY = "FL";
3.6.3 创建空表
Table.Builder tableBuilder = Table.newBuilder();
ColumnFamily cf = ColumnFamily.newBuilder().build();
tableBuilder.putColumnFamilies(CF_FAMILY, cf);
BigtableSession session = new BigtableSession(optionsBuilder
.setCredentialOptions( CredentialOptions.credential(
options.as( GcpOptions.class
).getGcpCredential())).build()));
BigtableTableAdminClient tableAdminClient = session.getTableAdminClient();
CreateTableRequest.Builder createTableRequestBuilder =
CreateTableRequest.newBuilder()
.setParent(getInstanceName(options))
.setTableId(TABLE_ID).setTable(tableBuilder.build());
tableAdminClient.createTable(createTableRequestBuilder.build());
3.6.4 将航班预测转换为突变
PCollection<FlightPred> preds = ...;
BigtableOptions.Builder optionsBuilder =
new BigtableOptions.Builder()
.setProjectId(options.getProject())
.setInstanceId(INSTANCE_ID)
.setUserAgent("datascience-on-gcp");
createEmptyTable(options, optionsBuilder);
PCollection<KV<ByteString, Iterable<Mutation>>> mutations = toMutations(preds);
mutations.apply("write:cbt",
BigtableIO.write()
.withBigtableOptions(optionsBuilder.build())
.withTableId(TABLE_ID));
3.6.5 创建行键和突变
FlightPred pred = c.element();
String key = pred.flight.getField(INPUTCOLS.ORIGIN)
+ "#" + pred.flight.getField(INPUTCOLS.DEST)
+ "#" + pred.flight.getField(INPUTCOLS.CARRIER)
+ "#" + (Long.MAX_VALUE - pred.flight.getFieldAsDateTime(
INPUTCOLS.CRS_DEP_TIME).getMillis());
List<Mutation> mutations = new ArrayList<>();
long ts = pred.flight.getEventTimestamp().getMillis();
for (INPUTCOLS col : INPUTCOLS.values()) {
addCell(mutations, col.name(), pred.flight.getField(col), ts);
}
if (pred.ontime >= 0) {
addCell(mutations, "ontime",
new DecimalFormat("0.00").format(pred.ontime), ts);
}
c.output(KV.of(ByteString.copyFromUtf8(key), mutations));
3.6.6 转换 Java 类型为字节
void addCell(List<Mutation> mutations, String cellName,
String cellValue, long ts) {
if (cellValue.length() > 0) {
ByteString value = ByteString.copyFromUtf8(cellValue);
ByteString colname = ByteString.copyFromUtf8(cellName);
Mutation m =
Mutation.newBuilder().setSetCell(
Mutation.SetCell.newBuilder()
.setValue(value)
.setFamilyName(CF_FAMILY)
.setColumnQualifier(colname)
.setTimestampMicros(ts)
).build();
mutations.add(m);
}
}
通过以上步骤,可以将管道中的航班预测流式传输到 Cloud Bigtable。
综上所述,在实时数据处理中,合理运用水印和触发器可以优化数据处理的正确性和性能,同时根据不同的业务需求选择合适的输出接收器和数据库,能够确保数据处理的高效性和可靠性。对于需要高吞吐量和低延迟的场景,Cloud Bigtable 是一个强大的选择,但需要精心设计表结构和行键以达到最佳性能。
4. 不同数据存储方案对比总结
4.1 存储方案对比表格
| 存储方案 | 事务支持 | 吞吐量 | 延迟 | 适用场景 |
|---|---|---|---|---|
| BigQuery | 弱(流式写入无严格事务) | 每秒最多 100,000 个事件 | 几秒 | 航班延迟等对事务要求不高、吞吐量适中、可接受几秒延迟的场景 |
| Cloud Storage 文本文件 | 无 | 取决于后续处理 | 高(按小时批量处理) | 数据保存、后续批量导入分析的场景 |
| Cloud Bigtable | 弱(部分操作支持原子性) | 可线性扩展,支持数百万读写/秒 | 毫秒级 | 高吞吐量、低延迟要求的场景,如空中交通管制 |
| Cloud SQL | 强 | 低 - 中等 | 中等 | 频繁更新、低吞吐量、需要多种工具访问的场景 |
| Cloud Datastore | 部分支持(实体组内) | 高 | 中等 | 大规模数据集、需要一定事务支持的场景 |
| Cloud Spanner | 强 | 高 | 毫秒级 | 强一致性、高可用性、全球可扩展的场景 |
4.2 选择存储方案的决策流程图
graph LR
A[数据处理需求] --> B{是否需要事务处理}
B -- 是 --> C{吞吐量要求}
C -- 低 - 中等 --> D[Cloud SQL]
C -- 高 --> E{是否需要全球扩展}
E -- 是 --> F[Cloud Spanner]
E -- 否 --> G[Cloud Datastore]
B -- 否 --> H{吞吐量要求}
H -- 低 - 中等 --> I{延迟要求}
I -- 几秒 --> J[BigQuery]
I -- 高(按小时) --> K[Cloud Storage 文本文件]
H -- 高 --> L{延迟要求}
L -- 毫秒级 --> M[Cloud Bigtable]
L -- 几秒 --> J
5. 实时数据处理的最佳实践建议
5.1 水印和触发器的使用建议
- 理解业务需求 :根据业务对数据正确性和处理性能的要求,选择合适的触发器。如果对数据准确性要求极高,可使用默认触发器;如果能接受一定的数据丢失以换取性能提升,可采用自定义的触发器设置。
- 监控水印 :通过 Cloud Dataflow 作业监控网页实时查看水印值,了解管道的延迟情况,及时调整系统参数。
- 测试抖动影响 :在模拟环境中添加不同类型的抖动(均匀分布、指数分布等),观察水印和触发器的行为,评估系统的稳定性和性能。
5.2 输出接收器选择建议
- 明确业务场景 :仔细分析业务的访问模式、事务需求、吞吐量和延迟要求,根据这些因素选择最合适的输出接收器。
- 考虑成本 :不同的存储方案成本不同,如 BigQuery 相对较为经济,而 Cloud Spanner 成本较高。在满足业务需求的前提下,选择成本效益最高的方案。
- 进行性能测试 :在实际部署前,对不同的存储方案进行性能测试,比较它们在吞吐量、延迟等方面的表现,确保系统能够满足业务的实时性要求。
5.3 Cloud Bigtable 使用建议
- 精心设计表结构 :根据业务查询模式,选择短宽表或高窄表设计,合理划分列族,提高数据存储和查询的效率。
- 优化行键设计 :行键的设计直接影响读写性能,要确保行键包含关键查询信息,并采用合适的时间戳处理方式,避免热点问题。
- 监控和调整 :定期监控 Cloud Bigtable 的性能指标,如读写吞吐量、延迟等,根据监控结果调整实例的配置和表结构。
6. 总结与展望
6.1 总结
实时数据处理在现代业务中扮演着越来越重要的角色,水印和触发器是处理乱序和延迟数据的关键机制,合理运用它们可以平衡数据处理的正确性和性能。在选择输出接收器时,需要综合考虑访问模式、事务、吞吐量和延迟等因素,不同的存储方案适用于不同的业务场景。Cloud Bigtable 作为一种高性能的存储解决方案,在高吞吐量、低延迟要求的场景中具有显著优势,但需要精心设计表结构和行键。
6.2 展望
随着数据量的不断增长和业务需求的日益复杂,实时数据处理技术将不断发展。未来,可能会出现更智能的水印和触发器算法,能够自动适应不同的业务场景和数据特征。同时,存储方案也将不断优化,提供更高的吞吐量、更低的延迟和更强的事务支持。此外,实时数据处理与人工智能、机器学习等技术的结合将更加紧密,为业务决策提供更及时、准确的支持。
在实际应用中,开发者需要不断学习和掌握新的技术和方法,根据业务需求灵活选择和组合各种工具和技术,以构建高效、可靠的实时数据处理系统。
超级会员免费看
866

被折叠的 条评论
为什么被折叠?



