RE:NODE

Формат MongoDB URI: mongodb+srv, хосты и опции

Автор Команда RE:NODE
16 мин чтения


MongoDB URI выглядит так: mongodb://[user:password@]host1[:port1][,host2[:port2],...][/database][?options]. Если ничего не указано, все официальные инструменты исходят из mongodb://localhost:27017. Форма mongodb+srv:// принимает ровно одно имя хоста без порта, запрашивает у DNS настоящий список хостов и по умолчанию включает TLS. Несколько хостов через запятую - это начальный список (seed list) для replica set или набора маршрутизаторов mongos, а не список запасных серверов: драйвер подключится к тому участнику, который сочтёт нужным, и именно поэтому существует directConnection.

Это весь формат. Дальше разбирается, что каждая часть делает на практике: значения по умолчанию, DNS-записи за +srv, как драйвер обращается с несколькими хостами, опции, меняющие поведение, и куда вставлять строку в PyMongo, драйвере Node, mongosh, конфигурационных файлах .NET и Compass. Если нужен более мягкий разбор частей URI и кодирования паролей, начните со статьи MongoDB connection strings. Если у вас есть URI, а сервер отвечает Authentication failed, переходите к руководству по authSource: здесь ни то, ни другое не повторяется.

Формат по частям#

code
mongodb://                                   schemeshopapp:pw@                                  credentials (optional)db1.example.net:27017,db2.example.net:27017  host list/shop                                        default database (optional)?replicaSet=rs0&appName=api                  options (optional)

Если соединить части без пробелов, получится mongodb://shopapp:pw@db1.example.net:27017,db2.example.net:27017/shop?replicaSet=rs0&appName=api.

Правила, которые легко забыть:

  • Порт необязателен и по умолчанию равен 27017 для каждого хоста, у которого он не указан. mongodb://db.example.net и mongodb://db.example.net:27017 - одно и то же.
  • Литерал IPv6 пишется в квадратных скобках: mongodb://[2001:db8::10]:27017. Без скобок двоеточия в адресе принимаются за разделитель порта, и URI не разбирается.
  • Unix domain socket записывается как путь в процентном кодировании на месте хоста: mongodb://%2Ftmp%2Fmongodb-27017.sock. mongod создаёт этот сокет по умолчанию в Linux и macOS, и это удобный способ подключиться с той же машины, вообще не открывая TCP-порт.
  • Слэш перед ? обязателен, когда есть опции. mongodb://host:27017/?tls=true корректен; драйверы по-разному относятся к отсутствующему слэшу, так что не полагайтесь на это.
  • Имена опций нечувствительны к регистру (authsource и authSource - одно и то же), значения чувствительны, а булевы значения - строчные строки true и false. Некоторые драйверы принимают также 1, yes и подобное, но это непереносимо.
  • Опции соединяются знаком &. Некоторые опции можно повторять, например readPreferenceTags; у остальных побеждает последняя либо драйвер выдаёт ошибку, в зависимости от драйвера.

Значение по умолчанию: localhost:27017

mongodb://localhost:27017 - строка из каждого руководства, значение по умолчанию на экране нового подключения в Compass и то, к чему подключается MongoClient() без аргументов в PyMongo. У неё есть известная ловушка. В Node.js 17 и новее localhost может сначала разрешаться в IPv6-адрес ::1, тогда как mongod по умолчанию слушает только 127.0.0.1. В итоге получается connect ECONNREFUSED ::1:27017 при совершенно исправно работающем сервере. В новых версиях Node пробуются оба семейства адресов, и это встречается реже, но надёжное решение - писать в локальных URI 127.0.0.1 вместо localhost или добавить ::1 в net.bindIp в mongod.conf.

mongosh без аргументов тоже использует не голое значение по умолчанию. Он подключается к mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000 и печатает эту строку первой, что неплохо подсказывает, какие опции стоит задавать, когда вы всегда работаете только с одним локальным сервером.

Какую базу данных называет путь

Часть /database - это база данных клиента по умолчанию и значение по умолчанию для authSource, если эта опция не указана. Она ничего не ограничивает: клиент, подключённый с /shop, всё равно может открыть client["reports"], если у пользователя есть права на неё. Если путь пуст, база по умолчанию зависит от драйвера (часто test или никакой), а authSource принимает значение admin. Именно второе значение по умолчанию вызывает большую часть путаницы с аутентификацией, а руководство по пользователям и ролям объясняет, почему пользователи живут там, где живут.

mongodb+srv:// и DNS-записи за ним#

URI со списком хостов из DNS (mongodb+srv://) заменяет перечень хостов одним DNS-именем. Увидев такую строку, драйвер делает два запроса ещё до открытия первого соединения.

hosts and portsdefault optionsnormal discoverymongodb+srv URIcluster0.example.netЗапрос SRV_mongodb._tcp.cluster0...Запрос TXTcluster0.example.netХосты и опцииtls=true impliedЧлены replica set:27017 each
Что драйвер делает с URI mongodb+srv

SRV-запись лежит по имени _mongodb._tcp.<hostname> и перечисляет по одному целевому хосту и порту на каждого участника. TXT-запись лежит по самому имени хоста и содержит опции URI в виде строки запроса, например authSource=admin&replicaSet=rs0. Там разрешены только три опции - authSource, replicaSet и loadBalanced, - а TXT-запись может быть только одна; несколько записей считаются ошибкой. Всё, что написано в самом URI, переопределяет TXT-запись.

Увидеть в точности то, что увидит драйвер, можно обычными DNS-инструментами:

bash
$ dig +short SRV _mongodb._tcp.cluster0.example.net0 0 27017 shard-00-00.example.net.0 0 27017 shard-00-01.example.net.0 0 27017 shard-00-02.example.net.$ dig +short TXT cluster0.example.net"authSource=admin&replicaSet=rs0"

В Windows то же делает nslookup -type=SRV _mongodb._tcp.cluster0.example.net. Если SRV-запрос вернулся пустым, в приложении вы увидите ошибку querySrv ENOTFOUND (Node) или ConfigurationError с упоминанием DNS-имени (PyMongo), и никакие изменения опций не помогут.

Правила, которые следуют из устройства механизма:

  • Ровно одно имя хоста и никакого порта. Порты берутся из SRV-записей.
  • Имена хостов, которые возвращает SRV-запись, должны находиться под тем же родительским доменом, что и запрошенное имя. cluster0.example.net может указывать на shard-00-00.example.net, но не на db.othercompany.com; драйверы отклоняют такие цели из соображений безопасности. Старые драйверы также требовали минимум три части в имени, разделённые точками; в новых версиях это ослабили - если двухчастное имя не работает, загляните в changelog вашего драйвера.
  • TLS включён по умолчанию. Если вам действительно нужно незашифрованное соединение, явно добавьте tls=false, но с +srv это почти никогда не требуется.
  • directConnection=true с +srv не допускается, потому что весь смысл в том, что драйвер находит несколько серверов.
  • Две новые опции настраивают поиск: srvServiceName (по умолчанию mongodb) меняет часть _mongodb в имени записи, а srvMaxHosts ограничивает, сколько из возвращённых хостов используется; она рассчитана на большие наборы маршрутизаторов mongos.

При подключении к шардированным кластерам через mongos драйверы периодически повторяют запрос SRV-записи и добавляют или убирают маршрутизаторы по мере изменения записей. Для replica set в этом нет необходимости, потому что после первого соединения сам набор сообщает свой состав.

mongodb+srv:// стоит использовать, когда эти записи кто-то ведёт за вас - управляемый кластер, состав которого меняется, или ваш собственный replica set с записями, опубликованными в вашей DNS-зоне. Для одного сервера это не нужно. Одна база с A-записью и mongodb://db.example.net:27017 проще, не требует дополнительных запросов и ломается реже и менее причудливо. О типах A, TXT и SRV, если вы хотите публиковать их сами, рассказывает статья DNS records explained.

Несколько хостов и replicaSet#

Список хостов через запятую - это seed list. Драйвер подключается к указанным хостам, спрашивает у каждого, как replica set видит себя в данный момент (команда hello), и дальше работает по тому, что сообщает сервер, а не по URI.

code
mongodb://db1.example.net:27017,db2.example.net:27017,db3.example.net:27017/shop?replicaSet=rs0

Что это значит на практике:

  • Перечислять всех участников не обязательно. Для обнаружения достаточно одного доступного участника, хотя если перечислить всех, запуск сработает и тогда, когда первый из них недоступен.
  • Порядок хостов не определяет, куда идут записи. Записи всегда идут на текущий primary, а чтения - туда, куда их направляет readPreference.
  • replicaSet должен совпадать с настоящим именем набора (значением _id, переданным в rs.initiate()). Участник, сообщающий другое имя набора, исключается из топологии, и если исключены все, вы получите тайм-аут выбора сервера, а не понятное сообщение. Настоящее имя показывает db.hello().setName в mongosh.
  • Имена хостов, которыми драйвер в итоге пользуется, берутся из конфигурации replica set, а не из вашего URI. Если набор был инициализирован с localhost:27017 или с внутренними именами контейнеров Docker, клиент вне этой сети узнаёт имена, которые не может разрешить, и после удачного первого контакта упирается в тайм-аут. Исправьте имена участников в конфигурации replica set через rs.reconfig(); не заметайте проблему под ковёр на стороне клиента.

В шардированном кластере список хостов содержит маршрутизаторы mongos, опции replicaSet нет, а драйвер распределяет операции между доступными маршрутизаторами, предпочитая те, у которых меньше задержка.

directConnection

directConnection=true велит драйверу общаться только с одним хостом из URI и пропустить обнаружение. Разрешено только с одним хостом и никогда с +srv. Используйте её, когда:

  • Нужно подключиться к конкретному участнику, обычно secondary, для обслуживания или для выполнения запроса без обращения к primary.
  • Настроенные в replica set имена хостов недоступны оттуда, где вы находитесь, например при SSH-туннеле к одному участнику. Такая схема описана в статье SSH tunnels and port forwarding.

Значение по умолчанию важно, и оно менялось. В PyMongo 4 и в драйвере Node начиная с 4.0 directConnection по умолчанию равна false, поэтому URI с одним хостом, который оказывается участником replica set, запускает обнаружение: драйвер находит primary и отправляет записи туда. PyMongo 3 вёл себя так, будто для одного хоста она равна true. Код, который после обновления рассчитывал на "подключаться только к этой машине", внезапно начинает работать с другим сервером, поэтому задавайте опцию явно, когда различие имеет значение. На отдельном сервере (standalone), который вообще не сообщает о replica set, оба значения приводят к одному результату.

loadBalanced=true - третья опция топологии, для кластеров за балансировщиком нагрузки, понимающим протокол MongoDB (её используют некоторые управляемые serverless-предложения). Её нельзя сочетать с replicaSet, directConnection=true или более чем одним хостом, и встретите вы её только в URI, который выдал провайдер.

Опции, меняющие поведение#

Полный список опций URI длинный. Здесь собраны те, что встречаются в реальных строках подключения и меняют работу клиента.

OptionDefaultEffect
authSourcepath db, else adminБаза, где хранится пользователь
tlsfalse; true for +srvШифрует соединение
retryWritestrueПовторяет неудавшуюся запись один раз
wserver defaultПодтверждение записи
readPreferenceprimaryКакие участники обслуживают чтения
maxPoolSize100Соединений на сервер
connectTimeoutMSdriver-specificТайм-аут TCP-соединения
appNamenoneМетка в логах сервера
directConnectionfalseПропустить обнаружение топологии

authSource

Указывает базу данных, в которой создан пользователь. Правила значений по умолчанию описаны выше, а разбор проблем - в руководстве по аутентификации. Добавить стоит только эквивалент для командной строки: mongosh принимает --authenticationDatabase, это та же настройка под другим именем.

tls

tls=true шифрует соединение и проверяет сертификат сервера по системному хранилищу доверенных сертификатов или по центру сертификации, указанному в tlsCAFile. ssl=true - более старый псевдоним, он по-прежнему работает. Сервер сначала должен быть настроен на TLS; если включить её в URI для сервера без TLS, соединение рвётся на рукопожатии, и обычно это проявляется как тайм-аут. tlsAllowInvalidCertificates=true и tlsInsecure=true существуют для тестов и никогда не должны попадать в production.

retryWrites и w

retryWrites=true стала значением по умолчанию в драйверах со времён MongoDB 4.2. Запись, завершившаяся сетевой ошибкой или выборами primary, повторяется один раз, и это безопасно, потому что сервер отслеживает идентификатор операции. Повторяемые записи требуют replica set или шардированного кластера; на отдельном сервере драйвер просто не повторяет запись. Иногда вы увидите retryWrites=false в строках, написанных для старых серверов или необычных движков хранения, - оставляйте значение по умолчанию, если только ошибка сервера не говорит иного.

w задаёт write concern: w=1 означает, что запись подтвердил primary, w=majority - что её подтвердило большинство голосующих участников, а w=0 - что не подтвердил никто и о сбоях вы никогда не узнаете. Начиная с MongoDB 5.0 неявное значение сервера по умолчанию для большинства replica set - majority, поэтому чаще всего правильно не указывать w в URI. journal=true добавляет условие "и запись попала на диск в журнал", а wtimeoutMS не даёт записи с majority ждать вечно, когда в сети слишком мало участников. На одиночном сервере majority выполняется самим этим сервером.

readPreference

Принимает primary, primaryPreferred, secondary, secondaryPreferred и nearest. Всё, кроме primary, может вернуть данные с небольшим отставанием, потому что secondary реплицируются асинхронно. maxStalenessSeconds (минимум 90) исключает secondary, отставшие слишком сильно, а readPreferenceTags=dc:east направляет чтения на участников с подходящими тегами. С одним хостом выбирать не из чего.

maxPoolSize и connectTimeoutMS

maxPoolSize действует на каждый сервер и на каждый клиент. Один клиент, работающий с replica set из трёх участников, при значении по умолчанию может открыть до трёхсот соединений, а у каждого процесса вашего приложения свой клиент. Задавайте значение осознанно; арифметика разобрана в статье connection pools and limits. minPoolSize держит соединения наготове, а maxIdleTimeMS закрывает простаивающие.

connectTimeoutMS - это тайм-аут TCP-соединения для каждого соединения, и его значение по умолчанию - одно из немногих, которые различаются между драйверами: общая спецификация говорит о 10 секундах, PyMongo использует 20, а драйвер Node - 30. Но не эта настройка заставляет зависать неверный URI - за это отвечает serverSelectionTimeoutMS, 30 секунд везде. Новые драйверы также поддерживают timeoutMS - единый клиентский лимит на всю операцию, который призван заменить несколько старых тайм-аутов отдельных этапов.

appName

Произвольная текстовая метка до 128 байт, отправляемая при рукопожатии соединения. Она видна в логе сервера для каждого соединения и в db.currentOp(), так что можно понять, какой сервис открыл какие соединения. Это ничего не стоит; задавайте её у каждого клиента.

Строки URI для типичных конфигураций#

Большинство строк укладывается в несколько шаблонов. Сравнить их рядом быстрее, чем читать спецификацию.

code
# Локальная разработкаmongodb://127.0.0.1:27017/shop# Один удалённый сервер, пользователь в adminmongodb://shopapp:pw@db.example.net:27017/shop?authSource=admin&appName=api# Replica set из трёх участников, чтение со secondary разрешеноmongodb://shopapp:pw@db1.example.net,db2.example.net,db3.example.net/shop  ?replicaSet=rs0&authSource=admin&readPreference=secondaryPreferred# Только один участник, через SSH-туннель на локальном порту 27018mongodb://shopapp:pw@127.0.0.1:27018/shop?directConnection=true&authSource=admin# Управляемый кластер, опубликованный в DNSmongodb+srv://shopapp:pw@cluster0.example.net/shop?appName=api# Тот же кластер, записанный вручную (то, что за вас делал +srv)mongodb://shopapp:pw@shard-00-00.example.net:27017,shard-00-01.example.net:27017  ,shard-00-02.example.net:27017/shop?replicaSet=rs0&authSource=admin&tls=true

Переносы строк в примерах с replica set сделаны только для удобства чтения; настоящий URI - одна строка без пробелов. Последний пример стоит запомнить на случай, когда проблема в DNS: чтобы превратить +srv в обычный список, нужно скопировать хосты из SRV-записи, опции из TXT-записи и самостоятельно добавить tls=true. Пропустите любой из трёх пунктов - и соединение сломается по-новому.

Куда вставлять строку в каждом клиенте#

Все официальные драйверы принимают один и тот же URI - в этом и смысл его существования. Различается то, откуда строка читается и что что переопределяет.

PyMongo

python
import osfrom pymongo import MongoClientclient = MongoClient(os.environ["MONGODB_URI"], appName="reports", maxPoolSize=10)db = client.get_default_database()   # the /database from the URI

Именованные аргументы и опции URI взаимозаменяемы, а если одна и та же опция есть в обоих местах, побеждает именованный аргумент. get_default_database() возвращает базу данных из пути и выбрасывает исключение, если её там нет. Чтобы проверить строку без подключения, pymongo.uri_parser.parse_uri(uri) возвращает разобранные хосты, учётные данные и опции в виде словаря и выбрасывает исключение при любой некорректности, в том числе для имени +srv без записей.

Драйвер Node.js

javascript
import { MongoClient } from "mongodb";const client = new MongoClient(process.env.MONGODB_URI, { appName: "api" });const db = client.db(); // the /database from the URI

Второй аргумент принимает те же опции в camelCase. client.db() без имени возвращает базу данных из URI по умолчанию. Mongoose передаёт строку напрямую через mongoose.connect(uri). Разбор выполняет пакет mongodb-connection-string-url, от которого зависит драйвер и которым вы можете пользоваться сами, чтобы изучить URI или скрыть пароль перед записью в лог.

mongosh

bash
$ mongosh 'mongodb://db1.example.net,db2.example.net/shop?replicaSet=rs0' \    -u shopapp -p --authenticationDatabase admin

Заключайте URI в одинарные кавычки, чтобы оболочка не трогала & и $. mongosh также принимает replica set в более старой форме --host rs0/db1.example.net:27017,db2.example.net:27017. После подключения db.hello() показывает setName, список участников и то, кто из них primary, - это снимает любые сомнения насчёт того, что обнаружит драйвер.

.NET: appsettings.json и web.config

Драйвер C# принимает URI в виде строки, а в ASP.NET Core она должна лежать в разделе ConnectionStrings:

appsettings.json
{  "ConnectionStrings": {    "Mongo": "mongodb://shopapp:pw@db.example.net:27017/shop?authSource=admin"  }}
Program.cs
var cs = builder.Configuration.GetConnectionString("Mongo");var url = new MongoUrl(cs);builder.Services.AddSingleton<IMongoClient>(new MongoClient(url));builder.Services.AddSingleton(sp =>    sp.GetRequiredService<IMongoClient>().GetDatabase(url.DatabaseName));

MongoUrl разбирает строку, а его DatabaseName даёт базу данных из пути. Регистрируйте клиент как singleton по той же причине, что и в любом другом драйвере: это пул. В production задавайте значение как переменную окружения ConnectionStrings__Mongo, а не записывайте пароль в файл; этот приём описан в статье environment variables and secrets.

В .NET Framework строка лежит в web.config или app.config и читается через ConfigurationManager.ConnectionStrings["Mongo"].ConnectionString. Этот файл - XML, поэтому каждый & между опциями нужно писать как &amp;:

web.config
<connectionStrings>  <add name="Mongo"       connectionString="mongodb://db.example.net:27017/shop?authSource=admin&amp;appName=legacy" /></connectionStrings>

Голый & там приводит к тому, что весь конфигурационный файл не загружается, и это обычно проявляется как ошибка запуска приложения без единого упоминания MongoDB. Та же идея для строк SQL Server разобрана в статье SQL Server connection strings.

Compass

Вставьте URI в поле на экране нового подключения. "Advanced Connection Options" разбивает его по вкладкам: на вкладке General есть переключатель схемы между mongodb и mongodb+srv, список хостов и флажок Direct Connection; Authentication содержит имя пользователя, пароль и базу аутентификации; у TLS/SSL и Proxy/SSH свои вкладки. Правки во вкладках переписывают строку и наоборот, поэтому Compass - разумный способ увидеть, как интерпретируется присланный вам URI, в том числе разобрал ли он список хостов так, как вы задумали.

Ошибки, связанные с самим форматом#

Это ошибки, вызванные формой строки, а не учётными данными или сетью.

  • Порт в URI с `+srv`. mongodb+srv://cluster0.example.net:27017 отклоняется сразу. Уберите порт.
  • Несколько хостов в URI с `+srv`. Тоже отклоняется; +srv принимает одно имя.
  • `directConnection=true` с несколькими хостами. Отклоняется, потому что нет единственного сервера, к которому можно подключиться напрямую.
  • Несовпадающее имя `replicaSet`. Выбор сервера завершается тайм-аутом через 30 секунд, хотя все хосты были доступны. Сравните с db.hello().setName.
  • Мышление в духе fallback. Если указать в одном URI рабочий и резервный серверы, отказоустойчивость между несвязанными машинами не появится. Если они не участники одного replica set, драйвер видит две несовпадающие топологии, и вы получите ошибки, а не резервирование.
  • Пробелы или переносы строк. При копировании URI с переносами из документа или сообщения в чате вместе с ним попадает пробел или перевод строки. Драйверы сообщают о неверном хосте или неизвестной опции, а лишний символ в большинстве логов не виден.
  • Незакодированные спецсимволы в пароле. Подробно разобрано в статье MongoDB connection strings; коротко: кодируйте один раз или передавайте учётные данные отдельными опциями.

На хостинге MongoDB RE:NODE каждый сервер создаётся со сгенерированными для него учётными данными, и подключаться к нему нужно напрямую по хосту и порту, указанным в тарифе. SRV-записи для него не публикуются, и никакого прокси перед ним нет, поэтому правильная форма - mongodb:// с этим хостом и портом, сгенерированным пользователем и authSource, указывающим на базу, в которой этот пользователь находится. Где панель показывает эти данные, видно в разделе о базах данных в руководствах. Если ваше приложение работает на одной из линеек для приложений, например Python, Node.js или C# / .NET, URI нужно записать в переменные окружения этого сервера на вкладке Startup, а не в код.

FAQ#

Какой MongoDB URI используется по умолчанию?

mongodb://localhost:27017, без учётных данных и без базы данных. Если на вашей машине localhost разрешается в IPv6, а mongod слушает только IPv4, используйте mongodb://127.0.0.1:27017.

Можно ли использовать mongodb+srv с IP-адресом?

Нет. +srv существует для поиска SRV- и TXT-записей по DNS-имени, а у IP-адреса их нет. Используйте mongodb:// с адресом и портом.

Нужно ли перечислять в URI всех участников replica set?

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

Почему после обновления драйвера приложение стало писать на другой сервер?

Скорее всего, изменилось значение directConnection по умолчанию. Новые драйверы находят replica set по одному хосту и направляют записи на primary. Добавьте directConnection=true, если вам действительно нужен именно этот сервер, и не добавляйте, если нужно обычное поведение replica set.

Важен ли порядок опций в URI?

Нет. Опции - это набор пар ключ-значение. Важно лишь то, что опция в URI переопределяет такую же опцию из TXT-записи +srv и что аргументы драйвера, переданные в коде, обычно переопределяют URI.

Что делает replicaSet на одиночном сервере?

На отдельном сервере она приводит к сбою соединения, потому что сервер не сообщает имени набора, а драйвер ждёт участника rs0. Добавляйте replicaSet только тогда, когда сервер действительно участник replica set, пусть даже из одного члена.