Как стать автором
Обновить

Практическое руководство по разработке бэкенд-сервиса на Python

Время на прочтение57 мин
Количество просмотров181K
Всего голосов 57: ↑55 и ↓2+66
Комментарии48

Комментарии 48

недавно подсказали либку для генерации данных в тестах
factoryboy.readthedocs.io
может как писать в бд, так и сырые данные генерировать
Мы когда-то mimesis использовали

разве mimesis это не просто генератор сырых данных? тут речь о том, что фабрика умеет работать с ORM моделями той же алхимии, а что использовать для данных (mimesis или faker) уже на усмотрение разработчика.

Судя по их примерам там только sqlalchemy.orm, так что для асинхронных библиотек (aiopg/asyncpg) придется самому писать, но благо там не ракеты запускать.

так это же тесты, можно и синхронно пописать в бд

Можно писать синхронно, а можно и свои фабрики сделать, что не очень сложно, зато тесты подготавливать потом гораздо проще.
Тут нет «правильного» решения, тут нужно от задачи исходить.
Там можно необходимые значения прописывать, зато не ломаешь голову над неважными или просто рыбой.
Монументальный труд, спасибо :) Кстати, судя по бенчмаркам, обычный синхронный код в языках без проблемы GIL (PHP / Go) работает на подобных задачах не хуже (производительность + латенси), чем завернутый в асинхронные библиотеки Python (или тот же NodeJS).
От задачи зависит, если нужно стримить что-то что не кончается, то для php ответ — скорее всего нет. Для Golang чаще всего — да.

«Асинхронные библиотеки» по сути же просто очень вкусный «синтаксический сахар» над epoll/kqueue/select.

В Go всё-таки далеко не "обычный синхронный код", скорее наоборот там всё асинхронное.

Именно код по умолчанию синхронный, а библиотеки (в том числе веб-серверы) чаще асинхронные, да.
Сам код — да, но при этом он весь выполняется файберами. В Go очень любопытная реализация неявной кооперативной многозадачности :)

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

Перед python ставите nginx и почти не имеете проблем :))

Мне вот интересно а как поможет nginx напротив prefork модели? Воркеры смогут обрабатывать не один запрос за раз?

у воркера есть три фазы работы


  • получение запроса
  • обработка запроса (полезная работа собственно)
  • отправка результата

Все бы ничего но окружающий мир не идеальный. И первая и последняя фаза на медленных клиентах может быть в несколько раз больше чем обработка запроса. Когда ставим nginx перед воркером, то первая и последняя фаза приближаются к идеалу. В итоге если к примеру получение и отправка запроса требовали 400 миллисекунд, а обработка запроса 200 миллисекунд, можно легко получить увеличение производительности на 100%.

Большое чтиво, все разжевано, но внимание уделили только самому приложению (и остались вопросы), а про остальные части жизненного цикла сказано мало. Я бы добавил еще несколько тем:


  1. Идемпотентность. Что будет, если один и тот же файл импорта загрузится несколько раз?
  2. Что будет с данными из 2 файлов импорта одновременно?
  3. Стоит ли обрабатывать импорты отдельными воркерами, которые можно горизонтально масштабировать?
  4. Тротлинг, кеш.
  5. Что будет делать CI при изменении версий зависимостей? Может лучше делать сборку через werf?
  6. При человеческой структуре и обвязке проекта (CI, тесты, semver, env-переменные) совсем не сказали про 12 factor app.
  7. В статье уделено время масштабированию, асинхронной обработке данных, но забыли рассказать о том, что постгрес плодит 1 процесс на 1 соединение. Было бы круто и про pg_bouncer упомянуть.
  8. Написали про health check и про несколько экземпляров для горизонтального масштабирования, а потом бац — деплой докер композом через анзибл. Как узнать, что деплой прошел успешно? Как откатиться?
  9. Очень мало написано про инфраструктуру. Сделали сервис, а что с ним дальше? Есть ли какой-то балансировщик перед ним? Что делать с zero-downtime deploy?

Сейчас более продвинутый стек — fastapi + pydantic. Преимущества перед aiohttp — наличие переиспользуемых middleware, совместимость с ASGI, возможность использовать uvloop без каких-либо изменений в коде.

Да, но как только понадобится HTTP клиент придется поставить aiohttp ;-)
Можно и httpx, но он над httpcore, который над h11 (http/1.1) и h2 (http/2), оба на питоне написаны, и перформансом не блещут. В aiohttp и для сервера и для клиента используется написанный на Cython парсер HTTP протокола, и только как фолбек, реализация на python.

Можно померяться бенчмарками, разумеется, но это все выглядит как вкусовщина.

Сериализация не через Marshmallow сделана? Окей что будет если я положу нативный UUID в PostgreSQL? :)


Просто я проверял несколько сериализаторов у всех кроме Marshmallow с этим были проблемы. А размещать uuid строкой в PostgreSQL при отличной нативной поддержке не хочется.

Marshmallow я использовал исключительно для валидации, сериализацию UUID asyncpg умеет из коробки. Кстати, ему можно указать как сериализовать/десериализовать произвольные типы.

Пример
import asyncio
import logging
import uuid

import asyncpg


async def main():
    logging.basicConfig(level=logging.INFO)
    
    conn = await asyncpg.connect('postgresql://user:hackme@localhost/example')
    await conn.execute(
        'CREATE TABLE IF NOT EXISTS examples '
        '(example_id serial primary key, uuid UUID)'
    )

    value = uuid.uuid4()
    result = await conn.fetchrow(
        'INSERT INTO examples (uuid) VALUES ($1) RETURNING examples.*', 
        value
    )
    assert isinstance(result['uuid'], uuid.UUID)
    assert value == result['uuid']


asyncio.run(main())

Я про сериализацию. В каком месте драйвер asyncpg делает сериализацию uuid в json?

Ага, думал вы другую задачу решаете. Сериализовать объект с произвольными типами в json можно силами asyncpg методом Connection.set_type_codec. Ему можно указать произвольный сериализатор, например json.dumps с параметром default из stdlib.

Пример
import asyncio
import json
import logging
import uuid
from functools import partial

import asyncpg


def convert(value):
    if isinstance(value, uuid.UUID):
        return str(value)
    raise NotImplementedError


async def main():
    logging.basicConfig(level=logging.INFO)

    conn = await asyncpg.connect('postgresql://user:hackme@localhost/example')
    await conn.set_type_codec(
        'json',
        encoder=partial(json.dumps, default=convert),
        decoder=json.loads,
        schema='pg_catalog'
    )
    await conn.execute(
        'CREATE TABLE IF NOT EXISTS examples '
        '(example_id serial primary key, data JSON)'
    )

    uuid_value = uuid.uuid4()
    result = await conn.fetchrow(
        'INSERT INTO examples (data) VALUES ($1) RETURNING examples.*',
        {'uuid': uuid_value}
    )

    # Не сработает, т.к. uuid десериализуется в виде строки.
    assert isinstance(result['data']['uuid'], uuid.UUID)


Вот десериализовать json-поле обратно в желаемые объекты без дополнительного инструмента уже просто так не получится — тут, безусловно, Marshmallow будет очень кстати.

Но тогда возникает вопрос: вы хотите хранить UUID как нативный тип PostgreSQL, или использовать тип данных JSON, который будет хранить UUID в виде строки?
Но тогда возникает вопрос: вы хотите хранить UUID как нативный тип PostgreSQL, или использовать тип данных JSON, который будет хранить UUID в виде строки?

У вас backend. Он внезапно в наружу отдает json и отдает его из базы. В PostgreSQL уже давно можно использовать uuid в качестве ключей. Что кстати весьма удобно при общении с внешним миром.


json dumps как раз и обламывается в этом случае :) Ну или включаем наши руки не для скуки и дописываем. Из коробки с этой задачей нормально справляется Marshmallow плюс он весьма хорошо интегрируется с SQLAlchemy что позволяет писать весьма компактный код.

Для сериализации данных для клиента Marshmallow полезен, если нужно из одной структуры в базе сделать совсем другую структуру и когда где-то необходимо описать этот маппинг.

В противном случае json.dumps вполне справится, параметром default можно сериализовать любой, в том числе произвольный объект и описать эти правила сериализации в одном месте.

В PostgreSQL уже давно можно использовать uuid в качестве ключей

Если в качестве ключа использовать случайный uuid то индекс будет перестраиваться на каждый запрос на вставку. Это тяжело.
Для сериализации данных для клиента Marshmallow полезен, если нужно из одной структуры в базе сделать совсем другую структуру и когда где-то необходимо описать этот маппинг.

Пишутся схемы ожидаемых полей и все. Декларативно и хорошо, вполне прозрачно.


В противном случае json.dumps вполне справится, параметром default можно сериализовать любой, в том числе произвольный объект и описать эти правила сериализации в одном месте.

По мне лучше декларативно схему описать. Это будет еще гарантировать, что в наружу лишнее не уйдет. Лишние данные просто отсекаются. В случае json.dumps это не произойдет.


Если в качестве ключа использовать случайный uuid то индекс будет перестраиваться на каждый запрос на вставку. Это тяжело.

Надо просто использовать функцию uuid_generate_v1mc() она имеет корелляцию к timestamp. Что позволяет это уменьшить.

Пишутся схемы ожидаемых полей и все. Декларативно и хорошо, вполне прозрачно.

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

Гарантировать, что в наружу лишнее не уйдет конечно можно и нужно схемами, но например в тестах. Это можно проверить 1 раз на CI.

Надо просто использовать функцию uuid_generate_v1mc() она имеет корелляцию к timestamp. Что позволяет это уменьшить.

Не понимаю только зачем. Не проще ли использовать первичным ключом int (оно очевидно будет быстрее и проще для постгрес — 4 байта против 16) и иметь дополнительное поле uuid4 с уникальным индексом, которое отдавать наружу клиентам, если вы хотите избежать перебора сущностей по id?
Если задача — отдать клиенту данные из базы без каких-либо изменений за исключением ряда столбцов — я бы не стал запрашивать их у базы данных — зачем попусту гонять данные по сети?

Как правило у вас задача стоит как сделайте CRUD по БД. И там вот такие вещи сильно ускоряют разработку и более прозрачны для других.


Не проще ли использовать первичным ключом int (оно очевидно будет быстрее и проще для постгрес — 4 байта против 16)

Для начала стоит использовать bigint. Это конечно не очевидно, но с переполнением int я уже сталкивался.


и иметь дополнительное поле uuid4 с уникальным индексом, которое отдавать наружу клиентам

И в этом случае вместо одного поля в 16 байт и одного индекса у меня будет 2 поля в сумме дающие 20 байт, а если все же брать bigint 24 байта и два индекса. И собственно зачем?

Для начала стоит использовать bigint. Это конечно не очевидно, но с переполнением int я уже сталкивался.
Зависит от задачи. В постоянно растущих таблицах в этом конечно есть смысл.
И в этом случае вместо одного поля в 16 байт и одного индекса у меня будет 2 поля в сумме дающие 20 байт, а если все же брать bigint 24 байта и два индекса. И собственно зачем?
Cкорость выполнение запросов (и как следствие — скорость работы сервиса) очень важна, в то время как диск почти ничего не стоит.

Предлагаемый вами вариант на вставке медленнее на ~20%
Для теста создал две таблицы, в которые вставлял 3 миллиона записей частями по 10 тысяч записей за раз:
CREATE TABLE citizens_int_uuid4 (
    citizen_id bigint primary key, 
    name text,
    external_id uuid DEFAULT uuid_generate_v4() UNIQUE
);

CREATE TABLE citizens_uuid1 (
    citizen_id uuid primary key DEFAULT uuid_generate_v1mc(),
    name text
);

Вариант с uuid1, который вы предлагаете, выполнился за 2 мин 27 сек. Вариант с uuid4 — за 2 мин.

Также попробовал что вариант с uuid4 как с bigint так и с int. Интересно, что разница получилась всего на 2-4 секунды.

Уверен, что другие операции (например, JOIN) тоже будут работать медленее, вопрос только насколько.
Зависит от задачи. В постоянно растущих таблицах в этом конечно есть смысл.

Сейчас это стоит делать всегда. Использовать сейчас 32 битный инт странное занятие.


Вариант с uuid1, который вы предлагаете, выполнился за 2 мин 27 сек. Вариант с uuid4 — за 2 мин.

А теперь сходите посмотрите что там с местом на диске и размер индексов. Смысл использования uuid1 был в том чтобы uuid были более равномерные.


Также попробовал что вариант с uuid4 как с bigint так и с int. Интересно, что разница получилась всего на 2-4 секунды.

