Привет, коллеги!
Сегодня у нас на повестке дня выбор между двумя технологиями: Polling и Webhook. Почему почему именно Webhook является go-to решением для большинства проектов?
Помните, как в начале 2010-х все разрабы активно юзали Polling? Это был золотой стандарт для многих мессенджер-ботов. Но технологии не стоят на месте. Webhook занял свое место, предлагая свои решения.
Polling
Polling, или опрос, – это процесс, при котором клиентский скрипт периодически отправляет запросы к серверу для проверки наличия новой инфы. В телеге это регулярный запрос к АПИ для получения новых обновлений или сообщений.
При увеличении нагрузки Polling может стать бутылочным горлышком. Такую проблемку можно решить увеличением интервала между запросами в моменты низкой активности и его уменьшение в пиковые часы.
Если бот обрабатывает состояния или сессии пользователей нужно учитывать, что при Polling состояние должно храниться на стороне сервера. Это означает необходимость использования БДшек и т.п
Постоянные запросы к серверу создают дополнительную нагрузку. Необходимо настраивать подходящий баланс, чтобы избежать чрезмерного трафика и потенциального блокирования вашего бота из-за слишком частых запросов. API в телеграме ожидаемо имеет ограничения на количество запросов в секунду. Необходимо убедиться, что бот не превышает эти лимиты.
Согласно документации максимальное количество сообщений, отправляемых ботом, ограничено примерно 30 сообщениями в секунду для обычных сообщений и около 20 сообщений в минуту для групповых чатов.
Если бот достигает этих лимитов, будут вылетать ошибки RetryAfter
от API. Постоянные попытки повторной отправки сообщений, игнорируя ошибки API, могут привести к временной блокировке бота.
В python-telegram-bot (с версии 20), существует встроенный механизм BaseRateLimiter
, который контролирует количество API запросов, совершаемых ботом за определенный временной интервал
Простейший скрипт Polling бота:
import time
import requests
TOKEN = "токен_бота"
URL = f"https://api.telegram.org/bot{TOKEN}/getUpdates"
def get_updates():
response = requests.get(URL)
return response.json()
def main():
while True:
updates = get_updates()
if updates["result"]:
# Обработка каждого нового сообщения
for item in updates["result"]:
print(f"Сообщение от пользователя: {item['message']['text']}")
time.sleep(2) # Задержка в 2 секунды между запросами
if __name__ == "__main__":
main()
Бот каждые две секунды спрашивает у сервера Telegram, не пришло ли новых сообщений. В реальности тут конечно должна быть обработка ошибок, логирование и другие элементы
С Polling было связано множество трудностей. Во-первых, нагрузка на сервера. Каждый бот постоянно что то запрашивал у сервера. Представьте себе тысячи ботов, делающих это каждую секунду. Это было не только неэффективно с точки зрения трафика, но и могло вызывать задержки в доставке сообщений.
Во-вторых каждый раз, когда соединение прерывалось или сервер не отвечал, это могло привести к потере апдейтов. Приходилось тщательно продумывать механизмы повторных попыток и логирования, чтобы минимизировать потери данных.
Со временем, когда телега становилась всё более попсовой, разработчики начали искать более эффективные способы взаимодействия с API. И здесь появляются вебхуки. В отличие от Polling, где бот активно запрашивал данные, Webhook позволяет серверу телеграма самому отправлять обновления боту, как только они появляются.
Webhook
Webhook – это, по сути, колллбек. Это URL, который вы предоставляете телеграму, и по которому телеграм будет отправлять обновления в формате JSON каждый раз, когда вашему боту приходит сообщение или происходит другое событие.
В начале вам нужно настроить Webhook. Это делается путем отправки запроса к Telegram API с указанием URL вашего сервера. Телега будет использовать этот URL для отправки обновлений:
from telegram.ext import Updater, CommandHandler
from telegram import Bot
import logging
# логирование
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO)
logger = logging.getLogger(__name__)
# функция для обработки команды /start
def start(update, context):
update.message.reply_text('Привет!')
def main():
TOKEN = 'token'
bot = Bot(TOKEN)
# Updater и передача токена вашего бота
updater = Updater(token=TOKEN, use_context=True)
# диспетчер для регистрации обработчиков
dp = updater.dispatcher
# регистрация команды /start
dp.add_handler(CommandHandler("start", start))
# настроим вебхук
updater.start_webhook(listen='0.0.0.0',
port=8443,
url_path=TOKEN,
key='YOUR_PRIVATE.key',
cert='YOUR_PUBLIC.pem',
webhook_url='https://YOUR.SERVER.IP.ADDRESS:8443/' + TOKEN)
updater.idle()
if __name__ == '__main__':
main()
YOUR_PRIVATE.key
и YOUR_PUBLIC.pem
это путь к ключу SSL, и YOUR.SERVER.IP.ADDRESS
адрес сервера.
Для Webhook необходим SSL-сертификат. Telegram требует, чтобы обмен данными был защищен. Телеграм поддерживает ограниченное количество портов для Webhook: 443, 80, 88, 8443.
Для настройки SSL-терминации на веб-сервере нужно установить и настроить Nginx или Apache. На Nginx:
server {
listen 443 ssl;
server_name YOUR.SERVER.IP.ADDRESS;
ssl_certificate /path/to/YOUR_PUBLIC.pem;
ssl_certificate_key /path/to/YOUR_PRIVATE.key;
location / {
proxy_pass http://localhost:8443;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Как только кто-то взаимодействует с ботом (например, отправляет ему сообщение), телеграм делает запрос на юрл вебхук с деталями этого события. Эти данные приходят в виде JSON-объекта, который содержат информацию, к примеру:
{
"update_id": 123456789,
"message": {
"message_id": 111,
"from": {
"id": 222,
"is_bot": false,
"first_name": "viktor",
"username": "viktor12313"
},
"chat": {
"id": 333,
"first_name": "viktor",
"username": "viktor12313",
"type": "private"
},
"date": 1609459200,
"text": "ку"
}
}
Сервер затем обрабатывает этот запрос, парсит полученный JSON и выполняет необходимые действия, например, отвечает на сообщение пользователя.
Следующий шаг - это создание обработчика для входящих сообщений. В коде это обычно реализуется через определение функций с декораторами, указывающими на типы обрабатываемых сообщений (например, текст, изображения и т.д.). В качестве примера, можно использовать библиотеку fastapi
вместе с telebot
. Создаётся экземпляр класса AsyncTeleBot
, затем настраивается webhook и определяются функции для обработки команд (/start
, /help
) и обычных текстовых сообщений. При получении обновления, бот использует await
для асинхронной обработки сообщения и отправляет ответ:
import fastapi
from telebot.async_telebot import AsyncTeleBot
import telebot
API_TOKEN = 'токен'
WEBHOOK_URL = 'вебхук'
bot = AsyncTeleBot(API_TOKEN)
app = fastapi.FastAPI()
@app.post('/')
async def process_webhook(update: dict):
if update:
update = telebot.types.Update.de_json(update)
await bot.process_new_updates([update])
@bot.message_handler(commands=['help', 'start'])
async def send_welcome(message):
await bot.reply_to(message, "Бот запущен...")
@bot.message_handler(func=lambda message: True, content_types=['text'])
async def echo_message(message):
await bot.reply_to(message, message.text)
Итого в таблице сравнение выглядит так:
Критерий |
Polling |
Webhook |
---|---|---|
Время отклика |
Относительно медленное |
Почти мгновенное |
Нагрузка на сервер |
Высокая, требует постоянных запросов |
Низкая, сервер ждёт уведомлений |
Сложность реализации |
Простая, легко реализуемая |
Средняя, требует настройки HTTPS, SSL |
Масштабируемость |
Ограничена, увеличение нагрузки на сервер с ростом числа пользователей |
Высокая, эффективна при большом количестве пользователей |
Использование ресурсов |
Активное, постоянный опрос сервера |
Пассивное,проще говоря принимай когда пришло |
Управление трафиком |
Неэффективное, из-за постоянных запросов |
Эффективное, обрабатываются только актуальные события |
Безопасность |
Более уязвим для ддос атак из-за открытых запросов |
Более безопасный, использует зашифрованные соединения |
Настройка и обслуживание |
Простая, не требует дополнительной настройки |
Сложнее, требует настройки веб-сервера и SSL-сертификата |
Webhook хоть и немного сложнее в настройке, имеет гораздо больше плюсов по сравнению с Polling.
В завершение хочу порекомендовать бесплатный урок на котором коллеги из OTUS разберут практические подходы, которые помогут вам писать чистый, поддерживаемый код на Python.
Комментарии (23)
devlev
17.01.2024 12:41+6Зачем отправлять запросы при Polling методе каждые 2 секунды, если сам по себе Polling метод подразумевает, что сервер как бы ждет некоторое время и держит соединение не отдавая его содержимое, пока данные не появятся?
Это тогда не Polling получается а какой Getting.
Когда делал своего бота (не на пайтоне), как раз использовал этот подход. Делаю запрос на сервер телеграмм с ожиданием в 30 секунд, если ответа не последовало, сбрасываю соединение и снова делаю новый запрос. Получается один запрос в 30 секунд что не так часто.
aamonster
17.01.2024 12:41+7При увеличении нагрузки Polling может стать бутылочным горлышком. Такую проблемку можно решить увеличением интервала между запросами в моменты низкой активности и его уменьшение в пиковые часы.
Э... А что, long poll ещё не изобрели?
SidVisceos
17.01.2024 12:41-4Тут и малограмотным должно быть понятно. Если у меня 1000 ботов на сервере - для лонгов нужно 1000 демонов поднимать и заставлять их жить. При хуках пофиг один бот или 1000.
mrprogre
17.01.2024 12:41А у моего бота такая схема: каждые 10 минут java приложение, которое запущено как сервис на моем сервере, сохраняет новости в БД на том же сервере. А пользователи через бота получают эти новости селектами к таблицам БД. В такой схеме есть смысл применять вебхук?
Sau
17.01.2024 12:41+1Как бот узнал о ваших пользователях? Они что-то ему отправили, верно? И отписаться могут, отправив команду? То есть канал связи двусторонний.
Вот, если тут скорость реакции важна, а накладные расходы на частый поллинг велики, то можно использовать вебхук. И наоборот, если при вызове хука нужна длительная операция (ну не знаю, запросить баланс пользователя у третьей стороны, например), то лучше использовать поллинг, иначе при увеличении бутылочным горлышком станут хуки.
SidVisceos
17.01.2024 12:41Не совсем понимаю как хуки могут что то замедлить.
Пример:
Прилетел апдейт \wallet от пользователя. Бот лезет в базу, стучится по апишке на биржу за курсом, в общем проводит время с кайфом) и через час ответит пользователю. В этот самый момент падает хуком от другого пользователя апдейт с другим запросом. Сервер порождает ещё один процесс и бот отвечает незамедлительно.
Может я Вас неправильно понял?
Sau
17.01.2024 12:41Одинокий хук, конечно, ничего не замедлит. Предположим раньше бот быстро отвечал пользователям. Но вот в новостях сказали что этот бот развод и все ломанулись проверять баланс вводя /wallet. Вот пользователь ввёл /wallet, ничего не произошло. Он ещё раз ввёл, ещё, чертыхнулся и пошёл жаловаться другим пользователям. Они тоже пошли вводить /wallet. Проходит пять минут, пользователь думает что может починили и снова пишет /wallet. И снова нет. В итоге сервер засыпан запросами. Сколько процессов сможет выдержать ваш сервер? А если и сможет выдержать всё, то у биржи может быть ограничение на число одновременных запросов.
SidVisceos
17.01.2024 12:41Разумно, логично. Спасибо.
Жаль не всегда можно реализовать без хуков. У меня например сейчас 2400 штук ботов, 7 разных типов (конструктор ботов через который пользователи создают сови боты). Как это сделать без хуков - даже предположить не могу.
mayorovp
17.01.2024 12:41Да без проблем, 2400 ботов - это всего лишь 2400 HTTP соединений с long polling. Держать столько соединений вообще не проблема пока памяти достаточно.
SidVisceos
17.01.2024 12:41Да. Не забывайте о возможности слать всякие sendMessage не после приёма хука, а в ответ на хук. То есть пришёл апдейт хуком и ты не отвечаешь кодом 200 и после шлёшь ответ, а прям вместе с кодом 200 отвечаешь на хук. Одно но. Один хук = один ответ (1 сообщение или правка 1 сообщения или 1 answerCallbackCuery,....).
ValeryIvanov
17.01.2024 12:41+8Сначала подумал, что автор статьи под polling имеет ввиду long polling, но нет, в статье описывается именно обычный polling и его недостатки, тогда как телеграм использует именно long polling.
Отсюда делаю вывод, что автор не вникал в предмет статьи и/или вовсе написал статью используя нейросеть
maxp
17.01.2024 12:41Согласен с мнением предыдущего оратора.
Увы, автор статьи не счёл нужным вникать в суть вопроса до того как рассказывать аудитории про "бутылочные горлышки" и т.п.
Коротко по сути вопроса:
в Телеграмме long polling (не просто polling!) сделан грамотно, удобен в использовании, надежен и бутылочным горлышком не является до тех по пока ваш инстанс обслужвает одного бота.
Webhook предпочтительнее когда:
- вебсервер уже есть для чего-то другого, а бот работает периодически на не частые запросы (в виде лямбды, например)
- один инстанс приложения обслуживает много разных ботов.Одна из неочевидных для новичков проблем вебхуков это то, что если Телега по какой-то причине не смогла успешно достучаться до хоста (протух серт, заблочили подсеть, упал хостинг, ...), она ставит общение на паузу и этот момент надо отдельно как-то отслеживать.
FisHlaBsoMAN
17.01.2024 12:41+1Что это за ересь? Зачем динамически менять интервал? Ради чего? Зачем его ставить всего 2 секунды? Кто так делает вообще? Вебхуки это плохо переносимая технология и она требует внешнего адреса и настройки вебсервера в режиме проксирования или выделенно прослушивающего https порт инстанса бота. Вообще в лонгполе (да и везде) всё решается асинхронным подходом. aiohttp берешь и прикручиваешь, перебирая полученные события где то там в фоне, в ожидании новых ответов. Не успеваешь обрабатывать - делай очереди. У меня десяток ботов на самописном движке, дергающим getupdates и переезд на новый хостинг у меня занял пол часа от силы на добавление темплейта сисд юнита и распихивания папок с данными, создание венва с requirements.txt и включения этих юнитов..
На лонгполл в один ответ влезает 100 событий при условии что их накопилось, а телеговские серваки просто моментально отгружают их. я как то во время разработки не уследил за счетчиком и от ошибки сети бот зациклился на получении одного и того же списка апдейтов и телега несколько минут в режиме нон-стоп нагенерила мне несколько гигов логов со строками апдейтов. И при условии моментального повтора запроса так же моментально отдают принятое сейчас сообщение. если юзать http библиотеку с сессиями и поддержкой соединения, то это работает просто моментально. И пропустить апдейты при условии сохранения предыдущего id ну просто невозможно. Ты в запросе последний id в телегу передаешь.
по-моему автор выдумал проблему где ее нет. Впрочем это не самостоятельный автор, а реклама курсов питона
starik-2005
Вебхуки - штука хорошая, но, как справедливо заметили, требует домена с сертификатом. Не знаю, работает ли оно с самоподписанными сертификатами, но и для них нужен внешний IP. А если вы в домашней сетке через роутер пилите свой первый телегобот, то у вас вообще никаких вариантов кроме пулинга нет.
Singrana
Вариант - у вас есть сервер, на нем принимаются запросы, и проксируются через тоннель к вам, на рабочую машину
Cregennan
Есть несколько вариантов (для дебага бота):
Использовать ZeroTier/TailScale для пробития NAT
Временно создать туннель с помощью Ngrok или похожего софта
Для продакшена же согласен, нужно будет брать белый IP
mayorovp
Если у вас совсем-совсем своего IP нет - то да, остаётся только long polling.
Но если есть хотя бы динамический IP - вы уже можете получить доменное имя через dynamic dns и получить на него сертификат от Let's Encrypt
iscareal
Я локально, когда делал бота на webhook использовал ngrok. Сейчас же в vs code во вкладке Ports можно сделать себе url, который не будет меняться в проекте. По поводу сервера - cloudflare покатит, мне кажется, чтобы не мучиться с сертификатами
SidVisceos
В vs code можно арендовать сервер чтоли?
mayorovp
Сервер нельзя (наверное), но порт, как оказалось - можно.
iscareal
Можно прокинуть порт. Бесплатная альтернатива ngrok
iRedds
Можно поднять локальный сервер бота, который будет работать напрямую с телеграмом и дергать хуки на локальном сервере.