Вопрос или проблема
У нас есть сервер обратного прокси nginx (инстанс ec2). В последнее время мы сталкиваемся с проблемой медленной реакции, когда количество запросов в минуту увеличивается. Ниже приведена информация о моем приложении и сервере nginx:
- Мы запускаем сервер приложений на базе Python Tornado как API-сервис.
- Мы настроили nginx так, чтобы Tornado работал на 16 разных портах, и входящие запросы распределялись по этим портам. Например: предположим, что базовый конечный пункт API – “api.ecom.in”, если мы получаем запрос на “api.ecom.in”, то этот запрос назначается на один из работающих портов запущенного приложения Tornado.
- Мы используем tornado-sqlalchemy для подключения к базе данных и пуллинга. Ниже приведен пример моего подключения к БД и сессии.
from tornado_sqlalchemy import SQLAlchemy
from contextlib import contextmanager
db = SQLAlchemy(url=DATABASE_URL, session_options={'expire_on_commit': False}, engine_options={'pool_size': 100,'max_overflow': 1400, 'echo': False, 'pool_recycle':1200 ,'connect_args': {'connect_timeout': 20}})
@contextmanager
def session_scope():
session = None
try:
session = db.sessionmaker()
yield session
except Exception:
if session:
session.rollback()
raise
else:
session.commit()
finally:
if session:
# session.expunge_all()
session.close()
- Ниже приведен пример моего обработчика:
@jwtauth
class GetSeasonalProductHandler(BaseHandler):
SUPPORTED_METHODS = ['GET']
async def get(self):
return self.send_response(data = await get_seasonal_product_service(self.request.query_arguments), status = 200)
@classmethod
def route_url(cls):
return [
(r'(?i)/api/v2/product_master/seasonal_product', cls, {})
]
- Как вы можете видеть выше, есть функция с именем: “get_seasonal_product_service”, ниже приведен код функции:
async def get_seasonal_product_service(request_args):
response_data = {}
region_id = request_args['region_id'][0].decode('utf8')
header_id = request_args['header_id'][0].decode('utf8') if 'header_id' in request_args else 1
customer_id = request_args['customer_id'][0].decode('utf8')
with session_scope() as session:
seasonal_products = session.execute(
"EXEC USP_ProductMasterSeasonal {region_id}, {header_id}, {customer_id}".format(
region_id = region_id,
header_id = header_id,
customer_id = customer_id
)
).fetchall()
product_list = []
for product in seasonal_products:
product_list.append(dict(product))
return product_list
Основная проблема: в последнее время, когда количество запросов в минуту увеличивается, время отклика моего API также начинает увеличиваться. Текущая величина запросов в минуту составляет около 12000 и более. В июле 2024 года мы смогли обрабатывать более 15000 запросов в минуту, и тогда все работало хорошо. Но с прошлой недели мы сталкиваемся с этой проблемой.
Мое наблюдение: я заметил, что мои SQL-запросы занимают слишком много времени. Я заметил, что удаление некоторых ненужных вызовов в моей служебной функции также немного помогло улучшить время отклика. Но, на мой взгляд, сокращение вызовов – это не решение, а временное решение. Мне нужно понять, нужно ли что-то оптимизировать на уровне сервера Python Tornado. Пожалуйста, помогите, если вы видите что-то не так в архитектуре Python. Я сам заметил, что tornado-sqlalchemy плохо оптимизирован для обработки асинхронных вызовов, и даже contextmanager работает синхронно. Но поскольку теперь моя вся архитектура зависит от этого синхронного session_scope(), у меня есть сомнения и трудности с его изменением на что-то асинхронное.
Что я пробовал до сих пор:
- На данный момент я удалил все ненужные вызовы БД из своего API-сервиса.
- Увеличил размер пула подключений. Добавил конфигурацию пула recycle = 1200, добавил timeout подключения к БД = 20
- Увеличил количество работающих портов сервера приложений Tornado.
- Мы планируем использовать Redis для кэширования некоторых ответов API, чтобы сократить обращения к БД (всё еще работаем над этим).
Ответ или решение
Проблема с медленным временем ответа в вашем API действительно может быть довольно сложной и многогранной. Судя по представленным данным, и учитывая, что вы уже предприняли некоторые действия, давайте рассмотрим возможные решения для оптимизации вашего приложения на Tornado и базы данных.
1. Оптимизация работа с базой данных
Поскольку вы заметили, что SQL-запросы требуют много времени, стоит обратить внимание на их оптимизацию:
-
Индексы: Убедитесь, что ваши таблицы в базе данных индексированы правильно. Неправильное индексирование может значительно увеличивать время выполнения запросов.
-
Анализ запросов: Используйте инструменты профилирования SQL, такие как
EXPLAIN
, чтобы увидеть, где "узкие места" в ваших запросах. Возможно, следует переписать некоторые запросы или объединить их, если это возможно. -
Асинхронные операции: Вы упомянули, что
tornado-sqlalchemy
может неэффективно обрабатывать асинхронные вызовы. Рассмотрите замену на более продуктивную библиотеку для асинхронного взаимодействия с базой данных, например,aiomysql
илиasyncpg
(для PostgreSQL). Это поможет вам избежать блокировок и повысит скорость работы.
2. Оптимизация архитектуры Tornado
Ваше приложение на Tornado также можно оптимизировать:
-
Пул соединений: Как вы уже сделали, увеличьте размер пула соединений. Однако будьте осторожны с чрезмерным увеличением, так как это может привести к исчерпанию ресурсов. Подумайте о тестировании реального количества соединений, необходимых для вашей нагрузки.
-
Избегайте блокирующих операций: Проверьте ваш код на наличие блокирующих операций при выполнении запросов к базе данных. Используйте асинхронные функции (например, с
async/await
), чтобы избежать блокировки ввода-вывода. -
Кэширование: Вы уже рассматриваете использование Redis. Это действительно хорошая идея, так как кэширование ответов и часто запрашиваемых данных в памяти снижает нагрузку на базу данных и уменьшает время ответа API.
3. Микросервисная архитектура
Если ваша система продолжает расти, рассмотрите переход на микросервисную архитектуру. Это позволит вам разделить вашу логику на несколько сервисов, которые могут масштабироваться независимо. Например, вы можете выделить сервис, который фокусируется на работе с продуктами и соответствующими данными, что упростит масштабирование и управление.
4. Анализ нагрузки
-
Мониторинг: Убедитесь, что вы выполняете мониторинг и анализ нагрузки на ваш сервер. Используйте такие инструменты, как Prometheus и Grafana, чтобы получить представление о производительности вашего приложения и базы данных.
-
Стресс-тестирование: Запустите стресс-тестирование вашего API с помощью таких инструментов, как LoadRunner или Apache JMeter, чтобы увидеть, как ваше приложение ведет себя под нагрузкой. Это поможет гарантировать, что у вас есть достаточная производительность, когда запросы начнут увеличиваться.
Заключение
Проблема медленных ответов на API часто связана с узкими местами в базе данных и архитектуре приложения. Ваша коррекция подхода к работу с базой данных, использование асинхронных библиотек и кэширование, а также масштабирование архитектуры могут значительно улучшить время ответа. Продолжайте мониторить и анализировать производительность, чтобы находить и устранять проблемы по мере их появления.