请求双发提高服务可用性
背景
为公司内部同学提供了maven聚合代理仓库,这个聚合仓库代理了外网上十个仓库,外网网络链路的不可靠加上外网仓库的稳定性不确定,代理仓库请求容易出现请求失败,虽然失败率不高(十万分之三),但maven构建时请求的敏感性,任何一次请求失败都会导致整个构建流程失败,这里还是有可以优化的点。刚好做容灾搭建了一套备份服务,可以用请求双发来做单服务的容错。
实现
请求双发在nginx层是没法做到的,由于我们有在用Go的httputil.ReverseProxy
在做业务proxy,这上面是一个实现口。
RevserseProxy的源码,发现是可以自定义Transport的。
type ReverseProxy struct {
...
// The transport used to perform proxy requests.
// If nil, http.DefaultTransport is used.
Transport http.RoundTripper
...
Transport是一个interface,返回一个http response。
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}
所以只要实现一个RoundTripper,并发请求主备机器,返回最快的response就行。
这里定义一个MultiRoundTripper
,这里当然不会再去从头实现一个发送http请求的的transport,用默认的http.DefaultTransport发送http请求就行,所以还是定义把这个interface暴露出去。
type MultiRoundTripper struct {
RT http.RoundTripper #发送http请求的transport
BackupHost map[string]string #需要双发的主备机器
SupportMethod map[string]struct{} #支持双发的方法
}
完整实现代码:
type wrapResponse struct {
res *http.Response
err error
}
func NewMultiRoundTripper(rt http.RoundTripper, BackupHost map[string]string, SupportMethod map[string]struct{}) http.RoundTripper {
return &MultiRoundTripper{RT: rt, BackupHost: BackupHost, SupportMethod: SupportMethod}
}
func (r *MultiRoundTripper) forward(req *http.Request, done chan wrapResponse, exit chan struct{}) {
stat.MultiRoundTripperTotalReq.Add(req.Host, 1)
res, err := r.RT.RoundTrip(req)
if err != nil {
// error handle
}
if res != nil {
res.Header.Set("X-Real-Server-Backend", req.Host)
}
select {
case done <- wrapResponse{res: res, err: err}:
case <-exit:
//不需要res了
if res != nil {
// 如果没读body,就不能复用长连接了
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
}
}
}
func (r *MultiRoundTripper) RoundTrip(req *http.Request) (res *http.Response, err error) {
backupHost, ok := r.BackupHost[req.Host]
if !ok {
return r.RT.RoundTrip(req)
}
if _, ok := r.SupportMethod[req.Method]; !ok {
return r.RT.RoundTrip(req)
}
backupReq := req.Clone(req.Context())
backupReq.Host = backupHost
backupReq.URL.Host = backupHost
done := make(chan wrapResponse)
exit := make(chan struct{})
go r.forward(req, done, exit)
go r.forward(backupReq, done, exit)
ret := <-done
res, err = ret.res, ret.err
// 状态码少于500认为正常,可以返回,需要关闭exit,通知另外一个goroutine
if err == nil && res.StatusCode < 500 {
close(exit)
return
}
if res != nil {
// 这里也需要关闭res.Body, 不然会fd leak
io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
}
//继续等待
ret = <-done
res, err = ret.res, ret.err
return
}
调用
multiRoundTripper = NewMultiRoundTripper(http.DefaultTransport, backupHost,
map[string]struct{}{"GET": struct{}{}, "HEAD": struct{}{}})
rp = &httputil.ReverseProxy{Transport: multiRoundTripper}
收益
不到100行代码就实现了请求容错、服务高可用,主备机器任意故障对用户请求无损,上线后错误率大概降到了十五万分之一。