Does anybody have opinions about or data on how much load (however quantified, on hardware of your choosing) a single y-websocket server can support?
The performance bottleneck of y-websocket server is the amount of memory that is used by each Y.Doc instance (every Yjs document that is currently being edited is loaded to memory). A single document (a small note, depending on your application), will use about 100kb of memory. That means you should be able to open about 10k connections on a 100mb instance.
Depending on your type of application, your users might create larger documents and you might need more or less memory.
I have plans to rework y-websocket to not load the document to memory at all. Until then you need to plan to scale y-websocket horizontally.
Thanks @dmonad – good to know.
Hi @dmonad, can you write a tutorial on scaling y-websocket horizontally? I was initially planning to use sharedb, but the delay in subscribe-queries acknowledging new records and the 16MB mongo-db doc limit have made me look elsewhere for the collaborative diagramming tool I am working on.
I like y-js, but not having any clear guide on how to scale y-websocket horizontally is holding me back. It’d be very helpful if you can finish this page: https://docs.yjs.dev/tutorials/untitled-3
16MB is pretty big for a document, even with Yjs’ ~50% overhead for text documents. Though if you’re talking diagramming, perhaps you are generating tons of updates from user dragging actions or something. Even still, you might do some experiments to confirm the 16MB limit in MongoDB is a bottleneck. Most databases don’t like documents this big; Mongo I think has one of the higher limits. (This can also be increased manually, iirc.) If you use Yjs’s differential update feature, you could technically ensure you never store updates larger than a certain size using a naive bin-packing algorithm, but you’d need to implement this.
One method of scaling is using a Pubsub service. Let’s say you have two instances of your server, A and B. User kapv89 connects to “Cool Document” on server A, and User braden connects to “Cool Document” on server B. When you update “Cool Document”, it gets rebroadcast on any websocket connections so other clients can see the update. Since I’m on Server B, I don’t receive the update and we go out of sync.
Instead of broadcasting messages directly via the websocket connections, you send the update to a Pubsub service, like Redis Pubsub for example. (Again, need to fork y-websocket to implement this). Each server listens to the corresponding Pubsub channel, and when it receives a message over the pubsub channel, it then rebroadcasts it along any active websocket connections.
kapv89 —ws—> Server A ------> Pubsub -----> ServerB —ws–> braden
This would work for any number of instances. The caveat here is each server would be holding the doc in memory. It might also be beneficial to rig up some kind of server affinity, so that connections to the same document prefer being routed to the same server instance. Another thing to watch out for is persistence; if every instance is persisting updates for the documents to a database, it can get tricky depending on how you are persisting state.
@braden put it nicely. You can scale your application horizontally using any pubsub server. y-redis is just one approach. I’m planning to rework the current y-redis implementation which is why the section is still unfinished.
Ueberdosis is working on a really nice alternative backend for Yjs that scales using y-redis. You should follow their progress. https://next.tiptap.dev/guide/collaborative-editing#our-plug--play-collaboration-backend
Unfortunately, there is no plug-n-play indefinitely scalable backend for Yjs yet. But all the tools are there to build it yourself.
The example ws-server available in y-websocket is a bit far off from something that can be made horizontally scalable. For example an update that is received on a socket connection somewhere over here https://github.com/yjs/y-websocket/blob/master/bin/utils.js#L169 will be the one that will be persisted to the database, because ydoc can be updated by redis pub-sub too.
I had to dig in the code y-protocols/sync and copy relevant parts till I had the update which would be applied to the y-doc, and then persisted that update to the db. This seems to be working fine.
Now comes the part of making the socket server horizontally scalable by applying patches from y-redis into my makeshift socket server connection
@dmonad @braden Can any of you help me with one specific thing - https://github.com/yjs/y-redis/blob/master/src/y-redis.js#L72 - why does y-redis use a queue to synchronize updates between processes? If I am able to isolate the update that a client has sent to a websocket server, I can just publish that update and all subscribers with the same docname can receive that update and apply that update to the doc they hold in memory, thus sending it forward to all the clients connected to them.
If a standard pubsub works, then I think I have a horizontally scalable implementation of a websocket-server for y-js with persistence on postgres using knex.
If pubsub has some issues, then I’d like to know them, and will implement a queue-system similar to y-redis on my server.
Either way, will like to open-source the horizontally scalable websocket-backend for yjs this weekend.
The messages are stored in a queue to retrieve the initial content after a server subscribed to the room. But you can also solve that differently (e.g. by asking other peers for the initial content directly).