Рубрики
Без рубрики

Мониторинг синхронизации и асинхрологических сетевых вызовов в Python с использованием стека TIG

Переиздано автором. Впервые появился в календаре веб -производительности 2020 года. Веб -приложения и API End … Tagged с помощью Python, Monitoring, Aiohttp, запросов.

Переиздано автором. Впервые появился в Календарь веб -производительности 2020 Анкет

Известно, что веб -приложения и конечные точки API выполняют бэкэнд -вызовы. Часто это все приложение делает: получает данные из нескольких бэкэндов, объединяет его и дает ответ.

Мониторинг того, сколько времени потребовалось данных, необходимо. Существует множество готовых к производству решений, которые обеспечивают такой мониторинг, но они могут не подходить для некоторых случаев. И я думаю, что это весело углубляться в вещи, чтобы получить больше понимания того, как все это работает.

Давайте посмотрим на примеры кода, в которых используются популярные библиотеки Python Networking и предназначены для сообщений о времени выполнения HTTP -запроса.

Что я собираюсь исследовать в этом посте

Я собираюсь сравнить, как сроки запроса ищут HTML -страницы, используя HTML -страницы Запросы Библиотека и для асинкнового получения те же страниц HTML с использованием aiohttp библиотека. Я стремлюсь визуализировать разницу во время и ввести инструменты, которые можно использовать для такого мониторинга.

Чтобы быть справедливым, Запросы Библиотека имеет плагины Это обеспечивает асинкнозный в io, и есть так много других способов достичь этого в Python … Я выбрал aiohttp Поскольку он предоставляет аккуратные возможности отслеживания времени запроса, и я часто использую эту библиотеку в дикой природе.

Чтобы отслеживать время запроса, мы будем использовать Telegraf , Influxdb и Графана куча. Эти инструменты очень просты в настройке локально, с открытым исходным кодом, бесплатно для личного использования, и могут использоваться в производственной среде.

Запуск примеров кода раздел подробно описывает, как запустить примеры кода и инфраструктуры мониторинга настройки (Telegraf, Influxdb, Grafana).

Весь код из этого письма доступен в Репо Анкет

Пример 0: мониторинг запроса времени запроса

Давайте погрузимся в пример первого кода Python. Вот что он делает:

  • В Forever Loop выполняет два HTTP -запроса, используя Запросы Библиотека Python
  • Отчеты запрашивать время и запрашивать исключения для телеграммы

Вот время выполнения запроса на панели инструментов:

Полный код примера 0 можно найти в Пример-0-Requests-send-stats.py Анкет

Поток выполнения высокого уровня можно следовать от Главный часть программы:

if __name__ == '__main__':
    while True:
        result = call_python_and_mozilla_using_requests()
        print(result)
        time.sleep(3)

Внутри call_python_and_mozilla_using_requests Два простых HTTP -запроса выполняются один за другим, и их текст ответа используется для составления результата:

def call_python_and_mozilla_using_requests():
    py_response = get_response_text('https://www.python.org/')
    moz_response = get_response_text('https://www.mozilla.org/en-US/')
    return (
        f'Py response piece: {py_response[:60].strip()}... ,\n'
        f'Moz response piece: {moz_response[:60].strip()}...'
    )

get_response_text Функция выполняет HTTP -запрос на заданный URL -адрес с примитивной обработкой исключений и крючками для отчета о времени выполнения запроса:

def profile_request(start_time, response, *args, **kwargs):
    elapsed_time = round((
        time.perf_counter() - start_time
    ) * 1000)
    send_stats(
        'requests_request_exec_time',
        elapsed_time,
        {'domain': URL(response.url).raw_host}
    )


def get_response_text(url):
    try:
        request_complete_callback = partial(
            profile_request,
            time.perf_counter()
        )
        response = requests.get(
            url,
            hooks={'response': request_complete_callback}
        )
        response.raise_for_status()
        return response.content.decode()
    except RequestException as e:
        send_stats(
            'requests_request_exception',
            1,
            {'domain': URL(url).raw_host, 'exception_class': e.__class__.__name__}
        )
        return f'Exception occured: {e}'

Этот код использует Запросы Библиотека ( Документы ). Основное использование для получения текстового контента из URL -адреса выглядит следующим образом:

response = requests.get(url).content.decode()

requests.get принимает необязательный крючки Аргумент, где указан функция, которая будет вызвана после завершения запроса – request_complete_callback Анкет

Эта функция обратного вызова может выглядеть смешно, если вы не знакомы с функциональным программированием. Частичный (profile_Request, time.perf_counter ()) сам по себе является функцией. Это та же функция, что и Profile_Request Но первый аргумент уже заполнен – time.perf_counter () был принят как start_time аргумент Этот трюк используется для правильного поставки start_time Для каждого запроса, как request_complete_callback Функция создается заново для каждого запроса, в то время как код для отправки времени выполнения запроса изолирован в другой функции Profile_Request Анкет Мы можем переписать это следующим образом:

def get_response_text(url):
    try:
        start_time = time.perf_counter()

        def profile_request(response, *args, **kwargs):
            elapsed_time = round((time.perf_counter() - start_time) * 1000)
            send_stats('requests_request_exec_time', elapsed_time, ...)

        response = requests.get(url, hooks={'response': profile_request})

И все будет работать хорошо. Теперь есть функция, определенная внутри функции, и get_response_text раздувается профилирующими вещами, что мне не нравится.

Вы можете прочитать больше о Частичные функции и Python functools Анкет

time.perf_counter () используется для измерения времени выполнения в Python ( Docs ). time.perf_counter () Возвращает микросекунд, которые преобразуются в миллисекунд с использованием * 1000 Анкет

Отправка статистики

send_stats Функция используется для сообщений о измерениях Telegraf: метрическое название 'requests_request_exec_time' , метрическое значение – это выполнение запроса времени, теги включают дополнительную полезную информацию (домен URL). get_response_text Также вызывает send_stats Когда происходит исключение, на этот раз проходя различное метрическое имя – 'requests_request_exception' Анкет

У меня есть еще один пост, в котором описываются способы отправки статистики из программы Python в Telegraf.

Короче, send_stats принимает метрическое имя, метрическое значение и словарь тегов. Они преобразуются в одну строку и отправляются в сокет, в котором Telegraf слушает данные измерения. Telegraf отправляет полученные метрики в базу данных (InfluxDB). Dashboard Grafana запрашивает базу данных, чтобы поместить точку на график для каждого показателя метрического значения.

Декоратор профиля

Кусок кода, который является декоратором, подходящим для любой функции (асинхронизация, синхронизация, метод класса или чистой функции), адаптирован здесь для измерения времени выполнения функции, который украшен. профиль Декоратор используется для профила общего времени выполнения функций call_python_and_mozilla_using_requests и call_python_and_mozilla_using_aiohttp (См. Следующие примеры). Не путайте с другим полезным инструментом – line_profiler – Это также обеспечивает профиль декоратор.

Запрашивает время выполнения на панели панели

Давайте запустим этот пример и настроем все инструменты мониторинга. Смотрите Примеры запуска кода о том, как запустить пример кода и настроить инфраструктуру мониторинга.

Мы можем настроить панель, которая показывает время выполнения запроса:

Синие точки общего времени выполнения примерно соответствуют сумме запроса времени на python.org и запросить mozilla.org взял (зеленые и желтые точки). Они измеряются примерно на 150 мсек в среднем.

Нужно больше исключений

Если мы изменим ‘ www.python.org ‘to’ www.python1.org ‘В функции call_python_and_mozilla_using_requests , исключения появляются в выводе терминала, а метрики исключений отправляются в Telegraf:

    Reported stats: aiohttp_request_exception=1, tags={'domain': 'www.python1.org', 'exception_class': 'ClientConnectorError'}
    'Py response piece: ...Exception occured: Cannot conn... 

Настройте отдельную панель Grafana, чтобы увидеть исключения на приборной панели:

Класс исключений отправляется в качестве тега вместе с метрическим значением. Это дает нам возможность построить разные строки для исключений разных классов. Чтобы достичь этого, выберите «Группа от – тег (exception_class)» при редактировании панели исключений запроса.

Пример 0 Улучшен: Повторное соединение

Код примера 0 может быть улучшен, чтобы повторно использовать одно и то же соединение для всех вызовов, выполняемых в том, что навсегда работал В то время как Loop – вот Улучшенная версия Анкет

Единственное значительное изменение кода – это:

...
session = requests.Session()
while True:
    result = call_python_and_mozilla_using_requests(session)
...

Создание соединения выдвинуто из В то время как петля. Теперь связь устанавливается раз и навсегда.

Давайте сравним, сколько времени требует выполнения запроса времени при повторном использовании:

Точки слева являются измерениями для оригинальной версии примера 0, а справа поступили из улучшенной версии. Мы определенно можем заметить, как общее время выполнения становится ниже, ниже 100 мсек в среднем.

Пример 1: Мониторинг времени запроса AIOHTTP

Давайте погрузимся в следующий пример кода. Вот что он делает:

  • В Forever Loop выполняет два асинкнозных HTTP -запросов с помощью aiohttp
  • крючны в aiohttp Запросить сигналы жизненного цикла
  • Отчеты запрашивать время и запрашивать исключения для телеграммы

Полный код примера 1 можно найти в Пример-1-Aiohttp-send-stats-basic.py Анкет

Поток выполнения высокого уровня аналогичен примеру 0, способ извлечения содержимого от URL-адресов отличается.

Сказка о двух HTTP -запросах

Начнем с функции call_python_and_mozilla_using_aiohttp Это выполняет два асинкновых HTTP -запросов и возвращает части содержимого ответа. Это сестра call_python_and_mozilla_using_requests Из примера 0:

async def get_response_text(url):
    try:
        async with ClientSession(trace_configs=[Profiler()]) as session:
            async with session.get(url) as response:
                response.raise_for_status()
                return await response.text()
    except ClientError as e:
        return f'Exception occured: {e}'

@profile
async def call_python_and_mozilla_using_aiohttp():
        py_response, moz_response = await asyncio.gather(
            get_response_text('https://www.python.org/'),
            get_response_text('https://www.mozilla.org/en-US/')
        )
        return (
            f'Py response piece: {py_response[:60].strip()}... ,\n'
            f'Moz response piece: {moz_response[:60].strip()}...'
        )

Здесь, aiohttp Библиотека Клиент используется для выполнения запроса ( Docs ). Основное использование для получения текстового контента из URL -адреса выглядит следующим образом:

async with ClientSession() as session:
    async with session.get(url) as response:
        return await response.text()

Что в основном происходит в get_response_text . get_response_text Также вызывает response.raise_for_status () , что повышает исключение, когда код состояния ответа является кодом ошибки или тайм -аутом. Исключение замолчало в get_response_text , так get_response_text всегда возвращается стр , либо с содержанием ответа, либо с сообщением исключения.

call_python_and_mozilla_using_aiohttp заботится о призваниях два URL -адреса, используя Asyncio.gather Анкет Порядок выполнения для call_python_and_mozilla_using_aiohttp справа:

ждет asyncio.gather Возвращает результат после завершения оба этих запросов. Общее время выполнения – это приблизительно время самого длинного запроса из этих двух. Вы, наверное, знаете, что это называется не блокирующим io. Вместо блокировки такая операция ввода -вывода освобождает поток выполнения, пока она не понадобится снова.

Синхронный, блокирующий в io, как в примере 0, имеет различный следующий порядок выполнения (см. Диаграмму выше, слева). Общее время выполнения – это приблизительно сумма времени выполнения обоих запросов. Для положительных целых чисел всегда верно, что A + b> max (a, b) Анкет Следовательно, асинхронное выполнение занимает меньше времени, чем синхронное, при условии, что в обоих случаях был доступен неограниченный процессор.

На панели, которая показывает время выполнения запросов и общее время выполнения, можно заметить, что общее время выполнения call_python_and_mozilla_using_aiohttp_exec_time Почти соответствует продолжительному времени запроса:

Общее время выполнения обоих запросов составляет 75-100 мсек.

Далее мы рассмотрим, как время исполнения каждого aiohttp Запрос сообщается.

AIOHTTP запросит сигналы

aiohttp Предоставляет способ выполнить пользовательскую функцию, когда выполнение HTTP -запроса продвигается через этапы жизненного цикла: до отправки запроса, когда соединение установлено, после получения отклика Chunk и т. Д. Для этого объект-торговец передается aiohttp. Клиентtrace_configs :

class Profiler(TraceConfig):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.on_request_start.append(on_request_start)
        self.on_request_end.append(on_request_end)
        self.on_request_exception.append(on_request_exception)

...
async with ClientSession(trace_configs=[Profiler()]) as session:
...

Profiler это подкласс aiohttp. TraceConfig . Это «подключает» функции, которые будут выполняться, когда запускается запрос ( on_request_start ), когда он заканчивается ( on_request_end ) и когда встречается исключение запроса ( on_request_exception ):

async def on_request_start(session, trace_config_ctx, params):
    trace_config_ctx.request_start = asyncio.get_event_loop().time()

async def on_request_end(session, trace_config_ctx, params):
    elapsed_time = round((
        asyncio.get_event_loop().time() - trace_config_ctx.request_start
    ) * 1000)
    send_stats(
        'aiohttp_request_exec_time',
        elapsed_time,
        {'domain': params.url.raw_host}
    )

async def on_request_exception(session, trace_config_ctx, params):
    send_stats(
        'aiohttp_request_exception',
        1,
        {'domain': params.url.raw_host, 'exception_class': params.exception.__class__.__name__}
    )

Обратите внимание, как вычисляется временная метка:

asyncio.get_event_loop().time()

Рекомендуется использовать внутренние монотонные часы Event Loop для вычисления Delta Time в асинкнозном коде.

Функции-кузлевы имеют аргументы Session, trace_config_ctx, params Анкет Давайте посмотрим на то, что они есть.

сессия это экземпляр aiohttp. Клиент Анкет

trace_config_ctx это контекст, который проходит через обратные вызовы. Пользовательские значения вызовы должны быть добавлены в него при выполнении запроса:

await session.get(url, trace_request_ctx={'flag': 'red'})
...

async def on_request_end(session, trace_config_ctx, params):
    if trace_config_ctx.trace_request_ctx['flag'] == 'red':
        ....

Таким образом, функциональный крючок может быть запрограммирован, чтобы вести себя по-разному для различных вызовов запроса или сообщать о дополнительных данных.

Запрос конечный крюк использует TRACE_CONFIG_CTX.REQUEST_START стоимость для вычисления общего времени запроса. TRACE_CONFIG_CTX.REQUEST_START Установлен в запросе Start Hook.

Params аргумент в on_request_end это aiohttp. TracerequestendParams И как таковой URL имущество. URL собственность Ярл. URL тип. params.url.raw_host Возвращает домен полученного URL -адреса. Домен отправляется в качестве тега для метрики, и это позволяет построить отдельные строки для разных URL.

Вызывающий асинхронный код из синхронного

Чтобы вызвать асинхронную функцию в контексте выполнения синхронизации, используется специальный инструмент, который адаптирован из Другая публикация Анкет Я не собираюсь погружаться в асинхронные способы Python в этом посте. Узнайте больше о Python’s асинсио , это довольно круто.

Сравните результаты, например, 0 и 1

Соединение не используется для обоих случаев здесь. Время выполнения для асинхронной версии ниже, как и ожидалось.

Пример 2: больше, больше статистики

aiohttp Предоставляет крючки для измерения больше, чем просто время выполнения запроса и исключения запроса.

Можно сообщить о статистике для:

  • Время разрешения DNS
  • DNS Cache Hit/Miss
  • В ожидании доступного времени подключения
  • соединение установления времени
  • соединение повторно используется
  • перенаправить происходящее
  • Полученная часть контента ответа
  • Запрос кусок отправлена

Впечатляет, не так ли? Документация по отслеживанию в aiohttp это Здесь Анкет

Давайте добавим больше крючков для жизненного цикла запроса:

class Profiler(TraceConfig):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.on_request_start.append(on_request_start)
        self.on_request_end.append(on_request_end)
        self.on_request_redirect.append(on_request_redirect)
        self.on_request_exception.append(on_request_exception)
        self.on_connection_queued_start.append(on_connection_queued_start)
        self.on_connection_queued_end.append(on_connection_queued_end)
        self.on_connection_create_start.append(on_connection_create_start)
        self.on_connection_create_end.append(on_connection_create_end)
        self.on_dns_resolvehost_start.append(on_dns_resolvehost_start)
        self.on_dns_resolvehost_end.append(on_dns_resolvehost_end)
        self.on_response_chunk_received.append(on_response_chunk_received)
        self.on_connection_reuseconn.append(on_connection_reuseconn)
        self.on_dns_cache_hit.append(on_dns_cache_hit)
        self.on_dns_cache_miss.append(on_dns_cache_miss)

Я не буду утомлять вас кодом для каждой функции, такой как on_dns_resulfost_end , это очень похоже на on_request_end . Полный код примера 2 – Здесь Анкет

Сообщенная статистика на приборной панели, например 2:

Мы видим, что разрешение DNS занимает пару миллисекундов и происходит для каждого вызова, а установление соединения занимает 30-40 мсек и происходит для каждого вызова. Кроме того, этот кеш DNS не поражен, DNS разрешается для каждого вызова.

Мы определенно можем улучшить это – в примере 3.

Пример 3: сеанс повторного использования AIOHTTP

Давайте изменим пример 2 кода, чтобы Клиент создан один раз, снаружи В то время как петля:

async def main_async():
    async with ClientSession(trace_configs=[Profiler()]) as session:
        while True:
            result = await call_python_and_mozilla_using_aiohttp(session)
            print(result)
            await asyncio.sleep(3)

И посмотрите, как теперь выглядят статистики:

Есть только одна точка для установления соединения, и по одной на наличие DNS Resoltion на домен. Есть много точек для события повторного использования подключения. Общее время выполнения ниже 50 мсек. Прохладный.

Полный исходный код примера 3 – Здесь Анкет

Сравните синхронизацию и асинхронные URL -адреса, с повторным использованием подключения и без него.

Общее время для обоих запросов (очень приблизительно):

Синхронизированный 80 мсек 150 мсек
Асинхро 40 мсек 80 мсек

Оригинал: “https://dev.to/cheviana/monitoring-sync-and-async-network-calls-in-python-using-tig-stack-3al5”