Sig 是一个用 Zig 语言编写的 Solana 验证器客户端,旨在优化读取性能(RPS),解决 Solana 网络中常见的 slot lag 问题。Sig 通过提高客户端多样性、提供更易读的代码库和利用 Zig 语言的技术优势,旨在提升 Solana 的用户体验和网络稳定性,并提供前所未有的可访问性。
Sig 是一个智能优化的 Solana 验证器实现,使用 Zig 编写,Zig 是一种前沿的底层编程语言。
对于在节点上触发的每个“写入” sendTransaction
RPC 调用,会触发 25 个“读取”调用。根据涵盖 Syndica 使用的各种 DApp 超过 2 年的数据,所有对节点发起的调用中,有 96.1% 是读取。尽管如此,在讨论吞吐量时,大多数注意力都集中在每秒交易数 (TPS) 指标上。Sig 的构建考虑了不同的范例:优化 每秒读取数 (RPS)。
以最终用户为中心构建的验证器客户端,不仅要在用户点击“提交交易”按钮时优先考虑流畅的用户体验,还要从他们加载页面时,到他们决定提交交易时,再到他们审核交易后更新的钱包余额时,都要优先考虑流畅的用户体验。区块链用户体验必须是稳健且“快速”的 端到端。
快速、可靠读取的重要性只会增加 - 随着区块链扩展,越来越多的数字世界存储在链上,读取将对 L1 提出不成比例的更大需求。Solana 在这方面今天的表现如何?不幸的是,不如领先的替代链以太坊。
与以太坊节点相比,Solana 节点经常落后。以下是一项研究,比较了过去 90 天内 Solana 和以太坊生态系统中公共和私有 RPC 提供商之间的插槽延迟。
较深的虚线表示严重的插槽延迟,峰值达到 50 个插槽或更多;Solana 的图表充满了这些虚线,而以太坊的图表则相对清晰。
DApp 上的用户体验取决于 L1 在多大程度上满足了预期。虽然以太坊的设计比 Solana 慢(分别为 12 秒和 400 毫秒的插槽时间),但 Solana 的速度受到整个生态系统中难以捉摸的 RPC 体验的阻碍。
Solana 插槽延迟通常是大量读取调用的结果。
根据对链上程序进行横截面抽样获得的数据,Solana 节点在性能下降(延迟飙升,通常导致插槽延迟)之前,每秒只能处理约 60 次 getProgramAccounts
调用。其他“重”方法的阈值也类似:仅 107 次 getSignaturesforAddress
调用、269 次 getTransaction
调用和 301 次 getBlocks
调用中的每一次都会降低节点的性能。(这是有道理的,因为这三种方法都依赖于 Google Bigtable,我们观察到,在调用历史数据的情况下,Google Bigtable 会显着降低 Max RPS。)
“重”调用往往 非常 重;例如,节点每秒可以处理的 getHealth
调用次数是 getProgramAccounts
调用次数的 250 倍,这很有意义,因为众所周知 getProgramAccounts
调用是非常消耗资源的。将调用从最轻到最重排序,负载因子呈指数级增长,这只是 RPS 指标的衍生物:
方法负载因子近似于单个方法的调用相对于其他方法在 Solana 节点上的负载程度。它仅使用标准化反向 Max RPS 计算(1/Max RPS * 10,000)。
当然,Solana 节点会因特定方法类型而承受压力,这不仅取决于调用的理论权重,还取决于其使用频率:
百分比反映了 2023 年期间所有 Syndica RPC 使用情况的内部研究。 方法的标签被省略,这些方法占总体调用量的比例小于1.5%。在此分析中排除了不经常使用的方法(占所有调用的 <0.01%)。这些数字是对总体 Solana RPC 调用细分的近似值。
考虑到方法的使用频率,一些方法属于低 RPS、高频率范围:
5 种圈出的方法 - getMultipleAccounts
、getTokenAccountsbyOwner
、getTransaction
、getSignaturesforAddress
和 getProgramAccounts
- 都具有相同的特征:它们经常使用, 而且 对节点来说负担很重。Syndica 的内部数据表明,考虑到它们的使用频率,这五种读取方法代表了典型 Solana 节点上的大部分“压力”。理想情况下,所有方法都将落在散点图的左上角区域;当你向右下方看时,你会遇到“问题”方法区域。
重要的是,唯一“写入”方法 - sendTransaction
- 高悬在圆圈上方,因为相对而言,它对 Solana 节点的负担并不大。解决插槽延迟问题需要提高 读取 链的效率。换句话说,流畅的 Solana UX 需要增加 RPS。
Sig 是一种强调最大化 RPS 的验证器实现。验证器级别的 RPC 优化意味着更低的延迟、轻量级的优化 RPC 方法、更高的吞吐量和改进的 UX。
作为加密领域最具可扩展性的区块链,Solana 发挥着重要作用。与以太坊相比,Solana 在一个最关键的区块链维度上落后:客户端多样性。
技术故障的单点
以太坊拥有其共识/执行协议的 9 个独立实现;Solana 只有两个活跃的验证器客户端,其中第二个 - Jito Labs 的客户端 - 与原始客户端共享一些关键组件。
拥有各种验证器客户端最关键的一点是消除由错误引起的单点故障。以太坊网络上的事件证明了这一点。例如,2023 年 5 月,以太坊客户端上的一个错误导致客户端难以及时处理 attestation,因此区块无法最终确定,如 事后报告 中所述。此外,在新版本发布后,有 报告 称验证器客户端的内存使用量增加,导致内存不足错误和随后的崩溃。
如果所有用户都运行相同的客户端,这些错误可能会破坏整个网络。但是,由于整个网络中客户端种类繁多,因此保持了稳定性。客户端多样性对于像以太坊和 Solana 这样的区块链网络的正常运行时间至关重要。
“验证器客户端多样性不仅仅是提供针对零日漏洞的保护。如果一个客户端中存在错误,那么其他客户端中极不可能存在该错误。这意味着单个客户端中的错误不太可能导致长时间的网络中断,特别是如果多个客户端以高速率使用。”
Solana 基金会,2023 年 3 月
语言多样性
以太坊与 Solana 验证器语言
虽然以太坊的验证器已使用七种语言实现,但 Solana 验证器仅使用 Rust 实现,很快还将使用 C(Jump 的 FireDancer)和 Go(Jump 的 Radiance)。
“以特定语言为基础的客户端开启并邀请了对该语言的实验和创新。围绕客户端的基础工具通常会滚雪球般发展成该语言中强大的工具和贡献者生态系统。”
以太坊基金会研究员 Danny Ryan
Solana 的贡献者数量不到以太坊的五分之一。Solana 开发社区中的许多人认为 Solana 存储库难以阅读、理解和贡献。这是有道理的 - Solana (Rust) 实现是最初的实现,因此遭受了多年的膨胀和向后兼容性补丁的困扰,这可以理解地导致了不太理想的设计决策。
近似衡量代码库复杂性的一种方法是通过代码行数 (LOC),特别是查看代码库中最大的文件,因为最大的文件在整个代码库中占不成比例的百分比。例如,最大的前 1% Solana 验证器文件占总代码行数的 20%,而最大的前 10% 占总代码行数的 58%。
绘制最大的文件(前 1%)在最流行的以太坊验证器客户端和 Solana 中,Solana (Rust) 代码库明显更加冗长:
Sig 专注于可读性和简洁性,因为可访问性是验证器客户端最被低估的功能。创新、协议维护/可观察性和开发人员工具集创建需要协议的模块化和对贡献者友好的实现。
Sig 将提供 Solana 验证器客户端的第一个易于阅读的实现,这将允许其他人基于它构建并回馈社区。
Zig 在强大的开发社区中获得了显着的吸引力。
与其他新兴语言相比,Zig 的受欢迎程度有所增长。以下是一个 Google 趋势细分,将 Zig 与其他类似的最新发布的编程语言进行比较,其中 Zig 为深绿线:
此外,尽管 Zig 是一种非常早期的语言,没有 v1 版本,但它已经吸引了一些大公司,如 Uber、AWS 和 CloudFlare,它们使用 Zig 工具链。Uber 使用 bazel-zig-cc
进行交叉编译,而像 Oven 这样的初创公司,开发软件开发工具和 TigerBeetle,一个金融会计数据库,使用 Zig 作为其主要编码语言。其他使用 Zig 的公司包括 HexOps、Cyber 和 Extism。
最后,Zig 的 Github 提交活动显示出稳步进展:
Zig 软件基金会将 Zig 描述为“一种通用编程语言和工具链,用于维护健壮、优化和可重用的软件。”它在简单性、语言功能和可读性之间提供了健康的平衡,而又不牺牲性能,这正是深度硬件优化的验证器客户端实现所需要的。
Zig 提供简单性而不牺牲力量。它的入门门槛很低,任何具有 C、C++、Rust 甚至 Go 经验的开发人员都可以轻松上手 Zig。它缺乏隐藏的控制流,使得即使是更大的代码库也易于理解和消化。
该语言提供了使用自定义分配器的能力,这些分配器是用户定义的规范,用于管理程序如何分配和释放内存。当程序的部分经常分配到堆时,这尤其有用,允许你使用像 Arena Allocator 这样的东西来获得更好的性能。再加上显式分配,这是一种要求程序员有意识地分配和释放内存的功能,Zig 为我们提供了对内存使用的高度控制,这是资源管理的重要方面,对于硬件优化的分布式系统至关重要。
Zig 提供的性能与最高效的底层语言 C 相当。Zig 还具有与 C 和 C++ 的互操作性,允许直接包含 C 或 C++ 库。这消除了对任何包装器或绑定的需求,简化了现有 C 或 C++ 代码的合并。
Zig 引入了“编译时”,这是一种允许在编译时执行代码某些部分的功能。此功能对于将潜在的运行时计算移动到编译时特别有用,从而减少了运行时期间的计算负载,并带来了更高效的程序,以及无需直接使用泛型或接口的简单元编程。如果没有有效的编译时元编程,则必须求助于宏或代码生成,或者更糟糕的是,在运行时做大量无用的工作。
Zig 能够实现几个以性能为中心的功能,这些功能将提高 RPS。
Zig 的通用 C 互操作性:
Zig 的一个强大方面是它与所有 C 代码的无缝互操作性。这种互操作性使我们能够利用庞大的 C 库、框架和工具生态系统,从它们的优化实现中受益。通过将 Zig 的现代语言功能与现有 C 代码的性能优化相结合,你可以获得两全其美,并可以使用现代编程语言功能(如显式错误处理、富有表现力的返回类型等)实现高性能,从而减少代码中的错误/bug,更易于理解的代码库,提高可维护性,并增强开发人员的生产力。
例如,以下是一个 Zig 代码片段,演示了 Zig 和 C 代码的无缝集成,利用高性能系统调用 recvmmsg
和 sendmmsg
以在单个系统调用中有效地处理 多个 消息:
const c = @cImport({
@cInclude("<sys/socket.h>");
@cInclude("<netinet/in.h>");
@cInclude("<stdlib.h>");
@cInclude("<stdio.h>");
@cInclude("<string.h>");
@cInclude("<errno.h>");
});
const std = @import("std");
const assert = std.debug.assert;
pub fn main() !void {
const port: u16 = 12345;
const num_messages: usize = 3;
var msgs: [num_messages][]const u8 = .{
"Hello from Zig!",
"Another message from Zig!",
"A third message from Zig!",
};
// Create a socket
const sockfd = c.socket(c.AF_INET, c.SOCK_DGRAM, 0);
if (sockfd < 0) {
@panic("socket creation failed");
}
defer c.close(sockfd);
// Prepare the server address
var servaddr: c.sockaddr_in = undefined;
servaddr.sin_family = c.AF_INET;
servaddr.sin_addr.s_addr = c.htonl(c.INADDR_ANY);
servaddr.sin_port = c.htons(@intCast(port));
// Bind the socket to the server address
if (c.bind(sockfd, @ptrCast(&servaddr), @sizeOf(c.sockaddr_in)) < 0) {
@panic("bind failed");
}
// Prepare mmsghdr and iovec arrays for sending
var send_mmsghdr: [num_messages]c.mmsghdr = undefined;
var send_iovec: [num_messages]c.iovec = undefined;
for (0..num_messages) |i| {
send_iovec[i].iov_base = @ptrCast(msgs[i].ptr);
send_iovec[i].iov_len = msgs[i].len;
send_mmsghdr[i].msg_hdr.msg_iov = &send_iovec[i];
send_mmsghdr[i].msg_hdr.msg_iovlen = 1;
}
// Send multiple messages using sendmmsg
const num_sent = c.sendmmsg(sockfd, @ptrCast(&send_mmsghdr), @intCast(num_messages), 0);
if (num_sent < 0) {
@panic("sendmmsg failed");
}
// Prepare mmsghdr and iovec arrays for receiving
var recv_mmsghdr: [num_messages]c.mmsghdr = undefined;
var recv_iovec: [num_messages]c.iovec = undefined;
var recv_buffers: [num_messages][1024]u8 = undefined;
for (0..num_messages) |i| {
recv_iovec[i].iov_base = @ptrCast(&recv_buffers[i]);
recv_iovec[i].iov_len = 1024;
recv_mmsghdr[i].msg_hdr.msg_iov = &recv_iovec[i];
recv_mmsghdr[i].msg_hdr.msg_iovlen = 1;
}
// Receive multiple messages using recvmmsg
const num_recv = c.recvmmsg(sockfd, @ptrCast(&recv_mmsghdr), @intCast(num_messages), 0, null);
if (num_recv < 0) {
@panic("recvmmsg failed");
}
// Print received messages
for (0..@intCast(num_recv)) |i| {
const received_len = recv_mmsghdr[i].msg_len;
const received_msg = recv_buffers[i][0..received_len];
std.debug.print("Received message {}: {s}\n", .{i + 1, received_msg});
}
}
Zig 使开发人员能够编写清晰、简洁的代码,从长远来看,这些代码是可维护和可扩展的,并且使读者能够一眼就对代码试图完成的任务有一个大致的了解。这是一个重要的功能,对 Solana 社区尤其有帮助,并将使他们能够继续创新、改进和回馈生态系统。
我们计划研究的其他一些优化方法包括:io_uring
用于高效的文件系统 I/O 和 eBPF XDP 用于高性能网络数据包处理。
io_uring 用于文件系统 I/O:
Zig 允许我们使用 io_uring
进行零系统调用文件系统 I/O,以优化 Bank 和 Blockstore 数据结构,从而实现更快的异步读/写操作。使用 io_uring
,我们可以更快地访问关键数据,这将改进 RPS。
eBPF XDP 用于网络:
我们计划使用 eBPF XDP(扩展的 Berkeley 数据包过滤器 eXpress 数据路径)来更有效地处理网络数据包。标准的网络数据包处理涉及进行昂贵的系统调用到内核,以将数据包读取到程序中。XDP 将能够在 Linux 内核中直接进行数据包过滤和操作,从而无需数据包遍历整个网络堆栈,从而显着降低延迟和开销。
我们正在通过处理任何区块链最重要的核心组件之一来开始 Sig 的开发:gossip 协议。Gossip 协议有效地将数据传播到整个网络中的节点。它是分布式系统的核心组件。以下是 Sig 的 gossip 运行中的预览:
Sig 的 gossip 间谍实际演示:Sig 的 gossip 间谍(顶部窗口)与 Solana 的 gossip(底部窗口)交互。
我们首先解决 gossip,因为它是节点进入网络的入口点,允许它识别网络中的其他节点,并接收和同步有关区块链状态的元数据。Solana 的 gossip 协议也与其他验证器组件(如区块传播 (Turbine)、运行时 (Sealevel)、共识 (Tower BFT) 等)隔离。这使我们能够快速构建完整客户端所需的一些核心原语/模块,例如序列化/反序列化框架、需要在客户端实现之间复制的数据结构、强大的测试框架和持久的构建管道。
Gossip 的工作原理
在网络中共享数据的一种直接方法是将数据广播到网络上的所有节点。但是,当网络包含大量节点时,这会将大量工作集中在一个节点上,这是不可取的。为了解决这个问题,可以将消息广播到节点子集,然后这些节点将消息广播到网络的其余部分,类似于树。以下是两种方法的图表。
Solana 的 gossip 协议使用树广播方法,并且基于 PlumTree 算法,进行了一些修改。为了理解 Solana 的 gossip 如何工作,首先了解 PlumTree 如何工作是有用的。
PlumTree 解释
PlumTree 算法的总体思路是将完整消息发送到节点子集(称为“活动集”),并将消息的哈希发送到所有其他节点。
哈希消息是一种数据高效的方式,用于通知其他节点它们应该很快收到完整消息。如果节点在一段时间内没有收到完整消息,它们将使用消息的哈希从另一个节点请求完整消息,并且两个节点都将彼此添加到其活动集中(在树中形成新的链接,用于数据传播)。此过程通过修复网络中损坏的数据传播路径来确保所有节点都收到完整消息。
当节点收到新的完整消息时,它会将该消息转发到其活动集 - 将数据传播到网络的其余部分。当节点收到之前已经收到的完整消息时,它们会向发送节点发送一条剪枝消息,然后发送节点会从其活动集中删除该节点(本质上,从树中删除一个分支)。
该算法形成了一个高效的树结构,用于在网络中进行数据传播。
Solana 如何改进 PlumTree
为了使该算法在节点可能存在对抗性的环境中工作,Solana 对该算法进行了一些更改。这包括定义五种消息类型:Push
、Prune
、Pull
、Ping
和 Pong
。
与 PlumTree 类似,Push
消息用于使用节点的活动集在网络中传播数据。当收到重复的 Push
消息时,会发送 Prune
消息以减少冗余数据传播。
Pull
消息用于确保所有节点都具有一致的数据(以防数据在 Push
消息中丢失)。Pull
消息会定期发送到一组随机节点,并包含一个布隆过滤器,该过滤器表示节点当前拥有的数据。收到 Pull
消息后,节点会搜索并响应用布隆过滤器中未包含的 gossip 数据。
最后,Ping
/Pong
消息用于检查节点是否仍然处于活动状态。具体来说,节点会定期 ping 其他节点,并期望收到相应的 Pong
响应。如果未收到 pong,则假定该节点处于非活动状态,并且不再发送 Push
/Pull
消息。
Solana Gossip 数据
gossip 协议传播的两个重要数据类型是 ContactInfo
和 Vote
数据结构。
联系信息结构是进入网络的入口点,用于发现和与网络中的其他节点进行通信。它包括有关哪些端口对应于特定任务的信息(包括区块/投票传播端口、RPC 端口、修复端口等)。投票结构是 Solana 共识协议的一部分,表示节点确认特定区块有效。
一个正常工作的 Gossip 实现对于节点至关重要,因为它能够与网络中的其他节点进行通信,并促进有效区块链的构建。因此,我们实现 Sig 的第一步就是一个正常工作的 Gossip 实现。
有关 Gossip 的更多信息,请参见此处:https://docs.solana.com/validator/gossip
我们将要实现组件的高级概述:
Syndica 旨在重写验证器实现的以下组件:
重放阶段
BlockStore (RocksDB)
Bank(帐户和状态)- AccountsDB
运行时/执行客户端(Sealevel 和 SVM)
修复阶段(以修复丢失的碎片)
领导者阶段
PoH 记录器
Turbine(区块传播)
Gossip:网络的入口点,节点在此发现其他节点,共享有关其设置和链的元数据,包括哪些端口已打开、投票信息等。
重放阶段:负责重放从领导者处收到的新区块,重建整个链的状态,并且需要多个其他组件,包括 BlockStore、Bank、运行时/执行客户端和修复阶段。
Blockstore:一个数据库,用于存储区块链元数据,包括碎片、交易、插槽信息等。
Bank:一个高度优化的数据库,用于表示特定插槽的区块链状态,包括所有帐户的状态。
运行时/执行客户端:Solana 虚拟机 (SVM),它是一个执行环境,通过使用链上程序处理交易来更新链上帐户。这包括 Sealevel,它并行处理批量的交易。
修复阶段:负责从其他节点获取丢失的碎片。
共识层:这包括 TowerBFT 协议,用于通过跟踪验证器对有效区块的投票来查找链的最新状态。该层还负责处理网络中的分叉。
RPC:负责使用远程过程调用向其他客户端提供链上数据,包括帐户余额、交易状态等。
GulfStream:由于 Solana 没有 mempool,因此 Gulfstream 负责将交易直接发送给即将到来的领导者。这需要知道验证器权益金额才能重建领导者计划。
领导者阶段:有了以上组件,节点就可以加入网络,对有效区块进行投票,并向客户端提供链上数据;但是,为了构造新的区块,它还需要一些其他组件,包括历史证明和 Turbine。
历史证明 (PoH):PoH 记录使用自哈希循环的时间流逝证明,并且在构造新区块时需要。
Turbine:一旦构造了一个新的区块,它就会使用 Solana 的 Turbine 协议在网络中传播,该协议类似于树状广播结构。
保持最新:
在我们的专用 Sig Twitter 上关注更新:https://twitter.com/sig_client
加入 Discord 上的讨论:https://discord.com/invite/XYdHaetwdd
关注我们的 Github 存储库:https://github.com/Syndica/sig
我们很高兴构建 Sig,并正在寻找高级工程师来帮助为 Solana 的未来做出贡献!
作为我们团队的一员,你将在从头开始设计和实现高性能 Solana 验证器客户端方面发挥关键作用。如果你是一位才华横溢的工程师,在协作和快节奏的环境中茁壮成长,并且你很高兴为 Solana 生态系统的发展做出贡献,我们很乐意听到你的来信。
有兴趣的候选人可以在 此处 申请。
- 原文链接: blog.syndica.io/introduc...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!