Nginx's built-in load balancing strategy - Weighted Round Robin

When setting up a reverse proxy in Nginx, you can specify the proportion of each backend server in the reverse proxy, which plays a role in load balancing. See an example of a reverse proxy:

upstream backend {  
        server a weight=5;  
        server b weight=3;  
        server c weight=1;  
    }

In the example, three background servers are set up, and the proportions are 5, 3, and 1 respectively. So how can we allocate it to the three servers in the background according to the proportion when a request is received? The simplest method I can think of is of course: if the current weight is greater than 0, send it to this server, and then reduce the weight by 1, but the result of the request distributed by this method is [a,a,a,a,a,b, b,b,c], although the target ratio is achieved, but for a period of time requests are sent to a, and the other is sent to b, which is obviously not a good way to deal with it, and it is quite suitable for each machine. Yu was busy for a while and idle for a while, and did not receive requests evenly.

So how does nginx do load balancing so that the frequency of requests received by each machine is more even. The answer is an algorithm called w eighted round robin (WRR). For the data interpretation and proof of the algorithm, there is time to discuss it separately. Let’s take a look at how nignx implements this algorithm first. 

The process of selecting a valid server from several servers in nginx is placed in a function called ngx_http_upstream_get_peer. Let’s take a look at the overall function first:

/* Select a backend server to process the request according to the weight of the backend server*/
static ngx_http_upstream_rr_peer_t *
ngx_http_upstream_get_peer(ngx_http_upstream_rr_peer_data_t *rrp)
{
    time_t                        now;
    uintptr_t                     m;
    ngx_int_t total;
    ngx_uint_t i, n;
    ngx_http_upstream_rr_peer_t  *peer, *best;

    now = ngx_time();

    best = NULL;
    total = 0;

    /* Traverse the list of backend servers */
    for (i = 0; i < rrp->peers->number; i++) {

        /* Calculate the position n of the current backend server in the bitmap */
        n = i / (8 * sizeof(uintptr_t));
        m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));

        /* The current backend server already has a record in the bitmap, it will not be selected again, that is, continue to check the next backend server*/
        if (rrp->tried[n] & m) {
            continue;
        }

        /* If the current backend server has no record in the bitmap, it may be selected, and then calculate its weight */
        peer = &rrp->peers->peer[i];

        /* Check the down flag of the current backend server, if it is 1, it means not participating in the policy selection, then continue to check the next backend server*/
        if (peer->down) {
            continue;
        }

        /*
         * The down flag of the current back-end server is 0, and then check whether the number of connection failures of the current back-end server has reached max_fails;
         * If the sleep time has not reached fail_timeout, the current backend server is not selected, and continue to check the next backend server;
         */
        if (peer->max_fails
            && peer->fails >= peer->max_fails
            && now - peer->checked <= peer->fail_timeout)
        {
            continue;
        }

        /* If the current backend server may be selected, calculate its weight */

        /*
         * During the above initialization process current_weight = 0, effective_weight = weight;
         * At this point, set the weight of the current backend server to the value of current_weight plus the original value plus effective_weight;
         * Set the total weight to the original value plus effective_weight;
         */
        peer->current_weight += peer->effective_weight;
        total += peer->effective_weight;

        /* The server is normal, adjust the value of effective_weight*/
        if (peer->effective_weight < peer->weight) {
            peer->effective_weight++;
        }

        /* If the weight current_weight of the current backend server is greater than the weight of the current best server, the current backend server is selected*/
        if (best == NULL || peer->current_weight > best->current_weight) {
            best = peer;
        }
    }

    if (best == NULL) {
        return NULL;
    }

    /* Calculate the position i of the selected backend server in the server list */
    i = best - &rrp->peers->peer[0];

    /* Record the value of the selected backend server in the current member of the ngx_http_upstream_rr_peer_data_t structure, which will be used when releasing the backend server*/
    rrp->current = i;

    /* Calculate the position of the selected backend server in the bitmap */
    n = i / (8 * sizeof(uintptr_t));
    m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));

    /* Record the selected backend server in the corresponding position of the bitmap */
    rrp->tried[n] |= m;

    /* Update the weight of the selected backend server */
    best->current_weight -= total;

    if (now - best->checked > best->fail_timeout) {
        best->checked = now;
    }

    /* Return the selected backend server */
    return best;
}

In the function, nginx actually does many other checks and other actions, such as checking whether the current server has been selected, whether it is valid, and so on. If you just focus on the selection algorithm, you can eliminate the other processes, leaving only:

/* Select a backend server to process the request according to the weight of the backend server*/
static ngx_http_upstream_rr_peer_t *
ngx_http_upstream_get_peer(ngx_http_upstream_rr_peer_data_t *rrp)
{
    ....
    best = NULL;
    total = 0;

    /* Traverse the list of backend servers */
    for (i = 0; i < rrp->peers->number; i++) {

        ........

        /* If the current backend server may be selected, calculate its weight */

        /*
         * During the above initialization process current_weight = 0, effective_weight = weight;
         * At this point, set the weight of the current backend server to the value of current_weight plus the original value plus effective_weight;
         * Set the total weight to the original value plus effective_weight;
         */
        peer->current_weight += peer->effective_weight;
        total += peer->effective_weight;

        /* The server is normal, adjust the value of effective_weight*/
        if (peer->effective_weight < peer->weight) {
            peer->effective_weight++;
        }

        /* If the weight current_weight of the current backend server is greater than the weight of the current best server, the current backend server is selected*/
        if (best == NULL || peer->current_weight > best->current_weight) {
            best = peer;
        }
    }

    if (best == NULL) {
        return NULL;
    }

    ........

    /* Update the weight of the selected backend server */
    best->current_weight -= total;

    if (now - best->checked > best->fail_timeout) {
        best->checked = now;
    }

    /* Return the selected backend server */
    return best;
}

Each backend peer has three weight variables

(1) weight

The weight of the backend specified in the configuration file, this value is fixed.

(2) effective_weight

The effective weight of the backend, the initial value is weight.

Why come out with an effective_weight instead of using weight directly? This is because if an error occurs on the current server, the effective_weight can be reduced to reduce the probability of this machine being selected. Of course, if the servers are normal,  effective_weight will always be equal to weight.

If the server with an error returns to normal later, the effective_weight will be gradually increased during the selection process, and finally return to the weight.

(3) current_weight

The current weight of the backend, which is 0 at the beginning, will be adjusted each time it is selected. For each backend, increase its current_weight to its effective_weight,

At the same time, the effective_weight of all backends is accumulated and saved as total.

If the current_weight of the backend is the largest, the backend is selected and its current_weight is subtracted from total.

If the backend is not selected, current_weight does not need to be decreased.

 

After clarifying the meaning of the three weight fields, the weighted round-robin algorithm can be described as:

1. For each request, iterate over all available backends in the cluster, and for each backend peer execute:

    peer->current_weight += peer->effecitve_weight。

    At the same time, the effective_weight of all peers is accumulated and saved as total.

2. Select the peer with the largest current_weight from the cluster as the selected backend.

3. For the selected backend, execute: peer->current_weight -= total.

You can directly understand the source code above. 

 

Take the initial configuration as an example

upstream backend {

    server a weight=5;

    server b weight=3;

    server c weight=1;

}

Initially  current_weight = { 0, 0, 0 }, effective_weight =  { 5, 3, 1 }. Assuming that the server does not fail halfway, total is always the sum of all effective_weights, which is 9.

serial number current_weight before selection After adding effective_weight choose after subtracting total
1  { 0, 0, 0 }  { 5, 3, 1 } a  { -4, 3, 1 }
2 { -4, 3, 1 } {1, 6, 2 } b { 1, -3, 2 }
3 { 1, -3, 2 } {6, 0, 3 } a { -3, 0, 3 }
4 { -3, 0, 3 } { 2, 3, 4 } c { 2, 3, -5 }
5 { 2, 3, -5 } { 7, 6, -4 } a { -2, 6, -4 }
6 { -2, 6, -4 } { 3, 9, -3 } b { 3, 0, -3 }
7 { 3, 0, -3 } { 8, 3, -2 } a { -1, 3, -2 }
8 { -1, 3, -2 } { 4, 6, -1 } b { 4, -3, -1 }
9 { 4, -3, -1 } { 9, 0, 0 } a  { 0, 0, 0 }
It can be seen that 9 requests are a round, and the complete sequence is [a, b, a, c, a, b, a, b, a], which is relatively a very average process, which can verify the effectiveness of the algorithm.


Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325720977&siteId=291194637