Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For...

36
©2016 Harmonic Inc. All rights reserved worldwide. Using asynchronous I/O in Rust Matthieu Wipliez Upstream Studies Engineer September, 17th, 2016 (was: Techniques for writing concurrent applications with asynchronous I/O)

Transcript of Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For...

Page 1: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

©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)

Page 2: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 3: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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)

Page 4: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 5: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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) });}

Page 6: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 7: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 8: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

8 ©2016 Harmonic Inc. All rights reserved worldwide.

Asynchronous I/O in Web servers

© 2016 DreamHost

Page 9: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 10: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

10 ©2016 Harmonic Inc. All rights reserved worldwide.

Side note: implementing polling for scalability

© 2002 Davide Libenzi

poll

epoll

Page 11: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 12: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

12 ©2016 Harmonic Inc. All rights reserved worldwide.

Asynchronous I/O in Rust

Page 13: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 14: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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...

Page 15: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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())

}

}

Page 16: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 17: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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?

Page 18: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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:

Page 19: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 20: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 21: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 22: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 23: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 24: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 25: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 26: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 27: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

...

Page 28: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 29: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 30: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 31: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

31 ©2016 Harmonic Inc. All rights reserved worldwide.

Request handling with Tokio service architecture

incoming

parse

handle

serialize

outgoing

transport

service

Page 32: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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) { // ... }}

Page 33: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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)

Page 34: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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

Page 35: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

35 ©2016 Harmonic Inc. All rights reserved worldwide.

Thank You!

Questions?

Code for the examples: https://github.com/matt2xu/rustfest2016

Page 36: Using asynchronous I/O in Rust · Non-blocking I/O in Rust standard library • API support – For structures TcpListener, TcpStream, UdpSocket… – Function set_non_blocking (since

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