和同事遇到一个问题,发现 gateway 报超时错误。但是查看应用的日志,却是一切正常。但是我记得之前遇到过因为超时,无法返回日志的情况。
Puma 是如何工作的
Puma 官网有文档,以及视频介绍 Puma 是如何工作的。Puma 会有多个进程,每个进程有多个线程,监听 3000 端口。
当有请求来的时候,会建立一个 socket,然后将请求转发给应用(Rails application)处理,处理完了以后再写回 socket。
当超时的时候
我搭了一个 API,这个 API 会在 90 秒后返回结果。
# controller.rb
def index
sleep(90)
render json: "ok"
end
然后在 terminal 里面调用 API,并设置 30 秒后 timeout。
curl -m 30 -v http://localhost:3001/api
30 秒之后,curl 断开连接。
* Operation timed out after 30002 milliseconds with 0 bytes received
* Closing connection 0
curl: (28) Operation timed out after 30002 milliseconds with 0 bytes received
观察 TCP 连接开始 4 次分手了。
netstat -tln | grep 3001
tcp4 14 0 127.0.0.1.3001 127.0.0.1.51756 CLOSE_WAIT
tcp4 0 0 127.0.0.1.51756 127.0.0.1.3001 FIN_WAIT_2
再过了 30 秒,连接彻底断开,netstat
已经查不到这个端口了。
30 秒之后,应用返回 200 response。
Completed 200 OK in 90001ms (Views: 0.2ms | ActiveRecord: 0.0ms | Allocations: 597)
难道不报错?
再去看 Puma 的代码。
在request.rb
的handle_request
中,Puma 会调用应用处理请求,并返回 response。整个请求做了异常处理,而且在我们的场景里面,这里不会报错。
status, headers, res_body = @thread_pool.with_force_shutdown do
@app.call(env)
end
这之后,Puma 会把res_body
写入socket
。
fast_write io, lines.to_s
fast_write
没有作异常处理,但是在handle_request
里面却又一个ensure
将整个异常给”吃”掉,不处理fast_write
中出现的任何错误。
如果在ensure
的前面插入rescue Exception => e
,并将exception
打印出来,就会看到下面的错误。可以看到,Puma 不能够正常写回 response,但是却不做任何处理。所以应用端对于 TCP 连接断开是无感知的,web server 这端虽然报错了,但不打印出来。
Socket timeout writing data
/Users/pzhong/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/puma-5.6.7/lib/puma/request.rb:241:in `rescue in fast_write'
/Users/pzhong/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/puma-5.6.7/lib/puma/request.rb:232:in `fast_write'
/Users/pzhong/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/puma-5.6.7/lib/puma/request.rb:154:in `handle_request'
/Users/pzhong/.rbenv/versions/3.1.1/lib/ruby/gems/3.1.0/gems/puma-5.6.7/lib/puma/server.rb:445:in `process_client'