หรือวิธีที่เราเขียนไลบรารี่ไคลเอ็นต์ C++ สำหรับ ZooKeeper, etcd และ Consul KV
ในโลกของระบบแบบกระจาย มีงานทั่วไปหลายประการ: การจัดเก็บข้อมูลเกี่ยวกับองค์ประกอบของคลัสเตอร์ การจัดการการกำหนดค่าของโหนด การตรวจจับโหนดที่ผิดพลาด การเลือกผู้นำ
โดยพื้นฐานแล้ว ระบบทั้งหมดเหล่านี้เป็นที่เก็บคีย์-ค่าที่ทนทานต่อข้อผิดพลาดและเป็นเส้นตรงได้ แม้ว่าแบบจำลองข้อมูลจะมีความแตกต่างอย่างมีนัยสำคัญ ซึ่งเราจะกล่าวถึงในภายหลัง แต่ก็สามารถแก้ไขปัญหาในทางปฏิบัติที่เหมือนกันได้ แน่นอนว่าแต่ละแอปพลิเคชันที่ใช้บริการประสานงานจะเชื่อมโยงกับหนึ่งในนั้น ซึ่งอาจนำไปสู่ความจำเป็นในการรองรับระบบหลายระบบในศูนย์ข้อมูลเดียว ซึ่งช่วยแก้ปัญหาเดียวกันสำหรับแอปพลิเคชันที่แตกต่างกัน
แนวคิดในการแก้ปัญหานี้เริ่มต้นจากหน่วยงานที่ปรึกษาของออสเตรเลีย และตกเป็นหน้าที่ของเราซึ่งเป็นกลุ่มนักเรียนเล็กๆ น้อยๆ ที่จะนำไปปฏิบัติ ซึ่งนั่นคือสิ่งที่ฉันจะพูดถึง
เราจัดการเพื่อสร้างไลบรารีที่มีอินเทอร์เฟซทั่วไปสำหรับการทำงานกับ ZooKeeper, etcd และ Consul KV ไลบรารีนี้เขียนด้วยภาษา C++ แต่มีแผนที่จะย้ายไปยังภาษาอื่น
โมเดลข้อมูล
ในการพัฒนาอินเทอร์เฟซทั่วไปสำหรับระบบที่แตกต่างกันสามระบบ คุณต้องเข้าใจว่าระบบเหล่านี้มีอะไรเหมือนกันและมีความแตกต่างกันอย่างไร ลองคิดดูสิ
ZooKeeper
คีย์ถูกจัดระเบียบเป็นแผนผังและเรียกว่าโหนด ดังนั้น สำหรับโหนด คุณสามารถรับรายการลูกๆ ของมันได้ การดำเนินการสร้าง znode (สร้าง) และการเปลี่ยนแปลงค่า (setData) จะถูกแยกออกจากกัน: เฉพาะคีย์ที่มีอยู่เท่านั้นที่สามารถอ่านและเปลี่ยนแปลงได้ นาฬิกาสามารถแนบไปกับการดำเนินการตรวจสอบการมีอยู่ของโหนด การอ่านค่า และรับรายการลูก Watch เป็นทริกเกอร์ที่เกิดขึ้นเพียงครั้งเดียวซึ่งจะเริ่มทำงานเมื่อเวอร์ชันของข้อมูลที่เกี่ยวข้องบนเซิร์ฟเวอร์มีการเปลี่ยนแปลง โหนดชั่วคราวใช้เพื่อตรวจจับความล้มเหลว พวกเขาจะเชื่อมโยงกับเซสชันของลูกค้าที่สร้างพวกเขา เมื่อไคลเอนต์ปิดเซสชั่นหรือหยุดแจ้งเตือน ZooKeeper เกี่ยวกับการมีอยู่ของมัน โหนดเหล่านี้จะถูกลบโดยอัตโนมัติ รองรับธุรกรรมแบบง่าย - ชุดของการดำเนินการที่สำเร็จหรือล้มเหลวทั้งหมด หากไม่สามารถทำได้อย่างน้อยหนึ่งรายการ
ฯลฯ
ผู้พัฒนาระบบนี้ได้รับแรงบันดาลใจมาจาก ZooKeeper อย่างชัดเจน ดังนั้นทุกอย่างจึงแตกต่างออกไป ไม่มีลำดับชั้นของคีย์ แต่สร้างชุดที่เรียงลำดับตามพจนานุกรม คุณสามารถรับหรือลบคีย์ทั้งหมดที่อยู่ในช่วงที่กำหนดได้ โครงสร้างนี้อาจดูแปลก แต่จริงๆ แล้วมีความหมายมาก และสามารถจำลองมุมมองแบบลำดับชั้นได้อย่างง่ายดาย
etcd ไม่มีการดำเนินการเปรียบเทียบและตั้งค่ามาตรฐาน แต่มีสิ่งที่ดีกว่า: ธุรกรรม แน่นอนว่ามันมีอยู่ในทั้งสามระบบ แต่ธุรกรรม ฯลฯ นั้นดีเป็นพิเศษ ประกอบด้วยสามช่วงตึก: ตรวจสอบ สำเร็จ ล้มเหลว บล็อกแรกประกอบด้วยชุดเงื่อนไข การดำเนินการที่สองและสาม ธุรกรรมจะดำเนินการแบบอะตอมมิก หากเงื่อนไขทั้งหมดเป็นจริง บล็อกความสำเร็จจะถูกดำเนินการ มิฉะนั้น บล็อกความล้มเหลวจะถูกดำเนินการ ใน API 3.3 บล็อกความสำเร็จและความล้มเหลวอาจมีธุรกรรมที่ซ้อนกันได้ นั่นคือมันเป็นไปได้ที่จะดำเนินการโครงสร้างแบบมีเงื่อนไขของระดับการซ้อนโดยพลการเกือบทั้งหมด คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับการตรวจสอบและการดำเนินการที่มีอยู่ได้
นาฬิกาก็มีอยู่ที่นี่เช่นกัน แม้ว่าจะซับซ้อนกว่าเล็กน้อยและสามารถนำกลับมาใช้ใหม่ได้ก็ตาม นั่นคือหลังจากติดตั้งนาฬิกาในช่วงคีย์แล้ว คุณจะได้รับการอัปเดตทั้งหมดในช่วงนี้จนกว่าคุณจะยกเลิกนาฬิกา ไม่ใช่เฉพาะรุ่นแรกเท่านั้น ในรูปแบบอื่น ๆ อะนาล็อกของเซสชันไคลเอนต์ ZooKeeper ถือเป็นการเช่า
กงสุล เค.วี.
นอกจากนี้ยังไม่มีโครงสร้างลำดับชั้นที่เข้มงวดที่นี่ แต่กงสุลสามารถสร้างลักษณะที่ปรากฏ: คุณสามารถรับและลบคีย์ทั้งหมดด้วยคำนำหน้าที่ระบุนั่นคือทำงานกับ "แผนผังย่อย" ของคีย์ ข้อความค้นหาดังกล่าวเรียกว่าการเรียกซ้ำ นอกจากนี้ กงสุลสามารถเลือกเฉพาะคีย์ที่ไม่มีอักขระที่ระบุหลังคำนำหน้า ซึ่งสอดคล้องกับการรับ "ลูก" ทันที แต่ควรจำไว้ว่านี่คือลักษณะที่ปรากฏของโครงสร้างแบบลำดับชั้น: ค่อนข้างเป็นไปได้ที่จะสร้างคีย์หากไม่มีพาเรนต์หรือลบคีย์ที่มีลูกในขณะที่ลูก ๆ จะถูกเก็บไว้ในระบบต่อไป
แทนที่จะเฝ้าดู กงสุลกลับบล็อกคำขอ HTTP โดยพื้นฐานแล้วสิ่งเหล่านี้เป็นการเรียกใช้วิธีการอ่านข้อมูลแบบธรรมดาซึ่งมีการระบุข้อมูลเวอร์ชันล่าสุดที่ทราบพร้อมกับพารามิเตอร์อื่น ๆ หากเวอร์ชันปัจจุบันของข้อมูลที่เกี่ยวข้องบนเซิร์ฟเวอร์มากกว่าเวอร์ชันที่ระบุ การตอบสนองจะถูกส่งกลับทันที ไม่เช่นนั้น - เมื่อค่าเปลี่ยนแปลง นอกจากนี้ยังมีเซสชันที่สามารถแนบไปกับคีย์ได้ตลอดเวลา เป็นที่น่าสังเกตว่าต่างจาก etcd และ ZooKeeper ที่การลบเซสชันนำไปสู่การลบคีย์ที่เกี่ยวข้อง มีโหมดที่เซสชันจะถูกยกเลิกการเชื่อมโยงจากเซสชันเหล่านั้น มีอยู่
วางมันทั้งหมดเข้าด้วยกัน
ZooKeeper มีโมเดลข้อมูลที่เข้มงวดที่สุด การสืบค้นช่วงนิพจน์ที่มีอยู่ใน etcd ไม่สามารถจำลองได้อย่างมีประสิทธิภาพใน ZooKeeper หรือ Consul ด้วยความพยายามที่จะรวมสิ่งที่ดีที่สุดจากบริการทั้งหมด เราจึงได้อินเทอร์เฟซที่เกือบจะเทียบเท่ากับอินเทอร์เฟซ ZooKeeper โดยมีข้อยกเว้นที่สำคัญดังต่อไปนี้:
ลำดับ คอนเทนเนอร์ และโหนด TTL ไม่รองรับ- ไม่รองรับ ACL
- เมธอด set จะสร้างคีย์หากไม่มีอยู่ (ใน ZK setData ส่งคืนข้อผิดพลาดในกรณีนี้)
- วิธี set และ cas ถูกแยกออกจากกัน (ใน ZK โดยพื้นฐานแล้วเป็นสิ่งเดียวกัน)
- วิธีการลบจะลบโหนดพร้อมกับแผนผังย่อย (ใน ZK Delete จะส่งคืนข้อผิดพลาดหากโหนดมีลูก)
- สำหรับแต่ละคีย์จะมีเพียงเวอร์ชันเดียว - เวอร์ชันค่า (ใน ZK
มีสามคน )
การปฏิเสธโหนดตามลำดับนั้นเกิดจากการที่ etcd และกงสุลไม่มีการสนับสนุนในตัว และผู้ใช้สามารถนำไปใช้งานได้อย่างง่ายดายบนอินเทอร์เฟซไลบรารีผลลัพธ์
การใช้พฤติกรรมที่คล้ายกับ ZooKeeper เมื่อลบจุดยอดจะต้องรักษาตัวนับลูกแยกต่างหากสำหรับแต่ละคีย์ใน etcd และกงสุล เนื่องจากเราพยายามหลีกเลี่ยงการจัดเก็บข้อมูลเมตา จึงตัดสินใจลบแผนผังย่อยทั้งหมด
รายละเอียดปลีกย่อยของการดำเนินการ
มาดูบางแง่มุมของการนำไลบรารีอินเตอร์เฟสไปใช้งานในระบบต่างๆ กันดีกว่า
ลำดับชั้นใน ฯลฯ
การรักษามุมมองแบบลำดับชั้นใน etcd กลายเป็นงานที่น่าสนใจที่สุดงานหนึ่ง การสืบค้นแบบช่วงช่วยให้เรียกค้นรายการคีย์ที่มีคำนำหน้าที่ระบุได้ง่าย เช่น หากคุณต้องการทุกสิ่งที่ขึ้นต้นด้วย "/foo"
คุณกำลังขอช่วง ["/foo", "/fop")
. แต่สิ่งนี้จะส่งคืนแผนผังย่อยทั้งหมดของคีย์ ซึ่งอาจไม่เป็นที่ยอมรับหากทรีย่อยมีขนาดใหญ่ ในตอนแรก เราวางแผนที่จะใช้กลไกการแปลที่สำคัญ
"/foo" -> "u01/foo"
"/foo/bar" -> "u02/foo/bar"
จากนั้นให้นำลูกกุญแจทั้งหมดไปทันที "/foo"
เป็นไปได้โดยการขอช่วง ["u02/foo/", "u02/foo0")
. ใช่ ในรูปแบบ ASCII "0"
ยืนอยู่หลังจากนั้น "/"
.
แต่จะดำเนินการลบจุดยอดในกรณีนี้ได้อย่างไร? ปรากฎว่าคุณต้องลบช่วงประเภททั้งหมด ["uXX/foo/", "uXX/foo0")
สำหรับ XX ตั้งแต่ 01 ถึง FF แล้วเราก็วิ่งเข้ามา
เป็นผลให้มีการคิดค้นระบบการแปลงคีย์แบบง่าย ๆ ซึ่งทำให้สามารถดำเนินการทั้งการลบคีย์และรับรายการลูกได้อย่างมีประสิทธิภาพ การเพิ่มอักขระพิเศษก่อนโทเค็นสุดท้ายก็เพียงพอแล้ว ตัวอย่างเช่น:
"/very" -> "/u00very"
"/very/long" -> "/very/u00long"
"/very/long/path" -> "/very/long/u00path"
จากนั้นทำการลบกุญแจ "/very"
กลายเป็นการลบล้าง "/u00very"
และช่วง ["/very/", "/very0")
และรับลูกทั้งหมด - เพื่อขอกุญแจจากระยะ ["/very/u00", "/very/u01")
.
การลบคีย์ใน ZooKeeper
ดังที่ฉันได้กล่าวไปแล้ว ใน ZooKeeper คุณไม่สามารถลบโหนดได้หากมีลูก ๆ เราต้องการลบคีย์พร้อมกับทรีย่อย ฉันควรทำอย่างไรดี? เราทำสิ่งนี้ด้วยการมองโลกในแง่ดี ขั้นแรก เราจะสำรวจแผนผังย่อยแบบวนซ้ำ โดยรับลูกของแต่ละจุดยอดด้วยแบบสอบถามแยกกัน จากนั้นเราสร้างธุรกรรมที่พยายามลบโหนดทั้งหมดของแผนผังย่อยตามลำดับที่ถูกต้อง แน่นอนว่า การเปลี่ยนแปลงสามารถเกิดขึ้นได้ระหว่างการอ่านแผนผังย่อยและการลบแผนผังย่อย ในกรณีนี้ ธุรกรรมจะล้มเหลว นอกจากนี้ ทรีย่อยอาจเปลี่ยนแปลงในระหว่างกระบวนการอ่าน การร้องขอลูกของโหนดถัดไปอาจส่งคืนข้อผิดพลาด ตัวอย่างเช่น โหนดนี้ถูกลบไปแล้ว ในทั้งสองกรณี เราจะทำซ้ำขั้นตอนทั้งหมดอีกครั้ง
แนวทางนี้ทำให้การลบคีย์ไม่มีประสิทธิภาพมากนักหากมีลูก และยิ่งไปกว่านั้นหากแอปพลิเคชันยังคงทำงานกับแผนผังย่อย การลบและสร้างคีย์ อย่างไรก็ตาม สิ่งนี้ช่วยให้เราหลีกเลี่ยงการใช้วิธีอื่นที่ซับซ้อนใน etcd และกงสุลได้
ตั้งไว้ใน ZooKeeper
ใน ZooKeeper มีวิธีการแยกต่างหากที่ทำงานกับโครงสร้างแบบต้นไม้ (สร้าง, ลบ, getChildren) และที่ทำงานกับข้อมูลในโหนด (setData, getData) ยิ่งกว่านั้น วิธีการทั้งหมดมีเงื่อนไขเบื้องต้นที่เข้มงวด: create จะส่งกลับข้อผิดพลาดหากโหนดมีอยู่แล้ว ถูกสร้าง ลบ หรือ setData – หากไม่มีอยู่แล้ว เราต้องการวิธีการแบบตั้งค่าที่สามารถเรียกได้โดยไม่ต้องคำนึงถึงการมีคีย์
ทางเลือกหนึ่งคือใช้แนวทางในแง่ดี เช่นเดียวกับการลบออก ตรวจสอบว่ามีโหนดอยู่หรือไม่ หากมีอยู่ ให้เรียก setData หรือสร้างใหม่ หากวิธีสุดท้ายแสดงข้อผิดพลาด ให้ทำซ้ำทั้งหมดอีกครั้ง สิ่งแรกที่ควรทราบก็คือการทดสอบการดำรงอยู่นั้นไม่มีจุดหมาย คุณสามารถเรียกสร้างได้ทันที การทำสำเร็จจะหมายความว่าไม่มีโหนดและถูกสร้างขึ้น มิฉะนั้น create จะส่งกลับข้อผิดพลาดที่เหมาะสม หลังจากนั้นคุณจะต้องเรียก setData แน่นอนว่าระหว่างการโทร จุดยอดสามารถลบออกได้โดยการเรียกที่แข่งขันกัน และ setData ก็จะส่งกลับข้อผิดพลาดเช่นกัน ในกรณีนี้คุณสามารถทำซ้ำทั้งหมดได้ แต่จะคุ้มไหม?
หากทั้งสองวิธีส่งคืนข้อผิดพลาด เราจะทราบแน่นอนว่าการลบที่แข่งขันกันเกิดขึ้น ลองจินตนาการว่าการลบนี้เกิดขึ้นหลังจากการเรียกชุด แล้วความหมายใดก็ตามที่เราพยายามจะสร้างขึ้นนั้นก็ถูกลบออกไปแล้ว ซึ่งหมายความว่าเราสามารถสรุปได้ว่าชุดนั้นดำเนินการได้สำเร็จ แม้ว่าจริงๆ แล้วไม่ได้เขียนอะไรเลยก็ตาม
รายละเอียดทางเทคนิคเพิ่มเติม
ในส่วนนี้เราจะพักจากระบบแบบกระจายและพูดคุยเกี่ยวกับการเขียนโค้ด
ข้อกำหนดหลักประการหนึ่งของลูกค้าคือข้ามแพลตฟอร์ม: ต้องรองรับบริการอย่างน้อยหนึ่งรายการบน Linux, MacOS และ Windows ในตอนแรก เราพัฒนาสำหรับ Linux เท่านั้น และเริ่มการทดสอบบนระบบอื่นๆ ในภายหลัง สิ่งนี้ทำให้เกิดปัญหามากมายซึ่งในบางครั้งยังไม่ชัดเจนว่าจะเข้าใกล้อย่างไร ด้วยเหตุนี้ บริการประสานงานทั้งสามบริการจึงได้รับการสนับสนุนบน Linux และ MacOS ในขณะที่ Consul KV เท่านั้นที่รองรับบน Windows
จากจุดเริ่มต้น เราพยายามใช้ไลบรารี่สำเร็จรูปเพื่อเข้าถึงบริการต่างๆ ในกรณีของ ZooKeeper ตัวเลือกนั้นตกอยู่
ด้วยแรงบันดาลใจจากอินเทอร์เฟซแบบอะซิงโครนัสของไลบรารี ZooKeeper C++ เราจึงตัดสินใจใช้อินเทอร์เฟซแบบอะซิงโครนัสด้วย ZooKeeper C++ ใช้พื้นฐานในอนาคต/สัญญาสำหรับสิ่งนี้ น่าเสียดายที่ใน STL มีการใช้งานเพียงเล็กน้อย ตัวอย่างเช่นไม่มี
การใช้งานของเรานั้นมีลักษณะเช่นนี้ เมื่อเรียก จะมีการสร้างคู่สัญญา/อนาคตเพิ่มเติม อนาคตใหม่จะถูกส่งคืน และอันที่ผ่านไปจะถูกวางไว้พร้อมกับฟังก์ชันที่เกี่ยวข้องและสัญญาเพิ่มเติมในคิว เธรดจากพูลเลือกหลายฟิวเจอร์สจากคิวและสำรวจโดยใช้ wait_for เมื่อผลลัพธ์พร้อมใช้งาน ฟังก์ชันที่เกี่ยวข้องจะถูกเรียกใช้และค่าที่ส่งคืนจะถูกส่งผ่านไปยังสัญญา
เราใช้เธรดพูลเดียวกันเพื่อดำเนินการค้นหาไปยัง etcd และกงสุล ซึ่งหมายความว่าไลบรารีพื้นฐานสามารถเข้าถึงได้โดยเธรดที่แตกต่างกันหลายตัว ppconsul ไม่ปลอดภัยสำหรับเธรด ดังนั้นการโทรถึงมันจึงได้รับการปกป้องด้วยการล็อค
คุณสามารถทำงานกับ grpc จากหลายเธรดได้ แต่มีรายละเอียดปลีกย่อย ในนาฬิกา etcd ถูกนำมาใช้ผ่านสตรีม grpc เหล่านี้เป็นช่องทางสองทิศทางสำหรับข้อความบางประเภท ไลบรารีจะสร้างเธรดเดียวสำหรับนาฬิกาทั้งหมดและเธรดเดียวที่ประมวลผลข้อความขาเข้า ดังนั้น grpc จึงห้ามไม่ให้เขียนแบบขนานเพื่อสตรีม ซึ่งหมายความว่าเมื่อเริ่มต้นหรือลบนาฬิกา คุณต้องรอจนกว่าคำขอก่อนหน้าจะเสร็จสิ้นการส่งก่อนที่จะส่งคำขอถัดไป เราใช้สำหรับการซิงโครไนซ์
ทั้งหมด
ดูตัวคุณเอง:
ทีมงานของเรา:
ที่มา: will.com