http://blog.codingnow.com/eo/skynet/
https://github.com/cloudwu/skynet
代码量(截止2014.5.21)
目录结构(编译后).lua 4941行
.h .c 10361行(不含第三方库)
.h .c 69953行(含第三方库)
---------------------------------------
对比我们的项目:
Common c/c++ 58590行
MapServer c/c++ 13621行
lua 60855行-> 这一项没什么参考性,重复文件过多
lua写的服务层 service :
abort.lua bootstrap.lua cmemory.lua console.lua datacenterd.lua dbg.lua
debug_console.lua gate.lua launcher.lua multicastd.lua service_mgr.lua snaxd.lua
lua写的lua库 lualib:
datacenter.lua jsonpack.lua loader.lua md5.lua mongo.lua mqueue.lua multicast.lua
redis.lua skynet skynet.lua snax snax.lua socketchannel.lua socket.lua
c写的lua扩展(库) luaclib :
bson.so cjson.so clientsocket.so int64.so md5.so memory.so mongo.so
multicast.so netpack.so profile.so skynet.so socketdriver.so
<== lualib-src
lua-bson.c lua-clientsocket.c lua-memory.c lua-mongo.c lua-multicast.c
lua-netpack.c lua-profile.c lua-seri.c lua-seri.h lua-skynet.c lua-socket.c
c写的服务层 cservice :
gate.so harbor.so logger.so master.so snlua.so
<== service-src
databuffer.h hashid.h service_gate.c service_harbor.c service_logger.c service_master.c service_snlua.c
c写的底层框架 skynet-src :
malloc_hook.c skynet_env.h skynet_handle.h skynet_main.c skynet_monitor.c skynet_server.c skynet_start.c socket_kqueue.h
malloc_hook.h skynet_error.c skynet_harbor.c skynet_malloc.h skynet_monitor.h skynet_server.h skynet_timer.c socket_poll.h
rwlock.h skynet.h skynet_harbor.h skynet_module.c skynet_mq.c skynet_socket.c skynet_timer.h socket_server.c
skynet_env.c skynet_handle.c skynet_imp.h skynet_module.h skynet_mq.h skynet_socket.h socket_epoll.h socket_server.h
skynet lua api
skynet = require "lualib/skynet.lua"
skynet.start(start_func)
c服务snlua启动后执行的第一个lua文件里面的主逻辑必定是skynet.start(start_func),由此开始运行lua服务的逻辑
start_func是当前lua服务的初始化函数,也是当前服务的第一个协程的函数
之后在收到非response消息时dispatch_message会创建更多的协程来做逻辑
而调用skynet.start(start_func)的主线程会调度上述这些协程(yield)
dispatch_message(...)
这就是ctx的消息处理函数(skynet_context.skynet_cb, 返回零(假)时释放消息的内存.那么lua层如何控制呢?c.callback(dispatch_message,false)就是释放内存,c.callback(dispatch_message,true)不释放内存,具体参见vim -t _callback.那么lua层如何控制发消息时不做复制呢?我看skynet.send/call/rawcall都不支持对复制的控制,参见vim -t _send. 底层的默认处理是:对于Lua string直接复制,对于lightuserdata不复制,对于其他类型报错)
dispatch_message将是本服务的发动机(消息驱动机),也就是底层工作线程拿到本服ctx.mq中的一个消息后执行的消息处理函数(vim -t _dispatch_message, 底层由worker thread取到消息后调用这个函数而触发lua函数的调用),也就是本lua服的主线程(虽然线程ID不固定)
dispatch_message每处理完mq中的一个消息都要遍历并执行消息处理过程中fork而没运行的新协程(遍历fork_queue并coroutine.resume())
对于收到的每个非response(prototype!=skynet.PTYPE_RESPONSE)消息启动一个新协程X,用该协程来运行协议类型对应的dispatch函数来处理消息
对于收到的每个response消息,根据session从session_id_coroutine取出协程并恢复执行
协程X运行业务逻辑时可能会“对其他服务做请求并等待结果”或者“睡眠几秒”,这时协程X用yield抛出“CALL”/“SLEEP”等返回值并挂起,主线程根据yield抛出的值对协程做不同处理
CALL -> 协程X已对其他服务发出请求并等待回应 -> 主线程把协程X记录到session_id_coroutine中,下次收到对应的response消息(sessionID一致)时唤醒
SLEEP -> 协程X[已调用skynet.sleep(ti)等定时器返回]或[已调skynet.wait()等其他服务返回,这种情况一般需要用skynet.wakeup()来唤醒,否则协程可能永远沉睡下去了] -> 主线程把协程X记录到session_id_coroutine,并记录sleep_session,对于sleep_session需要用skynet.wakeup(co)唤醒 (session_id_coroutine和sleep_session两者怎么维持数据一致,这个细节还需要结合实例再看看 markbyxds )
skynet.newservice(name,...)
创建lua服务 skynet.rawcall(".launcher", "lua" , skynet.pack("LAUNCH", "snlua", name, ...))
其实是skynet_context_new(module_name("snlua"), param("cmaster"))
即,用snlua服跑着一个lua逻辑服(service/cmaster.lua),snlua创造了一个lua环境的沙盒
除了service/launch.lua自身以外,其他lua服务一般都是由service/launch.lua这个lua服负责创建的
当然,launch服最终还是调用的skynet.launch("snlua","xxx")来创建服务
skynet.launch
创建c服务, lualib-src/lua-skynet.c -> skynet_command(CTX,"LAUNCH",..) -> skynet_context_new(mod,args)
对于skynet.launch("snlua","xxx"),这是创建c服务snlua然后在它上面跑lua服务xxx
skynet.monitor(service, query) 监控服务退出,细节还没仔细看 markbyxds
skynet.uniqueservice(global,...)
创建一个唯一的服务,调用多次service/***.lua也只启一个实例,比如clusterd和multicastd
global=true时,在所有节点之间是唯一的
其实是用skynet.call(异步变同步)的方式让service_mgr服务创建目标服务(类似于通知launch服创建服务一样)
service_mgr这边如已创建则直接返回服务地址;如没则创建;如正在创建则等结果
skynet.queryservice(global,...) global=true时
如果还没有创建过目标服务则一直等下去,直到目标服务被(其他服务触发而)创建
skynet.rawcall
向目标服发送无协议的消息并返回response,协程挂起等返回,用同步代码的样式实现异步逻辑
当 A call B 时,如果 B 在回应前就退出了,A 会收到一条异常,并正确的传播到 A 里的 call 调用处;
当 A call B ,而 B 在回应前,A 自己退出了,B 也会收到一条异常,提示 A 已经不在了。但不会影响 B 的执行流程,只是让框架回收一些必要的相关资源。
skynet.call 跟skynet.rawcall的区别是向目标服发送指定协议的消息
skynet.send 跟skynet.call的区别是仅仅发消息而已,不关心返回值也不会让当前协程挂起(非request-response模式)
skynet.wait()
把当前协程挂起放入session_id_coroutine/sleep_session
当收到等待的消息(会把消息对应的正在等待session放入wakeup_session中)后该协程恢复执行
skynet.sleep(ti)
类似于skynet.wait(),只是要事先通知底层定时器
等定时器(在ti时间达到后)发来消息时该协程恢复执行
skynet.wakeup(co)
如果协程co正处于挂起等待的状态(在sleep_session中)则把它加入wakeup_session
本服务的主协程会在调度过程中把wakeup_session中的协程唤醒执行
skynet.ret 在当前协程(为处理请求方消息而产生的协程)中给请求方(消息来源)的消息做回应
skynet.retpack 跟skynet.ret的区别是向请求方作回应时要用skynet.pack打包
skynet.register_protocol
注册协议:
协议名(name)
协议ID(id)
发送消息的打包函数(pack)
接收消息的拆包函数(unpack)
接收消息的(分发)处理函数(dispatch)
已注册的协议记录在lualib/skynet.lua:proto这个数据结构上
每个服务会默认初始化lua/response/error这几种协议
skynet.dispatch(typename, func)
修改以typename为协议名的协议:用func这个函数来作为协议的dispatch函数(默认的lua协议没提供dispatch,需要使用者根据业务需要写)
skynet.fork
创建一个新的协程
这里有做协程对象池来加速,类似于我们项目中的线程池,创建一堆协程并挂起,接到业务后拿协程跑业务逻辑
skynet.register(name)
注册当前服务的名字(默认用:%x作为name,本地服务的自定义名以.打头)
把
记录到handle_storage->name,参数必须符合本地服务的命名规范 skynet.self() 返回当前服务的handleID,如果还没注册就先注册(类似于skynet.register)
skynet.harbor(addr) return “addr(ctx的handleID)对应的harborID",boolean(是否远端节点)
skynet.address(addr) return "addr(ctx的handleID或者name)对应的name(string,:%x或者自定义名)"
skynet-src/skynet_server.c
skynet_context_new 创建服务在最底层对应的实体,使用skynet一般不用手动调这个接口
example/config
standalone
master在这个地址上监听(master-slave(1:n),控制节点-工作节点)
控制节点(也可以同时是工作节点)上需要运行一个master服务
每个工作节点上需要运行一个slave服务(按照standalone配置主动连接master,等待已有slave对自己的连接)
所有需要跟其他节点通信的节点都要运行一个harbor服务
这个配置非空就表示会以多进程的模式使用skynet:全局唯一服务(skynet.uniqueservice(true, ...) ->skynet.call(".service", "lua", "GLAUNCH", ...) )需要运行在master节点上(service_mgr是管理本节点上所有服务的服务,如果本节点是master节点,那么所有节点共享的全局唯一服务也由master节点上的service_mgr管理)
service/service_mgr.lua
...
snax
暂时不看这一块了,不喜欢过于傻瓜化的东西
启动流程
skynet main执行配置文件(lua)提取配置信息config
|
创建c服务logger(service-src/service_logger.c)
|
按照config.bootstrap(即,"snlua bootstrap")创建ctx(即,c服务,snlua)
|
snlua ctx初始化(snlua_init)时给自己发送了本服务的第一个消息("bootstrap")
|
snlua ctx收到消息后设置各项lua全局变量(LUA_PATH/LUA_CPATH/LUA_SERVICE/LUA_PRELOAD)
|
以"bootstrap"为参数执行lualib/loader.lua: loader的作用是以cml参数为名去各项代码目录查找lua文件,找到后loadfile并执行(等效于dofile)
==>
==> 至此,一个以c服务(snlua)为载体而运行lua服务(service/bootstrap.lua)的service正式开始!
发起连接
lualib/socket.lua 可以参见”slave连接master“和”旧的slave连接新的slave节点“的逻辑
或者直接用低级接口 lualib-src/lua-socket.c (socket.lua是在这个c库基础之上的封装)
在一个service内部同时使用lualib/socket.lua和lualib-src/lua-socket.c的时候要小心"socket"协议重复注册的问题:
require "socket.lua"会注册socket协议
socket.lua socket.open(addr,port)会暂时挂起当前协程,直到socket_server做连接成功的反馈:
forward_message(SKYNET_SOCKET_TYPE_CONNECT, true, &result);
或者简单使用 lualib-src/lua-clientsocket.c (这个不依赖于skynet,上面说的是需要依赖skynet模型socket_server线程的)
接受连接
Gate WatchDog agent
示例./skynet examples/config ./lua examples/client.lua 对上述概念的使用如下:
服务退出监测 G_NODE.monitor_exit
...
c
分布式架构
参与者模式,actor model 是1973年就提出的一个分布式并发编程模型,在erlang语言中得到广泛支持和应用
http://blog.codingnow.com/2013/12/skynet_monitor.html
master 控制节点,可以附属在某个工作节点中
harbor 工作节点,所有工作节点之间建TCP连接,所有harbor跟master之间建TCP连接;新的harbor启动时先连接master,master通知其他harbor去连接新的harbor
http://blog.codingnow.com/2014/06/skynet_harbor_redesign.html
消息处理 http://blog.codingnow.com/2013/10/skynet_lua_coroutine.html
设计说明 http://blog.codingnow.com/2013/08/skynet_update.html
snlua
skynet中的lua服务
启动分两步,先创建出空的 lua vm
然后注册一个专用于启动的消息处理函数,并立刻给自己发一个启动消息("lua服务的脚本名",ctx->mq收到的第一个消息)
接下来由这个启动消息来触发 lua vm 的进一步初始化过程
数据结构
skynet_module <-> void* instance (1:n)
void* instance <-> skynet_context (1:1)
skynet_context <-> message_queue (1:1) (skynet_context.handle == message_queue.handle)
struct modules *M (vim -t M)
管理所有c模块(skynet_module:cservice-src->cservice;不含luaclib,luaclib是c写的lua扩展库,在lua中由require引入),默认最多32个,可配置
struct handle_storage *H (vim -t H)
管理所有skynet_context的消息处理函数(vim -t skynet_handle_register)
每个skynet_context分配一个消息处理的唯一ID(skynet_context.handle: vim -t handle_name),服务地址,由低24位的自增id和高8位的harbor节点id组成
H->name 数组,记录所有skynet_context名字信息
strunct global_queue *Q (vim -t Q)
管理所有message_queue
skynet_module
c模块(cservice/*.so)的接口规范:
modname_create 创建一个跟模块(modname)对应的instance对象
比如,snlua.so创建一个snlua对象,内含lua_State, skynet_context
modname_init 初始化上述对象
modname_release 释放上述对象
skynet_context
跟一个skynet_module生成的instance对应(Vim -t skynet_context_new:用module对象创造一个对应的instance出来)
1. 4 + config.thread 个线程
main_thread
monitor_thread * 1
timer_thread * 1
socket_thread * 1
workder_thread * config.thread
2.main_thread
从lua中读取配置信息;
初始化(mq管理、c动态库管理、定时器、epoll对象);
bootstrap,其实就是执行配置文件中用bootstrap写明的初始化动作"snlua bootstrap",用snlua(c写的动态库snlua.so,及定制的lua解释器)执行lua写的初始化逻辑bootstrap.lua;
启动线程;
3.monitor_thread
检测skynet_context是否为空,是则退出线程
检测所有worker_thread对应的skynet_monitor,判断是否有线程进入了死循环(没明白,如果worker处理某条消息时间过长是否会误判?markbyxds )
4.timer_thread
还没细看,大体是用链表管理所有定时任务,线程循环中检测对于时间已到的任务投递消息到对应消息队列message_queue(根据handle可找到唯一对应的message_queue和skynet_context)。时间以0.01s为单位。
5.socket_thread
接收和管理连接;把网络层的信息(连接建立、连接断开、新数据)构建成消息投递到对应模块的消息队列
本进程内其他模块要启动端口监听、管理socket连接等操作时用管道通知到socket_thread
6.worker_thread
每次线程循环从全局消息队列(global_queue)取一个二级消息队列(message_queue),再从二级消息队列中取出一条消息做处理
每个worker_thread给一个skynet_monitor对象,记录worker_thread处理消息的总个数和状态(正在处理的消息信息)
知识点
1.pthread_key_t pthread_key_create() pthread_setspcific() pthread_getspcific()
线程存储(skynet用它记录线程类别的标示)
一个函数被不同线程调用来读写一个全局变量时,该变量归进程内所有线程共享;
把该变量跟pthread_key_t绑定存储的话,该变量就会在每个线程中有个独立的拷贝,供所在线程读写,线程间不会相互覆盖该变量
c用文件做数据(static)和方法的封装,线程存储可保证每个线程有同类型和数量的独立数据
http://blog.csdn.net/lmh12506/article/details/8452700
2.int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
修改某个信号对应的处理函数,sigaction.sa_handler设置成SIG_IGN表示忽略某信号
SIGPIPE 向坏掉的管道或者断开的socket发送数据时触发这个信号,该信号的默认行为是终止进程
skynet整个进程(所有线程)都忽略了SIGPIPE
3.rwlock
用两个int组成的struct来代表一个rwlock,加锁解锁用原子操作(__sync_add_and_fetch等,貌似是编译器支持的函数)来实现(自旋锁,省线程上下文切换)
4.require返回值,以前没注意过这个问题
5. luaL_newlibtable(L, l); //lua5.2 auxiliary library functions; lua5.1 not supported
lua_getfield(L, LUA_REGISTRYINDEX, "skynet_context"); //注册表、伪索引,所有c lib都可以通过这个table存储数据;我们的应用不能以“下划线+大写字母开头的名字”和“整数”作为key在这里存储数据
luaL_setfuncs(L,l,1); //把数组l中的所有函数注册到栈顶的table中,把栈顶的1个元素作为所有函数的upvalue
6.google proto buffer
边用边记
1.skynet语法错误提示不清楚,今天(2014.8.18)漏写了一个逗号,硬是让我找了两小时(日志如下所示,错误居然是agent某行漏了个逗号)
[LUA_DBUG] gate(:0100000b) notify new connect(fd(6),addr(127.0.0.1)) to watchdog(:0100000a) lanuch service received: 4 :0100000a LAUNCH "snlua agent" [DEBUG] skynet_context_new(module_name("snlua"), param("agent")) [:0100000c] LAUNCH snlua agent [LUA_DBUG] error in service :01000003 main thread: false ./lualib/skynet.lua:135: ./service/launcher.lua:123: session=0 address=:0100000c cmd=ERROR stack traceback: [C]: in function 'assert' ./service/launcher.lua:123: in function 'f' ./lualib/skynet.lua:109: in function <./lualib/skynet.lua:103> [:0100000c] KILL self [:01000003] lua call [100000c to :1000003 : 0 msgsz = 5] error : ./lualib/skynet.lua:458: ./lualib/skynet.lua:135: ./service/launcher.lua:123: session=0 address=:0100000c cmd=ERROR stack traceback: [C]: in function 'assert' ./service/launcher.lua:123: in function 'f' ./lualib/skynet.lua:109: in function <./lualib/skynet.lua:103> stack traceback: [C]: in function 'assert' ./lualib/skynet.lua:458: in function <./lualib/skynet.lua:430>
在service-src/service_snlua.c _init函数执行lua脚本(pcall)返回的时候打印错误信息就可以准确定位错误了
2.skynet.call 参数可以用light userdata, 数据部拷贝,在同一个进程内值传递了指针地址
cluster.call 参数用light userdta时候就会有内存错误,因为跨进程接收方访问的地址是无效的
所以在使用cluster功能时要传递一块C内存数据到另一个节点的话还得先转成Lua的string再调用cluster.call做发送,这里有多余的内存申请、内存拷贝和Lua栈交互。这里将是我把架子搭起来之后要重点优化的地方。
3.关于worker_thread唤醒:当socket_thread有消息时如果worker全是睡眠状态就唤醒(每次唤醒都会让所有worker都收到事件并醒来);timer_thread每轮循环(2500微秒)中只要有worker睡眠就全部唤醒;
4.worker睡眠条件:从global_queue(message_queue有消息才会放入global_queue)中取出message_queue,然后pop出一个或多个(由权重决定)消息用luaservice处理;从global_queue取出下一个message_queue,有则处理新的、没有就继续处理当前message_queue,当前message_queue没有消息也会处理下一个message_queue(maybe NULL);当每轮循环结束发现没有可处理的消息时worker就会进入睡眠(等待条件变量来唤醒)
5.在调用自杀函数(skynet.exit())的rpc里面return xx会报错(服务都没了还怎么发起对其他服务的应答)
6.mapserver
call lua function c_onRangeUpdate failed: attempt to yield across a C-call boundary
http://blog.codingnow.com/2012/06/continuation_in_lua_52.html
http://lilydjwg.is-programmer.com/2012/12/29/lua-caveats.36879.html
--> 只能在 Lua 代码中使用,不能跨越 C 函数调用界限。也就是说,从 C 代码中无法直接或者间接地挂起一个在进入这个 C 函数之前已经创建的协程。
--> aoi消息用timeout在下一帧发(暂时是这样,也可能用fork更好?)7.mapserver
8.baseserver950 [:0000000c] Unknown request (lua): ^\d2m^B^B^BB 951 [:0000000c] lua call [9 to :c : 23 msgsz = 21] error : ^[[31m./lualib/skynet.lua:543: ./lualib/skynet.lua:449: Unknown session : 23 from 9 952 stack traceback: 953 [C]: in function 'assert' 954 ./lualib/skynet.lua:543: in function 'skynet.dispatch_message'^[[0m --> skynet.start(function() InitWorld() --应该写在skynet.disaptch(..)之后,因为这时lua协议还没注册,InitWorld里面发起rpc触发db对map的rpc就会报上述错误 skynet.dispatch("lua", function(session, address, command, ...) UNIXTIME = os.time() local f = CMD[command] if f then skynet.ret(skynet.pack(f(...))) end end) end)
多协程并发创建地图(跟map交互挂起)会重复
--> 用skynet.wait()和skynet.wakeup(co)来解决,一个协程去创建时其他直接挂起,成功就全部唤醒9.时序问题
cluster2对cluster1发起RPC,cluster.call(...)
cluster1的处理函数里面用skynet.timeout(0,f)在f()中用RPC对cluster2回传一些数据
理论上说,cluster2应该先拿到cluster.call(..)的返回(协程阻塞被恢复并返回call的结果,nil或者有效数据),然后才会收到cluster1的RPC请求
-->实测结果:skynet不保证“cluster1对cluster2的cluster.call(..)反馈结果”和“cluster1在下一个协程对cluster2发起cluster.call(..)访问”两者到达cluster2的顺序
具体原因不明:是cluster1反馈结果的消息所走路径跟发cluster.call(..)时消息所走路径(转发消息的luaservice)不一样导致的吗?还是非同一条socket连接导致的?10.cluster实测结果显示:cluster通信,对于多协程并发做第一次通信,会创建多个socket连接,如果建立一次通信,后续并发都会走这个连接(9是不是跟这个有关呢)