From PHP to Golang or Rust? As Long You Are Using Raw SQL Calls…

Not all apples drop from the same tree, some are faster to hit the ground

Italo Baeza Cabrera
The Startup
7 min readSep 1, 2020

--

I was tasked not long ago to make a tiny private prototype. The old application was a monolithic w32 API software which wasn’t friendly with the Internet, and my prototype was a microservice-oriented web app. Given the current pandemic, most of work is being routed to web interfaces, and some applications are being hammered down to the point of data corruption, timeouts, or worse, virtual lines. This happens because the web interface is doing something while the other software has to poll the data and wait until something completes, and there is always somebody who just exits the old program or just sleeps the computer.

Once I did it and proved my point that this what the perfect opportunity to rewrite the old application to something more microservice-esque and elastic— clients paying at the month’s end was a timeout circus — I doubted if using the Lumen framework (PHP) with a sole SQL database was enough to keep it performanant. It’s known that PHP speed is good but not the best compared to most close-to-metal languages. Today, the top languages are C++, Golang and Rust, but that alone doesn’t mean C++, Golang and Rust have the same frameworks and tools that PHP have honed through its 25 years of existence, at least not on the microservice and web area of the software world.

The initial prototype that aimed to replace part of the Contract and Client management.

I went to the Rust and Golang routes to see if using these languages could offer more performance out of the box, but before fully diving in, I used TechEmpower benchmarks to check what were the top performing frameworks. You know, nobody spends two weeks trying to code just a database connection unless it’s the project itself, you will mostly use a framework for anything you want to do to avoid writing everything from line zero.

I have to say these benchmarks are outstanding, at least for single query statements. Having Actix (Rust) handling almost 700.000 requests per second is marvelous.

Let’s check multiple queries, since most microservices will vary between getting one or two records, and sometimes an INSERT operation in between.

Wow, again Rust and Golang frameworks perform very well. Note that using Actix with ORM objects is just as the same as using raw PHP and raw connections, something you would do… two decades ago?

Let’s be honest. It’s very difficult to make 20 queries in a single request for a microservice. Unless there is a complex scenario, the one I did doesn’t go over 3 at best, including a single row insert when everything goes alright.

Seems like Rust and Go should become my prefered choices. Rust is a more close-to-the-metal language, while Golang is aimed to microservices. I put both under my radar and started to dig what I could find. What you need to accomplish that top performance?

The ORM way, or the performance way

After checking how the applications performed on the top of the charts, it was clear that these instances were directly connecting to the database. It’s not something you should worry about in the microservices area — if you’re using complex JOIN queries then your microservice may be not as “micro” as you want.

Noting that the “ORM” column was full of raw connections to a PostgreSQL instance.

The problem with going for raw SQL calls is that you become dependant on the database engine itself. There is no way to swap a DB engine, like going from SQLite to Microsoft SQL Server, in a transparent way. Most of ORM offer a way to add drivers for each RDBMS, so you can safely switch from one to another.

I consider the Database Layer something apart from the Application, so these should be interchangeable anytime and keep the same behaviour. It makes our lives easier, and also allows both of them testable and easy to migrate if for some reason we need to change them. For example, you can test the application against SQLite before pushing a new revision that uses a real PostgreSQL database.

This is the part where ORM comes, and where most of the performance gains of any language are lost. ORMs are basically a layer between the framework and the database itself. One of the most easy ways to pick up are those who offer the ActiveRecord pattern: each record is an object that can be created, updated or deleted. This is one of the reasons why I choose Lumen/Laravel, since it’s brain dead easy to understand and quick to build from. When I need performance I can always instance a query builder to push a SQL statement to the database.

Note that adding the Diesel ORM to the mix slows the request around 40%.

Since each record must persist in memory as an object, this adds overhead, and the application suddenly starts to run logic related to the ORM checks themselves rather than the data it contains: initializing the connection, checking persistence, running callbacks, synchronizing values, parsing custom values, destroying old references, etc.

Look it this way: the data is inside a box, so everytime you need to do something with the data, you have to deal with the whole box, which is a good trade-of instead of dealing with every piece of data and other technicalities manually (like handling null values correctly). The problem is that some people overuse the ORM capabilities when not needed. For example, it’s not necessary to hydrate an ORM object with data if you only need to update it, nor doing it 100 times with other data you don’t need like timestamps.

So, checking benchmarks again, it’s not difficult to see that ORM overall make the application slower, compared to just using direct connections and raw queries. Some more, some less, but every framework slows down when an ORM is used to deal with all database objects.

It’s not all over, yet.

After seeing how Rust and Golang offers massive performance improvements on some frameworks, I think it will be safe to jump to these pastures as long I find a big performance problem related to the language itself instead of the database layer. It’s not that an ORM makes the application slower, because it does in some degree, but rather prioritize the included SQL Query Builder these frameworks offer instead of the big (but handy) ORM objects if these aren’t needed.

So, it’s no surprise that Lumen is 9 times slower than Actix, which is a close-to-metal executable with raw PostgreSQL connections, when you use Eloquent ORM objects. Completing 3.500 requests per second at ~150 ms is not that terrible either if we can just skip the ORM objects and just push raw statements back and forth.

Additionally, or better said for the lack of thereof, these frameworks don’t include a lot of useful tools compared to Lumen/Laravel. For example, there is no input validation, event bus, queued jobs, CLI commands, authorization, and even encryption, among other things. Most of these are things you must implement yourself. Lumen/Laravel offers these out of the box without needing to look around and pray the maintainer keeps it updated, and there is a well maintained gRPC extension for microservice-communication. When you are creating something where money is involved, and probably iterating over it for months or years, this is one bit is kinda important.

So, in the end, it’s not that I’m forced to go for other language, or framework. I’m forced to start with creating these services as fast as I can with production-ready tools, diagnose them along the way, and then later see what needs to be added, changed or removed. One of these solutions can be use Rust (Actix, Rocket) or Golang (Gin, Fiber, Chi, Echo) for the sake of performance, but until then, PHP can work just fine.

I sincerely hope that PHP 8 with its JIT compiler closes the gap, and doesn’t require having a buffer of 128MB for saving compiled machine-code, something that would make it close to JAVA-level of hardware requirements that nobody likes — from my perspective, 16MB should be enough for a microservice, but you do you.

It’s not that each microservice is gonna be hammered with 3.000 requests per second, at least not before PHP 8 comes.

--

--

Italo Baeza Cabrera
The Startup

Graphic Designer graduate. Full Stack Web Developer. Retired Tech & Gaming Editor. https://italobc.com