diarrhea
Joined 6 years ago
Joined 6 years ago
Why?
I built a system once which had (this is Rust) a Rayon worker thread pool of 4 threads and a Tokio async pool of 2 (multithreaded runtime). On a system of 6 vCPU. This ended up working fine. Tokio was not starved so handled network requests at low latency.
One difference is DuckDB is a pure network client. If one of its async threads is starved it is not the end of the world (e.g. k8s does not kill your pod for failure of replying to health checks).
That is fantastic. Last I checked models capable of running on commodity (anything below a dedicated GPU rack) hardware were very lackluster.
server:
@echo "Deploying machine (with ssh-agent forwarding): '$(MACHINE)'"
NIX_SSHOPTS="-A" \
nix run nixpkgs#nixos-rebuild -- switch \
--flake .#$(MACHINE) \
--target-host $(MACHINE).$(DOMAIN) \
--build-host $(MACHINE).$(DOMAIN) \
--no-reexec \
--verbose \
--sudo
Which has been working well. I admit I do not understand what all of these flags do in detail.This uses ssh agent forwarding, and then sudo via PAM. That allows for passwordless sudo. Building (well, activating) without sudo is pretty involved last I checked, I could not get it to work.
Says it right there actually:
> It’s recommended to not set this limit too low in order to avoid hanging on operations requiring spawn_blocking.
So a total like 6 - reasonable for a web backend - would be way too low.
For genuine CPU-bound work, submitting to a Rayon worker pool is the way to go. It solved a runtime starvation issue for us at work, spawn_blocking did not work.
The reason for all this is spawn_blocking having a very large underlying thread pool, in the hundreds. That is okay if you assume work will yield those threads and mostly sleep/wait. It is not okay if the work never yields, like pure data crunching. (Go solves this by forcefully preempting loops, no such thing in Rust without a language runtime)
Our solution shape was: multi-threaded Tokio (2 threads), then give the rest of available_concurrency to a Rayon thread pool. If you grant 6 vCPU you should see a thread pool of 4, and a maximum CPU consumption of about 400%, as the Tokio threads sit mostly idle (under low load single-threaded runtime should also suffice).
You inject the thread pool using an Arc.
Then, when work comes in, just spawn Tokio tasks liberally (cheap) and submit to the thread pool. Rayon will internally queue and limit concurrency and parallelism to 4 (this is the important bit compared to spawn_blocking: no way your system can hog all 6 threads with non-yielding work and starve Tokio runtime threads).
We use one-shot channels to submit results back, they are designed for exactly this. The tx aka sender end is sync, as there is never a wait (cannot block). The rx aka receiver side is async and can be awaited normally on the async side. This is a cheap operation, similar to Go.
Optionally you can reach for semaphores to also limit I/O concurrency. You probably want to do this for more control and avoiding resource exhaustion loudly (that is, not silently accidentally peg thousands of FDs, database connections, …).
It ended up working beautifully for our purposes and relatively simply. No lifetime woes, Arc solves those. Oneshot channels just transfer ownership etc.
Perhaps this is what TFA talks about, I have not read it.
One caveat: to reach all the above conclusions and designs, we had help from some genuine Rust experts. As much as I dislike Go, it "just works" there even if one writes naive code.
I also run self-hosted Wireguard. Initially on a Debian box, nowadays it is integrated into my router (admittedly, this is closed source). For around 6 years at this point.
The whole thing could not be easier and simpler. It has never randomly broken on me. It is fast. It is free. There is no middle man, no vendor.
I never understood the popularity of Tailscale, though that is on me. I'm sure it is a great product, I just never tried it, do not seem the target audience.
What confuses me is the often accompanying, sometimes aggressive anti-selfhosting stance in these sorts of threads. I do not see this in other topics, e.g. someone mentioning they run Jellyfin isn't met with "why not Plex?". Where does that come from? We are on HackerNews, not ProductShillNews, aren't we? I guess self hosting Wireguard is too boring to warrant any further discussion? The VPN equivalent of a Toyota Corolla.
> I suspect rather than hire less people we will just produce more code changes.
Why? Towards what end? Code changes are output, not outcome. It also needs to be connected to someone willing to pay you hard cash. That is the hard part, a race to the bottom, and the reason I also believe there will be downwards pressure on salaries and even employment.
They do, but you need far fewer or none of the original workers whose full-time job this sort of stuff was.
Raw math does not matter, but what you do with it. Similarly, you could earn a (modest) living knowing nothing but raw HTML, JavaScript and a bit of browser tech not too long ago. That is no longer possible.
Programming and software engineering will be devalued. These occupations won't disappear overnight, but you will see compensation and growth stagnate until equilibrium is reached again. Currently, supply outstrips demand, and I do think it is structural, not just hype.
I'm certainly not creative enough, but I currently do not see demand picking up sufficiently; Gen Z is bearish on social media, VR was a bust, blockchain was a bust, software has already penetrated almost all walks of live and lines of work. There is no next big thing (Internet, ...) on the horizon, to unlock the next order of magnitude of demand. There is certainly more work to do still, but it very suddenly does not require the same headcount, but something like 5%-30% less. Lots of the remaining work will be around integrating LLMs into existing software, which does not sound exciting either.
That is problematic for two reasons: it might be a large type, so copying might be expensive. Second, more likely, it might violate invariants in your domain. For a rate limiter, this might mean accidentally copying around some internal state like a mutex, which then exists n times instead of 1 time, which can represent a problem (e.g. if you want to internally limit whole-app concurrency toward Redis).