Slide 1

Slide 1 text

Jakub Marchwicki <@kubem> from sync to async the swampy grounds of handling http requests Jakub Marchwicki <@kubem>

Slide 2

Slide 2 text

Jakub Marchwicki <@kubem> Tomek Nurkiewicz http://nurkiewicz.github.io/talks/2018/reactive-lessons/

Slide 3

Slide 3 text

Jakub Marchwicki <@kubem> Tomek Nurkiewicz http://nurkiewicz.github.io/talks/2018/reactive-lessons/

Slide 4

Slide 4 text

Jakub Marchwicki <@kubem> Tomek Nurkiewicz http://nurkiewicz.github.io/talks/2018/reactive-lessons/

Slide 5

Slide 5 text

Jakub Marchwicki <@kubem> Tomek Nurkiewicz http://nurkiewicz.github.io/talks/2018/reactive-lessons/ The Point

Slide 6

Slide 6 text

Jakub Marchwicki <@kubem> What is The Point

Slide 7

Slide 7 text

Software engineer Consultant Trainer Chief Mob Officer Jakub Marchwicki <@kubem> http://jakub.marchwicki.pl

Slide 8

Slide 8 text

Jakub Marchwicki <@kubem> What is The Point a traffic which justifies the Netflix architecture

Slide 9

Slide 9 text

Jakub Marchwicki <@kubem> What is The Point a traffic which justifies the Netflix a reactive architecture

Slide 10

Slide 10 text

Jakub Marchwicki <@kubem> What is The Point a traffic which justifies the Netflix a reactive architecture way of solving business goals

Slide 11

Slide 11 text

Jakub Marchwicki <@kubem> traditional synchronous bleeding edge reactive architecture are not toggles architecture are sliders

Slide 12

Slide 12 text

Jakub Marchwicki <@kubem> https://dzone.com/articles/spring-boot-20-webflux-reactive-performance-test (...) better performance than synchronous code on high concurrency scenarios

Slide 13

Slide 13 text

Jakub Marchwicki <@kubem> https://dzone.com/articles/spring-boot-20-webflux-reactive-performance-test (...) better performance than synchronous code on high concurrency scenarios public Flux getUsersAsync() { return Flux .fromIterable(userList) .delaySubscription( Duration.ofMillis(delay) ); }

Slide 14

Slide 14 text

Jakub Marchwicki <@kubem> let’s decompose

Slide 15

Slide 15 text

Jakub Marchwicki <@kubem> synchronous, multithreaded, blocking each http request in handled exclusively by a single thread (end-2-end) number of threads (therefore nb of concurrent connections) is limited to the pool size processing time for each requests determines the throughput

Slide 16

Slide 16 text

Jakub Marchwicki <@kubem> This leads us to question... how many requests can we handle

Slide 17

Slide 17 text

Jakub Marchwicki <@kubem> ~450ms/request with up to 200 threads

Slide 18

Slide 18 text

Jakub Marchwicki <@kubem> ~450ms/request with up to 200 threads

Slide 19

Slide 19 text

Jakub Marchwicki <@kubem> ~450ms/request with up to 200 threads ~444 requests / second

Slide 20

Slide 20 text

Jakub Marchwicki <@kubem> ~450 ms/request with up to 200 threads ~444 requests / second on a long-term average

Slide 21

Slide 21 text

Jakub Marchwicki <@kubem> ~450 ms/request with up to 200 threads ~444 requests / second on a long-term average no. items in the queue average time spent in the queue arrival rate https://www.process.st/littles-law/

Slide 22

Slide 22 text

Jakub Marchwicki <@kubem> L = λ * W customers arrive at the rate of 10 per hour customer stay an average of 0.5 hour average number of customers in the store at any time to be 5 customers arrive at the rate of 20 per hour average number of customers in the store at any time to be ?? customer stay an average of ?? hour customer stay an average of 0.5 hour (stays the same) average number of customers in the store at any time to be 10

Slide 23

Slide 23 text

Jakub Marchwicki <@kubem> in theory practice meets theory

Slide 24

Slide 24 text

Jakub Marchwicki <@kubem> @GetMapping("/loans/{loanId}") public LoanInformation loanDetails(@PathVariable("loanId") UUID loanId) { var riskInformation = riskClient.getRiskAssessmentDetails(loanId); var loanDetails = loansClient.getLoanDetails(loanId); var member = membersClient.getMemberDetails(loanDetails.getMemberId()); return LoanInformation .fromLoanDetails(loanDetails) .requestId(UUID.randomUUID()) .member(member) .riskAssessment(riskInformation) .build(); }

Slide 25

Slide 25 text

Jakub Marchwicki <@kubem> Ideal world

Slide 26

Slide 26 text

Jakub Marchwicki <@kubem> Tomcat

Slide 27

Slide 27 text

Jakub Marchwicki <@kubem> Jetty

Slide 28

Slide 28 text

Jakub Marchwicki <@kubem> Undertow

Slide 29

Slide 29 text

Jakub Marchwicki <@kubem> Upon startup, Tomcat will create threads based on the value set for minSpareThreads (10) and increase that number based on demand, up to the number of maxThreads (200). If the maximum number of threads is reached, and all threads are busy, incoming requests are placed in a queue (acceptCount - 100) to wait for the next available thread. The server will only continue to accept a certain number of concurrent connections (as determined by maxConnections - 200). https://www.datadoghq.com/blog/tomcat-architecture-and-performance/

Slide 30

Slide 30 text

Jakub Marchwicki <@kubem> Undertow uses XNIO as the default connector. XNIO (...) default configuration (...) is I/O threads initialized to the number of your logical threads and the worker thread equal to 8 * CPU cores. So on typical 4 cores Intel CPU with hyper-threading you will end up with 8 I/O threads and 64 working threads. https://jmnarloch.wordpress.com/2016/04/26/spring-boot-tuning-your-undertow-application-for-throughput/

Slide 31

Slide 31 text

