ServiceStack Plugin for Service Discovery using Consul.io
A plugin for ServiceStack that provides
transparent client service discovery
using Consul.io as a Service Registry
This enables distributed servicestack instances to call one another,
without either knowing where the other is, based solely on a
copy of the .Net CLR request type.
Your services will not need to take any dependencies on each other
and as you deploy updates to your services they will automatically be registered
and used without reconfiguring the existing services.
The automatic and customisable health checks for each service will
also ensure that failing services will not be used, or if you
run multiple instances of a service, only the healthy and most responsive
service will be returned.
A consul agent must be running on the same machine as the AppHost.
Install the package https://www.nuget.org/packages/ServiceStack.Discovery.Consul
PM> Install-Package ServiceStack.Discovery.Consul
Add the following to your AppHost.Configure
method
public override void Configure(Container container)
{
SetConfig(new HostConfig
{
// the url:port that other services will use to access this one
WebHostUrl = "http://api.acme.com:1234",
// optional
ApiVersion = "2.0",
HandlerFactoryPath = "/api/"
});
// Register the plugin, that's it!
Plugins.Add(new ConsulFeature());
}
To call external services, you just call the Gateway and let it handle the routing for you.
public class MyService : Service
{
public void Any(RequestDTO dto)
{
// The gateway will automatically route external requests to the correct service
var internalCall = Gateway.Send(new InternalDTO { ... });
var externalCall = Gateway.Send(new ExternalDTO { ... });
}
}
It really is that simple!
Before you start your services, you’ll need to download consul and start the agent running on your machine.
The following will create an in-memory instance which is useful for testing
consul.exe agent -dev -advertise="127.0.0.1"
You should now be able see the Consul Agent WebUI link appear under Plugins on the metadata page.
docker pull consul
docker run -dp 8500:8500/tcp --name=dev-consul consul agent -dev -ui -client 0.0.0.0
This will create an in-memory instance using the official docker image
Once you have added the plugin to your ServiceStack AppHost and
started it up, it will self-register:
Each service can have any number of health checks. These checks are run by Consul and allow service discovery
to filter out failing instances of your services.
By default the plugin creates 2 health checks
NB From Consul 0.7 onwards, if the heartbeat check fails for 90 minutes, the service will automatically be unregistered
You can turn off the default health checks by setting the following property:
new ConsulFeature(settings => { settings.IncludeDefaultServiceHealth = false; });
You can add your own health checks in one of two ways
new ConsulFeature(settings =>
{
settings.AddServiceCheck(host =>
{
// your code for checking service health
if (...failing check)
return new HealthCheck(ServiceHealth.Critical, "Out of disk space");
if (...warning check)
return new HealthCheck(ServiceHealth.Warning, "Query times are slow than expected");
...ok check
return new HealthCheck(ServiceHealth.Ok, "working normally");
},
intervalInSeconds: 60 // default check once per minute,
deregisterIfCriticalAfterInMinutes: null // deregisters the service if health is critical after x minutes, null = disabled by default
);
});
If an exception is thrown from this
check, the healthcheck will return Critical to consul along with the exception
new ConsulFeature(settings =>
{
settings.AddServiceCheck(new ConsulRegisterCheck("httpcheck")
{
HTTP = "http://myservice/custom/healthcheck",
IntervalInSeconds = 60
});
settings.AddServiceCheck(new ConsulRegisterCheck("tcpcheck")
{
TCP = "localhost:1234",
IntervalInSeconds = 60
});
});
http checks must be GET and the health check expects a 200 http status code
_tcp checks expect an ACK response.
It is important to understand that in order to facilitate seamless service to service calls across different apphosts,
there are a few opinionated choices in how the plugin works with consul.
Firstly, the only routing that is supported, is the default pre-defined routes
The use of the Service Gateway, also dictates that the ‘IVerb’
interface markers must be specified on the
DTO’s in order to properly send the correct verb.
Secondly, lookups are ‘per DTO’ type name - This enables the service, apphost or namespaces to change over time for a DTO endpoint.
By registering all DTO’s in the same consul ‘service’, this allows seamless DNS and HTTP based lookups using only the DTO name.
Each service or apphost will not be shown in consul as a separate entry but rather ‘nodes’ under a single ‘api’ service.
For this reason, it is expected that:
Registering in this way allows for the most efficient lookup of the correct apphost for a DTO and also enables DNS queries
to be consistent and ‘guessable’.
# {dtoName}.{serviceName}.{type}.consul
hellorequest.api.service.consul
Changing the service name per apphost, makes it impossible to simply query a consul datacenter in either http or dns for
the a dto’s endpoint.
If there are types that you want to exclude from being registered
for discovery by other services, you can use one of the following options:
The ExcludeAttribute
: Feature.Metadata
or Feature.ServiceDiscovery
are not registered
[Exclude(Feature.ServiceDiscovery | Feature.Metadata)]
public class MyInternalDto { ... }
The RestrictAttribute
. Any type that does not allow RestrictAttribute.External
will be excluded.
See the documentation for more details
[Restrict(RequestAttributes.External)]
public class MyInternalDto { ... }
The default discovery mechanism uses the ServiceStack request types to resolve
all of the services capable of processing the request. This means that you should
always use unique request names across all your services for each of your RequestDTO’s
To override the default which uses Consul, you can implement your ownIServiceDiscovery<TServiceModel, TServiceRegistration>
client to use whatever backing store you want.
new ConsulFeature(settings =>
{
settings.AddServiceDiscovery(new CustomServiceDiscovery());
});
public class CustomServiceDiscovery : IServiceDiscovery<TServiceModel, TServiceRegistration>
{
...
}
By default a JsonServiceClient
is used for all external Gateway
requests.
To change this default, or just to add additional client configuration,
you can set the following setting:
new ConsulFeature(settings =>
{
settings.SetDefaultGateway(baseUri => new JsvServiceClient(baseUri) { UserName = "custom" });
});
You can then continue to use the Gateway as normal but any external call will now use your preferred IServiceGateway
public class EchoService : Service
{
public void Any(int num)
{
// this will use the JsvServiceClient to send the external DTO
var remoteResponse = Gateway.Send(new ExternalDTO());
}
}
you can add your own custom tags to register with consul. This can be useful when you override the
default ‘IDiscoveryTypeResolver’ or want to register different regions or environments for services
new ConsulFeature(settings => { settings.AddTags("region-us-east", "region-europe-west", "region-aus-east"); });
The following shows the services registered with consul and passing health
checks and the services running on different IP:Port/Paths