RAM Rekt!EOS存储陷阱

RAM是EOS存储的一个陷阱。这个讨论是关于灾难性智能合约数据设计的案例研究……以及如何解决它。这是技术性的,但任何人都可以在大部分时间跟得上,无论你是否是程序员。

我目前正在构建Achieveos:一个成就,奖杯或徽章系统的开放平台,将其数据写入EOS区块链。我们的想法是,你的成就应该永远存在,即使授予他们的组织早已消失。因为区块链上的数据是永远的,所以你应该去看看。

让我们弄清楚我们将如何存储这些永恒的成就。

EOS存储基础知识

乍一看,EOS智能合约存储系统看起来就像传统的数据库(只是忽略了混乱的C++语法):

1
2
3
4
5
struct [[eosio::table]] Organization {
uint64_t key;
string organization_name;
uint64_t primary_key() const { return key; }
};

Organization表有一个键字段,它是我们插入Organization的每行数据的唯一整数标识符。它还有一个organization_name用于显示目的。因此,key==0Organization可能是“Keith’s Gymnastics Team”,而key==1可能是“Barb’s Book Club”。

我希望能够在可自定义的类别中设置Organization的各种可能成就。对于我的体操运动员,我希望获得“Strength Goals”以及每个器械的具体成就(例如“Floor Exercise Achievements”)。

这很简单:

1
2
3
4
5
6
struct [[eosio::table]] Category {
uint64_t key;
uint64_t organization_id;
string category_name;
uint64_t primary_key() const { return key; }
};

看起来就像Organization表一样,但是这个组织有一个organization_id,它是对该类别Category所属Organization的引用。

所以我可以创建一个属于organization_id=0(“Keith’s Gymnastics Team”)的分类,并给它的acategory_name赋值“Strength Goals”。

很棒,到目前为止,这与大多数程序员已经熟悉的传统数据库完全相同。

但是有一个“但是”。

要付出的比你想象的要多

任何区块链上的数据存储总是有点贵;你要求整个网络永远存储你的数据。他们不会免费这样做。

幸运的是,到目前为止,我们的数据结构非常小。uint64_t字段是64位整数(1字节中有8位,因此每个uint64_t占用8个字节)。String中的每个字符都是一个字节。

因此,对于“Keith’s Gymnastics Team”(23个字符),总内存占用量为:

8 + 23 = 31个字节

EOS将你的数据存储在RAM中(使用硬盘驱动器会太慢)。EOS RAM存储的成本目前约为0.293美元/kB。1kB中有1,024个字节。因此,存储我们的第一个Organization的成本是:

(31字节/1024字节/kB$0.293/kB = $0.00887**

不到1美分就能永远存储是不是不错?

但是当我实际创建该组织Organization时,我的用户的RAM实际上是255个字节。等等,这比我预期的要多8倍!

真正的存储成本

经过大量的测试和大量的谷歌搜索,我终于找到了解释:

从我可以看出第一个组织Organization条目的成本是:

  • 为新用户创建表:112个字节。
  • 每个新行的开销:112个字节。
  • 该行的实际数据:31个字节。

表创建费用是每个用户的一次性费用。但是每个新行的112字节开销很重要。

Whelp,这不会起作用

让我们快速算一下,看看我们的其他数据存储需要如何发展。

我们需要一个Achievements表来存储不同的可能目标,奖杯等。每个成就Achievement都将引用其父组织Organization及其所属的类别Category

对于将要追求这些成就的所有人,我们需要一张Users表。

最后是UserAchievements表,用于指示哪些用户Users被授予了哪些成就Achievements

现在假设我们为500位用户Users提供了100种可能的Achievements,我们希望普通用户可以获得30个UserAchievements。为简单起见,我们只关注上面看到的开销成本:每行112个字节。

100 + 500 + 30*500 = 15,600行

15,600行*112字节/行/1024字节/kB = 1,706 kB

1,706 kB * $0.293/kB = $499.93

请记住,这只是开销。我们甚至没有计算存储我们实际数据的成本!

我的天啊!算了吧!

Vectors

请注意,所有行都很昂贵。实际的数据——这里有一个23字节的字符串,那里有几个8字节的整数——很小且很便宜。

如果我们重新设计数据结构以最小化所需的行数,该怎么办?

让我们重新审视我们的前两个表:组织Organization和类别CategoryCategory在其自己的表中的唯一原因是具有唯一的reference key

1
Achievement.category_id = 5 //文件在id是5的类别下归档

事实证明,我们可以使用Organization表中的C++向量Vector来完成同样的事情:

1
2
3
4
5
6
struct [[eosio::table]] Organization {
uint64_t key;
string organization_name;

vector<string> categories; uint64_t primary_key() const { return key; }
};

vector只是一个列表。因此,我的“Keith’s Gymnastics Team”组织Organization的新类别categories Vector 可能如下所示:

1
2
3
4
5
6
categories = [
"Strength Goals",
"Floor Exercise Achievements",
"Pommel Horse Achievements",
...
]

每个条目都有一个基于其位置的整数索引:

1
2
3
categories[0] == "Strength Goals"
categories[1] == "Floor Exercise Achievements"
categories[2] == "Pommel Horse Achievements"

这些指数——0,1,2——是指代该组织Organization内每个类别的独特,一致的方式。实际上,它们与先前的Category.key具有相同的目的。

但关键的区别在于,所有这些类别现在都包含在他们所属的组织Organization内。之前的Category表及其所有昂贵的每行开销已经完全消除。

更具体地说

在原始表结构中,新的Category将花费我们112字节的开销,加上数据本身的大小。

因此,将“Post-Season Training Goals”(26个字符)加上其关联的8字节key加上其8字节的引用添加到其父组织organization_id将花费:

112 + 26 + 8 + 8 = 154字节

但是在我们的新方法中,我们只需modify()现有的Organization行,并在类别向量的末尾添加一个新元素。存储的总净变化只是:

(现有行大小)+ 26字节等于增加26字节

与之前的方法相比,这节省了惊人的84%!

经验教训:vectors,vectors,无处不在!

因此,忘记关于为数据库构建数据的所有知识。伙计们,我们处在一个勇敢的新世界!最终的组织Organization表可以保存与该组织相关的所有内容——在一行中。这里详细介绍了完整的详细信息,但是最终版本在单个EOS存储表中使用了一些实用程序结构来统治它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Category {
string name;
vector<string> achievements;
};
struct UserAchievementsList {
vector<uint64_t> userachievements;
};
struct User {
string name;
map<uint64_t, UserAchievementsList> bycategory;
};
// The ONE and ONLY storage table!
struct [[eosio::table]] Organization {
uint64_t key;
string organization_name;
vector<Category> categories;
vector<User> users;
uint64_t primary_key() const { return key; }
};

成本改善

让我们为我的第一个类别“Strength Goals”添加一个新的成就:

1
org.categories[0].achievements.push_back("5-Minute Plank");

总净RAM支出:14个字节。

假设新的成就最终成为其类别中的索引ID 4。现在我们将授予新的Achievement给user_id为53:

1
org.users[53].bycategory[0].userachievements.push_back(4);

如果这是第一个从类别0授予user_id为53的成就,那么我们必须将0和4写入区块链。

总净RAM支出:16字节。

我们已经完全消除了杀手级的管理费用。

厉害吧!告诉你的RAM配额秘密,“不用客气。哈哈”

警告:如果我们最终通过将所有内容集中在一个日益臃肿的单个数据对象周围来达到其他限制,那还有待观察。但是,除非得到证实,否则我仍然会胜利的。

奖励:二级指数是双重打击

这更加令人讨厌,但EOS中的每个表都在其主键上编制索引。索引只是意味着它已经过优化,可以根据该键快速定位行。

与常规数据库一样,在其他字段上建立索引通常也是有利的,以便在这些字段上快速查找。想象一下UserAchievements表的一个版本,它引用了user_idachievement_id和它自己的主键。你可以很容易地想象想要快速检索特定用户的所有UserAchievementsuser_id上的索引)或者被授予特定成就的每个人的列表(achievement_id上的索引)。

