SlideShare a Scribd company logo
A look into the
internals of an
HTTP Client
How Event Loops, Decorators, and Reactive Streams
are used to create an HTTP Client.
Modern day backend systems
- Network communication is an important part of modern day
systems.
- External services
- Databases
- Clients
- Especially, with the popularity of microservices
architecture, inter-component communication is becoming
more important.
Case Study 1
- A well configured HTTP client can go a long way.
- Imagine a request per thread server uses an HTTP client with
a 10 second response timeout, without a circuit breaker.
- The server may not be able to serve important requests.
A thread-per-request
server
Push
Service
Payment
Service
User Requests
- All threads are busy waiting for a
push response
HTTP Client
Case Study 2
final MyResponse response;
try {
response = client.get("/");
} catch(IOException e) {
// log the exception and pray this doesn’t happen more often
}
It would be nice if we actually could understand the
stacktrace and know what the issue is.
Before diving in
- I’d like to share how a performant, extensible client is implemented
by analyzing Armeria’s WebClient.
- I imagine other http clients are implemented in a similar way.
- I hope this will be useful even for those who won’t be using
Armeria.
- Based on the most recent Armeria tree at the moment of writing
- Version 1.25.2
- It is not realistic to describe all code paths, I’ll be assuming only the
most general code path with a few details left out.
So what is Armeria
- An asynchronous microservice framework
- Supports HTTP/1, HTTP/2, PROXY, TLS
- Built on top of Netty
- Compliant with Reactive Streams
- https://github.com/line/armeria
A simple example
ClientFactory cf = ClientFactory.builder().build();
HttpRequest req = HttpRequest.of(HttpMethod.GET, "/hello");
HttpResponse res = WebClient.builder("http://foo.com")
.factory(
cf)
.build()
.
execute(req);
AggregatedHttpResponse aggRes = res.aggregate().join();
System.out.println(aggRes.contentUtf8());
I will focus on what happens when HttpClient#execute is called
A simple example (illustrated)
DefaultWebClient
HttpClientDelegate
Decorators
Name Server
DNS Query
Target Server
Acquire a connection
HttpRequest HttpResponse
subscribe()
read(), write()
ClientFactory
DefaultWebClient
Executed from an Event Loop starting from this point
High level view of what we’ll be looking at
1. Figure out where exactly we want to send
our request
2. Open and prepare a TCP connection
3. Send our request and receive the response
GET /hello HTTP/1.1
Host: foo.com
HTTP/1.1 200 OK
Request
Response
A simple example (illustrated)
DefaultWebClient
HttpClientDelegate
Decorators
Name Server
DNS Query
Target Server
Acquire a connection
HttpRequest HttpResponse
subscribe()
read(), write()
ClientFactory
DefaultWebClient
Executed from an Event Loop starting from this point
DNS
Eventually we need to know where to send the request
- foo.com -> 10.0.0.0
Some questions over the past years
- There are too many DNS queries which are flooding the DNS server. Is there a way to reduce
the number of DNS queries.
- The target server IP will be replaced. How can we replace the servers gracefully so that
requests aren’t failed.
- If a DNS resolution fails, will the request fail? (should it fail?)
The starting point is calling resolveAddress , which eventually calls AddressResolver#resolve to
resolve a host name to an InetSocketAddress.
DNS: The Basic Resolver
Name Server
Armeria
foo.com
123.123.123.123
TTL: 1 day
- Basically, we ask the Name Server if there is a record “foo.com”
- If there is, an IP address is returned.
DNS: The Basic Resolver
Name Server
Armeria
foo.com
123.123.123.123
1. DelegatingDnsResolver
- Delegates DNS resolution to
the Netty layer resolver
- Resolver.conf
- Google
- …
DNS: The Basic Resolver
What about DNS query flooding?
- Too many DNS queries may degrade a DNS server’s performance.
- If N clients request to M domains, that means N * M DNS resolutions may
occur per each TTL
We may need a cache layer which respects the DNS record TTL
DNS: The Basic Resolver
Query Name Server
123.123.123.123
1. CachingDnsResolver
- Caches entries for each DNS
query
2. DelegatingDnsResolver
- Delegates DNS resolution to
the Netty layer resolver
123.123.123.123
Armeria
foo.com
Checks cached entries
DNS: The Basic Resolver
- What about search domains?
Wikipedia: A search domain is a domain
used as part of a domain search list.The search
list, as well as the local domain name, is used by
a resolver to create a fully qualified domain
name (FQDN) from a relative name
If search domains are “a.com”, “b.com”, the
searched domains will be:
- mydomain.a.com.
- mydomain.b.com.
- mydomain.
DNS: The Basic Resolver
Query Name Server
123.123.123.123
1. SearchDomainDnsResolver
- Queries for each search
domain if exists
2. CachingDnsResolver
- Caches entries for each DNS
query
3. DelegatingDnsResolver
- Delegates DNS resolution to
the Netty layer resolver
- foo.com.a.com.
- foo.com.b.com.
- foo.com.
123.123.123.123
Armeria
Expands query to each search
domain
foo.com
Checks cached entries
DNS: The Basic Resolver
Sometimes, users would like to
take more control over dns
resolution results
- /etc/hosts
DNS: The Basic Resolver
Query Name Server
123.123.123.123
1. HostsFileDnsResolver
- Checks whether the hosts file
can be used for DNS resolution
2. SearchDomainDnsResolver
- Queries for each search domain if
exists
3. CachingDnsResolver
- Caches entries for each DNS
query
4. DelegatingDnsResolver
- Delegates DNS resolution to the
Netty layer resolver
Check if hosts file has the entry
- foo.com.a.com.
- foo.com.b.com.
- foo.com.
123.123.123.123
Armeria
Expands query to each search
domain
foo.com
Checks cached entries
DNS: The Basic Resolver
Query Name Server
123.123.123.123
1. DefaultDnsResolver
- Puts everything together
2. HostsFileDnsResolver
- Checks whether the hosts file can
be used for DNS resolution
3. SearchDomainDnsResolver
- Queries for each search domain if
exists
4. CachingDnsResolver
- Caches entries for each DNS query
5. DelegatingDnsResolver
- Delegates DNS resolution to the
Netty layer resolver
Check if hosts file has the entry
- foo.com.a.com.
- foo.com.b.com.
- foo.com.
123.123.123.123
Armeria
Expands query to each search
domain
foo.com
Checks cached entries
- Q: If a request is made on “foo.com” but a DNS resolution fails, should we fail
the request overall?
- A: It depends! Obviously, the user should be able to configure this.
- But what is the most reasonable default behavior?
The current default implementation:
1. If there are no previously cached entries, the request will fail.
2. If there is a previously cached entry, try to use the previous value. Meanwhile,
try to refresh the cached entry in the background.
Note: We actually recommend using the EndpointGroup API for this scenario, but this is a separate
topic.
DNS: The Basic Resolver
DNS: The Basic Resolver
Query Name Server
123.123.123.123
1. RefreshingAddressResolver
- Refreshes addresses in the background
and serves cached dns entries.
2. DefaultDnsResolver
- Puts everything together
3. HostsFileDnsResolver
- Checks whether the hosts file can be
used for DNS resolution
4. SearchDomainDnsResolver
- Queries for each search domain if exists
5. CachingDnsResolver
- Caches entries for each DNS query
6. DelegatingDnsResolver
- Delegates DNS resolution to the Netty
layer resolver
Check if hosts file has the entry
- foo.com.a.com.
- foo.com.b.com.
- foo.com.
123.123.123.123
Armeria
Expands query to each search
domain
foo.com
Checks cached entries
Refreshes cached entries in
the background
DNS
FAQ
- There are too many DNS queries which are flooding the DNS server. Is there a way to
reduce the number of DNS queries.
- We can check if the number of search domains (ndots) is causing too many DNS
queries
- We can check if the dns cache is configured correctly
- The target server IP will be replaced. How can we replace the servers gracefully so that
requests aren’t failed.
- We can temporarily clamp the DNS TTL to a smaller value while migration takes
place
- We could also check if the RefreshingAddressResolver cache configurations.
- If a DNS resolution fails, will the request fail? (should it fail?)
- By default, it won’t fail. The previous DNS query result will be used by default even
if the TTL passed.
High level view of what we’ll be looking at
1. Figure out where exactly we want to send
our request
2. Open and prepare a TCP connection
3. Send our request and receive the response
GET /hello HTTP/1.1
Host: foo.com
HTTP/1.1 200 OK
Request
Response
A simple example (illustrated)
DefaultWebClient
HttpClientDelegate
Decorators
Name Server
DNS Query
Target Server
Acquire a connection
HttpRequest HttpResponse
subscribe()
read(), write()
ClientFactory
DefaultWebClient
Executed from an Event Loop starting from this point
Connection Pooling
How many connections should be opened in the above scenario?
- HTTP1.1: If keep alive is enabled, a connection handle requests sequentially without closing
the connection. (assuming pipelining is disabled)
- HTTP2: A connection handle handle MAX_CONCURRENT_STREAMS number of requests
concurrently
Opening a connection consumes resources/latency, so Armeria reuses connections as much as
possible.
WebClient client = WebClient.of();
client.get("http://1.2.3.4");
client.get("http://1.2.3.4");
client.get("http://1.2.3.4");
Connection Pooling
Endpoint
- A connection to (10.0.0.0) cannot be shared with a connection to (10.0.0.1)
Protocol
- A connection using HTTP2 cannot be shared with a connection using HTTP1
Event Loop
- Armeria uses Netty under the hood for network IO
- Netty assumes a single connection is handled by a single event loop
- This means a connection for EventLoopA cannot be shared with a connection for
EventLoopB
- We want to share connections as much as possible
- A connection is sharable per (Endpoint, Protocol, Event Loop) for Armeria.
Event Loops
Event Loop (Wikipedia)
the event loop is a programming construct or design
pattern that waits for and dispatches events or
messages in a program
Event Loops
class MyEventLoop {
final Queue<Runnable> taskQueue = new ArrayDeque<>();
final Thread thread = new Thread() {
@Override
public void run() {
while (true) {
Runnable runnable = taskQueue.poll();
if (runnable != null) {
runnable.run();
}
}
}
};
MyEventLoop() {
thread.start();
}
public void schedule(Runnable runnable) {
taskQueue.add(runnable);
}
}
MyEventLoop myEventLoop = …
myEventLoop.schedule(() -> System.out.println(“Hello”))
A simple event loop
implemented in Java
Event Loops
Without event loops, a simple solution is locking
int inc = 0;
ReentrantLock lock = new ReentrantLock();
private void increment() {
lock.lock();
try {
inc++;
} finally {
lock.unlock();
}
}
void main() {
for (int i = 0; i < 100; i++) {
ForkJoinPool.commonPool().submit(() -> increment());
}
await().until(() -> inc == 100);
assert inc == 100;
}
Event Loops
Provides a much simpler way to do concurrent programming.
int inc = 0;
EventLoop eventLoop = ...;
private void increment() {
if (!eventLoop.inEventLoop()) {
eventLoop.execute(() -> increment());
return;
}
inc++;
}
void main() {
for (int i = 0; i < 100; i++) {
ForkJoinPool.commonPool().submit(() -> increment());
}
await().until(() -> inc == 100);
assert inc == 100;
}
Note: although the queue
add operation may also
lock, it will do so for a
short time only
Event Loops
How do we handle two concurrent requests to a single TCP connection?
1. We can acquire a lock before writing to a connection
2. A single channel (connection) is handled by a single EventLoop
- We don’t have to worry about race conditions when writing to the wire
POST /a HTTP/1.1
Host: foo.com
GET /b HTTP/1.1
Host: foo.com
Content-Length: 5
Hello
POST /a HTTP/1.1
Host: foo.com
Content-Length: 5
Hello
GET /b HTTP/1.1
Host: foo.com
Connection Pooling
- Each ClientFactory maintains a connection pool
- Each request is pre-assigned an Event Loop
- Before a connection is acquired, the call is executed from an event
loop
- In order to acquire a connection
- We check if an existing connection can be used
- We try to acquire a new (or pending) connection
EventLoop
EventLoop
EventLoop
HttpChannelPool
HttpChannelPool
HttpChannelPool
HTTP2 10.0.0.1
HTTP2 10.0.0.2
HTTP1 10.0.0.3
Connections
Connections
Connections
ClientFactory
—
HttpClientDelegate
WebClient WebClient WebClient
Connection Pooling
How many connections should be opened in the above scenario? (given that the request is made concurrently)
- 0 new connections if there are reusable connections available
- Otherwise:
- 3 connections if HTTP1
- 1 connection if HTTP2
What if there are too many connections being created?
- Check which protocol we are using
- If HTTP2, try tuning the MAX_CONCURRENT_STREAMS value
- Check how many endpoints are behind foo.com
WebClient client = WebClient.of();
client.get("http://1.2.3.4");
client.get("http://1.2.3.4");
client.get("http://1.2.3.4");
A simple example (illustrated)
DefaultWebClient
HttpClientDelegate
Decorators
Name Server
DNS Query
Target Server
Acquire a connection
HttpRequest HttpResponse
subscribe()
read(), write()
ClientFactory
DefaultWebClient
Executed from an Event Loop starting from this point
Session Protocol Negotiation
This step prepares each new connection so that we can start sending HTTP requests.
The trigger is when a new channel connection is attempted, which eventually invokes
HttpClientPipelineConfigurator#connect.
- It mostly involves adding the appropriate Netty handlers to the pipeline.
Each protocol has different steps for preparing a connection.
- h1c://foo.com
- h2c://foo.com
- http://foo.com
(Note that I won’t be discussing https, but it is a simple extension of cleartext
negotiation)
Session Protocol Negotiation
h1c://foo.com
- Add HTTP message serializers/deserializers to the Netty Pipeline
- There is no need for protocol negotiation for HTTP1
- Open a connection, send a request, and receive a response.
- Session protocol negotiation is done immediately and the session
future is completed.
Session Protocol Negotiation
h2c://foo.com
- Add HTTP2 message serializers/deserializers to the Netty Pipeline
- Send a connection preface and a settings frame
- Receive a connection preface and a settings frame
- The expected frames have been received and the session future is
completed - we’re now free to send HTTP2 frames
Session Protocol Negotiation
http://foo.com
What if server-side doesn’t support HTTP2?
- We fall back to HTTP1
Steps
1. Send HTTP2 preface and settings frame assuming prior knowledge that the server
supports HTTP2
2. The server sends an error response because it actually doesn’t support HTTP2
3. Close the connection, and store a cache entry that the server doesn’t support
HTTP2
4. Open a new connection and start using HTTP1
Session Protocol Negotiation
- In the end, a sessionPromise is completed and we have a new
connection ready for use
- All we now have to do is send our HttpRequest and receive a
HttpResponse
High level view of what we’ll be looking at
1. Figure out where exactly we want to send
our request
2. Open and prepare a TCP connection
3. Send our request and receive the response
GET /hello HTTP/1.1
Host: foo.com
HTTP/1.1 200 OK
Request
Response
A simple example (illustrated)
DefaultWebClient
HttpClientDelegate
Decorators
Name Server
DNS Query
Target Server
Acquire a connection
HttpRequest HttpResponse
subscribe()
read(), write()
ClientFactory
DefaultWebClient
Executed from an Event Loop starting from this point
Sending requests and receiving responses
ClientFactory cf = ClientFactory.builder().build();
HttpRequest req = HttpRequest.of(HttpMethod.GET, "/hello");
HttpResponse res = WebClient.builder("http://foo.com" )
.factory( cf)
.build()
. execute(req);
System.out.println(res);
^ This prints immediately even though we didn’t actually send the request
out!
AggregatedHttpResponse aggRes = res.aggregate().join();
System.out.println(aggRes.contentUtf8());
Going back to the original code
Sending requests and receiving responses
HttpRequestWriter requestWriter =
HttpRequest.streaming(HttpMethod.POST, "/");
HttpResponse response = WebClient.builder("foo.com")
.build()
.execute(
requestWriter);
requestWriter.write(HttpData.ofAscii("Hello "));
Thread.sleep(1000)
requestWriter.write(HttpData.ofAscii("World"));
requestWriter.close();
Actually, we can even stream data to the HttpRequest.
- In the following sample code, we acquire a HttpResponse object
even though we didn’t write the request fully yet.
Reactive Streams
From the website: Reactive Streams is an initiative to provide a standard for
asynchronous stream processing with non-blocking back pressure
Publisher Subscriber
subscribe()
Publisher Subscriber
request(1)
Publisher Subscriber
onNext(message)
1. A subscriber subscribes to a publisher
2. The subscriber can process 1 request
3. The publisher sends 1 message when available
Publisher Subscriber
request(1)
Publisher Subscriber
onNext(message)
4. The subscriber can process 1 request
5. The publisher sends 1 message when available
Reactive Streams
Subscriber<Integer> subscriber = new
Subscriber<Integer>() {
private Subscription s;
@Override
public void onSubscribe(Subscription s) {
this.s = s;
s.request(1);
}
@Override
public void onNext(Integer integer) {
submitAsyncTask(integer).thenRun(() ->
s.request(1));
}
...
};
Flux.just(1,2,3).subscribe(subscriber);
onSubscribe(s)
request(1)
onNext(1)
request(1)
onNext(2)
request(1)
onNext(3)
Publisher Subscriber
Reactive Streams
10G file
on disk
1G RAM
Server
Network
Request some data
Send a small chunk of data
Send a small chunk of data
Ready to receive more data
Request some more data
We can apply reactive
streams principles to
serve a large file by a
server.
- Obviously, we can’t
just load the entire
file and serve the data
all at once.
Reactive Streams
HttpRequest HttpRequestSubscriber
HttpResponseWrapper
DecodedHttpResponse
StreamMessageCollector
HTTP/1.1 200 OK
wo
Netty APIs
Hello
POST /hello HTTP1.1
Host: foo.com
rld
HttpObject
Encoder
HttpRespo
nseDecode
r
POST /hello HTTP/1.1
Host: foo.com
Hello
HTTP/1.1 200 OK
world
Reactive Streams
HttpRequest HttpRequestSubscriber
HttpResponseWrapper
DecodedHttpResponse
StreamMessageCollector
subscribe()
Netty APIs
subscribe()
onNext() {
writeToNetty()
}
tryWrite()
Netty’s
encoder
invokes write
HttpObject
Encoder
HttpRespon
seDecoder
Reactive Streams
ClientFactory cf = ClientFactory.builder().build();
HttpRequest req = HttpRequest.of(HttpMethod.GET, "/hello");
HttpResponse res = WebClient.builder("http://foo.com")
.factory(
cf)
.build()
.
execute(req);
AggregatedHttpResponse aggRes = res.aggregate().join();
System.out.println(aggRes.contentUtf8());
- HttpResponse and HttpRequest implement Reactive Streams Publisher
- The HttpResponse#aggregate() call internally subscribes to the HttpResponse
and collects the full HttpResponse contents
High level view of what we’ll be looking at
1. Figure out where exactly we want to send
our request
2. Open and prepare a TCP connection
3. Send our request and receive the response
4. A small detour into decorators
GET /hello HTTP/1.1
Host: foo.com
HTTP/1.1 200 OK
Request
Response
A simple example (illustrated)
DefaultWebClient
HttpClientDelegate
Decorators
Name Server
DNS Query
Target Server
Acquire a connection
HttpRequest HttpResponse
subscribe()
read(), write()
ClientFactory
DefaultWebClient
Executed from an Event Loop starting from this point
Decorators
- What if we want to add more functionality to our client?
- What is an HttpClient
- Basically sends an HttpRequest, and returns an
HttpResponse
- HttpClientDelegate also implements HttpClient
public interface HttpClient {
HttpResponse execute(ClientRequestContext ctx, HttpRequest req)
throws Exception;
}
Decorators
Say that we want to log every request before sending it out
class MyLoggingClient implements HttpClient {
final HttpClient delegate;
MyLoggingClient (HttpClient delegate) {
this.delegate = delegate;
}
@Override
public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws
Exception {
logger.info("req: {}", req);
return delegate.execute(ctx, req);
}
}
HttpClient actualClient = ...;
HttpClient myLoggingClient = new MyLoggingClient( actualClient );
myLoggingClient .execute(ctx, req);
Although slightly different, the implementation is similar to Armeria’s built in LoggingClient
Decorators
- Makes use of the decorator design pattern.
- Provides a point of customization before the request is sent
- There are many decorators provided by default (to name just a few)
- RetryingClient
- MetricCollectingClient
- LoggingClient
- CircuitBreakerClient
- AuthClient
Decorators
ClientFactory cf = ClientFactory.builder().build();
HttpRequest req = HttpRequest.of(HttpMethod.GET, "/hello");
HttpResponse res = WebClient.builder("http://foo.com")
.factory(cf)
.decorator(LoggingClient.newDecorator())
.decorator(RetryingClient.newDecorator(RetryRule.failsafe()))
.build()
.execute(req);
AggregatedHttpResponse aggRes = res.aggregate().join();
System.out.println(aggRes.contentUtf8());
We can add built-in decorators as well as custom decorators to our clients
for additional functionality
A simple example (illustrated)
DefaultWebClient
HttpClientDelegate
Decorators
Name Server
DNS Query
Target Server
Acquire a connection
HttpRequest HttpResponse
subscribe()
read(), write()
ClientFactory
DefaultWebClient
Executed from an Event Loop starting from this point
So what is Armeria
- An asynchronous microservices framework
- Supports HTTP/1, HTTP/2, PROXY, TLS
- Built on top of Netty
- Compliant with Reactive Streams
- https://github.com/line/armeria

