构建基于 OpenrResty + Lua 的短地址服务

作者: 昊炜 分类: 分享 发布时间: 2019-09-12 16:42

0x01: 编写 lua 脚本

以下为示例代码,使用了 MySQL 作为持久化的存储,使用了 OpenResty 自带的 lua_shared_dict 作为缓存,这里可以根据自身的实际需求去更换不同的存储。

local host = os.getenv("APP_FRONTEND_URL")
local db_host = os.getenv("DATABASE_HOST")
local db_port = os.getenv("DATABASE_PORT")
local db_database = os.getenv("DATABASE")
local db_user = os.getenv("DATABASE_USERNAME")
local db_password = os.getenv("DATABASE_PASSWORD")
local expire = os.getenv("CACHE_EXPIRED_MINUTES")

if host == nil then
  host = "SHORTLINK_DEFAULT_REDIRECT"
end

if db_host == nil or db_database == nil or db_user == nil or db_password == nil then
  ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
end

if db_port == nil then
  db_port = 3306
end

if expire == nil then
  expire = 15
end

local uri = ngx.var.request_uri

if not uri or uri == "/"
  ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
end

local links = ngx.shared.links
local redirect, flags, err = links:get(uri)

if not redirect or redirect == nil then

  local mysql = require "resty.mysql"
  local db, err = mysql:new()
  if not db then
    ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
  end

  db:set_timeout(1000)

  local ok, err, errno, sqlstate = db:connect{
    host = db_host,
    port = db_port,
    database = db_database,
    user = db_user,
    password = db_password,
    max_packet_size = 1024 * 1024
  }

  if not ok then
    db:close()
    ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
  end

  sql = "SELECT value FROM short_link WHERE `key` = " .. "\'" .. uri .. "\'" .. " ORDER BY created_at DESC LIMIT 1"

  local res, err, errno, sqlstate = db:query(sql)
  if not res or next(res) == nil then
    db:close()
    ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
  end

  local result = res[1]["value"]

  if(type(result) ~= "string") then
    db:close()
    ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
  end

  if string.match(result, "^https-://[%w+%.]+[/%w+]+.*") ~= nil then
    redirect = result
  elseif string.match(result, "^%w[%w+%.]+.*") ~= nil then
    redirect = "http://" .. result
  else
    redirect = host .. "/" .. result
  end

  local ok, err = links:set(uri, redirect, 60 * expire)
  if not ok then
    db:close()
    ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
  end

  db:close()
end

ngx.redirect(redirect, ngx.HTTP_MOVED_PERMANENTLY)

0x02: 搭建本地测试环境

本地的测试环境使用 docker 进行搭建,编写好 docker-compose 和相关配置文件后就可以进行测试。

# docker-compose.yaml
version: '3'
services:
  mysql:
    image: mysql:5.7
    ports:
      - "3306:3306"
    expose:
      - "3306"
    volumes:
      - ./data/mysql:/var/lib/mysql
      - ./logs/mysql:/var/log/mysql
      - ./etc/mysql:/etc/mysql
    environment:
      MYSQL_USER: test
      MYSQL_PASSWORD: test123456
      MYSQL_DATABASE: test
      MYSQL_ROOT_PASSWORD: test123456
    networks:
      - test
  openresty:
    image: openresty/openresty:centos
    ports:
      - 8080:8080
      - 80:80
    volumes:
      - ./etc/nginx/conf.d:/etc/nginx/conf.d
      - ./etc/nginx/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf
      - ./logs/nginx:/var/log/nginx/
      - ./lualib/shortlink.lua:/usr/local/openresty/lualib/shortlink.lua;
    environment:
      APP_FRONTEND_URL: xxxxxxx
      DATABASE_HOST: xxxxxxx
      DATABASE_PORT: xxxxxxx
      DATABASE: xxxxxxx
      DATABASE_USERNAME: xxxxxxx
      DATABASE_PASSWORD: xxxxxxx
      CACHE_EXPIRED_MINUTES: xxxxxxx
    networks:
      - test
    entrypoint:
        - /usr/bin/openresty
        - -g
        - daemon off;
networks:
  test:

# nginx.conf
user nobody;
worker_processes  1;

pid  logs/nginx.pid;
error_log  logs/error.log  notice;

env APP_FRONTEND_URL;
env DATABASE;
env DATABASE_HOST;
env DATABASE_PORT;
env DATABASE_USERNAME;
env DATABASE_PASSWORD;
env CACHE_EXPIRED_MINUTES;

events {
  worker_connections  1024;
}

http {
  include       mime.types;
  default_type  application/octet-stream;

  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';

  access_log  logs/access.log  main;

  gzip  on;
  sendfile  on;
  tcp_nopush  on;
  keepalive_timeout  60;
  lua_shared_dict links 128m;  # 用于分配缓存大小
  include /etc/nginx/conf.d/*.conf;
}
    
# shortlink.conf
server {
      listen       80;
      server_name  SHORTLINK_VHOST_NAME;
      access_log  /var/log/nginx/shortlink.access.log;
      error_log  /var/log/nginx/shortlink.error.log;

      resolver local=on ipv6=off;
      resolver_timeout 5s;

      location / {
          default_type text/plain;
          content_by_lua_file /usr/local/openresty/lualib/shortlink.lua;
      }

      location = /robots.txt {
            return 200 "User-Agent: *\nDisallow: ";
      }

      location = /probe {
          return 200 "";
      }
      
      #error_page  404              /404.html;

      # redirect server error pages to the static page /50x.html
      #
      error_page   500 502 503 504  /50x.html;
      location = /50x.html {
          root   /usr/local/openresty/nginx/html;
      }

      # deny access to .htaccess files, if Apache's document root
      # concurs with nginx's one
      #
      location ~ /\.ht {
        deny  all;
      }
    }

可以注意到,上面的 nginx 配置文件中加入了多条 env directives是为了让 nginx worker 可以继承传入的环境变量。resolver local=on ipv6=off;这行配置是为了 nginx 可以读取到 /etc/resolve.conf中的默认 dns 地址,也可以通过给地址自行配置,不配置会出现 no resolver defined to resolve xxx.xxx的错误,导致 lua 无法正常访问数据库。

0x03: 编写部署文件

完成上述工作后,就可以着手开始进行部署工作了,部署文件示例如下:

# config-map
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-shortlink-proxy
  annotations:
    configVersion: CONFIG_VERSION
  namespace: APP_ENV
data:
  nginx.conf: |
    user nobody;
    worker_processes  SHORTLINK_WORKER_NUM;

    pid  logs/nginx.pid;
    error_log  logs/error.log  notice;

    env APP_FRONTEND_URL;
    env DATABASE;
    env DATABASE_HOST;
    env DATABASE_PORT;
    env DATABASE_USERNAME;
    env DATABASE_PASSWORD;
    env CACHE_EXPIRED_MINUTES;

    events {
        worker_connections  1024;
    }

    http {
        include       mime.types;
        default_type  application/octet-stream;

        log_format  main  '$remote_addr - $remote_user [$time_local] 
                          "$request" '     '$status $body_bytes_sent 
                          "$http_referer" ' '"$http_user_agent"        
                          "$http_x_forwarded_for"';
        
        access_log  logs/access.log  main;

        gzip  on;
        sendfile  on;
        tcp_nopush  on;
        keepalive_timeout  60;
        lua_shared_dict links 128m;  
        include /etc/nginx/conf.d/*.conf;
    }

  shortlink.conf: |
    server {
      listen       80;
      server_name  SHORTLINK_VHOST_NAME;
      access_log  /var/log/nginx/shortlink.access.log;
      error_log  /var/log/nginx/shortlink.error.log;

      resolver local=on ipv6=off;
      resolver_timeout 5s;

      location / {
          default_type text/plain;
          content_by_lua_file /usr/local/openresty/lualib/shortlink.lua;
      }

      location = /robots.txt {
            return 200 "User-Agent: *\nDisallow: ";
      }

      location = /probe {
          return 200 "";
      }
      
      #error_page  404              /404.html;

      # redirect server error pages to the static page /50x.html
      #
      error_page   500 502 503 504  /50x.html;
      location = /50x.html {
          root   /usr/local/openresty/nginx/html;
      }

      # deny access to .htaccess files, if Apache's document root
      # concurs with nginx's one
      #
      location ~ /\.ht {
        deny  all;
      }
    }
  
  shortlink.lua: |
    local host = os.getenv("APP_FRONTEND_URL")
    local db_host = os.getenv("DATABASE_HOST")
    local db_port = os.getenv("DATABASE_PORT")
    local db_database = os.getenv("DATABASE")
    local db_user = os.getenv("DATABASE_USERNAME")
    local db_password = os.getenv("DATABASE_PASSWORD")
    local expire = os.getenv("CACHE_EXPIRED_MINUTES")

    if host == nil then
        host = "SHORTLINK_DEFAULT_REDIRECT"
    end

    if db_host == nil or db_database == nil or db_user == nil or db_password ==nil then
        ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
    end

    if db_port == nil then
        db_port = 3306
    end

    if expire == nil then
        expire = 15
    end

    local uri = ngx.var.request_uri

    if not uri or uri == "/"
        ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
    end

    local links = ngx.shared.links
    local redirect, flags, err = links:get(uri)

    if not redirect or redirect == nil then

        local mysql = require "resty.mysql"
        local db, err = mysql:new()
        if not db then
            ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
        end

        db:set_timeout(1000)

        local ok, err, errno, sqlstate = db:connect{
            host = db_host,
            port = db_port,
            database = db_database,
            user = db_user,
            password = db_password,
            max_packet_size = 1024 * 1024
        }

        if not ok then
            db:close()
            ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
        end

        sql = "SELECT value FROM shortlink WHERE `key` = " .. "\'" .. uri .. "\'" .. " ORDER BY time DESC LIMIT 1"

        local res, err, errno, sqlstate = db:query(sql)
        if not res or next(res) == nil then
            db:close()
            ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
        end

        local result = res[1]["value"]

        if(type(result) ~= "string") then
            db:close()
            ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
        end

        if string.match(result, "^https-://[%w+%.]+[/%w+]+.*") ~= nil then
            redirect = result
        elseif string.match(result, "^%w[%w+%.]+.*") ~= nil then
            redirect = "http://" .. result
        else
            redirect = host .. "/" .. result
        end
        
        local ok, err = links:set(uri, redirect, 60 * expire)
        if not ok then
            db:close()
            ngx.redirect(host, ngx.HTTP_MOVED_PERMANENTLY)
        end

        db:close()
    end

    ngx.redirect(redirect, ngx.HTTP_MOVED_PERMANENTLY)

# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: dp-shortlink-proxy
  labels:
      app: shortlink-proxy
  namespace: APP_ENV
spec:
  replicas: 1
  selector:
    matchLabels:
      app: shortlink-proxy
  template:
    metadata:
      annotations:
        configVersion: CONFIG_VERSION
        service.shortlink.version: SHORTLINK_VERSION
      labels:
        app: shortlink-proxy
    spec:
      imagePullSecrets:
      - name: registry-key
      containers:
      - name: shortlink-proxy
        image: SHORTLINK_IMAGE_REPOSITORY:SHORTLINK_VERSION
        imagePullPolicy: IfNotPresent
        env:
        - name: APP_FRONTEND_URL
          value: "https://APP_FRONTEND_SERVER_NAME"
        - name: DATABASE_HOST
          valueFrom:
            secretKeyRef:
              name: APP_ENV-database-secret
              key: host
        - name: DATABASE_PORT
          value: "DB_PORT"
        - name: DATABASE
          value: "DB_DATABASE"
        - name: DATABASE_USERNAME
          valueFrom:
            secretKeyRef:
              name: APP_ENV-database-secret
              key: username
        - name: DATABASE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: APP_ENV-database-secret
              key: password
        - name: CACHE_EXPIRED_MINUTES 
          value: "SHORTLINK_CACHE_TIMEOUT"
        ports:
        - name: shortlink-svc
          containerPort: 80
        volumeMounts:
        - name: shortlink-proxy-logs
          mountPath: /usr/local/nginx/logs
        - name: shortlink-proxy-conf
          mountPath: /usr/local/openresty/nginx/conf/nginx.conf
          subPath: nginx.conf
        - name: shortlink-proxy-conf
          mountPath: /etc/nginx/conf.d/shortlink.conf
          subPath: shortlink.conf
        - name: shortlink-proxy-conf
          mountPath: /usr/local/openresty/lualib/shortlink.lua
          subPath: shortlink.lua
        resources:
          requests:
            cpu: 100m
            memory: 300Mi
          limits:
            cpu: 2000m
            memory: 2000Mi
        command: ["/usr/bin/openresty"]
        args: ["-g", "daemon off;"]
        readinessProbe:
          httpGet:
            path: /probe
            port: 80
          initialDelaySeconds: 10
          periodSeconds: 10
      - name: filebeat
        image: SHORTLINK_IMAGE_REPOSITORY:SHORTLINK_VERSION
        imagePullPolicy: IfNotPresent
        workingDir: /opt/filebeat
        volumeMounts:
        - name: shortlink-proxy-logs
          mountPath: /var/logs/nginx
        - name: filebeat-conf
          mountPath: /opt/filebeat/filebeat.yml
          subPath: filebeat.yml
        command: ["./filebeat"]
        args: ["-e", "-c", "./filebeat.yml"]
      volumes:
      - name: shortlink-proxy-logs
        emptyDir: {}
      - name: shortlink-proxy-conf
        configMap:
          name: cm-shortlink-proxy
      - name: filebeat-conf
        configMap:
          name: cm-filebeat

# Service
apiVersion: v1
kind: Service
metadata:
    name: svc-shortlink-proxy
    annotations:
        service.shortlink.protocol: http
    labels:
        app: shortlink-proxy
    namespace: APP_ENV
spec:
    type: LoadBalancer
    ports:
    - name: shortlink-svc
      port: 80
      protocol: TCP
      targetPort: shortlink-svc
    selector:
        app: shortlink-proxy

# Secret
apiVersion: v1
kind: Secret
metadata:
  name: APP_ENV-database-secret
type: Opaque
stringData:
  host: DB_HOST
  username: DB_USERNAME
  password: DB_PASSWORD

上面的配置含有很多环境变量,我采取的方案是结合 env文件和脚本进行统一替换,编写好配置文件后,依次将上述配置文件应用到部署集群后就可以正式对外提供服务了。完结撒花,如果有什么写的不对的地方,欢迎各位观众老爷指出。

0x04 短地址怎么来,这里有个小 demo

<?php

function generateShortLinks(string $url, int $short_len = 6) : array
{
    $base_chars = [
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
        'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
        'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
        'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
        'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2',
        '3', '4', '5', '6', '7', '8', '9'
    ];
    $short_arr = [];
    $md5_arr = str_split(md5($long_url), 8);
    foreach ($md5_arr as $md5_str) {
        $hex = hexdec($md5_str) & 0x3fffffff;
        $short_str = null;
        for ($i = 0; $i < $short_len; $i++) {
            $index = 0x0000003d & $hex;
            $short_str .= $base_chars[$index];
            $hex = $hex >> ($short_len - 1);
        }
        array_push($short_arr, $short_str);
    }
    return $short_arr;
}


$long_url = 'https://www.apple.com/cn/';

var_dump(generateShortLinks($long_url));

发表评论

电子邮件地址不会被公开。 必填项已用*标注