Dieser Artikel wurde im Februar 2020 geschrieben
Da Elasticsearch und der ELK-Stack sowie Service Discovery jetzt mehrfach meinen Weg gekreuzt haben, wollte ich mir das Thema nochmal genauer ansehen. Und da die PlusCloud noch nicht soweit ist, habe ich mir ein Beispiel aus dem Triton-Umfeld ausgesucht.
Was ist das Problem?
- Generell: Wie kann man (dynamisch) Informationen zwischen Diensten übermitteln, damit diese verknüpft werden können?
- Am Beispiel ELK: Wie kann Kibana davon erfahren, welche IP-Adresse der Elastisearch-Server hat? Wie kann Logstash davon erfahren? Wie können die IP-Adressen dynamisch angepasst werden, wenn sich die Adresse des Elasticsearch-Servers ändert?
Dasselbe Problem stellt sich natürlich auch bei anderen Applikationen, die aus mehreren Komponenten bestehen, welche z. B. dynamisch skalieren oder bei einem Re-Deployment ggfs. automatisch eine neue IP-Adresse erhalten.
Für Triton habe ich ein Beispiel gefunden, in dem Consul für das Service Discovery verwendet wird:
Das Setup ist dabei unabhängig von Triton und kann überall per Docker ausgerollt werden. Dabei wird folgende docker-compose.yml
verwendet: docker-compose.yml
Nach dem Deployment mit docker-compose sieht der Stack zunächst so aus:
root@e3b75fc5-3621-cdd8-f9dd-c9acbf4a37e9:~/autopilotpattern/elk# triton-compose -p elk ps
Name Command State Ports
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
elk_consul_1 /bin/start -server -bootst ... Up 53/tcp, 53/udp, 8300/tcp, 8301/tcp, 8301/udp, 8302/tcp, 8302/udp, 8400/tcp, 0.0.0.0:8500->8500/tcp
elk_elasticsearch_1 /usr/local/bin/containerpi ... Up 9200/tcp, 9300/tcp
elk_elasticsearch_data_1 /usr/local/bin/containerpi ... Up 0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp
elk_elasticsearch_master_1 /usr/local/bin/containerpi ... Up 0.0.0.0:9200->9200/tcp, 0.0.0.0:9300->9300/tcp
elk_kibana_1 /usr/local/bin/containerpi ... Up 0.0.0.0:5601->5601/tcp
elk_logstash_1 /usr/local/bin/containerpi ... Up 0.0.0.0:12201->12201/tcp, 0.0.0.0:12201->12201/udp, 24224/tcp, 0.0.0.0:514->514/tcp, 0.0.0.0:514->514/udp
Bei Consul haben sich folgende Dienste registriert:
Elasticsearch müsste sich so sehen:
root@e3b75fc5-3621-cdd8-f9dd-c9acbf4a37e9:~/autopilotpattern/elk# curl -s http://10.65.69.139:9200/_cluster/health | jq
{
"cluster_name": "elasticsearch",
"status": "green",
"timed_out": false,
"number_of_nodes": 3,
"number_of_data_nodes": 2,
"active_primary_shards": 5,
"active_shards": 10,
"relocating_shards": 0,
"initializing_shards": 0,
"unassigned_shards": 0,
"delayed_unassigned_shards": 0,
"number_of_pending_tasks": 0,
"number_of_in_flight_fetch": 0,
"task_max_waiting_in_queue_millis": 0,
"active_shards_percent_as_number": 100
}
Das sollte sich ändern, wenn wir Nodes nachschieben:
root@e3b75fc5-3621-cdd8-f9dd-c9acbf4a37e9:~/autopilotpattern/elk# triton-compose -p elk scale elasticsearch_master=3
Creating and starting elk_elasticsearch_master_2 ... done
Creating and starting elk_elasticsearch_master_3 ... done
root@e3b75fc5-3621-cdd8-f9dd-c9acbf4a37e9:~/autopilotpattern/elk# triton-compose -p elk scale elasticsearch_data=3
Creating and starting elk_elasticsearch_data_2 ... done
Creating and starting elk_elasticsearch_data_3 ... done
Danach sieht Elasticsearch sich so:
root@e3b75fc5-3621-cdd8-f9dd-c9acbf4a37e9:~/autopilotpattern/elk# curl -s http://10.65.69.139:9200/_cluster/health | jq
{
"cluster_name": "elasticsearch",
"status": "green",
"timed_out": false,
"number_of_nodes": 7,
"number_of_data_nodes": 4,
"active_primary_shards": 5,
"active_shards": 10,
"relocating_shards": 0,
"initializing_shards": 0,
"unassigned_shards": 0,
"delayed_unassigned_shards": 0,
"number_of_pending_tasks": 0,
"number_of_in_flight_fetch": 0,
"task_max_waiting_in_queue_millis": 0,
"active_shards_percent_as_number": 100
}
Dann brauchen wir noch eine Quelle, die über Logstash Logdaten ins Elasticsearch spült:
root@e3b75fc5-3621-cdd8-f9dd-c9acbf4a37e9:~/autopilotpattern/elk# triton-docker run -d -m 128m -p 80 --name elk_nginx_syslog_1 --label "triton.cns.services=nginx-syslog" --restart always --log-driver syslog --log-opt syslog-address=tcp://10.65.69.142 --link elk_consul_1:consul -e CONSUL=consul -e BACKEND=kibana autopilotpattern/elk-nginx-demo:latest
8c562e733851ca6cc6b6b21317f9feba6b1fd48e5a9aea99b1ada1ab1a65e8c0
Und im Kibana kann man dann auch Logs sehen:
Damit ist das geplante Setup aufgebaut. Wie funktioniert es?
Consul
Consul ist ein key/value-Store ähnlich etcd, der von Hashicorp inzwischen mit weiteren Funktionen (z. B. Connect) ausgestattet worden ist. Im Rahmen des Aufkommens von Service-Meshes wird er inzwischen als "service networking solution" beschrieben, was eigentlich sehr gut paßt. Die Architektur kann man dem folgenden Diagramm entnehmen:
Für den Produktionseinsatz sollte Consul als Cluster (aus mindestens drei Nodes) sinnvollerweise in verschiedenen availability zones (AZ) aufgebaut werden. In unserem Setup, wird die Registrierung eines Dienstes in Consul durch ein kleines Hilfsprogramm (heute würde man vermutlich Sidecar sagen) durchgeführt, welches wir in jedem Container als erstes Starten (und welches dann die eigentliche Applikation im Container startet): Containerpilot. Die Konfigurationsdatei für Containerpilot (z. B. im Fall der Elasticsearch-Container) sieht so aus:
{
"consul": "{{ .CONSUL }}:8500",
"preStart": "/usr/local/bin/manage.sh preStart",
"services": [
{
"name": "{{ .ES_SERVICE_NAME }}",
"port": 9300,
"health": "/usr/local/bin/manage.sh health",
"poll": 10,
"ttl": 25
}
]
}
Wenn Containerpilot mit dieser Konfiguration startet, registriert er den Container mit dem Namen aus der docker-compose Datei und der ID des Containers in Consul:
root@e3b75fc5-3621-cdd8-f9dd-c9acbf4a37e9:~/autopilotpattern/elasticsearch# curl -Ls --fail http://10.65.69.146:8500/v1/catalog/service/elasticsearch-master | jq '.'
[
{
"Node": "d23389673e46",
"Address": "10.65.69.146",
"ServiceID": "elasticsearch-master-7aa7624c823e",
"ServiceName": "elasticsearch-master",
"ServiceTags": null,
"ServiceAddress": "10.65.69.140",
"ServicePort": 9300
},
{
"Node": "d23389673e46",
"Address": "10.65.69.146",
"ServiceID": "elasticsearch-master-a8442af13984",
"ServiceName": "elasticsearch-master",
"ServiceTags": null,
"ServiceAddress": "10.65.69.139",
"ServicePort": 9300
},
{
"Node": "d23389673e46",
"Address": "10.65.69.146",
"ServiceID": "elasticsearch-master-e004d9a4aece",
"ServiceName": "elasticsearch-master",
"ServiceTags": null,
"ServiceAddress": "10.65.69.144",
"ServicePort": 9300
},
{
"Node": "d23389673e46",
"Address": "10.65.69.146",
"ServiceID": "elasticsearch-master-d37b507e7b0e",
"ServiceName": "elasticsearch-master",
"ServiceTags": null,
"ServiceAddress": "10.65.69.156",
"ServicePort": 9300
}
]
Für Applikationen wie Kibana, die in ihrer Konfigurationsdatei die IP-Adresse eines Eleasticsearch-Masters benötigen, wird in der Containerpilot-Konfiguration ein "backend" definiert (in diesem Fall Elasticsearch):
{
"consul": "{{ .CONSUL }}:8500",
"onStart": ["/usr/local/bin/manage.sh", "onStart"],
"services": [
{
"name": "kibana",
"port": 5601,
"health": ["/usr/local/bin/manage.sh", "health"],
"poll": 10,
"ttl": 25
}
],
"backends": [
{
"name": "elasticsearch",
"poll": 5,
"onChange": ["/usr/local/bin/manage.sh", "reload"]
}
]
}
In der zugehörigen manage.sh
wird bei einer Änderung im Backend, die geänderte IP-Adresse des Elasticsearch-Masters eingetragen: manage.sh
Consul-Template
Um längere und komplexere Konfigurationsdateien mit Werten aus Consul auszufüllen, gib es Consul-Template. Dabei liest Consul-Template beim Start eine Template-Datei oder mehrere Template-Dateien ein und fragt bei Consul die Werte an, um sie auszufüllen. Normalerweise läßt man Consul-Template als Daemon laufen, der beim Start die initialen Werte einsetzt und danach auf Updates in Consul wartet, um damit ggfs. die Templates erneut auszufüllen, wenn sie eine Änderung ergeben hat. Zusätzlich kann Consul-Template nach der Änderung weitere Programme aufrufen, um z. B. einen Prozess dazu zu bringen, seine Konfigurationsdatei(en) neu zu laden.
Im vorliegenden Setup wird Consul-Template jedoch anders verwendet. Die Konfigurationsdatei von Containerpilot sieht im nginx-Container so aus:
{
"consul": "{{ .CONSUL }}:8500",
"preStart": "/usr/local/bin/reload.sh preStart",
"services": [
{
"name": "nginx",
"port": 80,
"publicIp": true,
"health": "/usr/bin/curl --fail -s http://localhost/health",
"poll": 10,
"ttl": 25
}
],
"backends": [
{
"name": "{{ .BACKEND }}",
"poll": 7,
"onChange": "/usr/local/bin/reload.sh"
}
],
"telemetry": {
"port": 9090,
"sensors": [
{
"name": "tb_nginx_connections_unhandled_total",
"help": "Number of accepted connnections that were not handled",
"type": "gauge",
"poll": 5,
"check": ["/usr/local/bin/sensor.sh", "unhandled"]
},
{
"name": "tb_nginx_connections_load",
"help": "Ratio of active connections (less waiting) to the maximum worker connections",
"type": "gauge",
"poll": 5,
"check": ["/usr/local/bin/sensor.sh", "connections_load"]
}
]
}
}
Consul-Template wird in der reload.sh
aufgerufen:
#!/bin/bash
SERVICE_NAME=${SERVICE_NAME:-nginx}
CONSUL=${CONSUL:-consul}
# Render Nginx configuration template using values from Consul,
# but do not reload because Nginx has't started yet
preStart() {
consul-template \
-once \
-dedup \
-consul ${CONSUL}:8500 \
-template "/etc/nginx/nginx.conf.ctmpl:/etc/nginx/nginx.conf"
}
# Render Nginx configuration template using values from Consul,
# then gracefully reload Nginx
onChange() {
consul-template \
-once \
-dedup \
-consul ${CONSUL}:8500 \
-template "/etc/nginx/nginx.conf.ctmpl:/etc/nginx/nginx.conf:nginx -s reload"
}
help() {
echo "Usage: ./reload.sh preStart => first-run configuration for Nginx"
echo " ./reload.sh onChange => [default] update Nginx config on upstream changes"
}
until
cmd=$1
if [ -z "$cmd" ]; then
onChange
fi
shift 1
$cmd "$@"
[ "$?" -ne 127 ]
do
onChange
exit
done
und erzeugt dabei entweder nur eine nginx.conf
oder erzeugt eine nginx.conf
und lässt sie vom nginx neu einlesen, sofern dieser vorher schon gelaufen ist und es Änderungen gegeben hat. Das Beispiel zeigt auch noch eine weitere Funktion von Containerpilot: Telemetry. In diesem Fall werden sehr einfach mit Hilfe der sensor.sh:
#!/bin/bash
set -e
help() {
echo 'Make requests to the Nginx stub_status endpoint and pull out metrics'
echo 'for the telemetry service. Refer to the Nginx docs for details:'
echo 'http://nginx.org/en/docs/http/ngx_http_stub_status_module.html'
}
# Cummulative number of dropped connections
unhandled() {
local accepts=$(curl -s --fail localhost/health | awk 'FNR == 3 {print $1}')
local handled=$(curl -s --fail localhost/health | awk 'FNR == 3 {print $2}')
echo $(expr ${accepts} - ${handled})
}
# ratio of connections-in-use to available workers
connections_load() {
local scraped=$(curl -s --fail localhost/health)
local active=$(echo ${scraped} | awk '/Active connections/{print $3}')
local waiting=$(echo ${scraped} | awk '/Reading/{print $6}')
local workers=$(echo $(cat /etc/nginx/nginx.conf | perl -n -e'/worker_connections *(\d+)/ && print $1')
)
echo $(echo "scale=4; (${active} - ${waiting}) / ${workers}" | bc)
}
# -------------------------------------------------------
# Un-scraped metrics; these raw metrics are available but we're not going
# to include them in the telemetry configuration. They have been left here
# as an example.
# The current number of active client connections including Waiting connections.
connections_active() {
curl -s localhost/health | awk '/Active connections/{print $3}'
}
# The current number of connections where nginx is reading the request header.
connections_reading() {
curl -s localhost/health | awk '/Reading/{print $2}'
}
# The current number of connections where nginx is writing the response back
# to the client.
connections_writing() {
curl -s localhost/health | awk '/Reading/{print $4}'
}
# The current number of idle client connections waiting for a request.
connections_waiting() {
curl -s localhost/health | awk '/Reading/{print $6}'
}
# The total number of accepted client connections.
accepts() {
curl -s localhost/health | awk 'FNR == 3 {print $1}'
}
# The total number of handled connections. Generally, the parameter value is the
# same as accepts unless some resource limits have been reached (for example, the
# worker_connections limit).
handled() {
curl -s localhost/health | awk 'FNR == 3 {print $2}'
}
# The total number of client requests.
requests() {
curl -s localhost/health | awk 'FNR == 3 {print $3}'
}
# -------------------------------------------------------
cmd=$1
if [ ! -z "$cmd" ]; then
shift 1
$cmd "$@"
exit
fi
help
Metriken generiert, die sofort von einer Prometheus-Instanz konsumiert werden könnten. Neben Consul-Template gibt es noch eine ganze Reihe weiterer Tools und Applikationen, die bei der Benutzung von Consul und seiner Nutzung mit anderen Applikationen helfen. Sogar ein Loadbalancer wie fabio.
Triton-Footprint
Zum Schluß noch kurz einen Blick auf den Footprint, den das Setup in einer Triton-Umgebung hat. Wenn das Setup per Docker-API ausgerollt wird, werden keine VMs sondern sogenannte "lx-branded" Zones (SmartOS-Zonen, in denen Linux-Binaries per Syscall-Mapping laufen) gestartet. Die 12 Zonen belegen dann etwa 3,2 GB Memory:
[root@hh24-gts2-de30 (de-gt-2) ~]# zonememstat -a
ZONE ALIAS RSS(MB) CAP(MB) NOVER POUT(MB) SWAP%
global - 1065 16777215 0 0 -
d2338967-3e46-423a-df63-f76831c43d0e elk_consul_1 14 128 0 0 0.94
bbd44e1e-b4fa-ef55-f756-a738c99f3dba elk_logstash_1 242 1024 0 0 5.28
d8d0a123-a2d5-cc87-a5d4-ef2b350b29a8 elk_kibana_1 265 1024 0 0 6.04
a8442af1-3984-c35a-9748-f155929216db elk_elasticsearch_1 416 4096 0 0 2.37
7aa7624c-823e-4a13-8340-e70997731893 elk_elasticsearch_master_1 375 4096 0 0 2.12
ec913608-e2b8-4b51-8a79-c7df42680160 elk_elasticsearch_data_1 447 4096 0 0 2.55
5eb09ae6-7362-ef3f-b5bc-be5eb7126a92 elk_elasticsearch_data_3 372 4096 0 0 2.10
b40414bc-f26b-45a3-a5dd-e6f1d99c4086 elk_elasticsearch_data_2 447 4096 0 0 2.57
e004d9a4-aece-cc24-cdea-aa436657e045 elk_elasticsearch_master_3 287 4096 0 0 1.60
d37b507e-7b0e-67ab-e579-9dcc6608c418 elk_elasticsearch_master_2 287 4096 0 0 1.59
8c562e73-3851-ca6c-c6b6-b21317f9feba elk_nginx_syslog_1 21 128 0 0 2.69
d6c48b73-7575-48a4-ff6c-f6cf0752de2f elk_nginx_syslog_2 18 128 0 0 2.31
Links: