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

Macroscaling via Microservices

Macroscaling via Microservices

Bitly provides functionality in our API and clients by implementing microservices, a trendy new word that describes a service-oriented architecture built using some common-sense guidelines. We use the Tornado web framework to implement these services, along with a liberal sprinkling of other languages and tools. I will illustrate our system architecture, our patterns and best practices for building a service, and show how to create a scalable application using these techniques.

Peter Herndon

August 16, 2014
Tweet

More Decks by Peter Herndon

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. 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
  4.                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
  5. 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
  6. import  tornado.options   from  libbitly.NSQReader  import  Reader,  run   from

     libbitly  import  logatron_client   from  libbitly  import  statsd_proxy  as  statsd   ! import  settings /<service>/queuereader_<service>.py, 1 of 4
  7. 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, 2 of 4
  8. 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, 3 of 4
  9. 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, 3 of 4
  10. 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, 4 of 4
  11. 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, 4 of 4
  12. 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, 4 of 4
  13. 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, 4 of 4
  14. 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
  15. 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
  16. 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, 1 of 2
  17. 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, 1 of 2
  18. 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, 1 of 2
  19. 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, 1 of 2
  20. 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, 1 of 2
  21.        @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, 2 of 2
  22.        @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, 2 of 2
  23. 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
  24. Shards Here, Shards There Web App Web App Web App

    Cache Database Database Database Database Database Database
  25. Workin’ On a Chain(ed) Gang Web App Database Worker Queue

    Web App Database Worker Queue Web App Database Worker Queue
  26. 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
  27. Where Am I Again? Web App Database Worker nsqd topic:

    'spam_counter' nsqd topic: 'spam_api' nsqlookupd topic: 'spam_api'?
  28. Channeling the Ghost nsqd topic: 'spam_api' Worker Worker Worker channel:

    'spam_counter' Worker channel: 'nsq_to_file''
  29. 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
  30.                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
  31. Unnamed Social Event!! 7PM - 10PM Tonight!! 85 5th Avenue,

    New York, NY 1003! ! ! Bitly, Continuum Analytics, untapt