A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

祝小康

初级黑马

  • 黑马币:35

  • 帖子:12

  • 精华:0

© 祝小康 初级黑马   /  2019-4-4 10:07  /  872 人查看  /  0 人回复  /   0 人收藏 转载请遵从CC协议 禁止商业使用本文

人们几乎已经逼近了单CPU的处理时延极限,于是人们希望通过多CPU的方式来提高处理带宽,从而得到更多的处理容量,理论上讲,这无可厚非,但现实中,这太难了。

几乎所有上世纪70年代以来的操作系统都不是为多核CPU并行编程而设计的,因此当它们遇到多核CPU的各种问题时,无一不是东填西补,最终情况依然不容乐观。这里说一个典型的,就是Linux内核协议栈的可伸缩性(scalable)问题,本文主要描述TCP新建连接方面的一个可伸缩性优化措施。

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

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

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

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

我们来看一下如何进一步拆解上面两个问题。本文主要描述如何把锁进行更加细粒度的拆解,下一篇文章聊聊cache相关的内容。

单一accept队列问题的解锁

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

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

我是在2015年中接触到这个项目的,当时感觉这种实现非常棒。

单一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;
}
1
可以看到,在当前的Linux TCP实现中,每一个hash bucket拥有一个spinlock,其实粒度已经够细了,参见我下面的文章:
Linux socket hash查找的持续优化历程:https://blog.csdn.net/dog250/article/details/80490859
在以往的年代,这里的性能更加糟糕!上述代码是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;

}

0 个回复

您需要登录后才可以回帖 登录 | 加入黑马