tomcat8.x parseHost bug导致的性能损耗
现象
- 通过jfr抓取的deoptimization event发现有很多parseHost相关的jit退化
jit deoptimization的日志也有这个
1
2
3
4
5
6
7
8
9
10
11<uncommon_trap thread='6154' reason='range_check' action='make_not_entrant' debug_id='0' compile_id='67267' compiler='c2' level='4' stamp='390.396'>
<jvms bci='4' method='org.apache.tomcat.util.http.parser.HttpParser isNumeric (I)Z' bytes='9' count='23695' iicount='23695'/>
</uncommon_trap>
<make_not_entrant thread='6154' compile_id='67267' compiler='c2' level='4' stamp='390.396'/>
67267 ! 4 org.apache.tomcat.util.http.parser.HttpParser::isNumeric (9 bytes) made not entrant
<writer thread='5831'/>
<uncommon_trap thread='5848' reason='range_check' action='none' debug_id='0' compile_id='117864' compiler='c2' level='4' count='16' state='range_check recompiled' stamp='605.407'>
<jvms bci='4' method='org.apache.tomcat.util.http.parser.HttpParser isAlpha (I)Z' bytes='9' count='8591' iicount='8591' range_check_traps='16'/>
<jvms bci='1' method='org.apache.tomcat.util.http.parser.HttpParser$DomainParseState next (I)Lorg/apache/tomcat/util/http/parser/HttpParser$DomainParseState;' bytes='249' count='5394' iicount='5394'/>
</uncommon_trap>线上火焰图,parseHost有较高的占比——1.29(572 samples)
压测时没有发现类似的问题
排查
有哪些类型的host
使用arthas查看线上host的值,发现主要有两种
- xx.xx.com
- 10.10.10.10:2279
第一种是域名形式的,由nginx调用过来。第二种是ip+port的形式,主要是健康检查保活。
第一种形式,在tomcat8.x版本下,会抛出ArrayIndexOutOfBoundException,异常的初始化比较耗费cpu。
第二种形式,则会正常解析结束。
找到有问题的char
出问题的代码如下,就是一个静态的数组,范围是0 ~ 127,存储其是否是字母、数字。
异常的case,是c超过了下标的范围,从而导致 ArrayIndexOutOfBoundsException 异常。
1 | public static boolean isAlpha(int c) { |
异常路径,有异常栈的填充,理论上应该比较慢。可以用monitor看到avg耗时,然后用watch找出耗时长的请求:
1 | [arthas@28]$ watch org.apache.tomcat.util.http.parser.HttpParser isAlpha 'params[0]' '#cost>0.05' -n 5 |
可以看到,有异常的值-1,-1会导致ArrayIndexOutOfBoundsException 异常。
有问题的char来源
接着跟进下-1的来源:
1 | [arthas@28]$ stack org.apache.tomcat.util.http.parser.HttpParser isNumeric 'params[0] < 0' -n 5 |
出问题的地方是从readHostDomainName过来的,看下对应实现:
readHostDomainName返回值是ip:port的分隔符,”:”的index位置。
解析逻辑,就是从host对应的reader中,不断地读取字符,传递给状态机。然后状态机根据传入的字符,进行不同状态的流转。
如果传入的是域名,则会一直读取到流结束,流结束之后,read返回的是-1,从而走到异常的逻辑。
如果传入的是ip:port,则会在流结束之前,正常的走到解析流程,不会走到异常的逻辑。
1 | static int readHostDomainName(Reader reader) throws IOException { |
看下状态机的状态:
1 | // 状态机实现 |
解决方案
tomcat已经在8.5.41版本修复,对应的release note:
https://tomcat.apache.org/tomcat-8.5-doc/changelog.html
对应的代码diff,可以看到优先处理流结束的情况,就会避免isAlpha抛异常:
结论
http/1.1之后,要求header中必须存在Host字段。
nginx在转发时,会将Host字段设置为对应的域名。同时探活时是单节点探活,对应的Host是ip:port
低版本的tomcat(< 8.5.41),在解析域名这种host的时候,存在bug。bug会导致isAlpha和isNumeric方法内部抛出ArrayIndexOutofRange异常。异常的影响主要有两点:
- 填充异常栈的cpu开销
- jit deopt的开销(native栈转interpret栈)
性能损失跟客户端的请求量相关,请求量越大,越明显