在优步工程Dockerizing MySQL

0
在优步工程Dockerizing MySQL

超级工程的无模式存储系统为优步一些最大的服务提供动力,比如夹层.Schemaless是一个可伸缩的、高可用的数据存储MySQL¹集群。当我们有16个集群时,管理这些集群相当容易。现在,我们有1000多个集群,包含4000多个数据库服务器,这需要不同类型的工具。

最初,我们所有的集群都由木偶例如,大量的临时脚本,以及无法按照优步的速度扩展的手动操作。当我们开始寻找一种更好的方法来管理越来越多的MySQL集群时,我们有几个基本的需求:

  • 在每台主机上运行多个数据库进程
  • 自动化的一切
  • 是否有一个单一入口点来管理和监控所有数据中心的所有集群

我们想出的解决方案是一种叫做Schemadock的设计。我们运行MySQL码头工人容器,由在配置文件中定义集群拓扑的目标状态管理。集群拓扑指定MySQL集群应该是什么样子;例如,应该有一个集群a,其中有3个数据库,哪个应该是主数据库。然后代理程序将这些拓扑应用于各个数据库。集中式服务维护和监视每个实例的目标状态,并对任何偏差做出反应。

Schemadock有很多组件,Docker是一个很小但很重要的组件。切换到更具可伸缩性的解决方案是一项重大工作,本文将解释Docker是如何帮助我们实现这一目标的。

为什么要选择Docker ?

运行容器化进程可以更容易地在同一主机上运行不同版本和配置的多个MySQL进程。它还允许我们在相同的主机上部署小型集群,这样我们就可以在更少的主机上运行相同数量的集群。最后,我们可以删除对Puppet的任何依赖,并将所有主机配置为相同的角色。

至于Docker本身,工程师们现在在Docker中构建我们所有的无状态服务。这意味着我们有很多关于Docker的工具和知识。Docker绝不是完美的,但目前它比其他替代品要好。

为什么不用Docker?

Docker的替代方案包括完全虚拟化,LXC容器,并直接在主机上管理MySQL进程,例如通过Puppet。对我们来说,选择Docker是相当简单的,因为它适合我们现有的基础设施。然而,如果你还没有运行Docker,那么仅仅是为MySQL做这件事就会是一个相当大的项目:你需要处理映像构建和分发、监控、升级Docker、日志收集、网络等等。

所有这些都意味着,只有在愿意在Docker上投入大量资源的情况下,才应该使用Docker。此外,Docker应该被视为一项技术,而不是解决所有问题的解决方案。在Uber,我们做了一个精心的设计,把Docker作为一个更大的系统中的一个组件来管理MySQL数据库。然而,并不是所有公司的规模都和优步一样,对他们来说,更直接的设置是像Puppet或Ansible也许更合适。

无模式MySQL Docker镜像

在它的基础上,我们的Docker映像只需要下载和安装Percona服务器并开始mysqld-这或多或少有点像现有的Docker MySQL映像。然而,在下载和开始之间,会发生一些其他事情:

  • 如果挂载的卷中没有现有数据,那么我们就知道处于引导场景中。对于一个大师,跑mysql_install_db并创建一些默认用户和表。对于一个奴才,从备份节点或集群中的其他节点发起数据同步。
  • 一旦容器有了数据,mysqld就会启动。
  • 如果任何数据复制失败,容器将再次关闭。

容器的角色是使用环境变量配置的。这里有趣的是,角色只控制如何检索初始数据——Docker映像本身不包含任何设置复制拓扑、状态检查等的逻辑。由于这个逻辑的变化比MySQL本身要频繁得多,因此将其分离是很有意义的。

MySQL数据目录是从主机文件系统中挂载的,这意味着Docker不会引入任何写入开销。但是,我们确实将MySQL配置烘焙到映像中,这基本上使它是不可变的。虽然你可以改变配置,但它永远不会生效,因为我们从来没有重用Docker容器。如果一个容器因为某种原因关闭了,我们不会重新启动它。我们删除容器,从最新的映像中使用相同的参数创建一个新的容器(如果目标状态已经改变,则创建一个新的),然后启动该容器。

这样做会给我们带来很多好处:

  • 配置漂移更容易控制。它可以归结为Docker映像版本,我们会积极监控。
  • 升级MySQL很简单。我们建立一个新的形象,然后有序地关闭集装箱。
  • 如果有东西坏了,我们就重新开始。而不是试图修补事情,我们只是放弃我们所拥有的,让新的容器接管。

通过支持无状态服务的相同的Uber基础设施来构建映像。相同的基础设施跨数据中心复制映像,使它们在本地注册中心中可用。

在同一个主机上运行多个容器有一个缺点。由于容器之间没有适当的I/O隔离,因此一个容器可能会使用所有可用的I/O带宽,从而导致其余容器无法使用。Docker 1.10引入了I/O配额,但我们还没有试验过。目前,我们通过不过度订阅主机和持续监控每个数据库的性能来解决这个问题。

Docker容器调度和拓扑配置

现在我们有了一个可以启动和配置为主或从属的Docker映像,需要一些东西来实际启动这些容器并将它们配置到正确的复制拓扑中。为此,在每个数据库主机上运行一个代理。代理接收应该在各个主机上运行的所有数据库的目标状态信息。典型的目标状态是这样的:

" schemadock01-mezzanine-mezzanine-us1-cluster8-db4 ": {

:“app_id mezzanine-mezzanine-us1-cluster8-db4”,

“状态”:“开始”,

"数据":{

“semi_sync_repl_enabled”:假的,

“名称”:“mezzanine-us1-cluster8-db4”,

:“master_host schemadock30”,

“master_port”:7335年,

“禁用”:假的,

“角色”:“奴才”,

“端口”:7335年,

“大小”:“所有”

这告诉我们,我们应该在主机schemadock01上运行一个夹层端口7335上的数据库随从,它应该将运行在schemadock30:7335上的数据库作为主数据库。它的大小为“all”,这意味着它是该主机上运行的唯一数据库,因此应该将所有内存分配给它。

如何创建这个目标状态是另一篇文章的主题,因此我们将跳过接下来的步骤:在主机上运行的代理接收它,在本地存储它,并开始处理它。

处理实际上是一个每30秒运行一次的无限循环,有点像每30秒运行一次Puppet。处理循环通过以下操作检查目标状态是否与系统的实际状态匹配:

  1. 检查容器是否已经在运行。如果没有,用配置创建一个并启动它。
  2. 检查容器复制拓扑是否正确。如果不是,试着修复它。
    • 如果它是一个从属角色,但应该是一个主角色,请验证它是安全的,可以更改为主角色。我们通过检查旧的master是只读的来做到这一点GTIDs已收到并申请。一旦出现这种情况,就可以安全地删除到旧master的链接并启用写操作。
    • 如果它是主模式,但应该禁用,则打开只读模式。
    • 如果它是一个随从,但是复制没有运行,那么就建立复制链路。
  3. 检查各种MySQL参数(read_only而且super_read_onlysync_binlog等等)。master应该是可写的,minions应该是read_only的,等等。此外,我们通过关闭binlog fsync和其他类似的参数²来减少minions的负载
  4. 启动或关闭任何支持容器,例如pt-heartbeat而且pt-deadlock-logger

请注意,我们非常赞同单进程、单用途容器的想法。这样我们就不需要重新配置正在运行的容器,而且更容易控制升级。

如果在任何时候发生错误,流程都会引发一个错误并终止。然后在下一次运行中重试整个过程。我们确保每个特工之间的协调尽可能少。这意味着我们不关心顺序,例如,在配置一个新集群时。如果你手动配置一个新的集群,你可能会这样做:

  1. 创建MySQL主服务器并等待它就绪
  2. 创建第一个随从,并将其连接到主服务器
  3. 重复其余的仆从

当然,这样的事情终究会发生。我们不关心的是显式的顺序。我们只需创建反映我们想要达到的最终状态的目标状态:

" schemadock01-mezzanine-cluster1-db1 ": {

"数据":{

“禁用”:假的,

“角色”:“大师”,

“端口”:7335年,

“大小”:“所有”

},

" schemadock02-mezzanine-cluster1-db2 ": {

"数据":{

:“master_host schemadock01”,

“master_port”:7335年,

“禁用”:假的,

“角色”:“奴才”,

“端口”:7335年,

“大小”:“所有”

},

" schemadock03-mezzanine-cluster1-db3 ": {

"数据":{

:“master_host schemadock01”,

“master_port”:7335年,

“禁用”:假的,

“角色”:“奴才”,

“端口”:7335年,

“大小”:“所有”

这将以随机顺序推送到相关代理,他们都开始工作。要达到目标状态,可能需要多次重试,具体取决于顺序。通常情况下,目标状态在几次重试后就能达到,但有些操作实际上可能需要100次重试。例如,如果随从首先开始处理,那么他们将无法连接到主服务器,他们必须稍后重试。由于主服务器启动和运行可能需要一些时间,因此从属服务器可能需要重试很多次:

一个2个随从在主服务器之前启动的例子。在初始启动时(步骤1和2),minions将无法从主服务器获取快照,这将导致启动过程失败。然后主服务器在步骤3中启动,而随从服务器能够在步骤4和5中连接和同步数据。
一个2个随从在主服务器之前启动的例子。在初始启动时(步骤1和2),minions将无法从主服务器获取快照,这将导致启动过程失败。然后主服务器在步骤3中启动,而随从服务器能够在步骤4和5中连接和同步数据。

有使用Docker Runtime的经验

我们的大多数主机运行Docker 1.9.1devicemapperLVM用于存储。使用LVMdevicemapper表现得比devicemapper回送。devicemapper在性能和可靠性方面有许多问题,但替代方案如汪汪汪而且OverlayFS也有很多问题³.这意味着社区中对于最佳存储选项存在很多困惑。到目前为止,OverlayFS获得了很大的吸引力,似乎已经稳定下来,所以我们将切换到它,并升级到Docker 1.12.1。

升级Docker的一个痛点是它需要重新启动,这也会重新启动所有的容器。这意味着必须控制升级过程,以便在升级主机时不运行master。希望Docker 1.12将是我们必须关心的最后一个版本;1.12提供了重启和升级Docker守护进程的选项,而无需重新启动容器。

每个版本都有许多改进和新功能,同时引入了相当数量的错误和倒退。1.12.1似乎比以前的版本更好,但我们仍然面临一些限制:

  • 码头工人检查在Docker运行了几天之后,有时会挂起。
  • 使用桥接网络用户代理导致TCP连接终止时的奇怪行为。客户端连接有时从未接收到RST信号,并且无论您配置何种超时都保持打开状态。
  • 容器进程偶尔会被重定向到pid 1 (init),这意味着Docker会失去对它们的跟踪。
  • 我们经常看到Docker守护进程需要很长时间来创建新容器的情况。

总结

我们提出了Uber存储集群管理的几个要求:

  1. 运行在同一主机上的多个容器
  2. 自动化
  3. 只有一个入口

现在,我们可以通过简单的工具和单一的UI进行日常维护,其中任何一个都不需要直接访问主机:

从我们的管理控制台截图。从这里开始,我们可以跟踪目标状态的进展,在本例中,我们先添加第二个集群,然后切断复制链路,从而将集群拆分为两个。
从我们的管理控制台截图。从这里开始,我们可以跟踪目标状态的进展,在本例中,我们先添加第二个集群,然后切断复制链路,从而将集群拆分为两个。

通过在每个主机上运行多个容器,我们可以更好地利用主机。我们可以以可控的方式进行整个舰队的升级。使用Docker让我们很快就达到了这一点。Docker还允许我们在本地测试环境中运行一个完整的集群设置,并尝试所有的操作过程。

我们在2016年初开始向Docker迁移,到目前为止,我们运行着大约1500个Docker生产服务器(仅用于MySQL),我们已经配置了大约2300个MySQL数据库。

Schemadock还有很多功能,但Docker组件对我们的成功有很大的帮助,它允许我们快速移动和试验,同时还可以连接到现有的Uber基础设施。整个旅行商店每天接收数百万次旅行,现在与其他商店一起运行在Dockerized MySQL数据库上。换句话说,Docker已经成为使用优步出行的关键部分。

Joakim Recht是Uber工程公司奥胡斯办公室的软件工程师,也是Schemaless基础设施自动化的技术主管。

图片来源:座头鲸-新翅目Sylke Rohrlach,根据CC-BY 2.0.图像裁剪的头部尺寸和颜色校正。


¹准确地说,是Percona Server 5.6

²Sync_binlog = 0而且Innodb_flush_log_at_trx_commit = 2

³一个小的问题选择:https://github.com/docker/docker/issues/16653https://github.com/docker/docker/issues/15629https://developer雷竞技到底好不好用blog.redhat.com/2014/09/30/overview-storage-scalability-docker/https://github.com/docker/docker/issues/12738

评论