热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

OpenResty最佳实践(2)

此文已由作者汤晓静授权网易云社区发布。欢迎访问网易云社区,了解更多网易技术产品运营经验。文章前部分用

此文已由作者汤晓静授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。

lua 协程与 nginx 事件机制结合

文章前部分用大量篇幅阐述了 lua 和 nginx 的相关知识,包括 nginx 的进程架构,nginx 的事件循环机制,lua 协程,lua 协程如何与 C 实现交互;在了解这些知识之后,本节阐述 lua 协程是如何和 nginx 的事件机制协同工作。

从 nginx 的架构和事件驱动机制来看, nginx 的并发处理模型概括为:单 worker + 多连接 + epoll + callback。 即每个 nginx worker 同时处理了大量连接,每个连接对应一个 http 请求,一个 http 请求对应 nignx 中的一个结构体(ngx_http_request_t):

struct ngx_http_request_s {
    uint32_t                          signature;         /* "HTTP" */

    ngx_connection_t                 *connection;    void                            **ctx;    void                            **main_conf;    void                            **srv_conf;    void                            **loc_conf;

    ngx_http_event_handler_pt         read_event_handler;
    ngx_http_event_handler_pt         write_event_handler;

    ....
}

结构体中的核心成员为 ngx_connection_t *connection,其定义如下:

struct ngx_connection_s {    void               *data;
    ngx_event_t        *read;      // epoll 读事件对应的结构体成员
    ngx_event_t        *write;     // epoll 写事件对应的结构体成员

    ngx_socket_t        fd;        // tcp 对应的 socket fd

    ngx_recv_pt         recv;
    ngx_send_pt         send;
    ngx_recv_chain_pt   recv_chain;
    ngx_send_chain_pt   send_chain;

    ngx_listening_t    *listening;

    ...
}

从如上结构体可知,每个请求中对应的 ngx_connection_t 中的读写事件和 epoll 关联;nginx epoll 的事件处理核心代码如下:

...

    events = epoll_wait(ep, event_list, (int) nevents, timer);    for (i = 0; i read;
        rev->handler(rev);

        ...

        wev = c->write;
        wev->handler(ev);

        ...
    }

nginx epoll loop 中调用 epoll_wait 获取 epoll 接管的激活事件,并通过 c 的指针强转,得到 ngx_connection_t 获取对应的连接和连接上对应的读写事件的回调函数,即通过 C 结构体变量成员之间的相关关联来串联请求和事件驱动,实现请求的并发处理;这里其实和高级语言的面向对象的写法如出一辙,只是模块和成员变量之间的获取方式的差异。

如果引入 lua 的协程机制,在 lua 代码中出现阻塞的时候,主动调用 coroutine.yield 将自身挂起,待阻塞操作恢复时,再将挂起的协程调用 coroutine.resume 恢复则可以避免在 lua 代码中写回调;而何时恢复协程可以交由 c 层面的 epoll 机制来实现,则可以实现事件驱动和协程之间的关联。现在我们只需要考虑,如何将 lua_State 封装的 lua land 和 C land 中的 epoll 机制融合在一起。

事实上 lua-nginx-module 确实是按照这种方式来处理协程与 nginx 事件驱动之间的关系,lua-nginx-module 为每个 nginx worker 生成了一个 lua_state 虚拟机,即每个 worker 绑定一个 lua 虚拟机,当需要 lua 脚本介入请求处理流程时,基于 worker 绑定的虚拟机创建 lua_coroutine 来处理逻辑,当阻塞发生、需要挂起时或者处理逻辑完成时挂起自己,等待下次 epoll 调度时再次唤醒协程执行。如下是 rewrite_by_lua 核心代码部分:

tatic ngx_int_tngx_http_lua_rewrite_by_chunk(lua_State *L, ngx_http_request_t *r){
    co = ngx_http_lua_new_thread(r, L, &co_ref);

    lua_xmove(L, co, 1);
    ngx_http_lua_get_globals_table(co);
    lua_setfenv(co, -2);

    ngx_http_lua_set_req(co, r);       // 此处设置协程与 ngx_http_request_t 之间的关系

    ...

    rc = ngx_http_lua_run_thread(L, r, ctx, 0);  // 运行 lua 脚本处理 rewrite 逻辑

    if (rc == NGX_ERROR || rc > NGX_OK) {        return rc;
    }

    ...
}

从上述代码片段中我们看到了协程与 ngx 请求之间的绑定关系,那么只要在 ngx_http_lua_run_thread 函数中(实际上是在 lua 脚本中)处理何时挂起 lua 的执行即可。大部分时候我们在 lua 中的脚本工作类型分两种,一种是基于请求信息的逻辑改写,一种是基于 tcp 连接的后端交互。逻辑改写往往不会发生 io 阻塞,即当前脚本很快执行完成后回到 C land,不需要挂起再唤醒的流程。而对于方式二,lua-nginx-module 提供了 cosocket api, 它封装了 tcp api,并且会在合适的时候(coroutine.yield 的调用发生在 IO 异常,读取包体完毕,或者 proxy_buffers 已满等情形,具体的实现读者可以参考 ngx_http_lua_socket_tcp.c 源码)调用 coroutine.yield 方法 。 OpenResty 最佳实践 (2)

综上所述,结合lua 协程和 nginx 事件驱动机制,使用 OpenResty 可以使用 lua 脚本方便的扩展 nignx 的功能。

OpenResty hooks (编程钩子)

OpenResty 最佳实践 (2)

init_by_lua

该阶段主要用于预加载一些 lua 模块, 如加载全局 json 模块:require 'cjson.safe';设置全局的 lua_share_dict 等,并且可以利用操作系统的 copy-on-write 机制;reload nginx 会重新加载该阶段的代码。

init_worker_by_lua

该阶段可用于为每个 worker 设置独立的定时器,设置心跳检查等。

rewrite_by_lua

实际场景中应用最多的一个 hooks 之一,可用于请求重定向相关的逻辑,如改写 host 头,改写请求参数和请求路径等

access_by_lua

该阶段可用于实现访问控制相关的逻辑,如动态限流、限速,防盗链等

content_by_lua

该阶段用于生成 http 请求的内容,和 proxy_pass 指令冲突;二者在同一个阶段只能用一个。该阶段可用于动态的后端交互,如 mysql 、 redis 、kafaka 等;也可用于动态的 http 内容生成,如使用 lua 实现 c 的 slice 功能,完成大文件的分片切割。

banalce_by_lua

该阶段可用于动态的设置 proxy_pass 的上游地址,例如用 lua 实现一个带监控检测机制的一致性 hash 轮序后端算法,根据上游的响应动态设置该地址是否可用。

body_filter_by_lua

用于过滤和加工响应包体,如对 chunk 模式的包体进行 gzip; 也可以根据包体的大小来动态设置 ngx.var.limit_rate.

header_filter_by_lua

调整发送给 client 端的响应头,也是最常用的 hooks 之一;比如设置响应的 server 头,修缓存头 cache-control 等。

log_by_lua

一方面可以设置 nginx 日志输出的字段值,另一方面我们也可以用 cosocket 将日志信息发送到指定的 http server;因响应头和响应体已发送给客户端,该阶段的操作不会影响到客户端的响应速度。

OpenResty 之 lua 编写常见陷阱

  • elseif,区别于 else if;

  • and & or,不支持问号表达式;lua 中 0 表示 true;

  • no continue,lua 中不支持 continue 语法;需要用 if 和 else 语句实现;

  • . & :,lua 中 object.method 和 object:method 行为不同,object:method 为语法糖,会扩展成第一个参数为 self

  • forgot return _M,在编写模块的时候如果最后忘记 return _M, 调用时会提示尝试对 string 调用方法的异常

OpenResty 编程优化

  • do local statement,尽量使用 local 化的变量声明,加速变量索引速度的同时避免全局命名空间的污染;

  • do not use blocked api,不要调用会阻塞 lua 协程的 api,比如 lua 原生的 socket,会造成 nginx worker block;

  • use ngx.ctx instead of ngx.var,ngx.var 会调用 ngx.var 的变量索引系统,比 ngx.ctx 低效很多;

  • decrease table resize,避免 lua table 表的 resize 操作,可以用 luajit 事先声明指定大小的 table。比如频繁的 lua 字符串相加的 .. 操作,当 lua 预分配内存不够时,会重新动态扩容(和 c++ vector 类型),会造成低效;

  • use lua-resty-core,使用 lua-resty-core api,该部分 api 用 luajit 的 ffi 实现比直接的 C 和 lua 交互高效;

  • use jit support function,少用不可 jit 加速的函数,那些函数不能 jit 支持,可以参看 luajit 文档。

  • ffi,对自己实现的 C 接口,也建议用 ffi 暴露出接口给 lua 调用。

nginx 易混易错配置说明

so_keepalive

