Блог - Linux, программирование, Я!

PythonКак правильно распарсить access лог Nginx/Apache

Наверняка у многих возникает такая задача - извлечь какие-то данные из Access лога Apache или Nginx. Записи лога обычно выглядят как то так:

91.77.238.152 - - [02/Dec/2013:20:31:45 +0400] "GET /blog/2012/01/31/plagin-l2tp-dlya-networkmanager/ HTTP/1.1" 200 70666 "http://yandex.ru/clck/jsredir?text=%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD%20l2tp%20%D0%B4%D0%BB%D1%8F%20networkmanager&uuid=..." "Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.1.16) Gecko/20111108 Iceweasel/3.5.16 (like Firefox/3.5.16)"

На первый взгляд кажется, что форматирование этой строки какое-то нелогичное и ничем не оправданное: где то значения заключены в кавычки, где-то в квадратные скобки, где то значение записано как есть. Из за этого кто-то пытается сконструировать огромную нечитаемую регулярку, которую приходится каждый раз писать заново, из за того, что через неделю понять что же она делает нереально. Кто то разбивает строку по пробелам и надеется на лучшее, при этом поле даты разрезается на 2 части а User-Agent превращается вообще в необрабатываемое месиво.

Мы же поступим иначе - попробуем найти систему в структуре этой строки и напишем полноценный лексический анализатор. (далее...)

Программирование[ПЕРЕВОД] Коллбеки императивны, обещания функциональны: крупнейшее упущение NodeJS.

Перевод статьи James Coglan Callbacks are imperative, promises are functional: Node’s biggest missed opportunity

"Главная особенность обещаний в том, что они невосприимчивы к изменяющимся обстоятельствам"
Франк Андервуд, "Карточный домик"

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

Сам термин "функциональное программирование" сбивает с толку тем, что заставляет множество людей считать его "программированием с функциями", как противопоставление программированию с объектами. Но, если объектно-ориентированное программирование рассматривает всё как объект, функциональное программирование рассматривает как значение - не только функции, а абсолютно всё. Это, конечно, включает в себя такие очевидные вещи, как числа, строки, списки и прочие данные, но также некоторые другие вещи, о которых мы, поклонники ООП, обычно не думаем как о значениях: операции ввода-вывода и прочие побочные эфекты, потоки событий GUI, проверки на NULL, даже понятие последовательности вызова функций. Если вы когда-либо слышали фразу "программируемая точка с запятой", то вы понимаете, о чём я говорю.

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

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

Имея такое определение, я хочу пояснить, что, по моему мнению, является основной ошибкой дизайна, внесённой Node.JS: решение, сделаное в самом начале его развития: решение использовать API на основе коллбеков, вместо "обещаний" (promises).

Все используют [коллбеки]. Если вы опубликуете модуль, который возвращает "обещания", никому это не будет интересно. Никто не будет пользоваться им.

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

Для 90% случаев у нас имеется этот супер простой интерфейс, так что когда вам нужно сделать одну вещь, вам достатчно сделать небольшой шажок, и всё готово. Но если у вас сложный случай, то вы пойдёте и установите "async", так же, как и другие 827 модулей, которые зависят от него на npm.

Mikeal Rogers, LXJS 2012

Эта цитата из недавнего выступления Mikeal Rogers, которое охватывает различные аспекты философии дизайна Node.JS:

В свете заявленной цели дизайна Node - сделать разработку быстрых, конкурентных, сетевых программ простой для не-экспертов, я считаю такое отношение контрпродуктивным. "Обещания" позволяют гораздо проще конструировать корректные, максимально конкурентные програмы, делая управление потоком исполнения чем-то, чем будет заниматься рантайм языка, вместо того, чтобы заставлять пользователя реализовывать его самостоятельно.

Написание корректных конкурентных программ обычно сводится к тому, чтобы совершать столько операций параллельно, сколько возможно, и при этом быть уверенным, что операции продолжают выполняться в правильном порядке. Несмотря на то, что JavaScript однопоточен, мы всё ещё можем получить race-condition из за асинхронности: любое действие, которое включает в себя IO, может освободить ресурсы процессора для других действий, пока оно ожидает срабатывания коллбека. Множество конкуррентных действий могут работать с общими данными в памяти, или выполнять перекрывающиеся последовательности команд в базе данных или DOM. Что я надеюсь показать в этой статье, "обещания" предоставляют способ описать прблемы, используя взаимозависимости между значениями, как в Экселе, так что ваши инструменты смогут корректно оптимизировать решение за вас, вместо того, чтобы вам придумывать управление потоками самостоятельно.

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

Для начала, я бы хотел вернуться к статье, которую я написал несколько лет назад, о том, что "обещания" это монады асинхронного программирования. Главный урок той статьи был в том, что монады это инструмент, который помогает вам соединять функции, т.е. строить цепочки, где результаты одних функций становятся входом для других. Это достигается использоваием структурных связей между значениями, и эти значения и их связи вновь будут играть здесь важную роль.

Я собираюсь использовать нотации типов Haskell далее, чтобы проиллюстрировать некоторые вещи. В Haskell запись foo :: bar обозначает " foo является значением типа bar". Запись foo :: Bar -> Qux обозначает " foo является функцией, которая принимает значение типа Bar и возвращает значение типа Qux". Если конкретные типы ввода/вывода функции не важны, мы используем одиночные строчные буквы foo :: a -> b. Если foo принимает несколько аргументов, мы добавляем больше стрелок, т.е. foo :: a -> b -> c означает, что foo принимает два аргумента с типами a и b и возвращает что-то с типом c.

Давайте, для примера, рассмотрим функцию Node.JS fs.readFile(). Она принимает имя файла в виде строки ( String) и коллбек, и не возвращает ничего. Коллбек принимает ошибку ( Error, которая может быть null) и Buffer, в котором находится содержимое файла, и так же не возвращает ничего. Мы можем сказать, что тип функции readFile это:

              
readFile :: String -> Callback -> ()

            

() в нотации Haskell это тип null. Коллбек сам по себе это функция, описание типа которой:

              
Callback :: Error -> Buffer -> ()

            

Совместив всё, мы можем сказать, что readFile принимает String и функцию, которая вызывается с Buffer:

              
readFile :: String -> (Error -> Buffer -> ()) -> ()

            

Теперь, давайте представим, что Node использует "обещания". В этой ситуации, ReadFile будет просто принимать String и возвращать "обещание Buffer"

              
readFile :: String -> Promise Buffer

            

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

              
callback :: a -> (Error -> b -> ()) -> ()
promise :: a -> Promise b

            

Эти null-значения, возвращаемые функциями, использующими коллбеки и есть причина того, что программирование с коллбеками сложное: такие функции ничего не возвращают, из за чего их сложно объединять. Функция, которая ничего не возвращает, вызывается только ради её сайд-эффекта, т.к. функция без сайд-эффекта и без возвращаемого значения - это просто чёрная дыра. Так что, программирование с коллбеками императивно по своей сути, речь идёт об упорядочивании выполнения полных сайд-эффектов процедур, вместо отображения входящих данных в выходящие посредством применения функций. Речь идёт ***!!!???*** о ручном управлении потоком исполнения, вместо решения проблем через взаимосвязь значений. И это именно то, что делает написание корректных конкурентных программ сложным.

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

              
fs.readFile('file1.txt',
  // проходит какое-то время...
  function(error, buffer) {
    // результат начинает существовать
  }
);

            

Получение результата из коллбека или event-based функции как правило означает "быть в правильном месте в нужное время". Если вы добавляете ваш обработчик события после того как событие уже сработало, либо у вас нет нужного обработчика для события, то вам не повезло - вы упустили результат. Такого рода вещи мешают писать HTTP сервра на Node. Если ваш поток управления не настроен как следует, ваша программа ломается.

"Обещания", с другой стороны, не зависят от времени или последовательности. Вы можете добавить обработчиков к "обещанию" как до, так и после того, как оно сработает, и вы сможете получить из него значение. Таким образом, функции, которые возвращают "обещания", сразу дают вам значение, представляющее результат, которое вы можете использовать в качестве значений первого рода и передавать в другие функции. Здесь отсутствуют "ожидание в сторонке", пока отработает коллбек, или какая-то возможность упустить событие. Пока у вас есть ссылка на "обещание", вы можете получить из него значение.

              
var p1 = new Promise();
p1.then(console.log);
p1.resolve(42);

var p2 = new Promise();
p2.resolve(2013);
p2.then(console.log);

// напечатает:
// 42
// 2013

            

Таким образом, хотя имя метода then() подразумевает что-то, связанное с последовательностью операций - и на самом деле это является побочным эффектом её работы ***???*** - вы можете думать о ней, как о функции unwrap() ("развернуть"). "Обещание" - это контейнер для "пока ещё неизвестного значения", и задача then() - извлечь значение из "обещания" и передать его другой функции: это функция bind для монад. Приведённый выше код ничего не говорит о том, когда значение стало доступно, или в каком порядке происходят события, он просто описывает некоторые зависимости: для того, чтобы залоггировать значение, вы должны сперва узнать это значение. Порядок исполнения программы автоматически вытекает из этой информации о зависимости. Это довольно тонкое различие, но мы увидим его более явно, когда будем обсуждать "ленивые обещания" в конце этой статьи.

Пока что все наши примеры были довольно тривиальными; маленькие функции, которые почти не взаимодействуют друг с другом. Чтобы понять, почему "обещания" более мощные, давайте попробуем что-нибудь посложнее. Скажем, у нас есть код, который получает время создания файла множества файлов, используя fs.stat(). Если бы это было синхронным, мы бы просто вызвали paths.map(fs.stat), но, т.к. мэппинг с асинхронными функциями сложен, мы попробуем модуль async.

              
var async = require('async'),
    fs    = require('fs');

var paths = ['file1.txt', 'file2.txt', 'file3.txt'];

async.map(paths, fs.stat, function(error, results) {
  // используем результат
});

            

(Да, я знаю, что у функций модуля fs есть синхронные версии, но большая часть IO не имеют такой опции. Так что подыграйте мне.)

Всё это выглядит хорошо и правильно, пока мы не захотим узнать размер первого файла для некоторой отдельной задачи. Мы можем вызвать для него fs.stat() снова:

              
var paths = ['file1.txt', 'file2.txt', 'file3.txt'];

async.map(paths, fs.stat, function(error, results) {
  // используем результат
});

fs.stat(paths[0], function(error, stat) {
  // используем stat.size
});

            

Это работает, но теперь мы вызываем fs.stat() дважды. Это может быть нормальным для операций с локальными файлами, но если мы загружаем некие большие файлы по HTTPS, это может стать большой проблемой. Мы решаем, что хотим дёрнуть файл только один раз, так что мы откатываемся к предыдущей версии, но обрабатываем первый файл отдельно:

              
var paths = ['file1.txt', 'file2.txt', 'file3.txt'];

async.map(paths, fs.stat, function(error, results) {
  var size = results[0].size;
  // используем size
  // используем остальные результаты
});

            

Это работает, но теперь наша вторая задача (использующая размер первого файла) блокируется в ожидании, пока обработается весь список. И, если возникнет ошибка с любым элементом из списка, мы всё равно не получим результат для первого элемента. Это не хорошо, так что мы пробуем следующий подход: мы отделяем первый файл от общего списка и обрабатываем его отдельно:

              
var paths = ['file1.txt', 'file2.txt', 'file3.txt'],
    file1 = paths.shift();

fs.stat(file1, function(error, stat) {
  // используем stat.size
  async.map(paths, fs.stat, function(error, results) {
    results.unshift(stat);
    // используем результат
  });
});

            

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

Ок, последний рывок к успеху. Мы знаем, что мы хотим получить stat для всех файлов, дёргая каждый файл только один раз, проделать какую-то работу над первым результатом, если он будет успешно получен и, если целый список так же будет успешно обработан, мы хотим проделать какую-то работу для этих результатов. Имея эти знания о зависимостях, попробуем выразить всё, используя async.

              
var paths = ['file1.txt', 'file2.txt', 'file3.txt'],
    file1 = paths.shift();

async.parallel([
  function(callback) {
    fs.stat(file1, function(error, stat) {
      // используем stat.size
      callback(error, stat);
    });
  },
  function(callback) {
    async.map(paths, fs.stat, callback);
  }
], function(error, results) {
  var stats = [results[0]].concat(results[1]);
  // используем stats
});

            

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

Ну, не совсем. Я уверен, что всё это выглядит весьма уродливо и это, несомненно, не масштабируется в случае, если проблема станет более сложной. Было проделано много работы, чтобы просто сделать работу программы корректной, намерения такого дизайна становятся неочевидными, так что дальнейшая поддержка скорее всего что-то сломает, последующие задачи смешиваются со стратегией того, как продолжать выполнять требования ***???***, и мы по прежнему имеем некоторые лишние перемешивания массива, чтобы сгладить последствия добавленного нами специального случая. Фу!

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

Но как же "обещания" могут исправить ситуацию? Ну, прежде всего нам нужны функции для работы с файловой системой, которые будут возвращать "обещания", вместо того, чтобы принимать коллбеки. Но, вместо того, чтобы писать такие функции руками, попробуем использовать мета-программирование для того, чтобы сконвертировать для нас таким образом любую функцию ***!!!***. Например, пусть оно принимает функцию с типом

              
String -> (Error -> Stat -> ()) -> ()

            

и возвращает другую функцию с типом

              
String -> Promise Stat

            

Вот такая функция:

              
// promisify :: (a -> (Error -> b -> ()) -> ()) -> (a -> Promise b)
var promisify = function(fn, receiver) {
  return function() {
    var slice   = Array.prototype.slice,
        args    = slice.call(arguments, 0, fn.length - 1),
        promise = new Promise();
    
    args.push(function() {
      var results = slice.call(arguments),
          error   = results.shift();
      
      if (error) promise.reject(error);
      else promise.resolve.apply(promise, results);
    });
    
    fn.apply(receiver, args);
    return promise;
  };
};

            

(Это решение не совсем общее, но оно будет работать для нашего случая.)

Теперь мы можем заново смоделировать нашу проблему. Всё, что мы делаем теперь - просто маппинг списка имён файлов на список "обещаний" stat:

              
var fs_stat = promisify(fs.stat);

var paths = ['file1.txt', 'file2.txt', 'file3.txt'];

// [String] -> [Promise Stat]
var statsPromises = paths.map(fs_stat);

            

Такой подход приносит первые плоды сразу же: если с использованием async.map() у вас нет данных для обработки, до тех пор, пока не будет обработан весь список, то имея список "обещаний" вы можете просто взять первый элемент и работать с ним:

              
statsPromises[0].then(function(stat) { /* используем stat.size */ });

            

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

Неодстающая часть нашего решения - как поступить, когда нам нужен список всех результатов stat. В предыдущих попытках мы заканчивали на том, что у нас есть список всех объектов Stat, но сейчас у нас есть только список объектов Promise Stat. Мы хотим подождать, пока все "обещания" сработают, и затем вернуть список всех Stat объектов. Другими словами, мы хотим превратить список "обещаний" в "обещание списка".

Давайте сделаем это, просто расширив массивы методами "обещаний", так, что список обещаний сам становится "обещанием", которое сработает тогда, когда сработают все его элементы.

              
// list :: [Promise a] -> Promise [a]
var list = function(promises) {
  var listPromise = new Promise();
  for (var k in listPromise) promises[k] = listPromise[k];
  
  var results = [], done = 0;
  
  promises.forEach(function(promise, i) {
    promise.then(function(result) {
      results[i] = result;
      done += 1;
      if (done === promises.length) promises.resolve(results);
    }, function(error) {
      promises.reject(error);
    });
  });
  
  if (promises.length === 0) promises.resolve(results);
  return promises;
};

            

(Эта функция похожа на функцию jQuery.when(), которая принимает список обещаний и возвращает новое обещание, которое срабатывает тогда, когда срабатывают все входящие аргументы.)

Теперь мы можем подождать все результаты, просто обернув наш массив в "обещание":

              
list(statsPromises).then(function(stats) { /* используем stats */ });

            

Таким образом, наше решение целиком уменьшилось до:

              
var fs_stat = promisify(fs.stat);

var paths = ['file1.txt', 'file2.txt', 'file3.txt'],
    statsPromises = list(paths.map(fs_stat));

statsPromises[0].then(function(stat) {
  // используем stat.size
});

statsPromises.then(function(stats) {
  // используем stats
});

            

Это описание решения, несомненно, более ясное. Используя немного обобщённого "клея" (наши функции-хелперы для "обещаний") и уже существующие методы массивов, мы решили нашу проблему корректным, эффективным методом, к тому же программа легко поддаётся изменениям. Нам не нужны специализированные методы и коллекции модуля async, мы просто берём независимые идеи "обещаний" и массивов и комбинируем их крайне эффективным способом.

Кстати, обратите внимание, что эта пргорамма ничего не говорит о том, что будет выполняться параллельно или последовательно. Она просто говорит что мы хотим получить, и от чего это зависит. А всеми оптимизациями занимается библиотека "обещаний".

По факту, многие операции с коллекциями модуля async могут быть с лёгкостью заменены операциями над списками обещаний. Мы уже видели как работает map; следующий код:

              
async.map(inputs, fn, function(error, results) {});

            

эквивалентен коду:

              
list(inputs.map(promisify(fn))).then(
    function(results) {},
    function(error) {}
);

            

async.each() это просто async.map(), но вы выполняете функции только ради их сайд-эффектов и просто выбрасываете возвращаемые значения; вы можете использовать map() вместо него.

async.mapSeries() (и, по аналогии с предыдущим примером, async.eachSeries()) это эквивалент выполнения reduce над списком "обещаний". Так и есть, вы берёте ваш список входных значений, и вызываете reduce, чтобы получить обещание, в котором каждое действие зависит от от успешности предыдущего. Давайте рассмотрим пример: реализуем эквивалент rm -rf на основе fs.rmdir(). Этот код:

              
var dirs = ['a/b/c', 'a/b', 'a'];
async.mapSeries(dirs, fs.rmdir, function(error) {});

            

эквивалентен следующему:

              
var dirs     = ['a/b/c', 'a/b', 'a'],
    fs_rmdir = promisify(fs.rmdir);

var rm_rf = dirs.reduce(function(promise, path) {
  return promise.then(function() { return fs_rmdir(path) });
}, unit());

rm_rf.then(
    function() {},
    function(error) {}
);

            

Где unit() представляет собой функцию, которая возвращает уже сработавшее "обещание", необходимое для того, чтобы начать цепочку (если вы понимаете монады, это функция return для обещаний):

              
// unit :: a -> Promise a
var unit = function(a) {
  var promise = new Promise();
  promise.resolve(a);
  return promise;
};

            

Этот reduce() подход просто берёт каждую пару последовательных путей в списке и использует promise.then(), чтобы сделать удаление пути зависимым от успешности пердыдущего шага. Оно обрабатывает удаление непустых директорий за вас: если предыдущее "обещание" было отменено из за такого рода ошибки, цепочка просто обрывается. Использование зависимости от значений для управления порядком исполнения - это основная идея того, как функциональные языки используют монады для обработки сайд-эффектов.

Этот последний пример несколько более многословный, по сравнению с кодом на async, но пусть это вас не смущает. Основная идея в том, что мы объединили отдельные идеи: "обещания" и операции с массивами, вместо того, чтобы полагаться на отдельные библиотеки управления потоком исполнения. Как мы видели ранее, этот подход позволяет писать более лёгкие для восприятия программы.

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

* A. Задачи в этой программе зависят друг от друга так то,
* B. В связи с этим, операции должны быть упорядочены вот так
* C. В связи с этим, давайте напишем код, чтобы описать B.

Использование графов зависимостей "обещаний" позволяет вам пропустить шаг B целиком. Вы пишете код, который описывает зависимости задач и позволяете компьютеру управлять потоком исполнения. Иначе говоря, коллбеки используют явный контроль потока исполнения, для того чтобы объединить множество маленьких значений, тогда как "обещания" используют явные зависимости между значениями, чтобы собирать вместе маленькие кусочки потока исполнения. Коллбеки импервтивны, "обещания" функциональны.

Обсуждение этой темы было бы неполным без одного финального применения "обещаний" и ключевой идеи функционального программирования: ленивости. Haskell - ленивый язык, что означает, что вместо рассмотрения вашей программы как скрипта, который исполняется сверху-вниз, он начинает с выражения, которое описывает вывод (ответ) программы - что программа пишет в stdio, базы данных и т.п. - и работает в обратном порядке. Он смотрит от каких выражений зависит это финальное выражение и двигается по этому графу в обратном порядке до тех пор, пока не вычислит всё, что нужно программе для того, чтобы сгенерировать вывод (ответ). Вычисления происходят только в том случае, когда они действительно нужны программе для того, чтобы выполнить свою работу.

Зачастую, лучшее решение какой-либо проблемы в computer science состоит в том, чтобы найти правильную структуру данных, моделирующую эту проблему. И у JavaScript имеется одна проблема очень похожая на тольо что мной описанную: загрузка модулей. Вы хотите загружать только те модули, которые действительно нужны вашей программе, и вы хотите делать это как можно эффективнее.

До того, как у нас появились CommonJS и AMD, котрые фактически являются способами управления зависимостями ***???***, у нас была кучка библиотек загрузки скриптов. Они в основном работали так же, как наш пример выше, где вы явно указываете, какие файлы могут быть загружены параллельно, а какие должны быть упорядочены. Вы, в основном, должны были изложить стратегию загрузки, что значительно труднее сделать корректно и эффективно, в отличие от того, чтобы просто описать зависимости между скриптами и позволить загрузчику оптимизировать всё за вас.

Давайте введём понятие LazyPromise. Это объект "обещание", который содержит функцию, которая выполняет какую то, возможно асинхронную, работу. Функция выполняется только один раз, когда кто-то вызывает метод then() "обещания": мы начинаем её выполнять только тогда, когда кому-то понадобится её результат. Это достигается переопределением then() таким образом, чтобы она начинала выполнять работу, если та ещё не стартовала.

              
var Promise = require('rsvp').Promise,
    util    = require('util');

var LazyPromise = function(factory) {
  this._factory = factory;
  this._started = false;
};
util.inherits(LazyPromise, Promise);

LazyPromise.prototype.then = function() {
  if (!this._started) {
    this._started = true;
    var self = this;
    
    this._factory(function(error, result) {
      if (error) self.reject(error);
      else self.resolve(result);
    });
  }
  return Promise.prototype.then.apply(this, arguments);
};

            

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

              
var delayed = new LazyPromise(function(callback) {
  console.log('Started');
  setTimeout(function() {
    console.log('Done');
    callback(null, 42);
  }, 1000);
});

            

Но если мы добавим следующую строук, то программа напечатает Started, подождёт секунду, после чего напечатает Done и 42.

              
delayed.then(console.log);

            

И, т.к. работа будет проделана только один раз, многократный вызов then() выдаёт результат каждый раз но не проделывает работу заново:

              
delayed.then(console.log);
delayed.then(console.log);
delayed.then(console.log);

// печатает:
// Started
// -- ждёт 1 секунду --
// Done
// 42
// 42
// 42

            

Используя эту крайне простую обобщённую абстракцию, мы можем построить оптимизирующую модульную структуру за минимальное время. Представьте, что мы хотим сделать кучу вот таких модулей: каждый модуль создаётся с именем, списком модулей, от которых он зависит и фабрикой, которая, если её вызвать со списком загруженных зависимостей, возвращает API модуля. Это очень похоже на то, как работает AMD.

              
// файл a.js
var A = new Module('A', [], function() {
  return {
    logBase: function(x, y) {
      return Math.log(x) / Math.log(y);
    }
  };
});

// файл b.js
var B = new Module('B', [A], function(a) {
  return {
    doMath: function(x, y) {
      return 'B result is: ' + a.logBase(x, y);
    }
  };
});

// файл c.js
var C = new Module('C', [A], function(a) {
  return {
    doMath: function(x, y) {
      return 'C result is: ' + a.logBase(y, x);
    }
  };
});

// файл d.js
var D = new Module('D', [B, C], function(b, c) {
  return {
    run: function(x, y) {
      console.log(b.doMath(x, y));
      console.log(c.doMath(x, y));
    }
  };
});

            

У нас получился ромб: D зависит от B и C, каждый из которых зависит от A. Это означает, что мы можем загрузить A, затем параллельно B и C. Далее, когда они все загрузятся, мы можем загружать D. Но мы хотим, чтобы наша утилита сделала всё за нас, вместо того, чтобы описывать эту стратегию самостоятельно.

Мы легко можем сделать это, смоделировав модуль как подтип LazyPromise. Его конструктор просто спрашивает значения своих зависимостей используя написанный ранее хелпер list, затем, после таймаута, который симулирует задержку на асинхронную загрузку, создаёт модуль с этими зависимостями.

              
var DELAY = 1000;

var Module = function(name, deps, factory) {
  this._factory = function(callback) {
    list(deps).then(function(apis) {
      console.log('-- module LOAD: ' + name);
      setTimeout(function() {
        console.log('-- module done: ' + name);
        var api = factory.apply(this, apis);
        callback(null, api);
      }, DELAY);
    });
  };
};
util.inherits(Module, LazyPromise);

            

Т.к. Module это LazyPromise, простое их определение, как в примере выше, не загружает ни один из них. Мы начинаем загружать их только тогда, когда пытаемся ими воспользоваться:

              
D.then(function(d) { d.run(1000, 2) });

