Sucha's Blog ~ Welcome

24年2月25日 周日 12:12

mmap(1)

自从 cincau 支持多进程的方式之后,同步数据的能力最开始是通过不同 worker 的 UDP socket 来传递的,即时性有限,大概固定几秒钟去同步一次,而且如果数据量大了也是个问题。

相比较下,即时性的问题才是主要的,比如其中一个 worker 接受了用户的登陆状态,用户的下个请求是另外一个 worker 来服务的,此时 cookie 的 session 信息根本还没同步过来,就成了问题。

当然这个问题也有粗放的解决办法,比如用 SQLite 来同步,但是这种 Key/Value 的信息也使用 SQLite 觉得还是太重了。

这里插播一句,之前了解过使用 mmap 来同步数据的 MMKV,因为 mmap 同样可以用来多进程同步数据,因此这个库也是可以多进程同步的,感觉这个库可以解决 95% 以上的同步问题,其他的例外情况,我下面会说。

在展开其他方案之前,我得检讨一下,一开始走了很多的弯路,虽然了解到需要使用一些多进程同步手段,比如最开始的 socket,比如可以使用 Linux 消息队列,以及 mmap,但是我把这个问题想得简单了,而且一开始我也没想起 MMKV。

比如我一开始就选择了 mmap,想做一个 mmap 的有序字典,我手头有 MIT LICENSE 的 AVL tree 代码,相比之下 MMKV 这种 bitcask 结构的数据库,是无序的,除非你 dump 出来所有的 key 再排序,否则你无法快速拿到按照 Key 排序的第一个节点,而 AVL tree 对 Key 排序的方案就可以。

可是已有的 AVL Tree 的方案,其对节点的引用是指针的,虽然 mmap 只管映射内存区域,但是不同的进程,其映射的内存区域很可能不一样,比如我使用的 LuaJIT,除了我自己主动调用的 mmap,其他不知道什么原因,也有不少的 mmap 调用。导致如果使用 AVL tree 的方案,除非是能保证每一个进程 mmap 过来给 AVL tree 使用的内存块,采用同样的起始地址,否则后续的指针计算,指向的节点地址就有问题。

虽然理论上,我可以将进程空间的一大段都交给这个 AVL tree 共享内存的方案,但是实际做起来其实也不方便,而且 mmap 这个系统调用其实很频繁,用 strace 就可以看到,其他的一些文章也有介绍。

所以,不得不将 AVL tree 的指针引用,改成 int32_t 的间接引用,这里的 int32_t 分成了高 16bit 和低 16bit,其中的高 16bit 用于 index 内存区块,低 16bit 用于 index 每个区块内的位置,byte offset。

之所以这么做,是因为内存块是动态申请的,不会一下就申请完所使用的内存,而是根据使用量逐步申请,虽说是共享内存,也总得使用实际内存空间的嘛。

另外这个 AVL tree 的方案相比 MMKV,其实有很大的限制,因为考虑到不想做 bitcask 那样的回收方案(简单点做),因此设置了限定最大的 Key/Value 长度,这个是调用库在初始化的时候就得进行设置的。

比如 Key/Value 的最大长度是 64 字节,其中 Key 是一个 timestamp 占 32 字节,后续的 Value 是一个 MD5 的 hash 占 32 字节,这里还可以插播一句,可以将这个 Value 作为 MMKV 的 Key,这样就实现了 MMKV Key 的有序,回应了最开始说的 95% 的应用场景。不过这里也得提一下,MMKV 还可以设置每个 Key 的过期时间,所以这个 95% 的应用场景,实际上还可以扩大不少。

回到刚刚的 AVL Tree,所有需要同步的数据都存放到 mmap 的内存区块中,每个进程都有对 AVL Tree 的引用,以及自己保留一份已经打开的内存区块列表,当别的进程因为塞入更多的数据申请了新的内存区块,其他进程在进入库调用的时候,检查到这个场景,将新的内存区块也 mmap 到自己的进程空间上。

因为每条数据是固定的长度(最大 Key/Value 长度),所以 AVL Tree 的 index 间接指针会忽略掉不同进程内存区块的起始地址不一致的情况,只关注内存区块的序号,以及每条数据对应内存区块的偏移字节,当这个内存区块已经映射到进程空间上后,对 index 的数据的读写就没有障碍了。

在实际使用中,当内存空间不足时,申请内存区区块序号(2^16个),映射内部的节点地址偏移,两者揉进每个节点的唯一 index,当删除节点时,节点可以回收而不需要整理内存区块,比如将该节点的 index 保存到未使用空间列表节点的 right 指针(间接指针)中。

上面说完了 mmap AVL Tree 的使用场景、面临的问题,提出的解决方案,和该方案的限制,下面列出来了一些实际跑通该场景的有趣细节:

