当前位置: 首页 > news >正文

Gin获取Response Body引发的OOM

有轮子尽量用轮子 😭 😭 😭 😭 😭 😭

我们在开发中基于Gin开发了一个Api网关,但上线后发现内存会在短时间内暴涨,然后被OOM kill掉。具体内存走势如下图:

在这里插入图片描述

放大其中一次

在这里插入图片描述

在图二中可以看到内存的增长是很快的,在一分半的时间内,内存增长了近2G。

对于这种内存短时间暴涨的问题,pprof不好管用,除非写个脚本定时去pprof

经过再次review代码,找到了原因了

package serverimport ("bytes""fmt""github.com/gin-gonic/gin"jsoniter "github.com/json-iterator/go"
)var json = jsoniter.ConfigCompatibleWithStandardLibrarytype BodyDumpResponseWriter struct {gin.ResponseWriterbody *bytes.Buffer
}func (w *BodyDumpResponseWriter) Write(b []byte) (int, error) {w.body.Write(b) // 注意这一行return w.ResponseWriter.Write(b)
}func ReadResponseBody(ctx *gin.Context) {rbw := &BodyDumpResponseWriter{body: &bytes.Buffer{}, ResponseWriter: ctx.Writer}ctx.Writer = rbwctx.Next()rawResp := rbw.body.String()if len(rawResp) == 0 {AbnormalPrint(ctx, "resp-empty", rawResp)return}ctx.Set(ctx_raw_response_body, rawResp)// 序列化Body,并放到ctx中// 读取响应Body的目的是记录审计日志用
}// AbnormalPrint 异常情况,打印信息到日志
func AbnormalPrint(ctx *gin.Context, typ string, rawResp string) {
// 具体代码忽略
}

简单一看,这不就是Gin获取响应体一种标准的方式吗?毕竟GitHub及Stack Overflow上都是这么写的
https://github.com/gin-gonic/gin/issues/1363
https://stackoverflow.com/questions/38501325/how-to-log-response-body-in-gin

那么问题出在哪呢?

再看下代码,可以看到这个代码的逻辑是每一个请求都会将响应的Body完整的缓存在内存一份,对于响应体很大的请求,在这里就会造成内存暴涨,比如:像日志下载。

找到了原因修改起来就比较简单了,根据请求响应的Header跳过文件下载类的请求;同时根据请求的Header跳过SSE及Websocket请求,因为这两类流的请求记录到审计日志中意义不大,而且在json序列化的时候也会有问题。

package serverimport ("bytes""fmt""net/http""strings""github.com/gin-gonic/gin"jsoniter "github.com/json-iterator/go"
)var json = jsoniter.ConfigCompatibleWithStandardLibrarytype BodyDumpResponseWriter struct {gin.ResponseWriterbody *bytes.Buffer
}func (w *BodyDumpResponseWriter) Write(b []byte) (int, error) {// 文件下载类请求,不再缓存相应结果if !isFileDownLoad(w.Header()) {w.body.Write(b)}return w.ResponseWriter.Write(b)
}func isNoNeedToReadResponse(req *http.Request) bool {if isSSE(req) || isWebsocket(req) {return true}return false
}func isSSE(req *http.Request) bool {contentType := req.Header.Get("Accept")if contentType == "" {contentType = req.Header.Get("accept")}contentType = strings.ToLower(contentType)// sseif !strings.Contains(contentType, "text/event-stream") {return false}return true
}// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
func isFileDownLoad(responseHeader http.Header) bool {contentType := strings.ToLower(responseHeader.Get("Content-Type"))if strings.Contains(contentType, "application/octet-stream") {return true}contentDisposition := responseHeader.Get("Content-Disposition")if contentDisposition != "" {return true}return false
}func isWebsocket(req *http.Request) bool {conntype := strings.ToLower(req.Header.Get("Connection"))upgrade := strings.ToLower(req.Header.Get("Upgrade"))if conntype == "upgrade" && upgrade == "websocket" {return true}return false
}func ReadResponseBody(ctx *gin.Context) {if isNoNeedToReadResponse(ctx.Request) {return}rbw := &BodyDumpResponseWriter{body: &bytes.Buffer{}, ResponseWriter: ctx.Writer}ctx.Writer = rbwctx.Next()contentType := ctx.Writer.Header().Get("content-type")if !strings.Contains(contentType, "application/json") {return}rawResp := rbw.body.String()if len(rawResp) == 0 {AbnormalPrint(ctx, "resp-empty", rawResp)return}ctx.Set(ctx_raw_response_body, rawResp)// 序列化Body,并放到ctx中// 读取响应Body的目的是记录审计日志用
}// AbnormalPrint 异常情况,打印信息到日志
func AbnormalPrint(ctx *gin.Context, typ string, rawResp string) {
// 具体代码忽略
}

