分享 图解 Unicorn 工作原理

early · 2018年12月06日 · 最后由 jasl 回复于 2018年12月07日 · 864 次阅读
本帖已被设为精华帖!

本文由内部分享整理而成,如发现有错误的地方,请不吝指出

什么是Unicorn

unicorn是Ruby Web应用中的一款应用服务器,提供两个功能:

  • 为Rack应用提供HTTP服务能力
  • 为应用实现高并发能力

unicorn工作在Web的应用层,直接调用后端Handler处理请求。 简单Web结构图

如何工作

提供HTTP服务功能:

  • 监听端口/unix socket,接收http请求
  • 解析http请求,调用应用处理请求
  • 将处理结果返回

通过多进程提供高并发能力。 由于Ruby全局解释锁阻碍的同一进程中的线程并行执行,Ruby应用中的高并发必须通过多进程实现。 同一时间只有一个线程能执行

线程中如果有IO操作时,GIL会自动释放,所以在线程中有IO操作的情况下,同一进程中的多个线程可以实现近似的并行执行。

通过Master-Workers进程结构提供服务:

  • 一个Master,管理Worker进程,处理外部信号,不处理请求
  • 多个Worker进程,处理实际的请求,彼此独立

此结构和Nginx多进程模式一致。Master作为劳心者,不处理实际的事务,只做顶层调度。Workers作为劳力者,只处理实际的请求,受制于Master。

请求处理过程

nginx-unicorn-flow.jpg

通过上图可总结如下:

  • Master监听端口,Fork子进程(也可以通过sparn)
  • 子进程通过select && accept调用获取连接
  • 子进程读socket数据,调用Rack App,再将结果回写socket

另外:

  • 通过kgio实现非阻塞系统调用
  • 通过C扩展实现高速HTTP协议解析

IO模型

由上图可以看出unicorn的IO模型有如下特点:

  • IO低效,阻塞(宏观视角,本身使用非阻塞调用)
  • 一个进程同时只能处理一个请求,吞吐量低下
  • 在Socket读写或数据库查询等等IO操作时,进程空闲,资源浪费
  • 无法处理慢IO,需要前置Nginx

与Puma的IO对比

puma-io.jpg

Puma实现了Reactor模型,所谓Reactor模型就是将Socket的操作实际的请求处理用不同的角色来做,不像unicorn那样用同一个角色(线程)来处理。避开了Socket操作时阻塞导致的进程资源浪费,实现了高效、分工、低耦合等。

同时引入多个工作线程来处理实际的请求。这样在某个线程调用外部接口或者查询数据库等IO操作时,其他工作线程可以并发执行,避免进程空闲。

整理特点如下:

  • IO相对高效,进程不容易空闲
  • 一个进程可以同时处理多个请求,吞吐量高

如何管理进程

Master

  • 杀掉超时的子进程, 代码
  • 维持子进程数量, 代码

Worker

  • 检测Master,同生共死,代码
  • 接收Master的指令

通信方式:

如何平滑重启

unicorn-hot-reload.png

平滑关键点在于:

  • 通过环境变量传递监听的socket
  • 设置监听socket close_on_exec,让操作系统保留监听socket

详细信息可以参考: Unicorn 进程如何保证平滑重启?

unicorn-killer

def process_client(client)
  super(client) # Unicorn::HttpServer#process_client
  return if @_worker_memory_limit_min == 0 && @_worker_memory_limit_max == 0
  @_worker_process_start ||= Time.now
  @_worker_memory_limit ||= @_worker_memory_limit_min + randomize(@_worker_memory_limit_max - @_worker_memory_limit_min + 1)
  @_worker_check_count += 1
  if @_worker_check_count % @_worker_check_cycle == 0
    rss = GetProcessMem.new.bytes
    logger.info "#{self}: worker (pid: #{Process.pid}) using #{rss} bytes." if @_verbose
    if rss > @_worker_memory_limit
      logger.warn "#{self}: worker (pid: #{Process.pid}) exceeds memory limit (#{rss} bytes > #{@_worker_memory_limit} bytes)"
      Unicorn::WorkerKiller.kill_self(logger, @_worker_process_start) # 关键点
    end
    @_worker_check_count = 0
  end
end

通过hack process_client方法,在请求处理完后,检测进程内存消耗等参数,操作阈值,则将自己干掉, Master会自动起新的Worker进程。

有关unicorn 中 fork,exec,preload 等更细节的描述,可以参考 Unicorn 进程如何保证平滑重启?

共收到 9 条回复
jasl 将本帖设为了精华贴 12月06日 23:24

可以搭配《理解Unix进程》做参考

题外话, 说起 Unicorn, 不知道什么原因他的作者 Eric Wong 将会永久离开 Ruby 社区.

彩色图很好,值得推广。 node、go听说不需要app服务器,不知道是否是这样?

chenge 回复

golang是这样的。

  • 本身很快,可以自己做http协议解析
  • 并发行好,accept得到一个新连接后,可以丢到一个gorouting里面执行socket操作和业务逻辑
  • gorouting本身很轻量,可以同时开非常多个,彼此之间可以实现并行执行

到处都在处理error,所以程序的健壮性可以得到保障。 一般应用引入一个http包,就可以简单地实现应用服务器的功能了。

ksec 回复

来源?Eric Wong 今天还在推补丁的 https://bugs.ruby-lang.org/users/724

ksec 回复

你说的是mongrel的作者吧

jasl 回复

https://bugs.ruby-lang.org/issues/13618#note-160

"Really? It seems to me nobody is interested in this feature, so I'll probably abandon it as I can't afford to hack on Ruby much longer."

https://bugs.ruby-lang.org/issues/15347#note-5

"Anyways, it's unlikely I'll be around in 2020 (or even 2019) so I won't be around to benefit from it; but I support the change regardless."

也有其他人问过他原因, 但他本人没有直接回覆

ksec 回复

auto fibers 要解决的问题,有好几个方案在竞争,@dsh0416 对这块比较了解

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册