文档章节

Knative 实战:三步走!基于 Knative Serverless 技术实现一个短网址服务

阿里巴巴云原生
 阿里巴巴云原生
发布于 09/20 17:24
字数 2127
阅读 5
收藏 0

短网址顾名思义就是使用比较短的网址代替很长的网址。维基百科上面的解释是这样的:

短网址又称网址缩短、缩短网址、URL 缩短等,指的是一种互联网上的技术与服务,此服务可以提供一个非常短小的 URL 以代替原来的可能较长的URL,将长的 URL 位址缩短。用户访问缩短后的 URL 时通常将会重定向到原来的长 URL

<a name="f19742d4"></a>

起源

虽然现在互联网已经非常发达了,但还是有很多场景会对用户输入的内容有长度限制。比如 :

  • 微薄、Twitter 长度不能超过 140 个字
  • 一些早期的 BBS 文章单行的长度不能超过 78 字符等场景
  • 运营商短信的长度不能超过 70 个字

而现在很多媒体、电商平台的内容大多都是多人协作通过比较复杂的系统、框架生成的,链接长度几十个甚至上百字符都是很平常的事情,所以如果在上述的几个场景中传播链接使用短网址服务就是一个必然的结果。比如下面这些短信截图你应该不会陌生:<br /> file

<a name="3dbf0c11"></a>

应用场景

短网址服务的最初本意就是缩短长 url,方便传播。但其实短网址服务还能做很多其他的事情。比如下面这些:

  • 访问次数的限制,比如只能访问 1 次,第二次访问的时候就拒绝服务
  • 时间的限制,比如只能在一周内提供访问服务,超过一周就拒绝服务
  • 根据访问者的地域的限制
  • 通过密码访问
  • 访问量统计
  • 高峰访问时间统计等等
  • 统计访问者的一些信息,比如:
    • 来源城市
    • 访问时间
    • 使用的终端设备、浏览器
    • 访问来源 IP
  • 在营销活动中其实还可以对不同的渠道生成不通的短网址,这样通过统计这些短网址还能判断不同渠道的访问量等信息

<a name="84db27ce"></a>

基于 Knative Serverless 技术实现一个短网址服务

在 Knative 模式下可以实现按需分配,没有流量的时候实例缩容到零,当有流量进来的时候再自动扩容实例提供服务。<br />现在我们就基于阿里云容器服务的 Knative 来实现一个 serverless 模式的短网址服务。本示例会给出一个完整的 demo,你可以自己在阿里云容器服务上面创建一个 Knative 集群,使用本示例提供服务。本示例中实现一个最简单的功能

  • 通过接口实现长网址到短网址的映射服务
  • 当用户通过浏览器访问短网址的时候通过 301 跳转到长网址

下面我们一步一步实现这个功能

<a name="68051bf4"></a>

数据库

既然要实现短网址到长网址的映射,那么就需要保存长网址的信息到数据库,并且生成一个短的 ID 作为短网址的一部分。所以我们首先需要选型使用什么数据库。在本示例中我们选择使用阿里云的表格存储,表格存储最大的优势就是按量服务,你只需要为你使用的量付费,而且价格也很实惠。如下所示的按量计费价格表。1G 的数据保存一年的费用是3.65292元/年( 0.000417 _ 24 _ 365=3.65292) ,是不是很划算。

file

<a name="7f69ad70"></a>

短网址生成 API

我们需要有一个 API 生成短网址

/new?origin-url=${长网址}

  • origin-url 访问地址

返回结果

vEzm6v

假设我们服务的域名是 short-url.default.serverless.kuberun.com ,那么现在访问 http://short-url.default.serverless.kuberun.com/vEzm6v 就可以跳转到长网址了。

<a name="83175ad0"></a>

代码实现

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 此镜像编部署服务。

<a name="b8cdb0b0"></a>

三步走起!!!

<a name="cea7acd0"></a>

第一步 准备数据库

首先到阿里云开通表格存储服务,然后创建一个实例和表。我们需要的结构比较简单,只需要短 URL ID 到长 URL 的映射即可,存储表结构设计如下:

名称 描述
id 短网址 ID
originUrl 长网址

<a name="110252e6"></a>

第二步 获取 access key

登陆到阿里云以后鼠标浮动在页面的右上角头像,然后点击 accesskeys 跳转到 accesskeys 管理页面<br /> file <br />点击显示即可显示 Access Key Secret<br /> file

