Knative 實戰:三步走!基於 Knative Serverless 技術實現一個短網址服務

短網址顧名思義就是使用比較短的網址代替很長的網址。維基百科上面的解釋是這樣的:html

短網址又稱網址縮短、縮短網址、URL 縮短等,指的是一種互聯網上的技術與服務,此服務能夠提供一個很是短小的 URL 以代替原來的可能較長的URL,將長的 URL 位址縮短。用戶訪問縮短後的 URL 時一般將會重定向到原來的長 URL

起源

雖然如今互聯網已經很是發達了,但仍是有不少場景會對用戶輸入的內容有長度限制。好比 :git

  • 微薄、Twitter 長度不能超過 140 個字
  • 一些早期的 BBS 文章單行的長度不能超過 78 字符等場景
  • 運營商短信的長度不能超過 70 個字

而如今不少媒體、電商平臺的內容大多都是多人協做經過比較複雜的系統、框架生成的,連接長度幾十個甚至上百字符都是很日常的事情,因此若是在上述的幾個場景中傳播連接使用短網址服務就是一個必然的結果。好比下面這些短信截圖你應該不會陌生:github

應用場景

短網址服務的最初本意就是縮短長 url,方便傳播。但其實短網址服務還能作不少其餘的事情。好比下面這些:數據庫

  • 訪問次數的限制,好比只能訪問 1 次,第二次訪問的時候就拒絕服務
  • 時間的限制,好比只能在一週內提供訪問服務,超過一週就拒絕服務
  • 根據訪問者的地域的限制
  • 經過密碼訪問
  • 訪問量統計
  • 高峯訪問時間統計等等
  • 統計訪問者的一些信息,好比:api

    • 來源城市
    • 訪問時間
    • 使用的終端設備、瀏覽器
    • 訪問來源 IP
  • 在營銷活動中其實還能夠對不一樣的渠道生成不通的短網址,這樣經過統計這些短網址還能判斷不一樣渠道的訪問量等信息

基於 Knative Serverless 技術實現一個短網址服務

在 Knative 模式下能夠實現按需分配,沒有流量的時候實例縮容到零,當有流量進來的時候再自動擴容實例提供服務。
如今咱們就基於阿里雲容器服務的 Knative 來實現一個 serverless 模式的短網址服務。本示例會給出一個完整的 demo,你能夠本身在阿里雲容器服務上面建立一個 Knative 集羣,使用本示例提供服務。本示例中實現一個最簡單的功能瀏覽器

  • 經過接口實現長網址到短網址的映射服務
  • 當用戶經過瀏覽器訪問短網址的時候經過 301 跳轉到長網址

下面咱們一步一步實現這個功能app

數據庫

既然要實現短網址到長網址的映射,那麼就須要保存長網址的信息到數據庫,而且生成一個短的 ID 做爲短網址的一部分。因此咱們首先須要選型使用什麼數據庫。在本示例中咱們選擇使用阿里雲的表格存儲,表格存儲最大的優點就是按量服務,你只須要爲你使用的量付費,並且價格也很實惠。以下所示的按量計費價格表。1G 的數據保存一年的費用是3.65292元/年( 0.000417 24 365=3.65292) ,是否是很划算。框架

短網址生成 API

咱們須要有一個 API 生成短網址less

/new?origin-url=${長網址}curl

  • origin-url 訪問地址

返回結果

vEzm6v

假設咱們服務的域名是 short-url.default.serverless.kuberun.com ,那麼如今訪問 http://short-url.default.serverless.kuberun.com/vEzm6v 就能夠跳轉到長網址了。

代碼實現

package main

import (
    "crypto/md5"
    "encoding/hex"
    "fmt"
    "log"
    "net/http"
    "os"
    "strconv"
    "time"

    "strings"

    "github.com/aliyun/aliyun-tablestore-go-sdk/tablestore"
)

var (
    alphabet = []byte("abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
    l        = &Log{}
)

func ShortUrl(url string) string {
    md5Str := getMd5Str(url)
    var tempVal int64
    var result [4]string
    for i := 0; i < 4; i++ {
        tempSubStr := md5Str[i*8 : (i+1)*8]
        hexVal, _ := strconv.ParseInt(tempSubStr, 16, 64)
        tempVal = 0x3FFFFFFF & hexVal
        var index int64
        tempUri := []byte{}
        for i := 0; i < 6; i++ {
            index = 0x0000003D & tempVal
            tempUri = append(tempUri, alphabet[index])
            tempVal = tempVal >> 5
        }
        result[i] = string(tempUri)
    }
    return result[0]
}

func getMd5Str(str string) string {
    m := md5.New()
    m.Write([]byte(str))
    c := m.Sum(nil)
    return hex.EncodeToString(c)
}

type Log struct {
}

func (log *Log) Infof(format string, a ...interface{}) {
    log.log("INFO", format, a...)
}

func (log *Log) Info(msg string) {
    log.log("INFO", "%s", msg)
}

func (log *Log) Errorf(format string, a ...interface{}) {
    log.log("ERROR", format, a...)
}

func (log *Log) Error(msg string) {
    log.log("ERROR", "%s", msg)
}

func (log *Log) Fatalf(format string, a ...interface{}) {
    log.log("FATAL", format, a...)
}

func (log *Log) Fatal(msg string) {
    log.log("FATAL", "%s", msg)
}

func (log *Log) log(level, format string, a ...interface{}) {
    var cstSh, _ = time.LoadLocation("Asia/Shanghai")
    ft := fmt.Sprintf("%s %s %s\n", time.Now().In(cstSh).Format("2006-01-02 15:04:05"), level, format)
    fmt.Printf(ft, a...)
}

func handler(w http.ResponseWriter, r *http.Request) {
    l := &Log{}
    l.Infof("Hello world received a request, url: %s", r.URL.Path)
    l.Infof("url:%s ", r.URL)
    //if r.URL.Path == "/favicon.ico" {
    //    http.NotFound(w, r)
    //    return
    //}

    urls := strings.Split(r.URL.Path, "/")
    originUrl := getOriginUrl(urls[len(urls)-1])
    http.Redirect(w, r, originUrl, http.StatusMovedPermanently)
}

func new(w http.ResponseWriter, r *http.Request) {
    l.Infof("Hello world received a request, url: %s", r.URL)
    l.Infof("url:%s ", r.URL)
    originUrl, ok := r.URL.Query()["origin-url"]
    if !ok {
        l.Errorf("no origin-url params found")
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("Bad request!"))
        return
    }

    surl := ShortUrl(originUrl[0])
    save(surl, originUrl[0])
    fmt.Fprint(w, surl)

}

func getOriginUrl(surl string) string {
    endpoint := os.Getenv("OTS_TEST_ENDPOINT")
    tableName := os.Getenv("TABLE_NAME")
    instanceName := os.Getenv("OTS_TEST_INSTANCENAME")
    accessKeyId := os.Getenv("OTS_TEST_KEYID")
    accessKeySecret := os.Getenv("OTS_TEST_SECRET")
    client := tablestore.NewClient(endpoint, instanceName, accessKeyId, accessKeySecret)

    getRowRequest := &tablestore.GetRowRequest{}
    criteria := &tablestore.SingleRowQueryCriteria{}

    putPk := &tablestore.PrimaryKey{}
    putPk.AddPrimaryKeyColumn("id", surl)
    criteria.PrimaryKey = putPk

    getRowRequest.SingleRowQueryCriteria = criteria
    getRowRequest.SingleRowQueryCriteria.TableName = tableName
    getRowRequest.SingleRowQueryCriteria.MaxVersion = 1

    getResp, _ := client.GetRow(getRowRequest)
    colmap := getResp.GetColumnMap()
    return fmt.Sprintf("%s", colmap.Columns["originUrl"][0].Value)
}

func save(surl, originUrl string) {
    endpoint := os.Getenv("OTS_TEST_ENDPOINT")
    tableName := os.Getenv("TABLE_NAME")
    instanceName := os.Getenv("OTS_TEST_INSTANCENAME")
    accessKeyId := os.Getenv("OTS_TEST_KEYID")
    accessKeySecret := os.Getenv("OTS_TEST_SECRET")
    client := tablestore.NewClient(endpoint, instanceName, accessKeyId, accessKeySecret)

    putRowRequest := &tablestore.PutRowRequest{}
    putRowChange := &tablestore.PutRowChange{}
    putRowChange.TableName = tableName

    putPk := &tablestore.PrimaryKey{}
    putPk.AddPrimaryKeyColumn("id", surl)
    putRowChange.PrimaryKey = putPk

    putRowChange.AddColumn("originUrl", originUrl)
    putRowChange.SetCondition(tablestore.RowExistenceExpectation_IGNORE)
    putRowRequest.PutRowChange = putRowChange

    if _, err := client.PutRow(putRowRequest); err != nil {
        l.Errorf("putrow failed with error: %s", err)
    }
}

func main() {
    http.HandleFunc("/", handler)
    http.HandleFunc("/new", new)
    port := os.Getenv("PORT")
    if port == "" {
        port = "9090"
    }

    if err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil); err != nil {
        log.Fatalf("ListenAndServe error:%s ", err.Error())
    }

}

代碼我已經編譯成鏡像,你能夠直接使用 registry.cn-hangzhou.aliyuncs.com/knative-sample/shorturl:v1 此鏡像編部署服務。

三步走起!!!

第一步 準備數據庫

首先到阿里雲開通表格存儲服務,而後建立一個實例和表。咱們須要的結構比較簡單,只須要短 URL ID 到長 URL 的映射便可,存儲表結構設計以下:

名稱 描述
id 短網址 ID
originUrl 長網址

第二步 獲取 access key

登錄到阿里雲之後鼠標浮動在頁面的右上角頭像,而後點擊 accesskeys 跳轉到 accesskeys 管理頁面

點擊顯示便可顯示 Access Key Secret

第三步 部署服務

Knative Service 的配置以下, 使用前兩步的配置信息填充 Knative Service 的環境變量。而後部署到 Knative集羣便可

apiVersion: serving.knative.dev/v1alpha1
kind: Service
metadata:
  name: short-url
  namespace: default
spec:
  template:
    metadata:
      labels:
        app: short-url
      annotations:
        autoscaling.knative.dev/maxScale: "20"
        autoscaling.knative.dev/minScale: "0"
        autoscaling.knative.dev/target: "100"
    spec:
      containers:
        - image: registry.cn-hangzhou.aliyuncs.com/knative-sample/shorturl:v1
          ports:
            - name: http1
              containerPort: 8080
          env:
          - name: OTS_TEST_ENDPOINT
            value: http://t.cn-hangzhou.ots.aliyuncs.com
          - name: TABLE_NAME
            value: ${TABLE_NAME}
          - name: OTS_TEST_INSTANCENAME
            value: ${OTS_TEST_INSTANCENAME}
          - name: OTS_TEST_KEYID
            value: ${OTS_TEST_KEYID}
          - name: OTS_TEST_SECRET
            value: ${OTS_TEST_SECRET}

使用上面的 knative service 部署服務,部署好之後多是下面這樣:

└─# kubectl get ksvc
short-url       http://short-url.default.serverless.kuberun.com       short-url-456q9       short-url-456q9       True

如今能夠開始測試

  • 生成一個短網址
└─# curl 'http://short-url.default.serverless.kuberun.com/new?origin-url=https://help.aliyun.com/document_detail/121534.html?spm=a2c4g.11186623.6.786.41e074d9oHpbO2'
vEzm6v

curl 命令輸出的結果 VR7baa 就是短網址的 ID

小結

本實戰咱們只需三步就基於 Knative 實現了一個 Serverless 的短網址服務,此短網址服務在沒有請求的時候能夠縮容到零節省計算資源,在有不少請求的時候能夠自動擴容。而且使用了阿里雲表格存儲,這樣數據庫也是按需付費。基於 Knative + TableStore 實現了短網址服務的 Serverless 化。



本文做者:一綠舟

原文連接

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索