- 风云的BLOG https://blog.codingnow.com/
- GitHub地址 https://github.com/cloudwu/skynet
- GitHub Wiki https://github.com/cloudwu/skynet/wiki
Skynet是什么呢?
- 轻量级游戏开发框架
- 使用框架实现Actor模型
游戏服务器开发时希望能够充分利用多核优势,将不同的业务放在独立的执行环境中处理,协同工作。这个执行环境最早期望是利用操作系统的进程,后来发现如果必定采用嵌入式语言如Lua,那么独立的操作系统进程的意义不大。
LuaState已经提供了良好的沙盒,隔离不同执行环境,而多线程模式可以使得状态共享、数据交换更加高效。但是多线程模型又存在诸多弊病,比如复杂的线程锁、线程调度问题等。这些问题都可以通过减小底层的规模精简设计,最终把危害限制在很小的范围内。
早期的Skynet v0版本中,Skynet是使用Erlang+C driver开发的,Erlang中的多进程似乎是:每个玩家都有一个代理服务。当在40v40时,使用台式机测试时是存在困难的,于是Cloud滋生了重新考虑之前的设计的念头。由于之前的设计方案是:每个代理的状态都需要通知给其他代理,这造成了大量内部消息重复,这明显是可以优化的。
掂量下,如果设计一个共享无锁队列,避免了无谓复制,内部消息包可以降低90%,整体性能能提升15%左右。为了验证Skynet v0的性能,编写了一个简单的Skynet v1,v1完全使用C作底层通信框架,摒弃了之前ZeroMQ提供通信的部分,提供Lua接口编写应用服务,整体代理在4000多行的C代码和1000多行的Lua代码,完成了Skynet v0一样的功能。
- Skynet提供了一个简洁、稳定、高效、高可用的分布式服务开发框架。
- Skynet是一个轻量级通用的服务器基础框架
- Skynet是基于C与Lua的开源服务端并发框架,使用单进程多线程Actor模型。
- Skynet服务器支持10K+客户端接入和处理
Skynet当前规模是8K多行的C代码和2K多行Lua代码,实现了一个多线程高并发的在线游戏后台服务框架,提供定时器、开发调度、服务扩展框架、异步消息队列、命名服务等基础能力,支持Lua脚本。
Skynet是一个轻量级网络服务器架构而非完整的游戏服务端,它是服务端的最底层框架,和游戏业务相关的服务都是基于此框架之上开发的。其功能只是管理好服务(加载和调度)和服务之间的调用(请求和响应)。
什么是Actor模型
- Actor模型主要用于处理并行计算
- Actor模型中一个actor是一个最基本的计算单元
- Actor模型是基于消息计算的
- Actor模型中actor之间是相互隔离的
为什么要使用Skynet呢?
- 逻辑高并发无需为多线程编程烦恼
- 业务功能使用Lua语言开发
- 逻辑同步而不阻塞
例如:C++服务端项目开发中会分为两种方式,一种是逻辑多线程,程序员需要维护锁。另一种是逻辑单线程,程序员无需考虑锁。在逻辑多线程中,由于程序员对锁的使用场景模糊造成逻辑开发困难,导致逻辑多线程并没有体现出它的优势。另外,对锁的粒度掌控不好。逻辑单线程开发虽然不用考虑锁,但限于单线程的处理能力,一般承载不高。Skynet框架采用的是逻辑多线程的方式,但并不需要考虑锁的问题。
Skynet框架为什么会采用Lua语言来开发呢?
Lua语言是一种脚本语言,数据结构简单。主要数据结构为table表,table可以是hash表,hash表中的键值对中键值可以是table表也可以是function。另外,table表提供了metatable元表,通过内置的属性来访问或设置table。
Lua自身提供了GC,一般的垃圾回收器都需要自己创建自己释放,在Lua中垃圾回收期会将没有引用的变量进行回收。无需为野指针头痛了。
Lua相当于C与C++的中间交集,因此Lua可以很好地和C和C++沟通 。由于Lua本身是使用C语言开发的,Lua和C或C++沟通主要是通过标准栈来实现(栈通信)。
Lua自身提供了GC,而C需要手动收回垃圾。当Lua和C通信时,是如何做到协同的呢?Lua提供了两种数据结构,分别是轻量用户数据(lightuserdata)和用户数据(userdata)。例如在C中使用userdata创建的变量,会交由Lua来管理生命周期。而使用lightuserdata创建的数据,则会由C来管理其生命周期。
Lua的function跟C或C++不同,Lua中的function实际上是由C中的function+upvalue共同组成的闭包。
Skynet框架为什么可以逻辑同步而不阻塞呢?
例如在C++中从Redis中读取一个数据时发送给客户端,首先服务器需要和Redis之间建立一条连接,然后在向Redis发送获取的消息。因此在C++单线程中很容易会发生阻塞逻辑线程,其他请求是没有办法进行处理的。而在Skynet中,所有的消息过来时,Lua会使用Coroutine来处理消息。当发送消息时会挂起当前Coroutine,当Redis返回数据时会唤醒当前Coroutine,将值返回给客户端。因此不会发生阻塞。在Skynet中实际上是采用同步的写法,最终运行是异步的实现。
Skynet特点的什么
- 少量C代码和大量Lua代码组成
- 基于Actor模型,天然多线程。
- 天然集成网络、数据库访问功能
- 使用Lua的协程处理消息永不堵塞
- 自带集群功能
- 官方只支持Linux
Skynet的优点是什么
- 高低级语言配合
Skynet是融合了低级语言C消息框架和高级动态语言Lua的混合体,这种结构称为hybird framework
混合框架,使用运行高效的C来编写服务节点,使用Lua开发高效且安全隔离的上层业务。
- 组件化能力
Skynet内核(C部分)自身支持加载模块*.so
,可使用C语言编写性能有要求的服务节点,通过消息与其他节点配合。而Lua又是对C语言极为友好的动态语言,所以可找到很多Lua的C扩展。
Skynet核心是什么
- C实现的消息循环和组件加载机制
- Lua实现的以消息为中心的进入退出协程的包装层
Skynet核心解决什么问题呢?
作为核心功能,Skynet仅解决一个问题:把符合规范的C模块从动态库(.so
文件)中启动起来,绑定一个永不重复(即使模块退出)的数字ID作为其处理器handle
。这里我们称模块为服务service
,服务之间可以自由发送消息,每个模块可以向Skynet框架注册一个回调函数callback
,用来接收发给它的消息。每个服务都是一个个消息包驱动,当没有包到来时,它们就会处于挂起状态,对CPU资源零消耗。如果需要自主逻辑则可利用Skynet提供的timeout
消息定期触发。
Skynet提供了名字服务,可以给特定的服务起一个易读的名字,而不是使用ID来指代。因为ID和运行时状态相关,无法保证每次启动服务都有一致的ID,当名字却可以。
Skynet核心不解决什么问题?
Skynet的消息传递都是单向的,以数据包为单位传递。并没有定义类似于TCP连接的概念,也没有约定RPC调用的协议。不规定数据包的编码方式,也没有提供一致的复杂数据结构的列集API。
Skynet原则上主张所有服务器都在同一个操作系统进程上协作完成,所以在核心层内部考虑跨机器通讯机制。Skynet不为单独服务的崩溃、重启提供支持,和普通的单线程程序一样,你要为你代码中的bug
和意外负责。和操作系统不同的是:操作系统会认为用户进程都是不可靠的,它不会让一个用户进程的错误影响到另一个进程。但Skynet内所有的服务都有统一的目的,为游戏的最终客户服务。因此某个环节出了错误都可能是致命的,因此没有必要被问题隔离开。
简单来说,Skynet只负责把一个数据包从一个服务内发送出去,让同一进程内的另一个服务接收然后调用对应的callback
函数处理。Skynet保证模块的初始化过程时,每个独立的callback
都是相互线程安全的。编写服务的人不需要特别的为多线程环境考虑任何问题,专心处理发送给它的一个个数据包即可,其实这就是Erlang的Actor模型。
为了提供高效的服务间通讯,Skynet并不关心数据包是怎样被打包的,它甚至不要求这个数据包内的数据是连续的,虽然这样做很危险,在跨机通讯中除非你保证你所有的数据包绝对不被传递到当前所在进程中。因为它仅仅是把数据包的指针以及声明的数据包长度传递出去。由于服务都是在同一个进程内,因此接收方取得这个指针后就可以直接处理其引用的数据了。这个机制在必要时,可以保证绝对的零拷贝,几乎等于在同一线程内做一次函数调用的开销,当然这只是Skynet提供性能上可能性。推荐一种更为可靠但性能略低的解决方案:约定每个服务发送出来的包都复制到用malloc
分配出来的连续内存。接收方在处理完这个数据块,也就是在处理的callback
函数调用完毕时,会默认调用free
函数释放掉所占用的内存空间。简单来说就是:发送方申请内存而接收方释放内存。