用于 listen 中,探测连接保活; 采用TCP连接的C/S模式软件,连接的双方在连接空闲状态时,如果任意一方意外崩溃、当机、网线断开或路由器故障,另一方无法得知TCP连接已经失效,除非继续在此连接上发送数据导致错误返回。很多时候,这不是我们需要的。我们希望服务器端和客户端都能及时有效地检测到连接失效,然后优雅地完成一些清理工作并把错误报告给用户。

如何及时有效地检测到一方的非正常断开,一直有两种技术可以运用。一种是由TCP协议层实现的Keepalive,另一种是由应用层自己实现的心跳包。

TCP默认并不开启Keepalive功能,因为开启 Keepalive 功能需要消耗额外的宽带和流量,尽管这微不足道,但在按流量计费的环境下增加了费用,另一方面,Keepalive设置不合理时可能会因为短暂的网络波动而断开健康的TCP连接。并且,默认的Keepalive超时需要7,200,000 milliseconds,即2小时,探测次数为 5 次。系统默认的 keepalive 配置如下:

net.ipv4.tcpkeepaliveintvl = 75
net.ipv4.tcpkeepaliveprobes = 5
net.ipv4.tcpkeepalivetime = 7200

如果在 listen 的时候不设置 so_keepalive 则使用了系统默认的 keepalive 探测保活机制,需要 2 小时才能清理掉这种异常连接;如果在 listen 指令中加入

so_keepalive=30m::10

可设置如果连接空闲了半个小时后每 75s 探测一次,如果超过 10 次 探测失败,则释放该连接。

sendfile/directio

sendfile

copies data between one file descriptor and another. Because this copying is done within the kernel, sendfile() is more efficient than the combination of read(2) and write(2), which would require transferring data to and from user space.

从 Linux 的文档中可以看出,当 nginx 有磁盘缓存文件时候,可以利用 sendfile 特性将磁盘内容直接发送到网卡避免了用户态的读写操作。

directio

Enables the use of the O_DIRECT flag (FreeBSD, Linux), the F_NOCACHE flag (macOS), or the directio() function (Solaris), when reading files that are larger than or equal to the specified size. The directive automatically disables (0.7.15) the use of sendfile for a given request

写文件时不经过 Linux 的文件缓存系统,不写 pagecache, 直接写磁盘扇区。启用aio时会自动启用directio, 小于directio定义的大小的文件则采用 sendfile 进行发送,超过或等于 directio 定义的大小的文件,将采用 aio 线程池进行发送,也就是说 aio 和 directio 适合大文件下载。因为大文件不适合进入操作系统的 buffers/cache,这样会浪费内存,而且 Linux AIO(异步磁盘IO) 也要求使用directio的形式。

proxy_request_buffering

控制处理客户端包体的行为,如果设置为 on, 则 nginx 会接收完 client 的整个包体后处理。如 nginx 作为反向代理服务处理客户端的上传操作,则先接收完包体再转发给上游,这样上游异常的时候,nginx 可以多次重试上传,但有个问题是如果包体过大,nginx 端如果负载较重话,会有大量的写磁盘操作,同时对磁盘的容量也有较高要求。如果设置为 off, 则传输变成流式处理,一个 chunk 一个 chunk 传输,传输出错更多需要 client 端重试。

proxy_buffer_size

Sets the size of the buffer used for reading the first part of the response received from the proxied server. This part usually contains a small response header. By default, the buffer size is equal to one memory page. This is either 4K or 8K, depending on a platform.

proxy_buffers

Sets the number and size of the buffers used for reading a response from the proxied server, for a single connection. By default, the buffer size is equal to one memory page. This is either 4K or 8K, depending on a platform.

proxy_buffering

Enables or disables buffering of responses from the proxied server.

When buffering is enabled, nginx receives a response from the proxied server as soon as possible, saving it into the buffers set by the proxy_buffer_size and proxy_buffers directives. If the whole response does not fit into memory, a part of it can be saved to a temporary file on the disk. Writing to temporary files is controlled by the proxy_max_temp_file_size and proxy_temp_file_write_size directives.

When buffering is disabled, the response is passed to a client synchronously, immediately as it is received. nginx will not try to read the whole response from the proxied server. The maximum size of the data that nginx can receive from the server at a time is set by the proxy_buffer_size directive.

当 proxy_buffering on 时处理上游的响应可以使用 proxy_buffer_size 和 proxy_buffers 两个缓冲区;而设置 proxy_buffering off 时,只能使用proxy_buffer_size 一个缓冲区。

proxy_busy_size