More Related Content

PPTX
Réseau sémaphore 7
PPTX
Projet IPTable
PDF
Android-Tp2: liste et adaptateurs
PDF
Authentification par certificat (clear box)
ODP
Diapo présentation cloud
PDF
Conception et implémentation d'un nouveau langage de programmation
PDF
Formation M2i - Windows Server 2022 : grande avancée ou simple appellation ?
PDF
mise en place de service dns sous ubuntu.pdf
Réseau sémaphore 7
Projet IPTable
Android-Tp2: liste et adaptateurs
Authentification par certificat (clear box)
Diapo présentation cloud
Conception et implémentation d'un nouveau langage de programmation
Formation M2i - Windows Server 2022 : grande avancée ou simple appellation ?
mise en place de service dns sous ubuntu.pdf

What's hot (20)

PDF
Cours frame relay
PPTX
expose type de reseaux.pptx
PDF
Mohamed youssfi support architectures logicielles distribuées basées sue les ...
PDF
Alphorm.com Support Formation Hacking & Sécurité Expert Vulnérabilités Web
PDF
fortinet et sécurité.pdf
PDF
CONCEPTION ET RÉALISATION D’UNE PLATEFORME D’ENSEIGNEMENT HYBRIDE D’UNE UNIV...
PDF
Cours les technologies WAN
PPTX
Chp5 - Sécurité des Services
PDF
configuration vpn-ipsec-routeur
PDF
Cours SNMP
PPTX
La sécurité des réseaux sans fil
PPTX
Codes Convolutifs
PDF
Support Web Services SOAP et RESTful Mr YOUSSFI
PDF
Connexion point à point (ppp, hdlc)
PPT
Soutenance Finale
PPTX
RADIUS ET TACACS+.pptx
DOC
mis en place dun vpn site à site
PDF
Architecture d'un réseau GSM 2G (Téléphonie Mobile)
PPTX
Chp3 - Les Services Web
PPTX
Introduction aux web services
Cours frame relay
expose type de reseaux.pptx
Mohamed youssfi support architectures logicielles distribuées basées sue les ...
Alphorm.com Support Formation Hacking & Sécurité Expert Vulnérabilités Web
fortinet et sécurité.pdf
CONCEPTION ET RÉALISATION D’UNE PLATEFORME D’ENSEIGNEMENT HYBRIDE D’UNE UNIV...
Cours les technologies WAN
Chp5 - Sécurité des Services
configuration vpn-ipsec-routeur
Cours SNMP
La sécurité des réseaux sans fil
Codes Convolutifs
Support Web Services SOAP et RESTful Mr YOUSSFI
Connexion point à point (ppp, hdlc)
Soutenance Finale
RADIUS ET TACACS+.pptx
mis en place dun vpn site à site
Architecture d'un réseau GSM 2G (Téléphonie Mobile)
Chp3 - Les Services Web
Introduction aux web services
Ad

Similar to Internals of how an Http Client works (Final) (3).pdf (20)

PPTX
Presentation2.pptx
PDF
Lets talk dns
PPTX
c5c1db8d-8375-4f17-bf6a-56ea5342e58d.pptx
PPTX
DNS(In_Linux).pptx
PDF
DNS Fundamentals Presentation_PANDI-2022.pdf
PPTX
Domain name system
PPTX
DNS for Developers - NDC Oslo 2016
PPTX
Domain Name System Explained
PPTX
IPv6 and the DNS, RIPE 73
PDF
Build Dynamic DNS server from scratch in C (Part1)
PDF
PPTX
How DNS works and How to secure it: An Introduction
PPTX
DNS_Tutorial 2.pptx
PPTX
Domain Name System and Dynamic Host Configuration Protocol.pptx
PPT
dns.04f.ppt
PPTX
DNS & HTTP overview
PDF
2 technical-dns-workshop-day1
PPTX
Domain name system
PDF
Domain Name System (DNS) Fundamentals
PPT
THBTHRTHRETHBTHJNRTFGNJRFTJNTNJMRTJNTTHJR
Presentation2.pptx
Lets talk dns
c5c1db8d-8375-4f17-bf6a-56ea5342e58d.pptx
DNS(In_Linux).pptx
DNS Fundamentals Presentation_PANDI-2022.pdf
Domain name system
DNS for Developers - NDC Oslo 2016
Domain Name System Explained
IPv6 and the DNS, RIPE 73
Build Dynamic DNS server from scratch in C (Part1)
How DNS works and How to secure it: An Introduction
DNS_Tutorial 2.pptx
Domain Name System and Dynamic Host Configuration Protocol.pptx
dns.04f.ppt
DNS & HTTP overview
2 technical-dns-workshop-day1
Domain name system
Domain Name System (DNS) Fundamentals
THBTHRTHRETHBTHJNRTFGNJRFTJNTNJMRTJNTTHJR
Ad

