Quantcast
Channel: 小惡魔 – 電腦技術 – 工作筆記 – AppleBOY
Viewing all 325 articles
Browse latest View live

在 AWS Lambda 上寫 Go 語言搭配 API Gateway

$
0
0
Snip20180124_2 這應該不是什麼新消息了,就是 AWS Lambda 正式支援 Go 語言,也就是可以將 Go 語言編譯出來的二進制檔案直接放進去 Lambda Function 內,前面可以搭配 API Gateway,後面可以搭配 CloudWatchS3,本文章會教大家如何將 Gin 打包編譯進 Lambda,官網其實也有提供 Library 或範例方便大家實作,大家可以參考看看。

撰寫 Lambda function

如果想搭配 API Gateway 後端 Lambda 可能接 Restful 或 GraphQL API 的話,肯定要 Listen 單一 Http Port,底下是用 Gin 來實現一個簡單的 http 伺服器:
package main

import (
    "log"
    "net/http"
    "os"

    "github.com/gin-gonic/gin"
)

func helloHandler(c *gin.Context) {
    name := c.Param("name")
    c.String(http.StatusOK, "Hello %s", name)
}

func welcomeHandler(c *gin.Context) {
    c.String(http.StatusOK, "Hello World from Go")
}

func rootHandler(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "text": "Welcome to gin lambda server.",
    })
}

func routerEngine() *gin.Engine {
    // set server mode
    gin.SetMode(gin.DebugMode)

    r := gin.New()

    // Global middleware
    r.Use(gin.Logger())
    r.Use(gin.Recovery())

    r.GET("/welcome", welcomeHandler)
    r.GET("/user/:name", helloHandler)
    r.GET("/", rootHandler)

    return r
}

func main() {
    addr := ":" + os.Getenv("PORT")
    log.Fatal(http.ListenAndServe(addr, routerEngine()))
}

可以很清楚看到在 Gin 內,只要實現 Router 部分,就可以透過 http.ListenAndServe 方式來啟動小型 Web 服務,但是上面的程式碼不能跑在 Lambda 內,這邊就要使用 Go 大神 TJ 所開發的 apex/gateway,只要將 http.ListenAndServe 換成 gateway.ListenAndServe 就可以了
func main() {
    addr := ":" + os.Getenv("PORT")
    log.Fatal(gateway.ListenAndServe(addr, routerEngine()))
}
有沒有簡單到不行?詳細範例可以參考此 GitHub Repo

建立 Lambda function

Snip20180124_3 這邊不詳細說明了,重點是在下拉選單請選擇 Go 1.x 版本即可,不知道官方什麼時候要升級 Node.js 版本 XD

編譯 Go 檔案並上傳

AWS Lambda 只有支援 Linux 架構,所以只需要透過底下指令就可以編譯出來: $ GOOS=linux go build -o main . $ zip deployment.zip main 把輸出檔案設定為 main,最後透過 zip 方式打包成 deployment.zip,並且從 AWS Web Console 頁面上傳。 Snip20180124_5 覺得每次都要手動上傳有點麻煩,歡迎大家試試看 drone-lambda,可以透過指令方式更新 Lambda function。下一篇會教大家自動化更新 Lambda $ drone-lambda --region ap-southeast-1 \ --access-key xxxx \ --secret-key xxxx \ --function-name upload-s3 \ --zip-file deployment.zip

API Gateway + Cloud Watch

快速參考底下測試方式 Snip20180124_6 可以看到在網址輸入 /user/appleboy 可以很快速拿到 Response,接著看看 Cloud Watch Snip20180124_7

效能測試 Benchmark

預設 AWS Lambda 使用 128 MB 記憶體,那下面透過 vegeta 來看看 Go 的效能。之後有機會可以跟 Python 或 Node.js 比較看看。底下是 128 MB 記憶體。每秒打 1024 request 並且持續 10 秒
$ vegeta attack -rate=1024 -duration=10s -targets=target2.txt | tee results.bin | vegeta report
Requests      [total, rate]            10240, 1024.10
Duration      [total, attack, wait]    20.335101947s, 9.999018014s, 10.336083933s
Latencies     [mean, 50, 95, 99, max]  6.091282008s, 4.893951645s, 14.508009942s, 17.11847442s, 20.128384389s
Bytes In      [total, mean]            143360, 14.00
Bytes Out     [total, mean]            0, 0.00
Success       [ratio]                  100.00%
Status Codes  [code:count]             200:10240
換 512 MB 每秒打 1024 request 並且持續 10 秒
Requests      [total, rate]            10240, 1024.10
Duration      [total, attack, wait]    11.989730554s, 9.999012371s, 1.990718183s
Latencies     [mean, 50, 95, 99, max]  1.491340336s, 1.114643849s, 4.112241113s, 6.087949237s, 10.107294516s
Bytes In      [total, mean]            143360, 14.00
Bytes Out     [total, mean]            0, 0.00
Success       [ratio]                  100.00%
Status Codes  [code:count]             200:10240
可以看到 128MB Latencies 是 6.091282008s 而 512MB 可以降到 1.491340336s

所有資料請直接參考 gin-lambda


自動化更新 AWS Lambda 函數

$
0
0
Snip20180125_1 昨天介紹了『在 AWS Lambda 上寫 Go 語言』,無服務器的時代已經來臨,透過昨天的教學,開發者可以很快的用 Go 語言寫簡易的 Restful APIGraphQL 服務,直接無痛丟到 AWS Lambda,然而寫完編譯打包上傳整個流程,是非常枯燥乏味的,如何有效地透過自動化工具像是 JenkinsDrone 來達到自動化上傳,減少工程師花時間手動上傳,省下的時間,可以讓工程師多寫個幾行程式碼呢。

上傳 AWS Lambda

要上傳更新 Lambda 服務,步驟很簡單 編譯 => 打包 => 上傳,三個步驟就可以搞定,底下來看看如何執行指令
# 編譯
$ GOOS=linux go build -o main .
# 打包
$ zip deployment.zip main
上傳部分則是可以透過 Web Console 或 AWS CLI,這些步驟可以很輕易的整合進 Jenkins 或其他 CI/CD 服務,但是我個人又開發了 drone-lambda,讓開發者可以不用裝任何套件,就可以跨平台執行 Lambda 上傳。

drone-lambda 功能

drone-lambda 是 Go 語言開發的跨平台 CLI 工具,目前支援 Windows, MacOS, Linux 三種系統環境,開發者可以透過此頁面來下載最新版本。此 CLI 功能如下
  • 動態指定 AWS Secret 及 Access Key
  • 動態指定 AWS Profile (多環境)
  • 支援自動 zip 打包功能
  • 支援 Rexp Path (打包多個檔案)
  • 支援 zip 上傳
  • 支援 s3 bucket 上傳
底下來看看如何使用此工具

透過 zip 檔案上傳

假設已經打包好 zip 檔案,就可以透過底指令上傳
$ drone-lambda --region ap-southeast-1 \
  --access-key xxxx \
  --secret-key xxxx \
  --function-name upload-s3 \
  --zip-file deployment.zip

透過 s3 bucket 上傳

前提是 zip 檔案已經在 s3 bucket 內
$ drone-lambda --region ap-southeast-1 \
  --access-key xxxx \
  --secret-key xxxx \
  --function-name upload-s3 \
  --s3-bucket some-bucket \
  --s3-key lambda-dir/lambda.zip

自動打包上傳

開發者可以指定需要打包的檔案,如果有多個檔案,這邊支援正規語法喔
$ drone-lambda --region ap-southeast-1 \
  --access-key xxxx \
  --secret-key xxxx \
  --function-name upload-s3 \
  --source main
其中的 main 就是 Go 語言產生的執行檔,使用 --source 好處就是直接幫忙打包成 zip 檔案直接上傳,這樣在 Deploy 流程就可以少一個 zip 步驟。

設定 AWS 權限

由於此工具使用到 AWS Lambda 上傳的功能,所以必須要在 AWS Role 打開底下權限:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "iam:ListRoles",
        "lambda:UpdateFunctionCode",
        "lambda:CreateFunction"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    }
  ]
}
注意務必加上 UpdateFunctionCode

整合 Drone CI/CD

在 Drone 的 Yaml 檔案設定方式非常容易,請直接參考底下:
  lambda:
    image: appleboy/drone-lambda
    pull: true
    secrets: [ aws_access_key_id, aws_secret_access_key ]
    region: ap-southeast-1
    function_name: gin
    source:
      - main
其中 aws_access_key_idaws_secret_access_key 可以透過 Web 或 CLI 方式設定。

後記

如果不用 Drone 而是 jenkins 或 GitLab CI,可以直接下載此工具,就可以使用了,也不需要寫任何 Plugin。

開源專案 drone-lambda

將 Go Html Template 存入 String 變數

$
0
0
Go-brown-side.shGo 語言內通常都將 Html Temaple 寫入到 io.Writer interface 像是 *http.ResponseWriter,但是有些情境需要將 Template 寫入到 String 變數內,例如實作簡訊 Template,這時候需要將 Html Temaple 轉成 String。該如何實作,非常簡單,只需要在任意變數內實作 io.Writer interface 即可,而 String 該如何轉換呢?可以使用 buffer’s pointer
func GetString(filename string, data interface{}) (string, error) {
    t := template.New(filename).Funcs(NewFuncMap())

    content, err := ReadFile(filename)

    if err != nil {
        logrus.Warnf("Failed to read builtin %s template. %s", filename, err)
        return "", err
    }

    t.Parse(
        string(content),
    )

    var tpl bytes.Buffer
    if err := t.Execute(&tpl, data); err != nil {
        return "", err
    }

    return tpl.String(), nil
}
其中 ReadFile 是讀取檔案函式,NewFuncMap 則是 Function Map

