Gunicorn prefork流程

prefork

python 中怎么实现的?

用的知识,和简单的思路。

下面是阅读Gunicorn源码之后,实现的一个简单的 pre-fork 程序。

  1. # -*- coding: utf-8 -*-
  2. #master-slaves.py python2.7.x
  3. #orangleliu@gmail.com
  4. '''
  5. 简单的模拟pre-fork模式,master进程控制多个子进程
  6. 这里实现这么几个信号
  7. INT ctrl+c 退出
  8. TTIN 增加一个worker
  9. TTOU 减少一个worker
  10. '''
  11. import os
  12. import sys
  13. import signal
  14. import time
  15. import random
  16. class Worker(object):
  17. '''
  18. 子进程要实现一些特定的信号来响应外界和父进程的操作
  19. '''
  20. def run(self):
  21. while True:
  22. time.sleep(3)
  23. class Master(object):
  24. WORKERS = {}
  25. SIG_QUEUE = []
  26. SIGNALS = [getattr(signal, "SIG%s" % x)
  27. for x in "INT TTIN TTOU".split()]
  28. SIG_NAMES = dict(
  29. (getattr(signal, name), name[3:].lower()) for name in dir(signal)
  30. if name[:3] == "SIG" and name[3] != "_"
  31. )
  32. def __init__(self, worker_nums=2):
  33. self.worker_nums = worker_nums
  34. self.master_name = "Master"
  35. self.reexec_pid = 0
  36. def start(self):
  37. print "start master"
  38. self.pid = os.getpid()
  39. self.init_signals()
  40. def init_signals(self):
  41. [signal.signal(s, self.signal) for s in self.SIGNALS]
  42. signal.signal(signal.SIGCHLD, self.handle_chld)
  43. def signal(self, sig, frame):
  44. '''
  45. 普通的信号发生的时候,往信号队列增加一个信号
  46. '''
  47. if len(self.SIG_QUEUE) < 5:
  48. self.SIG_QUEUE.append(sig)
  49. def run(self):
  50. self.start()
  51. try:
  52. self.manage_workers()
  53. while True:
  54. # 如果不增加sleep 整个master进程就会进入几乎100 cpu的状态
  55. # 使用sleep的好处就是master的cpu消耗小很多,对于来自系统的给master的信号可以即使反馈
  56. time.sleep(1)
  57. sig = self.SIG_QUEUE.pop(0) if len(self.SIG_QUEUE) else None
  58. if sig is None:
  59. self.manage_workers()
  60. continue
  61. if sig not in self.SIG_NAMES:
  62. print "unknow signals:%s"%sig
  63. continue
  64. signame = self.SIG_NAMES.get(sig)
  65. handler = getattr(self, "handle_%s"%signame, None)
  66. if not handler:
  67. print "Unhandler signal: %s"%signame
  68. continue
  69. handler()
  70. except StopIteration:
  71. self.halt()
  72. except KeyboardInterrupt:
  73. self.halt()
  74. except SystemExit:
  75. pass
  76. except Exception as e:
  77. print e
  78. self.stop()
  79. sys.exit(-1)
  80. def handle_chld(self, sig, frame):
  81. '''
  82. 对于子进程退出SIGCHLD信号处理,防止产生大量僵尸进程
  83. '''
  84. self.reap_workers()
  85. def handle_int(self):
  86. '''
  87. ctrl+c 关闭master进程,先关闭子进程,然后抛出异常,自己退出
  88. '''
  89. self.stop()
  90. raise StopIteration
  91. def handle_ttin(self):
  92. '''
  93. 增加一个子进程
  94. '''
  95. print "add a worker"
  96. self.worker_nums += 1
  97. self.manage_workers()
  98. def handle_ttou(self):
  99. '''
  100. 减少一个子进程
  101. '''
  102. print "deincrease a worker"
  103. if self.worker_nums <= 1:
  104. return
  105. self.worker_nums -= 1
  106. self.manage_workers()
  107. def stop(self):
  108. '''
  109. 停止子进程 这里都当做SIGTERM来处理
  110. '''
  111. print 'stop workers'
  112. sig = signal.SIGTERM
  113. self.kill_workers(sig)
  114. self.kill_workers(signal.SIGKILL)
  115. def halt(self, exit_status=0):
  116. '''
  117. master 进程自杀
  118. '''
  119. print "master exit"
  120. self.stop()
  121. sys.exit(exit_status)
  122. def reap_workers(self):
  123. '''
  124. 这里的检测也是为了避免僵尸进程,否则大量资源无法释放
  125. 参考:http://www.cnblogs.com/mickole/p/3187770.html
  126. '''
  127. try:
  128. while True:
  129. #os.waitpid 收集僵尸子进程的信息,并把它彻底销毁后返回
  130. #这里的 -1 代表所有子进程
  131. #os.WNOHANG 如果没有子进程信息就立刻返回
  132. wpid, status = os.waitpid(-1, os.WNOHANG)
  133. if not wpid:
  134. break
  135. else:
  136. exitcode = status >> 8
  137. worker = self.WORKERS.pop(wpid, None)
  138. if not worker:
  139. continue
  140. except OSError as e:
  141. #errno.ECHILD 是没有子进程错误
  142. if e.error != errno.ECHILD:
  143. raise
  144. def manage_workers(self):
  145. '''
  146. workers 的健康检查,数量是否对齐等
  147. '''
  148. if len(self.WORKERS.keys()) < self.worker_nums:
  149. self.spawn_workers()
  150. workers = self.WORKERS.items()
  151. while len(workers) > self.worker_nums:
  152. (pid, _) = workers.pop(0)
  153. self.kill_worker(pid, signal.SIGTERM)
  154. def spawn_worker(self):
  155. worker = Worker()
  156. pid = os.fork()
  157. #master进程处理
  158. if pid != 0:
  159. self.WORKERS[pid] = worker
  160. return pid
  161. #worker进程处理
  162. worker_pid = os.getpid()
  163. try:
  164. worker.run()
  165. sys.exit(0)
  166. except SystemExit:
  167. raise
  168. except Exception as e:
  169. print "work error %s"%str(e)
  170. sys.exit(-1)
  171. def spawn_workers(self):
  172. for i in range(self.worker_nums - len(self.WORKERS.keys())):
  173. self.spawn_worker()
  174. #为什么要那么端时间的休眠
  175. time.sleep(0.1*random.random())
  176. def kill_workers(self, sig):
  177. worker_pids = list(self.WORKERS.keys())
  178. for pid in worker_pids:
  179. self.kill_worker(pid, sig)
  180. def kill_worker(self, pid, sig):
  181. try:
  182. os.kill(pid, sig)
  183. except OSError as e:
  184. print "kill worker error: %s"%str(e)
  185. if __name__ == "__main__":
  186. Master().run()

Gunicorn worker 类型

  • sync
  • gthread
  • eventlet
  • gevent
  • tornado

根据底层动作的原理可以将worker分成三种类型:

  1. sync:底层实际是每个请求一个process处理
  2. gthread:底层实际是每个请求一个thread处理
  3. eventlet/gevent/tarnado:底层则是利用异步IO让一个process在等待IO响应时继续处理下个请求

用 process 处理请求

使用 sync 类型的worker运行 CPU bound/IO bound的任务在性能上的表现

  1. # views.py
  2. from django.shortcuts import render
  3. from django.http import HttpResponse
  4. # Create your views here.
  5. import time
  6. def ioTask(request):
  7. time.sleep(2)
  8. return HttpResponse("IO bound task finish!\n")
  9. def cpuTask(request):
  10. for i in range(10000000):
  11. n = i * i * i
  12. return HttpResponse("CPU bound task finish!\n")

输出

  1. 08:31:40 (gunicorn_demo-bLt-GVNF) root@arch gdemo siege -c 2 -r 1 http://192.168.37.145/worker/io/ -v
  2. ** SIEGE 4.0.4
  3. ** Preparing 2 concurrent users for battle.
  4. The server is now under siege...
  5. HTTP/1.1 200 2.00 secs: 22 bytes ==> GET /worker/io/
  6. # 下面请求开始被阻塞
  7. HTTP/1.1 200 4.00 secs: 22 bytes ==> GET /worker/io/
  8. Transactions: 2 hits
  9. Availability: 100.00 %
  10. Elapsed time: 4.00 secs
  11. Data transferred: 0.00 MB
  12. Response time: 3.00 secs
  13. Transaction rate: 0.50 trans/sec
  14. Throughput: 0.00 MB/sec
  15. Concurrency: 1.50
  16. Successful transactions: 2
  17. Failed transactions: 0
  18. Longest transaction: 4.00
  19. Shortest transaction: 2.00
  20. 08:40:51 root@arch ~ siege -c 2 -r 1 http://192.168.37.145/worker/cpu/ -v
  21. ** SIEGE 4.0.4
  22. ** Preparing 2 concurrent users for battle.
  23. The server is now under siege...
  24. HTTP/1.1 200 0.97 secs: 23 bytes ==> GET /worker/cpu/
  25. # 下面请求开始被阻塞
  26. HTTP/1.1 200 2.12 secs: 23 bytes ==> GET /worker/cpu/
  27. Transactions: 2 hits
  28. Availability: 100.00 %
  29. Elapsed time: 2.12 secs
  30. Data transferred: 0.00 MB
  31. Response time: 1.54 secs
  32. Transaction rate: 0.94 trans/sec
  33. Throughput: 0.00 MB/sec
  34. Concurrency: 1.46
  35. Successful transactions: 2
  36. Failed transactions: 0
  37. Longest transaction: 2.12
  38. Shortest transaction: 0.97

这种类型的好处是错误隔离高,一个 process 挂掉只会影响该 process 当下服务的请求,而不会影响其他请求。

坏处则为 process 资源开销较大,开太多 worker 时对内存或 CPU 的影响很大,因此 并发concurrency 理论上限极低。

用 thread 处理请求

当 gunicorn worker type 用 gthread 时,可额外加参数 —thread 指定每个 process 能开的 thread 数量,此时 concurrency 的上限为 worker 数量乘以给个 worker 能开的 thread 数量。

如下 gunicorn 启动时开了一个 pid 为 595 的 process 来处理请求, thread 数量为 2,理论上每次只能处理二个请求:

  1. 09:05:31 (gunicorn_demo-bLt-GVNF) root@arch gdemo gunicorn -w 1 -k sync --thread=2 gdemo.wsgi -b 192.168.37.145:80
  2. [2018-06-24 09:05:41 +0800] [18464] [INFO] Starting gunicorn 19.8.1
  3. [2018-06-24 09:05:41 +0800] [18464] [INFO] Listening at: http://192.168.37.145:80 (18464)
  4. [2018-06-24 09:05:41 +0800] [18464] [INFO] Using worker: threads
  5. [2018-06-24 09:05:41 +0800] [18467] [INFO] Booting worker with pid: 18467
  6. [2018-06-24 09:19:59 +0800] [18464] [INFO] Handling signal: winch
  7. [2018-06-24 09:20:05 +0800] [18464] [INFO] Handling signal: winch

用 siege 分别对 IO bound task 和 CPU bound task 发出 4 个请求可以明显看到第三个请求以后才会被阻塞:

  1. 09:22:34 root@arch ~ siege -c 4 -r 1 http://192.168.37.145/worker/io/ -v
  2. ** SIEGE 4.0.4
  3. ** Preparing 4 concurrent users for battle.
  4. The server is now under siege...
  5. HTTP/1.1 200 2.01 secs: 22 bytes ==> GET /worker/io/
  6. HTTP/1.1 200 2.01 secs: 22 bytes ==> GET /worker/io/
  7. # 下面的请求开始被阻塞
  8. HTTP/1.1 200 4.02 secs: 22 bytes ==> GET /worker/io/
  9. HTTP/1.1 200 4.01 secs: 22 bytes ==> GET /worker/io/
  10. Transactions: 4 hits
  11. Availability: 100.00 %
  12. Elapsed time: 4.02 secs
  13. Data transferred: 0.00 MB
  14. Response time: 3.01 secs
  15. Transaction rate: 1.00 trans/sec
  16. Throughput: 0.00 MB/sec
  17. Concurrency: 3.00
  18. Successful transactions: 4
  19. Failed transactions: 0
  20. Longest transaction: 4.02
  21. Shortest transaction: 2.01
  22. 09:23:39 root@arch ~ siege -c 4 -r 1 http://192.168.37.145/worker/cpu/ -v
  23. ** SIEGE 4.0.4
  24. ** Preparing 4 concurrent users for battle.
  25. The server is now under siege...
  26. HTTP/1.1 200 2.00 secs: 23 bytes ==> GET /worker/cpu/
  27. HTTP/1.1 200 2.00 secs: 23 bytes ==> GET /worker/cpu/
  28. # 下面的请求开始被阻塞
  29. HTTP/1.1 200 3.97 secs: 23 bytes ==> GET /worker/cpu/
  30. HTTP/1.1 200 3.97 secs: 23 bytes ==> GET /worker/cpu/
  31. Transactions: 4 hits
  32. Availability: 100.00 %
  33. Elapsed time: 3.97 secs
  34. Data transferred: 0.00 MB
  35. Response time: 2.99 secs
  36. Transaction rate: 1.01 trans/sec
  37. Throughput: 0.00 MB/sec
  38. Concurrency: 3.01
  39. Successful transactions: 4
  40. Failed transactions: 0
  41. Longest transaction: 3.97
  42. Shortest transaction: 2.00

这种类型的 worker 好处是 concurrency 理论上限会比 process 高,坏处依然是 thread 数量,OS 中 thread 数量是有限的,过多的 thread 依然会造成系统负担。

用异步IO处理每个请求

当 gunicorn worker type 用 eventlet、gevent、tarnado 等类型时,每个请求都由同一个 process 处理,而当遇到 IO 时该 process 不会等 IO 回应,会继续处理下个请求直到该 IO 完成,理论上 concurrency 无上限。

以 gevent 为例,gunicorn 启动时开了一个 pid 为 733 的 process 来处理请求:

  1. 09:44:31 (gunicorn_demo-bLt-GVNF) root@arch gdemo gunicorn -w 1 -k gevent gdemo.wsgi -b 192.168.37.145:80
  2. [2018-06-24 09:47:35 +0800] [36301] [INFO] Starting gunicorn 19.8.1
  3. [2018-06-24 09:47:35 +0800] [36301] [INFO] Listening at: http://192.168.37.145:80 (36301)
  4. [2018-06-24 09:47:35 +0800] [36301] [INFO] Using worker: gevent
  5. [2018-06-24 09:47:35 +0800] [36304] [INFO] Booting worker with pid: 36304

用 siege 对 IO bound task 发出 10 个请求可以明显看到没有任何请求被阻塞

  1. 10:06:55 root@arch ~ siege -c 10 -r 1 http://192.168.37.145/worker/io/ -v
  2. ** SIEGE 4.0.4
  3. ** Preparing 10 concurrent users for battle.
  4. The server is now under siege...
  5. # 可以明显看到没有任何请求被阻塞
  6. HTTP/1.1 200 2.01 secs: 22 bytes ==> GET /worker/io/
  7. HTTP/1.1 200 2.00 secs: 22 bytes ==> GET /worker/io/
  8. HTTP/1.1 200 2.00 secs: 22 bytes ==> GET /worker/io/
  9. HTTP/1.1 200 2.00 secs: 22 bytes ==> GET /worker/io/
  10. HTTP/1.1 200 2.01 secs: 22 bytes ==> GET /worker/io/
  11. HTTP/1.1 200 2.01 secs: 22 bytes ==> GET /worker/io/
  12. HTTP/1.1 200 2.01 secs: 22 bytes ==> GET /worker/io/
  13. HTTP/1.1 200 2.01 secs: 22 bytes ==> GET /worker/io/
  14. HTTP/1.1 200 2.01 secs: 22 bytes ==> GET /worker/io/
  15. HTTP/1.1 200 2.01 secs: 22 bytes ==> GET /worker/io/
  16. Transactions: 10 hits
  17. Availability: 100.00 %
  18. Elapsed time: 2.02 secs
  19. Data transferred: 0.00 MB
  20. Response time: 2.01 secs
  21. Transaction rate: 4.95 trans/sec
  22. Throughput: 0.00 MB/sec
  23. Concurrency: 9.94
  24. Successful transactions: 10
  25. Failed transactions: 0
  26. Longest transaction: 2.01
  27. Shortest transaction: 2.00

但当面临 CPU bound 请求时,则会退化成用 process 处理请求一样,concurrency 上限为 worker 数量。如下用 siege 对 CPU bound task 发出 10 个请求,可以看到第二个请求以后就被阻塞:

  1. 10:07:38 root@arch ~ siege -c 10 -r 1 http://192.168.37.145/worker/cpu/ -v
  2. ** SIEGE 4.0.4
  3. ** Preparing 10 concurrent users for battle.
  4. The server is now under siege...
  5. HTTP/1.1 200 0.96 secs: 23 bytes ==> GET /worker/cpu/
  6. # 下面请求开始被阻塞
  7. HTTP/1.1 200 1.90 secs: 23 bytes ==> GET /worker/cpu/
  8. HTTP/1.1 200 2.89 secs: 23 bytes ==> GET /worker/cpu/
  9. HTTP/1.1 200 4.16 secs: 23 bytes ==> GET /worker/cpu/
  10. HTTP/1.1 200 5.40 secs: 23 bytes ==> GET /worker/cpu/
  11. HTTP/1.1 200 6.38 secs: 23 bytes ==> GET /worker/cpu/
  12. HTTP/1.1 200 7.34 secs: 23 bytes ==> GET /worker/cpu/
  13. HTTP/1.1 200 8.30 secs: 23 bytes ==> GET /worker/cpu/
  14. HTTP/1.1 200 9.51 secs: 23 bytes ==> GET /worker/cpu/
  15. HTTP/1.1 200 10.52 secs: 23 bytes ==> GET /worker/cpu/
  16. Transactions: 10 hits
  17. Availability: 100.00 %
  18. Elapsed time: 10.52 secs
  19. Data transferred: 0.00 MB
  20. Response time: 5.74 secs
  21. Transaction rate: 0.95 trans/sec
  22. Throughput: 0.00 MB/sec
  23. Concurrency: 5.45
  24. Successful transactions: 10
  25. Failed transactions: 0
  26. Longest transaction: 10.52
  27. Shortest transaction: 0.96

因此使用非同步类型的 worker 好处和坏处非常明显,对 IO bound task 的高效能,但在 CPU bound task 会不如 thread。

结论

当谈到效能时,必须考虑到使用情境。 gunicorn + 异步IO 效能就一定比较好的说法并不一定成立。

从上面的数据三种类型的 worker 都有其相对适合的场景:

  • 当需要稳定的系统时, 用 process 处理请求可以保证一个请求的异常导致程式 crash 不会影响到其他请求。
  • 当 web 服务内大部分都是 cpu 运算时,用 thread 可以提供不错的效能。
  • 当 web 服务内大部分都是 io 时,用非同步 io 可以达到极高的 concurrency 数量。

附录

名词解释

websocket

是一个新协议,跟http协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是http协议上的一种补充。

WebSocket 是一个持久化协议,相对于HTTP这种非持久的协议来说。

简单的举个例子吧,用目前应用比较广泛的PHP生命周期来解释。

  • HTTP的生命周期通过Request来界定,也就是一个Request 一个Response,那么HTTP1.0,这次HTTP请求就结束了。
  • 在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。

但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。

跟Websocket有什么关系呢?

Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手。
在握手阶段是一样的

首先我们来看个典型的Websocket握手(借用Wikipedia的。。)

  1. GET /chat HTTP/1.1
  2. Host: server.example.com
  3. Upgrade: websocket
  4. Connection: Upgrade
  5. Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
  6. Sec-WebSocket-Protocol: chat, superchat
  7. Sec-WebSocket-Version: 13
  8. Origin: http://example.com

熟悉HTTP的童鞋可能发现了,这段类似HTTP协议的握手请求中,多了几个东西。

我会顺便讲解下作用。

  1. Upgrade: websocket
  2. Connection: Upgrade

这个就是Websocket的核心了,告诉Apache、Nginx等服务器:

注意啦,窝发起的是Websocket协议,快点帮我找到对应的助理处理~不是那个老土的HTTP。

  1. Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
  2. Sec-WebSocket-Protocol: chat, superchat
  3. Sec-WebSocket-Version: 13

首先,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:泥煤,不要忽悠窝,我要验证尼是不是真的是Websocket助理。
然后,SecWebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。。不过现在还好,已经定下来啦大家都使用的一个东西 脱水:**服务员,我要的是13岁的噢→
→**

然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!

  1. HTTP/1.1 101 Switching Protocols
  2. Upgrade: websocket
  3. Connection: Upgrade
  4. Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
  5. Sec-WebSocket-Protocol: chat

这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~

  1. Upgrade: websocket
  2. Connection: Upgrade

依然是固定的,告诉客户端即将升级的是Websocket协议,而不是mozillasocket,lurnarsocket或者shitsocket。
然后,Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。服务器:好啦好啦,知道啦,给你看我的ID CARD来证明行了吧。。
后面的,Sec-WebSocket-Protocol 则是表示最终使用的协议。

至此,HTTP已经完成它所有工作了,接下来就是完全按照Websocket协议进行了。
具体的协议就不在这阐述了。