Redis: репликация, часть 4 – написание Ansible роли

Автор: | 15/04/2019
 

В продолжение постов о создании Redis репликации и использования Redis Sentinel для его мониторинга.

Предыдущие части:

  1. Redis: репликация, часть 1 — обзор. Replication vs Sharding. Sentinel vs Cluster. Топология Redis

  2. Redis: репликация, часть 2 — Master-Slave репликация, и Redis Sentinel

  3. Redis: репликация, часть 3 — redis-py и работа с Redis Sentinel из Python

Следующая задача – добавить Ansible роль в нашу автоматизацию, которая будет запускать Redis мастер и два слейва, и три Sentinel-инстанса для мониторинга и восстановления репликации в случае выхода из строя Мастера.

Задача немного усложняется тем, что надо оставить в работе текущий Redis, пока бекенд-девелоперы не закончат обновление кода всех проектов на серверах.

Для этого – новые ноды Redis будут использовать порт 6389 вместо стандартного 6379, который останется для старого Redis, плюс напишем отдельные unit-файлы для systemd.

Схема репликации получается стандартная:

Т.е. будет три сервера:

  1. Console: наш “центровой” сервер, на котором запускаются всякие административные задачи. Там же будет Redis Master и первый инстанс Sentinel
  2. App-1 и App-2: два сервера наших приложений, на которых будут работать по одному Redis Slave и по одному Sentinel.

Ansible роль

Создаём каталоги для роли:

[simterm]

$ mkdir roles/redis-cluster/{tasks,templates}

[/simterm]

Добавляем вызов в плейбук:

...
    - role: redis-cluster
      tags: common, app, redis-cluster
      when: "'backend-bastion' not in inventory_hostname"
...

Переменные

Задаём переменные, которые будут использоваться в шаблонах:

...
### ROLES VARS ###

# redis-cluster
redis_cluster_config_home: "/etc/redis-cluster"
redis_cluster_logs_home: "/var/log/redis-cluster"
redis_cluster_data_home: "/var/lib/redis-cluster"
redis_cluster_runtime_home: "/var/run/redis-cluster"
redis_cluster_node_port: 6389
redis_cluster_master_host: "dev.backend-console-internal.example.com"
redis_cluster_name: "redis-{{ env }}-cluster"
redis_cluster_sentinel_port: 26389
...

Tasks

Создаём файл с задачами для роли – roles/redis-cluster/tasks/main.yml.

Создание роли начнём с запуска Redis Master.

Каталоги и файлы должны принадлежать пользователю redis.

Для Redis Master используем условие when: "'backend-console' in inventory_hostname" – имена хостов у нас dev.backend-console-internal.example.com для Console, и dev.backend-app1-internal.example.com и dev.backend-app2-internal.example.com – для Redis слейвов.

Описываем задачи:

- name: "Install Redis"
  apt:
    name: "redis-server"
    state: present

- name: "Create {{ redis_cluster_config_home }}"
  file:
    path: "{{ redis_cluster_config_home }}"
    state: directory
    owner: "redis"
    group: "redis"

- name: "Create {{ redis_cluster_logs_home }}"
  file:
    path: "{{ redis_cluster_logs_home }}"
    state: directory
    owner: "redis"
    group: "redis"

- name: "Create {{ redis_cluster_data_home }}"
  file:                                                                                                                                                                                                 
    path: "{{ redis_cluster_data_home }}"
    state: directory                                                                                                                                                                                    
    owner: "redis"
    group: "redis"
- name: "Copy redis-cluster-master.conf to {{ redis_cluster_config_home }}"
  template:
    src: "templates/redis-cluster-master.conf.j2"
    dest: "{{ redis_cluster_config_home }}/redis-cluster.conf"
    owner: "redis"
    group: "redis"
    mode: 0644
  when: "'backend-console' in inventory_hostname"

- name: "Copy Redis replication cluster systemd unit file"
  template:
    src: "templates/redis-cluster-replica-systemd.j2"
    dest: "/etc/systemd/system/redis-cluster.service"
    owner: "root"
    group:  "root"
    mode: 0644

- name: "Redis relication cluster restart"
  systemd:
    name: "redis-cluster"
    state: restarted
    enabled: yes
    daemon_reload: yes

Шаблоны

Создаём шаблоны файлов.

