为什么需要为Nginx创建模块

Nginx是一个Web服务器,可以解决许多业务任务,可以灵活配置,扩展并可以在几乎所有OS和平台上运行。 开箱即用的功能,功能和问题清单可在一本小手册中进行描述。 但是有时候,只有通过为nginx开发自己的模块才能解决许多业务任务。 这些是面向业务的模块,包含一些业务逻辑,而不仅仅是通用的系统解决方案。



通常,nginx中的所有内容都是曾经由某人编写的模块。 因此,在nginx下编写模块不仅是可能的,而且是必要的。 何时需要这样做以及为什么这样做, Vasily SoshnikovdedokOne )将举例说明几种情况。

让我们谈谈鼓励使用C语言编写模块的原因,nginx的体系结构和核心,HTTP模块,C模块,NJS,Lua和nginx.conf的结构。 重要的是,不仅要了解在nginx下进行开发的人员,而且还必须对在nginx中使用nginx-configs,Lua或其他语言的人员有所了解。

注意:本文基于Vasily Soshnikov的报告。 该报告将不断更新和更新。 资料中的信息是非常技术性的,为了充分利用该信息,读者需要具有平均水平及更高水平的使用nginx代码的经验。


简要介绍nginx


您与nginx一起使用的只是模块 。 nginx配置中的每个指令都是一个单独的模块,由nginx社区的同事精心编写。

nginx.conf中的指令也是解决特定问题的模块 。 因此,nginx模块中包含了所有内容。 add_header,proxy_pass,任何指令-这些是根据某些规则工作的模块或模块的组合。

Nginx是一个具有以下功能的框架 :网络和文件I / O,共享内存,配置和脚本。 这是一个庞大的低层库层,您可以在其中进行任何操作以使用网络驱动器。

Nginx快速,稳定,但是复杂 。 您应该编写这样的代码,以免失去nginx的这些品质。 不稳定的nginx在生产上是不满意的客户,所有由此产生的后果。

为什么要创建自己的模块


将HTTP协议转换为另一个协议。 这是经常激励创建特定模块的主要原因。

例如,memcached_pa​​ss模块将HTTP转换为另一个协议,并且您可以使用其他外部系统。 proxy_pass模块还允许您从HTTP转换为HTTP。 另一个很好的例子是fastcgi_pass。

这些都是以下形式的指令:“到某某后端,不是HTTP(但在proxy_pass HTTP的情况下)。”

动态内容插入:AdBlock绕过,广告插入。 例如,我们有一个后端,因此有必要修改来自它的内容。 例如,AdBlock,它分析广告插入代码,我们需要处理它-以一种或另一种方式对其进行调整。

嵌入内容经常需要做的另一件事是HLS缓存问题。 当参数被缓存在HLS内时,则两个用户可以获得相同的会话或相同的参数。 从那里,在需要跟踪某些内容时可以剪切或添加一些参数。

从Internet /移动仪表收集Clickstream数据。 在我的实践中很流行。 通常,这是在nginx上完成的,但不是在access.log上完成的,而是更加智能。

转换各种内容。 例如,使用rtmp模块不仅可以使用rtmp,还可以使用HLS。 这个模块可以处理很多视频内容。

通用授权点:SEP或Api网关。 当nginx作为基础架构的一部分时就是这种情况:授权,收集指标,将数据发送到监视和ClickStream。 Nginx在这里用作基础结构中心-后端的单个入口点。

丰富其后续跟踪的请求。 现代系统非常复杂,具有构成不同团队的几种后端。 通常,它们很难启动,有时甚至很难理解请求的来源和去向。 为了简化调试,一些大公司使用棘手的技术-他们将某些数据添加到请求中。 用户不会看到它们,但是从此数据可以很容易地跟踪系统内部的请求路径。 这称为跟踪

S3-代理。 今年,我经常看到人们通过s3处理对象。 但是没有必要在C模块上执行此操作,nginx中的基础结构也足够了。 为了解决其中一些问题,您可以使用Lua,NJS上正在解决某些问题。 但是有时候有必要用C编写模块。

什么时候创建模块


有两个条件可以了解时机已到。

功能泛化。 当您了解到其他人需要您的产品时,则应将其走私到Open Source中,创建通用功能,发布并使用它。

解决业务问题。 当企业设置只能通过为其nginx编写其自身的模块才能满足的要求时。 例如,动态插入/更改内容,ClickStream收集可以在Lua上完成,但很可能将无法正常工作。

Nginx架构


我已经写了很长一段时间的nginx代码。 我的模块中有9个正在生产中,其中一个是在开源中,还有许多在生产中。 因此,我有经验和理解。
Nginx是一个嵌套娃娃,其中的所有内容都是围绕内核构建的。
所以我了解nginx。
核心是epoll的包装器。
Epoll是一种允许您与任何描述符文件(不仅仅是套接字)异步工作的方法,因为描述符不仅是套接字。

核心之上是上游,HTTP和脚本。 通过脚本,我的意思是nginx.conf,而不是NJS。 在上游,HTTP和脚本编制之上,已经构建了HTTP模块,我们将在后面进行讨论。



上游服务器和HTTP的经典示例是上游服务器-配置中的指令。 HTTP模块的一个示例是add_header。 脚本的一个示例是配置文件本身。 该文件包含nginx组成的模块;可以通过某种方式对其进行解释,并允许您以管理员或用户身份执行操作。

我们不会在上游简单地考虑核心和驻留,因为它是nginx内部的一个独立的世界。 关于它们的故事值得几篇文章。

HTTP模块剖析


即使您没有在nginx中编写C代码,而是使用它,也请记住主要规则。
在nginx中,一切都遵循责任链-COR模式。
我不知道如何将其翻译成俄文,但我将描述其逻辑。 您的请求从位置开始,经过一系列配置的链模块。 这些模块中的每个模块均返回结果。 如果结果不好,则链条中断。



在NJS和Lua中开发模块或使用某些指令时,请不要忘记您的代码可能会使该链的执行崩溃。

与责任链最相似是Bash代码行:

grep -RI pool nginx | awk -F":" '{print $1}' | sort -u | wc -l 

在代码中,一切都非常简单:如果AWK落在行的中间,则将不会执行sort和以下命令。 nginx模块的工作原理与此类似,但事实是nginx中存在,您可以解决此问题-重新启动代码。 但是,您应该准备崩溃并运行,就像在配置中使用的模块一样,但事实并非如此。

HTTP模块的类型


HTTP和nginx是许多不同的阶段。

  • 相位处理-PHASE处理程序
  • 过滤器-正文/标题过滤器 。 此过滤器是标头或请求正文。
  • 代理 。 典型的代理模块是proxy_pass,fastcgi_pass,memcached_pa​​ss。
  • 特定负载平衡模块-负载平衡器 。 这是最不易扭曲的模块类型,尚未开发太多。 一个示例是Ketama CHash模块,它允许您在nginx内进行一致的哈希处理,以将请求分发到后端。

我将介绍每种类型及其用途。

相位处理程序


想象我们有几个阶段,从访问阶段开始。 每个阶段都有几个模块。 例如,ACCESS阶段分为连接,对nginx的请求,用户授权的验证。 每个模块都是链中的一个单元。 同相可以有无数个这样的模块。



最后的最后一个处理程序是CONTENT阶段,其中内容按需交付。
方式总是这样:请求-处理程序链-输出内容。
NGINX来源的模块开发人员可以使用的阶段:

 typedef enum { NGX_HTTP_POST_READ_PHASE = 0, NGX_HTTP_SERVER_REWRITE_PHASE, NGX_HTTP_FIND_CONFIG_PHASE, NGX_HTTP_REWRITE_PHASE, NGX_HTTP_POST_REWRITE_PHASE, NGX_HTTP_PREACCESS_PHASE, NGX_HTTP_ACESS_PHASE, NGX_HTTP_POST_ACESS_PHASE, NGX_HTTP_PRECONTENT_PHASE, NGX_HTTP_CONTENT_PHASE, NGX_HTTP_LOG_PHASE, } ngx_http_phases; 

阶段可以被覆盖,添加您自己的处理程序。 如果您不是nginx core的开发人员,那么现实生活中并不需要全部。 因此,我不会谈论每个阶段,而只会谈论我使用的主要阶段。

主要的是ACCESS_PHASE。 将您的授权添加到nginx尤其有用-在访问方面检查请求的执行情况。

我经常利用的下一个重要阶段是前提和内容阶段。 PRECONTENT_PHASE允许收集有关将作为响应发送给客户端的内容的度量。 CONTENT_PHASE允许基于某些内容生成自己的独特内容。

我经常使用的最后一个阶段是日志记录阶段LOG_PHASE。 顺便说一下,ACCESS_LOG指令在其中起作用。 日志记录阶段有最疯狂的限制,这使我发疯:您不能使用子请求,并且通常不能使用任何请求。 您已经将内容留给了用户,并且处理程序,后处理程序和任何子请求将不会执行。

