Play Scala 開(kāi)發(fā)技巧 - 請(qǐng)求限速

在系統(tǒng)開(kāi)發(fā)中,我們經(jīng)常需要保護(hù)一些安全性較高的接口,限制這些接口每秒處理的請(qǐng)求數(shù)量。例如對(duì)于一個(gè)計(jì)算密集型接口,假設(shè)壓測(cè)值是100rps, 如果實(shí)際情況長(zhǎng)期高于這個(gè)值,則會(huì)引起滾雪球效應(yīng),最終導(dǎo)致系統(tǒng)崩潰。下面我們一起來(lái)看看如何在 Play 中實(shí)現(xiàn)一個(gè)完全異步非阻塞的請(qǐng)求限速 ?本文代碼已提交至 play-community 項(xiàng)目,詳情請(qǐng)參考 controllers.demo.ThrottleDemoController 。

1 實(shí)現(xiàn)思路

當(dāng) Controller 接收到請(qǐng)求時(shí),為該請(qǐng)求建立一個(gè)“開(kāi)關(guān)”,并且把該“開(kāi)關(guān)”發(fā)送給“限速器”,"限速器"通過(guò)“開(kāi)關(guān)”控制請(qǐng)求的處理速度。本文采用 Promise 實(shí)現(xiàn)“開(kāi)關(guān)”,使用 Akka Stream 的 SouceQueue 實(shí)現(xiàn)“限速器”。

2 實(shí)現(xiàn)代碼

2.1 開(kāi)關(guān)

創(chuàng)建 ThrottledRequest 類(lèi),用于存放“開(kāi)關(guān)”和請(qǐng)求到達(dá)時(shí)間,

case class ThrottledRequest(promise: Promise[Boolean], time: Long)

2.2 限速器

使用 SourceQueue 創(chuàng)建“限速器”,處理請(qǐng)求限速和請(qǐng)求超時(shí),

val sourceQueue =
  Source
    .queue[ThrottledRequest](100, OverflowStrategy.backpressure)
    .throttle(1, 1.second, 1, ThrottleMode.shaping)
    .toMat(Sink.foreach{ r =>
      // 處理未超時(shí)請(qǐng)求
      if (System.currentTimeMillis() - r.time <= 15000) {
        r.promise.success(false)
      } else {
        r.promise.success(true)
      }
    })(Keep.left).run()

Source.queue 方法的聲明如下,

def queue[T](bufferSize: Int, overflowStrategy: OverflowStrategy): Source[T, SourceQueueWithComplete[T]]

這里我們?cè)O(shè)置緩沖區(qū)大小為 100, 設(shè)置緩沖區(qū)溢出時(shí)的處理策略為 backpressure ,以防止請(qǐng)求丟失。通過(guò) throttle 方法設(shè)置請(qǐng)求處理速度為 1 個(gè)每秒。 Sink 負(fù)責(zé)處理請(qǐng)求的放行和超時(shí)。

2.3 請(qǐng)求攔截

請(qǐng)求攔截 Action 負(fù)責(zé)攔截所有發(fā)往目標(biāo) Action 的請(qǐng)求,為每個(gè)請(qǐng)求創(chuàng)建“開(kāi)關(guān)”并發(fā)送給“限速器”,然后只放行被“限速器”打開(kāi)開(kāi)關(guān)的請(qǐng)求,

// 只有通過(guò)限速器(sourceQueue)的請(qǐng)求才會(huì)被執(zhí)行
def throttle[A](action: Action[A]) = Action.async(action.parser) { request =>
  val promise = Promise[Boolean]()
  sourceQueue.offer(ThrottledRequest(promise, System.currentTimeMillis()))
  promise.future.flatMap{ isTimeout =>
    if (!isTimeout) {
      action(request)
    } else {
      Future.successful(Forbidden("Timeout."))
    }
  }
}

2.4 目標(biāo) Action

目標(biāo) Action 組合了攔截 Action,代碼如下,

def throttledAction = throttle { Action { implicit request: Request[AnyContent] =>
  Ok("Finish.")
}}

2.5 定義路由

GET  /demo/throttle  controllers.demo.ThrottleDemoController.throttledAction

3 測(cè)試實(shí)現(xiàn)

上面我們實(shí)現(xiàn)了一個(gè)限速接口,每秒只處理 1 個(gè)請(qǐng)求,當(dāng)請(qǐng)求排隊(duì)超過(guò) 15 秒時(shí), 直接返回 403 響應(yīng)。 我們通過(guò)下面的測(cè)試代碼, 將 100 個(gè)請(qǐng)求瞬間發(fā)送過(guò)去,然后異步異步打印響應(yīng)信息,

val client = buildAHCClient
(1 to 100).foreach{ _ =>
  client.prepareGet(s"http://localhost:9000/demo/throttle")
    .execute(new AsyncCompletionHandler[Response] {
      override def onCompleted(response: Response): Response = {
        println(s"status: ${response.getStatusCode}, time: ${DateTime.now().toString("HH:mm:ss")}")
        response
      }
    })
}
Thread.sleep(100000)
client.close()

測(cè)試結(jié)果如下:

status: 200, time: 23:22:06
status: 200, time: 23:22:07
status: 200, time: 23:22:08
status: 200, time: 23:22:09
status: 200, time: 23:22:10
status: 200, time: 23:22:11
status: 200, time: 23:22:12
status: 200, time: 23:22:13
status: 200, time: 23:22:14
status: 200, time: 23:22:15
status: 200, time: 23:22:16
status: 200, time: 23:22:17
status: 200, time: 23:22:18
status: 200, time: 23:22:19
status: 200, time: 23:22:20
status: 200, time: 23:22:21
status: 403, time: 23:22:22
status: 403, time: 23:22:23
...

從上面可以看出,請(qǐng)求按照到達(dá)順序依次被處理,從響應(yīng)時(shí)間上看,目標(biāo)接口確實(shí)每秒只處理 1 個(gè)請(qǐng)求, 并且從 23時(shí)22分22秒 開(kāi)始,后面的請(qǐng)求均被超時(shí)處理。

4 小結(jié)

異步非阻塞代碼雖然寫(xiě)起來(lái)有點(diǎn)麻煩,并且不易于調(diào)試,但是在系統(tǒng)性能方面收益是巨大的。在相同的系統(tǒng)性能指標(biāo)下,異步非阻塞代碼可以讓硬件成本降到最低。理論上,使用異步非阻塞方式編寫(xiě)的系統(tǒng)可以在單個(gè)線(xiàn)程上運(yùn)行,并且可以保證較高的并發(fā)性,典型例子是Node.js。Play Framework 是一個(gè)完全異步非阻塞的 Web 開(kāi)發(fā)框架,相信在不久的將來(lái)在國(guó)內(nèi)會(huì)越來(lái)越受歡迎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容