EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Linux / Redis แยกต่อ user บน shared hosting — และบั๊ก maxmemory ที่ทำเว็บค้าง
Linux

Redis แยกต่อ user บน shared hosting — และบั๊ก maxmemory ที่ทำเว็บค้าง

โดย Ton-Or ·9 ก.ค. 2026·อ่าน 10 นาที
Redis แยกต่อ user บน shared hosting — และบั๊ก maxmemory ที่ทำเว็บค้าง

ปล่อยให้ทุกเว็บใช้ Redis ก้อนเดียวกันบน shared hosting คือรอวันเจ็บ — แต่พอแยก instance ต่อ user แล้วคุมด้วย cgroup ก็มีกับดักของมันเอง ตัวเลขสองตัวที่ไม่คุยกันทำเว็บค้างทั้งเว็บแบบหาสาเหตุยาก

พอดีบนเซิร์ฟเวอร์ shared hosting ถ้าปล่อยให้ทุกเว็บใช้ Redis ก้อนเดียวกัน จะเจอปัญหาคลาสสิกครับ: เว็บหนึ่งเขียน key ท่วม เว็บอื่นโดน evict ตาม, ข้อมูล cache ปนกันข้าม tenant, ใครสักคน FLUSHALL ทีเดียวเจ็บทั้งเครื่อง ผมเลยออกแบบเป็น Redis หนึ่ง instance ต่อหนึ่ง user คุมทรัพยากรด้วย systemd cgroup — ใช้มาสักพักก็เจอบั๊กที่อยากเล่าให้ฟังในครึ่งหลังของบทความ

ชื่อ user และตัวเลข tier ในบทความเป็นตัวอย่าง โครงสร้างและ config คือของจริง

โครงสร้าง: systemd template unit ตัวเดียว รันได้ทุก user

# /etc/systemd/system/[email protected] (ตัดมาเฉพาะส่วนสำคัญ)
[Unit]
Description=Redis per-user (%i)

[Service]
User=%i
Group=%i
ExecStart=/usr/bin/redis-server /usr/local/directadmin/data/users/%i/redis.conf
# กำแพงทรัพยากรจริงอยู่ตรงนี้ — cgroup v2
MemoryHigh=150M
MemoryMax=200M
CPUQuota=50%

ฝั่ง redis.conf ของแต่ละ user — จุดสำคัญคือไม่เปิด TCP port เลย ใช้ UNIX socket ในบ้านตัวเอง:

port 0
unixsocket /home/webuser/.redis/redis.sock
unixsocketperm 700
maxmemory 120mb
maxmemory-policy allkeys-lru
# กันยิงคำสั่งอันตรายจากฝั่งแอป
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""

ได้ isolation ครบ: เว็บใครเว็บมัน, ไฟล์ socket permission 700 คนอื่นเข้าไม่ได้, จะซน CONFIG SET จากโค้ด PHP ก็ไม่ได้เพราะ rename ทิ้งแล้ว (จะแก้ config ต้องแก้ไฟล์ + restart เท่านั้น)

แล้ววันหนึ่งเว็บ WooCommerce ก็ค้าง

ลูกค้ารายหนึ่งแจ้งว่ากดโพสต์สินค้าใน WordPress แล้วขึ้น:

RedisException: socket error on read socket

เป็นพักๆ ไม่ตลอด restart Redis แล้วหายแป๊บเดียวกลับมาอีก — อาการ "เดี๋ยวเป็นเดี๋ยวหาย" แบบนี้ส่วนใหญ่คือปัญหาทรัพยากรครับ ไปดูของจริง:

systemctl status redis@webuser
# Memory: 149.8M (high: 150.0M max: 200.0M)  <- ชนเพดาน high ตลอดเวลา

grep maxmemory /usr/local/directadmin/data/users/webuser/redis.conf
# maxmemory 1gb    <- ต้นเหตุ

เห็นหรือยังครับ — Redis ถูกบอกว่ามี 1GB แต่ cgroup ให้จริงแค่ 150MB

กลไกที่ทำให้ค้าง: ตัวเลขสองตัวที่ไม่คุยกัน

maxmemory ของ Redis

ถึงเพดานแล้ว evict เอง

พอถึงเพดาน Redis จะ evict key ทิ้งเองตาม allkeys-lru — เร็ว ไม่มีอาการ เว็บแค่ cache miss

MemoryHigh ของ cgroup

เกินแล้วโดน kernel เหยียบเบรก

process ไม่ตาย แต่โดน throttle + บีบ reclaim หน้า memory ทุก syscall ช้าลงมหาศาล — ฝั่งแอปเห็นเป็น timeout

พอ conf โกหกว่ามี 1GB, Redis ก็ไม่ evict อะไรเลย (ยังไม่ถึงเพดานของมัน) ใช้ไปเรื่อยๆ จนชน MemoryHigh ของ cgroup → โดน kernel เหยียบเบรกทั้ง process → ฝั่ง PHP รอ response ไม่ไหว → "socket error on read socket" นั่นเอง

