九、内存币——残酷的现实

  内存币矿池从想做,做出来,上线,一直到关门,总共不到 10 天时间,但回想起来比狗池其它所有的经历加起来更复杂。那十天无论是技术的踩坑、市场的折磨、还是团队内部的争执,都让人感到心累。我试着尽量说清楚那几天的事。

惊现大户

  故事的开头是充满惊奇的。12月24日下午5点,X 神突然给我们带来了内幕消息“大户问我们能不能做 MMC 的池”,因为这个币马上要上交易所了。“大户”?所有人同时冒出了黑人问号。虽说当时大家都忙着修矿池爆内存的问题,但是这条劲爆的消息还是吸引了所有人的注意力:

  • 大户是谁,在我们矿池挖吗?
  • X 神是怎么接触到大户的?
  • 大户是怎么知道 X 神在做矿池的?
  • 低调了两周最终还是没能隐藏身份么……

  一团团疑云笼罩在我们头上。

  在众神的催促下,X 神贴出了他与大户之间的聊天记录。原来 X 神在多天之前买币的时候第一次接触了大户。她凭借社交名媛的气质,吸引了大户的注意,而后又“不小心”透露了狗池是“学长”带着她一起做的,当然她自己完全就是打杂,什么都不懂……在获得了大户的信任之后,X 神晋升为金牌客服 X,我们也因此获得了和大户沟通的直接渠道。

  说起这个大户(大户 Q),在矿池的收入排名是第一名,外加第二名、第三名和第四名,一人承包了矿池几乎一半的收入。而他分四个账户来我们这里挖矿,并不是为了低调,而只是为了方便他的管理。因为他的算力来自于不同地区的网吧。

  大户 Q 和网吧老板合作,如果他在闲时挖矿,就给老板固定的分成。对于网吧老板而言,多花一些电费,就可以得到比电费高很多的合作费用。与此同时网吧的顾客几乎对此没有感知,因为用的是闲时CPU。

  我们看来,大户 Q 凭借那几万台弱机器,可以完虐那些管几十台E5 n核服务器的机房管理员。我们之前遇到的那次“DDoS”也是他的杰作。当然,后来这些弱机器也狠狠地坑了我们一把,暂且按下不表。

内存币

  回到正题上。虽说这还是我们第一次听说“MMC”这个币,但是上交易所这样的内幕消息,对我们来说确是求之不得。更重要的是,哪怕单凭大户 Q 的算力,也足够养活我们了。可惜的是,我们仅仅是意识到这是一个机会,却没有重视起来。技术宅们还沉迷于优化矿池的内存消耗问题。

  D 神用了一些碎片时间初步探索了一下 MMC,这个币全称是 Memory Coin,中文叫内存币,也是一个 CPU 币。内存币之所以只能用 CPU 挖,是因为它需要 1G 内存。计算前,它会先根据当前状态,创建一个 1G 内存的缓存,并且根据缓存里的数值,反复跳转几十次,最终挖到矿。如果用 GPU,给每个计算核心分配 1G 的显存几乎是不可想象的。当时主流的挖矿显卡,只有部分 A 卡的显存是上 G 的,但也只能调动起其中的一两个核心来挖矿,显卡的优势荡然无存。

  截至内幕消息那天,MMC 才发布 7 天,市面上还没出现矿池,留给我们巨大的想象空间。

  痛苦也就此开始。

  先吐槽一下 MMC,它就是一朵奇葩。比如它每出 20 个块,就会有一笔钱直接打给币的作者。于是我们必须要去适配这种特殊的策略,要不然挖到的块就没法被网络承认。当然这只是其中一个点,还有数不清的特殊实现等着我们一个个去适配……

  而我们遇到最大的技术障碍,是矿机的问题。我们刚开始尝试的第 1 天,由于市面上还没 MMC 矿池,所以也没有 MMC 的矿机,我们需要根据钱包代码自制一个矿机。结果到了第 2 天,当我们刚折腾出一些眉目的时候,第一个 MMC 矿池出现了,矿机也有了。我们又转去适配这个矿机……这个矿机各种不靠谱,稍微跑一会就会 core,还得配一个自动重启的脚本。吐槽归吐槽,最麻烦的是这个矿机的协议和之前质数币的完全不一样,甚至不是用长连接的,而是不停地发 HTTP 请求去轮询……经过半天的折腾之后我们放弃了适配,回来自己写矿机。

  多说一句这个第一个 MMC 矿池,一定是快糙猛的杰作,因为矿池上线几小时之后就关掉了。主页上写的是:用户数超过了 1000 人,遇到了我们“早就预料到的”性能问题,所以只能关了……看到后我们心中窃喜,看来我们双核机器支撑十万连接,还是挺有技术门槛的。

  到了第 3 天的上午,第二个 MMC 矿池:mmcpool 出现了(这个矿池的名字就叫 mmcpool)。短短 5 小时,这个矿池占领了 90% 的全网算力,速度惊人。我们估算了一下他的现金流速度,心态崩了。终于把所有精力都集中到赶制 MMC 矿池中来,誓要今晚发布。可惜的是,这个矿池的矿机我们也没法很快适配,再加上这个矿机也有很多设计不合理的地方,比如在切换块的时候会浪费很多算力(大约10秒)。最后我们还是继续开发自己的矿机。

  第 4 天,D 神全力开发矿机。mmcpool 已经占了99%的市场。在这样的环境下,我们的战略不得不定位成:必须要一次成功。

  第 5 天傍晚,我们的矿机经过一天的测试,可以发布了。凭借狗池在市场里口碑,发布之后很快获得了一些算力,不过也就只有这一些了。估算了一下,理论上需要 4 小时可以出块。然而……4小时过去了,什么都没发生。80% 的矿工失去了信心,去了别的矿池,这下出块更难了。

  团队内部也开始起了争执,分为了“一定有bug派”和“一定是人品不好派”。在数小时中,有bug派持续输出,一边审查代码,一边对人品不好派提供的各种证据提出尖锐的质疑。大家看了代码,推了公式,写了模拟代码,争论了两小时,最终达成了一致,真的就是人品不好。

  终于在快到 24 小时的时候,出块了!其实这时,按照期望应该出了两个块,如果用泊松分布估算,我们的运气属于全宇宙最差的15%……看着钱包里这份微薄的收入,大家泪流满面,五味杂陈,只能感叹一下代码没有bug。

  心累。

