Блог Дмитрия Сошникова

про технологии и человеческое счастье

Создание виртуальной машины для анализа данных в облаке Microsoft Azure

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

Для всех, кто хочет использовать облако для машинного обучения или глубокого обучения нейросетей, у нас есть хорошая новость – облако Microsoft Azure прекрасно вам подойдёт! И вот почему:

  • В Azure есть готовый образ Data Science Virtual Machine, как под управлением Windows, так и под Linux, на которых уже установлено всё необходимое вам программное обеспечение!
  • В нашем облаке вам доступны компьютеры с графическими процессорами NVidia (т.н. N-Series VMs), что сильно ускоряет процесс обучения нейросетей.

В этом посте я рассмотрю инструкцию по созданию и настройке виртуальной машины для глубокого обучения в облаке. При этом я буду предполагать, что у вас уже есть облачная подписка Microsoft Azure, привязанная в вашему Microsoft Account – если это не так, то можно обзавестись пробной подпиской.

Data Science Virtual Machine

Для задач обработки данных хорошо подходит специализированная виртуальная машина Data Science Virtual Machine. На ней изначально уже установлены:

  • Visual Studio и Visual Studio Code, с поддержкой R и Python
  • Anaconda – самая популярная cреда для поддержки различных окружений на Python и не только
  • Все необходимые фреймворки для обучения нейросетей: Microsoft Cognitive Toolkit (CNTK), Tensorflow, MXNet и др. Причем если вы используете виртуалку с GPU – все утилиты и библиотеки для работы с GPU (драйвера NVidia, CUDA, cuDNN и др.) будут уже настроены.
  • Jupyter Notebooks
  • Microsoft R
  • Все необходимые библиотеки и утилиты для доступа к Azure
  • Много полезных утилит (github, AzCopy, …) и прочих вкусностей

Виртуальные машины доступны как для Windows, так и для Linux:

image

При этом есть тонкости:

  • Data Science Virtual Machine for Windows 2016 – это самый лучший вариант! Может ставится как на GPU (причем как NC, так и NV – подробнее об этом ниже), так и на обычную VM.
  • Deep Learning Toolkit for DSVM – это более старая версия виртуальной машины для глубокого обучения на базе Windows Server, которая ставится только на виртуалки типа NC (которых иногда может не хватать).
  • Из Linux-вариантов поддержка GPU есть только в Ubuntu, но зато и выбор фреймворков там чуть шире.

Виртуалки с GPU

В Azure есть 2 типа виртуалок с GPU (т.н. N-Series VMs):

  • NC (Compute) – предназначены для вычислений и глубокого обучения. Поскольку такие виртуалки пользуются значительной популярностью, их может иногда не хватать в тех или иных регионах.
  • NV (Visualisation) – предназначены для запуска приложений графической визуализации. Тем не менее, на этих виртуалках теоретически можно считать нейросети, хотя делать это не рекомендуется. И стоят они чуть дороже.

Виртуальные машины с GPU доступны лишь в некоторых регионах, поэтому если вам не предлагают NC или NV в качестве опции при выборе типа виртуалки – попробуйте выбрать другой регион. Начинать стоит с США (South-Central US, East US) или Европы (North Europe). Вот на этой странице можно посмотреть доступность в разных регионах, выбирая их из drop-down вверху страницы.

Процесс создания VM

Ниже описывается процесс создания и настройки Data Science VM на GPU.

1. Заходим на Azure Portal и выбираем вверху пункт “создать”:

image

2. В строке поиска пишем Data Science и получаем на выбор несколько вариантов, приведенных на рисунке выше.

3. Выбираем нужны вариант и нажимаем “Создать”. После этого переходим вот к такому экрану конфигурации:

image

4. При установке машины с GPU в качестве диска обязательно выбрать HDD, поскольку N-Series VM не бывают с SSD, и если выбрать SSD-потом не будет доступна опция выбора N-Series.

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

6. В качестве региона выбираем подходящий регион по принципу, описанному выше. Если в выбранном регионе не окажется доступных VM с GPU – потом можно будет вернуться и сменить его.

7. Следующим пунктом выбираем размер и тип виртуалки. NC6 – это самая простая виртуалка с графическим процессором, и как видим на текущий момент она стоит около 60000 руб. в месяц.

image

8. В разделе “дополнительные параметры” в принципе можно ничего не настраивать, если вы просто создаете одну вирталку. Если же у вас уже есть какая-то виртуальная подсеть, или вы настраивали ранее сетевой доступ для другой VM – здесь есть некоторый простор для оптимизации.

9. На последнем экране читаем ещё раз выбранную конфигурацию и нажимаем “Купить”.

image

10. После этого машина долго и мучительно (более 10 минут) создается.

Процесс настройки VM

На этом этапе вы уже можете зайти в свою виртуалку, нажав кнопку “Connect” на страничке настроек (выделено красным):

image

Однако для комфортной работы с виртуальной машиной нужно сделать ещё несколько настроек:

1. Присвоить машине удобное DNS-имя, чтобы обращаться к ней по имени. Для этого надо нажать на IP-адрес на страничке выше (выделено синим), на открывшейся странице ввести DNS-имя и нажать “Сохранить”:

image

Также запомните окончание адреса, который зависит от региона. Для данной машины полный DNS-адрес будет smart8.southcentralus.cloudapp.azure.com. Теперь вы сможете просто подключаться к машине с помощью удалённого рабочего стола, используя DNS-адрес и введя указанные ранее данные о пользователе и пароль:

image

2. Очень удобно проводить обучение нейросетей не через удалённый терминал, а через браузер, используя Jupyter Notebook. Для этого рекомендуется настроить автоматический запуск Jupyter на виртуалке, а также пароль для доступа. Для этого войдите на машину через удалённый рабочий стол и запустите ярлык, показанный ниже:

image

После этого в появившемся текстовом окне необходимо дважды ввести пароль и нажать ENTER.

image

После этого необходимо немного подождать, и можно попробовать войти в Jupyter из локального браузера по адресу https://localhost:9999. При этом игнорируйте сообщения о том, что https-сертификат неверный – смело переходите на страницу, вводите установленный ранее пароль и начинайте работать!

image

3. Для того, чтобы Jupyter Notebook был доступен извне по порту 9999, необходимо добавить запись в Network Security Group для нашей машины (если это не было сделано по умолчанию). В панели управления Azure переходим в группу ресурсов нашей виртуалки и выбираем нечто, заканчивающееся на –nsg:

image

В открывшемся окне мы должны увидеть примерно такие правила:

image

Если правила Jupyter с портом 9999 нет, то его необходимо добавить вручную, указав протокол TCP и порт 9999.

После этого вы должны быть в состоянии зайти на Jupyter Notebook вашей машины, просто указав в браузере адрес https://smart8.southcentralus.cloudapp.azure.com:9999 и введя пароль, установленный на предыдущем шаге.

Простой запуск и остановка VM

Поскольку машины с GPU дорогие, то рекомендуется останавливать виртуалки, когда вы их не используете. Так вы сможете снизить расходы с $1000 в месяц, до $70-150 (таков на данный момент мой типовой счет за облако, если я не делаю чего-то экстраординарного). Чтобы каждый раз не заходить на Azure Portal, очень удобно настроить специального Azure Bot в скайпе, который позволит вам запускать и останавливать машины простыми командами.

Процесс настройки бота такой:

1. Заходите на http://botframework.com в раздел Bot Directory.

image

2. Как правило, Azure Bot будет доступен на первой странице. Если нет – найдите его!

3. Нажимаете на бота и устанавливаете его себе в Skype.

4. При первом диалоге с ботом надо будет аутентифицироваться – перейти по ссылке, залогиниться в Azure и скопировать код с сайта в чат с ботом. После этого бот вас запомнит, и делать это будет не нужно.

5. Для остановки и запуска виртуальных машин используйте команды start vm <имя>, stop vm <имя>

image

Обратите внимание, что только такое выключение машины (DEALLOCATION) ведет к тому, что с вас перестают списывать деньги за её использование. Если вы просто в удалённом терминале скажете Shutdown – машина будет остановлена, но не деаллоцирована, и деньги продолжат списываться. При деаллокации машины освобождаются лишние ресурсы, включая IP-адреса и временные диски.

Хранение данных

В заключение хотелось бы сказать несколько слов о хранении данных. Вместе с созданной машиной автоматически было создано два диска:

  • Диск C: – системный. На нем хранятся все программы и системные файлы, и даже есть немного места. Содержимое этого диска сохраняется при деаллокации машины. Однако захламлять его вашими данными – не слишком хорошая идея.
  • Диск D: – это быстрый диск для временных данных, его содержимое между запусками машины будет теряться.

Для долгосрочного хранения данных, а также для разделения их между машинами, можно использовать следующие подходы:

  • Создать файловое хранилище внутри Azure Storage и затем подключить его к своей машине командой net use. Такой подход имеет тот плюс, что вы сможете также подключить то же хранилище к своей основной машине, и тем самым получите простой способ обмениваться данными. Однако файловое хранилище относительно медленное, а если оно вдруг расположено в другом регионе – то совсем медленное. Кроме того, за пересылку данных между регионами взимается какая-то плата.
  • Создать виртуальный диск (Managed Disk) и подключить его к виртуальной машине. Виртуальные диски быстрее, чем файловые хранилища, но их сложнее подключить к внешним компьютерам.

Best Practice

В последнее время я делаю несколько проектов по анализу данных и обучению нейросетей, поэтому хочу поделиться с вами полезным приёмом использования виртуальных машин с GPU. Как известно, 80% времени обычно тратится на подготовку данных, и лишь 20% – на собственно запуск алгоритмов обучения. Поэтому я обычно имею в своём арсенале две виртуальные машины, подключенные к общему файловому хранилищу:

  • Виртуальная машина для подготовки данных – это обычная виртуалка, без GPU, достаточно мощная, чтобы с ней было комфортно работать по удалённому доступу. Обычно я использую что-то типа DS2.
  • Виртуальная машина для обучения – это NC6.

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

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

Заключение

Data Science Virtual Machine – это очень простой и быстрый способ начать использовать инструменты анализа данных и глубокого обучения. Я надеюсь, что мой опыт поможет вам использовать эти виртуальные машины в своей работе или исследованиях. Если у вас есть свой опыт использования виртуальных машин с GPU для анализа данных – делитесь опытом в комментариях!

2 comments

Как развернуть сервер Minecraft в облаке Microsoft Azure

Играть в Minecraft интересно, но ещё интереснее делать это коллективно, по сети. Поэтому многие любители Minecraft ищут возможность создать свой Minecraft-сервер, чтобы на нём заниматься коллективной постройкой своего мира. Сам процесс создания сервера неплохо описан, однако часто хочется разместить сервер не на своём компьютере, а где-то в интернет. Тут нам на помощь приходит облако Microsoft Azure!

К сожалению, в рамках бесплатного студенческого предложения студенты и школьники не могут создавать в облаке виртуальные машины. Однако, вы всегда можете использовать бесплатные облачные ресурсы на $25/мес. в рамках Visual Studio Dev Essentials, воспользоваться Azure Trial на один месяц, или получить промо-код Azure Pass в рамках какого-нибудь из мероприятий (недавно в рамках облачного дня Майкрософт мы раздали очень много таких кодов всем желающим), или у себя в учебном заведении от преподавателя.

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

Создание сервера из шаблона в Azure Portal

Создание виртуальной машины мы начинаем с кнопки создать, выбирая пункт вычисления.

image

Далее выбираем пункт показать все, и в строке поиска вводим ключевое слово Minecraft – после чего в результатах поиска вы сразу увидите Minecraft Server.

image

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

image

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

image

В окне свойств сервера самое главное для нас – это DNS-имя сервера, с помощью которого мы можем сразу подключаться к серверу из Minecraft! Кроме того, из этого же окна можно останавливать сервер, если вдруг он нам больше не нужен, чтобы экономить ресурсы.

image

Обратите внимание, что установленный сервер имеет версию 1.8, и при подключении к нему из Minecraft нам необходимо выбрать такую же версию клиента (это можно сделать из Minecraft Launcher).

Если вы всё сделали правильно, то на этом этапе вы уже можете использовать сервер, но не можете подключаться к нему как оператор.

Настройка параметров сервера

Для того, чтобы изменить режим игры (например, на creative), и добавить себя как оператора на сервер, нам необходимо изменить настройки сервера. Для этого необходимо подключиться к нему по протоколу SSH, используя имя и пароль администратора, которые мы задавали при создании сервера. Для поключения по SSH из Windows можно использовать популярный клиент putty.

image

После подключения, перейдем в директорию /srv/minecraft-server. В ней содержатся все необходимые для работы сервера файлы.

Конфигурация сервера расположена в файле server.properties. Для его редактирования необходимо сначала перейти в режим суперпользователя, а затем использовать свой любимый текстовый редактор:

image

Параметры, которые можно задать в этом файле, приведены на сайте Minecraft Wiki.

Чтобы сделать себя оператором, необходимо добавить информацию о себе в файл ops.json. Первоначально этот файл содержит пустой список [], в который необходимо добавить нужных пользователей. Например, в моём случае файл выглядит так:

[ { "name": "shwars",
    "uuid": "eb0ce7d8-65e9-4940-ab27-3e41b525c66b",
    "level": 4
} ]

Здесь uuid – это уникальный идентификатор пользователя, который можно узнать, введя своё имя на сайте http://mcuuid.net.

После внесения изменений необходимо перезапустить сервер командой:

systemctl restart minecraft-server

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

Разворачивание сервера Minecraft 1.9 из шаблона с GitHub

Давайте рассмотрим альтернативный способ разворачивания сервера Minecraft, который позволяет за один шаг задать пользователя-оператора, а также использовать более современную версию сервера 1.9. Для этого нам необходимо использовать внешний шаблон развертывания, который находится по адресу http://bit.ly/azminecraft.

Перейдя по этому адресу, мы увидим следующую страничку:

image

Здесь нам необходимо нажать кнопку Deploy to Azure, после чего в нашем облачном портале появится конфигурационная панель такого вида:

image

Здесь нам необходимо ввести параметры – имя minecraft-оператора, имя и пароль администратора сервера, а также DNS-имя сервера (например, studentcraft) и версию сервера (1.8 или 1.9). После этого нажимаем создать и ждём около 10 минут.

Если всё было сделано правильно, то ваш сервер получит адрес вида studentcraft.northeurope.cloudapp.azure.com (при условии, что вы выбрали Северную Европу как местоположения для развертывания, и studentcraft как имя сервера). Узнать точный адрес сервера вы сможете в панели управления в разделе все ресурсы, выбрав ресурс с окончанием publicip.

Заключение

Облако Microsoft Azure позволяет легко разворачивать любые виртуальные машины, в том числе и сервера Minecraft. Бесплатной облачной подписки Dev Essentials должно хватить, чтобы держать маломощный сервер работающим в течение месяца, или периодически включать и выключать более высокопроизводительный сервер. Развертывание своего сервера Minecraft в облаке – это хорошее упражнение для юных (будущих) IT-администраторов и разработчиков.

1 comment

Облачная погодная станция и интернет вещей

Наверняка вы слышали термин “интернет вещей” – про него сейчас много говорят. Речь идет о том, что количество подключенных к интернет устройств растёт экспоненциально, и скоро все вещи, окружающие нас, будут подключены к интернет и смогут взаимодействовать как с нами, так и между собой. Новогодние каникулы – отличное время для экспериментов, и мне захотелось немного поэкспериментировать с интернетом вещей и сделать свою погодную станцию. А заодно и сервис, который мог бы сравнивать данные о погоде “за окном” со значениями прогноза погоды, а в будущем возможно и делать какие-то предсказания на основе шаманского анализа временных рядов. Хочу поделиться с вами результатами.

Сделать свою погодную станцию – это традиционное развлечение, например оно описано здесь, здесь, здесь, и еще отчасти тут. Мне же хотелось построить всё на технологиях Майкрософт, при этом сделав упор на облачный сервис, который будет выполнять центральную роль во всей системе – а собственно сама станция будет просто отправлять данные в интернет. Посмотреть температуру за окном можно будет с мобильного устройства или через веб-интерфейс.

IMG_6268 image
image IMG_6263

Две составляющие интернета вещей

На сегодняшний день, интернет вещей становится возможным благодаря наличию двух важных составляющих:

  • Компактные клиентские устройства, подключенные к сети. С этой стороны процесс бурно развивается благодаря наличию недорогих микроконтроллеров, начиная от Arduino (который можно легко программировать на Visual Studio), и заканчивая экзотическими контроллерами, программируемыми на JavaScript.
  • Развитие облачных технологий, которые позволяют накапливать в облаке большие объемы данных и производить их анализ и обработку. С этой точки зрения очень интересно использование AzureML.

image

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

Архитектура облачной погодной станции

В соответствии с таким пониманием интернета вещей мы и спроектируем нашу погодную станцию. Роль конечного устройства будет выполнять датчик температуры и освещенности (зачем нужно мерить освещенность – я расскажу в отдельном посте), подключенный к интернет с помощью замечательного контроллера NetDuino Plus 2, а облачный сервис будет реализован на Microsoft Azure. Также мы будем получать сведения о текущей температуре из погодного сервиса OpenWeatherMap, чтобы сравнивать наши измерения с данным прогноза погоды.

image

Архитектура облачного сервиса

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

  • Хранилище данных. Нам важно сразу выбрать правильную архитектуру для хранения данных: полноценная база данных SQL, полуструктурированные Azure Storage или же решение типа NoSQL. В нашем случае проще всего было бы использовать SQL Azure, поскольку объем данных не очень велик – но мы предпочтем в образовательных целях использовать Azure Table Storage, истинное облачное хранилище, которое может потенциально сильно масштабироваться. Ценой такого решения будет то, что мы не сможем использовать достаточно богатые конструкции запросов к данным, что немного усложнит реализацию.
  • Веб-интерфейс, который будет отображать пользователю графики температуры, давления, освещенности и т.д. Я буду использовать ASP.NET Web Forms как самую простую технологию, однако на практике для повышения производительности конечно стоит рассмотреть ASP.NET MVC.
  • Веб-сервис для регистрации температуры, а также для передачи данных мобильному приложению. Используем замечательную технологию Web API, которую можно развернуть параллельно с веб-интерфейсом на ASP.NET (ниже расскажем, как).
  • Постоянно работающий процесс Worker Role, которые будет периодически собирать данные из сервиса OpenWeatherMap и сохранять в нашем хранилище.

Создание проекта в Visual Studio

Для начала создадим проект в Visual Studio (я буду использовать Visual Studio 2015). Если вы не хотите создавать проект сами, а лишь поэкспериментировать, то используйте готовый код проекта, доступный на GitHub: версию, описанную в статье, или последнюю версию.

Начнем с создания облачного сервиса, которые будет содержать в себе Web Role и Worker Role. Для работы с облачными проектами и инструментами вам потребуется установить Windows Azure SDK.

image

При создании облачной службы мы сразу добавляем в неё необходимые компоненты:

 image

При создании веб-составляющей WeatherMonitorWeb нас попросят выбрать стек технологий ASP.NET – в нашем случае я использовал ASP.NET Web Forms. Если кто-то поможет переписать этот проект на ASP.NET MVC – я буду признателен! Используйте pull request для исходного кода проекта на GitHub.

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

Организация доступа к данным

Данные о погоде будут хранится в табличном хранилище Microsoft Azure. Мы можем использовать встроенное хранилище, создаваемое в рамках облачной службы, или настроить отдельное хранилище, доступ к которому будет осуществляться из разных служб. Создадим отдельное хранилище – для этого в портале Microsoft Azure выберем следующий пункт:

image

При создании хранилища стоит использовать разумные опции (в зависимости от требований к надежности – выбирать реплицированное или локальное хранилище). После создания службы данных (мы назовём её weathermon) в меню “управление ключами” (внизу страницы) будут доступны ключи, которые потребуются нам позднее для доступа к данным.

image

Табличное хранилище позволяет хранить множество пар атрибут-значение на множестве распределенных серверов. Для удобства мы будем использовать Azure Storage SDK, который входит в состав Azure SDK. Необходимо добавить в проект ссылку (Reference) на WindowsAzure.Storage, либо использовать NuGet (install-package WindowsAzure.Storage).

Storage SDK позволит нам автоматически помещать в хранилище объекты, унаследованные от TableEntity – при этом сам SDK позаботится о том, как они будут преобразованы к атрибутам. Запрашивать данные из хранилища мы сможем с помощью простых и привычных LINQ-запросов, получая в результате коллекции объектов.

Для начала опишем класс для хранения одного измерения:

public enum WeatherInfoSource { WeatherService, Device };
public enum ReadingType { Temperature, Humidity, Pressure, Luminocity };

public class WeatherRecord : TableEntity
{

