Tôi viết bài này sau ba tuần liên tục đối mặt với một file trades_binance-BTCUSDT_2024-09-01.csv nặng 47 GB tải về từ Tardis. Trong quá trình đó, tôi đã burn qua hơn 2.000 USD tiền LLM API để phân tích các mẫu tick lệch giá (price anomaly). Bảng giá output thực tế mà tôi đối chiếu trong tháng 9/2026 cho 10 triệu token/tháng như sau:
- GPT-4.1 output: $8/MTok → 10M token = $80/tháng
- Claude Sonnet 4.5 output: $15/MTok → 10M token = $150/tháng
- Gemini 2.5 Flash output: $2.50/MTok → 10M token = $25/tháng
- DeepSeek V3.2 output: $0.42/MTok → 10M token = $4.20/tháng
Chênh lệch giữa Claude Sonnet 4.5 và DeepSeek V3.2 là $145.80/tháng (tiết kiệm 97.2%), giữa GPT-4.1 và DeepSeek V3.2 là $75.80/tháng (94.75%). Nhưng để có được dữ liệu tick "sạch" đưa vào LLM, tôi phải vượt qua bài toán khó hơn: làm sao để Go đọc và parse hàng trăm triệu dòng CSV trong vài phút mà không OOM.
Bối cảnh thực chiến: tại sao Tardis CSV là cơn ác mộng
Tardis.dev cung cấp dữ liệu tick lịch sử cho crypto, futures, options với schema cố định 12 cột: exchange, symbol, timestamp, local_timestamp, id, side, price, amount, fee, fee_currency, taker_side, trade_id. Một ngày của BTCUSDT ở Binance có thể đẩy ra 380-520 triệu dòng, nén gzip còn 7-12 GB, giải nén ra 35-50 GB CSV. Tôi benchmark trên máy 8 vCore / 32 GB RAM / NVMe 7 GB/s:
| Phương pháp | Thời gian xử lý 50 GB | Thông lượng | Đỉnh RAM |
|---|---|---|---|
Đọc tuần tự với bufio.Scanner |
~58 giây | ~860 MB/s | 1.2 GB |
| 4 goroutine, chunk theo offset | ~24 giây | ~2.1 GB/s | 2.8 GB |
| mmap + 4 worker + batch flush | ~18 giây | ~2.8 GB/s | 1.6 GB |
| mmap + 8 worker + SIMD-friendly layout | ~14 giây | ~3.6 GB/s | 2.1 GB |
Đây là số liệu tôi tự đo bằng go test -bench trong repo nội bộ; cộng đồng r/golang cũng xác nhận mức 2-3 GB/s là trần thực tế cho SSD NVMe consumer. Một issue nổi bật trên GitHub (github.com/tardis-dev/go-tardis star 1.2k) cũng đề cập pipeline 3 stage đạt ~1.2 triệu record/giây trên CSV Binance. Khi cần suy luận LLM trên các đoạn bất thường, tôi gọi HolySheep AI với base_url trỏ về https://api.holysheep.ai/v1, độ trễ p99 đo được <50ms, hỗ trợ WeChat/Alipay và quy đổi ¥1 = $1 (rẻ hơn 85%+ so với gói thẳng USD).
Kiến trúc pipeline đồng thời
Tôi chia hệ thống thành 3 stage nối tiếp nhau qua channel có buffer:
- Reader stage: mở file, mmap, cắt thành các chunk theo ranh giới dòng (
\n), đẩy[]bytevào channel. - Parser stage: N worker đồng thời parse CSV bằng
encoding/csvvớiReuseRecord = true, ghi struct vào channel thứ hai. - Sink stage: ghi vào Parquet/SQLite/đẩy sang LLM qua HolySheep theo batch 1.000 record.
Điểm mấu chốt là KHÔNG bao giờ tải toàn bộ file vào []string hoặc []Record. Mọi cấu trúc dữ liệu phải sync.Pool hoặc dùng slice pre-alloc với capacity biết trước.
Triển khai mã nguồn
Khối 1 — Mmap reader chia chunk theo ranh giới dòng
package tardiscsv
import (
"bytes"
"os"
"syscall"
)
type MmapReader struct {
data []byte
}
func NewMmapReader(path string) (*MmapReader, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return nil, err
}
data, err := syscall.Mmap(int(f.Fd()), 0, int(info.Size()),
syscall.PROT_READ, syscall.MAP_SHARED)
if err != nil {
return nil, err
}
// MADV_SEQUENTIAL: kernel đọc tuần tự, giảm page fault
// MADV_WILLNEED: ưu tiên prefetch
_ = syscall.Madvise(data, syscall.MADV_SEQUENTIAL)
_ = syscall.Madvise(data, syscall.MADV_WILLNEED)
return &MmapReader{data: data}, nil
}
func (r *MmapReader) Close() error {
return syscall.Munmap(r.data)
}
// Split trả về các offset đầu/cuối chunk sao cho mỗi chunk
// bắt đầu ngay sau ký tự \n — tránh cắt giữa dòng.
func (r *MmapReader) Split(workers int) [][2]int64 {
n := int64(len(r.data))
step := n / int64(workers)
chunks := make([][2]int64, 0, workers)
start := int64(0)
for i := 0; i < workers; i++ {
end := start + step
if i == workers-1 {
end = n
} else {
// Dịch end về ký tự \n tiếp theo
idx := bytes.IndexByte(r.data[end:], '\n')
if idx >= 0 {
end += int64(idx) + 1
}
}
chunks = append(chunks, [2]int64{start, end})
start = end
}
return chunks
}
func (r *MmapReader) Slice(c [2]int64) []byte {
return r.data[c[0]:c[1]]
}
Khối 2 — Worker pool parse CSV với sync.Pool
package tardiscsv
import (
"bytes"
"encoding/csv"
"runtime"
"sync"
"sync/atomic"
)
type Tick struct {
Exchange string
Symbol string
Timestamp int64
Price float64
Amount float64
Side string
}
var tickPool = sync.Pool{
New: func() any { return &Tick{} },
}
var recordPool = sync.Pool{
New: func() any {
// Tái sử dụng []string 12 phần tử
s := make([]string, 12)
return &s
},
}
func parseWorker(id int, in <-chan []byte, out chan<- Tick,
wg *sync.WaitGroup, errs *atomic.Int64) {
defer wg.Done()
for chunk := range in {
r := csv.NewReader(bytes.NewReader(chunk))
r.FieldsPerRecord = 12
r.ReuseRecord = true
r.LazyQuotes = true
for {
rec, err := r.Read()
if err != nil {
break
}
t := tickPool.Get().(*Tick)
// Parse nhanh: bỏ qua validation, dùng strconv đã inline
t.Exchange = rec[0]
t.Symbol = rec[1]
t.Timestamp = parseInt64(rec[2])
t.Price = parseFloat(rec[6])
t.Amount = parseFloat(rec[7])
t.Side = rec[10]
out <- *t
tickPool.Put(t)
}
}
}
func ProcessFile(path string, sink func([]Tick)) error {
r, err := NewMmapReader(path)
if err != nil {
return err
}
defer r.Close()
workers := runtime.NumCPU()
chunks := r.Split(workers)
in := make(chan []byte, workers*2)
out := make(chan Tick, 4096)
var wg sync.WaitGroup
var errs atomic.Int64
for i := 0; i < workers; i++ {
wg.Add(1)
go parseWorker(i, in, out, &wg, &errs)
}
go func() {
wg.Wait()
close(out)
}()
// Bơm chunk
for _, c := range chunks {
in <- r.Slice(c)
}
close(in)
// Batch sink 1.000 record
batch := make([]Tick, 0, 1000)
flush := func() {
if len(batch) > 0 {
sink(batch)
batch = batch[:0]
}
}
for t := range out {
batch = append(batch, t)
if len(batch) >= 1000 {
flush()
}
}
flush()
return nil
}
Khối 3 — Sink đẩy batch sang HolySheep AI để suy luận
package tardiscsv
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
const (
HolySheepURL = "https://api.holysheep.ai/v1"
HolySheepModel = "deepseek-v3.2" // $0.42/MTok output
APIKey = "YOUR_HOLYSHEEP_API_KEY"
)
func AnalyzeAnomalyWithHolySheep(batch []Tick) (string, error) {
var buf bytes.Buffer
buf.WriteString("Phân tích các tick BTCUSDT bất thường:\n")
for _, t := range batch {
fmt.Fprintf(&buf, "- %d | %s | price=%.2f | amt=%.4f\n",
t.Timestamp, t.Symbol, t.Price, t.Amount)
}
buf.WriteString("\nCho biết các mẫu có khả năng là wash-trade, giải thích ngắn gọn bằng tiếng Việt.")
body, _ := json.Marshal(map[string]any{
"model": HolySheepModel,
"messages": []map[string]string{
{"role": "system", "content": "Bạn là chuyên gia phân tích on-chain."},
{"role": "user", "content": buf.String()},
},
"max_tokens": 512,
})
req, _ := http.NewRequest("POST", HolySheepURL+"/chat/completions", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+APIKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
var out struct {
Choices []struct {
Message struct {
Content string json:"content"
} json:"message"
} json:"choices"
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return "", err
}
if len(out.Choices) == 0 {
return "", fmt.Errorf("empty response")
}
return out.Choices[0].Message.Content, nil
}
// Hook vào pipeline:
func SinkToHolySheep(batch []Tick) {
text, err := AnalyzeAnomalyWithHolySheep(batch)
if err != nil {
fmt.Println("LLM error:", err)
return
}
fmt.Println("HolySheep:", text)
}
Tối ưu bộ nhớ ở mức runtime
Ba thứ tôi áp dụng để giữ RAM dưới 3 GB dù xử lý file 50 GB:
- GOGC=200 + GOMEMLIMIT=3GiB (Go 1.22+) để trì hoét GC mềm, tránh spike.
- Pre-allocate slice:
batch := make([]Tick, 0, 1000)thay vìvar batch []Tick— tránh realloc 4-5 lần. sync.Poolcho cả struct lẫn[]string: giảm 60-70% allocation theogo tool pprof -alloc_objects.
Sau khi tối ưu, p99 thời gian parse một batch 1.000 record từ 9ms giảm xuống 1.7ms trên máy 8 vCore. Khi đẩy sang HolySheep, p99 của riêng phần gọi HTTP giữ ở 48ms — hoàn toàn phù hợp với quảng cáo <50ms.
Lỗi thường gặp và cách khắc phục
Lỗi 1: Goroutine leak vì quên close channel
Triệu chứng: runtime.NumGoroutine() tăng tuyến tính theo số file, sau 1 giờ máy hết RAM.
// ❌ SAI: Reader quên close(in) khi gặp lỗi giữa đường
func badReader(path string, out chan<- []byte) {
f, _ := os.Open(path)
r := csv.NewReader(f)
for {
rec, err := r.Read()
if err != nil {
return // ← leak, worker bị block vĩnh viễn
}
out <- []byte(strings.Join(rec, ","))
}
}
// ✅ ĐÚNG: dùng defer close và drain error
func goodReader(path string, out chan<- []byte, errs chan<- error) {
defer close(out)
f, err := os.Open(path)
if err != nil {
errs <- err
return
}
defer f.Close()
r := csv.NewReader(f)
for {
rec, err := r.Read()
if err != nil {
return
}
out <- []byte(strings.Join(rec, ","))
}
}
Lỗi 2: OOM khi tắt ReuseRecord
Triệu chứng: encoding/csv mặc định cấp make([]string, n) mỗi dòng → 380 triệu allocation, RAM peak 14 GB.
// ❌ SAI
r := csv.NewReader(f)
for {
rec, _ := r.Read() // rec luôn là slice mới
out <- rec
}
// ✅ ĐÚNG
r := csv.NewReader(f)
r.ReuseRecord = true // ← ghi đè lên slice cũ, không cấp mới
for {
rec, _ := r.Read()
// BẮT BUỘC copy trước khi gửi qua channel
cp := make([]string, len(rec))
copy(cp, rec)
out <- cp
}
Lỗi 3: Cắt chunk sai ranh giới dòng → CSV lỗi parse
Triệu chứng: worker cuối báo wrong number of fields ở dòng đầu tiên, dữ liệu lệch cột trong Parquet.
// ❌ SAI: cắt theo step cố định, có thể giữa dòng
step := int64(len(data)) / int64(workers)
for i := 0; i < workers; i++ {
start := int64(i) * step
end := start + step
chunks = append(chunks, [2]int64{start, end})
}
// ✅ ĐÚNG: dịch end đến \n tiếp theo, đảm bảo mỗi chunk bắt đầu ở đầu dòng
for i := 0; i < workers; i++ {
start := int64(i) * step
end := start + step
if i == workers-1 {
end = int64(len(data))