前編の続きです。
前回のデザパタを、Google検索を例に適用しましょうという話。
Google 1.0
まずは並行処理が入っていない、そのまま逐次処理の例です。
ダミーの検索関数type Search func(query string) Result
を返す、fakeSearch関数を作っておきます。
ダミーの検索関数Search
は、time.Sleep
で一定時間スリープして、検索しているフリをします。
package fake
import (
"fmt"
"math/rand"
"time"
)
var (
Web = fakeSearch("web")
Image = fakeSearch("image")
Video = fakeSearch("video")
)
type Search func(query string) Result
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))
}
}
type Result string
メイン関数です。Google関数にて、先ほどのダミー検索関数を逐次処理で3回呼び出しています。
package main
import (
"runtime"
"time"
"math/rand"
"fmt"
"github.com/tfutada/robpike/google/fake"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
rand.Seed(time.Now().UnixNano())
start := time.Now()
results := Google("golang")
elapsed := time.Since(start)
fmt.Println(elapsed)
fmt.Println(results)
}
func Google(query string) (results []fake.Result) {
results = append(results, fake.Web(query))
results = append(results, fake.Image(query))
results = append(results, fake.Video(query))
return
}
実行結果
/usr/local/go/bin/go run /Users/taka/go/src/github.com/tfutada/robpike/google/google1/main.go
160.479531ms
[web result for "golang"
image result for "golang"
video result for "golang"
]
Process finished with exit code 0
Google 2.0
Goルーチンを使用して並行処理します。前編のFanInパターンを使用します。
ダミーの検索関数をそれぞれ無名関数でラップし、チャネル経由で結果を返します。
func Google(query string) (results []fake.Result) {
c := make(chan fake.Result)
go func() { c <- fake.Web(query) }()
go func() { c <- fake.Image(query) }()
go func() { c <- fake.Video(query) }()
for i := 0; i < 3; i++ {
results = append(results, <-c)
}
return
}
実行結果
前回より早くなりました。一番検索結果が遅いものが、全体の処理時間になります。
/usr/local/go/bin/go run /Users/taka/go/src/github.com/tfutada/robpike/google/google2/main.go
82.139627ms
[web result for "golang"
image result for "golang"
video result for "golang"
]
Process finished with exit code 0
Google 2.1
前回のtimeoutパターンで、ある一定時間経過したら、検索を打ち切る処理を入れます。
func Google(query string) (results []fake.Result) {
c := make(chan fake.Result)
go func() { c <- fake.Web(query) }()
go func() { c <- fake.Image(query) }()
go func() { c <- fake.Video(query) }()
timeout := time.After(80 * time.Millisecond)
for i := 0; i < 3; i++ {
select {
case s := <- c:
results = append(results, s)
case <- timeout:
fmt.Println("Timed out!")
return
}
}
return
}
実行結果
処理が80msで打ち切られ、image検索が実行されてないのがわかります。
/usr/local/go/bin/go run /Users/taka/go/src/github.com/tfutada/robpike/google/google3/main.go
Timed out!
80.690454ms
[web result for "golang"
video result for "golang"
]
Process finished with exit code 0
Google 3.0
次のチューニングは、検索処理の冗長化を行うものです。
同じ検索処理を並行して複数実行し、一番早く帰ってきたものを正として使用します。
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
rand.Seed(time.Now().UnixNano())
start := time.Now()
results := First("golang",
fake.FakeSearch("replica 1"),
fake.FakeSearch("replica 2"),
fake.FakeSearch("replica 3"),
fake.FakeSearch("replica 4"),
fake.FakeSearch("replica 5")) // 注) fakeSearch -> FakeSearch
elapsed := time.Since(start)
fmt.Println(elapsed)
fmt.Println(results)
}
func First(query string, replicas ...fake.Search) fake.Result {
c := make(chan fake.Result)
searchReplica := func(i int) {c <- replicas[i](query)}
for i := range replicas {
go searchReplica(i) // クロージャだとうまくいかない。。。
}
return <-c
}
// 最初のfakeSearchを大文字に変えて、メイン関数から見れるようにしました。
func FakeSearch(kind string) Search {
// 同じ
実行結果
/usr/local/go/bin/go run /Users/taka/go/src/github.com/tfutada/robpike/google/google3/main.go
2.422057ms
replica 2 result for "golang"
Process finished with exit code 0
これを踏まえて、先ほどのGoogle 2.1のタイムアウトとの合わせ技をして、時間のかかるレプリカは切り捨てることで、より効率的に検索できるわけです。