Ditching Go for Node.js
github.com
github.com
Did you try using pprof and the other tooling go provides to better understand your performance limitations? Tooling is a lot better in go ecosystem for understanding CPU and memory consumption, so if/when you run into into issues with Node you're going to be in a world of pain (this is basically a large portion of my job in a large node.js code base in the $day_job). You'll basically have to resort to using lldb and heapdumps in the Node world. I'm surprised the number of concurrent clients you go with Go was so small. I know lots of people using Go and Gorilla websockets that exceed 1.5k clients with similar memory constraints. To be perfectly honest, it sounds like you're doing something wrong.
As of Go 1.4, the default stack size per goroutine was 2KB and not 4KB.
If you add in TypeScript, you'll have a better type system than Go provides in the Node ecosystem. That's a huge point for using Node.js, especially if there are multiple contributors and the code base lasts for many years.
Having worked on large backend codebases in both TS and JS, the difference is stark. The TS API in my case had something like 90% less errors, and those were all subtle bugs in business logic.
On the other hand, the large JS API over time had all sorts of unexpected type errors and undefined behavior. I'm aware that this is anecdotal evidence, but TS is pretty much a no-brainer now on backend.
I have no experience with maintaining TS projects over a longer period of time, but I have started some experimental TS projects where I was looking for type declarations.
What I found didn't seem to have any formal reference to a specific version of the library. I found that a bit scary. Not sure if it actually causes problems though.
So it's not that it's a source of errors as most libraries are backwards compatible.
It can be. But most popular libraries have very good version syncing, or even include definitions in the module itself. In the worst case, or if there are no typings available at all, you can just import the plain js, and use it untyped, which can be acceptable if it is not used in a lot of places.
> didn't seem to have any formal reference to a specific version
Yeah. It mostly works if you install the latest version of them both at the same time. Sometimes I've had to fiddle with backing the definition or the module back a version to get them to match.
That sounds like a red flag to me. I hardly have any type related bugs in my pure JS server code. Must be a poor developer in the team. Too easy to blame JS for that.
If TypeScript let's that developer make good code, then the variable is JavaScript.
When the tools help you, a good developer can focus on making better things rather than not making mistakes.
FWIW I predominantly use PHP, so I have no dog in this fight.
At least that’s some generalizations I’ve observed, YMMV.
And to hell with compile time. If the build takes 10x as long but we run 5% fewer prod servers, that pays for itself in minutes. And if type and nullability checking prevents one prod outage, that saves more time and stress than every build I've waited for this year.
What are some practical examples of strengths it has over Go?
Is there no movement to change that? What about `node --inspect`?
The thing about Intel non-Atom level hardware is that Intel has spent a lot of money over the better part of two decades packing processors full of features that make all kinds of theoretically inefficient things run pretty fast.
This is not true of the Broadcom SoCs on a Raspberry Pi.
"That said, I think Node is not the best system to build a massive server web. I would use Go for that. And honestly, that’s the reason why I left Node. It was the realization that: oh, actually, this is not the best server-side system ever."
Full interview: https://www.mappingthejourney.com/single-post/2017/08/31/epi...
But he also said that for less massive projects Node could be the right fit.
Isn't that basically agreeing with the blog post?
I don't see what's your point.
I actually think Node has not yet realized its best feature. It’s still in a research phase while it learns its best trick: components that bridge the client and server.
Meteor was an attempt. And server side boot in the MVC frameworks is another attempt. But both are wrong. Both try to create anonymous code that doesn’t know whether it’s on the client or server. But the client and server are different. The winning solution will acknowledge that, and just help the component do both. Once we have components that span client and server we can then write applications that don’t deal directly with HTTP. But not before.
That’ll be a huge thing.
What do you think about shared components using React SSR?
From my experience on some large prod deployment, Nodejs app takes much more memory and CPU vs Go app for doing similar work.
He knows it's implemented wrong, that's the point of the article: that Go doesn't make it easy for him to implement it right. The only question is the degree of wrongness. :)
While I think there are lots of valid criticisms of Go (concurrency correctness is still hard and its type system is not very good for certain tasks), none of these are reasons to switch to JavaScript, as the author did. If he was struggling with goroutines and channels, he could still elect for a Node-like architecture (even making it single-threaded by setting GOMAXPROCS=1). If he needed generics and unions (as he cited), he switched to a language without them and without any static types at all (or a single static type, if you will); in Go he could have downgraded to `interface{}` which is analogous to JavaScript's one static type, and he would have only gave up type safety in the bits of code where Go's type system was lacking instead of everywhere.
I don't want to give the impression of overselling Go here; it's just that for the cited criteria, Go is strictly better than JavaScript.
Also, the "just write Node-like code in Go" isn't a solution at all. You're back to fitting a square peg in a round hole.
It's not reasonable to switch from a language that gives you type safety in ~95% of cases to a language that gives you type safety in 0% of cases but which provides an easier transition to a type-safe language. This is why I'm dismissive.
> Also, the "just write Node-like code in Go" isn't a solution at all. You're back to fitting a square peg in a round hole.
You're wrong here. I read and write lots of Go code, and it's perfectly idiomatic to write single-threaded, asynchronous code. In fact, I'd probably do just this for his application, modulo a thin compatibility layer to deal with the fact that net/http spins up a goroutine for each request.
I do not see this in the code linked to from the article.
Each ChatHandler has three (buffered) channels: outgoingInfo.channel, sockChannel, readErrorChannel (not buffered)
There are some things that could cause bottle necks that are not related to goroutines and channels: Every published message calls GroupInfoManager.GetUsers to get a list of all the users in a group. That can be expensive (I don't know if it is or isn't). And then for every user in a channel, their userOutGoingInfo is retrieved.
I would suggest profiling before switching languages.
EDIT: changed "benchmarking" to "profiling" EDIT2: changed "are likely to" to "could" (... cause bottle necks) since I don't know
Boltdb was being handled badly: https://github.com/maxpert/raspchat/blob/79315d861968c126670... You should be using bolt's helper funcs, so you don't forget to close the transaction
https://github.com/maxpert/raspchat/blob/79315d861968c126670... At least this is actually deferred, but still.
https://github.com/maxpert/raspchat/blob/79315d861968c126670... These messages are horrifying, would be much better switching back to json and implementing a strategy like http://eagain.net/articles/go-dynamic-json/
It copies in an unlicensed "snowflake generation" thing? https://github.com/maxpert/raspchat/blob/79315d861968c126670... and then essentially relicenses it? That's illegal...?
They do have a better json pattern for some stuff: https://github.com/maxpert/raspchat/blob/79315d861968c126670... but I'd still steer away from reflection based, there's so much better ways of doing this stuff, like so: http://eagain.net/articles/go-json-kind/
The general use of inheritance and very non-idiomatic code makes me think this is another person who ditched Go before understanding any of it. It seems a very popular sport.
And then people wonder why it eats up 100% CPU and are completely maxed out before reaching 1000 requests/sec...
Yup... Bad slow language!
If it's THAT much easier to make a more efficient server with a fully dynamic language using just coroutines then why should anyone ever use Go in this case?
Saying, "this implementation is bad" might work in another comparison, but given how much of Golang's implementation and engineering philosophy has been driven by an argument of "simplicity" it seems like we keep seeing precious few returns.
This was NOT simple. The node.js version is simple. The Go version is overblown in complexity and abusing the language.
Rewrite the go version using just as simple of structures, and using a sqlite, and I'm extremely confident that it'd be more performant.
And since it's a library, who cares?
Golang isn't a language that's old enough to have accumulated a ton dissonance about the "right" way to do things. Golang encourages that kind of code and you see it all over GitHub. Too bad it's something of a trap.
But it's also worth noting Golang's current implementation of channels and messages is (as I've noted) going to be notably slow. Odds are you are going to avoid channels entirely if you care about maximizing multi-core performance.
Which is not to say that Golang "isn't fast". Just that it's not at all surprising that for some workloads NodeJS would outperform it.
For workloads where lots of concurrent and contingent I/O dominate, coroutines win handily.
As noted by other commenters, below, the code seems to have some issues. And, if it still didn't perform well after addressing those, somebody in gonuts would've helped teach how to profile it, and then expert-eyes could have looked over the profiler output and provided further feedback.
I did really love POE, though, so when I heard of Node.JS, I thought I will probably like this very much.
I am not sure what happened. I think it was the tutorials being always out of date. Node.js seem so be such a fast-moving target. I do not mind asynchronous, callback-driven code. But when a tutorial that was written three months ago fails to run because some library had a breaking API change in between, that tends to drive me away.
Think of Go what you want, but its policy towards backward compatibility is a big plus.
Stupid mistake. Please ignore.
The Go team has no problem with this; /x/ packages are explicitly not covered by the backwards compatibility guarantee, and in fact they may be considered to disappear at any point.
OTOH Node.js is much more dependant on third party libraries than Go
Most (well, all) Perl programming I do these days is simple scripts for automating tedious tasks, reporting, etc., and the odd CGI script.
Funny story, though: during my training I spent some time in a team doing in-house Perl development, and I sat next to Sebastian Riedel's desk! :-) It's a small world!
Also, unless you go low level (lower than frameworks like Tokio) you can't easily access file descriptors to watch them (e.g. epoll) for data waiting to be read. It makes it difficult to use WebSockets for their intended purpose: Real-time stuff.
Rust needs a web framework that has built-in support for Websockets (running on the same port as the main web server) and also provides low-level access to things like epoll. Something like a very thin abstraction on top of mio (that still gives you direct access to the mio TcpListener directly).
In my attempts to get Tokio reading a raw file descriptor I just couldn't get it working. I opened a bug and was told that raw fd support wasn't really supported (not well-tested because it only works on Unix and cross-cross-platform stuff is a higher priority). Very frustrating.
I wish the Tokio devs didn't make the underlying mio TcpListener private in their structs.
https://github.com/tomaka/rouille
https://github.com/actix/actix-web
Ref: https://github.com/flosse/rust-web-framework-comparison
If I wanted to implement RAFT I would probably pick Go. If I want a simple REST/GraphSQL server then Node.js is so much easier. `async/await` is nicer for me than goroutines and I find my code easier to reason about.
Full disclosure: I'm a Node.js core team member and a Go fan. Part of my reasoning might be how much nicer Node.js got these last couple of years.
Can you elaborate on why you think async/await is easier (for you) to reason about than a goroutine and a channel?
An async function is explicitly async (in its definition).
The syntax is obvious which is why JavaScript chose to go that route (rather than adopt channels at a language level for example).
Plus, 95% of the time I care about the singular return value of a function - I just want something that's a function but async - and not a green thread that's using a channel to send information back. Both conceptually and in the code it's a lot simpler.
Or
const results = await Promise.map(xs, x => process(x), { concurrency: 2 })
Or having 2 concurrent loops processing some data?All things that require more boilerplate in Go while trivial in Node.
It's hard to know without the code, but the author seems to be doing a few things wrong:
1. You only need a few channels, not N. Maybe 4-5 is enough. 2. In terms of goroutines you only need as many as are actively communicating with your server. So creating a new connection creates a goroutine, sending a message to a channel creates a goroutine etc. 3. You need something like Redis if you want to support multiple nodes
For inspiration check out this awesome project: https://github.com/faye/faye-redis-node
This will perform well in Node and even better in Go.
In many typical web applications you have less if no interaction between the connections, but rather complex logic running at each request. There the event-based approach, which must not block, is getting more complex to manage and you want to use all cpus in the system. There a goroutine based approach should shine much stronger, as the goroutines may block and you don't have to spread your program logic across callbacks.
I spend a lot of time thinking about why I'm creating a certain data model, whether I might need to change something in future, etc. About 60% of my productive time is spent thinking about how and why, so I hardly refactor. However, when the need arises, I wish I had something like Kotlin.
For the past few months I've been writing new JS code in TS, adding types here and there, I haven't tried out Kotlin on JS, but I'm hoping to go there.
I'm learning Go, but for other reasons. I find JS to be performant, my oldest active codebase has been around since the v0.8 days.
I had more than 90% test coverage, with meaningful tests so I didn't really find too many bugs but I could delete a lot of tests and run-time checks which were making sure stupid input don't cause unexpected behavior.
When I check my commits & logs, LOC/hour didn't change significantly but bugs/month reduced to nearly half if my SQL skills aren't failing me.
TypeScript and PureScript.
Even better, they play fairly well together. You can work in Purescript but still provide well typed integration points for contributors that can't.
And uh, no one here is talking about how fantastically slow Go channels are. But I just saw the code last night, and it's not hard for 20 year old techniques to beat out a "futex for literally every message send" technique.
Talking about channels, I haven't gotten there with my Go learning. Few things beat websockets + a nice wrapper (with simplicity). For example, I'm doing realtime transit, and as part of it I'm sending out hundreds of vehicle positions per 5-7 seconds.
On the back-end I have a pubsub through a gRPC stream, and I stream positions to socket.io topics. Works beautifully, I can't imagine having to roll it out manually over websockets. In another thread here, someone mentions how non-trivial working with websockets is in Rust. I think that until we have a socket.io (server) version for other languages, Node.js will always beat most other languages.
Please do consider checking out PureScript. It's a lot of the good parts of Haskell and some of it's libraries look like sorcery they're so good.
goroutines are now 2k. If you use 2 goroutines per connection that's 4k. If you have 10k connections that's roughly 20mb only which is very reasonable.
for {
select {
case <-finish:
done <- true
return
default:
// read from the socket
}
}
You need another thread to listen for messages on a channel and send them out the socket
typical: for {
select {
case <-finish:
done <- true
return
case msg <- msg_out:
// write msg to socket
}
} for {
select {
case <-finish:
done <- true
return
case msg <- msg_out:
// write handling
case <-pubsub:
// handle oob controls
default:
// read handling
}
}If he is in control of his protocol, why did he not shape it to suit his parser library? Instead of this:
message1 = { "@": "foo", "foo1": 1, "foo2": 2 }
message2 = { "@": "bar", "bar1": 1, "bar2": 2 }
Do this: message1 = { "foo": { "foo1": 1, "foo2": 2 } }
message2 = { "bar": { "bar1": 1, "bar2": 2 } }
Then you can read both types of messages into a single Go type, type Mesage struct {
Foo *FooMessage
Bar *BarMessage
}
After parsing, that element which is not nil tells you which type of message was sent.Please stop infecting us with Go-specific type tags (that btw make the protocol versioning story much more complicated) and either demand Go support generics like every other modern statically typed language, or accept your language is ill-equipped for parsing and check if things = nil a lot more.
Don't advocate for pushing your tooling's problems out on your peers.
If you've got an opcode, name it semantically with a name field. If you've got a type to serialize, do it semantically and don't just do like what the library I ported does and put the Golang type annotation in a string.
And don't let objects inhabit a fusion like this. It leads to surprising behavior with malformed messages.
I'm implementing channels/coroutines in clojure[0] and js[1].
These are alpha quality right now, but I already have channels with backpressure and async go blocks. I wrote the js implementation to show that these could be ported to any language.
The main thought behind this is: CSP - S = Communicating Processes = Green Threads
[0] https://github.com/divs1210/functional-core-async [1] https://github.com/divs1210/coroutines.js
See the top answer for this question in StackOverflow: https://stackoverflow.com/questions/5062614/how-to-decide-wh... . The top answer with 1360 upvotes is wrong and had little to do with the question. This is a recurrent thing in the node world.
Go ask this same thing on an Erlang, Haskell, Rust, etc. forum and I am sure the right answer will come up quickly.
https://github.com/housleyjk/ws-rs/
In any case, that is where I'd begin :)
Things become a mess in no time, tooling changes constantly, and maintaining legacy code while developing new parts with current best practices helps you build a Frankenstein in no time.
(and don't even mention about coffee, or it'll make me start ranting about the move from coffee to es6 and now TS!)
No one forces you to upgrade your perfectly functional es5 code to es2015, es2017, or typescript.
Yes, node moves fast and new features are introduced, but everything is largely backwards-compatible. You don't have to incorporate every shiny new feature, framework, or tooling.
If you do so by choice, this rant is rendered meaningless.
I sincerely suggest that you stop doing this. If JS folks had 10% of the "don't fix what isn't broken" philosophy of the Python developers who were presented with v3 as the future, we wouldn't have any of those "js fatigue" posts.
https://medium.com/@tjholowaychuk/farewell-node-js-4ba9e7f3e...
https://www.mappingthejourney.com/single-post/2017/08/31/epi...
There is no way Synapse is a good fit for something like RPi. Maybe when Dendrite is ready, it will make this possible.
For the receiving end, go can use http handlers for websockets. So when a message from any websocket is received the handler will be spawn and process it (just as any http request). Preferably dispatch it to a big central channel.
Keeping separate channels for each websocket seems overkill to me. Go has maps. Create a map with all the websocket connections and maybe smaller maps for each chat room, then set n workers to listen to the central channel and dispatch messages directly to each room member.
(If you're a 10x engineer, check his new tool for 1e10x engineers: https://news.ycombinator.com/item?id=15731936)