A while ago, I was asked to implement an Auth¹ microservice supposed to run on a low-resource machine². As I’m experienced in both Java and PHP — and obviously, talking about low resources, Java is a no-no — I decided to use PHP and Lumen (as it’s expected to be lightweight and stunningly fast³).
I started working, and not-surprisingly thanks to open-source packages, I could wrap all the things up in a couple of days. Then, I prepared the Dockerfile and the integration with CI build system. Soon after, I announced the employer that everything is ready to go. Sure, he was very happy and I was proud as much for getting the job done so fast.
But it didn’t take too long for him to call me back: “I’m really grateful for your efforts, but I remember explaining about low resources to you.” “Yeah, but you never mentioned how much low!”, I replied, slowly realizing the issue⁴. That being said, an early v2 release was not hard to expect.
I had a bunch of options on the table: trying to optimize the code, decreasing the number of PHP-FPM processes, using a more lightweight framework (e.g., Slim), etc. But I decided to choose the hard way: another programming language.
Before getting to the main topic, I have to note that: before deciding to choose another programming language, first, I reviewed the work done so far, in order to find out what was happening beyond my expectations.
The Dockerfile was based on an Alpine Linux distribution provided with PHP 7.2, Nginx and 8 PHP-FPM workers. The base Docker image was about 300MB (uncompressed) and the final image containing the project’s source code (including composer packages⁵) summing up to about 350MB.⁶
However, the Docker image size certainly says nothing about the actual memory/CPU usage, so I planned to run some benchmarks. Using a simple docker-compose file, the results show that it consumes about 130MB of memory, with no traffics. Hmmm… compared to, e.g., 15MB used by PostgreSQL database, it seemed a lot, but I could bear with that.
The real issue came along when putting the microservice under pressure. The memory usage suddenly raised up to 400MB, concluding that each PHP-FPM process uses an extra amount of 30MB for processing requests.⁷ This was very unpleasing, especially compared to the max amount of 25MB consumed by PostgreSQL. That was something I wouldn’t expect.
First Experience with Go
Go was my last decision. As someone in-a-hurry to ship a release as fast as possible, deciding on a completely different language, was a brave one. I was pretty familiar with Go’s C-like syntax, but beforehand, I had no real experience with it. Thus, I started learning Go in no time.
If you know C or one of its descendants, as of syntax, there’s not much to learn, however, the design philosophy is almost different.⁸ Especially, if you’re used to object-oriented languages, I suggest you to put it aside and learn Go from ground up.
Learning Go, the first thing that catches attentions is the way that Go handles errors. Clearly, Go designers do not believe in exceptions. Any exceptional case is either an error, or a specific condition which needs to be handled properly.⁹ This is a good practice that can lead to more stable softwares, but also a burden by fairly making
if the most recurring keyword throughout every Go source code.
By the way, it was really fun to learn and develop in Go. I enjoyed the compilation speed, the most. Personally, I liked the way Go deals with dependencies in a convention over configuration manner. But, unfortunately, versioning is almost gone, hoping that some stupid library provider wouldn’t decide to make any breaking changes.¹⁰
With all that said, only a week it was, the time required for me to learn and re-implement the Auth microservice completely in Go. Now, it was time to try some benchmarks; not to ship another release, blindfold.
What did I get?
One of the things that fascinated me was how easy it is to dockerize a go program. The following is what my Dockerfile looked like¹¹:
If you’ve ever dockerized a PHP project before, you would see that it couldn’t be any easier. I was happy that I didn’t need to take care of PHP modules, FPM and Nginx configurations, and particularly, composer dependencies, anymore.
As of benchmarking, the Docker image was only 30MB¹², that eventually, could lead to a faster deployment. And, the most interestingly, the memory usage was now around 4 to 10MB. It couldn’t get any better, right? Besides, as Go programs run natively, anyone can bet that it would be a lot faster, too.
My first real experience with Go was great. It is pretty easy to learn and catch up. The compilation is super fast and resulting executable runs perfectly, and since it has few external dependencies, it’s also easy to deploy. However, I had no adventure with Go’s advanced features, like concurrency and channels, but evidence shows that they’re perfect, too.
Besides, Go is ideal for microservices architecture, because dockerization is very trivial and easy, and the Docker images are very small and consistent. Also, it has a great tooling and support, both by Google and the community.
All that being said, do you suggest that I have to migrate to Go world? Absolutely, not! What I explain was a small example of how PHP is compared to Go in development of microservices. But for big projects, there are several issues:
- Hunting and hiring experienced Go developers is not as easy as widespread programming languages like PHP and Java. Also, Go developers are mostly multilingual and it’s hard not to confuse somewhat contradicting programming patterns and idioms.
- Versioning isn’t yet supported by many community-driven libraries which may cause maintenance issues.
- The IDE support is not yet mature. For this example, I used VS Code with Go extension, but it lacked essential features like refactoring. However, I didn’t try Jetbrains GoLand as it wasn’t free.
- Some prefer to call it a User microservice. User/Auth microservices don’t have much functionalities, but typically they need to operate in high traffic situations.
- Poor low-budget startups!
- Don’t believe in commercials!
- Honestly, I never felt it’s necessary to run a profile, thinking “C’mon! PHP is not that bad.”
- For sake of a faster startup, I decided to package the so-called vendor directory along with the source code.
- That, for sure, had pissed off the DevOps engineer.
- These are not strict numbers. The memory usage can highly vary depending on the application.
- Design Philosophy in Go (Video)
- Go presents so-called specific conditions in two ways: either by returning a bool or a special type known as error. So, error’s are not actual errors, but those which panic and you from-which may recover.
- I used the common go get method for resolving dependencies and I was too lazy to learn a new tool, such as dep.
- Using forego was really unnecessary, but I included it if someday I might require to run other processes in parallel. On the other hand, forego ensures that my program will run after abnormal terminations without the need for container to be restarted. Also, bash was strangely mandatory to run forego.
- Without forego, it was only 15MB.