什么是 AccountsDB

在 Solana 中,所有数据都存储在所谓的“帐户”中。Solana 上数据的组织方式类似于键值存储,其中数据库中的每个条目都称为“帐户”。AccountsDB 是一个由验证器存储的帐户数据库,用于跟踪帐户数据。因此,它是 Solana 的重要组成部分。

快照

当新的验证者启动时,它必须首先赶上链的当前状态;在 Solana 中,这是通过快照实现的。快照包含特定时隙的区块链的完整状态(包括所有帐户)。它们是从网络中的现有验证者请求/下载的,用于引导新的验证者(而不是从 Genesis 开始)。

快照包含两个主要组件:

  • 元数据文件,包含有关最新区块和数据库状态的信息
  • 帐户文件夹,其中包含保存帐户数据的文件。

下面是插槽处标准 Solana 快照的图表196493007,其中突出显示了两个组件。

注意:快照是以 Zstandard 格式压缩的 tar 存档形式构建和下载的,因此文件扩展名通常为.tar.zst。从快照加载时,第一步是将其解压缩并解压到如上所示的目录布局中。

帐户文件

快照中的主要组件之一是帐户文件,其中包含特定槽位的所有帐户数据。每个文件都以字节为单位组织成帐户列表。

注意:在 Rust 实现中,包含帐户的文件被称为 AppendVecs。然而,这个名字已经过时了,因为代码库中不再使用附加功能,所以我们将account files在本文中引用它们。

读取这些数据涉及:

  1. 将快照解压并解压到单独的账户文件中
  2. 将每个账户文件单独加载到内存中(通过mmap系统调用)
  3. 读取每个文件的整个长度并将其组织成 AccountsDB 代码可以使用的结构

账户文件格式如下:

注意:由于帐户的数据是可变长度数组,因此我们首先需要读取data_len变量,这样我们就知道要读入数据字段的字节数。在图中,帐户标题存储有关帐户的有用元数据(包括data_len),而帐户数据包含实际数据字节。

解析文件中所有帐户的伪代码如下:

账户索引

现在我们了解了账户文件是如何组织起来存储账户数据的,我们还需要通过账户的公钥来组织这些数据,即我们需要建立一个从给定公钥到文件中对应账户位置的映射。此映射也称为账户索引。

更具体地说,我们需要一个从公钥到 的元组的映射,(file_id, offset)其中file_id是要读取的帐户文件的名称, 是文件中帐户字节的索引。通过此映射,我们可以通过打开与 关联的文件并读取从 开始的字节来offset访问公钥的关联帐户。file_idoffset

帐户也可以跨槽更改(例如,快照可能包含跨不同槽的同一帐户的多个版本)。将每个帐户的公钥与多个文件位置的集合(或“Vec”)相关联。在 Agave 中,每个槽始终只有一个帐户文件。这意味着我们可以将帐户索引构造为Map<Pubkey, Vec<(file_id, offset)>>,它将从公钥映射到帐户引用集合。

要读取帐户的最新状态,我们还需要找到具有最高插槽的参考,这样我们也可以跟踪与每个相关联的插槽file_id。

由于 Solana 上有如此多的账户,索引可能会变得非常大,从而导致对 RAM 的需求很大。为了减少这些需求,Solana 使用了基于磁盘的哈希图

账户引用默认存储在磁盘上。当访问并从磁盘读取公钥时,其账户引用将缓存在基于 RAM 的哈希图中,以便以后更快地访问。

当读取给定公钥的账户时,一般流程如下:

  • 首先检查 RAM 索引,
  • 如果公钥不存在,请检查基于磁盘的索引,
  • 如果不存在,则表明该帐户不存在。

阅读帐户

如果我们想从特定帐户读取数据,并且我们知道该帐户的公钥,我们可以通过在帐户索引中查找其公钥来找到其在帐户文件中的位置。我们将从最近的插槽中选择帐户引用,然后从帐户文件中的该位置读取数据。验证器可以直接将此数据解释为帐户结构。

