บทนำ
ตลอดหลายเดือนของการปรับปรุง ค่า p99 แบบ end-to-end ของ Lighter ลดลงจาก 280 ms เหลือ 55 ms ที่คงที่ ค่า p99 การประมวลผลธุรกรรมลดลงจากการมีสไปค์ที่ 20–30 ms เหลือต่ำกว่า 1 ms เวลา apply ใน hot path อยู่ที่ 100–250 µs ทั้งหมดบน mainnet ที่สเกล
สเกลมากแค่ไหน? ในวันที่ 5 มิถุนายน 2026 Lighter ทำสถิติสูงสุดตลอดกาลที่ 811 ล้านธุรกรรมในวันเดียว ค่าเฉลี่ย 9,388 TPS และจุดสูงสุดที่ 20,740 TPS
sequencer ของ Lighter เขียนด้วยภาษา Go ธุรกรรมถูกดำเนินการบน Lighter L2 โดยมีหลักฐานการเข้ารหัสแบบสมบูรณ์ที่ settled ไปยัง L1 ข้อมูลเชิงลึกทางสถาปัตยกรรมที่สำคัญคือการพิสูจน์ ถูกแยก ออกจากการดำเนินการ: การสร้างหลักฐานทำงานแบบอะซิงโครนัสและไม่เคยบล็อกการซื้อขาย นั่นหมายถึงค่า latency ที่เทรดเดอร์ประสบจะถูกกำหนดโดยเลเยอร์ execution และ API ของ Lighter ทั้งหมด
เลเยอร์ API ของ Lighter คือบริการที่เทรดเดอร์โต้ตอบโดยตรง มันคงมุมมองที่สมบูรณ์และทันสมัยของสถานะ exchange ในหน่วยความจำ และให้บริการการเชื่อมต่อ REST และ WebSocket จากสถานะในเครื่องนั้น goroutine ที่สำคัญจะรับการอัปเดตสถานะจาก execution engine และนำไปใช้กับแคชในหน่วยความจำแบบเรียลไทม์ ทุกอย่าง downstream (การพุช WebSocket, การอ่าน API, การจำลองธุรกรรม) ให้บริการจากแคชเหล่านี้
แนวคิดหลักที่ครอบคลุม
แนวคิดต่อไปนี้ให้ภาพรวมระดับสูงของหัวข้อสำคัญที่ครอบคลุมในบทความทางเทคนิคที่เหลือ:
ค้นหาก่อน mainnet: สภาพแวดล้อม loadtest ที่เหมือนกันกับ mainnet พร้อมบัญชีสังเคราะห์และโหลดที่สมจริง และมีการ instrument มากกว่าการผลิตมาก จะจับคอขวดก่อนที่เทรดเดอร์จะสังเกตเห็น
การสังเกตการณ์แบบสองสภาพแวดล้อม: Loadtest มีเมตริกที่แพงเกินไปสำหรับการผลิต mainnet จะถูกตรวจสอบแบบเรียลไทม์ในด้านความสดใหม่ (freshness), latency การจำลอง และวงจรชีวิตของธุรกรรมแบบ end-to-end เมื่อรวมกันแล้วให้ความครอบคลุมที่สมบูรณ์
ตายด้วยการจัดสรรนับพันครั้ง: การแทนที่ประเภทที่ใช้ heap มากและการคัดลอกเชิงลึกอย่างเป็นระบบด้วยทางเลือกที่จัดสรรบน stack และ snapshot ที่ไม่เปลี่ยนรูป ในภาษาที่มี GC การจัดสรรน้อยลง = การหยุดน้อยลง = tail latency ต่ำลง
การควบคุมระดับ OS: การปัก Thread ที่สำคัญไปยังแกน CPU เฉพาะด้วย priority แบบ near real-time
การ部署ที่คำนึงถึงฮาร์ดแวร์: การจัดวางแบบ NUMA-aware ช่วยให้ hot path มีการเข้าถึงหน่วยความจำในพื้นที่ที่รวดเร็ว ชั้นสุดท้ายใน stack latency
ไบนารีเหนือความสะดวก: การ serialize แบบไบนารีที่เขียนด้วยตนเองบน hot path ช่วยลด reflection และ overhead การจัดสรร ทุกไมโครวินาทีที่ประหยัดได้จะทวีคูณในการอัปเดตหลายพันครั้งต่อวินาที
การค้นหาคอขวดก่อนที่จะถึง Mainnet
คุณไม่สามารถแก้สิ่งที่คุณไม่เห็น ก่อนที่จะปรับแต่งอะไร เราได้ลงทุนอย่างหนักในเครื่องมือเพื่อระบุว่าเวลาถูกใช้ไปที่ไหน
สภาพแวดล้อม Loadtest
เมื่อเราต้องการทดสอบอะไร เราจะเปิดสภาพแวดล้อม loadtest เฉพาะ: สำเนาที่เหมือนกันของโครงสร้างพื้นฐาน mainnet ที่รันบริการด้วยการตั้งค่าเดียวกันภายใต้โครงสร้างการ部署เดียวกัน มันไม่ได้รันตลอดเวลา เราจะเปิดเมื่อต้องการสำหรับการทดสอบเฉพาะและปิดหลังจากนั้น
เมื่อสภาพแวดล้อมนี้ทำงาน เราจะสร้างบัญชีสังเคราะห์และสร้างโหลดการซื้อขายที่สมจริงเพื่อจำลองสภาวะตลาดจริงภายใต้ความเครียด สิ่งนี้ช่วยให้:
- การจับเวลาแบบละเอียดพิเศษ: การจับเวลาทีละขั้นภายใน hot path, ระยะเวลาการสร้างแคชต่อการดำเนินการ และ timestamp วงจรชีวิตของธุรกรรมที่ติดตามทุกขั้นตอนตั้งแต่การส่งจนถึงการยืนยัน
- การทำโปรไฟล์และ flight recorder ตามความต้องการ: เราจับภาพโปรไฟล์ CPU, หน่วยความจำ และการดำเนินการตามคำขอระหว่างโหลด flight recorder ของ Go ให้การเก็บ trace ที่เปิดตลอดเวลา เมื่อตรวจพบเหตุการณ์ช้า วินาทีสุดท้ายของการดำเนินการจะถูกบันทึกโดยอัตโนมัติ ทำให้เราสามารถวินิจฉัย latency spike ชั่วคราวหลังจากข้อเท็จจริง
- การติดตามแบบกระจาย: เรา instrument ทุกฟังก์ชันหลักใน hot path ด้วย trace span ทำให้เรามองเห็นอย่างละเอียดว่าเวลาใช้ไปที่ไหนในรอบการอัปเดตเดียว ข้ามขอบเขตบริการ
การตรวจสอบแบบสองสภาพแวดล้อม
เราตรวจสอบทั้งสองสภาพแวดล้อมอย่างใกล้ชิด (mainnet และ loadtest) แต่ในระดับความละเอียดที่แตกต่างกัน
Mainnet ถูกตรวจสอบอย่างใกล้ชิดแบบเรียลไทม์ เราติดตามทุกสิ่งที่สำคัญต่อประสบการณ์การซื้อขาย:
- การติดตามความสดใหม่ (Freshness tracking): เราวัด latency ผ่านช่อง WebSocket
order_bookในตลาดที่มีการใช้งานมากที่สุด โดยพื้นฐานแล้วคือความคลาดเคลื่อนระหว่างเวลาที่การอัปเดต order book ถูกสร้างขึ้นและเวลาที่ลูกค้าได้รับ นี่คือเมตริกที่สะท้อนถึงประสบการณ์ที่เทรดเดอร์รู้สึกได้โดยตรง: order book ที่คุณกำลังดูอยู่นั้นล้าสมัยแค่ไหน? เราติดตามที่สองจุด จุดที่สำคัญที่สุดคือ execution engine → client: ท่อเต็มตั้งแต่เมื่อ execution engine ประมวลผลการเปลี่ยนแปลงสถานะจนถึงช่วงเวลาที่การอัปเดตorder_bookที่เกิดขึ้นถึงลูกค้า เรายังวัด API layer → client: เลเยอร์ API จะ timestamp การอัปเดตขาออกแต่ละครั้งและลูกค้าเปรียบเทียบกับนาฬิกาของตัวเอง ทำให้เราได้ last-mile latency แยกต่างหาก - Dry-run latency: เลเยอร์ API จะ dry-run ทุกธุรกรรม (ตรวจสอบลายเซ็น, nonce และยอดคงเหลือ) ก่อนที่จะส่งไปยัง execution engine ทุก dry-run จะถูกจับเวลาและรายงาน
- วงจรชีวิตของธุรกรรมแบบ end-to-end: ฮิสโทแกรม latency แบบเต็มรอบตั้งแต่การส่งจนถึงการยืนยัน
- ประสิทธิภาพของแคช: เราตอบสนองจากหน่วยความจำบ่อยแค่ไหนเทียบกับการถอยกลับไปยังพื้นที่จัดเก็บที่ช้ากว่า
Loadtest ถูกตรวจสอบอย่างละเอียดยิ่งขึ้น เนื่องจากไม่ได้ให้บริการเทรดเดอร์จริง เราจึงสามารถเพิ่มการ instrument ได้โดยไม่ต้องกังวลเรื่อง overhead: การจับเวลาทีละขั้นภายใน hot path, โปรไฟล์การจัดสรร, ที่เก็บ histogram พิเศษในการกลายพันธุ์แคชทุกครั้ง ความละเอียดที่ละเอียดยิ่งขึ้นนี้จับการถดถอยระดับจุลภาคที่เมตริกที่ปลอดภัยสำหรับการผลิตของ mainnet จะไม่เปิดเผย
การตรวจสอบ mainnet บอกเราว่าระบบทำงานอย่างไรสำหรับเทรดเดอร์จริง ในตอนนี้ การตรวจสอบ loadtest จับการถดถอย ก่อน ที่จะถึงการผลิต
การกำจัดการคัดลอกเชิงลึกและสงครามการจัดสรร Heap
ตัวเก็บขยะของ Go คือภาษี latency ทุกการจัดสรร heap ในที่สุดจะกลายเป็นการหยุด GC และการหยุด GC บนเครื่องมือ trading คือ latency spike สำหรับเทรดเดอร์ ดังนั้นเราจึงไปที่ hot path และลดการจัดสรร wherever ที่เราทำได้ ผลลัพธ์คือ latency spike น้อยลงและประสิทธิภาพที่คาดเดาได้มากขึ้น
การกำจัดการคัดลอกเชิงลึก
การคัดลอกเชิงลึกคือโรงงานจัดสรร เราจัดการกับมันในสองสามวิธี:
- snapshot ที่ไม่เปลี่ยนรูป: ทำให้แคช order book ไม่เปลี่ยนรูป การอ่านจะส่งคืนตัวชี้ไปยัง snapshot ปัจจุบัน ไม่จำเป็นต้องคัดลอก การอัปเดตจะสร้างเวอร์ชันใหม่และสลับเข้าไปแบบอะตอมผ่าน
atomic.Pointer - ลบการคัดลอกที่ไม่จำเป็น: พบ path โค้ดที่คัดลอกเชิงลึกข้อมูลที่ไม่เคยถูกกลายพันธุ์ในภายหลัง ลบทิ้งทั้งหมด
การออกแบบที่คำนึงถึงการจัดสรร
- ตัวเลขที่จัดสรรบน stack: แทนที่
big.Intและbig.Ratที่ใช้ heap มากด้วยทางเลือกที่จัดสรรบน stack (int128,float64,int64division) ทั่ว hot path: การแปลงราคา, แผนที่ความลึก order book, การคำนวณขนาด เร็วกว่าถึง 8.3 เท่าในฟังก์ชันการจัดรูปแบบที่สำคัญ ไม่มีการจัดสรร heap ต่อการดำเนินการ - การอัปเดตตามเงื่อนไข: ข้ามการจัดสรรเมื่อไม่มีอะไรเปลี่ยนแปลง
- คอลเลกชันที่กำหนดขนาดล่วงหน้า: กำจัดวงจรการเติบโตและคัดลอกโดยการกำหนดขนาดโครงสร้างข้อมูลล่วงหน้า
- การนำ buffer กลับมาใช้ใหม่: path subscriber ถอดรหัสการอัปเดตหลายพันครั้งต่อวินาที เราลดการจัดสรรด้วยการใช้ pool-based reuse ของ buffer กลาง
สิ่งนี้ทำให้ tail เรียบขึ้น ก่อนหน้านี้ p99 แบบ end-to-end ของ Lighter (execution engine ถึง client, วัดบนช่อง WebSocket order_book) จะสไปค์เป็น 200–280 ms ในช่วงที่มีการจัดสรรหนัก หลังจากการกำจัดการคัดลอกเชิงลึก, แคชที่ไม่เปลี่ยนรูป และการปรับ heap allocation แล้ว p99 ก็คงที่ในช่วง ~50–60 ms โดยแทบไม่มีสไปค์:

End-to-End Latency: Execution Engine -> Client (p99)
แรงดัน GC บนเลเยอร์ API ก็ลดลงอย่างเห็นได้ชัด แต่นี่ไม่ใช่แค่การเปลี่ยนแปลงเดียว เราได้ปรับโฉมวิธีการทำงานของแคชเซิร์ฟเวอร์ API ใหม่ทั้งหมด ก่อนหน้านี้แคชมี TTL-based expiry และจัดสรร object ใหม่ทุกครั้งที่มีการอัปเดต การจัดสรรอายุสั้นแต่ละครั้งกลายเป็นงาน GC หลังจากการปรับโฉม สถานะ exchange ทั้งหมดจะถูก hot-start จาก snapshot และเก็บไว้ในหน่วยความจำเป็นโครงสร้างที่ไม่เปลี่ยนรูปที่มีอายุยาวนานซึ่งจะถูกสลับแบบอะตอม สิ่งนี้ช่วยลด TTL churn และการจัดสรรต่อการอัปเดต และแรงดัน GC ก็ลดลงตามไปด้วย เราตรวจสอบการใช้หน่วยความจำอย่างใกล้ชิด และ working set มีขอบเขต
เราได้รัน apiserver แบบเก่าและ apiserver ที่ปรับโฉมใหม่ควบคู่กันบนทราฟิก mainnet ระยะเวลาหยุด GC (p75) ของเซิร์ฟเวอร์ที่ปรับโฉมใหม่อยู่ที่ประมาณ ~3 ms เทียบกับ ~5–6 ms บนเซิร์ฟเวอร์เก่า ประมาณครึ่งหนึ่งของเวลา GC หยุด:

GC Pause Duration: Standard vs. Snapshot API Server
ความถี่บอกเรื่องราวที่ชัดเจนยิ่งขึ้น apiserver เก่ากระตุ้นวงจร GC บ่อยกว่า ~2.2 เท่า เมื่อเทียบกับ apiserver ที่ปรับโฉมใหม่ สะท้อนถึงการจัดสรรอายุสั้นที่น้อยลงและแรงดัน GC โดยรวมที่น้อยลง:

GC Cycle Frequency: Standard API Server vs. Snapshot API Server
การปรับ GOGC บน Execution Engine
เราใช้แนวคิด GC ที่คล้ายกันกับ execution engine เอง พารามิเตอร์ GOGC ของ Go ควบคุมว่าตัวเก็บขยะทำงานก้าวร้าวแค่ไหน ค่าเริ่มต้นจะแลกเวลา CPU เพื่อประสิทธิภาพหน่วยความจำ แต่สำหรับ path ที่ไวต่อ latency การแลกเปลี่ยนนั้นผิด
หลังจากปรับ GOGC ระยะเวลา GC ของ execution engine ลดลงจากค่าเฉลี่ย ~30 µs โดยมีสไปค์ถึง 100 µs ลงมาอยู่ในช่วง ~10 µs ที่คงที่ การลดลง ~3 เท่าโดยแทบไม่มีสไปค์:

Execution Engine GC Duration After GOGC Tuning
ข้อมูล mainnet สามเดือนยืนยันว่าการปรับปรุงคงอยู่: ช่วงหลังคือแบนและคาดเดาได้
การประมวลผลธุรกรรมดีขึ้นตามไปด้วย จากข้อมูล mainnet 90 วันที่ครอบคลุมทุกประเภทธุรกรรม (สร้างคำสั่งซื้อ, ยกเลิก, ชำระบัญชี, ลดเลเวอเรจ, โอน และอื่นๆ) p99 ลดลงจากการมีสไปค์ที่ 20–30 ms บ่อยครั้งก่อนการปรับ GOGC ไปเป็นส่วนใหญ่ต่ำกว่า 1 ms หลังการปรับ โดยมีค่าผิดปกติ 3–4 ms เป็นครั้งคราว การปรับปรุงคงอยู่นานกว่าสองเดือน:

Transaction Processing Times - p99 (Last 90 days)
Snapshot Service — ข้อมูลน้อยลง, Deploy แบบร้อน
หลังจากที่เรากำจัด overhead การจัดสรรแล้ว เราก็จัดการกับคอขวดถัดไป: ปริมาณข้อมูลที่ไหลผ่าน message bus ภายใน เราสร้าง snapshot service ที่คง snapshot สถานะในหน่วยความจำแบบเต็ม สิ่งนี้มีผลกระทบหลักสองประการ:
- ข้อมูลบนสายน้อยลง เมื่อ snapshot service ถือสถานะเต็ม execution engine ไม่จำเป็นต้องผลักสถานะที่สมบูรณ์ผ่าน message bus ทุกครั้งที่มีการอัปเดต มันเขียนข้อมูลน้อยลง ซึ่งหมายถึงแบนด์วิดท์เครือข่ายที่ใช้ไขว้กันน้อยลงและงาน deserialization ที่ปลายรับน้อยลง
- Deploy โดยไม่ต้องอุ่นเครื่อง เมื่อ deploy เลเยอร์ API จะบูตจาก snapshot (บัญชี, order books, ข้อมูลตลาด, คีย์สาธารณะ API) และพร้อมให้บริการทันที ไม่มีช่วงอุ่นเครื่องในขณะที่แคชเติม หลังจากโหลด snapshot แล้ว เลเยอร์ API จะสมัครรับสตรีมการอัปเดตและนำเดลต้่าไปใช้แบบเรียลไทม์
เหนือฐาน snapshot แคชในหน่วยความจำจะถูกอัปเดตอย่างต่อเนื่อง:
- ข้อมูลบัญชี: การอ่านแบบ lock-free ผ่าน
sync.Map, การสลับตัวชี้แบบอะตอมสำหรับการอัปเดต - Order books: เก็บเป็น snapshot ที่ไม่เปลี่ยนรูป การอ่านจะได้รับตัวชี้ การอัปเดตจะสลับเป็นเวอร์ชันใหม่ ไม่มีการล็อกบน read path
- แคชคีย์ API: คีย์ทั้งหมดพอดีในหน่วยความจำ กำจัดการค้นหาภายนอกทั้งหมด
ผลลัพธ์: สถานะ exchange ทั้งหมดอยู่ในหน่วยความจำในเครื่อง อัปเดตแบบเรียลไทม์ และทุก deploy เริ่มต้นแบบร้อน
ผลกระทบต่อเนื่องต่อ execution engine มีนัยสำคัญ ก่อนหน้านี้ execution engine กำลังเขียนคีย์แคชผ่านเครือข่าย การอัปเดตที่บริการต่างๆ จะอ่านเป็นครั้งคราว ด้วยทุกอย่างที่อยู่ในหน่วยความจำและ snapshot service จัดการการกระจายสถานะ การเขียนเครือข่ายเหล่านั้นก็ไม่จำเป็นอีกต่อไป เราลบทิ้ง ผลลัพธ์: block times p99 ลดลงจาก ~2.6 ms เป็น ~1.2–1.8 ms เพียงเพราะ execution engine เขียนน้อยลงมาก:

Block Times - p99
การวางแผน CPU
เมื่อเราปรับทุกอย่างใน userspace อย่างเหมาะสมแล้ว เราก็เข้าถึง kernel
เรามี goroutines จำนวนมากและพวกมันต้องถูกจัดตารางเวลาบน CPU เพื่อรัน การลด overhead การจัดตารางเวลาเป็นสิ่งสำคัญสำหรับระบบ latency ต่ำ โดยค่าเริ่มต้น runtime ของ Go จะมัลติเพล็กซ์ goroutines ข้ามเธรด OS และ OS สามารถโยกย้ายเธรดข้ามแกน CPU ได้อย่างอิสระ ทั้งสองทำให้เกิด latency ที่คาดเดาไม่ได้
เรากำจัดสิ่งนี้โดยซ้อนกลไกสี่อย่าง:
- `runtime.LockOSThread()`: ล็อก goroutine กับเธรด OS เดียว ป้องกันไม่ให้ scheduler ของ Go ย้ายมัน
- CPU affinity ผ่าน `sched_setaffinity`: ปักเธรด OS นั้นกับแกน CPU เฉพาะ (Linux) สิ่งนี้ป้องกันไม่ให้ kernel ย้ายมันระหว่างแกน หลีกเลี่ยงการทำให้ L1/L2 cache เป็นโมฆะ
- การจัดตารางเวลาความสำคัญสูงผ่าน SCHED_FIFO: ยกระดับความสำคัญในการจัดตารางเวลาของเธรด ทำให้แน่ใจว่า kernel เลือกใช้มันมากกว่างานอื่น
- วงวน spin แบบ busy-wait: hot path รัน
selectด้วยdefaultที่ว่างเปล่า ดังนั้น goroutine ไม่เคย park หากไม่มีมัน Go จะย้าย goroutine ไปยังสถานะ "runnable" เมื่อไม่มีข้อมูล และการจัดตารางเวลาใหม่จะเพิ่ม wake-up latency ด้วยวงวน spin goroutine จะยังคงทำงานบนแกนที่ปักไว้และรับการอัปเดตใหม่โดยไม่มีความล่าช้าในการจัดตารางเวลา
การวัดผลกระทบอย่างถูกต้อง เป็นสิ่งสำคัญที่นี่ apply time แตกต่างกันไปตามสภาพทราฟิก ดังนั้นตัวเลขที่แน่นอนจะเปลี่ยนไปตามโหลด เพื่อแยกผลกระทบของการปัก เราได้รัน API server สองกลุ่มควบคู่กันภายใต้ทราฟิกเดียวกัน กลุ่มหนึ่งไม่ปักเป็นกลุ่มควบคุม และเราสลับอีกกลุ่มเป็นการปัก CPU ความแตกต่างร้อยละระหว่างพวกเขา วัดในเวลาเดียวกันภายใต้โหลดที่เหมือนกัน บอกเล่าเรื่องจริง
ก่อนปัก ทั้งสองกลุ่มไปด้วยกัน latency พื้นฐานเท่ากัน พฤติกรรมสไปค์เดียวกัน:

Hot Path Apply Time - Before CPU Planning
หลังจากเปิดใช้งานการปักในกลุ่มหนึ่ง มันอยู่ต่ำกว่า baseline ที่ไม่ปักอย่างสม่ำเสมอ ทราฟิกเดียวกัน latency ต่ำกว่า สไปค์ของกลุ่มที่ปักก็ถูกจำกัดให้ต่ำกว่าเช่นกัน เนื่องจากการกระวนกระวายจากการโยกย้ายเธรดและการทำให้ L1/L2 cache เป็นโมฆะถูกกำจัด:

Hot Path Apply Time - After CPU Planning
การ部署แบบ NUMA-Aware
การปัก CPU เพียงอย่างเดียวไม่เพียงพอถ้าการเข้าถึงหน่วยความจำของแกนที่ปักข้ามขอบเขต NUMA โหนด NUMA คือกลุ่มของ CPU ที่มีหน่วยความจำในพื้นที่ของตัวเอง การเข้าถึงหน่วยความจำจากโหนด NUMA ระยะไกลมี โทษ 10 เท่า เมื่อเทียบกับการเข้าถึงในพื้นที่
API servers ของ Lighter เดิมรันบนเครื่องที่ใหญ่กว่าด้วย 2 โหนด NUMA:
1$ lscpu | grep NUMA2NUMA node(s): 23NUMA node0 CPU(s): 0-954NUMA node1 CPU(s): 96-19156$ cat /sys/devices/system/node/node0/distance710 100
เมทริกซ์ระยะทางบอกเล่าเรื่อง: การเข้าถึงในพื้นที่มีค่าใช้จ่าย 10 การเข้าถึงข้ามโหนดมีค่าใช้จ่าย 100 โทษ 10 เท่า ด้วย API server N ตัวที่แชร์เครื่อง บางตัวจำเป็นต้องมี CPU ที่ปักบนโหนด NUMA หนึ่งและหน่วยความจำทำงาน (แคชในหน่วยความจำ, buffer อัปเดต) บนอีกโหนด ทุกรอบของ hot path จ่ายภาษีข้ามโหนด
การแก้ไขนั้นขัดกับสัญชาตญาณ: เราย้าย API servers ไปยัง เครื่องที่เล็กกว่าด้วยโหนด NUMA เดียว ครึ่งหนึ่งของสเปก แต่การเข้าถึงหน่วยความจำทั้งหมดตอนนี้รับประกันว่าอยู่ในพื้นที่:
1$ lscpu | grep NUMA2NUMA node(s): 13NUMA node0 CPU(s): 0-95
สิ่งนี้ลดต้นทุนและปรับปรุง latency ในเวลาเดียวกัน เวลา hot path apply ลดลงอีกในช่วง ~100–250 µs โดยช่วงนอก peak ลดลงเหลือ ~100 µs เปรียบเทียบกับช่วง ~200–520 µs ด้วยการปัก CPU เพียงอย่างเดียวบนเครื่อง 2 NUMA node:

Hot Path Apply Time - NUMA Optimization
การ Serialize แบบไบนารีที่กำหนดเอง
execution engine ของ Lighter เผยแพร่การอัปเดตสถานะไปยังเลเยอร์ API ผ่าน message bus ภายใน ทุกการอัปเดตระบบไหลผ่าน path นี้ การ serialize เดิมใช้ไลบรารีการเข้ารหัสทั่วไป แต่ reflection, type switch และการจัดสรรต่อฟิลด์สร้าง overhead ที่ไม่จำเป็นบน hot path
เราแทนที่สิ่งนั้นด้วยการ serialize แบบไบนารีที่เขียนด้วยตนเอง: encode/decode แบบ fixed-layout, zero-reflection สำหรับทุกประเภทเอนทิตีในระบบ แต่ละประเภทมี encoder/decoder เฉพาะที่อ่านและเขียนฟิลด์ที่ offset ไบต์ที่ทราบ ไม่มี reflection หรือ type switch และการจัดสรรน้อยที่สุด ทุก codec มีการทดสอบ round-trip และ fuzz เพื่อจับการถดถอย
ผลลัพธ์คือ overhead การ serialize น้อยลงอย่างมีนัยสำคัญบน path ที่ป้อนแคช downstream ทุกตัว
การวัดประสิทธิภาพอิสระมีให้ผ่านแดชบอร์ดที่สร้างและดูแลโดยสมาชิกชุมชน @UngusTrade ซึ่งเปรียบเทียบ latency ธุรกรรมสดในสถานที่ซื้อขาย perpetual: latency.perps.trading





![[Ultimate Edition] เจาะลึกวิเคราะห์ Recruit Holdings จากมุมมองอดีตพนักงาน (เขียนด้วยตัวเอง ไม่ใช่ AI)](/cdn-cgi/image/width=1920,quality=90,format=auto,metadata=none/https%3A%2F%2Fcms-assets.youmind.com%2Fmedia%2F1783619810056_4fyuz8_HMtxHM_bIAA-0_J.jpg)