龍昌博客

从Pythoneer转向Rubist

Unicorn是如何与nginx通讯的——介绍ruby中的unix Socket

| Comments

Ruby 应用服务典型地是与一个 web 服务一同使用的,如 nginx。当用户请求你的 Rails 应用中的页面时,nginx 将请求指派给应用服务。 然而这个过程是如何完成的呢?nginx 与 unicorn 是如何通讯的呢?

最有效的一种选择是使用 unix 套接字(sockets)。让我们来看看它们是如何工作的! 在这篇文章中我们将从一个基本的套接字(sockets)开始,最后将创建一个使用 nginx 代理的简单应用服务。

socket

套接字(sockets)允许进程之间通过对一个文件读或者写进行相互通讯。 在这个例子中 Unicorn 创建 socket 并监听它的连接。然后 Nginx 就可以连接到这个 socket 并与 Unicorn 通讯了。

什么是 unix socket?

Unix socket 使得一个进程通过类似文件的方式与另一个进程进行通讯。它是 IPC(Interprocess Communication) 的一种。

要使得可以通过 socket 访问进程,首先需要创建一个 socket 并作为一个文件保存在磁盘中。 然后监听这个 socket 的连入连接。当接收到一个连接时,就可以使用标准 IO 方法进行读写数据。

Ruby 通过以下一组类提供了 unix socket 所需的所有内容:

  • UNIXServer - 创建 socket 并保存到磁盘中,并且让你可以监听新连接。
  • UNIXSocket - 打开已存在的套接字(sockets)。

注意:还存在着其它类型的 socket,最突出的是 TCP socket。不过这篇文章只处理 unix socket。那么它们之间的区别是什么呢?unix socket 具有文件名。

最简单的 Socket

我们接下来看两个小程序。

第一个是服务端,它创建一个 UnixServer 实例,然后使用 server.accept 等待连接。当收到连接后则相互问候。

需要说明一下,acceptreadline 方法都会阻塞程序的执行,直到收到内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
require "socket"

server = UNIXServer.new('/tmp/simple.sock')

puts "==== Waiting for connection"
socket = server.accept

puts "==== Got Request:"
puts socket.readline

puts "==== Sending Response"
socket.write("I read you loud and clear, good buddy!")

socket.close

这里我们有了服务端,现在还需要客户端。

在下面这个例子中,我们打开由服务端创建的 socket,然后使用普通的 IO 方法进行发送和接收问候。

1
2
3
4
5
6
7
8
9
10
11
require "socket"

socket = UNIXSocket.new('/tmp/simple.sock')

puts "==== Sending"
socket.write("Hello server, can you hear me?\n")

puts "==== Getting Response"
puts socket.readline

socket.close

演示一下程序,先运行服务端,然后再运行客户端。你可以看到以下结果:

simple_ruby_socket_example 简单的 Unix socket 服务端/客户端交互的例子。左边是客户端,右边是服务端。

与 nginx 接合

现在我们知道如何创建一个 unix socket 的服务端了,我们可以很容易地与 nginx 接合。

不相信我?让我们来做一个快速的概念验证吧。我修改上面的代码使其输出从 socket 接收到的所有内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
require "socket"

# Create the socket and "save it" to the file system
server = UNIXServer.new('/tmp/socktest.sock')

# Wait until for a connection (by nginx)
socket = server.accept

# Read everything from the socket
while line = socket.readline
  puts line.inspect
end

socket.close

现在如果我修改 nginx 配置,将请求转发到 /tmp/socktest.sock socket 上。 我就能看到 nginx 发送来的数据了。(别担心,我们稍后会讨论它的配置的)

当我发起一个 web 请求时,nginx 将如下数据发送到我的服务端上:

request_http

太酷了!这就是一个包含了额外头信息的 HTTP 请求。现在我们准备来构建一个真正的应用服务。 但是,首先让我们讨论一个 nginx 的配置吧。

安装配置 Nginx

如果你还没有安装 nginx 的话,请先安装。对于 OSX 可以 homebrew 简单完成:

1
brew install nginx

现在我们配置 nginx 将 localhost:2048 的请求通过名为 /tmp/socktest.sock 的 socket 转发到上游服务端。 名字可以是任意的,它仅需要与我们 web 服务的 socket 名字匹配即可。

你可以将其保存至 /tmp/nginx.conf 并通过命令 nginx -c /tmp/nginx.conf 运行 nginx。

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
37
38
39
40
# Run nginx as a normal console program, not as a daemon
daemon off;

# Log errors to stdout
error_log /dev/stdout info;

events {} # Boilerplate

http {

  # Print the access log to stdout
  access_log /dev/stdout;

  # Tell nginx that there's an external server called @app living at our socket
  upstream app {
    server unix:/tmp/socktest.sock fail_timeout=0;
  }

  server {

    # Accept connections on localhost:2048
    listen 2048;
    server_name localhost;

    # Application root
    root /tmp;
 
    # If a path doesn't exist on disk, forward the request to @app
    try_files $uri/index.html $uri @app;

    # Set some configuration options on requests forwarded to @app
    location @app {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
      proxy_pass http://app;
    }

  }
}

以非后台模式运行 nginx。当你运行 nginx 时它应该看起来像如下这样:

nginx_non_daemon Nginx 以非后台模式运行

自定义应用服务

既然我们已经知道了如何将 nginx 与我们程序进行连接,那么我们就来构建一个简单的应用服务。 当 nginx 将请求转发到我们的 socket 时,它是一个标准的 HTTP 请求。 经过一些处理后我可以决定 socket 是否会返回一个有效的 HTTP 响应,它会在浏览器中显示。

下面这个应用接受任何请求并显示时间戳。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
require "socket"

# Connection creates the socket and accepts new connections
class Connection

  attr_accessor :path

  def initialize(path:)
    @path = path
    File.unlink(path) if File.exists?(path)
  end

  def server
    @server ||= UNIXServer.new(@path)
  end

  def on_request
    socket = server.accept
    yield(socket)
    socket.close
  end
end


# AppServer logs incoming requests and renders a view in response
class AppServer

  attr_reader :connection
  attr_reader :view

  def initialize(connection:, view:)
    @connection = connection
    @view = view
  end

  def run
    while true
      connection.on_request do |socket|
        while (line = socket.readline) != "\r\n"
          puts line
        end
        socket.write(view.render)
      end
    end
  end

end

# TimeView simply provides the HTTP response
class TimeView
  def render
%[HTTP/1.1 200 OK
 
 
The current timestamp is: #{ Time.now.to_i }
 
]
  end
end


AppServer.new(connection: Connection.new(path: '/tmp/socktest.sock'), view: TimeView.new).run

现在运行 nginx 和脚本,然后访问 localhost:2048。请求会发送到我的应用上,然后响应被浏览器渲染。太酷了!

appserver HTTP 请求信息由我们的应用服务输出到 STDOUT

以下就是我们的劳动成果。

timestamp 浏览器中显示服务端返回的时间戳

原文地址: http://blog.honeybadger.io/how-unicorn-talks-to-nginx-an-introduction-to-unix-sockets-in-ruby/

Ruby的内存陷阱

| Comments

Ruby有一套自动的内存管理机制。这在大多数情况下是不错的,但是有时它却是个麻烦。

Ruby的内存管理既简洁又笨重。它将对象(名为 RVALUE)存储在大约有16KB大小的堆中。 从底层上,RVALUE 是一个 C 的结构体,它包含了一个共同体表示不同的标准ruby对象。

因此在堆中存储着大小不超过40字节的 RVALUE 对象,如 StringArrayHash等。 这意味着小的对象在堆中很合适,但是一旦它们达到到阈值,那么就需要在Ruby的堆之外再分配一片额外的内存。

这块额外的内存空间是灵活的。一旦对象被垃圾回收了它就会被释放掉。但是堆本身的内存是不会被释放给操作系统的。

让我们来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def report
  puts 'Memory ' + `ps ax -o pid,rss | grep -E "^[[:space:]]*#{$$}"`
          .strip.split.map(&:to_i)[1].to_s + 'KB'
end

report
big_var = " " * 10_000_000
report
big_var = nil
report
ObjectSpace.garbage_collect
sleep 1
report

# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 11788KB

这里我们分配了大量的内存,使用完后又释放给操作系统。这一切看起来似乎没有问题。现在让我们稍微修改一下代码:

1
2
-  big_var = " " * 10_000_000
+  big_var = 1_000_000.times.map(&:to_s)

这只是一个简单的修改,不是吗。但是结果:

1
2
3
4
# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 57448KB

怎么回事?内存没有释放归还给操作系统。这是因为数组中的每个元素符合 RVALUE 的大小并存储在ruby的堆中。

在大多情况下这是正常的。现在ruby堆中多了许多空的位置,再次运行代码将不会再消耗额外的内存了。 每次我们处理 big_var 和一些空的堆时, GC[:heap_used]的值果然减小了。 对于这些操作Ruby是早有准备,注意这里是Ruby而不是操作系统。

因此,对于创建大量的符合40个字节的临时变量就要注意了:

1
2
big_var = " " * 10_000_000
big_var.gsub(/\s/) { |c| '-' }

结果同样是Ruby的内存疯狂增长,并且这部分内存在程序运行期间是不会归还给操作系统的:

1
2
3
4
# ⇒ Memory 10156KB
# ⇒ Memory 13788KB
# ⇒ Memory 13788KB
# ⇒ Memory 12808KB

这个问题不是太重要,稍微注意一下即可。

原文地址:http://rocket-science.ru/hacking/2013/12/17/ruby-memory-pitfalls/

未来的Ruby 3.0

| Comments

原文地址:http://hrnabi.com/2015/05/12/7035/ 谁日语好的为大家做下贡献,把原文翻译一下吧。

反正我不会日语,根据文中的一些汉字连蒙带猜的,大致看了一遍,现在简单总结一下。 如果有总结得不对的,本人不负责哦。

看来要玩好Ruby不仅需要学英语,也还得学日语啊。


在2014年9月举行的 RubyKaigi 2014 大会上,Matz在演讲过程中首次提到了 Ruby 3.0。
文中说的Ruby 3.0的三个工作方向:

  • Concurrency (并行性)
  • JIT (即时编译)
  • Static typing (静态类型)

Ruby要引入静态类型检查?
Matz说,在20世纪出生的语言大多是脚本语言,如:Ruby、PHP和Perl、JavaScript,这些都不是静态类型的。 另一方面,最近推出的如Scala和Dart、Go是属于静态类型的。 在Ruby中可以考虑引入Python这种通过注释来进行检查的方法。

Ruby要引入并行计算?
Matz详细讨论了静态类型,但没有提到并行计算。详细的内容是由笹田氏来说的。
(这部分内容我没看懂,只是大概知道这位笹田氏的博士论文与这个相关。)
关于并行计算,Matz提到了Erlang和Scala的actor模型。

最后期待下一个Ruby开发者大会。


好了,我只看懂了这么多。其余的各位感兴趣的自己去看原文吧。 (怎么感觉好坑啊,这总结得跟没总结一样啊。没办法了我的日语水平有限。)

Ruby中一些重要的钩子方法

| Comments

Ruby的哲学理念是基于一个基本的要素,那就是让程序员快乐。Ruby非常注重程序员的快乐,并且也提供了许多不同的方法来实现它。 它的元编程能力能够让程序员编写在运行时动态生成的代码。它的线程功能使得程序员有一种优雅的的方式编写多线程代码。 它的钩子方法能让程序员在程序运行时扩展它的行为。

上述的这些特性,以及一些其他很酷的语言方面,使得Ruby成为编写代码的优先选择之一。 本文将探讨Ruby中的一些重要的钩子方法。我们将从不同方面讨论钩子方法,如它们是什么,它们用于什么,以及我们如何使用它们来解决不同的问题。 我们同时也了解一下一些流行的Ruby框架/Gem包/库是如何使用它们来提供非常酷的特性的。

我们开始吧。

什么是钩子方法?

钩子方法提供了一种方式用于在程序运行时扩展程序的行为。 假设有这样的功能,可以在无论何时一个子类继承了一些特定的父类时收到通知, 或者是比较优雅地处理一个对象上的不可调用的方法而不是让编译器抛出异常。 这些情况就是使用钩子方法,但是它们的用法并不仅限于此。 不同的框架/库使用了不同的钩子方法来实现它们的功能。

在本文中我们将会讨论如下几个钩子方法:

  • included
  • extended
  • prepended
  • inherited
  • method_missing

included

Ruby给我们提供了一种方式使用 模块(modules) (在其他语言中被称作 混入类(mixins))来编写模块化的代码供其他的 模块/ 使用。 模块 的概念很简单,它就是一个可以在其他地方使用的独立代码块。

例如,如果我们想要编写一些代码在任何时候调用特定的方法都会返回一个静态字符串。 我们姑且将这个方法称作 name。你可能在其他地方也会想使用同一块代码。 这样最好是新建一个模块。让我们来创建一个:

1
2
3
4
5
module Person
  def name
    puts "My name is Person"
  end
end

这是一个非常简单的模块,仅有一个 name 方法用于返回一个静态字符串。在我们的程序中使用这个模块:

1
2
3
class User
  include Person
end

Ruby提供了一些不同的方法来使用模块include 是其中之一。include 所做的就是将在 module 内定义的方法在一个 class 的实例变量上可用。 在我们的例子中,是将 Person 模块中定义的方法变为一个 User 类实例对象的方法。 这就相当于我们是将 name 方法写在 User 类里一样,但是定义在 module 里的好处是可复用。 要调用 name 方法我们需要创建一个 User 的实例对象,然后再在这个对象上调用 name 方法。例如:

1
2
User.new.name
=> My name is Person

让我们看看基于 include 的钩子方法。included 是Ruby提供的一个钩子方法,当你在一些 module 或者 classinclude 了一个 module 时它会被调用。 更新 Person 模块:

1
2
3
4
5
6
7
8
9
module Person
  def self.included(base)
    puts "#{base} included #{self}"
  end

  def name
    "My name is Person"
  end
end

你可以看到一个新的方法 included 被定义为 Person 模块的类方法。当你在其他的模块或者类中执行 include Person 时,这个 included 方法会被调用。 该方法接收的一个参数是对包含该模块的类的引用。试试运行 User.new.name,你会看到如下的输出:

1
2
User included Person
My name is Person

正如你所见,base 返回的是包含该模块的类名。现在我们有了一个包含 Person 模块的类的引用,我们可以通过元编程来实现我们想要的功能。 让我们来看看 Devise是如何使用 included 钩子的。

Devise中的 included

Devise是Ruby中使用最广泛的身份验证gem包之一。它主要是由我喜欢的程序员 José Valim 开发的,现在是由一些了不起的贡献者在维护。 Devise为我们提供了从注册到登录,从忘记密码到找回密码等等完善的功能。它可以让我们在用户模型中使用简单的语法来配置各种模块:

1
devise :database_authenticatable, :registerable, :validatable

在我们模型中使用的 devise 方法在这里定义。 为了方便我将这段代码粘贴在下面:

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
def devise(*modules)
  options = modules.extract_options!.dup

  selected_modules = modules.map(&:to_sym).uniq.sort_by do |s|
    Devise::ALL.index(s) || -1  # follow Devise::ALL order
  end

  devise_modules_hook! do
    include Devise::Models::Authenticatable

    selected_modules.each do |m|
      mod = Devise::Models.const_get(m.to_s.classify)

      if mod.const_defined?("ClassMethods")
        class_mod = mod.const_get("ClassMethods")
        extend class_mod

        if class_mod.respond_to?(:available_configs)
          available_configs = class_mod.available_configs
          available_configs.each do |config|
            next unless options.key?(config)
            send(:"#{config}=", options.delete(config))
          end
        end
      end

      include mod
    end

    self.devise_modules |= selected_modules
    options.each { |key, value| send(:"#{key}=", value) }
  end
end

在我们的模型中传给 devise 方法的模块名将会作为一个数组保存在 *modules 中。 对于传入的模块调用 extract_options! 方法提取可能传入的选项。 在11行中调用 each 方法,并且每个模块在代码块中用 m 表示。 在12行中 m 将会转化为一个常量(类名),因此使用 m.to.classify 一个例如 :validatable 这样的符号会变为 Validatable 。 随便说一下 classify 是ActiveSupport的方法。
Devise::Models.const_get(m.to_classify) 会获取该模块的引用,并赋值给 mod。 在27行使用 include mod 包含该模块。 例子中的 Validatable 模块是定义在这里Validatableincluded 钩子方法定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def self.included(base)
  base.extend ClassMethods
  assert_validations_api!(base)

  base.class_eval do
    validates_presence_of   :email, if: :email_required?
    validates_uniqueness_of :email, allow_blank: true, if: :email_changed?
    validates_format_of     :email, with: email_regexp, allow_blank: true, if: :email_changed?

    validates_presence_of     :password, if: :password_required?
    validates_confirmation_of :password, if: :password_required?
    validates_length_of       :password, within: password_length, allow_blank: true
  end
end

此时模型是 base。在第5行的 class_eval 代码块会以该类作为上下文进行求值运算。 通过 class_eval 编写的代码与直接打开该类的文件将代码粘贴进去效果是一样的。 Devise是通过 class_eval 将验证包含到我们的用户模型中的。

当我们试着使用Devise注册或者登录时,我们会看到这些验证,但是我们并没有编写这些验证代码。 Devise是利用了 included 钩子来实现这些的。非常的优雅吧。

extended

Ruby也允许开发者 扩展(extend) 一个模块,这与 包含(include) 有点不同。 extend 是将定义在 模块(module) 内的方法应用为类的方法,而不是实例的方法。 让我们来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
module Person
  def name
    "My name is Person"
  end
end

class User
  extend Person
end

puts User.name # => My name is Person

正如你所看到的,我们将 Person 模块内定义的 name 方法作为了 User 的类方法调用。 extendPerson 模块内的方法添加到了 User 类中。extend 同样也可以用于将模块内的方法作为单例方法(singleton methods)。 让我们再来看另外一个例子:

1
2
3
4
5
6
7
8
9
# We are using same Person module and User class from previous example.     

u1 = User.new
u2 = User.new

u1.extend Person

puts u1.name # => My name is Person
puts u2.name # => undefined method `name' for #<User:0x007fb8aaa2ab38> (NoMethodError)

我们创建了两个 User 的实例对象,并将 Person 作为参数在 u1 上调用 extend 方法。 使用这种调用方式,Personname 方法仅对 u1 有效,对于其他实例是无效的。

正如 included 一样,与 extend 相对应的钩子方法是 extended。 当一个模块被其他模块或者类执行了 extend 操作时,该方法将会被调用。 让我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Modified version of Person module

module Person
  def self.extended(base)
    puts "#{base} extended #{self}"
  end

  def name
    "My name is Person"
  end
end

class User
  extend Person
end

该代码的运行结果是输出 User extended Person

关于 extended 的介绍已经完了,让我们来看看 ActiveRecord 是如何使用它的。

ActiveRecord中的 extended

ActiveRecord 是在 Ruby 以及 Rails 中广泛使用的ORM框架。它具有许多酷的特性, 因此使用它在很多情况下成为了ORM的首选。让我们进入 ActiveRecord 内部看看 ActiveRecord 是如何使用回调的。 (我们使用的是 Rails v3.2.21)

ActiveRecord这里 extendActiveRecord::Models 模块。

1
extend ActiveModel::Callbacks

ActiveModel 提供了一套在模型类中使用的接口。它们允许 ActionPack 与不是 ActiveRecord 的模型进行交互。 在这里ActiveModel::Callbacks 内部你将会看到如下代码:

1
2
3
4
5
def self.extended(base)
  base.class_eval do
    include ActiveSupport::Callbacks
  end
end

ActiveModel::Callbacksbase 即就是 ActiveRecord::Callbacks 调用了 class_eval 方法, 并包含了 ActiveSupport::Callbacks 模块。我们前面已经提到过了,对一个类调用 class_eval 与手动地将代码写在这个类里是一样的。 ActiveSupport::CallbacksActiveRecord::Callbacks 提供了 Rails 中的回调方法。

这里我们讨论了 extend 方法,以及与之对应的钩子 extended。并且也了解了 ActiveRecord / ActiveModel 是如何使用上述方法为我们提供可用功能的。

prepended

另一个使用定义在模块内部方法的方式称为 prependprepend 是在Ruby 2.0中引入的,并且与 includeextend 很不一样。 使用 includeextend 引入的方法可以被目标模块/类重新定义覆盖。 例如,如果我们在某个模块中定义了一个名为 name 的方法,并且在目标模块/类中也定义同名的方法。 那么这个在我们类在定义的 name 方法将会覆盖模块中的。而 prepend 是不一样的,它会将 prepend 引入的模块 中的方法覆盖掉我们模块/类中定义的方法。让我们来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module Person
  def name
    "My name belongs to Person"
  end
end

class User
  include Person
  def name
    "My name belongs to User"
  end
end

puts User.new.name
=> My name belongs to User

现在再来看看 prepend 的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module Person
  def name
    "My name belongs to Person"
  end
end

class User
  prepend Person
  def name
    "My name belongs to User"
  end
end

puts User.new.name
=> My name belongs to Person

使用 prepend Person 会将 User 中的同名方法给覆盖掉,因此在终端输出的结果为 My name belongs to Personprepend 实际上是将方法添加到方法链的前端。在调用 User 类内定义的 name 方法时,会调用 super 从而调用 Person 模块的 name

prepend 对应的回调名为(你应该猜到了) prepended。当一个模块被预置到另一个模块/类中时它会被调用。 我们来看下效果。更新 Person 模块的定义:

1
2
3
4
5
6
7
8
9
module Person
  def self.prepended(base)
    puts "#{self} prepended to #{base}"
  end

  def name
    "My name belongs to Person"
  end
end

你再运行这段代码应该会看到如下结果:

1
2
Person prepended to User
My name belongs to Person

prepend 的引入是为了去除 alias_method_chain hack的丑陋,它曾被Rails以及其他库广泛地使用以达到与 prepend 相同的功能。 因为 prepend 只有在 Ruby >= 2.0 的版本中才能使用,因此如果你打算使用 prepend 的话,那么你就应该升级你的Ruby版本。

inherited

继承是面向对象中一个最重要的概念。Ruby是一门面向对象的编程语言,并且提供了从基/父类继承一个子类的功能。 我们来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
class Person
  def name
     "My name is Person"
  end
end

class User < Person
end

puts User.new.name # => My name is Person

我们创建了一个 Person 类和一个子类 User。在 Person 中定义的方法也成为了 User 的一部分。 这是非常简单的继承。你可能会好奇,是否有什么方法可以在一个类被其他类继承时收到通知呢? 是的,Ruby有一个名为 inherited 的钩子可以实现。我们再看看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person
  def self.inherited(child_class)
    puts "#{child_class} inherits #{self}"
  end

  def name
    "My name is Person"
  end
end

class User < Person
end

puts User.new.name

正如你所见,当 Person 类被其他子类继承时 inherited 类方法将会被调用。 运行以上代码结果如下:

1
2
User inherits Person
My name is Person

让我们看看 Rails 在它的代码中是如何使用 inherited 的。

Rails中的 inherited

Rails应用中有一个重要的类名为 Application ,定义中 config/application.rb 文件内。 这个类执行了许多不同的任务,如运行所有的Railties,引擎以及插件的初始化。 关于 Application 类的一个有趣的事件是,在同一个进程中不能运行两个实例。 如果我们尝试修改这个行为,Rails将会抛出一个异常。让我们来看看Rails是如何实现这个特性的。

Application 类继承自 Rails::Application,它是在这里定义的。 在62行定义了 inherited 钩子,它会在我们的Rails应用 Application 类继承 Rails::Application 时被调用。 inherited 钩子的代码如下:

1
2
3
4
5
6
7
8
9
class << self
  def inherited(base)
    raise "You cannot have more than one Rails::Application" if Rails.application
    super
    Rails.application = base.instance
    Rails.application.add_lib_to_load_path!
    ActiveSupport.run_load_hooks(:before_configuration, base.instance)
  end
end

class << self 是Ruby中的另一个定义类方法的方式。在 inherited 中的第1行是检查 Rails.application 是否已存在。 如果存在则抛出异常。第一次运行这段代码时 Rails.application 会返回false然后调用 super。 在这里 super 即是 Rails::Engineinherited 钩子,因为 Rails::Application 继承自 Rails::Engine

在下一行,你会看到 Rails.application 被赋值为 base.instance 。其余就是设置Rails应用了。

这就是Rails如何巧妙地使用 inherited 钩子来实现我们的Rails Application 类的单实例。

method_missing

method_missing 可能是Ruby中使用最广的钩子。在许多流行的Ruby框架/gem包/库中都有使用它。 当我们试图访问一个对象上不存在的方法时则会调用这个钩子方法。 让我们来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
class Person
  def name
    "My name is Person"
  end
end

p = Person.new

puts p.name     # => My name is Person 
puts p.address  # => undefined method `address' for #<Person:0x007fb730a2b450> (NoMethodError)

我们定义了一个简单的 Person 类, 它只有一个 name 方法。然后创建一个 Person 的实例对象, 并分别调用 nameaddress 两个方法。因为 Person 中定义了 name,因此这个运行没问题。 然而 Person 并没有定义 address,这将会抛出一个异常。 method_missing 钩子可以优雅地捕捉到这些未定义的方法,避免此类异常。 让我们修改一下 Person 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person
  def method_missing(sym, *args)
     "#{sym} not defined on #{self}"
  end

  def name
    "My name is Person"
  end
end

p = Person.new

puts p.name     # => My name is Person
puts p.address  # => address not defined on #<Person:0x007fb2bb022fe0>

method_missing 接收两个参数:被调用的方法名和传递给该方法的参数。 首先Ruby会寻找我们试图调用的方法,如果方法没找到则会寻找 method_missing 方法。 现在我们重载了 Person 中的 method_missing,因此Ruby将会调用它而不是抛出异常。

让我们来看看 Rake 是如何使用 method_missing 的。

Rake中的 method_missing

Rake 是Ruby中使用最广泛的gem包之一。Rake 使用 method_missing 来提供访问传递给Rake任务的参数。 首先创建一个简单的rake任务:

1
2
3
task :hello do
  puts "Hello"
end

如果你通过调用 rake hello 来执行这个任务,你会看到输出 Hello。 让我们扩展这个rake任务,以便接收一个参数(一个人名)并向他打招呼:

1
2
3
task :hello, :name do |t, args|
  puts "Hello #{args.name}"
end

t 是任务名,args 保存了传递过来的参数。正如你所见,我们调用 args.name 来获取传递给 hello 任务的 name 参数。 运行该任务,并传递一个参数:

1
2
rake hello["Imran Latif"]
=> Hello Imran Latif

让我们来看看 Rake 是如何使用 method_missing 为我们提供了传递给任务的参数的。

在上面任务中的 args 对象是一个 Rake::TaskArguments 实例,它是在这里所定义。 这个类负责管理传递给Rake任务的参数。查看 Rake::TaskArguments 的代码,你会发现并没有定义相关的方法将参数传给任务。 那么 Rake 是如何将参数提供给任务的呢?答案是 Rake 是使用了 method_missing 巧妙地实现了这个功能。 看看第64行 method_missing 的定义:

1
2
3
def method_missing(sym, *args)
  lookup(sym.to_sym)
end

在这个类中定义 method_missing 是为了保证能够访问到那些未定义的方法,而不是由Ruby抛出异常。 在 method_missing 中它调用了 lookup 方法:

1
2
3
4
5
6
7
def lookup(name)
  if @hash.has_key?(name)
   @hash[name]
  elsif @parent
    @parent.lookup(name)
  end
end

method_missing 调用 lookup,并将方法名以 Symbol(符号) 的形式传递给它。 lookup 方法将会在 @hash 中进行查找,它是在 Rake::TaskArguments 的构造函数中创建的。 如果 @hash 中包含该参数则返回,如果在 @hash 中没有则 Rake 会尝试调用 @parentlookup。 如果该参数没有找到,则什么都不返回。

这就是 Rake 如何巧妙地使用 method_missing 提供了访问传递给Rake任务的参数的。 感谢Jim Weirich编写了Rake。

结束语

我们讨论了5个重要的Ruby钩子方法,探索了它们是如何工作的,以及一些流行的框架/gem包是如何使用它们来提供一些优雅的功能。 我希望你能喜欢这篇文章。请在评论中告诉我们你所喜欢的Ruby钩子,以及你使用它们所解决的问题。

原文地址: http://www.sitepoint.com/rubys-important-hook-methods/

使用Unicorn部署rails应用

| Comments

玩 rails 也有段时间了,最近研究下怎么部署一个 rails 应用。在几年前的话要部署 rails 应用是件很麻烦的事, 但是近几年出现了一些比较好的工具可以方便的进行 rails 部署。如: Unicorn、thin、Passenger等。

Unicorn 是一个 Rack 应用的HTTP服务器。之前玩 Python 的时候也有一个 Gunicorn ,使用它来部署 Python 的 Web 应用 也很方便,可以参考我之前的那篇文件 《使用gunicorn部署Django》

接下来简单分享下使用 Nginx + Unicorn 来部署 rails 的配置。

安装

首先安装 unicorn 包: $ gem install unicorn

然后编译一下静态文件:

1
2
$ RAILS_ENV=production rake assets:clean
$ RAILS_ENV=production rake assets:precompile

下载配置文件: $ curl -o config/unicorn.rb https://raw.github.com/defunkt/unicorn/master/examples/unicorn.conf.rb

接着根据情况修改相关配置,如: working_directory、listen 等。 例如我的是需要同时监听网络端口和 sock 文件,那么我的 listen 设置如下:

1
2
listen "#{root_path}/tmp/sockets/unicorn.sock", :backlog => 64
listen 8081, :tcp_nopush => true

配置 Nginx

然后配置 Nginx 的反向代理,以下是我的 Nginx 配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
upstream rails_server {
  server unix:/app_path/tmp/sockets/unicorn.sock fail_timeout=0;
}

server {
  listen 80;
  server_name webserver localhost;

  root /app_path/public;

  try_files $uri $uri @unicorn;

  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://rails_server;
  }
 
  location ~ /\.ht {
    deny  all;
  }
}

启动服务

配置完成之后,最后启动服务。

1
2
$ bundle exec unicorn_rails -c config/unicorn.rb -D -E production
$ sudo service nginx start

然后再在浏览器中访问试试。

Vagrant使用笔记

| Comments

Vgrant是一个基于 Ruby 使用 Virtualbox 进行创建和部署虚拟化环境的工具。 类似的工具之前有使用过 Docker。就我个人而言这两款工具之间,Docker是轻量级的VM, 因此性能应该会比较好,但是只能在64位的系统下使用。 而 Vgrant 是使用 Virtualbox 进行虚拟化,因此性能上不及 Docker, 不过它可以在32/64位的 Linux、Windows 等系统上运行。

我觉得 Vgrant 比较适合用于在开发环境中使用,而 Docker 比较适合用于生产环境。

安装

首先安装 virtualbox,然后再安装 Vgrant。

1.通过源代码安装

1
2
3
4
git clone https://github.com/mitchellh/vagrant
cd vagrant
bundle install
rake install

2.通过安装包安装
根据情况选择下载对应的安装包: http://www.vagrantup.com/downloads.html

注意:如果是 Windows 系统,可能还需要将 Vgrant 的路径添加到环境变量中,以便使用 vgrant 命令。

使用

Vagrant 的使用方法也很简单,基本如下:

1
2
3
4
5
6
7
8
# 这里我先添加一个 ArchLinux 的镜像
vagrant box add archlinux http://vagrant.srijn.net/archlinux-x64-2014-01-07.box
# 进行初始化
vagrant init archlinux
# 运行虚拟机
vagrant up
# 如果需要进行ssh连接到虚拟机中进行一些操作,可以执行该命令
vagrant ssh

其他的一些命令:

1
2
3
4
# 关闭虚拟机
vagrant halt
# 删除创建的虚拟机
vagrant destroy

vagrant的一些镜像: http://www.vagrantbox.es/

Redis集群配置实例

| Comments

通过配置 redis 的主从集群可将请求的负荷分散到多台服务器上。

redis 的集群配置比较简单,以下是一个例子。 假设有如下三台主机:

  • 172.17.0.11 (主)
  • 172.17.0.12 (从)
  • 172.17.0.13 (从)

在从服务器上添加如下配置:

1
slaveof 172.17.0.11 6379

如果主服务器设置了认证密码,那么还需要再添加一条配置:

1
masterauth <password>

然后分别启动三台服务器的 redis 服务即可。

接下来连接主服务器添加一些数据测试一下。

1
2
3
$ redis-cli -h 172.17.0.11
172.17.0.11:6379> set foo1 bar1
OK

然后再连接到从服务器查询结果。

1
2
3
$ redis-cli -h 172.17.0.12
172.17.0.12:6379> get foo1
"bar1"

这时发现数据已经同步过来了。

注意:从服务器默认是只读的。如果需要设置为可写,可将 slave-read-only 设置项的值设为 no 即可。

Mongodb分片配置实例

| Comments

数据分片即是从一个集合中选择一个片键(shard key)作为数据拆分的依据,原理与索引类似,然后将集合的数据拆分并保存到不同的服务器上。 以下通过一个例子来介绍一下Mongodb的分片配置。

有四台主机:

  • 172.17.0.6 (配置服务器)
  • 172.17.0.7 (mongos)
  • 172.17.0.8 (片服务器)
  • 172.17.0.9 (片服务器)

1.在 172.17.0.6 上启动 mongod 服务作为配置服务器;
修改配置,使其作为一个配置服务器,默认监听 27019 端口。

1
configsvr = true

启动服务 $ service mongodb start

2.在 172.17.0.7 上启动 mongos 服务作为路由服务;
建立mongos进程。(可以有多台配置服务器),用法如下:

1
$ mongos --configdb <config server hostnames>[,<config server hostnames>]

例如: $ mongos --configdb 172.17.0.6:27019

注意:在同一个分片集群中的每个 mongos 必须使用相同的 configDB 配置。

3.添加分片
一个片服务既可以是单个 mongod 实例,也可以是一个副本集。
1).先分别在 172.17.0.8 和 172.17.0.9 上启动片服务器,即就是一个普通的 mongod 服务。

1
$ service mongodb start

2).使用 mongo 客户端连接到 mongos 服务

1
$ mongo --host <hostname of machine running mongos> --port <port mongos listens on>

如: $ mongo --host 172.17.0.7 --port 27017

3).在 mongo 客户端上执行命令添加分片:

1
2
3
4
> use admin
> db.auth(<user>, <pswd>)
> sh.addShard("172.17.0.8:27017")
> sh.addShard("172.17.0.9:27017")

4.切片数据
1).首先对数据库进行切片
使用 mongo 客户端连接到 mongos ,执行命令打开数据库的分片功能,用法如下:

1
> sh.enableSharding("<database>")

例如要打开 mydb 数据库的分片功能: > sh.enableSharding("mydb")

2).然后对数据集合进行切片
命令用法如下:

1
> sh.shardCollection("<database>.<collection>", shard-key-pattern)

shard-key-pattern 与索引的用法一样,例如,要对 mydb 数据库的 test 集合按照 _id 字段进行分片: > sh.shardCollection("mydb.test", {"_id": "hashed"})

接下来通过一个程序来测试一下,向数据库中添加10000条数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env ruby
#-*- coding:utf-8 -*-

require "mongo"

begin
  conn = Mongo::Connection.new '172.17.0.7'
  db = conn['mydb']
rescue Exception=>e
  p e
  exit 1
end

i = 0
while i < 10000
  d = {'no' => i}
  d = db['test'].insert(d)
  i += 1
  puts d
end

然后分别查看 172.17.0.8 和 172.17.0.9 的状态:

1
2
3
4
5
6
7
172.17.0.8:
> db.test.count()
4952

172.17.0.9:
> db.test.count()
5048

数据基本上是平均的分布在两台服务器上。

参考: http://docs.mongodb.org/manual/tutorial/deploy-shard-cluster/

使用gunicorn部署Django

| Comments

Gunicorn 是 Python的 一个 WSGI HTTP服务器,根据它的介绍说是它来自于 Ruby 的 Unicorn。可以方便的部署 Python 的 Web 程序,而且本身支持多种 Python 的框架,如 Django、Paster等。

通过介绍来看貌似很不错的样子,只可惜我现在不玩 Python 了,于是就简单体验一下。

简单应用

首先是安装,这个可以直接使用 pip 来完成:

1
$ pip install gunicorn

然后再根据官方文档的介绍部署一个简单的例子试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cd examples
$ cat test.py
# -*- coding: utf-8 -
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.

def app(environ, start_response):
    """Simplest possible application object"""
    data = 'Hello, World!\n'
    status = '200 OK'
    response_headers = [
        ('Content-type','text/plain'),
        ('Content-Length', str(len(data)))
    ]
    start_response(status, response_headers)
    return iter([data])

$ gunicorn -b 0.0.0.0:8000 --workers=2 test:app

好的,现在程序运行起来了,可以访问 http://localhost:8000 看下效果。

gunicorn 也可以通过配置文件来设置一些内容, 一个配置文件是一个 python 脚本,格式类似 .ini 。通过 -c 参数指定要使用的配置文件。如:

1
2
3
# config.ini
bind = ["0.0.0.0:8000", "unix:///tmp/gunicorn.sock"]
workers = 3 

gunicorn 还能与 Django 和 Paster 应用集成:

1
2
$ gunicorn --env DJANGO_SETTINGS_MODULE=myproject.settings myproject.wsgi:application
$ gunicorn --paste development.ini -b :8080 --chdir /path/to/project

与 Nginx 部署

