前言
说到学习不晓得大家有没有一种对常识的渴望,对技术的极致谋求。不晓得大家有没有,“或者”我有。当然学习是为了获取常识、总结学习后果,便于对将来工作利用中得心应手。随着大家的激情剑拔弩张,想帮忙更多和笔者们一样的人独特学习,笔者们就产生了写书的想法。而后大家不约而同,把对常识的渴望和激情撰写成一本书。
书中作者来自各“网校”与“根底服务中台”的多位专家,在本书发明之前,组织过对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底层。