Servicios con persistencia en el orquestador de microservicos Nomad

Escrito por el , actualizado el .
planeta-codigo programacion software
Comentarios

Los servicios que necesitan almacenar datos como las bases de datos o un sistema de archivos tienen más restricciones que los servicios sin estado por la necesidad de tener acceso a esos datos. Esto hace que los contenedores puedan no tener tantan libertad para iniciarse en cualquier nodo. En Nomad una estategia es imponer ciertas restricciones a los servicios que almacenen estado para que solo se puedan iniciar en el nodo que donde estén almacenados.

Nomad
HashiCorp

Algunos servicios no necesitan almacenar ningún estado porque no lo necesitan o porque el estado se mantiene en otro servicio. Que un servicio no necesite mantener estado es bueno porque de esta manera el servicio se puede escalar al número de instancias adecuadas para prestar el servicio, también porque si falla una instancia la petición puede ser reenviada a otra instancia, una instancia que falla puede ser reemplazada sin problema en otro host. Sin embargo, hay otro tipo de instancias que si almacenan estado como las bases de datos ya sea PostgreSQL, MySQL, Redis, MongoDB, otra o simplemente archivos en el sistema de archivos.

Las instancias que tienen estado no son tan fáciles de reemplazar dado que los datos son necesarios para su funcionamiento, una instancia de un servicio con estado como no puede iniciarse en otro nodo libremente solo se puede iniciar en el nodo que contenga los datos. Eso o cuando el servicio se inicia en otro nodo los datos son trasladados o por algún mechanismo transparente a los servicios están disponibles en el nuevo nodo.

En Docker Swarm ciertos drivers de volúmenes pueden proporcionar volúmenes accesibles desde cualquier host del cluster pero por defecto Swarm no lo ofrece. En Kubernetes los volúmenes pueden ser dispositivos de almacenamiento provenientes de EBS de modo que si un pod es movido a otro host basta con que el pod sea conectado de nuevo al EBS anterior y los datos están accesibles en el nuevo nodo.

Nomad no proporciona soporte para que el almacenamiento persistente sea migrado a un nuevo nodo de Nomad si el servicio cambiado de ubicación. Para solventar esta limitación en el caso de los servicios con estado estos pueden ser tratados en cierta forma como animales de compañía o pets haciendo que siempre se ubiquen en el mismo nodo, una vez tiene siempre la misma ubicación basta con proporcionar el almacenamiento en el host ya sea en su sistema de archivos o para externalizarlo montando un almacenamiento EBS.

Para conseguir que un job de Nomad se ubique siempre en un mismo nodo hay que usar restricciones o costraints en la especificación del job. Las restricciones son las reglas que utiliza Nomad para elegir como candidatos los posibles nodos en los que ubicar el job, task group o task. Se pueden utilizar varios operadores entre los que está el de igualdad utilizado en el ejemplo. Una de las variables utilizables es el identificativo del nodo de Nomad, con él es posible conseguir que el job se ubique siempre en el mismo nodo. Los identificativos de los nodos son asignados por Nomad cuando se unen al cluster.

Con los siguientes comandos se inspecciona los nodos que forman parte del cluster de Nomad, entre sus datos está el identificativo de cada nodo formado por una cadena de 36 caracteres. En el modo verboso se emite el identificativo completo y una lista de propiedades del nodo entre los que están detalles de Consul, la CPU, driver que soporta, kernel, sistema operativo, … En documentación sobre interpolación de variables hay una lista de variables disponibles.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 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
$ nomad node status
ID        DC         Name       Class   Drain  Eligibility  Status
44511e01  localhost  archlinux  <none>  false  eligible     ready

$ nomad node status 44511e01
ID            = 44511e01
Name          = archlinux
Class         = <none>
DC            = localhost
Drain         = false
Eligibility   = eligible
Status        = ready
Uptime        = 1h40m31s
Driver Status = docker,exec,java,qemu,raw_exec

Node Events
Time                       Subsystem  Message
2019-04-26T17:57:41+02:00  Cluster    Node registered

Allocated Resources
CPU          Memory      Disk
0/30400 MHz  0 B/31 GiB  0 B/16 GiB

Allocation Resource Utilization
CPU          Memory
0/30400 MHz  0 B/31 GiB

Host Resource Utilization
CPU             Memory          Disk
3288/30400 MHz  5.2 GiB/31 GiB  (tmpfs)

Allocations
No allocations placed

$ nomad node status -verbose 44511e01
ID          = 44511e01-34b8-c1e6-7fe5-60be0ff35d0e
Name        = archlinux
Class       = <none>
DC          = localhost
Drain       = false
Eligibility = eligible
Status      = ready
Uptime      = 1h44m39s

Drivers
Driver    Detected  Healthy  Message                                                                         Time
docker    true      true     Healthy                                                                         2019-04-26T17:57:41+02:00
exec      true      true     Healthy                                                                         2019-04-26T17:57:41+02:00
java      true      true     Healthy                                                                         2019-04-26T17:57:41+02:00
qemu      true      true     Healthy                                                                         2019-04-26T17:57:41+02:00
raw_exec  true      true     Healthy                                                                         2019-04-26T17:57:41+02:00
rkt       false     false    Failed to execute rkt version: exec: "rkt": executable file not found in $PATH  2019-04-26T17:57:41+02:00

Node Events
Time                       Subsystem  Message          Details
2019-04-26T17:57:41+02:00  Cluster    Node registered  <none>

Allocated Resources
CPU            Memory          Disk
100/30400 MHz  1.0 GiB/31 GiB  300 MiB/16 GiB

Allocation Resource Utilization
CPU           Memory
19/30400 MHz  38 MiB/31 GiB

Host Resource Utilization
CPU             Memory          Disk
4564/30400 MHz  5.5 GiB/31 GiB  (tmpfs)

Allocations
ID                                    Eval ID                               Node ID                               Task Group  Version  Desired  Status   Created                    Modified
cd45371d-501a-1373-dfde-bb16c4ff20d3  ab9f5675-b5cb-9e5f-8e20-cb308dbfba32  44511e01-34b8-c1e6-7fe5-60be0ff35d0e  services    3        run      running  2019-04-26T18:14:28+02:00  2019-04-26T18:16:32+02:00

Attributes
consul.datacenter             = localhost
consul.revision               = ea5210a30
consul.server                 = true
consul.version                = 1.4.4
cpu.arch                      = amd64
cpu.frequency                 = 3800
cpu.modelname                 = Intel(R) Core(TM) i5-8259U CPU @ 2.30GHz
cpu.numcores                  = 8
cpu.totalcompute              = 30400
driver.docker                 = 1
driver.docker.bridge_ip       = 172.17.0.1
driver.docker.os_type         = linux
driver.docker.runtimes        = runc
driver.docker.version         = 18.09.4-ce
driver.docker.volumes.enabled = true
driver.exec                   = 1
driver.java                   = 1
driver.java.runtime           = OpenJDK Runtime Environment (build 1.8.0_212-b01)
driver.java.version           = 1.8.0_212
driver.java.vm                = OpenJDK 64-Bit Server VM (build 25.212-b01, mixed mode)
driver.qemu                   = 1
driver.qemu.version           = 3.1.0
driver.raw_exec               = 1
kernel.name                   = linux
kernel.version                = 5.0.7-arch1-1-ARCH
memory.totalbytes             = 33592107008
nomad.advertise.address       = 127.0.0.1:4646
nomad.revision                = 18dd59056ee1d7b2df51256fe900a98460d3d6b9
nomad.version                 = 0.9.0
os.name                       = arch
os.signals                    = SIGQUIT,SIGTTOU,SIGFPE,SIGTRAP,SIGTSTP,SIGINT,SIGURG,SIGTTIN,SIGUSR1,SIGIO,SIGIOT,SIGKILL,SIGSTOP,SIGCONT,SIGILL,SIGPROF,SIGSEGV,SIGSYS,SIGTERM,SIGXFSZ,SIGHUP,SIGWINCH,SIGABRT,SIGBUS,SIGCHLD,SIGPIPE,SIGUSR2,SIGXCPU,SIGALRM
unique.cgroup.mountpoint      = /sys/fs/cgroup
unique.consul.name            = archlinux
unique.hostname               = archlinux
unique.network.ip-address     = 127.0.0.1
unique.storage.bytesfree      = 16795955200
unique.storage.bytestotal     = 16796053504
unique.storage.volume         = tmpfs

Meta

En este caso solo hay un nodo registrado en Nomad, la siguiente definición de job en el fragmento constraint hace que Nomad lo ubique siempre en él.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
job "mongodb" {
  datacenters = ["localhost"]

  type = "service"

  constraint {
    attribute = "${node.unique.id}"
    value     = "44511e01-34b8-c1e6-7fe5-60be0ff35d0e"
  }

  group "services" {
    count = 1

    task "mongodb" {
      driver = "docker"

      config {
        image = "mongo:latest"

        port_map {
          port = 27017
        }
        volumes = [
          "/home/picodotdev/Software/nomad/mongodb:/data/db/"
        ]
      }

      resources {
        memory = 1024 # MB

        network {
          port "port" {}
        }
      }
    }
  }
}

Como el job se ubica en el mismo nodo siempre montando un directorio del nodo como un volumen de datos en el job y contenedor de Docker, los datos se persisten en el sistema de archivos y transcienden al tiempo de vida del job, se puede iniciar el job, insertar datos en la base de datos en este caso de MongoDB, eliminar el job, volverlo a iniciar y los mismos datos están presentes en MongoDB.

1
2
$ nomad job run mongodb.nomad

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                                    NAMES
b0d3f42c92fc        mongo:latest        "docker-entrypoint.s…"   3 minutes ago       Up 3 minutes        127.0.0.1:20180->27017/tcp, 127.0.0.1:20180->27017/udp   mongodb-ea10d440-1176-3bfb-5301-7ccd17af0281
$ docker exec -it b0d3f42c92fc bash
root@b0d3f42c92fc:/# mongo
MongoDB shell version v4.0.9
connecting to: mongodb://127.0.0.1:27017/?gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("ba120679-b965-49d0-a774-dff39d6b630a") }
MongoDB server version: 4.0.9
Server has startup warnings: 
2019-04-26T16:47:49.308+0000 I STORAGE  [initandlisten] 
2019-04-26T16:47:49.308+0000 I STORAGE  [initandlisten] ** WARNING: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine
2019-04-26T16:47:49.308+0000 I STORAGE  [initandlisten] **          See http://dochub.mongodb.org/core/prodnotes-filesystem
2019-04-26T16:47:50.133+0000 I CONTROL  [initandlisten] 
2019-04-26T16:47:50.133+0000 I CONTROL  [initandlisten] ** WARNING: Access control is not enabled for the database.
2019-04-26T16:47:50.133+0000 I CONTROL  [initandlisten] **          Read and write access to data and configuration is unrestricted.
2019-04-26T16:47:50.133+0000 I CONTROL  [initandlisten] 
---
Enable MongoDB's free cloud-based monitoring service, which will then receive and display
metrics about your deployment (disk utilization, CPU, operation statistics, etc).

The monitoring data will be available on a MongoDB website with a unique URL accessible to you
and anyone you share the URL with. MongoDB may use this information to make product
improvements and to suggest MongoDB products and deployment options to you.

To enable free monitoring, run the following command: db.enableFreeMonitoring()
To permanently disable this reminder, run the following command: db.disableFreeMonitoring()
---

> db.articles.insert({title: "Introducción a la base de datos NoSQL MongoDB", author: "picodotdev", date: new Date(2017,05,18,12,30), tags: ['mongodb', 'database', 'NoSQL'], comments: [{user: "jones", message: "MongoDB is great!"}, {user: "lina", message: "MongoDB is great!"}]})
WriteResult({ "nInserted" : 1 })
> db.articles.insert({title: "Introducción a la base de datos relacional PostgreSQL", author: "picodotdev", date: new Date(2017,05,17,12,00), likes: 100, tags: ['postgresql', 'database', 'SQL'], comments: [{user: "katy", message: "PostgreSQL rocks!"}, {user: "smith", message: "SQL language is powerful!"}]})
WriteResult({ "nInserted" : 1 })
> db.articles.find()
{ "_id" : ObjectId("5cc335873b17081f2ca1d4d5"), "title" : "Introducción a la base de datos NoSQL MongoDB", "author" : "picodotdev", "date" : ISODate("2017-06-18T12:30:00Z"), "tags" : [ "mongodb", "database", "NoSQL" ], "comments" : [ { "user" : "jones", "message" : "MongoDB is great!" }, { "user" : "lina", "message" : "MongoDB is great!" } ] }
{ "_id" : ObjectId("5cc335993b17081f2ca1d4d6"), "title" : "Introducción a la base de datos relacional PostgreSQL", "author" : "picodotdev", "date" : ISODate("2017-06-17T12:00:00Z"), "likes" : 100, "tags" : [ "postgresql", "database", "SQL" ], "comments" : [ { "user" : "katy", "message" : "PostgreSQL rocks!" }, { "user" : "smith", "message" : "SQL language is powerful!" } ] }
> db.articles.count()
2
> exit
bye
root@b0d3f42c92fc:/# exit
exit
$ nomad job stop --purge mongodb
==> Monitoring evaluation "f10589c6"
    Evaluation triggered by job "mongodb"
    Evaluation status changed: "pending" -> "complete"
==> Evaluation "f10589c6" finished with status "complete"
1
2
$ nomad job stop --prune mongodb

Para iniciar Consul y Nomad hay que utilizar los siguientes comandos y para el ejecutar job es requisito haber instalado Docker dado que en este ejemplo lo utiliza.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ consul agent -dev -datacenter localhost
==> Starting Consul agent...
==> Consul agent running!
           Version: 'v1.4.4'
           Node ID: '34294bf0-5802-0d94-4acd-cf8c9d090205'
         Node name: 'archlinux'
        Datacenter: 'localhost' (Segment: '<all>')
            Server: true (Bootstrap: false)
       Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600)
      Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302)
           Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false

$ sudo nomad agent -dev -dc localhost
[sudo] password for picodotdev: 
==> No configuration files loaded
==> Starting Nomad agent...
==> Nomad agent configuration:

       Advertise Addrs: HTTP: 127.0.0.1:4646; RPC: 127.0.0.1:4647; Serf: 127.0.0.1:4648
            Bind Addrs: HTTP: 127.0.0.1:4646; RPC: 127.0.0.1:4647; Serf: 127.0.0.1:4648
                Client: true
             Log Level: DEBUG
                Region: global (DC: localhost)
                Server: true
               Version: 0.9.0

Las restricciones se han de cumplir para elegir un nodo, por otro lado está también la afinidad. La afinidad es una preferencia utilizada por Nomad al seleccionar los nodos que tratará de cumplir si hay algún nodo disponible con las propiedades de afinidad deseadas pero si no hay un nodo disponible se elige algún otro.