Servicy

A service registration and discovery platform in Ruby for SOA.

The goal of Servicy is to provide a simple daemon which allows services in an SOA application configuration to register themselves by name and/or API, and allow service consumers to find instances of services to use, and be relatively sure of up-time, routing, etc.

Servicy could possibly used as a router as well, although that may be out of scope we will see.

General architecture ideas

Servicy would be broken into a few pieces:

  1. A daemon which can handle registration and query requests
  2. An in-memory database of services to facilitate queries
  3. A persistent cache of services to allow for fast restarts
  4. (possibly) A service/API router
  5. Reporting service to report on service health and usage.
  6. Client library for service discovery and connection
  7. Service library for registration, API discovery and building

One big issue that will have to be resolved is version dependencies. If there are two versions of a service, each with a different public API, then Servicy should know about the differences and send back to a service requester the correct version based on their requirements. For this, Servicy should have a way of discovering and/or services defining their API to Servicy.

Each service should also be able to define multiple instances, and Servicy should have multiple strategies for choosing instances of a given service. For example, round-robin, load-based (would require machine-level meta-data to be reported by the services), even requests, etc. To start with, a simple round-robin scheme will likely be the only implementation. Furthermore, Servicy should have knowledge of if a service is up or down, and automatically remove from its list of available providers, or add it back in as needed.

Also, Servicy should maintain statistics on usage for services and be able to report that information back to a user. Stats should include number of discoveries, cache hits/misses, service up-times, provider failures, and, if it works as a router, request latency. In the event that I end up not making it a router (seems likely at this point), it should still report latency for service discovery and query operations that require back-and-forth with the service.

Protocol ideas

Servicy is protocol agnostic, and the exact transport mechanism used to communicate between client and server doesn't matter. The data is sent as JSON underneath it all (for now; I may even change things to be format agnostic in the future). You can see examples of the JSON by looking in the Servicy::Transport::Message class.

Service registration

Service registration allows for a service to declare that they exist, how to reach them, and what functionality they provide. Services are named using a Java-style, inverted domain name. For example:

com.mycompany.users.authentication

might be the name of a user authentication service provided internally. This allows for services to be broken up by what other services they interact with, as well as be easy to read for users browsing the registry.

Beyond the name, a service must provide some other information for its registry. This includes:

  1. Host to connect to
  2. Port on the host to connect to
  3. Protocol used to communicate (HTTP, HTTPS, Thrift, etc.)
  4. Version number
  5. Heartbeat port

The host can be either an IP address or a fully-qualified hostname. In either case it should be route-able in the context of the Servicy host. In other words, if Servicy cannot connect with it, it won't add it to the registry. The port should be an integer between 1 and 65535. The protocol can be anything, however some standards are:

  • HTTP - Must use un-encrypted HTTP
  • HTTPS - Must use TLS over HTTP
  • HTTP/S - Can use either HTTP or HTTPS
  • Thrift
  • TCP - Direct, telnet-like communication
  • UDP - Direct connection, but don't expect replies

The protocol says nothing about what information needs to be sent or received by a client consuming the service. It is assumed that if the client is looking for the service, it knows how to use it once it is found, and will only use the protocol information to select between possible options provided by the consuming library. Protocols can also be provided as a list of possible options, ordered with most preferred first. At some point in the future, there may be a mechanism by which a service can describe itself in a more useful way, and client libraries can use that information to dynamically build an internal API.

The version number must follow the format:

major.minor.revision-patch

The "-patch" part can be omitted. For example:

1.0.2-p143

The heartbeat port is a port that Servicy can connect to to ensure that the service is still functioning. In many cases this will be the same port as the primary connection port; Servicy does not attempt to use the heartbeat connection for anything. It simply connects, and if successful, disconnects again. For this reason, your services should be resilliant to this type of traffic.

Along with the required information already listed, a service can provide some optional information about the API that they provide. This information comes with some pieces of information for each API endpoint or action provided. The information is:

  1. Endpoint identifier
  2. Parameter information
  3. Return information
  4. (optional) Human-readable documentation.

The endpoint identifier can be anything, and is just a string. For example, in an HTTP/S RESTful API, it may be something like:

/api/v1/user/login

The parameter information is information about each parameter that is expected by this endpoint. This data is structured, and requires information about name, data type, and weather or not the parameter is required.

The return information is simply a description of the data that should be expected to return.

The documentation is an arbitrary-length text string which can be used as documentation by a developer developing against the API.

Multiple service providers can provide the same service. In that case, they are simply added to a pool of possible providers that is selected from using some load-balancing strategy such as round-robin or random selection.

Any service provider that has registered will periodically get ICMP pinged by Servicy. If a response does not come back, it will be removed from the pool of providers. If a response does come back, Servicy will then attempt to connect to the heartbeat port to determine if the service is still functioning on the box. If it is not, the provider will be removed from the pool, otherwise it will be left in. This two-step process is to facilitate better logging and debugging of issues.

Service de-registration

A service can remove itself from the pool voluntarily be simply sending the name, version, host, and port of the service to be removed. However, this is not strictly needed as the periodic heartbeat check will notice if a service goes away. These checks happen every second.

A note on security ++++++++++++++++++

For the time-being, it is assumed that you will have Servicy and all the services it interacts with installed on internal, secured infrastructure. As such, Servicy takes a very trusting policy. However, that means that someone with malintent and access to your network could de-register services, or register a bunch of fake ones. Future versions of Servicy may attempt to resolve this issue. We'll see...

Service discovery

Once services are registered, a client needs to be able to discover them. Generally, they will discover a service and hold on to that information for the lifetime of their use, only requesting a new handle should the old one be lost. However, there is no reason why they couldn't request a new one every time.

NOTE In the future, it may be possible for Servicy to act as a router by simply returning connection information for itself, and doing blind pass-through with some strategy to the other providers. In that case, the policy that a client uses to cache or not cache their connection information may have to change.

A service consumer can query using one or both of two different dimensions:

  1. API name
  2. API functionality

When using the name dimension, you can also query by version using either an exact match, a list of preferences, a range, a minimum, or a maximum.

When querying using functionality, you query using one or more API method fingerprints. The fingerprints are in the following format:

method_name#arg-type,arg-type,...#return_type

where method_name is the name of the method you would like to call (or service end-point, or whatever. This is the same as the API endpoint in the service functionality registration information.), the arg-type pairs are the name of an argument (or an empty string in the case of un-named arguments), followed by a dash, followed by the argument type, and the return_type being the data-type for the return value.

For example, a query could look something like this:

user_create#username-string,password-string#User

Behind the scenes, Servicy will use this information to find all possible services that can fulfill this request. This will include any service that can perform this request but that may have more optional parameters. Type information can also be provided as a list of types, in the case of API's that can handle multiple types of information:

user_create#username-string,password-string|signed_string#User

The possible other types that the data can be are separated by a pipe, '|'.

Finally, the service consumer can provide a boolean flag indicating weather or not they wish to receive API information about the returned providers, or if they simply want connection information. If the flag is set to true, they will get full API documentation and definitions along with connection information.

The query system will either provide a successful "error"-code, along with a list of one or more service providers, a "not-found" error-code indicating that the query was valid, but that there are no available services which meet the requirements, or an "error" error-code that indicates that something was wrong with the provided query. In the last two queries, additional error information may be provided, but should be considered for human-use only; ie, showing an error message to a user.

Notes on design of your objects

See here. It's a good article. The upshot is that, while hiding weather or not an object is remote is good for programmer burden (ie, it reduces it), it's bad for API design.

Servicy's goal is to make it simple for you to build microservices in Ruby that can either run locally or remotely without the developer having to worry about or care which. That said, as the developer of a service, it is your responsibility to make sure that you don't make services that suck (tm).

Don't build your objects to have iterators that require multiple-traversals over the wire. Don't build objects that lazy-evaluate things. Don't build objects that themselves rely on other services that are poorly built. Etc.

Do eat your own dogfood, and use your own toilet paper. If it hurts for you to use it, with all your insider knowledge and perspective, it is downright agony for other people.

TODO

Things left to do:

  1. In the command-line tool, there is some kind of bug where the registration message never returns back to the client. This causes registration to hang forever...
  2. I should sit down and really think out code structure and refactor where needed. Things are getting a little difficult to follow if you are jumping around between files a bunch.
  3. I need to cut down on the number of latency and heartbeat entries that are saved. It gets a bit out of hand.