球是一个投资者交流的社区,用户可以在上面买卖股票,代销基金等各种金融衍生业务,同时也可以通过雪盈证券来进行沪、深、港、美股的交易。
为什么要引入 Docker
随着业务的发展,不同的社区业务之间所受到影响的概率正在逐渐升高,因此我们希望各个业务之间既能够不被打扰,又能在资源上、机器间、甚至网络上根据监管的要求予以不同层面的隔离。
早在 2014 年时,我们就发现容器技术具有本身镜像小、灵活、启动速度快等特点,而且在性能上比较适合于我们当时物理机不多的规模环境。
相比而言,传统的虚拟化技术不但实现成本高,而且性能损耗也超过 10%。因此,基于对镜像大小、启动速度、性能损耗、和隔离需求的综合考虑,我们选用了两种容器引擎:LXC 和 Docker。
我们把 MySQL 之类有状态的服务放在 LXC 里;而将线上业务之类无状态的服务放在 Docker 中。
容器使用方法
众所周知,Docker 是以类似于单机的软件形态问世的,最初它的宣传口号是:Build/Ship/Run。
因此它早期的 Workflow(流程)是:
- 在一台 Host 主机上先运行 Docker Build。
- 然后运用 Docker Pull,从镜像仓库里把镜像拉下来。
- 最后使用 Docker Run,就有了一个运行的 Container。
需要解决的问题
上述的流程方案伴随着如下有待解决的问题:
- 网络连通性,由于是单机软件,Docker 最初默认使用的是 Bridge 模式,不同宿主机之间的网络并不相通。因此,早期大家交流最多的就是如何解决网络连通性的问题。
- 多节点的服务部署与更新,在上马该容器方案之后,我们发现由于本身性能损耗比较小,其节点的数量会出现爆炸式增长。 因此,往往会出现一台物理机上能够运行几十个节点的状况。容器节点的绝对数量会比物理节点的数量大一个数量级,甚至更多。那么这么多节点的服务部署与更新,直接导致了工作量的倍数增加。
- 监控,同时,我们需要为这么多节点的运行状态采用合适的监控方案。
Docker 在雪球的技术实践
网络模式
首先给大家介绍一下我们早期的网络解决方案:在上图的左边,我们默认采用的是 Docker 的 Bridge 模式。
大家知道,默认情况下 Docker 会在物理机上创建一个名为 docker0 的网桥。
每当一个新的 Container 被创建时,它就会相应地创建出一个 veth,然后将其连到容器的 eth0 上。
同时,每一个 veth 都会被分配到一个子网的 IP 地址,以保持与相同主机里各个容器的互通。
由于在生产环境中不只一张网卡,因此我们对它进行了改造。我们产生了一个“网卡绑定”,即生成了 bond0 网卡。我们通过创建一个 br0 网桥,来替换原来的 docker0 网桥。
在该 br0 网桥中,我们所配置的网段和物理机所处的网段是相同的。由于容器和物理机同处一个网段,因此核心上联的交换机能够看到该容器和不同宿主机的 MAC 地址。这就是一个网络二层互通的解决方案。 该网络模式具有优劣两面性:
- 优点:由于在网络二层上实现了连接互通,而且仅用到了内核转发,因此整体性能非常好,与物理机真实网卡的效率差距不大。
- 缺点:管理较为复杂,需要我们自己手动的去管理容器的 IP 和 MAC 地址。 由于整体处于网络大二层,一旦系统达到了一定规模,网络中的 ARP 包会产生网络广播风暴,甚至会偶发出现 PPS(Package Per Second)过高,网络间歇性不通等奇怪的现象。 由于处于底层网络连接,在实现网络隔离时也较为复杂。
服务部署
对于服务的部署而言,我们最初沿用虚拟机的做法,将容器启动起来后就不再停下了,因此:
- 如果节点需要新增,我们就通过 Salt 来管理机器的配置。
- 如果节点需要更新,我们就通过 Capistrano 进行服务的分发,和多个节点的部署操作,变更容器中的业务程序。
其中,优势为:
- 与原来的基础设施相比,迁移的成本非常低。由于我们通过复用原来的基础设施,直接将各种服务部署在原先的物理机上进行,因此我们很容易地迁移到了容器之中。而对于开发人员来说,他们看不到容器这一层,也就如同在使用原来的物理机一样,毫无“违和感”。
- 与虚拟机相比,启动比较快,运行时没有虚拟化的损耗。
- 最重要的是一定程度上满足了我们对于隔离的需求。
而劣势则有:
- 迁移和扩容非常繁琐。例如:当某个服务需要扩容时,我们就需要有人登录到该物理机上,生成并启动一个空的容器,再把服务部署进去。此举较为低效。
- 缺乏统一的平台进行各种历史版本的管理与维护。我们需要通过文档来记录整个机房的容器数量,和各个容器的 IP/MAC 地址,因此出错的可能性极高。
- 缺少流程和权限的控制。我们基本上采用的是原始的管控方式。
自研容器管理平台
面对上述缺点,我们需要自行研发一个容器管理平台,去管理各种物理机、容器、IP 与 MAC 地址、以及进行流程控制。 因此我们变更的整套发布流程为:
- 由开发人员将代码提交到代码仓库(如 Github)之中。
- 触发一个 Hook 去构建镜像,在构建的同时做一些 CI(持续集成),包括静态代码扫描和单测等。
- 将报告附加到镜像的信息里,并存入镜像仓库中。
- 部署测试环境。
- 小流量上线,上线之后,做一些自动化的 API Diff 测试,以判断是否可用。
- 继续全量上线。
镜像构建
有了容器管理平台,就会涉及到镜像的自动构建。和业界其他公司的做法类似,我们也使用的是基于通用操作系统的镜像。
然后向镜像中添加那些我们公司内部会特别用到的包,得到一个通用的 base 镜像,再通过分别加入不同语言的依赖,得到不同的镜像。
每次业务版本发布,将代码放入相应语言的镜像即可得到一个业务的镜像。构建镜像的时候需要注意尽量避免无用的层级和内容,这有助于提升存储和传输效率。
系统依赖
我们的这一整套解决方案涉及到了如下周边的开源项目与技术:
负载均衡
由于会频繁发生节点的增减,我们该如何通过流量的调度和服务的发现,来实现自动加入负载均衡呢?对于那些非 Http 协议的 RPC,又该如何自动安全地摘掉某个节点呢?
我们在此使用了 Nginx+Lua(即 OpenResty),去实现逻辑并动态更改 Upstream。
当有节点启动时,我们就能够将它自动注册与加入;而当有节点被销毁时,也能及时将其摘掉。
同时,我们在内部使用了 Finagle 作为 RPC 的框架,并通过 ZooKeeper 实现了服务的发现。
日志收集
由于节点众多,我们需要进行各种日志的收集。在此,我们大致分为两类收集方式:
- 一类是 Nginx 这种不易侵入代码的,我们并没有设法去改变日志的流向,而是让它直接“打”到物理机的硬盘上,然后使用 Flume 进行收集,传输到 Kafka 中。
- 另一类是我们自己的业务。我们实现了一个 Log4 Appender,把日志直接写到 Kafka,再从 Kafka 转写到 ElasticSearch 里面。
网络模式
在该场景下,我们采用的是上述提到的改进后的 Bridge+Host 模式。
监控系统
监控系统由上图所示的几个组件所构成。它将收集(Collector)到的不同监控指标数据,传输到 Graphite 上,而 Grafana 可读取 Graphite 的信息,并用图形予以展示。
同时,我们也根据内部业务的适配需要,对报警组件 Cabot 进行了改造和定制。 此时我们的平台已经与虚拟机的用法有了较大的区别。如上图所示,主要的不同之处体现在编译、环境、分发、节点变更,流程控制、以及权限控制之上。我们的用法更具自动化。 由于是自行研发的容器管理平台,这给我们带来的直接好处包括:
- 流程与权限的控制。
- 代码版本与环境的固化,多个版本的发布,镜像的管理。
- 部署与扩容效率的大幅提升。
但是其自身也有着一定的缺点,包括:
- 在流程控制逻辑,机器与网络管理,以及本身的耦合程度上都存在着缺陷。因此它并不算是一个非常好的架构,也没能真正实现“高内聚低耦合”。
- 由于是自研的产品,其功能上并不完善,没能实现自愈,无法根据新增节点去自动选择物理机、并自动分配与管理 IP 地址。
引入 Swarm
2015 年,我们开始着手改造该容器管理平台。由于该平台之前都是基于 DockerAPI 构建的。
而 Swarm 恰好能对 Docker 的原生 API 提供非常好的支持,因此我们觉得如果引入 Swarm 的话,对于以前代码的改造成本将会降到最低。 那么我们该如何对原先的网络二层方案进行改造呢?如前所述,我们一直实现的是让容器的 IP 地址与物理机的 IP 地址相对等。
因此并不存在网络不通的情况。同时,我们的 Redis 是直接部署在物理机上的。
所以依据上图中各个列表的对比,我们觉得 Calico 方案更适合我们的业务场景。 因此,我们在上层使用 Rolling 来进行各种流程的操作,中下层则用 Swarm+Calico 来予以容器和网络的管理。 Calico 使用的是 DownwardDefault 模式,该模式通过运用 BGP 协议,来实现对于不同机器之间路由信息的分发。
在默认情况下,Calico 是 Node 与 Node 之间的 Mesh 方式,即:任意两个 Node 之间都有着 BGP 连接。
当我们在一台物理机上启动了某个容器之后,它就会添加一条包含着从容器 IP 地址到物理机的路由信息。
由于多台物理机同处一个 Mesh,那么每一台机器都会学习到该路由信息。而随着我们系统规模的逐渐增大,每一台物理机上的路由表也会相应地增多,这就会影响到网络的整体性能。
因此我们需要采用这种 Downward Default 部署模式,使得不必让每台物理机都拥有全量的路由表,而仅让交换机持有便可。
众所周知,BGP 会给每一台物理机分配一个 AS(自治域是 BGP 中的一个概念)号,那么我们就可以给各台物理机都分配相同的 AS 号。
而给它们的上联交换机分配另一个 AS 号,同时也给核心交换机再分配第三种 AS 号。
通过此法,每一台物理机只会和自己上联的交换机做路由分发,那么当有一个新的节点启动之后,我们便可以将这条路由信息插入到该节点自己的路由表中,然后再告知与其相连的上联交换机。
上联交换机在学习到了这条路由之后,再进一步推给核心交换机。
总结起来,该模式的特点是:
- 单个节点不必知道其他物理机的相关信息,它只需将数据包发往网关便可。因此单台物理机上的路由表也会大幅减少,其数量可保持在“单机上的容器数量 +一个常数(自行配置的路由)”的水平上。
- 每个上联交换机只需掌握自己机架上所有物理机的路由表信息。
- 核心交换机则需要持有所有的路由表。而这恰是其自身性能与功能的体现。
当然,该模式也带来了一些不便之处,即:对于每一个数据流量而言,即使目标 IP 在整个网络中并不存在,它们也需要一步一步地向上查询直到核心交换机处,最后再判断是否真的需要丢弃该数据包。
后续演进
在此之后,我们也将 DevOps 的思想和模式逐步引入了当前的平台。具体包括如下三个方面:
- 通过更加自助化的流程,来解放运维。让开发人员自助式地创建、添加、监控他们自己的项目,我们只需了解各个项目在平台中所占用的资源情况便可,从而能够使得自己的精力更加专注于平台的开发与完善。
- 如今,由于 Kubernetes 基本上已成为了业界的标准,因此我们逐步替换了之前所用到的 Swarm,并通过 Kubernetes 来实现更好的调度方案。
- 支持多机房和多云环境,以达到更高的容灾等级,满足业务的发展需求,并完善集群的管理。
上图展示了一种嵌套式的关系:在我们的每一个 Project 中,都可以有多个 IDC。
而每个 IDC 里又有着不同的 Kubernetes 集群。同时在每一个集群里,我们为每一个项目都分配了一个 Namespace。
根据不同的环境,这些项目的 Namespace 会拥有不同的 Deployment。例如想要做到部署与发布的分离,我们就相应地做了多个 Deployment,不同的 Deployment 标示不同的环境。
默认将流量引入第一个 Deployment,等到第二个 Deployment 被部署好以后,需要发布的时候,我们再直接把流量“切”过去。
同时,鉴于我们的平台上原来就已经具有了诸如日志、负载均衡、监控之类的解决方案。
而 Kubernetes 本身又是一个较为全面的解决方案,因此我们以降低成本为原则,谨慎地向 Kubernetes 进行过渡,尽量保持平台的兼容性,不至让开发人员产生“违和感”。 如今,我们的容器只有一千多个,项目数量大概有一百多个。但是我们在部署效率方面的提升还是非常显著的,我们的几十个开发人员每个月所发布的次数就能达到两千多次,每个交易日的日志量大概有 1.5T。