发现了一个关于 gin 1.3.0 框架的 bug
gin 1.3.0 框架 http 响应数据错乱问题排查
问题概述
客户端同时发起多个http请求,gin接受到请求后,其中一个接口响应内容为空,另外一个接口响应内容包含接口1,接口2的响应内容,导致响应数据错乱(偶现问题)
- 图1红框标注部分为正常请求响应
- 图1蓝框标注部分为异常请求响应(可以看到编号2531的响应数据只有一个状态码信息,并没有具体的返回内容)
- 图2 可以看到编号2533的响应数据包含两组object对象信息,其中第一条object信息应该是2531的响应数据
- 图1:
- 图2:
问题分析
因为此问题是偶现问题,有时响应数据又是正常的,而且本次新版本这两个接口代码也并没有修改,所以排查问题花了很长时间,下面是我一步步排查问题的过程.
- 首先怀疑是代码逻辑问题,通过review接口代码逻辑后,这两个接口逻辑都非常简单,且没有任何逻辑关联,所以基本上排除了接口逻辑问题。
- 怀疑是否是并发请求导致的问题,通过golang并且开启多个协程模拟并发发起http请求同时调用这两个接口100次,并没有复现出这种问题,所以可以排除并发请求导致的问题。
- 因为使用golang同时调用这两个接口没有复现此问题,怀疑是否是客户端调用的问题,是否共用了一个http连接发送请求,导致最终响应结果合并到了一起? review客户端代码后,发现代码逻辑也没有什么问题,并且通过抓包后却发现的确是后台响应的数据就有问题,所以可以确认就是后端的问题。
- 此时有同事建议打印一下两个接口后台请求和响应的对象内存地址看一下,是否是共用了同一个对象导致,果然打印之后发现,当数据错乱时,两个接口使用的是同一个对象,两个接口没有任何逻辑关系,为什么会使用同一个请求对象? 为什么就两个接口会出现数据错乱的问题? 难道是gin框架的问题? 此时我们尝试着调试代码去验证
实验验证
- 通过调试发现,调试信息如图3所示(第1部分为正常情况,可以观察到对象指针地址不一样,第2部分为异常情况,可以观察到对象指针地址一样):
- 图3:
此时我观察到每次这两个接口请求后面,都跟着另外一个接口请求,如图1所示的第2494条请求 /api/client/area/scenes 接口,并且本次新版本功能改动了这块的逻辑,会不会是受这个接口的影响了,于是我尝试恢复了这块的代码,恢复后测试多次发现问题无法重现,所以可以断定是受了这块代码的影响.
然而本次修改的代码逻辑主要是为了兼容老版本的客户端,为此接口添加了一个中间件,引入了gin框架的HandleContext(context) 方法,用来做一个统一的中间件,做路由的转发,具体代码逻辑如图4所示.
- 图4:
- gin框架为golang web开发中,很常用的框架,使用人员非常多,这么明显的问题不可能没人发现,虽然极力的认为不可能是框架的问题,但是事实表明就是这里的问题,于是通过查询资料发现,此方法的确可能出现问题,如图5所示
- 图5:
- 可以确认gin框架有问题了,可是原因是什么了?网上并没有详细的说明,于是我打算通过调试阅读源码的方式来测试,在阅读源码的时候我发现,本地代码和gin最新的官方源码已经不一致,于是我发现本地代码版本为1.3.0,而官方代码已经更新到1.6.3了, 如图6所示: 1.6.3已经删除了 engine.pool.Put(c) 此行代码
- 图6:
- 于是我尝试者把gin版本从1.3.0升级到1.6.3,看看问题是否已经解决,果然gin版本升级后,连续测试多次未能重现问题,所以可以确定就是这里的问题,并且问题已经解决
虽然问题已经解决了,但是为什么删除了这一行就可以了? 好像并没有搞清楚具体的原理是什么? 于是我尝试着继续分析原理
- engine.pool.Put(c) 函数使用的是 golang的 sync.Pool 类,sync.Pool设计的目的是用来保存和复用临时对象,以减少内存分配,降低CG压力,Pool对外暴露的主要有三个接口:get(),put(),new()
- Get 返回 Pool 中的任意一个对象。如果 Pool 为空,则调用 New 返回一个新创建的对象。
- sync.Pool 是一个临时对象池。一句话来概括,sync.Pool 管理了一组临时对象,当需要时从池中获取,使用完毕后从再放回池中,以供他人使用。
- Put的过程就是将临时对象放进 Pool 里面。
- 通过如下图7也可以看到 HandleContext 方法上面有一个 ServeHTTP 方法,可以明显看到此方法也调用了 engine.pool.Put(c) 方法,并且也调用了 engine.pool.Get().(Context) 方法,通过调试发现 ServeHTTP 为http请求通用的方法,所有请求都会先调用 ServeHTTP ,如果调用了 HandleContext 则会再调用 HandleContext ,具体执行顺序如下图7所示,如图可以看出来 engine.pool.Put(c) 会执行两次,这样就会导致在sync.Pool存在两个同样的对象,在后面的请求中通过 engine.pool.Get().(Context) 获取context对象时就会获取到相同的context对象,导致ResponseWriter指针一样,从而导致响应数据输出到同一个接口中.
- 图7:
小结
此次问题主要是使用了低版本的gin框架所致,所以可以看出即使再成熟的框架,也可能会存在bug,在实际开发过程中应该及时升级到框架的最新稳定版本, 这样可以防止一些潜在的bug,当发现一些未知的问题,不要凭空猜测,要尽可能的调试代码,找到问题的根本原因.
参考资料:
- gin框架介绍: https://studygolang.com/articles/26000?fr=sidebar
- sync.Pool原理:https://blog.csdn.net/u010853261/article/details/90647884
发现了一个关于 gin 1.3.0 框架的 bug的更多相关文章
- 【编程题目】n 个数字(0,1,…,n-1)形成一个圆圈,从数字 0 开始
第 18 题(数组):题目:n 个数字(0,1,…,n-1)形成一个圆圈,从数字 0 开始,每次从这个圆圈中删除第 m 个数字(第一个为当前数字本身,第二个为当前数字的下一个数字).当一个数字删除后, ...
- for循环练习题(1 ,判断任意一个数是91的多少倍 2,编写程序实现给定一个整数判断它从0到这个整数中间出现多少次9的次数)
1 //判断任意一个数是9的多少倍 #include <stdio.h> #include <stdlib.h> int main() { printf("请输入任意 ...
- javascript随机数发现的一个parseInt函数的问题
前几天想伪造一些数据,用到了随机数,没有自己写,便在网上找了一下,找到了这篇文章:https://www.cnblogs.com/starof/p/4988516.html .之后测试了一下,发现了一 ...
- 排查dubbo接口重复注销问题,我发现了一个巧妙的设计
背景 我在公司内负责自研的dubbo注册中心相关工作,群里经常接到业务方反馈dubbo接口注销报错.经排查,确定是同一个接口调用了两次注销接口导致,由于我们的注册中心注销接口不能重复调用,调用第二次会 ...
- range([start], stop[, step]):产生一个序列,默认从0开始
range([start], stop[, step]):产生一个序列,默认从0开始 >>> l = range(10) >>> l [0, 1, 2, 3, 4, ...
- solaris X86-64下一个ORACLE战斗11.2.0.3.8在一波折叠补丁
solaris X86-64下一个ORACLE战斗11.2.0.3.8补丁: 正确的步骤: 1.BUG6880880 .OPATCH补丁 2.BUG16902043.11.2.0.3.8补丁 情感是练 ...
- VS 2017开发ASP.NET Core Web应用过程中发现的一个重大Bug
今天试着用VS 2017去开发一个.net core项目,想着看看.net core的开发和MVC5开发有什么区别,然后从中发现了一个VS2017的Bug. 首先,我们新建项目,ASP.NET Cor ...
- 发现了一个非常棒的pyqt5的例子集
发现了一个非常棒的pyqt5的例子集 https://github.com/892768447/PyQt 各种各样的PyQt测试和例子 [Python3.4.4 or Python3.5][PyQt5 ...
- Java 读数据库字段时发现的一个现象
早上发现有一个网名叫“帅!是不需要理由”的一个人,在后台只能看到“帅!是不需要理”,“由”字就是不显示出来. 经过分析发现,在Access数据库中,name这个字段的长度是15,因为我知道Access ...
随机推荐
- 【Luogu】P6232 [eJOI2019]挂架 题解
这道题跟CSP/S 2019 D1T1有点像. 我们先来模拟一下 \(n=4\) 的情况, 不难得出,最后的衣架挂钩顺序: 下标: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 1 ...
- numpy的统计分析
一.排序 间接排序(argsort,lexsort) 根据一个或多个数据集进行排序 1.Sort() --对数值直接进行排序 a.一维排序 b.二维排序 c.axis的认知 2.argsort() - ...
- Serilog 源码解析——Sink 的实现
在上一篇中,我们简单地查看了 Serilog 的整体需求和大体结构.从这一篇开始,本文开始涉及 Serilog 内的相关实现,着重解决第一个问题,即 Serilog 向哪里写入日志数据的.(系列目录) ...
- 【SpringBoot】07.SpringBoot文件上传
SpringBoot文件上传 1.编写html文件在classpath下的static中 <!DOCTYPE html> <html> <head> <met ...
- 性能测试之JVM的监控Grafana
安装配置Grafana参考 https://testerhome.com/articles/23629 使用配置 下载jmx_exporter https://github.com/prometheu ...
- 【转载】TCP/IP协议栈
TCP/IP 协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连入因特网,以及数据如何在它们之间进行传输.TCP/IP 协议采用4层结构,分别是应用层.传输层.网络层和链路 ...
- File 方法
File类说明 存储在变量,数组和对象中的数据是暂时的,当程序终止时他们就会丢失.为了能够永 久的保存程序中创建的数据,需要将他们存储到硬盘或光盘的文件中.这些文件可以移动,传送,亦可以被其他程序使用 ...
- [MIT6.006] 1. Algorithmic Thinking, Peak Finding 算法思维,峰值寻找
[MIT6.006] 系列笔记将记录我观看<MIT6.006 Introduction to Algorithms, Fall 2011>的课程内容和一些自己补充扩展的知识点.该课程主要介 ...
- CSP-S 2020 Travels
CSP-S 2020 Travels DAY 0 I hit the board in the morning before departure The rest of the time is dec ...
- WIN10下安装python3.7.2出现“尝试创建C:\Users\XX\AppData\Roaming\Microsoft\Installer时出错”
WIN10下安装python3.7.2出现"尝试创建C:\Users\XX\AppData\Roaming\Microsoft\Installer时出错" 1.右键点击安装包以管理 ...