Google Cloud Storage 不一致,在可恢复上传完成后 500 毫秒之前返回 404

sci*_*lot 6 javascript file-upload consistency node.js google-cloud-storage

我正在使用云存储制作一个文件上传器,该文件上传器的行为不一致,这似乎与文档相反。

当您将对象上传到 Cloud Storage 并收到成功响应时,该对象可立即从 Google 提供服务的任何位置进行下载和元数据操作。无论您创建新对象还是替换现有对象,都是如此。由于上传是高度一致的,因此您永远不会收到 404 Not Found 响应或写入后读取或元数据更新后读取操作的陈旧数据。https://cloud.google.com/storage/docs/consistency#strongly_consistency_operations

...但是如果我上传后立即阅读,我会收到 404。

流程如下:

  1. 我的后端 NodeJS API 启动可恢复上传,创建存储桶的会话 URI
  2. 然后用户通过 PUT 从浏览器将文件直接上传到 GCS 会话 URI
  3. 前端发布了对我的 API 的更新,表示上传已完成。
  4. 然后我的 API 尝试下载相同的文件作为流并摄取它

我一切正常,但后来发现当上传新文件时(即存储桶中尚不存在),上传完成(步骤 2)和读取成功(步骤 4)之间需要 500 毫秒的延迟。如果我立即执行,我会得到 404。

文档指出,通常上传可以立即使用,除非有一些缓存。

重要提示:公开可读的缓存对象可能不会表现出强一致性。有关详细信息,请参阅缓存控制和一致性

我正在使用XMLHttpRequest将文件上传到 GCS 并使用该load事件来检测已完成的上传。根据我的阅读,这应该意味着已收到 200 响应,因此该文件已就位。尽管调试加载事件表明它只是另一个 100% 的“进度”事件。

我尝试过的

解决方法是setTimeout(done, 500)在第 3 步调用我的 API 之前,将 a 添加到加载事件处理程序中的最终回调中。

我已经对此进行了数十次测试,它是可靠的,可重复的,其中 0 - 400 毫秒失败,大约 500 毫秒以上“修复”它总是。

我尝试按照建议将缓存控制标头添加到原始 POST 中,这将上传会话设置为没有缓存 - 添加no-store似乎是正确的。我可以在 PUT 的标头中看到这一点(它实际上在响应中放置了比我设置的更多的无缓存选项)。这似乎根本不影响行为。

如果文件已经存在于存储中并被覆盖,则不会发生这种情况。(尽管我猜想如果我上传不同的文件,内容中可能仍然存在竞争条件)。

我似乎无法捕获异常,所以我真的不知道哪个对 GCS 的调用正在返回 404,无论是它 bucket.file()还是remoteFile.createReadStream()稍后从它读取(它位于我将可读流传递到的其他一些库的深处) )。

我还没有尝试过尝试/重试循环,因为我什至无法捕获错误。如果我无法保证一致的行为,这就是我想做的事情。

我尝试过使用gcs-resumable-upload 包和直接使用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',
                },
            },
        });

Run Code Online (Sandbox Code Playgroud)

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];
Run Code Online (Sandbox Code Playgroud)
  1. 上传步骤如下所示(浏览器中的 JS),打开文件并上传:
            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);
Run Code Online (Sandbox Code Playgroud)
  1. 后端 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()

Run Code Online (Sandbox Code Playgroud)

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) 
Run Code Online (Sandbox Code Playgroud)

错误消息是一个很大的结构:

{
  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,
      highWaterMark: 16384,
      finalCalled: false,
      needDrain: false,
      ending: true,
      ended: true,
      finished: true,
      destroyed: true,
      decodeStrings: true,
      defaultEncoding: 'utf8',
      length: 0,
      writing: false,
      corked: 0,
      sync: false,
      bufferProcessing: false,
      onwrite: [Function: bound onwrite],
      writecb: null,
      writelen: 0,
      afterWriteTickInfo: null,
      buffered: [],
      bufferedIndex: 0,
      allBuffers: true,
      allNoop: true,
      pendingcb: 0,
      prefinished: true,
      errorEmitted: false,
      emitClose: true,
      autoDestroy: true,
      errored: null,
      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, 23 Mar 2021 08:08:50 GMT',
      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,
      writecb: null,
      writechunk: null,
      writeencoding: 'buffer'
    }
  },
  domainEmitter: PassThrough {
    _readableState: ReadableState {
      objectMode: false,
      highWaterMark: 16384,
      buffer: BufferList { head: null, tail: null, length: 0 },
      length: 0,
      pipes: [],
      flowing: true,
      ended: false,
      endEmitted: false,
      reading: true,
      sync: false,
      needReadable: true,
      emittedReadable: false,
      readableListening: false,
      resumeScheduled: false,
      errorEmitted: true,
      emitClose: true,
      autoDestroy: true,
      destroyed: true,
      errored: [Circular *2],
      closed: true,
      closeEmitted: false,
      defaultEncoding: 'utf8',
      awaitDrainWriters: null,
      multiAwaitDrain: false,
      readingMore: false,
      decoder: null,
      encoding: null,
      [Symbol(kPaused)]: false
    },
    _events: [Object: null prototype] {
      prefinish: [Function: prefinish],
      reading: [Function: makeRequest],
      data: [Function (anonymous)],
      end: [Function (anonymous)]
    },
    _eventsCount: 4,
    _maxListeners: undefined,
    _writableState: WritableState {
      objectMode: false,
      highWaterMark: 16384,
      finalCalled: false,
      needDrain: false,
      ending: false,
      ended: false,
      finished: false,
      destroyed: true,
      decodeStrings: true,
      defaultEncoding: 'utf8',
      length: 0,
      writing: false,
      corked: 0,
      sync: true,
      bufferProcessing: false,
      onwrite: [Function: bound onwrite],
      writecb: null,
      writelen: 0,
      afterWriteTickInfo: null,
      buffered: [],
      bufferedIndex: 0,
      allBuffers: true,
      allNoop: true,
      pendingcb: 0,
      prefinished: false,
      errorEmitted: true,
      emitClose: true,
      autoDestroy: true,
      errored: [Circular *2],
      closed: true
    },
    allowHalfOpen: true,
    _read: [Function: bound ],
    _write: [Function (anonymous)],
    [Symbol(kCapture)]: false,
    [Symbol(kTransformState)]: {
      afterTransform: [Function: bound afterTransform],
      needTransform: true,
      transforming: false,
      writecb: null,
      writechunk: null,
      writeencoding: null
    }
  },
  domainThrown: false
}
Run Code Online (Sandbox Code Playgroud)

afa*_*rre 1

您使用的是 Cloud CDN 还是任何第三方 CDN?

询问原因有两个:

云存储还兼容第三方 CDN

来源

为了在向用户交付内容时获得最佳性能,我们建议将 Cloud Storage 与 Cloud CDN 结合使用。

来源

我建议首先查看您是否已经有任何可能影响对象缓存并因此导致您提到的延迟的 CDN。

如果情况并非如此,我建议使用 Cloud CDN,因为文档指出与 Cloud Storage 结合使用可提供最佳性能。除了可能带来的优化性能之外,Cloud CDN 还具有一些您可能会感兴趣的缓存设置

最后,您提到了在 HTTP 请求中使用该no-store标志,但请注意以下几点:

注意:Cache-Control 也是您可以在对象的 HTTP 请求中指定的标头;但是,Cloud Storage 会忽略此标头并根据存储的元数据值设置响应 Cache-Control 标头。

来源