部署 蝉游记网站的部署 Nginx,Unicorn,Capistrano,OOB,Graceful Restart

quakewang · 2013年06月27日 · 最后由 liwen_zhang 回复于 2016年05月06日 · 29004 次阅读
本帖已被管理员设置为精华贴

蝉游记( http://chanyouji.com )网站之前用 Nginx+Passenger+ 自制 script 来部署,随着用户增多,移动 app 的 api 调用增加,服务器增多和无缝部署重启的需求,转移到了 Nginx+Unicorn+Capistrano,写篇博客记录一下各种细节和需要注意的地方。

Nginx 的配置

gzip  on;
#开启gzip,同时对于api请求的json格式也开启gzip
gzip_types application/json;

#每台机器都运行nginx+unicorn,本机用domain socket,方便切换
upstream ruby_backend {
    server unix:/tmp/unicorn.sock fail_timeout=0;
    server 10.4.8.34:4096 fail_timeout=0;
    server 10.4.3.8:4096 fail_timeout=0;
}

#用try_files方式和proxy执行rails动态请求
server {
    listen       80;
    server_name  chanyouji.com;
    root         /www/youji_deploy/current/public;

    try_files $uri/index.html $uri.html $uri @user1;

    location @user2 {
      proxy_redirect     off;
      proxy_set_header   Host $host;
      proxy_set_header   X-Forwarded-Host $host;
      proxy_set_header   X-Forwarded-Server $host;
      proxy_set_header   X-Real-IP        $remote_addr;
      proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
      proxy_buffering    on;
      proxy_pass         http://ruby_backend;
   }
}

#用不同的域名提供静态资源服务,减少主域名带来的cookie请求和方便做cdn源
server {
    listen       80;
    server_name  cdn.chanyouji.cn cdnsource.chanyouji.cn;
    root         /www/youji_deploy/current/public;

    location ~ ^/(assets)/  {
      root /www/youji_deploy/current/public;
      gzip_static on; # to serve pre-gzipped version
      expires max;
      add_header Cache-Control public;
    }
}

unicorn.rb 的配置

worker_processes 6

app_root = File.expand_path("../..", __FILE__)
working_directory app_root

# Listen on fs socket for better performance
listen "/tmp/unicorn.sock", :backlog => 64
listen 4096, :tcp_nopush => false

# Nuke workers after 30 seconds instead of 60 seconds (the default)
timeout 30

# App PID
pid "#{app_root}/tmp/pids/unicorn.pid"

# By default, the Unicorn logger will write to stderr.
# Additionally, some applications/frameworks log to stderr or stdout,
# so prevent them from going to /dev/null when daemonized here:
stderr_path "#{app_root}/log/unicorn.stderr.log"
stdout_path "#{app_root}/log/unicorn.stdout.log"

# To save some memory and improve performance
preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
  GC.copy_on_write_friendly = true

# Force the bundler gemfile environment variable to
# reference the Сapistrano "current" symlink
before_exec do |_|
  ENV["BUNDLE_GEMFILE"] = File.join(app_root, 'Gemfile')
end

before_fork do |server, worker|
  # 参考 http://unicorn.bogomips.org/SIGNALS.html
  # 使用USR2信号,以及在进程完成后用QUIT信号来实现无缝重启
  old_pid = app_root + '/tmp/pids/unicorn.pid.oldbin'
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end

  # the following is highly recomended for Rails + "preload_app true"
  # as there's no need for the master process to hold a connection
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.connection.disconnect!
end

after_fork do |server, worker|
  # 禁止GC,配合后续的OOB,来减少请求的执行时间
  GC.disable
  # the following is *required* for Rails + "preload_app true",
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.establish_connection
end

GC OOB

这篇 newrelic 的文章解释很清楚: http://blog.newrelic.com/2013/05/28/unicorn-rawk-kick-gc-out-of-the-band/ 就是将 GC 延迟到用户请求完成以后,这样就会缩短响应时间,配合现成的 gem unicorn-worker-killer 也不用担心内存爆掉。

在 config.ru 里面配置:

require 'unicorn/oob_gc'
require 'unicorn/worker_killer'
#每10次请求,才执行一次GC
use Unicorn::OobGC, 10
#设定最大请求次数后自杀,避免禁止GC带来的内存泄漏(3072~4096之间随机,避免同时多个进程同时自杀,可以和下面的设定任选)
use Unicorn::WorkerKiller::MaxRequests, 3072, 4096
#设定达到最大内存后自杀,避免禁止GC带来的内存泄漏(192~256MB之间随机,避免同时多个进程同时自杀)
use Unicorn::WorkerKiller::Oom, (192*(1024**2)), (256*(1024**2))

require ::File.expand_path('../config/environment',  __FILE__)
run Youji::Application

Capistrano 部署脚本

set :unicorn_config, "#{current_path}/config/unicorn.rb"
set :unicorn_pid, "#{current_path}/tmp/pids/unicorn.pid"

namespace :deploy do
  task :start, :roles => :app, :except => { :no_release => true } do
    run "cd #{current_path} && RAILS_ENV=production bundle exec unicorn_rails -c #{unicorn_config} -D"
  end

  task :stop, :roles => :app, :except => { :no_release => true } do
    run "if [ -f #{unicorn_pid} ]; then kill -QUIT `cat #{unicorn_pid}`; fi"
  end

  task :restart, :roles => :app, :except => { :no_release => true } do
    # 用USR2信号来实现无缝部署重启
    run "if [ -f #{unicorn_pid} ]; then kill -s USR2 `cat #{unicorn_pid}`; fi"
  end
end

完成这些改进以后,部署蝉游记的新版本就只用输入 cap production deploy,然后就可以喝茶去了,也不用担心用户在重启动的时候会有短期卡死的问题 :)

补 2 张图: new relic 的监控图,和启用 OOB 之前相比,平均响应时间从 100ms 左右下降到了 90ms 左右:

服务器的内存和 CPU 使用:

楼主你要是早几个小时发帖我就不用转到 thin+nginx 上了 :(

好东西,多谢分享。

👍 又学了不少东西

我一直用的就是这套 Nginx+Unicorn+Capistrano。包括个人小站(www.fxgzj.com)也是。

无缝部署重启很赞

#4 楼 @outman 求教:个人小站关闭 GC 的话 对内存的压力大不大啊?个人小站 vps 内存是个瓶颈啊

#6 楼 @zj0713001 关闭 GC?这个还真不能关哦,楼主说的是延迟执行,如果关了内存泄露是必然的,那么多内存无法回收,不敢想象。不过像我的那个小站,非常小,延迟不延迟影响都不是很大,访问量本来就小嘛,也不会开太多的 worker,话说 Unicorn 是多进程模式,如果访问量比较大的话,估计内存就有点吃紧了,所以最好升级到 RUBY2.0,2.0 后对 GC 已经有了优化,可以利用 Linux 的 Copy on Write 技术,在一定程度上可以提高性能,节约内存,特别是读多写少的情况。

果断收藏。请教 @quakewang:Unicorn 进程本身是否还用了 service 或其他监测工作,还是说 unicorn-worker-killer 也能就会系统重启等情况?谢谢!

#9 楼 @ashchan unicorn 本身启动命令就是 capistrano 里面的 start 命令: "cd #{current_path} && RAILS_ENV=production bundle exec unicorn_rails -c #{unicorn_config} -D"

将他放到服务器的启动脚本里面就可以实现自动重启了。

#6 楼 @zj0713001 正如#8 楼 @outman 所说,ruby2.0 开启 COW,内存使用还是可接受的,unicorn.rb 里面有这样一段:

# To save some memory and improve performance
preload_app true
GC.respond_to?(:copy_on_write_friendly=) and
  GC.copy_on_write_friendly = true

另外我在主贴,补充了 2 个图,共 3 台服务器 (每台都是 2core, 4G,监控上显示 4 台,其中一台是内部测试的),每台开 6 个 unicorn worker,平均一个 200MB 左右,如果内存紧张的话,减少 worker 和 WorkerKiller::Oom 的内存上限,1G 左右的内存 +20% 的 CPU,很轻松一台就可以支持 200 RPM。

好漂亮的网站

#10 楼 @quakewang 谢谢,我也是这么用的。unicorn-worker-killer 看着非常不错!

Nginx 和 Unicorn 中间加个 Haproxy,这样有问题的时候能自动切断,还能控制分流的比例

#14 楼 @huacnlee 是在 Nginx 之前加 Haproxy 吧?在之间加没意义啊,我们以前是这样用的,那个项目是 Nginx+Passenger,不过 Unicorn 也一样: http://quake.iteye.com/blog/1313623

#8 楼 @outman 我的意思就是在 request 期间关闭 GC 就是延迟执行... 多谢指导哈

#15 楼 @quakewang 有意义啊,Haproxy 来管理后端的 App Server 可以更方便,而且具有更多的功能,比如根据 Cookie 强制指向某个节点

Nginx 架在前面又能自己处理静态文件

cap 我在不同服务器上分不同 multistage,没用帐号名不一样,然后部署老是串用户名,明明 production 在这台机器用这个用户名,却用了 dev 这个 stage 的用户名,很头大。

金数据用的技术栈几乎完全一样。用 Ruby 2.0 之后,目测内存没有增长的趋势,GC 什么的暂时就没弄。

好东西,赞楼主

学习啦~谢谢分享!

#18 楼 @as181920 我们也是分 stage 的,没出现你说的情况,不过和其他常见免密码私钥登录的方式不同,我是强制输入用户名密码的:

set(:user) do
   Capistrano::CLI.ui.ask "Give me a ssh user: "
end

不同 stage 的用户名不一样,这样也不容易搞混。

#22 楼 @quakewang 有没有不同用户的 cap 配置文件给参考下,或者上面这种输入用户的也 ok。email: [email protected]

如果用 Mina 替代 Capistrano 部署会更快

强悍。unicorn/worker_killer,正在寻觅的东西。

#24 楼 @camel
mina 用过。简明,挺好,特别不需要 recompile assets。

对于 sidekiq 这些服务(还有一些其他的),官方默认给出 cap 的配置,用 mina 的话也可以写,不过官方有现成的可以省时间(不是懒得学,确实时间少)。这是一个原因。

如果每个 app server 都是 Nginx+Unicorn 那你们是用 DNS 切换不同的 ip 地址来实现负载均衡?

想问一下,图片是直接上传的吗?还是改过?有利用什么又拍云什么的吗?

#27 楼 @steven_yue 目前访问量还小,没用 dns 切换或者硬件,ip 是指向其中的一台,负载均衡是由这台转发到本机或者其他 2 台机器的 unicorn,可以看上面的配置:

upstream ruby_backend {
    server unix:/tmp/unicorn.sock fail_timeout=0;
    server 10.4.8.34:4096 fail_timeout=0;
    server 10.4.3.8:4096 fail_timeout=0;
}

另外 2 台的 nginx 配置也是如此,指向本机和另外 2 台机器,unicorn 同时监听 domain socket 和 tcp port:

listen "/tmp/unicorn.sock", :backlog => 64
listen 4096, :tcp_nopush => false

这样好处是,除了 upstream 的配置稍微不同以外,每台都一样,我们是用云服务的,如果要扩充就可以直接镜像一个出来。如果一台出问题,ip 指向到另外一台就可以切换过去。

#28 楼 @Levan 是用七牛的,和又拍云类似,也是一个云存储的服务提供商,之前写过一篇博客介绍他们的:

http://quake.iteye.com/blog/1816807

@quakewang 好的,感谢,想问请教下主机的情况,应为速度确实挺快看着很爽,是用阿里吗?在 alexa 查了蝉游记的 ip,一万五左右,这种带宽大概要多少?

#31 楼 @Levan 主机是在 ucloud.cn,主要流量是图片,托管到七牛了,那边是按流量收费,目前费用在 500/月左右。而在主机这边带宽成本几乎是可以忽略的。

@quakewang 知道了。感谢

#32 楼 @quakewang 我现在也是 chanyouji 的用户(只看不发), 我身边也有人用 你说流量只¥500/month(我知道七牛大概的价格) 我不大相信,chayouji 不是有图片也有视频吗

amazing

#31 楼 @Levan 蝉游记的 web 访问应是很少的,主要是 APP 用户

#34 楼 @liuhui998 图片和视频经过压缩以后其实是不大的,手机上传的视频还有 30 秒限制。另外我们针对不同的分辨率使用不同尺寸的图片,所以流量费没你想象的那样多。

另外,我们的 web 访问和 app 访问都是相同服务器,而且访问量是相当的。

#36 楼 @quakewang 你们的 APP 和网站做的真心不错

真心不错,学习之。

网站做得很棒

比起这个我觉得你们的网站做真用心啊~佩服

和 knewone 的技术栈几乎一样,学到了不少东西,感谢!

区别只有我们用 upyun 存图片这么一点点

正。学习

43 楼 已删除

@quakewang 貌似现在网站 down 了?正想去更新游记那 😓

#44 楼 @young4u_amy 检查过是正常的啊,请问具体是什么错误?

46 楼 已删除

#45 楼 @quakewang 就是那种无法连接服务器 刚试了 2 个浏览器都打不开 现在好了 sorry 也可能是我的网络问题

我的配置跟你差不多 不过 Nginx 中,你是用了 try_files 配合 location @httpapp ,我是直接用 location /,这种配置之间有什么差别吗?

#48 楼 @HungYuHei 区别是一些静态 html 文件或者全页缓存 html 可以由 nginx 直接来提供服务,而不用转发到 unicorn 来处理。

@quakewang

我在一台 Staging 测试服务器上关闭 GC 以后发现一个问题: 在没有 Request 的情况下,内存会持续缓慢上升,因为 unicorn-worker-killer 是通过 check_cycle 去查内存的,这种情况下,超过设定的内存上限后仍然不会 KILL 掉 WORKER,所以长时间不访问网站,内存还是有爆掉的可能?

另外,有一个疑问,在没有请求的情况下,这种出现持续的内存泄漏正常么?

#52 楼 @chaixl 没测试过这个情况,不过直觉来说,如果没有请求,内存持续上升明显是不正常的。

upstream ruby_backend {
    server unix:/tmp/unicorn.sock fail_timeout=0;
    server 10.4.8.34:4096 fail_timeout=0;
    server 10.4.3.8:4096 fail_timeout=0;
}

这样就可以自动负载均衡么?还是需要有别的配置?

延迟 GC 和进程自杀的配置很有用啊,非常有用的分享。

似乎 unicorn 默认是不会处理静态文件的,为了配合 try_files $uri/index.html $uri.html $uri @httpapp;

应该还要另外写一个

location ~* ^.+\.(html|htm)$ {
          expires max;
          break;
  }

来处理静态文件的。

#32 楼 @quakewang 请问你们用的是 Ucloud 的云 db,还是用云主机自己搞的数据库?

#56 楼 @yzhrain 这样配置就可以了

#59 楼 @iamzhangdabei 没有用云 db,是用云主机上运行 mariadb

#60 楼 @quakewang 还想找个用过云 db 的人了解下性能怎么样呢...

弱弱地问一下,你们这么多图片为啥都不让浏览器缓存呢,第二次访问返回 304 什么的……

#62 楼 @aptx4869 所有图片都有缓存的压,请问具体是哪个图片有问题?能给一下 url,我来检查一下么。

#63 楼 @quakewang 大概是没考虑到 F5 刷新吧……F5 强迫症的人应该挺多的…… 在 ruby-china 按 F5 图片会返回 304 不过貌似响应速度有点慢 另外为啥 Server 是 Microsoft-IIS/6.0……

#64 楼 @aptx4869 我们用的 CDN response 的时候是采用 ETag 和 Cache-Controller header 来实现缓存,但是好像在处理这种 F5 强制刷新的时候,没有正确返回针对 If-None-Match 的 ETag 304 响应。我和 CDN 那边的人确认一下,十分感谢。

我有个项目用的是 nginx+passenger,看了你的帖子以及@robbin 的帖子http://ruby-china.org/topics/10832 之后我首先部署了 nginx+unicorn,然后修改了下 unicorn 的配置文件部署 nginx+rainbows,当我在用 jMeter 进行压测,线程开到 500 的时候,无论 nginx+unicorn 还是 rainbows 都会有错误出现,而是用 passenger 却不会出现这种情况。unicorn 和 passenger 性能差不多,rainbows 并发大概可以提高 30% 左右。

#64 楼 @aptx4869 CDN 提供商已经解决这个 F5 刷新的问题了,谢谢。 sever header 是自己设置的,可以快速干掉对 asp,php 等后缀扫描漏洞的远端 ip,还可以稍微迷惑一下小白黑客。

没有用到 god 吗?

问一下。 use Unicorn::WorkerKiller::Oom, (192*(1024**2)), (256*(1024**2)) 这个 192,256 指的是 RES 内存,还是 VIRT 内存

unicorn.rb 中使用了FILE来定位 app_root

app_root = File.expand_path("../..", __FILE__)
working_directory app_root

注意 unicorn 启动命令中,-c #{unicorn_config} 必须写完整路径,否则 capistrano 更新 current 符号链接后,unicorn 收到 USR2 后新启 master 会使用旧的 unicorn.rb。

run "cd #{current_path} && RAILS_ENV=production bundle exec unicorn_rails -c #{unicorn_config} -D"

我测试的时候,启动命令写成 cd /var/www/myapp/current && RAILS_ENV=production bundle exec unicorn_rails -c config/unicorn.rb -D,结果 cap production deploy 后,仍然是旧代码在跑。 为了减少出错,我现在把 unicorn.rb 中的 app_root 直接写死。

挖个坟,问一下 @quakewang , 现在你们蝉游记的并发大概是多少?

73 楼 已删除

#72 楼 @blackanger 上面有,可以算出来的,单台吞吐量接近 300,共有 3 台实例,大概是 900-1000 rqm/s 左右

求问,如果杀掉进程的时候,该进程正在处理请求,会怎么办,会出现这样的情况吗

#75 楼 @hustjackyan 不会出现,不然的话没人敢用unicorn-worker-killer

LZ,想请教一下,同一台服务器如何配置多个 unicorn?多个 unicorn 部署需要怎么处理?

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