撰写账户

AccountsDB 的另一个关键组件是跟踪新帐户状态。例如,当验证器处理新的交易块时,结果就是在特定时隙中产生一批新帐户状态。然后需要将这些新状态写入数据库。

编写这些帐户的两个主要步骤包括:

  1. 将账户数据写入与槽关联的新账户文件中
  2. 更新索引以指向这些新帐户的位置(即,为文件中的每个帐户附加一个新的(slot,file_id,offset)项)

后台线程

当新的帐户状态写入数据库时​​,我们必须通过执行 4 个关键任务来确保有效利用内存

  • Flushing
  • Cleaning
  • Shrinking
  • Purging

Flushing

为了减少内存需求,数据会定期从 RAM 刷新到磁盘。

例如,索引的基于 RAM 的哈希图充当最近访问的数据的缓存,并且当该数据在一段时间内未被使用时,它会被刷新到磁盘以便为更新的帐户索引腾出空间。

另一个示例是,新帐户状态首先存储在 RAM 中,只有当相关插槽成为根时才会刷新到磁盘。这种方法减少了(慢速)磁盘写入次数,只写入根数据。

Cleaning

为了限制内存增长,我们必须清理旧数据。

例如,如果我们有一个账户的两个版本,一个在槽位10,另一个在槽位15,并且槽位15是根(即不会被回滚),那么我们可以删除槽位10的账户对应的账户和索引数据。

清理的另一个例子是当一个帐户的 Lamport 数量降至零时,在这种情况下,我们可以删除整个帐户。

注意:清理阶段会清除索引条目,但不会回收账户文件中被账户数据占用的存储区域。清理后,该区域将被视为账户文件中的垃圾数据,这会浪费存储空间和网络带宽。

Shrinking

账户清理后,账户文件将包含“活跃”和“死亡”账户,其中“死亡”账户已被“清理”,因此不再需要。当账户文件中活跃账户数量较少时,可以将活跃账户复制到较小的文件中,而不包含死亡账户,从而节省磁盘内存。这称为收缩。

Purging

最后,我们还可以清除从根链分叉出来的槽的整个帐户文件。例如,如果分叉的其中一个分支成为根,那么我们可以删除与非根分支相关的所有帐户文件。

实现细节

介绍了 AccountsDB 工作原理的高级组件后,接下来我们将深入探讨实现细节。

从快照加载

我们将从更详细的概述开始,描述验证器如何从快照加载。

解压快照

从网络中的对等点下载快照通常包括:

  1. 完整快照
  2. 增量快照

完整快照包括网络上某个特定时隙的所有账户。增量快照是较小的快照,仅包含与完整快照相比发生变化的账户。例如,如果网络位于时隙 100,则完整快照可能包含时隙 75 的所有账户,而匹配的增量快照可能包含时隙 75 和时隙 100 之间发生变化的所有账户。

完整快照的创建成本通常很高,因为它们包含网络上的所有帐户,而增量快照的创建成本相对较低,因为它们仅包含网络帐户的子集。鉴于这一现实,验证者的典型方法是每隔一段时间创建一次完整快照,并更频繁地创建/更新增量快照。

完整快照遵循以下命名约定格式:

snapshot-{FULL-SLOT}-{HASH}.tar.zst

例如,snapshot-10-6ExseAZAVJsAZjhimxHTR7N8p6VGXiDNdsajYh1ipjAD.tar.zst 是插槽 10 处的完整快照,其哈希值为6ExseAZAVJsAZjhimxHTR7N8p6VGXiDNdsajYh1ipjAD。

增量快照遵循命名约定格式:

incremental-snapshot-{FULL-SLOT}-{INCREMENTAL-SLOT}-{HASH}.tar.zst.

