论坛首页 编程语言技术论坛

一个简单的delay server

浏览 10258 次
该帖已经被评为良好帖
作者 正文
   发表时间:2010-09-07  
web game里经常出现这样的需求:
1.建造一个房子,等待n秒后建好
2.种植一个植物,等待n秒后完成
3.生产一个汽车,等待n秒后完成
4.升级一个基地,等待n秒后完成
..................
无论是汽车还是房子,建造或升级这个动作很简单,只需要更新一下数据库里的某个字段。
关键是如何处理等待n秒这个操作。

cron + rake
最简单的做法就是后台定时rake,每隔一段时间扫描一下整个表,根据结束时间去改变状态字段。
但是这样作的缺陷很明显,即使扫描的时间间隔再短也达不到准确,还有就是rake每次执行都要加载一次rails环境,然后再释放,这样效率很低。

Delay Job:
看了各种delay job插件,这些插件主要解决的问题是异步的问题,同样达不到精确的计划。

异步线程or进程+sleep n
这种方案看起来不错,但是无论线程还是进程,一直sleep都会占很多资源.

EventMachine
引用
EventMachine是一个基于Reactor设计模式的、用于网络编程和并发编程的框架。Reactor模式描述了一种服务处理器,它接受事件并将其分发给已注册的事件处理。这种模式的好处就是清晰的分离了时间分发和处理事件的应用程序逻辑,而不需引入多线程来把代码复杂化。

好了,神器在手,,看代码:
Delay Server:

#!/usr/bin/env ruby

require 'rubygems'
require 'optparse'
require 'eventmachine'

require 'evma_httpserver'
require 'rack'

options = {
  :Port        => 3000,
  :Host        => "0.0.0.0",
  :environment => (ENV['RAILS_ENV'] || "development").dup
}

ARGV.clone.options do |opts|
  opts.on("-p", "--port=port", Integer,
          "Runs Delay Server on the specified port.", "Default: 3000") { |v| options[:Port] = v }
  opts.on("-b", "--binding=ip", String,
          "Binds Delay Server to the specified ip.", "Default: 0.0.0.0") { |v| options[:Host] = v }

  opts.on("-e", "--environment=name", String,
          "Specifies the environment to run this server under (test/development/production).",
          "Default: development") { |v| options[:environment] = v }

  opts.separator ""

  opts.on("-h", "--help", "Show this help message.") { puts opts; exit }

  opts.parse!
end

puts "=> Delay Server  starting on http://#{options[:Host]}:#{options[:Port]}"


ENV["RAILS_ENV"] = options[:environment]


require(File.join(File.dirname(__FILE__), 'config', 'boot'))
require(File.join(File.dirname(__FILE__), 'config', 'environment'))


class DelayServer < EM::Connection
  include EM::HttpServer

  def post_init
    super
  end

  def process_http_request

    params = Rack::Utils.parse_query(@http_query_string)
    
    response = EM::DelegatedHttpResponse.new(self)
    response.status = 200
    response.content_type 'text/html'

    current_time = Time.now
    response.content = "request time -> #{current_time.to_s(:db)} / delay time -> #{params['delay']}s\n"

    EM::add_timer(params["delay"]) do
      p params["operate"]
      puts Time.now.to_s(:db)
    end

    response.send_response
  end
end


trap(:INT) { exit }

puts "=> Ctrl-C to shutdown server"

EM.run{
  EM.start_server options[:Host], options[:Port], DelayServer
}





Rails Server:
class XxxController < ApplicationController
  def build
   #.................
   system('%Q{url  "http://0.0.0.0:3000?delay=10&operate=build"  > ./log/delay_server.log 2>&1 &})
   #.................
  end
end



其实rails server与delay server之间通讯可以有多种选择,比如drb
本着简单高效的原则,选择了http协议..
虽然eventmachine是非阻塞io,但是单个进程的吞吐量有限,利用http协议很容易在前面加个nginx,变成一个小集群....




   发表时间:2010-09-07  
eventmachine的add_timer不合适用来作这个需求,而且他在高并发下如果有相同的delay time会出现后者覆盖前者的情况。

webgame的delay job通常都是在数据库里面直接插入一条什么时间需要作什么事情的记录,后台一个job server每1秒都进行查询看是否有小于当前时间还未完成的job,然后处理掉,如果每秒要处理的事情太多,就会出现所谓的"卡机"。数据库1秒查询一次的代价其实是很小的。
0 请登录后投票
   发表时间:2010-09-07  
QuakeWang 写道
eventmachine的add_timer不合适用来作这个需求,而且他在高并发下如果有相同的delay time会出现后者覆盖前者的情况。

webgame的delay job通常都是在数据库里面直接插入一条什么时间需要作什么事情的记录,后台一个job server每1秒都进行查询看是否有小于当前时间还未完成的job,然后处理掉,如果每秒要处理的事情太多,就会出现所谓的"卡机"。数据库1秒查询一次的代价其实是很小的。


如果出现你说的覆盖的情况,那么每秒处理一个也是必然会卡机的。而且会越卡越多。。
add_timer方式也可以通过增加进程数量方式避免这个问题。而添加多个job server势必又增加了数据库查询。
add_timer方式如果出现你说的情况,还可以加一个定时任务,每隔一段时间把覆盖调的处理掉。


0 请登录后投票
   发表时间:2010-09-08  
event actor真是越来越多啊

如果这种delay job的场景更清除一些就好了。。

web game种花种草的,长时间的,我觉得job server来做比较合适

如果是射击,法术即时的,你这个思路也许不错哦
0 请登录后投票
   发表时间:2010-09-08  
Hooopo 写道
QuakeWang 写道
eventmachine的add_timer不合适用来作这个需求,而且他在高并发下如果有相同的delay time会出现后者覆盖前者的情况。

webgame的delay job通常都是在数据库里面直接插入一条什么时间需要作什么事情的记录,后台一个job server每1秒都进行查询看是否有小于当前时间还未完成的job,然后处理掉,如果每秒要处理的事情太多,就会出现所谓的"卡机"。数据库1秒查询一次的代价其实是很小的。


如果出现你说的覆盖的情况,那么每秒处理一个也是必然会卡机的。而且会越卡越多。。
add_timer方式也可以通过增加进程数量方式避免这个问题。而添加多个job server势必又增加了数据库查询。
add_timer方式如果出现你说的情况,还可以加一个定时任务,每隔一段时间把覆盖调的处理掉。



不是每秒处理一个,而是每一秒查询一次,有几个早于当前时间需要完成的任务,就处理几个。玩家说的“卡机”是指原定于12:00:00会完成的任务,由于11:59:59的任务太多,导致处理任务的时间过长,可能到12:00:10才会执行查询,进行处理。

eventmachine用处很多,但是用他的add_timer不合适用在web game这种定时完成任务,你可以拿真实的项目数据做对比压力测试看看,你会发现这个架构和数据库查询相比是没有硬件成本和开发成本优势的,你说的加进程或者定时任务更会让这个架构复杂和不稳定。
0 请登录后投票
   发表时间:2010-09-08  
离题问个问题,这个eventmachine做的功能能被node.js取代么?
0 请登录后投票
   发表时间:2010-09-08  
记录下duration和start time在数据库里
duration,start_time
5,  12:30:52

客户端的javascript access服务器端(比如login,或者客户端计时器到期)的时候,一条select,一条update.
0 请登录后投票
   发表时间:2010-09-08   最后修改:2010-09-09
刚才简单测试了一下,add_timer方式在并发情况不会出现覆盖现象,在同一时间如果出现处理不了的操作,前一个操作会阻塞后一个,但是每个操作最终还会执行,也就是quakewang说的卡机.
简单测试代码:
#!/usr/bin/env ruby

require 'rubygems'
require 'optparse'
require 'eventmachine'
require 'evma_httpserver'
require 'rack'

options = {
  :Port        => 3000,
  :Host        => "0.0.0.0",
  :environment => (ENV['RAILS_ENV'] || "development").dup
}

ARGV.clone.options do |opts|
  opts.on("-p", "--port=port", Integer,
          "Runs Delay Server on the specified port.", "Default: 3000") { |v| options[:Port] = v }
  opts.on("-b", "--binding=ip", String,
          "Binds Delay Server to the specified ip.", "Default: 0.0.0.0") { |v| options[:Host] = v }

  opts.on("-e", "--environment=name", String,
          "Specifies the environment to run this server under (test/development/production).",
          "Default: development") { |v| options[:environment] = v }

  opts.separator ""

  opts.on("-h", "--help", "Show this help message.") { puts opts; exit }

  opts.parse!
end

puts "=> Delay Server  starting on http://#{options[:Host]}:#{options[:Port]}"



ENV["RAILS_ENV"] = options[:environment]



require(File.join(File.dirname(__FILE__), 'config', 'boot'))
require(File.join(File.dirname(__FILE__), 'config', 'environment'))




class DelayServer < EM::Connection
  include EM::HttpServer

  def post_init
    super
  end

  def process_http_request

    #params = Rack::Utils.parse_query(@http_query_string)
    
    response = EM::DelegatedHttpResponse.new(self)
    response.status = 200
    response.content_type 'text/html'

    response.content = "ok"


    EM::add_timer(10) do
      sleep 0.5
      $count += 1
      p Time.now.to_s(:db) + "--->#{$count}"
    end

    response.send_response
  end
end



trap(:INT) { exit }

puts "=> Ctrl-C to shutdown server"



EM.run{
  $count = 0
  EM.start_server options[:Host], options[:Port], DelayServer
}

ab -c50 -n100 http://0.0.0.0:3000/


控制台输出:
引用

=> Ctrl-C to shutdown server
"2010-09-09 14:51:11--->1"
"2010-09-09 14:51:11--->2"
"2010-09-09 14:51:12--->3"
"2010-09-09 14:51:12--->4"
"2010-09-09 14:51:13--->5"
"2010-09-09 14:51:13--->6"
"2010-09-09 14:51:14--->7"
"2010-09-09 14:51:14--->8"
"2010-09-09 14:51:15--->9"
"2010-09-09 14:51:15--->10"
"2010-09-09 14:51:16--->11"
"2010-09-09 14:51:16--->12"
"2010-09-09 14:51:17--->13"
"2010-09-09 14:51:17--->14"
"2010-09-09 14:51:18--->15"
"2010-09-09 14:51:18--->16"
"2010-09-09 14:51:19--->17"
"2010-09-09 14:51:19--->18"
"2010-09-09 14:51:20--->19"
"2010-09-09 14:51:20--->20"
"2010-09-09 14:51:21--->21"
"2010-09-09 14:51:21--->22"
"2010-09-09 14:51:22--->23"
"2010-09-09 14:51:22--->24"
"2010-09-09 14:51:23--->25"
"2010-09-09 14:51:23--->26"
"2010-09-09 14:51:24--->27"
"2010-09-09 14:51:24--->28"
"2010-09-09 14:51:25--->29"
"2010-09-09 14:51:25--->30"
"2010-09-09 14:51:26--->31"
"2010-09-09 14:51:26--->32"
"2010-09-09 14:51:27--->33"
"2010-09-09 14:51:27--->34"
"2010-09-09 14:51:28--->35"
"2010-09-09 14:51:28--->36"
"2010-09-09 14:51:29--->37"
"2010-09-09 14:51:29--->38"
"2010-09-09 14:51:30--->39"
"2010-09-09 14:51:30--->40"
"2010-09-09 14:51:31--->41"
"2010-09-09 14:51:31--->42"
"2010-09-09 14:51:32--->43"
"2010-09-09 14:51:32--->44"
"2010-09-09 14:51:33--->45"
"2010-09-09 14:51:33--->46"
"2010-09-09 14:51:34--->47"
"2010-09-09 14:51:34--->48"
"2010-09-09 14:51:35--->49"
"2010-09-09 14:51:35--->50"
"2010-09-09 14:51:36--->51"
"2010-09-09 14:51:36--->52"
"2010-09-09 14:51:37--->53"
"2010-09-09 14:51:37--->54"
"2010-09-09 14:51:38--->55"
"2010-09-09 14:51:38--->56"
"2010-09-09 14:51:39--->57"
"2010-09-09 14:51:39--->58"
"2010-09-09 14:51:40--->59"
"2010-09-09 14:51:40--->60"
"2010-09-09 14:51:41--->61"
"2010-09-09 14:51:41--->62"
"2010-09-09 14:51:42--->63"
"2010-09-09 14:51:42--->64"
"2010-09-09 14:51:43--->65"
"2010-09-09 14:51:43--->66"
"2010-09-09 14:51:44--->67"
"2010-09-09 14:51:44--->68"
"2010-09-09 14:51:45--->69"
"2010-09-09 14:51:45--->70"
"2010-09-09 14:51:46--->71"
"2010-09-09 14:51:46--->72"
"2010-09-09 14:51:47--->73"
"2010-09-09 14:51:47--->74"
"2010-09-09 14:51:48--->75"
"2010-09-09 14:51:48--->76"
"2010-09-09 14:51:49--->77"
"2010-09-09 14:51:49--->78"
"2010-09-09 14:51:50--->79"
"2010-09-09 14:51:50--->80"
"2010-09-09 14:51:51--->81"
"2010-09-09 14:51:51--->82"
"2010-09-09 14:51:52--->83"
"2010-09-09 14:51:52--->84"
"2010-09-09 14:51:53--->85"
"2010-09-09 14:51:53--->86"
"2010-09-09 14:51:54--->87"
"2010-09-09 14:51:54--->88"
"2010-09-09 14:51:55--->89"
"2010-09-09 14:51:55--->90"
"2010-09-09 14:51:56--->91"
"2010-09-09 14:51:56--->92"
"2010-09-09 14:51:57--->93"
"2010-09-09 14:51:57--->94"
"2010-09-09 14:51:58--->95"
"2010-09-09 14:51:58--->96"
"2010-09-09 14:51:59--->97"
"2010-09-09 14:51:59--->98"
"2010-09-09 14:52:00--->99"
"2010-09-09 14:52:00--->100"


0 请登录后投票
   发表时间:2010-09-08  
g.zhen.ning 写道
离题问个问题,这个eventmachine做的功能能被node.js取代么?

可以,node里有setTimeout
http://nodejs.org/api.html#timers-84
0 请登录后投票
   发表时间:2010-09-08  
ray_linn 写道
记录下duration和start time在数据库里
duration,start_time
5,  12:30:52

客户端的javascript access服务器端(比如login,或者客户端计时器到期)的时候,一条select,一条update.

是用户在线时利用前端的timer,不在线时在登录时候同步吗?
这样有一个问题:
比如建造一个房子
如果玩家A在未建造完成之前下线,
虽然玩家A再次上线的时候会得到建造完成的房子,
但是在其他玩家在查看玩家A的建筑的时候是看不到玩家A的房子的...
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics