Содержание всех статей
Содержание
О чей пойдет речь
В этой статье хочу рассказать, как технически устроен мой Telegram чат для сайта, из каких компонентов состоит и с какими подводными камнями я сталкивался.
Может сложиться впечатление, что чат для сайта - это простое приложение. На самом деле, зависит от стадии развития проекта. Когда пет-проект превращается именно в IT продукт с платящими пользователями, резко возрастают требования к качеству.
Даже при 100 активных виджетах появляются нетривиальные задачи: и контроль нагрузки, и оптимизация отправки сообщений в Telegram (с его ограничениями), и администрирование пользователей, и контроль оплат. Дополнительно появляются проблемы в виде всяких говнюков DDOS атак, XSS и разных попыток поломать приложение.
В результате чат превращается во вполне себе ощутимую систему c кучей компонентов и всевозможных защит. Причем проект разрастается даже всего за год работы в очень медленном режиме.
Архитектура
Концептуально архитектура чата выглядит как на диаграмме ниже.
Уточнения:
Указаны только основные библиотеки и фреймворки, маленькие не указаны.
И клиентская часть (SSR), и серверная (API) работает в многопроцесcном режиме.
Пока что всё находится на одном сервере, так как запас по лимиту вертикального масштабирования еще довольно большой. Но заранее есть основа под разнесение каждого компонента на разные серверы.
![Архитектура проекта Архитектура проекта](https://habrastorage.org/getpro/habr/upload_files/681/452/15f/68145215f20f4f007556b8df4e0c17af.png)
Для сравнения, архитектура MVP
![Архитектура первой версии Архитектура первой версии](https://habrastorage.org/getpro/habr/upload_files/4a7/8c3/d6b/4a78c3d6bd93d86064fc81317e231646.png)
В первой статье я говорил кратко про все компоненты выше, но расскажу еще раз (слева направо):
DDOS-защита и CDN
Использую DDOS-Guard (насколько я знаю, они самые крупные в СНГ после Cloudflare). Недорого, удобно. В дополнение на уровне Nginx и на уровне API есть “локальная” защита. Срабатывает чуть быстрее в некритичных случаях.
Down detector
Самодельная программа, которая находится на другом сервере. Проверяет, доступен ли сайт, виджет и API. В случае, если что-то не так, мне в Telegram прилетает уведомление.
Вот так выглядит чат с оповещениями:
![Сюда приходят оповещения о поломках Сюда приходят оповещения о поломках](https://habrastorage.org/getpro/habr/upload_files/5ce/8ae/78f/5ce8ae78ff2c1a901796030bb5ae4fb6.png)
Я добавил в чат еще трех человек, которые практически не пересекаются по свои биоритмам. Следовательно, в любое время суток хоть кто-то увидит сообщение, если я сплю или занят, и позвонят мне.
Контейнеры и сборка
Docker + Docker Compose. Потенциально каждый из компонентов может быть спокойно разнесет каждый на свой сервер в случае необходимости (но в ближайшие годы я ее не вижу).
Reverse proxy и SSL
Nginx. Лично для меня самое удобное и производительное. Легко настраивается для сокетов, легко настраивается под провайдера DDOS защиты, быстро раздает статику.
Сайт и личный кабинет пользователя
Основная задача сайта, после хорошего пользовательского опыта - это удовлетворят требованиям поисков для продвижения в SEO выдаче. Следовательно, необходим SSR и быстрая отрисовка. Здесь также находится личный кабинет пользователя и прием платежей.
Для защиты от ботов при регистрации используется каптча от Яндекса. Бесплатно, интеграция стандартная за 30 минут.
Технологии: TypeScript, NextJS (серверный рендеринг) + React, cluster mode для NextJS с одни процессом на одно ядро. CSS библиотеки не используются, чтобы быстрее грузился сайт, ограничиваюсь React CSS Module.
Админка с аналитикой и менеджментом пользователей
Исторически сложилось, что это отдельный фронт. Здесь у меня просмотр логов, пользователей, статистики и т.д.
Технологии: TypeScript, React, Bootstrap 4.
Выглядит примерно так:
![Просмотр пользователей Просмотр пользователей](https://habrastorage.org/getpro/habr/upload_files/57b/053/2dd/57b0532dd0034bb727d0b55582d0bded.png)
Для входа администратора в аккаунт пользователя есть возможность копировать прямую ссылку с токеном для входа. Копирую через кнопку, вставляю в инкогнито и оказываюсь в личном кабинете пользователя. Итого, вход за 5 секунд.
Это оказалась необходимая функция на начальном этапе, чтобы с точки зрения пользователя смотреть, какие виджеты созданы и как настроены. Иначе было очень долго оказывать техническую поддержку, так как на каждый чих приходилось писать SQL запрос.
Отдельно уточню про логи и ошибки:
Логи для важных функций пишутся прямо в БД с ограниченным временем и объемом хранения.
Критичные ошибки (с платежной системой, с API Telegram’a) прилетают мне сразу в Telegram.
На отдельном сервере у меня стоит downdetector, чтобы сразу среагировать, если сайт, виджет или API недоступны.
Виджет для сайта
Собственно то, что и является конечным продуктом для клиента. Весь код компилируется в один .js файл, который клиенты вставляют на свой сайт.
Технологии: TypeScript, PreactJS, SocketIO. Особо добавить нечего.
Серверная часть с API
Здесь сосредоточена вся логика. Ее довольно много, но в детали углубляться смысла нет. Обычное приложение со своими нюансами.
Тестами покрыто ~5%-10% серверной части. Пет-проект все-таки ?. Покрыл только ключевые компоненты: взаимодействие с Telegram, взаимодействие с платежными системами и стресс-тест.
Технологии: TypeScript, NodeJS + NestJS, SocketIO, PM2 (менеджмент процессов), Jest (unit тесты) + Supertest (E2E тесты).
База данных
PostgreSQL 14. Не MySQL, потому что нынче проблемы с Oracle, мне более спокойно со 100% open source’ом. Не MariaDB, потому что PostgreSQL популярнее, в среднем быстрее, да и никогда не работал с ней.
Откровенно говоря, я еще не сталкивался с такими нагрузками на реляционную БД, где действительно был бы критичным выбор между MySQL или PostgreSQL, и при этом не нужно было смотреть в сторону альтернативных NoSQL баз.
Пересказать теоретические статьи на тему "PostgreSQL MySQL performance comparison" я могу, но вот в жизни еще не сталкивался с такими вопросами, где это имело бы значение. Поэтому выбор был по принципу "что популярнее, удобнее и с чем давно работаю".
Резервные копии
Делаются несколько раз в день стандартными средствами от FirstVDS + стандартным pg_dump + zip. Проверку восстановления из бекапов провожу примерно раз в месяц.
Кто не знает, довольно частая ситуация, когда бекапы настроили, вроде всё делается, а во время проблем оказывается, что или бекапы не такие, или восстановить нельзя.
Брокер сообщений и in-memory key-value хранилище
Redis. Не RabbitMQ для сообщений, потому что конкретно под этот проект базового механизма pub/sub в Redis хватает с головой. Учитывая, что и key-value, и брокинг в одном месте - выбрал Redis.
Тут нужно уточнение про виджет и Redis. Они взаимосвязаны.
Виджет подключается к серверу по сокетам. Так сообщения из Telegram отображаются быстрее, сервер не грузится лишний раз из-за long pooling’a.
Так как API запущено в несколько процессов (с разными зонами памяти), сокеты имеют свойство подключаться к разным процессам. Telegram доставляет данные так же в разные процессы. В результате процесс сокета и процесс с ответом от Telegram’a имеют свойство не сходится.
Чтобы их сопоставить, используется механизм publisher и subscriber в Redis. И сокет, и слушатель Telegram ответов подписываются на прослушивание ответов по своим ID. Как только приходит ответ от Telegram, Redis прокидывает сообщение в нужный сокет.
Дополнительно Redis используется для debounc’a, приоритизирования сообщений в Telegram и немного помогает с превентивной DDOS защитой.
Глобально с “концептуальной стороны” архитектура выглядит так. Далее расскажу про детали специфические для этого проекта.
Зачем оптимизировать виджет?
В мире веба существуют показатели Web Vitals. Они измеряются с помощью Google Page Speed Insights (на самом деле не совсем так, но неважно). Пример как раз моего сайта:
![Показатели Google Page Speed для Telegram Feedback Показатели Google Page Speed для Telegram Feedback](https://habrastorage.org/getpro/habr/upload_files/dfb/b65/04e/dfbb6504e33dea4653df3751ef9d9b92.png)
Любой скрипт, компонент, лишняя картинка или виджет тормозят сайт. Показатели падают, пробиться в SEO выдачу становится сложнее. А это одна из основных задач всех сайтов. При том, что практически каждый сайт обвешан Google Analytics, Яндекс Метрикой, медленными скрипами и разными CSS библиотеками.
У меня стояла задача сделать виджет насколько быстрым и маленьким (в плане скорости загрузки на сайт), чтобы он не влиял или почти не влиял на скорость. Если на этапе MVP виджет был более 500кб, включал несколько жирных библиотек и тонну CSS’a - после MVP это стало недопустимо.
Как я оптимизировал виджет
Делал я это в 4 шага.
(1) Вставляем SVG иконки в bundle.
Если раньше иконка грузилась с помощью тега:
<img src=”https://telegram-feedback.com/images/arrow.svg” />
Теперь все иконки в виджете имеют формат:
![Использование изображений в виджете Использование изображений в виджете](https://habrastorage.org/getpro/habr/upload_files/e4e/cd7/147/e4ecd7147b44e24b7276bbb7a2621e7a.png)
Это позволяет обратиться к серверу один раз, получив скрипт и картинки за раз, и уменьшить задержку прогрузки страницы, а не отправить еще несколько запросов для запроса картинок.
(2) Заменяем ReactJS на PreactJS
React в сборке весит ~40 Кб. Меняем его на похожую (а во многих случаях аналогичную библиотеку) PreactJS, которая весит 3 Кб. Функционал остается, вес меньше.
Уточнение: в теории, можно написать виджет и на чистом JS. Но это будет намного более дорого с точки зрения времени (напомню, для меня это все-таки пет-проект в свободное время, которого мало) и, скорее всего, я напишу больше кода для выборки элементов, чем 3 Кб.
(3) Удаляем лишние библиотеки
Точно не вспомню, какие именно библиотеки я использовал, но точно помню, что удобно было использовать styled-components для динамического CSS’a. Пришлось удалять вообще все библиотеки, кроме одной - SocketIO.
Вообще, сокеты есть в стандартном API браузера, но SocketIO слишком удобно использовать и эта библиотека сильно экономит время. Единственное, что мне было слишком больно удалять.
(4) Удаляем лишний CSS и JS
~70% кода виджета - это мой CSS и JS. В самом начале я сделал предустановленные цветовые темы и на каждую тему был свой CSS файл по ~10 Кб. К тому же, был генератор фильтров для SVG иконок (чтобы закрасить иконку, нужно применить к ней фильтр, а до этого его сгенерировать).
CSS удалил, фильтры теперь генерируются на сервере при создании или сохранении виджета.
Вот так настраивается цвет виджета со стороны пользователя:
![Ручная настройка дизайна виджета Ручная настройка дизайна виджета](https://habrastorage.org/getpro/habr/upload_files/1f1/764/df9/1f1764df952290dd766c0be4b7b6fbe3.jpg)
Вот так генерируются CSS фильтры для заданных HEX цветов (раньше это было в виджете):
![Генерация цветового фильтра для SVG изображения Генерация цветового фильтра для SVG изображения](https://habrastorage.org/getpro/habr/upload_files/6be/ece/9f6/6beece9f61e7180c1f98965455abcfc6.png)
![Класс, отвечающий за генерацию фильтра Класс, отвечающий за генерацию фильтра](https://habrastorage.org/getpro/habr/upload_files/73d/268/5d6/73d2685d63016dc388f9963cbe21b35e.png)
Уточню, код скопипастил со StackOverflow и не до конца понимаю, как эти цвета генерируются с точки зрения пересечения цветов. Зато честно и работает.
Теперь виджет весит ~150Кб и это самый маленький результат среди всех виджетов, которые я встречал (а я искал). В мобильной версии скорость сайта падает от 1% до 3%. Это в несколько раз лучше, чем скрипт Яндекс Метрик (скрипт у них меньше весит, что круто, но имеет намного больше бизнес-логики).
В перспективе уменьшу размер виджета еще сильнее и за рамки 1% потери скорости загрузки страницы выходить не буду.
Когда-то.
С какими атаками и проблемами сталкивался проект
Перечислю, как пытались ломать сайт и виджет.
XSS атаки, вставки гадости в формы
Результат: получилось через сообщения пробросить скрипт в панель администратора (причем это был пользователь с Хабра).
Однако сейчас абсолютно весь текст обязательно экранируется, все запросы экранируются, никаких запросов в БД с необработанным вводом от пользователя (хотя их и не было).DOS с одного компьютера
Результат: не получилось. Изначально была DDOS защита на уровне API, потом появилась на уровне Nginx’a, а потом подключил DDOS Guard.Слабый DDOS
Результат: не получилось. К тому моменту всё работало в кластерном режиме (и фронт, и сервер), были ограничения по скорости запросов от Nginx и со стороны API.Загрузить память сервера картинками
Результат: почти получилось, случайно заметил. Какой-тоговнюкнехороший человек начал грузить на сервер картинки весом 50 Мб с медленной периодичностью. Возможно даже вручную. Я заметил, что за пару часов память выросла на ~10Гб. Начал разбираться, нашел проблему.
Сейчас на размер картинок стоит ограничение. Если даже маленькими картинками пытаются забить память - предупреждаю клиента, что его пытаются сломать и отключаю загрузку картинок + чищу картинки на какое-то время.
Идея загрузки картинок мне изначально не нравилась. Или память севера забивается, или платный CDN нужен. Однако клиенты очень просили, пришлось сделать. Сейчас функция есть, но за попытками забить память очень пристально следит целый ряд защит.
На самом деле, память так конечно фиг забьешь. И диск большой, и защит понаставил. Да и в планах перейти на CDN от Selectel’a. Но всё равно лично мне функция с картинками не нравится ?.
Попытки регистрироваться много раз
Результат: немного получилось подпортить мне жизнь. Больше на статистику регистраций я не опираюсь, уведомления о регистрациях выключил. На всякий случай поставил каптчу (чего-то забыл о ней с самого начала).Подобрать пароль к PostgreSQL
За эту ошибку мне очень стыдно. Опытный разработчик, про закрытие портов и сложные пароли знаю. А так облажался. Очень стыдно… Просто детский сад.
В первые несколько месяцев проекта мне нужно было подключиться к БД с локального компьютера, чтобы поделать выборки данных. Я открыл порт PostgreSQL во внешний мир, поковырялся вечерок и забыл, что порт открыт.
Спустя день я увидел, что загрузка сервера всё время 100% из-за процесса pg. Закрыл порт - не помогло. Перезагрузка тоже. Начал копать дальше и оказалось, что я оставил стандартный пароль на базе ?♂️. Естественно, бот нашел базу, пароль подошел и у него вышло разгруляться. На тот момент база была не в Docker’e, кстати.
В общем пришлось пересоздавать базу и перенастраивать роли, закрывать нормальным паролем. На всякий случай восстановился из бекапа, который был до открытия портов. Пароли пользователей, разумеется, были захешированы.
Вроде ничего страшного не случилось, но ЧСВ понизилось. Мораль: серьезные пароли, никаких портов во внешний мир (даже если так удобнее), аккуратно с настройками. Ну а для удобства поставил себе графическую оболочку XFCE (всегда ее любил) и PgAdmin, ковыряюсь в базе только по VNC .Боты с подбором форм
Ну это классика с которой сталкиваются все сайты поголовно. Боты бегают по интернету и пытаются подобрать пароли к WordPress’ам, CMS’кам и т.д. У меня API свое, такое не угрожает.
Школьники, тролли и арабы
Не техническая проблема, но раздражает.Разные
говнюкизаходят на сайт, пишут в чат, отвлекают внимание, зачем-то кидают фотографии арабских женщин.
В мыслях есть когда-то сделать автоматическую блокировку при появлении мата или порнографического материала от посетителя (например, с помощью нейронки от вк).
Но тоже когда-нибудь.
Про ограничения Telegram
Клиенты меня часто спрашивают, как я обхожу ограничения Telegram. Кто не знает, у Telegram ботов есть лимиты.
Тут скажу две вещи:
1. Никак. Я с ними сосуществую и играю по правилам, а не обхожу.
2. Использую дебоунсер с приоритезацией сообщений и потом планирую использовать разных ботов для распределения нагрузки.
Как это работает:
Всем сообщениям и действиям проставляется приоритет. Например, сообщение от пользователя имеет максимальный приоритет и будет доставлено в чат в первую очередь. Показ разных действий (например, кнопка "показать, что отвечаю) имеют средний приоритет.
Если в очереди (в рамках 5 секунд, например) стоит 3 сообщения пользователя и действие для кнопки “показать, что я отвечаю”:
![Приоритет действий Приоритет действий](https://habrastorage.org/getpro/habr/upload_files/c10/f1d/067/c10f1d067fa4ed85a34a9a446e4794f7.png)
Я в первую очередь отправлю сначала 3 сообщения, а уже только потом покажу кнопки с функциями. Эта приоритезация очень классно работает, когда в очереди стоят сотни сообщений в десятки чатов.
Уточню: такие ситуации случаются крайне-крайне редко (судя по логам). Бывают сайты, у которых проходимость 100 000 посетителей в день и им часто пишет параллельно 5-10 пользователей.
Для таких сайтов, к сожалению, в целом Telegram не подойдет и у них сообщения действительно будут тормозить. В таком случае я говорю прямо, что со мной в рамках их задач работать будет неудобно.
Есть еще ограничение на отправку 40 сообщений в секунду во все чаты на одного бота. До этого ограничения я еще не добрался даже приблизительно. Однако я понимаю, что через какое-то время его достигну.
На этот случай у меня уже готов код, чтобы менять ботов. Если сейчас в системе есть один бот и пользователю нужно писать или добавлять его, в будущем этих ботов будет 2-5-10 или сколько более. Просто система будет выбирать наименее загруженного бота и подключать его к новым сайтам.
Послесловие
За почти год проект разросся. Даже несмотря на то, что у меня редко выходило выделять на него больше 10-20 часов в неделю. А частенько и не выходило ?.
У любого IT-проекта, которым действительно пользуются пользователи, всегда много нюансов. Приходится всё систематизировать, защищать, оптимизировать и делать надежным. Вот и у меня так вышло.
Какая мораль у этого всего не знаю. Рассказал просто, чтобы поделиться (и лишний раз рассказать потенциальным пользователям про проект, разумеется). Наверное, мораль в том, что если хочешь сделать даже маленький IT продукт - будь готов к сложностям и имей необходимые навыки.
Вот.