RealityServer Web Services API Programmer's Manual

UAC HAProxy Sample

HAProxy Integration

RealityServer ships with a sample UAC event plugin that integrates with the popular HAProxy load balancer. This plugin will communicate with an HAProxy statistics socket and raise and lower the weight of a given backend according to how many connected users there are. This allows for new users to be forwarded to the RealityServer instance with the lowest load. The plugin comes with complete source code and provides a full example of a UAC event plugin. The source can be found in the src/uac_haproxy directory.

This document will describe a sample load balanced architecture, show how to configure HAProxy to load balance between the servers and how to configure User Access Control on RealityServer and the UAC HAProxy plugin to control server weights.

Network Architecture

In this example we have 3 RealityServer instances running on 3 separate servers. These can be accessed on the local network by the following hostnames:

    rshost_1
    rshost_2
    rshost_3
    

The HOSTNAME environment variable on each machine is also set to the appropriate hostname above. RealityServer is running on each server on port 8080. Each will be limited to 4 simultaneous users.

We have one instance of HAProxy running on the following server:

    haproxy_1
    

The HAProxy host is exposed to the internet via port 80. The RealityServer hosts are only accessible on the internal network. All user access to RealityServer is mediated through HAProxy.

HAProxy Configuration

Note: The UAC HAProxy plugin requires HAProxy 1.5 as 1.4 only supports Unix Domain Sockets for the statistics socket. 1.5 supports Internet Sockets and so can be connected to remotely. It is possible to use 1.4 and expose the Unix Domain Socket to an external port using tools such as socat however configuration of that is beyond the scope of this document.

HAProxy is configured as follows:

    global
        stats socket *:10000 level admin

    defaults
        mode        http
        option      http-server-close

    frontend all 0.0.0.0:80
        default_backend rs_backend

    backend rs_backend
        balance roundrobin
        option forwardfor
        timeout queue 5s
        timeout server 5m
        timeout connect 4s
        option httpchk GET /haproxy_test/empty.html        
        cookie SERVERID insert nocache indirect maxidle 120s
        server rshost_1 rshost_1:8080 weight 255 maxconn 1024 cookie s1 check inter 1000
        server rshost_2 rshost_2:8080 weight 255 maxconn 1024 cookie s2 check inter 1000
        server rshost_3 rshost_3:8080 weight 255 maxconn 1024 cookie s3 check inter 1000
    

Global settings

    global
        stats socket *:10000 level admin
    

Sets up the statistics socket to listen on all interfaces, port 10000. Set to admin mode so that we change change the server weights.

    defaults
        mode        http
        option      http-server-close
    

We're an http proxy.

Frontend

    frontend all 0.0.0.0:80
        default_backend rs_backend
    

List on port 80 and we use the RealityServer backend.

Backend

    backend rs_backend
        balance roundrobin
        option forwardfor
        timeout queue 5s
        timeout server 5m
        timeout connect 4s
    

Setup a round robin backend load balancer with some time out values.

    
        option httpchk GET /haproxy_test/empty.html        
    

Use the given URL to check that given RealityServer instances are connectable. This URL will be exempt from UAC.

        cookie SERVERID insert nocache indirect maxidle 120s
    

HAProxy will use a cookie called SERVERID to persist which particular RealityServer a session is associated with. The cookie has a lifetime of 120s after which it expires and a new connection from the same user will go into the round robin pool. This idle time should match the session timeout in RealityServer.

        server rshost_1 rshost_1:8080 weight 255 maxconn 1024 cookie s1 check inter 1000
        server rshost_2 rshost_2:8080 weight 255 maxconn 1024 cookie s2 check inter 1000
        server rshost_3 rshost_3:8080 weight 255 maxconn 1024 cookie s3 check inter 1000
    

Backend servers to use. Each has a server name which matches its hostname and forwards requests to port 8080. The cookie keyword specifies the cookie value used to match requests to the server and the servers are checked for connectability every 1000ms.

Each server is set to an initial weight of 255 so they are all selected equally. As sessions start the plugin will modify each servers weight so that less heavily loaded servers will be prioritised for new users.

RealityServer Configuration

We add the following to a default RealityServer configuration:

    uac_user_limit 4
    uac_session_timeout 120

    <user haproxy>
    host haproxy_1
    stats_port 10000
    backend rs_backend
    server ${HOSTNAME}
    </user>

    <url /haproxy_test/.*>
    apply_uac off
    </url>
    <url /favicon.ico>
    apply_uac off
    </url>
    

UAC Configuration

    uac_user_limit 4
    uac_session_timeout 120
    

A limit of 4 users per RealityServer and a session timeout to match the cookie idle time in HAProxy.

HAProxy plugin Configuration

    <user haproxy>
    host haproxy_1
    stats_port 10000
    backend rs_backend
    server ${HOSTNAME}
    </user>
    

The HAProxy plugin is configured using a user config block called haproxy. Here we specify the server that HAProxy is running on, the statistics socket port, the name of the backed that the RealityServer instances are found in and the server name used to identify the host. Note that we are using the environment variable substitution system to specify the server name. This way we can share a single realityserver.conf file over all 3 servers.

URL Exemption

    <url /haproxy_test/.*>
    apply_uac off
    </url>
    <url /favicon.ico>
    apply_uac off
    </url>
    

We exempt the HAProxy test URL from UAC. Note that you will need to make an haproxy_test directory in content_root and place an empty.html file in there. It doesn't need to have any content, it just needs to be there so it can be served up. We also exempt /favicon.ico since many browsers request it by default.

UAC Plugin Implementation

The UAC HAProxy plugin can be found in the src/uac_haproxy directory in the RealityServer distribution. This contains 2 implementation files:
  • uac_haproxy_event_handler.cpp - Event handler implementation
  • uac_haproxy_plugin.cpp - Plugin implementation to install the event handler

This implementation overview will only cover the RealityServer aspects of the implementation, details such as platform specific networking implementations will be omitted. These can be found in the shipped source code.

UAC Handler Implementation

Initialization

    #include "uac_haproxy_event_handler.h"
    #include <mi/base/ilogger.h>

    #include <sstream>

    // include platform specific networking headers
    // ...

    UAC_HAProxy_event_handler::UAC_HAProxy_event_handler(
        const char *host,               // The hostname running HAProxy
        mi::Uint16 port,                // Port that the statistics socket is listening on
        const char *server,             // Our server name qualified with backend. EG: rs_backend/rshost_1
        mi::base::ILogger *logger) :    // The logger
    m_host(host),
    m_port(port),
    m_server(server),
    m_logger(logger,mi::base::DUP_INTERFACE)
    {
        // set weight to 100% to start with. do this since it may be set
        // to some other value from a previous run.
        set_weight(100);
    }

    const char * UAC_HAProxy_event_handler::get_name() const
    {
        // The name of our handler
        return "uac_haproxy_event_handler";
    }
    

Constructor simply takes the configuration values parsed from the config file and stores them internally. As the constructor is only called once on startup it also sets the initial weight of this server to 100% since it will initially have no users on it.

The Handler

    mi::Sint32 UAC_HAProxy_event_handler::handle(
        mi::nservices::IEvent_handler_context *context,
        mi::nservices::IEvent *event )
    {
        // Get the arguments from the event
        mi::base::Handle<mi::IString> session(event->get_event_data<mi::IString>(0));   // session ID, we ignore this
        mi::base::Handle<mi::INumber> curr_users(event->get_event_data<mi::INumber>(1));  // current number of assigned users
        mi::base::Handle<mi::INumber> max_users(event->get_event_data<mi::INumber>(2));   // maximum number of assigned users
        // extract the actual values from the interfaces
        mi::Size curr = curr_users->get_value<mi::Size>();
        mi::Size max = max_users->get_value<mi::Size>();
        // Calculate new server weight and send command to HAProxy. We'll use the percentage weight since 
        // it doesn't rely on knowing what the original weight in HAProxy was.
        mi::Sint32 weight = static_cast<mi::Sint32>(100*(max-curr)/static_cast<mi::Float32>(max));

        // log the weight change
        m_logger->printf(mi::base::MESSAGE_SEVERITY_INFO,"UAC_HAP","Update HAProxy server %s weight to %d%%",
                                                                                    m_server.c_str(),weight);
        // set the new server weight                                                                                    
        set_weight(weight);

        return 0;
    }    
    

The handler simply calculates the weight as the fraction of free slots in relation to maximum slots. This is converted to a percentage and set as the servers weight

Communication with HAPRoxy

    void UAC_HAProxy_event_handler::set_weight( mi::Sint32 weight_percentage )
    {
        std::stringstream sstr;
        sstr << "set weight " << m_server << " " << weight_percentage << "%" << "\n";

        // Connect to m_host:m_port via TCP and send sstr.str().c_str() to it to modify the servers weight.
        // ...
    }
    

This method constructs the command to modify the servers weight and sends it to the statistics socket.

UAC Plugin Install Implementation

The Handler itself is registered and installed in the IServices_plugin::initialize() implementation

    void UAC_HAProxy_plugin::initialize(mi::rswservices::IExtension_context* context)
    {
        // Get the logger
        mi::base::Handle<mi::neuraylib::IPlugin_api> plugin_api(context->get_plugin_api());
        mi::base::Handle<mi::neuraylib::ILogging_configuration> log_config(
            plugin_api->get_api_component<mi::neuraylib::ILogging_configuration>());
        mi::base::Handle<mi::base::ILogger> logger(log_config->get_forwarding_logger());
    

Gets the logger to provide to the handler and to log our own messages

        // Find the config and extract the elements. If any are not provided then we don't
        // install the handler.
        mi::base::Handle<const mi::rswservices::IConfiguration> config(context->get_configuration());
        mi::base::Handle<const mi::rswservices::IUser_configuration> hap_config(config->get_user("haproxy"));
        const char* hap_host = NULL;
        mi::Uint16 hap_port = 0;
        const char* hap_backend = NULL;
        const char* hap_server = NULL;

        if (hap_config.is_valid_interface()) {
            mi::base::Handle<const mi::rswservices::IUser_configuration> hap_config_subitem;
            hap_config_subitem = hap_config->get_subitem("host");
            if(hap_config_subitem.is_valid_interface()) {
                hap_host = hap_config_subitem->get_value();
            }
            hap_config_subitem = hap_config->get_subitem("stats_port");
            if(hap_config_subitem.is_valid_interface()) {
                hap_port = atoi(hap_config_subitem->get_value());
            }
            hap_config_subitem = hap_config->get_subitem("backend");
            if(hap_config_subitem.is_valid_interface()) {
                hap_backend = hap_config_subitem->get_value();    
            }
            hap_config_subitem = hap_config->get_subitem("server");
            if(hap_config_subitem.is_valid_interface()) {
                hap_server = hap_config_subitem->get_value();    
            }
        }
        if (hap_host == NULL || hap_port == 0 || hap_backend == NULL || hap_server == NULL) {
            logger->message(mi::base::MESSAGE_SEVERITY_INFO,"UAC_HAP","Not installing UAC HAProxy "
                                                                        "manager plugin as it is "
                                                                        "not configured.");
            return;
        }
    

Gets the 'haproxy' user configuration block and extracts all the configuration elements from it. If the block, or any of the elements we require, cannot be found then we log a message and do not continue with installation

        // All good, contruct the HAProxy server name
        std::string server = hap_backend + std::string("/") + hap_server;

        // Create and install the event handler
        mi::base::Handle<mi::nservices::IEvent_handler> haproxy_handler(new UAC_HAProxy_event_handler(
                                                                        hap_host,
                                                                        hap_port,
                                                                        server.c_str(),
                                                                        logger.get()));
        context->install_event_handler(haproxy_handler.get());
    

Generate the full HAProxy server name and instantiate the handler implementation. This is then installed into neuray services so it is available to the event system

        // Associate the handler with UAC add and remove session events
        mi::base::Handle<mi::nservices::IEvent_context> event_context(context->get_event_context());
        event_context->register_handler(mi::rswservices::UAC_SESSION_ADDED(),
                                            haproxy_handler->get_name(),
                                            "haproxy_add",0);
        event_context->register_handler(mi::rswservices::UAC_SESSION_REMOVED(),
                                            haproxy_handler->get_name(),
                                            "haproxy_remove",0);

        logger->message(mi::base::MESSAGE_SEVERITY_INFO,"UAC_HAP","UAC HAProxy manager plugin initialized.");
        logger->printf(mi::base::MESSAGE_SEVERITY_INFO,"UAC_HAP","Managing %s on HAProxy at %s:%u.",
                                                                                    server.c_str(),
                                                                                    hap_host,
                                                                                    hap_port);
    }    
    

Finally we obtain the web service event context and register the HAProxy handler with the session add and remove events. This ensures that the handler is called whenever a session is added or removed.