一、aws-go-sdk简介
AWS SDK for Go,是由亚马逊网络服务(Amazon Web Services,AWS)提供的官方软件开发工具包(SDK),用于使用Go编程语言构建与对象存储服务交互的应用程序。
AWS SDK for Go 支持与 AWS S3(Amazon Simple Storage Service)进行交互,提供了用于执行各种 基于S3 协议操作的方法,例如上传对象、下载对象、列出存储桶内容、删除对象等。可以使用这些方法来管理 S3 存储桶和对象。
目前国内外大多数存储厂商的对象存储都兼容s3协议,因此用可以直接用AWS SDK for Go对其对象存储进行操作。
二、 sdk使用以及容易踩中的坑
1、创建session
在使用 aws-go-sdk时,需要配置访问密钥、区域(Region)和其他与 S3 服务相关的参数来先创建session。这些参数将帮助 SDK 确定要与哪个支持 S3协议 存储桶进行通信。以下是一个创建session的例子:
session, _ := session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials("access_key", "secret_key", ""),
Endpoint: aws.String("end_point"),
Region: aws.String("ap-east-1"),
})
这样就创建了一个session,并由此创建client进行各类操作,但是直接这样创建往往会有很多问题,以下列举一下可能会碰到的坑。
(1)S3ForcePathStyle参数
默认情况下, S3 使用虚拟主机样式的 URL,其中存储桶名称出现在主机名的子域名中。这是 Amazon S3 的推荐 URL 格式,因为它更灵活,支持更多功能,并且在不同区域之间更加透明。目前国内外绝大多数云厂商都支持这种URL访问格式。
但是,对于一些特殊情况或需要与非标准 S3 存储系统进行交互时,有时只支持路径样式 URL。路径样式的 URL 中,存储桶名称出现在主机名后面的路径中。此时如果直接用上述方式创建session,在进行操作的时候会报错,这个时候需要将aws.Config中的S3ForcePathStyle设置为true,表示强制使用路径样式的 URL,如下所示:
session, _ := session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials("access_key", "secret_key", ""),
Endpoint: aws.String("end_point"),
Region: aws.String("ap-east-1"),
S3ForcePathStyle: aws.Bool(true)
})
(2)DisableRestProtocolURICleaning参数
本人曾经碰到过这种情况,上传一个名为a//b.txt的文件,上传完成后发现文件名变成了a/b.txt,或者明明有一个名为a//b.txt的对象在存储中,发送GET请求去获取,返回404文件不存在,打开debug日志,发现发送的请求中获取的对象名变成了a/b.txt。
这让人丈二和尚摸不着头脑,为啥对象名被改掉了。研究了sdk代码发现原来是SDK会自动尝试清理和规范化请求中的 URI,以使其符合 RESTful API 规范。这包括自动处理斜杠(/)和路径中的双斜杠(//),以确保最终的 URI 符合标准。因此连续的斜杠会被处理成一个斜杠,而开头的斜杠会被直接去掉。
但是实际上,对象存储的对象名千奇百怪,虽然官方建议不用奇奇怪怪的对象名,但是架不住存储用户自己想用。那么对于对象名中出现了不符合RESTful API的字符,因该如何使其操作成功呢。DisableRestProtocolURICleaning 是创建session时候的一个配置选项,用于控制是否启用 REST 协议 URI 清理。这个参数默认为false,即启动清理,如果需要关掉清理功能,则可以将其设置为true,这样你在进行上传、读取等操作的时候,sdk不会自动处理你的请求url,传入的是什么就是什么,示例如下所示:
session, _:= session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials("access_key", "secret_key", ""),
Endpoint: aws.String("end_point"),
Region: aws.String("ap-east-1"),
DisableRestProtocolURICleaning: aws.Bool(true),
})
(3)httpclient参数
一些生产环境需要高并发发送大量请求的模块在运行过程中曾经出现过这样的问题:运行一段时间后发现请求花费时间变得很久。使用命令查看系统状态,发现大量的端口被占用,查看sdk源码发现session中的httpclient使用的是default client,每建立一个连接就会占一个端口,当并发数较高的时候,连接没有被及时地关闭和释放,会导致端口资源耗尽。
为了解决这个问题,建议用自己创建的http.Client,其中使用http.Transport 来管理连接池,设置 MaxIdleConns 和 IdleConnTimeout 等参数来控制连接的数量和生命周期。防止端口资源被过多占用,示例如下:
sess, e := session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials("access_key", "secret_key", ""),
Endpoint: aws.String("end_point"),
Region: aws.String("ap-east-1"),
DisableRestProtocolURICleaning: aws.Bool(true),
HTTPClient: &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
IdleConnTimeout: 30 * time.Second,
},
Timeout: 60 * time.Second,
},
})
上面实例中设置了最大空闲连接数为100,空闲连接超时时间为30秒。这样就避免了大量端口被占用并得不到及时释放。
2、 添加公共头部
在发送请求的时候有些时候因为业务的需要,在请求中需要加上自定义头部,然而在每个请求中单独去添加头部比较麻烦,这个时候可以在session中设置好handlers,这样只要是由该session创建的相关的连接,都会带上这个头部,不用每个请求单独去加。假设创建了一个名为sess的session,可以像下面这个实例这样设置公共头部:
sess.Handlers.Send.PushFront(func(r *request.Request) {
r.HTTPRequest.Header.Set("headerKey", "headerValue")
})
刚开始的时候并没有发现这种写法有问题,但是偶然在一次事件中,发现了一个问题:有些时候请求第一次发送的时候因为网络抖动等外部因素失败了,sdk会默认进行重试发送,但是奇怪的事情出现了,重试请求竟然返回了403鉴权失败!
这就很奇怪了,为什么第一次发送请求鉴权通过了,重试请求中的密钥这些参数并没有变,但是重试请求却鉴权失败?
研究一下代码,发现了这样一个问题:
在request.Handlers中,有很多方法,如下所示:
这些方法是依次执行的,而鉴权这一步是在Sign这个步骤中,而我自定义的头部是在Send这一步加上的,第一次执行的时候请求中还没有自定义的头部,因此鉴权不会将这个头部加入签名,在形成签名后,请求中再加入我自定义的头部,这没有影响,鉴权能够正常通过。而第二次重试,sdk会复用之前构造的请求,请求中已经有我自定义的头部了,这个时候会将这个头部加入签名,在完成签名之后,又触发了Send这一步,修改了我自定义的头部。
这个时候问题来了:如果我这个头部是固定的键值对,那么无所谓,再触发一次Send并不会改变这个自定义头部的值,这个值还是和第一次请求中的值一样。但是对于我面对的业务场景,却不是这样,因为我自定义头部的值中是带有当前的时间戳的!!
这样就导致了重试的时候,Sign这一步用了带有a时间戳的头部进行签名,而签完名之后这个头部又被改成了带有b时间戳的值!!这样服务端接收到请求进行鉴权的时候,发现这个头部参与签名的值与头部当前值对不上,直接返回403鉴权失败。
为了结果这个问题,设置公共头部的方式可以改成如下所示:
sess.Handlers.Build.PushFront(func(r *request.Request) {
r.HTTPRequest.Header.Set("headerKey", "headerValue")
})
将设置自定义头部这一步放在Build,在Sign之前,这样每次重试都会先更新这个自定义头部,再用新头部参与签名,这样就能对的上了,鉴权可以正常通过。