太棒了,EOS可以做到这一点。

但是你为每个二级索引支付额外的开销。每行开销112字节基本上是索引主键的成本。你添加的每个附加索引每行都会产生另外112个字节!因此,对于我们的UserAchievements表,我们将支付:

  • 每行112字节主键索引
  • 每行user_id上的112字节二级索引
  • 每行的achievement_id上有112字节的二级索引
  • 实际数据为8 + 8 + 8 = 24字节(三个8字节整数)

这是360字节,只记录24字节的实际数据!!

如果需要的话,谨慎使用二级索引!

======================================================================

分享一些比特币、以太坊、EOS等区块链相关的交互式在线编程实战教程:

  • EOS入门教程,本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
  • 深入浅出玩转EOS钱包开发,本课程以手机EOS钱包的完整开发过程为主线,深入学习EOS区块链应用开发,课程内容即涵盖账户、计算资源、智能合约、动作与交易等EOS区块链的核心概念,同时也讲解如何使用eosjs和eosjs-ecc开发包访问EOS区块链,以及如何在React前端应用中集成对EOS区块链的支持。课程内容深入浅出,非常适合前端工程师深入学习EOS区块链应用开发。
  • java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
  • php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
  • c#比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在C#代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是C#工程师不可多得的比特币开发学习课程。
  • java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
  • python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
  • php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
  • 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
  • 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
  • ERC721以太坊通证实战,课程以一个数字艺术品创作与分享DApp的实战开发为主线,深入讲解以太坊非同质化通证的概念、标准与开发方案。内容包含ERC-721标准的自主实现,讲解OpenZeppelin合约代码库二次开发,实战项目采用Truffle,IPFS,实现了通证以及去中心化的通证交易所。
  • C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。
  • Hyperledger Fabric 区块链开发详解,本课程面向初学者,内容即包含Hyperledger Fabric的身份证书与MSP服务、权限策略、通道配置与启动、链码通信接口等核心概念,也包含Fabric网络设计、nodejs链码与应用开发的操作实践,是Nodejs工程师学习Fabric区块链开发的最佳选择。
  • Hyperledger Fabric java 区块链开发详解,课程面向初学者,内容即包含Hyperledger Fabric的身份证书与MSP服务、权限策略、频道配置与启动、链码通信接口等核心概念,也包含Fabric网络设计、java链码与应用开发的操作实践,是java工程师学习Fabric区块链开发的最佳选择。
  • tendermint区块链开发详解,本课程适合希望使用tendermint进行区块链开发的工程师,课程内容即包括tendermint应用开发模型中的核心概念,例如ABCI接口、默克尔树、多版本状态库等,也包括代币发行等丰富的实操代码,是go语言工程师快速入门区块链开发的最佳选择。

汇智网原创翻译,转载请标明出处。这里是RAM Rekt!EOS存储陷阱