Alexey Suvorov dev blog

Мой разработческий блог

Archive for the ‘MongoDb’ Category

mongodb: «old lock file, terminating»

with one comment

Короткий пост, про конкретную проблему.

Очень неприятный момент, особенно у нас, где сервера даже у «хорошего» хостера иногда падают от перебоев с питанием, причём падают как будто из них действительно шнур выдёргивали. Работая на Mongo 1.6.5 такой проблемы нет, но перейдя на 1.8.1 однажды утром выяснилось, что база сайта недоступна и сервак жутко тормозит.

В логах было что то вроде
exception in initAndListen std::exception: old lock file, terminating
На деле выяснилось, что Mongo с какой то версии стал создавать mongod.lock файл в директории с данными и при жёстком (без возможности корректно завершить работу сервиса или mongod процесса) выключении он этот файл не удаляет. После загрузки монго проверяет этот файл и видя в нём старые данные повисает в бесконечном цыкле пытаясь запуститься при этом отъедая весь процессор. Проблема решалась переводом сервиса монго в состояние Disabled, удалением lock файла и запуском службы, но это всё руками и вконечно в production такого допускать нельзя.

Проблема решилась относительно просто, выясняя как правильно хостить монго в сервисе, который я уже собрался написать как workaround всплыл такой код:

 if ( oldFile ) {
            // we check this here because we want to see if we can get the lock
            // if we can't, then its probably just another mongod running
            string errmsg;
            if (cmdLine.dur) {
                if (!dur::haveJournalFiles()) {
                    vector<string> dbnames;
                    getDatabaseNames( dbnames );
                    if ( dbnames.size() == 0 ) {
                        // this means that mongod crashed
                        // between initial startup and when journaling was initialized
                        // it is safe to continue
                    }
                    else {
                        errmsg = str::stream()
                            << "************** \n"
                            << "old lock file: " << name << ".  probably means unclean shutdown,\n"
                            << "but there are no journal files to recover.\n"
                            << "this is likely human error or filesystem corruption.\n"
                            << "found " << dbnames.size() << " dbs.\n"
                            << "see: http://dochub.mongodb.org/core/repair for more information\n"
                            << "*************";
                    }
                }
            }
            else {
                errmsg = str::stream()
                         << "************** \n"
                         << "old lock file: " << name << ".  probably means unclean shutdown\n"
                         << "recommend removing file and running --repair\n"
                         << "see: http://dochub.mongodb.org/core/repair for more information\n"
                         << "*************";
            }

            if (!errmsg.empty()) {
                cout << errmsg << endl;
#ifdef WIN32
                CloseHandle( lockFileHandle );
#else
                close ( lockFile );
#endif
                lockFile = 0;
                uassert( 12596 , "old lock file" , 0 );
            }
        }

Отмечу лишь, что лучше сразу идти на http://www.mongodb.org/display/DOCS/Journaling. При установке mongo нужно указать
--journal
что решает проблему со старым lock фалом, но добавляет небольшие (по заверениям разработчиков) накладные расходы при записи + будет тратится время на обработку этих журналов после hard reset, что в моей ситуации более чем приемлимо.

PS: Интересно было посмотреть в сорцы монго,встречаются комментарии

//instance.cpp line 773
/* This ought to be an unlink(), but Eliot says the last
time that was attempted, there was a race condition
with acquirePathLock().  */

Скажет — как отрежет.

Update:
При включении опции —journal и попытке подключить существующие файлы баз данных возможна ошибка:

