« June 2019 | Main

July 08, 2019

用 skynet 实现 unity 的 cache server

我们公司的一些 Unity 项目,当 cache server 的数据上涨到几百 G 后,经常遇到问题。最近一次是 nodejs 内存使用太多导致进程挂掉。

我不太想的明白,一个几乎不需要在内存中保留什么状态的服务,为啥会吃掉那么多内存。简单看了一下 cache server 的官方实现 感觉实现的挺糟糕的。它的业务很简单,还不如按协议自己实现一个。

Cache server 解决的是 Unity 的 asset 打包问题,一台机器如果把资源从 A 转换为 B ,那么可以把结果 B 提交到 cache server 上;后来的人就不需要重新做这个转换流程,而直接下载 B 。

怎么定义 一个 B 是从 A 转换过来的呢?Unity 的每个资源都有一个唯一的 guid ,如果经过转换,那么转换过程,包括了原始数据的 hash 版本号,平台,转换程序,转换参数等等,可以计算出一个 hash ;任何参数的变更都会导致这个 hash 变化。所以 guid + hash 就可以代表一个唯一资源的唯一版本。在需要做转换时,算出 hash 去服务器上查询,如果不存在,就在本地转换然后上传;如果后来的人重新进行这个过程,就会发现前人上传的结果,直接下载即可。

这个过程中, cache server 对资源如何转换完全不需要了解。它就是一个大的 hash 表,客户端可以用 guid+hash 作 key 读写数据。

官方是用 nodejs 实现的。我前几年看过一次,原来只有几百行一个很小的程序,这次再看发现增加了不少代码,但是基本业务并没有变化。我认为它一直都很糟糕,现在甚至比以前更糟糕。

简单列举一下我认为的问题:

  1. 用 js 实现了一个非常复杂的 cleanup 过程,用于淘汰掉可能不再使用的 cache 文件。这个工作不应该在服务本身的进程做,因为 nodejs 的逻辑是跑在同一线程中,cleanup 的过程很可能影响正常的服务业务。而淘汰 cache ,用一个 crontab 加一个脚本就可以做的更好。实在没必要增加服务的复杂度。

  2. 同时支持了 file cache 和 mem cache 。这可能是内存爆掉的元凶。mem cache 是没有必要的,因为如果进程本身占用内存不多的话,操作系统会把所有闲置内存用于 IO 的 cache 。这个服务可以看成是一个普通的静态文件服务,无非是打开一个硬盘目录中的文件,发送给客户端。自己再做一层 mem cache 实属浪费,它不会提升什么性能,如果提升了,那么一定是原有的东西实现的有问题罢了。

  3. 文件散列在 cache 目录中做的不好。只简单做了一级目录的散列。导致我们的项目同一个目录下有几万个文件。虽然在 linux 下 ext4 对同一目录下放太多文件支持的不错,但是很多 cache server 还是跑在 windows 下,ntfs 支持的就糟糕一些。更重要的是,这对远程管理的负担太大。就算在本地,列目录也会卡住(因为默认的列目录还需要对文件名排序)。

  4. cache server 本身的协议设计的太草率,基本就是能用就凑合。比如我摘取一小段官方文档里对握手协议的描述就可见一斑。

The server reads eight bytes of data from the first package received. If the package contain less than eight bytes, only those are used. Only exception if getting a one-byte package, in which case it should wait for the next package in order go get at least two bytes worth of data.

居然在一个 tcp 流上设计的协议,谈 first package ,又没有怎样区分一个 package 的描述。貌似指 api 的读取调用收到的第一串数据?握手有可能是 8 字节,也有可能是 1 字节。


除了协议之外,我认为用 skynet 实现一个一样的替代物应该没太大工作量。上周花了几个小时试了一下,果然只需要 200 行左右的代码。核心功能 100 行代码就可以了,另外做了不少优化的工作,以更适合管理更大的项目(多人协作的高并发,海量文件)。

目前,我自己写的版本在公司内部测试。希望日后可以逐步取代官方 nodejs 版本的服务。这样方便根据实际使用情况做进一步的优化。