本文介绍使用 BPF
统计网络流量。网络流量是云产品的重要计费指标,服务器每秒可以处理上百万的数据包,这也要求有高效的方法来统计流量,而 BPF
最初作为网络包处理的技术,被设计和构造成可以支持这个速率的流量处理。
使用 libpcap
在 BPF
之前的时代,我们可以使用 libpcap
实现与 tcpdump
类似的方式,捕获网络流量并拷贝到用户程序中进行统计。
下面是一个简单的示例:
代码语言:txt复制#define ETHERNET_HEADER_LEN 14
#define MIN_IP_HEADER_LEN 20
#define IP_HL(ip) (((ip)->ihl) & 0x0f)
const char* dev = "br-7e20abc6df31";
const char* filter_expr = "src net 172.20.0.0/16";
int64_t traffic = 0;
// Reference:
// https://www.tcpdump.org/manpages/
// https://tools.ietf.org/html/rfc791
void traffic_stat() {
// find the IPv4 network number and netmask for a device
bpf_u_int32 net = 0;
bpf_u_int32 mask = 0;
char errbuf[PCAP_ERRBUF_SIZE] = {0};
int ret = pcap_lookupnet(dev, &net, &mask, errbuf);
if (ret == PCAP_ERROR) {
fprintf(stderr, "pcap_lookupnet failed: %sn", errbuf);
exit(EXIT_FAILURE);
}
// open a device for capturing
int promisc = 1;
int timeout = 1000; // in milliseconds
const int SNAP_LEN = 64;
auto handle = pcap_open_live(dev, SNAP_LEN, promisc, timeout, errbuf);
if (!handle) {
fprintf(stderr, "pcap_open_live failed: %sn", errbuf);
exit(EXIT_FAILURE);
}
// compile a filter expression
struct bpf_program fp;
int optimize = 1;
ret = pcap_compile(handle, &fp, filter_expr, optimize, net);
if (ret == PCAP_ERROR) {
fprintf(stderr, "pcap_compile failed: %sn", pcap_geterr(handle));
exit(EXIT_FAILURE);
}
// set the filter
ret = pcap_setfilter(handle, &fp);
if (ret == PCAP_ERROR) {
fprintf(stderr, "pcap_setfilter failed: %sn", pcap_geterr(handle));
exit(EXIT_FAILURE);
}
// process packets from a live capture
int packet_count = -1; // -1 means infinity
pcap_loop(handle, packet_count, [](u_char* args, const struct pcap_pkthdr* header, const u_char* bytes) {
auto ip_header = reinterpret_cast<iphdr*>(const_cast<u_char*>(bytes) ETHERNET_HEADER_LEN);
const int ip_header_len = IP_HL(ip_header) * 4;
if (ip_header_len < MIN_IP_HEADER_LEN) {
return;
}
auto len = ntohs(ip_header->tot_len);
if (len <= 0) {
return;
}
auto traffic = reinterpret_cast<int64_t*>(args);
*traffic = len;
}, reinterpret_cast<u_char*>(&traffic));
// free a BPF program
pcap_freecode(&fp);
// close the capture device
pcap_close(handle);
}
br-7e20abc6df31
是使用 Docker
创建的网桥,IP 是 172.20.0.1
,使用 src net 172.20.0.0/16
表达式过滤出站流量。
这个程序可以做正确的事情,即统计流量。问题在于,它需要把所有流经网卡的流量都拷贝到用户程序,然后在进行统计,而这些拷贝随后就被丢弃,浪费了大量系统资源。
大杀器 BPF
而 BPF
显然是这个问题的完美解决方案。我们需要的只是累计出站流量,如果能放到内核态执行,意味着我们不需要拷贝网络数据包。
下面是一个使用 libbpf
编写的程序,它的作用与上述 libpcap
的作用一致:
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
int ifindex = 0;
__u64 traffic = 0;
SEC("tp_btf/netif_receive_skb")
int BPF_PROG(netif_receive_skb, struct sk_buff *skb)
{
if (skb->dev->ifindex == ifindex) {
traffic = skb->data_len;
}
return 0;
}
char LICENSE[] SEC("license") = "GPL";
这里,ifindex
即上述网络设备 br-7e20abc6df31
的接口索引。
在这个程序中,我们把它挂载在了 netif_receive_skb
这个 tracepoint
上,网卡接收到数据包后,我们判断是否是目的设备,若是,则累加流量。没有数据拷贝,没有上下文切换,简单高效。
实战
使用 dd
创建一个 512M 大小的文件:
$ dd if=/dev/zero of=/tmp/testfile bs=4096 count=131072
创建一个新的网络设备:
代码语言:txt复制$ sudo docker network create my-tc-net
使用上面创建的网络运行一个 nginx
服务器,提供文件下载:
$ sudo docker run -d --rm
-p 10086:80
-v /tmp/testfile:/home/data/testfile
-v $(PWD)/default.conf:/etc/nginx/conf.d/default.conf
--name my-nginx
--network my-tc-net
nginx:alpine
这是我们的服务器配置:
代码语言:txt复制server {
listen 80;
listen [::]:80;
server_name localhost;
location /downloads/ {
alias /home/data/;
}
}
运行上述的流量统计程序:
代码语言:txt复制$ sudo ./trafficstat
下载文件:
代码语言:txt复制$ curl http://localhost:10086/downloads/testfile --output testfile
查看流量统计输出:
代码语言:txt复制$ sudo ./trafficstat
...
traffic in bytes: 536878816
...
符合预期,大功告成。本文源代码可在这里找到。
结论
本文通过实例演示了使用 libbpf
编写 BPF
程序,实现在内核态高效统计网络流量的方案。