编程 eBPF 可观测性实战:Linux 内核级追踪与性能分析的完全指南(2026版)

2026-05-19 08:27:11 +0800 CST views 12

eBPF 可观测性实战:Linux 内核级追踪与性能分析的完全指南(2026版)

当你的服务出现偶发性延迟毛刺,Prometheus 看不到,日志里也没有;当内核态 CPU 飙升,perf 告诉你的是函数名,却给不出调用链——eBPF 是你最后也是最强的武器。

一、为什么 eBPF 是近十年 Linux 最重大的可观测性革命

2014 年之前,想在 Linux 内核里"看一眼"运行时状态,你的选择只有三个:修改内核源码重新编译(不可行)、写内核模块(crash 风险极高)、用 ptrace/strace 附加到进程(性能开销毁灭性)。

eBPF(extended Berkeley Packet Filter)改变了这一切。它本质是一个运行在 Linux 内核中的沙箱虚拟机,允许你在不修改内核源码、不加载内核模块的前提下,向内核注入一小段经过严格验证的字节码程序,在内核事件触发时执行,并将结果通过共享的 eBPF Map 传回用户空间。

这项技术最初是为网络包过滤设计的(经典 BPF,即 tcpdump 背后的机制),但 eBPF 将其扩展成了一个通用内核虚拟机,支持:

  • 内核任意函数埋点(kprobe)
  • 用户态任意函数埋点(uprobe)
  • 系统调用追踪(tracepoint)
  • 网络数据包处理(XDP)
  • 内核调度器扩展(sched_ext,2024+)
  • LSM 安全策略(KRSI)

最关键的:Verifier(验证器)保证你注入的程序不会让内核崩溃,哪怕程序有 bug,也只会被拒绝加载或在运行时被终止,不会影响内核稳定性。

1.1 与传统可观测性工具的本质区别

工具数据来源开销深度局限性
Prometheus + Node Exporter/proc /sys 采样进程级、系统级指标看不到内核内部、采样间隔内的事件丢失
perfPMU 硬件计数器 + 软件事件中-高符号级调用栈需要符号表、开销大、分析门槛高
ftrace内核静态埋点内核函数级只能追踪已有埋点、输出格式不友好
SystemTap动态内核模块灵活需要内核 debug 符号、稳定性风险
eBPF任意内核/用户态事件极低(JIT 原生执行)指令级、任意上下文需要 Linux 4.1+,复杂程序需 4.15+

真正的优势在于:eBPF 程序的执行是事件驱动的,只在触发时才运行,JIT 编译后以原生机器指令执行,单次事件处理开销在纳秒级。这意味着你可以在生产环境长期开启 eBPF 监控,而不会显著影响性能。


二、eBPF 核心原理深度解析

要真正用好 eBPF 做可观测性,必须理解它的几个核心概念。这部分会深入内核实现,因为这是你写出高质量 eBPF 程序的基础。

2.1 eBPF 程序的生命周期

用户空间                        内核空间
   |                               |
   |  1. 编写 eBPF C 程序          |
   |  2. clang/LLVM 编译为 eBPF 字节码 |
   |  3. 通过 bpf() 系统调用加载    |
   |----------------------------->|  4. Verifier 验证字节码安全性
   |                               |  5. JIT 编译为本地机器码
   |                               |  6. 附加到事件源(kprobe/tracepoint等)
   |                               |
   |  7. 通过 eBPF Map 读取结果    |
   |<----------------------------->|  8. 事件触发 → eBPF 程序执行 → 写入 Map

Verifier(验证器) 是 eBPF 安全性的核心。它会对字节码做两项关键检查:

  1. 控制流图检查:确保程序是无环的(DAG),且有明确的退出路径,防止无限循环。
  2. 寄存器状态检查:模拟执行每一条指令,跟踪所有寄存器和栈上变量的类型与边界。例如,如果你用一个数作为数组索引,Verifier 必须能证明这个数在合法范围内——否则程序会被拒绝加载。这就是为什么 eBPF C 代码里有很多看似多余的边界检查

从 Linux 5.3 开始,Verifier 支持 Bounded Loops(有限循环),之前 eBPF 程序完全不允许循环,只能用宏展开代替。

JIT 编译器:验证通过后,eBPF 字节码被 JIT 编译为本地机器指令(x86_64、ARM64 等均支持)。在 /proc/sys/net/core/bpf_jit_enable 设为 1 时开启。JIT 后的 eBPF 程序执行开销与内核原生代码相差无几。

2.2 eBPF Map:内核与用户空间的桥梁

eBPF Map 是内核与用户空间之间,以及 eBPF 程序之间共享数据的关键机制。它本质是一个内核管理的键值存储,支持多种数据结构:

Map 类型用途典型场景
BPF_MAP_TYPE_HASH哈希表统计每个进程的系统调用次数
BPF_MAP_TYPE_ARRAY数组(定长)直方图、计数器数组
BPF_MAP_TYPE_PERF_EVENT_ARRAYPerf Event 环形缓冲区高频事件采样输出
BPF_MAP_TYPE_RINGBUF环形缓冲区(Linux 5.8+)推荐,比 perf event array 更高效
BPF_MAP_TYPE_LRU_HASHLRU 淘汰哈希表只保留最近 N 个元素的追踪

RINGBUF vs PERF_EVENT_ARRAY:早期的 eBPF 程序用 perf_event_array 向用户空间发送事件,每个 CPU 核心一个缓冲区,内存开销大,且有事件丢失问题。Linux 5.8 引入的 BPF_MAP_TYPE_RINGBUF 是一个多 CPU 共享的环形缓冲区,内存效率更高,且保证事件有序,是目前推荐的事件输出方式。

2.3 挂载点类型详解

eBPF 程序必须挂载到特定的事件源才能执行。不同挂载点适用不同的可观测性场景:

kprobe / kretprobe:在内核函数的入口(kprobe)或返回点(kretprobe)注入 eBPF 程序。

// 追踪 sys_openat 系统调用
SEC("kprobe/do_sys_openat2")
int BPF_KPROBE(do_sys_openat2, int dfd, const char __user *filename, ...)
{
    // BPF_KPROBE 宏自动处理寄存器到参数的映射(不同架构 ABI 不同)
    pid_t pid = bpf_get_current_pid_tgid() >> 32;
    bpf_printk("pid %d opening file: %s", pid, filename);
    return 0;
}

注意:kprobe 挂载的是内核函数的符号名,不同内核版本的函名可能变化,这是 kprobe 的主要脆弱点。

tracepoint:内核静态埋点,接口更稳定。

SEC("tracepoint/syscalls/sys_enter_openat")
int trace_enter_openat(struct trace_event_raw_sys_enter *ctx)
{
    // tracepoint 的参数结构是固定的,在 /sys/kernel/debug/tracing/events/ 下可查
    const char *fname = (const char *)ctx->args[1];
    // ...
}

uprobe / uretprobe:在用户态程序的任意函数(甚至任意指令地址)挂载 eBPF 程序。这是追踪应用层代码的利器——无需重新编译、无需修改源码。

// 挂载到 /usr/lib/libc.so.6 的 malloc 函数入口
SEC("uprobe/malloc")
int BPF_UPROBE(malloc_entry, size_t size)
{
    pid_t pid = bpf_get_current_pid_tgid() >> 32;
    u64 alloc_size = size;
    // 记录每次 malloc 的大小,统计内存分配模式
    bpf_map_update_elem(&malloc_sizes, &pid, &alloc_size, BPF_ANY);
    return 0;
}

USDT(User Statically Defined Tracing):应用程序内预埋的静态追踪点(DTrace 风格),用 SEC("usdt/...") 挂载。许多语言运行时(Python、Ruby、Node.js)都内置了 USDT 探针。


三、现代 eBPF 可观测性工具链

理解原理之后,你需要一套高效的工具链来将 eBPF 用于实际的可观测性工作。2026 年的工具生态已经相当成熟。

3.1 BCC(BPF Compiler Collection)

BCC 是最早大规模流行的 eBPF 前端工具集,提供 Python + C 的混合开发模式:eBPF C 程序在内核执行,Python 负责用户空间的 Map 读取、数据处理和展示。

from bcc import BPF

bpf_program = r"""
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>

struct data_t {
    u32 pid;
    u64 delta_ns;
    char comm[TASK_COMM_LEN];
};

BPF_PERF_OUTPUT(events);

// 记录每个进程的统一调度延迟
SEC("tracepoint/sched/sched_wakeup")
int trace_sched_wakeup(struct trace_event_raw_sched_wakeup *ctx)
{
    struct data_t data = {};
    data.pid = ctx->pid;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    // 记录唤醒时间戳到 Hash Map
    u64 ts = bpf_ktime_get_ns();
    bpf_map_update_elem(&wakeup_ts, &data.pid, &ts, BPF_ANY);
    return 0;
}
"""

bpf = BPF(text=bpf_program)
bpf.attach_tracepoint("sched:sched_wakeup", "trace_sched_wakeup")

# 用户空间消费事件
def print_event(cpu, data, size):
    event = bpf["events"].event(data)
    print(f"PID {event.pid} ({event.comm.decode()}): {event.delta_ns}ns")

bpf["events"].open_perf_buffer(print_event)
while True:
    bpf.perf_buffer_poll()

BCC 的优点:生态丰富,内置大量现成工具(execsnoopbiosnooptcplatency 等),快速原型开发方便。

BCC 的缺点:依赖 LLVM/Clang 运行时,容器环境中部署复杂;Python 前端对 Go/Rust 开发者不友好;跨架构兼容性一般。

3.2 libbpf + BPF CO-RE(Compile Once – Run Everywhere)

这是目前推荐的 eBPF 开发方式。核心思想是:将 eBPF 程序编译为通用的 ELF 对象文件,在加载时通过 BTF(BPF Type Format)信息做运行时重定位,从而适应不同内核版本的数据结构布局变化。

BTF 是内核导出的类型信息(类似 DWARF 调试信息,但更紧凑),从 Linux 5.2 开始内置。有了 BTF,eBPF 程序可以用 bpf_core_read() 宏来访问内核结构体字段,而无需在编译时硬编码字段偏移量。

// CO-RE 风格:不硬编码 task_struct 的字段偏移
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
    struct task_struct *task = (struct task_struct *)bpf_get_current_task();
    u32 pid = bpf_core_read(&task->pid);   // CO-RE 自动计算字段偏移
    // ...
}

libbpf-bootstrap 项目提供了现代化的 Makefile + 示例模板,是入门 libbpf 开发的最佳起点。

3.3 bpftrace:eBPF 的一行命令神器

bpftrace 提供类 awk/DTrace 领域特定语言,适合快速排查问题,无需写完整 C 程序。

# 追踪所有 openat 系统调用,显示进程名和文件名
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s: %s\n", comm, str(args->filename)); }'

# 统计每个进程的系统调用耗时分布(直方图)
bpftrace -e '
kprobe:do_sys_openat2 {
    @start[tid] = nsecs;
}
kretprobe:do_sys_openat2 /@start[tid]/ {
    printf("%d us: %s\n", (nsecs - @start[tid]) / 1000, comm);
    delete(@start[tid]);
}'

# 追踪 malloc 申请超过 1MB 的调用(内存泄漏排查)
bpftrace -e '
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc /arg0 > 1048576/ {
    printf("Large malloc: %d bytes by %s (PID %d)\n", arg0, comm, pid);
    ustack;
}'

ustackkstack 内置函数可以打印用户态/内核态的调用栈,这是排查复杂问题的关键能力。

3.4 现代可观测性平台中的 eBPF

Cilium + Hubble(云原生网络可观测性):基于 eBPF 实现 Kubernetes 网络层的深度可观测性,提供 L7 协议解析(HTTP/gRPC/Kafka)和流量拓扑图。

Pixie(现在是无侵入可观测性的标杆):利用 eBPF 自动追踪 Kubernetes 集群内所有 Pod 的网络流量和系统调用,无需修改应用代码,自动生成服务拓扑和黄金信号(延迟、吞吐量、错误率)。

Falco(运行时安全 + 可观测性):用 eBPF 监控系统调用异常,检测可疑行为(如容器内读取 /etc/shadow、意外的 shell 执行)。

Perfetto(Android/Linux 系统级追踪):Google 开发的追踪工具,支持 eBPF 数据源,适合做端到端性能分析。


四、eBPF 可观测性实战:四个典型场景的完整代码

理论讲完了,现在进入实战。以下四个场景都是生产环境中真实存在的问题,用 eBPF 可以高效解决。

场景一:追踪生产环境偶发性 IO 延迟毛刺

问题:某个 Go 微服务偶尔出现 500ms 以上的请求延迟,日志和 metrics 都看不出原因,怀疑是磁盘 IO 抖动。

解决方案:用 eBPF 追踪所有块设备 IO 的延迟分布,定位是否是存储层问题。

// io_latency.bpf.c - 块设备 IO 延迟追踪
// 编译:clang -O2 -g -target bpf -c io_latency.bpf.c -o io_latency.bpf.o
// 加载:bpftool prog load io_latency.bpf.o /sys/fs/bpf/io_latency

#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

// 用直方图记录 IO 延迟分布(对数刻度)
BPF_HISTOGRAM(io_latency_ns, u64);

// 记录每个 bio 的提交时间戳
struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 8192);
    __type(key, u64);           // bio 指针作为 key
    __type(value, u64);         // 提交时间戳(ns)
} bio_start SEC(".maps");

// 追踪块设备 IO 提交(block:block_rq_issue tracepoint)
SEC("tracepoint/block/block_rq_issue")
int trace_block_rq_issue(struct trace_event_raw_block_rq_issue *ctx)
{
    u64 bio_ptr = (u64)ctx->rq;  // 用 request 指针作为 key
    u64 ts = bpf_ktime_get_ns();
    bpf_map_update_elem(&bio_start, &bio_ptr, &ts, BPF_ANY);
    return 0;
}

// 追踪块设备 IO 完成(block:block_rq_complete tracepoint)
SEC("tracepoint/block/block_rq_complete")
int trace_block_rq_complete(struct trace_event_raw_block_rq_complete *ctx)
{
    u64 bio_ptr = (u64)ctx->rq;
    u64 *start_ts = bpf_map_lookup_elem(&bio_start, &bio_ptr);
    if (!start_ts) return 0;

    u64 latency_ns = bpf_ktime_get_ns() - *start_ts;
    // 记录到直方图(自动做对数分桶)
    io_latency_ns.increment(bpf_log2l(latency_ns));

    bpf_map_delete_elem(&bio_start, &bio_ptr);
    return 0;
}

用户空间用 bpftool map dump 读取直方图数据,或用 bpftrace 一行命令完成同样的事情:

bpftrace -e '
tracepoint:block:block_rq_issue { @start[args->rq] = nsecs; }
tracepoint:block:block_rq_complete /@start[args->rq]/ {
    @latency = hist(nsecs - @start[args->rq]);
    delete(@start[args->rq]);
}'

场景二:无侵入式追踪 Go 程序的函数调用耗时

Go 程序编译时默认去除了符号表(用 go build -ldflags="-s -w" 会进一步剥离),传统 profiler 很难关联到具体的函数名。但用 eBPF uprobe 可以直接挂载到 Go 运行时的关键函数,或者通过 Go 的 runtime 导出信息来定位。

更实用的方案是:用 eBPF 追踪 Go 的 GC 暂停时间,这是 Go 服务延迟毛刺的常见原因。

# 用 bpftrace 追踪 Go GC 的 STW(Stop-The-World)暂停
bpftrace -e '
uprobe:/usr/local/go/bin/go:runtime.GC {
    @gc_start[tid] = nsecs;
    printf("GC start at %s\n", strftime(nsecs, "%H:%M:%S"));
}
uretprobe:/usr/local/go/bin/go:runtime.GC /@gc_start[tid]/ {
    printf("GC duration: %d us\n", (nsecs - @gc_start[tid]) / 1000);
    delete(@gc_start[tid]);
}'

更精准的方式是追踪 runtime.stwNanos(Go 1.14+ 暴露的 STW 时间指标),或者直接用 perf trace -e syscalls:sys_enter_futex 观察 GC 期间的 futex 竞争模式。

场景三:用 eBPF 做零侵入的 HTTP 请求追踪

传统的 APM(如 Jaeger、Zipkin)需要在应用代码中埋点,或者挂载 Java/Python Agent。eBPF 可以在不修改应用代码、不挂载 Agent 的前提下,追踪 HTTP 请求的完整生命周期。

核心技术:用 uprobe 挂载到 Go/Java/Node.js 的 HTTP 客户端/服务器关键函数,抓取 URL、状态码、延迟;用 kprobe 挂载到 tcp_sendmsg / tcp_recvmsg 做网络层关联。

// http_tracer.bpf.c - 简化的 HTTP 请求追踪(挂载到 Go 的 net/http)
// 实际生产中使用 Cilium/Pixie 等成熟方案,这里展示原理

SEC("uprobe/go_http_handler")
int trace_go_http_handler(struct pt_regs *ctx)
{
    // 在 Go 的 net/http.HandlerFunc 入口挂载
    u64 pid_tgid = bpf_get_current_pid_tgid();
    u64 ts = bpf_ktime_get_ns();
    bpf_map_update_elem(&http_start, &pid_tgid, &ts, BPF_ANY);
    return 0;
}

实际生产环境中,推荐用 PixieCilium Hubble,它们已经完整实现了 L7 协议解析:

# 用 Pixie 追踪所有 HTTP 请求(无需修改代码)
px run -f http_request_stats.pxl  # Pixie 的 PxL 脚本

# 用 Cilium 查看 Pod 间的 HTTP 流量
cilium hubble port-forward &
hubble observe --protocol http --since 1m

场景四:检测容器逃逸和内核漏洞利用(安全可观测性)

eBPF 不仅可以做性能分析,还可以做安全监控。Falco 的核心原理就是用 eBPF 监控系统调用,匹配安全规则。

# 用 Falco 规则检测容器内异常 shell 执行
# /etc/falco/rules.d/custom_rules.yaml
- rule: Shell in Container
  desc: 容器内启动了 shell 进程
  condition: >
    container.id != host and
    proc.name in (bash, sh, zsh, dash) and
    not proc.pname in (dockerd, containerd)
  output: >
    Shell launched in container (user=%user.name command=%proc.cmdline
    container=%container.name image=%container.image.repository)
  priority: WARNING

# 用 eBPF 直接检测:非特权进程尝试加载内核模块
bpftrace -e '
kprobe:init_module {
    u32 uid = bpf_get_current_uid_gid();
    if (uid != 0) {
        printf("ALERT: Non-root user (UID %d) attempting to load kernel module\n", uid);
        printf("Process: %s (PID %d)\n", comm, pid);
    }
}'

五、eBPF 性能优化与最佳实践

eBPF 虽然高效,但用不好仍然会带来显著开销。以下是经过生产环境验证的最佳实践。

5.1 减少 Map 查找次数

每次 bpf_map_lookup_elem 都是一次哈希表查找,在高频率事件中(如网络包处理)会成为瓶颈。优化策略:

  • 用 per-CPU MapBPF_MAP_TYPE_PERCPU_HASHBPF_MAP_TYPE_PERCPU_ARRAY):每个 CPU 核心有独立的 Map 副本,完全避免锁竞争。
  • 用 BPF 栈上变量:能放在栈上的局部状态就不要放 Map。
  • 批量处理:用 BPF_MAP_TYPE_RINGBUF 批量提交事件,减少用户空间唤醒次数。

5.2 减少 bpf_printk 的使用

bpf_printk 会向 /sys/kernel/debug/tracing/trace_pipe 输出调试信息,但每次调用都会触发一次 trace_printk,开销很大,而且 trace_pipe 是全局单消费者队列,多个程序会互相干扰。生产环境中应完全避免 bpf_printk,改用 bpf_trace_printk(更底层的接口)或者直接写 Ring Buffer Map。

5.3 Verifier 限制与规避技巧

Verifier 有严格的指令数量限制(Linux 5.3+ 是 100 万条指令),以及栈大小限制(512 字节)。复杂逻辑需要拆分:

  • 用尾调用(tail call)拆分大程序BPF_MAP_TYPE_PROG_ARRAY + bpf_tail_call,将大程序拆分为多个小 eBPF 程序,互相调用。
  • 用 BPF 迭代器(BPF iterators,Linux 5.8+):安全地在内核中遍历数据结构(如所有进程的任务结构体),避免在一次 eBPF 程序执行中处理过多数据。

