Tomcat启动慢问题,追根溯源,死磕到底

写作原由

解决问题固然重要,但是解决问题的思考过程我觉得更加值得分享,所以把自己的心路历程记录下来

问题发现

起因很简单,我接手的一个spring boot项目的启动特别慢,查找问题,启动慢的位置定位在了内嵌tomcat上。在网上针对tomcat启动慢问题有很多文章,但是大部分都是在说怎么去解决。了解了之后我还是疑惑不解,大致的疑惑在这:这次的spring boot版本是1.3,之前用的1.5版本从来没出过这个问题。是spring boot 做了啥配置了不去触发了一些tomcat初始化的步骤,还是tomcat做了啥不去干了这样一件事情,带着这样的疑惑我开始了一场源码之旅。

解决问题

我们来看下网上针对Tomcat启动慢的解答。tomcat的Session ID是通过SHA1算法计算得到的,计算Session ID的时候必须有一个密钥。为了提高安全性Tomcat在启动的时候回通过随机生成一个密钥。omcat 7版本后随机数的生成依赖
java.securitySecureRandom这个类(jdk)。
这个随机数的取值可以在jre里面去配置,securerandom.source=file:/dev/random
http://wiki.apache.org/tomcat/HowTo/FasterStartUp (Entropy Source部分)有一段解释,我们可以用cat 命令来进行查看一下这个/dev/random是什么东东,cat /dev/random | od -x 查看。二进制看的太乱转个十六进制看一下这里的输出便是:
随机数数据查看
不得不说我公司服务器生成的确是很慢,不知道运维爸爸干了啥。这个随机数的生成涉及到Linux 内核熵池的一个概念。所谓的熵就是描述混乱的一个量,熵值越大越混乱,那么所产生的随机数也就越随机,假作真时真亦假 无为有时有还无,嗯差不多就是这个意思。
想要解决可以去修改这个值,改成/dev/urandom。或者在启动时候指定jvm参数-Djava.security.egd=file:/dev/./urandom。问题解决了,但是依然还是有很多疑惑,回到开始的问题。究竟是spring boot做了什么,还是tomcat做了什么改变。


华丽的分割线,源码之旅

为了验证我的猜测,开始看源码,先找来一个启动不满的站点。先看各个的maven版本。启动慢的站点:spring boot 1.3对应tomcat8.0.30,启动不慢的站点spring boot 1.5对应tomcat8.5.27。完了,spring boot ,tomcat都有可能做了调整,再google也没有找到啥有用的,只能自己来了,来吧看源码。

debug

开始调试,先看日志,启动过程中有这么一段
这里写图片描述
看来tomcat还是不错的打印了自己这一段慢的类,那么我们就进去调试一下。
切入点有了开始debug,我这里用的远程调试,不知道的可以搜一下,因为问题出在测试环境,只能远程调试发现问题。
搜索关键字发现Tomcat的一段sessionid建立的源码,篇幅有限我就贴一半:
这里写图片描述
这里便是获取随机数的位置,随机数的具体实现是jdk去做的,这里不关注了,Linux就是取的那个/dev/random。好了那么开始断点,1.3spring boot的项目命中断点,随机数的获取等了好长。1.5的spring boot….,人呢,说好的来呢,我等的花都谢了,怎么没来。1.5的启动完了都没来。看来启动慢,不是因为改变了什么参数,是1.5的spring boot 或者tomcat没有走这一段sessionid的初始化创建。

寻找

从1.3的开始去找调用过程,看看有没有什么值得可疑的点,idea正好可以看见调用链,这是一段漫长的寻找,不断地尝试,不断地验证。起初我一直以为tomcat的8.5.27中有啥改变,因为调用链一直显示的都是tomcat的类和方法,tomcat初始化过程是engine—–>host—–>context这样嵌套创建的,基本没有spring boot的干预,难道是有什么参数,但是也没看见参数控制创建过程,受不了从engine创建一步步走,突然发现这么一个类,TomcatEmbeddedContext他是要给spring boot的类,查看类图:

这里写图片描述
世界在这一刻豁然开朗它继承了standardXontext这个类,tomcat,context创建的类,也就是spring boot在host创建后加入了一个自己的类,做了一些不可告人的事情去改变了tomcat的一些操作。对比1.3和1.5的两个类的去别发现,1.5中多了这么一段:

