TUN-3209: improve performance and reduce allocations during user header serialization from h1 to h2

benchmark                                    old ns/op     new ns/op     delta
BenchmarkH1ResponseToH2ResponseHeaders-4     10360         5048          -51.27%

benchmark                                    old allocs     new allocs     delta
BenchmarkH1ResponseToH2ResponseHeaders-4     135            26             -80.74%

benchmark                                    old bytes     new bytes     delta
BenchmarkH1ResponseToH2ResponseHeaders-4     8543          3667          -57.08%
This commit is contained in:
Igor Postelnik 2020-07-24 23:23:00 -05:00
parent 61d5461138
commit 44e3be2c88
2 changed files with 70 additions and 69 deletions

View File

@ -38,11 +38,12 @@ const (
// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3 // HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3
func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error { func H2RequestHeadersToH1Request(h2 []Header, h1 *http.Request) error {
for _, header := range h2 { for _, header := range h2 {
if !IsControlHeader(header.Name) { name := strings.ToLower(header.Name)
if !IsControlHeader(name) {
continue continue
} }
switch strings.ToLower(header.Name) { switch name {
case ":method": case ":method":
h1.Method = header.Value h1.Method = header.Value
case ":scheme": case ":scheme":
@ -116,18 +117,14 @@ func ParseUserHeaders(headerNameToParseFrom string, headers []Header) ([]Header,
} }
func IsControlHeader(headerName string) bool { func IsControlHeader(headerName string) bool {
headerName = strings.ToLower(headerName)
return headerName == "content-length" || return headerName == "content-length" ||
headerName == "connection" || headerName == "upgrade" || // Websocket headers headerName == "connection" || headerName == "upgrade" || // Websocket headers
strings.HasPrefix(headerName, ":") || strings.HasPrefix(headerName, ":") ||
strings.HasPrefix(headerName, "cf-") strings.HasPrefix(headerName, "cf-")
} }
// IsWebsocketClientHeader returns true if the header name is required by the client to upgrade properly // isWebsocketClientHeader returns true if the header name is required by the client to upgrade properly
func IsWebsocketClientHeader(headerName string) bool { func isWebsocketClientHeader(headerName string) bool {
headerName = strings.ToLower(headerName)
return headerName == "sec-websocket-accept" || return headerName == "sec-websocket-accept" ||
headerName == "connection" || headerName == "connection" ||
headerName == "upgrade" headerName == "upgrade"
@ -137,29 +134,24 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
h2 = []Header{ h2 = []Header{
{Name: ":status", Value: strconv.Itoa(h1.StatusCode)}, {Name: ":status", Value: strconv.Itoa(h1.StatusCode)},
} }
userHeaders := http.Header{} userHeaders := make(http.Header, len(h1.Header))
for header, values := range h1.Header { for header, values := range h1.Header {
for _, value := range values { h2name := strings.ToLower(header)
if strings.ToLower(header) == "content-length" { if h2name == "content-length" {
// This header has meaning in HTTP/2 and will be used by the edge, // This header has meaning in HTTP/2 and will be used by the edge,
// so it should be sent as an HTTP/2 response header. // so it should be sent as an HTTP/2 response header.
// Since these are http2 headers, they're required to be lowercase // Since these are http2 headers, they're required to be lowercase
h2 = append(h2, Header{Name: strings.ToLower(header), Value: value}) h2 = append(h2, Header{Name: "content-length", Value: values[0]})
} else if !IsControlHeader(header) || IsWebsocketClientHeader(header) { } else if !IsControlHeader(h2name) || isWebsocketClientHeader(h2name) {
// User headers, on the other hand, must all be serialized so that // User headers, on the other hand, must all be serialized so that
// HTTP/2 header validation won't be applied to HTTP/1 header values // HTTP/2 header validation won't be applied to HTTP/1 header values
if _, ok := userHeaders[header]; ok { userHeaders[header] = values
userHeaders[header] = append(userHeaders[header], value)
} else {
userHeaders[header] = []string{value}
}
}
} }
} }
// Perform user header serialization and set them in the single header // Perform user header serialization and set them in the single header
h2 = append(h2, CreateSerializedHeaders(ResponseUserHeadersField, userHeaders)...) h2 = append(h2, Header{ResponseUserHeadersField, SerializeHeaders(userHeaders)})
return h2 return h2
} }
@ -167,26 +159,48 @@ func H1ResponseToH2ResponseHeaders(h1 *http.Response) (h2 []Header) {
// Serialize HTTP1.x headers by base64-encoding each header name and value, // Serialize HTTP1.x headers by base64-encoding each header name and value,
// and then joining them in the format of [key:value;] // and then joining them in the format of [key:value;]
func SerializeHeaders(h1Headers http.Header) string { func SerializeHeaders(h1Headers http.Header) string {
var serializedHeaders []string // compute size of the fully serialized value and largest temp buffer we will need
serializedLen := 0
maxTempLen := 0
for headerName, headerValues := range h1Headers { for headerName, headerValues := range h1Headers {
for _, headerValue := range headerValues { for _, headerValue := range headerValues {
encodedName := make([]byte, headerEncoding.EncodedLen(len(headerName))) nameLen := headerEncoding.EncodedLen(len(headerName))
headerEncoding.Encode(encodedName, []byte(headerName)) valueLen := headerEncoding.EncodedLen(len(headerValue))
const delims = 2
serializedLen += delims + nameLen + valueLen
if nameLen > maxTempLen {
maxTempLen = nameLen
}
if valueLen > maxTempLen {
maxTempLen = valueLen
}
}
}
var buf strings.Builder
buf.Grow(serializedLen)
encodedValue := make([]byte, headerEncoding.EncodedLen(len(headerValue))) temp := make([]byte, maxTempLen)
headerEncoding.Encode(encodedValue, []byte(headerValue)) writeB64 := func(s string) {
n := headerEncoding.EncodedLen(len(s))
if n > len(temp) {
temp = make([]byte, n)
}
headerEncoding.Encode(temp[:n], []byte(s))
buf.Write(temp[:n])
}
serializedHeaders = append( for headerName, headerValues := range h1Headers {
serializedHeaders, for _, headerValue := range headerValues {
strings.Join( if buf.Len() > 0 {
[]string{string(encodedName), string(encodedValue)}, buf.WriteByte(';')
":", }
), writeB64(headerName)
) buf.WriteByte(':')
writeB64(headerValue)
} }
} }
return strings.Join(serializedHeaders, ";") return buf.String()
} }
// Deserialize headers serialized by `SerializeHeader` // Deserialize headers serialized by `SerializeHeader`
@ -225,18 +239,6 @@ func DeserializeHeaders(serializedHeaders string) ([]Header, error) {
return deserialized, nil return deserialized, nil
} }
func CreateSerializedHeaders(headersField string, headers ...http.Header) []Header {
var serializedHeaderChunks []string
for _, headerChunk := range headers {
serializedHeaderChunks = append(serializedHeaderChunks, SerializeHeaders(headerChunk))
}
return []Header{{
headersField,
strings.Join(serializedHeaderChunks, ";"),
}}
}
type ResponseMetaHeader struct { type ResponseMetaHeader struct {
Source string `json:"src"` Source string `json:"src"`
} }

View File

@ -37,12 +37,19 @@ func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) {
"Mock header 2": {"Mock value 2"}, "Mock header 2": {"Mock value 2"},
} }
headersConversionErr := H2RequestHeadersToH1Request(CreateSerializedHeaders(RequestUserHeadersField, mockHeaders), request) headersConversionErr := H2RequestHeadersToH1Request(createSerializedHeaders(RequestUserHeadersField, mockHeaders), request)
assert.True(t, reflect.DeepEqual(mockHeaders, request.Header)) assert.True(t, reflect.DeepEqual(mockHeaders, request.Header))
assert.NoError(t, headersConversionErr) assert.NoError(t, headersConversionErr)
} }
func createSerializedHeaders(headersField string, headers http.Header) []Header {
return []Header{{
headersField,
SerializeHeaders(headers),
}}
}
func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) { func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) {
request, err := http.NewRequest(http.MethodGet, "http://example.com", nil) request, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -509,40 +516,32 @@ func TestParseHeaders(t *testing.T) {
} }
mockHeaders := []Header{ mockHeaders := []Header{
{Name: "One", Value: "1"}, {Name: "One", Value: "1"}, // will be dropped
{Name: "Cf-Two", Value: "cf-value-1"}, {Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"}, {Name: "Cf-Two", Value: "cf-value-2"},
{Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)}, {Name: RequestUserHeadersField, Value: SerializeHeaders(mockUserHeadersToSerialize)},
} }
expectedHeaders := []Header{ expectedHeaders := []Header{
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
{Name: "Mock-Header-One", Value: "1"}, {Name: "Mock-Header-One", Value: "1"},
{Name: "Mock-Header-One", Value: "1.5"}, {Name: "Mock-Header-One", Value: "1.5"},
{Name: "Mock-Header-Two", Value: "2"}, {Name: "Mock-Header-Two", Value: "2"},
{Name: "Mock-Header-Three", Value: "3"}, {Name: "Mock-Header-Three", Value: "3"},
} }
parsedHeaders, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders) h1 := &http.Request{
assert.NoError(t, err) Header: make(http.Header),
assert.ElementsMatch(t, expectedHeaders, parsedHeaders)
}
func TestParseHeadersNoSerializedHeader(t *testing.T) {
mockHeaders := []Header{
{Name: "One", Value: "1"},
{Name: "Cf-Two", Value: "cf-value-1"},
{Name: "Cf-Two", Value: "cf-value-2"},
} }
err := H2RequestHeadersToH1Request(mockHeaders, h1)
_, err := ParseUserHeaders(RequestUserHeadersField, mockHeaders) assert.NoError(t, err)
assert.EqualError(t, err, fmt.Sprintf("%s header not found", RequestUserHeadersField)) assert.ElementsMatch(t, expectedHeaders, stdlibHeaderToH2muxHeader(h1.Header))
} }
func TestIsControlHeader(t *testing.T) { func TestIsControlHeader(t *testing.T) {
controlHeaders := []string{ controlHeaders := []string{
// Anything that begins with cf- // Anything that begins with cf-
"cf-sample-header", "cf-sample-header",
"CF-SAMPLE-HEADER",
"Cf-Sample-Header",
// Any http2 pseudoheader // Any http2 pseudoheader
":sample-pseudo-header", ":sample-pseudo-header",
@ -559,8 +558,8 @@ func TestIsControlHeader(t *testing.T) {
func TestIsNotControlHeader(t *testing.T) { func TestIsNotControlHeader(t *testing.T) {
notControlHeaders := []string{ notControlHeaders := []string{
"Mock-header", "mock-header",
"Another-sample-header", "another-sample-header",
} }
for _, header := range notControlHeaders { for _, header := range notControlHeaders {