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

Из чего состоит микросервис в конце 2024 года

Из чего состоит микросервис в конце 2024 года

Доклад-гайд с полным собранием всего, что вам понадобится для создания очень хорошего качества микросервисов. Выступление в Школе 21

Denis Anikin

October 11, 2024
Tweet

More Decks by Denis Anikin

Other Decks in Programming

Transcript

  1. Что я такое? — Я техлид/комьюнити лид — fullstack, python,

    typescript, devops, микросервисы, kubernetes — Выступаю на конференциях — Отвечаю за внутреннее сообщество питонистов https://xfenix.ru Кто я?
  2. — Всем питонистам, связанным с бекендом, надо пилить сервисы, микросервисы,

    монолиты — Повторяемый сервис/микросервис имеет много всяких штук — «Хороший» сервис содержит ещё больше всего Проблема
  3. — Расскажу из чего делать повторяемый микросервис/микролит — Расскажу из

    чего собрать инфраструктуру (!) — Расскажу про типовые библиотеки и повторяемые решения — Предложу решения по архитектуре — По-сути, в одном месте соберу полноценный гайд по тому как сделать вообще всё с хорошим (или пристойным качеством) Решение
  4. Всё не так. Всё плохо. Надо лучше, мы знаем как

    лучше (но не скажем/вместе не соберем)
  5. — SOLID (его надо внедрять постоянно, сам не умеет; дорого,

    но классно) — Наш архитектурный гайд (в процессе выделения в опенсорс) — Принцип «разделяй и властвуй» там где нет четких гайдлайнов — «Микролит» Что используем
  6. — Имеет смысл обратиться к «Agile Software Development, Principles, Patterns,

    and Practices» — L проверяемо (частично) с помощью MyPy и 100% покрытия кода аннотациями — D — DIP, достижим при помощи DI фреймворка — https://speakerdeck.com/xfenix/piterpy-2024-vniedriaia-solid SOLID
  7. — REST — RPC — MQ (consumer, producer) — GraphQL

    — gRPC Определяем тип сервисов
  8. — REST ✅ — RPC ✅ — MQ (consumer, producer)

    ✅ — GraphQL — gRPC Определяем тип сервисов
  9. — У этого стиля много проблем… — О сущностях —

    Выбирайте префикс — /api/ -> /rest/ — Что не укладывается, можно префиксить /rpc/ — Версионирование — в accept заголовках (https://github.com/community-of-python/fast-version) REST
  10. — Всё ретраим (обязательно ограничивая количество + exponential) — Circuit

    breaker — Всё: коннекты к любой базе (postgres, redis, mongo и т.п.), mq, http api — Если что-то в коде не вышло, попробуйте ещё раз — Всё приходящее (и уходящее) стоит валидировать и форматировать строгими моделями (Pydantic, MsgSpec, Zod) Reliability & resilience
  11. Конфигурация ruff [tool.ruff] fix = true unsafe-fixes = true line-length

    = 120 [tool.ruff.format] docstring-code-format = true [tool.ruff.lint] select = ["ALL"] ignore = ["EM", "FBT", "TRY003", "D1", "D203", "D213", "G004", "FA", "COM812", "ISC001"] [tool.ruff.lint.isort] no-lines-before = ["standard-library", "local-folder"] known-third-party = [] known-local-folder = [] lines-after-imports = 2 [tool.ruff.lint.extend-per-file-ignores] "tests/*.py" = ["S101", "S311"] [tool.coverage.report] exclude_also = ["if typing.TYPE_CHECKING:"]
  12. — EM — Заведение отдельной переменной со строкой, для того

    чтобы добавить ее в Exception не является обязательной практикой — FBT — boolean аргументы нормально (странное правило) — TRY003 — писать message в exception нормально — S101 — для тестов — D1, D203, D213 — докстринги — FA — старый питон — COM812 и ISC001 — несовместимы с ruff format Что отключить в ruff
  13. — Final (https://github.com/vrslev/auto-typing-final) — Literal (PrimaryColors = Literal["red", "blue", "yellow"])

    — Protocol («интерфейсы») — TYPE_CHECKING Модуль typing — интересное
  14. TYPE_CHECKING # плохо from pathlib import Path def func(path: Path)

    -> str: return str(path) # хорошо from typing import TYPE_CHECKING if TYPE_CHECKING: from pathlib import Path def func(path: Path) -> str: return str(path)
  15. Исправляем код на лету "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.formatOnPaste": true,

    "editor.formatOnType": true, "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll": true, "source.organizeImports": true } }
  16. — Именование переменных не должно быть слишком общим: «data», «user»,

    «link». Например, их лучше назвать — «request_data», «user_login», «link_to_home_page». Всё в мире — data (для понимания катастрофичности нейминга) — Не стоит именовать функции с префикса get — Необходимо давать названия функциям и переменным не короче 8 символов длиной — Функция — глагол, а не существительное — Самодокументируемый код Правила кодстайла 1
  17. — Встроенные модули и модули с импортами более чем 2

    объектов импортируем целиком — typing.Final — Докстринги не пишем — Индексные операции опасны (some_list[11]) — Сужайте эксепшены Правила кодстайла 2
  18. Случай почти из жизни # Import the time module so

    we can use it later import time # Define a function to print a greeting message def greet(): # Print "Hello, world!" to the console print("Hello, world!") # Call the greet function so the greeting is printed greet() # Wait for 2 seconds (so we don't finish too fast) time.sleep(2) # Print a goodbye message to the console print("Goodbye, world!")
  19. Случай из жизни class UserRepository: """Repository for users.""" def get_users(self):

    """Get all users.""" ... def update_users(self): """Update users.""" ...
  20. — Всё, кроме 1 и 0 — это константы (Final),

    Enum с понятными именами — Имена — от 8 символов — Функции — глаголы — Нет бесполезных докстрингов — SRP для функций, функции короткие (20-30 строчек) Самодокументируемость
  21. — Общий шаблон сервиса — Ключевой тезис: выделяйте в него

    минимум повторяемого кода — Это частая ошибка — свалить туда копипасту. Не делайте так, т.к. это приведет к страданиям — Почему: потому что вы разнесёте копипасту по куче сервисов и изменять её будет крайне сложно == «нам лень тащить новое» Кукикаттер
  22. Microbootstrap # settings.py from microbootstrap import LitestarSettings class YourSettings(LitestarSettings): #

    Your settings are stored here settings = YourSettings() # application.py import litestar from microbootstrap.bootstrappers.litestar import LitestarBootstrapper from your_application.settings import settings # Use the Litestar application! application: litestar.Litestar = LitestarBootstrapper(settings).bootstrap()
  23. До microbootstrap """Entrypoint for dialogs API.""" import asyncio import pathlib

    import fastapi import sentry_sdk from chat_daemon_tools.gunicorn_runner import GenericGunicornRunner from chat_log_tools import middleware as log_middleware from chat_log_tools import setup_json_logger from fastapi import status from fastapi_rest_versioning import VersionedFastAPI, set_accept_header from health_checks.api.fastapi import get_health_check_router from kerberos_tools import client as kerberos_client from opentelemetry import trace from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.instrumentation.aiohttp_client import AioHttpClientInstrumentor from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor from opentelemetry.sdk.resources import SERVICE_NAME, Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from platform_jwt_tools import security from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from starlette.middleware.cors import CORSMiddleware from dialogs import ioc, settings from dialogs.api import bot, dialogs, error_handlers, internal, messages, mobile from dialogs.services import messages as messages_service # pylint: disable=no-member _KERBEROS_RENEWAL_TASK: asyncio.Task | None = None CONTAINER: ioc.IOCContainer = ioc.IOCContainer() async def startup(): """Global initialization.""" if settings.KAFKA_USE_KERBEROS: global _KERBEROS_RENEWAL_TASK # pylint: disable=global-statement _KERBEROS_RENEWAL_TASK = await kerberos_client.issue_ticket_and_run_renewal_task( settings.KAFKA_KERBEROS_PRINCIPAL, pathlib.Path(settings.KAFKA_KERBEROS_KEYTAB_PATH), settings.KAFKA_KERBEROS_RENEWAL_INTERVAL_SECONDS, ) await CONTAINER.init_resources() CONTAINER.wire(modules=[dialogs, messages, internal, mobile, messages_service, bot]) async def shutdown(): """Global shutdown.""" if _KERBEROS_RENEWAL_TASK: _KERBEROS_RENEWAL_TASK.cancel() await CONTAINER.shutdown_resources() if settings.SENTRY_DSN: sentry_sdk.init(dsn=settings.SENTRY_DSN, default_integrations=False) # pylint: disable=E0110 set_accept_header(settings.PLATFORM_ACCEPT_HEADER) APP_OBJ: fastapi.FastAPI = VersionedFastAPI( docs_url=settings.DOC_PREFIX, on_startup=[startup], on_shutdown=[shutdown], openapi_url=f"{settings.API_PREFIX}/openapi.json", openapi_tags=settings.TAGS_METADATA, exception_handlers={status.HTTP_500_INTERNAL_SERVER_ERROR: error_handlers.handle_500_exception}, ) APP_OBJ.include_router(dialogs.ROUTER_OBJ, prefix=settings.API_PREFIX, tags=["dialogs"]) APP_OBJ.include_router(messages.ROUTER_OBJ, prefix=settings.API_PREFIX, tags=["messages"]) APP_OBJ.include_router(get_health_check_router(), prefix=settings.API_PREFIX, tags=["health"]) APP_OBJ.include_router(internal.ROUTER_OBJ, prefix=settings.INTERNAL_API_PREFIX, tags=["internal"]) APP_OBJ.include_router(mobile.ROUTER_OBJ, prefix=settings.MOBILE_API_PREFIX, tags=["mobile"]) APP_OBJ.include_router(bot.ROUTER_OBJ, prefix=settings.BOT_API_PREFIX, tags=["bot"]) APP_OBJ.add_middleware(log_middleware.RequestIDASGIMiddleware) APP_OBJ.add_middleware(SentryAsgiMiddleware) if settings.OPENTELEMETRY_ENDPOINT: EXPORTER: OTLPSpanExporter = OTLPSpanExporter(endpoint=settings.OPENTELEMETRY_ENDPOINT, insecure=True) TRACE_PROVIDER: TracerProvider = TracerProvider( resource=Resource.create( { SERVICE_NAME: settings.OPENTELEMETRY_SERVICE_NAME, } ) ) TRACE_PROVIDER.add_span_processor(BatchSpanProcessor(EXPORTER)) trace.set_tracer_provider(TRACE_PROVIDER) FastAPIInstrumentor.instrument_app(APP_OBJ) AioHttpClientInstrumentor().instrument() AsyncPGInstrumentor().instrument() Psycopg2Instrumentor().instrument() if settings.DEBUG: APP_OBJ.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) if __name__ == "__main__": server: GenericGunicornRunner = GenericGunicornRunner( APP_OBJ, f"0.0.0.0:{settings.APP_PORT}", workers=settings.APP_WORKERS, loglevel=settings.APP_LOG_LEVEL ) setup_json_logger(need_sentry=bool(settings.SENTRY_DSN)) security.set_jwt_public_key(settings.JWT_PUBLIC_KEY) server.run()
  24. — Не повторяем копипасту — В шаблоне только необходимый минимальный

    код для старта сервисов — Все повторяемое — отделяем в библиотеки — https://github.com/community-of-python/microbootstrap Кукикаттер + microbootstrap
  25. — Настройки APP server — Настройки sentry/glitchtip — Настройки логгирования

    — Настройки observability — Настройки CORS — Настройки документации — Всё это мы кладем в settings.py Настройки каждого сервиса
  26. DI

  27. — Dependency injector — That depends — Dishka — Встроенные:

    litestar, fasstream — https://speakerdeck.com/xfenix/nuzhien-li-nam-v-python-di-i-kakoi DI фреймворки
  28. — stamina — niquests (httpx) — pytest, hypothesis, schemathesis, faker,

    pytest-random — aiorun — https://github.com/community-of-python/health-checks Важные библиотеки
  29. — Собираемся kaniko — Какой образ тестируем, тот же и

    раскатываем в продакшн (важно) — Продуманная релизная политика (семвер, чистка, помечаем нужные с помощью атрибутов в artifactory) — Создаем релизы в гитлабе — Для тестов 2 флоу: просто контейнер (simple), docker compose (complex) — Композ единый и CI и локально, предполагается, что в нем вы гоняете тесты и разрабатываете на нем (через тесты) Ещё важное
  30. — Некоторые дополнительные советы можно найти в докладе на piterpy

    2024 «Практические шаблоны и лучшие практики для разработки Python- библиотек» — https://squidex.jugru.team/api/assets/srm/cb66dbef-3226-4b96-affd-2b6aa882e5e 7/piterpy.pdf Сборка пакетов+
  31. — Kubernetes — Центральный репозиторий iac, из него вся инфра

    — Там helmfile, отдельно хелмчарты-перезаливы, отдельно хелмчарты- перезаливы с изменениями, отдельно ваши собственные рукописные — Вам понадобятся: glitchtip, prometheus (thanos), grafana, elk, apisix, keycloack Как делать инфру