例如,incremental-snapshot-10-25-GXgKvm3NMAPgGdv2verVaNXmKTHQgfy2TAxLVEfAvdCS.tar.zst是一个增量快照,它基于来自插槽 10 的完整快照构建,并包含直到插槽 25 的帐户更改,并且具有的哈希值GXgKvm3NMAPgGdv2verVaNXmKTHQgfy2TAxLVEfAvdCS。

匹配快照和增量快照将具有相同的{FULL-SLOT}值。当验证器启动时,由于验证器可以一次下载多个快照,因此它将找到最新的快照和匹配的增量快照来加载并启动。

下载快照

要下载快照,验证者需要开始参与八卦以加入网络并识别其他节点。一段时间后,我们会寻找以下节点:

  • 具有匹配的碎片版本(即我们的网络版本/硬分叉与其匹配)
  • 拥有有效的 RPC 套接字(即,我们可以从它们那里下载)
  • 已经通过 gossip 分享了快照数据类型

快照哈希结构是一个八卦数据类型,其中包含:

  • 最大可用完整快照的槽位和哈希值
  • 以及可用增量快照的插槽和哈希列表

注意:快照哈希结构可以在此处查看。

然后,验证器开始下载快照,优先下载来自较高槽位的快照。如果我们有一个验证器列表trusted(启动时通过 CLI 提供),我们只会下载哈希值与受信任验证器哈希值匹配的快照。

然后对于每个节点,我们根据快照哈希结构中的信息构建快照的文件名:

  • 满的:snapshot-{slot}-{hash}.tar.zstd
  • 增量:incremental-snapshot-{base_slot}-{slot}-{hash}.tar.zstd

使用节点的 IP 地址、RPC 端口和文件路径,我们开始下载快照。我们会定期检查下载速度,确保速度足够快,或者尝试从另一个节点下载。

一旦验证器找到可用的快照,它将首先解压缩并解存档它们,从而形成如下所述的目录布局。

解压快照并构建相应银行和 AccountsDB 的主要代码路径通过运行调用的函数开始bank_from_snapshot_archives。

注意:银行是一种代表单个区块/槽的数据结构,它存储的信息包括父银行、其槽、其哈希值、区块高度等。
有关快照的规范,请参阅 Richard Patel 的《Solana 快照非正式指南》。

加载账户文件

解压快照的第一步是解压并加载账户文件,该过程发生在verify_and_unarchive_snapshots函数中。
在此函数中,会生成多个线程来从存档中解压和解包快照。解包快照后,会清理每个帐户文件,以确保它们包含有效帐户、映射到内存中并在数据库中进行跟踪。

注意:当验证器为某个槽位构建快照并打包这些帐户文件时,由于实现的原因,验证器将继续将帐户数据写入较新槽位的帐户文件中。由于此数据与快照的槽位不对应,因此应忽略此数据。这意味着,当从文件读取帐户时,我们需要从快照元数据文件(更具体地说是 AccountsDB 元数据)中读取长度,以了解要读取的有用数据的最后一个索引。
注意:在较旧的快照版本中,每个插槽可能有多个帐户文件,但在新版本中,每个插槽只有一个文件。
注意:在 Rust 实现中,帐户文件也称为storages和。stores

加载快照元数据

加载账户文件后,验证器使用中的 Bincode 格式从快照元数据文件中加载 AccountsDB 和银行元数据rebuild_bank_from_unarchived_snapshots。

注意:Bincode 是一种编码方案(https://github.com/bincode-org/bincode

银行元数据包括诸如纪元时间表、权益信息、区块高度等信息。AccountsDB 元数据包括诸如每个帐户文件中使用的数据长度、用于识别数据损坏的所有帐户的累积哈希值等信息。

注意:快照元数据文件位于/snapshots/{slot}/{slot}未存档快照中{slot}快照插槽的路径下。
注意:有关完整字段的列表,请参阅src/core/snapshot_fields.zigSig 存储库或参阅BankFieldsToDeserializeRustAccountsDbFields实现。

生成账户索引

使用 AccountsDB 元数据,验证器使用函数构建 AccountsDB 结构reconstruct_accountsdb_from_fields。然后使用 将帐户文件加载到数据库中AccountsDB :: initialize,并使用 生成索引AccountsDB :: generate_index。

索引的架构将索引划分为许多存储桶。每个存储桶都是一个迷你索引,仅代表一小部分账户。每个公钥都与基于公钥的前 N ​​位的特定存储桶相关联。每个存储桶都包含 RAM(使用简单的 HashMap -InMemAccountsIndex在代码库中称为)和磁盘存储(在代码库中称为Bucket使用 mmap'd 文件)

注意:RAM 映射使用 a作为其键,在 Rust 代码库中Vec<(Slot, File_id, Offset)>也称为 a 。slot list
注意:基于前 N 位的分组也类似于他们对 crds-shards 的方法(如本系列第 1 部分所述,涵盖 Gossip 协议)。
注意:Rust 代码中使用的 bucket 数量默认为 8192。

完整的架构如下图所示:

注意:为了确保在运行验证器时所有索引都存储在 RAM 中,您可以使用 --disable-accounts-disk-index

这些组件按照以下布局存储在 AccountsDB 结构中:

后台线程:刷新数据

由于基于 RAM 的索引存储了最近访问的索引数据,因此后台线程(在代码库中称为BgThreads)用于将旧索引数据刷新到磁盘,以便为更新的索引数据腾出空间。此刷新逻辑是使用AccountsInMemIndex :: flush_internal函数完成的。基于 RAM 的索引条目一旦“过期”,就会刷新到磁盘/存储桶内存中。年龄参数是根据我们希望将索引条目刷新到磁盘的频率配置的,并且与其插槽的根源时间以及其他一些原因有关。

基于磁盘的索引

RAM 索引是 RAM 内存支持的哈希图,而磁盘索引是磁盘内存支持的哈希图。虽然两者都是哈希图实现,但磁盘索引是在 Rust 存储库中从头实现的,解释起来更复杂。

磁盘索引的主要结构是Bucket包含BucketStorage存储基于文件的 mmap 文件的结构。此 mmap 文件将作为 hashmap 的后备内存。

为了支持 put、get 和 delete 方法,BucketStorage 需要实现一个名为 的特征BucketOccupied来识别哪些内存是空闲的,哪些内存被占用了。

这意味着BucketStorage商店:

  1. 一个 mmap 文件(mmap下图中的变量),以及
  2. 实现 BucketOccupied 的结构来跟踪哪些索引是空闲的/被占用的(contents下图中的变量)

代码库中有两种 BucketOccupied 特性的实现:

  • IndexBucket
  • DataBucket

每个Bucket实例包含一个IndexBucket和一个向量DataBuckets,如下所示:

当账户只有一个slot版本时,索引存储在 中IndexBucket。当账户有多个slot版本时,索引数据存储在 中DataBucket。

IndexBucket

在 mmap 文件中存储IndexBucket一个“IndexEntries”列表来表示索引数据,并使用BitVec存储每个索引的枚举来识别它是空闲的还是被占用的。

每个索引可能的枚举值包括:

  • Free:索引未被占用
  • ZeroSlots:索引已被占用,但尚未写入
  • SingleElement:只有一个插槽用于存储公钥,并且直接存储索引数据
  • MultipleSlots:为公钥存储了多个槽(并且索引信息存储在其中一个 DataBuckets 中)

下图解释了 IndexBucket 的组织结构:

请注意 mmap 文件如何包含 IndexEntries 列表,并且 BitVec 标识如何读取内容:

  • 图中第二条记录,BitVec的值为2,即OneSlotInIndex,因此single_element读取IndexEntry中的字段,直接获取账户参考值。
  • 在图表的最后一项中,BitVec 的值为 3,即MultipleSlots,因此multiple_slots读取 IndexEntry 中的字段,用于从数据存储桶中读取帐户引用(下一节将详细讨论)。

注意:由于有 4 个不同的枚举值可以有效地表示它们,我们只需要 2 位,这意味着我们可以使用 u64 来表示 64 / 2 = 32 个不同的索引。这就是BitVec结构的作用。

注意:完整实现类似于由磁盘内存支持的平面哈希图。对公钥进行哈希处理以获取开始搜索的索引,然后进行线性探测以找到匹配的密钥,但是,由于这是一个相对简单的实现,因此与 RAM 哈希图相比,它仍然很慢。

DataBuckets

当读取时IndexBucket,并且某个条目的 BitVec 值为MultipleSlots,则意味着 pubkeys 索引数据存储在 之一中DataBuckets。该MultipleSlots结构存储:

  • 存储在索引(字段中)中的插槽数num_slots,用于计算DataBucket要读取的 内容
  • 要读取的偏移量DataBucket(在offset字段中)。代码如下所示:

由于BucketStorage结构体包含 N DataBuckets,因此DataBucket将给定索引存储在哪个位置取决于公钥包含的以 2 的幂为增量的槽数。例如:

  • 包含 0-1 个槽的索引数据存储在第一个DataBucket
  • 包含2-3个槽的索引数据存储在第二个DataBucket
  • 包含4-7个槽的索引数据存储在第三个DataBucket
  • ...

查找pubkeys索引并读取对应值的逻辑写在了Bucket :: read_value中bucket_map/src/bucket.rs,读取对应值的代码如下:

生成索引

当验证器启动时,将使用generate_index_for_slot在每个帐户文件上调用的函数(并行)生成索引,以从文件中提取所有帐户并将每个帐户引用放置在其相应的索引箱中。

这也是生成二级索引的地方。

二级索引

考虑一下,如果你想获取数据库中具有特定所有者的所有帐户。为此,你需要遍历索引中的所有公钥,访问帐户文件中的帐户,并对帐户的所有者字段进行相等性检查。这就是getProgramAccounts代码中臭名昭著的 RPC 调用所做的事情。

如果有大量账户,这种线性搜索可能会非常昂贵,而且速度非常慢。

由于这种搜索非常常见,二级索引会存储具有特定所有者的公钥列表。这样,您便可以仅访问您知道具有特定所有者的公钥的完整帐户数据,从而使该过程更加高效。

功能、内置程序和系统调用

在对所有帐户进行索引后,将根据元数据创建银行,存储对完整 AccountsDB 的引用,并使用Bank :: finish_init激活功能和加载内置程序和系统调用的功能完成。

如果快照槽大于功能激活槽,则将激活功能。此逻辑在 中处理Bank :: apply_feature_activations。

一些示例功能包括:

  • blake3_syscall_enabled
  • disable_fees_sysvar
  • ETC

完整的功能列表可以FEATURE_NAMES在代码库中的变量下或此处的 GitHub wiki 页面上找到。

内置程序也添加到库中,Bank :: add_builtin所有内置程序都可以在BUILTINS变量名称下找到。这包括以下程序:

  • The system program
  • The vote program
  • The stake program

Bank :: fill_missing_sysvar_cache_entries最后,在加载时钟、纪元计划、租金等变量的 函数中加载系统变量。

阅读和写作记述

在所有账户都被索引之后,下一个要了解的重要部分是如何读取和写入账户

阅读帐户

在处理新区块时,验证器首先根据交易中定义的公钥加载相关账户。在搜索公钥的账户时,它会执行以下操作:

  • 通过考虑前 N 位来计算公钥应该属于的相应 bin
  • 然后搜索该 bin 的 RAM 内存
  • 如果不在 RAM 中,那么它会尝试在磁盘上查找
  • 如果它在磁盘上找到索引数据,它会将数据存储在 RAM 中,以便下次更快地查找

在Rust代码中,读取账户的代码路径从调用哪个函数开始,找到与公钥对应的索引数据。 AccountsDB :: do_load_with_populate_read_cacheAccountsDB :: read_index_for_accessor_or_load_slow

该函数首先找到相应的索引箱,然后调用InMemAccountsIndex:: get_internal它来搜索 RAM 和磁盘以查找索引数据。

主要流程的代码片段如下:

找到公钥的索引数据后,由于数据是跨不同槽的账户引用向量,因此使用最大的槽来标识账户的最新版本。

索引数据可以指向三个可能的位置

  • 在磁盘上的帐户文件中(可以使用file_id和偏移量查找)
  • 读缓存
  • 写缓存

我们已经讨论了如何从帐户文件中读取数据,但我们还没有讨论的两个位置是读取和写入缓存。读取缓存是另一个缓存层,在从帐户文件中读取帐户后,它会使用完整的帐户数据进行更新(不同于索引缓存)。 写入缓存包含尚未根化的插槽中的完整帐户数据(下一节将对此进行更多讨论)。

在代码中,使用并读取帐户retry_to_get_account_accessor,LoadedAccountAccessor :: check_and_get_loaded_account并为三个可能的位置中的每一个加载帐户。

撰写账户

处理完一组交易后,我们现在有了一组与特定槽相对应的新账户,这些账户我们想要存储和索引。为此,我们首先将账户存储到名为的写入缓存AccountsDB.accounts_cache中,并更新索引以指向缓存位置。

定期地,使用后台线程将此缓存(对应于根槽)中的帐户数据刷新到磁盘(下一节讨论)。

代码流从“store_cached”开始,它收集块中设置为write使用的所有账户collect_accounts_to_store,然后将它们存储在缓存中并使用函数进行索引store_cached_inline_update_index。

AccountsDB 后台服务

由于我们已经讨论了 AccountsDB 中的读/写工作方式,接下来我们将讨论 AccountsDB 如何确保内存得到有效利用。

这包括4个主要的后台任务:

  • Flushing 刷新:将数据从 RAM 写入磁盘
  • Cleaning 清理:查找旧帐户或零 Lamport 帐户并释放内存
  • Shrinking 缩减:缩减有大量死账户的账户文件
  • Purging 清除:删除分叉账户文件

Flushing,Cleaning,Shrinking均在AccountsBackgroundService结构中进行,

  • 获取当前根银行
  • 定期刷新根槽
  • 清理被清除的账户
  • 最后,运行收缩过程

主要代码解释如下:

下图解释了完整的流程:

Flushing

第一个后台任务是刷新,它从写入缓存中读取数据,并通过新帐户文件将与根槽关联的帐户推送到磁盘。每个根槽都会有一个关联的帐户文件,其中包含该槽中发生更改的帐户。

1)收集根槽和 2)将这些槽刷新到磁盘的完整代码路径从 开始bank.flush_accounts_cache()。
bank.flush_rooted_accounts_cache()通过读取来收集未刷新的根槽bank.maybe_unflushed_roots,以相反的顺序遍历根并使用将每个根刷新到磁盘。flush_slot_cache_with_clean

