关于OpenResty几个小坑的总结

Posted 子成简记

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于OpenResty几个小坑的总结相关的知识,希望对你有一定的参考价值。

前段时间接手了公司的一个OpenResty项目,以前虽然听说OpenResty大名,但从没真正接触过,也没碰过lua语言。在了解,优化以及增加项目功能的过程中,遇到了一些坑,这里总结下,也算是目前对其学习的一个小结。

 

1.  一个阻塞函数引发的bug

有段时间同事发现服务CPU占用一直很高,定位的过程中,发现是后台定时调用某个清理数据的接口时,这个接口会触发一个近乎无限的循环,其会造成CPU不停运算,从而利用率飙高。对于这种情况,一般会在其中加入sleep停顿,以缓解CPU的运算压力。如是google了一把,lua如何sleep搜到的一些网址包括StackOverflow很快给出了答案,如是选择了以下方法,每次循环都让它sleep 1s,功能验证效果很理想,觉得很easy

function sleep(n) os.execute("sleep " .. tonumber(n))end


可是后来却在nginx日志中发现,连接redis发现了大量timeout错误。一开始认为redis可能真的太忙了,因为我们redis服务器确实有些时间压力比较大,也出过此类错误,目前正在优化中。但是自己还是用redis-cli连了几把redis服务器,发现每次都瞬时连上了,没有任何延迟的迹象。这就让我很疑惑了,觉得问题不应该出在redis那里。于是再次查看Nginx的日志,发现除了超时错误,还出现大量的如下日志:

signal17 (SIGCHLD) receivedwaitpid() failed (10: No child processes)


这些都是以前日志没有的,心想我只改了这个函数sleep部分,难道是这里出错了。当暂停这个接口调用,重新reloadNginx,错误就没了。很显然这里有问题,为什么让CPU休息下,会导致redis连接超时呢?还会出现上面这样的日志?

问了周边同事也没有太了解的,只能继续google了,首先查的是异常多出的那两行日志,原来是前面调用os.executeos.execute会通过 fork() 创建新的(shell)子进程,而子进程退出时会发送SIGCHLD信号给父进程,这里nginx 进程就是父进程,它会打印出第一行提示信息。该消息的notice级别,它既不是错误,也不是警告,是无害的。当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD,然后会出现第二行日志。但这也没解释为什么会连接redis超时。后来改成openrestysleep关键字搜索时,终于发现了原因所在。当使用os.execute()调用sleep时,使用的是系统自带的阻塞API,会触发进程切换,当前进程会变成睡眠状态, 结果CPU就进入空闲状态,不再处理任何其他任务。如果Nginx当前处理这一个接口调用,看着功能很正常。实际上当我们做性能测试时,会发现此时Nginx服务器的性能会大大降低。如果中间涉及到其它接口调用访问redis,而redis的超时刚好设置为1s,那么可能会因为这个阻塞函数,导致redis的调用超时,从而导致大量的timeout错误。怎么解决这个问题呢?可以使用openresty提供的非阻塞APIngx.sleep()只会引起(进程内)协程的切换,但进程还是处于运行状态(其他协程还在运行)。从这里吸取的教训是:(1)改写或者优化接口功能,除了进行简单功能测试之外,应该还进行性能测试,否则会埋下隐藏的坑。(2)使用相关组件时,如果可以最好先看下它的最佳实践文档。在在 OpenResty最佳实践时提到,选择使用库的时候,有一个基本的原则:尽量使用 OpenResty 的库函数,尽量不用 Lua 的库函数,因为 Lua 的库都是同步阻塞的。

 

2.访问rediskey不存在误认为连接错误进行重试操作

我们访问redis的模块参考https://moonbingbing.gitbooks.io/openresty-best-practices/redis/out_package.html进行了二次封装,在这个封装中,当连接redis发生错误时,如redis服务器因切换出现短暂中断,会在外围进行重试操作。但是有时在日志中发现即使redis连接正常,访问其中一些不存在的key时,也会进行重试。原来这个封装,对于访问redis不存在的key时,返回的ngx.null,直接转换成了nil进行返回,而连接出错时,也是返回的nil。外围则对返回的nil进行判断,是nil便认为是连接错误便进行重试操作,从而造成了不必要的访问。后来计划将这种重试操作判断移入到封装的代码进行。在这里还顺便进一步深入了解了nilngx.null的区别。lua读取redis数据返回结果为空时,返回的结果不是nil而是userdata类型的ngx.nullnillua中有特殊的意义,如果一个变量被设置为nil相当于告知该变量未定义(不存在)一样,如果把redis查询的结果为空设置为nil,就无法把查询为空和未定义区分开来了,这样显然是不合理的。所以必须使用一个userdata类型的值来表示这个查询记录为空,但是又不等同于未定义变量(这里为ngx.null)。(https://notes.doublemine.me/2018-01-26-lua%E8%AF%BB%E5%8F%96redis%E6%95%B0%E6%8D%AE%E7%9A%84null%E5%88%A4%E6%96%AD.html)。从这里吸取到的教训是:使用第三方的模块,尤其是摘录的代码,需要多测试,多分析判断,不可盲用。


3.关于http协议状态码的设置

HTTP 状态码负责表示客户端 HTTP 请求的返回结果、标记服务器端的处理是否正常、通知出现的错误等工作。这部分本不应该属于OpenResty的内容,但是由于涉及到web服务,在项目中发现了很多各种状态码的返回,有时2XX,有时3XX,有时4XX,但是具体为什么这个位置用这些,觉得有必要做个简单总结(主要参考《图解HTTP》一书)。这样以后写程序对于这类状态码设置,心中大概有数不至于瞎用。对于状态码,数字中的第一位指定了响应类别,后两位无分类。响应类别有以下 5种。

1XX

Informational(信息性状态码)

接收的请求正在处理

2XX

Success(成功状态码)

请求正常处理完毕

3XX

Redirection(重定向状态码)

需要进行附加操作以完成请求

4XX

ClientError(客户端错误状态码)

服务器无法处理请求

5XX

ServerError(服务器错误状态码)

服务器处理请求出错

再简单列一些我们常见的状态码:

200 OK

表示从客户端发来的请求在服务器端被正常处理了

204 No Content

该状态码代表服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分。另外,也不允许返回任何实体的主体。比如,当从浏览器发出请求处理后,返回 204 响应,那么浏览器显示的页面不发生更新

301 Moved Permanently

永久性重定向。该状态码表示请求的资源已被分配了新的URI,以后应使用资源现在所指的 URI。

400 Bad Request 

该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。

401 Unauthorized

该状态码表示发送的请求需要有通过HTTP认证的认证信息。另外若之前已进行过 1 次请求,则表示用 户认证失败。

403 Forbidden

该状态码表明对请求资源的访问被服务器拒绝了。服务器端没有必要给出拒绝的详细理由。

404 Not Found

该状态码表明服务器上无法找到请求的资源。

500 Internal Server Error

该状态码表明服务器端在执行请求时发生了错误。也有可能是 Web应用存在的 bug 或某些临时的故障。

503 Service Unavailable

该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入RetryAfter首部字段再返回给客户端。


参考网址:

1,https://moonbingbing.gitbooks.io/openresty-best-practices/ngx_lua/sleep.html

2,https://notes.doublemine.me/2018-01-26-lua%E8%AF%BB%E5%8F%96redis%E6%95%B0%E6%8D%AE%E7%9A%84null%E5%88%A4%E6%96%AD.html

3,《图解HTP》


以上是关于关于OpenResty几个小坑的总结的主要内容,如果未能解决你的问题,请参考以下文章

几个关于js数组方法reduce的经典片段

mongodb3的几个小坑

Jexus~webApi程序的部署几个小坑

关于 Date() 函数在 iOS 中的一个小坑

关于 Date() 函数在 iOS 中的一个小坑

iOS总结:项目中的各种小坑汇总