文字产生以前,结绳记事是人类用来存储知识和信息的主要方式。此后,从竹简、纸张的发明,到工业时代的磁盘存储,再到信息时代的数据库,存储方式不断革新,“存力”不断提高。
11月3日,在2022云栖大会上,蚂蚁链历经4年技术攻关与测试验证的区块链存储引擎LETUS(Log-structured Efficient Trusted Universal Storage)正式发布。
这一款面向区块链可信数据存储的技术产品,不仅用来解决当前蚂蚁链及区块链产业的规模化发展问题,也面向Web3时代提供“可信存力”支撑。
我们认为,随着大量的数据和数字资产在数字化世界里流转,可信数据的“存力”将如同电力网络的承载力一样重要。
本文希望通过对LETUS的深入技术解读,回答读者们普遍关心的关键问题:LETUS是什么?主要解决哪些问题?为什么坚持用“可验证结构”?为什么要自研?以及未来要走向何处?
从2009年序号为0的创世块诞生至今已过去十多年,“中本聪”依然神秘,但区块链技术的发展却因为公链、token、开源的推动,没有丝毫神秘感。
经过几代技术演进,在比特币的UTXO模型基础上诞生了应用更为广泛、支持可编程智能合约的区块链技术:通过密码学、共识算法、虚拟机、可信存储等技术,多个参与方执行相同的“指令”,来完成同一个业务逻辑,如账户转账,或者合约调用,维护不可篡改和不可伪造的业务数据。
简单讲,可将这类账本数据库,看作一个去中心化防作恶、防篡改的复制状态机,所执行的是智能合约描述的业务逻辑,而状态机通过日志 (区块数据)产生新的状态(状态数据):
区块数据:包括交易、回执、世界状态Root Hash等信息,和数据库系统中的日志类似,但是块之间由Hash锚定防篡改,并且不会删除。(区块数据记录的是区块链上发生的每一笔交易,如:Alice向Bob转账xx)
状态数据:记录账户、资产、业务合约数据等状态信息,和数据库系统中表数据类似,需要实现可验证可追溯。(状态数据记录的是区块链上每个账户或智能合约的当前状态,如:Bob账户剩余xx)
链上数据的特点可以总结为以下三个:
持续增长:从创世块开始,账本数据随交易持续增长,保留周期长;
多版本:交易修改状态数据产生新版本,系统提供历史版本查询和验证功能;
可验证:交易和账户状态通过Merkle根哈希(Merkle Root Hash)锚定在区块头,通过SPV(simple payment verification,简单支付证明)提供存在性证明;

区块链应用通过可验证数据结构(Authenticated Data Structure,如Merkle tree)实现可验证和可追溯。我们认为,Web3“存力”一个非常重要的要素是可验证,而今天我们看到的区块链存储瓶颈大多来源于可验证结构ADS(如Merkle tree)的低效存取和查询,这正是蚂蚁链LETUS重点攻克的难题。
随着时间推移和链上交易的增加,对存储容量的要求也不断增长,随之而来的是区块数据存储成本的大幅提升;与此同时,链上状态数据规模也持续增加,可验证数据结构持续膨胀,导致交易性能随账户规模提升和历史状态数据增加而持续下降。
2019年,蚂蚁链上线了一个供应链金融业务,大家特别兴奋。但是,这种兴奋并没有维持多久,随着程序跑的时间越来越长,问题慢慢暴露出来。
供应链金融是面向ToB的,不像ToC端随时都有数据,可能会在某个时刻(比如每天晚上)有一笔状态数据非常大的交易进来,跑了一个星期后发现性能越来越慢。
链平台TPS的衰减和存储直接相关,而与共识、虚拟机都无关,随着业务合约持续写入数据,存储性能大幅衰减。
如果要在技术上长时间支持亿级账户规模、每天能稳定支撑亿级交易量,存储的规模和性能问题必须要攻克。
期间,团队也曾试过各种技术方法对他进行优化,得到一些缓解。但多次尝试之后发现,随着数量增加而出现的性能衰减,是一个绕不开的瓶颈,需要从本质上解决。
我们需要从问题表象分析背后的原因。
区块链应用通过可验证数据结构实现可验证和可追溯,但是可验证数据结构会带来读写放大(问题1)和数据局部性(问题2)。
而存储系统为了实现数据管理,需要对数据分页/分层、排序,如KV数据库基于LSM-tree将数据分层有序存储,而MySQL之类的数据库将数据分页,也会基于B-tree数据结构来排序索引。

业界现有的实现方式,大多采用基于LSM架构的通用 Key-Value 数据库,在数据库之上运行一个独立Merkle树来实现可验证,如:
以太坊:MPT(Merkle Patricia Tree)+LevelDB
Diem:JMT(Jellyfish Merkle Tree)+RocksDB
背后的核心矛盾为:
除此之外,吞吐、延时等存储性能(问题3)、持续增长下的存储成本(问题4)、单机存储下的规模瓶颈(问题5)也都是需要解决的问题。
在过去几年的快速发展中,区块链的业务场景对交易吞吐量和响应时间要求越来越高,很多技术也被推动迭代发展,如PBFT、HoneyBadger、MyTumbler等高性能共识算法,BTN等网络基础设施,JIT加持的WASM虚拟机、以及高效的并行执行技术。
但比较而言,存储的性能对区块链平台整体性能影响非常大。对面向2C场景的数字藏品类业务(如鲸探,需支持秒杀),交易TPS与延时要求极为苛刻;而对需要在链上保存大量数据的存证类业务,大容量存储带来的成本又十分可观。
要支撑业务的长期可持续发展,我们归纳出区块链存储面临的核心挑战:
这些问题在行业内很普遍。业界技术路线主要分三条:
根据公开信息,目前区块链产品中主流的MPT + LevelDB、JMT + RocksDB、MySQL等存储架构,没有能全部解决上述5个问题的方案,难以在支持多版本和可验证的同时,满足10亿级账户规模下的高性能、易扩展、低成本的业务要求。
我们自研了一套区块链存储引擎LETUS(Log-structured Efficient Trusted Universal Storage),保证完整的可验证、多版本能力,既满足区块数据不可篡改、可追溯、可验证等要求,也提供对合约数据友好访问、存储规模可分片扩展,高性能低成本等特性。同时也满足通用性,统一管理区块数据、状态数据。

