mirror of
https://gitlab.com/pulsechaincom/erigon-pulse.git
synced 2024-12-29 15:17:18 +00:00
139 lines
4.3 KiB
ReStructuredText
139 lines
4.3 KiB
ReStructuredText
|
===
|
||
|
ETL
|
||
|
===
|
||
|
|
||
|
ETL framework is most commonly used in [staged sync](../../eth/stagedsync).
|
||
|
|
||
|
It implements a pattern where we extract some data from a database, transform it,
|
||
|
then put it into temp files and insert back to the database in sorted order.
|
||
|
|
||
|
Inserting entries into our KV storage sorted by keys helps to minimize write
|
||
|
amplification, hence it is much faster, even considering additional I/O that
|
||
|
is generated by storing files.
|
||
|
|
||
|
It behaves similarly to enterprise [Extract, Tranform, Load] frameworks, hence the name.
|
||
|
We use temporary files because that helps keep RAM usage predictable and allows
|
||
|
using ETL on large amounts of data.
|
||
|
|
||
|
Example
|
||
|
=======
|
||
|
|
||
|
.. code-block:: go
|
||
|
|
||
|
func keyTransformExtractFunc(transformKey func([]byte) ([]byte, error)) etl.ExtractFunc {
|
||
|
return func(k, v []byte, next etl.ExtractNextFunc) error {
|
||
|
newK, err := transformKey(k)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return next(k, newK, v)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
err := etl.Transform(
|
||
|
db, // database
|
||
|
dbutils.PlainStateBucket, // "from" bucket
|
||
|
dbutils.CurrentStateBucket, // "to" bucket
|
||
|
datadir, // where to store temp files
|
||
|
keyTransformExtractFunc(transformPlainStateKey), // transformFunc on extraction
|
||
|
etl.IdentityLoadFunc, // transform on load
|
||
|
etl.TransformArgs{ // additional arguments
|
||
|
Quit: quit,
|
||
|
},
|
||
|
)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
Data Transformation
|
||
|
===================
|
||
|
|
||
|
Data could be transformed in two places along the pipeline:
|
||
|
|
||
|
* transform on extraction
|
||
|
|
||
|
* transform on loading
|
||
|
|
||
|
Transform On Extraction
|
||
|
=======================
|
||
|
|
||
|
.. code-block:: go
|
||
|
|
||
|
type ExtractFunc func(k []byte, v []byte, next ExtractNextFunc) error
|
||
|
|
||
|
Transform on extraction function receives the currenk key and value from the
|
||
|
source bucket.
|
||
|
|
||
|
Transform On Loading
|
||
|
====================
|
||
|
|
||
|
.. code-block:: go
|
||
|
|
||
|
type LoadFunc func(k []byte, value []byte, state State, next LoadNextFunc) error
|
||
|
|
||
|
As well as the current key and value, the transform on loading function
|
||
|
receives the `State` object that can receive data from the destination bucket.
|
||
|
|
||
|
That is used in index generation where we want to extend index entries with new
|
||
|
data instead of just adding new ones.
|
||
|
|
||
|
Buffer Types
|
||
|
============
|
||
|
|
||
|
Before the data is being flushed into temp files, it is getting collected into
|
||
|
a buffer until if overflows (`etl.ExtractArgs.BufferSize`).
|
||
|
|
||
|
There are different types of buffers available with different behaviour.
|
||
|
|
||
|
* `SortableSliceBuffer` -- just append (k, v1), (k, v2) onto a slice. Duplicate keys
|
||
|
will lead to duplicate entries: [(k, v1) (k, v2)].
|
||
|
|
||
|
* `SortableAppendBuffer` -- on duplicate keys: merge. (k, v1), (k, v2)
|
||
|
will lead to k: [v1 v2]
|
||
|
|
||
|
* `SortableOldestAppearedBuffer` -- on duplicate keys: keep the oldest. (k,
|
||
|
v1), (k v2) will lead to k: v1
|
||
|
|
||
|
Loading Into Database
|
||
|
=====================
|
||
|
|
||
|
We load data from the temp files into a database in batches, limited by
|
||
|
`IdealBatchSize()` of an `ethdb.Mutation`.
|
||
|
|
||
|
Saving & Restoring State
|
||
|
========================
|
||
|
|
||
|
Interrupting in the middle of loading can lead to inconsistent state in the
|
||
|
database.
|
||
|
|
||
|
To avoid that, the ETL framework allows storing progress by setting `OnLoadCommit` in `etl.TransformArgs`.
|
||
|
|
||
|
Then we can use this data to know the progress the ETL transformation made.
|
||
|
|
||
|
You can also specify `ExtractStartKey` and `ExtractEndKey` to limit the nubmer
|
||
|
of items transformed.
|
||
|
|
||
|
|
||
|
`etl.Transform` function
|
||
|
========================
|
||
|
|
||
|
The vast majority of use-cases is when we extract data from one bucket and in
|
||
|
the end, load it into another bucket. That is the use-case for `etl.Transform`
|
||
|
function.
|
||
|
|
||
|
`etl.Collector` struct
|
||
|
======================
|
||
|
|
||
|
If you want a more modular behaviour instead of just reading from the DB (like
|
||
|
generating intermediate hashes in https://github.com/ledgerwatch/turbo-geth/blob/master/core/chain_makers.go, you can use
|
||
|
`etl.Collector` struct directly.
|
||
|
|
||
|
It has a `.Collect()` method that you can provide your data to.
|
||
|
|
||
|
|
||
|
Optimizations
|
||
|
=============
|
||
|
|
||
|
* if all data fits into a single file, we don't write anything to disk and just
|
||
|
use in-memory storage.
|