Что на самом деле говорит нам, что можно использовать все что угодно.


Ну и да я просто напомню основной профиль нагрузки у СУБД это все же не только запись, но и чтение. А вот тут размер индексов и размер записи начинают влиять.

Целое работоспособное приложение и не реализовано ни одного алгоритма? Яндекс, ты ли это?
Это задание являлось вторым этапом отбора. Первым этапом был контест с алгоритмическими задачами.
alvassin По тексту не понятно, вы различаете или нет понятие модуля и пакета в python. И немного смутил момент с папочкой utils. Как вы следите чтобы все не писали одни и те-же utils в разных сервисах? Часто в utils пишут что-то, что не ясно куда положить. Как-то боретесь с этим в Яндекс?
rmsbeetle, согласно документации «модуль» — файл с инструкциями Python, а «пакет» — способ организации пространства имен модулей (синтаксически — с помощью точек). Однако в ней же слово «модуль» иногда трактуется более широко: например, технически asyncio — пакет (неймспейс с несколькими модулями), но иногда называется модулем.
Под пакетом я имел в виду единицу дистрибуции, а под модулем разные вещи, в зависимости от контекста. Я исправлю терминологию, чтобы не вводить никого в заблужение. Спасибо за замечание.

На второй вопрос я, пожалуй, не смогу ответить за весь Яндекс, так как в компании очень много команд, и у всех немного по-разному.
В Едадиле — это прозвучит очень банально — мы стараемся как можно больше делиться опытом друг с другом и общаться с коллегами, смотреть PR друг друга (посмотреть PR другой команды — скорее правило, а не исключение).
Если кто-то видит, что он уже решал похожую проблему — скорее всего появится хорошо решающая эту проблему библиотека, покрытая тестами. Например, в свое время так появился aiomisc, aiomisc-dependency, snakepacker, wsrpc и другие.
ОГРОМНОЕ СПАСИБО! Я В ЖИЗНИ СТАТЬИ ЛУЧШЕ НЕ ВИДЕЛ!
alvassin Спасибо за отличную статью!
Скажите, почему бы не использовать poetry? И setup.py не нужен, и зависимости в одном файле. Отказ от него здесь — просто вкусовщина, или есть какие-то противопоказания?
Существует два стандарта: зрелый setup.py и новый, описанный в PEP 518 и PEP 517.

PEP 518 описывает возможность указывать требования (зависимости) к системе сборки с помощью файла pyproject.toml и находится в статусе final. Это означает, что он больше не будет изменяться.

PEP 517 описывает интерфейсы для независимых от distutils и/или setuptools систем сборки (которые использует poetry и другие альтернативные инструменты), и находится в статусе provisional. Авторы PEP в данный момент собирают фидбек сообщества и PEP еще может измениться или его вообще могут не принять в существующем виде. Поэтому на мой взгляд рекомендовать его пока рано — не хотелось бы потом переписывать все проекты.

Вообще PEP 517 и poetry выглядят здорово, лично мне они нравятся. К слову, aiohttp уже вместе с setup.py использует pyproject.toml.
Большое спасибо за статью!
Принимал участие в конкурсе (не прошел, не успел задеплоиться).

Подскажите, пожалуйста, по следующим вопросам:
1. Почему не прикрутили NGINX? Не будет ли standalone сервер aiohttp медленее, чем с NGINX?
2. Почему citizens не разбиты на отдельные таблицы для каждого импорта? Намек на это также был в вашем FAQ видео. Еще это позволило бы лочиться только по citizens.id, а не по всей выгрузке (import_id).
3. Не рассматривался ли вариант получения (от клиента) выгрузки по частям? Возможно ли это сделать средствами aiohttp/python? Например, через request.content: docs.aiohttp.org/en/stable/streams.html или это не поможет читать данные кусками?
4. Не очень понял используется ли connection pool. В коде, при старте, приложение коннектится к postgres, но не очень понятен размер пула (или коннекшн один?).
5. Я c экосистемой python не очень хорошо знаком (в последние пару лет сижу на java), немного смутило, что нет разделения на слои («луковой» архитектуры).
Транспортный слой (контроллеры, у вас это handlers) перемешан со слоем бизнес логики и со слоем БД. Можно было бы как-нибудь разделить, часть вынести в сервисы. Кмк, это уменьшило бы связанность (если говорить о проекте, который будет со временем расти).
6. Так же не используется DI. Для python есть библиотека injector, но создается впечатление что в экосистеме python'a так не принято :)
Кстати, в Яндексе где-нибудь используется injector или своя DI-библиотека для python'а?

