Python — это мощный и гибкий язык программирования, используемый миллионами разработчиков по всему миру для создания своих приложений. Неудивительно, что разработчики Python обычно используют хостинг MongoDB, самую популярную базу данных NoSQL, для своих развертываний из-за ее гибкости и отсутствия требований к схеме.

Итак, как лучше всего использовать MongoDB с Python? PyMongo — это дистрибутив Python, содержащий инструменты для работы с MongoDB и рекомендуемый драйвер Python MongoDB. Это довольно зрелый драйвер, который поддерживает большинство распространенных операций с базой данных, и вы можете ознакомиться с этим учебником для ознакомления с драйвером PyMongo.

При развертывании в рабочей среде настоятельно рекомендуется настроить конфигурацию набора реплик MongoDB, чтобы ваши данные были географически распределены для обеспечения высокой доступности. Также рекомендуется включить соединения SSL для шифрования трафика клиент-база данных. Мы часто проводим тестирование характеристик отказоустойчивости различных драйверов MongoDB, чтобы квалифицировать их для производственных сценариев использования или когда наши клиенты обращаются к нам за советом. В этом посте мы покажем вам, как подключиться к набору реплик MongoDB с поддержкой SSL, настроенному с использованием самозаверяющих сертификатов, с помощью PyMongo, и как проверить поведение MongoDB при отказе в вашем коде.

Подключение к MongoDB SSL с использованием самозаверяющих сертификатов

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

Интересующие нас параметры mongo_client.MongoClient — это ssl и ss_ca_cert. Чтобы подключиться к конечной точке MongoDB с поддержкой SSL, использующей самозаверяющий сертификат, для параметра ssl должно быть задано значение True, а ss_ca_cert должен указывать в файл сертификата CA.

Если вы являетесь клиентом ScaleGrid, вы можете загрузить файл сертификата ЦС для своих кластеров MongoDB из консоли ScaleGrid, как показано здесь:

Таким образом, фрагмент подключения будет выглядеть так:

>>> import pymongo
>>> MONGO_URI = 'mongodb://rwuser:@SG-example-0.servers.mongodirector.com:27017,SG-example-1.servers.mongodirector.com:27017,SG-example-2.servers.mongodirector.com:27017/admin?replicaSet=RS-example&ssl=true'
>>> client = pymongo.MongoClient(MONGO_URI, ssl = True, ssl_ca_certs = '')
>>> print("Databases - " + str(client.list_database_names()))
Databases - ['admin', 'local', 'test']
>>> client.close()
>>>

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

Тестирование поведения при отказе

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

Вы можете проверить отказоустойчивость своих приложений, вызвав аварийное переключение во время выполнения рабочей нагрузки. Самый простой способ вызвать отказоустойчивость — запустить команду rs.stepDown():

RS-example-0:PRIMARY> rs.stepDown()
2019-04-18T19:44:42.257+0530 E QUERY [thread1] Error: error doing query: failed: network error while attempting to run command 'replSetStepDown' on host 'SG-example-1.servers.mongodirector.com:27017' :
DB.prototype.runCommand@src/mongo/shell/db.js:168:1
DB.prototype.adminCommand@src/mongo/shell/db.js:185:1
rs.stepDown@src/mongo/shell/utils.js:1305:12
@(shell):1:1
2019-04-18T19:44:42.261+0530 I NETWORK [thread1] trying reconnect to SG-example-1.servers.mongodirector.com:27017 (X.X.X.X) failed
2019-04-18T19:44:43.267+0530 I NETWORK [thread1] reconnect SG-example-1.servers.mongodirector.com:27017 (X.X.X.X) ok
RS-example-0:SECONDARY>

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

import logging
import traceback
...
import pymongo
...
logger = logging.getLogger("test")
MONGO_URI = 'mongodb://rwuser:@SG-example-0.servers.mongodirector.com:48273,SG-example-1.servers.mongodirector.com:27017,SG-example-2.servers.mongodirector.com:27017/admin?replicaSet=RS-example-0&ssl=true'
try:
    logger.info("Attempting to connect...")
    client = pymongo.MongoClient(MONGO_URI, ssl = True, ssl_ca_certs = 'path-to-cacert.pem')
    db = client['test']
    collection = db['test']
    i = 0
    while True:
        try:
            text = ''.join(random.choices(string.ascii_uppercase + string.digits, k = 3))
            doc = { "idx": i, "date" : datetime.utcnow(), "text" : text}
            i += 1
            id = collection.insert_one(doc).inserted_id
            logger.info("Record inserted - id: " + str(id))
            sleep(3)
        except pymongo.errors.ConnectionFailure as e:
            logger.error("ConnectionFailure seen: " + str(e))
            traceback.print_exc(file = sys.stdout)
            logger.info("Retrying...")
    logger.info("Done...")
