[OSS/S3] 对象存储 FAQ

1 概述: 对象存储-常见问题

2 FAQ for 对象存储(OSS/S3)

Q: 访问URL的风格对比: Virtual-Host-style(虚拟主机式) vs. Path-style(路径式)

Q: 火山云(TOS) 客户端的访问差异及示例?

所有客户端 : 支持 内网与外网 访问

  • endpoint (s3 访问方式 和 非 s3 访问方式; 内网与外网 的 endpoint 是完全不同的)

内网 : "tos-s3-ap-southeast-3.ivolces.com:443" or "tos-s3-ap-southeast-3.ivolces.com" / ...
外网 : "tos-s3-ap-southeast-3.volces.com:443" or "tos-s3-ap-southeast-3.volces.com" / ... (此方式的访问效率较低)
详情参见 : 地域和访问域名(Endpoint) - 火山云/TOS

java: com.amazonaws:aws-java-sdk-*(s3/kms/core/...):1.12.261 : 支持 s3 协议;支持 VistualHost 访问方式,但不支持 PathStyle 访问方式

//基于 AK/SK 创建会话凭据
com.amazonaws.auth.BasicAWSCredentials ossCredentials = new BasicAWSCredentials(accessKey, secretKey);
ClientConfiguration clientConfiguration = new ClientConfiguration();
clientConfiguration.setConnectionTimeout(9999 * 1000);
clientConfiguration.setRequestTimeout(9999 * 1000);
clientConfiguration.setSocketTimeout(9999 * 1000);
clientConfiguration.setProtocol(Protocol.HTTPS);

//创建客户端会话实例		
com.amazonaws.services.s3.AmazonS3 ossClient = AmazonS3ClientBuilder.standard()
	.withCredentials(new AWSStaticCredentialsProvider(ossCredentials))
	.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region))
	.withClientConfiguration(clientConfiguration)
	//.enablePathStyleAccess()
	.withPathStyleAccessEnabled( enablePathStyleAccess = false ) //华为云 OBS : 支持 PathStyle 访问方式; 火山云(TOS) : 不支持 PathStyle 访问方式
	.build();
	

//具体操作-上传文件到 OSS
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectKey, file);//String bucketName, String key, File file
String name = file.getName();
ObjectMetadata metadata = new ObjectMetadata();
metadata.addUserMetadata( FILE_TYPE , name.substring(name.lastIndexOf(".") + 1));// 记录文件类型
putObjectRequest.setMetadata(metadata);
com.amazonaws.services.s3.model.PutObjectResult result = ossClient.putObject(putObjectRequest);


//具体操作-根据桶名及文件夹名,获取该桶该文件夹的操作对象
ListObjectsRequest lor = new ListObjectsRequest().withBucketName(bucketName).withPrefix("perfFile/");
ObjectListing objectListing = S3.listObjects(lor);
//根据操作对象列出所有文件对象,单个对象使用objectSummary.getKey()即可获取此文件完整路径,配合桶名可以用于操作
for (S3ObjectSummary objectSummary : objectListing.getObjectSummaries()) {
//...
  String objectKey = objectSummary.getKey();
}

//具体操作-删除OSS文件
ossClient.deleteObject(bucketName, key);//String key
  • endpoint (s3 访问方式 和 非 s3 访问方式; 内网与外网 的 endpoint 是完全不同的)

内网 : "tos-s3-ap-southeast-3.ivolces.com:443" or "tos-s3-ap-southeast-3.ivolces.com" / ...
外网 : "tos-s3-ap-southeast-3.volces.com:443" or "tos-s3-ap-southeast-3.volces.com" / ...
详情参见 : 地域和访问域名(Endpoint) - 火山云/TOS

  • region

"ap-southeast-3" / "cn-beijing" / "cn-shanghai" / ...

python: tos.TosClientV2 : 不支持 s3 协议

  • 安装方式 : pip3 install tos
  • 示例代码
import os;
# from obs import ObsClient;
import tos;
from tos import TosClientV2 as TosClient;
import shutil;
import traceback;

""" 本地试验脚本 
地域和访问域名(Endpoint) | https://www.volcengine.com/docs/6349/107356?lang=zh
Volcengine TOS SDK for Python | https://pypi.org/project/tos/
    pip install tos
火山引擎 TOS Python SDK | https://github.com/volcengine/ve-tos-python-sdk/blob/main/README-zh.md
"""

AccessKey='xxx'
SecretKey='xxx'
#Endpoint='obs.cn-north-4.myhuaweicloud.com'
# 注: 火山云 , python tos 模块 ,仅支持 非 S3 的 endpoint; java 客户端,支持 s3 endpoint
# 内网(非S3) : tos-s3-ap-southeast-3.ivolces.com
# Endpoint='tos-ap-southeast-3.ivolces.com'
# 公网(非S3)
Endpoint='tos-ap-southeast-3.volces.com'
Region="ap-southeast-3"

BucketName="xxxEnv-tos-bigdata-private"
OssDirectory="XXXEnv/ODS/xxx/xxx/"
  # 需注意: 前面不要加 '/' 根目录前缀
#LocalSourcePath = "/data/xxxx/local_data/source/xxxDayNumber/xxxDeviceIdHash"
LocalSourcePath="E:/tmp/xxx-platform" + "/data/xxxx/local_data/source/normal_data/xxxDayNumber/xxxDeviceIdHash"
    # 或 : "E:\\tmp\\" + ...

# "obs://xxxEnv-tos-bigdata-private/XXXEnv/ODS/xxx/xxx/xxxDayNumber/XxxDeviceId/e7463ffdce5ced753a1d77c246fae273-0"
#ObjectKey = "xxxEnv/ODS/xxxxx/xxxDayNumber/xxxDeviceModelCode/XxxDeviceId/e7463ffdce5ced753a1d77c246fae273-0"
ObjectKey = "xxxEnv/ODS/xxx/xxxDayNumber/xxxDeviceModelCode/XxxDeviceId/e7463ffdce5ced753a1d77c246fae273-0"
#LocalSinkPath = "E:/tmp/xxx-platform" + "/xxx/local_data/sink/normal_data/xxxDayNumber/xxxDeviceIdHash/"
LocalSinkPath='E:\\work_data\\xxx\\XxxDeviceId\\e7463ffdce5ced753a1d77c246fae273-0'

#ossClient = ObsClient(access_key_id=AccessKey, secret_access_key=SecretKey,server=Endpoint)
ossClient = TosClient(ak=AccessKey, sk=SecretKey, endpoint=Endpoint, region=Region)



### CASE : 从 OBS 指定目录批量下载 对象文件到本地文件夹下
# https://xxxEnv-tos-bigdata-private.tos-s3-ap-southeast-3.ivolces.com/xxxEnv/ODS/xxx/xxxDayNumber/xxxDeviceModelCode/XxxDeviceId/e7463ffdce5ced753a1d77c246fae273-0
#local_path='/data/xxx/xxx/local_data/source/normal_data/xxxDayNumber/xxxDeviceIdHash'
local_path=LocalSourcePath

def exit_when_exception():
    print( traceback.format_exc() )
    shutil.rmtree(LocalSourcePath)
    os._exit(1)

try:
    # 创建 TosClientV2 对象,对桶和对象的操作都通过 TosClientV2 实现
    ossClient = tos.TosClientV2(AccessKey, SecretKey, Endpoint, Region)
    # 列举指定桶下所有对象
    truncated = True
    continuation_token = ''
    total_files = 0;
    success_files = 0;
    failure_files = 0;
    while truncated:
        result = ossClient.list_objects_type2(BucketName, prefix= OssDirectory, continuation_token=continuation_token)
        total_files = len(result.contents);
        for item in result.contents:
            if item.key.endswith("/") == False: # 文件对象,而非目录对象
                fileName = os.path.basename( item.key );
                try :
                    file_response = ossClient.get_object_to_file( BucketName, item.key, LocalSourcePath + "/" + fileName) # 会根据前缀在本地创建文件夹
                    success_files += 1;
                    # print("下载成功:" + item.key)
                except Exception as e:
                    failure_files += 1;
                    print(f"download file fail!objectKey:{item.key}, exception:{e}")
            else: # 目录对象, eg: 'xxxEnv/ODS/xxx/normal_data/xxxDayNumber/xxxDeviceIdHash/'
                total_files -= 1;
        truncated = result.is_truncated
        continuation_token = result.next_continuation_token

    # resp = obsClient.downloadFiles(bucketName=BucketName, prefix=OssDirectory, downloadFolder=LocalSourcePath)
    # result = "DownloadFiles summary : total_task:%d, success:%d ,failure:%d" % (resp.total_tasks, resp.successful_tasks, resp.failed_tasks)
    result = "DownloadFiles summary | total_task:%d, success:%d ,failure:%d" % ( total_files,success_files, failure_files )
    print(result)
except tos.exceptions.TosClientError as e:
    # 操作失败,捕获客户端异常,一般情况为非法请求参数或网络异常
    print('fail with client error, message:{}, cause: {}'.format(e.message, e.cause))
    exit_when_exception();
except tos.exceptions.TosServerError as e:
    # 操作失败,捕获服务端异常,可从返回信息中获取详细错误信息
    print('fail with server error, code: {}'.format(e.code))
    # request id 可定位具体问题,强烈建议日志中保存
    print('error with request id: {}'.format(e.request_id))
    print('error with message: {}'.format(e.message))
    print('error with http code: {}'.format(e.status_code))
    print('error with ec: {}'.format(e.ec))
    print('error with request url: {}'.format(e.request_url))
    exit_when_exception();
except Exception as e:
    print('fail with unknown error: {}'.format(e))
    exit_when_exception();


"""
### CASE : 从 本地文件 上传到 OBS 指定目录下 (OBS会自动创建对应路径)
try:
    #resp = ossClient.putFile(BucketName, ObjectKey, LocalSinkPath)
    resp = ossClient.put_object_from_file(BucketName, ObjectKey, LocalSinkPath)
    print(f"resp.response:{resp.resp}")
except:
    import traceback
    print(traceback.format_exc())
    # shutil.rmtree(LocalSourcePath)
    # shutil.rmtree(LocalSinkPath)
    os._exit(1)
"""

python : pyarrowpyarrow.fs.S3FileSystem 子模块 : 支持 s3 协议;支持 VistualHost 访问方式,但不支持 PathStyle 访问方式

#!/usr/bin/env python3.9
# -*- coding: utf-8 -*-

from pyarrow import fs
import pyarrow as pa
import pyarrow.parquet as pq


# 读取指定目录下的 若干 parquet 文件 by `pq.read_table`
#table = pq.read_table('/data/xxx/xxx/local_data/source/normal_data/xxxDayNumber/xxxDeviceIdHash')
table = pq.read_table('E:/work_data/xxx/xxx/xxxDayNumber/xxxDeviceIdHash')
file_options = pa.dataset.ParquetFileFormat().make_write_options(compression='zstd')

s3= fs.S3FileSystem(
    region="ap-southeast-3"
    , endpoint_override='tos-s3-ap-southeast-3.volces.com:443'
    , access_key='xxx'
    , secret_key='xxx'
    , force_virtual_addressing=True         # 设置为 True 即指定为 virtual 模式 (划重点) | https://arrow.apache.org/docs/python/generated/pyarrow.fs.S3FileSystem.html
);


# 检查 查询权限
try:
    # 尝试列出桶内内容
    file_info = s3.get_file_info( fs.FileSelector("xxxEnv-tos-bigdata-private/xxxxxDir", recursive=False))
    print("✅ 基础连接成功!能够访问存储桶。")
except Exception as e:
    print(f"❌ 权限验证失败: {e}")


# 检查 写入权限
test_path = "xxxEnv-tos-bigdata-private/test_connection.txt"
try:
    with s3.open_output_stream(test_path) as stream:
        stream.write(b"connection test")
    print(f"✅ 写入权限验证成功!文件已创建: {test_path}")
    # 可选:测试完成后删除测试文件
    # s3.delete_file(test_path)
except Exception as e:
    print(f"❌ 写入失败: 请检查 s3:PutObject 权限。错误详情:\n{e}")


pa.dataset.write_dataset(
    table
    , base_dir='xxxEnv-tos-bigdata-private/xxxEnv/ODS/xxx/xxxDayNumber'
    , partitioning=['key1','key2']
    , basename_template='13acbf6ba3fe361bb48f262c3c0148a6-{i}'
    , format='parquet'
    , max_partitions=1000000
    , existing_data_behavior='overwrite_or_ignore'
    , max_open_files=1000000
    , file_options=file_options
    , filesystem=s3
)

rows=table.num_rows

Q: 火山云(TOS)不支持 PathStyle 访问模式,仅支持 VirtualHost 访问模式,导致Java/Python客户端中报 InvalidPathAccess 错误,如何配置解决?

问题描述

  • java 等支持 s3 协议的客户端报: InvalidPathAccess 错误
  • pyarrow 报: AWS Error ACCESS_DENIED during HeadBucket operation: No response body.
table = pq.read_table('/data/source/normal_data/xxxDayNumber/xxxDeviceIdHash')
file_options = pa.dataset.ParquetFileFormat().make_write_options(compression='zstd')
pa.dataset.write_dataset(table, base_dir='xxxEnv-tos-bigdata-private/xxxEnv/ODS/xxx/xxxDayNumber',partitioning=['key1','key2'],basename_template='13acbf6ba3fe361bb48f262c3c0148a6-{i}',format='parquet',max_partitions=1000000,existing_data_behavior='overwrite_or_ignore',max_open_files=1000000,file_options=file_options,filesystem=s3)

异常日志:

# python3 xxxx.py
  ...
  File "/usr/local/lib/python3.10/dist-packages/pyarrow/dataset.py", line 1035, in write_dataset
    _filesystemdataset_write(
  File "pyarrow/_dataset.pyx", line 4177, in pyarrow._dataset._filesystemdataset_write
  File "pyarrow/error.pxi", line 92, in pyarrow.lib.check_status
OSError: When testing for existence of bucket 'xxxEnv-tos-bigdata-private': AWS Error ACCESS_DENIED during HeadBucket operation: No response body.

root@xxx-ecs:~# pip list | grep -i pyarrow
pyarrow                   23.0.0

原因分析

  • 火山云(TOS) : 不支持 PathStyle 访问模式,仅支持 VirtualHost 访问模式,将导致Java/Python客户端中报 InvalidPathAccess 错误
  • 华为云(OBS) : 支持 PathStyle 访问模式

解决方法

Q: 对象存储之Amazon s3、s3a、s3n的区别是什么?

问题描述

  • trino 的 连接配置文件 iceberg.properties 中出现类似配置:
connector.name=iceberg

iceberg.catalog.type=hadoop
hive.metastore.warehouse.dir=s3a://lakehouse/iceberg_warehouse
hive.s3.endpoint=http://localhost:9000
hive.s3.aws-access-key=minioadmin
hive.s3.aws-secret-key=minioadmin
hive.s3.path-style-access=true
hive.s3.ssl.enabled=false```

那么,这几个 s3 协议头的区别是什么呢?

s3://
s3a://
s3n://

问题分析

  • 简单说,这三个前缀不是“S3 协议的变种",而是 Apache Hadoop 生态中三代不同的 S3 文件系统连接器——URI 里改一个字母,背后加载的就是完全不同的 Java 实现。演进路径是 s3://s3n://s3a://,目前 只有 s3a:// 还在活跃维护

三代连接器的本质区别

维度 s3:// s3n:// s3a://
引入版本 Hadoop 早期(2006) Hadoop 0.20+ (2008) Hadoop 2.6(2014),2.7 稳定
底层实现 在 S3 之上模拟 inode 风格的块存储 通过 jets3t 库直接读写 S3 原生对象 基于 AWS SDK 的原生对象访问
与其他 S3 工具的互操作性 ❌ 专有块结构,不兼容 ✅ 完全兼容 ✅ 完全兼容(且可读取 s3n 写入的数据)
单文件上限 受块大小控制 5 GB(S3 早期 API 限制) 5 TB(多段上传)
现状 Hadoop 3.0 已移除 冻结维护,仅修紧急安全 bug 唯一活跃维护的连接器

注:以上数据综合自 Apache Hadoop 官方文档与 Hortonworks 的演进史。

s3://:最早的"块存储"方案

  • s3:// 是 2006 年 HADOOP-574 引入的,当时 S3 对单个 blob 的大小有限制,所以它在 S3 之上做了一层 inode 式的块文件系统,把大文件切成块存进去。
  • 优点: 目录 rename/delete 很快;
  • 缺点:
  • 数据格式是 Hadoop 私有的,任何其他 S3 客户端都读不了
  • 需要一个专用 bucket,无法与其他工具共享数据
  • Hadoop 官方早已标记为 deprecatedHadoop 3.0 里直接删除

如果你在老代码里看到 s3://,且跑在自建 Hadoop 集群上——这是已被淘汰的方案,数据需要迁移。

s3n://:第一个"原生"S3 连接器

  • s3n:// 在 2008 年作为 s3:// 的继任者出现,核心改变是直接用 S3 的对象模型(路径 = bucket + key),因此:
  • 数据可以被非 Hadoop 的 S3 工具直接读写
  • 受限于当时 S3 API单文件最大 5 GB
  • 底层依赖 jets3t 库,因升级 jets3t 总会引入新 bug,Hadoop 社区最终选择冻结版本、不再积极维护
  • 已知缺陷:关闭输入流时会读到流末尾,导致大文件 seek() 很慢
  • s3n:// 是 2008-2014 年间 Apache Hadoop 读写 S3 的事实上标准前缀

s3a://:现代标准,基于 AWS SDK

  • s3a:// 从 Hadoop 2.6 引入(HADOOP-10400,Common Crawl 贡献),2.7 版本开始稳定生产可用,是目前唯一还在持续迭代的连接器。

  • 关键能力:

  • 基于 Amazon 官方 Java SDK(现已升级到 V2 SDK)
  • 通过多段上传把单文件上限拉到 5 TB
  • 支持 IAM 角色、环境变量、Hadoop 密钥管理等多种认证方式
  • 支持服务端加密(SSE-S3 / SSE-KMS / SSE-C)和客户端加密
  • 针对 ORC / Parquet 等【列式数据】的高性能随机 IO
  • 兼容 s3n:// 写入的数据,迁移无碍
  • Hadoop 3.x 里 s3://s3n:// 实现已被彻底移除,只剩 s3a://

选型建议

  • 新项目 / 现代 Hadoop (2.7+) / Spark / Flink → 一律用 s3a://,这是官方推荐的唯一选择

  • Amazon EMR 的特殊情况:EMR 里的 s3:// 不是 Apache Hadoop 那个已被废弃的实现,而是 Amazon 自己的闭源客户端,功能上等价于甚至优于 s3n://

所以,在 EMR 上继续用 s3:// 没问题;但 EMR 不支持 s3a://,要用就得用 Amazon 提供的那套实现。换句话说:

  • 自建开源 Hadoop/Spark 集群 → 用 s3a://
  • Amazon EMR → 用 s3://(Amazon 版)
  • 混合环境 / 跨平台作业 → 优先 s3a://,EMR 上改用对应的 Amazon S3 连接器
  • 小结:

s3:// 是块存储时代的遗物,s3n:// 是过渡方案,s3a:// 是基于 AWS SDK 的现代标准
看到代码里的 s3://s3n://,第一反应应该是"要不要迁移到 s3a://"。

Y 推荐文献

https://www.volcengine.com/docs/6349/148777?lang=zh
https://www.volcengine.com/docs/6349/107356?lang=zh

  • 雅加达 Region:
curl -V  https://${bucketName}.tos-s3-ap-southeast-3.volces.com:443
  • OBS 华为云:(参考)
Object URI=https://{bucketname}.obs.cn-north-4.myhuaweicloud.com:443

X 参考文献

posted @ 2026-02-11 00:26  千千寰宇  阅读(106)  评论(0)    收藏  举报