最近要用Nginx lua进行http 数据交互,因此想到了resty/http.lua,因此开启一段性能调优之旅。
很简单的一段代码,利用http.lua request 函数发送http get 请求并返回body及相关信息。
在get 小文件的时候性能表现正常,符合预期,但是get 大文件的时候非常慢,在内网环境下GET 1个 1M左右的Object 竟然需要1s+,这性能实在不能忍,而且随着文件增大性能急剧下降。开始怀疑是不是http server 的原因,用wget 试了一下,发现很快,排除server的原因。百思不得其解后开始分析http.lua 代码
这是Lua 读取http body 代码,可以看出这里有个fetch_size参数,从代码上看直观含义是一次从底层网络读上来数据块的大小
161 local function read_body_data(sock, size, fetch_size, callback) 162 local p_size = fetch_size 163 while size and size > 0 do 164 if size < p_size then 165 p_size = size 166 end 167 local data, err, partial = sock:receive(p_size) 168 if not err then 169 if data then 170 callback(data) --这里有个callback,下面看看是啥 171 end 172 elseif err == "closed" then 173 if partial then 174 callback(partial) 175 end 176 return 1 -- 'closed' 177 else 178 return nil, err 179 end 180 size = size - p_size 181 end 182 return 1 183 end看下fetch size 设置值是多少
nreqt.fetch_size = reqt.fetch_size or 16*1024默认为16K
再看一下function read_body_data 在哪里调用的,参数callback 传又是什么
185 local function receivebody(sock, headers, nreqt) 186 local t = headers["transfer-encoding"] -- shortcut 187 local body = '' 188 local callback = nreqt.body_callback 189 if not callback then 190 local function bc(data, chunked_header, ...) 191 if chunked_header then return end 192 body = body .. data 193 end 194 callback = bc 195 end 196 if t and t ~= "identity" then 197 -- chunked 198 while true do 199 local chunk_header = sock:receiveuntil("\r\n") 200 local data, err, partial = chunk_header() 201 if not data then 202 return nil,err 203 else 204 if data == "0" then 205 return body -- end of chunk 206 else 207 local length = tonumber(data, 16) 208 209 -- TODO check nreqt.max_body_size !! 210 211 local ok, err = read_body_data(sock,length, nreqt.fetch_size, callback) 212 if err then 213 return nil,err 214 end 215 end 216 end 217 end 218 elseif headers["content-length"] ~= nil and tonumber(headers["content-length"]) >= 0 then 219 -- content length 220 local length = tonumber(headers["content-length"]) 221 if length > nreqt.max_body_size then 222 ngx.log(ngx.INFO, 'content-length > nreqt.max_body_size !! Tail it !') 223 length = nreqt.max_body_size 224 end 225 226 local ok, err = read_body_data(sock,length, nreqt.fetch_size, callback) 227 if not ok then 228 return nil,err 229 end 230 else 231 -- connection close 232 local ok, err = read_body_data(sock,nreqt.max_body_size, nreqt.fetch_size, callback) 233 if not ok then 234 return nil,err 235 end 236 end 237 return body 238 end这里可以看到我们的程序中没有传callback 进去,callback 默认是
190 local function bc(data, chunked_header, ...) 191 if chunked_header then return end 192 body = body .. data -- 注意这里会对每次接收到的body 进行拼接 193 end 194 callback = bc分析到这里问题已经很明显了
fetch_size 是一次sock:receive 调用读上来的body 的size,每次读出来fetch_size 的body 后会回调默认callback 对body 进行拼接,如果文件size 很大而fetch size 很小就会造成因字符串拼接造成的CPU资源消耗及内存消耗。而我们的场景是需要缓存所有body后处理,所以一次读出越多body越好。
默认Callback是
if chunked_header then return end body = body .. data end``` 假设按照fetch size默认值16k 来算,get 1MB 文件光string 拼接就要进行64次,所以一次性接收所有body性能最佳,fetch_size 设置为1GB。(大家都知道字符串拼接需要额外内存分配会消耗大量CPU) ### 5 结论 fetch_size 设置太小导致大文件body 拼接次数过多导致,从我的场景来看要缓存所有body后才能进行下一步因此fetch_size 设置越大越好 修正后代码为: url = uri, fetch_size = 1024*1024*1024, method = "GET", }```注意:如果你的业务场景是需要流式处理或者转发这个值只需要将fetch_size 调整为一个合适的值即可。
相关资源:OpenResty最佳实践