Проектирование Slack
Slack — это корпоративный мессенджер для командной работы, запущенный в 2013 году и приобретённый Salesforce за $27,7 млрд в 2021 году. Если Discord — это «голосовой чат для геймеров, ставший платформой для сообществ», то Slack — это «мессенджер для команд, ставший операционной системой для компаний».
Масштаб Slack (2025)
| Метрика | Значение |
|---|---|
| Ежедневно активных (DAU) | ~40-48 млн |
| Ежемесячно активных (MAU) | ~79 млн |
| Платящих организаций | ~200 000+ |
| Сообщений в день | ~1,5 млрд |
| Действий в неделю | ~5 млрд (сообщения, реакции, файлы, боты) |
| Среднее время в приложении | ~1 ч 42 мин/день |
| Fortune 100 компаний на Slack | 77% |
Slack vs Discord: в чём различие?
На первый взгляд Slack и Discord похожи: и там, и там есть каналы, сообщения в реальном времени, боты, интеграции. Но их архитектуры решают принципиально разные задачи:
| Аспект | Discord | Slack |
|---|---|---|
| Аудитория | Сообщества (от 1 до 15M+ участников) | Организации (от 5 до 500K сотрудников) |
| Голос/видео | Ядро продукта (SFU, WebRTC) | Дополнение (Huddles, через AWS) |
| Масштаб «сервера» | До 15M в одном (Midjourney) | До 500K (Enterprise Grid) |
| Данные | Сообщения хранятся «вечно» | Бесплатный план: 90 дней истории |
| Безопасность | Базовая | Enterprise-grade (compliance, DLP, eDiscovery) |
| Интеграции | Боты (Discord API) | 2 600+ приложений (Salesforce, Jira, Google) |
| Ключевая фича | Голосовые каналы | Slack Connect (кросс-организации) |
| Шардинг | По guild_id | По workspace_id (исторически) |
| Real-time | Elixir/BEAM Gateway | Flannel edge cache + Gateway Servers |
Ключевое архитектурное отличие Slack: система изначально спроектирована вокруг workspace (рабочего пространства организации) как единицы изоляции данных. Все данные одной организации живут вместе, и между организациями нет пересечений. Это даёт безопасность и compliance, но создаёт уникальные проблемы при масштабировании (о которых поговорим ниже).
Основные функции
- Каналы (Channels) — публичные и приватные каналы для обсуждений по темам.
- Личные сообщения (DMs) — переписка между пользователями.
- Треды (Threads) — ответы внутри сообщения, не засоряющие основной канал.
- Slack Connect — каналы, объединяющие пользователей разных организаций.
- Поиск — полнотекстовый поиск по всем сообщениям, файлам и каналам с учётом прав доступа.
- Интеграции и боты — 2 600+ приложений (Jira, GitHub, Google Drive, Salesforce).
- Huddles — лёгкие аудиозвонки прямо в канале (аналог голосовых каналов Discord).
- Enterprise Grid — архитектура для крупных компаний с тысячами рабочих пространств.
5.2. Оценка нагрузки (Capacity Estimation)
Сообщения
- 1,5 млрд сообщений/день = ~17 360 сообщений/сек в среднем.
- Пиковая нагрузка (рабочие часы): ~50 000-70 000 сообщений/сек.
- Среднее сообщение: ~300 байт (текст + метаданные + форматирование).
- 1,5 млрд × 300 байт = ~450 ГБ/день новых данных сообщений.
База данных
MySQL (через Vitess) обслуживает:
- 2,3 млн QPS на пике.
- 2 млн чтений/сек и 300 тыс. записей/сек.
- Медианная латентность: 2 мс, P99: 11 мс.
Сравните с Discord: Discord хранит триллионы сообщений в ScyllaDB (NoSQL), а Slack — в MySQL (реляционная БД), шардированном через Vitess. Это фундаментально разные подходы с разными компромиссами.
WebSocket-соединения
- ~40-48 млн DAU, каждый держит WebSocket-соединение ~9 часов.
- В пике: ~30-35 млн одновременных WebSocket-соединений.
- Каждое соединение проходит через Flannel (edge cache) и Gateway Server.
Файлы
- Slack — это не только текст. Пользователи делятся файлами: документами, изображениями, PDF, таблицами.
- Предположим, 5% сообщений содержат файлы: 75 млн файлов/день.
- Средний файл: ~500 КБ → ~37 ТБ/день нового файлового контента.
5.3. Высокоуровневая архитектура
┌──────────────────────────────────────────────────────┐
│ Клиенты (Web / Desktop / iOS / Android) │
│ │
│ ┌───────────────────┐ ┌────────────────────────┐ │
│ │ HTTP (API) │ │ WebSocket (real-time) │ │
│ └─────────┬─────────┘ └───────────┬────────────┘ │
└────────────┼────────────────────────┼────────────────┘
│ │
┌────────▼────────┐ ┌─────────▼───────────┐
│ Cloudfront CDN │ │ Edge PoP │
│ (статика) │ │ (Flannel Cache │
└────────┬────────┘ │ + Gateway Server) │
│ └─────────┬───────────┘
┌────────▼────────┐ │
│ Envoy Proxy │◄──────────────┘
│ (маршрутизация │
│ в ячейки) │
└────────┬────────┘
│
┌────────▼─────────────────────────────────────────────────┐
│ Cell (Ячейка) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Webapp │ │ Channel │ │ Message │ │ │
│ │ │ Servers │ │ Servers │ │ Servers │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Vitess (MySQL шардинг) │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Redis │ │ Memcached│ │ Solr │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
Разберём каждый слой подробно.
5.4. Flannel — edge-кэш нового поколения
Проблема: медленный «холодный старт»
Когда вы открываете Slack, клиенту нужно загрузить огромное количество данных: список каналов, участники, непрочитанные сообщения, статусы присутствия, настройки и т. д. Это называется session boot (инициализация сессии). Для крупных workspace с тысячами каналов и десятками тысяч участников это могло занимать секунды.
Что такое Flannel
Flannel — это геораспределённый микросервис, развёрнутый на edge (точках присутствия по всему миру). Flannel решает проблему медленного session boot и одновременно является точкой терминации WebSocket-соединений.
Edge PoP (Point of Presence) — сервер, физически расположенный ближе к пользователю (например, в Москве, Франкфурте, Токио). Вместо того чтобы все запросы летели в us-east-1 (Вирджиния), часть работы выполняется на ближайшем PoP. Это снижает латентность на десятки-сотни миллисекунд.
Как работает Flannel
┌─────────────────────────────────────────────────────────────────┐
│ Flannel (Edge PoP) │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Pre-Warmed In-Memory Cache │ │
│ │ │ │
│ │ Workspace "Acme Corp": │ │
│ │ Channels: [#general, #engineering, #random, ...] │ │
│ │ Members: [user_1, user_2, ..., user_5000] │ │
│ │ Bots: [slackbot, jira-bot, github-bot] │ │
│ │ Prefs: {user_1: {theme: "dark", ...}, ...} │ │
│ │ │ │
│ │ Workspace "BigCorp": │ │
│ │ Channels: [...] │ │
│ │ Members: [...] │ │
│ └───────────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼───────────────────────────┐ │
│ │ WebSocket Termination │ │
│ │ - Принимает WebSocket от клиента │ │
│ │ - Валидирует сессию │ │
│ │ - Обслуживает session boot из кэша │ │
│ │ - Прокидывает real-time события │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Cache Updater │ │
│ │ - Слушает real-time события (новые сообщения, │ │
│ │ смена статуса, изменения каналов) │ │
│ │ - Инкрементально обновляет кэш │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Ключевые свойства Flannel:
-
Pre-warmed cache — кэш «предпрогрет»: данные workspace загружаются в память до того, как пользователь подключится. Когда пользователь открывает Slack — данные уже готовы.
-
Workspace affinity — маршрутизатор на edge PoP направляет пользователей одного workspace на один и тот же Flannel-инстанс. Это максимизирует cache hit rate.
Affinity routing (маршрутизация с привязкой) — стратегия, при которой запросы от одного источника (workspace, пользователь) всегда направляются на один и тот же сервер. Это повышает эффективность кэша: если все сотрудники «Acme Corp» попадают на один Flannel, то данные workspace кэшируются один раз и обслуживают всех.
-
Real-time cache updates — Flannel слушает WebSocket-события и инкрементально обновляет кэш. Не нужно перезагружать весь кэш при каждом изменении.
-
Session boot из кэша — вместо обращения к бэкенду в us-east-1, session boot обслуживается локально из Flannel. Это сократило время инициализации с секунд до миллисекунд.
5.5. Real-time обмен сообщениями
Двухкомпонентная архитектура
Slack-клиенты общаются с двумя типами бэкенд-серверов:
┌───────────────────────────────────────────────────────┐
│ Клиент Slack │
│ │
│ ┌─────────────────────┐ ┌──────────────────────┐ │
│ │ HTTP (REST API) │ │ WebSocket │ │
│ │ │ │ │ │
│ │ - Отправка │ │ - Получение новых │ │
│ │ сообщений │ │ сообщений │ │
│ │ - Загрузка файлов │ │ - Typing indicator │ │
│ │ - Управление │ │ - Presence updates │ │
│ │ каналами │ │ - Реакции │ │
│ │ - Поиск │ │ - Channel updates │ │
│ └─────────┬───────────┘ └───────────┬──────────┘ │
└────────────┼────────────────────────────┼─────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Webapp Servers │ │ Gateway Servers │
│ (request/resp.) │ │ (stateful, │
│ │ │ in-memory) │
└──────────────────┘ └──────────────────┘
1. Webapp Servers — обрабатывают HTTP-запросы: отправка сообщений, загрузка файлов, управление каналами, API-вызовы от ботов. Это stateless серверы (без состояния) — любой запрос может обработать любой сервер.
2. Gateway Servers (GS) — поддерживают WebSocket-соединения с клиентами. Они stateful (с состоянием): каждый GS хранит в памяти информацию о подключённых пользователях и их подписках на каналы. GS развёрнуты в нескольких регионах.
Путь сообщения
User_A отправляет "Привет!" в #general:
1. User_A ──HTTP POST──→ Webapp Server
│
2. Webapp Server:
- Валидирует (есть ли доступ к #general?)
- Сохраняет в MySQL (через Vitess)
- Публикует событие в Channel Server
│
3. Channel Server (CS):
- Определяет получателей
(все подписчики #general,
которые сейчас онлайн)
- Fan-out: отправляет событие
Gateway Servers'ам получателей
│
4. Gateway Server:
- Получает событие
- Находит WebSocket-соединения
получателей
- Отправляет через WebSocket
│
5. User_B, User_C, ... ← получают "Привет!"
через WebSocket в реальном времени
Channel Servers
Channel Servers (CS) — ключевой компонент real-time архитектуры Slack. Каждый Channel Server отвечает за определённый набор каналов и знает, какие Gateway Servers держат подписчиков этих каналов.
┌─────────────────────────────────────────────────────┐
│ Channel Server (CS) │
│ │
│ Channel #general: │
│ Subscribers on GS-1: [User_A, User_B, User_C] │
│ Subscribers on GS-2: [User_D, User_E] │
│ Subscribers on GS-3: [User_F] │
│ │
│ Channel #engineering: │
│ Subscribers on GS-1: [User_A, User_C] │
│ Subscribers on GS-2: [User_D] │
│ │
│ При новом сообщении в #general: │
│ → Отправить на GS-1 (3 получателя) │
│ → Отправить на GS-2 (2 получателя) │
│ → Отправить на GS-3 (1 получатель) │
│ │
│ CS отправляет 3 сообщения (по числу GS), │
│ а не 6 (по числу получателей) — это оптимизация! │
└─────────────────────────────────────────────────────┘
Обратите внимание на оптимизацию: CS отправляет одно сообщение на Gateway Server, а не на каждого получателя. GS сам выполняет локальный fan-out до подключённых клиентов. Это уменьшает количество сетевых вызовов в разы.
Сравните с Discord: в Discord Guild Process отправляет события через Relay Processes (до 15 000 пользователей на Relay). В Slack Channel Server отправляет события через Gateway Servers. Концепция похожа, но реализация отличается: Discord использует Elixir/BEAM процессы, а Slack — отдельные сервисы.
Typing Indicators
Когда вы видите «User_A печатает...» в канале — это typing indicator. Это одно из самых частых событий в Slack, но оно не сохраняется в базе данных — только передаётся через WebSocket.
User_A начинает печатать
│
│ WebSocket: {type: "typing", channel: "#general", user: "User_A"}
▼
Gateway Server (User_A)
│
│ Перенаправляет в Channel Server
▼
Channel Server (#general)
│
│ Fan-out на все Gateway Servers с подписчиками #general
│ (НЕ сохраняет в БД — это эфемерное событие)
▼
User_B, User_C видят "User_A печатает..."
Эфемерное событие (ephemeral event) — событие, которое передаётся в реальном времени, но не сохраняется на сервере. Typing indicators, статусы присутствия (is_typing, is_online) — эфемерные. Сообщения, файлы, реакции — персистентные (сохраняются навсегда).
5.6. Хранение данных: MySQL + Vitess
Почему MySQL, а не NoSQL
Slack использует MySQL как основное хранилище. Это может показаться неожиданным — ведь Discord выбрал ScyllaDB (NoSQL), а TikTok — ByteGraph. Почему Slack остался на реляционной БД?
- ACID-транзакции — корпоративные клиенты требуют гарантий целостности данных. Потеря сообщения в Slack для компании = потеря рабочей коммуникации.
- Сложные запросы — поиск с учётом прав доступа, compliance-запросы (eDiscovery), аудит — всё это проще реализовать в SQL.
- Шардинг через Vitess — горизонтальное масштабирование MySQL решается на уровне инфраструктуры, а не на уровне приложения.
Vitess — горизонтальный шардинг MySQL
Vitess — система горизонтального масштабирования MySQL, изначально разработанная в YouTube/Google. Slack мигрировал 99% MySQL-трафика на Vitess.
Vitess — промежуточный слой (middleware) между приложением и MySQL, который: (1) автоматически шардирует данные по таблицам; (2) маршрутизирует запросы на нужный шард; (3) управляет failover (переключение на реплику при отказе мастера); (4) обеспечивает пул соединений (connection pooling). Приложение обращается к Vitess как к одной большой БД, а Vitess внутри распределяет запросы по тысячам шардов.
┌─────────────────────────────────────────────────────────────┐
│ Приложение │
│ SQL: SELECT * FROM messages WHERE channel_id = 42 │
│ AND created_at > '2025-01-01' │
└──────────────────────────┬──────────────────────────────────┘
│ (MySQL protocol)
▼
┌─────────────────────────────────────────────────────────────┐
│ VtGate │
│ (маршрутизатор запросов) │
│ │
│ Определяет: channel_id=42 → Shard 7 │
│ Перенаправляет запрос на Shard 7 │
└──────────────────────────┬──────────────────────────────────┘
│
┌──────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Shard 1 │ │ Shard 7 │ │ Shard N │
│ (MySQL) │ │ (MySQL) │◄── │ (MySQL) │
│ │ │ │ │ │
│ Primary │ │ Primary │ │ Primary │
│ + Replica│ │ + Replica│ │ + Replica│
└──────────┘ └──────────┘ └──────────┘
Стратегия шардинга
Slack шардирует данные по нескольким осям:
| Данные | Ключ шардирования | Почему |
|---|---|---|
| Сообщения | channel_id | Все сообщения одного канала рядом — быстрое чтение истории |
| Каналы | channel_id | Метаданные канала рядом с сообщениями |
| Пользователи | user_id | Профиль, настройки, токены — по пользователю |
| Workspaces | workspace_id | Настройки workspace, тарифы, compliance |
Обратите внимание: исторически Slack шардировал всё по
workspace_id— данные одной организации жили на одном шарде. Это было простым решением, но создавало проблему: если одна организация (например, IBM с 350 000 сотрудников) генерировала огромную нагрузку, весь шард перегружался. Переход на более гранулярное шардирование (поchannel_id,user_id) был одной из ключевых архитектурных миграций Slack.
Модель данных
-- Таблица сообщений (упрощённо) CREATE TABLE messages ( message_id BIGINT PRIMARY KEY, -- Snowflake-like ID channel_id BIGINT NOT NULL, -- Шардинг-ключ user_id BIGINT NOT NULL, text TEXT, thread_ts BIGINT, -- NULL если не в треде edited_at TIMESTAMP, created_at TIMESTAMP NOT NULL, INDEX idx_channel_time (channel_id, created_at DESC) ); -- Таблица каналов CREATE TABLE channels ( channel_id BIGINT PRIMARY KEY, workspace_id BIGINT NOT NULL, name VARCHAR(80), topic TEXT, purpose TEXT, is_private BOOLEAN DEFAULT FALSE, is_shared BOOLEAN DEFAULT FALSE, -- Slack Connect created_at TIMESTAMP NOT NULL ); -- Таблица участников канала CREATE TABLE channel_members ( channel_id BIGINT NOT NULL, user_id BIGINT NOT NULL, last_read_ts BIGINT, -- Аналог Read States в Discord PRIMARY KEY (channel_id, user_id) );
Производительность
На пике Vitess обрабатывает:
- 2,3 млн QPS (запросов в секунду).
- 2 млн операций чтения/сек, 300 тыс. операций записи/сек.
- Медианная латентность: 2 мс.
- P99 латентность: 11 мс.
Эти числа достигаются благодаря тысячам MySQL-шардов, распределённых по сотням серверов.
5.7. Cellular Architecture — ячеистая архитектура
Предыстория: инцидент 30 июня 2021
30 июня 2021 года у AWS произошёл сетевой сбой в одной из Availability Zones (AZ) в регионе us-east-1. Slack, инфраструктура которого размещалась в нескольких AZ этого региона, испытал серьёзную деградацию: соединения между серверами в разных AZ стали нестабильными, что привело к каскадным отказам.
Availability Zone (AZ) — физически изолированный дата-центр внутри одного региона AWS. Например, us-east-1 содержит 6 AZ (us-east-1a, us-east-1b, ...). AZ подключены друг к другу высокоскоростными каналами, но спроектированы так, чтобы отказ одной AZ не влиял на другие. На практике, как показал инцидент Slack, сетевые проблемы между AZ всё же могут вызвать каскадные сбои.
Этот инцидент стал триггером для миграции на cellular architecture (ячеистую архитектуру).
Что такое Cellular Architecture
Cellular Architecture — архитектурный паттерн, при котором инфраструктура разделяется на независимые ячейки (cells), каждая из которых содержит полный набор компонентов для обслуживания части трафика. Отказ одной ячейки не затрагивает другие.
Было: Multi-AZ (общая инфраструктура)
┌────────────────────────────────────────────────┐
│ us-east-1 │
│ │
│ AZ-a AZ-b AZ-c │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ Web │◄────►│ Web │◄────►│ Web │ ← общие │
│ │ DB │◄────►│ DB │◄────►│ DB │ ресурсы │
│ └─────┘ └─────┘ └─────┘ │
│ │
│ Проблема: сбой в AZ-b → деградация ВСЕГО │
└────────────────────────────────────────────────┘
Стало: Cellular Architecture (изолированные ячейки)
┌──────────────────────────────────────────────┐
│ us-east-1 │
│ │
│ ┌─ Cell A (AZ-a) ─┐ ┌─ Cell B (AZ-b) ─┐ │
│ │ Web │ │ Web │ │
│ │ Channel Server │ │ Channel Server │ │
│ │ MySQL (Vitess) │ │ MySQL (Vitess) │ │
│ │ Redis │ │ Redis │ │
│ │ Memcached │ │ Memcached │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ ┌─ Cell C (AZ-c) ─┐ │
│ │ Web │ Сбой в Cell B → │
│ │ Channel Server │ Cell A и Cell C │
│ │ MySQL (Vitess) │ продолжают работать! │
│ │ Redis │ │
│ │ Memcached │ │
│ └──────────────────┘ │
└──────────────────────────────────────────────┘
Как работает маршрутизация
Трафик распределяется по ячейкам с помощью Envoy (высокопроизводительный L7-прокси) и xDS (протокол динамической конфигурации):
Клиент Slack
│
▼
┌──────────────────┐
│ Envoy Proxy │
│ (edge layer) │
│ │
│ workspace_id → │
│ Cell mapping: │
│ Acme → Cell A │
│ BigCorp → Cell B│
│ Startup → Cell C│
└────────┬─────────┘
│
┌────────┼────────┐
▼ ▼ ▼
Cell A Cell B Cell C
Envoy — высокопроизводительный прокси-сервер, созданный в Lyft. Используется как edge proxy, service mesh sidecar и load balancer. Envoy поддерживает динамическую конфигурацию через xDS API — это значит, что правила маршрутизации можно менять «на лету», без перезапуска.
При обнаружении проблем в ячейке, трафик переключается на здоровые ячейки за секунды — через обновление конфигурации Envoy.
Blast Radius Isolation
Blast Radius (радиус поражения) — объём системы, затронутый отказом одного компонента. Цель cellular architecture — минимизировать blast radius: если ячейка упала, затронуты только workspace'ы, обслуживаемые этой ячейкой (например, 33% пользователей), а не все 100%.
Ключевое правило: компоненты внутри ячейки не зависят от компонентов в других ячейках. Никаких cross-cell вызовов в критическом пути обработки запроса.
5.8. Slack Connect и Shared Channels
Проблема: workspace как граница мира
Изначально в Slack каждый workspace — это «остров»: все данные изолированы, пользователи одного workspace не видят данные другого. Это прекрасно для безопасности, но создаёт проблему: как компаниям общаться друг с другом?
Решение: Shared Channels
Slack Connect позволяет создать канал, который соединяет до 250 организаций. Пользователи из разных компаний общаются в одном канале, как если бы они были коллегами.
┌──────────────────┐ ┌─────────────────┐
│ Workspace A │ │ Workspace B │
│ (Acme Corp) │ │ (BigCorp) │
│ │ │ │
│ #shared-project │◄──►│ #acme-collab │
│ (public) │ │ (private) │
│ │ │ │
│ User_A1 │ │ User_B1 │
│ User_A2 │ │ User_B2 │
└──────────────────┘ └─────────────────┘
Один и тот же канал, но:
- Разные названия (#shared-project vs #acme-collab)
- Разные настройки приватности (public vs private)
- Разные политики хранения данных
- Разные правила compliance
Архитектурный вызов
Shared Channels сломали фундаментальное допущение архитектуры Slack: что workspace — это атомарная единица разделения данных. Если канал принадлежит нескольким workspace'ам, то:
- На каком шарде хранить сообщения?
- Как обеспечить права доступа, если у каждого workspace свои настройки?
- Как фанаутить сообщения на Gateway Servers в разных ячейках?
Решение: Channel-Centric Model
Slack перестроил модель данных вокруг канала как центральной сущности, а не workspace:
Старая модель (workspace-centric):
Workspace A Workspace B
┌──────────┐ ┌──────────┐
│ Shard A │ │ Shard B │
│ │ │ │
│ Channels │ │ Channels │
│ Messages │ │ Messages │
│ Users │ │ Users │
└──────────┘ └──────────┘
Новая модель (channel-centric):
Channel Shard User Shard A User Shard B
┌──────────────┐ ┌──────────┐ ┌──────────┐
│ Channel data │ │ User_A1 │ │ User_B1 │
│ Messages │ │ User_A2 │ │ User_B2 │
│ Members list │ │ Prefs │ │ Prefs │
└──────────────┘ └──────────┘ └──────────┘
Теперь данные канала (сообщения, участники) шардируются по channel_id, а не по workspace_id. Это позволяет каналу принадлежать нескольким workspace'ам без дублирования данных.
5.9. Unified Grid — переархитектурирование для крупных клиентов
Проблема: Enterprise Grid
Крупные компании (IBM, Oracle, Salesforce) имеют сотни workspace'ов. Сотрудник такой компании может быть членом 50+ workspace'ов. При старой архитектуре:
- Каждый workspace = отдельное WebSocket-соединение.
- 50 workspace'ов = 50 WebSocket-соединений от одного клиента.
- Переключение между workspace'ами = переподключение к другому бэкенду.
Это было медленно, дорого и создавало плохой UX.
Решение: Unified Grid
Unified Grid — фундаментальная переработка архитектуры Slack, завершённая в марте 2024 года. Основные изменения:
1. Одно соединение — все workspace'ы
Было: Стало:
Client Client
│ │
├─ WS ─→ Workspace A └─ WS ─→ Unified Gateway
├─ WS ─→ Workspace B │
├─ WS ─→ Workspace C ├─ Workspace A
└─ WS ─→ Workspace D ├─ Workspace B
├─ Workspace C
4 WebSocket-соединения └─ Workspace D
(переключение = задержка)
1 WebSocket-соединение
(переключение мгновенно)
2. Org-wide данные без дублирования
Раньше: если организация «Acme Corp» насчитывает 100 workspace'ов, и в каждом нужно обновить уведомление — событие фанаутится 100 раз.
Теперь: real-time messaging стек был переработан так, что org-wide данные (профиль пользователя, DMs, уведомления) не дублируются в каждом workspace.
3. Миграция данных через Vitess
Unified Grid стал возможен благодаря миграции на Vitess: теперь данные можно шардировать по channel_id и user_id, а не только по workspace_id. Vitess обеспечивает прозрачное изменение схемы шардирования без даунтайма.
5.10. Поиск
Требования к поиску в Slack
Поиск в корпоративном мессенджере — это не просто «найти текст». Он должен:
- Учитывать права доступа — показывать результаты только из каналов, к которым пользователь имеет доступ.
- Работать с файлами — индексировать содержимое PDF, документов, таблиц.
- Поддерживать фильтры — по автору, каналу, дате, типу файла.
- Быть быстрым — результаты за миллисекунды.
- Соответствовать compliance — в Enterprise Grid администраторы могут искать по всем данным организации (eDiscovery).
Архитектура: Solr
Slack использует Apache Solr (поисковый движок на базе Lucene) для полнотекстового поиска.
Apache Solr — enterprise-grade поисковая платформа с открытым исходным кодом, построенная поверх Apache Lucene. Solr добавляет к Lucene: распределённый поиск (SolrCloud), фасетный поиск, кэширование, rich-text индексирование (PDF, Word), и REST API. Solr используется в Netflix, Apple, Bloomberg и других крупных компаниях.
┌────────────────────────────────────────────────────────────────┐
│ Поисковый запрос │
│ "quarterly report from:@john in:#finance after:2025-01-01" │
└───────────────────────────────┬────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ Query Parser │
│ │
│ Разбирает на компоненты: │
│ text: "quarterly report" │
│ from: user_john_id │
│ channel: channel_finance_id │
│ date_range: > 2025-01-01 │
│ + visibility_filter: channels, доступные user_current │
└───────────────────────────────┬────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────┐
│ Solr Query Execution │
│ │
│ 1. Полнотекстовый поиск по "quarterly report" │
│ 2. Фильтр по user_id автора │
│ 3. Фильтр по channel_id │
│ 4. Фильтр по дате │
│ 5. Фильтр видимости: │
│ - Публичные каналы workspace'а │
│ - Приватные каналы, где user — участник │
│ - DMs с user │
│ 6. Ранжирование: relevance × recency × popularity │
└───────────────────────────────┬────────────────────────────────┘
│
▼
Результаты поиска
(сообщения + файлы)
Индексирование файлов
Когда пользователь загружает файл в Slack, его содержимое извлекается и индексируется:
- PDF → извлекается текст с помощью Apache Tika.
- Word/Excel → аналогично.
- Изображения → OCR (опционально в Enterprise).
- Код → индексируется как plain text.
Файл индексируется как дочерний документ (child document) сообщения, в котором он был поделён. Это позволяет учитывать контекст: в каком канале и кем был поделён файл.
Visibility Filters
Ключевая сложность поиска в Slack — фильтрация по правам доступа при каждом запросе:
Для каждого результата поиска:
IF message.channel.type == "public":
visible = user.workspace_id == channel.workspace_id
ELIF message.channel.type == "private":
visible = user IN channel.members
ELIF message.channel.type == "dm":
visible = user IN dm.participants
ELIF message.channel.type == "shared" (Slack Connect):
visible = user IN channel.members
AND user's org has access
→ Показать только visible == true
Этот фильтр применяется на уровне Solr-запроса, а не после получения результатов. Это критически важно для производительности: Solr не должен вытаскивать миллионы документов, чтобы потом отфильтровать 99% из них.
5.11. Интеграции и боты
Масштаб экосистемы
Slack имеет одну из самых развитых экосистем интеграций:
- 2 600+ приложений в App Directory.
- 750 000+ кастомных приложений, созданных компаниями.
- Каждый день: 5 млрд+ действий, включая автоматизированные.
Типы интеграций
┌────────────────────────────────────────────┐
│ Slack Platform │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Incoming │ │ Outgoing │ │
│ │ Webhooks │ │ Webhooks │ │
│ │ │ │ │ │
│ │ Внешняя система │ │ Slack → │ │
│ │ → Slack │ │ Внешняя система │ │
│ │ (GitHub push → │ │ (сообщение → │ │
│ │ #dev-alerts) │ │ создать задачу │ │
│ │ │ │ в Jira) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Slash Commands │ │ Bot Users │ │
│ │ │ │ │ │
│ │ /deploy prod │ │ Полноценный бот │ │
│ │ /jira create │ │ с WebSocket │ │
│ │ /poll "Когда?" │ │ (как Discord │ │
│ │ │ │ боты) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Workflow Builder│ │ Block Kit │ │
│ │ │ │ │ │
│ │ No-code автомат.│ │ Интерактивные │ │
│ │ (если X → то Y) │ │ UI-компоненты │ │
│ │ │ │ (кнопки, формы, │ │
│ │ │ │ дропдауны) │ │
│ └─────────────────┘ └─────────────────┘ │
└────────────────────────────────────────────┘
Webhook — HTTP-callback: когда происходит событие, сервер-источник отправляет HTTP POST-запрос на URL, указанный получателем. Incoming Webhook: внешняя система отправляет данные в Slack (например, GitHub → Slack при новом PR). Outgoing Webhook: Slack отправляет данные из Slack во внешнюю систему при определённом триггере.
Rate Limiting для приложений
Slack применяет tiered rate limiting — разные лимиты для разных типов методов:
| Tier | Лимит | Примеры методов |
|---|---|---|
| Tier 1 (специальные) | 1 запрос/мин | admin.teams.create |
| Tier 2 (запись) | 20 запросов/мин | chat.postMessage, files.upload |
| Tier 3 (чтение) | 50 запросов/мин | conversations.history |
| Tier 4 (лёгкие) | 100+ запросов/мин | users.info |
При превышении лимита Slack возвращает HTTP 429 с заголовком Retry-After.
5.12. Сравнение с предыдущими системами
| Аспект | TikTok | Discord | Slack | ||
|---|---|---|---|---|---|
| Основная БД | Espresso | ByteGraph + S3 | PostgreSQL | ScyllaDB | MySQL (Vitess) |
| Шардинг | memberId | videoId | videoId/userId | channel bucket | channel_id (Vitess) |
| Кэш | Memcached | Redis | Memcached | ETS (BEAM) | Flannel + Memcached |
| Real-time | Ограниченно | Нет | Нет | Elixir Gateway | Flannel + GS + CS |
| Edge | CDN | Multi-CDN | Fastly | Cloudflare | Flannel (edge PoP) |
| Отказоустойчивость | Репликация | Multi-region | K8s | Consistent Hash | Cellular Architecture |
| Поиск | Galene (Lucene) | Мультимодальный | Elasticsearch | Elasticsearch | Solr (Lucene) |
| Кросс-организации | Нет | Нет | Нет | Нет | Slack Connect |
| Язык бэкенда | Java | Go / Python | Go | Elixir/Rust/C++ | PHP/Hack → Java/Go |
| Хостинг | Свои ДЦ | Гибрид | AWS | GCP | AWS (cellular) |
5.13. Итоговая архитектура
┌──────────────────────────────────────────────────────────────────┐
│ Клиенты (Web / Desktop / iOS / Android) │
│ │
│ ┌────────────────────┐ ┌─────────────────────┐ │
│ │ HTTP REST API │ │ WebSocket (unified) │ │
│ └─────────┬──────────┘ └──────────┬──────────┘ │
└─────────────┼───────────────────────────┼────────────────────────┘
│ │
┌────────▼────────┐ ┌──────────▼───────────┐
│ CloudFront │ │ Edge PoP │
│ CDN │ │ ┌─────────────────┐ │
└─────────────────┘ │ │ Flannel │ │
│ │ (pre-warmed │ │
│ │ edge cache, │ │
│ │ WS termination│ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Gateway Servers │ │
│ │ (stateful, │ │
│ │ in-memory) │ │
│ └────────┬────────┘ │
└───────────┼──────────┘
│
┌───────────▼──────────┐
│ Envoy Proxy │
│ (cell routing) │
└───────────┬──────────┘
│
┌─────────────────────┼────────────────────┐
│ │ │
┌─────────▼───────┐ ┌─────────▼───────┐ ┌─────────▼───────┐
│ Cell A │ │ Cell B │ │ Cell C │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Webapp │ │ │ │ Webapp │ │ │ │ Webapp │ │
│ │ Channel │ │ │ │ Channel │ │ │ │ Channel │ │
│ │ Servers │ │ │ │ Servers │ │ │ │ Servers │ │
│ ├───────────┤ │ │ ├───────────┤ │ │ ├───────────┤ │
│ │ Vitess │ │ │ │ Vitess │ │ │ │ Vitess │ │
│ │ (MySQL) │ │ │ │ (MySQL) │ │ │ │ (MySQL) │ │
│ ├───────────┤ │ │ ├───────────┤ │ │ ├───────────┤ │
│ │Redis Solr │ │ │ │Redis Solr │ │ │ │Redis Solr │ │
│ │Memcached │ │ │ │Memcached │ │ │ │Memcached │ │
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌──────────────────────────────────────────┐
│ Apache Kafka │
│ (CDC, аналитика, async processing) │
└──────────────────────────────────────────┘
5.14. Ключевые уроки и принципы
1. MySQL может масштабироваться до миллионов QPS
С Vitess Slack обрабатывает 2,3 млн QPS на MySQL. Реляционная БД — не приговор для масштабируемости. Ключ — правильный шардинг и промежуточный слой.
2. Edge cache — game changer для UX
Flannel превратил «холодный старт» (секунды) в мгновенную загрузку. Перенос данных ближе к пользователю через edge PoP — одна из самых высокоэффективных оптимизаций.
3. Cellular Architecture — страховка от каскадных сбоев
Изоляция ячеек гарантирует, что проблема в одной AZ не обрушит всю систему. Стоимость: дублирование инфраструктуры. Выигрыш: секунды на переключение трафика вместо часов на восстановление.
4. Фундаментальные допущения могут стать тюрьмой
Slack годами строился на допущении «workspace = атомарная единица». Это создало проблемы при введении Shared Channels и Enterprise Grid. Перестройка модели данных (workspace-centric → channel-centric) была болезненной, но необходимой.
5. Разделяй stateful и stateless
Webapp Servers (stateless) и Gateway Servers (stateful) масштабируются по-разному. Stateless легко горизонтально масштабировать (добавить серверов). Stateful требует workspace affinity и более аккуратного управления.
6. Compliance — архитектурное требование, а не фича
В корпоративном мессенджере данные нельзя «просто удалить» или «потерять». ACID-транзакции MySQL, eDiscovery через Solr, права доступа при каждом поисковом запросе — всё это вплетено в архитектуру, а не добавлено сверху.
7. Channel Server + Gateway Server — элегантная оптимизация fan-out
Вместо отправки одного сообщения каждому получателю, Channel Server отправляет одно сообщение на Gateway Server, а GS выполняет локальный fan-out. Это сокращает количество сетевых вызовов с N (число получателей) до K (число Gateway Servers).
5.15. Вопросы для самопроверки
- Почему Slack использует MySQL + Vitess, а не NoSQL (как Discord с ScyllaDB)? Какие преимущества даёт реляционная БД для корпоративного мессенджера?
- Что такое Flannel и какую проблему он решает? Чем он отличается от обычного CDN?
- Объясните разделение ролей между Webapp Servers, Channel Servers и Gateway Servers. Почему три компонента, а не один?
- Что такое Cellular Architecture и почему Slack мигрировал на неё после инцидента 2021 года? Как blast radius isolation работает на практике?
- Как Slack Connect (Shared Channels) сломал допущение «workspace = единица шардирования»? Как это было решено?
- Объясните, как Solr обеспечивает поиск с учётом прав доступа. Почему visibility filter применяется на уровне запроса, а не после получения результатов?
- Что такое Unified Grid? Какую проблему Enterprise Grid создавал при старой архитектуре (множество WebSocket-соединений)?
- Сравните подходы к real-time доставке сообщений в Slack (Flannel + GS + CS) и Discord (Elixir Gateway + Guild Process + Relays). Какие компромиссы у каждого подхода?
- Почему Slack перешёл на шардирование по
channel_idвместоworkspace_id? Какие проблемы создавал старый подход? - Как typing indicators обрабатываются иначе, чем обычные сообщения? Почему это важно для производительности?
Источники
- Scaling Datastores at Slack with Vitess — Slack Engineering
- Real-Time Messaging — Slack Engineering
- Real-Time Messaging Architecture at Slack — InfoQ
- Flannel: An Application-Level Edge Cache to Make Slack Scale — Slack Engineering
- Slack's Migration to a Cellular Architecture — Slack Engineering
- Slack Migrates to Cell-Based Architecture on AWS to Mitigate Gray Failures — InfoQ
- Unified Grid: How We Re-Architected Slack for Our Largest Customers — Slack Engineering
- Changing the Model: Why and How We Re-Architected Slack — InfoQ
- How Slack Built Shared Channels — Slack Engineering
- Managing Slack Connect — Slack Engineering
- Search at Slack — Slack Engineering
- The Query Strikes Again — Slack Engineering
- Building the Next Evolution of Cloud Networks at Slack — Slack Engineering
- Traffic 101: Packets Mostly Flow — Slack Engineering
- Scaling Slack — The Good, the Unexpected, and the Road Ahead — InfoQ
- How Slack Supports Billions of Daily Messages — ByteByteGo
- Slack Statistics 2025 — DemandSage
- Slack Statistics 2025 — SQMagazine
- Slack Statistics 2025 — Notta