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

Делаем шаблон типового микросервиса

Делаем шаблон типового микросервиса

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

Выступление для https://podlodka.io/pythoncrew

Denis Anikin

December 03, 2023
Tweet

More Decks by Denis Anikin

Other Decks in Programming

Transcript

  1. Денис Аникин
    https://xfenix.ru
    Делаем шаблон
    типового
    микросервиса

    View full-size slide

  2. Может вам и не нужен очередной fastapi
    cookiecutter?

    View full-size slide

  3. Денис Аникин
    4
    Что я делаю
    — работаю в Райфе
    — lead разработки в 3+ командах
    — community lead в Python Community
    — fullstack: typescript, python, devops
    — выступаю на конференциях и митапах
    https://xfenix.ru

    View full-size slide

  4. Чем вам поможет
    доклад?
    5

    View full-size slide

  5. Опишу проблематику
    6
    —У нас есть повторяемые микросервисы
    —Их достаточно много (от 3, например)
    —Мы хотим надежности в каждом сервисе
    —У нас есть много похожих базовых настроек

    View full-size slide

  6. Что будет входить в
    идеальный шаблон?
    7

    View full-size slide

  7. Список важных штук
    8
    —Кодовый стиль и подход к кодовому стилю
    —Автоматизации кодового стиля
    —Общие настройки и подходы, папки, файлы
    —Минимум копипасты
    —Общий CI/CD

    View full-size slide

  8. Стартуем
    9

    View full-size slide

  9. А что тут сказать?
    11
    —«Базовый» асинхронный фреймворк (альтернатива: litestar)
    —Идеально для микросервисов, базирующихся на REST

    View full-size slide

  10. ASGI: granian
    12

    View full-size slide

  11. Обидно вообще-то

    View full-size slide

  12. Осторожные замеры показали
    14
    —Granian значительно обходит uvicorn по RPS и Latency
    —Я делал об этом доклад на pitepy https://github.com/xfenix/piterpy-imya-mne-
    skorost
    —Просто заменой ASGI сервера, который для большинства представляет из
    себя, по сути, почти невидимую прослойку, мы получаем значительное
    ускорение вашего бекенда

    View full-size slide

  13. Какая мощь! Откуда?
    Нанотехнологии!

    View full-size slide

  14. Наш выбор
    18
    —Dependency Injector
    —Создаем (для начала) общий ioc.py и кладем зависимости туда
    —Вдвойне полезно, если ваши ручки перемешиваются с EDA архитектурой
    (появляются консьюмеры и продюсеры)

    View full-size slide

  15. 19
    Пример
    class IOCContainer(containers.DeclarativeContainer):
    orm_engine_replica = providers.Resource(resources.create_engine_resource)
    orm_session_factory_replica = providers.Resource(resources.create_session_factory,
    engine=orm_engine_replica)
    dialogs_orm_repository = providers.Factory(DialogsORMRepository,
    session_factory=orm_session_factory_replica)
    private_users_orm_repository = providers.Factory(
    PrivateUsersORMRepository, session_factory=orm_session_factory_replica
    )
    speech_analytics_service = providers.Factory(
    SpeechAnalyticsService,
    dialogs_repository=dialogs_orm_repository,
    private_users_repository=private_users_orm_repository,
    )
    httpx_connection: providers.Resource[httpx.AsyncClient] =
    providers.Resource(resources.create_httpx_resource)
    database_master_connection: providers.Resource[Database] =
    providers.Resource(resources.create_database_resource)
    kafka_producer: providers.Resource[generic_producer.AsyncKafkaProducer] = providers.Resource(
    resources.create_kafka_resource
    )
    ...

    View full-size slide

  16. 1 «грязный» ioc.py и
    чистейшая бизнес-
    логика в остальных
    местах
    20

    View full-size slide

  17. Подключение к базам
    21

    View full-size slide

  18. Надежность
    22
    —Пулинг (обязательно ограничивайте и учитывайте реплики в k8s)
    —Реконнект на ошибки с помощью backoff

    View full-size slide

  19. 23
    Пример
    class RetrySession(Session):
    @backoff.on_exception(
    backoff.expo,
    (psycopg.DatabaseError, sqlalchemy.exc.SQLAlchemyError),
    max_tries=settings.postgres_connection_tries,
    )
    def execute(
    self,
    statement: Executable,
    params: _CoreAnyExecuteParams | None = None,
    ...
    ) -> Result[typing.Any]:
    return super().execute(
    statement=statement,
    params=params,
    ...
    )

    View full-size slide

  20. Code style
    24

    View full-size slide

  21. Ваш друг #1: Ruff
    25

    View full-size slide

  22. Ваш друг #2: Mypy
    26

    View full-size slide

  23. Обосновываю почему
    27
    —Это популярные инструменты
    —Вам не нужно делать очень сложную конфигурацию из 30 плагинов
    —Вам не нужно делать специальную тайную конфигурацию в стиле «ты не
    поймешь» (тм), в стиле «для тех кто понимает и копает вглубь» (тм) и т.п.
    —Легкодоступная информация и легко воспроизводимая конфигурация

    View full-size slide

  24. Ruff исправляет.
    Ошибки.
    За вас!
    29

    View full-size slide

  25. Но если только вы
    включите все правила
    30

    View full-size slide

  26. Ещё бонус
    31
    —Ruff недавно представил ruff formatter
    —Drop-in replacement для black
    —НО, есть отличия https://docs.astral.sh/ruff/formatter/black/
    —НО (2), есть несовместимые правила линтера
    https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules

    View full-size slide

  27. Как ruff
    сконфигурировать?
    32

    View full-size slide

  28. Пример CLI
    33
    ruff check .\
    --ignore=I,EM,FBT,TRY003,S101,D1,FA,ANN101\
    --line-length=120\
    --fixable=ALL\
    --select=ALL \
    --unsafe-fixes

    View full-size slide

  29. Пример CLI с black
    34
    Да, это не совсем пакетное предложение
    ruff format .

    View full-size slide

  30. pyproject.toml
    35
    Для конфигурации ruff (в бонус пресет для тестов)
    [tool.ruff]
    fix = true
    unsafe-fixes = true
    line-length = 120
    select = ["ALL"]
    ignore = ["D1", "D203", "D213", "FA102", "ANN101"]
    [tool.ruff.isort]
    no-lines-before = ["standard-library", "local-folder"]
    known-third-party = []
    known-local-folder = ["whole_app"]
    [tool.ruff.extend-per-file-ignores]
    "tests/*.py" = ["ANN401", "S101", "S311"]

    View full-size slide

  31. Отмечу: пока ruff isort
    + ruff formatter пока не
    до конца умеют pep8 L
    36

    View full-size slide

  32. Некоторые правила мы все же отключаем
    37
    Лишь небольшая часть
    —I (возможно) так как передать конфигурацию глобально невозможно. Если
    вы индвидуально запускаете ruff в каждом проекте, то можно использовать
    pyproject.toml и отказаться ещё и от isort!
    —TRY003 писать message в exception это нормально
    —D1 можно пропускать все докстринги
    —ANN101 можно отключать, если вы не хотите везде подписывать typing.Self,
    так как он нормально выводится mypy сейчас автоматически
    —EM если вы любите добавлять сообщения в ваши эксепшены
    —FBT довольно спорный набор правил, запрещающий bool аргументы
    —(помним) про несовместимую простыню исключений для format
    https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules

    View full-size slide

  33. Некоторые правила для тестов
    38
    Лишь небольшая часть
    —S101 (вы наверняка хотите ассерты в тестах)
    —S311 (random в тестах — приемлимо)
    —ANN401 (иногда хочется any)

    View full-size slide

  34. И это не всё
    39

    View full-size slide

  35. Скорость!
    40
    И это не обман

    View full-size slide

  36. Другие интересные нюансы
    41
    —Более 600700 правил и число растет быстро
    —Есть кеширование
    —Дружит с монорепами

    View full-size slide

  37. Кое-что ещё:
    делайте все
    автоматическим
    42

    View full-size slide

  38. Как этого достичь?
    43
    —Вам нужен vscode
    —Вам понадобятся стандартные плагины mypy и ruff (black и isort, считай,
    мы уже выселяем)

    View full-size slide

  39. 44
    Vscode конфигурация
    "[python]": {
    "editor.defaultFormatter": "charliermarsh.ruff",
    "editor.formatOnPaste": true,
    "editor.formatOnType": true,
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
    "source.fixAll": true,
    "source.organizeImports": true
    }
    },

    View full-size slide

  40. Аннотации типов
    45

    View full-size slide

  41. Как стартануть?
    46
    Я знаю, может быть не просто
    —Берете ruff
    —Берете mypy
    —Включите режим strict в mypy
    —Включаете все правила в ruff
    —Они вас заставят J

    View full-size slide

  42. Стремитесь к покрытию кода в 100% и strict в mypy
    Аннотации tips & tricks
    47
    —Освойте не только стандартные типы, но и Literal и особенно Final
    —Final имеет смысл аннотировать все переменные вообще по-умолчанию (чаще всего
    изменение переменных и ведет к ошибкам)
    —Примитивные типы можно не аннотировать (a = 5 не надо расписывать как a: int = 5)
    —Дженерики
    —Annotated помогает в валидации
    —В модуле гораздо больше всего, чем Union, Optional, Dict, List и т.п.
    —сast, overload, final, never, noreturn, typealias, newtype, classvar, typeguard, unpack,
    Protocol, assert_type, assert_never, TYPE_CHECKING — полезные инструменты

    View full-size slide

  43. 48
    Пример с Final: до
    some_value = 5
    some_another_value = 'kek’
    for one_element in range(100):
    some_another_value = some_another_value * some_value
    some_value += 1

    View full-size slide

  44. 49
    Пример с Final: после
    import typing
    some_value: typing.Final = 5 # <- тут само выведет typing.Final[typing.Literal[5]]
    some_another_value: typing.Final = 'kek'
    for one_element in range(100):
    some_another_value = some_another_value * some_value # <- тайпчекер поймает
    some_value += 1

    View full-size slide

  45. 50
    TYPE_CHECKING

    View full-size slide

  46. 51
    Как победить coverage
    [tool.coverage.report]
    exclude_also = [
    "if typing.TYPE_CHECKING",
    ]

    View full-size slide

  47. Error Tracking
    52

    View full-size slide

  48. Так sentry же…?
    54

    View full-size slide

  49. Так же у вас есть
    более простая
    альтернатива
    55

    View full-size slide

  50. Совместима c
    sentry sdk
    57

    View full-size slide

  51. Observability
    58

    View full-size slide

  52. Тут всё непросто
    59

    View full-size slide

  53. 60
    Сейчас кто-то такой:

    View full-size slide

  54. OpenTelemetry
    61

    View full-size slide

  55. Как настроить телеметрию?
    63
    —Можно запустить автоматический инструментатор (opentelemetry-instrument), обернув
    в него запуск вашего сервиса
    https://opentelemetry.io/docs/instrumentation/python/automatic/example/#execute-the-
    automatically-instrumented-server
    —Либо настраивать вручную https://opentelemetry.io/docs/instrumentation/python/ (мы
    скорее идем по этому пути)
    —Кроме телеметрии вы можете собирать метрики
    —И так же сразу можете настроить отдачу метрик
    https://opentelemetry.io/docs/instrumentation/python/exporters/#prometheus с помощью
    prometheus-client и opentelemetry-exporter-prometheus

    View full-size slide

  56. Логгирование
    64

    View full-size slide

  57. Мы не используем для
    этого OpenTelemetry
    65

    View full-size slide

  58. А ещё…
    66

    View full-size slide

  59. Что сюда входит?
    68
    —Настраиваем конфигурацию structlog для json логов
    —https://www.youtube.com/watch?v=d2WcVUbKdbw не используем loguru
    —Не используем асинхронный логгер
    —Настраиваем MemoryHandler
    —* В идеале неплохо ещё добавить span id и trace id из open telemetry

    View full-size slide

  60. Локальная разработка
    69

    View full-size slide

  61. Ошибки, которые мы совершили
    70
    —Монолитная разработка
    —Разрабатывать все в одном compose
    —Разрабатывать в kuber кластере

    View full-size slide

  62. Так что же взять?
    71
    —Имеет смысл брать docker-compose
    —Добавляем в него необходимые инфраструктурные зависимости
    —Запускаем в нём тесты

    View full-size slide

  63. Ваша цель:
    больше статического
    анализа
    73

    View full-size slide

  64. Почему?
    74

    View full-size slide

  65. Пока роботы работают,
    вы отдыхаете
    75

    View full-size slide

  66. Роботы не ошибаются
    77

    View full-size slide

  67. 78
    Нууу…

    View full-size slide

  68. Отбросив шутки:
    нам так проще отловить
    все типовые сценарии, от
    которых мы все уже устали
    79

    View full-size slide

  69. Что пригодилось нам (и может пригодиться вам)
    80
    —Ruff
    —Mypy
    —Refurb
    —Trivy (static)
    —Deptry
    —Kube score
    —Hadolint

    View full-size slide

  70. Бонус-трек
    81
    Мы не используем (ещё), но вы можете нас обогнать…
    —Wily
    —Cohesion
    —Autoimport
    —Vulture

    View full-size slide

  71. Сборка #1: kaniko
    82

    View full-size slide

  72. Сборка #2: docker
    83

    View full-size slide

  73. Почему?
    84
    —Kaniko быстрый и не требует рута
    —Во всем приятнее собирать, чем с помощью docker, однако
    —Вы хотите --mount secret функциональность. Тогда ваш вариант:
    официальный docker c syntax 1.3

    View full-size slide

  74. Тесты запускаются в том
    самом docker-compose
    85

    View full-size slide

  75. Еще я бы добавил какой-то
    SAST сканер, но по ним у
    нас нет консенсуса…
    86

    View full-size slide

  76. Релизимся по тегу из
    мастера
    87

    View full-size slide

  77. Опция: можно ставить теги
    на мастер автоматом при
    мерже
    88

    View full-size slide

  78. Если вдруг вам интересно как
    89
    Тут примерный скрипт
    —https://github.com/xfenix/pycon2022/blob/main/scripts/auto-semver.py

    View full-size slide

  79. Как всё это упаковать?
    90

    View full-size slide

  80. 1. Основной layout, code style
    91
    —Cookiecutter
    —Бонус: scaraplate
    —Общий гайд
    —Здесь у нас лежат pyproject, нужные папки, кубер манифесты
    (бонус: вы можете сделать наследие от общего helm чарта, но
    мне эти универсальные чарты не близки)

    View full-size slide

  81. 92
    Cookiecutter — пример вызова
    cookiecutter https://github.com/tiangolo/full-stack-fastapi-postgresql

    View full-size slide

  82. 2. CI/CD
    93
    Gitlab version
    —Централизованный пайплайн с помощью include
    —Внутри имеет смысл обильно использовать reference и наследование
    —Имеет смысл версионировать шаблоны
    —Имеет смысл делать пресеты
    —Немного устаревший, но нормальный пример
    https://github.com/xfenix/pycon2022

    View full-size slide

  83. 2. CI/CD
    94
    Github actions
    —Маркетплейс ваш бро
    —Пока инклудов/шаринга нет…

    View full-size slide

  84. 3. Базовая настройка сервиса
    95
    —Мы делаем свой пакет
    —Этот пакет позволяет «обернуть» настройку в одну функцию и прокинуть
    туда все нужные опции (бонус: можно совместить с pydantic base settings)
    —В неё входит: opentelemetry, sentry/glitchtip, logging

    View full-size slide

  85. def build_app() -> FastAPI:
    fastapi_app = FastAPI()
    bootstrap_fastapi(
    fastapi_application=fastapi_app,
    sentry_parameters={
    "sentry_dsn": settings.sentry_dsn,
    },
    opentelemetry_parameters={
    "opentelemetry_endpoint": settings.opentelemetry_endpoint,
    "service_name": settings.awesome_service,
    "namespace": settings.namespace,
    "service_version": settings.version,
    "container_name": settings.container_name,
    "instruments": [
    RedisInsrumentator(),
    AioPikaInstrumentator(),
    ]
    },
    )
    return fastapi_app
    Как может выглядеть

    View full-size slide

  86. Памятка по работе
    97

    View full-size slide

  87. TBD
    98
    Мой взгляд
    Trunk (master или main)
    Feature/
    Bugfix/
    Hotfix/
    v1.2.3
    v1.2.4

    View full-size slide

  88. Бонус (?): stacked diffs
    99

    View full-size slide

  89. Ссылки и
    исходный код
    101

    View full-size slide

  90. Ссылки/исходники
    102
    —https://opentelemetry.io/ecosystem/registry/?language=python&component=instrumentation
    тут можно найти каталог интеграций с телеметрией
    —https://github.com/stars/xfenix/lists/python-code-style подборка утилит для качества кода
    —https://github.com/cookiecutter/cookiecutter
    —https://github.com/rambler-digital-solutions/scaraplate
    —https://github.com/xfenix/pycon2022
    —https://www.youtube.com/watch?v=d2WcVUbKdbw почему structlog
    —https://www.youtube.com/watch?v=G3JKtB8tgvg полный доклад про codestyle
    —https://github.com/xfenix/pycon2022/blob/main/scripts/auto-semver.py

    View full-size slide

  91. Возможно я поборю
    свою лень и сделаю
    релиз библиотеки и
    кукикаттера…
    103

    View full-size slide

  92. Денис Аникин
    https://xfenix.ru
    https://github.com/xfenix/
    Спасибо!
    Добавляйтесь в друзья,
    ставьте звезды J

    View full-size slide