[Golang] Golang buffered/unbuffer channel and pipeline

前提

如何說明一個人 Golang 寫得夠不夠熟練,我大部分都是問 “ 可以請你解釋一下 buffered 跟 unbuffered channel 的差異?”

往往這樣的題目都會問倒一堆人.不是大家對這個語言不熟悉,而是一般人在學習 golang 的 goroutine 的時候,原本就已經很少使用 channel 來管理,更別說使用 buffered channel .

這篇文章,我會稍微提一下 buffered/unbuffered channel 的差異.並且透過最近遇到 pipeline 的問題來討論一下.

Buffered/Unbuffered Channel

Unbuffered Channel

先講 unbuffered channel ,也就是最基本大家使用的 channel

ch := make(chan bool)

go func() {
    ch <- true
}()

// keep waiting after goroutine run
<-ch

這邊是一個最簡單的 unbuffered channel 的案例,這邊需要注意的相關事情.

  • 由於 unbuffered channel 只有一個位置,所以當你已經存入之後. (e.g. ch <- true) 第二個要存入也會卡住(直到第一個 pop 出來)
  • <-ch 會造成 STW(Stop The World) ,才會驅動 goroutine 驅動.也才能導致 ch <- true 才能跑得到.不然會卡死.這也是如果你想在同一個 goroutine 跑 chan push 跟 pop 會沒有作用的原因.

Buffered Channel

Buffered Channel 顧名思義就是具有多個的 channel,參考一下:

ch := make(chan int, 3) //建立大小為 3 的 buffered channel
go func() {
    ch <- 1
    ch <- 2
    ch <- 3
}()

fmt.Println(<-ch) //1
fmt.Println(<-ch) //2
fmt.Println(<-ch) //3

這是一個很簡單的例子,既然 channel 為一個 slice ,當然也可以 iterate 。

ch := make(chan int, 3) //建立大小為 3 的 buffered channel
go func() {
    ch <- 1
    ch <- 2
    ch <- 3
}()

for n := range ch {
    fmt.Println(n)
}

當你覺得好像很正確的時候,跑下去就會發生有問題. 參考 playground

問題發生在哪?

可以參考一下 Go By Example的這段說明. https://gobyexample.com/range-over-channels

This range iterates over each element as it’s received from queue. Because we closed the channel above, the iteration terminates after receiving the 2 elements.

必須改成以下的方式:

ch := make(chan int, 3) //建立大小為 3 的 buffered channel
go func() {
    ch <- 1
    ch <- 2
    ch <- 3
    close(ch)
}()

for n := range ch {
    fmt.Println(n)
}

參考 playground

Pipeline pattern

pipeline 可以很方便地處理多步驟地處理流程,比如說 ETL 或是 影像處理. 官方的 pipeline blog 有很詳盡的範例介紹.

在這裡先簡單的串起來一下,

func gen(nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		for _, n := range nums {
			fmt.Printf("%d into first queue \n", n)
			out <- n
			fmt.Printf("%d completed into first queue \n", n)
		}
		close(out)
	}()
	return out
}

func sq(in <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		for n := range in {
			fmt.Printf("%d into second queue \n", n)
			out <- n * n
			fmt.Printf("%d completed into second queue \n", n)
			time.Sleep(1 * time.Second)
		}
		close(out)
	}()
	return out
}

func main() {
	// Set up the pipeline.
	c := gen(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
	out := sq(c)

	// Consume the output.
	for v := range out {
		fmt.Println("Result:", v)
	}
}

//1 into first queue 
//1 completed into first queue 
//2 into first queue 
//1 into second queue 
//1 completed into second queue 

這個範例你可以很清楚看到,由於使用 unbuffered channel 所以 1 要先離開第一個 channel 之後,2 才能進去.

Fan-out with Pipeline

Fan-out 是一個作法就是一次讓多個 goroutine 來跑.也是可以搭配著 pipeline 來跑 fan-out . 讓我們來看以下的範例.

func sq(in <-chan int) <-chan int {
	out := make(chan int, 1000)
	for i := 0; i <= 3; i++ {
		go func() {
			for n := range in {
				out <- n * n
				fmt.Printf("%d is out queue \n", n)
				time.Sleep(1 * time.Second)
			}
		}()
	}
	return out
}

參考: Playground

你會發現這個無法跑完,因為他會卡住而跑出

fatal error: all goroutines are asleep - deadlock!

原因就出在之前提過的忘記 close channel . 記得,如果前面一個 pipeline 沒有 close channel ,後面的 for range 就會卡住.

那要如何能夠精準地知道何時才能 close channel?

最後解法

最後改法,加上 waitgroup 的方式來等到 fan-out 的結果:

Refer: Playground

[TIL][Kubernetes] 開發一個 Kubernetes Secret 相關應用筆記

前提:

通常在 Kubernetes 裡面要將設定檔寫入的方式有兩種:

當資料不敏感(具有保密資料) 的時候,我們會使用 ConfigMap ,當你要儲存比較敏感的資料 (service account, password, 私密資料) 就要使用 Secret

簡單介紹 Kubernetes Secret:

Kubernetes secret introduction from Evan Lin

不囉唆,看之前整理好的投影片.

不分類小筆記:

Secret 設定相關:

  • Kubenetes Secret 無法在 Entrypoint 就讀取到,必須等到 POD 跑起來後執行.
  • 如果要透過 Secret 來讀取 Kubernetes service account 的相關設定,建議跑在 Command, Args 裡面設定.

Kubernetes 執行 Commands 與 Args:

  • Kubernetes yaml 的 ARGs 主要拿來跑參數用,如果想要作為 command 跑 sh 的內容,要使用 && 就不能分散在 Args 之中.

如何透過 Secret 處理敏感資料 (密碼, Key, Kubenetes 設定文件)

  • 如果要將敏感的 json data (configuration, service accoutn, password config) 寫入 secret volume 可以透過 base64 來 encode
  • 透過 base64 encode 的 Secret 資料,不需要再跑 decode .可以直接在 Pod 裡面讀取.

[TIL][makefile] make file 進階用法 (Secondary Expansion)

問題敘述

我有下列的服務要透過 make file 來編譯 Dockerfile ,由於那些服務有版本的區別,而且不同的版本都需要同時存在. 也就是說 app:v1 跟 app:v2 必需要同時能存在,並且要能夠讓 make file 能夠支援新的版本 app:v3 的產生. 舉例來說,我之後只要輸入 make build-images 就要能夠自動跑出 app1-v1, app1-v2, app2-v1 並且能夠根據這些 build target 自動 build 相關的 docker image

  • build
    • app1
      • v1
        • Dockerfile
      • v2
        • Dockerfile
    • app2
      • v1
        • Dockerfile

如何得到 build target

首先我們要能搜集所有的 build targets ,透過一系列的 build target 再來跑相關的 make process.

BUILD_DOCKERFILES := $(sort $(wildcard build/*/*/Dockerfile))

這一段是找到所有具有 Dockerfile 的檔案.. 在這裡會找出…

  • build/app1/v1/Dockerfile
  • build/app1/v2/Dockerfile
  • build/app2/v1/Dockerfile

這時候我們要做一點小處理,要去除 Dockerfile 這個不需要的檔案名稱.這個要透過上一篇提過的 % 來處理. 快速講解一下,下面的例子是說,不論何種字串只要是 xxxx/Dockerfile 都會被取代成 xxxx

BUILD_DIRS := $(patsubst %/Dockerfile,%,$(BUILD_DOCKERFILES))

這時候 BUILD_DIRS 就會轉換為:

  • build/app1/v1
  • build/app1/v2
  • build/app2/v1

再來,我們要整理成 build-app-v1 的格式,這個可以透過 subst 來替代

BUILD_APP_VER := $(subst /,-,$(BUILD_DIRS))

這樣就會得到:

  • build-app1-v1
  • build-app1-v2
  • build-app2-v1

最後,我們要其轉換成 app1-v1, app1-v2 … 一樣透過 % 來替換

BUILD_NAMES := $(patsubst build-%,%,$(BUILD_APP_VER))

最後我們要得到 build target 希望是 build-app1-v1, build-app1-v2…

BUILD_TARGETS := $(addprefix notebook-image-,$(BUILD_NAMES))

這樣就可以得到我們要的結果.

最後整理一下…

BUILD_DOCKERFILES := $(sort $(wildcard build/*/*/Dockerfile))
BUILD_DIRS := $(patsubst %/Dockerfile,%,$(BUILD_DOCKERFILES))
BUILD_APP_VER := $(subst /,-,$(BUILD_DIRS))
BUILD_NAMES := $(patsubst build-%,%,$(BUILD_APP_VER))
BUILD_TARGETS := $(addprefix notebook-image-,$(BUILD_TARGETS))

開始撰寫 build target 本體要做的事情

這時候因為我們得到 build-app1-v1, build-app1-v2build-app2-v1. 我們就要來展開要做的事情.我們要透過展開 build-app1-v1 來編譯出 docker.io/evanlin/app:v1 的 docker tag image

一個錯誤的範例

build-%: build/$(subst -,/,%)/Dockerfile $(shell find build -type f)
...

先解釋一下,在 build-%: 後面的就是條件式 prerequisites ,也就是必須要符合條件內才會繼續往下執行.在這裡條件是 透過找所有 build 子目錄所有檔案,來確認是否有符合 build/app1/v1/Dockerfile 的檔案.

起出這樣看起來很正常,我們想要透過替換字串來讓 % 裡面的內容來修改 app1-v1app1/v1 但是這時候會發現無法替換.

Secondary Expansion

經過查詢 GNU Make: Secondary Expansion 可以達成我的需求.所以修改如下:

.SECONDEXPANSION:
build-%: build/$$(subst -,/,%)/Dockerfile $(shell find build -type f)
...

這樣就能夠找到,也能夠繼續之後的處理.

Reference

[TIL][shell] Switch from fish to zsh

摘要

當初玩 Mac 從一開始很痛苦的 bash ,到了換了好像超好用的 fish (friendly interactive shell) 可以參考一下 [TIL] Note: about change your shell from bash to fish

當然,之後也有想要換到 zsh 參考這篇 [TIL] Change fish shell theme with nerd font 但是又遲遲不敢換.

不過今天總算發生了重要的問題. 公司一堆 Operation 相關的 Makefile 竟然完全死在 fish 上面. GG

只好認真來換… zsh

基本的 zsh 配備

當然要先看看這篇我認為超適合當 zsh (還有 oh-my-zsh) 的推坑文超簡單!十分鐘打造漂亮又好用的 zsh command line 環境 基本流程就依照這篇就好,補充一些:

  • iterm2 換字型到 Sauce Code Pro Nerd Font Complete 如果你字體大小是在 12 很容易卡死(hang) . 就得要清除所有 iterm2 的設定 · 建議要先切換字體大小到 14 再換字型.
defaults delete com.googlecode.iterm2
  • iTerm2 本身對於各個 Shell 都有 integration ,可以讓你使用起來更方便.
curl -L https://iterm2.com/shell_integration/zsh \
  -o ~/.iterm2_shell_integration.zsh

source ~/.iterm2_shell_integration.zsh
  • POWERLEVEL9K 雖然很棒,但是不是太多功能需要.這裡列出我使用的功能
export TERM="xterm-256color"


POWERLEVEL9K_MODE='nerdfont-complete'
ZSH_THEME="powerlevel9k/powerlevel9k"
# command line 左邊想顯示的內容
POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(dir vcs) 
# command line 右邊想顯示的內容
POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(kubecontext) 

POWERLEVEL9K_SHORTEN_DIR_LENGTH=1
POWERLEVEL9K_SHORTEN_DELIMITER=""
POWERLEVEL9K_SHORTEN_STRATEGY="truncate_from_right"

進階設定

這裡介紹幾個好用 plugin ,可以幫助大家快速(無痛?)切換到 zsh

plugins=(
#git 可以有一些快捷鍵 ggl ...
  git
# 自動補全   
  zsh-completions
  gitfast
# 解壓縮用
  extract
# z: 可以快速並且支援模糊切換目錄 ex: z docume <tab>
  z
# 讓你喚回 fish 漂亮的顏色
  zsh-syntax-highlighting
# 就像 fish 一樣,打到一半自動建議你
  zsh-autosuggestions
)

相關習慣變更

  • Quick Search History:
    • 先按下 “Ctrl + R” 再打你需要的…
  • Auto-Completed:
    • 先打出你需要的 ex: gcloud 然後再按下 (up) 鍵
  • 相關路徑與設定
    • GOBIN, GOPATH
    • Heroku path
    • Gcloud path (reinstall again)
    • cocoa pods PATH
    • LIBRARY_PATH

其他就還好,大概就是之前被 fish 強制修改的習慣.

  • ; —> &&
  • $(VAR) —> (VAR)
  • env ABC=edc —> ABC=edc

VSCode integrated terminal 也要改

其實剛換過去,還沒有感覺到有太多的不同.不過 VSCode 一打開就發現有個嚴重的問題, ` vscode 還在使用 fish shell`

趕緊打開 user.setting (cmd+,) ,然後加入以下的修改

    "terminal.integrated.shell.osx": "zsh",
    "terminal.integrated.cursorBlinking": true,
    "terminal.integrated.fontSize": 12,

Reference

[TIL][makefile] 一些常用的 Makefile 指令整理

摘要

花了一點時間玩了一下 makefile ,有一些字串對應方式與使用方式在不使用 sed 的前提下其實也很方便. 順手整理一下一些常用的案例.

並且也列出一些使用 makefile 在編譯 Dockerfile 的時候經常會用到的範例.

基礎常見語法:

wildcard

透過 wildcard 可以擴展找出所有匹配的項目.

找出所有具有 Dockerfile 的目錄(並且依序排列)

DOCKERFILE_PATHS := $(sort $(wildcard */Dockerfile))

patsubst 替換字串

常用再換掉路徑,去除路徑.以下紀錄幾個常用的.

取得 Parent Path (ex: a/b/c/d –> a/b/c)

PARENT_PATHS := $(patsubst %/,%,$(dir $(CURRENT_PATHS)))

nodir 去除所有目錄

這個可以幫你去除所有目錄,剩下檔名. ex: a/b/c/d –> d

取得上一層目錄列表

PARENT_PATH_LIST := $(notdir $(dir $(CURRENT_PATHS)))

這裡也要解釋一下, dir 會取得目錄而已.跟 nodir 剛好相反.

另外, basenamedir 也不同.

$(basename a/b/c/d.a) —> d

進階用法 *, %$*$<

這邊講解起來會有點難懂,就用一個案例來講解.

  • $* 會找出所有結果
  • % 會將前方變數帶給之後用
  • $< 列出前面的結果

以下用個例子

範例(1) : 找出 Dockerfile 並且編譯

build-image-%: %/Dockerfile $(shell find $* -type f)
	time docker build  \
		--tag docker.io/$(YOUR_PROJECT)/%:master \
		--file $< \
		$(dir $<)

這個會去編譯某個你輸入底下的 Dockerfile .

舉例而言: 你有個目錄 tensorflow/Dockerfile 你就要輸入 make build-image-tensorflow ,他就會自動找出這個 Dockerfile 並且編譯名稱為 $(YOUR_PROJECT)/tensorflow:master 以下透過這個案例解釋每個用法:

  • % 會將前面輸入的,帶到後面.在這裡也就是 tensorflow 帶到第二個與第三個出現%的地方
  • $< 會列出 shell find $* -type f 結果,也就是列出所有檔案,並且符合 %/Dockerfile 也就是如果你有其他檔案舉例為 tensorflow/README.md 也不會列出.

似乎這樣好像很麻煩? 其實透過前面介紹的基礎常見語法,可以更方便編譯.

範例(2) : 找出所有符合目錄直接帶到以上編譯

#find all docker files
DOCKERFILES := $(sort $(wildcard */Dockerfile))

#extract dir name
DIRS := $(patsubst %/Dockerfile,%,$(basename $(DOCKERFILES)))

#remain only dir name list
NAMES := $(notdir $(DIRS))

#add prefix combine to function strings
TARGETS := $(addprefix build-image-,$(NAMES))

#get all functions and run it.
all: $(TARGETS)

舉例而言,你有以下目錄結構

  • a/README.md
  • b/Dockerfile
  • b/README.md
  • c/Dockerfile
  • d/test.c

你現在只要輸入 make all 就會自動把 b/Dockerfile 與 c/Dockerfile 找出來,並且組成字串 build-image-b build-image-c 然後呼叫到範例(1)來呼叫兩個 docker build

Reference

[TIL][vgo] 對於 Version and Go 相關詳解(part1)

目錄:

摘要

vgo 是 Golang 將在 1.11 提出的新功能.提供著套件的管理與版本的控制.

本篇文章會解釋 vgo (versioning go) 與 dep 在 sub-package的挑選上有什麼不同. 並且解釋出 vgo 如何解決掉大家的問題. 並且在 Go versioning 的準則 (principles) 上, Russ Cox 有列出以下的準則:

  • Compatibility
  • Repeatability
  • Cooperation

接下來,我會整理一下近期學習的經驗.試著讓各位能在這篇文章中了解.

Compatibility

這邊講的是套件在開發上的”兼容性”,他也很強調所謂的 “Semantic Import Versioning” . 也就是如同上圖所顯示的部分一樣,任何的產品與套件都應該遵循以上的版本法則.

  • Major version: 具有向後不相容的變動,就要變更.
  • Minor version: 當有新的功能加入,就要變更.
  • Patch version: 只是問題的修復.

在這裡,只要是 Major version 是一樣的話,就不應該產生有任何向後不相容的變動.也建議不要有 API 的版本變動.

Repeatability

為了讓每個使用軟體(或是套件)上能夠讓每個人在任何時間都能夠透過相同的程式碼與相同的套件版本來使用.

Opening keynote: Go with Versions - GopherConSG 2018 演講的時候 , Russ Cox 曾經提到一個很重要的概念.

What is software engineering?

Software engineering is what happens to programming when you add time and other programmers.

也就是說 Software Engineering 是經過一段時間的開發,並且是跟其他人一起開發. 所以來說”版本的可重複性”(Repeatability) 就格外的重要.

  • 你必須要在半年後接手開發的人,能夠拿到跟你目前一樣狀態的品質與原始碼.
  • 你也必須要讓其他人不論本地端是否有預裝哪些套件,也要能夠拿到相同的代碼

在提到其他的部分之前,我們得提一下跟 Repeatability 相反的部分. Low-Fidelity Builds

Low-Fidelity Builds

這裡解釋一下,何謂 “Low-Fidelity Builds” .並且解釋為何 vgo 能夠達成 High-Fidlity Build.

Low-Fidelity Builds (低忠誠度的版本),也就是表示你的版本控管並不嚴謹.使得你的套件在版本變更的時候,會讓你的產品穩定度低下.

假設我們開發套件 A ,裡面使用到 B 與 C 套件.假設 A 必須要使用到 B1.2 與 C1.2 才能正常運作.

範例 1: 使用 go get

當你使用 go get 他會檢查目前 GOPATH 資料夾內是否有相關套件, 條件如下:

  • 當套件存在的時候,他會直接使用該套件(不論版本是否新舊)
  • 當套件不存在的時候,會抓下最新的套件.

問題:

當你 GOPATH 裡面有 B1.1 這時候跑 go get 就不會更新套件,而造成你在跑的時候可能會造就 build fail 的狀態.

範例 2: 使用 go get -u

使用 go get -u 會無條件將所有使用的套件更新到最新版本,這並不是代表沒有問題的.

問題:

如同以上的圖示,如果執行到 go get -u 就會抓下最新的套件 B 與 套件 C .也就是說會抓下 B1.2C1.3 .由於套件 A 在開發的時候,並沒有測試過 C1.3 這個版本的套件,這時候可能會造成錯誤的產生.

結論

所以可以看得出來,不論是原生的 go get -d 或是更新套件 go get -u 的方式.透過這些方式所產生的套件管理都是具有低忠誠度的版本控管.

接下來會來解釋,透過 vgo 能夠如何來管理套件的版本並且解決原始 go get 所帶來的問題.

接下來會帶到 vgo 如何透過 Minimal Version Selection 來解決這樣的問題.

下次文章會開始解釋 MVS (Minimal Version Selection)

Reference