Статьи

Обнаружение и мониторинг серверов в драйверах MongoDB следующего поколения

[Этот пост был написан Джесси Джирью Дэвисом]

Как драйвер MongoDB обнаруживает и контролирует один сервер, набор серверов mongos или набор реплик? Как это определяет, какие типы серверов они? Как она поддерживает эту информацию в актуальном состоянии? Как он обнаруживает полный набор реплик с учетом начального списка хостов и как он реагирует на увольнения, выборы, перенастройки, сетевые ошибки или потерю сервера?

В прошлом каждый драйвер MongoDB отвечал на эти вопросы немного по-своему, и монго немного отличались от драйверов. Мы не могли ответить на такие вопросы, как: «Как только я добавлю вторичную копию в свой набор реплик, сколько времени потребуется драйверу, чтобы начать его использовать?» Или «Как драйвер определяет, когда основной шаг падает, и как он реагирует?»

Чтобы стандартизировать наши драйверы, я написал  Спецификацию обнаружения и мониторинга серверов с Дэвидом Голденом, Крейгом Уилсоном, Джеффом Йемином и Берни Хакеттом. Начиная с выпусков драйверов следующего поколения этой весной, все наши драйверы соответствуют спецификации и отвечают на эти вопросы одинаково. Или, если есть законные причины для их различия, существует как можно меньше различий, и каждое из них четко объяснено в спецификации. Даже в тех случаях, когда несколько ответов кажутся одинаково хорошими, водители договариваются об одном способе сделать это.

Спецификация описывает, как драйвер контролирует топологию:

Топология:  состояние вашего развертывания. Что это за тип развертывания, какие серверы доступны и какие они (монго, первичные, вторичные и т. Д.).

Спецификация охватывает все топологии MongoDB, но наборы реплик являются наиболее интересными. Поэтому я объясню алгоритм спецификации для наборов реплик, рассказав историю вашего приложения по мере того, как оно проходит жизненные этапы: оно запускается, обнаруживает набор реплик и достигает устойчивого состояния. Затем возникает кризис — я разливаю кофе на материнскую плату вашего основного сервера — и разрешение — набор реплик выбирает новый первичный, и драйвер обнаруживает его.

На каждом этапе мы будем наблюдать типичный многопоточный драйвер PyMongo 3.0, типичный однопоточный драйвер Perl Driver 1.0 и гибрид C Driver 1.2. (Я реализовал обнаружение и мониторинг сервера PyMongo. Дэвид Голден написал версию Perl, а Саманта Риттер и Джейсон Кери написали версию на C.)

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

Запускать

Когда ваше приложение инициализируется, оно создает MongoClient. В Python:

client = MongoClient(
    ‘mongodb://hostA,hostB/?replicaSet=my_rs’)

В Perl:

my $client = MongoDB::MongoClient->new({
    host => "mongodb://hostA,hostB/?replicaSet=my_rs"
});

В C вы можете создать клиента напрямую:

mongoc_client_t *client = mongoc_client_new (
    "mongodb://hostA,hostB/?replicaSet=my_rs");

Или создайте пул клиентов:

mongoc_client_pool_t *pool = mongoc_client_pool_new (
    "mongodb://hostA,hostB/?replicaSet=my_rs");
 
mongoc_client_t *client = mongoc_client_pool_pop (pool);

Важным улучшением драйверов следующего поколения является то, что конструктор больше не блокирует, пока он устанавливает первоначальное соединение. Вместо этого конструктор не выполняет сетевой ввод-вывод. PyMongo запускает фоновый поток на сервер (в этом примере два потока), чтобы инициировать обнаружение, и возвращает управление вашему приложению без блокировки. Perl ничего не делает, пока вы не попытаетесь выполнить операцию; тогда это соединяется по требованию.

В драйвере C, если вы создаете клиент напрямую, он ведет себя как драйвер Perl: он подключается по требованию в основном потоке. Но пул клиентов C Driver запускает один фоновый поток для обнаружения и мониторинга всех серверов.

Правило спецификации «нет ввода-вывода в конструкторах» — это большой выигрыш для веб-приложений, использующих наши драйверы следующего поколения: в случае кризиса серверы приложений могут быть перезапущены, а серверы MongoDB недоступны. Ваше приложение не должно выдавать ошибку при запуске, когда оно создает клиентский объект. Он запускается отключенным и пытается достичь ваших серверов, пока это не удастся.

открытие

Первоначальный список хостов, который вы предоставляете, называется «начальным списком»:

 Начальный список: начальный список адресов серверов, предоставленных MongoClient.

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

Я склонен думать о драйвере как о крошечной совокупности информации о вашей топологии. Мониторинг предоставляет информацию, а операции вашего приложения требуют информации. Их требования определены в Спецификации выбора сервера Дэвида Голдена  , а способ предоставления информации определен здесь, в Спецификации обнаружения и мониторинга сервера. В начале информации нет, а мониторы спешат ее снабжать. Подробнее о спросе я расскажу позже, в разделе «Кризис».

Многопоточная

Давайте начнем с PyMongo. В PyMongo, как и в других многопоточных драйверах, конструктор MongoClient запускает один поток монитора каждый для «hostA» и «hostB».

Монитор:  Поток или асинхронная задача, которая периодически проверяет состояние одного сервера.

Каждый монитор подключается к назначенному серверу и выполняет команду «ismaster». Не обращайте внимания на архаичное имя команды, которое датируется днями репликации мастер-подчиненный, которые давно заменяются наборами реплик. Команда ismaster — это рукопожатие клиент-сервер. Допустим, драйвер сначала получает ответ hostB:

ismaster = {
    "setName": "my_rs",
    "ismaster": false,
    "secondary": true,
    "hosts": [
        "hostA:27017",
        "hostB:27017",
        "hostC:27017"]}

hostB подтверждает, что он принадлежит вашему набору реплик, сообщает, что он является вторичным, и перечисляет членов в конфигурации набора реплик. PyMongo видит хост, о котором он не знал, hostC, поэтому он запускает новый поток для подключения к нему.

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

db = client.get_database("dbname", read_preference=ReadPreference.SECONDARY)
# Unblocks when a secondary is found.
db.collection.find_one()

Между тем, открытие продолжается. PyMongo ждет ответов ismaster от hostA и hostC. Допустим, что hostC отвечает следующим, и его ответ включает в себя «ismaster»: true:

ismaster = {
    "setName": "my_rs",
    "ismaster": true,
    "secondary": false,
    "hosts": [
        "hostA:27017",
        "hostB:27017",
        "hostC:27017"]}

Теперь PyMongo знает основной, поэтому все операции чтения и записи разблокированы. PyMongo все еще ждет ответа от hostA; как только это произойдет, он может использовать hostA для вторичного чтения.

Однопоточного

Многопоточный код Perl проблематичен, поэтому драйвер Perl не запускает поток для каждого хоста. Как тогда он обнаружит ваш сет? Когда вы создаете MongoClient, он не вводит / выводит. Он ждет, когда вы начнете операцию, прежде чем подключится. Как только вы это сделаете, он сканирует хосты последовательно, первоначально в случайном порядке.

Сканирование:  однопоточный драйвер, проверяющий состояние всех серверов.

Допустим, драйвер начинается с hostB, вторичного. Вот деталь, которую я не показывал вам ранее: участники набора реплик говорят, кто, по их мнению, является основным Ответ HostB включает в себя «primary»: «hostC: 27017»:

ismaster = {
    "setName": "my_rs",
    "ismaster": false,
    "secondary": true,
    "primary": "hostC:27017",
    "hosts": [
        "hostA:27017",
        "hostB:27017",
        "hostC:27017"]}

Драйвер Perl использует эту подсказку для размещения hostC следующим в порядке сканирования, потому что подключение к первичному является его главным приоритетом. Он проверяет hostC и подтверждает, что это первично. Наконец, он проверяет hostA, чтобы убедиться, что он может подключиться, и обнаруживает, что hostA является еще одним вторичным. Сканирование завершено, и драйвер продолжает работу вашего приложения.

Гибридный

Драйвер C имеет два режима для обнаружения и мониторинга сервера: однопоточный и объединенный. Однопоточный режим оптимизирован для встраивания драйвера C в такие языки, как PHP: приложения PHP развертывают множество однопоточных процессов, подключенных к MongoDB. Каждый процесс использует те же соединения для сканирования топологии, что и для операций приложения, поэтому общее количество соединений от многих процессов сводится к минимуму.

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

C Драйвер однопоточный режим

Драйвер C сканирует серверы в главном потоке, если вы создаете один клиент:

mongoc_client_t *client = mongoc_client_new (
    "mongodb://hostA,hostB/?replicaSet=my_rs");

В однопоточном режиме драйвер C блокирует сканирование вашей топологии периодически с основным потоком, как и драйвер Perl. Но в отличие от последовательного сканирования Perl Driver, C Driver проверяет все серверы параллельно. Используя неблокирующий сокет для каждого участника, он начинает проверку каждого участника одновременно и использует асинхронную функцию «опрос» для получения событий из сокетов, пока все не ответят или не истечет время ожидания. Драйвер обновляет свою топологию после завершения вызовов ismaster. Наконец, он завершает сканирование и возвращает управление вашему приложению.

Принимая во внимание, что сканирование топологии Perl Driver длится на сумму всех проверок сервера (включая тайм-ауты), сканирование топологии драйвера C длится только максимум длительности любой проверки или установки времени ожидания соединения, в зависимости от того, что короче. Иными словами, в однопоточном режиме вентиляторы C Driver запускаются одновременно для всех проверок, затем включаются вентиляторы после завершения или тайм-аута всех проверок. Этот метод сканирования топологии «разветвляется, разветвляется» дает драйверу C преимущество при сканировании очень больших наборов реплик или наборов с несколькими элементами с высокой задержкой.

Режим пула драйвера C
Чтобы активировать режим пула драйвера C, создайте пул клиентов:

mongoc_client_pool_t *pool = mongoc_client_pool_new (
    "mongodb://hostA,hostB/?replicaSet=my_rs");
 
mongoc_client_t *client = mongoc_client_pool_pop (pool);

Пул запускает один фоновый поток для мониторинга. Когда поток начинается, он разворачивается и подключается ко всем серверам в начальном списке, используя неблокирующие сокеты и простой цикл обработки событий. Получая ответы от ismaster от серверов, он обновляет свой взгляд на топологию, так же как и многопоточный драйвер, как PyMongo. Когда он обнаруживает новый сервер, он начинает подключаться к нему и добавляет новый сокет в список неблокирующих сокетов в своем цикле событий.

Как и в PyMongo, когда драйвер C находится в режиме фонового потока, операции вашего приложения разблокируются, как только мониторинг обнаруживает работоспособный сервер. Например, если ваш код C заблокирован в ожидании вставки в первичный, он разблокируется, как только обнаруживается первичный, а не в ожидании проверки всех вторичных серверов.

Устойчивое состояние

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

По умолчанию потоки монитора в PyMongo проверяют свои серверы каждые десять секунд, как и монитор драйвера C в режиме фонового потока. Драйвер Perl и драйвер C в однопоточном режиме блокируют ваше приложение для повторного сканирования набора реплик один раз в минуту.

Если вам нравится моя модель драйвера «спрос и предложение», то устойчивое состояние — это когда спрос вашего приложения на информацию о топологии удовлетворяется. Драйвер время от времени обновляет свой запас информации, чтобы убедиться, что он готов к будущим требованиям, но срочности нет.

Кризис

Так что я забрел в ваш центр обработки данных, взбивая мой капучино, и спотыкался и пролил его на материнскую плату hostC. Теперь у вашего набора реплик нет первичного. Что происходит дальше?

Когда ваше приложение выполняет следующую запись в первичную систему, оно получает тайм-аут сокета. Теперь он знает, что основной ушел. Его спрос на информацию больше не сбалансирован с предложением. Следующая попытка записать блоки, пока не найден основной.

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

Многопоточный : в драйверах, таких как PyMongo, потоки монитора ждут только полсекунды между проверками сервера, а не десять секунд. Они хотят знать как можно скорее, вернулся ли первичный или один из вторичных был избран первичным.

Однопоточный : Драйверы, такие как Perl Driver, спят полсекунды между сканированиями топологии. Операция записи приложения остается заблокированной, пока драйвер не найдет основной.

Однопоточный драйвер C : В однопоточном режиме драйвер C переходит на полсекунды между сканированием, как и драйвер Perl. Во время сканирования драйвер одновременно запускает неблокирующие команды ismaster на всех серверах, как я описал выше.

C Driver Pooled : каждый раз, когда поток монитора драйвера получает ответ ismaster, он планирует следующий вызов ismaster этого сервера в цикле событий только на полсекунды в будущем.

разрешение

Ваши подчиненные, hostA и hostB, незамедлительно обнаружат мой саботаж по hostC и проведут выборы. В MongoDB 3.0 выборы занимают всего пару секунд. Допустим, hostA становится основным.

Через полсекунды или меньше ваш драйвер перепроверяет hostA и видит, что он теперь является основным. Он разблокирует записи вашего приложения и отправляет их на hostA. В PyMongo потоки монитора расслабляются и возвращаются к своей стратегии медленного опроса: они спят десять секунд между проверками сервера. То же самое для монитора драйвера C в режиме фонового потока. Драйвер Perl и драйвер C в однопоточном режиме не сканируют топологию в течение еще одной минуты. Спрос и предложение снова находятся в балансе.

Тестирование на соответствие

Я особенно взволнован модульными тестами, которые сопровождают Спецификацию обнаружения и мониторинга сервера. У нас есть  38 тестов, которые формально указаны в файлах YAML , с входными данными и ожидаемыми результатами для ряда сценариев. Для каждого водителя мы пишем тестовый бегун, который передает входные данные драйверу и проверяет результат. Это устраняет путаницу в том, что означает спецификация, или соответствуют ли все драйверы ей. Вы можете отслеживать наш прогресс в достижении полного соответствия в  системе отслеживания проблем MongoDB .

Дальнейшее обучение

Спекуляция длинная, но послушная. Это объясняет алгоритм мониторинга в очень мелких деталях. Вы можете прочитать резюме и саму спецификацию здесь: