Привет! Меня зовут Сергей Тетерюков, и я работаю инженером инфраструктуры и автоматизации в X5 Tech. Недавно я написал для коллег обзорную статью о БД Apache Cassandra DB и её деплое, и теперь хочу поделиться ей с вами.

Краткое и поверхностное описание Cassandra DB
Apache Cassandra — высокопроизводительная, отказоустойчивая распределённая не реляционная (NoSQL) БД, позволяющая создать надёжное масштабируемое хранилище данных.
Cassandra использует язык CQL, синтаксис которого аналогичен SQL.
Компоненты cassandra db:
- Нода(Node) или узел – основной элемент хранилища cassandra db. 
- 
Дата-центр (Data-center) – состоит из одной или нескольких нод, связанных между собой. Может быть физическим или виртуальным. Деление на дата-центры позволяет распределять рабочую нагрузку, обеспечивает те самые reliability и fault tolerance. В каждый дата-центр обычно входят несколько нод, как минимум одна из которых выполняет роль раздающей (Seed); рекомендуется делать более одной seed-ноды на один дата-центр. 
- Кластер (Cluster) – объединение дата-центров. 
Для более детального изучения архитектуры и того, как работает Cassandra рекомендую ознакомиться с основными разделами из официальной документации:
Рассмотрим практическую продуктовую задачу
- Задеплоить кластер Apache Cassandra DB в нескольких окружениях (например, dev, stage, и prod). 
- Установить компоненты, необходимые для мониторинга Cassandra DB (экспортеры для сбора метрик состояния и ресурсов серверов, на которых развернуты ноды БД кассандры, и непосредственно метрик самой БД). 
Варианты реализации деплоя
- 
В докер-контейнерах. 
 Плюсы:
 — Есть готовые докер-образы, как для Cassandra DB, так и для node-exporter, cassandra-exporter => минимум настроек.Минусы (выявленные методом проб и ошибок на личном опыте): 
 — Требуется подготовка хостов — установка и настройка docker и сопутствующих пакетов.
 — Для более гибкой настройки потребуется модификация образов.
 — Для успешного объединения отдельных нод Cassandra DB в кластер могут потребоваться дополнительные танцы с бубном: так, например, у меня при одновременном запуске контейнеров ноды как минимум одна из нод не могла корректно получить токены, и не проходила проверку для получения статуса UN (Up, Normal). Пришлось настроить ожидание с запуском каждого следующего контейнера только после завершения запуска и инициализации предыдущего, начиная с seed-ноды.
 — Сложности с настройками контейнеров экспортеров (например, node-exporter официально не рекомендуется деплоить в докер-контейнере, т.к. для корректной работы ему требуется доступ к системе хоста).
 — Сложности взаимодействия контейнеров БД и экспортеров между собой. У меня, например, так и не удалось подружить контейнер cassandra-exporter с контейнером самой cassandra-db.
- 
В k8s-кластере. 
 Плюсы:
 — Как и в случае с docker: готовые докер-образы всех компонентов.
 — Возможности настроить автомасштабирование.
 — Высокая доступность и отказоустойчивость.
 — GAP-стек хорошо адаптирован для мониторинга kubernetes, в т.ч. есть готовые решения в виде helm-chart'ов.Минусы: 
 — Дополнительные сложности, связанные с реализацией деплоя stateful-приложения.
 — Для более гибкой настройки потребуется модификация образов.
 — Необходимо будет решать проблемы с объединением cassandra-узлов в единый cassandra-кластер и их идентификацией (для которой требуются ip-адреса узлов).
 — Масса сложностей (согласно информации с просторов сети) с настройками cassandra-exporter'а, чтобы он мог корректно забирать метрики.В сети можно найти несколько готовых helm-chart'ов для cassandra-db, например: один, второй (более не поддерживается разработчиками). Также есть несколько kubernetes-операторов разной степени готовности и поддержки, со своими плюсами и минусами. 
- 
Установка пакетов на хостах. 
 Плюсы:
 — Отсутствие проблем взаимодействия компонентов между собой, описанных для реализации в докер-контейнерах.
 — Возможность более гибкой настройки компонентов.Минусы: 
 —Требуются дополнительные настройки для организации работы cassandra как сервиса с автозапуском при старте хоста.
Для production среды рекомендуется деплой БД на хостах, этот вариант и будет рассмотрен далее.
В качестве основного инструмента для деплоя был выбран ansible. Соответственно для каждого компонента (БД и 2 экспортера) у нас будет своя ansible-role.
Общие особенности реализации процесса:
- ansible.cfg: 
[defaults]
host_key_checking = False
deprecation_warnings = False
interpreter_python = /usr/bin/python3
vault_password_file = vault_secret.sh
stdout_callback = yamlПара пояснений:
- [stdout_callback = yaml] — вывод при исполнении роли становится более читаемым; 
- [vault_password_file = vault_secret.sh] — данный параметр позволит хранить пароль от ansible-vault в качестве переменной окружения и не передавать его при запуске роли. 
В скрипт-файле vault_secret.sh:
#!/bin/bash
echo $ANSIBLE_VAULT_PASS- Организация директории group_vars: 
group_vars
├── all
│   └── var.yml
├── dev
│   ├── var.yml
│   └── vault.yml
├── prod
│   ├── var.yml
│   └── vault.yml
└── stage
    ├── var.yml
    └── vault.ymlВсе секреты храним в group_vars/<environment>/vault.yml, шифруемые c помощью ansible-vault.
Например, пароль от учётной записи, под которой будет осуществляться ssh-подключение к хостам — в group_vars/all/var.yml:
ansible_password: "{{ vault_ansible_password }}"В group_vars/<environment>/vault.yml:
ansible__vault_password: "MySecr3tP@s$w0rd!"И шифруем:
$ ansible-vault encrypt group_vars/test/vault.ymlПосле чего файл с паролем выглядит примерно вот так:
$ANSIBLE_VAULT;1.1;AES256
64613635303233376461643834326339343535303763353731643762383136656566666336646636
3866666162383666656137346365663631333532633762610a373733353863666230383630643661
61613262366138373537663866303434643565313262366133383732313062393531636431653862
3263653434313938320a386638653734376330646136303231323966393837333934373764306230
32306266366237393137303961633232666636643062306264633633623930666339326637643835
61396237386263613761626439303831313939366365303431616432656563366261633065636261
32623861653736386266343866386135373866636632316533363531356561643833326636303632
65613230323333383364323938376539303432656538353564383735373237306231366561383637
6338- Файл hosts(inventory): 
[seeds]
    csnd1 ansible_host=10.10.10.10
    csnd2 ansible_host=10.10.10.11
[workers]  
    csnd3 ansible_host=10.10.10.12    
[prod:children]
    seeds
    workers
[dc1:children]
    seeds
    workers
[all:vars]
    ansible_connection=ssh
    ansible_become=yes
    ansible_become_user=root- Объявление группы 'seeds' необходимо для дальнейшего определения seed-нод кластера. 
- Формирование остальных групп — в соответствии с требуемой в вашей задаче реализацией. 
Далее рассмотрим роли для деплоя каждого компонента
- 
Ansible-role для Cassandra DB: В качестве основы была выбрана готовая роль от LoCP (League of Crafty Programmers Ltd). Было изучено несколько вариантов, но роль LoCP, на мой субъективный взгляд, наиболее понятная и подходящая. Структура роли: 
roles/cassandra-db
├── defaults
│   └── main.yml
├── handlers
│   └── main.yml
├── tasks
│   ├── assertions.yml
│   ├── configure.yml
│   ├── hotfixes.yml
│   ├── install
│   │   ├── Debian.yml
│   │   └── RedHat.yml
│   ├── join_cluster.yml
│   ├── main.yml
│   ├── service.yml
│   └── variables.yml
├── templates
│   ├── cassandra.yaml.j2
│   └── etc
│       └── systemd
│           └── system
│               └── cassandra.service.j2
└── vars
    ├── Debian.yml
    ├── RedHat.yml
    └── main.ymlВ целом роль самодостаточная:
