4. 基于流的负载均衡:流的概念、流表建立、流老化机制、流哈希冲突处理
各位好,今天我们来聊聊基于流的负载均衡。说实话,这个主题在交换架构里属于「看着简单,做起来坑多」的那种。我早年刚接触数据中心交换机时,觉得不就是把流量分一分嘛,后来被线上问题教育了几次,才真正理解这里面的门道。
4.1 什么是「流」?
先说说流的概念。你想想看,网络里数据包成千上万,但很多包其实属于同一个「对话」。比如你打开一个网页,从TCP三次握手到HTTP请求响应,这中间几十个包,它们应该走同一条路径,对吧?
流,就是一组具有相同五元组(源IP、目的IP、源端口、目的端口、协议号)的数据包序列。说白了,就是一次「会话」的所有包。
核心要点:基于流的负载均衡,就是把同一个流的包始终发到同一个后端,不同流的包可以分散到不同后端。这样既保证会话一致性,又能充分利用多路径带宽。
我在项目中遇到过一种情况:某个客户抱怨他们的视频会议经常卡顿。查了半天,发现是负载均衡把同一个RTP流的包分到了两条链路上,导致接收端重组乱序。嗯,这就是典型的「没按流做哈希」的后果。
4.2 流表建立:第一次见到你,我就记住了
流表是怎么建立的?其实很简单——懒加载。
当一个数据包到达交换机时,交换芯片会先查流表。如果没找到匹配项,就说明这是一个新流。这时候芯片会做两件事:
- 计算哈希:根据五元组算出一个哈希值
- 写入流表:把哈希值和对应的出接口信息存下来
后续同一个流的包再来,直接查表命中,走固定路径。这就是所谓的「首包建流」机制。
小技巧:我建议在流表项里额外存一个「命中计数器」。这样你可以知道哪些流是热点,哪些流已经很久没人用了。对后续的流老化很有帮助。
代码层面,流表通常用哈希表实现。我见过一个比较简洁的流表结构:
struct flow_entry {
uint32_t hash_key; // 五元组哈希值
uint8_t src_ip[4]; // 源IP(可选,用于冲突检测)
uint8_t dst_ip[4]; // 目的IP
uint16_t src_port; // 源端口
uint16_t dst_port; // 目的端口
uint8_t protocol; // 协议号
uint8_t out_iface; // 出接口
uint32_t hit_count; // 命中次数
uint32_t timestamp; // 最后命中时间
struct flow_entry *next; // 链表指针(处理哈希冲突)
};
4.3 流老化机制:该忘就忘,别死扛
流表不能无限增长。交换机内存有限,而且很多流是短连接,建了之后很快就结束了。如果不清理,流表迟早被撑爆。
流老化,说白了就是「把不活跃的流踢出去」。常见的策略有三种:
| 老化策略 | 原理 | 适用场景 |
|---|---|---|
| 空闲超时 | 流表项超过N秒无命中,就删除 | 通用场景,最常用 |
| 硬超时 | 流表项创建后最多存活T秒,不管有没有命中 | 长连接保活场景 |
| LRU淘汰 | 流表满时,淘汰最近最少使用的项 | 内存紧张的高性能场景 |
我曾经踩过一个坑:某个业务用的是WebSocket长连接,空闲超时设成了30秒。结果用户经常莫名其妙断连。查了半天才发现,WebSocket空闲时没有数据包,流表项被老化了。后续包过来重新建流,哈希到了不同后端,会话就断了。
避坑指南:对于长连接业务,建议把空闲超时设大一些(比如300秒),或者干脆用硬超时。我曾经因为这个问题被运维同事追着骂了一下午……
4.4 流哈希冲突处理:撞车了怎么办?
哈希冲突是绕不开的问题。两个不同的流算出了相同的哈希值,怎么办?
常见的处理方式有这几种:
- 链表法:同一个哈希桶里挂链表,冲突的流串在一起。实现简单,但链表长了性能会下降。
- 开放寻址法:冲突了就往下一个空位放。适合流表比较稀疏的场景。
- 多级哈希:准备多个哈希函数,第一个冲突了就换第二个。我在博通的一些芯片上见过这种做法。
- Cuckoo哈希:冲突了就把原来的项踢走,让它去别的位置。吞吐量高,但实现复杂。
我个人习惯用链表法加「短链表优化」。什么意思呢?就是每个哈希桶的链表长度限制在4以内。超过4个,就认为这个桶太挤了,触发流表扩容或者强制老化一些流。这样能保证最坏情况下的查找性能。
关键点:哈希冲突处理不好,会导致「流极化」——多个流挤到同一个后端,其他后端闲着。负载均衡就名存实亡了。
嗯,这里还要提一句:哈希函数的选择也很重要。别用简单的异或或者加法,容易产生大量冲突。我推荐用CRC16或者MurmurHash,分布均匀,硬件实现也方便。
好了,关于基于流的负载均衡,核心就是这四个方面:流的概念、流表建立、流老化、哈希冲突处理。每个点都有不少细节,但抓住了主线,后面就好办了。