---
title: 持久账户存储
---

## 持久账户存储

账户集代表了验证节点处理过的所有交易的当前计算状态。 每个验证节点都需要维护这整个集合。 网络提出的每一个区块都代表着对这个集合的改变，由于每个区块都是一个潜在的回滚点，所以改变需要是可逆的。

NVME等持久性存储比DDR便宜20到40倍。 持久性存储的问题是，写和读的性能比DDR慢很多，必须注意数据的读写方式。 读取和写入都可以在多个存储驱动器之间分割，并行访问。 本设计提出了一种数据结构，允许存储的并发读取和并发写入。 写入通过使用AppendVec数据结构进行优化，允许单个写入者进行追加，同时允许多个并发读取者访问。 账户索引维护一个指针，指向每次分叉追加账户的位置，从而消除了对状态的显式检查点的需求。

## AppendVec

AppendVec是一个数据结构，它允许随机读取与单一的纯追加写入同时进行。 增长或调整AppendVec的容量需要独占访问。 这是用一个原子`offset`来实现的，它在一个完成的追加结束时更新。

AppendVec的底层内存是一个内存映射的文件。 内存映射文件允许快速的随机访问，分页由操作系统处理。

## 账户索引

账户索引的设计是为了支持所有当前分叉账户的单一索引。

```text
type AppendVecId = usize;

type Fork = u64;

struct AccountMap(Hashmap<Fork, (AppendVecId, u64)>);

type AccountIndex = HashMap<Pubkey, AccountMap>;
```

该索引是账户公钥的映射到分叉的映射，以及AppendVec中账户数据的位置。 要想获得一个特定分叉的账户版本。

```text
/// Load the account for the pubkey.
/// This function will load the account from the specified fork, falling back to the fork's parents
/// * fork - a virtual Accounts instance, keyed by Fork.  Accounts keep track of their parents with Forks,
///       the persistent store
/// * pubkey - The Account's public key.
pub fn load_slow(&self, id: Fork, pubkey: &Pubkey) -> Option<&Account>
```

通过指向存储偏移量的`AppendVecId`中的内存映射位置来满足读取。 可以返回一个没有拷贝的引用。

### 验证节点根分叉

[塔式BFT](tower-bft.md)最终选择一个分叉作为根分叉，分叉被压扁。 被压扁的/根分叉不能回滚。

当一个分叉被压扁时，它的父账户中所有还没有出现在分叉中的账户都会通过更新索引被拉升到分叉中。 被压扁的分叉中余额为零的账户会通过更新索引从分叉中移除。

当一个账户被_压扁_导致无法访问时，可以将其垃圾回收。

有三种可能的选择。

- 维护一个HashSet的根分叉。 预计每秒钟创建一个。 整个树可以在以后被垃圾回收。 另外，如果每个分叉都保持一个账户的引用计数，那么在更新索引位置时，垃圾收集可能会发生。
- 从索引中删除任何修剪过的分叉。 任何剩余的比根号低的分叉都可以被认为是根号。
- 扫描索引，将任何旧的根迁移到新的索引中。 任何比新根数低的剩余分叉都可以在以后删除。

## 只写附录

所有对账户的更新都是以纯追加更新的方式进行的。 每一次账户更新，AppendVec中都会存储一个新版本。

可以通过在一个分叉中返回一个已经存储的账户的可变引用来优化单个分叉内的更新。 银行已经跟踪账户的并发访问，并保证对特定账户分叉的写与对该分叉中的账户的读不会同时发生。 为了支持这个操作，AppendVec应该实现这个函数。

```text
fn get_mut(&self, index: u64) -> &mut T;
```

该API允许对`index`的内存区域进行并发的可变更访问。 它依靠银行保证对该索引的独家访问。

## 垃圾收集

随着账户的更新，它们会移动到AppendVec的末尾。 一旦容量用完，可以创建一个新的AppendVec，并将更新的内容存储在那里。 最终，对旧的AppendVec的引用将消失，因为所有的账户都已更新，旧的AppendVec可以被删除。

为了加快这个过程，可以将最近没有更新的账户移到新的 AppendVec 的前面。 这种形式的垃圾收集可以在不需要对任何数据结构进行独占锁的情况下完成，除了索引更新。

垃圾收集的初始实现是，一旦AppendVec中的所有账户成为陈旧版本，它就会被重用。 账户一旦被追加，就不会被更新或移动。

## 索引回收

在追加过程中，每个银行线程都有对账户的独占访问权，因为在数据提交之前，账户锁不能被释放。 但是在独立的AppendVec文件之间没有明确的写入顺序。 为了创建一个顺序，索引维护了一个原子写版本计数器。 每一次对AppendVec的追加都会在AppendVec中账户的条目中记录该追加的索引写版本号。

为了恢复索引，所有的AppendVec文件可以以任何顺序读取，并且每次分叉的最新写版本应该存储在索引中。

## 快照

要进行快照，需要将AppendVec中的底层内存映射文件刷新到磁盘。 索引也可以写到磁盘上。

## 性能

- 只进行追加写入的速度很快。 SSD和NVME，以及所有操作系统级别的内核数据结构，都允许在PCI或NVMe带宽允许的情况下以最快的速度运行追加(2,700 MB/s)。
- 每个重放和银行线程都会同时写入自己的AppendVec。
- 每个AppendVec可能会被托管在一个单独的NVMe上。
- 每个重放和银行线程都可以并发读取所有AppendVec，而不会阻止写入。
- 索引需要一个专属的写锁进行写入。 HashMap更新的单线程性能在每秒10m左右。
- Banking和Replay阶段应该使用每个NVMe的32个线程。 NVMe使用32个并发读取器或写入器具有最佳性能。
