百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 优雅编程 > 正文

优化多核CPU的TCP新建连接性能--重排spinlock

sinye56 2024-12-15 16:36 16 浏览 0 评论

传统上讲,Linux内核协议栈针对同一个Listener的TCP新建连接处理主要拥有两个瓶颈点:

  • 单一的accept队列
  • 单一的hash表(其实是两张,listener hash,establish hash)

TCP的新建连接会频繁操作上述两个数据结构,在多核CPU情况(后面简称SMP)下,为了保证数据的一致性,lock是绕不开的。不管多少个并行处理的CPU,在TCP新建连接时,必然要在操作上述两个数据结构时被串行化!这是悲哀的。

我们知道,随着CPU核数的增多,每秒能接纳的连接请求数也会随着增多,但由于上述两个串行化点的存在,这意味着lock冲突也会相应的增多!串行化的lock冲突意味着什么?请考虑地铁站入口,人们从多个大门涌入,最终却只有一个安检点,过了这个安检点又呈现了多个闸机…

最终,随着CPU核数的增多,性能并没有能线性地增长,最终的CPU核数/性能曲线便呈现了一种上凸的趋势。这一切都是因为锁。

我们来看一下如何进一步拆解上面两个问题。

单一accept队列问题的解锁

非常幸运,这个问题已经被google的reuseport机制解决了。详情请自行搜索reuseport相关的资料。

值得一提的是,新浪的fastsocket在google的reuseport机制基础上做了一个比较优雅的封装,使得应用程序不用修改就能享受到reuseport的收益,同时进一步地提高了TCP连接的可伸缩性问题。

单一establish hash表问题的解锁

根据我上周的压测,CPS数据获取过程中,短链接会频繁操作establish hash表,频繁调用inet_hash,inet_unhash两个函数(listener hash并不必在意,因为listener socket比较稳定,不会频繁生成和销毁),其中的热点在两个spinlock:

bool inet_ehash_insert(struct sock *sk, struct sock *osk)
{
    struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
    struct hlist_nulls_head *list;
    struct inet_ehash_bucket *head;
    spinlock_t *lock;
    bool ret = true;
    WARN_ON_ONCE(!sk_unhashed(sk));
    sk->sk_hash = sk_ehashfn(sk);
    head = inet_ehash_bucket(hashinfo, sk->sk_hash);
    list = &head->chain;
    // 以hash bucket来lock!!
    lock = inet_ehash_lockp(hashinfo, sk->sk_hash);
    spin_lock(lock); // 串行化lock
    if (osk) {
        WARN_ON_ONCE(sk->sk_hash != osk->sk_hash);
        ret = sk_nulls_del_node_init_rcu(osk);
    }
    if (ret)
        __sk_nulls_add_node_rcu(sk, list);
    spin_unlock(lock);
    return ret;
}

可以看到,在当前的Linux TCP实现中,每一个hash bucket拥有一个spinlock,其实粒度已经够细了.

在以往的年代,这里的性能更加糟糕!上述代码是4.14内核,几乎就是最新的版本了,我们看一下它的示意图:

上图的窘局其实是可以破解的,只需要把per slot的spinlock再做细分即可,改为per slot per CPU的spinlock,其实就是把每一个slot的链表摊开成per cpu的即可。这里决定一个socket应该给哪个CPU先使用一个最简单的策略,即调用inet_hash的时候哪个CPU在处理,就给哪个CPU。

为此,我们需要修改下面的数据结构:

struct inet_ehash_bucket {
    struct hlist_nulls_head chain;
};

这个数据结构便是上图中slot,我们需要将其改成:

struct inet_ehash_bucket {
    // struct hlist_nulls_head chain[NR_CPUS]
    struct hlist_nulls_head *chain;
};

我们稍微修改一下insert函数:

bool inet_ehash_insert(struct sock *sk, struct sock *osk)
{
    struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
    struct hlist_nulls_head *list;
    struct inet_ehash_bucket *head;
    spinlock_t *lock;
    bool ret = true;
    // 取当前CPU!
    int cpu = smp_processor_id();
    WARN_ON_ONCE(!sk_unhashed(sk));
    sk->sk_hash = sk_ehashfn(sk);
    sk->sk_hashcpu = cpu;
    head = inet_ehash_bucket(hashinfo, sk->sk_hash);
    // 取出对应CPU的list
    head = &head[cpu];
    list = &head->chain;
    lock = inet_ehash_lockp(hashinfo, sk->sk_hash);
    // 取出对应CPU的lock
    lock = &lock[cpu];
    spin_lock(lock);
    if (osk) {
        WARN_ON_ONCE(sk->sk_hash != osk->sk_hash);
        ret = sk_nulls_del_node_init_rcu(osk);
    }
    if (ret)
        __sk_nulls_add_node_rcu(sk, list);
    spin_unlock(lock);
    return ret;
}

是不是简单快捷呢?对应的lookup也要修改,在lookup的过程中,不再recheck slot的一致性,而要recheck CPU的一致性:

struct sock *__inet_lookup_established(struct net *net,
                  struct inet_hashinfo *hashinfo,
                  const __be32 saddr, const __be16 sport,
                  const __be32 daddr, const u16 hnum,
                  const int dif, const int sdif)
{
    INET_ADDR_COOKIE(acookie, saddr, daddr);
    const __portpair ports = INET_COMBINED_PORTS(sport, hnum);
    struct sock *sk;
    const struct hlist_nulls_node *node;
    unsigned int hash = inet_ehashfn(net, daddr, hnum, saddr, sport);
    unsigned int slot = hash & hashinfo->ehash_mask;
    struct inet_ehash_bucket *head = &hashinfo->ehash[slot];
    int cpu = smp_processor_id(), self; // 从当前CPU开始!如果底层有做CPU绑定的话,这样做就对了。
    self = cpu;
begin:
    head = &head[cpu];
    if (hlist_nulls_empty(&head->chain)) {
        goto recheck2;
    }
    sk_nulls_for_each_rcu(sk, node, &head->chain) {
        ... // 逻辑不变,省略
    }
    if (get_nulls_value(node) != cpu) {
        cpu = 0;
        goto begin;
    } else if (get_nulls_value(node) == cpu) {
recheck2:
        cpu ++;
        if (cpu >= nr_cpu_ids)
            cpu = 0;
        if (cpu == self)
            goto out;
        goto begin;
    }
out:
    sk = NULL;
found:
    return sk;
}

同时,ehash的每一个slot在初始化的时候,都要初始化成per CPU的(当然,我这里还没有用per CPU的API),并且把hlist的null尾用CPU id来初始化!

现在让我们看看采用per slot per CPU的新方案后,局面在观感上变成了什么样子:

我们知道,spinlock是不可睡眠的,除了被硬中断打破,所有的CPU在调用inet_hash的时候,几乎都是可以无竞争不自旋立即完成的。但是你可能注意到了,我在上文中没有提到inet_unhash的调用,我们知道,unhash的时候也是要持有spinlock的,如何来保证unhash的调用者和当初hash的调用者是同一个CPU呢?

答案显然是不能保证,因此正如nf_conntrack里unconfirm list和dying list的per cpu处理那般,在调用unhash的时候,cpu变量必须从socket里面取出来:

void inet_unhash(struct sock *sk)
{
    struct inet_hashinfo *hashinfo = sk->sk_prot->h.hashinfo;
    spinlock_t *lock;
    bool listener = false;
    int done;
    if (sk_unhashed(sk))
        return;
    if (sk->sk_state == TCP_LISTEN) {
        lock = &hashinfo->listening_hash[inet_sk_listen_hashfn(sk)].lock;
        listener = true;
    } else {
        // 取出hash时的cpu,确保从哪里insert就从哪里remove时而一致性。
        int cpu = sk->sk_hashcpu;
        if (cpu != smp_processor_id()) {
            DEBUG("Shit!:%d", misstat++);
        }
        lock = inet_ehash_lockp(hashinfo, sk->sk_hash);
        lock = &lock[cpu];
    }
    spin_lock_bh(lock);
    ...
}

现在问题来了。由于Linux调度器的调度策略影响,很有可能调用unhash时的CPU已经不是当初调用hash时的那个CPU了,最终在别的CPU上处理的unhash过程还是可能和其它一个调用hash过程的CPU竞争同一把锁。然而这是没有办法的,调度器不属于协议栈的范畴,我们能做的,仅仅是避免这种情况的发生,比如通过外部的机制或者工具,对进程和CPU进行强绑定或者弱绑定,尽最大的努力避免进程在CPU之间乒乓!

需要C/C++ Linux服务器架构师学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

相关推荐

一个不错的软件版本命名规范!

