PIFO到底是什么?【Programmable Packet Scheduling at Line Rate】

Programmable Packet Scheduling at Line Rate

ABSTRACT

如今,交换机提供了一个调度算法的small menu。虽然我们可以调整调度参数,但在交换机设计完成后,我们不能修改算法逻辑,也不能添加全新的算法。本文介绍了一种可编程分组调度器的设计,它允许调度算法–可能是目前未知的算法–被编程到交换机中,而不需要重新设计硬件。

我们的设计利用了调度算法做出两个决定的特性:调度分组的顺序和调度它们的时间。此外,我们观察到在许多调度算法中,在分组排队时可以对这两个问题做出明确的决定。我们使用这些观察结果来构建使用单一抽象的可编程调度器:PIFO(Push-in First-Out Queue,推入先出队列),即维护调度顺序或时间的优先队列。

我们展示了基于PIFO的调度器允许我们编程各种各样的调度算法。我们给出了一个用于64端口10Gbit/s共享内存(输出排队)交换机的调度器的硬件设计。我们的设计在芯片面积上额外增加了4%的成本。作为回报,它让我们可以编程许多如此形式化的算法,例如在每一级都有可编程决策的5级分层调度器(Hierarchical Scheduler)。

1. INTRODUCTION

当今最快的交换机,也称为线速交换机,提供了一个很小的调度算法菜单:通常是赤字轮询(Deficit Round Robin)[36]、严格优先级调度(strict priorityscheduling)和流量整形(traffic shaping)的组合。网络运营商可以更改这些算法中的参数,但不能更改现有算法中的核心逻辑,也不能在不构建新的交换机硬件的情况下编写新的算法。

相反,利用可编程分组调度器,网络运营商可以根据应用需求定制调度算法,例如,使用最短剩余处理时间(Shortest Remaining Processing Time)[35]最小化流完成时间(minimizing flow completiontimes)[12],使用加权公平排队(Weighted Fair Queueing)[20]跨流或租户(across flows or tenants)[26,33]灵活地分配带宽,使用最小空闲时间优先( Least Slack Time Firs)[29]最小化尾包延迟(minimizing tailpacket delay),等等。此外,有了可编程分组调度器,交换机供应商可以将调度算法实现为运行在可编程交换芯片上的程序,与将相同的算法作为刚性硬件 baking到芯片中相比,使这些算法更容易验证和修改。

本文提出了一种线速交换机中可编程分组调度的设计方案。所有的调度算法都做出两个基本决策:分组应该被调度的顺序和它们应该被调度的时间,分别对应于节省工作和不节省工作的算法。此外,对于许多调度算法,这两个决策可以在分组排队时做出。该观察结果提出了用于分组调度的自然硬件原语:推入先出队列(PIFO)[19,38]。PIFO是允许基于元素的等级(调度顺序或时间)将元素推入任意位置的优先级队列,但总是将元素从头部出列。

当等级表示调度时间时,PIFO实现日历队列;为此,我们区分PIFO和优先级队列。

我们开发了一个基于PIFO的调度规划模型(§2),有两个关键思想。首先,我们通过提供一个用于计算数据包排名的小程序,允许用户在PIFO中设置数据包的排名(§2.1)。将该程序与单个PIFO相结合允许用户对任何调度算法进行编程,其中缓冲分组的相对调度顺序不随未来分组到达而改变。其次,用户可以在树中将PIFO组合在一起进行编程,违反此相对排序属性的分层调度算法(§2.2和§2.3)。

我们发现,基于PIFO的调度器允许我们编程许多调度算法(§3),例如,加权公平队列(Weighted Fair Queueing)[20]、令牌桶过滤(Token Bucket Filtering)[10]、分层数据包公平队列(Hierarchical PacketFair Queueing)[13]、最小空闲时间优先(Least-Slack Time-First)[29]、速率控制服务规则(the Rate-Controlled Service Disciplines)[42]和细粒度优先调度(Fine-grained priority scheduling)(例如,最短作业优先(Shortest Job First))。到目前为止,这些算法的任何线速实现-如果它们全部存在的话-都已经硬连线到交换机硬件中。我们还通过举例说明了不能使用PIFO编程的调度算法,从而描述了PIFO抽象的局限性(§3.5)。

为了评估PIFO的硬件可行性,我们在Verilog[9]中实现了设计(§4),并将其综合成行业标准的16 nm标准单元库(§5)。我们设计中的主要操作是以线速对PIFO元素数组进行排序。为了实现这种传统上被认为难以实现的排序[30,36],我们利用了两个观察结果:第一,大多数调度算法跨流进行调度,每个流中的数据包排名越来越单调,因此,我们只需要对所有流的头部数据包进行排序,就可以从PIFO中出队。第二,晶体管缩放现在使得以线速对这些报头分组进行分类成为可能。

因此,我们发现(§5)构建可编程调度器是可行的,该调度器

·支持5级分层调度,其中每个级别的调度算法都是可编程的;

·以1 GHz的时钟频率运行-足以支持64端口10 Gbit/s共享内存交换机;

·与仅支持较小调度算法菜单的共享内存交换机相比,仅使用4%的额外芯片面积;以及

·与数据中心的典型共享内存交换机(60K数据包,1K流)具有相同的缓冲区大小[3]。

虽然我们还没有生产出支持PIFO的芯片,但我们的综合结果很有希望,为开关芯片制造商投资于可编程调度器的硬件提供了强有力的技术支持。为此,我们的可编程调度器的硬件参考模型的C++代码和硬件设计的Verilog代码可在http://web.mit.edu/pifo/.获得。

2. A PROGRAMMING MODEL FOR PACKET SCHEDULING

对于节省工作的调度算法,特点是分组被调度的顺序;对于非节省工作的调度算法,特征是每个分组被发送的时间。此外,对于实践中使用的大多数算法,当分组被排队到分组缓冲器[38]时,可以明确地确定这两个判决。

我们的编程模型是围绕这一观察结果构建的,有两个基本组件:
图1

正在计划STFQ的事务。P.x指的是数据包P.中的数据包字段x。y指的是跨数据包在交换机上持续存在的状态变量,例如,此片段中的LAST_Finish和VIRTUAL_TIME。p.rank表示数据包的计算排名。

1.推入先出队列(PIFO)[19],其主要保存入队单元的调度顺序或调度时间。PIFO是一种优先级队列,允许根据元素的等级将元素排队到任意位置,但将元素从头部出列。具有较低等级的元素首先出列;如果两个元素具有相同的值,则较早排队的元素首先出列。

2.在元素进入PIFO之前计算元素的rank。我们将此计算建模为封包事务(packet transaction)[37],这是一个原子执行的代码块,在将每个元素排队到PIFO之前,该代码块对每个元素执行一次。

我们注意到,使用PIFO抽象的调度不需要将包存储在每个流队列中。

我们现在描述我们编程模型中的三个主要抽象。首先,我们展示了如何使用调度事务来编写使用单个PIFO的简单节能调度算法(§2.1)。其次,我们将其推广到一棵调度树上,用来编写层次化的节功调度算法(§2.2)。第三,我们用整形事务来扩充这棵树的节点,以编程非节省工作的调度算法(§2.3)。

2.1 Scheduling transactions

在本文中,流是共享特定分组字段的公共值的任意分组集合。

因为原始的WFQ算法[20]具有复杂的虚拟时间计算,所以需要近似。

在处理N个连续分组后,交换机上可见的任何状态都与按照分组到达[37]的顺序跨N个分组的事务的串行执行相同。

调度事务(scheduling transaction)是与在分组入队之前针对每个分组执行一次的PIFO相关联的代码的挡路。调度事务计算分组的rank,从而确定其在PIFO中的位置。单个调度事务和PIFO足以指定任何调度算法,其中已经在缓冲器中的分组的相对调度顺序不会随着未来分组的到达而改变。

加权公平队列(WFQ)[20]就是一个例子,它实现了链路容量在共享一条链路的流之间的加权最大-最小分配。WFQ4的近似值包括赤字轮询(DRR)[36]、随机公平队列(SFQ)[30]和开始时间公平队列(STFQ)[25]。我们在这里考虑STFQ,并展示如何使用图1中的调度事务对其进行编程。

在分组入队之前,STFQ计算该分组的虚拟开始时间(图1中的p.start),作为该分组的流中的前一个分组的虚拟结束时间(图1中的last_Finish[f])的最大值和虚拟时间的当前值(图1中的virtual_time),该状态变量跟踪所有流中最后一个出列分组的虚拟开始时间(§5.5讨论如何在入队时访问该状态变量)。在此之前,STFQ计算该分组的虚拟开始时间(图1中的p.start)作为该分组流中的前一个分组的虚拟结束时间(图1中的last_Finish[f])和虚拟时间的当前值(§5.5讨论了如何在入队中访问该状态变量)。包按照增加的虚拟开始时间的顺序进行调度,虚拟开始时间是包在PIFO中的等级。
图2

使用PIFO编制HPFQ程序。“左”和“右”是类别。A、B、C和D是流。在调度树中的每个树节点内,第一行是分组谓词,第二行是调度事务。除了它们的flow()函数之外,所有三个节点都为调度事务执行相同的代码,该函数返回数据包的流/类。对于WFQ_Root,它返回包的类:Left/Right。对于WFQ_LEFT和WFQ_RIGHT,它返回数据包的流:A/B或C/D。

2.2 Scheduling trees

当新分组到达时需要改变缓冲分组的相对顺序的调度算法不能使用单个调度事务和PIFO来编程。这类算法的一个重要类别是分层调度器,它在分层的不同级别上组成多个调度策略。我们为这类算法引入了一棵调度树。

为了说明调度树,请考虑分层分组公平排队(HPFQ)[13]。HPFQ首先在类之间划分链路容量,然后递归地在每个类中的子类之间递归,一直到叶子节点。图2a提供了一个示例;每个子节点上的数字指示其相对于其兄弟节点的权重。不能使用单个调度事务和PIFO来实现HPFQ,因为已经缓存的分组的相对调度顺序可以随着将来的分组到达而改变(HPFQ论文[13]的第2.2节提供了一个示例)。

然而,HPFQ可以使用PIFO树来实现,其中调度事务附加到树中的每个PIFO。要查看如何执行,请观察HPFQ在层次结构的每个级别执行WFQ,每个节点在其子节点中使用WFQ。如§2.1所述,单个PIFO编码WFQ的当前调度顺序,即,如果没有其他到达,则编码调度顺序。类似地,可以使用PIFO树(图3)来编码HPFQ和其他分层调度算法的当前调度顺序,其中每个PIFO的元素要么是分组,要么是对其他PIFO的引用。要确定此计划顺序,请检查用于确定要调度的下一个子PIFO的根PIFO。然后,递归检查子PIFO以确定要调度的下一个孙子PIFO,直到到达确定要调度的下一个分组的叶PIFO。

当分组入队时,可以通过在PIFO树中的每个节点执行调度事务来修改PIFO树的当前调度顺序。这是我们的第二个编程抽象:调度树。此树中的每个节点都是一个具有两个属性的元组。首先,一个数据包谓词,指定哪些数据包在将元素排队到该节点的PIFO之前执行该节点的调度事务;该元素可以是数据包,也可以是对该节点的子PIFO的引用。其次,调度事务指定如何为排队到节点的PIFO中的元素(包或PIFO引用)计算排名。图2b显示了HPFQ的示例。

图3

当分组被排队到调度树中时,它在其分组预测匹配到达分组的每个节点处执行一个事务。这些节点形成从树叶到树根的路径,并且每个节点上的事务在该路径上更新该节点上的调度顺序。一个元素在从叶到根的路径上的每个节点处排队到PIFO中。在叶节点,该元素是数据包本身;在其他节点,它是指向叶的路径上的下一个PIFO的引用。分组按照PIFO树编码的顺序出列(图3)。

2.3 Shaping transactions

图4

使用PIFO对具有整形的分层结构进行编程。调度树中每个树节点内的第三行是整形事务。WFQ_Right、WFQ_Left和WFQ_Root的调度事务与图2相同。

到目前为止,我们只考虑了节省工作的调度算法。整形事务允许我们编写非节省工作的调度算法。非工作节约算法与工作节约算法的不同之处在于,它们决定数据包与调度顺序相对的调度。例如,考虑图4a中所示的算法,它扩展了前面的HPFQ示例,要求正确的类限制为5Mbit/s。我们在整篇文章中将此示例称为带整形的层次结构。

为了激发我们对非工作节约算法的抽象,回想一下,PIFO树通过从根PIFO遍历到叶PIFO来调度数据包,从而编码当前的调度顺序。通过这种编码,仅当PIFO引用驻留在PIFO中并且存在从根PIFO到该PIFO引用的PIFO引用链时,才能调度PIFO引用。为了对非节省工作的调度算法进行编程,我们提供了当PIFO引用进入PIFO树并因此可用于调度时延迟的能力。

为了将队列延迟到PIFO树中,我们在调度树的节点上增加了可选的第三个属性:对节点的数据包谓词匹配的所有数据包执行整形事务。节点上的整形事务确定对该节点的PIFO的引用何时可用于在该节点的父PIFO中进行调度。整形事务使用子节点的整形PIFO(与所有节点的调度PIFO不同)来实现,该PIFO保存对子的调度PIFO的引用,直到它们被释放到父的调度PIFO为止。整形事务使用挂钟离开时间作为整形PIFO的排序,这与使用相对调度顺序作为排序的调度事务不同。

一旦对子调度PIFO的引用已经从孩子的成形PIFO释放到父的调度PIFO,则通过执行当事人的调度事务并将其入队到父的调度PIFO来调度它。如果一个节点没有整形事务,那么对该节点调度PIFO的引用将立即进入其父节点的调度PIFO而不会延迟。整形期间的出队逻辑仍然遵循图3:从根递归出队,直到我们调度分组。

图4C示出了基于具有速率限制R和突发允许B的令牌桶过滤(Tbf)来推迟入队的整形事务的示例。在此,分组的SWALL时钟离开时间(P.Send_Time)用作整形PIFO中的它的秩。图4b显示了如何使用该整形事务来对具有整形的层次进行编程:TBF整形事务(TBF_RIGHT)确定何时将Right的调度PIFO的PIFO引用释放给Root的调度PIFO。

图5

使用PIFO对具有整形的分层结构进行编程。调度树中每个树节点内的第三行是整形事务。WFQ_Right、WFQ_Left和WFQ_Root的调度事务与图2相同。

整形过程中的操作时间。当分组在PIFO树中排队时,它在其谓词与该分组匹配的叶节点执行调度事务,然后沿着路径向上向执行调度事务的根继续,直到它到达也附加了整形事务的第一个节点。图5显示了在此节点(子节点)及其父节点(父节点)上执行的操作,以实现整形。

在Child处执行两个事务:用于将元素推入Child的调度PIFO的原始调度事务(图5中的步骤1a)和将引用Child的调度PIFO的元素R推入Child的整形PIFO的整形事务(步骤1b)。在R被推入Child‘s Shaping PIFO之后,该分组的进一步交易被暂停,直到达到R的等级,即挂钟时间T。

在T,R将从孩子的成形PIFO中出列,并排队到父的调度PIFO中(步骤2),使其可用于在父的调度。包的根路径的睡觉现在从父级开始恢复。如果存在以下情况,此挂起-恢复过程可能会多次发生具有整形事务的多个节点沿着包的路径从其叶到根。

3. THE EXPRESSIVENESS OF PIFOS

除了§2中的三个示例之外,我们现在还提供了更多示例(§3.1到§3.4),并描述了我们的编程模型的局限性(§3.5)。

3.1 Least Slack-Time First

最小空闲时间优先(LSTF)[29,31]按照分组空闲的递增顺序(即,到每个分组的最后期限的剩余时间)在每台交换机上调度分组。数据包松弛在终端主机或边缘交换机处初始化,并根据每个交换机队列中的等待时间减少。我们可以使用一个简单的调度事务来编写LSTF:

p.rank = p.slack + p.arrival_time

将数据包的到达时间与数据包中已有的空闲时间相加,可确保数据包在出队时按空闲时间的顺序出列,而不是入队。然后,在数据包出队后,我们从数据包的空闲时间中减去数据包出队的时间,其效果是将空闲时间减去交换机队列中的等待时间。该减法可以通过对可编程交换机[17]的出口流水线编程以将一个报头字段递减另一个来实现。

3.2 Stop-and-Go Queueing

图6

针对走走停停排队的整形事务

停走队列[24]是一种非节省工作的算法,它使用帧策略向分组提供有限的延迟。时间被划分为等长T的非重叠帧,其中帧中到达的每个分组在帧的末尾被发送,平滑了由前一跳引起的业务量模式中的任何突发性。

图6中的整形事务指定了方案,Frame_Begin_Time和Frame_End_Time是跟踪挂钟时间中当前帧的开始和结束的两个状态变量。当数据包入队时,其离开时间设置为当前帧的末尾。具有相同出发时间的多个包以先入先出的顺序发出,这由PIFO的同等等级打破平局的语义保证(§2)。

3.3 Minimum rate guarantees

当今许多交换机上的一种常见调度策略是为流提供最小速率保证,只要这种保证的总和不超过链路容量,可以使用具有两级PIFO树的PIFS来编程最小速率保证,其中树的根实现跨流的严格优先级调度。低于其最小速率的流优先于流进行调度高于他们的最低利率。然后,在树的下一级,PIFO为每个流实现FIFO规程。
图7

正在计划最少的事务。费率保证。

当分组排队时,我们在其叶节点执行FIFO调度事务,将其等级设置为到达时的挂钟时间。在根,PIFO引用(包的流标识符)使用反映流在当前包到达后是高于还是低于其速率限制的等级被推入根PIFO。为了确定这一点,我们运行图7中的调度事务,该事务使用令牌桶(状态变量TB),该令牌桶可以被填充到Burst_Size,以确定到达的分组将流置于MIN_RATE之上还是之下。

注意,具有图7中的调度事务的单个PIFO节点是不够的。它会导致流中的数据包重新排序:到达的数据包可能会导致流从较低的优先级移动到较高的优先级,并且在此过程中,会在来自同一流的较低优先级的数据包之前离开。两级树通过将优先级附加到特定流(而不是特定分组)的传输机会来解决此问题。现在,如果到达的分组导致流从低优先级移动到高优先级,则从该流调度的下一个分组是按FIFO顺序选择的该流中最早的分组,而不是到达的分组。

3.4 Other examples

现在我们简要描述几个可以使用PIFO编程的调度算法。

1.细粒度优先级调度。许多算法调度具有由终端主机初始化的字段的最低值的分组。这些算法可以通过将分组的等级设置为适当的字段来编程。这些算法及其使用的字段的示例是严格优先级调度(IP TOS字段)、最短流优先(流大小)、最短剩余处理时间(剩余流大小)、最小可达服务(流接收的字节数)和最早死线优先(截止日期之前的时间)。

2.服务曲线最早截止日期优先(SC-EDF)[34]按照从流的服务曲线计算的上行的递增顺序来调度分组,该服务曲线指定流在任何给定时间间隔上应该接收的服务。我们可以使用将数据包的等级设置为SC-EDF算法计算的截止日期的调度事务。

3.诸如抖动-EDD[41]和分层舍入[27]的速率控制服务规程(RCSD)[42]是一类非节省工作的调度器,其可以使用速率调节器来整形业务和使用分组调度器来调度整形的业务的组合来实现。通过使用整形事务设置速率调节器和使用调度事务设置分组调度器,可以使用PIFO来编程RCSD算法。

4.增量部署。运营商可能希望仅对其业务的子集使用可编程调度。这可以编程为分层调度算法,一个FIFO类专门用于遗留业务,另一个专门用于实验业务,在实验类中,操作员可以对任何调度树进行编程。

3.5 Limitations

改变流的所有分组的调度顺序。虽然PIFO树可以启用其中缓冲分组的相对调度顺序响应于新分组到达而改变的算法(§2.2),但它不允许对缓冲分组的调度顺序进行任意改变。具体地说,当来自该流的新分组到达时,它不支持更改该流的所有缓冲分组的调度顺序。

需要这种能力的算法的一个例子是pFabric[12],它引入了“饥饿预防”来以FIFO顺序调度具有最短剩余大小的流的分组,以防止分组重新排序。要了解这超出PIFO能力的原因,请考虑下面的到达序列,其中pi(J)表示来自具有剩余大小j的流i的分组,其中剩余大小是流中未确认字节的数量。

1.排队P0(7)

2.排队p1(9),p1(8).

3.调度顺序为:p0(7),p1(9),p1(8).

4.排队p1(6).

5.新的顺序是:p1(9),p1(8),p1(6),p0(7).

指定这些语义超出了我们已经开发的PIFO抽象的能力。例如,使用PIFO树添加层次级别是没有帮助的。假设我们编程了一棵PIFO树,在叶上实现FIFO,并在根上根据剩余的流大小在流中进行挑选。这将导致在排队P1(6)之后的调度顺序P1(9)、P0(7)、P1(8)、P1(6),问题是无法通过仅将对流1的一个引用排队来改变根PIFO中对流1的多个引用的调度顺序。

然而,单个PIFO可以实现无饥饿防止的pFabric,这与最短剩余处理时间(SRPT)规则相同(§3.4)。它还可以实现最短流优先(SFF)规则(§3.4),其性能几乎与pFabric[12]一样好。

跨调度树中的多个节点的流量整形。我们的编程模型将单个整形和调度事务附加到树节点。这允许我们在单个节点上强制限制,但不能跨多个节点强制限制。

例如,PIFO不能在一组流A、B和C上表达以下策略:WFQ,附加约束是A+B的总吞吐量不超过10Mbit/s。一种变通办法是跨两个类C1和C2将其实现为HPFQ,其中C1包含A和B,C2仅包含C。然后,我们对C1实施10Mbit/s的速率限制,如图4所示。但是,这并不等同于我们想要的策略。更一般地,我们的可编程调度的编程模型在调度和整形事务之间建立了一对一的关系,这对某些算法是有限制的。

输出速率限制。PIFO抽象使用整形事务来实施速率限制,该事务在将空间或PIFO引用入队到PIFO之前确定其调度时间。整形事务允许在输入侧,即在元素排队之前进行速率限制,另一种速率限制形式是在输出端,即通过限制元素被调度的速率。

为了说明不同之处,考虑具有两个优先级队列LO和HI的调度算法,其中LO被限制为10Mbit/s。为了使用输入角速率限制对其进行编程,我们将使用整形事务对LO施加10Mbit/s速率限制,并使用调度事务在LO和HI之间实现严格的优先级调度。现在,假设来自HI的分组长时间地饥饿LO。在此期间,来自LO的数据包在离开整形PIFO后,积累在与HI共享的PIFO中。现在,如果突然没有更多的HI分组,则来自L0的所有分组都以线速传输,并且在瞬时时间段内不再将速率限制为10Mbit/s,即,直到LO的所有实例都被排出与HI共享的PIFO。投入利率限制仍然提供长期的利率保证,而产出利率限制也提供短期的保证。

4. DESIGN

现在我们给出一种基于PIFO的可编程调度器的硬件设计。我们的目标是共享内存交换机,如Broadcom的三叉戟II3。在这些交换机中,解析器将来自所有端口的数据包馈送到共享入口管道,然后它们进入共享调度器和类似的共享出口管道。为了减少芯片面积,分组处理的组合逻辑和存储器在流水线和调度器中的端口之间共享。因此,交换机上的数字电路必须以最小数据包大小处理所有输出端口的聚合处理要求,例如,每个传输64字节数据包的64个10Gbit/s端口。在考虑最小数据包间间隙或1 GHz时钟频率后,这转换为每秒约1万亿个数据包。

我们首先描述如何实现调度和塑造事务(§4.1)。然后,我们展示一棵圆周率树是如何通过适当地互连PIFO块,可以使用PIFO块的全网状来实现FOS(§4.2)。我们还描述了编译器(§4.3)如何从调度树自动配置该网格。
图8

64端口共享内存交换机。组合逻辑和内存跨端口共享,无论是在流水线中还是在调度器中都是如此。交换机以1 GHz的时钟频率运行。

4.1 Scheduling and shaping transactions

为了编程和实现调度和整形事务,我们使用Domino[37],这是一个以线速对有状态数据平面算法进行编程的最新系统。Domino引入硬件原语(原子)和软件摘要(分组事务)来编写可编程交换机上的有状态算法[1,5,11,17]。

原子是代表可编程开关的指令集的处理单元,而包事务是保证自动执行的代码块。Domino编译器将调度或整形PacketTransaction编译成原子管道,该管道自动执行事务,如果事务不能在LineRate上运行,则拒绝该事务。事务可能会因为两个原因而被拒绝;要么是因为没有足够的原子来执行事务,要么是因为事务需要超出原子能力的计算。

Domino提出的原子应该具有足够的表现力,可以支持许多数据平面算法,并且足够小,可以在1 GHz下实现。例如,在一个32 nm的标准单元库[37]中,即使是最具表现力的原子对,也只占6,000μm2;一个200mm2交换芯片[23]可以以小于2%的面积支持300对原子。这300个原子足够许多数据平面算法使用[37]。Domino论文还展示了STFQ事务(图1)如何在具有PARAYS原子的交换机管道上以1 GHz的速度运行。

类似地,我们可以使用Domino编译器将其他调度和整形事务编译成一个原子管道。例如,令牌桶过滤事务(图4c)、最低速率保证事务(§3.3)、停走队列事务(§3.2)和LSTF事务(§3.1)都可以表示为Domino程序。Domino中的一个重要限制是没有循环,这排除了包含具有无限迭代计数的循环的秩计算。但是,我们还没有遇到需要此功能的调度或整形事务。

4.2 The PIFO mesh

图9

PIFO网格中的三个PIFO块

图10

一个PIFO挡路。在将元素入队到逻辑PIFO之前,入队操作在ATOM流水线中执行事务。出列操作在查找逻辑PIFO的下一跳之前将元素从逻辑PIFO出队.

我们将PIFO物理布局为一个完整的网格(图9)PIFO块(图10)。每个PIFO挡路支持多个逻辑PIFO。在分层调度算法中,这些逻辑PIFO对应于不同输出端口或不同类别的PIFO,它们共享PIFO所需的组合逻辑。我们期望在典型的交换机中有少量的PIFO块(例如,少于5个),因为每个PIFO挡路对应于分层调度树的不同级别,并且我们所知的最实用的分层调度算法不需要多于几个级别的分层。因此,在这些模块之间建立一个完整的网格是可行的(§5.3有更多细节)。

PIFO块在1 GHz下运行,并包含一个原子流水线,用于在进入逻辑PIFO之前执行调度和整形事务。在每个时钟周期中,每个PIFO块支持驻留在挡路内对数PIFO上的一个入队和出队操作(整形事务在每个时钟周期需要多于一个操作,将在§4.4中讨论)。

到PIFO挡路的接口是:

1.给定逻辑PIFOID、元素的排名和将随该元素一起携带的一些元数据(分组或对另一PIFO的引用),将元素(分组或对另一PIFO的引用)入队,诸如STFQ的排名计算所需的分组长度。入队不返回任何内容。

2.从块内的特定逻辑PIFO ID出列。出队将分组或引用返回到另一PIFO。

出队后,除了传输数据包外,PIFO块可能出于以下两个原因与另一个块通信:

1.使另一挡路中的逻辑PIFO出队,例如,当将PIFO序列从调度树的根到叶出队以发送分组时。

2.例如,当将刚刚从整形PIFO出队的分组入队时,将其入队到另一挡路中的逻辑PIFO中。

我们使用一个小查找表来配置这些出队后操作,该表查找出队后的“下一跳”。此查找表指定一个操作(入队、出队、传输)、下一个操作的PIFO挡路以及该操作需要的任何参数。

4.3 Compiling from a scheduling tree to a PIFO mesh

程序员应该不必手动配置PIFO网格。取而代之的是,编译器从调度虽然我们还没有建立这个编译器的原型,但是我们用HPFQ(图2)和Shaping(图4)说明了它是如何工作的。

编译器首先将调度树转换为指定每个PIFO上的入队和出队操作的对数PIFO树。图11a和12a分别显示了图2和图4的该树。然后,它通过将树的每一层分配给PIFO挡路,并根据树的要求配置查找表以连接PIFO块,从而将该树覆盖在PIFO网格上。图11b显示了图2的PIFO网格,而图12b显示了图4的PIFO网格。

如果树的特定级别具有来自另一级别的一个以上的入队或出队,这在整形事务存在时出现(§4.4),我们分配新的先进先出块以遵守任何先进先出挡路在每个时钟周期提供一个入队和出队操作的约束,例如,图12b具有包含tbf_right talone的额外的先进先出挡路。最后,我们使用Domino编译器编译调度和整形事务。

图11

将HPFQ(图2)编译为PIFO网格。在左边,逻辑PIFO树捕获PIFO之间的关系:哪些PIFO出队,哪些PIFO入队。红色箭头表示出列,蓝色箭头表示入队。在右边,我们在左边显示了逻辑PIFO树的物理PIFO网格,遵循相同的符号。

图12

将具有整形的层次结构(图2)编译为PIFO网格。适用与图11相同的注释。

4.4 Challenges with shaping transactions

每个PIFO挡路在每个时钟周期支持一次入队和出队操作。这对于只使用调度事务的任何算法(节省工作的算法)就足够了,因为对于这样的算法,每个分组需要在其调度树的每一级上最多一个入队和一个出队,并且我们将每一级上的PIFO映射到不同的PIFO挡路。

然而,塑造交易构成了挑战。考虑带有整形的层次结构(图12A)。当ShapingTransaction将元素入队到TBF_Right中时,这些元素将在未来时间T被释放到WFQ_Root中。如果分组到达T,则外部入队到WFQ_Root也可以在T发生。这会产生冲突,因为在同一周期中有两个入队操作。出列时也可能发生冲突。例如,如果TBF_RIGHT与另一个逻辑PIFO共享其先进先出队列挡路,则对两个逻辑PIFO的出队操作可以同时发生,因为TBF_RIGHT可以在任意挂钟时间出队。

在冲突中,两个操作中只有一个可以继续。我们将解决此冲突,以支持调度PIFO。整形PIFO用于将速率限制在低于线速率的速率。因此,他们可以承受几个小时的延误,直到没有冲突。相比之下,延迟调度PIFO的调度决策将意味着交换机将空闲并且不能满足其线速保证。

因此,塑造PIFO只能得到尽力而为的服务。这是有解决办法的。一种是超频(比方说)1.25 GHz的流水线,而不是1 GHz,为这种尽力而为的处理提供了散列时钟周期。另一种是提供多个端口到一个先进先出挡路,以支持每个时钟的多个操作。这些技术通常用于交换机的后台任务,例如回收缓冲区空间,也可以应用于PIFO网格。

5. HARDWARE IMPLEMENTATION

本节介绍我们的可编程调度器的硬件实现。我们讨论了性能要求(§5.1)、先入先出挡路的实现(§5.2)以及它们之间的全网状互连(§5.3)。最后,我们估计了设计的面积开销(§5.4)。

5.1 Performance requirements

我们的目标是实现一个带有通用共享内存交换机的可编程调度器好胜,比如今天许多数据中心使用的BroadcomTrident II[3]。基于三叉戟II,我们的目标是1000个可以跨逻辑PIFO灵活分配的流和12兆字节的数据包缓冲区大小[6],信元大小6为200字节。在最坏的情况下,每个Packee都是一个细胞。因此,每个PIFO块最多60K个分组/元素可以分布在多个逻辑PIFO上。

基于这些要求,我们的基准设计目标是支持64K分组和1024个流的PIFO挡路,可以在256个逻辑PIFO之间共享。此外,我们的目标是用于我们的PIFO挡路的16位等级字段和32位元数据字段(例如,P.Long图1)。我们将5个这样的块放在一个5-挡路的先入先出网格中,该网格在一个调度算法中可以支持高达5层的层次结构-对于我们所知的最实用的层次调度器来说已经足够了。

5.2 A single PIFO block

图13

先进先出挡路支持两种操作:将元素插入逻辑PIFO的入队和重新移动逻辑PIFO头部的出队。我们首先描述了一个具有单个逻辑PIFO的挡路的实现,然后将其扩展到同一挡路中的多个逻辑PIFO。

一个简单的实现是单个排序数组。传入的元素与所有数组元素进行并行比较,以确定新元素的位置,然后通过移位数组将其插入其中。但是,每个比较器需要一个比较器电路,不可能支持64K的比较器电路。

同时,几乎所有实际的调度算法例如基于业务类型、端口或地址将分组分组成流或类。然后,它们按FIFO顺序调度流的间隔,因为在流的连续包之间,包的秩增加了.这激发了包含两个部分的设计(图13):

1.流调度器,其基于每个流的头部(最早的)元素的等级来挑选要出队的元素。流调度器实际上是由所有流的头部元素组成的PIFO。

2.等级存储,一种FIFO存储体,按FIFO顺序为每个流存储超出头部的元素的等级。

该分解将请求排序的元素数量从分组数量(64K)减少到流数量(1024)。在入队期间,一个元素(等级和元数据)被附加到等级存储中适当的FIFO的末尾。对于流的第一个元素,我们绕过秩存储,直接将其推入流调度器。为了允许进入这个先入先出挡路,我们还向入队操作提供了流ID参数。

RANK存储所需的FIFO存储体是一种易于理解的硬件设计。这样的FIFO组被用来缓冲交换机中的分组有效负载,并且已经投入了大量的工程手段来优化它们。因此,我们把我们的重点放在仅在流调度器上实施工作。

Flow Scheduler。流调度器使用每个流中的头部元素的秩对数组进行排序。它支持在每个时钟周期内对其封闭的PIFO块进行一次入队和一次出队,这意味着每个时钟周期对流调度器的操作如下。

1.入队操作:当流由空变为非空时插入流。

2.出列操作:删除调度后清空的流,(或)如果流仍处于积压状态,则删除并重新插入具有下一个元素等级的流。

上述操作要求流调度器在每个时钟周期内部支持两个原语。

1.将最多两个元素推入流调度器:每个元素用于入队插入和出队重新插入。

2.弹出一个元素:用于从出队中移除。

这些原语并行访问流调度器的所有元素。为了促进这一点,我们在触发器中实现了流调度器,这与在SRAM中的秩存储不同。
图14

流调度器的硬件实现。流调度器中的每个元件连接到两个>比较器(2个推)和一个==比较器(1个POP)。
图15
用于流调度器的阶段流水线

流调度器是一个排序数组,其中通过执行以下三个步骤来实现推送(图14)。

1.使用比较器并行地将输入的等级与AR-射线中的所有等级进行比较。这会产生比较结果的位掩码,指示传入秩是否大于或小于数组元素的秩。

2.使用Apriority编码器找到该位掩码中的第一个0-1转换,以确定要推入的索引。

3.通过移位数组将元素推入此索引。

通过将head元素移出已排序的数组来实现弹出。

到目前为止,我们主要关注处理单个逻辑PIFO的流调度器的实现。为了处理多逻辑PIFO,我们保持元素按等级排序,而不管它们属于哪个逻辑PIFO;因此,推入逻辑不会改变。要从特定的逻辑PIFO弹出,我们比较所有元素以找到具有该逻辑PIFO ID的元素。其中,我们使用优先级编码器找到第一个元素,并通过移位数组来删除该元素。引入逻辑PIFO时,存储库实现不会改变;但是,我们确实要求一个流只属于一个逻辑PIFO。

为了在每个时钟周期同时发出2个PUSH和1个POP,我们配置了3个并行数字电路(图14)。PUSH和POP都需要2个时钟周期才能完成,并且需要流水线来维持所需的吞吐量(图15)。对于推入,流水线的第一级执行并行比较和优先级编码器步骤以确定索引;第二级使用索引将元素推入数组。类似地,对于POP,第一阶段执行相等检查(对于逻辑PIFOID)和优先级编码器步骤以计算索引;第二阶段使用索引将头元素从数组中取出。

我们的实现满足1 GHz的时序要求,每个时钟周期最多支持一个逻辑PIFO在一个PIFO挡路上进行一次入队/出队操作。因为重新插入操作需要弹出,然后访问排名存储下一个元素,然后推送,我们的实现支持从相同的逻辑PIFO出队,每4个周期只出队一次。这是因为如果在时钟周期1中初始化出队,则出队的POP在2中完成,在3中访问秩存储,并且在4中启动推送,使得周期5成为重新发出出队的最早时间。这一限制在实践中是无关紧要的。每4个周期从逻辑PIFO出队就足以服务当今最高的链路速度,100Gbit/s,这需要最多每5个时钟周期出队一次,最小数据包大小为64字节。每个周期仍然允许出列到不同的逻辑PIFO ID。

5.3 Interconnecting PIFO blocks

PIFO块之间的互连允许PIFO块进入其他块和从其他块出列。由于PIFO块的数量很少,我们在它们之间提供了一个完整的网格。对于我们的基线设计中的5-挡路PIFO网格,在PIFO块之间需要5*4=20组导线。每组都包含指定先入先出挡路上的入队和出队操作所需的所有输入。

对于我们的基线设计(§5.1),对于入队,我们需要逻辑PIFO ID(8位)、元素的秩(16位)、元素元数据(32位)和流ID(10位)。为出列时,我们需要逻辑PIFO ID(8位)和线路来存储出列元素的元数据字段(32位)。这就是说,每组线总计106位,或网格2120位。这是一块芯片的少量电线。例如,RMT的匹配-动作流水线在一对流水线级之间使用4000条1位线来在级之间移动其4K包头向量[17]。

5.4 Area overhead

因为我们的目标是共享内存交换机,所以调度逻辑是跨端口共享的,单个PIFO网格服务于整个交换机。因此,为了估算可编程调度器的面积开销,我们估算了单个PIFO网格的面积开销。我们的开销不必乘以端口数,对于具有相等聚合数据包速率的两个共享内存交换机(例如,6端口100G交换机和60端口10G交换机)来说,我们的开销是相同的。

为了确定PIFO网格的面积,我们计算单个PIFO挡路的面积,并将其乘以块的数量,因为互连的面积可以忽略不计。对于单个挡路的面积,我们分别估计了库存储、原子流水线和流调度器的面积,而忽略了小的下一跳查找表的面积。我们通过使用SRAM估计[8]来估计秩存储的面积,使用Domino[37]来估计原子流水线的面积,通过用Verilog[9]来实现流调度器的面积,并使用Cadence Meet RTL编译器[2]将其合成为16 nm标准单元库中的门级网表。RTL编译器还验证流调度器是否满足1 GHz的定时。

总体而言,我们的基准设计消耗了大约7.35mm2的芯片面积(表1)。使用Gibb等人提供的最小芯片面积200mm2估计,这大约是典型开关芯片芯片面积的3.7%。[23]。作为对这3.7%的回报,我们得到了比现有交换机更灵活的分组调度器,后者提供固定的两级或三级分层调度。我们3.7%的面积开销与其他可编程开关功能的开销相似,例如,2%用于可编程解析[23],15%用于可编程报头处理[17]。

从基线改变流调度器的参数。流调度器有四个参数:秩宽度、元数据宽度、逻辑PIFO数目和流出数目。其中,增加流的数量对流调度器是否满足1 GHz的定时影响最大。这是因为流调度器使用优先级编码器,其大小是流的数量,并且其关键路径延迟随流的数量而增加。通过将其他参数设置为它们的基准值,我们改变流的数量,以确定采用当今晶体管技术的流调度器的最终限制(表2),并发现我们可以扩展到2048个流,同时仍然满足1 GHz的定时。

其余参数影响流量调度器的面积,但对1 GHz的会议时间影响不大。例如,从占用0.224 mm~2的流调度器的基线设计开始,将秩宽度增加到32位使其增加到0.317 mm~2,将对数PIFO的数量增加到1024个使其增加到0.233 mm~2,并且增加元数据宽度增加到64位使其增加到0.317 mm~2。在所有情况下,流调度器继续满足定时要求。

表1

与基准交换机相比,5-挡路PIFO网状结构的芯片面积开销为3.7%。

表2

5.5 Additional implementation concerns

入队和出队之间的协调。当计算数据包排入队列时,一些调度算法在数据包出队时修改访问状态。一个例子是STFQ(§2.1),它在计算数据包的虚拟开始时间时访问VIRTUAL_TIME变量。这种入队-出队协调可以通过两种方式实现。一种是共享状态,可在入队和出队时访问,类似于队列占用计数器。另一种方法是周期性地同步相同状态的入队和出队视图:对于短时间排队,短期公平程度与入队侧的虚拟时间信息的更新程度直接相关。

缓冲区管理。我们的设计侧重于可编程的调度,并不管理交换机的数据缓冲区跨流的分配。缓冲区管理可以对每个流使用静态缓冲区限制。限制还可以是动态的,例如红色[22]和动态缓冲器大小调整[18]。

在共享内存交换机中,缓冲区管理与调度正交,并使用跟踪共享缓冲区中的流量占用情况。在数据包进入调度器队列之前,如果任何计数器超过不稳定或动态阈值,则丢弃该数据包。缓冲区管理的类似设计也可以与基于PIFO的调度器一起使用。

优先级流量控制。优先流控制(PFC)[7]是允许交换机向上游交换机发送暂停消息以请求其停止传输属于特定流的分组的标准。PFC可以集成到我们的硬件设计中,方法是在出队操作过程中屏蔽出队操作期间流调度器中的某些流,如果它们因为PFC暂停消息而暂停,并在接收到PFC恢复消息时取消它们的掩蔽。

多管道开关。当今最高端的交换机(如Broadcom Tomahawk[4])支持超过3 TBIT/秒的聚合容量。在最小数据包大小为64字节时,这相当于大约60亿个数据包/秒的聚合数据包速率。由于单个交换机管道(图8)通常以1 GHz的速度运行并处理10亿个数据包/秒,因此此类交换机需要多个单独共享对调度程序子系统访问的入口和出口管道。

在多流水线交换机中,每个挡路需要支持每个时钟周期(与进出流水线一样多)的多个入队和出队操作,因为每个时钟周期的任何一个输入端口都可以将分组入队,并且每个输入端口可以驻留在任何一个输入流水线上。类似地,每个出口流水线在每个时钟周期都需要一个新的分组,导致每个时钟周期有多个出列。

本文给出了一个完整的多流水线交换机设计方案,但我们目前的设计有利于多流水线交换机的实现。支持多个流水线的秩存储类似于今天的多流水线交换机的数据缓冲器。构建支持每个时钟的多个入队/出队的流调度器相对容易,因为它在触发器中维护,在触发器中添加多端口很简单(与SRAM不同)。

6. RELATED WORK

推入先出队列。PIFO首先被引入作为证明结构,以证明组合输入-输出队列交换机可以精确地仿真输出队列交换机[19]。我们在这里展示了PIFO可以用来作为线速可编程调度的抽象.

分组调度算法。文献中充斥着调度算法[12,13,24,25,29,35,36,42]。然而,线速交换机只支持少数几种:DRR、流量整形和严格的优先级。如§3所示,PIFO允许线速交换机运行许多此类调度算法,到目前为止,这些算法仅在软件路由器上运行。

可编程交换机。最近的工作提出了可编程开关的硬件结构[1,5,11,17]和软件配置[16,37]。虽然许多数据包处理任务都可以在这些交换机上编程,但调度不是其中之一。可编程的交换机可以通过提供可编程的入口流水线来调度和整形事务,从而帮助基于先进先出的调度器,而不需要每个先进先出挡路内部都有专用的ATOM流水线。然而,他们仍然需要PIFO来进行可编程调度。

通用分组调度(UPS)。UPS[31]通过寻求一种通用的、可以仿真任何调度算法的单一调度算法,来实现灵活分组调度的目标。理论上,UPS发现,如果预先知道要仿真的调度算法的分组离开时间,那么众所周知的LSTF调度规则[29]是通用的。UPS实践表明,通过适当地初始化SLACK,LSTF可以仿真多种不同的调度目标。LSTF使用PIFO是可编程的,但是用LSTF实际表示的方案集是有限的。例如,LSTF不能表示:

1.分层调度算法,如HPFQ,因为它只使用一个优先级队列。

2.非节省工作的算法。对于这样的算法,LSTF必须预先知道每个数据包的离开时间,这是不切实际的。

3.公平排队中的短期带宽公平性,因为LSTF除了一个优先级队列外不保持任何交换状态。如图1所示,编程公平排队算法要求我们维护一个虚拟的时间状态变量。如果没有这一点,一个新的流可能会有任意的虚拟开始时间,并被无限期地剥夺其公平份额。UPS提供了一个解决方案,要求定期评估公平份额,这在实践中是很难做到的。

4.在交换机上将来自不同点的流聚合为单个流的调度策略。一个例子是跨越视频和网络流量类别的公平排队,而不考虑端点。这样的策略要求交换机保持公平排队所需的状态,因为没有端点看到类内的所有流量。但是,LSTF不能循序渐进地维护和更新交换机状态。

UPS/LSTF中的限制是有限编程模型的结果。UPS假定交换机是固定的,不能对其进行编程以修改数据包字段。此外,它只有一个优先级队列。通过使用ATOM流水线执行调度和整形事务,并将多个PIFO组合在一起,PIFO表达了更广泛的调度算法。

优先级队列的硬件设计。P-堆是可扩展到40亿个条目的APIPELED二进制堆[14,15],但是,每个P-堆支持属于输入队列交换机中的单个10Gbit/s输入端口的流量,并且每个端口都有单独的P-堆实例[14]。这种按端口设计会在共享内存交换机上产生令人望而却步的区域开销,并阻止跨输出端口共享数据缓冲区和二进制堆。相反,在单个P堆上覆盖多个逻辑PIFO并非易事,这将允许跨端口共享P堆。

7. DISCUSSION

分组调度在竞争流之间分配稀缺的链路容量。该分配服务于应用或网络范围目标,例如最大-最小公平性(WFQ)或最小流完成时间(SRPT)。Pastwork显示了交换机支持灵活分配带来的显著性能优势[12、21、32、39]。然而,这些好处仍然没有实现,因为今天没有实施这样的计划的途径。PIFO提供了这条途径。它们表达了许多调度算法,并且可以线速实现。

如何使用可编程调度器仍不清楚,但至少PIFO将网络作为部署资源分配方案的额外表面。传输协议设计人员将不再需要将自己局限于数据中心传输的基于终端主机/边缘的解决方案。作为PIFO立即使用案例的一个示例,可以运行HPFQ进行流量隔离,其中类对应于数据中心中的不同租户,流对应于属于租户的所有源-目标VM对。如今,使用基于终端主机/边缘的解决方案很难提供这种隔离。

展望未来,交换机上的可编程调度器可以简化分组传输。例如,pFabric[12]通过将简单的交换调度器(最短的剩余处理时间)与简单的终端-主机协议(无拥塞控制的线速传输)相耦合来最小化流完成时间。其他传输机制[28、32]利用网络中的公平排队来简化传输并使其更具可预测性。

也就是说,我们目前的设计只是第一步,可以在几个方面进行改进。

1.调度树比直接配置PIFO网格更方便,但它仍然是一个低层次的抽象。是否有更高级别的抽象?

2.既然我们已经证明是可行的,那麽可编程调度在实际中又会如何应用呢?这将涉及对网络运营商进行调查,以了解可编程调度如何使他们受益。反过来,这将为硬件设计中各种参数的设置提供有价值的设计指导。

3.除了几个反例之外,我们还缺乏不能使用PIFO实现的算法的形式化描述。例如,有没有一种简单的、可检查的属性分离算法,可以使用PIFO实现,也可以不使用PIFO实现?在给定算法规格的情况下,我们是否可以自动检查该算法是否可以使用PIFO进行编程?

4.我们目前的设计可扩展到2048个流量。如果在64个端口上均匀分配,我们就可以在每个端口上对32个流进行调度。这允许跨业务聚合的每个端口调度(例如,在服务器内的32个租户之间公平排队),但不允许更细的粒度(例如,5元组)。理想情况下,要以最精细的粒度进行调度,我们的设计将支持60K流:每个数据包一个流的物理限制。我们目前支持到2048年。我们能弥合这个鸿沟吗?

8. CONCLUSION

直到最近,人们还普遍认为最慢的交换机芯片将是固定功能的;可编程设备不可能具有相同的性能。最近对可编程解析器[23]、快速可编程开关流水线[17]以及对它们进行编程的语言[16,40]的研究,再加上最近的多TB/s可编程商用芯片[1,11],表明这种变化可能正在发生。

但到目前为止,对分组调度器进行编程被认为是禁区-部分原因是所需的算法各不相同,而且因为调度器位于时序要求最严格的共享分组缓冲器的核心,人们普遍认为很难找到也可以在快速硬件中实现的有用的描述。

PIFO似乎是一个非常有前途的抽象:它们包含了各种现有的算法,并允许我们展示新的算法。此外,它们可以以适度的芯片面积开销以LineRate实现。

我们相信,最令人兴奋的结果将是许多新的调度器的产生,这些调度器由网络操作员发明,经过迭代和改进,然后根据自己的需要进行部署,研究实验将不再局限于模拟和进度,不再受供应商选择调度算法的限制。那些需要新算法的人可以创建自己的算法,甚至可以从开源报告或可重现的SIGCOMM论文中下载一个。

要做到这一点,我们需要具有可编程PIFO调度器的真正交换芯片。好消息是,我们看不出为什么未来的交换芯片不能包括可编程的PIFO调度器。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

粥粥粥少女的拧发条鸟

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值