Начнём с systemd. Т.к. наш Redis-кластер должен работать на нестандартных портах и с отдельными директориями – то дефолтный systemd unit-файл использовать нельзя.

Копируем его, и переписываем под себя.

systemd

Создаём шаблон roles/redis-cluster/templates/redis-cluster-replica-systemd.j2:

[Unit]
Description=Redis relication cluster node
After=network.target
    
[Service]
Type=forking
ExecStart=/usr/bin/redis-server {{ redis_cluster_config_home }}/redis-cluster.conf
PIDFile={{ redis_cluster_runtime_home }}/redis-cluster.pid
TimeoutStopSec=0
Restart=always
User=redis 
Group=redis
RuntimeDirectory=redis-cluster
  
ExecStop=/bin/kill -s TERM $MAINPID
    
UMask=007
PrivateTmp=yes
LimitNOFILE=65535
PrivateDevices=yes
ProtectHome=yes
ReadOnlyDirectories=/
ReadWriteDirectories=-{{ redis_cluster_data_home }}
ReadWriteDirectories=-{{ redis_cluster_logs_home }}
ReadWriteDirectories=-{{ redis_cluster_runtime_home }}
CapabilityBoundingSet=~CAP_SYS_PTRACE

ProtectSystem=true
ReadWriteDirectories=-{{ redis_cluster_config_home }}
    
[Install]
WantedBy=multi-user.target

В параметре ExecStart=/usr/bin/redis-server {{ redis_cluster_config_home }}/redis-cluster-master.conf задаём свой файл настроек Redis.

Redis Master

Создаём шаблон файла настроек Redis Master – roles/redis-cluster/templates/redis-cluster-master.conf.j2:

bind 0.0.0.0
protected-mode yes
port {{ redis_cluster_node_port }}
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize yes 
supervised no
pidfile {{ redis_cluster_runtime_home }}/redis-cluster.pid
loglevel notice
logfile {{ redis_cluster_logs_home }}/redis-cluster.log
databases 16
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir {{ redis_cluster_data_home }}
slave-serve-stale-data yes
slave-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
slave-priority 100
appendonly yes 
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
lua-time-limit 5000
slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events "" 
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0 
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
aof-rewrite-incremental-fsync yes

Позже его надо будет обновить, и переписать параметры под наше окружение, пока тут всё дефолтное, кроме bind и port.

Сейчас деплоим с помощью скрипта ansible_exec.sh.  В будущем задача будет вызываться из Jenkins:

[simterm]

$ ./ansible_exec.sh -t redis-cluster

Tags: redis-cluster
Env: mobilebackend-dev
...

[/simterm]

Проверяем статус Redis Master:

[simterm]

root@bttrm-dev-console:/home/admin# systemctl status redis-cluster.service
● redis-cluster.service - Redis relication cluster node
   Loaded: loaded (/etc/systemd/system/redis-cluster.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2019-04-03 14:05:46 EEST; 9s ago
  Process: 22125 ExecStop=/bin/kill -s TERM $MAINPID (code=exited, status=0/SUCCESS)
  Process: 22131 ExecStart=/usr/bin/redis-server /etc/redis-cluster/redis-cluster-master.conf (code=exited, status=0/SUCCESS)
 Main PID: 22133 (redis-server)
    Tasks: 3 (limit: 4915)
   Memory: 1.1M
      CPU: 14ms
   CGroup: /system.slice/redis-cluster.service
           └─22133 /usr/bin/redis-server 0.0.0.0:6389

Apr 03 14:05:46 bttrm-dev-console systemd[1]: Starting Redis relication cluster node...
Apr 03 14:05:46 bttrm-dev-console systemd[1]: redis-cluster.service: PID file /var/run/redis/redis-cluster.pid not readable (yet?) after start: No such file or directory
Apr 03 14:05:46 bttrm-dev-console systemd[1]: Started Redis relication cluster node.

[/simterm]

OK.

Redis Slaves

Добавляем конфиг для слейвов – roles/redis-cluster/templates/redis-cluster-slave.conf.j2.

Он практически аналогичен файлу настроек мастера, но тут добавляем slaveoff:

slaveof {{ redis_cluster_master_host }} {{ redis_cluster_node_port }}
bind 0.0.0.0
port {{ redis_cluster_node_port }}
pidfile {{ redis_cluster_runtime_home }}/redis-cluster.pid
logfile {{ redis_cluster_logs_home }}/redis-cluster.log
dir {{ redis_cluster_data_home }}
protected-mode yes
tcp-backlog 511
timeout 0 
tcp-keepalive 300
...

Добавляем задачу.

Тут используем условие when: "'backend-console' not in inventory_hostname", что бы файлы задеплоились на хосты App-1 и App-2:

...
- name: "Copy redis-cluster-slave.conf to {{ redis_cluster_config_home }}"
  template: 
    src: "templates/redis-cluster-slave.conf.j2"
    dest: "{{ redis_cluster_config_home }}/redis-cluster.conf"
    owner: "redis"
    group: "redis"
    mode: 0644
  when: "'backend-console' not in inventory_hostname"
...

Деплоим, проверяем:

[simterm]

root@bttrm-dev-app-1:/home/admin# redis-cli -p 6389 -a foobared info replication
# Replication
role:slave
master_host:dev.backend-console-internal.example.com
master_port:6389
master_link_status:down
master_last_io_seconds_ago:-1
...

[/simterm]

Проверяем репликацию.

Добавляем ключ на мастере:

[simterm]

root@bttrm-dev-console:/home/admin# redis-cli -p 6389 -a foobared set test 'test'
OK

[/simterm]

Получаем на слейвах:

[simterm]

root@bttrm-dev-app-1:/home/admin# redis-cli -p 6389 -a foobared get test
"test"
root@bttrm-dev-app-2:/home/admin# redis-cli -p 6389 -a foobared get test
"test"

[/simterm]

Redis Sentinel

Добавляем файл настроек для Sentinel, один для всех – roles/redis-cluster/templates/redis-cluster-sentinel.conf.j2.

Используем sentinel announce-ip, см. Redis: Sentinel – bind 0.0.0.0, проблема с localhost и announce-ip), хотя можно просто биндить на публичный интерфейс:

sentinel monitor {{ redis_cluster_name }} {{ redis_cluster_master_host }} {{ redis_cluster_node_port }} 2
bind 0.0.0.0
port {{ redis_cluster_sentinel_port }}
sentinel announce-ip {{ hostvars[inventory_hostname]['ansible_default_ipv4']['address'] }}
sentinel down-after-milliseconds {{ redis_cluster_name }} 6001
sentinel failover-timeout {{ redis_cluster_name }} 60000
sentinel parallel-syncs {{ redis_cluster_name }} 1
daemonize yes
logfile {{ redis_cluster_logs_home }}/redis-sentinel.log
pidfile {{ redis_cluster_runtime_home }}/redis-sentinel.pid

Добавляем шаблон systemd unit-файла – roles/redis-cluster/templates/redis-cluster-sentinel-systemd.j2:

[Unit]
Description=Redis relication Sentinel instance
After=network.target

[Service]
Type=forking
ExecStart=/usr/bin/redis-server {{ redis_cluster_config_home }}/redis-sentinel.conf --sentinel
PIDFile={{ redis_cluster_runtime_home }}/redis-sentinel.pid
TimeoutStopSec=0
Restart=always
User=redis 
Group=redis
 
ExecStop=/bin/kill -s TERM $MAINPID
    
ProtectSystem=true
ReadWriteDirectories=-{{ redis_cluster_logs_home }}
ReadWriteDirectories=-{{ redis_cluster_config_home }}
ReadWriteDirectories=-{{ redis_cluster_runtime_home }}

[Install]
WantedBy=multi-user.target

Добавляем оставнку инстансов Sentinel в начале файла roles/redis-cluster/tasks/main.yml, в противном случае во время деплоя, если Sentinel уже есть и запущен – он перезапишет изменения, которые мы деплоим из шаблонов:

- name: "Install Redis"
  apt:
    name: "redis-server"
    state: present

- name: "Redis replication Sentinel stop"
  systemd:
    name: "redis-sentinel"
    state: stopped
  ignore_errors: true
...

Добавляем копирование файлов и запуск Sentinel:

...
- name: "Copy redis-cluster-sentinel.conf to {{ redis_cluster_config_home }}"
  template: 
    src: "templates/redis-cluster-sentinel.conf.j2"
    dest: "{{ redis_cluster_config_home }}/redis-sentinel.conf"
    owner: "redis"
    group: "redis"
    mode: 0644
...
- name: "Copy Redis replication Sentinel systemd unit file"
  template:
    src: "templates/redis-cluster-sentinel-systemd.j2"
    dest: "/etc/systemd/system/redis-sentinel.service"
    owner: "root"
    group:  "root"
    mode: 0644
...
- name: "Redis relication Sentinel restart"
  systemd:
    name: "redis-sentinel"
    state: restarted
    enabled: yes
    daemon_reload: yes

Документация говорит, что их надо запускать в паузой в 30 секунд – но работает (пока) и без неё.

В процессе тестирования на Dev/Stage будем посмотреть.

Деплоим, проверяем:

[simterm]

root@bttrm-dev-console:/home/admin# redis-cli -p 26389 info sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=redis-dev-cluster,status=ok,address=127.0.0.1:6389,slaves=2,sentinels=3

[/simterm]

Проверка Sentinel failover

Запускаем тейлинг логов на инстансах:

[simterm]

root@bttrm-dev-app-1:/etc/redis-cluster# tail -f /var/log/redis-cluster/redis-sentinel.log

[/simterm]

На Мастере – проверяем адрес мастера:

[simterm]

root@bttrm-dev-console:/etc/redis-cluster# redis-cli -h 10.0.2.104 -p 26389 sentinel get-master-addr-by-name redis-dev-cluster
1) "127.0.0.1"
2) "6389"

[/simterm]

И статус репликации:

[simterm]

root@bttrm-dev-console:/etc/redis-cluster# redis-cli -h 10.0.2.104 -p 6389 info replication
# Replication
role:master
connected_slaves:2
...

[/simterm]

Роль – Мастер, два слейва – всё гуд.

Останавливаем мастер-ноду Redis:

[simterm]

root@bttrm-dev-console:/etc/redis-cluster# systemctl stop redis-cluster.service

[/simterm]

Логи на App-2:

11976:X 09 Apr 13:12:13.869 # +sdown master redis-dev-cluster 10.0.2.104 6389
11976:X 09 Apr 13:12:13.983 # +new-epoch 1
11976:X 09 Apr 13:12:13.984 # +vote-for-leader 8fd5f2bb50132db0dc528e69089cc2f9d82e01d0 1
11976:X 09 Apr 13:12:14.994 # +odown master redis-dev-cluster 10.0.2.104 6389 #quorum 2/2
11976:X 09 Apr 13:12:14.994 # Next failover delay: I will not start a failover before Tue Apr  9 13:14:14 2019
11976:X 09 Apr 13:12:15.105 # +config-update-from sentinel 8fd5f2bb50132db0dc528e69089cc2f9d82e01d0 10.0.2.71 26389 @ redis-dev-cluster 10.0.2.104 6389
11976:X 09 Apr 13:12:15.105 # +switch-master redis-dev-cluster 10.0.2.104 6389 10.0.2.71 6389
  1. sdown master: Sentinel посчитал, что мастер вышел из строя
  2. odown master quorum 2/2: оба инстанса Sentinel на App-1 и App-2 пришли к общему решению, что таки да -мастер умер
  3. switch-master ... 10.0.2.71 – Sentinel выполнил перенастройку Redis ноды 10.0.2.71 с роли Slave на роль нового Master-а

Всё работает?

Проверяем на 10.0.2.71, это App-1:

[simterm]

root@bttrm-dev-app-1:/etc/redis-cluster# redis-cli -p 6389 info replication
# Replication
role:master
connected_slaves:1
...

[/simterm]

Возвращаем к жизни Redis на Console-хосте:

[simterm]

root@bttrm-dev-console:/etc/redis-cluster# systemctl start redis-cluster.service

[/simterm]

На App-2 смотрим лог:

[simterm]

11976:X 09 Apr 13:17:23.954 # -sdown slave 10.0.2.104:6389 10.0.2.104 6389 @ redis-dev-cluster 10.0.2.71 6389
11976:X 09 Apr 13:17:33.880 * +convert-to-slave slave 10.0.2.104:6389 10.0.2.104 6389 @ redis-dev-cluster 10.0.2.71 6389

[/simterm]

Проверяем статус старого мастера:

[simterm]

root@bttrm-dev-console:/etc/redis-cluster# redis-cli -p 6389 info replication
# Replication
role:slave
master_host:10.0.2.71
master_port:6389
master_link_status:up
...

[/simterm]

Старый Мастер стал Слейвом.

Всё работает.

Готово.