Skip to content

Commit 8c18a0d

Browse files
authored
netty: use custom http2 headers for decoding.
The DefaultHttp2Headers class is a general-purpose Http2Headers implementation and provides much more functionality than we need in gRPC. In gRPC, when reading headers off the wire, we only inspect a handful of them, before converting to Metadata. This commit introduces a Http2Headers implementation that aims for insertion efficiency, a low memory footprint and fast conversion to Metadata. - Header names and values are stored in plain byte[]. - Insertion is O(1), while lookup is now O(n). - Binary header values are base64 decoded as they are inserted. - The byte[][] returned by namesAndValues() can directly be used to construct a new Metadata object. - For HTTP/2 request headers, the pseudo headers are no longer carried over to Metadata. A microbenchmark aiming to replicate the usage of Http2Headers in NettyClientHandler and NettyServerHandler shows decent throughput gains when compared to DefaultHttp2Headers. Benchmark Mode Cnt Score Error Units InboundHeadersBenchmark.defaultHeaders_clientHandler avgt 10 283.830 ± 4.063 ns/op InboundHeadersBenchmark.defaultHeaders_serverHandler avgt 10 1179.975 ± 21.810 ns/op InboundHeadersBenchmark.grpcHeaders_clientHandler avgt 10 190.108 ± 3.510 ns/op InboundHeadersBenchmark.grpcHeaders_serverHandler avgt 10 561.426 ± 9.079 ns/op Additionally, the memory footprint is reduced by more than 50%! gRPC Request Headers: 864 bytes Netty Request Headers: 1728 bytes gRPC Response Headers: 216 bytes Netty Response Headers: 528 bytes Furthermore, this change does most of the gRPC groundwork necessary to be able to cache higher ordered objects in HPACK's dynamic table, as discussed in [1]. [1] #2217
1 parent de9c320 commit 8c18a0d

File tree

13 files changed

+961
-28
lines changed

13 files changed

+961
-28
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/*
2+
* Copyright 2016, Google Inc. All rights reserved.
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google Inc. nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
package io.grpc.netty;
33+
34+
import static io.grpc.netty.Utils.CONTENT_TYPE_HEADER;
35+
import static io.grpc.netty.Utils.TE_TRAILERS;
36+
import static io.netty.util.AsciiString.of;
37+
38+
import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2RequestHeaders;
39+
import io.grpc.netty.GrpcHttp2HeadersDecoder.GrpcHttp2ResponseHeaders;
40+
import io.netty.handler.codec.http2.DefaultHttp2Headers;
41+
import io.netty.handler.codec.http2.Http2Headers;
42+
import io.netty.util.AsciiString;
43+
import org.openjdk.jmh.annotations.Benchmark;
44+
import org.openjdk.jmh.annotations.BenchmarkMode;
45+
import org.openjdk.jmh.annotations.CompilerControl;
46+
import org.openjdk.jmh.annotations.Mode;
47+
import org.openjdk.jmh.annotations.OutputTimeUnit;
48+
import org.openjdk.jmh.annotations.Scope;
49+
import org.openjdk.jmh.annotations.State;
50+
import org.openjdk.jmh.infra.Blackhole;
51+
52+
import java.util.concurrent.TimeUnit;
53+
54+
/**
55+
* Benchmarks for {@link GrpcHttp2RequestHeaders} and {@link GrpcHttp2ResponseHeaders}.
56+
*/
57+
@State(Scope.Thread)
58+
public class InboundHeadersBenchmark {
59+
60+
private static AsciiString[] requestHeaders;
61+
private static AsciiString[] responseHeaders;
62+
63+
static {
64+
setupRequestHeaders();
65+
setupResponseHeaders();
66+
}
67+
68+
// Headers taken from the gRPC spec.
69+
private static void setupRequestHeaders() {
70+
requestHeaders = new AsciiString[18];
71+
int i = 0;
72+
requestHeaders[i++] = of(":method");
73+
requestHeaders[i++] = of("POST");
74+
requestHeaders[i++] = of(":scheme");
75+
requestHeaders[i++] = of("http");
76+
requestHeaders[i++] = of(":path");
77+
requestHeaders[i++] = of("/google.pubsub.v2.PublisherService/CreateTopic");
78+
requestHeaders[i++] = of(":authority");
79+
requestHeaders[i++] = of("pubsub.googleapis.com");
80+
requestHeaders[i++] = of("te");
81+
requestHeaders[i++] = of("trailers");
82+
requestHeaders[i++] = of("grpc-timeout");
83+
requestHeaders[i++] = of("1S");
84+
requestHeaders[i++] = of("content-type");
85+
requestHeaders[i++] = of("application/grpc+proto");
86+
requestHeaders[i++] = of("grpc-encoding");
87+
requestHeaders[i++] = of("gzip");
88+
requestHeaders[i++] = of("authorization");
89+
requestHeaders[i] = of("Bearer y235.wef315yfh138vh31hv93hv8h3v");
90+
}
91+
92+
private static void setupResponseHeaders() {
93+
responseHeaders = new AsciiString[4];
94+
int i = 0;
95+
responseHeaders[i++] = of(":status");
96+
responseHeaders[i++] = of("200");
97+
responseHeaders[i++] = of("grpc-encoding");
98+
responseHeaders[i] = of("gzip");
99+
}
100+
101+
/**
102+
* Checkstyle.
103+
*/
104+
@Benchmark
105+
@BenchmarkMode(Mode.AverageTime)
106+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
107+
public void grpcHeaders_serverHandler(Blackhole bh) {
108+
serverHandler(bh, new GrpcHttp2RequestHeaders(4));
109+
}
110+
111+
/**
112+
* Checkstyle.
113+
*/
114+
@Benchmark
115+
@BenchmarkMode(Mode.AverageTime)
116+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
117+
public void defaultHeaders_serverHandler(Blackhole bh) {
118+
serverHandler(bh, new DefaultHttp2Headers(true, 9));
119+
}
120+
121+
/**
122+
* Checkstyle.
123+
*/
124+
@Benchmark
125+
@BenchmarkMode(Mode.AverageTime)
126+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
127+
public void grpcHeaders_clientHandler(Blackhole bh) {
128+
clientHandler(bh, new GrpcHttp2ResponseHeaders(2));
129+
}
130+
131+
/**
132+
* Checkstyle.
133+
*/
134+
@Benchmark
135+
@BenchmarkMode(Mode.AverageTime)
136+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
137+
public void defaultHeaders_clientHandler(Blackhole bh) {
138+
clientHandler(bh, new DefaultHttp2Headers(true, 2));
139+
}
140+
141+
@CompilerControl(CompilerControl.Mode.INLINE)
142+
private static void serverHandler(Blackhole bh, Http2Headers headers) {
143+
for (int i = 0; i < requestHeaders.length; i += 2) {
144+
bh.consume(headers.add(requestHeaders[i], requestHeaders[i + 1]));
145+
}
146+
147+
// Sequence of headers accessed in NettyServerHandler
148+
bh.consume(headers.get(TE_TRAILERS));
149+
bh.consume(headers.get(CONTENT_TYPE_HEADER));
150+
bh.consume(headers.method());
151+
bh.consume(headers.get(CONTENT_TYPE_HEADER));
152+
bh.consume(headers.path());
153+
154+
bh.consume(Utils.convertHeaders(headers));
155+
}
156+
157+
@CompilerControl(CompilerControl.Mode.INLINE)
158+
private static void clientHandler(Blackhole bh, Http2Headers headers) {
159+
// NettyClientHandler does not directly access headers, but convert to Metadata immediately.
160+
161+
bh.consume(headers.add(responseHeaders[0], responseHeaders[1]));
162+
bh.consume(headers.add(responseHeaders[2], responseHeaders[3]));
163+
164+
bh.consume(Utils.convertHeaders(headers));
165+
}
166+
167+
// /**
168+
// * Prints the size of the header objects in bytes. Needs JOL (Java Object Layout) as a
169+
// * dependency.
170+
// */
171+
// public static void main(String... args) {
172+
// Http2Headers grpcRequestHeaders = new GrpcHttp2RequestHeaders(4);
173+
// Http2Headers defaultRequestHeaders = new DefaultHttp2Headers(true, 9);
174+
// for (int i = 0; i < requestHeaders.length; i += 2) {
175+
// grpcRequestHeaders.add(requestHeaders[i], requestHeaders[i + 1]);
176+
// defaultRequestHeaders.add(requestHeaders[i], requestHeaders[i + 1]);
177+
// }
178+
// long c = 10L;
179+
// int m = ((int) c) / 20;
180+
//
181+
// long grpcRequestHeadersBytes = GraphLayout.parseInstance(grpcRequestHeaders).totalSize();
182+
// long defaultRequestHeadersBytes =
183+
// GraphLayout.parseInstance(defaultRequestHeaders).totalSize();
184+
//
185+
// System.out.printf("gRPC Request Headers: %d bytes%nNetty Request Headers: %d bytes%n",
186+
// grpcRequestHeadersBytes, defaultRequestHeadersBytes);
187+
//
188+
// Http2Headers grpcResponseHeaders = new GrpcHttp2RequestHeaders(4);
189+
// Http2Headers defaultResponseHeaders = new DefaultHttp2Headers(true, 9);
190+
// for (int i = 0; i < responseHeaders.length; i += 2) {
191+
// grpcResponseHeaders.add(responseHeaders[i], responseHeaders[i + 1]);
192+
// defaultResponseHeaders.add(responseHeaders[i], responseHeaders[i + 1]);
193+
// }
194+
//
195+
// long grpcResponseHeadersBytes = GraphLayout.parseInstance(grpcResponseHeaders).totalSize();
196+
// long defaultResponseHeadersBytes =
197+
// GraphLayout.parseInstance(defaultResponseHeaders).totalSize();
198+
//
199+
// System.out.printf("gRPC Response Headers: %d bytes%nNetty Response Headers: %d bytes%n",
200+
// grpcResponseHeadersBytes, defaultResponseHeadersBytes);
201+
// }
202+
}

benchmarks/src/jmh/java/io/grpc/netty/HeadersBenchmark.java renamed to benchmarks/src/jmh/java/io/grpc/netty/OutboundHeadersBenchmark.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
* Header encoding benchmark.
5757
*/
5858
@State(Scope.Benchmark)
59-
public class HeadersBenchmark {
59+
public class OutboundHeadersBenchmark {
6060
@Param({"1", "5", "10", "20"})
6161
public int headerCount;
6262

core/src/main/java/io/grpc/Metadata.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ public Metadata(byte[]... binaryValues) {
147147
checkArgument(binaryValues.length % 2 == 0,
148148
"Odd number of key-value pairs: %s", binaryValues.length);
149149
for (int i = 0; i < binaryValues.length; i += 2) {
150+
// The transport might provide an array with null values at the end.
151+
if (binaryValues[i] == null) {
152+
break;
153+
}
150154
String name = new String(binaryValues[i], US_ASCII);
151155
storeAdd(name, new MetadataEntry(name.endsWith(BINARY_HEADER_SUFFIX), binaryValues[i + 1]));
152156
}

0 commit comments

Comments
 (0)