ไฟล์ swap ที่หายไปซึ่งทำให้เซิร์ฟเวอร์ของเราค้าง
เมื่อวันที่ 24 กันยายน เซิร์ฟเวอร์ production เครื่องหนึ่งหยุดตอบสนอง load balancer ทำเครื่องหมายว่าไม่พร้อมใช้งาน และเครื่องมือคำสั่งระยะไกลของเราเข้าถึงไม่ได้ แต่ไม่มีอะไรล่มและไม่มีโปรเซสใดถูกฆ่าเพราะใช้หน่วยความจำมากเกินไป เซิร์ฟเวอร์กำลัง thrash: เมื่อไม่มี swap space และ container ได้ใช้หน่วยความจำมากกว่าที่เครื่องมี Linux จึงใช้เวลาทั้งหมดไปกับการเอาไฟล์ที่จำเป็นต่อการรันออกจากแคชและอ่านกลับจากดิสก์อีกครั้ง จนแทบไม่มีความคืบหน้า การเพิ่มไฟล์ swap ขนาด 2 GB บนทุกเซิร์ฟเวอร์แก้ปัญหานี้ได้ บทเรียนที่น่าสนใจกว่าคือเรื่อง health check
อาการ "ค้าง" เป็นอย่างไร
- health check ของ load balancer หมดเวลาบนเซิร์ฟเวอร์นั้น
- คำสั่งที่ส่งผ่าน AWS Systems Manager ค้างอยู่ในสถานะ "Pending" และไม่เคยรัน
- ไม่มี out-of-memory kill ในล็อก ไม่มีโปรเซสตาย
หนึ่งสัปดาห์ก่อนหน้านั้น เราได้ย้ายเซิร์ฟเวอร์ production ไปยังเครื่อง ARM ที่เล็กลง ซึ่งแต่ละเครื่องมี RAM 2 GB เซิร์ฟเวอร์ staging ของเราซึ่งเล็กกว่าอีก ก็เคยค้างแบบเดียวกันระหว่างการแตก container image ตอน deploy
ทำไมจึงไม่มีข้อผิดพลาดหน่วยความจำ?
Linux จะฆ่าโปรเซสก็ต่อเมื่อหน่วยความจำที่กู้คืนได้หมดจริงๆ ก่อนถึงจุดนั้น มันจะกู้หน่วยความจำจาก page cache ซึ่งคือสำเนาของไฟล์ที่เก็บในแรม รวมถึงโค้ดของทุกโปรแกรมที่กำลังรัน
บนเซิร์ฟเวอร์ของเรา ขีดจำกัดหน่วยความจำรวมของ container รวมกันได้ประมาณ 3.3 GB ในเครื่องที่ใช้งานได้ประมาณ 1.8 GB ปกติพวกมันไม่ได้ใช้ถึงขีดจำกัดพร้อมกันทั้งหมด แต่เมื่อใช้ kernel จะนำโค้ดโปรแกรมออกจากแคชเพื่อหาที่ว่าง และต้องอ่านกลับจากดิสก์ทันทีที่โค้ดนั้นกลับมาทำงาน วงจรนั้น หรือ thrashing สามารถทำให้เครื่องแทบไม่ตอบสนองทั้งที่ไม่ได้หมดหน่วยความจำทางเทคนิค ดังนั้นจึงไม่มีอะไรถูกฆ่า และไม่มีอะไรฟื้นตัว
เมื่อไม่มี swap kernel ไม่มีที่ให้หน่วยความจำที่ถูกจองไว้แต่ไม่ค่อยได้ใช้ ดังนั้นมันต้องเอาจากแคชแทน
วิธีแก้
สคริปต์ bootstrap ของเซิร์ฟเวอร์ตอนนี้สร้างไฟล์ swap ขนาด 2 GB พร้อม swappiness ต่ำ (10) ทุกครั้งที่เซิร์ฟเวอร์บูตหรือถูกซ่อมแซม swappiness ต่ำหมายความว่า kernel ยังคงชอบเก็บหน่วยความจำที่ใช้งานไว้ในแรม แต่หน้าที่ไม่ค่อยถูกแตะต้องสามารถย้ายไปดิสก์ได้ แทนที่จะบังคับให้โค้ดโปรแกรมออกจากแคช เราใช้มันสดกับเซิร์ฟเวอร์ production ทั้งสองเครื่องและ staging และ pipeline ของ deploy จะเผยแพร่สคริปต์ bootstrap ใหม่ทุกครั้งที่ deploy production เพื่อให้เซิร์ฟเวอร์ที่ถูกแทนที่ก็ได้มันไปด้วย
สิ่งที่น่ากังวลกว่า: เซิร์ฟเวอร์ที่ไม่ปกติยังคงอยู่
กลุ่มเซิร์ฟเวอร์ของเราใช้ EC2 health check คือ "เครื่องกำลังรันอยู่ไหม" ไม่ใช่ load balancer health check คือ "กำลังตอบคำขอไหม" ดังนั้นเซิร์ฟเวอร์ที่ load balancer ยอมแพ้ไปแล้วยังถือว่าปกติสำหรับกลุ่ม และจะไม่ถูกแทนที่เลย production จะเงียบๆ ทำงานบนเซิร์ฟเวอร์เครื่องเดียวจนมีใครสังเกตเห็นและรีบูตอีกเครื่อง
การสลับกลุ่มไปใช้ load balancer health check ฟังดูเป็นวิธีแก้ที่ชัดเจน แต่สำหรับเรามันไม่ตรงไปตรงมา ระหว่าง deploy เซิร์ฟเวอร์จะตอบ health endpoint ด้วยข้อผิดพลาดโดยตั้งใจ เพื่อให้ balancer ระบายการเชื่อมต่อออก (ดูวิธีที่เราทำให้ deploy เหลือ 502 เป็นศูนย์) กลุ่มที่แทนที่เซิร์ฟเวอร์ตามสัญญาณนั้นจะแทนที่มันกลางการ deploy ทุกครั้ง
ดังนั้นตอนนี้เราเพิ่มขั้นตอนหนึ่งในเช็คลิสต์การ deploy: หลังทุกการ deploy ให้ตรวจ health ของทุก target บน load balancer ไม่ใช่แค่ดูว่างาน deploy ผ่านหรือไม่ หากคำสั่งไปยังเซิร์ฟเวอร์ค้างอยู่ใน "Pending" แสดงว่ามันกำลัง thrash ให้รีบูต เพราะมันอยู่นอกการให้บริการแล้ว
สิ่งที่เราเรียนรู้
- "ไม่ล่ม" ไม่ได้แปลว่า "ปกติ" Thrashing ไม่ผลิตข้อผิดพลาด มีแค่ความช้าที่รุนแรงจนเครื่องหยุดตอบ
- เซิร์ฟเวอร์ขนาดเล็กต้องมี swap แม้เพียงเล็กน้อย พร้อม swappiness ต่ำ ก็ให้ kernel มีที่เก็บหน่วยความจำที่ไม่ได้ใช้
- ขีดจำกัดหน่วยความจำที่รวมกันแล้วเกินเครื่องคือการเดิมพัน บางครั้งคุณแพ้ รู้ว่าเมื่อไรที่คุณเดิมพันไปแล้ว
- รู้ว่า health check ไหนที่ autoscaling เชื่อถือ health check ที่ถามแค่ว่า "VM เปิดอยู่ไหม" จะไม่แทนที่เซิร์ฟเวอร์ที่หยุดทำงานแล้ว