В целом, статья очень поучительная, многие вещи не знал :)
Спасибо!
1. Почему не прикрутили NGINX? Не будет ли standalone сервер aiohttp медленее, чем с NGINX?
Андрей Светлов рекомендует использовать nginx c aiohttp по следующим причинам:

  • Nginx может предовратить множество атак на основе некорректных запросов HTTP, отклонять запросы, методы которых не поддерживаются приложением, запросы со слишком большим body.
    Это может снять какую-то нагрузку с приложения в production, но выходит за рамки данного задания.
  • Nginx здорово раздает статику.
    В нашем приложении нет статических файлов.
  • Nginx позволяет распределить нагрузку по нескольким экземплярам приложения слушающих разные сокеты (upstream module), чтобы загрузить все ядра одного сервера.
    На мой взгляд проще и эффективнее обслуживать запросы в одном приложении на 1 сокете несколькими форками, чем устанавливать и настраивать для этого отдельное приложение. Я рассказывал как это сделать в разделе «Приложение».
  • Добавлю от себя: nginx может играть роль балансера, распределяя нагрузку по разным физическим серверам (виртуалкам).
    По условиям задания, у нас есть только один сервер, где мы можем развернуть приложение, поэтому это тоже выходит за рамки нашей задачи.

Как вы видите, именно для этого приложения nginx не очень полезен. Но в других случаях (описанных выше), он может очень здорово увеличить эффективность вашего сервиса.

2. Почему citizens не разбиты на отдельные таблицы для каждого импорта? Намек на это также был в вашем FAQ видео. Еще это позволило бы лочиться только по citizens.id, а не по всей выгрузке (import_id).
Для этой задачи большой разницы нет, можно создавать и отдельные таблицы. Минусы этого подхода: если потребуется реализовать обработчик, возвращающий всех жителей всех выгрузок придется делать очень много UNION-ов, а также при создании таблиц с динамическими названиями их придется экранировать вручную, т.к. PostgreSQL не позволяет использовать аргументы в DDL.

3. Не рассматривался ли вариант получения (от клиента) выгрузки по частям? Возможно ли это сделать средствами aiohttp/python? Например, через request.content: docs.aiohttp.org/en/stable/streams.html или это не поможет читать данные кусками?
Получать и обрабатывать json запрос по частям cилами aiohttp можно
from http import HTTPStatus

import ijson
from aiohttp.web_response import Response

from .base import BaseView


class ImportsView(BaseView):
    URL_PATH = '/imports'

    async def post(self):
        async for citizen in ijson.items_async(
            self.request.content,
            'citizens.item',
            buf_size=4096
        ):
            # Обработка жителя
            print(citizen)
        return Response(status=HTTPStatus.CREATED)

Сейчас валидация входных данных в обработчике POST /imports происходит двумя Marshmallow схемами: CitizenSchema (проверяет конкретного жителя) ImportSchema (проверяет связи между жителями и уникальность citizen_id) и только затем пишется в базу.

В случае обработки жителей по одному, вы можете проверить только текущего жителя. Чтобы проверить уникальность его citizen_id и его родственные связи в рамках выгрузки придется так или иначе накапливать информацию о уже обработанных жителях выгрузки и откатывать транзакцию, если вы встретите некорректные данные. Можно хранить для каждого жителя только поля citizen_id и relatives, это сэкономит память, но код будет сложнее, поэтому в этом приложении от этого варианта я отказался.

4. Не очень понял используется ли connection pool. В коде, при старте, приложение коннектится к postgres, но не очень понятен размер пула (или коннекшн один?).
Используется пул с 10 подключениями (размер по умолчанию). При запуске aiohttp приложения cleanup_ctx вызывает генератор setup_pg, который в свою очередь создает объект asyncpgsa.PG (с пулом соединений) и сохраняет его в app['pg']. Базовый обработчик, для удобства предоставляет объект app['pg'] в виде свойства pg. Кол-во подключений, кстати, можно вынести аргументом ConfigArgParse — настраивать приложение будет очень удобно.

