请求双发提高服务可用性

背景

为公司内部同学提供了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行代码就实现了请求容错、服务高可用,主备机器任意故障对用户请求无损,上线后错误率大概降到了十五万分之一。