บทความเพื่อทำความเข้าใจกลไกการเร่งสแน็ปช็อตของไคลเอ็นต์ Geth
หมายเหตุบรรณาธิการ: บทความนี้มาจากผู้ที่ชื่นชอบ Ethereum (ID: ethfans)หมายเหตุบรรณาธิการ: บทความนี้มาจาก
ผู้ที่ชื่นชอบ Ethereum (ID: ethfans)
ผู้ที่ชื่นชอบ Ethereum (ID: ethfans)
, ผู้แต่ง: Péter Szilágyi, แปล: A Jian, พิมพ์ซ้ำโดย Odaily โดยได้รับอนุญาต
บทความนี้เป็นบทความแรกในชุด Geth Client Answers คุณสามารถถามคำถามเกี่ยวกับลูกค้า Geth ได้ ฉันจะใช้บทความเล็กๆ ทุกสัปดาห์เพื่อตอบคำถามที่ได้รับการโหวตมากที่สุด คำถามที่ได้รับการร้องขอมากที่สุดในสัปดาห์นี้คือ: คุณสามารถพูดคุยเกี่ยวกับความแตกต่างหลักระหว่างโครงสร้างฐานข้อมูลแบบแฟลตและโครงสร้างแบบดั้งเดิมได้หรือไม่?
สถานะของ Ethereum
ก่อนที่จะเจาะลึกโครงสร้างการเร่งความเร็ว (โครงสร้างการเร่งความเร็ว) เรามาทบทวนแนวคิดของ "สถานะ" ใน Ethereum และวิธีการจัดเก็บสถานะเมื่อพูดถึงระดับต่างๆ ของสิ่งที่เป็นนามธรรม
Ethereum มีสถานะที่แตกต่างกันสองประเภท: การรวบรวมบัญชี และ การรวบรวมสล็อตบัญชีต่อสัญญา จากมุมมองที่เป็นนามธรรม ข้อมูลทั้งสองชนิดเป็นคู่คีย์-ค่า การรวบรวมบัญชีจะจับคู่ที่อยู่กับที่อยู่นั้น 'nonce ยอดคงเหลือ ฯลฯ ช่องเก็บข้อมูลของสัญญาแมปค่าใด ๆ (กำหนดและใช้โดยสัญญา) กับค่าบางอย่าง
น่าเสียดายที่ในขณะที่จัดเก็บคู่คีย์-ค่าเหล่านี้เป็นข้อมูลแบบแฟลตอาจมีประสิทธิภาพมาก การตรวจสอบความถูกต้องของคู่คีย์-ค่าจะกลายเป็นเรื่องยากในการคำนวณ เมื่อใดก็ตามที่มีการแก้ไขข้อมูล เราจะต้องแฮชข้อมูลทั้งหมดจากล่างขึ้นบน
เพื่อหลีกเลี่ยงความจำเป็นในการแฮชฐานข้อมูลทั้งหมด เราสามารถแยกฐานข้อมูลออกเป็นส่วนๆ ที่อยู่ติดกันและสร้างโครงสร้างแบบต้นไม้ได้! ข้อมูลที่เป็นต้นฉบับและมีประโยชน์ที่สุดจะถูกวางไว้บนโหนดปลายสุด และแต่ละโหนดภายในบนแผนผังคือค่าแฮชของเนื้อหาด้านล่างโหนด ด้วยวิธีนี้ เมื่อเราต้องการแก้ไขค่าบางอย่าง เราจำเป็นต้องทำการดำเนินการแฮชของเลขลอการิทึมเท่านั้น โครงสร้างข้อมูลประเภทนี้มีชื่อที่รู้จักกันดีคือ "Merkel tree"
แต่ยังไม่จบ วิธีนี้ยังขาดความซับซ้อนในการคำนวณ แม้ว่าโครงสร้าง Merkle tree จะมีประสิทธิภาพมากเมื่อแก้ไขข้อมูลที่มีอยู่ หากการแทรกและการลบข้อมูลเปลี่ยนขอบเขตของบล็อคข้อมูลขนาดเล็กที่อยู่ด้านล่าง จะทำให้ค่าแฮชที่คำนวณได้ทั้งหมดเป็นโมฆะ
ในขณะนี้ แทนที่จะจัดกลุ่มฐานข้อมูลสุ่มสี่สุ่มห้า เราสามารถใช้คีย์เองเพื่อจัดระเบียบข้อมูล และจัดเรียงข้อมูลให้อยู่ในรูปแบบต้นไม้ตามคำนำหน้าทั่วไป! ด้วยวิธีนี้ การดำเนินการทั้งการแทรกและการลบจะไม่ส่งผลกระทบต่อโหนดทั้งหมด แต่จะส่งผลต่อโหนด (ลอการิทึม) บนเส้นทางจากรากของต้นไม้ไปยังใบไม้เท่านั้น โครงสร้างข้อมูลนี้เรียกว่า "Patricia tree"
การรวมสองวิธีข้างต้น - ลำดับชั้นของต้นไม้ Patricia และอัลกอริทึมแฮชของต้นไม้ Merkle - เป็นสิ่งที่เรียกว่า "ต้นไม้ Merkel-Patricia" ซึ่งใช้ในทางปฏิบัติ โครงสร้างข้อมูลที่แสดงถึงสถานะของ Ethereum ไม่ว่าจะเป็นการแก้ไข การแทรก การลบ หรือการตรวจสอบ มีเพียงลอการิทึมที่ซับซ้อนเท่านั้น! ข้อยกเว้นเล็กน้อยเพียงอย่างเดียวคือคีย์บางคีย์จะถูกแฮชก่อนการแทรกเพื่อให้การลองสมดุล
ที่เก็บข้อมูลสถานะของ Ethereum
ข้างต้นอธิบายว่าทำไม Ethereum จึงใช้โครงสร้างต้นไม้ Merkle Patricia เพื่อจัดเก็บสถานะของมัน น่าเสียดายที่ในขณะที่การดำเนินการที่จำเป็นนั้นรวดเร็ว แต่ละตัวเลือกมาพร้อมกับการเสียสละ ความซับซ้อนของลอการิทึมของการดำเนินการอัพเดตและตรวจสอบหมายความว่าการอ่านและการจัดเก็บสำหรับแต่ละคีย์นั้นเป็นลอการิทึม (การอ่านแบบลอการิทึมและการจัดเก็บลอการิทึม) นี่เป็นเพราะแต่ละโหนดภายในของโครงสร้างต้นไม้ต้องถูกจัดเก็บแยกกันบนฮาร์ดดิสก์
ในขณะนี้ ฉันไม่ทราบแน่ชัดว่าโครงสร้างบัญชีมีความลึกเพียงใด แต่เมื่อประมาณปีที่แล้ว สถานะบัญชีได้เติมเต็มโครงสร้างบัญชี 7 ระดับ ซึ่งหมายความว่าทุกการทำงานของทรี (เช่น ยอดการอ่าน การเขียนไม่มี) สัมผัสกับโหนดภายในอย่างน้อย 7-8 โหนด ดังนั้นจึงเข้าถึงฐานข้อมูลถาวรอย่างน้อย 7-8 โหนด LevelDB จัดระเบียบข้อมูลได้ถึง 7 ชั้น ดังนั้นจึงมีตัวคูณเพิ่มเติม ผลลัพธ์สุดท้ายคือการเข้าถึงสถานะเดียวคาดว่าจะขยายการเข้าถึงดิสก์แบบสุ่ม 25-50 รายการ คุณคูณค่านั้นด้วยสถานะทั้งหมดที่อ่านและเขียนของธุรกรรมทั้งหมดในบล็อก และคุณจะได้ตัวเลขที่น่ากลัว
[แน่นอนว่า การใช้งานไคลเอ็นต์ทั้งหมดพยายามให้โอเวอร์เฮดต่ำที่สุดเท่าที่จะเป็นไปได้ Geth ใช้พื้นที่หน่วยความจำที่ใหญ่ขึ้นเพื่อแคชโหนดต่างๆ นอกจากนี้ ยังใช้กลไกการตัดในหน่วยความจำเพื่อหลีกเลี่ยงการเขียนข้อมูลลงดิสก์ซึ่งจะถูกลบหลังจากบล็อกไม่กี่บล็อก แต่สิ่งนี้ต้องการบทความอื่นเพื่อให้ชัดเจน ]
สิ่งที่น่ากลัวก็คือตัวเลขนี้เป็นค่าใช้จ่ายในการรันโหนด Ethereum ที่รับประกันว่าจะตรวจสอบทุกสถานะตลอด 24 ชั่วโมงทุกวัน
เราทำได้ดีกว่านี้ไหม?
การเข้าชมทั้งหมดไม่เท่ากัน
การดำเนินการของ Ethereum อาศัยการพิสูจน์สถานะการเข้ารหัสลับ ตราบใดที่เราต้องการรักษาความสามารถในการตรวจสอบข้อมูลทั้งหมด เราไม่สามารถแก้ไขปัญหาการขยายการอ่านและเขียนของฮาร์ดดิสก์ได้ นั่นคือ เราทำได้และทำได้ เชื่อถือข้อมูลที่เราตรวจสอบแล้ว
การตรวจสอบวัตถุสถานะแต่ละรายการซ้ำๆ นั้นไม่มีความหมาย แต่ถ้าคุณต้องตรวจสอบทุกครั้งที่คุณดึงข้อมูลจากฮาร์ดดิสก์ แสดงว่าคุณกำลังทำสิ่งที่ไร้ความหมาย โครงสร้างแบบทรีของ Merkle Patricia ได้รับการออกแบบโดยเนื้อแท้สำหรับการดำเนินการเขียน แต่จะกลายเป็นภาระสำหรับการดำเนินการอ่าน เราไม่สามารถกำจัดมันได้ และเราไม่สามารถทำให้ผอมลงได้ แต่นั่นไม่ได้หมายความว่าเราต้องใช้มันในทุกโอกาส
สถานการณ์ของสถานะการเข้าถึงโหนด Ethereum สามารถแบ่งออกเป็นสามประเภทต่อไปนี้:
เมื่อนำเข้าบล็อกใหม่ การดำเนินการของรหัส EVM จะสร้างจำนวนการอ่านและเขียนสถานะที่สมดุลมากขึ้นหรือน้อยลง อย่างไรก็ตาม บล็อกที่ใช้สำหรับการโจมตีแบบปฏิเสธการให้บริการอาจสร้างการดำเนินการอ่านมากกว่าการดำเนินการเขียน
เมื่อตัวดำเนินการโหนดเรียกสถานะ (เช่น การเรียก ethcall และการดำเนินการที่คล้ายกัน) การดำเนินการโค้ด EVM จะสร้างการดำเนินการอ่านเท่านั้น (แน่นอนว่าอาจมีการดำเนินการเขียน แต่ข้อมูลที่สร้างขึ้นโดยการดำเนินการเหล่านี้จะถูกละทิ้งในที่สุดและจะไม่คงอยู่ ภายในฮาร์ดไดร์ฟ)
เมื่อโหนดซิงโครไนซ์บล็อกเชน ซิงโครไนเซอร์จะขอสถานะจากโหนดระยะไกล และผู้ร้องขอจะขุดข้อมูลและกระจายไปยังซิงโครไนเซอร์ผ่านเครือข่าย
จากรูปแบบการเข้าถึงข้างต้น การดำเนินการของโหนดจำนวนมากสามารถทำได้เร็วขึ้นมาก หากเราสามารถลัดวงจรการดำเนินการอ่านโดยไม่ต้องสัมผัสแผนผังสถานะ สิ่งนี้สามารถเปิดรูปแบบการเข้าถึงใหม่บางอย่าง (เช่น การวนซ้ำสถานะ) ที่ก่อนหน้านี้มีราคาแพงเกินไปที่จะเป็นไปได้
แน่นอนว่าการเสียสละเป็นสิ่งที่หลีกเลี่ยงไม่ได้ โครงสร้างการเร่งความเร็วใหม่จะแนะนำโอเวอร์เฮดเพิ่มเติมโดยไม่ต้องถอดโครงสร้างทรีออก คำถามเดียวคือ: ค่าโสหุ้ยพิเศษจ่ายออกไปเพียงพอที่จะรับประกันการลองหรือไม่?
โปรดปฏิบัติตาม
เราได้พัฒนาโครงสร้างต้นไม้ Merkle Patricia อันมหัศจรรย์เพื่อแก้ปัญหาทั้งหมดของเรา และตอนนี้เราต้องการให้การดำเนินการอ่านข้ามไป ดังนั้น เราควรใช้โครงสร้างการเร่งแบบใดเพื่อทำให้การดำเนินการอ่านเร็วขึ้นอีกครั้ง เห็นได้ชัดว่าถ้าเราไม่ต้องการโครงสร้างแบบต้นไม้ เราก็สามารถโยนความซับซ้อนทั้งหมดที่มาพร้อมกับโครงสร้างแบบต้นไม้ทิ้งไป และเราสามารถกลับสู่สถานะดั้งเดิมได้โดยตรง
ตามที่กล่าวไว้ในตอนต้นของบทความนี้ ในทางทฤษฎี วิธีการจัดเก็บข้อมูลของสถานะของ Ethereum ในสถานะที่เหมาะสมควรเป็นคู่ของคีย์-ค่าธรรมดา หากไม่มีข้อ จำกัด ของ Merkel Patricia tree ก็ไม่มีอะไรมาหยุดเราได้ เพื่อให้แผนการในอุดมคตินี้เป็นจริง!
ไม่นานมานี้ Geth ได้เปิดตัวโครงสร้างการเร่งความเร็วสแน็ปช็อต (ไม่ได้เปิดใช้งานตามค่าเริ่มต้น) สแน็ปช็อตคือมุมมองที่สมบูรณ์ของสถานะของ Ethereum ที่บล็อกที่กำหนด การแยกรายละเอียดการใช้งานออกไป มันเพียงแค่สแต็คช่องจัดเก็บบัญชีและสัญญาทั้งหมดเข้าด้วยกัน ซึ่งแสดงด้วยคู่คีย์-ค่าแบบแฟลต
เมื่อใดก็ตามที่เราต้องการเข้าถึงบัญชีหรือช่องเก็บข้อมูลใดช่องหนึ่ง เราต้องจ่ายเพียงหนึ่งการดำเนินการสืบค้นของ LevelDB แทนที่จะต้องสืบค้น 7-8 ครั้งในแต่ละทรี ตามทฤษฎีแล้ว การอัปเดตสแน็ปช็อตก็ง่ายมากเช่นกัน หลังจากประมวลผลบล็อก เราจำเป็นต้องดำเนินการเขียน LevelDB เพิ่มเติมอีก 1 รายการสำหรับช่องเก็บข้อมูลแต่ละช่องที่จะอัปเดต
โครงสร้างการเร่งสแน็ปช็อตช่วยลดความซับซ้อนของการดำเนินการอ่านจาก O(log n) เป็น O(1) (คูณด้วยค่าโสหุ้ยของ LevelDB) โดยมีค่าใช้จ่ายในการเปลี่ยนความซับซ้อนของการดำเนินการเขียนจาก O(log n) เป็น O( 1 + บันทึก n) (คูณด้วยโอเวอร์เฮดของ LevelDB) และเพิ่มพื้นที่จัดเก็บดิสก์จาก O(n บันทึก n) เป็น O(n + n บันทึก n)
ปีศาจอยู่ในรายละเอียด
การรักษาความพร้อมใช้งานของสแน็ปช็อตสถานะ Ethereum นั้นไม่ใช่เรื่องง่าย ตราบใดที่บล็อกยังคงถูกสร้างขึ้นทีละบล็อก วางซ้อนกันบนบล็อกสุดท้าย วิธีการรวมการเปลี่ยนแปลงล่าสุดลงในสแน็ปช็อตอย่างคร่าวๆ จะทำงานได้ดี อย่างไรก็ตาม แม้จะมีการปรับโครงสร้างบล็อกเชนเพียงเล็กน้อย (แม้แต่บล็อกเดียว) กลไกสแน็ปช็อตก็พังลงเพราะการดำเนินการเลิกทำนั้นไม่ได้ออกแบบไว้เลย สำหรับรูปแบบการแสดงข้อมูลแบบแฟลต การเขียนอย่างต่อเนื่องเป็นการดำเนินการแบบทางเดียว และที่แย่ไปกว่านั้น เราไม่มีทางเข้าถึงสถานะที่เก่ากว่าได้ (เช่น dApps บางตัวจำเป็นต้องเข้าถึงสถานะของ 3 บล็อกที่แล้ว หรือสถานะของ 64 บล็อกในโหมดซิงโครไนซ์แบบเร็ว/สแนป)
เพื่อเอาชนะข้อจำกัดเหล่านี้ สแนปช็อตของไคลเอนต์ Geth ประกอบด้วยสองส่วน: เลเยอร์ฮาร์ดดิสก์ถาวร ซึ่งเป็นสแน็ปช็อตที่สมบูรณ์ของสถานะที่บล็อกเก่า (เช่น 128 บล็อกบนสุด) และดิฟเลเยอร์ A ในหน่วยความจำ แผนผังของส่วนประกอบที่ใช้ในการรวบรวมการดำเนินการเขียนล่าสุด
เมื่อประมวลผลบล็อกใหม่ เราจะไม่รวมการเขียนเหล่านี้โดยตรงไปยังเลเยอร์ของฮาร์ดดิสก์ แต่เพียงแค่สร้างเลเยอร์ความแตกต่างในหน่วยความจำใหม่ที่มีการเปลี่ยนแปลงเหล่านี้ เมื่อเลเยอร์ต่างภายในหน่วยความจำสะสมจนมีจำนวนเลเยอร์สูงพอ เลเยอร์ล่างสุดจะเริ่มรวมการอัปเดตและส่งไปยังเลเยอร์ของฮาร์ดดิสก์ เมื่อจำเป็นต้องอ่าน state object เราจะเริ่มค้นหาจากชั้น diff ด้านบนและลงไปจนพบในชั้น diff หรือในชั้นของฮาร์ดดิสก์
การแสดงข้อมูลนี้มีประสิทธิภาพมากและแก้ปัญหาต่างๆ ได้ เนื่องจากชั้น diff ภายในหน่วยความจำก่อตัวเป็นต้นไม้ การจัดโครงสร้างใหม่ภายใน 128 บล็อกจำเป็นต้องนำชั้น diff ของบล็อกหลักออกเท่านั้น แล้วจึงเริ่มสร้าง dApps และ farsyncer ที่ต้องการสถานะที่เก่ากว่าสามารถเข้าถึงสถานะล่าสุด 128 สถานะล่าสุด ค่าโสหุ้ยกลายเป็นการค้นหาการแมป 128 รายการ แต่การค้นหาในหน่วยความจำ 128 รายการนั้นมีลำดับความสำคัญเร็วกว่าการอ่านฮาร์ดดิสก์ 8 ครั้งและการขยายระดับฐานข้อมูล 4 ถึง 5 เท่า
แน่นอนว่ายังมีข้อผิดพลาดมากมาย ฉันจะไม่ลงลึกมากเกินไป แต่เพียงระบุรายการต่อไปนี้:
การทำลายตัวเอง (การดำเนินการทำลายตัวเองตามสัญญา) (และการดำเนินการลบ) นั้นยากเป็นพิเศษในการจัดการเพราะพวกมันต้องการการลัดวงจรในการลงมาของดิฟเลเยอร์
หากมีการจัดระเบียบสายโซ่ใหม่ลึกกว่าชั้นฮาร์ดดิสก์ถาวร สแน็ปช็อตปัจจุบันจะถูกละทิ้งและสร้างใหม่ทั้งหมด การดำเนินการทั้งชุดมีราคาแพงมาก
เมื่อโหนดปิดลง เลเยอร์ diff ในหน่วยความจำจะต้องคงอยู่ในบันทึกและโหลดเพื่อสำรอง มิฉะนั้นสแน็ปช็อตจะไร้ประโยชน์หลังจากรีสตาร์ท
ใช้ชั้นความแตกต่างที่ต่ำที่สุดเป็นตัวสะสมที่จะถูกล้างข้อมูลไปยังดิสก์เมื่อมีการใช้หน่วยความจำเกินที่กำหนดเท่านั้น ซึ่งช่วยให้สามารถเขียนข้อมูลซ้ำซ้อนลงในช่องเดียวกันระหว่างกลุ่มต่างๆ ได้
ควรจัดสรรแคชการอ่านสำหรับชั้นของฮาร์ดดิสก์ เพื่อที่ฮาร์ดดิสก์จะไม่เสียหายหากสัญญาเข้าถึงสล็อตเก็บข้อมูลเดิมซ้ำๆ
ใช้ตัวกรองบานสะสมในเลเยอร์ diff ในหน่วยความจำเพื่อตรวจจับอย่างรวดเร็วว่าวัตถุสถานะมีแนวโน้มที่จะมีอยู่ในเลเยอร์ diff หรือควรข้ามไปที่ดิสก์โดยตรง
ข้อมูลต้นฉบับ (ที่อยู่บัญชี คีย์การจัดเก็บสัญญา) ไม่ได้ถูกกำหนดให้เป็นคีย์ แต่ค่าแฮชของข้อมูลเหล่านี้จะถูกใช้เป็นคีย์เพื่อให้แน่ใจว่าลำดับการวนซ้ำของสแน็ปช็อตจะเหมือนกับของ Merkle Patricia tree
เวลาในการสร้างชั้นของฮาร์ดดิสก์ถาวรนั้นยาวนานกว่าเวลาในการตัดหน้าต่างแผนผังสถานะ ดังนั้นแม้แต่ตัวสร้างก็จำเป็นต้องติดตามการทำงานของเชนแบบไดนามิก
การอยู่ร่วมกันของความงามและความอัปลักษณ์
โครงสร้างการเร่งความเร็วสแน็ปช็อตของ Geth ช่วยลดความซับซ้อนของการอ่านสถานะตามลำดับความสำคัญ ซึ่งหมายความว่าการโจมตี DoS แบบอ่านเป็นลำดับความสำคัญที่ยากต่อการเปิดตัว และการเรียก ethcall ก็เป็นลำดับความสำคัญที่เร็วกว่าเช่นกัน (สมมติว่าไม่มีคอขวดของ CPU)
แน่นอนว่าทั้งหมดนี้ไม่มีค่าใช้จ่าย หลังจากการซิงโครไนซ์เริ่มต้นเสร็จสิ้น โหนดที่เข้าร่วมในเครือข่ายหลักต้องใช้เวลา 9 ถึง 10 ชั่วโมงในการสร้างสแน็ปช็อตเริ่มต้น (จากนั้นจึงรักษาความพร้อมใช้งาน) และต้องใช้ฮาร์ดดิสก์เพิ่มเติมมากกว่า 15 GB
บทส่งท้าย
ส่วนที่แย่อยู่ตรงไหน? เราใช้เวลา 6 เดือนในการสร้างความมั่นใจให้มากพอที่จะปล่อยกลไกสแน็ปช็อต และยังไม่ใช่ฟีเจอร์เริ่มต้น จำเป็นต้องเปิดใช้งานด้วยแฟล็ก --snapshot และยังคงมีงานปรับปรุงเกี่ยวกับการใช้หน่วยความจำและการกู้คืนข้อขัดข้อง ทำ.
สรุปแล้วเราภูมิใจในการปรับปรุงนี้มาก มีงานจำนวนมากเข้ามาเกี่ยวข้อง และมันก็คลำหาในความมืด ดำเนินการทุกอย่างด้วยตัวเองและภาวนาให้มันได้ผล สิ่งที่น่าสนใจอีกอย่างคือ leaf sync เวอร์ชันแรกเขียนขึ้นเมื่อสองปีครึ่งที่แล้ว แต่ถูกบล็อกเนื่องจากเราขาดโครงสร้างการเร่งความเร็วที่จำเป็นในการขับเคลื่อน


