zoukankan      html  css  js  c++  java
  • 对udp dns的一次思考

      目前昨天查一个线上问题:“”dns服务器在我们的设备, 有大量的终端到设备上请求解析域名,但是一直是单线程,dns报文处理不过来”, 然而设备是多核,dns服务器一直不能利用多核资源,所以能不能使用多线程进行处理呢?

      udp不像tcp那样,udp没有连接的概念,也就是没有通过建立多个连接来提高对dns服务器并发访问,然而在多核环境下那就只能通过多线程来访问一个共享的udp socket,但是还是一个socket , 会涉及到多线程抢占资源问题。

      来看一下内核协议栈udp收到包代码:根据以前分析tcpip协议栈文章可以知道,报文在内核协议栈流程大约如下:

    •   netif_receive_skb
    •   pt_prev->func(skb, skb->dev, pt_prev, orig_dev);   调用ip_rcv  arp_rcv处理
    •         ip_rcv  ------- ip_rcv_finish
    •         根据路由选项是否local_input还是ip_forward准发
    •        ip_local_deliver(期间会涉及到ip 分片重组)
    •        ip_local_deliver_finish   (涉及到raw socket 收包问题)------>ret = ipprot->handler(skb);//这里面会进入udp tcp传输层
    •      udp_rcv  //收发包处理位置----->__udp4_lib_rcv
    •    sk = __udp4_lib_lookup; ////根据报文的端口号和目的端口号查询udptable,寻找应该接收该数据包的传输控制块
    •    udp_queue_rcv_skb//涉及到内核态和 用户态抢占socket的问题, 所以有收包队列 以及 sk_add_backlog队列
    •   sk->sk_data_ready(sk, skb_len) wake up 唤醒等待队列,
    •    如果找不到对应监听socket 则icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0); 回复不可达 这个和tcp回复的rst不一样!!!!

    也就是 报文送到那个socket是由__udp4_lib_lookup这个函数做出选择, 选择的依据是 ip 端口号 接口来进行处理选择对应的socket,对于

    dhcp dns服务器来说一般不会绑定接口,所以一般就是 设置ip  udp.port, 所以内核选择socket的时候一般也是通过对比ip port来查找。通过找出匹配层度最高的socket作为收包sk。

    如果要是允许有多个socket呢??

    那么不就是可以通过轮询选择或者hash选择出对应的socket 吗!!!!!!!! 所以在linux 3.9内核版本后面增加reuseport,允许多个socket绑定同一个ip port, 通过hash散列在桶里面,

    后面就允许多线程/多进程服务器的每个线程都bind 同一个端口,并且最终每个线程拥有一个独立的socket,而不是所有线程都访问一个socket;没有reuseport这个patch的话,这么做的后果就是服务器会报出一个类似“地址/端口被占用的”错误信息。

    socket  bind ip port时 会调用get_port  计算是否存在ip port存在冲突, linux 3.9patch中对hash 以及计算方式加入reuseport,可以允许多个socket bind同样的ip port。

    同一个客户端的数据总是分配给同一个 udp_sock。so!! 在写 UDP server 的时候,为了提高处理能力,可以起多个线程,每个线程读写自己的 UDP socket

    顺便看一看udp 怎样查找port:

    int udp_v4_get_port(struct sock *sk, unsigned short snum)
    {
        unsigned int hash2_nulladdr =
            udp4_portaddr_hash(sock_net(sk), htonl(INADDR_ANY), snum);
        unsigned int hash2_partial =
            udp4_portaddr_hash(sock_net(sk), inet_sk(sk)->inet_rcv_saddr, 0);
    
        /* precompute partial secondary hash */
        udp_sk(sk)->udp_portaddr_hash = hash2_partial;
        return udp_lib_get_port(sk, snum, ipv4_rcv_saddr_equal, hash2_nulladdr);
    }

    UDP 对于porttable维护一个是一port 进行hash  一个是以sip +port(port=0) 进行hash。

    struct udp_sock {
        /* inet_sock has to be the first member */
        struct inet_sock inet;
    #define udp_port_hash        inet.sk.__sk_common.skc_u16hashes[0]
    #define udp_portaddr_hash    inet.sk.__sk_common.skc_u16hashes[1]

    }

    UDP 协议的主要数据结构是两张 hash 表,指向 UDP 协议控制块 struct udp_sock。其中 hash1 以 port 为 key,

    hash2 以 IP+port (port=0)为 key 但是后续会使用udp_portaddr_hash  ^port 进行hash查找,实际上也就是ip+port表 ;

    所以一开始看的udp_portaddr_hash 是以ip+port=0 进行hash计算有点懵逼!!!

      1 int udp_lib_get_port(struct sock *sk, unsigned short snum,
      2              int (*saddr_comp)(const struct sock *sk1,
      3                        const struct sock *sk2,
      4                        bool match_wildcard),
      5              unsigned int hash2_nulladdr)
      6 {
      7     struct udp_hslot *hslot, *hslot2;
      8     struct udp_table *udptable = sk->sk_prot->h.udp_table;
      9     int    error = 1;
     10     struct net *net = sock_net(sk);
     11 
     12     if (!snum) {
         ............................
    ...........................
    51 } else {
    //以portnum 为key查找散列表
    52 hslot = udp_hashslot(udptable, net, snum); 53 spin_lock_bh(&hslot->lock); 54 if (hslot->count > 10) {//当端口hash表冲突链长度大于10时,启用二元组hash查询 55 int exist; 56 unsigned int slot2 = udp_sk(sk)->udp_portaddr_hash ^ snum;//相当于ip+port 选择key值 57 58 slot2 &= udptable->mask; 59 hash2_nulladdr &= udptable->mask; 60 61 hslot2 = udp_hashslot2(udptable, slot2); 62 if (hslot->count < hslot2->count)//如果hslot2项的冲突数目比hslot还多,那么查找hash2表inuse2是不划算的,返回直接查找hash1表inuse 63 goto scan_primary_hash; 64 65 exist = udp_lib_lport_inuse2(net, snum, hslot2, 66 sk, saddr_comp);//使用udp_lib_lport_inuse2()查找是否有匹配项;如果没有找到,则使用新的键值hash2_nulladdr,即[INADDR_ANY, snum]从hash2中取出表项 67 if (!exist && (hash2_nulladdr != slot2)) { 68 hslot2 = udp_hashslot2(udptable, hash2_nulladdr); 69 exist = udp_lib_lport_inuse2(net, snum, hslot2, 70 sk, saddr_comp);//再使用udp_lib_lport_inuse2()查找是否有匹配 71 } 72 if (exist) 73 goto fail_unlock; 74 else 75 goto found; 76 } 77 scan_primary_hash://scan_primary_hash代码段是在hash表的hslot项中查找,只有当在hash2中查找更费时时才会执行 78 if (udp_lib_lport_inuse(net, snum, hslot, NULL, sk, 79 saddr_comp, 0)) 80 goto fail_unlock; 81 } 82 found://执行sk的插入操作 83 inet_sk(sk)->inet_num = snum; 84 udp_sk(sk)->udp_port_hash = snum; 85 udp_sk(sk)->udp_portaddr_hash ^= snum; 86 if (sk_unhashed(sk)) { 87 if (sk->sk_reuseport && 88 udp_reuseport_add_sock(sk, hslot, saddr_comp)) { 89 inet_sk(sk)->inet_num = 0; 90 udp_sk(sk)->udp_port_hash = 0; 91 udp_sk(sk)->udp_portaddr_hash ^= snum; 92 goto fail_unlock; 93 } 94 95 sk_add_node_rcu(sk, &hslot->head); 96 hslot->count++; 97 sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1); 98 99 hslot2 = udp_hashslot2(udptable, udp_sk(sk)->udp_portaddr_hash); 100 spin_lock(&hslot2->lock); 101 if (IS_ENABLED(CONFIG_IPV6) && sk->sk_reuseport && 102 sk->sk_family == AF_INET6) 103 hlist_add_tail_rcu(&udp_sk(sk)->udp_portaddr_node, 104 &hslot2->head); 105 else 106 hlist_add_head_rcu(&udp_sk(sk)->udp_portaddr_node, 107 &hslot2->head); 108 hslot2->count++; 109 spin_unlock(&hslot2->lock); 110 } 111 sock_set_flag(sk, SOCK_RCU_FREE); 112 error = 0; 113 fail_unlock: 114 spin_unlock_bh(&hslot->lock); 115 fail: 116 return error; 117 }

     如果snum==0,即没有绑定本地端口,此时执行if部分代码段,这种情况一般发生在客户端使用socket,此时内核会为它选择一个未使用的端口:

    udptable中的hash公司为 jhash_1word((__force u32)saddr, net_hash_mix(net)) ^ port------>(num + net_hash_mix(net)) & mask简写一下, net_hash_mix(net)返回为0

    所以大约就是sip^port; 具体就不细看hash函数了。也许不是这样的。。。。

    if (!snum) {
            /*
     如果snum==0,即没有绑定本地端口,此时执行if部分代码段,这种情况一般发生在客户端使用socket,
     此时内核会为它选择一个未使用的端口:
    
    udptable中的hash公司为 jhash_1word((__force u32)saddr, net_hash_mix(net)) ^ port---
    --->(num + net_hash_mix(net)) & mask简写一下, net_hash_mix(net)返回为0
    
    所以大约就是sip^port; 具体就不细看hash函数了。也许不是这样的。。。。
    
    声明bitmap数组,大小为udp_table每个键值最多存储的表项,即最大端口号/哈希表大小。
    端口号的值规定范围是1-65536,而哈希表一般大小是256,因此实际分配bitmap[8]。
    low和high代表可用本地端口的下限和上限;remaining代表位于low和high间的端口号数目。
    用随机值rand生成first,注意它是unsigned short类型,16位,表示起始查找位置;last表示终止查找位置,
    first和last相差表大小保证了所有键值都会被查询一次(表的大小为偶数,所以用奇数防止落在同一个桶里面)。
    随机值rand最后处理成哈希表大小的奇数倍,
    之所以要是奇数倍,是为了保证哈希到同一个键值的所有端口号都能被遍历,
    可以试着1开始,每次+2和每次+3,直到回到1,所遍历的数有哪些不同,
            */
            int low, high, remaining;
            unsigned int rand;
            unsigned short first, last;
            //hash表达小一般是256
            DECLARE_BITMAP(bitmap, PORTS_PER_CHAIN);//PORTS_PER_CHAIN (MAX_UDP_PORTS / UDP_HTABLE_SIZE_MIN)----65536/256
    
            inet_get_local_port_range(net, &low, &high);
            remaining = (high - low) + 1;
    
            rand = prandom_u32();
            first = reciprocal_scale(rand, remaining) + low;
            /*
             * force rand to be an odd multiple of UDP_HTABLE_SIZE
             */
            rand = (rand | 1) * (udptable->mask + 1);
            last = first + udptable->mask + 1;
            do {/* 使用first值作为端口号,从udptable的hash表中找到hslot项,重置bitmap数组全0,
            调用函数udp_lib_lport_inuse()遍历hslot项的所有表项,将所有已经使用的sport对应于bitmap的位置置1。*/
                hslot = udp_hashslot(udptable, net, first);
                bitmap_zero(bitmap, PORTS_PER_CHAIN);
                spin_lock_bh(&hslot->lock);
                udp_lib_lport_inuse(net, snum, hslot, bitmap, sk,
                            saddr_comp, udptable->log);
    
                snum = first;
                /*
                 * Iterate on all possible values of snum for this hash.
                 * Using steps of an odd multiple of UDP_HTABLE_SIZE
                 * give us randomization and full range coverage.
                 此时bitmap中包含了所有哈希到hslot的端口的使用情况,下面要做的就是从first位置开始,
                 每次递增rand(保证哈希值不变),查找符合条件的端口:端口在low~high的可用范围内;
                 端口还没有被占用。
                 do{}while循环的判断条件snum!=first和snum+=rand一起保证了所有哈希到hslot的端口号都会被遍历到。
                 如果找到了可用端口号,即跳出,执行插入sk的操作,否则++first,
                 查找下一个键值,直到fisrt==last,表明所有键值都已轮循一遍,仍没有结果,则退出
                 */
                do {
                    if (low <= snum && snum <= high &&
                        !test_bit(snum >> udptable->log, bitmap) &&
                        !inet_is_local_reserved_port(net, snum))
                        goto found;
                    snum += rand;
                } while (snum != first);
                spin_unlock_bh(&hslot->lock);
            } while (++first != last);
            goto fail;

    这部分是查找网上分析结果!!还没有仔细研究他的这个hash算法

    问题2:由于udp是包模式!! 每次只能copy一个包!!!能不能copy多个!!!目前是可以的!!

    可以使用recvmmsg来继续降低系统调用的开销。recvmmsg是一个批量接口,它可以从socket里一次读出多个udp数据包,不像recvfrom那样一次只能读一个。如果客户端多、请求量大的话,recvmmsg的批量读就很有优势了。

    就像hash表一样存在冲突!!! 也就是读取的多个包可能不是一个客户端发过来的数据!!!! 那怎样区分多个客户端发来的数据呢??在udp数据部分来区分??还是??

    看内核代码应该是通过rcvmmsg的控制信息区分。。。。。读取数据包文的时候也会带上控制信息这样就可以知道对端了

  • 相关阅读:
    jquery效果,多个div,点击任何一个div,那么这个div会切换文字,变换背景颜色,再次点击其他的div ,这个div会发生刚才的变化,之前点击的div的颜色会变回来
    用js动态的改变img标签里面的src属性实现图片的循环切换
    清除浮动
    清除浮动clearfix
    转移符 个人工作中使用记录一下
    12.Django数据库操作(执行原生SQL)
    11.Django数据库操作(查)
    10.Django数据库操作(增删改)
    9.Django里的数据同步migrations命令
    8.Django模型类例子
  • 原文地址:https://www.cnblogs.com/codestack/p/12774946.html
Copyright © 2011-2022 走看看