Recently uploaded (20)

PDF
Adobe Illustrator 28.6 Crack My Vision of Vector Design
PDF
Understanding Forklifts - TECH EHS Solution
PPTX
VVF-Customer-Presentation2025-Ver1.9.pptx
PPTX
Reimagine Home Health with the Power of Agentic AI​
PPTX
Oracle E-Business Suite: A Comprehensive Guide for Modern Enterprises
PDF
How to Choose the Right IT Partner for Your Business in Malaysia
PDF
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
PDF
Softaken Excel to vCard Converter Software.pdf
PDF
System and Network Administration Chapter 2
PDF
AI in Product Development-omnex systems
PPTX
Introduction to Artificial Intelligence
PDF
Digital Strategies for Manufacturing Companies
PPTX
Agentic AI : A Practical Guide. Undersating, Implementing and Scaling Autono...
PDF
Wondershare Filmora 15 Crack With Activation Key [2025
PPTX
Essential Infomation Tech presentation.pptx
PPTX
Odoo POS Development Services by CandidRoot Solutions
PDF
Navsoft: AI-Powered Business Solutions & Custom Software Development
PDF
Design an Analysis of Algorithms II-SECS-1021-03
PPTX
Agentic AI Use Case- Contract Lifecycle Management (CLM).pptx
PDF
Adobe Premiere Pro 2025 (v24.5.0.057) Crack free
Adobe Illustrator 28.6 Crack My Vision of Vector Design
Understanding Forklifts - TECH EHS Solution
VVF-Customer-Presentation2025-Ver1.9.pptx
Reimagine Home Health with the Power of Agentic AI​
Oracle E-Business Suite: A Comprehensive Guide for Modern Enterprises
How to Choose the Right IT Partner for Your Business in Malaysia
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
Softaken Excel to vCard Converter Software.pdf
System and Network Administration Chapter 2
AI in Product Development-omnex systems
Introduction to Artificial Intelligence
Digital Strategies for Manufacturing Companies
Agentic AI : A Practical Guide. Undersating, Implementing and Scaling Autono...
Wondershare Filmora 15 Crack With Activation Key [2025
Essential Infomation Tech presentation.pptx
Odoo POS Development Services by CandidRoot Solutions
Navsoft: AI-Powered Business Solutions & Custom Software Development
Design an Analysis of Algorithms II-SECS-1021-03
Agentic AI Use Case- Contract Lifecycle Management (CLM).pptx
Adobe Premiere Pro 2025 (v24.5.0.057) Crack free

Internals of how an Http Client works (Final) (3).pdf

  • 1. A look into the internals of an HTTP Client How Event Loops, Decorators, and Reactive Streams are used to create an HTTP Client.
  • 2. Modern day backend systems - Network communication is an important part of modern day systems. - External services - Databases - Clients - Especially, with the popularity of microservices architecture, inter-component communication is becoming more important.
  • 3. Case Study 1 - A well configured HTTP client can go a long way. - Imagine a request per thread server uses an HTTP client with a 10 second response timeout, without a circuit breaker. - The server may not be able to serve important requests. A thread-per-request server Push Service Payment Service User Requests - All threads are busy waiting for a push response HTTP Client
  • 4. Case Study 2 final MyResponse response; try { response = client.get("/"); } catch(IOException e) { // log the exception and pray this doesn’t happen more often } It would be nice if we actually could understand the stacktrace and know what the issue is.
  • 5. Before diving in - I’d like to share how a performant, extensible client is implemented by analyzing Armeria’s WebClient. - I imagine other http clients are implemented in a similar way. - I hope this will be useful even for those who won’t be using Armeria. - Based on the most recent Armeria tree at the moment of writing - Version 1.25.2 - It is not realistic to describe all code paths, I’ll be assuming only the most general code path with a few details left out.
  • 6. So what is Armeria - An asynchronous microservice framework - Supports HTTP/1, HTTP/2, PROXY, TLS - Built on top of Netty - Compliant with Reactive Streams - https://github.com/line/armeria
  • 7. A simple example ClientFactory cf = ClientFactory.builder().build(); HttpRequest req = HttpRequest.of(HttpMethod.GET, "/hello"); HttpResponse res = WebClient.builder("http://foo.com") .factory( cf) .build() . execute(req); AggregatedHttpResponse aggRes = res.aggregate().join(); System.out.println(aggRes.contentUtf8()); I will focus on what happens when HttpClient#execute is called
  • 8. A simple example (illustrated) DefaultWebClient HttpClientDelegate Decorators Name Server DNS Query Target Server Acquire a connection HttpRequest HttpResponse subscribe() read(), write() ClientFactory DefaultWebClient Executed from an Event Loop starting from this point
  • 9. High level view of what we’ll be looking at 1. Figure out where exactly we want to send our request 2. Open and prepare a TCP connection 3. Send our request and receive the response GET /hello HTTP/1.1 Host: foo.com HTTP/1.1 200 OK Request Response
  • 10. A simple example (illustrated) DefaultWebClient HttpClientDelegate Decorators Name Server DNS Query Target Server Acquire a connection HttpRequest HttpResponse subscribe() read(), write() ClientFactory DefaultWebClient Executed from an Event Loop starting from this point
  • 11. DNS Eventually we need to know where to send the request - foo.com -> 10.0.0.0 Some questions over the past years - There are too many DNS queries which are flooding the DNS server. Is there a way to reduce the number of DNS queries. - The target server IP will be replaced. How can we replace the servers gracefully so that requests aren’t failed. - If a DNS resolution fails, will the request fail? (should it fail?) The starting point is calling resolveAddress , which eventually calls AddressResolver#resolve to resolve a host name to an InetSocketAddress.
  • 12. DNS: The Basic Resolver Name Server Armeria foo.com 123.123.123.123 TTL: 1 day - Basically, we ask the Name Server if there is a record “foo.com” - If there is, an IP address is returned.
  • 13. DNS: The Basic Resolver Name Server Armeria foo.com 123.123.123.123 1. DelegatingDnsResolver - Delegates DNS resolution to the Netty layer resolver - Resolver.conf - Google - …
  • 14. DNS: The Basic Resolver What about DNS query flooding? - Too many DNS queries may degrade a DNS server’s performance. - If N clients request to M domains, that means N * M DNS resolutions may occur per each TTL We may need a cache layer which respects the DNS record TTL
  • 15. DNS: The Basic Resolver Query Name Server 123.123.123.123 1. CachingDnsResolver - Caches entries for each DNS query 2. DelegatingDnsResolver - Delegates DNS resolution to the Netty layer resolver 123.123.123.123 Armeria foo.com Checks cached entries
  • 16. DNS: The Basic Resolver - What about search domains? Wikipedia: A search domain is a domain used as part of a domain search list.The search list, as well as the local domain name, is used by a resolver to create a fully qualified domain name (FQDN) from a relative name If search domains are “a.com”, “b.com”, the searched domains will be: - mydomain.a.com. - mydomain.b.com. - mydomain.
  • 17. DNS: The Basic Resolver Query Name Server 123.123.123.123 1. SearchDomainDnsResolver - Queries for each search domain if exists 2. CachingDnsResolver - Caches entries for each DNS query 3. DelegatingDnsResolver - Delegates DNS resolution to the Netty layer resolver - foo.com.a.com. - foo.com.b.com. - foo.com. 123.123.123.123 Armeria Expands query to each search domain foo.com Checks cached entries
  • 18. DNS: The Basic Resolver Sometimes, users would like to take more control over dns resolution results - /etc/hosts
  • 19. DNS: The Basic Resolver Query Name Server 123.123.123.123 1. HostsFileDnsResolver - Checks whether the hosts file can be used for DNS resolution 2. SearchDomainDnsResolver - Queries for each search domain if exists 3. CachingDnsResolver - Caches entries for each DNS query 4. DelegatingDnsResolver - Delegates DNS resolution to the Netty layer resolver Check if hosts file has the entry - foo.com.a.com. - foo.com.b.com. - foo.com. 123.123.123.123 Armeria Expands query to each search domain foo.com Checks cached entries
  • 20. DNS: The Basic Resolver Query Name Server 123.123.123.123 1. DefaultDnsResolver - Puts everything together 2. HostsFileDnsResolver - Checks whether the hosts file can be used for DNS resolution 3. SearchDomainDnsResolver - Queries for each search domain if exists 4. CachingDnsResolver - Caches entries for each DNS query 5. DelegatingDnsResolver - Delegates DNS resolution to the Netty layer resolver Check if hosts file has the entry - foo.com.a.com. - foo.com.b.com. - foo.com. 123.123.123.123 Armeria Expands query to each search domain foo.com Checks cached entries
  • 21. - Q: If a request is made on “foo.com” but a DNS resolution fails, should we fail the request overall? - A: It depends! Obviously, the user should be able to configure this. - But what is the most reasonable default behavior? The current default implementation: 1. If there are no previously cached entries, the request will fail. 2. If there is a previously cached entry, try to use the previous value. Meanwhile, try to refresh the cached entry in the background. Note: We actually recommend using the EndpointGroup API for this scenario, but this is a separate topic. DNS: The Basic Resolver
  • 22. DNS: The Basic Resolver Query Name Server 123.123.123.123 1. RefreshingAddressResolver - Refreshes addresses in the background and serves cached dns entries. 2. DefaultDnsResolver - Puts everything together 3. HostsFileDnsResolver - Checks whether the hosts file can be used for DNS resolution 4. SearchDomainDnsResolver - Queries for each search domain if exists 5. CachingDnsResolver - Caches entries for each DNS query 6. DelegatingDnsResolver - Delegates DNS resolution to the Netty layer resolver Check if hosts file has the entry - foo.com.a.com. - foo.com.b.com. - foo.com. 123.123.123.123 Armeria Expands query to each search domain foo.com Checks cached entries Refreshes cached entries in the background
  • 23. DNS FAQ - There are too many DNS queries which are flooding the DNS server. Is there a way to reduce the number of DNS queries. - We can check if the number of search domains (ndots) is causing too many DNS queries - We can check if the dns cache is configured correctly - The target server IP will be replaced. How can we replace the servers gracefully so that requests aren’t failed. - We can temporarily clamp the DNS TTL to a smaller value while migration takes place - We could also check if the RefreshingAddressResolver cache configurations. - If a DNS resolution fails, will the request fail? (should it fail?) - By default, it won’t fail. The previous DNS query result will be used by default even if the TTL passed.
  • 24. High level view of what we’ll be looking at 1. Figure out where exactly we want to send our request 2. Open and prepare a TCP connection 3. Send our request and receive the response GET /hello HTTP/1.1 Host: foo.com HTTP/1.1 200 OK Request Response
  • 25. A simple example (illustrated) DefaultWebClient HttpClientDelegate Decorators Name Server DNS Query Target Server Acquire a connection HttpRequest HttpResponse subscribe() read(), write() ClientFactory DefaultWebClient Executed from an Event Loop starting from this point
  • 26. Connection Pooling How many connections should be opened in the above scenario? - HTTP1.1: If keep alive is enabled, a connection handle requests sequentially without closing the connection. (assuming pipelining is disabled) - HTTP2: A connection handle handle MAX_CONCURRENT_STREAMS number of requests concurrently Opening a connection consumes resources/latency, so Armeria reuses connections as much as possible. WebClient client = WebClient.of(); client.get("http://1.2.3.4"); client.get("http://1.2.3.4"); client.get("http://1.2.3.4");
  • 27. Connection Pooling Endpoint - A connection to (10.0.0.0) cannot be shared with a connection to (10.0.0.1) Protocol - A connection using HTTP2 cannot be shared with a connection using HTTP1 Event Loop - Armeria uses Netty under the hood for network IO - Netty assumes a single connection is handled by a single event loop - This means a connection for EventLoopA cannot be shared with a connection for EventLoopB - We want to share connections as much as possible - A connection is sharable per (Endpoint, Protocol, Event Loop) for Armeria.
  • 28. Event Loops Event Loop (Wikipedia) the event loop is a programming construct or design pattern that waits for and dispatches events or messages in a program
  • 29. Event Loops class MyEventLoop { final Queue<Runnable> taskQueue = new ArrayDeque<>(); final Thread thread = new Thread() { @Override public void run() { while (true) { Runnable runnable = taskQueue.poll(); if (runnable != null) { runnable.run(); } } } }; MyEventLoop() { thread.start(); } public void schedule(Runnable runnable) { taskQueue.add(runnable); } } MyEventLoop myEventLoop = … myEventLoop.schedule(() -> System.out.println(“Hello”)) A simple event loop implemented in Java
  • 30. Event Loops Without event loops, a simple solution is locking int inc = 0; ReentrantLock lock = new ReentrantLock(); private void increment() { lock.lock(); try { inc++; } finally { lock.unlock(); } } void main() { for (int i = 0; i < 100; i++) { ForkJoinPool.commonPool().submit(() -> increment()); } await().until(() -> inc == 100); assert inc == 100; }
  • 31. Event Loops Provides a much simpler way to do concurrent programming. int inc = 0; EventLoop eventLoop = ...; private void increment() { if (!eventLoop.inEventLoop()) { eventLoop.execute(() -> increment()); return; } inc++; } void main() { for (int i = 0; i < 100; i++) { ForkJoinPool.commonPool().submit(() -> increment()); } await().until(() -> inc == 100); assert inc == 100; } Note: although the queue add operation may also lock, it will do so for a short time only
  • 32. Event Loops How do we handle two concurrent requests to a single TCP connection? 1. We can acquire a lock before writing to a connection 2. A single channel (connection) is handled by a single EventLoop - We don’t have to worry about race conditions when writing to the wire POST /a HTTP/1.1 Host: foo.com GET /b HTTP/1.1 Host: foo.com Content-Length: 5 Hello POST /a HTTP/1.1 Host: foo.com Content-Length: 5 Hello GET /b HTTP/1.1 Host: foo.com
  • 33. Connection Pooling - Each ClientFactory maintains a connection pool - Each request is pre-assigned an Event Loop - Before a connection is acquired, the call is executed from an event loop - In order to acquire a connection - We check if an existing connection can be used - We try to acquire a new (or pending) connection EventLoop EventLoop EventLoop HttpChannelPool HttpChannelPool HttpChannelPool HTTP2 10.0.0.1 HTTP2 10.0.0.2 HTTP1 10.0.0.3 Connections Connections Connections ClientFactory — HttpClientDelegate WebClient WebClient WebClient
  • 34. Connection Pooling How many connections should be opened in the above scenario? (given that the request is made concurrently) - 0 new connections if there are reusable connections available - Otherwise: - 3 connections if HTTP1 - 1 connection if HTTP2 What if there are too many connections being created? - Check which protocol we are using - If HTTP2, try tuning the MAX_CONCURRENT_STREAMS value - Check how many endpoints are behind foo.com WebClient client = WebClient.of(); client.get("http://1.2.3.4"); client.get("http://1.2.3.4"); client.get("http://1.2.3.4");
  • 35. A simple example (illustrated) DefaultWebClient HttpClientDelegate Decorators Name Server DNS Query Target Server Acquire a connection HttpRequest HttpResponse subscribe() read(), write() ClientFactory DefaultWebClient Executed from an Event Loop starting from this point
  • 36. Session Protocol Negotiation This step prepares each new connection so that we can start sending HTTP requests. The trigger is when a new channel connection is attempted, which eventually invokes HttpClientPipelineConfigurator#connect. - It mostly involves adding the appropriate Netty handlers to the pipeline. Each protocol has different steps for preparing a connection. - h1c://foo.com - h2c://foo.com - http://foo.com (Note that I won’t be discussing https, but it is a simple extension of cleartext negotiation)
  • 37. Session Protocol Negotiation h1c://foo.com - Add HTTP message serializers/deserializers to the Netty Pipeline - There is no need for protocol negotiation for HTTP1 - Open a connection, send a request, and receive a response. - Session protocol negotiation is done immediately and the session future is completed.
  • 38. Session Protocol Negotiation h2c://foo.com - Add HTTP2 message serializers/deserializers to the Netty Pipeline - Send a connection preface and a settings frame - Receive a connection preface and a settings frame - The expected frames have been received and the session future is completed - we’re now free to send HTTP2 frames
  • 39. Session Protocol Negotiation http://foo.com What if server-side doesn’t support HTTP2? - We fall back to HTTP1 Steps 1. Send HTTP2 preface and settings frame assuming prior knowledge that the server supports HTTP2 2. The server sends an error response because it actually doesn’t support HTTP2 3. Close the connection, and store a cache entry that the server doesn’t support HTTP2 4. Open a new connection and start using HTTP1
  • 40. Session Protocol Negotiation - In the end, a sessionPromise is completed and we have a new connection ready for use - All we now have to do is send our HttpRequest and receive a HttpResponse
  • 41. High level view of what we’ll be looking at 1. Figure out where exactly we want to send our request 2. Open and prepare a TCP connection 3. Send our request and receive the response GET /hello HTTP/1.1 Host: foo.com HTTP/1.1 200 OK Request Response
  • 42. A simple example (illustrated) DefaultWebClient HttpClientDelegate Decorators Name Server DNS Query Target Server Acquire a connection HttpRequest HttpResponse subscribe() read(), write() ClientFactory DefaultWebClient Executed from an Event Loop starting from this point
  • 43. Sending requests and receiving responses ClientFactory cf = ClientFactory.builder().build(); HttpRequest req = HttpRequest.of(HttpMethod.GET, "/hello"); HttpResponse res = WebClient.builder("http://foo.com" ) .factory( cf) .build() . execute(req); System.out.println(res); ^ This prints immediately even though we didn’t actually send the request out! AggregatedHttpResponse aggRes = res.aggregate().join(); System.out.println(aggRes.contentUtf8()); Going back to the original code
  • 44. Sending requests and receiving responses HttpRequestWriter requestWriter = HttpRequest.streaming(HttpMethod.POST, "/"); HttpResponse response = WebClient.builder("foo.com") .build() .execute( requestWriter); requestWriter.write(HttpData.ofAscii("Hello ")); Thread.sleep(1000) requestWriter.write(HttpData.ofAscii("World")); requestWriter.close(); Actually, we can even stream data to the HttpRequest. - In the following sample code, we acquire a HttpResponse object even though we didn’t write the request fully yet.
  • 45. Reactive Streams From the website: Reactive Streams is an initiative to provide a standard for asynchronous stream processing with non-blocking back pressure Publisher Subscriber subscribe() Publisher Subscriber request(1) Publisher Subscriber onNext(message) 1. A subscriber subscribes to a publisher 2. The subscriber can process 1 request 3. The publisher sends 1 message when available Publisher Subscriber request(1) Publisher Subscriber onNext(message) 4. The subscriber can process 1 request 5. The publisher sends 1 message when available
  • 46. Reactive Streams Subscriber<Integer> subscriber = new Subscriber<Integer>() { private Subscription s; @Override public void onSubscribe(Subscription s) { this.s = s; s.request(1); } @Override public void onNext(Integer integer) { submitAsyncTask(integer).thenRun(() -> s.request(1)); } ... }; Flux.just(1,2,3).subscribe(subscriber); onSubscribe(s) request(1) onNext(1) request(1) onNext(2) request(1) onNext(3) Publisher Subscriber
  • 47. Reactive Streams 10G file on disk 1G RAM Server Network Request some data Send a small chunk of data Send a small chunk of data Ready to receive more data Request some more data We can apply reactive streams principles to serve a large file by a server. - Obviously, we can’t just load the entire file and serve the data all at once.
  • 48. Reactive Streams HttpRequest HttpRequestSubscriber HttpResponseWrapper DecodedHttpResponse StreamMessageCollector HTTP/1.1 200 OK wo Netty APIs Hello POST /hello HTTP1.1 Host: foo.com rld HttpObject Encoder HttpRespo nseDecode r POST /hello HTTP/1.1 Host: foo.com Hello HTTP/1.1 200 OK world
  • 49. Reactive Streams HttpRequest HttpRequestSubscriber HttpResponseWrapper DecodedHttpResponse StreamMessageCollector subscribe() Netty APIs subscribe() onNext() { writeToNetty() } tryWrite() Netty’s encoder invokes write HttpObject Encoder HttpRespon seDecoder
  • 50. Reactive Streams ClientFactory cf = ClientFactory.builder().build(); HttpRequest req = HttpRequest.of(HttpMethod.GET, "/hello"); HttpResponse res = WebClient.builder("http://foo.com") .factory( cf) .build() . execute(req); AggregatedHttpResponse aggRes = res.aggregate().join(); System.out.println(aggRes.contentUtf8()); - HttpResponse and HttpRequest implement Reactive Streams Publisher - The HttpResponse#aggregate() call internally subscribes to the HttpResponse and collects the full HttpResponse contents
  • 51. High level view of what we’ll be looking at 1. Figure out where exactly we want to send our request 2. Open and prepare a TCP connection 3. Send our request and receive the response 4. A small detour into decorators GET /hello HTTP/1.1 Host: foo.com HTTP/1.1 200 OK Request Response
  • 52. A simple example (illustrated) DefaultWebClient HttpClientDelegate Decorators Name Server DNS Query Target Server Acquire a connection HttpRequest HttpResponse subscribe() read(), write() ClientFactory DefaultWebClient Executed from an Event Loop starting from this point
  • 53. Decorators - What if we want to add more functionality to our client? - What is an HttpClient - Basically sends an HttpRequest, and returns an HttpResponse - HttpClientDelegate also implements HttpClient public interface HttpClient { HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Exception; }
  • 54. Decorators Say that we want to log every request before sending it out class MyLoggingClient implements HttpClient { final HttpClient delegate; MyLoggingClient (HttpClient delegate) { this.delegate = delegate; } @Override public HttpResponse execute(ClientRequestContext ctx, HttpRequest req) throws Exception { logger.info("req: {}", req); return delegate.execute(ctx, req); } } HttpClient actualClient = ...; HttpClient myLoggingClient = new MyLoggingClient( actualClient ); myLoggingClient .execute(ctx, req); Although slightly different, the implementation is similar to Armeria’s built in LoggingClient
  • 55. Decorators - Makes use of the decorator design pattern. - Provides a point of customization before the request is sent - There are many decorators provided by default (to name just a few) - RetryingClient - MetricCollectingClient - LoggingClient - CircuitBreakerClient - AuthClient
  • 56. Decorators ClientFactory cf = ClientFactory.builder().build(); HttpRequest req = HttpRequest.of(HttpMethod.GET, "/hello"); HttpResponse res = WebClient.builder("http://foo.com") .factory(cf) .decorator(LoggingClient.newDecorator()) .decorator(RetryingClient.newDecorator(RetryRule.failsafe())) .build() .execute(req); AggregatedHttpResponse aggRes = res.aggregate().join(); System.out.println(aggRes.contentUtf8()); We can add built-in decorators as well as custom decorators to our clients for additional functionality
  • 57. A simple example (illustrated) DefaultWebClient HttpClientDelegate Decorators Name Server DNS Query Target Server Acquire a connection HttpRequest HttpResponse subscribe() read(), write() ClientFactory DefaultWebClient Executed from an Event Loop starting from this point
  • 58. So what is Armeria - An asynchronous microservices framework - Supports HTTP/1, HTTP/2, PROXY, TLS - Built on top of Netty - Compliant with Reactive Streams - https://github.com/line/armeria