← 返回蜂巢洞察

如何在 Cloud Run 上让文件仅被保存几分钟而不是几年

一份打印任务、一次性下载的内容或支持文件,通常只需要在有人使用完它们之后被删除即可。但大多数后端系统仍然会像Google Drive那样存储这些文件:给它们起个名字,不设置过期时间,并在数据库中记录相关信息,以便日后能够检索到这些文件。 本教程将向你介绍我在实际开发中使用的一种简单模式,这种模式适用于处理打印任务。这些原理并不局限于打印领域,你可以将其应用于任何需要临时存储数据的任务:接收数据、短暂保存它们、将它们交给授权的使用者,然后删除它们。 你不会得到一个完整的应用程序,而是会学到一些能够防止数据队列变成一个庞大的文件库的方法。 目录 你将构建什么 你需要什么 一个服务,而不是多个服务

< p>一份打印任务、一次性下载的内容或支持文件,通常只需要在有人使用完它们之后被删除即可。但大多数后端系统仍然会像Google Drive那样存储这些文件:给它们起个名字,不设置过期时间,并在数据库中记录相关信息,以便日后能够检索到这些文件。

< p>本教程将向你介绍我在实际开发中使用的一种简单模式,这种模式适用于处理打印任务。这些原理并不局限于打印领域,你可以将其应用于任何需要临时存储数据的任务:接收数据、短暂保存它们、将它们交给授权的使用者,然后删除它们。

< p>你不会得到一个完整的应用程序,而是会学到一些能够防止数据队列变成一个庞大的文件库的方法。

< h2 id="heading-table-of-contents">目录 < ul>
  • 你将构建什么

  • 你需要什么

  • 一个服务,而不是多个服务

  • 给这个对象起个普通的名字

  • 为存储目的进行加密,并在解密时保持透明

  • 将队列数据存储在会自动失效的内存中

  • 有目的地删除数据,然后清理那些被遗漏的文件

  • 哪些内容不应该被保留下来

  • 下一步该做什么

  • < h2 id="heading-what-you'll-build">你将构建什么 < p>整个系统的架构非常简单:

    < ol>
  • 客户端发送一个文件用于完成某项任务。

  • 你的API会将加密后的文件存储在对象存储系统中,同时会在Redis中记录这项任务的相关信息。

  • 经过授权的使用者可以一次性获取这个文件。

  • 当任务完成或定时器触发时,你会删除相应的文件及任务记录。

  • < p>成功的实现方式就是创建一个不包含任何冗余数据的系统,而不是建立一个可供搜索的档案库。

    < pre>客户端 | | HTTPS v Cloud Run(单节点服务) |-- Cloud Storage 加密后的文件存储 |-- Cloud KMS 为每个文件生成加密密钥 |-- Redis 包含过期时间的任务信息存储 | 经过授权的使用者 <-- 下载数据后删除相关记录 < h2 id="heading-what-you-need">你需要什么 < p>你应该已经具备以下基础知识:

    < ul>
  • Node.js和Express开发基础

  • 了解HTTPS和JSON的基本概念

  • 知道对象存储服务(如Amazon S3、Google Cloud Storage)用于存储文件,而Redis用于存储快速访问的键值对数据

  • < p>你并不需要Kubernetes或第二个“打印服务器”。如果你使用的是Google Cloud平台,那么Cloud Run、Cloud Storage、Cloud Key Management Service以及任何Redis实例就足够了。在其他云平台上,只要将相关服务的名称进行相应的调整,这个架构同样适用。

    < h2 id="heading-one-service-not-a-fleet">一个服务,而不是多个服务

    < p>一个仅需要存在20分钟的文件,根本不需要一台始终处于运行状态的虚拟机、一个集群系统,也不需要在客户办公室放置额外的设备。

    可以将网站和API放在同一个Cloud Run服务中。Cloud Run会在请求执行时收取CPU使用费用;当没有人进行上传操作时,也就无需为闲置的队列处理机制支付费用。

    然而这种做法也存在隐患:Cloud Run可以运行多个实例,但如果每个实例都将任务数据存储在自身的内存中,那么实例A就无法看到实例B所接收到的任务。此时用户如果重新刷新页面,相关任务信息可能会消失,甚至导致错误的结果被显示出来。

    因此:

    • 处理流程应当是无状态的。

    • 但队列系统却无法实现无状态设计。

    当Redis发生故障时,系统应该能够检测到这一异常并停止正常运行。如果仅通过活跃性检查来判断“节点是否正常运行”,那么请求就会被发送到一个无法准确反映队列状态的系统中,从而导致错误的结果。因此必须采用“失败即关闭”的机制,而绝不能让请求被转发到接收到了该请求的副本节点上。

    app.get("/up", (_req, res) => {
      res.status(200).send("ok");
    });
    
    app.get("/health", async (_req, res) => {
      const redisUp = await pingRedis();
      if (!redisUp) return res.status(503).send("queue down");
      res.status(200).send("ok");
    });
    

    对于负载均衡器或Cloud Run监控来说,应该使用/health路径;而仅仅为了确认容器是否能够正常响应请求,才应使用/up路径。

    为对象起一个乏味的名称

    如果对象的键名为aadhaar-ramesh.pdf,那么就已经相当于建立了一个“文件归档系统”了;接下来用户肯定会提出搜索功能的需求。

    应该使用随机生成的ID来存储数据:

    const { randomUUID } = require("crypto");
    
    function objectName() {
      return `jobs/${randomUUID()}.bin`;
    }
    

    如果确实需要保留人类可读的文件名,那么可以在任务记录中暂时保存它,但不要将其包含在对象名称中。另外,如果你的系统设计要求在任务完成后被删除,那么就完全没有必要将数据复制到另一个地方“以确保安全”——这样做反而会浪费资源。

    应该对每次访问所能添加的文件数量设置上限。例如规定最多只能上传5个文件,这样就可以有效控制存储空间、键操作次数以及队列长度。如果允许无限制上传,系统很快就会变得混乱不堪。

    为数据存储进行加密,并在解密时保持透明

    在涉及隐私保护的场景中,有两种不同的加密方式:

    • 零知识加密意味着服务器永远不会掌握明文或密钥。

    • 短期存储策略意味着服务器不会将文件长期保存下来。

    打印机、预览功能或“仅下载一次”的按钮通常只需要后一种加密方式,而非第一种。只有经过授权的人员才能查看数据的原始内容。这一点必须在相关文档和产品说明中明确说明;如果你的API会解压加密后的数据,那就绝对不能声称“服务器根本看不到文件内容”。

    以下是一种实际可行的处理方案:

    1. 用户在上传文件之前,会使用AES-GCM算法对数据进行加密,这样即使网络被截获,截获到的也只会是无法读取的加密数据。

    2. 在服务器端,首先解开加密层,确认数据的格式(PDF、JPEG、PNG等),然后再使用新的密钥对数据重新进行加密以便存储。

    3. 可以请求Cloud KMS(或你所在云平台的密钥管理服务)来对这个新生成的密钥进行再次封装。将封装后的密钥与任务数据一起保存,但不要将其包含在对象名称中。

    4. 当经过授权的用户需要读取数据时,再解开加密层进行解密,然后才能将数据流式传输给用户。

    检查这些“魔法字节”正是为什么在数据传输过程中需要进行解包操作的原因。如果你跳过了这个步骤,别人就可能上传HTML文件或其他垃圾数据,而你可能会在最糟糕的时刻才发现这个问题。

    这里只是一个简化的示例,并不是一个完整的加密库:

    const crypto = require("crypto"); function encryptForStorage(plainBuffer) { const dataKey = crypto.randomBytes(32); const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv("aes-256-gcm", dataKey, iv); const ciphertext = Buffer.concat([cipher.update(plainBuffer), cipher.final]); const tag = cipher.getAuthTag(); return { ciphertext, iv, tag, dataKey }; }

    将dataKey发送到KMS,然后只保存经过加密处理后的结果即可。AES-GCM的认证标签非常重要:如果有人篡改了这些数据,解密操作将会失败,而不会生成看起来似乎还正常的损坏文件。

    对于那些永远不会过期的数据存储方案来说,调用KMS的成本其实非常低廉。但为了节省一点点成本而忽略加密步骤,最终可能会导致整个数据系统出现严重问题。

    在柜台前工作的人员仍然能够看到这些页面内容,因为纸质文件是可以被拍照的。但是现代的技术设计可以避免需要永久保存这些副本的情况,也不会让打印机变得“失效”或无法正常使用。

    将过期队列存储在内存中

    Redis用于存储正在处理中的任务信息,而不是文件本身。

    const JOB_TTL_seconds = 60 * 60; async function saveJob(redis, job) { await redis.set( `job:${job.id}`, JSON.stringify({ id: job.id, objectName: job.objectName, shopId: job.shopId, status: "queued", createdAt: Date.now() }), "EX", JOB_TTL_seconds ); }

    设置一小时作为任务的有效期限,这只是根据实际需求来决定的,并非硬性规定。副本文件并不需要保存那些可以在明年再被查询到的信息。

    还需要存储以下内容:

    • 文件的元数据缓存

    • 记录“本次访问中处理的文件数量”的计数器

    • 一个锁机制,以防止两个Cloud Run实例同时执行清理操作

    不要将PDF文件直接存储在Redis中。Redis主要用于存储任务信息,而实际的文件数据应该存储在Cloud Storage中。当任务的有效期限到期时,相关记录会自动被删除,即使你忘记手动删除它们也是如此。这才是这种设计的目的所在。

    有目的地进行删除,然后再清理遗漏的内容

    当授权用户完成操作后,需要执行以下步骤:

    async function completeJob({ storage, redis, bucket, job }) { await storage.bucket(bucket).file(job.objectName).delete({ ignoreNotFound: true }); await redis.del(`job:${job.id}`); }

    首先删除文件本身,然后再删除对应的索引记录。如果先删除了索引记录而文件删除失败,那么你就可能会失去访问这些文件的唯一途径。因此,如果文件删除失败,应该保留相关任务记录并重新尝试删除操作。

    人们经常会关闭浏览器标签页,导致某些请求被中断。因此,应该每隔几分钟运行一次清理程序,以清除那些已经完成但尚未被删除的文件:

    • 如果某个任务已经完成,就删除所有剩余的文件。

    • 如果某个文件的访问时间已经超过了设定的有效期(大约30分钟就足够了),也应当将其删除。

    • 需要使用Redis锁机制,以防止两个实例同时尝试删除同一个文件。

    async function withLock(redis, fn) {
      const ok = await redis.set("lock:sweep", "1", "NX", "EX", 55);
      if (ok !== "OK") return;
      try {
        await fn();
      } finally {
        await redis.del("lock:sweep");
      }
    }
    

    Firestore、Postgres或任何其他数据库都可以用来记录商店的信息、店主信息以及使用次数。但这些数据库不应该保存PDF文件的第二份副本。如果文件数据与客户列表存储在同一个表中,那么你就建立了一个内容管理系统。通过这些数据可以记录“某次访问发生了”,但这个系统不应该是用于展示缩略图的 gallery。

    不应声称的事项

    请将这些限制条款写到README文件中,这样以后就无需再反复强调它们了:

    • 服务器在接收文件数据时可以看到明文内容(以便重新加密并进行验证),在用户授权下载文件时也会看到明文。这并不属于“零知识”技术。

    • 被删除的对象只会从你的存储桶中消失,但并不会从接收者的屏幕、打印机或相机上消失。

    • 如果Redis服务器出现故障,新的任务将无法被执行;使用虚假的本地队列来处理请求,其后果会比收到503错误更严重。

    • 随机生成的对象名称并不能起到访问控制的作用。在流式传输文件数据之前,仍然需要进行身份验证。

    下一步该怎么做

    首先在一个Cloud Run服务中构建最基本的处理流程:

    1. POST /jobs这个接口用于写入密文数据,并在Redis中创建一条带有EX时间限制的记录。

    2. GET /jobs/:id这个接口会先进行身份验证,然后流式传输明文数据,最后删除相关记录。

    3. 定期执行“清理操作”可以删除所有超过规定时间限制的文件数据。

    4. 当Redis服务器出现故障时,/health接口会返回503错误代码。

    如果这个处理流程看起来很简单、很乏味,那么说明你做得没错。正是这样的简单性才使得一个原本只有五分钟长的文件不会发展成一套复杂的文档管理系统。

    我在印度开发的Docshy系统中也采用了这种设计模式:使用位于孟买的Cloud Run服务(asia-south1区域)、Cloud Storage、Cloud KMS以及Redis来构建整个系统。

    相关文章

    技术实践

    如何在数据传输流程中对个人身份信息进行加密,同时确保这些信息仍可被检索到

    在Intuit,我的团队构建了一个数据处理流程,用于处理TurboTax电子申报系统中产生的数据。这些数据需要被用于制作仪表板、进行分析,以及满足其他后续使用需求。 其中最大的挑战之一就是如何保护那些属于 个人身份信息 的数据,比如社会安全号码。这类信息具有极高的敏感性,因此在数据处理的每一个环节都必须得到妥善保护。与此同时,分析团队有时也需要进行一些正常的业务操作,例如识别与特定客户相关的记录,或者进行精确匹配查询。 那么,如何在保证这些敏感信息不被泄露的同时,允许人们对这些数据进行受控的搜索呢?在本文中,我将解释解决这一问题所涉及的加密技术原理,包括以下内容: 概率加密与确定性加密 对加密

    阅读全文
    技术实践

    光标技术利用S3 WAL机制,使Git存储系统的处理能力提升到每秒可接收超过300次推送请求。

    Cursor推出了名为“Continuity”的Git存储架构,该架构以基于S3技术的预写日志作为数据存储的权威来源。这种设计将本地的NVMe仓库转换为缓存系统,并将副本协调机制与数据一致性保障机制分开处理。在模拟测试中,使用S3 Express One Zone时,Cursor能够实现线性扩展——当副本数量达到100个时,每秒仍可完成超过300次数据推送操作。 作者:Leela Kumili

    阅读全文
    技术实践

    “Graphify”:通过统一代码库的上下文信息来简化智能软件工程的开发流程

    Graphify是一款开源工具,其设计目的在于将代码库及非结构化数据转化为可供查询的知识图谱。该工具于2026年4月正式推出,有效解决了人工智能编码辅助工具在处理多文件数据时所面临的推理难题。最近的更新进一步提升了解析器的功能以及跨文件的数据处理能力。社区用户的反馈表明,这一技术架构具有很大的潜力,但同时也指出了其在日常工作中集成使用时所存在的挑战。 作者:Olimpiu Pop

    阅读全文
    技术实践

    如何使用Python和依赖关系图来检测公共数据集中隐藏的目标泄露现象

    不久前,我给一个机器学习模型提供了来自公共CDC数据集的五列数据,让它尝试预测同一文件中的第六列数据。该模型的R²值为0.998,这个数值已经非常接近完美了。 虽然这个结果看起来很成功,但实际上该模型对现实世界的认知几乎没有任何提升——因为CDC本身就是根据其他五列数据计算出了第六列数据的,所以该模型实际上只是机械地应用了CDC的计算方法而已。 数据科学家将这种问题称为 目标信息泄露 ,当输入给模型的数据本身就已经包含了答案的某些信息时,就会发生这种情况。 在公共数据中,这类问题很容易被隐藏起来,因为大量的公共数据都是通过其他公共数据计算得出的。例如,一个政府指数可能是根据调查数据构建的,而另

    阅读全文