gunicorn 本身也是一个 WSGI 应用,可以与 Nginx 一同使用。 以下是 Nginx + Gunicorn 部署 Django 的事例, Nginx 配置如下:

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
37
38
39
40
41
42
# nginx.conf
http {
    include mime.types;
    default_type application/octet-stream;
    access_log /tmp/nginx.access.log combined;
    sendfile on;

    upstream app_server {
        server unix:/tmp/gunicorn.sock fail_timeout=0;
        # For a TCP configuration:
        # server 192.168.0.7:8000 fail_timeout=0;
    }

    server {
        listen 80 default;
        client_max_body_size 4G;
        server_name _;

        keepalive_timeout 5;

        # path for static files
        root /path/to/app/current/public;

        location / {
            # checks for static file, if not found proxy to app
            try_files $uri @proxy_to_app;
        }

        location @proxy_to_app {
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_redirect off;

            proxy_pass   http://app_server;
        }

        error_page 500 502 503 504 /500.html;
        location = /500.html {
            root /path/to/app/current/public;
        }
    }
}

Gunicorn 的配置文件:

1
2
3
4
5
6
7
8
9
10
11
# gunicorn.ini
import os

bind = ["0.0.0.0:8000", "unix:///tmp/gunicorn.sock"]
workers = 3
chdir = os.path.dirname(os.path.realpath(__file__))
raw_env = ["DJANGO_SETTINGS_MODULE=app.settings"]
accesslog = "/tmp/gunicorn-access.log"
errorlog = "/tmp/gunicorn.log"
daemon = True
pidfile = "/tmp/gunicorn.pid"

运行:

1
2
$ gunicorn -c gunicorn.ini myproject.wsgi:application
$ service nginx start

其他内容

与 WSGI 应用一样,如果之后配置有改动可以向 gunicorn 服务进程发送 HUP 信号让其重新加载配置:

1
$ kill -s HUP <pid>

Mongodb集群配置实例

| Comments

Mongodb的集群有两种,一个是主从复制,另一种是副本集。

主从复制

根据 Mongodb 的官方文档说明,在生产环境中建议使用副本集代替主从复制。 http://docs.mongodb.org/manual/core/master-slave/

不过对于主从复制还是可以了解一下。假设有如下三台主机:

  • 172.17.0.4 (主)
  • 172.17.0.5 (从)
  • 172.17.0.6 (从)

要进行主从复制的配置,首先修改主服务器的配置信息:

1
2
master = true         # 以主服务器模式启动
bind_ip = 0.0.0.0

然后修改另两台从服务器配置信息:

1
2
3
slave = true
source = 172.17.0.4
bind_ip = 0.0.0.0

最后启动三台主机上的 Mongodb 服务,再通过一个简单的程序来测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env ruby
#-*- coding:utf-8 -*-

require "mongo"

begin
  conn = Mongo::Connection.new '172.17.0.4'
  db = conn['test']
rescue Exception=>e
  p e
  exit 1
end

i = 0
while i < 100
  d = {'no' => i}
  d = db['data'].insert(d)
  i += 1
  puts d
end

执行该脚本,向 172.17.0.4 主机的 Mongodb 中插入一些数据。然后发现数据被同步到了另外两台主机上。

主从之间安全认证:
如果启动了 auth 项,那么主从之间的认证需要使用 keyFile 选项。

执行如下命令生成 key 文件,并设置为只有 mongodb 的进程用户可读写:

1
2
3
$ openssl rand -base64 741 > /path/mongodb_keyFile
$ chmow 600 /path/mongodb_keyFile
$ chown mongodb:mongodb /path/mongodb_keyFile

将该文件复制到这三台主机中,然后分别修改主从的配置信息:

1
keyFile = /path/mongodb_keyFile

副本集

同样的对于这三台主机,我们重新修改配置设置为副本集的形式。

  • 172.17.0.4
  • 172.17.0.5
  • 172.17.0.6

首先修改配置文件,设置副本集的名字。
注意:副本集中所有主机设置的名字需要一样。这里我们设为 myrepl0
注意:设置副本集之前各个 mongodb 的数据目录必须都为空。

1
replSet = myrepl0

接着启动所有 mongodb 服务,然后对副本集进行初始化。
连接任意一台 mongodb 服务,执行如下操作:

1
2
3
4
5
> rs.initiate({'_id': 'myrepl0', 'members': [
    {'_id': 1, 'host': '172.17.0.4:27017'},
    {'_id': 2, 'host': '172.17.0.7:27017'},
    {'_id': 3, 'host': '172.17.0.8:27017'}
]})

现在副本集的初始化已完成,可以通过如下命令查看状态:

1
> rs.status()

在运行过程中可以随时添加或移除一个节点,如:

1
2
rs.add("172.17.0.8:27017")
rs.remove("172.17.0.8:27017")

可以再通过上面的程序添加一些数据。然后再连接到任意一台主机进行查询,看看数据是否已同步。

详细内容可参考文档: http://docs.mongodb.org/manual/core/replication/

安全认证:

1.禁用 auth 选项和 replSet 选项再运行 mongodb

2.连接到该 mongodb 服务并创建用户

1
2
3
4
5
6
7
8
9
10
> use admin
switched to db admin
> db.addUser('root','root')
{
        "user" : "root",
        "readOnly" : false,
        "pwd" : "2a8025f0885adad5a8ce0044070032b3",
        "_id" : ObjectId("54745351f79804bd44b596fb")
}
>

3.重新以 auth、keyFile 和 replSet 模式启动 mongodb

4.连接到刚刚创建用户的 mongodb 服务

5.跟之前的步骤一样,配置副本集

1
2
3
4
5
> rs.initiate({'_id': 'myrepl0', 'members': [
    {'_id': 1, 'host': '172.17.0.4:27017'},
    {'_id': 2, 'host': '172.17.0.7:27017'},
    {'_id': 3, 'host': '172.17.0.8:27017'}
]})

参考: http://docs.mongodb.org/manual/tutorial/deploy-replica-set-with-auth/