Table of Contents

S3 协议实现(network:///s3)使用指南——S3ObjectStore 对象层

给谁看:需要 S3 兼容对象存储的组件开发者;或自定义 IObjectStore 协议适配作者的参考实现。 回答什么:S3 兼容云怎么接入(SigV4/凭证/寻址)、对象层六件套怎么用、厂商差异怎么吸收、错误与重试矩阵。 桥层语义(RemoteFileSystem / Flush 唯一持久化点 / staging 编排 / 增量与恢复)已迁至 ../TC.Tier.Core/docs/network-file-system.md

定位TC.Tier.Core.IO.S3IObjectStore 的 S3 协议实现——零外部包(csproj 唯一引用 = ProjectReference → TC.Tier.Core;HTTP/加密/XML 全 BCL)。经 RemoteFileSystem 桥升格为统一文件 IO 协议(网络文件系统——四类文件系统之一);桥层契约见 ../TC.Tier.Core/docs/network-file-system.md。 其他 S3 兼容云(OSS/R2/B2 等)按同一契约接入即可。


0. 一句话总纲

一个客户端覆盖全部 S3 兼容云——换 endpoint/credentials 即达;厂商差异全部吸收在适配器内,永不上抛消费者。


1. 快速开始(各云接入矩阵)

using TC.Tier.Core.IO.S3;

// MinIO / 自建(path-style——两态皆可,默认)
using var minio = S3ObjectStore.Create(new S3ClientOptions
{
    Endpoint = "http://minio:9000",
    Bucket = "tier-logs",
    Credentials = new StaticCredentials("minioadmin", "minioadmin"),
});

// 腾讯 COS(★ 必须 virtual-host——path-style 会把整段路径当 key;实测 28/28)
using var cos = S3ObjectStore.Create(new S3ClientOptions
{
    Endpoint = "https://cos.ap-chengdu.myqcloud.com",   // 区域域名(桶名含 appid 后缀)
    Bucket = "tc-1253530278",
    Credentials = new StaticCredentials("<SecretId>", "<SecretKey>"),
    UseVirtualHostAddressing = true,
});

// AWS S3 / OSS(S3 兼容端点) / R2 / B2——换 endpoint/credentials 即达
endpoint 寻址 实测状态
MinIO(RELEASE.2025-01-20) http://host:9000 两态皆可 ✅ 契约套 25/25
腾讯 COS https://cos.<region>.myqcloud.com 必须 vhost ✅ 契约套 28/28(含会话治理/流式 List/chunked PUT)
AWS S3 https://s3.<region>.amazonaws.com 两态皆可 SigV4 黄金向量保底;真云终验按需
OSS / R2 / B2 各自 S3 兼容端点 按厂商文档 未逐家实测(SigV4 黄金向量 + 假服务器覆盖协议面)
COS 原生 V5 API 不支持(S3 兼容端点已实测覆盖,V5 不再实现)

2. S3ClientOptions 全参数

参数 默认 说明
Endpoint 必填 scheme://host[:port]
Bucket 必填 path-style 为桶名;vhost 形态下为 {bucket}-{appid} 全名(COS)
Region us-east-1 签名 scope 用(MinIO 默认即可)
Credentials 必填 ICredentialProvider(§3)
Timeout 100s 单请求粒度(含大对象上传——非 HttpClient 全局)
MaxRetries / RetryBaseDelay 3 / 200ms 幂等操作指数退避 + 抖动(§6 重试矩阵)
UseVirtualHostAddressing false {bucket}.{host}/{key} 寻址——实际请求主机 = 桶名前缀域名(COS/R2 必开)。⚠️ URL 主机 = HostHeader:与 SigningHost 并用时 URL 直接指向 SigningHost(绕过 endpoint 直连云端)——"vhost 走反代"形态当前不可表达
SigningHost null 连接 Host 与签名 Host 解耦——经自有域名反代到云原生域名时,签云域名(SigV4 所签 = 服务端所见,反代层负责 Host 改写与 SNI)。⚠️ 仅 path-style 组合有效(COS 实测拒绝此形态,见 §5 表)
PooledConnectionLifetime / PooledConnectionIdleTimeout 10 分钟 / 60s 连接池双防线(§6a)——防死连接复用导致的周期性 SSL 抖动
SupportsConditionalPut / SupportsConditionalDelete true 能力位声明(老端点按实际关闭→桥层锁降级)
SupportsStrongList true 写后立即可见(老 OSS 最终一致→读后短重试)

3. 凭证源(ICredentialProvider)

new StaticCredentials(accessKey, secretKey);   // 部署期固定(MinIO/自建典型)
new EnvironmentCredentials();                  // AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY(每次重读——外部 STS 刷新器改环境变量即生效)
// 自定义:实现 ICredentialProvider(配置文件/STS/Vault——每次签名前取当前凭证,过期刷新零改动)

会话 token(STS):S3Credentials(access, secret, sessionToken)——签名自动含 x-amz-security-token 头。


4. 使用面(IObjectStore 全量)

// 六件套
await store.PutAsync("seg-001", data, metadata);            // 整对象原子替换(元数据随 PUT 提交)
await store.PutAsync("seg-001", stream, length: -1);        // ★ 未知长度:spool 后 chunked 上传(零整驻内存)
var n = await store.GetAsync("seg-001", offset, buf);       // Range GET;offset≥长度→0(416 归一不抛)
var info = await store.HeadAsync("seg-001");                // 不存在→null
await store.DeleteAsync("seg-001");                          // 幂等
var entries = await store.ListAsync("logs/");                // 前缀枚举(分页内部归一)

// 条件写(fencing 底座——厂商强制力不一,客户端前置校验兜底归一)
await store.PutAsync("lock", token, condition: new PutCondition(IfMatch: null, IfNoneMatch: "*"));
await store.DeleteAsync("lock", new DeleteCondition(etag));  // token 防误删

// multipart(桥层编排底座;三禁令:禁跳 part / Create 不重试 / Complete NoSuchUpload→NotFound)
var session = store.CreateMultipartUpload("big", metadata);
var p1 = await session.UploadPartAsync(1, partData);
var p2 = await session.UploadPartCopyAsync(2, "src", 0, len);   // 服务端零出口流量
await session.CompleteAsync([p1, p2]);

// 会话治理(孤儿清理/运维面)
var sessions = await store.ListMultipartUploadsAsync();
await store.AbortMultipartUploadAsync(key, uploadId);        // 幂等(NoSuchUpload 视为成功)

// 流式 List(大桶——S3 实现真分页流式;其他实现 DIM 包装 ListAsync)
await foreach (var e in store.ListStreamingAsync("logs/")) { ... }

// 同步便捷包装(低频路径——SyncAsyncBridge 有界桥接,非裸 GetResult)
store.Put("k", data);  store.Head("k");  store.List("prefix");

PUT(Stream) 三形态

流形态 路径
可寻 + 长度已知 单遍流式 SHA-256 → 回卷 → 单段签名上传
不可寻 + 长度已知 chunked 流式签名直传STREAMING-AWS4-HMAC-SHA256-PAYLOAD 链式分帧:seed 签名 → 每 128KiB chunk 链式派生 → 终帧;免整驻免双遍哈希;单次发送不重试)
长度未知(length<0) spool 临时文件后 chunked 上传(本地文件系统中转——零整驻内存)

5. 厂商差异(适配器内吸收——消费者零感知)

差异 吸收方式
COS path-style 失真(整段路径被当 key) 文档约束:UseVirtualHostAddressing=true
COS 经反代 path-style + SigningHost 改写签名 Host 仍被拒(SignatureDoesNotMatch,2026-08-18 实测) vhost 要求在签名规范形态层面(服务端按 bucket-in-Host 计算规范请求)——反代改写绕不过;COS 只可用原生端点 + vhost(反代需 vhost 域名形态透传,另议)
COS/MinIO 条件 PUT 忽略/静默创建 客户端前置 Head + 本地校验(接受极小竞态——与条件 DELETE 同款)
条件 DELETE 强制不一(MinIO 失配不拦) Head 校验 + 无条件删常态化
XML xmlns 使用不一(AWS/MinIO) 解析一律 LocalName 匹配(命名空间免疫)
ListV2 分页参数差异 continuation-token 循环归一
S3 200 + Error body(multipart complete 延迟失败) 响应体根元素检测 → 按错误映射抛出

6. 错误与重试

错误出口统一 FileIOException

S3 响应 IOError
404 / NoSuchKey / NoSuchUpload NotFound
412 / PreconditionFailed PreconditionFailed
403 / SignatureDoesNotMatch / InvalidAccessKeyId AccessDenied
507 DiskFull
416 RangeNotSatisfiable (GetAsync 返回 0——不抛)
501 Unsupported

重试矩阵:GET/HEAD/PUT/DELETE/List/UploadPart/Complete 幂等可重试(5xx/429/网络抖动指数退避); CreateMultipartUpload 不重试(响应丢失重发会双开会话);chunked 直传单次发送(源不可回卷——重试语义归 spool 路径)。

6.1 连接稳定性(防周期性 SSL 抖动)

已知坑(Aliyun OSS SDK 时代):服务端 60-90s 主动断开空闲 TCP 连接,而客户端连接池 PooledConnectionLifetime = 0(永不回收)→ 池中堆积死连接 → 高并发复用死连接 → SSL 握手失败 周期性抖动。本客户端三层防御:

  1. PooledConnectionIdleTimeout 默认 60s(空闲即关)——客户端在服务端断开之前主动回收, 池中不留死连接;
  2. PooledConnectionLifetime 默认 10 分钟(总寿命到期重建)——兜底长寿命连接的中间设备劣化;
  3. .NET 8 SocketsHttpHandler 复用失败自动重建连接重发(stale 自愈)。

超时模型:Timeout每请求粒度(全局 HttpClient = Infinite)——大对象上传不会被小请求超时 误杀,反之亦然。两个池参数均可按目标端点调整(例如服务端空闲阈值更短的端点,下调 idle timeout)。


7. 测试与性能基线

测试三层门禁(离线黄金向量/假服务器 → MinIO 容器 → 真云环境变量)见 ../COORDINATION.md §4; 桥级契约测试在 tests/TC.Tier.Core.Tests/IO/Remote/(staging 语义/池协议/增量断言/洞读零 GET/孤儿清理等 60+ 例)。

性能基线(真实 COS @ ap-chengdu,2026-08-18 实测)

指标 数值
Range GET 延迟 p50 = 48ms / p99 = 87ms(客户端→成都 RTT 主导)
桥级全量 Flush(128MB multipart) 12.8 MB/s
桥级增量 Flush(追加 32MB) 耗时比 38%(未改 part 服务端自拷贝)
桥级洞区间读(8MB 整读) 38ms(本地零填充)

探针:benchmarks/TC.Tier.S3PerfProbe(对象层吞吐/延迟/TPS + bridge 模式桥级场景)。