Jakub Marchwicki <@kubem> Undertow uses XNIO as the default connector. XNIO (...) default configuration (...) is I/O threads initialized to the number of your logical threads and t`he worker thread equal to 8 * CPU cores. So on typical 4 cores Intel CPU with hyper-threading you will end up with 8 I/O threads and 64 working threads. https://jmnarloch.wordpress.com/2016/04/26/spring-boot-tuning-your-undertow-application-for-throughput/ var ioThreads = Math.max( Runtime.getRuntime().availableProcessors(), 2 ); var workerThreads = ioThreads * 8; number of processors available to the JVM number of logical cores: Core i7 w. HyperThreading: 8 number of logical cores: Q6700: 4 number of logical cores: docker --cpus=1 on a quad core: 8 Compare it to 100 or 1000, defaults at Tomcat or Jetty number of logical cores: docker --cpus=1 on a quad core: 1 (JDK10) number of logical cores: docker --cpuset-cpus=0,1 on a quad core: 2

Slide 32

Slide 32 text

TAKEAWAY Full stack developer doesn’t mean same technology on frontend and backend. Seniority comes from understanding layers beyond the code you craft.

Slide 33

Slide 33 text

Jakub Marchwicki <@kubem> not all threads were created equal requests

Slide 34

Slide 34 text

Jakub Marchwicki <@kubem> @GetMapping("/loans/{loanId}") public LoanInformation loanDetails(@PathVariable("loanId") UUID loanId) { var riskInformation = riskClient.getRiskAssessmentDetails(loanId); var loanDetails = loansClient.getLoanDetails(loanId); var member = membersClient.getMemberDetails(loanDetails.getMemberId()); return LoanInformation .fromLoanDetails(loanDetails) .requestId(UUID.randomUUID()) .member(member) .riskAssessment(riskInformation) .build(); } on demand computation, take time (~600ms) these two are relatively fast, direct lookups (~150 - 300 ms)

Slide 35

Slide 35 text

Jakub Marchwicki <@kubem> requests thread pool lookup member (~300ms) search members (~600ms) lookup loan (~150ms)

Slide 36

Slide 36 text

Jakub Marchwicki <@kubem> requests thread pool lookup member (~300ms) search members (~600ms) lookup loan (~150ms)

Slide 37

Slide 37 text

Jakub Marchwicki <@kubem> requests thread pool lookup member (~300ms) search members (~600ms) lookup loan (~150ms)

Slide 38

Slide 38 text

Jakub Marchwicki <@kubem> requests thread pool lookup member (~300ms) search members (~600ms) lookup loan (~150ms)

Slide 39

Slide 39 text

Jakub Marchwicki <@kubem>

Slide 40

Slide 40 text

Jakub Marchwicki <@kubem>

Slide 41

Slide 41 text

Jakub Marchwicki <@kubem> asynchronous, multithreaded, blocking

Slide 42

Slide 42 text

Jakub Marchwicki <@kubem> operations are done by choosing a worker thread from a thread pool the io thread is returned to the pool to run other requests, and process the upstream response asynchronously too the worker thread notifies the request thread when its work is complete. to offset these risks of backend latency, throttling mechanisms and circuit breakers help keep the blocking systems stable and resilient.

Slide 43

Slide 43 text

Jakub Marchwicki <@kubem> @GetMapping("/loans/{loanId}") public CompletableFuture loanDetails(@PathVariable("loanId") UUID loanId) { return supplyAsync(() -> loansClient.getLoanDetails(loanId), executor) .thenApply(l -> { Member memberDetails = membersClient.getMemberDetails(l.getMemberId()); return Tuple.of(l, memberDetails); }) .thenCombine(supplyAsync(() -> riskClient.getRiskAssessmentDetails(loanId), executor), (loanDetailsMember, riskInformation) -> LoanInformation .fromLoanDetails(loanDetailsMember.getLeft()) .requestId(UUID.randomUUID()) .member(loanDetailsMember.getRight()) .riskAssessment(riskInformation) .build()); }

Slide 44

Slide 44 text

Jakub Marchwicki <@kubem> requests thread pool lookup member (~300ms) search members (~600ms) lookup loan (~150ms) service thread pool

Slide 45

Slide 45 text

Jakub Marchwicki <@kubem> requests thread pool lookup member (~300ms) search members (~600ms) lookup loan (~150ms) service thread pool ?

Slide 46

Slide 46 text

TAKEAWAY Thread pool tuning is tied to what the application needs Understand the nature of the traffic

Slide 47

Slide 47 text

Jakub Marchwicki <@kubem> Worst case scenario??

Slide 48

Slide 48 text

Jakub Marchwicki <@kubem> Worst case scenario?? https://www.nurkiewicz.com/2011/03/tenfold-increase-in-server-throughput.html

Slide 49

Slide 49 text

Jakub Marchwicki <@kubem> asynchronous, single-threaded, non-blocking

Slide 50

Slide 50 text

Jakub Marchwicki <@kubem> @GetMapping("/loans/{loanId}") public Observable loanDetails(@PathVariable("loanId") UUID loanId) { Single loanDetails = loansClient.getLoanDetailsSingle(loanId).cache(); Single member = loanDetails .flatMap(l -> membersClient.getMemberDetailsSingle(l.getMemberId())); Single riskInformation = riskClient.getRiskAssessmentDetailsSingle(loanId); return Single.zip( loanDetails, member, riskInformation, (l, m, r) -> LoanInformation .fromLoanDetails(l) .requestId(UUID.randomUUID()) .member(m) .riskAssessment(r) .build() ).toObservable(); }

Slide 51

Slide 51 text

Jakub Marchwicki <@kubem> reactive programming does not build a reactive system

Slide 52

Slide 52 text

Jakub Marchwicki <@kubem> responsive handle requests in a reasonable time resilient stay responsive in the face of failures elastic scale up and down, be able to handle the load with minimal resources. message driven interactions using asynchronous message passing. a reactive system promise

Slide 53

Slide 53 text

Jakub Marchwicki <@kubem> a reactive programming - in technical terms Handling huge volumes of data in multi-userness environment Efficiency gains: data stays on the same CPU, use of CPU level caches, fewer context switches 25% increase in throughput corresponding with a 25% reduction in CPU utilization

Slide 54

Slide 54 text

Jakub Marchwicki <@kubem> a reactive programming - in technical terms Handling huge volumes of data in multi-userness environment Efficiency gains: data stays on the same CPU, use of CPU level caches, fewer context switches 25% increase in throughput corresponding with a 25% reduction in CPU utilization MOAR TRAFFIC!

Slide 55

Slide 55 text

Jakub Marchwicki <@kubem> the reactive promise - but... Blocking systems are easy to grok and debug a thread is always doing a single operation The event loop’s stack trace is meaningless when trying to follow a request Unhandled exceptions create dangling resources (exception swallow)

Slide 56

Slide 56 text

Jakub Marchwicki <@kubem> at what cost?

Slide 57

Slide 57 text

Jakub Marchwicki <@kubem> final List results = getQueries().stream() //there're 6 db queries .map(query -> db.apply(query)) .sorted(naturalOrder()) .collect(Collectors.toList()); final List results = Observable.from(getQueries()) // there're 6 db queries .flatMap(query -> Async.start(() -> db.apply(query), scheduler)) .toSortedList() .toBlocking() .single(); final List results = new ArrayList<>(); for (Query q: getQueries()) { String result = db.apply(q); results.add(result); } results.sort(naturalOrder());

Slide 58

Slide 58 text

Jakub Marchwicki <@kubem> [EL Warning]: 2009-08-29 12:53:13.718--Exception [EclipseLink-4002] (Eclipse Persistence Services - 1.1.2.v20090612-r4475): org.eclipse.persistence.exceptions.DatabaseException Internal Exception: java.sql.BatchUpdateException: The statement was aborted because it would have caused a duplicate key value in a unique or primary key constraint or unique index identified by 'SQL090829125312890' defined on 'REMINDISSUE'. Error Code: 20000 Exception in thread "main" javax.persistence.RollbackException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 1.1.2.v20090612-r4475): org.eclipse.persistence.exceptions.DatabaseException Internal Exception: java.sql.BatchUpdateException: The statement was aborted because it would have caused a duplicate key value in a unique or primary key constraint or unique index identified by 'SQL090829125312890' defined on 'REMINDISSUE'. Error Code: 20000 at org.eclipse.persistence.internal.jpa.transaction.EntityTransactionImpl.commitIntern at org.eclipse.persistence.internal.jpa.transaction.EntityTransactionImpl.commit(Entit at com.example.dao.jpa.JpaDAO.commit(JpaDAO.java:99) at com.example.dao.jpa.JpaDAO.persist(JpaDAO.java:41) at com.example.dao.jpa.JpaDAO.persist(JpaDAO.java:1) at com.example.test.TestDAO.main(TestDAO.java:44)

Slide 59

Slide 59 text

Jakub Marchwicki <@kubem> public static void main(String[] args) { Observable.empty() .observeOn(Schedulers.io()) .toBlocking() .first(); } Exception in thread "main" java.util.NoSuchElementException: Sequence contains no el at rx.internal.operators.OperatorSingle$ParentSubscriber.onCompleted(OperatorSin at rx.internal.operators.OperatorTake$1.onCompleted(OperatorTake.java:53) at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber.pollQueue(Operato at rx.internal.operators.OperatorObserveOn$ObserveOnSubscriber$1.call(OperatorOb at rx.internal.schedulers.ScheduledAction.run(ScheduledAction.java:55) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$2 at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(Sche at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:114 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:61 at java.lang.Thread.run(Thread.java:745)

Slide 60

Slide 60 text

TAKEAWAY Both WebMVC / Servlet / synchronous and WebFlux / RxJava / reactive have a reason to exist.

Slide 61

Slide 61 text

TAKEAWAY Know your clients users and their flows Know your expectations what you optimizing for Know your domain Know your opportunity costs

Slide 62

Slide 62 text

Jakub Marchwicki <@kubem> Thank you! https://speakerdeck.com/kubamarchwicki/sync-or-async Jakub Marchwicki <@kubem>