NodeJS + MariaDB Galera — is it the right choice for scalable ecommerce API?
When starting a new project, every developer has a hard time deciding on which technologies to use. You spend day and night with lots of questions spinning inside your head like:
- Which programming language should I choose?
- Is it fast? How does it scale?
- How big is the community? Are there enough libraries?
- How hard is it to find developers? Are they expensive?
And probably many more. This article will consider these questions when developing a scalable ecommerce API using NodeJS runtime together with MariaDB Galera database.
Is JavaScript the right choice?
JavaScript was a language generally created for the web, but that changed with the release of NodeJS. It uses Google Chrome V8 engine to run JavaScript outside the browser. Generally, JavaScript is a single-threaded language, but NodeJS partially solves this problem by providing an event-based non-blocking API for most of I/O operations, so they can be run concurrently. However, JavaScript code still remains single-threaded.
Community
According to StackOverflow Developer Survey JavaScript is the most popular language in the world. Because of its popularity, developers are easy to find and not expensive. According to the same study, the average salary of a JavaScript developer is $56k, compared to $80k of a Go developer. Also, JavaScript developers are more flexible because a single person can potentially work both on frontend and backend of the project, if the language is the same.
Choosing JavaScript as a language also allows to partially or fully switch to TypeScript later, which is among the most loved languages by developers.
JavaScript uses the NPM package manager. It has the largest package registry with more than 825K packages. That’s almost 4 times more than Pip, which is the biggest registry for Python packages having more than 220K projects. NPM probably contains every package you can think of — everything from as silly as is-odd to large-scale projects, so if you need a library, you can be almost sure that it’s already there.
Performance
NodeJS constantly receives criticism, because it’s a single-threaded language. Articles like this and this, clearly show better raw performance of Go compared to NodeJS. However, raw performance is not what matters the most for our use case. The most important part of the API is a web server and it turns out that NodeJS could be among the best in that category. A web server provided by µWebSockets.js library is among the fastest. The benchmark presented below is done by their development team and it clearly shows that the µWebSockets.js web server can handle much more requests than the ones provided in Go or Rust languages.
Scalability
The benchmark done by µWebSockets.js shows better performance among the competitors. However, it does not show another very important factor for large projects — scalability. To test that, I created a sample API https://github.com/MindaugasVarkalys/sample-api, which uses µWebSockets.js web server, and deployed it on a Kubernetes cluster.
Kubernetes is a open-source system built by Google which enables running, managing and deploying containerized applications on multiple servers. In order to run my application on Kubernetes, I built a Docker image and uploaded it to DockerHub https://hub.docker.com/repository/docker/minvar/sample-api
Testing environment
I built a Kubernetes cluster using Rancher RKE installer on 3 Google Cloud n1-standard-2 VM instances running Ubuntu 18.04. I didn’t use the Google Cloud Kubernetes Engine in order to learn building a Kubernetes cluster from scratch.
I also created 4 separate n1-highcpu-2 instances for benchmarking. I used Hey benchmarking tool to do requests to my API endpoint. I ran 100 concurrent requests from each machine for 1 minute concurrently and combined the results. This is the exact command I used to run benchmarks:
hey -z 1m -c 100 MY_DOMAIN
Vertical scaling
To test vertical scalability, I benchmarked a single pod with different limits on CPU usage. A usual metric for CPU limits on Kubernetes is miliCPUs. 1 miliCPU is equal to 0.1% of a single CPU usage, so 100miliCPUs is equal to 10% of single CPU usage. This is the result I got:
The chart shows that NodeJS scales linearly on a single CPU. However, NodeJS does not scale at all beyond a single CPU. This a limitation of JavaScript, since it cannot run on many threads, but that could be solved by horizontal scaling.
Horizontal scaling
Horizontal scaling was tested by increasing the number of pods in the cluster and checking how it affects the overall performance of the web server. The CPU limit of 100miliCPUs was put on every pod to remove the effect of vertical scaling in this testing. This is the result I got:
The chart shows that NodeJS linearly scales horizontally. However, the curve of horizontal scaling is not that steep as scaling vertically on a single CPU — increasing number of pods twice, the throughput of the web server increases only about 70%.
Ingress bottleneck
Kubernetes uses Ingress as reverse proxy abstraction which acts as a Level 7 Load Balancer and allows to route HTTP requests to a correct service depending on domain or URL. There are plenty of Ingress Controllers which implement that abstraction and could be installed on a Kubernetes cluster. During this benchmark I tried using an ingress-nginx and Traefik Ingress controllers. However, it turned out that the performance of the NodeJS web server was limited by the Ingress controllers, since they were much slower — ingress-nginx managed to run only around 4000 and Traefik around 7000 requests/sec. Having Ingress is a necessity for real life applications, since that’s the only way to allow different functionalities to be handled by different services, so let’s talk how this bottleneck can be solved without removing Ingress.
In my configuration, all the users are directed to the Ingress controller of the first node. That creates a bottleneck, since all the traffic has to be handled by the Level 7 load balancer running on Node1. An illustration of the bottleneck can be seen below.
In real life applications, this bottleneck can be solved by adding an additional Level 4 load balancer in front of Ingress. That way the cluster theoretically should handle as many time more requests as there are nodes in the cluster.
In the end, for this benchmark I decided not to use Ingress at all and created a NodePort service, which opens a port on each node of the cluster directing to Level 4 service load balancer. That way I was able to test the real performance of the web server without any delay created by the Ingress Controller or an additional load balancer.
Is MariaDB Galera the right choice for the database?
Requirements
When choosing a database, the most important question to ask yourself is what kind of data will be stored there? For the ecommerce API use case, we have entities for Users, Categories, Products, Orders, Carts, etc. These entities are strongly related to each other — some relations are One To Many (e.g. User-> Orders), some Many To Many (e.g. Categories -> Products).
Knowing that, the other important decision has to be made — whether to choose a relational SQL database or take a NoSQL one. NoSQL databases typically have much better scalability. However, they usually achieve that by not having the strong schema, foreign keys or ACID transactions functionalities. In my opinion, all of these functions are highly needed for the ecommerce API, because they prevent data corruption and always keep the data in a consistent state. Data is the most important part of any business product. If it’s lost or corrupted, it could make the entire company go bankrupt. Thus, the critical features given by SQL databases should not be traded for the scalability benefits provided by NoSQL databases.
In addition, most of companies using ecommerce already have their data inside the company’s internal database, which most of the time happens to be a SQL database. Thus, using a SQL database would make a transition of data easier. Apart from that, SQL language is familiar to most developers, data scientists, analysts and even managers. Therefore, the data stored in the database can always be easily understood by almost everyone in the company.
What is MariaDB?
MariaDB is known as a relational SQL database which is almost identical to MySQL, but it has some performance improvements. It uses the same MySQL InnoDB storage engine and is also fully compatible with most MySQL drivers.
Typically, MariaDB scales horizontally by using a master-slave architecture. There is a single master, which handles all write operations and replicates them to slaves, so read operations can be handled by slaves concurrently. However, this architecture allows to scale read operations only, since there always has to be a single master. The master also becomes a single-point of failure. If it fails, the database cannot function anymore.
What is MariaDB Galera?
MariaDB Galera Cluster is a special architecture of MariaDB which solves a single master problem by providing a synchronous multi-master cluster. Instead of a single master, each node in the cluster becomes a master and can handle both read and write operations. Thus, both write and read operations can be horizontally scaled and there is not a single point of failure. However, the majority of nodes still has to be functional in order for the database to work properly, since this is a requirement for Paxos algorithm, which is used for write operations replication across the cluster.
Scalability
It is stated that using MariaDB Galera near linear scalability may be reached depending on the application. However, there is no official benchmark done showing how well it scales for simple operations. I will test that using the same Kubernetes cluster, which was used for NodeJS testing.
Unfortunately, I was unable to do tests for vertical scaling, since the CPU limit feature is not supported on the stateful Kubernetes pods which are used by the databases. There are plenty of single node vertical scaling tests of MariaDB and vertical scaling is not that important for us. Thus, I will skip a vertical scaling test of a single pod.
Horizontal scaling
The horizontal scaling test was done by increasing number of pods in the cluster and checking how long it took to run 50K select by key queries and 10K insert queries with 100 concurrent clients. The tool used was mysqlslap and the exact commands used for testing can be seen below.
Select queries:
mysqlslap -h MY_HOST -p --create-schema=my_database --auto-generate-sql --number-of-queries=50000 --concurrency=100 --auto-generate-sql-load-type=key --auto-generate-sql-add-autoincrement --auto-generate-sql-write-number=50000 --auto-generate-sql-unique-query-number=50000
Insert queries:
mysqlslap -h MY_HOST -p --create-schema=my_database --auto-generate-sql --number-of-queries=10000 --concurrency=100 --auto-generate-sql-load-type=write --auto-generate-sql-add-autoincrement
This is the result I got:
As it can be seen from the chart, increasing the number of pods increases the throughput of selects, since they are distributed across the cluster. However, the performance of the selects starts to decrease when there are more than 5 instances. This is because there are only 3 nodes in the cluster and the pods do not have any CPU limit, so having too many pods overloads the nodes and the performance starts to decrease. So selects are effectively scalable until the number of pods overtakes the number of nodes in the cluster.
On the over hand, the performance of inserts decreases with an increasing number of pods. This is because inserts have to be duplicated across the cluster. According to the documentation, decreasing performance should happen only with simple inserts. Complicated inserts and transactions, which need lots of calculations, should still be scalable, since most of the calculations can be done locally and only data changes are broadcasted to the cluster.
For our use case, the low performance of inserts are not that relevant, since as mentioned earlier, inserts happen rarely in ecommerce. Much more often users query the data, so the scalability of selects is much more important and it can be seen by our benchmark.
Conclusion
NodeJS is a great platform which is definitely a good choice for the scalable ecommerce API. It has large community, tons of libraries and great scalability. The only downside is that it’s single-threaded, but that can easily solved by having a separate NodeJS instance for each CPU core.
On the other hand, MariaDB Galera choice depends on the scale of ecommerce. It has tons of great features like SQL language, ACID transactions, strong schema and foreign keys. However, all of this is achieved by sacrificing the scalability of inserts. Even though inserts happen rarely in ecommerce, it still can create problems, if the website is used at the global scale. Despite that, MariaDB Galera is a perfect choice for small and medium size ecommerce, but it should be used by always keeping in mind that the database switch might be needed if ecommerce reaches global scale.
References
- https://en.wikipedia.org/wiki/Object-relational_mapping
- https://www.toptal.com/back-end/server-side-io-performance-node-php-java-go
- https://www.intellectsoft.net/blog/nodejs-vs-golang/
- https://github.com/uNetworking/uWebSockets
- https://levelup.gitconnected.com/will-node-js-forever-be-the-sluggish-golang-f632130e5c7a
- https://nodejs.org/en/about/
- https://www.npmjs.com/
- https://pypi.org/
- https://www.npmjs.com/package/is-odd
- https://github.com/MindaugasVarkalys/sample-api
- https://kubernetes.io/
- https://github.com/rakyll/hey
- https://github.com/kubernetes/ingress-nginx
- https://kubernetes.io/docs/concepts/services-networking/ingress/
- https://en.wikipedia.org/wiki/Load_balancing_(computing)
- https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/
- https://kubernetes.io/docs/concepts/services-networking/service/#nodeport
- https://en.wikipedia.org/wiki/ACID
- https://en.wikipedia.org/wiki/InnoDB
- https://mariadb.com/kb/en/about-galera-replication/
- https://mariadb.com/kb/en/mysqlslap/
- https://en.wikipedia.org/wiki/SQL
- https://sequelize.org/
- https://typeorm.io/
- https://www.typescriptlang.org/
- https://blog.cleancoder.com/uncle-bob/2011/11/22/Clean-Architecture.html
- https://en.wikipedia.org/wiki/Reverse_proxy