翻译自 Apache Paimon官方文档

概览

概述

Apache Paimon (incubating) 是一项流式数据湖存储技术,可以为用户提供高吞吐、低延迟的数据摄入、流式订阅以及实时查询能力。

简单来说,Paimon的上游是各个CDC,即changlog数据流;而其自身支持实时sink与search(下沉与查询)changlog数据流。一般会与Flink等流式计算引擎集成使用。

流式数据湖是一种先进的数据存储架构,专门为处理大规模实时数据流而设计。在流式数据湖中,数据以流的形式持续不断地进入系统,而不是批量存储后处理。

数据湖是一个存储企业的各种各样原始数据的大型仓库,其中的数据可供存取、处理、分析及传输。

数据仓库中的数据是经过优化后(也可以看作是结构化的数据),且与该数据仓库支持的数据模型吻合的数据。

Paimon提供以下核心功能:

  • 统一批处理和流式处理:Paimon支持批量写入和批量读取,以及流式写入更改和流式读取表change log。
  • 数据湖:Paimon作为数据湖存储,具有成本低、可靠性高、元数据可扩展等优点。
  • Merge Engines:Paimon支持丰富的合并引擎(Merge Engines)。默认情况下,保留主键的最后一个条目。您还可以使用“部分更新”或“聚合”引擎。
  • Changelog Producer:用于在数据湖中生成和跟踪数据的变更日志(changelog);Paimon 支持丰富的 Changelog Producer,例如“lookup”和“full-compaction”;正确的changelog可以简化流式处理管道的构造。
  • Append Only Tables:Paimon支持只追加(append only)表,自动压缩小文件,提供有序的流式读取。您可以使用它来替换消息队列。

架构



架构如下所示

读/写:Paimon 支持多种读/写数据和执行 OLAP 查询的方式。

  • 对于读取,支持如下三种方式消费数据

    • 历史快照(批处理模式)
    • 最新的偏移量(流模式)
    • 混合模式下读取增量快照
  • 对于写入,它支持来自数据库变更日志(CDC)的流式同步或来自离线数据的批量插入/覆盖。

生态系统:除了Apache Flink之外,Paimon还支持Apache Hive、Apache Spark、Trino等其他计算引擎的读取。

底层存储:Paimon 将列式文件存储在文件系统/对象存储上,并使用 LSM 树结构来支持大量数据更新和高性能查询。

统一存储

对于 Apache Flink 这样的流引擎,通常有三种类型的connector:

  • 消息队列,例如 Apache Kafka,在该消息管道(pipeline)的源阶段和中间阶段使用,以保证延迟保持在秒级。
  • OLAP系统,例如ClickHouse,它以流方式接收处理后的数据并服务用户的即席查询。
  • 批量存储,例如Apache Hive,它支持传统批处理的各种操作,包括INSERT OVERWRITE。

Paimon 提供抽象概念的表。 它的使用方式与传统数据库没有什么区别:

  • 在批处理执行模式下,它就像一个Hive表,支持Batch SQL的各种操作。 查询它以查看最新的快照。
  • 在流执行模式下,它的作用就像一个消息队列。 查询它的行为就像从历史数据永不过期的消息队列中查询stream changelog。

基本概念

Snapshot

snapshot捕获table在某个时间点的状态。 用户可以通过最新的snapshot来访问表的最新数据。根据时间,用户还可以通过较早的快照访问表的先前状态。

Partition

Paimon 采用与 Apache Hive 相同的分区概念来分离数据。

分区是一种可选方法,可根据date, city, and department等特定列的值将表划分为相关部分。每个表可以有一个或多个分区键来标识特定分区。

通过分区,用户可以高效地操作表中的一片记录。

Bucket

未分区表或分区表中的分区被细分为Bucket(桶),以便为可用于更有效查询的数据提供额外的结构。

Bucket的范围由record中的一列或多列的哈希值确定。用户可以通过提供bucket-key选项来指定分桶列。如果未指定bucket-key选项,则主键(如果已定义)或完整记录将用作存储桶键。

