tech · Clock4 min · 29 Jan 2021

How to Make Proxy Balancing in Guzzle

Aleksandr Denisyuk

Aleksandr Denisyuk

PHP Developer

how to make proxy balancing
Illustration by Amir Kerr

Setting proxy in Guzzle allows hiding a real IP server, modifying request-response on the proxy side, and statistics gathering by the proxy. When your application requires a lot of proxies, the question of load allocation among them arises. Proxy balancing implies the load distribution among nodes according to the strategy, for example, Round-Robin or Random. To balance a proxy in Guzzle, we will use the orangesoft/throttler library that is easy-to-set-up and includes the following strategies:

  • Random
  • Weighted Random
  • Frequency Random
  • Round-Robin
  • Weighted Round-Robin
  • Smooth Weighted Round-Robin

Install Packages

To install the necessary packages to demonstrate proxy balancing in Guzzle, let’s use Composer package manager:

composer require \
    && orangesoft/throttler \
    && predis/predis \
    && guzzlehttp/guzzle

The package orangesoft/throttler is necessary for IP-addresses balancing, predis/predis — for saving balancing strategies between the callings of an app, guzzlehttp/guzzle — for HTTP Internet resources requests. 

Configure a Load Balancer

Now, let’s configure a load balancer for three proxies that will, later on, be sent to the Guzzle itself. A proxy is a node, which the load balancer will turn to, depending on the strategy. In the snippet below, we are using the WeightedRoundRobin-strategy and an InMemory-counter that accumulates in memory the state of a strategy and is dumped after PHP is completed. 

<?php

use Orangesoft\Throttler\Collection\Node;
use Orangesoft\Throttler\Collection\Collection;
use Orangesoft\Throttler\Strategy\WeightedRoundRobinStrategy;
use Orangesoft\Throttler\Strategy\InMemoryCounter;
use Orangesoft\Throttler\Throttler;
use Orangesoft\Throttler\ThrottlerInterface;

$collection = new Collection([
    new Node('user:pass@192.168.0.1', 5),
    new Node('user:pass@192.168.0.2', 1),
    new Node('user:pass@192.168.0.3', 1),
]);

$strategy = new WeightedRoundRobinStrategy(
    new InMemoryCounter()
);

$throttler = new Throttler($collection, $strategy);

Since we are using the WeightedRoundRobin-strategy, we specified weights for every node: for example, we set the weight of 5 for the 192.168.0.1 proxy, which means that the balancer will turn to this node five times as much as to the others. Now we only need to call the next() method that will return the next node, which you can send the request to, every time.

while (true) {
    /** @var Node $node */
    $node = $throttler->next();

    // ...
}

Visualization of the load balancer work for the WeightedRoundRobin-strategy can be as follows:

+-----------------------+
| user:pass@192.168.0.1 |
| user:pass@192.168.0.1 |
| user:pass@192.168.0.1 |
| user:pass@192.168.0.1 |
| user:pass@192.168.0.1 |
| user:pass@192.168.0.2 |
| user:pass@192.168.0.3 |
| etc.                  |
+-----------------------+

Write Middleware for Guzzle

Now it’s time to connect the above-configured proxy balancer to Guzzle. For this, we will code middleware for Guzzle that will add the proxy to every HTTP-request according to the chosen strategy:

<?php

use Orangesoft\Throttler\ThrottlerInterface;
use Psr\Http\Message\RequestInterface;

class ProxyMiddleware
{
    private $throttler;

    public function __construct(ThrottlerInterface $throttler)
    {
        $this->throttler = $throttler;
    }

    public function __invoke(callable $handler): \Closure
    {
        return function (RequestInterface $request, array $options) use ($handler) {
            $node = $this->throttler->next();

            $options['proxy'] = $node->getName();

            return $handler($request, $options);
        };
    }
}

ProxyMiddleware is only an intermediary between the load balancer and Guzzle.

Test Requests

Then, we should connect ProxyMiddleware to Guzzle for the proxy balancing to work:

<?php

use GuzzleHttp\HandlerStack;
use GuzzleHttp\Client;
use Psr\Http\Message\ResponseInterface;

/** @var Orangesoft\Throttler\ThrottlerInterface $throttler */

$stack = HandlerStack::create();

$stack->push(new ProxyMiddleware($throttler));

$client = new Client(['handler' => $stack]);

That’s all we should do to configure the balancer and connect it to Guzzle. Then, we can use Guzzle as always:

while (true) {
    /** @var ResponseInterface $response */
    $response = $client->get('https://httpbin.org/ip');

    // ...
}

The result of the proxy balancer work will be as follows:

+-------------+
| 192.168.0.1 |
| 192.168.0.1 |
| 192.168.0.1 |
| 192.168.0.1 |
| 192.168.0.1 |
| 192.168.0.2 |
| 192.168.0.3 |
| etc.        |
+-------------+

Keep Counter

In the orangesoft/throttler package, there is InMemoryCounter that stores the strategies states in memory and is dumped after PHP completion. It means that the sequence of selection of the nodes starts from the very beginning. You may need to save the sequence of the nodes balancing in the request-response model, queues, and microservices. For this, you need to implement the Counter interface and add it to RoundRobin- or WeightedRoundRobin-strategy. Below is an example of how to implement the counter with the help of the predis/predis library:

<?php

use Predis\Client;
use Orangesoft\Throttler\Strategy\Counter;
use Orangesoft\Throttler\Strategy\WeightedRoundRobinStrategy;

class RedisCounter implements Counter
{
    private $client;

    public function __construct(Client $client)
    {
        $this->client = $client;
    }

    public function increment(): int
    {
        $key = 'throttler:weighted-round-robin:counter';

        if (!$this->client->exists($key)) {
            $this->client->set($key, -1);
        }

        return $this->client->incr($key);
    }
}

All Random-strategies and SmoothWeightedRoundRobinStrategy don’t support the counters. There is no need to save the sequence for the Random-strategies, while marshaling is necessary for the SmoothWeightedRoundRobin-strategy to remember the last node. Using RedisCounter is easy:

/** @var Predis\Client $client */

$strategy = new WeightedRoundRobinStrategy(
    new RedisCounter($client)
);

After this, configure the Throttler class with a new strategy.

Final Snippet

Below is a snippet that you can use as a crib to implement proxy balancing in Guzzle:

<?php

use Orangesoft\Throttler\Collection\Node;
use Orangesoft\Throttler\Collection\Collection;
use Orangesoft\Throttler\Strategy\WeightedRoundRobinStrategy;
use Orangesoft\Throttler\Throttler;
use Orangesoft\Throttler\ThrottlerInterface;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Client;

/** @var RedisCounter $redisCounter */
/** @var ProxyMiddleware $proxyMiddleware */

$nodes = [
    new Node('user:pass@192.168.0.1', 5),
    new Node('user:pass@192.168.0.2', 1),
    new Node('user:pass@192.168.0.3', 1),
];

$throttler = new Throttler(
    new Collection($nodes),
    new WeightedRoundRobinStrategy($redisCounter)
);

$stack = HandlerStack::create();

$stack->push($proxyMiddleware);

$client = new Client(['handler' => $stack]);

After this, Guzzle will send all the requests with balanced proxies.

Rate this article!

(6 ratings, average: 5 out of 5)

red-title-line
Insights
Latest Articles