编程 eBPF 可观测性革命:从内核无侵入采集到生产级全链路追踪的深度实战

2026-07-23 03:43:51 +0800 CST views 7

eBPF 可观测性革命:从内核无侵入采集到生产级全链路追踪的深度实战

当你半夜被一条「接口 P99 突然飙到 3 秒」的告警叫醒,传统 APM 给你的却只有一句「/api/order 慢了」——你仍然不知道是 DNS 解析卡了、TCP 重传炸了、锁竞争还是下游 MySQL 在刷脏页。问题的根因往往发生在应用代码根本观测不到的地方:内核协议栈里。eBPF 的出现,第一次让我们可以在「不改一行业务代码、不重启进程、不挂载 sidecar」的前提下,直接从内核把真相抠出来。

一、背景:可观测性的三座大山与 Agent 之痛

在聊 eBPF 之前,先说清楚它要解决的问题。现代微服务的可观测性长期被三件事折磨:

  1. 侵入性。传统 APM(如某 APM、某 Pinpoint)要么要你在代码里埋 SDK,要么靠 Java agent 字节码插桩。这意味着:每接一个新语言要等官方 SDK;SDK 版本升级要重新发版;最要命的是——第三方闭源组件、Go 的 runtime、内核协议栈,你根本插不进去
  2. 盲区。一个 HTTP 请求从客户端到服务端,要经过 DNS → TCP 握手 → TLS → 内核 socket 缓冲 → 应用读取 → 业务处理 → 写回。绝大多数 APM 只能看到「应用读取之后」这一段。一旦慢在握手或内核排队,监控图上一片岁月静好,用户那边已经转圈圈转了五秒。
  3. 开销与语言绑定。每个 JVM 挂一个 agent,内存多吃几百 MB;每种语言一套采集逻辑,维护成本指数上升。

容器化把这件事变得更糟:一个 Pod 里可能跑着用四种语言写的微服务,你可能连它用的框架都说不全,更别提逐个埋点。

eBPF 给出的答案是:把探针从用户态「下沉」到内核态。内核是所有进程、所有网络、所有文件 I/O 的必经之路。只要能在这里挂一个安全的、经过验证的小程序,就能看到一切——而且业务代码零感知。

这不是噱头。今天你用的 Cilium(K8s 网络)、Falco(运行时安全)、Pixie(零注入追踪)、OpenTelemetry 的 eBPF Collector,底层全是 eBPF。

二、核心概念:eBPF 到底是什么

eBPF(extended Berkeley Packet Filter)字面意思是「扩展的伯克利包过滤器」。它的前身 BPF 诞生于 1992 年,初衷只是让 tcpdump 高效地过滤数据包——「只把匹配规则的网络包拷贝到用户态,而不是把所有包都拷过来」。

eBPF 把这套思想从「网络包过滤」扩展成了「在内核中安全运行任意(受限)程序」的通用虚拟机。它会经历这样一条生命周期:

   C / Rust 源码
        │ clang/LLVM 编译成 BPF 字节码
        ▼
   ┌─────────────── 验证器 (Verifier) ───────────────┐
   │  · 循环必须有界(否则拒绝)                       │
   │  · 所有内存访问必须可证明安全(无越界/野指针)     │
   │  · 栈上限 512 字节                               │
   │  · 不能导致内核崩溃、不能死循环                   │
   └──────────────────────────────────────────────────┘
        │ 通过则 JIT 编译为机器码
        ▼
   挂到内核钩子(Hook)上:kprobe / uprobe / tracepoint /
   fentry / XDP / TC / LSM / socket filter ...
        │
        ▼
   运行时通过 Map 与用户态双向通信

2.1 验证器:为什么 eBPF 不会搞崩内核

这是 eBPF 最反直觉、也最迷人的设计。一个普通内核模块(LKM)写错了能让整机 panic;但 eBPF 程序在加载时会被验证器做静态分析:

  • 有界循环:旧内核只允许有显式上界的循环(新内核 5.3+ 支持有界循环,但仍不许无限循环),验证器要保证程序一定能在有限步内终止。
  • 指针安全:任何对内存的读写,验证器都要证明「这个指针指向的内存是合法的、范围可控的」。你不能直接解引用一个从用户态来的野指针。
  • 栈大小:栈帧最多 512 字节(旧版 256),想存大数组得用 Map 而不是栈。

正是这套「内核里的类型系统 + 静态分析」,让 eBPF 可以安全地跑在生产机的特权上下文里。

2.2 Map:eBPF 与用户态的「共享内存」

eBPF 程序运行在内核态,你的采集器跑在用户态,二者靠 Map 通信。Map 本质上是一类内核态的键值存储,常见类型:

Map 类型用途典型场景
BPF_MAP_TYPE_HASH哈希表按 pid 聚合统计
BPF_MAP_TYPE_PERCPU_HASH每 CPU 独立哈希表高并发计数,避免锁竞争
BPF_MAP_TYPE_ARRAY定长数组全局配置、直方图桶
BPF_MAP_TYPE_PERF_EVENT_ARRAYPerf 环形缓冲把单条事件流推给用户态
BPF_MAP_TYPE_RINGBUF单一环形缓冲高吞吐事件,比 perf 更省
BPF_MAP_TYPE_LRU_HASHLRU 淘汰哈希连接跟踪表

2.3 钩子:你能「挂」在哪里

  • kprobe / kretprobe:动态挂到任意内核函数入口/返回。灵活但依赖函数名(内核版本一升级函数可能改名或内联)。
  • tracepoint:内核里预先埋好的稳定钩子,ABI 相对稳定,生产首选。
  • fentry / fexit:基于 BTF 的「静态插桩」,比 kprobe 更快、更稳,需要内核 5.5+ 和 BTF。
  • XDP / TC:网络数据路径最前端 / 流量控制层,做高性能网络可观测性甚至防火墙。
  • uprobe / uretprobe:挂到用户态函数,比如直接 hook 某个 Go 二进制里的 runtime.gcBgMarkWorker,无需重新编译就能观测 GC。
  • LSM (Linux Security Module):安全策略层,做运行时入侵检测。

2.4 CO-RE 与 BTF:一次编译,到处运行

这是 eBPF 能从「玩具」变成「生产基础设施」的关键。早期 eBPF 要读取内核结构体(比如 struct task_struct)的字段,得在编译时就把字段偏移量 hardcode 进去——换个内核版本字段偏移一变,程序就崩了。

BTF(BPF Type Format) 把内核的数据结构信息导出成一种紧凑的元数据。/sys/kernel/btf/vmlinux 这个文件就是内核的「自描述」。配合 CO-RE(Compile Once - Run Everywhere),eBPF 程序在加载时根据目标机器的 BTF 重新解析字段偏移,于是同一份字节码可以跑在 CentOS 3.10、Ubuntu 5.15、 Alma 6.8 上而无需重编。

判断一台机器能不能跑 CO-RE 的 eBPF:

ls -l /sys/kernel/btf/vmlinux   # 存在即支持 BTF
uname -r                         # 建议 4.18+(生产推荐 5.4+)

三、工具链与生态:从 bpftrace 到 Aya

你不需要从零手写字节码,生态已经分层:

  • bpftrace:DTrace 风格的单行/脚本语言,适合「临时排障」——一行命令看出是哪个进程在疯狂写磁盘。
  • BCC(BPF Compiler Collection):Python + C 的组合,适合写中小型采集脚本,C 写 eBPF 内核侧,Python 写用户态消费逻辑。
  • libbpf + libbpf-bootstrap:C 写生产级、CO-RE 友好的程序。
  • Aya:纯 Rust 实现 eBPF,无 libbpf 依赖、无 C 侧、编译期类型安全,正成为云原生基础设施的新宠(Cilium 部分组件已用 Rust)。
  • 生产级成品:Cilium(网络+可观测)、Pixie(零注入追踪)、Falco(安全)、OpenTelemetry eBPF Collector(指标/追踪采集)。

下面我们用 bpftrace 快速排障,用 BCC 写中等规模采集器,再用 Aya 看生产级结构。

四、架构分析:一个 eBPF 可观测系统的分层

生产级 eBPF 可观测系统通常是这样的分层:

┌─────────────────────────────────────────────┐
│  应用进程(完全无感知)                         │
└───────────────┬─────────────────────────────┘
                │ 系统调用 / socket / 文件 I/O
                ▼
