理解 timeout

timeout 又可以细分为 connect timeout、read timeout、write timeout。而 read timeout 和 write timeout 必须是在 connect 之后才能发生。

  • ReadTimeout的时间计算是从连接被接受(accept)到request body完全被读取(如果你不读取body,那么时间截止到读完header为止)
  • WriteTimeout的时间计算正常是从request header的读取结束开始,到response write结束为止 (也就是ServeHTTP方法的生命周期)

http vs fasthttp

  • fasthttp 使用 tcp 做长连接,使用连接池复用连接
  • net/http 是短连接,用完即断开连接

常见问题

golang http context deadline exceeded (client.timeout exceeded while awaiting headers)

  1. 请求已经到达服务端但超时,原因:服务方响应慢
  2. 请求没到服务端超时,原因:golang CPU调度不过来

net/http

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// net/http client 示例
import (
    "crypto/tls"
    "io/ioutil"
    "net/http"
    "net/url"
    "strings"
    "time"
)

type (
    RequestForm struct {
        Uri            string                 // 域名+路由
        Params         map[string]interface{} // 请求参数
        Header         url.Values             // 请求头
        RespBody       string                 // 响应内容
        RespStatusCode int                    // 响应状态码
        Resp           interface{}            // 响应结果结构化
    }

    client struct{}

    IClient interface {
        Get(dto *RequestForm) error
        Post(dto *RequestForm) error
    }
)

const ttl = 10 * time.Second

var (
    Client     IClient = new(client)
    HttpClient         = CreateHttpClient()
)

func CreateHttpClient() *http.Client {
    transport := &http.Transport{
        TLSHandshakeTimeout: 10 * time.Second,
        MaxIdleConnsPerHost: 300,
        MaxConnsPerHost:     500,
        IdleConnTimeout:     time.Minute,
        TLSClientConfig: &tls.Config{
            Renegotiation: tls.RenegotiateOnceAsClient,
        },
    }
    c := &http.Client{
        Timeout:   ttl,
        Transport: transport,
    }
    return c
}

func (c *client) Get(dto *RequestForm) error {
    req, err := http.NewRequest(http.MethodGet, dto.Uri, nil)
    if err != nil {
        return err
    }
    if dto.Header != nil {
        for k, v := range dto.Header {
            req.Header.Set(k, strings.Join(v, ","))
        }
    }

    resp, err := HttpClient.Do(req)
    if err != nil {
        return err
    }
    defer func() {
        _ = resp.Body.Close()
    }()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return err
    }
    dto.RespBody = string(body)
    dto.RespStatusCode = resp.StatusCode
    if dto.Resp != nil {
        JsonUnmarshalByte(body, dto.Resp)
    }

    return nil
}

func (c *client) Post(dto *RequestForm) error {
    bodyJson := JsonMarshal(dto.Params)
    req, err := http.NewRequest(http.MethodPost, dto.Uri, strings.NewReader(bodyJson))
    if err != nil {
        return err
    }
    if dto.Header != nil {
        for k, v := range dto.Header {
            req.Header.Set(k, strings.Join(v, ","))
        }
    }

    resp, err := HttpClient.Do(req)
    if err != nil {
        return err
    }
    defer func() {
        _ = resp.Body.Close()
    }()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return err
    }
    dto.RespBody = string(body)
    dto.RespStatusCode = resp.StatusCode
    if dto.Resp != nil {
        JsonUnmarshalByte(body, dto.Resp)
    }

    return nil
}

func JsonMarshal(v interface{}) string {
    bytes, err := json.Marshal(v)
    if err != nil {
        log.Println("json序列化:", err)
    }
    return string(bytes)
}
func JsonUnmarshalByte(src []byte, v interface{}) {
    err := json.Unmarshal(src, v)
    if err != nil {
        log.Println("json反序列化:", err)
    }
    return
}

fasthttp

github.com/valyala/fasthttp

