Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[Mat Ryer] How I build APIs capable of gigantic scale in Go

[Mat Ryer] How I build APIs capable of gigantic scale in Go

Presentation from GDG DevFest Ukraine 2017 - the biggest community-driven Google tech conference in the CEE.

Learn more at: https://devfest.gdg.org.ua

Google Developers Group Lviv

October 14, 2017
Tweet

More Decks by Google Developers Group Lviv

Other Decks in Technology

Transcript

  1. MAT RYER > Go programmer since before v1 > Open-source

    contributor > https://Gopherize.me << cute and ready to scale > Author of Go Programming Blueprints: Second Edition > Blog at matryer.com Follow me on Twitter @matryer
  2. HOW I BUILD APIS CAPABLE OF GIGANTIC SCALE IN GO

    > I don't actually have to do that much > Go runs extremely quickly > If you build for horizontal scale, you can add nodes to meet demand > Platforms as a Service even do this for you
  3. WHAT IS GO? > Golang (written, never said) > Modern

    programming language, built for modern situations > Strongly typed > Deliberately cut down and simple language
  4. WHO CARES HOW MODERN IT IS? 1973: When C was

    designed, a computer had a single core 2007: When Go was designed, multi-core computers cooperated at infinite horizontal scale > The Go Standard library comes with world class networking tools > We got HTTP/2 early and for free (zero code changes required)
  5. WHAT GO DOESN'T HAVE > Classes > Inheritance > Overloading

    methods and operators > Exceptions > Magic This is also a list of reasons why you should use Go
  6. YOU CAN ALREADY READ GO... package greeter import "fmt" func

    Greet(name string) string { return fmt.Sprintf("Hello %s", name) } > Go is a C based language
  7. STRUCTURES package greeter import "fmt" type Greeter struct { Format

    string } func (g Greeter) Greet(name string) string { return fmt.Sprintf(g.Format, name) } > Encapsulate data
  8. INTERFACES package http type Handler interface { ServeHTTP(w http.ResponseWriter, r

    *http.Request) } > Implicit implementation (duck typing or structural typing)
  9. go fmt TOOL > All Go code is formatted the

    same way > No more arguments about tabs, braces placement, etc > Great for new people joining teams
  10. LEARN MORE ABOUT GO Check out the language tour at

    https://tour.golang.org > Or just ping me on Twitter @matryer
  11. SIMPLE TRICK Assume your code will run behind a load

    balancer. If a client makes three requests, assume each request will be handled by a different node. This will change the way you think about: > In-process memory > Communication between processes
  12. http.Handler IS YOUR FRIEND package http type Handler interface {

    ServeHTTP(w http.ResponseWriter, r *http.Request) } type HandlerFunc func(w http.ResponseWriter, r *http.Request) func (fn HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { fn(w, r) } > Real code from the standard library
  13. MAKE A Server STRUCT type Server struct { database *db.Conn

    logger Logger email MailSender router *mux.Router } > Holds shared dependencies > Add NewServer function for required dependencies func NewServer(db *db.Conn, logger Logger, email Mailsender) *Server
  14. PUT ALL ROUTES IN routes.go package myapp func (s *server)

    routes() { s.router.Handle("/endpoint1", s.handleEndpointOne()) s.router.Handle("/endpoint2", s.handleEndpointTwo()) s.router.Handle("/endpoint3", s.handleEndpointThree()) } > One consistent place to look for routing
  15. HELPERS FOR RESPONDING func (s *Server) Respond(w http.ResponseWriter, r *http.Request,

    data interface{}, status int) Initially it can just handle JSON, but later you can change this without touching handler code.
  16. HELPERS FOR DECODING func (s *Server) Decode(r *http.Request, v interface{})

    error Initially it can just handle JSON, but later you can change this without touching handler code: return json.NewDecoder(r.Body).Decode(v)
  17. HANDLER FUNCTIONS AS METHODS ON Server func (s *Server) handleSomething(w

    http.ResponseWriter, r *http.Request) { data, err := s.database.Load() if err != nil { } }
  18. METHODS THAT RETURN HANDLERS CAN DO PER-HANDLER SETUP func (s

    *Server) handleSomething() http.Handler { tpl, err := template.ParseFiles("templates/layout.html") if err != nil { return errorHandler{err: err, status: http.StatusInternalServerError} } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := tpl.Execute(w, nil); err != nil { s.RespondErr(w, r, err, http.StatusInternalServerError) } }) } > I always do this, even if there's no setup
  19. THEY CAN ALSO TAKE IN PER-HANDLER DEPENDENCIES func (s *Server)

    handleTemplate(name string) http.Handler { tpl, err := template.ParseFiles("templates/layout.html", name) /// ... more code please ...
  20. I ❤ TDD func Test(t *testing.T) { is := is.New(t)

    mockEmailSender := &MockEmailSender{} srv := NewServer(nil, nil, mockEmailSender) req, err := http.NewRequest("GET", "/path", nil) is.NoErr(err) // http.NewRequest w := httptest.NewRecorder() srv.ServeHTTP(w, req) is.Equal(w.Code, http.StatusOK) is.Equal(w.Body.String(), "something") }
  21. TESTING > Put test code in *_test.go files > testing

    package > net/http/httptest package > github.com/matryer/is << testing mini-framework
  22. ERROR HANDLER type errorHandler struct { err error status int

    } func (e errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, e.err.Error(), e.status) }
  23. Server SHOULD IMPLEMENT http.Handler func (s *Server) ServeHTTP(w http.ResponseWriter, r

    *http.Request) { s.Router.ServeHTTP(w, r) } > It can then be used as the handler for all requests > Routing will happen inside it
  24. ROLL YOUR OWN Handler TYPE type Handler interface { ServeHTTP(ctx

    context.Context, w http.ResponseWriter, r *http.Request) error } > pro: Solve common problems once, and make your life easier > con: It makes it more difficult to use interchangably with other things written to support http.Handler
  25. ONE FUNCTION TO CONVERT BETWEEN HANDLERS func (s *Server) toHTTPHandler(h

    Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := contextFromSomewhere(r) err := h.ServeHTTP(ctx, w, r) if err != nil { // common error handling } }) }
  26. ...AND YOU CAN MAKE YOUR FUNC WRAPPER TOO type HandlerFunc

    func(ctx context.Context, w http.ResponseWriter, r *http.Request) error func (fn HandlerFunc) ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return fn(ctx, w, r) }
  27. SIMPLE MIDDLEWARE func something(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter,

    r *http.Request) { // Do stuff before h.ServeHTTP(w, r) // Do stuff after }) } http.Handle("/path", something(myHandler())) > It's handlers, all the way down > Use defer
  28. SIMPLE MIDDLEWARE WITH SETUP func something(h http.Handler) http.Handler { thing

    := expensiveThingToDo() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { thing.Do() h.ServeHTTP(w, r) }) } http.Handle("/path", something(myHandler()))
  29. GO TOOL TO HOST YOUR SERVER package main func main()

    { if err := http.ListenAndServe(":8080", NewServer()); err != nil { log.Fatalln(err) } } > Package main > Use http.ListenAndServe > Start simple like this, and improve it later
  30. COMPLETE APP ENGINE CODE package myapp func init() { http.Handle("/",

    NewServer()) } > Not package main, it is not really a Go tool > Use init function to bind to the catch-all endpoint > The router inside your Server will handle the rest
  31. GOOGLE APP ENGINE (STANDARD ENVIRONMENT) Provides hosting, data/file storage, task

    queues, cron jobs, memcache, monitoring, logging, debugging How it works: > Write your service (following TDD, of course) > Add app.yaml configuration file > Deploy it with: gcloud app deploy app.yaml
  32. GO DELIVERS STATIC BINARIES THAT ARE VERY QUICK > A

    new HTTP request to your service will spin up a new instance > In other languages, this introduces a noticable delay > With Go, you have to check the logs to see if a new instance was started or not
  33. APP.YAML WITH STATIC FOLDER runtime: go service: default api_version: go1

    handlers: - url: /static static_dir: static - url: /.* script: _go_app
  34. GOOGLE CLOUD DATASTORE Google Cloud Datastore is a NoSQL document

    database built for automatic scaling, high performance, and ease of application development. > Extremely fast > Has limitations, for good reasons like physics and computers
  35. REPRESENT DATA AS A STRUCT const KindConference = "Conference" type

    Conference struct { Key *datastore.Key `json:"id" datastore:"-"` Name string `json:"name" datastore:",noindex"` Year int `json:"year"` Location string `json:"location"` Topics []string `json:"topics"` }
  36. KEYS > Every entity needs a key > Getting and

    putting by key is strongly consistent > Querying is eventually consistent Three options: > Let Google Cloud Datastore generate keys for you > Use an int or a string
  37. DON'T STORE THE KEY IN THE ENTITY BODY > Although

    our Conference struct had a Key field, we are asking Datastore to ignore it type Conference struct { Key *datastore.Key `json:"id" datastore:"-"` ...
  38. PUTTING STUFF ukraineDevFest := Conference{ Name: "GDG DevFest Ukraine 2017",

    Year: 2017, Location: "Lviv", Topics: []string{"go", "js", "machine learning"}, } key := datastore.NewIncompleteKey(ctx, KindConference, nil) var err error if key, err = datastore.Put(ctx, key, &ukraineDevFest); err != nil { return errors.Wrap(err, "put Conference") } ukraineDevFest.Key = key
  39. GETTING STUFF key, err := datastore.DecodeKey(conferenceID) if err != nil

    { return err } var conf Conference if err := datastore.Get(ctx, key, &conf); err != nil { if err == datastore.ErrNoSuchEntity { http.NotFound(w, r) return } return err } conf.Key = key
  40. QUERYING var conferences []Conference iter := datastore.NewQuery(KindConference). Filter("Year =", year).

    Order("Name").Run(ctx) for { var conf Conference key, err := iter.Next(&conf) if err == datastore.Done { break } if err != nil { return err } conf.Key = key conferences = append(conferences, conf) }
  41. OH THAT'S... WEIRD > Datastore: Need an index for all

    kinds of queries you expect to do > urlfetch package gives you an http.Client that you have to use > Request time is limited (you've got to be quick)
  42. NEED SOME HELP? > Training, advice, code reviews, etc. >

    Check out https://standardlibrary.io/ > Tweet me @matryer