一.前言
前一篇文章介绍了一些CGI、FCGI相关的概念,详见 理解CGI、FCGI、php-cgi、php-fpm的概念 。从这篇文章知道PHP-FPM是PHP实现FCGI协议的一个组件,负责FCGI协议编码的请求的解析和转发。
fcgi4j是一个Java解析fcgi协议的组件,其git地址为:fcgi4j github。
二.问题
在工作中遇到的问题:
在线上,当PHP-FPM对数据包进行分包,fcgi4j拿到第2个包,去解析header时,出现BufferUnderflowException。
三.排查问题
BufferUnderflowException 就是常说的越界问题。比方说,读取NIO的Buffer时,如果Buffer的remaining为1,却要get超过1B的数据,这是就会越界。其中,Buffer的remaining的源码为:
/**
* Returns the number of elements between the current position and the
* limit.
*
* @return The number of elements remaining in this buffer
*/
public final int remaining() {
return limit - position;
}
注意,读取时,position一般指向0,其中有数据;limit一般指向有效数据的下一个位置,其中没有数据。所以,limit-position就是Buffer中存放的数据字节数。请看下图:
既然知道问题所在,看看问题是怎么出现的。通过加日志打印出解析HeaderBuffer时,PHP-FPM返回的HeaderBuffer的内容,以及position、limit信息。
这里有必要说下FCGI协议的Header。 详见这篇博文:FCGI协议的header解析。
在问题排查过程中,通过添加log信息,发现读取完数据,后面的解析出了问题,以为是PHP底层的PHP-FPM在分包的过程中出现了问题。因为,一个fcgi消息里可以最多存放65535B的数据,但是从日志中看,一个消息体的长度是8814时就被分包了。所以,希望PHP能够排查下底层的FPM实现。但是FPM是C语言实现的,PHP无法排查。只能在JAVA上继续做文章。
只是从Java代码上看是无法说服是PHP-FPM底层实现出现问题的。如果是PHP-FPM底层的问题,PHP业务方会通过其他途径解决这个问题(任由问题继续,领导来拍板)。所以,需要第三方的解析来看看数据包是不是出现问题。选择了tcpdump+wireshark进行抓包解析。悲剧的是,抓包工具能够正常解析所有数据。所以,是Java的fcgi4j出错了。tcpdump+wireshark抓包的使用,详见这篇博文:tcpdump+wireshark的使用。
通过添加日志发现fcgi4j在边界判断上有问题。
private int readStdoutData(ByteBuffer buffer, int available, int padding) throws IOException {
if (!buffer.hasRemaining()) {
return 0;
}
int read;
if (buffer.remaining() >= available) {
int limit = buffer.limit();
buffer.limit(buffer.position() + available);
read = IoUtils.socketRread(socketChannel, buffer);
buffer.limit(limit);
if (padding != 0) {
ByteBuffer paddingBuffer = ByteBuffer.allocate(padding);
IoUtils.socketRread(socketChannel, paddingBuffer);
paddingBuffer.flip();
LOG.debug("padding data : " + byteBufferToString(paddingBuffer));
}
} else {
read = IoUtils.socketRread(socketChannel, buffer);
}
return read;
}
上面的if (buffer.remaining() >= available)
当时没有等于号,这样导致当remaining==available时,导致padding读取不到。这样后面解析时就错过了。