知乎十万级容器规模的分布式镜像仓库实践

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 知乎在 2016 年已经完成了全量业务的容器化,并在自研容器平台上以原生镜像的方式部署和运行,并在后续陆续实施了 CI、Cron、Kafka、HAProxy、HBase、Twemproxy 等系列核心服务和基础组件的容器化,本篇文章分享知乎在镜像仓库这个容器技术核心组件的生产实践。

image

知乎在 2016 年已经完成了全量业务的容器化,并在自研容器平台上以原生镜像的方式部署和运行,并在后续陆续实施了 CI、Cron、Kafka、HAProxy、HBase、Twemproxy 等系列核心服务和基础组件的容器化。知乎既是容器技术的重度依赖者,也是容器技术的深度实践者,我们会陆续把容器技术的实践经验通过专栏和大家进行分享,本篇文章分享知乎在镜像仓库这个容器技术核心组件的生产实践。

基础背景

容器的核心理念在于通过镜像将运行环境打包,实现“一次构建,处处运行”,从而避免了运行环境不一致导致的各种异常。在容器镜像的发布流程中,镜像仓库扮演了镜像的存储和分发角色,并且通过 tag 支持镜像的版本管理,类似于 Git 仓库在代码开发过程中所扮演的角色,是整个容器环境中不可缺少的组成部分。

镜像仓库实现方式按使用范围可以分为 Docker Hub 和 Docker Registry 两类,前者是在公网环境下面向所有容器使用者开放的镜像服务,后者是供开发者或公司在内部环境下搭建镜像仓库服务,由于公网下载镜像的网络带宽、延迟限制以及可控性的角度考虑,在私有云环境下通常需要采用 Docker Registry 来搭建自己的镜像仓库服务。

Docker Registry 本身开源,当前接口版本为 V2 (以下描述均针对该版本),支持多种存储后端,如:

◾InMemory:A temporary storage driver using a local inmemory map. This exists solely for reference and testing.

◾FileSystem:A local storage driver configured to use a directory tree in the local filesystem.

◾S3:A driver storing objects in an Amazon Simple Storage Service (S3) bucket.

◾Azure:A driver storing objects in Microsoft Azure Blob Storage.

◾Swift:A driver storing objects in Openstack Swift.

◾OSS:A driver storing objects in Aliyun OSS.

◾GCS:A driver storing objects in a Google Cloud Storage bucket.

默认使用本地磁盘作为 Docker Registry 的存储,用下面的配置即可本地启动一个镜像仓库服务:

$ docker run -d \
  -p 5000:5000 \
  --restart=always \
  --name registry \
  -v /mnt/registry:/var/lib/registry \
  registry:2

生产环境挑战

很显然,以上面的方式启动的镜像仓库是无法在生产环境中使用的,问题如下:

1.性能问题:基于磁盘文件系统的 Docker Registry 进程读取延迟大,无法满足高并发高吞吐镜像请求需要,且受限于单机磁盘,CPU,网络资源限制,无法满足上百台机器同时拉取镜像的负载压力。

2.容量问题:单机磁盘容量有限,存储容量存在瓶颈。知乎生产环境中现有的不同版本镜像大概有上万个,单备份的容量在 15T 左右,加上备份这个容量还要增加不少。

3.权限控制:在生产环境中,需要对镜像仓库配置相应的权限认证。缺少权限认证的镜像仓库就如同没有认证的 Git 仓库一样,很容易造成信息泄露或者代码污染。

知乎的生产环境中,有几百个业务以及几万个容器运行在我们的容器平台上,繁忙时每日创建容器数近十万,每个镜像的平均大小在 1G 左右,部署高峰期对镜像仓库的压力是非常大的,上述性能和容量问题也表现的尤为明显。

知乎解决方案

为了解决上述的性能和容量等问题,需要将 Docker Registry 构造为一个分布式服务,实现服务能力和存储容量的水平扩展。这其中最重要的一点是为 Docker Registry 选择一个共享的分布式存储后端,例如 S3,Azure,OSS,GCS 等云存储。这样 Docker Registry 本身就可以成为无状态服务从而水平扩展。实现架构如下:

image

该方案主要有以下几个特点:

客户端流量负载均衡

为了实现对多个 Docker Registry 的流量负载均衡,需要引入 Load Balance 模块。常见的 Load Balance 组件,如 LVS,HAProxy,Nginx 等代理方案都存在单机性能瓶颈,无法满足上百台机器同时拉取镜像的带宽压力。因此我们采用客户端负载均衡方案,DNS 负载均衡:在 Docker daemon 解析 Registry 域名时,即通过 DNS 解析到某个 Docker Registry 实例 IP 上,这样不同机器从不同的 Docker Registry 拉取镜像,实现负载均衡。而且由于 Docker daemon 每次拉取镜像时只需解析一次 Registry 域名,对于 DNS 负载压力本身也很小。从上图可以看出,我们每一个 Docker Registry 实例对应一个 Nginx,部署在同一台主机上,对 Registry 的访问必须通过 Nginx,Nginx 这里并没有起到负载均衡的作用,其具体的作用将在下文描述。这种基于 DNS 的客户端负载均衡存在的主要问题是无法自动摘掉挂掉的后端。当某台 Nginx 挂掉时,镜像仓库的可用性就会受到比较严重的影响。因此需要有一个第三方的健康检查服务来对 Docker Registry 的节点进行检查,健康检查失败时,将对应的 A 记录摘掉,健康检查恢复,再将 A 记录加回来。

Nginx 权限控制

由于是完全的私有云,加上维护成本的考虑,我们的 Docker Registry 之前并没有做任何权限相关的配置。后来随着公司的发展,安全问题也变的越来越重要,Docker Registry 的权限控制也提上了日程。对于 Docker Registry 的权限管理,官方主要提供了两种方式,一种是简单的 basic auth,一种是比较复杂的token auth。我们对 Docker Registry 权限控制的主要需求是提供基本的认证和鉴权,并且对现有系统的改动尽量最小。basic auth 的方式只提供了基本的认证功能,不包含鉴权。而 token auth 的方式又过于复杂,需要维护单独的 token 服务,除非你需要相当全面精细的 ACL 控制并且想跟现有的认证鉴权系统相整合,否则官方并不推荐使用 token auth 的方式。这两种方式对我们而言都不是很适合。我们最后采用了 basic auth + Nginx 的权限控制方式。basic auth 用来提供基本的认证,OpenRestry + lua 只需要少量的代码,就可以灵活配置不同 URL 的路由鉴权策略。我们目前实现的鉴权策略主要有以下几种:

1.基于仓库目录的权限管理:针对不同的仓库目录,提供不同的权限控制,例如 /v2/path1 作为公有仓库目录,可以直接进行访问,而 /v2/path2 作为私有仓库目录,必须经过认证才能访问。

2.基于机器的权限管理:只允许某些特定的机器有 pull/push 镜像的权限。

Nginx 镜像缓存

Docker Registry 本身基于文件系统,响应延迟大,并发能力差。为了减少延迟提升并发,同时减轻对后端存储的负载压力,需要给 Docker Registry 增加缓存。Docker Registry 目前只支持将镜像层级 meta 信息缓存到内存或者 Redis 中,但是对于镜像数据本身无法缓存。我们同样利用 Nginx 来实现 URL 接口数据的 cache。为了避免 cache 过大,可以配置缓存失效时间,只缓存最近读取的镜像数据。主要的配置如下所示:

proxy_cache_path /dev/shm/registry-cache levels=1:2 keys_zone=registry-cache:10m max_size=124G;

加了缓存之后,Docker Registry 性能跟之前相比有了明显的提升。经过测试,100 台机器并行拉取一个 1.2G 的 image layer,不加缓存平均需要 1m50s,花费最长时间为 2m30s,添加缓存配置之后,平均的下载时间为 40s 左右,花费最长时间为 58s,可见对镜像并发下载性能的提升还是相当明显的。

HDFS 存储后端

Docker Registry 的后端分布式存储,我们选择使用 HDFS,因为在私有云场景下访问诸如 S3 等公有云存储网络带宽和时延都无法接受。HDFS 本身也是一个稳定的分布式存储系统,广泛应用在大数据存储领域,其可靠性满足生产环境的要求。但 Registry 的官方版本里并没有提供 HDFS 的 Storage Driver, 所以我们根据官方的接口要求及示例,实现了 Docker Registry 的 HDFS Storage Driver。出于性能考虑,我们选用了一个 Golang 实现的原生 HDFS Client (colinmarc/hdfs)。Storage Driver 的实现比较简单,只需要实现 StorageDriver 以及 FileWriter 这两个 interface 就可以了,具体的接口如下:

type StorageDriver interface {
    // Name returns the human-readable "name" of the driver。
    Name() string

    // GetContent retrieves the content stored at "path" as a []byte.
    GetContent(ctx context.Context, path string) ([]byte, error)

    // PutContent stores the []byte content at a location designated by "path".
    PutContent(ctx context.Context, path string, content []byte) error

    // Reader retrieves an io.ReadCloser for the content stored at "path"
    // with a given byte offset.
    Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error)

    // Writer returns a FileWriter which will store the content written to it
    // at the location designated by "path" after the call to Commit.
    Writer(ctx context.Context, path string, append bool) (FileWriter, error)

    // Stat retrieves the FileInfo for the given path, including the current
    // size in bytes and the creation time.
    Stat(ctx context.Context, path string) (FileInfo, error)

    // List returns a list of the objects that are direct descendants of the
    //given path.
    List(ctx context.Context, path string) ([]string, error)

    // Move moves an object stored at sourcePath to destPath, removing the
    // original object.
    Move(ctx context.Context, sourcePath string, destPath string) error

    // Delete recursively deletes all objects stored at "path" and its subpaths.
    Delete(ctx context.Context, path string) error
    URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error)
}

type FileWriter interface {
    io.WriteCloser

    // Size returns the number of bytes written to this FileWriter.
    Size() int64

    // Cancel removes any written content from this FileWriter.
    Cancel() error

    // Commit flushes all content written to this FileWriter and makes it
    // available for future calls to StorageDriver.GetContent and
    // StorageDriver.Reader.
    Commit() error
}

其中需要注意的是 StorageDriver 的 Writer 方法里的 append 参数,这就要求存储后端及其客户端必须提供相应的 append 方法,colinmarc/hdfs 这个 HDFS 客户端中没有实现 append 方法,我们补充实现了这个方法。

镜像清理

持续集成系统中,每次生产环境代码发布都对应有容器镜像的构建和发布,会导致镜像仓库存储空间的持续上涨,需要及时清理不用的镜像释放存储空间。但 Docker Registry 本身并没有配置镜像 TTL 的机制,需要自己开发定时清理脚本。

Docker Registry 删除镜像有两种方式,一种是删除镜像:

DELETE /v2/<name>/manifests/<reference>

另一种是直接删除镜像层 blob 数据:

DELETE /v2/<name>/blobs/<digest>

由于容器镜像层级间存在依赖引用关系,所以推荐使用第一种方式清理过期镜像的引用,然后由 Docker Registry 自身判断镜像层数据没有被引用后再执行物理删除。

未来展望

通过适当的开发和改造,我们实现了一套分布式的镜像仓库服务,可以通过水平扩容来解决单机性能瓶颈和存储容量问题,很好的满足了我们现有生产环境需求。但是在生产环境大规模分发镜像时,服务端(存储、带宽等)依然有较大的负载压力,因此在大规模镜像分发场景下,采用 P2P 的模式分发传输镜像更加合适,例如阿里开源的 Dragonfly 和腾讯开发的 FID 项目。知乎当前几乎所有的业务都运行在容器上,随着业务的快速增长,该分布式镜像仓库方案也会越来越接近性能瓶颈,因此我们在后续也会尝试引入 P2P 的镜像分发方案,以满足知乎快速增长的业务需求。

原文发布时间为:2018-07-05
本文作者:Docker
原文链接:知乎十万级容器规模的分布式镜像仓库实践

相关实践学习
通过ACR快速部署网站应用
本次实验任务是在云上基于ECS部署Docker环境,制作网站镜像并上传至ACR镜像仓库,通过容器镜像运行网站应用,网站运行在Docker容器中、网站业务数据存储在Mariadb数据库中、网站文件数据存储在服务器ECS云盘中,通过公网地址进行访问。
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
4月前
|
人工智能 安全 应用服务中间件
阿里巴巴 MCP 分布式落地实践:快速转换 HSF 到 MCP server
本文分享了阿里巴巴内部将大规模HSF服务快速转换为MCP Server的实践经验,通过Higress网关实现MCP协议卸载,无需修改代码即可接入MCP生态。文章分析了MCP生态面临的挑战,如协议快速迭代和SDK不稳定性,并详细介绍了操作步骤及组件功能。强调MCP虽非终极解决方案,但作为AI业务工程化的起点具有重要意义。最后总结指出,MCP只是AI原生应用发展的第一步,未来还有更多可能性值得探索。
1008 48
|
2月前
|
Cloud Native 中间件 调度
云原生信息提取系统:容器化流程与CI/CD集成实践
本文介绍如何通过工程化手段解决数据提取任务中的稳定性与部署难题。结合 Scrapy、Docker、代理中间件与 CI/CD 工具,构建可自动运行、持续迭代的云原生信息提取系统,实现结构化数据采集与标准化交付。
云原生信息提取系统:容器化流程与CI/CD集成实践
|
4月前
|
监控 Linux 应用服务中间件
Linux多节点多硬盘部署MinIO:分布式MinIO集群部署指南搭建高可用架构实践
通过以上步骤,已成功基于已有的 MinIO 服务,扩展为一个 MinIO 集群。该集群具有高可用性和容错性,适合生产环境使用。如果有任何问题,请检查日志或参考MinIO 官方文档。作者联系方式vx:2743642415。
1184 57
|
4月前
|
安全 JavaScript 前端开发
HarmonyOS NEXT~HarmonyOS 语言仓颉:下一代分布式开发语言的技术解析与应用实践
HarmonyOS语言仓颉是华为专为HarmonyOS生态系统设计的新型编程语言,旨在解决分布式环境下的开发挑战。它以“编码创造”为理念,具备分布式原生、高性能与高效率、安全可靠三大核心特性。仓颉语言通过内置分布式能力简化跨设备开发,提供统一的编程模型和开发体验。文章从语言基础、关键特性、开发实践及未来展望四个方面剖析其技术优势,助力开发者掌握这一新兴工具,构建全场景分布式应用。
434 35
|
4月前
|
关系型数据库 MySQL Docker
|
5月前
|
存储 负载均衡 测试技术
ACK Gateway with Inference Extension:优化多机分布式大模型推理服务实践
本文介绍了如何利用阿里云容器服务ACK推出的ACK Gateway with Inference Extension组件,在Kubernetes环境中为多机分布式部署的LLM推理服务提供智能路由和负载均衡能力。文章以部署和优化QwQ-32B模型为例,详细展示了从环境准备到性能测试的完整实践过程。
|
6月前
|
并行计算 PyTorch 算法框架/工具
融合AMD与NVIDIA GPU集群的MLOps:异构计算环境中的分布式训练架构实践
本文探讨了如何通过技术手段混合使用AMD与NVIDIA GPU集群以支持PyTorch分布式训练。面对CUDA与ROCm框架互操作性不足的问题,文章提出利用UCC和UCX等统一通信框架实现高效数据传输,并在异构Kubernetes集群中部署任务。通过解决轻度与强度异构环境下的挑战,如计算能力不平衡、内存容量差异及通信性能优化,文章展示了如何无需重构代码即可充分利用异构硬件资源。尽管存在RDMA验证不足、通信性能次优等局限性,但该方案为最大化GPU资源利用率、降低供应商锁定提供了可行路径。源代码已公开,供读者参考实践。
445 3
融合AMD与NVIDIA GPU集群的MLOps:异构计算环境中的分布式训练架构实践
|
6月前
|
Ubuntu 关系型数据库 MySQL
容器技术实践:在Ubuntu上使用Docker安装MySQL的步骤。
通过以上的操作,你已经步入了Docker和MySQL的世界,享受了容器技术给你带来的便利。这个旅程中你可能会遇到各种挑战,但是只要你沿着我们划定的路线行进,你就一定可以达到目的地。这就是Ubuntu、Docker和MySQL的灵魂所在,它们为你开辟了一条通往新探索的道路,带你亲身感受到了技术的力量。欢迎在Ubuntu的广阔大海中探索,用Docker技术引领你的航行,随时准备感受新技术带来的震撼和乐趣。
281 16
|
6月前
|
人工智能 运维 监控
领先AI企业经验谈:探究AI分布式推理网络架构实践
当前,AI行业正处于快速发展的关键时期。继DeepSeek大放异彩之后,又一款备受瞩目的AI智能体产品Manus横空出世。Manus具备独立思考、规划和执行复杂任务的能力,其多智能体架构能够自主调用工具。在GAIA基准测试中,Manus的性能超越了OpenAI同层次的大模型,展现出卓越的技术实力。
|
7月前
|
监控 Kubernetes Cloud Native
基于阿里云容器服务Kubernetes版(ACK)的微服务架构设计与实践
本文介绍了如何基于阿里云容器服务Kubernetes版(ACK)设计和实现微服务架构。首先概述了微服务架构的优势与挑战,如模块化、可扩展性及技术多样性。接着详细描述了ACK的核心功能,包括集群管理、应用管理、网络与安全、监控与日志等。在设计基于ACK的微服务架构时,需考虑服务拆分、通信、发现与负载均衡、配置管理、监控与日志以及CI/CD等方面。通过一个电商应用案例,展示了用户服务、商品服务、订单服务和支付服务的具体部署步骤。最后总结了ACK为微服务架构提供的强大支持,帮助应对各种挑战,构建高效可靠的云原生应用。

相关产品

  • 容器镜像服务