背景

作为一篇总结&回忆文,还是先跟各位说一下背景吧。

我们今年的程序设计课大作业是写一个斗兽棋的AI。其实我在去年或者前年的时候就有写一个AI对战的网站的想法,只是没有这个机会。于是我听到大作业的题目之后,我就问助教可不可以这么搞,嗯,萌萌哒助教同意啦。

于是我就开始写这么个网站了。

写网站

这个网站的域名 p2dv.in 实际上跟大作业一点关系都没有。暑假的时候突然想到了这么个域名,意思是 Place to dive in ,有点仿 v2ex.comWay to explore ,然后看好便宜就买了下来。这次发现有这么个闲置的域名,于是就直接拿来用了。我觉得如果你知道它的意思的话,这个域名应该是很好记的。

其实这个网站要说复杂也很复杂,原因是你一个人需要同时负责若干个部分。首先是网站本体,你得把前端的 html+css+javascript 写了,然后后端和数据库也得写。然后是守护进程,守护进程需要轮询数据库把未评测的任务都评测了。守护进程还牵扯了助教给的大作业 server 端,要和助教的程序能配合紧密。最后是运维,因为我每次都以时间很赶为借口,所以 bug 还是蛮多的,因此服务器的运行维护就很重要。事实上,网站上线初期,动不动就挂掉了(当然我肯定是只能去改代码了)

不过我接任务的时候估计了下,应该一个星期出头能解决。做出这样估计的最主要原因是:如果超过两星期,我的文化课就要挂惨了。然后技术上的原因是,这些技术我大部分都熟悉,也就是有做过项目。不得不说之前做过的项目对我这次写网站帮助非常大。

  • 高二的时候给学校写了个 Online Judge System。所以我对这个网站的整体思路是非常清楚的:网站和 daemon 分离, daemon 定期轮询数据库,然后评测。
  • 高三寒假的时候给朋友写了个华北电力大学校内二手交易网站 http://ncepubbb.com ,所以实际上这次网站的整体架构都是从那拷贝过来的,这样子工作量一下子就减轻了很多。

