内存爆了怎么办?从“蒙圈”到“搞定”的服务器内存优化实战

你的服务器是不是又“喘”了?

“老王,网站又打不开了,内存报警98%!”
“赶紧重启一下服务!”
“重启完好了,但俩小时后又满了…”

这样的场景你是不是也经历过?内存占用高,就像家里的下水道堵了,明明刚清过,没多久又满。今天咱们就聊聊怎么系统性地搞定内存占用高的问题,从“看见它高”到“知道为啥高”,再到“让它不高”,一步到位。

内存爆了怎么办?从“蒙圈”到“搞定”的服务器内存优化实战

先来认识一下内存占用高的几种“死法”

内存占用高不一定是坏事,有时候是程序在积极利用缓存(比如Redis、MySQL的InnoDB buffer pool),这叫“好高”。真正要警惕的是:

  • 内存泄漏:程序用完内存不释放,像拧不紧的水龙头,一点点把内存耗光。
  • 突发高峰:比如秒杀活动,瞬间创建大量对象,把内存撑爆。
  • 配置不当:比如Java堆内存设太大,或PHP进程数太多,把内存吃光。
  • 系统缓存无限增长:某些场景下Page Cache不释放,导致可用内存越来越少。

搞清楚是哪种情况,才能对症下药。

诊断篇:拿什么武器看清内存真相

1. free -h:一眼看出内存总量和剩余

$ free -h
              total        used        free      shared  buff/cache   available
Mem:           15G        8.2G        1.2G        123M        5.8G        6.5G
Swap:          2.0G        0.0K        2.0G

关键看 available 列,这才是真正可用的内存(包括可回收的缓存)。如果available很低,说明真不够了。

2. top / htop:看哪个进程最吃内存

top -o %MEM   <em># 按内存使用率排序</em>

找到PID后,可以用 pmap -x PID 看详细内存映射,或者用 smem 看更准确的内存统计(包括共享内存)。

3. 内存泄漏侦查:smem 和 /proc/PID/smaps

<em># 安装 smem(如果没有)</em>
apt install smem  <em># Ubuntu/Debian</em>
yum install smem  <em># CentOS</em>

<em># 查看进程实际物理内存(USS)</em>
smem -p -s rss -r | head -20

/proc/PID/smaps 里能看到每一块内存的详细属性,比如哪些是可回收的,哪些是匿名内存。

4. 系统层面的内存分布:slabtop

如果内存被内核slab消耗,用 slabtop 查看:

slabtop -o -s c   <em># 按缓存大小排序</em>

能帮你发现是不是内核模块或驱动有内存泄漏。

实战篇:不同场景的内存优化秘籍

场景一:Java应用内存飙升(OOM频繁)

症状:Java进程占用很高,GC日志频繁 Full GC,最后 OOM。

优化思路

  • 堆内存设置是否合理(-Xmx 和 -Xms)
  • 是否存在内存泄漏(对象一直引用不释放)
  • 是否使用了大对象(比如大List、HashMap)

代码示例:用MAT分析堆转储

<em># 生成堆转储文件</em>
jmap -dump:live,format=b,file=heap.hprof <PID>

<em># 用Eclipse MAT打开分析,重点关注:</em>
<em># - Leak Suspects Report(内存泄漏嫌疑)</em>
<em># - Dominator Tree(谁占了最大内存)</em>

JVM参数调优示例(针对Tomcat):

export JAVA_OPTS="-Xms4g -Xmx4g -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/opt/logs/heap.hprof"

场景二:PHP-FPM进程把内存吃光

症状:PHP-FPM 进程数很多,每个进程占用几百M,总内存爆了。

原因:PHP-FPM的 pm.max_children 太大,或者每个请求处理的数据量太大,内存没及时释放。

优化方案

<em>; /etc/php-fpm.d/www.conf</em>
pm = dynamic
pm.max_children = 50      # 最大子进程数,根据内存计算:总内存 / 单进程最大内存
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10

<em>; 单个进程最大请求数,防止内存碎片</em>
pm.max_requests = 500

同时,在代码中注意释放大变量:

<?php
<em>// 处理完大数据后,手动释放</em>
$bigArray = getBigData();
<em>// ... 使用 $bigArray</em>
unset($bigArray);  <em>// 显式释放,让PHP尽快回收内存</em>

场景三:Python应用内存泄漏(比如Flask、Django)

症状:Python进程内存缓慢增长,重启后正常,过一段时间又高。

排查工具:tracemalloc、objgraph、memory_profiler。

代码示例:用tracemalloc定位泄漏点

import tracemalloc
import linecache
import os

def display_top(snapshot, key_type='lineno', limit=10):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)
    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        print("#%s: %s:%s: %.1f KiB"
              % (index, frame.filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)
    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))

<em># 在应用入口启用</em>
tracemalloc.start()
<em># ... 运行一段时间后</em>
snapshot = tracemalloc.take_snapshot()
display_top(snapshot)

常见泄漏场景:

  • 全局变量或类属性存储了不断增加的数据(比如往list里无限append)
  • 循环引用导致垃圾回收不及时(可使用 gc.collect() 强制回收)

场景四:数据库缓存“吞”了太多内存(MySQL、Redis)

症状:MySQL占用内存很高,free -h 显示buff/cache很大,但可用内存很少。

MySQL优化:调整InnoDB缓冲池大小

<em># /etc/mysql/my.cnf</em>
[mysqld]
innodb_buffer_pool_size = 4G   # 设为物理内存的70%左右,留点给OS
innodb_log_file_size = 1G
innodb_flush_method = O_DIRECT  # 减少双缓冲,降低内存压力

Redis优化:设置最大内存和淘汰策略

<em># redis.conf</em>
maxmemory 4gb
maxmemory-policy allkeys-lru   <em># 当内存满时,淘汰最近最少使用的key</em>

同时监控Redis内存碎片:INFO memory,如果碎片率高,考虑重启或调整 activedefrag

场景五:Linux Page Cache 不释放

现象free 看到buff/cache很大,但 available 却很小,应用申请内存时卡顿。

原因:内核为了性能,把大量文件缓存留在内存里,但应用需要内存时,内核会回收,只是回收可能触发IO慢。

解决方法:手动释放缓存(临时)

<em># 释放页缓存、目录项、inode</em>
sync; echo 1 > /proc/sys/vm/drop_caches   <em># 只释放pagecache</em>
sync; echo 2 > /proc/sys/vm/drop_caches   <em># 释放dentries和inodes</em>
sync; echo 3 > /proc/sys/vm/drop_caches   <em># 释放全部</em>

更优雅的做法:调整 /proc/sys/vm/vfs_cache_pressure,默认100,可以调高到200让内核更积极回收缓存。

echo 200 > /proc/sys/vm/vfs_cache_pressure

预防篇:别再等内存报警了

1. 设置合理的内存报警阈值

比如可用内存低于20%就报警,而不是等到OOM。用Prometheus + node_exporter监控内存使用率。

2. 在代码里埋点

对于关键业务,记录每次请求的内存增量,比如PHP可以在入口和出口打印memory_get_usage()。

3. 定期重启“不干净”的服务

对于有内存泄漏但暂时无法修复的应用,可以用crontab每天凌晨重启一次,比如:

0 4 * * * systemctl restart php-fpm

4. 使用容器限制内存

如果用Docker,可以给每个容器设置内存上限,避免一个容器把整个物理机拖垮。

docker run -m 512m --memory-swap 512m myapp

总结:内存优化三板斧

第一板斧:看清本质
用free、top、smem、slabtop这些工具,分清内存是进程占了、内核占了还是缓存占了。available才是真的可用的。

第二板斧:对症下药

  • Java调堆、查泄漏;
  • PHP限制进程数、及时释放变量;
  • Python用tracemalloc定位泄漏点;
  • 数据库缓冲池别超过物理内存70%;
  • Page Cache高了就调整vfs_cache_pressure。

第三板斧:日常防守
设置报警、定期重启、容器化限制,把问题扼杀在摇篮里。

记住,内存优化不是一次性工作,而是一个持续观察、持续调整的过程。希望你的服务器从此告别“内存告急”,稳稳当当跑业务!

最后,动手试试吧:
先跑一遍 free -h 看看你服务器的available还剩多少,再 top -o %MEM 看看哪个进程最贪吃。然后挑一个场景,按我们上面说的去优化一下。搞定了别忘了回来点个赞!

© 版权声明
THE END
喜欢就支持一下吧
点赞13 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容