CPU过载内存溢出分析

CPU过载、内存溢出问题一般出现在线上项目中,因为开发环境用户量和数据量都很小,即使你的代码有问题也不会爆发出来。线上出现这样的问题是比较严重的,需要认真对待,妥善解决。

问题原因

造成这样问题有两个大类型的原因: 一,性能问题,高并发高访问情况下线程太多,内存不够用; 二,代码问题,如代码中有死循环、一次查询数据量太大、存在太多对象引用内存无法回收,数据库死锁等。

如果项目一直稳定运行,升级版本后出现问题,很大可能是代码问题,可以马上回滚到上一版本,然后排查代码。

使用top, free, uptime等命令查看CPU、内存使用情况。 cpu、内存使用率阶段性较高时,是由于某段时间并发量比较高,导致服务器性能出现瓶颈,那么说明你可能需要升级服务器或者横向添加服务器做集群负载了。  当CPU、内存使用长时间居高不下,那么很可能是代码出现了问题。需要进一步排查了。

解决方案

一.       提升性能

服务器性能瓶颈,可升级服务器CPU核心数,加大CPU内存,或采用集群方案,使用nginx做负载均衡,keepalived做nginx的双机热备。在此不作详细讨论,感兴趣的同学可参考我的另一篇博客:https://blog.csdn.net/chenguohong88/article/details/52955566

以上是从硬件上考虑,也可以从web项目本身进行性能上的优化。比如,数据库加索引、sql语句的优化。  还可以采用缓存来提高性能, 常见的缓存方案:http请求的缓存机制cache-control、varnish缓存、Redis缓存。 数据库索引是非常重要的,性能瓶颈一般在IO,没有索引的IO速度是惨不忍睹的,当然索引多了也会导致插入速度慢,因此索引的创建也是一门学问,在设计表时需要好好考虑的。

二.       排查代码中的问题

java项目:

 

1.      CPU分析:

java项目可以使用jvm自带的堆栈跟踪工具jstack,分析消耗CPU比较多的进程。参考:https://www.cnblogs.com/chenpi/p/5377445.html

2.      Java内存溢出的原因: 
(1)内存中加载的数据量过于庞大,,如一次性从数据库中取出过多的数据; 
(2)集合类中有对对象的引用,使用完之后未清空,使得JVM不能回收; 
(3)代码中存在死循环或者循环产生过多重复的对象尸体; 
(4)使用的第三方软件中的Bug; 
(5)JVM的启动参数设定的过小。

3.      解决Java内存溢出的方案: 
1. 修改JVM的启动参数,直接增加内存(使用-Xms, -Xmx)。-Xms表示初始堆大小,-Xmx表示最大堆大小,例如设置-Xmx3550m -Xms3550m来增加内存;

2. 检查错误日志,看OutOfMemory错误信息,根据具体错误进行分析,参考:http://outofmemory.cn/c/java-outOfMemoryError

3. 使用工具对内存堆栈进行分析,如JvisualVMJConsolejvmstat

node.js项目:

在进行内存分析前先了解一下node.js引擎v8的内存机制和垃圾回收机制:

1.      V8的内存机制

Node中并不像其他后端语言中,对内存的使用没有多少限制。在Node中使用内存,只能使用到系统的一部分内存,64位系统下约为1.4GB,32位系统下约为0.7GB。这归咎于Node使用了本来运行在浏览器的V8引擎。

V8引擎的设计之初只是运行在浏览器中,而在浏览器的一般应用场景下使用起来绰绰有余,足以胜任前端页面中的所有需求。

虽然服务端操作大内存也不是常见的需求,但是万一有这样的需求,还是可以解除限制的。在启动node程序的时候,可以传递两个参数来调整内存限制的大小。

node --max-nex-space-size=1024 app.js // 单位为KB
node --max-old-space-size=2000 app.js // 单位为MB

这两条命令分别对应Node内存堆中的「新生代」和「老生代」。

在Node中,使用Buffer可以读取超过V8内存限制的大文件。原因是Buffer对象不同于其他对象,它不经过V8的内存分配机制。这在于Node并不同于浏览器的应用场景。在浏览器中,JavaScript直接处理字符串即可满足绝大多数的业务需求,而Node则需要处理网络流和文件I/O流,操作字符串远远不能满足传输的性能需求。

一切JavaScript对象都用堆来存储。当我们在代码中声明变量并赋值时,所使用对象的内存就分配在堆中。如果已申请的对空闲内存不够分配新的对象,讲继续申请堆内存,直到堆的大小超过V8的限制为止。

 2.      V8的垃圾回收机制

V8的垃圾回收策略主要基于「分代式垃圾回收机制」,基于这个机制,V8把内存分为「新生代(New Space)」和「老生代 (Old Space)」。

新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。

前面提及到的 --max-old-space-size 命令就是设置老生代内存空间的最大值,而 --max-new-space-size 命令则可以设置新生代内存空间的大小。

垃圾回收算法有很多种,但是并没有一种是胜任所有的场景,在实际的应用中,需要根据对象的生存周期长短不一,而使用不同的算法,已达到最好的效果。在V8中,按对象的存活时间将内存的垃圾回收进行不同的分代,然后分别对不同的内存施以更高效的算法。

在新生代中,主要通过 Scavenge 算法进行垃圾回收。

在Scavenge算法中,它将堆内存一分为二,每一部分空间称为semispace。在这两个semispace空间中,只有一个处于使用中,另外一个处于闲置状态。处于使用状态的semispace称为From空间,处于闲置状态的semispace称为To空间。当我们分配对象时,先是从From空间中分配。当开始进行垃圾回收时,会检查From空间中存活的对象,这些存活的对象会被复制到To空间中,而非存活的对象占用的空间会被释放。完成复制后,From空间和To空间角色互换。简而言之,在垃圾回收的过程中,就是通过将存活对象在两个semispace空间之间进行复制。

在新生代存活周期长的对象会被移动到老生代中,主要符合两个条件中的一个:

1. 对象是否经历过Scavenge回收。

对象从From空间中复制到To空间时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge回收,如果已经经历过了,则将该对象从From空间中复制到老生代空间中。

2. To空间的内存占比超过25%限制。

当对象从From空间复制到To空间时,如果To空间已经使用超过25%,则这个对象直接复制到老生代中。这么做的原因在于这次Scavenge回收完成后,这个To空间会变成From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。

对于老生代的对象,由于存活对象占比较大比重,使用Scavenge算法显然不科学。一来复制的对象太多会导致效率问题,二来需要浪费多一倍的空间。所以,V8在老生代中主要采用「Mark-Sweep」算法与「Mark-Compact」算法相结合的方式进行垃圾回收。

Mark-Sweep是标记清除的意思,分为标记和清除两个阶段。在标记阶段遍历堆中的所有对象,并标记存活的对象,在随后的清除阶段中,只清除标记之外的对象。

但是Mark-Sweep有一个很严重的问题,就是进行一次标记清除回收之后,内存会变得碎片化。如果需要分配一个大对象,这时候就无法完成分配了。这时候就该Mark-Compact出场了。

Mark-Compact是标记整理的意思,是在Mark-Sweep基础上演变而来。Mark-Compact在标记存活对象之后,在整理过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。

鉴于Node单线程的特性,V8每次垃圾回收的时候,都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复应用逻辑,被称为「全停顿」。在分代垃圾回收中,一次小垃圾回收只收集新生代,且存活对象也相对较少,即使全停顿也没有多大的影响。但是在老生代中,存活对象较多,垃圾回收的标记、清理、整理都需要长时间的停顿,这样会严重影响到系统的性能。所以「增量标记 (Incrememtal Marking)」被提出来。它从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记,拆分为许多小「步进」,每做完一「步进」就让JavaScript应用逻辑执行一小会,垃圾回收与应用逻辑这样交替执行直到标记阶段完成。

3.      使用工具分析CPU/内存:

推荐几款node.js分析工具:

  1. https://github.com/chenguohong/node-monitor

  2.  Easy-Monitor:  http://easy-monitor.cn/document/

  3.  alinode:  http://alinode.aliyun.com/

三.       数据库问题

1.   MySQL
MySQL占用CPU过高,可以做如下考虑:
1)使用show processlist语句,查找负荷最重的SQL语句,优化该SQL,比如适当建立某字段的索引;
2)打开慢查询日志,将那些执行时间过长且占用资源过多的SQL拿来进行explain分析,导致CPU过高,多数是GroupBy、OrderBy排序问题所导致,然后慢慢进行优化改进。
3)创建、优化索引;
4)查看是否有数据库锁、事务锁;
5)调整一些MySQL Server参数,比如key_buffer_size、table_cache、innodb_buffer_pool_size、innodb_log_file_size、tmp_table_size、max_heap_table_size等;
6)如果数据量过大,可以考虑使用MySQL集群或者搭建高可用环境。
7)经常需要查询的数据放到缓存中,如使用Redis;
8)使用show processlist查看mysql连接数,看看是否超过了mysql设置的连接数。

 

2.   mongodb

1 mongodb 3.2+之后,默认的存储引擎为“wiredTiger”,大量优化了存储性能,建议升级到3.2+版本

2) 开启profiling 功能记录慢查询操作,然后进行优化;

3)  使用explain功能分析查询是否使用索引,如果没有添加索引;

4)使用mongostat监控mongodb状态;

5)使用db.serverStatus()查看服务器状态,db.serverStatus().locks查看锁信息;

6)数据量大时采用分片机制存储。

7MongoDB使用的是内存映射存储引擎,它会把磁盘IO操作转换成内存操作,如果是读操作,内存中的数据起到缓存的作用,如果是写操作,内存还可以把随机的写操作转换成顺序的写操作,总之可以大幅度提升性能。MongoDB并不干涉内存管理工作,而是把这些工作留给操作系统的虚拟缓存管理器去处理,这样的好处是简化了MongoDB的工作,但坏处是你没有方法很方便的控制MongoDB占多大内存,事实上MongoDB会占用所有能用的内存,所以最好不要把别的服务和MongoDB放一起,并且mongodb服务器的内存应足够的大。

可以使用db.serverStatus().mem命令或mongostat查看内存使用情况;

 

猜你喜欢

转载自blog.csdn.net/chenguohong88/article/details/79827763