原始slide在這裡。原文的範例缺了一些東西以致於compile起來會錯誤,我把缺的東西順手補上去,以及補上一些我這邊的心得跟額外的測試方法。
範例 : Google Search
- Google Search是幹嘛的
- 給一個query string,回傳搜尋結果(也許還加些廣告)
- 我們要怎麼得到搜尋結果
- 分別送query string到Web/Image/Youtube/Map/News…等搜尋引擎,然後把結果合併起來
我們要怎麼implement這個系統?
先來Mockup這個系統的樣子吧
我們用fakeSearch
來模擬一個Server,這個server可能會很快即時的反應,也可能最多會在100ms以後才反應。
var (
Web = fakeSearch("web")
Image = fakeSearch("image")
Video = fakeSearch("video")
)
type Search func(query string) Result
type Result string
//第一個版本的google search, 最直觀
func Google(query string) (results []Result) {
results = append(results, Web(query))
results = append(results, Image(query))
results = append(results, Video(query))
return
}
func fakeSearch(kind string) Search {
return func(query string) Result {
//模擬Server延遲
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
return Result(fmt.Sprintf("%s result for %q\n", kind, query))
}
}
測試一下這個mock能不能work
func main() {
rand.Seed(time.Now().UnixNano())
start := time.Now()
results := Google("golang")
elapsed := time.Since(start)
fmt.Println(results)
fmt.Println(elapsed)
}
Google Search 1.0
1.0就是我們放在mockup裡面的那個版本,google會去各種來源搜尋結果,然後把它給合併起來傳回去。
func Google(query string) (results []Result) {
results = append(results, Web(query))
results = append(results, Image(query))
results = append(results, Video(query))
return
}
Google Search 2.0
1.0顯然有些問題,他的query都是要等上一個query完成以後才能啟動,所以整個程序會被阻塞好一陣子。這個實作不需要鎖,沒有conditional variable,也不需要callbacks
func Google(query string) (results []Result) {
c := make(chan Result)
go func() { c <- Web(query) } ()
go func() { c <- Image(query) } ()
go func() { c <- Video(query) } ()
for i := 0; i < 3; i++ {
result := <-c
results = append(results, result)
}
return
}
Google Search 2.1
那要是我們設定一個timeout呢?
c := make(chan Result)
go func() { c <- Web(query) } ()
go func() { c <- Image(query) } ()
go func() { c <- Video(query) } ()
timeout := time.After(80 * time.Millisecond)
for i := 0; i < 3; i++ {
select {
case result := <-c:
results = append(results, result)
case <-timeout:
fmt.Println("timed out")
return
}
}
return
但是顯然的,drop掉太慢的search其實不太好。假設Server回應時間就是0-100之間,而太慢的response就是會被drop掉,那有沒有什麼方法能拿到完整的result呢?
Google Search 3.0
解決drop過慢result的問題
要解決2.1的問題其實也不難,我們對同一個服務(比方說Video
)做出多個request,取其中最快的一個就可以了 — 這也是google目前的做法,所以大家知道為什麼搜尋那麼耗電了吧….
func First(query string, replicas ...Search) Result {
c := make(chan Result)
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
func main() {
rand.Seed(time.Now().UnixNano())
start := time.Now()
result := First("golang",
fakeSearch("replica 1"),
fakeSearch("replica 2"))
elapsed := time.Since(start)
fmt.Println(result)
fmt.Println(elapsed)
}
所以3.0實作大概會長這樣
記得要先在上方額外宣告Web2/Image2/Video2
var (
Web = fakeSearch("web")
Image = fakeSearch("image")
Video = fakeSearch("video")
Web2 = fakeSearch("web")
Image2 = fakeSearch("image")
Video2 = fakeSearch("video")
)
然後3.0會是長這樣
c := make(chan Result)
go func() { c <- First(query, Web, Web2) } ()
go func() { c <- First(query, Image, Image2) } ()
go func() { c <- First(query, Video, Video2) } ()
timeout := time.After(80 * time.Millisecond)
for i := 0; i < 3; i++ {
select {
case result := <-c:
results = append(results, result)
case <-timeout:
fmt.Println("timed out")
return
}
}
return
3.0還有什麼問題呢?
這個留作Bonus給大家參考一下。3.0有一個相當明顯的bug就是,當First後面的所有Search都大於80的時候,會導致掉結果。比方說,c <- First(query, Image, Image2)
當後面兩個回應速度都比80慢(設定上他們速度是0-100),該結果就不見了,你會看到印出timed out。
所以我們還可以研究看看 :
- 如何避免掉資料?
- 如何在掉資料的時候動態把容忍值拉高?好處跟壞處分別是?
- 或者換個方法,如何動態增加replica?好處跟壞處分別是?在現實生活中,動態增加replica的話,應該要注意什麼條件?
- 事實上網路連線速度並非fakeSearch這樣的穩定在兩個數值中間徘徊,有沒有更好的模型可以模擬一段好一段壞的不穩定網路?甚至package drop?
如果要測試這個極端狀態的話,我們可以把timeout := time.After(80 *time.Millisecond)
的80改成一個更小的數字,比方說40。或者說,我們可以把fakeSearch的response time動態拉到一個更大的範圍,就可以很容易重現出這個掉資料的狀態。
來比較一下1.0 2.0 3.0的效能
demo code如下,結果大約會像是這樣
Function : main.Google1 Total time : 1639 Avg time : 163 Function : main.Google2 Total time : 712 Avg time : 71 Function : main.Google3 Total time : 550 Avg time : 55
package main
import (
"fmt"
"math/rand"
"reflect"
"runtime"
"time"
)
var (
Web = fakeSearch("web")
Image = fakeSearch("image")
Video = fakeSearch("video")
Web2 = fakeSearch("web")
Image2 = fakeSearch("image")
Video2 = fakeSearch("video")
)
type Search func(query string) Result
type Google func(query string) []Result
type Result string
func fakeSearch(kind string) Search {
return func(query string) Result {
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
return Result(fmt.Sprintf("%s result for %q\n", kind, query))
}
}
func Google1(query string) (results []Result) {
results = append(results, Web(query))
results = append(results, Image(query))
results = append(results, Video(query))
return
}
func Google2(query string) (results []Result) {
c := make(chan Result)
go func() { c <- Web(query) } ()
go func() { c <- Image(query) } ()
go func() { c <- Video(query) } ()
for i := 0; i < 3; i++ {
result := <-c
results = append(results, result)
}
return
}
func Google21(query string) (results []Result) {
c := make(chan Result)
go func() { c <- Web(query) } ()
go func() { c <- Image(query) } ()
go func() { c <- Video(query) } ()
timeout := time.After(80 * time.Millisecond)
for i := 0; i < 3; i++ {
select {
case result := <-c:
results = append(results, result)
case <-timeout:
fmt.Println("timed out")
return
}
}
return
}
func First(query string, replicas ...Search) Result {
c := make(chan Result)
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
func Google3(query string) (results []Result) {
c := make(chan Result)
go func() { c <- First(query, Web, Web2) } ()
go func() { c <- First(query, Image, Image2) } ()
go func() { c <- First(query, Video, Video2) } ()
timeout := time.After(80 * time.Millisecond)
for i := 0; i < 3; i++ {
select {
case result := <-c:
results = append(results, result)
case <-timeout:
fmt.Println("timed out")
return
}
}
return
}
func Benchmark(google Google) (output []Result, start time.Time, elapsed time.Duration) {
start = time.Now()
output = google("golang")
elapsed = time.Since(start)
return
}
func GetFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
func main() {
test := []Google{Google1, Google2, Google3}
const loops = 10
for _, google := range test {
var totalTime int64
for round := 0; round < loops; round++ {
rand.Seed(time.Now().UnixNano())
//fmt.Printf("start\t : %d -> %s\n", round, GetFunctionName(google))
_, _, elapsed := Benchmark(google)
totalTime += elapsed.Milliseconds()
//fmt.Printf("end\t : %d -> %s(%s)\n", round, GetFunctionName(google), elapsed)
}
fmt.Printf("Function : %s\tTotal time : %d\tAvg time : %d\n", GetFunctionName(google), totalTime, totalTime / 10)
}
}