Статьи

Спасите Обезьяну: Надежно Пишите в MongoDB

Реплика MongoDB устанавливает претензию «автоматическое аварийное переключение», когда основной сервер выходит из строя, и они соответствуют заявке, но обработка аварийного переключения в коде вашего приложения требует некоторой осторожности. Я познакомлю вас с написанием отказоустойчивого приложения на Python с использованием новой функции PyMongo 2.1: ReplicaSetConnection .

Настройка сцены

Мейбл Плавательная Чудо-Обезьяна участвует в ваших передовых исследованиях подводного плавания с обезьяной. Чтобы поддерживать ее жизнь под водой, ваша заявка должна измерить, сколько кислорода она потребляет каждую секунду, и направить такое же количество кислорода в свое снаряжение для подводного плавания. В этом посте я расскажу только о том, как писать в Mongo. Я вернусь к чтению позже.

Настройка MongoDB

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

$ mkdir db0 db1 db2
$ mongod --dbpath db0 --logpath db0/log --pidfilepath db0/pid --port 27017 --replSet foo --fork
$ mongod --dbpath db1 --logpath db1/log --pidfilepath db1/pid --port 27018 --replSet foo --fork
$ mongod --dbpath db2 --logpath db2/log --pidfilepath db2/pid --port 27019 --replSet foo --fork

(Убедитесь, что на этих портах не запущены процессы mongod.)

Теперь подключите узлы в вашем наборе реплик. Имя хоста моей машины — emptysquare.local; замените его своим при запуске примера:

$ hostname
emptysquare.local
$ mongo
> rs.initiate({
  _id: 'foo',
  members: [
    {_id: 0, host:'emptysquare.local:27017'},
    {_id: 1, host:'emptysquare.local:27018'},
    {_id: 2, host:'emptysquare.local:27019'}
  ]
})

 

Первый _id, ‘foo’, должен совпадать с именем, которое вы передали с -replSet в командной строке, иначе Mongo будет жаловаться. Если все правильно, Монго отвечает: «Конфиг теперь сохранен локально. Должен выйти через минуту. Запустите rs.status () несколько раз, пока не увидите, что набор реплик подключен к сети — stateStr первого члена будет «PRIMARY», а stateStrs двух других членов будет «SECONDARY». На моем ноутбуке это занимает около 30 секунд.

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

Определенно писать

Установите PyMongo 2.1 и создайте скрипт Python с именем mabel.py со следующим:

import datetime, random, time
import pymongo

mabel_db = pymongo.ReplicaSetConnection(
    'localhost:27017,localhost:27018,localhost:27019',
    replicaSet='foo'
).mabel

while True:
    time.sleep(1)
    mabel_db.breaths.insert({
        'time': datetime.datetime.utcnow(),
        'oxygen': random.random()
    }, safe=True)

    print 'wrote'

 mabel.py запишет количество кислорода, которое потребляет Мейбл (или, в нашем тесте, случайное количество) и вставит его в Mongo один раз в секунду. Запустить его:

$ python mabel.py
wrote
wrote
wrote

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

$ kill `cat db0/pid`

Возвращаясь к первому окну, все не так хорошо с нашим скриптом Python:

Traceback (most recent call last):
  File "mabel.py", line 10, in <module>
    'oxygen': random.random()
  File "/Users/emptysquare/.virtualenvs/pymongo/mongo-python-driver/pymongo/collection.py", line 310, in insert
    continue_on_error, self.__uuid_subtype), safe)
  File "/Users/emptysquare/.virtualenvs/pymongo/mongo-python-driver/pymongo/replica_set_connection.py", line 738, in _send_message
    raise AutoReconnect(str(why))
pymongo.errors.AutoReconnect: [Errno 61] Connection refused

Это ужасно. WTF случилось с «автоматическим переключением при отказе»? И почему PyMongo вызывает ошибку автоматического переподключения, а не автоматического переподключения?

Ну, автоматический переход на другой ресурс делает работу, в том смысле , что один из второстепенных маховых быстро взять на себя как новый основной. Выполните rs.status () в оболочке Монго, чтобы подтвердить, что:

$ mongo --port 27018 # connect to one of the surviving mongod's
PRIMARY> rs.status()
// edited for readability ...
{
	"set" : "foo",
	"members" : [ {
			"_id" : 0,
			"name" : "emptysquare.local:27017",
			"stateStr" : "(not reachable/healthy)",
			"errmsg" : "socket exception"
		}, {
			"_id" : 1,
			"name" : "emptysquare.local:27018",
			"stateStr" : "PRIMARY"
		}, {
			"_id" : 2,
			"name" : "emptysquare.local:27019",
			"stateStr" : "SECONDARY",
		}
	]
}

 

В зависимости от того, какой mongod был основным, ваш вывод может быть немного другим. Независимо от того , есть это новый первичный, так почему наши записи не получится ? Ответ в том, что PyMongo не пытается повторно вставить ваш документ — он просто сообщает вам, что первая попытка не удалась. Задача вашего приложения — решить, что с этим делать. Чтобы объяснить почему, давайте сделаем небольшое отступление.

Краткое отступление: Обезьяны против котят

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

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

Изо всех сил пытается написать

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

$ mongod --dbpath db0 --logpath db0/log --pidfilepath db0/pid --port 27017 --replSet foo --fork

И обновите mabel.py с помощью следующей бронированной петли:

while True:
    time.sleep(1)
    data = {
        'time': datetime.datetime.utcnow(),
        'oxygen': random.random()
    }

    # Try for five minutes to recover from a failed primary
    for i in range(60):
        try:
            mabel_db.breaths.insert(data, safe=True)
            print 'wrote'
            break # Exit the retry loop
        except pymongo.errors.AutoReconnect, e:
            print 'Warning', e
            time.sleep(5)

 Теперь запустите python mabel.py и снова убейте основной. Сделайте «kill` cat db1 / pid` »или« kill `cat db2 / pid`», в зависимости от того, какой mongod является основным на данный момент. Вывод mabel.py будет выглядеть так:

wrote
Warning [Errno 61] Connection refused
Warning emptysquare.local:27017: [Errno 61] Connection refused, emptysquare.local:27019: [Errno 61] Connection refused, emptysquare.local:27018: [Errno 61] Connection refused
Warning emptysquare.local:27017: not primary, emptysquare.local:27019: [Errno 61] Connection refused, emptysquare.local:27018: not primary
wrote
wrote
.
.
.

mabel.py проходит через несколько этапов скорби, когда основной умирает, но через несколько секунд он находит новый основной, вставляет свои данные и продолжает счастливо.

Как насчет дубликатов?

Оставляя в стороне обезьян и котят, еще одна причина, по которой PyMongo не повторяет автоматически ваши вставки, — это риск дублирования: если первая попытка вызвала ошибку, PyMongo не может знать, произошла ли ошибка до того, как Mongo записал данные или после. Что если мы закончим писать кислородные данные Мейбл дважды? Ну, есть способ, который можно использовать для предотвращения этого: создать идентификатор документа на клиенте.

Всякий раз, когда вы вставляете документ, Mongo проверяет, есть ли у него поле «_id», и если нет, он генерирует ObjectId для него. Но вы можете выбрать идентификатор нового документа перед его вставкой, если идентификатор уникален в коллекции. Вы можете использовать ObjectId или любой другой тип данных. В mabel.py вы можете использовать метку времени в качестве идентификатора документа, но я покажу вам более общеприменимый подход ObjectId:

from pymongo.objectid import ObjectId

while True:
    time.sleep(1)
    data = {
        '_id': ObjectId(),
        'time': datetime.datetime.utcnow(),
        'oxygen': random.random()
    }

    # Try for five minutes to recover from a failed primary
    for i in range(60):
        try:
            mabel_db.breaths.insert(data, safe=True)
            print 'wrote'
            break # Exit the retry loop
        except pymongo.errors.AutoReconnect, e:
            print 'Warning', e
            time.sleep(5)
        except pymongo.error.DuplicateKeyError:
            # It worked the first time
            pass

Мы устанавливаем идентификатор документа для вновь сгенерированного ObjectId в нашем коде Python, прежде чем войти в цикл повторных попыток. Затем, если наша вставка завершится успешно непосредственно перед тем, как основной файл умрет, и мы поймаем исключение AutoReconnect, то в следующий раз, когда мы попытаемся вставить документ, мы поймаем DuplicateKeyError и мы точно будем знать, что вставка прошла успешно. Вы можете использовать эту технику для безопасной, надежной записи в целом.


Библиография

Апокрифическая история о Мейбл, Плавучей чудо-обезьяне

Скорее всего, правда, очень жестокая история о 3 обезьянах, убитых из-за компьютерной ошибки

 

Источник: http://emptysquare.net/blog/save-the-monkey-reliably-writing-to-mongodb/