skynet浅析

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 对上述概念的使用如下:

skynet浅析_第1张图片

服务退出监测 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

950 [: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)
8.baseserver
        多协程并发创建地图(跟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是不是跟这个有关呢)










你可能感兴趣的:(编程语言-c/c++,编程语言-lua)