我看了下我在 Github (https://github.com/abcdabcd987/p2dv.in)上的 commit history,大概我是从11月6号或者7号开始写,然后到11月13号就上线测试了,开始有一两个同学提交 AI。效率不是很高,但是还算说得过去。我记得有个周末一整天没有出过宿舍。

然后就一路上跟着同学们的反馈改bug、加功能。

我印象比较深的(记忆力差就是这样,明明就是一个月内事情,但是基本全忘了)就是,有时在12点过后改 bug 或者上新 feature 。然后我们宿舍12点断网,我就不得不跑到宿舍门外去连 CMCC 的 Wi-Fi。一手捧电脑,一手敲键盘,站一会儿就觉得好不舒服。最衰的是上次, CMCC 连了好久连不上,我只好手机开热点了。

BUGs and New Features

网站一开始其实 BUG 非常多,功能也非常简陋。

初期最多的 bug 是和助教的server端交互。因为助教是指定监听 12345 端口,而因为某些我仍然不知道的原因,似乎上一次的任务退出后,端口仍然被占用,于是下一次的评测就失败了。后来我就把助教的server端改了,改为随机端口,当然这也得改助教给大家的 C++ 框架,于是这就害的好多不明真相的同学在纠结这个 main.cpp 纠结了好久。

  • 有同学反馈,希望在 Demo 页面能够手动上一步下一步,好,我就加上去(虽然最后这个功能加的 BUG 非常多,而且我懒得改)
  • 有同学反馈,比赛进行的时候不知道运行的怎么样了。好,我就加一个比赛实时的步数显示,再加上一个类似于 Codeforces 那个跳动的小球。
  • 有同学反馈,希望能根据胜率排序。好,我就加上胜率。(虽然这个也是算的乱七八糟,而且我也懒得改)
  • 有同学反馈,不知道 Invalid Operation 具体是什么。好,我就连着助教的server端一起改,把非法操作的信息输出出来。
  • 有同学反馈,调试不方便。好,我就记录大家 stderr 上的调试信息,然后显示出来。
  • 有同学反馈,弄一个 Rating System 多好。好,我就弄了个和 Codeforces 类似的 Rating System。
  • 有同学反馈,有人卡评测机。好,我就改程序,在评测之前先检查当前两个AI时候已经有正在运行的战役,有的话就置后评测,优先评测别的战役。

其实我改助教的server端改了非常多次,发了若干个 pull request。然后我发现,虽然我一直想吐槽助教的代码,但是实际上,几乎所有的 BUG 都是我引入的。

有钱就是任性

到了比较后期的时候,大家刷 AI 的积极性十分高,一不小心就弄了一两页的 Pending 状态。我想了想,之前 Github Education Pack 送了我 DigitalOcean $100,以及黑色星期五我在 Linode 充 $25 送 $25 。有钱就是任性(反正也是送的,而且重点是持续时间短),于是我决定,多开几台服务器,同时评测。

一开始是主服务器在 Linode ,然后在 DigitalOcean 开了3台评测服务器。后来发现还是不够用,于是找 DigitalOcean 客服增加了我账户的服务器上限,又多开了4台评测服务器。就这样8台评测机的状态持续了一个星期左右。大作业 Deadline 前一天我又增开了2台。

于是最后的结果就是出现了我见过的最贵的账单,以及我从未见过的服务器豪华阵容。

技术细节

网站后端是 Node.js 写的,守护程序是 Python 2 写的,数据库是 MongoDB 。主服务器运行于 Linode VPS 上, 2GB 内存,双核 CPU。评测服务器运行于 DigitalOcean VPS 上, 1GB 内存,单核 CPU。

这次遇到的主要技术挑战有:

Demo 页面怎么做的问题。我的想法是,把棋盘弄成表格,然后用一个 position: absolutediv 作移动中的棋子,用 jQuery 的动画效果来做。然后由于我太懒,实在是不想用动画队列,于是就导致了我弄单步动画的时候 BUG 非常多。这又一次告诉我们,该重写重写,该重构重构,不然后患无穷啊。这个东西第一次写,感觉还是蛮好玩的。

助教的server端和我的守护程序的交互问题。由于需要实时步数反馈,所以助教的server端不能阻塞。于是我就把助教的server端改成多线程的,留下一个线程和我的守护程序交互。这是我第一次写多线程。

分布式评测的问题。我知道如果直接读数据库的话,很有可能同一个任务分配给了若干台评测机,这样会造成一些不必要的麻烦。想了好久之后,发现还是只能用一个带锁的分配器来解决。我最后是用 tornado 写了个 web 服务,在主服务器上跑,用来和评测服务器的守护程序交互,在分配任务的时候先上锁,保证每个评测任务都只被分配一次。其实我在纠结要不要加密传输,毕竟一个对数据库和文件系统可以直接修改的 web 服务直接暴露在互联网上不是很好。然后当时想了想,太麻烦了,而且一个多星期之后就结束了,于是就弄了个简单的 token 。一旦 token 对上了,就当做是自己人。

Rating System 的问题。数学不好,也没学过这一方面的知识。于是就只好随便把 Elo Rating System (http://en.wikipedia.org/wiki/Elo_rating_system) 拿过来用了。感觉可能是由于AI对战本身的限制吧,这个算法实际上并不是很理想,尽管这个算法广泛运用于各种游戏中,比如《英雄联盟》。(实际上我第一次看到这个算法并不是在 Codeforces 上,而是在电影《社交网络》上)

最后就是数据库的索引问题。有一天我突然发现主服务器上 mongodb 一直占了 100% 的CPU,感觉很奇怪,因为明明数据量不是很大,而且我记得我上了索引了。然后查了很久之后,才发现是我当时上索引的时候,觉得评测状态没必要上索引。事实上,分配评测任务的时候,就是要以这个字段作为限制,因此实际上是最有必要上索引的字段。果然一加上索引之后, load 立马降下来了。

总结

做这个网站的经历真的是很令我难忘。当你看到一群人都在用你的网站的时候,这是一种特别开心的感受,特别有成就感。还有这也是我第一次动用多台服务器,10台服务器啊!觉得好厉害的样子!

感觉有点像之前写 cofun.org 的感受。那次写OJ的时候,一星期内提交次数超过1000次,然后大家刷 Rank 什么的。这次也是这样的。

致谢

  • 感谢助教周耀达的支持与配合
  • 感谢柯嵩宇、万诚、杨润哲、刘志健、薛震东等同学的反馈
  • 感谢舍友们的支持
  • 感谢小云南让我知道了七牛CDN用不了的原因
  • 感谢杨国炜两年前帮我做了那个跳动的小球
  • 感谢同学们的捧场

统计数据

  • 注册用户: 74
  • 提交AI次数: 2355
  • 战役总数: 26869
  • 数据库占用空间: 3.18GB
  • 存在时间: 2014-11-13 to 2014-12-22
  • 合计费用: $39.17
  • 主服务器流量: 43.7 GB
  • http方面
    • 日志占用空间: 1.30GB
    • 总请求数: 5562831
    • 带宽: 949.40 MB

最后贴一点图吧

主页

用户列表

比赛列表

AI 列表

Rating Chart

Demo Page

某台评测服务器的CPU监测

主服务器的CPU监测

出BUG只能一台一台服务器找

DigitalOcean 的评测服务器列表

http日志统计