nginx反向代理skynet

nginx作为一个web服务器和反向代理服务器,能够使web服务器和逻辑单独分离,也可以作为负载均衡服务器。基于这些优点,可以使用nginx反向代理skynet,即skynet监听本地的一个socket端口,收到请求后处理逻辑,然后返回结果。nginx和skynet可以同一台机器上,也可以在不同的机器上。

新建一个skynet服务并启动起来,当收到请求后简单返回一条信息

启动起来后log如下,监听8008端口

nginx配置

将所有以server为uri的请求全部转向skynet的8008端口上。

重新加载nginx,用浏览器打开http://localhost:8020/server会收到一条信息显示在浏览器上。

skynet已经自带了一个simpleweb例子,可以直接使用,还有一些解析header的代码,自己可以修改体验下skynet作为web服务器使用。

skynet中的协程

这篇是转来的,http://www.cnblogs.com/iirecord/p/skynet_coroutine.html
也是这一直很想写的,作者已经把skynet处理消息过程中的协程解释得很清析了,如下文

——————

阅读云大的博客以及网上关于 skynet 的文章,总是会谈服务与消息。不怎么看得懂代码,光读这些文字真的很空洞,不明白说啥。网络的力量是伟大的,相信总能找到一些解决自己疑惑的文章。然后找到了这篇讲解 skynet 消息队列的文章(最新的 skynet 消息队列代码已经有更新,变得更简洁易读)。了解了 skynet 消息是如何派发的,就想知道消息被派发出去到一个服务后,如何调用服务的 callback 函数,从而处理此消息。碰巧博主写了这篇讲解 skynet 如何注册回调函数的文章,于是 skynet 的概念“服务与消息”便在代码中得到了定位,便可以此为入口点探究 skynet 实现。

消息派发

这里云大已经很详细的介绍了,我就仅仅在这里略提一下。skynet 把消息分为不同的类别,不同类别的消息有不同的编码方式,若编写一个服务,你需要为此服务关注的消息类型注册 dispatch 函数用来接收此类别的消息。skynet 注册类别消息的 dispatch 函数有两种方式。

调用 skynet.register_protocol 注册。函数的参数是一个 table ,以”lua”类消息为例,里面有若干字段含义如下:

指定了 table 中的 dispatch 字段,以后”lua”类消息到达时便会调用此函数。

调用 skynet.dispatch 函数注册。为此,云大给出了一个惯用写法,以”lua”类消息为例,如下:

两种方式可以根据喜好选择,毕竟一个服务可能需要处理多种类型的消息,需要注册多个 dispatch 函数。

在 skynet 中用 Lua 编写一个服务必须调用 skynet.start 启动函数启动此服务。

skynet.start 其中在一个作用是调用 c.callback 函数把 skynet 框架的消息派发与你自定义的 dispatch 函数联系起来,这个联系的纽带就是 dispatch_message(skynet.lua) 函数。当服务的消息队列有消息到达时,框架从消息队列中取出消息经过一些转换调用到 dispatch_message 函数,然后 dispatch_message 函数根据协议类型调用相应的 dispatch 函数,最终到具体某条消息的处理函数。

消息执行

skynet 是基于服务的,服务间通过消息进行通信。实现方面 skynet 为每个服务创建一个 lua_State ,不同的服务 lua_State 是不同的,因此服务是相互独立互不影响的。对于消息,”skynet 的 lua 层会为每个请求创建一个独立的 coroutine”。经过上面一节,了解到消息会到达我们自定义的 dispatch 函数,此时进入了业务相关的代码逻辑中,我们只关注业务的逻辑而不关注底层消息如何到达这儿的。于是猜测应该是在 dispatch_message 函数中 skynet 会创建 coroutine 来具体处理某个消息。然后,我们猜想消息执行流程大概应该是这样的:

一条消息到达,服务的主线程创建 coroutine 处理此消息,处理完后执行权回到主线程,继续下一条消息处理。
一条消息到达,服务的主线程创建 coroutine 处理此消息,假设此服务是 A …

skynet启动过程_bootstrap

这遍摘自skynet 的wiki

skynet 由一个或多个进程构成,每个进程被称为一个 skynet 节点。本文描述了 skynet 节点的启动流程。

skynet 节点通过运行 skynet 主程序启动,必须在启动命令行传入一个 Config 文件名作为启动参数。skynet 会读取这个 config 文件获得启动需要的参数。

第一个启动的服务是 logger ,它负责记录之后的服务中的 log 输出。logger 是一个简单的 C 服务,skynet_error 这个 C API 会把字符串发送给它。在 config 文件中,logger 配置项可以配置 log 输出的文件名,默认是 nil ,表示输出到标准输出。

bootstrap 这个配置项关系着 skynet 运行的第二个服务。通常通过这个服务把整个系统启动起来。默认的 bootstrap 配置项为 “snlua bootstrap” ,这意味着,skynet 会启动 snlua 这个服务,并将 bootstrap 作为参数传给它。snlua 是 lua 沙盒服务,bootstrap 会根据配置的 luaservice 匹配到最终的 lua 脚本。如果按默认配置,这个脚本应该是 service/bootstrap.lua 。

如无必要,你不需要更改 booststrap 配置项,让默认的 bootstrap 脚本工作。目前的 bootstrap 脚本如下:

这段脚本通常会根据 standalone 配置项判断你启动的是一个 master 节点还是 slave 节点。如果是 master 节点还会进一步的通过 harbor 是否配置为 0 来判断你是否启动的是一个单节点 skynet 网络。

单节点模式下,是不需要通过内置的 harbor 机制做节点中通讯的。但为了兼容(因为你还是有可能注册全局名字),需要启动一个叫做 cdummy 的服务,它负责拦截对外广播的全局名字变更。

如果是多节点模式,对于 master 节点,需要启动 cmaster 服务作节点调度用。此外,每个节点(包括 master …

skynet启动过程_1

skynet的启动时需带个配置文件,这个文件其实是作为lua全局变量用的,见

配置了一些基本的环境变量后,转到skynet_start方法,开始启动skynet,在skynet_start方法中初始化一些变量后,系统启动的第一个服务是logger:

skynet通过skynet_context_new函数来实例化一个服务:先是从logger.so文件把模块加载进来;

让模块自生成一个新的实例;

分配一个新的handle;

初始化一个消息队列;

调用这个模块的初始化方法

最后是把自己的消息队列加入到全局消息队列中,把有加入到全局的消息队列后,才能收到消息回调

启动完成logger服务后,系统接下来要启动的服务是bootstrap,但先要加载snlua模块,所有的lua服务都属于snlua模块的实例。

其中参数cmdline是在config配置里的
bootstrap = “snlua bootstrap”

和加载logger服务类似,先是把snlua.so文件作为模块加载进来,调用模块自身的_create函数产生一个snlua实例,在service_snlua.c文件中。

在方法中启动了新生成了一个lua VM,出就是lua沙盒环境,这一点也比较重要,因为所有的lua服务都是是一个独立的VM中运行的,这也是云风的设计初衷。

接下来就会调用了service_snlua.c中的snlua_init方法

来初始化服务,在这个方法中做了两件事情;
注册了一个回调函数,当有消息到来时,这个函数会被调用
skynet_callback(ctx, l , _launch);
向自己发送了一条消息,并附带了一个参数,这个参数就是bootstrap。当把消息队列加入到全局队列后,收到的第一条消息就是这条消息。
收到第一条消息后,调用到callback函数,也就是service_snlua.c里的_launch方法

这个方法里把自己的回调函数给注销了,使它不再接收消息,为的是在lua层重新注册它,把消息通过lua接口来接收。紧接着执行_init方法。在_init方法里设置了一些虚拟机环境变量后,就加载执行了loader.lua文件,同时要把真正要加载的文件(这个时候是bootstrap)作为参数传给它, 控制权就开始转到lua层。
loader.lua是用来加载lua文件的,在loader.lua中会判断是否需要preload,最终会加载执行bootstrap.lua文件:

在这个文件里启动了其它一些服务,这些暂不看,在这个文件里调用了服务启动的接口skynet.start。这也是所有lua服务的标准启动入口,参数是一个回调方法,服务启动完成后会调到这个方法。做一些初始化的工作。

skynet.lua文件的start方法:

通过
c.callback(dispatch_message)
重新注册了callback函数,这样就能在lua接收消息了。收到消息时,通过dispatch_message方法来分发。
c.callback调用的是一个c函数,在lua-skynet.c文件的_callback方法。

在这个方法中可以看到,重新调用了skynet_callback来注册服务的回调函数。
到此,一个lua编写的服务就启动起来了。

skynet是什么

云风的skynet,定义为一个游戏服务器框架,用c + lua基于Actor模型实现。代码极其精简,c部分的代码只有三千行左右。
     整个skynet框架要解决的核心问题是:把一个消息(数据包)从一个服务(Actor)发送给另一个服务(Actor),并接收其返回。也就是在同一进程内(作者也强调并非只限于同一进程,因为可能会有集群间的通讯)的一个服务通过类似rpc之类的调用同一进程内的另外一个服务,并接收处理结果。而skynet就是处理这些服务间发送数据包的规则和正确性。
     skynet的核心层全部是c来实现。
     当系统启动的时候,会得到一个提前分配好的节点id,我们称之为harbor id,这个id是集群用的,一个集群内可以启动很多个skynet节点,每个节点都会分配到唯一的id。
     一个节点(即一个进程)内有很多个服务,服务可以狭义地暂且理解为功能模块。
     当初始化一个服务的时候,会生成一个skynet_context来作为服务的实例;一个唯一(即使是在集群里也是唯一)的服务handle,即服务的唯一id,用来识别服务;一个消息队列message_queue;还要向框架注册一个callback,当服务收到有发送来的消息时,通过这个方法传入。
     初始化一个服务的代码如下:

在skynet_handle_register方法中生成一个服务handle,handle是一个32位的整数,在生成handle的时候,是把该节点的harbor id写到了handle的高8位里面,所以一个服务的handle,就可以知道这个服务是哪个节点的。

所以说,harbor id最高也只有256个,也就意味着skynet集群最多只能有256个节点,而一个节点里最多也只能有24位个服务,即1.6M个。因为一个handle是32位的整数,高8位用来存储harbor id,只有低的24位用来分配给本节点的handle。
     消息队列message_queue是用来存储发送给该服务的消息的。所有发送给该服务的消息,都要先压到该服务的消息队列中。

     服务启动起来了,来看看数据包是如何从一个服务发送给另一个服务的。
     来看看 skynet_send 和 callback 函数的定义:

source和destination分别是发送方和接收方的handle。
type是发送方和接收方处理数据包的协议
session识别本次调用的口令,发送方发送一个消息后,保留该session,以便收到回应数据包时,能识别出是哪一次调用。
msg/sz是数据包的内容和长度,成对使用

skynet 的消息调度

Skynet 维护了两级消息队列。

每个服务实体有一个私有的消息队列,队列中是一个个发送给它的消息。消息由四部分构成:

struct skynet_message {
    uint32_t source;
    int session;
    void * data;
    size_t sz;
};
向一个服务发送一个消息,就是把这样一个消息体压入这个服务的私有消息队列中。这个结构的值复制进消息队列的,但消息内容本身不做复制。

Skynet 维护了一个全局消息队列,里面放的是诸个不为空的次级消息队列。

在 Skynet 启动时,建立了若干工作线程(数量可配置),它们不断的从主消息列队中取出一个次级消息队列来,再从次级队列中取去一条消息,调用对应的服务的 callback 函数进行出来。为了调用公平,一次仅处理一条消息,而不是耗净所有消息(虽然那样的局部效率更高,因为减少了查询服务实体的次数,以及主消息队列进出的次数),这样可以保证没有服务会被饿死。

这样,skynet就实现了把一个消息(数据包)从一个服务发送给另一个服务。

参考:Skynet 设计综述

actor原理

先从著名的c10k问题谈起。有一个叫Dan Kegel的人在网上(http://www.kegel.com/c10k.html)提出:现在的硬件应该能够让一台机器支持10000个并发的client。然后他讨论了用不同的方式实现大规模并发服务的技术,归纳起来就是两种方式:一个client一个thread,用blocking I/O;多个clients一个thread,用nonblocking I/O或者asynchronous I/O。目前asynchronous I/O的支持在Linux上还不是很好,所以一般都是用nonblocking I/O。大多数的实现都是用epoll()的edge triggering(传统的select()有很大的性能问题)。这就引出了thread和event之争,因为前者就是完全用线程来处理并发,后者是用事件驱动来处理并发。当然实际的系统当中往往是混合系统:用事件驱动来处理网络时间,而用线程来处理事务。由于目前操作系统(尤其是Linux)和程序语言的限制(Java/C/C++等),线程无法实现大规模的并发事务。一般的机器,要保证性能的话,线程数量基本要限制几百(Linux上的线程有个特点,就是达到一定数量以后,会导致系统性能指数下降,参看SEDA的论文)。所以现在很多高性能web server都是使用事件驱动机制,比如nginx,Tornado,node.js等等。事件驱动几乎成了高并发的同义词,一时间红的不得了。

其实线程和事件,或者说同步和异步之争早就在学术领域争了几十年了。1978年有人为了平息争论,写了论文证明了用线性的process(线程的模式)和消息传递(事件的模式)是等价的,而且如果实现合适,两者应该有同等性能。当然这是理论上的。针对事件驱动的流行,2003年加大伯克利发表了一篇论文叫“Why events are a bad idea (for high-concurrency servers)”,指出其实事件驱动并没有在功能上有比线程有什么优越之处,但编程要麻烦很多,而且特别容易出错。线程的问题,无非是目前的实现的原因。一个是线程占的资源太大,一创建就分配几个MB的stack,一般的机器能支持的线程大受限制。针对这点,可以用自动扩展的stack,创建的先少分点,然后动态增加。第二个是线程的切换负担太大,Linux中实际上process和thread是一回事,区别就在于是否共享地址空间。解决这个问题的办法是用轻量级的线程实现,通过合作式的办法来实现共享系统的线程。这样一个是切换的花费很少,另外一个可以维护比较小的stack。他们用coroutine和nonblocking I/O(用的是poll()+thread pool)实现了一个原型系统,证明了性能并不比事件驱动差。

那是不是说明线程只要实现的好就行了呢。也不完全对。2006年还是加大伯克利,发表了一篇论文叫“The problem with threads”。线程也不行。原因是这样的。目前的程序的模型基本上是基于顺序执行。顺序执行是确定性的,容易保证正确性。而人的思维方式也往往是单线程的。线程的模式是强行在单线程,顺序执行的基础上加入了并发和不确定性。这样程序的正确性就很难保证。线程之间的同步是通过共享内存来实现的,你很难来对并发线程和共享内存来建立数学模型,其中有很大的不确定性,而不确定性是编程的巨大敌人。作者以他们的一个项目中的经验来说明,保证多线程的程序的正确性,几乎是不可能的事情。首先,很多很简单的模式,在多线程的情况下,要保证正确性,需要注意很多非常微妙的细节,否则就会导致deadlock或者race condition。其次,由于人的思维的限制,即使你采取各种消除不确定的办法,比如monitor,transactional memory,还有promise/future,等等机制,还是很难保证面面俱到。以作者的项目为例,他们有计算机科学的专家,有最聪明的研究生,采用了整套软件工程的流程:design review, code review, regression tests, automated code coverage metrics,认为已经消除了大多数问题,不过还是在系统运行4年以后,出现了一个deadlock。作者说,很多多线程的程序实际上存在并发错误,只不过由于硬件的并行度不够,往往不显示出来。随着硬件的并行度越来越高,很多原来运行完好的程序,很可能会发生问题。我自己的体会也是,程序NPE,core dump都不怕,最怕的就是race condition和deadlock,因为这些都是不确定的(non-deterministic),往往很难重现。

那既然线程+共享内存不行,什么样的模型可以帮我们解决并发计算的问题呢。研究领域已经发展了一些模型,目前越来越多地开始被新的程序语言采用。最主要的一个就是Actor模型。它的主要思想就是用一些并发的实体,称为actor,他们之间的通过发送消息来同步。所谓“Don’t communicate by sharing memory, share memory by communicating”。Actor模型和线程的共享内存机制是等价的。实际上,Actor模型一般通过底层的thread/lock/buffer 等机制来实现,是高层的机制。Actor模型是数学上的模型,有理论的支持。另一个类似的数学模型是CSP(communicating sequential process)。早期的实现这些理论的语言最著名的就是erlang和occam。尤其是erlang,所谓的Ericsson Language,目的就是实现大规模的并发程序,用于电信系统。Erlang后来成为比较流行的语言。

类似Actor/CSP的消息传递机制。Go语言中也提供了这样的功能。Go的并发实体叫做goroutine,类似coroutine,但不需要自己调度。Runtime自己就会把goroutine调度到系统的线程上去运行,多个goroutine共享一个线程。如果有一个要阻塞,系统就会自动把其他的goroutine调度到其他的线程上去。
一些名词定义:Processes, threads, green threads, protothreads, fibers, coroutines: what’s the difference?

Process: OS-managed (possibly) truly concurrent, at least in the presence of suitable hardware support. Exist within their own address space.
Thread: OS-managed, within the same address …