ebpf 采集ebpf 采集tag+tcp五元组

在这里对文章题目作一些说明。笔者想了很长时间也无法给这篇文章想个恰当的表意题目。实际上使用ebpf来进行服务观测是有在进行的,比如获取目前l1s上的常见的四元组。但是本文不是介绍这部分可观测实践的。文章希望阐述的场景是:采集请求触发里的一些信息(诸如trace及其他header等)并和服务请求下游的传输层五元组(protocol, src-ip, src-port, dst-ip, dst-port)进行关联。这也是最近工作中实际遇到的问题。

基于ebpf的丰富的特性能够获取服务很多的信息,不同特性的组合更是可以达到极强的数据整合能力。比如通过uprobe便捷的获取业务信息后,结合kprobe来获取系统调用里的内容,可以获取一般侵入式可观测代码无法获取的内容。笔者最近遇到的一个实际问题是:获取服务A的接口/a响应后,向下游B发起的请求时,所使用的传输层五元组,同时带上结合一些/a触发时的一些内容,比如caller_fun或者traceId
这里值得说明的是,用户态请求的是一个域名。域名的解析是在golanghttp里完成的。但是请注意,golang发起tcp请求时,local port设置的是0,然后由内核态的tpc处理来选择一个空闲的port作为socket里的lport。这部分的信息通过代码的埋点显然是无法获取的(详情可参考TCP连接中客户端的端口号是如何确定的?)。
下面介绍下实现效果及思路。

关于bpftrace使用的介绍,可以参见:bpftrace 无侵入遍历golang链表,关于ebpf来进行数据采集的实践,可以参见ebpf采集mysql请求信息及ebpf对应用安全的思考

# 实现效果

服务端启动、触发的效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 启动目标服务
./caller_tuple
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /echo                     --> main.Echo (3 handlers)
# 这里触发一次接口调用
[GIN] 2023/02/24 - 22:05:29 | 200 |   85.618975ms |       127.0.0.1 | GET      "/echo"

bpftrace 采集端的效果:

1
2
3
4
5
6
7
8
# 启动采集
bpftrace ./caller.bt
Attaching 3 probes...
start to gather caller info.
get caller path: /echo
# 将 caller_path 和 传输层五元组结合起来(本机的IP实际上是输出的,但是为了信息安全,就使用 0.0.0.0 来代替了)
caller info: /echo
3326691  caller_tuple     0.0.0.0                            38610  110.242.68.66                           80

# 代码实现

这里分别上一下目标服务caller_func以及采集脚本caller.bt的代码,来说明下实现思路。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// ./caller_tuple/main.go
package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type Resp struct {
	Errno int64  `json:"errno"`
	Msg   string `json:"msg"`
}

func Echo(c *gin.Context) {
	req, _ := http.NewRequest(http.MethodGet, "http://baidu.com", nil)
	client := http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		c.JSON(http.StatusOK, &Resp{Errno: 1, Msg: "request error"})
		return
	}
	defer resp.Body.Close()
	c.JSON(http.StatusOK, &Resp{Errno: 0, Msg: "ok"})
	return
}

func main() {
	r := gin.Default()
	srv := &http.Server{
		Addr: "0.0.0.0:3344",
	}
	r.GET("/echo", Echo)
	srv.Handler = r
	srv.ListenAndServe()
}


// caller_tuple/caller.bt
#!/usr/bin/env bpftrace

#define AF_INET 2

struct sock_common {
        union {
                struct {
                        __be32 skc_daddr;
                        __be32 skc_rcv_saddr;
                };
        };
        union {
                unsigned int skc_hash;
                __u16 skc_u16hashes[2];
        };
        union {
                struct {
                        __be16 skc_dport;
                        __u16 skc_num;
                };
        };
        short unsigned int skc_family;
};

struct sock {
        struct sock_common __sk_common;
};

BEGIN{
    printf("start to gather caller info.
");
    @caller[pid] = "none";
}

// 这里通过 uprobe 来便捷的获取会话信息。同时将信息写入bpf_map
uprobe:./caller_tuple:"net/http.serverHandler.ServeHTTP"{
    $req_ptr = sarg3;
    $method_ptr = *(uint64*)($req_ptr);
    $method_len = *(uint64*)($req_ptr+8);

    /* read request.url.Path */
    $url_ptr = *(uint64*)($req_ptr + 16);
    $path_ptr = *(uint64*)($url_ptr+56);
    $path_len = *(uint64*)($url_ptr+64);
    printf("get caller path: %s
", str($path_ptr, $path_len));
    // 这里使用 pid 来作为 key 只是为了实现方便。实际可以采取其他更有区分性的内容。
    @caller_ptr[pid]=$path_ptr;
    @caller_len[pid]=$path_len;
}

// 通过 kprobe 来获取用户态无法获取的内容。同时通过 bpf_map 来控制生效及内容的交互。
kprobe:tcp_connect
{
    if (@caller_ptr[pid] == 0){
        return;
    }
    $ptr = @caller_ptr[pid];
    $len = @caller_len[pid];
    printf("caller info: %s
", str($ptr, $len));
    @caller_ptr[pid] = 0;
    @caller_len[pid] = 0;

  $sk = ((struct sock *) arg0);
  $inet_family = $sk->__sk_common.skc_family;

  if ($inet_family == AF_INET) {
    $daddr = ntop($sk->__sk_common.skc_daddr);
    $saddr = ntop($sk->__sk_common.skc_rcv_saddr);
    $lport = $sk->__sk_common.skc_num;
    $dport = $sk->__sk_common.skc_dport;
    $dport = (((($dport) >> 8) & 0xff) | ((($dport) & 0xff) << 8));
    printf("%-8d %-16s ", pid, comm);
    printf("%-39s %-6d %-39s %-6d
", $saddr, $lport, $daddr, $dport);
  }
}

这样就达到了笔者的目标。这只是ebpf应用的一个简单的场景,更多的metric采集内容仍在进行。
以上,周末愉快!

Licensed under CC BY-NC-SA 4.0
Hello, World!
使用 Hugo 构建
主题 StackJimmy 设计