之前写了一篇如何自动生成版本号的文章,《让你的C程序,自动打印版本信息》初衷是让自己的程序在运行时自动打印与版本相关的信息,避免测试时因为版本信息不确定导致的一些功能对应不上去的问题,当时留了一个坑,...

国产操作系统迎来发展风口 公务领域更能培育起Linux生态

谷歌和微软在俄罗斯市场的一番套路猛如虎,就让我们深刻地意识到了,只有自己的东西才能靠得住。也由此,国内操作系统发展迎来了发展风口。我就看到有朋友就秀出了他们单位采购的纯国产的主机,一款华为的主机,纯国...

5个大有“前途”的Linux桌面发行版本

ZD至顶网CIO与应用频道08月27日专栏:Linux无处不在。你的服务器里,你的电话、汽车、手表、烤面包机、冰箱……和台式机里都有Linux的身影。虽然在桌面上见到Linux的用户比在自动调温...

Linux 常用应用软件大全

编译自:https://www.fossmint.com/most-used-linux-applications/作者:MartinsD.Okoi译者:HankChow对于许多应用程序...

Linux 4.1 系列的最大版本 4.1.18 LTS发布,带来大量修改

(LCTT译注:这是一则过期的消息,但是为了披露更新内容,还是发布出来给大家参考)著名的内核维护者GregKroah-Hartman貌似正在度假中,因为SashaLevin2016年2月16日的...

Linux发行版需要杀软吗?卡巴斯基推出免费KVRT病毒扫描清理工具

IT之家6月4日消息,你认为使用Linux发行版,需要杀毒软件吗?或许很多用户认为Linux发行版偏小众,因此受到黑客攻击的风险也相对较小,不过卡巴斯基并不这么认为,近期推出了适用于...

适合开发人员的 5款 Linux 发行版

什么是Linux?Linux是基于Unix的操作系统。由LinusTorvalds开发于1991年首次发布其内核。因为Linux是开源软件,其发行版由不同组织发布,因此不同的发行版具有不同的风格...

VMware Workstation 17.0 Pro 发布:新增 TPM 2.0 完美兼容Win11

IT之家11月18日消息,VMwareWorkstation17.0Pro现已发布,它带来了许多新特性,例如微软Windows11硬性要求:虚拟可信平台模块(TPM)2.0。...

你是否需要一个容器专用的Linux发行版本?

单单使用容器是不够的,提供商们认为你需要一个容器专用的Linux发行版本。我们可以让容器在不同的操作系统上运行,不同的操作系统都有自己的虚拟化服务,如:SolarisZones、BSDJails、...

Tizen 3.0版本发布 采用Linux 4.1内核

2015-09-2111:31:39作者:马荣【中关村在线软件资讯】9月21日消息:尽管三星靠着Android系统设备在移动市场赚钱,但是仍然没有忘记自家的Tizen开发。现在Tizen3.0版...

欧拉操作系统演进:应用累计超130万套 支持鲲鹏、英特尔、飞腾等芯片

21世纪经济报道记者倪雨晴深圳报道4月15日,在欧拉开发者大会(openEulerDeveloperDay2022)的主论坛上,欧拉首个数字基础设施全场景长周期版openEuler22.03...

Papyros:以Material Design为灵感的Linux发行版本

项目团队并不希望只是采用传统的桌面主题,而是致敬谷歌Android系统的MaterialDesign设计语言想要打造出某些不同以往足够吸引用户的Linux发行版本,自然该版本还在不断的更新和改进中,...

比特网早报:全国空间计量技术委员会成立,银河麒麟操作系统上架微信Linux4.0.0版本

2024年11月6日消息,昨夜今晨,科技圈都发生了哪些大事?行业大咖抛出了哪些新的观点?比特网为您带来值得关注的科技资讯:全国空间计量技术委员会在北京成立近日,经市场监管总局批准,全国空间计量技术委员...

2024年最稳定的5个Linux发行版,赶紧收藏!

Linux是最流行的免费开源平台之一。Linux已被广泛使用,因为它安全、可扩展和灵活。Linux发行版收集开源代码,对其进行编译,并将其组合成一个可以轻松启动和安装的操作系统。它们还提供不同的...

彰显Linux生态繁华,Ubuntu、Fedora等四发行版同时发布新版本

上周对于开源社区来说是忙碌的一周。EndeavourOS和TrueNASScale于周二(4月16日)发布,Fedora于周三(4月17日)发布,Ubuntu于周四(4月18日)发布。四个新版本中都...

取消回复欢迎 发表评论: