Theo đuổi từng micro giây: Kỹ thuật tối ưu độ trễ của Lighter

@Lighter_xyz
TIẾNG ANH1 ngày trước · 08 thg 7, 2026
190K
552
79
39
266

TL;DR

Phân tích kỹ thuật về hành trình đạt độ trễ dưới một mili giây của Lighter, bao gồm tinh chỉnh GC, CPU affinity và quản lý bộ nhớ để mở rộng quy mô cho Ethereum L2 hiệu năng cao.

Giới thiệu

Qua nhiều tháng lặp đi lặp lại, p99 end-to-end của Lighter đã giảm từ 280 ms xuống còn 55 ms phẳng. p99 xử lý giao dịch giảm từ các đỉnh 20–30 ms xuống dưới 1 ms. Thời gian áp dụng hot path đạt 100–250 µs. Tất cả đều trên mainnet, ở quy mô lớn.

Quy mô lớn đến mức nào? Vào ngày 5 tháng 6 năm 2026, Lighter đạt mức cao nhất mọi thời đại với 811 triệu giao dịch trong một ngày, trung bình 9.388 TPS với các đỉnh 20.740 TPS.

Sequencer của Lighter được viết bằng Go. Các giao dịch được thực thi trên Lighter L2 với các bằng chứng mật mã đầy đủ được giải quyết lên L1. Hiểu biết then chốt về kiến trúc là việc chứng minh được tách rời khỏi thực thi: việc tạo bằng chứng chạy bất đồng bộ và không bao giờ chặn giao dịch. Điều đó có nghĩa là độ trễ mà các trader trải nghiệm hoàn toàn do các lớp thực thi và API của Lighter quyết định.

Lớp API của Lighter là dịch vụ mà các trader tương tác trực tiếp. Nó duy trì một chế độ xem hoàn chỉnh, cập nhật về trạng thái sàn giao dịch trong bộ nhớ và phục vụ các kết nối REST và WebSocket từ trạng thái cục bộ đó. Một goroutine quan trọng nhận các cập nhật trạng thái từ engine thực thi và áp dụng chúng vào các bộ nhớ đệm trong bộ nhớ theo thời gian thực. Mọi thứ hạ nguồn (WebSocket push, API đọc, mô phỏng giao dịch) đều phục vụ từ các bộ nhớ đệm này.

Các khái niệm chính được đề cập

Các khái niệm sau đây cung cấp một tổng quan cấp cao về các chủ đề chính được đề cập trong phần còn lại của bài viết kỹ thuật này:

Tìm nó trước mainnet: Một môi trường loadtest giống hệt với các tài khoản tổng hợp và tải thực tế, được đo lường công cụ chi tiết hơn nhiều so với sản xuất, phát hiện các nút thắt trước khi trader nhận thấy.

Khả năng quan sát hai môi trường: Loadtest mang các chỉ số quá tốn kém cho sản xuất; mainnet được giám sát theo thời gian thực về độ tươi mới, độ trễ mô phỏng và vòng đời giao dịch end-to-end. Cùng nhau, chúng cung cấp phạm vi bao phủ hoàn chỉnh.

Chết bởi hàng ngàn lần cấp phát: Thay thế có hệ thống các kiểu dữ liệu nặng trên heap và các bản sao sâu bằng các lựa chọn thay thế trên stack và các bản chụp nhanh bất biến. Trong một ngôn ngữ có GC, ít cấp phát hơn = ít dừng hơn = độ trễ đuôi thấp hơn.

Kiểm soát cấp độ OS: Gắn các luồng quan trọng vào các lõi CPU chuyên dụng với mức ưu tiên gần thời gian thực.

Triển khai nhận thức phần cứng: Sắp xếp nhận thức NUMA đảm bảo hot path có quyền truy cập bộ nhớ cục bộ nhanh. Lớp cuối cùng trong ngăn xếp độ trễ.

Nhị phân thay vì tiện lợi: Tuần tự hóa nhị phân tự viết trên hot path loại bỏ reflection và chi phí cấp phát. Mỗi microsecond tiết kiệm được nhân lên qua hàng ngàn bản cập nhật mỗi giây.

Tìm nút thắt trước khi chúng ảnh hưởng đến Mainnet

Bạn không thể sửa những gì bạn không thể thấy. Trước khi tối ưu hóa bất cứ điều gì, chúng tôi đã đầu tư mạnh vào công cụ để xác định chính xác nơi thời gian đang được sử dụng.

Môi trường Loadtest

Khi chúng tôi muốn kiểm tra một cái gì đó, chúng tôi khởi tạo một môi trường loadtest chuyên dụng: một bản sao giống hệt của cơ sở hạ tầng mainnet chạy các dịch vụ với cấu hình giống hệt nhau dưới cùng một cấu trúc liên kết triển khai. Nó không phải lúc nào cũng chạy; chúng tôi đưa nó lên theo yêu cầu cho một bài kiểm tra cụ thể và gỡ bỏ nó sau đó.

Khi môi trường này đã sẵn sàng, chúng tôi tạo các tài khoản tổng hợp và tạo tải giao dịch thực tế để mô phỏng các điều kiện thị trường thực tế dưới áp lực. Điều này cho phép:

  • Định thời gian chi tiết hơn: Định thời gian từng bước bên trong hot path, thời gian xây dựng bộ nhớ đệm cho mỗi thao tác và dấu thời gian vòng đời giao dịch theo dõi mọi giai đoạn từ gửi đến xác nhận.
  • Hồ sơ và flight recorder theo yêu cầu: Chúng tôi chụp các hồ sơ CPU, bộ nhớ và thực thi theo yêu cầu trong quá trình tải. Flight recorder của Go cung cấp cho chúng tôi thu thập dấu vết luôn bật. Khi một sự kiện chậm được phát hiện, vài giây cuối cùng của thực thi được tự động chụp lại, cho phép chúng tôi chẩn đoán các đỉnh độ trễ tạm thời sau khi thực tế.
  • Theo dõi phân tán: Chúng tôi đã trang bị cho mọi chức năng chính trong hot path các span dấu vết, cung cấp cho chúng tôi khả năng hiển thị chi tiết về chính xác nơi thời gian được sử dụng trong một chu kỳ cập nhật duy nhất, xuyên qua các ranh giới dịch vụ.

Giám sát hai môi trường

Chúng tôi giám sát chặt chẽ cả hai môi trường (mainnet và loadtest) nhưng ở các mức độ chi tiết khác nhau.

Mainnet được giám sát chặt chẽ theo thời gian thực. Chúng tôi theo dõi mọi thứ quan trọng đối với trải nghiệm giao dịch:

  • Theo dõi độ tươi mới: Chúng tôi đo độ trễ thông qua kênh WebSocket \order_book\ trên các thị trường hoạt động mạnh nhất. Về cơ bản, độ trễ giữa khi một bản cập nhật sổ lệnh được tạo ra và khi một khách hàng nhận được nó. Đây là chỉ số phản ánh trực tiếp nhất trải nghiệm mà các trader cảm nhận: sổ lệnh bạn đang xem cũ đến mức nào? Chúng tôi theo dõi điều này tại hai điểm. Quan trọng nhất là engine thực thi → khách hàng: toàn bộ đường ống từ khi engine thực thi xử lý một thay đổi trạng thái đến khi bản cập nhật \order_book\ kết quả đến được khách hàng. Chúng tôi cũng đo API layer → khách hàng: API layer đóng dấu thời gian cho mỗi bản cập nhật gửi đi và khách hàng so sánh nó với đồng hồ của chính nó, cho chúng tôi độ trễ dặm cuối một cách độc lập.
  • Độ trễ dry-run: API layer dry-run mọi giao dịch (xác thực chữ ký, nonce và số dư) trước khi chuyển nó đến engine thực thi. Mỗi dry-run được tính thời gian và báo cáo.
  • Vòng đời giao dịch end-to-end: Biểu đồ histogram độ trễ full-cycle từ gửi đến xác nhận.
  • Hiệu quả bộ nhớ đệm: Tần suất chúng tôi phục vụ từ bộ nhớ so với dựa vào bộ lưu trữ chậm hơn.

Loadtest được giám sát chi tiết hơn nữa. Bởi vì nó không phục vụ các trader thực, chúng tôi có thể tăng cường đo lường công cụ mà không lo lắng về chi phí: định thời gian từng bước bên trong hot path, hồ sơ cấp phát, các histogram bucket bổ sung trên mọi thay đổi bộ nhớ đệm. Chi tiết hơn này bắt các hồi quy vi mô mà các chỉ số an toàn sản xuất của mainnet sẽ không tiết lộ.

Giám sát mainnet cho chúng tôi biết hệ thống đang hoạt động như thế nào cho các trader thực ngay bây giờ. Giám sát loadtest bắt các hồi quy trước khi chúng đến sản xuất.

Loại bỏ sao chép sâu và cuộc chiến cấp phát heap

Garbage collector của Go là một khoản thuế độ trễ. Mỗi cấp phát heap cuối cùng trở thành một lần dừng GC, và các lần dừng GC trên một engine giao dịch là các đỉnh độ trễ cho các trader. Vì vậy, chúng tôi đã đi qua hot path và cắt giảm cấp phát bất cứ nơi nào có thể. Kết quả là ít đỉnh độ trễ hơn và hiệu suất dễ dự đoán hơn.

Loại bỏ sao chép sâu

Sao chép sâu là các nhà máy cấp phát. Chúng tôi đã giải quyết chúng theo một vài cách:

  • Bản chụp nhanh bất biến: Làm cho các bộ nhớ đệm sổ lệnh trở nên bất biến. Các lần đọc trả về một con trỏ đến bản chụp nhanh hiện tại, không cần sao chép. Các bản cập nhật tạo một phiên bản mới và hoán đổi nó nguyên tử thông qua \atomic.Pointer\.
  • Loại bỏ các bản sao không cần thiết: Tìm thấy các đường dẫn mã đã sao chép sâu dữ liệu không bao giờ được thay đổi sau đó. Loại bỏ chúng hoàn toàn.

Thiết kế nhận thức cấp phát

  • Số học trên stack: Thay thế \big.Int\ và \big.Rat\ nặng trên heap bằng các lựa chọn thay thế trên stack (\int128\, \float64\, \int64\ division) trên hot path: chuyển đổi giá, bản đồ độ sâu sổ lệnh, tính toán kích thước. Nhanh hơn tới 8,3 lần trên các chức năng định dạng chính, không cấp phát heap mỗi thao tác.
  • Cập nhật có điều kiện: Bỏ qua cấp phát khi không có gì thay đổi.
  • Bộ sưu tập đã định kích thước trước: Loại bỏ các chu kỳ tăng và sao chép bằng cách định kích thước cấu trúc dữ liệu ngay từ đầu.
  • Tái sử dụng bộ đệm: Đường dẫn người đăng ký giải tuần tự hóa hàng ngàn bản cập nhật mỗi giây. Chúng tôi đã giảm cấp phát bằng cách tái sử dụng dựa trên pool của các bộ đệm trung gian.

Điều này đã làm phẳng đuôi. Trước đây, p99 end-to-end của Lighter (engine thực thi đến khách hàng, đo trên kênh WebSocket \order_book\) sẽ tăng vọt lên 200–280 ms trong các giai đoạn cấp phát nặng. Sau khi loại bỏ sao chép sâu, bộ nhớ đệm bất biến và công việc cấp phát heap, p99 ổn định ở dải ~50–60 ms phẳng với hầu như không có đỉnh:

Lighter - inline image

Độ trễ End-to-End: Engine thực thi -> Khách hàng (p99)

Áp lực GC trên API layer cũng giảm đáng kể, nhưng đây không chỉ là một thay đổi. Chúng tôi đã thiết kế lại cơ bản cách các bộ nhớ đệm của máy chủ API hoạt động. Trước đây, các bộ nhớ đệm có thời gian hết hạn dựa trên TTL và cấp phát các đối tượng mới trên mỗi bản cập nhật. Mỗi cấp phát tồn tại ngắn đó trở thành công việc GC. Sau khi thiết kế lại, toàn bộ trạng thái sàn giao dịch được khởi động nóng từ một bản chụp nhanh và được giữ trong bộ nhớ dưới dạng các cấu trúc bất biến tồn tại lâu dài được hoán đổi nguyên tử. Điều đó đã loại bỏ sự xáo trộn TTL và các cấp phát mỗi bản cập nhật, và áp lực GC giảm theo. Chúng tôi giám sát chặt chẽ việc sử dụng bộ nhớ và tập làm việc được giới hạn.

Chúng tôi đã chạy apiserver kiểu cũ và apiserver đã thiết kế lại song song trên lưu lượng mainnet. Thời gian dừng GC (p75) của máy chủ đã thiết kế lại ở khoảng ~3 ms, so với ~5–6 ms trên máy chủ cũ. Khoảng một nửa thời gian dừng GC:

Lighter - inline image

Thời gian dừng GC: Máy chủ API tiêu chuẩn so với Snapshot

Tần suất kể một câu chuyện rõ ràng hơn. Apiserver cũ kích hoạt các chu kỳ GC ~2,2 lần thường xuyên hơn so với máy chủ đã thiết kế lại, phản ánh trực tiếp ít cấp phát tồn tại ngắn hơn và ít áp lực GC hơn tổng thể:

Lighter - inline image

Tần suất chu kỳ GC: Máy chủ API tiêu chuẩn so với Máy chủ API Snapshot

Tinh chỉnh GOGC trên Engine thực thi

Chúng tôi đã áp dụng tư duy GC tương tự cho chính engine thực thi. Tham số \GOGC\ của Go kiểm soát mức độ tích cực của garbage collector. Mặc định đánh đổi thời gian CPU cho hiệu quả bộ nhớ, nhưng đối với một đường dẫn nhạy cảm với độ trễ, sự đánh đổi là sai.

Sau khi tinh chỉnh \GOGC\, thời gian GC của engine thực thi giảm từ trung bình ~30 µs với các đỉnh đạt 100 µs, xuống còn dải ~10 µs ổn định. Giảm ~3 lần với hầu như không có đỉnh:

Lighter - inline image

Thời gian GC của Engine thực thi sau khi tinh chỉnh GOGC

Ba tháng dữ liệu mainnet xác nhận cải tiến đã được giữ vững: giai đoạn sau phẳng và có thể dự đoán được.

Xử lý giao dịch cũng được cải thiện theo. Qua 90 ngày dữ liệu mainnet bao gồm mọi loại giao dịch (tạo lệnh, hủy, thanh lý, giảm đòn bẩy, chuyển tiền và nhiều hơn nữa), p99 giảm từ các đỉnh thường xuyên 20–30 ms trước khi tinh chỉnh GOGC xuống chủ yếu dưới 1 ms sau đó, với các ngoại lệ thỉnh thoảng 3–4 ms. Cải tiến đã được giữ vững trong hơn hai tháng:

Lighter - inline image

Thời gian xử lý giao dịch - p99 (90 ngày qua)

Dịch vụ Snapshot — Ít dữ liệu hơn, Triển khai nóng

Sau khi chúng tôi loại bỏ chi phí cấp phát, chúng tôi giải quyết nút thắt tiếp theo: khối lượng dữ liệu chảy qua bus tin nhắn nội bộ. Chúng tôi đã xây dựng một dịch vụ snapshot duy trì các bản chụp nhanh trạng thái trong bộ nhớ đầy đủ. Điều này có hai tác động chính:

  • Ít dữ liệu trên đường truyền. Với dịch vụ snapshot giữ trạng thái đầy đủ, engine thực thi không còn cần phải đẩy trạng thái hoàn chỉnh qua bus tin nhắn trên mỗi bản cập nhật. Nó ghi ít dữ liệu hơn, có nghĩa là ít băng thông mạng được tiêu thụ hơn và ít công việc giải tuần tự hóa ở đầu nhận.
  • Triển khai không cần làm nóng. Khi triển khai, API layer khởi động từ một bản chụp nhanh (tài khoản, sổ lệnh, thông tin thị trường, khóa công khai API) và sẵn sàng phục vụ ngay lập tức. Không có giai đoạn làm nóng trong khi các bộ nhớ đệm được lấp đầy. Sau khi tải bản chụp nhanh, API layer đăng ký luồng cập nhật và áp dụng các delta theo thời gian thực.

Trên cơ sở snapshot, các bộ nhớ đệm trong bộ nhớ được cập nhật liên tục:

  • Thông tin tài khoản: Đọc không khóa thông qua \sync.Map\, hoán đổi con trỏ nguyên tử cho các bản cập nhật.
  • Sổ lệnh: Được lưu trữ dưới dạng các bản chụp nhanh bất biến. Các lần đọc nhận được một con trỏ, các bản cập nhật hoán đổi một phiên bản mới. Không có khóa trên đường đọc.
  • Bộ nhớ đệm khóa API: Tất cả các khóa đều vừa trong bộ nhớ. Loại bỏ hoàn toàn các tra cứu bên ngoài.

Kết quả: toàn bộ trạng thái sàn giao dịch nằm trong bộ nhớ cục bộ, được cập nhật theo thời gian thực và mọi lần triển khai đều bắt đầu nóng.

Hiệu ứng lan tỏa lên engine thực thi là đáng kể. Trước đây, engine thực thi đang ghi các khóa bộ nhớ đệm qua mạng, các bản cập nhật mà các dịch vụ sẽ đọc theo thời gian. Với mọi thứ nằm trong bộ nhớ và dịch vụ snapshot xử lý phân phối trạng thái, các ghi mạng đó trở nên không cần thiết. Chúng tôi đã xóa chúng. Kết quả: p99 thời gian khối giảm từ ~2,6 ms xuống ~1,2–1,8 ms, chỉ đơn giản là do engine thực thi ghi ít hơn nhiều:

Lighter - inline image

Thời gian khối - p99

Lập kế hoạch CPU

Khi chúng tôi đã tối ưu hóa mọi thứ trong không gian người dùng, chúng tôi đã tìm đến kernel.

Chúng tôi có nhiều goroutine và chúng cần được lập lịch trên CPU để chạy. Giảm thiểu chi phí lập lịch là quan trọng đối với các hệ thống có độ trễ thấp. Theo mặc định, runtime của Go đa kênh các goroutine qua các luồng OS và OS có thể di chuyển các luồng qua các lõi CPU một cách tự do. Cả hai đều đưa vào độ trễ không thể dự đoán.

Chúng tôi đã loại bỏ điều này bằng cách xếp chồng bốn cơ chế:

  1. `runtime.LockOSThread()`: Khóa goroutine vào một luồng OS duy nhất, ngăn scheduler của Go di chuyển nó.
  2. Ái lực CPU qua `sched_setaffinity`: Gắn luồng OS đó vào một lõi CPU cụ thể (Linux). Điều này ngăn kernel di chuyển nó giữa các lõi, tránh vô hiệu hóa bộ nhớ đệm L1/L2.
  3. Lập lịch ưu tiên cao qua SCHED_FIFO: Nâng mức ưu tiên lập lịch của luồng, đảm bảo kernel ưu tiên nó hơn các công việc khác.
  4. Vòng lặp spin busy-wait: Hot path chạy một \select\ với một trường hợp \default\ trống, do đó goroutine không bao giờ đỗ. Nếu không có nó, Go di chuyển goroutine đến trạng thái "có thể chạy" khi không có dữ liệu khả dụng và việc lập lịch lại thêm độ trễ đánh thức. Với vòng lặp spin, goroutine tiếp tục chạy trên lõi đã gắn và nhận các bản cập nhật mới với độ trễ lập lịch bằng không.

Đo lường tác động một cách chính xác rất quan trọng ở đây. Thời gian áp dụng thay đổi theo điều kiện lưu lượng, do đó các con số tuyệt đối thay đổi theo tải. Để cô lập tác động của việc gắn, chúng tôi đã chạy hai nhóm máy chủ API song song dưới cùng một lưu lượng. Một nhóm vẫn không được gắn làm đối chứng và chúng tôi chuyển nhóm kia sang gắn CPU. Sự khác biệt phần trăm giữa chúng, được đo cùng lúc dưới cùng một tải, cho thấy câu chuyện thực sự.

Trước khi gắn, cả hai nhóm theo dõi cùng nhau. Cùng độ trễ cơ sở, cùng hành vi đỉnh:

Lighter - inline image

Thời gian áp dụng Hot Path - Trước khi lập kế hoạch CPU

Sau khi kích hoạt gắn trên một nhóm, nó luôn nằm dưới đường cơ sở không gắn. Cùng lưu lượng, độ trễ thấp hơn. Các đỉnh của nhóm đã gắn cũng được giới hạn thấp hơn, bởi vì nhiễu di chuyển luồng và vô hiệu hóa bộ nhớ đệm L1/L2 đã được loại bỏ:

Lighter - inline image

Thời gian áp dụng Hot Path - Sau khi lập kế hoạch CPU

Triển khai nhận thức NUMA

Chỉ gắn CPU thôi là chưa đủ nếu các truy cập bộ nhớ của lõi đã gắn vượt qua ranh giới NUMA. Một nút NUMA là một nhóm CPU có bộ nhớ cục bộ của riêng chúng. Truy cập bộ nhớ từ một nút NUMA từ xa có hình phạt 10 lần so với truy cập cục bộ.

Các máy chủ API của Lighter ban đầu chạy trên các máy lớn hơn với 2 nút NUMA:

text
1$ lscpu | grep NUMA
2NUMA node(s): 2
3NUMA node0 CPU(s): 0-95
4NUMA node1 CPU(s): 96-191
5
6$ cat /sys/devices/system/node/node0/distance
710 100

Ma trận khoảng cách kể câu chuyện: truy cập cục bộ có giá 10, truy cập chéo nút có giá 100. Hình phạt 10 lần. Với N máy chủ API chia sẻ máy, một số chắc chắn có CPU đã gắn trên một nút NUMA và bộ nhớ làm việc của chúng (bộ nhớ đệm trong bộ nhớ, bộ đệm cập nhật) trên nút kia. Mỗi lần lặp hot path phải trả thuế chéo nút.

Sửa chữa là phản trực giác: chúng tôi đã chuyển các máy chủ API sang các máy nhỏ hơn với một nút NUMA duy nhất. Một nửa thông số kỹ thuật, nhưng tất cả các truy cập bộ nhớ hiện được đảm bảo là cục bộ:

text
1$ lscpu | grep NUMA
2NUMA node(s): 1
3NUMA node0 CPU(s): 0-95

Điều này đã cắt giảm chi phí và cải thiện độ trễ cùng một lúc. Thời gian áp dụng hot path giảm thêm xuống phạm vi ~100–250 µs, với giờ thấp điểm giảm xuống ~100 µs. So sánh với phạm vi ~200–520 µs chỉ với gắn CPU trên các máy 2 nút NUMA:

Lighter - inline image

Thời gian áp dụng Hot Path - Tối ưu hóa NUMA

Tuần tự hóa nhị phân tùy chỉnh

Engine thực thi của Lighter xuất bản các bản cập nhật trạng thái đến API layer thông qua một bus tin nhắn nội bộ. Mọi bản cập nhật hệ thống đều chảy qua đường dẫn này. Tuần tự hóa ban đầu sử dụng một thư viện mã hóa mục đích chung, nhưng reflection, type switches và cấp phát trên mỗi trường đã tạo ra chi phí không cần thiết trên một hot path.

Chúng tôi đã thay thế nó bằng tuần tự hóa nhị phân tự viết: bố cục cố định, giải mã không reflection cho mọi loại thực thể trong hệ thống. Mỗi loại có một bộ mã hóa/giải mã chuyên dụng đọc và ghi các trường tại các offset byte đã biết. Không có reflection hoặc type switches và cấp phát tối thiểu. Mọi codec đều có các bài kiểm tra round-trip và fuzz để bắt các hồi quy.

Kết quả là chi phí tuần tự hóa giảm đáng kể trên đường dẫn nuôi dưỡng mọi bộ nhớ đệm hạ nguồn.

Điểm chuẩn độc lập có sẵn thông qua một bảng điều khiển được xây dựng và duy trì bởi thành viên cộng đồng @UngusTrade, so sánh độ trễ giao dịch trực tiếp trên các địa điểm giao dịch perpetual: latency.perps.trading

Lưu một chạm

Đọc sâu bài viết viral bằng AI trong YouMind

Save the source, ask focused questions, summarize the argument, and turn a viral article into reusable notes in one AI workspace.

Explore YouMind
Dành cho nhà sáng tạo

Biến Markdown của bạn thành bài viết 𝕏 gọn gàng

Khi bạn đăng bài viết dài của riêng mình, việc định dạng hình ảnh, bảng và khối mã cho 𝕏 rất mệt mỏi. YouMind biến cả bản nháp Markdown thành một bài viết 𝕏 gọn gàng, sẵn sàng để đăng.

Thử Markdown sang 𝕏

Thêm pattern để giải mã

Bài viết viral gần đây

Khám phá thêm bài viết viral