跳到正文
DEV Community· EME GUG·· 3 小時前AI 評分59

小型 Linux VPS 的 RAM 用在哪裏?實用記憶體審計流程

Where Did My VPS RAM Go? A Practical Memory Audit for Small Linux Servers

AI 導讀

作者分享一套審計小型 Linux VPS 記憶體的實用流程,從查看 available、按 systemd unit 統計 PSS,到停用不需要的服務及為必要服務設定記憶體上限。文中的一部 2 GB VPS 按此流程處理約 30 分鐘後,可用記憶體由 180 MB 升至 1.3 GB,用戶毋須升級至 4 GB 方案。

正文

Tuần này trên Dev.to có một bài khá viral: tác giả xoá mail server khỏi VPS và lấy lại được 1.2 GB RAM. Đọc xong mình bật cười, vì chuyện này rất quen. Mình từng nhận một con VPS 2 GB của khách, trên đó có app Node.js nhỏ mà vẫn liên tục bị OOM killer xử. Kiểm tra mới thấy server đang chạy postfix, dovecot, clamav, một MySQL không ai dùng và snapd, tất cả là "quà tặng" từ một panel cài sẵn. Với dev Việt hay thuê VPS 5-10 USD/tháng ở Vultr, DigitalOcean hay các nhà cung cấp trong nước, mỗi 100 MB RAM đều đáng tiền. Bài này là quy trình mình dùng để audit RAM trên server Linux nhỏ: đo đúng chỗ, tìm ra kẻ ăn RAM, cắt bỏ hoặc giới hạn nó.

Bước 1: Đọc số liệu cho đúng trước khi hoảng

Sai lầm phổ biến nhất là chạy free -h, thấy cột free gần bằng 0 rồi kết luận server hết RAM. Linux dùng RAM trống làm page cache, nên cột cần nhìn là available.

# Tổng quan: nhìn cột 'available', không phải 'free'
free -h

# Top 10 process theo RSS
ps -eo pid,user,rss,comm --sort=-rss | head -n 11

# Xem RAM theo cgroup, tức là theo từng systemd service
sudo systemd-cgtop -m -n 1 --depth=3

# Liệt kê mọi service đang chạy, đếm xem có bao nhiêu cái bạn không nhận ra
systemctl list-units --type=service --state=running --no-pager

# Kernel đã từng kill process nào vì OOM chưa?
journalctl -k --since '7 days ago' | grep -i 'out of memory'

Có một bẫy nữa: RSS (Resident Set Size) đếm cả shared library. Nếu PHP-FPM có 20 worker, mỗi worker hiện RSS 60 MB, thì tổng thực tế không phải 1.2 GB, vì phần lớn memory là shared. Con số trung thực hơn là PSS (Proportional Set Size): phần shared được chia đều cho các process dùng chung nó.

flowchart TD
    A[Server chậm hoặc bị OOM] --> B[free -h: xem available]
    B --> C{available dưới 15%?}
    C -- Không --> D[Vấn đề nằm ở CPU hoặc IO, không phải RAM]
    C -- Có --> E[Gom PSS theo systemd unit]
    E --> F{Service có cần thiết?}
    F -- Không --> G[Disable và mask]
    F -- Có --> H[Tuning và đặt MemoryMax]
    G --> I[Đo lại sau 24h]
    H --> I

Bước 2: Gom PSS theo service bằng một script nhỏ

Tool smem làm được việc này nhưng không phải distro nào cũng có sẵn, và nó gom theo process chứ không theo service. Mình dùng script Python dưới đây, chỉ đọc /proc, không cần thư viện ngoài. Nó dùng smaps_rollup (có từ kernel 4.14) và cgroup v2 (mặc định trên Ubuntu 22.04+, Debian 12+).

#!/usr/bin/env python3
# pss_by_unit.py - gom PSS theo systemd unit. Chạy bằng: sudo python3 pss_by_unit.py
import os
from collections import defaultdict

totals = defaultdict(int)
for pid in filter(str.isdigit, os.listdir('/proc')):
    try:
        with open(f'/proc/{pid}/cgroup') as f:
            # cgroup v2: '0::/system.slice/nginx.service'
            cg = f.read().strip().split('::')[-1]
        unit = cg.rstrip('/').split('/')[-1] or 'kernel'
        with open(f'/proc/{pid}/smaps_rollup') as f:
            for line in f:
                if line.startswith('Pss:'):
                    totals[unit] += int(line.split()[1])  # đơn vị kB
                    break
    except (FileNotFoundError, PermissionError, ProcessLookupError):
        continue  # process đã chết giữa chừng, hoặc kernel thread

grand = sum(totals.values())
for unit, kb in sorted(totals.items(), key=lambda x: -x[1])[:15]:
    print(f'{kb / 1024:8.1f} MB  {kb * 100 / grand:5.1f}%  {unit}')
print(f'{grand / 1024:8.1f} MB  TOTAL')

Kết quả trên con VPS 2 GB mình kể ở trên trông như sau (rút gọn):

   412.3 MB   31.0%  mysql.service
   298.7 MB   22.5%  clamav-daemon.service
   201.4 MB   15.2%  myapp.service
   144.9 MB   10.9%  dovecot.service
    96.2 MB    7.2%  snapd.service
    61.0 MB    4.6%  postfix@-.service

