18
#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.

#golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

Embed Size (px)

Citation preview

Page 1: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

#golang @SkrzCzDevSkrz 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.

Page 2: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

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)

Page 3: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

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.

Page 4: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

😰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.

Page 5: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

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?

Page 6: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

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…

Page 7: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

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.

Page 8: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

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é.

Page 9: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

// 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”.

Page 10: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

// // 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.

Page 11: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

// // start RPC server // for i := 0; i < runtime.NumCPU(); i++ { channel, err := service.connection.Channel() if err != nil { log.Panicf("could not create channel: %s\n", err) }

requestDelivery, err := channel.Consume(...) if err != nil { log.Panicf("could not start consumer: %s\n", 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: %s\n", 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).

Page 12: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

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.

Page 13: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

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.

Page 14: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

#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.

Page 15: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

#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.

Page 16: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

#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.

Page 17: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

#GoInTheWild• Parse

http://blog.parse.com/learn/how-we-moved-our-api-from-ruby-to-go-and-saved-our-sanity/

• Docker https://www.docker.com/

• dl.google.com http://talks.golang.org/2013/oscon-dl.slide#1

• VividCortex (monitoring výkonu databází) https://www.vividcortex.com/

• CockroachDB (open-source DB ~ Google Spanner) https://github.com/cockroachdb

• Mattermost (open-source on-premise kopie Slack) https://github.com/mattermost/platform

• Bleve (full-text search) https://github.com/blevesearch/bleve

• Prometheus (monitoring, time-series databáze) http://prometheus.io/

• InfluxDB (time-series databáze)https://influxdb.com/

Zajímavé projekty, které používají Go.

Page 18: #golang @SkrzCzDev (Skrz DEV Cirkus 21.10.2015)

Díky! Otázky?

Chcete se o Go dozvědět víc? Přijďte na GoLang Meetup 12.11.2015 http://srazy.info/golang-meetup/5676