cpu飙高问题排查

1.top命令查看cpu情况

发现进程29851一直居高不下

2.先找出占用 CPU 最高的线程 ID

top  -H  -p  29851

发现前10个线程cpu都很高

3. 将线程 ID 转换为 16 进制

先看线程14401

(1)printf "%x\n" [线程ID]   得到16进制数据

4. 执行 jstack

 jstack 29851 > stack.txt 

5. 查找对应的高 CPU线程堆栈

grep -A 30   [16进制线程ID]   stack.txt

"http-nio-18080-exec-3" #106 daemon prio=5 os_prio=0 tid=0x00007ff3d7268800 nid=0x3843 waiting for monitor entry [0x00007ff322ae1000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at org.springframework.boot.loader.data.RandomAccessDataFile$FileAccess.read(RandomAccessDataFile.java:219)
        - locked <0x0000000640096fc8> (a java.lang.Object)
        at org.springframework.boot.loader.data.RandomAccessDataFile$FileAccess.access$400(RandomAccessDataFile.java:205)
        at org.springframework.boot.loader.data.RandomAccessDataFile.read(RandomAccessDataFile.java:117)
        at org.springframework.boot.loader.data.RandomAccessDataFile.access$700(RandomAccessDataFile.java:33)
        at org.springframework.boot.loader.data.RandomAccessDataFile$DataInputStream.doRead(RandomAccessDataFile.java:175)
        at org.springframework.boot.loader.data.RandomAccessDataFile$DataInputStream.read(RandomAccessDataFile.java:155)
        at java.util.zip.InflaterInputStream.fill(InflaterInputStream.java:238)
        at org.springframework.boot.loader.jar.ZipInflaterInputStream.fill(ZipInflaterInputStream.java:68)
        at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:158)
        at org.springframework.boot.loader.jar.ZipInflaterInputStream.read(ZipInflaterInputStream.java:52)
        at java.io.FilterInputStream.read(FilterInputStream.java:107)
        at com.maxmind.db.BufferHolder.<init>(BufferHolder.java:63)
        at com.maxmind.db.Reader.<init>(Reader.java:89)
        at com.maxmind.geoip2.DatabaseReader.<init>(DatabaseReader.java:33)
        at com.maxmind.geoip2.DatabaseReader.<init>(DatabaseReader.java:23)
        at com.maxmind.geoip2.DatabaseReader$Builder.build(DatabaseReader.java:129)
        at cn.com.antwalletbot.test.GeoIpTest.getCountry(GeoIpTest.java:43)
        at cn.com.antwalletbot.common.config.interceptor.IpRateLimiterFilter.doFilterInternal(IpRateLimiterFilter.java:74)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
        at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)

 

从这个线程堆栈来看,我发现了一个比较严重的性能问题:

(1)问题定位:

  • 线程状态是 BLOCKED,等待获取监视器锁
  • 问题出现在 IP 限流过滤器中 (IpRateLimiterFilter)
  • 具体是在读取 GeoIP 数据库时发生的阻塞

(2)具体调用链:

IpRateLimiterFilter.doFilterInternal -> GeoIpTest.getCountry -> DatabaseReader.Builder.build -> DatabaseReader.<init> -> Reader.<init> -> BufferHolder.<init>

(3)核心问题:

  • 每次请求都在尝试重新初始化 GeoIP 数据库读取器
  • 这是一个重量级操作,不应该在请求过滤器中反复进行
  • 多个线程在竞争同一个锁,导致大量阻塞

问题代码:

public class GeoIpTest {
    private static final Logger log = LoggerFactory.getLogger(GeoIpTest.class);

    public static void main(String[] args) {
        String ip = "2400:c600:3360:b864:1:0:de91:c6b5";
        getCountry(ip);
    }

    public static String getCountry(String ip) {
        try {
            if ("127.0.0.1".equals(ip)) {
                log.info("getCountry-本地IP,不进行查询");
                return StringUtils.EMPTY;
            }
            // 读取当前工程下的IP库文件
            InputStream inputStream = IpUtil.class.getClassLoader().getResourceAsStream("GeoLite2-Country.mmdb");


            // 使用 InputStream 创建 DatabaseReader
            DatabaseReader countryReader = new DatabaseReader.Builder(inputStream)
                    .withCache(new CHMCache())
                    .build();
            // 读取IP库文件

            CountryResponse countryResponse = countryReader.country(InetAddress.getByName(ip));
            String country = countryResponse.getCountry().getNames().get("zh-CN");
            log.info("从country IP库读取国家结果: {}", country);
            return country;
        } catch (Exception e) {
            //log.error("getCountry 从country IP库读取国家失败,ip:,错误信息是:{}", ip);
            return StringUtils.EMPTY;
        }
    }
} 

主要问题:

(1)主要性能问题: 

// 每次调用都重新创建 DatabaseReader,这是最大的性能瓶颈
DatabaseReader countryReader = new DatabaseReader.Builder(inputStream)
        .withCache(new CHMCache())
        .build();

(2)资源泄露问题:

// 没有关闭 InputStream 和 DatabaseReader
InputStream inputStream = IpUtil.class.getClassLoader().getResourceAsStream("GeoLite2-Country.mmdb");
// 应该使用 try-with-resources 或在 finally 中关闭

(3)改进方案:

@Component
public class GeoIpService {
    private static final Logger log = LoggerFactory.getLogger(GeoIpService.class);
    private DatabaseReader databaseReader;
    
    @PostConstruct
    public void init() {
        try (InputStream inputStream = GeoIpService.class.getClassLoader()
                .getResourceAsStream("GeoLite2-Country.mmdb")) {
            if (inputStream == null) {
                throw new IllegalStateException("Cannot find GeoLite2-Country.mmdb");
            }
            databaseReader = new DatabaseReader.Builder(inputStream)
                    .withCache(new CHMCache())
                    .build();
        } catch (Exception e) {
            log.error("Failed to initialize GeoIP database", e);
            throw new RuntimeException("Failed to initialize GeoIP database", e);
        }
    }
    
    @PreDestroy
    public void destroy() {
        if (databaseReader != null) {
            try {
                databaseReader.close();
            } catch (Exception e) {
                log.error("Error closing DatabaseReader", e);
            }
        }
    }

    public String getCountry(String ip) {
        if (StringUtils.isBlank(ip) || "127.0.0.1".equals(ip)) {
            return StringUtils.EMPTY;
        }

        try {
            CountryResponse response = databaseReader.country(InetAddress.getByName(ip));
            return response.getCountry().getNames().getOrDefault("zh-CN", StringUtils.EMPTY);
        } catch (Exception e) {
            log.error("Failed to get country for IP: {}", ip, e);
            return StringUtils.EMPTY;
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小王爱编程啊

请多多鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值