使用 HTTP 连接池有诸多好处,在高频访问网站时,复用 TCP 连接可以节省资源,加速请求。
前两天同事分享了相关内容,本文章就当时 Leader 提出的问题进行验证。
Q:接口请求耗时 100ms,连接池里有 10 个连接,使用 HTTP 连接池一秒钟能有多少次请求?
TCP 短连接与长连接
这里有一个 Python 的简单示例,正常请求服务,每次请求后会断开 TCP 连接,而使用 Session 创建的长连接则会在与远端通信时复用 TCP 连接。
import requests
def stand_alone():
for i in range(3):
r = requests.get("http://192.168.3.111:8081/")
print(r.status_code)
def using_session():
s = requests.Session()
for i in range(3):
r = s.get('http://192.168.3.111:8081/')
print(r.status_code)
if __name__ == "__main__":
# stand_alone()
using_session()
短连接
观察 Wireshark 抓包数据,最左侧的框线选择的就是一次 TCP 请求的起止,另外数据中的 [SYN]
表示 TCP 握手请求,也有助于判断三次请求共三个 TCP 连接过程。
长连接
使用长连接建立一次 TCP 请求后,三次 HTTP 请求使用同一连接。
粗略的计算可知短连接请求三次网站数据耗时 1.24s,使用长连接只需 0.83s,节省了三分之一的时间,请求量的增加,将更加明显。
HTTP 连接池
以上通过 Python 示例,直观了解到短连接和长连接在网络传输的不同。
HTTP 连接池即基于长连接构建。
Golang 使用连接池
在 Golang 中,借助 http.Client
可以实现相同的效果。
req-client.go
package main
import (
"io/ioutil"
"fmt"
"net/http"
"time"
"io"
)
var c = &http.Client{Timeout: time.Duration(3) * time.Second}
func makeRequest() {
resp, err := c.Get("http://192.168.3.111:8081/")
if err != nil {
fmt.Printf("Error %s", err)
return
}
defer resp.Body.Close()
_, err = io.Copy(ioutil.Discard, resp.Body)
fmt.Printf("status_code: %d\n", resp.StatusCode)
}
func main() {
for i:=0; i<3; i++ {
makeRequest()
}
}
按照预期,请求复用了 TCP 连接
进一步观察图中 HTTP 请求,单次请求从 GET 发起请求到服务回复 200OK 正好间隔约 100ms
对于文章顶部的问题,似乎可以得出结论,在与服务器的 TCP 连接建立后,通过连接池访问网站,一秒钟能够请求 10 次。
协程请求试试
以上方式得出一个“结论”,但如果结合协程来看,又一个问题出现了—— “如果开多个协程请求,那么协程间复用同一个 TCP 连接还是自己用自己的呢?”
带着问题进行验证,以下是协程请求的代码
req-client-go.go
package main
import (
"sync"
"io/ioutil"
"fmt"
"net/http"
"time"
"io"
)
var c = http.Client{Timeout: time.Duration(3) * time.Second}
func makeRequest(wg *sync.WaitGroup) {
resp, err := c.Get("http://192.168.3.111:8081/")
if err != nil {
fmt.Printf("Error %s", err)
return
}
defer resp.Body.Close()
_, err = io.Copy(ioutil.Discard, resp.Body)
fmt.Printf("status_code: %d\n", resp.StatusCode)
defer wg.Done()
}
func main() {
var wg sync.WaitGroup
for i:=0;i<3;i++ {
wg.Add(1)
go makeRequest(&wg)
}
wg.Wait()
}
从请求结果看,每个协程在使用 HTTP 连接池的时候,连接池都创建了一条 TCP 连接。
跟之前的效果相同,使用 for
循环没开协程请求的例子,main 也只是使用了一个 TCP 连接。
那么,应该有什么参数能控制 HTTP 连接池的最大创建连接数量
参数初探
携带参数的初始化
var c = &http.Client{
Timeout: time.Duration(10) * time.Second,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
MaxIdleConns: 100, // 最大的空闲链接,设置为0 不限制
MaxIdleConnsPerHost: 10, // 单个 Host 最大的空闲链接,设置为 0 为 DefaultMaxIdleConnsPerHost
MaxConnsPerHost: 2, // 单个 Host 最大的链接限制,设置为 0 不限制
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
根据配置说明,当 MaxConnsPerHost 设置为 2 的时候,如果使用协程请求,那么相同的 IP+Port 只会创建两个 TCP 连接。
再次并发请求服务,只创建了两个 TCP 连接,符合预期。
在其中一个 TCP 连接上 “右键 - Follow - TCP Stream” 可以更为直观的查看到这个 TCP 连接包含两个 HTTP 请求。
结论更新
当不使用协程请求时,一秒钟可以发起 10 次请求。 当使用协程并发请求,一秒钟可以发起 100 次请求。
借助这个问题逐步的验证,可以直观了解到 HTTP 连接池的用处,以及在 Golang 中使用连接池的行为,小有收获。
另外,在 Golang 中要用好连接池,还需要对各项参数有更多的了解,本次的学习就先到这里。