1、卷首语在应用服务器的发展历程中,Apache Tomcat 在其中扮演着极其重要的角色。从最初的 WebLogic 到 Jboss,到 Tomcat/Jetty,再到现在 Spring Boot 使用的嵌入式 Tomcat,我们见证了应用服务器从封闭到开源,逐步轻量化的演变过程。而 Tomcat 社区作为一个活跃的开源社区,从 上世纪 90 年代至今,已经存在了 25 年之久,且这个社区仍然在不断更新迭代,这离不开社区中有一群坚定的人,在这个社区中持之以恒的奋斗,同时我们也看到一些新兴力量的崛起,给这个社区注入了很多鲜活的血液,这其中也不乏来自中国的新生力量。本 篇 电 子 书 旨 在 全 面
2、 介 绍Apache Tomcat 的 前 世 今 生,我 们 将 探索 Apache Tomcat 的技术内幕,并分享在喜马拉雅中实践的经验,也将介绍在下一个云原生时代 Tomcat 的新形态,探索 GraalVM 静态编译在 Web 容器应用中的使用实践,同时,对于想参与 Tomcat 社区的朋友,还将具体了解到参与 Tomcat 社区的方法。此外,为了更好地观测 Tomcat,我们还将分享使用 APM 工具链快速定位 Tomcat 问题的最佳实践,最后了解到如果进一步保护 Tomcat,提升 Tomcat 的安全性。此外,不仅仅是 Apache Tomcat,我们还探索新兴的云原生网关
3、Dubbo-Go-Pixiu 的前世今生,以及介绍如何使用 Apache Sling 构建默认安全的 Web 应用程序。无论您是对 Tomcat 感兴趣的初学者,还是希望进一步了解其内部工作原理的资深开发者,亦或是想参与社区的开发人员,本电子书都将为将通过详细的实例和案例,以及清晰的解释,帮助您更好地理解和应用 Tomcat 技术。您将对 Tomcat 的技术内幕有更深入的了解,同时也能够利用这些知识来解决实际问题。目录页Tomcat 的技术内幕和在喜马拉雅的实践.6GraalVM static compilation in web container application.18How to
4、 participate in Tomcat community.28Web 容器可观测最佳实践.41Dubbo Echo System-Dubbo Go Pixiu.50Securing Apache Tomcat.58Secure By Default Web Applications Apache Sling-Robert Munteanu.70Tomcat 的技术内幕和在喜马拉雅的实践6Tomcat 的技术内幕和在喜马拉雅的实践1.IO Thread ModelTomcat 是一个非常完整、完善、功能非常强大的开源项目。我们每个人开始做开发,接触的第一个项目可能就是 Tomcat。我们
5、今天主要介绍的就是 IO 和线程模型,IO 包括 NIO和 NIO2。上图是 NIO 的线程模型,它负责接收连接。Tomcat 创建一个端口,然后我们接受浏览器发起的请求,accept 线程是 block,有连接就连接,没有连接就等待新的连接请求。它接收连接之后,会创建一些Socket,然后把这些Socket放到EventQueue里,通过Poller线程消费 EventQueue 里的 Socket,注册到 Selector 上。Poller 线程负责 Selector 循环,确认上面是否有读/写事件,如果有读/写事件发生,就生产一个 SocketProcessor,提交到 Catalina
6、 work thread pool。Tomcat 的技术内幕和在喜马拉雅的实践7Poller 线程负责将读取事件注册到 Selector,创建 SocketProcessor 提交到 Catalina 线程池里,从 Selector 注销读取事件。有一个读事件过来了,它要提交到 SocketProcessor真正去处理这个请求。但是 Poller 线程不负责读,它只做事件触发机制。即告诉你一个事件请求过来了,你可以开始读了。然后它要把这个事件 unregister 掉,因为一个请求需要保证只有一个 Catalina work 线程处理,否则就又会创建一个 Socket 交给另外一个线程处理。所
7、以在 Catalina work 线程处理的时候,一定不能有另外一个线程去读 Socket 的数据,否则包就会乱掉,所以就会有unregister 这个操作。Tomcat 的技术内幕和在喜马拉雅的实践8刚刚讲了 Poller 线程只是告诉你有读事件可以读了,具体的 IO 的操作还是由 Catalinawork 线程处理。具体的操作包括解析请求行没有阻塞、解析头无阻塞、过滤器路由器、servlet 路由器、读 body 阻塞。一般情况下,app 发一个请求过来,你的包可能是完整的,但如果发一半过来,这个请求行可能是不完整的,这个时候它是不会阻塞的,它就会注册一个读事件读剩余的。读完请求行以后,它
8、会继续解析头。解析头和解析行是一样的,也是不会阻塞。只需要配置好头的大小,它就会按照头的大小读。因为 Tomcat 里有很多 filter 和 servlet,然后我们自己做了一些路由,去匹配 filter 和servlet。一般我们这里处理完以后,我们就会去读它的 body,但它和请求行、请求头不一样,它会阻塞。所以如果 body 不完整,比如 content-length 是 1000,body 只读到500,就会阻塞在 Catalina work 线程。这个时候它会通过增加一个 wait,等到下次 Poller线程来唤醒 Catalina work 线程。前面介绍了 NIO 模型主要分为
9、三个部分 Accept 线程,Poller 线程,Catalina work 线程。Tomcat 的技术内幕和在喜马拉雅的实践9目前Tomcat已经支持了NIO2,它和NIO有一个很大的不同,中间没有Accept线程,Poller线程,它把 NIO 模式简化了。所以很多人说 NIO2 是异步的,但其实也不是,它只是基于自己的 JDK 实现 epoll、API 进行异步模拟的。NIO2 在事件可读的时候会告诉操作系统,然后我这个时候就返回去做其他事情了。操作系统会把数据读到我的包里,然后告诉我可以用数据了,我就直接读数据就行了。而 NIO 同步要发几个读,我需要自己触发操作系统去 copy 数据
10、过来。然后我还需要在这里等着,等 copy 完了,再去把数据读出来,是会阻塞在这里的。NIO2 并不是真正的 NIO,它的 API 使用机制模拟会告诉你读和写都完成了,所以整个模型是一个 EPoller 线程。它相当于直接操作 EPoller 的 wait,只要线程有事件来,它就会生成连接事件,读/写事件,他这里有一个队列,只要有事件就会一直读,不会说新建一个连接事件,我就处理读事件,它会把所有完整的事件先读出来。读完以后就可以去处理了,如果是连接,我就执行 accept 这个操作,把连接读出来。接受连接不像 NIO 是单路线,有一个区别是,一般连接是有限制的,默认是 1 万。如果你是-1或者
11、是没有限制的,accept 就一直不会阻塞。如果是有限制,可能达到目标就会阻塞。所以这个时候就会有两个不同的处理方式,如果是有限制的,不会用当前的 Poller 线程去做 accept 的操作,它会新提交一个任务到线程池里,让后面接着做 accept 的连接,因为不能阻塞 Poller 线程。如果当时我是个读事件的话,我只要读出来就能提交给 Catalina work 线程,这个时候读是由 Poller 线程完成的。假如我新建一个连接,做了三次握手,但是我不给你发数据。那么 Poller 线程就读不到数据,因此也就不会做处理。它只是注册一个事件到 Poller 线程,等数据来了再来读。还是和之
12、前的不同,不会像 NIO 一样,刚开始肯定要去注册一个读事件。NIO2 会直接读,一般很多的网络框架优化都是在这个地方做的。我是直接尝试读,不会去注册读事件。因为大部分情况下建立连接以后,后面就会发现不需要去做读事件的操作。因为你做一个读事件,这个读事件就会系统调用,就会涉及到上下文切换的开销。Tomcat 的技术内幕和在喜马拉雅的实践10我一直在这里习惯用 select,有事件就 EPoll 这个地方,没有就阻塞就 EPoll 这个地方。EPoller 线程就是做这个事情。刚刚我们接触的连接它也是通过 Catalina work 线程,它们起来的时候可以添加一个 accept task,就是
13、先让你接受请求。它和刚才说的读 body 一样,当 body 不够的时候,才会去注册一个事件到 Epoller 线程上去,它的读也是阻塞的。NIO 和 Catalina 不一样的是,它只负责读和写,比 NIO 少了注册事件的操作。Tomcat 的技术内幕和在喜马拉雅的实践11接下来做一个比较,看一看到底哪个更合适我们的业务场景使用。NIO2 和 NIO 相比,优势是连接一过来,它就可以主动去读这个数据,这样就减少了很多的注册和取消的事件。像 NIO 它就会先注册一个读事件,然后等数据来了,再把注册取消掉,最后交给后面的 Catalina work 线程处理这个请求。此外,它的性能模型更简单,它
14、少了 Poller 线程。NIO2 和 NIO 相比,缺点是容易受慢客户端影响。刚开始有事件过来的时候,它用 Poller线程来读,然后回调 Catalina work 线程让它继续读后面的数据。但是如果是没有 Poller线程的情况下,body 就会阻塞,而且这个时候是 Catalina work 线程。假如你设了 100个线程值,但读 body 都阻塞了,那么后面 Poller 线段就会连接一个新的连接,然后提交一个任务到 Catalina work 线程来处理。但这个时候就会导致没有线程来处理新的连接事件的请求。EPoll read 容易成为瓶颈。因为 IO 拷贝操作是 poll 线程的
15、所以如果是并发量比较大/body 比较大的情况下,很容易使 copy 的进度比较慢,还可能会影响你的建连效率。增加了复杂性。与 NIO 相比,它的复杂性确实增加了很多,它有各种的回调。因为它是通过异步模拟的 NIO 写代码的方式。此外,还有一点,NIO2 的文件发送没有基于应用操作系统这种零拷贝模式,直接通过操作系统发到连接缓存的 channel 里去。但还是用了普通的 IO 把它读出来,然后再把它写到channel 里面去。相比 NIO 反而增加了它拷贝的次数。那么到底怎么选呢?Tomcat 9 和 10 默认的还是 NIO。通过官方的配置我们也能看得出来,它建议的还是 NIO。虽然切换次
16、数减少了,系统调用次数减少,系统的切换减少了。但是性能在我们平时功能可能感觉不到,所以对于稳定还是 NIO 这种模式。2.Memory ModelTomcat 的技术内幕和在喜马拉雅的实践12接下来介绍一下 Tomcat 的内存模型。在平时的开发中,Tomcat 确实做的太优秀了,几乎感觉不到它里面的 buff 建连过来到它读出来再到我们。Tomcat 是基于 servlet 规划实现的 web 容器,所以我们拿到的第一个 Tomcat 的接口 API 是 Servlet API。它里面屏蔽了很多种内存,到底是操作系统读出来,读出来它里面是怎么 copy 的,到我们拿到的 request 参数
17、和 url 它中间经历了多少复制,以及中间是怎么管理的,我们平时几乎没有机会去了解。这是我们之前做 API 网关的时候,我们压测到性能问题的时候发现的,所以我们就对它们整个内部原理进理解了一下。总结出了这张图,它内存 buff 的原理。首先它缓存了请求行和请求头,从内核端到用户端我们一直拷贝,因为 Tomcat 也支持 APR协议,这个协议是堆外内存的,所以它不需要那么多拷贝。但我们默认的 http 协议默认都是堆内存的,所以必须要通过从内核端到用户端,通过堆外内存的转换再到它的整个包,这中间有两个拷贝。拷贝过来以后中间有一个 ByteBuffer,它的请求行还没写成原则,请求行的 heade
18、r、method、url、querystring 的协议都缓存在 ByteBuffer 里面。整个头有一个结束的标志叫 HeadEndOffset,因为这些是不变的,我们在读 body 数据是时候,不会把前面的数据给覆盖掉了。Tomcat 的技术内幕和在喜马拉雅的实践13因为 body 可能就不到 512k 或者 256k 就够了,但 body 可能是一个大的 Pod 请求,可能要发几 k 数据。因为 Socketsize 默认是 8k,如果你的 post 请求是 16k,你可能要拷贝多少次。它就会再对外出来,再拷贝到 ByteBuffer 里面,从 HeadEndOffset 那里重新写数据
19、再整个上到上面的 PostDataByte。所以如果整个拷贝出来,要经过三次的拷贝我们的Post 请求。写模型我们一般是 GetPutBuffer、OutPutBuffer,里面我们的那些请求,CharBuffer 里面是响应行和响应头,再写到 ByteBuffer 里面去。3.Classload FeatureTomcat 的技术内幕和在喜马拉雅的实践14Tomcat 一直比 servlet 多,classload 机制,它打破了双亲委派模型。我们 JDK 的 classload一般都是一层一层先问,我们先问他的附近有没有交代,一直到最顶上去。Tomcat 的整个原理是,首先 classp
20、ath 指定,然后这两个,CommonClassLoader 有自己的 class。它主要做了 WebAppClassLoader,因为 Tomcat 支持部署多个服务,我们一般还能做多个业务。那么它怎么隔离呢?每个应用都有一个 WebAppClassLoader,它会进入 JDK 里面加载一下。然后它这里两个顺序,一个是从 WEB-INF 下的 Class,这个是我们自己写代码的 ClassPath,还有一个我们依赖包。而且它还分顺序,需要先加载 class,这就是为什么我们能把我们依赖的第三方框架重写,还能加载的有序。因为 Tomcat 先加载了 class,我们编译的 class 里面的
21、路径代码,再加的依赖包里面的路径。4.Performance OptimizationTomcat 的技术内幕和在喜马拉雅的实践15我们的实践基本上都是用 Tomcat 来做的,不论是通过 Tomcat 部署的,还是 SpringBoot嵌套的,全部默认的是 Tomcat 容器。此外,还有一些个性化配置:Tomcat 的技术内幕和在喜马拉雅的实践16线程数,阻塞 web 服务可以 2k:我们所有的 web 都是 IO 型的,你要读数据库、调 rpc、调各种远程服务都是偏阻塞型的。web 服务你基本都可以设定很大的线程数,要不然很容易不饱满。Backlog,连接队列 1024,gc 影响建连:因
22、为 TPC 以后,你需要先到 Backlog 里面等到accept 建连,所以我们设置成 1024,你在 gc 可能会影响你的建连。maxHeepAliveRequests 1024,提升连接复用次数:Tomcat 连接多少次是有限制的,默认最多 100 次,比如请求往里发送 100 次之后,它就会把你 close,如果不 close 就会报错。所以我们会把次数调的比较大,让连接尽可能复用,减少建连的开销。maxHttpHeaderSize 32k,和网关一致,减少 400code:如果你公司会经过网关,经过 Tomcat,一些头比较大的时候,可能会等你 400 排查是哪里的问题。因为我们碰到
23、很多这种 case,所以我们尽快把全链路设置的一样大,减少配置的问题。对象池技术,下面两张图是 Tomcat 用的最多的链接池,和每个请求相关:Tomcat 针对线程做了比较好的优化,它用了对象池的技术,包括 Socket Channel。Socket建连它会有很多 servlet Channel,因为它会做一些对象池来缓存 eventCaches,Tomcat 的技术内幕和在喜马拉雅的实践17eventCaches 是 NIO 里面的。SocketProcessor 有事件读的话,就提交给 Catalina work线程,提交的 test 都是缓存的,通用的。SocketProcessor
24、提交现在运行里面,他真的去解析请求行或者请求头,它这里又是通用的,又是找了 Processor,它也是做缓存用的,尽可能减少应用对象的开销。Processor 和 Socket Channel 不一样,它缓存的速度和容量是有大小限制的,像 RequestProcessor 它的初始大小是 200,但它的 limit 是-1,即没有限制的。因为你放回去它就会膨胀,膨胀就会有-1,没有限制就会一直膨胀。如果并发压测就会导致比如 200-5000 个并发压,都膨胀就都全部缓存起来了。因为它这个东西还会做 Object 监控,把它全部放在 global 把它统计出来,就会导致只要送回去,就不会把清理,
25、所以这里面就会有很多。因为 Processor 还会把 Request、Pods都缓存在这里,这个时候你会发现这些东西基本都已经处理完请求了,但缓存里的单维还在这里,就是因为-1 没有限制。总结,本次分享就是这些,主要对 Tomcat 的网络 io,线程模型,内存模型,类加载机器,以及对像池技术做了一个介绍,欢迎交流讨论。GraalVM static compilation in web container application18GraalVM static compilation in web containerapplication1.Java Web 容器应用运行回顾在介绍静态编译在
26、 Web 容器的应用之前,我们先来简单回顾一下,非静态编译或者说常规的 Java Web 容器的运行过程。如上图所示,一个正常的 Java 应用,从编写代码、打包、运行过程,首先需要写一个 java的源文件,然后通过 javac 把源文件打包编译,编译成 class 的字件码。运行过程就是虚拟机加载字节码做解释执行,或者在解释执行之后,对热点代码进行即时编译,这个就是Java 应用的运行过程。在这个过程里,因为有了字节码的概念,让 Java Web 容器或者 Java 应用实现写一次,让应用实现跟平台无关,这是它的设计优势。GraalVM static compilation in web c
27、ontainer application19但这个优势是否就无可挑剔能够帮助 Java 应用广泛应用在各种场景下了呢?答案是否定的。例如:有的时候我们会发现写一个很简单的 Java 程序的 demo,代码可能不多,逻辑也不复杂,它的启动时间短则几秒或者十多秒,它的内存占用从几十兆到上百兆不等。但如果我们的应用想要快速扩缩容,就会对我们非常不利。这个现象产生的原因,就如上图所示,首先会有一个虚拟机启动的过程,对应图中红色的部分。在 Java 程序没执行之前,会首先把虚拟机加载到内存里去。当虚拟机加载到内存里后,它就会去做类加载,就是应用程序的加载过程。就如图中浅蓝色的 CL 过程,它会把应用程序
28、加载进去。之后 JVM 就会对程序进行解释执行了,这里要注意一下,它是解释执行不是编译,对应图中浅绿色的部分。当解释执行产生,程序就运行起来了,就可以处理请求了。处理请求的过程中,它就会进行垃圾回收,对应图中黄色的部分。当程序逐步的运行时,一些方法和一些代码段会被反复的执行。在执行的次数很高的情况下,解释执行的性能就会变得比较差了。这个时候在虚拟机里,会有一个叫即时编译 JIT 的概念,对应图中白色的部分。在我们的程序中它有一套规则,比如在一个时间内一个方法执行了 500 次,我们可能觉得它是个热点GraalVM static compilation in web container appl
29、ication20代码。然后我们对相关的代码通过 JIT 进行即时编译,对应图中绿色的部分,它是和机器强相关的编译后的汇编代码,它的执行效率是比较高的。所以从这张图我们可以看到,一个 Java 应用从启动到运行到性能的峰值,中间还是会经过一个比较长的时间,包括虚拟机初始化、应用初始化、应用预热等等过程。这就是 Java 应用/Java Web 应用启动比较耗时的原因。另外一个问题是,为什么 Java 应用写一个比较简单的逻辑,最终的占用内存会比较高呢?这个的原因从图中也可以看到,程序什么都不执行,就会加载一个虚拟机,它也会有一定的内存大小。如果我们用一些工具,比如 NMT 工具,可以打印一下我
30、们的应用,也可以看到 GC 本身占用的内存,它们都会有一定的内存。另外,也和解释执行编译的过程有关。比如 C/C+语言,会在执行之前做编译,编译的过程就有优化。而 Java 程序它是解释执行,它在解释的过程中会把我们的代码全部逐步地加载到内存中,因为它不知道哪一块代码需要,哪一块不需要,所以实际加载的代码会比实际要执行的代码要多很多。这个就是应用在运行过程中的额外的内存开销。2.静态编译技术GraalVM static compilation in web container application21基于以上的两个问题,是否有相应的解决办法呢?那就回到我们今天的主题静态编译技术,它能够有效的
31、帮助我们解决掉。静态的反义词是动态的,刚才介绍的 Java 就是运行过程中动态的编译。如果看过一些英文资料,应该知道一个词叫 AOT(Ahead of Time),即提前编译,它和静态编译是一个意思,只不过是不同的说法。所以静态编译技术就是在应用没有正式运行之前,对其进行编译处理,运行过程中我们就直接运行编译好的程序即可。上图左侧中右半部分就是正常的 JVM 的运行方式,左半部分是 Java Web 应用采用了静态编译技术的运行方式。首先也是写一个 Java 的代码,但它没有编译的过程,它直接把 Java程序通过静态编译成了一个 Native Image 的可执行文件。这个可执行文件和执行环境
32、是强相关的,所以直接运行就可以了。那么静态编译的输入是什么?静态编译下,针对垃圾回收问题,如何解决?如上图右侧所示,是一个静态编译的详细解读。最左侧是应用静态编译之前的输入,比如应用程序、应用程序依赖的三方库、JDK 里的依赖、Substrate VM。Substrate VM 是静态编译中很重要的运行时,它包括静态的编译器、垃圾回收模块,比如GraalVM,它在之前的社区版本里,支持 SerialGC 的垃圾回收算法,商业化的支持 G1。但在最新版本里,开源的社区版也支持 G1 垃圾收集器。GraalVM static compilation in web container applica
33、tion22那么静态编译如何解决前面的两个问题的呢?静态编译之后它就没有了 JVM 初始化的启动过程,也就是上图中红色的部分就没有了。解释执行和即时编译的部分也没有了,也就是浅绿色和的部分。即时编译的部分也没有了,它的内加载过程会比原来少很多,因为做了很多的编译优化。因此它一启动,执行的就是深绿色的部分,这样之前冷启动的问题就非常有效的解决了。另外,静态编译后也不需要加载虚拟机,也不需要加载那么多的类了,因为它会利用静态编译,通过静态分析把和应用运行无关的类,剔除在最终可执行文件以外。把程序运行的过程中,只会把我们必须的内容编译到可执行文件里面来,让我们的运行内存得到有效的降低。上图右侧是微服
34、务的框架,在适配了静态编译之后,展示了在启动速度、运行时、内存占用方面的表现,可以看到提升还是非常大的。GraalVM static compilation in web container application23任何技术都有两面性,静态编译也是一样。它虽然有很多优势,但也存在着一些不足。它让动态特性使用起来比较麻烦,因为静态编译的过程中会有一个静态分析,它会通过上下文无关的指向性分析,从入口开始对程序进行分析,分析哪些代码是我们应该运行的,哪些代码是不应该运行的,把不应该运行的代码从最终的可执行文件中剔除掉。如果它把某些代码误识别为不需要执行了,这些代码就不会包含在可执行文件里了。因此就
35、会使运行过程出现问题。这些容易被误理解为不需要执行的代码,主要包括反射、动态代理、JNI 等等。那么为什么会把这些代码误认为是不需要执行的代码呢?如上图右侧所示,它是一个很简单的反射逻辑,通过一个变量 className,也就是引用了一个类。在正常情况下,在运行过程中才知道这个变量是什么。而静态编译会在运行之前做编译,所以这个时候它无法知道 className 这个变量代表什么值,是哪个类的,这个类里的哪些方法会被反射调用。只有一些特殊场景它能知道,比如这个变量是 final 的。解决这个问题的方法是,动态特性可以通过配置文件,把程序里的动态特性做一些标注。在静态编译的时候,它会把配置文件里相
36、关的类编译进去。GraalVM static compilation in web container application24但应用程序里有这么多类,我怎么知道哪些是动态特性要找出来的呢?GraalVM 提供了native-image-agent 的工具,也就是在静态编译之前,我们预执行一下,这样它就可以把应用程序中所有和动态特性的内容都扫描出来了,然后把它记录在配置文件里。等到静态编译的时候,它就会把相关的东西都包含进去,这样就解决了代码里面动态特性可能会被误识别的问题。静态编译是一项技术,是一种方法,它可以解决一些问题。而 GraalVM 是静态编译的解决方案,它提供了像静态编译器等的
37、技术方案,让 Java 程序/Java Web 程序能够很简单地实现静态编译的效果。如果看过 GraalVM 相关的一些文档,就可以知道 GraalVM 是 Oracle推出的基于 Java 开发的开源高性能多语言运行时平台。那么它和静态编译又有什么关系呢?如上图所示,GraalVM 依赖于 OpenJDK 的底层运行时,它在中间提供了一个静态的编译器。另外,它在上层提供了一些解释器的框架。这就会让各种语言通过 GraalVM 相关的解释器框架很容易开发一个解释器。然后我们的程序就可以通过解释器翻译成 GraalVM 编译器能够编译的内容运行。GraalVM static compilatio
38、n in web container application25上图会更明显一点,GraalVM 的核心是中间 GraalVM JIT 的编译器,它能够提供静态编译的效果,让我们的程序像运行普通 Java 程序一样运行,同时也能支持其他多语言写的程序运行。3.静态编译助力 Java Web 容器云原生化GraalVM static compilation in web container application26Tomcat 通过分层架构让应用能够比较好的被托管,处理请求,返回响应。目前 Tomcat 9版本已经适配了 GraalVM 的静态编译技术,能够实现开箱即用的效果。下面展示一个小的
39、demo。这是一个最简单的 Java Web 的应用,我们用的 Spring Boot,它里面依赖的是 Tomcat 10内嵌的版本。这个 demo 很简单,就是一个 http 接口,正常应用启动的过程大概用了两秒多的时间。下面来看一下这个应用程序能否正常执行,可以看到它请求打印出了响应。再来看一下 JavaWeb 应用运行时内存的占用情况,可以看到它在运行过程中用了 110M 的内存,启动时间大概是两秒多。我们再来看一下它静态编译之后,运行时内存占用和启动时间的改善情况。现在通过SpringBoot 的版本内嵌了 Tomcat 之后,Spring Boot 通过集成 GraalVM 的一些组
40、件,通过一行命令,我们就可以对我们的应用进行静态编译了。现在我们的应用通过刚才的那一条命令就已经开始进行静态编译了。它会有一些初始化,做一些分析等等,这个过程可能会花个几分钟。整个静态编译过程就是 Java Web 应用在运行前做一下静态编译就好了,然后运行的过程中可能通过镜像、包的方式,都可以比较简单地运行静态编译之后的 Java Web 应用。现在它就开始执行相关的静态分析了,静态分析在整个静态编译过程中是一个比较耗时的过程。后续的步骤是编译过程中的,比如方法的解析,内敛一些方法,编译一些方法等等。整个静态编译过程和我们的应用也有关,较大的应用编译时间可能也会更长一点,小的应用则会短一点。
41、但编译时间不会影响应用的静态编译,因为我们不需要每改一行代码,就去做静态编译。只要应用里的动态特性能够被正常配置,假如不是写动态特性的东西,测试的时候可以不用做静态编译,运行的时候做即可。现在静态编译已经完成了,它实际用了 2 分 26 秒的时间。静态编译完之后,在应用 Topic目录下面,就有一个可执行的文件了,它就是静态编译的结果。我们直接执行这个文件,GraalVM static compilation in web container application27就可以启动我们的应用了,这一次的耗时是 0.17 秒,刚才是 2 秒多。现在通过静态编译之后,让 Java 应用/Java W
42、eb 应用的启动时间,变成了原来的 1/10 还不到了。下面看一下我们程序的执行逻辑,刚才看到请求能够被正常响应。再来看一下应用运行时的内存占用,可以看到一开始是 110.7M,现在只有 30M。在运行时内存占用方面,通过静态编译的方案,让 Java web 应用得到了显著的改善。现在静态编译相关的技术,在 Java/Java Web 领域得到了很大程度的推广和应用。微服务方面,比如国内的 Spring Cloud、Dubbo,还有一些其他的组件,在静态编译方面都做了适配,让大家能够比较方便地使用。上图是我们测试的一些比较复杂的 demo,可以看到不光是简单的应用,复杂的应用通过静态编译之后,
43、它的启动时间、运行时内存占用也都有非常明显的改善。How to participate in Tomcat community28How to participate in Tomcat community首先做个自我介绍,我叫李晗,目前任职于网易有道,2022 年成为了 Tomcat committer,今年成为了 Tomcat PMC member。本次将介绍四种参与到 Tomcat 社区的方式。1.翻译How to participate in Tomcat community29之前有过独立部署经验的同学可能知道,Tomcat 在不同环境下,输出日志的语言是不同的。比如中文,在中文环境
44、下最头疼的可能就是乱码问题,由于它的编码问题不是 UTF-8导致的,那么 Tomcat 是如何管理这个翻译的呢?大部分项目的日志会通过文件来区分不同的语言版本,Tomcat 也不例外。唯一不同的是,如果 Tomcat 想修改内容,不需要提 pr,也不需要动代码。因为 Tomcat 使用的是 Poeditor这个网站单独管理日志的翻译工作。上图展示的是一个我实际加入的中文翻译项目。目前 Tomcat 支持的语言大概有 20 种,比如常见的法语、西班牙语、德语等等。这个项目没有任何限制,不管你是否会代码,是否是 committer 都可以参与进来,只需注册一下账号即可。此外,还需要注意一点,在实际
45、翻译的时候,除了条目要对应之外,里面的占位符也要保持一致。2.邮件组在 Apache 基金会的所有项目几乎都有邮件组,Tomcat 也不例外。下面我主要介绍两个邮件组的用法。第一个是用户邮件组,它主要是用户相关的。比如在使用过程中遇到了一些 bug 或者有问题,甚至是“Tomcat 如何用?”这种问题,都可以在How to participate in Tomcat community30用户邮件组询问。但需要注意的是,请大家在邮件中尽量详细的描述一下问题的前因后果,以便我们更好的帮助大家排查问题以及复现问题。比如 Tomcat 的版本(7/8/9/11);操作系统(Linux/mac/Win
46、dows);配置文件;如果你使用的是 SpringBoot 的项目,最好贴一下 yml 文件;如果是独立部署的项目,就贴一下最主要的 server.xml 配置文件。但在贴信息的时候,请把敏感的重要数据模糊掉。此外,如果遇到了报错,比如空指针,需要把详细的栈信息贴上。第二个是开发邮件组,它主要和 Tomcat 的开发有关。需要注意的是,这个邮件组仅限于开发,如果有任何用户的问题还是得去用户邮件组。因为经过一段时间的观察,我们发现很多用户会往 dev 邮件组发送一些用户相关的问题,或者两个邮件组都发的情况,这对于一些订阅开发邮件组的朋友来说是存在困扰的。dev 邮件组主要用于关于 Tomcat
47、开发的讨论,比如最近比较火的 JDK 21 的虚拟线程,关于“Tomcat 是否支持虚拟线程以及如何支持?”,就是在这个邮件组下产生的。如果大家有一些比较感兴趣的话题,可以去这个邮件组下搜索。除了日常讨论之外,还有两个重要的作用,一个是接收提交 committer 的代码通知,一个是接收 Bugzilla 的通知。3.BUG&PRHow to participate in Tomcat community31上图是 Bugzilla,是 Apache 很早之前的一个 bug 管理系统。那么大家肯定会有一个疑问,Tomcat 为什么不用 Github 的 issues/pr 来管理呢。因为 To
48、mcat 截止到目前已经有二十多年的历史了,是 Apache 基金会下前五比较早的项目,所以自它被捐到 Apache 基金会以后,它的 bug 的管理方式一直是 Bugzilla。其次,对于 Tomcat 社区而言,Bugzilla 除了对于一些新手朋友不友好之外,对于老朋友来说,它的功能完全够用。只不过它的界面比较老,不是很美观。此前,Tomcat 社区也思考过,是否要将 Bugzilla 迁移到目前很新的系统上,比如 Github的 issues。因为从整体上来看,Bugzilla 就不像一个现代的 bug 管理系统,但考虑到不是所有人都能访问 Github,所以没有立马搬迁。目前 Tom
49、cat 对于 Github 的使用仅限于代码托管以及 PR 的接收。如果大家想提交一个代码的修改,可以使用 Github 提一个 PR 或者使用 Bugzilla 提一个 Patch。How to participate in Tomcat community32介绍完系统之后,下面介绍一下 IDE 的配置。就我目前的经历而言,对于一个新手,如果想参与到 Tomcat 的社区当中,面临的最大的问题是,这个代码太老了。我们目前常用的项目管理有 maven、gradle 等等,而 Tomcat 使用的是 Aapache Ant 工具来进行的依赖管理,对于新手来说会很陌生。作为新手,如果想尝试学一下
50、 Tomcat 代码的源码,第一步面临的问题就是配置。Tomcat社区为大家提供了三种 IDE 的配置,流程如下:第一步,先去 Tomcat 的仓库把代码拉下来,然后切到对应的版本。如果你想学习 Tomcat的最新版本的代码,直接切 main 分支即可。第二步,安装 ant 工具之后在项目目录下执行 ant 命令,然后整个 Tomcat 就可以构建了。99%不会报错,剩下 1%的报错可能和系统环境有关。如果真的有一些解决不了的问题,可以通过前面讲的用户邮件组发帖子,其他热心用户会帮你解答。在整个代码编译完成之后,进行配置 IDEA。目前国内有两种方式编译 Tomcat 的源码,第一种方式是把T






