Go se @SkrzCz používá pro výkonově nejnáročnější části aplikaci. Jedna z nich je servírování bannerů a výběr těch správných reklam do nich. 1000 req/s v peaku a "adbandit" má minimální nároky na server. Podívejte se, jak použít Go ve spolupráci s ReactPHP a RabbitMQ.
1 of 18
Download to read offline
More Related Content
#golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)
1. #golang @SkrzCzDev
Skrz DEV Cirkus
21.10.2015
V článku na Zdrojáku “Jak Skrz.cz řadí 20K nabídek podle real-time
analytiky” (https://www.zdrojak.cz/clanky/jak-skrz-cz-radi-20k-nabidek-podle-
real-time-analytiky/) jsem psal, že řazení probíhá v microservice napsané v Go.
Před ranking service ale vzniknul “adbandit”, service, která se stará o
automatickou optimalizaci nabídek v bannerech.
2. Skrz používá bannery od Seznamu pro promo nabídek (např. na Novinky.cz,
Proženy.cz). Nejdříve se dělal XML export položek, které se zobrazily v bannerech.
Problém však byl, že člověk viděl X krát jednu a tu samou položku a nedokázali jsme
položku, na kterou lidé neklikají, rychle stáhnout (XML si Seznam stahoval každých 10
minut).
Dospěli jsme tedy k řešení přes IFRAME a servírování banneru přímo z našich serverů.
Nejdříve to byl statický soubor, který jeden skript několikrát do minuty přegeneroval.
Ale chtěli jsme ještě dynamičtěji reagovat, a tak vzniknul “adserver” - server v
ReactPHP.
V průměru adserver obsluhuje 100-200 req/s, v peaku 500-1000 req/s a to s latencí
~50ms a velmi malými nároky na server (zabere třeba 4 jádra, 2GB RAM)
3. machine learning
statistika
http://www.mlguru.cz/bayesovsky-bandita-chytrejsi-a-levnejsi-ab-testovani/
Problém, který jsme řešili byl následující: Máme X ploch, Y pozic na plochu a Z reklam
(nabídek), které se mohou na pozicích zobrazovat. Chceme maximalizovat počet prokliků.
Nejdříve jsme reklamy vybíraly náhodně. Fungovalo to, ale ROI nebyla taková, jakou bychom
chtěli. Samozřejmě možnost je řadit podle CTR (click-through rate - poměr prokliků vůči počtu
impresí). Problém je ale např. s novými reklamami - nemají prokliky ani imprese, nelze určit
CTR. Jak moc by se měly takové reklamy zobrazovat? Kolik jim dát prostoru?
Narazil jsem na článek od Jirky Materny “Bayesovský bandita: chytřejší a levnější A/B
testování”. Neříkal bych tomu úplně “machine learning”, je to spíše chytřejší použití statistiky.
Nicméně výsledkem je online učící se algoritmus.
4. 😰
Pro pochopení, co je Beta rozdělení a
proč dobře modeluje problém, který
jsme řešili, doporučji projít odkazovaný
článek.
5. PHP?
Pak ale přišlo v čem to napsat. Potřebujeme v každém požadavku najít pro každou
reklamu hodnotu náhodné veličiny odpovídající jejímu Beta rozdělení. V peaku 1000
req/s, maximálně řekněme 100 reklam, tzn. potřebujeme Beta rozdělení spočíst 100-
tisickrát každou sekundu. Kompilovaná implementace Beta rozdělení to dokáže na
single-core milionkrát za sekundu, takže to jde.
Pro PHP existuje PECL balíček stats. Ale ten má poslední aktualizaci 2012 a nejsou
pro něj Debianí balíčky. Tedy pro deployment na produkci je to “no-no”. PHP
implementace Beta rozdělení, pokud bude jen 100 krát pomalejší, nebude stíhat.
Jaké jsou tedy další možnosti?
6. C? 💀 Java? 💩
Céčko by to mohlo stíhat. Jenže v něm nikdo neumí,
balíčkovací systém, kompilace, deloyment - hodně
složité.
Java je jednoduchá. Hodně lidí v ní umí, ale nikomu se
v tom dělat nechce :) A opět je tu kompilace a
deployment…
7. Go! 👍
roku 2009 jsem napsal 1. český tutorial 😛
http://programujte.com/clanek/2009112200-go-novy-
programovaci-jazyk-od-google/
(většina problémů, co se tam píše, už naštěstí neplatí 😊)
Go jsme znal, hodně jednoduchý jazyk, kdokoli se do něj
dokáže dostat během chvále, hodí se na psaní serverů.
Deployment je prostě nahrání jedné statické binárky na
server.
Má knihovny na komunikaci po síti a našel jsem
knihovnu na počítání mimo jiného Beta rozdělení (https://
code.google.com/p/gostat/). Takže Go!
Projdu na následujících reálné ukázky kódu adbandity,
jak vypadá Go.
8. func NewServingService(...) *ServingService {
service := &ServingService{
cfg: cfg,
dbmap: dbmap,
connection: connection,
doneChan: make(chan interface{}),
// ...
}
service.Start()
return service
}
Adbandit běží vedle Adserveru a komunikují spolu po síti.
Nejlepší by bylo, kdyby se Adserver přepsal celý do Go, ale
obsahoval už hodně logiky kolem výběru nabídek, byly v něm
připravené šablony pro bannery apod.
Adbandit se tedy skládá z několika service. `ServiceService`
se stará o vyřizování requestů na bannery. Adserver přijme
request, rozhodne, jestli má jít na Adbandit, a pokud ano,
přepošle na Adbandit, ten vrátí v JSONu položky, které má
Adserver zobrazit. Ten je hodí do šablony, dotáhne další
potřebné data a vyrenderuje.
Takhle se dělají v Go “konstruktory” - vytvoří se funkce
(uvozená “func”) s prefixem “New”. Metoda “Start” spustí v
service vše potřebné.
9. // Config - struct defining bandit config file
type Config struct {
Database Database
Rabbitmq Rabbitmq
HTTP HTTP
MutationsFile string `yaml:"mutations_file"`
// ...
}
// Database - SQL database configuration
type Database struct {
Driver string
Host string
Port int
User string
Password string
Database string
}
// Rabbitmq - RabbitMQ configuration
type Rabbitmq struct {
Host string
Port int
User string
Password string
Vhost string
RPCQueue string `yaml:"rpc_queue"`
}
// HTTP configuration
type HTTP struct {
Host string
Port int
HitDomain string `yaml:"hit_domain"`
}
Proměnná “cfg” na předchozím slajdu byla struktura typu
“Config”. Všimněte si, že Go nejdříve píše název field
struktury (proměnné) a poté typ (narozdíl např. od C/Java).
Taky zde není nic jako `public`/`private`. Go to rozlišuje
podle toho, jestli je první písmeno velké (public), nebo
malé (private).
Za typem field jde uvést ještě tzv. “struct tag”. Používají ho
různé serializační knihovny, např. zde YAML.
Hezké zarovnání za vás vyřeší “go fmt”. Obecně se
nemusíte v týmu hádat o coding style, všechny Go
zdrojáky mají jeden - jak vám to naformátuje “go fmt”.
10. //
// start HTTP server
//
service.server = &http.Server{
Addr: fmt.Sprintf("%s:%d",
service.cfg.HTTP.Host,
service.cfg.HTTP.Port,
),
Handler: http.HandlerFunc(service.handleHTTPRequest),
}
go func() {
if err := service.server.ListenAndServe(); err != nil {
panic(err)
}
}() Ve standardní knihovně je implementace HTTP serveru -
balíček `net/http`.
Před jakékoli volání funkce jde napsat `go`. To vytvoří tzv
“goroutine” - kus kódu, který se začne vykonávat
konkurenčně se současným. Tady v goroutine spustíme na
HTTP serveru metodu `ListenAndServe` - ta začne
poslouchat na socketu a vyřizovat requesty daným
handlerem.
11. //
// start RPC server
//
for i := 0; i < runtime.NumCPU(); i++ {
channel, err := service.connection.Channel()
if err != nil {
log.Panicf("could not create channel: %sn", err)
}
requestDelivery, err := channel.Consume(...)
if err != nil {
log.Panicf("could not start consumer: %sn", err)
}
go func(channel *amqp.Channel, requestDelivery <-chan amqp.Delivery, i int) {
for {
select {
case delivery := <-requestDelivery:
service.handleRequestDelivery(channel, delivery)
case <-service.doneChan:
if err := channel.Close(); err != nil {
log.Panicf("could not close channel: %sn", err)
}
break
}
}
}(channel, requestDelivery, i)
}
HTTP bohužel nestíhalo vyřizovat requesty, hlavně na
straně Adserveru - procesy umíraly na moc otevřených file
descriptorů.
Rozhodl jsem se zkusit nejjednodušší řešení - místo HTTP
jako transportního prokotolu jsem použil RPC přes
RabbitMQ (http://www.rabbitmq.com/tutorials/tutorial-six-
go.html). Na straně PHP přes async knihovnu BunnyPHP
(https://github.com/jakubkulhan/bunny).
12. func (service *ServingService) handleRequestDelivery(
channel *amqp.Channel,
delivery amqp.Delivery
) {
req := mq.ParseAdbanditRequest(delivery.Body)
buf := bytes.NewBuffer(nil)
service.handleAdbanditRequest(
buf,
int(req.BannerID),
req.Count,
req.UID,
false,
)
channel.Publish("", delivery.ReplyTo, false, false, amqp.Publishing{
CorrelationId: delivery.CorrelationId,
ContentType: "application/json",
Body: buf.Bytes(),
})
delivery.Ack(false)
} Ukázka, jak vypadá RPC přes RabbitMQ. `delivery.Body`
je request v JSON formátu. `mq.ParseAdbanditRequest`
pomocí standardní knihovny a “struct tagů” vyparsuje
data requestu.
13. txDeliveryChan, err := service.channel.Consume(...)
if err != nil {
panic(err)
}
service.txDeliveryChan = txDeliveryChan
go func() {
for {
select {
case delivery := <-service.actionDeliveryChan:
service.handleActionDelivery(delivery)
case delivery := <-service.txDeliveryChan:
service.handleTxDelivery(delivery)
case <-service.ticker.C:
service.tick()
case <-service.doneChan:
log.Print("ServingService done")
break
}
}
}()
Aby se mohly updatovat alfa a beta parametry Beta rozdělení,
Adbandit čte z RabbitMQ ještě všechna data o akcích uživatelů -
tzn. klicích na reklamy, impresím na reklamy.
Go poskytuje krásný “select” statement, který blokuje, dokud
nepřijde nějaká zpráva a podle toho začne vykonávat danou
větech.
14. #AwesomeGo
• výkon!
• built-in concurrency přímo v jazyku
• žádný callback/promise-hell jako v JS
• skvělá standardní knihovna, dobré 3rd party
• RabbitMQ knihovna:https://github.com/streadway/amqp
• database/sql: http://go-database-sql.org/
• mysql driver: https://github.com/go-sql-driver/mysql
• YAML: http://gopkg.in/yaml.v2
• CLI: https://github.com/codegangsta/cli
• go fmt; go test; a vůbec go tool
• rychlá kompilace, jednoduchá cross-kompilace
• env GOOS=linux GOARCH=amd64 go build .
Go se osvědčilo. Výkon o moc lepší být už
nemůže (je to zkompilované do binárkky),
poskytuje skvělou standardní knihovnu,
dobré 3rd-party knihovny a super tooling,
např.:
a) `go fmt` pro formátování kódu - neřešíte
v týmu coding style
b) `go test` pro unit testy a benchmarky,
nemusíte instalovat zvlášť nějaký
`goUnit`.
c) `go` příkaz umí i další věci (např. detekci
race conditions), zatím jsem však
nepotřeboval.
15. #NotSoAwesomeGo
• IDE?
• Atom? plugin do IntelliJ IDEA / PhpStorm?
• $GOPATH
• package manager / vendoring
• http://getgb.io/
• https://github.com/Masterminds/glide
• řešení $GO15VENDOREXPERIMENT?
• DI container
• https://github.com/facebookgo/inject
• https://github.com/karlkfi/inject
• https://github.com/peter-edge/go-inject
Věc, co mě nejvíc štve, je `$GOPATH` - všechno
se instaluje globálně, nelze pořádně řešit verze
závislostí. Existují různé 3rd-party package
managery pro Go. V Go 1.5 přibyla podpora pro
`vendor`, taková lokální `$GOPATH`, něco jako
`node_modules`. Package managery (např. Glide)
to již začaly využívat. Doufám, že co nejdříve
přidají to, co dělá Glide přímo do `go` toolu.
Taky mi chybí pořádný DI container. Z vypsaných
knihoven mi ani jedna napřijde, že by to řešila
dobře.
16. #GoNext
• Concurrency patterns:
https://talks.golang.org/2012/concurrency.slide#1
• Pipelines and cancellation:
https://blog.golang.org/pipelines
• Webové aplikace s net/http:
https://golang.org/doc/articles/wiki/
• gRPC:
http://www.grpc.io/
• Seznam dobrých knihoven:
http://awesome-go.com/
Odkazy, které doporučuji projít.
gRPC je RPC framework od Google.
Pokud bych měl znovu řešit propojení
Adserver/Adbandit, sáhl bych právě pro
gRPC. Ve Skrzu je použito právě pro
ranking service.