В продолжение постов о создании Redis репликации и использования Redis Sentinel для его мониторинга.
Предыдущие части:
-
Redis: репликация, часть 1 — обзор. Replication vs Sharding. Sentinel vs Cluster. Топология Redis
-
Redis: репликация, часть 2 — Master-Slave репликация, и Redis Sentinel
-
Redis: репликация, часть 3 — redis-py и работа с Redis Sentinel из Python
Следующая задача — добавить Ansible роль в нашу автоматизацию, которая будет запускать Redis мастер и два слейва, и три Sentinel-инстанса для мониторинга и восстановления репликации в случае выхода из строя Мастера.
Задача немного усложняется тем, что надо оставить в работе текущий Redis, пока бекенд-девелоперы не закончат обновление кода всех проектов на серверах.
Для этого — новые ноды Redis будут использовать порт 6389 вместо стандартного 6379, который останется для старого Redis, плюс напишем отдельные unit-файлы для systemd.
Схема репликации получается стандартная:
Т.е. будет три сервера:
- Console: наш «центровой» сервер, на котором запускаются всякие административные задачи. Там же будет Redis Master и первый инстанс Sentinel
- 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
sdown master: Sentinel посчитал, что мастер вышел из строяodown master quorum 2/2: оба инстанса Sentinel на App-1 и App-2 пришли к общему решению, что таки да -мастер умер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]
Старый Мастер стал Слейвом.
Всё работает.
Готово.





