Строка подключения MongoDB - это одна строка из пяти частей, и почти все проблемы с ней сидят в четвёртой или пятой:
mongodb://appuser:s3cr3t@db.example.net:27017/shop?authSource=admin&retryWrites=trueСхема, учётные данные, хост и порт, база данных по умолчанию, параметры. Если соединение отклоняется, значит, хост или порт неверны, либо сервер не слушает по адресу, до которого вы можете достучаться. Если аутентификация падает с учётными данными, в которых вы уверены, ответ почти всегда - authSource или пароль с символом, который нужно было закодировать через percent-кодирование. На эти две причины приходится большинство вопросов в поддержку о подключении к MongoDB, и обе исправляются в URI, а не на сервере.
Здесь мы разберём URI на части, рассмотрим mongodb+srv и случаи, когда он не подходит, параметры, которые стоит задавать, подключение из mongosh, Compass и драйверов Node и Python, а также что на самом деле сообщает каждая из распространённых ошибок.
Устройство URI MongoDB#
| Часть | Пример | Примечания |
|---|---|---|
| Схема | mongodb:// | Или mongodb+srv:// для списков seed в DNS |
| Учётные данные | appuser:s3cr3t@ | Необязательны; кодируйте оба через percent-кодирование |
| Хосты | db.example.net:27017 | Через запятую для replica set |
| База данных по умолчанию | /shop | База, которую драйвер использует, если вы не назвали другую |
| Параметры | ?authSource=admin | Разделяются &, ключи нечувствительны к регистру |
Два из этих пунктов нужно рассмотреть внимательнее, прежде чем идти дальше.
База данных по умолчанию - это та, которую драйвер возвращает из client.db() без аргумента, и, что важнее, она же служит значением по умолчанию для authSource. Её можно опустить: mongodb://user:pass@host:27017/?authSource=admin - полноценный URI, а косая черта перед ? обязательна, если вы хотите указать параметры без имени базы.
Список хостов - место, где объявляются replica set. mongodb://a:27017,b:27017,c:27017/?replicaSet=rs0 сообщает драйверу о трёх участниках; остальную топологию драйвер выясняет сам, определяет, кто сейчас primary, и перенаправляет записи, когда это меняется. На одиночном standalone-сервере хост один и параметра replicaSet нет, - это обычный случай для сервера баз данных, который вы арендуете отдельно. На RE:NODE отдельными линейками идут PostgreSQL и MongoDB, доступные по хосту и порту, указанным в тарифе, с паролем суперпользователя, сгенерированным для вашего сервера, а не опубликованным по умолчанию, так что ваш первый URI - это этот хост, этот порт и эти учётные данные.
Пароли, percent-кодирование и ошибка, которой никто не ждёт#
URI есть URI, поэтому любой символ в имени пользователя или пароле, имеющий в нём особое значение, нужно закодировать через percent-кодирование. В сгенерированных паролях таких символов полно.
| Символ | Кодированный |
|---|---|
: | %3A |
/ | %2F |
? | %3F |
# | %23 |
[ и ] | %5B и %5D |
@ | %40 |
% | %25 |
Пароль p@ss/w0rd превращается в p%40ss%2Fw0rd. Если пропустить кодирование, драйвер либо сразу выбросит Password contains unescaped characters, либо, что хуже, молча прочитает всё после лишнего @ как имя хоста и сообщит, что не может найти сервер с именем ss/w0rd.
Не кодируйте вручную. В каждом языке есть нужная функция:
const uri = `mongodb://${encodeURIComponent(user)}:${encodeURIComponent(pass)}@db.example.net:27017/shop?authSource=admin`;from urllib.parse import quote_plusuri = f"mongodb://{quote_plus(user)}:{quote_plus(pw)}@db.example.net:27017/shop?authSource=admin"Ещё лучше вообще не держать учётные данные в строке и передавать их драйверу отдельными аргументами, что поддерживает большинство драйверов. Тогда нечего кодировать и нечему утечь в строку лога, которая печатает URI.
mongodb:// или mongodb+srv://#
mongodb+srv:// - это не другой протокол. Это сокращение, которое говорит драйверу искать список хостов в DNS, а не читать его из строки. Получив mongodb+srv://db.example.net/, драйвер запрашивает SRV-запись _mongodb._tcp.db.example.net за именами хостов и портами, а затем TXT-запись на db.example.net за небольшим набором параметров по умолчанию, таких как replicaSet и authSource.
Три следствия, на которых спотыкаются:
- В URI с
+srvнельзя указать порт. Порты берутся из SRV-записей. - Имя хоста должно быть ровно одно, и SRV-записи обязаны существовать. Если направить
+srvна обычный хост, вы получитеquerySrv ENOTFOUND _mongodb._tcp.db.example.net. +srvпо умолчанию включает TLS. Сервер, не настроенный на TLS, откажет в handshake, и в ошибке TLS редко упоминается.
Итак: используйте mongodb+srv://, когда провайдер выдал вам строку с +srv, что обычно означает управляемый кластер, состав которого может меняться. Используйте обычный mongodb:// с хостом и портом для сервера, который вы арендуете отдельно. Если вы хотите поставить перед этим хостом удобное имя, с этим справится запись A, указывающая на адрес, без всей механики SRV; какую запись выбрать, разобрано в статье DNS-записи простыми словами.
authSource, пользователи и роли#
Пользователи MongoDB не глобальны. Каждый пользователь создаётся в конкретной базе данных и принадлежит ей, а имя этой базы и есть то, что означает authSource. Суперпользователь почти всегда живёт в admin, поэтому URI, называющий другую базу по умолчанию, должен это указать:
mongodb://root:pw@db.example.net:27017/shop?authSource=adminБез authSource=admin драйвер ищет пользователя root внутри shop, не находит и падает с Authentication failed и кодом ошибки 18. Учётные данные при этом были верными. Если authSource не указан, по умолчанию берётся база из URI; если в URI базы нет, по умолчанию берётся admin.
Не используйте суперпользователя для своего приложения. Заведите пользователя, ограниченного одной нужной ему базой:
use admindb.createUser({ user: "shopapp", pwd: passwordPrompt(), roles: [ { role: "readWrite", db: "shop" } ]})Такой пользователь, созданный в admin с ролью, ограниченной shop, подключается с authSource=admin и ничего не может сделать за пределами shop. Если создать его в shop, URI будет использовать authSource=shop; оба варианта допустимы, и выбрать одно соглашение и придерживаться его - значит сэкономить потом целый вечер.
Роли, которые стоит знать: read и readWrite для одной базы, dbAdmin для индексов и статистики, readWriteAnyDatabase для инструмента, который должен трогать всё, и root для суперпользователя. Для задания резервного копирования нужна backup; для проверки мониторинга нужна clusterMonitor. Давайте каждому подключающемуся своего пользователя, потому что именно это делает полезными лог сервера и db.currentOp(), когда что-то идёт не так. Более широкая версия этого довода есть в чек-листе безопасности баз данных.
Подключение через mongosh и Compass#
mongosh - текущая оболочка; старый бинарный файл mongo удалён в MongoDB 6.0. Он принимает либо полный URI, либо отдельные флаги:
$ mongosh "mongodb://shopapp@db.example.net:27017/shop?authSource=admin"$ mongosh --host db.example.net --port 27017 \ -u shopapp -p --authenticationDatabase admin shopНе указывайте пароль и позвольте оболочке его запросить. Набранный в командной строке, он попадает в историю оболочки и в список процессов, где его может прочитать любой другой пользователь машины.
Оказавшись внутри, четыре команды покажут, где вы находитесь:
db.runCommand({ connectionStatus: 1 }) // who am I, and with what rolesdb.getName() // which database am I inshow dbs // what can I seedb.serverStatus().connections // current, available, totalCreatedmongosh работает и неинтерактивно, что делает его полезным в задании cron или скрипте деплоя. mongosh "$MONGODB_URI" --quiet --eval 'db.orders.countDocuments()' печатает одно число и завершается; mongosh "$MONGODB_URI" --file migrate.js выполняет файл. Оба возвращают ненулевой код выхода, если скрипт выбросил исключение, поэтому они сочетаются с set -e так, как вы и хотели бы. Делайте такие скрипты идемпотентными, потому что второй запуск обычно и оказывается тем, который имеет значение.
Compass, официальный GUI, принимает тот же URI в поле подключения. Он разбирает его на поля, что делает его неплохим способом проверить, правильно ли составлена выданная вам строка, и умеет сохранять подключения в избранное. Он также предлагает вкладку SSH-туннеля, а это правильный ответ, когда базу данных вообще не следует выставлять в интернет, а достучаться до неё нужно через машину, которая может. Всё, что делает Compass, делает и mongosh, так что держите оболочку под рукой на случай, когда GUI уверенно ошибается.
Параметры подключения, которые стоит задавать#
| Параметр | По умолчанию | Что делает |
|---|---|---|
authSource | база из URI, иначе admin | Где был создан пользователь |
retryWrites | true | Повторяет запись один раз после сбоя сети |
w | majority в MongoDB 5.0+ | Сколько участников должны подтвердить запись |
readPreference | primary | Куда идут чтения в replica set |
maxPoolSize | 100 | Сколько соединений этот клиент откроет на сервер |
minPoolSize | 0 | Сколько соединений держится открытыми в простое |
maxIdleTimeMS | не задан | Закрывает соединения, простаивающие дольше этого времени |
serverSelectionTimeoutMS | 30000 | Сколько искать пригодный сервер, прежде чем упасть |
connectTimeoutMS | 30000 | Таймаут TCP-подключения |
socketTimeoutMS | не задан | Не трогайте, если не знаете зачем |
appName | нет | Метка, видимая в логах сервера и currentOp |
tls | false, true с +srv | Шифрует соединение |
compressors | нет | zstd, zlib или snappy для wire-протокола |
Три практических замечания. maxPoolSize=100 на клиента - это щедро; небольшое приложение с четырьмя рабочими процессами объявляет возможные четыреста соединений серверу, у которого может быть несколько сотен мегабайт памяти. Задайте значение, о котором вы подумали: 10-20 на процесс хватает для большинства нагрузок, а рассуждение обобщается в статье пулы соединений и лимиты.
serverSelectionTimeoutMS со значением по умолчанию в тридцать секунд - причина, по которой неверно настроенный URI выглядит как зависание, а не как сбой. Если снизить его в разработке до 5000, тридцатисекундная загадка превращается в немедленную и читаемую ошибку.
А appName=checkout-api ничего не стоит и окупается при первом же взгляде в лог сервера, когда хочется понять, какой из ваших сервисов выдал запрос, занявший девять секунд.
Два параметра на standalone-сервере не делают вообще ничего, и стоит знать какие. readPreference выбирает между участниками replica set, так что secondaryPreferred на одиночном сервере просто читает с единственного сервера, который есть. w=majority так же вырождается в «единственный узел подтвердил». Ни то ни другое не вредно, но ни то ни другое не даёт ни надёжности, ни масштабирования чтения, которые вы могли бы решить, что купили, и их установка не заменяет резервные копии. Настоящий рычаг надёжности на standalone-экземпляре - журналирование, которое включено по умолчанию и сбрасывается через короткий интервал, а настоящий рычаг масштабирования чтения - индекс, не дающий запросу читать миллион документов.
Драйверы: один клиент, используемый повторно#
Ошибка, порождающая большинство жалоб на производительность MongoDB, - создание клиента на каждый запрос. MongoClient - это пул, монитор топологии и набор фоновых потоков. Он рассчитан на то, чтобы его создали один раз при старте процесса и разделяли на всё время жизни процесса. Он потокобезопасен и безопасен для совместного использования между асинхронными задачами; между fork его делить небезопасно.
import { MongoClient } from "mongodb";const client = new MongoClient(process.env.MONGODB_URI, { maxPoolSize: 20, serverSelectionTimeoutMS: 5000, appName: "checkout-api",});await client.connect();const shop = client.db("shop");export const orders = shop.collection("orders");from pymongo import MongoClientclient = MongoClient( os.environ["MONGODB_URI"], maxPoolSize=20, serverSelectionTimeoutMS=5000, appName="checkout-api",)orders = client["shop"]["orders"]Оба клиента подключаются лениво: в сети ничего не происходит до первой операции, поэтому неверный URI может выглядеть нормально при старте и упасть на первом запросе. Если вы хотите узнать об этом при запуске, выполните ping в рамках проверки работоспособности:
await client.db("admin").command({ ping: 1 });Это же и правильное тело для проверки готовности, потому что оно доказывает, что пул может достичь сервера и пройти аутентификацию, а именно это и значит «база данных работает».
Как не пускать строку в репозиторий#
URI содержит пароль, а значит, это секрет, и его место в окружении, а не в дереве исходников:
MONGODB_URI=mongodb://shopapp:p%40ss@db.example.net:27017/shop?authSource=adminОтсюда четыре правила, и они скучны не случайно. Держите .env в .gitignore и коммитьте .env.example с формой, но без значений. Используйте для staging другого пользователя и другую базу, чем для production, чтобы неверно настроенный деплой в staging не мог писать в реальные данные. Записывайте URI в лог с вычищенными учётными данными или не записывайте вообще, потому что обработчик ошибок, печатающий строку подключения, навсегда отправляет пароль в ваш агрегатор логов. И меняйте пароль, когда кто-то уходит, что реалистично, только если приложение читает его из одного места. Где должны жить эти значения, разобрано в статье переменные окружения и секреты.
Доступность извне важна не меньше пароля. База данных, доступная из всего интернета с угадываемым пользователем, находится сканерами за часы, а коллекции с требованием выкупа, которые находят в MongoDB без аутентификации, - не миф. Убедитесь, что аутентификация действительно включена, по возможности ограничьте, откуда можно подключаться, и никогда не оставляйте тестовый экземпляр работающим с конфигурацией по умолчанию.
Ошибки и что они на самом деле означают#
`Authentication failed` (код 18) - гораздо чаще неверный authSource, чем неверный пароль. Проверьте, в какой базе был создан пользователь, затем попробуйте те же учётные данные в mongosh с --authenticationDatabase.
`MongoServerSelectionError` / `ServerSelectionTimeoutError` - драйвер не смог найти сервер для общения за время serverSelectionTimeoutMS. К учётным данным это не имеет отношения. Неверный хост, неверный порт, сервер не слушает по доступному адресу или firewall.
`connect ECONNREFUSED 203.0.113.10:27017` - по этому адресу что-то ответило и отказало. Обычно сервер привязан только к 127.0.0.1, что является значением по умолчанию с MongoDB 3.6.
`querySrv ENOTFOUND _mongodb._tcp.example.net` - URI с +srv, указывающий на хост без SRV-записей. Используйте mongodb:// с портом.
`Password contains unescaped characters` - закодируйте через percent-кодирование, как описано выше.
`command find requires authentication` - вы подключены, но анонимно. Учётных данных не было в URI, либо драйверу передали базу, которая не является authSource.
`Unsupported OP_QUERY command` - драйвер старше сервера и говорит на протоколе, удалённом в MongoDB 5.1. Обновите пакет драйвера.
FAQ#
Что такое authSource и зачем он нужен?
Он называет базу данных, в которой хранится учётная запись пользователя, а это не обязательно та база, в которой вы хотите работать. Суперпользователи обычно живут в admin, поэтому URI, указывающему на базу приложения, нужен authSource=admin, чтобы их найти. Ошибётесь - и получите сбой аутентификации при совершенно верных учётных данных.
Чем mongodb:// отличается от mongodb+srv://?
mongodb+srv:// запрашивает у DNS список серверов вместо чтения его из URI и по умолчанию включает TLS. Для него должны существовать SRV-записи, и порт в нём указать нельзя. Для одиночного standalone-сервера правильная форма - обычный mongodb:// с хостом и портом.
Можно ли подключаться к MongoDB из браузера?
Нет. MongoDB говорит на бинарном wire-протоколе поверх TCP, а не по HTTP, поэтому соединение устанавливает ваш серверный код. Поэтому же перед тарифом базы данных нет обратного прокси: проксировать было бы нечего. Ваше приложение подключается по порту, а браузер общается с вашим приложением.
Нужен ли TLS в моей строке подключения?
Если трафик идёт через публичный интернет - да, и сервер сначала должен быть для этого настроен. Внутри одной машины, где приложение и база данных на одном хосте, а база слушает только loopback-адрес, он ничего не добавляет. Включение tls=true против сервера, который для этого не настроен, падает на handshake.
Сколько соединений должно открывать моё приложение?
Гораздо меньше, чем 100 на клиента по умолчанию. Начните с 10-20 на процесс, умножьте на число процессов и сверьте результат с db.serverStatus().connections. Каждое открытое соединение стоит памяти на сервере, поэтому пул, размер которого выбран наудачу, - способ исчерпать её на небольшом экземпляре.
Что читать дальше?
Когда вы подключились, скорость базы решают два фактора: как устроены документы и какие индексы существуют - см. схема и индексы MongoDB. Прежде чем класть туда что-то настоящее, прочитайте mongodump и mongorestore и сделайте backup, который вы хотя бы раз восстановили.




Комментарии
Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.