将账户存储到磁盘的主要功能是AccountsDB :: store_accounts_custom将其store_to参数设置为Disk,它首先计算存储所有账户所需的总字节数,分配一个足够大的新账户文件,然后将账户写入其中。

将帐户写入磁盘后,索引将更新以指向新的帐户位置AccountsDB :: update_index。

注意:请注意上面的函数flush_slot_cache_with_clean, 后缀为with_clean。这暗示该函数在冲洗时还执行清洁任务(下面将更全面地介绍),它确实执行了清洁任务。

注意:未刷新的根以相反的顺序迭代,因为我们只刷新第一次出现的公钥。例如,如果unflushed_roots = [12, 19] 两个根中都存在和一个公钥,我们将只刷新槽 19 中的公钥帐户。为了实现这一点,代码在名为 的闭包中使用了一个公钥的 HashSet,should_flush_f该闭包在刷新槽时填充。

注意:此清理仅考虑缓存中的根,因此从技术上讲它并不是完全清理

在将根帐户状态刷新到磁盘时,有时新的根状态可能会导致帐户死亡。在迭代帐户以更新索引时,将使用被视为死亡的旧帐户数据填充“回收”数组。将帐户文件视为“死亡”并将文件排队以进行收缩的逻辑位于 中AccountsDB :: handle_reclaims。

根据清理情况,可能存在没有或只有少量活跃账户的账户文件。如果活跃账户为零,则账户文件被视为已死,所有相应账户数据将被删除。如果活跃账户数量较少(“少量”由活跃账户中的字节数定义),则账户文件将添加到shrink_canidate数组中以排队等待收缩。

下面是解释整个过程的伪代码:

此后,新的插槽被添加到“uncleaned_roots”,稍后用于删除零lampor帐户。

Cleaning

清理会找到没有 Lamport 的账户或具有可以删除的旧历史记录的账户,从索引中删除这些账户,然后将足够小的账户文件排队以进行缩小。

代码路径以 开头,它首先通过读取解析函数中其他根槽的 (pubkey, slot) 元组的变量AccountsDB :: clean_accounts来收集所有需要清理的公钥。 uncleaned_pubkeysAccountsDB :: construct_candidate_clean_keys

有关 的更多信息unclean_pubkeys:一旦新区块完全处理完毕,银行就会使用 进行冻结bank.freeze(),除其他事项外,它还会计算bank_hash唯一标识该区块的哈希值。此哈希值是其他哈希值的组合,包括区块中修改的所有帐户的哈希值,称为account_delta_hash。为了计算此值,calculate_accounts_delta_hash将调用该函数,该函数还会将所有帐户的公钥添加到uncleaned_pubkeys变量中。由于仅在刷新后调用清理流程,因此此清理将处理最近刷新的所有帐户。

对于所有这些公钥,零 Lamport 账户和旧账户是分开处理的。

为了处理旧帐户,clean_accounts_older_than_root会调用清除小于根槽的槽(除了最近的根槽),从索引的槽列表中删除该值。如果在此过程之后槽列表为空,则从索引中删除该条目。

在此过程中,被删除的索引值将被跟踪reclaimed(与刷新过程相同),稍后将被删除的账户计入dead其各自的账户文件中,然后导致账户文件死亡或成为缩减的候选。

处理零 Lamport 账户时,将保存该账户的对应账户文件减alive_account_count一,将该账户定义为dead,并删除索引数据。

然后,与刷新流程的结束类似,对于每个账户文件,如果文件仅包含少量alive账户,则将其添加到shrink_canidates变量中以排队等待缩减。

Shrinking

收缩操作会查找活动数量较少的账户文件,然后删除死账户并重新分配文件,以减少使用的磁盘内存量。

缩减的代码路径从shrink_candidate_slots读取变量中存储的所有槽的函数开始shrink_candidate_slots。对于每个槽,do_shrink_slot_store都会调用该函数,该函数会alive在槽的帐户文件中查找帐户。

要确定帐户是否有效,alive它会在索引中查找 (pubkey, slot)。如果索引中不存在元组,则认为该帐户有效dead,并将其存储在unrefed_pubkeys变量中。否则,如果帐户有效(并且索引中存在元组),则将其添加到alive_accounts变量中。

收集所有活跃账户后,它会将这些账户写入磁盘中一个大小合适的新账户文件,并删除旧账户文件。

Purging

最后,当一个新的槽被根化时,作为分叉一部分的银行将从写缓存中清除,并且通过Drop在银行结构上实现特征来删除索引数据。

原文:https://www.anza.xyz/blog/a-deep-dive-into-solana-s-accountsdb