用 Go 語言解決 MongoDB Transaction 機制

$
0
0
Screen Shot 2018-03-10 at 3.22.59 PM MongoDB 是一套具有高效能讀寫的 NoSQL 資料庫,但是不像傳統關連式資料庫,有非常好用的 Transaction 交易模式,而在 MongoDB 也可以透過 Two Phase Commits 來達成交易功能,大家可以先打開文件看看,非常冗長,工程師需要花很多時間閱讀文件並且實現出來。而在 Go 語言內,我們可以在 Single Thread 內同一時間點讀寫存取同一筆資料庫來解決此問題。此篇會透過 Go 語言方式來實現交易機制。

問題描述

底下步驟來產生資料
  1. 建立使用者,並且初始化每人 $1000 USD
  2. 接到新的交易請求
  3. 讀取使用者帳戶剩餘存款
  4. 將該帳號增加 $50 USD
根據上述的需求,我們可以知道,當有 100 個連線交易時,理論上該使用者的存款會變成 $1000 + $50*100 = $6000 USD。這是理想狀態,假設如果同時間打上來,大家可以知道最後存款肯定不到 $6000。底下程式碼可以複製出此問題
func main() {
    session, _ := mgo.Dial("localhost:27017")
    globalDB = session.DB("queue")
    globalDB.C("bank").DropCollection()

    user := currency{Account: account, Amount: 1000.00, Code: "USD"}
    err := globalDB.C("bank").Insert(&user)

    if err != nil {
        panic("insert error")
    }

    log.Println("Listen server on 8000 port")
    http.HandleFunc("/", pay)
    http.ListenAndServe(":8000", nil)
}
上述是主程式,新增一個 Handle 為 pay,用來處理交易。
func pay(w http.ResponseWriter, r *http.Request) {
    entry := currency{}
    // step 1: get current amount
    err := globalDB.C("bank").Find(bson.M{"account": account}).One(&entry)

    if err != nil {
        panic(err)
    }

    wait := Random(1, 100)
    time.Sleep(time.Duration(wait) * time.Millisecond)

    //step 3: subtract current balance and update back to database
    entry.Amount = entry.Amount + 50.000
    err = globalDB.C("bank").UpdateId(entry.ID, &entry)

    if err != nil {
        panic("update error")
    }

    fmt.Printf("%+v\n", entry)

    io.WriteString(w, "ok")
}

解決方式

這邊提供幾個解決方式,第一種就是透過 sync.Mutex 方式,直接將交易區段程式碼 lock 住,這樣可以避免同時寫入或讀出的問題。在 Handler 內直接新增底下程式碼就可以解決,詳細程式碼請參考 safe.go
    mu.Lock()
    defer mu.Unlock()
第二種方式可以用 Go 語言內的優勢: goroutine + channel,在這邊我們只要建立兩個 Channle,第一個是使用者帳號 (string) 第二個是輸出 Result (struct)。完整程式碼範例
    in = make(chan string)
    out = make(chan Result)
在 main func 內建立第一個 goroutine
    go func(in *chan string) {
        for {
            select {
            case account := <-*in:
                entry := currency{}
                // step 1: get current amount
                err := globalDB.C("bank").Find(bson.M{"account": account}).One(&entry)

                if err != nil {
                    panic(err)
                }

                //step 3: subtract current balance and update back to database
                entry.Amount = entry.Amount + 50.000
                err = globalDB.C("bank").UpdateId(entry.ID, &entry)

                if err != nil {
                    panic("update error")
                }

                out <- Result{
                    Account: account,
                    Result:  entry.Amount,
                }
            }
        }

    }(&in)
上面可以很清楚看到使用到 select 來接受 input channel,並且透過 go 將 for loop 丟到背景執行。所以在每個交易時,將帳號丟到 in channel 內,就可以開始進行交易,同時間並不會有其他交易。在 handler 內,也是透過此方式來讀取使用者最後存款餘額
    wg := sync.WaitGroup{}
    wg.Add(1)

    go func(wg *sync.WaitGroup) {
        in <- account
        for {
            select {
            case result := <-out:
                fmt.Printf("%+v\n", result)
                wg.Done()
                return
            }
        }
    }(&wg)

    wg.Wait()
不過上面這方法,可想而知,只有一個 Queue 幫忙處理交易資料,那假設有幾百萬個交易要同時進行呢,該如何消化更多的交易,就要將上面程式碼改成 Multiple Queue 完整程式碼範例。假設我們有 100 個帳號,開 10 個 Queue 去處理,每一個 Queue 來處理 10 個帳號,也就是說 ID 為 23 號的分給第 3 (23 % 10) 個 Queue,ID 為 59 號則分給第 9 個 Queue。
    for i := range in {
        go func(in *chan string, i int) {
            for {
                select {
                case account := <-*in:
                    out <- Result{
                        Account: account,
                        Result:  entry.Amount,
                    }
                }
            }

        }(&in, i)
    }
其中 channel 要宣告為底下: maxThread 為 10 (可以由開發者任意設定)
    in = make([]chan string, maxThread)
    out = make([]chan Result, maxThread)

效能測試

上述提供了三個解決方式,但是該選擇哪一種會比較好呢,底下是透過 [vegeta] http 效能檢測工具來實驗看看,底下先整理三種方法
  1. 使用 sync.Mutex
  2. 使用 single queue
  3. 使用 multiple queue
直接給數據看看
max Latencies mean Latencies
sync lock 25.558340237s 12.72966531s
single queue 672.252801ms 160.43181ms
multiple queue 476.134408ms 132.990084ms
可以看出來透過 multiple queue 測試效能是最好的,這數據是每秒打 1000 req,持續打 60 秒。

結論

這邊提供了三種解決方案,也許不是最好,如果能有更好的方式來解決會是更好,詳細的程式碼都有放在 go-transaction-example,歡迎大家拿去測試看看。

用 Nginx 來架設線上即時縮圖機

$
0
0
Screen Shot 2018-03-15 at 10.21.38 AM 在更早以前我們怎麼實現縮圖機制,當使用者上傳一張檔案,後端會固定將圖片縮圖成各種前端網頁需要的大小,不管前端頁面是否有使用,後端都會先產生好,這有什麼缺陷?
  1. 佔用硬碟空間大小
  2. 前端又需要另外一種格式的縮圖?
第二個問題比較麻煩,當前端需要另一種縮圖格式,後端就要開始掃描系統的全部圖片,再重新產生一次。非常耗費後端系統效能。後來才改成透過 URL 定義長寬來決定即時縮圖,在 Go 語言內可以選擇使用 picfit 來當作後端即時的縮圖機。本篇則是要提供另一種解法,就是使用 Nginx 搭配 image_filter 外掛來達成即時縮圖機制。

使用 image_filter

來看看縮圖網址
http://foobar.org/image_width/bucket_name/image_name
  • image_width: 圖片 width
  • bucket_name: 圖片目錄或 AWS S3 bucket
  • image_width: 圖片檔名
其中 bucket 可以是 AWS S3。底下是 Nginx 的簡單設定:
server {
  listen 80 default_server;
  listen [::]:80 default_server;
  server_name ${NGINX_HOST};

  location ~ ^/([0-9]+)/(.*)$ {
    set $width $1;
    set $path $2;
    rewrite ^ /$path break;
    proxy_pass ${IMAGE_HOST};
    image_filter resize $width -;
    image_filter_buffer 100M;
    image_filter_jpeg_quality ${JPG_QUALITY};
    expires ${EXPIRE_TIME};
  }
}
我們可以設定 expires 來讓使用這存在瀏覽器端,這樣下次瀏覽網頁的時候都可以使用快取機制。可以看到 IMAGE_HOST 可以是 AWS S3 URL。
  1. 先從 IMAGE_HOST 下載圖片
  2. Nginx 執行縮圖
  3. 儲存圖片在使用者 browser 端
Snip20180317_2 到這邊會有個問題,假設有一萬個使用者在不同的地方同時連線,Nginx 就需要處理 1 萬次,可以直接用 vegeta 來 Benchmark 試試看
$ echo "GET http://localhost:8002/310/test/26946324088_5b3f0b1464_o.png" | vegeta attack -rate=100 -connections=1 -duration=1s | tee results.bin | vegeta report
Requests      [total, rate]            100, 101.01
Duration      [total, attack, wait]    8.258454731s, 989.999ms, 7.268455731s
Latencies     [mean, 50, 95, 99, max]  3.937031678s, 4.079690985s, 6.958110121s, 7.205018428s, 7.268455731s
Bytes In      [total, mean]            4455500, 44555.00
Bytes Out     [total, mean]            0, 0.00
Success       [ratio]                  100.00%
Status Codes  [code:count]             200:100
Error Set:
上面數據顯示每秒打 100 次連線,ngix 需要花費 8 秒多才能執行結束。而延遲時間也高達 3 秒多。

加入 proxy cache 機制

透過 proxy cache 機制可以讓 nginx 只產生一次縮圖,並且放到 cache 目錄內可以減少短時間的不同連線。但是 image_filter 無法跟 proxy cache 同時處理,所以必須要拆成兩個 host 才可以達到此目的,如果沒有透過 proxy cache,你也可以用 cloudflare CDN 來達成此目的。請參考線上設定
proxy_cache_path /data keys_zone=cache_zone:10m;

server {
  # Internal image resizing server.
  server_name localhost;
  listen 8888;

  # Clean up the headers going to and from S3.
  proxy_hide_header "x-amz-id-2";
  proxy_hide_header "x-amz-request-id";
  proxy_hide_header "x-amz-storage-class";
  proxy_hide_header "Set-Cookie";
  proxy_ignore_headers "Set-Cookie";

  location ~ ^/([0-9]+)/(.*)$ {
    set $width $1;
    set $path $2;
    rewrite ^ /$path break;
    proxy_pass ${IMAGE_HOST};
    image_filter resize $width -;
    image_filter_buffer 100M;
    image_filter_jpeg_quality ${JPG_QUALITY};
  }
}


server {
  listen 80 default_server;
  listen [::]:80 default_server;
  server_name ${NGINX_HOST};

  location ~ ^/([0-9]+)/(.*)$ {
    set $width $1;
    set $path $2;
    rewrite ^ /$path break;
    proxy_pass http://127.0.0.1:8888/$width/$path;
    proxy_cache cache_zone;
    proxy_cache_key $uri;
    proxy_cache_valid 200 302 24h;
    proxy_cache_valid 404 1m;
    # expire time for browser
    expires ${EXPIRE_TIME};
  }
}

測試數據

這邊使用 minio 來當作 S3 儲存空間,再搭配 Nginx 1.3.9 版本來測試上面設定效能。底下是 docker-compose 一鍵啟動
version: '2'

services:
  minio:
    image: minio/minio
    container_name: minio
    ports:
      - "9000:9000"
    volumes:
      - minio-data:/data
    environment:
      MINIO_ACCESS_KEY: YOUR_MINIO_ACCESS_KEY
      MINIO_SECRET_KEY: YOUR_MINIO_SECRET_KEY
    command: server /data

  image-resizer:
    image: appleboy/nginx-image-resizer
    container_name: image-resizer
    ports:
      - "8002:80"
    environment:
      IMAGE_HOST: http://minio:9000
      NGINX_HOST: localhost

volumes:
  minio-data:
用 docker-compose up 可以將 nginx 及 minio 服務同時啟動,接著打開 http://localhost:9000 上傳圖片,再透過 vegeta 測試數據:
$ echo "GET http://localhost:8002/310/test/26946324088_5b3f0b1464_o.png" | vegeta attack -rate=100 -connections=1 -duration=1s | tee results.bin | vegeta report
Requests      [total, rate]            100, 101.01
Duration      [total, attack, wait]    993.312255ms, 989.998ms, 3.314255ms
Latencies     [mean, 50, 95, 99, max]  3.717219ms, 3.05486ms, 8.891027ms, 12.488937ms, 12.520428ms
Bytes In      [total, mean]            4455500, 44555.00
Bytes Out     [total, mean]            0, 0.00
Success       [ratio]                  100.00%
Status Codes  [code:count]             200:100
Error Set:
執行時間變成 993.312255ms,Latency 也降到 3.717219ms,效能提升了很多。透過簡單的 docker 指令就可以在任意機器架設此縮圖機。詳細步驟請參考 nginx-image-resizer
$ docker run -e NGINX_PORT=8081 \
  -e NGINX_HOST=localhost \
  -e IMAGE_HOST="http://localhost:9000" \
  appleboy/nginx-image-resizer

程式碼請參考 nginx-image-resizer

Go 語言搭配 Docker Healthy Check 檢查

$
0
0
Screen Shot 2018-03-17 at 11.40.12 PMDocker 1.12 版本後,提供了 HEALTHCHECK 指令,通過指定的一行命令來判斷容器內的服務是否正常運作。在此之前大部分都是透過判斷程式是否 Crash 來決定容器是否存活,但是這地方有點風險的是,假設服務並非 crash,而是沒辦法退出容器,造成無法接受新的請求,這就確保容器存活。現在呢我們可以透過在 Dockerfile 內指定 HEALTHCHECK 指令來確保服務是否正常。而用 Go 語言開發的 Web 服務該如何來實現呢?

建立 /healthz 路由

透過簡單的路由 /healthz 直接回傳 200 status code 即可 (使用 Gin 當例子)。
func heartbeatHandler(c *gin.Context) {
    c.AbortWithStatus(http.StatusOK)
}
透過瀏覽器 http://localhost:8080/healthz 可以得到空白網頁,但是打開 console 可以看到正確回傳值。 Snip20180317_4

建立 ping 指令

透過 net/http 套件可以快速寫個驗證接口的函式
func pinger() error {
    resp, err := http.Get("http://localhost:8080/healthz")
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    if resp.StatusCode != 200 {
        return fmt.Errorf("server returned non-200 status code")
    }
    return nil
}

增加 HEALTHCHECK 指令

Dockerfile 內增加底下內容:
HEALTHCHECK --start-period=2s --interval=10s --timeout=5s \
  CMD ["/bin/gorush", "--ping"]
  • –start-period: 容器啟動後需要等待幾秒,預設為 0 秒
  • –interval: 偵測間隔時間,預設為 30 秒
  • –timeout: 檢查超時時間
重新編譯容器,並且啟動容器,會看到初始狀態為 (health: starting) Snip20180317_5 經過 10 秒後,就會執行指定的指令,就可以知道容器健康與否,最後狀態為 (healtyy)Snip20180317_6 最後可以透過 docker inspect 指令來知道容器的狀態列表 (JSON 格式)
$ docker inspect --format '{{json .State.Health}}' gorush | jq
Snip20180318_8 從上圖可以知道每隔 10 秒 Docker 就會自動偵測一次。有了上述這些資料,就可以來寫系統報警通知了。

Go 語言基礎實戰教學影片上線了

$
0
0
Screen Shot 2018-03-17 at 11.40.12 PM

購買連結

去年在台灣推廣 Drone 這套由 Go 語言所撰寫的開源專案,身為維護者之一,也將 Drone 跟 Gitea 完成整合。如果對於從開發到部署整個流程,還不是很了解的朋友,我個人就是推薦使用 Drone,去年九月也推出 Drone 的教學影片系列:『一天學會自動化測試及部署』,在公司使用 Drone 到現在,不曾看到服務中斷,啟動速度也是毫秒等級。為什麼會這麼穩定及快速呢?當然要歸功於 Go 語言。而這次我個人針對 Go 語言,錄製了一份基礎教學『GO 語言基礎實戰』。

課程大綱

Go 語言 (又稱 Golang) 是 Google 推出新一代的強大語言,2018 年 1 月 Google 公佈了去年底統計的問卷結果,發現 61% 用 Go 來寫網站,37% 用來開發系統程式,36% 用來做 DevOps (多重選擇),可想而知 Go 語言可以拿來從底層寫到上層。高效能的 Goroutine (併發) 表現優於 Parallelism (並行),語法上簡潔又簡單,這也就是為什麼我們要來嘗試 Go 語言。

課程內容

本次的 Go 語言教學會著重在實作上的開發,也就是不只教您基礎的用法,而是會透過一些實際的案例讓大家了解 Go 語言開發技巧。內容大綱如下
  1. Go 背景介紹
  2. Go 環境建置
  3. Go 基本語法
  4. Go Goroutines 介紹
  5. Go Channel 介紹
  6. Go 簡易 HTTP 伺服器
  7. Go 簡易 Command Line 實作
  8. Go 跨平台編譯 (Windows, MacOS, Linux)
  9. Go 搭配 Docker 介紹
  10. Go 語言開發實戰案例

投影片

大家可以先參考這份投影片,讓您可以清楚了解 Go 語言的基礎,以及 Go 語言的由來。

購買

目前特價 $1600,課程還正在錄製中,每週都會有新的影片上架,歡迎大家參考看看,請點選底下購買連結:

Go 語言基礎實戰教學

買了結果沒興趣想退費怎麼辦?沒關係,在 Udemy 平台 30 天內都可以全額退費,所以不用擔心買了後悔。如果你對 Go 語言 (現在 $1600) 及 Drone 自動化部署 (現在 $1800) 都有興趣,想要一起購買,你可以直接匯款到底下帳戶,有合購優惠價
  • 富邦銀行: 012
  • 富邦帳號: 746168268370
  • 匯款金額: 台幣 $3000 元
匯款後請直接到 FB 找我,或者直接寫信給我也可以 appleboy.tw AT gmail.com。有任何問題都可以直接加我 FB,都是公開資訊。

如何使用 Go 語言 Flag 套件 (影片教學)

$
0
0
Go-brown-side.sh 之前寫過一篇『用 Golang 寫 Command line 工具』教學,作者我錄了一個教學影片,教大家如何使用 Go 語言Flag 套件,套件用法很簡單,相信看了底下的影片教學馬上就會了,但是在這邊強調,用 flag 的時機會是在寫 command line tool 給同事或者是自己用,如果是寫大型 Web Application,不推薦使用 flag,原因是 flag 不支援讀取系統環境變數,如果是 web 服務,想要動態改變 port 或者是 DB 連線資訊,就變得比較複雜,也無法搭配 Docker 使用,更不用說想結合 Kubernetes。如果要寫大專案,請使用 urfave/clispf13/cobra

線上課程

有興趣的話,可以直接購買課程,現在特價 $1600,未來會漲價,另外課程尚未錄製完成,會陸陸續續分享 Go 的開發技巧。

[影片教學] 使用 Filter 將專案跑在特定 Drone Agent 服務

$
0
0
drone-logo_512 Drone 是一套用 Go 語言撰寫的 CI/CD 開源專案,是由一個 Server 跟多個 Agent 所組成,Agent 上面必須安裝好 Dokcer 才可以順利測試及部署,但是團隊內會出現一個狀況,每個專案的測試及部署方式不同,有的測試需要 Agent 很多 CPU 或記憶體資源,有的小專案則不需要那麼多,但是當大專案把 agent 系統資源吃光,其他專案都跑不動了,這邊的解決方式就是再建立一台新的 Agent 服務,將需要大量資源的專案跑在該台新的 Agent,Drone 這邊有支援 filter 功能,讓開發者可以指定專案要跑在哪一台 Agent 上。底下來教大家如何設定 drone filter。

新增 DRONE_FILTER 設定

打開 docker-compose.yml,找到 Drone agent 的設定,加入底下變數:
DRONE_FILTER="ram <= 16 AND cpu <= 8"
這邊的意思是,專案需要的記憶體小於或等於 16,CPU 小於或等於 8

新增 Labels 在 .drone.yml

這邊要介紹 Labels,可以用來指定該專案要標記上哪些 Label,讓 drone server 可以根據這些 Label 來將 Job 丟到指定的 Agent 服務內。請打開 .drone.yml,加入底下設定
labels:
  - ram=14
  - cpu=8
可以看到上面設定 ram = 14 及 cpu = 8 可以看到符合上面 drone agent 的 filter 條件設定,所以 server 會將此 project 的工作都指定到特定的 agent 服務上,這樣就可以避免大專案跟小專案同時跑在同一台機器上。

教學影片

如果上述步驟不知道該如何操作,可以參考底下教學影片

結論

為了能讓團隊繼續成長,就必須要一直擴展 Agent。原先在公司內部建立一台 server 加上多台 Agent,而各團隊維護各自的 Agent 服務,團隊間不共享 Angent 資源,這樣避免各專案互相卡住。透過 drone filter 可以讓團隊管理各自的專案在自己的 agent 服務上。如果您對 Drone 有興趣,也可以參考 Udemy 上面的『一天學會 DevOps 自動化測試及部署』線上課程。

Go 語言的 init 函式

$
0
0
Go-brown-side.sh 本篇會帶大家認識 Go 語言的 init 函式,在了解 init func 之前,大家應該都知道在同一個 Package 底下是不可以有重複的變數或者是函式名稱,但是唯獨 init() 可以在同一個 package 內宣告多次都沒問題。底下看例子,可以發現的是不管宣告多少次,都會依序從最初宣告到最後宣告依序執行下來。
package main

import (
    "fmt"
)

func init() {
    fmt.Println("init 1")
}

func init() {
    fmt.Println("init 2")
}

func main() {
    fmt.Println("Hello, playground")
}

從其他 package 讀取 init func

有種狀況底下,主程式需要單獨讀取 package 內的 init func 而不讀取額外的變數,這時候就要透過 _ 來讀取 package。假設要讀取 lib/pq 內的 init,一定要使用 _
import(
    // Needed for the Postgresql driver
    _ "github.com/lib/pq
)
如果沒有加上 _,當編譯的時候就會報錯,原因就是 main 主程式內沒有用到 pq 內任何非 init() 的功能,所以不可編譯成功。如果有多個 package 的 init 需要同時引入,這邊也是會依照 import 的順序來讀取。

init 執行時機

大家一定很好奇 init 的執行時間是什麼時候,底下舉個例子
var global = convert()

func convert() int {
    return 100
}

func init() {
    global = 0
}

func main() {
    fmt.Println("global is", global)
}
或者是把 init() 放到最上面
func init() {
    global = 0
}

var global = convert()

func convert() int {
    return 100
}

func main() {
    fmt.Println("global is", global)
}
兩種結果都是 0,這邊大家就可以知,init 執行的時機會是在執行 main func 之前,所以不管前面做了哪些事情,都不會影響 init 的執行結果。最後提醒大家,只要 package 內有 init 的 func,在引入 package 時都會被執行。

線上影片


Go 語言線上課程目前特價 $1600,持續錄製中,每週都會有新的影片上架,歡迎大家參考看看,請點選底下購買連結:

點我購買

Go 語言的 vendor 目錄

$
0
0
Go-Logo_Blue 很多朋友剛入門 Go 語言時,第一個會遇到的問題是,該如何設定專案配置,讓專案可以正常執行,在個人電腦該如何開發多個專案,這邊就會遇到該如何設定 $GOPATH,我在這邊跟大家講個觀念,開發環境只會有一個 $GOPATH,不管團隊內有多少專案,都是存放在同一個 GOPATH,避免每次開專案都要重新設定 $GOPATH,而專案內用到的相依性套件,請各自維護,透過官方提供的 wiki,請選一套覺得好用的來使用吧,沒有最好的工具,找一套適合團隊是最重要的。

什麼是 vendor

這邊不多說了,直接看影片教學最快了。
Go 語言線上課程目前特價 $1600,持續錄製中,每週都會有新的影片上架,歡迎大家參考看看,請點選底下購買連結:

點我購買

如何在 Go 專案內寫測試

$
0
0
Go-brown-side.sh 相信大家都知道專案內不導入測試,未來越來越多功能,技術債就會越來越多,接手的人罵聲連連,而寫測試的簡單與否決定專案初期是否要先導入。為什麼專案要導入測試,導入測試有什麼好處,對於團隊而言,導入測試好處實在太多了,底下列了幾點是我個人覺得非常重要的。
  1. 減少 Review 時間
  2. 降低修改程式碼產生的的錯誤
  3. 確保程式碼品質
第一點非常實用,尤其在專案很忙的時候,同事間只有少許的時間可以幫忙看程式碼或討論,如果大家都有寫測試,在時間的壓力下,只要稍微看一下,CI/CD 驗證過無誤,大致上就可以上線了。第二點在於,團隊其他成員需要修改一個不確定的地方,商業邏輯修正可能會造成很大的錯誤,而測試在這時候就發揮效果。最後一點就是程式碼品質,不管是新功能,或者是 Bug,任何時間點都需要補上測試,就算 code coverage 已經很高了,但是只要有任何 bug 就要補測試,測試寫的越多,專案的品質相對會提高。在 Go 語言專案內該如何寫測試了,為什麼專案要導入 Go 語言的原因之一就是『寫測試太簡單』了,底下來介紹如何寫基本的測試。

內建 testing 套件

在 Go 語言內,不需要而外安裝任何第三方套件就可以開使寫測試,首先該將測試放在哪個目錄內呢?不需要建立特定目錄來存放測試程式碼,而是針對每個 Go 的原始檔案,建立一個全新測試檔案,並且檔名最後加上 _test 就可以了,假設程式碼為 car.go 那麼測試程式就是 car_test.go,底下舉個範例
package car

import "errors"

// Car struct
type Car struct {
    Name  string
    Price float32
}

// SetName set car name
func (c *Car) SetName(name string) string {
    if name != "" {
        c.Name = name
    }

    return c.Name
}

// New Object
func New(name string, price float32) (*Car, error) {
    if name == "" {
        return nil, errors.New("missing name")
    }

    return &Car{
        Name:  name,
        Price: price,
    }, nil
}
驗證上面的程式碼可以建立 car_test.go,並且寫下第一個測試程式
// Simple testing what different between Fatal and Error
func TestNew(t *testing.T) {
    c, err := New("", 100)
    if err != nil {
        t.Fatal("got errors:", err)
    }

    if c == nil {
        t.Error("car should be nil")
    }
}
首先 func 名稱一定要以 Test 作為開頭,而 Go 內建 testing 套件,可以使用簡易的 t.Fatal 或 t.Error 來驗證錯誤,這兩個的差異在於 t.Fatal 會中斷測試,而 t.Error 不會,簡單來說,假設您需要整個完整測試後才顯示錯誤,那就需要用 t.Error,反之就使用 t.Fatal 來中斷測試。

使用 testify 套件

這邊只會介紹一個第三方套件那就是 testify,裡面內建很多好用的測試等大家發掘,底下用簡單的 assert 套件來修改上方的測試程式:
func TestNewWithAssert(t *testing.T) {
    c, err := New("", 100)
    assert.NotNil(t, err)
    assert.Error(t, err)
    assert.Nil(t, c)

    c, err = New("foo", 100)
    assert.Nil(t, err)
    assert.NoError(t, err)
    assert.NotNil(t, c)
    assert.Equal(t, "foo", c.Name)
}
有沒有看起來比較簡潔。這邊測試用的 command,也可以針對單一函式做測試。
$ go test -v -run=TestNewWithAssert ./example18-write-testing-and-doc/...
可以看到 -run 讓開發者可以針對單一函式做測試,對於大型專案來說非常方便,假設修正完 bug,並且寫了測試,就可以針對單一函式做測試,這點 Go 做得相當棒。

平行測試

講平行測試之前,跟大家講個用 vscode 編輯器寫測試的一個小技巧,就是透過 vscode 可以幫忙產生測試程式碼,該如何使用呢?可以先將要測試的函式全選,然後按下 command + shift + p,就會出現底下命令列選擇。 Snip20180514_2 這邊為什麼要平行測試呢?原因是單一函式測試,假設一個情境需要執行時間為 0.5 秒,那麼假設寫了 10 種狀況,就需要 10 * 0.5 秒,這樣花費太久了。這時候就需要請 Go 幫忙做平行測試。先看看底下範例:
func TestCar_SetName(t *testing.T) {
    type fields struct {
        Name  string
        Price float32
    }
    type args struct {
        name string
    }
    tests := []struct {
        name   string
        fields fields
        args   args
        want   string
    }{
        {
            name: "no input name",
            fields: fields{
                Name:  "foo",
                Price: 100,
            },
            args: args{
                name: "",
            },
            want: "foo",
        },
        {
            name: "input name",
            fields: fields{
                Name:  "foo",
                Price: 100,
            },
            args: args{
                name: "bar",
            },
            want: "bar",
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            c := &Car{
                Name:  tt.fields.Name,
                Price: tt.fields.Price,
            }
            if got := c.SetName(tt.args.name); got != tt.want {
                t.Errorf("Car.SetName() = %v, want %v", got, tt.want)
            }
        })
    }
}
上面範例跑了兩個測試,一個是沒有 input value,一個則是有 input,根據 for 迴圈會依序執行測試,其中裡面的 t.Run 是指 sub test,如下圖 Snip20180514_3 上述的程式碼都是 vscode 幫忙產生的,開發者只需要把測試資料補上就可以了。假設有 10 個情境需要測試,那該如何讓 Go 幫忙平行測試呢?請使用 t.Parallel()
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            c := &Car{
                Name:  tt.fields.Name,
                Price: tt.fields.Price,
            }
            if got := c.SetName(tt.args.name); got != tt.want {
                t.Errorf("Car.SetName() = %v, want %v", got, tt.want)
            }
        })
    }
t.Run 的 callback 測試內補上 t.Parallel() 就可以了喔。寫到這邊,大家應該可以看出一個問題,就是平行測試的內容怎麼都會是測試同一個情境,也就是本來要測試 10 種情境,但是會發現 Go 把最後一個情境同時跑了 10 次?這邊的問題點出在哪邊,請大家注意 tt 變數,由於跑平行測試,那麼 for 迴圈最後一次就會蓋掉之前的所有 tt 變數,要修正此狀況也非常容易,在迴圈內重新宣告一次即可 tt := tt
    for _, tt := range tests {
        tt := tt
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            c := &Car{
                Name:  tt.fields.Name,
                Price: tt.fields.Price,
            }
            if got := c.SetName(tt.args.name); got != tt.want {
                t.Errorf("Car.SetName() = %v, want %v", got, tt.want)
            }
        })
    }

感想

本篇尚未寫到『整合性測試』也就是該如何搭配 Database 進行資料庫測試,會在開新的一篇做介紹。本文內容也有錄製影片放在 Udemy 上面,如果覺得寫的不錯,也可以參考我的教學影片。

直接購買線上影片

Drone 搭配 Kubernetes 部署 Go 語言項目

$
0
0
Screen Shot 2018-06-04 at 9.19.46 AM 在之前寫過一篇『Drone 搭配 Kubernetes 升級應用程式版本』,裡面內容最主要介紹 honestbee 撰寫的 drone 外掛: drone-kubernetes,但是此外掛並非用 Go 語言所撰寫,而是用 Shell Script 透過 kubectl set image 方式來更新專案項目,但是這邊會有幾個缺點,第一點就是假設在 Develop 環境永遠都是吃 master 分支,也就是讀取 Image 的 latest 標籤,這時候此外掛就無法作用,第二點此外掛無法讀取 kubernetes YAML 檔案,假設專案要修正一個 ENV 值,此外掛也無法及時更新。綜合這兩點因素,只好捨棄此外掛,而本篇會帶給大家另一個用 Go 語言所撰寫的外掛,是由 @Sh4d1 所開發的項目,用法相當容易,底下會一步一步教大家如何部署 Go 語言項目。

GitHub 工作流程及部署

Screen Shot 2018-06-04 at 9.44.15 AM 團隊只會有兩種環境,一種是 Staging 另一種則是 Production,而前者是根據只要 master 分支有變動,則會更新 Staging,而後者則需要下 Tag 才會正式部署到 Production,在 Drone 預設值內,是不開啟 Tag 事件,所以需自行到後台打開 (如下圖),未來可以透過 drone-cli 用 command 方式打開,目前此功能正在 Review 中。 Snip20180604_6 底下會來一步一步教大家如何設定 Drone。

準備 Go 項目

本篇會用 Go 語言寫個小型 Http 服務,來證明使用 tag 事件及 master 分支都可以正確部署,底下先看看 Go 的程式碼:
package main

import (
    "log"
    "net/http"
    "os"
    "strings"
)

var version = "master"

func showVersion(w http.ResponseWriter, r *http.Request) {
    log.Println(version)
    w.Write([]byte(version))
}

func sayHello(w http.ResponseWriter, r *http.Request) {
    message := r.URL.Path
    message = strings.TrimPrefix(message, "/")
    message = "Hello, drone got the message: " + message
    log.Println(message)
    w.Write([]byte(message))
}

func main() {
    // use PORT environment variable, or default to 8080
    port := "8080"
    if fromEnv := os.Getenv("PORT"); fromEnv != "" {
        port = fromEnv
    }
    http.HandleFunc("/version", showVersion)
    http.HandleFunc("/", sayHello)
    log.Println("Listen server on " + port + " port")
    if err := http.ListenAndServe(":"+port, nil); err != nil {
        log.Fatal(err)
    }
}
從上面程式可以看到,在編譯 Go 語言專案時,可以從外部帶入 version 變數,證明目前的 App 版本。請參考 Makefile 內的
build:
ifneq ($(DRONE_TAG),)
    go build -v -ldflags "-X main.version=$(DRONE_TAG)" -a -o release/linux/amd64/hello
else
    go build -v -ldflags "-X main.version=$(DRONE_COMMIT)" -a -o release/linux/amd64/hello
endif
只要是 master 分支的 commit,就會執行 -X main.version=$(DRONE_COMMIT),如果是 push tag 到伺服器,則會執行 -X main.version=$(DRONE_TAG)。最後看看 Drone 如何編譯
pipeline:
  build_linux_amd64:
    image: golang:1.10
    group: build
    environment:
      - GOOS=linux
      - GOARCH=amd64
      - CGO_ENABLED=0
    commands:
      - cd example19-deploy-with-kubernetes && make build
記得將 GOOS, GOARCHCGO_ENABLED 設定好。

上傳容器到 DockerHub

上一個步驟可以編譯出 linux 的二進制檔案,這時候就可以直接放到容器內直接執行:
FROM plugins/base:multiarch

ADD example19-deploy-with-kubernetes/release/linux/amd64/hello /bin/

ENTRYPOINT ["/bin/hello"]
其中 plugins/base:multiarch 用的是 docker scratch 最小 image 搭配 SSL 憑證檔案,接著把 go 編譯出來的二進制檔案放入,所以整體容器大小已經是最小的了。看看 drone 怎麼上傳到 DockerHub
  docker_golang:
    image: plugins/docker:17.12
    secrets: [ docker_username, docker_password ]
    repo: appleboy/golang-http
    dockerfile: example19-deploy-with-kubernetes/Dockerfile
    default_tags: true
    when:
      event: [ push, tag ]
其中 default_tags 會自動將 master 分支上傳到 latest 標籤,而假設上傳 1.1.1 版本時,drone 則會幫忙編譯出三個不同的 tag 標籤,分別是 1, 1.1, 1.1.1 這是完全符合 Semantic Versioning,如果有在開源專案打滾的朋友們,一定知道版本的重要性。而 Drone 在這地方提供了很簡單的設定讓開發者可以上傳一次 tag 做到三種不同的 image 標籤。

部署更新 Kubernetes

這邊推薦大家使用 Sh4d1/drone-kubernetes 外掛,使用之前請先設定好三個參數:
  1. KUBERNETES_SERVER
  2. KUBERNETES_CERT
  3. KUBERNETES_TOKEN
KUBERNETES_SERVER 可以打開家目錄底下的 ~/.kube/config 檔案直接找到,cert 及 token 請先透過 pod 找到 secret token name:
$ kubectl describe po/frontend-9f5ccc8d4-8n9xq | grep SecretName | grep token
    SecretName:  default-token-r5xdx
拿到 secret name 之後,再透過底下指令找到 ca.crttoken
$ kubectl get secret default-token-r5xdx -o yaml | egrep 'ca.crt:|token:'
其中 token 還需要透過 base64 decode 過,才可以設定到 drone secret。完成上述步驟後,可以來設定 drone 部署:
  deploy:
    image: sh4d1/drone-kubernetes
    kubernetes_template: example19-deploy-with-kubernetes/deployment.yml
    kubernetes_namespace: default
    secrets: [ kubernetes_server, kubernetes_cert, kubernetes_token ]
其中 deployment.yml 就是該服務的 deploy 檔案:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: frontend
  # these labels can be applied automatically
  # from the labels in the pod template if not set
  labels:
    app: gotraining
    tier: frontend
spec:
  # this replicas value is default
  # modify it according to your case
  replicas: 3
  # selector can be applied automatically
  # from the labels in the pod template if not set
  # selector:
  #   app: guestbook
  #   tier: frontend
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  minReadySeconds: 5
  template:
    metadata:
      labels:
        app: gotraining
        tier: frontend
    spec:
      containers:
      - name: go-hello
        image: appleboy/golang-http:VERSION
        imagePullPolicy: Always
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
        ports:
        - containerPort: 8080
        env:
        - name: FOR_GODS_SAKE_PLEASE_REDEPLOY
          value: 'THIS_STRING_IS_REPLACED_DURING_BUILD'
大家可以找到 image: appleboy/golang-http:VERSION,這邊需要寫個 sed 指令來取代 VERSION,部署到 staging 則是 latest,如果是 tag 則取代為 DRONE_TAG
ifneq ($(DRONE_TAG),)
    VERSION ?= $(DRONE_TAG)
else
    VERSION ?= latest
endif

prepare:
    sed -ie "s/VERSION/$(VERSION)/g" deployment.yml
這邊有個問題就是,我們怎麼讓在同一個 image:latest 下,也可以保持更新 App 呢,首先必須設定 imagePullPolicyAlways,以及設定一個 env 讓 drone 可以動態修改 template 檔案
        env:
        - name: FOR_GODS_SAKE_PLEASE_REDEPLOY
          value: 'THIS_STRING_IS_REPLACED_DURING_BUILD'
目的是讓每次 kubernetes 都可以讀取不一樣的 template 確保 image 都可以即時更新,假設少了上述步驟,是無法讓 staging 保持更新狀態。畢竟使用 kubectl apply 時,如果 yaml 檔案是沒有更動過的,就不會更新。
prepare:
    sed -ie "s/VERSION/$(VERSION)/g" deployment.yml
    sed -ie "s/THIS_STRING_IS_REPLACED_DURING_BUILD/$(shell date)/g" deployment.yml
    cat deployment.yml
而 Tag 就不用擔心,原因就是 VERSION 就會改變不一樣的值,所以肯定會即時更新,那假設團隊想要上傳相同 tag (這是不好的做法,請盡量不要使用),這時候動態修改 env 的作法就發揮功效了。從上面的教學,現在我們看安新的透過 GitHub Flow 來完成部署 Staging 及 Production 了。

影片簡介

下面影片並無包含實作部分,會介紹我在團隊怎麼使用 GitHub Flow 部署,更多實作詳細細節,可以參考 Udemy 上面影片

心得

作者我尚未深入玩過 GitLab CI 或者是 Jenkins 搭配 Kubernetes 來部署專案,但是我相信以複雜及學習難度來說,用 Drone CI 是比較簡單的,這部分就不多說了,大家實際操作比較過才知道。希望能帶給有在玩 Drone 的開發者有些幫助。另外我在 Udemy 上面開了兩門課程,一門 drone 另一門 golang 教學,如果對這兩門課有興趣的話,都可以購買,目前都是特價 $1800 如果兩們都有興趣想一起合買,請直接匯款到下面帳戶,特價 $3000
  • 富邦銀行: 012
  • 富邦帳號: 746168268370
  • 匯款金額: 台幣 $3000 元
匯款後請直接到 FB 找我,或者直接寫信給我也可以 appleboy.tw AT gmail.com。有任何問題都可以直接加我 FB,都是公開資訊。上面程式碼範例請參考如下

線上程式碼

如何在 Go 語言內寫效能測試

$
0
0
Go-brown-side.sh Go 語言不只有內建基本的 Testing 功能,另外也內建了 Benchmark 工具,讓開發者可以快速的驗證自己寫的程式碼效能如何?該如何使用基本的 Benchmark 工具,底下用簡單的例子來說明如何寫 Benchmark,透過內建工具可以知道程式碼單次執行多少時間,以及用了多少記憶體。不多說直接用『數字轉字串』來當例子。

線上影片

如果您不想看底下的文字說明,可以直接參考線上影片教學: 另外我在 Udemy 上面開了兩門課程,一門 drone 另一門 golang 教學,如果對這兩門課有興趣的話,都可以購買,目前都是特價 $1800 如果兩們都有興趣想一起合買,請直接匯款到下面帳戶,特價 $3000
  • 富邦銀行: 012
  • 富邦帳號: 746168268370
  • 匯款金額: 台幣 $3000 元

如何寫 Benchmark

建立 main_test.go 檔案
func BenchmarkPrintInt2String01(b *testing.B) {
    for i := 0; i < b.N; i++ {
        printInt2String01(100)
    }
}
  • 檔案名稱一定要用 _test.go 當結尾
  • func 名稱開頭要用 Benchmark
  • for 循環內要放置要測試的程式碼
  • b.N 是 go 語言內建提供的循環,根據一秒鐘的時間計算
  • 跟測試不同的是帶入 b *testing.B 參數
底下是測試指令:
$ go test -v -bench=. -run=none .
goos: darwin
goarch: amd64
BenchmarkPrintInt2String01-4    10000000               140 ns/op
PASS
基本的 benchmark 測試也是透過 go test 指令,不同的是要加上 -bench=.,這樣才會跑 benchmark 部分,否則預設只有跑測試程式,大家可以看到 -4 代表目前的 CPU 核心數,也就是 GOMAXPROCS 的值,另外 -run 可以用在跑特定的測試函示,但是假設沒有指定 -run 時,你會看到預設跑測試 + benchmark,所以這邊補上 -run=none 的用意是不要跑任何測試,只有跑 benchmark,最後看看輸出結果,其中 10000000 代表一秒鐘可以跑 1000 萬次,每一次需要 140 ns,如果你想跑兩秒,請加上此參數在命令列 -benchtime=2s,但是個人覺得沒什麼意義。

效能比較

底下直接看看『數字轉字串』效能評估,參考底下寫出三種數字轉字串函式,線上程式碼
func printInt2String01(num int) string {
    return fmt.Sprintf("%d", num)
}

func printInt2String02(num int64) string {
    return strconv.FormatInt(num, 10)
}
func printInt2String03(num int) string {
    return strconv.Itoa(num)
}
接著寫 benchmark,線上程式碼
func BenchmarkPrintInt2String01(b *testing.B) {
    for i := 0; i < b.N; i++ {
        printInt2String01(100)
    }
}

func BenchmarkPrintInt2String02(b *testing.B) {
    for i := 0; i < b.N; i++ {
        printInt2String02(int64(100))
    }
}

func BenchmarkPrintInt2String03(b *testing.B) {
    for i := 0; i < b.N; i++ {
        printInt2String03(100)
    }
}
跑測試
$ go test -v -bench=. -run=none -benchmem .
goos: darwin
goarch: amd64
BenchmarkPrintInt2String01-4    10000000               125 ns/op              16 B/op          2 allocs/op
BenchmarkPrintInt2String02-4    30000000                37.8 ns/op             3 B/op          1 allocs/op
BenchmarkPrintInt2String03-4    30000000                38.6 ns/op             3 B/op          1 allocs/op
PASS
ok      _/Users/mtk10671/git/go/src/github.com/go-training/training/example20-write-benchmark   3.800s
可以很清楚看到使用 strconv.FormatInt 效能是最好的。透過 -benchmem 可以清楚知道記憶體分配方式,用此方式就可以知道要優化哪些函示。1 allocs/op 代表每次執行都需要搭配一個記憶體空間,而一個記憶體空間為 3 Bytes

在本機端快速產生網站免費憑證

$
0
0
SSL-Certificate 大家看到網站免費憑證,一定會想到 Let’s encrypt 服務商提供一個網域可以使用 100 個免費憑證,如果您有很多 subdomain 需求,還可以申請獨立一張 wildcard 憑證,但是這是在伺服器端的操作,假設在本機端開發,該如何快速產生憑證,這樣開啟瀏覽器時,就可以看到綠色的 https 字眼 Snip20180706_2

安裝 mkcert

本篇介紹一個用 Go 語言寫的工具叫做 mkcert,此工具目前只有支援 MacOS 及 Linux 環境,未來會支援 Windows,如果有在玩 Windows 的開發者,也可以直接開 PR 啦。安裝方式非常簡單。在 MacOS 可以用 brew
$ brew install mkcert
$ brew install nss # if you use Firefox

使用 mkcert

第一步驟就是先初始化目錄
$ mkcert -install
接著看看有幾個網站 domain 需要在本機端使用可以一次申請
$ mkcert myapp.dev example.com
Using the local CA at "/Users/xxxxxx/Library/Application Support/mkcert" ✨

Created a new certificate valid for the following names 📜
 - "example.com"
 - "myapp.dev"

The certificate is at "./example.com+1.pem" and the key at "./example.com+1-key.pem" ✅

撰寫簡單 https 服務

這邊用 Go 語言當例子
package main

import (
    "log"
    "net/http"
)

func helloServer(w http.ResponseWriter, req *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte("This is an example server.\n"))
}

func main() {
    log.Println("Server listen in 443 port. Please open https://localhost/hello")
    http.HandleFunc("/hello", helloServer)
    err := http.ListenAndServeTLS(":443", "ssl/localhost.pem", "ssl/localhost-key.pem", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}
其中 ssl/localhost.pemssl/localhost-key.pem 就是剛剛透過 mkcert 產生出來的金鑰。透過 curl 工具,可以快速驗證是否成功:
$ curl -v https://localhost/hello
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: O=mkcert development certificate
*  start date: Jul  5 02:06:09 2018 GMT
*  expire date: Jul  6 02:06:09 2028 GMT
*  subjectAltName: host "localhost" matched cert's "localhost"
*  issuer: O=mkcert development CA
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7f8fca805800)
> GET /hello HTTP/2
> Host: localhost
> User-Agent: curl/7.54.0
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
< HTTP/2 200
< content-type: text/plain
< content-length: 27
< date: Fri, 06 Jul 2018 02:30:54 GMT
<
This is an example server.
* Connection #0 to host localhost left intact
上面範例放在 go-training 專案內,歡迎大家取用。

Go 語言實戰 GraphQL

$
0
0
Screen Shot 2018-07-19 at 8.58.48 AM 很高興能在 2018 ModernWeb 研討會跟大家分享用 Go 語言實戰 GraphQL,相信大家都知道 GraphQL 帶給前端後端及手機開發者很多好處,強烈推薦大家來嘗試看看。這場議程最主要是推廣 GraphQL 及 Go 語言。底下有投影片大家可以參考看看。

大綱

在這場議程,大致上有四大議題跟大家探討
  1. 為什麼我捨棄 RESTful 轉向 GraphQL
  2. 什麼是 GraphQL,它解決了什麼問題
  3. 在 Go 語言實戰 GraphQL 技巧踩雷
  4. 如何撰寫 GraphQL 測試
很感謝會後蠻多朋友跑過來跟我討論,但是礙於要趕搭高鐵回新竹,所以沒跟大家深入探討。如果有任何問題都可以直接聯絡我。

用 Caddy 申請 Let’s Encrypt Wildcard 憑證

$
0
0
Screen Shot 2018-07-27 at 11.29.44 AM 2018 年 3 月 Let’s Encrypt 官方正式公告支援 Wildcard Certificate 憑證,有在玩多個 subdomain 有福了,未來只要申請一張 *.example.com 就全部通用啦,當然很高興 Caddy 也跟進了,在 v11.0 正式支援多種 DNS Provider,只要申請 DNS 提供商的 API Key 或 Secret 設定在啟動 Caddy 步驟內就可以了。底下用 Godaddy 舉例。

申請 Godaddy API Key

請直接上 Godaddy 開發者網站申請,就可以正式拿到 Key 跟 Secret https___developer_godaddy_com_keys__🔊

下載 Caddy 執行檔

可以直接到官方網站下載,請記得選擇您的 DNS Provider plugin 才可以 Download_Caddy_🔊 接著點選左下角的 Download 按鈕,下方會顯示可以透過 CURL 方式來安裝
$ curl https://getcaddy.com | bash -s personal http.cache,http.expires,tls.dns.godaddy
直接把上面的指令貼到 Linux Console 上,這樣系統會預設將 Caddy 安裝到 /usr/local/bin 底下。

Caddy 設定檔並啟動

打開您的 Caddyfile 或者是其他檔案。裡面寫入
*.design.wu-boy.com {
    proxy / localhost:8081 {
        websocket
    }

    tls {
        dns godaddy
    }
}
請注意 dns 區域,請填上 DNS Provider,如果不知道要填什麼值,可以參考線上文件,完成後可以透過底下指令啟動:
$ GODADDY_API_KEY=xxxx \
GODADDY_API_SECRET=xxxx \
CADDYPATH=/etc/caddy/ssl \
caddy -conf=/etc/caddy/Caddyfile
啟動後,可以打開網頁測試看看 Screenshot_2018_7_27__11_25_AM

用 Drone CI/CD 整合 Packer 自動產生 GCP 或 AWS 映像檔

$
0
0
Screen Shot 2018-07-29 at 12.47.51 PM 本篇來介紹 Hashicorp 旗下其中一個產品叫 Packer,其實在 Hashicorp 旗下有很多其他雲端工具都非常好用,如果大家有興趣都可以上官網參考看看。而 Packer 是用來產生各大雲平台映像檔的工具,平行產生 AWS, GCP, DockerDigitalOcean … 等等眾多雲平台之映像檔對 Packer 來說相當容易,詳細可以參考這邊,也就是說透過 Packer 來統一管理各大雲平台的映像檔,用 JSON 檔案進行版本控制。假設您有需求要管理工程團隊所使用的 Image,你絕對不能錯過 Packer。Packer 不是用來取代像是 Ansible 或是 Chef 等軟體,而是讓開發者更方便整合 Ansible .. 等第三方工具,快速安裝好系統環境。

影片教學

如果不想看底下文字介紹,可以直接參考 Youtube 影片: 如果您對 Drone 整合 Docker 有興趣,可以直接參考線上課程 買了結果沒興趣想退費怎麼辦?沒關係,在 Udemy 平台 30 天內都可以全額退費,所以不用擔心買了後悔。

自動建立 AWS AMI 映像檔

不多說直接拿實際例子來實做看看,假設我們有個需求,就是需要產生一個 AMI 裡面已經內建包含了 Docker 服務,該如何來實現呢?底下是 Packer 所撰寫的 JSON 檔案,底下範例可以直接在這邊找到
{
  "variables": {
    "aws_access_key": "{{env `AWS_ACCESS_KEY_ID`}}",
    "aws_secret_key": "{{env `AWS_SECRET_ACCESS_KEY`}}",
    "ssh_bastion_host": "",
    "ssh_bastion_port": "22",
    "ssh_bastion_username": "",
    "ssh_bastion_private_key_file": "",
    "region": "ap-southeast-1"
  },
  "builders": [{
    "type": "amazon-ebs",
    "access_key": "{{user `aws_access_key`}}",
    "secret_key": "{{user `aws_secret_key`}}",
    "region": "{{user `region`}}",
    "source_ami_filter": {
      "filters": {
        "virtualization-type": "hvm",
        "name": "ubuntu/images/*ubuntu-xenial-16.04-amd64-server-*",
        "root-device-type": "ebs"
      },
      "owners": ["099720109477"],
      "most_recent": true
    },
    "instance_type": "t2.micro",
    "ssh_username": "ubuntu",
    "ami_name": "ggz-docker-image-{{isotime | clean_ami_name}}",
    "tags": {
      "Name": "ggz",
      "Environment": "production"
    },
    "communicator": "ssh",
    "ssh_bastion_host": "{{user `ssh_bastion_host`}}",
    "ssh_bastion_port": "{{user `ssh_bastion_port`}}",
    "ssh_bastion_username": "{{user `ssh_bastion_username`}}",
    "ssh_bastion_private_key_file": "{{user `ssh_bastion_private_key_file`}}"
  }],
  "provisioners": [{
      "type": "file",
      "source": "{{template_dir}}/welcome.txt",
      "destination": "/home/ubuntu/"
    },
    {
      "type": "shell",
      "script": "{{template_dir}}/docker.sh",
      "execute_command": "echo 'ubuntu' | sudo -S sh -c '{{ .Vars }} {{ .Path }}'"
    }
  ]
}
第一部分 variables 讓開發者可以定義變數,可以讀取系統環境變數,第二部分 builders 就是用來定義要產生不同平台的 Image,像是 GCP 或 AWS,可以看到是傳入一個 Array 值,上面的例子就是要產生 AWS AMI,所以設定 "type": "amazon-ebs",第三部分 provisioners,就是來寫 script,映像檔預設可能會有一些檔案,或者是預設安裝一些工具,看到 type 可以是 fileshell 等等,也就是說 provisioners 可以讓開發者安裝套件,更新 Kernel,建立使用者,或者是安裝下載 application source code。這對於部署來說是一個非常棒的工具。

執行 Packer

完成上述 JSON 檔案後,就可以透過 Packer 來產生 AWS AMI
$ packer build -var-file=config/mcs.json mcs.json
amazon-ebs output will be in this color.

==> amazon-ebs: Prevalidating AMI Name: ggz-docker-image-2018-07-29T06-11-12Z
    amazon-ebs: Found Image ID: ami-1c6627f6
==> amazon-ebs: Creating temporary keypair: packer_5b5d5a80-c1e2-e266-e0b8-bc7c6e63dba3
==> amazon-ebs: Creating temporary security group for this instance: packer_5b5d5a82-5d1f-c702-18f4-992ac37e885a
==> amazon-ebs: Authorizing access to port 22 from 0.0.0.0/0 in the temporary security group...
==> amazon-ebs: Launching a source AWS instance...
==> amazon-ebs: Adding tags to source instance
    amazon-ebs: Adding tag: "Name": "Packer Builder"
    amazon-ebs: Instance ID: i-0d12e2a9e6f00a410
==> amazon-ebs: Waiting for instance (i-0d12e2a9e6f00a410) to become ready...
透過 -var-file 將隱秘資訊寫到檔案內,像是 AWS Secret Key 等等。

整合 Drone CI/CD

上一個步驟可以透過指令方式完成映像檔,本章節會教大家如何跟 Drone 整合,這邊可以直接使用 drone-packer 套件,使用文件也已經放到 drone plugin 首頁了。使用方式非常簡單,請參考底下範例:
pipeline:
  packer:
    image: appleboy/drone-packer
    pull: true
    secrets: [ aws_access_key_id, aws_secret_access_key ]
    template: ggz.json
    actions:
      - validate
      - build
    when:
      branch: master
其中 template 請輸入 json 檔案路徑,actions 目前只有支援 validatebuild,我建議兩者都寫,先驗證 json 檔案是否寫錯,再執行 build。另外我們可以看到
  "variables": {
    "aws_access_key": "{{env `AWS_ACCESS_KEY_ID`}}",
    "aws_secret_key": "{{env `AWS_SECRET_ACCESS_KEY`}}"
  },
其中 aws_access_key 是讀取系統環境變數 AWS_ACCESS_KEY_ID,所以可以透過 drone secret 將變數設定上去
$ drone secret add \
  -repository go-ggz/packer \
  -image appleboy/drone-packer \
  -event push \
  -name aws_access_key_id \
  -value xxxxxx
請注意記得將敏感資訊綁定在 -image 身上,避免被偷走。上面的範例,可以直接參考 go-ggz/packer

在 Go 語言內的 URL RawQuery 的改變

$
0
0
更新 (2018.08.29) 感謝中國網友幫忙發個 Issue,大家有空可以關注看看,等官方怎麼回應 Go-Logo_Blue Go 語言內的 net/url 函式庫讓開發者可以簡單的 Parse 指定的 URL,最近 Google 上了這個 Patch,這個 Patch 讓原本的 RawQuery 值產生了變化,原先沒有驗證 RawQuery 是否包含了不合法的字元,現在只要 RawQuesy 內含有任意的不合法字元,就會直接被 QueryEscape 函式轉換,這個 Patch 不影響這次 Go 1.11 版本,會影響的是明年 2019 年釋出的 Go 1.12 版本,但是大家都知道在 GitHub 上面有在寫測試的話,都會在 Travis 內加入 master 版本當作驗證,如果有用到 RawQuery 的話,肯定會遇到這問題,底下來描述為什麼會出現這問題。

RawQuery 含有不合法字元

首先來看看在 Go 1.11 版本時本來應該輸出什麼,請直接線上看例子
package main

import (
    "log"
    "net/url"
)

func main() {
    u, err := url.Parse("http://bing.com/search?k=v&id=main&id=omit&array[]=first&array[]=second&ids=111&ids[j]=3.14")
    if err != nil {
        log.Fatal(err)
    }

    if u.RawQuery != "k=v&id=main&id=omit&array[]=first&array[]=second&ids=111&ids[j]=3.14" {
        log.Fatal("RawQuery error")
    }

    log.Printf("%#v", u.Query())
}
在 Go 1.11 以前,你會直接看到底下輸出:
url.Values{“k”:[]string{“v”}, “id”:[]string{“main”, “omit”}, “array[]”:[]string{“first”, “second”}, “ids“:[]string{“111”}, “ids[j]”:[]string{“3.14”}}
url 函式庫幫忙把 RawQuery 整理成 map[string][]string 格式,所以在 URL 內可以直接 Parse array[]=first&array[]=second 多個 Array 值。這個預設行為在最新的 Go 語言被換掉了,現在執行 u.Query() 你會看到變成底下,整串的 Raw Query String 被當長一個 Key 值了。
url.Values{“k=v&id=main&id=omit&array[]=first&array[]=second&ids=111&ids[j]=3.14″: []string{“”}}
這就是最大的改變,造成在 Travis 執行錯誤。

如何修正

修正方式其實很簡單,自己在寫個小型 Parser 把原本的格式在轉換就好,請參考線上解法
package main

import (
    "log"
    "net/url"
    "strings"
)

func main() {
    u, err := url.Parse("http://bing.com/search?k=v&id=main&id=omit&array[]=first&array[]=second&ids=111&ids[j]=3.14")
    if err != nil {
        log.Fatal(err)
    }

    if u.RawQuery != "k=v&id=main&id=omit&array[]=first&array[]=second&ids=111&ids[j]=3.14" {
        log.Fatal("RawQuery error")
    }

    log.Printf("%#v", u.Query())

    query := resetQuery(map[string][]string{"k=v&id=main&id=omit&array[]=first&array[]=second&ids=111&ids[j]=3.14": []string{""}})
    log.Printf("%#v", query)
}

func resetQuery(m map[string][]string) map[string][]string {
    dicts := make(map[string][]string)
    for k, v := range m {
        lists := strings.Split(k, "&")
        if len(lists) == 1 {
            dicts[k] = v
            continue
        }
        for _, vv := range lists {
            p := strings.Split(vv, "=")
            dicts[p[0]] = append(dicts[p[0]], p[1])
        }
    }
    return dicts
}
只要 RawQuery 裡面有包含底下字元,就會被 escape 掉
// validQuery reports whether s is a valid query string per RFC 3986
// Section 3.4:
//     query       = *( pchar / "/" / "?" )
//     pchar       = unreserved / pct-encoded / sub-delims / ":" / "@"
//     unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
//     sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
//                   / "*" / "+" / "," / ";" / "="

後記

現在含以前的版本都不會遇到這問題,如果你有用一些 Framework 請務必在明年釋出下一版後,一起跟著升級,Gin 現在已經發 Patch 修正了。

在 PostgreSQL 時區轉換及計算時間

$
0
0
2000px-Postgresql_elephant.svg 通常在使用資料表時,都會在每一筆紀錄上面寫入當下時間,而這個時間會根據目前系統所在的時區而有所不同,當然我們都會使用 UTC+0 作為標準時區,而欄位我們則會是使用 timestamp 或者是 unix time 格式,兩者最大的差異就是在前者 (timestamp) 會根據目前系統的時區來記錄,而後者 (unix time) 則是紀錄秒數差異 (Jan 01 1970) 而不會隨著系統時區改變而變化。如果是發展開源專案,則會使用後者居多,這樣不會因為使用者時區變化,而產生不同的差異,在 Gitea 開源專案保留了兩者,但是只要計算時間則是用 (unix time) 作轉換。

計算時區問題

針對底下兩個問題來看看該如何在 PostgreSQL 內計算時區,首先我們先定義在系統存放的時間都是統一 UTC+0 時區,而使用者查詢時的瀏覽器為台灣時間 Asia/Taipei 時區為 UTC+08:00,底下是第一個問題
查詢條件為當下時間過去 24 小時的全部紀錄
也就是說現在時間為 2018-09-02 15:00 那就是請抓取 2018-09-01 15:002018-09-02 15:00 區間內所有記錄,這個問題其實不難,跟時區也沒有任何關係,不管系統是存 UTC+0 或 UTC+8 都不影響。只要我們抓 now() 往前推算 24 小時即可。假設資料表有一個欄位為 created_at 存的是 timestamp 格式。底下就是解法:
select title, desc from users \
  where created_at > now() - interval '1 day'
其中 now() - interval '1 day' 代表著現在時間去減掉 1 天的時間。這邊沒有時區的問題,假設另一個問題如下:
請查詢過去 7 天的記錄 (含當下當天資料)
假設現在時間為 2018-09-02 16:00+08:00 (台灣時間星期天),這時候我們預設的查詢時間範圍會是 2018-08-27 00:00+08002018-09-02 16:00+08:00 時間區間內所有資料,底下是目前資料庫的資料:
id created_at (utc+0)
1231 2018-08-26 18:25:35.624
1225 2018-08-26 19:15:19.187
1220 2018-08-27 04:24:59.306
1222 2018-08-27 05:38:57.174
1230 2018-08-27 07:21:35.897
1239 2018-08-28 07:37:52.345
1264 2018-08-30 05:21:17.157
1290 2018-08-31 12:05:04.764
1356 2018-08-31 20:51:29.784
1358 2018-09-01 12:14:13.118
1355 2018-09-01 19:21:36.482
1354 2018-09-02 03:18:38.626
1361 2018-09-02 03:37:05.171
這時候使用上面的解法試試看:
where created_at  > now() - interval '6 day'
拿到底下資料
id created_at (utc+0)
1239 2018-08-28 07:37:52.345
1264 2018-08-30 05:21:17.157
1290 2018-08-31 12:05:04.764
1356 2018-08-31 20:51:29.784
1358 2018-09-01 12:14:13.118
1355 2018-09-01 19:21:36.482
1354 2018-09-02 03:18:38.626
1361 2018-09-02 03:37:05.171
這時候你會發,怎麼 27 號的資料都沒有進來呢?原因出在 now() - interval '6 day' 計算出來的結果會是讀取時間大於 2018-08-27 16:00+08:00,那換算 UTC 時間則為 2018-08-27 08:00+00:00,這樣是不對的,那 8/27 該天的 00:00 ~ 08:00 的時間也沒被算進去,這時候需要時間的轉換
where created_at  > (now() - interval '6 day')::date
(now() - interval '6 day')::date 就可以把時間調整為當天 00:00 開始計算。這樣我們找出來的資料便是:
id created_at (utc+0)
1220 2018-08-27 04:24:59.306
1222 2018-08-27 05:38:57.174
1230 2018-08-27 07:21:35.897
1239 2018-08-28 07:37:52.345
1264 2018-08-30 05:21:17.157
1290 2018-08-31 12:05:04.764
1356 2018-08-31 20:51:29.784
1358 2018-09-01 12:14:13.118
1355 2018-09-01 19:21:36.482
1354 2018-09-02 03:18:38.626
1361 2018-09-02 03:37:05.171
可以正確抓到 2018-08-27 的資料,但是看到這邊是不是又覺得怪怪的,最前面兩筆應該也要被算進來,我們先把上面的時區全部 +08:00
select created_at, \
  created_at at time zone 'UTC' at time zone 'Asia/Taipei'
created_at (utc+0) created_at (utc+8)
2018-08-26 18:25:35.624 2018-08-27 02:25:35.624
2018-08-26 19:15:19.187 2018-08-27 03:15:19.187
2018-08-27 04:24:59.306 2018-08-27 12:24:59.306
2018-08-27 05:38:57.174 2018-08-27 13:38:57.174
2018-08-27 07:21:35.897 2018-08-27 15:21:35.897
2018-08-28 07:37:52.345 2018-08-28 15:37:52.345
2018-08-30 05:21:17.157 2018-08-30 13:21:17.157
2018-08-31 12:05:04.764 2018-08-31 20:05:04.764
2018-08-31 20:51:29.784 2018-09-01 04:51:29.784
2018-09-01 12:14:13.118 2018-09-01 20:14:13.118
2018-09-01 19:21:36.482 2018-09-02 03:21:36.482
2018-09-02 03:18:38.626 2018-09-02 11:18:38.626
2018-09-02 03:37:05.171 2018-09-02 11:37:05.171
有沒有發現第一筆跟第二筆,在台灣時間是在 08-27 號,所以理論上應該要是我們的查詢範圍之間,但是沒有被查到。解決方式就是將欄位都先轉成使用者時區再去做計算
created_at at time zone 'utc' time zone 'Asia/Taipei' > \
 (now() at time zone 'Asia/Taipei1' - interval '6 day')::date
其中關鍵點就在把 created_at 先轉 utc+0 再轉 utc+8 最後才做比較。

後記

使用者時區會隨在手機的所在地點做轉換,所以這邊的最好的作法就是,在資料庫統一存放 UTC+0 的時區,接著在 App 端登入帳號時,將使用者時區字串帶入到 Token 內,這樣使用者從台灣飛到美國時,登入 App 就能即時看到美國時區的資料。
Viewing all 325 articles
Browse latest View live