解决阿里云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()
}
}
不同云服务商的函数计算
接口不同,很多都需要引入SDK
按照规定的方式接收Event
事件。阿里云支持的自定义运行时则更通用化,只是转发请求
,能使用流行的Spring
、Ktor
等框架。其HTTP
触发器收到请求
后会在指定配置的实例中启动用户上传的程序,直接把请求
转发到实例的80
端口,收到响应
即判断任务完成,然后终止程序。
注意函数计算
启动的Ktor
,其生命周期和在普通服务器上并不一样,返回HTTP响应
后会被立即终止,抛入后台线程的任务不会存活,所以必须在执行完所有操作后才能返回响应
。而且函数
默认是同步调用,如果超过一定时间不回复报警请求
,函数计算
收到超时反馈也会导致程序在未完成的情况下被终止。
函数计算
中与同步调用
相对的还有异步调用,阿里云会对Header
里标识异步调用
的请求
先返回202
,再触发对应程序的后台执行,更适合处理耗时任务。操作OSS
的JVM
程序从冷启动
到执行完成需耗时数秒(本例是2秒),为避免报警
等待超时,应该定义一个轻量的同步Trigger
,让其在触发异步
任务后立即返回,这样既能启动耗时操作也不会让报警请求
等待过长时间。
创建Trigger
是因为报警配置只能指定URL
,不能自定义Header
,导致不能触发异步调用
,所以需要中间人。
做完测试一下,如果成功那把悬在头上的达摩克利斯之剑就可以扔掉了。