你使用的Go版本是什么(go version
)?
$ go1.21 version
go version go1.21.2 darwin/arm64
这个问题在最新版本的Go中是否重现?
是的,这个问题出现在Go 1.21系列和开发分支中。
你使用的操作系统和处理器架构是什么(go env
)?
go env
输出
$ go1.21 env
GO111MODULE=''
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/rhys/Library/Caches/go-build'
GOENV='/Users/rhys/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/rhys/go/pkg/mod'
GONOPROXY='*'
GONOSUMDB='*'
GOOS='darwin'
GOPATH='/Users/rhys/go'
GOPRIVATE='*'
GOPROXY='direct'
GOROOT='/Users/rhys/go/version/go1.21'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='local'
GOTOOLDIR='/Users/rhys/go/version/go1.21/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.21.2'
GCCGO='gccgo'
AR='ar'
CC='clang'
CXX='clang++'
CGO_ENABLED='1'
GOMOD='/dev/null'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/lf/n4ry0qv5639_3z0nhyhy3z7c0000gs/T/go-build3568422790=/tmp/go-build -gno-record-gcc-switches -fno-common'
你做了什么?
我在网络上进行HTTP请求,有时路由更改会导致TCP流从0%数据包丢失变为100%数据包丢失。
你期望看到什么?
我期望一个TCP流有100%数据包丢失时,不会被用于新的请求。
你看到了什么?
对于HTTP/1,每个连接一次只用于一个请求。连接上的完全数据包丢失会导致分配给它的请求(如果有的话)超时失败,或者分配给该连接的下一个请求失败。http.Transport池通过关闭连接来应对这种情况,防止它被用于任何其他请求。这是好的。
对于HTTP/2,每个连接可以同时用于多个并发请求。连接上的完全数据包丢失会导致分配给它的所有请求(如果有的话)超时失败。这是可以理解的,但如果客户端没有为打开的流设置用户定义的限制,那么似乎就无法避免这种情况。
但是继续使用HTTP/2,如果连接尚未达到其并发流的限制,http.Transport池将继续将新请求分配给该连接,即使它分配给该连接的其他请求未能导致在客户端接收到任何HTTP/2帧。如果我理解正确,Go httptest.Server允许每个连接最多250个并发流。当一个连接遇到数据包丢失时,成百上千的请求失败是不幸的,似乎是我们可以改进的地方。
最后,在HTTP/2中,当连接经历100%数据包丢失并且如我上面所述导致超时时,任何超时的请求都会停止计入连接的并发流限制。这导致死亡连接看起来有能力处理新的请求。这可能导致数千甚至数万个请求失败,特别是如果并发性从未超过http.Transport预期单个连接可以处理的数量。这可能会持续到操作系统的TCP连接缓冲区在OS中填满,或者直到操作系统超时该连接(这可能需要几分钟)。当一个连接遇到数据包丢失时,成千上万的请求超出了连接的最大流限制,这是我们应该能够解决的问题。
这些HTTP/1和Go的HTTP/2对网络扰动的React之间的巨大差异使得很难依赖Go的HTTP/2支持进行生产。
CC @neild
下面的复现器(折叠)结构化为一个无条件失败的测试以打印日志。它一次只发出一个请求,带有超时。每隔一段时间,它会“拔掉”池中的一个TCP连接以模拟(从Go的Angular 来看,如果不是操作系统的Angular )100%的数据包丢失。通常池中只有一个连接。
以下是在具有10ms超时的HTTP/1.1下进行30,000个请求以及在5000、15,000和25,000个请求后拔掉连接的样子:
$ go1.21 test ./h2_unplug_test.go -test.run=/http1 -request-timeout=10ms -request-count=30000 -unplug-interval=10000
--- FAIL: TestNetsplit (2.17s)
--- FAIL: TestNetsplit/http1 (2.17s)
h2_unplug_test.go:155: total=30000 unplugs=3 failures=3
FAIL
FAIL command-line-arguments 2.358s
FAIL
以下是在具有10ms超时的HTTP/2下进行30,000个请求以及在5000个请求后尝试拔掉连接以及在15,000和25,000个请求后尝试拔掉连接的样子(尽管没有新的连接可以拔掉)。它观察到了25,000个请求的失败:
$ go1.21 test ./h2_unplug_test.go -test.run=/http2 -request-timeout=10ms -request-count=30000 -unplug-interval=10000
--- FAIL: TestNetsplit (275.02s)
--- FAIL: TestNetsplit/http2 (275.02s)
h2_unplug_test.go:155: total=30000 unplugs=1 failures=25000
FAIL
FAIL command-line-arguments 275.201s
FAIL
./h2_unplug_test.go
package main
import (
"context"
"flag"
"io"
"math/rand"
"net"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
)
var (
requestTimeout = flag.Duration("request-timeout", 10*time.Millisecond, "Timeout for each HTTP request")
requestCount = flag.Int("request-count", 1000, "Number of HTTP requests to send")
unplugInterval = flag.Int("unplug-interval", 10, "Number of HTTP requests to issue between each unplug event")
)
func TestNetsplit(t *testing.T) {
testcase := func(
srvConfig func(srv *httptest.Server),
rtConfig func(rt *http.Transport),
test func(t *testing.T, url string, client *http.Client,
withConns func(fn func(conns []*proxyConn) []*proxyConn),
),
) func(t *testing.T) {
return func(t *testing.T) {
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
return
}))
srvConfig(srv)
srv.StartTLS()
defer srv.Close()
dialer := &net.Dialer{}
var (
pipeMu sync.Mutex
pipes []*proxyConn
)
var proxyMu sync.Mutex
proxy, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("proxy listen: %v", err)
}
defer proxy.Close()
connPair := func() (net.Conn, net.Conn, error) {
proxyMu.Lock()
defer proxyMu.Unlock()
c1, err := net.Dial(proxy.Addr().Network(), proxy.Addr().String())
if err != nil {
return nil, nil, err
}
c2, err := proxy.Accept()
if err != nil {
return nil, nil, err
}
return c1, c2, nil
}
transport := srv.Client().Transport.(*http.Transport).Clone()
transport.ForceAttemptHTTP2 = true
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
c3, err := dialer.DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
c1, c2, err := connPair()
if err != nil {
return nil, err
}
pipe := newProxyConn(c2, c3)
pipeMu.Lock()
pipes = append(pipes, pipe)
pipeMu.Unlock()
return c1, nil
}
client := &http.Client{Transport: transport}
withConns := func(fn func(conns []*proxyConn) []*proxyConn) {
pipeMu.Lock()
defer pipeMu.Unlock()
pipes = fn(pipes)
}
test(t, srv.URL, client, withConns)
}
}
makeRequest := func(ctx context.Context, url string, client *http.Client) error {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
_, err = io.Copy(io.Discard, resp.Body)
if err != nil {
return err
}
return nil
}
run := func(t *testing.T, url string, client *http.Client, withConns func(func(conns []*proxyConn) []*proxyConn)) {
var failures int64
var unplugs int64
total := *requestCount
for i := 0; i < total; i++ {
if interval := *unplugInterval; i%interval == interval/2 { // midway or a bit less
withConns(func(conns []*proxyConn) []*proxyConn {
if l := len(conns); l > 0 {
idx := rand.Intn(l)
pipe := conns[idx]
pipe.unplug()
unplugs++
conns[idx] = conns[l-1]
conns = conns[:l-1]
}
return conns
})
}
err := func() error {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, *requestTimeout)
defer cancel()
return makeRequest(ctx, url, client)
}()
if err != nil {
failures++
}
}
t.Errorf("total=%d unplugs=%d failures=%d", total, unplugs, failures)
}
t.Run("http1", testcase(
func(srv *httptest.Server) { srv.EnableHTTP2 = false },
func(rt *http.Transport) {},
run,
))
t.Run("http2", testcase(
func(srv *httptest.Server) { srv.EnableHTTP2 = true },
func(rt *http.Transport) {},
run,
))
}
// A proxyConn connects two net.Conns together, copying the data in each
// direction, until it is closed or unplugged. Unplugging a proxyConn causes it
// to silently stop copying the data.
type proxyConn struct {
c1 net.Conn
c2 net.Conn
once sync.Once
}
func newProxyConn(c1, c2 net.Conn) *proxyConn {
pc := &proxyConn{c1: c1, c2: c2}
go io.Copy(c1, c2)
go io.Copy(c2, c1)
return pc
}
func (pc *proxyConn) unplug() {
dl := time.Now().Add(-1 * time.Second)
pc.c1.SetDeadline(dl)
pc.c2.SetDeadline(dl)
}
func (pc *proxyConn) close() {
pc.once.Do(func() {
pc.c1.Close()
pc.c2.Close()
})
}
3条答案
按热度按时间sh7euo9m1#
CC @neild
3pmvbmvn2#
当你说“100%数据包丢失”时,你是通过DeadlineExceeded(和/或499)超时检测到的吗?
这是一个有趣的问题,我想知道其他语言是如何处理这个问题的。
0qx6xfy63#
这在应用程序中表现为超时/DeadlineExceeded,是的。(或者,如果最严格的超时来自较早的调用者层,则为Canceled。)
"100%数据包丢失"是TCP连接中导致内核没有/不提供我们期望可用的信息的问题。这种可观察行为的其他原因(例如另一端的应用程序决定休眠而不是写入我们的数据)也会对Go的HTTP/2支持的用户产生相同的影响。