Sucha's Blog ~ Archive for June, 2023

23年6月24日 周六 23:36

cincau 支持多进程模型

自从 mnet 在 Mac/Linux 下支持了多进程模型,cincau 也支持上了。

luajit 会检测是否增加了多进程的配置,如果有多进程的配置,将创建了一个控制面用的 UDP。然后 monitor 根据 worker 的存活数量,fork() 足够数量的 process。

控制面之所以选用 UDP 而不是 TCP,是因为 UDP 是按帧发送的,不需要再做一层协议的解析,系统内也不存在丢帧,只要发送的数量不多就没啥问题,缺点是一帧的数据长度有限。

另外,monitor 通过 fork() 拿到了 worker pid 后,需要将 pid 传递给调用的 luajit 做记录。而 worker 在被 fork() 后,需要往 monitor 控制面的 UDP port 发送数据,传递自己的 port 和 index 的对应关系。

每个 process 都有控制面的 UDP 用来收发内部数据,接口部分是提供了类似 worker index 来指明需要发送到的 worker port。之所以用 worker index,是因为 worker 被重新创建后,绑定 port 是会变化的(配置文件中不需要指明每一个 worker 的 port)。worker index 序号将会从 1 到 worker count,其他的序号代表 monitor。

而且在每个 worker process 内,是不知道其他 worker 的控制面端口的,只能通过 monitor 来转发,monitor 毕竟拥有所有 worker 的 index 和 port 的对应关系。

之所以 monitor 需要占据其他非 worker 的 index,举个例子,比如搜集当前所有 worker 的某项数据。

可以向 worker 1 发送事件,worker 1 接收到后,往 worker 序号为 index + 1 的发送,一直到最后一个 worker 发送给序号 worker count + 1,最后就是 monitor 收到了,你可以认为数据是被不断叠加的,一直到 monitor 收到结果。

这样本轮就搜集完了。如果需要传递收集的结果,monitor 可以广播到所有的 worker ,这样就完成了一个数据的循环。

假若某个 worker 挂掉后,拥有 cookie 的客户端如果继续访问,接管的其他 process 为了保持登陆态(比如拥有服务端定义的最新的 cookie 数据),可以事先通过某个事件同步 cookie,monitor 也一样接收。这样当 worker 被 fork() 后,拿到的将是跟其他 worker 一样的的 cookie 数据。

上述的 monitor 模型依赖 mnet 的多进程模型,虽然持有 listen fd,但是不处理 accept 事件,只有一个系统内的控制面 UDP 来传递数据,其他的工作,只是检查、拉起 worker process,状态相对稳定。

另外在 .mooc 配置文件上,透出来了多进程配置中的数据回调、发送函数,除了 WORKER_ONLINEWORKER_ROUTEWOREKR_SESSION 是保留事件外,其他的可以自行添加处理。

23年6月11日 周日 11:49

mnet 支持多进程模型

mnet 目前还未原生支持多线程,比如在同一进程内持有所有 socket 列表,无法独立区分不同线程,反正把这个区分交给调用方来解决。

而对于 mnet 多进程模型,之前也无法区分,比如无法在不同进程 listen 同一个 fd。

当然现在可以了,是基于 Mac/Linux 的 fork() 和 waitpid() 等系统调用做的,Windows 下面没有实验,也不打算实验了。

上面链接的例子,是基于 Linux 的 fork() 系统调用,子进程可以访问父进程的所有资源,比如打开的 fd 等等,在代码方面,指针、fd 在 fork() 后看起来跟父进程是一摸一样的。

多进程模型下,fork() 出来的子进程会有多个,由于 event queue(epoll/kqueue)在内核中唯一,虽然子进程拥有该 event queue 的 fd,但实际上无法操作,验证得自己重新创建一条才行,所以子进程被 fork() 出来后,立即自己新建立一条 event queue,将之前拥有的 fd 都添加上去。

其次由于 listen fd 实际上也是父子进程都拥有的,在多条 event queue 中被监听,如果新的链接进来,其实只能 accept 一次,其余进入 accept 函数的实际上会 accept 失败,貌似较老版本的 Linux 会造成其他的问题,具体我也没有进一步地了解。这些是我从网上不知名博客搜到的,当时是想借鉴 nginx 的多进程模型的,就搜到了这个问题。

解决的办法是使用进程间锁,创建只有一个临界资源的 semaphore,子进程们在进入实际 accept 前,尝试独占该 accept 系统调用,如果尝试失败,将放弃此次 accept。该系统 API 在 Mac/Linux 下面的接口是一样的,

对于父进程,在这个多进程模型中,将上升为 monitor,并放弃对该 listen fd 的 accept,所以判断较为简单,不需要争夺关联 accept 的临界锁。因为多次 fork() 而拥有所有子进程的 pid,转而 waitpid(),在子进程崩溃后,可以重新 fork() 出新的子进程接着服务。

多进程模型相较于多线程,优势在于资源的隔离。进程之间通过操作系统的进程模型来隔离,子进程崩溃后还可以再次拉起来。劣势在于资源共享,如果子进程之间需要大量的资源同步和共享,因为都需要通过系统的 IPC,消耗会变大,而且也不方便。