大咖专栏
换一换
陶辉 《深入理解Nginx:模...
文章数 12
被赞数 93
订阅数 108
洪志道 洪志道的专栏
文章数 5
被赞数 20
订阅数 29
陈友行 陈友行的专栏
文章数 5
被赞数 25
订阅数 39
李志荣 这是李志荣的专栏
文章数 2
被赞数 8
订阅数 11
楚发 楚发的专栏
文章数 1
被赞数 3
订阅数 10
刘彦梅 刘彦梅的专栏
文章数 0
被赞数 0
订阅数 8
朱照远 朱照远的专栏
文章数 0
被赞数 0
订阅数 8
张炎泼 张炎泼的专栏
文章数 1
被赞数 0
订阅数 6
洪海 洪海的专栏
文章数 0
被赞数 0
订阅数 6
热门文章
可靠分布式系统-paxos的直观解释 张炎泼 发表于 : 2020-10-14 14:23

本文链接: https://blog.openacid.com/algo/paxos/前言paxos是什么?在分布式系统中保证多副本数据强一致的算法.paxos有啥用?没有paxos的一堆机器, 叫做分布式;有paxos协同的一堆机器, 叫分布式系统.Google Chubby的作者Mike Burrows说过:这个世界上只有一种一致性算法,那就是Paxos …其他一致性算法, 都可以看做paxos在实现中的变体和扩展.另外一个经常被提及的分布式算法是raft, raft的贡献在于把一致性算法落地. 因为 Leslie Lamport 的理论很抽象, 要想把他的理论应用到现实中, 还需要工程师完全掌握他的理论再添加工程必要的环节才能跑起来.经常有人问起raft和paxos的区别, 或在实现中应该选择哪个, 在不了解paxos之前可能会有这种疑问. 对于这个问题, 就像是被问及四则运算和算盘有什么区别, 小店老板应该使用四则远算还是用算盘结账一样.记得 Leslie Lamport 2015年时来了一次北京, 那时会场上有人也问了老爷子 paxos和raft有啥区别.老爷子当时给出的回答是: 没听过raft…raft的核心可以认为是multi paxos的一个应用, 对于要掌握一致性算法的核心内容, 从paxos入手, 更容易去掉无关干扰, 直达问题本质. 所以我们选择paxos作为了解一致性算法的入口, 聊开了聊透了.网络上raft比paxos流行, 因为raft的描述更直白一些, 实际上raft比paxos更复杂. raft详细的解释了”HOW”, 缺少”WHY”的解释. paxos从根本上解释清楚了”WHY”, 但一直缺少一份通俗易懂的教程. 以至于没有被更广泛的接受. 所以就有了本文, 一篇paxos入门教程, 从基本的分布式中的复制的问题出发, 通过逐步解决和完善这几个问题, 最后推导出paxos的算法.本文分为2个部分:前1部分是分布式一致性问题的讨论和解决方案的逐步完善, 用人话得出paxos算法的过程. 如果只希望理解paxos而不打算花太多时间深入细节, 只阅读这1部分就可以啦.第2部分是paxos算法和协议的严格描述. 这部分可以作为paxos原paper的实现部分的概括. 如果你打算实现自己的paxos或类似协议, 需要仔细了解协议细节, 希望这部分内容可以帮你节省阅读原paper的时间.图片是xp之前做过的paxos分享使用的slides, 在此基础上加入了更多口头解释的内容.分布式系统要解决的问题slide-00 slide-01 paxos的工作, 就是把一堆运行的机器协同起来, 让多个机器成为一个整体系统. 在这个系统中, 每个机器都必须让系统中的状态达成一致, 例如三副本集群如果一个机器上上传了一张图片, 那么另外2台机器上也必须复制这张图片过来, 整个系统才处于一个一致的状态.slide-02 我是无需解释的目录页. slide-03 分布式系统的一致性问题最终都归结为分布式存储的一致性. 像aws的对象存储可靠性要求是9~13个9. 而这么高的可靠性都是建立在可靠性没那么高的硬件上的.slide-04 几乎所有的分布式存储(甚至单机系统, 参考EC第一篇:原理, EC第二篇:实现, EC第三篇:极限) 都必须用某种冗余的方式在廉价硬件的基础上搭建高可靠的存储. 而冗余的基础就是多副本策略, 一份数据存多份. 多副本保证了可靠性, 而副本之间的一致, 就需要paxos这类分布式一致性算法来保证.slide-05 在早些年各种各样的复制策略都被提出来来解决各种场景下的需要. 除了复制的份数之外, 各种各样的算法实际上都是在尝试解决一致的问题. 从下一页开始简单回顾下各种复制策略, 看看他们的优缺点以及paxos如何解决副本之间一致性的问题.不太完美的复制策略slide-06 无需解释的目录页 slide-07 主从异步复制是最简单的策略之一, 它很容易实现, 但存在一个问题: 客户端收到一个数据已经安全(OK)的信息, 跟数据真正安全(数据复制到全部的机器上)在时间上有一个空隙, 这段时间负责接收客户端请求的那个机器(master)如果被闪电击中或被陨石砸到或被打扫卫生的大姐踢断了电源, 那数据就可能会丢失. 因此它不是一个可靠的复制策略(使用主从异步复制要求你必须相信宇宙中不存在闪电陨石和扫地大姐).slide-08 跟主从异步复制相比, 主从同步复制提供了完整的可靠性: 直到数据真的安全的复制到全部的机器上之后, master才告知客户端数据已经安全.但主从同步复制有个致命的缺点就是整个系统中有任何一个机器宕机, 写入就进行不下去了. 相当于系统的可用性随着副本数量指数降低.slide-09 然鹅, 在同步和异步之间, 做一个折中, 看起来是一个不错的方案. 这就是半同步复制. 它要求master在应答客户端之前必须把数据复制到足够多的机器上, 但不需要是全部. 这样副本数够多可以提供比较高的可靠性; 1台机器宕机也不会让整个系统停止写入.但是它还是不完美, 例如数据a复制到slave-1, 但没有到达slave-2; 数据b复制达到了slave-2但没有到达slave-1, 这时如果master挂掉了需要从某个slave恢复出数据, 任何一个slave都不能提供完整的数据. 所以在整个系统中, 数据存在某种不一致.slide-10 为了解决半同步复制中数据不一致的问题, 可以将这个复制策略再做一改进: 多数派读写: 每条数据必须写入到半数以上的机器上. 每次读取数据都必须检查半数以上的机器上是否有这条数据.在这种策略下, 数据可靠性足够, 宕机容忍足够, 任一机器故障也能读到全部数据.slide-11 然鹅多数派读写的策略也有个但是, 就是对于一条数据的更新时, 会产生不一致的状态. 例如:node-1, node-2都写入了a=x,下一次更新时node-2, node-3写入了a=y.这时, 一个要进行读取a的客户端如果联系到了node-1和node-2, 它将看到2条不同的数据.为了不产生歧义, 多数派读写还必须给每笔写入增加一个全局递增的时间戳. 更大时间戳的记录如果被看见, 就应该忽略小时间戳的记录. 这样在读取过程中, 客户端就会看到a=x₁, a=y₂ 这2条数据, 通过比较时间戳1和2, 发现y是更新的数据, 所以忽略a=x₁. 这样保证多次更新一条数据不产生歧义.slide-12 是的, 但是又来了. 这种带时间戳的多数派读写依然有问题. 就是在客户端没有完成一次完整的多数派写的时候: 例如, 上面的例子中写入, a=x₁写入了node-1和node-2, a=y₂时只有node-3 写成功了, 然后客户端进程就挂掉了, 留下系统中的状态如下:这时另一个读取的客户端来了,如果它联系到node-1和node-2, 那它得到的结果是a=x₁.如果它联系到node-2和node-3, 那它得到的结果是a=y₂.整个系统对外部提供的信息仍然是不一致的.slide-13 现在我们已经非常接近最终奥义了, paxos可以认为是多数派读写的进一步升级, paxos中通过2次原本并不严谨的多数派读写, 实现了严谨的强一致consensus算法.从多数派读写到paxos的推导slide-14 首先为了清晰的呈现出分布式系统中的核心问题: 一致性问题, 我们先设定一个假象的存储系统, 在这个系统上, 我们来逐步实现一个强一致的存储, 就得到了paxos对一致性问题的解决方法.slide-15 在实现中, set命令直接实现为一个多数派写, 这一步非常简单. 而inc操作逻辑上也很简单, 读取一个变量的值i₁, 给它加上一个数字得到i₂, 再通过多数派把i₂写回到系统中.slide-16 冰雪如你一定已经看到了这种实现方式中的问题: 如果有2个并发的客户端进程同时做这个inc的操作, 在多数派读写的实现中, 必然会产生一个Y客户端覆盖X客户端的问题. 从而产生了数据更新点的丢失.而paxos就是为了解决这类问题提出的, 它需要让Y能检测到这种并发冲突, 进而采取措施避免更新丢失.slide-17 提取一下上面提到的问题: 让Y去更新的时候不能直接更新i₂, 而是应该能检测到i₂的存在, 进而将自己的结果保存在下一个版本i₃中, 再写回系统中.而这个问题可以转化成: i的每个版本只能被写入一次, 不允许修改. 如果系统设计能满足这个要求, 那么X和Y的inc操作就都可以正确被执行了.slide-18 于是我们的问题就转化成一个更简单, 更基础的问题: 如何确定一个值(例如iⱼ)已经被写入了.直观来看, 解决方法也很简单, 在X或Y写之前先做一次多数派读, 以便确认是否有其他客户端进程已经在写了, 如果有, 则放弃.slide-19 但是!!!, 这里还有个并发问题, X和Y可能同时做这个写前读取的操作, 并且同时得出一个结论: 还没有其他进程在写入, 我可以写. 这样还是会造成更新丢失的问题.slide-20 为了解决上面的问题, 存储节点还需要增加一个功能, 就是它必须记住谁最后一个做过写前读取的操作. 并且只允许最后一个完成写前读取的进程可以进行后续写入, 同时拒绝之前做过写前读取的进程写入的权限.可以看到, 如果每个节点都记得谁读过, 那么当Y最后完成了写前读取的操作后, 整个系统就可以阻止过期的X的写入.这个方法之所以能工作也是因为多数派写中, 一个系统最多只能允许一个多数派写成功. paxos也是通过2次多数派读写来实现的强一致.slide-21 以上就是paxos算法的全部核心思想了, 是不是很简单? 剩下的就是如何实现的简单问题了: 如何标识一个客户端如X和Y, 如何确认谁是最后一个完成写前读写的进程, 等等.slide-22 Leslie Lamport 就这么把这么简单的一个算法写了个paper就获得了图领奖! 骚年, 改变世界就这么容易!paxos算法描述接下来的篇幅中我们将用计算机的语言准确的描述整个paxos运行的过程.slide-23 首先明确要解决的问题:slide-24 我们要介绍的paxos实际上是最朴实的classic paxos, 在这之后我们顺提下几个老爷子对paxos的优化, multi paxso和fast paxos, 它们都是针对paxos的理论层面的优化.slide-25 paxos算法中解决了如何在不可靠硬件基础上构建一个可靠的分布式系统的方法. 但paxos核心算法中只解决网络延迟/乱序的问题, 它不试图解决存储不可靠和消息错误的问题, 因为这两类问题本质上跟分布式关系不大, 属于数据校验层面的事情.有兴趣可以参考 Byzantine Paxos#Byzantine_Paxos) 的介绍.slide-26 本文尽量按照 Classic Paxos 的术语来描述,老爷子后面的一篇 Fast Paxos 实现了fast-paxos, 同时包含了classic-paxos, 但使用了一些不同的术语表示.Proposer 可以理解为客户端.Acceptor 可以理解为存储节点.Quorum 在99%的场景里都是指多数派, 也就是半数以上的Acceptor.Round 用来标识一次paxos算法实例, 每个round是2次多数派读写: 算法描述里分别用phase-1和phase-2标识. 同时为了简单和明确, 算法中也规定了每个Proposer都必须生成全局单调递增的round, 这样round既能用来区分先后也能用来区分不同的Proposer(客户端).slide-27 在存储端(Acceptor)也有几个概念:last_rnd 是Acceptor记住的最后一次进行写前读取的Proposer(客户端)是谁, 以此来决定谁可以在后面真正把一个值写到存储中.v 是最后被写入的值.vrnd 跟v是一对, 它记录了在哪个Round中v被写入了.v和vrnd是用于恢复一次未完成的paxos用的. 一次未完成的paxos算法运行可能留下一些没有达到多数派的值的写入(就像原生的多数派写的脏读的问题), paxos中通过vrnd来决定哪些值是最后写入的, 并决定恢复哪个未完成的paxos运行. 后面我们会通过几个例子来描述vrnd的作用.slide-28 首先是paxos的phase-1, 它相当于之前提到的写前读取过程. 它用来在存储节点(Acceptor)上记录一个标识: 我后面要写入; 并从Acceptor上读出是否有之前未完成的paxos运行. 如果有则尝试恢复它; 如果没有则继续做自己想做的事情.我们用类似yaml的格式来描述phase-1的请求/应答的格式:phase-1成后, acceptor应该记录X的rnd=1, 并返回自己之前保存的v和vrnd.slide-29 Proposer X收到多数(quorum)个应答, 就认为是可以继续运行的.如果没有联系到多于半数的acceptor, 整个系统就hang住了, 这也是paxos声称的只能运行少于半数的节点失效.这时Proposer面临2种情况:所有应答中都没有任何非空的v, 这表示系统之前是干净的, 没有任何值已经被其他paxos客户端完成了写入(因为一个多数派读一定会看到一个多数派写的结果). 这时Proposer X继续将它要写的值在phase-2中真正写入到多于半数的Acceptor中.如果收到了某个应答包含被写入的v和vrnd, 这时, Proposer X 必须假设有其他客户端(Proposer) 正在运行, 虽然X不知道对方是否已经成功结束, 但任何已经写入的值都不能被修改!, 所以X必须保持原有的值. 于是X将看到的最大vrnd对应的v作为X的phase-2将要写入的值.这时实际上可以认为X执行了一次(不知是否已经中断的)其他客户端(Proposer)的修复.slide-30 在第2阶段phase-2, Proposer X将它选定的值写入到Acceptor中, 这个值可能是它自己要写入的值, 或者是它从某个Acceptor上读到的v(修复).同样用类似yaml的方式描述请求应答:slide-31 当然这时(在X收到phase-1应答, 到发送phase-2请求的这段时间), 可能已经有其他Proposer又完成了一个rnd更大的phase-1, 所以这时X不一定能成功运行完phase-2.Acceptor通过比较phase-2请求中的rnd, 和自己本地记录的rnd, 来确定X是否还有权写入. 如果请求中的rnd和Acceptor本地记录的rnd一样, 那么这次写入就是被允许的, Acceptor将v写入本地, 并将phase-2请求中的rnd记录到本地的vrnd中.用例子看paxos运行好了paxos的算法描述也介绍完了. 这些抽象的算法描述, 其中的规则覆盖了实际所有可能遇到的情况的处理方式. 一次不太容易看清楚它们的作用, 所以我们接下来通过几个例子来看看paxos如何处理各种不同状态并最终使整个系统的状态达成一致.slide-32 没冲突的例子不解释了 slide-33 X和Y同时运行paxos, Y迫使X中断的例子:X成功完成了写前读取(phase-1), 将rnd=1写入到左边2个Acceptor.Y用更大的rnd=2, 覆盖了X的rnd, 将rnd=2写入到右边2个Acceptor.X以为自己还能运行phase-2, 但已经不行了, X只能对最左边的Acceptor成功运行phase-2, 而中间的Acceptor拒绝了X的phase-2.Y对右边2个Acceptor成功运行了phase-2, 完成写入v=y, vrnd=2.slide-34 继续上面的例子, 看X如何处理被抢走写入权的情况:这时X的phase-2没成功, 它需要重新来一遍, 用更大的rnd=3.X成功在左边2个Acceptor上运行phase-1之后, X发现了2个被写入的值: v=x, vrnd=1 和 v=y, vrnd=2; 这时X就不能再写入自己想要写入的值了. 它这次paxos运行必须不能修改已存在的值, 这次X的paxos的运行唯一能做的就是, 修复(可能)已经中断的其他proposer的运行.这里v=y, vrnd=2 是可能在phase-2达到多数派的值. v=x, vrnd=1不可能是, 因为其他proposer也必须遵守算法约定, 如果v=x, vrnd=1在某个phase-2达到多数派了, Y一定能在phase-1中看到它, 从而不会写入v=y, vrnd=2.因此这是X选择v=y, 并使用rnd=3继续运行, 最终把v=y, vrnd=3写入到所有Acceptor中.slide-35 Paxos 还有一个不太重要的角色Learner, 是为了让系统完整加入的, 但并不是整个算法执行的关键角色, 只有在最后在被通知一下.Paxos 优化slide-36 第一个优化 multi-paxos:paxos诞生之初为人诟病的一个方面就是每写入一个值就需要2轮rpc:phase-1和phase-2. 因此一个寻常的优化就是用一次rpc为多个paxos实例运行phase-1.例如, Proposer X可以一次性为i₁~i₁₀这10个值, 运行phase-1, 例如为这10个paxos实例选择rnd为1001, 1002…1010. 这样就可以节省下9次rpc, 而所有的写入平均下来只需要1个rpc就可以完成了.这么看起来就有点像raft了:再加上commit概念(commit可以理解为: 值v送达到多数派这件事情是否送达到多数派了),和组成员变更(将quorum的定义从”多于半数”扩展到”任意2个quourm必须有交集”).slide-37 第二个优化 fast-paxos:fast-paxos通过增加quorum的数量来达到一次rpc就能达成一致的目的. 如果fast-paxos没能在一次rpc达成一致, 则要退化到classic paxos.slide-38 fast-paxos为了能在退化成classic paxos时不会选择不同的值, 就必须扩大quorum的值. 也就是说fast-round时, quorum的大小跟classic paxos的大小不一样. 同样我们先来看看为什么fast-quorum不能跟classic-quorum一样, 这样的配置会引起classic阶段回复时选择错误的值 y₀:slide-39 要解决这个问题, 最粗暴的方法是把fast-quorum设置为n, 也就是全部的acceptor都写入成功才认为fast-round成功(实际上是退化到了主从同步复制). 这样, 如果X和Y两个proposer并发写入, 谁也不会成功, 因此X和Y都退化到classic paxos进行修复, 选任何值去修复都没问题. 因为之前没有Proposer认为自己成功写入了.如果再把问题深入下, 可以得出, 如果classic paxos的quorum是n/2+1, 那么fast-round的quorum应该是大于¾n, ¾的由来可以简单理解为: 在最差情况下, 达到fast-quorum的acceptor在classic-quorum中必须大于半数, 才不会导致修复进程选择一个跟fast-round不同的值.slide-40 下面是一个fast-round中X成功, Y失败的冲突的例子:X已经成功写入到4(fast-quorum>¾n)个acceptor, Y只写入1个, 这时Y进入classic-round进行息修复, 可以看到, 不论Y选择哪3(classic quorum)个acceptor, 都可以看到至少2个x₀, 因此Y总会选择跟X一样的值, 保证了写入的值就不会被修改的条件.slide-41 再来看一个X和Y都没有达到fast-quorum的冲突:这时X和Y都不会认为自己的fast-round成功了, 因此修复过程选择任何值都是可以的. 最终选择哪个值, 就回归到X和Y两个classic-paxos进程的竞争问题了. 最终会选择x₀或y₀中的一个.其他slide-42 一个很容易验证的优化, 各种情况下都能得到一致的结果.slide-43 广告页, 不解释了 本次的 pdf 可以下载和在线看哦:可靠分布式系统-paxos的直观解释.pdf可靠分布式系统-paxos的直观解释.html本文链接: https://blog.openacid.com/algo/paxos/

点赞 0
0 条评论
从通用规则中学习Nginx模块的定制指令 陶辉 发表于 : 2020-09-21 10:48

上一篇文章中,我介绍了如何定制属于你自己的Nginx,本文将介绍nginx.conf文件的配置语法、使用方式,以及如何学习新模块提供的配置指令。每个Nginx模块都可以定义自己的配置指令,所以这些指令的格式五花八门。比如content_by_lua_block后跟着的是Lua语法,limit_req_zone后则跟着以空格、等号、冒号等分隔的多个选项。这些模块有没有必然遵循的通用格式呢?如果有,那么掌握了它,就能快速读懂生产环境复杂的nginx.conf文件。其次,我们又该如何学习个性化十足的模块指令呢?其实,所有Nginx模块在介绍它的配置指令时,都遵循着相同的格式:Syntax、Default、Context、Description,这能降低我们的学习门槛。如果你还不清楚这一套路,那就只能学习其他文章翻译过的二手知识,效率很低。比如搭建静态资源服务用到的root、alias指令,该如何找到、阅读它的帮助文档?为什么官方更推荐使用root指令?alias指令又适合在哪些场景中使用呢? nginx.conf配置文件中的语法就像是一门脚本语言,你既可以定义变量(set指令),也可以控制条件分支(if指令),还有作用域的概念(server{}块、location{}块等)。所以,为复杂的业务场景写出正确的配置文件,并不是一件很容易的事。为此,Nginx特意针对vim编辑器提供了语法高亮功能,但这需要你手动打开,尤其是include文件散落在磁盘各处时。本文将会系统地介绍nginx.conf配置文件的用法,并以搭建静态资源服务时用到的root、alias指令为例,看看如何阅读Nginx模块的指令介绍。同时,本文也是Nginx开源社区基础培训系列课程第一季,即6月11日晚第2次视频直播的部分文字总结。快速掌握Nginx配置文件的语法格式Nginx是由少量框架代码、大量模块构成的,其中,Nginx框架会按照特定的语法,将配置指令读取出来,再交由模块处理。因此,Nginx框架定义了通用的语法规则,而Nginx模块则定义了每条指令的语法规则,作为初学者,如果将学习目标定为掌握所有的配置指令,方向就完全错了,而且这是不可能完成的任务。比如,ngx_http_lua_module模块定义了content_by_lua_block指令,只要它符合框架定义的{}块语法规则,哪怕大括号内是一大串Lua语言代码,框架也会把它交由ngx_http_lua_module模块处理。因此,下面这行指令就是合法的:content_by_lua_block {ngx.say("Hello World ")}再比如,ngx_http_limit_req_module模块定义了limit_req_zone指令,只要它符合指令行语法(以分号;结尾),框架就会将指令后的选项将由模块处理。所以,即使下面这行指令出现了r/s(每秒处理请求数)这样新定义的单位,仍然是合法的:limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;所以,在我看来,只要弄清楚了以下2点,就能快速掌握Nginx配置文件,:1.     Nginx框架定义了每条指令的基本格式,这是所有模块必须遵守的规则,这包括以下5条语法:n  通过{}大括号作为分隔符的配置块语法。比如http{ }、location{ }、upstream{ }等等,至于配置块中究竟是放置Javascript语言、Lua语言还是字符串、数字,这完全由定义配置块的Nginx模块而定。n  通过;分号作为分隔符的指令语法。比如root html;就打开了静态资源服务。n  以#作为关键字的注释语法。比如#pid logs/nginx.pid;指令就是不会生效的。n  以$作为关键字的变量语法。变量是Nginx模块之间能够互相配合的核心要素,也是Nginx与管理员之间的重要接口,通过$变量名的形式,就可以灵活控制Nginx模块的行为。下一篇文章我会详细介绍Nginx变量。n  include指令可以将其他配置文件载入到nginx.conf中,这样可以提升配置的可维护性。例如includemime.types;语句,就将Content-Type与文件后缀名的映射关系,放在了独立的mime.types文件中,降低了耦合性。2.     Nginx框架为了提高模块解析指令选项的效率,提供了一系列通用的工具函数,绝大多数模块都会使用它们,毕竟这降低了模块开发的难度以及用户的学习成本。比如,当配置文件中包含字节数时,Nginx框架提供了ngx_conf_set_size_slot函数, 各模块通过它就可以解析以下单位: 空间单位 意义 k/K KB m/M MB g/G GB 因此,limit_req_zone指令中zone=one:10m中就定义10MB的共享内存,这替代了很不好理解的10485760字节。再比如,读取时间可以使用以下单位: 时间单位 意义 ms 毫秒 s 秒 m 分钟 h 小时 d 天 w 周 M 月 y 年 这样,ssl_session_cache shared:SSL:2h;指令就设置TLS会话信息缓存2小时后过期。除以上规则外,如果编译了pcre开发库后,你还可以在nginx.conf中使用正则表达式,它们通常以~符号打头。如何使用Nginx配置文件?掌握了语法规则后,nginx.conf配置文件究竟是放在哪里的呢?编译Nginx时,configure脚本的--prefix选项可以设置Nginx的运行路径,比如:./configure –prefix=/home/nginx此时,安装后的Nginx将会放在/home/nginx目录,而配置文件就会在/home/nginx/conf目录下。如果你没有显式的指--prefix选项,默认路径就是/usr/local/nginx。由于OpenResty修改了configure文件,因此它的默认路径是/usr/local/openresty/nginx。在默认路径确定后,nginx.conf配置文件就会放在conf子目录中。当然,通过--conf-path选项,你可以分离它们。另外在运行Nginx时,你还可以通过nginx-c PATH/nginx.conf选项,指定任意路径作为Nginx的配置文件。由于配置语法比较复杂,因此Nginx为vim编辑器准备了语法高亮功能。在Nginx源代码中,你可以看到contrib目录,其中vim子目录提高了语法高亮功能:[contrib]# tree vim  vim  |-- ftdetect  | `-- nginx.vim  |-- ftplugin  | `-- nginx.vim  |-- indent  | `-- nginx.vim  `-- syntax   `-- nginx.vim当你将contrib/vim/* 复制到~/.vim/目录时(~表示你当前用户的默认路径,如果.vim目录不存在时,请先用mkdir创建),再打开nginx.conf你就会发现指令已经高亮显示了: 出于可读性考虑,你或许会将include文件放在其他路径下,此时再用vim打开这些子配置文件,可能没有语法高亮效果。这是因为contrib/vim/ftdetect/nginx.vim文件定义了仅对4类配置文件使用语法高亮规则://对所有.nginx后缀的配置文件语法高亮  au BufRead,BufNewFile *.nginx set ft=nginx  //对/etc/nginx/目录下的配置文件语法高亮  au BufRead,BufNewFile */etc/nginx/* set ft=nginx  //对/usr/local/nginx/conf/目录下的配置文件语法高亮  au BufRead,BufNewFile */usr/local/nginx/conf/* set ft=nginx  //对任意路径下,名为nginx.conf的文件语法高亮  au BufRead,BufNewFile nginx.conf set ft=nginx因此,你可以将这类文件的后缀名改为.nginx,或者将它们移入/etc/nginx/、/usr/local/nginx/conf/目录即可。当然,你也可以向ftdetect/nginx.vim添加新的识别目录。即使拥有语法高亮功能,对于生产环境中长达数百、上千行的nginx.conf,仍然难以避免出现配置错误。这时可以通过nginx -t或者nginx -T命令,检查配置语法是否正确。出现错误时,Nginx会在屏幕上给出错误级别、原因描述以及到底是哪一行配置出现了错误。例如:# nginx -t  nginx: [emerg] directive "location" has no opening "{" in /usr/local/nginx/conf/notflowlocation.conf:1281  nginx: configuration file /usr/local/nginx/conf/nginx.conf test failed从上面的错误信息中,我们知道Nginx解析配置文件失败,错误发生在include的子配置文件/usr/local/nginx/conf/notflowlocation.conf的第1281行,从描述上推断是location块的配置出现了错误,可能是缺失了大括号,或者未转义的字符导致无法识别出大括号。当你修改完配置文件后,可以通过nginx -s reload命令重新载入指令。这一过程不会影响正在服务的TCP连接,在描述Nginx进程架构的文章中,我会详细解释其原因。搭建静态资源服务,root与alias有何不同?接下来我们以root和alias指令为例,看看如何掌握配置指令的使用方法。配置指令的说明,被放置在它所属Nginx模块的帮助文档中。因此,如果你对某个指令不熟悉,要先找到所属模块的说明文档。对于官方模块,你可以进入nginx.org站点查找。搭建静态资源服务的root/alias指令是由ngx_http_core_module模块实现的,因此,我们可以进入http://nginx.org/en/docs/http/ngx_http_core_module.html页面寻找指令介绍,比如root指令的介绍如下所示:Syntax:root path;  Default: root html;  Context:http, server, location, if in location这里Syntax、Default、Context 3个关键信息,是所有Nginx配置指令共有的,下面解释下其含义:l  Syntax:表示指令语法,包括可以跟几个选项,每个选项的单位、分隔符等。rootpath指令,可以将URL映射为磁盘访问路径path+URI,比如URL为/img/a.jpg时,磁盘访问路径就是html/img/a.jpg。注意,这里path既可以是相对路径,也可以是绝对路径。作为相对路径,path的前缀路径是由configure --prefix指定,也可以在运行时由nginx -p path指定。l  Default:表示选项的默认值,也就是说,即使你没有在nginx.conf中写入root指令,也相当于配置了root html;l  Context:表示指令允许出现在哪些配置块中。比如root可以出现在server{}中,而alias则只能出现在location{}中。为什么root指令的Context,允许其出现在http{ }、server{ }、location { }、if { }等多个配置块中呢?这是因为,Nginx允许多个配置块互相嵌套时,相同指令可以向上继承选项值。例如下面两个配置文件是完全等价的:server{   root html;   location / {   } }  server{   root html;   location / {   root html;   } }这种向上承继机制,可以简化Nginx配置文件。因此,使用root指令后,不用为每个location块重复写入root指令。相反,alias指令仅能放置在location块中,这与它的使用方式有关:Syntax:alias path;  Default:—  Context:locationalias的映射关系与其所属的location中匹配的URI前缀有关,比如当HTTP请求的URI为/a/b/c.html时,在如下配置中,实际访问的磁盘路径为/d/b/c.html:location /a {   alias /d;  }因此,当URI中含有磁盘路径以外的前缀时,适合使用alias指令。反之,若完整的URI都是磁盘路径的一部分时,则不妨使用root指令。学习其他指令时,如果你不清楚它属于哪一个模块,还可以查看以字母表排序的指令索引http://nginx.org/en/docs/dirindex.html页面,点击后会进入所属模块的指令介绍页面。如果是第三方模块,通常在README文件中会有相应的指令介绍。比如OpenResty模块的指令会放在GitHub项目首页的README文件中: 而TEngine模块的指令介绍则会放在tengine.taobao.org网站上: 从这两张截图中可以看到,第三方模块在解释指令的用法时,同样遵循着上文介绍过的方式。小结本文介绍了Nginx配置文件的使用方法。学习Nginx的通用语法时,要先掌握Nginx框架解析配置文件的5条基本规则,这样就能读懂nginx.conf的整体结构。其次,当模块指令包含时间、空间单位时,会使用Nginx框架提供的通用解析工具,熟悉这些时、空单位会降低你学习新指令的成本。配置文件的位置,可以由编译期configure脚本的—prefix、--conf-path选项指定,也可以由运行时的-p选项指定。复杂的配置文件很容易出错,通过nginx -t/T命令可以检测出错误,同时屏幕上会显示出错的文件、行号以及原因,方便你修复Bug。用vim工具编辑配置文件时,将Nginx源码中contrib/vim/目录复制到~/.vim/目录,就可以打开语法高亮功能。对于子配置文件,只有放置在/etc/nginx或者/usr/local/nginx/conf目录中,或者后缀为.nginx时,才会高亮显示语法。当然,你可以通过ftdetect/nginx.vim文件修改这一规则。学习模块指令时,要从它的帮助文档中找到指令的语法、默认值、上下文和描述信息。比如,root和alias的语法相似,但alias没有默认值,仅允许出现在location上下文中,这实际上与它必须结合URI前缀来映射磁盘路径有关。由于每个Nginx模块都能定义独特的指令,这让nginx.conf变成了复杂的运维界面。在掌握了基本的配置语法,以及第三方模块定义指令时遵循的潜规则后,你就能游刃有余地编写Nginx配置文件。 

