对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的控制信息区分。。。。。读取数据包文的时候也会带上控制信息这样就可以知道对端了
对udp dns的一次思考的更多相关文章
- 对udp dns的思考2
上一篇文章写道了udp 使用reuseport 多线程编程!!! 但是有几个问题需要考虑一下: 之前hash使用sip sport dip dport为key, 很正常同一个客户端回hash到同一个s ...
- nodejs的某些api~(四)udp&dns
今天记udp/数据报套接字和dns. udp UDP/数据报套接字 => require('dgram');dgram.createServer([type],[cb]);type:可以是'ud ...
- 一次误报引发的DNS检测方案的思考:DNS隧道检测平民解决方案
摘自:http://www.freebuf.com/articles/network/149328.html 通过以上分析得出监控需要关注的几个要素:长域名.频率.txt类型.终端是否对解析ip发起访 ...
- ss客户端以及tcp,udp,dns代理ss-tproxy本地安装版--centos7.3 x64以上(7.3-7.6x64测试通过)
因为下载的文件,从cn下载很慢,或者下不动,所以我弄了一个本地安装版 本地安装的文件,我是从网上单独下载了,这里就不提供了. 记得在最后设置允许访问的局域网IP段 ## iptables 配置ipta ...
- ss客户端以及tcp,udp,dns代理ss-tproxy在线安装版--centos7.3 x64以上(7.3-7.6x64测试通过)
#!/bin/sh # # Script for automatic setup of an SS-TPROXY server on CentOS 7.3 Minimal. # export PATH ...
- DNS分别在什么情况下使用UDP和TCP
DNS同时占用UDP和TCP端口53是公认的,这种单个应用协议同时使用两种传输协议的情况在TCP/IP栈也算是个另类.但很少有人知道DNS分别在什么情况下使用这两种协议. 如果用wiresha ...
- DNS用的是TCP协议还是UDP协议
DNS占用53号端口,同时使用TCP和UDP协议.那么DNS在什么情况下使用这两种协议? DNS在区域传输的时候使用TCP协议,其他时候使用UDP协议. DNS区域传输的时候使用TCP协议: 1.辅域 ...
- 没错,请求DNS服务器还可以使用UDP协议
目录 简介 搭建netty客户端 在netty中发送DNS查询请求 DNS消息的处理 总结 简介 之前我们讲到了如何在netty中构建client向DNS服务器进行域名解析请求.使用的是最常见的TCP ...
- TCP/UDP端口列表
http://zh.wikipedia.org/wiki/TCP/UDP%E7%AB%AF%E5%8F%A3%E5%88%97%E8%A1%A8 TCP/UDP端口列表 本条目可通过翻译外语维 ...
随机推荐
- Ztree树节点应用
树节点增删改查: 前台jsp页面: <% String root=request.getContextPath();//获取项目目录 %> <SCRIPT type="te ...
- linux(centos8):基于java13安装rocketmq-4.7.1(解决jdk不兼容的报错)
一,Rocketmq是什么? 1, RocketMQ是一个队列模型的消息中间件,具有高性能.高可靠.高实时.分布式特点 相比kafka,rocketmq的实时性更强 2,官方网站: http://ro ...
- springboot入门系列(五):SpringBoot连接多RabbitMQ源
SpringBoot连接多RabbitMQ源 在实际开发中,很多场景需要异步处理,这时就需要用到RabbitMQ,而且随着场景的增多程序可能需要连接多个RabbitMQ.SpringBoot本身提供了 ...
- 【手摸手,带你搭建前后端分离商城系统】03 整合Spring Security token 实现方案,完成主业务登录
[手摸手,带你搭建前后端分离商城系统]03 整合Spring Security token 实现方案,完成主业务登录 上节里面,我们已经将基本的前端 VUE + Element UI 整合到了一起.并 ...
- 实战三:将nacos作为配置中心
一,引入nacos配置中心依赖 <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId&g ...
- Vue 学习第二部
目录 通过axios实现数据请求 json json数据的语法 js中是提供的接送数据转换方法 ajax 数据接口 ajax的使用 同源策略 ajax跨域(跨源)方案之cors 组件化开发 组件[co ...
- 重新开始记录java教程
最近在工作上学到了很多的知识和经验,以后每天都来记录给博客园上面
- socket 参考文档
socket.io 中文手册,socket.io 中文文档转载于:http://www.cnblogs.com/xiezhengcai/p/3956401.html 服务端 io.on('connec ...
- 栈的C++实现
数据结构c++实现系列第一篇. 话不多说,直接上代码. sichstack.h (头文件) 1 #pragma once 2 #include<string> 3 4 namespace ...
- Mongodb命令 --- MongoDB基础用法(二)
Mongodb命令 数据库操作 创建数据库 MongoDB 创建数据库的语法格式如下: use DATABASE_NAME 如果数据库不存在,则创建数据库,否则切换到指定数据库. 删除数据库 Mong ...