# Data Storager

**data-storager** (в коде и переменных окружения также встречается историческое имя **es-storager**) — микросервис, который читает данные из реляционных баз **Webim** и переносит их в поисковый кластер (**Elasticsearch** или **OpenSearch**) и в **ClickHouse**. Исходные записи в meta-БД и базах **pro** сервис не изменяет.

Компонент не входит в основной пакет **Webim Server**. Его выдаёт [техническая поддержка](mailto:support@webim.ru) вместе с инструкцией по установке конкретной поставки. Статья описывает архитектуру, настройки и типовые операции, которые нужно знать администратору hosted-инсталляции.

Связанные материалы:

  * [Подключение Elasticsearch или OpenSearch](https://webim.ru/kb/devops/connect-elasticsearch.html) — поиск в [истории диалогов](https://webim.ru/kb/for-admins/view-dialog-history.html);

  * [Режимы подсчёта статистики](https://webim.ru/kb/for-analytics/statistics/calculation-modes.html) — роль сервиса в [Статистике версии 2](https://webim.ru/kb/for-analytics/statistics/);

  * [Требования к ПО](https://webim.ru/kb/devops/sys-reqs-for-server/sys-reqs-server-sw.html) и [сетевые требования](https://webim.ru/kb/devops/sys-reqs-for-server/sys-reqs-server-nw.html).

## Как устроен сервис

Один процесс **producer** раз в **три минуты** просматривает список аккаунтов и публикует в общий топик **Apache Kafka** задание `DiscoverAndStoreRequest` (имя аккаунта). Несколько независимых **consumer**-процессов читают тот же топик и каждый делает свою работу:

```
meta-БД (таблица accountkeyvalue)
        │  раз в 3 минуты
        ▼
   producer  ──►  топик Kafka (DISCOVER_AND_STORE_CHATS_TOPIC)
                       │
          ┌────────────┼────────────────────┐
          ▼            ▼                    ▼
 subscriber-chats   subscriber-stats   subscriber-report-stats (опционально)
          │            ▼                    ▼
          ▼       ClickHouse           ClickHouse
 Elasticsearch /   (статистика v2,     (отчётный стор)
 OpenSearch        базы webim_stats_*)
```

Маркеры прогресса и список временно «заблокированных» аккаунтов хранятся в **Redis**. Благодаря этому consumer-ы можно запускать, останавливать и масштабировать независимо, не теряя позицию переноса.

Сервис **не получает содержимое чатов из Kafka**: в топике только триггер. Данные consumer читает сам из баз **pro**.

### Компоненты

| Компонент | Роль | Обязателен |
|-----------|------|------------|
| **producer** | Планировщик: один экземпляр на весь контур. Не запускайте отдельный producer на каждый consumer. | Да |
| **subscriber-chats** | Индексация тредов, сообщений, посетителей, оценок и опросов в **Elasticsearch** / **OpenSearch**. | Для поиска в истории |
| **subscriber-stats** | Наполнение **ClickHouse** для **Статистики версии 2**. | Для статистики v2 через storager |
| **subscriber-report-stats** | Отдельный ETL в отчётный ClickHouse (виджеты статистики и дашборда поверх HTTP API). | Если используете отчётный HTTP API |
| **stats.py** | HTTP-приложение (FastAPI): служебные эндпоинты, метрики, при включении — API отчётов и статика виджетов. | Да, если нужны `/health`, `/metrics` или отчётный API |
| **dashboard-realtime** | Периодический снимок «живых» показателей дашборда в Redis. | Только для realtime-дашборда |
| **subscriber-chat-states**, **subscriber-periods** | Индексация истории состояний чата и периодов онлайна операторов в поисковый кластер. | По необходимости поставки |

## Функции

  * индексация истории чатов и связанных объектов в **Elasticsearch** / **OpenSearch**;

  * индексация истории состояний чатов и периодов онлайна операторов в тот же кластер — опционально, отдельными компонентами `subscriber-chat-states` и `subscriber-periods`;

  * перенос статистических данных в **ClickHouse** для **Статистики версии 2**;

  * инкрементальное обновление по триггерам из **Kafka**;

  * опциональный отчётный HTTP API и встраиваемые виджеты статистики / дашборда;

  * публикация метрик **Prometheus** и проверка состояния.

## Что должно быть в инфраструктуре

Перед запуском согласуйте с поддержкой и подготовьте:

  * **Apache Kafka** (и права на чтение/запись общего топика; по умолчанию имя топика — `es-discover-and-store-chats`, **2 партиции**);

  * **Redis** — маркеры, кэш и список аккаунтов, для которых недавно произошла ошибка обработки;

  * **meta-БД** Webim (MySQL или PostgreSQL) — таблица `accountkeyvalue`;

  * базы **pro** каждого индексируемого аккаунта (MySQL или PostgreSQL);

  * кластер **Elasticsearch** или **OpenSearch**, если нужен поиск в истории;

  * **ClickHouse**, если нужна статистика v2 и/или отчётный API;

  * файл геоданных Sypex Geo по пути `GEO_DB_PATH` (если в поставке используется определение страны в статистике).

Версии смежных продуктов см. в [требованиях к ПО сервера](https://webim.ru/kb/devops/sys-reqs-for-server/sys-reqs-server-sw.html). Типовая поставка сервиса рассчитана на **Python 3.10**.

## Какие аккаунты обрабатываются

**producer** собирает список аккаунтов из двух источников и отбрасывает те, что временно попали в Redis-множество `storager.failed_accounts` (после ошибки обработки; срок — **15 минут**).

### Маркер в meta-БД (основной способ)

В таблицу `accountkeyvalue` добавьте строку с ключом `es_storager_key`. Значение должно **посимвольно** совпадать с переменной **`ES_KEY_VALUE`** в `.env` того инстанса **data-storager**, который должен обрабатывать аккаунт. Так несколько инстансов сервиса могут делить аккаунты между собой.

Пример для MySQL:

```sql
INSERT INTO accountkeyvalue (`accountname`, `key`, `value`)
VALUES ('shop', 'es_storager_key', '1.0');
```

Подробнее о маркере — в статье [Подключение Elasticsearch или OpenSearch](https://webim.ru/kb/devops/connect-elasticsearch.html).

### Список в `.env`

Переменная `PRODUCE_FOR_ACCOUNTS` — имена аккаунтов через запятую. Такие аккаунты producer обрабатывает **даже без** строки `es_storager_key` в meta-БД:

```ini
PRODUCE_FOR_ACCOUNTS=shop,support
```

### Подключение к базе pro

По умолчанию URL берётся из `PRO_DATABASE_DEFAULT_URL`, имя базы подставляется из `PRO_DATABASE_TEMPLATE` (плейсхолдер `{account_name}`).

Если имя аккаунта не совпадает с ключом подключения (другой хост или другое имя базы), задайте в meta-БД ключ `pro_db_connection_key`. Сервис ищет переменную окружения `DATABASE_CONNECTION_<NAME>_URL`, где `<NAME>` — значение ключа в верхнем регистре.

```sql
INSERT INTO accountkeyvalue (`accountname`, `key`, `value`)
VALUES ('shop', 'pro_db_connection_key', 'db1');
```

```ini
DATABASE_CONNECTION_DB1_URL=mysql+aiomysql://webim_service:PASSWORD@db.example.local:3306/{database}
```

!!! Warning "Внимание"
    Если `pro_db_connection_key` задан, а соответствующей переменной `DATABASE_CONNECTION_<NAME>_URL` нет, сервис **молча** использует `PRO_DATABASE_DEFAULT_URL`. При неверном ключе это выглядит как ошибка «Unknown database» в логах consumer, а аккаунт периодически попадает в `failed_accounts`.

### Глубина первого переноса в поисковый кластер

Если маркеров в Redis ещё нет, **subscriber-chats** ограничивает «назад» по ключам в `accountkeyvalue`. Приоритет у `es_store_since`.

| Ключ | Смысл | Формат значения |
|-------|--------|-----------------|
| `es_store_since` | Фиксированная дата начала индексации | `YYYY-MM-DDTHH:MM:SS+TZ`, например `2026-01-01T00:00:00+03:00` |
| `es_store_period` | Глубина от текущего момента | микросекунды, например `2592000000000` — 30 суток |

Для **subscriber-stats** (ClickHouse статистики v2) при отсутствии маркера глубина по умолчанию — **последние 30 дней** (начало суток). Маркеры этого контура в Redis живут **7 суток**: если процессы не работали дольше, прогресс сбрасывается, и перенос снова начнётся с этой границы.

## Сетевые требования

HTTP-порт веб-приложения задаётся `STATS_WEB_PORT` (по умолчанию **8000/tcp**), адрес прослушивания — `STATS_BIND_IP` (по умолчанию `0.0.0.0`). Прямой доступ из интернета к этому порту, как правило, не нужен: откройте его только для внутренней сети, балансировщика и системы мониторинга.

На том же порту:

  * `/health` — проверка: если в Redis есть аккаунты из `failed_accounts`, ответ будет с кодом **500**;

  * `/metrics` — метрики **Prometheus** веб-приложения;

  * `/static/…` и `/reports…` — при включённом отчётном контуре.

Каждый Faust-воркер (**producer**, consumer-ы) дополнительно поднимает HTTP-сервер метрик на порту **`PROMETHEUS_PORT`** (по умолчанию **9090**). Если несколько воркеров запущены на одном хосте без изоляции портов, назначьте им разные значения.

Для hosted-инсталляции на узле с сервисом нужны следующие направления (порты — значения по умолчанию; актуальные смотрите в `.env`).

| Направление | Назначение | Порт/протокол | Примечание |
|-------------|------------|---------------|------------|
| Входящие | HTTP веб-приложения | `STATS_WEB_PORT` (**8000/tcp**) | `/health`, `/metrics`, при необходимости отчётный API |
| Входящие | Метрики воркеров | `PROMETHEUS_PORT` (**9090/tcp**) | Сбор **Prometheus** с процессов Faust |
| Исходящие | meta-БД и базы **pro** | **3306/tcp** или **5432/tcp** | Сервис читает данные напрямую из СУБД, не через HTTPS бэкенда |
| Исходящие | Redis | порт из `REDIS_CACHE_LOCATION` | Маркеры и кэш |
| Исходящие | Elasticsearch / OpenSearch | **9200/tcp** | Запись индексов истории |
| Исходящие | ClickHouse | **9000/tcp** (native); при необходимости **8123/tcp** (HTTP) | Статистика v2 и/или отчётный стор |
| Исходящие | Apache Kafka | порт брокера | Публикация и чтение заданий |
| Исходящие | Sentry | **443/tcp** | Только если задан `SENTRY_DSN` |

Если **data-storager** и **Speller** стоят на одном хосте без сетевой изоляции, назначьте им разные HTTP-порты.

## Переменные окружения

Сервис настраивается через `.env` (или эквивалент в оркестраторе). Полный шаблон поставляется вместе с компонентом. Ниже — параметры, которые нужно согласовать при развёртывании.

### Ядро и Kafka

| Переменная | Назначение |
|------------|------------|
| `KAFKA_BROKER` | Адрес брокера **Apache Kafka**. |
| `DISCOVER_AND_STORE_CHATS_TOPIC` | Имя общего топика заданий. |
| `ES_KEY_VALUE` | Значение маркера `es_storager_key` в meta-БД. |
| `PRODUCE_FOR_ACCOUNTS` | Дополнительный список аккаунтов через запятую. |
| `BATCH_SIZE` | Размер пакета при чтении БД и записи в поисковый кластер. |
| `LOGGING_LEVEL`, `LOGGING_FORMAT` | Уровень и формат логов. |
| `SENTRY_DSN`, `SENTRY_ENV` | Опциональная отправка ошибок в **Sentry**. |
| `DEFAULT_TZ` | Часовой пояс по умолчанию. |
| `GEO_DB_PATH` | Путь к файлу Sypex Geo. |
| `DEFAULT_HIDDEN_COUNTRIES` | Страны, скрываемые в статистике. |

### Базы данных

| Переменная | Назначение |
|------------|------------|
| `META_DATABASE_URL` | Подключение к meta-БД. Для MySQL — префикс `mysql+aiomysql://`, для PostgreSQL — `postgresql+asyncpg://` (схема meta задаётся параметром `schema=`). |
| `PRO_DATABASE_TEMPLATE` | Шаблон имени базы **pro**, например `webim_service_pro_{account_name}`. |
| `PRO_DATABASE_DEFAULT_URL` | URL по умолчанию; плейсхолдер `{database}` заменяется именем из шаблона. |
| `DATABASE_CONNECTION_<NAME>_URL` | Именованные подключения для `pro_db_connection_key`. |
| `REDIS_CACHE_LOCATION` | URL **Redis**, например `redis://redis.example.local:6379/2`. |

### Поисковый кластер

| Переменная | Назначение |
|------------|------------|
| `SEARCH_ENGINE_TYPE` | `elasticsearch` или `opensearch`. |
| `SEARCH_ENGINE_URLS` | URL узлов кластера (через запятую). Допустимы устаревшие имена `ELASTIC_URLS`, `ELASTIC_USER`, `ELASTIC_PASSWORD`, `ELASTIC_INDEX_TEMPLATE`. |
| `SEARCH_ENGINE_USER`, `SEARCH_ENGINE_PASSWORD` | Учётная запись кластера. Нужны права на работу с индексами. |
| `SEARCH_ENGINE_INDEX_TEMPLATE` | Шаблон имён индексов, например `{account}-{object}`. |
| `ES_INDEX_VERSION` | Суффикс версии в ключах маркеров Redis. |

На стороне **Webim Server** подключение к тому же кластеру задаётся в `/etc/webim/db.json` и параметрах account config `elasticsearch_history` / `elasticsearch_threadprocessor` — см. [Подключение Elasticsearch или OpenSearch](https://webim.ru/kb/devops/connect-elasticsearch.html).

### ClickHouse статистики v2

| Переменная | Назначение |
|------------|------------|
| `STATS_DATABASE_TEMPLATE` | Шаблон имени базы, по умолчанию `webim_stats_{account_name}`. |
| `STATS_DATABASE_DEFAULT_URLS` | URL реплик **ClickHouse** через запятую. Список трактуется как **реплики одной и той же копии данных**. Не указывайте здесь разные шарды: запись уйдёт на один узел, а чтение увидит неполные данные. |

### HTTP и мониторинг

| Переменная | Назначение |
|------------|------------|
| `STATS_BIND_IP` | Адрес прослушивания веб-приложения (по умолчанию `0.0.0.0`). |
| `STATS_WEB_PORT` | HTTP-порт (по умолчанию `8000`). |
| `PROMETHEUS_PORT` | Порт метрик Faust-воркера (по умолчанию `9090`). |

### Отчётный HTTP API (опционально)

Эти переменные нужны, если в поставке включены виджеты статистики / дашборда (`subscriber-report-stats` и `stats.py`).

| Переменная | Назначение | По умолчанию |
|------------|------------|--------------|
| `REPORT_CH_URLS` | URL **одного** экземпляра (или реплик) отчётного ClickHouse. | — |
| `REPORT_CH_DATABASE_TEMPLATE` | Имя **общей** базы отчётов на все аккаунты (без плейсхолдера `{account_name}`: изоляция — колонкой `account_name`). | — |
| `REPORT_STATS_MAX_AGE_DAYS` | Глубина **хранения** отчётных данных, дни. Уменьшение значения при следующем populate удалит более старые строки. | `730` |
| `REPORTS_MAX_RANGE_DAYS` | Максимальная **ширина одного** запроса отчёта, дни; `0` — без ограничения. | `31` |
| `REPORT_SCAN_LOOKBACK_DAYS` | Запас вниз по дате создания треда при скане; `0` — полное чтение таблицы. | `90` |
| `REPORTS_REQUIRE_SIGNATURE` | Требовать подписанный токен. В промышленной среде оставляйте `True`. | `True` |
| `REPORTS_SIGNING_SECRET` | Секрет HMAC (≥ 32 символов), **только на сервере**. При включённой подписи и коротком секрете веб-приложение не стартует. | — |
| `REPORTS_SIGNING_SECRET_PREVIOUS` | Предыдущий секрет на время ротации. | — |
| `REPORTS_MAX_TOKEN_TTL_DAYS` | Потолок остаточного срока токена; `0` — без ограничения. | `0` |
| `REPORTS_CORS_ORIGINS` | Origin страниц, с которых браузер может читать API (через запятую). Это не замена токену. | `*` |
| `REPORTS_RATE_LIMIT` / `REPORTS_RATE_LIMIT_WINDOW` | Лимит запросов на IP и окно в секундах; `0` в лимите — выключить. | `120` / `60` |
| `REPORTS_TRUST_PROXY_HEADER` | Брать IP клиента из `X-Forwarded-For`. Включайте **только** за прокси, который сам выставляет заголовок. | `False` |
| `DASHBOARD_REALTIME_UPDATE_INTERVAL` | Интервал живого опроса дашборда, сек. | `15` |
| `DASHBOARD_PREV_PERIODS_UPDATE_INTERVAL` | Интервал исторического опроса дашборда, сек. | `60` |

!!! Note "N.B."
    База отчётного контура создаётся администратором заранее. Процесс ETL не выполняет `CREATE DATABASE`. Пользователю ClickHouse нужны права на таблицы этой базы, включая DDL и мутации (`CREATE TABLE` / `VIEW`, `DROP`, `ALTER`, `OPTIMIZE`, `TRUNCATE` — уточняйте по поставке).

## Масштабирование и отказоустойчивость

  * **producer** — ровно **один** экземпляр на контур.

  * Consumer-ы масштабируются по числу партиций топика (в типовой конфигурации — 2). Все экземпляры одного consumer должны использовать **общий Redis**.

  * Веб-приложение `stats.py` можно поставить за прокси в нескольких процессах.

  * Отчётный ClickHouse: в `REPORT_CH_URLS` перечисляйте только **реплики**. Если кластер шардирован и реплик нет, указывайте **один** узел: несколько шардов в этом списке разнесут запись и испортят отчёты без явной ошибки в логе.

  * Источник истины для чатов — базы **pro**. Поисковый кластер и ClickHouse — производные витрины: после потери витрины данные можно перезалить (см. ниже).

## Эксплуатация

Команды выполняются через CLI Faust. В Docker подставьте имя контейнера вашей поставки, например:

```bash
docker exec -it es-storager-subscriber-chats-1 faust -A subscriber_chats -l info health
```

### Поиск (subscriber-chats)

| Задача | Команда |
|---------|---------|
| Проверить связь с кластером поиска | `faust -A subscriber_chats -l info health` |
| Сбросить список `failed_accounts` в Redis | `faust -A subscriber_chats -l info reset` |
| Поставить задание на индексацию аккаунта | `faust -A subscriber_chats -l info indexchats --account_name=shop` |
| Проиндексировать один тред | `faust -A subscriber_chats -l info indexchat --account_name=shop --chat_id=<id>` |
| Состояние маркеров | `faust -A subscriber_chats -l info info --account_name=shop` |
| Сбросить маркеры (переиндексация с начала глубины) | `faust -A subscriber_chats -l info refresh --account_name=shop` |
| Удалить индекс аккаунта | `faust -A subscriber_chats -l info drop --account_name=shop` |
| Обновить mapping индекса | `faust -A subscriber_chats -l info mapping --account_name=shop` |

### Статистика v2 (subscriber-stats)

Даты: `%d-%m-%Y` или `%d-%m-%YT%H:%M:%S`.

```bash
faust -A subscriber_stats -l info populate --account_name=shop --from=01-01-2026 --to=31-01-2026
faust -A subscriber_stats -l info nextstep --account_name=shop
faust -A subscriber_stats -l info refresh --account_name=shop
```

`refresh` сбрасывает маркеры всех storager-ов статистики v2 и запускает пересчёт.

Для «дыр» в ClickHouse, которые наполняет **Chat Backend**, по-прежнему можно использовать скрипт `populate_clickhouse.sh` — см. [Перенос данных в ClickHouse](https://webim.ru/kb/devops/populate-clickhouse.html).

### Отчётный контур (subscriber-report-stats)

```bash
faust -A subscriber_report_stats -l info migrate_schema --account_name=shop
faust -A subscriber_report_stats -l info populate --account_name=shop --from=01-01-2026 --to=31-01-2026
faust -A subscriber_report_stats -l info refresh --account_name=shop
faust -A subscriber_report_stats -l info purge --account_name=shop
```

Схема таблиц создаётся идемпотентно при первом событии по аккаунту; `migrate_schema` прогревает её заранее. Очистка старше `REPORT_STATS_MAX_AGE_DAYS` выполняется автоматически не чаще раза в сутки (можно вызвать `purge` вручную).

## Отчётный HTTP API и встраивание

Если в поставке включён отчётный контур, веб-приложение отдаёт:

| Метод | Путь | Назначение |
|-------|------|------------|
| `GET` | `/reports` | Один отчёт (`report`, `start`, `end`; необязательно `department`, `locale`, `category_id`) |
| `GET` | `/reports/list` | Список отчётов |
| `GET` | `/reports/filters` | Значения фильтров и граница хранения `min_date` |
| `GET` | `/l/o/current-stats` | Данные дашборда (`realtime=true` или `false`) |

Авторизация: параметр `token` (подписанный токен). В режиме разработки при `REPORTS_REQUIRE_SIGNATURE=False` допускается `account_name` — **в промышленной среде не используйте**.

Токен несёт имя аккаунта и срок действия, подписывается HMAC-SHA256 секретом `REPORTS_SIGNING_SECRET`. Бэкенд страницы-хоста выпускает **новый** токен на каждый рендер с коротким TTL. Отдельного отзыва токенов нет: экстренно отзывают все токены ротацией секрета (`REPORTS_SIGNING_SECRET_PREVIOUS` на время перекрытия). Формат токена и примеры реализации на PHP / Node.js / Python входят в поставку компонента (файл `report_storager/EMBED.md`).

Виджеты подключаются одним тегом (скрипт сам догружает недостающие стили и библиотеки). Origin API по умолчанию совпадает с origin скрипта; при другом origin задайте его на странице. Для кросс-доменных запросов со страниц **Webim Server** укажите эти origin в `REPORTS_CORS_ORIGINS`.

Лимит `REPORTS_MAX_RANGE_DAYS` отклоняет слишком широкий период кодом **400** (`range_too_wide`), а не сужает его молча. Частотный лимит (`429`) не затрагивает `/health`, `/metrics` и `/static`. Если Redis недоступен, частотный лимитер пропускает запрос (fail-open): это защита от нагрузки, а не авторизация.

## Логирование и мониторинг

Логи пишутся в стандартный вывод процесса.

Метрики воркеров (имена с префиксом `es_storager_`) включают счётчики проиндексированных и неуспешных чатов, маркеры прогресса, число опубликованных заданий. На отчётном API: `report_storager_auth_rejected` (причины отказа видны в метке, в HTTP-ответе всегда общий **401**) и `report_storager_rate_limited`.

!!! Note "N.B."
    При инфраструктурной ошибке (нет базы, таймаут СУБД) счётчик неуспешных чатов может вырасти сразу на величину `BATCH_SIZE`, хотя документы в кластер ещё не отправлялись. Ориентируйтесь также на логи и на отсутствие роста `es_storager_indexed_chats`.

## Типовые проблемы

| Симптом | Что проверить |
|---------|----------------|
| Аккаунт не обрабатывается | Есть `es_storager_key` = `ES_KEY_VALUE` или имя в `PRODUCE_FOR_ACCOUNTS`; аккаунт не в `failed_accounts` (`reset`) |
| Ошибка «Unknown database» | `pro_db_connection_key` и наличие `DATABASE_CONNECTION_<NAME>_URL`; иначе используется `PRO_DATABASE_DEFAULT_URL` |
| Нет связи с поиском | `faust -A subscriber_chats -l info health`; URL, учётная запись, сеть до **9200/tcp** |
| Нужно переиндексировать историю | `refresh` или `drop` + `indexchats` |
| В статистике v2 дыра за период | `populate` у `subscriber_stats` и/или [populate_clickhouse.sh](https://webim.ru/kb/devops/populate-clickhouse.html) |
| Отчёты пустые / 500 | Доступен ли ClickHouse по `REPORT_CH_URLS`; создана ли схема (`migrate_schema`); эмитит ли producer аккаунт |
| `401`, «Ссылка устарела» | Истёк или неверно подписан токен; секрет на хосте и в сервисе совпадает |
| Фронт не загружает отчёты | `REPORTS_CORS_ORIGINS`; прокси до `STATS_WEB_PORT` |
| Realtime-дашборд пустой | Запущен `dashboard-realtime`; недавно открывали дашборд этого аккаунта |

---

**data-storager** обеспечивает перенос данных из баз **Webim** в поисковый кластер и **ClickHouse**. Для работы нужны **Kafka**, **Redis**, доступ к meta-БД и базам **pro**, согласованный маркер `es_storager_key` / `ES_KEY_VALUE` и (по задачам инсталляции) кластер поиска и ClickHouse.

---

Источник: https://webim.ru/kb/devops/services/data-storager.html
