Gunicorn启动Thrift服务

来源:互联网 发布:python exchange 邮件 编辑:程序博客网 时间:2024/05/17 01:42

用Gunicorn启动Thrift服务

  开始写博客,记录,总结,分享。
  今天写一下关于Gunicorn和thrift的使用,也是最近做项目时用到的技术。关于thrift,网上都有很多介绍,不必多说。thrift支持很多语言,我们现在需要用到python做一些服务的事情,所以下面主要说的是thrift server端的内容。虽说是针对thrift的内容,但是thrift只是问题所在,主要的解决方式是使用gunicorn,所以后面说的主要是gunicorn的内容。

Thrift

  Thrift里面有介绍各种server类型以及如何启动,类似于以下这种(摘自于thrift/tutorial/py源码):

    handler = CalculatorHandler()    processor = Calculator.Processor(handler)    transport = TSocket.TServerSocket(port=9090)    tfactory = TTransport.TBufferedTransportFactory()    pfactory = TBinaryProtocol.TBinaryProtocolFactory()    server = TServer.TSimpleServer(processor, transport, tfactory, pfactory)    server.serve()

  Thrift支持多线程的server用于提高并发处理能力,如 TServer.TThreadedServer 和 TServer.TThreadPoolServer。但仅这些离实际的分布式系统使用还不够,还需做一些工作。受限于GIL,python多线程并不能很好的利用多核CPU。可以对此做测试,即使用TThreadPoolServer启动server,在我8核电脑环境下,cpu使用率也上不了100%,好尴尬啊。
  对于高并发后台服务,还需要考虑以下问题:
- 集群下如何部署启动;
- 服务挂了如何自动恢复以保持可用;
- client端如何找到server地址及端口,也就是服务发现的问题。
  Thrift并没有在这方面做什么内容,或者说这些并不算它应包含的范畴之内,但是实际的后台应用都需要涉及这些内容。

Gunicorn

  还好有gunicorn!
  如果您有“gunicorn是什么”等类似的问题,还是请先自行百度吧。为什么用gunicorn,它使用了master worker模型,这是一个多进程并发模型,解决python多线程的困难啊!!!另外,对于python web,gunicorn基本上是必需品。
  那么,gunicorn + thrift?在github早有人实现了:gunicorn_thrift(https://github.com/eleme/gunicorn_thrift,饿了么的项目,赞)。
  我是从gunicorn_thrift的代码开始,认识到了gunicorn。。。有点和别人轨迹不一样啊,囧。看gunicorn_thrift的代码,主要是实现了thrift的workers,剩下的主体还都是gunicorn的逻辑,所以把gunicorn过了一遍。
  下面主要讲讲gunicorn原理及结构,最关键的是咋用。看网上都说推荐看它的源码,写的非常好。

  • master-worker模型

      就像我刚刚说的,gunicorn使用了master worker模型,一个主进程master负责接收信号,管理worker进程,真正监听处理网络事件的是worker进程。
      master维护worker进程的数量,有worker挂掉了,master能够自动新建一个worker进程。master还负责接收信号,就是 un*x系统里的信号signal(SIGTERM,SIGINT,SIGHUP,SIGQUIT,SIGTTIN,SIGTTOU等),用于处理退出逻辑,worker数量增减等。另外,socket是在master创建的,这也意味着,worker进程继承了socket,每个worker都会监听端口请求,但是一个请求只会被一个worker处理。
      worker则是真正实现了用户定义的worker class。gunicorn提供了同步sync和异步async的eventlet、gevent以及tornado、gthread、gaiohttp等worker种类。worker进程接收(accept),解析(parse),处理(handle)客户端发起的请求(request),真正对请求作出响应(response)的是用户使用的app应用程序,通常就是web应用定义的app。由此可以看出,worker其实相当于对app进行了一次封装,所有client的请求先经过gunicorn,由worker进程接收,再交给app进行响应。
      看下源码(部分主要结构,精简了一下):

class Arbiter(object):    def run(self):        # 程序入口,在master进程中,start创建socket,manage管理worker进程        self.start()        self.manage_workers()    def start(self):        self.LISTENERS = create_sockets()   # 在master中创建socket,监听端口    def manage_workers(self):        self.spawn_workers()   # master进程管理worker    def spawn_workers(self):        self.spawn_worker()    def spawn_worker(self):        # 创建worker类的对象,worker_class就是用户选择的worker种类,包括sync、gevent等        worker = self.worker_class(***, self.LISTENERS, self.app, ***)        pid = os.fork()   # fork出worker进程         worker.init_process()   # worker进程初始化,开始运行

  说了这么多,master-worker模型有啥好处?
  多进程当然比单进程能支撑更多请求,而且由于是多进程模型,每个进程各自处理请求,某个进程挂了不会影响其他进程的服务。而且master会随时监控worker进程的状态,具有自动恢复工作进程的能力。

  • Settings
      毕竟大多数时候,我们更需要关注如何使用gunicorn。在官网或其他介绍性的文章里,都有不少对使用gunicorn的介绍。我这里就简单一些,并且把我认为值得注意的地方提一下。
gunicorn --workers=2 test:app

  这是官方找出的使用方法,当然可选的参数设置还有:

-c CONFIG, --config=CONFIG, 指定config文件-b BIND, --bind=BIND, 指定绑定端口-k WORKERCLASS, --worker-class=WORKERCLASS,指定worker-class类型...

  网上列举了大概6、7个参数设置说明,但是,但是,但是,如果你看gunicorn的源码,从config里面可以找到60+个Setting子类,简直了。不过这些参数设置可以进行归类,包括以下几个方面:
  Config File、Server Socket、Worker Processes、Security、Debugging、Server Mechanics、Logging、Process Naming、Django、Server Hooks、SSL
  当然,有些参数设置并不常用,但是,还是说一下需要注意的一些内容吧:
  首先,建议启动gunicorn的时候,都把配置写到用户自定义的conf.py文件里,毕竟这么多内容,都写在命令行,不容易看也不容易改啊~~~,所以最简单的就是,gunicorn -c conf.py test:app,然后所有其他选项都写在conf.py里。
  然后,对于conf文件,如果有,程序在启动的时候会执行用户配置的conf.py文件load_config_from_file,然后对配置做一些参数检查等操作。建议看看gunicorn的源码config.py,里面的参数设置类都继承自Setting类,并且都用SettingMeta(元类MetaClass)一个个给__new__出来的,动态创建设置类,并控制这些类创建时的行为,简直是艺术!
  最后,提一下Server Hooks配置。从源码可以看到,Setting的子类里面,有一部分定义了一些方法,但是没有实现。刚一开始我很奇怪,函数里都写着pass,怎么用啊。后来查看gunicorn的example才明白。以when_ready例。在Arbiter的start里面,有调用到cfg.when_ready方法,明显就是调用setting里配置的方法。对于这些server hooks,可以在自己定义的conf文件里重新实现这些方法,用来控制自己需要的一些行为。下面是gunicorn的一个example片段:

bing = '127.0.0.1:8000'workers = 1worker_class = 'sync'loglevel = 'info'def post_fork(server, worker):    server.log.info("Worker spawned (pid : %s)", worker.pid)def when_ready(server):    server.log.info("Server is ready. Spawning workers")

  可以看到,对于server hooks的配置,可以由用户在conf.py中自己定义具体实现,来控制程序的行为。这一点有时非常重要,后面会讲到!!!
  

Gunicorn on Thrift

  前面提到的那个开源项目gunicorn_thrift登场了。
  用gunicorn启动thrift的使用方法,如同gunicorn启动其它server的方式一样,就不再赘述了。不同的就是worker类的选择,选好thrift实现的worker类,选好thrift自身包含的Protocol(协议)、Transport(传输层)等参数,其它的和gunicorn都一致。下面主要说一下,对于上面thrift part部分提到的问题,用gunicorn_thrift如何解决。
  对于故障自动恢复问题,主要是关注gunicorn的master进程监控的问题。因为对于worker进程来讲,master会自动监控worker进程,所以不需要再另外关注。进程监控,有很多工具,如god,supervisor等。
  在集群上部署(或者说分布式服务),说白了就是在多个服务器上部署相同的service,对外提供一致服务。其实单机和多机的部署是一样的,主要的区别是多服务器下,client端请求如何指向到可用的service地址,将请求分散到不同机器上。对于web应用来说,也就是如何做路由和负载均衡的问题,对于thrift server,同样也有这个问题。其实对于这个问题,可以拆分成两个问题,一个是动态提供集群的可用服务器地址,另一个是在此基础上做负载均衡。对于第一个问题,也就是服务发现的问题。
  服务发现,也就是集群管理(个人理解),有很多解决的利器,常用的有zookeeper、etcd等,下面以zookeeper举例。当利用集群提供服务,必须知道每台服务器的状态,如果有一个机器上的server挂掉,需要让client端知道并且把请求分流到其它的server端;如果新加一台server到集群中,同样的道理,client端请求也可以知道并且把请求流量加到新的服务节点上。如果利用zookeeper做服务发现,一般是用server在zookeeper上创建EPHEMERAL(临时)节点,这样当server挂掉的时候,临时节点也自动删除,能够感知server的存在。
  gunicorn_thrift提供了这种逻辑!

class ThriftApplication(Application):    def run(self):        if self.cfg.service_register_cls:            service_register_cls = utils.load_obj(self.cfg.service_register_cls)            self.service_watcher = service_register_cls(self.cfg.service_register_conf, self)            instances = []            for i in self.cfg.address:                port = i[1]                instances.append({'port':{'main':port}, 'meta': None, 'state': 'up'})            self.service_watcher.register_instances(instances)        super(ThriftApplication, self).run()

  在Application正式运行之前,如果实现了注册类,那么进行注册的逻辑。但是,gunicorn_thrift只是提供了逻辑,没有具体实现service_register_cls(注册类)、service_register_conf(注册配置)和register_instances(注册逻辑),这些都交给了使用者自己实现。我自己实现了一下基于zookeeper的注册逻辑,感兴趣的同学可以看下我的代码 https://github.com/wangyibo360/gunicorn_thrift(当然源分支没有把我的代码merge,因为注册的实现有太多方式,zookeeper只是一种)。
  在我的代码实现里,把server的ip和端口写到了zookeeper的临时节点里,供对外查找服务地址使用。当然这只是一个简易版,真正在公司使用可能还需复杂一些,除ip和port外,节点路径可能包括${cluster},${service},${package},${version}信息等,视业务具体情况而定。
  还有一点我有些异议,在gunicorn_thrift项目里,注册是写在run之前的,对此,我可能更倾向于写在server hooks里的when_ready函数中。因为run之前注册好服务地址,对于客户端就视为可见了。但是此时application并没有执行逻辑,也就是app并没有做好服务的准备,master、worker都没有运行起来,如果此时有故障挂掉,那么将不能对client提供服务,而请求还不断向这个地址发送。以上是我个人的一点理解,也有可能我理解的有误。
  当然,除了实现注册逻辑之外,我还实现了主动取消注册的逻辑。zookeeper的临时节点因为server进程挂掉而自动删除,但是我发现有一定的时间滞后性。在滞后的这段时间,有可能还有请求向这个server地址发送,还有一种可能是在监控进程发现server挂掉滞后自动修复,重新实现地址注册,此时原有的临时节点还没有删除掉,导致新的无法实现注册,而临时节点在滞后一定时间后删除,导致服务已经restart了,但是没有注册到zookeeper上,无法提供服务。所以,在on_exit中,我实现了取消注册的逻辑。on_exit是在halt中调用的,目的就是为了实现客户的程序退出逻辑,因此写在这里比较合适。halt是master在检测到退出信号量时做出的动作,包括SIGTERM、SIGINT、SIGQUIT等。
  
  到此,可以先告一段落了。用gunicorn实现对thrift server的启动运行就介绍到这里。总结一下,用gunicorn能带来性能上的好处,同时能提供服务发现等方面的功能,可以说是有益的补充了thrift在这方面的不足。
  最后,在上文中我提到,服务发现,同时还提到了负载均衡。这方面还需要客户端去实现,这方面以后有机会再说吧。

2 0
原创粉丝点击