再谈 TCP 连接的源端口选择

TCP 源端口的选择有两个场景:

  • 主机场景
  • SNAT 场景

先看主机场景,其中又区分了两类互斥的场景:

  • bind 时选端口:如果 bind 的端口被某条 established 连接使用,则无法 bind;
  • connect 时选端口:如果端口已经被 bind,则无法在 connect 时被选中。

因此必须在 bind 和 connect 之间以及 bind,connect 彼此之间做互斥,遍历某端口的使用链表(bind 只涉及 2 元组,因此只涉及 port hash list)时需要 lock,由于一台主机可能有多个进程同时进行 bind 和 connect,为提高并发能力,Linux 整了个花活儿:

  • bind 先遍历奇数端口,再遍历偶数端口;
  • connect 先遍历偶数端口,再遍历奇数端口。

这样就巧妙地将 bind 和 connect 的遍历路径错开,沿着这个思路继续,还可以更花,比如在奇数或偶数的遍历空间内,根据进程 pid 再次区分奇偶跨度,比如奇数跨度按 1,5,9,13,… 顺序遍历,而偶数跨度按 3,7,11,… 顺序遍历,诸如此类。

本着 “及时停止优化” 的哲学,审视一下花活的代价是高尚的。

在奇数(或偶数)端口号都已经用尽情况下,花活儿不但发挥不了作用,还平添遍历一边奇数空间的开销,因为在遍历一遍之前,进程并不知道奇数空间已经用尽,虽然可以记录一个 anchor 来记录 “奇数空间已经用尽” 的事实,但本质上这个问题的原因是 16bit 的端口空间在 TCP 看来太少,但对实现时需要遍历时又太大,而之所以搞这些精致的花活儿,还是 16bit 空间太小。

所以不要觉得内核妹忒内儿提的任何派驰都是优化,相当大一部分是在恶心人,它们只是针对了场景,而在这些假设之外,性能反而劣化。这类似抛硬币猜正反,但凡做任何假设,总的成功率都会下降。

再看 SNAT 场景。先看一个没有花活儿的实现(约等于但不等于 Linux 内核 nf_conntrack SNAT 实现):

