文章目录
秒杀笔记 —— 性能优化
主要介绍下服务端的性能优化技巧。
影响性能的因素
首先要认识什么是“性能”,服务设备不同,性能的定义也不尽相同,例如:CPU看主频、磁盘主要看IOPS(Input/Output Operations Per Second,即每秒进行读写操作的次数)。
本篇“性能”主要指服务端性能 —— QPS(Query Per Second,每秒请求数)来衡量,还有一个影响和QPS也息息相关,就是响应时间(Response Time, RT),可以理解为服务器处理响应的耗时。
正常情况下,响应时间(RT)越短,一秒钟处理的请求数(QPS)就越多,在单线程处理情况下看起来就是线性关系,即我们把每个请求的响应时间降到最低,性能就会提高。
想到响应时间总会有一个极限,不可能无限下降,所以又出现了通过多线程来处理请求。
基于这个理论“总QPS = (1000ms / 响应时间) x 线程数量”,这样性能就和两个因素相关了,一个是一次响应的服务端耗时,一个是处理请求的线程数。
那么,这两个因素会造成什么影响呢?
响应时间与QPS的关系
大部分WEB系统来说,响应时间一般是由CPU执行时间和线程等待时间(比如RPC、IO等待、Sleep、Wait等)组成,服务器在处理请求时,一部分CPU在做运算,另一部分在各种等待。
真正对性能有影响的是CPU的执行时间,不是减少服务器等待时间即可提升性能,CPU的执行是真正消耗了服务器资源。实际测试发现如果减少CPU一半的执行时间,就可以增加一倍的QPS。
所以,致力于减少CPU的执行时间才是提升性能的一个方向。
线程数对QPS的影响
但看 “总QPS”计算公式,会觉得线程越多QPS也就越高,这是错误的!线程不是越多越好,线程也会消耗资源,线程越多,系统线程切换的成本就会越来越高,而且每个线程都会耗费一定内存。
认识线程切换(待完善)
大部分多线程场景默认配置:“线程数 = 2 * CPU核数 + 1 ”
。
除了上面常规公式,还有最佳实践得出的公式:
线程数 = [( 线程等待时间 + 线程CPU时间) / 线程CPU时间] X CPU时间
除此之外就是通过性能测试压出最符合实际业务和公司设备的公式
提升服务器性能关键点
如何提升服务器性能 |
---|
1. 减少CPU执行时间 |
2. 设置一个合理的并发数 |
如何发现瓶颈
知道如何提升服务器性能,那么我们如何发现系统瓶颈呢?
对于服务器而言,出现瓶颈的位置会很多,例如CPU、内存、磁盘以及网络等都可以会导致瓶颈。
不同的系统对瓶颈关注点也不尽相同,例如缓存系统更关注内存,而存储型系统I/O更容易成为瓶颈。
秒杀场景下,瓶颈更多发生于CPU上。
CPU 诊断方式
诊断方式 | 描述 |
---|---|
工具 | JProfiler / Yourkit (可列出请求中每个函数CPU执行时间,可发现哪个函数消耗CPU时间最多) |
jstack |
定时打印调用栈,如果某些函数调用频繁或者耗时较多,就会出现在系统调用栈中。 |
秒杀场景下,服务端的瓶颈大多会出现在CPU上,除恶服务端,网络传输也有可能出现瓶颈(例如请求较多,同时页面又很大)
如何判断CPU是否为瓶颈?
当QPS达到极限时,服务器CPU使用率是否超过95%,如果没有,CPU还有提升空间,要么有锁限制,要么就是出现过多的本地I/O等待发生。
JProfiler 使用(待完善)
如何优化系统
减少编码
Java编码运行速度较慢(它的语言缺点),涉及字符串操作(如输入输出操作、I/O 操作)都比较耗CPU资源,无论是磁盘 I/O 还是网络 I/O ,都需要将字符转换为字节,而这个转换必须编码。
每个字符编码需要查表(编码比对表)
非常耗资源,所以,减少字符字节或者相反的转换、减少字符编码会提高效率。即减少编码可以提升性能。
如何减少编码
静态字符串提前编码转换为字节码,再通过OutputStream()函数写,就可以减少静态数据的编码转换。例如网页是可以直接进行流输出的(通过resp.getOutputStream()函数写数据)。
扩展阅读
《深入分析 Java Web 技术内幕》第17章 —— Velocity优化实践,通过把静态字符串提前编码成字节并缓存,然后直接输出字节内容,从而减少编码性能消耗,并提升性能。
减少序列化
减少Java中的序列化也能提升性能。又因为序列化和编码通常是同时发生,所以减少序列化即减少了编码。
序列化大多发生到场景在RPC中,因此减少RPC就是减少序列化,可以将多个关联性比较强的应用进行“合并部署”,从而减少不同应用之间的RPC,可以减少序列化的消耗。
合并部署:将两个原本在不同机器的不同应用合并部署到一台机器上,并且还要在同一个Tomcat容器中,且不能走本机的Socket。才能避免序列化发生。
Java极致优化
Java和通用Web服务器(如Nginx或Apache服务器)相比,处理大并发HTTP请求时较弱,所以大流量的Web系统做静态话改造,而Java只需处理少量数据的动态请求。可以采用以手段进行Java优化:
- 直接使用Servlet
避免使用传统的MVC框架,可以避免一大堆复杂且用处不大的处理逻辑。 - 直接输出流数据
使用 resp.getOutputStream() 而不是 resp.getWriter()函数 ,省掉部分不变字符数据的编码,从而提升性能;数据输出时推荐JSON而不是模版引擎(一般是解释执行)来输出页面
为什么要使用JSON传输数据?(待完善)
并发读优化
并发读优化第一时间想到就是放到Tair内缓存里面。集中式缓存为了保证命中率一般采用一致性Hash,所以同一个Key会落到同一台机器上。
Tair 缓存搭建及实战(待完善)
虽然 单台缓存机器能支撑30W/S的请求,但是远不足以应对“秒杀”场景,如何突破瓶颈:
采用应用层LoadCache,在秒杀系统的单机上缓存商品相关数据。
如何缓存数据?
将数据划分为动态数据和静态数据
- 静态数据
商品中“标题”、“描述”不变的数据,秒杀前全量推送到秒杀机器上,并一直缓存到秒杀结束
; - 动态数据
库存这类动态数据,采用“被动失效”的方式缓存一定时间(多少秒)
,失效后再去缓存拉取最新数据;
疑问:
库存这种频繁更新的数据,一旦数据不一致,会不会出现超卖?
答疑:
借鉴分层校验原则
,“读”场景允许有一定的脏数据,误判只会导致少量原本无库存的下单请求误认为有库存,等到 真正写数据
时在保证最终一致性,从高可用和一致性之间取平衡,来解决高并发数据问题。
总结
- 发现短板
数据传输本身有物理距离的限制(光速:C=30万千米/秒; 光纤:V=C/1.5=20万千米/秒)
- 网速限制(带宽,传输速率,可以根据当下的权威数据进行计算)
- 网络结构(交换机/网卡限制)
- TCP/IP
- 虚拟机(内存/CPU/IO等资源的限制)
- 以及应用本身瓶颈
- 减少数据
- 服务端处理数据时不可避免地存在字符和字节的相互转化(RPC)
- HTTP请求做Gzip压缩,网络传输耗时,这些都和数据大小有关
- 数据分级
- 保证首屏为先
- 重要信息为先
- 次要信息则异步加载
- 减少中间环节
- 减少字符到字节的转换
- 增加预处理(提前做字符到字节的转换)
- 去掉不需要操作
- 基线搭建
- 应用基线
- 性能基线(何时性能突然下降)
- 成本基线(用了多少台机器)
- 链路基线(系统发生什么变化)