Skip to content

Styx Object Model

Mikko Karjalainen edited this page May 7, 2020 · 15 revisions

Problem Statement

Styx application routing was originally designed around its origin configuration file structure. It was built around BackendService and Origin object model serialised directly from the origins.yaml file.

It was quite monolithic and not flexible for further changes. For this reason we have designed a new highly composable object model to route incoming requests. Some of the early phases of the work is documented here: Advanced Routing Configuration.

As of writing (1.0.0.beta8 release) the new object model is still experimental. Therefore Styx Core contains both the old monolithic application routing engine and the new routing object engine.

Styx Object Model

Styx Core consist of 3 kinds of entities:

  1. Data Plane entities filter and route incoming requests to appropriate backend services.
  2. Control Plane entities that monitor and configure data plane objects. Such as health check monitor.
  3. Servers, listen to incoming network traffic, and pass them on to data plane.

Styx has a common configuration object framework that covers all these 3 entities. It provides a standard configuration format, validation, loading, storage, and lifecycle management.

A Styx configuration object has:

  • An unique name (among similar "entities")
  • Type
  • Tags
  • Configuration (in yaml)

Example

routingObjects:
  root:
    type: PathPrefixRouter
    config:
      routes:
        - { prefix: /, destination: landingApp }
        - { prefix: /api/, destination: apiServer }

  landingApp:
    type: LoadBalancingGroup
    config:
      origins: landing

  apiServer:
    type: LoadBalancingGroup
    config:
      origins: apiServer

  landing01:
    type: HostProxy
    tags:
      - landing
    config:
      host: "remotehost01:443"

  landing02:
    type: HostProxy
    tags:
      - landing
    config:
      host: "remotehost02:443"

  apiServer01:
    type: HostProxy
    tags:
      - apiServer
    config:
      host: "remoteapihost01:443"

  apiServer02:
    type: HostProxy
    tags:
      - apiServer
    config:
      host: "remoteapihost02:443"

Control Plane Objects

Control plane objects are long running processes that configure and monitor the data plane objects, but won't participate in data plane processing. They are used for

  1. Configuring the data plane from external configuration sources.
  2. Monitoring and health checking.
  3. Exposing metrics and information.

See Control Plane Object Reference for detailed information.

Data Plane Objects

Styx data plane is a directed acyclic graph of named rouging objects. Each in some way processing the request passing through. Request messages enter the data plane typically at the root object (but can be any other named object) and they trickle down some path towards the leaf. Response messages follow the same path, but from leaf to root.

The data plane is declared in the routing object configuration.

There are two ways to specify relationships between the objects: 1) a named reference or 2) embedding an anonymous object.

The previous configuration example demonstrates named references. Each object is named, and the forwarding graph is formed by the named references between the objects. The previous configuration can be illustrated as:

  PathPrefixRouter 
          |                                       +--- HostProxy 
          |  "/"                                  |    (tag: landing)     
          +------------> LoadBalancingGroup ------+
          |                app: landing           |
          |                                       +--- HostProxy 
          |                                            (tag: landing)
          |
          |
          |  "/api/"
          +------------> LoadBalancingGroup ------+--- HostProxy 
                           app: apiServer         |    (tag: apiServer)
                                                  |
                                                  +--- HostProxy 
                                                       (tag: apiServer)

We could also embed the two LoadBalancingGroup objects in PathPrefixRouter. Like so:

routingObjects:
  root:
    type: PathPrefixRouter
    config:
      routes:
        - prefix: /, 
          destination:
            type: LoadBalancingGroup
            config:
              origins: landing

        - prefix: /api/, 
          destination:
            type: LoadBalancingGroup
            config:
              origins: apiServer
  ...

By embedding the LoadBalancingGroup objects, we have eliminated them from the data plane graph. The relevant functionality is now appears in the root PathPrefixRouter. The DAG thus becomes:

  PathPrefixRouter 
          |                             +--- HostProxy 
          |  "/" (app: landing)         |    (tag: landing)     
          +-----------------------------+
          |                             |
          |                             +--- HostProxy 
          |                                  (tag: landing)
          |
          |  "/api/" (app: apiServer)
          +-----------------------------+--- HostProxy 
                                        |    (tag: apiServer)
                                        |
                                        +--- HostProxy 
                                             (tag: apiServer)

We have implemented a "backwards compatibility mode" allowing existing deployments to keep origins.yaml configuration with the new routing object engine (PR #458). It translates the origin file automatically in this routing object configuration.

See Routing Object Reference for detailed information.

Server Object Reference

HttpServer

Opens a TCP server socket for incoming HTTP messages. Passes the incoming HTTP messages to Styx data plane.

Configuration schema:

    val SCHEMA = `object`(
            field("port", integer()),
            field("handler", string()),
            optional("compressResponses", bool()),
            optional("tlsSettings", `object`(
                    optional("sslProvider", string()),
                    optional("certificateFile", string()),
                    optional("certificateKeyFile", string()),
                    optional("sessionTimeoutMillis", integer()),
                    optional("sessionCacheSize", integer()),
                    optional("cipherSuites", list(string())),
                    optional("protocols", list(string()))
            )),

            optional("maxInitialLength", integer()),
            optional("maxHeaderSize", integer()),
            optional("maxChunkSize", integer()),

            optional("requestTimeoutMillis", integer()),
            optional("keepAliveTimeoutMillis", integer()),
            optional("maxConnectionsCount", integer()),

            optional("bossExecutor", string()),
            optional("workerExecutor", string())
    )

Important fields:

  • port - A TPC port number to listen to. Pass 0 to automatically allocate a free port.
  • handler - A Styx routing object reference. Passes all received traffic to this styx routing object. Returns 502 Bad Gateway if this object doesn't exist.

Routing Object Reference

LoadBalancingGroup

Balances incoming traffic using Power of 2 algorithm among all group members.

Schema:

        val SCHEMA = `object`(
                field("origins", string()),
                optional("originRestrictionCookie", string()),
                optional("stickySession", `object`(
                        field("enabled", bool()),
                        field("timeoutSeconds", integer())
                ))
        )

Any named routing object can become a member regardless of type. The group membership is indicated by origins field, which is a non-empty string. Any object appropriately tagged with origins value automatically becomes a member. Suppose origins is green. Then any object tagged with lbGroup=green automatically becomes a group member.

A routing object can be tagged with state=active, state=unreachable or state=inactive tags. A group member with state receives traffic only if it is active. It assumed active if the state tag is absent.

  • state=active means the object is administratively enabled, and reachable.
  • state=unreachable means the object is administratively enabled, but declared unreachable by the health check function.
  • state=inactive means the object is administratively disabled.

HostProxy

Relays traffic to the configured remote host.

    public static final Schema.FieldType SCHEMA = object(
            field("host", string()),
            optional("tlsSettings", object(
                    optional("trustAllCerts", bool()),
                    optional("sslProvider", string()),
                    optional("trustStorePath", string()),
                    optional("trustStorePassword", string()),
                    optional("protocols", list(string())),
                    optional("cipherSuites", list(string())),
                    optional("additionalCerts", list(object(
                            field("alias", string()),
                            field("certificatePath", string())
                    ))),
                    atLeastOne("trustAllCerts",
                            "trustStorePath",
                            "trustStorePassword",
                            "protocols",
                            "cipherSuites",
                            "additionalCerts")
            )),
            optional("connectionPool", object(
                    optional("maxConnections", integer()),
                    optional("maxPendingConnections", integer()),
                    optional("connectTimeoutMillis", integer()),
                    optional("socketTimeoutMillis", integer()),
                    optional("pendingConnectionTimeoutMillis", integer()),
                    optional("connectionExpirationSeconds", integer()),
                    atLeastOne("maxConnections",
                            "maxPendingConnections",
                            "connectTimeoutMillis",
                            "socketTimeoutMillis",
                            "pendingConnectionTimeoutMillis",
                            "connectionExpirationSeconds")
            )),
            optional("responseTimeoutMillis", integer()),
            optional("metricPrefix", string())
    );

This object is always a terminal object in the data path. It cannot pass the traffic any further. It always produces a response.

ConditionRouter

Performs a routing decision based on some condition.

    public static final Schema.FieldType SCHEMA = object(
            field("routes", list(object(
                    field("condition", string()),
                    field("destination", routingObject()))
            )),
            optional("fallback", routingObject())
    );

See The Styx Manual for details.

PathPrefixRouter

Performs a routing decision based on longest URL path prefix match.

Similar to ConditionRouter but the condition is always a path prefix.

    public static final Schema.FieldType SCHEMA = object(
            field("routes", list(object(
                    field("prefix", string()),
                    field("destination", routingObject()))
            ))
    );

InterceptorPipeline

A container for running Styx interceptors, both external plugins and built in interceptors.

    public static final Schema.FieldType SCHEMA = object(
            optional("pipeline", or(string(), list(string()))),
            field("handler", routingObject())
    );

See Styx User Manual for details.

StaticResponseHandler

Responds with a HTTP response.

NOTE: We are likely to rename this to StaticResponse.

    public static final Schema.FieldType SCHEMA = object(
            field("status", integer()),
            optional("content", string()),
            optional("headers", list(object(
                    field("name", string()),
                    field("value", string())
            ))));

This is a terminal object that cannot pass the request any further. Guaranteed to always respond with given response parameters.

Control Plane Object Reference

HealthCheckMonitor

Monitors a group of routing objects. The group membership follows the LoadBalancingGroup configuration: the objects=GROUPNAME field identifies the group name, and the objects tagged with lbGroup=GROUPNAME are included in the monitoring group.

Schema:

        val SCHEMA = SchemaDsl.`object`(
                field("objects", string()),
                optional("path", string()),
                optional("timeoutMillis", integer()),
                optional("intervalMillis", integer()),
                optional("healthyThreshold", integer()),
                optional("unhealthyThreshold", integer())
        )

The health check monitor is intended for probing HostProxy objects (because they communicate over the network). But as with LoadBalancingGroup any type of object can be a group member.

The monitor periodically probes its group members. A probe is considered successful when the underlying object responses with a successful HTTP status code (less than 400).

It tags the healthy group members with state=active, and unhealthy or unreachable members with state=unreachable.

The Lifecycle Tags section below provides more information about health check provider object tagging policy.

YamlFileConfigurationService

Configures Styx data plane from Styx origins yaml configuration.

        val SCHEMA = SchemaDsl.`object`(
                field("originsFile", string()),
                optional("monitor", bool()),
                optional("ingressObject", string())

Reads the origins file from location given by originsFile field. It constructs a graph of routing objects corresponding to the yaml file and injects them to the styx object database:

  • Creates a HostProxy object for each origin.
  • Creates LoadBalancingGroup for each BackendService.
  • When health checking is enabled, starts a HealthCheckMonitor for each LoadBalancingGroup.
  • Creates a PathPrefixRouter that is the root of the resulting object graph.

The optional ingressObject gives a name to the root PathPrefixRouter object. When the ingressObject is absent, the root object is called <service-name>-router.

You still need to configure Styx to pass the received traffic to the root object (by setting the httpPipeline).

You can configure multiple YamlFileConfigurationService objects, one for each origins file. But you will need to take care to avoid name clashes for the generated objects.

When monitor is true, the service automatically monitors for configuration changes, and reconfigures the data plane accordingly.

Lifecycle Tags

Tags are used to record state and health information about routing objects, in particular about HostProxy objects.

The state tag

The state tag can have the following values:

  • active - The object is running normally, and can be included in the rotation of a LoadBalancer object.
  • inactive - The HostProxy has been deactivated via the Admin interface, and is not included in the rotation of a LoadBalancer object.
  • unreachable - A HealthCheck service has determined that this HostProxy's downstream origin is not reachable. The object is not included in the rotation of a LoadBalancer.

The state tag is always present, and always has a value, on a HostProxy object.

If a HealthCheck service is configured on a HostProxy object, that object will start up in the unreachable state. It is the responsibility of the HealthCheck service to determine that the associated origin is reachable, and to change the HostProxy state to active.

If no HealthCheck service is configured on a HostProxy object, that object will start up in the active state.

A HealthCheck service may change the state tag value of a HostProxy object from active to unreachable, and from unreachable to active.

A request via the Admin interface can change the state tag value of a HostProxy object from either active or unreachable to inactive. If a HealthCheck service is configured, activating the object via the Admin interface will change the state from inactive to unreachable. If no HealthCheck service is configured, activating the object via the Admin interface will change the state from inactive to active.

The healthCheck tag

The healthCheck tag is used by the HealthCheck service to record information about whether healthcheck probes are currently failing or succeeding via a HostProxy object. Probes that are failing on an active object, or succeeding on an unreachable object, can lead to a change in the state tag.

The tag's value has the format on[;<condition>:<count>]

  • Where an object's state is active and the health probes are passing, or the state is unreachable and the probes are still failing, then the tag's value will just be on.
  • Where an object's state is active but health probes are currently failing, then the tag's value will be on;probes-FAIL:<count>, where <count> is the number of consecutive failed probes. When this count reaches a configured threshold, the HealthCheck service will change the state tag to unreachable.
  • Where an object's state is unreachable but health probes are currently succeeding, then the tag's value will be on;probes-OK:<count>, where <count> is the number of consecutive successful probes. When this count reaches a configured threshold, the HealthCheck service will change the state tag to active.

The tag is not present when health checking is not configured on an object.