Higress 基於自定義插件訪問 Redis

作者:鈺誠

簡介

基於 wasm 機制,Higress 提供了優秀的可擴展性,用戶可以基於 Go/C++/Rust 編寫 wasm 插件,自定義請求處理邏輯,滿足用戶的個性化需求,目前插件已經支持 redis 調用,使得用戶能夠編寫有狀態的插件,進一步提高了 Higress 的擴展能力。

文檔在插件中調用 Redis [ 1] 中提供了完整的網關通過插件調用 Redis 的例子,包括阿里雲 Redis 實例創建與配置、插件代碼編寫、插件上傳與配置、測試樣例等流程。接下來本文重點介紹幾個基於 Redis 的插件。

多網關全侷限流

網關已經提供了 sentinal 限流 [2 ] ,能夠有效保護後端業務應用。通過 redis 插件限流,用戶可以實現多網關的全侷限額管理。

以下爲插件代碼示例,在請求頭階段檢查當前時間內請求次數,如果超出配額,則直接返回 429 響應。

func onHttpRequestHeaders(ctx wrapper.HttpContext, config RedisCallConfig, log wrapper.Log) types.Action {
    now := time.Now()
    minuteAligned := now.Truncate(time.Minute)
    timeStamp := strconv.FormatInt(minuteAligned.Unix(), 10)
    // 如果 redis api 返回的 err != nil,一般是由於網關找不到 redis 後端服務,請檢查是否誤刪除了 redis 後端服務
    err := config.client.Incr(timeStamp, func(response resp.Value) {
        if response.Error() != nil {
            log.Errorf("call redis error: %v", response.Error())
            proxywasm.ResumeHttpRequest()
        } else {
            ctx.SetContext("timeStamp", timeStamp)
            ctx.SetContext("callTimeLeft", strconv.Itoa(config.qpm-response.Integer()))
            if response.Integer() == 1 {
                err := config.client.Expire(timeStamp, 60, func(response resp.Value) {
                    if response.Error() != nil {
                        log.Errorf("call redis error: %v", response.Error())
                    }
                    proxywasm.ResumeHttpRequest()
                })
                if err != nil {
                    log.Errorf("Error occured while calling redis, it seems cannot find the redis cluster.")
                    proxywasm.ResumeHttpRequest()
                }
            } else {
                if response.Integer() > config.qpm {
                    proxywasm.SendHttpResponse(429, [][2]string{{"timeStamp", timeStamp}, {"callTimeLeft", "0"}}, []byte("Too many requests\n"), -1)
                } else {
                    proxywasm.ResumeHttpRequest()
                }
            }
        }
    })
    if err != nil {
        // 由於調用redis失敗,放行請求,記錄日誌
        log.Errorf("Error occured while calling redis, it seems cannot find the redis cluster.")
        return types.ActionContinue
    } else {
        // 請求hold住,等待redis調用完成
        return types.ActionPause
    }
}

插件配置如下:

測試結果如下:

結合通義千問實現 token 限流

對於提供 AI 應用服務的開發者來說,用戶的 token 配額管理是一個非常關鍵的功能,以下例子展示瞭如何通過網關插件實現對通義千問後端服務的 token 限流功能。

首先需要申請通義千問的 API 訪問,可參考此鏈接 [3 ] 。之後在 MSE 網關配置相應服務以及路由,如下所示:

編寫插件代碼,插件中,在響應 body 階段去寫入該請求使用的 token 額度,在處理請求頭階段去讀 redis 檢查當前剩餘 token 額度,如果已經沒有 token 額度,則直接返回響應,中止請求。

func onHttpRequestBody(ctx wrapper.HttpContext, config TokenLimiterConfig, body []byte, log wrapper.Log) types.Action {
  now := time.Now()
  minuteAligned := now.Truncate(time.Minute)
  timeStamp := strconv.FormatInt(minuteAligned.Unix(), 10)
  config.client.Get(timeStamp, func(response resp.Value) {
    if response.Error() != nil {
      defer proxywasm.ResumeHttpRequest()
      log.Errorf("Error occured while calling redis")
    } else {
      tokenUsed := response.Integer()
      if config.tpm < tokenUsed {
        proxywasm.SendHttpResponse(429, [][2]string{{"timeStamp", timeStamp}, {"TokenLeft", fmt.Sprint(config.tpm - tokenUsed)}}, []byte("No token left\n"), -1)
      } else {
        proxywasm.ResumeHttpRequest()
      }
    }
  })

  return types.ActionPause
}

func onHttpResponseBody(ctx wrapper.HttpContext, config TokenLimiterConfig, body []byte, log wrapper.Log) types.Action {
  now := time.Now()
  minuteAligned := now.Truncate(time.Minute)
  timeStamp := strconv.FormatInt(minuteAligned.Unix(), 10)
  tokens := int(gjson.ParseBytes(body).Get("usage").Get("total_tokens").Int())
  config.client.IncrBy(timeStamp, tokens, func(response resp.Value) {
    if response.Error() != nil {
      defer proxywasm.ResumeHttpResponse()
      log.Errorf("Error occured while calling redis")
    } else {
      if response.Integer() == tokens {
        config.client.Expire(timeStamp, 60, func(response resp.Value) {
          defer proxywasm.ResumeHttpResponse()
          if response.Error() != nil {
            log.Errorf("Error occured while calling redis")
          }
        })
      }
    }
  })
  return types.ActionPause
}

測試結果如下:

基於 cookie 的緩存、容災以及會話管理

除了以上兩個限流的例子,基於 Redis 可以實現更多的插件對網關進行擴展。例如基於 cookie 來做緩存、容災以及會話管理等功能。

  • 緩存&容災:基於用戶 cookie 信息緩存請求應答,一方面能夠減輕後端服務壓力,另一方面,當後端服務不可用時,能夠實現容災效果。
  • 會話管理:使用 Redis 存儲用戶的認證鑑權信息,當請求到來時,先訪問 redis 查看當前用戶是否被授權訪問,如果未被授權再去訪問認證鑑權服務,可以減輕認證鑑權服務的壓力。
func onHttpRequestHeaders(ctx wrapper.HttpContext, config HelloWorldConfig, log wrapper.Log) types.Action {
  cookieHeader, err := proxywasm.GetHttpRequestHeader("cookie")
  if err != nil {
    proxywasm.LogErrorf("error getting cookie header: %v", err)
    // 實現自己的業務邏輯
  }
    // 根據自己需要對cookie進行處理
  cookie := CookieHandler(cookieHeader)
  config.client.Get(cookie, func(response resp.Value) {
    if response.Error() != nil {
      log.Errorf("Error occured while calling redis")
      proxywasm.ResumeHttpRequest()
    } else {
      // 實現自己的業務邏輯
      proxywasm.ResumeHttpRequest()
    }
  })
  return types.ActionPause
}

總結

Higress 通過支持 redis 調用,大大增強了插件的能力,使插件功能具有更廣闊的想象空間,更加能夠適應開發者多樣的個性化需求,如果大家有更多關於 Higress 的想法與建議,歡迎與我們聯繫!

相關鏈接:

[1] 在插件中調用 Redis

https://help.aliyun.com/zh/mse/user-guide/develop-gateway-plug-ins-by-using-the-go-language?spm=a2c4g.11186623.0.0.45a53597EVVAC0#5e5a601af18al

[2] sentinal 限流

https://help.aliyun.com/zh/mse/user-guide/configure-a-throttling-policy?spm=a2c4g.11186623.0.i4

[3] 鏈接

https://help.aliyun.com/zh/dashscope/developer-reference/api-details?spm=a2c4g.11186623.0.i4#602895ef3dtl1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章