解决阿里云OSS无CDN时的流量风险
立泉对个人博客而言阿里云的OSS对象存储
服务非常适合存放诸如图片、视频、JS
、CSS
的静态资源,尤其非备案的海外站点,资源托管在国内云上是其在无法使用CDN
的条件下加速访问的几乎唯一解,从我2年来的使用体验来看对速度和稳定性都十分满意。
阿里OSS
并不“廉价”,但普通博客站点存储的数据不会很多,流量也不会很大,以标准存储
+按量计费计算,每GB存储0.12元,每GB流量0.25~0.5元,我的博客每月费用不过1元左右,约等于免费。
非正常
正常情况下费用可以忽略,但却存在一些“非正常”情况费用可能会高的吓人。最近看到一些恶意刷OSS
流量的帖子,有人一晚上几个小时被盗刷(下载)几TB流量产生了几千元账单:
很多案例都有一个特点就是阿里云在用户余额不足的情况下并没有及时停止服务,而是随流量的不断增加继续产生账单,一叠加就成百上千元。但是它的计费说明里明明写着欠费后停止服务,为什么还会出现这种情况?
原因是计费周期
和账单处理时延
,阿里云在《降低因恶意访问流量导致大额资金损失的风险》里有提到,虽然OSS
会在用户欠费时停止服务但这个“欠费”并非实时。按量付费
的账单周期是1小时,再加上处理账单的时延
,受到攻击时的峰值流量可能1小时后才会生成账单扣费,“余额不足”的判断也要在1小时后才会出现,这个时候即使停止服务在此时间差里消耗的流量也会产生费用,导致余额为负值。比如在7:00~8:00受到攻击,因为账单周期
和系统的处理时延
可能直到9:00才生成账单,此时已经被攻击2个小时,以普通家庭的千兆带宽被盗刷几TB流量是很轻松的。
解决
有些案例阿里云会减免一部分受攻击的费用,但毕竟它确实向运营商付出了外网流量成本,所以剩余费用还是要用户支付。对使用OSS
的小博主,这种巨额流量像一把悬在头上的达摩克利斯之剑,随时落下。
首先Referer防盗链
对这种恶意流量攻击是没用的,伪造Referer
非常简单,它只能起到防止被其它站点盗链
的作用。此外最直观的解决方法就是流量封顶
、带宽限制
和IP请求数限制
,但奇怪的是不止阿里云,其它云服务商的对象存储
也都不提供这类细粒度控制功能。而它们的CDN内容分发网络
却提供,并且流量费用更便宜,大概只有OSS
的一半,所以外套一层CDN
再配合它更细致的流量、带宽控制手段就是应对这种攻击行为最合适的方法。
只不过大陆法规不允许非备案站点使用国内CDN
,如果用国外Cloudflare
这样无需备案、无限流量的免费CDN
在国内访问又会是南辕北辙。本来低延时的OSS
请求经CDN
到国外绕一圈后,虽然面对流量攻击无需回源不会产生流量费用,但速度必然下降,与使用OSS
的加速初衷背道而驰,称其“减速CDN
”也不为过。
上面已经确认阿里云并没有给OSS
提供细粒度的用量控制功能,但文档中提到可以通过云监控设置对OSS
实时运行状态的报警规则。比如连续多少时间段请求数、流量达到多少时触发报警,在通知用户的同时还会触发一个可配置的HTTP
请求,配合OSS
本身的控制API
可以把Bucket
权限由公开访问变为Private
私有,这样停止OSS
对外服务后流量攻击也就随之失效。
是的,这就是解决流量风险的唯一方法,实时监控、触发报警、停止服务…我可以接受临时停服,不接受飞来账单😶。
函数计算
按照以上思路,需要搭建一个HTTP服务
来执行OSS
的权限变更操作,基于Serverless
的Function Compute函数计算
是一个很好的选择。Serverless
即无需购置服务器
,只要提供一个匹配HTTP
请求的函数
,它会在云服务商收到请求时被触发执行,其余时间则不消耗计算资源,也不产生费用,是按量付费
思路下的最优解。
阿里云的函数计算费用非常低,低到什么程度呢,调用价格0.009元/万次
,算力价格0.00009元/vCPUx秒
,内存价格0.000009元/GBx秒
,对这种很少触发的监控需求来说是等于0的。
一个调用OSS
权限变更API
的示例:
// Ktor的HTTP server
fun Application.configureRouting() {
routing {
// 处理报警触发的POST请求
post("/oss-shutdown") {
LogUtil.debug("oss shutdown start...")
val timeMs = measureTimeMillis {
// 执行时间可能长达数秒
shutdownOSS()
}
LogUtil.debug("oss shutdown done, time = $timeMs ms")
call.respondText("response to request: done")
}
}
}
private fun shutdownOSS() {
// OSS的外网接入点,这个是杭州地区的
val endpoint = "https://oss-cn-hangzhou.aliyuncs.com"
// RAM用户的访问ID和密钥,用于鉴权
val accessKeyId = "****************"
val accessKeySecret = "****************"
val credentialsProvider: CredentialsProvider = DefaultCredentialProvider(accessKeyId, accessKeySecret)
// 阿里云提供的控制OSS的SDK
val ossClient = OSSClientBuilder().build(endpoint, credentialsProvider)
try {
// 设置存储Bucket的读写权限为Private私有,终止外部访问
ossClient.setBucketAcl("bucket name", CannedAccessControlList.Private)
} catch (e: Exception) {
LogUtil.error("oss shutdown error, $e.cause")
} finally {
ossClient?.shutdown()
}
}
注意HTTP
触发的函数计算
和Spring
、Ktor
处理HTTP
请求的行为并不一样,阿里云收到请求会触发对应函数
执行,在函数
中返回HTTP
响应后其就会被立即终止,抛入其它线程的异步任务也不会存活,所以必须在执行完所需操作后才能返回响应。而且函数
默认是同步调用,报警请求在触发其执行后会等待响应,如果超过一定时间不回复,服务端收到超时反馈也会导致函数
在未完成的情况下被终止。
函数计算
中与同步调用
相对的还有异步调用,阿里云会对Header
里标识异步调用
的请求直接返回202
,然后再触发对应函数
的后台执行,可用来处理一些耗时任务。对操作OSS
而言,2核心4GB的实例冷启动
执行完成需要2秒,为避免报警等待超时,应该定义一个轻量的同步Trigger
,让其在触发操作OSS
的异步
任务后立即返回,这样既能启动耗时操作,也不会让报警请求等待过长时间。
创建Trigger
是因为报警配置只能指定URL
,不能自定义Header
,导致不能触发阿里云函数计算
的异步调用
,所以需要中间人。
做完这些之后,测试一下,如果成功的话那把悬在头上的达摩克利斯之剑也就可以扔掉了。