Skip to content

Commit 6d8d212

Browse files
int88michaelsproul
andcommitted
use state cache to optimise historical state lookup (#4228)
## Issue Addressed #3873 ## Proposed Changes add a cache to optimise historical state lookup. ## Additional Info N/A Co-authored-by: Michael Sproul <[email protected]>
1 parent 45835f6 commit 6d8d212

File tree

8 files changed

+90
-8
lines changed

8 files changed

+90
-8
lines changed

beacon_node/src/cli.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,13 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
519519
.help("Specifies how many blocks the database should cache in memory [default: 5]")
520520
.takes_value(true)
521521
)
522+
.arg(
523+
Arg::with_name("historic-state-cache-size")
524+
.long("historic-state-cache-size")
525+
.value_name("SIZE")
526+
.help("Specifies how many states from the freezer database should cache in memory [default: 1]")
527+
.takes_value(true)
528+
)
522529
/*
523530
* Execution Layer Integration
524531
*/

beacon_node/src/config.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,12 @@ pub fn get_config<E: EthSpec>(
383383
.map_err(|_| "block-cache-size is not a valid integer".to_string())?;
384384
}
385385

386+
if let Some(historic_state_cache_size) = cli_args.value_of("historic-state-cache-size") {
387+
client_config.store.historic_state_cache_size = historic_state_cache_size
388+
.parse()
389+
.map_err(|_| "historic-state-cache-size is not a valid integer".to_string())?;
390+
}
391+
386392
client_config.store.compact_on_init = cli_args.is_present("compact-db");
387393
if let Some(compact_on_prune) = cli_args.value_of("auto-compact-db") {
388394
client_config.store.compact_on_prune = compact_on_prune

beacon_node/store/src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use types::{EthSpec, MinimalEthSpec};
77
pub const PREV_DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 2048;
88
pub const DEFAULT_SLOTS_PER_RESTORE_POINT: u64 = 8192;
99
pub const DEFAULT_BLOCK_CACHE_SIZE: usize = 5;
10+
pub const DEFAULT_HISTORIC_STATE_CACHE_SIZE: usize = 1;
1011

1112
/// Database configuration parameters.
1213
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -17,6 +18,8 @@ pub struct StoreConfig {
1718
pub slots_per_restore_point_set_explicitly: bool,
1819
/// Maximum number of blocks to store in the in-memory block cache.
1920
pub block_cache_size: usize,
21+
/// Maximum number of states from freezer database to store in the in-memory state cache.
22+
pub historic_state_cache_size: usize,
2023
/// Whether to compact the database on initialization.
2124
pub compact_on_init: bool,
2225
/// Whether to compact the database during database pruning.
@@ -43,6 +46,7 @@ impl Default for StoreConfig {
4346
slots_per_restore_point: MinimalEthSpec::slots_per_historical_root() as u64,
4447
slots_per_restore_point_set_explicitly: false,
4548
block_cache_size: DEFAULT_BLOCK_CACHE_SIZE,
49+
historic_state_cache_size: DEFAULT_HISTORIC_STATE_CACHE_SIZE,
4650
compact_on_init: false,
4751
compact_on_prune: true,
4852
prune_payloads: true,

beacon_node/store/src/hot_cold_store.rs

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ pub struct HotColdDB<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> {
6262
pub hot_db: Hot,
6363
/// LRU cache of deserialized blocks. Updated whenever a block is loaded.
6464
block_cache: Mutex<LruCache<Hash256, SignedBeaconBlock<E>>>,
65+
/// LRU cache of replayed states.
66+
state_cache: Mutex<LruCache<Slot, BeaconState<E>>>,
6567
/// Chain spec.
6668
pub(crate) spec: ChainSpec,
6769
/// Logger.
@@ -129,6 +131,7 @@ impl<E: EthSpec> HotColdDB<E, MemoryStore<E>, MemoryStore<E>> {
129131
cold_db: MemoryStore::open(),
130132
hot_db: MemoryStore::open(),
131133
block_cache: Mutex::new(LruCache::new(config.block_cache_size)),
134+
state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)),
132135
config,
133136
spec,
134137
log,
@@ -162,6 +165,7 @@ impl<E: EthSpec> HotColdDB<E, LevelDB<E>, LevelDB<E>> {
162165
cold_db: LevelDB::open(cold_path)?,
163166
hot_db: LevelDB::open(hot_path)?,
164167
block_cache: Mutex::new(LruCache::new(config.block_cache_size)),
168+
state_cache: Mutex::new(LruCache::new(config.historic_state_cache_size)),
165169
config,
166170
spec,
167171
log,
@@ -977,40 +981,70 @@ impl<E: EthSpec, Hot: ItemStore<E>, Cold: ItemStore<E>> HotColdDB<E, Hot, Cold>
977981

978982
/// Load a frozen state that lies between restore points.
979983
fn load_cold_intermediate_state(&self, slot: Slot) -> Result<BeaconState<E>, Error> {
984+
if let Some(state) = self.state_cache.lock().get(&slot) {
985+
return Ok(state.clone());
986+
}
987+
980988
// 1. Load the restore points either side of the intermediate state.
981989
let low_restore_point_idx = slot.as_u64() / self.config.slots_per_restore_point;
982990
let high_restore_point_idx = low_restore_point_idx + 1;
983991

992+
// Use low restore point as the base state.
993+
let mut low_slot: Slot =
994+
Slot::new(low_restore_point_idx * self.config.slots_per_restore_point);
995+
let mut low_state: Option<BeaconState<E>> = None;
996+
997+
// Try to get a more recent state from the cache to avoid massive blocks replay.
998+
for (s, state) in self.state_cache.lock().iter() {
999+
if s.as_u64() / self.config.slots_per_restore_point == low_restore_point_idx
1000+
&& *s < slot
1001+
&& low_slot < *s
1002+
{
1003+
low_slot = *s;
1004+
low_state = Some(state.clone());
1005+
}
1006+
}
1007+
1008+
// If low_state is still None, use load_restore_point_by_index to load the state.
1009+
let low_state = match low_state {
1010+
Some(state) => state,
1011+
None => self.load_restore_point_by_index(low_restore_point_idx)?,
1012+
};
1013+
9841014
// Acquire the read lock, so that the split can't change while this is happening.
9851015
let split = self.split.read_recursive();
9861016

987-
let low_restore_point = self.load_restore_point_by_index(low_restore_point_idx)?;
9881017
let high_restore_point = self.get_restore_point(high_restore_point_idx, &split)?;
9891018

990-
// 2. Load the blocks from the high restore point back to the low restore point.
1019+
// 2. Load the blocks from the high restore point back to the low point.
9911020
let blocks = self.load_blocks_to_replay(
992-
low_restore_point.slot(),
1021+
low_slot,
9931022
slot,
9941023
self.get_high_restore_point_block_root(&high_restore_point, slot)?,
9951024
)?;
9961025

997-
// 3. Replay the blocks on top of the low restore point.
1026+
// 3. Replay the blocks on top of the low point.
9981027
// Use a forwards state root iterator to avoid doing any tree hashing.
9991028
// The state root of the high restore point should never be used, so is safely set to 0.
10001029
let state_root_iter = self.forwards_state_roots_iterator_until(
1001-
low_restore_point.slot(),
1030+
low_slot,
10021031
slot,
10031032
|| (high_restore_point, Hash256::zero()),
10041033
&self.spec,
10051034
)?;
10061035

1007-
self.replay_blocks(
1008-
low_restore_point,
1036+
let state = self.replay_blocks(
1037+
low_state,
10091038
blocks,
10101039
slot,
10111040
Some(state_root_iter),
10121041
StateRootStrategy::Accurate,
1013-
)
1042+
)?;
1043+
1044+
// If state is not error, put it in the cache.
1045+
self.state_cache.lock().put(slot, state.clone());
1046+
1047+
Ok(state)
10141048
}
10151049

10161050
/// Get the restore point with the given index, or if it is out of bounds, the split state.

book/src/advanced_database.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ the `--slots-per-restore-point` flag:
5858
lighthouse beacon_node --slots-per-restore-point 32
5959
```
6060

61+
### Historic state cache
62+
63+
Lighthouse includes a cache to avoid repeatedly replaying blocks when loading historic states. Lighthouse will cache a limited number of reconstructed states and will re-use them when serving requests for subsequent states at higher slots. This greatly reduces the cost of requesting several states in order, and we recommend that applications like block explorers take advantage of this cache.
64+
65+
The historical state cache size can be specified with the flag `--historic-state-cache-size` (default value is 1):
66+
67+
```bash
68+
lighthouse beacon_node --historic-state-cache-size 4
69+
```
70+
6171
## Glossary
6272

6373
* _Freezer DB_: part of the database storing finalized states. States are stored in a sparser

book/src/api-lighthouse.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ curl "http://localhost:5052/lighthouse/database/info" | jq
456456
"config": {
457457
"slots_per_restore_point": 2048,
458458
"block_cache_size": 5,
459+
"historic_state_cache_size": 1,
459460
"compact_on_init": false,
460461
"compact_on_prune": true
461462
},

book/src/database-migrations.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ curl "http://localhost:5052/lighthouse/database/info"
9292
"slots_per_restore_point": 8192,
9393
"slots_per_restore_point_set_explicitly": true,
9494
"block_cache_size": 5,
95+
"historic_state_cache_size": 1,
9596
"compact_on_init": false,
9697
"compact_on_prune": true
9798
}

lighthouse/tests/beacon_node.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,6 +1669,25 @@ fn block_cache_size_flag() {
16691669
.with_config(|config| assert_eq!(config.store.block_cache_size, 4_usize));
16701670
}
16711671
#[test]
1672+
fn historic_state_cache_size_flag() {
1673+
CommandLineTest::new()
1674+
.flag("historic-state-cache-size", Some("4"))
1675+
.run_with_zero_port()
1676+
.with_config(|config| assert_eq!(config.store.historic_state_cache_size, 4_usize));
1677+
}
1678+
#[test]
1679+
fn historic_state_cache_size_default() {
1680+
use beacon_node::beacon_chain::store::config::DEFAULT_HISTORIC_STATE_CACHE_SIZE;
1681+
CommandLineTest::new()
1682+
.run_with_zero_port()
1683+
.with_config(|config| {
1684+
assert_eq!(
1685+
config.store.historic_state_cache_size,
1686+
DEFAULT_HISTORIC_STATE_CACHE_SIZE
1687+
);
1688+
});
1689+
}
1690+
#[test]
16721691
fn auto_compact_db_flag() {
16731692
CommandLineTest::new()
16741693
.flag("auto-compact-db", Some("false"))

0 commit comments

Comments
 (0)