我将解释为什么令人讨厌。 假设您要在记录阶段跨过nginx和Kafka。 在这一阶段,一切都已经完成:内容,状态,所有数据都有计算得出的大小,但是您不能将其作为子请求。 他们在那里不工作。 您必须在日志记录阶段在裸套接字上进行写操作才能将数据发送到Kafka。

正文/标题过滤器


过滤器有两种类型:主体过滤器和标题过滤器。

主体过滤器的一个示例是gzip过滤器模块。 为什么需要身体过滤器? 假设您有一个特定的proxy_pass,并且想要以某种方式转换内容或对其进行分析。 在这种情况下,应使用“身体”过滤器。

它的工作方式是这样的:您会遇到许多块,您可以对它们做一些事情,查看其内容,集合等。 但是过滤器也有很大的局限性。 例如,如果您决定更改正文-以插入或剪切响应正文,请记住,HTTP属性(例如内容供稿)将被替换。 如果不提供限制并正确反映在代码中,则可能导致奇怪的结果。

每个人都使用过的add_header是Header过滤器的一个示例。 该算法的工作原理与“身体”过滤器相同。 已为客户端准备了响应,并且add_header过滤器允许您在此处执行操作:添加标头,删除标头,替换标头,发送子请求。

顺便说一句,在“正文”过滤器和“标题”过滤器中,有一些子请求可用,您甚至可以将内部标识发送到那里的其他位置。

代理人


这是最复杂和有争议的模块类型,允许您将请求代理到外部系统,例如, 将HTTP转换为另一个协议 。 示例:proxy_pass,redis_pass,tnt_pass。

代理是nginx核心开发人员提出的使编写代理模块更加容易的接口。 如果以经典方式完成此操作,则对于此类代理PHASES处理程序,过滤器,平衡器将被执行。 但是,如果要将HTTP转换为的协议与经典协议有所不同,那么就会出现大问题。 nginx提供的代理API根本不合适-您必须从头开始发明此代理模块。

此类模块的一个很好的例子是postgres_pass。 它允许nginx与PostgreSQL通信。 该模块完全不使用nginx中开发的接口-它具有自己的路径。
记住代理,但最好不要写。 要编写代理,您必须全心学习所有的nginx-这是很长而且很困难的。

负载均衡器


负载均衡器的任务非常简单-以循环模式工作。 假设您有一个上游部分,其中有一些服务器,您指定权重和平衡方法。 这是典型的负载平衡器。

此模式并不总是适用。 因此,开发了Ketama CHash模块,有条件地有可能对某些服务器达成一致的哈希请求。 有时很方便。 Nginx Lua提供balancer_by_lua。 在Lua上,您通常可以编写任何平衡器。

C模块


接下来是我对C模块开发的绝对主观意见。 首先-我的主观规则。

该模块以nginx.conf指令开头。 即使您正在制作仅由您的公司操作的C模块,也要始终考虑指令。 开始使用它们设计模块,因为这是系统管理员将要与其通信的内容。 这很重要-与他或与操作您的C模块的人员协调所有细微差别。 NGINX是一种著名的产品,其指令遵循系统管理员已知的某些法律。 因此,请务必考虑一下。

使用nginx代码样式。 想象一下,您的模块将被另一个人支持。 如果他已经熟悉nginx及其代码样式,那么他将更容易阅读和理解您的代码。

最近,一个来自德国的好朋友要求我帮助他解决nginx代码中的错误。 我不知道他写的是哪种代码风格,但我什至无法正常阅读代码。

使用正确的内存池。 即使您对nginx有很多经验,也请始终牢记这一点。 Nginx的C语言新手开发人员通常犯的一个错误是获取错误的池。

一些背景知识:nginx通常使用弱分配器的思想。 您可以在此处使用malloc,但不建议使用。 它有自己的平板,自己的内存分配器,您需要使用它。 因此,每个对象都有一个指向其池的链接,并且需要使用该池。 一个典型的新手错误是在标头过滤器中使用池连接,而不是池请求。 这意味着,如果我们有一个保持活动的连接,则该池将不断膨胀,直到内存不足或发生其他副作用。 因此,这很重要。

而且,这种错误极难出现。 Valgrind(“ syshniks”会理解)不适用于平板分配-它会显示奇怪的图片。

