当前位置: 首页 > 科技观察

自古以来,JSON序列化都是兵家必争之地

时间:2023-03-21 10:31:13 科技观察

上面说到,使用ioutil.ReadAll读取一个大的ResponseBody,存在读取Body超时的问题。01Stackoverflow[1]的领军人物morganbaz认为Go语言中使用iotil.ReadAll来读取一个大的ResponseBody是非常低效的;另外,如果ResponseBody足够大,还有内存泄漏的风险。data,err:=iotil.ReadAll(r)iferr!=nil{returnerr}json.Unmarshal(data,&v)有更高效的解析json数据的方式,会使用Decodertypeerr:=json.NewDecoder(r).Decode(&v)iferr!=nil{returnerr}这个方法不仅更加简洁,而且从内存和时间的角度来看也更加高效。解码器不需要分配一个巨大的内存字节来容纳数据读取——它可以简单地重用一个小缓冲区来获取所有数据并逐步解析它。这在内存分配上节省了大量时间,并消除了GC的压力JSON解码器可以在第一块数据进入后立即开始解析数据——它不需要等待所有内容都完成下载。02子孙后代乘凉。对于前人的思考,我想补充两点。①.官方的ioutil.ReadAll通过初始大小为512字节的切片读取读取器。我们的响应体大约是50M。显然,它会频繁触发分片扩容,造成不必要的内存分配和gc压力。goslice扩容时机:如果需求小于256字节,扩容2倍;如果超过256字节,则扩容1.25倍。②你怎么理解morganbaz提到的内存泄露的风险?内存泄漏是指程序动态分配的堆内存由于某种原因没有被释放,造成系统内存浪费,减慢程序运行速度,促使系统崩溃等严重后果。通过ioutil.ReadAll读取大体将触发切片扩展。按理说,这种做法只会导致内存浪费,最终会被gc释放掉。为什么原作者要强调内存泄漏的风险呢?我咨询了一些童鞋,对于需要长时间运行的高并发服务器程序,如果内存没有及时释放,最终可能会耗尽系统所有的内存,这就是隐式内存泄漏。03JSON序列化是战场。morganbaz先生提出使用标准库encoding/json边读边反序列化,减少内存分配,加快反序列化速度。JSON序列化自古就是兵家必争之地[2],各大语言对序列化的实现思路不同,性能差异很大。下面使用高性能的json序列化库json-iterator与原生的ioutil.ReadAll+json.Unmarshal方法进行对比。顺便也检验一下我最近实践pprof[3]的结果#goget"github.com/json-iterator/go"packagemainimport("bytes""flag""log""net/http""os""runtime/pprof""time"jsoniter"github.com/json-iterator/go")varcpuprofile=flag.String("cpuprofile","","writecpuprofiletofile.")varmemprofile=flag.String("memprofile","","writememprofiletofile")funcmain(){flag.Parse()if*cpuprofile!=""{f,err:=os.Create(*cpuprofile)iferr!=nil{log.Fatal(err)}pprof.StartCPUProfile(f)deferpprof.StopCPUProfile()}c:=&http.Client{超时:60*time.Second,//传输:tr,}body:=sendRequest(c,http.MethodPost)log.Println("responsebodylength:",body)if*memprofile!=""{f,err:=os.Create(*memprofile)iferr!=nil{log.Fatal("无法创建内存配置文件:",err)}deferf.Close()//省略了错误处理,例如iferr:=pprof.WriteHeapProfile(f);err!=nil{log.Fatal("无法写入内存配置文件:",err)}}}funcsendRequest(client*http.Client,methodstring)int{endpoint:="http://xxxxx.com/table/instance?method=batch_query"expr:="idcin(logicidc_hd1,logicidc_hd2,officeidc_hd1)"varjson=jsoniter.ConfigCompatibleWithStandardLibraryjsonData,err:=json.Marshal([]string{expr})log.Println("开始请求:"+time.Now().Format("2006-01-0215:04:05.010"))response,err:=client.Post(endpoint,"application/json",bytes.NewBuffer(jsonData))iferr!=nil{log.Fatalf("向api端点发送请求时出错,%+v",err)}log.Println("服务器处理完成,准备接收响应:"+time.Now().Format("2006-01-0215:04:05.010"))deferresponse.Body.Close()varrespResponsevarrecords=make(map[string][]Record)resp.Data=&recordserr=json.NewDecoder(response.Body).Decode(&resp)iferr!=nil{log.Fatalf("不能parseresponsebody,%+v",err)}log.Println("Clientread+parseend:"+time.Now().Format("2006-01-0215:04:05.010"))var结果=make(map[string]*Data,len(records))for_,r:=rangerecords[expr]{result[r.Ins.Id]=&Data{Active:"0",IsProduct:true}}returnlen(result)}#省略反序列化对象类型内存比较不是简单的序列化比较,前者反馈后者的优化效果---json-iterator边读边反序列化------io.ReadAll+json.Unmarshal反序列化---我们可以点击查看io.ReadAll+json.Unmarshal的内存消耗在哪里?总计:59.59MB59.59MB(平,加)100%626..funcReadAll(rReader)([]byte,error){627..b:=make([]byte,0,512)628..对于{629。.如果len(b)==cap(b){630..//添加更多容量(让append选择多少)。63159.59MB59.59MBb=append(b,0)[:len(b)]632。.}633。.n,err:=r.Read(b[len(b):cap(b)])634。.b=b[:len(b)+n]635。.如果错了!=零{636..如果错误==EOF{从上图也可以确认,io.ReadAll不断扩充初始的512字节的slice来存放整个Response.Body,导致常驻内存为59M。也可以比较alloc_space来分配内存,(alloc_space和inuse_space的区别可以大致理解为gc释放的部分)。从结果来看,与io.ReadAll+json.Unmarshal相比,json-iterator动态分配的内存比较少。ref:排查go开发的HttpClient读取Body超时04我的结果ioutil.ReadAll读取大response.body的风险:性能差和内存泄漏的风险。隐式内存泄漏:对于高并发、长时间运行的web程序,未能及时释放内存最终会导致内存耗尽。JSON序列化是一个战场,而json-iterator是一个与标准encode/jsonapi用法兼容的高性能序列化器。pprof内存诊断的构成和调试指标的含义。参考链接[1]Stackoverflow:https://stackoverflow.com/questions/52539695/alternative-to-ioutil-readall-in-go[2]自古以来,JSON序列化就是兵家必争之地:https://yalantis。com/blog/speed-up-json-encoding-decoding/[3]pprof实践:https://segmentfault.com/a/1190000016412013