« May 2014 | Main | July 2014 »

June 23, 2014

Linode 服务真不错

今天才发现 linode 出了 10$ 一个月的服务,同时以前 20$ 的服务硬件全面升级了。

反正我的主机用不了这些,翻墙看 youtube 都用不了每个月 3T 的流量。决定降级,可以便宜一点。在 linode 的管理面板上找到了 Resize ,从 linode2048 换到了 linode1024 。马上就成功了。

然后立刻收到了账单邮件。说是因为这个月还有一周,这周的原费用是 $5.34 ,新费用是 $2.67 。退了 $2.67 到我的账户里。:)

再帮它打个广告吧。作为一个在中国生活的程序员,购买一个 linode (我的 referral code : 538bab39bc1265a2ce54115d1f86e2bc81e4d133 ),上 google 必备。

June 21, 2014

重新设计并实现了 skynet 的 harbor 模块

skynet 是可以启动多个节点,不同节点内的服务地址是相互唯一的。服务地址是一个 32bit 整数,同一进程内的地址的高 8bit 相同。这 8bit 区分了一个服务处于那个节点。

每个节点中有一个特殊的服务叫做 harbor (港口) ,当一个消息的目的地址的高 8 位和本节点不同时,消息被投递到 harbor 服务中,它再通过 tcp 连接传输到目的节点的 harbor 服务中。


不同的 skynet 节点的 harbor 间是如何建立起网络的呢?这依赖一个叫做 master 的服务。这个 master 服务可以单独为一个进程,也可以附属在某一个 skynet 节点内部(默认配置)。

master 会监听一个端口(在 config 里配置为 standalone 项),每个 skynet 节点都会根据 config 中的 master 项去连接 master 。master 再安排不同的 harbor 服务间相互建立连接。

最终一个有 5 个节点的 skynet 网络大致是这样的:

network.png

上面蓝色的是 master 服务,下面 5 个 harbor 服务间是互连的。master 又和所有的 harbor 相连。

这就是早期的 skynet 分布式集群方案。有一篇 2 年前的 blog 记录了当时的想法,可以一窥历史。

由于历史变迁,从早期的手脚架不全,到如今的 skynet 的基础设置日臻完善。这部分代码也改写过很多次,每每想做大的改动,都不敢过于激进。

最近,我们的一个新项目要上线,由于运营方只能提供虚拟机,且网络状态不是很好,暴露了 skynet 在启动组网阶段的一些时序漏洞。所以这个周末我咬牙把这块东西全部重新设计实现了。

当初为了简化设计,每两台机器间的连接使用了两条 TCP 连接。数据流在每条连接上都是单向的,即谁发起连接,谁就在这个连接上单向推送数据。这样做的好处是,如果双方都是可信的机器的话,可以省去握手的协议。

如果采用一条连接,双工使用,势必需要在接受连接时询问对方是谁。组网代码的复杂度就高了许多。

但是,两条连接的问题也很明显。当我可以向对方发送数据成功后,对方未必反向连接成功。就需要做更复杂的状态管理。当然,一旦组网成功,就没有太大区别了。

这块代码一开始就是 C 语言编写的,最早期利用 zeromq 搭了初期的雏形,后来又利用独立的 gate 服务重新实现了早期协议(实现方法上还留有 zeromq 的痕迹);再后来 gate 服务用后期完善的 socket 模块改写,一直演化到今天。在这段时间里,我充分意识到,让多台机器两两互连,组成一个通讯网络是一个及其复杂的事情。而组网本身又不要求特别高的效率,对效率有要求的只是网络组建好之后的消息转发而已。

所以把这快业务分离出来,用 lua 来编写组网模块要更合适一些。


这个周末,我按新的想法重新实现了这块代码。由于全部重新实现,就顺便把协议也重新设计了。

现在,节点间不再需要两条连接,而只用一条。每个节点加入网络(首先接入 master)后,由 master 通知它网络中已有几个节点,他会等待所有现存节点连接过来。所以连接建立后,就关闭监听端口。

如果再有新节点加入网络,老节点主动去连接新节点。这样做的好处是,已经在工作的节点不需要打开端口等待。

这套代码实现在 cmaster.lua 和 cslave.lua 中,取代原来的 service_master.cservice_harbor.c ,用 lua 编写有更大的弹性。这两个服务还负责同步 skynet 网络中的全局可见的服务名字,原本在 C 版本中,这部分实现的很繁琐,改为 lua 后,清晰了许多。

原有的远程消息代理模块被剥离出来,还是放在 service_harbor.c 中,比之前的代码篇幅小了很多。它只负责管理 tcp 连接的 fd, 而不必操心连接连接的过程。这样,也不再依赖额外的 gate 服务,只需要做简单的拼包处理即可。

至此,C 版本的 gate 已经完全脱离了核心模块。在一般应用中,后来写的 lua 版 gate 已经足够用了。


此外,对于松散的集群结构,我推荐使用 skynet 的单结点模式,在上层用 tcp 连接互连,并只使用简单的 rpc 协议。在目前的 skynet 版本中,有封装好的 cluster 模块 可供使用。

这种做法要求明确本地服务的调用和远程调用的区别。虽然远程调用的性能可能略低,但由于不像底层 harbor 那样把本地、远程服务的区别透明化,反倒不容易出问题。且 tcp 连接使用了更健壮的 socketchannel ,一旦连接断开,发起 rpc 的一方会收到异常,也可以重试(自动重连)。

而底层的 harbor 假设机器间是可靠连接,不会断开。而一旦内部网络不健康,很可能会导致整个系统无法正常工作。它的设计目的并不是为了提供弹性扩展的分布式方案,而是为了突破单机性能上限的问题。

两个跨机方案各有利弊,所以还请设计系统的时候权衡。只使用其中一个方案或是两个同时用,应该都有适用的场合。


这次大的修改,将作为 0.4.0 版的核心特性发布。不过我想把它们在 dev 分支 放一段时间,希望有能力的同学可以帮忙 review 一下代码,或做一些测试。

ps. 最近 skynet 社区比以前活跃了许多。看的到不少利用 skynet 开发的游戏项目了。为 skynet 减少了不少 bug。感谢大家。

June 11, 2014

一个适用于腾讯开放平台的 tunnel

目前如果要与腾讯合作运营游戏的话,是必须把服务器运行在腾讯的云平台上的。鉴于腾讯的强势地位,严禁自己托管机器,也禁止选择别家的云服务。至于腾讯开放平台,谁用谁知道,呵呵。既然都叫”开放“了,你就能想像是什么意思,所谓共和国还要在名字前加个 “人民” 呢 :)

如果你把 “必须运营在腾讯云平台” 这条当成企鹅税来看待,也就了然了。无非是多花点钱用差点的硬件,增加点程序员人工为它写些代码适配,出点啥莫名其妙的问题,忍忍,多吐点槽就好了。要理解大公司部门多,各个部门都需要完成自己的 KPI ,刷点存在感。

当然也有可能你的程序无法获得需要的处理能力,或是你不堪忍受在云平台上的各种奇葩。那么还有一条路可以偷偷走。那就是想办法自己另外托管机器,然后写一个 tunnel 程序把服务器连起来。

鉴于腾讯开放平台的开放性,普通现成的 tunnel 程序可能满足不了需求。晓靖同学最近在学 go 语言兴致比较高,用 go 实现了一个。需要的同学可以自取

这个东西完成了这样的功能:

你可以在开放平台下运行一份 gate ,监听一个端口。用户可以通过这个端口请求你提供的服务。

由于你可能没有权限从开放平台向外网发出连接。所以 gate 还需要再监听另一个端口,让你的服务器可以从外部连接进去。

然后,你在你的服务器这边(可能是你自己另外托管的机器上,也可以直接是你家里或办公室里的机器)启动一份 node 连接上放在开放平台中的 gate 。连接建立后,node 会把用户对 gate 的所有请求都转换成本地请求,你的服务器只需要启动在和 node 同一台机器上,就能处理它们了。

由于开放平台 tgw 的某种奇葩设计,从外部连入的 tcp 连接都会多出一个莫名其妙的包头 。自然你不希望看见它。gotunnel 这个服务会很贴心的帮你去掉它们。

当然,使用 tunnel 会使得所有用户的数据流都需要绕道几次才能辗转抵达你的真正服务器,所以单纯的请求回应测试会看到性能下降不少。但是长期总吞吐量是不变的。它在中间仅仅是做了包转发的工作,并没有降低你的服务器的处理能力。

采用这个方案,你可以大大节省在腾讯运营游戏的成本(可以用更经济的硬件,更少的维护成本,不需要为了开放平台改写你的程序),又可以满足开放平台的 KPI ,满足腾讯各个部门的存在感,最终用户也能得到更好的游戏体验。获得一个三赢的局面。

June 10, 2014

skynet 主题 T 恤

目前联系了广州一家制衣厂做一些 skynet 的主题 T 恤。由于定的量很少,所以价格不算便宜(但材料选用的那家厂最好的)。

有同学帮忙开了家 taobao 店用于收款(不是我开的),想要的人自己下单。凑够 100 件就开工。如果做不成就退款。

想要的同学点这里

June 09, 2014

skynet 的集群方案

上周 release 了 skynet 的 0.3 版,其最重要的新特性就是给出了一套新的集群方案。

在过去,skynet 的集群限制在 255 个节点,为每个服务的地址留出了 8bit 做节点号。消息传递根据节点号,通过节点间互联的 tcp 连接,被推送到那个 skynet 节点的 harbor 服务上,再进一步投递。

这个方案可以隐藏两个 skynet 服务的位置,无论是在同一进程内还是分属不同机器上,都可以用唯一地址投递消息。但其实现比较简单,没有去考虑节点间的连接不稳定的情况。通常仅用于单台物理机承载能力不够,希望用多台硬件扩展处理能力的情况。这些机器也最好部署在同一台交换机下。


之前这个方案弹性不够。如果一台机器挂掉,使用相同的节点 id 重新接入 skynet 的后果的不可预知的。因为之前在线的服务很难知道一个节点下的旧地址全部失效,新启动的进程的内部状态已经不可能和之前相同。

所以,我用更上层的 skynet api 重新实现了一套更具弹性的集群方案。

和之前的方案不同,这次我不打算让集群间的通讯透明。如果你有一个消息是发放到集群内另一台机器中的某个服务的,需要用特别的集群消息投递 api 。节点本身用字符串名字,而不是 id 区格。集群间的消息用统一的序列化协议(为了简化协议)。

这套新的方案,可以参考 examples 下的 config.c1 和 config.c2 分别启动两个节点相互通讯。

如果使用这套方案,就可以不用老的多节点机制了(当然也可以混用)。为了简化配置,你可以将 skynet 配置为 harbor = 0 ,关闭老的多节点方案。这样,address standalone master 等配置项都不需要填写。

取而代之的是,配置一个 cluster 项,指向一个 lua 文件,描述每个节点的名字和地址。

新的 cluster 目前只支持一个 rpc call 方法 。用来调用远程服务。api 和 skynet.call 类似,但需要给出远程节点的字符串名字,且通讯协议必须用 lua 类型。

这套新方案可以看成是对原有集群的一个补充。当你需要把多台机器部署到不同机房,节点间的关系比较弱,只是少部分具名服务间需要做 rpc 调用,那么新的方案可能更加合适一些。因为当远程节点断开联系后,发起 rpc 的一方会捕获到异常;且远程节点用名字索引,不受 255 个限制。断开连接后,也可以通过重连恢复服务。