摘要:数据中心网络协议栈正在转向硬件,以在低延迟和低CPU利用率的情况下实现100 Gbps甚至更高的数据速率。但是,NIC中网络协议栈的硬连线方式扼杀了传输协议的创新。本文通过设计Tonic(一种用于传输逻辑的灵活硬件架构)来实现高速网卡中的可编程传输协议。在100Gbps的速率下,传输协议必须每隔几纳秒在NIC上仅使用每个流状态的几千比特生成一个数据段。通过识别跨不同传输协议的传输逻辑的通用模式,我们为传输逻辑设计了一个高效的硬件“模板”,该模板在使用简单的API编程的同时可以满足这些约束。基于FPGA的原型系统实验表明,Tonic能够支持多种协议的传输逻辑,并能满足100Gbps背靠背128字节数据包的时序要求。也就是说,每隔10 ns,我们的原型就会为下游DMA流水线的一千多个活动流中的一个生成一个数据段的地址,以便获取和传输数据包。
01 介绍
传输协议以及网络协议栈的其余部分传统上都在软件中运行。尽管软件网络协议栈一直在努力提高其性能和效率[1,6,25,32],但为了跟上当今数据中心的应用程序[25,32,38],软件网络协议栈往往会消耗30-40%的CPU周期。
随着数据中心转移到100 Gbps以太网上,软件网络协议栈的CPU利用率变得越来越高。因此,多个供应商开发了完全在网卡(NIC)上运行的硬件网络协议栈[8,10]。但是,在这些NIC上仅实现了两种主要的传输协议,它们都是硬连线方式并且只能由供应商修改。
RoCE. RoCE用于远程直接内存访问(RDMA)[8],使用DCQCN[43]进行拥塞控制,并使用简单的go-back-N策略进行可靠数据传输。
TCP. 一些供应商将他们选择的TCP变体卸载到NIC,以便直接通过套接字API(TCP卸载引擎[10])使用或启用RDMA(iWARP[7])。
然而,在过去几十年中提出的用于可靠传输[16,21,24,27,33,34]和拥塞控制[12,17,19,35,42,43]的无数可能算法中,这些协议仅使用了一小部分。例如,最近的研究表明,低延迟数据中心网络可以显著受益于接收端驱动的传输协议[21,24,36],这并不是当今硬件堆栈的选择。在微软数据中心部署RoCE网卡的尝试中,运营商需要修改数据传输算法以避免网络中出现活锁,但必须依赖NIC供应商进行更改[22]。已经提出了其他算法来改进RoCE的简单可靠传输算法[31,34]。多年来,TCP在各种网络中的优化列表证明了传输协议对可编程性的需求。
在本文中,我们研究如何实现硬件传输协议可编程化。即使NIC供应商开放了硬件编程接口,在高速硬件中实现传输协议也需要大量的专业知识、时间和精力。为了跟上100Gbps的速度,传输协议应该每隔几纳秒生成并传输一个数据包。它需要能够处理超过1000个活动流,这在今天的数据中心服务器中是很普遍的[15,37,38]。然而,NIC在其片上内存和计算资源的数量方面受到极大的限制[30,34]。
我们认为,高速网卡上的传输协议可以通过编程实现,而不需要让用户接触高速硬件编程的全部复杂性。我们的论点主要基于两个因素:
首先,可编程传输逻辑是实现灵活硬件传输协议的关键。传输协议的实现完成了多种功能,例如连接管理、数据缓冲区管理和数据传输。然而,它的核心任务是决定传输哪些数据段(数据传输)和何时传输(拥塞控制),统称为传输逻辑,这也是大多数创新的地方。因此,在高速NIC上实现可编程传输协议的关键是使用户能够修改传输逻辑。
其次,可以利用传输逻辑中的通用模式来创建可重用的高速硬件模块。尽管它们在应用级API(例如,TCP的套接字和字节流抽象与RDMA的基于消息的谓词API)以及连接和数据缓冲区管理方面存在差异,但传输协议有几种共同的模式。例如,不同的传输协议使用不同的算法来检测丢失的数据包。但是,一旦数据包被宣布丢失,可靠传输协议就会将其重传而优先于发送一个新的数据段。另一个例子,在拥塞控制中,给定由控制环路确定的参数(例如,拥塞窗口和速率),只有几种常见的方法来计算流在任何时候可以传输多少字节。这使我们能够为硬件中的传输逻辑设计一个高效的“模板”,可以使用简单的API对其编程。
基于这些观点,我们设计并开发了Tonic,这是一种可编程的硬件架构,可以使用简单的API来实现各种传输协议的传输逻辑,同时支持100Gbps的数据速率。每个时钟周期,Tonic都会生成下一个数据段的地址进行传输。数据段由下游DMA流水线从内存中提取,并由硬件网络协议栈的其余部分转换为一个完整的数据包(图1)。
我们认为Tonic将驻留在NIC上,取代传输协议硬件实现中的硬编码传输逻辑(例如,未来的RDMA NIC和TCP卸载引擎)。Tonic为传输逻辑提供了一个统一的可编程架构,与不同传输协议的具体实现如何执行连接和数据缓冲区管理以及它们的应用层API无关。然而,我们将以Socket API为例,描述Tonic如何与传输层的其余部分交互(§2),以及如何将其集成到Linux内核中以与应用程序进行交互(§5)。
我们在约8000行的Verilog代码中实现了Tonic原型,并在不到2 0 0行代码中实现各种传输协议[13,16,23,24,34,43]的传输逻辑,从而证明了Tonic的可编程性。我们还使用FPGA展示了Tonic满足∼100Mpps的时序,即支持100Gbps的背靠背128B数据包。也就是说,每隔10 ns,Tonic可以生成下游DMA流水线获取和发送一个数据包所需的传输元数据。从生成到传输,单个段地址通过Tonic的延迟约为0.1µs,Tonic最多可支持2048个并发流。
02 Tonic作为传输逻辑
本节概述了Tonic如何适应传输层(§2.1),以及如何克服在高速NIC上实现传输逻辑的挑战(§2.2)。
>2.1 Tonic如何适应传输层
位于应用程序和堆栈其余部分之间的传输层协议执行两个主要功能:
连接管理:连接管理包括创建和配置端点(例如,TCP的套接字和RDMA的队列对),并在开始时建立连接,在结束时关闭连接并释放其资源。
数据传输:数据传输涉及以段流的形式可靠而高效地将数据从一个端点传输到另一个端点1。不同的传输协议为应用程序请求数据传输提供了不同的API:TCP提供了字节流的抽象,应用程序可以连续向该字节流追加数据,而在RDMA中,对队列对的每个“send”调用都会创建单独的工作请求,并被视为单独的消息。此外,在不同的传输协议实现中,管理应用程序的数据缓冲区的具体情况也有所不同。无论如何,传输协议必须将未完成的数据以适合单个数据包的多个数据段形式传输到目的地。确定哪些字节构成下一个数据段以及何时传输它是通过数据传输和拥塞控制算法来完成的,我们统称为传输逻辑并在Tonic中实现。
图1显示了Tonic如何适合硬件网络协议栈的高级概述。为了将Tonic与连接管理和应用级API的细节分离,连接设置和拆卸需要在Tonic之外运行。Tonic依靠传输层的其余部分为每个已建立的连接提供唯一的标识符(流id),并使用这些ID显式地添加和删除连接。
对于发送端的数据传输,Tonic跟踪未完成的字节数和特定于传输的元数据以实现传输逻辑,即在拥塞控制算法指定的时间为每个流生成下一个数据段的地址。因此,Tonic不需要存储和/或处理实际的数据字节;它依靠传输层的其余部分来管理主机上的数据缓冲区,并在现有连接上有新的数据传输请求时通知它(有关详细信息,请参阅§5)。
传输逻辑的接收端主要涉及生成控制信号,如确认,每个数据包的授权令牌[21,24,36]或周期性拥塞通知数据包(CNP)[43],而传输层的其余部分则管理接收数据缓冲区并将接收的数据传输给应用程序。虽然处理接收到的数据可能会相当复杂,但在接收端上生成控制信号通常比发送端上更简单。因此,虽然我们主要关注发送端,但我们重用发送端的模块来实现接收端,仅用于生成每个数据包的累积和选择性ack,并以线速授予令牌。
>2.2 硬件设计挑战
由于两个主要限制,在NIC中以线速实现传输逻辑极具挑战性:
时序限制。数据中心的数据包大小中值小于200字节[15,37]。要使这些小数据包达到100Gbps,网卡必须每10 ns发送一个数据包。因此,每隔10 ns,传输逻辑应该确定接下来由哪个活动流来传输哪个数据段。为了做出该决定,它需要使用每个流的一些状态(例如,已确认的数据段、重复的ack、速率/窗口大小等)。当各种传输事件发生时(例如,接收确认或超时),更新这些状态。这些更新可能涉及不可忽略的硬件开销操作,例如搜索位图和数组。
为了在处理每个事件时有更多的时间,同时仍然每隔∼10 ns确定下一个据段,我们可以设想将传输事件的处理流水线化到跨多个阶段。当传入事件来自不同的流时,因为它们更新不同的状态使得流水线更容易处理。处理相同流的背靠背事件(例如,在接收确认的同时生成数据段)需要更新到相同的状态,这使得在确保状态一致性的同时流水线事件处理变得困难。因此,我们努力在10ns内处理每个传输事件,而不是快速合并下一个事件的状态,以防它影响相同的流。
内存限制:一个典型的数据中心服务器有超过1000个并发活动流,其中包含数千字节的动态数据[15,37,38]。由于NIC只有几兆字节的高速内存[30,34],因此传输协议在NIC上的每个流只能存储几千字节的状态。
Tonic的目标是满足这些严格的时序和内存限制,同时通过简单的API支持可编程性。为此,我们确定了各种协议中传输逻辑的通用模式,并将这些模式实现为可重用的固定功能模块。这些模式允许我们针对时序和内存优化这些模块,同时通过减少用户必须指定的功能来简化API编程。这些模式在表1中进行了总结,并将在下一节中详细讨论,在那里我们将描述Tonic的组件以及这些模式如何影响它们的设计。
03 Tonic架构
发送端的传输逻辑决定了每个流要传输哪些数据段(数据传递)和何时传输(拥塞控制)。从概念上讲,拥塞控制算法执行credit management,即确定给定流一次可以传输多少字节。数据传输算法执行segment selection,即确定特定流应该传输哪个连续的字节序列。尽管术语“数据传输”和“拥塞控制”通常与基于TCP的传输协议相关联,但Tonic为传输逻辑提供了一种通用的可编程架构,也可用于其他类型的传输协议,例如接收端驱动[21,24,36]和基于RDMA的[8]传输协议。
Tonic利用data delivery和credit management之间天然的功能分离,将它们划分为具有独立状态的两个组件(图2)。data delivery引擎处理与数据段的生成、跟踪和传输相关的事件,而credit引擎处理与调整每个流的信用并为具有足够信用的流发送段地址相关的事件。
以两个引擎之间的轻量级协调为代价,这种划分方式帮助Tonic在每个周期同时处理多个事件(例如,接收确认和段传输)的同时满足其时序限制。这些事件必须读取其相应流的当前状态,对其进行更新,并将其写回内存以便在下一周期中处理事件。然而,在每个周期中对内存的并发读写成本很高。与使用宽内存来服务所有传输事件不同,分区允许data delivery和credit engines使用更窄的内存来服务于与其特定功能相关的事件,从而满足时间限制。
在本节中,我们将在§3.1中介绍引擎如何协调,在保持输出链路利用率的同时,公平有效地从每个周期的几千个流中挑选一个流进行分段传输。接下来,§3.2和§3.3描述了每个引擎中的固定功能和可编程事件处理模块,以及它们的设计是如何从表1中的模式中获得灵感的。我们在§3.4中介绍了Tonic在一个循环中接收到同一流的多个事件时解决冲突的解决方案,并在§3.5中介绍了它的编程接口。
>3.1 高效的流调度
在任何时候,如果它(1)有足够的信用并且(2)有新的或丢失的数据段要发送时,一个流只能传输一个数据段。为了节省工作量,Tonic必须跟踪符合传输条件的流集合(满足上述两个标准),并且在每个周期选择要传输的流时仅在这些流中进行选择。要有效地做到这一点很有挑战性。我们有超过1000多个流,它们的状态分布在两个引擎上:只有信用引擎知道每个流有多少信用,只有数据传输引擎知道流的段的状态,并且可以生成它的下一个段的地址。我们不能通过在每个循环中检查两个引擎的所有流的状态来找到在该循环中符合传输条件的流。
相反,我们将段地址的生成与它们最终传输到DMA流水线的过程解耦。我们允许数据传递引擎为一个流生成最多N个段地址,而不必有足够的信用将它们发送出去。在信用引擎中,我们为每个流保留一个大小为N的环形缓冲区来存储这些未完成的段地址。当流有足够的信用来发送一个段时,信用引擎从缓冲区退出队列并输出一个段地址,并向数据传输引擎发送信号以减少该流中未完成段的数量。
这解决了两个引擎之间的分区状态问题。数据传输引擎不需要跟踪流的信用变化来生成段地址。仅当段地址从缓冲区出列时才需要通知它。此外,信用引擎不需要知道所有流段的确切状态。如果流的环形缓冲区为空,则该流没有要发送的段。否则,当流有足够的信用时,已经有可以输出的段地址。
不过,数据传输引擎不能简单地在每个周期检查所有流的状态,以确定哪些流可以生成段。相反,我们在数据传输引擎中动态维护活动流集,即该流至少要有一个要生成的段且未完成段少于N个 (参见图2中的红色编号圆圈)。当创建一个流时,会将其添加到活动集中。每个周期,从集合中选择并移除一个流以用于段生成(步骤1)。一旦被处理(步骤2),只有当它有更多的数据段要发送并且少于N个未完成的数据段时,它才会被插回集合中(步骤3)。否则,如果稍后接收到来自信用引擎的ack或信号激活了流程,则将其插入到集合中(步骤9)。此外,生成的段地址被转发到信用引擎(步骤4),用于插入环形缓冲区(步骤5)。
类似地,信用引擎维护准备传输流的集合,即在其环形缓冲区中具有一个或多个段地址的流,并且有足够的信用将至少一个段发送出去。每个周期,从集合中选择一个流(步骤6),发送来自其环形缓冲区中的一个段地址(步骤7),减少其信用,并且如果它有更多的段地址和信用用于进一步传输,则将其插回集合中(步骤8)。它还向数据传输引擎发送关于传输的信号(步骤9),以减少该流中未完成段的数量。
为了公平起见,当从活动(或准备传输)集合中挑选流时,Tonic使用FIFO在集合中的流之间实现循环调度(参见[39]中的活动列表)。循环调度的选择不是最基本的;任何其他满足我们的时序约束的调度器都可以取代FIFO来支持其他调度规则[40]。
>3.2 灵活的段选择
对于B字节的信用,一个流可以发送S = max(B,MSS)字节,其中MSS是最大段的大小。在传输协议中,数据传输算法使用确认来跟踪数据每个字节的状态(例如,已传输、丢失、正在传输和未传输),并使用该状态来决定下一步传输哪个连续的S字节数据。
然而,在高速NIC中实现数据传输算法有两个主要挑战。首先,由于内存限制,NIC无法存储每个字节的信息。其次,除了少数例外[8,34],这些算法是为软件设计的,在软件中,它们可以存储并自由循环使用大量元数据来聚合信息。这种计算灵活性在这些算法中创造了显著的多样性。不过由于NIC硬件比软件更受约束,因此我们并不打算支持所有的数据传输算法,而是在寻找能够在各种算法中通用,同时也适合硬件实现的模式。
· 预先计算的固定段边界
数据传输算法可以从数据流中的任何位置选择要发送的下一个字节,并生成具有可变边界的段。但是,由于NIC无法保持每个字节状态,因此当流请求传输新数据时,Tonic要求将数据划分为固定大小的段(通过内核模块或驱动程序,参见§5)。这样,数据传输算法可以使用分段信息来选择下一段。
注意,可以根据每个流的吞吐量和延迟需求为每个流配置固定的段大小。对于基于消息的传输协议(例如RoCEv2),具有固定的段边界自然适合;消息长度是已知的,可以从一开始就选择最佳的段大小。对于具有字节流抽象的协议(例如TCP和NDP),固定段大小应在数据添加到流中时即时确定。对于高带宽数据流,可以将其设置为MSS(如果使用TSO[18],则设置为更大)。对于偶尔生成小数据段的流,可以将段大小设置为较小的值,这取决于是在通知Tonic之前将多个小段合并为一个较大的段,还是立即传输小段(§5)。无论如何,为了避免在NIC上存储每个字节的状态,段大小应该在Tonic之外决定,并且不经常更改。
·有限窗口中每小段状态
不同于流的可用信用,如果一个新段距离第一个未确认的段超过K个段,数据传输算法通常不会传输该新段,以限制发送端和接收端需要保持的状态2。但是,在具有10µs RTT的100 Gbps网络中,K可以达到约128个段。幸运的是,我们观察到对于大多数的数据传输算法来说存储以下每段状态就足够了:(1)段是否已确认(存在选择性确认)?(2) 如果没有,它是丢失了还是还在传输中?(3) 如果丢失,是否已重新传输(避免冗余重传)?
更具体地说,我们观察到在没有明确的否定确认的情况下,数据传输算法从肯定确认中为每个段积累丢失的证据,例如重复累积(例如,TCP NewReno[23])或选择性ack(例如,RDMA和TCP SACK的IRN [16])。一旦某个段的累积证据超过阈值,该算法就可以自信地宣布该片段丢失。通常,段i的丢失证据也是每个未确认的段j(j < i)的丢失证据。因此,这些算法中的大多数可以重写为只跟踪第一个未确认段的全部丢失证据,并根据需要增量计算其余部分的证据。基于这些观察结果(表1中的#1和#2),我们在Tonic的数据传输引擎中使用一组固定位图来跟踪流的段状态,并实现优化的固定功能位图操作,以便在传输事件中更新它们。
· 并发事件处理
对于每个流,有四个主要事件会影响其下一个段地址的生成。首先,接收到确认可以向前移动窗口并使流能够生成更多的段,或者发送信号段丢失并触发重传。第二,没有确认(即超时)也可能导致更多的段被标记为丢失并触发重传。第三,段地址的生成会增加流的未完成段的数量,如果超过N,则可以停用该流。第四,段地址传输(除credit引擎外)减少了未完成段的数量,可以使流生成更多的段地址。
Tonic的数据传输引擎有四个模块来处理这四个事件(图2)。每个周期,每个模块从数据传输引擎中的内存中读取其接收到处理事件的流状态,并相应地更新流状态。数据传输引擎中的流状态包括一组固定的变量,用于跟踪跨事件段的当前窗口的状态,以及可编程组件中使用的用户定义变量(表2)。作为固定状态变量的一个示例,Tonic为每个流保留了一组固定的位图(见§3.2.2):ack位图跟踪选择性确认的段,marked-for-rtx跟踪需要重传的丢失段,rtx-cnt存储有关其先前重新传输的信息。
以下段落简要描述了每个事件处理模块如何影响流的状态,以及是否存在我们可以利用的通用模式,以固定功能的方式实现其全部或部分功能。
输入。该模块处理确认过程(和其他输入数据包,见§3.3.3)。响应确认的状态变量的一些更新在所有数据传输算法中都是类似的,并且不需要编程(例如,更新窗口边界,并在ack位图中标记选择性确认的段,见§3.2.2),而依赖于确认作为信号的丢失检测和恢复,不同算法之间的差异很大,必须由用户进行编程(表1中的#4)。因此,输入模块被设计为两级流水线:一个用于公共更新的固定功能阶段,另一个用于丢失检测和恢复的可编程阶段。
这种两阶段设计的好处是常见的更新主要涉及位图和数组(§3.2.2),而它们在硬件中被实现为环形缓冲区,并且跨元素修改的成本很高。例如,在所有数据传输算法中,如果一个输入数据包累计确认了段A,并有选择性地确认段S,则wnd-start将会被更新为max(wnd-start,A),acked[s]为1,并且所有位图和数组的边界将根据新的wnd-start进行更新。通过将这些更新转移到一个固定的功能阶段,我们可以(i)优化它们以满足Tonic的时序和内存限制,(ii)为程序员提供一个专用阶段,即一个独立的周期,用于进行丢失检测和恢复。在这个专门阶段,程序员可以使用前一阶段更新的状态变量和内存中的其余变量来推断段丢失(并执行§3.3.3中讨论的其他用户定义的计算)。
定期更新。数据传输引擎对活动流进行迭代,每次发送一个到该模块,以检查重传计时器是否过期,并执行其他用户定义的定期更新(§3.3.3)。因此,凭借其10 ns的时钟周期,Tonic可以在其重传计时器到期后的几us内覆盖每个流。该模块必须是可编程的,因为重传超时是用于检测丢失的信号(表1中的#4)。与输入模块的可编程阶段类似,程序员可以使用每个流的状态变量来推断段损失。
段生成。给定一个活动流及其变量,该模块生成下一个段的地址并将其转发给信用引擎。Tonic根据以下观察结果(表1中的#3)可以将段地址生成作为一个固定的功能模块来实现:尽管不同的可靠数据传输算法有不同的方法来推断段丢失,但一旦检测到丢失的段,在发送任何新的数据之前重新传输它是合乎逻辑的。因此,无论数据传输算法如何,选择下一段的过程都是相同的,并且在Tonic中作为一个固定功能模块来实现。因此,该模块优先重发marked-for-rtx中丢失的段,而不是发送下一个新的段,即highest\_sent+1,同时也增加了未完成段的数量。
数据段传输。该模块为固定功能,在从信用引擎传输段地址时触发。它减少了相应流中未完成段的数量。如果流由于环形缓冲区满了而被停用,则会再次将其插入活动集中。
>3.3 灵活的信用管理
传输协议使用拥塞控制算法,通过控制流的传输速度来避免网络过载。这些算法包括一个控制回路,该回路通过监测输入控制数据包流(例如,确认和拥塞通知数据包(CNP))来估计网络容量,并设置限制输出数据包的参数。虽然控制回路在许多算法中有所不同,但基于参数的信用计算却不尽相同。Tonic具有用于信用计算的高效固定功能模块(§3.3.1和§3.3.2),并将参数调整归入可编程模块(§3.3.3)。
· 常用的信用计算模式
拥塞控制算法有多种方法来估计网络容量。然而,它们通过三种主要方式(表1中的#5)对数据传输进行限制:
拥塞窗口。控制回路限制了一个流从第一个未确认的字节开始,最多只能飞行W个字节。因此,如果字节i是第一个未确认的字节,则流不能发送超过i +W的字节。跟踪飞行中的段以执行拥塞窗口可能会变得复杂(例如,存在选择性确认的情况下)并在数据传输引擎中传入模块的固定功能阶段实现。
速率。控制回路限制流的平均速率(R)和最大突发大小(D)。因此,如果流在最后一次传输的t0时间具有信用c0,则在时间t时的信用将为min(R∗(t−t0)+c0,D)。正如我们在§4中所示,在严格的时序和内存约束下实现精确的单流速率限制是一项具有挑战性的任务,而在Tonic中有一个优化的固定功能实现。
授权令牌。控制回路从接收端接收令牌并将其添加到流的信用中,而不是估计网络容量。因此,一个流的信用是接收的令牌总数减去传输的字节数,信用计算逻辑由简单的加法组成。
由于大多数拥塞控制算法都使用这些算法3,因此我们优化了它们的实现,以满足Tonic的时序和内存约束。拥塞窗口的计算主要受ack的影响。因此,拥塞窗口的计算和实施发生在数据传输引擎中。对于其他两个信用计算方案,信用引擎处理与信用相关事件,用户可以简单地选择在信用引擎中使用哪个方案。
· 信用计算的事件处理
从概念上讲,有三个主要事件可以触发流的信用计算,信用引擎有不同的模块可以在每个周期内并发处理它们(图2)。首先,当从数据传输引擎接收到一个段地址并且该段地址是流的环形缓冲区中的唯一地址时,流现在可以根据其信用(排队模块)进行传输或保持空闲。第二,当一个流传输一个段地址时,它的信用度必须降低,我们应该根据它更新的信用度和它的环形缓冲区(传输模块)的占用情况来确定它是否有资格再次传输。第三,可以向流中添加信用的事件(例如,来自授权令牌和泄露速率限制),这是基于速率和基于令牌的信用计算之间的主要区别。
当使用授权令牌时,信用引擎需要两个专用模块来增加流的信用:一个用于处理来自接收端的输入授权令牌,另一个用于增加超时重传的信用。当基于速率时,信用引擎不需要任何额外的模块来增加信用,因为速率为R字节/周期的流在每个周期内隐式地获得R字节的信用,因此,我们可以提前计算它何时符合传输条件。
假设在周期T0中,传输模块从流f中发送了一个段,并且正在判断该流是否符合进一步发送的条件。假设f在环形缓冲区中有更多的段,但缺少L字节的信用。当T=L/R,传输模块可以计算何时有足够的信用,并为T周期设置一个计时器。当计时器到期时,f至少有一个段有足够的信用,因此它可以直接插入ready-to-tx。当f到达ready-to-tx的头部并在T1周期中再次由传输模块处理时,传输模块可以将f的信用增加(T1−T0)∗ R−S,其中S是在时间T1时传输的段的大小4。请注意,在使用速率时,信用引擎必须执行分割并维护单流计时器。我们将在§4中讨论这些操作的硬件实现。
· 灵活的参数调整
拥塞控制算法通常有一个控制回路,该回路持续监测网络并根据估计的网络容量调整信用计算参数,即速率或窗口大小。参数调整要么由输入的数据包(例如,确认和它们的信号,如TCP变体中的ECN或延迟以及Timely,以及DCQCN中的拥塞通知数据包(CNP))触发,要么由周期性计时器和计数器触发(TCP变体和字节计数器中的超时,以及DCQCN中的各种计时器),在某些情况下,也受到段丢失的触发(TCP中重复确认后的窗口调整)。
针对这些触发器, Tonic的用户可以使用“Incoming”模块的可编程平台(该模块可以查看所有输入数据包)以及定时器和计数器的“Periodic Updates”模块来指定参数调整逻辑。这两个模块都位于数据传输引擎中,可以访问段状态信息,以防参数调整需要段状态(如掉线)。更新后的参数将转发到信用引擎。
如§6.1.1所示。我们在Tonic中实现了几种拥塞控制算法,它们的参数调整计算在我们的10 ns时钟周期内完成。那些使用整数运算的算法不需要做任何修改。对于那些具有浮点运算的操作,例如DCQCN,我们使用整数运算将其近似为某个小数点。如果一个算法需要高精度和复杂的浮点运算来调整参数,而又不能在一个时钟周期内实现[19],则可以将计算交给Tonic之外的浮点运算模块。该模块可以执行异步计算,并将输出存储在单独的内存中,该内存通过“周期性更新”模块合并到Tonic中。
>3.4 处理冲突事件
Tonic力求同时处理事件,以便对事件作出反应。因此,如果一个流在同一个周期中接收到多个事件,它允许事件处理模块处理事件并更新流的状态变量,并在将其写回内存之前协调状态(图2中的合并模块)。
根据定义,由于确认和重传超时是互斥的。因此,如果在同一周期内接收到对同一流的确认和超时,则Tonic将丢弃超时。这大大简化了合并逻辑,因为几个变量(窗口大小和重传计时器周期)仅由这两个事件修改,因此永远不会同时更新。我们可以使用简单的、预定义的合并逻辑来解决剩余变量的并发更新。例如,段生成增加未完成段的数量,而段传输减少未完成段的数量;如果两个事件同时影响同一个流,则数字不会更改。用户定义的变量在Incoming或Periodic Updates模块中更新,如果两个更新发生在同一个周期中,我们依赖程序员来指定哪些更新的变量应该优先。
>3.5 Tonic的编程接口
为了在Tonic中实现新的传输逻辑,程序员只需指定(i)使用三种信用管理方案中的哪一种,(ii)响应确认和超时的丢失检测和恢复逻辑,以及(iii)响应输入数据包或周期计时器和计数器的拥塞控制参数调整。第一个用于为信用引擎选择合适的模块,最后两个插入到数据传输引擎的相应可编程阶段(图2)。
为了指定Incoming模块的可编程阶段的逻辑,程序员需要编写一个函数,该函数接收输入数据包(ack或其他控制信号)、新确认的段数、用ack中的信息更新的ack位图、wnd- start的新旧值(以防窗口因新的累积ack而向前移动),以及流的其余状态变量(表2)作为输入。在输出中,它们可以在marked-for-rtx中标记要重传的段范围,更新拥塞控制参数,如窗口大小和速率,并重置重传计时器。周期性更新模块的编程接口等等。
在指定这些函数时,程序员可以使用整数算术运算(例如,带有小宽度操作数是加减乘除)和有限的只读位图操作集(例如,索引查找),并在更新的ack位图中查找第一个设置位(示例程序见附录F)。请注意,数据传输引擎中的一个专用固定功能阶段在收到ack时执行代价高昂的通用位图更新(§3.2.3)。我们在§6.1.1中说明了可以使用此接口实现多种传输协议,并举出了一些不能实现的示例。
04 硬件实现
在本节中,我们描述了在Tonic严格的时序和内存约束下最难实现的Tonic组件的硬件设计。
高精度的单流速率限制。在T=L/R周期中,一个具有每周期速率为R字节和要发送L字节的流将具有足够的信用用于传输。Tonic需要在信用引擎中执行此计算,但必须将R表示为一个整数,因为它无法进行浮点除法。这在速率限制精度和Tonic可以支持的速率范围之间进行了权衡。如果R以字节/周期为单位,则我们不能支持低于1字节/周期或∼1 Gbps的速率。如果我们用每千个周期的字节数表示R,我们可以支持低至1 Mbps的速率。然而,T=L/ R决定了从现在开始该流有多少个周期有资格传输,这导致较高带宽流的速率一致性和精度较低。为了在不牺牲精度的情况下支持大范围的速率,Tonic在不同的精度级别保留了流的多种表示形式,并在任何时刻挑选最精确的表示来计算T(详情见附录B)。
高效的位图操作。Tonic使用高达128位的位图来跟踪每个流的段状态。位图以环形缓冲区的形式实现。头指针对应于第一个未确认的段,并使用新的ack沿着缓冲区向前移动。为了有效地实现其输出取决于位图中所有位的值的操作,我们必须将缓冲区分成多层的小部分,并行处理它们,并连接结果。在Tonic中经常使用的一种这样的操作是查找报头之后的第一个设置位。环形缓冲区中报头的移动使该操作的实现变得复杂,因为跟踪每层中的报头需要额外的处理,使得它很难在我们的10 ns目标内进行计算。相反,Tonic在输入环形缓冲区上使用轻量级预处理,以完全避免各层中的报头索引计算(详细信息见附录C)。
并行内存访问。在每个周期中,数据传输引擎中的五个模块(包括Incoming模块的两段)同时访问其内存(§3.2.3)。然而,FPGA只有双端口RAM存储器,每个端口在每个周期都能进行读或写。构建具有更多并发读、写的存储器需要在单独的存储器“分块”中保存数据的多个副本,并跟踪分块中每个地址的最新数据5[26]。为了避免支持五个并发读写,我们设法将每个流的状态变量划分为两个组,每个组最多处理四个事件。因此,Tonic可以使用两个具有四个读写端口的存储器,而不是单个具有五个端口的存储器,同时为所有处理模块提供并发访问。
05 将Tonic集成到传输层
Tonic的传输逻辑有意与其他传输功能(如连接管理、应用级API和缓冲区管理)的具体实现相分离。本节提供一个示例,说明Tonic如何与Linux内核交互,以了解新连接、数据传输请求和连接终止6。创建套接字后,应用程序使用各种系统调用进行连接管理和数据传输。由于Tonic主要关注传输逻辑的发送端,因此我们只讨论与传输层的发送端相关的系统调用和修改。
连接管理。客户端上的connect()发起连接,服务器上的listen()和accept()监测并接受连接,close()终止连接。由于连接管理发生在Tonic之外,因此这些系统调用的内核实现保持不变。但是,一旦连接建立,内核会将其映射到[0,N)中的唯一flow id,其中N是Tonic支持的最大流数,并通过NIC驱动程序通知Tonic关于新连接。
具体地说,通过内核中连接的Transmission Control Block (TCB),通信终端的IP地址和端口与为连接选择的flow id和固定段大小一起发送到Tonic。内核只需要跟踪用于连接管理的TCB字段(例如,IP地址、端口和TCP FSM)、数据缓冲区的指针以及与接收器相关的字段。发送端上用于数据传输的字段(即snd.nxt、snd.una和snd.wnd)存储在Tonic中并由Tonic处理。最后,在调用close()之后,内核使用该连接的flow id通知Tonic终止。
数据传输。send()将数据添加到连接的套接字缓冲区,该缓冲区存储等待传输的未完成数据。Tonic为未完成的数据保留几比特的每段状态,并以段执行所有传输逻辑计算。因此,在Tonic开始传输之前,数据应该被划分为同等大小的段(§3.2)。因此,对send()的修改主要涉及根据连接配置的段大小来确定套接字缓冲区中数据的段边界,并决定何时将新段通知Tonic。具体来说,内核除了头部和尾部外,还为每个连接的套接字缓冲区保留一个额外的指针,称为tonic-tail。它指向已通知Tonic的最后一段。head和tonic-tail的更新被发送给Tonic,以便在生成下一段地址时从内存中获取。
从一个空的套接字缓冲区开始,当应用程序调用send()时,数据被复制到套接字缓冲区,tail也相应地更新。假设连接配置的段大小为C,那么数据就会被分割成C大小的段。假设数据被分割成S个段和B(B<C)个剩余字节。然后,内核将tonic-tail更新为指向最后一个C大小的段的末尾(即head+C*S),并通知Tonic更新tonic-tail。对于可配置的时间T,额外的B字节对Tonic来说仍是未知的,以防应用程序调用send以提供更多数据。在这种情况下,将数据添加到套接字缓冲区,tonic-tail和tail之间的数据被同样地分割,相应地更新tonic-tail,并且通知Tonic有新的数据段。
如果在时间T之后没有足够的数据用于C大小的段,内核需要通知Tonic“子段”(小于C的段)及其大小,并相应地更新Tonic-Tail。注意,Tonic要求除突发事件中的最后一个数据段外的所有数据段的大小相同,因为所有计算(包括窗口更新)都是以数据段为单位的。因此,在创建一个“子段”之后,如果应用程序有更多数据,Tonic只有在完成当前段的传输后才能开始传输。Tonic在成功传输最后一个“子段”后通知内核,此时head和tonic-Tail将会相等,内核继续对套接字缓冲区中的剩余数据进行分区,并像以前一样更新Tonic。注意,Tonic可以使用可配置的频率周期地向内核转发确认信息,使head向前移动,为套接字缓冲区的新数据腾出空间。
可以根据每个流的延迟和吞吐量特性为其配置C和T。对于高带宽流,可以将C设置为MSS(如果使用TSO,则设置为更大)。对于零星产生小段的流,设置C和T不是那么直接,因为段不能在Tonic内合并。我们在附录D中详细讨论了决定这些参数的权衡问题。
其他考虑事项。如§6所示,Tonic当前的设计支持2048个并发流,与数据中心[15,37]中观察到的工作集以及文献[20]中的其他硬件负载相匹配。如果一个主机的开放连接超过Tonic所能支持的数量,内核可以按照先到先得的原则将数据流的数据传输分流到Tonic,或者让用户在创建套接字时设置标志,并在Tonic耗尽新流的资源后回退到软件。另外,现代基于FPGA的网卡有一个大的DRAM直接连接到FPGA[20]。DRAM可以用来存储更多连接的状态,并在它们激活和需要传输数据时将它们来回交换到Tonic的内存中。此外,为了提供对硬件传输逻辑性能的可见性,Tonic可以为内核提供一个接口,以便定期从NIC提取传输统计数据。
其他传输层。上面的设计是如何将Tonic集成到常用传输层的一个示例。但是,TCP、套接字和字节流并不适用于所有应用程序。事实上,一些具有高带宽、低延迟流的数据中心应用程序开始使用RDMA及其基于消息的API来代替[5,9,22,35]。Tonic也可以被集成到基于RDMA的传输中,我们将在附录A中讨论这一点。
06 评估
为了评估Tonic,我们用Verilog实现了一个原型(约8K行代码),并用C++实现了一个周期精确的硬件仿真器 (约2K行代码) [11]。该仿真器与NS3网络仿真器[4]集成在一起进行端到端的实验。
要在Tonic的Verilog原型上实现传输协议,程序员只需要提供三个Verilog文件:(i) incoming.v,描述丢失检测和恢复逻辑以及如何改变信用管理参数(即,速率或窗口) 以响应于输入数据包;该代码被插入到数据传输引擎中的输入流水线的第二阶段;(ii) periodic\_updates.v,描述丢失检测和恢复逻辑以响应超时,以及如何改变信用管理参数(即,速率或窗口)以响应周期性定时器和计数器,该代码被插入到数据传输引擎中的周期性更新模块中 (iii)user\_configs.vh,其指定要使用三个信用计算方案中的哪一个以及用户定义的状态变量和其他参数的初始值,例如初始窗口大小、速率和信用。
我们从以下两个方面对Tonic进行评估:
硬件设计(§6.1)。我们使用Tonic的Verilog原型来评估其硬件架构的可编程性和可拓展性。Tonic能否支持各种传输协议吗?它是否减少了在NIC中实施传输协议的开发工作?Tonic能否支持具有多个变量的复杂用户定义逻辑吗?它能够支持多少个单流段和并发流?
端到端行为(§6.2)。我们使用Tonic的周期性精确仿真器和NS3来比较Tonic的端到端行为和两个协议的硬编码实现:New Reno[23]和使用DCQCN的 RoCEv2 [43],对于单个流和多个流共享一个瓶颈链路。
>6.1 硬件设计
评估硬件设计效率有两个主要指标:(i)资源利用率。FPGA由原语块组成,这些原语块可以通过配置和连接以实现Verilog程序:look-up tables (LUT)是主要的可重新配置逻辑块,而block RAM(BRAM)用于实现存储器。(ii)定时。在每个周期开始时,每个模块的输入被写入一组输入寄存器。在下一个周期开始之前,该模块必须处理输入并准备输出寄存器的结果。Tonic 的meet timing必须是100 MHz,才能每10 ns传输一个段地址。也就是说,要实现100 Gbps,每个模块中从输入到输出寄存器的每条路径的处理延迟必须保持在10 ns以内。
我们使用这两个指标来评估Tonic的可编程性和可扩展性。这些指标高度依赖于用于合成的特定目标。我们使用Kintex UltraScale+XCKU15P FPGA作为我们的目标,是因为该FPGA和其他具有类似功能的FPGA在今天的商业可编程NIC[2,3]中都是bump-in-the-wire结构。这是一个保守的选择,因为这些NIC是为10-40 Gbps以太网设计的。一块100 Gbps的网卡可能会有一个功能更强大的FPGA。此外,我们将Tonic的所有组件综合到FPGA上,作为一个独立的原型进行评估。然而,考虑到固定功能模块和可编程模块之间有明确的接口,可以设想将固定功能组件作为ASIC实现以提高效率。除非另有说明,否则在所有实验中,我们都将并发流的最大数量设置为1024,将最大窗口大小设置为128段7。
· 硬件可编程性
我们已经在Tonic中实现了六种协议的发送端传输逻辑,作为文献中各种类型的段选择和信用计算算法的代表。表3总结了固定功能模块和用户定义模块的资源利用率,以及用于实现它们的代码行和用户定义状态字节数。虽然我们对所有协议使用相同的单流状态变量集(表2),但并非所有协议在处理传输事件时都使用所有变量。例如,位图只被有选择性ack的协议使用。因此,通过一些预处理,从Verilog设计中去掉不相关的变量和计算,甚至可以进一步降低资源利用率。
Reno[13]和New Reno[23]代表TCP变体,仅使用累积ack来实现可靠传输以及用于信用管理的拥塞窗口。Reno只能使用快速重传来恢复窗口内的一次丢失,而New Reno使用部分确认来更有效地恢复同一窗口中的多个丢失。SACK受到RFC 6675[16]的启发,表示使用选择性ack的TCP变体。我们的实现是每个ack至少有一个SACK块,但可以扩展到更多。NDP[24]代表接收器驱动的协议,最近提出用于低延迟数据中心网络[21,36]。它使用明确的NACK和超时进行丢失检测,并授予令牌进行拥塞控制。RoCEv2 w/DCQCN[43]是一种广泛使用的以太网RDMA传输协议,IRN[34]是一种最新的基于硬件的协议,用于改进ROCE的简单数据传输算法。这两者都使用速率限制器进行信用管理。
注意,如§3.2所述,并非所有数据传输算法都适用于硬件实现。例如,由于NIC上的内存限制,不可能在网卡上为每个数据包(新数据包和重传输数据包)保留时间戳。因此,严重依赖每包时间戳的传输协议(例如,QUIC[27])需要被修改以使用更少的时间戳工作,例如,对于要卸载到硬件的正在传输的段的子集。
从这些结果中可以得出三个重要结论:
① Tonic支持多种传输协议。
② 使用Tonic,上述每种协议的实现只需不到200行Verilog代码,用户自定义逻辑占用不到FPGA 0.6%的LUT。相比之下,Tonic的固定功能模块可以在这些协议中重复使用,使用约8K行代码实现,消耗了60倍的LUT。
③ 不同的信用管理方案具有不同的开销。对于使用拥塞窗口的传输协议,窗口计算与数据传输引擎重叠,因此在数据传输引擎中实现(§3.3.1)。因此,他们的信用引擎使用的资源比其他公司少。与强制执行接收器生成的授权令牌相比,速率限制需要更多的单流状态和更复杂的操作(§4),但需要更少的内存端口用于并发读取和写入(§3.3.2),总体上导致更低的BRAM和更高的LUT利用率。
· 硬件可扩展性
我们通过研究Tonic架构中的可变性来源(可编程模块和各种参数)如何影响内存利用率和计时来评估Tonic的可扩展性。
可编程模块中的用户定义逻辑可以具有任意长的相关操作链,这可能会导致时序冲突。我们用不同数量的算术、逻辑和位图运算为incoming.v(数据传输引擎中Incoming模块的可编程阶段)生成了70个随机程序,并分析了在不违反10 ns时序的情况下相关操作链的长度。这些程序使用高达125B的状态,最大依赖于65个逻辑电平(分别比表3中的基准协议多6倍和2倍)。每个逻辑电平代表几个原始逻辑块(LUT、MUX、DSP等)中的一个,它们链接在一起以实现Verilog程序中的路径。
我们将这些程序插入Tonic,对其进行综合,并分析逻辑级数与最大延迟路径延迟之间的关系(表4)。逻辑级数不超过32的程序始终满足时序要求,而逻辑级数超过43的程序则不符合时序要求。逻辑级数在32到42之间的,最大延迟路径的等待时间约为10 ns。根据最大延迟路径上原语的组合及其延迟,该区域中的程序可能会满足时序要求。我们的基准协议在其最大延迟路径上有13到29个逻辑级数,并且都满足时序。因此,Tonic不仅支持我们的基准协议,而且还有空间支持未来更复杂的协议。
用户定义的状态变量增加了影响BRAM利用率的内存宽度。我们在SACK、IRN和NDP增加了额外的变量,以查看在不违反时序和耗尽BRAM的情况下内存的宽度,并针对三种信用管理方案中的每一种重复该实验,因为它们具有不同的存储器足迹(表4)。Tonic支持用户自定义状态的448B拥塞窗口信用管理,340B速率,256B授予令牌(表3中的协议使用小于30B)。
最大窗口大小确定存储在数据传输引擎中的每个流位图的大小,以跟踪流的段状态,从而影响内存利用率和位图操作的复杂性,从而影响时序。Tonic可以支持256位的位图(即跟踪256段),使用它我们可以在高达30µs RTT的网络中支持单个100Gbps流(表4)。
并发流的最大数量决定内存深度和用于流调度的FIFO大小(§3.1)。因此,它会影响内存利用率和队列操作,从而影响时序。Tonic可以在硬件上扩展到2048个并发流(表4),这与数据中心[15,37]和文献[20]中的其他硬件卸载观察到的活动流集的大小相匹配。
Tonic有更多的空间来支持未来的协议,这些协议比我们的基准协议更复杂,具有更多用户定义的变量。它可以跟踪每个流的256个段,支持2048个并发流。有了更强大的FPGA和更多的BRAM,Tonic可以支持更大的窗口和更多的流。
>6.2 端到端行为
为了检查Tonic的端到端行为,并验证不同协议中基于Tonic的传输逻辑实现的保真度,我们在C++中为Tonic开发了一个周期精确的硬件仿真器。我们将此仿真器与NS3一起使用,以显示基于Tonic实现的NewReno和RoCEv2 w/DCQCN发送端与其硬编码的NS3实现相匹配。请注意,这些仿真的目的是分析和验证Tonic的端到端行为。Tonic支持100Gbps线路速率的能力已在§6.1中得到证明。因此,在我们的仿真中,我们使用10Gbps和40Gbps作为线速率,仅仅是为了使硬件仿真在几秒钟内的多个流在计算上易于处理。
· TCP New Reno
我们在Tonic中基于RFC6582实现了TCP New Reno,并使用NS3的本地网络堆栈进行硬编码实现。我们基于Tonic的实现与NS3中未修改的本地TCP接收器配合使用。在所有仿真中,主机通过10Gbps链路连接到一个交换机,RTT为10µs,缓冲区为5.5MB,最小重传超时为200ms(Linux默认值),段大为1000B,并且在接收器上启用延迟ack。
单流。我们启动从一个主机到另一个主机的单一流,并在接收方的NIC上随机丢弃数据包。图3(a)和图3(b)分别显示了拥塞窗口和传输序列号的更新(重传用大圆点标记)。Tonic在这两种情况下的行为都与硬编码的实现密切相关。这些细微的差异源于这样一个事实:在NS3的网络堆栈中,所有计算都在同一虚拟时间步长中进行,而在Tonic中,每个事件(输入数据包、段地址生成等)都在100ns周期内处理(从10ns增加到10G线速率)。
多个流。两个发送端各发送100个流到一个接收端,因此200个流在5秒钟内共享一个瓶颈链路。基于Tonic实现的200个流的平均吞吐量的CDF与硬编码的实现密切相关(图3(c))。我们观察到重传次数的分布也是类似的。在分析毫秒级的流吞吐量时,我们注意到硬编码实现的变化比Tonic大,因为相对于NS3的堆栈,Tonic在同一主机上的流执行每包循环调度。
· RoCEv2 with DCQCN
我们用Tonic实现ROCE w/DCQCN[43],并使用作者在[44]中的NS3进行硬编码实现。我们基于Tonic的实现与未经修改的硬编码ROCE接收器一起工作。在所有仿真中,主机通过40Gbps链路连接到同一交换机,RTT为4µs,网段大小为1000B,我们使用[44]中的默认DCQCN参数。
单流。Tonic是一种基于速率的算法,它使用CNP和周期性定时器和计数器进行拥塞控制,而不是TCP中的丢包。因此,为了观察单个流的速率更新,我们从两台主机向同一接收器运行两个流一秒钟,以造成拥塞并跟踪其中一个流的吞吐量变化,因为它们都收敛到相同的速率。Tonic的行为与硬编码的实现非常匹配(图4)。我们还运行了一个100Gbps的DCQCN流,其中包含128B背靠背数据包,并确认Tonic可以使100Gbps链路饱和。
多流。两个发送方各向一个接收方发送100个流,因此200个流在一秒钟内共享一条瓶颈链路。Tonic和硬编码的实现都在同一主机上的流之间执行每数据包循环调度。结果,这两种情况下的所有流最终的平均吞吐量为203±0.2 Mbps。此外,我们观察到两种情况下CNP的分布是匹配的。
07 相关工作
Tonic是第一个能够支持100Gbps的可编程硬件传输逻辑架构。在本节中,我们回顾最密切相关的先前工作。
商用硬件网络协议栈。一些网卡的硬件网络堆栈带有硬连线传输协议[8,10]。但是,它们只实现了两种协议,要么是ROCE[8]要么是供应商选择的TCP变体,并且只能由供应商修改。Tonic使程序员能够以最小的努力在硬件中实现各种传输协议。由于缺乏对这些NIC体系结构的公开详细描述,我们无法将我们的设计决策与他们的进行比较。
非商业性硬件传输协议。最近的工作探索了高速运行、占用内存少的硬件传输协议[30,31,34]。Tonic通过使研究人员能够以适度的开发努力实施新的协议来促进这一领域的创新。
加速网络功能。一些学术和工业项目将终端主机虚拟交换和网络功能卸载到FPGA,处理已经生成的数据包流[14,20,28,29,41]。另一方面,Tonic通过一次跟踪可能的几百个数据段来实现NIC中的传输逻辑,以便在运行用户定义的传输逻辑的同时以线速生成数据包,以确保高效可靠的传输。
附录
附录A 在RDMA中集成Tonic
远程直接内存访问(RDMA)使应用程序能够直接访问远程端点上的内存,而不涉及CPU。为此,端点创建类似于连接的queue pair,并发布请求,称为Work Queue Elements (WQEs),用于发送或接收彼此的内存数据。虽然RDMA起源于InfiniBand networks,但以太网上的RDMA在数据中心变得越来越普遍[9,22,35]。在本节中,我们使用RDMA来指代以太网上的RDMA实现。
一旦创建了队列对,RDMA NIC就可以将新的“连接”添加到Tonic,并在the sender side使用它来传输数据,以响应不同的WQE。每个WQE对应于一个单独的消息传输,因此很好地满足了Tonic在开始数据传输之前确定段边界的需要。
例如,在RDMA写入中,一个端点发布一个Request WQE以写入另一个端点上的内存。在Request WQE中指定数据长度、发送方上的数据源地址和接收方上的数据接收器地址。因此,RDMA应用程序和Tonic之间的缓冲区可以决定段边界,并通知Tonic要从发送器上读取数据的段数和源存储器地址。一旦Tonic生成下一个网段地址,RDMA网卡的其余部分就可以从发送方的内存中对其进行DMA,并添加合适的报头。RDMA发送类似于RDMA写入,不同之处在于它需要接收方上的接收WQE来指定来自发送方的数据应该写入的接收方地址。所以,Tonic仍然可以在发送方以相同的方式使用。
另一个示例,在一个RDMA Read中,一个端点从另一个端点上的存储器请求数据。因此,响应方端点应该向请求方端点传输数据。同样,数据长度、响应方的数据源地址和请求方的数据接收器地址在WQE中指定。因此,缓冲区可以决定数据段边界,并使用Tonic传输数据。
因此,Tonic可以集成到RDMA 网卡中,以取代数据传输发送方的硬编码传输逻辑。事实上,我们的两个基准协议ROCE w/DCQCN[43]和IRN[34]是为RDMA 网卡提出的。也就是说,这是假设在另一端有一个兼容的接收器来生成控制信号(例如,确认、拥塞通知等)。无论选择在发送端的Tonic上实现哪种传输协议都需要这些信号。
虽然以太网上的RDMA的一些实现,如iWarp[7]处理失序(OOO)数据包并实现类似TCP/IP的确认,但其他的(即RoCE[8])假设一个无损网络并具有更简单的传输协议,不要求接收器处理OOO数据包和产生频繁的控制信号。然而,随着以太网上的RDMA在数据中心越来越普遍,在这些网卡中也正在实现在接收器上处理OOO数据包和生成各种控制信号的能力,以实现更有效的传输[34,43]。
最后,Tonic在同一流中提供有序可靠的数据传输。因此,通过同一流发送的消息以相同的顺序传输给接收方。然而,有时需要支持通信端点(例如,队列对)的无序消息传输,例如,当消息彼此独立时,或者当使用“非连接”的端点(例如,一个发送者和多个接收者)时,可以提高应用的性能。通过在Tonic中为同一通信端点创建多个流并同时使用它们,仍然可以使用Tonic支持无序消息传输。扩展Tonic以支持同一流中的无序消息传输是未来工作的一个有趣途径。
附录B 高精度单流速率限制
在信用引擎中使用速率时,如果一个速率为R字节/周期的流需要L字节的信用来传输一个段,则Tonic计算T=L/R作为该流将有足够的信用来传输的时间。它设置一个定时器,该定时器在T周期内到期,并且在其到期时,将未准备好发送的流排队以进行传输(§3.3.2)。由于Tonic无法在其时间限制内进行浮点除法,因此R必须表示为整数。
这在速率限制精度和Tonic可以支持的速率范围之间进行了权衡。如果我们将R表示为每个周期的字节数,则可以计算流将具有足够信用的确切周期,但不能支持低于每周期一个字节或约1Gbps的速率。如果我们用每千次周期的字节数来表示R,我们可以支持较低的速率(例如,1 Mbps),但T=L/R将确定从现在起该流可以有多少千次周期有资格进行传输。这导致较高带宽流的速率一致性和精度较低。举个具体的例子,对于一个20Gbps的流,R将是每千次周期25000字节。假设流有1500字节的数据段要传输。它将有足够的信用在8个周期内完成,但是必须等待[1500/25000]=1000个周期来排队等待传输。
Tonic没有对R进行单一表示,而是对每个流保留多个变量R1,. . ., Rk,每个变量以不同的精确程度代表流的速率。由于拥塞控制环路根据网络容量调整速率,Tonic可以有效地在R1、.。。。,Rk之间切换以选择最精确的表示法来随时计算T。这使得Tonic能够在不牺牲速率限制精度的情况下支持各种的速率。
附录C 高效的位图操作
Tonic使用高达128位的位图来跟踪每个流的段窗口的状态。位图被实现为环形缓冲区,头部指针对应于第一个未确认的段。随着新的确认到来,头部指针在环形中向前移动。为了有效地实现其输出取决于位图中所有位的值的操作,我们必须通过将环形缓冲区划分为较小的部分,并行处理它们,并合并结果来对它们进行并行化。对于较大的环形缓冲器,这种分割运行的模式在多层中重复。由于每一层都依赖于前一层的输入,我们必须将每一层的计算保持在最低限度,以保持在10 ns的目标范围内。
其中一种操作是找到头部之后的第一个设置位。此操作用于在marked-for-rtx位图中查找下一个丢失的数据段进行重传。环形缓冲区的头部移动使该操作的实现变得复杂。假设我们有一个32位的环形缓冲区A32,第5和30位设置为1,头部位于索引6。因此,find first(A32,6)=30。我们把环形缓冲区分成8个4位的部分,并将结果送入8位环形缓冲区A8,其中A8[i]=OR(A32[i:i+3])。因此,只设置了A8[1]和A8[7]。然而,由于A32[4:7]中的设置位在原环形缓冲区中的头部之前,我们不能简单地使用1作为A8的头部索引,否则我们将错误地生成5而不是30作为最终结果。因此,我们需要额外的计算来找到正确的新头部。对于具有多层这种分割运行模式的较大环形缓冲区,我们需要计算每一层的头部。
相反,我们在输入环形缓冲区上使用了轻量级的预处理,以完全避免头部索引计算。更具体地说,使用A32作为输入,我们计算出等于A32的A’32,只是从索引0到头部(在我们的例子中是6)的所有位都被设置为0。从索引0开始,A’32中的第一个设置位始终比A32中的第一个设置位更接近原始头部。因此,如果A’32有任何设置位,则find first(A32,6)等于find first(A’32,0),否则等于find first(A32,0)。这样,与输入的头部索引H无关,我们总是可以从头部索引固定为0的两个子问题中求解find first(A,H)。
附录D 使用Tonic通过Socket API确定流量的C和T
在§5中,我们提供了一个示例,说明如何将Tonic集成到Linux内核中,以便应用程序可以通过Socket API使用它。我们引入了两个参数:(i)C,它是流的固定段大小;(ii)T,它是内核在向Tonic发送“子段”(小于C的段)之前等待来自应用程序的更多数据的持续时间。C和T可以根据每个流的延迟和吞吐量特性进行配置。对于高带宽流,可以将C设置为MSS(如果更大,则可以使用TSO)。对于只偶尔生成数据段的流,正如我们下面讨论的那样,设置C和T并不是那么简单。
在固定C的情况下,增加T会导致更多的小数据段在发送到Tonic进行传输之前被合并成C大小的数据段,但代价是延迟更高。C决定了Tonic生成的数据段的大小和子数据段的数量。回顾一下§5,当没有足够的数据在T内形成一个完整的C大小的段时,就会创建一个子段。Tonic需要除突发中的最后一个子段外的所有段的大小相等。因此,即使在子段被发送到Tonic进行传输之后,更多的数据被添加到套接字缓冲区,Tonic也必须成功地传送所有先前的段,然后才能开始传输新的段。因此,最好是产生更大的分段但更少的子分段。在T固定的情况下,增加C会产生更大的段。然而,为了产生更少的子段,C应该被挑选出来,以便在大多数情况下突发中的数据可以被C整除。突发在时间上被T分隔。因此,T的选择会影响C的选择,反之亦然。
例如,如果应用程序周期性地生成128字节的请求,则C可以很容易地设置为128,T则基于周期性。作为另一个例子,对于一个周期性地生成大小不一的段的应用程序,将T设置为0并且将C设置为最大预期段大小,会导致Tonic传输由应用产生的数据段而不进行整合,从而潜在地创建许多子段。对于同一应用程序,将T设置为0,将C设置为最小预期段大小可能会导致Tonic生成许多小段,因为所有分段都将被分解为最小预期段大小。请注意,如果Tonic用于仅偶尔生成数据段的流,则这些权衡会变得更加明显。对于高带宽流,C可以设置为MSS(如果使用TSO则会更大),而T则取决于应用程序的流量模式和突发度。
THE END
作者:网络交换FPGA
原文链接:网络交换FPGA
推荐阅读
- 业界第一个真正意义上开源100 Gbps NIC Corundum介绍
- 【重磅干货】手把手教你动态编辑Xilinx FPGA内LUT内容
- 介绍一篇可以动态编辑Xilinx FPGA内LUT内容的深度好文!
更多IC设计技术干货请关注IC设计技术专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。