<a name="c3070031"></a>

第三步 部署服务

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

<a name="5db9fd7c"></a>

小结

本实战我们只需三步就基于 Knative 实现了一个 Serverless 的短网址服务,此短网址服务在没有请求的时候可以缩容到零节省计算资源,在有很多请求的时候可以自动扩容。并且使用了阿里云表格存储,这样数据库也是按需付费。基于 Knative + TableStore 实现了短网址服务的 Serverless 化。

“ 阿里巴巴云原生微信公众号(ID:Alicloudnative)关注微服务、Serverless、容器、Service Mesh等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的技术公众号。”

© 著作权归作者所有

阿里巴巴云原生

阿里巴巴云原生

粉丝 28
博文 104
码字总数 323836
作品 0
杭州
私信 提问
三步走!基于 Knative Serverless 技术实现一个短网址服务

作者 | 阿里云智能事业群技术专家 牛秋霖(冬岛) 短网址顾名思义就是使用比较短的网址代替很长的网址。维基百科上面的解释是这样的: 短网址又称网址缩短、缩短网址、URL 缩短等,指的是一种...

阿里巴巴云原生
09/20
0
0
《Knative 实战:三步走!基于 Knative Serverless 技术实现一个短网址服务》

短网址顾名思义就是使用比较短的网址代替很长的网址。维基百科上面的解释是这样的: 短网址又称网址缩短、缩短网址、URL 缩短等,指的是一种互联网上的技术与服务,此服务可以提供一个非常短...

短网址顾名思义就是使用比较短的网址代替很长的网址。维基百科上面的解释是这样的:
09/23
0
0
Knative:重新定义 Serverless | GIAC 实录

Knative 是Google 发起的 Serverless 项目,希望通过提供一套简单易用的 Serverless 开源方案,将 Serverless 标准化。 本文根据敖小剑在 2018 年上海 GIAC 演讲内容整理,文末有PPT获取地址...

s花苞酱
01/02
0
0
Knative 实战:基于 Knative Serverless 技术实现天气服务-下篇

上一期我们介绍了如何基于 Knative Serverless 技术实现天气服务-上篇,首先我们先来回顾一下上篇介绍的内容: 通过高德天气 API 接口,每隔 3 个小时定时发送定时事件,将国内城市未来 3 天...

阿里巴巴云原生
10/12
14
0
Knative-开源的Serverless架构方案

Knative(发音为 kay-nay-tiv)是谷歌开源的一套 Serverless 架构方案,它扩展了 Kubernetes,提供了一组中间件,提高了构建可在本地、云和第三方数据中心等地方运行的现代化、以源为中心且基...

openthings
05/13
242
0

没有更多内容

加载失败,请刷新页面

加载更多

JS 打印控制

JS 打印控制 var PrintStartString = "<!--打印开始标示符-->";//设置打印开始区域var PrintEndString = "<!--打印结束标示符-->";//设置打印结束区域var HtmlText = window.do......

DrChenXX
15分钟前
5
0
LevelDB:使用介绍

LevelDB 提供的接口其实很简单,下面举例进行简单说明。 安装 git clone https://github.com/google/leveldb cd leveldb mkdir -p build && cd build cmake -DCMAKE_BUILD_TYPE=Release .. ......

slagga
20分钟前
5
0
《JavaScript正则表达式迷你书》读书笔记

正则基础 常见简写形式 字符组 具体含义 记忆方式 \d 表示 [0-9]。表示是一位数字。 其英文是 digit(数字) \D 表示 [^0-9]。表示除数字外的任意字符。 \w 表示 [0-9a-zA-Z_]。表示数字、大小...

muzi131313
25分钟前
4
0
Git的反悔操作

概述 这次主要来讲讲Git的反悔操作,自己平时在写代码的过程中经常会出现想要弃用所有的改动或回滚到上一次commit的情况。Git上的反悔操作有reset、rebase、revert等,每个操作各有区别和对应...

duduYZ
26分钟前
2
0
实现双向绑定Proxy比defineproperty优劣如何?

前言 双向绑定其实已经是一个老掉牙的问题了,只要涉及到MVVM框架就不得不谈的知识点,但它毕竟是Vue的三要素之一. Vue三要素 响应式: 例如如何监听数据变化,其中的实现方法就是我们提到的双向...

寻找海蓝
38分钟前
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部