Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

Copyright © 2020, Oracle and/or its affiliates. All rights reserved. Делаем CompletableFuture быстрее советы и трюки по производительности Сергей Куксенко Oracle Февраль, 2020

Slide 3

Slide 3 text

Copyright © 2020, Oracle and/or its affiliates. All rights reserved. Safe Harbor Statement The following is intended to outline our general product direction. It is intended for information purposes only, and may not be incorporated into any contract. It is not a commitment to deliver any material, code, or functionality, and should not be relied upon in making purchasing decisions. The development, release, and timing of any features or functionality described for Oracle’s products remains at the sole discretion of Oracle. 3

Slide 4

Slide 4 text

Кто я? • Java Performance Engineer at Oracle, @since 2010 • Java Performance Engineer, @since 2005 • Java Engineer, @since 1996 4

Slide 5

Slide 5 text

j.u.c.CompletableFuture • появился в Java8 5

Slide 6

Slide 6 text

j.u.c.CompletableFuture • появился в Java8 • не использовался в Java8 5

Slide 7

Slide 7 text

j.u.c.CompletableFuture • появился в Java8 • не использовался в Java8 5

Slide 8

Slide 8 text

j.u.c.CompletableFuture • появился в Java8 • не использовался в Java8 5

Slide 9

Slide 9 text

j.u.c.CompletableFuture • Начиная с Java9: – Process API – HttpClient (до 11 в инкубаторе)* *основная часть советов отсюда 6

Slide 10

Slide 10 text

HttpClient (a.k.a. JEP-110) 7

Slide 11

Slide 11 text

HttpClient Обработка запросов: • синхронная (блокирующая) • асинхронная 8

Slide 12

Slide 12 text

синхронная HttpClient client = «create client»; HttpRequest request = «create request»; HttpResponse response = client.send(request, BodyHandler.asString()); if (response.statusCode() == 200) { System.out.println("We’ve got: " + response.body()); } ... 9

Slide 13

Slide 13 text

асинхронная HttpClient client = «create client»; HttpRequest request = «create request»; CompletableFuture> futureResponse = client.sendAsync(request, BodyHandler.asString()); futureResponse.thenAccept( response -> { if (response.statusCode() == 200) { System.out.println("We’ve got: " + response.body()); } }); ... 10

Slide 14

Slide 14 text

создание клиента HttpClient client = HttpClient.newBuilder() .authenticator(someAuthenticator) .sslContext(someSSLContext) .sslParameters(someSSLParameters) .proxy(someProxySelector) .executor(someExecutorService) .followRedirects(HttpClient.Redirect.ALWAYS) .cookieManager(someCookieManager) .version(HttpClient.Version.HTTP_2) .build(); Хорошее правило для асинхронного API 11

Slide 15

Slide 15 text

CompletableFuture CompletionStage 12

Slide 16

Slide 16 text

Про производительность разработчиков 13

Slide 17

Slide 17 text

43 метода CompletionStage 14

Slide 18

Slide 18 text

43 метода CompletionStage 15

Slide 19

Slide 19 text

43 метода CompletionStage 42 имеют вид: • somethingAsync(..., executor) • somethingAsync(...) • something(...) 16

Slide 20

Slide 20 text

j.u.c.CompletionStage • somethingAsync(..., executor) – выполняем действия в executor • somethingAsync(...) – somethingAsync(..., ForkJoinPool.commonPool()) • something(...) – по умолчанию поговорим позже 17

Slide 21

Slide 21 text

j.u.c.CompletionStage CompletionStage • apply(Function) ⇒ CompletionStage – combine(BiFunction) • accept(Consumer) ⇒ CompletionStage • run(Runnable) ⇒ CompletionStage 18

Slide 22

Slide 22 text

j.u.c.CompletionStage • унарные – thenApply, thenAccept, thenRun • «OR» – applyToEither, acceptEither, runAfterEither • «AND» – thenCombine, thenAcceptBoth, runAfterBoth 19

Slide 23

Slide 23 text

