Comments 48
factoryboy.readthedocs.io
может как писать в бд, так и сырые данные генерировать
разве mimesis это не просто генератор сырых данных? тут речь о том, что фабрика умеет работать с ORM моделями той же алхимии, а что использовать для данных (mimesis
или faker
) уже на усмотрение разработчика.
«Асинхронные библиотеки» по сути же просто очень вкусный «синтаксический сахар» над epoll/kqueue/select.
В Go всё-таки далеко не "обычный синхронный код", скорее наоборот там всё асинхронное.
Перед python ставите nginx и почти не имеете проблем :))
у воркера есть три фазы работы
- получение запроса
- обработка запроса (полезная работа собственно)
- отправка результата
Все бы ничего но окружающий мир не идеальный. И первая и последняя фаза на медленных клиентах может быть в несколько раз больше чем обработка запроса. Когда ставим nginx перед воркером, то первая и последняя фаза приближаются к идеалу. В итоге если к примеру получение и отправка запроса требовали 400 миллисекунд, а обработка запроса 200 миллисекунд, можно легко получить увеличение производительности на 100%.
Большое чтиво, все разжевано, но внимание уделили только самому приложению (и остались вопросы), а про остальные части жизненного цикла сказано мало. Я бы добавил еще несколько тем:
- Идемпотентность. Что будет, если один и тот же файл импорта загрузится несколько раз?
- Что будет с данными из 2 файлов импорта одновременно?
- Стоит ли обрабатывать импорты отдельными воркерами, которые можно горизонтально масштабировать?
- Тротлинг, кеш.
- Что будет делать CI при изменении версий зависимостей? Может лучше делать сборку через werf?
- При человеческой структуре и обвязке проекта (CI, тесты, semver, env-переменные) совсем не сказали про 12 factor app.
- В статье уделено время масштабированию, асинхронной обработке данных, но забыли рассказать о том, что постгрес плодит 1 процесс на 1 соединение. Было бы круто и про pg_bouncer упомянуть.
- Написали про health check и про несколько экземпляров для горизонтального масштабирования, а потом бац — деплой докер композом через анзибл. Как узнать, что деплой прошел успешно? Как откатиться?
- Очень мало написано про инфраструктуру. Сделали сервис, а что с ним дальше? Есть ли какой-то балансировщик перед ним? Что делать с zero-downtime deploy?
Сейчас более продвинутый стек — fastapi + pydantic. Преимущества перед aiohttp — наличие переиспользуемых middleware, совместимость с ASGI, возможность использовать uvloop без каких-либо изменений в коде.
Можно померяться бенчмарками, разумеется, но это все выглядит как вкусовщина.
Сериализация не через Marshmallow сделана? Окей что будет если я положу нативный UUID в PostgreSQL? :)
Просто я проверял несколько сериализаторов у всех кроме Marshmallow с этим были проблемы. А размещать uuid строкой в PostgreSQL при отличной нативной поддержке не хочется.
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?
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 что позволяет писать весьма компактный код.
В противном случае
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корость выполнение запросов (и как следствие — скорость работы сервиса) очень важна, в то время как диск почти ничего не стоит.
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 секунды.
Что на самом деле говорит нам, что можно использовать все что угодно.
Ну и да я просто напомню основной профиль нагрузки у СУБД это все же не только запись, но и чтение. А вот тут размер индексов и размер записи начинают влиять.
Под пакетом я имел в виду единицу дистрибуции, а под модулем разные вещи, в зависимости от контекста. Я исправлю терминологию, чтобы не вводить никого в заблужение. Спасибо за замечание.
На второй вопрос я, пожалуй, не смогу ответить за весь Яндекс, так как в компании очень много команд, и у всех немного по-разному.
В Едадиле — это прозвучит очень банально — мы стараемся как можно больше делиться опытом друг с другом и общаться с коллегами, смотреть PR друг друга (посмотреть PR другой команды — скорее правило, а не исключение).
Если кто-то видит, что он уже решал похожую проблему — скорее всего появится хорошо решающая эту проблему библиотека, покрытая тестами. Например, в свое время так появился aiomisc, aiomisc-dependency, snakepacker, wsrpc и другие.
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 или это не поможет читать данные кусками?
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), немного смутило, что нет разделения на слои («луковой» архитектуры).У нас микросервисы, поэтому на мой взгляд нужно руководствоваться правилом KISS. Если что — можно быстро написать новый и выкинуть старый.
Транспортный слой (контроллеры, у вас это handlers) перемешан со слоем бизнес логики и со слоем БД. Можно было бы как-нибудь разделить, часть вынести в сервисы. Кмк, это уменьшило бы связанность (если говорить о проекте, который будет со временем расти).
6. Так же не используется DI. Для python есть библиотека injector, но создается впечатление что в экосистеме python'a так не принято :) Кстати, в Яндексе где-нибудь используется injector или своя DI-библиотека для python'а?Не хотелось переусложнять это приложение. В Едадиле мы часто пользуемся модулем
aiomisc-dependency
. Он здорово выручает, когда в одной программе живет несколько сервисов (REST API, почтовый сервис, еще что-нибудь) и им требуется раздлеляемый пул ресурсов (например, один connection pool asyncpg), хотя этот модуль вполне можно использовать и для создания любых других объектов. Что касается Яндекса — не могу сказать, разработчиков очень много и у всех есть свои любимые инструменты.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
— и это пока последняя команда:
Потому что результаты:
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)
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 документация сервиса
# Останавливает контейнер 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. $ 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 адреса и его сетевой стек не изолируется.$ 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
Практическое руководство по разработке бэкенд-сервиса на Python