Avatar
😀

Organizations

  • 事件总线

    消息订阅,发布是每个服务器都必备的机制,当然 skynet 也不例外,而且更加优雅。

    绕不开传统的事件订阅流程,skynet的事件总线是一个process,意味着

    1. 所以事件订阅发布都是异步的,

    2. 所有消息的发布是可调度的(参见上节 scheduler),

    3. 可以通过控制独占来解决事件总线的调度频率

    emm… 似乎也没什么好些的,eventbus更像底层某些机制的封装,了解即可

    Created Thu, 30 Jan 2025 00:00:00 +0000
  • 简介

    skynet 实现了一套调度器机制,用以调度 process, 与云风大佬不同的是,sched 支持公平模式以及独占模式,独占模式主要是为了解决特性场景下业务的响应速度,从而提升体验感, 其次,不一定需要按照每个 process 对应一条协程,减少runtime调度以及内存的开销

    Sched

    一个伪线程的逻辑处理器概念,它分为独占和负载两种模式。

    • 独占是为了更好的处理实时性更高的业务,它不会被其他任务抢占

    • 负载又可分为两种运行态,均匀的处理业务以及从其他process上偷窃任务,尽量保证Processor不会过于闲置,除此之外,负载Processor可随着任务的变动而增加(不会超过最大设定值),特别的当某个任务陷入”死循环”或者是超出设定运行阈值的时候会重新创建一个Processor`,并让之前的挂起(在C版本中将会被强制关闭)。

    • 多数时候协程过多会造成系统压力。

    C版本和Go版本调度和设计上差异不大,但一些细节上的处理可能不同,因为C可以提供更多的底层控制

    Process

    process 是一种用户态的伪概念,用于描述一个 actor的具体实现方式,也是 skynet最小调度单元,这种概念对于分布式而言更友好,无关乎process在哪,只需要知道 PID 或者 Alias 即可向其投送消息。 主要是对业务几乎无侵入

    Message

    messageskynet 基础消息承载结构

    服务的消息队列

    Actor 模型最重要的的概念是 mailbox,它代表了一个实体需要处理的队列容器,

    得益于go的简单性,可以使用 channel 来实现,但这种方式的实现性能不高,因为 channel 底层的结构使用的是互斥锁,

    所以我采用了mpsc 实现了无锁队列,性能更优于 channel

    TODO: 吞吐量对比

    消息的接受和发送

    • 发送

    用户不需要构建这个结构体,仅仅需要指定 dest 以及需要发送的数据,而且 skynet 消息投递被设计成不允许发送 nil 因为这是无任何意义的,相反它还会消耗服务投递的性能,如果确实有这种需求,可以发送 struct{}{}

    至于这个节点是再本地,还是其他地方并不重要

    • 接收

    接受回调只包含5个关键参数 context,addr,session,mtype,msg,size

    • context 其实就是创建process指定的结构指针,用于表示处理上下文

    • addr 即为投递者的Pid,(需要注意的是,skynet支持redirect 以及 fake模式,所以这地址需要在发送的时候明确)

    • session 主要的作用是用以区分这条消息是否是同步请求, 如若大于0,则其值就是请求序列号,只需要通过 skynet.ret(msg) 返回即可

    Created Fri, 22 Jul 2022 00:00:00 +0000
  • 工作中曾经开发了一个cobweb的分布式服务器框架(基于golang,c),但是在实际开发过程中代码难以维护以及更新,主要是每次都需要跨平台进行编译,特别是cgo 往往需要指定平台的系统库,而且一些不规范的使用方式造成无法充分发挥多核的优势,可以参见 关于Go协程的思考 虽然1.16 支持抢占式,但错误的使用方式依然造成了cpu过高的问题。,后续重新设计了skynet 是一个actor模型分布式服务框架,使用go编写。

    尽管Actor模型和CSP模型各有所有长,为什么不采用CSP主要有以下方面考虑。

    1. CSP模式使用尽管很简单,但是一个致命的问题是无法控制消息的优先级,当然若只处理一个Channel那可以规避,那么为啥还需要使用CSP,而且像go channel 本身是基于互斥锁(1.16)实现,且无法进行优化和更加精细的控制,只能依赖于runtime的调度。(网上所说什么时候触发调度,我认为channel不能包含其中,它本质也是加锁导致切换)

    2. 隔离性太弱,后续一些新的channel引入也会造成破坏性修改。

    3. select-case模式会随着等待数量的增加性能会慢慢减弱。

    4. channel 多大合适?

    它是一个年轻的框架,仅仅经历了两款项目的迭代 现在版本为 v1.6.0 2023-05-28 重启了 v2 版本

    Created Mon, 20 Jun 2022 00:00:00 +0000
  • 简介

    sktpmd模块是skynet底层集群模块,它承担了skynet网络节点之间的通讯职能。全名为(skynet port managment daemon)

    架构

    1. sktpmd 为了满足对等网络的性质,所以每次和其他节点建立连接是有两条连接, 当A节点于B节点建立连接,首先A节点发送握手等待B确认,B确认完成之后重复走A的流程,这样一个双向连接就被建立了起来,1.6.0改变了个行为,对于像存在类似缓存,或者数据中心的业务而言的单向节点而言,只需要一条连接即可,节省资源。

    2. sktpmd现在支持原始的tcp,udp,unix协议,后续规划可能由reliable udp实现,降低集群通讯延时并提供更好的性能和时延性。

    3. 远程命名服务,通过内置命令生成唯一的Name,通过Name来与其他节点通讯是友好的。

    使用

    • 启动也非常简单,无须任何代码,仅仅只需要在 conf.conf 中配置一下即可,使用的时候跟节点内通讯无任何区别。因为我已经作平了本地和节点之间的差异。

    • 内部均由kproto进行编码,提供更快的序列化方式。

    • example call

    skynet.send("host:port@name","rpc"...) -- 通过域名或者地址+端口的形式和其他节点进行通讯
    skynet.send("alias","rpc",...) -- 通过别名
    skynet.send(pid, "rpc",...)              -- 通过pid亦可
    

    tunnel

    既然节点之间是双向连接,所以连接数量为 f(n) = n²-n,如果节点过的时候,势必造成 socket fd 消耗殆尽,

    基于这个问题,可以通过内置的tun,来设置代理,这么一来,tun的作用相当于这个集群节点的网关。因为其内部的节点相对于其他 tun 代理是不可见的,

    通过配置tun的规则开启多个,则可以实现业务拆分。

    2022-10-07 此模块被弃用,可以用多节点转接的方式或代理的方式做到,如 send("n1.n2.n3@name")

    服务发现

    sktpmd 提供了一套服务发现机制,但其运作原理是不同于 etcd 或者 consul,它本身是一个惰性发现,它不需要一个中心服维持它们的关系。

    sktpmd整个发现流程是基于 gossip 算法来发现的,但一些api依然可以主动触发,v1.6.0 这个模块将保留,因为集群模式的逻辑改变了

    v1.6.0集群建立

    v1.5.0 之前节点之间都是双向链接,但考虑到一个单向服务器,如 dns server,conf server 等,大部分是 request/response 模式,惰性连接的收益很大,所以去除之前的一些设计。

    网络底层

    参考 Go协程的思考,在linux下,使用了 epoll。所以尽量部署到 linux 下以发挥更好的性能

    Created Mon, 30 May 2022 00:00:00 +0000
  • 等后续补充,很多概念描述起来比较绕,等有空再写 (偷个懒)

    Created Sat, 30 Apr 2022 00:00:00 +0000
  • 无论是对于C版本还是Go版本的skynet而言,一个高效的内存分配器可以提高内存的使用效率,这里效率无论是对于内存碎片亦或是GC而言,都是一种更高效的手段

    基于 SLAB 算法的分段内存分配器

    SLAB 最开始是阅读 linux 源码学习的算法,在skynet中它确实有更优秀的性能,因为它直接分配了一块大共用内存,所以不会产生任何GC和真实分配,但在业务开发过程中,一旦忘记释放 那么这段内存将不能再被使用和获取了(也就是野指针),直到程序结束。最后的保守策略依然会向runtime申请内存,将会导致内存占用过高。

    而且内置的Debug模式也无法定位到这个指针,原因在于 golang 堆栈伸缩会导致指针地址变动,所以 Debug 只能定位到存在 memory-leak,而无法知道具体位置。若需要具体位置则需要hook这个调用栈,性能方面得不偿失

    基于 sync.Pool 的分段内存分配器

    将不同size的buffer放入不同的池中,按需进行分配,减少race的开销,这个方法虽然简单,但是性能是低于 slab

    但它确实能减轻心智负担,代价就是牺牲了部分性能以及gc压力,但这也是skynet默认使用的策略。如果需要使用可以在编译指令中指明slab

    Zmalloc

    zmalloc 本身也是 slab的升级版本,增加可伸缩链表实现对于预分配内存额外部分的缓冲池。

    在24个线程的cpu条件测试结果如下,zmalloc保持了一种稳定的时间复杂度,额外产生的内存分配也很少

    - time(ns) alloc/op(B)
    slab-128 23.5 1
    sync.Pool-128 2490 65562
    zmalloc-128 43.31 16
    slab-256 24 256
    sync.Pool-256 2204 65562
    zmalloc-256 44 16
    slab-1024 92 1024
    sync.Pool-1024 2490 65562
    zmalloc-1024 49 17
    slab-4096 365 4096
    sync.Pool-4096 2210 65562
    zmalloc-4096 45 23

    API

    • skynet.zalloc(n) 用以分配指定大小的内存块,考虑到 64在go中为tiny-size,直接会从P上分配,所以zmalloc分配块从128开始

    • skynet.zrealloc(buf,n) realloc函数会先检查buf,确保是否需要重新分配内存

    Created Thu, 21 Apr 2022 00:00:00 +0000