┌─────────────────────────────────────────────┐
│  内核态 eBPF 程序                              │
│   kprobe/tracepoint/fentry → 过滤/聚合         │
│   热点事件 → Map / RingBuf                     │
└───────────────┬─────────────────────────────┘
                │ Map 读写 / 事件流
                ▼
┌─────────────────────────────────────────────┐
│  用户态采集 Agent(DaemonSet / systemd)       │
│   消费 RingBuf → 关联 pid/comm/cgroup          │
│   →  enrichment(Pod 名、容器、服务名)         │
└───────────────┬─────────────────────────────┘
                │ OTLP (gRPC/HTTP)
                ▼
┌─────────────────────────────────────────────┐
│  后端:Prometheus / Jaeger / Tempo / Loki      │
│  或统一到 OpenTelemetry Collector              │
└─────────────────────────────────────────────┘

关键设计点:

  1. 内核态只做「过滤 + 抽样 + 轻聚合」,重活(关联元数据、格式化、上报)留给用户态,避免 eBPF 程序本身太重被验证器拒绝或拖慢内核。
  2. enrichment 是价值所在。eBPF 看到的是 pid/comm,但 SRE 需要的是「order-service 这个 Pod 的 P99」。Agent 要通过 cgroup id → Kubernetes Pod 元数据做映射(这正是 Pixie/Cilium 的玩法)。
  3. 与 OpenTelemetry 对齐。事件最终转成 OTLP 的 metrics / traces / logs,复用现有后端,避免又造一套孤立的监控孤岛。

五、代码实战一:用 bpftrace 五分钟定位生产问题

bpftrace 是「临时排障」的瑞士军刀。下面的脚本都不需要写一行 C。

场景 A:哪个进程在疯狂 syscall?

# 按进程统计系统调用次数,Ctrl+C 后打印
tracepoint:raw_syscalls:sys_enter
{
    @syscalls[comm] = count();
}

场景 B:openat 延迟分布(是不是文件系统慢了?)

tracepoint:syscalls:sys_enter_openat
{
    @start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_openat
/ @start[tid] /
{
    @open_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}

场景 C:按进程统计 TCP 发送字节数(谁在偷跑流量?)

kprobe:tcp_sendmsg
{
    @bytes[comm] = sum(arg2);
}

tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size) 的第三个参数 arg2 就是本次发送的字节数。bpftrace 的 comm 内置变量直接给出当前进程名。

场景 D:TCP 重传——网络质量的红灯

kprobe:tcp_retransmit_skb
{
    @retrans[comm] = count();
}

重传一涨,基本断定是链路质量问题(丢包、拥塞),与应用代码无关——传统 APM 很难直接暴露这点。

场景 E:给 Go 服务做无侵入 GC 观测
即使 Go 二进制默认 strip 了符号,只要编译时保留(或容器里用未 strip 的镜像),就能 uprobe 到 runtime:

# 需要目标二进制含 runtime 符号
uprobe:/app/server:runtime.gcBgMarkWorker
{
    @gc_count[pid] = count();
}

实际生产中更稳的做法是 uprobe 到可观测的框架函数,或借助 Go 的 GODEBUG / pprof 协作。这里展示的是 eBPF「无需改代码就能看到 runtime 内部」的能力。

bpftrace 的价值在于:从「怀疑是网络问题」到「确认是 TCP 重传导致」,只需一条命令、几秒钟,而不用等下次发版埋点。

六、代码实战二:用 BCC 写进程级网络监控

当排障脚本要常驻、要做复杂用户态处理时,BCC 是更合适的选择。下面是一个统计「每个进程 TCP 发送字节数」并实时打印的采集器。

#!/usr/bin/env python3
# net_perproc.py —— 基于 BCC 的进程级网络发送监控
from bcc import BPF
import time

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

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

BPF_PERF_OUTPUT(events);

// BCC 约定:kprobe__<函数名> 自动 attach 到该内核函数
int kprobe__tcp_sendmsg(struct pt_regs *ctx,
                        struct sock *sk,
                        struct msghdr *msg,
                        size_t size) {
    struct data_t data = {};
    // 高 32 位是 pid(实际是 tgid),低 32 位是 tid
    data.pid  = bpf_get_current_pid_tgid() >> 32;
    data.bytes = size;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}
"""

b = BPF(text=bpf_text)

# 用户态消费:把内核态 Perf Buffer 里的事件打印出来
def print_event(cpu, data, size):
    event = b["events"].event(data)
    comm = event.comm.decode("utf-8", "replace")
    print(f"pid={event.pid:<6} comm={comm:<16} bytes={event.bytes}")

b["events"].open_perf_buffer(print_event)

print("Tracing TCP send... Ctrl-C to stop")
while True:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        break

几个关键点:

  • kprobe__tcp_sendmsg 是 BCC 的语法糖,自动把程序挂到 tcp_sendmsg
  • bpf_get_current_pid_tgid() 返回 pid_tgid,高 32 位是线程组 ID(即我们常说的进程 pid),低 32 位是 tid。
  • events.perf_submit 把事件推到 BPF_PERF_OUTPUT 声明的 Perf Buffer,用户态 open_perf_buffer + perf_buffer_poll 异步消费。

如果想进一步看「连到了哪个 IP」,可以再加一个 tcp_v4_connect 的 kprobe,用 bpf_probe_read_kernelsk->__sk_common.skc_daddr

int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) {
    u32 daddr = 0;
    bpf_probe_read_kernel(&daddr, sizeof(daddr),
                          &sk->__sk_common.skc_daddr);
    // daddr 是网络字节序的 IPv4,可做黑白名单/地理映射
    return 0;
}

注意:skc_daddr 字段名随内核版本可能微调,这正是 CO-RE + BTF 要解决的痛点——用 BCC 的 CO-RE 写法(BPF_CORE_READ)可避免 hardcode 偏移。

七、代码实战三:用 Aya(Rust)构建生产级采集器

BCC 依赖 LLVM/Clang 在线编译,体量重、启动慢,不太适合塞进一个轻量 DaemonSet。Aya 用纯 Rust 写 eBPF,编译产物是独立的 ELF,加载快、无外部依赖,正成为云原生基础设施的主流选择。

项目结构

my-ebpf-agent/
├── Cargo.toml          # workspace
├── ebpf/               # eBPF 内核侧(no_std, aya-bpf)
│   ├── Cargo.toml
│   └── src/main.rs
└── src/                # 用户态 agent(aya + tokio)
    └── main.rs

内核侧 ebpf/src/main.rs

#![no_std]
#![no_main]

use aya_bpf::{
    macros::{kprobe, map},
    maps::PerfEventArray,
    programs::ProbeContext,
};

#[repr(C)]
pub struct NetEvent {
    pub pid: u32,
    pub bytes: u64,
    pub comm: [u8; 16],
}

// PerfEventArray 把单条事件流推到用户态
#[map]
static EVENTS: PerfEventArray<NetEvent> = PerfEventArray::with_max_entries(1024, 0);

#[kprobe(name = "tcp_sendmsg_probe")]
pub fn tcp_sendmsg_probe(ctx: ProbeContext) -> u32 {
    // 第三个参数(index=2)是 size_t size
    let size: u64 = ctx.arg(2).unwrap_or(0);
    let pid = (aya_bpf::helpers::bpf_get_current_pid_tgid() >> 32) as u32;

    let mut comm = [0u8; 16];
    unsafe {
        aya_bpf::helpers::bpf_get_current_comm(
            &mut comm as *mut _ as *mut core::ffi::c_char, 16);
    }

    let evt = NetEvent { pid, bytes: size, comm };
    EVENTS.output(&ctx, &evt, 0);
    0
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

用户态 src/main.rs(基于 Aya 的 async API)

use aya::{Bpf, programs::KProbe, maps::PerfEventArray, util::online_cpus};
use bytes::BytesMut;
use tokio::sync::mpsc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // 加载编译好的 eBPF ELF(CO-RE,跨内核版本无需重编)
    let mut bpf = aya::BpfLoader::new()
        .load(aya::include_bytes_aligned!(
            "../../target/bpfel-unknown-none/release/ebpf"
        ))?;

    let program: &mut KProbe = bpf
        .program_mut("tcp_sendmsg_probe")
        .unwrap()
        .try_into()?;
    program.load()?;
    program.attach("tcp_sendmsg", 0)?; // 挂到内核 tcp_sendmsg

    // 消费 PerfEventArray:每个 CPU 一个 buffer,避免锁竞争
    let mut perf_array = PerfEventArray::try_from(bpf.map_mut("EVENTS").unwrap())?;
    for cpu in online_cpus()? {
        let mut buf = perf_array.open(cpu, None)?;
        tokio::spawn(async move {
            let mut buffers = (0..10).map(|_| BytesMut::with_capacity(1024)).collect::<Vec<_>>();
            loop {
                if let Ok(events) = buf.read_events(&mut buffers) {
                    for i in 0..events.read {
                        let data = &buffers[i];
                        // data 前 4 字节 pid,接着 8 字节 bytes,再 16 字节 comm
                        // 解析后做 enrichment → 上报 OTLP
                    }
                }
            }
        });
    }

    tokio::signal::ctrl_c().await?; // 常驻,直到 Ctrl+C
    Ok(())
}

这段代码展示了 Aya 的核心范式:内核侧用 aya-bpf 写、用户态用 aya 加载,共享 NetEvent 结构体定义,CO-RE 保证跨内核可移植。相比 BCC 少了一次在线编译,DaemonSet 镜像能从几百 MB 瘦到几十 MB。

八、代码实战四:零代码改造的分布式追踪

eBPF 最让人兴奋的能力,是不用改业务代码就能重建分布式追踪。原理如下:

W3C 定义了 traceparent HTTP 头(格式 version-traceid-spanid-traceflags)。在一个微服务调用下游时,这个头会被写进 HTTP 请求。如果我们能在内核 socket 写路径tcp_sendmsg)上 hook,读出这次发送缓冲区的开头几个字节,就能捕获 traceparent,从而知道「这个 TCP 连接上的请求属于哪个 Trace」——即使应用用的是最老的、没接任何 tracing SDK 的框架。

一个 BCC 思路(读取 iovec 里的用户态缓冲区前 N 字节):

int kprobe__tcp_sendmsg(struct pt_regs *ctx,
                        struct sock *sk,
                        struct msghdr *msg,
                        size_t size) {
    // 只采样、只关心 HTTP(粗略按端口/长度过滤可在此加)
    char head[32] = {};
    // msg->msg_iter 是 iov 迭代器,取第一个 iovec 的 base
    struct iovec iov;
    bpf_probe_read_kernel(&iov, sizeof(iov), &msg->msg_iter.__iov);
    bpf_probe_read_user(&head, sizeof(head), iov.iov_base);
    // head 里若含 "traceparent: ",即可解析出 trace_id / span_id
    bpf_trace_printk("first bytes: %s\\n", &head);
    return 0;
}

实战要点:TLS 流量在内核态已经是密文,无法直接读 header。两种解法:(1) 对明文 HTTP/gRPC 直接用上述方法;(2) 对 TLS,改为 uprobe 到应用所用的 TLS 库写函数(如 Go 的 crypto/tls、OpenSSL 的 SSL_write),在加密之前捕获明文——Pixie 正是这么做的。

把解析出的 trace_id / span_id / 时间戳 / 源目 IP:Port 组装成 OTLP Span,上报到 Jaeger/Tempo,你就得到了一张零埋点的服务调用拓扑图。这就是「可观测性民主化」:新接的语言、老旧的遗留系统,只要跑在 Linux 上,立刻被看见。

九、性能优化:把开销压到 1% 以内

eBPF 快,但写得糙一样能拖垮内核。几条实战铁律:

  1. 抽样,不要全采。高频事件(如每个网络包)全采会淹没有户态。用随机丢弃:
    if (bpf_get_prandom_u32() % 100 != 0) return 0; // 只采 1%
    
  2. 用 Per-CPU Map 替代普通 Hash。多核并发写同一个 HASH 要加锁;PERCPU_HASH 每核一份副本,用户态再汇总,几乎无锁。
  3. RingBuf 优于 Perf Buffer。需要高吞吐事件流时,BPF_MAP_TYPE_RINGBUF 是单一环形缓冲、多生产者共享,比「每 CPU 一个 Perf Buffer + 各自拷贝」更省内存、吞吐更高。
  4. 内核态只过滤、不格式化。不要在内核里 sprintf 一大串,把原始字段丢进 Map/Buffer,格式化留给用户态。
  5. 优先 tracepoint / fentry,避开 kprobe 的不稳定。kprobe 依赖函数名,内核一升级函数可能被内联或改名;tracepoint 是内核承诺的稳定 ABI,fentry 在有 BTF 时更快。
  6. 验证器友好的循环。避免复杂指针追逐;大数组用 Map 而非栈(栈只有 512 字节)。

经验值:一个设计良好的 eBPF 网络/追踪探针,在 10K QPS 量级的服务上 CPU 开销通常 < 1%——远低于挂一个 JVM agent 的 5%~10%。

十、生产落地 Checklist 与常见坑

部署前检查

  • 内核 ≥ 4.18(CO-RE 推荐 5.4+,fentry 需 5.5+),/sys/kernel/btf/vmlinux 存在。
  • 权限:加载 eBPF 需要 CAP_BPF + CAP_SYS_ADMIN(或容器 privileged: true / 挂对应 capabilities)。K8s 里用 securityContext 精确授权比直接 privileged 更稳。
  • 资源:Map 占用内核内存,给 DaemonSet 设 resources.limits,避免 Map 无限膨胀拖垮节点。

常见坑

  • 验证器拒绝:报错 R0 invalid mem access 'inv' 之类,多半是内核结构体字段访问没用 CO-RE 的 BPF_CORE_READ,或循环边界验证器推不出。改写法 + 开 BPF_LOG 看详细拒绝原因。
  • 内核版本漂移:同一份 eBPF 在测试集群(5.15)跑得好好的,到生产(3.10)挂了——老内核没 BTF,CO-RE 失效。对策:锁定最低内核版本,或为非 CO-RE 内核准备兼容路径。
  • 容器里读不到符号:uprobe 业务进程需要目标二进制带符号;Alpine 的 musl strip 策略、Distroless 镜像都可能让符号丢失,需改用未 strip 的基础镜像或保留调试信息。
  • 与 seccomp / AppArmor 冲突:某些强化策略禁止 bpf() 系统调用,需显式放行。
  • 热升级:eBPF 程序更新要「先挂新再摘旧」,避免采集断档;用 bpf_link 管理生命周期比手动 detach 更安全。

十一、总结与展望

eBPF 不是又一个监控工具,它是可观测性的新底座。它把「看见系统」的能力从「应用层主动上报」变成了「内核层被动透视」——业务代码零改动、新语言零适配、遗留系统零改造。

今天它已经支撑起 Cilium 的网络可观测、Pixie 的零注入追踪、Falco 的运行时安全。下一步的趋势很清楚:

  • 与 OpenTelemetry 深度融合:eBPF 采集的 metrics / traces / logs 通过 OTLP 统一进现有后端,结束「再买一套 APM」的孤岛历史。
  • eBPF + AI 异常检测:内核级细粒度指标(每连接的重传、每进程的系统调用熵)喂给机器学习模型,做亚秒级的异常定位,而不是等告警阈值被冲破。
  • 边界与克制:eBPF 能力越强,越要守住安全与稳定——验证器、权限最小化、资源上限,是把「内核透视」变成「生产可靠」的三道闸门。

如果你还没在生产环境摸过 eBPF,今晚就在一台测试机上敲下第一条 bpftrace 命令。当你第一次看到「原来慢的不是我的代码,是内核在重传」的那一刻,你就再也回不去那个靠猜排障的时代了。


延伸阅读:bpftrace 官方参考手册、BCC 示例库(tools/ 目录下上百个现成脚本)、Aya 文档与 aya-template、Linux 内核 Documentation/bpf/bpftool 是调试 eBPF 的必备瑞士军刀。

推荐文章

淘宝npm镜像使用方法
2024-11-18 23:50:48 +0800 CST
Vue3 结合 Driver.js 实现新手指引
2024-11-18 19:30:14 +0800 CST
使用 node-ssh 实现自动化部署
2024-11-18 20:06:21 +0800 CST
跟着 IP 地址,我能找到你家不?
2024-11-18 12:12:54 +0800 CST
php腾讯云发送短信
2024-11-18 13:50:11 +0800 CST
Paperclip:全AI运作的公司框架
2026-05-18 14:24:25 +0800 CST
程序员茄子在线接单