j.u.c.CompletionStage Осталось: • thenCompose (flatMap в мире CF) • handle/whenComplete • exceptionally/exceptionallyCompose • toCompletableFuture 20

Slide 24

Slide 24 text

j.u.c.CompletableFuture Завершить разными способами • complete/completeAsync/completeExceptionally • cancel • obtrudeValue/obtrudeException • completeOnTimeout/orTimeout 21

Slide 25

Slide 25 text

j.u.c.CompletableFuture Получить значение • get/join – блокируемся • get(timeout, timeUnit) – чуть-чуть блокируемся • getNow(valueIfAbsent) – не блокируемся 22

Slide 26

Slide 26 text

j.u.c.CompletableFuture Подглядеть статус • isDone • isCompletedExceptionally • isCancelled 23

Slide 27

Slide 27 text

j.u.c.CompletableFuture Создать future • completedFuture/completedStage • failedFuture/failedStage • runAsync(Runnable) → CompletableFuture • supplyAsync(Supplier) → CompletableFuture 24

Slide 28

Slide 28 text

Блокирующий или Асинхронный? 25

Slide 29

Slide 29 text

Блокирующий или Асинхронный • Блокирующий: – R doSmth(...); • Асинхронный: – CompletableFuture doSmthAsync(...); 26

Slide 30

Slide 30 text

Блокирующий ⇔ Асинхронный R doSmth(...) CompletableFuture doSmthAsync(...) CompletableFuture .supplyAsync(() -> doSmth()) doSmthAsync(...) .join() 27

Slide 31

Slide 31 text

Blocking via async R doSmth(...) { return doSmthAsync(...).join(); } Это вообще работает? 28

Slide 32

Slide 32 text

User threads Executor threads doSmth doSmthAsync join work 29

Slide 33

Slide 33 text

А померить? 30

Slide 34

Slide 34 text

А померить? 30

Slide 35

Slide 35 text

А померить? 30

Slide 36

Slide 36 text

Мораль Перемещение работы из потока в поток снижает производительность 31

Slide 37

Slide 37 text

Async via blocking CompletableFuture doSmthAsync(...) { return CompletableFuture.supplyAsync(()->doSmth(...), executor); } Это вообще работает? 32

Slide 38

Slide 38 text

Вернемся к HttpClient’у public HttpResponse send(HttpRequest req, BodyHandler responseHandler) { ... } public CompletableFuture> sendAsync (HttpRequest req, BodyHandler responseHandler) { return CompletableFuture.supplyAsync(() -> send(req, responseHandler), executor); } Это вообще работает? 33

Slide 39

Slide 39 text

Вернемся к HttpClient’у public HttpResponse send(HttpRequest req, BodyHandler responseHandler) { ... } public CompletableFuture> sendAsync (HttpRequest req, BodyHandler responseHandler) { return CompletableFuture.supplyAsync(() -> send(req, responseHandler), executor); } Иногда 33

Slide 40

Slide 40 text

Нельзя так просто взять и сделать «sendAsync» • послать «header» • послать «body» • получить «header» от сервера • получить «body» от сервера 34

Slide 41

Slide 41 text

Нельзя так просто взять и сделать «sendAsync» • послать «header» • послать «body» • ждать «header» от сервера • ждать «body» от сервера 34

Slide 42

Slide 42 text

User threads Executor threads sendAsync supplyAsync send 35

Slide 43

Slide 43 text

User threads Executor threads sendAsync supplyAsync send wait/await 35

Slide 44

Slide 44 text

User threads Executor threads sendAsync supplyAsync send wait/await receiveResponse notify/signal 35

Slide 45

Slide 45 text

User threads Executor threads sendAsync supplyAsync send wait/await receiveResponse notify/signal 35

Slide 46

Slide 46 text

User threads Executor threads sendAsync supplyAsync send wait/await receiveResponse notify/signal DON’T! 35

Slide 47

Slide 47 text

User threads Executor threads sendAsync supplyAsync send wait/await Aux threads receiveResponse notify/signal processResponse 35