LETUS在2022云栖大会发布
4年前不敢想象的能力,现在具备了(以下数据为统一环境下的测试结果):
但这背后是一个技术架构的跨越,从下图左边的可验证数据结构+KV数据库架构,升级为现在的LETUS存储引擎,架构更简洁,系统更高效。

如Alice给Bob转账,只需要写增量数据,不需要写入7个Merkle树节点,数据局部性更友好,如Alice和Bob的账户数据,按区块号有序,不再hash随机。
回顾这四年,主要经历的三个大的阶段。
第一年里,为了满足业务急迫诉求,我们需要在有限时间内,实现亿级账户规模和交易TPS。先从已有系统入手,深度优化了状态树,基于开源MPT到自研FDMT,同时调优RocksDB数据库、增加并发、提升介质性能。
一系列优化措施缓解了问题,但依然无法根本解决,例如数据规模增加后,写放大依然有几十倍,数据在底层存储里依然随机分布。
为了能彻底解决上述所有问题,我们不得不重新思考存储引擎的设计。
针对读写放大(问题1)、数据局部性(问题2)和性能(问题3),我们结合区块链特征,如可验证数据结构的读写行为、链上数据的多版本诉求、只追加和不可篡改等,重新设计存储引擎的架构分层、关键组件、索引数据结构:
根据区块链特征,我们根据可验证数据结构的读写行为、链上数据的多版本诉求,重新设计存储引擎的架构分层、关键组件、索引数据结构:

基于这些核心设计,实现了成本降低的同时性能提升,链平台交易TPS、延时等性能指标不会随着数据规模的提升而衰减。
虽然存储资源占用大幅降低后,但是链上数据依然面临持续增长带来的高成本问题(问题4)。
基于LETUS架构的后台数据治理框架,我们能很方便的扩展实现数据迁移/压缩/垃圾回收等治理策略,基于这些策略,为用户提供进一步降成本能力,并针对自己的业务特点来选择使用:
针对问题5,LETUS采用分布式存储架构,实现单个共识参与方计算和存储分离,计算层和存储层可分别部署独立集群,通过高性能网络通讯框架进行数据读写访问。
为了对海量状态数据进行灵活的数据分片,并且保证各个区块链的参与方hash计算的一致性,将数据切片为256个最小存储单元(msu),并将一个或者多个msu构成一个状态数据分片(partition),将所有数据分片调度到多个物理机器。从而实现规模弹性扩展,解决了单机存储的容量瓶颈和带宽瓶颈。
为了全面落地铺开的同时让业务平稳运行,能够开着飞机换引擎,在这几年的研发过程里,我们充分准备、循序渐进的分阶段落地:

2021年5月,基于LETUS存储引擎的区块数据冷热分层,在版权存证业务灰度上线,存储成本降低71%,解决容量瓶颈并降低运维成本。
2021年8月,基于LETUS存储引擎的状态数据,在数字藏品平台“鲸探”双写灰度上线,并成功支撑秒杀场景;
2022年2-6月,LETUS引擎的历史状态数据裁剪、存储服务架构升级等生产ready,在数字藏品和版权存证等业务全面落地,并从灰度双写切为单写;LETUS单写意味着对硬件资源要求大幅下降,我们将“鲸探”生产环境的云资源全面降配,降配后链平台性能水位提升200%,同时存储成本下降75%。
蚂蚁一直坚持“成熟一个开放一个”的技术战略。同样的,LETUS不只为蚂蚁链定制,也同样给其他联盟链、公链提供高性能、低成本的支持。
蚂蚁链坚持技术自研,确保在共识协议、智能合约、网络传输、存储引擎、跨链技术、区块链隐私计算等领域处于全球领先水平。我们始终认为,坚持技术自主研发是建立长期可持续竞争力的关键。
在“可信存力”这条赛道上,我们也需要为进一步的技术壁垒提前布局,如合约结构化查询语言,为链上合约实现结构化+可验证的查询能力,提升开发者体验;Fast-Sync与节点多形态,提升组网效率和节点成本灵活性;以及Web3等潜在的技术生态。
技术创新永远在路上。接下来,继续沿着硬核技术方向突破,啃一些硬骨头,持续为整个价值互联网提供可靠的、可持续的存力。
文 | LETUS技术负责人 田世坤
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
总的来说,我对ruby还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚
Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack
在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/
我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为
我有一大串格式化数据(例如JSON),我想使用Psychinruby同时保留格式转储到YAML。基本上,我希望JSON使用literalstyle出现在YAML中:---json:|{"page":1,"results":["item","another"],"total_pages":0}但是,当我使用YAML.dump时,它不使用文字样式。我得到这样的东西:---json:!"{\n\"page\":1,\n\"results\":[\n\"item\",\"another\"\n],\n\"total_pages\":0\n}\n"我如何告诉Psych以想要的样式转储标量?解