Миграция БД на Windows Azure SQL VM. BLOB Storage+REST

Теперь, когда мы имеем созданную в Облаке с установленным на нее SQL Server и умеем со стороны клиента с ним соединяться, как с локальным SQL Server, остается наполнить его данными. Предположим, в рамках гибридного сценария часть БД планируется перенести на Azure SQL VM. В этой статье будет рассматриваться сценарий, когда БД обособляется в виде файла (или нескольких файлов) посредством создания ее резервной копии, detach, data-tier application и т.д., файл доставляется на Azure SQL VM и превращается обратно в базу путем восстановления из бэкапа, attach, deploy/import data-tier application и т.д. Первое и последнее действие не вызывают вопросов у DBA. Осталось понять, как лучше доставить отчужденный файл с базой (.bak, .mdf, .bacpac, …) на облачную виртуалку с SQL Server.

Для примера перенесем любимую базу данных AdventureWorks в виде ее резервной копии:    

 

backup database AdventureWorks2012 to disk = 'c:\Temp\AdventureWorks2012.bak' with init, compression, stats = 10

Скрипт 1    

Файлы небольших размеров, как этот, можно, не мудрствуя лукаво, переносить обычным Copy/Paste на удаленный рабочий стол виртуальной машины SQL Server. Еще в голову приходит сделать на виртуалке папку общего доступа и скопировать туда, используя продвинутые средства копирования с возможностью распараллеливания, коррекции и взобновления в случае сбоев, а также передать файл по FTP. Эти способы очевидны. В данном посте мы задействуем иной способ: передадим файл бэкапа с локальной машины в Azure Storage в виде блоба и скачаем его оттуда внутрь облачной виртуалки. У нас уже имеется один Storage Account, созданный автоматически при создании виртуальной машины, в котором был автоматически контейнер по имени vhds, в котором в виде блоба хранится виртуальный диск нашей виртуальной машины. Для чистоты эксперимента создадим новый Storage Account, в том же центре обработки данных, что и облачная виртуалка, для сокращения накладных расходов.

image001

Рис.1

image002

Рис.2

image003

Рис.3

Отдельный Storage Account создавался довольно долго, обозначаясь при этом на портале однообразным статусом Creating… Нажатие Refresh в окне браузера обновило статус на ResolvingDns… Наконец (> 5 мин.), очередной Refresh показал, что Storage Account успешно создан:

image004

Рис.4

Внутри Azure Storage данные могут храниться в виде блобов или таблиц - см. Azure Data Management and Business Analytics. Таблицы не являются таблицами в строгом реляционном понимании. Это просто слабо структурированные наборы пар ключ-значение подобно тому, что когда-то называлось SQL Data Services - см. Введение в SQL Azure. По сравнению с SDS нынешние таблицы могут партиционироваться по ключу. Разные партиции хранятся на разных машинах в Облаке, чем достигается горизонтальное масштабирование, как при шардинге в случае SQL Azure Database. Блобы бывают блочные и страничные. Структура блочных блобов оптимизирована для подокового доступа, страничных – для случайного чтения/записи. Страничная структура позволяет запсать в блоб диапазон байтов. Подробно разница между ними объясняется здесь. Например, виртуальные диски хранятся как страничные блобы. Хранение блобов осуществляется внутри контейнеров, которые создаются в рамках Storage Account. Создадим в эккаунте tststorage контейнер под хранение AdventureWorks2012.bak:

image005

Рис.5

image006

Рис.6

Публичный контейнер позволяет видеть любому желающему содержащиеся в нем блобы. Публичный блоб позволяет любому желающему доступаться к любому блобу, но содержание контейнера недоступно. Наконец, частный контейнер означает, что для доступа к блобу потребуется указывать ключ Storage Account. Изменить впоследствии уровень доступа к контейнеру можно при помощи кнопки Edit Container:

image007

Рис.7

Сделанную в Скрипте 1 резервную копию базы для простоты будем загружать в Azure Storage как блочный блоб. Для операций над блобами в Облаке (равно как и над таблицами, и очередями) можно использовать REST, что позволяет работать напрямую через Интернет (HTTP Request/Response), привлекая широкий диапазон средств разработки. REST API для работы с блобами описывается здесь. Например, так можно посмотреть, какие блобы лежат в публичном контейнере: http://tststorage.blob.core.windows.net/container1?restype=container&comp=list

Рис.8

Контейнер container1 сейчас пуст. Чтобы загрузить в него AdventureWorks2012.bak, нужно использовать метод PUT:

using System;

using System.Net;

using System.IO;

using System.Security.Cryptography;

using System.Text;

using System.Globalization;

 

class Program

{

    static void Main(string[] args)

    {

        string fileFullName = @"c:\Temp\AdventureWorks2012.bak"; //@"c:\Temp\aaa.txt";

 

        string storageAccount = "tststorage";

        string containerName = "container1";

        string accessKey = "xws7rilyLjqdw8t75EHZbsIjbtwYDvpZw790lda0L1PgzEqKHxGNIDdCdQlPEvW5LdGWK/qOZFTs5xE4P93A5A==";

 

        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(String.Format("https://{0}.blob.core.windows.net/{1}/{2}", storageAccount, containerName, Path.GetFileName(fileFullName)));

 

        FileStream fs = File.OpenRead(fileFullName);

        byte[] fileContent = new byte[fs.Length];

        fs.Read(fileContent, 0, fileContent.Length);

        fs.Close();

 

        req.Method = "PUT";

        req.ContentLength = fileContent.Length;

        req.Headers.Add("x-ms-blob-type", "BlockBlob");

        req.Headers.Add("x-ms-date", DateTime.UtcNow.ToString("R", CultureInfo.InvariantCulture));

        req.Headers.Add("x-ms-version", "2011-08-18");

        string canonicalizedString = BuildCanonicalizedString(req, String.Format("/{0}/{1}/{2}", storageAccount, containerName, Path.GetFileName(fileFullName)));

        req.Headers["Authorization"] = CreateAuthorizationHeader(canonicalizedString, storageAccount, accessKey);

        req.Timeout = 100 * 60 * 1000;

        Stream s = req.GetRequestStream();

        s.Write(fileContent, 0, fileContent.Length);

 

      DateTime dt = DateTime.Now;

        req.GetResponse();

        System.Diagnostics.Debug.WriteLine(DateTime.Now - dt);

     

    }

 

    static string CreateAuthorizationHeader(string canonicalizedString, string storageAccount, string accessKey)

    {

   HMACSHA256 hmacSha256 = new HMACSHA256(Convert.FromBase64String(accessKey));

        byte[] dataToHMAC = Encoding.UTF8.GetBytes(canonicalizedString);

        string signature = Convert.ToBase64String(hmacSha256.ComputeHash(dataToHMAC));

        return "SharedKey " + storageAccount + ":" + signature;

    }

 

    static string BuildCanonicalizedString(HttpWebRequest req, string canonicalizedResource)

    {

        StringBuilder sb = new StringBuilder();

        sb.Append(req.Method + "\n\n\n");

        sb.Append(String.Format("{0}\n\n\n\n\n\n\n\n\n", req.ContentLength));

        sb.Append("x-ms-blob-type:" + req.Headers["x-ms-blob-type"] + '\n');

        sb.Append("x-ms-date:" + req.Headers["x-ms-date"] + '\n');

        sb.Append("x-ms-version:" + req.Headers["x-ms-version"] + '\n');

        sb.Append(canonicalizedResource);

        return sb.ToString();

    }

}

Скрипт 2

В этом коде все достаточно очевидно за исключением, пожалуй, одного момента. Несмотря на то, что контейнер container1 был создан (Рис.6) как публичный, запись блоба требует авторизации. Кто и какие операции может выполнять над блобами и контейнерами в зависимости от установленного уровня доступа описывается здесь. Вне зависимости от уровня доступа право на запись имеет владелец. Чтобы авторизоваться как владелец в HTTP Request требуется установить заголовок Authorization. Строка, записываемая в этот заголовок, в соответствии с требованиями схем аутентификации содержит подпись, которая представляет собой Hash-based Message Authentication Code (HMAC) канонизированной строки в кодировке UTF-8, где хэш вычисляется по алгоритму SHA256 на основе ключа доступа. Канонизированная строка складывается из метода доступа REST, размера загружаемого файла, типа блоба (x-ms-blob-type = блочный или страничный) даты/времени HTTP-запроса в формате UTC (x-ms-date), даты версии блобовского сервиса Azure, обслуживающего данный HTTP-запрос (x-ms-version) и др. Здесь не требуется блистать высоким программерским искусством, нужна лишь кропотливость и внимательность, т.к. малейшая неаккуратность при формировании канонизированной строки неумолимо влечет ошибку HTTP 403 Forbidden.

Ключи доступа (основной и запасной) формируются на этапе создания Storage Account (Рис.3), их можно посмотреть в свойствах контейнера:

image009

Рис.9

image010

Рис.10

Любой из них можно задавать в качестве accessKey для создания цифровой подписи при авторизации - HMACSHA256 hmacSha256 = new HMACSHA256(Convert.FromBase64String(accessKey));

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

Прочие комментарии.

· Если блоб с таким именем в контейнере существует, он молчаливо перетирается.

· Имя контейнера чувствительно к регистру.

· Время загрузки, очевидно, зависит от скорости сетки. Например, с работы данный 45-меговый бэкап залился со свистом за 00:01:07. Из дома получалось в разы медленнее.

В данном демонстрационном примере бэкап имел достаточно "детский" размер. Блочные блобы ограничены размером в 200 ГБ. Блочный блоб размером менее 64 МБ может быть загружен одной операцией записи, как мы наблюдали в примере Скрипт 2. В противном случае следует разбивать его на куски и загружать поблочно с использованием методов Put Block / Put Block List. При заливке в Azure Storage крупных файлов следует применять страничные блобы. Страничный блоб состоит из 512-байтных страниц, его максимальный размер составляет 1 ТБ. Пример на запись/чтение диапазона страниц страничного блоба приводится здесь.

 

 

Алексей Шуленин