Gin获取Response Body引发的OOM

news2024/12/21 19:17:57

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

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

在这里插入图片描述

放大其中一次

在这里插入图片描述

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

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

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

package server

import (
	"bytes"
	"fmt"

	"github.com/gin-gonic/gin"
	jsoniter "github.com/json-iterator/go"
)

var json = jsoniter.ConfigCompatibleWithStandardLibrary

type BodyDumpResponseWriter struct {
	gin.ResponseWriter
	body *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 = rbw

	ctx.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 server

import (
	"bytes"
	"fmt"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	jsoniter "github.com/json-iterator/go"
)

var json = jsoniter.ConfigCompatibleWithStandardLibrary

type BodyDumpResponseWriter struct {
	gin.ResponseWriter
	body *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)
	// sse
	if !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 = rbw

	ctx.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 request
	req := ctx.Request()

	response, err := HttpClient.Do(proxy_req)
	if err != nil {
		// 打印异常
		return
	}

	defer response.Body.Close()

	//copy response header
	if response != nil && response.Header != nil {
		for k, values := range response.Header {
			for _, value := range values {
				ctx.ResponseWriter().Header().Set(k, value)
			}
		}
	}
	// status code
	ctx.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
	<-errc

	ctx.Application().Logger().Infof("websocket proxy to %s over", target)
}

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/371173.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OllyDbg

本文通过吾爱破解论坛上提供的OllyDbg版本为例&#xff0c;讲解该软件的使用方法 F2对鼠标所处的位置打下断点&#xff0c;一般表现为鼠标所属地址位置背景变红F3加载一个可执行程序&#xff0c;进行调试分析&#xff0c;表现为弹出打开文件框F4执行程序到光标处F5缩小还原当前…

EF 框架的简介、发展历史;ORM框架概念

一、EF 框架简介EF 全称是 EntityFramework 。Entity Framework是ADO.NET 中的一套支持开发面向数据的软件应用程序的技术,是微软的一个ORM框架。ORM框架&#xff08;Object Relational Mapping&#xff09; 翻译过来就是对象关系映射。如果不用ORM框架&#xff0c;我们一般这样…

考虑交叉耦合因素的IPMSM无传感器改进线性自抗扰控制策略

考虑交叉耦合因素的IPMSM无传感器改进线性自抗扰控制策略一级目录二级目录三级目录控制原理ELADRC信号提取龙格贝尔观测器方波注入simulink仿真给定转速&#xff1a;转速环&#xff1a;电流环&#xff1a;一级目录 二级目录 三级目录 首先声明一下&#xff0c;本篇博客是复现…

分析 HTTP,TCP 的长连接和短连接以及 socket

1、HTTP 协议与 TCP/IP 协议的关系 HTTP 的长连接和短连接本质上是 TCP 长连接和短连接。HTTP 属于应用层协议&#xff0c;在传输层使用 TCP 协议&#xff0c;在网络层使用 IP 协议。IP 协议主要解决网络路由和寻址问题&#xff0c;TCP 协议主要解决如何在 IP 层之上可靠的传递…

Apache Hadoop生态部署-Flume采集节点安装

目录 Apache Hadoop生态-目录汇总-持续更新 一&#xff1a;安装包准备 二&#xff1a;安装与常用配置 2.1&#xff1a;下载解压安装包 2.2&#xff1a;解决guava版本问题 2.3&#xff1a;修改配置 三&#xff1a;修复Taildir问题 3.1&#xff1a;Taildir Source能断点续…

SpringMVC请求转发和重定向

请求转发&#xff1a;forward:重定向&#xff1a;redirect转发&#xff1a;由服务器的页面进行跳转&#xff0c;不需要客户端重新发送请求&#xff1a;特点如下&#xff1a;1、地址栏的请求不会发生变化&#xff0c;显示的还是第一次请求的地址2、请求的次数&#xff0c;有且仅…

已解决kettle新建作业,点击保存抛出异常Invalid state, the Connection object is closed.

已解决kettle新建作业&#xff0c;点击保存进资源数据库抛出异常Invalid state, the Connection object is closed.的解决方法&#xff0c;亲测有效&#xff01;&#xff01;&#xff01; 文章目录报错问题报错翻译报错原因解决方法联系博主免费帮忙解决报错报错问题 一个小伙伴…

JS 执行机制 详解(附图)

一、JS是单线程JS语言的一大特点就是单线程&#xff0c;也就是说&#xff0c;同一个时间只能做一件事。这是JS这门脚本语言诞生的使命所致——用来处理页面中用户的交互&#xff0c;以及操作DOM而诞生的。单线程就意味着&#xff0c;所有任务需要排队&#xff0c;前一个任务结束…

C++014-C++字符串