except Exception as e:
    logger.error("Exception seen: " + str(e))
    traceback.print_exc(file = sys.stdout)
finally:
    client.close()

Тип записей, которые это пишет, выглядит так:

RS-example-0:PRIMARY> db.test.find()
{ "_id" : ObjectId("5cb6d6269ece140f18d05438"), "idx" : 0, "date" : ISODate("2019-04-17T07:30:46.533Z"), "text" : "400" }
{ "_id" : ObjectId("5cb6d6299ece140f18d05439"), "idx" : 1, "date" : ISODate("2019-04-17T07:30:49.755Z"), "text" : "X63" }
{ "_id" : ObjectId("5cb6d62c9ece140f18d0543a"), "idx" : 2, "date" : ISODate("2019-04-17T07:30:52.976Z"), "text" : "5BX" }
{ "_id" : ObjectId("5cb6d6329ece140f18d0543c"), "idx" : 4, "date" : ISODate("2019-04-17T07:30:58.001Z"), "text" : "TGQ" }
{ "_id" : ObjectId("5cb6d63f9ece140f18d0543d"), "idx" : 5, "date" : ISODate("2019-04-17T07:31:11.417Z"), "text" : "ZWA" }
{ "_id" : ObjectId("5cb6d6429ece140f18d0543e"), "idx" : 6, "date" : ISODate("2019-04-17T07:31:14.654Z"), "text" : "WSR" }
..

Обработка исключения ConnectionFailure

Обратите внимание, что мы перехватываем исключение ConnectionFailure, чтобы справиться со всеми проблемами, связанными с сетью, с которыми мы можем столкнуться из-за отработки отказа — мы печатаем исключение и продолжаем попытки записи в базу данных. Документация драйвера рекомендует следующее:

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

Давайте запустим это и выполним отработку отказа базы данных во время ее выполнения. Вот что происходит:

04/17/2019 12:49:17 PM INFO Attempting to connect...
04/17/2019 12:49:20 PM INFO Record inserted - id: 5cb6d3789ece145a2408cbc7
04/17/2019 12:49:23 PM INFO Record inserted - id: 5cb6d37b9ece145a2408cbc8
04/17/2019 12:49:27 PM INFO Record inserted - id: 5cb6d37e9ece145a2408cbc9
04/17/2019 12:49:30 PM ERROR PyMongoError seen: connection closed
Traceback (most recent call last):
    id = collection.insert_one(doc).inserted_id
  File "C:\Users\Random\AppData\Local\Programs\Python\Python36-32\lib\site-packages\pymongo\collection.py", line 693, in insert_one
    session=session),
...
  File "C:\Users\Random\AppData\Local\Programs\Python\Python36-32\lib\site-packages\pymongo\network.py", line 173, in receive_message
    _receive_data_on_socket(sock, 16))
  File "C:\Users\Random\AppData\Local\Programs\Python\Python36-32\lib\site-packages\pymongo\network.py", line 238, in _receive_data_on_socket
    raise AutoReconnect("connection closed")
pymongo.errors.AutoReconnect: connection closed
04/17/2019 12:49:30 PM INFO Retrying...
04/17/2019 12:49:42 PM INFO Record inserted - id: 5cb6d3829ece145a2408cbcb
04/17/2019 12:49:45 PM INFO Record inserted - id: 5cb6d3919ece145a2408cbcc
04/17/2019 12:49:49 PM INFO Record inserted - id: 5cb6d3949ece145a2408cbcd
04/17/2019 12:49:52 PM INFO Record inserted - id: 5cb6d3989ece145a2408cbce

Обратите внимание, что драйверу требуется около 12 секунд, чтобы понять новую топологию, подключиться к новому основному и продолжить запись. Вызвано исключение errors.AutoReconnect, которое является подклассом ConnectionFailure.

Вы можете сделать еще несколько прогонов, чтобы увидеть, какие другие исключения видны. Например, вот еще одна трассировка исключения, с которой я столкнулся:

id = collection.insert_one(doc).inserted_id
  File "C:\Users\Random\AppData\Local\Programs\Python\Python36-32\lib\site-packages\pymongo\collection.py", line 693, in insert_one
    session=session),
