閒聊
這個打水印的 demo
其實已經完成許久,一直沒有總結總結,有空填了一下自己的坑吧,也讓自己複習複習。
背景
公司是做圖形設計資源站點,詳情、搜尋頁面都需要提供預覽圖片,圖片都是包含公司的水印的圖片,水印圖片單獨儲存。現在公司需要更換水印圖,所以要獲取全部的原圖,打上新水印,再替換現有的圖片。
方案
公司主要是 PHP
開發, 本來是打算用 Laravel
的 command
指令碼完成的,但想到當時團隊也是有意向轉 Go
方向並且Go
在有協程的加持,可以開多個協程來執行打水印這部分工作,所以最終採用了 Go
語言來完成這個需求,算是一個的新的嘗試。
實現
整體流程是:遍歷資料庫資料->獲取原圖資訊->下載原圖->生成水印->上傳水印圖->更新資料庫水印圖片,本文主要敘述生成水印此步驟,因為這一塊與 PHP
的實現上略有不同,在 Go
上可使用協程併發處理,而 PHP
只能單個處理。
Go實現
go version go1.17.7 darwin/arm64
同步
同步執行流程與 PHP
一樣,對比一下兩者效率
main.go
// 同步
func syncGen() {
startT := time.Now()
// 開啟水印圖
water, err := pkg.OpenPngImage("./test_water.png")
if err != nil {
log.Fatal(err)
}
// 這個迴圈是遍歷原圖
imgPath := "./test.png"
for i := 1; i <= num; i++ {
id := i + 1000 // 模擬 id
if err = pkg.Generate(imgPath, id, water); err != nil {
log.Println("水印圖生成失敗,id=" + strconv.Itoa(i))
}
}
// 計算耗時
tc := time.Since(startT)
fmt.Printf("同步執行時間 = %v\n", tc)
}
go run main.go -n 1
水印圖:1 張
同步執行時間 = 109.644375ms
1632*874 JPEG(24位顏色) 78.87kb
PHP實現
PHP 7.4.28 (cli)
臨時寫了個 PHP
版本的同功能demo
$t1 = microtime(true);
$dst = '/home/jiumu/code/go-image-water-case/test1.png';
$src = '/home/jiumu/code/go-image-water-case/test_water1.png';
$font = '/home/jiumu/code/go-image-water-case/font.ttf';
$t3 = microtime(true);
$srcIm = imagecreatefrompng($src);
$t4 = microtime(true);
$num = 1;
for ($i = 0; $i < $num; $i++) {
$t5 = microtime(true);
$dstIm = imagecreatefrompng($dst);
$t6 = microtime(true);
$dstSize = getimagesize($dst);
$srcSize = getimagesize($src);
$new = imagecreatetruecolor($dstSize[0], $dstSize[1]);
imagecopy($new, $dstIm, 0, 0, 0, 0, $dstSize[0], $dstSize[1]);
imagecopy($new, $srcIm, $dstSize[0] - 200, $dstSize[1] - 220, 0, 0, $srcSize[0], $srcSize[1]);
$rgb = imagecolorallocate($dstIm, 21, 33, 57);//字型顏色
$id = $i + 1000;
imagefttext($new, 30, 0, $dstSize[0] - 200, $dstSize[1] - 20, $rgb, $font, "ID: {$id}");
$t7 = microtime(true);
imagejpeg($new, __DIR__ . "/water/{$i}.jpeg");
$t8 = microtime(true);
imagedestroy($dstIm);
imagedestroy($new);
}
imagedestroy($srcIm);
$t2 = microtime(true);
echo '總耗時: ' . round($t2 - $t1, 5). 's' . PHP_EOL;
echo '總耗時: ' . round($t2 - $t1, 5) * 1000 . 'ms' . PHP_EOL;
結果
$ php index.php
總耗時: 0.05506s
總耗時: 55.06ms
1632*874 JPEG(24位顏色) 78.82kb
PHP 既然比Go快了近一倍?難道是我Go的程式碼寫的有問題?後來發現PHP生成的圖片質量只有Go的75%的質量,所以把Go這邊的質量降到了75%,效率依然沒有變化,在一籌莫展的時候,想到了 PProf
找一下效能到底卡在哪裡了。
生成 1 張水印圖 pprof 的部分截圖
找出耗時原因了,原來耗時的都卡在了 png.Decode()
和 jpeg.Encode()
這兩步了,再來看看 PHP
這兩步的時間
php index.php
總耗時: 55.06ms
水印decode耗時: 0.87ms
原圖decode耗時: 24.47ms
新圖encode耗時: 19.63ms
所以 Go
比 PHP
慢的原因就是出現在 decode()
和 encode()
這兩步了:
PHP
在decode
兩張圖片用時 25.34ms,encode 用時 19.63msGo
在decode
兩張圖片用時 60 ms,encode 用時 50ms
圖片大小和耗時成正比,測試原圖是 1.43 M,如果有知道原因的小夥伴可以分享分享
協程
雖然我 decode
和 encode
慢,但我有 goroutine
啊,來看看 Go
的表現
各生成100張水印圖:
php
總耗時: 5.34911s
總耗時: 5349.11ms
go
NumCPU:8
NumGoroutine: 102
$ go run main.go -n 100
水印圖:100 張
同步執行時間 = 9.583812958s
無併發數量控制時間 = 2.072805143s
協程狀態下比 PHP 快了 2.5 倍,同步情況下,還是比php慢2倍。
協程控制
由於啟動的 goroutine 協程不受控制,如果panic了則無法處理,成為野生攜程,導致程式掛掉。
// Go 避免 go func(){} 如果方法中丟擲 panic 無法被捕獲到
// 或者是每在每個 go 前面都 recover() 一次,造成的程式碼混亂不可維護
func Go(f func()) {
defer func() {
if err := recover(); err != nil {
// 記錄日誌
log.Println(err)
}
}()
go f()
}
開啟協程的方式則變成:
pkg.Go(func() {
// code
})
由於程式碼篇幅較長,所以程式碼沒有貼出來,有興趣的小夥伴可以到此檢視原始碼 github.com/zxr615/go-image-water-c...
Go 的 decode
/ encode
為何相對 php
慢這麼多,希望有知道的朋友可以交流交流。
總結
Go
提供的工具很方便的找出效能瓶頸,如上文的PProf
- 協程的優勢非常大,在自身函式較慢的情況下,可以充分利用協程發揮系統效能趕超。
參考
本作品採用《CC 協議》,轉載必須註明作者和本文連結