Sticky Load Balancing with Varnish

Niklas Norberg niklas.norberg at bahnhof.se
Wed Apr 14 14:28:11 CEST 2010


Hi,

last week I started writing a sticky load balancer.

At that time (2.0.6) I lacked that req.* wasn't available in vcl_deliver
so I had to use some global C-variables to share data from vcl_recv to
vcl_deliver. Because of this I had to add some thread guard C-code and
all together it worked but I wasn't fully satisfied. The guard code
contained sleep in order to wait for other thread so that just one
thread at a time would go through this "set/get" cycle.

This week I discovered that 2.1.0 was released and that:
- req.* is now available in vcl_deliver.

So I rewrote it, just by removing the thread C-code, and here it is.
I've tested it with JMeter and it balances correct (i.e. according to
the defined weights).

So I also vote for keeping this as a documented configuration rather
than a built-in feature. Unless the planned sticky load balancing will
have something above the rudimentary.

Comments?


With kind regards,

Niklas Norberg


LBsubs.vcl (which I also atach for row break sanity):



C{
    // Obs This string is also used hard coded in pure VCL code
    static const char VARNISH_LB_COOKIE_NAME[] = "VARNISH_LB=";
    // Let STICKY balance everything(/) for four hours a time:
    static const char VARNISH_LB_ENDING[] =
                "; path=/; Max-Age=14400; Comment=Varnish Sticky Load
Balancing Cookie";
    static const int CANDIDATE_CNT = 3;
    static const int MAX_LEN_LB_ID = 2; // covers 1-99
    
    // Load balancing weights:
    // The first value should be the sum of the others.
    static const int LB_FACTOR[4] = {10, 7, 1, 2};
    // The first is not used, just to keep indexes same in the arrays.
    static int lbStatus[4] = {-1, 0, 0, 0};
    
    static int lastCandidate = 1;
    
    /**
        Load Balancing according to Request Counting Algorithm, see:

http://httpd.apache.org/docs/2.2/mod/mod_proxy_balancer.html#requests
        or write your own.
    */
    int getCandidate4LB() {
        /*
        for each worker in workers
            worker lbstatus += worker lbfactor
            total factor    += worker lbfactor
            if worker lbstatus > candidate lbstatus
                candidate = worker
        candidate lbstatus -= total factor
        */
        int worker=1;
        // This local copy is really overkill
        int candidate = lastCandidate;
        while ( worker<=CANDIDATE_CNT ) {
            lbStatus[worker] += LB_FACTOR[worker];
            if ( lbStatus[worker] > lbStatus[candidate] )
                candidate = worker;
            worker++;
        }
        //int totalFactor = LB_FACTOR[0];
        //lbStatus[candidate] -= totalFactor;
        lbStatus[candidate] -= LB_FACTOR[0];
        lastCandidate = candidate;
        return candidate;
    }

    
}C

/**
    Check if a LB cookie is present.

    If it is
        - Copy the LB cookie value to a marker.
        - set backend
        - unset marker

    If it isn't
        - find candidate for LB and set value to marker
        - set backend
        - don't unset marker so a cookie can be set in vcl_deliver.
*/
sub recv_loadBalancingOnStickyCookie {
    if (req.http.Cookie ~ "VARNISH_LB=") {
        set req.http.StickyVarnish =
            regsub( req.http.Cookie, "^.*?VARNISH_LB=([^;]*);*.*$",
"\1" );
        call chooseBackend;
        unset req.http.StickyVarnish;
    } else {
        call findCandidate4LB;
        call chooseBackend;
    }
}

/**
    Get candidate from some fancy smanchy algorithm.
    Set value to marker.
*/
sub findCandidate4LB {
    // set req.http.StickyVarnish = "X";:
    C{
    // get index for backend
    int idxBackend = getCandidate4LB();
    
    // Store index in a header marker so we can use this later
    char strBackendIndex[MAX_LEN_LB_ID + 1];
    sprintf(strBackendIndex, "%d", idxBackend);
    VRT_SetHdr(sp, HDR_REQ, "\016StickyVarnish:", strBackendIndex,
vrt_magic_string_end);
    }C
//     C{
//     //Debug test:
//     int i=1;
//     while(i<=100) {
//         syslog( LOG_INFO, "getCandidate4LB(): %d",
getCandidate4LB() );
//         i++;
//     }
//     }C
}

