采用ruby+eventmachine+em-http-request实现一个站点可用性监控。采用C/S的架构,将client部署在各个机房节点,定时探测域名并将结果返回给server,server将数据插入监控系统进行绘图和报警(注:我们使用zabbix)。
EventMachine,是类似python中的twisted的一个基于Reactor设计模式的事件驱动I/O框架,它可以很好的消除网络变成的复杂度,我们只需要在相应的方法中实现自己的业务逻辑,而不用去关心socket和线程等实现问题。
首先,构建一个简单的server:
1 2 3 |
EM.run do EM.start_server("127.0.0.1", "6000", Demo) end |
然后我们再实现一下DemoServer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Demo < EM::Connection def post_init @port, @ip = Socket.unpack_sockaddr_in(get_peername) p "client : ip => #{@ip} , port => #{@port} connected" end def unbind unless self.ip.nil? || self.port.nil? || self.ip.empty? || self.port.to_s.empty? p "client: #{@ip}:#{@port} has disconnected..." else p "A unknown client has disconnected..." end end def receive_data(data) p "received data : #{data}" end end |
在EM中定义了一些回调函数,我们可以根据我们的需要去实现这些方法,就可以在我们需要的阶段完成相对应的业务逻辑,如上例中:
post_init
: 一旦连接建立后它就会调用,如果我们想在连接建立后做些事情(如:保存对方的ip和端口信息或给对方发一段字符串等)unbind
: 任何一端关闭连接,都会调用该函数。received
: 无论何时我们从网络连接里接受到数据,该函数就会被调用。
上面的小例子,完成了一个监听在127.0.0.1:6000上的socketserver:
- 当客户端连接完成时,输出客户端的ip和port
- 当客户端断开连接时,若ip和port不为空,则输出断开client的ip和port,否则输出unknown client
- 当服务端收到数据时,则直接将数据输出
由此可见,使用EM构建网络应用是件十分简单高效的事。
接下来讲讲em-http-request模块
em-http-request,是EM的一个http库,功能很强大,通过简单几行代码就可以完成http请求功能,目前官方列出的功能支持有:
- Asynchronous HTTP API for single & parallel request execution
- Keep-Alive and HTTP pipelining support
- Auto-follow 3xx redirects with max depth
- Automatic gzip & deflate decoding
- Streaming response processing
- Streaming file uploads
- HTTP proxy and SOCKS5 support
- Basic Auth & OAuth
- Connection-level & Global middleware support
- HTTP parser via http_parser.rb
- Works wherever EventMachine runs: Rubinius, JRuby, MRI、
em-http-request使用起来十分简单, 举例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
def async_fetch(url) f = Fiber.current http = EventMachine::HttpRequest.new(url, :connect_timeout => 10, :inactivity_timeout => 20).get http.callback { f.resume(http) } http.errback { f.resume(http) } Fiber.yield if http.error p [:HTTP_ERROR, http.error] end http end EventMachine.run do Fiber.new{ puts "Setting up HTTP request #1" data = async_fetch('http://0.0.0.0/') puts "Fetched page #1: #{data.response_header.status}" puts "Setting up HTTP request #2" data = async_fetch('http://www.yahoo.com/') puts "Fetched page #2: #{data.response_header.status}" puts "Setting up HTTP request #3" data = async_fetch('http://non-existing.domain/') puts "Fetched page #3: #{data.response_header.status}" EventMachine.stop }.resume end puts "Done" |
上面的例子,是fiber+em-http的例子:
- http = EventMachine::HttpRequest.new(url, :connect_timeout => 10, :inactivity_timeout => 20).get, 一句话,完成了http请求,并将请求的结果对象赋值给http.
- 上面的例子还通过fiber进行了并行调用.
Fiber.new 创建纤程
f = Fiber.current 获取当前纤程
http.callback { f.resume(http) } 定义callback回调,
http.errback { f.resume(http) } 定义errback回调
Fiber.yield 挂起当前纤程,并交出控制权
详细了解纤程的信息可以, 移驾Fiber
基本上通过上面介绍的这种简单的方式,就可以构建起基本的域名监控系统了。
附带一提, 对于数据库操作,使用ruby的sequel,一个使用简单,功能强大的数据库组件。
例如:
1 |
my_posts = posts.where(:category => 'ruby', :author => 'david') |
等于
1 |
select * from posts where category = 'ruby' and author = 'david' |
看起来很清晰明了, 又避免写复杂的sql.
感兴趣的朋友可以去sequel看看
遇到的问题:
1. redirect失效问题:
- 由于我们的服务对外并非一个IP,同时还存在基于地域、运营商的DNS调度(后续可详见即将发表的SMARTDNS一文),那么我们的实现方式是,client先通过本地配置的DNS获取域名对应的IP列表,在通过设置header的方式进行http请求。
- 问题来了,在使用过程中,发现redirect的配置会失效,一直获取不到最终页面。追踪每次请求的返回,发现HEADER中的HOST字段,在redirect之后,仍然为指定的
:head => {'Host' => host}
,导致redirect失效,访问仍会落在Host对应的virtualserver上。
知道结症所在,我们下面来通过一行简单的代码来修复这个问题:
找到em-http-request中的client.rb文件,添加如下内容至unbind方法:
1 |
@req.headers.delete('Host') if @req.headers.has_key?('Host') |
修改后的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
def unbind(reason = nil) if finished? if redirect? begin @conn.middleware.each do |m| m.response(self) if m.respond_to?(:response) end # one of the injected middlewares could have changed # our redirect settings, check if we still want to # follow the location header if redirect? @req.followed += 1 @cookies.clear @cookies = @cookiejar.get(@response_header.location).map(&:to_s) if @req.pass_cookies @req.set_uri(@response_header.location) @req.headers.delete('Host') if @req.headers.has_key?('Host') @conn.redirect(self) else succeed(self) end rescue Exception => e on_error(e.message) end else succeed(self) end else on_error(reason) end end |
5 Comments
ruby的一些库 还是相当好用的,赞
石总,不容易啊~~~ T_T
学习了
我发现用em-http-request的keepalive 和 pipeline时如果遇到重定向,程序的反应会很慢。或许你知道原因。
现在我还没有用到keepalive之类的, 如果方便,可以给我发个你的代码片段,我trace一下.