Spring Boot+Jetty应用添加equalsIgnoreCase后CPU占用飙升问题
Spring Boot + Jetty 应用请求头过滤导致CPU飙升问题排查与解决
问题场景
基于Jetty容器的大型Spring Boot Web应用,原有慢响应检测过滤器运行多年稳定。近期为避免记录敏感数据,添加了忽略大小写校验以过滤authorization请求头,仅1-2个线程处理该逻辑,但CPU占用从约10%飙升至98%。性能分析显示耗时集中在java.lang.StringLatin1.regionMatchesCI和java.lang.Character.toLowerCase方法。
无问题的旧代码
Enumeration<String> headers = request.getHeaderNames(); while (headers.hasMoreElements()) { String name = headers.nextElement(); Enumeration<String> headerValues = request.getHeaders(name); while (headerValues.hasMoreElements()) { r.append(name); r.append(": "); r.append(headerValues.nextElement()); r.append(System.lineSeparator()); } }
存在问题的新代码
Enumeration<String> headers = request.getHeaderNames(); while (headers.hasMoreElements()) { String name = headers.nextElement(); Enumeration<String> headerValues = request.getHeaders(name); while (headerValues.hasMoreElements()) { if (!name.equalsIgnoreCase("authorization")) { r.append(name); r.append(": "); r.append(headerValues.nextElement()); r.append(System.lineSeparator()); } } }
原因分析
- 循环层级错误导致重复校验:将请求头名称的校验放在请求头值的循环内部,同一个请求头的每个值都会重复执行一次
equalsIgnoreCase校验。例如某个请求头有100个值,就会重复100次相同的名称校验,完全无意义。 - 无前置长度过滤的低效匹配:
equalsIgnoreCase会逐字符进行大小写转换和比较,即使请求头名称长度和authorization(13个字符)不符,也会执行完整的字符遍历。对于大量非目标请求头,这会产生大量无意义的计算。 - 逐字符转换的累积开销:
equalsIgnoreCase底层依赖StringLatin1.regionMatchesCI实现,该方法会对每个字符调用Character.toLowerCase,多次重复调用时累积的性能开销非常可观。
解决方法
优化方案1:调整校验层级+前置长度过滤
将请求头名称的校验移到请求头值循环外部,同一个请求头仅校验一次;同时先判断长度,快速排除非目标请求头,避免不必要的字符比较。
Enumeration<String> headers = request.getHeaderNames(); // 缓存目标字符串和长度,避免重复创建 final String TARGET_HEADER = "authorization"; final int TARGET_LENGTH = TARGET_HEADER.length(); while (headers.hasMoreElements()) { String name = headers.nextElement(); // 先判断长度,快速跳过非目标请求头 boolean isAuthorization = false; if (name.length() == TARGET_LENGTH) { // 用regionMatches指定大小写不敏感,性能优于equalsIgnoreCase isAuthorization = name.regionMatches(true, 0, TARGET_HEADER, 0, TARGET_LENGTH); } Enumeration<String> headerValues = request.getHeaders(name); while (headerValues.hasMoreElements()) { if (!isAuthorization) { r.append(name); r.append(": "); r.append(headerValues.nextElement()); r.append(System.lineSeparator()); } } }
优化方案2:预转小写比较(可选)
如果请求头名称的大小写差异不大,也可以将请求头名称转一次小写后用普通equals比较,性能略高于regionMatches:
Enumeration<String> headers = request.getHeaderNames(); final String TARGET_LOWER = "authorization"; final int TARGET_LENGTH = TARGET_LOWER.length(); while (headers.hasMoreElements()) { String name = headers.nextElement(); boolean isAuthorization = false; if (name.length() == TARGET_LENGTH) { isAuthorization = TARGET_LOWER.equals(name.toLowerCase()); } Enumeration<String> headerValues = request.getHeaders(name); while (headerValues.hasMoreElements()) { if (!isAuthorization) { r.append(name); r.append(": "); r.append(headerValues.nextElement()); r.append(System.lineSeparator()); } } }
效果说明
优化后,请求头名称的校验次数从「请求头值的总数量」降低到「请求头的数量」,同时大部分非目标请求头会通过长度判断直接跳过字符比较,CPU开销会大幅回落至原有水平。
内容的提问来源于stack exchange,提问作者mjaggard
相关产品推荐
相关产品推荐