细节太多了,当然相比较 SQLITE,上述两套方案的效率会高很多。

MMKV 已经很成熟,多平台都能支持,AVL Tree 是我当初小看了 mmap 导致的,而且有先天的短板,虽然说提供了有序 Key 的能力,但这个是在约束了最大 Key、Value(每条数据都是这个最大长度)来达到的,因为可以避免替换 mmap 共享的内存区块。

24年1月28日 周日 22:47

WebSocket(1)

研究了一下 WebSocket,在 HTTP/1.1 之上,提供了一个双工长连接,通过 header 的 Upgrade 来完成协议升级及参数交换,之后就都是 websocket 的 frame 了。其最基础的协议描述在 RFC6455,压缩部分的扩展在 RFC7962

之前研究了一下 C 写的 Websocket,验证了一下是可以正常工作的,但它 frame 的解析部分不好单独摘出来。

client 端的 html + js websocket data send/recv 其实很好写,browser 本来就有 WebSocket 的接口给调用,拉起本地的一个 html 就行了,但是 browser 无法指定 http header 的参数(包括 ws extension),发送 text、binary 的 frame 时,是通过 js 的 string、uint8 array 来指定的。

想着找一个可以直接提供解析 websocket frame 的库,虽然找到了 C 写的 wslay,但是实际使用过程中,感觉使用实在是繁琐。

比如 websocket frame 的结构如下:

 0               1               2               3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

回到之前说的 wslay,霓虹国写的东西太复杂,居然 header 没检测全都能返回好几种错误,我感觉这部分其实可以简单点,比如 header 都解析不了的时候其实可以提示数据不足(毕竟 header 数据都不足),缺点是放弃了一部分错误的检查。

在 HTTP/1.1 的协议升级到 websocket 之前,header 需要交换一些数据,比如 client 会发送如下的字段

Sec-WebSocket-Version: 13
Sec-WebSocket-Key: fSQMpewdgSIz1IhuhL3ERQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

说到 permessage-deflate,server 在 deflate payload 后构建 frame 时,需要设置 rsv1 bit 为 1,要不 client 会忽略 extensions 的描述。

还有 server 在 inflate frame payload 时,实际上认为不断喂进来的 payload 是连续的 data stream,因此不需要做 inflateEnd。

这个感觉也跟 HTTP/1.1 的实际处理类似,实际每个 http requet / response 都是一个单独的 inflate / deflate stream,websocket 升级了协议后,可以认为不断发送过来(出去)的 payload 是没有结束的 http body 数据,因此不需要结束这个 inflate / delfate stream。

其他比如 ping、pong、close frame 类型没怎么研究,不过感觉 close 没啥作用,也许是用于实际关闭 tcp 链接前的一个处理吧,也许给调用层处理,在 websocket frame 这一层感觉没什么必要,毕竟无论如何总是要处理没有发送 close 的资源注销的,那又何必多此一举要这个 close 呢。

23年12月30日 周六 22:22

遗迹2

这个月忙得要死,继续遗迹2水一篇。

郁闷的是还没通关,感觉应该是最后一关了吧,那个守卫太难打了,而且我的 6600xt 显卡即便用最低特效感觉都挺卡的,玩得有点吃力。

于是我就回过头去肝装备和等级,收获有一些,但不多,估计要年后才能继续这个最终 BOSS 了。

另外,这个游戏有些流程挺坑的,它的大部分地图都是随机的,有不同的版本,比如我进去的涅鲁德,如果最终不打守卫,或者不插钥匙到主控台的话,其实后续是可以过来场景下选择其他的小地图,继续肝等级和装备的,但是我当时不懂呀,钥匙随机就插入主控台了,也没听到从未见过的保管人说了啥,然后继续断断续续肝了涅鲁德的守卫人后,这个场景差不多关闭,只剩下一个宇宙的一角,只有一个录音机的叙事声音,其他场景都被破坏无法进去玩了。

这也太郁闷了,然后我之前不知道这个插钥匙的影响,没有备份角色资料,以及游戏世界的资料,于是就再也回不去刚开局遇到的涅鲁德了,只能开冒险模式单独玩这个章节,重开的这个章节也是随机的,还不是之前原来的那个。

另外,不知道是不是魂系游戏都这样,就是等级、装备上去后,之前比如在我遇到的第一个场景喜乐宫,我当时肝了好几个小时才通,反正拿的远程很难打,现在等级装备上去后,就感觉太容易了,刷经验没有任何乐趣可言。

网上搜了一下,貌似通关一次后,可以选择将世界升级为二周目,难度会上升一个级别,之前的老头环也是如此,有这样的一个选项,不过我没升级,感觉打同样的场景可太没意思了。

