设计

这部分是采用gunicorn的英文文档,同时加入自己的理解,源代码,以及参考资料等等

服务器模型

gunicorn是基于”pre-fork worker”模型,这就意味着有一个中心主控master进程,由它来管理一组worker进程。这个主控进程并不知晓任何客户端,所有的请求和响应都完全是由多个worker进程来处理

解释pre-fork

pre-fork服务器和fork服务器相似,通过一个单独的进程来处理每条请求。

不同的是,pre-fork服务器会通过预先开启大量的进程,等待并处理接到的请求。

由于采用了这种方式来开启进程,服务器并不需要等待新的进程启动而消耗时间,因而能够以更快的速度应付多用户请求。

另外,pre-fork服务器在遇到极大的高峰负载时仍能保持良好的性能状态。这是因为不管什么时候,只要预先设定的所有进程都已被用来处理请求时,服务器仍可追加额外的进程。

缺点是,当遇到高峰负载时,由于要启动新的服务器进程,不可避免地会带来响应的延迟。

主控master进程

主控master进程,就是一个简单的循环,用来不断侦听不同进程信号并作出不同的动作,仅此而已。它通过一些信号,诸如TTIN, TTOU, 和CHLD等等, 管理着那些正在运行的worker进程。

TTIN 和 TTOU信号是告诉主控master进程增加或减少正在运行的worker数量。

CHLD信号是在一个子进程已经中止之后,由主控master进程重启这个失效的worker进程。

我们看几段代码,在gunicorn/arbiter.py中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Arbiter(object):
    """
    Arbiter maintain the workers processes alive. It launches or
    kills them if needed. It also manages application reloading
    via SIGHUP/USR2.
    """

    # A flag indicating if a worker failed to
    # to boot. If a worker process exist with
    # this error code, the arbiter will terminate.
    WORKER_BOOT_ERROR = 3

    START_CTX = {}

    LISTENER = None
    WORKERS = {}
    PIPE = []

    # I love dynamic languages
    SIG_QUEUE = []
    SIGNALS = map(
        lambda x: getattr(signal, "SIG%s" % x),
        "HUP QUIT INT TERM TTIN TTOU USR1 USR2 WINCH".split()
    )
    SIG_NAMES = dict(
        (getattr(signal, name), name[3:].lower()) for name in dir(signal)
        if name[:3] == "SIG" and name[3] != "_"
    )

    def __init__(self, app):
        os.environ["SERVER_SOFTWARE"] = SERVER_SOFTWARE

        self.setup(app)

        self.pidfile = None
        self.worker_age = 0
        self.reexec_pid = 0
        self.master_name = "Master"

在上面代码中的第23行中,列举了相应的多个信号:

  • HUP,重启所有的配置和所有的worker进程
  • QUIT,正常关闭,它会等待所有worker进程处理完各自的东西后关闭
  • INT/TERM,立即关闭,强行中止所有的处理
  • TTIN,增加一个worker进程
  • TTOU,减少一个worker进程
  • USR1,重新打开由master和worker所有的日志处理
  • USR2,重新运行master和worker
  • WINCH,正常关闭所有worker进程,保持主控master进程的运行

下面是针对不同信号的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def handle_chld(self, sig, frame):
    "SIGCHLD handling"
    self.wakeup()

def handle_hup(self):
    """\
    HUP handling.
    - Reload configuration
    - Start the new worker processes with a new configuration
    - Gracefully shutdown the old worker processes
    """
    self.log.info("Hang up: %s", self.master_name)
    self.reload()

def handle_quit(self):
    "SIGQUIT handling"
    raise StopIteration

def handle_int(self):
    "SIGINT handling"
    self.stop(False)
    raise StopIteration

def handle_term(self):
    "SIGTERM handling"
    self.stop(False)
    raise StopIteration

def handle_ttin(self):
    """\
    SIGTTIN handling.
    Increases the number of workers by one.
    """
    self.num_workers += 1
    self.manage_workers()

def handle_ttou(self):
    """\
    SIGTTOU handling.
    Decreases the number of workers by one.
    """
    if self.num_workers <= 1:
        return
    self.num_workers -= 1
    self.manage_workers()

def handle_usr1(self):
    """\
    SIGUSR1 handling.
    Kill all workers by sending them a SIGUSR1
    """
    self.kill_workers(signal.SIGUSR1)
    self.log.reopen_files()

def handle_usr2(self):
    """\
    SIGUSR2 handling.
    Creates a new master/worker set as a slave of the current
    master without affecting old workers. Use this to do live
    deployment with the ability to backout a change.
    """
    self.reexec()

def handle_winch(self):
    "SIGWINCH handling"
    if os.getppid() == 1 or os.getpgrp() != os.getpid():
        self.log.info("graceful stop of workers")
        self.num_workers = 0
        self.kill_workers(signal.SIGQUIT)
    else:
        self.log.info("SIGWINCH ignored. Not daemonized")

同步workers

大多数情况下,采用的worker类型是同步方式,也就是说一次仅处理一个请求。这种模型方式是最简单的,因为期间发生的任何错误最多只影响到一个请求。

尽管下面我们描述的内容,也是一次处理一个请求,但实际上在编写应用的时候,是加一些条件的。(??)

异步workers

可用的异步workers,主要是基于greenlets软件包(通过eventlet和gevent)。greenlet是用python来实现的协程方式(cooperative multi-threading)。通常情况下,我们编写的应用代码不需要作出什么改变,就能使用上这些异步workers的特性的。

tornado workers

还有一个可以用上的worker,是tornado worker,它是用在那些采用tornado框架的程序上。尽管tornado worker也可以用于wsgi程序上,但是这不是一个推荐的做法。

选择worker进程类型

默认的同步worker在cpu和带宽方面会消耗资源的。这就意味着你的应用不可能无节制地做任何事。例如,互联网上的一个请求,必须要遵守这个准则。

这个资源绑定的条件,也就是为什么我们会在gunicorn默认配置前做一个缓冲的代理。如果你把同步worker,直接暴露在internet上,一个dos(Denial of Service)攻击就会给服务器不停地制造大流量无用数据,而是服务器无法正常提供服务。Slowloris,就是这样一个有趣的例子,专门用来做这事的。

有些情况可以考虑采用异步worker:

  • 需要长时间阻塞调用的应用,比如外部的web service
  • 直接给internet提供服务
  • 流请求和响应(是类似flv流么?)
  • 长轮询
  • Web sockets(WebSocket是HTML5规格中的一个非常重要的新特性,它的存在可以允许用户在浏览器中实现双向通信,实现数据的及时推送)
  • Comet(基于 HTTP 长连接的“服务器推”技术,是一种新的 Web 应用架构。基于这种架构开发的应用中,服务器端会主动以异步的方式向客户端程序推送数据,而不需要客户端显式的发出请求。Comet 架构非常适合事件驱动的 Web 应用,以及对交互性和实时性要求很强的应用)

启动多少个workers?

不要试图做这样的事,你预期多少个客户端就启用多少个worker。gunicorn只需要启用4–12个workers,就足以每秒钟处理几百甚至上千个请求了。

在处理请求时,gunicorn依靠操作系统来提供负载均衡。通常我们推荐的worker数量是:(2 x $num_cores) + 1,这个公式很简单,它是基于给定的核心处理器数量,在其他worker处理请求时,每个worker将从socket那进行读写操作。

很显然,你的硬件环境和应用将影响到worker数量。我们推荐先采用上述公式来安排,在应用启动之后,然后再通过TTIN和TTOU这两个信号来调整worker数量。

记住:太多的worker,肯定会在某一个时刻,让你的整个系统急剧降低性能。(只能意译了)