You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

126 lines
13 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 05 | 影响性能的因素有哪些?又该如何提高系统的性能?
不知不觉,我们已经讲到第五篇了,不知道听到这里,你对于秒杀系统的构建有没有形成一些框架性的认识,这里我再带你简单回忆下前面的主线。
前面的四篇文章里,我介绍的内容多少都和优化有关:第一篇介绍了一些指导原则;第二篇和第三篇从动静分离和热点数据两个维度,介绍了如何有针对性地对数据进行区分和优化处理;第四篇介绍了在保证实现基本业务功能的前提下,尽量减少和过滤一些无效请求的思路。
这几篇文章既是在讲根据指导原则实现的具体案例,也是在讲如何实现能够让整个系统更“快”。我想说的是,优化本身有很多手段,也是一个复杂的系统工程。今天,我就来结合秒杀这一场景,重点给你介绍下服务端的一些优化技巧。
## 影响性能的因素
你想要提升性能,首先肯定要知道哪些因素对于系统性能的影响最大,然后再针对这些具体的因素想办法做优化,是不是这个逻辑?
那么哪些因素对性能有影响呢在回答这个问题之前我们先定义一下“性能”服务设备不同对性能的定义也是不一样的例如CPU主要看主频、磁盘主要看IOPSInput/Output Operations Per Second即每秒进行读写操作的次数
而今天我们讨论的主要是系统服务端性能一般用QPSQuery Per Second每秒请求数来衡量还有一个影响和QPS也息息相关那就是响应时间Response TimeRT它可以理解为服务器处理响应的耗时。
正常情况下响应时间RT越短一秒钟处理的请求数QPS自然也就会越多这在单线程处理的情况下看起来是线性的关系即我们只要把每个请求的响应时间降到最低那么性能就会最高。
但是你可能想到响应时间总有一个极限不可能无限下降所以又出现了另外一个维度即通过多线程来处理请求。这样理论上就变成了“总QPS =1000ms / 响应时间)× 线程数量”,这样性能就和两个因素相关了,一个是一次响应的服务端耗时,一个是处理请求的线程数。
接下来,我们一起看看这个两个因素到底会造成什么样的影响。
**首先我们先来看看响应时间和QPS有啥关系**。
对于大部分的Web系统而言响应时间一般都是由CPU执行时间和线程等待时间比如RPC、IO等待、Sleep、Wait等组成即服务器在处理一个请求时一部分是CPU本身在做运算还有一部分是在各种等待。
理解了服务器处理请求的逻辑估计你会说为什么我们不去减少这种等待时间。很遗憾根据我们实际的测试发现减少线程等待时间对提升性能的影响没有我们想象得那么大它并不是线性的提升关系这点在很多代理服务器Proxy上可以做验证。
如果代理服务器本身没有CPU消耗我们在每次给代理服务器代理的请求加个延时即增加响应时间但是这对代理服务器本身的吞吐量并没有多大的影响因为代理服务器本身的资源并没有被消耗可以通过增加代理服务器的处理线程数来弥补响应时间对代理服务器的QPS的影响。
其实真正对性能有影响的是CPU的执行时间。这也很好理解因为CPU的执行真正消耗了服务器的资源。经过实际的测试如果减少CPU一半的执行时间就可以增加一倍的QPS。
也就是说我们应该致力于减少CPU的执行时间。
**其次我们再来看看线程数对QPS的影响**。
单看“总QPS”的计算公式你会觉得线程数越多QPS也就会越高但这会一直正确吗显然不是线程数不是越多越好因为线程本身也消耗资源也受到其他因素的制约。例如线程越多系统的线程切换成本就会越高而且每个线程也都会耗费一定内存。
那么,设置什么样的线程数最合理呢?其实**很多多线程的场景都有一个默认配置,即“线程数 = 2 \* CPU核数 + 1”**。除去这个配置,还有一个根据最佳实践得出来的公式:
> 线程数 = \[(线程等待时间 + 线程CPU时间) / 线程CPU时间\] × CPU数量
当然,最好的办法是通过性能测试来发现最佳的线程数。
换句话说要提升性能我们就要减少CPU的执行时间另外就是要设置一个合理的并发线程数通过这两方面来显著提升服务器的性能。
现在你知道了如何来快速提升性能那接下来你估计会问我应该怎么发现系统哪里最消耗CPU资源呢
## 如何发现瓶颈
就服务器而言会出现瓶颈的地方有很多例如CPU、内存、磁盘以及网络等都可能会导致瓶颈。此外不同的系统对瓶颈的关注度也不一样例如对缓存系统而言制约它的是内存而对存储型系统来说I/O更容易是瓶颈。
**这个专栏中我们定位的场景是秒杀它的瓶颈更多地发生在CPU上**。
那么如何发现CPU的瓶颈呢其实有很多CPU诊断工具可以发现CPU的消耗最常用的就是JProfiler和Yourkit这两个工具它们可以列出整个请求中每个函数的CPU执行时间可以发现哪个函数消耗的CPU时间最多以便你有针对性地做优化。
当然还有一些办法也可以近似地统计CPU的耗时例如通过jstack定时地打印调用栈如果某些函数调用频繁或者耗时较多那么那些函数就会多次出现在系统调用栈里这样相当于采样的方式也能够发现耗时较多的函数。
虽说秒杀系统的瓶颈大部分在CPU但这并不表示其他方面就一定不出现瓶颈。例如如果海量请求涌过来你的页面又比较大那么网络就有可能出现瓶颈。
怎样简单地判断CPU是不是瓶颈呢一个办法就是看当QPS达到极限时你的服务器的CPU使用率是不是超过了95%如果没有超过那么表示CPU还有提升的空间要么是有锁限制要么是有过多的本地I/O等待发生。
现在你知道了优化哪些因素,又发现了瓶颈,那么接下来就要关注如何优化了。
## 如何优化系统
对Java系统来说可以优化的地方很多这里我重点说一下比较有效的几种手段供你参考它们是减少编码、减少序列化、Java极致优化、并发读优化。接下来我们分别来看一下。
1\. 减少编码
Java的编码运行比较慢这是Java的一大硬伤。在很多场景下只要涉及字符串的操作如输入输出操作、I/O操作都比较耗CPU资源不管它是磁盘I/O还是网络I/O因为都需要将字符转换成字节而这个转换必须编码。
每个字符的编码都需要查表,而这种查表的操作非常耗资源,所以减少字符到字节或者相反的转换、减少字符编码会非常有成效。减少编码就可以大大提升性能。
那么如何才能减少编码呢例如网页输出是可以直接进行流输出的即用resp.getOutputStream()函数写数据把一些静态的数据提前转化成字节等到真正往外写的时候再直接用OutputStream()函数写,就可以减少静态数据的编码转换。
我在《深入分析Java Web技术内幕》一书中介绍的“Velocity优化实践”一章的内容就是基于把静态的字符串提前编码成字节并缓存然后直接输出字节内容到页面从而大大减少编码的性能消耗的网页输出的性能比没有提前进行字符到字节转换时提升了30%左右。
2\. 减少序列化
序列化也是Java性能的一大天敌减少Java中的序列化操作也能大大提升性能。又因为序列化往往是和编码同时发生的所以减少序列化也就减少了编码。
序列化大部分是在RPC中发生的因此避免或者减少RPC就可以减少序列化当然当前的序列化协议也已经做了很多优化来提升性能。有一种新的方案就是可以将多个关联性比较强的应用进行“合并部署”而减少不同应用之间的RPC也可以减少序列化的消耗。
所谓“合并部署”就是把两个原本在不同机器上的不同应用合并部署到一台机器上当然不仅仅是部署在一台机器上还要在同一个Tomcat容器中且不能走本机的Socket这样才能避免序列化的产生。
另外针对秒杀场景我们还可以做得更极致一些接下来我们来看第3点Java极致优化。
3\. Java极致优化
Java和通用的Web服务器如Nginx或Apache服务器相比在处理大并发的HTTP请求时要弱一点所以一般我们都会对大流量的Web系统做静态化改造让大部分请求和数据直接在Nginx服务器或者Web代理服务器如Varnish、Squid等上直接返回这样可以减少数据的序列化与反序列化而Java层只需处理少量数据的动态请求。针对这些请求我们可以使用以下手段进行优化
* 直接使用Servlet处理请求。避免使用传统的MVC框架这样可以绕过一大堆复杂且用处不大的处理逻辑节省1ms时间具体取决于你对MVC框架的依赖程度
* 直接输出流数据。使用resp.getOutputStream()而不是resp.getWriter()函数可以省掉一些不变字符数据的编码从而提升性能数据输出时推荐使用JSON而不是模板引擎一般都是解释执行来输出页面。
4\. 并发读优化
也许有读者会觉得这个问题很容易解决无非就是放到Tair缓存里面。集中式缓存为了保证命中率一般都会采用一致性Hash所以同一个key会落到同一台机器上。虽然单台缓存机器也能支撑30w/s的请求但还是远不足以应对像“大秒”这种级别的热点商品。那么该如何彻底解决单点的瓶颈呢
答案是采用应用层的LocalCache即在秒杀系统的单机上缓存商品相关的数据。
那么又如何缓存Cache数据呢你需要划分成动态数据和静态数据分别进行处理
* 像商品中的“标题”和“描述”这些本身不变的数据,会在秒杀开始之前全量推送到秒杀机器上,并一直缓存到秒杀结束;
* 像库存这类动态数据,会采用“被动失效”的方式缓存一定时间(一般是数秒),失效后再去缓存拉取最新的数据。
你可能还会有疑问:像库存这种频繁更新的数据,一旦数据不一致,会不会导致超卖?
这就要用到前面介绍的读数据的分层校验原则了,读的场景可以允许一定的脏数据,因为这里的误判只会导致少量原本无库存的下单请求被误认为有库存,可以等到真正写数据时再保证最终的一致性,通过在数据的高可用性和一致性之间的平衡,来解决高并发的数据读取问题。
## 总结一下
性能优化的过程首先要从发现短板开始,除了我今天介绍的一些优化措施外,你还可以在减少数据、数据分级(动静分离),以及减少中间环节、增加预处理等这些环节上做优化。
首先是“发现短板”比如考虑以下因素的一些限制光速光速C = 30万千米/秒光纤V = C/1.5=20 万千米/秒即数据传输是有物理距离的限制的、网速2017年11月知名测速网站Ookla发布报告全国平均上网带宽达到61.24 Mbps千兆带宽下10KB数据的极限QPS 为1.25万QPS=1000Mbps/8/10KB、网络结构交换机/网卡的限制、TCP/IP、虚拟机内存/CPU/IO等资源的限制和应用本身的一些瓶颈等。
其次是减少数据。事实上有两个地方特别影响性能一是服务端在处理数据时不可避免地存在字符到字节的相互转化二是HTTP请求时要做Gzip压缩还有网络传输的耗时这些都和数据大小密切相关。
再次,就是数据分级,也就是要保证首屏为先、重要信息为先,次要信息则异步加载,以这种方式提升用户获取数据的体验。
最后就是要减少中间环节,减少字符到字节的转换,增加预处理(提前做字符到字节的转换)去掉不需要的操作。
此外要做好优化你还需要做好应用基线比如性能基线何时性能突然下降、成本基线去年双11用了多少台机器、链路基线我们的系统发生了哪些变化你可以通过这些基线持续关注系统的性能做到在代码上提升编码质量在业务上改掉不合理的调用在架构和调用链路上不断的改进。
最后,欢迎你在留言区和我交流,你也可以说说在实际工作中,关于性能提升还有哪些更好的思路或者方案,我们一起沟通探讨。