Skynet 设计综述

  1. 多线程模式,可以使得状态共享、数据交换更加高效。而多线程模型的诸多弊端,比如复杂的线程锁、线程调度问题等,都可以通过减小底层的规模,精简设计,最终把危害限制在很小的范围内。
  2. 做为核心功能,Skynet 仅解决一个问题:把一个符合规范的 C 模块,从动态库(so 文件)中启动起来,绑定一个永不重复(即使模块退出)的数字 id 做为其 handle 。模块被称为服务(Service),服务间可以自由发送消息。每个模块可以向 Skynet 框架注册一个 callback 函数,用来接收发给它的消息。每个服务都是被一个个消息包驱动,当没有包到来的时候,它们就会处于挂起状态,对 CPU 资源零消耗。如果需要自主逻辑,则可以利用 Skynet 系统提供的 timeout 消息,定期触发。Skynet 提供了名字服务,还可以给特定的服务起一个易读的名字,而不是用 id 来指代它。id 和运行时态相关,无法保证每次启动服务,都有一致的 id ,但名字可以。
  3. Skynet 原则上主张所有的服务都在同一个 OS 进程中协作完成。所以在核心层内,不考虑跨机通讯的机制,也不为单独一个服务的崩溃,重启等提供相应的支持。
  4. Skynet 只负责把一个数据包从一个服务内发送出去,让同一进程内的另一个服务收到,调用对应的 callback 函数处理。
  5. 数据包通常是在一个服务内打包生成的,Skynet 并不关心数据包是怎样被打包的,它甚至不要求这个数据包内的数据是连续的(虽然这样很危险,在后面会谈及的跨机通讯中会出错,除非你保证你的数据包绝对不被传递出当前所在的进程)。它仅仅是把数据包的指针,以及你声称的数据包长度(并不一定是真实长度)传递出去。由于服务都是在同一个进程内,接收方取得这个指针后,就可以直接处理其引用的数据了。

    这个机制可以在必要时,保证绝对的零拷贝,几乎等价于在同一线程内做一次函数调用的开销。

    但,这只是 Skynet 提供的性能上的可能性。它推荐的是一种更可靠,性能略低的方案:它约定,每个服务发送出去的包都是复制到用 malloc 分配出来的连续内存。接收方在处理完这个数据块(在处理的 callback 函数调用完毕)后,会默认调用 free 函数释放掉所占的内存。即,发送方申请内存,接收方释放。

  6. 我们来看看 skynet_send 和 callback 函数的定义:

    int skynet_send(
      struct skynet_context * context, 
      uint32_t source, 
      uint32_t destination,
      int type,
      int session,
      void * msg, 
      size_t sz
    );
    
    typedef int (*skynet_cb)(
      struct skynet_context * context,
      void *ud, 
      int type, 
      int session, 
      uint32_t source ,
      const void * msg,
      size_t sz
    );
    
    //发送一个数据包,就是发送 msg/sz 对。
    //我们可以在 type 里打上 dontcopy 的 tag (PTYPE_TAG_DONTCOPY) ,让框架不要复制 msg/sz 指代的数据包。否则 skynet 会用 malloc 分配一块内存,把数据复制进去。callback 函数在处理完这块数据后,会调用 free 释放内存。你可以通过让 callback 返回 1 ,阻止框架释放内存。这通常和在 send 时标记 dontcopy 标记配对使用。
  7. skynet 核心并不解决进程间通讯的问题。数据交换方式并非类似 TCP 的数据流,所以,没有必要把服务间的通讯形式强行统一为单个数据块。最合适做进程内通讯的方式就是 C 结构。消息发送方和接收方都处于同一个进程内时,它们一定可以识别同一个 C 结构映射的内存块,不必考虑内存布局,字节序等问题。在这个层面上使用这种更高效的数据交换方式,可以极大的提升性能。

  8. session 是什么?由于每个服务仅有一个 callback 函数,好比在 ip 协议中去掉了端口的设定,所有发送到一个 ip 地址上的 ip 包就无法被分发到不同的进程了。这时,我们就需要有另一个东西来区分这个包。这就是 session 的作用。使用 skynet_send 发送一个包的时候,你可以在 type 里设上 alloc session 的 tag (PTYPE_TAG_ALLOCSESSION)。send api 就会忽略掉传入的 session 参数,而会分配出一个当前服务从来没有使用过的 session 号,发送出去。同时约定,接收方在处理完这个消息后,把这个 session 原样发送回来。这样,编写服务的人只需要在 callback 函数里记录下所有待返回的 session 表,就可以在收到每个消息后,正确的调用对应的处理函数。

  9. type 的作用?type 表示的是当前消息包的协议组别,而不是传统意义上的消息类别编号。协议组别类型并不会很多,所以,我限制了 type 的范围是 0 到 255 ,由一个字节标识。在实现时,我把 type 编码到了 size 参数的高 8 位。因为单个消息包限制长度在 16 M (24 bit)内,是个合理的限制。这样,为每个消息增加了 type 字段,并没有额外增加内存上的开销。

  10. 为什么整个系统不统一使用一种消息编码协议?这样,所有服务间都不会有沟通障碍。这样,甚至 session 参数也可以编码在数据包中。那仅仅是一丁点效率问题。但是,在真正的项目开发中,这其实做起来很难。尤其在使用第三方库的时候,你需要做很多低效的封装工作,才可以把交互协议都统一起来。而且,这个一致的编码协议也就成了系统底层约定的一部分,成为所有开发人员都需要了解的知识

  11. handle 也就是每个服务的地址,在接口上看用的是一个 32 位整数。但实际上单个服务中 handle 的最终限制在 24bit 内,也就是 16M 个。高 8 位是保留给集群间通讯用的。我们最终允许 255 个 skynet 节点部署在不同的机器上协作。每个 skynet 节点有不同的 id 。这里被称为 harbor id 。这个是独立指定,人为管理分配的(也可以写一个中央服务协调分配)。每个消息包产生的时候,skynet 框架会把自己的 harbor id 编码到源地址的高 8 位。这样,系统内所有的服务模块(用handle来标识,高8位是 harbor id ,低24为本节点中的服务标识符),都有不同的地址了。从数字地址,可以轻易识别出,这个消息是远程消息,还是本地消息。

  12. 集群间的通讯,是由一个独立的 harbor 服务来完成的。所有的消息包在发送时,skynet 识别出这是一个远程消息包时,都会把它转发到 harbor 服务内。harbor 服务会建立 tcp 连接到所有它认识的其它 skynet 节点内的 harbor 服务上。

  13. 全局名字服务:

    skynet 目前支持一个全局名字服务,可以把一个消息包发送到特定名字的服务上。这个服务不必存在于当前 skynet 节点中。这样,我们就需要一个机构能够同步这些全局名字。为此,我实现了一个叫做 master 的服务。它的作用就是广播同步所有的全局名字,以及加入进来的 skynet 节点的地址。本质上,这些地址也是一种名字。同样可以用 key-value 的形式储存。即,每个 skynet 节点号对应一个字符串的地址。

  14. 组播

 

转载于:https://my.oschina.net/zengjs275/blog/719663

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值