เส้นทางสู่อาการค้างconf 1GBcgroup 150M→Redis ไม่ evictยังไม่ถึงเพดานตัวเอง→ชน MemoryHighkernel throttle→PHP timeoutsocket error

กฎที่สรุปได้: maxmemory ต้องไม่เกิน 80% ของ cgroup high

เผื่อ 20% ให้ overhead ของ Redis เอง (จอง memory ตอน fork, buffer ฯลฯ):

MemoryHigh 150M  ->  maxmemory 120mb
MemoryHigh  50M  ->  maxmemory  40mb

แบบนี้ Redis จะ evict เองก่อนถึงมือ kernel เสมอ — เว็บได้ cache miss (ช้าลงนิด) แทนที่จะได้ socket timeout (พังทั้งหน้า)

MemoryHigh 150Mcgroupใช้ได้ปกติ ถึง MemoryHighthrottleตั้งถูกmaxmemory 120mbเผื่อ 20%Redis evict เองก่อนถึงมือ kernel เว็บได้แค่ cache missตั้งผิดmaxmemory 1gb (ยาวเกินกราฟ)ไม่ evict จนชน MemoryHigh แล้วโดน throttle ฝั่ง PHP เห็นเป็น timeoutMemoryMax 200M
สเกลเดียวกันทั้งสามแถว ตัวเลขในแอปต้องอยู่ต่ำกว่ากำแพงของ cgroup ไม่เกิน 80 เปอร์เซ็นต์ของ MemoryHigh ไม่งั้นแอปไม่มีวันรู้ตัวว่าถูกบีบ

กันเกิดซ้ำด้วย guard script

แก้เครื่องเดียวไม่พอครับ ต้องกันทั้งระบบ — cron ทุก 15 นาที ไล่ทุก instance:

#!/bin/bash
# redis-tier-guard: บังคับ conf maxmemory <= 80% cgroup + restart ตัวที่กำลังโดน throttle
for u in $(systemctl list-units 'redis@*' --state=running --no-legend | sed 's/redis@\([^.]*\).*/\1/'); do
  CONF=/usr/local/directadmin/data/users/$u/redis.conf
  HIGH=$(systemctl show redis@$u -p MemoryHigh --value)         # bytes
  [ "$HIGH" = "infinity" ] && continue
  CAP=$((HIGH * 80 / 100 / 1048576))                             # MB ที่อนุญาต
  CUR=$(grep -oP '^maxmemory \K[0-9]+' <<< "$(grep -i '^maxmemory ' $CONF | sed 's/mb//i')")
  if [ "${CUR:-0}" -gt "$CAP" ]; then
    sed -i "s/^maxmemory .*/maxmemory ${CAP}mb/i" $CONF
    systemctl restart redis@$u
    echo "[FIX] $u: maxmemory ${CUR}mb -> ${CAP}mb (cgroup high $((HIGH/1048576))M)"
    # ตรงนี้ต่อ alert เข้า Telegram/อีเมลของทีมได้
  fi
  # ตัวที่ใช้จริงเกิน 90% ของ high = กำลังจะโดน throttle -> restart ระบาย
  USED=$(systemctl show redis@$u -p MemoryCurrent --value)
  [ "$USED" != "" ] && [ "$USED" -gt $((HIGH * 90 / 100)) ] && systemctl restart redis@$u
done
# /etc/cron.d/redis-tier-guard
*/15 * * * * root /usr/local/sbin/redis-tier-guard.sh >> /var/log/redis-tier-guard.log 2>&1

จุดตัดสินใจที่ผมคิดนานคือ "ทำไมไม่ alert เฉยๆ แล้วให้คนมาแก้" — คำตอบคือเคสนี้ผลของการปล่อยไว้คือเว็บลูกค้าค้างกลางดึก ส่วนผลของ auto-fix คือ cache โดน restart หนึ่งรอบ (เสีย cache นิดหน่อย) เทียบกันแล้ว auto-fix ถูกกว่าเยอะ หลุด tier เมื่อไหร่ระบบต้องดึงตัวเองกลับมาใช้งานได้เอง

อย่าเอา queue ไปฝากใน Redis แบบนี้

allkeys-lru แปลว่าทุก key มีสิทธิ์โดน evict — cache หายไม่เป็นไร แต่ job ใน queue หายคืองานหาย ถ้าแอป (เช่น Laravel) จะใช้ queue ให้แยกไปใช้ database driver หรือ Redis instance ต่างหากที่ตั้ง noeviction เท่านั้น เจ็บมาแล้วครับ

ส่วนตัวผมถือว่ากฎ 80% นี้ใช้ได้กับทุกบริการที่อยู่ใต้ cgroup ไม่ใช่แค่ Redis — ตัวเลขข้างในแอปต้องต่ำกว่ากำแพงข้างนอกเสมอ ไม่งั้นแอปไม่มีวันรู้ตัวว่ากำลังโดนบีบ ใครจะเอาโครงนี้ไปใช้ ปรับตัวเลข tier ตามเครื่องได้ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย