
Từ ClickHouse đến RocksDB: Cách chúng tôi xây dựng lại lớp lưu trữ lịch sử của Solana
Mục lục
- Lưu trữ lịch sử Solana là gì và tại sao nó quan trọng
- ClickHouse: Lựa chọn thực dụng ban đầu
- Điểm ClickHouse bắt đầu thất bại
- RocksDB: Không phải cơ sở dữ liệu
- Cách RocksDB khắc phục những hạn chế của ClickHouse
- Những gì chúng tôi xây dựng bên trên
- Tối ưu RocksDB cho quy mô lớn
- Tinh chỉnh theo từng chỉ mục, không phải theo cơ sở dữ liệu
- Xác định xem bạn có thực sự cần WAL hay không
- Điều chỉnh bộ lọc bloom theo tỷ lệ hit/miss
- Nén dữ liệu có thể nén, không phải dữ liệu được truy cập thường xuyên
- Cân nhắc I/O trực tiếp
- Chọn bộ nhớ đệm chịu được tranh chấp
- Song song hóa tra cứu nhiều khóa thay vì tuần tự hóa
- Mục tiêu chúng tôi đang hướng đến
Khi chúng tôi nói rằng mình đã chuyển hơn 300 terabyte dữ liệu lịch sử của Solana khỏi ClickHouse sang RocksDB, phản ứng gần như luôn giống nhau: Tại sao lại làm như vậy?
ClickHouse là lựa chọn hiển nhiên cho khối lượng công việc phân tích ở quy mô petabyte, còn RocksDB thì không.
ClickHouse được một số đơn vị tiêu thụ dữ liệu lớn nhất thế giới tin dùng.
Ví dụ, Cloudflare vận hành ClickHouse trên hơn một nghìn bản sao để xử lý hàng trăm triệu thao tác chèn mỗi giây.
Anthropic vận hành một hệ thống ClickHouse tùy chỉnh, cách ly mạng để hỗ trợ khả năng quan sát của Claude. Uber, eBay và Bloomberg cũng đã sử dụng ClickHouse trong môi trường production suốt nhiều năm.
Mặt khác, RocksDB là một kho khóa-giá trị nhúng với tài liệu khá hạn chế, chủ yếu được dùng làm engine bên trong các cơ sở dữ liệu khác (ví dụ: CockroachDB, TiKV, MyRocks), thay vì làm nền tảng trực tiếp cho một dịch vụ dữ liệu lịch sử hướng đến khách hàng.
Tuy nhiên, việc chuyển đổi đã giảm dung lượng lưu trữ nén của chúng tôi từ ~330TB xuống ~190TB, khắc phục phần đuôi dài của các truy vấn chậm nhất (ví dụ: độ trễ p95 của các lệnh gọi getTransactionsForAddress giảm từ 350ms xuống 30ms) và đưa chúng tôi vào vị thế có thể đổi mới lớp đọc của Solana—điều không thể thực hiện với ClickHouse ở mức hiệu năng và quy mô mà Helius yêu cầu.
Bài viết này giải thích lý do chúng tôi di chuyển khỏi ClickHouse, những điều học được về khối lượng công việc trong quá trình đó và cách chúng tôi sử dụng RocksDB trong môi trường production ở quy mô lớn.
Lưu trữ lịch sử Solana là gì và tại sao nó quan trọng
Về bản chất, Solana là một mạng lưới các node giao tiếp để thống nhất về thông tin mới mà không cần tin tưởng lẫn nhau.
Một node là máy tính trên Solana chạy một client (ví dụ: Agave, Firedancer) tuân thủ một bộ quy tắc cụ thể để hỗ trợ việc thống nhất thông tin mới.
Validator là một node Solana bảo vệ mạng bằng cách tạo block (tức là nối thêm các nhóm transaction vào sổ cái của Solana) và bỏ phiếu về tính hợp lệ của các block khác.
Các node RPC là những validator không tham gia tạo block hoặc bỏ phiếu. Thay vào đó, chúng quan sát mạng và theo dõi mọi thông tin mới mà mạng tạo ra. RPC cho phép người dùng truy vấn mạng để lấy dữ liệu cụ thể bằng đặc tả JSON-RPC.
Tuy nhiên, các node này không lưu dữ liệu mãi mãi.
Để đáp ứng các yêu cầu phần cứng của Solana, những block, transaction và trạng thái account cũ sẽ bị lược bỏ, nhờ đó các node chỉ giữ lại góc nhìn gần nhất về lịch sử Solana.
Điều này gây khó khăn khi cần tra cứu chữ ký transaction từ một năm trước, lấy mọi chữ ký từng tương tác với một ví trong suốt vòng đời của ví hoặc quét toàn bộ lịch sử thực thi của một chương trình.
Theo nghĩa rộng, lưu trữ lịch sử là toàn bộ lớp lưu dữ liệu của Solana kể từ block khởi nguyên. Lớp này ghi lại và lập chỉ mục mọi thứ do chuỗi tạo ra—mọi block, transaction, tương tác account, Cross Program Invocation (CPI) và nhật ký transaction—đồng thời duy trì khả năng truy vấn vượt quá khoảng thời gian lưu trữ ngắn hạn tiêu chuẩn ~2 ngày (tức 1 epoch).
Lớp lưu trữ lịch sử giúp các truy vấn dữ liệu quá khứ trở nên khả thi.
Phương pháp mặc định để lưu dữ liệu lịch sử trên Solana là Google BigTable. Anza duy trì một phiên bản BigTable mà các nhà cung cấp RPC có thể truy cập theo nhu cầu để phục vụ truy vấn lịch sử.
Giải pháp này hoạt động, nhưng BigTable đắt đỏ, chi phí dữ liệu đầu ra khiến việc vận hành bản sao riêng trở nên tốn kém và gần như không có công việc kỹ thuật nào ở phía bạn có thể giúp nó nhanh hơn—bạn phụ thuộc hoàn toàn vào hệ thống lưu trữ của Google, phải chịu toàn bộ cơ cấu chi phí nhưng không có quyền kiểm soát.
Old Faithful là một bộ công cụ do Triton One duy trì, có thể tạo Content Addressable Archives (CAR) từ các kho lưu trữ sổ cái RocksDB và phân phối chúng qua các giao diện RPC và gRPC tiêu chuẩn của Solana. Đây là một bước tiến đáng kể trong việc phi tập trung hóa lớp lưu trữ lịch sử của Solana, cung cấp một nguồn lịch sử dự phòng có giá trị và có thể xác minh.
Tuy nhiên, những đánh đổi về trải nghiệm nhà phát triển và hiệu năng (ví dụ: để bắt đầu cần có công cụ tùy chỉnh thay vì các giao diện mà đội ngũ có thể dùng để xây dựng, đồng thời giải pháp này được tối ưu cho lưu trữ bền vững hơn là hiệu năng và quy mô) khiến nó không thể trở thành một lựa chọn thay thế thực sự cho các khối lượng công việc nhạy cảm với độ trễ.
Vấn đề cốt lõi của lưu trữ lịch sử là bạn có N petabyte dữ liệu thô. Với hơn 500 tỷ transaction, ~1,3 nghìn tỷ hàng chỉ mục từ account đến transaction và các mẫu truy cập ngẫu nhiên, làm thế nào để tra cứu trong chưa đến 10ms? Truy vấn đầu cuối dưới 50ms sẽ hoạt động ra sao? Cả Google BigTable lẫn Old Faithful đều không đưa ra được giải pháp cho vấn đề này.
Hơn nữa, nếu muốn xây dựng các tùy chọn lọc và sắp xếp tùy chỉnh hoặc cải thiện trải nghiệm nhà phát triển bằng cách cung cấp thứ gì đó phong phú hơn các phương thức JSON-RPC tiêu chuẩn, bạn sẽ cần chỉ mục lưu trữ lịch sử riêng.
Vì vậy, chúng tôi đã xây dựng một chỉ mục.
ClickHouse: Lựa chọn thực dụng ban đầu
ClickHouse là điểm khởi đầu thực dụng nhất để xây dựng hệ thống lưu trữ lịch sử mới của chúng tôi. Đây là con đường nhanh nhất để ra mắt một sản phẩm vốn đã tốt hơn sản phẩm được xây dựng trên BigTable.
ClickHouse là một cơ sở dữ liệu dạng cột trưởng thành với công cụ và tài liệu rất tốt. Nó nén hiệu quả dữ liệu chuỗi thời gian, điều đặc biệt hữu ích cho Solana vì dữ liệu block về cơ bản được sắp xếp theo thời gian.
Việc tạo cơ sở dữ liệu, viết SQL, lặp lại quá trình thiết kế schema và tối ưu thời gian đưa sản phẩm ra thị trường đều khá đơn giản, nhờ đó khách hàng có thể sớm sử dụng sản phẩm.
ClickHouse không tự mình phục vụ toàn bộ lưu lượng. Thay vào đó, nó là tầng dưới cùng trong ngăn xếp lưu trữ của chúng tôi, được tổ chức theo độ mới của dữ liệu.
Dữ liệu mới nhất (tức khoảng một đến hai phút gần nhất hoặc vài trăm slot) nằm trong một kho lưu trữ trong bộ nhớ. Dữ liệu của khoảng hai tuần gần nhất nằm trong Postgres. ClickHouse lưu phần lịch sử còn lại của Solana.
Một router thông minh đứng trước ba nguồn này để các truy vấn điểm được gửi đến tầng nóng nhất có chứa dữ liệu, còn truy vấn phạm vi được chia tách giữa các tầng rồi ghép lại thành một kết quả duy nhất.
ClickHouse hoạt động tương đối tốt với các khối lượng công việc mà nó được thiết kế để xử lý. Ví dụ: “cung cấp mọi block trong phạm vi slot này” hoặc “quét hoạt động của account này theo thời gian”.
Các truy vấn chuỗi thời gian hoạt động ổn định. Chúng tôi có thể nạp bù dữ liệu, chạy phân tích tùy biến và trả lời hầu hết câu hỏi mà các nhà cung cấp RPC trước đây cần xử lý. Phía nhập dữ liệu chưa bao giờ gặp khó khăn; chúng tôi chỉ ghi ~10MB/s vào chỉ mục qua LaserStream.
Sai lầm khi chọn ClickHouse không nằm ở việc chúng tôi chọn ClickHouse. Sai lầm là giả định rằng khối lượng công việc sẽ tiếp tục ở dạng mà ClickHouse có thể xử lý trên quy mô lớn.
Điểm ClickHouse bắt đầu thất bại
Không phải mọi phương thức lịch sử của Solana mà chúng tôi quan tâm đều thuộc loại chuỗi thời gian. Một chữ ký trên Solana (tức mã định danh transaction dùng chung) về cơ bản là 64 byte dữ liệu ngẫu nhiên.
Khi ai đó gọi getTransaction cho một chữ ký nhất định, đó là một truy vấn điểm ngẫu nhiên đồng đều. Không có slot, khoảng thời gian hay bất kỳ tính cục bộ nào để tận dụng.
Điều tương tự cũng xảy ra với các truy vấn khác, chẳng hạn như “lấy tất cả chữ ký từng tương tác với account này”, vì khóa account cũng thực chất là ngẫu nhiên. Vấn đề này xuất hiện bất cứ khi nào khóa chính là UUID, hàm băm hoặc một mã định danh có entropy cao khác.
Các engine dạng cột không được xây dựng để giải quyết vấn đề này.
ClickHouse lưu dữ liệu trong các phần đã sắp xếp và sử dụng chỉ mục chính thưa. Điều này có nghĩa là không có nhiều dữ liệu để loại trừ khi tra cứu khóa ngẫu nhiên. Đến một lúc nào đó, hệ thống sẽ phải đọc nhiều granule hơn mong muốn. Một lần tra cứu chữ ký có thể chạm tới 20 granule—tương đương khoảng 10.000 hàng và ~60 thao tác I/O ổ đĩa—trước cả chi phí tìm kiếm nhị phân hoặc CPU.
Chỉ riêng I/O đã mất khoảng 5ms cho một lần tra cứu. Vì kho lưu trữ có dạng cột, việc đọc một transaction đồng nghĩa với đọc ~15 cột dưới dạng các thao tác I/O riêng biệt trên một granule 512 hàng.
Điều này có nghĩa là, chẳng hạn, một lệnh gọi getTransactionsForAddress yêu cầu đầy đủ chi tiết transaction sẽ phân nhánh thành tối đa 100 lần tra cứu như vậy, đẩy ngưỡng độ trễ tối thiểu lên gần 100ms trước khi một byte duy nhất được tuần tự hóa.
Một batch getTransaction lớn (tức tối đa 1.000) đẩy ngưỡng này lên gần một giây.
Các cơ chế trừu tượng giúp quét nhanh (ví dụ: thực thi vector hóa, cụ thể hóa muộn) không giải quyết hoàn toàn vấn đề này. Các lượt tra cứu đã được tối ưu gần như đến giới hạn. Chúng tôi không thiếu chỉ mục. Không có phép chiếu nào có thể bổ sung.
Nói đơn giản, cách bố trí dữ liệu cơ bản là sai đối với yêu cầu của chúng tôi.
Các phương thức tốn kém nhất mà chúng tôi bổ sung (ví dụ: getTransactionsForAddress, getTransfersByAddress) chính là các mẫu khóa ngẫu nhiên mà ClickHouse xử lý kém nhất.
Một số truy vấn mất 2–3 giây, dù lẽ ra chỉ cần vài mili giây. Chúng tôi liên tục nhận cảnh báo về hiệu năng suy giảm đối với những khối lượng công việc không thể khắc phục tận gốc.
Vì các tổ chức và khách hàng doanh nghiệp đang sử dụng Helius ở quy mô lớn, điều này hoàn toàn không thể chấp nhận về lâu dài.
Mở rộng ClickHouse cho khối lượng công việc này đồng nghĩa với tăng số lượng máy chủ và ở quy mô của chúng tôi, điều đó đồng nghĩa với việc nhân khối lượng công việc lên nhiều lần. Chỉ đổ tiền vào phần cứng sẽ không giải quyết được vấn đề.
Ngăn xếp RPC của Solana đang trưởng thành. Hệ sinh thái đã đạt đến bước ngoặt mà các phương thức JSON-RPC tiêu chuẩn không còn đủ nữa. Khách hàng yêu cầu các truy vấn lịch sử phong phú và sẽ không có ai khác xây dựng chúng trên BigTable.
Nếu muốn mở rộng giới hạn đó, chúng tôi phải thay đổi lớp lưu trữ.
RocksDB: Không phải cơ sở dữ liệu
Bất chấp tên gọi, RocksDB không phải là cơ sở dữ liệu. Nó là một thư viện.
Không có ngôn ngữ truy vấn, giao thức client/server, engine SQL, phép join hay chỉ mục theo nghĩa cơ sở dữ liệu truyền thống. Đây là một giao diện nói rằng: “Đây là khóa (một số byte), đây là giá trị (cũng là một số byte), hãy lưu nó bền vững trên ổ đĩa; sau đó, trả lại giá trị cho khóa này.” Chỉ vậy thôi.
Nó là một thành phần nguyên thủy. Có thể nói là thành phần nguyên thủy để xây dựng các engine lưu trữ.
Mọi thứ thường được kỳ vọng ở một cơ sở dữ liệu truyền thống (ví dụ: trình lập kế hoạch truy vấn, giao thức truyền dẫn, sao chép, khả năng quan sát) đều phải được xây dựng.
Điều này nghe có vẻ là bất lợi, cho đến khi hiểu nó mang lại điều gì. Một kho khóa-giá trị thuần túy được hỗ trợ bởi cây Log-Structured Merge (LSM) trên ổ đĩa chính xác là cấu trúc phù hợp cho các lượt tra cứu khóa ngẫu nhiên đồng đều với thông lượng cao. Không có trình lập kế hoạch truy vấn gây thêm chi phí, không có giả định về bố cục dạng cột cần dung hòa và không có frontend SQL cần tính vào ngân sách tài nguyên.
Bạn lưu các byte, đọc chúng trở lại và đặt bộ lọc bloom cùng bộ nhớ đệm ở đúng vị trí để giữ chi phí đọc ngẫu nhiên ở mức thấp.
Đây chính là lúc cách nhìn nhận nó như một anti-pattern không còn đứng vững.
Chúng tôi không thay ClickHouse bằng RocksDB—chúng tôi thay ClickHouse bằng một cơ sở dữ liệu tùy chỉnh có engine lưu trữ là RocksDB. Đây là hai việc khác nhau đáng kể.
Đây cũng chính là mô hình mà hầu hết cơ sở dữ liệu production sử dụng bên trong. Ví dụ: CockroachDB, TiKV, MyRocks và các kho trạng thái Kafka Streams đều được xây dựng trên RocksDB.
Điểm khác biệt là họ bọc RocksDB trong một cơ sở dữ liệu khác trước, còn chúng tôi tự xây dựng cơ sở dữ liệu xoay quanh RocksDB và tinh chỉnh nó cho đúng các mẫu truy cập mà hệ thống lưu trữ lịch sử yêu cầu.
Cách RocksDB khắc phục những hạn chế của ClickHouse
Vậy chính xác thì một kho khóa-giá trị giải quyết vấn đề khóa ngẫu nhiên mà engine dạng cột không thể xử lý như thế nào? Tất cả phụ thuộc vào cách các byte được lưu trên ổ đĩa.
RocksDB sử dụng cơ chế nén theo tầng. Các lần ghi mới đi vào tầng 0, một khu vực tập kết chưa sắp xếp—về cơ bản giống tình huống trong ClickHouse—nơi các phần trong một phân vùng không được sắp xếp toàn cục.
Tuy nhiên, các tầng từ 1 đến N là những dải dữ liệu được sắp xếp hoàn toàn. Sau khi dữ liệu được nén, metadata min/max thực sự có thể loại trừ phạm vi tìm kiếm, ngay cả với các khóa ngẫu nhiên đồng đều, vì tầng đó được sắp xếp toàn cục. Theo một nghĩa nào đó, mọi thứ trong ClickHouse đều hoạt động giống tầng 0 của RocksDB.
Chúng tôi tận dụng đặc điểm này cho việc nạp bù dữ liệu.
Khi xây dựng chỉ mục chữ ký, chúng tôi sắp xếp trước toàn bộ lịch sử và tải thẳng vào tầng dưới cùng. Từ đó trở đi, chỉ dữ liệu mới đi vào tầng 0 và nhanh chóng được hợp nhất xuống dưới.
Kết quả là gần như mọi lượt tra cứu đều đọc từ một dải dữ liệu đã sắp xếp duy nhất. Về cơ bản, chúng tôi đã sắp xếp dữ liệu ngẫu nhiên bằng RocksDB.
Những gì chúng tôi xây dựng bên trên
ClickHouse cung cấp sẵn rất nhiều thứ: engine truy vấn, giao thức truyền dẫn, client và cách phân phối dữ liệu. RocksDB không cung cấp tùy chọn nào trong số này; nó chỉ lưu byte bền vững. Mọi thứ khác đều do chúng tôi xây dựng.
Vì vậy, chúng tôi xây dựng cơ sở dữ liệu riêng trên RocksDB, chuyên biệt cho các mẫu truy cập dữ liệu lịch sử của Solana.
Hiện có hai chỉ mục trong RocksDB:
- Chữ ký -> Vị trí (tức chỉ mục chữ ký, ánh xạ chữ ký 64 byte của một transaction đến slot và vị trí của transaction đó trong block).
- Slot -> Block (tức chỉ mục từ slot đến block, ánh xạ vị trí nêu trên đến dữ liệu transaction).
Điều quan trọng là không có đường dẫn trực tiếp từ chữ ký đến dữ liệu transaction và việc giải quyết hạn chế đó chính là lý do hai chỉ mục này tồn tại.
Về cơ bản, chữ ký là một mã định danh ngẫu nhiên dài 64 byte. Thứ thực sự cho biết vị trí của transaction là slot và chỉ mục của nó trong block đó. Vì vậy, việc lấy một transaction sẽ cần hai bước nhảy (một để phân giải chữ ký thành vị trí cụ thể và một để lấy chi tiết transaction từ vị trí đó), còn việc lấy một block chỉ cần một bước vì bên gọi đã cung cấp slot.
Kết hợp lại, các chỉ mục này hỗ trợ một số phương thức nặng nhất mà Helius phục vụ (ví dụ: getBlock, getTransaction và getTransactionsForAddress, đặc biệt khi details được đặt thành full).
Đường đọc trông gần như giống với trong ClickHouse. Một yêu cầu đi vào, client nội bộ của chúng tôi gọi cơ sở dữ liệu và kết quả được trả về. Thứ thay đổi là engine bên dưới. Mỗi phương thức là một đường dẫn được mã hóa cứng và tinh chỉnh thủ công. Khi người dùng truy vấn một transaction, đường dẫn getTransaction chuyên dụng sẽ đi thẳng đến các byte.
Đường dẫn này nhanh vì chúng tôi kiểm soát mọi lớp của nó. Chúng tôi sử dụng io_uring cho I/O tệp và mạng để truyền dữ liệu ra khỏi RocksDB. Khi dữ liệu nằm ở dạng không nén trên ổ đĩa, chúng tôi sao chép thẳng dữ liệu từ ổ đĩa đến card mạng mà không đi vòng qua không gian người dùng.
Các phương thức được mã hóa cứng, I/O được tinh chỉnh đầu cuối và không có thành phần thừa thãi ở giữa.
Lợi ích thể hiện rõ trong môi trường production.
Gần đây, chúng tôi duy trì lưu lượng getBlock ở mức 150 Gbit/s trong năm đến sáu giờ liên tục, đồng thời sử dụng hết công suất của phần lớn card mạng. Không sự cố, không cảnh báo, hiệu năng thô không đổi.
Trên toàn hệ thống,
- Dung lượng lưu trữ nén giảm gần một nửa, từ ~330TB xuống ~190TB
- Độ trễ P95 của các lệnh gọi
getTransactiongiảm từ 7ms xuống 1ms - Độ trễ P95 của các lệnh gọi
getTransactionsForAddressgiảm từ 350ms xuống 30ms - Độ trễ P95 của các lệnh gọi
getBlockgiảm từ 50ms xuống 35ms
Đúng như dự kiến, các lệnh gọi getBlock cải thiện ít nhất. ClickHouse vốn đã lưu transaction theo các khối 512 hàng, giúp phân bổ chi phí của dạng cột cho những lượt đọc có kích thước block. Phần lớn thời gian đầu cuối của getBlock được dành cho mã hóa Base58 và JSON cũng như tái hợp block, vì vậy việc thay engine lưu trữ sẽ không tăng tốc quá trình này.
Câu chuyện sâu hơn ở đây—cách chúng tôi khiến io_uring, Rust bất đồng bộ và một thư viện đồng bộ như RocksDB phối hợp trong khối lượng công việc mạng có thông lượng cao—xứng đáng có một bài viết riêng trong tương lai.
Tuy nhiên, phần sau sẽ đi sâu vào một số tối ưu hóa mà chúng tôi thấy hữu ích khi làm việc với RocksDB.
Tối ưu RocksDB cho quy mô lớn
Không có một cấu hình RocksDB “nhanh” duy nhất. Các thiết lập phù hợp phụ thuộc hoàn toàn vào mẫu truy cập của chỉ mục đang được tinh chỉnh.
Chỉ mục chữ ký -> vị trí và chỉ mục slot -> block của chúng tôi chạy trong cùng một tiến trình trên cùng phần cứng nhưng gần như đối lập hoàn toàn về cách tinh chỉnh.
Vì vậy, thay vì cung cấp một tệp cấu hình với các thiết lập vốn chưa chắc phù hợp với khối lượng công việc của bạn, phần này trình bày cách chúng tôi cân nhắc những đánh đổi có thể áp dụng rộng rãi.
Tinh chỉnh theo từng chỉ mục, không phải theo cơ sở dữ liệu
Mỗi chỉ mục của chúng tôi là một column family riêng với các tùy chọn riêng. Một chỉ mục phục vụ các lượt tra cứu điểm ngẫu nhiên đồng đều; chỉ mục còn lại chứa các giá trị lớn, có thể nén và được lấy theo thứ tự. Việc xử lý chúng giống nhau sẽ bỏ lỡ nhiều cơ hội tối ưu hiệu năng. Gần như mọi quyết định nêu dưới đây nên được hiểu là “với mẫu truy cập này, hãy làm X.”
Xác định xem bạn có thực sự cần WAL hay không
Dữ liệu lịch sử có thể được xây dựng lại. Tức là dữ liệu được truyền vào từ LaserStream và bắt nguồn từ chính chuỗi.
Chúng tôi ghi dữ liệu với Write Ahead Log (WAL) bị tắt, nên không phải trả chi phí cho độ bền của nhật ký ghi trước mà mình không cần.
Điểm tinh tế là việc tắt WAL cũng vô hiệu hóa tính nhất quán mặc định của RocksDB sau sự cố giữa các column family, vì vậy cần khôi phục tính nhất quán theo cách khác. Bài học ở đây là thiết lập độ bền phải phù hợp với khả năng khôi phục dữ liệu. Dữ liệu có thể xây dựng lại từ nguồn thượng nguồn có yêu cầu rất khác so với một hệ thống bản ghi chính thức.
Điều chỉnh bộ lọc bloom theo tỷ lệ hit/miss
Bộ lọc bloom đáng để sử dụng bộ nhớ vì có thể xác định với chi phí thấp rằng một khóa không tồn tại khi tra cứu, nghĩa là chúng chỉ hữu ích khi truy vấn bị miss.
Một lượt tra cứu chữ ký gần như luôn hit vì bên gọi có chữ ký và muốn lấy dữ liệu transaction tương ứng.
Tầng LSM dưới cùng chứa phần lớn áp đảo dữ liệu của chúng tôi. Khi phần lớn dữ liệu nằm ở một tầng duy nhất và khối lượng công việc chủ yếu là hit, các bộ lọc ở tầng đó tiêu thụ nhiều bộ nhớ nhất nhưng làm ít việc nhất. Trong trường hợp đó, nên cân nhắc liệu có thực sự cần chúng ở đó hay không.
Nén dữ liệu có thể nén, không phải dữ liệu được truy cập thường xuyên
Nén là quyết định theo từng chỉ mục. Dữ liệu có entropy cao, như chữ ký 64 byte, không thể nén xuống tỷ lệ dưới 1,0, nghĩa là việc nén chỉ làm tăng chi phí giải nén trên đường nóng của mọi lượt tra cứu. Đây là lý do chúng tôi đặt chế độ nén thành None.
Ngược lại, dữ liệu block nén tốt vì có kích thước lớn hơn và lặp lại nhiều hơn. Vì vậy, chỉ mục slot -> block của chúng tôi sử dụng zstd. Lưu ý rằng cả hai chỉ mục dùng cùng một cơ sở dữ liệu nhưng hưởng lợi từ những lựa chọn khác nhau. Quyết định này hoàn toàn dựa trên việc các byte có thể nén hay không và chỉ mục bị giới hạn bởi độ trễ hay dung lượng lưu trữ.
Việc phân tách theo từng chỉ mục này là một phần quan trọng giúp chúng tôi giảm dung lượng từ ~330TB xuống ~190TB mà không ảnh hưởng đến độ trễ tra cứu điểm.
Cân nhắc I/O trực tiếp
Chúng tôi đọc bằng I/O trực tiếp và cũng sử dụng nó cho thao tác flush và compaction. Ở quy mô này, bộ nhớ đệm trang của hệ điều hành cạnh tranh cùng một lượng RAM với bộ nhớ đệm block của chúng tôi. Với các lượt tra cứu điểm ngẫu nhiên, việc lưu đệm hai lần đó chủ yếu là lãng phí—chúng tôi muốn tự quản lý một bộ nhớ đệm block lớn duy nhất để cung cấp độ trễ có thể dự đoán.
Lưu ý rằng điều này phụ thuộc vào khối lượng công việc:
I/O trực tiếp có thể ảnh hưởng tiêu cực đến các hệ thống quét nhiều hoặc thiếu tài nguyên, vì vậy nên thử nghiệm A/B thay vì áp dụng một cách mù quáng.
Chọn bộ nhớ đệm chịu được tranh chấp
Dưới tải đồng thời lớn trên các khóa nóng, bộ nhớ đệm LRU dùng chung tiêu chuẩn trở thành nút thắt do tranh chấp khóa. Chúng tôi sử dụng HyperClockCache của RocksDB cho các chỉ mục nóng; giải pháp này hoạt động tốt hơn nhiều khi nhiều luồng đồng thời truy cập dồn dập vào cùng các mục phổ biến.
Song song hóa tra cứu nhiều khóa thay vì tuần tự hóa
Thay vì tuần tự hóa N lượt đọc, RocksDB có thể phát nhiều thao tác I/O đồng thời qua io_uring và để chúng hoàn thành song song. Với một phương thức như getTransactionsForAddress, vốn phân nhánh thành nhiều lượt tra cứu điểm bên dưới, đây là điểm khác biệt giữa độ trễ tăng theo số lượng khóa và độ trễ giữ ở mức gần như không đổi.
RocksDB cung cấp mọi tùy chọn nhưng không đưa ra giả định nào về dữ liệu của bạn. Thành quả của chúng tôi đến từ việc hiểu các mẫu truy cập và tinh chỉnh từng chỉ mục theo đặc điểm riêng, thay vì tìm kiếm một cấu hình toàn cục duy nhất có thể làm tốt mọi thứ.
Mục tiêu chúng tôi đang hướng đến
Hiện nay, hệ thống lưu trữ lịch sử hoạt động tại các khu vực lớn hơn của chúng tôi như EWR, FRA và Tokyo. Khi engine lưu trữ đã trả về kết quả tra cứu trong vài micro giây và sử dụng hết công suất card mạng, cơ sở dữ liệu không còn là thứ đánh thức chúng tôi giữa đêm; giải quyết một nút thắt thường sẽ làm lộ ra nút thắt tiếp theo.
Khi một lượt tra cứu gần như không còn chi phí, chi phí chủ đạo chuyển từ phần mềm sang khoảng cách giữa người dùng và máy chủ. Yêu cầu vẫn phải di chuyển từ vị trí của người dùng đến nơi backend của chúng tôi hoạt động rồi quay trở lại. Khi đó, bạn đang đối đầu với các định luật vật lý.
Đó là vấn đề mà Gatekeeper giải quyết.
Gatekeeper là edge gateway nội bộ của chúng tôi, được viết bằng Rust trên Hyper, chấm dứt kết nối ở gần người dùng và định tuyến từng yêu cầu đến backend qua đường ngắn nhất hiện có. Đây là nơi cuộc chiến độ trễ đang diễn ra: gộp kết nối, tinh chỉnh TLS và socket, định tuyến theo khoảng cách và tình trạng hoạt động, cùng triển khai không thời gian ngừng trên toàn bộ hạ tầng toàn cầu—tất cả nhằm cắt giảm vài mili giây trên đường đến những byte mà hệ thống lưu trữ lịch sử vốn đã phục vụ trong vài micro giây.
Làm cho một yêu cầu duy nhất trở nên nhanh là bài toán cơ sở dữ liệu. Làm cho mọi yêu cầu đều nhanh từ bất kỳ đâu trên thế giới, không có khung thời gian bảo trì và không làm rớt kết nối, là một bài toán hoàn toàn khác. Đó cũng là vấn đề tiếp theo mà chúng tôi tập trung giải quyết.
Đây là điều chúng tôi muốn nói khi đề cập đến đổi mới lớp đọc của Solana: hoàn thiện mọi lớp, từ engine lưu trữ trả lời truy vấn đến edge gateway quyết định câu trả lời đó đến tay người dùng cuối nhanh đến mức nào.
Suy cho cùng, lưu trữ lịch sử là các lượt tra cứu điểm bằng khóa ngẫu nhiên. Đây là mẫu truy cập khiến engine dạng cột thất bại vì những lý do mang tính cấu trúc. Vấn đề không nằm ở việc tinh chỉnh, phân mảnh hay cách sắp xếp các view. Những giới hạn chúng tôi gặp phải với ClickHouse là hệ quả vốn có của lựa chọn này, không phải do cấu hình ngẫu nhiên gây ra. Khối lượng công việc lịch sử của Solana được xác định bởi mẫu truy cập khó nhất, không phải mẫu dễ nhất.
Ở quy mô của một mạng lưới đang cố gắng trở thành lớp thanh toán cho tài chính toàn cầu, lớp đọc không thể chỉ là phiên bản được tinh chỉnh tốt hơn của hệ thống mà chúng ta đã vượt quá giới hạn. Các tổ chức và ứng dụng phụ thuộc vào truy vấn lịch sử phong phú cần những phương thức, phạm vi dữ liệu và độ trễ mà ngăn xếp mặc định không thể cung cấp. Lớp đọc của Solana phải được xây dựng trên một nền tảng phù hợp với trường hợp khó ngay từ đầu. Đó là lựa chọn chúng tôi đặt cược với RocksDB và cũng là tiêu chuẩn chúng tôi áp dụng cho mọi thứ khác.
Nếu bạn muốn xây dựng tương lai của ngành tài chính ở quy mô lớn, hãy cùng chúng tôi hiện thực hóa điều đó. Solana đang chạy đua để trở thành lớp thanh toán cho tài chính toàn cầu và lưu trữ lịch sử chỉ là một phần nhỏ của bài toán lớn hơn nhiều. Nếu bạn quan tâm đến compaction LSM và tinh chỉnh theo từng chỉ mục, hoặc muốn giải quyết các bài toán hệ thống khó trên một số phần cứng tốt nhất có thể mua được, bạn sẽ cảm thấy đây là nơi dành cho mình.
Chúng tôi đang tuyển dụng cho nhiều vị trí trong đội ngũ kỹ thuật. Xem tất cả vị trí đang tuyển tại helius.dev/careers.
Bài viết liên quan
Đăng ký nhận tin từ Helius
Luôn cập nhật những thông tin mới nhất về phát triển Solana và nhận thông báo khi chúng tôi đăng bài