大户加入

  说回大户,当初我们开 MMC 矿池,就是因为是大户 Q 的建议。然而为什么我们发布了一天多,还不见大户来捧场呢?

  其实在做内存币之前,我们就意识到了,大户 Q 的主要机器都是低端机器,而且是网吧的空闲计算资源,留给挖矿的内存就不多了。但我们没想到的是,他那几万台机器绝大多数只能保证有 256M 内存,如果需要1G内存,可能就只能拿得出几十台……

  内存不够怎么办?群神都是做算法出身的,大家不约而同地想到了用时间换空间的方法,经过 D 神一天的实现和优化,最后在 256M 内存挖矿,大约也可以达到 1G 内存的 ⅓ 性能。

  定制没有白费。第 7 天的傍晚,金牌客服 X 带来了大户的消息:“大户先开几十台 1G 内存的 E3,然后几千台 512M 一线程,最后几万台都连上,把其他池都干掉。。。”这些豪言壮语听着开心,但是我们心里都在打鼓,扣除了 ⅔ 的性能之后,对大户来说挖 MMC 还划算么,性价比不高的话,或许最后只能“帮个忙”。

  大户的几千台机器进来,立竿见影,没几分钟,矿池就出了一个块。恰好在此时,mmcpool 挂了,借着“双喜临门”的势头,狗池一下子吸引到了大量的矿工,我们的双核机器也赶紧升级到了8核,确保服务质量。可惜的是,mmcpool 一恢复,好多矿工又跑了回去。

  说实话,mmcpool 的技术肯定是比我们差很多的,比如前面提到的,他们的矿机会浪费10几秒的算力,而且 reject 率很高,也就是矿工的无效计算非常多。所以比起来,在我们这挖矿,要比他们的收益高30%以上。照理说,矿工是非常趋利的,实际上呢,mmcpool 的头部效应太明显了,矿工们普遍会倾向于去出块多的矿池挖矿,毕竟跟着大多数人走,比较保险。R 神叹息道:“印证了互联网产品重要的不是技术”。

  大户最后确实没有拿他的主力机器来挖 MMC,不过在他象征性的帮助之下,狗池的算力稳定在了全网算力的 20%到30%之间,保持了两天。这两天我们也没闲着,比如把矿池做成了动态 share 难度的,这点对于矿池的稳定性非常重要。因为 mmcpool 经常会挂掉,我们时不时会因此得到大量的短时算力,可能在半小时内,我们的算力就会翻倍,这也意味着矿池的 CPU 消耗也要翻倍。这套策略,会在算力增大的时候,以合适的速度提高 share 难度,反之亦然。我们设计的这套策略在当时似乎非常先进,几乎没有其它竞品在用,当然现在的矿池基已经标配这个了。不过这种锦上添花的东西矿工不太有感知,更多的只是降低我们的运维成本。

  除了矿池的优化,我们还对矿机做了很多优化,技术上,适配了各种古老的操作系统,做了各种编译优化。用户体验上,我们把运行脚本一起打包进了压缩包里。因为我们发现很多人会改 bat 脚本,却不会自己创建一个新的 bat 脚本,所以光写在文档里还不够……

  其实大户加入后的第二天,还发生了一件事,有个新的 MMC 矿池出现了,号称支持用 GPU 挖矿。我不想把它的名字写出来,就叫他“GPU 池”吧。GPU 池也有大户加持,刚上线就占了半壁江山。GPU 池似乎有点黑,我们实测下来收益不到狗池的一半,仔细分析来看,不像是他技术不行,而更像是算力被他黑走了。

DDoS

  经过一周的折腾,MMC 矿池总算走上了正轨,正当我们想歇一口气的时候,又一个杯具发生了。早上 7 点,金牌客服 X 突然在群里着急地说,矿池连不上了!

矿池在线算力为0

  口碑不能砸,D 神赶紧到电脑前排查,一看,DigitalOcean 发了一封站内信,说我们的 IP 被人 DDoS 了,所以他们就把那个 IP 封了。申请解封之后不到 10 秒,系统又自动把这个 IP 封了。我们装了抓包软件,看看到底是什么样的攻击,又申请了一次解封,在那上线的几秒钟,我们看到了 1Gbits/s 的巨大流量(也不知道是不是网卡被打满了)。之后分析发现,是一种叫做 UDP DNS Query Flood 的攻击。虽然我们的服务器配置完全可以抵挡这样的攻击,但对于机房来说,这么大的流量会影响他的正常服务,就选择把我们关掉了。我们后来怎么申请,DigitalOcean 都不肯给我们解封了,只祝福我们“I hope you have better luck in the future”。

DDoS 当时用 nload 命令看到的效果

  这个攻击者一看就是圈内人士,他只攻击了我们用来挖矿的 IP,而完全没去动我们 web 服务的 IP,一点资源都没有浪费。与此同时,mmcpool 也正在被攻击,只有那个 GPU 池还活着。把一个币交给一个黑池真是让人心痛啊。

  我们调研了几小时,发现要防 DDoS 对我们这种做小本生意的人来说是无解的。如果套一层 CDN,矿机的长连接就很难保持。如果选那些防 DDoS 的机房,成本又远超我们的收益。当时我们能找到的防 DDoS 产品,每月要付大几千,而我们那两天从 MMC 中得到的收入只有每天 300 元,况且这个币还不知道能活多久……

  最后我们把矿池公告改成了“矿池正在被 DDoS 攻击,欢迎选择竞争对手的产品”,直接放弃了这个市场。虽然心有不甘,但更多的是如释重负。至少我们还有 DTC 矿池,给我们带来稳定的收入。大家也开始自嘲,被竞争对手认可了,好开心;狗池让我见识到了 DDoS,这辈子值了。

  金牌客服 X 的一句话打断了群里的自嘲:不如先弄个私池,让大户 solo。

八、性能危机——CPU、内存、磁盘,成本啊……

CPU危机

其实这个 CPU 危机,就是上一章里挖的坑。矿池经过几天的发展,四核的机器也几乎已经满了,特别到了周末,挖矿的人也开始变多。必须要想办法优化了。

优化的过程充满了戏剧性,H 神抱着试一试地心理,搜了一下,发现 Go 语言可以调用 C 语言写的 so 模块。然后 H 神又抱着试一试的心理,试了一下两个 Go 和 C 版本 share 检查程序的速度,发现居然 C 比 Go 快 10 倍!于是这个问题就这么解决了,此时距离矿池上线刚刚一周……(要是一开始就知道可以 Go 调用 C 就好了,也免去了 H 神花了好几天把 C 的代码翻译成 Go 的,现在又要亲手把它废掉……)

那些年 Go 语言刚出来,宣传里面有提到说它是编译语言,性能非常好,堪比 C。大家听后都觉得顺理成章,都没想着怀疑一下,结果在我们这个计算密集的程序里,居然有 10 倍的性能差距。

新版上线之后,CPU 使用率有了非常显著的降低,从此我们再也没担心过 CPU 的成本。有趣的是,就在我们更新新版之后的第二天,矿池的用户又开始猛增。要是没有 H 神的及时优化,CPU 占用应该已经到了红圈的位置了。


cpu
优化前后 CPU 使用率

内存危机

内存问题其实早在矿池上线第一天就出现了,一直在优化,从来没解决。CPU 的瓶颈解决之后,内存更是直接站在了瓶颈的位置上。DigitalOcean 上,CPU 和内存是搭配出售的,如果我选双核,那就只能搭配 2G 或者 4G 的内存;如果选四核,那就只能搭配 8G 的内存。现在 CPU 用四核绰绰有余,但是 8G 内存还是非常吃紧。还是直接看图吧。


memory
优化前内存使用率

可以看出内存的增长速度非常可怕,大概半天时间就会内存溢出,导致矿池挂掉。图里面的每次内存直线下降,都是因为重启了矿池,可能是因为挂掉重启或者升级策略主动重启。

照理来说,重启前后的内存消耗应该是相当的,可能少数用户会因为重启矿池而放弃重连,大部分用户还是会连回来的。但是图片里的这个直线下降的效果可不是这么说的,重启前后的内存差异非常的大。这只有一种可能,内存泄露了。连接矿池的矿机可能处于各种不同的网络环境下,断线是常有的事情,所以矿机一般都有断线自动重连的功能。再加上有些用户只在开机的时候顺便挖个矿,“翻台率”其实非常高。要是之前的内存不能及时释放,那最后肯定就是图中的效果了。

其实Go语言是自带内存回收机制的,当时的版本是 Go 1.2,传说这个版本的内存回收还不太成熟。我们自己体验到的就和传说中的一样。还有传说 Go 1.3 会改好 GC 问题(据R神介绍,实际上到了 1.5 版才算改好了),不过估计还得等大半年吧,可能等不到那一天矿池就倒闭了。那剩下的路子似乎只能是少申请内存:申请次数要少,能公用的内存就公用;申请的量也要少,这样至少可以减缓增速,多活几天。

优化的过程当然是非常的苦逼,自己挖的坑就得自己填,当初快糙猛写的代码,内存用的很随意,现在就一个个改吧。大多数还是非常好改的。除了直接改内存,我们还尽可能缩减“go”关键词的使用,每一次“go”都会有一些开销,积少成多。最早我们为了效率,把逻辑上可以并发的所有操作全都 go 了,经过这次精简,每个矿机只保留两个协程,一个等新块(与主程序通信),一个等 share(与矿机通信)。

经过两天的改进,内存终于稳定下来了,基本可以持续运行了。

硬盘危机

硬盘危机一开始还真的没想到,像我这种有数据完整性强迫症的人,特别不喜欢删数据,终于在矿池运行三周之后,硬盘容量告警了。实际上最占硬盘的只有两部分,一个是日志,一个是数据库。日志其实就是存储了每个用户的 IP,通信交互等情况,用于紧急的时候排查问题;数据库存的是所有的账单信息,每个人为什么这么发钱,根据什么算的。

随着时间的积累,这两部分数据都越来越大。当然我们也一拖再拖,一直拖到 1月12日,如果这天再不备份数据库,第二天连备份数据库的空间也没了……

其实硬盘危机相比前面两个还是很好解决的,日志就打包之后就传到另一台备份机上。数据库每天凌晨定期热备,然后只保留一周的流水,和当前的状态数据,剩下的数据全都直接删掉。备份文件同样也是压缩传到备份机上。这个备份机其实也是 VPS,只不过是内存少硬盘大而已。我们选了一台 512G 硬盘的备份机,可以用很久了。相比之下矿池的 Web 节点和矿池服务节点都只有 30G 硬盘。

当然,做这三个优化其实都是因为成本问题。我们在做优化的时候,土豪 F 神一边围观我们优化,一边幽幽地说“升级机器”。土豪的视角还是很先进的,我们其他人只关注了硬件成本,而没有关注人力或者时间成本。特别当我们在做内存优化的时候,我们就已经错过了内存币矿池的先手机会。

七、幸福的烦恼

  还没睡到 3 小时,就被 D 神的电话叫醒:矿池挂了……两人一通排查,认定是使用人数太多导致了内存溢出。不用多说,既然内存不足,就升级机器吧。当初我们选择 DigitalOcean 就是因为它支持快速升级,万一配置不够,点点鼠标,几秒钟就可以升级到更高的配置。重新打开矿池,一切指标都正常,不过我也已经进入了兴奋状态,睡不着了。

  我们一边庆幸狗池选在周六发布,这样周日不管出现什么事情都可以及时修复。另一方面也在考虑,有没有什么方法可以及时找出异常,最好在矿池挂掉之前就能发现苗头,及时解决问题。这时,沉默已久的M神突然出现,秒杀了这个需求。M 神有着丰富的运维经验,他手上的服务器多得需要用两位字母加两位数字编号来命名。M 神为几台矿池服务器安装配置了 Munin,这是一个可以在 Web 端一目了然看到 CPU、内存、硬盘、网络等重要系统指标的监视工具。这款工具还可以方便地配置告警条件,比如硬盘快满的时候会提醒我们。这给我们之后及时发现“幸福的烦恼”提供了大量的便利。

分钱 Bug


矿工账单页面插图
矿工账单页面插图

  看着 X 神画的 gopher 开心分钱的图,心情也是非常的舒畅,但在矿池发布的第二天下午,我们的心情却是非常紧张的。凌晨我们已经见识到了狗池在连续出两个块的时候分钱会有问题,现在随着用户越来越多,出块也越来越快,这个 bug 就发生地更频繁了。这还好,一开始多分了钱,后面就少分点,还是可以慢慢纠正回来的,但是另一个 bug 就更要命了:我们发现,当两个用户同时出块的时候,比特币钱包会告诉我这两个块都成功了……这下就麻烦了,我们以为发出去的钱,其实并没有发出去,后面算的钱就全都错了。一旦有人发现收到的金额不对,就会怀疑狗池在黑他们的钱,这可了得,口碑坏了就什么都没了。

  保证分钱的正确性是狗池的首要目标,我们这帮技术宅什么都没,只能靠着目前看来还不错的口碑让狗池继续壮大。第一个 bug 经过 R 神周日加班终于搞定了;第二个 bug 在我看来是比特币钱包留下的坑,既然钱包里不加锁,只能在矿池里加了,结果我加了一堆锁,总算是把这个问题绕过去了,这时的代码已经丑得我不想再改了……几天后,D 神提出了一个完美的无锁方案,彻底解决了这个问题,当然这是后话了。

新版上线

  上线一天,狗池的代码还充满了 bug,修修补补免不了要重新发布新版本。作为一个网络服务,发布新版本有着与生俱来的优势:只要在服务器上更新一下就可以了。但是作为一个矿池,上线新版本却又不像网站更新这么容易。首先,我们得保证分钱不能错,share 数据都存在内存里,需要及时备份到硬盘,已经分配的钱需要确认写到数据库里了。其次,重启时间要尽可能快,时间就是金钱,停 1 分钟可能会少赚几毛钱,但是如果停了 10 分钟,可能有些矿工就会跑去别的矿池了,所以这里一定要争分夺秒。

  我们最终采用了一种低成本的土办法来上线新版矿池。首先 touch exitpool 发出退出指令,当矿池发现存在“exitpool”这个文件时,就会开始备份各种数据。与此同时,我们人肉观察什么时候矿池结束运行,一旦发现停了,就马上运行新版。新版跑起来之后就要观察指标,万一指标异常,要马上回滚运行之前的版本。回滚命令和运行新版的命令在操作之前都会提前写好,到时候只要直接粘贴就可以了。其实这套方案可以用更自动化的脚本来完成,不过起初我们担心会出错,就在人工监视下一步步执行,后来 bug 少了,矿池也很少更新了,一直没人去写这个脚本,所以直到最后,我们还是这么纯手工地重启矿池。

“DDoS”

  那是一个风和日丽的周一下午,狗池经过昨天的调教,已经稳定运行了大半天了。开完了组会,我回到电脑前,看到群里已经有了 N 条未读消息,就冒了个泡“长求总”,紧接着群里就开始刷屏“报告包工头,我们被 DDoS 了”。

  其实对于 DDoS,我们早有耳闻,论坛上经常看人提到说某某矿池今天被人 DDoS 了,也有一些矿池主打就是抗 DDoS,用着省心。在做矿池的时候我们也考虑过防 DDoS,不过想着这怎么也得两个月之后才需要考虑吧,怎么这不到两天就……

  说回这次 DDoS,其实我们之前都没见过,为什么这么快就认定这是被攻击了呢?回想起下午 1 点多的时候,矿池在线的矿工还只有 1000 人,没想到在之后短短的一小时里,矿工数猛增了 10000 多人,直接导致 CPU 满了。奇怪的是,虽然矿工数多了十倍,但是每分钟 share 数却不见怎么增加。D 神查看日志发现,新进矿池的这 10000 多个“矿工”行为非常异常,一方面大量反复重连,另一方面又几乎不参与计算,因此认定这绝对是被攻击了。

  虽说 DDoS 是分布式攻击,但是经过统计发现,这次攻击的 IP 比较集中,D 神、R 神、S 神经过讨论决定,先封杀连接数最多的 100 个
IP。当然,封杀 IP 这么高大上的功能,是不会存在于我们这么快糙猛做出来的矿池里了。好在 Linux 下,可以简单地配置 iptables 直接封杀来自某些 IP 的所有连接。就这样,经过 D 神的一番操作,矿工数降到了 7000。攻击者似乎也意识到了 D
神的封杀,放慢了攻击脚步。此时是下午 4 点,距离发现攻击刚过一小时。

  但是这种手工封杀的方式毕竟不是长久之策,最好矿池就有识别恶意连接的功能,直接封杀那批伪矿工。于是我开始仔细分析这些伪矿工的行为。又一番统计之后,我惊奇地发现,那些伪矿工其实也在计算,只不过计算性能非常非常的低。之前见到的矿工,大多是职业矿工,他们用 8 核或是 32 核的服务器来挖矿,也有少量矿工可能是用自己的双核、四核电脑在挖。但这批新矿工,不仅在用单核挖矿,而且挖矿效率低的惊人,基本只有普通单核的四分之一……

  那么问题来了,我们要怎么对待这些低效的矿工呢?封杀似乎不一定合理,说不定人家真的就是拿着大量的烂机器,想好好挖矿呢……而且对于我们矿池来说,再弱的矿工也是能给我们带来收益的,我们更应该好好对待他们啊。

  既然暂时选择了要好好对待每一个矿工,那只能在自己身上想办法解决问题了。现在的瓶颈在 CPU 上,那么就只有两条出路了,要不优化程序,要不升级机器。回想起昨天才刚升级了机器,现在就要升级了,这还没怎么赚钱,成本就越来越高了,会不会倒闭啊……

  我们还是先优化一些已知的问题吧。比如之前为了调试,打印了大量日志,现在是时候砍掉大部分了。经过一番简单地优化,上线一看,确实 CPU 使用率下降了,但是下降地非常微弱(图中箭头处)。


CPU占用率
服务器 CPU 使用率变化曲线图(可以看到 CPU 占用在 2 点多开始突然上升)

  实际上最方便的找瓶颈的方法是使用性能分析工具。Go 语言也有(pprof),但是似乎不太给力,我们在线上环境测试了一次,分析报告等了一小时还没出来。但是如果线下测试,由于计算量太小,又找不到瓶颈在哪。所以最后还是只能回到人肉分析。

  于是我们又一个个模块地分析,在分析到 share 验证模块的时候,我突然想起 H 神写完这个模块的时候提了一句,“每次计算需要 70ms”。现在的 share 数量大约是每分钟 2000 个,也就是说,每秒的计算量是 0.07*2000/60 = 2.3 CPU 秒,一秒钟有两秒多的计算需求,这已经超出了双核 CPU 的极限。看来瓶颈就是在这了,但是这部分计算是必不可少的,而且验证 share 的算法也是非常成熟的,我们也不可能一时半会把它优化下来。哎,还是花钱求平安吧,继续升级服务器。

  晚上 8 点,在大家十万个不情愿之下,终于还是把机器升级到了四核,CPU 安静地飘在 75% 的位置上,连接数居然到了接近 2 万;挖矿速度也有了巨大的提升,矿池的算力已经接近数据币全网算力的 50%。看起来之前矿池在双核机器上跑是被严重地压抑了性能。

  矿池终于回到了正常状态,可以暂时歇一口气了。接下来的主要任务是优化程序的性能,避免成本一直上涨。但是在此之前,我们还是很想八卦一下这接近两万台机器,到底是谁的。可以肯定的是,这些机器挖到的矿都是给同一个钱包的,说明这些机器都是一个人的。但是谁会有这么多机器呢?我们想来想去觉得只有黑客手中的肉鸡,才会有这么大的规模。但是从 IP 分布来看,这些机器都集中在几个城市,网段也很接近,像是几个网吧的。不过网吧一般都有还原卡,不太容易中木马……真是越想越绕,不知道到底是何方神圣。还是随他去吧,能让我们赚钱就行。

六、今晚上线——人品、人品和水军

  开发过程看似持续了两周,其实最后几天都在集中测试。最难测试的就是挖到区块之后的提交功能。当时我们所有参与的人都开着机器跑挖矿程序,就算这样,也要一整天时间才能挖到一个块,获得一次珍贵的提交机会。在修复了若干 bug 之后,终于迎来了“今晚上线”的这一天。

  其实这里“上线”更确切地说应该是公开矿池的地址,吸引矿工来挖矿。毕竟要真的说上线的话,矿池已经在线跑了好几天了,还在我们这么多“矿工”的努力下,正常出了两个块。

  对于这次“上线”,大家心里都没底,毕竟我们这帮技术宅完全没有搞营销的经验,在矿工圈里也没有什么熟人。看来只能硬着头皮小心翼翼地贴小广告了。

  首先拟了个发布稿,贴在当时最“官方”的 bitcointalk 论坛(山寨币的发布,都在这个论坛),开头是“我做了个新的 DTC 矿池,欢迎大家来测试……”。其实第一版稿件里用的全是“我们”,后来一方面为了装 B,另一方面为了给矿工一种好说话的感觉,把稿子里所有的“我们”都改成了“我”。这个规矩一直严格执行,直到矿池倒闭。

  等论坛帖子发布 10 多分钟后,我那个潜伏在数据币矿工群的小号终于发挥用处了,发出了这条关键信息。

小白水晶 2013-12-14 22:13:18
好像有新的矿池了?不知道靠谱不,论坛里贴的 https://bitcointalk.org/index.php?topic=370920.0

  这种弱弱的语气也是我们一群人讨论了很久之后的产物。这条消息之后,原本活跃的群突然就安静了……诡异的安静让人有些不安。要知道那时候的矿池,还没网页,只有帖子里的这几句话。要是就这么被认作黑池而没人上钩,我们就要白干一场了。五分钟后,群又恢复到之前热闹的状态了,好几个矿工表示准备试试。

  看后台数据,连上矿池的人在缓慢地增长,发布之初,还是我们自己人的 10 台机器,慢慢就变成了 20、50……大概经过了一小时,已经有 100 台机器连上了矿池。突然,出块了!一阵兴奋之余,我们赶紧分头检查发放的工资、数据库、日志,看看有没有异常现象。一切都好,这才舒了一口气。

  矿工收到了工资,就开始在群里帮我们宣传“一小时挖两个币可以哦,大家快上”。我们其实每个人都已经加了群,虽然看着很激动,但也不敢乱说话,要是被认出来就不好了。这时候就只能靠群里“自干五”们的支持了。分析后台数据,排名第一的矿工用了 14 台 8 核电脑在挖矿,比起他来,我们自己的算力加起来也只是他的零头啊。

  又出块了!这次不是我们看后台数据发现的,而是矿工率先在群里说的。这下群里炸了锅“趁池子还没黑,快去挖”。看了真是哭笑不得啊。连接数 200、300 涨的飞快,远超我们的预期。又一个块!这短短的一个小时,出了三个块,小心脏受不了了……心跳正常之后,仔细算了算,按照期望,这时应该能出一到两个块,出了三个块纯属人品爆发。


blocks

挖矿出块页面插图

  接下来的一小时,我们感受到了什么是人品守恒。出来混,总是要还的。矿工的数量还在增长,但是却一直不出块了。有矿工开始破口大骂,我们只能仔细地排查错误,但是最终的结论是,运气太差。连接数开始缓慢下降,难道矿池才开业两小时就要准备关门了?

  为了缓和紧张的氛围,我们手动统计了所有矿工的欠款,跟帖贴在了发布稿的后面。这一招似乎非常有效,矿工看到池主说还欠自己钱,就暂时相信矿池还没跑路,愿意继续挖着试试了。群里有个矿工看到了这个帖子,猜测是不是池主在手动发钱才这么慢的。潜伏在矿工群里的众神瞬间化身为水军,顺势把舆论导向了这个有趣的中性话题上。

  终于在这个难熬的一小时快要结束的时候,一下子出了两个块,终于可以安心了。这时,R 神也写完了一个显示欠款页面,放出来大家一起检查。所有人同时发现了一个奇怪的现象,怎么有些人的欠款是负数,也就是说,给这些人多发了钱……这是要把自己弄倒闭的节奏啊……

  由于打印了大量的日志,排查起来非常方便。20 分钟之后,终于发现是数据同步的问题:我们最初设计的时候,为了图方便,把“不需要这么实时”的 share 统计数据,通过数据库来传递,反正最后也要入库。没想到在连续出块的时候,数据库还没来得及写入新数据,就接到了读取请求,这就导致矿池用旧的分钱方案又多分了一次钱。还好这个错误只会在连续出块的时候遇到,应该不严重,这真是个幸福的烦恼啊。此时已是凌晨两点,然而大家都非常兴奋,丝毫没有睡意,于是继续讨论怎么修复这个 bug。

  凌晨三点,方案落定,最佳解决方法应该是把关键数据都放到内存里,但是这得明天才能修完了。鉴于矿池运行稳定,大家都准备洗洗睡了。突然,矿池挂了……日志说,是同时打开文件数太多导致的。原来 Ubuntu 默认只能同时打开 1024 个文件(网络连接也算在内),现在已经有 400 多个矿工,再加上一些没释放的资源,已经达到了这个上限。问题倒是很好解决,改一下系统参数就好了。能在醒着的时候遇到这个错误真是幸运。

  到凌晨4点,终于能真的平静下来洗洗睡了。

五、包工头

  到 12 月 1 日,我们已经大致搞明白了比特币的原理,也知道矿池要做的事情,我手痒先设计了一个能用的矿池架构。这个架构主要有四个部分:钱包通信、矿机通信、主逻辑和 Web 界面。这些部分的功能是这样的(请跳过以下无聊的四段):

  钱包通信,顾名思义就是和钱包客户端通信了。比特币是在 P2P 网络上建立的,而其钱包客户端就是其中的一个普通节点,也就是说,这个客户端有着完整的功能。比如转账,比如宣布挖矿成功。我们也能从钱包客户端里面读取到当前虚拟币网络的任何信息。

  矿机通信,顾名思义就是和挖矿程序通信了。功能很单纯,有新的挖矿任务时,就给矿机发放任务;矿机挖到 share 时(下面会介绍什么是 share),就解析数据,并拉去检查提交。

  主逻辑,顾名思义就是主逻辑了。这个模块最初设计的时候是为了打通各模块之间的数据流,确保有新任务的时候可以及时通知到所有矿工。比如矿工算出新块了,要第一时间告诉钱包,及时同步到虚拟币网络。主逻辑的设计思想是尽可能抽象和简洁,随着后续的优化,主循环里面一共只有 20 多行核心代码。

  Web 界面,顾名思义就是个网页了。为了让矿工相信我们不是黑池,得把所有挖到的矿以及每个矿工的收入公开透明地展示出来。这个模块非常独立,上线之后才开始做。


architecture
最早规划的模块结构图(后期经过优化已经有所改变)

  我给自己封了个包工头的职位(史上亲自干活最多的包工头,本章标题就是为了纪念这个唯一出场机会的),由于整个设计思路数我最清晰,所以各个模块之间的协调由我来定。具体在开发过程中,每部分又可以继续拆分出一些独立模块,最后分出了八个独立模块大家一起来写。我写主逻辑以及矿机交互;D 神写分钱机制以及钱包通信;H 神写 share 验证;R 神写数据库接口、share 统计、Web。四个人,两周的业余时间基本都在开发矿池了。

  其实嘛,对于码农来说,编写矿池这几天完全就是“日常”,最没什么可以回忆了。基本都是,提出方案,讨论优化,然后敲代码,测试。不过,有些细节设计还是值得分享一下的。

狗池

  都写到第五章了,还是没说狗池是什么,现在终于要揭秘了:狗池就是我们做的矿池的名字。“池”肯定就是矿池的意思了,那为什么叫“狗”呢?我们在选择编程语言的时候,R 神和 F 神同时建议使用 Go 语言,可以应对“百万级”矿工的连接。D 神:“于是就叫 GoPool,简称狗池。”R 神:“贱名好养活。”于是名字就这么定了……不过后来因为 gopool.net 被注册了,我们只好抢了个 gpool.net,中文名还是不变。

  顺便插播一段漫画调节一下枯燥的氛围。Go 语言的吉祥物其实是 gopher,就是下面左边这个很萌的形象,后来 X 神为 Web 界面上每个功能都设计了一个对应的 gopher 风格的插图,后面不定期插播这套插图,先放一个首页的插图。


gopher首页插图
gopher以及矿池首页插图

share

  前面提到了 share,这是一个在矿池挖矿特有的概念。之前提过,矿池的设计目的就是让小算力的矿工能按照他们出的力分钱,从而得到持续稳定的小额收入。那怎么评估每个矿工出的力呢?其实思路很简单,就是让矿工做一些低难度的“挖矿”任务。比如比特币需要矿工找到一些前面有 67 个 0 的 hash 值,那么矿池可以降低难度到 50 个 0,如果矿工找到一个至少有 50 个 0 的方案,就可以提交,这个难度就小很多了。矿池如果在里面发现有 67 个 0 的,就可以提交给比特币网络了。质数币也类似,只是不以 hash 前面的 0 为难度衡量指标了,而以对应质数序列的长度为目标。质数币网络需要我们找到长度为 10 的序列,矿池就把难度降低到 6。这种矿工提交的低难度方案,就是 share。share 里面可能会有难度符合虚拟币网络要求的方案,就可以变成区块。

  这里还有个有趣的地方。矿工会不会在算到低难度的 share 时,就提交到矿池蹭分红,而在算到难度满足区块的要求时,直接擅自提交独占成果呢?其实这是不行的。还记得最早提过的挖矿其实在计算 hash(“基准字符串”+“随机字符串”)么,里面的基准字符串已经编码了:这个矿挖到之后是分给谁的。所以对于矿工而言,要么参加分红,要么自己挖矿,没法两者兼得。

分钱

  虚拟币矿池的分钱策略有两种现成的方案,PPLNS 和 PPS,具体可以看这个介绍。我们最后考虑到编程实现上的便利,改造了 PPLNS 策略,按照最近一小时的 share 数进行分配。对于矿工而言,PPLNS 手续费更低,只要收入稳定,一般都会选手续费低的。

  对于矿池而言,分钱还需要考虑另一个问题,那就是在哪分钱。这是什么意思呢?在虚拟币世界里有两种方式可以得到钱,一种是挖矿获得,另一种是别人转账过来。前面提到“基准字符串”里面编码了挖矿成功之后钱分给谁。如果是第一种方式,可以直接按照之前矿工的算力投入编写分钱方案,挖到矿时直接分给矿工。如果是第二种方式,则挖矿的收入全给矿池,之后通过转账的方式,把矿池里的钱转给矿工。

  这两种方案其实各有优点,前一种矿工可以及时看到收益;后一种按需取款,避免钱包都是碎银子……可能有人会觉得虚拟币钱包里的钱不就是个数字么,多笔小额的收入有什么关系。其实还真有关系,虚拟币用钱(给别人转账)的时候,需要说清楚,这笔支出的钱,分别是从哪几笔收入里来的。当然这些工作都是钱包客户端自动完成的。不过大量的碎银子会拖慢钱包的速度,甚至有可能导致转账数据包太大,而需要缴纳额外的手续费。

  我们最后采用的是前一种方案,因为该方案挖到的矿都直接分给矿工了,而矿池就不会存有大量虚拟币,也就不会成为黑客的攻击目标了。(后一种方案钱全在矿池,可以携款潜逃 23333)

  简单总结一下整个开发过程中的一些想法,产品设计上,我们第一目标是降低自己的风险,第二目标是为矿工提供稳定的收入。技术设计上,目标就是低成本(代码量和服务器成本),高性能。虽然这个目标不一定能泛化,但是在当时的情形下应该还是比较适合的。