5. Я c экосистемой python не очень хорошо знаком (в последние пару лет сижу на java), немного смутило, что нет разделения на слои («луковой» архитектуры).
Транспортный слой (контроллеры, у вас это handlers) перемешан со слоем бизнес логики и со слоем БД. Можно было бы как-нибудь разделить, часть вынести в сервисы. Кмк, это уменьшило бы связанность (если говорить о проекте, который будет со временем расти).
У нас микросервисы, поэтому на мой взгляд нужно руководствоваться правилом KISS. Если что — можно быстро написать новый и выкинуть старый.

6. Так же не используется DI. Для python есть библиотека injector, но создается впечатление что в экосистеме python'a так не принято :) Кстати, в Яндексе где-нибудь используется injector или своя DI-библиотека для python'а?
Не хотелось переусложнять это приложение. В Едадиле мы часто пользуемся модулем aiomisc-dependency. Он здорово выручает, когда в одной программе живет несколько сервисов (REST API, почтовый сервис, еще что-нибудь) и им требуется раздлеляемый пул ресурсов (например, один connection pool asyncpg), хотя этот модуль вполне можно использовать и для создания любых других объектов. Что касается Яндекса — не могу сказать, разработчиков очень много и у всех есть свои любимые инструменты.
Спасибо за подробные ответы!
Для валидации входящих запросов и генерации сваггер доков можно еще можно использовать вот эту библиотеку github.com/hh-h/aiohttp-swagger3
С удовольствием прослушал курс бэк-энд разработчика, попал на эту публикацию и решил собрать сервис.
1. make postgres
2. make docker
3. docker run -it \
-e ANALYZER_PG_URL=postgresql://user:hackme@localhost/analyzer \
alvassin/backendschool2019 analyzer-db upgrade head
— и это пока последняя команда:
Потому что результаты:
Ошибка присоединения к Postgres
Traceback (most recent call last):
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 2285, in _wrap_pool_connect
    return fn()
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 303, in unique_connection
    return _ConnectionFairy._checkout(self)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 773, in _checkout
    fairy = _ConnectionRecord.checkout(pool)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 492, in checkout
    rec = pool._do_get()
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/impl.py", line 238, in _do_get
    return self._create_connection()
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 308, in _create_connection
    return _ConnectionRecord(self)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 437, in __init__
    self.__connect(first_connect_check=True)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 657, in __connect
    pool.logger.debug("Error on connect(): %s", e)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/util/langhelpers.py", line 68, in __exit__
    compat.raise_(
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/util/compat.py", line 178, in raise_
    raise exception
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 652, in __connect
    connection = pool._invoke_creator(self)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/engine/strategies.py", line 114, in connect
    return dialect.connect(*cargs, **cparams)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/engine/default.py", line 488, in connect
    return self.dbapi.connect(*cargs, **cparams)
  File "/usr/share/python3/app/lib/python3.8/site-packages/psycopg2/__init__.py", line 126, in connect
    conn = _connect(dsn, connection_factory=connection_factory, **kwasync)
psycopg2.OperationalError: could not connect to server: Connection refused
        Is the server running on host "localhost" (127.0.0.1) and accepting
        TCP/IP connections on port 5432?
could not connect to server: Cannot assign requested address
        Is the server running on host "localhost" (::1) and accepting
        TCP/IP connections on port 5432?


The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/bin/analyzer-db", line 11, in <module>
    load_entry_point('analyzer==0.0.1', 'console_scripts', 'analyzer-db')()
  File "/usr/share/python3/app/lib/python3.8/site-packages/analyzer/db/__main__.py", line 32, in main
    exit(alembic.run_cmd(config, options))
  File "/usr/share/python3/app/lib/python3.8/site-packages/alembic/config.py", line 546, in run_cmd
    fn(
  File "/usr/share/python3/app/lib/python3.8/site-packages/alembic/command.py", line 298, in upgrade
    script.run_env()
  File "/usr/share/python3/app/lib/python3.8/site-packages/alembic/script/base.py", line 489, in run_env
    util.load_python_file(self.dir, "env.py")
  File "/usr/share/python3/app/lib/python3.8/site-packages/alembic/util/pyfiles.py", line 98, in load_python_file
    module = load_module_py(module_id, path)
  File "/usr/share/python3/app/lib/python3.8/site-packages/alembic/util/compat.py", line 173, in load_module_py
    spec.loader.exec_module(module)
  File "<frozen importlib._bootstrap_external>", line 783, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/usr/share/python3/app/lib/python3.8/site-packages/analyzer/db/alembic/env.py", line 79, in <module>
    run_migrations_online()
  File "/usr/share/python3/app/lib/python3.8/site-packages/analyzer/db/alembic/env.py", line 67, in run_migrations_online
    with connectable.connect() as connection:
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 2218, in connect
    return self._connection_cls(self, **kwargs)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 103, in __init__
    else engine.raw_connection()
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 2317, in raw_connection
    return self._wrap_pool_connect(
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 2288, in _wrap_pool_connect
    Connection._handle_dbapi_exception_noconnection(
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 1554, in _handle_dbapi_exception_noconnection
    util.raise_(
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/util/compat.py", line 178, in raise_
    raise exception
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/engine/base.py", line 2285, in _wrap_pool_connect
    return fn()
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 303, in unique_connection
    return _ConnectionFairy._checkout(self)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 773, in _checkout
    fairy = _ConnectionRecord.checkout(pool)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 492, in checkout
    rec = pool._do_get()
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/impl.py", line 238, in _do_get
    return self._create_connection()
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 308, in _create_connection
    return _ConnectionRecord(self)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 437, in __init__
    self.__connect(first_connect_check=True)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 657, in __connect
    pool.logger.debug("Error on connect(): %s", e)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/util/langhelpers.py", line 68, in __exit__
    compat.raise_(
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/util/compat.py", line 178, in raise_
    raise exception
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/pool/base.py", line 652, in __connect
    connection = pool._invoke_creator(self)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/engine/strategies.py", line 114, in connect
    return dialect.connect(*cargs, **cparams)
  File "/usr/share/python3/app/lib/python3.8/site-packages/sqlalchemy/engine/default.py", line 488, in connect
    return self.dbapi.connect(*cargs, **cparams)
  File "/usr/share/python3/app/lib/python3.8/site-packages/psycopg2/__init__.py", line 126, in connect
    conn = _connect(dsn, connection_factory=connection_factory, **kwasync)
sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) could not connect to server: Connection refused
        Is the server running on host "localhost" (127.0.0.1) and accepting
        TCP/IP connections on port 5432?
could not connect to server: Cannot assign requested address
        Is the server running on host "localhost" (::1) and accepting
        TCP/IP connections on port 5432?

(Background on this error at: http://sqlalche.me/e/e3q8)



Посмотрел sudo netstat -tupln
 Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:46624         0.0.0.0:*               LISTEN      6645/kited      
tcp        0      0 127.0.0.1:46601         0.0.0.0:*               LISTEN      6929/code       
tcp        0      0 192.168.122.1:53        0.0.0.0:*               LISTEN      5020/dnsmasq    
tcp        0      0 10.75.69.1:53           0.0.0.0:*               LISTEN      3251/dnsmasq    
tcp        0      0 127.0.1.1:53            0.0.0.0:*               LISTEN      2476/dnsmasq    
tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN      16677/cupsd     
tcp6       0      0 :::8081                 :::*                    LISTEN      4938/docker-proxy
tcp6       0      0 fd42:c2ba:2d3b:518e::53 :::*                    LISTEN      3251/dnsmasq    
tcp6       0      0 fe80::b8bb:caff:fe06:53 :::*                    LISTEN      3251/dnsmasq    
tcp6       0      0 ::1:631                 :::*                    LISTEN      16677/cupsd     
tcp6       0      0 :::5432                 :::*                    LISTEN      25651/docker-proxy
,,,


Не видно tcp4 — но ведь все сделано, как Васин рассказал ;-)
Оба контейра живы, docker run alvassin/backendschool2019 analyzer-db --help — работает
Postgres-контейнер тоже
Хоть это вне архитектуры приложения — но как-то потерял несколько часов — не могу понять, почему не устанавливается соединение.
Спасибо заранее

Пообщался в переписке с автором alvassin — если производить запуск 3 команды (на одном хосте) и дополнением --network host — все заработает:


3. docker run -it  --network host \
-e ANALYZER_PG_URL=postgresql://user:hackme@localhost/analyzer \
alvassin/backendschool2019 analyzer-db upgrade head

Аналогично для запуска приложения:


4. docker run -it -p 8081:8081 --network host -e ANALYZER_PG_URL=postgresql://user:hackme@localhost/analyzer alvassin/backendschool2019

5. http://0.0.0.0:8081 в браузере

и видна swagger документация сервиса

Что делает команда make postgres
# Останавливает контейнер analyzer-postgres, если он запущен
$ docker stop analyzer-postgres || true

# Запускает контейнер с именем analyzer-postgres
$ docker run --rm --detach --name=analyzer-postgres \
    --env POSTGRES_USER=user \
    --env POSTGRES_PASSWORD=hackme \
    --env POSTGRES_DB=analyzer \
    --publish 5432:5432 postgres

У вас не получалось подключиться, потому что если при запуске контейнера сеть не указана явно, он создается в дефолтной bridge-сети. В такой сети контейнеры могут обращаться друг к другу (и к хосту) только по ip адресу. Кстати, Docker for Mac предоставляет специальное DNS имя host.docker.internal для обращений к хост-машине из контейнеров, что довольно удобно для локальной разработки.

Аргумент --publish 5432:5432 указывает Docker добавить в iptables правило, по которому запросы к хост-машине на порт 5432 отправляются в контейнер с Postgres на порт 5432.

Поэксперементируем с подключением к контейнеру с Postgres
Подключение с хост-машины на published порт:
$ telnet localhost 5432
Trying ::1...
Connected to localhost.
Escape character is '^]'.

Подключение из контейнера с приложением (по IP хост-машины на published-порт):
# Получаем IP хост-машины
$ docker run -it alvassin/backendschool2019 /bin/bash -c \
    'apt update && apt install -y iproute2 && ip route show | grep default'
default via 172.17.0.1 dev eth0

# Применяем миграции
$ docker run -it \
    -e ANALYZER_PG_URL=postgresql://user:hackme@172.17.0.1/analyzer \
    alvassin/backendschool2019 \
    analyzer-db upgrade head

Подключение из контейнера с приложением (по host.docker.internal, published порт нужен, потому что подключаемся через хост-машину):
$ export HOST=host.docker.internal
$ docker run -it \
    -e ANALYZER_PG_URL=postgresql://user:hackme@${HOST}/analyzer \
    alvassin/backendschool2019 \
    analyzer-db upgrade head

Подключение из контейнера с приложением (по IP контейнера Postgres, published порт в этом случае не нужен):
# Получаем IP
$ docker inspect \
    -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
    analyzer-postgres
172.17.0.2

$ docker run -it \
    -e ANALYZER_PG_URL=postgresql://user:hackme@172.17.0.2/analyzer \
    alvassin/backendschool2019 \
    analyzer-db upgrade head


Если контейнер с приложением запускается в сети хост-машины (параметр --network host), он не получает отдельного ip адреса и его сетевой стек не изолируется.

Поэтому такой контейнер имеет доступ к портам хост-машины через localhost.
$ docker run -it \
    --network host \
    -e ANALYZER_PG_URL=postgresql://user:hackme@localhost/analyzer \
    alvassin/backendschool2019 \
    analyzer-db upgrade head


Третий вариант подружить контейнеры — создать bridge сеть вручную и запустить в ней оба контейнера — и Postgres и контейнер с приложением. В отличие от дефолтной bridge-сети, в созданных пользователем bridge-сетях можно обращаться по имени контейнера.

Это бы выглядело следующим образом
# Создаем сеть
$ docker network create analyzer-net

# Запускаем контейнер с PostgreSQL в сети analyzer-net
$ docker run --rm -d \
    --name=analyzer-postgres \
    --network analyzer-net \
    -e POSTGRES_USER=user \
    -e POSTGRES_PASSWORD=hackme \
    -e POSTGRES_DB=analyzer \
    -p 5432:5432 \
    postgres

# Запускаем контейнер с приложением в сети analyzer-net,
# обращаемся по имени к контейнеру с Postgres
$ docker run -it --network analyzer-net \
    -e ANALYZER_PG_URL=postgresql://user:hackme@analyzer-postgres/analyzer \
    alvassin/backendschool2019 \
    analyzer-db upgrade head

Зарегистрируйтесь на Хабре, чтобы оставить комментарий