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:
- AppHost.AfterInit - Registers the service and it's operations in the service registry.
- AppHost.OnDispose - Unregisters the service when the AppHost is shutdown.
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
- Heartbeat : Creates an endpoint in your service http://locahost:1234/reply/json/heartbeat that expects a 200 response
- If Redis has been configured in the AppHost, it will check Redis is responding
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:
- DTO names are not changed over time breaking the predefined routes.
- DTO names 'globally unique' in all discovery enabled apphosts to avoid DTO name collisions.
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 own
IServiceDiscovery<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