不要使用阻塞的I / O。 那些想更快地应用外部事物的人的典型错误是使用阻塞的I / O和阻塞的套接字。 您永远无法在nginx中执行此操作-其中有许多进程,但是每个进程使用一个线程。

您可以执行多线程,但是通常,这只会使情况变得更糟。 如果在这样的体系结构中使用阻塞I / O,那么每个人都会等待这个阻塞块。

我将破译我在上面说的话。

该模块以nginx.conf指令开头

确定指令应驻留在哪些数组中:Main,Server,HTTP,位置,if位置。
尽量避免放置,如果-通常会导致对nginx配置非常奇怪的使用。

nginx中的所有指令都生活在不同的上下文和不同的范围内。 add_header指令可以在HTTP级别,位置级别,if位置级别上工作。 通常在文档中对此进行描述。
了解您的指令可以在哪些级别上执行,以及在哪里执行指令:PHASE Handler,Body / Header过滤器。
这很重要,因为在nginx中配置被冻结。 按照惯例,当您在上面的某个位置编写add_header时,此值将在底部已经存在的add_header中进行平滑处理。 因此,您将添加两个标题。 这适用于任何指令。

如果指定主机端口,则反之亦然-套接字池。 应该指出一次。

总的来说,我会禁止任何合并-您只是不需要它。 因此,您应该始终清楚地确定您的指令或指令集位于配置中的哪个Nginx数组中。

很好的例子:

 location /my_location/ { add_header “My-Header” “my value”; } 

这里add_header只是添加到位置。 相同的add_header可能在上方,并且所有内容都会被扭曲。 这是有据可查的行为。
考虑什么可能阻碍指令的执行。
假设您正在开发“身体”过滤器。 如前所述,nginx只是将您的模块放在一个公共链中,并且不能保证gzip模块在编译时不会进入Body过滤器的链中。 在这种情况下,如果有人打开gzip模块,则数据将发送到您的gzip模块。 这可能会威胁您根本无法对内容执行任何操作。 例如,您可以重新gzip,但是从CPU角度来看这是一种嘲弄。

相同的规则适用于所有阶段处理程序-无法保证谁会在之前被调用,谁会在之后被调用。 因此,尊重将被召唤的那个人,并记住某些gzip或其他内容可能会意外地飞向您。

Nginx代码样式


在创建产品时,请记住有人会支持它。 不要忘记代码样式nginx。
在编写nginx模块之前,请熟悉一下源代码: 第二

如果将来您从事nginx模块的开发,那么您将充分了解nginx的来源。 您会喜欢它们,因为没有文档 。 当您需要将一些片段从nginx传输到模块时,您将很好地学习nginx目录结构,学习使用Grep(可能是Sed)。

内存池


池必须正确使用。 例如,“ r-> connection-> pool!= R-> pool”。 在处理请求时,决不能使用内存池配置,它会膨胀直到nginx重新启动。

了解对象的寿命。 假设请求重播恰好具有此管道生存期。 在这个游泳池中,您可以放置​​很多东西并腾出空间。 理论上,连接可以无限期地存在-最好在其中放置真正重要的内容。

尽量不要使用外部分配器,例如malloc / free 。 这会对内存碎片产生不良影响。 如果您处理大量数据并使用大量malloc,则此nginx的速度会非常慢。

对于Valgrind的爱好者来说,有一种 黑客可以让您使用Valgrind调试nginx池。 如果您在nginx上有很多C代码,那么这很重要,因为即使是经验丰富的开发人员也可能会出错。

阻止I / O

这里的一切都很简单-不要使用阻塞I / O。
否则,至少保持连接会出现问题,但最大程度地,一切都会在很长一段时间内正常工作。

我知道有人在阻止模式下在nginx中使用Quora的情况(不要问为什么)。 这导致了一个事实,即保持活动的连接会放弃其活动并始终超时。 最好不要这样做-一切都会长时间低效运行,并且您将不得不扭曲一百万个超时,因为nginx会在许多事情上开始超时。

但是C模块还有另一种选择-NJS和Lua。

当您不需要开发C模块时


今年,我有了NJS的第一手工作经验,给人一种主观的印象,甚至意识到那里缺少的东西,所以一切都很好。 我还想谈一谈我在nginx下开发Lua的经验,并分享Lua中存在的问题。

Lua / LuaJit Essentials


Nginx不使用Lua,而是使用LuaJit。 但这不是Lua,因为Lua已经改进了两个版本,而LuaJit则停留在过去的某个地方。 作者实际上并没有开发LuaJit-他经常住在叉子里。 最新的fork是LuaJit2 。 这在同一OpenResty中增加了奇怪的情况。

垃圾收集器需要注意 。 LuaJit无法解决此问题-提出一些解决方法。 在巨大的负载下,当客户端上会显示很多保持活动状态的Garbage Collector时,图表上出现了故障,并显示了500个错误。 Internet上有很多有关此的信息。

字符串实现会导致性能问题 。 这只是LuaJit的弊端,在Lua中得到了修复。 LuaJit中字符串的实现完全违反了任何逻辑。 行以最疯狂的方式减慢速度,这与内部实现相关。

无法使用许多现成的库 。 Lua最初是阻塞的,因此Lua和LuaJit上的大多数库都使用阻塞的I / O。 由于nginx不会阻塞,因此无法在nginx内部使用使用任何阻塞I / O的现成库。 这将减慢nginx。

使用LuaJit的原因与使用模块的原因相同:

  • 复杂模块的原型;
  • HMAC,SHA的授权计算;
  • 平衡器 ;
  • 小型应用程序:标头处理程序,重定向规则;
  • 计算nginx.conf的变量。

哪里不使用LuaJit更好?
主要规则:不要在Lua上处理巨大的物体-这是行不通的。
Lua上的内容处理程序也不起作用 。 尝试将逻辑减至最少。 一个简单的平衡器将起作用,但是Lua上的侧栏将非常差。

共享内存或垃圾收集器将会出现。 不要将共享内存与Lua一起使用-垃圾收集器将迅速并有保证地将整个大脑带出生产。

不要将协程与大量保持活力的化合物一起使用。 协程会在LuaJit垃圾收集器内产生更多的垃圾,这很糟糕。

如果您已经在使用LuaJit,请记住:

  • 关于内存监控;
  • 监测和优化垃圾收集器的工作;
  • 关于垃圾收集器的工作原理,如果您确实为LuaJit编写了一个复杂的应用程序,因为您必须添加一些新内容。

NJS


当我在NGINX Conf上时,他们说服我不要用C编写代码会很酷。我认为我必须尝试一下,这就是我所得到的。

授权书 它有效,代码简单,不影响速度-一切都很好。 我最初使用的小原型是10行代码。 但是这10行对s3进行了授权。

计算nginx.conf的变量。 使用NJS可以计算许多变量。 在nginx内部,这很酷。 Lua中有这样的功能,但是有一个垃圾收集器,所以它不是很酷。

但是,并非一切都那么好。 为了在NJS上做一些很棒的事情,他错过了一些事情。

共享内存 。 我修补了共享内存,这是我自己的叉子,所以现在就足够了。

支持更多阶段的过滤器 。 在NJS中,只有内容阶段和变量,并且头过滤器非常缺乏。 您必须编写拐杖来添加很多标题。 没有足够的主体过滤器来处理复杂的逻辑或处理内容。

有关如何监视和配置文件的信息 。 我现在知道如何使用,但是我必须研究资料。 没有足够的信息或工具来进行正确的分析。 如果是,它将隐藏在找不到的地方。 同时,关于在哪里可以使用NJS以及在哪里不能使用NJS的信息不足。

C模块 。 我渴望扩展NJS。

后记


为什么要创建自己的模块? 解决一般和业务问题。

什么时候需要在C中实现模块? 如果没有其他选择。 例如,沉重的负担,内容的插入或基本的硬件节省。 然后必须在C中确保做到这一点。在大多数情况下,Lua或NJS是合适的。 但是您必须始终思考。

然后在Lua上? 当您不能用C语言编写时。例如,您不需要使用巨大的RPS转换请求正文。 您的客户数量正在增长,在某个时候您将不再应付-考虑一下。

NJS? 当LuaJit完全厌倦了其垃圾收集器和字符串时。 例如,授权在Lua上生成了许多垃圾对象,但这并不重要。 但是,这反映在监视和烦人中。 现在它已不再出现在我的监视中,并且一切都变得很好。

在HighLoad ++ 2019上,瓦西里·索什尼科夫(Vasily Soshnikov)将继续nginx模块的主题,并更多地谈论NJS,不要忘记与LuaJit和C进行比较。

请参阅网站上的报告的完整列表 ,并于11月7日至8日在针对高负载系统开发人员的最大型会议上与您见面。 在时事通讯电报频道中关注我们的新想法。

Source: https://habr.com/ru/post/zh-CN471684/


All Articles