Могу ли я одновременно создать и сохранить файл в хранилище Azure? - PullRequest
1 голос
/ 09 мая 2020

Я пытаюсь создать файл CSV и импортировать его в учетную запись хранения Azure.

public static void ExportCsvToStorageAccount(string fileName, CloudBlobContainer container, IEnumerable<ReportRow> reportEntries)
{
    using (var ms = new MemoryStream())
    {
        using (var file = new StreamWriter(ms))
        {
            file.WriteLine("Date,StoreId,ItemId,SalesQuantity");

            foreach (var row in reportEntries)
            {
                var line = $"\"{row.Date}\",\"{row.StoreId}\",\"{row.ItemId}\",\"{row.SalesQuantity}\"";
                file.WriteLine(line);
            }

            var blockBlob = container.GetBlockBlobReference($"{fileName}.csv");
            ms.Position = 0;
            blockBlob.UploadFromStream(ms);
        }
    }
}

Я создаю файл в памяти, а затем копирую его и загружаю в azure.

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

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

Есть ли способ записать прямо в Azure? (Цель состоит в том, чтобы спасти барана)

Изменить:

С вводом ответа Gaurav Mantri-AIS я придумал это (потому что у меня больше, чем 50000 записей, что является пределом блоков),

public static void ExportCSVToStorageAccount(string fileName, CloudBlobContainer container, IEnumerable<RawReportRow> reportEntries)
{
    var blob = container.GetAppendBlobReference($"{fileName}.csv");
    blob.CreateOrReplace();

    blob.AppendText($"Date,StoreId,ItemId,SalesQuantity{Environment.NewLine}");
    foreach (var row in reportEntries)
    {
        var line = $"\"{row.Date}\",\"{row.StoreId}\",\"{row.ItemId}\",\"{row.SalesQuantity}\"{Environment.NewLine}";
        blob.AppendText(line);
    }
}

Проблема с этим решением в том, что оно занимает слишком много времени, от 5 минут до более часа. Я, вероятно, делаю что-то не так, поскольку AppendBlob должен хорошо работать с добавлением, но, похоже, это не так.

Есть идеи, как немного улучшить скорость записи?

Ответы [ 2 ]

0 голосов
/ 19 мая 2020

Я собираюсь получить go, в значительной степени основанный на ответе Gaurav Mantri-AIS. Потому что я думаю, что вы, ребята, что-то поняли.

Давайте объединим усилия здесь ... С одной стороны, вы хотели бы как можно скорее написать в Blob, чтобы ограничить использование памяти. С другой стороны, мы не хотим писать в каждой строке , так как это превышает лимит блока. Поэтому нам нужно иметь в памяти X записей , прежде чем записывать их в большой двоичный объект.

Я пробую здесь использовать псевдокод с X со значением 50. Я думаю, что это значение можно (и нужно) оптимизировать для использования памяти, производительности и количества блоков:

public static void ExportCsvToStorageAccount(string fileName, CloudBlobContainer container, IEnumerable<string> reportEntries)
{
    List<string> blockIds = new List<string>();
    CloudBlockBlob blob = container.GetBlockBlobReference(fileName);
    int counter = 0;
    StringBuilder builder = new StringBuilder();
    foreach (var row in reportEntries)
    {
        builder.Append($"\"{row.Date}\",\"{row.StoreId}\",\"{row.ItemId}\",\"{row.SalesQuantity}\"");
        counter++;

        if (counter % 50 == 0)
        {
            var blockId = Convert.ToBase64String(Encoding.UTF8.GetBytes(counter.ToString("d6")));
            blob.PutBlock(blockId, new MemoryStream(Encoding.UTF8.GetBytes(line)), string.Empty);
            builder = new StringBuilder();
            blockIds.Add(blockId);
        }
    }
    // Check if there's anything still in the String Builder and write it
    if (builder.Length != 0)
    {
        var blockId = Convert.ToBase64String(Encoding.UTF8.GetBytes(counter.ToString("d6")));
        blob.PutBlock(blockId, new MemoryStream(Encoding.UTF8.GetBytes(line)), string.Empty);             
    }
    blob.PutBlockList(blockIds);
}

Еще одна вещь, которую следует принять во внимание, - это тот факт, что Azure привязки функций для хранилища позволяют привязать большой двоичный объект к Stream. Это дает мне возможность подумать о двух вещах:

  • вы можете использовать Azure для этого
  • должно быть возможно получить ссылку на поток на Blob

РЕДАКТИРОВАТЬ:
Я погрузился в исходный код azure-webjobs-sdk и обнаружил, что он использует CloudBlobStream. Несмотря на то, что он помечен как устаревший, вы все равно можете получить CloudBlobStream, позвонив OpenWriteAsync на CloudBlockBlob. У меня не было времени протестировать пример, но я нашел этот пример здесь, на SO: Загрузка файла в Azure Blob на лету .

public async Task<Stream> GetWriteStreamAsync(string storagePath, string contentType)
{
    var blockBlob = blobContainer.GetBlockBlobReference(storagePath);
    blockBlob.Properties.ContentType = contentType;
    CloudBlobStream bb = await blockBlob.OpenWriteAsync();
    return bb;
}
0 голосов
/ 09 мая 2020

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

Вот пример кода (хотя и непроверенный):

    public static void ExportCsvToStorageAccount(string fileName, CloudBlobContainer container, IEnumerable<ReportRow> reportEntries)
    {
        using (var ms = new MemoryStream())
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("Date,StoreId,ItemId,SalesQuantity");
            foreach (var row in reportEntries)
            {
                var line = $"\"{row.Date}\",\"{row.StoreId}\",\"{row.ItemId}\",\"{row.SalesQuantity}\"";
                sb.AppendLine(line);
            }
            var buffer = Encoding.UTF8.GetBytes(sb.ToString());
            ms.Write(buffer, 0, buffer.Length);
            var blockBlob = container.GetBlockBlobReference($"{fileName}.csv");
            ms.Position = 0;
            blockBlob.UploadFromStream(ms);
        }
    }

ОБНОВЛЕНИЕ

Предполагая, что вы используете SDK версии 9.3.3, вы можете использовать метод UploadText и напрямую загрузить строку в Azure Storage. Что-то вроде:

    public static void ExportCsvToStorageAccount(string fileName, CloudBlobContainer container, IEnumerable<string> reportEntries)
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendLine("Date,StoreId,ItemId,SalesQuantity");
        foreach (var row in reportEntries)
        {
            var line = $"\"{row.Date}\",\"{row.StoreId}\",\"{row.ItemId}\",\"{row.SalesQuantity}\"";
            sb.AppendLine(line);
        }
        var blockBlob = container.GetBlockBlobReference($"{fileName}.csv");
        blockBlob.UploadText(sb.ToString());
    }

UPDATE 2

Еще одна альтернатива - загрузить каждую строку как отдельный блок, а затем окончательно зафиксировать список блоков. Однако имейте в виду, что в большом двоичном объекте может быть только 50000 блоков, и этот метод не сработает, если в ваших данных более 50000 записей. Чтобы обойти это ограничение, вы можете объединить определенные записи и сохранить их как блок.

Вот пример кода:

    public static void ExportCsvToStorageAccount(string fileName, CloudBlobContainer container, IEnumerable<string> reportEntries)
    {
        List<string> blockIds = new List<string>();
        CloudBlockBlob blob = container.GetBlockBlobReference(fileName);
        int counter = 0;
        foreach (var row in reportEntries)
        {
            var line = $"\"{row.Date}\",\"{row.StoreId}\",\"{row.ItemId}\",\"{row.SalesQuantity}\"";
            var blockId = Convert.ToBase64String(Encoding.UTF8.GetBytes(counter.ToString("d6")));
            blob.PutBlock(blockId, new MemoryStream(Encoding.UTF8.GetBytes(line)), string.Empty);
            blockIds.Add(blockId);
            counter++;
        }
        blob.PutBlockList(blockIds);
    }
...