并发访问的 Azure CloudAppendBlob 错误
Azure CloudAppendBlob errors with concurrent access
我的理解是 Azure CloudAppendBlob 不会出现并发问题,因为您只能附加到此 blob 存储并且不需要比较 E-tags。如此 post 所述:
具体来说:
In addition, Append Blob supports having multiple clients writing to the same blob without any need for synchronization (unlike block and page blob)
然而,以下单元测试引发:
412 the append position condition specified was not met.
堆栈跟踪
Microsoft.WindowsAzure.Storage.Blob.BlobWriteStream.Flush()
Microsoft.WindowsAzure.Storage.Blob.BlobWriteStream.Commit()
Microsoft.WindowsAzure.Storage.Blob.CloudAppendBlob.UploadFromStreamHelper
Microsoft.WindowsAzure.Storage.Blob.CloudAppendBlob.AppendFromStream
Microsoft.WindowsAzure.Storage.Blob.CloudAppendBlob.AppendFromByteArray
Microsoft.WindowsAzure.Storage.Blob.CloudAppendBlob.AppendText
这是单元测试。也许该服务会处理来自不同上下文的请求,但不会像这样并行处理?
[TestMethod]
public void test_append_text_concurrency()
{
AppendBlobStorage abs = new AppendBlobStorage(new TestConnectConfig(), "testappendblob");
string filename = "test-concurrent-blob";
abs.Delete(filename);
Parallel.Invoke(
() => { abs.AppendText(filename, "message1\r\n"); },
() => { abs.AppendText(filename, "message2\r\n"); }
);
string text = abs.ReadText(filename);
Assert.IsTrue(text.Contains("message1"));
Assert.IsTrue(text.Contains("message2"));
}
AppendBlobStorage 中的方法
public void AppendText(string filename, string text)
{
CloudAppendBlob cab = m_BlobStorage.BlobContainer.GetAppendBlobReference(filename);
// Create if it doesn't exist
if (!cab.Exists())
{
try
{
cab.CreateOrReplace(AccessCondition.GenerateIfNotExistsCondition(), null, null);
}
catch { }
}
// Append the text
cab.AppendText(text);
}
也许我遗漏了什么。我尝试这样做的原因是因为我有多个 Web 作业,它们都可以写入此附加 blob,我认为这就是它的设计目的?
经过更多搜索,这似乎是一个实际问题。
我猜 AppendBlobStorage 是相当新的。 (目前 AppendBlobStorage 还存在其他问题。请参阅
无论如何,我通过使用 AppendBlock 变体而不是此处建议的 AppendText 解决了这个问题:
https://azurekan.wordpress.com/2015/09/08/issues-with-adding-text-to-azure-storage-append-blob/
通过上面定义的单元测试的 appendtext 方法的更改
public void AppendText(string filename, string text)
{
if (string.IsNullOrWhiteSpace(filename))
throw new ArgumentException("filename cannot be null or empty");
if (!string.IsNullOrEmpty(text))
{
CloudAppendBlob cab = m_BlobStorage.BlobContainer.GetAppendBlobReference(filename);
// Create if it doesn't exist
if (!cab.Exists())
{
try
{
cab.CreateOrReplace(AccessCondition.GenerateIfNotExistsCondition(), null, null);
}
catch (StorageException) { }
}
// use append block as append text seems to have an error at the moment.
using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(text)))
{
cab.AppendBlock(ms);
}
}
}
对于需要更通用的解决方案的人,我创建了一个扩展方法:
public static async Task AppendTextConcurrentAsync(this CloudAppendBlob appendBlob, string content)
{
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)))
{
await appendBlob.AppendBlockAsync(stream);
}
}
此解决方案与您在 CloudAppendBlob
上使用其他 Append*
方法的方式更加一致。
您可以尝试 AppendTextAsync。在类似的情况下,这似乎对我有用。使用 lock 关键字也可能有效。
public void Log(string message)
{
lock (this.appendBlob)
{
appendBlob.AppendText(string.Format("[{0:s}] {1}{2}", DateTime.Now, message, Environment.NewLine));
}
}
class CloudAppendBlob 的追加方法,包括
AppendBlock/AppendFromByteArray/AppendFromFile/AppendFromStream/AppendText
基本上他们都会使用相同的 rest api 端点。阅读文档:
https://docs.microsoft.com/en-us/rest/api/storageservices/append-block
但只有AppendBlock应该在multi-writer场景下使用,其他的都应该在single-writer场景下使用。原因是:AppendBlock 不会发送 header x-ms-blob-append-offset PUT HTTP 请求。
header x-ms-blob-append-offset 基本上说,必须将此块数据附加到 blob 的此偏移量处。
因此对于 AppendBlock,http 请求如下所示:
PUT https://test.blob.core.windows.net/test/20180323.log?comp=appendblock HTTP/1.1
User-Agent: Azure-Storage/9.1.0 (.NET CLR 4.0.30319.42000; Win32NT 6.2.9200.0)
x-ms-version: 2017-07-29
x-ms-client-request-id: bb7f5a93-191d-40f9-8b92-4ec0476be920
x-ms-date: Fri, 23 Mar 2018 20:21:29 GMT
Authorization: SharedKey XXXXX
Host: test.blob.core.windows.net
Content-Length: 99
对于所有其他附加方法,它将发送 header x-ms-blob-append-offset。这个 header 的值应该是附加前blob的当前长度。那么图书馆如何知道价值呢?它实际上会发送一个 HEAD http 请求来获取该信息
HEAD http://test.blob.core.windows.net/test/20180323.log HTTP/1.1
User-Agent: Azure-Storage/9.1.0 (.NET CLR 4.0.30319.42000; Win32NT 6.2.9200.0)
x-ms-version: 2017-07-29
x-ms-client-request-id: 1cdb3731-9d72-41ab-afee-d4f462e9b0c2
x-ms-date: Fri, 23 Mar 2018 20:29:19 GMT
Authorization: SharedKey XXXX
Host: test.blob.core.windows.net
响应 header Content-Length 的值将是 header x-ms-blob-append-offset 在以下 PUT http 请求中:
PUT http://test.blob.core.windows.net/test/20180323.log?comp=appendblock HTTP/1.1
User-Agent: Azure-Storage/9.1.0 (.NET CLR 4.0.30319.42000; Win32NT 6.2.9200.0)
x-ms-version: 2017-07-29
x-ms-blob-condition-appendpos: 1287
x-ms-client-request-id: 1cdb3731-9d72-41ab-afee-d4f462e9b0c2
x-ms-date: Fri, 23 Mar 2018 20:29:20 GMT
Authorization: SharedKey XXXXX
Host: test.blob.core.windows.net
Content-Length: 99
所以原来的问题,当两个并行任务同时调用AppendText时,两个任务很可能会发送HEAD http请求来获取blob的当前长度, 这将是相同的。然后先发送 PUT http 请求的任务会成功,但是稍后发送 PUT http 请求的任务将失败,因为 blob 的长度已经改变,并且该偏移量已经被第一个 PUT http 请求占用。
因此,如果您有 multi-writer 场景,AppendBlock 是目前有效的方法。但你必须知道
- 您将无法控制块在 blob 中的位置
- blob 块有大小限制(我认为是 4M)
- 如果使用AppendBlock上传超过4M的数据,请求会失败,响应:HTTP/1.1 413 请求body太大,超过最大允许限制
- 如果你使用除AppendBlock以外的其他方法上传大数据,它会发送一个HEAD http请求获取blob长度,然后自动将数据拆分成多个PUT http要求。块大小可以由 CloudAppendBlob.StreamWriteSizeInBytes 控制。如果不设置,默认为4M。
- 所以正如名字AppendBlock所暗示的,它只能追加一个块,不能超过一个块。所以如果你想上传一个大的 blob,你已经自己拆分了数据。但是如果你有一个 multi-writer 的场景,你不能保证分裂的块会在 blob 中在一起。
我的理解是 Azure CloudAppendBlob 不会出现并发问题,因为您只能附加到此 blob 存储并且不需要比较 E-tags。如此 post 所述:
具体来说:
In addition, Append Blob supports having multiple clients writing to the same blob without any need for synchronization (unlike block and page blob)
然而,以下单元测试引发:
412 the append position condition specified was not met.
堆栈跟踪
Microsoft.WindowsAzure.Storage.Blob.BlobWriteStream.Flush()
Microsoft.WindowsAzure.Storage.Blob.BlobWriteStream.Commit()
Microsoft.WindowsAzure.Storage.Blob.CloudAppendBlob.UploadFromStreamHelper
Microsoft.WindowsAzure.Storage.Blob.CloudAppendBlob.AppendFromStream
Microsoft.WindowsAzure.Storage.Blob.CloudAppendBlob.AppendFromByteArray
Microsoft.WindowsAzure.Storage.Blob.CloudAppendBlob.AppendText
这是单元测试。也许该服务会处理来自不同上下文的请求,但不会像这样并行处理?
[TestMethod]
public void test_append_text_concurrency()
{
AppendBlobStorage abs = new AppendBlobStorage(new TestConnectConfig(), "testappendblob");
string filename = "test-concurrent-blob";
abs.Delete(filename);
Parallel.Invoke(
() => { abs.AppendText(filename, "message1\r\n"); },
() => { abs.AppendText(filename, "message2\r\n"); }
);
string text = abs.ReadText(filename);
Assert.IsTrue(text.Contains("message1"));
Assert.IsTrue(text.Contains("message2"));
}
AppendBlobStorage 中的方法
public void AppendText(string filename, string text)
{
CloudAppendBlob cab = m_BlobStorage.BlobContainer.GetAppendBlobReference(filename);
// Create if it doesn't exist
if (!cab.Exists())
{
try
{
cab.CreateOrReplace(AccessCondition.GenerateIfNotExistsCondition(), null, null);
}
catch { }
}
// Append the text
cab.AppendText(text);
}
也许我遗漏了什么。我尝试这样做的原因是因为我有多个 Web 作业,它们都可以写入此附加 blob,我认为这就是它的设计目的?
经过更多搜索,这似乎是一个实际问题。
我猜 AppendBlobStorage 是相当新的。 (目前 AppendBlobStorage 还存在其他问题。请参阅
无论如何,我通过使用 AppendBlock 变体而不是此处建议的 AppendText 解决了这个问题:
https://azurekan.wordpress.com/2015/09/08/issues-with-adding-text-to-azure-storage-append-blob/
通过上面定义的单元测试的 appendtext 方法的更改
public void AppendText(string filename, string text)
{
if (string.IsNullOrWhiteSpace(filename))
throw new ArgumentException("filename cannot be null or empty");
if (!string.IsNullOrEmpty(text))
{
CloudAppendBlob cab = m_BlobStorage.BlobContainer.GetAppendBlobReference(filename);
// Create if it doesn't exist
if (!cab.Exists())
{
try
{
cab.CreateOrReplace(AccessCondition.GenerateIfNotExistsCondition(), null, null);
}
catch (StorageException) { }
}
// use append block as append text seems to have an error at the moment.
using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(text)))
{
cab.AppendBlock(ms);
}
}
}
对于需要更通用的解决方案的人,我创建了一个扩展方法:
public static async Task AppendTextConcurrentAsync(this CloudAppendBlob appendBlob, string content)
{
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(content)))
{
await appendBlob.AppendBlockAsync(stream);
}
}
此解决方案与您在 CloudAppendBlob
上使用其他 Append*
方法的方式更加一致。
您可以尝试 AppendTextAsync。在类似的情况下,这似乎对我有用。使用 lock 关键字也可能有效。
public void Log(string message)
{
lock (this.appendBlob)
{
appendBlob.AppendText(string.Format("[{0:s}] {1}{2}", DateTime.Now, message, Environment.NewLine));
}
}
class CloudAppendBlob 的追加方法,包括
AppendBlock/AppendFromByteArray/AppendFromFile/AppendFromStream/AppendText
基本上他们都会使用相同的 rest api 端点。阅读文档: https://docs.microsoft.com/en-us/rest/api/storageservices/append-block
但只有AppendBlock应该在multi-writer场景下使用,其他的都应该在single-writer场景下使用。原因是:AppendBlock 不会发送 header x-ms-blob-append-offset PUT HTTP 请求。
header x-ms-blob-append-offset 基本上说,必须将此块数据附加到 blob 的此偏移量处。
因此对于 AppendBlock,http 请求如下所示:
PUT https://test.blob.core.windows.net/test/20180323.log?comp=appendblock HTTP/1.1
User-Agent: Azure-Storage/9.1.0 (.NET CLR 4.0.30319.42000; Win32NT 6.2.9200.0)
x-ms-version: 2017-07-29
x-ms-client-request-id: bb7f5a93-191d-40f9-8b92-4ec0476be920
x-ms-date: Fri, 23 Mar 2018 20:21:29 GMT
Authorization: SharedKey XXXXX
Host: test.blob.core.windows.net
Content-Length: 99
对于所有其他附加方法,它将发送 header x-ms-blob-append-offset。这个 header 的值应该是附加前blob的当前长度。那么图书馆如何知道价值呢?它实际上会发送一个 HEAD http 请求来获取该信息
HEAD http://test.blob.core.windows.net/test/20180323.log HTTP/1.1
User-Agent: Azure-Storage/9.1.0 (.NET CLR 4.0.30319.42000; Win32NT 6.2.9200.0)
x-ms-version: 2017-07-29
x-ms-client-request-id: 1cdb3731-9d72-41ab-afee-d4f462e9b0c2
x-ms-date: Fri, 23 Mar 2018 20:29:19 GMT
Authorization: SharedKey XXXX
Host: test.blob.core.windows.net
响应 header Content-Length 的值将是 header x-ms-blob-append-offset 在以下 PUT http 请求中:
PUT http://test.blob.core.windows.net/test/20180323.log?comp=appendblock HTTP/1.1
User-Agent: Azure-Storage/9.1.0 (.NET CLR 4.0.30319.42000; Win32NT 6.2.9200.0)
x-ms-version: 2017-07-29
x-ms-blob-condition-appendpos: 1287
x-ms-client-request-id: 1cdb3731-9d72-41ab-afee-d4f462e9b0c2
x-ms-date: Fri, 23 Mar 2018 20:29:20 GMT
Authorization: SharedKey XXXXX
Host: test.blob.core.windows.net
Content-Length: 99
所以原来的问题,当两个并行任务同时调用AppendText时,两个任务很可能会发送HEAD http请求来获取blob的当前长度, 这将是相同的。然后先发送 PUT http 请求的任务会成功,但是稍后发送 PUT http 请求的任务将失败,因为 blob 的长度已经改变,并且该偏移量已经被第一个 PUT http 请求占用。
因此,如果您有 multi-writer 场景,AppendBlock 是目前有效的方法。但你必须知道
- 您将无法控制块在 blob 中的位置
- blob 块有大小限制(我认为是 4M)
- 如果使用AppendBlock上传超过4M的数据,请求会失败,响应:HTTP/1.1 413 请求body太大,超过最大允许限制
- 如果你使用除AppendBlock以外的其他方法上传大数据,它会发送一个HEAD http请求获取blob长度,然后自动将数据拆分成多个PUT http要求。块大小可以由 CloudAppendBlob.StreamWriteSizeInBytes 控制。如果不设置,默认为4M。
- 所以正如名字AppendBlock所暗示的,它只能追加一个块,不能超过一个块。所以如果你想上传一个大的 blob,你已经自己拆分了数据。但是如果你有一个 multi-writer 的场景,你不能保证分裂的块会在 blob 中在一起。