NATS是CloudFoundry内部的神经系统,是一款基于EventMachine、使用“发布--订阅”机制的轻量级消息中间件。基于EM的特点使得NATS在Ruby环境下有着处理高并发请求的能力。NATS对消息本身不做持久化,所以匹配和订阅的过程比较简洁高效。目前,NATS server是CF中一处需要解决的单点依赖。
NATS主要的依赖gem包包括:eventmachine, json_pure, daemons, thin
NATS的代码结构比较简单:
源码地址: https://github.com/derekcollison/nats/
lib
nats/client.rb NATS的客户端
nats/server.rb 启动NATS server的入口类
nats/server/server.rb NATS server的实际代码
nats/server/connection.rb 网络连接和消息处理的部分
nats/server/sublist.rb 订阅者管理的部分
nats/server/options.rb NATS的启动选项
nats/ext/ 对JRuby和Ruby1.8的补充部分
另外,在github上的cluster分支下,我们还可以看到NATS Cluster的代码:
lib
nats/server/cluster.rb NATS server集群的代码
nats/server/route.rb NATS路由集群的代码
NOTE:在阅读代码之前,我们最好先按照github上readme部分熟悉下NATS的使用。
一、/lib/nats/server.rb
NATSD::Server.start_http_server EventMachine::start_server(NATSD::Server.host, NATSD::Server.port, NATSD::Connection)
这里,我们就可以看到NATS与EM的密切关系,请尝试对比一下下面两段代码:
左边部分是我们之前见过的启动一个”Echo server“的简单例子,而右边则是NATS中启动核心NATS server的方法。没错NATS就是基于EM的网络编程的典型例子。在接下来的NATS#connection部分,我们还可以看到更多这样的例子。
2、HTTP Monitor Server,一个基于Thin的监控服务器。跟CF其他组件一样,这个monitor server主要用来响应/varz和/healthz请求。
# Check to see if we need to fire up the http monitor port and server if NATSD::Server.options[:http_port] begin NATSD::Server.start_http_server rescue => e log "Could not start monitoring server on port #{NATSD::Server.options[:http_port]}" log_error exit(1) end end
查看/lib/nats/server/options.rb中关于:http_port的定义,我们可以看到启动monitor的方法:
opts.on("-m", "--http_port PORT", "Use HTTP PORT ")。没错,就是加参数-m。
比如,我们访问localhost:http_port/varz,可以看到这个nats-server的信息。
{ "in_bytes": 0, "out_bytes": 0, "cpu": 3.2, "start": "Tue Oct 09 17:15:54 +0800 2012", "connections": 0, "options": { "max_pending": 2000000, "users": [ { "pass": "nats", "user": "nats" } ], ... ...
而如果我们访问/healthz,会收到一个“ok”,这与CloudFoundry component的机制也是类似的:
@healthz = "ok\n" ... ... map '/healthz' do run lambda { |env| [200, RACK_TEXT_HDR, NATSD::Server.healthz] } end
二、核心数据机构——Sublist
Sublist用来描述订阅了某个topic的订阅者(subs)有哪些。subs注册的topic以点号分隔成为token,NATS支持两种通配符:>和*,他们的定义如下
# "*" matches any token, at any level of the subject. like *.foo foo.*bar
# ">" matches any length of the tail of a subject and can only be the last token# E.g. 'foo.>' will match 'foo.bar', 'foo.bar.baz', 'foo.foo.bar.bax.22'
比如:
发布者的主题为foo.bar,那么订阅了foo.bar和foo.*的订阅者都是会响应的订阅者
发布者的主题为foo.bar.hoge,那么订阅了foo.>的订阅者是会响应的订阅者,而定于了foo.*的订阅者则是无效的
NATS的主题匹配的机制是通过两种数据结构来描述的:level和node
SublistLevel = Struct.new(:nodes, :pwc, :fwc)
SublistNode = Struct.new(:leaf_nodes, :next_level)
如果想搞清楚上述数据结构的含义,我们不妨从下面添加订阅者的Sublist#insert方法来入手大胆猜想一下:
... for token in tokens # This is slightly slower than direct if statements, but looks cleaner. case token when FWC then node = (level.fwc || (level.fwc = SublistNode.new([]))) when PWC then node = (level.pwc || (level.pwc = SublistNode.new([]))) else node = ((level.nodes[token]) || (level.nodes[token] = SublistNode.new([]))) end level = (node.next_level || (node.next_level = SublistLevel.new({}))) end node.leaf_nodes.push(subscriber) ...通过阅读下面的insert方法,我们分析出Node与Level的关系应该是下面这个样子:
结合上图,我们可以看到:
level存储的内容包括
node存储的内容包括:
所以,当NATS添加一个订阅者时,首先需要遍历该sub的主题:
1、然后从sublist的root开始,查看每一个level中是否有该token对应的node,比如node1=nodes['A'](或者是与通配符关联的,比如node3),记录下这个node的next_level。当然,如果当前的sublist里没有这样的node的话,直接新建出来。
2、继续遍历。如果主题的token都已经遍历完了,就直接在上次遍历得到的——比如node1——的leaf_nodes中添加本次参加订阅的所有订阅者(subs)
3、否则的话,按照next_level继续进行遍历,直到2步中遍历结束的情况出现
当插入操作结束后,Sublist的结构大致如下图所示。
而当NATS需要按照指定主题找到对应的subs时,实际上是在上图按照下面的顺序进行搜索:(每个token一个迭代)
token -> node -> level -> token -> node -> level -> ... -> token -> node-> leaf_nodes,好了,这个leaf_nodes数组就是订阅了token.token...token这个主题的订阅者集合了。
在这样的机制下,通配符和非通配符的查询情况是完全类似的,只是如果一个token是pwc的话,就应该在2步中匹配到订阅任何token的nodes;而fwc意味着这个token必然是一个主题的最后一个token,而我们只要拿到这个fwc关联的node的leaf_nodes就可以了。
有了上述sublist建立的基础,下面这段匹配方法的实现就很好理解了:
... tokens = subject.split('.') matchAll(@root, tokens) ... def matchAll(level, tokens) node, pwc = nil, nil # Define for scope i, ts = 0, tokens.size while (i < ts) do return if level == nil # Handle a full wildcard here by adding all of the subscribers. @results.concat(level.fwc.leaf_nodes) if level.fwc # Handle an internal partial wildcard by branching recursively lpwc = level.pwc matchAll(lpwc.next_level, tokens[i+1, ts]) if lpwc node, pwc = level.nodes[tokens[i]], lpwc #level = node.next_level if node level = node ? node.next_level : nil i += 1 end @results.concat(pwc.leaf_nodes) if pwc @results.concat(node.leaf_nodes) if node end
1、遍历到的各level中不含通配符。这时的匹配过程就是遍历tokens数组,按照前面提到的顺序找到最后一个node,该node上记录的leaf_nodes就是我们需要的订阅者集合。
2、遍历到的level中包含*通配符。此时:
如果已经是最后一趟遍历(e.g. Sublist 记录了订阅了A.*的subs,然后有一个A.B的匹配请求),循环就会跳出并且result被赋值为最后一次遍历时pwc指向的node的leaf_nodes。
如果遍历尚未结束(e.g. Sublist 记录了订阅了A.*.B 的subs,然后有一个A.C.B的匹配请求),我们会从*后面的tokens和level开始递归进行同样的匹配操作,这个过程会一直进行到得到最终的订阅者才会回溯。
回溯到原来的代码段,.B以及后面的部分的subs已经找到并放到results中了。接下来继续从C开始查询sublist,所以有类似A.C.B这样的订阅者,也会同样被添加到results中。如果从这层level里查询不到C主题关联的node,遍历也会被结束。
3、遍历到的level中包含>通配符。这种最好处理,由于已经是最后一个token了,所以只要拿到这个fwc关联的node的leaf_nodes就可以了
Sublist的结构其实比较简单,但是我们介绍的还是比较罗嗦,大家最好自己举几个例子走下上面的流程,相信Sublist就会清晰很多。
三、/lib/nats/server/server.rb
这是server具体实现类,server的主要功能都在这个部分有描述。
def subscribe(sub) @sublist.insert(sub.subject, sub) end def unsubscribe(sub) @sublist.remove(sub.subject, sub) end
def deliver_to_subscriber(sub, subject, reply, msg)
这里需要使用到../server/connection.rb中的方法:
conn.queue_data("MSG #{subject} #{sub.sid} #{reply}#{msg.bytesize}#{CR_LF}#{msg}#{CR_LF}")
connection.rb在后面还有介绍,这里只看相关的方法。
def flush_data return if @writev.nil? || closing? send_data(@writev.join) @writev, @writev_size = nil, 0 end def queue_data(data) EM.next_tick { flush_data } if @writev.nil? (@writev ||= []) << data @writev_size += data.bytesize flush_data if @writev_size > MAX_WRITEV_SIZE end
3、将某个消息路由给所有订阅者
对应的方法是route_to_subscribers(subject, reply, msg),这个方法的主要部分如下:
... ... @sublist.match(subject).each do |sub| # Skip anyone in the closing state next if sub.conn.closing unless sub[:qgroup] deliver_to_subscriber(sub, subject, reply, msg) else if NATSD::Server.trace_flag? trace("Matched queue subscriber", sub[:subject], sub[:qgroup], sub[:sid], sub.conn.client_info) end # Queue this for post processing qsubs ||= Hash.new qsubs[sub[:qgroup]] ||= [] qsubs[sub[:qgroup]] << sub end end return unless qsubs qsubs.each_value do |subs| # Randomly pick a subscriber from the group sub = subs[rand*subs.size] if NATSD::Server.trace_flag? trace("Selected queue subscriber", sub[:subject], sub[:qgroup], sub[:sid], sub.conn.client_info) end deliver_to_subscriber(sub, subject, reply, msg) end ... ...
我们可以看到,这个方法的核心是按照subject遍历sublist,进而调用deliver_to_subscriber来实现路由过程。
注意,这里有一个queue subscriber的判断,如果需要用到qgroup的话,nats首先生成qsubs,然后随机从group中取一个subscriber进行是路由。
关于queue subscriber的使用README里有说明,其实就是一组订阅了同一主题的subs分为一个group,然后按照上面的机制挑选一个sub进行消息转发。
http_server = Thin::Server.new(@options[:http_net], port, :signals => false)
四、/lib/nats/server/connection.rb
connection主要负责接收和分析数据,它重写了EM#Connection类中的方法,所以nats connection被作为前面NATS server的连接参数传给EM。关于EM中connectioin的作用可以参考EventMachine的有关文章。
前面已经提到的queue_data,我们这里重点介绍其receive_data方法。
Connection有两种模式:等待控制信息(AWAITING_CONTROL_LINE) 等待数据消息(AWAITING_MSG_PAYLOAD)
控制信息类型:
PUB_OP、SUB_OP、UNSUB_OP、PING、PONG、CONNECT、INFO、UNKONWN
我们这里仅介绍两个最重要的控制信息的处理过程:
PUB_OP:
控制信息格式:PUB MSG_SUB MSG_REPLY MSG_SIZE
其中MSG_SUB是topic,MSG_REPLY是消息的响应,MSG_SIZE是消息的长度
1、connection会转入到AWAITING_MSG_PAYLOAD模式
@parse_state = AWAITING_MSG_PAYLOAD2、在AWAITING_MSG_PAYLOAD模式下,connection收到客户端发来的消息,于是截取发布的消息内容
msg = @buf.slice(0, @msg_size)3、然后路由给订阅过的订阅者进行处理
Server.route_to_subscribers(@msg_sub, @msg_reply, msg)
控制信息格式:SUB MSG_SUB QGROUP SID
其中,MSG_SUB是消息的topic,QGROUP 是queue group信息,SID是订阅连接的ID
subscriber = Subscriber.new(self, sub, sid, qgroup, 0)
2、将subscriber加入由sid指定的server端的订阅者集合中
@subscriptions[sid] = sub
3、调用Server#subscribe将该subscriber添加到Sublist中,以便将来进行按主题查询
Server.subscribe(sub)
subscribe方法实际上就是调用了sublist#insert方法
五、NATS的client端
Client端是负责向NATS server发送操作命令的部分,我们仅解释下其中几个方法:
EM.run { @client = connect(*args, &blk) }
client = EM.connect(@uri.host, @uri.port, self, opts) client.on_connect(&blk) if blk return client
def subscribe(subject, opts={}, &callback) return unless subject sid = (@ssid += 1) sub = @subs[sid] = { :subject => subject, :callback => callback, :received => 0 } sub[:queue] = opts[:queue] if opts[:queue] sub[:max] = opts[:max] if opts[:max] send_command("SUB #{subject} #{opts[:queue]} #{sid}#{CR_LF}") # Setup server support for auto-unsubscribe unsubscribe(sid, opts[:max]) if opts[:max] sid end
参考send_commant方法,其他的控制信息也是通过类似的send_command方法发给server端的。
六、其他
NATS的源码分析就到这里了,但是要真正理解NATS的话,还需要多做实验,多看EventMachine的原理。其实CloudFoundry之所以选择NATS这样的轻量级消息中间件,更多的考虑应该是这种基于Ruby和EM的代码在高并发时的良好表现,以及Ruby本身简洁高效的网络编程技术。这两点在分布式系统的设计中都是很重要的因素。
不过如果是企业私有云考虑的话,稳定性和可靠性往往要占到很高的比重,所以单节点的NATS很容易让人忧心忡忡。鉴于NATS官方的cluster还在beta版,且没有明确的deadline,所以国外有人曾建议过如下几种替代方案:
– AMQP(RabbitMQ)
– Storm
– RestMS
– XMPP
中间件选型这个课题就比较复杂了,不过我们应该考虑的方面应该要包括Ruby客户端的支持,Sub/Pub消息机制,通配符的支持,基于server或broker的工作方式等几个方面吧。