Tue Apr 19 20:53:58 [dur] lsn set 477451
Tue Apr 19 20:54:23 [initandlisten] connection accepted from 127.0.0.1:1196 #2
Tue Apr 19 20:54:23 [conn2] createPrivateMap failed *:/****/****/*******.* errno:8 Недостаточно памяти для обработки команды.
Tue Apr 19 20:54:23 [conn2] Assertion: 13636:createPrivateMap failed (look in log for error)
Tue Apr 19 20:54:23 [conn2] end connection 127.0.0.1:1196

Written by alexeysuvorov

18.04.2011 at 8:11 пп

Опубликовано в .net, MongoDb

MongoDB C# driver (10gen) — продолжение

2 комментария

После первого поста про mongodb я на несколько недель переключался на f# и вернувшись понял, что всё забыто. Пришлось перечитывать собственный пост, и видимо не одному мне раз даже Dmitry Nesteruk влепил мне like. Лирическое отступление закончилось, дальше будут несколько вещей, которые так или иначе отняли у меня время на расследование как это работает, а иногда и на копание в исходных кодах драйвера. Сразу оговорюсь, в данной статье как и в предыдущей речь пойдёт о драйвере, ссылка на который размещена на сайте монго, это драйвер от 10gen.

Частичная загрузка объектов

Начав переводить один из реальных проектов на Монго я сразу столкнулся с необходимостью загружать объекты без вложенных коллекций, которые он содержит. Надо заметить, что рассматриваемый драйвер работает очень просто. Через классы хелперов и визардов он просто складывает строки чтобы получить работающий запрос на родном для mongo javascript. Чтобы не загружать какое то поле, достаточно в запросе указать field_name:0

  db.users.find({}, {thumbnail:0});

В драйвере всё не так очевидно, но после некоторого копания удалось получить вот такой код:

MongoCollection coll = GetCollection();
FieldsBuilder fbExclude = Fields.Exclude(new string[]{“thumbnail”});
//can be FindAllAs<TEntity>()
MongoCursor result = coll.FindAll().SetFields(fbExclude);

Все результаты в курсоре будут без поля thumbnail, проверено, работает замечательно. Все не загруженные поля будут инициализированы значениями по умолчанию, для коллекций null. Тут есть одна тонкость, если вы не загрузите какое либо поле, а потом запишете этот объект, то он совершенно законно перетрёт старый в базе и вот такой тест упадёт на последнем Assert-е:

public class Data
{
  [BsonId]
  public ObjectId Id {get;set;}
  public int Area {get;set;}
}
Data x = new Data();
x.Area = 20;
var db = GetDb();
var coll = db.GetCollection<Data>(typeof(Data).FullName);
db.ClearColl<Data>();
coll.Save(x);
Data y = coll.FindAllAs<Data>().SetFields(Fields.Exclude(new string[]{"Area"})).FirstOrDefault();
Assert.AreEqual(0, y.Area);
coll.Save(y);
Data z = coll.FindAllAs<Data>().SetFields(Fields.Exclude(new string[] { "Area" })).FirstOrDefault();
//fail of course
Assert.AreEqual(20, z.Area);

Я думаю это как то решается через Find and modify, но пока такой потребности не стоит и всё что нужно менять грузится полностью.

Выбор по элементу вложенного массива

В моём случае была задача выбрать регион на основе почтового кода одной европейской страны. Т.к. по историческим причинам общая логика работала не во всех случаях, то регион должен был определятся по следующему алгоритму: если есть точное совпадение, то выбираем найденный регион, если совпадения нет, то оставляем первые 2 цифры и делаем предположение о регионе на основе диапазона почтовых кодов в регионе, причём самих диапазонов может быть много. Объект выглядит как то так:

public class HighLevelCodeInterval 
{
  public HighLevelCodeInterval() { }
    
  public HighLevelCodeInterval(int mn, int mx) 
  {
    Min = mn; Max = mx;
  }
  public int Min { get; set; }
  public int Max { get; set; }
}
public class RegionObject 
{
  public RegionObject() 
  {
    PostalCodes = new List<int>();
  }
 
  [BsonId]
  public int Id { get; set; }
  public string Name { get; set; }
  public List<HighLevelCodeInterval> HighLevelCodes { get; set; }
  public List<int> PostalCodes { get; set; }
}

Тогда запрос выборки по конкретному коду будет:

QueryComplete q = Query.EQ("PostalCodes", postCode);
MongoCollection coll = GetCollection();
RegionObject result = coll.FindOneAs<RegionObject>();

Я бы сказал неожиданно просто. Для поиска по диапазонам приведу только сам запрос:

int nCodeValue = …;
Query.And(     
     	Query.LTE("HighLevelCodes.Min", nCodeValue),
                	Query.GTE("HighLevelCodes.Max", nCodeValue)
)

Надо признать, что поиска по вложенным массивам в монго предстален очень достойно.

Идентификаторы

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

public class EmployeeIdGenerator : IIdGenerator 
{
  object GenerateId(){ ... }
  bool IsEmpty(object id) { ... }
}
public class Employee 
{ 
  [BsonId(IdGenerator = typeof(EmployeeIdGenerator)]
  public int Id { get; set; }
  // other fields or properties
}

Сам я правда не пробовал.

Заключение

Документо ориентированные базы данных подходят далеко не для всех проектов. Если не получается красиво, то может просто DDD неприменим. Я нашёл что один из моих проектов идеально укладывается в DDD и несказанно рад исчезновению огромного числа таблиц, если всё будет хорошо, то скоро будут тесты производительности в духе MondoDB vs MSSQL на реально работающем приложении.

PS: Хочу упомянуть утилиту , которая очень помогает посмотреть что записалось в базу в результате выполненных операций – это Mongo Vue

Written by alexeysuvorov

11.03.2011 at 11:08 пп

Опубликовано в .net, C#, MongoDb

MongoDB и официальный cssharp-driver

2 комментария

Для MongoDb существует много драйверов, но вот, не так недавно появился официальный. Насколько я понял он разрабатывается компанией http://www.10gen.com и скачать его можно здесь . В этом посте я не буду рассуждать что лучше и зачем вообще нужен MongoDb, я просто пройдусь по основным действиям и моментам, которые показались мне неочевидными при использовании драйвера.

Идентификаторы

У каждого объекта должен быть идентификатор. К нему предъявляется несколько требований: не должен быть null, не должен быть значение по умолчанию (например 0 для int).  Также идентификатор может быть структурой, например:

public struct Pair
{
public int Low{get;set;}
public int High{get;set;}
}

В базе это будет выглядеть так:
MongoDb csharp-driver

Теоретически идентификатором может быть любой объект который может быть сериализован в BsonValue (а это практически любой объект), но в этом случае перед тем как искать по такому объекту его нужно секонвертировать в BsonDocument или BsonValue. Так же есть ObjectId специальный тип в библиотеке MongoDb.Bson, хранит в себе временную метку, идентификатор компьютера на котором создан и какой то pid (возможно идентификатор процесса), если всё равно что использовать в качестве идентификатора, то стоит рассмотреть использование этого типа, потому что сериализуется он на ура и монго сам присвоит ему уникальное значение при сохранении в базу.

Поле идентификатора выбирается для энтити в соответствии с конвенцией или атрибутом. По умолчанию идентификатором будет поле с именем Id если ни одно другое поле не помечено атрибутом [BsonId]. Подробнее про conventions и атрибуты сериализации можно почитать тут

Поиск объекта по идентификатору:

Поиск по простому идентификатору

public class CustomIdEntity 
{
  public string Id { get; set; }
  public string Name { get; set; }
}

var result = demoCollection.FindOneByIdAs<CustomIdEntity>("alexey.suvorov");

Поиск по составному идентификатору

public struct Pair 
{
  public int Low { get; set; }
  public int High { get; set; }
}

public class CustomIdEntity 
{
  [BsonId]
  public Pair ComplexId { get; set; }
  public string Name { get; set; }
  public string Id { get; set; }
}

var result = demoCollection.FindOneByIdAs<CustomIdEntity>((new Pair() { High = 1, Low = 2 }).ToBsonDocument());

Ещё одна особенность MongoDb — как бы не было названо свойство в энтити в базе оно всегда называется _id, возможно это можно изменить с помощью переопределения маппинга, но по умолчанию следующий код работать не будет:

public class DemoEntity 
{
  [BsonId]
  public ObjectId Id { get; set; }
  public string Name { get; set; }
}

[TestMethod]
public void TestInsertDelete() 
{
  var server = new MongoServer(new MongoUrl(ConnectionString));
  var db = server.GetDatabase(TestDatabaseName);
  var demoCollection = db.GetCollection<DemoEntity>(typeof(DemoEntity).FullName);
  const string ConstName = "Some name";

  demoCollection.RemoveAll();
  var de1 = new DemoEntity();
  de1.Name = ConstName;
  demoCollection.Insert<DemoEntity>(de1);
  Assert.IsFalse(de1.Id == ObjectId.Empty);
  //тут всё хорошо, потому что драйвер знает как правильно искать по id 
  var objFromDb = demoCollection.FindOneByIdAs<DemoEntity>(de1.Id);
  Assert.IsNotNull(objFromDb);
  Assert.AreEqual(ConstName, objFromDb.Name);
  //а вот тут вернётся null, не смотря на то, что запрос логически безупречен
  var searchResult = demoCollection.FindOneAs<DemoEntity>(Query.EQ("Id", de1.Id));
  //этот Assert не проходит
  Assert.IsNotNull(searchResult);
}

На самом деле невелика беда, но если нужен метод  RemoveById, то писать его нужно как то так

public static class MongoExtensions 
{
  private const string ID_COLUMN_NAME = "_id";
  public static void RemoveById(this MongoCollection collection, BsonValue val) {
    collection.Remove(Query.EQ(ID_COLUMN_NAME, val));
  }
}

Запросы

Данный драйвер не поддерживает LINQ, либо авторы не почувствовали в себе силы реализовать эту функцию, либо они просто сочли что LINQ «не ложиться» на идею документо ориентированной БД и не вписывается в DDD, вопрос спорный, но я тоже не вижу как и зачем может быть реализован join для документо ориентированной базы данных. Классы запросов находятся в пространстве имён MongoDB.Driver.Builders и я успел попробовать не все классы, но уже кое что потребовало копания в сорцах, чтобы выяснить как заставить это работать.

Начнём с поиска на основе данных во вложенных объектах. В mongo действует dot notation, подробнее тут , пример:

public class ChildChild 
{
  public int SomeVal { get; set; }
}

public class Child 
{
  public int Age { get; set; }
  public ChildChild ChildChildInstance { get; set; }
}

public class Parent 
{
  public ObjectId Id { get; set; }
  public string Name { get; set; }
  public Child ChildInstance { get; set; }
}

[TestMethod]
public void TestQueryToChild() 
{
  var server = new MongoServer(new MongoUrl(ConnectionString));
  var db = server.GetDatabase(TestDatabaseName);
  ClearCollection<Parent>(db);
  const string NameTemplate = "Parent {0}";
  
  var parentCollection = db.GetCollection<Parent>(typeof(Parent).FullName);

  for (int i = 0; i < 10; i++) 
  {
    var cc = new ChildChild() { SomeVal = i };
    var c = new Child() { Age = i, ChildChildInstance = cc};
    var p = new Parent() {
      Name = string.Format(NameTemplate, i),
      ChildInstance = c
    };
    parentCollection.Insert(p);
  }

  //запрос через dot notation
  var resColl = parentCollection.FindAs<Parent>(Query.EQ("ChildInstance.Age", 5));
  Assert.AreEqual(1, resColl.Count());
  Assert.AreEqual(string.Format(NameTemplate, 5), resColl.First().Name);

  //запрос через совпадение объекта
  var childChildDoc = (new ChildChild() { SomeVal = 5 }).ToBsonDocument();
  var resultColl = parentCollection.FindAs<Parent>(Query.EQ("ChildInstance.ChiъldChildInstance", childChildDoc));

  Assert.AreEqual(string.Format(NameTemplate, 5), resultColl.First().Name);
}

Тут есть принципиальная разница между запросами. Запрос через dot notation подразумевает частичное совпадение объекта, т.е. конкретного поля или полей, а вот запрос на основе объекта подразумевает полное совпадение объекта по которому ищем (совпадение бинарных данных после сериализации в BSON).

Соответственно Query.EQ (равно, если кто ещё не понял) возвращает нам CompletedQuery, т.е. не подразумевающий добавления дальнейших условий, тогда как  Query.GT (строго больше) или Query.LTE (меньше равно) возвращают QueryConditionList к которому условия могут быть добавлены, что для меня было не очень очевидно и я сначала пытался сделать проверку на вхождения значения в интервал через Query.And. Правильно сделать:

//постарше, но в рамках 😉
collection.FindAs<Girl>(Query.GT("Age", 25).LT(35));

Вернёмся к LINQ. Все разнообразные Find* методы возвращают MongoCursor;

подробнее о нём тут. По сути это IEnumerable, и к нему подходят стандартные методы LINQtoObject, но они не будут транслированы в запросы на сервере, простой тест:

[TestMethod]
public void TestLinq() 
{
  var server = new MongoServer(new MongoUrl(ConnectionString));
  var db = server.GetDatabase(TestDatabaseName);
  int nCount = 1000000;
  ClearCollection<DemoPerson>(db);

  var collection = db.GetCollection<DemoPerson>(typeof(DemoPerson).FullName);

  for (int i = 0; i < nCount; i++) 
  {
    collection.Insert(new DemoPerson() { Age = i, Name = string.Format("Name {0}",i) });
  }
  var start = DateTime.Now;
  var res = collection.Find(Query.Null).Where(x => x.Age < 99440 && x.Age < 99450);
  Assert.AreEqual(9, res.ToList().Count);
  var end = DateTime.Now;
  
  var start2 = DateTime.Now;
  var res2 = collection.Find(Query.GT("Age", 99440).LT(99450));
  Assert.AreEqual(9, res2.ToList().Count);
  var end2 = DateTime.Now;
  
  //много секунд и памяти
  var diff = end - start;
  //мало секунд и памяти
  var diff2 = end2 - start2;
}

Так что в запросах разбираться всё таки придётся и тут немного примеров, правда они не на C#.

Заключение

За бортом остались индексы, частичная загрузка объектов и map reduce, но я сам пока до этого не добрался. Хочу так же отметить, что Mongo оставляет более приятное впечатление чем тот же RavenDB от которого веет Nhibernate в каждой строчке. Кода не будет, потому что у приведённых примеров нет общей идеи и целосности, если что то не очень понятно я постараюсь оперативно отвечать в комментариях.

Использованные версии
MongoDb: 1.6.5
Driver: 0.9.0.3992

Written by alexeysuvorov

20.12.2010 at 3:12 дп

Опубликовано в .net, C#, MongoDb