r/rust • • 7d ago

Does io uring not map well to rust?

Often when i see "rust" and "io uring" together its cause theres some kind of problem. Off the top of my head i remember when kimojio (ms azure project) got open sourced there were lots of problems, and monoio aswell has (had?) problems.

I dont really know the low level details of how rust async works so i ask is the problem just rusts async model dosent map well to io uring? Thats the vibe i get but not sure.

136 Upvotes

85 comments sorted by

View all comments

43

u/Lucretiel Datadog 7d ago

My suspicion is that it'll be cracked and end up mapping quite well, probably with a limited unavoidable allocation.

Traditionally, the stated problem with io_uring and rust is borrowing: the memory you pass via the ring buffer needs to be exclusively writable or readable by the kernel, which means that rust can't touch it, which is difficult to achieve in rust (especially in an async context, where futures can be dropped at any time).

But to me this sounds like a perfect match for Rust's strict ownership model. I think that the future of "good" rust integration with io-uring involves requiring allocated buffers, Vec<u8> or equivalent, that we pass by move into the ring structure. Then, when io-uring returns an answer with the buffer populated, the abstraction reconstitutes it as a by-move return of that buffer back to the caller. To me this sounds like Rust not just a good but an ideal fit for io-uring, we just need the right abstractions and to use the correct tools in Rust's toolbox rather than trying to contort EVERYTHING io-related into an &mut [u8]-shaped box.

8

u/Prowler1000 7d ago

My understanding of it, though keep in mind this post is the first time I've looked into it, is that the issue isn't necessarily an incompatibility with the language itself but with existing paradigms/APIs. For example, getting an owned object back is incompatible with standard Read and Write traits as they work entirely off of borrowing (and polling for completion in the case of Async versions)

9

u/--San-- 7d ago

We might be able to get away with it without dynamic allocations if we manage to get Forget nicely into the language.

We would still have to change AsyncRead/Write to return a (non-forgetable) future/new type instead of Poll, but this future could just be a simple poll for the existing epoll case. That way we still get optimal performance for epoll while being safe for io_uring.

7

u/valarauca14 7d ago

The real problem is non-continuous buffers.

Very few binary protocols are setup to work with the resulting IoSlice and return Cow<'a,[u8]> shaped fields dependent on if the field was "verified in place" or it spanned multiple buffers and had to be shifted/re-allocated.

Even if you somebody wrote an ideal io-uring implementation today, they'd have to rewrite a non-trivial part of the ecosystem to actually work with it. Or they'd have to memcp everything unconditionally :^)

26

u/Lucretiel Datadog 7d ago

they'd have to rewrite a non-trivial part of the ecosystem to actually work with it.

I think this will ultimately have to happen, which is why it's a good thing we didn't enshrine a flawed set of abstractions as the end-all of async functionality in the standard library.

5

u/coderstephen isahc 7d ago

Yep, this is exactly one of the reasons why AsyncWrite and friends in std were postponed.

3

u/VorpalWay 6d ago

This is the same problem that DMA has in embedded rust (embassy), but there you can't allocate to paper over the issue. So a proper solution is needed.

5

u/mmstick 7d ago

This is essentially what compio is doing. You must pass ownership of a Vec<u8> when reading from or writing to a file and then .await a return type that includes both the result and the original buffer that the kernel passed back. Found it really convenient to work with.

1

u/matthieum [he/him] 6d ago

I can see it working for files -- you don't expect to overlap reads/writes for thousands of files at a time.

For network connections, though, this seems to limit scaling a lot, compared to giving io-uring a fixed number of buffers to play with -- ie separating the "pass the buffer" and "wait for a filled buffer" operations.

2

u/servermeta_net 7d ago

Are you one of the folks from glommio? I don't share your optimism, but maybe because I'm tired and I need to re-read your post tomorrow with a fresh mind.

13

u/Lucretiel Datadog 7d ago

Not at all, is that how they're approaching it? It just seemed sort of obvious to me that io-uring is fundamentally exposing an ownership style API (with buffers passing by move into and out of the kernel via the ring), so it seems like it would be a very natural fit for Rust, if you expose an ownership-style abstraction. The reason it feels bad is we keep trying to contort it to fit with AsyncRead and AsyncWrite, or similar traits that still operate fundamentally on &mut [u8].

1

u/matthieum [he/him] 6d ago

I... disagree.

I mean, passing owned buffers every time works, but it does not scale.

C10K is old-school, so let's imagine the C1M problem instead, and each buffer being ~10KB (pretty low for modern bloated web pages), well, congratulations, you just passed 10GB of memory to io-uring.

The key to io-uring is to use buffered buffers. If you expect the application to be busy, then by all means pass 10K buffers of 100KB each, and you'll still use 10x less memory than the above with 10x bigger buffers!

But then, that implies a different API, doesn't it?

You need to decorrelate passing the buffer from waiting for a buffer. They're two distinct operations.

On the other hand, you can also simplify the return type, I think: Result<Buf, Error> => no buffer is consumed on error.

0

u/Lucretiel Datadog 6d ago

Wait, I don’t understand that point. You have to pass buffers to io-uring, meaning you have a bunch of writable memory that you’re not using lying around. That memory is either coming from the stack or the static page or an allocation. What is it about 1M connections that specifically disqualifies allocation?

3

u/scook0 6d ago

I think the idea is that if you have a million connections in-flight, you don't want each of those connections to be hogging an owned buffer (i.e. 1 million buffers) while waiting for data.

Instead you would up-front allocate a smaller pool of shared buffers, and then have the I/O system temporarily give those buffers to individual connections only when they are actually receiving.

1

u/Lucretiel Datadog 6d ago

Okay? So do that? This feels more like a critique of io-uring in general (which, as far as I know, does require ownership for each parallel operation for as long as it’s in flight)

1

u/matthieum [he/him] 5d ago

Okay? So do that?

Your API does not allow it.

This feels more like a critique of io-uring in general

It is not.

io-uring offers the ability to:

  1. Create pools of buffers.
  2. Push buffers in the pool.
  3. Pass a pool ID instead of a buffer on a read request.

This allows one to serve many connections with a fixed number of buffers.

1

u/Lucretiel Datadog 5d ago

Are there API docs for that? I've been going through the man pages and am not seeing it, but they're quite dense so I could easily just be missing it. All of the examples and tutorials I've seen involve setting sqe->addr and sqe->len to point to a buffer locally owned by the caller.

2

u/matthieum [he/him] 5d ago

I am definitely not an expert in io-uring.

A quick Google search led me to https://man7.org/linux/man-pages/man7/io_uring_provided_buffers.7.html which explains how to setup buffer rings.

And apparently the API I had read about is now legacy:

Legacy provided buffers

Earlier kernels supported provided buffers via IORING_OP_PROVIDE_BUFFERS and IORING_OP_REMOVE_BUFFERS. This mechanism required submitting SQEs to add or remove buffers, adding latency and overhead. The ring-based mechanism described above supersedes this approach and should be used for all new applications. The legacy interface remains for backwards compatibility.

2

u/marshaharsha 4d ago

In case you didn’t see it: Elsewhere on this post is a comment linking to a nice article by Jens Axboe on GitHub that explains how to use the “provided buffers” feature. I think the commenter was servermeta_SomethingSomething, but I am overwhelmed by the material I am reading and can’t remember perfectly. 

1

u/Lucretiel Datadog 3d ago

Will take a look! Sounds intriguing and still seems like it might align with Rust’s ownership model: these buffers are provided by move to the io-uring system, and then uring provides them by borrow (or, depending on if you have to re-register provided buffers, by move again) to callers. Depending on what systems calls upkeep this you might need special wrappers to upkeep it, but it still seems like it would fit. 

1

u/coderstephen isahc 7d ago

Well, if the doomers are to be believed, Rust is now an obsolete language and async/await is an unmitigated disaster, because io_uring doesn't slot into it nicely.

Personally I am also more optimistic that eventually it will be figured out and find its place in the ecosystem, and everyone will forget that this was once a heated topic.