DEV Community

EME GUG
EME GUG

Posted on

Where Did My VPS RAM Go? A Practical Memory Audit for Small Linux Servers with PSS, cgroups v2 and systemd

Tuần này có một bài trên Dev.to được chú ý: tác giả xóa mail server khỏi VPS và lấy lại được 1.2 GB RAM. Đọc xong mình nhớ lại con VPS 2 GB của mình. Mình cài nó "cho đủ thứ" rồi một hôm thấy OOM killer giết mất Postgres lúc 3 giờ sáng. Rất nhiều anh em Việt Nam chạy side project trên VPS 1-4 GB (Vultr, DigitalOcean, Hetzner, hoặc VPS trong nước giá rẻ), và kịch bản thường gặp là mua thêm RAM chứ không đi tìm xem RAM đang bị dùng vào đâu. Bài này là quy trình mình hay dùng để kiểm tra bộ nhớ một server Linux nhỏ: đọc số liệu cho đúng, tìm ra service tốn RAM nhất, cắt bớt những gì không cần và đặt giới hạn để một service không kéo sập cả máy. Mình test trên Ubuntu 24.04 và Debian 12, cả hai đều dùng cgroups v2 và systemd 252+.

1. Đọc free -h cho đúng trước đã

Sai lầm phổ biến nhất là nhìn cột free rồi hoảng:

$ free -h
               total        used        free      shared  buff/cache   available
Mem:           1.9Gi       1.1Gi        92Mi        18Mi       780Mi       680Mi
Swap:             0B          0B          0B
Enter fullscreen mode Exit fullscreen mode

free chỉ còn 92 MiB nhưng như vậy không có nghĩa là sắp hết RAM. Kernel dùng RAM trống làm page cache (buff/cache) và sẽ trả lại khi có process cần. Con số cần nhìn là available. Đây là ước lượng của kernel về lượng RAM có thể cấp cho process mới mà không phải swap.

Một vài quy tắc mình dùng:

  • available > 30% total: ổn, chưa cần làm gì.
  • available < 15% và có swap đang tăng dần (vmstat 5, xem cột si/so): bắt đầu điều tra.
  • dmesg -T | grep -i 'killed process' có kết quả: đã có sự cố thật, phải xử lý ngay.

Quy trình điều tra tổng quát như sau:

flowchart TD
    A[free -h: available thấp?] -->|Không| Z[Không cần làm gì]
    A -->|Có| B[Xem RAM theo service: cgroup / PSS]
    B --> C{Service có cần thiết?}
    C -->|Không| D[systemctl disable --now]
    C -->|Có| E[Tune config: worker, buffer, pool]
    E --> F[Đặt MemoryHigh / MemoryMax]
    D --> G[Thêm zram làm lớp đệm]
    F --> G

2. Tìm thủ phạm: RSS nói dối, PSS và cgroup thì không

top hay htop hiển thị RSS (Resident Set Size). Vấn đề là RSS tính trùng shared memory. Ví dụ 10 worker PHP-FPM cùng dùng chung 30 MB thư viện thì mỗi worker vẫn báo RSS 30 MB, tổng lên 300 MB dù thực tế chỉ tốn 30 MB. Cộng RSS lại thì lần nào cũng ra con số lớn hơn RAM thật.

Có hai cách đo chính xác hơn.

Cách 1: Đọc thẳng từ cgroup. Với systemd, mỗi service nằm trong một cgroup riêng, và kernel đếm bộ nhớ của từng cgroup trong file memory.current:

#!/usr/bin/env bash
# mem-by-service.sh: top 15 service tốn RAM nhất (cgroups v2)
for d in /sys/fs/cgroup/system.slice/*.service; do
  [ -f "$d/memory.current" ] || continue
  bytes=$(cat "$d/memory.current")
  printf '%8d MiB  %s\n' $(( bytes / 1048576 )) "$(basename "$d")"
done | sort -rn | head -15
Enter fullscreen mode Exit fullscreen mode

Muốn xem realtime thì dùng systemd-cgtop -m (sắp xếp theo memory). Lưu ý memory.current có tính cả page cache của service đó, nên một database vừa đọc nhiều file sẽ trông "béo" hơn thực tế. Muốn tách riêng thì xem anon (heap thật) và file (cache) trong memory.stat.

Cách 2: Tính PSS theo service. PSS (Proportional Set Size) chia đều phần shared memory cho các process đang dùng chung, nên cộng lại sẽ khớp với thực tế. smem (apt install smem) tính được PSS nhưng chỉ group theo process hoặc user. Mình viết một script nhỏ để group theo systemd unit:

#!/usr/bin/env python3
# pss_by_unit.py: chạy bằng sudo, cần kernel >= 4.14 (smaps_rollup)
import os
from collections import Counter

usage = Counter()
for pid in filter(str.isdigit, os.listdir("/proc")):
    try:
        with open(f"/proc/{pid}/cgroup") as f:
            # cgroups v2: "0::/system.slice/nginx.service"
            cg = f.read().strip().split("::")[-1]
        with open(f"/proc/{pid}/smaps_rollup") as f:
            pss_kb = next(int(l.split()[1]) for l in f if l.startswith("Pss:"))
    except (FileNotFoundError, PermissionError, ProcessLookupError, StopIteration):
        continue  # process đã chết hoặc là kernel thread
    unit = cg.rstrip("/").split("/")[-1] or "/"
    usage[unit] += pss_kb

total = sum(usage.values())
for unit, kb in usage.most_common(15):
    print(f"{kb / 1024:8.1f} MiB  {kb * 100 / total:5.1f}%  {unit}")
print(f"{total / 1024:8.1f} MiB  total PSS")
Enter fullscreen mode Exit fullscreen mode

Trên con VPS của mình, script in ra kết quả khá bất ngờ: snapd + snap.lxd ~110 MiB, multipathd ~25 MiB, một container Redis "test thử" từ năm ngoái ~180 MiB, và fwupd ngồi đó không làm gì.

3. Cắt giảm: những thứ thường thừa trên VPS

Để hiểu vì sao cgroup giúp "truy vết" được, hãy xem cấu trúc systemd tổ chức process:

graph TD
    R["/sys/fs/cgroup (root)"] --> S[system.slice]
    R --> U[user.slice]
    S --> N[nginx.service]
    S --> P[postgresql.service]
    S --> D[docker.service]
    S --> X[snapd.service]
    D -.-> C["docker-xxx.scope (container)"]

Mỗi node là một cgroup, mỗi cgroup có memory.current, memory.high, memory.max. Process nào cũng thuộc đúng một node, nên không có RAM nào "mồ côi".

Danh sách mình thường dọn trên VPS Ubuntu image mặc định:

# Không dùng snap? Gỡ hẳn, tiết kiệm 80-150 MiB
sudo snap list && sudo apt purge -y snapd

# Không có SAN/iSCSI thì multipathd vô dụng
sudo systemctl disable --now multipathd.service multipathd.socket

# Không có modem, không cần firmware update trên VM
sudo systemctl disable --now ModemManager.service fwupd.service

# Mail server cài "cho có"? Nếu chỉ cần gửi mail, dùng SMTP relay bên ngoài
sudo systemctl disable --now postfix.service

# Container cũ
docker ps -a --format '{{.Names}}\t{{.Status}}'
docker stats --no-stream
Enter fullscreen mode Exit fullscreen mode

Với những service cần giữ, cấu hình mặc định thường được viết cho máy lớn:

  • PostgreSQL 16: shared_buffers khoảng 25% RAM là hợp lý, nhưng max_connections = 100 với work_mem lớn mới là nguồn gây phình RAM. Trên VPS 2 GB, mình để max_connections = 30 và đặt PgBouncer phía trước.
  • MySQL 8.0: performance_schema = OFF giúp tiết kiệm 100-200 MiB, innodb_buffer_pool_size nên đặt rõ ràng thay vì để mặc định.
  • PHP-FPM: đổi pm = dynamic sang pm = ondemand với pm.max_children tính bằng (RAM dành cho PHP) / (PSS trung bình mỗi worker). Con số PSS lấy từ script ở trên chứ đừng lấy RSS.
  • Node.js: đặt --max-old-space-size=256 nếu bạn biết app không cần nhiều hơn, vì V8 khá "tham" khi còn RAM trống.

4. Đặt rào chắn: MemoryHigh, MemoryMax và zram

Dọn xong vẫn cần giới hạn, nếu không chỉ một memory leak cũng đủ khiến OOM killer chọn sai nạn nhân. systemd cho phép đặt giới hạn theo từng service mà không cần Docker:

sudo systemctl edit myapp.service
Enter fullscreen mode Exit fullscreen mode
[Service]
# Vượt MemoryHigh: kernel throttle + reclaim mạnh, service chậm lại nhưng không chết
MemoryHigh=400M
# Vượt MemoryMax: OOM chỉ trong cgroup này, các service khác an toàn
MemoryMax=512M
MemorySwapMax=128M
Enter fullscreen mode Exit fullscreen mode

Sau đó systemctl daemon-reload && systemctl restart myapp, rồi kiểm tra bằng systemctl show myapp -p MemoryHigh -p MemoryMax. Mình thích đặt MemoryHigh thấp hơn MemoryMax khoảng 20%, vì khi service bắt đầu chậm, monitoring sẽ báo trước lúc nó bị kill.

Cuối cùng là zram: tạo swap nén ngay trong RAM. Với tỷ lệ nén zstd thường khoảng 3:1, đây là cách rẻ nhất để có thêm "đệm" trên VPS không có (hoặc có disk swap rất chậm):

sudo apt install -y systemd-zram-generator
sudo tee /etc/systemd/zram-generator.conf <<'EOF'
[zram0]
zram-size = ram / 2
compression-algorithm = zstd
EOF
sudo systemctl daemon-reload && sudo systemctl start systemd-zram-setup@zram0.service
zramctl   # kiểm tra DATA vs COMPR
Enter fullscreen mode Exit fullscreen mode

Nếu muốn kill sớm, kiểm soát được thay vì để kernel OOM killer xử lý lúc đã quá muộn, hãy bật systemd-oomd (có sẵn trên Ubuntu 22.04+) hoặc cài earlyoom.

Kết luận

Trước khi bấm nâng cấp gói VPS, hãy dành 30 phút làm theo checklist này:

  1. Nhìn available, không nhìn free. Page cache không phải là RAM bị mất.
  2. Đo theo service, không theo process. Dùng systemd-cgtop -m hoặc memory.current để có cái nhìn nhanh, script PSS ở trên để có số chính xác. Đừng cộng RSS.
  3. Gỡ những gì không dùng: snapd, multipathd, ModemManager, mail server và container cũ thường cộng lại được vài trăm MiB.
  4. Tune service cần giữ dựa trên PSS thực tế: connection pool, worker count, buffer.
  5. Đặt MemoryHigh/MemoryMax cho service dễ leak, và bật zram làm lớp đệm.

Trên con VPS 2 GB của mình, cả quy trình giúp lấy lại khoảng 600 MiB, và từ đó đến nay OOM killer chưa ghé lại lần nào. Cứ vài tháng nên chạy lại script một lần, vì server nào cũng dần tích tụ những thứ mình cài "tạm" rồi quên mất.

Top comments (0)