Why do people say that GoLang has better performance than a NodeJs Server. I did a test where I sent 1000 simultaneous requests to write to a topic on Google Cloud Pub/Sub. I tried with a Gin Golang server, and also the normal HTTP Server.
I created a docker container and assigned 250mb of memory. The server crashed very quickly. I then increased the memory for the experiment up to 500MB. The server still crashed 8 out of 10 times. GoLang is able to handle 1000 simultaneous requests for simple operations such as just returning a JSON response, but seems to run into issues when writing to a db or publishing to a topic. I also logged the number of go-routines with the runtime. The amount was well over 1000 when the 1000 requests hit.
I ran the same experiment with a NodeJs Server sending 1000 simultaneous requests to publish a message to a topic. I was expecting the container to crash, but the memory in the docker stats showed that it never even went above 100MB. The messages were all published to the topic very quickly and the memory dropped back close to zero less than a second later.
And now for the code.
// Publish Message Function Node JS
const pubSubClient = new PubSub({
projectId: "project-id"
});
export async function publishMessage(msg: string) {
// Publishes the message as a string, e.g. "Hello, world!" or JSON.stringify(someObject)
const dataBuffer = Buffer.from(msg);
const topicName = "instant-messages";
try {
const messageId = await pubSubClient.topic(topicName).publish(dataBuffer);
console.log(`Message ${messageId} published.`);
} catch (error: any) {
console.error(`Received error while publishing: ${'message' in error ? error?.message : ''}`);
process.exitCode = 1;
}
}
Here is the index for the NodeJS app
// Index for NodeJS pubs
import express from "express";
import cors from "cors";
import routes from "./routes/index";
const app = express();
const port = process.env.PORT || 5000;
app.use(cors());
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(routes);
app.use(express.raw({ type: "application/octet-stream" }));
app.listen(port, () => console.log("Connected on port", port));
Here is the route for publishing to the topic
import { Router } from "express";
import { publishMessage } from "../pubsub/publishMessage";
const router = Router();
router.post("/", async (req, res) => {
try {
await publishMessage(JSON.stringify(req.body));
res.json(true);
} catch (e) {
console.error("Error posting message", e);
return res.status(500).json("Processing Error");
}
});
export default router;
And now for the slow non memory efficient GoLang code. First with the net/http package
package main
import (
"fmt"
"runtime"
"net/http"
"bitbucket.org/websockets/controllers"
"bitbucket.org/websockets/db"
"bitbucket.org/websockets/handler"
// "bitbucket.org/websockets/pubsubs"
// "github.com/gin-gonic/gin"
)
func main() {
// server := gin.Default()
db.ConnectToMongo()
// controllers.Router(server)
// go pubsubs.PullInstantMessages()
// server.GET("/ws", func(c *gin.Context) {
// handler.WsHandler(c.Writer, c.Request)
// })
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
handler.WsHandler(w, r)
})
http.HandleFunc("/api/messages", func(w http.ResponseWriter, r *http.Request) {
controllers.CreateDefaultMessage(w, r)
fmt.Println("Number of go routines", runtime.NumGoroutine())
})
fmt.Println("number of go routines", runtime.NumGoroutine())
// server.Run(":8080")
http.ListenAndServe(":8080", nil)
}
Create Default Message which will call the pubSub
package controllers
import (
"encoding/json"
"fmt"
"net/http"
"runtime"
"bitbucket.org/websockets/models"
"bitbucket.org/websockets/pubsubs"
"github.com/gin-gonic/gin"
)
func CreateDefaultMessage(w http.ResponseWriter, r *http.Request) {
fmt.Println("Number of go routines", runtime.NumGoroutine())
var message models.Message
err := json.NewDecoder(r.Body).Decode(&message)
if err != nil {
fmt.Println("Error", err)
json.NewEncoder(w).Encode(err.Error())
return
}
fmt.Println("message received", message)
b, err2 := json.Marshal(&message)
if err2 != nil {
json.NewEncoder(w).Encode(err2.Error())
return
}
err3 := pubsubs.PubIMessage(b)
if err3 != nil {
fmt.Println("Error publishing message")
}
json.NewEncoder(w).Encode(true)
}
Here is finally where the publishing to the topic is occurring
import (
"context"
"fmt"
"os"
"path/filepath"
"cloud.google.com/go/pubsub"
"google.golang.org/api/option"
)
func PubIMessage(msg []byte) error {
fmt.Println("Do I get run?")
return publish(PID, IM, msg)
}
func publish(projectID string, topicID string, msg []byte) error {
ctx := context.Background()
fmt.Println("Is the publish getting run?")
s, er := filepath.Abs("./key.json")
if er != nil {
fmt.Println("Error resolving path to credentials", er)
return er
}
client, err := pubsub.NewClient(ctx, projectID, option.WithCredentialsFile(s))
if err != nil {
return fmt.Errorf("pubsub.NewClient: %v", err)
}
defer client.Close()
t := client.Topic(topicID)
result := t.Publish(ctx, &pubsub.Message{
Data: []byte(msg),
})
// Block until the result is returned and a server-generated
// ID is returned for the published message.
id, err := result.Get(ctx)
fmt.Println("result of publish", result)
if err != nil {
return fmt.Errorf("get: %v", err)
}
fmt.Fprintf(os.Stdout, "Published a message; msg ID: %v\n", id)
return nil
}
And now here is the Gin implementation. It's pretty much the same, just without being commented out.
// Main entry
package main
import (
"bitbucket.org/websockets/controllers"
"bitbucket.org/websockets/db"
"bitbucket.org/websockets/handler"
"bitbucket.org/websockets/pubsubs"
"github.com/gin-gonic/gin"
)
func main() {
server := gin.Default()
db.ConnectToMongo()
controllers.Router(server)
go pubsubs.PullInstantMessages()
server.GET("/ws", func(c *gin.Context) {
handler.WsHandler(c.Writer, c.Request)
})
server.Run(":8080")
}
Here is the controllers which has the Router function that was in main
package controllers
import (
"encoding/json"
"fmt"
"net/http"
"bitbucket.org/websockets/models"
"bitbucket.org/websockets/pubsubs"
"github.com/gin-gonic/gin"
)
func createMessage(c *gin.Context) {
var message models.Message
err := c.ShouldBindJSON(&message)
if err != nil {
fmt.Println("Error", err)
c.JSON(422, err.Error())
return
}
fmt.Println("message received", message)
b, err2 := json.Marshal(&message)
if err2 != nil {
c.AbortWithError(http.StatusBadRequest, err2)
return
}
pubsubs.PubIMessage(b)
c.JSON(http.StatusOK, true)
}
And the Actual Publishing code is still the same
import (
"context"
"fmt"
"os"
"path/filepath"
"cloud.google.com/go/pubsub"
"google.golang.org/api/option"
)
func PubIMessage(msg []byte) error {
return publish(PID, IM, msg)
}
func publish(projectID string, topicID string, msg []byte) error {
ctx := context.Background()
fmt.Println("Is the publish getting run?")
s, er := filepath.Abs("./key.json")
if er != nil {
fmt.Println("Error resolving path to credentials", er)
return er
}
client, err := pubsub.NewClient(ctx, projectID, option.WithCredentialsFile(s))
if err != nil {
return fmt.Errorf("pubsub.NewClient: %v", err)
}
defer client.Close()
t := client.Topic(topicID)
result := t.Publish(ctx, &pubsub.Message{
Data: []byte(msg),
})
// Block until the result is returned and a server-generated
// ID is returned for the published message.
id, err := result.Get(ctx)
fmt.Println("result of publish", result)
if err != nil {
return fmt.Errorf("get: %v", err)
}
fmt.Fprintf(os.Stdout, "Published a message; msg ID: %v\n", id)
return nil
}
CodePudding user response:
Go has an upper hand in the case of raw performance. Go can work without an interpreter and is compiled right into machine code. It gives Go the same level of performance as low-level languages.
Both Go and Node.js feature a garbage collector, which prevents memory leak and ensures stability with memory footprint. In this way, Node.js is extremely helpful with memory management as it works as an efficient garbage collector.
And of course, Node.js is single-threaded, that means it can only do one task at a time, while Golang have concurrency built in.
CodePudding user response:
While GoLang has tremendous speed, it is a memory hog. https://tpaschalis.github.io/goroutines-size/#:~:text=Some people learn that “a,represents its initial stack size. Each go routine can take up a few kb of memory. Depending on your use case. GoLang can still be a great option if you are not concerned about cost, and are more concerned with speed. It might also be better suited for different operations instead of handling high traffic websites, but might be better suited for processing operations very quickly.
