Один стендап в Яндекс.Такси, или Чему нужно научить бэкенд-разработчика

от автора

Моё имя Олег Ермаков, я работаю в команде бэкенд-разработки приложения Яндекс.Такси. У нас принято проводить ежедневные стендапы, где каждый из нас рассказывает о сделанных за день задачах. Вот как это бывает…

Имена сотрудников может и изменены, а вот задачи вполне себе реальны!

На часах 12:45, вся команда собирается в переговорке. Первым слово берет Иван, стажёр-разработчик.

Иван:

Я работал над задачей отображения всех возможных вариантов сумм, которые пассажир мог дать водителю при известной стоимости поездки. Задача достаточно известная — называется «Размен монет». С учётом специфики добавил в алгоритм несколько оптимизаций. Отдал пул-реквест на ревью еще позавчера, но с тех пор я исправляю замечания.

По довольной улыбке Анны стало понятно, чьи замечания исправляет Иван.

В первую очередь произвёл минимальную декомпозицию алгоритма, раcхардкодил получение банкнот. В первой реализации возможные банкноты были прописаны в коде, поэтому вынес их в конфиг по странам.

Добавил комментариев на будущее, чтобы любой читающий мог быстро разобраться в алгоритме:

for exception in self.exceptions[banknote]:     exc_value = value + exception.delta     if exc_value - cost >= banknote:         continue     if exc_value > cost >= exception.banknote:         banknote_results.append(exc_value)  # основные разветвления алгоритма дают некратные купюры for exception in self.exceptions[banknote]:     # для таких исключений можно посчитать результат по остатку от     # деления таких купюр     exc_value = value + exception.delta     # но при этом результат не может получиться больше самой банкноты     # (corner case)     if exc_value - cost >= banknote:         continue     if exc_value > cost >= exception.banknote:         banknote_results.append(exc_value)

Ну и, естественно, остаток времени потратил на покрытие всего кода тестами.