// напечатает:
// 
// -- module LOAD: A
// ..ждём 1 секунду..
// -- module done: A
// -- module LOAD: B
// -- module LOAD: C
// ..ждём 1 секунду..
// -- module done: B
// -- module done: C
// -- module LOAD: D
// ..ждём 1 секунду..
// -- module done: D
// B result is: 9.965784284662087
// C result is: 0.10034333188799373

            

Как видим, A загружается первым, затем, когда загрузка завершится, B и C начинают загружаться одновременно, и, после того, как оба они загрузятся, загружается D - в точности как мы и хотели. Если вы попробуете вызвать просто C.then(function() {}), то увидите, что загрузятся только A и C; модули, которые не находятся в графе необходимых нам, не загружаются.

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

Такова реальная сила "обещаний". Они не просто способ избежать пирамиды отступов на уровне синтаксиса. Они предоставляют вам абстракцию, которая позволяет вам моделировать проблемы на более высоком уровне и перекладывать больше работы на ваши инструменты. И, на самом деле, это именно то, что все мы должны требовать от нашего ПО. Если Node действительно серьёзно хочет сделать конкуррентное программирование простым, они должны дать обещаниям второй шанс.

James Coglan Callbacks are imperative, promises are functional: Node’s biggest missed opportunity

ПрограммированиеБенчмарк HTML парсеров

Переписывал на работе кусок одного сервиса с Python на Erlang. Сам сервис занимается тем, что скачивает по HTTP значительное количество однотипных HTML страниц и извлекает из них некоторую информацию. Основная CPU нагрузка сервиса приходится на парсинг HTML в DOM дерево.

Сперва захотелось сравнить производительность Erlang парсера mochiweb_html с используемым из Python lxml.etree.HTML(). Провел простейший бенчмарк, нужные выводы сделал, а потом подумал что неплохо было бы добавить в бенчмарк ещё парочку-другую парсеров и платформ, оформить покрасивее, опубликовать код и написать статью.
На данный момент успел написать бенчмарки на Erlang, Python, PyPy, NodeJS и С в следующих комбинациях:

  • Erlang - mochiweb_html
  • CPython - lxml.etree.HTML
  • CPython - BeautifulSoup 3
  • CPython - BeautifulSoup 4
  • CPython - html5lib
  • PyPy - BeautifulSoup 3
  • PyPy - BeautifulSoup 4
  • PyPy - html5lib
  • Node.JS - cheerio
  • Node.JS - htmlparser
  • Node.JS - jsdom
  • C - libxml2 (скорее для справки)

В тесте сравниваются скорость обработки N итераций парсера и пиковое потребление памяти.

Интрига: кто быстрее - Python или PyPy? Как сказывается иммутабельность Erlang на скорости парсинга и потреблении памяти? Насколько быстра V8 NodeJS? И как на всё это смотрит код на чистом C. (далее...)

linux на сервереКак удалить миллионы файлов из одной папки

Очередной пост-ответ на статью на хабре. Прочитал статью Необычное переполнение жесткого диска или как удалить миллионы файлов из одной папки и очень удивился. Неужели в стандартном инструментарии Linux нет простых средств для работы с переполненными директориями и необходимо прибегать к столь низкоуровневым способам, как вызов getdents() напрямую.

Для тех, кто не в курсе проблемы, краткое описание: если вы случайно создали в одной директории огромное количество файлов без иерархии - т.е. от 5 млн файлов, лежащих в одной единственной директории, то быстро удалить их не получится. Кроме того, не все утилиты в linux могут это сделать в принципе - либо будут сильно нагружать процессор/HDD, либо займут очень много памяти.

Так что я выделил время, организовал тестовый полигон и попробовал различные средства, как предложенные в комментариях, так и найденные в различных статьях и свои собственные.
Своего рода, расстановка точек над i в вопросе удаления файлов из переполненной директории. (далее...)

PythonСамая короткая запись асинхронных вызовов в tornado v2, или патчим Python AST

Меня очень заинтересовала статья Самая короткая запись асинхронных вызовов в tornado или патчим байткод в декораторе, не столько с практической точки зрения, сколько с точки зрения реализации.
Всё-таки модификация байткода в рантайме это слишком опасная и ненадежная операция. И уж наверняка не поддерживаемая альтернативными интерпретаторами Python.

Попробуем исправить этот недостаток способом, который для этого куда больше предназначен и который применяется для схожих целей во многих других языках (я точно встречал в Lisp и Erlang). Этот способ - модификация Абстрактного синтаксического дерева (AST) программы.
(далее...)

PythonРаздача больших файлов через сервер TornadoWEB

TornadoWeb - это легковесный веб-сервер, написанный на Python и использующий неблокирующие сокеты и epool/kqueue для работы с сетью.Сам по себе сервер неплохой, довольно популярный. Привлекает своим крайне простым API (по сравнению с тем-же Twisted) и высокой производительностью.

Первый вопрос, который может возникнуть - "зачем раздавать большие файлы через Tornado?" Ведь даже официальная документация рекомендует использовать для этого Nginx. Что-ж, в большинстве случаев так и есть. Но ситуации могут быть разные. В моём случае через Tornado успешно раздавалось большое количество мелких страничек, хранящихся в SQLite и один большой файл на 200МБ со списком всех доступных URL. Поднимать ради одного этого файла Nginx совершенно не хотелось.

Второй вопрос - "ну так в чем проблема - раздавай на здоровье!". Вот тут мы и сталкиваемся с неприятной особенностью этого сервера - стандартный StaticFileHandlerзагружает весь файл целиком в память перед тем как отдать его клиенту ( пруфлинк). Помимо этого, занятая память не освобождается, если клиент разорвал подключение не скачав весь файл целиком.

Вот эти 2 проблемы и будем решать. (далее...)

ОбщекомпьютерноеНа стачку! Впечатления и обзор конференции. День 2.

Вечер прогулок по городу, ночь в гостинице и вот он - второй день конференции. Решил доклады не пропускать (не зря же 12 часов в поезде ехал!) и пошел сразу на первый доклад.

(далее...)

ОбщекомпьютерноеНа стачку! Впечатления и обзор конференции. День 1.

