go分片读取大json避免OOM
背景
项目需要用go读取一个超过2GB的json文件,请求偶尔会触发OOM导致容器重启。
希望牺牲一些效率,分片读取减少内存年占用。
方法
直接调用ReadFile会载入整个文件,需要用到Decoder将json转化为stream
json结构
{
"category_0": [{...},{...},{...}],
"category_1": [{...},{...},{...}],
"category_2": [{...},{...},{...}],
}
每次遇到的具体元素会被抽象为token
// A Token holds a value of one of these types:
//
// - [Delim], for the four JSON delimiters [ ] { }
// - bool, for JSON booleans
// - float64, for JSON numbers
// - [Number], for JSON numbers
// - string, for JSON string literals
// - nil, for JSON null
type Token any
通过Token精确控制读取位置,调用Decode完成局部数据的Unmarshal操作
实现
func ReadJson(ctx context.Context, jsonPath string) error {
// ReadFile results OOM
file, err := os.Open(jsonPath)
if err != nil {
return err
}
defer file.Close()
decoder := json.NewDecoder(file)
// Read opening bracket {
if _, err := decoder.Token(); err != nil {
return err
}
// Process batches to save memory
const batchSize = 10000
batch := make([]models.Result, 0, batchSize)
for decoder.More() {
// Read category key
categoryToken, err := decoder.Token()
if err != nil {
return err
}
category := categoryToken.(string)
// Read array opening bracket [
if _, err := decoder.Token(); err != nil {
return err
}
// Process individual elements
for decoder.More() {
var result models.Result
if err := decoder.Decode(&result); err != nil {
return err
}
batch = append(batch, result)
// Insert when batch is full
if len(batch) >= batchSize {
if err := InsertBatch(ctx, &batch, category); err != nil {
return err
}
batch = batch[:0] // clear and reuse memory
}
}
// Insert remaining items
if len(batch) > 0 {
if err := InsertBatch(ctx, &batch, category); err != nil {
return err
}
batch = batch[:0]
}
// Read array closing bracket ]
if _, err := decoder.Token(); err != nil {
return err
}
}
return nil
}