优步工程团队对容器化MySQL的应用

MySQL数据目录将从宿主机的文件系统直接挂载,这意味着Docker不会产生写操作的负担。不过我们会将MySQL的配置直接放入镜像中,使其成为常量。虽然可以更改配置,但由于Docker容器绝对不会重新使用,因此配置的改动也绝对不会生效。如果因为某些原因必须关闭一个容器,我们不会将这样的容器重新启动。我们会删除容器,使用最新镜像通过相同参数(如果目标状态有变化则使用新参数)重建一个容器,然后重新启动实例。

这样的做法为我们提供了多个收益:

  • 更易于控制配置漂移。有变化的最多只是Docker镜像的版本,并且我们会密切监视版本变化。
  • MySQL的升级过程更简单。我们会新建一个镜像,随后按顺序关闭老的容器。
  • 如果任何地方出错,只须重头再来。不再需要考虑该如何打补丁,抛弃原有的一切新建所需容器即可。

镜像的构建工作也是通过驱动无状态服务的同一个优步基础架构完成的。该基础架构会将镜像复制到所有数据中心,使其可在每个数据中心的本地注册表(Registry)中使用。

用一台宿主机运行多个容器也有相应的劣势。由于容器间无法进行恰当的I/O隔离,一个容器可能占用掉所有可用的I/O带宽,导致其他容器开始卡顿。Docker 1.10引入了I/O配额功能,但我们尚未对该功能进行过测试。目前我们主要通过避免超额订阅(Oversubscribing)宿主机,以及对每个数据库进行持续监控等方式降低这一问题的影响。

Docker容器的调度和拓扑的配置

在具备了可作为主数据库(Master)或从属数据库(Minion)配置并启动的Docker镜像后,需要通过某种方式启动这些容器并配置恰当的复制拓扑。为此我们在每台数据库宿主机上运行了一个代理(Agent)。该代理可以获取每台宿主机上应该具备的所有数据库的目标状态信息。一个典型的目标状态是类似这样的:

“schemadock01-mezzanine-mezzanine-us1-cluster8-db4”: {   “app_id”: “mezzanine-mezzanine-us1-cluster8-db4”,   “state”: “started”,   “data”: {     “semi_sync_repl_enabled”: false,     “name”: “mezzanine-us1-cluster8-db4”,     “master_host”: “schemadock30”,     “master_port”: 7335,     “disabled”: false,     “role”: “minion”,     “port”: 7335,     “size”: “all” }}

从这些信息中可以看出,宿主机schemadock01上通过7335端口运行了一个Mezzanine从属数据库,该数据库的主数据库位于schemadock30:7335。这个数据库的尺寸为“all”,意味着这是该宿主机上运行的唯一数据库,因此可以获得全部的可分配内存。

如何确定这样的目标状态,这是另一个完全不同的话题,随后我们还将撰文介绍,此处暂且不表,继续介绍下一个步骤:宿主机上运行的代理会接收这些信息,将其存储在本地,然后开始进行必要的处理。

这个处理过程实际上是一种无穷无尽的环路,每30秒进行一次,这有些类似于每30秒运行一次Puppet。该处理环路会通过下列操作检查目标状态与系统的实际状态是否匹配:

  1. 检查是否有一个容器已经在运行。如果没有,则使用配置创建一个并将其启动。
  2. 检查该容器是否应用了正确的复制拓扑。如果不正确,则尽量修复。
    • 如果本应是主数据库但实际为从属数据库,首先确认能否安全地更改其角色。为此我们会检查原本的主数据库是否为只读的,并且所有GTID均已收到并应用。一旦符合要求,即可安全地删除到原本主数据库的链接并启用写入。
    • 如果是主数据库但本应禁用,则开启只读模式。
    • 如果是从属数据库但复制未运行,则设置复制链接。
  3. 根据具体角色检查各种MySQL参数(read_onlysuper_read_onlysync_binlog等)。主数据库应当是可写的,从属数据库应当是只读的。此外我们会关闭binlog fsync以及其他类似参数[2]以降低从属数据库的负载。
  4. 启动或关闭任何其他用于提供支持的容器,例如pt-heartbeat和pt-deadlock-logger。

这里需要注意,我们会尽可能采用单进程、单用途容器这种做法。这样就无需重新配置运行中的容器,并且升级的过程也更易于控制。

如果任何一点出现错误,执行过程将抛出错误信息并将其忽略。整个过程会在下次运行时重试。我们会尽可能确保不同代理之间只需要最少量的协调。这意味着我们并不关心具体顺序,例如供应新集群时的供应顺序。如果用手工的方式供应新集群,可能需要执行类似下面的操作: