如何解决Google Cloud Storage 不是强一致性,在可恢复上传完成后 500 毫秒返回 404
我正在使用 Cloud Storage 制作文件上传器,该文件上传器出现不一致的行为,这似乎与文档相悖。
当您将对象上传到 Cloud Storage 并收到成功响应时,该对象可立即从 Google 提供服务的任何位置进行下载和元数据操作。无论您是创建新对象还是替换现有对象,都是如此。由于上传是高度一致的,因此您永远不会收到 404 Not Found 响应或写后读取或元数据更新后读取操作的陈旧数据。 https://cloud.google.com/storage/docs/consistency#strongly_consistent_operations
...但是如果我在上传后立即阅读它会收到 404。
流程如下:
- 我的后端 NodeJS API 启动一个可恢复的上传,创建一个到存储桶的会话 URI
- 然后用户通过从浏览器到会话 URI 的 PUT 将文件直接上传到 GCS
- 前端向我的 API 发布更新,说明上传已完成。
- 然后我的 API 尝试下载与流相同的文件并摄取它
我一切正常,但后来发现当上传新文件时(即存储桶中尚不存在),上传完成(第 2 步)和读取成功之间需要 500 毫秒的延迟(第四步)。如果我立即执行此操作,则会收到 404。
The docs states 通常可以立即上传,除非有缓存。
重要提示:可公开读取的缓存对象可能不会表现出强一致性。 See Cache control and consistency 了解详情。
我使用 XMLHttpRequest
将文件上传到 GCS,并使用 load
事件检测上传完成。 From what I read 这应该意味着已收到 200 响应,因此文件已就位。尽管调试加载事件显示它只是另一个 100% 的“进度”事件。
我的尝试
解决方法是在加载事件处理程序的最终回调中添加一个 setTimeout(done,500)
,然后在第 3 步调用我的 API。
我已经对此进行了数十次测试,它是可靠的、可重复的,其中 0 - 400 毫秒失败,而大约 500 毫秒以上总是“修复”它。
我已经尝试添加 cache control headers to the original POST as recommended,它将上传会话设置为没有缓存 - 添加 no-store
似乎是正确的。我可以在 PUT 的标头中看到这一点(它实际上在响应中放置了比我设置的更多的无缓存选项)。这似乎根本不影响行为。
如果文件已经存在于bucked中并被覆盖,则不会发生这种情况。 (虽然我猜如果我上传不同的文件,内容中可能仍然存在竞争条件)。
我似乎无法捕捉到异常,所以我真的不知道对 GCS 的哪个调用正在返回 404,无论是 bucket.file()
还是 remoteFile.createReadStream()
或稍后从中读取(这是很深的在我将可读流传递到的其他一些库中)。
我还没有尝试过尝试/重试循环,因为我什至无法捕捉到错误。如果我不能保证一致的行为,这就是我想要做的。
我尝试过使用 the gcs-resumable-upload package 和直接使用 Storage.File,两者的效果似乎相同。
开始上传的NodeJS API是这样的:
1a) gcs-resumable-upload
版本
const {createURI} = require('gcs-resumable-upload');
const sessionURI = await createURI({
bucket: bucketName,file: filename,origin: origin,customrequestOptions: { //todo: this doesn't fixe the race
headers: {
'Cache-Control': 'no-store',},});
1b) Storage.File
版本
const {Storage,File} = require('@google-cloud/storage');
const storage = new Storage();
const bucket = storage.bucket(bucketName);
const file = bucket.file(filename);
const resp = await file.createResumableupload({origin: origin})
const sessionURI = resp[0];
var reader = new FileReader();
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener("load",function(e){
setTimeout(done,500);// todo I get 404s in the next step without 500ms delay?
// done(); // fails
},false);
xhr.open("PUT",sessionUrl);
xhr.overrideMimeType('text/plain; charset=x-user-defined-binary');
reader.onload = function(evt) {
xhr.send(evt.target.result);
};
reader.readAsBinaryString(file);
- 后端 NodeJS API 基本上就是这样做的(带有一些错误处理):
const {Storage} = require('@google-cloud/storage');
const storage = new Storage();
const bucket = storage.bucket(bucketName);
let remoteFile,stream;
remoteFile = bucket.file(filename);
stream = remoteFile.createReadStream()
stream
然后返回并发送到使用它来读取内容的库。
这就是它出错的地方,尽管它在一个滴答事件中异步出错,而且我还没有设法从任何地方尝试/捕获它(这有点奇怪)。
错误堆栈为:
<ref *2> ApiError: No such object: MY-BUCKETNAME/MY-FILENAME
at new ApiError (node_modules/@google-cloud/common/build/src/util.js:59:15)
at Util.parseHttpRespMessage (node_modules/@google-cloud/common/build/src/util.js:161:41)
at Util.handleResp (node_modules/@google-cloud/common/build/src/util.js:135:76)
at Duplexify.<anonymous> (node_modules/@google-cloud/storage/build/src/file.js:880:31)
at Duplexify.emit (events.js:314:20)
at Duplexify.EventEmitter.emit (domain.js:548:15)
at Passthrough.emit (events.js:314:20)
at Passthrough.EventEmitter.emit (domain.js:548:15)
at onResponse (node_modules/retry-request/index.js:208:19)
at Passthrough.<anonymous> (node_modules/retry-request/index.js:155:11)
at Passthrough.emit (events.js:326:22)
at Passthrough.EventEmitter.emit (domain.js:548:15)
at node_modules/teeny-request/build/src/index.js:184:27
at processticksAndRejections (internal/process/task_queues.js:93:5)
{
code: 404,errors: [],response: <ref *1> Passthrough {
_readableState: ReadableState {
objectMode: false,highWaterMark: 16384,buffer: BufferList { head: null,tail: null,length: 0 },length: 0,pipes: [],flowing: false,ended: true,endEmitted: true,reading: false,sync: false,needReadable: false,emittedReadable: false,readableListening: false,resumeScheduled: false,errorEmitted: false,emitClose: true,autoDestroy: true,destroyed: true,errored: null,closed: true,closeEmitted: true,defaultEncoding: 'utf8',awaitDrainWriters: Set(0) {},multiAwaitDrain: true,readingMore: false,decoder: null,encoding: null,[Symbol(kPaused)]: true
},_events: [Object: null prototype] {
prefinish: [Function: prefinish],error: [Array],close: [Array],end: [Function: onend],finish: [Function: onfinish]
},_eventsCount: 5,_maxListeners: undefined,_writableState: WritableState {
objectMode: false,finalCalled: false,needDrain: false,ending: true,finished: true,decodeStrings: true,writing: false,corked: 0,bufferProcessing: false,onwrite: [Function: bound onwrite],writecb: null,writelen: 0,afterWriteTickInfo: null,buffered: [],bufferedindex: 0,allBuffers: true,allNoop: true,pendingcb: 0,prefinished: true,closed: true
},allowHalfOpen: true,statusCode: 404,statusMessage: 'Not Found',request: {
agent: false,headers: [Object],href: 'https://storage.googleapis.com/storage/v1/b/MY-BUCKETNAME/o/MY-FILENAME?alt=media'
},body: [Circular *1],headers: {
'alt-svc': 'h3-29=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"','cache-control': 'private,max-age=0',connection: 'close','content-length': '55','content-type': 'text/html; charset=UTF-8',date: 'Tue,23 Mar 2021 08:08:50 GMT',expires: 'Tue,server: 'UploadServer',vary: 'Origin,X-Origin','x-guploader-uploadid': 'ABg5-Uz0P1kWSLFABXOpJ_mbQY5-4wEnMekQdubli1S4aYDWoIgqVKG1M5zlZ_ePd0iJDlzCl_ThYvmfpvcXpgwCcnN993kZog'
},toJSON: [Function: toJSON],[Symbol(kCapture)]: false,[Symbol(kTransformState)]: {
afterTransform: [Function: bound afterTransform],needTransform: false,transforming: false,writechunk: null,writeencoding: 'buffer'
}
},domainEmitter: Passthrough {
_readableState: ReadableState {
objectMode: false,flowing: true,ended: false,endEmitted: false,reading: true,needReadable: true,errorEmitted: true,errored: [Circular *2],closeEmitted: false,awaitDrainWriters: null,multiAwaitDrain: false,[Symbol(kPaused)]: false
},reading: [Function: makeRequest],data: [Function (anonymous)],end: [Function (anonymous)]
},_eventsCount: 4,ending: false,finished: false,sync: true,prefinished: false,_read: [Function: bound ],_write: [Function (anonymous)],needTransform: true,writeencoding: null
}
},domainThrown: false
}
解决方法
您使用的是 Cloud CDN 还是任何第三方 CDN?
询问有两个原因:
Cloud Storage 也兼容第三方 CDN
为了在向用户提供内容时获得最佳性能,我们 建议将 Cloud Storage 与 Cloud CDN 结合使用。
我建议首先查看您是否已经有任何可能影响对象缓存并因此导致您提到的延迟的 CDN。
如果不是这种情况,我建议使用 Cloud CDN 作为文档状态,结合 Cloud Storage 可提供最佳性能。除了可能带来的优化性能外,Cloud CDN 还具有一些您可能会感兴趣的 caching settings。
最后,您提到了在 HTTP 请求中使用 no-store
标志,但请注意以下几点:
注意:Cache-Control 也是您可以在 HTTP 中指定的标头 对对象的请求;但是,Cloud Storage 会忽略此标头,并且 根据存储的元数据设置响应 Cache-Control 标头 价值。
,此后我发现错误发生在第一个流 read 发生时,在我的情况下,它位于第三方库的深处,该库将 on('data') 事件附加到我的流中通过了。所以 bucket.file(path)
可以工作,file.createReadStream()
可以工作并且不会出错,但是一旦您从流中读取它就会发出“找不到文件的错误”。
所以我写了一个预测试解决方法,我自己打开流,在将它传递给第 3 方库之前,为一个“数据”事件读取一点,然后关闭它,然后创建并传递第二个溪流。如果在预读期间失败,我会捕获错误,并使用计时器递归等待。这很有效,我发现它在大约 30% 的时间内捕获并修复错误,有时需要等待大约 2 秒。
然后我发现实用方法 file.exists()
报告的布尔值与我的测试读取循环相同,因此我可以简化预测试解决方法,将其用作等待标志。
async waitTillFileExists(filename,file){
let retries = 20;
let delay = 500; //ms
// Recursive setTimeout,with a promise wrapper that the caller can await.
return new Promise((resolve,reject)=> {
const fnTest = async () => {
const fileExistsResponse = await file.exists()
if(fileExistsResponse[0]){
return resolve(true);
}
else if(retries-- > 0){
setTimeout(fnTest,delay);
}
else reject(`waitTillFileExists ${retries} retries exhausted!`);
}
// begin
setTimeout(fnTest,delay);
});
}
这不是原始一致性问题的解决方案,而是有效地解决了它,假设文件保存和后续提取不会立即保持一致。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。