Efficient HTTP Apis

4,887 views

Published on

Developers choose HTTP for its ubiquity. HTTP's semantics are cherry-picked or embraced in the myriad of apis we develop and consume. Efficiency discussions are commonplace: Does this design imply N+1 requests? Should we denormalize the model? How do consumers discover changes in state? How many connections are needed to effectively use this api?
Meanwhile, HTTP 1.1 is a choice, as opposed to constant. SPDY and HTTP/2 implementations surface, simultaneously retaining semantics and dramatically changing performance implications. We can choose treat these new protocols as more efficient versions HTTP 1.1 or buy into new patterns such as server-side push.
This session walks you through these topics via an open source project from Square called okhttp. You'll understand how okhttp addresses portability so that you can develop against something as familiar as java's HTTPUrlConnection. We'll review how to use new protocol features and constraints to keep in mind along the way. You'll learn how to sandbox ideas with okhttp's mock server, so that you can begin experimenting with SPDY and HTTP/2 today!

Published in: Technology, Design
0 Comments
23 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
4,887
On SlideShare
0
From Embeds
0
Number of Embeds
300
Actions
Shares
0
Downloads
59
Comments
0
Likes
23
Embeds 0
No embeds

No notes for slide

Efficient HTTP Apis

  1. 1. Efficient HTTP Apis A walk through http/2 via okhttp @adrianfcole
  2. 2. introduction hello http api! hello okhttp! uh oh.. scale! hello http/2! wrapping up
  3. 3. introduction hello http api! hello okhttp! uh oh.. scale! hello http/2! wrapping up
  4. 4. * Can be blamed for the http/2 defects in okhttp! @adrianfcole • engineer at Pivotal on Spring Cloud • lots of open source work • focus on cloud computing
  5. 5. okhttp • HttpURLConnection Apache HttpClient compatible • designed for java and android • BDFL: Jesse Wilson from square https://github.com/square/okhttp
  6. 6. introduction hello http api! hello okhttp! uh oh.. scale! hello http/2! wrapping up
  7. 7. $ curl https://apihost/things -H ‘SecurityToken: b08c85073c1a2d02’ -H ‘Accept: application/json’ [ { "id": 1, "owner_id": 0, "name": "Able" }, ... { "id": 26, "owner_id": 0, $ curl https://apihost/things/2 -H ‘SecurityToken: b08c85073c1a2d02’ -H ‘Accept: application/json’ { "id": 2, "owner_id": 0, "name": "Beatific" } $ curl -X POST https://apihost/things -H ‘SecurityToken: b08c85073c1a2d02’ -H ‘Content-Type: application/json’ -d ’{ "name": "Open-minded" }’ $ curl -X DELETE https://apihost/things/2 -H ‘SecurityToken: b08c85073c1a2d02’ json apis are so simple
  8. 8. $ curl https://apihost/things -H ‘SecurityToken: b08c85073c1a2d02’ -H ‘Accept: application/json’ [ { "id": 1, "owner_id": 0, "name": "Able" }, ... { "id": 26, "owner_id": 0, "name": "Zest" } $ curl https://apihost/things/2 -H ‘SecurityToken: b08c85073c1a2d02’ -H ‘Accept: application/json’ { "id": 2, "owner_id": 0, "name": "Beatific" } $ curl -X POST https://apihost/things -H ‘SecurityToken: b08c85073c1a2d02’ -H ‘Content-Type: application/json’ -d ’{ "name": "Open-minded" }’ $ curl -X DELETE https://apihost/things/2 -H ‘SecurityToken: b08c85073c1a2d02’ so many bytes?! 1642!
  9. 9. $ curl --compress https://apihost/things -H ‘SecurityToken: b08c85073c1a2d02’ -H ‘Accept: application/json’ -H ‘Accept-encoding: gzip, deflate’ [ { "id": 1, "owner_id": 0, "name": "Able" }, ... { "id": 26, "owner_id": 0, "name": "Zest" $ curl https://apihost/things/2 -H ‘SecurityToken: b08c85073c1a2d02’ -H ‘Accept: application/json’ { "id": 2, "owner_id": 0, "name": "Beatific" } $ curl -X POST https://apihost/things -H ‘SecurityToken: b08c85073c1a2d02’ -H ‘Content-Type: application/json’ -d ’{ "name": "Open-minded" }’ $ curl -X DELETE https://apihost/things/2 -H ‘SecurityToken: b08c85073c1a2d02’ now with gzip 318
  10. 10. java + gson url = new URL(“https://apihost/things”); connection = (HttpURLConnection) url.openConnection(); connection.setRequestProperty(“SecurityToken”, “b08c85073c1a2d02”); connection.setRequestProperty(“Accept”, “application/json”); connection.setRequestProperty(“Accept-Encoding”, "gzip, deflate"); is = connection.getInputStream(); isr = new InputStreamReader(new GZIPInputStream(is)); things = new Gson().fromJson(isr, new TypeToken<List<Thing>>(){});
  11. 11. okhttp + gson client = new OkHttpClient(); url = new URL(“https://apihost/things”); connection = new OkUrlFactory(client).open(url); connection.setRequestProperty(“SecurityToken”, “b08c85073c1a2d02”); connection.setRequestProperty(“Accept”, “application/json”); connection.setRequestProperty(“Accept-Encoding”, "gzip, deflate"); is = connection.getInputStream(); isr = new InputStreamReader(is); // automatic gunzip things = new Gson().fromJson(isr, new TypeToken<List<Thing>>(){}); is.close();
  12. 12. We won! • List body reduced from 1642 to 318 bytes! • We saved some lines using OkHttp • Concession: cpu for compression, curl is a little verbose.
  13. 13. introduction hello http api! hello okhttp! uh oh.. scale! hello http/2! wrapping up
  14. 14. okhttp • v2.5 has lots of stuff since v1 • buffer, call, cache, interceptor, url • http/2 and WebSocket support • Apache adapter https://github.com/square/okhttp
  15. 15. okhttp client = new OkHttpClient(); request = new Request.Builder() .url(“https://apihost/things”) .header(“SecurityToken”, “b08c85073c1a2d02”) .header(“Accept”, “application/json”) .header(“Accept-Encoding”, "gzip, deflate”).build(); // ^^ look, ma.. I’m immutable! reader = client.newCall(request).execute().body().charStream(); things = new Gson().fromJson(reader, new TypeToken<List<Thing>>(){}); • OkHttp has its own api, which has a concise way to get bytes or chars (via okio).
  16. 16. okhttp call client = new OkHttpClient(); request = new Request.Builder() .url(“https://apihost/things”) .header(“SecurityToken”, “b08c85073c1a2d02”) .header(“Accept”, “application/json”) .header(“Accept-Encoding”, "gzip, deflate").build(); call = client.newCall(request); call.enqueue(new ParseThingsCallback()); call.cancel(); // changed my mind • OkHttp also has an async api, which (also) supports cancel!
  17. 17. • # Explicity trust certs for your properties • new CertificatePinner.Builder() • .add(“my.biz”, publicKeySHA1) • .add(“*.my.biz”, publicKeySHA1) • .build() • # Respond to authentication challenges • new Authenticator() { • public Request authenticate(Proxy p, Response rsp) { • return rsp.request().newBuilder() • .header(“Authorization", “Bearer …”) • # Set your own floor for TLS • new ConnectionSpec.Builder(MODERN_TLS) okhttp security
  18. 18. okhttp interceptor client = new OkHttpClient(); client.interceptors().add((chain) ->
 chain.proceed(chain.request().newBuilder()
 .addHeader("SecurityToken", securityToken.get())
 .build())
 ); request = new Request.Builder() .url(“https://apihost/things”) .header(“Accept-Encoding”, "gzip, deflate").build(); call = client.newCall(request); call.enqueue(new ParseThingsCallback()); • Now you can change your security token!
  19. 19. okio • ByteString • Unbounded Buffer • Source/Sink abstraction https://github.com/square/okio
  20. 20. // Okio is (more than) a prettier DataInputStream BufferedSink sink = Okio.buffer(Okio.sink(file)); sink.writeUtf8("Hello, java.io file!”); sink.writeLong(1000000000000000L); sink.close(); // Streaming writes with no housekeeping new RequestBody() { @Override public void writeTo(BufferedSink sink) throws IOException { sink.writeUtf8("Numbersn"); sink.writeUtf8("-------n"); for (int i = 2; i <= 997; i++) { sink.writeUtf8(String.format(" * %s = %sn", i, factor(i))); } } —snip— // Lazy, composable bodies new MultipartBuilder("123") .addPart(RequestBody.create(MediaType.parse(“text/plain”, "Quick")) .addPart(new StreamingBody("Brown")) okio samples
  21. 21. samples • Starters like web crawler • How-to guide w/ recipes https://github.com/square/okhttp/tree/master/samples
  22. 22. introduction hello http api! hello okhttp! uh oh.. scale! hello http/2! wrapping up
  23. 23. Hey.. List #1 is slow! 368!
  24. 24. Ask Ilya why! • TCP connections need 3- way handshake. • TLS requires up to 2 more round-trips. • Read High Performance Browser Networking http://chimera.labs.oreilly.com/books/1230000000545
  25. 25. HttpUrlConnection • http.keepAlive - should connections should be pooled at all? Default is true. • http.maxConnections - maximum number of idle connections to each host in the pool. Default is 5. • get[Input|Error]Stream().close() - recycles the connection, if fully read. • disconnect() - removes the connection.
  26. 26. Don’t forget to read! ... is = tryGetInputStream(connection); isr = new InputStreamReader(is); things = new Gson().fromJson(isr, new TypeToken<List<Thing>>(){}); ... InputStream tryGetInputStream(HttpUrlConnection connection) throws IOException { try { return connection.getInputStream(); } catch (IOException e) { InputStream err = connection.getErrorStream();   while (in.read() != -1); // skip err.close(); throw e; }
  27. 27. or just use okhttp try (ResponseBody body = client.newCall(request).execute().body()) { // connection will be cleaned on close. } client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Request request, IOException e) { // body is implicitly closed on failure } @Override public void onResponse(Response response) { // make sure you call response.body().close() when done } });
  28. 28. We won! • Recycled socket requests are much faster and have less impact on the server. • Concessions: must read responses, concurrency is still bounded by sockets.
  29. 29. Let’s make List #2 free Cache-Control: private, max-age=60, s-maxage=0 Vary: SecurityToken Step 1: add a little magic to your server response Step 2: make sure you are using a cache! client.setCache(new Cache(dir, 10_MB)); Step 3: pray your developers don’t get clever // TODO: stupid server sends stale data without this new Request.Builder().url(”https://apihost/things?time=” + currentTimeMillis());
  30. 30. // Limit cache duration thisMessageWillSelfDestruct = new Request.Builder() .url(“https://foo.com/weather”) .cacheControl(new CacheControl.Builder().maxAge(12, HOURS).build()) .build(); // Opt-out of response caching notStored = new Request.Builder() .url(“https://foo.com/private”) .cacheControl(CacheControl.FORCE_NETWORK) .build(); // Offline mode offline = new Request.Builder() .url(“https://foo.com/image”) .cacheControl(CacheControl.FORCE_CACHE) .build(); okhttp cache recipes
  31. 31. We won again! • No time or bandwidth used for cached responses • No application-specific cache bugs code. • Concessions: only supports “safe methods”, caching needs to be tuned.
  32. 32. introduction hello http api! hello okhttp! uh oh.. scale! hello http/2! wrapping up
  33. 33. http/1.1 • rfc2616 - June 1999 • text-based framing • defined semantics of the web http://www.w3.org/Protocols/rfc2616/rfc2616.html
  34. 34. FYI: RFC 2616 is dead! • RFC 7230-5 replaces RFC 2616. • Checkout details on www.mnot.net/blog
  35. 35. spdy/3.1 http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1 • google - Sep 2013 • binary framing • retains http/1.1 semantics • concurrent multiplexed streams
  36. 36. http/2 https://tools.ietf.org/html/rfc7540 • rfc7540 - May 2015 • binary framing • retains http/1.1 semantics • concurrent multiplexed streams
  37. 37. multiplexing header compression flow control priority server push http/2 headline features
  38. 38. multiplexing header compression server push http/2 headline features
  39. 39. Looking at the whole message Request: request line, headers, and body Response: status line, headers, and body
  40. 40. http/1.1 round-trip GZIPPED DATA Content-Length: 318 Cache-Control: private, max-age=60, s- maxage=0 Vary: SecurityToken Date: Sun, 02 Feb 2014 20:30:38 GMT Content-Type: application/json Content-Encoding: gzip Host: apihost SecurityToken: b08c85073c1a2d02 Accept: application/json Accept-encoding: gzip, deflate GET /things HTTP/1.1 HTTP/1.1 200 OK • http/1.1 sends requests and responses one at a time over a network connection
  41. 41. http/2 round-trip GZIPPED DATA :status: 200 content-length: 318 cache-control: private, max-age=60, s- maxage=0 vary: SecurityToken date: Sun, 02 Feb 2014 20:30:38 GMT content-type: application/json :method: GET :authority: apihost :path: /things securitytoken: b08c85073c1a2d02 accept: application/json accept-encoding: gzip, deflate HEADERS Stream: 3 Flags: END_HEADERS, END_STREAM HEADERS Stream: 3 Flags: END_HEADERS DATA Stream: 3 Flags: END_STREAM • http/2 breaks up messages into stream-scoped frames
  42. 42. Interleaving HEADERS Stream: 5 Flags: END_HEADERS, END_STREAM HEADERS Stream: 3 Flags: END_HEADERS DATA Stream: 5 Flags: DATA Stream: 5 Flags: END_STREAM HEADERS Stream: 3 Flags: END_HEADERS, END_STREAM HEADERS Stream: 5 Flags: END_HEADERS DATA Stream: 3 Flags: END_STREAM • http/2 multiplexes a network connection by interleaving request/response frames
  43. 43. Canceling a Stream HEADERS Stream: 5 Flags: END_HEADERS, END_STREAM HEADERS Stream: 3 Flags: END_HEADERS DATA Stream: 5 Flags: HEADERS Stream: 3 Flags: END_HEADERS, END_STREAM HEADERS Stream: 5 Flags: END_HEADERS DATA Stream: 3 Flags: END_STREAM RST_STREAM Stream: 5 ErrorCode: CANCEL • interrupts a response without affecting the connection
  44. 44. control frames HEADERS Stream: 5 HEADERS Stream: 3 DATA Stream: 5 DATA Stream: 3 HEADERS Stream: 3 HEADERS Stream: 5 SETTINGS Stream: 0 SETTINGS Stream: 0 DATA Stream: 5 • connection-scoped frames allow runtime updates, like flow-control
  45. 45. OkHttp/2 Architecture Frame Reader Thread Frame Writer Lock Control Frame Queue FramedConnection
  46. 46. multiplexing header compression server push http/2 headline features
  47. 47. http/1.1 headers GZIPPED DATA Content-Length: 318 Cache-Control: private, max-age=60, s- maxage=0 Vary: SecurityToken Date: Sun, 02 Feb 2014 20:30:38 GMT Content-Type: application/json Content-Encoding: gzip Host: apihost SecurityToken: b08c85073c1a2d02 Accept: application/json Accept-encoding: gzip, deflate GET /things HTTP/1.1 HTTP/1.1 200 OK 168! 195! 318 • You can gzip data, but not headers!
  48. 48. header compression GZIPPED DATA :status: 200 content-length: 318 cache-control: private, max-age=60, s-maxage=0 vary: SecurityToken date: Sun, 02 Feb 2014 20:30:38 GMT content-type: application/json content-encoding: gzip :method: GET :authority: apihost :path: /things securitytoken: b08c85073c1a2d02 accept: application/json accept-encoding: gzip, deflate HEADERS Stream: 3 Flags: END_HEADERS, END_STREAM HEADERS Stream: 3 Flags: END_HEADERS DATA Stream: 3 Flags: END_STREAM 8 bytes 52 bytes compressed 8 bytes 85 bytes compressed • 161 byte overhead instead of 363 8 bytes
  49. 49. indexed headers! GZIPPED DATA :status: 200 content-length: 318 cache-control: private, max-age=60, s-maxage=0 vary: SecurityToken date: Sun, 02 Feb 2014 20:30:39 GMT content-type: application/json content-encoding: gzip :method: GET :authority: apihost :path: /things securitytoken: b08c85073c1a2d02 accept: application/json accept-encoding: gzip, deflate HEADERS Stream: 3 Flags: END_HEADERS, END_STREAM HEADERS Stream: 3 Flags: END_HEADERS DATA Stream: 3 Flags: END_STREAM 8 bytes 28 bytes compressed 8 bytes 30 bytes compressed • 82 byte overhead instead of 363 8 bytes
  50. 50. hpack http://tools.ietf.org/html/rfc7541 • rfc7540 - May 2015 • static and dynamic table • huffman encoding
  51. 51. multiplexing header compression server push http/2 headline features
  52. 52. push promise :method: GET :path: /things ... HEADERS Stream: 3 HEADERS Stream: 3 DATA Stream: 4 :method: GET :path: /users/0 ... PUSH_PROMISE Stream: 3 Promised-Stream: 4 HEADERS Stream: 4 push response goes into cache DATA Stream: 3 Server guesses a future request or indicates a cache invalidation
  53. 53. okhttp + http/2 • OkHttp 2.3+ supports http/2 on TLS+ALPN connections. • Works out of the box on Android • Java needs hacks until JEP 244 For now, add jetty’s ALPN to your bootclasspath. * Verify version matches your JRE * $ java -Xbootclasspath/p:/path/to/alpn-boot-8.1.4.v20150727.jar
  54. 54. We won! • 1 socket for 100+ concurrent streams. • Advanced features like flow control, cancellation and cache push. • Dramatically less header overhead.
  55. 55. okhttp tools • There are tools that leverage OkHttp natively… • including its http/2 features.
  56. 56. MockWebServer SSLSocketFactory factory = SslContextBuilder.localhost().getSocketFactory(); OkHttpClient client = new OkHttpClient() .setSslSocketFactory(factory); @Rule public final MockWebServer server = new MockWebServer() .setSslSocketFactory(factory); @Test public void evenWithHttp2SlowResponsesKill() throws Exception { server.enqueue(new MockResponse().setBody(lotsOfThings) .throttleBody(1024, 1, SECONDS)); // slow connection 1KiB/second request = new Request.Builder() .url(server.getUrl(”/”) + ”things”) —snip— • MockWebServer uses http/2 by default when using TLS
  57. 57. Retrofit 2 @Headers("Accept-Encoding: gzip, deflate”) interface ThingService { @GET("things") Single<List<Things> list(); // RxJava support! } retrofit = new Retrofit.Builder() .client(client) .baseUrl(“https://apihost/“) .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create()) .build(); thingsService = retrofit.create(ThingService.class); subscription = thingsService.iist().subscribe(System.out::println) • Retrofit 2 extends OkHttp with api model binding
  58. 58. okcurl $ java -Xbootclasspath/p:alpn-boot.jar -jar okcurl.jar https://twitter.com -X HEAD --frames >> CONNECTION 505249202a20485454502f322e300d0a0d0a534d0d0a0d0a << 0x00000000 6 SETTINGS >> 0x00000000 6 SETTINGS >> 0x00000000 4 WINDOW_UPDATE >> 0x00000000 0 SETTINGS ACK >> 0x00000003 60 HEADERS END_STREAM|END_HEADERS << 0x00000000 0 SETTINGS ACK << 0x00000003 2097 HEADERS END_STREAM|END_HEADERS >> 0x00000000 8 GOAWAY
  59. 59. introduction hello http api! hello okhttp! uh oh.. scale! hello http/2! wrapping up
  60. 60. Engage! • Consider http/2 or at least spdy! • Check-out okhttp and friends • Test http/2 load balancers like google cloud https://github.com/square/okhttp/tree/master/mockwebserver http://square.github.io/retrofit/ https://cloud.google.com/compute/docs/load-balancing/http/
  61. 61. Thank you! github square/okhttp @adrianfcole

×