高速通信計算研究所

slankdevの報告

DPDKとLinuxカーネルでパケット処理のレイテンシ比較

DPDKを使って開発をするのですが、どれだけ早いのか一応自分で測ってまとめてみました。 今回はrdtscを使ってレイテンシ計測を行います。ただ、DPDKの通信速度を計るだけでなく、 Linuxカーネルで同様の通信をした時の速度と比較をします。

RDTSC とは

RDTSC(Road Time Stamp Counter)とは起動後のCPUクロックをカウントするカウンタで 相対時刻を計測する時などに使用できる。もっとも制度が高いらしい。 c言語からそのままシステムコールなどで呼び出すことができないので、インラインアセンブラで rdtsc命令を呼び出して、それをcの変数に入れてCPUクロック数を計測する。

#include <stdio.h>
#include <stdint.h>
uint64_t rdtsc() 
{
    unsigned int eax, edx;
    /* 詳しくはintel arch manualググればでます */
    __asm__ volatile("rdtsc" : "=a"(eax), "=d"(edx));
    return ((uint64_t)edx << 32) | eax;
}

要は、この関数をパケットのフォワードの前後に入れて、その差分をとれば、 フォワードを初めてから完了するまでに何クロックしたかを知ることができる。 ターボブーストなどをいじっていると、 起動中に勝手にクロック周波数が変わってしまうことがあるので、事前に固定をする必要がある。

linuxカーネルの場合 /sys/devices/system/cpu/cpufreq/policy{CPU番号}/cpuinfo_{cur, max, min}_freq でクロック周波数の現在値、最大値、最小値を知ることができるので、 これらの値を設定してクロック周波数が動的に変更されないように設定してから実験を行う。

実験環境の準備

今回のレイテンシ計測は2台のマシン(Master, Slaveとする)を 直接1本のLANケーブルでつないで行う。Master側でパケットフォワードを行うプログラムを動かし、 Slave側からパケットを送信してその時間を計測する。 MasterにはDPDK-16.04をインストールして有効化している。

MasterとSlavenのマシンスペックを以下に示す。(ScreenFetchの出力結果です)

実験の説明

Master側ではDPDKを使用したフォワードプログラムとLinux KernelのPF_PACKET(以下PF_PACKET) を使用したフォワードプログラムの2つ使い、動作速度の差を比較していく。

Slave側ではPF_PACKETを使用したパケットの送受信プログラムを用意した。 こちらでもDPDKを使って計測する実験は今後やっていこうと思う。

今回のレイテンシ計測の実験は以下の2種類行うこととした。それぞれ、 DPDKとPF_PACKETを使用した方法があるので、全部で4回計測実権を行う。 レイテンシ計測は誤差を少なくするため、106パケットを送信して、それぞれのレイテンシの 平均値を計測するものとする。

Latency Test 1

master側での計測

          Slave    Master 
            |        |
            |------->| 受信後
            |        | 
            |<-------| 送信後
            |        | 
            |        |

Latency Test 2

slave側での計測

          Slave    Master 
            |        |
      送信後 |------->| 
            |        | 
      受信後 |<-------| 
            |        | 
            |        |

使用するプログラム

本実験で使用するプログラムにはLibPGENと筆者が使用する便利関数をまとめたライブラリを使用しているので、 そちらはご了承ください。

slaveで使用するパケット送受信プログラムを以下に示す。

#include <stdio.h>
#include <pgen2.h>
#include <slankdev.h>
#define TEST1 1
const char* dev = "eth0";

void calc_latency(pgen::packet& p, pgen::stream& s, int cnt)
{
    uint64_t total = 0;
    for (int i=0; i<cnt; i++) {
        uint64_t now = rdtsc();
        s << p;
        uint8_t buf[10000];
        size_t recvlen = s.recv(buf, sizeof buf);
        total += rdtsc() - now;
    }
    printf("latency %lu cycles \n", total/cnt);
}

int main()
{
    pgen::arp pack;
    pack.ETH.src.setbydev(dev);
    pack.ETH.dst.setbcast();
    pack.ARP.operation = 1;
    pack.ARP.hwsrc = pack.ETH.src;
    pack.ARP.psrc  = "192.168.1.111";
    pack.ARP.hwdst = pack.ETH.dst;
    pack.ARP.pdst  = "192.168.1.1";
    
    pack.compile();
    pack.hex();
    pgen::net_stream net(dev, pgen::open_mode::netif);

#if TEST1
    for (;;)
        net << pack;
#else
    for (;;)
        calc_latency(pack, net, 1000*1000);
#endif
}

masterで動作させるPF_PACKETを使用したパケットフォワードプログラムを以下に示す。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>

#include <slankdev.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <net/if.h>

#include <netpacket/packet.h>
#include <netinet/if_ether.h>
#include <arpa/inet.h>
#include <sys/socket.h>


static struct {
    uint64_t total_cycles;
    uint64_t total_pkts;
} latency_numbers;

const char* dev = "enp3s0"; // IntelNIC


int main(int argc, char** argv)
{

    intfd fd;
    fd.socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

    struct ifreq ifreq;
    memset(&ifreq, 0, sizeof(ifreq));
    strncpy(ifreq.ifr_name, dev, sizeof(ifreq.ifr_name)-1);
    fd.ioctl(SIOCGIFINDEX, &ifreq);

    struct sockaddr_ll sa;
    sa.sll_family = AF_PACKET;
    sa.sll_protocol = htonl(ETH_P_ALL);
    sa.sll_ifindex = ifreq.ifr_ifindex;
    fd.bind((struct sockaddr*)&sa, sizeof(sa));

    fd.ioctl(SIOCGIFFLAGS, &ifreq);
    ifreq.ifr_flags = ifreq.ifr_flags | IFF_PROMISC;
    fd.ioctl(SIOCSIFFLAGS, &ifreq);

    int count;
    for (count=0; ; count++) {
        
        uint8_t buf[1000];
        size_t res = fd.read(buf, sizeof(buf));
        uint64_t before = rdtsc();

        fd.write(buf, res);
        uint64_t after = rdtsc();
        latency_numbers.total_cycles += after - before;
        latency_numbers.total_pkts   += 1;

        if (latency_numbers.total_pkts > (1000 * 1000ULL)) {
            printf("Latency = %lu cycles\n",
                    latency_numbers.total_cycles / latency_numbers.total_pkts);
            latency_numbers.total_cycles = latency_numbers.total_pkts = 0;
        }

    }
    return 0;
}

Masterで動作させるDPDKを使用したパケットフォワードプログラムを以下に示す。

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>

#include <rte_version.h>
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <rte_ether.h>
#include <rte_cycles.h>
#include <rte_lcore.h>
#include <rte_mbuf.h>

#define RX_RING_SIZE 128
#define TX_RING_SIZE 512

#define NUM_MBUFS       8191
#define MBUF_CACHE_SIZE 250
#define BURST_SIZE      32


static const struct rte_eth_conf port_conf_default = {
    .rxmode = { .max_rx_pkt_len = ETHER_MAX_LEN }
};

static struct {
    uint64_t total_cycles;
    uint64_t total_pkts;
} latency_numbers;


static uint16_t add_timestamps(uint8_t port __rte_unused, uint16_t qidx __rte_unused,
        struct rte_mbuf **pkts, uint16_t nb_pkts,
        uint16_t max_pkts __rte_unused, void *_ __rte_unused)
{
    unsigned i;
    uint64_t now = rte_rdtsc();

    for (i = 0; i < nb_pkts; i++) {
        pkts[i]->udata64 = now;
    }
    return nb_pkts;
}

static uint16_t calc_latency(uint8_t port __rte_unused, uint16_t qidx __rte_unused,
        struct rte_mbuf **pkts, uint16_t nb_pkts, void *_ __rte_unused)
{
    uint64_t cycles = 0;
    uint64_t now = rte_rdtsc();
    unsigned i;

    for (i = 0; i < nb_pkts; i++) {
        cycles += now - pkts[i]->udata64;
    }
    latency_numbers.total_cycles += cycles;
    latency_numbers.total_pkts += nb_pkts;

    if (latency_numbers.total_pkts > (1000 * 1000ULL)) {
        printf("Latency = %lu cycles\n",
                latency_numbers.total_cycles / latency_numbers.total_pkts);
        latency_numbers.total_cycles = latency_numbers.total_pkts = 0;
    }
    return nb_pkts;
}


static __attribute((noreturn)) void lcore_main(void)
{
    const uint8_t num_ports = rte_eth_dev_count();

    uint8_t port;
    for (port = 0; port < num_ports; port++) {
        if (rte_eth_dev_socket_id(port) > 0 && rte_eth_dev_socket_id(port) != (int)rte_socket_id())
            printf("WARNING: port %u is on remote NUMA node to "
                    "polling thread. \n\tPerformance will "
                    "not be optimal. \n ", port);
    }
    printf("\nCore %u forwarding packets. [Ctrl+C to quit]\n", rte_lcore_id());


    for (;;) {
        struct rte_mbuf* bufs[BURST_SIZE];
        for (port=0; port<num_ports; port++) {
            const uint16_t num_rx = rte_eth_rx_burst(port, 0, bufs, BURST_SIZE);

            if (unlikely(num_rx == 0))
                continue;

            const uint16_t num_tx = rte_eth_tx_burst(port, 0, bufs, num_rx);

            if (unlikely(num_tx < num_rx)) {
                uint16_t buf;
                for (buf=num_tx; buf<num_rx; buf++)
                    rte_pktmbuf_free(bufs[buf]);
            }
        }
    }
}


static int port_init(uint8_t port, struct rte_mempool* mbuf_pool)
{

    struct rte_eth_conf port_conf = port_conf_default;
    int ret;
    uint16_t q;

    if (port >= rte_eth_dev_count())
        return -1;

    const uint16_t rx_rings = 1;
    const uint16_t tx_rings = 1;
    ret = rte_eth_dev_configure(port, rx_rings, tx_rings, &port_conf);
    if (ret != 0)
        return ret;

    for (q=0; q < rx_rings; q++) {
        ret = rte_eth_rx_queue_setup(port, q, RX_RING_SIZE, 
                rte_eth_dev_socket_id(port), NULL, mbuf_pool);
        if (ret < 0)
            return ret;
    }

    for (q=0; q < tx_rings; q++) {
        ret = rte_eth_tx_queue_setup(port, q, TX_RING_SIZE, 
                rte_eth_dev_socket_id(port), NULL);
        if (ret < 0)
            return ret;
    }

    ret = rte_eth_dev_start(port);
    if (ret < 0)
        return ret;

    struct ether_addr addr;
    rte_eth_macaddr_get(port, &addr);
    printf("Port %u MAC: %02" PRIx8 " %02" PRIx8 " %02" PRIx8
            " %02" PRIx8 " %02" PRIx8 " %02" PRIx8 "\n",
            (unsigned)port,
            addr.addr_bytes[0], addr.addr_bytes[1],
            addr.addr_bytes[2], addr.addr_bytes[2],
            addr.addr_bytes[3], addr.addr_bytes[4]);

    rte_eth_promiscuous_enable(port);
    rte_eth_add_rx_callback(port, 0, add_timestamps, NULL);
    rte_eth_add_tx_callback(port, 0, calc_latency, NULL);
    return 0;
}


int main(int argc, char** argv)
{
    int ret = rte_eal_init(argc, argv);
    if (ret < 0)
        rte_exit(EXIT_FAILURE, "rte_eal_init() failed\n");

    uint32_t num_ports = rte_eth_dev_count();
    printf("%d ports found  \n", num_ports);
    if (num_ports < 1)
        rte_exit(EXIT_FAILURE, "rte_eth_dev_count()");
    
    struct rte_mempool* mbuf_pool = rte_pktmbuf_pool_create(
            "MBUF_POOL_SLANK", NUM_MBUFS * num_ports, 
            MBUF_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());  

    if (mbuf_pool == NULL)
        rte_exit(EXIT_FAILURE, "rtembuf_pool_create()");

    uint8_t portid;
    for (portid=0; portid < num_ports; portid++) {
        if (port_init(portid, mbuf_pool) != 0)
            rte_exit(EXIT_FAILURE, "Cannot init port %"PRIu8 "\n", portid);
    }

    if (rte_lcore_count() > 1) 
        printf("WARNING: Too many lcores enabled. Only 1 used. \n");

    lcore_main();
    return 0;
}

実験結果

先ほど説明した方法で4つのテストを行った。 それぞれ何回ずつか実権を行ってだいたい有意そうな値を以下に示す。

Index 計測場所 データプレーン レイテンシ速度
Latency Test 1.1 Master [DPDK使用] 30 [clock/packet]
Latency Test 1.2 Master [PF_PACKET使用] 3000 [clock/packet]
Latency Test 2.1 Slave [DPDK使用] 4500 [clock/packet]
Latency Test 2.2 Slave [PF_PACKET使用] 4500 [clock/packet]

Latency Test 1は1000000回パケットを送受信するのにそこまで時間がかからなかったが、 Latency Test 2はTest1と比べ、すこし時間がかかった。Slave側のデータプレーンは Linux Kernelなため、ある程度パケットをとりきれていない。 (DPDKのエコーの場合早すぎて、ドロップしている?)

Test2はlinux kernelでパケットの送受信をしていることもあり、DPDKレベルの速度は 記録できなかった。

Test2はパケットを送信して、受信するまでの全てを、kernelに頼っているため、 高速通信の動作を感じ取ることができなかった。 Test1のパケットフォワードは完全にDPDKとKernelのレイテンシの差が現れているので、 今回はこれについて少し考えてみることにした。

通信速度に対する考察

パケットをフォワードするのにKernelとDPDKはそれぞれ3000クロックと30クロックが必要であることがわかった。 CPUの動作周波数は今回は約3GHzなので、1パケットを64byte(ショートパケットとして考え)これを計算すると。

DPDKの場合
1 packet = 30 clock

CPUが3GHzとすると、30 clock = 1 / 108 sec なので 1 sec = 108 packet 送信できる

1 packet = 64Byteとすると、
1 sec = 50 Gbit 送信可能 -> 50Gbps

Kernelの場合
1 packet = 3000 clockなので DPDKの100倍 -> 0.5Gbps

カーネルを通してパケット処理をしてしまうと、とてもじゃないが、ギガビットイーサネットには ならなさそう。。「GB Ethernetって大変なんです。」 by @herumi.

パケットの送受信だけのレイテンシを固定値とすればネットワーク機器のパケット処理部分のレイテンシを測れば、 その機器の総合の理論値をしらべることができるので、意識して開発していきたい。

今回に関しては、パケットを受信してそのまま送信するコードのレイテンシなため、このような数値になったが、 本来のアプリケーションの場合では、パケットのルーテイングやいろいろな処理も含まれているので、 もっともっとレイテンシがかかることが予想できる。

やっぱりこうなると、DPDKどうしの通信を行ってみようと思う。今回はずいぶん時間がかかったので、 また後日やってみようと思う。