reuseport socket 查找的一致性 hash

我吐槽过,reuseport socket 没做一致性 hash,socket 新建或者退出时,会对所有 session 产生影响:

  • 新建 socket 会追加到 group 最后,增加组员数量 num,进而影响 hvalue % num 结果;
  • socket 退出时会用 group 最后一个 socket 替换退出者的 index,递减组员数量,影响 hash 结果。

要做一致性 hash,需要单独写 ebpf 程序 attach 到 reuseport lookup 的位置,但要想实现这个 ebpf 程序其实也麻烦,因为相当于你要手工跟踪和维护 socket 的 slot index,比方说 session k 会映射到 index = m 的一个socket m,但由于某个 socket 退出,socket m 的 index 发生了变化,要想捕捉到这个变化以重建映射,需要监控 socket 退出事件并持续跟踪 socket m 的原始 slot index,这件事可做,但非常繁琐。

还是老方法,麻烦的事情需要拿点东西来换。这次拿点 standby 或占位符来换。

如果我需要在 n 个 reuseport socket 之间做一致性 hash 映射,我会创建 n + d 个 reuseport socket,k 可以从 1 到 n 不等,然后我的 ebpf 始终只做 index = hvalue % n 即可,这种情况下,即使 n 个 socket 中有一个挂了,后面那 d 个 standby socket 的最后一个马上就填充了退出者的位置,接管服务,可谓透明无感。
在这里插入图片描述

核心思想是,由 ebpf 程序自行控制从哪里到哪里进行 hash,而不是有多少算多少,全部参与运算。示例代码如下:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>

#define FIRST_N 4

SEC("sk_reuseport")
int reuseport_prog(struct sk_reuseport_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    struct ethhdr *eth = data;
    if (data + sizeof(*eth) > data_end)
        return SK_DROP;

    data += sizeof(*eth);
    struct iphdr *ip = data;
    if (data + sizeof(*ip) > data_end)
        return SK_DROP;

    data += ip->ihl * 4;
    struct udphdr *udp = data;
    if (data + sizeof(*udp) > data_end)
        return SK_DROP;

    __u64 hash = ip->saddr;
    hash = hash * 31 + ip->daddr;
    hash = hash * 31 + udp->source;
    hash = hash * 31 + udp->dest;

    return hash % FIRST_N;
}

这个一致性 hash 策略也将大大降低昨天我那个热升级方案的复杂性。假设旧版本服务有 m 个 reuseport socket,新版本 socket 有 n 个,我在升级的时候,会创建 n + m 个 reuseport socket,其中后面 m 个并不提供任何服务,它们只是在前面旧版本服务退出后占位符,等到旧版本服务全部退出后,它们也将按顺序集体退出,这样真正的新版本服务 socket index 只需要一次新集体平移 m 个单位即可:
在这里插入图片描述

唯一的问题还是需要服务自行跟踪和维护自己的 index,所以 reuseport 至少还缺一个 getsockopt 调用:SO_REUSEPORT_GETINDEX:

void get_reuseport_index(int sockfd) 
{
    int index;
    socklen_t len = sizeof(index);
    
    if (getsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT_GETINDEX, &index, &len) == 0) {
        printf("This socket is at index %d in the reuseport group\n", index);
    } else {
        perror("getsockopt(SO_REUSEPORT_GETINDEX) failed");
    }
} 

否则,就要通过下面的非常规手段获得进程 pid,socket index 的关系:

// 运行方法 bpftrace ./trace.bt

kprobe:reuseport_add_sock
{
    $pid = pid;
    $sk = (struct sock *)arg1;
    $port = (($sk->__sk_common.skc_num & 0xff) << 8) | (($sk->__sk_common.skc_num >> 8) & 0xff);
    $reuseport = (struct sock_reuseport *)($sk->sk_reuseport_cb);
    printf("add sock PID: %d, Port: %d, Sock:%p\n", $pid, $port, $sk);
    printf("curr num_socks and this index is %d \n", $reuseport->num_socks);
}

kprobe:reuseport_detach_sock
{
    $pid = pid;
    $sk = (struct sock *)arg0;
    $port = (($sk->__sk_common.skc_num & 0xff) << 8) | (($sk->__sk_common.skc_num >> 8) & 0xff);
    $reuseport = (struct sock_reuseport *)($sk->sk_reuseport_cb);
    printf("remove sock PID: %d, Port: %d, Sock:%p\n", $pid, $port, $sk);
    printf("curr num_socks and this index is %d \n", $reuseport->num_socks);
}

这些关系被打印出来只是在这篇文章中被看清,实际上应该保存起来备用,在 SO_REUSEPORT_GETINDEX 被支持之前,这个信息非常重要。

注意,下面的方法不能保证导出的顺序就是 reuseport socket group 的 index 顺序,慎用!甚至 proc/$pid/fd 的链接时间戳都不好使:

root@vbox:/proc/net# cat udp
   sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode ref pointer drops
  384: 00000000:3039 00000000:0000 07 00000000:00000000 00:00000000 00000000     0        0 16567 2 00000000c107c9c0 0
  384: 00000000:3039 00000000:0000 07 00000000:00000000 00:00000000 00000000     0        0 15892 2 00000000023d3ee4 0
root@vbox:~# ls -l /proc/1000/fd
total 0
lrwx------ 1 root root 64 Mar 31 21:30 0 -> /dev/pts/0
lrwx------ 1 root root 64 Mar 31 21:30 1 -> /dev/pts/0
lrwx------ 1 root root 64 Mar 31 21:30 2 -> /dev/pts/0
lrwx------ 1 root root 64 Mar 31 21:30 3 -> 'socket:[15892]' 

浙江温州皮鞋湿,下雨进水不会胖。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值