Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For...
Transcript of Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For...
©2016 Harmonic Inc. All rights reserved worldwide.
Using asynchronous I/O in Rust
Matthieu WipliezUpstream Studies Engineer
September, 17th, 2016
(was: Techniques for writing concurrent applications with asynchronous I/O)
2 ©2016 Harmonic Inc. All rights reserved worldwide.
• About you: background and experience with asynchronous I/O?
• This talk
– Asynchronous I/O for networking
– Rust code examples accessible on GitHub
• Author of edge-rs: Web framework in Rust– Based on Hyper
About: you, this talk, and the speaker
3 ©2016 Harmonic Inc. All rights reserved worldwide.
• In Rust, two traits:– Read– Write
• Typical use:
Synchronous (blocking) I/O
let mut buf = [0; 4096];let num_bytes = try!(stream.read(&mut buf));// ...let mut response = Vec::new();// ...stream.write_all(&response)
4 ©2016 Harmonic Inc. All rights reserved worldwide.
• One or more processes listen on a port
• Spawn one thread per connection
Synchronous I/O architecture
...Thread 1 Thread 2 Thread n
connection 1 connection 2 connection n
5 ©2016 Harmonic Inc. All rights reserved worldwide.
Implementation of a synchronous server in Rust
let listener = TcpListener::bind("0.0.0.0:113").unwrap();
for stream in listener.incoming() { let stream = stream.unwrap(); thread::spawn(move || { // connection handler handle_connection (stream) });}
6 ©2016 Harmonic Inc. All rights reserved worldwide.
Asynchronous I/O architecture
Thread 1
Event loop
sockets
Thread 2
Event loop
sockets
Thread 3
Event loop
sockets
Thread 4
Event loop
sockets
polling polling polling polling
7 ©2016 Harmonic Inc. All rights reserved worldwide.
• Throughput: number of requests per second
• Latency: time to serve a request
• Memory consumption
Advantages of asynchronous I/O
8 ©2016 Harmonic Inc. All rights reserved worldwide.
Asynchronous I/O in Web servers
© 2016 DreamHost
9 ©2016 Harmonic Inc. All rights reserved worldwide.
“The classic Unix way to wait for I/O events on multiple file descriptors is with the select() and poll() system calls.” [Jonathan Corbet http://lwn.net/Articles/14168/]
30+ years of asynchronous I/O in Unix
select4.2 BSD
1983
pollSVR3 Unix
1986
pollLinux 2.1.23
1997
epollLinux 2.5.44
2002...
kqueueFreeBSD 4.1
2000
10 ©2016 Harmonic Inc. All rights reserved worldwide.
Side note: implementing polling for scalability
© 2002 Davide Libenzi
poll
epoll
11 ©2016 Harmonic Inc. All rights reserved worldwide.
• Do not block the current thread!
• Otherwise, here is what happens:
Something to keep in mind with asynchronous I/O
time
synchronous asynchronous
12 ©2016 Harmonic Inc. All rights reserved worldwide.
Asynchronous I/O in Rust
13 ©2016 Harmonic Inc. All rights reserved worldwide.
Non-blocking I/O in Rust standard library
• API support
– For structures TcpListener , TcpStream , UdpSocket…
– Function set_non_blocking (since Rust 1.9)
• Semantics
– When a read or a write “needs to block to complete, but the blocking operation was requested
to not occur”, returns ErrorKind::WouldBlock
• Limitations: no polling API
14 ©2016 Harmonic Inc. All rights reserved worldwide.
• Thin wrapper around underlying libraries
• Low-level, zero allocation
• Callbacks with tokens
mio: Support for asynchronous polling
mio
epoll kqueue I/O Completion Ports
Application
higher level
lower level...
15 ©2016 Harmonic Inc. All rights reserved worldwide.
const SERVER: Token = Token(10_000_000);
const CLIENT: Token = Token(10_000_001);
struct EchoConn {
sock: TcpStream,
buf: Option<ByteBuf>,
mut_buf: Option<MutByteBuf>,
token: Option<Token>,
interest: Ready
}
type Slab<T> = slab::Slab<T, Token>;
impl EchoConn {
fn new(sock: TcpStream) -> EchoConn {
EchoConn {
sock: sock,
buf: None,
mut_buf: Some(ByteBuf::mut_with_capacity(2048)),
token: None,
interest: Ready::hup()
}
}
fn readable(&mut self, event_loop: &mut EventLoop<Echo>) ->
io::Result<()> {
…
}
Using mio directly?
fn writable(&mut self, event_loop: &mut EventLoop<Echo>) -> io::Result<()> {
let mut buf = self.buf.take().unwrap();
match self.sock.try_write_buf(&mut buf) {
Ok(None) => {
debug!("client flushing buf; WOULDBLOCK");
self.buf = Some(buf);
self.interest.insert(Ready::writable());
}
Ok(Some(r)) => {
debug!("CONN : we wrote {} bytes!", r);
self.mut_buf = Some(buf.flip());
self.interest.insert(Ready::readable());
self.interest.remove(Ready::writable());
}
Err(e) => debug!("not implemented; client err={:?}", e),
}
assert!(self.interest.is_readable() || self.interest.is_writable(), "actual={:?}", self.interest);
event_loop.reregister(&self.sock, self.token.unwrap(), self.interest,
PollOpt::edge() | PollOpt::oneshot())
}
…
}
16 ©2016 Harmonic Inc. All rights reserved worldwide.
Using mio in practice
• If possible, use a higher-level library on top of mio
– Such as: rotor, mioco, coio, tokio
mio
epoll kqueue I/O Completion Ports
Applicationhigher level
lower level...
Middleware
17 ©2016 Harmonic Inc. All rights reserved worldwide.
• Distinctive features:
– No need to register interest
– Futures-based asynchronous I/O
• Futures (a.k.a promise)
– Deferred computation
– Solves “callback hell”
Why Tokio?
18 ©2016 Harmonic Inc. All rights reserved worldwide.
Before Tokio: callbacks and interest
pub trait Handler<T: Transport> {
fn on_request(&mut self, request: Request<T>) -> Next;
fn on_request_readable (&mut self, request: &mut http::Decoder<T>) -> Next;
fn on_response(&mut self, response: &mut Response) -> Next;
fn on_response_writable (&mut self, response: &mut http::Encoder<T>) -> Next;
}
For instance Handler trait in Hyper:
19 ©2016 Harmonic Inc. All rights reserved worldwide.
From callbacks to futures
if let Ready(addr) = resolve(url) { if let Ready(tcp) = connect(&addr) { if let Ready(data) = download(tcp) { // ... } }}
let addr = resolve(url);let tcp = addr.and_then(|addr| connect(&addr));let data = tcp.and_then(|conn| download(conn));// ...
Before
After
20 ©2016 Harmonic Inc. All rights reserved worldwide.
Definition of Future
fn poll(&mut self) -> Result<Async<T>, E>;
error? Err(error)yes
Async::NotReadydone?no
Async::Ready(value)
yes
21 ©2016 Harmonic Inc. All rights reserved worldwide.
Non-blocking iterator: Stream
fn poll(&mut self) -> Result<Async<Option<T>>, E>;
error? Err(error)yes
Async::NotReadydone?no
Async::Ready(Some(value))
yes
more? Async::Ready(None)noyes
22 ©2016 Harmonic Inc. All rights reserved worldwide.
• Do not block a future. Never wait in a future!
• What if your program needs to:
– Do compute-intensive work?
– Call blocking functions?
• Then: use a thread pool– A number of worker threads executing jobs
– For futures: futures_cpupool
Futures and (not) blocking
23 ©2016 Harmonic Inc. All rights reserved worldwide.
Tokio stack
low-level event loop (mio)
transport (tokio-proto)
future-based event loop (tokio-core)
service (tokio-service)higher level
lower level
24 ©2016 Harmonic Inc. All rights reserved worldwide.
History of Tokio and futures
initial commit of mio
August, 2014
tokio-core
August, 2016
release of Rust 1.0
May, 2015
initial commit of futures-rs
March, 2016
futures-rs
tokio-rs
25 ©2016 Harmonic Inc. All rights reserved worldwide.
use tokio_core::reactor::Core;
let mut event_loop = Core::new().unwrap();
// let listener = ...
let future = listener.incoming().for_each(|_| { // ...});
event_loop.run(future).unwrap();
Event loop in Tokio
Thread
Event loop
sockets
26 ©2016 Harmonic Inc. All rights reserved worldwide.
use tokio_core::reactor::Core;
let mut event_loop = Core::new().unwrap();let handle = event_loop.handle();
// ...
let future = listener.incoming().for_each(|(stream, _)| { // ... handle.spawn(MyHandler::new(stream)); Ok(())});
event_loop.run(future).unwrap();
Handling connections
Thread
Event loop
sockets
27 ©2016 Harmonic Inc. All rights reserved worldwide.
• Goal: identify the user of a particular TCP connection
• Example: who is connected on freenode.net on IRC from 43.124.2.250:45387 ?
Use case: Identification Protocol (RFC 1413)
Ident client
freenode.net
Ident server
43.124.2.250
45387, 6667
45387, 6667: UNIX: matt
...
28 ©2016 Harmonic Inc. All rights reserved worldwide.
impl Future for IdentHandler { type Item = (); type Error = io::Error;
fn poll(&mut self) -> Poll<(), io::Error> { if try!(self.stream.read_line(&mut self.request)) > 0 { let reply = self.handle(); try!(self.stream.get_ref().write_all(reply.as_ref()));
self.request.clear(); return Ok(Async::NotReady); }
Ok(Async::Ready(())) // EOF }}
First solution using tokio-core
29 ©2016 Harmonic Inc. All rights reserved worldwide.
• Calling read_line would block
– BufRead::read_line -> BufRead::read_until -> BufRead::fill_buf -> Read::read
– const DEFAULT_BUF_SIZE: usize = 8 * 1024;
• Calling write_all could block
– write would not block, but it may only write a subset of data
• Possible solutions: intermediate buffers, streams
Fixing our implementation
30 ©2016 Harmonic Inc. All rights reserved worldwide.
Another solution using Tokio service
low-level event loop (mio)
transport (tokio-proto)
future event loop (tokio-core)
service (tokio-service)higher level
lower level
31 ©2016 Harmonic Inc. All rights reserved worldwide.
Request handling with Tokio service architecture
incoming
parse
handle
serialize
outgoing
transport
service
32 ©2016 Harmonic Inc. All rights reserved worldwide.
impl Parse for MyParser { type Out = Request;
fn parse(&mut self, buf: &mut BlockBuf) -> Option<Request> { // ... }}
Implement the transport
impl Serialize for MySerializer { type In = Response;
fn serialize(&mut self, frame: Response, buf: &mut BlockBuf) { // ... }}
33 ©2016 Harmonic Inc. All rights reserved worldwide.
Putting it all together (simplified)
use tokio::service::simple_service;
let service = simple_service(move |request: String| { let query: ident::Query = request.parse().unwrap(); let reply = query.process(&ip);
// return future from reply futures::finished(reply.to_string())});
let transport = new_ident_transport (stream);pipeline::Server::new(service, transport)
34 ©2016 Harmonic Inc. All rights reserved worldwide.
• Leads to higher performance
• Rapidly evolving ecosystem
• Support in Rust at a turning point
• Next steps
Conclusion: Asynchronous I/O
35 ©2016 Harmonic Inc. All rights reserved worldwide.
Thank You!
Questions?
Code for the examples: https://github.com/matt2xu/rustfest2016
36 ©2016 Harmonic Inc. All rights reserved worldwide.
• About futures:– Introduction: https://aturon.github.io/blog/2016/08/11/futures/– Details about the design: https://aturon.github.io/blog/2016/09/07/futures-design/
• About Tokio:– Announcing Tokio: https://medium.com/@carllerche/announcing-tokio-df6bb4ddb34– Integration of Tokio with futures: http://aturon.github.io/blog/2016/08/26/tokio/
• Threads and processes on Linux:– http://stackoverflow.com/questions/807506/threads-vs-processes-in-linux
• Comparison of Web servers:– https://help.dreamhost.com/hc/en-us/articles/215945987-Web-server-performance-comparison
Bibliography