此文已由做者汤晓静受权网易云社区发布。
html
欢迎访问网易云社区,了解更多网易技术产品运营经验。mysql
文章前部分用大量篇幅阐述了 lua 和 nginx 的相关知识,包括 nginx 的进程架构,nginx 的事件循环机制,lua 协程,lua 协程如何与 C 实现交互;在了解这些知识以后,本节阐述 lua 协程是如何和 nginx 的事件机制协同工做。nginx
从 nginx 的架构和事件驱动机制来看, nginx 的并发处理模型归纳为:单 worker + 多链接 + epoll + callback。 即每一个 nginx worker 同时处理了大量链接,每一个链接对应一个 http 请求,一个 http 请求对应 nignx 中的一个结构体(ngx_http_request_t):c++
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,其定义以下:git
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 的事件处理核心代码以下:github
... events = epoll_wait(ep, event_list, (int) nevents, timer); for (i = 0; i < events; i++) { c = event_list[i].data.ptr; instance = (uintptr_t) c & 1; c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1); // epoll 获取激活事件,将事件转换成 ngx_connection_t ... rev = c->read; rev->handler(rev); ... wev = c->write; wev->handler(ev); ... }
nginx epoll loop 中调用 epoll_wait 获取 epoll 接管的激活事件,并经过 c 的指针强转,获得 ngx_connection_t 获取对应的链接和链接上对应的读写事件的回调函数,即经过 C 结构体变量成员之间的相关关联来串联请求和事件驱动,实现请求的并发处理;这里其实和高级语言的面向对象的写法一模一样,只是模块和成员变量之间的获取方式的差别。redis
若是引入 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 核心代码部分:sql
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 方法 。 编程
综上所述,结合lua 协程和 nginx 事件驱动机制,使用 OpenResty 可使用 lua 脚本方便的扩展 nignx 的功能。
该阶段主要用于预加载一些 lua 模块, 如加载全局 json 模块:require 'cjson.safe';设置全局的 lua_share_dict 等,而且能够利用操做系统的 copy-on-write 机制;reload nginx 会从新加载该阶段的代码。
该阶段可用于为每一个 worker 设置独立的定时器,设置心跳检查等。
实际场景中应用最多的一个 hooks 之一,可用于请求重定向相关的逻辑,如改写 host 头,改写请求参数和请求路径等
该阶段可用于实现访问控制相关的逻辑,如动态限流、限速,防盗链等
该阶段用于生成 http 请求的内容,和 proxy_pass 指令冲突;两者在同一个阶段只能用一个。该阶段可用于动态的后端交互,如 mysql、redis、kafaka 等;也可用于动态的 http 内容生成,如使用 lua 实现 c 的 slice 功能,完成大文件的分片切割。
该阶段可用于动态的设置 proxy_pass 的上游地址,例如用 lua 实现一个带监控检测机制的一致性 hash 轮序后端算法,根据上游的响应动态设置该地址是否可用。
用于过滤和加工响应包体,如对 chunk 模式的包体进行 gzip; 也能够根据包体的大小来动态设置 ngx.var.limit_rate.
调整发送给 client 端的响应头,也是最经常使用的 hooks 之一;好比设置响应的 server 头,修缓存头 cache-control 等。
一方面能够设置 nginx 日志输出的字段值,另外一方面咱们也能够用 cosocket 将日志信息发送到指定的 http server;因响应头和响应体已发送给客户端,该阶段的操做不会影响到客户端的响应速度。
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 调用方法的异常
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 调用。
用于 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
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的形式。
控制处理客户端包体的行为,若是设置为 on, 则 nginx 会接收完 client 的整个包体后处理。如 nginx 做为反向代理服务处理客户端的上传操做,则先接收完包体再转发给上游,这样上游异常的时候,nginx 能够屡次重试上传,但有个问题是若是包体过大,nginx 端若是负载较重话,会有大量的写磁盘操做,同时对磁盘的容量也有较高要求。若是设置为 off, 则传输变成流式处理,一个 chunk 一个 chunk 传输,传输出错更多须要 client 端重试。
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.
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.
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 一个缓冲区。
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 中分配的大小。
该指令可做用于 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 operation | signal |
---|---|
reload | SIGHUP |
reload | SIGUSR1 |
stop | SIGTERM |
quit | SIGQUIT |
hot update | SIGUSR2 & SIGWINCH & SIGQUIT |
stop 发送 SIGTERM 信号,表示要求强制退出,quit 发送 SIGQUIT,表示优雅地退出。 具体区别在于,worker 进程在收到 SIGQUIT 消息(注意不是直接发送信号,因此这里用消息替代)后,会关闭监听的套接字,关闭当前空闲的链接(能够被抢占的链接),而后提早处理全部的定时器事件,最后退出。没有特殊状况,都应该使用 quit 而不是 stop。
master 进程收到 SIGHUP 后,会从新进行配置文件解析、共享内存申请,等一系列其余的工做,而后产生一批新的 worker 进程,最后向旧的 worker 进程发送 SIGQUIT 对应的消息,最终无缝实现了重启操做。 再 master 进程从新解析配置文件过程当中,若是解析失败则会回滚使用原来的配置文件,即 reload 失败,此时工做的仍是老的 worker。
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 而直接进行压缩,颇有可能出现日志丢失的状况)。
某些时候咱们须要进行二进制热更新,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 信号,让其退出了。
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 HashMap在并发场景下踩过的坑
【推荐】 外包筛选心得