Давненько не бывал на конференциях, наконец-то удалось совместить наличие хорошей конференции с возможностью на нее пойти. И так, 13 и 14-го апреля в г. Ульяновск проходила конференция "Стачка!" на которой я имел удовольствие поприсутствовать. Сразу отмечу, что несмотря на то, что конференция была бесплатной, уровень организации и качество докладов были на уровне вполне известных платных конференций. Выбрали отличное живописное место, организовали онлайн трансляцию (на сайте выложены архивы с видео докладов). Спасибо организаторам, постарались на славу!

Теперь расскажу о докладах (в общем-то краткий пересказ докладов, много букв! Если нужны подробности - смотрите видео на сайте). (далее...)

linux на десктопеПлагин L2TP для NetworkManager

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

TL;DR: теперь подключаться к L2TP VPN можно через NetworkManager.

Допилил сегодня плагин для Network Manager с поддержкой L2TP. Поддерживает VPN через L2TP и, теоретически, L2TP через IPSec.
Я проверял на "beeline домашний интернет" ( http://help.internet.beeline.ru/internet/install/windows7/l2tp) и в общем-то для этого его и взялся дорабатывать. В принципе, можно даже озаглавить статью типа " Настройка beeline l2tp vpn через NetworkManager".

Установка

Можно установить из PPA:
https://launchpad.net/~seriy-pr/+archive/network-manager-l2tp

              sudo apt-add-repository ppa:seriy-pr/network-manager-l2tp
sudo apt-get update
sudo apt-get install network-manager-l2tp-gnome
            

* Если первая команда не сработает, нужно сперва сделать sudo apt-get install python-software-properties
После установки настоятельно рекомендую выполнить

              sudo service xl2tpd stop
sudo update-rc.d xl2tpd disable
            

После этого можно будет создать новое VPN подключение "Layer 2 Tunnelling Protocol (L2TP)" (см. скриншот).
Напомню как:

  1. Кликаем по иконке NM в трее
  2. Выбираем "Изменить соединения"
  3. Открываем вкладку VPN
  4. Жмем "Добавить"
  5. В выпадающем списке выбираем "Layer 2 Tunnelling Protocol (L2TP)" жмем создать
  6. Дальше уже заполняем окно с настройками для нового соединения

Можно сразу попасть на 5-й пункт если запустить в консоли nm-connection-editor --type=vpn --create

Если что-то не работает

Можете пожаловаться мне. Делать это следует так:
Сперва выполняем команды lsb_release -a и uname -a и отправляем мне что они напечатают.
В идеале прикладывайте еще ссылку на инструкцию вашего провайдера по настройке VPN для Windows.
Дальше в зависимости от того какая возникла проблема:

Не устанавливается пакет

Присылайте вывод команды
apt-cache policy network-manager-l2tp
Ну и какую ошибку пишет при установке.

Не получается Добавить новое VPN подключение

Имеется в виду что пакет установился, но при попытке создать и настроить новое VPN подключение возникают проблемы (нет пункта "Layer 2 Tunnelling Protocol (L2TP)" или программа вылетает или не сохраняются настройки).
В консоли запускаете команду
nm-connection-editor --type=vpn --create
И пробуете добавить и настроить новое подключение. Присылайте что напечатает в консоль и опишите саму проблему конечно же.

При попытке подключения происходят ошибки

Открываете 2 консоли, в одной запускаете

              sudo /usr/lib/NetworkManager/nm-l2tp-service --debug
            

в другой

              tail -f -n 0 /var/log/syslog
            

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

На компьютере, куда я хочу установить плагин нет интернета.

Есть скрипт на Python, который умеет выкачивать deb пакеты с плагином и всеми зависимостями.
На компьютере с интернетом окрываете эту ссылку https://gist.github.com/3029495, копируете код в файл, называете его, например, l2tp-downloader.py.
Далее, если компьютер с интернетом на Linux, то просто запускаете этот файл в консоли командой python l2tp-downloader.py и отвечаете на вопросы.
Если компьютер под Windows, нужно скачать и установить Python 2.7 с сайта http://python.org/ и с его помощью запустить скрипт.
После того как скрипт отработает, он создаст .tar архив, в котором будут лежать все зависимости и файл README с инструкцией по установке.

Работает ли плагин на KDE?

Плагин состоит из 2-х частей: независимый от интерфейса "бэкенд" и интерфейс для Gnome. Интерфейса под KDE я не делал. Но независимые разработчики недавно выпустили универсальный интерфейс для KDE панели plasma [ 1][ 2]. Проект новый, так что присутствует пока не во всех дистрибутивах, но в 2014 году должен постепенно появиться (запланирован в Ubuntu 14.04, Fedora 20). Пакет в репозиториях называется plasma-nm. Если plasma-nm у вас установлен, всё равно нужно "доставить" бэкенд-часть, например так:

              sudo apt-add-repository ppa:seriy-pr/network-manager-l2tp
sudo apt-get update
sudo apt-get install network-manager-l2tp
            

Т.е. нужно установить пакет network-manager-l2tp (не network-manager-l2tp-gnome!!!).

Альтернативные GUI программы для подключения к L2TP VPN в Linux

Если этот плагин почему то так у вас и не заработал, можете попробовать одну из следующих GUI-программ (к слову, все эти L2TP-GUI, в т.ч. и мой плагин, "внутри себя" используют одни и те же консольные программы: xl2tpd (для L2TP), pppd (для VPN) и openswan (для IPSec), но генерируют к ним немного разные конфиги и запускают их немного по разному).

vpnpptp

Официальный сайт vpnpptp. Программа написана на Pascal и Bash с использованием Qt. Из плюсов - много настроек, разрабатывается нашими соотечественниками, оптимизирована для подключения к beeline. Из минусов, пожалуй, довольно нестабильная работа, необходимость вводить root-пароль для подключения и не очень удобный интерфейс. (Ну и еще сам код программы жуткий).

L2tp-IPsec-VPN

Сайт, Wiki. Написана на C++ и Qt. Из плюсов - очень хорошая поддержка IPsec, неплохой интерфейс, можно подключаться без root пароля, активно разрабатывается, дружелюбный разработчик =). Из минусов - опять же, недостаточно стабильная работа, поддержку работы БЕЗ IPSec (нужно для работы с beeline) добавили совсем недавно (по моей просьбе), но заставить его работать с beeline так и не получилось.

Эпилог

Разработку веду на Github https://github.com/seriyps/NetworkManager-l2tp.
Пожелания, багрепорты, комментарии, благодарности приветствуются.

Тикеты с просьбой добавить поддержку L2TP в NM появились на Launchpad и Gnome в сентябре 2008г. Изначально плагин разработал Alexey Torkhov, но затем прекратил его поддержку.