点赞 3
0 条评论
都是事件驱动,为什么Nginx的性能远高于Redis? 陶辉 发表于 : 2020-09-16 09:31

谈到Redis缓存,我们描述其性能时会这么说:支持1万并发连接,几万QPS。而我们描述Nginx的高性能时,则会宣示:支持C10M(1千万并发连接),百万级QPS。Nginx用C语言开发,而Redis是用同一家族的C++语言开发的,C与C++在性能上是同一级数的。Redis与Nginx同样使用了事件驱动、异步调用、Epoll这些机制,为什么Nginx的并发连接会高出那么多呢?(本文不讨论Redis分布式集群)这其实是由进程架构决定的。为了让进程占用CPU的全部计算力,Nginx充分利用了分时操作系统的特点,比如增加CPU时间片、提高CPU二级缓存命中率、用异步IO和线程池的方式回避磁盘的阻塞读操作等等,只有清楚了Nginx的这些招数,你才能将Nginx的性能最大化发挥出来。为了维持Worker进程间的负载均衡,在1.9.1版本前Nginx使用互斥锁,基于八分之七这个阈值维持了简单、高效的基本均衡。而在此之后,使用操作系统提供的多ACCEPT队列,Nginx可以获得更高的吞吐量。本文将会沿着高性能这条主线介绍Nginx的Master/Worker进程架构,包括进程间是如何分担流量的,以及默认关闭的多线程模式又是如何工作的。同时,本文也是Nginx开源社区基础培训系列课程第一季,即6月18日晚第3次直播课的部分文字总结。如何充分使用多核CPU?由于散热问题,CPU频率已经十多年没有增长了。如下图统计了1970年到2018年间CPU性能的变化,可以看到表示频率的绿色线从2005年后就不再升高,服务器多数都在使用能耗比更经济的2.x GHz CPU: (图片来源:https://www.karlrupp.net/2018/02/42-years-of-microprocessor-trend-data/)我们知道,CPU频率决定了它的指令执行速度,当频率增长陷入瓶颈,这意味着所有单进程、单线程的软件性能都无法从CPU的升级上获得提升,包括本文开头介绍的Redis服务就是如此。如果你熟悉JavaScript语言,可能使用过NodeJS这个Web服务,虽然它也是高并发服务的代表,但同样受制于单进程、单线程架构,无法充分CPU资源。CPU厂商对这一问题的解决方案是横向往多核心发展,因此上图中表示核心数的黑色线至2005年后快速上升。由于操作系统使用CPU核心的最小单位是线程,要想同时使用CPU的所有核心,软件必须支持多线程才行。当然,进程是比线程更大的调度粒度,所以多进程软件也是可以的,比如Nginx。下图是Nginx的进程架构图,可以看到它含有4类进程:1个Master管理进程、多个Worker工作进程、1个CacheLoader缓存载入进程和1个Cache Manager缓存淘汰进程。 其中,Master是管理进程,它长期处于Sleep状态,并不参与请求的处理,因此几乎不消耗服务器的IT资源。另外,只有在开启HTTP缓存后,Cache Loader和Cache Manager进程才存在,其中,当Nginx启动时加载完磁盘上的缓存文件后,Cache Loader进程也会自动退出。关于这两个 Cache进程,我会在后续介绍Nginx缓存时中再详细说明。负责处理用户请求的是Worker进程,只有Worker进程能够充分的使用多核CPU,Nginx的QPS才能达到最大值。因此,Worker进程的数量必须等于或者大于CPU核心的数量。由于Nginx采用了事件驱动的非阻塞架构,繁忙时Worker进程会一直处于Running状态,因此1个Worker进程就能够完全占用1个CPU核心的全部计算力,如果Worker进程数超过了CPU核心数,反而会造成一些Worker进程因为抢不到CPU而进入Sleep状态休眠。所以,Nginx会通过下面这行代码自动获取到CPU核心数:ngx_ncpu = sysconf(_SC_NPROCESSORS_ONLN);如果你在nginx.conf文件中加入下面这行配置:worker_processes auto;Nginx就会自动地将Worker进程数设置为CPU核心数:if (ngx_strcmp(value[1].data, "auto") == 0) {  ccf->worker_processes = ngx_ncpu;   return NGX_CONF_OK; }为了防止服务器上的其他进程占用过多的CPU,你还可以给Worker进程赋予更高的静态优先级。Linux作为分时操作系统,会将CPU的执行时间分为许多碎片,交由所有进程轮番执行。这些时间片有长有短,从5毫秒到800毫秒不等,内核分配其长短时,会依据静态优先级和动态优先级。其中,动态优先级由内核根据进程类型自动决定,比如CPU型进程就能比IO型进程获得更长的时间片,而静态优先级可以通过setpriority函数设置。在Linux中,静态优先级共包含40级,从-20到+19不等,其中-20表示最高优先级。进程的默认优先级是0,所以你可以通过调级优先级,让Worker进程获得更多的CPU资源。在nginx.conf文件中,worker_priority配置就能设置静态优先级,比如:worker_priority -10;由于每个CPU核心都拥有一级、二级缓存(Intel的Smart Cache三级缓存是所有核心共享的),为了提高这两级缓存的命中率,还可以将Worker进程与CPU核心绑定在一起。CPU缓存由于离计算单元更近,而且使用了更快的存储介质,所以二级缓存的访问速度不超过10纳秒,相对应的,主存存取速度至少在60纳秒以上,因此频繁命中CPU缓存,可以提升Nginx指令的执行速度。在nginx.conf中你可以通过下面这行配置绑定CPU:worker_cpu_affinity auto;Nginx的多进程架构已经能够支持C10M级别的高并发了,那么Nginx中的多线程又是怎么回事呢?这要从Linux文件系统的非阻塞调用说起。Worker进程上含有数万个并发连接,在处理连接的过程中会产生大量的上下文切换。由于内核做一次切换的成本大约有5微秒,随着并发连接数的增多,这一成本是以指数级增长的。因此,只有在用户态完成绝大部分的切换,才有可能实现高并发。想做到这一点,只有完全使用非阻塞的系统调用。对于网络消息的传输,non-block socket可以完美实现。然而,Linux上磁盘文件的读取却是个大问题。我们知道,机械硬盘上定位文件很耗时,由于磁盘转速难以提高(服务器磁盘的转速也只有10000转/秒),所以定位操作需要8毫秒左右,这是一个很高的数字。写文件时还可以通过Page Cache磁盘高速缓存的write back功能,先写入内存再异步回盘,读文件时则没有好办法。虽然Linux提供了原生异步IO系统调用,但在内存紧张时,异步AIO会回退到阻塞API(FreeBSD操作系统上的AIO没有这个问题)。所以,为了缩小阻塞API的影响范围,Nginx允许将读文件操作放在独立的线程池中执行。比如,你可以通过下面这2条配置,为HTTP静态资源服务开启线程池:thread_pool name threads=number [max_queue=number]; aio threads[=pool];这个线程池是运行在Worker进程中的,并通过一个任务队列(max_queue设置了队列的最大长度),以生产者/消费者模型与主线程交换数据,如下图所示: 在极端场景下(活跃数据占满了内存),线程池可以将静态资源服务的性能提升9倍,具体测试可以参见这篇文章:https://www.nginx.com/blog/thread-pools-boost-performance-9x/。到这里你可能有个疑问:又是多进程,又是多线程,为什么Nginx不索性简单点,全部使用多线程呢?这主要由2个原因决定:l  首先,作为高性能负载均衡,稳定性非常重要。由于多线程共享同一地址空间,一旦出现内存错误,所有线程都会被内核强行终止,这会降低系统的可用性;l  其次,Nginx的模块化设计允许第三方代码嵌入到核心流程中执行,这虽然大大丰富了Nginx生态,却也引入了风险。因此,Nginx宁肯选用多进程模式使用多核CPU,而只以多线程作为补充。Worker进程间是如何协同处理请求的?当一个进程监听了80端口后,其他进程绑定该端口时会失败,通常你会看到“Address already in use”的提示。那么,所有Worker进程同时监听80或者443端口,又是怎样做到的呢?如果你用netstat命令,可以看到只有进程ID为2758的Master进程监听了端口: Worker进程是Master的子进程,用ps命令可以从下图中看到,2758有2个Worker子进程,ID分别为3188和3189: 由于子进程自然继承了父进程已经打开的端口,所以Worker进程也在监听80和443端口,你可以用lsof命令看到这一点: 我们知道,TCP的三次握手是由操作系统完成的。其中,成功建立连接的socket会放入ACCEPT队列,如下图所示: 这样,当Worker进程通过epoll_wait的读事件(Master进程不会执行epoll_wait函数)获取新连接时,就由内核挑选1个Worker进程处理新连接。早期Linux内核的挑选算法很糟糕,特别是1个新连接建立完成时,内核会唤醒所有阻塞在epoll_wait函数上的Worker进程,然而,只有1个Worker进程,可以通过accept函数获取到新连接,其他进程获取失败后重新休眠,这就是曾经广为人知的“惊群”现象。同时,这也很容易造成Worker进程间负载不均衡,由于每个Worker进程绑定1个CPU核心,当部分Worker进程中的并发TCP连接过少时,意味着CPU的计算力被闲置了,所以这也降低了系统的吞吐量。Nginx早期解决这一问题,在通过应用层accept_mutex锁完成的,在1.11.3版本前它是默认开启的:accept_mutex on;这把锁的工作原理是这样的:同一时间,通过抢夺accept_mutex锁,只有唯一1个持有锁的Worker进程才能将监听socket添加到epoll中:if (ngx_shmtx_trylock(&ngx_accept_mutex)) {  ngx_listening_t* ls = cycle->listening.elts;   for (ngx_uint_ti = 0; i < cycle->listening.nelts; i++) {  ngx_connection_t * c = ls[i].connection;  if (ngx_add_event(c->read, NGX_READ_EVENT, 0) == NGX_ERROR) {  return NGX_ERROR;  }  } }这就解决了“惊群”问题,我们再来看负载均衡功能是怎么实现的。在nginx.conf中可以通过下面这行配置,设定每个Worker进程能够处理的最大并发连接数:worker_connections number;当空闲连接数不足总数的八分之一时,Worker进程会大幅度降低获取accept_mutex锁的概率:ngx_int_tngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n; if (ngx_accept_disabled > 0) {  ngx_accept_disabled--; } else {  if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {  return; }  ... }我们还可以通过accept_mutex_delay配置控制负载均衡的执行频率,它的默认值是500毫秒,也就是最多500毫秒后,并发连接数较少的Worker进程会尝试处理新连接:accept_mutex_delay 500ms;当然,在1.11.3版本后,Nginx默认关闭了accept_mutex锁,这是因为操作系统提供了reuseport(Linux3.9版本后才提供这一功能)这个更好的解决方案。我们先来看一下处理新连接的性能对比图: 上图中,横轴中的default项开启了accept_mutex锁。可以看到,使用reuseport后,QPS吞吐量有了3倍的提高,同时处理时延有明显的下降,特别是时延的波动(蓝色的标准差线)有大幅度的下降。reuseport能够做到这么高的性能,是因为内核为每个Worker进程都建立了独立的ACCEPT队列,由内核将建立好的连接按负载分发到各队列中,如下图所示: 这样,Worker进程的执行效率就高了很多!如果你想开启reuseport功能,只需要在listen指令后添加reuseport选项即可: 当然,Master/Worker进程架构带来的好处还有热加载与热升级。在https://www.nginx-cn.net/article/70这篇文章中,我对这一流程有详细的介绍。小结最后对本文的内容做个总结。材料、散热这些基础科技没有获得重大突破前,CPU频率很难增长,类似Redis、NodeJS这样的单进程、单线程高并发服务,只能向分布式集群方向发展,才能继续提升性能。Nginx通过Master/Worker多进程架构,可以充分使用服务器上百个CPU核心,实现C10M。 为了榨干多核CPU的价值,Nginx无所不用其极:通过绑定CPU提升二级缓存的命中率,通过静态优先级扩大时间片,通过多种手段均衡Worker进程之间的负载,在独立线程池中隔离阻塞的IO操作,等等。可见,高性能既来自于架构,更来自于细节。 

点赞 6
1 条评论
打开NGINX Unit世界 洪志道 发表于 : 2020-08-31 22:08

引子我是NGINX Unit的贡献者,Unit是我非常喜欢的一个开源软件。 我将写一系列Unit的文章分享Unit的世界,相信这个优秀的软件会有非常好的前景。  NGINX Unit是什么?NGINX Unit是一个全新的,由NGINX作者亲自设计,带领NGINX核心团队开发的纯c软件。官方的定义:Unit是一个动态的web和应用服务器。因此它的三大核心为:动态,web和应用。    Unit总体架构  后续会有专门文章分析Unit构架设计,敬请关注。动态动态指两部分,动态配置和应用进程的动态管理。这里只介绍动态配置,这是它最大的亮点之一。 动态配置一直是NGINX软件的缺陷,重新设计的Unit没有这个问题。 简单说,Unit已经没有配置文件。Unit提供了http API接口,所有配置的更新都通过RESTful方式操作。    应用Unit是个多语言应用软件,它支持同时多个语言,甚至同个语言的不同版本,比如python2和python3,php5和php7。NGINX还有个问题,它不支持应用开发。是的,lua模块已经能做非常多的应用了。但是官方想支持更多主流的语言,于是有了这个设计。   webUnit已经支持了static和proxy两个功能,还比较粗糙。相信这些核心功能未来能跟nginx一样完善。其它Unit已经支持TLS,HTTP/2也在计划当中。此外不得不提的是Unit支持了类似容器的名称空间(namespace)和文件系统隔离(file system isolation)。  Unit搭建文件服务器1. 安装> git clone git@github.com:nginx/unit.git && cd unit > ./configure && make  2. 启动> ./build/unitd  3. 配置> cat config.json {         "listeners": {                 "127.0.0.1:80": {                         "pass": "routes"                 }         },         "routes": [                 {                         "action": {                                 "share": "/var/www/"                         }                 }         ] } EOF  > curl -X PUT --data-binary @config.json --unix-socket control.unit.sock http://localhost/config {     "success": "Reconfiguration done." } 4. 访问> curl http://127.0.0.1:80  更多请看官方文档 下篇介绍:Unit架构设计  

点赞 4
1 条评论
SamW_NGINX 2020-09-12 19:15
非常好!,期待系列。
点赞 0
nginx源码分析之变量设计 洪志道 发表于 : 2020-08-31 14:36

nginx的配置文件使用简单灵活,某些部分还具备脚本语言的特点,变量就是其中一个特色。本文将分析变量是如何设计实现的。 0. 什么是变量脚本语言都有变量这个东西,其作用就是让内容可变,用名称代替可变的内容,所以变量具有赋值和取值的特点。nginx的变量跟php一样,以$开头。两种用法:赋值:set $some_var nginx;取值:$some_var; 1.整体设计  * 创建所有变量只能在配置文件解析,也就是工作进程启动之前创建,有些是内置的变量,有些是自定义的变量。没什么区别,比如 $http_host是内置变量,set $some_var some_val。通过set指定创建的是自定义变量,当然也可以其它方式,如果你自己写模块的话。这时用到 cmcf->variables_keys, cmcf->variables 两个数组,数组元素类型为ngx_http_variable_t。  * 使用 变量使用(即拿它的值)要先获取索引(发生在配置阶段),这是为了加快访问速度,然后根据索引拿它的值(发生在运行阶段)。这时用到r->variables数组,数组元素为ngx_http_variable_value_t(ngx_variable_value_t的别名)。配置时:n = ngx_http_get_variable_index(cf, name); 运行时:v = ngx_http_get_indexed_variable(r, n);  cmcf->variables_keys 创建的变量都存在这个数组cmcf->varialles 使用的变量(为了拿索引)都存储在这里。nginx会在init conf时检查cmcf->variables的所有变量必须在cmcf->variables_keys里。r->variables 将cmcf->variables导入到这里,为了更方便处理,这时只需要拿值 2、创建变量nginx有内置的变量,分布在好多个模块里,这些变量在配置解析之前构建完成,接下来解析配置,可能碰到变量(内置或自定义),最后处理所有的变量。preconfigure : 添加内置变量,比如 $request, $server_name, $args 等。parse             : 添加自定义变量。init                 : 处理所有变量,比如自定义变量没有对应相同的内置变量,当作错误处理。如果最终处理成功,会有个hash存储这些变量的数据。我们看下内部如何实现的:  * preconfigure阶段:cmcf->variables_keys:所有内置变量都会添加到这个数组里。每个成员的结构体是ngx_http_variable_ttypedef struct {    ngx_str_t                     name;   /* must be first to build the hash */    ngx_http_set_variable_pt      set_handler;    ngx_http_get_variable_pt      get_handler;    uintptr_t                     data;    ngx_uint_t                    flags; # 这个比较重要,看下面解释    ngx_uint_t                    index;} ngx_http_variable_t;   name是名称,set_handler和get_handler分别用于赋值和取值,需要配合data,这几个比较简单。index是索引的意思,可以通过根据这个值拿到对应的变量,具体后面会再讲到。  flags是变量标记,不同的标记使其用法和用途不同,有NGX_HTTP_VAR_CHANGEABLE,NGX_HTTP_VAR_NOCACHEABLE标记。有NGX_HTTP_VAR_CHANGEABLE标记意味着变量是可变的。比如 $server_name是不可变的,你不能这样操作 set $server_name "err"; $args是可变的,就可以这样操作 set $args "ok"; 内置变量在源码里都有指定它的标记,自定义变量都是可变的。  3.使用变量要获取nginx的变量的值有两种方式:索引和变量名  *索引 如前面介绍,先在配置阶段拿索引,然后在运行阶段根据索引拿值 *变量名 ngx_http_variable_value_t * ngx_http_get_variable(ngx_http_request_t *r, ngx_str_t *name, ngx_uint_t key); 在整个配置文件解析处理后,nginx会构造一个hash:cmcf->variables_hash,存储所有的变量。可想而知,根据变量名就可以快速拿到对应的变量。当然用索引的方式更快,至于选哪种视情况而定了。 4. 总结要理解变量,要先理解nginx的两个阶段,解析阶段和运行阶段。解析阶段尽量做事前工作,如创建变量、拿索引等。到了运行阶段就可以快速的拿变量的值。还可以看出nginx的一个重要设计,解析时函数的参数基本有 ngx_conf_t *cf,到了运行阶段就是 ngx_http_request_t *r了。使用到的东西也更简化了,比如变量在解析阶段,需要有get和set(可选),但到了运行阶段,只需要拿值。这种细微的设计变化,可以好好思考,并转化成自己的理解。我经常推荐看nginx源码的同学可以从ngx_log_t *log这个东西入手,因为它反映了整个nginx的生命周期。

点赞 3
0 条评论
nginx源码分析之缓存设计 洪志道 发表于 : 2020-08-31 14:35

nginx一向以高性能著称,缓存是其中一大利器,本文将分享nginx中各个级别缓存的原理和内部是如何实现的。 0.什么是缓存缓存存在计算机中的各个领域,其目的都是为了提升处理速度,可以说是以空间换取了时间。但事实并不是这么简单,因为引入了缓存,就要处理一致性、过期处理、缓存策略和命中率等。例如CPU有几级缓存,memcached作为内存缓存服务器,redis也是,还有文件缓存服务器,CDN等。总而言之,访问目标后‘备份’到某中间层,下次再访问时,直接从中间层获取,加快了访问速度。所以目标越往后,处理速度越慢,程序要做的就是尽量命中在前面,不要让访问穿透到后面目标。 1.nginx缓存nginx属于web应用服务器,是典型的request/response模式。整体而言,nginx的缓存可以总结为3个:304响应、静态级别缓存、动态级别缓存。下面一一介绍:  *304响应这个其实不算真正意义上的缓存,之所以提出来,是因为它的重要性,也因为涉及客户端的缓存。当客户端访问某URL时,如果服务器响应304状态码,其意义是告诉客户端您访问的东西没有变化过,内容从自己本地获取。浏览器就是典型的应用。当你第一次访问某静态资源时,响应200,当按F5时,会响应304。实现原理是nginx根据头部信息if_modified_since与静态资源的时间戳比较,如果一样,则返回304。做web开发应该清楚这个状态码。  *静态级别缓存location /test {    open_file_cache  max=100;}这个级别的缓存是缓存静态资源文件的句柄。nginx处理一个请求时,会打开一个文件句柄,然后在请求处理结束后关闭它。加入open_file_cache后,就会将文件句柄保存在工作进程中(不是共享内存),只要工作进程存在,当后面的请求访问同样的文件时,就不用重新打开文件。这样的好处是节省了文件打开关闭的开销。  *动态级别缓存http {    ...    fastcgi_cache_path  /tmp/abc keys_zone=c1:10M;    server {        location ~ \.php$ {            ...            fastcgi_cache  c1;            fastcgi_cache_valid  200 30s;        }    }} 这个级别的缓存是缓存动态请求的内容。当客户端访问某动态文件时(http://example.com/test.php),nginx将请求重新封装成fastcgi协议,然后转发给php-fpm处理,当php-fpm正常响应后,nginx将响应内容保存到某目录的文件里。下次同样的请求过来,就直接从缓存目录的文件中读取,比第一次快且省事很多。  聪明的你可能已经想到动态级别缓存可以再加上静态级别缓存,没错的。这3种响应是独立的,可以灵活配合使用。现在您已经明白缓存的原理,接下来看下如何实现的。  2.304响应ngx_http_not_modified_filter_module处理了这个功能,属于http request header filter模块。源码非常简单。static ngx_int_t ngx_http_not_modified_header_filter(ngx_http_request_t *r){    ...    if (r->headers_in.if_modified_since) {         if (r->headers_in.if_modified_since            && ngx_http_test_if_modified(r))        {            return ngx_http_next_header_filter(r);        }         /* not modified */        r->headers_out.status = NGX_HTTP_NOT_MODIFIED;        return ngx_http_next_header_filter(r);    }     return ngx_http_next_header_filter(r);} static ngx_uint_t ngx_http_test_if_modified(ngx_http_request_t *r){    ...     if (r->headers_out.last_modified_time == (time_t) -1) {        return 1;    }     clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);     if (clcf->if_modified_since == NGX_HTTP_IMS_OFF) {    ims = ngx_parse_http_time(r->headers_in.if_modified_since->value.data,                              r->headers_in.if_modified_since->value.len);     if (ims == r->headers_out.last_modified_time) {        return 0;    }    if (clcf->if_modified_since == NGX_HTTP_IMS_EXACT        || ims < r->headers_out.last_modified_time)    {        return 1;    }    return 0;} 3.静态级别缓存typedef struct {    ngx_rbtree_t             rbtree;    ngx_rbtree_node_t        sentinel;    ngx_queue_t              expire_queue;    ngx_uint_t               current;    ngx_uint_t               max;    time_t                   inactive; } ngx_open_file_cache_t; 记得这个只在工作进程中处理,不涉及共享内存。看的出来ngx_open_file_cache_t是一个红黑树,也是一个队列。每个文件的文件(根据完整文件名)对应一个节点。这样就完整的维护了所有的可能要缓存的文件。又因为缓存需要处理过期,所以最近访问的文件会保持在expire_queue队列的前面,每次访问将从队列中删除,然后移到最前面。整个逻辑封装在一个函数里:ngx_int_t ngx_open_cached_file(ngx_open_file_cache_t *cache, ngx_str_t *name, ngx_open_file_info_t *of, ngx_pool_t *pool) { ... } 4.动态级别缓存ngx_http_file_cache_t {    ngx_http_file_cache_sh_t {        tree        queue        ...    }}ngx_http_cache_t {    keys    key    ngx_http_file_cache_node_t}如果简化点处理过程就是:根据url算出md5,然后将动态请求响应内容存储到文件名为某md5的文件里。http://example.com/a.php -> d41d8cd98f00b204e9800998ecf8427e -> /tmp/abc/d41d8cd98f00b204e9800998ecf8427ehttp://example.com/b.php -> d41d8cd98f00b204e9800998ecf8432d -> /tmp/abc/d41d8cd98f00b204e9800998ecf8432dhttp://example.com/c.php -> d41d8cd98f00b204e9800998ecf84a32 -> /tmp/abc/d41d8cd98f00b204e9800998ecf84a32因为文件存储在硬盘里,所以这里用到共享内存,而不是工作进程的内存中,来保存这种对应关系。我们可以将每个请求(r)当作各个缓存节点,对应ngx_http_cache_t。ngx_http_file_cache_t就是缓存的集合。明白这个关系后,我们解释下整个流程是如何从开始到结束的。  a)根据fastcgi_cache_path的keys_zone,nginx创建了多个 ngx_http_file_cache_t。根据fastcgi_cache指定了某个location将用到哪个ngx_http_file_cache_t。b)为每个请求创建一个ngx_http_cache_t(在ngx_http_file_cache_new函数里),然后继续产生一个key,作为本节点的索引。你可以将请求、请求响应内容、内容存储文件都当作同一个东西,最终都是节点,对应ngx_http_cache_t。所以需要有       key这个东西。c)处理缓存目录和文件名d)nginx处理动态请求是在ngx_event_pipe里的,当需要缓存里,pipe会有个temp_path将响应内容存储到这个文件里e)最后将pipe的temp_path重命名成前面产生的文件名,结束了。 5.小结动态请求的缓存处理代码还是有点复杂的,有兴趣的同学研究下其如何实现很有好处,可以体会下nginx的代码精致程度。如一惯风格,写作主要以白话方式分享原理,细节点到即止,细节上有疑问欢迎交流。

点赞 4
0 条评论