อินเทอร์เน็ตถูกสร้างขึ้นมาได้ดีจนคนส่วนใหญ่มองว่ามันเป็นทรัพยากรธรรมชาติอย่างมหาสมุทรแปซิฟิก มากกว่าที่จะเป็นสิ่งที่มนุษย์สร้างขึ้น เมื่อไหร่ที่คุณเคยเห็นเทคโนโลยีที่มีขนาดใหญ่ขนาดนี้ แล้วไร้ข้อผิดพลาดขนาดนี้มาก่อน?

Alan Kay ในการให้สัมภาษณ์กับ Dr. Dobb’s Journal (2012)

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

นอกจากนี้ คุณอาจมี nonfunctional requirements (ความต้องการที่ไม่ใช่เชิงฟังก์ชัน) ด้วย เช่น แอปควรเร็ว เชื่อถือได้ ปลอดภัย ถูกกฎหมาย และบำรุงรักษาง่าย requirements เหล่านี้อาจไม่ได้ถูกเขียนไว้ชัดเจนเพราะดูค่อนข้างเป็นเรื่องที่รู้กันอยู่แล้ว แต่ก็สำคัญไม่แพ้ฟังก์ชันการทำงานของแอป เพราะแอปที่ช้าจนใช้ไม่ไหวหรือไม่น่าเชื่อถือ ก็แทบเท่ากับไม่มีแอปนั้นอยู่เลย

nonfunctional requirements หลายข้อ เช่น ความปลอดภัย อยู่นอกขอบเขตของหนังสือเล่มนี้ แต่เราจะพูดถึงบางข้อ และบทนี้จะช่วยให้คุณกำหนดข้อกำหนดเหล่านี้สำหรับระบบของคุณเองได้ โดยเฉพาะเราจะดูเรื่องต่อไปนี้:

  • การกำหนดและวัด performance (ประสิทธิภาพ) ของระบบ

  • ความหมายของการที่บริการ reliable (เชื่อถือได้) คือยังทำงานได้ถูกต้องต่อไป แม้มีสิ่งผิดปกติเกิดขึ้น

  • การทำให้ระบบ scalable (ปรับขนาดได้) โดยมีวิธีเพิ่มกำลังประมวลผลอย่างมีประสิทธิภาพเมื่อ load ของระบบเพิ่มขึ้น

  • การทำให้ระบบบำรุงรักษาง่ายขึ้นในระยะยาว

คำศัพท์ในบทนี้จะมีประโยชน์ในบทต่อ ๆ ไปด้วย เมื่อเราลงลึกถึงวิธีสร้างระบบที่ต้องจัดการข้อมูลจำนวนมาก อย่างไรก็ตาม นิยามเชิงนามธรรมอาจอ่านแล้วแห้งไปหน่อย เพื่อให้เห็นภาพชัดขึ้น เราจะเริ่มบทนี้ด้วยกรณีศึกษาของบริการ social network ซึ่งเป็นตัวอย่างจริงของ performance และ scalability

Case Study: Social Network Home Timelines (กรณีศึกษา: Home Timelines ของ Social Network)

ลองนึกว่าเราได้รับมอบหมายให้สร้าง social network แบบ X (เดิมคือ Twitter) ที่ผู้ใช้โพสต์ข้อความและติดตามผู้ใช้คนอื่นได้ นี่เป็นเวอร์ชันที่ง่ายกว่าวิธีที่บริการแบบนี้ทำงานจริงมาก [ 1 , 2 , 3 ] แต่ก็ช่วยให้เห็นปัญหาบางอย่างที่เกิดขึ้นในระบบขนาดใหญ่

สมมติว่าผู้ใช้ทั้งหมดโพสต์รวมกัน 500 ล้านโพสต์ต่อวัน หรือเฉลี่ย 5,800 โพสต์ต่อวินาที บางช่วงอัตรานี้อาจพุ่งสูงถึง 150,000 โพสต์ต่อวินาที [ 4 ] สมมติต่อไปอีกว่าผู้ใช้ทั่วไปติดตาม 200 คนและมีผู้ติดตาม 200 คน (แม้ตัวเลขจริงจะกระจายกว้างมาก: คนส่วนใหญ่มีผู้ติดตามไม่กี่คน ส่วนคนดังบางคน เช่น Barack Obama มีผู้ติดตามมากกว่า 100 ล้านคน)

Representing Users, Posts, and Follows (การแทนข้อมูลผู้ใช้ โพสต์ และการติดตาม)

เราเก็บข้อมูลทั้งหมดไว้ใน relational database ดังที่แสดงใน Figure 2-1 เรามีตารางหนึ่งตารางสำหรับผู้ใช้ หนึ่งตารางสำหรับโพสต์ และหนึ่งตารางสำหรับความสัมพันธ์การติดตาม

Diagram illustrating a simple relational schema for a social network, showing tables for users, posts, and follow relationships with sample data.

Figure 2-1. A simple relational schema for a social network in which users can follow one another (โครงสร้าง relational อย่างง่ายสำหรับโซเชียลเน็ตเวิร์กที่ผู้ใช้สามารถติดตามกันได้)

สมมติว่า read หลักที่ social network ของเราต้องรองรับคือ home timeline ซึ่งแสดงโพสต์ล่าสุดจากคนที่ผู้ใช้ติดตามอยู่ (เพื่อความง่าย เราจะไม่นับโฆษณา โพสต์แนะนำจากคนที่ไม่ได้ติดตาม และฟีเจอร์เสริมอื่น ๆ) เราดึง home timeline ของผู้ใช้คนหนึ่งได้ด้วย SQL query ต่อไปนี้:

SELECT posts.*, users.* FROM posts
  JOIN follows ON posts.sender_id = follows.followee_id
  JOIN users   ON posts.sender_id = users.id
  WHERE follows.follower_id = current_user
  ORDER BY posts.timestamp DESC
  LIMIT 1000

เมื่อ execute query นี้ database จะใช้ตาราง follows เพื่อหาทุกคนที่ current_user ติดตามอยู่ จากนั้นหาโพสต์ล่าสุดของผู้ใช้เหล่านั้น แล้วเรียงตาม timestamp เพื่อเอา 1,000 โพสต์ล่าสุดจากทุกคนที่ติดตาม

โพสต์ควรจะสดใหม่ ดังนั้นสมมติว่าหลังจากมีคนโพสต์ เราต้องการให้ผู้ติดตามเห็นโพสต์นั้นภายในห้าวินาที วิธีหนึ่งคือให้ client ของผู้ใช้ทำ query ซ้ำทุกห้าวินาทีตราบที่ผู้ใช้ยังออนไลน์อยู่ (วิธีนี้เรียกว่า polling ) ถ้าสมมติว่ามีผู้ใช้ 10 ล้านคนออนไลน์และล็อกอินพร้อมกัน เราจะต้องรัน query 2 ล้านครั้งต่อวินาที และต่อให้ poll ห่างขึ้น ก็ยังเป็นปริมาณที่มากอยู่ดี

query นี้ยังมีต้นทุนค่อนข้างสูง: ถ้าผู้ใช้ติดตาม 200 คน query ต้องดึงลิสต์โพสต์ล่าสุดของแต่ละคนใน 200 คนนั้นแล้ว merge เข้าด้วยกัน timeline query สองล้านครั้งต่อวินาที คูณกับ 200 บัญชีที่ติดตาม เท่ากับต้อง lookup 400 ล้านครั้งต่อวินาที ซึ่งเป็นตัวเลขมหาศาล และนี่ยังเป็นแค่กรณีทั่วไป ผู้ใช้บางคนติดตามเป็นหมื่นบัญชี สำหรับคนเหล่านี้ query นี้แพงมากและยากที่จะทำให้เร็ว

Materializing and Updating Timelines (การ Materialize และอัปเดต Timelines)

เราจะทำให้ดีกว่านี้ได้อย่างไร? อย่างแรก แทนที่จะ polling ควรให้ server push โพสต์ใหม่ไปหาผู้ติดตามที่ออนไลน์อยู่โดยอัตโนมัติ อย่างที่สอง เราควร precompute ผลลัพธ์ของ query เพื่อให้ request ขอ home timeline ของผู้ใช้ตอบจาก cache ได้

ลองนึกว่าเราเก็บ data structure ที่มี home timeline (คือโพสต์ล่าสุดจากคนที่ติดตามอยู่) ของผู้ใช้แต่ละคนไว้ ทุกครั้งที่มีผู้ใช้โพสต์ เราจะหาผู้ติดตามทั้งหมดของเขาแล้วแทรกโพสต์นั้นลงใน home timeline ของผู้ติดตามแต่ละคน เหมือนส่งข้อความเข้ากล่องจดหมาย เมื่อผู้ใช้ล็อกอิน เราก็ให้ home timeline ที่ precompute ไว้แล้วได้เลย นอกจากนี้ เพื่อแจ้งเตือนว่ามีโพสต์ใหม่บน timeline client ของผู้ใช้แค่ subscribe สตรีมของโพสต์ที่ถูกเพิ่มเข้า home timeline ของตัวเอง

ข้อเสียของแนวทางนี้คือเราต้องทำงานมากขึ้นทุกครั้งที่มีผู้ใช้โพสต์ เพราะ home timeline เป็น derived data ที่ต้องอัปเดต ขั้นตอนนี้แสดงใน Figure 2-2 เมื่อ request เริ่มต้นหนึ่งครั้งทำให้เกิด request ตามมาหลายรายการ เราเรียกว่า fan-out ซึ่งบอกว่า request เพิ่มขึ้นเป็นกี่เท่า

Diagram showing the fan-out process, illustrating how a user's post is distributed to each follower's home timeline, demonstrating derived data updating in a messaging system.

Figure 2-2. Fan-out: delivering new posts to every follower of the user who made the post (Fan-out: การส่งโพสต์ใหม่ไปยังผู้ติดตามทุกคนของผู้ใช้ที่โพสต์)

ที่อัตรา 5,800 โพสต์ต่อวินาที ถ้าโพสต์ทั่วไปไปถึงผู้ติดตาม 200 คน (คือ fan-out factor เท่ากับ 200) เราจะต้องเขียน home timeline มากกว่า 1 ล้านครั้งต่อวินาที นับว่ามาก แต่ก็ยังประหยัดกว่าอย่างเห็นได้ชัดเมื่อเทียบกับการ lookup โพสต์ 400 ล้านครั้งต่อวินาทีที่ต้องทำหากไม่ใช้วิธีนี้

ถ้าอัตราการโพสต์พุ่งสูงเพราะเหตุการณ์พิเศษ เราไม่จำเป็นต้องส่งเข้า timeline ทันที แต่ใส่ไว้ใน queue แล้วยอมรับว่าโพสต์จะขึ้นบน timeline ของผู้ติดตามช้าลงเล็กน้อยชั่วคราวได้ และแม้ในช่วง load spike แบบนี้ timeline ก็ยังโหลดเร็ว เพราะเราตอบจาก cache อยู่

การ precompute และอัปเดตผลลัพธ์ของ query แบบนี้เรียกว่า materialization และ timeline cache เป็นตัวอย่างของ materialized view (แนวคิดที่เราจะพูดถึงอีกในบทต่อ ๆ ไป) Materialized view ทำให้ read เร็วขึ้น แต่แลกกับการที่ต้องทำงานตอน write มากขึ้น สำหรับผู้ใช้ส่วนใหญ่ ต้นทุนของ write ไม่มากนัก แต่ social network ก็ยังต้องคิดถึงกรณีสุดโต่งบางกรณีด้วย:

  • ถ้าผู้ใช้ติดตามบัญชีจำนวนมาก และบัญชีเหล่านั้นโพสต์บ่อย materialized timeline ของผู้ใช้คนนั้นจะถูกเขียนในอัตราสูง แต่ผู้ใช้มักไม่ได้อ่านโพสต์ทั้งหมดใน timeline อยู่แล้ว ดังนั้นปล่อยให้ write ของ timeline บางส่วนหายไป แล้วแสดงแค่ตัวอย่างโพสต์จากบัญชีที่ติดตามก็ไม่เป็นไร [ 5 ]

  • เมื่อบัญชีคนดังที่มีผู้ติดตามมากโพสต์ เราต้องทำงานเยอะเพื่อแทรกโพสต์นั้นลง home timeline ของผู้ติดตามนับล้านคน กรณีนี้ปล่อยให้ write บางส่วนหายไปไม่ได้ วิธีแก้วิธีหนึ่งคือจัดการโพสต์ของคนดังแยกจากโพสต์ของคนอื่น: เราลดงานการเพิ่มโพสต์คนดังลงใน timeline นับล้านได้ โดยเก็บโพสต์เหล่านั้นแยกไว้ แล้ว merge กับ materialized timeline ตอน read แม้จะปรับแต่งแบบนี้แล้ว การรองรับคนดังบน social network ก็อาจยังต้องใช้โครงสร้างพื้นฐานจำนวนมาก [ 6 ]

Describing Performance (การอธิบายประสิทธิภาพ)

เวลาพูดถึง performance ของซอฟต์แวร์ เรามักดู metric หลักสองแบบ:

Response time (เวลาตอบสนอง)

เวลาตั้งแต่ผู้ใช้ส่ง request จนได้รับคำตอบกลับ วัดเป็นวินาที (หรือมิลลิวินาที หรือไมโครวินาที)

Throughput (ปริมาณงาน)

จำนวน request ต่อวินาที หรือปริมาณข้อมูลต่อวินาที ที่ระบบประมวลผลอยู่ สำหรับทรัพยากรฮาร์ดแวร์ที่จัดสรรไว้ค่าหนึ่ง จะมี maximum throughput (ปริมาณงานสูงสุด) ที่ระบบรับไหว หน่วยวัดคือ "จำนวนสิ่งที่ประมวลผลได้ต่อวินาที"

ในกรณีศึกษา social network "จำนวนโพสต์ต่อวินาที" และ "timeline write ต่อวินาที" เป็น throughput metric ส่วน "เวลาที่ใช้โหลด home timeline" และ "เวลาที่โพสต์ใช้กว่าจะถึงผู้ติดตาม" เป็น response time metric

Throughput และ response time มักสัมพันธ์กัน ตัวอย่างความสัมพันธ์นี้ของ online service แสดงใน Figure 2-3 บริการจะมี response time ต่ำเมื่อ throughput ของ request ต่ำ แต่ response time จะเพิ่มขึ้นเมื่อ load เพิ่มขึ้น สาเหตุคือ queueing (การเข้าคิว): เมื่อ request มาถึงระบบที่ load สูง CPU มักกำลังประมวลผล request ก่อนหน้าอยู่ request ที่เข้ามาใหม่จึงต้องรอจนกว่าตัวก่อนหน้าจะเสร็จ และเมื่อ throughput เข้าใกล้ขีดสูงสุดที่ฮาร์ดแวร์รับไหว ความล่าช้าจาก queueing จะพุ่งขึ้นอย่างรวดเร็ว

A graph illustrates the relationship between throughput and response time, showing that response time increases sharply as throughput nears the hardware's capacity due to queueing.

Figure 2-3. As the throughput of a service approaches its capacity, the response time increases dramatically because of queueing. (เมื่อ throughput ของบริการเข้าใกล้ขีดความสามารถของมัน response time จะเพิ่มขึ้นอย่างมากเนื่องจาก queueing)

When an Overloaded System Won’t Recover (เมื่อระบบที่โอเวอร์โหลดไม่สามารถฟื้นตัว)

ถ้าระบบใกล้โอเวอร์โหลดจน throughput ถูกดันเข้าใกล้ขีดจำกัด บางครั้งมันอาจเข้าสู่ vicious cycle ที่ทำให้ประสิทธิภาพลดลงและโอเวอร์โหลดหนักขึ้นเรื่อย ๆ ตัวอย่างเช่น ถ้ามี request รออยู่ในคิวยาว response time อาจเพิ่มขึ้นมากจน client timeout แล้วส่ง request ซ้ำ ทำให้อัตรา request สูงขึ้นไปอีกและปัญหาแย่ลง ซึ่งเรียกว่า retry storm (พายุการส่งซ้ำ) แม้ load จะลดลงแล้ว ระบบแบบนี้ก็อาจยังค้างอยู่ในสถานะโอเวอร์โหลดจนกว่าจะถูก reboot หรือ reset ด้วยวิธีอื่น ปรากฏการณ์นี้เรียกว่า metastable failure (ความล้มเหลวแบบ metastable) และอาจทำให้ระบบ production ล่มอย่างรุนแรง [ 7 , 8 , 9 ]

เพื่อไม่ให้ retry ทำให้บริการโอเวอร์โหลด ฝั่ง client สามารถเพิ่มและสุ่มระยะเวลาระหว่างการ retry ( exponential backoff [ 10 , 11 ]) และหยุดส่ง request ไปยังบริการที่เพิ่งตอบ error หรือ timeout ไปชั่วคราว (โดยใช้ circuit breaker [ 12 , 13 ] หรือ token bucket algorithm [ 14 ]) ฝั่ง server เองก็ตรวจจับได้เมื่อใกล้โอเวอร์โหลด และเริ่มปฏิเสธ request ล่วงหน้า ( load shedding [ 15 ]) หรือตอบกลับไปขอให้ client ส่งช้าลง ( backpressure [ 1 , 16 ]) การเลือก queueing และ load balancing algorithm ก็ช่วยได้เช่นกัน [ 17 ]

ในบรรดา performance metric response time มักเป็นสิ่งที่ผู้ใช้สนใจมากที่สุด ส่วน throughput กำหนดทรัพยากรการคำนวณที่ต้องใช้ (เช่น ต้องมี server กี่เครื่อง) จึงเป็นต้นทุนของการรองรับ workload หนึ่ง ๆ ถ้า throughput มีแนวโน้มเพิ่มจนเกินความสามารถของฮาร์ดแวร์ที่มีอยู่ ก็ต้องขยายความจุ ระบบจะเรียกว่า scalable (ปรับขนาดได้) ถ้าเพิ่มทรัพยากรการคำนวณแล้ว throughput สูงสุดของมันเพิ่มขึ้นได้อย่างมีนัยสำคัญ

ในส่วนนี้เราจะเน้นที่ response time เป็นหลัก ส่วน throughput และ scalability เราจะกลับมาดู ใน "Scalability" อีกครั้ง

Latency and Response Time (Latency และ Response Time)

คำว่า "latency" และ "response time" บางครั้งใช้แทนกันได้ แต่ในหนังสือเล่มนี้เราจะใช้สองคำนี้และคำที่เกี่ยวข้องอีกไม่กี่คำในความหมายเฉพาะ (แสดงใน Figure 2-4 ):

  • Response time (เวลาตอบสนอง) คือสิ่งที่ client เห็น โดยรวมความล่าช้าทั้งหมดที่เกิดขึ้นในระบบ

  • Service time (เวลาบริการ) คือช่วงเวลาที่บริการกำลังประมวลผล request ของ client อยู่จริง ๆ

  • Queueing delays (ความล่าช้าจากการเข้าคิว) เกิดได้หลายจุดใน flow เช่น หลังรับ request แล้ว request อาจต้องรอจนกว่า CPU จะว่างจึงเริ่มประมวลผลได้ หรือ response packet อาจต้องถูก buffer ไว้ก่อนส่งผ่านเครือข่าย ถ้างานอื่นบนเครื่องเดียวกันกำลังส่งข้อมูลจำนวนมากออกทาง network interface

  • Latency เป็นคำกว้าง ๆ ที่ครอบคลุมเวลาที่ request ไม่ได้ถูกประมวลผลอยู่ นั่นคือช่วงที่มัน latent (แฝงอยู่) โดยเฉพาะอย่างยิ่ง network latency หรือ network delay (ความหน่วงของเครือข่าย) หมายถึงเวลาที่ request และ response ใช้เดินทางผ่านเครือข่าย

Diagram illustrating the components of response time, showing user and service interaction with network latency, queueing delay, and service time.

Figure 2-4. Response time, service time, network latency, and queueing delay (Response time, service time, network latency, และ queueing delay)

ใน Figure 2-4 เวลาไหลจากซ้ายไปขวา node ที่สื่อสารกันแต่ละตัวแสดงเป็นเส้นแนวนอน และ request หรือ response message แสดงเป็นลูกศรเฉียงหนาจาก node หนึ่งไปอีก node หนึ่ง คุณจะเจอไดอะแกรมแบบนี้บ่อยตลอดทั้งเล่ม

Response time อาจต่างกันมากในแต่ละ request แม้คุณจะส่ง request เดิมซ้ำแล้วซ้ำเล่าก็ตาม มีหลายปัจจัยที่ทำให้เกิดความล่าช้าแบบสุ่มได้ เช่น context switch ไปยัง background process, network packet สูญหายและต้อง TCP retransmission, การหยุดเพื่อทำ garbage collection, page fault ที่บังคับให้ต้องอ่านจากดิสก์ หรือแม้แต่การสั่นสะเทือนทางกลใน server rack [ 18 ] เราจะพูดถึงหัวข้อนี้โดยละเอียดใน "Timeouts and Unbounded Delays"

Queueing delay มักเป็นสาเหตุหลักของความแปรปรวนใน response time เพราะ server ประมวลผลงานแบบขนานได้จำกัด (เช่น ถูกจำกัดด้วยจำนวน CPU core) แค่ request ที่ช้าไม่กี่รายการก็ทำให้ request ที่ตามมาช้าไปด้วย ผลกระทบนี้เรียกว่า head-of-line blocking (การปิดกั้นหัวแถว) แม้ request ที่ตามมาจะมี service time สั้น client ก็ยังเห็น response time โดยรวมที่ช้า เพราะต้องรอ request ก่อนหน้าให้เสร็จ Queueing delay ไม่รวมอยู่ใน service time ด้วยเหตุนี้การวัด response time ฝั่ง client จึงสำคัญ

Average, Median, and Percentiles (ค่าเฉลี่ย มัธยฐาน และ Percentiles)

เนื่องจาก response time ต่างกันไปในแต่ละ request เราจึงควรมองมันไม่ใช่เป็นตัวเลขเดียว แต่เป็น distribution (การแจกแจง) ของค่าที่วัดได้ ใน Figure 2-5 แท่งสีเทาแต่ละแท่งแทน request หนึ่งไปยังบริการ และความสูงของแท่งแสดงว่า request นั้นใช้เวลานานเท่าใด request ส่วนใหญ่ค่อนข้างเร็ว แต่บางครั้งมี outliers (ค่าผิดปกติ) ใช้เวลานานกว่ามาก ความแปรผันของ network delay เรียกอีกอย่างว่า jitter

Chart depicting response times of 100 service requests, highlighting mean, median, and 95th and 99th percentiles to illustrate distribution and outliers.

Figure 2-5. Illustrating mean and percentiles: response times for a sample of 100 requests to a service (การแสดงค่าเฉลี่ยและ percentiles: response times สำหรับตัวอย่าง 100 request ไปยังบริการ)

บริการมักรายงาน average (ค่าเฉลี่ย) response time ของบริการ (ในทางเทคนิคคือ arithmetic mean ซึ่งหาได้โดยรวม response time ทั้งหมดแล้วหารด้วยจำนวน request) ค่าเฉลี่ยของ response time มีประโยชน์ในการประมาณขีดจำกัดของ throughput [ 19 ] แต่ค่าเฉลี่ยไม่ใช่ metric ที่ดีนักถ้าคุณอยากรู้ response time "ทั่วไป" เพราะมันไม่ได้บอกว่ามีผู้ใช้กี่คนที่เจอความล่าช้านั้นจริง ๆ