uint16 tcp_get_port(uint32 sip, unit32 dip, uint16 sport, uint16 dport)
{
        struct entry *ent;
        int anchor = random16();
        int begin = PORT_BASE;

retry:
        // 4 元组 hash 而不是 3 元组,防止访问局部性导致 bucket 链表过长
        hash = jhash(sip, sport, dip, dport, 2);
        uniq = true;
        list_for_each_from(ent, bucket[hash]) {
                if (ent->sip == sip && ent->dip == dip && ent->sport == sport && ent->dport == dport) {
                        uniq = false;;
                        break;
                }
        }
        if (uniq) {
                ent = malloc(sizeof(*ent));
                ent->sip = sip;
                ent->dip = dip;
                ent->sport = sport;
                ent->dport = dport;
                hash_insert(ent, hash);
                return sport;
        }
        if (sport - begin < K && retried ++ > MAX_RETRIES) {
                // 列维长跳
                if (retried % (MAX_RETRIES/N) == 0)
                        anchor = random16(;
                sport = anchor;
                anchor ++;
                goto retry;
        }
        return -1;
}

要整花活儿炫点技巧,一步一步来。

首先,递增 anchor 和列维长跳已够合理,人类文明的轨迹就是列维长跳的结果,典型的大杂居,小聚居对资源利用的合理分布非常有效,正因为此,中原文明才能在几千年时间同化掉整个东亚大陆。然而这太直观了,以至于不够 “厉害”。

如果不使用列维长跳来搜索可用端口,可以用时间戳(精确到 us),进程 pid 一起组成候选 sport。使用时间戳可以自然跨过同时竞争同一端口的进程,从而减少互斥开销,但还有更好的。

如果系统中已经没有可用端口,在遍历一遍前,进程并不知晓这个事实,因此要引入 “快速失败” 机制。这并不难,只需要提供一些积累信息。于是有以下的算法:

uint16 get_port_new(uint32 sip, unit32 dip, uint16 sport, uint16 dport)
{
        struct entry3 *ent3;
        uint hash3;

        hash3 = jhash(sip, dip, dport, 2);
        list_for_each_from(ent3, bucket3[hash3]) {
                if (ent3->sip == sip && ent3->dip == dip && ent3->dport == dport) {
                        if (ent3->free = ent3->end) // 已经满了,无空闲端口可用
                                return -1;
                        sport = ent3->free + 1;
                        ent3->free ++;
                        break;
                }
        }
        if (ent3 == NULL) {
                ent3 = malloc(sizeof(*ent3));
                free_port_init(ent3);
                sport = ent3->free + 1;
                hash3_insert(ent3, hash3);
        }
        return sport;
}

魔鬼在细节,太复杂必然是错的,太简单也可能不对。

随机释放端口意味着端口分配并非连续可得,因此还是要一个一个尝试,于是代码又恢复成了冗杂,3 元组 hash 只是提高了运算复杂度降低的概率,所有的重试依然还要进行:

uint16 tcp_get_port_new(uint32 sip, unit32 dip, uint16 sport, uint16 dport)
{
        struct entry3 *ent3;
        struct entry4 *ent4;
        int anchor = -1;
        int begin = PORT_BASE;
        int find = false;

        hash3 = jhash(sip, dip, dport, 2);
        list_for_each_from(ent3, bucket3[hash3]) {
                if (ent3->sip == sip && ent3->dip == dip && ent3->dport == dport) {
                        find = true;
                        uniq = false;;
                        if (ent3->full)
                                return -1;
                        anchor = ent3->free;
                        break;
                }
        }
        // 有了以上 3 元组 hash 辅助,下面的过程将省去遍历过程,几乎直接找到端口
        if (anchor == -1)
                anchor = 1;
retry:
        // 4 元组 hash 而不是 3 元组,防止访问局部性导致 bucket 链表过长
        hash4 = jhash(sip, sport, dip, dport, 2);
        uniq = true;
        list_for_each_from(ent4, bucket4[hash4]) {
                if (ent4->sip == sip && ent4->dip == dip && ent4->sport == sport && ent4->dport == dport) {
                        uniq = false;;
                        break;
                }
        }
        if (uniq) {
                ent4 = malloc(sizeof(*ent4));
                ent4->sip = sip;
                ent4->dip = dip;
                ent4->sport = sport;
                ent4->dport = dport;
                if (find == false) {
                        ent3 = malloc(sizeof(*ent3));
                        ent3->full = false;
                        ent3->end = 65535;
                        hash3_insert(ent3, hash3);
                }
                ent3->free = sport + 1;
                hash4_insert(ent4, hash4);
                return sport;
        }
        if (sport - begin < K && retried ++ > MAX_RETRIES) {
                // 列维长跳
                if (retried % (MAX_RETRIES/N) == 0)
                        anchor = random16(;
                sport = anchor;
                anchor ++;
                goto retry;
        }
        return -1;
}

合适的数据结构上场比算法更能解决大部分问题,就像设置一个 anchor 的威力一样,如果用 stack 或 queue 组织空闲端口,事情就会高尚。

使用 stack 会倾向于使用最近被释放的 port,而使用 queue 则相反,使用最早被释放的 port,以 stack 为例,意思如下:

void free_port(uint16 port, uint32 sip, unit32 dip, uint16 dport)
{
    hash3 = jhash(sip, dip, dport, 2);
    list_for_each_from(ent3, bucket3[hash3]) {
        if (ent3->sip == sip && ent3->dip == dip && ent3->dport == dport) {
            sport = ent3->free.push(ent3.port);
            break;
        }
    }
}
uint16 get_port(uint32 sip, unit32 dip, uint16 sport, uint16 dport)
{
    struct entry3 *ent3;
    struct port_entry *port;
    uint hash3;

    hash3 = jhash(sip, dip, dport, 2);
    list_for_each_from(ent3, bucket3[hash3]) {
        if (ent3->sip == sip && ent3->dip == dip && ent3->dport == dport) {
            if (ent3->stack.num == 0) // 已经满了,无空闲端口可用
                return -1;
            port = ent3->free.pop();
            break;
        }
    }
    if (port == NULL) {
        ent3 = malloc(sizeof(*ent3));
        free_port_init(ent3);
        hash3_insert(ent3, hash3);
        port = ent3->free.pop();
    } 
    return port.num;
}

只要 hash 足够好,源端口几乎可以在 O(1) 时间获得。值得注意的是,stack,queue 与 bitmap 相比具有优势,因为 bitmap 强依赖于实现,可以利用硬件高速实现,也可以使用 for-for 嵌套实现。

这些基本技巧已经谈不上是什么花活,malloc 库函数以及文件系统空闲块管理都会用类似机制管理空闲资源,没什么大不了,但 Linux 内核为什么没有使用这种技巧而遍历端口一个一个尝试呢?

首先是简单性,可维护性,这个不必多说,其次是资源占用,无论 bitmap,stack 还是 queue,动用的数据结构都对内存空间有不同需求,而通用操作系统不能假设空间需求的满足,但却可以用时间换空间。换句话说,内存不足就跑不起来了,而时间复杂度就算再高顶多就是慢,通用系统的可用性指标即程序可以完成。

想要程序运行快,除了算法分析外,局部性一定要利用:

  • 时间局部性:针对目标,访问过的网站接下来还会被访问;
  • 空间局部性:针对源,相邻 IP 地址会接连发起访问。

正如大约 10 年前我在 nf_conntrack 中加了一个不到 10 行代码的 recent cache 就获得 30%+ 的吞吐性能提升一样,局部性原理几乎控制着一切。

该总结一下了。

不管多么 trick 的算法,不管它多么高效,“及时停止优化” 永远是对的。沉迷于优美的算法将设计一个不优美的传输协议,因为你迫不及待希望用上这个算法,就很容易复制缺陷,在新协议里将 port(or channel,whatever) 继续设置为 16bit,因为只有这样你的算法才能表现出最优解。

但只要把 port(or channel,whatever) 设置成 32bit,一切问题将迎刃而解,但优美算法永远也用不上了。结构决定行为,而不是算法决定行为。

当审视本文描述的所有这一切,如果有人问你到底什么是正确的,他的目的并非要获取一个真正有价值的方案,而是要获得一个和他的想法一致的方案,因为人们总想听到自己想听到的,这是人们成功或失败的根源。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/908891.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Halcon-模板匹配(WPF)

halcon的代码 dev_open_window (0, 0, 512, 512, black, WindowHandle) read_image (Image, C:/Users/CF/Desktop/image.jpg) dev_display (Image)draw_rectangle1 (WindowHandle, Row1, Column1, Row2, Column2) gen_rectangle1 (Rectangle, Row1, Column1, Row2, Column2) r…

Day02 C++ 环境设置

2024.11.1 C 环境设置 如果您想要设置 C 语言环境&#xff0c;需要确保电脑上有以下两款可用的软件&#xff0c;文本编辑器和 C 编译器。 一、文本编辑器 通过编辑器创建的文件通常称为源文件&#xff0c;源文件包含程序源代码。 C 程序的源文件通常使用扩展名 .cpp、.cp 或…

c++拷贝构造函数

1.拷贝构造函数 拷贝构造函数的调用时机 class A { public://默认构造函数A(){m_Hp 100;cout << "A默认构造函数调用完毕" << endl;}//有参构造函数A(int hp){m_Hp hp;cout << "A有参构造函数调用完毕" << endl;}A(const A&…

Node.js 完全教程:从入门到精通

Node.js 完全教程&#xff1a;从入门到精通 Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境&#xff0c;允许开发者在服务器端使用 JavaScript。它的非阻塞 I/O 和事件驱动架构使得 Node.js 非常适合于构建高性能的网络应用。本文将详细介绍 Node.js 的安装、基本语…

C++STL——list

C教学总目录 list 1、list简介2、构造函数3、迭代器4、访问和容量函数5、修改类函数6、操作类函数 1、list简介 list是带头双向循环链表&#xff0c;也是模板类&#xff0c;使用时要指明类型&#xff0c;包含于头文件<list> 由于list是双向循环链表&#xff0c;在任意位置…

大数据计算里的-Runtime Filter

文章目录 Runtime Filter示例 Runtime Filter 从纸面意义来看&#xff0c;就是程序在运行时&#xff0c;根据实际的数据去进行一些过滤操作。这和静态的规则优化不同&#xff0c;静态优化不考虑实现的数据的分布。 示例 select a.* ,b.* a join b on a.idb.id假设一下数据…

SD教程 ControlNet 之MLSD 室内设计

你是否已经厌倦了传统的室内设计方式&#xff0c;想探索新方法来增强作品设计感&#xff1f;本期小编就同大家分享一个新武器&#xff0c;用Stable Diffusion的ControlNet来打造一个室内设计全新工作流。无论你是经验丰富的室内设计师还是初学小白&#xff0c;都将使你的日常工…

蓬勃发展:移动开发——关于软件开发你需要知道些什么

一、前言 移动开发一直都是软件开发领域中最有趣的领域之一&#xff0c;这是因为&#xff1a; 1、移动开发为“只有一个人”的开发团队提供了一个非常独特的机会&#xff0c;让他可以在相对较短的时间内建立一个实际的、可用的、有意义的应用程序&#xff1b; 2、移动开发也代…

性能测试 —— MySQL性能测试方案设计!

01、慢查询 查看是否开启慢查询 mysql> show variables like %slow%’; 如图所示&#xff1a; 系统变量log_slow_admin_statements 表示是否将慢管理语句例如ANALYZE TABLE和ALTER TABLE等记入慢查询日志 启用log_slow_extra系统变量 &#xff08;从MySQL 8.0.14开始提…

每日OJ题_牛客_爱吃素_数学_C++_Java

目录 牛客_爱吃素_数学 题目解析 C代码 Java代码 牛客_爱吃素_数学 爱吃素 (nowcoder.com) 描述&#xff1a; 牛妹是一个爱吃素的小女孩&#xff0c;所以很多素数都害怕被她吃掉。 一天&#xff0c;两个数字aaa和bbb为了防止被吃掉&#xff0c;决定和彼此相乘在一…

Vue 自定义icon组件封装SVG图标

通过自定义子组件CustomIcon.vue使用SVG图标&#xff0c;相比iconfont下载文件、重新替换更节省时间。 子组件包括&#xff1a; 1. Icons.vue 存放所有SVG图标的path 2. CustomIcon.vue 通过icon的id索引对应的图标 使用的时候需要将 <Icons></Icons> 引到使用的…

MySQL-如果你在添加外键时忘加约束名,如何找到系统默认的约束名

问题 在你添加约束的时候&#xff0c;一般都会为其取名以方便后期的修改&#xff0c;但是如果你忘记了呢&#xff0c;如何找到系统默认的约束名 解决方法 -- 查找约束名 SELECTCONSTRAINT_NAME FROMINFORMATION_SCHEMA.KEY_COLUMN_USAGE WHERETABLE_NAME emp ANDREFERENCED_T…

KP8530X系列KP85301SGA 650V耐压 集成自举二极管的半桥栅极驱动器 北欧双线灯调色解决方案

KP8530X同系列选型参考&#xff1a; KP85301SGA兼容IR2103 KP85302SGA兼容IR2106、IR2001、IR2005、IR2186 KP85303SGA兼容IR2104 KP85304SGA兼容IR2304 KP85305SGA兼容IR21857 KP8530X系列KP85301SGA是一款 650V 耐压&#xff0c;集成自举二极管的半桥栅极驱…

IPC-A-610J-中文版 CHINESE-中文版 2024 电子组件的可接受性

IPC-A-610J-中文版 CHINESE-中文版 2024 电子组件的可接受性.pdf 链接: https://pan.baidu.com/s/1UreAzlB_P7tGH_WoFL2Ybg?pwd1234 提取码: 1234 https://share.weiyun.com/eQCyAPYh 通过网盘分享的文件&#xff1a;IPC-A-610J-中文版 CHINESE-中文版 2024 电子组件的可接受性…

JAVA设计模式之【建造者模式】

1 定义 建造者模式&#xff08;Builder Pattern&#xff09;使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式。 2 类图 产品类&#xff08;Product&#xff09;&#xff1a;表示被创建的复杂…

SQL server 列转行

在 SQL Server 中&#xff0c;将列转换为行的操作通常被称为“透视”&#xff08;Pivot&#xff09;的逆操作&#xff0c;即“反透视”&#xff08;Unpivot&#xff09;。SQL Server 提供了 UNPIVOT 关键字来实现这一功能。假设你有一个表 EmployeeDetails&#xff0c;其中包含…

分类算法——决策树 详解

决策树的底层原理 决策树是一种常用的分类和回归算法&#xff0c;其基本原理是通过一系列的简单决策&#xff0c;将数据集划分为多个子集&#xff0c;从而实现分类。决策树的核心思想是通过树形结构表示决策过程&#xff0c;节点代表特征&#xff0c;边代表决策&#xff0c;叶子…

【小白学机器学习31】 大数定律,中心极限定理,标准正态分布与概率的使用

目录 1 正态分布相关的2个相关定理 1.1 大数定律&#xff1a;(证明了)分布的稳定性 1.2 中心极限定理&#xff1a;(证明了)分布的收敛性 2 使用标准差和概率的2种思路 2.1 标准正态分布的曲线 2.2 两种使用方式 2.3 第1种&#xff1a;按整数倍标准差δ 作为标准使用 2.…

Sourcetree 启动问题(完美解决)

概述 sourcetree 之所以打开就闪退&#xff0c;是因为没有关闭 sourcetree 关机或者系统自动更新等没有关闭sourcetree就直接关机的行为导致缓存信息不匹配&#xff0c;删除的目的是为了重新加载缓存。 下方直接上图&#xff0c;按顺序操作&#xff1a; 1.找到 sourceTree 安…

Sigrity Power SI VR noise Metrics check模式如何进行电源噪声耦合分析操作指导

SSigrity Power SI VR noise Metrics check模式如何进行电源噪声耦合分析操作指导 Sigrity Power SI的VR noise Metrics check模式本质上是用来评估和观测器件的电源网络的耦合对于信号的影响,输出S参数以及列出具体的贡献值。 以下图为例