5

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

 3 years ago
source link: https://segmentfault.com/a/1190000040201606
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

发布于 33 分钟前

  说到学习不知道大家有没有一种对知识的渴望,对技术的极致追求。不知道大家有没有,“或许”我有。当然学习是为了获取知识、总结学习结果,便于对未来工作应用中得心应手。随着大家的热情一触即发,想帮助更多和笔者们一样的人共同学习,笔者们就产生了写书的想法。然后大家不谋而合,把对知识的渴望和热情撰写成一本书。

  书中作者来自各“网校”与“基础服务中台”的多位专家,在本书创造之前,组织过对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所示。
1.png
<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所示。
2.png
<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所示。
3.png
<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底层设计与源码分析》进行学习。

4.png
  如果对Nginx底层源码比较感兴趣,可以购买本书纸质版本进行阅读。或者加入作者团队,一起学习共勉。作者所在团队分别为“网校团队”与“基础服务中台团队”。
在“网校团队”可以与大佬们手牵手一起学习底层哟,在“基础服务中台团队”也可以和老师们学习Nginx底层与k8s底层。
5.png

6.png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK