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
กลไกที่ทำให้ค้าง: ตัวเลขสองตัวที่ไม่คุยกัน
ถึงเพดานแล้ว evict เอง
พอถึงเพดาน Redis จะ evict key ทิ้งเองตาม allkeys-lru — เร็ว ไม่มีอาการ เว็บแค่ cache miss
เกินแล้วโดน kernel เหยียบเบรก
process ไม่ตาย แต่โดน throttle + บีบ reclaim หน้า memory ทุก syscall ช้าลงมหาศาล — ฝั่งแอปเห็นเป็น timeout
พอ conf โกหกว่ามี 1GB, Redis ก็ไม่ evict อะไรเลย (ยังไม่ถึงเพดานของมัน) ใช้ไปเรื่อยๆ จนชน MemoryHigh ของ cgroup → โดน kernel เหยียบเบรกทั้ง process → ฝั่ง PHP รอ response ไม่ไหว → "socket error on read socket" นั่นเอง
กฎที่สรุปได้: 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 (พังทั้งหน้า)
กันเกิดซ้ำด้วย 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 เมื่อไหร่ระบบต้องดึงตัวเองกลับมาใช้งานได้เอง
allkeys-lru แปลว่าทุก key มีสิทธิ์โดน evict — cache หายไม่เป็นไร แต่ job ใน queue หายคืองานหาย ถ้าแอป (เช่น Laravel) จะใช้ queue ให้แยกไปใช้ database driver หรือ Redis instance ต่างหากที่ตั้ง noeviction เท่านั้น เจ็บมาแล้วครับ
ส่วนตัวผมถือว่ากฎ 80% นี้ใช้ได้กับทุกบริการที่อยู่ใต้ cgroup ไม่ใช่แค่ Redis — ตัวเลขข้างในแอปต้องต่ำกว่ากำแพงข้างนอกเสมอ ไม่งั้นแอปไม่มีวันรู้ตัวว่ากำลังโดนบีบ ใครจะเอาโครงนี้ไปใช้ ปรับตัวเลข tier ตามเครื่องได้ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



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