polyglot php golang architecture

Leveraging a polyglot architecture with PHP and GoLang

by Daniel Elmalem
#PHP#GoLang#Architecture

This article will delve into the fundamentals of CPU operation within the realm of PHP and Golang applications, alongside multithreading. We'll explore a potential architecture to combine the strengths of both PHP and Golang to enhance productivity, costs, and performances.

The fundamental operations of a CPU

In a multi-core processor, each core follows a repetitive process of fetching and executing instructions from memory sequentially. The operating system provides the memory address of the instruction, which the core then retrieves, decodes, and executes.

While fetching an instruction, the core advances an internal pointer to locate the next instruction's memory address, known as the program counter.

$programCounter = 1;

while (true) {
    $instruction = fetch($programCounter);

    $programCounter++;

    $instruction->execute();
}

During execution, the core utilizes registers to store program states between instructions. For multiple reasons the operating system does not permit the core to keep executing instructions for a single program indefinitely. Through context switching the operating system intervenes to redirect core attention to other programs' instructions that must be executed.

$programCounter = 1;

while (true) {
    $instruction = fetch($programCounter);

    $programCounter++;

    $instruction->execute();

    if ($interrupt) {
        saveState();

        $programCounter = getNewAddress();
    }
}

Context switching enables multitasking when the number of programs running exceeds the number of CPU cores. However excessive switches will slow performances. For example if a user enters a character into an editor they expect the character to be printed on the screen immediately. This printing may be delayed if there is too much context switching, because the core is jumping between instructions from too many programs.

One solution to improve performance and run more programs in parallel is to increase the number of CPU cores. Also note that we will need to increase the machine memory capacity as each program instance occupies it's own isolated space. This approach will heavily increase server costs on the long run.

PHP processes on a single thread

The illustration above highlights the problem: three PHP processes operating within their individual memory spaces on a single execution thread, resulting in increased context-switching and memory usage.

Multi-threading

Multi-threading arises as a solution to mitigate the burden of managing numerous processes simultaneously.

Through multi-threading, a singular program can initiate multiple threads of execution that access the same memory space. This capability allows the program to execute numerous instructions in parallel across multiple CPU cores or concurrently by alternating on a single core while avoiding excessive memory consumption.

Additionally, the process of context switching between threads within the same program is more performant compared to switching between distinct programs. This is due to a smaller number of registers requiring updates, since a portion of the program's state is shared among the threads.

In the diagram below one process is running using three execution threads and a single memory space. This allows a reduced memory consumption and context switching.

One process multi-threaded

PHP and Context Management

Sadly PHP doesn't support multi-threading natively. Concurrent requests are handled by the creation of multiple processes thanks to PHP-FPM. While this may initially seems a disadvantage, it actually contributes to the appeal of writing PHP programs.

The absence of shared memory between concurrent requests eliminates concerns regarding race conditions, deadlocks, and atomicity planning. Each process operates within its own memory space, ensuring independence and preventing interference and value alterations by other threads. Analogous to distinct databases for each program instance, this isolation simplifies operations by averting competition over read/write over the same record in a table.

In contrast, multi-threaded programs demand meticulous attention when reading from and writing to memory. This can prolong development time and be more prone to errors and bugs.

PHP's rapid deployment capabilities enable swift web application launches, ideal for scenarios where heavy traffic isn't anticipated. Most applications will not experince traffic that will forces the creation of excessive number of processes. However, if it is the case then CPU cores and memory management might become a major business concern to avoid enormous bills from your cloud provider.

To reduce the number of PHP programs running, strategies like caching and database query optimization aids in mitigating resource consumption. Yet once I/O operations are optimized, to reduce server costs we must look into improving memory and CPU usage.

Golang to the rescue

GoLang was specifically designed with concurrency in mind, offering a distinct execution model compared to PHP. In GoLang, a program launches multiple threads, leveraging available CPU cores for parallel execution and enhance memory utilization by loading program code into memory just once. Additionaly, GoLang introduces smaller execution units called "Goroutines" that operate on top of threads. Each incoming request triggers a separate Goroutine, which is then scheduled to run on available threads. This innovative approach allows a single thread to handle instructions for multiple Goroutines concurrently.

When a Goroutine enters a waiting state the Go runtime efficiently reallocates resources efficiently in favor of an another one. This occurs particularly during I/O operations, where a Goroutine temporarily halts CPU instructions.

Goroutine threads

By adopting this approach, the GoLang runtime maximizes the utilization of CPU cores by prioritizing the execution of user-land instructions. This optimization minimizes context switching, allowing CPU cores to dedicate more time to handling incoming traffic. Compared to traditional threads or process-based context switching, the overhead of context switching between Goroutines is significantly reduced and fully managed by the Go runtime environment with minimal intervention from the operating system.

Goroutines queue runtime

Beyond the advantages of Goroutines, shared memory facilitates connection pooling. This enables the establishment of a set of connections to resources like databases and HTTP servers, which are kept alive and shared across multiple execution units. Thanks to it there is no need to repeatedly open new connections for each interaction with a network service. In addition and in contrary to PHP, when a Goroutine no longer requires a connection it is returned to the shared pool eliminating the need to remain idle while waiting for program termination.

Make PHP & Golang work together

In one hand, PHP enables us to move quickly with a vast talent pool of skilled developers, very structured online resources and tutorials and a mature ecosystem of excellent libraries and frameworks. On the other hand GoLang allows us to reduced memory, CPU usage and OS-level context switches.

Instead of choosing one or the other let's see how we can leverage the benefits of both languages. The main idea is to start fully with PHP and analyze our logs and metrics dashboards to pinpoint endpoints experiencing high traffic and excessive I/O operations. We can then migrate these endpoints to Go, alleviating the load on our PHP application servers. This approach optimizes CPU and memory resource utilization, allowing the remaining endpoints to serve users effectively. As a result, the Go program efficiently manages the designated endpoints while maintaining the advantages of PHP for other components.

A possible architecture is described below:

PHP and Golang architecture

Between our frontend clients and PHP/Go backends lies a load balancer, orchestrating SSL negotiations and directing requests to specific endpoints towards our Go backend, while directing all other traffic to the PHP backend.

Both the PHP and Go backends share access to the same array of auxiliary resources, such as database clusters, cache clusters, and task queues. A separate Go program operates in the background, managing queued and scheduled tasks to alleviate the PHP backend of background processing duties.

The number of concurrent connections to the database is regulated by factors including the quantity of PHP FPM workers, the connection pool size of the Go web server, and the pool size of the Go worker server. One potential approach involves utilizing an RDS proxy between the PHP servers and the RDS cluster. This will facilitates shared connections among multiple PHP workers and reduce the count of concurrent open connections.

Conclusion

Using a polyglot architecture allows us to leverage the strengths of both PHP and GoLang, providing a well-rounded solution to meet diverse needs. PHP excels in rapid development, while GoLang enhances performance and scalability through efficient resource utilization.

If you're not currently facing challenges with compute infrastructure costs, I would recommend sticking with PHP, especially when coupled with robust frameworks like Laravel or Symfony. This approach enables faster development to deploy features and onboard customers more rapidly. Once you run into infrastructure costs issues, transitioning certain microservices to GoLang will help reduce resource usage and optimize efficiency.