乐趣区

关于c++:多位专家力推丨大量经典案例分析集锦带你深入学习Nginx底层源码

前言

  说到学习不晓得大家有没有一种对常识的渴望,对技术的极致谋求。不晓得大家有没有,“或者”我有。当然学习是为了获取常识、总结学习后果,便于对将来工作利用中得心应手。随着大家的激情剑拔弩张,想帮忙更多和笔者们一样的人独特学习,笔者们就产生了写书的想法。而后大家不约而同,把对常识的渴望和激情撰写成一本书。

  书中作者来自各“网校”与“根底服务中台”的多位专家, 在本书发明之前, 组织过对 Nginx 底层源码的浏览与调试。经验过数个月的学习与实际,通过积攒积淀了本书。在本书中 RTMP 模块的解析局部,也利用到屡次直播顶峰,经验过数百万在线直播验证。

准备常识

  在学习 Nginx 源码之前能够对一些常识进行初步理解,把握这些常识后,这样便于对常识学习与了解。

  • C/C++ 根底:首先要把握 C /C++ 语言根底,在浏览源码过程中,是否了解语意、语法、以及相干的业务逻辑。便于学习 Nginx 的相干常识。
  • GDB 调试:在本书中一些调试代码片段是采纳 GDB 进行调试,理解 GDB 调试工具也便于对 Nginx 的过程和一些逻辑进行调试。
  • Nginx 根底应用:学习的版本为 Nginx1.16 版本,如果您在浏览过程中,对 Nginx 的一些应用曾经理解。在浏览的过程中,能够起到一些帮忙。
  • HTTP 协定与网络编程基础知识。

联结作者

  • 网校团队:聂松松

好未来学而思网校学习研发直播零碎后端负责人,负责网校外围直播零碎开发和架构工作。毕业于东北大学计算机科学与技术业余,9 年以上音视频及流媒体相干工作教训,精通 Nginx、ffmpeg 相干技术栈。

  • 根底服务中台:赵禹

好将来后端资深开发,曾参加自主守业。目前负责将来云容器平台 kubernetes 组件开发,隶属于容器 iaas 团队。相熟 PHP、Ngnix、Redis、Mysql 等源码实现,乐于钻研技术。

  • 网校团队:施洪宝

好将来后端开发专家,东南大学硕士,对 Redis、Nginx、Mysql 等开源软件有较深的了解,相熟 C /C++、Golang 开发,乐于钻研技术,合著有 <<Redis5 设计与源码剖析 >>。

  • 网校团队:景罗

开源爱好者,高级技术专家,曾任搜狐团体大数据高级研发工程师、新浪微博研发工程师,7 年后端架构教训,相熟 PHP、Nginx、Redis、MySQL 等源码实现,善于高并发解决及大型网站架构,“打造学习型团队”的践行者。

  • 网校团队:黄桃

高级技术专家,8 年后端工作教训,善于高性能网站服务架构与稳定性建设,著有《PHP7 底层设计与源码实现》等书籍。

  • 网校团队:李乐

好未来学而思网校 PHP 开发专家,西安电子科技大学硕士,乐于钻研技术与源码钻研,对 Redis 和 Nginx 有较深了解。合著有《Redis5 设计与源码剖析》。

  • 根底服务中台:张报

好将来团体接入层网关方向负责人,对 Nginx,Tengine,Openresty 等高性能 web 服务器有深刻了解,精通大型站点架构与流量调度零碎的设计与实现。

  • 网校团队:闫昌

好将来后端开发专家,深耕信息安全畛域多年,对 Linux 下服务端开发有较深见解,善于高并发业务的实现。

  • 网校团队:田峰

学而思网校学服研发部负责人。13 年多的互联网从业教训,先后次要在搜狗、百度、360、好将来公司从事研发和技术团队管理工作,在高性能服务架构设计及简单业务零碎开发方面领有丰盛教训。

学习疏导

  在学习的过程中,可能大部分人更关注的是收益问题。一方面是技术的硬技能收益,另一方面高阶晋升。

  说到学习,那么能够整顿一些学习门路。能够通过一张图来看看学习 Nginx 源码都须要学习些什么内容。并且能够怎么样去学习,如图 1 - 1 所示。

<center> 图 1 -1 Nginx 学习纲要 </center>
  通过上图,能够清晰的理解到学习 Nginx 源码都须要学习些什么内容。
