Live data from Hacker News

Evio – Fast event-loop networking for Go

github.com

41–50 of 67 posts

Re: Evio – Fast event-loop networking for Go

#41

Earlier quoted context omitted.

The goroutine implementation scales, while other thread implementations (by default) do not. That's a semantic difference. A Go server can have millions of active goroutines with moderate resource use. You can achieve the same on Linux or Solaris using kernel threads, but you have to work at it. With Go you don't have to work at it, and it works on macOS and Windows and a few other OSs too. This is all comparisons be…

> You can achieve the same on Linux or Solaris using kernel threads, but you have to work at it. By setting the thread stack size to a reasonable value. That's it. And, in fact, on 64-bit you often don't even need to do that. The difference you're describing is a difference in default thread stack sizes , which is hardly a paradigm shift. We're talking about one call to pthread_attr_setstacksize().

It's not nearly as simple as you claim.

First: if you have an epoll loop it is also the cost of the thread context switch, which has definitely us in RPC systems using kernel threads. By contrast the goroutine gets scheduled onto the kernel thread that answered the poll, saving the switch.

Second: as I alluded to earlier, linux and solaris can scale their kernel thread implementations, not all OSs can. My experiences with large numbers of threads on the BSDs and Windows (in years past admittedly) suggest other kernels don't have thread implementations designed to scale to such high numbers. Solving the problem in userspace means Go programs written in this style are portable across operating systems.

Third: you can only adjust stack sizes down if you know your program always keeps its stacks small. If you depend on libraries you don't own in C/C++, that's a difficult assumption. Go grows the stacks, so if you hit some corner case where a small number of goroutines need some significant amount of stack, your program uses more memory, but typically keeps working. No need for careful (manual!) stack accounting.

If all this were as easy as you say, we would still write nearly all our C/C++ servers using threads. We don't because it's not.

Re: Evio – Fast event-loop networking for Go

#42
post #8

One of my favorite things about Go is that it cuts through the "threads vs. events" debate by offering thread-style programming with event-style scaling using what you might call green threads (compiler assisted cooperative multitasking that has the programming semantics of preemptive multitasking). That is, I can write simple blocking code, and my server still scales. Using event loop programming in Go would take aw…

It doesn't really "cut through" the debate any more than any other implementation of threads does. The only difference between Go and plain old one-thread-per-connection is that regular threads run in the kernel, while Go threads run in userspace. That's not a semantic difference, only an implementation detail (a large detail, to be clear, but still an implementation detail). There were historical implementations of…

What you call a "Go thread" has a precise name (goroutine) and running in userspace is hardly the only difference between a goroutine and a kernel thread.

Creating and destroying kernel threads is significantly more expensive.

A kernel thread has a fixed stack and if you go beyond, you crash. Which means that you have to create kernel threads with worst-case-scenario stack sizes (and pray that you got it right).

Goroutine has an expandable stack and starts with very small stack (which is partly why it's faster; setting up kernel page mappings to create a contiguous space for a large stack is not free).

Finally, goroutine scheduling is different than kernel thread scheduling: a blocked goroutine consumes no CPU cycles.

In a 4 core CPU there is no point in running more than 4 busy kernel threads but kernel scheduler has to give each thread a chance to run. The more threads you have, the more time kernel spends and pointless work of ping-ponging between threads. That hurts throughput, especially when we're talking about high-load servers (serving thousands or even millions of concurrent connections).

Go runtime only creates as many threads as CPUs and avoids this waste.

That's why high-perf servers (like nginx) don't just use kernel thread per connection and go through considerable complexity of writing event driven code.

Go gives you straightforward programming model of thread-per-connection with scalability and performance much closer to event-driven model.

You work on Rust and are well informed about this topic so I'm sure you know all of that.

Which is why it amazes me the lengths to which you go to denigrate Go in that respect and minimize what is a great and unique programming model among mainstream languages.

Re: Evio – Fast event-loop networking for Go

#43
post #40

This is single-threaded? What are you going to do with the other 31 or 63 cores? The single-threaded nature of applications liked Redis an Haproxy is a singificant impediment to their vertical scalability. CPUs aren't getting faster, we're just going to get more cores, so anything that assumes there's only a single core seems like a dead end. Haproxy literally just added multithreading support in 1.8.

The CPU is rarely the bottleneck and for both Redis/HAProxy the vertical scalability solution has been to launch multiple processes or forks with different core affinities. There are downsides of course (no IPC) but I still argue that CPU is not the bottleneck for 99% of usage scenarios.

HAProxy added threading support in 1.8 as you pointed out and Redis has started the same (for a certain subset of processing) in 4.0 as well. They're getting there but concurrency is tough.

To suggest that his product is a "dead end" due to not supporting threading seems a bit premature, as Redis and HAProxy are extremely well-regarded in their niche and they made it there without threading, and we've been at maximal clock speed for nearly a decade.

Re: Evio – Fast event-loop networking for Go

#44
post #40

This is single-threaded? What are you going to do with the other 31 or 63 cores? The single-threaded nature of applications liked Redis an Haproxy is a singificant impediment to their vertical scalability. CPUs aren't getting faster, we're just going to get more cores, so anything that assumes there's only a single core seems like a dead end. Haproxy literally just added multithreading support in 1.8.

> This is single-threaded? What are you going to do with the other 31 or 63 cores?

Yes, the event loop is single-threaded. The other cores can be used for other stuff, but not the event loop.

It's completely possible with this library to process operations in a background thread and wake up the loop when it's time to write a response. If that's what the developer desires.

> anything that assumes there's only a single core seems like a dead end.

If my documentation somehow implies that systems running this library do not have multiple cores then I'm sorry for the confusion. This library makes no assumption about the host server, and it does not limit the application to a single core. It just runs the event loop in one thread.

Re: Evio – Fast event-loop networking for Go

#45
post #43
post #40

This is single-threaded? What are you going to do with the other 31 or 63 cores? The single-threaded nature of applications liked Redis an Haproxy is a singificant impediment to their vertical scalability. CPUs aren't getting faster, we're just going to get more cores, so anything that assumes there's only a single core seems like a dead end. Haproxy literally just added multithreading support in 1.8.

The CPU is rarely the bottleneck and for both Redis/HAProxy the vertical scalability solution has been to launch multiple processes or forks with different core affinities. There are downsides of course (no IPC) but I still argue that CPU is not the bottleneck for 99% of usage scenarios. HAProxy added threading support in 1.8 as you pointed out and Redis has started the same (for a certain subset of processing) in 4.…

> There are downsides of course (no IPC) but I still argue that CPU is not the bottleneck for 99% of usage scenarios.

I suppose my experience might be unusual, but I frequently log in to c3.8xlarge redis machines that have a single core pegged at 100% and the rest doing nothing. Yes multiple processes help, but that requires updating clients and makes it harder to share memory.

> To suggest that his product is a "dead end" due to not supporting threading seems a bit premature, as Redis and HAProxy are extremely well-regarded in their niche and they made it there without threading.

Well yeah, CPUs hitting their GHZ limit and the dramatic increase in the number of cores per machine is a relatively recent phenomena.

I just think its weird to start a brand new project making those same assumptions, especially when the underlying programming language was explicitly designed with concurrency in mind.

It'd be like building a new networking library in Rust which ditches memory safety.

Re: Evio – Fast event-loop networking for Go

#46
post #42

Earlier quoted context omitted.

It doesn't really "cut through" the debate any more than any other implementation of threads does. The only difference between Go and plain old one-thread-per-connection is that regular threads run in the kernel, while Go threads run in userspace. That's not a semantic difference, only an implementation detail (a large detail, to be clear, but still an implementation detail). There were historical implementations of…

What you call a "Go thread" has a precise name (goroutine) and running in userspace is hardly the only difference between a goroutine and a kernel thread. Creating and destroying kernel threads is significantly more expensive. A kernel thread has a fixed stack and if you go beyond, you crash. Which means that you have to create kernel threads with worst-case-scenario stack sizes (and pray that you got it right). Goro…

> What you call a "Go thread" has a precise name (goroutine)

I call goroutines threads because they are user-level threads.

As an analogy, NVIDIA calls local threadgroups "warps", but that doesn't make them not local threadgroups.

> Creating and destroying kernel threads is significantly more expensive.

Because kernel threads usually have larger stacks. But they don't always have large stacks: that is configurable. Other than the stack size, the primary difference is simply that kernel threads are created in kernel space and user threads are created in userspace.

> A kernel thread has a fixed stack and if you go beyond, you crash. Which means that you have to create kernel threads with worst-case-scenario stack sizes (and pray that you got it right).

You can do stack switching in 1:1 too. After all, if you couldn't, then Go couldn't do stack switching at all, since goroutines are built on top of kernel threads.

Go's small stacks are really a property of the moving GC, not a property of the threading model.

> In a 4 core CPU there is no point in running more than 4 busy kernel threads but kernel scheduler has to give each thread a chance to run.

> Go runtime only creates as many threads as CPUs and avoids this waste.

Not if they're blocked doing I/O!

If they're not blocked doing I/O, then Go tries to do preemption just as the kernel does. (I say "tries to" because Go currently cannot preempt outside function boundaries; this is a significant downside of M:N threading compared to 1:1 kernel threading.)

> That's why high-perf servers (like nginx) don't just use kernel thread per connection and go through considerable complexity of writing event driven code.

High-performance servers like nginx use an event loop because it's the only way to get the absolute fastest performance, with no overhead of stacks at all. The fact that the project described in the article gets better performance than Go's threads is proof of that fact, in fact.

It would be possible, and interesting, to do Go-like 1:1 threading with small stacks.

> Go gives you straightforward programming model of thread-per-connection with scalability and performance much closer to event-driven model.

Sure. But that's mostly because of the GC, not because of the M:N threading model.

> Which is why it amazes me the lengths to which you go to denigrate Go in that respect and minimize what is a great and unique programming model among mainstream languages.

It's not unique. As I said, NGPT used to do M:N for pthreads. Solaris used to do M:N for pthreads. The JVM used to do M:N.

Re: Evio – Fast event-loop networking for Go

#47

Earlier quoted context omitted.

> You can achieve the same on Linux or Solaris using kernel threads, but you have to work at it. By setting the thread stack size to a reasonable value. That's it. And, in fact, on 64-bit you often don't even need to do that. The difference you're describing is a difference in default thread stack sizes , which is hardly a paradigm shift. We're talking about one call to pthread_attr_setstacksize().

It's not nearly as simple as you claim. First: if you have an epoll loop it is also the cost of the thread context switch, which has definitely us in RPC systems using kernel threads. By contrast the goroutine gets scheduled onto the kernel thread that answered the poll, saving the switch. Second: as I alluded to earlier, linux and solaris can scale their kernel thread implementations, not all OSs can. My experiences…

> First: if you have an epoll loop it is also the cost of the thread context switch, which has definitely us in RPC systems using kernel threads. By contrast the goroutine gets scheduled onto the kernel thread that answered the poll, saving the switch.

I'm not comparing M:N to a 1:1 system where all I/O is proxied out to another thread sitting in an epoll loop. I'm comparing M:N to 1:1 with blocking I/O. In this scenario, the kernel switches directly onto the appropriate thread.

> Second: as I alluded to earlier, linux and solaris can scale their kernel thread implementations, not all OSs can.

The vast majority of Go users are running Linux. And on Windows, UMS is 1:1 and is the preferred way to do high-performance servers; it avoids a lot of the problems that Go has (for instance, playing nicely with third-party code).

> Third: you can only adjust stack sizes down if you know your program always keeps its stacks small.

You could do 1:1 with stack growth just as Go does. As I've said before, small stacks are a property of the relocatable GC, not a property of the thread implementation.

> If all this were as easy as you say, we would still write nearly all our C/C++ servers using threads.

We don't write C/C++ servers using threads because (1) stackless use of epoll is faster than both 1:1 threading and M:N threading, as this project shows; (2) C/C++ can't do relocatable stacks, as the language is hostile to precise moving GC.

Re: Evio – Fast event-loop networking for Go

#48
post #13

This project is not intended to be a general purpose replacement for the standard Go net package or goroutines. It's for building specialized services such as key value stores, L7 proxies, static websites, etc. You would not want to use this framework if you need to handle long-running requests (milliseconds or more). For example, a web api that needs to connect to a mongo database, authenticate, and respond; just us…

This is an extremely helpful explanation. Would you consider adding a "Rationale" subheading to the readme and pasting this in wholesale? Great project, thanks for sharing!

Re: Evio – Fast event-loop networking for Go

#49

Earlier quoted context omitted.

That is exactly what Go does by default. Any time a blocking operation is performed, Go either leaves the OS-level thread blocked there and switches away, or hand the blocking operation to an internal thread which is running epoll for the whole process. The end result is much easier than Python/NodeJs because there is no explicit "async/await" or deferred-style programming. You simply write linear code and any blocki…

In other words, Go uses threads. There's nothing semantically unique to Go about this model.

It depends if you're describing a semantic model or you're concerned about implementation details.

Semantically, a goroutine is a thread, within a shared memory model. But what makes Go unique (or let's say more unique) is that it offers programmers a thread-like programming approach (linear, blocking code) but internally turns it into an event-driven approach (epoll/kqueue) for networking.

Moreover, the fact that goroutines are much cheaper than OS-level threads enable a more pervasive approach to concurrency.

Re: Evio – Fast event-loop networking for Go

#50
post #13

This project is not intended to be a general purpose replacement for the standard Go net package or goroutines. It's for building specialized services such as key value stores, L7 proxies, static websites, etc. You would not want to use this framework if you need to handle long-running requests (milliseconds or more). For example, a web api that needs to connect to a mongo database, authenticate, and respond; just us…

This is an extremely helpful explanation. Would you consider adding a "Rationale" subheading to the readme and pasting this in wholesale? Great project, thanks for sharing!

I just added it. Thanks for the suggestion
Post reply on HN