こんにちは。配信・インフラチームの川住です。
先日の記事にもありますが、最近、弊社DSP『Bypass』のRTB入札サーバはGo言語で実装されたものに完全にリプレイスされました。以前の入札サーバはLuaとC言語で実装されていましたが、規模の拡大に伴ってより大量のリクエストを高速に捌く必要が出てきたため、弊社SSP『AdStir』での開発・運用実績があり、Luaより処理が高速で、かつ比較的容易にHTTPサーバを実装できるGo言語へのリプレイスに至りました。
今回は、アドサーバの実装にGo言語を用いるメリットをいくつか紹介します。
リクエスト処理時のリソース消費がNginx+Luaの構成に比べて少ない
従来のLua実装では、ngx_luaモジュールを使用しており、Nginxの各プロセス内でLuaのプログラムを実行する形を取っていました。したがって、比較的大きなサイズのプロセスがforkされてしまうため、メモリ消費量が非常に多くなっていました。それに対して、Go言語を用いたサーバでは、プロセスをforkすることなくリクエストを並行して処理できるため、メモリ消費量がLua実装に比べて非常に少なく済んでいます。
強力なキャッシュモジュールの存在
RTBの入札サーバでは、大量のリクエストを高速に捌く必要があります。そのため、memcachedなどのKVSとの通信にかかるコスト (通信回数, データサイズ etc.) も考慮しなければなりません。Go言語には『go-cache』という強力なインメモリキャッシュのライブラリがあり、こちらを使用することで、KVSから取得したデータをプロセス内に保持でき、KVSとの通信コストを減らせます。Go言語のサーバ自体は1プロセスで動作しているため、キャッシュデータの共有も比較的容易に行えます。
以下にgo-cacheを用いたデータの取得と格納のコードを掲載します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package main import ( "fmt" "github.com/patrickmn/go-cache" "time" ) // [output] // key = value // not found func main() { go_cache := cache.New(time.Duration(5)*time.Minute, 0) go_cache.Set("key", "value", time.Duration(5)*time.Second) if value, exists := go_cache.Get("key"); exists { fmt.Printf("%s = %s\n", "key", value) } else { fmt.Println("not found") } time.Sleep(time.Duration(10) * time.Second) if value, exists := go_cache.Get("key"); exists { fmt.Printf("%s = %s\n", "key", value) } else { fmt.Println("not found") } } |
上記のように、Get関数とSet関数を用いて簡単にデータの取得や格納を行えます。データの格納時にはTTLも設定できます。しかし、go-cacheではデータの取得や格納を行う際にMutexを用いた排他制御を行っています。そのため、RTBの入札サーバのようにデータの読み書きが頻繁な環境で使用すると、go-cacheへのアクセス自体がボトルネックとなるため性能が低下してしまいます。このような環境では、go-cacheのインスタンスを複数生成しておき、キーによってシャーディングするなどして同一資源へのアクセスを分散させる必要があります。以下にシャーディング処理の一例を掲載します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
package main import ( "fmt" "github.com/patrickmn/go-cache" "time" ) const ( NUM_INSTANCE = 5 ) type GoCacheClient struct { CacheInstance []*cache.Cache } func (cc *GoCacheClient) Get(key string) (interface{}, bool) { return cc._getInstance(key).Get(key) } func (cc *GoCacheClient) Set(key string, value string, ttl time.Duration) { cc._getInstance(key).Set(key, value, ttl) } func (cc *GoCacheClient) _getInstance(key string) *cache.Cache { // djb2 i, hash := 0, uint32(5381) for _, c := range key { hash = ((hash << 5) + hash) + uint32(c) } i = int(hash) % NUM_INSTANCE fmt.Printf("get_instance: %d\n", i) return cc.CacheInstance[i] } func NewGoCacheClient(ttl time.Duration) *GoCacheClient { ret := &GoCacheClient{ CacheInstance: make([]*cache.Cache, NUM_INSTANCE, NUM_INSTANCE), } for i := 0; i < NUM_INSTANCE; i++ { ret.CacheInstance[i] = cache.New(ttl, 0) } return ret } // [output] // get_instance: 3 // get_instance: 4 // get_instance: 3 // key11111 = value1 // get_instance: 4 // key22222 = value2 func main() { go_cache := NewGoCacheClient(time.Duration(5) * time.Minute) go_cache.Set("key11111", "value1", time.Duration(10)*time.Second) go_cache.Set("key22222", "value2", time.Duration(10)*time.Second) if value, exists := go_cache.Get("key11111"); exists { fmt.Printf("key11111 = %s\n", value) } else { fmt.Println("not found") } if value, exists := go_cache.Get("key22222"); exists { fmt.Printf("key22222 = %s\n", value) } else { fmt.Println("not found") } } |
まとめ
Go言語を用いることで、高速に動作するアドサーバを比較的容易に実装できます。ただし、Go言語のサーバでは1プロセスで処理を行うため、共有資源の排他制御がボトルネックとなる可能性があり、その点を考慮しつつ実装する必要があります。