В продолжение постов о создании 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 роль
Создаём каталоги для роли:
Добавляем вызов в плейбук:
... - 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:
Проверяем статус Redis Master:
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" ...
Деплоим, проверяем:
Проверяем репликацию.
Добавляем ключ на мастере:
Получаем на слейвах:
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 будем посмотреть.
Деплоим, проверяем:
Проверка Sentinel failover
Запускаем тейлинг логов на инстансах:
На Мастере — проверяем адрес мастера:
И статус репликации:
Роль — Мастер, два слейва — всё гуд.
Останавливаем мастер-ноду Redis:
Логи на 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:
Возвращаем к жизни Redis на Console-хосте:
На App-2 смотрим лог:
Проверяем статус старого мастера:
Старый Мастер стал Слейвом.
Всё работает.
Готово.