3 ms·
As someone new to backend - is there a simple alternative architecture for this project? I hope there's an easier way to host a million bits of state and sync w
by wonger_ 2y ago
As someone new to backend - is there a simple alternative architecture for this project? I hope there's an easier way to host a million bits of state and sync with clients. Some of the solutions in the post went over my head.
Kudos to the author - your projects are great.
- isoprophlex 2y agoI don't think it gets much simpler than this to be honest... except for un-scalable things like keeping a single global list of a million booleans in the same process as your backend api.
- wild_egg 2y agoMaybe dumb question but why is keeping a million booleans in one process un-scalable? That's only 125KB of memory which can easily live inside CPU cache and be operated on atomically.
- eieio 2y ago(I'm the author) I don't think it's unscalable at all - it's just that if you do this there's not a great story for adding a second machine if the first one can't handle the load (but ofc a beefy machine with a fast implementation could handle a lot of load). When we did the go rewrite we considered just getting one beefier box and doing everything there (and maybe moving Redis state in memory), but it felt easier and safer to do a drop-in rewrite.
- alright2565 2y ago> there's not a great story for adding a second machine if the first one can't handle the load I mean this is basically your redis situation right? Just with a very specialized "redis". You could scale this out, even after the pretty massive ability to scale up is exceeded. Have some front-end servers that act as a connection pooler to your datastore. Or shard the datastore, and have clients only request from the shards that they are currently looking at.
- eieio 2y agoRight, and then the question is "is my specialized datastore gonna be faster than Redis" right? And it seems totally reasonable that you could make something faster eventually - but I think it's not a reasonable goal within the timeframe of the go rewrite (one Sunday afternoon and evening). Especially if you want to extend that system such that other services could talk to it! The entire timeframe of this project was 2 weeks, and the critical period (most activity / new eyes) was a couple of days.
- alright2565 2y agoSorry, I'm talking hypothetically about how this would be designed, not in the context of your specific timeframe! > "is my specialized datastore gonna be faster than Redis" Absolutely! With how efficient this code would be, you'd likely never need to scale horizontally, and in that case it is extremely easy to compete with a network hop (at least 1ms latency) versus L1 cache (<50ns) The comparison with redis otherwise only applies once you do need to scale horizontally. There's also the fact that redis must be designed to support any query pattern efficiently; a much harder problem then supporting just 1-2 queries efficiently.
- eieio 2y ago> Sorry, I'm talking hypothetically about how this would be designed, not in the context of your specific timeframe! Oh sure, yeah, I think we just agree then!
- Boxxed 2y agoBut it's all going through one redis box, isn't it? That feels like you're still limited by your one beefy machine.
- eieio 2y agoEh, like I said below, I think this gets into "do I think I can write something that scales substantially better than Redis" / "was I at risk of generating load that Redis couldn't handle" and I think the answer to both of those questions is no. But that's based on my skillset and the timeframe of the project; I'm not disputing that the project is bounded by Redis's performance.
- isoprophlex 2y agoOh no that's absolutely fine. I wasn't thorough. As commented by the author, that probably gets you very far. However... You'd need to persist those booleans somewhere eventually, of course, if you want the state to survive a process restart. And if you want multiple concurrent connections from the same box, you have to somehow allow multiple writers to the same object. And if you want multiple boxes (for redundancy, load spreading, geo distribution...), you need a way to do the writing and reading from several different boxes... By this time you're basically building redis.
- eieio 2y agoAuthor here! Sorry that some of the stuff went over your head! I wanted to include longer descriptions of the tech I was using but the post was already suuuper long and I felt like I couldn't add more. Very happy to answer any questions that you've got here! I'm not sure how you'd simplify the architecture in a major way to be honest. There are certainly services you could use for stuff like this, but I think there you're offloading the complexity to someone else? Ultimately you need: * A database that tracks which boxes are checked (that's Redis) * A choice about how to put your data in your database (I chose to just store 1 million bits for simplicity) * A way to tell your clients what the current state is (I chose to send them all 1 million bits - it's nice that this is not that much data) * A way for clients to tell you when they check a box + update your state (that's Flask + the websocket) * A way to tell your clients when a box is checked/unchecked (that's also Flask + websockets. I chose to send both updates about individual boxes and also updates about all 1 million boxes) * A way to avoid rendering 1 million dom elements all the time (react-window) The other stuff (nginx for static content + a reverse proxy) is mostly just to make things easier to scale; you could implement this solution without those details and the site would work fine, it just wouldn't be able to handle the same load.
- sa46 2y agoJust spitballing: could you change the database to a bool array? Guard it with a RWMutex and persist on server shutdown. The bottleneck probably moves to pushing updates from a single server, but Go can probably handle a few tens of thousands of goroutines.
- summerlight 2y ago> persist on server shutdown Probably this is not the simplest thing to do if you want a certain degree of reliability. Should be definitely easier than writing the entire storage engine, but likely an overkill for this kind of overnight hobby projects.
- stefs 2y agoone RW mutex would mean you'd lock the whole array; that way data access becomes pretty much single-threaded. simplest solution that comes to mind: AtomicIntegerArray (or whatever it is in your language of choice). you could also implement a bitset over AtomicLongArray. more complicated: partition into x*x chunks and rw-locking those. this could be backed by an mmap'ed a million bytes for persistence, but no idea if that'd make the app disk io bound or something.
- paxys 2y agoThis is pretty much as simple as it gets tbh. A couple of web servers backed by a cache/pubsub queue. It may have been possible to do it all in-memory on a single large host, but then if it is unable to meet the demand or fails for whatever reason then you are completely out of luck.
- 10000truths 2y agoSure. Everything described in the article could be crammed into a single process. Instead of using a database, you could store the bitset in a file and mmap it. And instead of using a reverse proxy, you could handle the HTTP requests and WebSocket connections directly from the application.
- intelVISA 2y agoAll that cloud infra and Very Smart system design for what could be served by a single dedi & AF_XDP in C++/Rust. Always wonder why clearly smart people put themselves through these heroic gymnastics just to avoid learning a bit of <sys/socket.h> Still a cool write-up B).
- eieio 2y agoFWIW I think it'd be a huge mistake for me to spend a bunch of time learning to write performant code! My takeaway from this experience is that I have no idea which of my sites is going to be popular - most of my projects are not nearly this successful - and that I should be optimizing for speed of creation, not speed of an individual project. My comparative advantage is "writing ok code and making ok decisions very quickly" and I'm going to continue to lean into that.
- imtringued 2y agoUse a quad tree to summarize entire blocks of checkboxes with the same state as a tuple (checked, start_x, start_y, end_x, end_y). You know, the blatantly obvious things.