十大Kubernetes CI/CD工具


Kubernetes也被称为Kube,是一个用于容器调度的开源平台,在动态环境中管理容器的生命周期。

Kubernetes具有可移植、可扩展和可伸缩的特性。当下使用Kubernetes加CI/CD(持续集成/持续交付)已经是非常普遍的组合。

使用CI/CD工具部署云原生应用,能够简化开发和部署的每个阶段。将云原生应用的开发与CI/CD集成,可以使其更加健壮。

与传统的VM交付对比,Kubernetes的持续交付效率更高。在需要更新和变更时,你根本不需要让整个应用先停下来。

基于Kubernetes的CI/CD流水线通常包含4个主要的组件:
  • 版本控制系统Version Control System
  • CI系统
  • Docker仓库
  • Kubernetes集群


上述各个部件的协调和自动化能使软件交付做到无缝、连续。

下面就是十大Kubernetes CI/CD工具:

Helm

Helm是Kubernetes最知名的包装管理器之一。它使用“charts”(图表),chart是Kubernetes包和应用程序需要的任何其他依赖项的定义。当你从命令行调用chart时,Helm为Kubernetes部署创建YAML文件,然后将它们添加到集群中。Helm是开源的,这意味着我们可以为自己的组织下载、更改和使用charts。

Helm最大的优点是,它使复杂应用的部署更具可移植性。Helm还支持自动回滚,并且对开发人员来说更容易理解。但Helm的缺点就是很难搭建和维护。

Ksonnet和Jsonnet

Ksonnet(构建于JSON模板语言Jsonnet之上的)是一个配置管理工具。它提供了一种打包Kubernetes资源的方法,随后可用于创建部署所需的配置文件。Ksonnet是基于命令行界面的,而Jsonnet作为一种数据模板语言,是用来描述应用程序的。

该工具的优点是,熟悉JSON的开发人员可以轻松使用JSON Net来部署他们的应用程序。然而,使用JSON和使用Jsonnet还是有一些区别的,对于开发人员来说也就存在学习成本。

Draft

Draft由微软开发的,是一个在Kubernetes上创建基于云的应用的部署工具。当代码经过持续集成后,Draft可以用来生成Docker镜像。还可以使用它来创建Helm图表,生成基于Kubernetes的应用的YAML文件。

该工具的优势在于,可以将其与Helm结合使用,打包并部署应用程序。缺点是它需要大量的配置。

Jenkins X

Jenkins X是Kubernetes部署中比较流行和强大的CI工具之一。作为一款开源的自动化工具,带有用于CI的内置插件,它由Java编写的。

可以使用Jenkins来持续构建和测试软件项目,这样可以更容易地对项目进行更改。同时,可以使用此工具通过集成大量测试和部署技术来持续交付项目。尽管这是个功能强大的工具,但同时也是比较复杂,容易出错的工具。

CircleCI

CircleCI是另一个持续集成和交付工具。它是一个基于云的工具,包括一个用于Kubernetes自动部署的API。

由于CircleCI是基于云的,所以不需要专门的服务器。

CircleCI的优势在于,它使用了许多测试方法,如单元测试、集成测试和部署前的功能测试。该工具的缺点是,它缺乏使它成为一个完整CD流水线的所有部件。

Travis

Travis是一款商业CI工具,这点不像Jenkins。我们可以使用该工具注册、链接代码仓库、构建以及测试应用。还可以将该工具与其他常见的云仓库集成,如Bitbucket和GitHub。

Travis是一个基于云计算的工具,不需要专用服务器。该工具允许我们在不同操作系统、不同机器上进行测试。

Travis对于开源项目是免费的,但是对于商业项目需要订阅,每个企业项目大概69美元/每月。

GitLab

GitLab是一个基于Web的工具,具有CI/CD流水线的特性。除了CI/CD部署工具外,它还拥有自己的代码库,其中包含wiki、代码审查、问题跟踪。GitLab是一个开源平台,可以毫不费力地在一台服务器上处理近25,000个用户。它还内置自动部署Kubernetes组件,并支持Helm图表。

Weave Could

Weave Cloud是一个CD工具,可以让你快速监控和管理Docker容器。它还提供了一种快速设置CI/CD流水线和Kubernetes集群的方法。它允许我们以更快的速度启动、更新和回滚来部署应用程序。该工具使用Git作为声明式基础设施和应用的唯一信任来源。

缺点是它需要相当多的配置才能正常工作。

Spinnaker

Spinnaker是Netflix开发的一款开源工具。它管理流水线和部署工作,也支持Helm图表。它是一个开源的、跨多云的工具,提供了非常高效的持续交付。缺点是,这个工具最初是为了支持VM而不是为Kubernetes构建的,因此设置起来有点复杂。

Codefresh

Codefresh是一个CD/CD流水线工具,也支持Helm图表。它允许我们使用自己的CI和镜像仓库。它帮助我们构建一个简单但功能强大的CI/CD流水线。它附带了一套广泛的插件,帮助我们集成我们想要的工具。缺点是,第三方工具是用它们的图形用户界面设置的,使得流水线增加了更多的复杂性。同时,Codefresh是一款商业工具,其价格从每月34美元起。

结论

最近,开发人员对持续集成和持续交付越来越熟悉。如果没有合适的CI/CD流水线,就无法创建新的软件。Kubernetes正在迅速流行起来,所有的工具都在升级以与Kubernetes集成。因此,在上面的文章中,我们试图汇编最流行的Kubernetes CI/CD工具清单。

大家都在使用哪些工具来创建和管理CI/CD流水线?欢迎在评论区留言。

原文连接:Top 10 Kubernetes CI/CD Tools(翻译:伊海峰)
继续阅读 »

Kubernetes也被称为Kube,是一个用于容器调度的开源平台,在动态环境中管理容器的生命周期。

Kubernetes具有可移植、可扩展和可伸缩的特性。当下使用Kubernetes加CI/CD(持续集成/持续交付)已经是非常普遍的组合。

使用CI/CD工具部署云原生应用,能够简化开发和部署的每个阶段。将云原生应用的开发与CI/CD集成,可以使其更加健壮。

与传统的VM交付对比,Kubernetes的持续交付效率更高。在需要更新和变更时,你根本不需要让整个应用先停下来。

基于Kubernetes的CI/CD流水线通常包含4个主要的组件:
  • 版本控制系统Version Control System
  • CI系统
  • Docker仓库
  • Kubernetes集群


上述各个部件的协调和自动化能使软件交付做到无缝、连续。

下面就是十大Kubernetes CI/CD工具:

Helm

Helm是Kubernetes最知名的包装管理器之一。它使用“charts”(图表),chart是Kubernetes包和应用程序需要的任何其他依赖项的定义。当你从命令行调用chart时,Helm为Kubernetes部署创建YAML文件,然后将它们添加到集群中。Helm是开源的,这意味着我们可以为自己的组织下载、更改和使用charts。

Helm最大的优点是,它使复杂应用的部署更具可移植性。Helm还支持自动回滚,并且对开发人员来说更容易理解。但Helm的缺点就是很难搭建和维护。

Ksonnet和Jsonnet

Ksonnet(构建于JSON模板语言Jsonnet之上的)是一个配置管理工具。它提供了一种打包Kubernetes资源的方法,随后可用于创建部署所需的配置文件。Ksonnet是基于命令行界面的,而Jsonnet作为一种数据模板语言,是用来描述应用程序的。

该工具的优点是,熟悉JSON的开发人员可以轻松使用JSON Net来部署他们的应用程序。然而,使用JSON和使用Jsonnet还是有一些区别的,对于开发人员来说也就存在学习成本。

Draft

Draft由微软开发的,是一个在Kubernetes上创建基于云的应用的部署工具。当代码经过持续集成后,Draft可以用来生成Docker镜像。还可以使用它来创建Helm图表,生成基于Kubernetes的应用的YAML文件。

该工具的优势在于,可以将其与Helm结合使用,打包并部署应用程序。缺点是它需要大量的配置。

Jenkins X

Jenkins X是Kubernetes部署中比较流行和强大的CI工具之一。作为一款开源的自动化工具,带有用于CI的内置插件,它由Java编写的。

可以使用Jenkins来持续构建和测试软件项目,这样可以更容易地对项目进行更改。同时,可以使用此工具通过集成大量测试和部署技术来持续交付项目。尽管这是个功能强大的工具,但同时也是比较复杂,容易出错的工具。

CircleCI

CircleCI是另一个持续集成和交付工具。它是一个基于云的工具,包括一个用于Kubernetes自动部署的API。

由于CircleCI是基于云的,所以不需要专门的服务器。

CircleCI的优势在于,它使用了许多测试方法,如单元测试、集成测试和部署前的功能测试。该工具的缺点是,它缺乏使它成为一个完整CD流水线的所有部件。

Travis

Travis是一款商业CI工具,这点不像Jenkins。我们可以使用该工具注册、链接代码仓库、构建以及测试应用。还可以将该工具与其他常见的云仓库集成,如Bitbucket和GitHub。

Travis是一个基于云计算的工具,不需要专用服务器。该工具允许我们在不同操作系统、不同机器上进行测试。

Travis对于开源项目是免费的,但是对于商业项目需要订阅,每个企业项目大概69美元/每月。

GitLab

GitLab是一个基于Web的工具,具有CI/CD流水线的特性。除了CI/CD部署工具外,它还拥有自己的代码库,其中包含wiki、代码审查、问题跟踪。GitLab是一个开源平台,可以毫不费力地在一台服务器上处理近25,000个用户。它还内置自动部署Kubernetes组件,并支持Helm图表。

Weave Could

Weave Cloud是一个CD工具,可以让你快速监控和管理Docker容器。它还提供了一种快速设置CI/CD流水线和Kubernetes集群的方法。它允许我们以更快的速度启动、更新和回滚来部署应用程序。该工具使用Git作为声明式基础设施和应用的唯一信任来源。

缺点是它需要相当多的配置才能正常工作。

Spinnaker

Spinnaker是Netflix开发的一款开源工具。它管理流水线和部署工作,也支持Helm图表。它是一个开源的、跨多云的工具,提供了非常高效的持续交付。缺点是,这个工具最初是为了支持VM而不是为Kubernetes构建的,因此设置起来有点复杂。

Codefresh

Codefresh是一个CD/CD流水线工具,也支持Helm图表。它允许我们使用自己的CI和镜像仓库。它帮助我们构建一个简单但功能强大的CI/CD流水线。它附带了一套广泛的插件,帮助我们集成我们想要的工具。缺点是,第三方工具是用它们的图形用户界面设置的,使得流水线增加了更多的复杂性。同时,Codefresh是一款商业工具,其价格从每月34美元起。

结论

最近,开发人员对持续集成和持续交付越来越熟悉。如果没有合适的CI/CD流水线,就无法创建新的软件。Kubernetes正在迅速流行起来,所有的工具都在升级以与Kubernetes集成。因此,在上面的文章中,我们试图汇编最流行的Kubernetes CI/CD工具清单。

大家都在使用哪些工具来创建和管理CI/CD流水线?欢迎在评论区留言。

原文连接:Top 10 Kubernetes CI/CD Tools(翻译:伊海峰) 收起阅读 »

低复杂度——服务网格的下一站


【编者的话】作为一个曾经在新造车公司的基础架构团队任职,为支持公司的“互联网基因”和“数字化转型”落地了云原生基础设施平台,并在尝试采用服务网格未成的我来说,看到这篇文章深有感触。尤其是文中所说的“人少,问题多,需要快速输出价值”,直戳到了痛处。有限的人手有限的时间,我们需要将大部分精力集中在解决成熟度曲线较低的基本问题上,要想很好的运行复杂的系统是非常困难的。

服务网格是一个新的基础设施层,可以承载很多的功能,未来还会有更大的想象空间和光明的未来。

以上的种种原因,也促使我后来选择进入一家提供服务网格的产品企业,也希望服务网格可以被更简单的使用。

“道阻且长,行则将至!”

本文翻译自 Chris Campbell 的《How Unnecessary Complexity Gave the Service Mesh a Bad Name》。

关键要点

  • 采用服务网格有巨大的价值,但必须以轻量级的方式进行,以避免不必要的复杂性。
  • 在实施服务网时,要采取务实的方法,与技术的核心功能保持一致,并小心干扰(译者:注意力的分散)。
  • 服务网格的一些核心特性包括标准化监控、自动加密和身份识别、智能路由、可靠的重试和网络可扩展性。
  • 服务网格可以提供强大的功能,但这些功能会分散本应对核心优势的关注,并且这些功能也不是实施服务网格的主要原因。
  • 在初始实施服务网格时没有必要去关注那些明显会分散注意力的功能,比如复杂的控制平面、多集群支持、Envoy、WASM 和 A/B 测试。


服务网格是 Kubernetes 世界中的一个热门话题,但许多潜在的采用者已经有些失望了。服务网格的落地受到压倒性的复杂性和看似无穷无尽的供应商解决方案的限制。在我亲自浏览了这个领域之后,我发现采用服务网格具有巨大的价值,但它必须以轻量级的方式完成,以避免不必要的复杂性。尽管普遍存在幻灭感,但服务网格的未来依然光明。

在工作中学习

我进入服务网格的世界始于我在一家老牌的财富 500 强技术公司担任云计算架构师的角色。在开始我们的服务网格之旅时,我身边有许多强大的工程师,但大多数人几乎没有云计算开发经验。我们的组织诞生于云计算之前,完全实现云计算的价值需要时间。我们的传统业务线主要集中在技术栈的硬件元素上,云计算的决策最初是由为运送硬件或为该硬件提供固件和驱动程序而开发的流程驱动的。

随着该组织经历其“数字化转型”,它越来越依赖于提供高质量的软件服务,并逐渐开发出更好的方法。但作为云计算架构师,我仍在为优先考虑硬件的业务流程,以及具有不同技能、流程和信念的工程团队导航。随着时间的推移,我和我的团队在将 .NET 应用程序迁移到 Linux、采用 Docker、迁移到 AWS 以及与之相关的最佳实践(如持续集成、自动化部署、不可变基础设施、基础设施即代码、监控等)方面变得熟练并成功。但挑战依然存在。

在此期间,我们开始将我们的应用程序拆分为一组微服务。起初,这是一个缓慢的转变,但最终这种方法流行起来,开发人员开始更喜欢构建新的服务而不是添加到现有服务。我们这些基础设施团队的人把这看作是一种成功。唯一的问题是与网络相关的问题数量激增,开发人员正在向我们寻求答案,而我们还没有准备好有效地应对这种冲击。

服务网格的援救

我第一次听说服务网格是在 2015 年,当时我正在研究服务发现工具并寻找与 Consul 集成的简单方法。我喜欢将应用程序职责卸载到“sidecar”容器的想法,并找到了一些可以帮助做到这一点的工具。大约在这个时候,Docker 有一个叫做“链接”的功能,让你可以将两个应用程序放在一个共享的网络空间中,这样它们就可以通过 localhost 进行通信。此功能提供了类似于我们现在在 Kubernetes pod 中所拥有的体验:两个独立构建的服务可以在部署时进行组合以实现一些附加功能。

我总是抓住机会用简单的方案来解决大问题,因此这些新功能的力量立即打动了我。虽然这个工具是为了与 Consul 集成而构建的,但实际上,它可以做任何你想做的事情。这是我们拥有的基础设施层,可以用来一次为所有人解决问题。

这方面的一个具体例子出现在我们采用过程的早期。当时,我们正致力于跨不同服务的日志标准化输出。通过采用服务网格和这种新的设计模式,我们能够将我们的人的问题——让开发人员标准化他们的日志——换成技术问题——将所有流量传递给可以为他们记录日志的代理。这是我们团队向前迈出的重要一步。

我们对服务网格的实现非常务实,并且与该技术的核心功能非常吻合。然而,大部分营销炒作都集中在不太需要的边缘案例上,在评估服务网格是否适合你时,能够识别这些干扰是很重要的。

核心功能

服务网格可以提供的核心功能分为四个关键责任领域:可观察性、安全性、连接性和可靠性。这些功能包括:

标准化监控

我们取得的最大胜利之一,也是最容易采用的,是标准化监控。它的运营成本非常低,可以适应你使用的任何监控系统。它使组织能够捕获所有 HTTP 或 gRPC 指标,并以标准方式在整个系统中存储它们。这控制了复杂性并减轻了应用程序团队的负担,他们不再需要实现 Prometheus 指标端点或标准化日志格式。它还使用户能够公正地了解其应用程序的黄金信号。

自动加密和身份识别

证书管理很难做好。如果一个组织还没有在这方面进行投入,他们应该找到一个网格来为他们做这件事。证书管理需要维护具有巨大安全隐患的复杂基础设施代码。相比之下,网格将能够与编排系统集成,以了解工作负载的身份,在需要时可以用来执行策略。这允许提供与 Calico 或 Cilium 等功能强大的 CNI 提供的安全态势相当或更好的安全态势。

智能路由

智能路由是另一个特性,它使网格能够在发送请求时“做正确的事”。场景包括:
  1. 使用延迟加权算法优化流量
  2. 拓扑感知路由以提高性能并降低成本
  3. 根据请求成功的可能性使请求超时
  4. 与编排系统集成以进行 IP 解析,而不是依赖 DNS
  5. 传输升级,例如 HTTP 到 HTTP/2


这些功能可能不会让普通人感到兴奋,但随着时间的推移,它们从根本上增加了价值

可靠的重试

在分布式系统中重试请求可能很麻烦,但是它几乎总是需要实现的。分布式系统通常会将一个客户端请求转换为更多下游请求,这意味着“尾巴”场景的可能性会大大增加,例如发生异常失败的请求。对此最简单的缓解措施是重试失败的请求。

困难来自于避免“重试风暴”或“重试 DDoS”,即当处于降级状态的系统触发重试、随着重试增加而增加负载并进一步降低性能时。天真的实现不会考虑这种情况,因为它可能需要与缓存或其他通信系统集成以了解是否值得执行重试。服务网格可以通过对整个系统允许的重试总数进行限制来实现这一点。网格还可以在这些重试发生时报告这些重试,可能会在你的用户注意到系统降级之前提醒你。

网络可扩展性

也许服务网格的最佳属性是它的可扩展性。它提供了额外的适应性层,以应对 IT 下一步投入的任何事情。Sidecar 代理的设计模式是另一个令人兴奋和强大的功能,即使它有时会被过度宣传和过度设计来做用户和技术人员还没有准备好的事情。虽然社区在等着看哪个服务网格“生出”,这反映了之前过度炒作的编排战争,但未来我们将不可避免地看到更多专门构建的网格,并且可能会有更多的最终用户构建自己的控制平面和代理以满足他们的场景。

服务网格干扰

平台或基础设施控制层的价值怎么强调都不为过。然而,在服务网格世界中,我了解到入门的一个主要的挑战是,服务网格解决的核心问题通常甚至不是大多数服务网格项目交流的焦点!

相反,来自服务网格项目的大部分交流都围绕着听起来很强大或令人兴奋但最终会让人分心的功能。这包括:

强(复)大(杂)的控制平面

要很好地运行复杂的软件是非常困难的。这就是为什么如此多的组织使用云计算来使用完全托管的服务来减轻这一点的原因。那么为什么服务网格项目会让我们负责操作如此复杂的系统呢?系统的复杂性不是资产,而是负债,但大多数项目都在吹捧它们的功能集和可配置性。

多集群支持

多集群现在是一个热门话题。最终,大多数团队将运行多个 Kubernetes 集群。但是多集群的主要痛点是你的 Kubernetes 管理的网络被切分。服务网格有助于解决这个 Kubernetes 横向扩展问题,但它最终并没有带来任何新的东西。是的,多集群支持是必要的,但它对服务网格的承诺被过度宣传了。

Envoy

Envoy 是一个很棒的工具,但它被作为某种标准介绍,这是有问题的。Envoy 是众多开箱即用的代理之一,你可以将其作为服务网格平台的基础。但是 Envoy 并没有什么内在的特别之处,使其成为正确的选择。采用 Envoy 会给你的组织带来一系列重要问题,包括:
  • 运行时成本和性能(所有这些过滤器加起来!)
  • 计算资源需求以及如何随负载扩展
  • 如何调试错误或意外行为
  • 你的网格如何与 Envoy 交互以及配置生命周期是什么
  • 运作成熟的时间(这可能比你预期的要长)


服务网格中代理的选择应该是一个实现细节,而不是产品要求。

WASM

我是 Web Assembly(WASM)的忠实拥趸,已经成功地使用它在 Blazor 中构建前端应用程序。然而,WASM 作为定制服务网格代理行为的工具,让你处于获得一个全新的软件生命周期开销的境地,这与你现有的软件生命周期完全正交!如果你的组织还没有准备好构建、测试、部署、维护、监控、回滚和版本代码(影响通过其系统运行的每个请求),那么你还没有准备好使用 WASM。

A/B 测试

直到为时已晚,我才意识到 A/B 测试实际上是一个应用程序级别的问题。在基础设施层提供原语来实现它是很好的,但是没有简单的方法来完全自动化大多数组织需要的 A/B 测试水平。通常,应用程序需要定义独特的指标来定义测试的积极信号。如果组织想要在服务网格级别投入 A/B 测试,那么解决方案需要支持以下内容:
  1. 对部署和回滚的精细控制,因为它可能同时进行多个不同的“测试”
  2. 能够捕获系统知道的自定义指标并可以根据这些指标做出决策
  3. 根据请求的特征暴露对流量方向的控制,其中可能包括解析整个请求正文


这需要实现很多,没有哪个服务网格是开箱即用的。最终,我们的组织选择了网格之外的特征标记解决方案,其以最小的努力取得了巨大的成功。

我们在哪里结束

最终,我们面临的挑战并不是服务网格独有的。我们工作的组织有一系列限制条件,要求我们对解决的问题以及如何解决问题采取务实的态度。我们面临的问题包括:
  • 一个拥有大量不同技能的开发人员的大型组织
  • 云计算和 SaaS 能力普遍不成熟
  • 为非云计算软件优化的流程
  • 碎片化的软件工程方法和信念
  • 有限的资源
  • 激进的截止日期


简而言之,我们人少,问题多,需要快速输出价值。我们必须支持主要不是 Web 或云计算的开发者,我们需要扩大规模以支持有不同方法和流程的大型工程组织来做云计算。我们需要将大部分精力集中在解决成熟度曲线较低的基本问题上。

最后,当面对我们自己的服务网格决策时,我们决定建立在 Linkerd 服务网格上,因为它最符合我们的优先事项:低运营成本(计算和人力)、低认知开销、支持性社区以及透明的管理——同时满足我们的功能要求和预算。在 Linkerd 指导委员会工作了一段时间后(他们喜欢诚实的反馈和社区参与),我了解到它与我自己的工程原则有多么的契合。Linkerd 最近在 CNCF 达到毕业状态[5],这是一个漫长的过程,强调了该项目的成熟及其广泛采用。

原文链接:https://mp.weixin.qq.com/s/ny5AsOPbaDWQwMk4DSe6MA
继续阅读 »

【编者的话】作为一个曾经在新造车公司的基础架构团队任职,为支持公司的“互联网基因”和“数字化转型”落地了云原生基础设施平台,并在尝试采用服务网格未成的我来说,看到这篇文章深有感触。尤其是文中所说的“人少,问题多,需要快速输出价值”,直戳到了痛处。有限的人手有限的时间,我们需要将大部分精力集中在解决成熟度曲线较低的基本问题上,要想很好的运行复杂的系统是非常困难的。

服务网格是一个新的基础设施层,可以承载很多的功能,未来还会有更大的想象空间和光明的未来。

以上的种种原因,也促使我后来选择进入一家提供服务网格的产品企业,也希望服务网格可以被更简单的使用。

“道阻且长,行则将至!”

本文翻译自 Chris Campbell 的《How Unnecessary Complexity Gave the Service Mesh a Bad Name》。

关键要点

  • 采用服务网格有巨大的价值,但必须以轻量级的方式进行,以避免不必要的复杂性。
  • 在实施服务网时,要采取务实的方法,与技术的核心功能保持一致,并小心干扰(译者:注意力的分散)。
  • 服务网格的一些核心特性包括标准化监控、自动加密和身份识别、智能路由、可靠的重试和网络可扩展性。
  • 服务网格可以提供强大的功能,但这些功能会分散本应对核心优势的关注,并且这些功能也不是实施服务网格的主要原因。
  • 在初始实施服务网格时没有必要去关注那些明显会分散注意力的功能,比如复杂的控制平面、多集群支持、Envoy、WASM 和 A/B 测试。


服务网格是 Kubernetes 世界中的一个热门话题,但许多潜在的采用者已经有些失望了。服务网格的落地受到压倒性的复杂性和看似无穷无尽的供应商解决方案的限制。在我亲自浏览了这个领域之后,我发现采用服务网格具有巨大的价值,但它必须以轻量级的方式完成,以避免不必要的复杂性。尽管普遍存在幻灭感,但服务网格的未来依然光明。

在工作中学习

我进入服务网格的世界始于我在一家老牌的财富 500 强技术公司担任云计算架构师的角色。在开始我们的服务网格之旅时,我身边有许多强大的工程师,但大多数人几乎没有云计算开发经验。我们的组织诞生于云计算之前,完全实现云计算的价值需要时间。我们的传统业务线主要集中在技术栈的硬件元素上,云计算的决策最初是由为运送硬件或为该硬件提供固件和驱动程序而开发的流程驱动的。

随着该组织经历其“数字化转型”,它越来越依赖于提供高质量的软件服务,并逐渐开发出更好的方法。但作为云计算架构师,我仍在为优先考虑硬件的业务流程,以及具有不同技能、流程和信念的工程团队导航。随着时间的推移,我和我的团队在将 .NET 应用程序迁移到 Linux、采用 Docker、迁移到 AWS 以及与之相关的最佳实践(如持续集成、自动化部署、不可变基础设施、基础设施即代码、监控等)方面变得熟练并成功。但挑战依然存在。

在此期间,我们开始将我们的应用程序拆分为一组微服务。起初,这是一个缓慢的转变,但最终这种方法流行起来,开发人员开始更喜欢构建新的服务而不是添加到现有服务。我们这些基础设施团队的人把这看作是一种成功。唯一的问题是与网络相关的问题数量激增,开发人员正在向我们寻求答案,而我们还没有准备好有效地应对这种冲击。

服务网格的援救

我第一次听说服务网格是在 2015 年,当时我正在研究服务发现工具并寻找与 Consul 集成的简单方法。我喜欢将应用程序职责卸载到“sidecar”容器的想法,并找到了一些可以帮助做到这一点的工具。大约在这个时候,Docker 有一个叫做“链接”的功能,让你可以将两个应用程序放在一个共享的网络空间中,这样它们就可以通过 localhost 进行通信。此功能提供了类似于我们现在在 Kubernetes pod 中所拥有的体验:两个独立构建的服务可以在部署时进行组合以实现一些附加功能。

我总是抓住机会用简单的方案来解决大问题,因此这些新功能的力量立即打动了我。虽然这个工具是为了与 Consul 集成而构建的,但实际上,它可以做任何你想做的事情。这是我们拥有的基础设施层,可以用来一次为所有人解决问题。

这方面的一个具体例子出现在我们采用过程的早期。当时,我们正致力于跨不同服务的日志标准化输出。通过采用服务网格和这种新的设计模式,我们能够将我们的人的问题——让开发人员标准化他们的日志——换成技术问题——将所有流量传递给可以为他们记录日志的代理。这是我们团队向前迈出的重要一步。

我们对服务网格的实现非常务实,并且与该技术的核心功能非常吻合。然而,大部分营销炒作都集中在不太需要的边缘案例上,在评估服务网格是否适合你时,能够识别这些干扰是很重要的。

核心功能

服务网格可以提供的核心功能分为四个关键责任领域:可观察性、安全性、连接性和可靠性。这些功能包括:

标准化监控

我们取得的最大胜利之一,也是最容易采用的,是标准化监控。它的运营成本非常低,可以适应你使用的任何监控系统。它使组织能够捕获所有 HTTP 或 gRPC 指标,并以标准方式在整个系统中存储它们。这控制了复杂性并减轻了应用程序团队的负担,他们不再需要实现 Prometheus 指标端点或标准化日志格式。它还使用户能够公正地了解其应用程序的黄金信号。

自动加密和身份识别

证书管理很难做好。如果一个组织还没有在这方面进行投入,他们应该找到一个网格来为他们做这件事。证书管理需要维护具有巨大安全隐患的复杂基础设施代码。相比之下,网格将能够与编排系统集成,以了解工作负载的身份,在需要时可以用来执行策略。这允许提供与 Calico 或 Cilium 等功能强大的 CNI 提供的安全态势相当或更好的安全态势。

智能路由

智能路由是另一个特性,它使网格能够在发送请求时“做正确的事”。场景包括:
  1. 使用延迟加权算法优化流量
  2. 拓扑感知路由以提高性能并降低成本
  3. 根据请求成功的可能性使请求超时
  4. 与编排系统集成以进行 IP 解析,而不是依赖 DNS
  5. 传输升级,例如 HTTP 到 HTTP/2


这些功能可能不会让普通人感到兴奋,但随着时间的推移,它们从根本上增加了价值

可靠的重试

在分布式系统中重试请求可能很麻烦,但是它几乎总是需要实现的。分布式系统通常会将一个客户端请求转换为更多下游请求,这意味着“尾巴”场景的可能性会大大增加,例如发生异常失败的请求。对此最简单的缓解措施是重试失败的请求。

困难来自于避免“重试风暴”或“重试 DDoS”,即当处于降级状态的系统触发重试、随着重试增加而增加负载并进一步降低性能时。天真的实现不会考虑这种情况,因为它可能需要与缓存或其他通信系统集成以了解是否值得执行重试。服务网格可以通过对整个系统允许的重试总数进行限制来实现这一点。网格还可以在这些重试发生时报告这些重试,可能会在你的用户注意到系统降级之前提醒你。

网络可扩展性

也许服务网格的最佳属性是它的可扩展性。它提供了额外的适应性层,以应对 IT 下一步投入的任何事情。Sidecar 代理的设计模式是另一个令人兴奋和强大的功能,即使它有时会被过度宣传和过度设计来做用户和技术人员还没有准备好的事情。虽然社区在等着看哪个服务网格“生出”,这反映了之前过度炒作的编排战争,但未来我们将不可避免地看到更多专门构建的网格,并且可能会有更多的最终用户构建自己的控制平面和代理以满足他们的场景。

服务网格干扰

平台或基础设施控制层的价值怎么强调都不为过。然而,在服务网格世界中,我了解到入门的一个主要的挑战是,服务网格解决的核心问题通常甚至不是大多数服务网格项目交流的焦点!

相反,来自服务网格项目的大部分交流都围绕着听起来很强大或令人兴奋但最终会让人分心的功能。这包括:

强(复)大(杂)的控制平面

要很好地运行复杂的软件是非常困难的。这就是为什么如此多的组织使用云计算来使用完全托管的服务来减轻这一点的原因。那么为什么服务网格项目会让我们负责操作如此复杂的系统呢?系统的复杂性不是资产,而是负债,但大多数项目都在吹捧它们的功能集和可配置性。

多集群支持

多集群现在是一个热门话题。最终,大多数团队将运行多个 Kubernetes 集群。但是多集群的主要痛点是你的 Kubernetes 管理的网络被切分。服务网格有助于解决这个 Kubernetes 横向扩展问题,但它最终并没有带来任何新的东西。是的,多集群支持是必要的,但它对服务网格的承诺被过度宣传了。

Envoy

Envoy 是一个很棒的工具,但它被作为某种标准介绍,这是有问题的。Envoy 是众多开箱即用的代理之一,你可以将其作为服务网格平台的基础。但是 Envoy 并没有什么内在的特别之处,使其成为正确的选择。采用 Envoy 会给你的组织带来一系列重要问题,包括:
  • 运行时成本和性能(所有这些过滤器加起来!)
  • 计算资源需求以及如何随负载扩展
  • 如何调试错误或意外行为
  • 你的网格如何与 Envoy 交互以及配置生命周期是什么
  • 运作成熟的时间(这可能比你预期的要长)


服务网格中代理的选择应该是一个实现细节,而不是产品要求。

WASM

我是 Web Assembly(WASM)的忠实拥趸,已经成功地使用它在 Blazor 中构建前端应用程序。然而,WASM 作为定制服务网格代理行为的工具,让你处于获得一个全新的软件生命周期开销的境地,这与你现有的软件生命周期完全正交!如果你的组织还没有准备好构建、测试、部署、维护、监控、回滚和版本代码(影响通过其系统运行的每个请求),那么你还没有准备好使用 WASM。

A/B 测试

直到为时已晚,我才意识到 A/B 测试实际上是一个应用程序级别的问题。在基础设施层提供原语来实现它是很好的,但是没有简单的方法来完全自动化大多数组织需要的 A/B 测试水平。通常,应用程序需要定义独特的指标来定义测试的积极信号。如果组织想要在服务网格级别投入 A/B 测试,那么解决方案需要支持以下内容:
  1. 对部署和回滚的精细控制,因为它可能同时进行多个不同的“测试”
  2. 能够捕获系统知道的自定义指标并可以根据这些指标做出决策
  3. 根据请求的特征暴露对流量方向的控制,其中可能包括解析整个请求正文


这需要实现很多,没有哪个服务网格是开箱即用的。最终,我们的组织选择了网格之外的特征标记解决方案,其以最小的努力取得了巨大的成功。

我们在哪里结束

最终,我们面临的挑战并不是服务网格独有的。我们工作的组织有一系列限制条件,要求我们对解决的问题以及如何解决问题采取务实的态度。我们面临的问题包括:
  • 一个拥有大量不同技能的开发人员的大型组织
  • 云计算和 SaaS 能力普遍不成熟
  • 为非云计算软件优化的流程
  • 碎片化的软件工程方法和信念
  • 有限的资源
  • 激进的截止日期


简而言之,我们人少,问题多,需要快速输出价值。我们必须支持主要不是 Web 或云计算的开发者,我们需要扩大规模以支持有不同方法和流程的大型工程组织来做云计算。我们需要将大部分精力集中在解决成熟度曲线较低的基本问题上。

最后,当面对我们自己的服务网格决策时,我们决定建立在 Linkerd 服务网格上,因为它最符合我们的优先事项:低运营成本(计算和人力)、低认知开销、支持性社区以及透明的管理——同时满足我们的功能要求和预算。在 Linkerd 指导委员会工作了一段时间后(他们喜欢诚实的反馈和社区参与),我了解到它与我自己的工程原则有多么的契合。Linkerd 最近在 CNCF 达到毕业状态[5],这是一个漫长的过程,强调了该项目的成熟及其广泛采用。

原文链接:https://mp.weixin.qq.com/s/ny5AsOPbaDWQwMk4DSe6MA 收起阅读 »

微服务的未来 —— 更多层抽象


微服务是在 10 年前出现的,是软件融合进化的例子之一。虽然这个词可以归功于软件咨询公司 Thoughtworks 的 James Lewis 和 Martin Fowler,Adrian Cockcroft 也曾提出类似的想法。但当时在 Netflix 和许多硅谷的其他公司,如亚马逊、Google 和 eBay 等公司大致在相同的时间内独立搭建了或多或少相同的架构模式。

在这个词诞生后的十年里,我们看到了 Kubernetes、服务网格和无服务器的兴起,我们也开始看到微服务被应用到了前端。除了可以横向扩展,微服务还可以让开发人员更快地部署代码,有利于组件的可替换性而不是可维护性。

无论好坏,对许多人来说,微服务已经成为默认的架构选择。对于拥有自主团队和松散耦合系统的组织来说,微服务可以很好地工作,但它们带来了所有分布式系统都无法逃避的复杂性。

“我坚决认为公共云比私有云和数据中心更好,这些好处是一目了然的。在许多情况下,是恐惧让人们畏缩不前。“独立技术顾问 Sam Newman 告诉 The New Stack,他的 Building Microservices 一书的第二版将在今年 8 月出版(本文发布于 7 月 27 日)。“但是对于微服务,事情将比这复杂得多的多。”

考虑到这一点,在进入微服务时代的十年里,思考一下我们已经走到了哪一步,以及我们还需要解决哪些问题是很有意思的。

盘点:部署和运行时间

现在有各种各样成熟的、设计良好的微服务框架,涵盖了大多数语言的基础知识,在 JVM 上有大量的选择,包括 Spring Boot、Dropwizard、Helidon、Lagom、Micronaut 和 Quarkus,同时还有 Go kit(Go)、Flask 和 Falcon(Python)、Node.js(JavaScript)等选择。

同样地,好的监控工具也比比皆是。OpenTelemetry 的出现尤其重要。它由 OpenTracing 和 OpenCensus 合并而成,拥有广泛的供应商和语言支持,为分布式遥测数据提供标准化。这意味着开发人员只需要对他们的代码进行一次检测,然后就可以交换和改变监控工具,比较相互竞争的解决方案,甚至在生产中为不同的需求运行多个不同的监控解决方案。

然而,当我们看向部署和运行时,情况就变得有点模糊了。Kubernetes 已经或多或少地成为微服务的代名词,它的复杂性不断增加,促使云原生咨询公司 Container Solutio 的首席科学家 Adrian Mouat 猜测我们将看到它的竞争对手出现。

“值得注意的是,这种复杂性不仅仅是隐藏在引擎盖下。“Mouat 说:“它正在溢出到界面上,影响到用户。“黑进 kubectl 运行并得到一个演示并运行仍然相当容易。但是,运行生产应用程序并弄清楚如何安全地暴露它们需要了解大量不同的功能,这不可避免地导致 YAML 文件比大多数微服务源代码还要长。”

Newman 总结了一个基本挑战:“Kubernetes 对开发者并不友好。我们仍然没有一个好的、可靠的、类似 Heroku 的抽象,在 Kubernetes 之上被广泛使用,这让我感到震惊。”

Spotify 的工程总监 Pia Nilsson 曾谈到,这家快速扩张的公司的新工程师平均需要 60 天才能合并他们的第 10 个 pull request。作为回应,该公司建立了一个 开发者门户网站 Backstage,现在是云原生计算基金会的一个 沙盒项目。

Netflix 非常重视 DevEx—— 该公司为开发者铺设的 “道路”—— 利用它来帮助加速 GraphQL 等新技术的采用。同样,我们已经看到了内部建设和通过 Humanitec 等供应商建设的开发者平台的崛起。 Ambassador Labs 有一个相关的开发者控制平面的概念 —— 它的网站声称,“使开发者能够控制和配置整个云开发循环,以便更快地发布软件”。


“Kubernetes 对开发者并不友好。我们仍然没有一个好的、可靠的、类似 Heroku 的抽象,在 Kubernetes 之上被广泛使用,这让我感到震惊。” ——Sam Newman, Building Microservices 作者
Ambassador Labs 的开发者关系总监 Daniel Bryant 告诉 The New Stack:“如果你看看 Airbnb、Shopify 和 Lunar 等公司正在做什么,它们之间有一个明显的共同点。他们正在为他们的开发者创建一个类似于 Heroku 的 CLI,这样,像‘创建新的微服务’这样的命令就会产生一些支架,插入 CI,插入管道,插入可观察性。问题是,你向开发者展示的抽象是什么,以便他们获得所需的可见性,同时也使他们所需的要求变得清晰?”

Bryant 特继续说:“开发者需要指定某些操作特性:这是一个内存大的服务;这个服务需要低延迟;这个服务需要非常接近那个服务。目前,你通过启动 Kubernetes 和编写大量的 YAML 来做到这一点。那里的抽象并不完全正确,特别是当你引入其他部署机制时,如无服务器和低代码/无代码。

“我想知道谁能通过平台暴露出正确的抽象概念,然后让工程师决定如何打包他们的代码 —— 但他们打包的方式是一样的,而平台暴露出一些传统上属于运维的属性。”

开放应用模型(OAM)

其他几个关于 Kubernetes 的倡议也值得跟踪。由微软和阿里云联合创建的开放应用模型(OAM)是一个描述应用的规范,将应用定义与集群的操作细节分开。因此,它使应用程序开发人员能够专注于其应用程序的关键要素,而不是其部署地点的操作细节。

Crossplane 是 OAM 的 Kubernetes 特定实现。它可以被企业用来在各种基础设施和云供应商之间建立和运维一个内部平台即服务(PaaS),这使得它在多云环境中特别有用,比如在那些兼并和收购越来越常见的大型企业中。

虽然 OAM 试图将部署细节的责任从编写服务代码中分离出来,但服务网格旨在通过一个专门的基础设施层将服务间通信的责任从个人开发者那里转移出来,该层侧重于使用代理管理服务间的通信。不幸的是,它们也有复杂性的问题,而且还可能引入相当大的性能开销。

因此,到目前为止,许多在生产中成功实施服务网格的案例都是在那些非常精通技术的初创公司。在 2020 年与 InfoQ 的 Wes Reisz 的播客中,Newman 建议在选择之前等待 6 个月,他告诉 The New Stack,他仍然给出同样的建议。

“就该技术栈的权重、管理、影响以及性能带来的影响而言,它们的现实情况是非常可怕的,”Newman 说。“对有一些组织说,如果没有它们,有些事情是不可能完成的,Monzo 就是一个很好的例子 —— 在一个组织中,你有一个异构的技术栈,你需要做大规模的双向 TLS,我可以看到它的价值。但在我看来,它仍然是‘概念很好,执行不力’。我想,我们可能会在很长时间内仍这样说。”

隐藏服务网格

有一件事可能会发生,至少对企业客户来说,性能问题往往不是那么尖锐,那就是服务网格被推到平台的更深处,并在很大程度上对开发者隐藏。例如,红帽 OpenShift 将 Istio 整合到平台层,还有多个类似的计划,将服务网格与公有云平台更紧密地整合在一起,如 AWS App Mesh 和 Google Cloud Platform Traffic Director。

关于服务网格的工作还在继续,以减少其所带来的网络开销。Cilium 团队的工作很有希望,它利用 Linux 内核中的 eBPF 功能来实现它所说的 “非常有效的网络、策略执行和负载均衡功能”。


我认为现在我们需要为其他人提供领域驱动设计(DDD)。因为即使是普通的开发者而不是架构师,也需要对如何确定实体的范围和边界有一定的了解,这其中有很多是回到了良好的 API 设计上。——Daniel Bryant,开发者关系总监,Ambassador Labs
但另一种可能性是,我们可能完全转向不同的运行时。Leading Edge Forum 的顾问 Simon Wardley 认为,函数即服务(Faas)/无服务器将最终取代 Kubernetes,成为分布式应用事实上的标准运行时,我们也看到了一些真实的生产实例,比如 BBC,它的大部分在线架构已经从之前的 LAMP 堆栈直接转向了 AWS 上的 Lambda。

“我认为 FaaS 是一个管理部署的伟大抽象,”Newman 说。“作为一个对开发者友好的部署软件的抽象,它是自 Heroku 以来我们拥有的最好的东西。我确实认为目前的实现方式很差,但他们会改进。但他们只处理了在一次在一个地方执行一件任务。这并没有解决更大的网络系统的抽象问题”。

作为一个例子,Newman 引用了微软 Azure 的 Durable Functions,它通过响应式扩展提供了类似于连续性的东西,允许开发者在无服务器环境中建立有状态的工作流和函数。但是,虽然部署抽象可能会有所改善,但如果想象你可以完全抽象出编写分布式系统的复杂性,那就太天真了。

“你不能假设你说的东西就在那里,”Newman 说。“你不能假设数据会神奇地从一个时间点瞬时传送到另一个时间点。因为它不是这样的。而且,再多的抽象也无法解决这个基本问题。”

自主团队的架构

另一个仍然具有挑战性的领域与整个系统架构有关,以及围绕团队组织和结构的相关问题。正如 IBM 的全球开发者 leader Holly Cummins 在《云原生是关于文化而不是容器》一文中指出的,“即使有适当的自主团队,系统级的考虑也不会消失”。

Eric Evans 的《领域驱动设计》是微服务运动的基石,任何软件架构师都应该阅读,Bryant 说。但他更进一步说:

“我认为现在我们需要为我们其他人提供 DDD,“他告诉 The New Stack。“因为即使是普通的开发者而不是架构师,也需要对如何确定实体和边界的范围有一定的了解,其中很多都要回到良好的 API 设计。一旦你理解了耦合和内聚的重要性,关注点和边界的分离,无论你处理的是什么抽象(模块、类、服务、应用),你都会自然而然地跳到这个齿轮上。”

Newman 的 Building Microservices 一书的第二版即将推出,该书介绍了很多这些概念,并考虑到了下一代服务。

在更新这本书时,Newman 告诉 The New Stack,“我想多谈一点耦合性。我想多谈一点内聚力。我想更多地谈论信息隐藏,这对我来说是现在最重要的事情。

“我认为,即使人们掌握了分布式系统方面的知识,他们也没有掌握一个事实,即从根本上说,微服务只是模块化架构的一种形式。然而,很多创建微服务的人对什么是模块化架构或如何进行模块化毫无概念。”

Newman 在新书中还引入了自 2014 年第一版出版以来出现的一些组织思维的变化。他特别引用了马修・斯凯尔顿(Matthew Skelton)和曼努埃尔・派斯(Manuel Pais)关于如何组织业务和技术团队以实现快速流动的极具影响力的作品《团队拓扑(Team Topologies)》,以及尼科尔・福斯格伦(Nicole Forsgren)、杰兹・汉伯(Jez Humble)和吉恩・金(Gene Kim)的《加速(Accelerate)》一书,该书探讨了精益管理和 DevOps 原则背后的科学。

修订过程不仅揭示了有多少关于微服务的新知识可以分享,而且这些知识是如何不断积累的。

“这本书可以让你广泛了解什么是微服务以及它对软件开发的影响,“Newman 说。“我发现我在向人们推荐,哦,你应该读那本书的第四章。现在我会说这个,而不是那个。我不想在推荐自己的书上一直含糊其辞。这就是为什么我写了第二版:因为我希望它是好的、准确的。”

原文链接:https://mp.weixin.qq.com/s/Ys0bFB7v2payCIuHDnDpSA
继续阅读 »

微服务是在 10 年前出现的,是软件融合进化的例子之一。虽然这个词可以归功于软件咨询公司 Thoughtworks 的 James Lewis 和 Martin Fowler,Adrian Cockcroft 也曾提出类似的想法。但当时在 Netflix 和许多硅谷的其他公司,如亚马逊、Google 和 eBay 等公司大致在相同的时间内独立搭建了或多或少相同的架构模式。

在这个词诞生后的十年里,我们看到了 Kubernetes、服务网格和无服务器的兴起,我们也开始看到微服务被应用到了前端。除了可以横向扩展,微服务还可以让开发人员更快地部署代码,有利于组件的可替换性而不是可维护性。

无论好坏,对许多人来说,微服务已经成为默认的架构选择。对于拥有自主团队和松散耦合系统的组织来说,微服务可以很好地工作,但它们带来了所有分布式系统都无法逃避的复杂性。

“我坚决认为公共云比私有云和数据中心更好,这些好处是一目了然的。在许多情况下,是恐惧让人们畏缩不前。“独立技术顾问 Sam Newman 告诉 The New Stack,他的 Building Microservices 一书的第二版将在今年 8 月出版(本文发布于 7 月 27 日)。“但是对于微服务,事情将比这复杂得多的多。”

考虑到这一点,在进入微服务时代的十年里,思考一下我们已经走到了哪一步,以及我们还需要解决哪些问题是很有意思的。

盘点:部署和运行时间

现在有各种各样成熟的、设计良好的微服务框架,涵盖了大多数语言的基础知识,在 JVM 上有大量的选择,包括 Spring Boot、Dropwizard、Helidon、Lagom、Micronaut 和 Quarkus,同时还有 Go kit(Go)、Flask 和 Falcon(Python)、Node.js(JavaScript)等选择。

同样地,好的监控工具也比比皆是。OpenTelemetry 的出现尤其重要。它由 OpenTracing 和 OpenCensus 合并而成,拥有广泛的供应商和语言支持,为分布式遥测数据提供标准化。这意味着开发人员只需要对他们的代码进行一次检测,然后就可以交换和改变监控工具,比较相互竞争的解决方案,甚至在生产中为不同的需求运行多个不同的监控解决方案。

然而,当我们看向部署和运行时,情况就变得有点模糊了。Kubernetes 已经或多或少地成为微服务的代名词,它的复杂性不断增加,促使云原生咨询公司 Container Solutio 的首席科学家 Adrian Mouat 猜测我们将看到它的竞争对手出现。

“值得注意的是,这种复杂性不仅仅是隐藏在引擎盖下。“Mouat 说:“它正在溢出到界面上,影响到用户。“黑进 kubectl 运行并得到一个演示并运行仍然相当容易。但是,运行生产应用程序并弄清楚如何安全地暴露它们需要了解大量不同的功能,这不可避免地导致 YAML 文件比大多数微服务源代码还要长。”

Newman 总结了一个基本挑战:“Kubernetes 对开发者并不友好。我们仍然没有一个好的、可靠的、类似 Heroku 的抽象,在 Kubernetes 之上被广泛使用,这让我感到震惊。”

Spotify 的工程总监 Pia Nilsson 曾谈到,这家快速扩张的公司的新工程师平均需要 60 天才能合并他们的第 10 个 pull request。作为回应,该公司建立了一个 开发者门户网站 Backstage,现在是云原生计算基金会的一个 沙盒项目。

Netflix 非常重视 DevEx—— 该公司为开发者铺设的 “道路”—— 利用它来帮助加速 GraphQL 等新技术的采用。同样,我们已经看到了内部建设和通过 Humanitec 等供应商建设的开发者平台的崛起。 Ambassador Labs 有一个相关的开发者控制平面的概念 —— 它的网站声称,“使开发者能够控制和配置整个云开发循环,以便更快地发布软件”。


“Kubernetes 对开发者并不友好。我们仍然没有一个好的、可靠的、类似 Heroku 的抽象,在 Kubernetes 之上被广泛使用,这让我感到震惊。” ——Sam Newman, Building Microservices 作者
Ambassador Labs 的开发者关系总监 Daniel Bryant 告诉 The New Stack:“如果你看看 Airbnb、Shopify 和 Lunar 等公司正在做什么,它们之间有一个明显的共同点。他们正在为他们的开发者创建一个类似于 Heroku 的 CLI,这样,像‘创建新的微服务’这样的命令就会产生一些支架,插入 CI,插入管道,插入可观察性。问题是,你向开发者展示的抽象是什么,以便他们获得所需的可见性,同时也使他们所需的要求变得清晰?”

Bryant 特继续说:“开发者需要指定某些操作特性:这是一个内存大的服务;这个服务需要低延迟;这个服务需要非常接近那个服务。目前,你通过启动 Kubernetes 和编写大量的 YAML 来做到这一点。那里的抽象并不完全正确,特别是当你引入其他部署机制时,如无服务器和低代码/无代码。

“我想知道谁能通过平台暴露出正确的抽象概念,然后让工程师决定如何打包他们的代码 —— 但他们打包的方式是一样的,而平台暴露出一些传统上属于运维的属性。”

开放应用模型(OAM)

其他几个关于 Kubernetes 的倡议也值得跟踪。由微软和阿里云联合创建的开放应用模型(OAM)是一个描述应用的规范,将应用定义与集群的操作细节分开。因此,它使应用程序开发人员能够专注于其应用程序的关键要素,而不是其部署地点的操作细节。

Crossplane 是 OAM 的 Kubernetes 特定实现。它可以被企业用来在各种基础设施和云供应商之间建立和运维一个内部平台即服务(PaaS),这使得它在多云环境中特别有用,比如在那些兼并和收购越来越常见的大型企业中。

虽然 OAM 试图将部署细节的责任从编写服务代码中分离出来,但服务网格旨在通过一个专门的基础设施层将服务间通信的责任从个人开发者那里转移出来,该层侧重于使用代理管理服务间的通信。不幸的是,它们也有复杂性的问题,而且还可能引入相当大的性能开销。

因此,到目前为止,许多在生产中成功实施服务网格的案例都是在那些非常精通技术的初创公司。在 2020 年与 InfoQ 的 Wes Reisz 的播客中,Newman 建议在选择之前等待 6 个月,他告诉 The New Stack,他仍然给出同样的建议。

“就该技术栈的权重、管理、影响以及性能带来的影响而言,它们的现实情况是非常可怕的,”Newman 说。“对有一些组织说,如果没有它们,有些事情是不可能完成的,Monzo 就是一个很好的例子 —— 在一个组织中,你有一个异构的技术栈,你需要做大规模的双向 TLS,我可以看到它的价值。但在我看来,它仍然是‘概念很好,执行不力’。我想,我们可能会在很长时间内仍这样说。”

隐藏服务网格

有一件事可能会发生,至少对企业客户来说,性能问题往往不是那么尖锐,那就是服务网格被推到平台的更深处,并在很大程度上对开发者隐藏。例如,红帽 OpenShift 将 Istio 整合到平台层,还有多个类似的计划,将服务网格与公有云平台更紧密地整合在一起,如 AWS App Mesh 和 Google Cloud Platform Traffic Director。

关于服务网格的工作还在继续,以减少其所带来的网络开销。Cilium 团队的工作很有希望,它利用 Linux 内核中的 eBPF 功能来实现它所说的 “非常有效的网络、策略执行和负载均衡功能”。


我认为现在我们需要为其他人提供领域驱动设计(DDD)。因为即使是普通的开发者而不是架构师,也需要对如何确定实体的范围和边界有一定的了解,这其中有很多是回到了良好的 API 设计上。——Daniel Bryant,开发者关系总监,Ambassador Labs
但另一种可能性是,我们可能完全转向不同的运行时。Leading Edge Forum 的顾问 Simon Wardley 认为,函数即服务(Faas)/无服务器将最终取代 Kubernetes,成为分布式应用事实上的标准运行时,我们也看到了一些真实的生产实例,比如 BBC,它的大部分在线架构已经从之前的 LAMP 堆栈直接转向了 AWS 上的 Lambda。

“我认为 FaaS 是一个管理部署的伟大抽象,”Newman 说。“作为一个对开发者友好的部署软件的抽象,它是自 Heroku 以来我们拥有的最好的东西。我确实认为目前的实现方式很差,但他们会改进。但他们只处理了在一次在一个地方执行一件任务。这并没有解决更大的网络系统的抽象问题”。

作为一个例子,Newman 引用了微软 Azure 的 Durable Functions,它通过响应式扩展提供了类似于连续性的东西,允许开发者在无服务器环境中建立有状态的工作流和函数。但是,虽然部署抽象可能会有所改善,但如果想象你可以完全抽象出编写分布式系统的复杂性,那就太天真了。

“你不能假设你说的东西就在那里,”Newman 说。“你不能假设数据会神奇地从一个时间点瞬时传送到另一个时间点。因为它不是这样的。而且,再多的抽象也无法解决这个基本问题。”

自主团队的架构

另一个仍然具有挑战性的领域与整个系统架构有关,以及围绕团队组织和结构的相关问题。正如 IBM 的全球开发者 leader Holly Cummins 在《云原生是关于文化而不是容器》一文中指出的,“即使有适当的自主团队,系统级的考虑也不会消失”。

Eric Evans 的《领域驱动设计》是微服务运动的基石,任何软件架构师都应该阅读,Bryant 说。但他更进一步说:

“我认为现在我们需要为我们其他人提供 DDD,“他告诉 The New Stack。“因为即使是普通的开发者而不是架构师,也需要对如何确定实体和边界的范围有一定的了解,其中很多都要回到良好的 API 设计。一旦你理解了耦合和内聚的重要性,关注点和边界的分离,无论你处理的是什么抽象(模块、类、服务、应用),你都会自然而然地跳到这个齿轮上。”

Newman 的 Building Microservices 一书的第二版即将推出,该书介绍了很多这些概念,并考虑到了下一代服务。

在更新这本书时,Newman 告诉 The New Stack,“我想多谈一点耦合性。我想多谈一点内聚力。我想更多地谈论信息隐藏,这对我来说是现在最重要的事情。

“我认为,即使人们掌握了分布式系统方面的知识,他们也没有掌握一个事实,即从根本上说,微服务只是模块化架构的一种形式。然而,很多创建微服务的人对什么是模块化架构或如何进行模块化毫无概念。”

Newman 在新书中还引入了自 2014 年第一版出版以来出现的一些组织思维的变化。他特别引用了马修・斯凯尔顿(Matthew Skelton)和曼努埃尔・派斯(Manuel Pais)关于如何组织业务和技术团队以实现快速流动的极具影响力的作品《团队拓扑(Team Topologies)》,以及尼科尔・福斯格伦(Nicole Forsgren)、杰兹・汉伯(Jez Humble)和吉恩・金(Gene Kim)的《加速(Accelerate)》一书,该书探讨了精益管理和 DevOps 原则背后的科学。

修订过程不仅揭示了有多少关于微服务的新知识可以分享,而且这些知识是如何不断积累的。

“这本书可以让你广泛了解什么是微服务以及它对软件开发的影响,“Newman 说。“我发现我在向人们推荐,哦,你应该读那本书的第四章。现在我会说这个,而不是那个。我不想在推荐自己的书上一直含糊其辞。这就是为什么我写了第二版:因为我希望它是好的、准确的。”

原文链接:https://mp.weixin.qq.com/s/Ys0bFB7v2payCIuHDnDpSA 收起阅读 »

2021 年最佳 CI/CD 工具

CD CI

【编者的话】为持续集成(CI)和持续交付(CD)选择最好的工具是一件相当有挑战的事。以下列出了 30 多种工具和平台,可帮助你做出充分了解。

持续集成(CI)和持续交付(CD)简介

CD 的意思是持续交付,而 CI 的意思是持续集成。

CI 和 CD 都是 DevOps 的一部分,在这种情况中,将开发和运营流程结合到单一的协作工作流中,以确保两个团队在同一页面下。

CI 代表什么?持续集成是指应用程序的新建的部分都集成到现有的部分中,经过仔细测试,然后进入下一阶段。CD 持续交付是开发人员逐步构建项目,每个阶段都可以随时交付给客户。

2021 年可供选择的 CI/CD 工具

下面列出了 30 多种 CI CD 工具,这些工具可用于 DevOps 流程中的通用和特定任务。

1. Jenkins

基于 Java 基础,免费开源软件市场上最受欢迎的 CI/CD 工具之一。

Jenkins 将持续交付和集成工具与实时测试和报告相结合。

2. TeamCity

TeamCity 是 JetBrains 的产品。与 Jenkins 一样,也是 Java 的开源软件。TeamCity 与 Docker 和 Kubernetes 完美集成。

3. CircleCI

CircleCI 简化了 DevOps 自动化和软件部署流程的最佳 CI/CD 工具。借助CircleCI,工作流可以在多个容器之间拆分、共享和重用。

4. Travis CI

Travis CI 增强了安全级别,成为企业发展首选。该平台与 Lambda Test 完美集成,简化了跨不同浏览器、平台和环境的 DevOps 测试过程。

5. Bamboo

Bamboo 被认为是最流行的持续集成工具之一。Bamboo 是由 Atlassian 开发。该平台允许启动和测试 100 个远程构建代理,并基于云或自托管的解决方案。

6. GOCD

具有突出特点的持续集成平台:GOCD允许 DevOps 专家创建价值流图,以更好地查看和映射DevOps 管道。

7. CodeShip

CodeShip 的显着特点是可以直接从 GitHub 进行测试、构建和部署。

8. GitLab CI

如果开发团队在 GitLab 存储库中托管代码,则使用此工具进行 DevOps 工程是一个明智的选择。GitLab CI 允许 DevOps 开发人员从单个仪表板审查代码、部署、集成和交付。

9. Jenkins X

Jenkins X 是用于 DevOps 部署使用 Kubernetes 和 Docker 构建的应用程序的 CI/CD 工具之一。该平台非常适合在将不同环境合并到存储库之前对其进行智能管理。

10. Shippable

Shippable 的理念是使软件发布更具可预测性并避免Bug和错误。Shippable 结合了广泛的测试、自动化和部署工具。

11. Buildkite

如果要在自己的基础架构上开发应用程序,Buildkite 非常适合此特定功能。Buildkite 构建快速、安全且有良好的测试管道,以简化持续集成。

12. Concourse CI

非常适合在 DevOps 流程中使用 Vagrant 的应用程序的 CI/CD 工具之一。它侧重于非常直观的 UI,以帮助 DevOps 顾问构建可视化的管道。

13. Codefresh

此工具基于 Kubernetes 构建,可以使用在 Azure 和 Amazon Web Services(AWS)上进行部署。内置库加快了添加和删除开发人员需要或不需要的功能的过程。

14. Buddy

Buddy 的开发人员表示,这是获得最快 CI/CD 的平台。在Buddy的帮助下,平均部署时间减少到 12 秒,而 DevOps 团队采用 CI/CD 的速度可以提高 87%。

15. Buildbot

一个用 Python 构建的持续交付测试框架。Buildbot 还具有一组用于发布管理、应用程序部署和自动化的功能。

16. Semaphore

Semaphore 是特别适用于 iOS 应用程序的 CI/CD 工具。Semaphore 允许为 Windows 和 Linux 构建的解决方案运行 DevOps 流程,此外Semaphore 还支持许多用于创建 Android 应用程序的编程语言。

17. Wercker

Oracle 创建的基于 Docker 的 CD 工具。Wercker 可以免费使用,也可以在私有云中付费使用。

18. Integrity

具有令人印象深刻的标准功能集的持续集成服务器。

19. Weave Flux

该平台是在 CD 管道末端使用的持续交付工具。它帮助开发人员确保配置更改已上传到集群。

20. NeverCode

一个完全自动化的工具,可在不同环境中部署测试场景,生成报告,并在每次提交后构建代码,无需人工干预。

21. AutoRabit

如果使用 SalesForce 开发应用程序,可以考虑使用此工具,因为 AutoRabit 与 SalesForce 完美配合。SalesForce 平台允许开发人员改进他们的代码、管理发布和提高数据安全性。

22. CruiseControl

一个适用于自定义解决方案的开源和基于 Java 的持续集成框架。它带有数 10 个插件,开发人员可以根据自己的需要进行定制。

23. Bitrise

如果未来的解决方案需要一流的用户体验,除了其他 CI/CD 工具外,可以关注此平台。它主要支持用于创建移动应用程序的编程语言,并提供一种制作具有竞争力的解决方案的方法。

24. Drone CI

CICD 工具列表中又一个值得关注的平台。Drone CI 的显着特点是可以创建分布式管道。

25. UrbanCode

此解决方案适用于企业应用程序。持续集成和持续交付的每个功能都是根据企业解决方案的需求量身定制的,增强的数据安全性就是其中之一。

26. Strider

Strider 是用于简化 CI/CD 任务的完美可定制且有效的解决方案。它与 Heroku 集成(云服务提供商)并支持 Docker。

27. FinalBuilder

一种持续交付工具,可以最大化的自动化构建。它带有 600 多个可用于测试的预开发脚本和操作。

28. GitHub Actions

此工具允许直接在 GitHub 存储库中创建 CI/CD 工作流,这样就可以在 GitHub 上管理所有任务。

29. Azure DevOps

Azure DevOps是微软开发的一体化解决方案。有了它,你可以使用内置的 Azure Pipelines、Visual Studio Team Services(VSTS)和软件交付服务。

30. Terraform

另一个著名的基础设施管理平台。考虑将 Terraform 添加到 CircleCI 以在同一管道中部署您的基础设施。

31. Tekton

借助这个直观的工具,您可以使用简单的代码通过并行执行来表达复杂的工作流。Tekton是与 Kubernetes 一起使用的最佳工具之一。

32. Big Eval

在一个面向数据的项目,Big Eval 可以满足 DevOps 需求的解决方案。该工具在回归测试和基于元数据的测试的帮助下检查项目开发的每个阶段的数据质量,从而创建合格的 DataOps 流程。

原文链接:https://www.toutiao.com/a7009087679573148174
继续阅读 »

【编者的话】为持续集成(CI)和持续交付(CD)选择最好的工具是一件相当有挑战的事。以下列出了 30 多种工具和平台,可帮助你做出充分了解。

持续集成(CI)和持续交付(CD)简介

CD 的意思是持续交付,而 CI 的意思是持续集成。

CI 和 CD 都是 DevOps 的一部分,在这种情况中,将开发和运营流程结合到单一的协作工作流中,以确保两个团队在同一页面下。

CI 代表什么?持续集成是指应用程序的新建的部分都集成到现有的部分中,经过仔细测试,然后进入下一阶段。CD 持续交付是开发人员逐步构建项目,每个阶段都可以随时交付给客户。

2021 年可供选择的 CI/CD 工具

下面列出了 30 多种 CI CD 工具,这些工具可用于 DevOps 流程中的通用和特定任务。

1. Jenkins

基于 Java 基础,免费开源软件市场上最受欢迎的 CI/CD 工具之一。

Jenkins 将持续交付和集成工具与实时测试和报告相结合。

2. TeamCity

TeamCity 是 JetBrains 的产品。与 Jenkins 一样,也是 Java 的开源软件。TeamCity 与 Docker 和 Kubernetes 完美集成。

3. CircleCI

CircleCI 简化了 DevOps 自动化和软件部署流程的最佳 CI/CD 工具。借助CircleCI,工作流可以在多个容器之间拆分、共享和重用。

4. Travis CI

Travis CI 增强了安全级别,成为企业发展首选。该平台与 Lambda Test 完美集成,简化了跨不同浏览器、平台和环境的 DevOps 测试过程。

5. Bamboo

Bamboo 被认为是最流行的持续集成工具之一。Bamboo 是由 Atlassian 开发。该平台允许启动和测试 100 个远程构建代理,并基于云或自托管的解决方案。

6. GOCD

具有突出特点的持续集成平台:GOCD允许 DevOps 专家创建价值流图,以更好地查看和映射DevOps 管道。

7. CodeShip

CodeShip 的显着特点是可以直接从 GitHub 进行测试、构建和部署。

8. GitLab CI

如果开发团队在 GitLab 存储库中托管代码,则使用此工具进行 DevOps 工程是一个明智的选择。GitLab CI 允许 DevOps 开发人员从单个仪表板审查代码、部署、集成和交付。

9. Jenkins X

Jenkins X 是用于 DevOps 部署使用 Kubernetes 和 Docker 构建的应用程序的 CI/CD 工具之一。该平台非常适合在将不同环境合并到存储库之前对其进行智能管理。

10. Shippable

Shippable 的理念是使软件发布更具可预测性并避免Bug和错误。Shippable 结合了广泛的测试、自动化和部署工具。

11. Buildkite

如果要在自己的基础架构上开发应用程序,Buildkite 非常适合此特定功能。Buildkite 构建快速、安全且有良好的测试管道,以简化持续集成。

12. Concourse CI

非常适合在 DevOps 流程中使用 Vagrant 的应用程序的 CI/CD 工具之一。它侧重于非常直观的 UI,以帮助 DevOps 顾问构建可视化的管道。

13. Codefresh

此工具基于 Kubernetes 构建,可以使用在 Azure 和 Amazon Web Services(AWS)上进行部署。内置库加快了添加和删除开发人员需要或不需要的功能的过程。

14. Buddy

Buddy 的开发人员表示,这是获得最快 CI/CD 的平台。在Buddy的帮助下,平均部署时间减少到 12 秒,而 DevOps 团队采用 CI/CD 的速度可以提高 87%。

15. Buildbot

一个用 Python 构建的持续交付测试框架。Buildbot 还具有一组用于发布管理、应用程序部署和自动化的功能。

16. Semaphore

Semaphore 是特别适用于 iOS 应用程序的 CI/CD 工具。Semaphore 允许为 Windows 和 Linux 构建的解决方案运行 DevOps 流程,此外Semaphore 还支持许多用于创建 Android 应用程序的编程语言。

17. Wercker

Oracle 创建的基于 Docker 的 CD 工具。Wercker 可以免费使用,也可以在私有云中付费使用。

18. Integrity

具有令人印象深刻的标准功能集的持续集成服务器。

19. Weave Flux

该平台是在 CD 管道末端使用的持续交付工具。它帮助开发人员确保配置更改已上传到集群。

20. NeverCode

一个完全自动化的工具,可在不同环境中部署测试场景,生成报告,并在每次提交后构建代码,无需人工干预。

21. AutoRabit

如果使用 SalesForce 开发应用程序,可以考虑使用此工具,因为 AutoRabit 与 SalesForce 完美配合。SalesForce 平台允许开发人员改进他们的代码、管理发布和提高数据安全性。

22. CruiseControl

一个适用于自定义解决方案的开源和基于 Java 的持续集成框架。它带有数 10 个插件,开发人员可以根据自己的需要进行定制。

23. Bitrise

如果未来的解决方案需要一流的用户体验,除了其他 CI/CD 工具外,可以关注此平台。它主要支持用于创建移动应用程序的编程语言,并提供一种制作具有竞争力的解决方案的方法。

24. Drone CI

CICD 工具列表中又一个值得关注的平台。Drone CI 的显着特点是可以创建分布式管道。

25. UrbanCode

此解决方案适用于企业应用程序。持续集成和持续交付的每个功能都是根据企业解决方案的需求量身定制的,增强的数据安全性就是其中之一。

26. Strider

Strider 是用于简化 CI/CD 任务的完美可定制且有效的解决方案。它与 Heroku 集成(云服务提供商)并支持 Docker。

27. FinalBuilder

一种持续交付工具,可以最大化的自动化构建。它带有 600 多个可用于测试的预开发脚本和操作。

28. GitHub Actions

此工具允许直接在 GitHub 存储库中创建 CI/CD 工作流,这样就可以在 GitHub 上管理所有任务。

29. Azure DevOps

Azure DevOps是微软开发的一体化解决方案。有了它,你可以使用内置的 Azure Pipelines、Visual Studio Team Services(VSTS)和软件交付服务。

30. Terraform

另一个著名的基础设施管理平台。考虑将 Terraform 添加到 CircleCI 以在同一管道中部署您的基础设施。

31. Tekton

借助这个直观的工具,您可以使用简单的代码通过并行执行来表达复杂的工作流。Tekton是与 Kubernetes 一起使用的最佳工具之一。

32. Big Eval

在一个面向数据的项目,Big Eval 可以满足 DevOps 需求的解决方案。该工具在回归测试和基于元数据的测试的帮助下检查项目开发的每个阶段的数据质量,从而创建合格的 DataOps 流程。

原文链接:https://www.toutiao.com/a7009087679573148174 收起阅读 »

如何在Service Mesh微服务架构中实现金丝雀发布?


今天的文章聊聊有关Service Mesh微服务架构的话题,如果对之前的聊过的话题还不了解,可以参考文末的推荐阅读。本文主要讲的话题是:如何在Service Mesh微服务架构中实现“金丝雀发布”?

什么是金丝雀发布

既然要聊具体的实现,那么在开始之前,先科普下什么是“金丝雀发布”。金丝雀发布也叫“灰度发布”,具体来说就是在发布线上版本时,先将少量的生产流量打到服务的新版本,以验证新版本的准确性和可靠性,待发布的新版本得到线上流量的全面验证后,在逐步将所有流量放入新版本,以实现生产服务版本的稳定更新。


为什么叫金丝雀发布呢,是因为金丝雀对矿场中的毒气比较敏感,所以在矿场开工前工人们会放一只金丝雀进去,以验证矿场是否存在毒气,这便是金丝雀发布名称的由来。
在不同技术栈场景中,金丝雀发布的实现方式也不尽相同:有通过nginx实现的、也有借助A/B测试实现的。而随着以Kubernetes为代表的云原生基础设施的普及,金丝雀发布作为一项基本的服务发布功能,其实现方式也有了一些新的趋势——那就是逐步与云原生基础设施融为一体,成为基础设施服务的一部分

Kubernetes中的金丝雀(灰度)发布

接下来,先看看在Kubernetes中是如何实现版本更新的。以下内容假设你已经有了一套可用的Kubernetes环境,如果没有可以查看文末推荐阅读的文章链接,参考相关分享自行部署。

滚动更新

在介绍Kubernetes中的金丝雀(灰度)发布之前,先来了解下Kubernetes中最重要的应用部署方式——“滚动升级”。

所谓“滚动升级”:是指当更新了Kubernetes中Deployment编排资源的Pod模版(例如更新镜像版本号)之后,Deployment就需要遵循一种叫做“滚动更新(rolling update)”的方式,来升级现有的容器,从而实现应用对外服务的“不中断更新部署”。Kubernetes实现“滚动升级”的示意图如下:
1.png

如上图所示,滚动升级的过程为:

  1. 当容器开始升级时,集群中会先启动一个新版本的Pod,并终止一个旧版本的Pod。
  2. 如果此时,新版本的Pod有问题启动不了,那么“滚动升级”就会停止,并允许开发和运维人员介入。而在这个过程中,由于应用本身还有两个旧版本的Pod在线,所以服务并不会受到太大的影响。
  3. 而如果新版本的Pod启动成功,且服务访问正常,则继续滚动升级,直至按照Deployment编排器设置的副本数量,完成后续旧版本Pod的升级。


在Kubernetes中Deployment还可以通过相应地“滚动升级”策略,来控制Pod的滚动升级行为,以进一步保证服务的连续性。例如:“在任何时间窗口内,只有指定比例的Pod处于离线状态;在任何时间窗口内,只有指定比例的新Pod被创建出来”。可以通过相应地控制参数进行设置,如下:
...
spec:
selector:
matchLabels:
  app: micro-api
replicas: 3
#设置滚动升级策略
#Kubernetes在等待设置的时间后才开始进行升级,例如5秒
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
  #升级过程中最多可以比原先设置多出的Pod数量
  maxSurge: 1
  #升级过程中Deployment控制器最多可以删除多少个旧Pod,主要用于提供缓冲时间
  maxUnavailable: 1
...

在上面RollingUpdate Strategy(滚动升级策略)的配置中:
  • maxSurge:指定的是,除了设定的Pod副本数量之外,在一次“滚动”中,Deployment控制器还可以创建多少个新的Pod。
  • maxUnavailable:指的是,在一次“滚动”中,Deployment控制器可以删除多少个旧Pod。



通过这种精确的“滚动升级”策略,可以使得Kubernetes服务版本发布的过程更加平滑。此外,这两个配置还可以通过百分比的方式来表示,比如“maxUnavailable=50%”,指的是Deployment控制器最多可以一次删除“50%*设定Pod副本数”个Pod。
接下来具体演示下在Kubernetes中进行服务滚动升级的详细过程。

该项目以Spring Boot编写的Java服务为主,在体验上更接近真实的项目开发场景。项目的结构如下:

2.png

该项目所在的GitHub地址为:https://github.com/manongwudi/ ... -demo

“滚动升级”演示:

这里先借助示例项目中的“micro-api”服务来演示其在Kubernetes中进行“滚动升级”的过程,步骤如下:

1、首先准备“micro-api”服务的k8s发布文件(如:micro-api.yaml)。代码如下:

apiVersion: v1
kind: Service
metadata:
name: micro-api
spec:
type: ClusterIP
ports:
- name: http
  port: 19090
  targetPort: 9090
selector:
app: micro-api

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: micro-api
spec:
selector:
matchLabels:
  app: micro-api
replicas: 3
#设置滚动升级策略
#Kubernetes在等待设置的时间后才开始进行升级,例如5秒
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
  #升级过程中最多可以比原先设置多出的Pod数量
  maxSurge: 1
  #升级过程中Deployment控制器最多可以删除多少个旧Pod
  maxUnavailable: 1
template:
metadata:
  labels:
    app: micro-api
spec:
  #设置的阿里云私有镜像仓库登陆信息的secret(对应2.1.2的设置)
  imagePullSecrets:
    - name: regcred
  containers:
    - name: micro-api
      image: registry.cn-hangzhou.aliyuncs.com/wudimanong/micro-api:1.0-SNAPSHOT
      imagePullPolicy: Always
      tty: true
      ports:
        - name: http
          protocol: TCP
          containerPort: 19090

上述部署文件设置了“micro-api”服务的Pod副本个数为“3”,并且设置了相应地滚动升级策略。

2、接下来执行Kubernetes部署命令,如下:

$ kubectl apply -f micro-api.yaml

成功后,查看Deployment创建后的状态信息,命令效果如下:
$ kubectl get deployments
NAME          READY   UP-TO-DATE   AVAILABLE   AGE
micro-api     3/3     3            3           190d

从上述命令的返回结果中,可以看到三个状态字段,它们的含义如下所示:

  • READY:表示用户期望的Pod副本个数,以及当前处于Running状态的Pod个数。
  • UP-TO-DATE:当前处于最新版本的Pod个数。所谓最新版本,指的是Pod的Spec部分与Deployment中Pod模版里定义的完全一致。
  • AVAILABLE:当前已经可用的Pod的个数——既是Running状态,又是最新版本,并且已经处于Ready(监控检查正确)状态的Pod个数。


3、模拟服务版本升级,触发滚动升级。

接下来重新构建“micro-api”服务的版本,并将其上传至私有镜像仓库。之后,通过命令修改“micro-api”的Deployment所使用的镜像,并触发滚动升级。

修改Deployment所使用的镜像的命令如下:

$ kubectl set image deployment/micro-api micro-api=registry.cn-hangzhou.aliyuncs.com/wudimanong/micro-api:1.1-SNAPSHOT
deployment.apps/micro-api image updated

这里使用了“kubectl set image”指令,主要是为了方便操作,也可以直接在Kubernetes部署文件中进行镜像版本的修改。

修改完Deployment的镜像版本后,Kubernetes会立即触发“滚动升级”的过程。可以通过“kubectl rollout status”指令来查看Deployment资源的状态变化。具体如下:

$ kubectl rollout status deployment/micro-api

Waiting for deployment "micro-api" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "micro-api" rollout to finish: 2 out of 3 new replicas have been updated...

Waiting for deployment "micro-api" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "micro-api" rollout to finish: 2 of 3 updated replicas are available...
Waiting for deployment "micro-api" rollout to finish: 2 of 3 updated replicas are available...
deployment "micro-api" successfully rolled out

这时,也可以通过查看Deployment的Events,看到这个“滚动升级”的过程。具体如下:
$ kubectl describe deployment micro-api
...
OldReplicaSets:  <none>
NewReplicaSet:   micro-api-d745d8649 (3/3 replicas created)
Events:
Type    Reason             Age   From                   Message
----    ------             ----  ----                   -------
Normal  ScalingReplicaSet  12m   deployment-controller  Scaled up replica set micro-api-677dd4d5b6 to 1
Normal  ScalingReplicaSet  12m   deployment-controller  Scaled down replica set micro-api-57c7cb5b74 to 2
Normal  ScalingReplicaSet  12m   deployment-controller  Scaled up replica set micro-api-677dd4d5b6 to 2
Normal  ScalingReplicaSet  5m1s  deployment-controller  Scaled down replica set micro-api-677dd4d5b6 to 0
Normal  ScalingReplicaSet  5m    deployment-controller  Scaled up replica set micro-api-d745d8649 to 2
Normal  ScalingReplicaSet  56s   deployment-controller  Scaled down replica set micro-api-57c7cb5b74 to 0
Normal  ScalingReplicaSet  56s   deployment-controller  Scaled up replica set micro-api-d745d8649 to 3

可以看到,当你修改了Deployment里的Pod定义后,“Deployment Controller”会使用这个修改后的Pod模版,创建一个新的ReplicaSet,这个新的ReplicaSet的初始Pod副本数是:0。

然后在Age=12 m的位置,开始将这个新的ReplicaSet所控制的Pod副本数从0个变成1个。

紧接着,在Age=12 m的位置,又将旧ReplicaSet所控制的Pod副本数减少1个,即“水平收缩”成两个副本。

如此交替进行,新ReplicaSet所管理的Pod的副本数,从0个变成1个,再变成2个,最后变成3个;而旧ReplicaSet所管理的Pod的副本数则从3个变成2个,最后变成0个

这样,就完成了一组Pod的版本升级过程。而像这样将一个Kubernetes集群中正在运行的多个Pod版本,交替逐一升级的过程,就是“滚动升级”

金丝雀(灰度)发布

上面内容比较详细的演示了Kubernetes的“滚动升级”的方式,虽然通过滚动升级的方式可以方便、平滑的实现版本更新,但是这个过程,并没有灰度功能。滚动升级的方式,虽然中间有缓冲交替的过程,但这种过程是自动的、迅速的,滚动升级过程结束就相当于直接进行了新版本的全量发布

而对于需要进行金丝雀(灰度)发布的场景,“滚动升级”的方式很显然是不够用的。那么,在Kubernetes中应该如何结合版本更新做到金丝雀(灰度)发布呢?

具体步骤如下:

1、编写实现新版本灰度发布的部署文件。

为了实现在Kubernetes中的金丝雀(灰度)发布过程的可观测,我们重新定义下具体的Kubernetes发布文件(如:micro-api-canary.yaml)的内容如下:
...
spec:
selector:
matchLabels:
  app: micro-api
replicas: 3
#设置滚动升级策略
#Kubernetes在等待设置的时间后才开始进行升级,例如5秒
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
  #升级过程中最多可以比原先设置多出的Pod数量
  maxSurge: 1
  #升级过程中Deployment控制器最多可以删除多少个旧Pod,主要用于提供缓冲时间
  maxUnavailable: 1
...

上述发布文件与上一小节中演示滚动升级时,发布文件的内容一致,只是为了方便观察灰度发布过程的实现,这里通过“track: canary”对新发布的Pod版本进行标记

设置新版本的镜像为:“micro-api:1.3-SNAPSHOT”。并且通过“spec.selector.matchLabels.app:micro-api”与历史版本Pod所对应的Service(micro-api.yaml文件中定义的Service)资源定义匹配。

2、执行“滚动升级”发布命令,实现“灰度发布”效果。
$ kubectl apply -f micro-api-canary.yaml && kubectl rollout pause deployment/micro-api

上面通过“kubectl rollout pause”命令实现对Deployment的金丝雀(灰度发布)。执行发布命令之后的运行效果如下:
$ kubectl get pods --show-labels -o wide
NAME                         READY   STATUS    RESTARTS   AGE     IP          NODE         NOMINATED NODE   READINESS GATES   LABELS
micro-api-57c7cb5b74-mq7m9   1/1     Running   0          6m20s   10.32.0.3   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=57c7cb5b74
micro-api-57c7cb5b74-ptptj   1/1     Running   0          6m20s   10.32.0.4   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=57c7cb5b74
micro-api-7dbb6c5d66-4rbdc   1/1     Running   0          5m33s   10.32.0.6   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=7dbb6c5d66,track=canary
micro-api-7dbb6c5d66-cfk9l   1/1     Running   0          5m33s   10.32.0.5   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=7dbb6c5d66,track=canary

查看Deployment的滚动升级情况,命令如下:
$ kubectl get deployments
NAME            READY   UP-TO-DATE   AVAILABLE   AGE
micro-api       4/3     2            4           194d

可以看到此时“micro-api” ready的数量为4,其中两个旧版本Pod,两个新版本Pod。

3、接下来进行流量测试。

查询两组Pod版本所对应的Service资源的IP,命令如下:
# kubectl get svc micro-api
NAME        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)     AGE
micro-api   ClusterIP   10.110.169.161   <none>        19090/TCP   194d

接下来,模拟对服务的接口进行批量访问,命令如下:

$ for i in {1..10}; do curl 10.110.169.161:19090/test/test; done

{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"} 

可以看到,此时流量会随机的流向旧版本和新版本(日志标记为V3)的服务。

4、将服务版本升级为新版本。

如果新版本的服务经过线上流量测试验证没有问题,则可以通过“rollout resume”命令将整体服务的版本升级为新版本。命令如下:
$ kubectl rollout resume deployment micro-api
deployment.apps/micro-api resumed

升级后的效果如下:
$ kubectl get pods --show-labels -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP          NODE         NOMINATED NODE   READINESS GATES   LABELS
micro-api-7dbb6c5d66-4rbdc   1/1     Running   0          18m   10.32.0.6   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=7dbb6c5d66,track=canary
micro-api-7dbb6c5d66-bpjtg   1/1     Running   0          84s   10.32.0.3   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=7dbb6c5d66,track=canary
micro-api-7dbb6c5d66-cfk9l   1/1     Running   0          18m   10.32.0.5   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=7dbb6c5d66,track=canary

可以看到,此时目标服务已经通过“滚动升级”的方式完成了全量更新。而如果存在问题,则通过“kubectl rollout undo”命令进行回滚即可!

从上述过程可以看到,Kubernetes中的金丝雀(灰度发布)主要是通过操纵(如:pause)“滚动升级”的过程来实现的——通过发布一定数量的新版本Pod,并利用Service资源类型本身的负载均衡能力来实现流量在新/旧Pod之间的随机交替。

这样的方式虽然已经可以满足一些简单的场景,但是没有办法做到更精准的灰度流量控制。这时候就需要借助 Service Mesh 中的解决方案了,下面我们来看看在 Istio 中如何做到精准流量的金丝雀(灰度)发布。

Istio中的金丝雀(灰度)发布

以下内容默认你已经在Kubernetes中安装了Istio环境,如果还没有安装可以参考《干货|如何步入Service Mesh微服务架构时代》中分享的内容。

Istio与Kubernetes实现金丝雀(灰度)发布的方式不一样,Istio通过Envoy(SideCar)强大的路由规则管理能力,可以非常灵活地控制对应版本的流量占比,从而实现具备精准流量控制能力的金丝雀(灰度)发布功能

Istio通过Envoy(SideCar)实现金丝雀(灰度)发布的流量路由示意图如下(继续以“micro-api”服务为例):
3.png

从上图中可以大致看出,Istio具备强大的流量管理能力,而这种能力对于实现流量精准控制的金丝雀(灰度)发布功能来说,自然是水到渠成的。

具体来说,在Istio中是通过VirtualService(虚拟服务)这种特定的资源在服务网格中实现流量路由的。通过VirtualService可以方便地定义流量路由规则,并在客户端试图连接到服务时应用这些规则,并最终到达目标服务。

接下来,具体演示如何在Istio中通过VirtualService实现金丝雀(灰度)发布。步骤如下:

1、首先发布一个v1版本的服务。

要在Istio中实现更精准的版本控制,需要在发布Pod资源时,通过明确的“版本标签”进行指定。准备“micro-api”服务v1版本的Kubernetes部署文件(micro-api-canary-istio-v1.yaml):
apiVersion: v1
kind: Service
metadata:
name: micro-api
spec:
type: ClusterIP
ports:
- name: http
  port: 19090
  targetPort: 9090
selector:
app: micro-api

---

apiVersion: apps/v1
kind: Deployment
meta data:
name: micro-api-v1
spec:
selector:
matchLabels:
  app: micro-api
  #这里是关键,需要设置版本标签,以便实现灰度发布
  version: v1
replicas: 3
#设置滚动升级策略
#Kubernetes在等待设置的时间后才开始进行升级,例如5秒
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
  #升级过程中最多可以比原先设置多出的Pod数量
  maxSurge: 1
  #升级过程中Deployment控制器最多可以删除多少个旧Pod,主要用于提供缓冲时间
  maxUnavailable: 1
template:
metadata:
  labels:
    app: micro-api
    #设置版本标签,便于灰度发布
    version: v1
spec:
  #设置的阿里云私有镜像仓库登陆信息的secret
  imagePullSecrets:
    - name: regcred
  containers:
    - name: micro-api
      image: registry.cn-hangzhou.aliyuncs.com/wudimanong/micro-api:1.1-SNAPSHOT
      imagePullPolicy: Always
      tty: true
      ports:
        - name: http
          protocol: TCP
          containerPort: 19090

“spec.selector.matchLabels.version:v1”标签用来标注服务的版本,该标签是后续Istio的流量管理规则中,识别服务版本的主要依据。

准备好发布文件后,执行发布命令:
$ kubectl apply -f micro-api-canary-istio-v1.yaml

此时,一个低版本的服务就运行成功了!接下来我们模拟对其实施金丝雀(灰度)发布。

2、发布一个v2版本的服务(升级的目标版本)。

与v1版本服务一样,发布的v2版本的服务也需要明确版本标签,其发布文件(micro-api-canary-istio-v2.yaml)的内容如下:

apiVersion: apps/v1
kind: Deployment
metadata:
name: micro-api-v2
spec:
selector:
matchLabels:
  app: micro-api
  #设置好版本标签,便于灰度发布
  version: v2
replicas: 3
#设置滚动升级策略
#Kubernetes在等待设置的时间后才开始进行升级,例如5秒
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
  #升级过程中最多可以比原先设置多出的Pod数量
  maxSurge: 1
  #升级过程中Deployment控制器最多可以删除多少个旧Pod,主要用于提供缓冲时间
  maxUnavailable: 1
template:
metadata:
  labels:
    app: micro-api
    #设置好版本标签,便于灰度发布
    version: v2
spec:
  #设置的阿里云私有镜像仓库登陆信息的secret
  imagePullSecrets:
    - name: regcred
  containers:
    - name: micro-api
      image: registry.cn-hangzhou.aliyuncs.com/wudimanong/micro-api:1.3-SNAPSHOT
      imagePullPolicy: Always
      tty: true
      ports:
        - name: http
          protocol: TCP
          containerPort: 19090

执行发布命令:
$ kubectl apply -f micro-api-canary-istio-v2.yaml 
deployment.apps/micro-api-v2 created

此时,系统中就存在了两组版本的Pod资源,具体如下:

# kubectl get pods
NAME                            READY   STATUS    RESTARTS   AGE
micro-api-v1-565d749dd4-7c66z   1/1     Running   2          13h
micro-api-v1-565d749dd4-7dqfb   1/1     Running   2          13h
micro-api-v1-565d749dd4-l62wc   1/1     Running   2          13h
micro-api-v2-6f98c598c9-5stlw   1/1     Running   0          82s
micro-api-v2-6f98c598c9-f2ntq   1/1     Running   0          82s
micro-api-v2-6f98c598c9-l8g4j   1/1     Running   0          82s

接下来将演示如何利用Istio强大的流量管理功能,来实现流量在这两组版本Pod资源之间的精确控制!

3、创建Istio网关资源。

在Istio中要实现流量的精确控制,需要将VirtualService绑定到具体的Ingressgateway(入口网关)资源。因此在创建VirtualService资源实现流量路由及控制前,需要创建一个Istio网关。部署文件(micro-gateway.yaml)的内容如下:
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: micro-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
    number: 80
    name: http
    protocol: HTTP
  hosts:
    - "*"

上述部署文件执行后将创建一个名称为“micro-gateway”的Istio网关,并允许所有主机(hosts:"*"指定)通过该网关。

4、创建Istio虚拟服务资源VirtualService。

前面提到过在Istio中主要是通过VirtualService(虚拟服务)来实现服务网格内的流量路由及控制。接下来我们看看VirtualService资源的具体创建方式,准备资源文件(如virtual-service-all.yaml),内容如下:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: micro-api-route
spec:
#用于定义流量被发送到的目标主机(这里为部署在Kubernetes中的micro-api服务)
hosts:
- micro-api.default.svc.cluster.local
#将VirtualService绑定到Istio网关,通过网关来暴露路由目标
gateways:
- micro-gateway
http:
- route:
    #设置旧版本(V1)版本的流量占比为70%
    - destination:
        host: micro-api.default.svc.cluster.local
        subset: v1
      #通过权重值来设置流量占比
      weight: 70
    #设置新版本(V2)版本的流量占比为30%
    - destination:
        host: micro-api.default.svc.cluster.local
        subset: v2
      weight: 30

如上所示,VirtualService资源具备针对http的精准流量控制能力,可以将指定占比的流量路由到特定的“subset”指定的版本。而为了实现这一能力,VirtualService资源还需要与Istio网关绑定,通过Istio网关来暴露路由目标

5、创建Istio目标路由规则资源。

虚拟服务VirtualService在Istio中主要用于控制流量的行为,而定义流量行为的路由规则则需要通过“DestinationRule”路由规则资源来定义。创建路由规则文件(destination-rule-all.yaml),具体内容如下:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: micro-api-destination
spec:
#与Deployment资源对应的Service资源名称关联
host: micro-api
#流量策略设置:负载均衡策略、连接池大小、局部异常检测等,在路由发生后作用于流量
trafficPolicy:
#限流策略
connectionPool:
  tcp:
    maxConnections: 10
  http:
    http1MaxPendingRequests: 1
    maxRequestsPerConnection: 1
#设置目的地的负债均衡算法
loadBalancer:
  simple: ROUND_ROBIN
#目的地指的是不同的子集(subset)或服务版本。通子集(subset),可以识别应用程序的不同版本,以实现流量在不同服务版本之间的切换
subsets:
- name: v1
  labels:
    version: v1
- name: v2
  labels:
    version: v2

如上所示,通过subsets属性,定义了VirtualService资源用于路由的具体版本标签匹配信息。至此,针对两个版本服务的灰度流量控制规则就设置好了,接下来测试具体的金丝雀(灰度)发布效果。

6、测试Istio实现金丝雀(灰度)发布的流量控制效果。

在正式测试之前,可以通过命令查看下当前的部署资源情况:

#查看部署的Deployment资源
kubectl get deploy  | grep micro-api

micro-api-v1             3/3     3            3           21h
micro-api-v2             3/3     3            3           8h

#查看两组版本Pod资源对应的K8s-Service的服务IP
kubectl get svc micro-api

NAME        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)     AGE
micro-api   ClusterIP   10.110.169.161   <none>        19090/TCP   205d

#查看VirtualService资源定义
kubectl get vs

NAME              GATEWAYS          HOSTS                                   AGE
micro-api-route   [micro-gateway]   [micro-api.default.svc.cluster.local]   7h34m

#查看定义的路由规则资源
kubectl get dr

NAME                    HOST        AGE
micro-api-destination   micro-api   7h27m

通过上面的资源信息查看,这里我们已经可以查到Deployments对应的K8s-Service资源的IP,但如果通过K8s-Service资源来进行测试的话,会发现流量的控制并不精准,并不能达到我们设置的70%流量流向v1,30%的流量流向v2(因为这是随机流量)

因此,要使用Istio的精准流量控制功能,还需要使用Istio的Ingressgateway。查看Istio的Ingressgateway资源IP的命令如下:

#查看ingress的IP
kubectl get svc -n istio-system | grep ingress

istio-ingressgateway   LoadBalancer   10.98.178.61     <pending>     15021:31310/TCP,80:32113/TCP,443:31647/TCP,31400:30745/TCP,15443:30884/TCP   7h54m

接下来,通过Ingress的IP来访问“micro-api”服务,命令及效果如下:

# for i in {1..10}; do curl -H "Host:micro-api.default.svc.cluster.local" 10.98.178.61:80/test/test; done

{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"} 

如上所示,流量按照设定的比例(v1:70%;v2:30%)进行了分流。

7、测试将流量全部切向新版本。

为了更明显地验证Istio的流量控制效果,接下来,我们通过变更VirtualService资源的流量设置占比,将流量全部切到新版本。变更后的VirtualService资源的配置文件内容如下:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: micro-api-route
spec:
#用于定义流量被发送到的目标主机(这里为部署在Kubernetes中的micro-api服务)
hosts:
- micro-api.default.svc.cluster.local
#将VirtualService绑定到Istio网关,通过网关来暴露路由目标
gateways:
- micro-gateway
http:
- route:
    #设置旧版本(V1)版本的流量占比为70%
    - destination:
        host: micro-api.default.svc.cluster.local
        subset: v1
      #通过权重值来设置流量占比
      weight: 0
    #设置新版本(V2)版本的流量占比为30%
    - destination:
        host: micro-api.default.svc.cluster.local
        subset: v2
      weight: 100

继续通过Istio网关访问目标服务,命令如下:

# for i in {1..10}; do curl -H "Host:micro-api.default.svc.cluster.local" 10.98.178.61:80/test/test; done

{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"} 

可以观察到,此时流量已经全部切换到了新版本服务!

后记

在微服务时代,不同的服务之间相互联系,关系错综复杂,部署升级一个服务,可能造成整个系统的瘫痪,因此,需要选择合适的部署方式,从而将风险降到最低。金丝雀(灰度)发布只是多种部署方式的一种,还有蓝绿部署、滚动部署(如K8s的滚动升级)等,可以根据不同的业务场景选择不同的发布形式。

原文链接:https://mp.weixin.qq.com/s/a8OLm7udd0T0PY2TskZELw
继续阅读 »

今天的文章聊聊有关Service Mesh微服务架构的话题,如果对之前的聊过的话题还不了解,可以参考文末的推荐阅读。本文主要讲的话题是:如何在Service Mesh微服务架构中实现“金丝雀发布”?

什么是金丝雀发布

既然要聊具体的实现,那么在开始之前,先科普下什么是“金丝雀发布”。金丝雀发布也叫“灰度发布”,具体来说就是在发布线上版本时,先将少量的生产流量打到服务的新版本,以验证新版本的准确性和可靠性,待发布的新版本得到线上流量的全面验证后,在逐步将所有流量放入新版本,以实现生产服务版本的稳定更新。


为什么叫金丝雀发布呢,是因为金丝雀对矿场中的毒气比较敏感,所以在矿场开工前工人们会放一只金丝雀进去,以验证矿场是否存在毒气,这便是金丝雀发布名称的由来。
在不同技术栈场景中,金丝雀发布的实现方式也不尽相同:有通过nginx实现的、也有借助A/B测试实现的。而随着以Kubernetes为代表的云原生基础设施的普及,金丝雀发布作为一项基本的服务发布功能,其实现方式也有了一些新的趋势——那就是逐步与云原生基础设施融为一体,成为基础设施服务的一部分

Kubernetes中的金丝雀(灰度)发布

接下来,先看看在Kubernetes中是如何实现版本更新的。以下内容假设你已经有了一套可用的Kubernetes环境,如果没有可以查看文末推荐阅读的文章链接,参考相关分享自行部署。

滚动更新

在介绍Kubernetes中的金丝雀(灰度)发布之前,先来了解下Kubernetes中最重要的应用部署方式——“滚动升级”。

所谓“滚动升级”:是指当更新了Kubernetes中Deployment编排资源的Pod模版(例如更新镜像版本号)之后,Deployment就需要遵循一种叫做“滚动更新(rolling update)”的方式,来升级现有的容器,从而实现应用对外服务的“不中断更新部署”。Kubernetes实现“滚动升级”的示意图如下:
1.png

如上图所示,滚动升级的过程为:

  1. 当容器开始升级时,集群中会先启动一个新版本的Pod,并终止一个旧版本的Pod。
  2. 如果此时,新版本的Pod有问题启动不了,那么“滚动升级”就会停止,并允许开发和运维人员介入。而在这个过程中,由于应用本身还有两个旧版本的Pod在线,所以服务并不会受到太大的影响。
  3. 而如果新版本的Pod启动成功,且服务访问正常,则继续滚动升级,直至按照Deployment编排器设置的副本数量,完成后续旧版本Pod的升级。


在Kubernetes中Deployment还可以通过相应地“滚动升级”策略,来控制Pod的滚动升级行为,以进一步保证服务的连续性。例如:“在任何时间窗口内,只有指定比例的Pod处于离线状态;在任何时间窗口内,只有指定比例的新Pod被创建出来”。可以通过相应地控制参数进行设置,如下:
...
spec:
selector:
matchLabels:
  app: micro-api
replicas: 3
#设置滚动升级策略
#Kubernetes在等待设置的时间后才开始进行升级,例如5秒
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
  #升级过程中最多可以比原先设置多出的Pod数量
  maxSurge: 1
  #升级过程中Deployment控制器最多可以删除多少个旧Pod,主要用于提供缓冲时间
  maxUnavailable: 1
...

在上面RollingUpdate Strategy(滚动升级策略)的配置中:
  • maxSurge:指定的是,除了设定的Pod副本数量之外,在一次“滚动”中,Deployment控制器还可以创建多少个新的Pod。
  • maxUnavailable:指的是,在一次“滚动”中,Deployment控制器可以删除多少个旧Pod。



通过这种精确的“滚动升级”策略,可以使得Kubernetes服务版本发布的过程更加平滑。此外,这两个配置还可以通过百分比的方式来表示,比如“maxUnavailable=50%”,指的是Deployment控制器最多可以一次删除“50%*设定Pod副本数”个Pod。
接下来具体演示下在Kubernetes中进行服务滚动升级的详细过程。

该项目以Spring Boot编写的Java服务为主,在体验上更接近真实的项目开发场景。项目的结构如下:

2.png

该项目所在的GitHub地址为:https://github.com/manongwudi/ ... -demo

“滚动升级”演示:

这里先借助示例项目中的“micro-api”服务来演示其在Kubernetes中进行“滚动升级”的过程,步骤如下:

1、首先准备“micro-api”服务的k8s发布文件(如:micro-api.yaml)。代码如下:

apiVersion: v1
kind: Service
metadata:
name: micro-api
spec:
type: ClusterIP
ports:
- name: http
  port: 19090
  targetPort: 9090
selector:
app: micro-api

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: micro-api
spec:
selector:
matchLabels:
  app: micro-api
replicas: 3
#设置滚动升级策略
#Kubernetes在等待设置的时间后才开始进行升级,例如5秒
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
  #升级过程中最多可以比原先设置多出的Pod数量
  maxSurge: 1
  #升级过程中Deployment控制器最多可以删除多少个旧Pod
  maxUnavailable: 1
template:
metadata:
  labels:
    app: micro-api
spec:
  #设置的阿里云私有镜像仓库登陆信息的secret(对应2.1.2的设置)
  imagePullSecrets:
    - name: regcred
  containers:
    - name: micro-api
      image: registry.cn-hangzhou.aliyuncs.com/wudimanong/micro-api:1.0-SNAPSHOT
      imagePullPolicy: Always
      tty: true
      ports:
        - name: http
          protocol: TCP
          containerPort: 19090

上述部署文件设置了“micro-api”服务的Pod副本个数为“3”,并且设置了相应地滚动升级策略。

2、接下来执行Kubernetes部署命令,如下:

$ kubectl apply -f micro-api.yaml

成功后,查看Deployment创建后的状态信息,命令效果如下:
$ kubectl get deployments
NAME          READY   UP-TO-DATE   AVAILABLE   AGE
micro-api     3/3     3            3           190d

从上述命令的返回结果中,可以看到三个状态字段,它们的含义如下所示:

  • READY:表示用户期望的Pod副本个数,以及当前处于Running状态的Pod个数。
  • UP-TO-DATE:当前处于最新版本的Pod个数。所谓最新版本,指的是Pod的Spec部分与Deployment中Pod模版里定义的完全一致。
  • AVAILABLE:当前已经可用的Pod的个数——既是Running状态,又是最新版本,并且已经处于Ready(监控检查正确)状态的Pod个数。


3、模拟服务版本升级,触发滚动升级。

接下来重新构建“micro-api”服务的版本,并将其上传至私有镜像仓库。之后,通过命令修改“micro-api”的Deployment所使用的镜像,并触发滚动升级。

修改Deployment所使用的镜像的命令如下:

$ kubectl set image deployment/micro-api micro-api=registry.cn-hangzhou.aliyuncs.com/wudimanong/micro-api:1.1-SNAPSHOT
deployment.apps/micro-api image updated

这里使用了“kubectl set image”指令,主要是为了方便操作,也可以直接在Kubernetes部署文件中进行镜像版本的修改。

修改完Deployment的镜像版本后,Kubernetes会立即触发“滚动升级”的过程。可以通过“kubectl rollout status”指令来查看Deployment资源的状态变化。具体如下:

$ kubectl rollout status deployment/micro-api

Waiting for deployment "micro-api" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "micro-api" rollout to finish: 2 out of 3 new replicas have been updated...

Waiting for deployment "micro-api" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "micro-api" rollout to finish: 2 of 3 updated replicas are available...
Waiting for deployment "micro-api" rollout to finish: 2 of 3 updated replicas are available...
deployment "micro-api" successfully rolled out

这时,也可以通过查看Deployment的Events,看到这个“滚动升级”的过程。具体如下:
$ kubectl describe deployment micro-api
...
OldReplicaSets:  <none>
NewReplicaSet:   micro-api-d745d8649 (3/3 replicas created)
Events:
Type    Reason             Age   From                   Message
----    ------             ----  ----                   -------
Normal  ScalingReplicaSet  12m   deployment-controller  Scaled up replica set micro-api-677dd4d5b6 to 1
Normal  ScalingReplicaSet  12m   deployment-controller  Scaled down replica set micro-api-57c7cb5b74 to 2
Normal  ScalingReplicaSet  12m   deployment-controller  Scaled up replica set micro-api-677dd4d5b6 to 2
Normal  ScalingReplicaSet  5m1s  deployment-controller  Scaled down replica set micro-api-677dd4d5b6 to 0
Normal  ScalingReplicaSet  5m    deployment-controller  Scaled up replica set micro-api-d745d8649 to 2
Normal  ScalingReplicaSet  56s   deployment-controller  Scaled down replica set micro-api-57c7cb5b74 to 0
Normal  ScalingReplicaSet  56s   deployment-controller  Scaled up replica set micro-api-d745d8649 to 3

可以看到,当你修改了Deployment里的Pod定义后,“Deployment Controller”会使用这个修改后的Pod模版,创建一个新的ReplicaSet,这个新的ReplicaSet的初始Pod副本数是:0。

然后在Age=12 m的位置,开始将这个新的ReplicaSet所控制的Pod副本数从0个变成1个。

紧接着,在Age=12 m的位置,又将旧ReplicaSet所控制的Pod副本数减少1个,即“水平收缩”成两个副本。

如此交替进行,新ReplicaSet所管理的Pod的副本数,从0个变成1个,再变成2个,最后变成3个;而旧ReplicaSet所管理的Pod的副本数则从3个变成2个,最后变成0个

这样,就完成了一组Pod的版本升级过程。而像这样将一个Kubernetes集群中正在运行的多个Pod版本,交替逐一升级的过程,就是“滚动升级”

金丝雀(灰度)发布

上面内容比较详细的演示了Kubernetes的“滚动升级”的方式,虽然通过滚动升级的方式可以方便、平滑的实现版本更新,但是这个过程,并没有灰度功能。滚动升级的方式,虽然中间有缓冲交替的过程,但这种过程是自动的、迅速的,滚动升级过程结束就相当于直接进行了新版本的全量发布

而对于需要进行金丝雀(灰度)发布的场景,“滚动升级”的方式很显然是不够用的。那么,在Kubernetes中应该如何结合版本更新做到金丝雀(灰度)发布呢?

具体步骤如下:

1、编写实现新版本灰度发布的部署文件。

为了实现在Kubernetes中的金丝雀(灰度)发布过程的可观测,我们重新定义下具体的Kubernetes发布文件(如:micro-api-canary.yaml)的内容如下:
...
spec:
selector:
matchLabels:
  app: micro-api
replicas: 3
#设置滚动升级策略
#Kubernetes在等待设置的时间后才开始进行升级,例如5秒
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
  #升级过程中最多可以比原先设置多出的Pod数量
  maxSurge: 1
  #升级过程中Deployment控制器最多可以删除多少个旧Pod,主要用于提供缓冲时间
  maxUnavailable: 1
...

上述发布文件与上一小节中演示滚动升级时,发布文件的内容一致,只是为了方便观察灰度发布过程的实现,这里通过“track: canary”对新发布的Pod版本进行标记

设置新版本的镜像为:“micro-api:1.3-SNAPSHOT”。并且通过“spec.selector.matchLabels.app:micro-api”与历史版本Pod所对应的Service(micro-api.yaml文件中定义的Service)资源定义匹配。

2、执行“滚动升级”发布命令,实现“灰度发布”效果。
$ kubectl apply -f micro-api-canary.yaml && kubectl rollout pause deployment/micro-api

上面通过“kubectl rollout pause”命令实现对Deployment的金丝雀(灰度发布)。执行发布命令之后的运行效果如下:
$ kubectl get pods --show-labels -o wide
NAME                         READY   STATUS    RESTARTS   AGE     IP          NODE         NOMINATED NODE   READINESS GATES   LABELS
micro-api-57c7cb5b74-mq7m9   1/1     Running   0          6m20s   10.32.0.3   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=57c7cb5b74
micro-api-57c7cb5b74-ptptj   1/1     Running   0          6m20s   10.32.0.4   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=57c7cb5b74
micro-api-7dbb6c5d66-4rbdc   1/1     Running   0          5m33s   10.32.0.6   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=7dbb6c5d66,track=canary
micro-api-7dbb6c5d66-cfk9l   1/1     Running   0          5m33s   10.32.0.5   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=7dbb6c5d66,track=canary

查看Deployment的滚动升级情况,命令如下:
$ kubectl get deployments
NAME            READY   UP-TO-DATE   AVAILABLE   AGE
micro-api       4/3     2            4           194d

可以看到此时“micro-api” ready的数量为4,其中两个旧版本Pod,两个新版本Pod。

3、接下来进行流量测试。

查询两组Pod版本所对应的Service资源的IP,命令如下:
# kubectl get svc micro-api
NAME        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)     AGE
micro-api   ClusterIP   10.110.169.161   <none>        19090/TCP   194d

接下来,模拟对服务的接口进行批量访问,命令如下:

$ for i in {1..10}; do curl 10.110.169.161:19090/test/test; done

{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"} 

可以看到,此时流量会随机的流向旧版本和新版本(日志标记为V3)的服务。

4、将服务版本升级为新版本。

如果新版本的服务经过线上流量测试验证没有问题,则可以通过“rollout resume”命令将整体服务的版本升级为新版本。命令如下:
$ kubectl rollout resume deployment micro-api
deployment.apps/micro-api resumed

升级后的效果如下:
$ kubectl get pods --show-labels -o wide
NAME                         READY   STATUS    RESTARTS   AGE   IP          NODE         NOMINATED NODE   READINESS GATES   LABELS
micro-api-7dbb6c5d66-4rbdc   1/1     Running   0          18m   10.32.0.6   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=7dbb6c5d66,track=canary
micro-api-7dbb6c5d66-bpjtg   1/1     Running   0          84s   10.32.0.3   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=7dbb6c5d66,track=canary
micro-api-7dbb6c5d66-cfk9l   1/1     Running   0          18m   10.32.0.5   kubernetes   <none>           <none>            app=micro-api,pod-template-hash=7dbb6c5d66,track=canary

可以看到,此时目标服务已经通过“滚动升级”的方式完成了全量更新。而如果存在问题,则通过“kubectl rollout undo”命令进行回滚即可!

从上述过程可以看到,Kubernetes中的金丝雀(灰度发布)主要是通过操纵(如:pause)“滚动升级”的过程来实现的——通过发布一定数量的新版本Pod,并利用Service资源类型本身的负载均衡能力来实现流量在新/旧Pod之间的随机交替。

这样的方式虽然已经可以满足一些简单的场景,但是没有办法做到更精准的灰度流量控制。这时候就需要借助 Service Mesh 中的解决方案了,下面我们来看看在 Istio 中如何做到精准流量的金丝雀(灰度)发布。

Istio中的金丝雀(灰度)发布

以下内容默认你已经在Kubernetes中安装了Istio环境,如果还没有安装可以参考《干货|如何步入Service Mesh微服务架构时代》中分享的内容。

Istio与Kubernetes实现金丝雀(灰度)发布的方式不一样,Istio通过Envoy(SideCar)强大的路由规则管理能力,可以非常灵活地控制对应版本的流量占比,从而实现具备精准流量控制能力的金丝雀(灰度)发布功能

Istio通过Envoy(SideCar)实现金丝雀(灰度)发布的流量路由示意图如下(继续以“micro-api”服务为例):
3.png

从上图中可以大致看出,Istio具备强大的流量管理能力,而这种能力对于实现流量精准控制的金丝雀(灰度)发布功能来说,自然是水到渠成的。

具体来说,在Istio中是通过VirtualService(虚拟服务)这种特定的资源在服务网格中实现流量路由的。通过VirtualService可以方便地定义流量路由规则,并在客户端试图连接到服务时应用这些规则,并最终到达目标服务。

接下来,具体演示如何在Istio中通过VirtualService实现金丝雀(灰度)发布。步骤如下:

1、首先发布一个v1版本的服务。

要在Istio中实现更精准的版本控制,需要在发布Pod资源时,通过明确的“版本标签”进行指定。准备“micro-api”服务v1版本的Kubernetes部署文件(micro-api-canary-istio-v1.yaml):
apiVersion: v1
kind: Service
metadata:
name: micro-api
spec:
type: ClusterIP
ports:
- name: http
  port: 19090
  targetPort: 9090
selector:
app: micro-api

---

apiVersion: apps/v1
kind: Deployment
meta data:
name: micro-api-v1
spec:
selector:
matchLabels:
  app: micro-api
  #这里是关键,需要设置版本标签,以便实现灰度发布
  version: v1
replicas: 3
#设置滚动升级策略
#Kubernetes在等待设置的时间后才开始进行升级,例如5秒
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
  #升级过程中最多可以比原先设置多出的Pod数量
  maxSurge: 1
  #升级过程中Deployment控制器最多可以删除多少个旧Pod,主要用于提供缓冲时间
  maxUnavailable: 1
template:
metadata:
  labels:
    app: micro-api
    #设置版本标签,便于灰度发布
    version: v1
spec:
  #设置的阿里云私有镜像仓库登陆信息的secret
  imagePullSecrets:
    - name: regcred
  containers:
    - name: micro-api
      image: registry.cn-hangzhou.aliyuncs.com/wudimanong/micro-api:1.1-SNAPSHOT
      imagePullPolicy: Always
      tty: true
      ports:
        - name: http
          protocol: TCP
          containerPort: 19090

“spec.selector.matchLabels.version:v1”标签用来标注服务的版本,该标签是后续Istio的流量管理规则中,识别服务版本的主要依据。

准备好发布文件后,执行发布命令:
$ kubectl apply -f micro-api-canary-istio-v1.yaml

此时,一个低版本的服务就运行成功了!接下来我们模拟对其实施金丝雀(灰度)发布。

2、发布一个v2版本的服务(升级的目标版本)。

与v1版本服务一样,发布的v2版本的服务也需要明确版本标签,其发布文件(micro-api-canary-istio-v2.yaml)的内容如下:

apiVersion: apps/v1
kind: Deployment
metadata:
name: micro-api-v2
spec:
selector:
matchLabels:
  app: micro-api
  #设置好版本标签,便于灰度发布
  version: v2
replicas: 3
#设置滚动升级策略
#Kubernetes在等待设置的时间后才开始进行升级,例如5秒
minReadySeconds: 5
strategy:
type: RollingUpdate
rollingUpdate:
  #升级过程中最多可以比原先设置多出的Pod数量
  maxSurge: 1
  #升级过程中Deployment控制器最多可以删除多少个旧Pod,主要用于提供缓冲时间
  maxUnavailable: 1
template:
metadata:
  labels:
    app: micro-api
    #设置好版本标签,便于灰度发布
    version: v2
spec:
  #设置的阿里云私有镜像仓库登陆信息的secret
  imagePullSecrets:
    - name: regcred
  containers:
    - name: micro-api
      image: registry.cn-hangzhou.aliyuncs.com/wudimanong/micro-api:1.3-SNAPSHOT
      imagePullPolicy: Always
      tty: true
      ports:
        - name: http
          protocol: TCP
          containerPort: 19090

执行发布命令:
$ kubectl apply -f micro-api-canary-istio-v2.yaml 
deployment.apps/micro-api-v2 created

此时,系统中就存在了两组版本的Pod资源,具体如下:

# kubectl get pods
NAME                            READY   STATUS    RESTARTS   AGE
micro-api-v1-565d749dd4-7c66z   1/1     Running   2          13h
micro-api-v1-565d749dd4-7dqfb   1/1     Running   2          13h
micro-api-v1-565d749dd4-l62wc   1/1     Running   2          13h
micro-api-v2-6f98c598c9-5stlw   1/1     Running   0          82s
micro-api-v2-6f98c598c9-f2ntq   1/1     Running   0          82s
micro-api-v2-6f98c598c9-l8g4j   1/1     Running   0          82s

接下来将演示如何利用Istio强大的流量管理功能,来实现流量在这两组版本Pod资源之间的精确控制!

3、创建Istio网关资源。

在Istio中要实现流量的精确控制,需要将VirtualService绑定到具体的Ingressgateway(入口网关)资源。因此在创建VirtualService资源实现流量路由及控制前,需要创建一个Istio网关。部署文件(micro-gateway.yaml)的内容如下:
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: micro-gateway
spec:
selector:
istio: ingressgateway
servers:
- port:
    number: 80
    name: http
    protocol: HTTP
  hosts:
    - "*"

上述部署文件执行后将创建一个名称为“micro-gateway”的Istio网关,并允许所有主机(hosts:"*"指定)通过该网关。

4、创建Istio虚拟服务资源VirtualService。

前面提到过在Istio中主要是通过VirtualService(虚拟服务)来实现服务网格内的流量路由及控制。接下来我们看看VirtualService资源的具体创建方式,准备资源文件(如virtual-service-all.yaml),内容如下:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: micro-api-route
spec:
#用于定义流量被发送到的目标主机(这里为部署在Kubernetes中的micro-api服务)
hosts:
- micro-api.default.svc.cluster.local
#将VirtualService绑定到Istio网关,通过网关来暴露路由目标
gateways:
- micro-gateway
http:
- route:
    #设置旧版本(V1)版本的流量占比为70%
    - destination:
        host: micro-api.default.svc.cluster.local
        subset: v1
      #通过权重值来设置流量占比
      weight: 70
    #设置新版本(V2)版本的流量占比为30%
    - destination:
        host: micro-api.default.svc.cluster.local
        subset: v2
      weight: 30

如上所示,VirtualService资源具备针对http的精准流量控制能力,可以将指定占比的流量路由到特定的“subset”指定的版本。而为了实现这一能力,VirtualService资源还需要与Istio网关绑定,通过Istio网关来暴露路由目标

5、创建Istio目标路由规则资源。

虚拟服务VirtualService在Istio中主要用于控制流量的行为,而定义流量行为的路由规则则需要通过“DestinationRule”路由规则资源来定义。创建路由规则文件(destination-rule-all.yaml),具体内容如下:
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: micro-api-destination
spec:
#与Deployment资源对应的Service资源名称关联
host: micro-api
#流量策略设置:负载均衡策略、连接池大小、局部异常检测等,在路由发生后作用于流量
trafficPolicy:
#限流策略
connectionPool:
  tcp:
    maxConnections: 10
  http:
    http1MaxPendingRequests: 1
    maxRequestsPerConnection: 1
#设置目的地的负债均衡算法
loadBalancer:
  simple: ROUND_ROBIN
#目的地指的是不同的子集(subset)或服务版本。通子集(subset),可以识别应用程序的不同版本,以实现流量在不同服务版本之间的切换
subsets:
- name: v1
  labels:
    version: v1
- name: v2
  labels:
    version: v2

如上所示,通过subsets属性,定义了VirtualService资源用于路由的具体版本标签匹配信息。至此,针对两个版本服务的灰度流量控制规则就设置好了,接下来测试具体的金丝雀(灰度)发布效果。

6、测试Istio实现金丝雀(灰度)发布的流量控制效果。

在正式测试之前,可以通过命令查看下当前的部署资源情况:

#查看部署的Deployment资源
kubectl get deploy  | grep micro-api

micro-api-v1             3/3     3            3           21h
micro-api-v2             3/3     3            3           8h

#查看两组版本Pod资源对应的K8s-Service的服务IP
kubectl get svc micro-api

NAME        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)     AGE
micro-api   ClusterIP   10.110.169.161   <none>        19090/TCP   205d

#查看VirtualService资源定义
kubectl get vs

NAME              GATEWAYS          HOSTS                                   AGE
micro-api-route   [micro-gateway]   [micro-api.default.svc.cluster.local]   7h34m

#查看定义的路由规则资源
kubectl get dr

NAME                    HOST        AGE
micro-api-destination   micro-api   7h27m

通过上面的资源信息查看,这里我们已经可以查到Deployments对应的K8s-Service资源的IP,但如果通过K8s-Service资源来进行测试的话,会发现流量的控制并不精准,并不能达到我们设置的70%流量流向v1,30%的流量流向v2(因为这是随机流量)

因此,要使用Istio的精准流量控制功能,还需要使用Istio的Ingressgateway。查看Istio的Ingressgateway资源IP的命令如下:

#查看ingress的IP
kubectl get svc -n istio-system | grep ingress

istio-ingressgateway   LoadBalancer   10.98.178.61     <pending>     15021:31310/TCP,80:32113/TCP,443:31647/TCP,31400:30745/TCP,15443:30884/TCP   7h54m

接下来,通过Ingress的IP来访问“micro-api”服务,命令及效果如下:

# for i in {1..10}; do curl -H "Host:micro-api.default.svc.cluster.local" 10.98.178.61:80/test/test; done

{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"} 

如上所示,流量按照设定的比例(v1:70%;v2:30%)进行了分流。

7、测试将流量全部切向新版本。

为了更明显地验证Istio的流量控制效果,接下来,我们通过变更VirtualService资源的流量设置占比,将流量全部切到新版本。变更后的VirtualService资源的配置文件内容如下:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: micro-api-route
spec:
#用于定义流量被发送到的目标主机(这里为部署在Kubernetes中的micro-api服务)
hosts:
- micro-api.default.svc.cluster.local
#将VirtualService绑定到Istio网关,通过网关来暴露路由目标
gateways:
- micro-gateway
http:
- route:
    #设置旧版本(V1)版本的流量占比为70%
    - destination:
        host: micro-api.default.svc.cluster.local
        subset: v1
      #通过权重值来设置流量占比
      weight: 0
    #设置新版本(V2)版本的流量占比为30%
    - destination:
        host: micro-api.default.svc.cluster.local
        subset: v2
      weight: 100

继续通过Istio网关访问目标服务,命令如下:

# for i in {1..10}; do curl -H "Host:micro-api.default.svc.cluster.local" 10.98.178.61:80/test/test; done

{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"}
{"code":0,"data":"V3|无依赖测试接口返回->OK!","message":"成功"} 

可以观察到,此时流量已经全部切换到了新版本服务!

后记

在微服务时代,不同的服务之间相互联系,关系错综复杂,部署升级一个服务,可能造成整个系统的瘫痪,因此,需要选择合适的部署方式,从而将风险降到最低。金丝雀(灰度)发布只是多种部署方式的一种,还有蓝绿部署、滚动部署(如K8s的滚动升级)等,可以根据不同的业务场景选择不同的发布形式。

原文链接:https://mp.weixin.qq.com/s/a8OLm7udd0T0PY2TskZELw 收起阅读 »

Redis大集群扩容性能优化实践


背景

在现网环境,一些使用Redis集群的业务随着业务量的上涨,往往需要进行节点扩容操作。

之前有了解到运维同学对一些节点数比较大的Redis集群进行扩容操作后,业务侧反映集群性能下降,具体表现在访问时延增长明显。

某些业务对Redis集群访问时延比较敏感,例如现网环境对模型实时读取,或者一些业务依赖读取Redis集群的同步流程,会影响业务的实时流程时延。业务侧可能无法接受。

为了找到这个问题的根因,我们对某一次的Redis集群迁移操作后的集群性能下降问题进行排查。

问题描述

这一次具体的Redis集群问题的场景是:某一个Redis集群进行过扩容操作。业务侧使用Hiredis-vip进行Redis集群访问,进行MGET操作。

业务侧感知到访问Redis集群的时延变高。

现网环境说明

  • 目前现网环境部署的Redis版本多数是3.x或者4.x版本;
  • 业务访问Redis集群的客户端品类繁多,较多的使用Jedis。本次问题排查的业务使用客户端Hiredis-vip进行访问;
  • Redis集群的节点数比较大,规模是100+;
  • 集群之前存在扩容操作。


观察现象

因为时延变高,我们从几个方面进行排查:
  • 带宽是否打满;
  • CPU是否占用过高;
  • OPS是否很高。


通过简单的监控排查,带宽负载不高。但是发现CPU表现异常:
1.jpg

对比OPS和CPU负载

观察业务反馈使用的MGET和CPU负载,我们找到了对应的监控曲线。

从时间上分析,MGET和CPU负载高并没有直接关联。业务侧反馈的是MGET的时延普遍增高。此处看到MGET的OPS和CPU负载是错峰的。
2.jpg

此处可以暂时确定业务请求和CPU负载暂时没有直接关系,但是从曲线上可以看出:在同一个时间轴上,业务请求和CPU负载存在错峰的情况,两者间应该有间接关系。

对比Cluster指令OPS和CPU负载

由于之前有运维侧同事有反馈集群进行过扩容操作,必然存在slot的迁移。

考虑到业务的客户端一般都会使用缓存存放Redis集群的slot拓扑信息,因此怀疑Cluster指令会和CPU负载存在一定联系。

我们找到了当中确实有一些联系:
3.png

此处可以明显看到:某个实例在执行Cluster指令的时候,CPU的使用会明显上涨。

根据上述现象,大致可以进行一个简单的聚焦:
  • 业务侧执行MGET,因为一些原因执行了Cluster指令;
  • Cluster指令因为一些原因导致CPU占用较高影响其他操作;
  • 怀疑Cluster指令是性能瓶颈。


同时,引申几个需要关注的问题:

  • 为什么会有较多的Cluster指令被执行?

  • 为什么Cluster指令执行的时候CPU资源比较高?

  • 为什么节点规模大的集群迁移slot操作容易“中招”?


问题排查

Redis热点排查

我们对一台现场出现了CPU负载高的Redis实例使用perf top进行简单的分析:
4.png

从上图可以看出来,函数(ClusterReplyMultiBulkSlots)占用的CPU资源高达51.84%,存在异常。

ClusterReplyMultiBulkSlots实现原理

我们对clusterReplyMultiBulkSlots函数进行分析:
void clusterReplyMultiBulkSlots(client *c) {
/* Format: 1) 1) start slot
 *            2) end slot
 *            3) 1) master IP
 *               2) master port
 *               3) node ID
 *            4) 1) replica IP
 *               2) replica port
 *               3) node ID
 *           ... continued until done
 */

int num_masters = 0;
void *slot_replylen = addDeferredMultiBulkLength(c);

dictEntry *de;
dictIterator *di = dictGetSafeIterator(server.cluster->nodes);
while((de = dictNext(di)) != NULL) {
    /*注意:此处是对当前Redis节点记录的集群所有主节点都进行了遍历*/
    clusterNode *node = dictGetVal(de);
    int j = 0, start = -1;

    /* Skip slaves (that are iterated when producing the output of their
     * master) and  masters not serving any slot. */
    /*跳过备节点。备节点的信息会从主节点侧获取。*/
    if (!nodeIsMaster(node) || node->numslots == 0) continue;
    for (j = 0; j < CLUSTER_SLOTS; j++) {
        /*注意:此处是对当前节点中记录的所有slot进行了遍历*/
        int bit, i;
        /*确认当前节点是不是占有循环终端的slot*/
        if ((bit = clusterNodeGetSlotBit(node,j)) != 0) {
            if (start == -1) start = j;
        }
        /*简单分析,此处的逻辑大概就是找出连续的区间,是的话放到返回中;不是的话继续往下递归slot。
          如果是开始的话,开始一个连续区间,直到和当前的不连续。*/
        if (start != -1 && (!bit || j == CLUSTER_SLOTS-1)) {
            int nested_elements = 3; /* slots (2) + master addr (1). */
            void *nested_replylen = addDeferredMultiBulkLength(c);

            if (bit && j == CLUSTER_SLOTS-1) j++;

            /* If slot exists in output map, add to it's list.
             * else, create a new output map for this slot */
            if (start == j-1) {
                addReplyLongLong(c, start); /* only one slot; low==high */
                addReplyLongLong(c, start);
            } else {
                addReplyLongLong(c, start); /* low */
                addReplyLongLong(c, j-1);   /* high */
            }
            start = -1;

            /* First node reply position is always the master */
            addReplyMultiBulkLen(c, 3);
            addReplyBulkCString(c, node->ip);
            addReplyLongLong(c, node->port);
            addReplyBulkCBuffer(c, node->name, CLUSTER_NAMELEN);

            /* Remaining nodes in reply are replicas for slot range */
            for (i = 0; i < node->numslaves; i++) {
                /*注意:此处遍历了节点下面的备节点信息,用于返回*/
                /* This loop is copy/pasted from clusterGenNodeDescription()
                 * with modifications for per-slot node aggregation */
                if (nodeFailed(node->slaves[i])) continue;
                addReplyMultiBulkLen(c, 3);
                addReplyBulkCString(c, node->slaves[i]->ip);
                addReplyLongLong(c, node->slaves[i]->port);
                addReplyBulkCBuffer(c, node->slaves[i]->name, CLUSTER_NAMELEN);
                nested_elements++;
            }
            setDeferredMultiBulkLength(c, nested_replylen, nested_elements);
            num_masters++;
        }
    }
}
dictReleaseIterator(di);
setDeferredMultiBulkLength(c, slot_replylen, num_masters);
}

/* Return the slot bit from the cluster node structure. */
/*该函数用于判断指定的slot是否属于当前clusterNodes节点*/
int clusterNodeGetSlotBit(clusterNode *n, int slot) {
return bitmapTestBit(n->slots,slot);
}

/* Test bit 'pos' in a generic bitmap. Return 1 if the bit is set,
* otherwise 0. */
/*此处流程用于判断指定的的位置在bitmap上是否为1*/
int bitmapTestBit(unsigned char *bitmap, int pos) {
off_t byte = pos/8;
int bit = pos&7;
return (bitmap[byte] & (1<<bit)) != 0;
}
typedef struct clusterNode {
...
/*使用一个长度为CLUSTER_SLOTS/8的char数组对当前分配的slot进行记录*/
unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
...
} clusterNode;

每一个节点(ClusterNode)使用位图(char slots[CLUSTER_SLOTS/8])存放slot的分配信息。

简要说一下BitmapTestBit的逻辑:clusterNode->slots是一个长度为CLUSTER_SLOTS/8的数组。CLUSTER_SLOTS是固定值16384。数组上的每一个位分别代表一个slot。此处的bitmap数组下标则是0到2047,slot的范围是0到16383。

因为要判断pos这个位置的bit上是否是1,因此:
  • off_t byte = pos/8:拿到在bitmap上对应的哪一个字节(Byte)上存放这个pos位置的信息。因为一个Byte有8个bit。使用pos/8可以指导需要找的Byte在哪一个。此处把bitmap当成数组处理,这里对应的便是对应下标的Byte。
  • int bit = pos&7:拿到是在这个字节上对应哪一个bit表示这个pos位置的信息。&7其实就是%8。可以想象对pos每8个一组进行分组,最后一组(不满足8)的个数对应的便是在bitmap对应的Byte上对应的bit数组下标位置。
  • (bitmap[byte] & (1<<bit)):判断对应的那个bit在bitmap[byte]上是否存在。


以slot为10001进行举例:
5.png

因此10001这个slot对应的是下标1250的Byte,要校验的是下标1的bit。

对应在ClusterNode->slots上的对应位置:
6.png

图示绿色的方块表示bitmap[1250],也就是对应存放slot 10001的Byte;红框标识(bit[1])对应的就是1<<bit 的位置。bitmap[byte] & (1<<bit),也就是确认红框对应的位置是否是1。是的话表示bitmap上10001已经打标。

总结ClusterNodeGetSlotBit的概要逻辑是:判断当前的这个slot是否分配在当前node上。因此ClusterReplyMultiBulkSlots大概逻辑表示如下:
7.png

大概步骤如下:
  • 对每一个节点进行遍历
  • 对于每一个节点,遍历所有的slots,使用ClusterNodeGetSlotBit判断遍历中的slot是否分配于当前节点。


从获取CLUSTER SLOTS指令的结果来看,可以看到,复杂度是<集群主节点个数> *<slot总个数>。其中slot的总个数是16384,固定值。

Redis热点排查总结

就目前来看,CLUSTER SLOTS指令时延随着Redis集群的主节点个数,线性增长。而这次我们排查的集群主节点数比较大,可以解释这次排查的现网现象中CLUSTER SLOTS指令时延为何较大。

客户端排查

了解到运维同学们存在扩容操作,扩容完成后必然涉及到一些key在访问的时候存在MOVED的错误。

当前使用的Hiredis-vip客户端代码进行简单的浏览,简要分析以下当前业务使用的Hiredis-vip客户端在遇到MOVED的时候会怎样处理。由于其他的大部分业务常用的Jedis客户端,此处也对Jedis客户端对应流程进行简单分析。

Hiredis-vip对MOVED处理实现原理

Hiredis-vip针对MOVED的操作:

查看Cluster_update_route的调用过程:

此处的cluster_update_route_by_addr进行了CLUSTER SLOT操作。可以看到,当获取到MOVED报错的时候,Hiredis-vip会重新更新Redis集群拓扑结构,有下面的特性:
  • 因为节点通过ip:port作为key,哈希方式一样,如果集群拓扑类似,多个客户端很容易同时到同一个节点进行访问;
  • 如果某个节点访问失败,会通过迭代器找下一个节点,由于上述的原因,多个客户端很容易同时到下一个节点进行访问。


Jedis对MOVED处理实现原理

对Jedis客户端代码进行简单浏览,发现如果存在MOVED错误,会调用renewSlotCache。

继续看renewSlotCache的调用,此处可以确认:Jedis在集群模式下在遇到MOVED的报错时候,会发送Redis命令CLUSTER SLOTS,重新拉取Redis集群的slot拓扑结构。

客户端实现原理小结

由于Jedis是Java的Redis客户端,Hiredis-vip是c++的Redis客户端,可以简单认为这种异常处理机制是共性操作。

对客户端集群模式下对MOVED的流程梳理大概如下:
8.png

总的来说:
  • 使用客户端缓存的slot拓扑进行对key的访问;

  • Redis节点返回正常:
    • 访问正常,继续后续操作

  • Redis节点返回MOVED:
    • 对Redis节点进行CLUSTER SLOTS指令执行,更新拓扑;
    • 使用新的拓扑对key重新访问。


客户端排查小结

Redis集群正在扩容,也就是必然存在一些Redis客户端在访问Redis集群遇到MOVED,执行Redis指令CLUSTER SLOTS进行拓扑结构更新。

如果迁移的key命中率高,CLUSTER SLOTS指令会更加频繁的执行。这样导致的结果是迁移过程中Redis集群会持续被客户端执行CLUSTER SLOTS指令。

排查小结

此处,结合Redis侧的CLUSTER SLOTS机制以及客户端对MOVED的处理逻辑,可以解答之前的几个个问题:

为什么会有较多的Cluster指令被执行?

因为发生过迁移操作,业务访问一些迁移过的key会拿到MOVED返回,客户端会对该返回重新拉取slot拓扑信息,执行CLUSTER SLOTS。

为什么Cluster指令执行的时候CPU资源比较高?

分析Redis源码,发现CLUSTER SLOT指令的时间复杂度和主节点个数成正比。业务当前的Redis集群主节点个数比较多,自然耗时高,占用CPU资源高。

为什么节点规模大的集群迁移slot操作容易“中招”?

迁移操作必然带来一些客户端访问key的时候返回MOVED;

客户端对于MOVED的返回会执行CLUSTER SLOTS指令;

CLUSTER SLOTS指令随着集群主节点个数的增加,时延会上升;

业务的访问在slot的迁移期间会因为CLUSTER SLOTS的时延上升,在外部的感知是执行指令的时延升高。

此段与本文无关(webflux 是兼容Spring MVC 基于@Controller,@RequestMapping等注解的编程开发方式的,可以做到平滑切换)

优化

现状分析

根据目前的情况来看,客户端遇到MOVED进行CLUSTER SLOTS执行是正常的流程,因为需要更新集群的slot拓扑结构提高后续的集群访问效率。

此处流程除了Jedis,Hiredis-vip,其他的客户端应该也会进行类似的slot信息缓存优化。此处流程优化空间不大,是Redis的集群访问机制决定。

因此对Redis的集群信息记录进行分析。

Redis集群元数据分析

集群中每一个Redis节点都会有一些集群的元数据记录,记录于server.cluster,内容如下:
typedef struct clusterState {
...
dict *nodes;          /* Hash table of name -> clusterNode structures */
/*nodes记录的是所有的节点,使用dict记录*/
...
clusterNode *slots[CLUSTER_SLOTS];/*slots记录的是slot数组,内容是node的指针*/
...
} clusterState;

如上所述,原有逻辑通过遍历每个节点的slot信息获得拓扑结构。

Redis集群元数据分析

观察CLUSTER SLOTS的返回结果:
/* Format: 1) 1) start slot
*            2) end slot
*            3) 1) master IP
*               2) master port
*               3) node ID
*            4) 1) replica IP
*               2) replica port
*               3) node ID
*           ... continued until done
*/

结合server.cluster中存放的集群信息,笔者认为此处可以使用server.cluster->slots进行遍历。因为server.cluster->slots已经在每一次集群的拓扑变化得到了更新,保存的是节点指针。

优化方案

简单的优化思路如下:
  • 对slot进行遍历,找出slot中节点是连续的块;
  • 当前遍历的slot的节点如果和之前遍历的节点一致,说明目前访问的slot和前面的是在同一个节点下,也就是是在某个节点下的“连续”的slot区域内;
  • 当前遍历的slot的节点如果和之前遍历的节点不一致,说明目前访问的slot和前面的不同,前面的“连续”slot区域可以进行输出;而当前slot作为下一个新的“连续”slot区域的开始。


因此只要对server.cluster->slots进行遍历,可以满足需求。简单表示大概如下:
9.png

这样的时间复杂度降低到<slot总个数>

实现

优化逻辑如下:
void clusterReplyMultiBulkSlots(client * c) {
/* Format: 1) 1) start slot
 *            2) end slot
 *            3) 1) master IP
 *               2) master port
 *               3) node ID
 *            4) 1) replica IP
 *               2) replica port
 *               3) node ID
 *           ... continued until done
 */
clusterNode *n = NULL;
int num_masters = 0, start = -1;
void *slot_replylen = addReplyDeferredLen(c);

for (int i = 0; i <= CLUSTER_SLOTS; i++) {
    /*对所有slot进行遍历*/
    /* Find start node and slot id. */
    if (n == NULL) {
        if (i == CLUSTER_SLOTS) break;
        n = server.cluster->slots[i];
        start = i;
        continue;
    }

    /* Add cluster slots info when occur different node with start
     * or end of slot. */
    if (i == CLUSTER_SLOTS || n != server.cluster->slots[i]) {
        /*遍历主节点下面的备节点,添加返回客户端的信息*/
        addNodeReplyForClusterSlot(c, n, start, i-1);
        num_masters++;
        if (i == CLUSTER_SLOTS) break;
        n = server.cluster->slots[i];
        start = i;
    }
}
setDeferredArrayLen(c, slot_replylen, num_masters);


通过对server.cluster->slots进行遍历,找到某个节点下的“连续”的slot区域,一旦后续不连续,把之前的“连续”slot区域的节点信息以及其备节点信息进行输出,然后继续下一个“连续”slot区域的查找于输出。

优化结果对比

对两个版本的Redis的CLUSTER SLOTS指令进行横向对比。

测试环境&压测场景

操作系统:manjaro 20.2

硬件配置:
  • CPU:AMD Ryzen 7 4800H
  • DRAM:DDR4 3200MHz 8G*2


Redis集群信息:

  • 持久化配置
    • 关闭aof
    • 关闭bgsave

  • 集群节点信息:
    • 节点个数:100
    • 所有节点都是主节点


压测场景:
  • 使用benchmark工具对集群单个节点持续发送CLUSTER SLOTS指令;
  • 对其中一个版本压测完后,回收集群,重新部署后再进行下一轮压测。


CPU资源占用对比

perf导出火焰图。原有版本:
10.png

优化后:
11.png

可以明显看到,优化后的占比大幅度下降。基本符合预期。

耗时对比

在上进行测试,嵌入耗时测试代码:
else if (!strcasecmp(c->argv[1]->ptr,"slots") && c->argc == 2) {
    /* CLUSTER SLOTS */
    long long now = ustime();
    clusterReplyMultiBulkSlots(c);
    serverLog(LL_NOTICE,
        "cluster slots cost time:%lld us", ustime() - now);


输入日志进行对比。

原版的日志输出:

37351:M 06 Mar 2021 16:11:39.313 * cluster slots cost time:2061 us。

优化后版本日志输出:

35562:M 06 Mar 2021 16:11:27.862 * cluster slots cost time:168 us。

从耗时上看下降明显:从2000+us 下降到200-us;在100个主节点的集群中的耗时缩减到原来的8.2%;优化结果基本符合预期。

总结

这里可以简单描述下文章上述的动作从而得出的这样的一个结论:性能缺陷

简单总结下上述的排查以及优化过程:
  • Redis大集群因为CLUSTER命令导致某些节点的访问延迟明显;
  • 使用perf top指令对Redis实例进行排查,发现clusterReplyMultiBulkSlots命令占用CPU资源异常;
  • 对clusterReplyMultiBulkSlots进行分析,该函数存在明显的性能问题;
  • 对clusterReplyMultiBulkSlots进行优化,性能提升明显。


从上述的排查以及优化过程可以得出一个结论:目前的Redis在CLUSTER SLOT指令存在性能缺陷。

因为Redis的数据分片机制,决定了Redis集群模式下的key访问方法是缓存slot的拓扑信息。优化点也只能在CLUSTER SLOTS入手。而Redis的集群节点个数一般没有这么大,问题暴露的不明显。

其实Hiredis-vip的逻辑也存在一定问题。如上面所说,Hiredis-vip的slot拓扑更新方法是遍历所有的节点挨个进行CLUSTER SLOTS。如果Redis集群规模较大而且业务侧的客户端规模较多,会出现连锁反应:
  1. 如果Redis集群较大,CLUSTER SLOTS响应比较慢;
  2. 如果某个节点没有响应或者返回报错,Hiredis-vip客户端会对下一个节点继续进行请求;
  3. Hiredis-vip客户端中对Redis集群节点迭代遍历的方法相同(因为集群的信息在各个客户端基本一致),此时当客户端规模较大的时候,某个Redis节点可能存在阻塞,就会导致hiredis-vip客户端遍历下一个Redis节点;
  4. 大量Hiredis-vip客户端挨个地对一些Redis节点进行访问,如果Redis节点无法负担这样的请求,这样会导致Redis节点在大量Hiredis-vip客户端的“遍历”下挨个请求:
  5. 最终的表现是大部分Redis节点的CPU负载暴涨,很多Hiredis-vip客户端则继续无法更新slot拓扑。



结合上述第3点,可以想象一下:有1w个客户端对该Redis集群进行访问。因为某个命中率较高的key存在迁移操作,所有的客户端都需要更新slot拓扑。由于所有客户端缓存的集群节点信息相同,因此遍历各个节点的顺序是一致的。这1w个客户端都使用同样的顺序对集群各个节点进行遍历地操作CLUSTER SLOTS。由于CLUSTER SLOTS在大集群中性能较差,Redis节点很容易会被大量客户端请求导致不可访问。Redis节点会根据遍历顺序依次被大部分的客户端(例如9k+个客户端)访问,执行CLUSTER SLOTS指令,导致Redis节点挨个被阻塞。
最终结果是大规模的Redis集群在进行slot迁移操作后,在大规模的Hiredis-vip客户端访问下业务侧感知是普通指令时延变高,而Redis实例CPU资源占用高涨。这个逻辑可以进行一定优化。

目前上述分节3的优化已经提交并合并到Redis 6.2.2版本中。

参考资料:
  1. https://github.com/vipshop/hiredis-vip
  2. https://github.com/redis/jedis
  3. https://github.com/redis/redis
  4. https://perf.wiki.kernel.org/index.php/Main_Page


原文链接:https://mp.weixin.qq.com/s/GvaefJ_m9b4rulwWp2MD_A
继续阅读 »

背景

在现网环境,一些使用Redis集群的业务随着业务量的上涨,往往需要进行节点扩容操作。

之前有了解到运维同学对一些节点数比较大的Redis集群进行扩容操作后,业务侧反映集群性能下降,具体表现在访问时延增长明显。

某些业务对Redis集群访问时延比较敏感,例如现网环境对模型实时读取,或者一些业务依赖读取Redis集群的同步流程,会影响业务的实时流程时延。业务侧可能无法接受。

为了找到这个问题的根因,我们对某一次的Redis集群迁移操作后的集群性能下降问题进行排查。

问题描述

这一次具体的Redis集群问题的场景是:某一个Redis集群进行过扩容操作。业务侧使用Hiredis-vip进行Redis集群访问,进行MGET操作。

业务侧感知到访问Redis集群的时延变高。

现网环境说明

  • 目前现网环境部署的Redis版本多数是3.x或者4.x版本;
  • 业务访问Redis集群的客户端品类繁多,较多的使用Jedis。本次问题排查的业务使用客户端Hiredis-vip进行访问;
  • Redis集群的节点数比较大,规模是100+;
  • 集群之前存在扩容操作。


观察现象

因为时延变高,我们从几个方面进行排查:
  • 带宽是否打满;
  • CPU是否占用过高;
  • OPS是否很高。


通过简单的监控排查,带宽负载不高。但是发现CPU表现异常:
1.jpg

对比OPS和CPU负载

观察业务反馈使用的MGET和CPU负载,我们找到了对应的监控曲线。

从时间上分析,MGET和CPU负载高并没有直接关联。业务侧反馈的是MGET的时延普遍增高。此处看到MGET的OPS和CPU负载是错峰的。
2.jpg

此处可以暂时确定业务请求和CPU负载暂时没有直接关系,但是从曲线上可以看出:在同一个时间轴上,业务请求和CPU负载存在错峰的情况,两者间应该有间接关系。

对比Cluster指令OPS和CPU负载

由于之前有运维侧同事有反馈集群进行过扩容操作,必然存在slot的迁移。

考虑到业务的客户端一般都会使用缓存存放Redis集群的slot拓扑信息,因此怀疑Cluster指令会和CPU负载存在一定联系。

我们找到了当中确实有一些联系:
3.png

此处可以明显看到:某个实例在执行Cluster指令的时候,CPU的使用会明显上涨。

根据上述现象,大致可以进行一个简单的聚焦:
  • 业务侧执行MGET,因为一些原因执行了Cluster指令;
  • Cluster指令因为一些原因导致CPU占用较高影响其他操作;
  • 怀疑Cluster指令是性能瓶颈。


同时,引申几个需要关注的问题:

  • 为什么会有较多的Cluster指令被执行?

  • 为什么Cluster指令执行的时候CPU资源比较高?

  • 为什么节点规模大的集群迁移slot操作容易“中招”?


问题排查

Redis热点排查

我们对一台现场出现了CPU负载高的Redis实例使用perf top进行简单的分析:
4.png

从上图可以看出来,函数(ClusterReplyMultiBulkSlots)占用的CPU资源高达51.84%,存在异常。

ClusterReplyMultiBulkSlots实现原理

我们对clusterReplyMultiBulkSlots函数进行分析:
void clusterReplyMultiBulkSlots(client *c) {
/* Format: 1) 1) start slot
 *            2) end slot
 *            3) 1) master IP
 *               2) master port
 *               3) node ID
 *            4) 1) replica IP
 *               2) replica port
 *               3) node ID
 *           ... continued until done
 */

int num_masters = 0;
void *slot_replylen = addDeferredMultiBulkLength(c);

dictEntry *de;
dictIterator *di = dictGetSafeIterator(server.cluster->nodes);
while((de = dictNext(di)) != NULL) {
    /*注意:此处是对当前Redis节点记录的集群所有主节点都进行了遍历*/
    clusterNode *node = dictGetVal(de);
    int j = 0, start = -1;

    /* Skip slaves (that are iterated when producing the output of their
     * master) and  masters not serving any slot. */
    /*跳过备节点。备节点的信息会从主节点侧获取。*/
    if (!nodeIsMaster(node) || node->numslots == 0) continue;
    for (j = 0; j < CLUSTER_SLOTS; j++) {
        /*注意:此处是对当前节点中记录的所有slot进行了遍历*/
        int bit, i;
        /*确认当前节点是不是占有循环终端的slot*/
        if ((bit = clusterNodeGetSlotBit(node,j)) != 0) {
            if (start == -1) start = j;
        }
        /*简单分析,此处的逻辑大概就是找出连续的区间,是的话放到返回中;不是的话继续往下递归slot。
          如果是开始的话,开始一个连续区间,直到和当前的不连续。*/
        if (start != -1 && (!bit || j == CLUSTER_SLOTS-1)) {
            int nested_elements = 3; /* slots (2) + master addr (1). */
            void *nested_replylen = addDeferredMultiBulkLength(c);

            if (bit && j == CLUSTER_SLOTS-1) j++;

            /* If slot exists in output map, add to it's list.
             * else, create a new output map for this slot */
            if (start == j-1) {
                addReplyLongLong(c, start); /* only one slot; low==high */
                addReplyLongLong(c, start);
            } else {
                addReplyLongLong(c, start); /* low */
                addReplyLongLong(c, j-1);   /* high */
            }
            start = -1;

            /* First node reply position is always the master */
            addReplyMultiBulkLen(c, 3);
            addReplyBulkCString(c, node->ip);
            addReplyLongLong(c, node->port);
            addReplyBulkCBuffer(c, node->name, CLUSTER_NAMELEN);

            /* Remaining nodes in reply are replicas for slot range */
            for (i = 0; i < node->numslaves; i++) {
                /*注意:此处遍历了节点下面的备节点信息,用于返回*/
                /* This loop is copy/pasted from clusterGenNodeDescription()
                 * with modifications for per-slot node aggregation */
                if (nodeFailed(node->slaves[i])) continue;
                addReplyMultiBulkLen(c, 3);
                addReplyBulkCString(c, node->slaves[i]->ip);
                addReplyLongLong(c, node->slaves[i]->port);
                addReplyBulkCBuffer(c, node->slaves[i]->name, CLUSTER_NAMELEN);
                nested_elements++;
            }
            setDeferredMultiBulkLength(c, nested_replylen, nested_elements);
            num_masters++;
        }
    }
}
dictReleaseIterator(di);
setDeferredMultiBulkLength(c, slot_replylen, num_masters);
}

/* Return the slot bit from the cluster node structure. */
/*该函数用于判断指定的slot是否属于当前clusterNodes节点*/
int clusterNodeGetSlotBit(clusterNode *n, int slot) {
return bitmapTestBit(n->slots,slot);
}

/* Test bit 'pos' in a generic bitmap. Return 1 if the bit is set,
* otherwise 0. */
/*此处流程用于判断指定的的位置在bitmap上是否为1*/
int bitmapTestBit(unsigned char *bitmap, int pos) {
off_t byte = pos/8;
int bit = pos&7;
return (bitmap[byte] & (1<<bit)) != 0;
}
typedef struct clusterNode {
...
/*使用一个长度为CLUSTER_SLOTS/8的char数组对当前分配的slot进行记录*/
unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */
...
} clusterNode;

每一个节点(ClusterNode)使用位图(char slots[CLUSTER_SLOTS/8])存放slot的分配信息。

简要说一下BitmapTestBit的逻辑:clusterNode->slots是一个长度为CLUSTER_SLOTS/8的数组。CLUSTER_SLOTS是固定值16384。数组上的每一个位分别代表一个slot。此处的bitmap数组下标则是0到2047,slot的范围是0到16383。

因为要判断pos这个位置的bit上是否是1,因此:
  • off_t byte = pos/8:拿到在bitmap上对应的哪一个字节(Byte)上存放这个pos位置的信息。因为一个Byte有8个bit。使用pos/8可以指导需要找的Byte在哪一个。此处把bitmap当成数组处理,这里对应的便是对应下标的Byte。
  • int bit = pos&7:拿到是在这个字节上对应哪一个bit表示这个pos位置的信息。&7其实就是%8。可以想象对pos每8个一组进行分组,最后一组(不满足8)的个数对应的便是在bitmap对应的Byte上对应的bit数组下标位置。
  • (bitmap[byte] & (1<<bit)):判断对应的那个bit在bitmap[byte]上是否存在。


以slot为10001进行举例:
5.png

因此10001这个slot对应的是下标1250的Byte,要校验的是下标1的bit。

对应在ClusterNode->slots上的对应位置:
6.png

图示绿色的方块表示bitmap[1250],也就是对应存放slot 10001的Byte;红框标识(bit[1])对应的就是1<<bit 的位置。bitmap[byte] & (1<<bit),也就是确认红框对应的位置是否是1。是的话表示bitmap上10001已经打标。

总结ClusterNodeGetSlotBit的概要逻辑是:判断当前的这个slot是否分配在当前node上。因此ClusterReplyMultiBulkSlots大概逻辑表示如下:
7.png

大概步骤如下:
  • 对每一个节点进行遍历
  • 对于每一个节点,遍历所有的slots,使用ClusterNodeGetSlotBit判断遍历中的slot是否分配于当前节点。


从获取CLUSTER SLOTS指令的结果来看,可以看到,复杂度是<集群主节点个数> *<slot总个数>。其中slot的总个数是16384,固定值。

Redis热点排查总结

就目前来看,CLUSTER SLOTS指令时延随着Redis集群的主节点个数,线性增长。而这次我们排查的集群主节点数比较大,可以解释这次排查的现网现象中CLUSTER SLOTS指令时延为何较大。

客户端排查

了解到运维同学们存在扩容操作,扩容完成后必然涉及到一些key在访问的时候存在MOVED的错误。

当前使用的Hiredis-vip客户端代码进行简单的浏览,简要分析以下当前业务使用的Hiredis-vip客户端在遇到MOVED的时候会怎样处理。由于其他的大部分业务常用的Jedis客户端,此处也对Jedis客户端对应流程进行简单分析。

Hiredis-vip对MOVED处理实现原理

Hiredis-vip针对MOVED的操作:

查看Cluster_update_route的调用过程:

此处的cluster_update_route_by_addr进行了CLUSTER SLOT操作。可以看到,当获取到MOVED报错的时候,Hiredis-vip会重新更新Redis集群拓扑结构,有下面的特性:
  • 因为节点通过ip:port作为key,哈希方式一样,如果集群拓扑类似,多个客户端很容易同时到同一个节点进行访问;
  • 如果某个节点访问失败,会通过迭代器找下一个节点,由于上述的原因,多个客户端很容易同时到下一个节点进行访问。


Jedis对MOVED处理实现原理

对Jedis客户端代码进行简单浏览,发现如果存在MOVED错误,会调用renewSlotCache。

继续看renewSlotCache的调用,此处可以确认:Jedis在集群模式下在遇到MOVED的报错时候,会发送Redis命令CLUSTER SLOTS,重新拉取Redis集群的slot拓扑结构。

客户端实现原理小结

由于Jedis是Java的Redis客户端,Hiredis-vip是c++的Redis客户端,可以简单认为这种异常处理机制是共性操作。

对客户端集群模式下对MOVED的流程梳理大概如下:
8.png

总的来说:
  • 使用客户端缓存的slot拓扑进行对key的访问;

  • Redis节点返回正常:
    • 访问正常,继续后续操作

  • Redis节点返回MOVED:
    • 对Redis节点进行CLUSTER SLOTS指令执行,更新拓扑;
    • 使用新的拓扑对key重新访问。


客户端排查小结

Redis集群正在扩容,也就是必然存在一些Redis客户端在访问Redis集群遇到MOVED,执行Redis指令CLUSTER SLOTS进行拓扑结构更新。

如果迁移的key命中率高,CLUSTER SLOTS指令会更加频繁的执行。这样导致的结果是迁移过程中Redis集群会持续被客户端执行CLUSTER SLOTS指令。

排查小结

此处,结合Redis侧的CLUSTER SLOTS机制以及客户端对MOVED的处理逻辑,可以解答之前的几个个问题:

为什么会有较多的Cluster指令被执行?

因为发生过迁移操作,业务访问一些迁移过的key会拿到MOVED返回,客户端会对该返回重新拉取slot拓扑信息,执行CLUSTER SLOTS。

为什么Cluster指令执行的时候CPU资源比较高?

分析Redis源码,发现CLUSTER SLOT指令的时间复杂度和主节点个数成正比。业务当前的Redis集群主节点个数比较多,自然耗时高,占用CPU资源高。

为什么节点规模大的集群迁移slot操作容易“中招”?

迁移操作必然带来一些客户端访问key的时候返回MOVED;

客户端对于MOVED的返回会执行CLUSTER SLOTS指令;

CLUSTER SLOTS指令随着集群主节点个数的增加,时延会上升;

业务的访问在slot的迁移期间会因为CLUSTER SLOTS的时延上升,在外部的感知是执行指令的时延升高。

此段与本文无关(webflux 是兼容Spring MVC 基于@Controller,@RequestMapping等注解的编程开发方式的,可以做到平滑切换)

优化

现状分析

根据目前的情况来看,客户端遇到MOVED进行CLUSTER SLOTS执行是正常的流程,因为需要更新集群的slot拓扑结构提高后续的集群访问效率。

此处流程除了Jedis,Hiredis-vip,其他的客户端应该也会进行类似的slot信息缓存优化。此处流程优化空间不大,是Redis的集群访问机制决定。

因此对Redis的集群信息记录进行分析。

Redis集群元数据分析

集群中每一个Redis节点都会有一些集群的元数据记录,记录于server.cluster,内容如下:
typedef struct clusterState {
...
dict *nodes;          /* Hash table of name -> clusterNode structures */
/*nodes记录的是所有的节点,使用dict记录*/
...
clusterNode *slots[CLUSTER_SLOTS];/*slots记录的是slot数组,内容是node的指针*/
...
} clusterState;

如上所述,原有逻辑通过遍历每个节点的slot信息获得拓扑结构。

Redis集群元数据分析

观察CLUSTER SLOTS的返回结果:
/* Format: 1) 1) start slot
*            2) end slot
*            3) 1) master IP
*               2) master port
*               3) node ID
*            4) 1) replica IP
*               2) replica port
*               3) node ID
*           ... continued until done
*/

结合server.cluster中存放的集群信息,笔者认为此处可以使用server.cluster->slots进行遍历。因为server.cluster->slots已经在每一次集群的拓扑变化得到了更新,保存的是节点指针。

优化方案

简单的优化思路如下:
  • 对slot进行遍历,找出slot中节点是连续的块;
  • 当前遍历的slot的节点如果和之前遍历的节点一致,说明目前访问的slot和前面的是在同一个节点下,也就是是在某个节点下的“连续”的slot区域内;
  • 当前遍历的slot的节点如果和之前遍历的节点不一致,说明目前访问的slot和前面的不同,前面的“连续”slot区域可以进行输出;而当前slot作为下一个新的“连续”slot区域的开始。


因此只要对server.cluster->slots进行遍历,可以满足需求。简单表示大概如下:
9.png

这样的时间复杂度降低到<slot总个数>

实现

优化逻辑如下:
void clusterReplyMultiBulkSlots(client * c) {
/* Format: 1) 1) start slot
 *            2) end slot
 *            3) 1) master IP
 *               2) master port
 *               3) node ID
 *            4) 1) replica IP
 *               2) replica port
 *               3) node ID
 *           ... continued until done
 */
clusterNode *n = NULL;
int num_masters = 0, start = -1;
void *slot_replylen = addReplyDeferredLen(c);

for (int i = 0; i <= CLUSTER_SLOTS; i++) {
    /*对所有slot进行遍历*/
    /* Find start node and slot id. */
    if (n == NULL) {
        if (i == CLUSTER_SLOTS) break;
        n = server.cluster->slots[i];
        start = i;
        continue;
    }

    /* Add cluster slots info when occur different node with start
     * or end of slot. */
    if (i == CLUSTER_SLOTS || n != server.cluster->slots[i]) {
        /*遍历主节点下面的备节点,添加返回客户端的信息*/
        addNodeReplyForClusterSlot(c, n, start, i-1);
        num_masters++;
        if (i == CLUSTER_SLOTS) break;
        n = server.cluster->slots[i];
        start = i;
    }
}
setDeferredArrayLen(c, slot_replylen, num_masters);


通过对server.cluster->slots进行遍历,找到某个节点下的“连续”的slot区域,一旦后续不连续,把之前的“连续”slot区域的节点信息以及其备节点信息进行输出,然后继续下一个“连续”slot区域的查找于输出。

优化结果对比

对两个版本的Redis的CLUSTER SLOTS指令进行横向对比。

测试环境&压测场景

操作系统:manjaro 20.2

硬件配置:
  • CPU:AMD Ryzen 7 4800H
  • DRAM:DDR4 3200MHz 8G*2


Redis集群信息:

  • 持久化配置
    • 关闭aof
    • 关闭bgsave

  • 集群节点信息:
    • 节点个数:100
    • 所有节点都是主节点


压测场景:
  • 使用benchmark工具对集群单个节点持续发送CLUSTER SLOTS指令;
  • 对其中一个版本压测完后,回收集群,重新部署后再进行下一轮压测。


CPU资源占用对比

perf导出火焰图。原有版本:
10.png

优化后:
11.png

可以明显看到,优化后的占比大幅度下降。基本符合预期。

耗时对比

在上进行测试,嵌入耗时测试代码:
else if (!strcasecmp(c->argv[1]->ptr,"slots") && c->argc == 2) {
    /* CLUSTER SLOTS */
    long long now = ustime();
    clusterReplyMultiBulkSlots(c);
    serverLog(LL_NOTICE,
        "cluster slots cost time:%lld us", ustime() - now);


输入日志进行对比。

原版的日志输出:

37351:M 06 Mar 2021 16:11:39.313 * cluster slots cost time:2061 us。

优化后版本日志输出:

35562:M 06 Mar 2021 16:11:27.862 * cluster slots cost time:168 us。

从耗时上看下降明显:从2000+us 下降到200-us;在100个主节点的集群中的耗时缩减到原来的8.2%;优化结果基本符合预期。

总结

这里可以简单描述下文章上述的动作从而得出的这样的一个结论:性能缺陷

简单总结下上述的排查以及优化过程:
  • Redis大集群因为CLUSTER命令导致某些节点的访问延迟明显;
  • 使用perf top指令对Redis实例进行排查,发现clusterReplyMultiBulkSlots命令占用CPU资源异常;
  • 对clusterReplyMultiBulkSlots进行分析,该函数存在明显的性能问题;
  • 对clusterReplyMultiBulkSlots进行优化,性能提升明显。


从上述的排查以及优化过程可以得出一个结论:目前的Redis在CLUSTER SLOT指令存在性能缺陷。

因为Redis的数据分片机制,决定了Redis集群模式下的key访问方法是缓存slot的拓扑信息。优化点也只能在CLUSTER SLOTS入手。而Redis的集群节点个数一般没有这么大,问题暴露的不明显。

其实Hiredis-vip的逻辑也存在一定问题。如上面所说,Hiredis-vip的slot拓扑更新方法是遍历所有的节点挨个进行CLUSTER SLOTS。如果Redis集群规模较大而且业务侧的客户端规模较多,会出现连锁反应:
  1. 如果Redis集群较大,CLUSTER SLOTS响应比较慢;
  2. 如果某个节点没有响应或者返回报错,Hiredis-vip客户端会对下一个节点继续进行请求;
  3. Hiredis-vip客户端中对Redis集群节点迭代遍历的方法相同(因为集群的信息在各个客户端基本一致),此时当客户端规模较大的时候,某个Redis节点可能存在阻塞,就会导致hiredis-vip客户端遍历下一个Redis节点;
  4. 大量Hiredis-vip客户端挨个地对一些Redis节点进行访问,如果Redis节点无法负担这样的请求,这样会导致Redis节点在大量Hiredis-vip客户端的“遍历”下挨个请求:
  5. 最终的表现是大部分Redis节点的CPU负载暴涨,很多Hiredis-vip客户端则继续无法更新slot拓扑。



结合上述第3点,可以想象一下:有1w个客户端对该Redis集群进行访问。因为某个命中率较高的key存在迁移操作,所有的客户端都需要更新slot拓扑。由于所有客户端缓存的集群节点信息相同,因此遍历各个节点的顺序是一致的。这1w个客户端都使用同样的顺序对集群各个节点进行遍历地操作CLUSTER SLOTS。由于CLUSTER SLOTS在大集群中性能较差,Redis节点很容易会被大量客户端请求导致不可访问。Redis节点会根据遍历顺序依次被大部分的客户端(例如9k+个客户端)访问,执行CLUSTER SLOTS指令,导致Redis节点挨个被阻塞。
最终结果是大规模的Redis集群在进行slot迁移操作后,在大规模的Hiredis-vip客户端访问下业务侧感知是普通指令时延变高,而Redis实例CPU资源占用高涨。这个逻辑可以进行一定优化。

目前上述分节3的优化已经提交并合并到Redis 6.2.2版本中。

参考资料:
  1. https://github.com/vipshop/hiredis-vip
  2. https://github.com/redis/jedis
  3. https://github.com/redis/redis
  4. https://perf.wiki.kernel.org/index.php/Main_Page


原文链接:https://mp.weixin.qq.com/s/GvaefJ_m9b4rulwWp2MD_A 收起阅读 »

0.2 秒居然复制了 100G 文件?


cp 引发的思考

今天同事用 cp 命令,把他给惊到了!

背景是这样的:他用 cp 拷贝了一个 100 G 的文件,竟然一秒不到就拷贝完成了!

ls 看一把文件,显示文件确实是 100 G。
sh-4.4# ls -lh  
-rw-r--r-- 1 root root 100G Mar  6 12:22 test.txt

但是 copy 起来为什么会这么快呢?
sh-4.4# time cp ./test.txt ./test.txt.cp  

real 0m0.107s  
user 0m0.008s  
sys 0m0.085s

一个 SATA 机械盘的写能力能到 150 M/s (大部分的机械盘都是到不了这个值的)就算非常不错了,正常情况下,copy 一个 100G 的文件至少要 682 秒(100 G/ 150 M/s),也就是 11 分钟。

实际情况却是 cp 一秒没到就完成了工作,惊呆了,为啥呢?

更诡异的是:他的文件系统只有 40 G,为啥里面会有一个 100 G的文件呢?

同事把我找来,看看这个诡异的问题。

分析文件

我让他先用 du 命令看一下,却只有 2M ,根本不是 100G,这是怎么回事?
sh-4.4# du -sh ./test.txt  
2.0M ./test.txt

再看 stat 命令显示的信息:
sh-4.4# stat ./test.txt  
File: ./test.txt  
Size: 107374182400 Blocks: 4096       IO Block: 4096   regular file  
Device: 78h/120d Inode: 3148347     Links: 1  
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)  
Access: 2021-03-13 12:22:00.888871000 +0000  
Modify: 2021-03-13 12:22:46.562243000 +0000  
Change: 2021-03-13 12:22:46.562243000 +0000  
Birth: -

stat 命令输出解释:
  1. Size 为 107374182400(知识点:单位是字节),也就是 100G;
  2. Blocks 这个指标显示为 4096(知识点:一个 Block 的单位固定是 512 字节,也就是一个扇区的大小),这里表示为 2M。


划重点:
  • Size 表示的是文件大小,这个也是大多数人看到的大小;
  • Blocks 表示的是物理实际占用空间


同事问道:“文件大小和实际物理占用,这两个竟然不是相同的概念 !为什么是这样?”

“看来,我们必须得深入文件系统才能理解了,来,我给你好好讲讲。”

文件系统

文件系统听起来很高大上,通俗话就用来存数据的一个容器而已,本质和你的行李箱、仓库没有啥区别,只不过文件系统存储的是数字产品而已。

我有一个视频文件,我把这个视频放到这个文件系统里,下次来拿,要能拿到我完整的视频文件数据,这就是文件系统,对外提供的就是存取服务

现实的存取场景

例如你到火车站使用寄存服务:

存行李的时候,是不是要登记一些个人信息?对吧,至少自己名字要写上。可能还会给你一个牌子,让你挂手上,这个东西就是为了标示每一个唯一的行李。
1.gif

取行李的时候,要报自己名字,有牌子的给他牌子,然后工作人员才能去特定的位置找到你的行李。
2.gif

划重点:存的时候必须记录一些关键信息(记录 ID、给身份牌),取的时候才能正确定位到。

文件系统

回到我们的文件系统,对比上面的行李存取行为,可以做个简单的类比。
  1. 登记名字就是在文件系统记录文件名;
  2. 生成的牌子就是元数据索引;
  3. 你的行李就是文件;
  4. 寄存室就是磁盘(容纳东西的物理空间);
  5. 管理员整套运行机制就是文件系统。


上面的对应并不是非常严谨,仅仅是帮助大家理解文件系统而已,让大家知道其实文件系统是非常朴实的一个东西,思想都来源于生活。

空间管理

现在思考文件系统是怎么管理空间的?

如果,一个连续的大磁盘空间给你使用,你会怎么使用这段空间呢?

直观的一个想法,我把进来的数据就完整的放进去。
3.gif

这种方式非常容易实现,属于眼前最简单,以后最麻烦的方式。因为会造成很多空洞,明明还有很多空间位置,但是由于整个太大,形状不合适(数据大小),哪里都放不下。因为你要放一个完整的空间。

怎么改进?有人会想,既然整个放不进去,那就剁碎了呗。这里塞一点,那里塞一点,就塞进去了。

对,思路完全正确。改进的方式就是切分,把空间按照一定粒度切分。每个小粒度的物理块命名为 Block,每个 Block 一般是 4K 大小,用户数据存到文件系统里来自然也是要切分,存储到磁盘上各个角落。
4.gif

图示标号表示这个完整对象的 Block 的序号,用来复原对象用的。

随之而来又有一个问题:你光会切成块还不行,取文件数据的时候,还得把它们给组合起来才行。

所以,要有一个表记录文件对应所有 Block 的位置,这个表被文件系统称为inode

写文件的流程是这样的:
  1. 先写数据:数据先按照 Block 粒度存储到磁盘的各个位置;
  2. 再写元数据:然后把 Block 所在的各个位置保存起来,即inode(我用一本书来表示)。


5.gif

读文件流程则是:
  1. 先读inode,找到各个 Block 的位置;
  2. 然后读数据,构造一个完整的文件,给到用户。


6.gif

inode/block 概念

好,我们现在来看看inode,直观地感受一下:
7.png

这个 inode 有文件元数据和 Block 数组(长度是 15),数组中前两项指向 Block 3 和 Block 11,表示数据在这两个块中存着。

你肯定会意识到:Block 数组只有 15 个元素,每个 Block 是 4K, 难道一个文件最大只能是 15 * 4K = 60 K?

这是绝对不行的!

最简单的办法就是:把这个 Block 数组长度给扩大!

比如我们想让文件系统最大支持 100G 的文件,Block 数组需要这么长:

(10010241024)/4 = 26214400

Block 数组中每一项是 4 个字节,那就需要 (26214400*4)/1024/1024 = 100M

为了支持 100G 的文件,我们的 Block 数组本身就得 100M !

并且对每个文件都是如此 !即使这个文件只有 1K! 这将是巨大浪费!

肯定不能这么干,解决方案就是间接索引,按照约定,把这 15 个槽位分作 4 个不同类别来用:
  1. 前 12 个槽位(也就是 0 - 11 )我们成为直接索引
  2. 第 13 个位置,我们称为 1 级索引
  3. 第 14 个位置,我们称为 2 级索引
  4. 第 15 个位置,我们称为 3 级索引


8.jpg

直接索引:能存 12 个 block 编号,每个 block 4K,就是 48K,也就是说,48K 以内的文件,前 12 个槽位存储编号就能完全 hold 住。

一级索引:也就是说这里存储的编号指向的 block 里面存储的也是 block 编号,里面的编号指向用户数据。一个 block 4K,每个元素 4 字节,也就是有 1024 个编号位置可以存储。

所以,一级索引能寻址 4M(1024 * 4K)空间 。
9.png

二级索引:二级索引是在一级索引的基础上多了一级而已,换算下来,有了 4M 的空间用来存储用户数据的编号。所以二级索引能寻址 4G(4M/4 * 4K)的空间。
10.png

三级索引:三级索引是在二级索引的基础上又多了一级,也就是说,有了 4G 的空间来存储用户数据的 block 编号。所以二级索引能寻址 4T(4G/4 * 4K)的空间。
11.png

所以,在这种文件系统(如ext2)上,通过这种间接块索引的方式,最大能支撑的文件大小 = 48K + 4M + 4G + 4T ,约等于 4 T。

这种多级索引寻址性能表现怎么样?

在不超过 12 个数据块的小文件的寻址是最快的,访问文件中的任意数据理论只需要两次读盘,一次读 inode,一次读数据块。

访问大文件中的数据则需要最多五次读盘操作:inode、一级间接寻址块、二级间接寻址块、三级间接寻址块、数据块。

为什么 cp 那么快?

接下来我们要写入一个奇怪的文件,这个文件很大,但是真正的数据只有 8K:

在 [0 , 4K] 这位置有 4K 的数据。

在 [1T , 1T+4K] 处也有4K数据。

中间没有数据,这样的文件该如何写入硬盘?
  1. 创建一个文件,这个时候分配一个 inode;
  2. 在 [0 , 4K] 的位置写入 4K 数据,这个时候只需要 一个 block,把这个编号写到 block[0] 这个位置保存起来;
  3. 在 [1T , 1T+4K] 的位置写入 4K 数据,这个时候需要分配一个 block,因为这个位置已经落到三级索引才能表现的空间了,所以需要还需要分配出 3 个索引块;
  4. 写入完成,close 文件.


实际存储如图:
12.png

这个时候,我们的文件看起来是超大文件,size 等于 1T + 4K ,但里面实际的数据只有 8K,位置分别是 [0 , 4K ] ,[1T , 1T+4K]。

由于没写数据的地方不用分配物理 block 块,所以实际占用的物理空间只有 8K。

重点:文件 size 只是 inode 里面的一个属性,实际物理空间占用则是要看用户数据放了多少个 block ,没写数据的地方不用分配物理 block 块。

这样的文件其实就是稀疏文件, 它的逻辑大小和实际物理空间是不相等的。

所以当我们用 cp 命令去复制一个这样的文件时,那肯定迅速就完成了。

总结

好,我们再深入思考下,文件系统为什么能做到这一点?
  • 首先,最关键的是把磁盘空间切成离散的、定长的 block 来管理;
  • 然后,通过 inode 能查找到所有离散的数据(保存了所有的索引);
  • 最后,实现索引块和数据块空间的后分配。


这三点是层层递进的。

后记

我把这点小知识给小伙伴讲了一小时,看到他感动欲哭的表情,我觉得他学 fei 了,非常满意。

是我想太多了吗?中午吃饭都没叫我。

原文链接:https://mp.weixin.qq.com/s/GqKHhAd93iQorDeGyHBMnA
继续阅读 »

cp 引发的思考

今天同事用 cp 命令,把他给惊到了!

背景是这样的:他用 cp 拷贝了一个 100 G 的文件,竟然一秒不到就拷贝完成了!

ls 看一把文件,显示文件确实是 100 G。
sh-4.4# ls -lh  
-rw-r--r-- 1 root root 100G Mar  6 12:22 test.txt

但是 copy 起来为什么会这么快呢?
sh-4.4# time cp ./test.txt ./test.txt.cp  

real 0m0.107s  
user 0m0.008s  
sys 0m0.085s

一个 SATA 机械盘的写能力能到 150 M/s (大部分的机械盘都是到不了这个值的)就算非常不错了,正常情况下,copy 一个 100G 的文件至少要 682 秒(100 G/ 150 M/s),也就是 11 分钟。

实际情况却是 cp 一秒没到就完成了工作,惊呆了,为啥呢?

更诡异的是:他的文件系统只有 40 G,为啥里面会有一个 100 G的文件呢?

同事把我找来,看看这个诡异的问题。

分析文件

我让他先用 du 命令看一下,却只有 2M ,根本不是 100G,这是怎么回事?
sh-4.4# du -sh ./test.txt  
2.0M ./test.txt

再看 stat 命令显示的信息:
sh-4.4# stat ./test.txt  
File: ./test.txt  
Size: 107374182400 Blocks: 4096       IO Block: 4096   regular file  
Device: 78h/120d Inode: 3148347     Links: 1  
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)  
Access: 2021-03-13 12:22:00.888871000 +0000  
Modify: 2021-03-13 12:22:46.562243000 +0000  
Change: 2021-03-13 12:22:46.562243000 +0000  
Birth: -

stat 命令输出解释:
  1. Size 为 107374182400(知识点:单位是字节),也就是 100G;
  2. Blocks 这个指标显示为 4096(知识点:一个 Block 的单位固定是 512 字节,也就是一个扇区的大小),这里表示为 2M。


划重点:
  • Size 表示的是文件大小,这个也是大多数人看到的大小;
  • Blocks 表示的是物理实际占用空间


同事问道:“文件大小和实际物理占用,这两个竟然不是相同的概念 !为什么是这样?”

“看来,我们必须得深入文件系统才能理解了,来,我给你好好讲讲。”

文件系统

文件系统听起来很高大上,通俗话就用来存数据的一个容器而已,本质和你的行李箱、仓库没有啥区别,只不过文件系统存储的是数字产品而已。

我有一个视频文件,我把这个视频放到这个文件系统里,下次来拿,要能拿到我完整的视频文件数据,这就是文件系统,对外提供的就是存取服务

现实的存取场景

例如你到火车站使用寄存服务:

存行李的时候,是不是要登记一些个人信息?对吧,至少自己名字要写上。可能还会给你一个牌子,让你挂手上,这个东西就是为了标示每一个唯一的行李。
1.gif

取行李的时候,要报自己名字,有牌子的给他牌子,然后工作人员才能去特定的位置找到你的行李。
2.gif

划重点:存的时候必须记录一些关键信息(记录 ID、给身份牌),取的时候才能正确定位到。

文件系统

回到我们的文件系统,对比上面的行李存取行为,可以做个简单的类比。
  1. 登记名字就是在文件系统记录文件名;
  2. 生成的牌子就是元数据索引;
  3. 你的行李就是文件;
  4. 寄存室就是磁盘(容纳东西的物理空间);
  5. 管理员整套运行机制就是文件系统。


上面的对应并不是非常严谨,仅仅是帮助大家理解文件系统而已,让大家知道其实文件系统是非常朴实的一个东西,思想都来源于生活。

空间管理

现在思考文件系统是怎么管理空间的?

如果,一个连续的大磁盘空间给你使用,你会怎么使用这段空间呢?

直观的一个想法,我把进来的数据就完整的放进去。
3.gif

这种方式非常容易实现,属于眼前最简单,以后最麻烦的方式。因为会造成很多空洞,明明还有很多空间位置,但是由于整个太大,形状不合适(数据大小),哪里都放不下。因为你要放一个完整的空间。

怎么改进?有人会想,既然整个放不进去,那就剁碎了呗。这里塞一点,那里塞一点,就塞进去了。

对,思路完全正确。改进的方式就是切分,把空间按照一定粒度切分。每个小粒度的物理块命名为 Block,每个 Block 一般是 4K 大小,用户数据存到文件系统里来自然也是要切分,存储到磁盘上各个角落。
4.gif

图示标号表示这个完整对象的 Block 的序号,用来复原对象用的。

随之而来又有一个问题:你光会切成块还不行,取文件数据的时候,还得把它们给组合起来才行。

所以,要有一个表记录文件对应所有 Block 的位置,这个表被文件系统称为inode

写文件的流程是这样的:
  1. 先写数据:数据先按照 Block 粒度存储到磁盘的各个位置;
  2. 再写元数据:然后把 Block 所在的各个位置保存起来,即inode(我用一本书来表示)。


5.gif

读文件流程则是:
  1. 先读inode,找到各个 Block 的位置;
  2. 然后读数据,构造一个完整的文件,给到用户。


6.gif

inode/block 概念

好,我们现在来看看inode,直观地感受一下:
7.png

这个 inode 有文件元数据和 Block 数组(长度是 15),数组中前两项指向 Block 3 和 Block 11,表示数据在这两个块中存着。

你肯定会意识到:Block 数组只有 15 个元素,每个 Block 是 4K, 难道一个文件最大只能是 15 * 4K = 60 K?

这是绝对不行的!

最简单的办法就是:把这个 Block 数组长度给扩大!

比如我们想让文件系统最大支持 100G 的文件,Block 数组需要这么长:

(10010241024)/4 = 26214400

Block 数组中每一项是 4 个字节,那就需要 (26214400*4)/1024/1024 = 100M

为了支持 100G 的文件,我们的 Block 数组本身就得 100M !

并且对每个文件都是如此 !即使这个文件只有 1K! 这将是巨大浪费!

肯定不能这么干,解决方案就是间接索引,按照约定,把这 15 个槽位分作 4 个不同类别来用:
  1. 前 12 个槽位(也就是 0 - 11 )我们成为直接索引
  2. 第 13 个位置,我们称为 1 级索引
  3. 第 14 个位置,我们称为 2 级索引
  4. 第 15 个位置,我们称为 3 级索引


8.jpg

直接索引:能存 12 个 block 编号,每个 block 4K,就是 48K,也就是说,48K 以内的文件,前 12 个槽位存储编号就能完全 hold 住。

一级索引:也就是说这里存储的编号指向的 block 里面存储的也是 block 编号,里面的编号指向用户数据。一个 block 4K,每个元素 4 字节,也就是有 1024 个编号位置可以存储。

所以,一级索引能寻址 4M(1024 * 4K)空间 。
9.png

二级索引:二级索引是在一级索引的基础上多了一级而已,换算下来,有了 4M 的空间用来存储用户数据的编号。所以二级索引能寻址 4G(4M/4 * 4K)的空间。
10.png

三级索引:三级索引是在二级索引的基础上又多了一级,也就是说,有了 4G 的空间来存储用户数据的 block 编号。所以二级索引能寻址 4T(4G/4 * 4K)的空间。
11.png

所以,在这种文件系统(如ext2)上,通过这种间接块索引的方式,最大能支撑的文件大小 = 48K + 4M + 4G + 4T ,约等于 4 T。

这种多级索引寻址性能表现怎么样?

在不超过 12 个数据块的小文件的寻址是最快的,访问文件中的任意数据理论只需要两次读盘,一次读 inode,一次读数据块。

访问大文件中的数据则需要最多五次读盘操作:inode、一级间接寻址块、二级间接寻址块、三级间接寻址块、数据块。

为什么 cp 那么快?

接下来我们要写入一个奇怪的文件,这个文件很大,但是真正的数据只有 8K:

在 [0 , 4K] 这位置有 4K 的数据。

在 [1T , 1T+4K] 处也有4K数据。

中间没有数据,这样的文件该如何写入硬盘?
  1. 创建一个文件,这个时候分配一个 inode;
  2. 在 [0 , 4K] 的位置写入 4K 数据,这个时候只需要 一个 block,把这个编号写到 block[0] 这个位置保存起来;
  3. 在 [1T , 1T+4K] 的位置写入 4K 数据,这个时候需要分配一个 block,因为这个位置已经落到三级索引才能表现的空间了,所以需要还需要分配出 3 个索引块;
  4. 写入完成,close 文件.


实际存储如图:
12.png

这个时候,我们的文件看起来是超大文件,size 等于 1T + 4K ,但里面实际的数据只有 8K,位置分别是 [0 , 4K ] ,[1T , 1T+4K]。

由于没写数据的地方不用分配物理 block 块,所以实际占用的物理空间只有 8K。

重点:文件 size 只是 inode 里面的一个属性,实际物理空间占用则是要看用户数据放了多少个 block ,没写数据的地方不用分配物理 block 块。

这样的文件其实就是稀疏文件, 它的逻辑大小和实际物理空间是不相等的。

所以当我们用 cp 命令去复制一个这样的文件时,那肯定迅速就完成了。

总结

好,我们再深入思考下,文件系统为什么能做到这一点?
  • 首先,最关键的是把磁盘空间切成离散的、定长的 block 来管理;
  • 然后,通过 inode 能查找到所有离散的数据(保存了所有的索引);
  • 最后,实现索引块和数据块空间的后分配。


这三点是层层递进的。

后记

我把这点小知识给小伙伴讲了一小时,看到他感动欲哭的表情,我觉得他学 fei 了,非常满意。

是我想太多了吗?中午吃饭都没叫我。

原文链接:https://mp.weixin.qq.com/s/GqKHhAd93iQorDeGyHBMnA 收起阅读 »

高性能、免运维,博云开源云原生本地存储方案:Carina


2021 年 10 月 11 日,博云正式开源 Carina 本地存储方案,Carina 基于 Kubernetes 及 LVM 实现,提供了数据库与中间件等有状态应用在 Kubernetes 中运行所必须的高性能的本地存储能力,极大减少了存储系统的运维压力。今年 9 月,Carina 还以首批成员身份加入了由中国信通院发起的可信开源社区共同体,并获得可信开源项目成员证书。

博客图片.jpg


Carina 最大的特点是高性能和免运维,为中间件、数据库等有状态服务提供了匹配本地磁盘的高 IOPS 和极低延迟的性能指标,同时易安装、自运维能力又极大的减轻了存储系统的运维压力。另外,Carina 还提供了本地磁盘管理能力、PV 高级调度能力、PV 自动分层技术、卷拓扑能力、自动 failover 能力、动态 IO 限速、监控告警、多种存储供给能力等高级功能。

目前, Carina 项目代码已在 Github 上开源,项目地址为:https://github.com/carina-io/carina。欢迎广大技术开发者和爱好者前去试用。

01 功能亮点

灵活高效的供给高 IOPS、低延迟的存储
免运维,自动管理本地磁盘、自动组建 RAID
多种调度策略可配
支持带宽 IOPS 限速
支持 PV 数据自动分层
支持 PV 自动扩容
支持 RAID 管理能力
支持容灾转移能力
提供块和文件的访问方式

02 Why Carina?

Kubernetes 原生支持
完全兼容的 Kubernetes API ,无需额外开发,依赖组件很少且均为通用开源组件。
本地磁盘管理
自动管理本地磁盘,提供 RAID 组建、数据分层、磁盘限速等高级功能。
设备注册
将本地磁盘注册为 Kubernetes 设备,参与容器调度评分。
容灾转移
支持在节点删除,将存储卷在其他节点重建。
文件和块存储
同时支持为容器提供文件存储和块存储,以及在线扩容。

03 How it works

云端是标准 Kubernetes 集群,可以使用任何 CSI 存储插件,比如 Ceph-CSI。在集群中运行 carina-controller carina-scheduler 在每个节点运行 carina-node。

Carina 主要有三部分组成,分别是 carina-controller、carina-scheduler 和 carina-node,其架构图如下所示:

如上图架构所示,Carina 能够自动发现本地裸盘,并根据其磁盘特性划分为 hdd 磁盘卷组及 ssd 磁盘卷组等,针对于本地数据高可用,Carina 推出了基于 bcache 的磁盘缓存功能以及自动组件 RAID 功能;

Carina-node 是运行在每个节点上的 agent 服务,利用 lvm 技术管理本地磁盘,按照类别将本地磁盘划分到不同的 VG 卷组,并从中划分 LV 提供给 POD 使用;

Carina-scheduler 是 kubernetes 的调度插件,负责基于申请的 PV 大小,节点剩余磁盘空间大小,节点负载使用情况进行合理的调度。默认提供了 spreadout 及 binpack 两种调度策略;

Carina-controller 是 Carina 的控制平面,监听 PVC 等资源,维护 PVC,LV 之间的关系。

04 Carina VS Ceph-CSI / NFS-CSI

Carina 不同于 Ceph-CSI,NFS-CSI 等 Kubernetes 网络存储插件。这些插件为网络存储插件,解决了应用在 Kubernetes 场景下数据跟随的问题,而 Carina 解决的是在数据库和中间件场景下对挂载设备高性能读写的问题。

05 Carina 应用场景

场景一:数据库 Redis、Mysql
Redis 作为高性能的内存型数据库缓存服务,同样有数据落盘的需求,而使用网络存储往往有比较大延迟,在使用 Carina 情况下,能够提供和读写本地磁盘一致的性能。Redis 主从模式其本身已经解决了数据多地备份的问题,Carina 并不会提供更多冗余的数据备份,节省了磁盘空间。Mysql 作为严重依赖存储的数据库服务,使用 Carina 提供的存储卷使 Mysql 在云上运行可以获得更接近在物理机上运行的性能。

场景二:消息服务 rocketmq、activemq
大多数消息中间件都是基于内存的,为了维持消息不丢失,消息中间件还是有落盘的需求,比如对于需要 ACK 应答的消息中间件,若是消息非常多,消息服务一般会选择将时间较久的消息落盘,对于消息中间件来说对磁盘性能要求可谓极高,Carina 恰恰提供了等同于本地磁盘的读写性能,且对于消息中间件并未有多副本存储需求,因此 Carina 也避免了存储多副本带来的性能消耗.

场景三:普通应用 POD
Carina 的部署、运维、使用极其简易,可以被当做一般项目的本地存储使用,相当于 hostpath 。与 hostpath 不同的是 hostpath 要求在宿主机建立相关存储目录。Carina 则完全不用关心节点机器,直接创建原生 pvc 即可。

06 Join Us

Github:https://github.com/carina-io/carina
官方网站: http://www.opencarina.io
官方邮箱:carina@beyondcent.com
加粗文字
继续阅读 »

2021 年 10 月 11 日,博云正式开源 Carina 本地存储方案,Carina 基于 Kubernetes 及 LVM 实现,提供了数据库与中间件等有状态应用在 Kubernetes 中运行所必须的高性能的本地存储能力,极大减少了存储系统的运维压力。今年 9 月,Carina 还以首批成员身份加入了由中国信通院发起的可信开源社区共同体,并获得可信开源项目成员证书。

博客图片.jpg


Carina 最大的特点是高性能和免运维,为中间件、数据库等有状态服务提供了匹配本地磁盘的高 IOPS 和极低延迟的性能指标,同时易安装、自运维能力又极大的减轻了存储系统的运维压力。另外,Carina 还提供了本地磁盘管理能力、PV 高级调度能力、PV 自动分层技术、卷拓扑能力、自动 failover 能力、动态 IO 限速、监控告警、多种存储供给能力等高级功能。

目前, Carina 项目代码已在 Github 上开源,项目地址为:https://github.com/carina-io/carina。欢迎广大技术开发者和爱好者前去试用。

01 功能亮点

灵活高效的供给高 IOPS、低延迟的存储
免运维,自动管理本地磁盘、自动组建 RAID
多种调度策略可配
支持带宽 IOPS 限速
支持 PV 数据自动分层
支持 PV 自动扩容
支持 RAID 管理能力
支持容灾转移能力
提供块和文件的访问方式

02 Why Carina?

Kubernetes 原生支持
完全兼容的 Kubernetes API ,无需额外开发,依赖组件很少且均为通用开源组件。
本地磁盘管理
自动管理本地磁盘,提供 RAID 组建、数据分层、磁盘限速等高级功能。
设备注册
将本地磁盘注册为 Kubernetes 设备,参与容器调度评分。
容灾转移
支持在节点删除,将存储卷在其他节点重建。
文件和块存储
同时支持为容器提供文件存储和块存储,以及在线扩容。

03 How it works

云端是标准 Kubernetes 集群,可以使用任何 CSI 存储插件,比如 Ceph-CSI。在集群中运行 carina-controller carina-scheduler 在每个节点运行 carina-node。

Carina 主要有三部分组成,分别是 carina-controller、carina-scheduler 和 carina-node,其架构图如下所示:

如上图架构所示,Carina 能够自动发现本地裸盘,并根据其磁盘特性划分为 hdd 磁盘卷组及 ssd 磁盘卷组等,针对于本地数据高可用,Carina 推出了基于 bcache 的磁盘缓存功能以及自动组件 RAID 功能;

Carina-node 是运行在每个节点上的 agent 服务,利用 lvm 技术管理本地磁盘,按照类别将本地磁盘划分到不同的 VG 卷组,并从中划分 LV 提供给 POD 使用;

Carina-scheduler 是 kubernetes 的调度插件,负责基于申请的 PV 大小,节点剩余磁盘空间大小,节点负载使用情况进行合理的调度。默认提供了 spreadout 及 binpack 两种调度策略;

Carina-controller 是 Carina 的控制平面,监听 PVC 等资源,维护 PVC,LV 之间的关系。

04 Carina VS Ceph-CSI / NFS-CSI

Carina 不同于 Ceph-CSI,NFS-CSI 等 Kubernetes 网络存储插件。这些插件为网络存储插件,解决了应用在 Kubernetes 场景下数据跟随的问题,而 Carina 解决的是在数据库和中间件场景下对挂载设备高性能读写的问题。

05 Carina 应用场景

场景一:数据库 Redis、Mysql
Redis 作为高性能的内存型数据库缓存服务,同样有数据落盘的需求,而使用网络存储往往有比较大延迟,在使用 Carina 情况下,能够提供和读写本地磁盘一致的性能。Redis 主从模式其本身已经解决了数据多地备份的问题,Carina 并不会提供更多冗余的数据备份,节省了磁盘空间。Mysql 作为严重依赖存储的数据库服务,使用 Carina 提供的存储卷使 Mysql 在云上运行可以获得更接近在物理机上运行的性能。

场景二:消息服务 rocketmq、activemq
大多数消息中间件都是基于内存的,为了维持消息不丢失,消息中间件还是有落盘的需求,比如对于需要 ACK 应答的消息中间件,若是消息非常多,消息服务一般会选择将时间较久的消息落盘,对于消息中间件来说对磁盘性能要求可谓极高,Carina 恰恰提供了等同于本地磁盘的读写性能,且对于消息中间件并未有多副本存储需求,因此 Carina 也避免了存储多副本带来的性能消耗.

场景三:普通应用 POD
Carina 的部署、运维、使用极其简易,可以被当做一般项目的本地存储使用,相当于 hostpath 。与 hostpath 不同的是 hostpath 要求在宿主机建立相关存储目录。Carina 则完全不用关心节点机器,直接创建原生 pvc 即可。

06 Join Us

Github:https://github.com/carina-io/carina
官方网站: http://www.opencarina.io
官方邮箱:carina@beyondcent.com
加粗文字 收起阅读 »

云上 MySQL 的这 8 个要点,运维要了解一下


使用云上的 MySQL 时,会遇到很多人询问 CDB 的 为了更好的了解云上的 MySQL,本文将介绍一些重要的知识点。

实例类型

目前云数据库 MySQL 支持三种架构:基础版、高可用版、单节点高 IO 版。
  • 基础版是单个节点部署,价格低,性价比非常高,由于是单节点,数据安全性以及可用性不能保证,不建议生产环境使用
  • 高可用版采用一主 N 从的高可用模式,实时热备,提供宕机自动检测和故障自动转移。主从复制方式有三种:异步、半同步、强同步。高可用版默认一主一从异步复制方式,可以通过购买和升级迁移到一主二从强同步模式。
  • 单节点高 IO 版采用单个物理节点部署,性价比高;底层存储使用本地 NVMe SSD 硬盘,提供强大的 IO 性能。目前应用于只读实例,帮助业务分摊读压力,适用于有读写分离需求的各个行业应用。


数据库实例复制方式

异步复制

应用发起数据更新(含 insert、update、delete 等操作)请求,Master 在执行完更新操作后立即向应用程序返回响应,然后 Master 再向 Slave 复制数据。

数据更新过程中 Master 不需要等待 Slave 的响应,因此异步复制的数据库实例通常具有较高的性能,且 Slave 不可用并不影响 Master 对外提供服务。但因数据并非实时同步到 Slave,而 Master 在 Slave 有延迟的情况下发生故障则有较小概率会引起数据不一致。

腾讯云数据库 MySQL 异步复制采用一主一从的架构。

半同步复制

应用发起数据更新(含 insert、update、delete 操作)请求,Master 在执行完更新操作后立即向 Slave 复制数据,Slave 接收到数据并写到 relay log 中(无需执行) 后才向 Master 返回成功信息,Master 必须在接受到 Slave 的成功信息后再向应用程序返回响应。

仅在数据复制发生异常(Slave 节点不可用或者数据复制所用网络发生异常)的情况下,Master 会暂停(MySQL 默认 10 秒左右)对应用的响应,将复制方式降为异步复制。当数据复制恢复正常,将恢复为半同步复制。

腾讯云数据库 MySQL 半同步复制采用一主一从的架构。

强同步复制

应用发起数据更新(含 insert、update、delete 操作)请求,Master 在执行完更新操作后立即向 Slave 复制数据,Slave 接收到数据并执行完 后才向 Master 返回成功信息,Master 必须在接受到 Slave 的成功信息后再向应用程序返回响应。

因 Master 向 Slave 复制数据是同步进行的,Master 每次更新操作都需要同时保证 Slave 也成功执行,因此强同步复制能最大限度的保障主从数据的一致性。但因每次 Master 更新请求都强依赖于 Slave 的返回,因此 Slave 如果仅有单台,它不可用将会极大影响 Master 上的操作。

腾讯云数据库 MySQL 强同步复制采用一主两从的架构,仅需其中一台 Slave 成功执行即可返回,避免了单台 Slave 不可用影响 Master 上操作的问题,提高了强同步复制集群的可用性。

高可用实现原理

目前使用最多的就是高可用版本的一主一从架构,正常情况下,客户通过 VIP:Port 的方式链接到主库上,从库通过 binlog 和主进行同步。云上 MySQL 在数据库所在的物理机发生硬件故障时是如何保证高可用呢?

主所在物理机发生故障

  • 正常情况下,客户端通过 VIP:Port 的方式链接到主库上,从库通过 binlog 和主进行同步。如下图中的步骤 1
  • 当主库所在的宿主机发生异常宕机,此时客户端的链接就会被切换到从库(客户端具有断线重连几乎不受影响),此时从库进行读写。主库故障后,云平台会自动生成一个新的主从高可用实例,将最近一天的冷备导入到新实例对,在和当前的旧的从库进行 binlog 的同步。如下图中的步骤 2
  • binlog 增量同步完成后,旧的从库会和新的实例对一直进行同步状态,直至维护时间再次进行主动切换,切换时存在秒级闪断,业务有重连可以忽略闪断。此时客户端直接通过 VIP+Port 的方式连接到新建的实例对。旧实例就会被删除。详细的步骤如下图步骤 3


1.png

MySQL 主库故障切换示意图

从所在的物理机发生故障

从库所在的物理机发生故障是,对客户端来说业务是完全不受影响,在从库所在物理机异常后,云平台会自动发起重建从库的流程,在健康的物理机上新建一个从库,导入冷备数据后和主库进行同步,同步完毕后,此时数据库又恢复了主从高可用状态。

实例升级

数据库的升级不仅包含数据库版本升级,还包括硬件升配,当然硬件的降配具体的原理也是一样的。
  • 在控制台发起实例升级的任务后,云平台会自动创建一个新的实例对,该新实例对的配置是需要调整到的配置。先将最近一次的备份导出到新建实例对内,在和主实例进行 binlog 同步。如下图步骤 1
  • 主实例和新建实例对同步完成后,用户可以自行选择立即切换或在维护期内切换。整个切换过程秒级即可完成,完成后吗,客户端连接数据库请求都会到目标实例对,源实例对则会被自动回收。如下图步骤 2


从上面的步骤我们可以看到升级实例时,完全不影响数据库的正常使用。升级主要花费的时间是导入冷备和追 binlog 这两个步骤,而这两个环节的所需的时间取决于客户的数据量大小和产生的 binlog 的大小。一般导入冷备的速度是 50G/h(理论值仅供参考)。
2.png

数据库实例升级示意图

binlog 介绍

binlog 日志用于记录所有更改数据的语句,俗称二进制日志,主要用于复制和即时点恢复。主从复制也是依赖于 binlog 的。类似于 Oracle 的 archivelog,Mongodb的oplog,所有和写有关或者可能有关的语句,都会记录在 binlog 文件中。云上的 MySQL 数据库的 binlog 文件都是每 1G 自动生成一个(新购实例也可能 256M 做一次切割),除非做了 flush logs 的操作。

MySQL 的 binlog 默认保留 5 天,所以如果需要回档的话,只能恢复到 5 天内的任意时间点。

另外控制台下载的 binlog 日志,需要在本地解析的话,须确保客户端的 MySQL 版本与 CDB for MySQL 的版本一致,否则会出现解析出乱码的情况,建议使用 3.4 或以上版本的 mysqlbinlog

回档介绍

回档是将数据库通过冷备和 binlog 恢复到之前的某个时间点的一种操作。 CDB 的回档分为普通回档、快速回档以及极速回档:
  • 普通回档:导入该实例的全量备份,再在对选中的库、表进行回档。该回档模式无限制,但回档速度较慢
  • 快速回档:仅导入所选中库级别的备份和 binlog,如有跨库操作,且关联库未被同时选中,将会导致回档失败
  • 极速回档:仅导入所选中表级别的备份和 binlog,如有跨表操作,且关联表未被同时选中,将会导致回档失败。极速模式下,请手动选择需要回档的表。如果表已经被删除,需要客户自行创建表在进行回档操作。


慢查询

慢查询就是执行数据库查询时消耗时间比较大的 SQL 语句。MySQL CPU 利用率过高,大部分原因与低效 SQL 有关系,通过优化低效 SQL 基本可以解决大部分问题。MySQL 慢查询时间的默认值是 10s,在遇到性能问题时,若发现没有慢查询,建议将其参数调成 1s ,再观察业务周期内的慢查询,进而对其慢查询进行优化。

如果出现全表扫描较高的情况,可以打开 log_queries_not_using_indexes 参数,此时未使用索引的全表扫描也可以记录到慢查询里面。这个参数并不建议一直打开,会对数据库的磁盘造成较大影响。

MySQL 空间

用户使用查询语句得到的 MySQL 空间和控制台看到的已使用空间相比有很大出入,为什么?

MySQL 的空洞效应导致,使用过程中的一些碎片没有得到合理释放因此查询语句查出来的空间和控制台统计的实际已使用空间相比少了许多,这部分是碎片,彻底解决需要在夜深人静的时候执行 optimize table。

原文链接:https://cloud.tencent.com/deve ... 79285,作者:苏欣
继续阅读 »

使用云上的 MySQL 时,会遇到很多人询问 CDB 的 为了更好的了解云上的 MySQL,本文将介绍一些重要的知识点。

实例类型

目前云数据库 MySQL 支持三种架构:基础版、高可用版、单节点高 IO 版。
  • 基础版是单个节点部署,价格低,性价比非常高,由于是单节点,数据安全性以及可用性不能保证,不建议生产环境使用
  • 高可用版采用一主 N 从的高可用模式,实时热备,提供宕机自动检测和故障自动转移。主从复制方式有三种:异步、半同步、强同步。高可用版默认一主一从异步复制方式,可以通过购买和升级迁移到一主二从强同步模式。
  • 单节点高 IO 版采用单个物理节点部署,性价比高;底层存储使用本地 NVMe SSD 硬盘,提供强大的 IO 性能。目前应用于只读实例,帮助业务分摊读压力,适用于有读写分离需求的各个行业应用。


数据库实例复制方式

异步复制

应用发起数据更新(含 insert、update、delete 等操作)请求,Master 在执行完更新操作后立即向应用程序返回响应,然后 Master 再向 Slave 复制数据。

数据更新过程中 Master 不需要等待 Slave 的响应,因此异步复制的数据库实例通常具有较高的性能,且 Slave 不可用并不影响 Master 对外提供服务。但因数据并非实时同步到 Slave,而 Master 在 Slave 有延迟的情况下发生故障则有较小概率会引起数据不一致。

腾讯云数据库 MySQL 异步复制采用一主一从的架构。

半同步复制

应用发起数据更新(含 insert、update、delete 操作)请求,Master 在执行完更新操作后立即向 Slave 复制数据,Slave 接收到数据并写到 relay log 中(无需执行) 后才向 Master 返回成功信息,Master 必须在接受到 Slave 的成功信息后再向应用程序返回响应。

仅在数据复制发生异常(Slave 节点不可用或者数据复制所用网络发生异常)的情况下,Master 会暂停(MySQL 默认 10 秒左右)对应用的响应,将复制方式降为异步复制。当数据复制恢复正常,将恢复为半同步复制。

腾讯云数据库 MySQL 半同步复制采用一主一从的架构。

强同步复制

应用发起数据更新(含 insert、update、delete 操作)请求,Master 在执行完更新操作后立即向 Slave 复制数据,Slave 接收到数据并执行完 后才向 Master 返回成功信息,Master 必须在接受到 Slave 的成功信息后再向应用程序返回响应。

因 Master 向 Slave 复制数据是同步进行的,Master 每次更新操作都需要同时保证 Slave 也成功执行,因此强同步复制能最大限度的保障主从数据的一致性。但因每次 Master 更新请求都强依赖于 Slave 的返回,因此 Slave 如果仅有单台,它不可用将会极大影响 Master 上的操作。

腾讯云数据库 MySQL 强同步复制采用一主两从的架构,仅需其中一台 Slave 成功执行即可返回,避免了单台 Slave 不可用影响 Master 上操作的问题,提高了强同步复制集群的可用性。

高可用实现原理

目前使用最多的就是高可用版本的一主一从架构,正常情况下,客户通过 VIP:Port 的方式链接到主库上,从库通过 binlog 和主进行同步。云上 MySQL 在数据库所在的物理机发生硬件故障时是如何保证高可用呢?

主所在物理机发生故障

  • 正常情况下,客户端通过 VIP:Port 的方式链接到主库上,从库通过 binlog 和主进行同步。如下图中的步骤 1
  • 当主库所在的宿主机发生异常宕机,此时客户端的链接就会被切换到从库(客户端具有断线重连几乎不受影响),此时从库进行读写。主库故障后,云平台会自动生成一个新的主从高可用实例,将最近一天的冷备导入到新实例对,在和当前的旧的从库进行 binlog 的同步。如下图中的步骤 2
  • binlog 增量同步完成后,旧的从库会和新的实例对一直进行同步状态,直至维护时间再次进行主动切换,切换时存在秒级闪断,业务有重连可以忽略闪断。此时客户端直接通过 VIP+Port 的方式连接到新建的实例对。旧实例就会被删除。详细的步骤如下图步骤 3


1.png

MySQL 主库故障切换示意图

从所在的物理机发生故障

从库所在的物理机发生故障是,对客户端来说业务是完全不受影响,在从库所在物理机异常后,云平台会自动发起重建从库的流程,在健康的物理机上新建一个从库,导入冷备数据后和主库进行同步,同步完毕后,此时数据库又恢复了主从高可用状态。

实例升级

数据库的升级不仅包含数据库版本升级,还包括硬件升配,当然硬件的降配具体的原理也是一样的。
  • 在控制台发起实例升级的任务后,云平台会自动创建一个新的实例对,该新实例对的配置是需要调整到的配置。先将最近一次的备份导出到新建实例对内,在和主实例进行 binlog 同步。如下图步骤 1
  • 主实例和新建实例对同步完成后,用户可以自行选择立即切换或在维护期内切换。整个切换过程秒级即可完成,完成后吗,客户端连接数据库请求都会到目标实例对,源实例对则会被自动回收。如下图步骤 2


从上面的步骤我们可以看到升级实例时,完全不影响数据库的正常使用。升级主要花费的时间是导入冷备和追 binlog 这两个步骤,而这两个环节的所需的时间取决于客户的数据量大小和产生的 binlog 的大小。一般导入冷备的速度是 50G/h(理论值仅供参考)。
2.png

数据库实例升级示意图

binlog 介绍

binlog 日志用于记录所有更改数据的语句,俗称二进制日志,主要用于复制和即时点恢复。主从复制也是依赖于 binlog 的。类似于 Oracle 的 archivelog,Mongodb的oplog,所有和写有关或者可能有关的语句,都会记录在 binlog 文件中。云上的 MySQL 数据库的 binlog 文件都是每 1G 自动生成一个(新购实例也可能 256M 做一次切割),除非做了 flush logs 的操作。

MySQL 的 binlog 默认保留 5 天,所以如果需要回档的话,只能恢复到 5 天内的任意时间点。

另外控制台下载的 binlog 日志,需要在本地解析的话,须确保客户端的 MySQL 版本与 CDB for MySQL 的版本一致,否则会出现解析出乱码的情况,建议使用 3.4 或以上版本的 mysqlbinlog

回档介绍

回档是将数据库通过冷备和 binlog 恢复到之前的某个时间点的一种操作。 CDB 的回档分为普通回档、快速回档以及极速回档:
  • 普通回档:导入该实例的全量备份,再在对选中的库、表进行回档。该回档模式无限制,但回档速度较慢
  • 快速回档:仅导入所选中库级别的备份和 binlog,如有跨库操作,且关联库未被同时选中,将会导致回档失败
  • 极速回档:仅导入所选中表级别的备份和 binlog,如有跨表操作,且关联表未被同时选中,将会导致回档失败。极速模式下,请手动选择需要回档的表。如果表已经被删除,需要客户自行创建表在进行回档操作。


慢查询

慢查询就是执行数据库查询时消耗时间比较大的 SQL 语句。MySQL CPU 利用率过高,大部分原因与低效 SQL 有关系,通过优化低效 SQL 基本可以解决大部分问题。MySQL 慢查询时间的默认值是 10s,在遇到性能问题时,若发现没有慢查询,建议将其参数调成 1s ,再观察业务周期内的慢查询,进而对其慢查询进行优化。

如果出现全表扫描较高的情况,可以打开 log_queries_not_using_indexes 参数,此时未使用索引的全表扫描也可以记录到慢查询里面。这个参数并不建议一直打开,会对数据库的磁盘造成较大影响。

MySQL 空间

用户使用查询语句得到的 MySQL 空间和控制台看到的已使用空间相比有很大出入,为什么?

MySQL 的空洞效应导致,使用过程中的一些碎片没有得到合理释放因此查询语句查出来的空间和控制台统计的实际已使用空间相比少了许多,这部分是碎片,彻底解决需要在夜深人静的时候执行 optimize table。

原文链接:https://cloud.tencent.com/deve ... 79285,作者:苏欣 收起阅读 »

教你用 Docker 搭建网站


2013 年发布至今, Docker 一直广受瞩目,被认为可能会改变软件行业。

但是,许多人并不清楚 Docker 到底是什么,要解决什么问题,好处又在哪里?本文就来详细解释,帮助大家理解它,还带有简单易懂的实例,教你如何将它用于日常开发。

环境配置的难题

软件开发最大的麻烦事之一,就是环境配置。用户计算机的环境都不相同,你怎么知道自家的软件,能在那些机器跑起来?

用户必须保证两件事:操作系统的设置,各种库和组件的安装。只有它们都正确,软件才能运行。举例来说,安装一个 Python 应用,计算机必须有 Python 引擎,还必须有各种依赖,可能还要配置环境变量。

如果某些老旧的模块与当前环境不兼容,那就麻烦了。开发者常常会说:"它在我的机器可以跑了"(It works on my machine),言下之意就是,其他机器很可能跑不了。

环境配置如此麻烦,换一台机器,就要重来一次,旷日费时。很多人想到,能不能从根本上解决问题,软件可以带环境安装?也就是说,安装的时候,把原始环境一模一样地复制过来。

虚拟机

虚拟机(virtual machine)就是带环境安装的一种解决方案。它可以在一种操作系统里面运行另一种操作系统,比如在 Windows 系统里面运行 Linux 系统。应用程序对此毫无感知,因为虚拟机看上去跟真实系统一模一样,而对于底层系统来说,虚拟机就是一个普通文件,不需要了就删掉,对其他部分毫无影响。

虽然用户可以通过虚拟机还原软件的原始环境。但是,这个方案有几个缺点:
  • 资源占用多,虚拟机会独占一部分内存和硬盘空间。它运行的时候,其他程序就不能使用这些资源了。哪怕虚拟机里面的应用程序,真正使用的内存只有 1MB,虚拟机依然需要几百 MB 的内存才能运行。
  • 冗余步骤多,虚拟机是完整的操作系统,一些系统级别的操作步骤,往往无法跳过,比如用户登录。
  • 启动慢,启动操作系统需要多久,启动虚拟机就需要多久。可能要等几分钟,应用程序才能真正运行。


Linux 容器

由于虚拟机存在这些缺点,Linux 发展出了另一种虚拟化技术:Linux 容器(Linux Containers,缩写为 LXC)。

Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。或者说,在正常进程的外面套了一个保护层。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。

由于容器是进程级别的,相比虚拟机有很多优势。
  • 启动快,容器里面的应用,直接就是底层系统的一个进程,而不是虚拟机内部的进程。所以,启动容器相当于启动本机的一个进程,而不是启动一个操作系统,速度就快很多。
  • 资源占用少,容器只占用需要的资源,不占用那些没有用到的资源;虚拟机由于是完整的操作系统,不可避免要占用所有资源。另外,多个容器可以共享资源,虚拟机都是独享资源。
  • 体积小,容器只要包含用到的组件即可,而虚拟机是整个操作系统的打包,所以容器文件比虚拟机文件要小很多。


总之,容器有点像轻量级的虚拟机,能够提供虚拟化的环境,但是成本开销小得多。

Docker 是什么?

Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。

Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。

总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。

Docker 的用途

Docker 的主要用途,目前有三大类。
  • 提供一次性的环境。比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。
  • 提供弹性的云服务。因为 Docker 容器可以随开随关,很适合动态扩容和缩容。
  • 组建微服务架构。通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。


Docker 的安装

Docker 是一个开源的商业产品,有两个版本:社区版(Community Edition,缩写为 CE)和企业版(Enterprise Edition,缩写为 EE)。企业版包含了一些收费服务,个人开发者一般用不到。下面的介绍都针对社区版。

Docker CE 的安装请参考官方文档。


安装完成后,运行下面的命令,验证是否安装成功。
$ docker version
# 或者
$ docker info

Docker 需要用户具有 sudo 权限,为了避免每次命令都输入 sudo,可以把用户加入 Docker 用户组(官方文档)。
$ sudo usermod -aG docker $USER

Docker 是服务器----客户端架构。命令行运行 docker 命令的时候,需要本机有 Docker 服务。如果这项服务没有启动,可以用下面的命令启动(官方文档)。
# service 命令的用法
$ sudo service docker start

# systemctl 命令的用法
$ sudo systemctl start docker

image 文件

Docker 把应用程序及其依赖,打包在 image 文件里面。只有通过这个文件,才能生成 Docker 容器。image 文件可以看作是容器的模板。Docker 根据 image 文件生成容器的实例。同一个 image 文件,可以生成多个同时运行的容器实例。

image 是二进制文件。实际开发中,一个 image 文件往往通过继承另一个 image 文件,加上一些个性化设置而生成。举例来说,你可以在 Ubuntu 的 image 基础上,往里面加入 Apache 服务器,形成你的 image。
# 列出本机的所有 image 文件。
$ docker image ls

# 删除 image 文件
$ docker image rm [imageName] 

image 文件是通用的,一台机器的 image 文件拷贝到另一台机器,照样可以使用。一般来说,为了节省时间,我们应该尽量使用别人制作好的 image 文件,而不是自己制作。即使要定制,也应该基于别人的 image 文件进行加工,而不是从零开始制作。

为了方便共享,image 文件制作完成后,可以上传到网上的仓库。Docker 的官方仓库 Docker Hub 是最重要、最常用的 image 仓库。此外,出售自己制作的 image 文件也是可以的。

实例:hello world

下面,我们通过最简单的 image 文件“hello world”,感受一下 Docker。

首先,运行下面的命令,将 image 文件从仓库抓取到本地。
$ docker image pull library/hello-world

上面代码中,docker image pull 是抓取 image 文件的命令。library/hello-world 是 image 文件在仓库里面的位置,其中 library 是 image 文件所在的组,hello-world 是 image 文件的名字。

由于 Docker 官方提供的 image 文件,都放在 library 组里面,所以它的是默认组,可以省略。因此,上面的命令可以写成下面这样。
$ docker image pull hello-world

抓取成功以后,就可以在本机看到这个 image 文件了。
$ docker image ls

现在,运行这个 image 文件。
$ docker container run hello-world

docker container run 命令会从 image 文件,生成一个正在运行的容器实例。

注意,docker container run 命令具有自动抓取 image 文件的功能。如果发现本地没有指定的 image 文件,就会从仓库自动抓取。因此,前面的 docker image pull 命令并不是必需的步骤。

如果运行成功,你会在屏幕上读到下面的输出。
$ docker container run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

... ...

输出这段提示以后,hello world 就会停止运行,容器自动终止。

有些容器不会自动终止,因为提供的是服务。比如,安装运行 Ubuntu 的 image,就可以在命令行体验 Ubuntu 系统。
$ docker container run -it ubuntu bash

对于那些不会自动终止的容器,必须使用 docker container kill 命令手动终止。
$ docker container kill [containID]

容器文件

image 文件生成的容器实例,本身也是一个文件,称为容器文件。也就是说,一旦容器生成,就会同时存在两个文件: image 文件和容器文件。而且关闭容器并不会删除容器文件,只是容器停止运行而已。
# 列出本机正在运行的容器
$ docker container ls

# 列出本机所有容器,包括终止运行的容器
$ docker container ls --all

上面命令的输出结果之中,包括容器的 ID。很多地方都需要提供这个 ID,比如上一节终止容器运行的 docker container kill 命令。

终止运行的容器文件,依然会占据硬盘空间,可以使用 docker container rm 命令删除。
$ docker container rm [containerID] 

运行上面的命令之后,再使用 docker container ls --all 命令,就会发现被删除的容器文件已经消失了。

Dockerfile 文件

学会使用 image 文件以后,接下来的问题就是,如何可以生成 image 文件?如果你要推广自己的软件,势必要自己制作 image 文件。

这就需要用到 Dockerfile 文件。它是一个文本文件,用来配置 image。Docker 根据 该文件生成二进制的 image 文件。

下面通过一个实例,演示如何编写 Dockerfile 文件。

实例:制作自己的 Docker 容器

下面我以 koa-demos 项目为例,介绍怎么写 Dockerfile 文件,实现让用户在 Docker 容器里面运行 Koa 框架。

作为准备工作,请先 下载源码
$ git clone https://github.com/ruanyf/koa-demos.git
$ cd koa-demos

编写 Dockerfile 文件

首先,在项目的根目录下,新建一个文本文件 .dockerignore,写入下面的 内容
.git
node_modules
npm-debug.log

上面代码表示,这三个路径要排除,不要打包进入 image 文件。如果你没有路径要排除,这个文件可以不新建。

然后,在项目的根目录下,新建一个文本文件 Dockerfile,写入下面的 内容
FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000

上面代码一共五行,含义如下。
  • FROM node:8.4:该 image 文件继承官方的 node image,冒号表示标签,这里标签是 8.4,即 8.4 版本的 Node。
  • COPY . /app:将当前目录下的所有文件(除了 .dockerignore 排除的路径),都拷贝进入 image 文件的 /app 目录。
  • WORKDIR /app:指定接下来的工作路径为 /app
  • RUN npm install:在 /app 目录下,运行 npm install 命令安装依赖。注意,安装后所有的依赖,都将打包进入 image 文件。
  • EXPOSE 3000:将容器 3000 端口暴露出来, 允许外部连接这个端口。


创建 image 文件

有了 Dockerfile 文件以后,就可以使用 docker image build 命令创建 image 文件了。
$ docker image build -t koa-demo .
# 或者
$ docker image build -t koa-demo:0.0.1 .

上面代码中,-t 参数用来指定 image 文件的名字,后面还可以用冒号指定标签。如果不指定,默认的标签就是 latest。最后的那个点表示 Dockerfile 文件所在的路径,上例是当前路径,所以是一个点。

如果运行成功,就可以看到新生成的 image 文件 koa-demo 了。
$ docker image ls

生成容器

docker container run 命令会从 image 文件生成容器。
$ docker container run -p 8000:3000 -it koa-demo /bin/bash
# 或者
$ docker container run -p 8000:3000 -it koa-demo:0.0.1 /bin/bash

上面命令的各个参数含义如下:
  • -p 参数:容器的 3000 端口映射到本机的 8000 端口。
  • -it 参数:容器的 Shell 映射到当前的 Shell,然后你在本机窗口输入的命令,就会传入容器。
  • koa-demo:0.0.1:image 文件的名字(如果有标签,还需要提供标签,默认是 latest 标签)。
  • /bin/bash:容器启动以后,内部第一个执行的命令。这里是启动 Bash,保证用户可以使用 Shell。


如果一切正常,运行上面的命令以后,就会返回一个命令行提示符。
root@66d80f4aaf1e:/app#

这表示你已经在容器里面了,返回的提示符就是容器内部的 Shell 提示符。执行下面的命令。
root@66d80f4aaf1e:/app# node demos/01.js

这时,Koa 框架已经运行起来了。打开本机的浏览器,访问 http://127.0.0.1:8000,网页显示“Not Found”,这是因为这个 demo 没有写路由。

这个例子中,Node 进程运行在 Docker 容器的虚拟环境里面,进程接触到的文件系统和网络接口都是虚拟的,与本机的文件系统和网络接口是隔离的,因此需要定义容器与物理机的端口映射(map)。

现在,在容器的命令行,按下 Ctrl + c 停止 Node 进程,然后按下 Ctrl + d (或者输入 exit)退出容器。此外,也可以用 docker container kill 终止容器运行。
# 在本机的另一个终端窗口,查出容器的 ID
$ docker container ls

# 停止指定的容器运行
$ docker container kill [containerID] 

容器停止运行之后,并不会消失,用下面的命令删除容器文件。
# 查出容器的 ID
$ docker container ls --all

# 删除指定的容器文件
$ docker container rm [containerID] 

也可以使用 docker container run 命令的 --rm 参数,在容器终止运行后自动删除容器文件。
$ docker container run --rm -p 8000:3000 -it koa-demo /bin/bash

CMD 命令

上一节的例子里面,容器启动以后,需要手动输入命令 node demos/01.js。我们可以把这个命令写在 Dockerfile 里面,这样容器启动以后,这个命令就已经执行了,不用再手动输入了。
FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000
CMD node demos/01.js

上面的 Dockerfile 里面,多了最后一行 CMD node demos/01.js,它表示容器启动后自动执行 node demos/01.js

你可能会问,RUN 命令与 CMD 命令的区别在哪里?简单说,RUN 命令在 image 文件的构建阶段执行,执行结果都会打包进入 image 文件;CMD 命令则是在容器启动后执行。另外,一个 Dockerfile 可以包含多个 RUN 命令,但是只能有一个 CMD 命令。

注意,指定了 CMD 命令以后,docker container run 命令就不能附加命令了(比如前面的 /bin/bash),否则它会覆盖 CMD 命令。现在,启动容器可以使用下面的命令。
$ docker container run --rm -p 8000:3000 -it koa-demo:0.0.1

发布 image 文件

容器运行成功后,就确认了 image 文件的有效性。这时,我们就可以考虑把 image 文件分享到网上,让其他人使用。

首先,去 hub.docker.comcloud.docker.com 注册一个账户。然后,用下面的命令登录。
$ docker login

接着,为本地的 image 标注用户名和版本。
$ docker image tag [imageName] [username]/[repository]:[tag]
# 实例
$ docker image tag koa-demos:0.0.1 ruanyf/koa-demos:0.0.1

也可以不标注用户名,重新构建一下 image 文件。
$ docker image build -t [username]/[repository]:[tag] .

最后,发布 image 文件。
$ docker image push [username]/[repository]:[tag]

发布成功以后,登录 hub.docker.com,就可以看到已经发布的 image 文件。

其他有用的命令

Docker 的主要用法就是上面这些,此外还有几个命令,也非常有用。

docker container start

前面的 docker container run 命令是新建容器,每运行一次,就会新建一个容器。同样的命令运行两次,就会生成两个一模一样的容器文件。如果希望重复使用容器,就要使用 docker container start 命令,它用来启动已经生成、已经停止运行的容器文件。
$ docker container start [containerID] 

docker container stop

前面的 docker container kill 命令终止容器运行,相当于向容器里面的主进程发出 SIGKILL 信号。而 docker container stop 命令也是用来终止容器运行,相当于向容器里面的主进程发出 SIGTERM 信号,然后过一段时间再发出 SIGKILL 信号。
$ docker container stop [containerID] 

这两个信号的差别是,应用程序收到 SIGTERM 信号以后,可以自行进行收尾清理工作,但也可以不理会这个信号。如果收到 SIGKILL 信号,就会强行立即终止,那些正在进行中的操作会全部丢失。

docker container logs

docker container logs 命令用来查看 Docker 容器的输出,即容器里面 Shell 的标准输出。如果 docker run 命令运行容器的时候,没有使用 -it 参数,就要用这个命令查看输出。
$ docker container logs [containerID] 

docker container exec

docker container exec 命令用于进入一个正在运行的 Docker 容器。如果 docker run 命令运行容器的时候,没有使用 -it 参数,就要用这个命令进入容器。一旦进入了容器,就可以在容器的 Shell 执行命令了。
$ docker container exec -it [containerID] /bin/bash

docker container cp

docker container cp 命令用于从正在运行的 Docker 容器里面,将文件拷贝到本机。下面是拷贝到当前目录的写法。
$ docker container cp [containID]:[/path/to/file] . 

Docker 是一个容器工具,提供虚拟环境。很多人认为,它改变了我们对软件的认识。

站在 Docker 的角度,软件就是容器的组合:业务逻辑容器、数据库容器、储存容器、队列容器......Docker 使得软件可以拆分成若干个标准化容器,然后像搭积木一样组合起来。
1.png

这正是微服务(microservices)的思想:软件把任务外包出去,让各种外部服务完成这些任务,软件本身只是底层服务的调度中心和组装层。
2.jpg

微服务很适合用 Docker 容器实现,每个容器承载一个服务。一台计算机同时运行多个容器,从而就能很轻松地模拟出复杂的微服务架构。
3.png

之前的内容介绍了 Docker 的概念和基本用法,接着往下介绍,如何在一台计算机上实现多个服务,让它们互相配合,组合出一个应用程序。
4.png

我选择的示例软件是 WordPress。它是一个常用软件,全世界用户据说超过几千万。同时它又非常简单,只要两个容器就够了(业务容器 + 数据库容器),很适合教学。而且,这种"业务 + 数据库"的容器架构,具有通用性,许多应用程序都可以复用。

为了加深读者理解,本文采用三种方法,演示如何架设 WordPress 网站。
  • 方法 A:自建 WordPress 容器
  • 方法 B:采用官方的 WordPress 容器
  • 方法 C:采用 Docker Compose 工具


预备工作:image 仓库的镜像网址

本教程需要从仓库下载 image 文件,但是国内访问 Docker 的官方仓库很慢,还经常断线,所以要把仓库网址改成国内的镜像站。这里推荐使用官方镜像 registry.docker-cn.com 。下面是我的 Debian 系统的默认仓库修改方法,其他系统的修改方法参考官方文档

打开 /etc/default/docker 文件(需要 sudo 权限),在文件的底部加上一行。
DOCKER_OPTS="--registry-mirror=https://registry.docker-cn.com"

然后,重启 Docker 服务。
$ sudo service docker restart

现在就会自动从镜像仓库下载 image 文件了。

方法 A:自建 WordPress 容器

前面说过,本文会用三种方法演示 WordPress 的安装。第一种方法就是自建 WordPress 容器。

官方的 PHP image

首先,新建一个工作目录,并进入该目录。
$ mkdir docker-demo && cd docker-demo

然后,执行下面的命令。
$ docker container run \
--rm \
--name wordpress \
--volume "$PWD/":/var/www/html \
php:5.6-apache

上面的命令基于 php 的 image 文件新建一个容器,并且运行该容器。php 的标签是 5.6-apache,说明装的是 PHP 5.6,并且自带 Apache 服务器。该命令的三个参数含义如下。
  • --rm:停止运行后,自动删除容器文件。
  • --name wordpress:容器的名字叫做 wordpress
  • --volume &quot;$PWD/&quot;:/var/www/html:将当前目录($PWD)映射到容器的 /var/www/html(Apache 对外访问的默认目录)。因此,当前目录的任何修改,都会反映到容器里面,进而被外部访问到。


运行上面的命令以后,如果一切正常,命令行会提示容器对外的 IP 地址,请记下这个地址,我们要用它来访问容器。我分配到的 IP 地址是 172.17.0.2。

打开浏览器,访问 172.17.0.2,你会看到下面的提示。
Forbidden
You don't have permission to access / on this server.

这是因为容器的 /var/www/html 目录(也就是本机的 docker-demo 目录)下面什么也没有,无法提供可以访问的内容。

请在本机的 docker-demo 目录下面,添加一个最简单的 PHP 文件 index.php
<?php 
phpinfo();
?>

保存以后,浏览器刷新 172.17.0.2,应该就会看到熟悉的 phpinfo 页面了。
5.jpeg

拷贝 WordPress 安装包

既然本地的 docker-demo 目录可以映射到容器里面,那么把 WordPress 安装包拷贝到 docker-demo 目录下,不就可以通过容器访问到 WordPress 的安装界面了吗?

首先,在 docker-demo 目录下,执行下面的命令,抓取并解压 WordPress 安装包。
$ wget https://cn.wordpress.org/wordpress-4.9.4-zh_CN.tar.gz
$ tar -xvf wordpress-4.9.4-zh_CN.tar.gz

解压以后,WordPress 的安装文件会在 docker-demo/wordpress 目录下。

这时浏览器访问 http://172.17.0.2/wordpress,就能看到 WordPress 的安装提示了。
6.png

官方的 MySQL 容器

WordPress 必须有数据库才能安装,所以必须新建 MySQL 容器。

打开一个新的命令行窗口,执行下面的命令。
$ docker container run \
-d \
--rm \
--name wordpressdb \
--env MYSQL_ROOT_PASSWORD=123456 \
--env MYSQL_DATABASE=wordpress \
mysql:5.7

上面的命令会基于 MySQL 的 image 文件(5.7版本)新建一个容器。该命令的五个命令行参数的含义如下。
  • -d:容器启动后,在后台运行。
  • --rm:容器终止运行后,自动删除容器文件。
  • --name wordpressdb:容器的名字叫做 wordpressdb
  • --env MYSQL_ROOT_PASSWORD=123456:向容器进程传入一个环境变量 MYSQL_ROOT_PASSWORD,该变量会被用作 MySQL 的根密码。
  • --env MYSQL_DATABASE=wordpress:向容器进程传入一个环境变量 MYSQL_DATABASE,容器里面的 MySQL 会根据该变量创建一个同名数据库(本例是 WordPress)。


运行上面的命令以后,正常情况下,命令行会显示一行字符串,这是容器的 ID,表示已经新建成功了。

这时,使用下面的命令查看正在运行的容器,你应该看到 wordpresswordpressdb 两个容器正在运行。
$ docker container ls

其中,wordpressdb 是后台运行的,前台看不见它的输出,必须使用下面的命令查看。
$ docker container logs wordpressdb

定制 PHP 容器

现在 WordPress 容器和 MySQL 容器都已经有了。接下来,要把 WordPress 容器连接到 MySQL 容器了。但是,PHP 的官方 image 不带有 mysql 扩展,必须自己新建 image 文件。

首先,停掉 WordPress 容器。
$ docker container stop wordpress

停掉以后,由于 --rm 参数的作用,该容器文件会被自动删除。

然后,在 docker-demo 目录里面,新建一个 Dockerfile 文件,写入下面的内容。
FROM php:5.6-apache
RUN docker-php-ext-install mysqli
CMD apache2-foreground

上面代码的意思,就是在原来 PHP 的 image 基础上,安装 mysqli 的扩展。然后,启动 Apache。

基于这个 Dockerfile 文件,新建一个名为 phpwithmysql 的 image 文件。
$ docker build -t phpwithmysql .

Wordpress 容器连接 MySQL

现在基于 phpwithmysql image,重新新建一个 WordPress 容器。
$ docker container run \
--rm \
--name wordpress \
--volume "$PWD/":/var/www/html \
--link wordpressdb:mysql \
phpwithmysql

跟上一次相比,上面的命令多了一个参数 --link wordpressdb:mysql,表示 WordPress 容器要连到 wordpressdb 容器,冒号表示该容器的别名是 mysql

这时还要改一下wordpress目录的权限,让容器可以将配置信息写入这个目录(容器内部写入的/var/www/html目录,会映射到这个目录)。
$ chmod -R 777 wordpress

接着,回到浏览器的 http://172.17.0.2/wordpress 页面,点击“现在就开始!”按钮,开始安装。

WordPress 提示要输入数据库参数。输入的参数如下。
7.png

  • 数据库名:wordpress
  • 用户名:root
  • 密码:123456
  • 数据库主机:mysql
  • 表前缀:wp_(不变)


点击“下一步”按钮,如果 Wordpress 连接数据库成功,就会出现下面的页面,这就表示可以安装了。
8.png

至此,自建 WordPress 容器的演示完毕,可以把正在运行的两个容器关闭了(容器文件会自动删除)。
$ docker container stop wordpress wordpressdb

方法 B:Wordpress 官方镜像

上一部分的自建 WordPress 容器,还是挺麻烦的。其实不用这么麻烦,Docker 已经提供了官方 WordPress image,直接用那个就可以了。有了上一部分的基础,下面的操作就很容易理解了。

基本用法

首先,新建并启动 MySQL 容器。
$ docker container run \
-d \
--rm \
--name wordpressdb \
--env MYSQL_ROOT_PASSWORD=123456 \
--env MYSQL_DATABASE=wordpress \
mysql:5.7

然后,基于官方的 WordPress image,新建并启动 WordPress 容器。
$ docker container run \
-d \
--rm \
--name wordpress \
--env WORDPRESS_DB_PASSWORD=123456 \
--link wordpressdb:mysql \
wordpress

上面命令中,各个参数的含义前面都解释过了,其中环境变量 WORDPRESS_DB_PASSWORD 是 MySQL 容器的根密码。

上面命令指定 wordpress 容器在后台运行,导致前台看不见输出,使用下面的命令查出 wordpress 容器的 IP 地址。
$ docker container inspect wordpress

上面命令运行以后,会输出很多内容,找到 IPAddress 字段即可。我的机器返回的 IP 地址是172.17.0.3

浏览器访问172.17.0.3,就会看到 WordPress 的安装提示。
9.png

WordPress 容器的定制

到了上一步,官方 WordPress 容器的安装就已经成功了。但是,这种方法有两个很不方便的地方。
  • 每次新建容器,返回的 IP 地址不能保证相同,导致要更换 IP 地址访问 WordPress。
  • WordPress 安装在容器里面,本地无法修改文件。


解决这两个问题很容易,只要新建容器的时候,加两个命令行参数就可以了。

先把刚才启动的 WordPress 容器终止(容器文件会自动删除)。
$ docker container stop wordpress

然后,使用下面的命令新建并启动 WordPress 容器。
$ docker container run \
-d \
-p 127.0.0.2:8080:80 \
--rm \
--name wordpress \
--env WORDPRESS_DB_PASSWORD=123456 \
--link wordpressdb:mysql \
--volume "$PWD/wordpress":/var/www/html \
wordpress

上面的命令跟前面相比,命令行参数只多出了两个。
  • -p 127.0.0.2:8080:80:将容器的 80 端口映射到 127.0.0.28080 端口。
  • --volume &quot;$PWD/wordpress&quot;:/var/www/html:将容器的 /var/www/html 目录映射到当前目录的 wordpress 子目录。


浏览器访问 127.0.0.2:8080:80 就能看到 WordPress 的安装提示了。而且,你在 wordpress 子目录下的每次修改,都会反映到容器里面。

最后,终止这两个容器(容器文件会自动删除)。
$ docker container stop wordpress wordpressdb

方法 C:Docker Compose 工具

上面的方法 B 已经挺简单了,但是必须自己分别启动两个容器,启动的时候,还要在命令行提供容器之间的连接信息。因此,Docker 提供了一种更简单的方法,来管理多个容器的联动。

Docker Compose 简介

10.jpeg

Compose 是 Docker 公司推出的一个工具软件,可以管理多个 Docker 容器组成一个应用。你需要定义一个 YAML 格式的配置文件 docker-compose.yml,写好多个容器之间的调用关系。然后,只要一个命令,就能同时启动/关闭这些容器。
# 启动所有服务
$ docker-compose up
# 关闭所有服务
$ docker-compose stop

Docker Compose 的安装

Mac 和 Windows 在安装 Docker 的时候,会一起安装 docker compose。Linux 系统下的安装参考官方文档

安装完成后,运行下面的命令。
$ docker-compose --version

WordPress 示例

docker-demo 目录下,新建 docker-compose.yml 文件,写入下面的内容。
mysql:
image: mysql:5.7
environment:
 - MYSQL_ROOT_PASSWORD=123456
 - MYSQL_DATABASE=wordpress
web:
image: wordpress
links:
 - mysql
environment:
 - WORDPRESS_DB_PASSWORD=123456
ports:
 - "127.0.0.3:8080:80"
working_dir: /var/www/html
volumes:
 - wordpress:/var/www/html

上面代码中,两个顶层标签表示有两个容器 mysqlweb。每个容器的具体设置,前面都已经讲解过了,还是挺容易理解的。

启动两个容器。
$ docker-compose up

浏览器访问 http://127.0.0.3:8080,应该就能看到 WordPress 的安装界面。

现在关闭两个容器。
$ docker-compose stop

关闭以后,这两个容器文件还是存在的,写在里面的数据不会丢失。下次启动的时候,还可以复用。下面的命令可以把这两个容器文件删除(容器必须已经停止运行)。
$ docker-compose rm

原文链接:
继续阅读 »

2013 年发布至今, Docker 一直广受瞩目,被认为可能会改变软件行业。

但是,许多人并不清楚 Docker 到底是什么,要解决什么问题,好处又在哪里?本文就来详细解释,帮助大家理解它,还带有简单易懂的实例,教你如何将它用于日常开发。

环境配置的难题

软件开发最大的麻烦事之一,就是环境配置。用户计算机的环境都不相同,你怎么知道自家的软件,能在那些机器跑起来?

用户必须保证两件事:操作系统的设置,各种库和组件的安装。只有它们都正确,软件才能运行。举例来说,安装一个 Python 应用,计算机必须有 Python 引擎,还必须有各种依赖,可能还要配置环境变量。

如果某些老旧的模块与当前环境不兼容,那就麻烦了。开发者常常会说:"它在我的机器可以跑了"(It works on my machine),言下之意就是,其他机器很可能跑不了。

环境配置如此麻烦,换一台机器,就要重来一次,旷日费时。很多人想到,能不能从根本上解决问题,软件可以带环境安装?也就是说,安装的时候,把原始环境一模一样地复制过来。

虚拟机

虚拟机(virtual machine)就是带环境安装的一种解决方案。它可以在一种操作系统里面运行另一种操作系统,比如在 Windows 系统里面运行 Linux 系统。应用程序对此毫无感知,因为虚拟机看上去跟真实系统一模一样,而对于底层系统来说,虚拟机就是一个普通文件,不需要了就删掉,对其他部分毫无影响。

虽然用户可以通过虚拟机还原软件的原始环境。但是,这个方案有几个缺点:
  • 资源占用多,虚拟机会独占一部分内存和硬盘空间。它运行的时候,其他程序就不能使用这些资源了。哪怕虚拟机里面的应用程序,真正使用的内存只有 1MB,虚拟机依然需要几百 MB 的内存才能运行。
  • 冗余步骤多,虚拟机是完整的操作系统,一些系统级别的操作步骤,往往无法跳过,比如用户登录。
  • 启动慢,启动操作系统需要多久,启动虚拟机就需要多久。可能要等几分钟,应用程序才能真正运行。


Linux 容器

由于虚拟机存在这些缺点,Linux 发展出了另一种虚拟化技术:Linux 容器(Linux Containers,缩写为 LXC)。

Linux 容器不是模拟一个完整的操作系统,而是对进程进行隔离。或者说,在正常进程的外面套了一个保护层。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。

由于容器是进程级别的,相比虚拟机有很多优势。
  • 启动快,容器里面的应用,直接就是底层系统的一个进程,而不是虚拟机内部的进程。所以,启动容器相当于启动本机的一个进程,而不是启动一个操作系统,速度就快很多。
  • 资源占用少,容器只占用需要的资源,不占用那些没有用到的资源;虚拟机由于是完整的操作系统,不可避免要占用所有资源。另外,多个容器可以共享资源,虚拟机都是独享资源。
  • 体积小,容器只要包含用到的组件即可,而虚拟机是整个操作系统的打包,所以容器文件比虚拟机文件要小很多。


总之,容器有点像轻量级的虚拟机,能够提供虚拟化的环境,但是成本开销小得多。

Docker 是什么?

Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。

Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。

总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。

Docker 的用途

Docker 的主要用途,目前有三大类。
  • 提供一次性的环境。比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。
  • 提供弹性的云服务。因为 Docker 容器可以随开随关,很适合动态扩容和缩容。
  • 组建微服务架构。通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。


Docker 的安装

Docker 是一个开源的商业产品,有两个版本:社区版(Community Edition,缩写为 CE)和企业版(Enterprise Edition,缩写为 EE)。企业版包含了一些收费服务,个人开发者一般用不到。下面的介绍都针对社区版。

Docker CE 的安装请参考官方文档。


安装完成后,运行下面的命令,验证是否安装成功。
$ docker version
# 或者
$ docker info

Docker 需要用户具有 sudo 权限,为了避免每次命令都输入 sudo,可以把用户加入 Docker 用户组(官方文档)。
$ sudo usermod -aG docker $USER

Docker 是服务器----客户端架构。命令行运行 docker 命令的时候,需要本机有 Docker 服务。如果这项服务没有启动,可以用下面的命令启动(官方文档)。
# service 命令的用法
$ sudo service docker start

# systemctl 命令的用法
$ sudo systemctl start docker

image 文件

Docker 把应用程序及其依赖,打包在 image 文件里面。只有通过这个文件,才能生成 Docker 容器。image 文件可以看作是容器的模板。Docker 根据 image 文件生成容器的实例。同一个 image 文件,可以生成多个同时运行的容器实例。

image 是二进制文件。实际开发中,一个 image 文件往往通过继承另一个 image 文件,加上一些个性化设置而生成。举例来说,你可以在 Ubuntu 的 image 基础上,往里面加入 Apache 服务器,形成你的 image。
# 列出本机的所有 image 文件。
$ docker image ls

# 删除 image 文件
$ docker image rm [imageName] 

image 文件是通用的,一台机器的 image 文件拷贝到另一台机器,照样可以使用。一般来说,为了节省时间,我们应该尽量使用别人制作好的 image 文件,而不是自己制作。即使要定制,也应该基于别人的 image 文件进行加工,而不是从零开始制作。

为了方便共享,image 文件制作完成后,可以上传到网上的仓库。Docker 的官方仓库 Docker Hub 是最重要、最常用的 image 仓库。此外,出售自己制作的 image 文件也是可以的。

实例:hello world

下面,我们通过最简单的 image 文件“hello world”,感受一下 Docker。

首先,运行下面的命令,将 image 文件从仓库抓取到本地。
$ docker image pull library/hello-world

上面代码中,docker image pull 是抓取 image 文件的命令。library/hello-world 是 image 文件在仓库里面的位置,其中 library 是 image 文件所在的组,hello-world 是 image 文件的名字。

由于 Docker 官方提供的 image 文件,都放在 library 组里面,所以它的是默认组,可以省略。因此,上面的命令可以写成下面这样。
$ docker image pull hello-world

抓取成功以后,就可以在本机看到这个 image 文件了。
$ docker image ls

现在,运行这个 image 文件。
$ docker container run hello-world

docker container run 命令会从 image 文件,生成一个正在运行的容器实例。

注意,docker container run 命令具有自动抓取 image 文件的功能。如果发现本地没有指定的 image 文件,就会从仓库自动抓取。因此,前面的 docker image pull 命令并不是必需的步骤。

如果运行成功,你会在屏幕上读到下面的输出。
$ docker container run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

... ...

输出这段提示以后,hello world 就会停止运行,容器自动终止。

有些容器不会自动终止,因为提供的是服务。比如,安装运行 Ubuntu 的 image,就可以在命令行体验 Ubuntu 系统。
$ docker container run -it ubuntu bash

对于那些不会自动终止的容器,必须使用 docker container kill 命令手动终止。
$ docker container kill [containID]

容器文件

image 文件生成的容器实例,本身也是一个文件,称为容器文件。也就是说,一旦容器生成,就会同时存在两个文件: image 文件和容器文件。而且关闭容器并不会删除容器文件,只是容器停止运行而已。
# 列出本机正在运行的容器
$ docker container ls

# 列出本机所有容器,包括终止运行的容器
$ docker container ls --all

上面命令的输出结果之中,包括容器的 ID。很多地方都需要提供这个 ID,比如上一节终止容器运行的 docker container kill 命令。

终止运行的容器文件,依然会占据硬盘空间,可以使用 docker container rm 命令删除。
$ docker container rm [containerID] 

运行上面的命令之后,再使用 docker container ls --all 命令,就会发现被删除的容器文件已经消失了。

Dockerfile 文件

学会使用 image 文件以后,接下来的问题就是,如何可以生成 image 文件?如果你要推广自己的软件,势必要自己制作 image 文件。

这就需要用到 Dockerfile 文件。它是一个文本文件,用来配置 image。Docker 根据 该文件生成二进制的 image 文件。

下面通过一个实例,演示如何编写 Dockerfile 文件。

实例:制作自己的 Docker 容器

下面我以 koa-demos 项目为例,介绍怎么写 Dockerfile 文件,实现让用户在 Docker 容器里面运行 Koa 框架。

作为准备工作,请先 下载源码
$ git clone https://github.com/ruanyf/koa-demos.git
$ cd koa-demos

编写 Dockerfile 文件

首先,在项目的根目录下,新建一个文本文件 .dockerignore,写入下面的 内容
.git
node_modules
npm-debug.log

上面代码表示,这三个路径要排除,不要打包进入 image 文件。如果你没有路径要排除,这个文件可以不新建。

然后,在项目的根目录下,新建一个文本文件 Dockerfile,写入下面的 内容
FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000

上面代码一共五行,含义如下。
  • FROM node:8.4:该 image 文件继承官方的 node image,冒号表示标签,这里标签是 8.4,即 8.4 版本的 Node。
  • COPY . /app:将当前目录下的所有文件(除了 .dockerignore 排除的路径),都拷贝进入 image 文件的 /app 目录。
  • WORKDIR /app:指定接下来的工作路径为 /app
  • RUN npm install:在 /app 目录下,运行 npm install 命令安装依赖。注意,安装后所有的依赖,都将打包进入 image 文件。
  • EXPOSE 3000:将容器 3000 端口暴露出来, 允许外部连接这个端口。


创建 image 文件

有了 Dockerfile 文件以后,就可以使用 docker image build 命令创建 image 文件了。
$ docker image build -t koa-demo .
# 或者
$ docker image build -t koa-demo:0.0.1 .

上面代码中,-t 参数用来指定 image 文件的名字,后面还可以用冒号指定标签。如果不指定,默认的标签就是 latest。最后的那个点表示 Dockerfile 文件所在的路径,上例是当前路径,所以是一个点。

如果运行成功,就可以看到新生成的 image 文件 koa-demo 了。
$ docker image ls

生成容器

docker container run 命令会从 image 文件生成容器。
$ docker container run -p 8000:3000 -it koa-demo /bin/bash
# 或者
$ docker container run -p 8000:3000 -it koa-demo:0.0.1 /bin/bash

上面命令的各个参数含义如下:
  • -p 参数:容器的 3000 端口映射到本机的 8000 端口。
  • -it 参数:容器的 Shell 映射到当前的 Shell,然后你在本机窗口输入的命令,就会传入容器。
  • koa-demo:0.0.1:image 文件的名字(如果有标签,还需要提供标签,默认是 latest 标签)。
  • /bin/bash:容器启动以后,内部第一个执行的命令。这里是启动 Bash,保证用户可以使用 Shell。


如果一切正常,运行上面的命令以后,就会返回一个命令行提示符。
root@66d80f4aaf1e:/app#

这表示你已经在容器里面了,返回的提示符就是容器内部的 Shell 提示符。执行下面的命令。
root@66d80f4aaf1e:/app# node demos/01.js

这时,Koa 框架已经运行起来了。打开本机的浏览器,访问 http://127.0.0.1:8000,网页显示“Not Found”,这是因为这个 demo 没有写路由。

这个例子中,Node 进程运行在 Docker 容器的虚拟环境里面,进程接触到的文件系统和网络接口都是虚拟的,与本机的文件系统和网络接口是隔离的,因此需要定义容器与物理机的端口映射(map)。

现在,在容器的命令行,按下 Ctrl + c 停止 Node 进程,然后按下 Ctrl + d (或者输入 exit)退出容器。此外,也可以用 docker container kill 终止容器运行。
# 在本机的另一个终端窗口,查出容器的 ID
$ docker container ls

# 停止指定的容器运行
$ docker container kill [containerID] 

容器停止运行之后,并不会消失,用下面的命令删除容器文件。
# 查出容器的 ID
$ docker container ls --all

# 删除指定的容器文件
$ docker container rm [containerID] 

也可以使用 docker container run 命令的 --rm 参数,在容器终止运行后自动删除容器文件。
$ docker container run --rm -p 8000:3000 -it koa-demo /bin/bash

CMD 命令

上一节的例子里面,容器启动以后,需要手动输入命令 node demos/01.js。我们可以把这个命令写在 Dockerfile 里面,这样容器启动以后,这个命令就已经执行了,不用再手动输入了。
FROM node:8.4
COPY . /app
WORKDIR /app
RUN npm install --registry=https://registry.npm.taobao.org
EXPOSE 3000
CMD node demos/01.js

上面的 Dockerfile 里面,多了最后一行 CMD node demos/01.js,它表示容器启动后自动执行 node demos/01.js

你可能会问,RUN 命令与 CMD 命令的区别在哪里?简单说,RUN 命令在 image 文件的构建阶段执行,执行结果都会打包进入 image 文件;CMD 命令则是在容器启动后执行。另外,一个 Dockerfile 可以包含多个 RUN 命令,但是只能有一个 CMD 命令。

注意,指定了 CMD 命令以后,docker container run 命令就不能附加命令了(比如前面的 /bin/bash),否则它会覆盖 CMD 命令。现在,启动容器可以使用下面的命令。
$ docker container run --rm -p 8000:3000 -it koa-demo:0.0.1

发布 image 文件

容器运行成功后,就确认了 image 文件的有效性。这时,我们就可以考虑把 image 文件分享到网上,让其他人使用。

首先,去 hub.docker.comcloud.docker.com 注册一个账户。然后,用下面的命令登录。
$ docker login

接着,为本地的 image 标注用户名和版本。
$ docker image tag [imageName] [username]/[repository]:[tag]
# 实例
$ docker image tag koa-demos:0.0.1 ruanyf/koa-demos:0.0.1

也可以不标注用户名,重新构建一下 image 文件。
$ docker image build -t [username]/[repository]:[tag] .

最后,发布 image 文件。
$ docker image push [username]/[repository]:[tag]

发布成功以后,登录 hub.docker.com,就可以看到已经发布的 image 文件。

其他有用的命令

Docker 的主要用法就是上面这些,此外还有几个命令,也非常有用。

docker container start

前面的 docker container run 命令是新建容器,每运行一次,就会新建一个容器。同样的命令运行两次,就会生成两个一模一样的容器文件。如果希望重复使用容器,就要使用 docker container start 命令,它用来启动已经生成、已经停止运行的容器文件。
$ docker container start [containerID] 

docker container stop

前面的 docker container kill 命令终止容器运行,相当于向容器里面的主进程发出 SIGKILL 信号。而 docker container stop 命令也是用来终止容器运行,相当于向容器里面的主进程发出 SIGTERM 信号,然后过一段时间再发出 SIGKILL 信号。
$ docker container stop [containerID] 

这两个信号的差别是,应用程序收到 SIGTERM 信号以后,可以自行进行收尾清理工作,但也可以不理会这个信号。如果收到 SIGKILL 信号,就会强行立即终止,那些正在进行中的操作会全部丢失。

docker container logs

docker container logs 命令用来查看 Docker 容器的输出,即容器里面 Shell 的标准输出。如果 docker run 命令运行容器的时候,没有使用 -it 参数,就要用这个命令查看输出。
$ docker container logs [containerID] 

docker container exec

docker container exec 命令用于进入一个正在运行的 Docker 容器。如果 docker run 命令运行容器的时候,没有使用 -it 参数,就要用这个命令进入容器。一旦进入了容器,就可以在容器的 Shell 执行命令了。
$ docker container exec -it [containerID] /bin/bash

docker container cp

docker container cp 命令用于从正在运行的 Docker 容器里面,将文件拷贝到本机。下面是拷贝到当前目录的写法。
$ docker container cp [containID]:[/path/to/file] . 

Docker 是一个容器工具,提供虚拟环境。很多人认为,它改变了我们对软件的认识。

站在 Docker 的角度,软件就是容器的组合:业务逻辑容器、数据库容器、储存容器、队列容器......Docker 使得软件可以拆分成若干个标准化容器,然后像搭积木一样组合起来。
1.png

这正是微服务(microservices)的思想:软件把任务外包出去,让各种外部服务完成这些任务,软件本身只是底层服务的调度中心和组装层。
2.jpg

微服务很适合用 Docker 容器实现,每个容器承载一个服务。一台计算机同时运行多个容器,从而就能很轻松地模拟出复杂的微服务架构。
3.png

之前的内容介绍了 Docker 的概念和基本用法,接着往下介绍,如何在一台计算机上实现多个服务,让它们互相配合,组合出一个应用程序。
4.png

我选择的示例软件是 WordPress。它是一个常用软件,全世界用户据说超过几千万。同时它又非常简单,只要两个容器就够了(业务容器 + 数据库容器),很适合教学。而且,这种"业务 + 数据库"的容器架构,具有通用性,许多应用程序都可以复用。

为了加深读者理解,本文采用三种方法,演示如何架设 WordPress 网站。
  • 方法 A:自建 WordPress 容器
  • 方法 B:采用官方的 WordPress 容器
  • 方法 C:采用 Docker Compose 工具


预备工作:image 仓库的镜像网址

本教程需要从仓库下载 image 文件,但是国内访问 Docker 的官方仓库很慢,还经常断线,所以要把仓库网址改成国内的镜像站。这里推荐使用官方镜像 registry.docker-cn.com 。下面是我的 Debian 系统的默认仓库修改方法,其他系统的修改方法参考官方文档

打开 /etc/default/docker 文件(需要 sudo 权限),在文件的底部加上一行。
DOCKER_OPTS="--registry-mirror=https://registry.docker-cn.com"

然后,重启 Docker 服务。
$ sudo service docker restart

现在就会自动从镜像仓库下载 image 文件了。

方法 A:自建 WordPress 容器

前面说过,本文会用三种方法演示 WordPress 的安装。第一种方法就是自建 WordPress 容器。

官方的 PHP image

首先,新建一个工作目录,并进入该目录。
$ mkdir docker-demo && cd docker-demo

然后,执行下面的命令。
$ docker container run \
--rm \
--name wordpress \
--volume "$PWD/":/var/www/html \
php:5.6-apache

上面的命令基于 php 的 image 文件新建一个容器,并且运行该容器。php 的标签是 5.6-apache,说明装的是 PHP 5.6,并且自带 Apache 服务器。该命令的三个参数含义如下。
  • --rm:停止运行后,自动删除容器文件。
  • --name wordpress:容器的名字叫做 wordpress
  • --volume &quot;$PWD/&quot;:/var/www/html:将当前目录($PWD)映射到容器的 /var/www/html(Apache 对外访问的默认目录)。因此,当前目录的任何修改,都会反映到容器里面,进而被外部访问到。


运行上面的命令以后,如果一切正常,命令行会提示容器对外的 IP 地址,请记下这个地址,我们要用它来访问容器。我分配到的 IP 地址是 172.17.0.2。

打开浏览器,访问 172.17.0.2,你会看到下面的提示。
Forbidden
You don't have permission to access / on this server.

这是因为容器的 /var/www/html 目录(也就是本机的 docker-demo 目录)下面什么也没有,无法提供可以访问的内容。

请在本机的 docker-demo 目录下面,添加一个最简单的 PHP 文件 index.php
<?php 
phpinfo();
?>

保存以后,浏览器刷新 172.17.0.2,应该就会看到熟悉的 phpinfo 页面了。
5.jpeg

拷贝 WordPress 安装包

既然本地的 docker-demo 目录可以映射到容器里面,那么把 WordPress 安装包拷贝到 docker-demo 目录下,不就可以通过容器访问到 WordPress 的安装界面了吗?

首先,在 docker-demo 目录下,执行下面的命令,抓取并解压 WordPress 安装包。
$ wget https://cn.wordpress.org/wordpress-4.9.4-zh_CN.tar.gz
$ tar -xvf wordpress-4.9.4-zh_CN.tar.gz

解压以后,WordPress 的安装文件会在 docker-demo/wordpress 目录下。

这时浏览器访问 http://172.17.0.2/wordpress,就能看到 WordPress 的安装提示了。
6.png

官方的 MySQL 容器

WordPress 必须有数据库才能安装,所以必须新建 MySQL 容器。

打开一个新的命令行窗口,执行下面的命令。
$ docker container run \
-d \
--rm \
--name wordpressdb \
--env MYSQL_ROOT_PASSWORD=123456 \
--env MYSQL_DATABASE=wordpress \
mysql:5.7

上面的命令会基于 MySQL 的 image 文件(5.7版本)新建一个容器。该命令的五个命令行参数的含义如下。
  • -d:容器启动后,在后台运行。
  • --rm:容器终止运行后,自动删除容器文件。
  • --name wordpressdb:容器的名字叫做 wordpressdb
  • --env MYSQL_ROOT_PASSWORD=123456:向容器进程传入一个环境变量 MYSQL_ROOT_PASSWORD,该变量会被用作 MySQL 的根密码。
  • --env MYSQL_DATABASE=wordpress:向容器进程传入一个环境变量 MYSQL_DATABASE,容器里面的 MySQL 会根据该变量创建一个同名数据库(本例是 WordPress)。


运行上面的命令以后,正常情况下,命令行会显示一行字符串,这是容器的 ID,表示已经新建成功了。

这时,使用下面的命令查看正在运行的容器,你应该看到 wordpresswordpressdb 两个容器正在运行。
$ docker container ls

其中,wordpressdb 是后台运行的,前台看不见它的输出,必须使用下面的命令查看。
$ docker container logs wordpressdb

定制 PHP 容器

现在 WordPress 容器和 MySQL 容器都已经有了。接下来,要把 WordPress 容器连接到 MySQL 容器了。但是,PHP 的官方 image 不带有 mysql 扩展,必须自己新建 image 文件。

首先,停掉 WordPress 容器。
$ docker container stop wordpress

停掉以后,由于 --rm 参数的作用,该容器文件会被自动删除。

然后,在 docker-demo 目录里面,新建一个 Dockerfile 文件,写入下面的内容。
FROM php:5.6-apache
RUN docker-php-ext-install mysqli
CMD apache2-foreground

上面代码的意思,就是在原来 PHP 的 image 基础上,安装 mysqli 的扩展。然后,启动 Apache。

基于这个 Dockerfile 文件,新建一个名为 phpwithmysql 的 image 文件。
$ docker build -t phpwithmysql .

Wordpress 容器连接 MySQL

现在基于 phpwithmysql image,重新新建一个 WordPress 容器。
$ docker container run \
--rm \
--name wordpress \
--volume "$PWD/":/var/www/html \
--link wordpressdb:mysql \
phpwithmysql

跟上一次相比,上面的命令多了一个参数 --link wordpressdb:mysql,表示 WordPress 容器要连到 wordpressdb 容器,冒号表示该容器的别名是 mysql

这时还要改一下wordpress目录的权限,让容器可以将配置信息写入这个目录(容器内部写入的/var/www/html目录,会映射到这个目录)。
$ chmod -R 777 wordpress

接着,回到浏览器的 http://172.17.0.2/wordpress 页面,点击“现在就开始!”按钮,开始安装。

WordPress 提示要输入数据库参数。输入的参数如下。
7.png

  • 数据库名:wordpress
  • 用户名:root
  • 密码:123456
  • 数据库主机:mysql
  • 表前缀:wp_(不变)


点击“下一步”按钮,如果 Wordpress 连接数据库成功,就会出现下面的页面,这就表示可以安装了。
8.png

至此,自建 WordPress 容器的演示完毕,可以把正在运行的两个容器关闭了(容器文件会自动删除)。
$ docker container stop wordpress wordpressdb

方法 B:Wordpress 官方镜像

上一部分的自建 WordPress 容器,还是挺麻烦的。其实不用这么麻烦,Docker 已经提供了官方 WordPress image,直接用那个就可以了。有了上一部分的基础,下面的操作就很容易理解了。

基本用法

首先,新建并启动 MySQL 容器。
$ docker container run \
-d \
--rm \
--name wordpressdb \
--env MYSQL_ROOT_PASSWORD=123456 \
--env MYSQL_DATABASE=wordpress \
mysql:5.7

然后,基于官方的 WordPress image,新建并启动 WordPress 容器。
$ docker container run \
-d \
--rm \
--name wordpress \
--env WORDPRESS_DB_PASSWORD=123456 \
--link wordpressdb:mysql \
wordpress

上面命令中,各个参数的含义前面都解释过了,其中环境变量 WORDPRESS_DB_PASSWORD 是 MySQL 容器的根密码。

上面命令指定 wordpress 容器在后台运行,导致前台看不见输出,使用下面的命令查出 wordpress 容器的 IP 地址。
$ docker container inspect wordpress

上面命令运行以后,会输出很多内容,找到 IPAddress 字段即可。我的机器返回的 IP 地址是172.17.0.3

浏览器访问172.17.0.3,就会看到 WordPress 的安装提示。
9.png

WordPress 容器的定制

到了上一步,官方 WordPress 容器的安装就已经成功了。但是,这种方法有两个很不方便的地方。
  • 每次新建容器,返回的 IP 地址不能保证相同,导致要更换 IP 地址访问 WordPress。
  • WordPress 安装在容器里面,本地无法修改文件。


解决这两个问题很容易,只要新建容器的时候,加两个命令行参数就可以了。

先把刚才启动的 WordPress 容器终止(容器文件会自动删除)。
$ docker container stop wordpress

然后,使用下面的命令新建并启动 WordPress 容器。
$ docker container run \
-d \
-p 127.0.0.2:8080:80 \
--rm \
--name wordpress \
--env WORDPRESS_DB_PASSWORD=123456 \
--link wordpressdb:mysql \
--volume "$PWD/wordpress":/var/www/html \
wordpress

上面的命令跟前面相比,命令行参数只多出了两个。
  • -p 127.0.0.2:8080:80:将容器的 80 端口映射到 127.0.0.28080 端口。
  • --volume &quot;$PWD/wordpress&quot;:/var/www/html:将容器的 /var/www/html 目录映射到当前目录的 wordpress 子目录。


浏览器访问 127.0.0.2:8080:80 就能看到 WordPress 的安装提示了。而且,你在 wordpress 子目录下的每次修改,都会反映到容器里面。

最后,终止这两个容器(容器文件会自动删除)。
$ docker container stop wordpress wordpressdb

方法 C:Docker Compose 工具

上面的方法 B 已经挺简单了,但是必须自己分别启动两个容器,启动的时候,还要在命令行提供容器之间的连接信息。因此,Docker 提供了一种更简单的方法,来管理多个容器的联动。

Docker Compose 简介

10.jpeg

Compose 是 Docker 公司推出的一个工具软件,可以管理多个 Docker 容器组成一个应用。你需要定义一个 YAML 格式的配置文件 docker-compose.yml,写好多个容器之间的调用关系。然后,只要一个命令,就能同时启动/关闭这些容器。
# 启动所有服务
$ docker-compose up
# 关闭所有服务
$ docker-compose stop

Docker Compose 的安装

Mac 和 Windows 在安装 Docker 的时候,会一起安装 docker compose。Linux 系统下的安装参考官方文档

安装完成后,运行下面的命令。
$ docker-compose --version

WordPress 示例

docker-demo 目录下,新建 docker-compose.yml 文件,写入下面的内容。
mysql:
image: mysql:5.7
environment:
 - MYSQL_ROOT_PASSWORD=123456
 - MYSQL_DATABASE=wordpress
web:
image: wordpress
links:
 - mysql
environment:
 - WORDPRESS_DB_PASSWORD=123456
ports:
 - "127.0.0.3:8080:80"
working_dir: /var/www/html
volumes:
 - wordpress:/var/www/html

上面代码中,两个顶层标签表示有两个容器 mysqlweb。每个容器的具体设置,前面都已经讲解过了,还是挺容易理解的。

启动两个容器。
$ docker-compose up

浏览器访问 http://127.0.0.3:8080,应该就能看到 WordPress 的安装界面。

现在关闭两个容器。
$ docker-compose stop

关闭以后,这两个容器文件还是存在的,写在里面的数据不会丢失。下次启动的时候,还可以复用。下面的命令可以把这两个容器文件删除(容器必须已经停止运行)。
$ docker-compose rm

原文链接:
收起阅读 »