Higress 基於自定義外掛訪問 Redis

阿里云云栖号發表於2024-04-28

簡介

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

Higress 基於自定義外掛訪問 Redis

文件在外掛中呼叫 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
    }
}

外掛配置如下:

Higress 基於自定義外掛訪問 Redis

測試結果如下:

Higress 基於自定義外掛訪問 Redis

結合通義千問實現 token 限流

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

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

Higress 基於自定義外掛訪問 RedisHigress 基於自定義外掛訪問 Redis

編寫外掛程式碼,外掛中,在響應 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
}

測試結果如下:

Higress 基於自定義外掛訪問 RedisHigress 基於自定義外掛訪問 Redis

基於 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

作者: 鈺誠

原文連結

本文為阿里雲原創內容,未經允許不得轉載。

相關文章