p.s. 作为一个包工头,每天的 push 是必不可少的,那几天我的口头禅是“今晚上线”。然而这并没什么卵用。

四、说干就干——YY 变成现实的重要一步

  经过 D 神连续 4 天的洗脑,大家都已经相信了做矿池是一个发家致富的好路子。但是为什么没人动手做呢?当时没人能搞明白矿池是怎么回事,甚至连比特币的原理也不清楚。这种情况下,从零开始做矿池,技术门槛实在太高。既然我已经率先被洗脑成功,就一定要把这件事干成。

  第一步,拉人。土豪 F 神建了个群,把我们有意向参与开发的人加进了 QQ 群。这居然是个 2000 人的群,大家幻想着事成之后,直接把这个群当作用户群。

  第二步,分工。在所有人都很迷茫的时候,唯一能做的就是学习。先弄明白比特币、矿池的原理再说。

  当时可供学习的素材主要有代码和文档。比特币自然是开源的,也有一些矿池是开源的,比如 P2Pool。文档在当时反而很少,基本只能看比特币最早那篇简短的论文。中文的资料把比特币说得很神,英文也很难找到详细介绍比特币技术的资料,更不用说矿池了。

  配合代码和文档,还有一种很重要的学习素材,那就是数据。矿机(挖矿程序)和矿池的通信数据,比特币客户端存储的数据。于是,大家就开始分头行动,看比特币代码、看矿池代码、看开源矿机代码、分析不开源矿机的通信数据、编译开源矿池测试。这些工作相辅相成,大家有条不紊地进行着。

  和大神们一起讨论与探索,进度还是非常快的:

  第一天,大家就弄明白了比特币的原理(最有效的是看比特币的区块数据),什么是难度,比特币是怎么保证全网 10 分钟只出一个区块的。

  第二天,大概弄明白了质数币的原理,那些质数到底是怎么存的,怎么检验的。

  第三天,矿机与主流矿池的通信已经基本被翻译出来。至此我们终于发现,原来矿池比我们想象中的简单得多,倒是 P2Pool 有很多我们不需要的功能,代码过于复杂了。于是我们停止了为期两天的争论,放弃了修改 P2Pool 的方案,决定自己重写。

 p.s. 这几天所有人的口头禅都是:太 NB 了,怎么设计得这么精妙!

二、故事的开头

  尽管从开篇看来,做矿池是顺理成章的事情,然而故事真实的开头是这样的:

  首先,我们这帮技术宅(D神、R神、H神、M神、F神、S神、X神……)有一个群,平时在群里灌水,偶尔讨论技术。群里的话题是从这天开始变化的——

  • 2013.11.18 早上,R 神发了条微博说,睡觉前买了一个比特币,花了 3000,醒来之后涨到了 3600。见此大家各种膜拜。
  • 2013.11.18 白天,好消息:国内最大的比特币交易平台拿到了风投。于是比特币涨到了 4000 多一个。
  • 2013.11.18 晚上,好消息:美帝公开表示不会取缔比特币。于是比特币一下子飙升到 7000 一个……
  • 此后的若干天,技术宅们一有空就在 YY 怎么从中捞一笔。
  • 2013.11.23 D 神发现了矿池,于是开始天天给我们洗脑做出来可以躺着数钱。
  • 2013.11.26 我终于受不了洗脑,决定推动开发矿池。

三、数据币——走向人生巅峰,要先登上这个小山包

  伟大的 D 神在给我们洗脑要做矿池的时候,心里已经想好了要做“数据币”的矿池。这个数据币又是什么鬼?炒过币的人可能知道除了比特币之外,还存在着很多的山寨币。而数据币只是茫茫币海中一种非常普通的山寨币。

山寨币

  山寨币,其实英文是 alternative coin,其实这两个名字都挺确切的,就看从什么角度去看山寨币了。

  从技术创新角度看,山寨币只是在比特币之上做了点微创新。比特币提出了一整套金融体系,当之无愧是革命性的创新。而山寨币,说白了只是 fork 了比特币的代码,然后改了其中的一些参数而已。比如虚拟币市场中仅次于“金币”比特币的“银币”莱特币,作为最大的山寨币,主要也就三点微创新:改多了货币总额,加速了转账时间,然后把挖矿算法从 SHA-256 改成了 Scrypt。看起来确实挺山寨的。

  但是从应用的角度看,这些参数改动并不是毫无根据的,每个改动都可以成为山寨币的卖点。转账时间更短,可以让虚拟币交易更快捷,提高其实用性。而挖矿算法上,Scrypt 算法更适合 GPU 计算,相对而言更平民化。要知道比特币挖矿就是因为用了 SHA-256 算法,在 2013 年之后算力已经完全被专用集成电路(ASIC)垄断[引用],GPU 矿工根本没法获得收益。算力被垄断,对于虚拟币来说是很危险的,这点上,莱特币相比比特币稍微安全一些。这么看来,山寨币确实也是 alternative 的。

  制造一种山寨币,在技术并不难,难的是推广和运营。比如先要让人相信这个山寨币不是骗钱的(作者没有预挖之类);然后文案上要把“改参数”的故事说好,让人认为这是革命性的创新,值得去投资;最后对于特别山寨的山寨币而言,要想方设法让币上交易平台,上越大的交易平台,大家都认可度就越高。

  对于山寨币,中国比特币首富李笑来也曾公开表示他不会投资山寨币,他认为山寨币的创新和比特币比起来,根本算不上创新,不会长存。其实很多人也都是这个观点,但是芸芸众生就算后知后觉了首富的观点,也不见得就有觉悟不去碰山寨币。因为大家都相信,自己不是最后一个“接盘侠”。

比特币莱特币质数币数据币
btcltc
xpmdtc
SHA-256,ASIC挖矿Scrypt,GPU挖矿质数序列,CPU挖矿质数序列,CPU挖矿

数据币和质数币

  回到数据币,当然也是些微创新。数据币其实是一个在质数币上二次山寨的山寨山寨币。质数币的微创新,主要在其挖矿算法上。它需要矿工挖到足够长的质数链(第一类坎宁安链,第二类坎宁安链,和双坎宁安链)来证明其工作力投入。矿工在投入质数币挖掘的同时,也是在试图挖掘世界上最大的质数序列,有不少世界纪录是质数币创下的[引用1引用2]。从这个角度看,质数币可以算是一种有用的币。

  那数据币又在质数币上加了什么微创新呢?数据币在转账时可以带一句话,或者带个种子文件。以后这条信息就会永远留存在数据币的 P2P 网络中,可以真正的“永流传”,当然前提是一直有人用……

  D 神为什么选了数据币呢?

  第一,质数币或者数据币所用的质数挖掘算法在当时只能使用 CPU 来挖矿。比起比特币的矿卡和莱特币的显卡,CPU 挖矿更难被垄断,更平易近人。其实我们要是去做显卡币,自己都没机器测试。

  第二,数据币一直没有出现公开的矿池。这对于我们来说是个绝佳的切入口。虽说这个山寨币小的连矿池都没人来做,但是从 D 神在数据币矿工群里混了几天所了解的情况来看,矿工的激情以及整个币的体量还是足够养活我们的。

  在干活之前先画个大饼。所有矿池都会宣传自己的挖矿效率,列出最近出了多少区块(区块是挖矿的最小单位,一“块”矿里有 N 个币,N 在一段时间里基本不变)。同时我们也知道矿池的手续费是多少,所以很快就可以算出矿池的收入。当时质数币的第三大矿池(一共就三个),日入过万!YY 着我们从中随便抢点零头,也是非常可观的啊。

零、开坑按

2016年5月2日有新闻报道称,澳大利亚企业家克莱格·莱特(Craig Wright)拿出了一些证据证明了他就是比特币之父“中本聪”。回想起三年前,在比特币最火爆的时候,自己也曾参与其中。这段经历可能非常独特,故在此与大家分享。

2013 年秋天,大妈们在跳完广场舞后开始讨论比特币。比特币的价格从年初不到 100 元一个,一路飙升到 7000 元一个,深受投机人士的欢迎。就在这样的环境下,我们几个技术宅开始 YY自己能从这一波中捞点什么。当时我们发现了这些致富大法:1. 炒币,缺钱缺运气。2. 挖矿,缺机器,其实也是缺钱。3. 套利,缺钱。4. 做交易平台,似乎政策风险太大了,不敢做。 5. 做矿池,似乎只要技术。对于没钱没背景只有技术的我们来说,显然第五种致富大法,也就是做“矿池”看起来更值得一试。

一、矿池是什么,能吃吗?

想知道矿池能不能吃,就要先从挖矿说起。“挖矿”是生产比特币的过程,从“挖”这个字不难猜到,“挖矿”是一件费时费力同时需要一定运气的事情。如果用最原始的手段来挖矿,我们需要先下载一个比特币客户端(通常也叫比特币钱包,毕竟一般只用它来放“钱”),等客户端自动同步完比特币网络上的数据后,就会自动开始挖矿。

从技术上看,挖矿是个什么样的过程呢?简单的说,挖矿就是在算哈希,地球上所有想挖比特币的矿工一起算哈希。大约每 10 分钟,比特币网络就会公布一个基准字符串。收到这个字符串之后,所有挖矿的机器,都要做同一件事情:在这个基准字符串的后面加上一个随机字符串,并且希望这个合成的这个字符串,通过两次 SHA-256 哈希算法后,得到的输出,前面要有足够多的 0。

SHA-256(SHA-256(“基准字符串” + “随机字符串”)) = 000000000000…01011101…

到目前为止,SHA-256 还没被破解,也就是说,我们还没有办法通过构造原串,得到期望的输出。所以对于我们勤劳朴实的矿工而言,唯一能做的就是,不断地替换随机字符串,直到人品大爆发,随机到我们想要的结果。比如,现在如果要挖到比特币,需要哈希结果中,前面有 67 个 0。这是个什么概念?大概是说,我们如果不停地随机,不停地哈希,会有 2 的 67 次方之一的概率,随机到我们想要的结果,然后就挖到一大块比特币,里面大概有 25 个比特币(具体数值因时间而异)。简单估计一下,$2^{67}=147573952589676412928$,如果我们的机器非常快,1 秒算 1 亿次哈希,那么……运气正常的话,大概需要46795.4 年才能挖到比特币,这可真不是一般人玩得起的。

既然单干玩不起,那就大家一起来吧,这便有了矿池。矿池就是一堆人一起挖矿,挖到矿之后,根据大家投入的算力按比例分配收益。只要在矿池挖矿的人足够多,小算力的矿工就可以收到小额但是很稳定的收益,而不像独立挖矿的时候那样(行话叫 solo),平时影子都没,突然有一天变成爆发户。矿池的收入模式非常简单,就是手续费提成。这简直就是一个一本万利的项目啊,成本可能就是几台服务器,做出来躺着数钱就行了。

前前后后写论文也有将近一年的时间了。这个研究的课题到目前还比较热门,在此分享博士论文。希望读者有所收获,少走一些弯路。

论文下载地址:http://pan.baidu.com/s/1jGWmmZO
arXiv 地址:https://arxiv.org/abs/1611.05962

感谢赵老师的指导,以及各位老师同学的宝贵建议!

有什么疑问或者发现什么问题都可以直接在这里评论。

  自认为这是一篇有用的文章,因此在发表之前先放到 arXiv 上,供大家参考,请批评指正。
  论文地址:http://arxiv.org/abs/1507.05523
  实验代码地址:https://github.com/licstar/compare

  准备这篇论文大概花了半年时间,从去年 11 月开始做实验,到今年成文。期间消耗大约 10 万 CPU 小时,然后在几十万个结果里面做人肉数据挖掘,最后得到了一些可能有用的结论。

  看标题就能够猜到论文的大概内容了,就是希望找到一种简单有效的词向量学习方法。怎么会想到做这件苦逼的事情呢?半年之前在我准备上一篇论文的时候(RCNN,强行广告,欢迎引用,文章点此,上面的常用链接里有文章相关信息)有一个意外的发现:选用不同的词向量作为模型的初始值,效果的差异非常大!!!当时正值我已经调了好几天参数,黔驴技穷之时,没想到手滑用错了个词向量,效果突然变 NB 了。这下我就脑洞大开了,要是能搞出个宇宙第一词向量,岂不是以后可以随便灌水?后面的故事肯定就能猜到了,半年之前我掉进这个坑里,只为寻找这个“宇宙第一”词向量。然后我实现了几个主流的词向量模型,遍历了各种语料和参数,榨干了实验室机器的空闲资源跑词向量,从生成的几十万个词向量里寻找规律,最后发现“宇宙第一”词向量应该是不存在的,不过生成一个好用的词向量还是有一定套路可循的。为避免某些读者对后面冗长的文字没有兴趣,我先用最简单的话描述我们发现的这个套路,那就是:

首先根据具体任务,选一个领域相似的语料,在这个条件下,语料越大越好。然后下载一个 word2vec 的新版(14年9月更新),语料小(小于一亿词,约 500MB 的文本文件)的时候用 Skip-gram 模型,语料大的时候用 CBOW 模型。最后记得设置迭代次数为三五十次,维度至少选 50,就可以了。

  似乎结论很“显然”,不过这种简单的策略,真的就这么有效吗?word2vec 在半年前已经是最流行的词向量生成工具了,它之所以能这么流行,除了头上顶着 Google 光环,很大程度上是因为这是一个好用的工具包,而不一定是因为它的效果就是最好的。质疑 word2vec 的效果,是因为它的两个模型(Skip-gram 和 CBOW)相对前人的模型做了大量简化,去掉了词序,又去掉了神经网络的隐藏层,最后变成了一个 log 线性模型。我主观地认为,这些简化是会影响词向量的性能的,毕竟词序很有用(上面提到的我的上一篇论文就是讲这个的,再次广告);然后神经网络的表达能力也比 logistic 回归要强得多。这两部分简化到底会带来多少损失,还是要靠数据来说话。所以我考虑的第一个问题就是,什么模型效果好。前面博客提到过的经典模型(NNLM、LBL、C&W)在 word2vec 出现之前,是最主流的方法。这些方法就保留了词序信息,而且也有隐藏层,都放到一起比较。有了这些模型,还缺一个只保留词序,但没有隐藏层的模型。所以我设计了一个中间模型,叫 Order,保留了词序,但没有隐藏层。最后再加上开源的 GloVe,一共比较了 Skip-gram、CBOW、Order、LBL、NNLM、C&W、GloVe 这 7 个模型。

  那么最严峻的问题来了,怎么样才算好的词向量呢,应该用什么指标来衡量呢?Mikolov 推广 word2vec 的时候,设计了一个经典的指标(king-queen=man-woman),由于有开放的数据集和评测代码,后来大量的工作都只用这一个指标来评价词向量。我觉得只用这个指标肯定是不对的,为什么词向量非得有这种线性平移的特性啊,如果说这是个加分项,还说得过去,但要成为唯一衡量标准,似乎没什么道理。我觉得 GloVe 那篇论文就做的很好,里面比较了一大堆指标。所以我就从根源想起,我们拿词向量是用来干什么呢?①有人拿它寻找近义词或者相关词,直接根据向量空间里的距离远近来判定词的关系。②也有不少早期的工作,直接拿词向量做特征,在现有系统中加入词向量作为特征。特征嘛,就是要个多样性,虽然不知道词向量包含了什么信息,但是说不定就带着新的信息,效果就能提升了。③还有大量基于神经网络的工作,拿词向量作为神经网络的初始值。神经网络的初始值选得好,就有可能收敛到更好的局部最优解。好,就是这三种指标了:语义特性、用作特征、用作初始值。基于这三大类用法,我们具体找了 8 个指标,进行比较,综合评价词向量的性能。(题外话,其实我从一开始就很想找到一个终极指标,用一个指标定江山,但是自从看到这 8 个指标的结果如此不一致之后,我终于放弃了这个念头。)

  为了公平的比较,肯定需要设定一个相同的语料,所有模型都用同样的语料来训练。选个什么语料呢?脑海中的第一反应是:大语料。其实小规模语料(几十兆) Turian 在 2010 年已经做过一些实验了,比较了 C&W 和 HLBL 这两个模型。大规模语料下,结论会不会不一样呢?找了两个大语料,维基百科英文版和纽约时报。此前,主流论点是:语料越大越好。所有语料都堆到一起,不管是什么内容,我语料越大,涵盖的语义信息就越丰富,效果就越好。GloVe 和 C&W 都是这么干的。自然,我也把这两个大语料混合在一起训练了。同时由于好奇心,我还把这两个语料单独拿出来训练词向量。反正就是挂机跑嘛。另一方面,为了验证一下是不是真的语料越大越好,我也在大语料中抽了 10M、100M、1G 词的子集,顺便验证一下别人的结论。说到这里我忍不住剧透了,实验中最意外的结果就是,语料对词向量的影响比模型的影响要重要得多得多得多(重要的事说三遍)。本来我只想固定一个语料,公平比较模型,结果却发现语料比模型重要(抱歉,是四遍)……后来又加了一个小规模的 IMDB 语料,进一步证实了这个结论。实验结果就是这样。

  当然,为了公平比较,还有一些其它的因素需要考虑,比如上下文窗口都开成一样大,词向量的维度也需要统一。还有个很重要的参数,那就是迭代次数。这些词向量模型都是用迭代方法优化的,迭代次数肯定会影响性能。对所有模型固定迭代次数可能不合适,毕竟模型之间的差异还是蛮大的。所以为了省事,我们对所有实验迭代足够多的次数,从 100 次到 10000 次,取最好的那次。做了这个决定之后,人省事了,机器累死了,而且对于每个语料、每个模型、每个参数,还要保留不同迭代次数的词向量,很快就把 3T 硬盘塞满了……一边删数据,一边继续做实验……这种暴力手段肯定是不适合实际应用的,所以我们也努力从实验数据中寻找规律,希望找到一种合适的迭代停止条件。

  最后总结一下,我们认为模型、语料、参数三方面会影响词向量的训练,所以从这三方面入手分析到底应该怎么生成一个好的词向量,论文由此展开。在博客里主要就介绍一下不方便写进论文里的故事和背景。

—————-枯燥开始的分割线—————-

  简单列举一下文章的贡献。
  模型方面,因为所有的词向量模型都是基于分布式分布假说的(distributional hypothesis):拥有相似上下文的词,词义相似。这里有两个对象,一个是我们需要关注的词(目标词),另一个是这个词对应的上下文。所以,我们从两个角度去总结模型:1.目标词和上下文的关系,2.上下文怎么表示。在这种分类体系下,我们对现有的主流词向量模型进行了总结和比较,发现,目标词和上下文的关系主要有两种,大多数模型都是根据上下文,预测目标词。而 C&W 模型则是对目标词和上下文这一组合,打分。实验发现,通过上下文预测目标词的模型,得到的词向量,更能捕获替换关系(paradigmatic)。在上下文的表示方面,我们分析了几种表示方法之后,发现可以通过模型复杂程度对这些模型进行排序。排序之后,实验结果就容易解释了:简单的模型(Skip-gram)在小语料下表现好,复杂的模型在大语料下略有优势。从实践中看,word2vec 的 CBOW 模型在 GB 级别的语料下已经足够好。我前面提到的 Order 模型,加入了词序信息,其实很多时候比 CBOW 更好,不过带来的提升并不大。

  语料方面,很多论文都提到语料越大越好,我们发现,语料的领域更重要。领域选好了,可能只要 1/10 甚至 1/100 的语料,就能达到一个大规模泛领域语料的效果。有时候语料选的不对,甚至会导致负面效果(比随机词向量效果还差)。文章还做了实验,当只有小规模的领域内语料,而有大规模的领域外语料时,到底是语料越纯越好,还是越大越好。在我们的实验中,是越纯越好。这一部分实验数据比较丰富,原文相对清楚一些。

  参数方面,主要考虑了迭代次数和词向量的维度。其实词向量都是迭代算法,一般迭代算法都需要多迭代几次。旧版的 word2vec 只迭代了一次,效果很受限制,换新版就好了(也欢迎用我们论文实验的方法,用 AdaGrad 优化,相比原版 word2vec 的学习速率下降法,这样还能在之前的基础上继续迭代)。然后迭代次数怎么选呢?机器学习里很常用的迭代停止指标是看验证集的损失是否到达峰值,认为这个时候模型开始过拟合了。按照这个方法,我们可以从训练语料中分出一个验证集看损失函数的变化。但是实验中我们发现,这种策略并不好。主要原因就是,训练词向量的目标是,尽可能精确地预测目标词,这一目标和实际任务并不一致。所以更好的方法是,直接拿实际任务的验证集来做终止条件。如果实际任务做起来很慢(比如 NER 任务的开源实现大概做一次要两小时),文章还给了一种参考的方法,随便挑一个任务当验证集用,一般都比损失函数靠谱。
  词向量的维度则只有一些实验结果。做词向量语义分析任务的时候,一般维度越大效果越好。做具体 NLP 任务(用作特征、用作神经网络初始化)的时候,50 维之后效果提升就比较少了。这部分的结果很依赖于具体任务的实现,或许用上了更先进的神经网络优化方法,词向量作为初始值带来的影响又会有新的结论。

—————-枯燥结束的分割线—————-

  完全公平以及全面的比较是很难做到的,我们也在尽可能尝试逼近这个目标,希望这些结论会有用。请批评指正。当然,也非常非常欢迎引用~~~~~