模拟 http 客户端请求[参考文章 https://juejin.im/post/6844903761832476686]

fasthttp 不会将请求 header 值存储为导出的 map[string]string,而是存储为未存储的 []byte (将索引存储到其中)

1
2
3
4
5
6
7
var req fasthttp.Request

// 获取请求头选项
v := string(req.Request.Header.Peek("User-Agent"))

// 打印请求头
fmt.Println(string(req.Header.Header()))

服务器端超时设置

服务器端超时原理

参考文章

http.Server有两个设置超时的方法: ReadTimeout 和 andWriteTimeout`。你可以显示地设置它们:

  • ReadTimeout的时间计算是从连接被接受(accept)到request body完全被读取(如果你不读取body,那么时间截止到读完header为止)。
  • WriteTimeout的时间计算正常是从request header的读取结束开始,到 response write结束为止 (也就是 ServeHTTP 方法的声明周期)。

可能报错

  • timeout
  • dialing to the given TCP address timed out【解决方案:请求服务器负载过高导致,提升请求服务器的配置或者降低并发数】
  • the server closed connection before returning the first response byte. Make sure the server returns ‘Connection: close’ response header before closing the connection【解决方案:调小 MaxIdleConnDuration 参数,避免请求服务的 keep-alive 过短主动关闭】
  • no free connections available to host【解决方案:调高参数 MaxConnsPerHost(每个 host 最大的连接数,默认512)】
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
// fasthttp client 示例
type RequestForm struct {
    Uri      string                 // 请求uri
    Params   map[string]interface{} // 请求参数
    Header   url.Values             // 请求头
    RespBody string                 // 响应结果
    Resp     interface{}            // 响应结果结构化
}

type client struct{}

type IClient interface {
    Get(dto *RequestForm) error
    Post(dto *RequestForm) error
}

const ttl = 5 * time.Second

var (
    defaultDialer = &fasthttp.TCPDialer{Concurrency: 200} // tcp 并发200

    FastClient = CreateFastHttpClient()

    Client IClient = new(client)
)

func CreateFastHttpClient() fasthttp.Client {
    return fasthttp.Client{
        MaxConnsPerHost:     300,
        MaxIdleConnDuration: 10 * time.Second, // 空闲链接时间应短,避免请求服务的 keep-alive 过短主动关闭,默认10秒
        MaxConnDuration:     10 * time.Minute,
        ReadTimeout:         30 * time.Second,
        WriteTimeout:        30 * time.Second,
        MaxResponseBodySize: 1024 * 1024 * 10,
        MaxConnWaitTimeout:  time.Minute,
        Dial: func(addr string) (net.Conn, error) {
            idx := 3 // 重试三次
            for {
                idx--
                conn, err := defaultDialer.DialTimeout(addr, 10*time.Second) // tcp连接超时时间10s
                if err != fasthttp.ErrDialTimeout || idx == 0 {
                    return conn, err
                }
            }
        },
    }
}

func (*client) createSign(dto *RequestForm, ts string) (string, error) {
    paramsStr, err := helper.HttpBuildQuery(dto.Params)
    if err != nil {
        return "", err
    }
    paramsStr = helper.Md5(paramsStr)
    sign := helper.Md5(config.OAPrefix + config.OAKey + config.OASecret + dto.Uri + ts + paramsStr)
    return sign, nil
}

func (c *client) Get(dto *RequestForm) error {
    req := fasthttp.AcquireRequest()
    resp := fasthttp.AcquireResponse()
    defer func() {
        fasthttp.ReleaseResponse(resp)
        fasthttp.ReleaseRequest(req)
    }()

    ts := strconv.FormatInt(time.Now().Unix(), 10)
    sign, err := c.createSign(dto, ts)
    if err != nil {
        return err
    }

    dto.Params["sign"] = sign
    dto.Params["api"] = dto.Uri
    dto.Params["key"] = config.OAKey
    dto.Params["ts"] = ts
    paramsStr, err := helper.HttpBuildQuery(dto.Params)
    if err != nil {
        return err
    }

    req.SetRequestURI("https://xxx.com" + "?" + paramsStr)
    req.Header.SetMethod(http.MethodGet)

    if dto.Header != nil {
        for k, v := range dto.Header {
            req.Header.Set(k, strings.Join(v, ","))
        }
    }

    if err := FastClient.DoTimeout(req, resp, ttl); err != nil {
        return err
    }

    dto.RespBody = string(resp.Body())

    if resp.StatusCode() != fasthttp.StatusOK || !gjson.Get(dto.RespBody, "success").Bool() {
        return errors.New(gjson.Get(dto.RespBody, "msg").String())
    }

    if dto.Resp != nil {
        helper.JsonUnmarshal(dto.RespBody, dto.Resp)
    }

    return nil
}

func (c *client) Post(dto *RequestForm) error {
    req := fasthttp.AcquireRequest()
    resp := fasthttp.AcquireResponse()
    defer func() {
        fasthttp.ReleaseResponse(resp)
        fasthttp.ReleaseRequest(req)
    }()

    ts := strconv.FormatInt(time.Now().Unix(), 10)
    sign, err := c.createSign(dto, ts)
    if err != nil {
        return err
    }

    dto.Params["sign"] = sign
    dto.Params["api"] = dto.Uri
    dto.Params["key"] = config.OAKey
    dto.Params["ts"] = ts

    // application/json 编码方式
    req.SetBody(helper.JsonMarshalByte(dto.Params))
    req.Header.SetContentType("application/json")

    // application/x-www-form-urlencoded 编码方式
    // paramsStr, err := helper.HttpBuildQuery(dto.Params)
    // if err != nil {
    //     return err
    // }
    // req.SetBody([]byte(paramsStr))
    // req.Header.SetContentType("application/x-www-form-urlencoded")

    req.SetRequestURI("http://xxx.com" + dto.Uri)
    req.Header.SetMethod(http.MethodPost)

    if dto.Header != nil {
        for k, v := range dto.Header {
            req.Header.Set(k, strings.Join(v, ","))
        }
    }

    if err := FastClient.DoTimeout(req, resp, ttl); err != nil {
        return err
    }

    dto.RespBody = string(resp.Body())

    if resp.StatusCode() != fasthttp.StatusOK || !gjson.Get(dto.RespBody, "success").Bool() {
        return errors.New(gjson.Get(dto.RespBody, "msg").String())
    }

    if dto.Resp != nil {
        helper.JsonUnmarshal(dto.RespBody, dto.Resp)
    }

    return nil
}

// 上传文件到服务端,link: 文件链接,objResp:结果结构体
func UploadFile(link string, objResp interface{}) error {
    // 下载文件
    _, stream, err := FastClient.Get(nil, link)
    if err != nil {
        return err
    }

    urlInfo, err := url.Parse(link)
    if err != nil {
        return err
    }

    filename := fmt.Sprintf("%s%s", uuid.New().String(), path.Ext(urlInfo.Path))

    httpReq := fasthttp.AcquireRequest()
    defer fasthttp.ReleaseRequest(httpReq)

    httpResp := fasthttp.AcquireResponse()
    defer fasthttp.ReleaseResponse(httpResp)

    //新建一个缓冲,用于存放文件内容
    bodyBufer := &bytes.Buffer{}
    //创建一个multipart文件写入器,方便按照http规定格式写入内容
    bodyWriter := multipart.NewWriter(bodyBufer)
    //从bodyWriter生成fileWriter,并将文件内容写入fileWriter,多个文件可进行多次
    fileWriter,err := bodyWriter.CreateFormFile("media", filename)
    if err != nil{
        fmt.Println(err.Error())
        return err
    }

    _, err = io.Copy(fileWriter, bytes.NewReader(stream))
    if err != nil{
        return err
    }

    // 加入其他普通参数
    // for k, v := range dto.ParamsStr {
    //     _ = bodyWriter.WriteField(k, v)
    // }

    // 停止写入
    _ = bodyWriter.Close()

    httpReq.SetRequestURI("https://qyapi.weixin.qq.com/cgi-bin/media/upload")
    httpReq.Header.SetContentType(bodyWriter.FormDataContentType())
    httpReq.SetBody(bodyBufer.Bytes())
    httpReq.Header.SetMethod(http.MethodPost)

    if err := FastClient.DoTimeout(httpReq, httpResp, HttpTTL); err != nil {
        return err
    }

    return json.Unmarshal(httpResp.Body(), &objResp)
}

什么是Keep-Alive模式

我们知道HTTP协议采用“请求-应答”模式,当使用普通模式,即非KeepAlive模式时,每个请求/应答客户和服务器都要新建一个连接,完成之后立即断开连接(HTTP协议为无连接的协议);当使用Keep-Alive模式(又称持久连接、连接重用)时,Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。

http 1.0中默认是关闭的,需要在http头加入"Connection: keep-alive",才能启用Keep-Alive;http 1.1中默认启用Keep-Alive,如果加入"Connection: close “,才关闭。目前大部分浏览器都是用http1.1协议,也就是说默认都会发起Keep-Alive的连接请求了,所以是否能完成一个完整的Keep-Alive连接就看服务器设置情况。

连接

决定着我们是不是要开启 KeepAlive 的因素是一个页面请求中是否会向会向服务器发出多个HTTP的请求。会则开启能够提高访问性能。