深入理解skynet —— 总体框架 Domi●Cat

实际使用skynet 已经接近一年了,一直想对 skynet 进行深入理解。于是,便打算做一个“深入理解skynet”的系列文章。大概会从以下几个方面对 skynet 进行全面深入的剖析:

  1. skynet的整体架构
  2. skynet的服务
  3. skynet定时器
  4. skynet网络处理
  5. skynet的集群

这是整个系列的第一篇,主要从一个全局的高度来简单概述 skynet 框架,了解 skynet 的核心模块和组件以及它们提供的功能。

从一个例子出发

在这之前,先不去谈 skynet 是一个怎样的框架,先想象出一个现实生活的银行场景:

这是我个人喜欢且推荐的一种学习方法,即无限向现实世界靠拢。

我们日常生活或多或少都要去银行办理业务,银行会有多个办事窗口,比如个人业务办理(个人存取款、个人贷款等)、对公业务办理、出纳专柜、贵宾室等等,这些业务就是银行提供的服务,这些服务解决用户的不同需求,而这些服务都是在窗口下由银行柜员操作完成。除了窗口服务,其实还有其他助手服务在一直提供着,例如银行大堂经理接待指引客户,定时叫号等。

首先,要了解 skynet 内的一个非常重要的概念 ———『服务』。所谓的服务,本质就是一个接收消息然后处理消息的过程,它跟我们日常生活中所说的服务(例如:外卖服务、快递服务等等)是一样的,它不是一个真实可见的物体,而是一个抽象的逻辑过程,它需要一个执行者来执行并完成它。这其实就是 Actor 模型,Actor 模型是一个概念模型,用于处理并发计算。它定义了一系列系统组件应该如何动作和交互的通用规则,即 Actor 的参与者 = {消息队列, 处理逻辑(服务)}

了解了“服务”后,现在,我们把上面的银行场景套用在 skynet 框架上,可以做出下面的转换关系:

  • 服务 = 银行提供的业务(个人业务、对公业务…)
  • 工作线程 = 办事窗口(负责处理客户业务需求并完成需求)
  • CPU 核心 = 银行柜员(需要在办事窗口上工作)
  • 网络线程 = 大堂接待(也相当于一个办事窗口)
  • 定时器线程 = 银行的排队叫号系统

有了这一层转换后,我们再转入到 skynet 的代码框架中,就清晰很多了。比如,对于工作线程的抢占执行,就类似银行柜员工作人员不足,导致有一些办事窗口没人工作,从而导致银行等待的人增多;在办理某一个业务时,可能会涉及到其他业务,就需要多个窗口沟通合作,这就类似 skynet 的服务间消息传递。

目录结构

从 github 拉取 skynet 仓库,按照文档编译代码后,会看到下面的代码目录结构:

├── 3rd
├── HISTORY.md
├── LICENSE
├── Makefile
├── README.md
├── cservice
├── examples
├── luaclib
├── lualib
├── lualib-src
├── platform.mk
├── service
├── service-src
├── skynet
├── skynet-src
└── test

抛开根目录的一些说明文件和版权文件等等,核心包括以下几个部分:

  • skynet-src : skynet C层核心代码目录,是 skynet 底层的实现,编译后得到可执行文件 skynet
  • service-src: skynet 服务模块代码目录,编译后的动态库会放入到 cservice 文件夹内
  • lualib-src : skynet c编写的 lua 库,编译后的动态库会放入到 luaclib 文件夹内,例如 skynet.so 就包含了 skynet 框架底层导出给 lua 层的 api
  • lualib : lua 封装的常用库,包括 http、md5、数据库驱动等
  • service : lua 实现的 skynet 的服务(底层都是 snlua 服务实例)
  • 3rd : 第三方依赖库,包括:jemalloc(可选择是否使用)、lua(云风优化过的版本,也可以使用原生版本)、lpeg 和 md5

底层模块

我所认为的底层模块是指,skynet 底层框架的模块组成(代码位于 skynet-src 中),主要包括:

  • 服务模块
  • 定时器模块
  • 网络模块
  • 监控模块

在代码中,大部分的模块对象都是以大写字母来命名,例如:static struct timer * TI,所以,只要在源码中出现了全局静态变量,都需要特别关注下。 但是,上面的监控模块比较特殊,它会侵入式的嵌入到其他模块的工作线程中。

每一个模块都有对应线程来运行着,其中服务模块可以开启多个工作线程,其线程数量由配置字段 thread 控制,例如:thread = 8,一般配置成 cpu 的核心数量即可。

以上的这些核心模块,我都会在后面的文章中一一对其进行细致的剖析。

服务模块

服务模块由 消息队列服务实例 组成,在 C 层中,它们封装在一个 context 的实例中,这些服务由一个或多个 worker 线程来驱动运行(弹出消息并由服务实例执行消息)。

一个 skynet 节点(进程)可以创建多个 context 实例,它们集中由 handle_storage *H 来管理,例如:服务注册、分配服务句柄id、服务命名等。而 context 中的消息队列处理会特殊一点,消息列表默认会放入到一个全局消息队列 global_queue *Q 中。

接下来简单描述下工作线程的执行逻辑:工作线程从全局队列中 pop 出一个有消息的次级消息队列(也就是一个服务的消息队列),然后从次级消息队列中再 pop 一个消息,然后由服务回调函数调用。

定时器模块

定时器模块提供了服务定时消息的支持,实现算法为时间轮算法。它提供的功能包括两点:定时消息的添加和定时消息的派发。

这个模块其实是 skynet 底层框架中最简单的一个,提供的功能简洁清晰。需要注意底层并没有提供定时消息删除的支持,因为云风认为底层不需要关系定时器删除,如果一个定时器消息需要取消,只需要在上层忽略处理即可。详情可以参考一下云风的 blog,里面有相关的解释。当然,网上也有各种删除定时器的实现。

网络模块

网络模块可能是 skynet 中最为复杂的模块了,它不仅仅提供了对客户端网络连接的管理的支持,还能向其他主机发起连接。它支持 epoll(linux) 和 kqueue(freeBSD) 两种网络 IO 模型。还需要特别注意一点的是,因为 skynet 是一个消息驱动的模型,所以 skynet 的网络模块使用了一种叫 self pipe trick 1的技巧来唤醒异步调用,实现细节是利用了 pipe() 的“半双工”管道 + select,为了解决可在网络线程在即时处理网络事件的同时,也能即时处理来自非网络(比如内部消息队列)的其它消息。

监控模块

我个人认为监控模块不仅仅只是 thread_monitor 线程,还包括上层提供的 debug console 配套功能。监控系统能帮助我们分析节点的服务存在的问题,例如:服务死循环、服务内存溢出、cpu耗时过长等问题。

最后,贴上一张 skynet 总体框架的图,把其中涉及到的核心模块通过的图的方式展现出来,方便理解和记忆。 skynet-framework

周边组件

周边组件是指服务于核心框架的其他模块,它们对核心框架的功能进行封装、扩展。大致可以分为以下几部分:

  • 服务模板动态库(用于创建服务实例)
  • 一些 lua 的 C 库,例如:bson
  • 一些 c 功能模块以及其对应的 lua 的 C API,例如,sproto 库
  • 一些 snlua 的核心 lua 服务,例如:bootstarp 服务、launcher 服务

这其实有点类似 Linux 的架构,分为了:内核、系统调用和shell。skynet 的底层框架就是内核,底层向服务暴露一些系统调用api,然后服务使用系统调用实现更复杂的功能。


  1. 参考知乎问题pipe异步使用场景?。