WEBKT

用 eBPF 追踪 Node.js 网络请求:揪出性能瓶颈,优化网络配置

65 0 0 0

用 eBPF 追踪 Node.js 网络请求:揪出性能瓶颈,优化网络配置

作为一名 Node.js 开发者,你是否曾遇到过以下困扰?

  • 线上 Node.js 应用的网络延迟突然增高,用户体验直线下降,却苦于找不到根源?
  • 怀疑 Node.js 应用存在网络丢包问题,但缺乏有效的手段进行验证和诊断?
  • 想深入了解 Node.js 应用的网络通信细节,却被复杂的 TCP/IP 协议栈和内核机制所困扰?

如果你的答案是肯定的,那么 eBPF (extended Berkeley Packet Filter) 将会是你的秘密武器!

什么是 eBPF?

eBPF 最初是为网络数据包过滤而设计的,但现在已经发展成为一个强大的、通用的内核态虚拟机。它允许你在内核中安全地运行自定义代码,而无需修改内核源码或加载内核模块。这为性能分析、安全监控、网络调试等领域带来了无限可能。

为什么选择 eBPF 追踪 Node.js 网络请求?

相较于传统的用户态追踪工具(如 tcpdumpWireshark)和侵入式性能分析方法(如 APM 工具),eBPF 具有以下优势:

  • 低开销:eBPF 程序运行在内核态,可以高效地访问内核数据,减少用户态和内核态之间的数据拷贝和上下文切换,从而降低性能开销。
  • 高精度:eBPF 可以精确地追踪到每个网络数据包的发送和接收时间,以及内核中的各种事件,提供高精度的时间戳和上下文信息。
  • 灵活性:你可以使用 eBPF 编写自定义的追踪逻辑,根据自己的需求收集和分析网络数据,实现高度定制化的性能分析。
  • 安全性:eBPF 程序在运行前会经过内核的验证器进行安全检查,确保不会崩溃内核或访问非法内存,从而保证系统的稳定性。

准备工作

