Análisis de puntos de función central de Apache Hudi

Malo

Parte del código de este artículo corresponde a la versión 0.14.0

Antecedentes de desarrollo

El requisito inicial es que Uber tendrá muchos escenarios de actualización a nivel récord. Uno de los principales escenarios de Hudi dentro de Uber es la comparación entre los pasajeros que realizan pedidos de taxi y los conductores que reciben pedidos. Los pasajeros y los conductores son dos flujos de datos respectivamente. A través de Upsert de Hudi, la capacidad y La función de lectura incremental puede unir estos dos flujos de datos en el nivel de minutos para obtener datos coincidentes entre pasajero y conductor.
Para mejorar la puntualidad de las actualizaciones, se propone un nuevo marco como una solución incremental casi en tiempo real.
 

imagen.png


También se puede ver por el nombre Hadoop Upsert and Incremental que la función principal de Hudi son las capacidades incrementales y de inserción, que se construyen sobre Hadoop.

Estructura general

imagen.png

imagen.png


Puntos de función central

Admite actualización y eliminación

Indexación | Apache Hudi
utiliza principalmente tecnología de indexación para lograr operaciones de inserción y eliminación eficientes. La clave de sudadera con capucha (clave de registro) de un registro se puede asignar a una ID de archivo a través del índice, y luego el método de inserción para actualizar y eliminar entradas se determina según el tipo de tabla y el tipo de datos escritos.

Tipo de índice

  1. BloomFilter se implementa de forma predeterminada. De forma predeterminada, cada vez que se confirma un archivo, el filtro de floración creado por la clave contenida en el archivo y el rango de la clave se escriben en el pie de página del archivo de parquet.
  2. Índice global de HBase, dependiente del clúster externo
  3. Índice simple (consulta si el archivo correspondiente existe según el campo clave)
  4. El índice de depósitos se divide primero en depósitos y luego se aplica un hash para resolver el problema de la baja eficiencia del índice de filtro de floración en escenarios a gran escala.

Índice de cubeta


 

Clase de implementación de índice


Los tipos de índice también se dividen en globales y no globales. BloomFilter Index y Simple Index tienen opciones globales. HBase es naturalmente la opción global. El índice global garantizará la unicidad de las claves en la partición global y el costo será mayor.

imagen.png

imagen.png

Odps/MaxCompute también admite actualización y eliminación
Cómo actualizar o eliminar datos_El servicio de computación de big data nativo de la nube MaxCompute-Alibaba Cloud Help Center
también se implementa utilizando la idea de archivo base + registro delta

Hive3.0 también admite actualización, eliminación y semántica ACID
Ejecución de Apache Hive 3, nuevas funciones, consejos y trucos | Adaltas
Hive Transactions - Apache Hive - Apache Software Foundation

La diferencia es que Hudi admite la inserción de tablas de datos, lo que significa que puede garantizar la unicidad de la clave primaria de los datos al escribir, mientras que odps y hive solo deberían admitir la actualización de datos mediante actualizaciones y eliminaciones de declaraciones dml, y los escenarios de cobertura son diferentes. Este último solo debe usarse en escenarios de corrección de datos. Como opción para ingresar al lago, aún necesita admitir upsert de forma natural.

Soporte de transacciones ACID

Creo que el soporte de transacciones es la parte central de Hudi, porque las actualizaciones y eliminaciones de datos dependen en gran medida de las capacidades de transacción. Los almacenes de datos tradicionales solo proporcionan semántica de inserción y los archivos solo se pueden agregar. La demanda de garantías de transacciones será mucho más débil, y la mayoría es leído. Llegaron datos incompletos (la adición se produjo después de escribir los datos de la partición).
Sin embargo, cuando es necesario admitir la semántica de actualización y eliminación, la demanda de garantías de transacción será mucho mayor, por lo que se puede ver que para habilitar las capacidades de actualización y eliminación de tablas en hive y odps, primero debe habilitar los atributos de transacción de la tabla.
La implementación de transacciones en hudi
**MVCC ** logra el aislamiento de instantáneas entre múltiples escritores y lectores a través del mecanismo mvcc.

imagen.png

Control de concurrencia optimista de OCC
De forma predeterminada, Hudi considera la escritura por un solo escritor, en este caso el rendimiento es el máximo. Si hay varios escritores, debe habilitar el control de simultaneidad para varios escritores.

 
 
hoodie.write.concurrency.mode=optimistic_concurrency_control
# 指定锁的实现 默认是基于filesystem 的锁机制(要求filesystem能提供原子性的创建和删除保障)
hoodie.write.lock.provider=<lock-provider-classname>

Admite control de concurrencia optimista en la granularidad del archivo. Cuando se completa la escritura y la confirmación, si occ está activado, el bloqueo se adquirirá primero y luego se confirmará. Parece que este bloqueo es un bloqueo con granularidad global. Tome el bloqueo del sistema de archivos como ejemplo del
proceso de confirmación.

 
 
protected void autoCommit(Option<Map<String, String>> extraMetadata, HoodieWriteMetadata<O> result) {
final Option<HoodieInstant> inflightInstant = Option.of(new HoodieInstant(State.INFLIGHT,
getCommitActionType(), instantTime));
// 开始事务,如果是occ并发模型,会获取锁
this.txnManager.beginTransaction(inflightInstant,
lastCompletedTxn.isPresent() ? Option.of(lastCompletedTxn.get().getLeft()) : Option.empty());
try {
setCommitMetadata(result);
// reload active timeline so as to get all updates after current transaction have started. hence setting last arg to true.
// 尝试解冲突,冲突判定的策略是可插拔的,默认是变更的文件粒度查看是否有交集. 目前冲突的文件更改是无法处理的,会终止commit请求
TransactionUtils.resolveWriteConflictIfAny(table, this.txnManager.getCurrentTransactionOwner(),
result.getCommitMetadata(), config, this.txnManager.getLastCompletedTransactionOwner(), true, pendingInflightAndRequestedInstants);
commit(extraMetadata, result);
} finally {
this.txnManager.endTransaction(inflightInstant);
}
}

Proceso de adquisición de cerraduras

 
 
@Override
public boolean tryLock(long time, TimeUnit unit) {
try {
synchronized (LOCK_FILE_NAME) {
// Check whether lock is already expired, if so try to delete lock file
// 先检查lock file 是否存在,默认路径是 base/.hoodie/lock 也就是所有的commit操作都会操作这个文件
if (fs.exists(this.lockFile)) {
if (checkIfExpired()) {
fs.delete(this.lockFile, true);
LOG.warn("Delete expired lock file: " + this.lockFile);
} else {
reloadCurrentOwnerLockInfo();
return false;
}
}
// 如果文件不存在,则获取锁,创建文件
acquireLock();
return fs.exists(this.lockFile);
}
} catch (IOException | HoodieIOException e) {
// 创建时可能会发生失败,则返回false获取锁失败
LOG.info(generateLogStatement(LockState.FAILED_TO_ACQUIRE), e);
return false;
}
}

Si los archivos modificados por las dos solicitudes de escritura no se superponen, se pasarán directamente en la fase de resolución de conflictos. Si hay superposición, la escritura confirmada más tarde fallará y se revertirá.

Diseños de archivos

VACA

MOR

  • Una tabla corresponde al directorio base de un archivo distribuido.
  • Los archivos de cada partición se organizan según grupos de archivos y cada grupo de archivos corresponde a un ID de archivo.
  • Cada grupo de archivos contiene múltiples segmentos de archivos.
  • Cada segmento tiene un archivo base (archivo parquet) y un conjunto de archivos delta de archivos .log

El archivo base es el archivo principal que almacena conjuntos de datos de Hudi y se almacena en formato de columnas, como Parquet. El formato es

 
 
<fileId>_<writeToken>_<instantTime>.parquet

El archivo de registro es un archivo utilizado para almacenar datos modificados en la tabla MOR. A menudo también se le llama registro Delta. El archivo de registro no existirá de forma independiente. Debe estar subordinado a un archivo base en formato Parquet. Un archivo base y varios archivos subordinados para El archivo de registro consta de un segmento de archivo.

 
 
.<fileId>_<baseCommitTime>.log.<fileVersion>_<writeToken>

File Slice, en la tabla MOR, una colección de archivos que consta de un archivo base y varios archivos de registro subordinados a él se denomina File Slice. File Slice es un concepto específico para la tabla MOR, para la tabla COW, dado que no genera un archivo de registro, el File Silce solo contiene un archivo base, o cada archivo base es un File Silce independiente.

FileId相同的文件属于同一个File Group。同一File Group下往往有多个不同版本(instantTime)的Base File(针对COW表)或Base File + Log File的组合(针对MOR表),当File Group内最新的Base File迭代到足够大( >100MB)时,Hudi就不会在当前File Group上继续追加数据了,而是去创建新的File Group。

这里面可以看到根据大小上下限来决定是否创建新的File Group在hudi中叫自适应的file sizing。这里其实就是在partition的粒度下创建了更小粒度的group. 类似于Snowflake中的micro partition技术。这个对于行级别的更新是很友好的,不管是cow还是mor表都减少了更新带来的重写数据的范围。

多种查询类型

  • Snapshot Queries可以查询最新COMMIT的快照数据。针对Merge On Read类型的表,查询时需要在线合并列存中的Base数据和日志中的实时数据;针对Copy On Write表,可以查询最新版本的Parquet数据。Copy On Write和Merge On Read表支持该类型的查询。 批式处理
  • Incremental Queries支持增量查询的能力,可以查询给定COMMIT之后的最新数据。Copy On Write和Merge On Read表支持该类型的查询。 流式/增量处理。 增量读取的最开始的意义应该是能加速数仓计算的pipeline,因为在传统离线数仓里面只能按照partition粒度commit,因为无法将paritition做到特别细粒度,最多可能到小时,30min,那么下游调度就只能按这个粒度来调度计算。而hudi里面基于事务就可以非常快速的commit,并提供commit 之后的增量语义,那么就可以加速离线数据处理pipeline。衍生的价值应该是可以让他提供类似消息队列的功能,这样就可以也当做一个实时数仓来用(如果时效性够的话)
  • Read Optimized Queries只能查询到给定COMMIT之前所限定范围的最新数据。Read Optimized Queries是对Merge On Read表类型快照查询的优化,通过牺牲查询数据的时效性,来减少在线合并日志数据产生的查询延迟。因为这种查询只查存量数据,不查增量数据,因为使用的都是列式文件格式,所以效率较高。

Metadata管理

Hudi默认支持了写入表的元数据管理,metadata 也是一张MOR的hoodie表. 初始的需求是为了避免频繁的list file(分布式文件系统中这一操作通常很重)。Metadata是以HFile的格式存储(Hbase存储格式),提供高效的kv点查效率
Metadata 相关功能的配置org.apache.hudi.common.config.HoodieMetadataConfig
提供了哪些元数据?

  • hoodie.metadata.index.bloom.filter.enable保存数据文件的bloom filter index
  • hoodie.metadata.index.column.stats.enable保存数据文件的column 的range 用于裁剪优化

flink data skipping支持: [HUDI-4353] Column stats data skipping for flink by danny0405 · Pull Request #6026 · apache/hudi · GitHub

Catalog 支持 基于dfs 或者 hive metastore 来构建catalog 来管理所有在hudi上的表的元数据

 
 
CREATE CATALOG hoodie_catalog
WITH (
'type'='hudi',
'catalog.path' = '${catalog default root path}',
'hive.conf.dir' = '${directory where hive-site.xml is located}',
'mode'='hms' -- supports 'dfs' mode that uses the DFS backend for table DDLs persistence
);

其他表服务能力

schema evolution, clustering,clean, file sizing..

插件实现

写入类型

Write Operations | Apache Hudi

  • Upsert 默认,会先按索引查找来决定数据写入更新的位置或者仅执行插入。如果是构建一张数据库的镜像表可以使用这种方式。
  • Insert 没有去重的逻辑(不会按照record key去查找),对于没有去重需求,或者能容忍重复,仅仅需要事务保障,增量读取功能可以使用这种模式
  • bulk_insert 用于首次批量导入,通常通过Flink batch任务来运行,默认会按照分区键来排序,尽可能的避免小文件问题
  • delete 数据删除 软删除和硬删除

插件支持多种写入模式, 参见org.apache.hudi.table.HoodieTableSink#getSinkRuntimeProvider。常见的有
Streaming Ingestion | Apache Hudi
BULK_INSERT, bulk insert 模式通常是用来批量导入数据,
每次写入数据RowData时,会同时更新bloom filter索引(将record key 添加到bloom filter 中). 在一个parquet文件写完成之后,会将构建的bloom filter信息序列化成字符串, 以及此文件的key range,序列化后保存到file footer中(在没开启bloom filter索引时也会做这一步).

 
 
public Map<String, String> finalizeMetadata() {
HashMap<String, String> extraMetadata = new HashMap<>();
extraMetadata.put(HOODIE_AVRO_BLOOM_FILTER_METADATA_KEY, bloomFilter.serializeToString());
if (bloomFilter.getBloomFilterTypeCode().name().contains(HoodieDynamicBoundedBloomFilter.TYPE_CODE_PREFIX)) {
extraMetadata.put(HOODIE_BLOOM_FILTER_TYPE_CODE, bloomFilter.getBloomFilterTypeCode().name());
}
if (minRecordKey != null && maxRecordKey != null) {
extraMetadata.put(HOODIE_MIN_RECORD_KEY_FOOTER, minRecordKey.toString());
extraMetadata.put(HOODIE_MAX_RECORD_KEY_FOOTER, maxRecordKey.toString());
}
return extraMetadata;
}

Append Mode: 仅只有Insert的数据
Upsert:

  • bootstrap index 生成BootstrapOperator用于基于已经存在的hoodie表的历史数据集,构建初始的index索引(可选)通过参数index.bootstrap.enabled开启,默认为false。加载过程会可能会比较慢,开启的情况下需要等到所有task都加载完成才能处理数据。这个加载需要获取所有分区的 索引,加载到state中. 这个理论上是需要读取metadata列 _hoodie_record_key和 _hoodie_partition_path 然后构建出IndexRecord,所以会很慢。
  • stream writer 写入时会先通过BucketAssignFunction计算数据应该落到哪个bucket(file group)去, 感觉bucket这个词和bucket index有点冲突,这里是两个概念,这里主要还是划分数据所属哪个file,这一步就会用到前面构建的索引,所以默认情况下flink的索引是基于state的
 
 
// Only changing records need looking up the index for the location,
// append only records are always recognized as INSERT.
HoodieRecordGlobalLocation oldLoc = indexState.value();
// change records 表示会更改数据的写入类型如update,delete
if (isChangingRecords && oldLoc != null) {
// Set up the instant time as "U" to mark the bucket as an update bucket.
// 打标之后如果partition 发生变化了,例如partition 字段发生了变化 ? 状态中存储的就是这个数据应该存放的location
if (!Objects.equals(oldLoc.getPartitionPath(), partitionPath)) {
if (globalIndex) {
// if partition path changes, emit a delete record for old partition path,
// then update the index state using location with new partition path.
// 对于全局索引,需要先删除老的分区的数据,非全局索引不做跨分区的改动
HoodieRecord<?> deleteRecord = new HoodieAvroRecord<>(new HoodieKey(recordKey, oldLoc.getPartitionPath()),
payloadCreation.createDeletePayload((BaseAvroPayload) record.getData()));
deleteRecord.unseal();
deleteRecord.setCurrentLocation(oldLoc.toLocal("U"));
deleteRecord.seal();
out.collect((O) deleteRecord);
}
location = getNewRecordLocation(partitionPath);
} else {
location = oldLoc.toLocal("U");
this.bucketAssigner.addUpdate(partitionPath, location.getFileId());
}
} else {
location = getNewRecordLocation(partitionPath);
}

可以看到在BucketAssigner这一步就已经确定了record 已经落到哪个fileid中(也就是打标的过程),所以默认就走的是基于state的索引。 在这里org.apache.hudi.table.action.commit.FlinkWriteHelper#write区别于org.apache.hudi.table.action.commit.BaseWriteHelper#write。好处就是不用像BloomFilter 索引去读取文件key 以及并且没有假阳的问题,坏处就是需要在写入端通过state来维护索引。除了默认基于State索引的方式, Flink 也支持BucketIndex。

总体感觉,索引的实现比较割裂,交由各个引擎的实现端来完成。而且流式写入依赖内部状态索引可能稳定性的问题。

小结

  1. 相比传统数仓支持update, delete(更轻量)
  2. ACID 事务特性 (地基功能) + 索引机制。
  3. 支持增量读取和批式读取
  4. 提供健全的文件和表的metadata,加速查询端数据裁剪能力
  5. 目前看不支持dim join
  6. El posicionamiento es almacenamiento integrando flujo y lotes + actualización del almacén de datos tradicional. No existe reemplazo para los sistemas de almacenamiento olap y kv.

En general, el valor principal de Hudi es
la reducción de la latencia de datos de un extremo a otro.
En la solución de actualización T + 1 tradicional basada en Hive, solo se puede lograr la actualización de los datos a nivel diario, dependiendo de la granularidad de la partición. Debido a que en el almacén de datos tradicional fuera de línea, las confirmaciones solo se pueden realizar de acuerdo con la granularidad de la partición. Dado que la partición no se puede hacer particularmente detallada, la presión de la administración de archivos será grande, puede ser de hasta 30 minutos por hora. la programación descendente solo se puede programar de acuerdo con esta granularidad. Hudi puede confirmar muy rápidamente en función de las transacciones y proporciona semántica incremental después de la confirmación, lo que puede acelerar el proceso de procesamiento de datos fuera de línea.

Efficient Upsert
no necesita sobrescribir toda la tabla o partición cada vez, pero puede realizar actualizaciones locales con granularidad de archivos para mejorar el almacenamiento y la eficiencia informática.

Ambos están garantizados por transacciones ACID. Por lo tanto, el nombre de Hudi fue elegido muy bien y básicamente describe todas sus funciones principales.

Supongo que te gusta

Origin blog.csdn.net/Gefangenes/article/details/132483906
Recomendado
Clasificación