- Позволяет развернуть кластер cassandra как на серверах семейства RedHat, так и Debian. 
- Содержит шаги, необходимые для добавления cassandra в систему в качестве сервиса. 
- Есть таски для добавления новых нод в кластер. 
Пояснения и советы:
- Для корректного создания основного конфигурационного файла cassandra — cassandra.yaml — не забудьте добавить в defaults/main.yml либо в yaml-конфиг, вызывающий роль (по аналогии с примером из README.md данной роли), параметры cassandra_configuration и cassandra_regex_replacements: 
cassandra_datacenter1_name: "dc1"
cassandra_rack_name: "rack1"
seeds: "{{ seeds_list | join(',') }}"
 
 
cassandra_regex_replacements:
    - path: cassandra-env.sh
      line: MAX_HEAP_SIZE="{{ cassandra_max_heapsize_mb }}M"
      regexp: ^#MAX_HEAP_SIZE="4G"
    - path: cassandra-env.sh
      line: HEAP_NEWSIZE="{{ cassandra_heap_new_size_mb }}M"
      regexp: ^#HEAP_NEWSIZE="800M"
    - path: cassandra-rackdc.properties
      line: 'dc={{ cassandra_datacenter1_name }}'
      regexp: '^dc='
    - path: cassandra-rackdc.properties
      line: 'rack={{ cassandra_rack_name }}'
      regexp: '^rack='
 
 
сassandra_configuration:
    authenticator: PasswordAuthenticator
    cluster_name: "{{ envname }}-cluster"
    commitlog_directory: /data/cassandra/commitlog
    commitlog_sync: periodic
    commitlog_sync_period_in_ms: 10000
    data_file_directories:
        - /data/cassandra/data
    endpoint_snitch: GossipingPropertyFileSnitch
    hints_directory: "/data/cassandra/hints"
    listen_address: "{{ ansible_default_ipv4.address }}"
    rpc_address: "{{ ansible_default_ipv4.address }}"
    partitioner: org.apache.cassandra.dht.Murmur3Partitioner
    num_tokens: 256
    saved_caches_directory: /data/cassandra/saved_caches
    seed_provider:
        - class_name: "org.apache.cassandra.locator.SimpleSeedProvider"
          parameters:
              - seeds: "{{ seeds }}"
    start_native_transport: true- Значения cassandra_max_heapsize_mb и cassandra_heap_new_size_mb рассчитываются автоматически (на основе фактических технических параметров серверов) в тасках, описанных в tasks/variables.yml 
- seeds_list определяется следующей конструкцией в group_vars/all.yml: 
seeds_list: "{{ groups['seeds'] | map('extract',hostvars,'ansible_host') | list }}"Соответственно в hosts(inventory) должна быть группа seeds, которые и будут определены в качестве сидов кластера.
- 
В качестве бонуса, если вдруг в вашей задаче требуется первоначальная конфигурация БД, например, добавить пространство ключей (keyspace) и пользователя (user): а. Для начала надо дождаться, что кластер поднялся и слушает порты для подключения к нему (9042) и взаимодействия нод между собой (7000). Для этого можно добавить следующую таску после всех задач конфигурации и запуска: 
- name: Wait for the Cassandra Listener
  wait_for:
    timeout: 120
    host: "{{ ansible_default_ipv4.address }}"
    port: "{{ item }}"
  loop:
    - 9042
    - 7000b. В templates/ добавить шаблон cqlsh-скрипта, например, в моём случае было достаточно такого:
ALTER KEYSPACE system_auth
    WITH replication = {
        'class': 'NetworkTopologyStrategy',
        '{{ cassandra_datacenter1_name }}': {{ cass_dc1_nodes_number }}
    };
CREATE ROLE IF NOT EXISTS {{ cassandra_user }} 
WITH PASSWORD = '{{ cassandra_user_password }}'
AND LOGIN = true;
CREATE KEYSPACE IF NOT EXISTS {{ cassandra_keyspace }} 
    WITH replication = {
        'class': 'NetworkTopologyStrategy',
        '{{ cassandra_datacenter1_name }}': {{ cass_dc1_nodes_number }}
    };Не забыть объявить необходимые переменные:
cassandra_user: "test_user"
cassandra_keyspace: "test_keyspace"
cassandra_datacenter1_name: "dc1"
cassandra_rack_name: "rack1"
cass_dc1_nodes_number: "{{ groups['dc1'] | length }}"
 
cqlsh_script_template: "cqlsh_script.txt.j2"
cqlsh_script_path: "/root/cqlsh_script.txt"cassandra_user_password — добавить в ansible-vault
c. Скопировать скрипт на одну ноду кластера, выполнить его и после удалить:
- block:
  - name: Apply cqlsh_script
    template:
      src: "{{ cqlsh_script_template }}"
      dest: "{{ cqlsh_script_path }}"
      owner: root
      group: root
      mode: '0644'
  - name: add DB data (keyspace, user)
    shell: "cqlsh {{ ansible_default_ipv4.address }}  -u cassandra -p cassandra -f  {{ cqlsh_script_path }}"
  - name: Remove cqlsh_script file
    file:
      path: "{{ cqlsh_script_path }}"
      state: absent
when: inventory_hostname == groups['seeds'][0]- 
Ansible-role для node-exporter'а: Основа — готовая роль под авторством cloudalchemy. Структура роли: 
roles/node-exporter
├── defaults
│   └── main.yml
├── handlers
│   └── main.yml
├── tasks
│   ├── configure.yml
│   ├── install.yml
│   ├── main.yml
│   ├── preflight.yml
│   └── selinux.yml
├── templates
│   ├── config.yaml.j2
│   └── node_exporter.service.j2
└── vars
    └── main.ymlНеобходимости в модификации данной роли почти нет (за исключением моментов, связанных с особенностями инфраструктуры (например, доступность внешних репозиториев из внутренней сети вашей компании), например, node_exporter_repo_url), собственная документация роли, на мой взгляд исчерпывающая.
Пояснения и советы:
- Роль адаптирована под дистрибутивы семейств RHEL, Fedora, ClearLinux. 
- Node-exporter будет функционировать в качестве systemd service unit. 
- 
Предусмотрены два варианта установки экспортера: a. Бинарник скачивается из репозитория, адрес которого передаётся в переменной node_exporter_repo_url. В случае, если ваше окружение имеет ограничения в доступе к официальному репозиторию, следует заменить его на адрес своего, доступного репозитория. b. Бинарник заранее помещается в директорию с файлами роли (roles/node-exporter/files), либо по другому удобному вам пути, который далее передаётся в переменной node_exporter_binary_local_dir Выбирайте более подходящий для вас вариант, но не передавайте одновременно значения и в node_exporter_repo_url, и в node_exporter_binary_local_dir, т.к. они отвечают за взаимозаменяемые сценарии. 
- Для изменения версии экспортера — поменять значение параметра node_exporter_version в defaults/main.yml. Здесь же найдутся параметры для более гибкой настройки конфигурации (config.yaml) — параметры: 
node_exporter_tls_server_config: {}
node_exporter_http_server_config: {}
node_exporter_basic_auth_users: {}- При необходимости внести кастомные изменения в конфигурационные файлы настройки экспортера (config.yaml) и настройки сервиса (node_exporter.service): шаблоны файлов расположены в каталоге templates/. 
- Если по каким-то причинам вам потребуется поменять порт, на котором будет работать экспортер — меняйте переменную node_exporter_web_listen_address (стандартное значение "0.0.0.0:9100"). Аналогично с эндпоинтом для метрик — переменная node_exporter_web_telemetry_path (стандартное значение "/metrics"). 
3.Ansible-role для cassandra-exporter'а:
В качестве основы была выбрана роль от criteo. Струтура:
roles/cassandra-exporter
├── README.md
├── defaults
│   └── main.yml
├── files
│   └── config.yml
├── handlers
│   └── main.yml
├── tasks
│   └── main.yml
└── templates
    ├── cassandra_exporter.sh.j2
    └── etc
        └── systemd
            └── system
                └── prometheus-cassandra-exporter.service.j2Изначально эта роль является развитием оригинального JMX exporter, но доработана для более лёгкой интеграции с Cassandra DB. По сути, установки экспортера с помощью этой роли достаточно, чтобы после настройки прометеуса на забор метрик с этого экспортера и установки в графане этого дашборда получить исчерпывающий набор информативных графиков для мониторинга нашего кассандра-кластера в разрезе работы БД.
