Patterns for scalable web services in Go
Richard Crowley
Betable operations
Richard Crowley
Betable operations
r@rcrowley.org or @rcrowley
2007 Wash U grad
Gambling-as-a-Service
Licensed and regulated so game developers don’t have to be
Of human resources
Of machine resources
Who has written at least one Go program?
Who has Go in production?
Who has only Go in production?
Brevity
Static type system
Compiles to x86, ARM, etc.
CSP
Thompson, Pike, Cox, Griesemer
Brad Fitz
It isn’t Java
It isn’t running on the JVM
(Guilty)
Your problem won’t tolerate a naive garbage collector
Library immaturity overburdens your team
Always google it as “golang”
package main import ( "fmt" "net/http" ) func main() { http.HandleFunc( "/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, www!") }, ) http.ListenAndServe(":8080", nil) }
func handler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) fmt.Fprintln(w, "Hello, www!") })
May be named or anonymous
All functions are closures
Zero or more typed parameters
Parameters are pass-by-value
Zero or more typed return values
func fail() error { return errors.New("fail") } func multifail() (*Win, error) { return nil, errors.New("multifail") }
Conventionally return an error
last
There are no exceptions because errors are not exceptional
Use error
to communicate what happened to the caller
type handler struct { counter metrics.Counter } type Authorization struct { Username, Password string }
Structs are types
Zero or more typed fields
func (h *handler) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) fmt.Fprintln(w, "Hello, www!") h.counter.Inc(1) }
Method receiver may be any type in the declaring package
// In the standard library's io/io.go: type Reader interface { Read(p []byte) (n int, err error) } // In the standard library's net/http/server.go: type Handler interface { ServeHTTP(ResponseWriter, *Request) }
Interfaces are also types
Zero or more method signatures
Types that implement all methods implement the interface implicitly
func main() { mux := http.NewServeMux() mux.Handle("/", &handler{}) server := &http.Server{Handler: mux} listener, err := net.Listen("tcp", ":8080") if nil != err { log.Fatalln(err) } if err := server.Serve(listener); nil != err { log.Fatalln(err) } }
http.ServeMux
is another http.Handler
Raise your hand if you’ve been burned by BaseHTTPServer
or WEBrick
What’s Serve
do, anyway?
(For one thing, power dl.google.com)
func main() { listener, err := net.Listen("tcp", ":1234") if nil != err { log.Fatalln(err) } for { conn, err := listener.Accept() if nil != err { log.Fatalln(err) } go handle(conn) } }
go
makes any function call asynchronous in a “goroutine”
func handle(conn net.Conn) { defer conn.Close() p := make([]byte, 4096) for { n, err := conn.Read(p) if nil != err { log.Println(conn.RemoteAddr(), err) break } log.Printf("%v p: %s", conn.RemoteAddr(), p[:n]) if _, err := conn.Write(p[:n]); nil != err { log.Println(conn.RemoteAddr(), err) break } } }
No callbacks, no miniature state machines
Many goroutines are scheduled onto GOMAXPROCS
operating system threads
Cheap enough to not worry about pooling
This is how the standard library HTTP server works, too
Tony Hoare’s Communicating Sequential Processes in CACM volume 21 issue 8
Go’s summary: “Do not communicate by sharing memory; instead, share memory by communicating.”
Useful for connection handling, critical sections, and pooled operations
Been there, done that
Hoare’s “Parallel Commands”
ch := make(chan int) // unbuffered ch := make(chan int, 1) // buffer length of 1 ch <- 47 // send <-ch // receive
Buffered (asynchronous + backpressure) or unbuffered (synchronous)
Concurrent send and receive operations
Faster than mutexes
Hoare’s “Input and Output Commands”
Sacrificing shared nothing in the small precludes diagonal scaling
Non-empty channel buffers are an opportunity for data loss
The least surprising opportunity to screw things up
http
package is annoyingly unhelpful
Not being addressed in Go 1.2 so we take matters into our own hands
func main() { mux := http.NewServeMux() mux.Handle("/", &handler{}) server := &http.Server{Handler: mux} listener, err := net.Listen("tcp", ":1234") if nil != err { log.Fatalln(err) } ch := make(chan os.Signal) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) go server.Serve(listener) log.Println(<-ch) listener.Close() time.Sleep(60e9) // FIXME! }
func main() { // mux ... server ... listener, ppid, err := goagain.GetEnvs() if nil != err { listener, _ = net.Listen("tcp", ":8080") go server.Serve(listener) } else { go server.Serve(listener) goagain.KillParent(ppid) } goagain.AwaitSignals(listener) listener.Close() time.Sleep(60e9) // FIXME! }
goagain
handles SIGUSR2
Difficult to supervise; systemd
> Upstart
Gorilla, Revel: more ambitious and all-knowing; appropriate for web applications
gorest
: hides a lot in struct tags
pat
: unopinionated but leaves most concerns unaddressed
Many others we can talk about over beers
Newly open-source!
Inspired by Dropwizard
How Betable builds web services in Go
github.com/rcrowley/go-tigertonic
mux := http.NewServeMux() mux.Handle("/", &handler{})
http.ServeMux
only routes prefixes
mux := tigertonic.NewTrieServeMux() mux.Handle("GET", "/foo/{bar}/baz", &handler{})
tigertonic.TrieServeMux
is method- and wildcard-aware
Responds 404 and 405 appropriately
var handler http.Handler = tigertonic.Marshaled(func( url.URL, http.Header, *MyRequest, ) (int, http.Header, *MyResponse, error) { return http.StatusOK, nil, &MyResponse{}, nil })
Static request and response types
No hassling with json.Encoder
Responds 400, 406, and 415 appropriately
Responds with JSON in case of error, too
Still an http.Handler
decoder := reflect.ValueOf(json.NewDecoder(r.Body)) out := decoder.MethodByName("Decode").Call([]reflect.Value{ reflect.New(m.v.Type().In(2).Elem()), })
User code deals only with static types
if !t.Out(2).Implements(reflect.TypeOf((*Response)(nil)).Elem()) { panic("type ... was %v, not Response", t.Out(2)) }
Runtime enforces fuzzy interfaces
handler = tigertonic.If( func(r *http.Request) (http.Header, error) { if "" == r.Header.Get("X-Condition") { return nil, Forbidden{errors.New("forbidden")} } return nil, nil }), handler, // from two slides ago )
If
naturally wraps any http.Handler
Special case of First
middleware chains
And yes, it’s an http.Handler
Logging via the standard library
Metric collection via go-metrics
http.Handler
all the way down
handler = tigertonic.Logged(handler, nil)
tigertonic.Logged
for full (optionally redacted) request and response logs
handler = tigertonic.ApacheLogged(handler)
tigertonic.ApacheLogged
for Apache combined logs
handler = tigertonic.Counted(handler, "my-handler", nil) handler = tigertonic.Timed(handler, "my-handler", nil)
tigertonic.Counted
and tigertonic.Timed
for knowing how many and how fast
We mostly use tigertonic.Timed
on each route individually
Strongly-typed per-request context
Middleware chains
URL namespaces
Virtual hostnames
HTTP Basic auth
CORS basics
TLS
Construct a fake http.Request
Call ServeHTTP
Use httptest.ResponseRecorder
Call your Marshaled
function directly
go get
tool invites you right in
Go-specific solutions:
gopack
and johnny-deps
git-submodule
is perfectly fine
Google vendors third-party code
We vendor third-party code as part of CI
TLS termination in Tiger Tonic
No crypto to block Nginx event loop
One fewer hops means lower latency
c, _ := tls.LoadX509KeyPair(certPathname, keyPathname) listener, err := tls.Listen("tcp", ":4443", &tls.Config{ Certificates: []tls.Certificate{c}, CipherSuites: []uint16{tls.TLS_RSA_WITH_RC4_128_SHA}, })
Always use net.Conn
and net.Listener
interfaces to serve either TLS and non-TLS
Replace crypto/tls
package with openssl
binding as an optimization
Favor local variables that don’t “escape” onto the heap
Reuse allocated buffers
A small heap is a fast heap
Use the standard library
Implement interfaces when you can
Fail fast and with details
Accepted in the large
Holds true in the small within a Go process
Goroutine-per-connection
Limit concurrency when necessary: one channel, m senders, and n receivers
Macro statistics from Counted
and Timed
Micro statistics from runtime/pprof
and go tool pprof
Use http.Handler
for web services
Use concurrency primitives for concurrent or pipelined tasks
Bridge the gap cautiously
Remember that errors are not exceptional
github.com/d2fn/gopack
github.com/rcrowley/goagain
github.com/rcrowley/go-metrics
github.com/rcrowley/go-tigertonic
github.com/VividCortex/johnny-deps