Recently, I was lucky enough to have the chance to speak at my local PHP meetup and then later at DundeePHP. When given the chance to do a talk, I like to use it as an excuse to learn something new. Nothing focuses the mind like a deadline. A few ideas came and went but the idea that stuck was serverless, specifically FaaS (Functions as a Service). The talk has been and gone and I wanted to give the content life beyond the evening itself. I’ve written this article, summarising my thoughts and findings, to share a few of the key learnings with you and to hopefully help navigate the world of serverless PHP.
I remember when AWS Lambda was first released in 2014. I sent an email around the office as I was super excited about the possibilities it opened up. It seemed like a fresh way to write and run code. However, in the nearly 4 years since AWS Lambda was released, I never quite got around to trying it out.
The latest Digital Ocean quarterly report shows that “nearly half of developers fail to clearly understand what it [serverless] is.”. It also shows that serverless is still quite a bit behind in developer mindshare compared to the adoption and interest of containers. As with every new technology, there are some hyperbolic statements around serverless however, I am broadly excited about the benefits it provides and new possibilities it opens up compared to both traditional servers and even containers.
This article is mainly about using PHP in a serverless environment and I won’t delve into the details, pro/cons behind serverless. If you have not tried, or read much on, serverless I recommend you read this article by Mike Roberts. It does a much better job than I could hope to do of explaining serverless.
If you look at the big three cloud providers, Amazon, Google and Microsoft, they each provide their own FaaS solution. Lambda, Cloud Functions and Azure Functions respectively. If you take a look at the documentation, none provide support for PHP*. This is quite a disappointment if you are a PHP developer as the only official way you can run your code is to use another language. Obviously this is not possible in many situations, such as if you have a substantial amount of code already written in PHP, if you are in a team where the majority of team members are comfortable using PHP or simply, you quite like PHP.
I think one reason that we have yet to see PHP become available from these providers is due to PHPs unique execution model. PHP’s share nothing architecture is quite different when compared to other languages that are supported. Hopefully, we shall see at least one of the big three fully support PHP in the near future. Until then, we will need to think outside of the box in order to get our PHP code running in this new serverless world.
I will look at three different ways that we can run PHP on a serverless platform. I unfortunately did not have an endless pot of time so I chose to focus on AWS Lambda.
PHP / Node.js
The first solution comes from Amazon themselves. We leverage the fact that when using Lambda, your code is in fact running on Linux and you have the ability to upload other files along with your code. We use this to upload a PHP binary which we have compiled using the Amazon Linux Docker image, our PHP code and some bootstrap code written in a language supported by Lambda, such as Node.js. We use Node.js to handle the initial Lambda invocation but then quickly spawn a PHP process to handle everything else. The solution looks something like this:
This approach has the advantage of being quite simple; we are simply spawning a new process. However, we are paying a performance penalty because every Lambda invocation requires a new PHP process to be started, which adds some overhead.
I wanted to stretch my legs a little bit and look at some other solutions. This led to having a look at some interesting Go and .NET libraries.
PHP / Go
My next attempt at getting PHP to run on Lambda was using Go. I can’t take full credit for this idea, it came from a Reddit comment.
Unlike PHP, Go is a compiled language. Once you have written your Go program, it is compiled into a statically linked binary without external dependencies. So, how do we use Go to get PHP running within Lambda? Well, we use a very nifty Go package called go-php. It allows us to embed PHP within our compiled Go binary and easily call PHP from Go. Our Lambda execution then looks something like this:
Like the previous example, a fully supported language is used to handle the initial Lambda invocation. However, instead of spawning a new process we simply start the PHP interpreter that has been embedded in our Go binary.
This approach has the benefit that we are not paying the price of a new process being spawned. Instead, when Lambda is invoked we start up the PHP interpreter and execute our PHP script. When Lambda is invoked again. soon after its first invocation. it is considered “warm” and our Go program with our running PHP interpreter is still in memory so it can quickly execute our script again.
This method of embedding PHP within a Go binary is a little tricky and the go-php library documentation is perhaps not as helpful as it could be for this use case. I had to open an issue on Github in order to get it to work. That being said, the package maintainer was extremely helpful.
If I am going to be totally honest, to get this fully working took many, many hours, which I put down to my lack of Go knowledge. This was the first Go program I had ever written. Nothing like jumping in at the deep end.
In the depths of struggling to get PHP to compile successfully I had a quick look to see if I could find another way to get PHP running. That’s when I found PeachPie.
PeachPie is a .NET project which “allows PHP to be executed within the .NET framework”. It does this by using a custom compiler which compiles PHP code into .NET CIL. There is a great visualisation of this on the PeachPie homepage. By using PeachPie we are no longer uploading a PHP file to Lambda as it has instead all been converted into .NET.
In a very simple diagram, this process makes our Lambda function look like:
I was a bit sceptical of PeachPie however, after using the project I am impressed. I thought I would have difficulties getting my PHP code to compile but it worked first time. Even when I made a mistake, the error messages were very helpful. PeachPie can even run Wordpress so most, if not all, of PHPs features are supported.
After all this exploration, I now had three different ways to run PHP on Lambda and I was interested to see the performance differential between the solutions. Rather than just benchmarking “Hello World” I tried to use a benchmark that was at least somewhat similar to the real world.
The benchmark I wrote did the following:
- “Loaded” 5000 users from an array
- Calculated the distance from Edinburgh, to each user
- Sorted users based on distance
- Returned the closest user
I also implemented the benchmark in both Node and Python so we have a baseline to compare our PHP code to.
We are not loading our users from a database as I wanted to maximise the time we were in code rather than waiting for I/O. Instead, we simply have an array with 5000 elements each containing a user record.
In retrospect, it would have been perhaps better to just do a “Hello World” benchmark as it would have been easier and fairer when comparing our PHP code to other languages.
The timings below include the HTTP request/response times including going through the AWS API Gateway. This was done so it was a somewhat realistic situation. The benchmark was performed from an EC2 instance in the same region and I used ApacheBench to perform the actual benchmarks.
Above are the results from the benchmarks across the different languages and different Lambda sizes. The size of the Lambda instance drastically affects the performance. This is because the more memory allocated to the Lambda instance, the more CPU power is allocated as well.
The Node.js performance here is quite impressive! Neither Python or any of our PHP implementations really get close. This is especially true at the smaller Lambda sizes where the different is quite large.
Our PeachPie implementation is the slowest of all. This is somewhat surprising as on the PeachPie site they have a suite of benchmarks where the performance looks strong. The performance shown in these benchmarks did not translate well into my use case however the project is only at version 0.9 and they have lot’s of room for performance improvements.
Our PHP/Node and PHP/Go implementations are actually quite close to each other but we can see the performance difference caused by us having to spawn a new PHP process in the PHP/Node implementation. As the Lambda sizes get larger, the difference between the two do get smaller.
I ran some additional benchmarks at different Lambda sizes to get an idea of the performance trends.
What’s interesting about this graph is that we don’t see any performance boosts past 1536MB. That’s because above 1536MB, instead of getting raw single core CPU performance, we get access to a second CPU core. None of our implementations are multithreaded so we don’t see any performance increase.
No matter which language you are writing your Lambdas in, I recommend doing some benchmarks of your code to see what the sweet spot for your particular use case is.
After all this investigation, if I was going to run PHP on Lambda I would use the Node/PHP implementation. The performance is reasonable and we are relying only on features already present in Node.js and PHP.
Our Go/PHP implementation is faster, however it is much tricker to setup. In my opinion, the performance benefit is not worth the extra setup cost. If performance is your main objective then I recommend using a fully supported language such as Node.js. One useful feature of FaaS is that you can use PHP for the majority of your functions but use something like Node.js for the functions that are performance sensitive.
PeachPie, while interesting, does not quite have the performance of the others. If you have some existing .NET and PHP code that you wish to interoperate between, it’s worth a look. Perhaps something to keep an eye on for the future.
*This is a bit of a lie, Azure do say they support PHP however it is an experimental language, stating: “The experimental languages in 1.x don’t scale well and don’t support all bindings.” and “don’t use experimental languages for anything that you rely on, as there is no official support for them”. I did try to give it a test but could not get it to trigger using a HTTP request. https://docs.microsoft.com/en-us/azure/azure-functions/supported-languages
Slides from the original talk are available here: https://docs.google.com/presentation/d/12_EBxzVRlijT4SmYWiR7xKTGMVz2pdr9f0Ead3BHLtE/edit?usp=sharing