App thật của khách chỉ chiếm 15%. Phần còn lại là những thứ không ai dùng. clamav-daemon là kẻ ăn RAM kinh điển: nó load toàn bộ virus signature database vào memory, thường mất 1 GB trở lên trên các bản mới.

Bước 3: Cắt bỏ những gì không cần

Nguyên tắc của mình: nếu không giải thích được vì sao service đó tồn tại, nó không nên chạy. Nhưng đừng apt purge ngay lập tức. Hãy disable và mask trước, theo dõi vài ngày, rồi mới gỡ hẳn.

# Dừng và chặn không cho tự khởi động lại
sudo systemctl disable --now clamav-daemon clamav-freshclam dovecot postfix
sudo systemctl mask clamav-daemon dovecot postfix

# Kiểm tra có gì còn listen trên port mail không (25, 110, 143, 587, 993)
sudo ss -tlnp | grep -E ':(25|110|143|587|993)\b'

# Server không dùng snap thì gỡ luôn (Ubuntu)
sudo snap list && sudo apt purge -y snapd

# Giới hạn dung lượng journald, mặc định nó có thể chiếm tới 10% disk
sudo mkdir -p /etc/systemd/journald.conf.d
printf '[Journal]\nSystemMaxUse=200M\nRuntimeMaxUse=50M\n' | sudo tee /etc/systemd/journald.conf.d/size.conf
sudo systemctl restart systemd-journald

Nếu app vẫn cần gửi email (reset password, thông báo), bạn không cần một mail server đầy đủ. Gửi qua SMTP relay của nhà cung cấp email transactional, hoặc gọi HTTP API của họ trực tiếp từ app. Tự vận hành mail server thời nay còn kéo theo SPF, DKIM, DMARC, chuyện IP bị blacklist… tốn công hơn nhiều so với vài trăm MB RAM.

Với MySQL: nếu chỉ có một app nhỏ, hãy cân nhắc chuyển sang SQLite. Còn nếu vẫn giữ MySQL 8.0, chỉ riêng việc đặt innodb_buffer_pool_size=128M và performance_schema=OFF trong my.cnf cũng đã tiết kiệm được 200-300 MB.

Bước 4: Giới hạn những service phải giữ lại

Với service bắt buộc phải chạy, mình dùng cgroup v2 thông qua systemd để nó không thể kéo cả server sập theo khi bị memory leak. MemoryHigh là ngưỡng mềm: kernel bắt đầu reclaim và throttle. MemoryMax là ngưỡng cứng: vượt quá thì OOM killer chỉ kill trong phạm vi service đó, sshd của bạn vẫn sống.

sudo systemctl edit myapp.service
# Thêm vào drop-in:
# [Service]
# MemoryHigh=300M
# MemoryMax=400M
# Restart=on-failure

sudo systemctl daemon-reload && sudo systemctl restart myapp
systemctl show myapp -p MemoryCurrent -p MemoryHigh -p MemoryMax

Với app Node.js, nhớ đặt thêm --max-old-space-size=256 để V8 chủ động GC trước khi đụng trần cgroup, thay vì bị kill đột ngột.

Cuối cùng, thay vì tạo swap file trên disk (chậm, nhất là trên VPS dùng network storage), mình dùng zram: swap được nén ngay trong RAM, tỉ lệ nén thường 2-3 lần với dữ liệu kiểu JSON hoặc text.

sudo apt install -y zram-tools
printf 'ALGO=zstd\nPERCENT=50\n' | sudo tee /etc/default/zramswap
sudo systemctl restart zramswap
swapon --show
graph LR
    K[Kernel và page cache] --- S[system.slice]
    S --> N[nginx: không giới hạn, rất nhẹ]
    S --> A[myapp: MemoryMax 400M]
    S --> D[postgres: MemoryMax 512M]
    S --> SSH[sshd: luôn sống sót]
    A -. vượt ngưỡng .-> O[OOM chỉ kill myapp]

Kết luận

Trend AI agent đang khiến nhiều người quên mất những kỹ năng nền tảng, nhưng đọc /proc và hiểu memory vẫn là thứ cứu bạn lúc 2 giờ sáng khi server sập. Checklist cho lần audit tới:

  1. Nhìn available, không nhìn free. Page cache không phải là RAM bị mất.
  2. Đo bằng PSS và gom theo systemd unit. RSS cộng dồn sẽ cho bạn con số sai.
  3. Service nào không giải thích được thì disable và mask. Mail server, clamav, snapd là những ứng viên hàng đầu.
  4. Đặt MemoryMax cho mọi app tự viết, để memory leak chỉ kill app đó chứ không kill cả server.
  5. Dùng zram thay cho swap file trên VPS nhỏ.
  6. Đo lại sau 24-48h và ghi lại baseline, để lần sau có cái mà so sánh.

Trên con VPS kể ở đầu bài, sau khoảng 30 phút làm theo quy trình này, available tăng từ 180 MB lên 1.3 GB, và khách không phải nâng lên gói 4 GB. Trước khi bấm nút upgrade, hãy audit trước. Tiết kiệm 5 USD mỗi tháng cho mỗi server, nhân lên vài chục server thì không còn là chuyện nhỏ.

來源:DEV Community · dev.to