Slide 48

Slide 48 text

RTFM (HttpClient.Builder) /** * Sets the executor to be used for asynchronous tasks. If this method is * not called, a default executor is set, which is the one returned from * {@link java.util.concurrent.Executors#newCachedThreadPool() * Executors.newCachedThreadPool}. * * @param executor the Executor * @return this builder */ public abstract Builder executor(Executor executor); Хороший асинхронный API должен работать с любым executor’ом. 36

Slide 49

Slide 49 text

RTFM (java.util.concurrent.Executors) /** * Creates a thread pool that creates new threads as needed, but * will reuse previously constructed threads when they are * available. These pools will typically improve the performance * of programs that execute many short-lived asynchronous tasks. * Calls to {@code execute} will reuse previously constructed * threads if available. If no existing thread is available, a new * thread will be created and added to the pool. Threads that have * not been used for sixty seconds are terminated and removed from * the cache. Thus, a pool that remains idle for long enough will * not consume any resources. Note that pools with similar * properties but different details (for example, timeout parameters) * may be created using {@link ThreadPoolExecutor} constructors. * * @return the newly created thread pool */ public static ExecutorService newCachedThreadPool() 37

Slide 50

Slide 50 text

CachedThreadPool • Что хорошо: – если все потоки заняты, задача будет запущена в новом потоке • Что плохо: – если все потоки заняты, новый поток будет создан 38

Slide 51

Slide 51 text

sendAsync via send Один HttpRequest порождал ∼ 20 потоков. Значит ли, что 100 одновременных запросов ⇒ ∼ 2000 потоков? 39

Slide 52

Slide 52 text

sendAsync via send Один HttpRequest порождал ∼ 20 потоков. Значит ли, что 100 одновременных запросов ⇒ ∼ 2000 потоков? 100 одновременных запросов ⇒ OutOfMemoryError! 39

Slide 53

Slide 53 text

Удаляем ожидание (шаг 1) Executor thread Condition responseReceived; R send(...) { sendRequest(...); responseReceived.await(); processResponse(); ... } Aux thread ... receiveResponse(...) { ... responseReceived.signal(); ... } 40

Slide 54

Slide 54 text

Удаляем ожидание (шаг 1) «CompletableFuture» как одноразовый «Condition» Executor thread CompletableFuture<...> responseReceived; R send(...) { sendRequest(...); responseReceived.join(); processResponse(); ... } Aux thread ... receiveResponse(...) { ... responseReceived.complete(); ... } 41

Slide 55

Slide 55 text

Удаляем ожидание (шаг 2) CompletableFuture<...> sendAsync(...) { return CompletableFuture.supplyAsync(() -> send(...)); } R send(...) { sendRequest(...); responseReceived.join(); return processResponse(); } 42

Slide 56

Slide 56 text

Удаляем ожидание (шаг 2) CompletableFuture<...> sendAsync(...) { return CompletableFuture.supplyAsync(() -> sendRequest(...)) .thenApply((...) -> responseReceived.join()) .thenApply((...) -> processResponse()); } 43

Slide 57

Slide 57 text

Удаляем ожидание (шаг 2) CompletableFuture<...> sendAsync(...) { return CompletableFuture.supplyAsync(() -> sendRequest(...)) .thenCompose((...) -> responseReceived) .thenApply((...) -> processResponse()); } 44

Slide 58

Slide 58 text

User threads Executor threads sendAsync supplyAsync send thenCompose Aux threads future receiveResponse complete processResponse 45

Slide 59

Slide 59 text

А что там с производительностью? Удаление wait()/await() ⇓ +40% 46

Slide 60

Slide 60 text

Мораль Блокировки внутри «CompletableFuture» цепочки снижают производительность 47

Slide 61

Slide 61 text

Задачка Поток 1 future.thenApply((...) -> foo()); Поток 2 future.complete(...); В каком потоке будет выполняться foo()? A) поток 1 B) поток 2 C) поток 1 или поток 2 D) поток 1 и поток 2 48

Slide 62

Slide 62 text

