Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Трейсинг в микросервисной архитектуре на Python

Трейсинг в микросервисной архитектуре на Python

Василий Новиков (C.Nord, Fullstack developer) @ MoscowPython Meetup 74 (online)

"Будут затронуты следующие темы:

основная идея трейсинга микросервисов в контексте APM (application performance management), основные понятия в трейсинге на примере OpenTracing и Jaeger;
краткий обзор существующих инструментов, библиотек для трейсинга. Как обеспечить 80% трейсинга и почти не писать код;
особенности подготовки к трейсингу кода многопоточных и асинхронных (Tornado и Asyncio) приложений;
советы по тестированию кода с трейсингом;
краткий обзор будущего трейсинга — OpenTelemetry".

Видео: http://www.moscowpython.ru/meetup/74/python-microservices-tracing/

Moscow Python Meetup
PRO

June 25, 2020
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

  1. Трейсинг в микросервисах на Python
    Новиков Василий
    C.Nord

    View Slide

  2. Три кита observability

    View Slide

  3. View Slide

  4. client: get report request
    prepare report
    get data from service 1
    get data from service 2
    get from db
    get from cache
    authorization
    time
    prepare data
    prepare data
    error
    context

    View Slide

  5. Что есть на рынке?

    View Slide

  6. Основные понятия
    Trace
    Span
    - operation_name
    - start_time
    - finish_time
    - reference
    - tag
    - log
    - baggage
    span 1
    span 2
    span 3
    span 4
    time

    View Slide

  7. Host
    Основные понятия
    Application
    Tracer client
    Instrumentation
    Code
    Tracer Agent
    Tracer Backend
    Collector
    Storage
    API UI

    View Slide

  8. Process 1
    Основные понятия
    Process 2
    Request (HTTP, gRPC, …)
    intra-process
    tracing
    inter-process tracing
    intra-process
    tracing
    inject extract

    View Slide

  9. Основные понятия
    Tracer API (на примере opentracing)
    tracer = opentracing.global_tracer()
    start_span(...)
    start_active_span(...)
    active_span
    scope_manager
    extract(...)
    inject(...)
    intra-process
    inter-process
    scope_manager = tracer.scope_manager
    activate(span, finish_on_close=False)
    active

    View Slide

  10. Intra-process
    tracer = opentracing.global_tracer()
    with tracer.start_active_span('foo'):
    with tracer.start_active_span('bar'):
    # do something
    with tracer.start_active_span('baz'):
    # do something

    View Slide

  11. Intra-process
    tracer = opentracing.global_tracer()
    with tracer.start_active_span('foo'):
    with tracer.start_span('bar'):
    # do something
    with tracer.start_span('baz'):
    # do something

    View Slide

  12. Intra-process
    tracer = opentracing.global_tracer()
    foo = tracer.start_span('foo')
    scope = tracer.scope_manager.activate(foo)
    bar = tracer.start_span('bar')
    # do something
    bar.finish()
    baz = tracer.start_span('baz')
    # do something
    baz.finish()
    scope.close()
    tracer = opentracing.global_tracer()
    with tracer.start_active_span('foo'):
    with tracer.start_span('bar'):
    # do something
    with tracer.start_span('baz'):
    # do something

    View Slide

  13. Inter-process: Extract
    def handle_request(request):
    span = before_request(request)
    with tracer.scope_manager.activate(span, finish_on_close=True):
    real_handler(request)
    def before_request(request):
    tracer = opentracing.global_tracer()
    # extracting
    span_context = tracer.extract(format=Format.HTTP_HEADERS, carrier=request.headers)
    span = tracer.start_span(operation_name=request.operation, child_of=span_context)
    span.set_tag('http.url', request.full_url)
    ...
    return span

    View Slide

  14. Inter-process: Inject
    def make_request(request):
    with before_request(request):
    return urllib2.urlopen(request)
    def before_request(request):
    tracer = opentracing.global_tracer()
    op = request.operation
    span = tracer.start_span(operation_name=op)
    span.set_tag('http.url', request.full_url)
    # injecting
    http_header_carrier = {}
    tracer.inject(span_context=span, format=Format.HTTP_HEADERS, carrier=http_header_carrier)
    for key, value in http_header_carrier.items():
    request.add_header(key, value)
    return span

    View Slide

  15. 1. Неявное инструментирование: декораторы / метаклассы / миксины / патчинг.
    Например: http-клиенты, обработка входящих http-запросов, вызов методов
    интерфейсов БД / кэша и т.д.
    2. Явное инструментирование кода бизнес-логики приложения.
    Инструментирование

    View Slide

  16. Существующее инструментирование
    Open Tracing
    Оф. репозиторий библиотек https://opentracing.io/registry
    Патчер для авто-трейсинга популярных библиотек (requests, psycopg2, Celery и пр.)
    https://github.com/uber-common/opentracing-python-instrumentation
    Open Census
    https://github.com/census-instrumentation/opencensus-python#integration
    Open Telemetry
    https://github.com/open-telemetry/opentelemetry-python

    View Slide

  17. Scope Manager
    ThreadLocalScopeManager (default, python 2.7+)
    ContextVarsScopeManager (python 3.7+)
    AsyncioScopeManager
    TornadoScopeManager
    GeventScopeManager

    View Slide

  18. Scope Manager
    import jaeger_client
    config = jaeger_client.Config(
    config={...},
    scope_manager=ContextVarsScopeManager()
    )
    tracer = config.initialize_tracer()

    View Slide

  19. Синхронный Python

    View Slide

  20. Многопоточное приложение
    def bar():
    with global_tracer().start_active_span('bar'):
    with global_tracer().start_active_span('baz'):
    pass
    def foo():
    with global_tracer().start_active_span('foo'):
    t = Thread(target=bar)
    t.start()
    t.join()

    View Slide

  21. Многопоточное приложение
    def bar():
    with global_tracer().start_active_span('bar'):
    with global_tracer().start_active_span('baz'):
    pass
    def foo():
    with global_tracer().start_active_span('foo'):
    t = Thread(target=bar)
    t.start()
    t.join()

    View Slide

  22. Многопоточное приложение
    Если нужен auto-propagation:
    1. Делать обертки кода, в которых порождаются потоки.
    2. Патчить threading.Thread.

    View Slide

  23. def bar(parent_span):
    tracer = global_tracer()
    with tracer.scope_manager.activate(
    parent_span, finish_on_close=False):
    with tracer.start_active_span('bar'):
    with tracer.start_active_span('baz'):
    pass
    def foo():
    with global_tracer().start_active_span('foo') as scope:
    t = Thread(target=bar, kwargs=dict(span=scope.span))
    t.start()
    t.join()
    Многопоточное приложение

    View Slide

  24. Обработка исключений
    with tracer.start_active_span('get_report'):
    get_report()

    View Slide

  25. Обработка исключений
    span = tracer.start_span('get_report')
    tracer.scope_manager.activate(span)
    try
    get_report()
    exception Exception as e:
    span.set_tag(tags.ERROR, True)
    span.log_kv({...})
    raise e
    finally:
    span.finish()
    with tracer.start_active_span('get_report'):
    get_report()

    View Slide

  26. Асинхронный Python

    View Slide

  27. Callback hell
    Задача: трейсить вызов функции с колбэком.
    def send_sms(phone, body, on_done):
    # send sms
    on_done(status)

    View Slide

  28. Callback hell
    def trace_send_sms(original):
    @functools.wraps(original)
    def _send_sms(phone, body, on_done):
    span = global_tracer().start_span('send_sms')
    span.set_tag('phone', phone)
    with tracer.scope_manager.activate(
    span, finish_on_close=False
    ):
    original(phone, body, wrapped_cb(on_done, span))
    return _send_sms
    def wrapped_cb(cb, span):
    @functools.wraps(cb)
    def _on_done(status, res):
    with global_tracer().scope_manager.activate(
    span, finish_on_close=True
    ):
    span.set_tag('status', status)
    cb(status, res)
    return _on_done

    View Slide

  29. async / await
    ContextVarsScopeManager (python 3.7+) vs AsyncioScopeManager (3.4+)
    Следует использовать ContextVarsScopeManager.
    Работает на базе contextvars.
    Имеет auto-propagation для запланированных в loop корутин.
    Для python < 3.7, нужен патч (см. https://github.com/fantix/aiocontextvars/pull/66)

    View Slide

  30. async / await
    loop = asyncio.get_event_loop()
    async def make_order(...):
    with tracer.start_active_span('make_order'):
    order = await prepare_order()
    # do something
    loop.create_task(send_sms(phone, message))

    View Slide

  31. 1. Неявное инструментирование: декораторы / метаклассы / миксины / патчинг.
    Например: http-клиенты, обработка входящих http-запросов, вызов методов
    интерфейсов БД / кэша и т.д.
    2. Явное инструментирование кода бизнес-логики приложения.
    3. Предусмотрите “рубильник”, которым можно выключить трейсинг на уровне
    кодовой базы.
    Инструментирование

    View Slide

  32. Тегирование и логирование спанов
    Заранее подумайте о тегах, которые будут включены в спаны (идентификаторы
    запросов, клиентов, версий приложения и т.д.). Это поможет найти интересующий
    трейс во время эксплуатации.
    Для стандартных тегов рекомендуется придерживаться спецификации.
    Например для OT:
    https://github.com/opentracing/specification/blob/master/semantic_conventions.md
    Для спана, завершившегося ошибкой:
    span.set_tag(tags.ERROR, True) # "error"=true

    View Slide

  33. Сэмплирование
    Сэмплирование в Jaeger:
    - четыре режима: Constant, Probabilistic, Rate Limiting, Remote;
    - сэмплируется начальный спана трейса (head-based sampling);
    - сэмплирование последующих спанов наследуется у родителя;
    - чтобы форсировать сэмплирование -- установить тег “sampling.priority”.

    View Slide

  34. Сэмплирование
    Может быть удобно при отладке: тег приоритета сэмплирования “sampling.priority”
    if request.get('debug'):
    span = tracer.start_span(
    operation_name='foo',
    tags={tags.SAMPLING_PRIORITY: 1}
    )

    View Slide

  35. Сэмплирование
    В Jaeger нет tail-based sampling

    View Slide

  36. Тестирование
    1. Пишите unit-тесты для кода инструментирования.
    2. Тестируйте свой код с включенным и выключенным трейсингом.
    Пример из жизни:

    View Slide

  37. Клиент и агент

    View Slide

  38. Клиент и агент
    Особенности jaeger client для Python:
    1. Отправка трейсов только по UDP. Клиент и агент должны быть на одном хосте.
    2. Клиент на базе Tornado (асинхронный фреймворк).
    3. По умолчанию, для отправки трейсов порождается новый поток.
    4. В многопроцессных приложениях клиент должен инстанциироваться после форка,
    иначе можно получить deadlock.

    View Slide

  39. По умолчанию, для отправки трейсов порождается новый поток.
    Это может отрицательно сказываться на производительности асинхронных
    приложений.
    Клиент и агент
    Python 2.7 + Tornado 5

    View Slide

  40. Open Telemetry
    1. Метрики и трейсинг (и теперь еще логи).
    2. Совместим с Open Tracing и Open Census.
    3. Поддерживает W3C Trace Context (https://www.w3.org/TR/trace-context/)
    4. Статус: beta.
    5. Из коробки имеет инструментирование для популярных библиотек.
    6. Python 3.4+
    7. Интеграция с коммерческими системами.

    View Slide

  41. Спасибо за внимание
    t.me/novikov_v
    linkedin.com/in/novikovv

    View Slide