PythonИспользование Django ORM отдельно от Django

Понадобилось использовать Django ORM вне контекста самой Django. Конечно, проще и правильнее было бы использовать SQLAlchemy т.к. она рассчитана на такое использование, но мне нужно было написать паука, сохраняющего данные в БД и веб-интерфейс к этим данным. Веб-интерфейс точно решил делать на Django, а в пауке использовать те-же самые модели, чтобы не проделывать одну и ту же работу дважды. Т.к. паук был первым этапом работ а админка - вторым, появилась необходимость создавать таблицы из моделей без использования "syncdb".

В качестве основы использовалкод модуля django-standalone истатью Django ORM без Джанги -эта статья практически полностью решает проблему, но есть и несколько недостатков:

  • Модели для создания таблиц нужно перечислять вручную
  • Не создаются внешние ключи
  • По - мелочи - нужно каждой модели вручную задавать app_label

Попытаемся исправить эти недостатки.

(далее...)

PythonПлагин Vkontakte.ru для Gwibber

Вступление

Gwibber + VkontakteНе так давно решил вплотную познакомиться со всей кухней OpenSource со стороны разработчика и в качестве первой пробы решил написать плагин к Gwibber для поддержки Вконтакте. Надо сказать, что тикет на Launchpad на эту тему висит с середины 2010 года (датирован 2010-05-01) с довольно длинным обсуждением, было несколько попыток реализовать поддержку, но готового решения никто не предложил. Помимо Launchpad, появился топик на форуме Ubuntu.ru, в котором топикстартер сообщил, что реализовал поддержку ВК, но код так никому и не показал.

Так что в начале декабря взялся за работу и через 2 месяца рабочая протестированная версия плагина была готова. На данный момент я уже отправил запрос о включении плагина в официальную ветку Gwibber и буквально сегодня разработчики отметили его для включения в релиз 2.91.5.
Описание функционала, инструкция для желающих протестировать плагин в работе уже сейчас под катом... (далее...)

ОбщекомпьютерноеОтчет о конференции ADD-2010 в Ярославле

Побывал 23-24 сентября на конференции Application developer days в Ярославле. Вот решил написать краткий отчет. (далее...)

ПрограммированиеИнтеграция Redmine и Eclipse

Вступление

Логотипы eclipse и redmineБольшую часть времени разработчики проводят в двух местах - в IDE и в багтрекере/системе управления задачами (ну и, естественно, за гуглением/чтением всяких блогов, литературы etc). При этом, как правило, работая над конкретной задачей, разработчик использует весьма небольшое количество файлов проекта и возможностей IDE. Таким образом, все остальное окружение, не затронутое данной конкретной задачей, только отвлекает и сбивает с толку. Помочь решить эту проблему призванзамечательный плагин для не менее замечательной IDE Eclipse под названием mylyn. (далее...)

linux на сервереBasic HTTP авторизация для Nginx

В принципе это тривиальная операция описанная в документации к Nginx, но немного усложняет этот процесс необходимость создавать htpasswd файл, причем в самом Nginx средств для его создания нет (и это правильно), но предлагается создавать его используя утилиту htpasswd входящую в состав Apache. Что самое смешное, на многих серверах, где используется Nginx, апача нет и не ожидается. Более того, большинство администраторов хостингов, "познавших" для себя Nginx начинают нескрываемо недолюбливать апач и верят, что наступит тот час, когда для Nginx наконец сделают аналог htaccess и апач, исправно служивший им долгие годы, наконец-то сгинет в небытие. Чтобы исправить сей досадный факт, добавлю в статью онлайн-формочку для генерации htpasswd фйла.
(далее...)

linux на сервереНастраиваем девелоперский DNS сервер

Рассмотрим эволюцию организации работы веб-разработчика с несколькими проектами на разных адресах и поможем поскорее перескочить через первые ступеньки!

Все на localhost

Когда ты в одиночку разрабатываешь один единственный веб-сервис или сетевое приложение, то, как правило, вполне хватает локального домена localhost. Но что происходит, когда появляется необходимость разрабатывать и поддерживать одновременно два или более совершенно разных приложения? Сперва ты пытаешься рассовывать разные приложения по подпапкам одного домена вроде http://localhost/app1/ http://localhost/app2/ и т.д. Ок, при более-менее простых приложениях такой вариант еще может работать. Но когда возникает необходимость в привязке приложения к корню сайта в относительных внутренних ссылках, когда необходимы специфические настройки веб-сервера (например специфические реврайт-правила), то сложность и запутанность полученной системы крайне резко возрастает, начинают появляться различные баги и замедляется процесс разработки.

Файл hosts

Позже, изрядно помучавшись с однодоменным расположением проектов, вдруг узнаешь про существование волшебного файлика hosts ( /etc/hosts) с помошью которого можно создавать для себя дополнительные "локалхосты", т.е. получаем возможность любое конкретное доменное имя направить на любой конкретный IP адрес. Отлично! Теперь можно разнести все свои "локалхосты" по отдельным "поддоменам" типа app1.loc app2.loc просто прописав в /etc/hosts ( WINDOWS\system32\drivers\etc\hosts для виндов) всего пару строк
127.0.0.1 app1.loc
127.0.0.1 app2.loc
Выглядит просто и понятно, все записи под рукой, можно легко добавить и удалить нужный домен, изменения, как правило, вступают в силу мгновенно. В принципе этим можно ограничиться если у тебя не так много проектов, меняются они нечасто и ты работаешь над ними неторопясь и ОДИН. Но есть у такого подхода 2 существенных недостатка:

  1. Для того чтобы к имени app1.loc добавить поддомен, например static.app1.loc допустим, для хранения статических файлов, приходится создавать в файле hosts дополнительную запись типа 127.0.0.1static.app2.loc т.к. hosts файл не поддерживает wildcard формы записи - прописать в нем 127.0.0.1*.app1.loc нельзя!
    Такую ситуацию еще можно терпеть если у вас действительно нужно добавить всего 1-2 поддомена, но что, если ваше приложение предоставляет своим пользователям отдельные поддомены? http://user.app1.loc, http://pi_es.seriyps.ru/, http://seriyps.habrahabr.ru/ ? Отладка такого проекта с использованием файла hosts становится практически невыполнимой задачей. Опять же при росте количества проектов файл hosts забивается мусором и в нем становится трудно что-то найти.
  2. Если вы разрабатываете ваше приложение не в одиночку а целым офисом, в котором стоит девелоперский веб-сервер, то для того чтобы дать коллеге ссылку на домен из вашего файла hosts вам как минимум нужно будет убедиться что этот адрес есть и в его файле hosts. Согласитесь, это даже звучит смешно!

Свой девелоперский DNS сервер

Ну вот мы и добрались до самого праведного способа организации разработки интернет-приложения для доменов - это использование своего DNS сервера. Имеется в виду конечно не "каждому девелоперу - по DNS серверу" а установка централизованного DNS сервера для офиса. Хотя даже для единоличной разработки свой DNS все равно удобнее hosts файла хотя бы из за наличия wildcards. Собственно преимущества такого подхода очевидны:

  1. Проект разрабатывается в той среде, в которой он будет использоваться в продакшене. Никаких посторонних файлов и настроек веб-сервера как в случае с расположением проектов в поддиректориях localhost
  2. Настоящие доменные ЗОНЫ со всеми типами записей в т.ч. и wildcard... @, *, MX, CNAME и пр.
  3. Все пользователи, прописавшие себе этот DNS (а если он включается автоматом по DHCP то все еще больше упрощается) смогут открывать ваши ссылки не задумываясь "a прописал-ли я ее себе в hosts файл.."

Устанавливаем и настраиваем DNS сервер.

Вступление у меня получилось длинным а само описание настройки выглядит куда скромнее, и это весьма символично.. Ведь, как оказалось, настроить простой DNS сервер с одной девелоперской зоной можно за 5 минут а откладывать на потом это можно бесконечно долго. Давайте наконец приступим!

Настройка простейшей dev зоны для DNS сервера BIND9 (named) на Ubuntu/Debian

Т.к. мы настраиваем DNS для небольшой сети "для своих", можно пренебречь некоторыми мерами безопасности. Например, мы не будем запускать bind в chroot окружении и не будем ограничивать подключение к ним с различных IP. К тому же для named уде при установке создаются вполне приемлемые правила для apparmor.
Для начала установим сам bind

sudo apt-get install bind9

В Ubuntu/Debian конфигурационные файлы Bind хранятся в директории /etc/bind, файлы, создаваемые при работе Bind хранятся в /var/cache/bind , главный конфигурационный файл bind находится по адресу /etc/bind/named.conf . В принципе, все опции можно вносить в него, но рекомендуется использовать для этого подключаемые файлы: named.conf.options - для настроек сервера и named.conf.local - для добавления зон. Таким образом нас интересуют именно эти файлы. Кстати, их синтаксис похож на C код, поэтому даже можно использовать подсветку синтаксиса C. Приступим:
Открываем файл /etc/bind/named.conf.options и приводим его содержимое к виду

options {
        directory       "/var/named";
        //раскомментировать строчки ниже, если хотим кешировать ответы DNS провайдера.. может немного "ускорить интернет"
        // forwarders {
        //      192.0.2.1; IP адреса DNS провайдера из "cat /etc/resolv.conf" при подключенном интернете
        //      192.0.2.2;
        // };
};

directory - указывает на директорию, в которой хранятся файлы сервера, такие как кеш, в forvarders можно добавить адреса DNS серверов вашего провайдера для кеширования их ответов и таким образом несколько сократить сетевые издержки на DNS запросы. Существует еще куча опций, но в нашем простейшем сервере они не нужны.
Прекрасно, общие настройки сделаны. Уже сейчас можно использовать наш сервер в качестве кеширующего DNS. Но нас интересует возможность создания и администрирования собственной зоны!
Открываем /etc/bind/named.conf.local и пишем в него

zone "dev" {
        type master;
        file "/etc/bind/db.dev";
        
};

type master означает что мы являемся единственным авторитетным источником информации о зоне dev, file указывает на расположение файла с информацией о зоне. Остается только создать этот файл
/etc/bind/db.dev

@ IN SOA dev. root.dev.    ( ; dev. - имя зоны, root.dev. - e-mail администратора зоны, в котором @ заменено на . т.е. это было root@dev но лучше вставить настоящий e-mail
        20100506        ; Серийный номер зоны. Обычно - текущая дата
        3h              ; обновление каждые 3 часа
        1h              ; повтор каждый час
        1w              ; информация хранится 1 неделю
        1d    )         ; TTL записи - 1 день

@   IN    NS    dev.        ; сервер имен. Рекомендую оставить как тут
@   IN    A  198.51.100.1       ; A - запись - IP адрес сервера для данной зоны (ваш девелоперский сервер). @ для имени домена означает "корень зоны"
*   IN    CNAME  @              ; CNAME запись. По сути - символическая ссылка. * для имени домена означает "любой поддомен".
                                  ; Т.е. при обращении на любой поддомен в зоне dev будет возвращаться IP 198.51.100.1
database IN A 198.51.100.2      ; Но поддомен database.dev будет расположен на другом IP адресе

Не забывайте ставить завершающие точки в имени домена!
Сохраняем, заставляем bind перечитать конфигурацию

sudo service bind9 reload

проверяем

nslookup any.dev
Server:            127.0.0.1
Address:        127.0.0.1#53

any.dev canonical name = dev.
Name:   dev
Address: 198.51.100.1
nslookup database.dev
Server:            127.0.0.1
Address:        127.0.0.1#53

Name:   database.dev
Address: 198.51.100.2

В случае возникновения каких-то проблем смотрим syslog - как правило bind сообщает об ошибках именно туда.
Понятно что таким-же образом можно создавать или переопределять любые другие зоны - добавляется блок zone в named.conf.local и создается соответствующий файл с информацией зоны.

Использование DNS сервера

Сервер есть, осталось его подключить!

Ubuntu

Если вы не пользуетесь NetworkManager-ом, то достаточно в самое начало файла /etc/resolv.conf прописать ваш DNS сервер.
Если NetworkManager используется, открываем "Изменить соединения", выбираем нужное соединение (проводное, VPN, etc), открываем вкладку "Параметры IPv4" и заменяем "Автоматически (DHCP)" на "Автоматически (DHCP, только адрес)", затем в поле "Серверы DNS" вписываем первым IP вашего DNS сервера и дальше через запятую ip адреса из /etc/resolv.conf
Переподключаем сеть.

Windows

В свойствах соединения в настройках TCP/IP прописываем первым наш DNS и после DNS провайдера.

Удачных экспериментов!
Почитать:
Официальный сайт bind
Кеширующий DNS сервер для локальной сети на основе BIND 9
Установка Bind (named) на CentOS
Установка и настройка DNS сервера bind9 Ubuntu-Debian HOWTO