前言
众所周知,cloud foundry是vmware主导的主要用ruby开发的第一款开源paas云平台,对于想要学习ruby的朋友,它的源码就是我们最好的学习工具。 如果说你在进行cloud foundry的相关开发工作,为了了解其内部的工作机制,源码是不得不透彻地分析一下的。 源码的github地址:https://github.com/cloudfoundry 为了循循渐进,由简入繁地深入cloud foundry,我们从nats的源码开始入手。
nats源码特点
在cloud foundry的各个组件中nats的源代码是最清晰,也是最具有可读性的,很适合对cloud foundry一知半解的朋友入手,它的主线基本上是一个 eventmachine和一个基于thin的http monitor。在读源码之前你需要有必要的ruby功底,还需要一些gem包(如eventmachine)的基本了解。下面我们就从简单介绍eventmachin入手。
eventmachine简介
首先,网上有一篇不错的博文http://blog.csdn.net/resouer/article/details/7975550,可以了解到一些eventmachine的基础知识,在这里我对 nats中常用的再强调一下。
- EM::Connection 是EM下控制连接的类,在nats中以模块的形式出现,如果想要定义自己的连接名称,只需要继承这个类即可。Connection中有两个 最基本的方法:recive_data(data)和send_data(data),从方法定义的名称就可以看到他们的作用,recive_data用于接收客户端发来的数据,send_data 用于向客户端发送数据。在nats中阅读Connection模块的时候,只要抓住这两个方法,其他的都是辅助控制连接的。
- EM.start_server(ip,port,connection) 是EM服务器启动代码,它在主机(ip)的port端口上监听对EM的访问,连接的属性动作由connetcion控制。
- EM::Timer与EM::PeriodicTimer 是两个定时器,前者是一次性的(触发一次事件后消失),后者是周期性的。两者的参数是定时器的时间间隔,可 以为它们定义句柄,以便在程序运行时控制他们,比如调用cancel或stop函数停止定时器。
- EM.next_tick(do_work) 是EM在网络编程中协调并发的一种策略,由于ruby在执行多线程的时候并不是系统级的并发,而是通过ruby解释器调度的一 种伪并发,所以ruby在多任务一起处理时有它语言的先天不足。为了弥补这个不足,EM采用了一个让人拍案叫绝的解决办法,即:EM运用一个主进程,在它 运行起来的时候并不去阻塞它,如果碰到需要诸如IO等需要等待的工作时,就把它交给下一个循环(next_tick),在任务需要执行的条件已经成熟的时候 会轮到一个循环来执行这个任务。
- EM.run do ... end 是基于EM编程的程序入口,服务器端在这里启动服务器监听,客户端可以在这里启动指向服务器的连接。
以上是对nats中常见的关于EM编程的归纳,如果想深入了解EM的工作方式,还有待更深入的研究。
nats源码文件组织方式
用ruby编写的软件程序都有一个不约而同地习惯,就是bin文件夹下都是一些很简单的命令,在nats中bin文件夹下文件的内容基本只包含一些require, 真正的代码部分都在lib里面,平行的其它文件夹是一些测试和教你如何使用的代码。我们重点看lib/nats下的各个文件。
- server.rb nats服务器启动的入口文件,在里面进行了服务器的初始化建立,server的EM启动,和一个基于thin的monitor的启动。
- client.rb nats的客户端代码,这cloud foundry的各个组件中都会有这个gem包,其它的各个组件都必须通过它和nats_server建立连接。
- server 服务器的主要代码部分
- options.rb nats服务器的参数控制文件
- connection.rb nats中eventmachine的连接控制模块
- server.rb 控制各个连接的server主程序
- connz.rb 通过monitor的conn监控时,返回的连接信息
- sublist.rb 基于发布与订阅模式的消息处理核心数据结构
- const.rb nats中需要的一些全局常量
- varz.rb 通过monitor的varz监控时,返回的服务器信息
- util.rb 一些关于log,trace,debug等的基本函数封装
- ext 导入一些必须的gem包,解析一些数据格式信息
nats的参数控制
首先来看nats的参数,通过对参数的了解,可以窥得nats的全貌,nats的参数是靠options.rb这个文件控制的,它定义了NATSD::Server下的一个内建类, 参数的解析分为三层:
- 一些默认的参数,他们通过const.rb中的常量定义好,如:DEFAULT_PORT等
- 用户通过配置文件传入的参数,它比默认的参数的优先级高,能够覆写掉默认的参数
- 用户通过命令行传入的参数,它们具有最高的优先级,能够overwrite前两种参数
nats中有一些我们比较陌生的参数,列几个解释一下
- --daemonize 守护进程标志,设定它可以在系统启动nats的守护进程,一般建议启动它
- --http_port 为monitor指定一个端口,如何需要通过http去对nats进行数据采集,需要设定它
- --ssl 启动https/ssl,一个针对http数据传输的加密协议
- --no_epoll linux下,一种针对EM中更好的IO资源调度策略
- --user --paas nats的用户认证策略,在搭建私有云的时候,在安全方面有较高要求时可以设定用户认证
- 一些负载能力方面的参数如max_control_line,max_payload等,分别对应于一条控制命令和一条消息的最大长度
- 一些超时方面的参数如auth_timeout,ssl_timeout,分别对应于用户认证超时和ssl认证超时
nats消息发布与订阅的核心数据结构
这一块是整个nats运转机制的核心类,是它控制着消息的控制与转发,发布与订阅模式的大致概括为,一条带有主题的消息发布到nats服务器上后, 会被转发给所有关心这个主题的订阅者(客户端)。通过阅读README文件我们得知nats的消息主题有两个通配符:*,>。主题通过<.>被分成各级token, 比如:
- 订阅主题'foo.*.baz'将匹配到'foo.bar.baz','foo.22.baz'等主题
- 订阅主题'foo.>'将匹配到'foo.bar', 'foo.bar.baz', 'foo.foo.bar.bax.22'等主题,但是>必须是相关订阅主题的最后token,而主题'foo.*'将不 会匹配到'foo.bar.baz'上面。
下面我们可以进入sublist.rb的阅读了,进入这个类就定义了两个符号:pwc,:fwc风别指向通配符:*与>。还定义了两种结构类:
- SublistNode = Struct.new(:leaf_nodes, :next_level)
- SublistLevel = Struct.new(:nodes, :pwc, :fwc)
如果对树这种数据结构比较了解,对这两种结构也应该不在话下,SublistNode中:leaf_nodes指向一个数组,数组里面是一个一个的订阅者(后面我 们将会讲到订阅者Subscriber类,里面是EM连接信息),:next_level指向下一级的SublistLevel,订阅主题有多少级的token,就有多少级的SublistLevel 。level里面的:nodes指向一个hash表,hash的key就是这一级的token,hash的value就是这一级的SublistNode,如果token是,这个SublistNode将会 由:pwc指向,如果token是>,将会由:fwc指向,所以只有token非也非>时,存储订阅者节点的node才会出现在:nodes中hash指向的value中。
如果有众多的主题和订阅者,那么上述结构将会形成一颗庞大的树。sublist类中提供的方法:
1. insert(subject, subscriber) 将一个订阅者按subject插入:leaf_nodes指向的数组中
2. remove(subject, subscriber) 将一个订阅者从它所在的:SublistNode中删除,如果node中订阅者为空,这个node将被清除
3. match(subject) 一条消息中的主题会匹配到若干订阅它的订阅者,这个函数将演绎匹配的动作。
这一部分算法性比较强,remove与match中都有递归,由于不好表述,建议拿出一张纸一步一步画出整个过程,你会对整个设计拍案叫绝。其中有对 主题匹配结果的cache缓存,当cache溢出或对订阅树操作时,缓存将被清空。
server主进程
- 阅读到这里,感叹到写代码过程中清晰度的重要性,cloud foundry中各个类和方法的定义都可以让你一目了然,这不得不感谢开发者们的精心设计 和对ruby语言的良好把握。server里面的变量和方法定义,我们只需要看名字就知道它的功能了。
- 一开始定义了一个结构Subscriber就是我们在上文中提到的订阅者定义,它保存了这个订阅者的EM连接(:conn),主题(:subject),订阅id(:sid ,一个连接对应的客户端可能订阅多个主题),组(:qgroup,标识订阅者的组别,消息会被随机发送到组里面的任一个订阅者)等等。
- 上面提到一颗庞大的订阅树,它到底存在哪?就是server类中的@sublist。它实际上是sublist类的一个实例对象,这个server控制的所有订阅和方 法如subscribe(订阅),unsubscribe(取消订阅),deliver_to_subscriber(将消息发给一个订阅者),route_to_subscribers(将消息路由给匹配 的所有订阅者)都是对这个实例对象的操作。
- 在server初始化建立的时候,它会根据daemonize_flag的参数设置,选择启动一个daemon进程,然后入口EM处的monitor的启动方法 NATSD::Server.start_http_server也在这里给出了定义,如果想要简单了解thin的使用,这段代码会是一个不错的选择。
- 做一个测试,我们将nats_server在一台主机上启动起来,在浏览器中输入http://ip_addresss:http_port/(healrhz|varz|connz),就会依次得到 ok,nats_server,nats_connections等返回信息,后两个respones是json格式的数据。
Connection模块
上面提到sublist类是消息发布订阅模式的核心数据结构算法,那么Connection模块就是整个nats的EM核心。 按照前面说的方法,先找recive_data与send_data方法,然后再去看其它的实现方法,这里我们来梳理一下:
- flush_data 将@writev中的数据发送出去
- queue_data(data) 一个消息缓存队列,将消息存入@writev里,@writev存满后调用flush_data,这里面就会用到EM.next_tick,解决@writev中数 据发送和写入的并行矛盾。
- post_init 连接的初始化工作,如果初始化成功,就会建立一条client连接,其中的类变量@subscriptions保存这个客户端的所有订阅者
- receive_data(data) Connection接收数据和分析数据的主要代码,它采用自动机的设计思想,分为两个状态:AWAITING_CONTROL_LINE 和 AWAITING_MSG_PAYLOAD,由变量名就可以看出,连接始终处于等待操作码(control_line),分析操作码并做相应的动作,等待消息,处理并发送消息的 循环中,操作码主要有PUB_OP(发布),SUB_OP(订阅),UNSUB_OP(取消订阅),CONNECT(连接请求,用于身份的鉴定,里面会涉及到EM的定时器, 如果定时器超时,则身份认定失败,连接断开),unbind(解除绑定,删除此客户端的订阅信息)。
后记
nats的源码有很多我们值得关注的地方,如EM的设计,sublist数据结构的算法设计等等,更多细节还需去仔细推敲源码,特别是其中发布与订阅模式 的设计与实现绝对值得我们去学习和借鉴。更多cloud foundry的组件源码分析我会陆续更新。