Задачка Поток 1 future.thenApply((...) -> foo()); Поток 2 future.complete(...); В каком потоке будет выполняться foo()? A) поток 1 B) поток 2 C) поток 1 или поток 2 D) поток 1 и поток 2 правильный ответ 48

Slide 63

Slide 63 text

Так где же CompletableFuture выполняет действия по умолчанию? 49

Slide 64

Slide 64 text

Это очень просто • Завершающий поток выполняет действия, привязанные «до» завершения. • Конструирующий поток выполняет действия, если CompletableFuture завершен «до» конструирования. 50

Slide 65

Slide 65 text

Это не всегда просто • Завершающий поток выполняет действия, привязанные «до» завершения. • Конструирующий поток выполняет действия, если CompletableFuture завершен «до» конструирования. Это параллелизм, тут гонки. 50

Slide 66

Slide 66 text

Действия могут быть выполнены в: • завершающем потоке – complete, completeExceptionally ... • конструирующем потоке – thenApply, thenCompose ... • запрашивающем потоке – get, join ... 51

Slide 67

Slide 67 text

Где пруфы? 52

Slide 68

Slide 68 text

jcstress (все уже написано до нас) http://openjdk.java.net/projects/code-tools/jcstress/ The Java Concurrency Stress tests (jcstress) is an experimental harness and a suite of tests to aid the research in the correctness of concurrency support in the JVM, class libraries, and hardware. 53

Slide 69

Slide 69 text

Пример 1 CompletableFuture<...> f = new CompletableFuture<>(); f.complete(...); f.thenApply(a -> action()); Результаты: Occurrences Expectation Interpretation 1,630,058,138 ACCEPTABLE action in chain construction thread 197,470,850 ACCEPTABLE action in completion thread 54

Slide 70

Slide 70 text

Пример 2 CompletableFuture<...> f = new CompletableFuture<>(); f.thenApply(a -> action()); f.complete(...); f.complete(...); Результаты: Occurrences Expectation Interpretation 819,755,198 ACCEPTABLE action in successful completion thread 163,205,510 ACCEPTABLE action in failed completion thread 55

Slide 71

Slide 71 text

Пример 3 CompletableFuture<...> f = new CompletableFuture<>(); f.thenApply(a -> action()); f.complete(...); f.join(); Результаты: Occurrences Expectation Interpretation 904,651,258 ACCEPTABLE action in completion thread 300,524,840 ACCEPTABLE action in join thread 56

Slide 72

Slide 72 text

Пример 4 CompletableFuture<...> f = new CompletableFuture<>(); f.thenApply(a -> action1()); f.thenApply(a -> action2()); f.complete(...); f.join(); Результаты: Occurrences Expectation Interpretation 179,525,918 ACCEPTABLE both actions in the same thread 276,608,380 ACCEPTABLE actions in different threads 57

Slide 73

Slide 73 text

Что быстрее? По умолчанию future .thenApply((...) -> foo1()) .thenApply((...) -> foo2()) Async future .thenApplyAsync((...) -> foo1(), executor) .thenApplyAsync((...) -> foo2(), executor); 58

Slide 74

Slide 74 text

А померить? 59

Slide 75

Slide 75 text

А померить? 59

Slide 76

Slide 76 text

А померить? 59

Slide 77

Slide 77 text

CompletableFuture • thenSomethingAsync(...) – предсказуемость • thenSomething(...) – производительность 60

Slide 78

Slide 78 text

Мораль Перемещение работы из потока в поток снижает производительность 61

Slide 79

Slide 79 text

Когда необходима предсказуемость HttpClient, вспомогательный поток «SelectorManager»: • ждет на Selector.select • читает из Socket • выделяет HTTP2 фреймы • распределяет фреймы получателям 62

Slide 80

Slide 80 text

User threads sendAsync Executor threads thenCompose thenApply(foo) thenApply(bar) SelectorManager future receiveResponse complete foo bar 63

Slide 81

Slide 81 text

User threads sendAsync Executor threads thenCompose thenApply(foo) thenApply(bar) SelectorManager future receiveResponse complete foo bar foo bar DON’T! 63

Slide 82

Slide 82 text

Когда необходима предсказуемость CompletableFuture<...> response; Executor thread «SelectorManager» ... .thenCompose(() -> response) response.complete(...); ... 64

Slide 83

Slide 83 text

Можно так (@since 9) CompletableFuture<...> response; Executor thread «SelectorManager» ... .thenCompose(() -> response) response.completeAsync(..., executor); ... 65

Slide 84

Slide 84 text

Или так CompletableFuture<...> response; Executor thread «SelectorManager» ... .thenComposeAsync(() -> response, executor) response.complete(...); ... 66

Slide 85

Slide 85 text

Что мы имеем (в обоих случаях) • Плюсы: – «SelectorManager» защищен • Минусы: – Перемещаем работу из потока в поток 67

Slide 86

Slide 86 text

Еще вариант CompletableFuture<...> response; Executor thread «SelectorManager» CompletableFuture<...> cf = response; if(!cf.isDone()) { response.complete(...); cf = cf.thenApplyAsync(x -> x, executor); } ...thenCompose(() -> cf); ... 68

Slide 87

Slide 87 text

А что там с производительностью? Подкрутили complete() ⇓ +16% 69

Slide 88

Slide 88 text

Мораль Перемещение работы из потока в поток снижает производительность 70

Slide 89

Slide 89 text

А что если ответ приходит очень быстро? CompletableFuture<...> sendAsync(...) { return sendHeaderAsync(..., executor) .thenCompose(() -> sendBody()) .thenCompose(() -> getResponseHeader()) .thenCompose(() -> getResponseBody()) ... } Иногда (3% запросов) CompletableFuture уже завершен getResponseBody() выполняется в пользовательском потоке 71

Slide 90

Slide 90 text

Есть же thenComposeAsync() • Плюсы: – Пользовательский поток защищен • Минусы: – Перемещаем работу из потока в поток 72

Slide 91

Slide 91 text

Сделаем так CompletableFuture<...> sendAsync(...) { CompletableFuture start = new CompletableFuture<>(); CompletableFuture<...> end = start.thenCompose(v -> sendHeader()) .thenCompose(() -> sendBody()) .thenCompose(() -> getResponseHeader()) .thenCompose(() -> getResponseBody()) ...; start.completeAsync(() -> null, executor); // trigger execution return end; } 73

Slide 92

Slide 92 text

А что там с производительностью? Задержанный старт ⇓ +10% 74

Slide 93

Slide 93 text

Мораль Может быть полезно сначала сконструировать, а потом исполнять 75

Slide 94

Slide 94 text

Вернемся к CachedThreadPool • Что хорошо: – если все потоки заняты, задача будет запущена в новом потоке • Что плохо: – если все потоки заняты, новый поток будет создан Что выбрать executor’ом по умолчанию? 76

Slide 95

Slide 95 text

Попробуем разные CachedThreadPool 35500 ops/sec FixedThreadPool(2) 61300 ops/sec +72% 77

Slide 96

Slide 96 text

Мораль Не все ThreadPool’ы одинаково полезны быстры 78

Slide 97

Slide 97 text

Золотое правило производительности Забудьте про черный ящик Если вы что-то используете, то чтобы оно работало быстро, вы должны знать, как оно устроено внутри 79

Slide 98

Slide 98 text

Q & A ? 80

Slide 99

Slide 99 text

Appendix 81

Slide 100

Slide 100 text

просто пример thenCompose // e.g. how to make recursive CompletableFuture chain CompletableFuture<...> makeRecursiveChain(...) { if(«recursion ends normally») { return CompletableFuture.completedFuture(...); else if(«recursion ends abruptly») { return CompletableFuture.failedFuture(...); // appeared in Java9 } return CompletableFuture.supplyAsync(() -> doSomething(...)) .thenCompose((...) -> makeRecursiveChain(...)); } 82

Slide 101

Slide 101 text

83