Трейсинг в микросервисной архитектуре на 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/

53b0434aded1fb944ec3037c382158c1?s=128

Moscow Python Meetup

June 25, 2020
Tweet

Transcript

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

  2. Три кита observability

  3. None
  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
  5. Что есть на рынке?

  6. Основные понятия Trace Span - operation_name - start_time - finish_time

    - reference - tag - log - baggage span 1 span 2 span 3 span 4 time
  7. Host Основные понятия Application Tracer client Instrumentation Code Tracer Agent

    Tracer Backend Collector Storage API UI
  8. Process 1 Основные понятия Process 2 Request (HTTP, gRPC, …)

    intra-process tracing inter-process tracing intra-process tracing inject extract
  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
  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
  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
  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
  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
  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
  15. 1. Неявное инструментирование: декораторы / метаклассы / миксины / патчинг.

    Например: http-клиенты, обработка входящих http-запросов, вызов методов интерфейсов БД / кэша и т.д. 2. Явное инструментирование кода бизнес-логики приложения. Инструментирование
  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
  17. Scope Manager ThreadLocalScopeManager (default, python 2.7+) ContextVarsScopeManager (python 3.7+) AsyncioScopeManager

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

    tracer = config.initialize_tracer()
  19. Синхронный Python

  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()
  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()
  22. Многопоточное приложение Если нужен auto-propagation: 1. Делать обертки кода, в

    которых порождаются потоки. 2. Патчить threading.Thread.
  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() Многопоточное приложение
  24. Обработка исключений with tracer.start_active_span('get_report'): get_report()

  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()
  26. Асинхронный Python

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

    body, on_done): # send sms on_done(status)
  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
  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)
  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))
  31. 1. Неявное инструментирование: декораторы / метаклассы / миксины / патчинг.

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

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

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

    if request.get('debug'): span = tracer.start_span( operation_name='foo', tags={tags.SAMPLING_PRIORITY: 1} )
  35. Сэмплирование В Jaeger нет tail-based sampling

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

    код с включенным и выключенным трейсингом. Пример из жизни:
  37. Клиент и агент

  38. Клиент и агент Особенности jaeger client для Python: 1. Отправка

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

    отрицательно сказываться на производительности асинхронных приложений. Клиент и агент Python 2.7 + Tornado 5
  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. Интеграция с коммерческими системами.
  41. Спасибо за внимание t.me/novikov_v linkedin.com/in/novikovv