| ▲ | Epoll vs. io_uring in Linux(sibexi.co) |
| 90 points by Sibexico 6 hours ago | 24 comments |
| |
|
| ▲ | toast0 3 hours ago | parent | next [-] |
| > But my students weren’t as happy as I was - they wanted to build something genuinely useful, and they were really disappointed that our “product” had strong architectural limits and couldn’t outperform titans like nginx and haproxy. I took a (very brief) look at the github repo [1], it doesn't look like you're doing anything with cpu pinning. You can probably eke (thanks) out a bit more performance if you cpu pin your threads and cpu pin your listen sockets (sockopt SO_INCOMING_CPU). If you also cpu align your outgoing sockets, you should get a significant boost, but afaik, there's no great api for that. Linux does have an api for compatible NICs (traffic steering/flow steering) which can work, but if you know what hash your NIC uses (it's probably toeplitz) and you manage source port selection to your backend, you can pick ports that will hash properly. The goal is for your proxy to be able to handle packets without any cross cpu communication. [1] https://github.com/sibexico/TinyGate |
| |
| ▲ | Sibexico 3 hours ago | parent | next [-] | | Basically, v0 and v1 of the repo is completely different implementations, written almost from scratch. Now working on the 3rd one implementation, I believe the last one. :) Completely different architectural choices was made. | |
| ▲ | jibal 3 hours ago | parent | prev [-] | | eke |
|
|
| ▲ | spliffedr 4 hours ago | parent | prev | next [-] |
| Take a look at https://github.com/concurrencykit/ck and https://github.com/microsoft/mimalloc, it will fit well for a zero-copy and mem aligned reverse proxy. Also, if you want to add a DDoS protection and more advanced L4 stuff check out https://docs.ebpf.io/ebpf-library/libxdp/libxdp/ |
| |
| ▲ | Sibexico 3 hours ago | parent [-] | | Yeah, the plan was to apply optimizations at the other levels, then we will go to allocators. Studying the allocators rn with my students, the previous post in the blog was about custom allocator on the Zig lang. |
|
|
| ▲ | GalaxyNova 12 minutes ago | parent | prev | next [-] |
| The year is 2050; there are 20 different ways to poll a socket on Linux. |
|
| ▲ | mrlonglong 5 hours ago | parent | prev | next [-] |
| Boost asio if you love C++ and asynchronous networking. |
| |
| ▲ | MathMonkeyMan 5 hours ago | parent | next [-] | | I switched out asio's epoll backend for its io_uring in a database server and CPU utilization shot up. Probably depends on usage and the specifics of how it's integrated into the event code. | | |
| ▲ | vlovich123 4 hours ago | parent | next [-] | | That’s paradoxically what you can expect on a busy server - your CPU can spend time doing work that would have been previously IO wait time. Of course, it could be a bug in the implementation where you’re spinning doing no work erroneously, but depends on the details. | | |
| ▲ | saghm 4 hours ago | parent [-] | | Yeah, the explanation that I've usually heard for this sort of thing is that it's intended to get back CPU time that's lost when too many system threads are blocking to keep something on every core even during I/O (or pay for it in terms of the context switching overhead if you compensate for this with an extremely large number of system threads). The theory is that you'll avoid idle CPU compared to the common "one thread per core" way of doing things due to some of them being idle during I/O, at the cost of using some extra CPU to handle more things in user space. Obviously how much this helps can vary between use cases, but the measure of how much it's helping (or if it's maybe not helping at all!) is throughput, not CPU utilization. | | |
| ▲ | vlovich123 3 hours ago | parent [-] | | That is an imprecise explanation being conveyed to you - thread per core is still valuable in an io_uring world. The reason for that is how computers are built - its inherent in the kinds of operations that are cheap and what happens otherwise, the I/O model doesn’t matter. Specifically, not thread per core code has the following issues: * you have to use atomics/locks to synchronize data access. This involves expensive HW operations to implement the semantics of what an atomic operation is * you have to deal with lock contention and cache contention * when an OS migrates the core that is executing your code, you’ve suddenly got cold caches all over the place (icache, dcache, and TLB). There’s also a bunch of related things that pop up - even if you do thread per core, the processor interrupts for events probably land on a different CPU resulting in extra overhead within the OS to deliver the event to you. Io_uring doesn’t “handle more things in user space”. It specifically avoids a bunch of overheads; you’re context switching less (other cores can execute the OS code to process your request) and you can pipeline I/O (you can tell the OS “do IO A, then B, then C and tell me when that’s all done”) and you get fewer memory copies (the kernel reads into your buffer directly without needing to create another copy although this is more nuanced). Anyway, the better mental model is specifically io_uring is more efficient and thus CPUs spend less time standing around waiting for things to happen at the hardware level (context switching, waiting for locks, etc). If the CPUs weren’t actually spending much time waiting, then you don’t get much benefit. This is the same phenomenon as Jevons paradox in economics; IO gets cheaper so you can do more of it within a given time unit and thus your CPUs end up more often having real work to do. |
|
| |
| ▲ | toast0 an hour ago | parent | prev [-] | | In addition to the other discussion. It's important to measure outcomes and not just look at the cpu meter... At the same load, how did latency look for A vs B. What was throughput and latency at maximum load like for A vs B. For whichever one had the smaller max throughput, what did latency look like for the other option. For bonus points while testing: is there another observable metric to indicate available capacity, if cpu % free is less useful. |
| |
| ▲ | LoganDark 2 hours ago | parent | prev [-] | | Boost is so inconvenient, they're huge dynamic libraries that are a pain to build and use. Even when I was already using CMake, getting Boost installed in a way where it could be discovered was super annoying. (I was on Mac, though) |
|
|
| ▲ | Uptrenda 5 hours ago | parent | prev | next [-] |
| Yes, io_uring is significantly faster than epoll (I think I had like 20% faster req/s with io_uring.) The catch is that its kernel opt-in and disabled just about everywhere for security reasons. I think that it has direct memory sharing between the kernel and user-land which is kind of yikes. There's been multiple exploits that hit io_uring in recent times. It's because of this that even engineering projects that try to reach the highest performance possible (like Go) don't really bake io_uring in as a sane default. Though if you want to take the risk you can always run it yourself for your favourite language. It is faster but the cost is possible exploits. |
| |
| ▲ | Cloudef 2 hours ago | parent | next [-] | | Quite depends, I had times when my posix emulation of io_uring (with poll, not epoll) was faster than io_uring. For large zero-copy buffers, io_uring is king however. Also io_uring is useful even for non asynchronous IO as it can implement chain of operations as single atomic operation (mkdir + open it for example). For something like networking, if you are maximizing packets per second, you'll hit kernel limits[1] very quickly and instead have to start leveraging features like GSO/GRO or completely bypass the network stack. 1: https://github.com/axboe/liburing/discussions/1346 | | |
| ▲ | lukeh 9 minutes ago | parent [-] | | Also it’s nice for things like SPI which have no user space non-blocking API. |
| |
| ▲ | csdreamer7 3 hours ago | parent | prev | next [-] | | RHEL 9 and 10 now fully support io_uring by default. It is very recent, but this covers a lot of corporate Linux installs. Gemini 'said' Ubuntu and SuSE support it as well, but did not provide any links to prove it. https://access.redhat.com/solutions/4723221 Go should reconsider support. They should have a 'go' at it. | | |
| ▲ | insanitybit 2 hours ago | parent [-] | | It's still seccomp'd off in most environments because io-uring is still a seccomp bypass that doesn't play well with kernel security systems (audit subsystem), even if it weren't also like the #1 or #2 exploit vector for privesc. |
| |
| ▲ | omcnoe 3 hours ago | parent | prev | next [-] | | For a project like Go, wouldn't it be an option to do one-time iouring feature detection in the runtime startup? Exploits are an issue for the entire OS, not the program choosing to use iouring, yeah? | |
| ▲ | happyPersonR 4 hours ago | parent | prev [-] | | Any kind of poll mode networking: Rdma, dpdk, io_uring it’s really kind of up to the user to do the memory isolation In io_urings case tho, you can’t do much because the rings are in the kernel. I’m hopeful though that with Llm things will get better. But it’s just hard problem to solve .
Very difficult to do in the kernel itself, and folks don’t really even understand tuning for it. | | |
| ▲ | kshri24 3 hours ago | parent [-] | | The ring buffers are in shared memory not kernel private. The ring buffers (submission and completion) are shared between kernel space and user space. User publishes requests via submission queue entries (updates tail of buffer while kernel reads head of the buffer), kernel shifts the submission queue buffer on its end and returns a completion queue event by publishing to completion buffer. User pulls from this buffer (specifically the head, kernel updates tail of buffer) in user space. |
|
|
|
| ▲ | up2isomorphism 4 hours ago | parent | prev [-] |
| The author takes a very benchmark focus on this topic which only says part of the story particularly for complex systems. Noticed that there are a number of very similar interface that exist on other platform like windows long before io_uring, but that does make Linux’s I/O system worse or slow than these platforms. A fast server is likely fast in either multiplexing or async API if implemented correctly in almost all cases. |
| |
| ▲ | Sibexico 3 hours ago | parent | next [-] | | I'm now a Windows developer, mostly working with Linux and FreeBSD. Thx for the point, I'll look how it works in Windows systems. | |
| ▲ | RossBencina 3 hours ago | parent | prev [-] | | There is no benchmark in the post. There is analysis, discussion and code examples for epoll and io_uring usage. |
|