@Override
    public void setManager(Manager manager) {
        if (manager instanceof ManagerBase) {
            ((ManagerBase) manager).setSessionIdGenerator(new LazySessionIdGenerator());
        }
        super.setManager(manager);
    }

进入LazySessionIdGenerator:

@Override
    protected void startInternal() throws LifecycleException {
        setState(LifecycleState.STARTING);
    }

而tomcat原本的是什么呢,是下面这个:

@Override
    protected void startInternal() throws LifecycleException {
        // Ensure SecureRandom has been initialised
        generateSessionId();

        setState(LifecycleState.STARTING);
    }

其实spring boot1.5版本后之接就去掉了generateSessionId();这个,之间初始化的时候不创建sessionid了,而这个generateSessionId()就是会调用获取随机数的那个方法。终于一切知晓了,是spring boot搞得鬼去spring boot初始化的地方看看,他是怎么初始化内嵌tomcat的

@Override
    public EmbeddedServletContainer getEmbeddedServletContainer(
            ServletContextInitializer... initializers) {
        Tomcat tomcat = new Tomcat();
        File baseDir = (this.baseDirectory != null ? this.baseDirectory
                : createTempDir("tomcat"));
        tomcat.setBaseDir(baseDir.getAbsolutePath());
        Connector connector = new Connector(this.protocol);
        tomcat.getService().addConnector(connector);
        customizeConnector(connector);
        tomcat.setConnector(connector);
        tomcat.getHost().setAutoDeploy(false);
        configureEngine(tomcat.getEngine());
        for (Connector additionalConnector : this.additionalTomcatConnectors) {
            tomcat.getService().addConnector(additionalConnector);
        }
        prepareContext(tomcat.getHost(), initializers);
        return getTomcatEmbeddedServletContainer(tomcat);
    }

果真在host创建后进行了一个操作prepareContext(tomcat.getHost(), initializers);进入这里

protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
        File docBase = getValidDocumentRoot();
        docBase = (docBase != null ? docBase : createTempDir("tomcat-docbase"));
        final TomcatEmbeddedContext context = new TomcatEmbeddedContext();
        context.setName(getContextPath());
        context.setDisplayName(getDisplayName());
        context.setPath(getContextPath());
        context.setDocBase(docBase.getAbsolutePath());
        context.addLifecycleListener(new FixContextListener());
        context.setParentClassLoader(
                this.resourceLoader != null ? this.resourceLoader.getClassLoader()
                        : ClassUtils.getDefaultClassLoader());
        resetDefaultLocaleMapping(context);
        addLocaleMappings(context);
        try {
            context.setUseRelativeRedirects(false);
        }
        catch (NoSuchMethodError ex) {
            // Tomcat is < 8.0.30. Continue
        }
        SkipPatternJarScanner.apply(context, this.tldSkipPatterns);
        WebappLoader loader = new WebappLoader(context.getParentClassLoader());
        loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
        loader.setDelegate(true);
        context.setLoader(loader);
        if (isRegisterDefaultServlet()) {
            addDefaultServlet(context);
        }
        if (shouldRegisterJspServlet()) {
            addJspServlet(context);
            addJasperInitializer(context);
            context.addLifecycleListener(new StoreMergedWebXmlListener());
        }
        context.addLifecycleListener(new LifecycleListener() {

            @Override
            public void lifecycleEvent(LifecycleEvent event) {
                if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
                    TomcatResources.get(context)
                            .addResourceJars(getUrlsOfJarsWithMetaInfResources());
                }
            }

        });
        ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
        host.addChild(context);
        configureContext(context, initializersToUse);
        postProcessContext(context);
    }

spring boot 创建了final TomcatEmbeddedContext context = new TomcatEmbeddedContext();对象然后host.addChild(context);将他加入到了host创建后的步骤中完成了tomcat的初始化,同时也对内嵌的tomcat做了一定的修改。

结束

探究的过程其实就是一个猜想——>寻找—–>验证的过程,过程很累,但结果很爽。至于spring boot为啥要这么做?sessionid初始化不创建那么啥时候创建?为什么测试环境熵池这么少,以前就没有问题吗?这些秘密还是没有解决。生活还在继续,问题还在继续,不断探究吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值