...
  File "C:\Users\Randome\AppData\Local\Programs\Python\Python36-32\lib\site-packages\pymongo\network.py", line 150, in command
    parse_write_concern_error=parse_write_concern_error)
  File "C:\Users\Random\AppData\Local\Programs\Python\Python36-32\lib\site-packages\pymongo\helpers.py", line 132, in _check_command_response
    raise NotMasterError(errmsg, response)
pymongo.errors.NotMasterError: not master

Это исключение также является подклассом ConnectionFailure.

Параметр «retryWrites»

Еще одна область для тестирования поведения MongoDB при отказоустойчивости — посмотреть, как другие изменения параметров влияют на результаты. Одним из релевантных параметров является ‘retryWrites’:

retryWrites: (логическое значение) Будут ли поддерживаемые операции записи, выполняемые в этом MongoClient, повторяться один раз после сетевой ошибки в MongoDB 3.6+. По умолчанию False.

Давайте посмотрим, как этот параметр работает с отработкой отказа. Единственное изменение, внесенное в код:

client = pymongo.MongoClient(MONGO_URI, ssl = True, ssl_ca_certs = 'path-to-cacert.pem', retryWrites = True)

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

04/18/2019 08:49:30 PM INFO Attempting to connect...
04/18/2019 08:49:35 PM INFO Record inserted - id: 5cb895869ece146554010c77
04/18/2019 08:49:38 PM INFO Record inserted - id: 5cb8958a9ece146554010c78
04/18/2019 08:49:41 PM INFO Record inserted - id: 5cb8958d9ece146554010c79
04/18/2019 08:49:44 PM INFO Record inserted - id: 5cb895909ece146554010c7a
04/18/2019 08:49:48 PM INFO Record inserted - id: 5cb895939ece146554010c7b <<< Failover around this time
04/18/2019 08:50:04 PM INFO Record inserted - id: 5cb895979ece146554010c7c
04/18/2019 08:50:07 PM INFO Record inserted - id: 5cb895a79ece146554010c7d
04/18/2019 08:50:10 PM INFO Record inserted - id: 5cb895aa9ece146554010c7e
04/18/2019 08:50:14 PM INFO Record inserted - id: 5cb895ad9ece146554010c7f
...

Обратите внимание, что вставка после отработки отказа занимает около 12 секунд, но проходит успешно, так как параметр retryWrites гарантирует повторную попытку записи. Помните, что установка этого параметра не освобождает вас от обработки исключения ConnectionFailure — вам нужно беспокоиться о чтении и других операциях, поведение которых не зависит от этого параметра. Это также не решает проблему полностью, даже для поддерживаемых операций — иногда отработка отказа может занять больше времени, и одной retryWrites будет недостаточно.

Настройка значений времени ожидания сети

rs.stepDown() приводит к довольно быстрому переходу на другой ресурс, поскольку первичный набор реплик получает указание стать вторичным, а вторичные реплики проводят выборы, чтобы определить новый первичный. В производственных развертываниях сетевая нагрузка, разделение и другие подобные проблемы задерживают обнаружение недоступности основного сервера, тем самым увеличивая время отработки отказа. Вы также часто сталкивались с ошибками PyMongo, такими как errors.ServerSelectionTimeoutError, errors.NetworkTimeout и т. д. во время проблем с сетью и отказоустойчивости.

Если это происходит очень часто, вы должны настроить параметры тайм-аута. Связанными параметрами времени ожидания MongoClient являются serverSelectionTimeoutMS, connectTimeoutMS и socketTimeoutMS. Из них выбор большего значения для serverSelectionTimeoutMS чаще всего помогает справляться с ошибками во время отработки отказа:

serverSelectionTimeoutMS: (целое число) определяет, как долго (в миллисекундах) драйвер будет ждать, пока не найдет доступный подходящий сервер для выполнения операции с базой данных; во время ожидания могут выполняться несколько операций мониторинга сервера, каждая из которых контролируется connectTimeoutMS. По умолчанию 30000 (30 секунд).

Готовы использовать MongoDB в своем приложении Python? Прочтите нашу статью Начало работы с Python и MongoDB, чтобы узнать, как приступить к работе всего за 5 простых шагов. ScaleGrid — единственный поставщик MongoDB DBaaS, предоставляющий вам полный SSH-доступ к вашим экземплярам, ​​чтобы вы могли запускать свой сервер Python на том же компьютере, что и ваш сервер MongoDB. Автоматизируйте облачные развертывания MongoDB на AWS, Azure или DigitalOcean с помощью выделенных серверов, высокой доступности и аварийного восстановления, чтобы вы могли сосредоточиться на разработке своего приложения Python.