新浪微博混合云架构实践挑战之镜像分发实战

阅读数:1507 2016 年 6 月 6 日 17:38

编者按:《微博混合云架构》专栏是 InfoQ 向新浪微博技术团队的系列约稿,本专栏包含 8 篇内容,详细阐述以 DCP 设计理念为指导思想的混合云架构实践。本文是该系列的第四篇,主要讲解在新浪微博混合云在镜像分发方面的一些实践经验。

《微博混合云架构》专栏主要包括以下 8 篇内容:

  1. 混合云架构挑战与概述
  2. DCP 的不可变基础设施
  3. DCP 的弹性调度揭秘
  4. DCP 的镜像分发实战
  5. DCP 的容器编排设计与实践
  6. DCP 的服务发现
  7. DCP 的容量决策评估
  8. DCP 的监控体系

相信大家通过前面几篇的介绍,对微博 DCP 系统的架构已经有了一些了解,今天我们来聊一聊微博混合云在镜像分发方面的一些实践经验。

由于微博的业务特点,经常会面对突发的几倍于往常峰值的流量,比如重大新闻、娱乐圈的热点事件等。要想能相对平稳的应对这种峰值,我们必须具有快速且大批量甚至翻倍的扩容能力。要具备这种能力,除了要对整个系统的架构设计进行改造之外,对于我们的分发系统(无论是 Docker 还是非 Docker,扩容本质都是分发)也是一个很大的挑战。

整体架构

前几篇讲到,私有云(内网)和公有云(阿里云)是为了满足不同的场景,所以他们对于镜像仓库服务的需求也会有所不同:

  • 内网镜像仓库的压力主要来自日常发布以及弹性调度,带宽压力相对平稳。
  • 内网的环境要比阿里云复杂的多,各个业务间的操作系统版本,软件版本,配置等都存在很大差异。
  • 阿里云镜像仓库的压力是由当次需要扩容的机器数量决定的,无法预先估计出来。
  • 阿里云的环境是可定制的、纯净的、统一的。

基于以上几点,我们在内网和阿里云也采用了不同的部署架构,如下图:

为了消除差异,在内网和阿里云,使用了相同的域名,分别指向内网的 LVS 和阿里云的 SLB;其余的细节我们稍后再讲。

目前来看这个架构解决了我们面临的两个主要问题:

一:阿里云的分发能力问题

关于阿里云中的镜像分发,主要有两个痛点:

  • 镜像体积大:微博平台的业务以 Java 为主,众所周知,Java 的运行时环境是比较重的,我们一般的业务镜像体积都在 700M 以上。
  • 全量 Pull:每次扩容都是新的机器,所以每台机器都要拉取完整的镜像,而不是增量 Pull。

其实这两个痛点可以归为一点,就是扩容带来的带宽压力非常大。以扩容 50 台为例,估算一下总带宽消耗:50 * 700M = 35GB = 280Gb;理论上,1 台千兆网卡的机器可以在 280/60 ≈ 5 分钟内分发完,但实际上会出现大量 Pull 失败的情况。

为此,我们利用镜像仓库内置的 proxy 机制,构建了一套可以弹性扩容的多级缓存架构(上图中右侧部分)

  1. 我们将镜像仓库服务也作为一个特殊的服务池来管理,其操作系统镜像是定制的;
  2. 部署一台镜像仓库作为常备服务,同时也作为一级缓存,提高镜像仓库服务本身的扩容速度;
  3. 当需要在阿里云上进行大批量业务扩容时,会先按照一定比例扩容镜像仓库服务(这个比例是和镜像大小、机器硬件配置、所处网络环境等相关的,在我们的场景中是 1:20),作为二级缓存。

对镜像仓库的操作系统镜像,我们也做了一些优化,如下:

  • 支持指定一个镜像列表进行预热:可以通过环境变量 (docker run-e参数)将当次扩容需要的业务镜像列表传入镜像仓库容器内;这样,镜像仓库就可以支撑多种业务同时扩容;
  • 内置了 JDK,Tomcat 等常用的基础镜像,减少预热镜像的耗时;
  • 预热完毕后,自动调用阿里云 SLB 接口,添加后端节点,之后就可以对外提供服务了。

阿里云镜像仓库临时扩容区的生命周期如下图:

注意,预热这一步是很关键的,如果不预热,所有请求都会逐级回穿(而不是等待),结果可想而知。

备注1:20 是反复测试后得出的比例——即在只有 1 台镜像仓库的情况下,最大能供 20 台机器同时拉取 700M 的镜像,时间在 2 分钟以内;数量再多的话,就会出现拉取失败的情况。

目前,这套架构保证了在阿里云中 10 分钟扩容 1000 个节点的能力,并且同时降低了成本和专线的带宽压力。

二:内网环境不统一问题

由于 Docker 版本的更新非常快,且向后兼容性不够好。随着使用 Docker 的时间越来越长,我们的生产环境上运行的 Docker 版本也越来越多,从 1.2 到 1.8 都有。在 Docker 1.6 发布时,原有的镜像仓库项目(docker-registry)被标记为 Deprecated,迁移到了新的 distribution 项目,它使用 Go 实现,API 也和原来有很大的不同。Docker 1.6 以下的版本只支持旧版的 API,1.6 及以上的版本默认使用新的 API 和镜像仓库交互,并支持 fallback 到旧版 API。

为此,我们同时保留了 docker-registry 和 distribution 两个服务,并使用 docker-compose 编排了一组能够和我们已有的所有版本 Docker 正常交互的服务,大致的结构如下:

如图中所示,在 Nginx 层通过配置将来自不同版本 Docker 的请求转发给相应的后端;为了避免镜像同步的问题,拒绝来自 Docker 1.6 以下的所有 push 请求,同时利用内置的 notification 机制实现镜像的自动同步(单向)。

对于存储层,我们对比了 Glusterfs、Swift、Ceph 之后,最后选用了 Ceph。原因有几个:配置简单,社区更活跃,支持块存储。在保证了高吞吐量的同时,也解决了单点问题,一旦 distribution 服务器出现问题,只要 Ceph 还在,就可以快速重建出来。

这里值得一提的是,distribution 本身就是一个 Ceph 客户端,可以直接和 Ceph 交互,配置也很简单;而 docker-registry 则需要通过 ceph-gateway 来访问 Ceph,配置要复杂一些。

备注最新版本的 distribution 中已经把 Ceph 相关的配置和 rados 驱动代码都移除了,只能通过部署一套 Swift gateway 的方式来间接访问 Ceph。

Docker 及其工具本身的问题

当然,除了上面两个和微博环境相关的问题之外,还有一些 Docker 本身及其周边工具的问题。

上面提到,Docker 的版本更新非常快,而且经常有颠覆性的更新(比如 1.6 和 1.10),这也反映出整个 Docker 社区是比较激进的,所以很多使用上的细节的东西需要我们在实践中一点点去积累经验,同时关注 Docker 社区的动态,才能更加得心应手的使用这项技术。

这里列举几个我们碰到的相对比较重大的问题(和镜像仓库相关的):

proxy 机制的缓存失效问题

先来看一下镜像仓库 proxy 机制(官方一般叫 pull through cache)的原理:

一个 distribution 可以被配置为另一个 distribution(官方或私有都可以)的 proxy(只读的,即只能 pull,不能 push),在 config.yml 中添加如下配置即可:

proxy:
  remoteurl: http://10.10.10.10

配置好之后,当一个 Docker pull 请求过来时,会检查本地是否已经存在被请求的镜像;如果没有,则会穿透到配置的后端,并同时缓存在本地,这样后面的请求就不会再穿透了。

缓存默认的有效期为一周(可通过修改代码的方式调整,需要重新编译);超过有效期之后,会由一个 scheduler 协程来删除被缓存镜像的所有 layer 的文件及元数据。当一个镜像缓存过期之后,这个镜像的 pull 都会失败,并提示 image not found。

问题的原因在于 distribution 检测缓存有效的逻辑有问题,具体这里就不详述了,感兴趣的同学可以参考这个 issue 或者阅读源代码。

官方就这个问题做过一次修复,但并不彻底。在官方彻底修复之前,我们的解决办法是,将缓存有效期设置的长一点(一个月),同时定期(一个月)清除一次缓存目录,再重启 distribution 容器。

使用 HTTPS 协议时,X-Forwarded-Proto 头的设置问题

有时候,出于安全或使用规范等原因,我们会自己搭建一套支持 HTTPS 协议访问的镜像仓库服务;业界普遍的做法是把证书配置在 Nginx 层,并且转换为 HTTP 协议,再传给后端,我们也是这么做的。

这里有一点需要注意:原理上,无论是 Docker pull 还是 push,其实都是一系列的 HTTP 请求。而对于 Docker push,distribution 会根据”X-Forwarded-Proto”这个 Header 值来判断下一次返回给 client 的 Location 是 HTTP 协议还是 HTTPS 协议。所以要保证在请求到达 distribution 端时,X-Forwarded-Proto 头的值是正确的,是客户端最开始发起请求的协议。否则在开启镜像仓库的权限控制后,会有 push 失败的情况。

当整个 HTTP 调用栈中存在多层 Nginx 或类似的反向代理程序时,尤其要注意这个问题。比如,在我们的两层 Nginx 中,分别是这样配置的:

LVS 层:

location / {
    ...
    proxy_set_header X-Forwarded-Proto $scheme;
    ...
}

docker-compose 层:

location / {
  ...
    # proxy_set_header  X-Forwarded-Proto $scheme;
    ...
}

使用了阿里云 SLB 之后,docker pull 会等待一段时间才开始拉取镜像

在将阿里云的 registry 域名指向 SLB 之后,发现一个奇怪的现象,每次从域名去 docker pull 都会等待一段时间才开始拉取镜像,而直接按 IP 拉取则没有问题,如下:

docker pull registry.api.weibo.com/busybox:latest // 等待大约 20 秒才开始下载镜像 
docker pull 10.75.0.52/busybox:latest // 正常

经过一番调查,发现 Docker daemon 在收到 pull 命令后,会先检测指定的地址是否是一个合法的服务提供者;检测时,会按照 https+v2、https+v1、http+v2、http+v1 的顺序逐个请求;而阿里云的 SLB 对于未监听的端口,默认行为是不回包,所以客户端只能等待超时。

我们的解决办法是给 SLB 添加了 TCP 的 443 端口,这样客户端很快就知道这个 distribution 并不提供 HTTPS 服务,从而立即 fallback 到 HTTP 协议。

此问题在 Docker 1.6.2 版本中存在,至于后面的版本有没有做修复,有兴趣的同学可以自行试验。

总结

镜像仓库服务作为微博混合云基础设施中的一部分,其分发能力和稳定性至关重要。我们采取的架构方案是综合考虑了微博的业务特点、成本、历史遗留问题等诸多因素而设计的,并不一定适合于大家。同时,为了解决需要先扩容镜像仓库的问题,缩短总扩容耗时,我们也在开发一套基于 BitTorrent 协议的镜像分发方案,希望能和业界同行多交流学习。


感谢魏星对本文的策划和审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

评论

发布