服务器运维的几重境界

Posted on Oct 12, 2014

这里所说的运维是一个比较宽泛的概念,基本上与游戏本身无关的、程序员(主要针对服务器)又要去做的事情都涵盖在内,包括版本管理、发布部署、集群管理、服务器容灾、服务器扩容、数据备份恢复、监控警报等等。游戏开发这几年,经历过两家公司,算上现在这个总共有 5 个项目,其实远不算见多识广,很多实践方法是听同事讲的或者招聘的时候打听来的。我大体整理了一下游戏运维发展脉络,顺便展望一下今后的趋势。

“虽然我很笨,但是我很勤奋。”

就像愚公移山,最原始的运维靠的是蛮力。愚公的愚主要就体现在做事情太直接,缺乏进一步的思考。

要打版本?新建个目录把代码 check 出来,make 一下就编译好了,再改一下版本号就大功告成。

怎么部署呢?scp 拷贝到生产环境,再运行脚本解压缩重启一下。

要加新服务器?在新机器上安装好游戏运行要用的软件和库,用 vi 修改全局配置文件把新机器加上去,再用 scp 拷贝到每台服务器,最后全重启一遍。

服务器挂了怎么办?有备用的空闲机器就可以直接用了,如果没有还得花时间装软件。当然改全局配置再整体重启是免不了的了。

问题是很显然的吧。运维人员主要在做重复性的工作,服务器需要 24 小时值守。时间久了一定会疲惫的,而且各种手动修改操作很容易误操作带来不可挽回的损失。

“机器能做的事情交给机器去做,但是关键的操作还是得靠人啊!”

到这个阶段开始考虑编写脚本或者引入工具了,目的是为了减少重复性的工作。

打版本使用写好的自动化脚本,要打版本的时候一行命令版本包就出来了。版本号可能在代码库的某个文件里,打版本之前需要手动改一下提交,做的好点的会在脚本里做自去递增。更进一步会用上可持续集成工具(如 jenkins),这样在网页上点一下就能出版本,打版本的事情就可以完全交给非技术人员了。

更新部署这类线上操作还是要交给运维人员来手动做,但是会编写大量的脚本将操作简化。特别是对于服务器集群的批量操作,通常会在集群中选一台主机作为中控机/跳板机,直接在中控机中使用expect + ssh 脚本批量操作远程主机。需要更新的版本和公用配置也可以从中控机上批量分发出去,可以用脚本,也可以用 rsync 进行同步。

引入监控系统监控服务器的运行状态,有异常情况时通过短信电话通知。

通过使用自动化脚本,运维人员大量的日常事务得到了简化。虽然还是要 24 小时待命,可能时不时得半夜起床处理线上问题,但至少不用轮流加夜班,可以每天回家睡觉了。

“我们已经不是两三岁的小孩了。”

运行在服务器上的程序就像没长大的小孩子,一有风吹草动就又哭又闹。运维就像是同时照顾几十个小孩的保姆,一刻不得闲。实际上我们完全可以通过良好的软件设计来提高程序的可用性。可以使用守护进程保证进程宕了之后能自动重新启动,最好能注册为服务在系统重启后也能恢复服务,如果能做到恢复之前的会话状态就更好了。相互之间有交互的进程不要设计成强制要求特定启动顺序,也就是说要有进程之间设计自动重连的机制。数据库要能在故障时自动进行主从库切换。

对运维友好的程序有了一定的自我修复能力,这样一来大部分问题就不再是需要立即着手解决的了,我们终于可以睡个安稳觉了!

注:后面介绍的 3 个工具基本上是我这几天陆续看来的,这里只是做一下介绍和展望,具体实践有待后面继续探索。

“Linux 上装应用也能像苹果 AppStore 一样便捷吗?”

答案是完全可以,我们要用的 工具是 Docker。简单的说,Docker 可以用来把一个程序和这个程序运行所需要的整套环境打包成一个镜像,这个镜像可以被分发到任意一个 Linux 系统中运行,唯一的要求是这个系统中安装了 Docker。

Docker 官方提供了一个镜像发布中心 DockerHub,用户可以自由发布制作好的镜像供他人使用,也有各种软件维护者发布的官方镜像。DockerHub 听起来像是 Docker 平台上的 GitHub,实际上意义远不止于此,我认为其地位应该相当于 Linux 界的 AppStore。

试想我现在想在某台服务器上运行 nginx,我只需要从 DockerHub 下载 nginx 镜像,再 docker run 启动一个容器来运行这个镜像,我不需要规划把 nginx 安装到系统的哪个目录,也不关心这个 nginx 的版本到底依赖于哪个版本的 libc。同样如果我想要一个 apache 服务器,那么我就下载一个 apache 镜像下来运行,完全不用先准备一套 jdk 环境。不同应用之间是完全独立的,即使需要不同的 jdk 版本也不会产生冲突。

游戏版本的发布过程也将发生深刻变化。我们不再需要在每台服务器上安装游戏运行的依赖库,转而制作一个包含了所有依赖的基础镜像。需要发布版本时把编好的程序和配置添加到基础镜像中就得到了一个游戏版本镜像,这个镜像拷贝到任意装了 Docker 的 Linux 系统上都能一致地运行。

最后我们的服务器集群每台主机上需要安装的唯一软件就是 Docker,服务器的配置工作大幅简化。

“像生态系统一样会自我修复。”

前面所说程序的自我修复能力,其实只能部分解决软件方面的缺陷,对硬件故障是无能为力的。在服务器集群的长期运行中,出现硬件故障的概率是 100%。主机不再可用时唯一的办法是换用备机,可问题是服务器列表往往是做一份配置文件分发到每台服务器的,换用备机必须把新的配置重新分发,如果有必要还得一一重启……

这里要用到的是分布式数据管理工具 etcd(类似的还有 zookeeper、doozer 等)。应用 etcd 我们可以把服务器共用配置存储在分布式环境,注意不是存在某台主机上,而是由多台主机共同维护并保证其一致性,这样一来任何一台主机故障了都不会影响数据的完整性。同时各个游戏组件可以作为观察者向 etcd 注册自己感兴趣的配置,于是我们只要通过 etcd 修改配置,集群中的所有主机都能得到通知并更新自己保存的服务器列表了。

我们甚至可以做得更智能一点,运行一个监控程序监视各个系统组件的运行状态,如果发现某节点故障了无法提供服务,那么自动先择备用节点替换之,并通过 etcd 更新配置。

如此一来,整个故障恢复过程完全是自动化的,我们的集群就像生态系统一样有了自我修复的能力。

“聪明的程序员善于抽象,卓越的程序员在抽象之上抽象。”

最后顺着上面的思路介绍一个工具:fleet。fleet 对分布式程序的运行模式作了进一步抽象,把分布式系统分为 Service 和 Machine 两个独立的部分。

比如游戏中用一个 game 进程承载一个区的玩家,那么 game 就是 Service,体现为一个 Docker 镜像。如果我们要运行 5 个区,但是只有 4 台服务器,那么就选其中一个服务器运行两个 game,等到有多出来的服务器的时候再转一个 game 过去运行就好了。如果有一台机器突然坏了呢?好说,从所有服务器中找一台空闲点的再运行一个 game,顺便把这个 game 的新 ip+port 通过 etcd 广播一下就行了。只要有一个调度机制,这些都可以自动完成,完全不需要人工干预,我们只需要在系统负载过高的时候扔几台主机进集群完事。 这样的世界多么美好!


Q&A

手游的服务器,一般都是购买腾讯云或者阿里云的,一般不用我们去维护的吧?

用过腾讯云的话,想必一定经历过“母机挂掉”导致主机直接没法使用的情况吧,我们甚至会直接收到腾讯云的邮件,告知“某部分主机晚上 12:00 至 06:00 需要停机维护,请做好备份工作,必要的话请迁移备机"。

在这种情况下,备机的启用、环境搭建、加入集群、更新配置,这些都是开发者自己的事情,腾讯云是不管的。

扩容的情况类似,比如游戏开新服,腾讯云只提供装好系统的虚拟主机,具体的配置工作还是得我们自己来。

如果使用开放平台提供的存储服务,数据库的备份和恢复或许开发者可以不用操心。这个我也不是很了解,鉴于腾讯云的不靠谱程度,我们之前的数据库一直都是自己买主机搭建 NoSQL 服务的。
至于监控,腾讯云只能提供基础平台的监控,比如主机掉线、CPU 内存占用过高等。应用层面的监控他们是不管的,比如游戏进程挂了,数据库存盘压力过大,登录排队堆积,这些都是开发者自己需要解决的问题。

总之云平台提供的是基础服务,大部分的集群的管理还是得开发者自己来。

Docker 中运行其它软件,会有性能上的损耗吗?还有,管理进程的方式是不是改变了?

Docker 容器运行在与 Host 隔离的文件系统和网络环境中,所以性能损耗一定是会有的。 与传统 VM 不同的是,Docker 并不是采用的硬件虚拟化,而是利用 Linux 的 LXC 实现的操作系统层面的隔离,容器中运行的进程实际上还是运行在 Host 上的,可以直接在 Host 上通过 ps 命令看到。

一些资料给出的答案是“性能逼近于直接运行”,我理解的是与直接运行性能上的差异主要体现在启动过程(通常需要 1 秒左右),启动完成后性能上差别应该是可以忽略不计的。

使用 Docker 后,原本对进程的管理转变为对容器的管理。

我们通过 docker run 启动容器,通过 docker ps 查看当前运行中的容器,docker stop 或 docker kill 中止容器。

同时 Docker 还提供了 RESTful 的 Remote API,让我们可以更便捷地进行远程管理。

Docker 生产环境运行的稳定不?

呃,这几天刚在看,项目还在研发中,目前并没有生产环境可以验证……