นักเรียนห้าคนและร้านค้าคีย์-ค่าสามแห่งที่กระจาย

หรือวิธีที่เราเขียนไลบรารี่ไคลเอ็นต์ C++ สำหรับ ZooKeeper, etcd และ Consul KV

ในโลกของระบบแบบกระจาย มีงานทั่วไปหลายประการ: การจัดเก็บข้อมูลเกี่ยวกับองค์ประกอบของคลัสเตอร์ การจัดการการกำหนดค่าของโหนด การตรวจจับโหนดที่ผิดพลาด การเลือกผู้นำ อื่น ๆ. เพื่อแก้ไขปัญหาเหล่านี้จึงมีการสร้างระบบกระจายพิเศษ - บริการประสานงาน ตอนนี้เราจะสนใจสามรายการ: 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"). แต่สิ่งนี้จะส่งคืนแผนผังย่อยทั้งหมดของคีย์ ซึ่งอาจไม่เป็นที่ยอมรับหากทรีย่อยมีขนาดใหญ่ ในตอนแรก เราวางแผนที่จะใช้กลไกการแปลที่สำคัญ ดำเนินการใน zetcd. มันเกี่ยวข้องกับการเพิ่มหนึ่งไบต์ที่จุดเริ่มต้นของคีย์ ซึ่งเท่ากับความลึกของโหนดในแผนผัง ผมขอยกตัวอย่างให้คุณฟัง

"/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++ซึ่งในที่สุดก็ไม่สามารถคอมไพล์บน Windows ได้ อย่างไรก็ตาม สิ่งนี้ก็ไม่น่าแปลกใจ: ไลบรารี่ถูกวางตำแหน่งเป็นลินุกซ์เท่านั้น สำหรับกงสุล ทางเลือกเดียวคือ พีคอนซูล. จะต้องเพิ่มการสนับสนุนเข้าไป เซสชั่น и การทำธุรกรรม. สำหรับ etcd ไม่พบไลบรารี่ที่รองรับโปรโตคอลเวอร์ชันล่าสุด ดังนั้นเราจึงทำเพียงเท่านั้น สร้างไคลเอนต์ grpc.

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

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

เราใช้เธรดพูลเดียวกันเพื่อดำเนินการค้นหาไปยัง etcd และกงสุล ซึ่งหมายความว่าไลบรารีพื้นฐานสามารถเข้าถึงได้โดยเธรดที่แตกต่างกันหลายตัว ppconsul ไม่ปลอดภัยสำหรับเธรด ดังนั้นการโทรถึงมันจึงได้รับการปกป้องด้วยการล็อค
คุณสามารถทำงานกับ grpc จากหลายเธรดได้ แต่มีรายละเอียดปลีกย่อย ในนาฬิกา etcd ถูกนำมาใช้ผ่านสตรีม grpc เหล่านี้เป็นช่องทางสองทิศทางสำหรับข้อความบางประเภท ไลบรารีจะสร้างเธรดเดียวสำหรับนาฬิกาทั้งหมดและเธรดเดียวที่ประมวลผลข้อความขาเข้า ดังนั้น grpc จึงห้ามไม่ให้เขียนแบบขนานเพื่อสตรีม ซึ่งหมายความว่าเมื่อเริ่มต้นหรือลบนาฬิกา คุณต้องรอจนกว่าคำขอก่อนหน้าจะเสร็จสิ้นการส่งก่อนที่จะส่งคำขอถัดไป เราใช้สำหรับการซิงโครไนซ์ ตัวแปรตามเงื่อนไข.

ทั้งหมด

ดูตัวคุณเอง: liboffkv.

ทีมงานของเรา: รีด โรมานอฟ, อีวาน กลูเชนคอฟ, มิทรี คามัลดินอฟ, วิคเตอร์ คราปิเวนสกี้, วิตาลี อิวานิน.

ที่มา: will.com

เพิ่มความคิดเห็น