剛好想說要用Varish來做一下Minio(S3)的cache, 研究了一下順便做個紀錄

先在Ubuntu上裝來試試, 可以用apt 來安裝:

apt install varnish

在Ubuntu 20.04上(WSL用的版本)是6.2.1的版本, 最新版應該是7.2, 不過沒差, 做法都一樣

Varnish預設的設定在/etc/varnish/default.vcl, 打開這檔案你就可以看到像這樣的內容:

vcl 4.0;

# Default backend definition. Set this to point to your content server.
backend default {
    .host = "127.0.0.1";
}
sub vcl_recv {
    # Happens before we check if we have this in cache already.
    #
    # Typically you clean up the request here, removing cookies you don't need,
    # rewriting the request, etc.
}

sub vcl_backend_response {
    # Happens after we have read the response headers from the backend.
    #
    # Here you clean the response headers, removing silly Set-Cookie headers
    # and other mistakes your backend does.
}

sub vcl_deliver {
    # Happens when we have all the pieces we need, and are about to send the
    # response to the client.
    #
    # You can do accounting or modifying the final object here.
}

Varnish的設定檔用的是一種叫做vcl的語言, 它會被Varnish先compile過後才會被使用, 所以改好這檔案後, 如果你跑 sudo system start varnish (這是WSL2上用的, 其他地方可能就是systemctl), 如果你寫錯了, 一開始跑就可以發現出錯了

以上面那個例子來說, 它會預設快取你local上的web server

但如果是要連接Minio (S3)是不夠的, 因為如果單純把backend設成 Minio server, 那client還是會需要access key和secret key才可以存取, 如果你希望讓它跟存取靜態網站一樣, 那你可以能會希望把這兩個設定放在後端

Vanish出場是沒支援可以call S3 API的, 這時候就要透過一個VMOD - AWSRest, 這VMOD是可以在你去backend (Minio/S3) 拿資料前先幫你用你的access key, secret key算好簽章(signature), 所以我們要先安裝這個VMOD

安裝VMOD你會先需要libvarnishapi-dev, 可以用apt install libvarnishapi-dev來安裝, 另外AWSRest還會需要mhash, 你還會需要安裝apt-get install libmhash-dev

裝好後, 從 https://github.com/xcir/libvmod-awsrest 抓取最新的source code, 進入目錄後執行

./autogen.sh
./configure
make
sudo make install

沒意外的話就可以完成安裝, 要確認是不是已經安裝好了, 我們可以在default.vcl加上

vcl 4.0;
import awsrest;

# ....

重啟varnish有成功, 表示應該是沒啥問題才對

我在我本地端電腦跑了個Minio, port為9000, 有一個bucket叫做mmmbux, 裡面有個檔案, key為20220101/a.c, access key為TGhYs2FYBGMYueAz, secrect key為IM2SgF7LxIlZVbeo3Vv7OdQzA7pnZFB1, Varnish則是跑在port 6081上

首先我們來看看怎讓client/browser在不用提供access key/secret key的狀況下可以存取物件

vcl 4.0;
import awsrest;

backend default {
    .host = "127.0.0.1";
    .port = "9000";
}

sub vcl_recv {
    set req.http.host = "127.0.0.1";
    awsrest.v4_generic(
        service           = "s3",
        region            = "ap-northeast-1",
        access_key        = "TGhYs2FYBGMYueAz",
        secret_key        = "IM2SgF7LxIlZVbeo3Vv7OdQzA7pnZFB1",
        signed_headers    = "host;",
        canonical_headers = "host:" + req.http.host + awsrest.lf()
    );
}

Ok, 其實就很簡單的在vcl_recv上加上那幾行就好, 這時候你就可以用 http://localhost:6081/mmmbux/20220101/a.c 來存取 mmmbux 這bucket上 2022/0101/a.c 這個檔案了

那, 如果我不想把bucket name當作url的一部分呢?

sub vcl_recv {
    set req.http.host = "127.0.0.1";
    set req.url = "mmmbux/" + req.url
    awsrest.v4_generic(
        service           = "s3",
        region            = "ap-northeast-1",
        access_key        = "TGhYs2FYBGMYueAz",
        secret_key        = "IM2SgF7LxIlZVbeo3Vv7OdQzA7pnZFB1",
        signed_headers    = "host;",
        canonical_headers = "host:" + req.http.host + awsrest.lf()
    );
}

上面這段就是把你進來的url加上mmmbux/當新的url, 這樣做的話, 你的新url就會是 http://localhost:6081/20220101/a.c

那如果我想進一步, 把它變成 http://localhost:6081/files/20220101/a.c 呢?

sub vcl_recv {
    set req.http.host = "127.0.0.1";
    
    if (req.url ~ "^/files/") {
        set req.url = regsub(req.url, "^/files/", "/mmmbux/");
        awsrest.v4_generic(
          service           = "s3",
          region            = "ap-northeast-1",
          access_key        = "TGhYs2FYBGMYueAz",
          secret_key        = "IM2SgF7LxIlZVbeo3Vv7OdQzA7pnZFB1",
          signed_headers    = "host;",
          canonical_headers = "host:" + req.http.host + awsrest.lf()
        );
    } else {
        return(synth(404));
    }
}

上面這段就是把/files/後面的都到 mmmbux這bucket去抓, 然後其他目錄都回傳 404 Not found

那如果我想要用docker跑也要有這VMod呢? 我把這Dockerfile的範例放在https://github.com/julianshen/varnish-awsrest-docker, Varnish 官方的docker image有提供 install-vmod 這script讓你安裝vmod, 所以只需要給它awsrest的tarball: https://github.com/xcir/libvmod-awsrest/archive/refs/tags/v70.12.tar.gz 即可, 我做了一個現成的image放在quay.io/jlnshen/varnish-awsrest

一般來說, container通常會設計成只專注在它單一的任務上, 也就是通常不會把一個http server跟db server跑在同一個container內, Kubernetes 的Pod的設計, 讓我們可以在同一個Pod內放多個containers, 因此可以延伸出init container, sidecar container來輔助原本的container, 中間可以透過分享Volume或是直接透過loopback網路來共享資料, 但還是會有情境是, 你會希望可以在某個container空間內執行某個程式, docker的話, 你可以用 docker exec ,那在Kubernetes呢?

Kubernetes 也是可以用kubectl 達成同樣的目的, 像是:

 kubectl exec mycontainer ls

其實跟docker exec 是類似的, 如果是要執行shell進去執行其他的維護:

kubectl exec mycontainer -it /bin/sh

但如果是, 你要從其他的pod去執行其他的pod裡面的指令呢? 像是用Cron job定期去執行某特定pod裡面的程式?

我們也是可以透過呼叫Kubernetes API來達成這目的的, 如同下面這個範例:

func runCommand(clientset *kubernetes.Clientset, pod string, ns string, cmd ...string) (string, error) {
	req := clientset.CoreV1().RESTClient().Post().Resource("pods").Name(pod).Namespace(ns).SubResource("exec")
	option := &v1.PodExecOptions{
		Command: cmd,
		Stdin:   false,
		Stdout:  true,
		Stderr:  true,
		TTY:     false,
	}

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	req.VersionedParams(option, scheme.ParameterCodec)

	exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
	if err != nil {
		return "", err
	}

	sopt := remotecommand.StreamOptions{
		Stdout: &stdout,
		Stderr: &stderr,
	}

	err = exec.Stream(sopt)

	if err != nil {
		return "", err
	}

	if stderr.String() != "" {
		return stdout.String(), errors.New(stderr.String())
	}

	return stdout.String(), nil
}

由於Kubernetes API還沒包裝exec這個資源, 所以要用.Resource("pods").Name(pod).Namespace(ns).SubResource("exec")去取用, stdin, stdout, stderr都還是可以串接回來的

但如果你直接在你的pod內執行這段的話, 其實不會成功的, 因為你沒有權限可以去做, 你必須先設定好一個Role如下:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: executor
rules:
  - verbs:
      - get
      - list
    apiGroups:
      - ''
    resources:
      - pods
      - pods/log
  - verbs:
      - create
    apiGroups:
      - ''
    resources:
      - pods/exec

我的情境是會用list pods找出pod的名稱, 在用這個名稱去找到特定的pod執行, 所以會需要前半段get和list的權限, 後半段對pods/execcreate的權限才是執行這段程式真正需要的

要達成這件事, 只需要利用turf.js就好, turf.js提供了一大堆處理地理資訊的相關工具, 只拿來算距離, 還真算有點小用 :P

以下是範例:

const { point } = require('@turf/helpers');
const distance = require('@turf/distance').default;

const from = point([120.9758828, 24.8043379]);
const to = point([120.92846560331556, 24.846169955749275]);

const result = distance(from, to, { units: 'kilometers' });

這邊point回傳的是GeoJson的Feature, 由於GeoJson是經度在前緯度在後, 如果你去Google map複製座標來的話, 會剛好相反, 這邊需要注意, 以上就可以算出這兩點到底距離幾公里了

turf.js可以做的相當多, 算距離只是其中之一, 如果你要找出像是包含所有點的最小矩形, 或是合併多個多邊形(像是合併行政區域), 都可拿來使用

WSL 當作開發環境固然方便好用的, 但也吃蠻大空間的, 最近在開發的東西, 需要跑一個postgresql, 裝不少資料, 吃掉我蠻多硬碟空間的, 偏偏我SSD就只有小小的512G (好吧, 的確寒酸到不像開發者的電腦), 一下子就吃滿滿了, 所以就必須要把這個給搬到我另一個比較大的磁碟救急(說是救急的意思是, 預期它會吃上1TB, 不過這又是會碰到一個問題, 之後再解了)

WSL新開的映像檔都是放在C:(畢竟不是謎片,不會自動住到D槽…咦?!), 要把它搬家的話, 需要先把它export出來, export的方法很簡單:

wsl --shutdown
wsl --export Ubuntu-20.04 d:\ubuntuback.tar

先shutdown是想保險一點, 所以也先把相關的視窗(像是Terminal, VSCode)都關一關, 如果映像檔很大, 這預期要做非常久, 像我這個有200G以上, 放下去, 基本上我就去看電視不管它了, 當然也要確保一下你目標硬碟夠大, 這邊Ubuntu-20.04是我要備份的目標, 如果不知道名字是啥可以用wsl -l查詢

export完之後, 接著就用:

wsl --import Ubuntu20dev e:\wsl\dev D:\ubuntuback.tar

這邊Ubuntu20dev是新的名字, 不要跟舊的重複了, import完後就可以用wsl -d Ubuntu20dev 登入進去玩了

不過登入後, 咦, 等一下, 怎麼會是用root? 之前舊的並不是呀! 要解決這個問題, 新增這個檔案/etc/wsl.conf, 裡面內容是

[user]
default=yourloginname

把wsl再shutdown之後再重新進來就不會是root了

PostGIS 是讓PostgresSQL可以支援地理資訊資料的一個擴充, 而geometryPostGIS定義來儲存地理資料的資料型態, 包含了座標點(POINT), 線, 多邊形等等

GORM則是Golang界算蠻有名的ORM套件, 支援了蠻多不同的關聯式資料庫

不過GORM是沒有直接支援PostGIS的geometry這個資料型態的, 畢竟geometry並非一般SQL標準的資料型別, 因此如果要讓GORM可以來存取geometry, 就得使用它所提供的Customize Data Type

要存取這種自訂的資料型別, 需要實做ScannerValuer這兩個介面:

type Scanner interface {
	// Scan assigns a value from a database driver.
	//
	// The src value will be of one of the following types:
	//
	//    int64
	//    float64
	//    bool
	//    []byte
	//    string
	//    time.Time
	//    nil - for NULL values
	//
	// An error should be returned if the value cannot be stored
	// without loss of information.
	//
	// Reference types such as []byte are only valid until the next call to Scan
	// and should not be retained. Their underlying memory is owned by the driver.
	// If retention is necessary, copy their values before the next call to Scan.
	Scan(src interface{}) error
}

type Valuer interface {
	// Value returns a driver Value.
	// Value must not panic.
	Value() (Value, error)
}

那在實做前, 可以先來看看geometry實際存到資料庫是長怎麼樣? 它長的就會像是一串這樣的文字0101000020E61000001E64C73CEA905EC09CD6C962A7C84240, 看起來就是十六進位編碼過的文字, 根據文件, 它是經過EWKB編碼過的(EWKB是Postgis從WKB延伸來的), 然後編碼過的binary資料再轉成Hex字串

這部分的解碼, 可以不用自己寫, 使用open source的好處就是可以踩在前人的肩膀上, 可以利用go-goem來幫我們處理這件事, 這邊以處理座標點為例 (採用常見的EPSG:4326座標系統), 定義一個叫做GeoPoint的型別給GORM使用, 而這個型別的實作可以是這樣:

type GeoPoint geom.Point

func (g *GeoPoint) Scan(val interface{}) error {
	pt, err := ewkbhex.Decode(val.(string))

	if err == nil {
		if p, ok := pt.(*geom.Point); ok {
			*g = GeoPoint(*p)
		} else {
			return errors.New(fmt.Sprint("Failed to unmarshal geometry:", val))
		}
	}

	return err
}

func (g GeoPoint) Value() (driver.Value, error) {
	pt := &g
	toPt, err := ewkbhex.Encode((*geom.Point)(pt).SetSRID(4326), binary.BigEndian)

	return toPt, err
}

其實非常簡單的利用了go-goem提供的ewkbhex來編解碼而已