记一次lua io使用不当导致内存泄露问题

Posted 编程阁楼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记一次lua io使用不当导致内存泄露问题相关的知识,希望对你有一定的参考价值。

01. 背景
小编公司的网关上提供了一个文件上传的接口,最近在与外部对接发现,当文件超过100m后,大概率会导致上传失败。


02.分析过程

一、文件大小限制
对应状态码为413,"Request Entity Too Large"错误。
这个问题的出现是因为nginx层有默认的body size阈值限制,默认是1m,可以通过client_max_body_size进行调整。小编这里是这么修改的:
client_max_body_size 500m;

业务上要求能支持最大500m文件上传。


二、客户端超时断开连接
对应状态码为499,"Client Closed Reques"错误。该状态码语义为服务端处理时间长,客户端等不及,主动关闭了连接。
针对这种情况小编也做了相应的timeout调整:
proxy_send_timeout 600s;proxy_connect_timeout 600s;proxy_read_timeout 600s;send_timeout 600s;
当然了,延长超时时间可能带来副作用,小编这里也仅针对文件上传接口做了设置。

三、服务端错误

对应状态码有500,502和504。

500, Internal Server Error , 服务器内部错误,服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。502,Bad Gateway,网关错误,它往往表示网关从上游服务器中接收到的响应是无效的。504,Gateway Timeout,网关超时。
出现以上几种错误码时,可能的原因就不那么好分析了。小编这里在上传100m以上文件时大概率会出现以上错误中的一个。以下是一次500错误的错误信息:
2020/09/07 15:21:33 [error] 51#0: *4260 [lua] responses.lua:121: send(): failed to get from node cache: callback threw an error: /usr/local/share/lua/5.1/pgmoon/init.lua:233: attempt to index local 'sock' (a nil value), client: 172.168.0.1, server: kong, request: "POST /obs/zcy.obs.file.upload HTTP/1.1", host: "api.test.com"172.18.212.3 - - [07/Sep/2020:15:21:33 +0800] api.test.com POST "/obs/zcy.obs.file.upload" "-" 500 54 "-" - - 90.599 - "Apache-HttpClient/4.5.1 (Java/1.8.0_92)" "172.18.212.3" "-" ggtest -2020/09/07 15:22:44 [error] 48#0: *105700 lua coroutine: memory allocation error: not enough memorystack traceback:coroutine 0: [C]: in function '(for generator)' /usr/local/share/lua/5.1/multipart.lua:43: in function 'decode' /usr/local/share/lua/5.1/multipart.lua:115: in function 'Multipart' ...are/lua/5.1/kong/plugins/hmac-auth-ocean-file/access.lua:273: in function 'init_form_args' ...are/lua/5.1/kong/plugins/hmac-auth-ocean-file/access.lua:619: in function 'do_authentication' ...are/lua/5.1/kong/plugins/hmac-auth-ocean-file/access.lua:676: in function 'execute' ...re/lua/5.1/kong/plugins/hmac-auth-ocean-file/handler.lua:14: in function <...re/lua/5.1/kong/plugins/hmac-auth-ocean-file/handler.lua:12>coroutine 1: [C]: in function 'resume' coroutine.wrap:21: in function <coroutine.wrap:21> /usr/local/share/lua/5.1/kong/init.lua:379: in function 'access' access_by_lua(nginx-kong.conf:94):2: in function <access_by_lua(nginx-kong.conf:94):1>, client: 172.18.212.3, server: kong, request: "POST /obs/zcy.obs.file.upload HTTP/1.1", host: "api.test.com"
从这些日志中可以看出,"memory allocation error: not enough memory"内存不足了。
查看监控系统是发现以下可以情况, 上传文件是内存在持续飙升,且内存飙升远大于文件大小:

这是为什么呢?

03. 开始填坑

一、内存飙升理论可能性分析

通常kong网关(实际上是nginx)不会出现内存瓶颈。原因是nginx会将超过一定大小的请求缓存成本地文件。
2020/09/08 18:47:56 [warn] 60#0: *127418 a client request body is buffered to a temporary file /usr/local/kong/client_body_temp/0000000009, client: 172.18.212.3, server: kong, request: "POST /obs/zcy.obs.file.upload HTTP/1.1", host: "api.test.com"
这个也可以通过配置项进行调整:
  
    
    
  
请求体的size大于nginx配置里的client_body_buffer_size,则会导致请求体被缓冲到磁盘临时文件里,client_body_buffer_size默认是8k或者16k
我们上传的文件远大于这个buffer值,所以会生成本地缓存文件,即理论上不会出现内存持续飙升的问题。

想明白以上问题,小编开始猜测是kong网关上处理文件上传的插件逻辑出了问题。

二、签名插件逻辑大bug

小编这里的签名插件是用lua脚本写的,核心片段如下:

--这里必须使用'Content-Type',首字符大写if string.sub(receive_headers[CONTENT_TYPE], 1, 20) == "multipart/form-data;" then --判断是否是multipart/form-data类型的表单 is_have_file_param = true local body_data = ngx.req.get_body_data() if not body_data then local datafile = ngx.req.get_body_file() ngx.log(ngx.ERR, "body is in file: ", tostring(datafile))
if not datafile then error_code = 1 error_msg = "no request body found" else local fh, err = io.open(datafile, "r") if not fh then error_code = 2 error_msg = "failed to open " .. tostring(datafile) .. "for reading: " .. tostring(err) else fh:seek("set") body_data = fh:read("*a") fh:close() if body_data == "" then error_code = 3 error_msg = "request body is empty" end end end end
关键代码是以下两行:
fh:seek("set") --设置文件从头开始读取body_data = fh:read("*a") --读取全部内容
What?读取缓存文件所有内容?!
聪明的你已经意识到了,这里是要将文件所有内容读取出来。如果文件很大,这不就要占用大量的内存了吗?事实上 问题就出 在这里了。


三、历史原因回溯
为什么这里要读取文件所有内容呢?小编分析这里本意并非读取所有内容,而是要获取post请求的request body data。我们知道,通过lua脚本有时是无法直接读取到request中的body data数据的(回看《 》)。正因为请求的body内容被缓存在了文件当中,我们才不得不去加载文件,解析出响应的数据。

04. 解决方案

原因已经找到,接下来看怎么解决该问题。我们来看下缓存文件中的内容是什么:

------ZcyOpenBoundaryj5pjAaN060Reqm05Content-Disposition: form-data; name="_data_"Content-Type: text/plain; charset=UTF-8Content-Transfer-Encoding: 8bit
{"fileName":"nohup.out","bizCode":"1071"}------ZcyOpenBoundaryj5pjAaN060Reqm05Content-Disposition: form-data; name="file"; filename="deaultFilename"Content-Type: application/octet-stream
... 省略内容 ...
实际上,小编这签名插件要获取的就是这部分内容,key= _ data_,对应的value={"fileName":"nohup.out","bizCode":"1071"}。为了满足签名算法,这里需要将这部分内容获取到,包装成lua table。

我们完全可以换个思路来实现,看代码:
--这里必须使用'Content-Type',首字符大写if string.sub(receive_headers[CONTENT_TYPE], 1, 20) == "multipart/form-data;" then --判断是否是multipart/form-data类型的表单 is_have_file_param = true local body_data = ngx.req.get_body_data() if not body_data then local datafile = ngx.req.get_body_file() ngx.log(ngx.ERR, "body is in file: ", tostring(datafile))
if not datafile then error_code = 1 error_msg = "no request body found" else local fh, err = io.open(datafile, "r") if not fh then error_code = 2 error_msg = "failed to open " .. tostring(datafile) .. "for reading: " .. tostring(err) else fh:seek("set") local i = 0 local continue_flag = false for line in fh:lines() do local match_result = string.match(line, "_data_") if match_result then continue_flag = true end if continue_flag then i = i + 1 end if i==5 then local position = string.find(line, "\r"); local body_data_str = string.sub(line, 1, position-1) args["_data_"] = unescape(body_data_str) break end end fh:close() end end end
核心代码:
local i = 0local continue_flag = falsefor line in fh:lines() do local match_result = string.match(line, "_data_") if match_result then continue_flag = true end if continue_flag then i = i + 1 end if i==5 then local position = string.find(line, "\r"); local body_data_str = string.sub(line, 1, position-1) args["_data_"] = unescape(body_data_str) break endend
这里小编把读取文件的方法改成了fh:lines(),即逐行读取内容。当获取到_data_的value后即结束文件读取。实际上通过这种方式仅仅需要内存中加载极少的内容即可。

05. 效果立现

插件代码优化后,再来上传300m文件试试,轻松搞定了,而且内存也几乎没有波动。


06. 总结
网关平台在处理小文件上传时没有问题,但当处理超大文件时内存泄露问题就显露出来,作为有求的程序员,我们需要保持对问题的敬畏之心,努力找出问题的根源,才能从源头将问题解决掉。

以上是关于记一次lua io使用不当导致内存泄露问题的主要内容,如果未能解决你的问题,请参考以下文章

记一次 .NET 某手术室行为信息系统 内存泄露分析

fastjson反序列化使用不当导致内存泄露

记一次spark内存泄露问题

记一次spark内存泄露问题

记一次spark内存泄露问题

生产问题一则:ThreadLocal使用不当导致的内存泄露