5.4 生产环境部署建议

  1. 始终用 CO-RE + libbpf,不要用 BCC 的嵌入式 clang 编译(容器环境中难以复现 build 环境)。
  2. 用 Aya(Rust eBPF 框架)写 eBPF 程序:Aya 完全用 Rust 编写 eBPF 程序,无需 C/Clang,类型安全,且支持 CO-RE。2026 年 Aya 已经相当成熟。
  3. 监控 eBPF 程序本身的性能影响:用 bpftool prog show 查看每个 eBPF 程序的运行次数和(Linux 5.15+)累计执行时间。
  4. 注意内核版本兼容:用 bpftool feature probe 检测当前内核支持的 eBPF 特性,做特性门控。
# 查看所有已加载的 eBPF 程序及其统计信息
bpftool prog list
# 输出示例:
# 123: kprobe  name trace_open  tag abc123...  jited  memlock 4096  run_time_ns 123456789

# 查看特定程序的详细统计
bpftool prog show id 123

六、eBPF + Wasm:内核可编程性的下一步

eBPF 解决了内核可编程性问题,但它的沙箱限制很严格(不能用任意内核 API,不能调用任意函数)。WebAssembly(Wasm)正在成为 eBPF 的有力补充——Wasm 提供沙箱化、跨平台的用户态可编程能力,而 eBPF 负责内核态可编程。

具体场景:

  • eBPF 做包过滤,Wasm 做复杂策略逻辑:Cilium 正在探索用 Wasm 编写网络策略,eBPF 负责高性能的数据包拦截和转发,Wasm 负责灵活的 L7 策略判断(如 JWT 验证、复杂 ACL)。
  • Pixie 用 Wasm 做数据预处理:在 eBPF 采集到原始追踪数据后,用 Wasm 模块在用户空间做过滤、聚合、格式转换,避免把所有原始数据都发送到后端。
  • eBPF + Wasm 做边缘计算:eBPF 负责内核态的网络/安全策略,Wasm 负责应用态的业务逻辑,两者结合构成一个完全可编程的边缘计算平台。

七、总结与展望

eBPF 已经不是"新技术"了——从 2014 年进入 Linux 内核,到 2026 年的今天,它已经成为 Linux 可观测性、网络、安全的事实标准基础设施

对开发者,eBPF 意味着你终于有了一个稳定、高效、零侵入的方式来"看透"整个系统——从网卡到应用层,从内核调度器到 malloc 调用,一切皆可追踪。

对平台工程师,eBPF 意味着你可以用 Cilium 替代 kube-proxy,用 Pixie 替代传统 APM,用 Falco 做运行时安全,整个可观测性栈都建立在 eBPF 这个统一的基础设施之上。

未来,eBPF 还在继续进化:sched_ext(可替换的 CPU 调度器框架,已在 Linux 6.12+ 中可用)让你可以完全用 eBPF 程序实现自定义调度策略;eBPF 的 LSM 挂钩(KRSI)正在成为 seccomp 的下一代替代品;而 eBPF + Wasm 的结合,则可能在未来几年彻底改变边缘计算和平台工程的玩法。

如果你还没在生产环境中用过 eBPF,2026 年是最好的开始时间。从 bpftrace 的一行命令开始,用 Cilium 做 K8s 网络,用 Pixie 做可观测性——你会发现,原来"看懂系统在做什么"可以这么简单。


参考资料与进一步阅读

复制全文 生成海报 eBPF Linux 可观测性 性能分析 BPF

推荐文章

FastAPI 入门指南
2024-11-19 08:51:54 +0800 CST
npm速度过慢的解决办法
2024-11-19 10:10:39 +0800 CST
WebSocket在消息推送中的应用代码
2024-11-18 21:46:05 +0800 CST
PHP 的生成器,用过的都说好!
2024-11-18 04:43:02 +0800 CST
如何开发易支付插件功能
2024-11-19 08:36:25 +0800 CST
Go 中的单例模式
2024-11-17 21:23:29 +0800 CST
Mysql允许外网访问详细流程
2024-11-17 05:03:26 +0800 CST
git使用笔记
2024-11-18 18:17:44 +0800 CST
Nginx 反向代理
2024-11-19 08:02:10 +0800 CST
程序员茄子在线接单