【问题标题】:Can I create and Save a file in Azure storage at the same time?我可以同时在 Azure 存储中创建和保存文件吗?
【发布时间】:2020-05-19 14:54:53
【问题描述】:

我正在尝试创建 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。

我的“问题”是为此我需要先将整个文件保存在内存中,然后才开始复制(如果文件太大并且机器内存不足,这可能是一个问题)。

理想情况下,我可以直接写入 azure,或者在我填满内存流缓冲区后立即将其复制到 azure,然后在其顶部再次写入,而不是在内存流缓冲区中分配更多空间。

有没有办法直接写入 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 应该执行良好的追加,但似乎并非如此。

关于如何稍微提高写入速度的任何想法?

【问题讨论】:

  • 也许是 BlockBlobClient 类? docs.microsoft.com/en-us/dotnet/api/…
  • 一般来说,AppendText 和 Append 方法有一个最大限制 - 每个追加 100MB。还要确保在最后添加 blob.Flush 调用以实际执行上传。

标签: c# stream azure-blob-storage


【解决方案1】:

当然可以这样做。一种解决方案是使用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 存储。比如:

    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());
    }

更新 2

另一种选择是将每一行作为单独的块上传,然后最终提交块列表。但是请记住,一个 blob 中只能有 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);
    }

【讨论】:

  • 通过查看,它似乎使用了两倍的内存,因为文件现在存储在 byte[] 缓冲区和 MemoryStream ms 中
  • 这仍然使我们首先在字符串构建器中预构建整个文件。如果那个 UploadText 方法可以选择追加而不是重写,我可以直接写入 azure,那将是理想的场景
  • 你的 blob 有多大?还有另一种方法,但我认为这将是一个过度设计的解决方案。
  • 目前只有 100mb。我只是想在节省资源的同时节省资源。您的解决方案是让我使用 blob.AppendText(line);而不是 UploadText(line)。我现在正在使用 blob = container.GetAppendBlobReference($"{fileName}.csv") 并尝试直接在其中写入...但我得到一个 InvalidOperationException:blob 引用的 Blob 类型与 blob 类型的 blob 不匹配斑点。
  • 我放弃了:)。请发布您最终实施的解决方案。我很想知道这一点。
【解决方案2】:

我要试一试,主要基于Gaurav Mantri-AIS' answer。因为我认为你们正在做某事。

让我们在这里共同努力...一方面,您希望尽快写入 Blob 以限制内存使用。另一方面,我们不想写每一行,因为这超出了块限制。因此,在将其写入 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 函数存储绑定使您能够将 blob 绑定到 Stream。这让我有两件事需要考虑:

  • 您可以为此使用 Azure 函数
  • 应该可以获得对 Blob 的流引用

编辑:
我深入研究了azure-webjobs-sdk 的源代码,发现它使用了CloudBlobStream。尽管它被标记为旧版,您仍然可以通过在CloudBlockBlob 上调用OpenWriteAsync 来获得CloudBlobStream。我没有时间测试示例,但我确实在 SO:Uploading a file to Azure Blob on the fly 上找到了这个示例。

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;
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-07-15
    • 1970-01-01
    • 2011-10-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-18
    • 1970-01-01
    相关资源
    最近更新 更多