ในฐานะที่เป็น APM แบบครบวงจรที่เติบโตขึ้นเรื่อย ๆ เราใช้เวลามากมายในการทำให้แน่ใจว่า AppSignal สามารถรับมือกับปริมาณการใช้งานที่เพิ่มขึ้นได้ โดยปกติ เราจะไม่พูดถึงวิธีที่เราทำอย่างนั้น บล็อกของเราเต็มไปด้วยบทความเกี่ยวกับสิ่งที่ยอดเยี่ยมภายใต้ประทุนของ Ruby หรือการทำสิ่งบ้าๆ กับ Elixir แต่ไม่เกี่ยวกับสิ่งที่ทำให้ AppSignal ทำงาน
อย่างไรก็ตาม ในครั้งนี้ เราต้องการแบ่งปันการเปลี่ยนแปลงที่ใหญ่กว่าบางส่วนในกลุ่มของเราที่เราได้ทำขึ้นในช่วงไม่กี่ปีที่ผ่านมา เพื่อให้เราสามารถดำเนินการ (อย่างง่ายดาย) กับคำขอจำนวนสองหลักพันล้านที่ส่งถึงเราทุกเดือน ในเวลาจริง ดังนั้นวันนี้เราใช้ประสบการณ์การปรับขนาดเพื่อหารือเกี่ยวกับสแต็กของเราเองและช่วยคุณในเรื่องนี้
จากการติดตั้งรางมาตรฐานไปจนถึงชิ้นส่วนที่กำหนดเองเพิ่มเติม
AppSignal เริ่มต้นจากการตั้งค่า Rails แบบมาตรฐาน เราใช้แอป Rails ที่รวบรวมข้อมูลผ่านจุดปลาย API ซึ่งสร้างงาน Sidekiq เพื่อประมวลผลในเบื้องหลัง
หลังจากนั้นไม่นาน เราก็ได้เปลี่ยน Rails API ด้วยมิดเดิลแวร์แบบแร็คเพื่อเพิ่มความเร็ว และต่อมาก็แทนที่ด้วยเว็บเซิร์ฟเวอร์ Go ที่ส่งงานที่เข้ากันได้กับ Sidekiq ไปที่ Redis
สถานะแอปและการเพิ่ม/อัปเดต
แม้ว่าการตั้งค่านี้จะทำงานได้ดีมาเป็นเวลานาน เราก็เริ่มพบปัญหาที่ฐานข้อมูลไม่สามารถติดตามจำนวนการสืบค้นที่ดำเนินการได้ ณ จุดนี้ เรากำลังดำเนินการกับคำขอนับหมื่นล้านรายการแล้ว เหตุผลหลักคือแต่ละกระบวนการของ Sidekiq จำเป็นต้องรับสถานะของแอปทั้งหมดจากฐานข้อมูลเพื่อเพิ่มตัวนับที่ถูกต้องและอัปเดตเอกสารที่ถูกต้อง
เราสามารถบรรเทาสิ่งนี้ได้บ้างด้วยการแคชข้อมูลในเครื่อง แต่เนื่องจากลักษณะการตั้งค่าแบบ Round-robin ของเรา ยังคงหมายความว่าแต่ละเซิร์ฟเวอร์จำเป็นต้องมีแคชของข้อมูลทั้งหมด เนื่องจากเราไม่แน่ใจว่าเซิร์ฟเวอร์ใดที่เพย์โหลด จะจบลง เราตระหนักว่าด้วยการเติบโตของข้อมูลที่เราประสบกับการตั้งค่านี้จะเป็นไปไม่ได้ในอนาคต
เข้าสู่คาฟคา
ในการค้นหาวิธีที่ดีกว่าในการจัดการข้อมูล เราจึงตัดสินใจใช้ Kafka เป็นไปป์ไลน์การประมวลผลข้อมูล แทนที่จะรวมเมตริกในฐานข้อมูล ตอนนี้เรารวมเมตริกใน โปรเซสเซอร์ ของ Kafka . เป้าหมายของเราคือไปป์ไลน์ Kafka ของเราไม่เคยสอบถามฐานข้อมูลจนกว่าจะล้างข้อมูลรวม ซึ่งจะทำให้จำนวนการสืบค้นต่อเพย์โหลดลดลงจากการอ่านและเขียนสูงสุดสิบครั้งในการเขียนเพียงครั้งเดียวที่ส่วนท้ายของไปป์ไลน์
เราระบุคีย์สำหรับแต่ละข้อความ Kafka และ Kafka รับประกันว่าคีย์เดียวกันจะสิ้นสุดในพาร์ติชันเดียวกัน ซึ่งถูกใช้โดยเซิร์ฟเวอร์เดียวกัน เราใช้ ID ของแอปเป็นคีย์สำหรับข้อความ ซึ่งหมายความว่าแทนที่จะมีแคชสำหรับลูกค้าทั้งหมดบนเซิร์ฟเวอร์ เราเพียงแค่แคชข้อมูลสำหรับแอปที่เซิร์ฟเวอร์ได้รับจาก Kafka ไม่ใช่ทุกแอป
Kafka เป็นระบบที่ยอดเยี่ยม และเราได้ย้ายเข้ามาในช่วงสองปีที่ผ่านมา ตอนนี้การประมวลผลเกือบทั้งหมดเสร็จสิ้นใน Rust ผ่าน Kafka แต่ยังมีสิ่งอื่นๆ ที่ทำได้ง่ายกว่าใน Ruby เช่น การส่งการแจ้งเตือนและงานที่ต้องใช้ฐานข้อมูลจำนวนมาก ซึ่งหมายความว่าเราต้องการวิธีรับข้อมูลจาก Kafka ไปยังสแต็ก Rails ของเรา
การเชื่อมต่อ Kafka และ Ruby/Rails
เมื่อเราเริ่มการเปลี่ยนแปลงนี้ มี Kafka Ruby gem สองสามตัว แต่ไม่มีใครทำงานกับ Kafka เวอร์ชันล่าสุด (ณ เวลา 0.10.x) และส่วนใหญ่ยังไม่ได้รับการดูแล
เราดูที่การเขียนอัญมณีของเราเอง (ซึ่งในที่สุดเราก็ทำ) เราจะเขียนเพิ่มเติมเกี่ยวกับสิ่งนั้นในบทความอื่น แต่การมีคนขับที่ดีเป็นเพียงส่วนหนึ่งของข้อกำหนดเท่านั้น นอกจากนี้เรายังต้องการระบบเพื่อใช้ข้อมูลและดำเนินงานใน Ruby และสร้างพนักงานใหม่เมื่อตัวเก่าขัดข้อง
ในที่สุด เราก็ได้วิธีแก้ปัญหาที่ต่างออกไป สแต็ก Kafka ของเราสร้างขึ้นใน Rust และเราเขียนไบนารีขนาดเล็กที่ใช้ sidekiq_out
และสร้างงานที่เข้ากันได้กับ Sidekiq ใน Redis วิธีนี้ทำให้เราสามารถปรับใช้ไบนารีนี้บนเครื่องของผู้ปฏิบัติงานของเรา และมันจะป้อนงานใหม่เข้าสู่ Sidekiq เช่นเดียวกับที่คุณทำใน Rails เอง
ไบนารีมีตัวเลือกสองสามอย่าง เช่น การจำกัดปริมาณข้อมูลใน Redis เพื่อหยุดการใช้หัวข้อ Kafka จนกว่าขีดจำกัดจะถูกล้าง ด้วยวิธีนี้ ข้อมูลทั้งหมดจาก Kafka จะไม่จบลงในหน่วยความจำของ Redis เกี่ยวกับคนงานหากมีงานในมือ
จากมุมมองของ Ruby งานที่สร้างใน Rails กับงานที่มาจาก Kafka ไม่แตกต่างกันเลย ช่วยให้เราสามารถสร้างต้นแบบพนักงานใหม่ที่ได้รับข้อมูลจาก Kafka และประมวลผลใน Rails เพื่อส่งการแจ้งเตือนและอัปเดตฐานข้อมูลโดยไม่ต้องรู้อะไรเกี่ยวกับ Kafka
ทำให้การโยกย้ายไปยัง Kafka ง่ายขึ้น เนื่องจากเราสามารถสลับไปใช้ Kafka และย้อนกลับได้โดยไม่ต้องปรับใช้รหัส Ruby ใหม่ นอกจากนี้ยังทำให้การทดสอบง่ายสุดๆ เนื่องจากคุณสามารถสร้างงานในชุดทดสอบเพื่อให้ Ruby ใช้งานได้โดยไม่ต้องตั้งค่า Kafka stack ทั้งหมดในเครื่อง
เราใช้ Protobuf เพื่อกำหนดข้อความ (ภายใน) ทั้งหมดของเรา ด้วยวิธีนี้เราจึงมั่นใจได้ว่าหากการทดสอบผ่าน ผู้ปฏิบัติงานจะประมวลผลงานจาก Kafka ได้อย่างถูกต้อง
ในที่สุด โซลูชันนี้ช่วยเราประหยัดเวลาและพลังงานได้มาก และทำให้ชีวิตง่ายขึ้นมากสำหรับทีม Ruby ของเรา
ข้อดีข้อเสีย
เช่นเดียวกับทุกสิ่ง มีข้อดีและข้อเสียบางประการสำหรับการตั้งค่านี้:
ข้อดี:
- ไม่จำเป็นต้องเปลี่ยนแปลง Ruby, รองรับ API
- ปรับใช้และเปลี่ยนกลับได้ง่าย
- สลับระหว่าง Kafka และ Ruby ได้ง่าย
- ข้อความจะไม่โอเวอร์โหลด Redis เมื่อใช้ตัวจำกัด ซึ่งจะบันทึกหน่วยความจำบนเซิร์ฟเวอร์ โดยเก็บข้อความไว้ใน Kafka แทน
- การปรับขนาดในแนวนอนทำให้เกิดแคชที่เล็กลงในแต่ละเซิร์ฟเวอร์ เนื่องจากข้อความที่มีคีย์
ข้อเสีย:
- ยังคงมีปัญหาที่แต่ละเธรด Sidekiq ต้องการการเข้าถึงแคชของข้อมูลทั้งหมดสำหรับแอปจากพาร์ติชั่นที่เซิร์ฟเวอร์ใช้ (เช่น Memcache)
- แยกกระบวนการทำงานบนเซิร์ฟเวอร์
- ตัวประมวลผลสนิมยอมรับการชดเชยข้อความเมื่อข้อความถูกล้างไปยัง Redis ซึ่งหมายความว่าจะรับประกันว่าจะอยู่ใน Redis แต่ไม่มีการรับประกันว่าข้อความจะได้รับการประมวลผลโดย Ruby ซึ่งหมายความว่าในกรณีที่เซิร์ฟเวอร์ขัดข้อง จะมี เป็นโอกาสที่บางข้อความที่อยู่ใน Redis แต่ไม่ได้รับการประมวลผลจะไม่ได้รับการประมวลผล
ซิเดกิกและคาฟคา
การใช้ Sidekiq ช่วยเราได้อย่างมากในขณะที่ย้ายไปป์ไลน์การประมวลผลไปยัง Kafka ตอนนี้เราได้ย้ายออกจาก Sidekiq ไปเกือบหมดแล้ว และกำลังจัดการทุกอย่างผ่านไดรเวอร์ Kafka โดยตรง แต่สำหรับบทความอื่น
ตอนจบที่มีความสุขนี้สรุปเรื่องราวความรัก เราหวังว่าคุณจะชอบมุมมองด้านประสิทธิภาพและการปรับขนาด และประสบการณ์ของเราในการปรับขนาด AppSignal เราหวังว่าเรื่องราวนี้เกี่ยวกับการตัดสินใจของเราเกี่ยวกับสแต็กของเราจะช่วยคุณได้
ตรวจสอบส่วนที่เหลือของบล็อกหรือติดตามเราเพื่อติดตามเมื่อตอนต่อไปเกี่ยวกับการตั้งค่า Kafka ของเราได้รับการเผยแพร่ และถ้าคุณกำลังมองหา APM แบบครบวงจรที่นักพัฒนาสำหรับนักพัฒนาอย่างแท้จริง มาหาเราสิ