老头环我的操作熟练度很差,几乎可以说是靠着 TB 买的魂升级属性硬给拗过的;相比下遗迹 2 因为大部分的地图有随机场景,估计继续肝 2 周目的感觉会好一些吧,不过我现在还没通关一周目,后面再说吧。

23年11月26日 周日 23:58

法环和遗迹2

法环(艾尔登法环)是我第一次玩魂系,进入游戏的大树守卫者就已经让我崩溃了,后面就是熟悉的操作,上 TB 买游戏内魂币,升级人物属性, 后面的难度确实大大降低,很容易就搞到了厉害的武器,没多久就通关了。

不过也因为走了捷径,后面的游戏性其实没有那么强了,很少死了。

也看过不少厉害的游戏玩家,那个操作,我实在是学不来,罢了罢了,我只是通关走过场。

之后又看到了遗迹2的介绍,也算是一个魂系,配合 FPS 之类的远程打击,以及职业切换(还没玩到这个部分,不熟悉),那就入手试试吧。

然后花费了一个晚上,才算入门,死了四五十次都有,这次倒没想着要上 TB 买游戏币升经验,反正就试试看吧,看独立玩这个魂系能否进行下去吧, 感觉游戏性还是可以的,只是要计算的东西不少,比如弹药用量就有受的,因为有的枪射速太快了,伤害反而低,感觉升级武器的系统还是挺重要的, 武器反而不需要随便换,没那个必要。

相比法环,没有那么在意近距离的格斗,那个实在是太难了,除非把脑子和手都训练成一块,肌肉记忆,要不哪里来的及。

所以攻击力、弹药用量比较重要,可以继续搞搞。

23年10月29日 周日 14:50

装了一套 AMD YES R5-5500

最近装了一套 AMD YES R5-5500,内存金百达银爵 3600 长鑫 Adie,主板是爱国嘉 B450M K,风扇利民 AX120 R SE。本来是看中主板的 PBO 超频和内存 XMP 超频的,但是内存默认是 2666,XMP 也才 3200,PBO 默认的话,感觉差点意思,反正只依赖主板提供参数的话感觉不大够。

在 B 站学习的过程中,我找到了内存超频的小参可以升到 4133(5600g内存超频(3600至4133)金百达银爵3600长鑫A die 可以直接抄作业,手把手),经过了系统自带的内存诊断和 AIDA64 内存测试都稳稳的。

在内存 4133 和 CPU 主频 4.5G 的情况下,此时 CPUZ 分数可以到 610 左右。

CPU 主频定到 4.5G 参考的是((新)r5 5500+c9bjz+5600xt超频视频详解(技嘉b450m小雕),含2k分辨率游戏实测),待机温度 40+,但是 5500 积热,FPU 单烤温度一下就到了 80,此时风扇散热片还是凉的,如果机箱风道好的话,也许可以抗住吧。还有就是 Core Temp 上面电压总是固定为 1.35v,感觉有点高不知道为啥。

用了一阵,还是将 CPU 改成了 PBO Advance 来控制,教程(技嘉b450m主板锐龙5000系列开PBO2教程),PBO 定格后频率在 3.6 到 4.45 之间浮动,电压 1.2x 附近感觉还行,FPU 单烤温度要比定频 4.5G 好一点点。

显卡也是老古董 10 系,正常用整机功率 50W ~ 70W 这样,FPU 单烤时会飙升到 130W,不管是定频 4.5 还是 PBO Advance 定格都一样。

深圳秋天用了一阵,感觉还行,希望夏天应该没事吧。

23年9月30日 周四 21:54

买了一个 RX6600-XT

之前用了大概半年的 RX580,黑苹果 4K 输出,感觉挺好,只是机器有点浪费,毕竟 CPU 和内存都还不错的,想搞个双系统游戏主机,买了 1T 的 SSD,顺利安装上了 Win11,这样短板就来到了显卡 RX580。

PDD 上比较了半天,觉得 RX6600-XT 也不错,价钱不错,性能主流,就入手了。也用了一阵子了,感觉不错,相比 RX580 耗能要少,散热压力小一些,关键是主流游戏 1080、甚至 2K 都还行,现在闲暇时间,不少都是切换到 Win11 下玩游戏了。

黑苹果可加上 NTFS 的 KEXT 驱动,读写 Win11 分区不成问题,所以对于黑苹果来说,也算是扩大了硬盘容量。虽然 SATA 3.0 600MB 的速度远不如 M2 的速率,但总比没有好嘛,可惜主板是 ITX,只有一个 M2 的接口,要不谁不想接双 M2 呢。

22年8月27日 周日 20:57

新机箱和 ARGB 灯

之前的 ITX 机箱散热不大行,于是换成了一个大一点的机箱,一整面都是钢化玻璃,算是海景房配置了。

这个便宜的机箱虽然有开机键,但是没有电源指示灯,主板是否上电,得仔细看风扇才能知道,这样子不如搞个 ARGB 灯呢,再说之前都没玩过。

主板是支持 ARGB 灯的,PDD 上也很便宜,不到 10 块钱一个的 9cm 风扇就有带灯的了。

而且主板打开 ARGB 后,默认配置的 ARGB 幻彩就很好看,各种颜色无缝转换真的挺好看的,windows 系统下面不用设置,我就用默认的,但是问题来了,重启进入 mac 后,幻彩变成了单色灯,如果再重启,灯就不亮了,估计是 mac 下面覆写了 ARGB 灯的配置,而主板支持在操作系统上面做配置。

但是主板没有提供这个比如说固定配置,或者只读的模式,防止操作系统覆盖,这个就很蛋疼了。

我是找了不少 mac 下面配置 ARGB 的,貌似华硕在 mac 下有专门的 app,但是我这个牌子的没有,而且我这个牌子的在 windows 下使用厂商提供的 ARGB 软件,都无法设置 ARGB 灯,感觉都没用心做,至少几家主板厂家上层配置的 app 是不通用的。

还好 PDD 上面有独立供电,不依赖主板 5v3 针 ARGB 接口的控制线,还支持无线设置,我没买无线的,买的就是几块钱便宜的线控设置,线控设置好后,重启也不影响。控制线的电源可以是 USB、大 4 pin 或者 sata 线,另外一端就直接当成 5v3 针的公口,风扇的 ARGB 控制线直接接了上去,避免了主板、操作系统的介入操作,配置直接在线控按钮上切换就好。

默认的颜色不好看,换了好几种,终于换到了幻彩,那就可以了,大赞🥰

23年7月29日 周六 21:39

记录 LXC/LXD 使用相关

不用 docker 的原因是,有些时候需要创建不同版本库的开发环境,这个时候,其实只是引用路径的不同,文件系统甚至是 directory 的,而不是 image,当然 LXD 貌似也没有提供类似 docker 的分层的文件系统,主要是我也不大懂 docker。

有一说一,后面其实发现 alpine 除了空间占用小一些,其他都是劣势,比如 musl 的 CRT,居然不能打印调用栈,我也是服气了,下面的内容来自之前的学习,大概是能建立起一个可以用的系统容器。

下面比如 a1、a2 是 alpine instance。

创建 profile

这个 profile 就是 default profile,后续从不同 image(发行版本)创建 container 时,会使用这里的 storage、network 等的配置

创建 instance

这里使用了 alpine 3.18 的 image,由于 container 可以直接保存东西,所以直接拉起来,这里 ash 是不会运行 ~/.profile 的,如果是从 sshd 登陆进去的才会,所以这种情况下需要 source 一下 shell 的配置文件。

$ lxc launch images:alpine/3.18 a1
$ lxc exec a1 ash

挂载 host 目录到 instance

比如挂载 host /wwwdata 到 a1 相关目录,之后删除这个挂载,这里的前提是文件其实是只读的,如果需要挂载读写的文件系统,需要创建一个用户权限 map 配置,在容器内的 /etc 相关配置中,不过感觉也不是很方便。

$ lxc config device add a1 sharedwww disk source=/wwwdata/ path=/var/www/html/
$ lxc config device remove a1 sharedwww

其他的问题

用 alpine 容器还遇到问题,比如拉起 cincau 映射 host 端口到容器,容器内的 proxy,居然一直长连接到内部服务器,从不关闭连接,人家 ngnix proxy 就做得很好,由于遇到上面说的 musl CRT 不能打印堆栈,就没有继续研究下去了。

作为封闭的开发环境感觉还行,作为 production 的容器,首先不能用 alpine image,其次,还有不少需要摸索调整的东西吧。

先记录一下。

23年7月08日 周六 22:17

OTP 研究

学习了一下 OTP 这个东西,比如用的是 LuaOTP

区分为 OTP、TOTP、HOTP 这三种,OTP 英文展开为 One Time Password,是服务端对客户端的一次性密码,属于双因素验证的范畴。

TOTP 这里,公式中的一个参与计算的因子是时间,比如这个时间因子,可以 30 秒更新一次,那这个一次性密码,也是 30 秒更新一次。

HOTP 有一个 counter 的概念,每次用过后都会变化,每次都生成新的,对应的是该一次性密码的使用次数。

上述 LuaOTP 支持 Google Authenticator,浏览器 JS 这边用的是菜鸟教程介绍的qrcodejs,用来生成二维码图片。

当然也可以不生成图片,而是导出字符串,Google Authenticator 也同样支持字符串导入。

之前不大理解这一套,搞了一下,发现也没有那么难。

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,消耗会变大,而且也不方便。