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

quakewang · 发布于 2013年06月27日 · 最后由 liwen_zhang 回复于 2016年05月06日 · 18940 次阅读
162
本帖已被设为精华帖!

蝉游记( 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使用:

共收到 75 条回复
599

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

1924

好东西,多谢分享。

3753

👍 又学了不少东西

1249

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

C5fc5e

无缝部署重启很赞

3753

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

1249

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

78

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

162

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

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

162

#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。

827

好漂亮的网站

78

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

De6df3

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

162

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

3753

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

De6df3

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

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

2511

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

2079

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

291

好东西,赞楼主

360

学习啦~谢谢分享!

162

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

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

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

2511

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

558

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

6224

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

2511

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

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

96

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

744

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

162

#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指向到另外一台就可以切换过去。

162

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

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

744

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

162

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

744

@quakewang 知道了.感谢

780

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

amazing

780

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

162

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

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

780

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

2991

真心不错,学习之。

216

网站做得很棒

4113

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

1208

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

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

4584

正。学习

43楼 已删除
96

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

162

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

46楼 已删除
96

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

77

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

162

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

2870

@quakewang

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

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

162

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

177

赞!

142
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;
}

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

142

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

142

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

应该还要另外写一个

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

来处理静态文件的。

1207

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

162

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

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

1207

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

96

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

162

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

96

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

162

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

6560

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

162

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

96

没有用到 god 吗?

1207

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

9169

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直接写死。

487

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

73楼 已删除
12435

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

8087

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

11314

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

1745

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

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