背景
之前分析诡异的502问题的时候,还有一个疑点没有解释:为什么 tomcat 在接收到 response 的时候再次自行做分块处理呢?
问题可以转化为:何时tomcat对 response 做分块?分块的条件是什么?
何时准备输出 response
处理分块应该在拿到 response 之后,这就需要再次追溯tomcat的请求处理流程,直接从 NioEndpoint 看起,Poller 线程取出 events() 之后进行事件的处理:org.apache.tomcat.util.net.NioEndpoint.Poller#processKey
,这里是读写事件,进入 Socket 请求处理逻辑:org.apache.tomcat.util.net.NioEndpoint#processSocket(org.apache.tomcat.util.net.NioEndpoint.KeyAttachment, org.apache.tomcat.util.net.SocketStatus, boolean)
而 org.apache.tomcat.util.net.NioEndpoint.SocketProcessor
本身就是一个 Runnable 任务,进入 process 方法:org.apache.coyote.http11.AbstractHttp11Processor#process
看代码 tomcat 源码中将请求处理分成了不同的阶段,比如:org.apache.coyote.Constants#STAGE_PARSE
, 这是一个很重要的线索,
// Request states
public static final int STAGE_NEW = 0;
public static final int STAGE_PARSE = 1;
public static final int STAGE_PREPARE = 2;
public static final int STAGE_SERVICE = 3;
public static final int STAGE_ENDINPUT = 4;
public static final int STAGE_ENDOUTPUT = 5;
public static final int STAGE_KEEPALIVE = 6;
public static final int STAGE_ENDED = 7;
结合 process()
方法的源码,可以猜测对于输出的处理应该在 EndInput -> EndOutput 之间。
// Finish the handling of the request
rp.setStage(org.apache.coyote.Constants.STAGE_ENDINPUT);
if (!isAsync() && !comet) {
if (getErrorState().isError()) {
// If we know we are closing the connection, don't drain
// input. This way uploading a 100GB file doesn't tie up the
// thread if the servlet has rejected it.
getInputBuffer().setSwallowInput(false);
} else {
// Need to check this again here in case the response was
// committed before the error that requires the connection
// to be closed occurred.
checkExpectationAndResponseStatus();
}
endRequest();
}
rp.setStage(org.apache.coyote.Constants.STAGE_ENDOUTPUT);
重要的就是 endRequest()
这个方法。
public void endRequest() {
// Finish the handling of the request
if (getErrorState().isIoAllowed()) {
try {
getInputBuffer().endRequest();
} catch (IOException e) {
setErrorState(ErrorState.CLOSE_NOW, e);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
// 500 - Internal Server Error
// Can't add a 500 to the access log since that has already been
// written in the Adapter.service method.
response.setStatus(500);
setErrorState(ErrorState.CLOSE_NOW, t);
getLog().error(sm.getString("http11processor.request.finish"), t);
}
}
if (getErrorState().isIoAllowed()) {
try {
getOutputBuffer().endRequest();
} catch (IOException e) {
setErrorState(ErrorState.CLOSE_NOW, e);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
setErrorState(ErrorState.CLOSE_NOW, t);
getLog().error(sm.getString("http11processor.response.finish"), t);
}
}
}
做了两件事:getInputBuffer().endRequest();
和getOutputBuffer().endRequest();
我们关心的是对输出的处理:
public void endRequest() throws IOException {
if (!committed) {
// Send the connector a request for commit. The connector should
// then validate the headers, send them (using sendHeader) and
// set the filters accordingly.
response.action(ActionCode.COMMIT, null);
}
if (finished)
return;
if (lastActiveFilter != -1)
activeFilters[lastActiveFilter].end();
flushBuffer(true);
finished = true;
}
如果 response 没有 commit 则执行 commit,然后如果没有 finish 则进行 flushBuffer ,见名知意,flush才是最后提交到 OS 进行写出的步骤。
而其中很重要的 action 方法就提示我们进入另一条很重要的源码分析线索:org.apache.coyote.http11.AbstractHttp11Processor#action
,这个方法根据不同的 code 让 connector 做不同的处理。看 commit 动作:
case COMMIT: {
// Commit current response
if (response.isCommitted()) {
return;
}
// Validate and write response headers
try {
prepareResponse();
getOutputBuffer().commit();
} catch (IOException e) {
setErrorState(ErrorState.CLOSE_NOW, e);
}
break;
}
方法org.apache.coyote.http11.AbstractHttp11Processor#prepareResponse
何时处理 response 分块?
就是实际准备输出内容的地方,那如何处理 chunk?
准备响应阶段
//org.apache.coyote.http11.AbstractHttp11Processor#prepareResponse
// 主要是校验 header / 状态码
long contentLength = response.getContentLengthLong();
boolean connectionClosePresent = false;
if (contentLength != -1) {
headers.setValue("Content-Length").setLong(contentLength);
getOutputBuffer().addActiveFilter
(outputFilters[Constants.IDENTITY_FILTER]);
contentDelimitation = true;
} else {
// If the response code supports an entity body and we're on
// HTTP 1.1 then we chunk unless we have a Connection: close header
connectionClosePresent = isConnectionClose(headers);
if (entityBody && http11 && !connectionClosePresent) {
getOutputBuffer().addActiveFilter
(outputFilters[Constants.CHUNKED_FILTER]);
contentDelimitation = true;
headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED);
} else {
getOutputBuffer().addActiveFilter
(outputFilters[Constants.IDENTITY_FILTER]);
}
}
内容大意是:看是否有指定 content-lenght,如果有则不 chunk
如果没有,则看条件:entityBody && http11 && !connectionClosePresent,都满足则进行 chunk 处理
至此,tomcat 自动进行response分块的条件已经清晰了:body 有内容,http1.1 协议,连接不关闭,三个条件都满足才处理。
复现502时的请求
模拟当时的请求:
$ curl -v "http://localhost:8080/index.jsp" --http1.0
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /index.jsp HTTP/1.0
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< Date: Sun, 18 Nov 2018 15:10:12 GMT
< Connection: close
<
* Closing connection 0
{'data': 'OK'}%
关键点就是:当时 tomcat 接收到的 local nginx 发来的请求是 http1.0 的,所以不满足分块响应的条件,也就不会自动分块,但是 response header 又提示了有分块,所以被认为是一个错误的响应,而被丢弃掉了。