В продолжение постов о создании 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]
Старый Мастер стал Слейвом.
Всё работает.
Готово.