โดยทั่วไปแล้วการใช้ percentiles จะดีกว่า ลองนำลิสต์ response time มาเรียงจากเร็วที่สุดไปช้าที่สุด median (มัธยฐาน) คือจุดกึ่งกลาง เช่น ถ้า median response time คือ 200 ms แปลว่าครึ่งหนึ่งของ request ตอบกลับในเวลาน้อยกว่า 200 มิลลิวินาที และอีกครึ่งใช้เวลานานกว่านั้น median จึงเป็น metric ที่ดีถ้าอยากรู้ว่าโดยทั่วไปผู้ใช้ต้องรอนานแค่ไหน Median ยังเรียกอีกชื่อว่า 50th percentile ซึ่งบางครั้งย่อว่า p50

ถ้าอยากรู้ว่า outlier แย่แค่ไหน ให้ดู percentile ที่สูงขึ้น: 95th , 99th และ 99.9th percentiles ที่พบบ่อย (ย่อว่า p95 , p99 และ p999 ) ตัวอย่างเช่น ถ้า 95th percentile response time คือ 1.5 วินาที แปลว่า 95 ใน 100 request ใช้เวลาน้อยกว่า 1.5 วินาที และอีก 5 ใน 100 request ใช้เวลา 1.5 วินาทีขึ้นไป ดังแสดงใน Figure 2-5

Response time ที่ percentile สูง หรือที่เรียกว่า tail latencies (ความหน่วงส่วนท้าย) สำคัญเพราะส่งผลโดยตรงต่อประสบการณ์ของผู้ใช้ ตัวอย่างเช่น Amazon กำหนด response time ที่ต้องการของ internal service ที่ 99.9th percentile ทั้งที่มีผลกับแค่ 1 ใน 1,000 request เพราะลูกค้าที่ request ช้าที่สุดมักเป็นคนที่มีข้อมูลในบัญชีมากที่สุด จากการซื้อของจำนวนมาก นั่นคือเป็นลูกค้าที่มีค่าที่สุด [ 20 ] การทำให้เว็บไซต์เร็วสำหรับลูกค้าเหล่านี้เพื่อให้พวกเขาพอใจจึงสำคัญ

ส่วนการ optimize ที่ 99.99th percentile (request ที่ช้าที่สุด 1 ใน 10,000) Amazon เห็นว่าแพงเกินไปและให้ประโยชน์ไม่คุ้มกับเป้าหมายของตน การลด response time ที่ percentile สูงมากทำได้ยาก เพราะได้รับผลจากเหตุการณ์สุ่มที่อยู่นอกการควบคุมได้ง่าย และประโยชน์ที่ได้ก็ลดลงเรื่อย ๆ

The User Impact of Response Times (ผลกระทบของ Response Times ต่อผู้ใช้)

ดูเหมือนชัดเจนว่าผู้ใช้ย่อมชอบบริการที่เร็วกว่าบริการที่ช้า [ 21 ] แต่การหาข้อมูลที่เชื่อถือได้มาวัดว่า latency ส่งผลต่อพฤติกรรมของผู้ใช้มากแค่ไหนนั้นยากอย่างน่าประหลาดใจ

สถิติที่ถูกอ้างถึงบ่อยบางตัวไม่น่าเชื่อถือ เช่น ในปี 2006 Google รายงานว่าผลการค้นหาที่ช้าลงจาก 400 ms เป็น 900 ms สัมพันธ์กับ traffic และรายได้ที่ลดลง 20% [ 22 ] แต่การศึกษาอีกชิ้นของ Google ในปี 2009 รายงานว่า latency ที่เพิ่มขึ้น 400 ms ทำให้จำนวนการค้นหาต่อวันลดลงเพียง 0.6% [ 23 ] และในปีเดียวกัน Bing พบว่าเวลาโหลดที่เพิ่มขึ้นสองวินาทีทำให้รายได้จากโฆษณาลดลง 4.3% [ 24 ] ดูเหมือนบริษัทเหล่านี้จะไม่ได้เปิดเผยข้อมูลที่ใหม่กว่านี้ต่อสาธารณะ

การศึกษาที่ใหม่กว่าของ Akamai อ้างว่า response time ที่เพิ่มขึ้น 100 ms ทำให้ conversion rate ของเว็บไซต์ ecommerce ลดลงได้ถึง 7% [ 25 ] แต่เมื่อดูละเอียดขึ้น การศึกษาเดียวกันนี้แสดงว่าหน้าเว็บที่โหลด เร็วมาก ก็สัมพันธ์กับ conversion rate ที่ต่ำกว่าเช่นกัน! ผลที่ดูขัดแย้งกันนี้อธิบายได้ว่า หน้าเว็บที่โหลดเร็วที่สุดมักเป็นหน้าที่ไม่มีเนื้อหาที่เป็นประโยชน์ (เช่น หน้า error 404) แต่เพราะการศึกษานี้ไม่ได้แยกผลของเนื้อหาหน้าเว็บออกจากผลของเวลาโหลด ผลลัพธ์จึงอาจไม่มีความหมาย

การศึกษาของ Yahoo ในปีถัดมาเปรียบเทียบ click-through rate ของผลการค้นหาที่โหลดเร็วกับที่โหลดช้า โดยควบคุมให้คุณภาพของผลการค้นหาเท่ากัน [ 26 ] พบว่าผลการค้นหาที่เร็วมีคลิกมากกว่า 20%–30% เมื่อคำตอบเร็วกับช้าต่างกัน 1.25 วินาทีขึ้นไป

Use of Response Time Metrics (การใช้ Response Time Metrics)

High percentile มีความสำคัญอย่างยิ่งใน backend service ที่ถูกเรียกหลายครั้งเพื่อตอบ end-user request เดียว แม้จะเรียกแบบขนาน request ก็ยังต้องรอให้การเรียกที่ช้าที่สุดเสร็จ แค่การเรียกที่ช้าครั้งเดียวก็ทำให้ end-user request ทั้งหมดช้าลงได้ ดังแสดงใน Figure 2-6 ต่อให้การเรียก backend ที่ช้ามีเพียงเปอร์เซ็นต์น้อย แต่ถ้า end-user request ต้องเรียก backend หลายครั้ง โอกาสที่จะเจอการเรียกที่ช้าก็สูงขึ้น end-user request ในสัดส่วนที่มากขึ้นจึงช้าตามไปด้วย (ผลกระทบนี้เรียกว่า tail latency amplification (การขยายความหน่วงส่วนท้าย) [ 27 ])

Diagram illustrating how a single slow backend request can delay an entire end-user request, showing various backend response times with one significantly larger delay.

Figure 2-6. When several backend calls are needed to serve a request, just a single slow call can slow down the entire end-user request. (เมื่อต้องเรียก backend หลายครั้งเพื่อตอบ request การเรียกที่ช้าเพียงครั้งเดียวก็ทำให้ end-user request ทั้งหมดช้าลงได้)

Percentile มักถูก ใช้ใน service level objectives (SLOs) และ service level agreements (SLAs) เพื่อกำหนดประสิทธิภาพและ availability ที่คาดหวังจากบริการ [ 28 ] ตัวอย่างเช่น SLO อาจตั้งเป้าให้บริการมี median response time ต่ำกว่า 200 ms และ 99th percentile ต่ำกว่า 1 วินาที และตั้งเป้าว่าอย่างน้อย 99.9% ของ request ที่ถูกต้องต้องได้ response ที่ไม่มี error ส่วน SLA คือสัญญาที่ระบุว่าจะเกิดอะไรขึ้นถ้าไม่เป็นไปตาม SLO (เช่น ลูกค้าอาจมีสิทธิ์ได้เงินคืน) อย่างน้อยนั่นคือแนวคิดพื้นฐาน แต่ในทางปฏิบัติ การกำหนด availability metric ที่ดีสำหรับ SLO และ SLA ไม่ใช่เรื่องตรงไปตรงมา [ 29 , 30 ]

Computing Percentiles (การคำนวณ Percentiles)

ถ้า คุณอยากเพิ่ม response time percentile ใน monitoring dashboard ของบริการ คุณต้องคำนวณมันอย่างมีประสิทธิภาพและต่อเนื่อง ตัวอย่างเช่น คุณอาจเก็บ rolling window ของ response time ของ request ใน 10 นาทีที่ผ่านมา แล้วทุกนาทีก็คำนวณ median และ percentile ต่าง ๆ จากค่าใน window นั้น และพล็อต metric เหล่านั้นบนกราฟ

วิธี implement ที่ง่ายที่สุดคือเก็บลิสต์ response time ของ request ทั้งหมดใน time window แล้วเรียงลิสต์นั้นทุกนาที ถ้าวิธีนี้ช้าเกินไปสำหรับคุณ ยังมี algorithm ที่คำนวณค่าประมาณของ percentile ได้ดี โดยใช้ CPU และหน่วยความจำน้อยมาก library แบบ open source สำหรับประมาณค่า percentile ได้แก่ HdrHistogram [ 31 ], t-digest [ 32 , 33 ], OpenHistogram [ 34 ], และ DDSketch [ 35 ]

ระวังว่าการเฉลี่ย percentile (เช่น เพื่อลดความละเอียดของเวลา หรือรวมข้อมูลจากหลายเครื่อง) ไม่มีความหมายทางคณิตศาสตร์ วิธีที่ถูกต้องในการรวมข้อมูล response time คือการรวม histogram เข้าด้วยกัน [ 36 ]

Reliability and Fault Tolerance (ความน่าเชื่อถือและการทนทานต่อข้อผิดพลาด)

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

  • แอปพลิเคชันทำงานตามฟังก์ชันที่ผู้ใช้คาดหวัง

  • แอปพลิเคชันรับมือได้เมื่อผู้ใช้ทำผิดพลาดหรือใช้ซอฟต์แวร์ในแบบที่ไม่คาดคิด

  • performance ดีพอสำหรับ use case ที่ต้องการ ภายใต้ load และปริมาณข้อมูลที่คาดไว้

  • ระบบป้องกันการเข้าถึงโดยไม่ได้รับอนุญาตและการใช้งานในทางที่ผิดได้

ถ้าทั้งหมดนี้รวมกันหมายถึง "ทำงานได้ถูกต้อง" เราก็เข้าใจ reliability (ความน่าเชื่อถือ) ได้โดยคร่าวว่าคือ "การทำงานได้ถูกต้องต่อไปแม้มีสิ่งผิดปกติเกิดขึ้น" เพื่อให้พูดถึงสิ่งที่ผิดพลาดได้แม่นยำขึ้น เราจะแยก fault ออกจาก failure [ 37 , 38 , 39 ]:

Fault (ข้อบกพร่อง)

Fault เกิดขึ้นเมื่อ ส่วนหนึ่ง ของระบบทำงานไม่ถูกต้อง เช่น ฮาร์ดไดรฟ์ตัวเดียวทำงานผิดปกติ เครื่องเดียว crash หรือบริการภายนอก (ที่ระบบพึ่งพา) หยุดให้บริการ

Failure (ความล้มเหลว)

Failure เกิดขึ้นเมื่อระบบ โดยรวม ไม่ให้บริการตามที่ต้องการแก่ผู้ใช้ หรืออีกนัยหนึ่งคือเมื่อไม่เป็นไปตาม SLO

ความต่างระหว่าง fault กับ failure อาจทำให้สับสน เพราะมันเป็นสิ่งเดียวกันที่อยู่คนละระดับ เช่น ถ้าฮาร์ดไดรฟ์หยุดทำงาน เราบอกว่าฮาร์ดไดรฟ์นั้น failed ถ้าระบบมีฮาร์ดไดรฟ์เพียงตัวเดียว ระบบก็หยุดให้บริการตามที่ต้องการ จึงถือว่า failed ด้วย แต่ถ้าระบบมีฮาร์ดไดรฟ์หลายตัว การที่ไดรฟ์ตัวเดียวเสียก็เป็นแค่ fault เมื่อมองจากระบบที่ใหญ่กว่า และระบบนั้นอาจทนต่อ fault ดังกล่าวได้ด้วยการเก็บสำเนาข้อมูลไว้บนไดรฟ์อื่น

Fault Tolerance (การทนทานต่อข้อผิดพลาด)

เราเรียกระบบว่า fault-tolerant (ทนทานต่อข้อผิดพลาด) ถ้ายังให้บริการตามที่ต้องการแก่ผู้ใช้ได้แม้เกิด fault บางอย่าง ถ้าส่วนใดส่วนหนึ่งเสียแล้วระบบรับไม่ไหว เราเรียกส่วนนั้นว่า single point of failure (SPOF) (จุดเดียวที่ทำให้ล้มเหลว) เพราะ fault ในส่วนนั้นจะลุกลามจนทำให้ทั้งระบบล้มเหลว

ตัวอย่างเช่น ในกรณีศึกษา social network fault ที่อาจเกิดขึ้นคือ ระหว่างขั้นตอน fan-out เครื่องที่อัปเดต materialized timeline crash หรือใช้งานไม่ได้ เพื่อให้ขั้นตอนนี้ fault-tolerant เราต้องมั่นใจว่าเครื่องอื่นรับงานต่อได้ โดยไม่พลาดโพสต์ที่ควรจะส่ง และไม่ส่งโพสต์ซ้ำ (แนวคิดนี้เรียกว่า exactly-once semantics และเราจะดูรายละเอียดใน Chapter 12 )

Fault tolerance จำกัดอยู่ที่ fault บางประเภทและบางจำนวนเสมอ เช่น ระบบอาจทนฮาร์ดไดรฟ์ที่เสียพร้อมกันได้สูงสุดสองตัว หรือทนโหนดที่ crash ได้สูงสุดหนึ่งในสามโหนด การทนต่อ fault ได้ไม่จำกัดจำนวนนั้นไม่สมเหตุสมผล ถ้าโหนดทั้งหมด crash ก็ไม่มีอะไรทำได้ ถ้าทั้งโลก (และ server ทั้งหมดบนนั้น) ถูกหลุมดำกลืน การทนต่อ fault แบบนั้นต้องมี hosting ในอวกาศ ขอให้โชคดีกับการขออนุมัติงบนั้นก็แล้วกัน

แม้จะขัดกับสามัญสำนึก แต่ในระบบ fault-tolerant แบบนี้ การ เพิ่ม อัตรา fault โดยตั้งใจกระตุ้นให้เกิดก็อาจสมเหตุสมผล เช่น สุ่มฆ่า process โดยไม่แจ้งเตือน วิธีนี้เรียกว่า fault injection (การฉีดข้อผิดพลาด) บั๊กร้ายแรงจำนวนมากแท้จริงเกิดจากการจัดการ error ที่ไม่ดี [ 40 ] การกระตุ้น fault โดยตั้งใจทำให้กลไก fault tolerance ถูกใช้งานและทดสอบอยู่ตลอด ซึ่งอาจช่วยเพิ่มความมั่นใจว่า fault จะถูกจัดการถูกต้องเมื่อเกิดขึ้นเองตามธรรมชาติ Chaos engineering (วิศวกรรมความโกลาหล) เป็นศาสตร์ที่เน้นสร้างความมั่นใจในกลไก fault tolerance ผ่านการทดลอง เช่น ฉีด fault เข้าไปโดยตั้งใจ [ 41 ]

แม้เรามักเลือกทนต่อ fault มากกว่าป้องกัน fault แต่บางกรณีการป้องกันดีกว่าการแก้ไข (เช่น เพราะไม่มีวิธีแก้) ซึ่งเป็นกรณีของเรื่องความปลอดภัย เช่น ถ้าผู้โจมตีบุกรุกระบบและเข้าถึงข้อมูลที่ละเอียดอ่อนได้ เหตุการณ์นั้นย้อนกลับไม่ได้ อย่างไรก็ตาม หนังสือเล่มนี้ส่วนใหญ่พูดถึง fault ประเภทที่แก้ไขได้ ดังที่อธิบายในหัวข้อถัดไป

Hardware and Software Faults (ข้อผิดพลาดด้านฮาร์ดแวร์และซอฟต์แวร์)

เวลาคิดถึงสาเหตุที่ระบบล้มเหลว สิ่งแรกที่ผุดขึ้นมาในหัวคือ hardware fault:

  • ฮาร์ดดิสก์แบบ magnetic ประมาณ 2%–5% เสียต่อปี [ 42 , 43 ] ใน storage cluster ที่มีดิสก์ 10,000 ตัว จึงควรคาดว่าโดยเฉลี่ยจะมีดิสก์เสียหนึ่งตัวต่อวัน ข้อมูลล่าสุดชี้ว่าดิสก์น่าเชื่อถือขึ้น แต่อัตราที่เสียก็ยังไม่น้อย [ 44 ]

  • solid state drive (SSD) ประมาณ 0.5%–1% เสียต่อปี [ 45 ] bit error จำนวนเล็กน้อยถูกแก้ไขโดยอัตโนมัติ [ 46 ] แต่ error ที่แก้ไขไม่ได้เกิดขึ้นประมาณปีละครั้งต่อไดรฟ์ แม้แต่ในไดรฟ์ที่ค่อนข้างใหม่ (คือสึกหรอน้อย) อัตราข้อผิดพลาดนี้สูงกว่าของ magnetic hard drive [ 47 , 48 ]

  • ฮาร์ดแวร์ส่วนอื่น ๆ (เช่น power supply, RAID controller และ memory module) ก็เสียได้เช่นกัน แต่น้อยกว่าฮาร์ดไดรฟ์ [ 49 , 50 ]

  • ประมาณ 1 ใน 1,000 เครื่องมี CPU core ที่บางครั้งคำนวณผลลัพธ์ผิด ซึ่งน่าจะเกิดจากข้อบกพร่องในการผลิต [ 51 , 52 , 53 ] บางกรณีการคำนวณผิดทำให้ crash แต่บางกรณีโปรแกรมก็แค่คืนผลลัพธ์ที่ผิดออกมา

  • ข้อมูลใน RAM อาจเสียหายได้ ทั้งจากเหตุการณ์สุ่มอย่างรังสีคอสมิก และจากข้อบกพร่องทางกายภาพถาวร แม้ใช้หน่วยความจำที่มี error-correcting code (ECC) แล้ว ก็ยังมีเครื่องมากกว่า 1% ที่เจอ error ซึ่งแก้ไขไม่ได้ในแต่ละปี ซึ่งมักทำให้เครื่อง crash และต้องเปลี่ยน memory module ที่เสียหาย [ 54 ] นอกจากนี้ รูปแบบการเข้าถึงหน่วยความจำที่ผิดปกติบางแบบสามารถพลิกบิตได้ด้วยความน่าจะเป็นสูง [ 55 ]

  • datacenter ทั้งแห่งอาจใช้งานไม่ได้ (เช่น เพราะไฟดับหรือตั้งค่าเครือข่ายผิดพลาด) หรืออาจถูกทำลายถาวร (เช่น จากไฟไหม้ น้ำท่วม หรือแผ่นดินไหว [ 56 ]) พายุสุริยะ ซึ่งเกิดขึ้นเมื่อดวงอาทิตย์ปล่อยอนุภาคมีประจุจำนวนมากจนเกิดกระแสไฟฟ้าขนาดใหญ่ในสายส่งระยะไกล อาจทำลายโครงข่ายไฟฟ้าและสายเคเบิลเครือข่ายใต้ทะเลได้ [ 57 ] แม้ความล้มเหลวขนาดใหญ่แบบนี้จะเกิดขึ้นยาก แต่ถ้าบริการทนต่อการสูญเสีย datacenter ไม่ได้ ผลกระทบก็อาจร้ายแรง [ 58 ]

เหตุการณ์เหล่านี้เกิดยากพอที่ระบบเล็กมักไม่ต้องกังวล ตราบใดที่เปลี่ยนฮาร์ดแวร์ที่เสียได้ง่าย แต่ในระบบขนาดใหญ่ hardware fault เกิดบ่อยพอจนกลายเป็นส่วนหนึ่งของการทำงานปกติของระบบ

Tolerating hardware faults through redundancy (การทนต่อข้อผิดพลาดฮาร์ดแวร์ผ่านความซ้ำซ้อน)

วิธีรับมือแรกกับฮาร์ดแวร์ที่ไม่น่าเชื่อถือมักเป็นการเพิ่มความซ้ำซ้อนให้ฮาร์ดแวร์แต่ละส่วนเพื่อลดอัตราความล้มเหลวของระบบ ดิสก์อาจตั้งค่าเป็น RAID (กระจายข้อมูลข้ามหลายดิสก์ในเครื่องเดียวกัน เพื่อให้ดิสก์ที่เสียไม่ทำให้ข้อมูลหาย) server อาจมี dual power supply และ CPU แบบ hot-swappable และ datacenter อาจมีแบตเตอรี่และเครื่องปั่นไฟดีเซลเป็นไฟสำรอง ความซ้ำซ้อนแบบนี้มักทำให้เครื่องทำงานต่อเนื่องได้หลายปี

ความซ้ำซ้อนได้ผลดีที่สุดเมื่อ fault ของแต่ละส่วนเป็นอิสระต่อกัน คือเมื่อการเกิด fault หนึ่งไม่เปลี่ยนโอกาสที่ fault อื่นจะเกิด แต่ประสบการณ์แสดงว่าความล้มเหลวของส่วนประกอบต่าง ๆ มีความสัมพันธ์กันอย่างมีนัยสำคัญ [ 43 , 59 , 60 ] การที่ server rack ทั้งชุดหรือ datacenter ทั้งแห่งใช้งานไม่ได้ ก็ยังเกิดบ่อยกว่าที่เราต้องการ

ความซ้ำซ้อนของฮาร์ดแวร์ช่วยเพิ่ม uptime ของเครื่องเดียว แต่ดังที่กล่าวไว้ใน "Distributed Versus Single-Node Systems" ระบบ distributed มีข้อดี เช่น ทนต่อการที่ datacenter หนึ่งแห่งหยุดให้บริการทั้งหมดได้ ด้วยเหตุนี้ ระบบ cloud จึงให้ความสำคัญกับความน่าเชื่อถือของแต่ละเครื่องน้อยลง และเน้นทำให้บริการ highly available ด้วยการทนต่อโหนดที่เสียในระดับซอฟต์แวร์ ผู้ให้บริการ cloud ใช้ availability zones (โซนความพร้อมใช้งาน) เพื่อบอกว่าทรัพยากรใดอยู่ที่เดียวกันทางกายภาพ ทรัพยากรในสถานที่เดียวกันมีแนวโน้มล้มเหลวพร้อมกันมากกว่าทรัพยากรที่อยู่ห่างกันทางภูมิศาสตร์

เทคนิค fault tolerance ที่เราพูดถึงในหนังสือเล่มนี้ออกแบบมาให้ทนต่อการสูญเสียเครื่องทั้งเครื่อง rack หรือ availability zone โดยทั่วไปจะให้เครื่องใน datacenter หนึ่งรับช่วงต่อเมื่อเครื่องในอีก datacenter เสียหรือเข้าถึงไม่ได้ เราจะพูดถึงเทคนิคเหล่านี้ในบท 6 , 10 และจุดอื่น ๆ ในหนังสือเล่มนี้

ระบบที่ทนต่อการสูญเสียเครื่องทั้งเครื่องได้ยังมีข้อดีด้านการปฏิบัติการด้วย ระบบ single-server ต้องมี downtime ที่วางแผนไว้ถ้าต้อง reboot เครื่อง (เช่น เพื่อติดตั้ง security patch ของระบบปฏิบัติการ) แต่ระบบ multi-node ที่ fault-tolerant สามารถ patch ได้โดย restart ทีละโหนด โดยไม่กระทบบริการของผู้ใช้ วิธีนี้เรียกว่า rolling upgrade (การอัปเกรดแบบโรลลิง) ซึ่งเราจะพูดถึงเพิ่มเติมใน Chapter 5

Software faults (ข้อผิดพลาดของซอฟต์แวร์)

แม้ hardware failure อาจสัมพันธ์กันอยู่บ้าง แต่โดยส่วนใหญ่ก็ยังเป็นอิสระต่อกัน เช่น ถ้าดิสก์ตัวหนึ่งเสีย ดิสก์ตัวอื่นในเครื่องเดียวกันมักยังปกติดี อย่างน้อยก็ชั่วขณะหนึ่ง ในทางกลับกัน software fault มักสัมพันธ์กันสูงมาก เพราะโดยทั่วไปหลายโหนดรันซอฟต์แวร์เดียวกัน จึงมีบั๊กเดียวกันด้วย [ 61 , 62 ] fault แบบนี้คาดการณ์ยาก และมักทำให้ระบบล้มเหลวมากกว่า hardware fault ที่ไม่สัมพันธ์กันมาก [ 49 ] ตัวอย่างได้แก่:

  • บั๊กซอฟต์แวร์ที่ทำให้ทุกโหนดล้มเหลวพร้อมกันในบางสถานการณ์ เช่น เมื่อวันที่ 30 มิถุนายน 2012 leap second (อธิกวินาที) ทำให้แอปพลิเคชัน Java จำนวนมากค้างพร้อมกันเพราะบั๊กใน Linux kernel ส่งผลให้บริการอินเทอร์เน็ตหลายแห่งล่ม [ 63 ] และเพราะบั๊กใน firmware SSD บางรุ่นทั้งหมดเสียกะทันหันหลังทำงานครบ 32,768 ชั่วโมง (น้อยกว่าสี่ปี) ทำให้ข้อมูลบนนั้นกู้คืนไม่ได้ [ 64 ]

  • process ที่ทำงานผิดปกติและกินทรัพยากรที่มีจำกัดร่วมกัน เช่น CPU time, หน่วยความจำ, พื้นที่ดิสก์, network bandwidth หรือ thread [ 65 ] เช่น process ที่ใช้หน่วยความจำมากเกินไปตอนประมวลผล request ขนาดใหญ่อาจถูกระบบปฏิบัติการ kill หรือบั๊กใน client library อาจทำให้ปริมาณ request สูงกว่าที่คาดไว้มาก [ 66 ]

  • บริการที่ระบบพึ่งพาช้าลง ไม่ตอบสนอง หรือเริ่มตอบกลับด้วยข้อมูลที่เสียหาย

  • ปฏิสัมพันธ์ระหว่างระบบต่าง ๆ ทำให้เกิด emergent behavior ที่ไม่เกิดขึ้นเมื่อทดสอบแต่ละระบบแยกกัน [ 67 ]

  • Cascading failure (ความล้มเหลวแบบลูกโซ่) ที่ปัญหาของส่วนประกอบหนึ่งทำให้อีกส่วนโอเวอร์โหลดและช้าลง จนทำให้ส่วนอื่นล้มตามไปอีก [ 68 , 69 ]

บั๊กที่ก่อให้เกิด software fault ประเภทนี้มักซ่อนอยู่เป็นเวลานาน จนกว่าจะถูกกระตุ้นโดยสถานการณ์ผิดปกติบางชุด ซึ่งเผยให้เห็นว่าซอฟต์แวร์ตั้งสมมติฐานบางอย่างเกี่ยวกับสภาพแวดล้อมของมันไว้ และแม้สมมติฐานนั้น โดยปกติ เป็นจริง แต่สุดท้ายก็มีเหตุผลบางอย่างที่ทำให้มันไม่เป็นจริงอีกต่อไป [ 70 , 71 ]

ปัญหา systematic fault ในซอฟต์แวร์ไม่มีวิธีแก้ลัด แต่มีเรื่องเล็ก ๆ หลายอย่างที่ช่วยได้ เช่น คิดอย่างรอบคอบเกี่ยวกับสมมติฐานและปฏิสัมพันธ์ในระบบ ทดสอบอย่างละเอียด ทำให้แน่ใจว่ามี process isolation ปล่อยให้ process crash แล้ว restart หลีกเลี่ยง feedback loop เช่น retry storm (ดู "When an Overloaded System Won't Recover" ) วัด ตรวจสอบ และวิเคราะห์พฤติกรรมของระบบใน production

Humans and Reliability (มนุษย์และความน่าเชื่อถือ)

มนุษย์เป็นผู้ออกแบบและสร้างระบบซอฟต์แวร์ และ operator (ผู้ปฏิบัติการ) ที่ดูแลให้ระบบทำงานต่อไปก็เป็นมนุษย์เช่นกัน มนุษย์ต่างจากเครื่องจักรตรงที่ไม่ได้ทำตามกฎอย่างเดียว จุดแข็งอย่างหนึ่งของเราคือความคิดสร้างสรรค์และการปรับตัวเพื่อทำงานให้สำเร็จ แต่ลักษณะนี้ก็ทำให้คาดเดาไม่ได้ และบางครั้งนำไปสู่ความผิดพลาดที่ทำให้เกิด failure ได้ แม้จะตั้งใจดีที่สุดก็ตาม ตัวอย่างเช่น การศึกษาหนึ่งเกี่ยวกับบริการอินเทอร์เน็ตขนาดใหญ่พบว่า การเปลี่ยน configuration โดย operator เป็นสาเหตุหลักของการล่ม ขณะที่ hardware fault (ของ server หรือเครือข่าย) เกี่ยวข้องเพียง 10%–25% ของกรณี [ 72 ]

เป็นเรื่องน่าล่อใจที่จะป้ายปัญหาแบบนี้ว่าเป็น "human error" แล้วหวังว่าจะแก้ได้ด้วยการควบคุมพฤติกรรมของคนให้ดีขึ้นผ่านขั้นตอนที่เข้มงวดและการทำตามกฎ แต่การโทษคนไม่ช่วยอะไร สิ่งที่เราเรียกว่า "human error" ไม่ใช่สาเหตุที่แท้จริงของเหตุการณ์ แต่เป็นอาการของปัญหาในระบบสังคมและเทคนิค ที่ผู้คนกำลังพยายามทำงานของตนอย่างเต็มที่ [ 73 ] ระบบที่ซับซ้อนมักมี emergent behavior ซึ่งการโต้ตอบที่ไม่คาดคิดระหว่างส่วนประกอบก็อาจนำไปสู่ความล้มเหลวได้เช่นกัน [ 74 ]

มาตรการทางเทคนิคหลายอย่างช่วยลดผลกระทบจากความผิดพลาดของมนุษย์ได้ เช่น การทดสอบอย่างละเอียด (ทั้งการทดสอบที่เขียนด้วยมือและ property testing กับ input สุ่มจำนวนมาก) [ 40 ] กลไก rollback สำหรับย้อนการเปลี่ยน configuration อย่างรวดเร็ว การค่อย ๆ ปล่อยโค้ดใหม่ การตรวจสอบที่ละเอียดและชัดเจน เครื่องมือ observability สำหรับวินิจฉัยปัญหาใน production (ดู "Problems with Distributed Systems" ) และอินเทอร์เฟซที่ออกแบบมาดี ซึ่งส่งเสริม "สิ่งที่ถูกต้อง" และกีดกัน "สิ่งที่ผิด"

แต่ทั้งหมดนี้ต้องลงทุนทั้งเวลาและเงิน และในความเป็นจริงของการทำธุรกิจประจำวัน องค์กรมักให้ความสำคัญกับกิจกรรมที่สร้างรายได้ มากกว่ามาตรการที่ช่วยให้ระบบทนต่อความผิดพลาดได้ดีขึ้น เมื่อต้องเลือกระหว่างฟีเจอร์เพิ่มกับการทดสอบเพิ่ม หลายองค์กรก็เลือกฟีเจอร์ ซึ่งเข้าใจได้ แล้วเมื่อความผิดพลาดที่ป้องกันได้เกิดขึ้นจนได้ การโทษคนที่ทำพลาดก็ไม่สมเหตุสมผล ปัญหาอยู่ที่ลำดับความสำคัญขององค์กร

องค์กรต่าง ๆ นำวัฒนธรรม blameless postmortems (การวิเคราะห์หลังเหตุการณ์ที่ไม่มีการตำหนิ) มาใช้มากขึ้นเรื่อย ๆ คือหลังเกิดเหตุการณ์ คนที่เกี่ยวข้องจะได้รับการสนับสนุนให้เล่ารายละเอียดทั้งหมดของสิ่งที่เกิดขึ้นโดยไม่ต้องกลัวถูกลงโทษ เพราะจะช่วยให้คนอื่นในองค์กรเรียนรู้วิธีป้องกันปัญหาแบบเดียวกันในอนาคต [ 75 ] กระบวนการนี้อาจเผยว่าต้องเปลี่ยนลำดับความสำคัญทางธุรกิจ ลงทุนกับด้านที่ถูกละเลย เปลี่ยนแรงจูงใจของคนที่เกี่ยวข้อง หรือดึงปัญหาเชิงระบบอื่นมาให้ผู้บริหารสนใจ

ตามหลักทั่วไป เวลาสืบสวนเหตุการณ์ ควรระวังคำตอบที่ง่ายเกินไป "บ็อบควรระวังมากขึ้นตอน deploy การเปลี่ยนแปลงนั้น" ไม่ช่วยอะไร แต่ "เราต้องเขียน backend ใหม่ด้วย Haskell" ก็ไม่ช่วยเช่นกัน ผู้บริหารควรใช้โอกาสนี้ทำความเข้าใจว่าระบบสังคมและเทคนิคทำงานอย่างไรจากมุมมองของคนที่ใช้งานมันทุกวัน และปรับปรุงตามคำติชมนั้น [ 73 ]

How Important Is Reliability? (ความน่าเชื่อถือสำคัญแค่ไหน?)

ความน่าเชื่อถือไม่ได้สำคัญแค่กับโรงไฟฟ้านิวเคลียร์และระบบควบคุมการจราจรทางอากาศ แอปพลิเคชันทั่วไปก็ถูกคาดหวังให้เชื่อถือได้เช่นกัน บั๊กในแอปพลิเคชันธุรกิจทำให้เสียผลิตภาพ (และมีความเสี่ยงทางกฎหมายถ้ารายงานตัวเลขผิด) และเว็บไซต์ ecommerce ที่ล่มอาจสร้างความเสียหายมหาศาลทั้งรายได้ที่เสียไปและชื่อเสียง

ในหลายแอปพลิเคชัน การหยุดให้บริการชั่วคราวสองสามนาทีหรือไม่กี่ชั่วโมงยังพอรับได้ [ 76 ] แต่ข้อมูลที่สูญหายถาวรหรือเสียหายนั้นร้ายแรง ลองนึกถึงพ่อแม่ที่เก็บรูปและวิดีโอทั้งหมดของลูกไว้ในแอปรูปภาพของคุณ [ 77 ] พวกเขาจะรู้สึกอย่างไรถ้า database นั้นเสียหายขึ้นมากะทันหัน? และพวกเขาจะรู้ไหมว่าต้องกู้คืนคอลเล็กชันของตัวเองจาก backup อย่างไร?