在开始之前,你需要确保你的系统满足以下条件:

  • Linux 内核版本 >= 4.14:这是 eBPF 功能的基本要求。你可以使用 uname -r 命令查看内核版本。
  • 安装 bcc 工具:bcc (BPF Compiler Collection) 是一个用于创建 eBPF 程序的工具包,提供了 Python 绑定和各种示例程序。你可以参考 bcc 的官方文档 (https://github.com/iovisor/bcc) 安装 bcc 工具。
  • Node.js 环境:你需要安装 Node.js 和 npm 包管理器。

实战:使用 eBPF 追踪 Node.js 网络请求

接下来,我们将通过一个实际的例子,演示如何使用 eBPF 追踪 Node.js 应用的网络请求,并分析网络延迟和丢包率。

1. 编写 eBPF 程序

首先,我们需要编写一个 eBPF 程序,用于抓取 Node.js 应用的网络数据包,并记录相关信息。以下是一个简单的 eBPF 程序示例,使用 Python 和 bcc 编写:

#!/usr/bin/env python
from bcc import BPF
import socket
import struct
# 定义 eBPF 程序
program = '''
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <net/inet_sock.h>
#include <linux/tcp.h>
struct data_t {
u32 pid;
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
u64 ts;
};
BPF_PERF_OUTPUT(events);
int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.saddr = sk->__sk_common.skc_rcv_saddr;
data.daddr = sk->__sk_common.skc_daddr;
data.sport = sk->__sk_common.skc_num;
data.dport = sk->__sk_common.skc_dport;
data.ts = bpf_ktime_get_ns();
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
'''
# 加载 eBPF 程序
bpf = BPF(text=program)
# 定义事件处理函数
def print_event(cpu, data, size):
event = bpf['events'].event(data)
print("%-6d %-16s %-16s %-6d %-6d %-14d" % (event.pid, socket.inet_ntoa(struct.pack('I', event.saddr)),
socket.inet_ntoa(struct.pack('I', event.daddr)), event.sport, event.dport, event.ts))
# 打印表头
print("PID SADDR DADDR SPORT DPORT TIMESTAMP")
# 绑定事件处理函数
bpf['events'].open_perf_buffer(print_event)
# 循环读取事件
while True:
try:
bpf.perf_buffer_poll()
except KeyboardInterrupt:
exit()

这个 eBPF 程序使用 kprobe 探针技术,在 tcp_v4_connect 函数被调用时触发,该函数是 TCP 连接建立的关键函数。程序会提取连接的 PID、源 IP 地址、目标 IP 地址、源端口、目标端口和时间戳等信息,并通过 perf_output 将数据发送到用户态。

2. 运行 eBPF 程序

将上述代码保存为 tcp_connect.py 文件,并使用 root 权限运行:

sudo python tcp_connect.py

3. 启动 Node.js 应用

接下来,启动你想要追踪的 Node.js 应用。为了方便演示,我们可以使用一个简单的 HTTP 服务器:

const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello, World!\n');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});

将上述代码保存为 server.js 文件,并使用 Node.js 运行:

node server.js

4. 发起网络请求

使用 curl 命令向 Node.js 应用发起网络请求:

curl http://127.0.0.1:3000

5. 分析 eBPF 输出

此时,你应该能在 tcp_connect.py 的输出中看到类似以下的信息:

PID SADDR DADDR SPORT DPORT TIMESTAMP
3456 127.0.0.1 127.0.0.1 54321 3000 1678886400123456

这些信息包含了连接的 PID、源 IP 地址、目标 IP 地址、源端口、目标端口和时间戳。你可以使用这些信息来分析 Node.js 应用的网络请求延迟。

进阶:分析网络延迟和丢包率

上面的例子只是一个简单的演示,你可以根据自己的需求扩展 eBPF 程序,收集更多信息,并进行更深入的分析。

以下是一些可以扩展的方向:

  • 追踪 TCP 连接的建立、数据传输和关闭过程:可以使用 kprobe 探针技术,在 tcp_v4_connecttcp_sendmsgtcp_close 等函数被调用时触发,记录连接的状态变化和数据传输情况。
  • 计算网络延迟:可以在 tcp_sendmsgtcp_recvmsg 函数被调用时,记录发送和接收的时间戳,并计算时间差,从而得到网络延迟。
  • 检测网络丢包:可以使用 tracepoint 探针技术,在 tcp:tcp_retransmit_skb 事件发生时触发,该事件表示 TCP 数据包被重传,通常是由于网络丢包引起的。
  • 分析 TCP 拥塞控制算法:可以使用 kprobe 探针技术,在 TCP 拥塞控制算法相关的函数被调用时触发,例如 tcp_ Reno_congestion_controltcp_cubic_congestion_control 等,了解 TCP 连接的拥塞状态和拥塞控制行为。

示例:使用 eBPF 计算网络延迟

以下是一个使用 eBPF 计算网络延迟的示例程序:

#!/usr/bin/env python
from bcc import BPF
import socket
import struct
# 定义 eBPF 程序
program = '''
#include <uapi/linux/ptrace.h>
#include <net/sock.h>
#include <net/inet_sock.h>
#include <linux/tcp.h>
struct data_t {
u32 pid;
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
u64 tx_ts;
u64 rx_ts;
};
BPF_HASH(start, u64, struct data_t);
BPF_PERF_OUTPUT(events);
int kprobe__tcp_sendmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) {
u64 key = ((u64)sk) << 32 | bpf_get_current_pid_tgid();
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.saddr = sk->__sk_common.skc_rcv_saddr;
data.daddr = sk->__sk_common.skc_daddr;
data.sport = sk->__sk_common.skc_num;
data.dport = sk->__sk_common.skc_dport;
data.tx_ts = bpf_ktime_get_ns();
start.update(&key, &data);
return 0;
}
int kprobe__tcp_recvmsg(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size, int flags) {
u64 key = ((u64)sk) << 32 | bpf_get_current_pid_tgid();
struct data_t *data = start.lookup(&key);
if (data == NULL) {
return 0;
}
u64 rx_ts = bpf_ktime_get_ns();
u64 latency = rx_ts - data->tx_ts;
data->rx_ts = rx_ts;
events.perf_submit(ctx, data, sizeof(struct data_t));
start.delete(&key);
return 0;
}
'''
# 加载 eBPF 程序
bpf = BPF(text=program)
# 定义事件处理函数
def print_event(cpu, data, size):
event = bpf['events'].event(data)
latency = (event.rx_ts - event.tx_ts) / 1000000.0 # 转换为毫秒
print("%-6d %-16s %-16s %-6d %-6d %-10.2f ms" % (event.pid, socket.inet_ntoa(struct.pack('I', event.saddr)),
socket.inet_ntoa(struct.pack('I', event.daddr)), event.sport, event.dport, latency))
# 打印表头
print("PID SADDR DADDR SPORT DPORT LATENCY")
# 绑定事件处理函数
bpf['events'].open_perf_buffer(print_event)
# 循环读取事件
while True:
try:
bpf.perf_buffer_poll()
except KeyboardInterrupt:
exit()

这个程序使用 kprobe 探针技术,在 tcp_sendmsgtcp_recvmsg 函数被调用时触发。tcp_sendmsg 函数被调用时,程序会记录发送的时间戳和连接信息,并存储在一个 BPF 哈希表中。tcp_recvmsg 函数被调用时,程序会从哈希表中查找对应的发送时间戳,计算网络延迟,并通过 perf_output 将数据发送到用户态。

注意事项

  • eBPF 程序的安全性:eBPF 程序运行在内核态,需要经过内核的验证器进行安全检查。编写 eBPF 程序时,要特别注意程序的安全性,避免崩溃内核或访问非法内存。
  • eBPF 程序的性能:eBPF 程序虽然具有低开销的优势,但如果编写不当,也可能对系统性能产生影响。编写 eBPF 程序时,要尽量减少不必要的操作,避免循环和复杂的计算。
  • 内核版本的兼容性:不同的内核版本可能支持不同的 eBPF 功能和 API。编写 eBPF 程序时,要考虑内核版本的兼容性,避免使用过新的功能或 API。

总结

eBPF 是一个强大的工具,可以用于追踪 Node.js 应用的网络请求,分析网络延迟和丢包率,并优化网络配置。通过本文的介绍,你应该已经掌握了使用 eBPF 追踪 Node.js 网络请求的基本方法。希望你能将 eBPF 应用到实际工作中,解决 Node.js 应用的网络性能问题。

更进一步

  • 探索更多的 eBPF 工具:除了 bcc,还有其他的 eBPF 工具,例如 bpftrace、ply 等。这些工具提供了不同的编程接口和功能,可以满足不同的需求。
  • 深入学习 eBPF 原理:了解 eBPF 的底层原理,可以帮助你更好地理解 eBPF 的工作方式,并编写更高效和安全的 eBPF 程序。
  • 参与 eBPF 社区:eBPF 是一个活跃的开源社区,你可以参与社区讨论,分享你的经验和知识,并为 eBPF 的发展做出贡献。

通过 eBPF,你可以更深入地了解 Node.js 应用的网络行为,从而更好地优化应用性能,提升用户体验。 祝你使用 eBPF 顺利!

网络侦探柯南 eBPFNode.js网络性能

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/9437