/**
    Choose backend (via director) from the marker ("StickyVarnish").
    Change this as necessary.
    Directors and Backends are defined elsewhere.
*/
sub chooseBackend {
    // if else in weight order, switch statements would have been better
    if (req.http.StickyVarnish == "1") {
        set req.backend = Backends1;
    } else
    if (req.http.StickyVarnish == "3") {
        set req.backend = Backends3;
    } else
    if (req.http.StickyVarnish == "2") {
        set req.backend = Backends2;
    }
}


/**
    Set the Sticky Varnish Load Balancing Cookie as:
    "VARNISH_LB=X; ..."
*/
sub deliver_setLBCookie {
    if (req.http.StickyVarnish) {
        C{
        
        // Also send cookie from backend if any
        char* existing_set_cookie = VRT_GetHdr(sp, HDR_OBJ,
"\013Set-Cookie:");
        
        int len;
        if (existing_set_cookie == NULL)
            len = 0;
        else
            len = strlen(existing_set_cookie) + 1; // + 1 for "\r"
        len += strlen(VARNISH_LB_COOKIE_NAME);
        len += MAX_LEN_LB_ID;
        len += strlen(VARNISH_LB_ENDING);
        
        char set_cookie[len + 1];
        set_cookie[0] = '\0';
        
        if (existing_set_cookie != 0) {
            strcat( set_cookie, existing_set_cookie );
            strcat( set_cookie, "\r" );
        }
        strcat( set_cookie, VARNISH_LB_COOKIE_NAME );
        char* strBackend = VRT_GetHdr(sp, HDR_REQ,
"\016StickyVarnish:");
        strcat( set_cookie, strBackend );
        strcat( set_cookie, VARNISH_LB_ENDING );
        
        // Send cookie(s)
        VRT_SetHdr(sp, HDR_RESP, "\013Set-Cookie:", set_cookie,
vrt_magic_string_end);
        }C
    }
}


/**
    Call the LB subs from the subs:
    - vcl_recv
    - vcl_deliver
*/
/*
sub vcl_recv {
    call recv_loadBalancingOnStickyCookie;
}
sub vcl_deliver {
    call deliver_setLBCookie;
}
*/

/**
    If directors are defined as below we can in practice:
    Load balance to a specific backend by choosing a director
    and still have a fallback function.
    Risk for wrong backend = (1+1)/4294967295 = 5 * 10^-10,
    a price I'm willing to take for this fallback function.
    This way I can restart the backends and still keep the
    load balancing intact when all is up again.
*/
/*
director Backends1 random {
    { .backend = www1; .weight = 4294967295;}
    { .backend = www2; .weight = 1;}
    { .backend = www3; .weight = 1;}
}

director Backends2 random {
    { .backend = www1; .weight = 1;}
    { .backend = www2; .weight = 4294967295;}
    { .backend = www3; .weight = 1;}
}

director Backends3 random {
    { .backend = www1; .weight = 1;}
    { .backend = www2; .weight = 1;}
    { .backend = www3; .weight = 4294967295;}
}
*/




-------------- next part --------------

C{
    // Obs This string is also used hard coded in pure VCL code
    static const char VARNISH_LB_COOKIE_NAME[] = "VARNISH_LB=";
    // Let STICKY balance everything(/) for four hours a time:
    static const char VARNISH_LB_ENDING[] =
                "; path=/; Max-Age=14400; Comment=Varnish Sticky Load Balancing Cookie";
    static const int CANDIDATE_CNT = 3;
    static const int MAX_LEN_LB_ID = 2; // covers 1-99
    
    // Load balancing weights:
    // The first value should be the sum of the others.
    static const int LB_FACTOR[4] = {10, 7, 1, 2};
    // The first is not used, just to keep indexes same in the arrays.
    static int lbStatus[4] = {-1, 0, 0, 0};
    
    static int lastCandidate = 1;
    
    /**
        Load Balancing according to Request Counting Algorithm, see:
        http://httpd.apache.org/docs/2.2/mod/mod_proxy_balancer.html#requests
        or write your own.
    */
    int getCandidate4LB() {
        /*
        for each worker in workers
            worker lbstatus += worker lbfactor
            total factor    += worker lbfactor
            if worker lbstatus > candidate lbstatus
                candidate = worker
        candidate lbstatus -= total factor
        */
        int worker=1;
        // This local copy is really overkill
        int candidate = lastCandidate;
        while ( worker<=CANDIDATE_CNT ) {
            lbStatus[worker] += LB_FACTOR[worker];
            if ( lbStatus[worker] > lbStatus[candidate] )
                candidate = worker;
            worker++;
        }
        //int totalFactor = LB_FACTOR[0];
        //lbStatus[candidate] -= totalFactor;
        lbStatus[candidate] -= LB_FACTOR[0];
        lastCandidate = candidate;
        return candidate;
    }

    
}C

/**
    Check if a LB cookie is present.

    If it is
        - Copy the LB cookie value to a marker.
        - set backend
        - unset marker

    If it isn't
        - find candidate for LB and set value to marker
        - set backend
        - don't unset marker so a cookie can be set in vcl_deliver.
*/
sub recv_loadBalancingOnStickyCookie {
    if (req.http.Cookie ~ "VARNISH_LB=") {
        set req.http.StickyVarnish =
            regsub( req.http.Cookie, "^.*?VARNISH_LB=([^;]*);*.*$", "\1" );
        call chooseBackend;
        unset req.http.StickyVarnish;
    } else {
        call findCandidate4LB;
        call chooseBackend;
    }
}

/**
    Get candidate from some fancy smanchy algorithm.
    Set value to marker.
*/
sub findCandidate4LB {
    // set req.http.StickyVarnish = "X";:
    C{
    // get index for backend
    int idxBackend = getCandidate4LB();
    
    // Store index in a header marker so we can use this later
    char strBackendIndex[MAX_LEN_LB_ID + 1];
    sprintf(strBackendIndex, "%d", idxBackend);
    VRT_SetHdr(sp, HDR_REQ, "\016StickyVarnish:", strBackendIndex, vrt_magic_string_end);
    }C
//     C{
//     //Debug test:
//     int i=1;
//     while(i<=100) {
//         syslog( LOG_INFO, "getCandidate4LB(): %d", getCandidate4LB() );
//         i++;
//     }
//     }C
}

/**
    Choose backend (via director) from the marker ("StickyVarnish").
    Change this as necessary.
    Directors and Backends are defined elsewhere.
*/
sub chooseBackend {
    // if else in weight order, switch statements would have been better
    if (req.http.StickyVarnish == "1") {
        set req.backend = Backends1;
    } else
    if (req.http.StickyVarnish == "3") {
        set req.backend = Backends3;
    } else
    if (req.http.StickyVarnish == "2") {
        set req.backend = Backends2;
    }
}


/**
    Set the Sticky Varnish Load Balancing Cookie as:
    "VARNISH_LB=X; ..."
*/
sub deliver_setLBCookie {
    if (req.http.StickyVarnish) {
        C{
        
        // Also send cookie from backend if any
        char* existing_set_cookie = VRT_GetHdr(sp, HDR_OBJ, "\013Set-Cookie:");
        
        int len;
        if (existing_set_cookie == NULL)
            len = 0;
        else
            len = strlen(existing_set_cookie) + 1; // + 1 for "\r"
        len += strlen(VARNISH_LB_COOKIE_NAME);
        len += MAX_LEN_LB_ID;
        len += strlen(VARNISH_LB_ENDING);
        
        char set_cookie[len + 1];
        set_cookie[0] = '\0';
        
        if (existing_set_cookie != 0) {
            strcat( set_cookie, existing_set_cookie );
            strcat( set_cookie, "\r" );
        }
        strcat( set_cookie, VARNISH_LB_COOKIE_NAME );
        char* strBackend = VRT_GetHdr(sp, HDR_REQ, "\016StickyVarnish:");
        strcat( set_cookie, strBackend );
        strcat( set_cookie, VARNISH_LB_ENDING );
        
        // Send cookie(s)
        VRT_SetHdr(sp, HDR_RESP, "\013Set-Cookie:", set_cookie, vrt_magic_string_end);
        }C
    }
}


/**
    Call the LB subs from the subs:
    - vcl_recv
    - vcl_deliver
*/
/*
sub vcl_recv {
    call recv_loadBalancingOnStickyCookie;
}
sub vcl_deliver {
    call deliver_setLBCookie;
}
*/

/**
    If directors are defined as below we can in practice:
    Load balance to a specific backend by choosing a director
    and still have a fallback function.
    Risk for wrong backend = (1+1)/4294967295 = 5 * 10^-10,
    a price I'm willing to take for this fallback function.
    This way I can restart the backends and still keep the
    load balancing intact when all is up again.
*/
/*
director Backends1 random {
    { .backend = www1; .weight = 4294967295;}
    { .backend = www2; .weight = 1;}
    { .backend = www3; .weight = 1;}
}

director Backends2 random {
    { .backend = www1; .weight = 1;}
    { .backend = www2; .weight = 4294967295;}
    { .backend = www3; .weight = 1;}
}

director Backends3 random {
    { .backend = www1; .weight = 1;}
    { .backend = www2; .weight = 1;}
    { .backend = www3; .weight = 4294967295;}
}
*/


More information about the varnish-misc mailing list