gitbook/手把手带你搭建秒杀系统/docs/429098.md
2022-09-03 22:05:03 +08:00

165 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 12高性能优化单机Java极致优化
你好,我是志东,欢迎和我一起从零打造秒杀系统。
今天这节课我们主要是聊一聊和Java相关的一些技术点的优化方向包括Tomcat、RPC框架、JVM以及CDN等。但在开始之前呢我们先来说个基本知识点那就是关于程序代码的两种运行模式**CPU密集型与IO密集型**
CPU密集型操作顾名思义就是需要持续依赖CPU资源来执行的操作比如各种逻辑计算、解析、判断等等。在这种情况下我们的优化方向是尽可能地利用多核CPU资源并且避免让CPU做无效的切换因为CPU已经在不停地工作了谁来干都一样同时切换CPU还浪费资源。所以这个时候我们最好让任务线程数和CPU核数保持一致从而最大限度地利用CPU资源。
和CPU密集型操作相对的就是IO密集型操作了比如磁盘IO或者网络IO这个过程操作系统会挂起任务线程让出CPU资源。此时如果任务线程较少同时IO时间相对较长那可能会出现所有线程都被挂起然后CPU资源都在闲着的情况所以此时我们需要适当地增加任务线程数量来提高吞吐量同时将CPU资源利用起来。
那为什么要说这个呢因为这是做程序优化的基本原则。通过前面课程的学习我们知道秒杀系统里有提供两种类型的服务一个是Web服务一个是RPC服务前者一般提供HTTP接口后者提供RPC接口。当然这两种服务我们一般都是通过Tomcat来启动发布但它们两者之间还是有些不同的。Web服务接受和处理请求走的是Tomcat那套线程模型而RPC服务则是根据选择的RPC框架的不同而有所变化所以这节课我们首先来了解一下Tomcat相关的知识。
## **Tomcat**
根据我们以往“知己知彼”的学习方式,先看下 **Tomcat在NIO线程模型下是怎么工作的**,简图如下所示:
![](https://static001.geekbang.org/resource/image/09/9c/09f73f9dabbyyabe8c78dcbc0abfc79c.jpg?wh=1378x1190)
简单来说就是:
* Tomcat启动时会创建一个Server端的Socket来监控我们配置的端口号
* 之后使用一个Acceptor来接受请求然后将请求放到一个Poller下的事件队列中
* Poller会轮询取出事件队列中的Channel并将其注册到自身下的Selector
* 而Selector也会不停轮询检查就绪的Channel然后将其交给Tomcat线程池
* Tomcat线程池会拿出一个线程来进行处理包括解析请求头、请求体等并将其封装进HttpServletRequest
* 最后执行自定义的Servlet业务逻辑执行完毕将响应结果返回。
所以从上图可以看出所谓的非阻塞其实就是相对以前的BIOTomcat不再是用一个线程将一个请求从头处理到尾而是分阶段来执行了。**好处显然易见,那就是提高了系统吞吐量。**
在了解了Tomcat基本原理之后我们再回过头来看下有什么地方是我们可以入手优化的。先看下Tomcat给我们开放了哪些可配置项
```plain
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
```
上面是Tomcat的Connector默认配置首先是端口号其次是protocol也就是上面说到的线程模型。Tomcat 8之后默认使用的都是NIO模式这个也可以通过我们服务的启动来查看
![图片](https://static001.geekbang.org/resource/image/0c/76/0cdfc74546cdaa36b5003acef59fac76.jpg?wh=852x127)
如上图所示就代表分别使用的是NIO模式和NIO2(AIO)模式当然还可以选择BIO模式以及APR模式。具体对比可参考下表
![图片](https://static001.geekbang.org/resource/image/89/42/8966cb8e33788bdded84f325d19ed942.png?wh=1260x508)
那说完线程模型的选择从上图中我们可以看到有个Tomcat线程池的概念它是通过哪些配置来控制的呢这里我们只摘几个重要的配置说一下详细信息如下表所示
![图片](https://static001.geekbang.org/resource/image/ac/db/ac0fddbddeb80f53565debe35de5e4db.png?wh=1254x794)
说完了Tomcat的配置这里再简单说说 Servlet 的部分知识。我们都知道Servlet从3.0开始加入了异步从3.1开始又新增了对IO非阻塞的支持那么这个和Tomcat线程模型中提到的异步非阻塞是一个概念吗这里我们就来捋一捋。
首先从上面的Tomcat线程模型图中我们可以清晰地看到NIO或AIO的概念是针对请求的接收来说而Servlet的异步非阻塞主要是针对请求的处理已经是到了Tomcat线程池那里了。
我们先来看下Servlet3.0前后的变化对比,如下图所示:
![](https://static001.geekbang.org/resource/image/76/f8/7614fe72f3cff040947c1e5yy9c119f8.jpg?wh=1824x1190)
概述一下就是Servlet3.0之前Tomcat线程在执行自定义Servlet时如果过程中发生了IO那么Tomcat线程只能在那等着结果这时线程是被挂起的如果被挂起的多了自然会影响对其他请求的处理。
所以在Servlet3.0之后支持在这种情况下将这种等待的任务交给一个自定义的业务线程池去做这样Tomcat线程可以很快地回到线程池处理其他请求。而业务线程在执行完业务逻辑以后通过调用指定的方法告诉Tomcat线程池接下来可以将业务线程执行的结果返回给调用方这样就实现了同步转异步的效果。
这样做的好处可能对提高系统的吞吐量有一定帮助但从JVM层面来说并没有减少工作量。业务线程在执行任务遇到IO时依然会阻塞现在只是由业务线程池代替了Tomcat线程池做了最耗时的那部分工作这样也许可以将原来的200个Tomcat线程拆分成20个Tomcat线程、180个业务线程来配合工作。这里原生Servlet以及SpringMVC对异步功能支持的测试代码你可以看GitHub代码库中的AsyncServlet类和TestAsyncController类相信你一看就明白了。
接着我们再聊一下Servlet3.1的非阻塞这块简单来说就是针对请求消息体的读取这是个IO过程以前是阻塞式读取现在支持非阻塞读取了。实现的大致原理就是在读取数据时新增一个监听事件在读取完成后由Tomcat线程执行回调。
在了解了Tomcat线程模型之后我们接着再说下RPC框架相关的知识。
## **RPC框架**
虽然RPC服务处理请求的过程会依据选用的RPC框架而有所不同但绝大部分RPC框架底层使用的都是Netty而Netty则是基于NIO开发的一种网络通信框架支持多种通信协议其服务端线程模型简略图如下所示
![](https://static001.geekbang.org/resource/image/0c/64/0ca1cd1d776fdb9c272f63b23c657d64.jpg?wh=1570x1118)
简单描述就是:
* 在服务启动时会创建一个Server端Socket监控我们配置的端口号
* 然后将NioServerSocketChannel注册到Boss Pool中的一个Selector上
* 再之后对Selector做轮询将就绪状态的连接封装成NioSocketChannel并注册到Worker Pool下的一个Selector上
* 而Worker Pool下的Selector也是同样轮询找出可读和可写状态的分别执行不同操作。
* 同时两个Pool中都有任务队列是不同场景下用户自定义或外部通过特定方式提交过去的任务都会被依次执行。
所以当我们的应用只提供RPC服务时我们可以将Tomcat的核心线程池配置也就是minSpareThreads配置成1因为用不到。而我们主要需要调整的是RPC框架的相关配置以Dubbo为例我们看下 <dubbo:protocol> 的主要配置项:
![图片](https://static001.geekbang.org/resource/image/23/41/2340f6d3b57a6310fda38622d93cab41.png?wh=1258x584)
在Netty中虽然只有一个Worker Pool但会做两种类型的事情一个是做IO处理包括请求消息的读写另一个是做业务逻辑处理。
而Dubbo将其分成了两个线程池也就是上面表格中的两个线程池配置。这两个线程池做的事情会根据Dispatcher的配置而有所不同。Netty是以事件驱动的形式来工作的像请求、响应、连接、断开、异常等操作都是事件而Dubbo中的Dispatcher就是将不同的事件类型分给不同的线程池来处理如果你感兴趣的话可以去看下Dubbo中WrappedChannelHandler类的5个实现类分别对应Dispatcher的5个选项。
最后一个配置项Queues这个默认值是0也就是不接受等待如果没有空闲线程处理任务将会直接返回。这个得和客户端配置配合使用如果这里配置了0那客户端最好配置重试。
讲完了两种服务的底层线程模型之后,我们再来介绍一下静态资源相关的优化。
## **静态资源**
我们知道在秒杀系统中,客户端与服务端既有动态数据交互,也有静态数据交互,而我们做系统优化有个基本的原则,即**前后端交互越少,数据越小,链路越短,数据离用户越近,响应就越快。**
![](https://static001.geekbang.org/resource/image/ce/76/ce46453e685ed16f58ae3f365a1cd376.jpg?wh=1456x325)
基于这个原则针对以上的静态数据我们就可以把静态文件CDN化资源前移到全国各地的CDN节点上用户秒杀的时候就近进行下载就不需要都挤到中心的Tomcat服务器上了。
静态资源前移,大家平常也会做,感受比较深的是不是就是客户端的页面加载更快了,但除了性能的提升外,其实它对系统稳定也至关重要。
试想一下当几百万人同时来拉取这些较大的资源文件时对中心的Tomcat服务器以及公司的网络带宽都是巨大的压力。京东当初在进行口罩抢购的时候这些静态资源就差点把公司的出口带宽打满影响交易大盘后来紧急扩容才避免了危机。
另外这些静态资源对Tomcat所在物理机的网卡挑战也很大京东在资源CDN化前物理机的万兆网卡曾被打满后来经过优化之后网卡的流量只有原来的10%了。
![](https://static001.geekbang.org/resource/image/a4/13/a48171f5a6a8fc510d18c19aa0f29c13.jpg?wh=1110x414)
在最后我们再说下Java运行的基础环境JVM相关的知识以及优化。
## **JVM**
这里如果你对一些基本概念比如JVM内存结构、GC原理、垃圾收集器类型等还不了解那建议你先了解一下会有事半功倍的效果。这块的内容比较多又比较重要但我们没办法一一展开只说最核心的优化点。
先看个JVM内存模型以及常用配置如下图所示
![](https://static001.geekbang.org/resource/image/2a/81/2afaeb5e409a78b968812f6363fb0881.jpg?wh=1668x1313)
其实针对JVM的优化我们最关心的无非就两个问题一个是垃圾回收器怎么选择另一个就是对选择的垃圾回收器如何做优化。这里我们分别讲一下。
对于垃圾回收器的选择是需要分业务场景的。如果我们提供的服务对响应时间敏感并且堆内存能够给到8G以上的那建议选择G1堆内存较小或JDK版本较低的可以选择CMS。相反如果对响应时间不敏感追求一定的吞吐量的则建议选择ParallelGC同时这也是JDK8的默认垃圾回收器。
**选择完垃圾回收器之后,接下来就针对不同的垃圾回收器,分别做不同的参数优化。**
首先是ParallelGC其主要配置参数如下
![图片](https://static001.geekbang.org/resource/image/50/10/50387966e16a2c74266f815f6872ea10.png?wh=1258x376)
然后是CMS在ParallelGC配置参数的基础上增加以下配置
![图片](https://static001.geekbang.org/resource/image/e7/ea/e7df72c932f39fb02b3566ab388f83ea.png?wh=1256x556)
再说下G1的优化配置在使用了G1的情况下就不要设置 -Xmn 和 XX:NewRatio了同样是在ParallelGC配置参数的基础上增加以下配置
![图片](https://static001.geekbang.org/resource/image/37/6b/37351b2260ffff7384ea30cb43bbb76b.png?wh=1252x196)
因为我们秒杀的业务场景更适合选择G1来做垃圾回收器那这里也给一个在8核16G容器下的JVM配置具体如下
```plain
-Xms8192m -Xmx8192m -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:ParallelGCThreads=8 -XX:ConcGCThreads=4 -XX:G1HeapRegionSize=8m
```
## **总结**
今天主要围绕着Java对与其息息相关的Tomcat、JVM、RPC框架以及静态资源的优化做了分析和讲解。
对于Tomcat的优化在秒杀的特定业务场景下针对线程模型的选择从理论和实际压测上看NIO2比NIO是有吐吞量的提升但不是很大如果为了省事选择默认的NIO即可。而APR的话因为我们静态资源都上到CDN了并且Web服务并不直接对外请求由Nginx转发过来也不要求是HTTPS方式所以这里也不考虑了和线程池相关的配置最好按照这节课中的建议做适当的调整。
同时我们也提到了Servlet在3.0和3.1版本提供的异步非阻塞功能由于秒杀的接口入参不涉及文件之类的较大消息体所以IO非阻塞可以不用。而异步功能这块其实可以有更好的选择那就是Vertx技术这也是我们在下节课中将会单独介绍的一种异步化编程思想技术。
而对于RPC框架我们主要介绍了基于NIO开发的一种网络通信框架Netty了解了Netty主要使用两个池子即使用Boss Pool和Worker Pool来实现Reactor模式。同时选择了一个具体的RPC框架Dubbo来做了详细的配置优化讲解。
在聊完了两种服务的底层线程模型与优化后我们介绍了静态资源的优化方案即将静态资源上到CDN以减轻对秒杀域名流量的压力同时可以依靠CDN的全国部署快速加载到对应的静态资源。
另外我们还提到了Java运行的环境JVM包括垃圾回收器的选择与优化即如果我们提供的服务对响应时间敏感并且堆内存能够给到8G以上的那就选择G1而堆内存较小或JDK版本较低的可以选择CMS。相反如果对响应时间不敏感追求一定的吞吐量的则建议选择ParallelGC。同时针对不同的垃圾回收器也给出了对应的优化配置。
当然以上所有的优化建议,在调整后都需要做实际业务场景下的压测,毕竟实践才是检测真理的唯一标准!
## **思考题**
这节课我们介绍了通过Tomcat发布的Web服务和RPC服务两者走的底层线程模型是不同的如果我们的服务既提供HTTP接口也提供RPC接口我们该通过何种方式才能将二者的相互影响降至最低呢
期待你的思考,也欢迎在留言区中分享讨论。我们下节课再见!