HTTP传输接收服务的一次CPU占比100%排查

53 篇文章 0 订阅
34 篇文章 0 订阅

笔者有个HTTP数据接收的项目上线,接收了大量的客户端HTTP请求,部署在三台LINUX 服务器上。

linux服务器64G内存,操作系统为CENT OS7.6

WEB容器采用TOMCAT9.0,服务端程序使用JAVA 开发,JDK版本为1.8,负责接收客户端持续不断的JSON请求,服务端将JSON解析为JAVA对象,并对每个对象的属性作合法性验证(比如,某个属性必须是16位的数字和字母且允许|间隔等待)以及相关性验证(比如 某个值必须等于A+B+C)。

上线几日来,每天接收80万的基本流水以及800万条的扩展流水。

上线首日,客户端(发送HTTP请求)报告有大量的连接超时(每次请求达到几十秒才能完成,正常时至多几十毫秒一次请求即可得到服务器200的响应)。

登录到服务端服务器排查:

在控制台命令行输入 top -c 命令,目的是显示进程运行信息列表,在屏幕显示中找到CPU占比最高的进程。

比如,最耗CPU的进程是PID为10165的进程,将ID记录下来。

在控制台命令行输入top -Hp 10165 ,显示一个进程的线程运行信息列表。

进程10165内,最耗CPU的线程PID为10604。

在控制台输入 printf “%x” 10604 将线程ID转换为16进制 ,比如 296c。

在控制台输入  jstack 10165|grep 296c -A 30   用于显示该线程的代码堆栈:

[root@localhost qf]# jstack 10165|grep 296c-A 30
"http-nio-9088-exec-43" #81 daemon prio=5 os_prio=0 tid=0x00007f36f8040800 nid=0x296c runnable [0x00007f37344bf000]
   java.lang.Thread.State: RUNNABLE
    at java.util.regex.Pattern$CharProperty.match(Pattern.java:3790)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4264)
    at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
    at java.util.regex.Pattern$Loop.match(Pattern.java:4799)
    at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
    at java.util.regex.Pattern$Branch.match(Pattern.java:4616)
    at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
    at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
    at java.util.regex.Pattern$Loop.match(Pattern.java:4799)
    at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
    at java.util.regex.Pattern$Branch.match(Pattern.java:4616)
    at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
    at java.util.regex.Pattern$Curly.match0(Pattern.java:4286)
    at java.util.regex.Pattern$Curly.match(Pattern.java:4248)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
    at java.util.regex.Pattern$Loop.match(Pattern.java:4799)
    at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
    at java.util.regex.Pattern$BranchConn.match(Pattern.java:4582)
    at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)
    at java.util.regex.Pattern$BmpCharProperty.match(Pattern.java:3812)
    at java.util.regex.Pattern$GroupHead.match(Pattern.java:4672)
    at java.util.regex.Pattern$Branch.match(Pattern.java:4618)
    at java.util.regex.Pattern$GroupTail.match(Pattern.java:4731)

 

 

 由上述可看到线程在执行正则表达式验证时发生了疑似死循环(因为该线程占用了大量的消耗时间,没有释放连接,且堆栈信息一直停顿在此处代码)。

打开ECLIPSE项目,在源代码中排查正则表达式:

import java.util.regex.Pattern;

 

 //版本号组合(验证 )
    public final static String INTERVAL_RATEVERSION_MULTIPROVINCE_REG_EXPRESS=
                "^(((11|12|13|14|15|21|22|23|31|32|33|34|35|36|37|41|42|43|44|45|50|51|52|53|61|62|63|64|65)\\d{8}\\d{3})+(\\|((11|12|13|14|15|21|22|23|31|32|33|34|35|36|37|41|42|43|44|45|50|51|52|53|61|62|63|64|65)\\d{8}\\d{3})+))+(,(((11|12|13|14|15|21|22|23|31|32|33|34|35|36|37|41|42|43|44|45|50|51|52|53|61|62|63|64|65)\\d{8}\\d{3})+(\\|((11|12|13|14|15|21|22|23|31|32|33|34|35|36|37|41|42|43|44|45|50|51|52|53|61|62|63|64|65)\\d{8}\\d{3})+))+)*$";

...

...

//私有方法:正则表达式匹配

private void _toCheckValueFormatByRegExpress(FieldsValidatorEnums fieldsValidatorEnum, Object fieldValue) {
        if(StringUtils.isNotBlank(fieldsValidatorEnum.getRegExpress())) {
            boolean isWrong = (!fieldValue.toString().matches(fieldsValidatorEnum.getRegExpress()));//正则校验
            if (isWrong) {
                this.tipInfo += fieldsValidatorEnum.getTipForCheck(fieldValue);// 
            }
            this.validateResult = this.validateResult || (isWrong);
        }
    }

经单元测试,发现上述正则表达式在匹配验证时程序进入了死循环。

经改造,将java自带的正则替换为Google的正则

import com.google.re2j.Pattern;

将正则匹配的私有方法,改写为:

    public static boolean patternMatch(String regex, String source ) {
        if (StringUtils.isBlank(regex) || source == null) {
            
            return false;
        }

        String matchId = null;
        if (log != null && log.isDebugEnabled()) {
            matchId = UUID.randomUUID().toString().replaceAll("-", "");
        
        }

        Pattern pattern = patternMap.computeIfAbsent(regex, Pattern::compile);
        boolean  b = pattern.matches(source);
      
        return b;
    }

 经测试,执行归于正常,再次上线,HTTP延时消失,CPU占比不再冒高。

08-07
本次测试采取负载测试、并发测试、可靠性测试。测试方案采取模拟真实用户使用场景,模拟指定人数在一定时间点击界面产生请求数。 在并发10(单位个/s)、20、40、80、160、500、1000、2000基准下,调整用户数(虚拟用户用一个线程,下统称线程数)、点击准备时间(用户点击时间模拟时间,下称Ramp-up单位秒)和用户点击次数(下称循环),例如10个用户,每个用户每5秒点击1次,则线程数为10,Ramp-up为5,循环数为1。详细测试策略请看2.1。 对登录、数据新增(用户)、编辑(用户)、获取(用户)和删除(用户)进行负载测试,获得其稳定负载值。 对全站使用策略100-100-1-1进行并发测试,挑选用户服务所有接口。基础数据服务中挑选和用户服务关联功能接口5个,组织结构接口4个,和用户服务无关行政区3个接口。具体接口请查看附件1。 对全站进行可靠性测试,根据以上测试接口,选择稳定并发数后持续测试-模拟时长8+小时。 稳定性测试是通过运行状态和资源指标2个方面来分析及评估系统稳定性,请求记录项响应时间平均值、最小值、最大值、标准偏差、异常(百分)、吞吐量、接收、发送、平均字节数,服务器资源指标CPU、Memory,在此额外添加记录数据库数据。通过调试测试策略、分析实验数据得出相关系统稳定性结论,从而达到平台能力验证、规划能力、性能调优、缺陷发现等目

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

安优小青和他的程序生活

希望我对您有所帮助~

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值