อีกตัวอย่างของซอฟต์แวร์ที่ไม่น่าเชื่อถือจนทำร้ายผู้คน คือเรื่องอื้อฉาว Post Office Horizon ระหว่างปี 1999 ถึง 2019 ผู้จัดการสาขาไปรษณีย์ในอังกฤษหลายร้อยคนถูกตัดสินว่ามีความผิดฐานลักทรัพย์หรือฉ้อโกง เพราะซอฟต์แวร์บัญชีแสดงว่าบัญชีของพวกเขามียอดเงินขาด ต่อมาจึงชัดเจนว่ายอดขาดเหล่านี้จำนวนมากเกิดจากบั๊กในซอฟต์แวร์ และคำตัดสินหลายคดีถูกพลิกกลับ [ 78 ] ต้นเหตุของสิ่งที่น่าจะเป็นความยุติธรรมที่คลาดเคลื่อนครั้งใหญ่ที่สุดในประวัติศาสตร์อังกฤษนี้ คือข้อสันนิษฐานของกฎหมายอังกฤษที่ว่าคอมพิวเตอร์ทำงานถูกต้อง (หลักฐานที่คอมพิวเตอร์สร้างขึ้นจึงเชื่อถือได้) เว้นแต่จะมีหลักฐานตรงกันข้าม [ 79 ] วิศวกรซอฟต์แวร์อาจขำกับความคิดที่ว่าซอฟต์แวร์จะไม่มีบั๊ก แต่นั่นปลอบใจอะไรไม่ได้เลยสำหรับคนที่ถูกจำคุกโดยไม่เป็นธรรม ถูกประกาศล้มละลาย หรือแม้แต่ฆ่าตัวตาย จากคำตัดสินที่ผิดพลาดซึ่งเกิดจากระบบคอมพิวเตอร์ที่ไม่น่าเชื่อถือ

ในบางสถานการณ์เราอาจเลือกยอมลดความน่าเชื่อถือเพื่อลดต้นทุนการพัฒนา (เช่น เมื่อทำต้นแบบผลิตภัณฑ์สำหรับตลาดที่ยังไม่ได้พิสูจน์) แต่เราควรรู้ตัวเสมอว่ากำลังใช้ทางลัด และจำไว้ว่าอาจมีผลตามมา

Scalability (ความสามารถในการปรับขนาด)

ระบบที่ทำงานได้น่าเชื่อถือในวันนี้ ไม่ได้แปลว่าจะน่าเชื่อถือในอนาคตเสมอไป สาเหตุทั่วไปอย่างหนึ่งที่ทำให้ประสิทธิภาพแย่ลงคือ load ที่เพิ่มขึ้น เช่น ระบบอาจโตจากผู้ใช้พร้อมกัน 10,000 คนเป็น 100,000 คน หรือจาก 1 ล้านเป็น 10 ล้าน หรืออาจต้องประมวลผลข้อมูลมากกว่าเดิมหลายเท่า

Scalability (ความสามารถในการปรับขนาด) คือคำที่เราใช้อธิบายความสามารถของระบบในการรับมือกับ load ที่เพิ่มขึ้น เวลาพูดถึง scalability บางครั้งก็จะมีคนบอกทำนองว่า "คุณไม่ใช่ Google หรือ Amazon หยุดกังวลเรื่อง scale แค่ใช้ relational database" คติพจน์นี้ใช้กับคุณได้หรือไม่ ขึ้นอยู่กับประเภทของแอปพลิเคชันที่คุณสร้าง

ถ้าคุณกำลังสร้างผลิตภัณฑ์ใหม่ที่ตอนนี้มีผู้ใช้ไม่กี่คน เช่นใน startup เป้าหมายหลักทางวิศวกรรมมักเป็นการทำให้ระบบเรียบง่ายและยืดหยุ่นที่สุดเท่าที่จะทำได้ เพื่อให้ปรับเปลี่ยนฟีเจอร์ของผลิตภัณฑ์ได้ง่ายเมื่อคุณเรียนรู้ความต้องการของลูกค้ามากขึ้น [ 80 ] ในสภาพแวดล้อมแบบนี้ การกังวลเรื่อง scale สมมุติที่อาจจำเป็นในอนาคตส่งผลตรงข้ามกับที่ตั้งใจ อย่างดีที่สุด การลงทุนใน scalability ก็เป็นความพยายามที่สูญเปล่าและเป็น premature optimization อย่างแย่ที่สุด มันจะล็อกคุณไว้กับการออกแบบที่ไม่ยืดหยุ่น ทำให้พัฒนาแอปพลิเคชันต่อได้ยากขึ้น

Scalability ไม่ใช่ป้ายกำกับมิติเดียว การพูดว่า " X ปรับขนาดได้" หรือ " Y ปรับขนาดไม่ได้" ไม่มีความหมาย การพูดถึง scalability คือการตั้งคำถามอย่างเช่น:

  • ถ้าระบบโตไปในทิศทางหนึ่ง เรามีทางเลือกอะไรบ้างในการรับมือกับการเติบโตนั้น?

  • เราจะเพิ่มทรัพยากรการคำนวณเพื่อรับ load ที่เพิ่มขึ้นได้อย่างไร?

  • ถ้าการเติบโตเป็นไปตามที่คาดไว้ในตอนนี้ เมื่อไหร่เราจะถึงขีดจำกัดของสถาปัตยกรรมปัจจุบัน?

ถ้าแอปพลิเคชันของคุณประสบความสำเร็จจนคนใช้เยอะ และต้องรับมือกับ load ที่เพิ่มขึ้น คุณจะได้เรียนรู้ว่า performance bottleneck อยู่ตรงไหน และต้อง scale ในมิติใด ถึงตอนนั้นก็ถึงเวลาสนใจเทคนิคด้าน scalability

Understanding Load (การทำความเข้าใจโหลด)

ก่อนอื่นคุณต้องเข้าใจ load ปัจจุบันของระบบให้ชัดเจน แล้วจึงค่อยคุยเรื่องการเติบโตได้ ("จะเกิดอะไรขึ้นถ้า load เพิ่มเป็นสองเท่า?") ส่วนใหญ่แล้วจะวัดเป็น throughput เช่น จำนวน request ต่อวินาทีที่ส่งเข้าบริการ ปริมาณข้อมูลใหม่ที่เข้ามาต่อวันเป็นกิกะไบต์ หรือจำนวนการชำระเงินจากตะกร้าสินค้าต่อชั่วโมง บางครั้งคุณสนใจค่าสูงสุดของปริมาณที่ผันแปร เช่น จำนวนผู้ใช้ที่ออนไลน์พร้อมกัน ในกรณีศึกษา social network ของเรา

บ่อยครั้ง ลักษณะทางสถิติอื่น ๆ ของ load ก็มีผลต่อรูปแบบการเข้าถึงข้อมูลและข้อกำหนดด้าน scalability เช่น คุณอาจต้องรู้สัดส่วน read ต่อ write ใน database, cache hit rate หรือจำนวนรายการข้อมูลต่อผู้ใช้ (ในกรณีศึกษาของเราคือผู้ติดตาม) บางทีกรณีทั่วไปคือสิ่งที่สำคัญสำหรับคุณ หรือบางที bottleneck ของคุณอาจถูกกำหนดโดยกรณีสุดโต่งไม่กี่กรณี ทั้งหมดขึ้นอยู่กับรายละเอียดของแอปพลิเคชันของคุณ

เมื่อเข้าใจ load ของระบบแล้ว คุณก็ตรวจสอบได้ว่าจะเกิดอะไรขึ้นเมื่อ load เพิ่มขึ้น โดยดูได้สองทาง:

  • ถ้าเพิ่ม load ในลักษณะหนึ่งโดยไม่เปลี่ยนทรัพยากรของระบบ (CPU, หน่วยความจำ, network bandwidth ฯลฯ) performance ของระบบได้รับผลกระทบอย่างไร?

  • ถ้าเพิ่ม load ในลักษณะหนึ่งและต้องการให้ performance คงเดิม ต้องเพิ่มทรัพยากรมากแค่ไหน?

โดยปกติเป้าหมายคือรักษา performance ของระบบให้อยู่ในข้อกำหนดของ SLA (ดู "Use of Response Time Metrics" ) พร้อมกับลดต้นทุนการรันระบบให้น้อยที่สุด ยิ่งต้องใช้ทรัพยากรการคำนวณมาก ต้นทุนก็ยิ่งสูง ฮาร์ดแวร์บางชนิดอาจคุ้มค่ากว่าชนิดอื่น และปัจจัยเหล่านี้ก็เปลี่ยนไปได้เมื่อมีฮาร์ดแวร์ชนิดใหม่ออกมา

ถ้าเพิ่มทรัพยากรเป็นสองเท่าแล้วรับ load ได้เป็นสองเท่าโดยที่ performance เท่าเดิม เราบอกว่าคุณมี linear scalability (การปรับขนาดเชิงเส้น) ซึ่งเป็นสิ่งที่ดี บางครั้งก็อาจรับ load สองเท่าได้ด้วยทรัพยากรน้อยกว่าสองเท่า เพราะ economies of scale หรือการกระจาย peak load ได้ดีขึ้น [ 81 , 82 ] แต่มีแนวโน้มมากกว่าที่ต้นทุนจะโตเร็วกว่าเชิงเส้น ความไม่มีประสิทธิภาพมีได้หลายสาเหตุ เช่น ถ้ามีข้อมูลจำนวนมาก การประมวลผล write request หนึ่งครั้งอาจต้องทำงานมากกว่าตอนที่มีข้อมูลน้อย แม้ request จะมีขนาดเท่ากัน

Shared-Memory, Shared-Disk, and Shared-Nothing Architectures (สถาปัตยกรรม Shared-Memory, Shared-Disk และ Shared-Nothing)

วิธีที่ง่ายที่สุดในการเพิ่มทรัพยากรฮาร์ดแวร์ของบริการคือย้ายไปใช้เครื่องที่แรงกว่า CPU core แต่ละตัวไม่ได้เร็วขึ้นอย่างมีนัยสำคัญอีกแล้ว แต่คุณซื้อเครื่อง (หรือเช่า cloud instance) ที่มี CPU core, RAM และพื้นที่ดิสก์มากขึ้นได้ วิธีนี้เรียกว่า vertical scaling (การปรับขนาดแนวตั้ง) หรือ scaling up

คุณทำ parallelism บนเครื่องเดียวได้โดยใช้หลาย process หรือ thread ทุก thread ใน process เดียวกันเข้าถึง RAM ก้อนเดียวกันได้ วิธีนี้จึงเรียกว่า shared-memory architecture (สถาปัตยกรรมหน่วยความจำร่วม) ปัญหาของแนวทาง shared-memory คือต้นทุนโตเร็วกว่าเชิงเส้น เครื่องระดับสูงที่มีฮาร์ดแวร์เป็นสองเท่าของเครื่องระดับล่างมักแพงกว่าสองเท่าอย่างมาก และเพราะมี bottleneck เครื่องนั้นก็ไม่น่าจะรับ load ได้เป็นสองเท่าจริง

อีกแนวทางหนึ่งคือ shared-disk architecture (สถาปัตยกรรมดิสก์ร่วม) ซึ่งใช้หลายเครื่องที่มี CPU และ RAM เป็นของตัวเอง แต่เก็บข้อมูลบน disk array ที่แชร์กันระหว่างเครื่อง โดยเชื่อมต่อผ่านเครือข่ายความเร็วสูง: network-attached storage (NAS) หรือ storage area network (SAN) สถาปัตยกรรมนี้ใช้กันมาแต่เดิมกับ workload ของ data warehousing ในองค์กร แต่ contention และ overhead ของการ lock จำกัดความสามารถในการ scale ของแนวทาง shared-disk [ 83 ]

ในทางตรงกันข้าม shared-nothing architecture (สถาปัตยกรรมที่ไม่มีการแชร์อะไรเลย) [ 84 ] (หรือเรียกอีกอย่างว่า horizontal scaling หรือ scaling out ) คือระบบ distributed ที่มีหลายโหนด โดยแต่ละโหนดมี CPU, RAM และดิสก์ของตัวเอง การประสานงานระหว่างโหนดทำที่ระดับซอฟต์แวร์ ผ่านเครือข่ายทั่วไป

ข้อดีของแนวทางนี้ ซึ่งได้รับความนิยมในช่วงไม่กี่ปีที่ผ่านมา คือมีศักยภาพที่จะ scale ได้แบบเชิงเส้น ใช้ฮาร์ดแวร์แบบไหนก็ได้ที่คุ้มราคาที่สุด (โดยเฉพาะบน cloud) ปรับทรัพยากรฮาร์ดแวร์ตาม load ที่เพิ่มหรือลดได้ง่ายกว่า และ fault-tolerant ได้ดีกว่าด้วยการกระจายระบบไปหลาย datacenter และหลาย region ข้อเสียคือต้องทำ explicit sharding (ดู Chapter 7 ) และต้องเผชิญความซับซ้อนทั้งหมดของระบบ distributed (ซึ่งกล่าวถึงใน Chapter 9 )

ระบบ database แบบ cloud native บางระบบแยก storage ออกจากการ execute transaction เป็นคนละบริการ (ดู "Separation of storage and compute" ) โดยมี compute node หลายตัวใช้บริการ storage เดียวกันร่วมกัน โมเดลนี้คล้าย shared-disk architecture อยู่บ้าง แต่หลีกเลี่ยงปัญหา scalability ของระบบรุ่นเก่า เพราะบริการ storage ไม่ได้ให้ abstraction เป็น filesystem (NAS) หรือ block device (SAN) แต่เปิด API ที่ออกแบบมาเฉพาะสำหรับความต้องการของ database [ 85 ]

Principles for Scalability (หลักการสำหรับ Scalability)

สถาปัตยกรรมของระบบที่ทำงานในระดับ scale ใหญ่มักเฉพาะเจาะจงกับแอปพลิเคชันนั้น ๆ มาก ไม่มีสถาปัตยกรรมที่ scale ได้แบบสากลที่ใช้ได้กับทุกกรณี (เรียกอย่างไม่เป็นทางการว่า magic scaling sauce ) ตัวอย่างเช่น ระบบที่ออกแบบมารับ 100,000 request ต่อวินาที โดยแต่ละ request ขนาด 1 kB จะหน้าตาต่างจากระบบที่ออกแบบมารับ 3 request ต่อนาที โดยแต่ละ request ขนาด 2 GB มาก ทั้งที่ทั้งสองระบบมี throughput ของข้อมูลเท่ากัน (100 MB/วินาที)

นอกจากนี้ สถาปัตยกรรมที่เหมาะกับ load ระดับหนึ่งไม่น่าจะรับ load ที่มากกว่าเดิม 10 เท่าได้ ถ้าคุณทำงานกับบริการที่โตเร็ว คุณจึงน่าจะต้องทบทวนสถาปัตยกรรมใหม่ทุกครั้งที่ load เพิ่มขึ้นหนึ่ง order of magnitude และเนื่องจากความต้องการของแอปพลิเคชันมักเปลี่ยนไปเรื่อย ๆ โดยปกติจึงไม่คุ้มที่จะวางแผนรองรับ scale ในอนาคตไกลเกินกว่าหนึ่ง order of magnitude

หลักการทั่วไปที่ดีสำหรับ scalability คือแบ่งระบบออกเป็นส่วนเล็ก ๆ ที่ส่วนใหญ่ทำงานอิสระจากกันได้ ซึ่งเป็นหลักการพื้นฐานของ microservices (ดู "Microservices and Serverless" ), sharding ( Chapter 7 ), stream processing ( Chapter 12 ), และ shared-nothing architecture ความท้าทายคือการรู้ว่าจะลากเส้นแบ่งตรงไหนระหว่างสิ่งที่ควรอยู่ด้วยกันกับสิ่งที่ควรแยกจากกัน คำแนะนำในการออกแบบ microservices หาอ่านได้จากหนังสือเล่มอื่น [ 86 ] ส่วน sharding ของระบบ shared-nothing เราจะพูดถึงใน Chapter 7

หลักการดีอีกข้อคืออย่าทำให้ซับซ้อนเกินจำเป็น ถ้า database บนเครื่องเดียวทำงานได้ ก็อาจดีกว่า setup แบบ distributed ที่ซับซ้อน ระบบ autoscaling (ที่เพิ่มหรือลดทรัพยากรโดยอัตโนมัติตามความต้องการ) เจ๋งดี แต่ถ้า load ของคุณค่อนข้างคาดเดาได้ ระบบที่ scale ด้วยมืออาจมีเรื่องไม่คาดฝันตอน operate น้อยกว่า (ดู "Operations: Automatic Versus Manual Rebalancing" ) ระบบที่มี 5 บริการง่ายกว่าระบบที่มี 50 บริการ สถาปัตยกรรมที่ดีมักผสมผสานแนวทางต่าง ๆ อย่างเหมาะสมกับสถานการณ์จริง

Maintainability (ความสามารถในการบำรุงรักษา)

ซอฟต์แวร์ไม่สึกหรอหรือเสื่อมสภาพทางกายภาพ จึงไม่พังแบบเดียวกับวัตถุเชิงกล แต่ข้อกำหนดของแอปพลิเคชันมักเปลี่ยนไปเรื่อย ๆ สภาพแวดล้อมที่ซอฟต์แวร์ทำงานอยู่ก็เปลี่ยนด้วย (เช่น dependency และ platform พื้นฐาน) และซอฟต์แวร์ก็อาจมีบั๊กที่ต้องแก้

ยอมรับกันอย่างกว้างขวางว่าต้นทุนส่วนใหญ่ของซอฟต์แวร์ไม่ได้อยู่ที่การพัฒนาครั้งแรก แต่อยู่ที่การบำรุงรักษาต่อเนื่อง ได้แก่ การแก้บั๊ก การดูแลให้ระบบทำงานต่อไป การสืบสวนความล้มเหลว การปรับให้เข้ากับ platform ใหม่ การปรับให้รองรับ use case ใหม่ การชำระ technical debt และการเพิ่มฟีเจอร์ใหม่ [ 87 , 88 ]

การบำรุงรักษาอาจซับซ้อน โดยเฉพาะกับระบบ legacy ระบบที่ทำงานสำเร็จมานานอาจใช้เทคโนโลยีล้าสมัยที่วิศวกรปัจจุบันเข้าใจกันไม่มาก (เช่น mainframe และโค้ด COBOL) และความรู้ในองค์กรว่าระบบถูกออกแบบมาอย่างไรและทำไมก็อาจหายไปเมื่อคนลาออก บางครั้งอาจต้องแก้ความผิดพลาดของคนอื่นด้วย เพราะระบบคอมพิวเตอร์มักผูกอยู่กับองค์กรของคนที่มันสนับสนุน การบำรุงรักษาระบบแบบนี้จึงเป็นปัญหาของคนพอ ๆ กับปัญหาทางเทคนิค [ 89 ]

ระบบทุกระบบที่เราสร้างในวันนี้จะกลายเป็นระบบ legacy สักวัน ถ้ามันมีค่าพอที่จะอยู่ได้นาน เพื่อลดความเจ็บปวดของคนรุ่นหลังที่ต้องบำรุงรักษาซอฟต์แวร์ของเรา เราควรออกแบบโดยคำนึงถึงการบำรุงรักษา แม้เราจะไม่สามารถคาดเดาได้เสมอไปว่าการตัดสินใจใดอาจสร้างปัญหาด้านการบำรุงรักษาในอนาคต ในหนังสือเล่มนี้เราจะให้ความสนใจกับหลักการหลายข้อที่ใช้ได้อย่างกว้างขวาง:

Operability (ความสามารถในการปฏิบัติการ)

ทำให้องค์กรดูแลให้ระบบทำงานได้ราบรื่นได้ง่าย

Simplicity (ความเรียบง่าย)

ช่วยให้วิศวกรใหม่เข้าใจระบบได้ง่าย โดย implement ด้วย pattern และโครงสร้างที่เป็นที่รู้จักและสอดคล้องกัน และหลีกเลี่ยงความซับซ้อนที่ไม่จำเป็น

Evolvability (ความสามารถในการพัฒนา)

ช่วยให้วิศวกรเปลี่ยนแปลงระบบในอนาคตได้ง่าย รวมถึงปรับและขยายระบบให้รองรับ use case ที่ไม่คาดคิดเมื่อข้อกำหนดเปลี่ยนไป

Operability: Making Life Easy for Operations (Operability: ทำให้ชีวิตของทีมปฏิบัติการง่ายขึ้น)

เราเคยพูดถึงบทบาทของ operations ไว้ใน "Operations in the Cloud Era" และเห็นแล้วว่ากระบวนการของคนสำคัญต่อการ operate ที่เชื่อถือได้อย่างน้อยเท่ากับเครื่องมือซอฟต์แวร์ จริง ๆ แล้วมีคนเสนอไว้ว่า "operations ที่ดีมักหลบข้อจำกัดของซอฟต์แวร์ที่แย่ (หรือยังไม่สมบูรณ์) ได้ แต่ซอฟต์แวร์ที่ดีก็ทำงานได้อย่างน่าเชื่อถือไม่ได้ ถ้า operations แย่" [ 62 ]

ในระบบขนาดใหญ่ที่มีเครื่องหลายพันเครื่อง การดูแลด้วยมือแพงเกินไป จึงต้องใช้ automation แต่ automation ก็เป็นดาบสองคม จะมี edge case อยู่เสมอ (เช่น ความล้มเหลวที่เกิดยาก) ที่ต้องให้ทีม operations เข้ามาแทรกแซงด้วยมือ และเพราะกรณีที่ automation จัดการเองไม่ได้มักซับซ้อนที่สุด ยิ่งมี automation มาก ก็ยิ่งต้องมีทีม operations ที่ มีทักษะมากขึ้น เพื่อแก้ปัญหาเหล่านั้น [ 90 ]

นอกจากนี้ automation ที่ทำงานผิดพลาดมักแก้ปัญหาได้ยากกว่าระบบที่ให้ operator ทำบางอย่างด้วยมือ ด้วยเหตุนี้ automation ที่มากขึ้นจึงไม่ได้ดีกว่าเสมอไปในแง่ operability แต่ automation ในระดับหนึ่งก็สำคัญ จุดที่เหมาะสมขึ้นอยู่กับรายละเอียดเฉพาะของแอปพลิเคชันและองค์กรของคุณ

operability ที่ดีหมายถึงการทำให้งานประจำง่ายขึ้น เพื่อให้ทีม operations ไปโฟกัสกับกิจกรรมที่มีมูลค่าสูงได้ ระบบข้อมูลช่วยได้โดยทำสิ่งต่อไปนี้ [ 91 ]:

  • ให้เครื่องมือ monitoring ตรวจสอบ metric หลักของระบบได้ และรองรับเครื่องมือ observability (ดู "Problems with Distributed Systems" ) เพื่อให้เห็นพฤติกรรมของระบบตอน runtime มีเครื่องมือทั้งเชิงพาณิชย์และ open source หลายตัวที่ช่วยเรื่องนี้ได้ [ 92 ]

  • หลีกเลี่ยงการพึ่งพาเครื่องใดเครื่องหนึ่ง (ให้ถอดเครื่องออกเพื่อบำรุงรักษาได้ โดยที่ระบบโดยรวมยังทำงานต่อเนื่อง)

  • มีเอกสารที่ดี และ operational model ที่เข้าใจง่าย ("ถ้าฉันทำ X, Y จะเกิดขึ้น")

  • มีค่า default ที่ดี แต่ยังให้อิสระแก่ผู้ดูแลระบบในการ override ค่า default เมื่อจำเป็น

  • self-heal ได้เมื่อเหมาะสม แต่ยังให้ผู้ดูแลระบบควบคุมสถานะของระบบด้วยมือได้เมื่อจำเป็น

  • มีพฤติกรรมที่คาดเดาได้ และลดเรื่องที่ไม่คาดฝัน

Simplicity: Managing Complexity (Simplicity: การจัดการความซับซ้อน)

โปรเจกต์ซอฟต์แวร์ขนาดเล็กอาจมีโค้ดที่เรียบง่ายและสื่อความหมายได้อย่างน่าพอใจ แต่เมื่อโปรเจกต์ใหญ่ขึ้น มันมักซับซ้อนและเข้าใจยาก ความซับซ้อนนี้ทำให้ทุกคนที่ทำงานกับระบบช้าลง และยิ่งเพิ่มต้นทุนการบำรุงรักษา โปรเจกต์ซอฟต์แวร์ที่จมอยู่กับความซับซ้อนบางครั้งถูกเรียกว่า big ball of mud (ก้อนโคลนก้อนใหญ่) [ 93 ]

เมื่อความซับซ้อนทำให้การบำรุงรักษายาก งบประมาณและกำหนดการมักเลยเถิด ในซอฟต์แวร์ที่ซับซ้อน ยังมีความเสี่ยงสูงที่จะเกิดบั๊กเมื่อเปลี่ยนแปลงโค้ด เมื่อนักพัฒนาเข้าใจและวิเคราะห์ระบบได้ยาก สมมติฐานที่ซ่อนอยู่ ผลข้างเคียงที่ไม่ตั้งใจ และปฏิสัมพันธ์ที่ไม่คาดคิดก็จะถูกมองข้ามได้ง่าย [ 71 ] ในทางกลับกัน การลดความซับซ้อนช่วยให้บำรุงรักษาซอฟต์แวร์ได้ง่ายขึ้นมาก ความเรียบง่ายจึงควรเป็นเป้าหมายสำคัญของระบบที่เราสร้าง

ระบบที่เรียบง่ายเข้าใจง่ายกว่า เราจึงควรพยายามแก้ปัญหาที่ได้รับด้วยวิธีที่ง่ายที่สุดเท่าที่จะทำได้ น่าเสียดายที่พูดง่ายกว่าทำ การจะบอกว่าอะไรเรียบง่ายหรือไม่มักเป็นเรื่องส่วนบุคคล เพราะไม่มีมาตรฐานที่เป็นกลางของความเรียบง่าย [ 94 ] ตัวอย่างเช่น ระบบหนึ่งอาจซ่อน implementation ที่ซับซ้อนไว้หลัง interface ที่เรียบง่าย ขณะที่อีกระบบอาจมี implementation ที่เรียบง่ายแต่เปิดเผยรายละเอียดภายในให้ผู้ใช้เห็นมากกว่า — ระบบไหนเรียบง่ายกว่ากัน?

มีความพยายามหนึ่งที่จะวิเคราะห์ความซับซ้อนโดยแบ่งออกเป็นสองประเภท คือ essential และ accidental [ 95 ] แนวคิดคือ essential complexity (ความซับซ้อนที่จำเป็น) มีอยู่ในตัว problem domain ของแอปพลิเคชันอยู่แล้ว ส่วน accidental complexity (ความซับซ้อนที่เกิดโดยบังเอิญ) เกิดขึ้นเพียงเพราะข้อจำกัดของเครื่องมือที่เรามี น่าเสียดายที่การแบ่งแบบนี้ก็ยังมีข้อบกพร่อง เพราะเส้นแบ่งระหว่าง essential กับ accidental เปลี่ยนไปเมื่อเครื่องมือของเราพัฒนาขึ้น [ 96 ]

หนึ่งในเครื่องมือที่ดีที่สุดของเราสำหรับจัดการความซับซ้อนคือ abstraction (นามธรรม) abstraction ที่ดีสามารถซ่อนรายละเอียดของ implementation จำนวนมากไว้หลัง façade ที่สะอาดและเข้าใจง่าย และ abstraction ที่ดียังนำไปใช้กับแอปพลิเคชันได้หลากหลาย การ reuse แบบนี้ไม่เพียงมีประสิทธิภาพกว่าการ implement สิ่งที่คล้ายกันซ้ำหลายครั้ง แต่ยังนำไปสู่ซอฟต์แวร์ที่มีคุณภาพสูงขึ้นด้วย เพราะเมื่อ component ที่ถูกทำเป็น abstraction มีคุณภาพดีขึ้น ทุกแอปพลิเคชันที่ใช้มันก็ได้ประโยชน์

ตัวอย่างเช่น ภาษาโปรแกรมระดับสูงเป็น abstraction ที่ซ่อน machine code, CPU register และ system call ส่วน SQL เป็น abstraction ที่ซ่อนโครงสร้างข้อมูลที่ซับซ้อนบนดิสก์และในหน่วยความจำ request ที่แข่งกันจาก client อื่น และความไม่สอดคล้องกันหลัง crash แน่นอนว่าเวลาเขียนโปรแกรมด้วยภาษาระดับสูง เราก็ยังใช้ machine code อยู่ เพียงแต่ไม่ได้ใช้มัน โดยตรง เพราะ abstraction ของภาษาโปรแกรมทำให้เราไม่ต้องคิดถึงมัน

Abstraction สำหรับโค้ดแอปพลิเคชันที่ตั้งใจลดความซับซ้อนของมัน สร้างได้โดยใช้ methodology เช่น design patterns (รูปแบบการออกแบบ) [ 97 ] และ domain-driven design (DDD) (การออกแบบที่ขับเคลื่อนด้วยโดเมน) [ 98 ] หนังสือเล่มนี้ไม่ได้พูดถึง abstraction ที่เฉพาะกับแอปพลิเคชันแบบนั้น แต่พูดถึง abstraction ที่ใช้ได้ทั่วไป ซึ่งคุณใช้เป็นฐานสร้างแอปพลิเคชันได้ เช่น database transaction, index และ event log ถ้าอยากใช้เทคนิคอย่าง DDD ก็ implement ต่อยอดจากพื้นฐานที่อธิบายไว้ในหนังสือเล่มนี้ได้

Evolvability: Making Change Easy (Evolvability: ทำให้การเปลี่ยนแปลงง่าย)

ข้อกำหนดของระบบแทบจะไม่คงที่ตลอดไป มันมักเปลี่ยนอยู่ตลอดเวลา เช่น คุณได้เรียนรู้ข้อเท็จจริงใหม่ เกิด use case ที่ไม่เคยคาดคิด ลำดับความสำคัญทางธุรกิจเปลี่ยน ผู้ใช้ขอฟีเจอร์ใหม่ platform ใหม่มาแทนที่ของเก่า ข้อกำหนดทางกฎหมายหรือข้อบังคับเปลี่ยน หรือการเติบโตของระบบบังคับให้ต้องเปลี่ยนสถาปัตยกรรม เป็นต้น

ในแง่ของกระบวนการขององค์กร Agile working pattern (รูปแบบการทำงานแบบ Agile) เป็นกรอบสำหรับปรับตัวให้เข้ากับการเปลี่ยนแปลง ชุมชน Agile ยังพัฒนาเครื่องมือและกระบวนการทางเทคนิคที่เป็นประโยชน์สำหรับการสร้างซอฟต์แวร์ในสภาพแวดล้อมที่เปลี่ยนบ่อย เช่น test-driven development (TDD) และ refactoring ในหนังสือเล่มนี้ เราจะหาวิธีเพิ่มความคล่องตัวในระดับระบบ ที่ประกอบด้วยหลายแอปพลิเคชันหรือบริการที่มีลักษณะต่างกัน

ความง่ายในการปรับเปลี่ยนระบบข้อมูลให้เข้ากับข้อกำหนดที่เปลี่ยนไปเกี่ยวข้องใกล้ชิดกับความเรียบง่ายและ abstraction ของระบบ ระบบที่ loosely coupled และเรียบง่ายมักปรับเปลี่ยนได้ง่ายกว่าระบบที่ tightly coupled และซับซ้อน เพราะแนวคิดนี้สำคัญ เราจึงใช้คำอีกคำหนึ่งเรียกความคล่องตัวในระดับระบบข้อมูล คือ evolvability (ความสามารถในการพัฒนา) [ 99 ]

ปัจจัยสำคัญอย่างหนึ่งที่ทำให้การเปลี่ยนแปลงในระบบขนาดใหญ่ทำได้ยากคือการย้อนกลับไม่ได้ [ 100 ] ตัวอย่างเช่น สมมติว่าคุณกำลังย้ายจาก database หนึ่งไปอีกตัวหนึ่ง ถ้ากลับไปใช้ระบบเก่าไม่ได้เมื่อระบบใหม่มีปัญหา ความเสี่ยงจะสูงกว่ากรณีที่กลับได้ง่ายมาก ดังนั้นการกระทำที่ย้อนกลับไม่ได้จึงต้องทำอย่างระมัดระวังมาก ส่วนการลดสิ่งที่ย้อนกลับไม่ได้ช่วยเพิ่มความยืดหยุ่น

Summary (สรุป)

ในบทนี้เราได้ดูตัวอย่างของ nonfunctional requirements หลายข้อ ได้แก่ performance, reliability, scalability และ maintainability ผ่านหัวข้อเหล่านี้ เรายังได้รู้จักหลักการและคำศัพท์ที่จะใช้ต่อไปตลอดทั้งเล่ม

เราเริ่มต้นด้วยกรณีศึกษาการ implement home timeline ใน social network ซึ่งแสดงความท้าทายบางอย่างที่เกิดขึ้นเมื่อระบบมี scale ใหญ่ จากนั้นเราพูดถึงวิธีวัด performance (เช่น ใช้ response time percentile) และวัด load ของระบบ (เช่น ใช้ throughput metric) และวิธีที่ metric เหล่านี้ถูกนำไปใช้ใน SLA Scalability เป็นแนวคิดที่เกี่ยวข้องกันอย่างใกล้ชิด คือการทำให้แน่ใจว่า performance ยังคงเดิมเมื่อ load เพิ่มขึ้น เราได้เห็นหลักการทั่วไปบางข้อของ scalability เช่น การแบ่งงานออกเป็นส่วนย่อยที่ทำงานได้อย่างอิสระ และเราจะลงรายละเอียดทางเทคนิคของเทคนิคด้าน scalability ในบทต่อ ๆ ไป

เพื่อให้ได้ reliability คุณใช้เทคนิค fault tolerance ได้ ซึ่งช่วยให้ระบบให้บริการต่อไปได้แม้ส่วนประกอบ (เช่น ดิสก์ เครื่อง หรือบริการอื่น) จะมี fault เราได้เห็นตัวอย่างของ hardware fault ที่เกิดขึ้นได้ และแยกมันจาก software fault ซึ่งอาจจัดการยากกว่า เพราะมักสัมพันธ์กันสูง อีกแง่หนึ่งของการสร้าง reliability คือการทำให้ระบบทนต่อความผิดพลาดของมนุษย์ และเราได้เห็น blameless postmortem เป็นเทคนิคในการเรียนรู้จากเหตุการณ์

สุดท้าย เราได้ดูหลายแง่มุมของ maintainability รวมถึงการช่วยงานของทีม operations การจัดการความซับซ้อน และการทำให้พัฒนาฟังก์ชันของแอปพลิเคชันต่อไปได้ง่ายเมื่อเวลาผ่านไป ไม่มีคำตอบง่าย ๆ ว่าจะบรรลุเป้าหมายเหล่านี้ได้อย่างไร แต่วิธีหนึ่งที่ช่วยได้คือสร้างแอปพลิเคชันจาก building block ที่เป็นที่เข้าใจดีและให้ abstraction ที่เป็นประโยชน์ ส่วนที่เหลือของหนังสือเล่มนี้จะครอบคลุม building block ที่คัดเลือกมา ซึ่งพิสูจน์แล้วว่ามีคุณค่าในทางปฏิบัติ