From d838f222d3114ecbbb212ab255d3bf9c25ab4f30 Mon Sep 17 00:00:00 2001 From: Alex Ostrovski Date: Wed, 19 Jun 2024 18:39:50 +0300 Subject: [PATCH] refactor(db): Combine storage log pruning into single query (#2279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What ❔ Minor follow-up for https://github.com/matter-labs/zksync-era/pull/2268 that combines both parts of log pruning into a single query. ## Why ❔ Easier to maintain and could be slightly more efficient since intermediate data doesn't need to travel from Postgres to the node and back. ## Checklist - [x] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [x] Documentation comments have been added / updated. - [x] Code has been formatted via `zk fmt` and `zk lint`. - [x] Spellcheck has been run via `zk spellcheck`. --- ...60cd2f3d5223add676591cb0577e0a77403cb.json | 16 --- ...9b5c09854efaa4c0a35466b138587dce03f25.json | 15 +++ ...94d8d631d56c5753f4e944f1cdf3e05b04a8c.json | 35 ------- core/lib/dal/src/pruning_dal/mod.rs | 99 ++++++------------- core/node/db_pruner/src/metrics.rs | 5 +- 5 files changed, 47 insertions(+), 123 deletions(-) delete mode 100644 core/lib/dal/.sqlx/query-327974ef6d0c7edf56339d310ec60cd2f3d5223add676591cb0577e0a77403cb.json create mode 100644 core/lib/dal/.sqlx/query-6ad9adcbd60483148983a495d0e9b5c09854efaa4c0a35466b138587dce03f25.json delete mode 100644 core/lib/dal/.sqlx/query-8c2f1f7bccc6af93714a74f732f94d8d631d56c5753f4e944f1cdf3e05b04a8c.json diff --git a/core/lib/dal/.sqlx/query-327974ef6d0c7edf56339d310ec60cd2f3d5223add676591cb0577e0a77403cb.json b/core/lib/dal/.sqlx/query-327974ef6d0c7edf56339d310ec60cd2f3d5223add676591cb0577e0a77403cb.json deleted file mode 100644 index 7ecce5be1f35..000000000000 --- a/core/lib/dal/.sqlx/query-327974ef6d0c7edf56339d310ec60cd2f3d5223add676591cb0577e0a77403cb.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n DELETE FROM storage_logs USING UNNEST($1::bytea[], $2::BIGINT[], $3::INT[]) AS new_logs (hashed_key, miniblock_number, operation_number)\n WHERE\n storage_logs.hashed_key = new_logs.hashed_key\n AND (storage_logs.miniblock_number, storage_logs.operation_number) < (new_logs.miniblock_number, new_logs.operation_number)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "ByteaArray", - "Int8Array", - "Int4Array" - ] - }, - "nullable": [] - }, - "hash": "327974ef6d0c7edf56339d310ec60cd2f3d5223add676591cb0577e0a77403cb" -} diff --git a/core/lib/dal/.sqlx/query-6ad9adcbd60483148983a495d0e9b5c09854efaa4c0a35466b138587dce03f25.json b/core/lib/dal/.sqlx/query-6ad9adcbd60483148983a495d0e9b5c09854efaa4c0a35466b138587dce03f25.json new file mode 100644 index 000000000000..93d1966f370c --- /dev/null +++ b/core/lib/dal/.sqlx/query-6ad9adcbd60483148983a495d0e9b5c09854efaa4c0a35466b138587dce03f25.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH\n new_logs AS MATERIALIZED (\n SELECT DISTINCT\n ON (hashed_key) hashed_key,\n miniblock_number,\n operation_number\n FROM\n storage_logs\n WHERE\n miniblock_number BETWEEN $1 AND $2\n ORDER BY\n hashed_key,\n miniblock_number DESC,\n operation_number DESC\n )\n DELETE FROM storage_logs USING new_logs\n WHERE\n storage_logs.hashed_key = new_logs.hashed_key\n AND (storage_logs.miniblock_number, storage_logs.operation_number) < (new_logs.miniblock_number, new_logs.operation_number)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "6ad9adcbd60483148983a495d0e9b5c09854efaa4c0a35466b138587dce03f25" +} diff --git a/core/lib/dal/.sqlx/query-8c2f1f7bccc6af93714a74f732f94d8d631d56c5753f4e944f1cdf3e05b04a8c.json b/core/lib/dal/.sqlx/query-8c2f1f7bccc6af93714a74f732f94d8d631d56c5753f4e944f1cdf3e05b04a8c.json deleted file mode 100644 index ffb51e0dd865..000000000000 --- a/core/lib/dal/.sqlx/query-8c2f1f7bccc6af93714a74f732f94d8d631d56c5753f4e944f1cdf3e05b04a8c.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT DISTINCT\n ON (hashed_key) hashed_key,\n miniblock_number,\n operation_number\n FROM\n storage_logs\n WHERE\n miniblock_number BETWEEN $1 AND $2\n ORDER BY\n hashed_key,\n miniblock_number DESC,\n operation_number DESC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "hashed_key", - "type_info": "Bytea" - }, - { - "ordinal": 1, - "name": "miniblock_number", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "operation_number", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "8c2f1f7bccc6af93714a74f732f94d8d631d56c5753f4e944f1cdf3e05b04a8c" -} diff --git a/core/lib/dal/src/pruning_dal/mod.rs b/core/lib/dal/src/pruning_dal/mod.rs index 16f85f2e0fad..7f30af034e2b 100644 --- a/core/lib/dal/src/pruning_dal/mod.rs +++ b/core/lib/dal/src/pruning_dal/mod.rs @@ -1,6 +1,5 @@ use std::ops; -use itertools::Itertools; use zksync_db_connection::{connection::Connection, error::DalResult, instrument::InstrumentExt}; use zksync_types::{L1BatchNumber, L2BlockNumber}; @@ -28,7 +27,6 @@ pub struct PruningInfo { pub struct HardPruningStats { pub deleted_l1_batches: u64, pub deleted_l2_blocks: u64, - pub overwriting_logs: u64, pub deleted_storage_logs: u64, pub deleted_events: u64, pub deleted_call_traces: u64, @@ -42,14 +40,6 @@ enum PruneType { Hard, } -/// Raw database presentation of a primary key in the `miniblocks` table. -#[derive(Debug)] -struct StorageLogPrimaryKey { - hashed_key: Vec, - miniblock_number: i64, - operation_number: i32, -} - impl PruningDal<'_, '_> { pub async fn get_pruning_info(&mut self) -> DalResult { let pruning_info = sqlx::query!( @@ -183,18 +173,9 @@ impl PruningDal<'_, '_> { self.clear_transaction_fields(first_l2_block_to_prune..=last_l2_block_to_prune) .await?; - // Storage log pruning is designed to use deterministic indexes and thus have predictable performance. - // - // - `get_pks_for_latest_logs` is guaranteed to use the block number index (that's the only WHERE condition), - // and the supplied range of blocks should be reasonably small. - // - `prune_storage_logs` is virtually guaranteed to use the primary key index since the query removes ranges w.r.t. this index. - // - // Combining these two queries or using more sophisticated queries leads to fluctuating performance due to - // unpredictable indexes being used. - let new_logs = self - .get_pks_for_latest_logs(first_l2_block_to_prune..=last_l2_block_to_prune) + let deleted_storage_logs = self + .prune_storage_logs(first_l2_block_to_prune..=last_l2_block_to_prune) .await?; - let deleted_storage_logs = self.prune_storage_logs(&new_logs).await?; let deleted_l1_batches = self.delete_l1_batches(last_l1_batch_to_prune).await?; let deleted_l2_blocks = self.delete_l2_blocks(last_l2_block_to_prune).await?; @@ -204,7 +185,6 @@ impl PruningDal<'_, '_> { deleted_events, deleted_l2_to_l1_logs, deleted_call_traces, - overwriting_logs: new_logs.len() as u64, deleted_storage_logs, } } else { @@ -324,62 +304,45 @@ impl PruningDal<'_, '_> { Ok(execution_result.rows_affected()) } - /// Gets primary keys for all latest logs in the specified L2 block range. - async fn get_pks_for_latest_logs( + /// Removes storage logs overwritten by the specified new logs. + async fn prune_storage_logs( &mut self, l2_blocks_to_prune: ops::RangeInclusive, - ) -> DalResult> { - sqlx::query_as!( - StorageLogPrimaryKey, - r#" - SELECT DISTINCT - ON (hashed_key) hashed_key, - miniblock_number, - operation_number - FROM - storage_logs - WHERE - miniblock_number BETWEEN $1 AND $2 - ORDER BY - hashed_key, - miniblock_number DESC, - operation_number DESC - "#, - i64::from(l2_blocks_to_prune.start().0), - i64::from(l2_blocks_to_prune.end().0) - ) - .instrument("hard_prune_batches_range#get_latest_logs") - .with_arg("l2_blocks_to_prune", &l2_blocks_to_prune) - .report_latency() - .fetch_all(self.storage) - .await - } - - /// Removes storage logs overwritten by the specified new logs. - async fn prune_storage_logs(&mut self, new_logs: &[StorageLogPrimaryKey]) -> DalResult { - let (hashed_keys, block_numbers, operation_numbers): (Vec<_>, Vec<_>, Vec<_>) = new_logs - .iter() - .map(|log| { - ( - log.hashed_key.as_slice(), - log.miniblock_number, - log.operation_number, - ) - }) - .multiunzip(); + ) -> DalResult { + // Storage log pruning is designed to use deterministic indexes and thus have predictable performance. + // + // - The WITH query is guaranteed to use the block number index (that's the only WHERE condition), + // and the supplied range of blocks should be reasonably small. + // - The main DELETE query is virtually guaranteed to use the primary key index since it removes ranges w.r.t. this index. + // + // Using more sophisticated queries leads to fluctuating performance due to unpredictable indexes being used. let execution_result = sqlx::query!( r#" - DELETE FROM storage_logs USING UNNEST($1::bytea[], $2::BIGINT[], $3::INT[]) AS new_logs (hashed_key, miniblock_number, operation_number) + WITH + new_logs AS MATERIALIZED ( + SELECT DISTINCT + ON (hashed_key) hashed_key, + miniblock_number, + operation_number + FROM + storage_logs + WHERE + miniblock_number BETWEEN $1 AND $2 + ORDER BY + hashed_key, + miniblock_number DESC, + operation_number DESC + ) + DELETE FROM storage_logs USING new_logs WHERE storage_logs.hashed_key = new_logs.hashed_key AND (storage_logs.miniblock_number, storage_logs.operation_number) < (new_logs.miniblock_number, new_logs.operation_number) "#, - &hashed_keys as &[&[u8]], - &block_numbers, - &operation_numbers + i64::from(l2_blocks_to_prune.start().0), + i64::from(l2_blocks_to_prune.end().0) ) .instrument("hard_prune_batches_range#prune_storage_logs") - .with_arg("new_logs.len", &new_logs.len()) + .with_arg("l2_blocks_to_prune", &l2_blocks_to_prune) .report_latency() .execute(self.storage) .await?; diff --git a/core/node/db_pruner/src/metrics.rs b/core/node/db_pruner/src/metrics.rs index 1070ad842703..0d4d88513dbc 100644 --- a/core/node/db_pruner/src/metrics.rs +++ b/core/node/db_pruner/src/metrics.rs @@ -16,7 +16,6 @@ enum PrunedEntityType { L1Batch, L2Block, StorageLog, - OverwritingLog, // not really removed; just used to measure query complexity Event, L2ToL1Log, CallTrace, @@ -44,7 +43,6 @@ impl DbPrunerMetrics { let HardPruningStats { deleted_l1_batches, deleted_l2_blocks, - overwriting_logs, deleted_storage_logs, deleted_events, deleted_call_traces, @@ -52,13 +50,12 @@ impl DbPrunerMetrics { } = stats; tracing::info!( "Performed pruning of database, deleted {deleted_l1_batches} L1 batches, {deleted_l2_blocks} L2 blocks, \ - {deleted_storage_logs} storage logs ({overwriting_logs} overwriting logs), \ + {deleted_storage_logs} storage logs, \ {deleted_events} events, {deleted_call_traces} call traces, {deleted_l2_to_l1_logs} L2-to-L1 logs" ); self.deleted_entities[&PrunedEntityType::L1Batch].observe(deleted_l1_batches); self.deleted_entities[&PrunedEntityType::L2Block].observe(deleted_l2_blocks); - self.deleted_entities[&PrunedEntityType::OverwritingLog].observe(overwriting_logs); self.deleted_entities[&PrunedEntityType::StorageLog].observe(deleted_storage_logs); self.deleted_entities[&PrunedEntityType::Event].observe(deleted_events); self.deleted_entities[&PrunedEntityType::L2ToL1Log].observe(deleted_l2_to_l1_logs);