Также важно отметить, что в рамках этой роли экпортер устанавливается в формате standalone, т.е. как отдельный компонент. Если у вас на проекте предполагается высокая нагрузка на cassandra DB, что закономерно повлечёт за собой увеличение количества метрик, то возможно вам стоит рассмотреть установку cassandra-exporter как java-agent (что позволит ускорить сбор метрик и снизить нагрузку на систему; из минусов — необходимость встраивать агент в конфигурацию cassandra DB, и, как следствие, потенциальные проблемы с совместимостью); в этом случае советую ознакомиться со следующей реализацией экспортера.
Пояснения и советы:
- cassandra-exporter будет функционировать в качестве systemd service unit. 
- Версия экспортера задаётся в defaults/main.yml в переменной cassandra_exporter_version. Актуальную версию можно посмотреть здесь. 
- Порт, на котором будет висеть экспортер задаётся в defaults/main.yml в переменной cassandra_exporter_listen_port (значение по умолчанию в роли — 8080, но вообще стандартный порт для cassandra-exporter — 9500). Этот же порт следует прописывать в конфиге прометеуса для стягивания метрик. 
- Бинарник и конфиг-файл скачиваются из репозитория, адреса передаются в переменных cassandra_exporter_binary_url и cassandra_exporter_config_url. В случае, если ваша инфраструктура имеет ограничения в доступе к официальному репозиторию, следует заменить значения переменных на адреса своего, доступного репозитория. 
- Если вам потребуется, чтобы установочный бинарник и/или файл конфигурации сразу были в проекте, и не нужно было их скачивать в процессе отработки роли, то потребуются следующие доработки роли: 
 — Закинуть эти файлы (по аналогии с node-exporter'ом,) в директорию с файлами роли (roles/cassandra-exporter/files) либо в другую директорию проекта.
 — Добавить переменные cassandra_exporter_binary, cassandra_exporter_config, в которых будут указаны пути до файлов в проекте (если файлы будут помещены в roles/cassandra-exporter/files, то в значениях переменных достаточно будет указать только имена этих файлов, например: 'cassandra_exporter.jar', 'config.yml').
 — Добавить следующие исполнительные таски:
- name: 'copy cassandra exporter binary'
  copy:
    src: '{{ cassandra_exporter_binary }}'
    dest: '{{ cassandra_exporter_dist_dir }}/cassandra.jar'
    owner: '{{ cassandra_exporter_user }}'
    group: '{{ cassandra_exporter_group }}'
    mode: '0644'
  when: cassandra_exporter_binary is definedА в таске 'download cassandra exporter binary' добавить условие выполнения:
when: cassandra_exporter_binary is not defined- name: 'сopy cassandra exporter config'
  copy:
    src: '{{ cassandra_exporter_config }}'
    dest: '{{ cassandra_exporter_config_dir }}/config.yml'
    owner: '{{ cassandra_exporter_user }}'
    group: '{{ cassandra_exporter_group }}'
    mode: '0644'
  when: cassandra_exporter_config is definedПеременные cassandra_exporter_dist_dir и cassandra_exporter_config_dir - уже заданы в роли.
Конфиг-файл экспортера советую именно таким образом держать в проекте, чтобы при необходимости иметь возможность вносить доработки/изменения через код.
Итог
Имеем набор ролей, который позволит тремя командами:
$ ansible-playbook -i ansible/hosts ansible/install-cassandra-db.yml
$ ansible-playbook -i ansible/hosts ansible/install-cassandra-exporter.yml
$ ansible-playbook -i ansible/hosts ansible/install-node-exporter.ymlРазвернуть полный набор — кластер БД cassandra и его мониторинг как на уровне ресурсов системы, так и на уровне самой БД. Все три компонента установятся на хостах в качестве systemd service unit'ов, перезапускаемых при старте системы, так что в случае перезагрузки хостов, не потребуется ручного запуска.
При установке желательно придерживаться последовательности:
- cassandra-db 
- cassandra-exporter — в этом случае cassandra-exporter сразу найдёт запущенный сервис; node-exporter от остальных компонентов не зависит, и его можно устанавливать в любой момент. 
В заключение, для проверки работы каждого компонента:
- 
БД должна быть доступна при обращении к любой из нод кластера на порт 9042. — Для удалённого подключения через консоль нам понадобится клиент cqlsh: 
$ cqlsh <cassandra_node1_ip-address>:9042 -u my_cass_userБолее подробно о работе с клиентом cqlsh.
— Для диагностики кластера при локальном подключении к хостам-нодам cassandra можно использовать инструмент — nodetool, устанавливается автоматически вместе с cassandra-db. Более подробно о работе с nodetool.
- Метрики node-exporter должны быть доступны при обращении к каждому хосту-ноде кластера на порт 9100, 
- Метрики cassandra-exporter — на порт 9500. 
 
          