    public WeatherRecord(DateTime When, double Reading, WeatherInfoSource src, ReadingType type)
    {
        this.Reading = Reading;
        this.WeatherInfoSource = src;
        this.When = When;
        this.ReadingType = type;
        PartitionKey = string.Format("{0}.{1}", When.Year, When.Month);
        RowKey = string.Format("{0}-{1}:{2}.{3}.{4},{5}:{6}", src, type, When.Year, When.Month, When.Day, When.Hour, When.Minute);
    }

    public WeatherRecord() { }

    public DateTime When { get; set; }

    public int iWeatherInfoSource
    {
        get; set;
    }
    public WeatherInfoSource WeatherInfoSource
    {
        get { return (WeatherInfoSource)iWeatherInfoSource; }
        set { iWeatherInfoSource = (int)value; }
    }

    public int iReadingType { get; set; }

    public ReadingType ReadingType
    {
        get { return (ReadingType)iReadingType; }
        set { iReadingType = (int)value; }
    }
    public double Reading { get; set; } 

}

Основные поля в этом классе:

  • WeatherInfoSource – показывает, взято ли измерение из погодного сервиса или из погодной станции
  • ReadingType – описывает тип измерения: температура, давление, влажность, освещенность
  • Reading – само значение измерения
  • When – время проведения измерения

В этом классе есть две важные тонкости:

  • Мы не можем хранить в TableEntity данные не-примитивных типов. Поэтому вместо публично доступных свойств WeatherInfoSource и ReadingType перечислимого типа, мы описываем поля iReadingType и iWeatherInfoSource типа int, а WeatherInfoSource/ReadingType делаем свойствами, которые скрывают в себе операции преобразования типов
  • Поля PartitionKey и RowKey определяют, как данные будут распределяться по серверам в случае значительных объемов. Все объекты с одним PartitionKey хранятся вместе. В нашем случае мы будем хранить вместе все данные за одни месяц, т.к. нам их в дальнейшем может понадобится агрегировать. Правильный выбор PartitionKey/RowKey определяет архитектуру распределенного хранения данных, и может сильно влиять на производительность.

Для доступа к данным создадим статический класс WeatherDB, в котором будут находится необходимые функции. В конструкторе класса мы будем инициализировать внутренние поля для доступа к таблице и создавать таблицу, если её не существует:

static CloudStorageAccount CloudStore;
static CloudTableClient CloudTable;
public static CloudTable Table;

static WeatherDB()
{
    string str = "DefaultEndpointsProtocol=http;AccountName=weathermon;AccountKey=...";
    CloudStore = CloudStorageAccount.Parse(str);
    CloudTable = CloudStore.CreateCloudTableClient();
    Table = CloudTable.GetTableReference("WeatherRecord");
    Table.CreateIfNotExists();
}

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

Для помещения объекта в таблицу используем следующий метод:

public static async void Record(ReadingType t, WeatherInfoSource src, double x)
{
    var rd = new WeatherRecord(CurrentDateTime, x, src, t);
    await Table.ExecuteAsync(TableOperation.Insert(rd));
}

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

public static IEnumerable<WeatherRecord> GetData()
{
    var q = new TableQuery<WeatherRecord>();
    return WeatherDB.Table.ExecuteQuery(q);
}

Более интересная операция – это извлечение последних заданных измерений из таблицы. Это нужно, чтобы в приложении в дальнейшем показывать два значения температуры, полученных с сенсора и из погодного сервиса. Следующая функция возвращает массив последних значений заданного ReadingType, по одному измерению для каждого WeatherInfoSource:

public static WeatherRecord[] GetCurrentReading(ReadingType type)
{
    var data = (from z in WeatherDB.Table.CreateQuery<WeatherRecord>()
               where z.When > DateTime.Now.AddMinutes(-10)
               // where z.ReadingType == type
               select z).ToList();
    var res = from z in data
              where z.ReadingType == type
              group z by z.WeatherInfoSource into g
              select g.OrderByDescending(x => x.When).First();
    return res.ToArray();
}

Этот метод устроен достаточно хитро. Проблема в том, что мы не можем использовать функции сортировки данных на сервере, поскольку данные возможно распределены по серверам. В первом LINQ-запросе, который выполняется на сервере, нам доступны только очень простые операции. Поэтому вначале мы выбираем из таблицы в память все данные за последние 10 минут – чтобы уменьшить размер обрабатываемых данных. При этом мы даже не может отфильтровать их по ReadingType – соответствующая строчка закомментарена.

Далее мы производим запрос в память – и здесь нам уже доступна вся мощь LINQ, включая group into, с помощью которого мы и выделяем требуемые нам данные.

Worker Role для доступа к погодному сервису

Теперь запрограммируем Worker Role, который будет обращаться к погодному сервису и раз в 10 минут считывать значение погодных параметров. Для этого нам нужно будет зарегистрироваться в сервисе OpenWeatherMap и получить оттуда API Key для нашего приложения. Для доступа к OpenWeatherMap из нашего сервиса будем использовать библиотеку OpenWeatherMapClient.

Worker Role – это процесс, который запускается в облаке и всё время работает. Основной его функционал описывается одним классом, а главная функция – это асинхронная функция RunAsync:

private async Task RunAsync(CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        var Weather = await WeatherClient.CurrentWeather.GetByName(TargetCity,MetricSystem.Metric);
        WeatherDB.Record(ReadingType.Temperature, WeatherInfoSource.WeatherService, Weather.Temperature.Value);
        WeatherDB.Record(ReadingType.Pressure, WeatherInfoSource.WeatherService, Weather.Pressure.Value);
        WeatherDB.Record(ReadingType.Humidity, WeatherInfoSource.WeatherService, Weather.Humidity.Value);
        await Task.Delay(1000*60*NumMinutes);
    }
}

В ней всё очень просто: считываем текущую погоду через OpenWeatherMapClient, а потом вызываем описанные ранее функции для записи данных в хранилище. Обратите внимание, что поскольку функция асинхронная – она может спокойно “засыпать” на длительное время с помощью Task.Delay, освобождая процессорное время для других задач. Кроме того, в асинхронной функции можно удобно вызывать другие асинхронные методы – как это мы делаем с вызовом API OpenWeatherMap. Пока производится удалённый вызов, наш сервис также находится в состоянии спокойного ожидания.

Нам осталось в методе OnStart() инициализировать все необходимые переменные, в т.ч. параметры: название города и периодичность выполнения в минутах:

TargetCity = CloudConfigurationManager.GetSetting("City");
WeatherClient = new OpenWeatherMapClient("...OpenWeatherMapAPIKey...");
NumMinutes = int.Parse(CloudConfigurationManager.GetSetting("NumMinutes"));

Чтобы мы могли использовать CloudConfigurationManager.GetSetting, нам надо прописать соответствующие параметры в конфигурации нашего облачного сервиса. Идем в основной раздел решения в Visual Studio, отвечающий за облачный сервис (WeatherMonitor), выбираем WeatherMonitorWorker, и в разделе “параметры” добавляем два параметра NumMinutes и City типа string.

image

Всегда когда возможно стоит использовать такой способ конфигурации сервисов, поскольку соответствующие значения можно будет менять как из интерфейса Visual Studio, так и из портала Windows Azure. Это очень удобно!

Теперь уже можно запустить наше приложение и проверить его в работе! Убедится, что данные записываются в хранилище, можно с использованием очень удобного инструмента Azure Storage Explorer.

Отображение данных на веб-портале

У нашего приложения также есть веб-составляющая WeatherMonitorWeb. Это приложение ASP.NET Web Forms, в которое на главную страницу мы добавим элемент управления Chart Control для отображения графика измерений, и пару элементов управления для выбора параметров:

    <asp:Chart ID="MainChart" runat="server" Height="600" Width="800">
        <series>
            <asp:Series Name="MainSeries" ChartArea="MainChartArea" ChartType="Line" Color="Black"/>
            <asp:Series Name="RealSeries" ChartArea="MainChartArea" ChartType="Point" Color="Red" />
        </series>
        <chartareas>
            <asp:ChartArea Name="MainChartArea" />
        </chartareas>
    </asp:Chart>
<br /><br />
<asp:DropDownList runat="server" AutoPostBack="True" ID="ddDispType">
    <asp:ListItem Value="Temperature" />
    <asp:ListItem Value="Pressure" />
    <asp:ListItem Value="Humidity" />
    <asp:ListItem Value="Luminocity" />
</asp:DropDownList>
<asp:DropDownList runat="server" AutoPostBack="True" ID="ddTimePeriod">
    <asp:ListItem Value="Day" />
    <asp:ListItem Value="Week" />
    <asp:ListItem Value="Month" />
    <asp:ListItem Value="Year" />
</asp:DropDownList>

Здесь в Chart Control мы накладываем друг на друга два графика: реальные результаты измерений (которые мы научимся измерять чуть позже), и сохраненные нами значения из погодного сервиса.

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

ReadingType t = ReadingType.Temperature;
DateTime fr = DateTime.Now.AddYears(-1);
switch (ddTimePeriod.SelectedValue)
{
    case "Day":
        fr = DateTime.Now.AddDays(-1); break;
    case "Month":
        fr = DateTime.Now.AddMonths(-1); break;
    case "Year":
        fr = DateTime.Now.AddYears(-1); break;
    case "Week":
        fr = DateTime.Now.AddDays(-7); break;
}
switch(ddDispType.SelectedValue)
{
    case "Temperature":
        t = ReadingType.Temperature; break;
    case "Pressure":
        t = ReadingType.Pressure; break;
    case "Humidity":
        t = ReadingType.Humidity; break;
    case "Luminocity":
        t = ReadingType.Luminocity; break;
}

Далее собственно переходим к получению данных и их отображению с помощью DataBinding:

var db = WeatherDB.GetData();
var data = from z in db
            where z.WeatherInfoSource == WeatherInfoSource.WeatherService
            where z.ReadingType == t
            where z.When > fr
            orderby z.When descending
            select new { When = z.When, Data = z.Reading };
MainChart.Series[0].Points.DataBind(data,"When", "Data", "");
var rdata = from z in db
               where z.WeatherInfoSource == WeatherInfoSource.Device
               where z.ReadingType == t
               where z.When > fr
               orderby z.When descending
               select new { When = z.When, Data = z.Reading };
MainChart.Series[1].Points.DataBind(rdata, "When", "Data", "");

Здесь мы используем не очень хороший подход – сначала запрашиваем все данные в память с помощью описанного ранее метода GetData(), а уже потом осуществляем их выборку и сортировку. Оставляю читателю оптимизацию этого кода в качестве самостоятельного упражнения (Pull Requests приветствуются) – это можно сделать, ограничив объем передаваемых данных нужным периодом, как это сделано в функции GetCurrentReading выше.

На этом этапе вы должны быть в состоянии запустить сервис на своём компьютере, дать ему поработать некоторое время, и увидеть график температуры, собранной с OpenWeatherMap. Теперь самое время разместить проект в реальном облаке – для этого выберем облачный сервис WeatherMonitor правой кнопкой и используем “Опубликовать”.

При этом скорее всего у вас возникнут проблемы, связанные с особенностью реализации Chart Control. Дело в том, что этот элемент использует временные файлы в системной директории, которая при облачном развертывании оказывается защищена от записи. Для борьбы с этой проблемой, надо добавить в Web.Config следующие строчки конфигурации:

<configuration>
  <appSettings>
    <add key="ChartImageHandler" value="storage=session;timeout=20;" />

Создаем сервис WebAPI

Для того, чтобы мы могли в дальнейшем записывать данные от контроллера NetDuino, нам необходимо предусмотреть простое API, чтобы запрос по некоторому адресу URL автоматически приводил к записи соответствующего значения в таблицу. Для этого проще всего использовать технологию Web API, которую можно развернуть вместе с сайтом на базе ASP.NET. Подробнее этот процесс описан в этой англоязычной статье.

Первым делом, в файл Global.asax.cs необходимо добавить следующий код настройки маршрутизации, выполняемый при запуске приложения:

void Application_Start(object sender, EventArgs e)
{
    RouteTable.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = System.Web.Http.RouteParameter.Optional });
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
}

Теперь для того чтобы создать API по записи температуры, нам надо создать класс контроллера TemperatureController.cs – проще всего это сделать через добавление в проект класса Web API:

image 

Контроллер позволяет определять методы, которые будут вызываться при получении GET или POST-запросов по адресам типа /api/temperature или /api/temperature/13. Например, для записи температуры в базу по GET-запросу, переопределим следующий метод:

// GET api/<controller>/5
public async Task<string> Get(int id)
{
    await WeatherDB.RecordSuccess(ReadingType.Temperature, WeatherInfoSource.Device, id / 100.0);
    return DateTime.Now.ToString();
}

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

Аналогичным образом мы опишем метод, который по обычному запросу по адресу /api/temperature без параметров вернет последние измерения температуры. Все измерения возвращаются в формате JSON:

public WeatherRecord[] Get()
{
    return WeatherDB.GetCurrentReading(ReadingType.Temperature);
}

Аналогично создаются контроллеры для давления, влажности, освещенности.

Теперь мы можем, сделав запрос по адресу http://weathermon.cloudapp.net/temperature/-13, записать в таблицу любое измерение. Это измерение можно будет сразу посмотреть на графике.

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

Для реализации устройства, которое будет измерять температуру, давление и освещенность за окном, используем замечательный микроконтроллер NetDuino Plus 2. Его основной плюс состоит в том, что он имеет встроенный контроллер Ethernet, а также является совместимым в Arduino по форм-фактору.

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

Schematic

Board

Для сборки устройства я использовал обычный тонкий провод и паяльник, собрав схему прямо “на проводах” без использование макетной платы (конструкция видна на фото вверху статьи). Провод Ethernet втыкался прямо из марштуризатора (важно, чтобы на маршрутизаторе был включен DHCP), питание – от стандартного блока питания на 9 вольт.

В полученном дизайне значение освещенности мы будем снимать в аналогового входа 0, а сенсором давления/температуры будем управлять через последовательную шину I2C.

Пишем программу для NetDuino

Я был искренне и приятно удивлен, но программа для NetDuino пишется на слегка урезанном .NET – .NET Micro Framework (русскоязычный сайт). В отличие от Arduino, где работа с Ethernet Shield происходит достаточно сложно, здесь нам сразу становятся доступны все функции .NET по работе с сетью.

Для работы с сенсором я использовал библиотеку SensorLib. Поскольку программирование в .NET требует Visual Studio 2013 (вот тут рассказывается про работу .NET MF с VS2013), я создал отдельный проект (код на GitHub).

Вся функциональность программируется в одном главном файле в функции Main. Вначале мы инициализируем порты и создаем объект для работы с сенсором:

AnalogInput photo_in = new AnalogInput(Pins.GPIO_PIN_A0);
OutputPort pin = new OutputPort(Pins.GPIO_PIN_SDA, true);
Thread.Sleep(200);
pin.Write(false); pin.Dispose();

Debug.Print("Initializing sensor library...");
SensorLibrary.BMP180 sensor = new SensorLibrary.BMP180();
sensor.Init(SensorLibrary.BMP180.Mode.BMP085_MODE_STANDARD);

После этого, основная работа контроллера заключена в бесконечный цикл. Он проводит измерение, отправляет его в интернет и затем засыпает на заданное количество минут:

while (true)
{
    double pressure = sensor.GetPressure() / 100.0F;
    double pressuremm = pressure * 0.75006375541921;
    Debug.Print("Pressure:    " + pressuremm.ToString() + " mmHg");
    double temperature = sensor.GetTemperature();
    Debug.Print("Temperature: " + temperature.ToString() + " C");
    int lum = photo_in.Read();
    Debug.Print("Luminocity: " + lum.ToString());
    record("Temperature", (int)(temperature * 100));
    record("Pressure", (int)(pressuremm * 10));
    record("Luminocity", lum);
    Thread.Sleep(1000 * 60 * 10);
}

Для отправки в интернет служит функция record – она инициирует запрос в интернет по соответствующей URL и игнорирует результат:

private static void record(string measurement, int value)
{
    TouchUrl("http://weathermon.cloudapp.net/api/" +
	  measurement + "/" + value.ToString());
}

private static string TouchUrl(string url)
{
    using (var req = (HttpWebRequest)WebRequest.Create(url))
    {
        req.Method = "GET";
        using (var resp = req.GetResponse())
        {
		  // Если нужно - тут можем прочесть результат
        }
    }
}

На всякий случай мы также предусмотрели промежуточную печать с помощью Debug.Print – это очень удобно, поскольку когда плата подключена к компьютеру по USB, мы можем сразу видеть соответствующие строки прямо в Visual Studio. Также .NET MicroFramework поддерживает удобную отладку прямо из Visual Studio.

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

Если вы посмотрите внимательнее на историю данных – в некоторых местах реальные измерения отсутствуют. Это происходит потому, что мы не предусмотрели обработку исключений нашим контроллером. Если вдруг доступ в интернет временно теряется, приложение генерирут исключение, и бесконечный цикл останавливается. Для более устойчивой работы контроллера требуется более аккуратно подходить к вопросам исключительных ситуаций (Pull Request Welcome)!

Реализуем приложение для Windows Phone

Заключительным аккордом нашего упражнения станет реализация приложения для Windows Phone, которое позволит отображать текущую температуру. Будем использовать созданное нами WebAPI, которое возвращает необходимые данные в формате JSON. Для работы с такими данными нам потребуется подключить к вновь созданному проекту Windows Phone библиотеку JSON.NET (через NuGet – install-package Newtonsoft.Json).

Чтобы десериализовывать данные, нам потребуется класс, аналогичный созданному ранее WeatherRecord. Однако подключить WeatherLib напрямую к нашему проекту мы не можем, поскольку она не поддерживает платформу Windows Runtime, да и тащить за собой в приложения для смартфона всю облачную функциональность не очень правильно. Поэтому мы можем воспользоваться Cut-And-Paste, либо создать упрощенный вариант класса WeatherRecord SimpleWeatherRecord.

Для получения результата вызова JSON-метода создадим функцию GetReading, которая будет вызывать нужные метод API и возвращать массив последних измерений типа SimpleWeatherRecord:

public async Task<SimpleWeatherRecord[]> GetReading(string rtype)
{
    var cli = new HttpClient();
    var str = string.Format("http://weathermon.cloudapp.net/api/{0}", rtype);
    var s = await cli.GetStringAsync(new Uri(str));
    return Newtonsoft.Json.JsonConvert.DeserializeObject<SimpleWeatherRecord[]>(s);
}

Обратите внимание на естественную асинхронность метода, и на простоту преобразования из формата JSON в строго типизированный массив объектов – одной строчкой!

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

private async void Page_Loaded(object sender, RoutedEventArgs e)
{
    var ind = StatusBar.GetForCurrentView().ProgressIndicator;
    await ind.ShowAsync();
    ind.Text = "Getting temperature...";
    TempService.Text = "--"; TempOut.Text = "--";
    var temp = await GetReading("temperature");
    foreach(var x in temp)
    {
        if (x.iWeatherInfoSource == 0) // данные из сервиса
            TempService.Text = x.Reading.ToString();
        else // данные с термометра
            TempOut.Text = x.Reading.ToString();
    }
    await ind.HideAsync();
}

Полученные с сервиса данные обычно содержат массив из двух измерений – последнее измерение температуры на улице и из OpenWeatherMap. Пройдясь по массиву циклом, мы помещаем каждое из измерений в соответствующий ему TextBlock. Если вдруг за последние 10 минут не было сделано измерения (это возможно, если например наше устройство NetDuino перестало передавать данные в облако), то соответствующей записи не будет в массиве, и текстовое поле будет отображать значение “—“.

Конечно, было бы правильнее сразу сделать это приложение универсальным приложением Windows. Но я оставлю это в качеству упражнения читателям, и буду ждать ваших Pull Requests.

Заключение

Статья получилась достаточно длинной, но, надеюсь, полезной! Она показывает, как на одном языке программирования C# и в одной среде (Visual Studio) вы можете создавать совершенно разное программное обеспечение – облачные сервисы, приложения для смартфонов, программы для микроконтроллеров. Программист все время использует (почти) одну и ту же библиотеку базовых функций, удобную среду программирования и отладки. Всё это делает платформу Майкрософт приятной в использовании, настолько, что я решил потратить часть новогодних каникул на проектирование этой погодной станции.

В статье содержится ряд пожеланий по улучшению. Мне будет очень любопытно, если кто-то подключится и в режиме pull request улучшит или дополнит мои примеры. Давайте вместе улучшать примеры в лучших традициях Open Source!

Эта статья – по всей видимости не последняя про погодную станцию, я расскажу вам про обработку получаемых данных на F#, а также возможно про создание аналогичной погодной станции на Arduino, как на более распространенном контроллере. Если у вас есть пожелания или идеи, о чем лучше рассказать – пишите мне! В любом случае, мне будет очень интересно узнать (в комментариях или вконтакте), сколько человек дочитало статью до конца, что показалось полезным, а что – нет.

Счастливого кодирования!

2 comments