Как давнего программиста на Python меня заинтриговало интервью с Доном Сайми (Don Syme), архитектором языка F#. В этом интервью Дон упомянул, что «некоторые рассматривают [F#] как строго типизированную разновидность Python, вплоть до различий в синтаксисе». Это поразило меня, и я решил, что этот вопрос стоит изучить глубже.
Как оказалось, F# — совершенно новый и очень интересный язык программирования, который до сих пор остается загадкой для многих разработчиков. F# предлагает те же преимущества в эффективности труда программистов, что и Ruby/Python в последние годы. F# — подобно Ruby и Python — высокоуровневый язык с минимальным синтаксисом и элегантностью выражения. Но настоящая уникальность F# заключается в том, что он сочетает в себе эти практичные средства с изощренной системой логического распознавания типов (type-inference system) и многими достижениями из мира функционального программирования. Это размещает F# в классе с несколькими узлами.
Но новый высокопродуктивный язык программирования — не единственная на сегодня новая интересная технология.
Широкое распространение облачных платформ вроде Windows Azure делают доступными распределенные хранилища и компьютерные ресурсы как крупным компаниям, так и разработчикам-одиночкам. Наряду с облачными хранилищами появились полезные инструменты наподобие горизонтально масштабируемого алгоритма MapReduce, которые позволяют быстро писать код, способный эффективно анализировать и сортировать потенциально гигантские наборы данных.
Этот подход удобен для отладки на скорую руку F#-программ. Такие инструменты дают возможность, написав несколько строк кода и развернув их в облаке, манипулировать гигабайтами данных. Просто замечательно.
В этой статье я поделюсь с вами некоторыми из своих находок в области F#, Windows Azure и MapReduce. Я покажу, как можно использовать F# и алгоритм MapReduce для разбора файлов журналов в Windows Azure. Сначала мы рассмотрим некоторые методики создания прототипов (prototyping), чтобы уменьшить сложность программирования MapReduce, а затем перенесем результаты нашего труда в… облако.
Азы работы с F#
Один из новых стилей работы, ставших доступными .NET-программистам с появлением F#, — рабочий процесс Interactive, привычный многим программистам на Perl, Python и Ruby. При таком стиле программирования часто используют среду интерактивного кодирования вроде оболочки самого Python или отдельный инструмент, например IPython. Это позволяет разработчику импортировать класс из модуля, создать его экземпляр, а затем, используя командную строку, изучить его методы и данные.
Распространенный способ интерактивной разработки, который станет привычным .NET-программистам, — простое написание кода в Visual Studio и передача его фрагментов в окно F# Interactive для выполнения. Фрагменты отправляются нажатием комбинации клавиш Alt+Enter применительно к текущему выделенному тексту. Кроме того, нажатие Alt+' позволяет отправить в это окно единственную строку. Пример использования этой методики показан на рис. 1.
Рис. 1. Использование окна F# Interactive
Этот подход удобен для отладки на скорую руку F#-программ; кроме того, в разрабатываемом вами скрипте на F# доступны и IntelliSense, и выполнение из командной строки.
Второй подход — вы вновь пишете код в Visual Studio, но потом копируете разделы кода из Visual Studio и напрямую вставляете их в автономную F# Interactive Console (рис. 2). В этом случае вы должны добавлять в конец вставленного кода две точки с запятыми. Это позволяет интерактивно взаимодействовать с кодом и дополнительно использовать преимущества выполнения из командной строки. Возможно, вы перейдете на этот вариант, когда привыкнете программировать в более интерактивном стиле.
Рис. 2. F# Interactive Console
Вы также можете вести интерактивную разработку на F#, запуская свой код непосредственно из Windows PowerShell; иначе говоря, передавая скрипт в fsi.exe (исполняемый файл F# Interactive Console) Одно из преимуществ такого подхода в том, что это позволяет быстро создавать прототипы скриптов и передавать результаты на стандартный вывод. Кроме того, можно итеративно изменять код в «облегченном» текстовом редакторе, например в Notepad++. На рис. 3 показан пример вывода от скрипта MapReduce, запускаемого из Windows PowerShell; этот скрипт я буду часто использовать в данной статье.
Рис. 3. Выполнение скрипта на F# в Windows PowerShell
PS C:\Users\Administrator\Desktop> & 'C:\Program Files (x86)\FSharp-2.0.0.0\bin\fsi.exe' mapreduce.fsscript
192.168.1.1, 11
192.168.1.2, 9
192.168.1.3, 8
192.168.1.4, 7
192.168.1.5, 6
192.168.1.6, 5
192.168.1.7, 5
Все эти разные способы написания кода помогают справляться со сложными алгоритмами, программированием для сетей и облака. Вы можете быстро писать черновые варианты скриптов и запускать их из командной строки, чтобы увидеть, дают ли они ожидаемые вами результаты. После этого вы сможете вернуться к созданию более крупных проектов в Visual Studio.
Теперь, когда вы знаете азы работы в F#, давайте углубимся в какой-нибудь реальный код.
Разбор журналов в стиле MapReduce
В дополнение к ранее упомянутым преимуществам интерактивного программирования код на F# является четким и эффективным. Пример на рис. 4 состоит менее чем из 50 строк кода, но содержит все важные части алгоритма MapReduce для вычисления десяти наиболее часто встречающихся IP-адресов в наборе файлов журналов.
Рис. 4.Алгоритм MapReduce для разбора файла журнала
open System.IO
open System.Collections.Generic
// Map Phase
let inputFile = @"web.log"
let mapLogFileIpAddr logFile =
let fileReader logFile =
seq { use fileReader = new StreamReader(File.OpenRead(logFile))
while not fileReader.EndOfStream do
yield fileReader.ReadLine() }
// Takes lines and extracts IP Address Out,
// filter invalid lines out first
let cutIp =
let line = fileReader inputFile
line
|> Seq.filter (fun line -> not (line.StartsWith("#")))
|> Seq.map (fun line -> line.Split [|' '|])
|> Seq.map (fun line -> line.[8],1)
|> Seq.toArray
cutIp
// Reduce Phase
let ipMatches = mapLogFileIpAddr inputFile
let reduceFileIpAddr =
Array.fold
(fun (acc : Map<string, int>) ((ipAddr, num) : string * int) ->
if Map.containsKey ipAddr acc then
let ipFreq = acc.[ipAddr]
Map.add ipAddr (ipFreq + num) acc
else
Map.add ipAddr 1 acc)
Map.empty
ipMatches
// Display Top 10 Ip Addresses
let topIpAddressOutput reduceOutput =
let sortedResults =
reduceFileIpAddr
|> Map.toSeq
|> Seq.sortBy (fun (ip, ipFreq) -> -ipFreq)
|> Seq.take 10
sortedResults
|> Seq.iter(fun (ip, ipFreq) ->
printfn "%s, %d" ip ipFreq);;
reduceFileIpAddr |> topIpAddressOutput
Эту автономную версию, которая впоследствии станет сетевой, можно разбить на три фазы: сопоставление (map phase), сокращение (reduce phase) и отображение (display phase).
Первой фазой является сопоставление. Функция mapLogFileIpAddr принимает файл журнала как параметр. В этой функции определена другая функция, fileReader, в которой используется методика функционального программирования для отложенного получения строки текста из файла (впрочем, языки вроде C# и Python тоже позволяют делать это). Далее функция cutIp разбирает каждую строку ввода, отбрасывает строки комментариев и возвращает IP-адрес и целое число (1).
Это эффективная методика обработки данных. Выделите весь блок кода, отвечающего за сопоставление, и выполните его в окне F# Interactive вместе со строкой:
let ipMatches = mapLogFileIpAddr inputFile
Вы увидите следующий вывод:
val ipMatches : seq<string * int>
Заметьте, что на данный момент никаких операций не выполнялось и что файл журнала не считывался. Единственное, что было сделано, — оценено выражение. Благодаря этому выполнение откладывается до тех пор, пока в нем не появится реальная потребность, и данные не извлекаются в память только ради того, чтобы оценить выражение. Это эффективная методика обработки данных, и ее эффективность станет особенно заметной, когда вам понадобится разбор гигантских файлов журналов, размеры которых исчисляются гигабайтами или терабайтами.
Если вы хотите прочувствовать разницу, просто добавьте line в функцию cutIp, чтобы она выглядела так (Примечание. строка |> Seq.toArray для конструкции функции совсем необязательна; в нашем примере она специально используется для убыстрения работы функции, если бы ее не было, функция просто бы работала медленно, как, например, функция mapLogFileIpAddr.):
let cutIp =
let line = fileReader inputFile
line
|> Seq.filter (fun line -> not (line.StartsWith("#")))
|> Seq.map (fun line -> line.Split [|' '|])
|> Seq.map (fun line -> line.[8],1)
|> Seq.toArray
cutIp
Если вы отправите этот код интерпретатору F# и предоставите большой файл журнала, содержащий несколько гигабайт данных, то можете прогуляться и выпить чашечку кофе, так как ваш компьютер будет занят чтением всего файла и генерацией для него сопоставлений «ключ-значение» в памяти.
В следующей части конвейера данных я принимаю вывод от предыдущей и отправляю результаты в анонимную функцию, которая подсчитывает число вхождений IP-адресов в последовательности. Все значения добавляются в структуру данных Map через рекурсию. Этот стиль программирования может оказаться трудным в понимании для разработчиков — новичков в функциональном программировании, поэтому вы, возможно, захотите вставить выражения print внутрь анонимной функции, чтобы видеть, что именно она делает.
При императивном программировании вы могли бы добиться того же, обновляя изменяемый словарь, в котором каждый IP-адрес хранится как ключ, перебирая последовательность IP-адресов в цикле, а потом обновляя счетчик для каждого адреса.
Последняя фаза не имеет ничего общего с алгоритмом MapReduce, но полезна при изучении скриптов на этапе создания прототипов. Результаты фазы сопоставления передаются из структуры данных Map в Seq, сортируются, а затем выводятся 10 наиболее часто встречающихся. Заметьте, что при таком стиле конвейеризации данных результаты одной операции перетекают в другую безо всякого цикла for.
MapReduce и Windows Azure
Закончив проверку прототипа, пора заняться его переносом в среду, напоминающую производственную. В качестве примера я перенес программу из настольной системы в Windows Azure.
Для начала будет полезно посмотреть на примеры F#-кода для Windows Azure по ссылке code.msdn.microsoft.com/fsharpazure и установить шаблоны Windows Azure. Особенно интересен пример webcrawler, где рабочая роль F# использует хранилище двоичных объектов и очереди. Этот проект стоит тщательно изучить, если вас интересует более «продвинутое» применение F# в сочетании Windows Azure.
Я не стану детально рассматривать конфигурирование фермы MapReduce со множеством узлов. Вместо этого я представлю общий обзор. Вся специфика изложена в статье Джоша Твиста (Josh Twist) в журнале MSDNMagazine «Synchronizing Multiple Nodes in Windows Azure» (msdn.microsoft.com/magazine/gg309174).
Сконфигурировать ферму MapReduce в Windows Azure можно несколькими способами. Один из примеров использования рабочих ролей F#, которые равномерно распределяются между рабочими ролями сопоставления (Map Workers) и сокращения (Reduce Workers), показан на рис. 5. Сделать это очень легко: вы просто копируете и вставляете функцию сопоставления в Map Worker, а функцию сокращения — в Reduce Worker.
Рис. 5. Ферма MapReduce в Windows Azure
Детальное объяснение распределенного алгоритма и его возможных реализаций отлично представлено в презентации MapReduce, подготовленной Джеффом Дином (Jeff Dean) и Санжаем Гемаватом (Sanjay Ghemawat) (labs.google.com/papers/mapreduce-osdi04-slides/). Однако в примере на рис. 5 показано, что рабочие роли F# параллельно используют несколько файлов журналов. Потом они возвращают свой вывод, состоящий из ключей IP-адресов со значением 1, через Windows Azure AppFabric Service Bus рабочим ролям сокращения или с помощью записи на диск.
Далее рабочие роли сокращения считывают эти промежуточные данные и выдают суммарный счетчик пар «ключ-значение», записывая их в хранилище двоичных объектов. Каждая такая роль создает собственный отчет, и все эти отчеты надо объединить перед сортировкой и отображением главной рабочей ролью (Master Worker).
Создание и публикация рабочей роли
Закончив прототип и планирование высокоуровневой архитектуры, создайте необходимый проект в Visual Studio 2010 и опубликуйте его в Windows Azure.
Создание рабочей роли F# — не столь простой процесс, как хотелось бы, поэтому давайте разберем все необходимые этапы. Сначала вам потребуется скачать ранее упомянутые шаблоны Windows Azure F#. Затем вы должны создать проект Visual F# для Windows Azure. Я присвоил проекту имя AzureFSharpProject.
После этого у вас появится возможность создать рабочую роль F#, как показано на рис. 6.
Рис. 6. Создание рабочей роли F#
В этот момент вы можете поместить свою функцию сопоставления в роль Map Worker, а функцию сокращения в роль Reduce Worker. Затем создайте дополнительные рабочие роли для других ролей Map Worker и Reduce Worker в зависимости от масштаба ваших потребностей в обработке данных. Канонический справочник — документ Google MapReduce по ссылке labs.google.com/papers/-mapreduce.html. В нем подробно описываются архитектура MapReduce, возможные проблемы и сценарии применения.
Когда вы будете готовы к публикации в Windows Azure, щелкните правой кнопкой мыши свой проект и выберите Publish | Create Service Package Only, как показано на рис. 7.
Рис. 7. Публикация в Windows Azure
Наконец, вам потребуется зарегистрироваться на портале управления Windows Azure и через его интерфейс создать рабочую роль (рис. 8).
Рис. 8. Конфигурирование новой рабочей роли
В этот момент вы можете подключать свои узлы так, как считаете нужным, и обрабатывать алгоритмом MapReduce журналы в облаке. Конечно, этот же подход легко применим к источникам данных, отличных от простых файлов журналов. Универсальный F#-алгоритм MapReduce — наряду с интерактивными средствами, которые я продемонстрировал при его кодировании, — можно использовать практически для любой работы, связанной с разбором, сопоставлением и сокращением.
Дальнейшие шаги
F# — мощный язык программирования, который позволяет решать многие задачи с помощью как небольших блоков кода, так и более сложных программ, составляемых из этих блоков. В этой статье я продемонстрировал, как, написав всего 50 строк кода на F#, можно превратить алгоритм MapReduce в анализатор журналов на основе Windows Azure.
Что касается реализации MapReduce в Windows Azure, то, вероятно, вам стоит прочитать еще пару интересных статей по этой тематике. Сначала обратите внимание на статью «Building a Scalable, Multi-Tenant Application for Windows Azure» в MSDN, где обсуждаются рабочие роли и MapReduce (msdn.microsoft.com/library/ff966483). Потом прочтите статью Хуана Диаз (Juan Diaz) «Comparison of the use of Amazon’s EC2 and Windows Azure, cloud computing and implementation of MapReduce», в блоге по ссылке (bit.ly/hBQFSt).
Если вы еще не освоили F#, то, надеюсь, эта статья подтолкнет вас к тому, чтобы попробовать поработать на этом языке. А если вам интересно прослушать все интервью Дона Сайми (Don Syme), отправляйтесь в блог Simple-Talk (bit.ly/eI74iO).