RUB = [1, 2, 5, 10, 50, 100, 200, 500, 1000, 2000, 5000] CUSTOM_BANKNOTES = [1, 3, 7, 11]  @pytest.mark.parametrize(     'cost, banknotes, expected_changes',     [         # no banknotes         (             321, [], [],         ),         # zero cost         (             0, RUB, [],         ),         # negative cost         (             -13, RUB, [],         ),         # simple testcase         (             264, RUB, [265, 270, 300, 400, 500, 1000, 2000, 5000],         ),         # cost bigger than max banknote         (             6120, RUB, [6121, 6150, 6200, 6300, 6500, 7000, 8000, 10000],         ),         # min cost         (             1, RUB, [2, 5, 10, 50, 100, 200, 500, 1000, 2000, 5000],         ),         ...     ], )

Помимо обычных тестов, которые запускаются при каждом билде проекта, написал тест, использующий алгоритм без оптимизаций (считай — полный перебор). Результат работы этого алгоритма для каждой купюры из первых 10 тысяч случаев положил в файл и прогнал отдельно на алгоритме с оптимизациями, чтобы быть уверенным, что он действительно работает верно.

Давайте на минуту отвлечёмся от стендапа и подведем локальные итоги всего того, о чём говорит Иван. При написании кода основная цель — обеспечить его работоспособность. Чтобы эта цель была достигнута, необходимо выполнить следующие задачи:

  • Декомпозировать бизнес-логику на атомарные фрагменты. Читаемость усложняется при просмотре полотна кода, написанного в одной функции.
  • Добавить комментарии в «особо сложные» части кода. У нас в команде следующий подход: если на код-ревью задают вопрос по поводу реализации (просят объяснить алгоритм), то необходимо добавить комментарий. А ещё лучше подумать об этом заранее и добавить его самому.
  • Написать тесты, покрывающие основные ветви выполнения алгоритмов. Тесты — не только метод проверки работоспособности кода. Они ещё выполняют роль примера использования вашего модуля.

Увы, но даже специалисты с многолетним опытом не всегда используют эти подходы в своей работе. В Школе бэкенд-разработки, которую мы сейчас делаем, студенты получат практические навыки написания архитектурно качественного кода. Ещё одна наша цель — распространение практик покрытия проекта тестами.

Но вернёмся на стендап. После Ивана выступает Анна.

Анна:

Я разрабатываю микросервис отдачи промотирующих изображений. Как вы помните, сервис изначально отдавал статичные данные-стабы. Затем тестировщики попросили кастомизировать их, и я вынесла их в конфиг, а сейчас делаю «честную» реализацию с отдачей данных из базы (PostgreSQL 10.9). Мне очень помогла заложенная изначально декомпозиция, в рамках которой интерфейс получения данных в бизнес-логике не меняется, а каждый новый источник (будь то конфиг, база данных или внешний микросервис) лишь реализует свою логику.

Я проверила написанную систему под нагрузкой, тестирование показало, что ручка начинает резко тормозить, когда мы ходим в БД. По explain увидела, что индекс не используется. Пока не придумала, как пофиксить.

Вадим:

А что за запрос?

Аня:

Два условия под OR:

SELECT * FROM table_1          JOIN table_2 ON table_1.some_id = table_2.some_id WHERE (table_2.attr1 = 'val' OR table_1.attr2 IN ('val1', 'val2'))   AND table_1.deleted_at IS NULL   AND table_2.deleted_at IS NULL ORDER BY table_2.created_at 

Explain запроса показал, что в нём не используется один из индексов по атрибутам attr1 таблицы table_2 и attr2 таблицы table_1.

Вадим:

Сталкивался с аналогичным поведением в MySQL, проблема как раз в условии по OR, из-за которого используется лишь один индекс, скажем, attr2. А второе условие использует seq scan — полный проход по таблице. Запрос можно разбить на два независимых запроса. Как вариант — разделить и замержить результат запросов на стороне бэкенда. Но тогда нужно подумать над тем, чтобы обернуть эти два запроса в транзакцию, либо объединить их с помощью UNION — по сути, на стороне базы:

SELECT * FROM table_1          JOIN table_2 ON table_1.some_id = table_2.some_id WHERE (table_2.attr1 = 'val')   AND table_1.deleted_at IS NULL   AND table_2.deleted_at IS NULL ORDER BY table_2.created_at   SELECT * FROM table_1 JOIN table_2 ON table_1.some_id = table_2.some_id WHERE (table_1.attr2 IN ('val1'     , 'val2')) AND table_1.deleted_at IS NULL AND table_2.deleted_at IS NULL ORDER BY table_2.created_at

Аня:

Спасибо, попробую ^_^

Снова подведём итоги:

  • Почти все задачи продуктовой разработки связаны с получением записей из внешних источников (сервисов или баз данных). Нужно тщательно подойти к вопросу декомпозиции классов, выгружающих данные. Правильно спроектированные классы позволят вам без проблем писать тесты и модифицировать источники данных.
  • Чтобы эффективно работать с БД, нужно знать особенности выполнения запросов, например разбираться в explain.

Работа с информацией и организация потоков данных — неотъемлемая часть задач любого бэкенд-разработчика. Школа познакомит с архитектурой взаимодействия сервисов (и источников данных). Студенты научатся работать с базами архитектурно и с точки зрения эксплуатации — миграции данных и тестирования.

Последним на встрече выступает Вадим.

Вадим:

Я на неделе дежурил, разбирал очередь инцидентов. Одна нелепая ошибка в коде заняла ну очень много времени: в проде не было логов по запросу, хотя их создание было прописано в коде.

По скорбному молчанию всех присутствующих понятно — все уже так или иначе сталкивались с проблемой.

Для получения всех логов в рамках запроса используется request_id, который прокидывается во все записи в следующем виде:

# запись без request_id logger.info(     'my log msg',  )  # запись с request_id logger.info(     'my log msg',      extra=log_extra,  # здесь передается request_id — связующая информация о запросе )

log_extra — это словарь с метаинформацией запроса, ключи и значения которого будут записаны в лог. Без передачи log_extra в функцию логирования запись не будет связана со всеми другими логами, потому что в ней не будет request_id.

Пришлось исправлять ошибку в сервисе, перевыкатывать его и лишь потом разбираться с инцидентом. Такое случается уже не первый раз. Чтобы больше это не повторялось, я постарался исправить проблему глобально и избавиться от log_extra.

Сначала я написал враппер над стандартным исполнением запроса:

async def handle(self, request, handler):     log_extra = request['log_extra']     log_extra_manager.set_log_extra(log_extra)     return await handler(request)

Нужно было решить, каким образом хранить log_extra в рамках одного запроса. Здесь было два варианта. Первый — изменить task_factory для eventloop из asyncio:

class LogExtraManager:     __init__(self, context: Any,                   settings: typing.Optional[Dict[str, dict]],                   activations_parameters: list) -> None:     loop = asyncio.get_event_loop()     task_factory = loop.get_task_factory()     if task_factory is None:         task_factory = _default_task_factory          @functools.wraps(task_factory)     def log_extrad_factory(ev_loop, coro):         child_task = task_factory(ev_loop, coro)         parent_task = asyncio.Task.current_task(loop=ev_loop)         log_extra = getattr(parent_task, LOG_EXTRA_CONTEXT_KEY, None)         setattr(child_task, LOG_EXTRA_CONTEXT_KEY, log_extra)         return child_task          # updating loop, so any created task will     # get the log_extra of its parent     loop.set_task_factory(log_extrad_factory)      def set_log_extra(log_extra: dict):          loop = asyncio.get_event_loop()          task = asyncio.Task.current_task(loop=loop)          setattr(task, LOG_EXTRA_CONTEXT_KEY, log_extra)

Второй вариант — «протолкнуть» через команду инфраструктуры переход на Python 3.7 для использования contextvars:

log_extra_var = contextvars.ContextVar(LOG_EXTRA_CONTEXT_KEY)  class LogExtraManager:     def set_log_extra(log_extra: dict):          log_extra_var.set(log_extra)

Ну и дальше нужно было пробросить сохраненную в контексте log_extra в logger.

class LogExtraFactory(logging.LogRecord):     # this class allows to create log rows with log_extra in the record      def __init__(self, *args, **kwargs):         super().__init__(*args, **kwargs)         task = asyncio.Task.current_task()         log_extra = getattr(task, LOG_EXTRA_CONTEXT_KEY, None)         if not log_extra:             return         for key in log_extra:             self.__dict__[key] = log_extra[key]  logging.setLogRecordFactory(LogExtraFactory)

Итоги:

  • В Яндекс.Такси (да и повсеместно в Яндексе) активно используется asyncio. Важно не только уметь его использовать, но и понимать его внутреннее устройство.
  • Выработайте в себе привычку читать чейнджлоги всех новых версий языка, думайте, как вы можете облегчить жизнь себе и коллегам с помощью нововведений.
  • При работе со стандартными библиотеками не бойтесь залезать в их исходный код и разбираться в их устройстве. Это очень полезный навык, который позволит вам глубже понять работу модуля и откроет новые возможности в реализации фич.

Преподаватели Школы бэкенда съели не один пуд соли и набили уйму шишек в асинхронной работе сервисов. Они расскажут студентам об особенностях асинхронной работы Python — и на уровне применения на практике, и в части разбора внутренностей пакетов.

Книги и ссылки

В изучении Python вам могут помочь:

  • Три книги: Python Cookbook, Diving Into Python 3 и Python Tricks.
  • Видеолекции таких столпов IT-индустрии, как Реймонд Хеттингер и Дэвид Бизли. Из видеолекций первого можно выделить доклад «Beyond PEP 8 — Best practices for beautiful intelligible code». У Бизли советую посмотреть выступление про asyncio.

Чтобы обрести более высокоуровневое понимание архитектуры, прочтите книги:

  • «Высоконагруженные приложения». Здесь подробно расписаны вопросы взаимодействия с данными (кодирование данных, работа с распределёнными данными, репликация, секционирование, транзакции и т. д.).
  • «Микросервисы. Паттерны разработки и рефакторинга». В книге показаны основные подходы к микросервисной архитектуре, описаны недостатки и проблемы, с которыми приходится сталкиваться при переходе с монолита на микросервисы. В посте про них почти ничего нет, но всё равно советую прочитать эту книгу. Вы начнёте понимать тенденции в построении архитектур и изучите основные практики декомпозиции кода.

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

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


ссылка на оригинал статьи https://habr.com/ru/company/yandex/blog/464179/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *