แนะนำ Pulsora: ฐานข้อมูลไทม์ซีรีส์ที่เขียนด้วย Rust สำหรับข้อมูลเทรด
เรามีกระแสข้อมูลพุ่งเข้ามาแต่ไม่มีที่ดี ๆ ให้เก็บมัน
ปัญหาภายในนั้นเฉพาะเจาะจง: จัดเก็บและคิวรีกระแสข้อมูลตลาดที่ใหญ่และต่อเนื่อง — tick, quote, bar ของสัญลักษณ์จำนวนมาก — ให้เร็วพอที่การนำเข้าจะไม่กลายเป็นคอขวด และคิวรีตามช่วงเวลาคืนค่ากลับมาโดยที่เราไม่ต้องกลั้นหายใจ ข้อมูลตลาดคือเวอร์ชันที่สะอาดและโหดที่สุดของภาระงานไทม์ซีรีส์ มันมาตามลำดับ มันมาตลอดเวลา และมันไม่เคยหยุด ปริมาณไม่ได้น่าสนใจเพราะมันใหญ่ครั้งเดียว แต่มันน่าสนใจเพราะมันใหญ่ทุก ๆ วินาที ตลอดไป
ตอนแรกเราลองสิ่งที่ชัดเจนก่อน: หยิบฐานข้อมูลไทม์ซีรีส์ที่มีอยู่แล้วมาใช้ ปกติแล้วนั่นคือการตัดสินใจที่ถูกต้อง และช่วงหนึ่งมันก็ใช้ได้ดี จากนั้นความต้องการด้านอัตราการเขียนก็ค่อย ๆ สูงขึ้น พื้นที่บนดิสก์โตเร็วยิ่งกว่า และคิวรีตามช่วงเวลาที่เราใช้งานจริงเริ่มรู้สึกถึงน้ำหนักของดัชนี ถึงจุดหนึ่ง ชั้นสตอเรจทำงานบัญชีต่อแถวมากกว่างานจริงบนแถวเสียอีก
เราจึงสร้าง Pulsora — ฐานข้อมูลไทม์ซีรีส์ที่เขียนด้วย Rust ปรับให้เหมาะกับข้อมูลตลาดและชุดข้อมูลที่เรียงตามเวลา มันเป็นโอเพนซอร์สบน GitHub ภายใต้สัญญาอนุญาต Apache-2.0 โพสต์นี้คือการแนะนำว่ามันคืออะไร ทำไมมันถึงมีอยู่ และจริง ๆ แล้วมันจัดเก็บและคิวรีข้อมูลอย่างไร — อ้างอิงจากโค้ด ไม่ใช่จากโบรชัวร์
ทำไมต้องสร้างเองแทนที่จะรับมาใช้
การสร้างฐานข้อมูลไม่ใช่สิ่งที่ทำกันเล่น ๆ เกณฑ์ที่ต้องผ่านให้ได้คือ: เราต้องการอะไรเป็นพิเศษที่เราไม่ได้รับมาในราคาที่ถูกพอ?
มีสามสิ่งที่ผุดขึ้นมาซ้ำ ๆ
อัตราการเขียนที่ขยายตามบล็อก ไม่ใช่ตามแถว กระแส tick คือแถวเล็ก ๆ ที่แทบเหมือนกันนับล้านแถว สตอเรจทั่วไปส่วนใหญ่อยากทำดัชนีให้ทุกแถว นั่นคือรายการดัชนีหนึ่งรายการต่อหนึ่งแถว และที่จำนวนแถวของเรา ดัชนีกลายเป็นต้นทุนหลัก — ทั้งในการขยายการเขียน บนดิสก์ และในการ compaction เราต้องการให้ต้นทุนสตอเรจต่อแถวเข้าใกล้ศูนย์ และให้ดัชนีขยายตามจำนวน บล็อก ที่เราเขียน ไม่ใช่ตามจำนวนแถว
การบีบอัดที่เข้าใจข้อมูล ข้อมูลตลาดบีบอัดได้สวยงามหากคุณมองมันเป็นคอลัมน์ของชนิดข้อมูลที่รู้แน่ชัด timestamp มาที่ช่วงเวลาเกือบสม่ำเสมอ ราคาเปลี่ยนช้า ๆ ปริมาณเป็นจำนวนเต็มเล็ก ๆ การบีบอัดแบบเรียงตามแถวทั่วไปทิ้งสิ่งเหล่านั้นไปเกือบหมด เราต้องการการบีบอัดแบบคอลัมน์ที่รู้ชนิดข้อมูล — แบบที่เปเปอร์ Gorilla ของ Facebook วางไว้ — สร้างมาในตัว ไม่ใช่แปะเพิ่มทีหลัง
อินเทอร์เฟซที่น่าเบื่อและยืดหยุ่นเรื่องฟอร์แมต เรานำเข้าจากผู้ผลิตข้อมูลหลายแหล่งและบริโภคเข้าสู่เครื่องมือหลายตัว เราไม่ต้องการโปรโตคอลสายเฉพาะตัว เราต้องการ POST CSV จากสคริปต์ สตรีม Apache Arrow จากไปป์ไลน์ และอ่านกลับเป็น JSON, Arrow หรือ CSV ขึ้นอยู่กับว่าใครเป็นคนถาม HTTP และฟอร์แมตคอลัมน์ที่รู้จักกันดี ไม่มีอะไรแปลกประหลาด
ไม่มีข้อใดที่แปลกใหม่ในตัวมันเอง แต่การผสมผสาน — สำหรับภาระงาน ของเรา ด้วยข้อจำกัดด้านพื้นที่ ของเรา — เพียงพอที่จะให้เหตุผลในการเขียนมันขึ้นมา และเมื่อคุณกำลังสร้างมันอยู่แล้ว การเขียนด้วย Rust เพื่อให้ได้ความถูกต้องตอน compile บนเส้นทางร้อนคือส่วนที่ง่าย เราเคยเขียนไว้ก่อนหน้านี้ว่าทำไมเราถึงสร้างเครื่องมือของเราเอง Pulsora อยู่ในสายเลือดเดียวกันนั้นพอดี
วันนี้ Pulsora คืออะไร
ขอพูดตรง ๆ เรื่องขอบเขต: Pulsora คือ 0.1.0 มันคือเอนจินที่ทำงานของมันได้ดี ไม่ใช่แพลตฟอร์มแบบคลัสเตอร์ มี replication รองรับหลายผู้เช่าพร้อม query planner มันคือสตอเรจไทม์ซีรีส์แบบคอลัมน์โหนดเดียว ฝัง RocksDB พร้อม REST API เท่านั้นเอง และเท่านั้นเองคือประเด็น
นี่คือรูปร่างของมัน:
- แบ็กเอนด์สตอเรจ: RocksDB พร้อมชั้นคอลัมน์ที่เขียนเองทับไว้ด้านบน
- ฟอร์แมตการนำเข้า: CSV, Apache Arrow IPC stream และ Protocol Buffers
- ฟอร์แมตผลลัพธ์ของคิวรี: JSON (ค่าเริ่มต้น), Arrow IPC stream, Protobuf และ CSV — เจรจาผ่านเฮดเดอร์
Accept - สคีมา: อนุมานอัตโนมัติตอนนำเข้าครั้งแรก คุณไม่ต้องประกาศตาราง คุณ POST ข้อมูลแล้ว Pulsora หาคอลัมน์และชนิดข้อมูลเอง
- อินเทอร์เฟซ: HTTP/REST API ที่ให้บริการโดย Axum บน Tokio
- ความทนทาน: write-ahead log แบบเลือกเปิดได้ เพื่อให้แถวที่อยู่ในบัฟเฟอร์รอดจากการแครช
ทั้งหมดคือไบนารีเดียวชื่อ pulsora มีแฟล็กบรรทัดคำสั่งเดียว
# รันด้วยค่าเริ่มต้น
cargo run
# หรือชี้ไปยังไฟล์คอนฟิก
cargo run -- -c pulsora.toml
แฟล็ก -c/--config นั้นคือพื้นผิวบรรทัดคำสั่งทั้งหมด ที่เหลือทั้งหมดอยู่ใน TOML นี่เป็นความตั้งใจ — คอนฟิกคือ API สำหรับการดำเนินงาน ส่วน API ตอนรันไทม์คือ HTTP
มันจัดเก็บข้อมูลบนดิสก์อย่างไร
นี่คือส่วนที่เราใส่ใจมากที่สุด จึงเป็นส่วนที่ควรค่าแก่การอธิบาย
บล็อกแบบคอลัมน์ ไม่ใช่แถว
แถวเข้ามา แต่มันไม่ได้ถูกจัดเก็บเป็นแถว Pulsora สะสมแถวที่เข้ามาในบัฟเฟอร์ในหน่วยความจำ และเมื่อบัฟเฟอร์ถึงเกณฑ์ขนาด (buffer_size ค่าเริ่มต้น 1000) หรือเกณฑ์เวลา (flush_interval_ms) มันจะจัดกลุ่มแถวเหล่านั้นเป็น ColumnBlock — ก้อนของแถวที่จัดเก็บแบบคอลัมน์ต่อคอลัมน์ แต่ละคอลัมน์ถูกบีบอัดอย่างอิสระด้วยอัลกอริทึมที่เลือกตามชนิดข้อมูลของมัน
บล็อกคือหน่วยของการจัดเก็บ ColumnBlock มีหน้าตาคร่าว ๆ ดังนี้:
pub struct ColumnBlock {
pub row_count: usize, // จำนวนแถวในบล็อกนี้
pub columns: HashMap<String, Vec<u8>>, // ข้อมูลคอลัมน์ที่บีบอัดแล้ว
pub null_bitmaps: HashMap<String, Vec<u8>>, // การติดตามค่า null ต่อคอลัมน์
}
การจัดเก็บแบบคอลัมน์แทนที่จะเป็นแถวคือสิ่งที่ทำให้การบีบอัดได้ผล เพราะทุกค่าในคอลัมน์มีชนิดข้อมูลเดียวกันและขนาดใกล้เคียงกัน นั่นคือเหตุผลทั้งหมดที่สตอเรจแบบคอลัมน์มีอยู่ และข้อมูลไทม์ซีรีส์คือภาระงานที่มันถูกคิดค้นขึ้นมาเพื่อรองรับ
การบีบอัดเฉพาะตามชนิดข้อมูล
คอลัมน์แต่ละชนิดได้รับกลยุทธ์การบีบอัดที่สร้างมาเพื่อรูปร่างของมัน ตามเอกสารของ repository:
| ชนิดข้อมูล | การบีบอัด | อัตราส่วนทั่วไป (ตัวเลขของ repo) | ทำไมจึงได้ผล |
|---|---|---|---|
| Timestamp | Delta-of-delta + varint | 5–10x | ช่วงเวลาเกือบสม่ำเสมอ |
| Float | XOR (Gorilla) + varfloat | 2–5x | ค่าที่เปลี่ยนแปลงช้า |
| Integer | Delta + varint | 3–8x | ข้อมูลเรียงลำดับ / ตัวนับ |
| String | การเข้ารหัสแบบพจนานุกรม | 2–4x | ค่าที่ซ้ำ ๆ (สัญลักษณ์) |
| Boolean | การเข้ารหัสแบบ run-length (RLE) | 10–50x | ข้อมูลกระจัดกระจาย |
เส้นทางของ timestamp คือกรณีตำรา tick มาที่ช่วงเวลาที่ เกือบ คงที่ ดังนั้นผลต่างอันดับสอง (delta-of-delta) มักเป็นศูนย์หรือเล็กจิ๋ว และ varint เข้ารหัสตัวเลขจิ๋ว ๆ ในหนึ่งไบต์ Float ใช้การแพ็กบิตแบบ XOR-กับค่าก่อนหน้า จากอัลกอริทึม Gorilla — เมื่อราคาแทบไม่ขยับ บิตที่ผ่าน XOR ส่วนใหญ่เป็นศูนย์และถูกแพ็กจนแทบหายไป สตริงอย่างสัญลักษณ์ ticker ซ้ำไม่รู้จบ ดังนั้นพจนานุกรมจึงแมปแต่ละสัญลักษณ์ที่ต่างกันไปยังจำนวนเต็มเล็ก ๆ เพียงครั้งเดียว
ใต้ทั้งหมดนั้น RocksDB ใส่การบีบอัดบล็อกทั่วไปอีกชั้นหนึ่ง — lz4 เป็นค่าเริ่มต้น โดยมี none, snappy และ zstd ให้เลือก ดังนั้นคุณจึงได้การเข้ารหัสที่รู้ชนิดข้อมูลก่อน แล้วตามด้วยการบีบอัดทั่วไปทับด้านบน
อัตราส่วนเหล่านี้คือคำกล่าวอ้างเรื่องเบนช์มาร์กของ repository เอง วัดด้วยชุด Criterion ภายใต้ benches/ ผมรายงานมันในฐานะตัวเลขของโปรเจกต์ ไม่ใช่คำตัดสินอิสระ — ลองรัน cargo bench กับข้อมูลของคุณเองและเชื่อตัวเลขนั้นแทน
ดัชนีที่ไม่โตตามจำนวนแถว
นี่คือการตัดสินใจด้านดีไซน์ที่ทุกอย่างหมุนรอบมัน
สตอเรจไทม์ซีรีส์ส่วนใหญ่เก็บดัชนีต่อแถวเพื่อให้หาแถวเดี่ยวใด ๆ ตามเวลาได้ Pulsora ไม่ทำ มันเก็บ ดัชนีระดับบล็อกเท่านั้น — เมตาดาตาเกี่ยวกับแต่ละบล็อก ไม่เคยเกี่ยวกับแถวรายตัว เลย์เอาต์คีย์บนดิสก์ ตามเอกสารสถาปัตยกรรม มีหน้าตาดังนี้:
Block index: [table_hash:u32]['B'][min_ts:i64][block_id:u64]
value: [block_id][min_ts][max_ts][rows][min_id][max_id] — หนึ่งรายการต่อบล็อก
Block data: [table_hash:u32]['D'][block_id:u64] — บล็อกแบบคอลัมน์ที่บีบอัดแล้ว
Overrides: [table_hash:u32]['O'][block_id:u64] — ตำแหน่งของแถวที่ตายแล้ว
table_hash คือแฮช FNV-1a ขนาด 4 ไบต์ของชื่อตาราง ใช้เป็นคีย์พรีฟิกซ์เพื่อให้ตารางถูกแยกกันโดยไม่ต้องลากชื่อสตริงไปกับทุกคีย์ หลังพรีฟิกซ์ ไบต์เดียว ('B', 'D', 'O') แยกดัชนี ข้อมูล และชุด override ออกจากกัน
ผลที่ตามมา: ต้นทุนสตอเรจขยายตามจำนวนบล็อก ไม่ใช่จำนวนแถว เขียนหนึ่งพันล้านแถวแล้วคุณไม่ได้เพิ่มรายการดัชนีใด ๆ นอกเหนือจากบล็อกที่ใส่มันไว้ นั่นคือข้อกำหนดที่เราซื้อในราคาถูกไม่ได้ และคือแก่นของเหตุผลที่ Pulsora มีอยู่
การอัปเดตแถวจัดการได้โดยไม่ต้องเขียนบล็อกใหม่ REPLACE จะทำเครื่องหมายตำแหน่งของสำเนาเดิมที่ถูกแทนที่ใน ชุด override ของบล็อกเก่า — ชุดของตำแหน่งแถวที่ตายแล้วต่อบล็อก ถูกผสานผ่านตัวดำเนินการ merge ของ RocksDB (set union) ใน write batch เดียวกันกับข้อมูลใหม่ การตรวจว่าแถวยังมีชีวิตคือการอ่านแบบจุดหนึ่งครั้งต่อบล็อก ไม่ใช่การ lookup ต่อแถว
เส้นทางการเขียน
การนำเข้าคือ POST สคีมาถูกอนุมานในครั้งแรก
curl -X POST http://localhost:8080/tables/stocks/ingest \
-H "Content-Type: text/csv" \
--data-binary @ticks.csv
แถวเฮดเดอร์ของ CSV นั้นกลายเป็นสคีมา Pulsora สุ่มตัวอย่างค่า ตรวจหาชนิดของแต่ละคอลัมน์ (Integer, Float, String, Boolean, Timestamp) และระบุคอลัมน์ timestamp อัตโนมัติ — มันเข้าใจ RFC 3339, YYYY-MM-DD HH:MM:SS, แบบวันที่อย่างเดียว และ Unix timestamp ทั้งแบบวินาทีและมิลลิวินาที จากนั้นเป็นต้นมา สคีมาของตารางนั้นถูกตรึงไว้และข้อมูลที่เข้ามาจะถูกตรวจสอบกับมัน
สำหรับไปป์ไลน์ที่ต้องการอัตราสูงกว่า ข้ามการแพร์ส CSV ไปเลยแล้วสตรีม Apache Arrow:
curl -X POST http://localhost:8080/tables/stocks/ingest \
-H "Content-Type: application/vnd.apache.arrow.stream" \
--data-binary @ticks.arrow
Arrow มาพร้อมสคีมาที่แนบมาแล้ว จึงไม่มีขั้นตอนอนุมานและไม่มีการแพร์สข้อความ — คอลัมน์เป็นคอลัมน์อยู่แล้ว การนำเข้า Protobuf ทำงานแบบเดียวกันสำหรับผู้ผลิตข้อมูลที่พูดมันได้อยู่แล้ว
ภายในนั้น ขั้นตอนคือ: แพร์สอินพุต ตรวจสอบหรืออนุมานสคีมา บัฟเฟอร์แถวในหน่วยความจำ (ขจัดข้อมูลซ้ำตอนเข้ามา การเขียนล่าสุดชนะ) และ — หากเปิด WAL — ต่อท้ายแต่ละแถวลงในไฟล์ .wal ก่อนที่มันจะแตะหน่วยความจำ เพื่อให้การแครชไม่ทำให้ข้อมูลในบัฟเฟอร์หาย เมื่อบัฟเฟอร์ flush แถวจะกลายเป็น ColumnBlock ที่บีบอัด บล็อกและรายการดัชนีของมันถูกเขียนลง RocksDB ใน batch เดียว และ WAL ถูกตัดทิ้ง (truncate)
เส้นทางการอ่าน
คิวรีคือ GET พร้อมช่วงเวลาและการแบ่งหน้า
# ทั้งหมด (ถูกจำกัดด้วย limit ค่าเริ่มต้น)
curl "http://localhost:8080/tables/stocks/query"
# ช่วงเวลาหนึ่ง
curl "http://localhost:8080/tables/stocks/query?start=2024-01-01T09:30:00&end=2024-01-01T16:00:00"
# แบ่งหน้า
curl "http://localhost:8080/tables/stocks/query?limit=1000&offset=0"
start และ end รับฟอร์แมต timestamp ชุดที่ยืดหยุ่นชุดเดียวกับการนำเข้า limit ค่าเริ่มต้นคือ 1000 โดยไม่มีการบังคับค่าสูงสุด ส่วน offset ข้ามแถวเพื่อการแบ่งหน้า
เอนจินคิวรีแก้คำขอด้วยดัชนีบล็อกก่อน: มันสร้างช่วงคีย์ไบนารีจากขอบเขตเวลาและวนเฉพาะบล็อกที่ [min_ts, max_ts] ทับซ้อนกับช่วงนั้น มันรวบรวมบล็อกผู้สมัคร ดึงแต่ละบล็อกที่ไม่ซ้ำมาครั้งเดียว คลายการบีบอัด แคชผลลัพธ์ที่คลายแล้วเพื่อให้การเข้าถึงซ้ำภายในคิวรีไม่ต้องคลายใหม่ ใช้ชุด override เพื่อข้ามแถวที่ตายแล้ว ดึงแถวที่ต้องการ และซีเรียลไลซ์มันในฟอร์แมตที่เฮดเดอร์ Accept ขอ
อยากได้ผลลัพธ์กลับมาเป็น Arrow สำหรับไปป์ไลน์ปลายทางแทน JSON? เปลี่ยนเฮดเดอร์เดียว:
curl "http://localhost:8080/tables/stocks/query?limit=10000" \
-H "Accept: application/vnd.apache.arrow.stream" > out.arrow
curl "http://localhost:8080/tables/stocks/query?limit=10000" \
-H "Accept: text/csv" > out.csv
คิวรีเดียวกัน สี่ฟอร์แมตผลลัพธ์ที่เป็นไปได้ ไม่มีขั้นตอนแปลงฝั่งคุณ ยังมีเอนด์พอยต์อ่านอีกหยิบมือหนึ่ง — GET /tables เพื่อแสดงรายการตาราง, GET /tables/{name}/schema สำหรับสคีมาที่อนุมาน, GET /tables/{name}/count สำหรับนับจำนวนแถว, GET /tables/{name}/row/{id} เพื่อดึงแถวเดียวด้วย id และ GET /health
ลองใช้
Pulsora build ได้ด้วย Rust toolchain ปัจจุบันและรันเป็นไบนารีเดียว
# โคลนและรันจากซอร์ส
git clone https://github.com/muvon/pulsora.git
cd pulsora
cargo run
# หรือติดตั้งไบนารีโดยตรง
cargo install --git https://github.com/muvon/pulsora.git
# นำเข้า CSV
curl -X POST http://localhost:8080/tables/stocks/ingest \
-H "Content-Type: text/csv" \
-d "timestamp,symbol,price,volume
2024-01-01 09:30:00,AAPL,150.00,1000
2024-01-01 09:31:00,AAPL,150.25,1500
2024-01-01 09:32:00,AAPL,149.75,2000"
# อ่านกลับ
curl "http://localhost:8080/tables/stocks/query?limit=10"
# ดูสคีมาที่อนุมาน
curl "http://localhost:8080/tables/stocks/schema"
การปรับจูนอยู่ใน pulsora.toml การตั้งค่าที่ควรรู้ก่อน:
[storage]
data_dir = "./data"
buffer_size = 1000 # แถวที่บัฟเฟอร์ก่อนเขียนบล็อก
flush_interval_ms = 1000 # เวลาสูงสุดที่แถวรอในบัฟเฟอร์
wal_enabled = true # แถวในบัฟเฟอร์รอดจากการแครช
[ingestion]
batch_size = 10000 # แถวต่อ batch การเขียนลงฐานข้อมูล
[performance]
compression = "lz4" # none | snappy | lz4 | zstd
cache_size_mb = 256 # แคชบล็อกสำหรับการอ่าน
หากคุณให้ความสำคัญกับการบีบอัดสูงสุดมากกว่าความหน่วง ตั้ง flush_interval_ms = 0 เพื่อโหมด batch-only ที่กั้นแถวไว้จนกว่าบัฟเฟอร์จะเต็ม ทำให้ได้บล็อกที่ใหญ่กว่าและบีบอัดดีกว่า หากคุณให้ความสำคัญกับความหน่วงในการอ่านข้อมูลร้อน เพิ่ม cache_size_mb ชุดปุ่มปรับทั้งหมดมีบันทึกไว้ใน doc/CONFIGURATION.md
มันไม่ใช่อะไร และต่อไปคืออะไร
ผมจะช่วยคุณให้ไม่ต้องผิดหวังจากการค้นพบสิ่งเหล่านี้ด้วยวิธีที่ยากลำบาก วันนี้ Pulsora เป็นโหนดเดียว ไม่มีคลัสเตอร์ ไม่มี replication ไม่มี SQL ไม่มี query planner ทั่วไป ไม่มี predicate pushdown นอกเหนือจากช่วงเวลา ไม่มีการจำกัดอัตรา คิวรีกรองตามเวลาและแบ่งหน้า มันไม่ทำ GROUP BY ไฟล์ doc/ARCHITECTURE.md ระบุทิศทางที่เรากำลังมองอยู่ — column pruning และ predicate pushdown, แคชบล็อกที่หนุนหลังด้วยดิสก์, สตอเรจแบบแบ่งชั้นร้อน/อุ่น/เย็น และการทำคลัสเตอร์ — แต่นั่นคือโรดแมป ไม่ใช่ความจริง ผมอยากให้คุณรู้ขอบเขตมากกว่าให้คุณไปชนมัน
สิ่งที่มัน เป็น ในวันนี้คือเอนจินที่โฟกัส ซึ่งนำเข้ากระแสข้อมูลที่เรียงตามเวลา บีบอัดมันอย่างหนักด้วยอัลกอริทึมที่เข้าใจมัน และให้บริการคิวรีตามช่วงเวลาโดยไม่มีดัชนีที่พองตามจำนวนแถว นั่นคือปัญหาที่เรามี และเป็นปัญหาที่ Pulsora แก้
โอเพนซอร์ส
Pulsora อยู่บน GitHub ภายใต้ Apache-2.0 มันคือ Rust — RocksDB สำหรับสตอเรจ, Axum และ Tokio สำหรับ API, Arrow และ Prost สำหรับฟอร์แมตคอลัมน์และ protobuf — และ build ได้ด้วย cargo build ธรรมดา ชุดเบนช์มาร์กอยู่ใน benches/ หากคุณอยากวัดประสิทธิภาพการนำเข้าและคิวรีกับข้อมูลของคุณเองแทนที่จะเชื่อของเรา
หากคุณกำลังจมอยู่ในแถวที่เรียงตามเวลา — tick ตลาด ค่าจากเซ็นเซอร์ อะไรก็ตามที่มาตามลำดับและไม่เคยหยุด — โคลนมัน ชี้สตรีมไปที่มัน แล้วบอกเราว่ามันพังตรงไหน นั่นคือทางที่เร็วที่สุดที่มันจะดีขึ้น
— Don
Pulsora เป็นโอเพนซอร์สบน GitHub ที่ github.com/muvon/pulsora ภายใต้สัญญาอนุญาต Apache-2.0 เจอบั๊กหรืออยากได้ฟีเจอร์? เปิด issue



