[OSS/S3] 对象存储 FAQ
1 概述: 对象存储-常见问题
2 FAQ for 对象存储(OSS/S3)
Q: 访问URL的风格对比: Virtual-Host-style(虚拟主机式) vs. Path-style(路径式)
- Host-style 访问: https://bucket.minio.example.com
- Path-style 访问: https://minio.example.com/bucket
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
- Volcengine TOS SDK for Python | https://pypi.org/project/tos/
- 火山引擎 TOS Python SDK | https://github.com/volcengine/ve-tos-python-sdk/blob/main/README-zh.md
- 示例代码
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 : pyarrow 的 pyarrow.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: 火山云(TOS) 客户端的访问差异及示例?"
- 参见官方: 使用 PathStyle 方式访问 TOS 时,报错 InvalidPathAccess - 火山云/TOS
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 官方早已标记为
deprecated,Hadoop 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 推荐文献
-
火山云 TOS 客户端 / endpoint :
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 参考文献
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!

浙公网安备 33010602011771号