Discovery Service¤
Service discovery enables communication among multiple ASAB microservices in a server cluster. Each microservice can search for and find address to API interface (URL) of any other service within the cluster.
In service discovery, there are two main roles: the "server" and the "client."
The server advertises its "position" in the cluster and provides an API for communication. The client uses this API to interact with the server.
In the context of service discovery, all microservices in the cluster act as both servers and clients.
Prerequisites¤
Following requirements must be fulfilled:
- Zookeeper connection and configuration must be the same for all services in the cluster.
- Zookeeper container must be initialized in the service.
asab.WebService
andasab.WebContainer
must be initialized.asab.APIService
must be initialized.- Environment variables
NODE_ID
,SERVICE_ID
andINSTANCE_ID
must be set. INSTANCE_ID
(or hostname ifINSTANCE_ID
is missing) must be resolvable.
Server - Advertising into ZooKeeper¤
Even though the service can provide multiple communication interfaces, major use case is the web API.
A "server" application has to provide the API and advertise its address into a consensus technlogy - ZooKeeper.
Application requirements¤
Services inside the application must be initialized in the right order.
class MyApp(asab.Application):
def __init__(self):
super.__init__(self)
# Initialize web server
self.add_module(asab.web.Module)
websvc = self.get_service("asab.WebService")
self.WebContainer = asab.web.WebContainer(websvc, "web")
# Initialize zookeeper
self.add_module(asab.zookeeper.Module)
self.ZooKeeperService = self.get_service("asab.ZooKeeperService")
self.ZooKeeperContainer = asab.zookeeper.ZooKeeperContainer(
self.ZooKeeperService, "zookeeper"
)
# Initialize ApiService
self.ASABApiService = asab.api.ApiService(self)
self.ASABApiService.initialize_web(self.WebContainer)
self.ASABApiService.initialize_zookeeper(self.ZooKeeperContainer)
Configuration¤
ZooKeeper configuration must be the same for all services in the cluster. Port can be set in the configuration.
[zookeeper]
servers=zookeeper-1:2181,zookeeper-2:2181,zookeeper-3:2181
[web]
listen=0.0.0.0 8090
Environment variables¤
The discovery is based on provided "ids". Use either a set of specific ids provided as environemntal variable, or create custom discovery ids within the application.
Pre-cast ids used for service discovery. Provide them as environment variables:
Variable | Description | Example |
---|---|---|
SERVICE_ID |
Identifier of the ASAB microservice. | my-app |
INSTANCE_ID |
Identifier of a specific instance of the ASAB microservice. | my-app-1, my-app-2, ... |
NODE_ID |
Identifier of a cluster node. | node-1, node-2, ... |
Instance ID must be resolvable
ASAB framework cannot set up networking. In order to enable service discovery, INSTANCE_ID
of a service must be resolavable from the client. If INSTANCE_ID
is missing, hostname is taken instead and then, hostname must be resolvable.
Information advertised¤
When all requirements of a server-side microservice are fullfilled, the service advertises into the consensus a unique information bound to its runtime.
The file of JSON format contains information describing the running application.
{
"host": "server-name-1",
"appclass": "MyApp",
"node_id": "node-1",
"service_id": "my-app",
"instance_id": "my-app-1",
"launch_time": "2023-12-01T10:52:29.656397Z",
"process_id": 1,
"containerized": true,
"created_at": "2023-11-23T11:08:22.454248Z",
"version": "v23.47-alpha",
"CI_COMMIT_TAG": "v23.47-alpha",
"CI_COMMIT_REF_NAME": "v23.47-alpha",
"CI_COMMIT_SHA": "...",
"CI_COMMIT_TIMESTAMP": "2023-11-23T11:00:48+00:00",
"CI_JOB_ID": "...",
"CI_PIPELINE_CREATED_AT": "2023-11-23T11:06:40Z",
"CI_RUNNER_ID": "..",
"CI_RUNNER_EXECUTABLE_ARCH": "linux/amd64",
"web": [
[
"0.0.0.0",
8090
]
]
}
Client - Using Service Discovery¤
All services are being "server" and "client" at the same time. This paragraph describes how to discover (as a client) a service in the cluster.
Call API using DiscoveryService.session()¤
Once the service propagates itself into ZooKeeper, other services in the cluster can use its API.
DiscoveryService
provides a method session()
which inherits from aiohttp.ClientSession
. It can be used the same way as aiohttp.ClientSession
. Instead of explicit URL, use URL with asab
domain.
The URL is constructed in the format http://<value>.<key>.asab/...
where key is the name of the identifier (e.g. instance_id
, service_id
) and value is its value (e.g. my_app_1
)
Example
class MyApp(asab.Application):
def __init__(self):
super.__init__(self)
# Initialize web server
self.add_module(asab.web.Module)
websvc = self.get_service("asab.WebService")
self.WebContainer = asab.web.WebContainer(websvc, "web")
# Initialize zookeeper
self.add_module(asab.zookeeper.Module)
self.ZooKeeperService = self.get_service("asab.ZooKeeperService")
self.ZooKeeperContainer = asab.zookeeper.ZooKeeperContainer(
self.ZooKeeperService, "zookeeper"
)
# The DiscoverySession is functional only with ApiService initialized.
self.ASABApiService = asab.api.ApiService(self)
self.ASABApiService.initialize_web(self.WebContainer)
self.ASABApiService.initialize_zookeeper(self.ZooKeeperContainer)
self.DiscoveryService = self.get_service("asab.DiscoveryService")
async def main(self):
async with self.DiscoveryService.session() as session:
try:
# use URL in format: <protocol>://<value>.<key>.asab/<endpoint> where key is "service_id" or "instance_id" and value the respective service identificator
async with session.get("http://my_application_1.instance_id.asab/asab/v1/config") as resp:
if resp.status == 200:
config = await resp.json()
except asab.api.discovery.NotDiscoveredError as e:
L.error(e)
Warning
asab.DiscoveryService
is functional only with ApiService
initialized. That means, WebContainer
and ZooKeeperContainer
must be also present in the application.
Warning
Discovery Service searches for microservices in the same ZooKeeper path as defined in the configuration. Therefore, all services in the cluster should be configured with the same ZooKeeper path.
locate()¤
Returns set of URLs based on an id of a service. Provide the filter as a dictionary. The keys of the dictionary can be node_id
, service_id
, instance_id
or custom ids.
Example
To look for all URLs of a service called very-nice-service
, use inside the application:
discover()¤
Returns a dictionary with all known services, organized by their ids and information on how to resolve them.
Custom discovery ids¤
Custom identifiers can be set during runtime.
Example
Inside the application, when Api Service is already initialized:
The argument of the method must be a dictionary, where key is string and value is a list.
The custom_id
can be used in both discovery session and locate()
method.
Using service discovery during authorization¤
When using authorization server e.g. SeaCat Auth to provide authorization for each API call, it can be also found in the cluster through service discovery. In order to connect asab.AuthService
with the service discovery, several requiremetns must be met:
- API Service must be fully initiliazed BEFORE Auth Service.
- Authorization server (SeaCat Auth) must be present in the cluster, resolvable by its instance id, and advertising itself into the ZooKeeper (being server itself).
[auth]
configuration section must contain URL recognizable by the service discovery.
Auth Service using service discovery
API Service must be fully initiliazed BEFORE Auth Service.
class MyApp(asab.Application):
def __init__(self):
super.__init__(self)
# Initialize web server
self.add_module(asab.web.Module)
websvc = self.get_service("asab.WebService")
self.WebContainer = asab.web.WebContainer(websvc, "web")
# Initialize zookeeper
self.add_module(asab.zookeeper.Module)
self.ZooKeeperService = self.get_service("asab.ZooKeeperService")
self.ZooKeeperContainer = asab.zookeeper.ZooKeeperContainer(
self.ZooKeeperService, "zookeeper"
)
# The DiscoverySession is functional only with ApiService initialized.
self.ASABApiService = asab.api.ApiService(self)
self.ASABApiService.initialize_web(self.WebContainer)
self.ASABApiService.initialize_zookeeper(self.ZooKeeperContainer)
self.DiscoveryService = self.get_service("asab.DiscoveryService")
# Initialize authorization after ASABApiService.initialize_zookeeper() to get DiscoveryService into auth module
self.AuthService = asab.web.auth.AuthService(self)
self.AuthService.install(self.WebContainer)
[auth]
configuration section must contain URL recognizable by the service discovery.
Reference¤
asab.api.discovery
¤
DiscoveryResolver
¤
Bases: DefaultResolver
Custom resolver for Discovery Session based on default aiohttp
resolver.
Source code in asab/api/discovery.py
resolve(hostname, port=0, family=socket.AF_INET)
async
¤
Resolve a hostname only with '.asab' domain. and return a list of dictionaries
containing information about the resolved hosts further used by aiohttp.TCPConnector
.
The hostname to resolve must be in the format of "
Source code in asab/api/discovery.py
DiscoveryService
¤
Bases: Service
Service for discovering ASAB microservices in a server cluster. It is based on searching in ZooKeeper /run
path.
Source code in asab/api/discovery.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
|
get_advertised_instances()
async
¤
This method is here for backward compatibility. Use discover()
method instead.
Returns a list of dictionaries. Each dictionary represents an advertised instance
obtained by iterating over the items in the /run
path in ZooKeeper.
Source code in asab/api/discovery.py
locate(instance_id=None, **kwargs)
async
¤
Return a list of URLs for a given instance or service ID.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
instance_id
|
str
|
The ID of a specific instance of a service that the client wants to locate. |
None
|
service_id
|
str
|
The |
required |
Returns: A list of URLs in the format "http://servername:port" for the specified instance or service.
Source code in asab/api/discovery.py
session(base_url=None, auth=None, headers=None, **kwargs)
¤
Open HTTP session with custom hostname resolver for ASAB microservices.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
|
param base_url: Base URL to use for requests. |
required | |
|
param auth: Client request to extract authorization from, or the string "internal", to use internal authorization. |
required | |
|
param headers: Custom session HTTP headers. |
required |
Usage:
```python
Without authorization¤
async with self.DiscoveryService.session() as session:
# use URL in format:
Using end-user authorization (from the UI)¤
async with self.DiscoveryService.session(auth=request) as session: async with session.get("http://my_application_1.instance_id.asab/asab/v1/config") as resp: ...
Using internal m2m authorization¤
async with self.DiscoveryService.session(auth="internal") as session: async with session.get("http://my_application_1.instance_id.asab/asab/v1/config") as resp: ...