Лучший навык в работе — умение признавать свои ошибки. Ошибаются все, а вот учатся на них и выносят пользу — далеко не многие.
3 мая вы могли заметить, что Имперский Стражник с ночи работал не очень стабильно: не отвечал на некоторые запросы, а на другие отвечал по несколько минут. Мы признаём — мы накосячили. И сильно. Давайте вместе посмотрим, что мы делали и где всё пошло не так.
Уже давно у нас стояла задача на создание второй, текстовой модели для ИИ-модерации. Сейчас мы умеем проверять только визуал, но если запрещённый контент находится в тексте — мы его не видим. Тут и должна помочь текстовая модель.
27 апреля — 1 мая
Мы берём в активную разработку задачу на создание и интеграцию текстовой модели. Первая итерация: для хранения угрозы используется единое поле
2 мая, 13:00 — первый звоночек
Нам присылают репорт и интересное предложение. Суть: Стражник выдал ограничение за порнографию в ответ на стикер, где был лишь шуточный текст про половой акт.
Предложение звучало так:
Мы задумываемся и понимаем: поинт крайне корректный. Нужно срочно исправлять ситуацию, пока массовые ограничения с такой логикой не улетели всем подряд.
2 мая, 14:15
Создаём задачу
Разделяем текстовую и визуальную угрозу.
3 мая, 1:26
Выкатываемся на прод (не с первого раза). Пропускная способность крайне низкая, при этом
3 мая, 1:52
Отваливается Prometheus.
Мы уже понимаем: вечер будет весёлый.
За 7 минут дебага находим кривой запрос сбора статистики и правим его.
3 мая, 2:27
После ещё пары хотфиксов мы наконец в проде. Всё работает. Без даунтайма внедрили фичу. Красота.
Тогда мы ещё не знали, что натворили.
За пару минут система разгоняется до 11 ops.
Что такое 11 ops? Это 11 файлов, которые скачиваются, проверяются и сохраняются каждую секунду.
3 мая, 3:01 — первый звонок армагеддона
Начинаем получать от BOT API:
Мы подумали, что упёрлись в лимит
Оказалось, мы упёрлись в лимит общего количества запросов к BOT API. Ошибки начали ловить вообще все методы бота.
Через 8 минут внедряем rate limiter и заканчиваем рабочий день.
3 мая, 14:17
Замечаем странное поведение. Несмотря на лимитер, Too Many Requests становится только больше.
Очень странно.
Я решаю написать миксин, который будет отдавать в Prometheus статистику запросов в разрезе бота и метода.
3 мая, 14:40
Дописываю логику, собираю дашборд… и тут становится понятно то, что мы уже начинали подозревать.
Лимитер. Не работает.
При лимите в 20 ops…
МЫ ДЕЛАЕМ 95 OPS.
3 мая, 16:36
Релизим новый лимитер, который уже нормально работает с потоками и не допускает ошибок прошлой версии.
Но за время разработки у нас случился тот самый:
Из-за разгона до 173 ops Telegram на минуту отрезал нам вообще все запросы. Полностью.
Да, было больно.
3 мая, 17:00
Инцидент полностью разрешён. Очереди разгребены, фон ошибок снижен, всё стабилизировалось.
Что можно сказать по итогу?
Ну… мы очень жёстко накосячили 😅
Недостаток метрик и нормальных prod-like тестов привёл к тому, что в продакшен уехал код, который устроил харакири нашей пропускной способности.
Но зато:
— появились новые дашборды
— алерты на рост ops
— понимание, куда смотреть и чёткое осознание того, насколько опасно релизить в прод
Вот как-то так :3
А у вас в проектах были подобные инциденты?
Буду рад почитать истории в комментариях. Ну и да, ещё покидаю туда цитаты из рабочего чатика — там были настоящие легендарные сообщения
3 мая вы могли заметить, что Имперский Стражник с ночи работал не очень стабильно: не отвечал на некоторые запросы, а на другие отвечал по несколько минут. Мы признаём — мы накосячили. И сильно. Давайте вместе посмотрим, что мы делали и где всё пошло не так.
Уже давно у нас стояла задача на создание второй, текстовой модели для ИИ-модерации. Сейчас мы умеем проверять только визуал, но если запрещённый контент находится в тексте — мы его не видим. Тут и должна помочь текстовая модель.
27 апреля — 1 мая
Мы берём в активную разработку задачу на создание и интеграцию текстовой модели. Первая итерация: для хранения угрозы используется единое поле
danger с приоритизацией по опасности. На наш взгляд, это была рабочая и вполне нормальная логика. Мы дописали код и выкатили релиз в продакшен.2 мая, 13:00 — первый звоночек
Нам присылают репорт и интересное предложение. Суть: Стражник выдал ограничение за порнографию в ответ на стикер, где был лишь шуточный текст про половой акт.
Предложение звучало так:
«А ты не думал им отдельные категории выдать? Потому что порнография текстом и порнография картинкой — вещи немного разные».
Мы задумываемся и понимаем: поинт крайне корректный. Нужно срочно исправлять ситуацию, пока массовые ограничения с такой логикой не улетели всем подряд.
2 мая, 14:15
Создаём задачу
IG-132841, в рамках которой планируем рефактор:intent_mediaintent_textРазделяем текстовую и визуальную угрозу.
3 мая, 1:26
Выкатываемся на прод (не с первого раза). Пропускная способность крайне низкая, при этом
Load Average — 23. Потеряли индексы в БД. Спустя 20 минут восстанавливаем индексы, чиним медленные запросы и продолжаем жить.3 мая, 1:52
Отваливается Prometheus.
Мы уже понимаем: вечер будет весёлый.
За 7 минут дебага находим кривой запрос сбора статистики и правим его.
3 мая, 2:27
После ещё пары хотфиксов мы наконец в проде. Всё работает. Без даунтайма внедрили фичу. Красота.
Тогда мы ещё не знали, что натворили.
За пару минут система разгоняется до 11 ops.
Что такое 11 ops? Это 11 файлов, которые скачиваются, проверяются и сохраняются каждую секунду.
3 мая, 3:01 — первый звонок армагеддона
Начинаем получать от BOT API:
TelegramError: 429: Too Many Requests: retry after 60Мы подумали, что упёрлись в лимит
getFile. Ну бывает. Сейчас ограничим скорость и пойдём спать. Ага. Конечно.Оказалось, мы упёрлись в лимит общего количества запросов к BOT API. Ошибки начали ловить вообще все методы бота.
Через 8 минут внедряем rate limiter и заканчиваем рабочий день.
3 мая, 14:17
Замечаем странное поведение. Несмотря на лимитер, Too Many Requests становится только больше.
Очень странно.
Я решаю написать миксин, который будет отдавать в Prometheus статистику запросов в разрезе бота и метода.
3 мая, 14:40
Дописываю логику, собираю дашборд… и тут становится понятно то, что мы уже начинали подозревать.
Лимитер. Не работает.
При лимите в 20 ops…
МЫ ДЕЛАЕМ 95 OPS.
3 мая, 16:36
Релизим новый лимитер, который уже нормально работает с потоками и не допускает ошибок прошлой версии.
Но за время разработки у нас случился тот самый:
«пик, после которого лимиты Telegram посадили нас на пику».
Из-за разгона до 173 ops Telegram на минуту отрезал нам вообще все запросы. Полностью.
Да, было больно.
3 мая, 17:00
Инцидент полностью разрешён. Очереди разгребены, фон ошибок снижен, всё стабилизировалось.
Что можно сказать по итогу?
Ну… мы очень жёстко накосячили 😅
Недостаток метрик и нормальных prod-like тестов привёл к тому, что в продакшен уехал код, который устроил харакири нашей пропускной способности.
Но зато:
— появились новые дашборды
— алерты на рост ops
— понимание, куда смотреть и чёткое осознание того, насколько опасно релизить в прод
Вот как-то так :3
А у вас в проектах были подобные инциденты?
Буду рад почитать истории в комментариях. Ну и да, ещё покидаю туда цитаты из рабочего чатика — там были настоящие легендарные сообщения
🔥21❤6❤🔥1
Свердловский депутат требует вернуть опасную детскую игру
Челябинский депутат Госдумы Лантратова требует запретить видеоигры
Вражеские бирюльки. РКН запретит культовые компьютерные игры для 87 млн
Депутаты Госдумы России предложили ограничить доступ к видеоиграм по «китайскому сценарию»
Депутат Гусев предложил ввести самозапрет на компьютерные игры: «Для многих это уже форма зависимости»
Мы уже привыкли к жёсткой стигматизации игры в СМИ как источника всего плохого и вообще чуть ли не абсолютного зла. И ведь причём это далеко не только наша и далеко не новая тенденция.
Многие политики, журналисты, да и обычные люди старших поколений привыкли обсуждать игры в двух полярностях: полезное и вредное, будто это какой-то прибор.
Но давайте честно — игры уже давно не просто безделушки. Это социальное, экономическое и даже политическое явление. Вещь, которая влияет на жизни миллионов людей.
А когда что-то влияет на мышление, поведение и отношения — она становится предметом этики.
А в ней нам надо ставить вопрос не "нравятся ли вам игры?", а "какими они нас делают?"
Причём это вопрос не только про агрессию или зависимость. Это вопрос про эмпатию, социализацию, моральный выбор, ощущение ответственности, отношение к насилию, смерти, власти, труду, даже к самому себе.
И мне кажется, что мы как общество до сих пор толком не дошли до этого разговора. Мы всё ещё пытаемся решить сложнейшую культурную тему лозунгами уровня:
игры полезны
или
игры вредны
Хотя всё сильно сложнее.
Как думаете, наше политическое и социологическое развитие уже дошло до такого разговора — или пока нет?
Давайте обсудим это в рамках рубрики об этике в играх.
❤26🔥4👀4❤🔥1🤯1
Давайте немного о коде и решениях 😄
Имперский Стражник — большая система с кучей разных источников сообщений. Сообщение может уйти из одного из 200 классов ядра или ещё большего зоопарка внешних модулей, но есть одна важная проблема: отправку сообщений надо контролировать.
Надо следить за лимитами, чтобы не передалбывать Bot API зазря. Надо следить за ответами, чтобы, если мы вышли за лимит, подождать и повторно отправить запрос. Надо вообще убеждаться в доставке, синхронизировать состояние и ответы.
Короче, следить надо за всем. Так что давайте я расскажу, что мы сделали, чтобы всё это работало и жило с учётом нашего стека.
Начнём с учёта отправки, лимитов и структуризации. Я решил, что за отправку сообщений будет отвечать синглтон-сервис, на который все остальные сервисы будут скидывать задачи. Но как это всё соединить, учитывая необходимость быстрого масштабирования и подключения новых модулей?
Тут на помощь пришёл Bull (не реклама, если что, просто делюсь либой, на которой делал решение). Давайте посмотрим, как выглядит воркер отправки:
Небольшой такой код, но он позволяет нам чётко контролировать очередь и лимиты. А дальше посмотрим на отправку и приём)
В комментариях разрешается открыть портал в холивар))
Имперский Стражник — большая система с кучей разных источников сообщений. Сообщение может уйти из одного из 200 классов ядра или ещё большего зоопарка внешних модулей, но есть одна важная проблема: отправку сообщений надо контролировать.
Надо следить за лимитами, чтобы не передалбывать Bot API зазря. Надо следить за ответами, чтобы, если мы вышли за лимит, подождать и повторно отправить запрос. Надо вообще убеждаться в доставке, синхронизировать состояние и ответы.
Короче, следить надо за всем. Так что давайте я расскажу, что мы сделали, чтобы всё это работало и жило с учётом нашего стека.
Начнём с учёта отправки, лимитов и структуризации. Я решил, что за отправку сообщений будет отвечать синглтон-сервис, на который все остальные сервисы будут скидывать задачи. Но как это всё соединить, учитывая необходимость быстрого масштабирования и подключения новых модулей?
Тут на помощь пришёл Bull (не реклама, если что, просто делюсь либой, на которой делал решение). Давайте посмотрим, как выглядит воркер отправки:
new Worker(process.env.DEBUG ? "guard_dev_send" : "guard_send", async (job) => { // тут я переключаю очередь в зависимости от режима, в котором запущен сервис, чтобы не задевать прод во время тестирования
let telegram = job.data.target === "media" ? mediaBot.telegram : bot.telegram; // в данный момент бот, через который будет идти отправка, выбирается немного костыльно, но мы в работе :D
try {
return await telegram.sendMessage(job.data.id, job.data.text, job.data.extra); // отправляем сообщение
} catch (e: any) {
if (e.code === 429) {
await job.moveToDelayed((Date.now() / 1000) + 30000); // словили рейтлимит, ждём 30 секунд и пробуем опять
} else {
e._sourceStack = job.data.stack; // подпихиваем стек до очереди в ошибку
e._stack = e.stack; // стек внутри очереди
e._message = e.message; // и причину
return e;
}
}
}, {
connection: {
host: process.env.REDIS_HOST,
port: Number.parseInt(process.env.REDIS_PORT ?? "6379")
},
limiter: { // режем скорость отправки
max: 2,
duration: 1000
},
concurrency: 1, // и ограничиваем параллельность
}).on("failed", (job, err) => {
Queues.logger.error({
job,
err
}, "Failed to send message"); // если что-то пошло не так, логируем
});
Небольшой такой код, но он позволяет нам чётко контролировать очередь и лимиты. А дальше посмотрим на отправку и приём)
В комментариях разрешается открыть портал в холивар))
❤21😁5🔥2🎉1
Давайте опять поговорим про очереди в Стражнике. Мы уже увидели сниппет кода, отвечающий за обработку, но как мы реализовали отправку? Вот тут всё начинает становиться сильно интереснее.
Основная сложность, с которой мы столкнулись, — необходимость рефакторить весь код. Хотелось избежать редактирования вообще всех мест, где идёт отправка сообщений. Поэтому мы вспомнили очень важную вещь, которая есть в JS: почти всё можно перезаписать по своему желанию. Этим мы и воспользовались, перезаписав метод отправки сообщения и подменив логику отправки в Telegram логикой отправки в очередь.
Получился вот такой прикольчик:
Тут мы перезаписали оригинальный метод ответа на сообщение своим собственным. Собственно, вуаля — половина работы отменена)
Дальше задача номер два, которая стала сильно интереснее: отправка сообщений разделяется на два типа — там, где нам нужен ответ, и там, где нам наплевать на ответ.
Нашей первой реализацией была идея внутри каждой операции отправки сообщения создавать листенер, который будет ждать ответа о том, что сообщение отправилось, и резолвить промис с ответом. А делать
Что могло пойти не так? Да, как оказалось, много чего. У нас утекли листенеры. Их стало так много, что Node.js перестал справляться с их контролем.
Встал вопрос: как сделать решение, которое не перегрузит среду, но сможет дожидаться ответа по отправке сотен сообщений и делать это асинхронно?
И тут я придумал прикольное решение: создаём
В отправке мы создаём задачу и промис, записывая в мапу ID задачи и
И вместо сотен листенеров делаем один-единственный глобальный, который будет работать до самого завершения процесса:
И тем самым получается, что в команде код выглядит как самый обычный вызов:
А на самом деле за пару миллисекунд запрос обходит две очереди и несколько сервисов, выполняя работу чётенько и безопасненько)
С первого взгляда выглядит как быстрое и логичное решение, но мы к этому шли месяц-полтора.
Вот как-то так.
Основная сложность, с которой мы столкнулись, — необходимость рефакторить весь код. Хотелось избежать редактирования вообще всех мест, где идёт отправка сообщений. Поэтому мы вспомнили очень важную вещь, которая есть в JS: почти всё можно перезаписать по своему желанию. Этим мы и воспользовались, перезаписав метод отправки сообщения и подменив логику отправки в Telegram логикой отправки в очередь.
Получился вот такой прикольчик:
ctx.reply = async function (text: string, options?: ExtraSendMessage): Promise<Message> {
return Queues.getInstance().sendMessage("replyQueued", this.chat.id, text, options);
}
Тут мы перезаписали оригинальный метод ответа на сообщение своим собственным. Собственно, вуаля — половина работы отменена)
Дальше задача номер два, которая стала сильно интереснее: отправка сообщений разделяется на два типа — там, где нам нужен ответ, и там, где нам наплевать на ответ.
Нашей первой реализацией была идея внутри каждой операции отправки сообщения создавать листенер, который будет ждать ответа о том, что сообщение отправилось, и резолвить промис с ответом. А делать
await промиса или нет — уже решение на уровне бизнес-логики.Что могло пойти не так? Да, как оказалось, много чего. У нас утекли листенеры. Их стало так много, что Node.js перестал справляться с их контролем.
Встал вопрос: как сделать решение, которое не перегрузит среду, но сможет дожидаться ответа по отправке сотен сообщений и делать это асинхронно?
И тут я придумал прикольное решение: создаём
Map, который будет хранить ID задачи на отправку и коллбэк.
private _promisedMessages = new Map<string, { resolve: (message: Message) => void, reject: (error: Error) => void }>();
В отправке мы создаём задачу и промис, записывая в мапу ID задачи и
resolve этого промиса в качестве коллбэка.
async sendMessage(target: TargetBot, name: string, id: number, text: string, extra: ExtraSendMessage = {}, priority = 10) {
let error = new Error("Stack trace"); // получаем стектрейс до этого момента
let job = await this._sendQueue.add(name, {id, text, extra, target, stack: error.stack}, {priority});
return new Promise<Message>((resolve, reject) => {
this._promisedMessages.set(job.id!, {resolve, reject});
});
}
И вместо сотен листенеров делаем один-единственный глобальный, который будет работать до самого завершения процесса:
new QueueEvents(process.env.DEBUG ? "guard_dev_send" : "guard_send", {
connection: {
host: process.env.REDIS_HOST,
port: Number.parseInt(process.env.REDIS_PORT ?? "6379"),
},
})
.on("completed", async ({jobId, returnvalue}) => {
if (this._promisedMessages.has(jobId)) { // проверяем, что это сообщение ожидается на этом воркере
let {resolve, reject} = this._promisedMessages.get(jobId)!; // получаем коллбэки этого сообщения
this._promisedMessages.delete(jobId); // удаляем его из мапы
resolve(returnvalue); // отдаём результат промису
}
});
И тем самым получается, что в команде код выглядит как самый обычный вызов:
if (!peer.isUserKnown()) {
await ctx.reply(await ctx.lang.t("commands.general.userNotFound"));
return;
}
А на самом деле за пару миллисекунд запрос обходит две очереди и несколько сервисов, выполняя работу чётенько и безопасненько)
С первого взгляда выглядит как быстрое и логичное решение, но мы к этому шли месяц-полтора.
Вот как-то так.
❤🔥24❤5🔥4
Я живой (вроде), так что попробуем оживится ещё и в ваших глазах.
Встречаемся в 22:30 (МСК) в уже знакомом нам месте.
https://www.twitch.tv/sleepless_code
Встречаемся в 22:30 (МСК) в уже знакомом нам месте.
https://www.twitch.tv/sleepless_code
❤15❤🔥1
Бессонный кодер
Я живой (вроде), так что попробуем оживится ещё и в ваших глазах. Встречаемся в 22:30 (МСК) в уже знакомом нам месте. https://www.twitch.tv/sleepless_code
Twitch
sleepless_code - Twitch
sleepless_code streams live on Twitch! Check out their videos, sign up to chat, and join their community.
❤🔥11🔥1