其实,写这篇文章的目的并不是为了阐述这个问题如何解决,而是想说:

  • Copy 代码的时候留意下自己的场景
  • 尽量用轮子,而不是自己去造轮子

在我们手写API网关的时候,还遇到过以下问题

  • 第一版的网络处理也是手写的,导致对于各种Content-Type处理不好;
  • 因为要解析Body,也没有精力去适配各种压缩协议,所以在网关这里会强制关闭压缩;
  • 手写网络处理,会一些情况会出现一些诡异的问题
    • 比如:我们支持页面终端连接到K8S集群,而这个终端连接走的是Websocket,假设支持该连接操作的服务是A(就是:页面< - - - - - - >服务A< - - - - - - >K8S集群),那么后面过网关的请求部分请求会直接请求到服务A上(此时根本没有走网关的API router,
      直接就复用Websocket这个连接了),即使这些API不是服务A的。

第一版手写网络请求处理的代码示意如下:

func proxyHttp(ctx context.Context, proxy_req *http.Request, domain string) {// origin requestreq := ctx.Request()response, err := HttpClient.Do(proxy_req)if err != nil {// 打印异常return}defer response.Body.Close()//copy response headerif response != nil && response.Header != nil {for k, values := range response.Header {for _, value := range values {ctx.ResponseWriter().Header().Set(k, value)}}}// status codectx.StatusCode(response.StatusCode)buf := make([]byte, 1024)for {len, err := response.Body.Read(buf)if err != nil && err != io.EOF {// 打印异常break}if len == 0 {break}ctx.ResponseWriter().Write(buf[:len])ctx.ResponseWriter().Flush()continue}ctx.Next()
}func proxyWebSocket(ctx context.Context, request *http.Request, target string) {var logger = ctx.Application().Logger()responseWriter := http.ResponseWriter(ctx.ResponseWriter())conn, err := net.Dial("tcp", target)if err != nil {// 打印异常return}hijacker, ok := responseWriter.(http.Hijacker)if !ok {http.Error(responseWriter, "Not a hijacker?", 500)return}nc, _, err := hijacker.Hijack()if err != nil {// 打印异常return}defer nc.Close()defer conn.Close()err = request.Write(conn)if err != nil {// 打印异常return}errc := make(chan error, 2)cp := func(dst io.Writer, src io.Reader) {_, err := io.Copy(dst, src)errc <- err}go cp(conn, nc)go cp(nc, conn)// wait over<-errcctx.Application().Logger().Infof("websocket proxy to %s over", target)
}

后来换成了基础类库的httputil.ReverseProxy来处理网络连接,问题解决。

相关文章:

Gin获取Response Body引发的OOM

有轮子尽量用轮子 &#x1f62d; &#x1f62d; &#x1f62d; &#x1f62d; &#x1f62d; &#x1f62d; 我们在开发中基于Gin开发了一个Api网关&#xff0c;但上线后发现内存会在短时间内暴涨&#xff0c;然后被OOM kill掉。具体内存走势如下图&#xff1a; 放大其中一次 在…...

不同方案特性对比

特性对比项 2.4G 蓝牙 868M WIFI 通信速率 低 低 低 高 距离&#xff08;实用可靠&#xff09; 20米 10米 30米 15米 确定性 高 低 高 高 可靠性&#xff08;距离内&#xff09; 高 低 高 高 刷新一个标签时间&#xff08;通常&#xff09; 0.5-1s …...

线性数据结构:链表 LinkList

一、前言 链表的历史 于1955-1956年&#xff0c;由兰德公司的Allen Newell、Cliff Shaw和Herbert A. Simon开发了链表&#xff0c;作为他们的信息处理语言的主要数据结构。链表的另一个早期出现是由 Hans Peter Luhn 在 1953 年 1 月编写的IBM内部备忘录建议在链式哈希表中使…...

对restful的支持 rust-grpc-proxy

目录前言快速体验说明1. 启动目标服务2. 启动代理3. 测试4. example.sh尾语前言 继上一篇博文的展望&#xff0c;这个月rust-grpc-proxy提供了对restful的简单支持。 并且提供了完成的用例&#xff0c;见地址如下&#xff0c; https://github.com/woshihaoren4/grpc-proxy/tre…...

【模拟集成电路】环路滤波器(LPF)设计

环路滤波器 LPF 设计 前言环路滤波器设计仿真结果各部分链接链接&#xff1a;前言 本文主要内容是对环路滤波器 模块设计设计进行阐述&#xff0c;LPF在电荷泵频率综合器中&#xff0c;主要作用是进行滤波&#xff0c;消除毛刺&#xff0c;因此一个简单的RC就可以起到很好的效果…...

adb及cmd部分常用命令

adb及cmd部分常用命令cmd常用命令adb常用命令内存/cpu相关此文章日常记录&#xff0c;有可能存在不准确的地方&#xff0c;仅供参考即可。 cmd常用命令 返回上一级&#xff1a; cd… 进入指定盘&#xff1a; D: 进入指定路径&#xff1a; cd 文件路径 查看子文件列表&#xf…...

ProtoBuf介绍

1 编码和解码编写网络应用程序时&#xff0c;因为数据在网络传输的都是二进制字节码数据&#xff0c;在发送数据时进行编码&#xff0c;在接受数据时进行解码codec&#xff08;编码器&#xff09;的组成部分有2个&#xff1a;decoder&#xff08;解码器&#xff09;和encoder&a…...

数据结构:完全二叉树开胃菜小练习

目录 一.前言 二.完全二叉树的重要结构特点 三.完全二叉树开胃菜小练习 1.一个重要的数学结论 2.简单的小练习 一.前言 关于树及完全二叉树的基础概念(及树结点编号规则)参见:http://t.csdn.cn/imdrahttp://t.csdn.cn/imdra 完全二叉树是一种非常重要的数据结构: n个结点的…...

mybatis与jpa

1、官方文档 mybatis&#xff1a;mybatis-spring – jpa&#xff1a;https://springdoc.cn/spring-data-jpa/ 应用文档 jpa详解_java菜鸟1的博客-CSDN博客 JPA简介及其使用详解_Tourist-xl的博客-CSDN博客_jpa的作用 2、使用比较 mybatis一般用于互联网性质的项目&#x…...

js 求解《初级算法》66. 加一

一、题目描述 给定一个由 整数 组成的 非空 数组所表示的非负整数&#xff0c;在该数的基础上加一。最高位数字存放在数组的首位&#xff0c; 数组中每个元素只存储单个数字。你可以假设除了整数 0 之外&#xff0c;这个整数不会以零开头。 示例 1&#xff1a; 输入&#xff1a…...

力扣-游戏玩法分析

大家好&#xff0c;我是空空star&#xff0c;本篇带大家了解一道简单的力扣sql练习题。 文章目录前言一、题目&#xff1a;511. 游戏玩法分析二、解题1.正确示范①提交SQL运行结果2.正确示范②提交SQL运行结果3.正确示范③提交SQL运行结果4.正确示范④提交SQL运行结果5.其他总结…...

ZZNUOJ_用C语言编写程序实现1186 : 奖学金(结构体专题)(附完整源码)

题目描述 某校发放奖学金共5种,获取条件各不同: 1.阳明奖学金,每人8000,期末平均成绩>80,且在本学期发表论文大于等于1篇; 2.梨洲奖学金,每人4000,期末平均成绩>85,且班级评议成绩>80; 3.成绩优秀奖,每人2000,期末平均成绩>90; 4.西部奖学金,…...

加油站ai系统视频监测 yolov5

加油站ai系统视频监测通过yolov5网络模型深度学习边缘计算技术&#xff0c;加油站ai系统视频监测对现场卸油过程中人员违规离岗、现场灭火器没有按要求正确摆放、以及卸油前需要遵守静电释放15分钟、打电话、明火烟雾情况、抽烟行为进行自动识别。YOLO系列算法是一类典型的one-…...

【JDK8新特性之Stream流-Stream结果收集案例实操】

一.JDK8新特性之Stream流-Stream结果收集以及案例实操 二.Stream结果收集(collect函数)-实例实操 2.1 结果收集到集合中 /*** Stream将结果收集到集合中以及具体的实现 collect*/Testpublic void test01(){// 收集到List中 接口List<Integer> list Stream.of(1, 2, 3…...

Fiddler 抓包工具

HTTP代理所谓的http代理&#xff0c;其实就是代理客户机的http访问&#xff0c;主要代理浏览器访问页面。代理服务器是介于浏览器和web服务器之间的一台服务器&#xff0c;有了它之后&#xff0c;浏览器不是直接到Web服务器去取回网页而是向代理服务器发出请求&#xff0c;Requ…...

2023最新版网络安全保姆级指南,手把手带你从零基础进阶渗透攻防工程师

前言 一份网络攻防渗透测试的学习路线&#xff0c;不藏私了&#xff01; 1、学习编程语言(phpmysqljshtml) 原因&#xff1a; phpmysql可以帮助你快速的理解B/S架构是怎样运行的&#xff0c;只有理解了他的运行原理才能够真正的找到问题/漏洞所在。所以对于国内那些上来就说…...

排序基础之选择排序法

目录 前言 一、什么是选择排序 二、实现选择排序 三、使用泛型扩展 四、使用自定义类型测试 前言 今天天气不错&#xff0c;这么好的天气不干点啥实在是有点可惜了&#xff0c;于是乎&#xff0c;拿出键盘撸一把&#xff01; 来&#xff0c;今天来学习一下排序算法中的选…...

2.24测试用例

一.测试模型1.V模型特点:1.明确标注了测试的类型2.明确标注了测试阶段和开发阶段的对应关系缺点:测试后置2.W模型也叫双v模型,测试阶段全流程介入缺点:1.上一阶段完成.下一个阶段才能开始2.开发模型和测试模型也保持着一种线性的前后关系3.重文档,重过程,不支持敏捷模式二.设计…...

面试必刷101 Java题解 -- part 1

练习地址 面试必刷101-牛客1、链表反转2、链表内指定区间反转**3. 链表中的节点每k个一组翻转**4、**合并两个排序的链表**5、**合并k个已排序的链表**6、**判断链表中是否有环****7、链表中环的入口结点**8、链表中倒数最后k个结点**9、删除链表的倒数第n个节点****10、两个链…...

Python---关联与继承

专栏&#xff1a;python 个人主页&#xff1a;HaiFan. 专栏简介&#xff1a;Python在学&#xff0c;希望能够得到各位的支持&#xff01;&#xff01;&#xff01; 关联与继承前言has a关联关系is a继承关系子类不添加__init__子类添加__init__前言 has a关联关系 has - a 是在…...

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周&#xff0c;有很多同学在写期末Java web作业时&#xff0c;运行tomcat出现乱码问题&#xff0c;经过多次解决与研究&#xff0c;我做了如下整理&#xff1a; 原因&#xff1a; IDEA本身编码与tomcat的编码与Windows编码不同导致&#xff0c;Windows 系统控制台…...

Fabric V2.5 通用溯源系统——增加图片上传与下载功能

fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...

Java + Spring Boot + Mybatis 实现批量插入

在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法&#xff1a;使用 MyBatis 的 <foreach> 标签和批处理模式&#xff08;ExecutorType.BATCH&#xff09;。 方法一&#xff1a;使用 XML 的 <foreach> 标签&#xff…...

解读《网络安全法》最新修订,把握网络安全新趋势

《网络安全法》自2017年施行以来&#xff0c;在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂&#xff0c;网络攻击、数据泄露等事件频发&#xff0c;现行法律已难以完全适应新的风险挑战。 2025年3月28日&#xff0c;国家网信办会同相关部门起草了《网络安全…...

Spring Security 认证流程——补充

一、认证流程概述 Spring Security 的认证流程基于 过滤器链&#xff08;Filter Chain&#xff09;&#xff0c;核心组件包括 UsernamePasswordAuthenticationFilter、AuthenticationManager、UserDetailsService 等。整个流程可分为以下步骤&#xff1a; 用户提交登录请求拦…...

【无标题】湖北理元理律师事务所:债务优化中的生活保障与法律平衡之道

文/法律实务观察组 在债务重组领域&#xff0c;专业机构的核心价值不仅在于减轻债务数字&#xff0c;更在于帮助债务人在履行义务的同时维持基本生活尊严。湖北理元理律师事务所的服务实践表明&#xff0c;合法债务优化需同步实现三重平衡&#xff1a; 法律刚性&#xff08;债…...

第一篇:Liunx环境下搭建PaddlePaddle 3.0基础环境(Liunx Centos8.5安装Python3.10+pip3.10)

第一篇&#xff1a;Liunx环境下搭建PaddlePaddle 3.0基础环境&#xff08;Liunx Centos8.5安装Python3.10pip3.10&#xff09; 一&#xff1a;前言二&#xff1a;安装编译依赖二&#xff1a;安装Python3.10三&#xff1a;安装PIP3.10四&#xff1a;安装Paddlepaddle基础框架4.1…...

​​企业大模型服务合规指南:深度解析备案与登记制度​​

伴随AI技术的爆炸式发展&#xff0c;尤其是大模型&#xff08;LLM&#xff09;在各行各业的深度应用和整合&#xff0c;企业利用AI技术提升效率、创新服务的步伐不断加快。无论是像DeepSeek这样的前沿技术提供者&#xff0c;还是积极拥抱AI转型的传统企业&#xff0c;在面向公众…...

goreplay

1.github地址 https://github.com/buger/goreplay 2.简单介绍 GoReplay 是一个开源的网络监控工具&#xff0c;可以记录用户的实时流量并将其用于镜像、负载测试、监控和详细分析。 3.出现背景 随着应用程序的增长&#xff0c;测试它所需的工作量也会呈指数级增长。GoRepl…...

Canal环境搭建并实现和ES数据同步

作者&#xff1a;田超凡 日期&#xff1a;2025年6月7日 Canal安装&#xff0c;启动端口11111、8082&#xff1a; 安装canal-deployer服务端&#xff1a; https://github.com/alibaba/canal/releases/1.1.7/canal.deployer-1.1.7.tar.gz cd /opt/homebrew/etc mkdir canal…...