首先非常感谢这次大会的组织者们( @jasl @lgn21st @Rei @martin91 还有其他我不认识的小伙伴) 可以想象举办一个 500 人规模的技术大会,而且还要保持高质量是何其的不容易,谢谢你们。
这里我先分享一下几个我印象比较深比较感兴趣的的讲师的演讲,(其他讲师的分享也很精彩,只不过有些并未涉猎)
nginx with ruby - @tylerdiaz
tylerdiaz 分享的主题的 mruby,据我了解是被广泛应用于日本的嵌入式开发领域。国内在这一块的应用没怎么听过,这也是 Ruby 社区近几年推的几个发展方向之一吧,国内的 Ruby 社区的路子相对来说没有那么宽。tylerdiaz 作为开场的英文演讲舞台风格非常好,尤其考虑到了我们英语可能听不懂听不清怕,刻意的把自己的语速降的很慢 (英文演讲的语速都比较快)。
异步编程奥德赛 - @dsh0416
之前队长有过多少次交流,在上海的分享会上也聊过关于异步编程的话题,midori 在我研究实时通讯的时候研究过它的源代码,本来是在我的分享上也准备了 midori 的介绍,不过后来看到队长的分享排在了我的前面,所以也就无所谓讲了,另外队长在台下和我说,这么多人的分享还是非常紧张的。不过他上台之后在聚光灯下,幽默风趣的演讲风格一点也看不出来紧张哈哈。
为 Ruby 设计一款 AOT - @psvr
这个工具做的很实用,我之前的工作中就是有一个将 Rails 项目打包发布的需求,当时考虑的还是使用 JRuby 打成一个 Jar 包进行处理,现在有这种解决方案,可以解决很多的现实问题。
金数据是如何鉴黄的 - @warmwind
金数据使用 SVM 做鉴黄的分享非常棒,近些年的技术发展的方向,Ruby 社区现在也开始关注机和实践机器学习了,薄荷其实也在广告过滤上面应用了机器学习不过使用的是贝叶斯方法。
机器学习入门 - @ihower
ihower 的分享作为机器学习的 101 非常不错,我之前对深度学习是没有什么了解的,其实在之前吃饭交流的时候,ihower 就已经给我们讲了一下关于深度神经网络的相关知识,可以说收获颇丰。
报名讲师的契机是在大会征集题目开始后,@vincent就和我说,我们要不要把最近这大半年在 Ruby Web 实时通讯上面的研究和实践给大家分享一下,(其实今年 3 月份的时候,Ruby 上海活动的时候分享了一下)这样我们才真正的把之前涉及到的关于 web 实时通讯的演讲的方案,重新整理,有些还不是很透彻的地方,有重新温习。最后整理出一份比较完整的介绍 RubyWeb 实时通讯方案的分享。
我自己的分享感觉语速和节奏没有控制好,有一有要讲的东西没表达出来,虽然我经常在公司内部做分享,但是十几二十人和 500 人还是比不了的。还有当@bhuztez 说出成功人士才跑 benchmark 之后压力山大,我的分享里面有六个 benchmark🤣。
下面内容是摘取自我在薄荷内部分享的关于实时通讯的四篇文章:
hijacking api 是在 Rack 1.5 是被加入的功能,因为 Rack API 本身无法支持想 http streaming websocket 这两长连接的请求,但是这个 API 是在 Rack 1.x 的临时方案,基本上不会再 Rack 2.0 中出现。
native websocket 这里的 native 是指定由 application server 提供内置的 websocket 功能,通过对外暴露 on_message on_open 这些回调接接口来进行请求处理,而不是像 hijacking API 通过直接暴露 socket io 对象。
HijackingAPI 和『native』websocket 的主要区别就是两种方式使用 reactor 和事件循环的数量。Hijacking API 是从 Server 中获取 socket,这就意味着应用程序需要自己监听 socket 的事件,因为这时候 server 已经忽略了这个 socket. 也就是说你需要自己在应用程序中管理你的 socket,再启动一个 reactor,一个事件循环器和线程池。使用 hijacking API 同时也需要应用程序自己去管理 socket 的 output buffer ( IO#write 和 read 在 network buffer 已满的时候,只能做部分写入或读取,application server 为处理 HTTP 请求的时候就已经内置该功能了,所以你又要为 websocket 在来实现一套)。
同样 websocket 协议的处理也是需要应用程序自己处理的(当然如果 server 和应用程序都没有使用 C 扩展或其他来加速处理协议的话,也无所谓)
总结下来没有使用 hijacking API 的“native”websocket 可以用少的代码和资源获得更好的性能,并且所有的事件和网络处理都可以集中一个模块还中,而 hijacking API 的实现需要在 server 和应用程序中各自实现一套。
注:本文基于 em-midori v0.4.1 版本所写
Midori从 web 框架的角度去看,它是一种类 Sinatra 式的 Web 开发框架,不同点就是它是基于事件循环模式的异步 web 框架,这样它真正要对标的就是 async_sinatra 这里的异步框架。与 sinatra 一样,midori 不是一个全栈式的 MVC 框架,当前版本强调的是通过 DSL 来到达简洁易用,快速构建 API 系统的 web 框架。
从 em-midori 开始 midori 底层使用的 I/O 库是 EventMachine,并且是用 EventMachine 事件循环库。后期的 midori 该为使用 nio4r 来作为底层支持,使用它的原因是 nio4r 的开发支持,稳定性和特性要比 EM 好。midori 的路由解析器使用了 sinatra 出品的 Mustermann,在这方面我觉得是做的比较好的,使用业界成熟的经过考验的组件,有助于提高框架的稳定性和实用性,并且在路由匹配功能上使用深度优先搜索算法,加快路由速度。
大体上来说 midori 的架构是建立在 fiber 和 nio4r 之上,也是框架本身宣传的特点异步框架,那么底层就要使用支持异步的框架。Midori 是使用自制生态的 Muraski 组件来使用 nio4r 的,而 nio4r 底层又是使用 libev 网络库实现 I/O 能力的。
Midori 框架启动和接收请求的执行流程:
Midori 的启动器Midori::Runner的内部使用了 Ruby 标准库的 TCPServer 来启动 web 服务,在将端口 i/o 对象通过 EventLoop 注册到 nio4r 的监听器上,当有 socket 数据通过 I/O 端口发送过来后,就会执行回调函数,调用Midori::Connection来处理请求。
class Midori::Runner
...
def start
return false if running? || EventLoop.running?
@logger.info "Midori #{Midori::VERSION} is now running on #{bind}:#{port}".blue
@server = TCPServer.new(@bind, @port)
EventLoop.register(@server, :r) do |monitor|
socket = monitor.io.accept_nonblock
connection = Midori::Connection.new(socket)
connection.server_initialize(@api, @logger)
end
async_fiber(Fiber.new do
@before.call
end)
EventLoop.start
nil
end
...
end
整个事件循环和监听 I/O 的 nio4r 都被封装在了 Muraski 组件中。其核心代码就是支持 I/O 事件回调函数和启动事件循环。
module EventLoop
class << self
def register(io, interest=(:rw), &callback)
config if @selector.nil?
if @queue[io.to_i].nil?
@queue[io.to_i] = Array.new
config if @selector.nil?
@selector.register(io, interest)
@ios[io] = { callback: callback }
else
@queue[io.to_i] << [io, interest, callback]
end
nil
end
def start
return if running?
@stop = false
until @stop
config if @selector.nil?
@selector.select(0.2) do |monitor| # Timeout for 0.2 secs
@ios[monitor.io][:callback].call(monitor)
end
timer_once
end
@stop = nil
end
end
end
当 EventLoop 注册的回调函数被调用后,从框架的角度就是一个连接被建立一个请求被接受,那么接下来就需用 Connection 去处理连接,在 Connection 的内部非常有意思的是,其主要职责就是再注册一个事件循环器来持续监听建立起来的 socket 连接,剩下的处理请求的数据是交由 mixin 的 Server 模块处理。
class Midori::Connection
include Midori::Server
def listen(socket)
EventLoop.register(socket, :rw) do |monitor|
@monitor = monitor
if monitor.readable?
receive_data(monitor)
end
if monitor.writable?
if !@data == ''
monitor.io.write_nonblock(@data)
@data = ''
elsif @close_flag
close_connection
end
end
end
end
end
这样分解的作用就是,Midori 是作用一个同事支持短连接和 websocket 长连接的 web 框架,底层的应用服务器部分也是要自己处理的(因为没有使用 Rack)。那么对一个长连接的数据不是在 socket 建立和就发送过来的,而是会持续不断的发送和接受直到连接关闭,所以 Midori 在主事件循环监听 I/O 端口外,还内嵌了事件循环监听特定 socket 连接。
Midori 中 Fiber 使用的最大用途其实是在 midori-contrib 的数据库驱动中,以 mysql2 为例 midori 利用元编程打开了 mysql2 的 I/O 查询部分,使用 Fiber 在 I/O 阻塞的时候挂起,等 I/O 返回时候恢复。
socket = MYSQL_SOCKETS[conn.socket]
await(Promise.new do |resolve|
count = 0
EventLoop.register(socket, :rw) do
if (count == 0)
# Writable
count += 1
conn.query(sql,
database_timezone: timezone,
application_timezone: Sequel.application_timezone,
stream: stream,
async: true)
else
# Readable
begin
EventLoop.deregister(socket)
resolve.call(conn.async_result)
rescue ::Mysql2::Error => e
resolve.call(PromiseException.new(e))
next
end
end
end
end)
http 请求和 websocket 也都是通过 Fiber 作为执行单元来执行。但是 Fiber 在这里并没有起到什么实质性的作用 fiber 被创建后就直接被执行了,期间也没有挂起调度。它可能是要在 Midori 未来的版本中会有特殊的用途,目前的版本是完全可以使用 proc 对象替代的。
module Midori::Server
def receive_data(monitor)
lambda do
async_fiber(Fiber.new do
begin
_sock_domain, remote_port, _remote_hostname, remote_ip = monitor.io.peeraddr
port, ip = remote_port, remote_ip
@request.ip = ip
@request.port = port
data = monitor.io.read_nonblock(16_384)
if @request.parsed? && @request.body_parsed?
websocket_request(StringIO.new(data))
else
@request.parse(data)
receive_new_request if @request.parsed && @request.body_parsed?
end
rescue => e
close_connection
@logger.warn "#{@request.ip} - - #{e.class} #{e.backtrace.join("\n")}".yellow
end
end)
end.call
end
end
Midori 作为一个年轻的 Ruby Web 框架站在巨人的肩膀上,做出了不错的性能,而且是国人所做,所以应当支持和鼓励,但是客观的说在目前国内的 Ruby 生产实践中,依然是以 Rails 为核心的全栈式 web 框架为主流,类似 sinatra 或其他的异步框架,市场份额非常少而且 Midori 现在发布稳定的正式版。另外从性能角度看 Midor 上随有优势,但按照 roadmap v2.0 版本,要支持 MVC 和 http2,在性能上难免有所下降。
造轮子容易,用轮子难,所以希望 Midori 项目能够改进不足,补齐文档,多加宣传。主厨推荐 (Omakase) 吃了这么多年,是不是该换换口味了。
I/O 多路复用
当应用程序(用户线程或进程)发送监听端口的系统调用后,内核会在自身不断的轮询给端口看看是否有网络数据到了(socket 套接字)如果有数据到了,就会通知刚才的用户线程,在这个阶段 I/O 还是阻塞的,但是不同点就是它在发送监听系统调用的时候是可以监听多个套接字的,这也就是在同一个路径是复用了 I/O,剩下其他的部分和 I/O 阻塞都是差不多的,别小看这点改进,在多路复用情况下,select 系统调用在 64bit 系统上可以 切换处理 2048 个 socket,这样的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
那么我们常用的 libevent 框架,它的底层就是使用了这个这些 I/O 多路复用的系统调用,包括 select epoll poll 和 kqueue,(这里说一下 libevent 是跨平台的 事件驱动系统函数库,主要作用就是抹平了,不同操作系统对于 I/O 多路复用系统调用函数的不同 API,(就是统一 API) )
epoll 和 kqueue 是这个模型上的 select 和 poll 的升级版,目前最流行使用(包括 nginx nodejs) 主要区别就是 select 在它监听的所有 socket(fd) 有任意一个有数据返回了,内核就会将所有的 fd 都复制会用户线程中,然后再由 select 去遍历看看到底是哪一个 fd 有数据了,最后再处理请求。而 epoll 是使用了一个 callback 的机制,在它监听的每一个 fd 上都有一个 callback,当内核收到数据后,就会使用有数据的 fd 的 callback 通知 epoll,具体是哪一个 fd 有了数据,然后在处理特定的请求,这样的好处就是可以有效减少 fd 集合在内核和用户空间之间的复制操作。
reactor
是一种服务器端的事件驱动模式,服务处理程序使用解多路分配策略,底层是一种 I/O 的多路复用,I/O 同步非阻塞它的是有以下几个部分组成:
EventMachine 实际上在事件驱动模型上就是根据 reactor 模式实现的,上面介绍了它的工作原理,我们再看看它的优点:
iodine 是支持 rack 的,事件驱动并发框架,作为 plezi 的底层库,基本上 I/O,多线程,pub/sub 都是使用 iodine 来实现的
Websockets
iodine 处理 websocket 的方式与其他 Rack based 的 server 有所不同,它没有使用 rack.env 的 hijack API 来截获 websocket upgrade 请求,而是使用 C 扩展在 Ruby 处理请求之前,就在 socket 上判断是否为 websocket upgrade 请求然后,再执行 websocket parser(在 Ruby 之前运行所以不受 GIL 的影响,parser 是 CPU 密集操作如果有 GIL 的话多个 I/O 请求需要排队执行 parser)最后设置 rack.env[websocket.upgrade?] 变量后面的 rack app 自行判断如何处理 websocket upgrade 请求。
PubSub
iodine 底层使用了 facil.io 库提供原生的 pub/sub 支持,同时也可以通过 Redis 进行扩展,iodine 的 RedisEngine 支持 redis 单机和集群两种模式。并且Iodine::PubSub::RedisEngine
的 Redis 客户端是异步非阻塞的客户端。
require 'uri'
# 使用Redis 作为pub/sub queue
uri = URI(ENV["REDIS_URL"])
Iodine.default_pubsub = Iodine::PubSub::RedisEngine.new(uri.host, uri.port, 0, uri.password)
# 非阻塞Redis Client 发送命令使用block 回调
Iodine.default_pubsub.send("CLIENT LIST") { |reply| puts reply }
Iodine 使用里多线程(pthread) 处理一些可以并行执行的任务(例如协议解析)因有共享资源处理,所以 iodine 使用了 spin-locks,在处理锁的问题上,得益于应用使用同一个 reactor, 服务器可以检查资源锁的状态,然后重新调整安排其他任务的执行,最大化利用资源。多线程并行执行因为在 C 层面使用 pthread 处理,所以在事件转移到上层的应用程序之前,内部的处理都是不受 GIL 的影响的。
参考资料
支撑 ActionCable 的底层库
I/O 模型
Unix 网络编程
Websocket / Upgrade support proposal