在学习初期,能够先理解 Nginx 源码与编译装置和 Nginx 架构根底与设计念想,从 Nginx 的劣势、源码构造、过程模型等几个方面理解 Nginx。而后在学习 Nginx 的内存治理、从内存池、共享内存开展对 Nginx 内存治理与应用。紧接着能够开展对 Nginx 的数据结构学习,别离对字符串、数组、链表、队列、散列、红黑树、根底树的数据结构和算法应用。

  学习完数据结构后,能够对 Nginx 的配置解析、通过 main 配置块、events 配置块与 http 配置块进行学习,而后学习 Nginx 配置解析的全副过程。接下来能够学习过程机制,通过过程模式、master 过程、worker 过程,以及过程建通信机制残缺理解 Nginx 过程的治理。而后在学习 HTTP 模块,通过模块初始化流程、申请解析、HTTP 的 11 个阶段解决,以及 HTTP 申请响应,把握 HTTP 模块的处理过程。

  学习完 HTTP 模块后,再来学习 Upsteam 机制,对 Upstream 初始化、上下游建设、长连贯、FastCGI 模块做肯定了解。

  而后能够理解一些模块,比方 Nginx 工夫模块实现,Nginx 事件模型的文件事件、工夫事件、过程池、连接池等事件处理流程。其次是 Nginx 的负载平衡、限流、日志等模块实现。

  如果要跨平台应用 Nginx,能够理解跨平台实现,对 Nginx 的 configure 编译文件,跨平台原子操作锁进行肯定理解。

  对直播比拟感兴趣,还能够学习 Nginx 直播模块 RTMP 实现,通过 RTMP 协定,模块解决流程,进一步理解 RTMP 模块实现。

基础架构与设计理念

  从诞生以来,Nginx 始终以高性能、高牢靠、易扩大闻名于世,这得益于它诸多优良的设计理念,本章就站在宏观的角度来观赏 Nginx 的架构设计之美。

Nginx 过程模型

  现在大多数零碎都须要应答海量的用户流量,人们也越来越关注零碎的高可用、高吞吐、低延时、低消耗等个性,此时玲珑且高效的 Nginx 走进了大家的视线,并很快受到了人们的青眼。Nginx 的全新过程模型与事件驱动设计使其能天生轻松应答 C10K 甚至 C100K 高并发场景。

  Nginx 应用了 Master 治理过程(治理过程 Master)和 Worker 工作过程(工作过程 Worker)的设计,如图 2 - 1 所示。

<center> 图 2 -1 Master-Worker 过程模型 </center>

  Master 过程负责管理各个 Worker,通过信号或管道的形式来管制 Worker 的动作。当某个 Worker 异样退出时,Master 过程个别会启动一个新的 Worker 过程代替它。Worker 是真正解决用户申请的过程,各 Worker 过程是平等的,它们通过共享内存、原子操作等一些过程间通信机制来实现负载平衡。多过程模型的设计充分利用了 SMP(Symmetrical Multi-Processing)多核架构的并发解决能力,保障了服务的健壮性。

  同样是基于多过程模型,为什么 Nginx 能具备如此强的性能与超高的稳定性,其起因有以下几点。

  • 异步非阻塞:

  Nginx 的 Worker 过程全程工作在异步非阻塞模式下,从 TCP 连贯的建设到读取内核缓冲区里的申请数据,再到各 HTTP 模块解决申请,或者是反向代理时将申请转发给上游服务器,最初再将响应数据发送给用户,Worker 过程简直不会阻塞,当某个零碎调用产生阻塞时(例如进行 I / O 操作,然而操作系统还没将数据筹备好),Worker 过程会立刻解决下一个申请,当条件满足时操作系统会告诉 Worker 过程持续实现这次操作,一个申请可能须要多个阶段能力实现,然而整体上看每个 Worker 始终处于高效的工作状态,因而 Nginx 只须要很多数 Worker 过程就能解决大量的并发申请。当然,这些都得益于 Nginx 的全异步非阻塞事件驱动框架,尤其是在 Linux2.5.45 之后操作系统的 I / O 多路复用模型中新增了 epoll 这款神器,让 Nginx 换上了全新的发动机一路狂飙到性能之巅。

  • CPU 绑定

  通常在生产环境中配置 Nginx 的 Worker 数量等于 CPU 外围数,同时会通过 worker_cpu_affinity 将 Worker 绑定到固定的核上,让每个 Worker 独享一个 CPU 外围,这样既能无效地防止频繁的 CPU 上下文切换,也能大幅提高 CPU 缓存命中率。

  • 负载平衡

  当客户端试图与 Nginx 服务器建设连贯时,操作系统内核将 socket 对应的 fd 返回给 Nginx,如果每个 Worker 都争抢着去承受(accept)连贯就会造成驰名的“惊群”问题,也就是最终只会有一个 Worker 胜利承受连贯,其余 Worker 都白白地被作零碎唤醒,这势必会升高零碎的整体性能。另外,如果有的 Worker 运气不好,始终承受失败,而有的 Worker 自身曾经很繁忙却承受胜利,就会造成 Worker 之间负载的不平衡,也会升高 Nginx 服务器的解决能力与吞吐量。Nginx 通过一把全局的 accept_mutex 锁与一套简略的负载平衡算法就很好的解决了这两个问题。首先每个 Worker 在监听之前都会通过 ngx_trylock_accept_mutex 无阻塞的获取 accept_mutex 锁,只有胜利抢到锁的 Worker 才会真正监听端口并 accept 新的连贯,而抢锁失败的 Worker 只能持续解决已承受连贯上的事件。其次,Nginx 为每个 Worker 设计了一个全局变量 ngx_accept_disabled,并通过如下形式对该值进行初始化:

ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n

  其中 connection_n 示意每个 Worker 一共可同时承受的连接数,free_connnection_n 示意闲暇连接数,Worker 过程启动时,闲暇连接数与可承受连接数相等,也就是 ngx_accept_disabled 初始值为 -7/8 * connection_n。当 ngx_accept_disabled 为负数时,表明闲暇连接数曾经有余总数的 1 / 8 了,此时阐明该 Worker 过程非常忙碌,于是它本次事件循环放弃争抢 accept_mutex 锁,专一于解决已有的连贯,同时会将本人的 ngx_accept_disabled 减一,下次事件循环时持续判断是否进入抢锁环节。上面的代码摘要展现了上述算法逻辑:

if (ngx_use_accept_mutex) {if (ngx_accept_disabled > 0) {ngx_accept_disabled--;} else {if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {return;}
 ……
    }

  总体上来看这种设计略显毛糙,但它胜在简略实用,肯定水平上保护了各 Worker 过程的负载平衡,防止了单个 Worker 耗尽资源而拒绝服务,晋升了 Nginx 服务器的高性能与健壮性。

  另外,Nginx 也反对单过程工作模式,然而这种模式不能施展 CPU 多核的解决能力,通常只实用于本地调试。

Nginx 模块化设计

  Nginx 主框架中只提供了大量的外围代码,大量弱小的性能是在各模块中实现的。模块设计齐全遵循了高内聚、低耦合的准则,每个模块只解决本人职责之内的配置项,专一实现某项特定的性能,各类型的模块实现了对立的接口标准,这大大加强了 Nginx 的灵活性与可扩展性。

模块分类

  Nginx 官网将泛滥模块按性能分为 5 类,如图 2 - 2 所示。

<center> 图 2 -2 Nginx 模块分类图 </center>

1)外围模块: Nginx 中最重要的一类模块,蕴含了 ngx_core_module、ngx_http_module、ngx_events_module、ngx_mail_module、ngx_openssl_module、ngx_errlog_module 这 6 个具体的模块,每个外围模块定义了同一种格调类型的模块。

2)HTTP 模块:与解决 HTTP 申请密切相关的一类模块,HTTP 模块蕴含的模块数量远多于其余类型的模块,Nginx 大量丰盛的性能根本都是通过 HTTP 模块实现的。

3)Event 模块: 定义了一系列能够运行在不同操作系统,不同内核版本的事件驱动模块,Nginx 的事件处理框架完满的反对了各类操作系统提供的事件驱动模型,包含 epoll,poll,select,kqueue,eventport 等。

4)Mail 模块: 与邮件服务相干的模块,Mail 模块使 Nginx 具备了代理 IMAP、POP3、SMTP 等协定的能力。

5)配置模块: 此类模块只有 ngx_conf_module 一个成员,然而它是其余模块的根底,因为其余模块在失效前都须要依赖配置模块解决配置指令并实现各自的筹备工作,配置模块领导了所有模块依照配置文件提供性能,它是 Nginx 可配置性、可定制化、可扩大的根底。

模块接口

  尽管 Nginx 模块数量泛滥,性能简单多样,但并没有给开发人员带来多少困扰,因为所有的模块都遵循了同一个 ngx_module_t 接口设计规范,定义如下:

struct ngx_module_s {
    ngx_uint_t            ctx_index;
    ngx_uint_t            index;
    char                   *name;
    ngx_uint_t            spare0;
    ngx_uint_t            spare1;
    ngx_uint_t            version;
    const char           *signature;
    void                   *ctx;
    ngx_command_t        *commands;
    ngx_uint_t            type;

    ngx_int_t           (*init_master)(ngx_log_t *log);
    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);
    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);
    ngx_int_t           (*init_thread)(ngx_cycle_t *cycle);
    void                (*exit_thread)(ngx_cycle_t *cycle);
    void                (*exit_process)(ngx_cycle_t *cycle);
    void                (*exit_master)(ngx_cycle_t *cycle);

    uintptr_t             spare_hook0;
    uintptr_t             spare_hook1;
    uintptr_t             spare_hook2;
    uintptr_t             spare_hook3;
    uintptr_t             spare_hook4;
    uintptr_t             spare_hook5;
    uintptr_t             spare_hook6;
    uintptr_t             spare_hook7;
};

  这是 Nginx 源码中十分重要的一个构造体,它蕴含了一个模块的根本信息:包含模块名称、模块类型、模块指令、模块程序等。留神,其中的 init_master、init_module、init_process 等 7 个钩子函数,让每个模块可能在 Master 过程启动与退出、模块初始化、Worker 过程启动与退出等阶段嵌入各自的逻辑,这大大提高了模块实现的灵活性。

  后面咱们提到,Nginx 对所有模块都进行了分类,每类模块都有本人的个性,实现了本人的特有的办法,那怎么能将各类模块都能和 ngx_module_t 这惟一的构造体关联起来呢?仔细的读者可能曾经留神到,ngx_module_t 中有一个类型为 void 的 ctx 成员,它定义了该模块的公共接口,它是 ngx_module_t 和各类模块的关系纽带。何谓“公共接口”? 简略点讲就是每类模块都有各自家族特有的协定标准,通过一个 void 类型的 ctx 变量进行形象,同类型的模块只须要遵循这一套标准即可。这里拿外围模块和 HTTP 模块举例说明:
对于外围模块,ctx 指向的是名为 ngx_core_module_t 的构造体,这个构造体很简略,除了一个 name 成员就只有 create_conf 和 init_conf 两个办法,所有的外围模块都会去实现这两个办法,如果有一天 Nginx 又发明了新的外围模块,那它也肯定是依照 ngx_core_module_t 这个公共接口来实现。

typedef struct {
    ngx_str_t          name;
    void               *(*create_conf)(ngx_cycle_t *cycle);
    char               *(*init_conf)(ngx_cycle_t *cycle, void *conf);
} ngx_core_module_t;

  而对于 HTTP 模块,ctx 指向的是名为 ngx_http_module_t 的构造体,这个构造体里定义了 8 个通用的办法,别离是 http 模块在解析配置文件前后,以及创立与合并 http 段、server 段、location 段配置时所调用的办法,如上面代码所示:

typedef struct {ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);

    void       *(*create_main_conf)(ngx_conf_t *cf);
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);

    void       *(*create_srv_conf)(ngx_conf_t *cf);
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

    void       *(*create_loc_conf)(ngx_conf_t *cf);
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;

  Nginx 在启动的时候,就能够依据以后的执行上下文来顺次调用所有 HTTP 模块里 ctx 所指定的办法。更重要的是,对于一个开发者来说,只须要依照 ngx_http_module_t 里的接口标准实现本人想要的逻辑,这样不仅升高了开发成本,也减少了 Nginx 模块的可扩展性和可维护性。
从全局的角度来看,Nginx 的模块接口设计兼顾了统一化与差异化的思维,以最简略实用的形式实现了模块的多态性。

模块分工

  既然 Nginx 对模块进行了分类,每个模块都实现了某种特定的性能。那这么多模块是如何无效的组织起来的呢? Nginx 启动过程中各模块都须要实现哪些筹备工作呢?解决申请的过程中各模块又是如何相互协作完成使命的呢?本章先让咱们有一个大体的意识,前面章节里会具体论述。

  事实上,Nginx 主框架只关怀 6 个外围模块的实现,每个外围模块别离“代言”了一种类型的模块。例如对于 HTTP 模块,对立由 ngx_http_module 治理,什么时候创立各 HTTP 模块存储配置项的构造体,什么时候执行各模块的初始化操作,齐全由 ngx_http_module 外围模块掌控。就如同一家大型公司的治理团队,每个高级管理者负责了一个大部门,部门内每个员工专一于实现各自的使命。最高层领导只用关注各部门管理者,各部门管理者只需治理各自的上司。这种分层的思维使得 Nginx 的源代码也具备了高内聚低耦合的特点。

  Nginx 启动时须要实现配置文件的解析,这部分工作齐全是以 Nginx 配置模块与解析引擎为根底实现的,对于每一项配置指令,除了须要精准无误的读取辨认,更重要的是存储与解析。首先 Nginx 会找到对该指令感兴趣的模块并调用该模块事后设定好的处理函数,少数状况下这里会将参数保留到该模块存储配置项的构造体里并进行初始化操作。而外围模块在启动过程中不仅会创立用于保留该“家族”所有存储配置构造体的容器,而且会按程序将各构造体组织起来,这样泛滥的模块的配置信息对立由其所属家族的“老大”治理起来,Nginx 也能依照序号从这些全局的容器里迅速获取到某个模块的配置项。另外,对于事件模块在启动过程中须要实现最重要的工作,就是依据用户配置以及操作系统抉择一款事件驱动模型,在 Linux 零碎中,Nginx 会默认抉择 epoll 模型,在 Worker 过程被 fork 进去并进入初始化阶段时,事件模块会创立各自的 epoll 对象,并通过 epoll_ctl 零碎调用将监听端口的 fd 增加到 epoll 中。

  对于用户申请的解决则次要是各 HTTP 模块负责,为了让解决流程更加灵便,各模块耦合度更低,Nginx 无意将解决 HTTP 申请的过程划分为了 11 个阶段,每个阶段实践上都容许多个模块执行相应的逻辑。在启动阶段解析完配置文件之后,各 HTTP 模块会将各自的 handler 函数以 hook 的模式挂载到某个阶段中。Nginx 的事件模块会依据各种事件调度 HTTP 模块顺次执行各阶段的 handler 解决办法,并通过返回值来断定是持续向下执行还是完结以后申请,这种流水线式的申请解决流程使各 HTTP 模块齐全解耦,给 Nginx 模块的设计带来了极大的便捷,开发者在实现模块外围解决逻辑之后,只须要思考将 handler 函数注册到哪个阶段即可。

  Nginx 自开源以来,社区涌现了大量低劣的第三方模块,极大的扩大了原生 Nginx 的外围性能,这些都得益于 Nginx 优良的模块化设计思维。

Nginx 事件驱动

  Nginx 全异步事件驱动框架是保障其高性能的重要基石。事件驱动并不是 Nginx 独创的,这一概念很早就呈现在了计算机领域,它指的是在继续的事物治理过程中进行决策的一种策略,即追随以后工夫点上呈现的事件,调动可用资源,执行相干工作,使一直呈现的问题得以解决,避免事务沉积。通常事件驱动架构外围由三局部组成:事件收集器、事件发生器、事件处理器。顾名思义,事件收集器专门负责收集所有的事件,作为一款 Web 服务器,Nginx 次要解决的事件来自于网络和磁盘,包含 TCP 连贯的建设与断开,接管和发送网络数据包,磁盘文件的 I / O 操作等,每种类型都对应了一个读事件和写事件。事件散发器则负责将收集到的事件散发到指标对象中,Nginx 通过 event 模块实现了读写事件的治理和散发;而事件处理器作为消费者,负责接管散发过去的各种事件并解决,通常 Nginx 中每个模块都有可能成为事件消费者,模块解决完业务逻辑之后立即将控制权交还给事件模块,进行下一个事件的调度散发。因为生产事件的主体是各 HTTP 模块,事件处理函数是在一个过程中实现,只有各 HTTP 模块不让过程进入休眠状态,那么整个申请的处理过程是十分迅速的,这是 Nginx 放弃超高网络吞吐量的要害。当然,这种设计会减少了肯定的编程难度,开发者须要通过肯定的伎俩(例如异步回调的形式)解决阻塞问题。
不同操作系统提供了不同事件驱动模型,例如 Linux 2.6 零碎同时反对 epoll、poll、select 模型,FreeBSD 零碎反对 kqueue 模型,Solaris 10 上反对 eventport 模型。为了保障其跨平台个性,Nginx 的事件框架完满的反对了各类操作系统的事件驱动模型,针对每一种模型 Nginx 都设计了一个 event 模块,包含了 ngx_epoll_module、ngx_poll_module、ngx_select_module、ngx_kqueue_module 等。事件框架会在模块初始化时选取其中一个作为 Nginx 过程的事件驱动模块,对于大多数生产环境中 Liunx 零碎的 Web 服务器,Nginx 默认选取最弱小的事件治理 epoll 模型,这部分常识咱们将在第 7 章进行具体解说。

总结

   更多内容把握能够购买《Nginx 底层设计与源码剖析》进行学习。


  如果对 Nginx 底层源码比拟感兴趣,能够购买本书纸质版本进行浏览。或者退出作者团队,一起学习共勉。作者所在团队别离为“网校团队”与“根底服务中台团队”。
在“网校团队”能够与大佬们手牵手一起学习底层哟,在“根底服务中台团队”也能够和老师们学习 Nginx 底层与 k8s 底层。

退出移动版