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

Microservices and Message Queues for Great Scale

Microservices and Message Queues for Great Scale

Patterns, best practices and approaches to building microservices and running the NSQ open source message queue system.

Peter Herndon

May 20, 2015
Tweet

More Decks by Peter Herndon

Other Decks in Technology

Transcript

  1. 16 x 9 Microservices and Message Queues for Great Scale

    Gluecon May 20, 2015 189 slides 48 Images & diagrams 14 credits
  2. • ENCODES • http://www.google.com —> http://bit.ly/1luWqJk • 867,039,834 requests per

    month • ~335 rps What is Bitly? user gives URL, we return a shortened hash Bitlink. event is "encode". (On the opposite side)
  3. • DECODES • http://bit.ly/1luWqJk —> http://www.google.com • 10,252,698,468 requests per

    month • ~4000 rps What is Bitly? Bitlink in wild, clicks the Bitlink, redirect to original URL."decode". (Our team)
  4. • ~16 Engineers • 2 Dedicated Ops • 6 Engineers

    On-Call Rotation Who is Bitly? is rather small.
  5. • No single point of failure • Self-healing • Upgrade

    without an outage • Strive for predictability via Heather McKelvey, VPE Basho Architecture Goals
  6. • No single point of failure • Self-healing • Upgrade

    without an outage • Strive for predictability via Heather McKelvey, VPE Basho Architecture Goals
  7. good ops are used to handling multiple services and the

    communications between them, and the failures that occur, push responsibility to where it (fits more naturally)
  8. And if your engineers can’t handle a monolith, they can’t

    handle microservices either. Teamwork: devops: devs are ops, ops are devs.
  9. Trade-offs: higher latency per request, due to overhead of async,

    but way more concurrent connections. Ghetto of Tornado, instead of main Python land.
  10. Conventions One way to make your life easier is to

    attend conventions, I mean make and follow conventions. (We have)
  11. • /<service> • /<service>/app • /<service>/conf • /<service>/lib • /<service>/scripts

    • /<service>/tests Directory Layout standard directory layout for a microservice.
  12. import  functools   import  tornado.options   tornado.options.define("environment",  default="dev",  help="environment")  

    options  =  {          'dev':  {                  'certfile':  'conf/aps_development.pem',                  'apns_host':  dict(host='gateway.sandbox.push.apple.com',  port=2195),                  'feedback_host':  dict(host='feedback.sandbox.push.apple.com',  port=2196),                  'memcached':  expand_role('memcached.dev',  port=11211),                  'apns_reconnect_lag':  5,                  'feedback_enabled':  False,                  'feedback_reconnect_lag':  60,          },          'del':  {                  'certfile':  'conf/aps_production.pem',                  'apns_host':  dict(host='gateway.push.apple.com',  port=2195),   /<service>/settings.py Bunch of dictionaries, that separate environments and roles
  13. import  logging   import  tornado.httpserver   import  tornado.ioloop   import

     tornado.options   import  app.api   import  app.basic   import  lib.connection   /<service>/<service>_api.py, 1 of 4 standard imports,
  14. class  Application(tornado.web.Application):          def  __init__(self):    

                 debug  =  tornado.options.options.environment  ==  'dev'                  if  debug:                          logging.getLogger().setLevel(logging.DEBUG)                  app_settings  =  {                          'debug':  debug,                          'login_url':  'dummy',                          'apns_conn':  lib.connection.APNSConn()                  }   /<service>/<service>_api.py, 2 of 4 set up logging and app settings, then we (define our)
  15. handlers  =  [        (r'/push$',  app.api.PushHandler),    

       (r'/flush$',  app.api.FlushHandler),        (r'/deactivated_tokens$',   app.api.DeactivatedTokensHandler),        (r'/stats$',  app.api.StatsHandler),        (r’/ping$',  app.api.PingHandler),      ]   tornado.web.Application.__init__(self,  handlers,   **app_settings)   /<service>/<service>_api.py, 3 of 4 URL routing and initialize the server. (Finally)
  16. if  __name__  ==  '__main__':          tornado.options.define('port',  default=7320,

     help="Listen  on  port",  type=int)          tornado.options.parse_command_line()          logging.info("Starting  apns_api  on  0.0.0.0:%d"  %  tornado.options.options.port)          http_server  =  tornado.httpserver.HTTPServer(request_callback=Application())          http_server.listen(tornado.options.options.port,  address='0.0.0.0')          tornado.ioloop.IOLoop.instance().start()   /<service>/<service>_api.py, 4 of 4 define port, parse command line options, start the server’s event loop
  17. • settings.py • <service>_api.py • queuereader_<service>.py • README.md /<service> Queuereaders

    are part of streaming architecture, which I'll cover shortly. In the (app directory)
  18. import  simplejson  as  json   import  tornado.httpclient   import  tornado.web

      class  BaseHandler(tornado.web.RequestHandler):          def  error(self,  status_code,  status_txt,  data=None):                  """write  an  api  error  in  the  appropriate  response  format"""                  self.api_response(status_code=status_code,                  status_txt=status_txt,                  data=data)          def  api_response(self,  data,  status_code=200,  status_txt="OK"):                  """write  an  api  response  in  json"""                  self.set_header("Content-­‐Type",  "application/json;  charset=utf-­‐8")                  self.finish(json.dumps(dict(data=data,                        status_code=status_code,                        status_txt=status_txt)))   /<service>/app/basic.py writing our responses in our standard API response format, (including)
  19. import  simplejson  as  json   import  tornado.httpclient   import  tornado.web

      class  BaseHandler(tornado.web.RequestHandler):          def  error(self,  status_code,  status_txt,  data=None):                  """write  an  api  error  in  the  appropriate  response  format"""                  self.api_response(status_code=status_code,                  status_txt=status_txt,                  data=data)          def  api_response(self,  data,  status_code=200,  status_txt="OK"):                  """write  an  api  response  in  json"""                  self.set_header("Content-­‐Type",  "application/json;  charset=utf-­‐8")                  self.finish(json.dumps(dict(data=data,                        status_code=status_code,                        status_txt=status_txt)))   /<service>/app/basic.py errors, and our (successful API responses as well)
  20. import  simplejson  as  json   import  tornado.httpclient   import  tornado.web

      class  BaseHandler(tornado.web.RequestHandler):          def  error(self,  status_code,  status_txt,  data=None):                  """write  an  api  error  in  the  appropriate  response  format"""                  self.api_response(status_code=status_code,                  status_txt=status_txt,                  data=data)          def  api_response(self,  data,  status_code=200,  status_txt="OK"):                  """write  an  api  response  in  json"""                  self.set_header("Content-­‐Type",  "application/json;  charset=utf-­‐8")                  self.finish(json.dumps(dict(data=data,                        status_code=status_code,                        status_txt=status_txt)))   /<service>/app/basic.py
  21. import  simplejson  as  json   import  tornado.httpclient   import  tornado.web

      class  BaseHandler(tornado.web.RequestHandler):          def  error(self,  status_code,  status_txt,  data=None):                  """write  an  api  error  in  the  appropriate  response  format"""                  self.api_response(status_code=status_code,                  status_txt=status_txt,                  data=data)          def  api_response(self,  data,  status_code=200,  status_txt="OK"):                  """write  an  api  response  in  json"""                  self.set_header("Content-­‐Type",  "application/json;  charset=utf-­‐8")                  self.finish(json.dumps(dict(data=data,                        status_code=status_code,                        status_txt=status_txt)))   /<service>/app/basic.py setting content type to JSON. In (api.py)
  22. class  ListHandler(basic.BaseHandler):          @app.basic.format_api_errors      

       @tornado.web.asynchronous          def  get(self):                  unit  =  self.get_unit_argument()                  total_key  =  self.get_argument("key")                  self.get_all_units(total_key,   user_callback=self.async_callback(self.finish_list,  unit=unit)) /<service>/app/api.py method to handle (HTTP GET)
  23. class  ListHandler(basic.BaseHandler):          @app.basic.format_api_errors      

       @tornado.web.asynchronous          def  get(self):                  unit  =  self.get_unit_argument()                  total_key  =  self.get_argument("key")                  self.get_all_units(total_key,   user_callback=self.async_callback(self.finish_list,  unit=unit)) /<service>/app/api.py and sets up a (callback)
  24. class  ListHandler(CookieMonsterAPIHandler):          @app.basic.format_api_errors      

       @tornado.web.asynchronous          def  get(self):                  unit  =  self.get_unit_argument()                  total_key  =  self.get_argument("key")                  self.get_all_units(total_key,   user_callback=self.async_callback(self.finish_list,  unit=unit)) /<service>/app/api.py self.finish_list(). Older Tornado code, similar to Twisted or default node.js last time I looked.
  25. class  ListHandler(CookieMonsterAPIHandler):          @app.basic.format_api_errors      

       @tornado.web.asynchronous          def  get(self):                  unit  =  self.get_unit_argument()                  total_key  =  self.get_argument("key")                  self.get_all_units(total_key,   user_callback=self.async_callback(self.finish_list,  unit=unit)) /<service>/app/api.py Note the @tornado.web.asynchronous (decorator)
  26. class  ListHandler(CookieMonsterAPIHandler):          @app.basic.format_api_errors      

       @tornado.web.asynchronous          def  get(self):                  unit  =  self.get_unit_argument()                  total_key  =  self.get_argument("key")                  self.get_all_units(total_key,   user_callback=self.async_callback(self.finish_list,  unit=unit)) /<service>/app/api.py makes method asynchronous (finish_list callback)
  27.        @app.basic.format_api_errors          def  finish_list(self,

     data,  unit):                  if  data  is  None:                          raise  tornado.web.HTTPError(500,  'UNKNOWN_ERROR')                  units  =  self.get_int_argument("units",  1)                  unit_reference_dt  =  self.get_unit_reference_datetime()                  output  =  ListHandler.get_output_counts(data,                   unit,  units,  unit_reference_dt)                  logging.debug("output:  %r"  %  output)                  self.api_response(output) /<service>/app/api.py accepts the response from the callback and if successful, (outputs)
  28.        @app.basic.format_api_errors          def  finish_list(self,

     data,  unit):                  if  data  is  None:                          raise  tornado.web.HTTPError(500,  'UNKNOWN_ERROR')                  units  =  self.get_int_argument("units",  1)                  unit_reference_dt  =  self.get_unit_reference_datetime()                  output  =  ListHandler.get_output_counts(data,                   unit,  units,  unit_reference_dt)                  logging.debug("output:  %r"  %  output)                  self.api_response(output) /<service>/app/api.py response in standard API response format. A (modern version)
  29. class  ListHandler(basic.BaseHandler):          @app.basic.format_api_errors      

       @gen.coroutine          def  get(self):                  unit  =  self.get_unit_argument()                  total_key  =  self.get_argument("key")                  data  =  yield  self.get_all_units(total_key)         units  =  self.get_int_argument("units",  1)                  unit_reference_dt  =  self.get_unit_reference_datetime()                  output  =  ListHandler.get_output_counts(data,                   unit,  units,  unit_reference_dt)                  logging.debug("output:  %r"  %  output)   /<service>/app/api.py, Modernized would use the gen.coroutine decorator and yield on the async call. The (lib directory)
  30. • __init__.py • <datastore_interface>.py • <service>_sq.py /<service>/lib holds our datastore

    access code. The datastore will be specific to the service. No ORMs, just DAOs. (conf directory)
  31. • <service>_backup.sh • <service>_restore.sh • <service>_load.sh • … /<service>/scripts will

    hold whatever utility scripts make sense, including nagios check scripts. Conventions help build a service that is easily maintained, particularly at 3am. Operational simplification. Implementing a Bitly Service (in Go)
  32. eshistory ├── README.md ├── eshistory_api │ ├── api_formatter │ │

    └── api_response_writer.go │ ├── blacklist_handlers.go │ ├── eshistory_api.go │ ├── handlers.go │ ├── routes │ │ └── routes.go │ ├── routes.go Directory Structure Main project directory, (API directory)
  33. eshistory ├── README.md ├── eshistory_api │ ├── api_formatter │ │

    └── api_response_writer.go │ ├── blacklist_handlers.go │ ├── eshistory_api.go │ ├── handlers.go │ ├── routes │ │ └── routes.go │ ├── routes.go Directory Structure files (implement API)
  34. eshistory ├── README.md ├── eshistory_api │ ├── api_formatter │ │

    └── api_response_writer.go │ ├── blacklist_handlers.go │ ├── eshistory_api.go │ ├── handlers.go │ ├── routes │ │ └── routes.go │ ├── routes.go Directory Structure sub packages implement (related functionality)
  35. eshistory ├── README.md ├── eshistory_api │ ├── api_formatter │ │

    └── api_response_writer.go │ ├── blacklist_handlers.go │ ├── eshistory_api.go │ ├── handlers.go │ ├── routes │ │ └── routes.go │ ├── routes.go Directory Structure Scroll down (internal packages used by API *and* queuereaders)
  36. eshistory ├── internal │ ├── eshistory │ │ ├── eshistory.go

    │ │ └── eshistory_test.go │ ├── messages │ │ └── messages.go │ └── statsd │ └── statsd.go Directory Structure elasticsearch client, struct definitions, our statsd client for metrics, and (queue readers)
  37. eshistory ├── queuereader_bulk_action │ ├── bulk_action_sq.go │ └── queuereader_bulk_action.go ├──

    queuereader_history_es │ ├── history_es_sq.go │ ├── history_es_sq_test.go │ ├── queuereader_history_es.go │ └── queuereader_history_es_integration_test.go ├── queuereader_history_raw │ ├── handler.go Directory Structure Go-specific (settings)
  38. eshistory ├── queuereader_history_raw │ ├── handler.go │ └── queuereader_history_raw.go ├──

    settings.json Directory Structure API main executable eshistory_api.go. First you (have your)
  39. package  main   import  (     "flag"    

    "fmt"     "net"     "net/http"     "os"     "strings"     "bitly/eshistory/eshistory_api/routes"   eshistory/eshistory_api/eshistory_api.go Standard library imports, then (your)
  40.   "bitly/eshistory/internal/eshistory"     "bitly/eshistory/internal/statsd"     "bitly/libbitly/exitchan"    

    "bitly/libbitly/memstats"     "bitly/libbitly/request_logger"     "bitly/libbitly/roles"     "bitly/libbitly/settings"     "bitly/libbitly/statsdproxy"     log  "github.com/Sirupsen/logrus"     "github.com/jehiah/memcache_pycompat"   )   eshistory/eshistory_api/eshistory_api.go Bitly and third-party imports. Next we (have)
  41. var  (     statsdQueue          *statsdproxy.StatsdQueue

        eshistoryClient  *eshistory.Client     blacklist              =  NewGreyList("../conf/blacklist.dat")     whitelist              =  NewGreyList("../conf/whitelist.dat")   )   eshistory/eshistory_api/eshistory_api.go Global variables, including reading in our blacklist data
  42. func  main()  {     var  (      

    environment        =  flag.String("environment",  "dev",  "run  environment  type")       settingsFile      =  flag.String("settings-­‐file",  "../settings.json",  "path  to  settings.json  file")       port                      =  flag.Int("port",  7850,  "port  to  listen  on  for  http  requests")       verbose                =  flag.Bool("verbose",  false,  "verbose  logging")       logRequests        =  flag.Bool("log-­‐requests",  false,  "turn  on  logging  requests  in  Common  Log  Format")       logMemoryStats  =  flag.Bool("log-­‐memory-­‐stats",  false,  "turn  on  logging  of  memory  statistics")     )     flag.Parse()     settings.Load(*settingsFile,  *environment)     roles.LoadRoles("/bitly/local/conf/roles.json")     //gpg  =  settings.OpenGpgSettingsFromSettingsFile(*settingsFile)     if  *verbose  {       log.SetLevel(log.DebugLevel)       log.Info("Verbose  logging  enabled")       *logRequests  =  true       *logMemoryStats  =  true     }  else  {       switch  settings.GetString("log_level")  {       case  "debug":   eshistory/eshistory_api/eshistory_api.go The main func sets up CLI flags, loads settings, and sets the log (level)
  43. func  main()  {     var  (      

    environment        =  flag.String("environment",  "dev",  "run  environment  type")       settingsFile      =  flag.String("settings-­‐file",  "../settings.json",  "path  to  settings.json  file")       port                      =  flag.Int("port",  7850,  "port  to  listen  on  for  http  requests")       verbose                =  flag.Bool("verbose",  false,  "verbose  logging")       logRequests        =  flag.Bool("log-­‐requests",  false,  "turn  on  logging  requests  in  Common  Log  Format")       logMemoryStats  =  flag.Bool("log-­‐memory-­‐stats",  false,  "turn  on  logging  of  memory  statistics")     )     flag.Parse()     settings.Load(*settingsFile,  *environment)     roles.LoadRoles("/bitly/local/conf/roles.json")     //gpg  =  settings.OpenGpgSettingsFromSettingsFile(*settingsFile)     if  *verbose  {       log.SetLevel(log.DebugLevel)       log.Info("Verbose  logging  enabled")       *logRequests  =  true       *logMemoryStats  =  true     }  else  {       switch  settings.GetString("log_level")  {       case  "debug":   eshistory/eshistory_api/eshistory_api.go logRequests flag we’ll see in action later, but adds standard request logging (~common log format) to our application — that does *not* come in Go standard library.
  44.     switch  settings.GetString("log_level")  {       case  "debug":

            log.SetLevel(log.DebugLevel)       case  "info":         log.SetLevel(log.InfoLevel)       case  "error":         log.SetLevel(log.ErrorLevel)       default:         log.SetLevel(log.WarnLevel)       }     }     eshistoryClient  =  &eshistory.Client{       Conn:                eshistory.InitElasticsearchConnection("eshistory_elasticsearch"),       IndexName:      settings.GetString("es_index_name"),       IndexType:      settings.GetString("es_index_type"),       MC:                    memcache.NewClient(settings.GetRole("memcached").Entries()),       StatsdQueue:  statsd.InitStatsd(),     }   eshistory/eshistory_api/eshistory_api.go The remants of setting the log level and (initializing the connection to our datastore)
  45.     switch  settings.GetString("log_level")  {       case  "debug":

            log.SetLevel(log.DebugLevel)       case  "info":         log.SetLevel(log.InfoLevel)       case  "error":         log.SetLevel(log.ErrorLevel)       default:         log.SetLevel(log.WarnLevel)       }     }     eshistoryClient  =  &eshistory.Client{       Conn:                eshistory.InitElasticsearchConnection("eshistory_elasticsearch"),       IndexName:      settings.GetString("es_index_name"),       IndexType:      settings.GetString("es_index_type"),       MC:                    memcache.NewClient(settings.GetRole("memcached").Entries()),       StatsdQueue:  statsd.InitStatsd(),     }   eshistory/eshistory_api/eshistory_api.go ElasticSearch and memcached. (Here we are)
  46.   statsdQueue  =  statsd.InitStatsd()     listenAddr  :=  fmt.Sprintf("0.0.0.0:%d",  *port)

        listener,  err  :=  net.Listen("tcp",  listenAddr)     if  err  !=  nil  {       log.Fatalf("FATAL:  Listen  on  %s  has  failed:  %s",  listenAddr,  err)     }     exitChan  :=  exitchan.New()     go  func()  {       <-­‐exitChan       err  :=  listener.Close()       if  err  !=  nil  {         log.Fatalf("FATAL:  got  signal  but  failed  to  close  listener:  %s",  err)       }     }()     log.Infof("logMemoryStats:  %s",  *logMemoryStats)     if  *logMemoryStats  {   eshistory/eshistory_api/eshistory_api.go setting up our connection to statsd, establishing our TCP listener on the (provided port)
  47.   statsdQueue  =  statsd.InitStatsd()     listenAddr  :=  fmt.Sprintf("0.0.0.0:%d",  *port)

        listener,  err  :=  net.Listen("tcp",  listenAddr)     if  err  !=  nil  {       log.Fatalf("FATAL:  Listen  on  %s  has  failed:  %s",  listenAddr,  err)     }     exitChan  :=  exitchan.New()     go  func()  {       <-­‐exitChan       err  :=  listener.Close()       if  err  !=  nil  {         log.Fatalf("FATAL:  got  signal  but  failed  to  close  listener:  %s",  err)       }     }()     log.Infof("logMemoryStats:  %s",  *logMemoryStats)     if  *logMemoryStats  {   eshistory/eshistory_api/eshistory_api.go … (creating our exit channel signal handler)
  48.   statsdQueue  =  statsd.InitStatsd()     listenAddr  :=  fmt.Sprintf("0.0.0.0:%d",  *port)

        listener,  err  :=  net.Listen("tcp",  listenAddr)     if  err  !=  nil  {       log.Fatalf("FATAL:  Listen  on  %s  has  failed:  %s",  listenAddr,  err)     }     exitChan  :=  exitchan.New()     go  func()  {       <-­‐exitChan       err  :=  listener.Close()       if  err  !=  nil  {         log.Fatalf("FATAL:  got  signal  but  failed  to  close  listener:  %s",  err)       }     }()     log.Infof("logMemoryStats:  %s",  *logMemoryStats)     if  *logMemoryStats  {   eshistory/eshistory_api/eshistory_api.go and setting up the response to a signal. Next, (we create a net/http Handler for routing)
  49.   if  *logMemoryStats  {       go  memstats.MemStatsStatsd()  

        go  memstats.MemStats()     }     var  router  http.Handler     log.Infof("logRequests:  %s",  *logRequests)     if  *logRequests  {       router  =  request_logger.LoggingHandler(os.Stdout,  routes.NewRouter(app_routes))     }  else  {       router  =  routes.NewRouter(app_routes)     }     httpServer  :=  &http.Server{       Handler:  router,       //   MaxHeaderBytes:  MaxReqHeaderBytes,     }     log.Infof("INFO:  Listening  for  requests  on  %s",  listener.Addr())   eshistory/eshistory_api/eshistory_api.go … and create a gorilla-mux router with (our API routes)
  50.   if  *logMemoryStats  {       go  memstats.MemStatsStatsd()  

        go  memstats.MemStats()     }     var  router  http.Handler     log.Infof("logRequests:  %s",  *logRequests)     if  *logRequests  {       router  =  request_logger.LoggingHandler(os.Stdout,  routes.NewRouter(app_routes))     }  else  {       router  =  routes.NewRouter(app_routes)     }     httpServer  :=  &http.Server{       Handler:  router,       //   MaxHeaderBytes:  MaxReqHeaderBytes,     }     log.Infof("INFO:  Listening  for  requests  on  %s",  listener.Addr())   eshistory/eshistory_api/eshistory_api.go If logRequests is true, as noted earlier, (we wrap the handler in a request-logging handler)
  51.   if  *logMemoryStats  {       go  memstats.MemStatsStatsd()  

        go  memstats.MemStats()     }     var  router  http.Handler     log.Infof("logRequests:  %s",  *logRequests)     if  *logRequests  {       router  =  request_logger.LoggingHandler(os.Stdout,  routes.NewRouter(app_routes))     }  else  {       router  =  routes.NewRouter(app_routes)     }     httpServer  :=  &http.Server{       Handler:  router,       //   MaxHeaderBytes:  MaxReqHeaderBytes,     }     log.Infof("INFO:  Listening  for  requests  on  %s",  listener.Addr())   eshistory/eshistory_api/eshistory_api.go This pattern is common in Go, and is analogous to Python decorators. Finally, we (tell the app to serve)
  52.   err  =  httpServer.Serve(listener)     if  err  !=  nil

     &&  !strings.Contains(err.Error(),  "use  of  closed  network   connection")  {       log.Fatalf("FATAL:  Server()  error:  %s",  err)     }     statsdQueue.Close()     log.Info("Exiting...")   }   eshistory/eshistory_api/eshistory_api.go Next up, we have our (routes definitions)
  53. package  main   import  (     "bitly/eshistory/eshistory_api/routes"   )

      var  app_routes  =  routes.Routes{     routes.Route{       "Ping",       "GET",       "/ping",       PingHandler,     },     routes.Route{       "Get",       "GET",       "/get",       GetHandler,     },   eshistory/eshistory_api/routes.go Each route has a name, a method, a URL pattern and a (HandlerFunc)
  54. package  routes   import  (     "net/http"    

    "github.com/gorilla/mux"   )   //  Route  defines  a  route  with  a  Name,  Method,  Pattern  and  HandlerFunc   type  Route  struct  {     Name                string     Method            string     Pattern          string     HandlerFunc  http.HandlerFunc   }   eshistory/eshistory_api/routes/routes.go In this subpackage we define what a Route is
  55. //  NewRouter  creates  a  new  router  from  Routes   func

     NewRouter(routes  Routes)  *mux.Router  {     router  :=  mux.NewRouter().StrictSlash(false)     for  _,  route  :=  range  routes  {       router.Methods(route.Method)                            .Path(route.Pattern)                            .Name(route.Name)                            .Handler(route.HandlerFunc)     }     return  router   }   eshistory/eshistory_api/routes/routes.go In eshistory_api.go, our main func passes those routes into the NewRouter function to create the net/http Handler. Moving on (to our main handler definitions)
  56. package  main   import  (     "encoding/json"    

    "net/http"     "sort"     "strconv"     "strings"     "time"     "bitly/eshistory/eshistory_api/api_formatter"     "bitly/eshistory/internal/eshistory"     "bitly/eshistory/internal/messages"     "bitly/libbitly/settings"   eshistory/eshistory_api/handlers.go we have our imports. I’d like to point out (api_formatter)
  57. package  main   import  (     "encoding/json"    

    "net/http"     "sort"     "strconv"     "strings"     "time"     "bitly/eshistory/eshistory_api/api_formatter"     "bitly/eshistory/internal/eshistory"     "bitly/eshistory/internal/messages"     "bitly/libbitly/settings"   eshistory/eshistory_api/handlers.go which is our Go library for formatting our conventional API JSON responses. The equivalent of the code in Python is in BaseHandler class.
  58.   log  "github.com/Sirupsen/logrus"     "github.com/bitly/go-­‐nsq"     gomemcache  "github.com/bradfitz/gomemcache/memcache"

        "github.com/jehiah/memcache_pycompat"   )   type  tag  struct  {     Value  string  `json:"tag"`   }   type  tags  []tag   type  userTagsResponse  struct  {   eshistory/eshistory_api/handlers.go Finishing up the imports, I highly recommend (you use logrus)
  59.   log  "github.com/Sirupsen/logrus"     "github.com/bitly/go-­‐nsq"     gomemcache  "github.com/bradfitz/gomemcache/memcache"

        "github.com/jehiah/memcache_pycompat"   )   type  tag  struct  {     Value  string  `json:"tag"`   }   type  tags  []tag   type  userTagsResponse  struct  {   eshistory/eshistory_api/handlers.go as it is a great tool that provides a massive improvement over standard library logging. You get log levels, structured data, output formats including text, XML and JSON, and TTY coloring. (Next we have)
  60. type  userTagsResponse  struct  {     Results      

       tags  `json:"results"`     ResultCount  int    `json:"result_count"`   }   type  searchRegexResponse  struct  {     Page                int                            `json:"page"`     PerPage          int                            `json:"perpage"`     Results          []eshistory.Link  `json:"results"`     ResultCount  int                            `json:"result_count"`   }   type  backfillPrivacyResponse  struct  {     User        string  `json:"user"`   eshistory/eshistory_api/handlers.go a set of structs used to establish types for responses from handlers. Useful pattern, define the output of handlers to ensure consistency and make it easy to fill in the blanks, and encode into JSON.
  61. //  UserTagsHandler  implements  /user_tags,  and  returns  a  slice  of  tags

     and   their  total  count.   func  UserTagsHandler(rw  http.ResponseWriter,  req  *http.Request)  {     w  :=  &api_formatter.JSONResponseWriter{rw,  false}     user  :=  req.FormValue("user")     handlerName  :=  "UserTagsHandler"     if  user  ==  "bitly"  {       log.Warnf("query  for  bitly  user  should  never  happen  %r",  req)       if  err  :=  w.WriteAPIResponse(nil);  err  !=  nil  {         log.WithFields(log.Fields{"error":  err,  "handler":   handlerName}).Error("WriteAPIResponse  error")       }       return     }     statsdQueue.Increment("tags")   eshistory/eshistory_api/handlers.go Moving on to an actual handler implementation, notice (the use of JSONResponseWriter from api_formatter)
  62. //  UserTagsHandler  implements  /user_tags,  and  returns  a  slice  of  tags

     and   their  total  count.   func  UserTagsHandler(rw  http.ResponseWriter,  req  *http.Request)  {     w  :=  &api_formatter.JSONResponseWriter{rw,  false}     user  :=  req.FormValue("user")     handlerName  :=  "UserTagsHandler"     if  user  ==  "bitly"  {       log.Warnf("query  for  bitly  user  should  never  happen  %r",  req)       if  err  :=  w.WriteAPIResponse(nil);  err  !=  nil  {         log.WithFields(log.Fields{"error":  err,  "handler":   handlerName}).Error("WriteAPIResponse  error")       }       return     }     statsdQueue.Increment("tags")   eshistory/eshistory_api/handlers.go Which is another wrapper, in this case of http.ResponseWriter. (Here’s an example of logrus)
  63. //  UserTagsHandler  implements  /user_tags,  and  returns  a  slice  of  tags

     and  their  total  count.   func  UserTagsHandler(rw  http.ResponseWriter,  req  *http.Request)  {     w  :=  &api_formatter.JSONResponseWriter{rw,  false}     user  :=  req.FormValue("user")     handlerName  :=  "UserTagsHandler"     if  user  ==  "bitly"  {       log.Warnf("query  for  bitly  user  should  never  happen  %r",  req)       if  err  :=  w.WriteAPIResponse(nil);  err  !=  nil  {         log.WithFields(log.Fields{"error":  err,  "handler":   handlerName}).Error("WriteAPIResponse  error")       }       return     }     statsdQueue.Increment("tags")     tagFilter  :=  req.FormValue("tag_filter")     userTags,  err  :=  eshistoryClient.GetUserTags(user)     if  err  !=  nil  {   eshistory/eshistory_api/handlers.go showing how you can add structured field data
  64. //  UserTagsHandler  implements  /user_tags,  and  returns  a  slice  of  tags

     and  their  total  count.   func  UserTagsHandler(rw  http.ResponseWriter,  req  *http.Request)  {     w  :=  &api_formatter.JSONResponseWriter{rw,  false}     user  :=  req.FormValue("user")     handlerName  :=  "UserTagsHandler"     if  user  ==  "bitly"  {       log.Warnf("query  for  bitly  user  should  never  happen  %r",  req)       if  err  :=  w.WriteAPIResponse(nil);  err  !=  nil  {         log.WithFields(log.Fields{"error":  err,  "handler":   handlerName}).Error("WriteAPIResponse  error")       }       return     }     statsdQueue.Increment("tags")     tagFilter  :=  req.FormValue("tag_filter")     userTags,  err  :=  eshistoryClient.GetUserTags(user)     if  err  !=  nil  {   eshistory/eshistory_api/handlers.go as well as specify the log level
  65.   var  filteredTags  tags     for  _,  t  :=

     range  uTags  {       if  strings.HasPrefix(t.Value,  string(tagFilter))  {         filteredTags  =  append(filteredTags,  t)       }     }     sort.Sort(filteredTags)     finalResponse  :=  userTagsResponse{       Results:          filteredTags,       ResultCount:  len(filteredTags),     }     if  err  :=  w.WriteAPIResponse(finalResponse);  err  !=  nil  {       log.WithFields(log.Fields{"response":  finalResponse,  "error":  err,  "handler":   handlerName}).Error("WriteAPIResponse  error")     }   eshistory/eshistory_api/handlers.go I’ve skipped down a bit in UserTagsHandler, but here we have a list of tags that we filter, then sort (then populate a userTagsResponse struct)
  66.   var  filteredTags  tags     for  _,  t  :=

     range  uTags  {       if  strings.HasPrefix(t.Value,  string(tagFilter))  {         filteredTags  =  append(filteredTags,  t)       }     }     sort.Sort(filteredTags)     finalResponse  :=  userTagsResponse{       Results:          filteredTags,       ResultCount:  len(filteredTags),     }     if  err  :=  w.WriteAPIResponse(finalResponse);  err  !=  nil  {       log.WithFields(log.Fields{"response":  finalResponse,  "error":  err,  "handler":   handlerName}).Error("WriteAPIResponse  error")     }   eshistory/eshistory_api/handlers.go which we then pass to (WriteAPIResponse)
  67.   var  filteredTags  tags     for  _,  t  :=

     range  uTags  {       if  strings.HasPrefix(t.Value,  string(tagFilter))  {         filteredTags  =  append(filteredTags,  t)       }     }     sort.Sort(filteredTags)     finalResponse  :=  userTagsResponse{       Results:          filteredTags,       ResultCount:  len(filteredTags),     }     if  err  :=  w.WriteAPIResponse(finalResponse);  err  !=  nil  {       log.WithFields(log.Fields{"response":  finalResponse,  "error":  err,  "handler":   handlerName}).Error("WriteAPIResponse  error")     }   eshistory/eshistory_api/handlers.go to convert to our standard JSON response format. So (let’s take a look at the API response writer)
  68. //  adapted  from  https://github.com/ant0ine/go-­‐json-­‐rest/master/rest/response.go   //  to  add  methods  for

     wrapping  our  responses  as  our  particular  JSON  format   package  api_formatter   import  (     "bufio"     "encoding/json"     "net"     "net/http"     log  "github.com/Sirupsen/logrus"   )   //  A  ResponseWriter  interface  dedicated  to  JSON  HTTP  response.   //  Note  that  the  object  instantiated  by  the  ResourceHandler  that  implements  this   interface,   //  also  happens  to  implement  http.ResponseWriter,  http.Flusher  and  http.CloseNotifier.   type  ResponseWriter  interface  {   eshistory/eshistory_api/api_formatter/api_response_writer.go Here we declare the standard net/http ResponseWriter interface, and implement the required methods — (Golang inheritance, effectively)
  69.   //  Identical  to  the  http.ResponseWriter  interface     Header()

     http.Header     //  Use  EncodeJSON  to  generate  the  payload,  write  the  headers  with  http.StatusOK  if     //  they  are  not  already  written,  then  write  the  payload.     //  The  Content-­‐Type  header  is  set  to  "application/json",  unless  already  specified.     WriteJSON(v  interface{})  error     //  Encode  the  data  structure  to  JSON,  mainly  used  to  wrap  ResponseWriter  in     //  middlewares.     EncodeJSON(v  interface{})  ([]byte,  error)     //  Similar  to  the  http.ResponseWriter  interface,  with  additional  JSON  related     //  headers  set.     WriteHeader(int)     //  Use  Error  to  generate  an  error  message  using  Bitly  standard  API  response  format     Error(int,  string,  interface{})   eshistory/eshistory_api/api_formatter/api_response_writer.go We have Header, WriterHeader, WriteJSON, EncodeJSON, (Error method declarations)
  70.   //  Identical  to  the  http.ResponseWriter  interface     Header()

     http.Header     //  Use  EncodeJSON  to  generate  the  payload,  write  the  headers  with  http.StatusOK  if     //  they  are  not  already  written,  then  write  the  payload.     //  The  Content-­‐Type  header  is  set  to  "application/json",  unless  already  specified.     WriteJSON(v  interface{})  error     //  Encode  the  data  structure  to  JSON,  mainly  used  to  wrap  ResponseWriter  in     //  middlewares.     EncodeJSON(v  interface{})  ([]byte,  error)     //  Similar  to  the  http.ResponseWriter  interface,  with  additional  JSON  related     //  headers  set.     WriteHeader(int)     //  Use  Error  to  generate  an  error  message  using  Bitly  standard  API  response  format     Error(int,  string,  interface{})   eshistory/eshistory_api/api_formatter/api_response_writer.go So let’s look at three that make up the main logic, (WriteJSON, Error, and WriteAPIResponse)
  71.   //  Use  WriteAPIResponse  to  generate  a  Bitly-­‐standard  API  response

     format     WriteAPIResponse(interface{})   }   //  APIResponse  holds  the  structure  of  a  Bitly-­‐standard  API  response   type  APIResponse  struct  {     Data              interface{}  `json:"data"`     StatusCode  int                  `json:"status_code"`     StatusText  string            `json:"status_txt"`   }   //  JSONResponseWriter  instantiated  by  the  resource  handler.   //  It  implements  the  following  interfaces:   //  ResponseWriter   //  http.ResponseWriter   //  http.Flusher   //  http.CloseNotifier   eshistory/eshistory_api/api_formatter/api_response_writer.go Again, we (define the structure of an APIResponse)
  72.   //  Use  WriteAPIResponse  to  generate  a  Bitly-­‐standard  API  response

     format     WriteAPIResponse(interface{})   }   //  APIResponse  holds  the  structure  of  a  Bitly-­‐standard  API  response   type  APIResponse  struct  {     Data              interface{}  `json:"data"`     StatusCode  int                  `json:"status_code"`     StatusText  string            `json:"status_txt"`   }   //  JSONResponseWriter  instantiated  by  the  resource  handler.   //  It  implements  the  following  interfaces:   //  ResponseWriter   //  http.ResponseWriter   //  http.Flusher   //  http.CloseNotifier   eshistory/eshistory_api/api_formatter/api_response_writer.go StatusCode 200 or 404, StatusText OK or NOT FOUND
  73. type  JSONResponseWriter  struct  {     http.ResponseWriter     WroteHeader

     bool   }   //  WriteHeader  writes  the  HTTP  Content-­‐Type  Header  as  part  of  the  response,   //  and  defaults  to  application/json;  charset=utf-­‐8  if  not  overridden  by   //  w.Header().Set(...)   func  (w  *JSONResponseWriter)  WriteHeader(code  int)  {     if  w.Header().Get("Content-­‐Type")  ==  ""  {       w.Header().Set("Content-­‐Type",  "application/json;  charset=utf-­‐8")     }     w.ResponseWriter.WriteHeader(code)     w.WroteHeader  =  true   }   eshistory/eshistory_api/api_formatter/api_response_writer.go We create (the struct of the ResponseWriter wrapper)
  74. type  JSONResponseWriter  struct  {     http.ResponseWriter     WroteHeader

     bool   }   //  WriteHeader  writes  the  HTTP  Content-­‐Type  Header  as  part  of  the  response,   //  and  defaults  to  application/json;  charset=utf-­‐8  if  not  overridden  by   //  w.Header().Set(...)   func  (w  *JSONResponseWriter)  WriteHeader(code  int)  {     if  w.Header().Get("Content-­‐Type")  ==  ""  {       w.Header().Set("Content-­‐Type",  "application/json;  charset=utf-­‐8")     }     w.ResponseWriter.WriteHeader(code)     w.WroteHeader  =  true   }   eshistory/eshistory_api/api_formatter/api_response_writer.go and implement (WriteHeader)
  75. type  JSONResponseWriter  struct  {     http.ResponseWriter     WroteHeader

     bool   }   //  WriteHeader  writes  the  HTTP  Content-­‐Type  Header  as  part  of  the  response,   //  and  defaults  to  application/json;  charset=utf-­‐8  if  not  overridden  by   //  w.Header().Set(...)   func  (w  *JSONResponseWriter)  WriteHeader(code  int)  {     if  w.Header().Get("Content-­‐Type")  ==  ""  {       w.Header().Set("Content-­‐Type",  "application/json;  charset=utf-­‐8")     }     w.ResponseWriter.WriteHeader(code)     w.WroteHeader  =  true   }   eshistory/eshistory_api/api_formatter/api_response_writer.go which (sets the content type)
  76. type  JSONResponseWriter  struct  {     http.ResponseWriter     WroteHeader

     bool   }   //  WriteHeader  writes  the  HTTP  Content-­‐Type  Header  as  part  of  the  response,   //  and  defaults  to  application/json;  charset=utf-­‐8  if  not  overridden  by   //  w.Header().Set(...)   func  (w  *JSONResponseWriter)  WriteHeader(code  int)  {     if  w.Header().Get("Content-­‐Type")  ==  ""  {       w.Header().Set("Content-­‐Type",  "application/json;  charset=utf-­‐8")     }     w.ResponseWriter.WriteHeader(code)     w.WroteHeader  =  true   } eshistory/eshistory_api/api_formatter/api_response_writer.go to application/json if it has not yet been set manually. Next up is (WriteJSON)
  77. //  WriteJSON  encodes  the  object  in  JSON  and  calls  Write.

      func  (w  *JSONResponseWriter)  WriteJSON(v  interface{})  error  {     b,  err  :=  w.EncodeJSON(v)     if  err  !=  nil  {       return  err     }     log.WithFields(log.Fields{"RESPONSE":  string(b)}).Debug("JSON  Response")     _,  err  =  w.Write(b)     if  err  !=  nil  {       return  err     }     return  nil   }   eshistory/eshistory_api/api_formatter/api_response_writer.go which encodes the passed-in argument as JSON and uses the wrapped writer to write out the JSON response
  78. //  WriteAPIResponse  accepts  a  generic  dictionary  and  writes  a  Bitly-­‐standard

      //  API  response,  e.g.  `{"status_code":  200,  "status_txt":  "OK",  "data":  ...}`   func  (w  *JSONResponseWriter)  WriteAPIResponse(v  interface{})  error  {     r  :=  APIResponse{       Data:              v,       StatusCode:  200,       StatusText:  "OK",     }     err  :=  w.WriteJSON(r)     return  err   }   eshistory/eshistory_api/api_formatter/api_response_writer.go WriteAPIResponse ensures our data matches our (JSON response format convention)
  79. //  WriteAPIResponse  accepts  a  generic  dictionary  and  writes  a  Bitly-­‐standard

      //  API  response,  e.g.  `{"status_code":  200,  "status_txt":  "OK",  "data":  ...}`   func  (w  *JSONResponseWriter)  WriteAPIResponse(v  interface{})  error  {     r  :=  APIResponse{       Data:              v,       StatusCode:  200,       StatusText:  "OK",     }     err  :=  w.WriteJSON(r)     return  err   }   eshistory/eshistory_api/api_formatter/api_response_writer.go …by constructing an APIResponse (with the data)
  80. //  WriteAPIResponse  accepts  a  generic  dictionary  and  writes  a  Bitly-­‐standard

      //  API  response,  e.g.  `{"status_code":  200,  "status_txt":  "OK",  "data":  ...}`   func  (w  *JSONResponseWriter)  WriteAPIResponse(v  interface{})  error  {     r  :=  APIResponse{       Data:              v,       StatusCode:  200,       StatusText:  "OK",     }     err  :=  w.WriteJSON(r)     return  err   }   eshistory/eshistory_api/api_formatter/api_response_writer.go … (and calling WriteJSON)
  81. //  WriteAPIResponse  accepts  a  generic  dictionary  and  writes  a  Bitly-­‐standard

      //  API  response,  e.g.  `{"status_code":  200,  "status_txt":  "OK",  "data":  ...}`   func  (w  *JSONResponseWriter)  WriteAPIResponse(v  interface{})  error  {     r  :=  APIResponse{       Data:              v,       StatusCode:  200,       StatusText:  "OK",     }     err  :=  w.WriteJSON(r)     return  err   }   eshistory/eshistory_api/api_formatter/api_response_writer.go
  82. //  Error  accepts  a  generic  dictionary  and  writes  a  Bitly-­‐standard

      //  API  error  response,  e.g.  `{"status_code":  <code>,  "status_txt":  "OH  NOES",   "data":  ...}`   func  (w  *JSONResponseWriter)  Error(c  int,  t  string,  v  interface{})  error  {     w.ResponseWriter.WriteHeader(c)     w.WroteHeader  =  true     r  :=  APIResponse{       Data:              v,       StatusCode:  c,       StatusText:  t,     }     err  :=  w.WriteJSON(r)     return  err   }   eshistory/eshistory_api/api_formatter/api_response_writer.go The Error function (writes the status code header)
  83. //  Error  accepts  a  generic  dictionary  and  writes  a  Bitly-­‐standard

      //  API  error  response,  e.g.  `{"status_code":  <code>,  "status_txt":  "OH  NOES",   "data":  ...}`   func  (w  *JSONResponseWriter)  Error(c  int,  t  string,  v  interface{})  error  {     w.ResponseWriter.WriteHeader(c)     w.WroteHeader  =  true     r  :=  APIResponse{       Data:              v,       StatusCode:  c,       StatusText:  t,     }     err  :=  w.WriteJSON(r)     return  err   }   eshistory/eshistory_api/api_formatter/api_response_writer.go … then (constructs an APIResponse
  84. //  Error  accepts  a  generic  dictionary  and  writes  a  Bitly-­‐standard

      //  API  error  response,  e.g.  `{"status_code":  <code>,  "status_txt":  "OH  NOES",   "data":  ...}`   func  (w  *JSONResponseWriter)  Error(c  int,  t  string,  v  interface{})  error  {     w.ResponseWriter.WriteHeader(c)     w.WroteHeader  =  true     r  :=  APIResponse{       Data:              v,       StatusCode:  c,       StatusText:  t,     }     err  :=  w.WriteJSON(r)     return  err   }   eshistory/eshistory_api/api_formatter/api_response_writer.go and writes it out to the client.
  85. WHY GO? Measurably better memory utilization Better CPU efficiency Slightly

    lower-latency response times Fewer careless coding mistakes http://photos3.meetupstatic.com/photos/event/c/5/6/e/highres_358490542.jpeg
  86. WHY NOT GO? Greater rigidity of data Less programmer productivity

    Resource efficiency improvements can be marginal Lack of language ecosystem maturity - example: dependency management. (Next, let's queue up)
  87. Message Queues If microservices allow for logical code distribution, message

    queues allow for asynchronous data distribution. Examples include Apache Kafka and RabbitMQ. But before we get into the details, I’d like to (make a very important distinction)
  88. between command-and-control messages v. event notification messages. Command messages tend

    to lead to tighter coupling between the message and the command worker. With (event notifications)
  89. you place a message on the queue saying some kind

    of event occurred. (Listeners interested in that kind of event)
  90. NSQ http://nsq.io NSQ is the distributed pub-sub asynchronous message queue

    system we wrote at Bitly, in Go. (The repositories)
  91. Servers • nsqd • nsqlookupd • nsqadmin • pynsqauthd Components

    include the main message queue server, nsqd, and other servers that round out the ecosystem: nsqlookupd for topic and channel lookup, nsqadmin for administration, and pynsqauthd for very basic authentication and authorization
  92. Clients • go-nsq • pynsq Client Components Bitly-maintained clients in

    Go and Python, plus others in various languages.
  93. Utilities • nsq_tail • nsq_to_file • to_nsq • nsq_to_nsq •

    nsq_stat Rounding Out the Package The included utilities do make the system much easier to use.
  94. Basic Web App Web App Database Basic web app. In

    Python, Django + Postgres, Flask + Postgres, Tornado + Postgres. (First bottleneck is the web layer)
  95. Scaling the Mountain Web App Database Web App Web App

    so you scale horizontally. (Which exposes the next bottleneck, the database)
  96. Cache Rules Everything Around Me Database Web App Web App

    Web App Cache So add caching layer. This approach (works for a while)
  97. Replication Database Database Web App Web App Web App Cache

    …but DB requests still take too long, so replicate
  98. Sharding Web App Web App Web App Cache Database Database

    Database Database Database Database …and then shard. (But individual requests still take too long, because they are doing too much work. )
  99. Breaking Out of the Request-Response Cycle Database Web App Cache

    Queue Worker So add message queue and worker. In Python, this is often Celery backed by Redis or RabbitMQ. (The web app)
  100. Introducing Messages Database Web App Cache Queue Worker Web app

    sends messages to queue, (the workers pull messages and perform tasks)
  101. Persistent Database Web App Cache Queue Worker …write results to

    database, file system, etc. (Now, let's look at NSQ)
  102. Still Persistent Web App Database Worker Queue With NSQ, the

    worker writes results (and the web app)
  103. Event Queued Web App Database Worker Queue writes event messages

    to an nsqd instance local to the web service (but the worker)
  104. Listening Web App Database Worker Queue Queue is listening to

    an nsqd running on another server or service. (Which allows for a distributed architecture)
  105. Workin’ On a Chain Gang Web App Database Worker Queue

    Web App Database Worker Queue Web App Database Worker Queue composed of microservices listening for events published by other services. (How does a worker find the server or servers publishing to a particular topic?)
  106. if  __name__  ==  "__main__":          tornado.options.parse_command_line()  

           logatron_client.setup()                    Reader(                  topic='spam_api',                  channel='queuereader_spam_metrics',                  validate_method=validate_message,                  message_handler=count_spam_actions,                  lookupd_http_addresses=settings.get('nsq_lookupd')          )          run() /<service>/queuereader_<service>.py When you are building a queuereader, this is how you find your topic. (So how are topics created?)
  107. Topic Creation Web App Database Worker Queue topic: ‘spam_api’ First

    time app writes to a TOPIC in the local nsqd, (nsqd creates the topic and)
  108. Topic Registration Web App Database Worker Queue topic: ‘spam_api’ nsqlookupd

    and registers it with nsqlookupd. (When a worker needs all nsqd instances providing that topic)
  109. Topic Lookup Web App Database Worker nsqd topic: 'spam_counter' nsqd

    topic: 'spam_api' nsqlookupd topic: 'spam_api'? it asks nsqlookupd, which replies with the IP addresses of all nsqd instances publishing that topic. (So what is a channel?)
  110. Channel Registration Web App Database Worker nsqd topic: 'spam_counter' nsqd

    topic: 'spam_api' channel: 'spam_counter' When a queuereader connects to nsqd for a particular topic, it registers itself under a channel name, thereby creating it. (Channels are simply names for logical groups of consumers.)
  111. Cross-Town Traffic nsqd topic: 'spam_api' Worker Worker Worker channel: 'spam_counter'

    But channels are important because messages are divided by # of subscribers to a channel; this allows horizontal scaling of queuereaders. (Each channel receives)
  112. Channels nsqd topic: 'spam_api' Worker Worker Worker channel: 'spam_counter' Worker

    channel: 'nsq_to_file'' a full copy of all messages published in the topic. (So how do you)
  113. nsqadmin Each topic provides sparklines of queue depth, number of

    messages, and rate per minute. (Clicking into a topic)
  114. • settings.py • <service>_api.py • queuereader_<service>.py • README.md /<service> Queuereaders

    are part of streaming architecture that implements the worker. The main queuereader is also a (Tornado executable)
  115. if  __name__  ==  "__main__":          tornado.options.parse_command_line()  

           logatron_client.setup()                    Reader(                  topic=settings.get('nsqd_output_topic'),                  channel='queuereader_spam_metrics',                  validate_method=validate_message,                  message_handler=count_spam_actions,                  lookupd_http_addresses=settings.get('nsq_lookupd')        )          run() /<service>/queuereader_<service>.py We looked at the topic, channel and nsqlookupd settings earlier, now I want to direct your attention to (validate method)
  116. if  __name__  ==  "__main__":          tornado.options.parse_command_line()  

           logatron_client.setup()                    Reader(                  topic=settings.get('nsqd_output_topic'),                  channel='queuereader_spam_metrics',                  validate_method=validate_message,                  message_handler=count_spam_actions,                  lookupd_http_addresses=settings.get('nsq_lookupd')        )          run() /<service>/queuereader_<service>.py which determines whether a given message in the topic is one we want to handle or not. (If True)
  117. def  validate_message(message):          if  message.get('o')  ==  '+'

     and  message.get('l'):                  return  True          if  message.get('o')  ==  '-­‐'  and  message.get('l')\            and  message.get('bl'):                     return  True          return  False /<service>/queuereader_<service>.py we pass the message to the message handler function, if False we skip the message.
  118. if  __name__  ==  "__main__":          tornado.options.parse_command_line()  

           logatron_client.setup()                    Reader(                  topic=settings.get('nsqd_output_topic'),                  channel='queuereader_spam_metrics',                  validate_method=validate_message,                  message_handler=count_spam_actions,                  lookupd_http_addresses=settings.get('nsq_lookupd')        )          run() /<service>/queuereader_<service>.py The message_handler setting specifies the function to call with the message.
  119. def  count_spam_actions(message,  nsq_msg):          key_section  =  statsd_keys[message['o']]

             key  =  key_section.get(message['l'],  key_section['default'])          statsd.incr(key)                    if  key  ==  'remove_by_manual':                  key_section  =  statsd_keys['-­‐manual']                  key  =  key_section.get(message['bl'],  key_section['default'])                  statsd.incr(key)                    return  nsq_msg.finish() /<service>/queuereader_<service>.py which in this case just counts the number of spam encodes, and then (finishes)
  120. def  count_spam_actions(message,  nsq_msg):          key_section  =  statsd_keys[message['o']]

             key  =  key_section.get(message['l'],  key_section['default'])          statsd.incr(key)                    if  key  ==  'remove_by_manual':                  key_section  =  statsd_keys['-­‐manual']                  key  =  key_section.get(message['bl'],  key_section['default'])                  statsd.incr(key)                    return  nsq_msg.finish() /<service>/queuereader_<service>.py the message. Calling finish() acks the message as handled to nsqd. Other options are to requeue the message, or requeue it with backoff.
  121. package  main   import  (     "flag"    

    "log"     "sync"     "bitly/eshistory/internal/eshistory"     "bitly/eshistory/internal/statsd"     "bitly/libbitly/exitchan"     "bitly/libbitly/nsqutils"     "bitly/libbitly/roles"     "bitly/libbitly/settings"   /eshistory/queuereader_bulk_action/queuereader_bulk_action.go We have our standard library imports and our Bitly and third-party imports
  122.   "bitly/libbitly/statsdproxy"     "github.com/bitly/go-­‐nsq"   )   var  (

        environment    =  flag.String("environment",  "dev",   "environment  to  execute  in")     settingsFile  =  flag.String("settings-­‐file",  "../ settings.json",  "path  to  the  settings.json  file")   )   type  ActionHandler  struct  {     Client            *eshistory.Client   /eshistory/queuereader_bulk_action/queuereader_bulk_action.go We set up the environment and settingsFile global variables
  123. type  ActionHandler  struct  {     Client      

         *eshistory.Client     producer        *nsq.Producer     statsdQueue  *statsdproxy.StatsdQueue   }   func  main()  {     flag.Parse()     settings.Load(*settingsFile,  *environment)     roles.LoadRoles("/bitly/local/conf/roles.json")     //     gpg  =   settings.OpenGpgSettingsFromSettingsFile(*settingsFile)   /eshistory/queuereader_bulk_action/queuereader_bulk_action.go We create an ActionHandler with an elasticsearch client and a statsd connection. This ActionHandler is not only a consumer of nsqd messages, but also a producer — it writes messages to other topics.
  124.   producer,  err  :=  nsq.NewProducer("127.0.0.1:4150",   nsq.NewConfig())     if

     err  !=  nil  {       log.Fatalf("Failed  creating  producer  %s",  err)     }     var  wg  sync.WaitGroup     wg.Add(1)   /eshistory/queuereader_bulk_action/queuereader_bulk_action.go Here we set up the producer connection and the wait group.
  125.   client  :=  &eshistory.Client{       Conn:    

            eshistory.InitElasticsearchConnection("eshistory_elasticsearch "),       IndexName:  settings.GetString("es_index_name"),       IndexType:  settings.GetString("es_index_type"),     }     handler  :=  &ActionHandler{       Client:            client,       producer:        producer,       statsdQueue:  statsd.InitStatsd(),     }   /eshistory/queuereader_bulk_action/queuereader_bulk_action.go Initialize the elasticsearch client and instantiate the handler
  126.   nsqutils.RunConsumer(nsqutils.Options{       Topic:        

         "bulk_action",       Channel:          "queuereader_bulk_action",       Handler:          handler,       MaxInFlight:  settings.GetInt("max_in_flight"),       ExitChan:        exitchan.New(),       WaitGroup:      &wg,       UserAgent:      "queuereader_bulk_action",     })     wg.Wait()   } /eshistory/queuereader_bulk_action/queuereader_bulk_action.go Then we tie it all together by creating and running a consumer of the "bulk_action" topic. Our handler logic is implemented in (bulk_action_sq.go)
  127. package  main   import  (     "encoding/json"    

    "log"     "bitly/eshistory/internal/eshistory"     "bitly/eshistory/internal/messages"     "bitly/libbitly/settings"     "github.com/bitly/go-­‐nsq"     "github.com/deckarep/golang-­‐set"   )   /eshistory/queuereader_bulk_action/bulk_action_sq.go Which starts out with imports
  128. //HandleMessage  implements  handling  NSQ  bulk_action  messages   func  (h  *ActionHandler)

     HandleMessage(m  *nsq.Message)  error  {     var  msg  messages.BulkAction     if  err  :=  json.Unmarshal(m.Body,  &msg);  err  !=  nil  {       log.Printf("Error:  unable  to  decode  json,  skipping:  %s",   m.Body)       return  nil     }     if  msg.Action  ==  ""  {       log.Printf("Error:  msg.Action  is  blank  for  message  %s",   m.Body)       return  nil     }     log.Printf("Got  message  with  action:  %s",  msg.Action)   /eshistory/queuereader_bulk_action/bulk_action_sq.go HandleMessage handles incoming "bulk_action" messages by deserializing the JSON, (building)
  129.   //  Get  query  string  from  message     queryTerms

     :=  eshistory.RegexSearch{       User:                    msg.User,       Archived:            msg.Archived,       Private:              msg.Private,       CustomBitlink:  msg.CustomBitlink,       CreatedBefore:  msg.CreatedBefore,       CreatedAfter:    msg.CreatedAfter,       ModifiedAfter:  msg.ModifiedAfter,       SearchTags:        msg.SearchTags,       SearchTerm:        msg.SearchTerm,       Keyword:              msg.Keyword,     }   /eshistory/queuereader_bulk_action/bulk_action_sq.go the search query,
  130.   query  :=  eshistory.SearchRegexQuery(queryTerms)     //  Submit  search  to

     eshistory     links,  err  :=  h.SearchLinks(msg.User,  query)     if  err  !=  nil  {       log.Printf("Error  '%s'  retrieving  links  for  user  %s  with  query   %s",  err,  msg.User,  query)       return  err     }     var  action  string     var  historyMsg  messages.HistoryMessage     for  _,  link  :=  range  links  {   /eshistory/queuereader_bulk_action/bulk_action_sq.go getting all the encodes from elasticsearch by calling SearchLinks
  131.   var  action  string     var  historyMsg  messages.HistoryMessage  

      for  _,  link  :=  range  links  {       switch  msg.Action  {       case  "archive",  "set_private":         action  =  "edit"       case  "edit_tags":         action  =  "edit_tags"         //      *  Calculate  new  set  of  tags  per  link         var  iTags  []interface{}         if  link.Tags  !=  nil  {           iTags  =  toGenericSlice(*link.Tags)         }   /eshistory/queuereader_bulk_action/bulk_action_sq.go and for each encode, we create a message for the "history" topic with the appropriate action to take — tagging, archiving, or making the encode private.
  132.       //      *  If  too  many

     tags  per  link,  submit  a  new  user_event   message  with  link  details         if  len(finalTags)  >  settings.GetInt("limit_tags_per_user")   {           if  err  =  h.SendEventMessage(msg,  *link.UserHash,   startingTags);  err  !=  nil  {             log.Printf("Error  sending  event  message:  %s",  err)             return  err           }           //      *  and  skip  to  next  link           continue         }       default:         continue   /eshistory/queuereader_bulk_action/bulk_action_sq.go If tagging, we check for exceeding the limits of tags for the user, and submit an "event" message via SendEventMessage.
  133.     //Submit  a  message  to  history  queue    

      historyMsg  =  messages.HistoryMessage{         Action:          action,         User:              msg.User,         UserHash:      *link.UserHash,         GlobalHash:  *link.GlobalHash,         LongURL:        *link.LongURL,         Timestamp:    *link.Timestamp,         EditParams:  msg.EditParams,         AddTags:        msg.AddTags,         RemoveTags:  msg.RemoveTags,       }       hmsg,  err  :=  json.Marshal(historyMsg)       if  err  !=  nil  {   /eshistory/queuereader_bulk_action/bulk_action_sq.go We build the message to submit to the "history" topic,
  134.     if  err  =  h.producer.Publish("history",  hmsg);  err  !=  nil

     {         log.Printf("Error  publishing  to  nsq  %s",  err)         return  err       }     }     return  nil   }   /eshistory/queuereader_bulk_action/bulk_action_sq.go and publish it.
  135. //SearchLinks  returns  all  links  that  match  the  given  query  and

     user   func  (h  *ActionHandler)  SearchLinks(user  string,  query  string)   ([]eshistory.Link,  error)  {     pageSize  :=  settings.GetInt("query_page_size")     links,  countResults,  err  :=  h.Client.SearchRegex(user,  query,   pageSize,  0,  "",  "")     if  err  !=  nil  {       log.Fatalf("Search  error  '%s'  for  user  %s:  %s",  err,  user,   query)       return  nil,  err     }     h.statsdQueue.Increment("searchlinks_req")     h.statsdQueue.IncrementBy("searchlinks_req_results",   int32(countResults))   /eshistory/queuereader_bulk_action/bulk_action_sq.go The SearchLinks method finds and returns all the encodes that match the query submitted from HandleMessage. (SendEventMessage)
  136. //SendEventMessage  submits  a  UserEventMessage  to  the  'event'   topic  

    func  (h  *ActionHandler)  SendEventMessage(b   messages.BulkAction,  uhash  string,  tags  []string)  error  {     userEventMsg  :=  messages.UserEventMessage{       Action:              "exceeded_limit_tags_per_user",       Timestamp:        b.Timestamp,       CurrentUser:    b.User,       Link:                  uhash,       AddTags:            b.AddTags,       RemoveTags:      b.RemoveTags,       StartingTags:  tags,       RemoteIP:          b.RemoteIP,     }   /eshistory/queuereader_bulk_action/bulk_action_sq.go defines the helper method on the ActionHandler to submit a user event message to the 'event' topic. We build a new NSQ message
  137.   u,  err  :=  json.Marshal(userEventMsg)     if  err  !=

     nil  {       log.Printf("Error  marshalling  user  event  message  to  JSON   bytes:  %#v",  userEventMsg)       return  err     }     if  err  =  h.producer.Publish("event",  u);  err  !=  nil  {       log.Printf("Error  publishing  to  nsq  %s",  err)       return  err     }     return  nil   }   /eshistory/queuereader_bulk_action/bulk_action_sq.go
  138.   u,  err  :=  json.Marshal(userEventMsg)     if  err  !=

     nil  {       log.Printf("Error  marshalling  user  event  message  to  JSON   bytes:  %#v",  userEventMsg)       return  err     }     if  err  =  h.producer.Publish("event",  u);  err  !=  nil  {       log.Printf("Error  publishing  to  nsq  %s",  err)       return  err     }     return  nil   }   /eshistory/queuereader_bulk_action/bulk_action_sq.go marshal it to JSON, and publish it to nsqd. Next, let's take a look at the included quality of life (utilities)
  139. Features & Guarantees (aka Trade-Offs) Distributed, No SPOF || Horizontally

    Scalable || TLS || statsd integration || Easy to Deploy || Cluster Administration & Quality of Life utilities
  140. Messages NOT Durable If an nsqd instance goes down, its

    messages go with it. You can tweak how much memory it is allowed to use, and messages over that mark get written to disk, but setting memory to zero is not a realistic strategy.
  141. Delivered at least once A message may be delivered and

    acted upon, but not acknowledged by the client. In these cases, the message would get requeued by nsqd.
  142. Eventually-Consistent Discovery If an nsqd instance goes down, nsqlookupd will

    notice. When it comes back up, nsqlookupd will eventually notice. If the nsqlookupd instances are partitioned from each other, one can know about an nsqd instance the others do not. (Some notes and then questions)
  143. Be a Linux Unicorn: From Casual User to Kernel Hacker

    Georgi Knox Breakout 2, 1:45pm My colleague is presenting
  144. Polar Bear - https://www.flickr.com/photos/ucumari/6885713416/ Server Room - https://www.flickr.com/photos/89228431@N06/11285592553/ Monolith -

    https://www.flickr.com/photos/pictures_of_england/5449218146/ Gravel - https://www.flickr.com/photos/ivan_herman/5991136992/ Spaghetti - https://www.flickr.com/photos/jshj/824608884/ Python XRay - https://www.flickr.com/photos/thomashawk/222703559/ Tornado - https://www.flickr.com/photos/indigente/798304 Gopher - https://golang.org/doc/gopher/appenginegophercolor.jpg Plush Gophers - http://photos3.meetupstatic.com/photos/event/c/5/6/e/highres_358490542.jpeg Split Gopher - http://engineroom.teamwork.com/content/images/2014/Aug/b-2.png Command Key - https://www.flickr.com/photos/klash/3175479797 iPhone6 Event - https://www.flickr.com/photos/notionscapital/15067798867 Wait for iPhone - https://www.flickr.com/photos/josh_gray/662814907 NSQ Logo - http://nsq.io All other photos by T. Peter Herndon Photo Credits