写在前面
ns-3 是一个用于互联网系统的离散事件的开源网络模拟器。我基于阿里巴巴的 HPCC 的仿真项目对其源码进行简要学习。所参考的资料见文章末尾 。
一、HPCC 仿真项目基于 ns-3 源码的改动
我们先简单的看一下 HPCC 仿真项目对于 ns-3 源码的改造,方便后续可能将相关模块功能进行移植到高版本的需求。
原作者在项目文档 中也给出了有关说明,这里结合代码来看一下具体的修改。
1.1 Point-to-Point 模块
这部分主要是 RDMA 设备的实现以及各种拥塞控制算法:
qbb-net-device.cc/h
: 实现RDMA网络设备,是HPCC的基础网络设备
rdma-hw.cc/h
: 实现拥塞控制的核心逻辑,包含HPCC、DCQCN、TIMELY等算法
rdma-queue-pair.cc/h
: 实现队列对管理
rdma-driver.cc/h
: 负责分配队列对和管理多个网卡
pause-header.cc/h
: PFC(Priority Flow Control)数据包的头部定义
cn-header.cc/h
: CNP(Congestion Notification Packet)的头部定义
qbb-header.cc/h
: ACK消息的头部定义
pint.cc/h
: PINT(Probabilistic In-band Network Telemetry)的编码/解码算法
switch-node.cc/h
: 交换机节点类
switch-mmu.cc/h
: 交换机内存管理单元
1.2 network 模块
这部分主要实现了交换机端口队列和网络遥测功能:
broadcom-egress-queue.cc/h
: 交换机端口的多队列实现
custom-header.cc/h
: 自定义头部类,用于加速头部解析
int-header.cc/h
: INT(In-band Network Telemetry)的头部定义,是 HPCC 获取精确网络负载信息的关键
1.3 applications 模块
rdma-client.cc/h
: 生成RDMA流量的应用层实现
二、RDMA 的实现
2.1 架构梳理
HPCC 项目中的 RDMA 主要包括以下几个关键组件:
RdmaClient
:应用层组件,负责创建 RDMA 连接请求
RdmaDriver
:驱动层,连接应用和硬件层
RdmaHw
:硬件层,实现 RDMA 核心功能
RdmaQueuePair
:队列对,管理发送和接收队列
QbbNetDevice
:网络设备层,负责数据包的实际发送和接收
对应的代码主要集中在 Point-to-Point 模块和 Application 模块中。
graph TD
A[应用层: RdmaClient] -->|创建QP| B[驱动层: RdmaDriver]
B -->|初始化/管理| C[硬件层: RdmaHw]
C -->|控制| D[队列对: RdmaQueuePair]
C -->|收发数据| E[网络设备: QbbNetDevice]
E -->|数据包传输| F[物理网络]
D -->|数据流控制| E
subgraph 拥塞控制算法
G[DCQCN]
H[HPCC]
I[TIMELY]
J[DCTCP]
end
C -->|使用| G
C -->|使用| H
C -->|使用| I
C -->|使用| J
2.2 RDMA 队列对(Queue Pair)实现
队列对(QP)在代码 simulation/src/point-to-point/model/rdma-queue-pair.h
中通过 RdmaQueuePair
类实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class RdmaQueuePair : public Object {public : Time startTime; Ipv4Address sip, dip; uint16_t sport, dport; uint64_t m_size; uint64_t snd_nxt, snd_una; uint16_t m_pg; uint32_t m_win; DataRate m_rate; DataRate m_max_rate; Time m_nextAvail; };
2.3 RDMA 数据传输流程
整个传输过程可以参考下方时序图:
sequenceDiagram
participant App as RdmaClient
participant Driver as RdmaDriver
participant HW as RdmaHw
participant QP as RdmaQueuePair
participant NIC as QbbNetDevice
participant Network as 网络
App->>Driver: AddQueuePair(size, pg, sip, dip, sport, dport)
Driver->>HW: AddQueuePair(...)
HW->>QP: 创建QP
HW->>NIC: NewQp(qp)
NIC->>HW: GetNxtPacket(qp)
HW->>QP: 获取下一个数据包
HW-->>NIC: 返回数据包
NIC->>Network: 发送数据包
Network-->>NIC: 接收ACK
NIC->>HW: Receive(ACK)
HW->>QP: Acknowledge(seq)
HW->>QP: 更新发送速率
QP-->>HW: IsFinished()
HW->>Driver: QpComplete(qp)
Driver->>App: notifyAppFinish()
2.3.1 连接建立
RDMA 应用层发起连接:
1 2 3 4 5 6 7 8 9 void RdmaClient::StartApplication (void ) { Ptr<Node> node = GetNode (); Ptr<RdmaDriver> rdma = node->GetObject <RdmaDriver>(); rdma->AddQueuePair (m_size, m_pg, m_sip, m_dip, m_sport, m_dport, m_win, m_baseRtt, MakeCallback (&RdmaClient::Finish, this )); }
然后通过驱动层传递到硬件层:
1 2 3 4 void RdmaDriver::AddQueuePair (uint64_t size, uint16_t pg, Ipv4Address sip, Ipv4Address dip, uint16_t sport, uint16_t dport, uint32_t win, uint64_t baseRtt, Callback<void > notifyAppFinish) { m_rdma->AddQueuePair (size, pg, sip, dip, sport, dport, win, baseRtt, notifyAppFinish); }
硬件层创建并初始化队列对:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 void RdmaHw::AddQueuePair (uint64_t size, uint16_t pg, Ipv4Address sip, Ipv4Address dip, uint16_t sport, uint16_t dport, uint32_t win, uint64_t baseRtt, Callback<void > notifyAppFinish) { Ptr<RdmaQueuePair> qp = CreateObject <RdmaQueuePair>(pg, sip, dip, sport, dport); qp->SetSize (size); qp->SetWin (win); qp->SetBaseRtt (baseRtt); qp->SetVarWin (m_var_win); qp->SetAppNotifyCallback (notifyAppFinish); uint32_t nic_idx = GetNicIdxOfQp (qp); m_nic[nic_idx].qpGrp->AddQp (qp); uint64_t key = GetQpKey (dip.Get (), sport, pg); m_qpMap[key] = qp; DataRate m_bps = m_nic[nic_idx].dev->GetDataRate (); qp->m_rate = m_bps; qp->m_max_rate = m_bps; if (m_cc_mode == 3 ){ qp->hp.m_curRate = m_bps; if (m_multipleRate){ for (uint32_t i = 0 ; i < IntHeader::maxHop; i++) qp->hp.hopState[i].Rc = m_bps; } } m_nic[nic_idx].dev->NewQp (qp); }
2.3.2 数据发送
RDMA 硬件的 RdmaHw::GetNxtPacket
方法负责创建数据包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 # simulation/src/point-to-point/model/rdma-hw.cc Ptr<Packet> RdmaHw::GetNxtPacket (Ptr<RdmaQueuePair> qp) { uint32_t payload_size = qp->GetBytesLeft (); if (m_mtu < payload_size) payload_size = m_mtu; Ptr<Packet> p = Create <Packet>(payload_size); SeqTsHeader seqTs; seqTs.SetSeq (qp->snd_nxt); seqTs.SetPG (qp->m_pg); p->AddHeader (seqTs); UdpHeader udpHeader; udpHeader.SetDestinationPort (qp->dport); udpHeader.SetSourcePort (qp->sport); p->AddHeader (udpHeader); Ipv4Header ipHeader; ipHeader.SetSource (qp->sip); ipHeader.SetDestination (qp->dip); ipHeader.SetProtocol (0x11 ); p->AddHeader (ipHeader); PppHeader ppp; ppp.SetProtocol (0x0021 ); p->AddHeader (ppp); qp->snd_nxt += payload_size; qp->m_ipid++; return p; }
2.3.3 数据接收
当接收到数据包时,硬件的处理流程如下,对不同类型的数据包,调用不同的函数进行处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 # simulation/src/point-to-point/model/rdma-hw.cc int RdmaHw::Receive (Ptr<Packet> p, CustomHeader &ch) { if (ch.l3Prot == 0x11 ){ ReceiveUdp (p, ch); }else if (ch.l3Prot == 0xFF ){ ReceiveCnp (p, ch); }else if (ch.l3Prot == 0xFD ){ ReceiveAck (p, ch); }else if (ch.l3Prot == 0xFC ){ ReceiveAck (p, ch); } return 0 ; }
其中,对于普通的 UDP 数据包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # simulation/src/point-to-point/model/rdma-hw.cc int RdmaHw::ReceiveUdp (Ptr<Packet> p, CustomHeader &ch) { uint8_t ecnbits = ch.GetIpv4EcnBits (); Ptr<RdmaRxQueuePair> rxQp = GetRxQp (ch.dip, ch.sip, ch.udp.dport, ch.udp.sport, ch.udp.pg, true ); if (ecnbits != 0 ){ rxQp->m_ecn_source.ecnbits |= ecnbits; rxQp->m_ecn_source.qfb++; } rxQp->m_ecn_source.total++; int x = ReceiverCheckSeq (ch.udp.seq, rxQp, payload_size); if (x == 1 || x == 2 ){ m_nic[nic_idx].dev->RdmaEnqueueHighPrioQ (newp); m_nic[nic_idx].dev->TriggerTransmit (); } return 0 ; }
2.3.4 ACK 的处理流程
当接收到ACK时,接收方的硬件需要更新队列对状态并调整发送速率:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 # simulation/src/point-to-point/model/rdma-hw.cc int RdmaHw::ReceiveAck (Ptr<Packet> p, CustomHeader &ch) { Ptr<RdmaQueuePair> qp = GetQp (ch.sip, port, qIndex); if (!m_backto0){ qp->Acknowledge (seq); }else { uint32_t goback_seq = seq / m_chunk * m_chunk; qp->Acknowledge (goback_seq); } if (qp->IsFinished ()){ QpComplete (qp); } if (ch.l3Prot == 0xFD ) RecoverQueue (qp); if (cnp){ if (m_cc_mode == 1 ){ cnp_received_mlx (qp); } } if (m_cc_mode == 3 ){ HandleAckHp (qp, p, ch); } dev->TriggerTransmit (); return 0 ; }
具体拥塞算法的实现,此处暂略。
2.3.5 RDMA 硬件层与网络设备的交互
这部分比较简单,事实上就是通过回调函数来进行状态的同步更新:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # simulation/src/point-to-point/model/rdma-hw.cc void RdmaHw::Setup (QpCompleteCallback cb) { for (uint32_t i = 0 ; i < m_nic.size (); i++){ Ptr<QbbNetDevice> dev = m_nic[i].dev; if (dev == NULL ) continue ; dev->m_rdmaEQ->m_qpGrp = m_nic[i].qpGrp; dev->m_rdmaReceiveCb = MakeCallback (&RdmaHw::Receive, this ); dev->m_rdmaLinkDownCb = MakeCallback (&RdmaHw::SetLinkDown, this ); dev->m_rdmaPktSent = MakeCallback (&RdmaHw::PktSent, this ); dev->m_rdmaEQ->m_rdmaGetNxtPkt = MakeCallback (&RdmaHw::GetNxtPacket, this ); } m_qpCompleteCallback = cb; }
2.3.6 以 HPCC 拥塞控制算法为例
HPCC 拥塞控制算法的代码实现也在 simulation/src/point-to-point/model/rdma-hw.cc
中,它利用INT(带内网络遥测)获取精确的链路负载信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 void RdmaHw::HandleAckHp (Ptr<RdmaQueuePair> qp, Ptr<Packet> p, CustomHeader &ch) { IntHeader &ih = ch.ack.ih; if (m_fast_react && ih.nhop > 0 ) FastReactHp (qp, p, ch); UpdateRateHp (qp, p, ch, false ); }void RdmaHw::UpdateRateHp (Ptr<RdmaQueuePair> qp, Ptr<Packet> p, CustomHeader &ch, bool fast_react) { IntHeader &ih = ch.ack.ih; bool updated = false ; for (uint32_t i = 0 ; i < ih.nhop; i++){ uint32_t b = ih.hop[i].GetByteCounter (); uint32_t t = ih.hop[i].GetTimeDelta (); double u = b * 8.0 / t; double new_rate = qp->hp.m_curRate.GetBitRate (); if (u > m_targetUtil) { new_rate = new_rate * m_targetUtil / u; } else if (u < m_targetUtil * m_utilHigh) { if (qp->hp.m_incStage < m_miThresh) new_rate = new_rate + m_rai.GetBitRate (); else new_rate = new_rate * (1 + m_rhai); qp->hp.m_incStage++; } DataRate new_rate_dr = DataRate (new_rate); if (new_rate_dr < m_minRate) new_rate_dr = m_minRate; if (new_rate_dr > qp->m_max_rate) new_rate_dr = qp->m_max_rate; ChangeRate (qp, new_rate_dr); updated = true ; } }
整个流程大致如下:
graph TD
subgraph 应用层
A[RdmaClient] -->|1. 创建QP| B[RdmaDriver]
end
subgraph 驱动层
B -->|2. 初始化QP| C[RdmaHw]
end
subgraph 硬件层
C -->|3. 创建QP| D[RdmaQueuePair]
C -->|4. 注册QP| E[QbbNetDevice]
end
subgraph 数据传输
E -->|5. 请求数据包| C
C -->|6. 获取数据| D
C -->|7. 返回数据包| E
E -->|8. 发送数据包| F[网络]
F -->|9. 接收数据包| G[接收方QbbNetDevice]
G -->|10. 解析数据包| H[接收方RdmaHw]
H -->|11. 处理数据包| I[RdmaRxQueuePair]
I -->|12. 生成ACK| G
G -->|13. 发送ACK| F
F -->|14. 接收ACK| E
E -->|15. 处理ACK| C
end
subgraph 拥塞控制
C -->|16. 提取INT信息| J[INT头部]
J -->|17. 计算链路利用率| K[拥塞控制算法]
K -->|18. 更新发送速率| D
end
subgraph 完成处理
D -->|19. 检查完成状态| C
C -->|20. 通知完成| B
B -->|21. 通知应用| A
end
写在后面
本文主要梳理了 RDMA 整体的工作流程,对 RDMA 通信中的几个关键方法进行了注释和分析,大致理清了一些方法的调用关系,仅供参考。
参考资料