wuyongxiu / wuyongxiu.github.io

随便记录一下......
http://wuyongxiu.github.io
6 stars 3 forks source link

Http: invalid read on closed body #14

Open wuyongxiu opened 7 years ago

wuyongxiu commented 7 years ago

一 问题:


1)前言
在请求后端服务的时候,如果应用了负载均衡策略时,当请求服务后端其中一个节点失败后,会根据负载均衡策略请求下一个节点,直到请求成功或者因为失败次数达到一定值将后端服务设置为不可用状态。
在这个过程中,由于go语言的底层机制,在发送http请求(并且Body里有值时),每次调用RoundTrip函数后,无论是请求成功或是失败,都会自动关闭请求body,所以若请求后端服务失败后,再次请求下一个节点时,会因为request的body在上一次请求中被关闭而报出http: invalid read on closed body错误从而导致这次请求也失败。
2)问题细节:
如图,我后端有个日志服务kdcs,它有两个节点 http://localhost:9604/kdcs.... 和http://192.168.206.231:9606/kdcs... 采用的转发策略是 轮询法,所以当 转发给localhost失败时(本机没有运行kdcs),它会自动重试转发到192.168.206.231(这个地址的kdcs是可以访问的)。
但是如果我转发的请求是 get 或者Post(body没有值的)方法的,那么失败后,会重试成功。可是如果我转发的请求是 Post并且body里有值的,在转发给 localhost失败后,转发给192.168.206.231时也会因为 http: invalid read on closed body而转发失败。

image
转发的伪代码是(只是一个小demo)

for{
...
//新建一个请求
req=&http.Request{}
*outreq = *req
...
//转发
  res,err:=transport.RoundTrip(req)  //这一步执行完后,roundtrip一定会将req的body关闭
  if err!=nil{
//转发失败,重试 
continue  //由于前面一步将body关闭了,所以再一次重试时,会报错。
}
...
//成功,return
return

}



二 解决方案:


解决方法就是对request的body进行封装,使得body只能手动关闭,而不能被transport自动关闭。 这个问题在很多包含请求转发模块的项目里都有遇到,例如 flynn,里面就有详细的原因解释和解决方法,我也是参照这个方法,将问题解决掉的:https://github.com/flynn/flynn/issues/872
1)对io.ReadCloser进行封装后构造了fakeCloseReadCloser,该结构体的Close方法什么都不执行,RealClose方法才是真正的关闭此结构体

image
2) 对请求Body进行封装,构造成fakeCloseReadCloser结构
image
3) 当转发结束后,才调用RealClose()函数,关闭body。如图,如果还要继续执行for循环,也就是调用其它节点,这个body 因为没有调用RealClose()函数,尽管RoundTrip函数仍会调用close()函数,因为fakeCloseReadCloser的close()函数什么也没做,所以body就不会给关闭了,也就不会出现 再次转发时出现的 http:invalid read on closed body 的错误了。
image
PS: caddy里也有这个问题,issues还是我们测试和提出的哦,激动.gif