解决阿里云OSS无CDN时的流量风险

立泉

对个人博客而言阿里云的OSS对象存储服务非常适合存放一些诸如图片、视频、JS、CSS之类的静态资源,尤其非备案的海外站点,资源托管在国内云上是其在无法使用CDN的条件下加速访问的几乎唯一解,从我自己2年来的使用体验来看对其速度和稳定性都十分满意。

Aliyun OSS overview

阿里的OSS并不免费,但普通博客站点存储的数据并不会很多,流量也不会很大,以标准存储使用按量计费来计算,每GB存储0.12元,每GB流量0.25~0.5元,我的博客每月费用也不过1元左右,约等于免费了。

非正常

正常情况下费用可以忽略,但却存在一些“非正常”情况费用却可能会高的吓人…最近看到很多恶意刷OSS流量的帖子,有人一晚上几个小时被盗刷(下载)几TB流量产生了几千元的账单:

很多案例都有一个特点就是阿里云在用户余额不足的情况下并没有及时停止服务,而是随流量的不断增加继续产生账单,一叠加就成百上千元。但是它的计费说明里明明写着欠费后会停止服务,为什么还会出现这种情况?

原因是计费周期账单处理时延,阿里云在《降低因恶意访问流量导致大额资金损失的风险》里有提到,虽然OSS会在用户欠费时停止服务但这个“欠费”并不是实时的。按量付费账单周期是1小时,再加上处理账单的时延,受到攻击时的峰值流量可能1小时后才会生成账单扣费,“余额不足”的判断也要在1小时后才会出现,这个时候即使停止服务在此时间差里消耗的流量也会产生费用,导致余额为负值。比如在7:00~8:00受到攻击,因为账单周期和系统的处理时延可能直到9:00才会生成账单,此时已经被攻击了2个小时,以普通家庭的千兆带宽被盗刷几TB流量是很轻松的。

解决

有些案例阿里云会减免一部分受攻击的费用,但毕竟它也确实向运营商付出了外网流量成本,所以剩余费用还是要用户支付,所以我这样的小博主还是得想办法避免这种巨额流量,否则它就像一把悬在头上的达摩克利斯之剑,随时会落下…

首先Refer防盗链对这种恶意流量攻击是没用的,伪造Refer非常简单,它只能起到防止被其它站点盗链的作用。此外最直观的解决方法就是流量封顶带宽限制IP请求数限制,但奇怪的是不止阿里云,其它云服务商的对象存储也都不提供这类细粒度控制功能。而它们的CDN内容分发网络却提供,并且流量费用更便宜,大概只有OSS的一半,所以外套一层CDN再配合它更细致的流量、带宽控制手段就是应对这种攻击行为最合适的方法。

只不过大陆法规不允许非备案站点使用国内CDN,如果用国外Cloudflare这样无需备案、无限流量的免费CDN在国内访问又会是南辕北辙,因为本来国内低延时的快速OSS请求经CDN到国外绕一圈后,虽然在面对流量攻击时无需回源不会产生流量费用,但速度必然明显下降,与使用OSS的加速初衷背道而驰,称其“减速CDN”也不为过…所以CDN就别想了。

上面已经确认阿里云并没有给OSS提供细粒度的用量控制功能,但文档中提到可以通过云监控设置对OSS实时运行状态的报警规则。比如连续多少时间段请求数、流量达到多少时触发报警,在通知用户的同时还会触发一个可配置的HTTP请求,再配合OSS本身的控制API可以把Bucket权限由公开访问变为Private私有,这样就变相停止了OSS的对外服务,流量攻击也就会随之失效。

是的,这就是解决流量风险的唯一方法,实时监控、触发报警、停止服务…我可以接受临时停服,不接受飞来账单😶。

函数计算

按照以上思路,需要搭建一个HTTP服务来响应执行OSS的权限变更操作,基于ServerlessFunction 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触发的函数计算SpringKtor处理HTTP请求的行为并不一样,阿里云收到请求会触发对应函数执行,在函数中返回HTTP响应后其就会被立即终止,抛入其它线程的异步任务也不会存活,所以必须在执行完所需操作后才能返回响应。而且函数默认是同步调用,报警请求在触发其执行后会等待响应,如果超过一定时间不回复,服务端收到超时反馈也会导致函数在未完成的情况下被终止。

函数计算中与同步调用相对的还有异步调用,阿里云会对Header里标识异步调用的请求直接返回202,然后再触发对应函数的后台执行,可用来处理一些耗时任务。对操作OSS而言,4核心8GB的实例冷启动执行完成需要2秒,为避免报警等待超时,应该定义一个轻量的同步Trigger,让其在触发操作OSS异步任务后立即返回,这样既能启动耗时操作,也不会让报警请求等待过长时间。

创建Trigger是因为报警配置只能指定URL,不能自定义Header,导致不能触发阿里云函数计算异步调用,所以需要中间人。

做完这些之后,测试一下,如果成功的话那把悬在头上的达摩克利斯之剑也就可以扔掉了。

arrow_upward