« January 2020 | Main

February 12, 2020

git shallow clone 的一个问题

最近在家办公,遇到的第一个问题就是家里网络极不稳定,无法 git clone 那些庞大的仓库。我知道 git 现在支持浅拷贝,用 --depth 1 就可以取出某个分支的 HEAD 指向的拷贝所依赖的所有文件。

但是,光用 git clone url --depth 1 还不能解决我的所有问题。因为需要传输的文件还是太大,不稳定的网络无法坚持到我顺利下载完成就断开了。而 git clone 得下载过程似乎不能续传,我只好另辟蹊径。

过去翻 git 文档时,曾经了解过有一个 git bundle 这个指令可以对需要传输的内容打包,然后大多数依赖接受数据的 git 指令都可以直接接收用 git bundle 打好的包,而不必通过网络传输。事实上,git clone 的过程就是在远程打好包,然后传输到本地,再解开的。

我想好的曲线救国的策略就是,找一台远程连接顺畅的虚拟主机,进行这样的步骤:

  1. git clone url --depth 1 在远程复制一份仓库的浅拷贝
  2. git bundle create bundlefile --all 打个包。
  3. 用支持续传的 sftp 下载这个 bundlefile 到本地。
  4. 在本地用 git clone bundlefile 还原。

但实际操作到第 4 步的时候,发现 git 报告 "fatal: Failed to traverse parents of commit xxxxxxxx" ,这个 xxxxxxxx 就是我那个仓库 HEAD 的 hash 。

我 google 了一下,找到 一篇 2015 年的帖子 ,谈的是同样的问题。

大意是说,当我们的仓库是一个 shallow clone 的时候,有一个 .git/shallow 文件指明了这点。但是 bundle 却没有打包进这个文件。也就是说,一个 bundle 无法说明自己是一个 shallow clone 。

根据我的理解,bundle 其实是 git 给仓库生成的 delta 信息,而非对整个仓库的复制。而 git clone 必须依赖 base 和 delta 才能还原仓库(不知道是否理解正确)。对于完整仓库的 bundle 是一个特例,我们可以从中还原出来,但是对于 shallow 仓库来说,缺少了 delta 就失败了。

鉴于这个帖子的讨论是 2015 年的,可能现在已经解决了。但我把本地的 git client 升级到了最新的 2.25.0 ,依然无法处理以上问题。

第一天时因为急用,我直接对远程仓库目录打了个包下载到本地。今天有点时间,又重新研究了一下。

既然这个 bundlefile 文件中的确包含有所有我需要的所有文件,那么终归是有办法的。

我尝试用 git init 创建了一个新仓库,然后在仓库下运行 git bundle unbundle bundlefile ,还原出所有仓库文件。这时的仓库是没有 HEAD 的,不过既然我们知道 HEAD hash ,可以自己用 git checkout hash 取出来。然后再用 git switch -c master 固定在主分支上就好了。所有文件都能顺利检出。

不过,接下来,我们运行 git log 会发现出错。再检查,的确是缺少 .git/shallow 文件,也就是说现在复制出来的这个仓库,git 还不知道是 shallow clone 。好在这个文件很简单,就是 HEAD 的 hash 。手工创建一个就好了。

到这一步,大功告成。

February 05, 2020

skynet 版的 cache server 改进

去年我实现的 Unity cache server 的替代品 已经逐渐在公司内部取代 Unity 官方的版本。反应还不错,性能,可维护性以及稳定性都超过官方版本。

最近疫情严重,公司安排所有人员在家办公,今天是开工第三天。前两天比较混乱,毕竟在家办公的决定是在假期中间做出的,并没有预先准备,我们的这个 cacheserver 也在第一天受到了极大的考验,暴露出一个问题,好在只是内存用量预警,并没有出任何差错。我花了一天多的时间排查和解决问题,感觉这是一个极好的案例,值得记录一下。

周一复工那一天,公司内网的 cache server 服务器突然内存使用量峰值超过了 20G ,接近硬件配置的 32G ,SA 预警。好在不久以后就回落,并稳定下来。

这个服务器是基于 skynet 编写的,一开始只有 200 行左右的一个单一 lua 文件,非常简单。我认为设计和实现都是清晰可靠的。设计的时候就考虑过大用户量大数据量的应用场景(我们在生产环境运行的这个服务器实际管理数据已经超过了 800G),最多会有几百用户的并发。重新实现的原因是因为原版占用内存太多,所以内存使用量本来就是设计的主要考量因素。

我没有采用一个连接一个 agent 的方案,而是使用了固定数量的 agent ,用简单的负载均衡方法把外部连接分摊进去。

另外,还加了一个几十行的 C 模块,用来绕过 lua 虚拟机直接发送大文件。因为,如果把文件读入 Lua 虚拟机后,会增加额外的内存以及 gc 的负担。skynet 提供了 C API ,直接从 C 层打开读取本地文件并从网络发送走更直接高效。

考虑到大数据文件可能很大,在 C 模块里,我还特地按 4K 一个数据块的处理,并没有把大文件一次读入内存。

结合实际情况,我猜测了内存峰值出现的原因:大量数据积压在 skynet 内部的待发送队列中。

这是 skynet 当初设计的一个权衡下的结果。skynet 的网络发送 api 被设计成非阻塞,原子性,且在连接没有断开时一定发送成功的。这样可以方面业务层使用。业务层不用像使用 posix 标准 send 那样,还要检查返回值,看真的发送了多少字节。

而 skynet 的大多数应用场景,重头在业务处理,而不是 IO 消耗,一般不会出现 IO 积压的情况。但 cacheserver 这个业务不同,是典型的重 IO 环境。虽然通讯协议是串行的,一个一个文件处理,但客户端却可以一次性提交很多文件请求,每个请求都只有几十个字节,但回应的文件却可以很大。加上发送 api 是非阻塞的,这样就会导致大量待发送的数据进入 skynet 的网络层。

为什么这半年一直没有出现问题呢?因为我们一直是在速度很快的内网使用这个服务器(也是官方推荐的使用方法)。而且使用是渐进的,不会突然请求很多文件。而这次,大量同事在家办公,使用 vpn 连入内网,连接速度很慢。在第一天,家中的开发环境是全新的,需要全量拉所有的资源数据。种种因素加起来,在高并发高数据量的情况下,问题就暴露了出来。

其实这个问题之前也有人遇到过,见这个 issue 。只是我虽然帮别人解决了需求,自己并未实践此类业务。解决方法并不复杂,使用 socket.warning 设置回调即可。当发送缓冲区过载,skynet 会发送一个 warning 消息给发送服务,告知缓冲区太长;这个时候业务应该停止发送数据(或进一步的测试一下带宽,限制流量),等到缓冲区清空(同样也会收到消息),再继续发送。

值得注意的是,如果加入 socket.warning 消息的监控,就不能再在 C 库函数中发送整个数据文件了。因为,如果文件过大,在发送的过程中,是没有机会处理新的消息,并挂起发送过程的。

我的修改方案是给 C 的发送函数增加了 offset 选项,并可以返回已经发送的字节数。当一次发送量超过一个阈值后,就中断返回。由 lua 调用方来决策是否应该挂起等待,或继续发送。最终添加了几十行代码就解决了这次的问题。


2 月 6 日补充:修改后的版本,这两天生产环境监控,内存峰值在 30M 以下。