首页 · ← SuperJ 手册 中文|EN

S3 兼容的分片上传

sj.aws 与 sj.aws.s3 实现了 AWS Signature Version 4(SigV4)请求签名和一个 S3 分片上传客户端,直接构建在 sj.net.NioAdapter 的 TLS 传输之上(sj.http.HttpClient 仅支持明文)。本教程覆盖凭据、TLS 传输的接线、四个分片调用、uploadFile 便捷包装,以及 — 放在最前面讲,因为它改变你实际使用的方式 — 它今天能到达与不能到达的边界。

开始之前:它到底能连上什么

connectTls 能连上真正的 AWS S3(#4224)。S3 协商的正是 SuperJ 提供的那一套套件,TLS_AES_128_GCM_SHA256 + X25519 + ECDSA-P256,而且它的证书链可用普通 CA bundle 验证。把信任锚定在 OS bundle 上,并指明你期望的主机名:

byte[] pem = /* read /etc/ssl/cert.pem, or your distro's ca-certificates.crt */;
TlsConfig tls = new TlsConfig()
    .trustAnchors(Pem.certBundle(pem))
    .expectHost("s3.ap-northeast-1.amazonaws.com");

对由你控制密钥的固定端点(下面的演示对它的 mock 服务器就是这么做的),改用 TlsConfig.pinnedPeerKey(...);对单个 CA 则用 trustAnchor(...)。

两件需要知道的事:

临时凭据可用:AwsCredentials 携带可选的会话 token,签名器会正确覆盖它(#4232)。仍然要你自己做的是获取它 — IMDSv2 流程与到期驱动的刷新不在 SDK 里。见 凭据。

下面的演示在进程内立起一个最小的 mock S3 服务器,并通过真实的 loopback TLS 连接与它通信,因此它在没有凭据、没有网络的情况下就能运行,而其余每个细节 — 请求构建、SigV4 签名、响应解析 — 与真实 S3 兼容端点所见完全一致。自己跑一下:

superj compile "$SJ_HOME/demo/sj/demo/S3Demo.sj" --sdk-path "$SJ_HOME/sdk" --link --output s3demo
./s3demo

用 --sdk-source "$SJ_HOME/sdk/sj" 替代 --sdk-path 会以全程序方式构建同一个演示;它也能工作,但要在 clang 里花约 100 秒(#5279)。

凭据

import sj.aws.AwsCredentials;

// Long-term keys.
AwsCredentials creds = new AwsCredentials(accessKeyId, secretAccessKey);

// Temporary keys — STS, or an IMDSv2 instance role.
AwsCredentials creds = new AwsCredentials(accessKeyId, secretAccessKey, sessionToken);

// Or from the environment: AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY, plus
// AWS_SESSION_TOKEN when it is set. Null if either key is unset.
AwsCredentials creds = AwsCredentials.fromEnvironment();

存在会话 token 时,每个请求都携带 x-amz-security-token,并且把它纳入签名 — SigV4 要求两者兼备,只做其中之一会以 SignatureDoesNotMatch 失败,而错误里没有任何信息说明缺的是哪一半。你不需要做任何事来安排这一切;S3Client 和 SigV4Signer 之间已经就此达成一致。

SDK 不做的是抓取或刷新这些凭据。IMDSv2 流程(PUT /latest/api/token,然后 GET /latest/meta-data/iam/security-credentials/<role>)以及在凭据到期前重读它们,是应用的任务 — 在它们轮换时构建新的 AwsCredentials 和新的 S3Client。

构建客户端

S3Client 通过构造函数注入接收它的 NioAdapter,而不是在内部自建 — 事件循环由你驱动,所以你在该 adapter 上做的任何其他事(另一条连接、一个服务器)在 S3Client 调用自身的有界 poll() 循环于其下运行时照常工作:

import sj.net.NioAdapter;
import sj.net.NioConfig;
import sj.net.TlsConfig;
import sj.aws.s3.S3Client;

NioAdapter adapter = new NioAdapter(new NioConfig(65536, 4096, 8, 65536, false, 16));
TlsConfig tls = new TlsConfig().pinnedPeerKey(pin);   // or .trustAnchor(caDer)

S3Client client = new S3Client(adapter, host, port, region, bucket, creds, tls);

寻址是 path-style(/<bucket>/<key>),不是 virtual-hosted — 更简单,而且无论 DNS 如何设置都对任何 S3 兼容端点有效。

分片调用

string uploadId = client.createMultipartUpload("my/object.bin");

string etag1 = client.uploadPart("my/object.bin", uploadId, 1, part1, 0, part1.length);
string etag2 = client.uploadPart("my/object.bin", uploadId, 2, part2, 0, part2.length);

PartETag[] parts = new PartETag[2];
parts[0] = new PartETag(1, etag1);
parts[1] = new PartETag(2, etag2);
client.completeMultipartUpload("my/object.bin", uploadId, parts);

completeMultipartUpload 不是可选项 — 它才是真正完成上传的那个调用;在它成功之前,各分片一直处于未组装(并计费)状态。中途出问题时,用下面的调用清理:

client.abortMultipartUpload("my/object.bin", uploadId);

每个分片的 SHA-256 载荷哈希都预先算好(没有 chunked/streaming SigV4)— 反正整个分片本来就要在内存里才能写出。任何调用收到非 2xx 响应都会抛出 S3Exception,当响应带有 <Error><Code>/<Message> 时携带解析结果。

uploadFile:替你切分本地文件

string uploadId = client.uploadFile("/path/to/local/file", "my/object.bin");

它经 sj.io.MemoryMapped 把文件切成固定大小的分片(远高于 S3 的 5 MiB 下限),以每分片有界重试的方式顺序上传,然后调用 completeMultipartUpload — 或在某个分片重试耗尽、或完成本身失败时调用 abortMultipartUpload。

在长跑进程中上传:零分配重载

SuperJ 没有 GC,所以一个 string 会被保留到进程结束。上面的重载把上传 ID 作为一个 string 返回,每次调用成本约 48 字节 — 永久。对批处理作业这不算什么,对一个跑上几个月的归档器却是慢性泄漏。针对第二种情况有一个重载:

ByteArray uploadId = new ByteArray(256);

while (moreFiles()) {
    uploadId.clear();
    int n = client.uploadFile(path, key, uploadId);   // retains NOTHING
    // the ID is uploadId.array()[0..n), or uploadId.flip().toString() if you
    // actually need it as text
}

如果你根本不想要这个 ID,给 uploadIdOut 传 null。装不下 ID 的缓冲区由 ByteArray 自己的边界报告,而不是静默截断。

上传路径上的其他一切已经什么都不保留 — 请求缓冲区、TLS 连接(跨请求复用)、SigV4 签名、CompleteMultipartUpload XML,以及分片 ETag(它们保存为字节区间而非 PartETag 对象,正是为了让每个分片不需要 string)。examples/test_4590_zero_alloc_upload.sj 以等式断言了这一点 — 200 次上传、600 个 HTTP 请求、保留零字节 — examples/test_4589_zero_alloc_request.sj 对单个请求做了同样的事。走到这一步靠的是 #4242、#4314、#4589 和 #4590;账目在 #4636,如果你想要明细。

读取对象

long size = client.headObject("my/object.bin");            // -1 if the server sent no Content-Length

byte[] buf = new byte[(int) size];
int n = client.getObject("my/object.bin", buf, 0, buf.length);

// Inclusive, as HTTP defines it — this reads 100 bytes.
int m = client.getObjectRange("my/object.bin", 0, 99, buf, 0, buf.length);

如果对象装不进目标缓冲区,getObject 会抛异常而不是返回一次短读 — 一个被当作完整报告的截断 body,正是你最终写出损坏文件还浑然不觉的方式。

所以对任何尺寸不由你控制的东西,用 downloadFile,即 uploadFile 在读取侧的镜像:

long written = client.downloadFile("my/object.bin", "/path/to/local/file");
long written = client.downloadFile("my/object.bin", "/path/to/local/file", 1 << 20);  // 1 MiB chunks

它先 HEAD 对象,然后用区间 GET 逐段直接写到磁盘,因此无论对象多大,内存都保持在约 2 倍 chunk 大小。正是这一点让多 GB 的冷存储读取在这里成为可能 — 没有流式响应解析器,所以整对象的 getObject 确实需要把整个对象放进内存(瞬态地放两次)。

刻意不做的部分

如果你需要,每一项都是合理的后续工作;对这个客户端为之构建的用例,没有一项是必需的。

SuperJ — manual · generated from s3.md at pack time · Powered by superJ — this site is served by superj_web 中文|EN