When buffering of responses from the proxied server is enabled, limits the total size of buffers that can be busy sending a response to the client while the response is not yet fully read. In the meantime, the rest of the buffers can be used for reading the response and, if needed, buffering part of the response to a temporary file. By default, size is limited by the size of two buffers set by the proxy_buffer_size and proxy_buffers directives.

当接收上游的响应发送给 client 端时,也需要一个缓存区,即发送给客户端而未确认的部分,这个 buffer 也是从 proxy_buffers 中分配,该指令限定能从 proxy_buffers 中分配的大小。

keepalive

该指令可作用于 nginx.conf 和 upstream 的 server 中;当作用于 nginx.conf 中时,表示作为 http server 端回复客户端响应后,不关闭该连接,让该连接保持 ESTAB 状态,即 keepalive。 当该指令作用于 upstrem 块中时,表示发送给上游的 http 请求加入 connection: keepalive, 让服务端保活该连接。值得注意的是服务端和客户端均需要设置 keepalive 才能实现长连接。 同时 keepalive指令需要和 如下两个指令配合使用:

keepalive_requests 100;keepalive_timeout 65;

keepalive_requests 表示一个长连接可以复用的次数,keepalive_timeout 表示长连接在空闲多久后可以关闭。 keepalive_timeout 如果设置过大会造成 nginx 服务端 ESTAB 状态的连接数增多。

nginx 维护与更新

nginx 信号集和 nginx 操作之间的对应关系如下:

nginx operation signal
reload SIGHUP
reload SIGUSR1
stop SIGTERM
quit SIGQUIT
hot update SIGUSR2 & SIGWINCH & SIGQUIT

stop vs quit

stop 发送 SIGTERM 信号,表示要求强制退出,quit 发送 SIGQUIT,表示优雅地退出。 具体区别在于,worker 进程在收到 SIGQUIT 消息(注意不是直接发送信号,所以这里用消息替代)后,会关闭监听的套接字,关闭当前空闲的连接(可以被抢占的连接),然后提前处理所有的定时器事件,最后退出。没有特殊情况,都应该使用 quit 而不是 stop。

reload

master 进程收到 SIGHUP 后,会重新进行配置文件解析、共享内存申请,等一系列其他的工作,然后产生一批新的 worker 进程,最后向旧的 worker 进程发送 SIGQUIT 对应的消息,最终无缝实现了重启操作。 再 master 进程重新解析配置文件过程中,如果解析失败则会回滚使用原来的配置文件,即 reload 失败,此时工作的还是老的 worker。

reopen

master 进程收到 SIGUSR1 后,会重新打开所有已经打开的文件(比如日志),然后向每个 worker 进程发送 SIGUSR1 信息,worker 进程收到信号后,会执行同样的操作。reopen 可用于日志切割,比如 nginx 官方就提供了一个方案:

$ mv access.log access.log.0
 $ kill -USR1 `cat master.nginx.pid`
 $ sleep 1
 $ gzip access.log.0    # do something with access.log.0

这里 sleep 1 是必须的,因为在 master 进程向 worker 进程发送 SIGUSR1 消息到 worker 进程真正重新打开 access.log 之间,有一段时间窗口,此时 worker 进程还是向文件 access.log.0 里写入日志的。通过 sleep 1s,保证了 access.log.0 日志信息的完整性(如果没有 sleep 而直接进行压缩,很有可能出现日志丢失的情况)。

hot update

某些时候我们需要进行二进制热更新,nginx 在设计的时候就包含了这种功能,不过无法通过 nginx 提供的命令行完成,我们需要手动发送信号。

首先需要给当前的 master 进程发送 SIGUSR2,之后 master 会重命名 nginx.pid 到 nginx.pid.oldbin,然后 fork 一个新的进程,新进程会通过 execve 这个系统调用,使用新的 nginx ELF 文件替换当前的进程映像,成为新的 master 进程。新 master 进程起来之后,就会进行配置文件解析等操作,然后 fork 出新的 worker 进程开始工作。

接着我们向旧的 master 发送 SIGWINCH 信号,然后旧的 master 进程则会向它的 worker 进程发送 SIGQUIT 信息,从而使得 worker 进程退出。向 master 进程发送 SIGWINCH 和 SIGQUIT 都会使得 worker 进程退出,但是前者不会使得 master 进程也退出。

最后,如果我们觉得旧的 master 进程使命完成,就可以向它发送 SIGQUIT 信号,让其退出了。

引用

  • OpenResty 最佳实践

  • lua-nginx-module Readme

  • nginx doc

  • lua 与 c 进行交互

  • 浅谈 nginx 信号集

免费体验云安全(易盾)内容安全、验证码等服务

更多网易技术、产品、运营经验分享请点击。

相关文章:

【推荐】 Kylin存储和查询的分片问题


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 我们


推荐阅读
  • 深入解析 Vue 中的 Axios 请求库
    本文深入探讨了 Vue 中的 Axios 请求库,详细解析了其核心功能与使用方法。Axios 是一个基于 Promise 的 HTTP 客户端,支持浏览器和 Node.js 环境。文章首先介绍了 Axios 的基本概念,随后通过具体示例展示了如何在 Vue 项目中集成和使用 Axios 进行数据请求。无论你是初学者还是有经验的开发者,本文都能为你解决 Vue.js 相关问题提供有价值的参考。 ... [详细]
  • Java高并发与多线程(二):线程的实现方式详解
    本文将深入探讨Java中线程的三种主要实现方式,包括继承Thread类、实现Runnable接口和实现Callable接口,并分析它们之间的异同及其应用场景。 ... [详细]
  • Ihavetwomethodsofgeneratingmdistinctrandomnumbersintherange[0..n-1]我有两种方法在范围[0.n-1]中生 ... [详细]
  • 解决Bootstrap DataTable Ajax请求重复问题
    在最近的一个项目中,我们使用了JQuery DataTable进行数据展示,虽然使用起来非常方便,但在测试过程中发现了一个问题:当查询条件改变时,有时查询结果的数据不正确。通过FireBug调试发现,点击搜索按钮时,会发送两次Ajax请求,一次是原条件的请求,一次是新条件的请求。 ... [详细]
  • 本文详细解析了使用C++实现的键盘输入记录程序的源代码,该程序在Windows应用程序开发中具有很高的实用价值。键盘记录功能不仅在远程控制软件中广泛应用,还为开发者提供了强大的调试和监控工具。通过具体实例,本文深入探讨了C++键盘记录程序的设计与实现,适合需要相关技术的开发者参考。 ... [详细]
  • 深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案
    深入剖析Java中SimpleDateFormat在多线程环境下的潜在风险与解决方案 ... [详细]
  • 如何利用Java 5 Executor框架高效构建和管理线程池
    Java 5 引入了 Executor 框架,为开发人员提供了一种高效管理和构建线程池的方法。该框架通过将任务提交与任务执行分离,简化了多线程编程的复杂性。利用 Executor 框架,开发人员可以更灵活地控制线程的创建、分配和管理,从而提高服务器端应用的性能和响应能力。此外,该框架还提供了多种线程池实现,如固定线程池、缓存线程池和单线程池,以适应不同的应用场景和需求。 ... [详细]
  • 设计实战 | 10个Kotlin项目深度解析:首页模块开发详解
    设计实战 | 10个Kotlin项目深度解析:首页模块开发详解 ... [详细]
  • 在Python多进程编程中,`multiprocessing`模块是不可或缺的工具。本文详细探讨了该模块在多进程管理中的核心原理,并通过实际代码示例进行了深入分析。文章不仅总结了常见的多进程编程技巧,还提供了解决常见问题的实用方法,帮助读者更好地理解和应用多进程编程技术。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • 本文介绍如何使用 Python 的 DOM 和 SAX 方法解析 XML 文件,并通过示例展示了如何动态创建数据库表和处理大量数据的实时插入。 ... [详细]
  • 本文对比了杜甫《喜晴》的两种英文翻译版本:a. Pleased with Sunny Weather 和 b. Rejoicing in Clearing Weather。a 版由 alexcwlin 翻译并经 Adam Lam 编辑,b 版则由哈佛大学的宇文所安教授 (Prof. Stephen Owen) 翻译。 ... [详细]
  • 重要知识点有:函数参数默许值、盈余参数、扩大运算符、new.target属性、块级函数、箭头函数以及尾挪用优化《深切明白ES6》笔记目次函数的默许参数在ES5中,我们给函数传参数, ... [详细]
  • 实验九:使用SharedPreferences存储简单数据
    本实验旨在帮助学生理解和掌握使用SharedPreferences存储和读取简单数据的方法,包括程序参数和用户选项。 ... [详细]
  • 本文提出了一种基于栈结构的高效四则运算表达式求值方法。该方法能够处理包含加、减、乘、除运算符以及十进制整数和小括号的算术表达式。通过定义和实现栈的基本操作,如入栈、出栈和判空等,算法能够准确地解析并计算输入的表达式,最终输出其计算结果。此方法不仅提高了计算效率,还增强了对复杂表达式的处理能力。 ... [详细]
author-avatar
好人森森_195
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有