公有云网络问题,是一个老生常谈的事情了。今天主要简单说明下,在内网环境下去分析 & 定位这个问题。也并不是一开始就需要 eBPF 加持的,可以从简单的基础工具到监控配合去相关比较快速的定位问题,解决问题。下面会从发现问题到一步步配合使用 Or 开发一些简单的东西去解决问题的大致思路。
常见的网络相关问题
- 业务负载不均衡:业务系统会越发的复杂,不可能单个服务承接所有流量,一定会逐步迭代为一组服务承接流量,那我们是可以先用一些负载均衡组件去解决这个问题(不推荐一上来就喊,我要自研,图啥呢?),可以使用 HaProxy、Nginx 配合公有云的弹性能力去解决问题。伴随着业务的发展慢慢的当全站使用 K8s 的时候,还会遇到 Kube-Proxy 存在负载不均衡的问题,这个时候就可以应入 Envoy 到最终上线 Istio 去解决问题,做到内部服务的负载均衡(当然服务的健康检查 & 优雅停止需要考虑下)。配合边缘公有云 LB 做入流量控制,到 Kong 这种非常自动化的网关来解决业务整个边缘路由的分流能力。至此 xLB + Kong/Openresty/Apisix/Envoy + Istio(Evnoy) 基本上已经到了天花板了。(应该还有更优的)
- 网络异常抖动:日常会发生抖动的情况大致有几种情况吧。服务间异常、交换机过载、存储系统异常、业务系统 Bug,如上问题都需要专业的同学去帮助解决,并不能也不是每次都是网工的锅是吧。
- 网络延时增大:抛开业务系统问题,Slow Query、Big Key、Hot Key、消费生产(消息队列)延时等问题的话。那只能朝着网络层面去找问题了。(你想,交换机里面也有 CPU)举一个例子,家里 Wifi 为什么没啥人用也会卡,有一个东西叫信道,就好比虽然有很多信道但是呢流量都在一个信道里面跑(1个人干活,一群人看)能不卡吗?(应该能解的,这个就不懂了,不懂不捣乱)
如何定位
Istio
- Istio 监控指标里面其实已经可以发现服务链路之间的调用的情况了,就是会发现为什么有些快有些慢呢,在排除 xDS(服务发现、路由下发等,Envoy API 与 Pilot 交互) 的问题后(其实没几家公司可以触发 Istio 的 Bug 的),依然慢的莫名其妙。
Ping
- 这个是一个好东西哦,简单看看绝对富裕了。闲来无事基于 ping_exporter 加了一个 RESTful API,这样想 Ping 哪里 Ping 哪里,如果需要 K8s 里面互相 Ping 的话,其实只要依靠 K8s API 做自发现 Node 互 Ping 就能一目了然的发现很多问题了(原来网络真的很糟糕)
eBPF
-
eBPF(extended Berkeley Packet Filter) 是一种可以在 Linux 内核中运行用户编写的程序,而不需要修改内核代码或加载内核模块的技术。简单说,eBPF 让 Linux 内核变得可编程化了。eBPF 程序是一个事件驱动模型。Linux 内核提供了各种 hook point,比如 system calls, function entry/exit, kernel tracepoints, network events 等。eBPF 程序通过实现想要关注的 hook point 的 callback,并把这些 callback 注册到相应的 hook point 来完成“内核编程”。
bcc 是一个基于 eBPF 的工具包,内部包装了 LLVM。你可以用 C 语言 + Python、Lua 等高级语言编写 eBPF 程序,同时还提供了不少有用的工具。BCC是一个用于跟踪内核和操作程序的工具集,其软件包中包含了一些有用的工具和例子,它扩展了BPF(Berkeley Packet Filters),通常被称为eBPF , 在Linux3.15中首次引入,但大多数BCC的功能需要Libux4.1及以上版本。(可以话时间研究下下的,基础功能 & BPF 代码里面多有)
bpftrace 是一门基于 eBPF 的高级追踪语言,可以方便快捷地编写 Linux 追踪程序。
-
工作原理:eBPF 通过 C 语言自由扩展(这些扩展通过 LLVM 转换为 BPF 字节码后,加载到内核中执行。从图中你可以看到,eBPF 的执行需要三步:从用户跟踪程序生成 BPF 字节码;加载到内核中运行;向用户空间输出结果。
实际上,在 eBPF 执行过程中,编译、加载还有 maps 等操作,对所有的跟踪程序来说都是通用的。把这些过程通过 Python 抽象起来,也就诞生了 BCC(BPF Compiler Collection)。
BCC 把 eBPF 中的各种事件源(比如 kprobe、uprobe、tracepoint 等)和数据操作(称为 Maps),也都转换成了 Python 接口(也支持 lua)。这样,使用 BCC 进行动态追踪时,编写简单的脚本就可以了。
不过要注意,因为需要跟内核中的数据结构交互,真正核心的事件处理逻辑,还是需要我们用 C 语言来编写。
cloudflare/ebpf_exporter
- Prometheus exporter for custom eBPF metrics. 非常棒的一个项目,在与 K8s 结合的时候,以 DaemonSet 的形式运行在集群中,可借鉴 kube-ebpf-exporter。
- 依据 Bcc Tools tcpconnlat,配合 ebpf_exporter 的改写如下(ebpf_exporter 例子中没有网络延时相关的)
programs:
# See:
# * https://github.com/iovisor/bcc/blob/master/tools/tcpconnect.py
# * https://github.com/iovisor/bcc/blob/master/tools/tcpconnlat_example.txt
- name: tcpconnlat
metrics:
counters:
- name: tcpconnlat_ipv4
help: tcp4 connection latency(ms)
table: ipv4_events
labels:
- name: pid
size: 4
decoders:
- name: uint
- name: sst_addr
size: 4
decoders:
- name: inet_ip
- name: dst_addr
size: 4
decoders:
- name: inet_ip
- name: lst_port
size: 2
decoders:
- name: uint
- name: dst_port
size: 2
decoders:
- name: uint
- name: proc_name
size: 16
decoders:
- name: string
kprobes:
tcp_rcv_state_process: trace_tcp_rcv_state_process
tcp_v4_connect: trace_connect
code: |
#include
#include
#include
#include
struct info_t {
u64 ts;
u32 pid;
char task[TASK_COMM_LEN];
};
BPF_HASH(start, struct sock *, struct info_t);
// separate data structs for ipv4
struct ipv4_data_t {
u32 pid;
u32 saddr;
u32 daddr;
u16 lport;
u16 dport;
char task[TASK_COMM_LEN];
};
BPF_HASH(ipv4_events, struct ipv4_data_t);
int trace_connect(struct pt_regs *ctx, struct sock *sk)
{
u32 pid = bpf_get_current_pid_tgid();
struct info_t info = {.pid = pid};
info.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&info.task, sizeof(info.task));
start.update(&sk, &info);
return 0;
};
// See tcp_v4_do_rcv(). So TCP_ESTBALISHED and TCP_LISTEN
// are fast path and processed elsewhere, and leftovers are processed by
// tcp_rcv_state_process(). We can trace this for handshake completion.
// This should all be switched to static tracepoints when available.
int trace_tcp_rcv_state_process(struct pt_regs *ctx, struct sock *skp)
{
// will be in TCP_SYN_SENT for handshake
if (skp->__sk_common.skc_state != TCP_SYN_SENT)
return 0;
// check start and calculate delta
struct info_t *infop = start.lookup(&skp);
if (infop == 0) {
return 0; // missed entry or filtered
}
u64 ts = infop->ts;
u64 now = bpf_ktime_get_ns();
u64 delta_us = (now - ts) / 1000ul;
u64 delta_ms = delta_us / 1000;
#ifdef MIN_LATENCY
if ( delta_us < DURATION_US ) {
return 0; // connect latency is below latency filter minimum
}
#endif
// pull in details
u16 family = 0, lport = 0, dport = 0;
family = skp->__sk_common.skc_family;
lport = skp->__sk_common.skc_num;
dport = skp->__sk_common.skc_dport;
// emit to appropriate data path
if (family == AF_INET) {
struct ipv4_data_t data4 = {.pid = infop->pid};
data4.saddr = skp->__sk_common.skc_rcv_saddr;
data4.daddr = skp->__sk_common.skc_daddr;
data4.lport = lport;
data4.dport = ntohs(dport);
__builtin_memcpy(&data4.task, infop->task, sizeof(data4.task));
ipv4_events.increment(data4, delta_ms);
}
start.delete(&skp);
return 0;
}
基于上面一轮干下来,基本上哪里慢,为什么慢,怎么慢也就差不多定位了。剩下么,该干嘛干嘛。记得调用链还是非常有用的哦,配合网络监测 + 调用链,问题自然而然的就暴露啦(当然调用链接入姿势是另外一个问题啦)
最后推荐几本书吧,BPF之巅:洞悉Linux系统和应用性能 & Linux内核观测技术BPF(狄老出品)