yafeiaa Blogs

Golang| golang项目中对json处理的优化

本文主要介绍我在golang项目中遇到的一个json.Marshal导致的内存占用大的问题,以及解决思路和方案。

问题描述

在我的一个golang项目中,其中有一个http服务,它提供的api接口需要返回一些数Mb级别的json数据,在很长的一段时间内都能正常使用,但是随着业务的增长,发现这个服务使用的内存越来越大,从最开始的2gb规模增长到了10gb甚至更高,而且随着时间的推移,内存占用还在不断增长,最终导致服务OOM、进程被kill。

内存使用量

问题分析

为了解决这个问题,我首先通过监控面板对比了该服务器的几个指标:

  • CPU使用量
  • 内存使用量
  • goroutines
  • 向操作系统请求的内存量

在某个内存增长的点,这些指标的值变化如下图:

内存使用量

可以在图上看到,内存使用量增长的时候,cpu使用量在增长、向操作系统请求的内存量也在增长。但是goroutines没有明显变化。

这里可以排除是goroutines泄漏导致的问题,因为goroutines数量一直都比较平稳。

排除了goroutines泄漏的问题之后,这里我们猜测是正常的服务器行为导致的内存使用量增多。

然后,我通过golang的pprof endpoint中暴露出的数据,对内存使用量增长的原因进行了分析。在golang的pprof中暴露了几个关于内存的数据:

  • inuse_space — 已分配但尚未释放的内存空间
  • inuse_objects——已分配但尚未释放的对象数量
  • alloc_space — 分配的内存总量(已释放的也会统计)
  • alloc_objects — 分配的对象总数(无论是否释放)

这里我们重点对inuse_space进行分析:

Inuse_space

inuse_space

在图中,可以明显看到,约89%的内存都来自于json.Marshal函数,json.Marshal在这个服务中充当的作用就是对一些大对象进行序列化,返回给客户端。

所以,这里可以确认,内存使用量增长的原因就是json.Marshal函数导致的。

解决方案

既然问题已经确认,那么接下来就是想办法解决这个问题。

通过对json.Marshal函数的源码进行分析,发现它使用的是反射机制,对结构体进行遍历,然后对每个字段进行序列化,最终将序列化后的结果拼接成一个字符串返回。

众所周知,golang是静态编译型语言,为了在运行时提高灵活性,我们有些时候不得不使用反射机制,但是golang的反射机制性能较差(具体见参考资料)。所以会导致json.Marshal函数在序列化大对象的时候,性能较差。

在社区中,有很多对标准库encoding/json的优化方案,比如:

  • 字节跳动开源的sonic:https://github.com/bytedance/sonic
  • 滴滴开源的json-iterator:https://github.com/json-iterator/go
  • jsonparser:https://github.com/buger/jsonparser

其中,json-interator完全兼容encoding/json接口,便于直接替换标准库,而sonic和jsonparser则没有兼容encoding/json,需要修改代码。

所以,基于兼容性考虑,我选择了json-iterator。

1、安装json-iterator

go get github.com/json-iterator/go

2、导入json-iterator并替换encoding/json

import (
    // "encoding/json"
    "github.com/json-iterator/go"
)
// 替换encoding/json
var json = jsoniter.ConfigCompatibleWithStandardLibrary

优化效果

在经过调整之后,再次观察在相同qps下,inuse_space的分布:

inuse_space

从图上可以看到,json.Marshal函数造成的大量占用已经消失。

从grafana的内存使用量上也可以看到内存使用量也降到了原来的1/3左右。

参考资料