At first, we thought that sticking with a familiar language was the responsible thing to do — we’re a small team, already engaged in two adventurous moves: a switch to microservices and a full rebuild of our legacy web application, a high-traffic game platform.
However, in the end we decided to go all the way and dropped PHP for Go. In this post we’ll explain why. We’ll also share some thoughts about databases within our microservices architecture.
Microservices and PHP: a conceptual misfit
Our familiar language was PHP. It powered our legacy application, and we had two vague arguments to stick with it:
- We know PHP and its quirks. It works. Slack is taking it seriously. Why drop something that works for us?
- There are lots of PHP developers out there. PHP will make it easier to grow our team.
That sounded fair, but we soon abandoned those thoughts when it became clear that PHP really wasn’t the right choice for our case.
We’re migrating to a microservices architecture because we want our high-traffic infrastructure (2M daily active users) to be scalable. In the long run, as we build towards 10M users and beyond, but also on a daily and hourly basis: as entire countries go to sleep and wake up again, our infrastructure should scale accordingly.
PHP doesn’t work well with that:
PHP has high startup costs. It was once designed (or grew) to run scripts that are short-lived, therefore persistency is not native to the language. This means that for every request, database connections and classes have to be instantiated, which adds unnecessary latency. There are solutions like, for example, connection pooling via PHP-FPM or Apache, or C bindings to get persistent connections with Redis. But since we’re aiming for high performance, these dependencies made us question PHP as the right tool for the job.
Containerized PHP is a minefield. PHP requires Nginx and PHP-FPM (or similar) for process management and connection pooling. This means that for every deployed microservice, PHP-FPM and Nginx have to be running as well. This wastes resources and makes scaling inefficient. There’s also the issue of configuration for optimization. Optimizing a single instance of PHP to perform well on a machine is hard enough already as it requires understanding and configuring PHP, PHP-FPM and Nginx. We can’t imagine the world of pain we’d end up in trying to configure multiple PHP stacks in an elastic Kubernetes environment, where you don’t know what’s running on the same machine.
With microservices the complexity is in the architecture: you’re dealing with a complex interacting system of simple services. Since we already committed to this architecture, adding more long-term overhead with a conceptual misfit for a language would have felt wrong.
And what about the hiring argument? We found it to be invalid for our case. Like microservices, we believe that developers should be language-agnostic. We’d much rather hire a smart developer willing and able to learn a new language to get things done, than a specialist stuck in their ways. In that sense, dropping PHP is actually liberating.
Going for Go
The two main contenders for the spot as our language of preference were Node.js and Go. We did some research and decided for Go over Node.
So why Go?
Performance. Go binaries spawn a long-running process, meaning low start-up costs for every request and persistent connections. This — together with the fact that Go (with its goroutines) was designed for networking and multicore computing — makes it super-fast and efficient in handling large amounts of concurrent requests.
Go can compile to a single binary that is small and portable. This makes it exceptionally suitable for use in a Docker container. Deploying our Go containers takes just seconds due to their small size (most are 4–5MB) and thanks to static linking there’s no need for OS or runtime dependencies inside the container. For reference, our front-end containers are around 55MB when using a Node alpine image.
Go is strictly typed. This makes internal communication in your code more reliable. It also helps catch issues during build rather than during runtime.
Go’s toolchain is incredible. While tooling is an issue in a lot of languages, Google decided to tackle this right from the start, providing a lot of common tooling as part of the language’s installation.
We considered the downsides as well:
- Go doesn’t ship with dependency management. They’re working on that though, and when it arrives, it’ll most likely be good. For now you can either check in your vendors or give it a shot with a tool like Glide.
- More boilerplate code. This is the flip side of Go’s elegance and simplicity.
However, we choose to accept this: writing Go does take a bit of effort, but it leads to quality and keeps us in touch with what our code is actually doing.
That’s not to say that Go is all we’ll ever use. For server-side rendering we use Node, because it allows us to share logic between our front and back-end. We can also imagine using Java for specific problems, because it’s been around for so long and has tons of libraries. We want to use the best tool for the job. That said, for most purposes Go will be our tool of choice.
As we began to write our first services in Go, we also started thinking about databases. We’re used to MySQL which has served us well in the past, but it was often a bottleneck for performance.
In our legacy stack we also used a lot of Redis for caching, which was great for performance because it effectively reduced the amount of expensive joins that we were doing. So when we started exploring databases in our new stack, it made sense to explore the NoSQL space to see if we could avoid those joins altogether.
We played around with two databases:
MongoDB — because we were curious to see if a document store would be a good solution to store games with lots of metadata. What we didn’t like though: we have to manage it ourselves at Google Cloud, and according to the community, it doesn’t scale well at all. We like to avoid DevOps work where we can, so this was a deal breaker.
Cassandra — because it’s a database known to scale well and is being used by high-traffic platforms Netflix and Reddit. What we liked: it’s intensely fast and scales linearly. However, we found that it’s just too complex for content management purposes. Cassandra works well if you know exactly how you’re going to query your data. This might be the case for an analytics service with large sets of data, but in an agile product design environment, where use cases adapt as a product evolves, Cassandra is a beast that is powerful, yet overweight for most purposes.
Sticking with SQL
In the meantime, we’d grown closer to the concept of microservices, and more comfortable with the idea of building small, independent services that get something done and are written to be easily upgraded or replaced whenever needed.
That’s why we decided to stick with MySQL as our default database. We’ve been working with it for years and know how to design database schemas for high performance. It doesn’t scale linearly, but that’s okay for now: because of the modular nature of a microservices architecture, application load is distributed over many different microservices on many different machines. And with every microservice potentially having access to their own 32-core database machine with a couple of read replicas, we’ll get a long way.
We’re quite happy that we’re not overengineering for now. And if there’s a service that does require Cassandra or some other database, there’s nothing that keeps us from migrating that particular service.
So why MySQL? For now mostly because it’s managed at Google Cloud and we’re pragmatic when it comes to DevOps. We’d like to try Postgres because it’s open-source, has a strong community, and apparently has improved a lot over the years. So depending on when it comes out of alpha at Google Cloud, we might give that a shot as well.
Update: the folks at Reddit rightly pointed out that we misphrased our argument about PHP’s start-up costs. Our point still stands, but we’ve cleaned up this paragraph for the sake of accuracy.
This post is part of a series by our team covering:
- Our Web Platform (Part 0)
Explaining the context for our stack change and objectives for our new architecture.
- Our Infrastructure (Part 1)
Covering our choices for Kubernetes, Prometheus, gRPC and Google Cloud Platform.
- Our Front-end (Part 3)
React/Redux and its implications on building a brand new front of house.
→ Liked this story? Please click the 👏 below so other people can also find it!