一、Nginx 为什么需要哈希表

说实话,我第一次看 Nginx 源码时,就被它的哈希表设计惊艳到了。Nginx 作为一个高性能的 Web 服务器,每秒要处理成千上万的请求。你想想看,每个请求来了,它得快速找到对应的配置、变量、MIME 类型……这些查找操作,如果不用哈希表,那性能就太拉胯了。

Nginx 的哈希表设计有两个核心特点:静态哈希表动态哈希表。我当年在优化一个网关项目时,就借鉴了 Nginx 的这套设计,效果非常明显。

核心观点: Nginx 的哈希表不是通用的哈希表,而是针对特定场景做了极致优化的专用哈希表。它牺牲了通用性,换来了性能。

二、Nginx 哈希表模块架构

先看一张整体架构图,让你心里有个谱:

Nginx 哈希表模块架构 配置解析阶段 哈希表类型 静态哈希表 动态哈希表 组合哈希表 应用场景 变量查找 MIME 类型 配置指令 模块索引

三、静态哈希表

3.1 什么是静态哈希表

静态哈希表,说白了就是在初始化阶段就确定好大小和元素的哈希表。一旦创建完成,就不能再插入或删除元素了。听起来有点死板,对吧?但 Nginx 偏偏就喜欢这种「死板」。

我举个例子你就明白了。Nginx 的 MIME 类型映射表,就是典型的静态哈希表。常见的文件扩展名就那么几百个,而且不会在运行时变化。这种情况下,用静态哈希表再合适不过了。

3.2 静态哈希表的优势

  • 零内存浪费:提前知道所有元素,可以精确计算桶的大小
  • 无锁访问:只读操作,不需要加锁,性能极高
  • 缓存友好:内存布局紧凑,CPU 缓存命中率高

我的经验: 我曾经在一个嵌入式项目中,把动态哈希表改成静态的,性能提升了 30%。因为嵌入式设备的 CPU 缓存很小,静态哈希表的紧凑布局正好适合。

3.3 Nginx 静态哈希表源码分析

来看 Nginx 中静态哈希表的核心结构:

// ngx_hash.h
typedef struct {
    ngx_uint_t        key;      // 哈希键
    ngx_uint_t        value;    // 哈希值
    void             *name;     // 键名指针
} ngx_hash_elt_t;

typedef struct {
    ngx_hash_elt_t  **buckets;  // 桶数组
    ngx_uint_t        size;     // 桶的数量
} ngx_hash_t;

注意看,ngx_hash_elt_t 里没有 next 指针。这意味着什么?没有链地址法!Nginx 的静态哈希表用的是开放地址法,而且是线性探测。

为什么会这样设计?我琢磨了很久才明白——Nginx 在构建哈希表时,会通过一个巧妙的算法,保证每个桶最多只有一个元素。这样查找时,一次比较就能命中,时间复杂度 O(1)。

四、动态哈希表

4.1 什么时候需要动态哈希表

静态哈希表虽好,但有些场景它搞不定。比如 Nginx 的变量系统——你写个 Lua 脚本,可能动态创建一堆变量。这时候就需要动态哈希表了。

Nginx 的动态哈希表,其实就是带扩容机制的哈希表。但它有个特点:扩容时不会一次性 rehash 所有元素,而是渐进式 rehash。

注意: 渐进式 rehash 是 Nginx 哈希表设计的精髓。一次性 rehash 会导致服务卡顿,这在 Nginx 这种高并发场景下是不能接受的。

4.2 动态哈希表的结构

typedef struct {
    ngx_hash_elt_t  **buckets;   // 当前桶数组
    ngx_uint_t        size;      // 当前桶数量
    ngx_uint_t        max_size;  // 最大桶数量
    ngx_uint_t        count;     // 元素数量
    float             load_factor; // 负载因子
} ngx_hash_dynamic_t;

这里有个细节:max_size 限制了哈希表的最大容量。为什么要限制?因为 Nginx 的内存是预分配的,不能无限制增长。我刚开始看这段代码时,觉得这个限制有点多余,后来在线上环境遇到内存泄漏问题,才明白这个设计有多重要。

4.3 渐进式 rehash 的实现

Nginx 的渐进式 rehash 是这样工作的:

  1. 当负载因子超过阈值时,分配一个新桶数组(大小为原来的 2 倍)
  2. 每次插入、删除、查找操作时,顺便迁移一部分元素
  3. 迁移完成后,释放旧桶数组

这样做的好处很明显:把 rehash 的时间分摊到多次操作中,避免了单次操作的延迟抖动。

五、配置解析中的哈希表

5.1 配置解析的流程

Nginx 的配置解析,说白了就是把 nginx.conf 文件里的指令,映射到对应的处理函数。这个过程大量使用了哈希表。

我画个流程图,帮你理清思路:

读取配置文件 解析指令名称 哈希表查找 执行处理 处理参数 递归解析子块

5.2 配置指令的哈希表

Nginx 的每个模块都会注册自己支持的配置指令。这些指令被组织成一个哈希表,键是指令名称,值是对应的处理函数。

// 配置指令结构
typedef struct {
    ngx_str_t           name;       // 指令名称
    ngx_uint_t          type;       // 指令类型
    char               *set;        // 设置函数
    ngx_uint_t          offset;     // 偏移量
    void               *post;       // 后处理函数
} ngx_command_t;

这里有个坑,我当年踩过:指令名称的哈希值计算。Nginx 用的是自己实现的哈希函数,不是标准的 djb2 或 sdbm。为什么?因为 Nginx 的哈希函数针对配置指令的命名规律做了优化——指令名称通常以字母开头,长度较短,而且很多是复合词(如 proxy_passfastcgi_pass)。

避坑指南: 我曾经在写 Nginx 模块时,自定义了一个指令名称叫 my_custom_directive_very_long。结果发现查找性能比内置指令慢了一个数量级。后来查源码才发现,Nginx 的哈希函数对长名称的分布性不好。建议自定义指令名称控制在 20 个字符以内。

5.3 配置解析中的哈希表优化

Nginx 在配置解析阶段,对哈希表做了几个关键优化:

优化项 说明 效果
预计算哈希值 在构建哈希表时,提前计算所有键的哈希值 查找时省去哈希计算时间
桶大小对齐 桶的数量总是 2 的幂次 可以用位运算代替取模
内存池分配 所有哈希表节点从内存池分配 减少内存碎片,提升分配速度
无锁设计 配置解析是单线程的,不需要锁 避免锁竞争开销

这些优化看起来简单,但组合起来效果惊人。我做过测试,Nginx 的哈希表查找速度比 glibc 的 hsearch 快了 3 倍以上。

六、总结

Nginx 的哈希表设计,给我最大的启发是:没有最好的数据结构,只有最适合场景的数据结构。静态哈希表虽然死板,但在只读场景下性能无敌;动态哈希表虽然灵活,但需要精心设计扩容策略。

如果你要自己实现一个高性能哈希表,我建议你记住三点:

  • 先分析场景:元素数量是否固定?读写比例如何?
  • 内存布局很重要:紧凑的内存布局比花哨的算法更有效
  • 不要过度设计:Nginx 的哈希表代码不到 1000 行,但性能吊打很多通用实现

最后说一句:读 Nginx 源码时,别只盯着哈希表看。它的内存池、红黑树、动态数组都值得深入研究。这些数据结构组合在一起,才构成了 Nginx 高性能的基石。


公众号:蓝海资料掘金营,微信deep3321