Skip to content

Running Discovery Service

How to run up the full KXI Discovery Service, including the registry and the API proxy layers

This allows any service in any language to

  • interact with the Discovery Service over an OpenAPI-defined specification
  • register itself and get information about other running processes

Prerequisites

Docker example

Run both the Discovery Registry and the Discovery Proxy.

Create a KX Docker bridge network.

docker network create kx

Run the Discovery Registry.

docker run -it --rm \
    --network=kx \
    --name=registry \
    -e JAVA_OPTS="-Deureka.instance.appname=DOCKER_EX -Deureka.server.responseCacheUpdateIntervalMs=1000" \
    -p 8761:8761 \
    registry.dl.kx.com/kxi-eureka-discovery:1.0.0

This will run the Discovery Registry with this embedded configuration.

Custom configuration

Run the Discovery Proxy.

The proxy’s JSON configuration file specifies the adaptor to load and where to find the registry. Identify the file with an environment variable.

The proxy requires a kdb+ license

echo '{"discovery":{"adaptor":"discEurekaAdaptor.q","registry":"registry:8761"}}' > config.json
# Run
docker run -it --rm \
    --name=discproxy \
    --network=kx \
    --env KXI_CONFIG_FILE=/opt/kx/config/config.json \
    -e "KDB_LICENSE_B64=$KDB_LICENSE_B64" \
    -p 5000:5000 \
    -v $(pwd):/opt/kx/config \
    registry.dl.kx.com/kxi-discovery-proxy:0.8.2 -p 5000

This builds and runs a local Eureka registry, accessible at http://localhost:8761.

The Discovery Proxy is connected to the Registry and is available at:

  • http://localhost:5000 (REST)
  • `:localhost:5000 (q IPC)

Docker Compose example

Docker Compose allows for a structured approach to the Discovery Service runtime

As in the Docker example, the proxy requires the same configuration file config.json as initialized there, and a kdb+ Cloud Edition license provided as a base64 string.

docker-compose.yml

version: "3.7"
# All our processes will run on a dedicated network 'kx'
networks:
  kx:
    name: kx
    driver: bridge
# Here we define the services we wish to run
services:
  registry:
    image: registry.dl.kx.com/kxi-eureka-discovery:1.0.0
    environment:
       - JAVA_OPTS=-Deureka.instance.appname=DOCKER_COMPOSE_EX -Deureka.server.responseCacheUpdateIntervalMs=1000
    # Expose port 8761 locally
    ports:
      - 8761:8761
    networks:
      - kx

  proxy:
    image: registry.dl.kx.com/kxi-discovery-proxy:0.8.2
    ports:
      - 5000:5000
    environment:
      - KXI_CONFIG_FILE=/opt/kx/config/config.json
      - KDB_LICENSE_B64=$KDB_LICENSE_B64
    volumes:
      - ./:/opt/kx/config
    command: -p 5000
    networks:
      - kx
docker-compose -f docker-compose.yml up

Like the Docker example this runs the Discovery Registry with the -embedded configuration.

Custom configuration

Multi-node example

For a fully distributed fault-tolerant service, run the Discovery Service in multi-node mode

This requires more configuration for each of the components and the docker-compose file.

Set up a custom Discovery Registry configuration.

application_3nodeRegistry.yaml

spring:
  profiles: default
  application:
    name: KXI-DISCOVERY

server:
  port: 8761
  tomcat:
    accesslog:
      enabled: true
      suffix: .log
      prefix: access_${POD_HOST}
      directory: /data/logs

eureka:
  server:
    enableSelfPreservation: false
    evictionIntervalTimerInMs: 5000
    renewalPercentThreshold: 0.850000
    responseCacheUpdateIntervalMs: 1000
  instance:
    hostname: ${POD_HOST}
    preferIpAddress: false
    leaseRenewalIntervalInSeconds: 30
    leaseExpirationDurationInSeconds: 90
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
      defaultZone: http://registry-0:8761/eureka/,http://registry-1:8761/eureka/,http://registry-2:8761/eureka/

Set up a Discovery Proxy configuration file for each node.

echo '{"discovery":{"adaptor":"discEurekaAdaptor.q","registry":"registry-0:8761"}}' > proxyConfig-0.json
export KXI_CONFIG_FILE=$(pwd)/proxyConfig-0.json
echo '{"discovery":{"adaptor":"discEurekaAdaptor.q","registry":"registry-1:8761"}}' > proxyConfig-1.json
export KXI_CONFIG_FILE=$(pwd)/proxyConfig-1.json
echo '{"discovery":{"adaptor":"discEurekaAdaptor.q","registry":"registry-2:8761"}}' > proxyConfig-2.json
export KXI_CONFIG_FILE=$(pwd)/proxyConfig-2.json

Set up a docker-compose3.yml

version: "3.7"
# Volume to load config file
volumes:
  config:
# All processes will run on a dedicated network 'kx'
networks:
  kx:
    name: kx
    driver: bridge
# Here we define the services we wish to run
services:
  registry-0:
    image: registry.dl.kx.com/kxi-eureka-discovery:1.0.0
    # Expose port 8761 (default reigstry port)
    ports:
      - 9000:8761
    environment:
      # - JAVA_OPTS=-Dlogging.level.org.springframework=DEBUG
      - CUSTOM_CONFIG=/data/config/application_3nodeRegistry.yaml
      - POD_HOST=registry-0
    networks:
      - kx
    volumes:
      - ./:/data/config
      - ./logs:/data/logs

  proxy-0:
    image: registry.dl.kx.com/kxi-discovery-proxy:0.8.2
    ports:
      - 5000:5000
    environment:
      - KXI_CONFIG_FILE=/opt/kx/config/proxyConfig-0.json
      - KDB_LICENSE_B64=$KDB_LICENSE_B64
    volumes:
      - ./:/opt/kx/config
    command: -p 5000
    networks:
      - kx

  registry-1:
    image: registry.dl.kx.com/kxi-eureka-discovery:1.0.0
    # Expose port 8761 (default reigstry port)
    ports:
      - 9001:8761
    environment:
      #- JAVA_OPTS=-Dlogging.level.org.springframework=DEBUG
      - CUSTOM_CONFIG=/data/config/application_3nodeRegistry.yaml
      - POD_HOST=registry-1
    networks:
      - kx
    volumes:
      - ./:/data/config
      - ./logs:/data/logs

  proxy-1:
    image: registry.dl.kx.com/kxi-discovery-proxy:0.8.2
    ports:
      - 5001:5000
    environment:
      - KXI_CONFIG_FILE=/opt/kx/config/proxyConfig-1.json
      - KDB_LICENSE_B64=$KDB_LICENSE_B64
    volumes:
      - ./:/opt/kx/config
    command: -p 5000
    networks:
      - kx

  registry-2:
    image: registry.dl.kx.com/kxi-eureka-discovery:1.0.0
    # Expose port 8761 (default reigstry port)
    ports:
      - 9002:8761
    environment:
      #- JAVA_OPTS=-Dlogging.level.org.springframework=DEBUG
      - CUSTOM_CONFIG=/data/config/application_3nodeRegistry.yaml
      - POD_HOST=registry-2
    networks:
     - kx
    volumes:
      - ./:/data/config
      - ./logs:/data/logs

  proxy-2:
    image: registry.dl.kx.com/kxi-discovery-proxy:0.8.2
    ports:
      - 5002:5000
    environment:
      - KXI_CONFIG_FILE=/opt/kx/config/proxyConfig-2.json
      - KDB_LICENSE_B64=$KDB_LICENSE_B64
    volumes:
      - ./:/opt/kx/config
    command: -p 5000
    networks:
      - kx

Run the Discovery service.

docker-compose -f docker-compose3.yml up

Kubernetes

A Helm chart enables use of the Discovery Service within a Kubernetes-orchestrated system

Pre-requisites

Create a KX license secret in the Kubernetes cluster: initialize a secret within our kdb+ cluster to store the kdb+ license, which can be used by all our services.

# Retrieve B64 representation of license
$ base64 -w 0 $QLIC/kc.lic
<b64String>

Create a Kubernetes secret resource.

$ cat kx-license-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: kx-license-info
type: Opaque
stringData:
  license: <b64String>

Apply the secret to the cluster.

$ kubectl apply -f  kx-license-secret.yaml
secret/license-info configured

Create a simple configuration: install-config.yml.

global:
    image:
        repository: registry.dl.kx.com
    imagePullSecrets:
        - name: kx-repo-access

    license:
        secretName: kx-license-info

Install the Helm chart.

# Assumes the helm repo has been set up to `kxi-repo`
helm install kxi-discovery-service kxi-repo/kxi-discovery-service \
    -f install-config.yml

Example clients

The Discovery Service requires services to

  • register with it, providing information on what and who they are, including metadata necessary within the application
  • heartbeat on a timer to remain active with the service

Simple client code (kdb+ and kdb+ Cloud Edition) below demonstrates the use of each API. Each client uses the register, service, update, status and deregister APIs.

kdb+

\c 20 200
discProxyH:@[hopen;`::5000;{[e]0N!"Could not connect to proxy: ",e;exit[1];}]
uid:"ex_svc_12ab"
service:"ex_svc"
host:"hostA"
port:5050
ip:"0.0.0.0"

// register
args:`uid`service`hostname`port`ip`status`metadata!
  (uid;service; host; port;ip;"UP"; enlist[`connectivity]!enlist `aeron)

registerResp:registerResp:discProxyH(`.sd.register; args)
if[200 <> first registerResp; ' last registerResp]

// get services
getServiceStatus:{[h;p_uid]
  getServicesResp:h(`.sd.getServices; ()!());
  if[200 <> first getServicesResp; ' last getServicesResp];
  -1 .Q.s resp:select from last getServicesResp where uid like p_uid;
  resp }

while[1<>count getServiceStatus[discProxyH;uid];system"sleep 1"]

// heartbeat - check the proxy
.z.ts:{[]
  args:`uid`service`hostname!(uid;service;host);
  heartbeatResp:discProxyH(`.sd.heartbeat;args); }

// update status
args:`uid`service`hostname`port`ip`status`metadata!
  (uid;service; host; port; ip; "DOWN"; enlist[`connectivity]!enlist`aeron)

updateResp:discProxyH(`.sd.updateStatus; args)
getServiceStatus[discProxyH;uid];

// update details
args:`uid`service`hostname`port`ip`status`metadata!
  (uid;service; host; port; ip; "UP"; `connectivity`data!`aeron`quotes)

updateDetailsResp:discProxyH(`.sd.updateDetails;args)
getServiceStatus[discProxyH;uid];

// deregister
\t 0
args:`uid`service`hostname!(uid;service;host)
deregisterResp:discProxyH(`.sd.deregister; args)
while[1=count getServiceStatus[discProxyH;uid];system"sleep 1"]
exit 0

kdb+ Cloud Edition

\c 20 200
uid:"ex_svc_12ab"
service:"ex_svc"
host:"hostA"
port:5050
ip:"0.0.0.0"

// add use of QLog
id:.com_kx_log.init[`:fd://stdout; ()];
.qlog:.com_kx_log.new[`qlog; ()];

// register
opts:()!();
headers:("Accept";"Content-Type")!2#enlist "application/json"
body:.j.j `uid`service`hostname`port`ip`status`metadata!
  (uid;service; host; port; ip; "UP"; enlist[`connectivity]!enlist `aeron)
opts:``headers`body!(::;headers;body)
resp:.kurl.sync("http://localhost:5000/register";`POST;opts)
if[200 <> first resp; ' last resp]
.qlog.info last resp;

// get services
getServiceStatus:{[p_uid]
  resp:.kurl.sync(
    "http://localhost:5000/services";
    `GET;
    ``headers!(::;("Accept";"Content-Type")!2#enlist "application/json") 
  );
  if[200 <> first resp; ' last resp];
  .qlog.info lr:last resp;
  select from .j.k[lr]where uid like p_uid }

while[0=count getServiceStatus[uid];system"sleep 1"]
// heartbeat
.z.ts:{[]
  body:.j.j`uid`service`hostname!(uid; service; host);
  opts:`headers`body!(headers;body);
  .kurl.sync("http://localhost:5000/heartbeat";`POST;opts); }
\t 2000

// update details
body:.j.j`uid`service`hostname`port`ip`status`metadata!
  (uid;service; host; port; ip; "UP"; `connectivity`data!`aeron`quotes)
opts:`headers`body!(headers;body)
resp:.kurl.sync("http://localhost:5000/update";`POST;opts)
if[200 <> first resp; ' last resp]

// update status
body:.j.j`uid`service`hostname`status!(uid; service; host; "DOWN")
opts:`headers`body!(headers;body)
resp:.kurl.sync("http://localhost:5000/status";`POST;opts)
if[200 <> first resp; ' last resp]

// deregister
\t 0
body:.j.j`uid`service`hostname!(uid; service; host)
opts:`headers`body!(headers;body)
resp:.kurl.sync("http://localhost:5000/deregister";`POST;opts)
if[200 <> first resp; ' last resp]
while[1=count getServiceStatus[uid];system"sleep 1"]

exit 0