spring-boot 1.4.x遇到的cpu高的问题
阅读原文时间:2023年07月14日阅读:1

如果你的spring-boot应用里tomcat线程耗cpu较高,并主要耗在做读取jar的操作上(堆栈类似下面),可能跟我们遇到同样的问题。

    CRC32.update(byte[], int, int) line: 76
    JarInputStream(ZipInputStream).read(byte[], int, int) line: 200
    JarInputStream.read(byte[], int, int) line: 207
    JarInputStream(ZipInputStream).closeEntry() line: 140
    JarInputStream(ZipInputStream).getNextEntry() line: 118
    JarInputStream.getNextEntry() line: 142
    JarInputStream.getNextJarEntry() line: 179
    JarWarResourceSet.getArchiveEntries(boolean) line: 112
    JarWarResourceSet(AbstractArchiveResourceSet).getResource(String) line: 256
    StandardRoot.getResourceInternal(String, boolean) line: 280
    CachedResource.validateResource(boolean) line: 95
    Cache.getResource(String, boolean) line: 69
    StandardRoot.getResource(String, boolean, boolean) line: 215
    StandardRoot.getResource(String) line: 205
    Mapper.internalMapWrapper(Mapper$ContextVersion, CharChunk, MappingData) line: 1027
    Mapper.internalMap(CharChunk, CharChunk, String, MappingData) line: 842
    Mapper.map(MessageBytes, MessageBytes, String, MappingData) line: 698
    CoyoteAdapter.postParseRequest(Request, Request, Response, Response) line: 672
    CoyoteAdapter.service(Request, Response) line: 344
    Http11Processor.service(SocketWrapperBase<?>) line: 784
    Http11Processor(AbstractProcessorLight).process(SocketWrapperBase<?>, SocketEvent) line: 66
    AbstractProtocol$ConnectionHandler<S>.process(SocketWrapperBase<S>, SocketEvent) line: 802
    NioEndpoint$SocketProcessor.doRun() line: 1410
    NioEndpoint$SocketProcessor(SocketProcessorBase<S>).run() line: 49
    ThreadPoolExecutor(ThreadPoolExecutor).runWorker(ThreadPoolExecutor$Worker) line: 1142
    ThreadPoolExecutor$Worker.run() line: 617
    TaskThread$WrappingRunnable.run() line: 61
    TaskThread(Thread).run() line: 745  

这种情况只发生在 spring-boot 1.4.x版本(及1.3.x版本,更早的没有确认),1.5.x已经没有这个问题。

主要的改变在org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory的内部类StoreMergedWebXmlListeneronStart方法:

// TomcatEmbeddedContext 启动时触发了该监听器
private void onStart(Context context) {
    ServletContext servletContext = context.getServletContext();
    if (servletContext.getAttribute(MERGED_WEB_XML) == null) {
        servletContext.setAttribute(MERGED_WEB_XML, getEmptyWebXml());
    }
    // 注意最后这句,1.5.3版本已经去掉了这句,它导致变慢
    TomcatResources.get(context).addClasspathResources();
}

addClasspathResources方法里对于jar资源的处理,不同的tomcat版本方式有所不同,spring-boot 中如果使用嵌入式的 tomcat8 的话这些jar资源会记录到StandardRoot里的jarResources集合里,它们会被定时清理。

tomcat容器的后台线程(ContainerBackgroundProcessor)会触发StandardRoot里的清理逻辑

    public void backgroundProcess() {
        cache.backgroundProcess();
        gc();
    }

    public void gc() {
        for (List<WebResourceSet> list : allResources) {
            for (WebResourceSet webResourceSet : list) {
                webResourceSet.gc();
            }
        }
    }

    // JarWarResourceSet里的gc方法
    public void gc() {
        synchronized (archiveLock) {
            if (archive != null && archiveUseCount == 0) {
                try {
                    archive.close();
                } catch (IOException e) {
                    // Log at least WARN
                }
                archive = null;
                archiveEntries = null;
            }
        }
    }

请求过来时,Mapper阶段会根据请求路径去找映射的资源,Cache不管命中还是未命中,都会对资源进行validate,在validateResource时要去遍历WebResourceRoot里所有的资源(包括所有的jar资源),若应用依赖的jar比较多时,会导致cpu较高。

spring-boot 1.5 版本里不会再将 BOOT-INF/lib 下的所有jar添加到tomcat的WebResourceRoot里,升级到1.5.3后这个情况没有再发生。

http://hongjiang.info/spring-boot-1-4-bug/