Bucket是读写的最小存储单元,因此Bucket的数量限制了最大处理并行度。 不过这个数字不应该太大,因为它会导致大量 小文件和低读取性能。 一般来说,每个桶中建议的数据大小约为200MB - 1GB。

Consistency Guarantees

Paimon Writer 使用两阶段提交协议以原子方式将一批record提交到Table中。每次提交时最多生成两个snapshot。

对于任意两个同时修改table的写入者,只要他们不修改同一个Bucket,他们的提交就可以并行发生。如果他们修改同一个Bucket,则仅保证快照隔离。也就是说,最终表状态可能是两次提交的混合,但不会丢失任何更改。

文件

概述

一张表的所有文件都存储在一个基本目录下。 Paimon 文件以分层方式组织。 下图说明了文件布局。 从snapshot文件开始,Paimon reader可以递归地访问表中的所有记录。

Snapshot Files

所有snapshot文件都存储在snapshot目录中。

snapshot文件是一个 JSON 文件,包含有关此snapshot的信息,包括

  • 正在使用的Schema文件
  • 包含此snapshot的所有更改的清单列表(manifest list)

Manifest Files

所有清单(manifest)列表和清单文件都存储在清单目录中。

清单列表(manifest list)是清单文件名的列表。

清单文件是包含有关 LSM 数据文件和changelog文件的更改的文件。 例如对应快照中创建了哪个LSM数据文件、删除了哪个文件。

Data Files

数据文件按分区和桶(Bucket)分组。每个Bucket目录都包含一个 LSM 树及其changelog文件。

目前,Paimon 支持使用 orc(默认)、parquet 和 avro 作为数据文件格式。

LSM-Trees

Paimon 采用 LSM 树(日志结构合并树)作为文件存储的数据结构。 如下简要介绍

Sorted Runs

LSM 树将文件组织成多个 sorted runs。 sorted runs由一个或多个数据文件组成,并且每个数据文件恰好属于一个 sorted runs。

数据文件中的记录按其主键排序。 在 sorted runs中,数据文件的主键范围永远不会重叠。



如图所示的,不同的 sorted runs可能具有重叠的主键范围,甚至可能包含相同的主键。查询LSM树时,必须合并所有 sorted runs,并且必须根据用户指定的合并引擎和每条记录的时间戳来合并具有相同主键的所有记录。

写入LSM树的新记录将首先缓存在内存中。当内存缓冲区满时,内存中的所有记录将被顺序并刷新到磁盘,并创建一个新的 sorted runs。

Compaction

当越来越多的记录写入LSM树时,sorted runs的数量将会增加。由于查询LSM树需要将所有 sorted runs合并起来,太多 sorted runs将导致查询性能较差,甚至内存不足。

为了限制 sorted runs的数量,我们必须偶尔将多个 sorted runs合并为一个大的 sorted runs。 这个过程称为压缩。

然而,压缩是一个资源密集型过程,会消耗一定的CPU时间和磁盘IO,因此过于频繁的压缩可能会导致写入速度变慢。 这是查询和写入性能之间的权衡。 Paimon 目前采用了类似于 Rocksdb 通用压缩的压缩策略。

默认情况下,当Paimon将记录追加到LSM树时,它也会根据需要执行压缩。 用户还可以选择在专用压缩作业中执行所有压缩。

可以将 sorted runs 理解为多个有序的Data File组成的一个有序文件。

Changelog表是创建表时的默认表类型。用户可以在表中插入、更新或删除记录。

主键由一组列组成,这些列包含每个记录的唯一值。Paimon通过对每个bucket中的主键进行排序来实现数据排序,允许用户通过对主键应用过滤条件来实现高性能。

通过在变更日志表上定义主键,用户可以访问以下特性。

Bucket

桶(Bucket)是进行读写操作的最小存储单元,每个桶目录包含一个LSM树。

Fixed Bucket

配置一个大于0的桶,使用Fixed bucket模式,根据Math.abs(key_hashcode % numBuckets)来计算记录的桶。

重新缩放桶只能通过离线进程进行。桶的数量过多会导致小文件过多,桶的数量过少会导致写性能不佳。

Dynamic Bucket

配置'Bucket'='-1'。 先到达的key会落入旧的bucket,新的key会落入新的bucket,bucket和key的分布取决于数据到达的顺序。 Paimon 维护一个索引来确定哪个键对应哪个桶。

Paimon会自动扩大桶的数量。

  • Option1: 'dynamic-bucket.target-row-num':控制一个桶的目标行数。
  • Option2:'dynamic-bucket.initial-buckets':控制初始化bucket的数量。
Normal Dynamic Bucket Mode

当更新不跨分区(没有分区,或者主键包含所有分区字段)时,动态桶模式使用 HASH 索引来维护从键到桶的映射,它比固定桶模式需要更多的内存。

如下:

  • 一般来说,没有性能损失,但会有一些额外的内存消耗,一个分区中的 1 亿个条目多占用 1 GB 内存,不再活动的分区不占用内存。
  • 对于更新率较低的表,建议使用此模式,以显着提高性能。
Cross Partitions Upsert Dynamic Bucket Mode

当需要跨分区upsert(主键不包含所有分区字段)时,Dynamic Bucket模式直接维护键到分区和桶的映射,使用本地磁盘,并在启动流写作业时通过读取表中所有现有键来初始化索引 。 不同的合并引擎有不同的行为:

  1. Deduplicate:删除旧分区中的数据,并将新数据插入到新分区中。
  2. PartialUpdate & Aggregation:将新数据插入旧分区。
  3. FirstRow:如果有旧值,则忽略新数据。

性能:对于数据量较大的表,性能会有明显的损失。而且,初始化需要很长时间。

如果你的upsert不依赖太旧的数据,可以考虑配置索引TTL来减少索引和初始化时间:

'cross-partition-upsert.index-ttl':rocksdb索引和初始化中的TTL,这样可以避免维护太多索引而导致性能越来越差。

但请注意,这也可能会导致数据重复。

Merge Engines

当Paimon sink收到两条或更多具有相同主键的记录时,它会将它们合并为一条记录以保持主键唯一。 通过指定merge-engine属性,用户可以选择如何将记录合并在一起。

Deduplicate

deduplicate合并引擎是默认的合并引擎。 Paimon 只会保留最新的记录,并丢弃其他具有相同主键的记录。

具体来说,如果最新的记录是DELETE记录,则所有具有相同主键的记录都将被删除。

Partial Update

通过指定 'merge-engine' = 'partial-update',用户可以通过多次更新来更新记录的列,直到记录完成。 这是通过使用同一主键下的最新数据逐一更新值字段来实现的。 但是,在此过程中不会覆盖空值。

如下所示:

  • <1, 23.0, 10, NULL>-
  • <1, NULL, NULL, 'This is a book'>
  • <1, 25.2, NULL, NULL>

假设第一列是主键key,那么最后的结果是 <1, 25.2, 10, 'This is a book'>

Sequence Group

序列字段并不能解决多流更新的部分更新表的乱序问题,因为多流更新时序列字段可能会被另一个流的最新数据覆盖。

因此我们引入了部分更新表的序列组(Sequence Group)机制。 它可以解决:

  1. 多流更新时出现混乱。 每个流定义其自己的序列组。
  2. 真正的部分更新,而不仅仅是非空更新。

如下所示:

CREATE TABLE T (
k INT,
a INT,
b INT,
g_1 INT,
c INT,
d INT,
g_2 INT,
PRIMARY KEY (k) NOT ENFORCED
) WITH (
'merge-engine'='partial-update',
'fields.g_1.sequence-group'='a,b',
'fields.g_2.sequence-group'='c,d'
); INSERT INTO T VALUES (1, 1, 1, 1, 1, 1, 1); -- g_2 is null, c, d should not be updated
INSERT INTO T VALUES (1, 2, 2, 2, 2, 2, CAST(NULL AS INT)); SELECT * FROM T; -- output 1, 2, 2, 2, 1, 1, 1 -- g_1 is smaller, a, b should not be updated
INSERT INTO T VALUES (1, 3, 3, 1, 3, 3, 3); SELECT * FROM T; -- output 1, 2, 2, 2, 3, 3, 3

对于 sequence-group,有效的比较数据类型包括:DECIMAL、TINYINT、SMALLINT、INTEGER、BIGINT、FLOAT、DOUBLE、DATE、TIME、TIMESTAMP 和 TIMESTAMP_LTZ。

Aggregation

可以为输入字段指定聚合函数,支持聚合中的所有函数。

如下所示:

CREATE TABLE T (
k INT,
a INT,
b INT,
c INT,
d INT,
PRIMARY KEY (k) NOT ENFORCED
) WITH (
'merge-engine'='partial-update',
'fields.a.sequence-group' = 'b',
'fields.b.aggregate-function' = 'first_value',
'fields.c.sequence-group' = 'd',
'fields.d.aggregate-function' = 'sum'
);
INSERT INTO T VALUES (1, 1, 1, CAST(NULL AS INT), CAST(NULL AS INT));
INSERT INTO T VALUES (1, CAST(NULL AS INT), CAST(NULL AS INT), 1, 1);
INSERT INTO T VALUES (1, 2, 2, CAST(NULL AS INT), CAST(NULL AS INT));
INSERT INTO T VALUES (1, CAST(NULL AS INT), CAST(NULL AS INT), 2, 2); SELECT * FROM T; -- output 1, 2, 1, 2, 3
Default Value

如果无法保证数据的顺序,仅通过覆盖空值的方式写入字段,则读表时未覆盖的字段将显示为空。

CREATE TABLE T (
k INT,
a INT,
b INT,
c INT,
PRIMARY KEY (k) NOT ENFORCED
) WITH (
'merge-engine'='partial-update'
);
INSERT INTO T VALUES (1, 1, CAST(NULL AS INT), CAST(NULL AS INT));
INSERT INTO T VALUES (1, CAST(NULL AS INT), CAST(NULL AS INT), 1); SELECT * FROM T; -- output 1, 1, null, 1

如果希望读表时未被覆盖的字段有默认值而不是null,则需要fields.name.default-value

CREATE TABLE T (
k INT,
a INT,
b INT,
c INT,
PRIMARY KEY (k) NOT ENFORCED
) WITH (
'merge-engine'='partial-update',
'fields.b.default-value'='0'
); INSERT INTO T VALUES (1, 1, CAST(NULL AS INT), CAST(NULL AS INT));
INSERT INTO T VALUES (1, CAST(NULL AS INT), CAST(NULL AS INT), 1); SELECT * FROM T; -- output 1, 1, 0, 1

Aggregation

有时用户只关心聚合结果。 聚合 合并引擎根据聚合函数将同一主键下的各个值字段与最新数据一一聚合。

每个不属于主键的字段都可以被赋予一个聚合函数,由 fields.<field-name>.aggregate-function 表属性指定,否则它将使用 last_non_null_value 聚合作为默认值。 例如,请考虑下表定义。

CREATE TABLE MyTable (
product_id BIGINT,
price DOUBLE,
sales BIGINT,
PRIMARY KEY (product_id) NOT ENFORCED
) WITH (
'merge-engine' = 'aggregation',
'fields.price.aggregate-function' = 'max',
'fields.sales.aggregate-function' = 'sum'
);

price字段将通过 max 函数聚合,sales字段将通过 sum 函数聚合。 给定两个输入记录 <1, 23.0, 15> 和 <1, 30.2, 20>,最终结果将是 <1, 30.2, 35>。

当前支持的聚合函数和数据类型有:

  • sum:支持 DECIMAL、TINYINT、SMALLINT、INTEGER、BIGINT、FLOAT 和 DOUBLE。
  • min/max:支持 CHAR、VARCHAR、DECIMAL、TINYINT、SMALLINT、INTEGER、BIGINT、FLOAT、DOUBLE、DATE、TIME、TIMESTAMP 和 TIMESTAMP_LTZ。
  • last_value / last_non_null_value:支持所有数据类型。
  • listagg:支持STRING数据类型。
  • bool_and / bool_or:支持BOOLEAN数据类型。
  • first_value/first_not_null_value:支持所有数据类型。

只有 sum 支持撤回(UPDATE_BEFORE 和 DELETE),其他聚合函数不支持撤回。 如果允许某些函数忽略撤回消息,可以配置:'fields.${field_name}.ignore-retract'='true'

First Row

通过指定 'merge-engine' = 'first-row',用户可以保留同一主键的第一行。 它与Deduplicate合并引擎不同,在First Row合并引擎中,它将生成仅insert changelog。

  1. First Row合并引擎必须与查找变更日志生成器一起使用。
  2. 不能指定sequence.field。
  3. 不接受 DELETE 和 UPDATE_BEFORE 消息。 可以配置 first-row.ignore-delete 来忽略这两种记录。

这对于替代流计算中的log deduplication有很大的帮助。

Changelog Producers

流式查询会不断产生最新的变化。

通过在创建表时指定更改changelog-producer表属性,用户可以选择从表文件生成的更改模式。

None

默认情况下,不会将额外的changelog producer应用于表的写入器。 Paimon source只能看到跨snapshot的合并更改,例如删除了哪些键以及某些键的新值是什么。

但是,这些合并的更改无法形成完整的changelog,因为我们无法直接从中读取键的旧值。 合并的更改要求消费者“记住”每个键的值并重写这些值而不看到旧的值。 然而,一些消费者需要旧的值来确保正确性或效率。

考虑一个消费者计算某些分组键的总和(可能不等于主键)。 如果消费者只看到一个新值5,它无法确定应该将哪些值添加到求和结果中。 例如,如果旧值为 4,则应在结果中加 1。 但如果旧值是 6,则应依次从结果中减去 1。 旧的value对于这些类型的消费者来说很重要。

总而言之,没有一个changelog producer最适合数据库系统等使用者。 Flink 还有一个内置的“规范化”运算符,可以将每个键的值保留在状态中。 很容易看出,这种操作符的成本非常高,应该避免使用。 (可以通过“scan.remove-normalize”强制删除“normalize”运算符。)

Input

通过指定 'changelog- Producer' = 'input',Paimon Writer依赖他们的输入作为完整changelog的来源。 所有输入记录将保存在单独的changelog file中,并由 Paimon source提供给消费者。

当 Paimon 编写者的输入是完整的changelog(例如来自数据库 CDC)或由 Flink 状态计算生成时,可以使用input changelog producer.

Lookup

如果您的输入无法生成完整的changelog,但想摆脱昂贵的标准化运算符,则可以考虑使用'lookup' changelog producer.

通过指定'changelog- Producer' = 'lookup',Paimon将在提交数据写入之前通过'lookup'生成changelog。



Lookup 会将数据缓存在内存和本地磁盘上,您可以使用以下选项来调整性能:



Lookup changelog- Producer 支持changelog- Producer.row-deduplicate以避免为同一记录生成-U、+U changelog。

Full Compaction

如果你觉得“lookup”的资源消耗太大,可以考虑使用“full-compaction”changelog Producer,它可以解耦数据写入和changelog生成,更适合高延迟的场景(例如10分钟) )。

通过指定 'changelog- Producer' = 'full-compaction',Paimon 将比较完全压缩之间的结果并生成差异作为changelog。changelog的延迟受到完全压缩频率的影响。

通过指定 full-compaction.delta-commits 表属性,在增量提交(检查点 checkpoint)后将不断触发 full compaction。 默认情况下设置为 1,因此每个检查点都会进行完全压缩并生成change log。



Full-compaction changelog- Producer 支持changelog- Producer.row-deduplicate 以避免为同一记录生成-U、+U 变更日志。

Sequence Field

默认情况下,主键表根据输入顺序确定合并顺序(最后输入的记录将是最后合并的)。 然而在分布式计算中,会存在一些导致数据混乱的情况。 这时,可以使用时间字段作为sequence.field,例如:

CREATE TABLE MyTable (
pk BIGINT PRIMARY KEY NOT ENFORCED,
v1 DOUBLE,
v2 BIGINT,
dt TIMESTAMP
) WITH (
'sequence.field' = 'dt'
);

无论输入顺序如何,具有最大sequence.field 值的记录将是最后合并的记录。

Sequence Auto Padding:

当记录更新或删除时,sequence.field必须变大,不能保持不变。 对于-U和+U,它们的序列字段必须不同。 如果您无法满足此要求,Paimon 提供了自动填充序列字段的选项。

  1. 'sequence.auto-padding' = 'row-kind-flag':如果对-U和+U使用相同的值,就像Mysql Binlog中的“op_ts”(数据库中进行更改的时间)一样。 建议使用自动填充行类型标志,它会自动区分-U(-D)和+U(+I)。
  2. 精度不够:如果提供的sequence.field不满足精度,比如大约秒或毫秒,可以将sequence.auto-padding设置为秒到微或毫秒到微,这样序列号的精度 将由系统弥补到微秒。
  3. 复合模式:例如“second-to-micro,row-kind-flag“,首先将micro添加到第二个,然后填充row-kind标志。

Row Kind Field

默认情况下,主键表根据输入行确定行类型。 您还可以定义“rowkind.field”以使用字段来提取行类型。

有效的行类型字符串应为“+I”、“-U”、“+U”或“-D”。

聊聊流式数据湖Paimon(一)的更多相关文章

  1. Apache Hudi 0.9.0版本重磅发布!更强大的流式数据湖平台

    1. 重点特性 1.1 Spark SQL支持 0.9.0 添加了对使用 Spark SQL 的 DDL/DML 的支持,朝着使所有角色(非工程师.分析师等)更容易访问和操作 Hudi 迈出了一大步. ...

  2. 字节跳动流式数据集成基于Flink Checkpoint两阶段提交的实践和优化

    背景 字节跳动开发套件数据集成团队(DTS ,Data Transmission Service)在字节跳动内基于 Flink 实现了流批一体的数据集成服务.其中一个典型场景是 Kafka/ByteM ...

  3. FunDA(2)- Streaming Data Operation:流式数据操作

    在上一集的讨论里我们介绍并实现了强类型返回结果行.使用强类型主要的目的是当我们把后端数据库SQL批次操作搬到内存里转变成数据流式按行操作时能更方便.准确.高效地选定数据字段.在上集讨论示范里我们用集合 ...

  4. 流式数据分析模型kafka+storm

    http://www.cnblogs.com/panfeng412/archive/2012/07/29/storm-stream-model-analysis-and-discussion.html ...

  5. 基于Apache Hudi构建数据湖的典型应用场景介绍

    1. 传统数据湖存在的问题与挑战 传统数据湖解决方案中,常用Hive来构建T+1级别的数据仓库,通过HDFS存储实现海量数据的存储与水平扩容,通过Hive实现元数据的管理以及数据操作的SQL化.虽然能 ...

  6. Robinhood基于Apache Hudi的下一代数据湖实践

    1. 摘要 Robinhood 的使命是使所有人的金融民主化. Robinhood 内部不同级别的持续数据分析和数据驱动决策是实现这一使命的基础. 我们有各种数据源--OLTP 数据库.事件流和各种第 ...

  7. 基于Apache Hudi在Google云构建数据湖平台

    自从计算机出现以来,我们一直在尝试寻找计算机存储一些信息的方法,存储在计算机上的信息(也称为数据)有多种形式,数据变得如此重要,以至于信息现在已成为触手可及的商品.多年来数据以多种方式存储在计算机中, ...

  8. 翻译-In-Stream Big Data Processing 流式大数据处理

    相当长一段时间以来,大数据社区已经普遍认识到了批量数据处理的不足.很多应用都对实时查询和流式处理产生了迫切需求.最近几年,在这个理念的推动下,催生出了一系列解决方案,Twitter Storm,Yah ...

  9. Flink 另外一个分布式流式和批量数据处理的开源平台

    Apache Flink是一个分布式流式和批量数据处理的开源平台. Flink的核心是一个流式数据流动引擎,它为数据流上面的分布式计算提供数据分发.通讯.容错.Flink包括几个使用 Flink引擎创 ...

  10. 【node】记录项目的开始与完成——pipeline_kafka流式数据库管理项目

    前言: 我始终坚信的一点是,学习的效果80%来自总结,甚至全部都是.总结的好处就是让你能翻出你的过往,指出其中的不足,看到未来的改进方法,好的总结更能让知识产生飞跃,所以在工作之余,部署项目之际,总结 ...

随机推荐

  1. 如何在Vue3中配置国际化语言i18n

    1. 安装 vue-i18n npm i vue-i18n -S 2. 创建一个i8n的配置文件 如:i18nConfig.js // 配置 vue-i18n 实现国际化语言设置 import { c ...

  2. EtherCAT转Modbus网关用Modbus Slave模拟从站配置案例

    EtherCAT转Modbus网关用Modbus Slave模拟从站配置案例 兴达易控EtherCAT到Modbus网关可以用作Modbus从站的配置.EtherCAT到Modbus网关允许Modbu ...

  3. Teamcenter RAC 开发之《PlaceHolder》

    背景 做个swing表单,有时候想实现一些网页input标签的placeHolder提示,可能本人写vue or html写多,对某些细节有强迫症,所以找小下资料 实现方法(Swingx) 看源码

  4. Docker常见使用

    DockerFile: FROM openjdk:8 MAINTAINER guotong ADD lpl-1.0-SNAPSHOT.jar lpl-1.0-SNAPSHOT.jar EXPOSE 8 ...

  5. 搞懂fflush(stdout)

    使用 printf 或 cout 打印内容时,输出永远不会直接写入"屏幕".而是,被发送到 stdout. (stdout 就像一个缓冲区) 默认情况下,发送到 stdout 的输 ...

  6. 爬虫系列——Beautifulsoup4

    文章目录 一 介绍 二 基本使用 三 遍历文档树 四 搜索文档树 五 修改文档树 六 总结 ​ 一 介绍 Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库.它能 ...

  7. Django框架项目之项目基础——项目介绍、需求分析、pip安装源、环境搭建、前台、后台、跨域请求

    文章目录 1 路飞学城 1.1 企业的web项目类型 1.2 企业项目开发流程 1.3 立项申请阶段 2. 需求分析 2.1 首页 2.2 登录注册 2.3 课程列表 2.4 课程详情 2.5 购物车 ...

  8. Rust WebAssembly 绑定输入类型(基于 Serde)

    前言 单位有个项目要共享前后端检查策略后端用的正好也是 Rust,但是 Rust 默认的 wasm-bindgen 包中提供的转换操作非常少,像 Vec<T> <=> Arra ...

  9. log4j2同步日志引发的性能问题

    1 问题回顾 1.1 问题描述 在项目的性能测试中,相关的接口的随着并发数增加,接口的响应时间变长,接口吞吐不再增长,应用的CPU使用率较高. 1.2 分析思路 谁导致的CPU较高,阻塞接口TPS的增 ...

  10. 《最新出炉》系列初窥篇-Python+Playwright自动化测试-21-处理鼠标拖拽-番外篇

    1.简介 前边宏哥拖拽有提到那个反爬虫机制,加了各种参数,以及加载js脚本文件还是有问题,偶尔宏哥好像发现了解决问题的办法,看到了黎明的曙光,宏哥就说试一下看看行不行,万一实现了.结果宏哥试了结果真的 ...