文章目录C014-C字符串字符串目标char[]和stringchar[]char*string字符常量与字符串常量字符串的输入题目描述 字符串输出题目描述在线练习&#xff1a;总结C014-C字符串 在线练习&#xff1a; http://noi.openjudge.cn/ https://www.luogu.com.cn/ 字符串 目标 1、了解字符串…

OAuth2.0从入门到实战(附github地址)

OAuth2.0 文章目录OAuth2.0OAuth2.0的含义与思想[快递员的例子]([OAuth 2.0 的一个简单解释 - 阮一峰的网络日志 (ruanyifeng.com)](https://www.ruanyifeng.com/blog/2019/04/oauth_design.html))互联网的例子令牌与密码OAuth2.0的四种授权方式RFC 6749一、授权码(前后端分离)…

Vue3电商项目实战-商品详情模块6【17-商品详情-标签页组件、18-商品详情-热榜组件、19-商品详情-详情组件、20-商品详情-注意事项组件】

文章目录17-商品详情-标签页组件18-商品详情-热榜组件19-商品详情-详情组件20-商品详情-注意事项组件17-商品详情-标签页组件 目的&#xff1a;实现商品详情组件和商品评价组件的切换 大致步骤&#xff1a; 完成基础的tab的导航布局完成tab标签页的切换样式效果使用动态组件完…

【设计模式】 策略模式介绍及C代码实现

【设计模式】 策略模式介绍及C代码实现 背景 在软件构建过程中&#xff0c;某些对象使用的算法可能多种多样&#xff0c;经常改变&#xff0c;如果将这些算法都编码到对象中&#xff0c;将会使对象变得异常复杂&#xff0c;而且有时候支持不使用的算法也是一个性能负担。 如何…

go单元测试

接着上一篇中的go module创建项目calc为例&#xff0c;在simplemath包中&#xff0c;是使用在命令行中使用交互式的方式进行测试&#xff0c;现在可以为这几个函数实现单元测试&#xff0c; go test&#xff0c;这个测试工具来自于 Go 官方的 gc 工具链。 运行 go test 命令将执…

JVM本地方法接口和本地方法栈

1、本地方法概述简单地讲&#xff0c;一个Native Method是一个Java调用非Java代码的接囗。一个Native Method是的实现由非Java语言实现&#xff0c;比如C。这个特征并非Java所特有&#xff0c;很多其它的编程语言都有这一机制&#xff0c;比如在C中&#xff0c;你可以用extern …

openpnp - 零碎记录

文章目录openpnp - 零碎记录概述笔记配置文件保存无效ENDopenpnp - 零碎记录 概述 这段时间, 正在配置校准手头的openpnp设备, 用的官网最新的openpnp2.0. 由于openpnp的bug和自己的不细致, 导致多次校准失败. 现在从头校准时, 每进行一步, 就保存一下配置文件, 如果最终发现…

MySQL_主从复制读写分离

主从复制 概述 主从复制是指将主数据库的DDL和DML操作通过二进制日志传到从库服务器中&#xff0c;然后在从库上对这些日志重新执行&#xff08;也叫重做&#xff09;&#xff0c;从而使得从库和主库的数据保持同步。 MySQL支持一台主库同时向多台从库进行复制&#xff0c;从…

leetcode 31~40 学习经历

leetcode 31~40 学习经历31. 下一个排列32. 最长有效括号33. 搜索旋转排序数组34. 在排序数组中查找元素的第一个和最后一个位置35. 搜索插入位置36. 有效的数独37. 解数独38. 外观数列39. 组合总和40. 组合总和 II小结31. 下一个排列 整数数组的一个 排列 就是将其所有成员以序…

3.JVM内存分配机制详解【2023】

redis跳表 内容概要 内存分配 1.类加载检查 &#x1f60a;虚拟机遇到一条new指令时&#xff0c;首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用&#xff0c;并且检查这个 符号引用代表的类是否已被加载、解析和初始化过。如果没有&#xff0c;那必须先执…

MySQL/Oracle获取当前时间几天/分钟前的时间

获取当前时间 要想获取当前时间几天/分钟前的时间&#xff0c;首先要知道怎么获取当前时间&#xff1b; 对于MySQL和Oracle获取当前时间的方法是不一样的&#xff1b; MySQL&#xff1a; select NOW(); 示例&#xff1a; Oracle&#xff1a; select sysdate from dual; 示…

西北工业大学2020-2021学年大物(I)下期末试题选填解析

2 位移电流。磁效应服从安培环路&#xff0c;热效应不服从焦耳-楞次定律。注意&#xff0c;它是变化的电场而非磁场产生。3 又考恒定磁场中安培环路定理。4感生电场5 麦克斯韦速率分布函数。6 相同的高温热源和低温热源之间的一切可逆热机的工作效率相等&#xff0c;无论工质如…