這是一個建立於
的文章,其中的資訊可能已經有所發展或是發生改變。
之前一直在用qiniu的儲存服務,產生圖片的縮圖,模糊圖,視頻的webp,現在需要把儲存移到s3上,那麼這些圖片,視頻處理就要自己動手寫了,本文梳理一下大致的思路。
分析需求
先看一下qiniu的介面是如何處理圖片的,例如先截取視頻第一秒的圖片,再把圖片縮減,最後儲存到一個新的key,命令可以這麼寫 vframe/jpg/offset/1|imageMogr2/thumbnail/400x|saveas/xxx, 可以看到三個操作之間用 | 符號分割,類似unix 的 pipe 操作。
上面的操作算作一個cmd, 一次API請求可以同時處理多個cmd,cmd之間用分號分割, 處理完畢後,在回調中把處理結果返回,例如
{ "id": "xxxxx", "pipeline": "xxx", "code": 0, "desc": "The fop was completed successfully", "reqid": "xTsAAFnxUbR5J10U", "inputBucket": "xxx", "inputKey": "xxxxx", "items": [ { "cmd": "vframe/jpg/offset/1|imageMogr2/thumbnail/400x|saveas/ZmFtZS1wcml2YXRlOm1vbWVudC9jb3Zlci9zbmFwL3ZpZGVvL2M5YzdjZjQ5LTU3NGQtNGZjMS1iZDFkLTRkYjZkMzlkZWY1Ni8wLzA=", "code": 0, "desc": "The fop was completed successfully", "hash": "FhdN6V8EI4vW4XJGALSfxutvMEIv", "key": "xx", "returnOld": 0 }, { "cmd": "vframe/jpg/offset/1|imageMogr2/thumbnail/400x|imageMogr2/blur/45x8|saveas/ZmFtZS1wcml2YXRlOm1vbWVudC9jb3Zlci9zbmFwL3ZpZGVvL2M5YzdjZjQ5LTU3NGQtNGZjMS1iZDFkLTRkYjZkMzlkZWY1Ni8wLzBfYmx1cg==", "code": 0, "desc": "The fop was completed successfully", "hash": "FgNiRzrCsa7TZx1xVSb_4d5TiaK3", "key": "xxx", "returnOld": 0 } ]}
分解需求
這個程式大致需要這麼幾個部分:
-
一個http介面,接受任務,接受後把任務扔到隊列,返回一個job ID。 worker非同步處理任務,worker的個數 和 每個worker 並行的處理的個數 能夠配置,worker有重試機制。
-
從 job payload 中解析出需要做的任務,解析出每個cmd, 最好能並存執行每一個 cmd, 記錄每一個cmd的結果
-
每個cmd中有多個 operation, 並且用 pipe 串連,前一個operaion的輸出是後一個operation的輸入
可以把 1 和 2,3 分開來看,1 比較獨立,之前寫過一個worker的模型,參考的是這篇文章 Handling 1 Million Requests per Minute with Go,比較詳細,是用 go channel 作為queue的,我加了一個 beanstalk 作為 queue的 providor。還有一點改進是,文章中只提供了worker數量的設定,我再加了一個參數,設定每個worker可以並存執行的協程數。所以下面主要講講3, 2的解決辦法
Pipe
可以參考這個庫 pipe, 用法如下:
p := pipe.Line( pipe.ReadFile("test.png"), resize(300, 300), blur(0.5),)output, err := pipe.CombinedOutput(p)if err != nil { fmt.Printf("%v\n", err)}buf := bytes.NewBuffer(output)img, _ := imaging.Decode(buf)imaging.Save(img, "test_a.png")
還是比較方便的,建一個 Cmd struct, 利用正則匹配一下每個 Operation 的參數,放入一個 []Op slice, 最後執行,struct和方法如下:
type Cmd struct { cmd string saveas string ops []Op err error}type Op interface { getPipe() pipe.Pipe}type ResizeOp struct { width, height int}func (c ResizeOp) getPipe() pipe.Pipe { return resize(c.width, c.height)}//使用方法cmdStr := `file/test.png|thumbnail/x300|blur/20x8`cmd := Cmd{cmdStr, "test_b.png", nil, nil}cmd.parse()cmd.doOps()
sync.WaitGroup
單個cmd處理解決後,就是多個cmd的並行問題,沒啥好想的,直接用 sync.WaitGroup 就可以完美解決。一步一步來,我們先看看這個struct的使用方法:
func main() { cmds := []string{} for i := 0; i < 10000; i++ { cmds = append(cmds, fmt.Sprintf("cmd-%d", i)) } results := handleCmds(cmds) fmt.Println(len(results)) // 10000}func doCmd(cmd string) string { return fmt.Sprintf("cmd=%s", cmd)}func handleCmds(cmds []string) (results []string) { fmt.Println(len(cmds)) //10000 var count uint64 group := sync.WaitGroup{} lock := sync.Mutex{} for _, item := range cmds { // 計數加一 group.Add(1) go func(cmd string) { result := doCmd(cmd) atomic.AddUint64(&count, 1) lock.Lock() results = append(results, result) lock.Unlock() // 計數減一 group.Done() }(item) } // 阻塞 group.Wait() fmt.Printf("count=%d \n", count) // 10000 return}
group本質大概是一個計數器,計數 > 0時, group.Wait() 會阻塞,直到 計數 == 0. 這裡還有一點要注意,就是 results = append(results, result) 的操作是線程不安全的,清楚這裡 results 是共用的,需要加鎖來保證同步,否則最後 len(results) 不為 10000。
我們建一個BenchCmd, 來存放 cmds. 如下:
type BenchCmd struct { cmds []Cmd waitGroup sync.WaitGroup errs []error lock sync.Mutex}func (b *BenchCmd) doCmds() { for _, item := range b.cmds { b.waitGroup.Add(1) go func(cmd Cmd) { cmd.parse() err := cmd.doOps() b.lock.Lock() b.errs = append(b.errs, err) b.lock.Unlock() b.waitGroup.Done() }(item) } b.waitGroup.Wait()}
最後的調用就像這樣:
var cmds []Cmdcmd_a := Cmd{`file/test.png|thumbnail/x300|blur/20x8`, "test_a.png", nil, nil}cmd_b := Cmd{`file/test.png|thumbnail/500x1000|blur/20x108`, "test_b.png", nil, nil}cmd_c := Cmd{`file/test.png|thumbnail/300x300`, "test_c.png", nil, nil}cmds = append(cmds, cmd_a)cmds = append(cmds, cmd_b)cmds = append(cmds, cmd_c)bench := BenchCmd{ cmds: cmds, waitGroup: sync.WaitGroup{}, lock: sync.Mutex{},}bench.doCmds()fmt.Println(bench.errs)
這隻是一個初級的實驗,思考還不夠全面,並且只是模仿API,qiniu應該不是這麼做的,耦合更低,可能各個Cmd都有各自處理的叢集,那pipe這個庫就暫時沒法解決了,目前的局限在於 每個Cmd必須都在一個進程中。