GS DevOps 实践

现状&契机&目标

DevOps 是分工细化下的产物,用于提高开发和运维的协作效率,实现快速交付。这里主要从开发的角度谈谈游戏服务器中的DevOps,以及我们最近做的一些尝试。

1. 现状

我们现在是不同的项目使用不同的语言和框架,上线前,运维了解熟悉各个项目组的配置方案,部署流程,数据管理等,同时开发也需要熟悉运维的检测工具,接入监控报警机制。这通常需要一周甚至更多的时间,并且线上部署和本地部署可能是两套完全不同的流程,由于项目组相关性太强,运维通常也是专职维护指定项目,效率很低。上线后,如果出了BUG 需要紧急修复,通常是开发人员直接ssh到正式环境的主机上,进行日志查看,状态调试,甚至代码修改,对操作人员的要求比较高并且风险大,没有充分发挥运维的作用。

2. 契机

  • 微服务理念,比如服务发现,错误处理,无状态服务等等,使得应用的节点拓扑和交互边界更加清晰规范,更易于管理。
  • 容器技术为节点提供了比物理机更快速,一致,隔离的环境,使得服务部署更加容易。容器也比原生OS进程提供了更多的资源控制,状态监控等功能。

3. 目标

整个 DevOps 其实就两个关键点:

  1. 怎么部署上去: 构建与部署流程,其实就是CI流程,目前还没有比较好的基于Docker的界面化CI工具
  2. 部署之后怎么管理: 包括状态监控,故障转移等,大部分工作docker swarm都已经做好了,GS只要支持服务发现即可,或者用 docker swarm 网络的服务发现,我们是自己做的 etcd+grpc。

具体实践

就我们目前而言,DevOps的主要途径是让运维尽早地参与到项目的框架设计中,制定相关的标准和规范,开发过程通过遵守这些规范以达成更快的交付速度,更稳定,安全的运行状态,以及更系统,完善的维护流程。这个过程需要开发人员懂一些基本运维知识,最好就是开发环境的部署流程和线上环境一样,这样,运维在规范开发,开发过程中也在完善运维流程,达成双向促进。这里从 GS 的角度谈谈我们从”可运维”角度做的一些具体实践。

1. GS 配置

GS 配置指节点(容器)配置,通常是运维最关心的,配置方案有很多种,配置文件,启动参数,配置服务,甚至硬编码,之前我们更多的是用配置文件,即将配置模板加入代码仓库,然后在本地拷贝一份改成自己的配置,这份配置是不加入仓库的,这种方案有两个缺点: 1. 拿到代码才能拿到配置模板,2. 需要手动改配置文件,这就产生了文件路径依赖,并且不方便做自动化。Go 工业级编程这篇文章中曾经提到:最好的配置方案就是通过启动参数,这一点我深表赞同,最好的配置方案就是当你拿到这个二进制时,你就拿到了这个二进制的使用方法,并且作者提到的配置方案已经有了很好的实现: cli.v2,可以结合 yml 文件更好地简化和管理配置。我们目前所有的运维配置都通过命令行指定。

命令行配置可以很方面地过渡到容器,绝大部分的参数都通过容器启动参数传入,极少数特殊配置可以考虑通过ENV传入,这两种方案应该能满足任何情况,比文件配置具备更高的灵活性和安全性。

2. GD 配置

在我们之前的实践中,代码和策划配置csv文件常常是放在一起的,这本身不是一个很好的实践,比如我要热更配置,就必须要更新代码仓库。在容器化之后,这种缺点会更明显,更新配置就意味着要更新代码重新构建镜像,也就要停服,自然也就做不了热更。因此我们现在统一把csv配置通过脚本导入到 mongodb,将策划配置作为外部输入源来管理,这样不同的服务器可以指定使用同一个 DB 的配置(策划配置只读),热更也得以实现(先更新配置并生成到 DB,再发送hotload命令给GS)。

3. 日志规范

日志数据通常分为两大类: 逻辑调试日志和数据分析日志,
调试日志是给开发者看得,通常分为几个等级,有时候为了方便,我们还会按照模块功能对调试日志进行归类,比如网络消息日志,地图日志等,数据分析日志主要是给运营,数据部看的,包括 IPO 日志和运营日志。所有这些日志可能来源于不同的节点上,这给日志查阅和整理带来了难度,之前大多数项目组可能都是直接将日志存为文件,然后运维和开发者需要知道每个GS的日志目录在哪里,在容器化部署下,这暴露出几个问题:

  1. 为了保证日志在容器停止后不丢失,容器的目录需要挂载出来,这样容器和宿主机绑在了一起,不具备位置透明性
  2. 当容器被部署在其它物理机的时候,日志前后就被分隔开了
  3. 多个无状态容器的日志或者逻辑相关的容器日志分散在各处,不方便按照时间线统一查看

因此,基于以上种种原因,我们需要一个统一的日志分发中心,目前我们用的是 Fluentd,它是一个开源的日志收集器,并且已经集成到 Docker log-driver这里有它的简单使用文档,使用了 Fluentd 之后,整个日志流变成这样:

4. 微容器

微容器是指仅包含OS库和运行应用所需要的依赖以及应用本身,其他都不需要的容器。微容器是容器轻量化的产物,它有如下好处:

  1. 镜像很小,占用更低的磁盘空间,并且可以快速发布和部署
  2. 更小的镜像意味着更小的攻击面,基础 OS 就更安全

微容器我们主要要从两个方面着手: 轻量基础镜像和二进制部署。

得益于Docker的联合文件系统,通常我们平时在使用Dockerfile时,都很少去关心基础镜像的大小,只有在初次并且本地没有对应的基础镜像层时,才会重新从Hub拉取。因此为了方便,通常项目组的Dockerfile都是基于ubuntu这类OS镜像,导致镜像的体积动辄几百 M,比如ubuntu16.04大小为115M,而golang:1.9大小为735M,目前有很多精简的基础镜像,如scratch, alpine等,alpine是一个只有4M左右的轻型Linux发行版,目前大部分官方镜像都已经支持alphine 作为基础镜像,比如也有对应的go1.9-alpine轻量版,大小降低至286M。由于我们是二进制部署,因此我们直接用的alpine作为基础镜像。

二进制部署是指镜像中只存在二进制文件而不是整个源代码,这样一方面减少了镜像体积,另一方面提升了安全性。通常我们是在本地或者某个临时的”构建容器”中编译代码,然后将生成的二进制拷贝到”运行容器”中,这些方案要么很麻烦,要么不容易保证构建环境和运行环境的一致性,直到 Docker17.05引入了多段构建,可以在一个 Dockerfile 中完成之前需要多个 Dockerfile 才能完成的工作。如下是我们当前的 Dockerfile:

#  编译阶段
FROM golang:alpine AS compiler
ADD . /go/src/ngs
# 跑单元测试
RUN go test ngs/...
WORKDIR /go/src/ngs
# 二进制部署必须CGO_ENABLED=0,而race必须在CGO_ENABLED=1下才可用
# 参考: https://github.com/golang/go/issues/128440
RUN CGO_ENABLED=0 go build -o bin/game ngs/game

# 发布阶段
FROM alpine:latest
WORKDIR /tmp/
# https support
RUN apk add --no-cache ca-certificates apache2-utils
COPY --from=compiler /go/src/ngs/bin/game .
COPY --from=compiler /go/src/ngs/gdconf/others gdconf/others
ENTRYPOINT ["./game"]

相比之前的源码部署,精简了基础镜像,并且没有了源码和构建工具,镜像从之前的823M (基于go1.9基础镜像)缩小到32M。

5. 网络管理

目前我们的容器化部署全部使用 host 网络模式,主要是为了简单和高效,同时对 docker bridge/overlay 网络理解得还不是很深,这会导致容器与主机的网络有所关联。但是基于 Etcd 服务发现机制,容器仍然可以透明在不同主机上进行故障转移和重新部署,因此容器对主机的依赖相对较轻。至于是否需要用 docker bridge/overlay,还有待研究,目前我觉得Docker网络还是要和docker swarm结合起来才能发挥最大优势,我们并不打算用 docker swarm,因为GS大部分节点是强状态的,不能像docker service那样透明扩展,路由,和收缩,或者换句话将,每个节点就是都是一个 service,那这样的话,用了 docker swarm 的好处基本就只有故障转移了。因此我们主要还是单容器部署,每个 GS 容器需要手动配置部署在哪个host上,在之上做一些状态监控,如果容器挂了,可以自己做自动重启,但是如果物理机挂了,需要手动部署到其它host上,然后基于自己做的服务发现将容器加入集群,我将这种集群称为”半自动集群”。

6. 测试

我们的测试流程主要包含单元测试和集成测试,单元测试就是 go test,是源码级的白盒测试,单元测试我们放在 Dockerfile 中做,每次构建会跑所有的单元测试,如果有测试没跑通,则构建失败。集成测试则是基于机器人测试用例的黑盒测试,需要单独跑在一个容器中,以 exitcode 作为测试是否成功的标志,集成测试在服务器部署之后,如果集成测试失败,则整个部署工作流失败。

7. CI 工具

谈到持续集成(CI)工具,大部分项目组都用的 Jenkins,我们前期也用的 Jenkins, 结合 GitLab WebHook 去做自动构建部署。在开发环境中,QA,GD 都能够很快上手。Jenkins很灵活,这也可能导致项目组的集成流程中过度依赖于Jenkins本身,比如依赖Jenkins宿主机环境或路径,在Jenkins上写了很多临时脚本等,另外 Jenkins 缺乏对容器更好的支持,如镜像管理,容器监控等。就运维层面来说,Jenkins 只是个 Trigger + Scripts,离真正的 Ops 工具还差很远。因此目前运维基于 Docker API 搞了一套 Web 持续集成工具,将前面提到的”可运维”指标标准化,流程化。

从 CI 方面,保留 Trigger,WorkFlow,Stage 的概念:

  • Trigger: 触发器,比如代码提交,定时触发,手动触发
  • WorkFlow: 工作流,由 Trigger 触发,包含多个顺序执行的 Stage,比如某个工作流包含Build,Deploy,Test三个 Stage
  • Stage: 阶段,包含多个可并行执行的 Task,比如Test Stage 包含多个可并发执行的 Test,其中一个失败,则整个 Stage 失败
  • Task: 最小粒度的执行单位,目前支持构建,部署和测试

从 Docker 方面,集成 Docker Swarm 的 Cluster, Service, Task(这个 Task 是 Swarm 中的概念,相当于一个 Container)及部署策略等,支持单容器部署,同时,基于 Docker API 提供容器和物理机的管理,监控以及报警机制。基于 Docker Swarm,可以实现:

  • 资源管理: 集群/主机/容器/镜像的查看和管理
  • 监控报警: 集群/主机/容器的资源占用,阈值和报警机制
  • 扩容收缩: 主要针对无状态容器,可以手动扩容或收缩
  • 故障转移: 基于 Docker Service 的容器级和主机级的故障转移

这样,项目在开发过程中就可以通过这套工具去做持续集成,走运维标准的配置,部署,监控方案,项目在开发期间就去考虑和满足运维上的需求和规范,项目上线之后,服务器交付基本就是零成本的。