原文:
zh.annas-archive.org/md5/a4bac036332585d145cfa15fc921b7c0
译者:飞龙
前言
在这本书被写作的时候,世界因为一个世纪以来最大的大流行病而发生了巨大变化。然而,随着软件行业采用 DevOps 和云原生开发来处理加速的软件交付速度,本书的价值也变得前所未有。
我们按照生命周期、复杂性和成熟度的增量顺序组织了本书的主题。但是,DevOps 是一个足够广泛的旅程,您可能会发现某些章节对您的项目需求更为相关。因此,我们设计了章节,以便您可以任意顺序开始,并专注于您需要专业知识、示例和最佳实践的特定主题,以提升您的知识水平。
我们希望您享受阅读本书的乐趣,就像我们享受整理内容一样。我们唯一的要求是与朋友或同事分享您新发现的知识,以便我们所有人都能成为更好的开发者。
本书中使用的约定
本书使用以下排版约定:
斜体
表示新术语、URL、电子邮件地址、文件名和文件扩展名。
常量宽度
用于程序列表,以及在段落中引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。
常量宽度粗体
显示用户需要按照字面意思键入的命令或其他文本。
常量宽度斜体
显示应由用户提供值或根据上下文确定值替换的文本。
Tip
这个元素表示一个提示或建议。
注意
这个元素表示一般注意事项。
警告
这个元素表示警告或注意事项。
使用代码示例
补充材料(代码示例、练习等)可以从https://github.com/devops-tools-for-java-developers下载。
如果您有技术问题或在使用示例代码时遇到问题,请发送电子邮件至bookquestions@oreilly.com。
本书的目的是帮助您完成工作。一般来说,如果本书提供示例代码,您可以在自己的程序和文档中使用它们。除非您复制了大量代码,否则无需联系我们以获得许可。例如,编写一个使用本书多个代码片段的程序不需要许可。销售或分发 O’Reilly 图书示例需要许可。通过引用本书并引用示例代码来回答问题不需要许可。将本书大量示例代码合并到您产品的文档中需要许可。
我们感激您的认可,尽管通常并不要求署名。署名通常包括书名、作者、出版商和 ISBN。例如:“DevOps Tools for Java Developers by Stephen Chin, Melissa McKay, Ixchel Ruiz, and Baruch Sadogursky (O’Reilly)。版权 2022 Stephen Chin, Melissa McKay, Ixchel Ruiz, and Baruch Sadogursky,978-1-492-08402-0。”
如果您认为您对代码示例的使用超出了公平使用或上述许可,请随时通过 permissions@oreilly.com 与我们联系。
致谢
没有我们家人和朋友的支持,这本书将无法完成。当我们全情投入地编写这本书时,他们帮助照顾我们所爱的人和个人健康,以便我们能够提供涵盖整个 DevOps 和 Java 生态系统所需的广泛内容。
还要特别感谢 JFrog 的首席执行官 Shlomi Ben Haim。当其他公司收缩并紧缩预算时,Shlomi 个人支持了这本书,并赋予了我们专注于涵盖这一极其广泛主题的紧张任务的自由和灵活性。
特别感谢 Ana-Maria Mihalceanu,对第八章的贡献,以及 Sven Ruppert,对第七章的贡献。
我们要感谢我们的技术审阅者 Daniel Pittman,Cameron Pietrafeso,Sebastian Daschner 和 Kirk Pepperdine,为提升本书的准确性做出贡献。
最后,特别感谢您,读者,您已经采取了主动行动,提升了整个 DevOps 流水线的知识,并成为自动化、流程和文化改进的推动者。
第一章:DevOps 针对(或可能反对)开发人员
巴鲁克·萨多古斯基
当你在这里打呼噜时,
睁眼的阴谋占据他的时间。
如果你关心生活,
摆脱沉睡,小心:
醒来,醒来!
威廉·莎士比亚,《暴风雨》
有些人可能会问,DevOps 运动是否只是针对开发人员的运维方面的阴谋。大多数人(如果不是全部的话)这样做并不期望得到严肃的回答,至少因为他们把这个问题当作开玩笑。这也是因为——不管你的起源是开发还是运维方面——当有人谈论 DevOps 时,大约 60 秒后会有人询问,“但是 DevOps 真的是什么?”
而且你会想,自从这个术语被创造出来以来的 11 年时间内(在这个期间,行业专业人员已经讨论、辩论和喧嚷过它),我们都应该已经达成了一个标准、毫无废话、共同理解的定义。但事实并非如此。事实上,尽管企业对 DevOps 人员的需求呈指数级增长,但几乎可以肯定地说,随机选择的五个 DevOps 职称的员工中,没有一个人能精确地告诉你 DevOps 是什么。
因此,如果在谈到这个话题时你仍然感到困惑,不要感到尴尬。概念上,DevOps 可能不容易理解,但也不是不可能。
但无论我们如何讨论这个术语或者我们可能同意的定义是什么,有一件事,比其他一切都更重要,那就是必须牢记:DevOps 是一个完全虚构的概念,而发明者来自运维方面。
DevOps 是由运维方面创造的概念
我关于 DevOps 的假设可能会引起争议,但也可以证明。让我们从事实开始。
陈列品 1:《凤凰项目》
《凤凰项目》 由吉恩·金等人(IT 革命)出版近十年来成为经典之作。这不是一本传统意义上的操作手册。这是一部小说,讲述了一个问题重重的公司及其突然被分配任务的 IT 经理,要实施一个已经超出预算和拖延了数月的至关重要的公司倡议的故事。
如果你生活在软件领域,那么这本书的其他主要人物对你来说应该很熟悉。不过,现在让我们来看看他们的专业头衔:
-
IT 服务支持总监
-
分布式技术总监
-
零售销售经理
-
主系统管理员
-
首席信息安全官
-
首席财务官
-
首席执行官
注意它们之间的联系了吗?他们是有史以来关于 DevOps 最重要的书籍之一的主人公,而且其中一个也不是开发人员。即使开发人员确实出现在情节中,嗯…我们只能说他们的表现并不是特别抢眼。
当胜利到来时,故事的英雄(连同一个支持的董事成员)发明了 DevOps,拯救了项目的失败,扭转了公司的命运,并被提升为企业的首席信息官(CIO)。每个人都过得很幸福——即使不是永远,至少是在这种成功通常会给你带来两三年时间之前,重新证明自己的价值之前。
陈列 2:《DevOps 手册》
最好在 Gene Kim 等人的《DevOps 手册》(IT Revolution)之前阅读《凤凰项目》,因为前者可以将你置于一个高度真实的、人性化的场景中。你可以轻易地沉浸在人物类型、专业困境和人际关系中。DevOps 的实现方式和原因会像是对一系列情况的必然和理性回应,这些情况本可以导致业务崩溃。故事中的赌局、人物和他们所做的选择似乎都非常合理。可能很容易与你自己的经验进行类比。
《DevOps 手册》允许你更深入地探索 DevOps 原则和实践的各个概念部分。正如其副标题所示,这本书在解释“如何在技术组织中创建世界一流的灵活性、可靠性和安全性”方面走得很远。但这是否应该是关于开发的?是否应该或不应该可能存在争议。无可辩驳的是,这本书的作者们都是聪明、超级有才华的专业人士,可以说是 DevOps 的奠基人。然而,陈列 2 并非在这里赞扬他们,而是要仔细审视他们的背景。
让我们从吉恩·金(Gene Kim)开始。他创立了软件安全和数据完整性公司 Tripwire,并担任首席技术官(CTO)长达十多年。作为一名研究者,他致力于研究和理解大型复杂企业和机构中正在发生和已经发生的技术变革。除了合著《凤凰项目》外,他还于 2019 年合著了《独角兽项目》(稍后我会详细说说)。他的整个职业生涯都深深植根于运维。即使《独角兽》说它是“关于开发者”,它仍然是从运维人员的角度来看待开发者!
至于《手册》的其他三位作者:
-
杰兹·汉布尔(Jez Humble)曾担任过包括站点可靠性工程师(SRE)、首席技术官(CTO)、交付架构和基础设施服务副主任以及开发者关系的职位。一个运维人员!尽管他的最后一个头衔提到开发,工作并不是关于那个。它是关于与开发者的关系。它是关于缩小开发与运维之间的鸿沟,关于这些他已经广泛地写作、教学和演讲。
-
帕特里克·德博伊斯曾担任首席技术官、市场战略总监和 Dev♥Ops 关系总监(心形是他添加的)。他将自己描述为一个专业人士,通过在开发、项目管理和系统管理中使用敏捷技术来“弥合项目和运维之间的鸿沟”。这确实听起来像一个运维人员。
-
截至目前为止,约翰·威利斯担任 DevOps 和数字实践副总裁。此前,他曾担任生态系统开发总监、解决方案副总裁,特别是在 Opscode(现在被称为 Progress Chef)担任过培训与服务副总裁。尽管约翰的职业生涯更深入地涉及开发,但他的大部分工作都与运维有关,特别是他将注意力集中在打破曾经将开发人员和运维人员分为独立阵营的壁垒上。
正如你所看到的,所有的作者都有运维背景。巧合吗?我认为 不是。
还不确定 DevOps 是由运维驱动的?那我们来看看今天试图向我们推销 DevOps 的领导人吧。
谷歌搜索
截至目前为止,如果你在谷歌搜索中键入“什么是 DevOps?”只是想看看会出现什么,你的第一页结果很可能包括以下内容:
-
敏捷管理,一个系统管理公司
-
Atlassian,其产品包括项目和问题跟踪、列表制作以及团队协作平台
-
亚马逊网络服务(AWS)、微软 Azure 和 Rackspace Technology,它们都销售云运维基础设施
-
Logz.io,销售日志管理和分析服务
-
New Relic,其专业是应用程序监控
所有这些都非常关注运维。是的,第一页包含了一个稍微偏向开发的公司和另一个与搜索无直接关系的公司。重点是,当你试图寻找 DevOps 时,大部分内容都倾向于运维。
它是做什么的?
DevOps 是 一种事物!它 非常 受欢迎。因此,许多人会想要明确知道,DevOps 做 什么,它实质性地产生了什么。而不是深入探讨这一点,让我们从结构上看待它,将其概念化,就像你会看待侧向的、八字形的无限符号一样。在这种光线下,我们看到一个从编码到构建再到测试再到发布再到部署再到运维再到监控的流程循环,然后再回到开始计划新功能的过程,如 图 1-1 所示。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0101.png
图 1-1. DevOps 无限循环
如果这对某些读者看起来很熟悉,那是因为它与敏捷开发周期有概念上的相似性(图 1-2)。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0102.png
图 1-2. 敏捷开发周期
这两个永无止境的故事没有根本的差异,除了运维人员将自己嫁接到敏捷圈的旧世界之外,本质上将其分成两个圈子,并将其关注和痛苦塞入曾被认为只属于开发人员的领域。
行业现状
自 2014 年以来,进一步证明 DevOps 是一种由运维驱动的现象,已经打包成了一份易于阅读的年度摘要,其中包含了全球数万名行业专业人士和组织收集、分析和总结的数据。《加速:DevOps 现状》报告主要由 DevOps 研究与评估(DORA)完成,是软件行业中最重要的文献,用来衡量 DevOps 现状及其未来发展方向。例如,在2018 年版中,我们可以看到对以下问题的严肃关注:
-
组织多频繁部署代码?
-
从代码提交到成功运行生产环境通常需要多长时间?
-
发生故障或停机时,通常需要多长时间恢复服务?
-
部署变更的百分比导致服务降级或需要补救?
请注意,所有这些都是非常侧重于运维的关注点。
什么构成工作?
现在让我们来看一下《加速:DevOps 现状》报告和《凤凰项目》如何定义工作。首先,计划的工作侧重于业务项目和新功能,涵盖了运维和开发。内部项目包括服务器迁移、软件更新以及对已部署项目反馈驱动的变更可能是广泛的,并且可能或可能不会更多地倾向于 DevOps 等式的一侧。
那么,像支持升级和紧急停机这样的非计划活动呢?这些都非常侧重于运维,就像编写新功能、修复错误和重构一样——这一切都是如何通过将开发人员包括在 DevOps 故事中来使运维生活变得更轻松。
如果我们的工作不是部署和运维,那么我们的工作是什么?
显然,DevOps 并不是开发人员(或者曾经)要求的任何东西。这是一种运维发明,目的是让其他人工作更加努力。假设这是真的,让我们思考一下,如果开发人员能够团结一致地说:“你们的运维问题是你们的,而不是我们的。”好吧。但是在这种情况下,询问反抗的开发人员他们对“完成”的定义将是完全正确和合理的。他们认为他们需要达到什么标准才能说:“我们的工作做得很好,我们的部分现在已经完成”?
这并不是一个轻率的问题,我们可以找到答案的来源。其中之一,尽管并不完美且经常受到批评,是软件工艺宣言,提出了四个应激发开发人员的基本价值观。让我们来考虑一下:
精心打造的软件
是的,质量确实很重要。
持续增加价值
没有异议。当然,我们希望提供人们需要、想要或愿意的服务和功能。
专业人士的社区
从宏观角度来看,谁会反对呢?在行业同行之间的友好相处只是职业上的邻里之间的亲切关系。
富有成效的合作伙伴关系
合作肯定是游戏的名字。开发人员并不反对质量保证(QA)、运维或产品本身。因此,在这种情况下,这只是与每个人友好相处的问题(只要其他团队不开始指定他们的工作职责)。
到底什么算是“完成”?
借助我们迄今为止建立的一切,我们可以安全地说,我们需要编写简单、可读、易理解和易部署的代码。我们必须确保满足非功能性需求(例如性能、吞吐量、内存占用、安全性、隐私等)。我们应该努力避免产生任何技术负担,并且如果幸运的话,沿途还能减轻一些负担。我们必须确保所有测试都通过。而且我们有责任与质量保证团队保持良好的关系(当他们满意时,我们也会很开心)。
有了高质量的代码,再加上积极的团队领导和同行评审,一切都应该顺利。通过产品团队为价值和附加值定义标准,可以牢固地建立基准。通过他们的反馈,产品所有者帮助确定这些基准是否(或是否未能)得到满足,以及到了什么程度。这是一个非常好的简略定义,说明了一个优秀的软件开发人员完成了他们需要做的事情。它还表明,“做得好”如果没有与运营人员的参与和清晰的沟通,是无法充分衡量(甚至无法知晓)的。
竞争?
所以是的,尽管可以证明 DevOps 真的不是开发人员迫切需要的东西,但同样可以证明它的无限实践对每个人都有好处。但仍然存在一些顽固分子;那些想象开发人员和例如质量保证(QA)测试人员之间存在竞争甚至敌意的人。开发人员努力工作来创建他们的作品,然后感觉 QA 团队几乎像是黑客,试图证明某些问题,不停地深挖。
这就是 DevOps 咨询的用武之地。每个尽责的开发者都希望为自己的工作感到自豪。发现缺陷可能看起来像是批评,但实际上只是来自另一个方向的尽责工作。开发人员和质量保证人员之间良好、清晰、开放和持续的沟通有助于强化 DevOps 的好处,但也清楚地表明每个人最终都在为同一个目标而努力。当质量保证人员发现缺陷时,他们所做的只是帮助他们的开发人员同事编写更好的代码,成为更好的开发人员。这种运作方式展示了运维方面与开发方面之间相互作用的例子,显示了这两个世界之间的区别和分离之间的有用模糊。他们的关系必然是共生的,并且再次沿着无穷无尽的活动连续体工作,一个方面为了所有人的共同利益而通知另一个方面。
前所未有的情况
对 DevOps 的需求增长不仅来自于软件公司内部,也同样来自于外部力量。这是因为我们的期望,所有我们的期望,作为生活在 21 世纪世界中的人,继续迅速变化。我们对不断改进的软件解决方案越来越依赖,我们就越没有时间浪费在信息和沟通差距以及开发人员与运营人员之间的延迟上。
以银行为例。十年前,大多数主要银行都有相当合适的网站。你可以登录查看你的账户、你的对账单和最近的交易。也许你甚至开始通过银行提供的电子服务进行电子支付。尽管这些服务很好,提供了一定程度的便利,但你可能仍然需要去(或者至少感觉更舒服去)你的当地分行处理银行事务。
今天的完全数字化体验是以前所没有的—配有移动应用程序、自动账户监控和警报,以及足够的服务,使得普通账户持有人在线上完成所有事情变得越来越普遍。你甚至可能是那些不仅不在乎是否再次进入实体分行,甚至不知道那个分行在哪里的人之一!而且,银行正通过整合和关闭实体分行,并为客户转移到在线领域提供激励措施,以应对这些迅速变化的银行习惯。这在 COVID-19 危机期间加速进行,当时分行的访问仅限预约服务、有限的步行进入以及更短的营业时间。
因此,如果您的银行网站在银行部署更好、更安全的网站时停机维护了 12 个小时,那么 10 年前您可能会轻松接受这一情况。如果这将带来更高质量的服务,那么 12 小时算什么呢?您并不需要全天 24/7 的在线银行服务,而且当地的分行也可以为您提供服务。然而,今天的情况已经完全不同了。半天的停机时间是不可接受的。实际上,您希望您的银行始终开放和可用。这是因为您(以及全世界)对质量的定义已经发生了变化。这种变化使得 DevOps 比以往任何时候都更为重要。
容量和速度
推动 DevOps 增长的另一个压力是正在存储和处理的数据量。这是完全合理的。如果我们日常生活中越来越多的依赖软件,那么它产生的数据量显然会大幅增加。到 2020 年,全球数据领域总量接近 10 泽字节。十年前,这个数字是 0.5 泽字节。预计到 2025 年,据合理估计,这个数字将以指数方式增长至超过 50 泽字节!
这不仅仅是像谷歌、Netflix、Facebook、Microsoft、Amazon、Twitter 等巨头变得越来越强大,因此需要处理更多的数据。这一预测确认了越来越多的公司将进入大数据的世界。随之而来的是对大量增加的数据负载的需求,以及远离提供给定生产环境精确副本的传统分阶段服务器环境。而这种转变是基于这样一个事实:维护这种一对一方案在规模或速度方面已不再可行。
乐观的日子已经一去不复返,以往一切都可以在投入生产之前进行测试,但现在不再可能。有些软件公司会发布一些他们并不完全信任的东西进入生产环境。这会导致我们恐慌吗?不会。需要快速发布并保持竞争力的必要性应该激发创新和创造力,以最佳方式执行受控的转换、测试程序以及更多的在-生产测试,现在被称为渐进式交付。这伴随着特性标志和可观察性工具,如分布式跟踪。
一些人把渐进式交付与爆炸装置的爆炸半径等同起来。这个想法是,当我们部署到生产环境时,爆炸是可以预料到的。因此,为了优化这样的部署,我们能期望的最好结果就是尽量减少伤亡,尽量减小爆炸半径的大小。通过改进服务器、服务和产品的质量来始终如一地实现这一点。如果我们认同质量是开发者关注的问题,并且其实现是开发者定义中“完成”的一部分,那么这意味着在开发者“完成”的那一刻和下一个运维生产的那一刻之间不可能有暂停或断开的时刻。因为这一切发生的时间不会超过我们重新回到开发阶段,就像修复了错误,由于故障而恢复了服务等等。
完成和完成
或许很明显的是,从运维环境中产生并继续生成的期望和需求的事实,驱动了推动到 DevOps。因此,开发者面临的期望和需求的增加,并不是来自运维人员对开发者同事的某种腐烂仇恨,也不是一种剥夺他们睡眠的阴谋。相反,所有这一切,DevOps 所代表的一切,都是对我们变化世界的实际政治业务响应,以及它们在软件行业整体上所强加的变化的回应。
事实是每个人都有新的责任,其中一些责任需要专业人士(当然是许多部门),随时准备回应任何时候的责任,因为我们的世界是不停止的。这里有另一种说法:我们对“完成”的旧定义已经完成了!
我们的新定义是站点可靠性工程(SRE)。这个由 Google 创造的术语通过弥合任何可能存在的两者之间的悬殊感,永久将开发与运维结合在一起。虽然 SRE 的关注领域可能被开发人员或运维人员的人员所占据,但这些天,公司通常有专门的 SRE 团队,专门负责检查与性能、效率、应急响应、监控、容量规划等相关的问题。SRE 专业人员像软件工程师一样思考,为系统管理问题制定策略和解决方案。他们是越来越多地使自动化部署工作的人。
当 SRE 人员感到满意时,这意味着构建变得更加可靠、可重复和快速,特别是因为现在的情况是在无状态环境中运行的可扩展、向后和向前兼容代码,在一个不断扩展的服务器宇宙中发出事件流,以实现实时可观察性和在出现问题时发出警报。当新的构建发生时,它们需要快速启动(并且预计同样快速地死亡)。服务需要尽快恢复到完全功能状态。当功能失效时,我们必须通过 API 具有即时关闭它们的能力。当发布新软件并更新其客户端用户时,但遇到错误时,我们必须具有执行快速且无缝回滚的能力。旧客户端和旧服务器需要能够与新客户端进行通信。
当 SRE 评估和监控这些活动并制定战略响应时,所有这些领域的工作完全由开发人员来完成。因此,虽然开发人员正在执行,SRE 今天定义了完成。
浮动如蝴蝶…
除了已提到的所有考虑因素之外,我们现代 DevOps(以及相关的 SRE)时代必须定义代码的一个基本特征是精益。而这里我们指的是节约成本。你可能会问,“但是,代码与节约成本有什么关系呢?”
嗯,一个例子可能是云服务提供商向公司收费的种种离散服务。这些成本中有一部分直接受到那些企业云服务订阅者输出的代码的影响。因此,成本的降低可以来自于创新开发工具的创建和使用,以及编写和部署更好的代码。
全球化、从不关闭的、由软件驱动的社会的本质,以及对更新、更好的软件功能和服务的持续需求,意味着 DevOps 不能只关注生产和部署。它必须还要关注业务本身的底线。尽管这可能看起来又是抛入混合中的另一个负担,但在老板说必须削减成本的下一次时,考虑一下这个。与其采取消极、膝跳反应的解决方案,比如裁员或减少工资和福利,需要的节约可能可以通过积极的、提高业务形象的举措来实现,比如转向无服务器架构和搬迁至云端。这样,没人会被解雇,休息室里的咖啡和甜甜圈依然是免费的!
精益不仅节省了金钱,还给公司提供了改善市场影响的机会。当公司能够在不裁员的情况下实现效率时,它们可以保持团队力量的最佳水平。当团队继续得到良好的报酬和关心时,他们会更有动力地提供他们可能的最佳工作。当该产出取得成功时,意味着客户很满意。只要客户继续因为更快的部署而获得良好运行的新功能,那么…他们会继续回来,并向他人传播这一消息。而这是一个良性循环,意味着银行里有钱。
完整性、认证和可用性
与任何和所有的 DevOps 活动一起,一个永恒的问题是安全性。当然,有些人会选择通过雇佣一位首席信息安全官来解决这个问题。这是很好的,因为当出了问题时总会有一个可以责怪的人。一个更好的选择可能是在一个 DevOps 框架内实际分析,个体员工、工作团队和整个公司如何思考安全性以及如何加强安全性。
我们将在第十章中更详细地讨论这个问题,但现在可以考虑一下:违规行为、错误、结构化查询语言(SQL)注入、缓冲区溢出等并不是新鲜事。不同的是它们出现的速度越来越快,数量越来越多,以及恶意个体和实体行动的聪明程度。这并不奇怪。随着越来越多的代码发布,越来越多的问题将随之而来,而每种问题都需要不同的解决方案。
随着部署速度的加快,对风险和威胁的反应变得愈发重要。2018 年发现的熔断和幽灵安全漏洞清楚地表明,有些威胁是无法预防的。我们在一场比赛中,唯一要做的就是尽快部署修复措施。
激烈的紧迫性
现在应该清楚了,DevOps 不是一个阴谋,而是对进化压力的回应。 这是一种手段,具有以下功能:
-
提供更好的质量
-
节省成本
-
更快地部署特性
-
加强安全性
无论谁喜欢与否,或者谁首先想出了这个想法,甚至它的原始意图都不重要。重要的是在下一节中涵盖的内容。
软件行业已完全拥抱 DevOps
到目前为止,每家公司都是一家 DevOps 公司。所以,加入进来吧…因为你别无选择。
如前所述,今天的 DevOps,DevOps 已经演变成的样子,是一个无限循环。这并不意味着团体和部门不再存在。这也不意味着每个人都要对自己关注的领域以及沿着这个连续体的每个人的领域负责。
确实意味着每个人都应该一起工作。确实意味着企业内的软件专业人员必须意识到并合理考虑所有其他同事正在做的工作。他们需要关心同行正面临的问题,这些问题如何以及会如何影响他们自己的工作,公司提供的产品和服务,以及这种整体性如何影响他们公司在市场上的声誉。
这就是为什么 DevOps 工程师 是一个毫无意义的术语,因为它暗示了存在可以全面和胜任地执行(或至少完全熟悉)DevOps 无限循环中发生的一切的人。这样的人不存在。他们永远也不会存在。事实上,甚至试图成为一个 DevOps 工程师也是一个错误,因为它完全违背了 DevOps 的本质,即消除代码开发人员与 QA 测试人员、发布人员等之间的隔离。
DevOps 是努力的汇聚,利益的结合和反馈,以不断创造、保障、部署和完善代码。DevOps 关乎协作。由于协作是有机的、沟通的努力,嗯……正如协作工程不是一回事一样,DevOps 工程也不是一回事(无论任何学院或大学可能承诺什么)。
使其具体化
知道 DevOps 是什么(以及不是什么)只是建立一个概念。问题是,如何在各个软件公司中明智有效地实施和持续发展它?最好的建议?看这里。
首先,你可以有 DevOps 推动者、DevOps 传道士、DevOps 顾问和教练(我知道 Scrum 已经糟糕透顶了所有这些术语,但没有更好的替代)。那没关系。但是 DevOps 不是一个工程学科。我们想要站点/服务可靠性工程师、生产工程师、基础设施工程师、QA 工程师等等。但一旦一家公司有了一个 DevOps 工程师,几乎可以保证接下来会有一个 DevOps 部门,这只会是另一个可能不过是重新包装的现有部门,以便看起来公司已经跟上了 DevOps 的浪潮。
DevOps 办公室并不是进步的标志。相反,它只是回到了未来。然后,接下来需要的是促进 Dev 和 DevOps 之间合作的方法,这将需要创造另一个术语。DevDevOps 怎么样?
其次,DevOps 关注细微之处和小事情。就像文化(特别是企业文化)一样,它关乎态度和关系。你可能无法明确定义这些文化,但它们依然存在。DevOps 也不仅仅是关于代码、工程实践或技术能力。没有你能购买的工具,没有逐步手册,也没有家庭版棋盘游戏可以帮助你在组织中创建 DevOps。
这关乎在公司中鼓励和培养的行为。而其中大部分只是关于如何对待普通员工,公司的结构以及人们担任的职位。关乎人们有多少机会聚在一起(特别是在非会议设置中),他们坐在哪里吃饭,交流工作和非工作内容,讲笑话等等。正是在这些地方而不是数据中心,文化形成、成长和改变。
最后,公司应该积极寻找并投资于 T 型人才(俄语读者可能建议的Ж型更好)。与专精于一领域的 I 型个体或对许多领域略知一二而无专业技能的全才相对,T 型人才在至少一方面拥有世界一流的专业知识。这是“T”形图中长垂直线的基础,坚定地根植于他们的深度知识和经验之中。这个“T”字上面横跨着在其他领域积累的广泛能力、专业知识和智慧。
总体来说,这样的人表现出了清晰而敏锐的适应环境、学习新技能和应对当今挑战的能力。事实上,这几乎是理想的 DevOps 员工的完美定义。T 型人才使企业能够有效地处理优先工作负载,而不仅仅是公司认为他们内部能力所能承受的。T 型人才能看到并对大局感兴趣。这使他们成为出色的合作伙伴,进而导致建立有权力的团队。
我们都收到了这个信息
好消息是,十年后,运维发明了 DevOps,他们完全明白这不仅仅是关于他们。这是关乎每个人。我们可以用自己的眼睛看到这种变化。例如,2019 年“加速:DevOps 现状”报告吸引了更多开发人员参与研究,而不是运维或 SRE 人员!要找到更深入的证据证明事情已经改变,我们回到了基因·金。同样在 2019 年,这位帮助将运维方程式搬上小说舞台的人发行了《独角兽项目》(IT 革命)。如果早期的书籍对开发人员短视,那么这里的英雄是麦克辛,她公司的首席开发人员(也是最终的救世主)。
DevOps 始于运维,毫无疑问。但其动机并非要压制开发人员,也不是运维专业人士的至高无上。它的起源和现在仍然是基于每个人都看到每个人,欣赏他们对企业价值和贡献的认识—这不仅仅是出于尊重或礼貌,而是出于个人自身利益以及企业的生存、竞争力和增长。
如果你担心 DevOps 会让你淹没在运维概念的海洋中,实际上情况可能恰恰相反。只需看看Google 定义的 SRE(这家公司发明了这一学科):
当你把运维视为软件问题时,就得到了 SRE。
所以,运维人员现在也想成为开发人员了?欢迎加入。所有软件专业人士随时都要面对软件问题。我们是解决问题的行业——这意味着每个人都有点像 SRE,有点像开发人员,有点涉及运维……因为这些都是相互交织的方面,使我们能够为当今的软件问题以及明天的个人和社会问题提出解决方案。
第二章:真相系统
斯蒂芬·钦
一个复杂的运行良好的系统,往往是从一个简单的运行良好的系统演变而来的。
约翰·高尔(高尔定律)
要构建有效的 DevOps 流水线,重要的是要有一个统一的真相系统,以了解哪些位和字节正在被部署到生产环境中。通常,这始于一个包含所有源代码的源代码管理系统,这些代码被编译和构建成生产部署。通过将生产部署追溯到源代码控制中的特定修订版本,您可以对错误、安全漏洞和性能问题进行根本原因分析。
源代码管理在软件交付生命周期中发挥了几个关键角色:
协作
大型团队在单个代码库上工作时,如果没有有效的源代码管理,将不断地被彼此阻塞,随着团队规模的增长,生产力将降低。
版本控制
源代码系统让您跟踪代码的版本,以确定部署到生产环境或发布给客户的内容。
历史
通过在开发软件时保留所有版本的时间记录,可以回滚到较旧的代码版本,或者确定导致回归的特定更改。
归属
知道谁在特定文件中进行了更改,可以帮助您确定所有权,评估领域专业知识,并在进行更改时评估风险。
依赖
源代码已成为项目的其他关键元数据的规范来源,比如对其他软件包的依赖关系。
质量
源代码管理系统允许在接受更改之前轻松进行同行审查,从而提高软件的整体质量。
由于源代码管理在软件开发中起着如此关键的作用,重要的是要了解它的工作原理,并选择一个最适合您的组织和期望的 DevOps 工作流程的系统。
源代码管理的三代
协作是软件开发的重要组成部分,随着团队规模的扩大,有效地在共享代码库上进行协作的能力通常成为开发人员生产力的瓶颈。此外,系统的复杂性往往会增加,因此,与管理十几个文件或少数模块不同,通常会看到需要大量更新的数千个源文件,以实现系统范围的更改和重构。
为了管理对代码库的协作需求,源代码管理(SCM)系统被创建了。第一代 SCM 系统通过文件锁定处理协作。其中的例子有 SCCS 和 RCS,需要您在编辑之前锁定文件,进行更改,然后释放锁定以便其他人贡献。这似乎消除了两位开发者做出冲突更改的可能性,但存在两个主要缺点:
-
生产力仍然受到影响,因为在编辑之前你必须等待其他开发人员完成他们的更改。在文件较大的系统中,这实际上可能将并发性限制为一次只能一个开发人员。
-
这并未解决跨文件的冲突问题。仍然可能存在两个开发人员修改具有相互依赖关系的不同文件,并通过引入冲突更改来创建错误或不稳定的系统。
第二代版本控制系统有了重大改进,始于由 Dick Grune 创建的 Concurrent Versions System(CVS)。CVS 在其处理文件锁定(或者说不处理)的方法上具有革命性。与其阻止您更改文件不同,它允许多个开发人员对同一文件进行同时(可能是冲突的)更改。后来通过文件合并解决了这一问题:通过差异(diff)算法分析冲突文件,并向用户展示需要解决的冲突更改。
通过延迟解决冲突更改到检入,CVS 允许多个开发人员自由修改和重构大型代码库而不会被同一文件的其他更改阻塞。这不仅提高了开发人员的生产力,还允许将大型功能分离并单独测试,然后将其合并到集成的代码库中。
目前最流行的第二代 SCM 是 Apache Subversion,它被设计为 CVS 的即插即用替代品。它相比 CVS 有几个优点,包括将提交追踪为单个版本,从而避免可能损坏 CVS 仓库状态的文件更新冲突。
第三代版本控制是分布式版本控制系统(DVCS)。在 DVCS 中,每个开发人员都有整个仓库的副本以及本地存储的完整历史记录。与第二代版本控制系统一样,你首先检出仓库的副本,进行更改,然后再次提交。然而,为了将这些更改与其他开发人员集成,你需要以点对点的方式同步整个仓库。
几种早期的 DVCS 系统存在,包括 GNU Arch、Monotone 和 Darcs,但 Git 和 Mercurial 使得 DVCS 变得流行起来。Git 是作为对 Linux 团队需求的直接响应而开发的,他们需要一个稳定可靠的版本控制系统,以支持开源操作系统开发的规模和要求,它已成为开源和商业版本控制系统使用的事实标准。
DVCS 相比基于服务器的版本控制系统提供了几个优点:
完全脱机工作。
由于你拥有仓库的本地副本,因此可以在没有网络连接的情况下进行代码的检入、检出、合并和分支管理。
没有单一故障点。
不像基于服务器的 SCM 那样,只有一个包含完整历史记录的存储库副本存在,DVCS 在每个开发者的机器上创建存储库的副本,增加了冗余性。
更快的本地操作
由于大多数版本控制操作都是在本地进行的,它们比网络速度或服务器负载不受影响,因此速度更快。
分散控制
由于同步代码涉及复制整个存储库,这使得分叉代码库变得更容易,在开源项目的情况下,当主项目停滞或走向不良时,这也使得启动独立努力变得更容易。
迁移的便利性
从大多数源代码管理工具转换到 Git 是一个相对简单的操作,并且你可以保留提交历史。
分布式版本控制也有一些缺点,包括以下几点:
较慢的初始存储库同步
初始同步包括复制整个存储库历史记录,这可能会慢得多。
更大的存储需求
由于每个人都拥有存储库的完整副本和所有历史记录,因此非常大和/或长期运行的项目可能需要大量的磁盘空间要求。
没有锁定文件的能力
基于服务器的版本控制系统在需要编辑不能合并的二进制文件时提供了一些锁定文件的支持。DVCS 的锁定机制不能被强制执行,这意味着只有可以合并的文件(例如文本)适合进行版本控制。
选择你的源代码控制
希望到目前为止,你已经确信使用现代分布式版本控制系统(DVCS)是正确的方式。它为任何规模的团队提供了最佳的本地和远程开发能力。
此外,在常用的版本控制系统中,Git 已经成为采纳的明显赢家。这在查看最常用的版本控制系统的 Google 趋势分析中清楚地显示出来,如图 2-1 所示。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0201.png
图 2-1. 2004 年至 2022 年版本控制系统的受欢迎程度(来源:Google Trends)
Git 已经成为开源社区中的事实标准,这意味着支持其使用的广泛基础以及丰富的生态系统存在。然而,有时候说服老板或同事采用新技术可能会很困难,特别是如果他们对传统的源代码控制技术有深入的投资。
这里有一些你可以用来说服老板升级到 Git 的理由:
可靠性
Git 的写法类似于文件系统,包括适当的文件系统检查工具(git fsck
)和校验和以确保数据的可靠性。鉴于它是分布式版本控制系统(DVCS),你可能也将数据推送到多个外部存储库,从而创建数据的多个冗余备份。
性能
Git 并不是第一个分布式版本控制系统(DVCS),但它的性能非常出色。它从头开始设计,旨在支持 Linux 开发,能够处理极其庞大的代码库和数千名开发者。Git 仍然由一个庞大的开源社区积极开发和维护。
工具支持
Git 有超过 40 个前端界面,并且几乎在每个主要 IDE(JetBrains IntelliJ IDEA、Microsoft Visual Studio Code、Eclipse、Apache NetBeans 等)中都有完全支持,因此你不太可能找到一个不完全支持 Git 的开发平台。
集成
Git 与 IDE、问题跟踪器、消息平台、持续集成服务器、安全扫描工具、代码审查工具、依赖管理和云平台有着一流的集成。
升级工具
有迁移工具可简化从其他版本控制系统到 Git 的过渡,如支持从 Subversion 到 Git 的双向变更的 git-svn
,或者为 Git 提供的 Team Foundation Version Control (TFVC) 仓库导入工具。
总之,升级到 Git 几乎没有什么损失,并且有很多额外的功能和集成可以开始利用。开始使用 Git 就像下载适用于你的开发机器的版本并创建本地仓库一样简单。
然而,真正的力量在于与你的团队协作,如果你有一个中央仓库来推动变更并进行协作,这将非常方便。几家公司提供商业 Git 仓库,你可以自行托管或在它们的云平台上运行。这些包括 AWS CodeCommit、Assembla、Azure DevOps、GitLab、SourceForge、GitHub、RhodeCode、Bitbucket、Gitcolony 等。根据 JetBrains 2020 年“开发者生态系统现状”报告中显示的数据(见图 2-2),这些基于 Git 的源代码管理系统占据了超过 96% 的商业源代码控制市场份额。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0202.png
图 2-2. JetBrains “开发者生态系统现状 2020” 报告关于版本控制服务使用情况的数据(来源:JetBrains CC BY 4.0)
所有这些版本控制服务都提供基本版本控制之上的额外服务,包括以下功能:
-
协作
代码审查
拥有有效的代码审查系统对于维护代码完整性、质量和标准至关重要。
高级拉取请求/合并功能
许多供应商在 Git 的基础上实现了高级功能,帮助多仓库和团队工作流更高效地进行变更请求管理。
工作流自动化
在大型组织中,批准流程可能既流畅又复杂,因此通过团队和公司工作流程的自动化可以提高效率。
团队评论/讨论
有效的团队互动和讨论可以与特定的拉取请求和代码更改相关联,有助于改善团队内外的沟通。
在线编辑
在浏览器中的集成开发环境允许在几乎任何设备上从任何地方进行源代码协作。GitHub 甚至最近发布了Codespaces,提供了由 GitHub 托管的完整功能的开发环境。
-
合规/安全性
追踪
能够追踪代码历史是任何版本控制系统的核心特性,但通常还需要额外的合规检查和报告。
审计变更
出于控制和法规目的,通常需要审计代码库的变更,因此具备自动化工具是有帮助的。
权限管理
细粒度的角色和权限管理允许限制对敏感文件或代码库的访问。
物料清单
出于审计目的,通常需要一个完整的所有软件模块和依赖项的列表,并且可以从源代码生成。
安全漏洞扫描
许多常见的安全漏洞可以通过扫描代码库并查找用于利用已部署应用程序的常见模式来发现。在开发过程的早期阶段使用自动化漏洞扫描器可以帮助识别漏洞。
-
集成
问题追踪
通过与问题追踪器的紧密集成,您可以将特定的变更集与软件缺陷关联起来,从而更容易地识别修复错误的版本并跟踪任何回归问题。
CI/CD
通常,持续集成服务器将用于构建检入源代码。紧密集成使得更容易启动构建、报告成功和测试结果,并自动推广和/或部署成功的构建。
二进制包存储库
从二进制存储库获取依赖项并存储构建结果提供了一个中心位置来查找构件和分阶段部署。
消息集成
团队协作对于成功的开发工作至关重要,并且通过像 Slack、Microsoft Teams、Element 等平台简化讨论源文件、检入和其他源代码控制事件的功能,可以简化沟通。
客户端(桌面/IDE)
许多免费客户端和各种 IDE 的插件允许您访问您的源代码控制系统,包括来自 GitHub、Bitbucket 等的开源客户端。
在选择版本控制服务时,重要的是确保它与团队的开发工作流程相匹配,与您已经使用的其他工具集成,并符合您公司的安全策略。通常公司会有一个在整个组织中标准化的版本控制系统,但是如果公司标准不是像 Git 这样的分布式版本控制系统,采用更现代的版本控制系统可能会有好处。
如何进行第一个拉取请求
为了感受版本控制的工作方式,我们将通过一个简单的练习来创建您的第一个拉取请求,以便将您的贡献加入 GitHub 上官方书籍存储库中的读者评论部分,让您可以与其他读者一起展示您掌握的现代 DevOps 最佳实践!
此练习不需要安装任何软件或使用命令行,因此完成此练习应该是非常简单和直接的。强烈推荐完成此练习,以便您了解我们稍后在本章更详细介绍的分布式版本控制的基本概念。
首先,您需要导航到书籍存储库。在这个练习中,您需要登录以便可以从 Web 用户界面创建拉取请求。如果您还没有 GitHub 帐户,注册并开始使用非常简单和免费。
显示 Java 开发人员的 DevOps 工具存储库 GitHub 页面如图 2-3 所示。GitHub UI 默认显示根文件和名为README.md的特殊文件的内容。我们将对 Markdown 语言的可视文本文件自述文件进行编辑。
由于我们仅具有对此存储库的读取访问权限,我们将创建一个称为分支的个人克隆存储库,可以自由编辑并提出更改。一旦您登录 GitHub,您可以通过单击右上角突出显示的“分支”按钮来启动此过程。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0203.png
图 2-3. 包含本书示例的 GitHub 存储库
您的新分支将在 GitHub 的个人帐户下创建。一旦创建了您的分支,请完成以下步骤以打开基于 Web 的文本编辑器:
-
单击README.md文件以编辑详细页面以查看详细信息。
-
单击详细页面上的铅笔图标以编辑文件。
一旦单击铅笔图标,您将看到基于 Web 的文本编辑器,如图 2-4 所示。滚动到访客日志部分,并在末尾添加您自己的个人评论,让大家知道您已完成此练习。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0204.png
图 2-4. 用于快速更改文件的 GitHub 基于 Web 的文本编辑器
访客日志条目的推荐格式如下:
*Name* (@*optional_twitter_handle*): *Visitor comment*
如果您想在 Twitter 句柄上装点门面并链接到您的个人资料,Twitter 链接的 Markdown 语法如下所示:
@*twitterhandle*
要查看您的更改,可以单击“预览更改”选项卡,在将其插入原始自述文件后显示渲染的输出。
当您满意您的更改后,请向下滚动到代码提交部分,如图 2-5 所示。输入有关更改的有用描述以解释您的更新。然后继续单击“提交更改”按钮。
对于本例,我们将简单地提交到主分支,默认情况下是这样的。但是,如果您在共享存储库中工作,您将把拉取请求提交到可以单独集成的功能分支。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0205.png
图 2-5. 使用 GitHub UI 提交更改到您具有写入访问权限的存储库
在您对分叉存储库进行更改后,您可以将其作为对原始项目的拉取请求提交。这将通知项目维护者(在本例中是书籍作者),等待审查的建议更改,并让他们选择是否将其集成到原始项目中。
要做到这一点,请转到 GitHub 用户界面中的“拉取请求”选项卡。此屏幕上有一个创建“新拉取请求”的按钮,将为您提供要合并的“基础”和“头”存储库的选择,如 图 2-6 所示。
在这种情况下,由于只有一个更改,应正确选择默认的存储库。只需单击“创建拉取请求”按钮,即可提交针对原始存储库的新拉取请求以供审查。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0206.png
图 2-6. 从分叉存储库创建拉取请求的用户界面
这完成了您对拉取请求的提交!现在轮到原始存储库所有者审查、评论或接受/拒绝拉取请求了。虽然您没有写入原始存储库以查看其外观,但 图 2-7 显示了将呈现给存储库所有者的内容。
一旦存储库所有者接受您的拉取请求,您的自定义访客日志问候语将添加到官方书籍存储库中。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0207.png
图 2-7. 用于合并生成的拉取请求的存储库所有者用户界面
这个工作流程是处理项目集成的分叉和拉取请求协作模型的一个示例。我们将稍微详细地讨论协作模式以及适合使用它们的项目和团队结构在 “Git 协作模式” 中。
Git 工具
在前一节中,我们展示了使用 GitHub UI 进行 Git 的整个基于 Web 的工作流程。然而,除了代码审查和存储库管理之外,大多数开发人员在 Git 的基于客户端的用户界面中度过了大部分时间。可用的客户端界面可以广泛分为以下几类:
命令行
官方的 Git 命令行客户端可能已安装在您的系统上,或者很容易添加。
GUI 客户端
官方的 Git 发行版附带了几个开源工具,可以更轻松地浏览您的修订历史或结构化提交。此外,还有几个第三方免费和开源的 Git 工具可以让您更轻松地使用您的仓库。
Git 的 IDE 插件
通常,您只需使用您喜爱的 IDE 就能够使用分布式源代码控制系统。许多主要的 IDE 都默认支持 Git,或者提供了一个良好支持的插件。
Git 命令行基础知识
Git 命令行是管理源代码控制系统的最强大接口,可以通过所有本地和远程选项来管理您的仓库。您可以在控制台上键入以下内容来检查是否已安装 Git 命令行:
git --version
如果您已安装 Git,命令将返回您使用的操作系统和版本,类似于此内容:
git version 2.26.2.windows.1
不过,如果您尚未安装 Git,以下是在各种平台上获取它的最简单方法:
-
Linux 发行版:
-
基于 Debian:
sudo apt install git-all
-
基于 RPM:
sudo dnf install git-all
-
-
macOS
-
在 macOS 10.9 或更高版本上运行
git
将提示您安装它。 -
另一个简单的选项是安装GitHub Desktop,它会安装并配置命令行工具。
-
-
Windows
-
最简单的方法是简单地安装 GitHub Desktop,它会同时安装命令行工具。
-
另一个选择是安装Git for Windows。
-
无论您使用哪种方法来安装 Git,您最终都将获得相同的出色命令行工具,这些工具在所有桌面平台上得到了良好的支持。
要开始,了解基本的 Git 命令是很有帮助的。图 2-8 显示了一个典型的仓库层次结构,其中包含一个中央仓库和三个已经在本地克隆了它的客户端。请注意,每个客户端都有仓库的完整副本以及可以进行更改的工作副本。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0208.png
图 2-8. 分布式版本控制协作的典型中央服务器模式
以下显示了一些 Git 命令,允许您在仓库和工作副本之间移动数据。现在让我们来看看一些最常用的命令,用于管理您的仓库和在 Git 中进行协作。
-
仓库管理:
clone
在本地文件系统上创建与另一个本地或远程仓库的连接副本。对于那些从 CVS 或 Subversion 等并发版本控制系统过来的人来说,此命令的作用类似于
checkout
,但在语义上有所不同,因为它创建了远程仓库的完整副本。图中的所有客户端在开始时都会克隆中央服务器。所有的客户端都克隆了中央服务器。init
创建一个新的空仓库。不过,大多数情况下,您会首先克隆一个现有的仓库。
-
变更集管理:
add
将文件修订版添加到版本控制中,可以是新文件或对现有文件的修改。这与 CVS 或 Subversion 中的
add
命令不同,因为它不会跟踪文件,需要每次文件更改时调用。确保在提交之前调用add
以添加所有新文件和修改文件。mv
重命名或移动文件/目录,并更新下一个提交的版本控制记录。在使用上类似于 Unix 中的
mv
命令,应该使用它来代替文件系统命令以保持版本控制历史完整。restore
允许您从 Git 索引中恢复文件,如果它们被删除或错误修改。
rm
移除文件或目录,并更新下一个提交的版本控制记录。在使用上类似于 Unix 中的
rm
命令,应该使用它来代替文件系统命令以保持版本控制历史完整。 -
历史控制:
branch
如果没有参数,则列出本地仓库中的所有分支。也可用于创建新分支或删除分支。
commit
将工作副本中的更改保存到本地仓库。在运行
commit
之前,请确保通过调用add
、mv
和rm
对已添加、修改、重命名或移动的文件进行注册。您还需要指定一个提交消息,可以在命令行上使用-m
选项完成;如果省略,则会生成一个文本编辑器(如vi
)来允许您输入消息。merge
将命名提交中的更改合并到当前分支。如果合并历史已经是当前分支的后代,则使用“快进”来按顺序组合历史。否则,将创建一个合并,合并历史;用户将提示解决任何冲突。此命令也被
git pull
使用来集成来自远程仓库的更改。rebase
在上游分支上重播当前分支的提交。与
merge
不同之处在于结果将是线性历史,而不是合并提交,这可以使修订历史更容易遵循。缺点是当移动历史时,rebase 会创建全新的提交,因此如果当前分支包含先前已推送的更改,则正在重写其他客户端可能依赖的历史。reset
将
HEAD
还原到先前状态,并具有几个实用用途,例如撤消add
或撤消提交。但是,如果这些更改已经被推送到远程,这可能会导致与上游仓库的问题。请谨慎使用!switch
切换工作副本中的分支。如果您在工作副本中有更改,则可能会导致三向合并,因此最好先提交或隐藏您的更改。使用
-c
选项,此命令将创建一个分支并立即切换到它。tag
允许您在特定提交上创建一个由 PGP 签名的标签。这将使用默认电子邮件地址的 PGP 密钥。由于标签是经过加密签名和唯一的,因此在推送后不应该被重用或更改。此命令的其他选项允许删除、验证和列出标签。
log
以文本格式显示提交日志。它可用于快速查看最近的更改,并支持用于显示的历史子集和输出格式的高级选项。在本章的后面,我们还将介绍如何使用
gitk
等工具来可视化浏览历史记录。 -
协作:
fetch
从远程仓库拉取历史记录到本地仓库,但不尝试将其与本地提交合并。这是一个安全的操作,可以在任何时候重复执行,而不会引起合并冲突或影响工作副本。
pull
等效于
git fetch
后跟git merge FETCH_HEAD
。它方便了从远程仓库抓取最新更改并将其与您的工作副本集成的常见工作流程。然而,如果您有本地更改,pull
可能会导致合并冲突,您将被迫解决。因此,通常更安全的做法是先fetch
,然后决定是否仅需简单合并。push
将本地仓库中的更改发送到上游远程仓库。在
commit
后使用此命令将您的更改推送到上游仓库,以便其他开发人员可以看到您的更改。
现在您已经对 Git 命令有了基本的了解,让我们将这些知识付诸实践。
Git 命令行教程
为了演示如何使用这些命令,我们将通过一个简单的示例来从头开始创建一个新的本地仓库。对于这个练习,我们假设您正在使用一个类似于 Bash 的命令行 shell 的系统。这是大多数 Linux 发行版以及 macOS 的默认设置。如果您使用的是 Windows,您可以通过 Windows PowerShell 来完成这个操作,它有足够的别名来模拟基本命令的 Bash。
如果这是您第一次使用 Git,建议您输入您的姓名和电子邮件,这将与您所有的版本控制操作相关联。您可以使用以下命令来实现这一点:
git config --global user.name *"Put Your Name Here"*
git config --global user.email *"your@email.address"*
配置个人信息后,转到适当的目录创建您的工作项目。首先,创建项目文件夹并初始化仓库:
mkdir tutorial
cd tutorial
git init
这将创建仓库并初始化,使您可以开始跟踪文件的修订版本。让我们创建一个可以添加到修订控制的新文件:
echo "This is a sample file" > sample.txt
要将此文件添加到修订控制中,请使用以下git add
命令:
git add sample.txt
您可以使用git commit
命令将此文件添加到版本控制中:
git commit sample.txt -m "First git commit!"
恭喜您使用 Git 进行了第一次命令行提交!您可以通过使用git log
命令来双重检查确保您的文件正在被修订控制跟踪,它应该返回类似以下的输出:
commit 0da1bd4423503bba5ebf77db7675c1eb5def3960 (HEAD -> master)
Author: Stephen Chin <steveonjava@gmail.com>
Date: Sat Mar 12 04:19:08 2022 -0700
First git commit!
从这里,您可以看到 Git 存储库中存储的一些细节,包括分支信息(默认分支是master
)和按全局唯一标识符(GUID)分类的修订。虽然您可以从命令行做更多事情,但通常更容易使用为您的工作流程构建的 Git 客户端或 IDE 集成,该工具专为开发人员工作流程设计。接下来的几节将介绍这些客户端选项。
Git 客户端
几个免费开源的客户端可供您使用,可使您更轻松地使用 Git 存储库,并针对不同的工作流程进行了优化。大多数客户端并不尝试做到一切,而是专注于为特定工作流程提供可视化和功能。
默认的 Git 安装附带了一些方便的可视化工具,使提交和查看历史更加容易。这些工具是用 Tcl/Tk 编写的,跨平台,并且可以轻松地从命令行启动,以补充 Git 命令行界面(CLI)。
第一个工具gitk
提供了一个选择,用于浏览、查看和搜索本地存储库的 Git 历史,而不是使用命令行。显示 ScalaFX 开源项目历史记录的gitk
用户界面显示在图 2-9 中。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0209.png
图 2-9. 捆绑的 Git 历史查看器应用程序
gitk
的顶部窗格显示具有分支信息的修订历史,以可视方式绘制,这对于解密复杂的分支历史非常有用。在此之下是可用于查找包含特定文本的提交的搜索过滤器。最后,对于所选更改集,您可以看到已更改的文件以及更改的文本差异,这也是可搜索的。
Git 随附的另一个工具是git-gui
。与仅显示有关存储库历史的信息的gitk
不同,git-gui
允许您通过执行许多 Git 命令(包括commit
、push
、branch
、merge
等)来修改存储库。
图 2-10 显示了用于编辑本书源代码存储库的git-gui
用户界面。在左侧,显示了所有工作副本的更改,未暂存的更改显示在顶部,下一个提交中将包含的文件显示在底部。所选文件的详细信息显示在右侧,其中包括新文件的完整文件内容,或者修改文件的差异。在右下角,提供了用于常见操作(如重新扫描、签名、提交和推送)的按钮。高级操作(如分支、合并和远程存储库管理)的其他命令可在菜单中找到。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0210.png
图 2-10. 捆绑的 Git 协作应用程序
git-gui
是 Git 的一个以工作流驱动的用户界面的示例。它不公开命令行上可用的完整功能集,但对于常用的 Git 工作流程非常方便。
另一个以工作流驱动的用户界面的例子是GitHub Desktop。这是最受欢迎的第三方 GitHub 用户界面,正如前面提到的,它还方便地与命令行工具捆绑在一起,因此您可以将其用作 Git CLI 和前述捆绑 GUI 的安装程序。
GitHub Desktop 类似于git-gui
,但经过了优化以与 GitHub 的服务集成,并且用户界面设计得非常易于遵循类似于 GitHub Flow 的工作流程。编辑源存储库的 GitHub Desktop 用户界面,另一本优秀书籍The Definitive Guide to Modern Java Clients with JavaFX,显示在图 2-11 中。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0211.png
图 2-11. GitHub 的开源桌面客户端
除了与git-gui
具有相同类型的功能以查看更改、提交修订版本和拉取/推送代码之外,GitHub Desktop 还具有许多高级功能,可使管理代码变得更加容易:
-
提交归因
-
语法高亮差异
-
图像差异支持
-
编辑器和 shell 集成
-
拉取请求的 CI 状态
GitHub Desktop 可以与任何 Git 存储库一起使用,但具有专门针对与 GitHub 托管存储库一起使用的功能。以下是一些其他受欢迎的 Git 工具:
由 Atlassian 制作的免费但专有的 Git 客户端。它是 GitHub Desktop 的一个很好的替代品,并且只对 Atlassian 的 Git 服务 Bitbucket 有轻微偏见。
商业和功能丰富的 Git 客户端。对于开源开发者是免费的,但对于商业用途是付费的。
基于 TortoiseSVN 的自由 GNU 公共许可证(GPL)的 Git 客户端。唯一的缺点是它只支持 Windows。
其他
Git GUI 客户端的完整列表维护在Git 网站上。
Git 桌面客户端是您可以使用的可用源代码控制管理工具库的强大补充。然而,最有用的 Git 界面可能已经在您的 IDE 中就在您的指尖。
Git IDE 集成
许多集成开发环境(IDE)都包含 Git 支持,要么作为标准功能,要么作为一个得到很好支持的插件。你很可能不需要去找其他东西,只需在你喜欢的 IDE 中进行基本的版本控制操作,如添加、移动和删除文件,提交代码和将更改推送到上游存储库。
JetBrains IntelliJ IDEA 是最受欢迎的 Java IDE 之一。它有一个开源的社区版,也有一个商业版,提供了额外的功能,适用于企业开发者。IntelliJ 的 Git 支持功能齐全,能够同步远程仓库的更改,跟踪和提交在 IDE 中进行的更改,并集成上游更改。图中展示了 Git 更改集的集成提交选项卡 Figure 2-12。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0212.png
图 2-12. IntelliJ 用于管理工作副本更改的提交选项卡
IntelliJ 提供了丰富的功能集,您可以使用它来定制 Git 的行为以适应团队的工作流程。例如,如果您的团队喜欢 git-flow 或 GitHub Flow 的工作流程,您可以选择在更新时合并(有关 Git 工作流的更多细节请参见下一节)。然而,如果您的团队希望保持像 OneFlow 中规定的线性历史,您可以选择在更新时进行变基。IntelliJ 还支持本地凭据提供程序以及开源的 KeePass 密码管理器。
另一个提供出色 Git 支持的 IDE 是 Eclipse,这是一个完全开源的 IDE,拥有强大的社区支持,并由 Eclipse Foundation 运营。Eclipse 的 Git 支持由 EGit 项目提供,该项目基于 JGit,这是 Git 版本控制系统的纯 Java 实现。
由于与嵌入式 Java 实现的 Git 紧密集成,Eclipse 提供了最全面的 Git 支持。从 Eclipse 用户界面,您几乎可以完成从命令行执行的所有操作,包括变基、挑选、打标签、打补丁等。从偏好设置对话框中可以看到丰富的功能集,如 Figure 2-13 所示。该对话框有 12 个配置页面详细说明 Git 集成的工作,并支持一个长达 161 页的用户指南。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0213.png
图 2-13. Eclipse 的 Git 配置偏好对话框
其他可以期待有很好 Git 支持的 Java IDE 包括以下几款:
NetBeans
提供了一个 Git 插件,完全支持从 IDE 进行的工作流程。
Visual Studio Code
支持 Git 以及其他开箱即用的版本控制系统。
BlueJ
由伦敦国王学院构建的受欢迎的学习 IDE 还支持其团队工作流中的 Git。
Oracle JDeveloper
虽然它不支持复杂的工作流程,JDeveloper 提供了对克隆、提交和推送到 Git 仓库的基本支持。
在本章至今,您已经向您的工具库中添加了一整套新的命令行、桌面和集成工具,用于处理 Git 存储库。 这一系列社区和行业支持的工具意味着,无论您的操作系统、项目工作流程甚至团队偏好如何,您都会发现完整的工具支持可以让您在源代码控制管理方面取得成功。 下一节将更详细地介绍由完整的 Git 工具范围支持的协作模式。
Git 协作模式
分布式版本控制系统已经被证明可以扩展到拥有数百名合作者的非常大的团队。 在这种规模下,需要就统一的协作模式达成一致,以帮助团队避免重复工作、避免大量且难以管理的合并,并减少在管理版本控制历史记录上的阻塞时间。
大多数项目遵循中央存储库模型:一个单一的存储库被指定为用于集成、构建和发布的官方存储库。 即使分布式版本控制系统允许非集中式的对等交换修订版,但最好将其保留给在少数开发人员之间进行短期努力的项目。 对于任何大型项目,具有单一真实性的系统是重要的,并且需要一个所有人都同意是官方代码线的存储库。
对于开源项目,常见的做法是一组有限的开发人员具有对中央存储库的写访问权限,而其他提交者则会fork该项目并发出拉取请求以包含他们的更改。 最佳实践是提出小型拉取请求,并且除了拉取请求创建者之外,还有其他人接受它们。 这对于拥有数千名贡献者的项目具有很好的扩展性,并且在代码库不被充分理解时允许核心团队进行审查和监督。
然而,对于大多数企业项目来说,首选的是具有单个主分支的共享存储库。 使用拉取请求相同的工作流程可以使中央或发布分支保持清洁,但这简化了贡献过程,并鼓励更频繁的集成,从而减少了合并更改的大小和难度。 对于有紧迫截止日期或遵循具有短周期迭代的敏捷过程的团队,这也减少了最后一刻集成失败的风险。
大多数团队采用的最后一个最佳实践是使用分支来处理功能,然后将其集成回主要代码线。 Git 使得创建短期分支成本低廉,因此常见的做法是为仅需几个小时的工作创建一个分支,然后将其合并回来。 创建长期功能分支的风险在于,如果它们与代码开发的主干分支相差太大,那么将它们集成回来就会变得困难。
遵循这些分布式版本控制的通用最佳实践,出现了几种协作模式。 它们有很多共同之处,主要在于它们对分支、历史管理和集成速度的处理方式上有所不同。
git-flow
Git-flow是最早的 Git 工作流之一,受到了 Vincent Driessen 的一篇博客文章的启发。它为后来的 Git 协作工作流(如 GitHub Flow)奠定了基础;然而,git-flow 比大多数项目需要的工作流更为复杂,可能会增加额外的分支管理和集成工作。
主要特点包括以下内容:
开发分支
每个特性都有一个分支
合并策略
不要快进合并
重置历史
不进行重置
发布策略
单独的发布分支
在 git-flow 中,有两个长期存在的分支:一个用于开发集成,称为develop,另一个用于最终发布,称为master。开发人员预计会在按照他们正在进行的特性命名的特性分支上进行所有编码,并在完成后将其与开发分支集成。当开发分支具有进行发布所需的特性时,将创建一个新的发布分支,用于通过补丁和错误修复稳定代码库。
一旦发布分支稳定并准备好发布,它就会被整合到主分支,并添加一个发布标签。一旦在主分支上,只能应用热修复,这是在专用分支上管理的小改动。这些热修复还需要应用到开发分支和任何其他需要相同修复的并发发布。图 2-14 展示了一个 git-flow 的示意图。
由于 git-flow 的设计决策,它往往会创建复杂的合并历史。通过不利用快速合并或重置,每次集成都会成为一个提交,即使使用可视工具也很难跟踪并发分支的数量。此外,复杂的规则和分支策略需要团队培训,并且难以用工具强制执行,通常需要通过命令行界面进行检查和集成。
小贴士
Git-flow 最适用于需要同时维护多个发布版的显式版本化项目。通常情况下,这对于只有一个最新版本并且可以通过单一发布分支管理的 Web 应用来说并不适用。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0214.png
图 2-14. 使用 git-flow 管理分支和集成 (来源:Vincent Driessen,知识共享署名-相同方式共享)
如果你的项目正处于 git-flow 擅长的甜蜜点,那么它是一个非常经过深思熟虑的协作模型。否则,你可能会发现一个更简单的协作模型就足够了。
GitHub Flow
GitHub Flow是对 git-flow 复杂性的回应而推出的简化 Git 工作流,由 Scott Chacon 在另一篇著名的博客文章中提出。GitHub Flow 或类似的变种已被大多数开发团队采用,因为它在实践中更容易实现,处理了持续发布的 Web 开发的常见情况,并得到了良好的工具支持。
关键特点包括以下几点:
开发分支
特性分支
合并策略
无快速向前合并
重置历史
无重置
发布策略
没有单独的发布分支
GitHub 流采用简单的分支管理方法,将 master 作为主要代码线和发布分支。开发者在短暂的特性分支上完成所有工作,并在他们的代码通过测试和代码审查后立即将其集成回主分支。
提示
总的来说,GitHub 流通过简单的工作流程和简单的分支策略充分利用了现有的工具。因此,不熟悉团队流程或不熟悉命令行 Git 界面的开发者发现 GitHub 流易于使用。
GitHub 流协作模型非常适合服务器端和云部署应用程序,其中唯一有意义的版本是最新发布。事实上,GitHub 流建议团队持续部署到生产环境,以避免特性堆积,即单个发布构建中包含多个增加复杂性的特性,使得确定破坏性变更更加困难。然而,对于具有多个并发发布的更复杂工作流程,需要修改 GitHub 流以适应。
GitLab 流
GitLab 流 实际上是 GitHub 流的扩展,在 GitLab 的 网站 上有文档记录。它遵循相同的核心设计原则,使用主分支作为单个长期存在的分支,并在特性分支上进行大部分开发。然而,它添加了一些扩展以支持许多团队采用的发布分支和历史清理作为最佳实践。
关键特点包括以下几点:
开发分支
特性分支
合并策略
开放式
重置历史
可选
发布策略
单独的发布分支
GitHub 流和 GitLab 流之间的关键区别在于添加了发布分支。这是因为大多数团队并不像 GitHub 那样实践持续部署。拥有发布分支可以在推送到生产之前稳定代码;然而,GitLab 流建议在主分支上进行补丁,然后挑选它们进行发布,而不是像 git-flow 那样有额外的热修复分支。
另一个重要的区别是愿意使用 rebase
和 squash
来编辑历史。通过在提交到主分支之前清理历史,可以更轻松地回溯历史,发现关键变更或引入的错误。然而,这涉及重写本地历史,在已经推送到中央仓库时可能会很危险。
提示
GitLab 流是对 GitHub 流协作工作流理念的现代演绎,但最终你的团队必须根据项目需求决定特性和分支策略。
OneFlow
OneFlow,另一种基于 git-flow 的协作工作流,由亚当·鲁卡提出,并在详细的 博客 中介绍。OneFlow 与 GitHub/GitLab Flow 一样,在压缩独立的开发分支以支持特性分支和直接集成到主分支方面进行了相同的适应。然而,它保留了在 git-flow 中使用的发布和热修复分支。
Key attributes include the following:
Development branches
每个特性分支
合并策略
No fast-forward merges without rebase
Rebasing history
推荐使用 rebase
发布策略
单独的发布分支
OneFlow 的另一个重大偏差是,它非常倾向于修改历史以保持 Git 修订历史的可读性。它提供了三种合并策略,具有不同程度的修订清洁度和回滚友好性:
Rebase
这使得合并历史基本上是线性的并且易于跟踪。它有一个通常的警告,即推送到中央服务器的变更集不应该进行 rebase,并且使得回滚变得更加困难,因为它们不会捕获在一个单一提交中。
merge -no-ff
这与 git-flow 中使用的策略相同,并且其缺点是合并历史主要是非顺序的,难以跟踪。
rebase + merge -no-ff
这是一个重新基于 rebase 的解决方法,最后增加了额外的合并集成,以便可以作为一个单元回滚,尽管它仍然基本上是顺序的。
Tip
OneFlow 是一个经过深思熟虑的 Git 协作工作流,是根据大型企业项目开发人员的经验创建的。它可以看作是 git-flow 的现代变体,应该能够满足任何规模项目的需求。
Trunk-Based Development
所有上述方法都是特性分支开发模型的变种;所有活跃的开发都在分支上进行,然后合并到主分支或专用开发分支。它们充分利用了 Git 在分支管理方面的强大支持,但如果特性不够细粒度,就会遭受几十年来困扰团队的典型集成问题。特性分支在活跃开发越长,与主分支(或主干)同时进行的其他特性和维护发生冲突的可能性就越高。
基于主干的开发 通过建议所有开发都在主分支上进行,并且在测试通过时随时进行非常短的集成来解决这个问题,但不一定等待完整的特性完成。
Key attributes include the following:
开发分支
可选,但不能有长期存在的分支
Merge strategy
Only if using development branches
Rebasing history
推荐使用 rebase
发布策略
Separate release branches
Paul Hammant 是主张基于主干的开发的坚定支持者,他建立了一个完整的网站,并撰写了一本相关主题的书籍。尽管这并不是协作源代码管理系统中的新方法,但它已被证明是大团队敏捷开发的有效方法,无论是在经典的中央化 SCM 如 CVS 和 Subversion 上,还是现代的分布式版本控制系统如 Git 上同样适用。
总结
良好的源代码管理系统和实践为快速构建、发布和部署代码的稳健 DevOps 方法奠定了基础。在本章中,我们讨论了源代码管理系统的历史,并解释了为什么全球开始接受分布式版本控制。
这种整合建立了丰富的源代码控制服务器、开发工具和商业集成生态系统。最终,通过 DevOps 思想领袖对分布式版本控制的采纳,建立了可以遵循的最佳实践和协作工作流程,以帮助您的团队成功采用现代化的源代码管理系统。
在接下来的几章中,我们将深入探讨与您的源代码管理系统连接的系统,包括持续集成、包管理和安全扫描,这些系统能让您快速部署到传统或云原生环境中。您正在打造一个全面支持您需要满足质量和部署目标的工作流的 DevOps 平台。
第三章:容器简介
梅丽莎·麦凯
任何傻瓜都可以知道。关键在于理解。
阿尔伯特·爱因斯坦
如果你知道为什么,你可以任何怎样都行。
弗里德里希·尼采
在撰写本文时,生产和其他环境中使用容器的使用正在呈指数级增长,而围绕应用容器化的最佳实践仍在讨论和定义中。随着我们专注于效率提升并考虑具体用例,经验丰富的博客圈和专业实践者已经发展出了一些高度推荐的技术和模式。并且如预期的那样,已经发展出了相当一部分模式和常见用途,以及希望本章能帮助您识别和避免的反模式。
我自己对容器的试错式介绍感觉就像是搅动了一个黄蜂窝(哦,那些蛰伤!)。毫无疑问,我毫无准备。表面上看,容器化似乎简单得令人难以置信。现在我知道如何在 Java 生态系统中开发和部署容器,我希望以一种方式传授这些知识,帮助你避免类似的痛苦。本章概述了您成功容器化应用所需的基本概念,并讨论了为什么您甚至想要做这样的事情。
第四章讨论了微服务的更大图景,但在这里我们将从学习微服务部署的基本构建块开始,如果您尚未遇到的话,您无疑会遇到:容器。请注意,微服务的概念作为一种架构关注,并不意味着一定要使用容器;相反,特别是在云原生环境中部署这些服务通常是围绕容器化展开对话的关键。
让我们从考虑为什么我们会使用容器开始。做到这点的最佳方式是回过头来,了解我们是如何开始的。耐心是一种美德。如果你坚持不懈,通过这段历史课程将自然而然地使你更清楚地理解什么是容器。
理解问题的本质
我确信我不是唯一一个经历“房间里的大象”陪伴的人。尽管庞大的身影、震耳欲聋的噪音以及被忽视时可能带来的危险后果,这个象大小的主题却被允许自由漫游,毫无挑战地。我亲眼目睹过。我也有过这样的罪行。我甚至曾经有幸成为这只大象。
在容器化的背景下,我要提出这样一个论点,我们需要解决两只房间里的大象——以两个问题的形式:*什么是容器?和为什么我们会使用容器?*听起来很简单。怎么可能有人会忽略这些基本的起点呢?
或许这是因为微服务运动现在比以往任何时候都更多地引入了有关部署容器的讨论,我们担心错过时机。也许这是因为容器实施在目前极为流行的 Kubernetes 潮流中被默认期望,而“我们的 K8s 集群”是我们对话中的新潮流。甚至可能仅仅是因为在 DevOps 生态系统中,我们面临如此多的新技术和工具的攻击,作为开发者(尤其是 Java 开发者),如果我们停下来问问题,我们就害怕被落下。无论原因如何,在我们甚至能够详细讨论如何构建和使用容器之前,这些什么和为什么的问题必须先解决。
多年来,我有幸与不可思议的同事和导师们一起工作,对此深表感激。在职业生涯的初期,我经常回想起一些至理名言。它很简单;始终以一个不断重复的问题开始并继续进行任何项目的工作:你试图解决的问题是什么? 你解决方案的成功将取决于它如何满足这个要求——确实解决了最初的问题。
仔细考虑你是否从根本上解决了正确的问题。特别警惕拒绝那些实际上是实施指令的问题陈述,比如这样一个:通过将应用程序分解为容器化的微服务来提高其性能。你将更好地通过像这样一个问题陈述服务:为了减少客户完成目标所需的时间,将应用程序的性能提高 5%。请注意,后者包含一个具体的度量标准来衡量成功,并不限于微服务的实现。
这个原则同样适用于你日常选择使用的工具、选择编码的框架和语言、你如何设计系统,甚至如何打包和部署软件到生产环境。你所做的选择解决了什么问题?你如何知道你选择了最合适的工具?其中一种方法是了解特定工具旨在解决的问题。而了解其历史是做到这一点的最佳方式。这种做法应该适用于你使用的每一个工具。我保证,了解其历史后,你将能做出更好的决策,并从绕过已知的陷阱中受益,或者至少有理由接受任何不利因素并继续前进。
我的计划不是要完全无聊地向你讲述历史细节,但在你开始对每一行代码进行容器化之前,你应该了解一些基本信息和重要的里程碑。通过更多地了解原始问题及其解决方案,你将能够智能地解释为什么选择使用容器进行部署。
我不打算回溯到宇宙大爆炸,但我会回顾 50 多年前的情况,主要是为了表明虚拟化和容器化并不是新概念。事实上,这个概念已经经过半个多世纪的努力和改进。我挑选了一些重点来快速介绍,让我们跟上时代的步伐。这不是深入技术的手册,而是足够让你了解随着时间的推移取得的进展以及我们是如何达到今天的地步的一些材料。
让我们开始。
容器的历史
在 20 世纪 60 年代和 70 年代,计算资源一般极为有限且昂贵(按今天的标准)。进程完成需要很长时间(同样按今天的标准),通常一个计算机会长时间专门为单个用户的单个任务而运行。开始了改进计算资源共享和解决这些限制带来的瓶颈和低效的努力。但仅仅能够共享资源还不够。出现了一种需求,即在互相不干扰或者导致一个人无意间导致整个系统崩溃的情况下共享资源的方法。硬件和软件方面推进了虚拟化技术的发展。软件方面的一个发展是chroot
,我们将从这里开始。
1979 年,在 Unix 第七版开发期间,开发了chroot
,并在 1982 年加入了伯克利软件分发(BSD)。这个系统命令改变了进程及其子进程的根目录,导致文件系统的视图受限,以提供一个测试不同分发环境的环境,例如。尽管是朝着正确方向迈出的一步,但chroot
只是提供我们今天所需应用隔离的开端。2000 年,FreeBSD 扩展了这个概念,并在 FreeBSD 4.0 版中引入了更复杂的jail
命令和实用程序。其功能(在稍后的 5.1 和 7.2 版本中得到改进)有助于进一步隔离文件系统、用户和网络,并包括为每个jail
分配 IP 地址的能力。
2004 年,Solaris 容器和区域使我们更进一步,通过给应用程序提供完整的用户、进程和文件系统空间以及系统硬件访问权限。 谷歌在 2006 年推出了其进程容器,后来改名为cgroups,它的核心是隔离和限制进程的资源使用。 2008 年,cgroups被合并到 Linux 内核中,随后,与 Linux 命名空间一起,IBM 开发了 Linux 容器(LXC)。
现在事情变得更加有趣。 Docker 在 2013 年成为开源项目。 同年,谷歌提供了其 Let Me Contain That For You(lmctfy)开源项目,该项目使应用程序能够创建和管理自己的子容器。 从那时起,我们看到了容器的使用激增——尤其是 Docker 容器。 最初,Docker 将 LXC 作为其默认的执行环境,但在 2014 年,Docker 选择将其用于启动容器的 LXC 工具集替换为libcontainer,这是一个用 Go 编写的本地解决方案。 不久之后,lmctfy 项目停止了活跃开发,并打算与 libcontainer 项目合作,并将核心概念迁移到 libcontainer 项目中。
在这段时间内发生了很多事情。 我故意跳过了关于其他项目、组织和规范的更多细节,因为我想要谈论的是 2015 年的一个特定事件。 这个事件尤其重要,因为它将让您对市场变化背后的一些活动和动机有所了解,特别是涉及 Docker 的情况。
2015 年 6 月 22 日,宣布成立了开放容器倡议组织(OCI)。 这是Linux 基金会旗下的一个组织,旨在为容器运行时和镜像规范创建开放标准。 Docker 是重要的贡献者,但 Docker 宣布这个新组织时列出了参与者,包括 Apcera,亚马逊网络服务(AWS),思科,CoreOS,EMC,富士通,谷歌,高盛,惠普,华为技术,IBM,英特尔,Joyent,Pivotal Software,Linux 基金会,Mesosphere,微软,Rancher Labs,红帽和 VMware。 显然,容器的发展及其周围的生态系统已经达到了一个引人注目的地步,并且发展到了确立一些共同基础对所有涉及方都有益处的地步。
在 OCI 成立时,Docker 还宣布了将捐赠其基础容器格式和运行时 runC 的意图。 紧随其后,runC 成为了OCI 运行时规范的参考实现,而 Docker v2 Schema 2 镜像格式,在 2016 年 4 月捐赠,成为了OCI 镜像格式规范的基础。 这些规范的版本 1.0都于 2017 年 7 月发布。
注意
runC是 libcontainer 的一个再打包,符合 OCI 运行时规范的要求。事实上,截至本文撰写时,runC 的源代码中包含一个名为libcontainer的目录。
随着容器生态系统的发展,这些系统的编排也在快速发展之中。2015 年 7 月 21 日,在 OCI 成立一个月后,Google 发布了 Kubernetes v1.0。与此同时,Cloud Native Computing Foundation (CNCF)与 Google 和 Linux 基金会合作成立。Google 在 2016 年 12 月发布的 Kubernetes v1.5 中另一个重要的进展是开发了容器运行时接口(CRI),这为 Kubernetes 的机器守护进程kubelet支持替代低级别容器运行时提供了必要的抽象层。2017 年 3 月,CNCF 的另一成员 Docker 贡献了其自己开发的与 CRI 兼容的运行时containerd,用于将 runC 整合到 Docker v1.11 中。
2021 年 2 月,Docker 向 CNCF 捐赠了另一个参考实现。此贡献集中于图像分发(推送和拉取容器镜像)。三个月后,即 2021 年 5 月,OCI 基于 Docker Registry HTTP API V2 协议发布了版本 1.0 的OCI 分发规范。
如今,像 Kubernetes 这样的容器编排系统在云原生部署中非常普遍。容器在保持在各种主机中灵活部署方面起着重要作用,并在扩展分布式应用程序方面发挥了重要作用。包括 AWS、Google Cloud、Microsoft Azure 在内的云服务提供商正在不断增强其提供的共享基础设施和按使用量付费的存储。
恭喜你已经走完了那段历史!在几段文字中,我们跨越了 50 多年的发展和进步。你已经了解了一些已经发展成为我们解决方案的项目,以及容器及其部署背景中使用的一些常见术语。你还了解了 Docker 对今天容器状态的重大贡献——这正是我们深入了解容器生态系统、容器背后的技术细节以及实施组件的理想时机。
但等等!在我们深入讨论之前,让我们讨论第二只大象。你已经了解了发生了什么,但是为什么行业会以这种方式转变呢?
为什么要使用容器?
知道容器是什么以及如何描述它们还不够。要能够有条理地讨论它们,你应该理解为什么使用它们。使用容器的优势是什么?鉴于你现在对容器及其历史的了解,其中一些可能显而易见,但在激烈竞争之前深入探讨仍然是值得的。项目变更和任何新技术栈的引入都应该经过深思熟虑的成本效益分析。跟风并不是一个足够的理由。
你的第一个问题很可能是:为什么容器是开发者关注的事情?——确实是一个合理的问题。如果容器只是一种部署方法,似乎这应该是运维的责任范围。在这里,我们接近了开发和运维之间模糊的界线,这是支持 DevOps 思维方式的一个论据。将你的应用打包成容器,从开发者的角度来看,需要比你最初想象的更多的思考和远见。在学习了一些最佳实践和他人经验中遇到的问题后,你会在开发应用的同时考虑打包的问题。在这个过程中的某些方面将影响你关于应用或服务如何使用内存、文件系统的决策,如何插入可观察性钩子,如何允许不同的配置,以及如何与其他服务(如数据库)通信。这些只是几个例子。最终,这将取决于你的团队如何组织,但在一个 DevOps 团队中,作为开发者,掌握如何构建和维护容器镜像以及理解容器环境将是非常有价值的。
我最近有机会参加了“开发者大会”云与 DevOps 国际专场的座谈会,主题是“云计算的效率与简易性:未来将会带来什么?”作为讨论的一部分,我们谈论了当前可用的技术状态以及我们期望更多简化的领域。我在讨论中引入了以下问题/类比:*如果我们期望自己制造汽车,今天有多少人会开车?*在这个领域的许多技术仍处于非常早期阶段。市场上急需能够充分利用云计算提供的可伸缩性、可用性和弹性,并以减少复杂性为目标打包的全功能产品制造商。然而,我们仍然在设计用于构建这样东西的各个部件和零件之中。
容器在这方面是一个巨大的进步,提供了在打包应用程序和部署应用程序的基础设施之间提供有用的抽象级别。我预计有一天开发人员将不再需要涉及容器级别的细节,但目前,我们应该。至少,我们应该有一个位置来确保开发方面的问题在前进中得到解决。为了达到这个目的,以及消除你为什么甚至应该提出容器主题的任何剩余疑虑,让我们更多地了解一下。
想想打包、部署和运行你的 Java 应用程序需要做的一切。为了开始开发,你需要在开发机器上安装特定版本的 Java 开发工具包(JDK)。然后,你可能会安装诸如 Apache Maven 或 Gradle 之类的依赖管理器,以获取你选择在应用程序中使用的所有所需的第三方库,并将其打包成 WAR 或 JAR 文件。到这一步,它可能已经准备好部署到…… 某个地方。
开始出现问题。在生产服务器上安装了什么——Java 运行时的哪个版本,什么应用服务器(例如,JBoss,Apache Tomcat,WildFly)?在生产服务器上是否运行了其他可能干扰应用程序性能的进程?你的应用程序是否因为任何原因需要 root 访问权限,并且你的应用程序用户是否以正确的权限设置适当地配置?你的应用程序是否需要访问外部服务,比如数据库或 API 进行存活或健康检查?在回答这些问题之前,你甚至是否有权限访问专用的生产服务器,还是需要开始请求为你的应用程序提供一个生产服务器?那么当你的应用程序受到大量活动的影响时会发生什么——你能够快速自动地扩展,还是必须重新开始配置过程?
考虑到这些问题,很容易理解为什么使用虚拟机(VM)的虚拟化变得如此有吸引力。虚拟机在隔离应用程序进程方面提供了更多的灵活性,而快照虚拟机可以在部署中提供一致性。然而,VM 映像很大,并且不容易移动,因为它们包含整个操作系统,这增加了它们的整体体积。
在向其他开发人员首次介绍容器时,我多次收到过这样的回答,“哦!所以容器就像虚拟机?”虽然将容器类比于虚拟机是方便的,但存在重要区别。虚拟机(VMware vSphere、Microsoft Hyper-V 等)是硬件的抽象,模拟完整的服务器。在某种意义上,整个操作系统都包含在虚拟机中。虚拟机由一个称为hypervisor的软件层管理,它根据需要将主机的资源划分和分配给虚拟机。
另一方面,容器并不像传统虚拟机那样重。例如,Linux 容器不包含整个操作系统,可以被视为共享主机操作系统的 Linux 发行版。正如在图 3-1 中所示,VM 和容器是不同的抽象级别,Java 虚拟机(JVM)也是如此。
JVM 在这一切中扮演了什么角色?当像虚拟机这样的术语被重载时会让人感到困惑。JVM 完全是一个不同的抽象,并且是一个进程虚拟机,与系统虚拟机形成对比。它的主要任务是为 Java 应用程序提供 Java 运行环境(或 JRE,即 JVM 的实现)。JVM 虚拟化主机的处理器,以便执行 Java 字节码。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0301.png
图 3-1. 虚拟机与容器
容器是一个轻量级解决方案,承诺解决大部分围绕应用程序一致性、进程隔离和操作系统级别依赖的问题。这种打包服务或应用程序的方法可以利用缓存机制,大幅减少部署和启动应用程序所需的时间。与等待定制的供应和设置不同,容器可以部署到现有基础设施上——无论是现有的专用服务器、私人数据中心中的现有 VM,还是云资源。
即使您选择在生产环境中不使用容器,也强烈建议考虑一些围绕开发和测试环境的其他用例。
将新开发人员引入团队的一个重大挑战是设置他们的本地开发环境所花费的时间。一般来说,人们普遍认为,让开发人员达到能够贡献其第一个 bug 修复或改进的水平需要一些时间。虽然一些公司会规定开发工具(通常认为一致性可以提高支持工作的效率),但今天开发者比以往更有更多的选择。我认为,在开发人员已经习惯于不同工具时,强迫他们使用特定的工具集实际上会产生相反的效果。坦率地说,在许多情况下,这实际上已经不再必要——特别是现在我们可以利用容器。
容器有助于保持运行环境的一致性,并且在正确配置后,可以轻松在开发、测试或生产模式下启动。由于环境与应用程序一同打包在容器镜像中,因此由于缺少依赖项而导致服务或应用程序在这些环境中行为不同的风险大大降低。
这种可移植性提升了开发人员在本地环境中进行变更的理智测试能力,以及部署与生产环境中相同版本的代码以重现错误的能力。使用容器进行集成测试还带来了额外的好处,即尽可能地复现生产环境。例如,不再使用内存数据库进行集成测试,而是可以启动与生产中使用的数据库版本匹配的容器。像 TestContainers 这样的项目可以防止由于轻微的 SQL 语法或其他数据库软件版本之间的差异而导致的行为不规则。以这种方式使用容器可以通过避免在本地安装新软件或同一软件的多个版本而简化效率。
如果迄今为止我们对容器有什么了解,那就是它们以某种形式很可能会继续存在。本节从容器使用在过去几年中的指数增长开始,围绕容器生态系统不断开发和改进的工具集已经在开发和运营过程中获得了牢固的立足点。除了在完全不同方向上(请记住,容器已经有超过 50 年的历史了)可能会有巨大且目前未知的进展之外,你应该建议了解容器生态系统及如何充分利用这项技术。
容器解剖简介
作为开发人员,我第一次接触容器是通过一个由第三方承包商开发的项目,而现在这个项目由我的团队负责进一步开发和维护。除了将初始代码库引入我们内部的 GitHub 组织之外,还需要进行大量设置,以在项目周围建立我们的内部 DevOps 环境——设置我们的持续集成和部署(CI/CD)流水线,以及我们的开发和测试环境,当然还有我们的部署流程。
我将这种经历比作整理我的桌面(尤其是在几天疏忽之后)。这里我将完全过多地透露关于我的个人习惯的内容,但为了表达这一点,这是值得的。清理我的桌面最耗时的部分是一堆文件和邮件,它们总是长得足以倒塌。匆忙赶回家,将这些物品放在厨房柜台上,因为脑海中有其他紧急任务,常常放在已有的文件堆上……并且承诺稍后处理它们。问题是,我从不知道里面会有什么。这堆可能包含需要支付的账单、需要归档的重要文件,或者需要回复并在我们家庭日历上进行安排的邀请函或信件。我常常对预计要花费的时间感到害怕,这只会导致一堆被忽视的信件变得更大。
对于我们团队负责的项目,我的第一步是象征性地整理桌面。在源代码中找到的 Dockerfile 相当于解决了那些令人头疼的文件堆。尽管通过并学习这些概念是必要的,但我感觉自己被从手头任务上偏离了。在启动新项目时学习新技术有时并没有得到应有的时间规划,即使它会为项目进度表增加变数和固有风险。这并不意味着新技术永远不应该引入。开发人员绝对需要学习行业的新变化和技术,但最好通过限制引入项目的新技术数量或在时间表的变化上坦率地面对来减少风险。
注意
Dockerfile 是一个包含提供容器蓝图指令的文本文件。这个文件通常命名为 Dockerfile,尽管最初是专门为 Docker 设计的,由于其广泛的使用,其他构建镜像工具也支持使用 Dockerfile 来构建容器镜像(如 Buildah、kaniko 和 BuildKit)。
这里提供的信息并非是对已有文档的简单复述(例如,在线Docker 入门指南非常出色)。相反,我希望像剥洋葱一样,以一种方式来介绍基础知识,并为您提供即时价值和足够的细节,以便更好地评估准备好自己的桌面并准备好开展业务所需的工作量。现在您已经掌握了关于容器及其产生过程的大量信息。接下来的部分涵盖了开发人员将接触到的术语和功能。
Docker 架构和容器运行时
就像 Kleenex 是面巾纸的品牌一样,Docker 是容器的品牌。Docker 公司围绕容器化开发了一整套技术栈。因此,即使Docker 容器和Docker 镜像这些术语已经被泛化使用,但当你将 Docker Desktop 安装到你的开发机上时,你得到的不仅仅是运行容器的能力。你得到的是一个完整的容器平台,使得开发者能够轻松便捷地构建、运行和管理它们。
需要理解的是,安装 Docker 并非构建容器镜像或运行容器的必要条件。它只是一个被广泛使用且方便的工具而已。就像你可以在没有使用 Maven 或 Gradle 的情况下打包一个 Java 项目一样,你可以在没有使用 Docker 或 Dockerfile 的情况下构建一个容器镜像。我给新接触容器技术的开发者的建议是利用 Docker 提供的工具集,然后尝试其他选项或方法,以便对比和获得更好的使用体验。即使你选择使用 Docker 之外的其他工具或方法,花费在工程化良好的开发者体验上的时间和精力也足以给包含 Docker Desktop 在你的开发环境中带来很大的收益。
使用 Docker,你可以获得一个隔离的环境,用户/应用程序可以在其中操作,共享主机系统的操作系统/内核,而不会干扰同一系统上另一个隔离的环境(容器)的操作。Docker 使你能够做到以下几点:
-
定义容器(一种镜像格式)
-
构建容器镜像
-
管理容器镜像
-
分发/分享容器镜像
-
创建容器环境
-
启动/运行容器(容器运行时)
-
管理容器实例的生命周期
容器领域远不止 Docker,但许多容器工具集的替代方案专注于这些项目的子集。从学习 Docker 如何运作开始,有助于理解和评估这些替代方案。
在线可以找到许多描述 Docker 架构的图片和图表。一个图片搜索很可能会得到一个版本的 图 3-2。这个图表相当好地展示了 Docker 在开发机上的工作原理 —— Docker CLI 是你可以使用的接口,用来向 Docker 守护进程发送命令来构建镜像,从外部仓库(默认是 Docker Hub)检索请求的镜像,在本地存储中管理这些镜像,然后使用这些镜像在你的机器上启动和运行容器。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0302.png
图 3-2. Docker 架构
当首次介绍这个领域时,其中一个更令人困惑的概念是对 Docker 生态系统的一个方面的关注:容器运行时。再强调一遍,这只是 Docker 提供的整个技术栈的一部分,但是因为编排框架如 Kubernetes 需要这部分功能来启动和运行容器,所以它通常被称为 Docker 的一个单独实体(在替代容器运行时的情况下,也是如此)。
关于容器运行时的主题值得单独列出这一节,因为对于新接触容器世界的人来说,这可能是最令人困惑的方面之一。更加令人困惑的是,容器运行时分为两种不同的类别,低级或高级,这取决于实现了哪些功能。而且为了让您保持警惕,这些功能集可能会有重叠。
这是一个展示容器运行时如何与您早前学到的 OCI 和诸如 containerd 和 runC 等项目结合在一起的可视化图表的好地方。图 3-3 说明了旧版和新版 Docker、高级和低级运行时之间的关系,以及 Kubernetes 的位置。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0303.png
图 3-3. 容器生态系统中的运行时
提示
我遇到过的关于容器运行时的详细解释之一,并带有历史视角的最佳解释是由 Google 云平台团队的开发者倡导 Ian Lewis 撰写的 博客系列。
在 2016 年发布的 1.11 版本之前,Docker 可以被描述为一个将整个运行时所需的功能集合以及其他管理工具封装在一起的单块应用程序。在过去的几年里,Docker 进行了大量的代码重组,开发了抽象化并提取了离散功能。Docker 贡献给 OCI 的 runC 项目就是出于这个努力。这是第一个,也是一段时间内唯一实现 OCI Runtime 规范的低级容器运行时的实现。
还有其他的运行时存在,并且截至目前,这是一个活跃的领域,因此请务必参考 OCI 维护的当前列表 以获取最新信息。值得注意的低级运行时项目包括 crun,由 Red Hat 领导的 C 实现;以及 railcar,由 Oracle 领导的 Rust 实现,尽管该项目现在已经存档。
制定规范是一项具有挑战性的任务,参与 OCI 运行时规范的协作同样具有挑战性。在发布 1.0 版本之前,花费了不少时间来确定边界——规范中应该包括什么内容和不应该包括什么内容。然而,显而易见的是,仅仅实现 OCI 运行时规范并不足以推动实施的采用。我们需要额外的功能来使低级运行时对开发者更加可用,因为我们关注的远不止容器的启动和运行。
这将我们引向更高级的运行时,如 containerd 和 cri-o,这两个是当前的主要解决方案,涵盖了许多围绕容器编排的关注点,包括镜像管理和分发。这两个运行时都实现了 CRI(这简化了 Kubernetes 部署的路径),并将低级容器活动委托给符合 OCI 标准的低级运行时(例如 runC)。
您的机器上的 Docker
容器的第二个重要理解点是它们并非魔法。容器利用了现有的 Linux 特性(如本章开头所述)。容器的具体实现细节有所不同,但容器镜像本质上就是一个完整文件系统的 tar 压缩包,而运行中的容器则是一个受限的 Linux 进程,从而与主机上运行的其他进程隔离开来。例如,Docker 容器的实现主要涉及以下三个要素:
-
命名空间
-
cgroups
-
联合文件系统
但是容器在本地文件系统上是什么样子的呢?首先,让我们弄清楚 Docker 在开发机器上的存储位置。然后让我们来看一看从 Docker Hub 拉取的真实 Docker 镜像。
安装 Docker Desktop 后,从终端运行 docker info
命令将为您提供关于安装的详细信息。此输出包括关于镜像和容器存储位置的信息,标签为 Docker Root Dir
。下面是示例输出(为简洁起见进行了截断),指示 Docker 根目录为 /var/lib/docker:
$ docker info
Client:
Context: default
Debug Mode: false
Plugins:
app: Docker App (Docker Inc., v0.9.1-beta3)
buildx: Build with BuildKit (Docker Inc., v0.5.1-docker)
compose: Docker Compose (Docker Inc., 2.0.0-beta.1)
scan: Docker Scan (Docker Inc., v0.8.0)
Server:
Containers: 5
Running: 0
Paused: 0
Stopped: 5
Images: 62
Server Version: 20.10.6
Storage Driver: overlay2
…
Docker Root Dir: /var/lib/docker
…
这个结果来自 macOS Big Sur 上已有的 Docker Desktop(版本 3.3.3)安装。快速列出 /var/lib/docker 显示如下内容:
$ ls /var/lib/docker
ls: /var/lib/docker: No such file or directory
根据前面的输出,系统上有 5 个停止的容器和 62 个镜像,那么为什么这个目录不存在呢?输出有误吗?您可以查看另一个位置的镜像和容器存储位置,如 图 3-4,这是 Docker Desktop UI 的 macOS 版本中可用的“首选项”部分的截图。
然而,此位置完全不同。存在一个合理的解释,并且请注意,根据您的操作系统不同,您的安装可能略有不同。这个原因很重要,因为 Docker Desktop for Mac 需要在安装期间实例化一个 Linux 虚拟机来运行 Linux 容器。这意味着之前输出中提到的 Docker 根目录实际上是指向此 Linux 虚拟机内部的一个目录。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0304.png
图 3-4. Docker Desktop Preferences
但等等……如果您在 Windows 上怎么办?因为容器共享主机的操作系统,所以基于 Windows 的容器需要在 Windows 环境中运行,而基于 Linux 的容器需要在 Linux 环境中运行。Docker Desktop(版本 3.3.3)相比早期版本(即 Docker Toolbox),无需安装额外的支持软件即可运行 Linux-based 容器是一大进步。在旧版本中,要在 Mac 上运行 Docker,您需要安装像 VirtualBox 和 boot2docker 这样的软件才能如预期地启动和运行。今天,Docker Desktop 在幕后处理所需的虚拟化。Docker Desktop 还通过 Windows 10 上的 Hyper-V 支持 Windows 容器,以及通过 Windows Subsystem for Linux 2(WSL 2)在 Windows 10 上支持 Linux 容器。然而,要在 macOS 上运行 Windows 容器,仍然需要 VirtualBox。
现在你知道我们需要访问 Linux 虚拟机才能进入这个 Docker 根目录,让我们使用命令 docker pull *IMAGE NAME*
拉取一个 Docker 镜像,并看看它在文件系统中的样子:
$ docker pull openjdk
Using default tag: latest
latest: Pulling from library/openjdk
5a581c13a8b9: Pull complete
26cd02acd9c2: Pull complete
66727af51578: Pull complete
Digest: sha256:05eee0694a2ecfc3e94d29d420bd8703fa9dcc64755962e267fd5dfc22f23664
Status: Downloaded newer image for openjdk:latest
docker.io/library/openjdk:latest
命令 docker images
列出了所有本地存储的镜像。从输出中可以看出,我们之前拉取的命令带来了带有标签 latest
的 openjdk 镜像。这是默认行为,但我们也可以指定特定的 openjdk 镜像版本,比如 docker pull openjdk:11-jre
:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
...
openjdk latest de085dce79ff 10 days ago 467MB
openjdk 11-jre b2552539e2dd 4 weeks ago 301MB
...
您可以通过使用 image ID 运行 **docker inspect**
命令来了解最新的 openjdk 镜像的更多详细信息:
$ docker inspect de085dce79ff
[
{
"Id": "sha256:de085dce79ff...",
"RepoTags": [
"openjdk:latest"
],
...
"Architecture": "amd64",
"Os": "linux",
"Size": 467137618,
"VirtualSize": 467137618,
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/581137...ca8c47/diff:/var
/lib/docker/overlay2/7f7929...8f8cb4/diff",
"MergedDir": "/var/lib/docker/overlay2/693641...940d82/merged",
"UpperDir": "/var/lib/docker/overlay2/693641...940d82/diff",
"WorkDir": "/var/lib/docker/overlay2/693641...940d82/work"
},
"Name": "overlay2"
},
"RootFS": {
"Type": "layers",
"Layers": [
"sha256:1a3adb4bd0a7...",
"sha256:046fa1e6609c...",
"sha256:a8a84740beab..."
]
},
...
docker inspect
命令会输出大量有趣的信息。但我想在这里强调的是 GraphDriver
部分,其中包含属于此镜像的所有层所在的路径。
Docker 镜像由对应于 Dockerfile 中用于构建镜像的指令的层组成。这些层被转换为目录,并且可以在不同镜像之间共享以节省空间。
注意 LowerDir、MergedDir 和 UpperDir 部分。LowerDir 部分包含用于构建原始镜像的所有目录或层,这些层是只读的。UpperDir 目录包含容器运行时修改的所有内容。如果需要对 LowerDir 中的只读层进行修改,则会将该层复制到 UpperDir 中,然后可以对其进行写入操作。这称为写时复制操作。
重要的是要记住 UpperDir 中的数据是临时数据,仅在容器存在期间有效。事实上,如果您有意保留的数据,应该利用 Docker 的卷特性并挂载一个即使容器停止后仍然存在的位置。例如,运行在容器中的数据库驱动应用程序可能会利用挂载到容器的卷来存储数据库数据。
最后,MergedDir 部分有点像虚拟目录,它将 LowerDir 和 UpperDir 中的所有内容合并在一起。联合文件系统的工作方式是,任何编辑后复制到 UpperDir 的层将覆盖 LowerDir 中的层。
警告
注意所有对 /var/lib/docker 目录的引用,这是 Docker 的根目录。如果您监控此目录的大小,会发现随着创建和运行的镜像和容器数量增加,该目录所需的存储空间会显著增加。考虑挂载一个专用驱动器,并确保定期清理未使用的镜像和容器。此外,确保容器化应用程序不会持续产生未管理的数据文件或其他工件。例如,可以使用日志传送或日志轮换来管理容器及其运行进程生成的日志。
可以使用相同的镜像启动任意数量的容器。每个容器将以镜像蓝图创建并独立运行。在 Java 的上下文中,将容器镜像视为 Java 类,将容器视为从该类实例化的 Java 对象。
可以停止并稍后重新启动容器,而无需重新创建。要列出系统上的容器,请使用 docker ps -a
命令。请注意,-a
标志将显示已停止的容器以及当前正在运行的容器:
$ docker ps -a
CONTAINER ID IMAGE COMMAND STATUS NAMES
9668ba978683 openjdk "tail -f" Up 19 seconds vibrant_jang
582ad818a57b openjdk "jshell" Exited (0) 14 minutes ago zealous_wilson
如果您导航到 Docker 的根目录,您会看到一个名为 containers 的子目录。在这个目录中,您会找到根据系统上每个容器的 container ID 命名的额外子目录。停止的容器将在这些目录中保留其状态和数据,以便在需要时可以重新启动。当使用 docker rm *CONTAINER NAME*
删除容器时,相应的目录将被删除。
警告
记得定期清理系统中未使用的容器(删除而不仅仅是停止)。我亲眼见证了缺少这个部署过程的情况。每次发布新镜像时,旧容器都会停止,基于新镜像启动新容器。这是一个疏忽,很快就会消耗硬盘空间,最终阻止新的部署。以下 Docker 命令可以批量清理未使用的容器:
docker container prune
docker-desktop:~# ls /var/lib/docker/
builder containers overlay2 swarm volumes
buildkit image plugins tmp
containerd network runtimes trust
docker-desktop:~# ls /var/lib/docker/containers/
9668ba978683b37445defc292198bbc7958da593c6bb3cef6d7f8272bbae1490
582ad818a57b8d125903201e1bcc7693714f51a505747e4219c45b1e237e15cb
注意
如果你在 Mac 上进行开发,请记住你的容器运行在一个小型虚拟机中,你需要首先访问该虚拟机,然后才能查看 Docker 根目录的内容。例如,在 Mac 上,您可以通过以特权模式交互式运行一个已安装nsenter的容器来访问和导航到此目录(可能需要使用sudo
运行):
docker run -it --privileged --pid=host debian \
nsenter -t 1 -m -u -n -i sh
较新的 Windows 版本(10+)现在具有使用 Windows Subsystem for Linux(WSL)原生运行 Linux 容器的功能。在文件资源管理器中可以找到 Windows 11 Home 的默认 Docker 根目录:
*\wsl.localhost\docker-desktop-data\version-pack-data* *community\docker*
基本标记和镜像版本管理
使用镜像一段时间后,您会发现其标识和版本管理与您对 Java 软件版本化的方式有所不同。使用像 Maven 这样的构建工具已经让大多数 Java 开发者习惯于标准的语义版本控制,并始终指定依赖版本(或者至少接受 Maven 在特定依赖树中选择的版本)。这些限制措施在其他包管理器(如 npm)中稍微放松,其中可以将依赖版本指定为范围,以便轻松和灵活地更新依赖项。
如果不理解,镜像版本管理可能会成为一个障碍。没有(至少不是 Java 开发者习惯的那种)限制措施。在标记镜像方面的灵活性优于任何强制执行的良好实践。然而,仅仅因为你能做到,并不意味着你应该这样做,就像正确版本化 Java 库和包一样,最好从一开始就采用符合逻辑和遵循公认模式的命名和版本化方案。
容器镜像名称和版本遵循特定的格式,包括多个组件,这些在示例和教程中很少以完整形式出现。大多数在互联网上找到的示例代码和 Dockerfile 都使用缩写格式标识镜像。
将图像管理视为目录结构最为直观,其中图像的名称(例如openjdk)是包含此图像所有可用版本的目录。通常使用图像的名称和版本,称为标签来标识图像。但这两个组件由子组件组成,如果未指定,则具有默认值,并且通常在命令中甚至会省略标签。例如,拉取openjdk Docker 图像的最简单命令可能采用以下形式:
docker pull openjdk
这个命令实际上给我们带来了什么?openjdk图像有几个版本可供选择?确实有,如果您关注可重复构建,您会立即注意到此模糊性可能是一个问题。
第一步是在此命令中包含图像标签,表示一个版本。以下命令意味着我将拉取openjdk图像的版本 11:
docker pull openjdk:11
那么之前我拉取的是什么,如果不是 11?如果未指定标签,默认情况下会隐含使用特殊标签latest
。此标签旨在指向可用图像的最新版本,但情况并非总是如此。任何时候,都可以更新标签以指向图像的不同版本,并且在某些情况下,您可能会发现标签latest
根本未设置指向任何内容。
同样容易出错的是命名规则,特别是标签,在不同的上下文中可能意味着不同的东西。术语标签可以指代特定版本,也可以指代包括所有标识组件在内的完整图像标签,包括图像名称。
下面是包含所有可能组件的 Docker 图像标签的完整格式:
[ *registry* [ :*port* ] / ] *name* [ :*tag* ]
唯一必需的组件是图像的名称,也称为图像仓库。如果未指定标签,则假定为latest。如果未指定注册表,则 Docker Hub 是默认注册表。以下命令是如何引用不同于 Docker Hub 上的注册表中的图像的示例:
docker pull artifactory-prod.jfrog.io/openjdk:11
图像和容器层
要构建高效的容器,深入理解层的重要性至关重要。构建容器源代码(即容器图像)的详细信息极大地影响其大小和性能,并且一些方法还涉及安全问题,使这一概念变得更加重要。
基本上,Docker 镜像是通过建立基础层,然后逐步进行小的更改,直到达到所需的最终状态而构建的。每个图层代表一组更改,包括但不限于创建用户及相关权限、修改配置或应用程序设置,以及更新现有软件包或添加/删除软件包。这些更改都是对最终文件系统中文件集的添加、修改或移除。图层叠加在彼此之上,每个图层都是从前一个图层的更改增量,并由其内容的 SHA-256 哈希摘要进行标识。如“你的机器上的 Docker”所讨论的那样,这些图层存储在根 Docker 目录中。
可视化图层
一个真正可视化图层的好方法是使用命令行工具dive
,该工具可在GitHub上找到。图 3-5 展示了使用从 Docker Hub 拉取的官方最新openjdk镜像运行该工具的屏幕截图。左侧窗格显示了组成openjdk镜像的三个图层的详细信息。右侧窗格突出显示了每个图层对镜像文件系统应用的更改。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0305.png
图 3-5. dive
与 openjdk
dive
工具在展示基于openjdk镜像启动容器时文件系统的外观时非常有用。随着你浏览每个后续图层,你可以看到对初始文件系统所做的更改。这里最重要的部分是,后续的图层可能会混淆前一个图层文件系统的部分(如果有文件的移动或删除),但原始图层仍以其原始形式存在。
利用层缓存
利用图像层可以加快图像请求、构建和推送速度。这是减少图像存储所需空间的巧妙方法。这种策略允许多个图像共享相同的图像层,并减少本地缓存或存储在注册表中的图像拉取或推送所需的时间和带宽。
如果你使用 Docker,系统将保留你从外部注册表请求或自行构建的所有图像的内部缓存。在推送和拉取新图像时,会在本地缓存和注册表之间比较每个图像层,并决定是推送还是拉取单个图层,以提高效率。
任何曾经与内部 Maven 仓库(我们都曾在某个时刻吧?)或任何缓存机制(如此类推)挣扎过的人都非常清楚,内部缓存提供的效率和性能改进也伴随着注意事项。有时候你在缓存中存储的并不是你想要使用的内容。在活跃的开发和本地测试中,如果不注意如何及何时使用本地镜像缓存,很容易出现使用过期缓存的情况。
例如,命令 docker run openjdk
和 docker pull openjdk
在涉及缓存时表现不同。前者在本地缓存中查找指定标签为 latest
的镜像。如果镜像存在,搜索将被视为满足,将启动一个基于缓存镜像的新容器。后者的命令将进一步更新来自远程注册表中存在更新的 openjdk 镜像。
另一个常见的错误是假设 Dockerfile 中的命令在重新构建镜像时会再次运行。这在 RUN
命令中尤为常见,如 RUN apt-get update
。如果 Dockerfile 中的这行代码根本没有变化,比如你未指定包名及其具体版本,那么包含此命令的初始层将存在于缓存中,不会重新构建。这不是错误,而是缓存的一种特性,用于加快构建过程。如果确定层已构建,则不会重新构建该层。
为了避免过期缓存,你可能会试图在 Dockerfile 中将多个命令组合成一行(生成一个层),以便更容易识别和更频繁地执行更改。但这种方法的问题在于,如果把太多内容压缩成一个层,将完全失去缓存的好处。
提示
作为开发人员,要注意本地缓存。除了本地开发外,还要考虑持续集成、构建服务器和自动集成测试如何使用缓存。确保所有系统在这方面的一致性将有助于避免不明原因和间歇性的失败。
最佳镜像构建实践和容器注意事项
在构建和使用镜像时,你会发现即使是最基本的构建过程也可能在许多地方给自己制造麻烦。以下是一些在开始镜像构建旅程时要牢记的实践方法。你会发现更多内容,但这些是最重要的。
尊重 Docker 上下文和 .dockerignore 文件。
不希望将开发环境配置、密钥、.git目录或其他敏感隐藏目录包含在生产 Docker 镜像中。构建 Docker 镜像时,需提供上下文,即要在构建过程中提供文件的位置。
以下是一个虚构的 Dockerfile 示例:
FROM ubuntu
WORKDIR /myapp
COPY . /myapp
EXPOSE 8080
ENTRYPOINT ["start.sh"]
看到COPY
指令了吗?取决于您作为上下文发送的内容,这可能会有问题。它可能会将所有工作目录中的内容复制到您构建的 Docker 映像中,并最终出现在从此映像启动的任何容器中。
确保使用*.dockerignore*文件来排除上下文中不希望无意中出现的文件。您可以使用它来避免意外添加任何本地存储的用户特定文件或机密信息。事实上,通过排除构建不需要访问的任何内容,您可以大大减少上下文的大小(以及构建所需的时间):
# Ignore these files in my project
**/*.md
!README.md
passwords.txt
.git
logs/
*/temp
**/test/
.dockerignore匹配格式遵循Go 的filepath.Match
规则。
使用可信的基础映像
无论您选择使用包含 OpenJDK、Oracle JDK、GraalVM 或其他包含 Web 服务器或数据库的映像,确保使用可信的映像作为父映像,或者从头开始创建您自己的映像。
Docker Hub 自称是全球最大的公共可用容器映像库,拥有来自软件供应商、开源项目和社区的超过 100,000 个映像。并非所有这些映像都应该信任用作基础映像。
Docker Hub 包含一组经策划的映像,标记为“Docker 官方映像”,适合用作基础映像(请注意,分发这些映像需要与 Docker 的协议达成一致)。这些详细信息来自官方映像的在线 Docker 文档:
Docker, Inc.赞助了一个专门的团队,负责审查和发布 Docker 官方映像中的所有内容。该团队与上游软件维护者、安全专家以及更广泛的 Docker 社区合作。
与了解 Java 依赖项引入到您的项目及依赖树深度同样重要的是,了解您的基础映像在 Dockerfile 顶部的那一行FROM
中带入了什么。Dockerfile 的继承结构很容易掩盖基础映像在其后带入的额外库和包,这些可能是您不需要的,甚至可能是恶意内容。
指定包版本并跟上更新
鉴于前面讨论过的有关缓存的警告以及保持可重复构建的愿望,像在 Java 项目中一样在您的 Dockerfile 中指定版本。避免由新版本或意外更新引起的构建失败和意外行为。
尽管如此,如果由于构建失败或测试而强迫您查看版本,很容易对更新版本变得漠不关心。定期审计项目以获取所需的更新,并使这些更新成为有意义的。这应该成为您常规项目规划的一部分。我建议将这种活动与任何其他特性开发或错误修复分开,以消除开发生命周期中无关的动态部分。
保持镜像小巧
镜像很容易变得非常大,速度很快。在自动化构建中监控大小的增加,并设置异常大小变化的通知。贪吃的磁盘存储包可以通过基础镜像的更新轻易悄然进入,或者无意中包含在COPY
语句中。
利用多阶段构建保持您的镜像小巧。可以通过创建一个使用多个FROM
语句的 Dockerfile 来设置多阶段构建,每个语句都使用不同的基础镜像开始构建阶段。通过使用多阶段构建,您可以避免在生产镜像中包含不需要的(实际上也不应该包含)构建工具或包管理器。例如,以下 Dockerfile 显示了一个两阶段构建。第一阶段使用包含 Maven 的基础镜像。在 Maven 构建完成后,所需的 JAR 文件被复制到第二阶段,该阶段使用不包含 Maven 的镜像:
###################
# First build stage
###################
FROM maven:3.8.4-openjdk-11-slim as build
COPY .mvn .mvn
COPY mvnw .
COPY pom.xml .
COPY src src
RUN ./mvnw package
####################
# Second build stage
####################
FROM openjdk:11-jre-slim-buster
COPY --from=build target/my-project-1.0.jar .
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "my-project-1.0.jar"]
这也是实现自定义distroless镜像的好方法,该镜像已经剥离了所有内容(包括 shell),只保留了运行应用程序的绝对必需品。
警惕外部资源
我经常看到在 Dockerfile 中请求外部资源的要求,形式为用于安装专有软件的wget
命令,甚至用于执行自定义安装的 shell 脚本的外部请求。这使我感到恐惧。这不仅仅是普通的怀疑和偏执。即使外部资源是可信的,当你把构建的控制权交给外部方之后,你更可能遭遇无法解决的构建失败。
当我指出这一观察时,我经常得到的第一个反应是:“不用担心,因为一旦构建了您的镜像,它就被缓存或存储在基础镜像中,您将永远不必再次发出请求。”
这是绝对真实的。一旦您存储了基础镜像或您的镜像层被缓存,您就可以放心了。但是,当新的构建节点(没有缓存)投入使用时,或者当新的开发人员加入您的团队时,构建该镜像可能会失败。当您需要构建基础镜像的新版本时,您的构建可能会失败。为什么?因为一次又一次地,资源的外部管理者会移动它们,限制对它们的访问,或者简单地丢弃它们。
保护你的机密
我包括这个因为除了一开始就不将机密移入您的映像之外,不要认为在 Dockerfile 中使用命令从基础映像或任何先前层中删除它们就足够好。我曾经见过这种情况,将其作为“修复”无法立即重建的基础层的黑客方式。
现在您了解了层次结构的工作原理,您知道后续层次删除项目并不实际从底层删除它们。如果您对基于该映像的运行容器执行exec
,则看不到它们,但它们仍然存在。它们存在于存储映像的系统上,它们存在于启动基于该映像的容器的任何地方,它们还存在于您选择的映像注册表中,以供长期存储。这几乎相当于将您的密码检入源代码控制中。一开始就不要将机密放入映像中。
了解您的输出
多种因素可能导致容器在运行时持续增长。其中最常见的之一是未适当处理日志文件。确保您的应用程序记录到一个卷中,您可以实施日志轮换解决方案。考虑到容器的临时性质,将用于故障排除或合规性的日志存储在容器内部(在 Docker 主机上)是没有意义的。
总结
本章的大部分内容是关于探索 Docker 的。这是一个开始的绝佳地方。一旦您熟悉了映像和容器,您可以扩展到生态系统中提供的其他工具。根据您选择的操作系统和项目构建工具,诸如Buildah,Podman或Bazel这样的工具可能非常适合您。您还可以选择使用像Jib这样的 Maven 插件来构建您的容器映像。
有一个警告:无论选择哪种工具,请了解您的映像和容器是如何构建的,以免在准备部署时遭受臃肿和/或不安全的映像和容器的后果。
第四章:解构单体架构
伊什切尔·鲁伊兹
终极目标应该是通过数字创新改善人类生活的质量。
马化腾
通过历史,人类一直着迷于将想法和概念分解为简单或复合部分。通过分析和综合的结合,我们可以达到更高层次的理解。
亚里士多德称分析为“将每个复合物分解成组成这些合成物的要素。因为分析是综合的反面。综合是从原则到由原则派生的事物的道路,而分析是从终点返回到原则。”
软件开发遵循类似的方法:将系统分析为其组成部分,识别输入、期望输出和详细功能。在软件开发的分析过程中,我们意识到,非特定于业务的功能总是需要来处理输入,并通信或持久化输出。这使得明显的是,我们可以从可重复使用的、明确定义的、上下文绑定的原子功能中受益,这些功能可以被共享、消费或互连,以简化软件构建。
允许开发人员主要专注于实现业务逻辑,以满足客户/企业的明确定义的需求,满足一些潜在用户集的感知需求,或者使用功能来满足个人需求(自动化任务)一直是长期以来的愿望。每天都浪费太多时间在重新发明最常见的可靠样板代码。
近年来,微服务模式因承诺的优势而声名鹊起并获得动力。在实现这种架构模式的好处的同时,减少采用它的缺点,避免已知的反模式,采用最佳实践,并理解核心概念和定义至关重要。本章涵盖了微服务的反模式,并包含了使用 Spring Boot、Micronaut、Quarkus 和 Helidon 等流行微服务框架编写的代码示例。
传统上,单体架构提供或部署单个单元或系统,从单一源应用程序满足所有需求,可以识别出两个概念:单体应用程序 和 单体架构。
单体应用程序 有 唯一的 部署实例,负责执行特定功能所需的所有步骤。这种应用程序的一个特征是独特的执行接口点。
单块架构指的是所有需求均由单一来源处理,并且所有部分作为一个单元交付的应用程序。组件可能被设计为限制与外部客户的交互,以显式限制私有功能的访问。单块中的组件可能是相互连接或相互依赖的,而不是松散耦合的。换句话说,从外部或用户的视角来看,对其他独立组件的定义、接口、数据和服务知之甚少。
粒度是一个组件向软件的其他外部合作或协作部分公开的聚合级别。软件的粒度水平取决于几个因素,例如必须在一系列组件内保持的机密级别,不可暴露或对其他消费者可用。
现代软件架构越来越专注于通过捆绑或组合来自不同来源的软件组件来提供功能,这导致或强调了详细级别上的更细粒度。因此,向不同组件、客户或消费者公开的功能要比单块应用程序更多。
要确定一个模块有多独立或可互换,我们应该仔细看以下特征:
-
依赖的数量
-
这些依赖的强度
-
它所依赖的模块的稳定性
对前述特征赋予的任何高分应触发对模块建模和定义的第二次审查。
云计算
云计算有多个定义。彼得·梅尔(Peter Mell)和蒂姆·格兰斯(Tim Grance)将其定义为一种模型,用于实现对共享可配置计算资源池(如网络、服务器、存储、应用程序和服务)的无处不在、方便、按需网络访问,可以快速配置和释放,几乎不需要管理工作或与服务提供商的互动。
近年来,云计算有了显著增长。例如,2020 年第四季度云基础设施服务支出增长了 32%,达到了 399 亿美元。总支出比上一季度高出 30 亿美元,比 2019 年第四季度高出近 100 亿美元,据Canalys 数据显示。
存在多家提供商,但市场份额并不均匀分布。三家领先的服务提供商是亚马逊网络服务(AWS)、微软 Azure 和谷歌云。AWS 是领先的云服务提供商,在 2020 年第四季度占据了总支出的 31%。Azure 的增长率加快,增长了 50%,市场份额接近 20%,而谷歌云占据了总市场的 7%。
云计算服务的利用一直存在滞后。Cinar Kilcioglu 和 Aadharsh Kannan 在 2017 年在“第 26 届国际万维网会议”上报告,数据中心中云资源的使用显示出租户配置和支付的资源(租用 VM)与实际资源利用(CPU、内存等)之间存在显著差距。也许客户只是将他们的 VM 保持开启,但实际上并没有使用它们。
云服务根据用于不同类型计算的类别进行划分:
软件即服务(SaaS)
客户可以使用提供者在云基础设施上运行的应用程序。这些应用程序可以通过薄客户端接口(如 Web 浏览器)或程序接口从各种客户端设备访问。客户不管理或控制底层云基础设施,包括网络、服务器、操作系统、存储甚至单个应用程序功能,但可能有限制的特定于用户的应用程序配置设置。
平台即服务(PaaS)
客户可以将使用由提供者支持的编程语言、库、服务和工具创建的客户制作或购买的应用程序部署到云基础设施。消费者不管理或控制底层云基础设施,包括网络、服务器、操作系统或存储,但可以控制已部署的应用程序,可能还可以配置应用程序托管环境的设置。
基础设施即服务(IaaS)
客户能够配置处理、存储、网络和其他基本计算资源。他们可以部署和运行任意软件,其中包括操作系统和应用程序。客户不管理或控制底层云基础设施,但可以控制操作系统、存储和部署的应用程序,并可能对某些网络组件有限的控制。
微服务
微服务 这个术语并不是最近才出现的。彼得·罗杰斯在 2005 年提出了微网络服务 这个术语,同时倡导软件即微网络服务 这一概念。微服务架构 ——作为面向服务架构(SOA)的一种演变——将应用程序组织为一组相对轻量级的模块化服务。从技术上讲,微服务是 SOA 实现方法的一种特殊化。
微服务 是小型且松散耦合的组件。与单体应用程序相比,它们可以独立部署、扩展和测试,具有单一职责,由上下文界定,是自治的和分散的。它们通常围绕业务能力构建,易于理解,并可以使用不同的技术栈进行开发。
一个微服务应该有多小?它应该足够微小,以允许小型、自包含和严格执行的功能原子共存、发展或替换前一个版本,以适应业务需求。
每个组件或服务几乎不了解其他独立组件的定义,与服务的所有交互都通过其 API 进行,该 API 封装了其实现细节。这些微服务之间的消息传递使用简单的协议,通常不需要大量数据。
反模式
微服务模式导致了显著的复杂性,并非在所有情况下都是理想的。该系统由许多独立工作的部分组成,其本质使其更难以预测在现实世界中的表现如何。
这种增加的复杂性主要是由于(潜在的)成千上万的微服务在分布式计算机网络中异步运行。请记住,难以理解的程序也难以编写、修改、测试和衡量。所有这些问题都将增加团队理解、讨论、跟踪和测试接口和消息格式所需的时间。
关于这个特定主题有几本书籍、文章和论文可供参考。我推荐访问Microservices.io、马克·理查兹(Mark Richards)的报告Microservices AntiPatterns and Pitfalls(O’Reilly)以及 2018 年大卫德比(Davide Taibi)和瓦伦蒂娜·莱纳杜茨(Valentina Lenarduzz)在《IEEE 软件》上发表的“关于微服务坏味道定义”的论文。
一些最常见的反模式包括以下内容:
API 版本控制(静态协议陷阱)
API 需要进行语义版本控制,以允许服务知道它们是否正在与正确版本的服务通信,或者是否需要调整其通信以适应新的协议。
不当的服务隐私依赖性
微服务需要其他服务的私密数据而不是处理自己的数据,这通常与数据建模问题有关。可以考虑的解决方案之一是合并这些微服务。
多用途巨型服务
几个业务功能被实现在同一个服务中。
记录
错误和微服务信息被隐藏在每个微服务容器内。在软件生命周期的各个阶段发现问题时,应优先采用分布式日志记录系统。
复杂的服务间或循环依赖
循环服务关系 被定义为两个或多个相互依赖的服务之间的关系。循环依赖可能会损害服务扩展或独立部署的能力,并违反无环依赖原则(ADP)。
缺失的 API 网关
当微服务直接相互通信,或者当服务消费者直接与每个微服务通信时,系统的复杂性增加,维护减少。在这种情况下的最佳实践是使用 API 网关。
一个 API 网关 接收来自客户端的所有 API 调用,然后通过请求路由、组合和协议转换将它们引导到适当的微服务。网关通常通过调用多个微服务并聚合结果来处理请求,以确定最佳路由。它还能够在内部使用之间进行 Web 协议和 Web 友好协议之间的转换。
应用程序可以使用 API 网关为移动客户端提供一个单一的端点,通过单个请求查询所有产品数据。API 网关整合了各种服务,如产品信息和评论,并将结果合并和公开。
API 网关是应用程序访问数据、业务逻辑或功能(RESTful API 或 WebSocket API)的门卫,允许实时双向通信应用程序。API 网关通常处理接受和处理多达数十万个并发 API 调用的所有任务,包括流量管理、跨源资源共享(CORS)支持、授权和访问控制、阻塞、管理和 API 版本控制。
过度共享
在分享足够的功能以避免重复自己与创建依赖混乱的纠结之间,有一条薄线阻止了服务变更分离。如果需要更改过度共享的服务,评估接口的建议变更最终会导致一个涉及更多开发团队的组织任务。
在某些时候,需要分析是否将冗余或库提取到新的共享服务中,这些相关的微服务可以独立安装和开发。
DevOps 和 微服务
微服务完美地符合 DevOps 理念,利用小团队逐步对企业服务进行功能更改——将大问题分解成小片段并系统化处理的理念。为了减少开发、测试和部署之间的摩擦,必须存在一系列持续交付管道,以保持这些阶段的稳定流动。
DevOps 是这种架构风格成功的关键因素,提供必要的组织变更,以最小化负责每个组件的团队之间的协调,并消除开发和运营团队之间有效互动的障碍。
警告
我强烈反对任何团队在没有健全的 CI/CD 基础设施或对流水线基本概念没有广泛理解的情况下采用微服务模式。
微服务框架
JVM 生态系统庞大且提供了许多特定用例的替代方案。提供了几十种微服务框架和库,以至于在候选项中选择优胜者可能有些棘手。
话虽如此,由于几个原因,某些候选框架已经获得了流行:开发者体验、上市时间、可扩展性、资源(CPU、内存)消耗、启动速度、故障恢复、文档、第三方集成等等。这些框架——Spring Boot、Micronaut、Quarkus 和 Helidon——在接下来的章节中进行了介绍。请注意,一些说明可能需要根据更新版本进行额外调整,因为其中一些技术正在快速发展。我强烈建议查阅每个框架的文档。
此外,这些示例需要至少 Java 11,并且尝试使用本地镜像还需要安装 GraalVM。有许多方法可以在您的环境中安装这些版本。我建议使用SDKMAN!来安装和管理它们。为简洁起见,我专注于生产代码——一个单一框架可以填写整本书!毫无疑问,您还应该关注测试。每个示例的目标是构建一个简单的“Hello World” REST 服务,该服务可以接受一个可选的名称参数并回复问候语。
如果您以前没有使用过 GraalVM,它是一个涵盖几种技术的综合项目,使以下功能成为可能:
-
一个用 Java 编写的即时编译器(JIT),可以在运行时编译代码,将解释代码转换为可执行代码。Java 平台已经有过几个 JIT,大多数是用 C 和 C++组合编写的。Graal 碰巧是最现代的一个,用 Java 编写。
-
Substrate VM 是一个虚拟机,能够在 JVM 之上运行托管语言,如 Python、JavaScript 和 R,使得托管语言能够更紧密地集成 JVM 的能力和特性。
-
本地镜像是一种依赖预编译(AOT)的实用工具,将字节码转换为机器可执行代码。所得的转换产生一个特定于平台的二进制可执行文件。
这里介绍的四个候选框架都以某种方式支持 GraalVM,主要依赖于 GraalVM Native Image 来生成特定于平台的二进制文件,旨在减少部署大小和内存消耗。请注意,在使用 Java 模式和 GraalVM Native Image 模式之间存在权衡。后者可以生成具有较小内存占用和更快启动时间的二进制文件,但需要较长的编译时间;长时间运行的 Java 代码最终会变得更加优化(这是 JVM 的关键特性之一),而原生二进制文件在运行时无法进行优化。开发体验也各不相同,您可能需要使用额外的工具进行调试、监控、测量等。
Spring Boot
Spring Boot 可能是这四个候选框架中最为人熟知的,因为它建立在 Spring Framework 所奠定的遗产之上。如果开发者调查结果可信,超过 60% 的 Java 开发者在与 Spring 相关的项目中有一定的经验,使 Spring Boot 成为最受欢迎的选择。
Spring 的方式允许您通过组合现有组件、定制其配置并承诺低成本的代码拥有权来组装应用程序(或在我们的情况下是微服务),因为您的自定义逻辑理论上应比框架提供的内容更小,对于大多数组织来说这是正确的。关键是找到一个可以在编写自己的组件之前进行调整和配置的现有组件。Spring Boot 团队着重于添加所需的多个有用集成,从数据库驱动程序到监控服务、日志记录、日志处理、批处理、报告生成等等。
启动 Spring Boot 项目的典型方式是浏览至 Spring Initializr,选择您在应用程序中需要的功能,然后单击生成按钮。此操作将创建一个 ZIP 文件,您可以将其下载到本地环境以开始使用。在 图 4-1 中,我选择了 Web 和 Spring Native 功能。第一个功能添加了组件,使您可以通过 REST API 公开数据;第二个功能增强了构建,使用 Graal 可以创建额外的打包机制,生成原生镜像。
在项目的根目录解压 ZIP 文件并运行./mvnw verify
命令,确保一个健康的起点。如果您之前没有在目标环境上构建过 Spring Boot 应用程序,您会注意到该命令会下载一组依赖。这是正常的 Apache Maven 行为。除非在 pom.xml 文件中更新了依赖版本,否则下次调用 Maven 命令时不会再次下载这些依赖。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0401.png
图 4-1. Spring Initializr
项目结构应该是这样的:
.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── demo
│ │ ├── DemoApplication.java
│ │ ├── Greeting.java
│ │ └── GreetingController.java
│ └── resources
│ ├── application.properties
│ ├── static
│ └── templates
└── test
└── java
我们当前的任务需要两个未由 Spring Initializr 网站创建的附加源:Greeting.java 和 GreetingController.java。这两个文件可以使用您选择的文本编辑器或 IDE 创建。首先,Greeting.java 定义了一个数据对象,将用于将内容呈现为 JavaScript 对象表示法(JSON),这是一种通过 REST 公开数据的典型格式。还支持其他格式,但是 JSON 支持无需任何额外的依赖项即可直接使用。此文件应如下所示:
package com.example.demo;
public class Greeting {
private final String content;
public Greeting(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
除了它是不可变的数据持有者之外,这个数据持有者没有什么特别之处;根据您的用例,您可能希望切换到可变的实现,但目前这样就足够了。接下来是 REST 端点本身,定义为 /greeting 路径上的一个GET
调用。Spring Boot 更偏爱 controller 的原型来创建这种组件,毫无疑问是在回顾 Spring MVC(是的,那就是模型-视图-控制器)作为首选选项来创建 Web 应用程序的日子里。可以随意使用不同的文件名,但是组件的注解必须保持不变:
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private static final String template = "Hello, %s!";
@GetMapping("/greeting")
public Greeting greeting(@RequestParam(value = "name",
defaultValue = "World") String name) {
return new Greeting(String.format(template, name));
}
}
控制器可以接受 name
参数作为输入,并在未提供此参数时使用值 World
。请注意,映射方法的返回类型是一个普通的 Java 类型;这是我们在前一步中定义的数据类型。Spring Boot 将根据应用于控制器及其方法的注解和设置的合理默认值,自动将数据从 JSON 格式转换为 JSON 格式。如果我们保持代码不变,greeting()
方法的返回值将自动转换为 JSON 负载。这是 Spring Boot 开发经验的威力,依赖于可以根据需要进行微调的默认值和预定义的配置。
您可以通过调用 /.mvnw spring-boot:run
命令来运行应用程序,该命令将在构建过程中运行应用程序,也可以通过生成应用程序 JAR 并手动运行它来运行应用程序,即 ./mvnw package
后跟 java -jar target/demo-0.0.1.SNAPSHOT.jar
。无论哪种方式,都会启动一个内嵌的 Web 服务器,监听 8080 端口; /greeting 路径将映射到 GreetingController 的一个实例。现在只剩下发出一些查询,比如以下内容:
// using the default name parameter
$ curl http://localhost:8080/greeting
{"content":"Hello, World!"}
// using an explicit value for the name parameter
$ curl http://localhost:8080/greeting?name=Microservices
{"content":"Hello, Microservices!"}
在运行应用程序时,请注意应用程序生成的输出。在我的本地环境中,它显示(平均)JVM 启动需要 1.6 秒,而应用程序初始化需要 600 毫秒。生成的 JAR 文件大小大约为 17 MB。您可能还想记录这个微不足道的应用程序的 CPU 和内存消耗。有人建议使用 GraalVM Native Image 可以减少启动时间和二进制文件大小。让我们看看如何在 Spring Boot 中实现这一点。
记得当项目创建时我们选择了 Spring Native 特性吗?不幸的是,到了 2.5.0 版本,生成的项目在pom.xml文件中并未包含所有必需的指令。我们需要进行一些调整。首先,由spring-boot-maven-plugin
创建的 JAR 文件需要一个分类器;否则,生成的本地镜像可能无法正确创建。这是因为应用程序 JAR 文件已经包含了所有依赖项,位于 Spring Boot 特定路径下,这个路径并不被native-image-maven-plugin
处理,我们也需要进行配置。更新后的pom.xml文件应该如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<spring-native.version>0.10.0-SNAPSHOT</spring-native.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>${spring-native.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>${spring-native.version}</version>
<executions>
<execution>
<id>test-generate</id>
<goals>
<goal>test-generate</goal>
</goals>
</execution>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
<profiles>
<profile>
<id>native-image</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>native-image-maven-plugin</artifactId>
<version>21.1.0</version>
<configuration>
<mainClass>
com.example.demo.DemoApplication
</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>native-image</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
在我们尝试之前还有一步:确保安装了与pom.xml文件中找到的native-image-maven-plugin
版本接近的 GraalVM 版本作为您当前的 JDK。还必须在系统中安装native-image
可执行文件;您可以通过调用gu install native-image
来完成。gu
命令由 GraalVM 安装提供。
当所有设置就绪后,我们可以通过调用./mvnw -Pnative-image package
生成一个本地可执行文件。你会注意到屏幕上会有大量的文本输出,可能会下载新的依赖项,并且可能会有一些关于缺少类的警告——这是正常的。构建时间也比平时长,这就是这种打包解决方案的权衡所在:我们增加了开发时间以加快生产环境中的执行时间。一旦命令完成,您会注意到在target目录中出现了一个名为com.example.demo.demoapplication的新文件。这就是本地可执行文件。继续运行它吧。
你注意到启动速度有多快了吗?在我的环境中,平均启动时间为 0.06 秒,而应用程序初始化需要 30 毫秒。你可能还记得在 Java 模式下运行时,这些数字分别为 1.6 秒和 600 毫秒。这真是一个严重的速度提升!现在看看可执行文件的大小;在我的情况下,大约是 78 MB。哦,看起来有些事情变得更糟了,或者说没有?这个可执行文件是一个单一的二进制文件,包含了运行应用程序所需的一切,而我们之前使用的 JAR 文件则需要 Java 运行时才能运行。Java 运行时的大小通常在 200 MB 左右,并由多个文件和目录组成。当然,可以使用jlink创建较小的 Java 运行时,这样在构建过程中会增加另一个步骤。没有免费的午餐。
现在我们暂停使用 Spring Boot,记住,它的功能远不止这里展示的。接下来我们看看下一个框架。
Micronaut
Micronaut 于 2017 年诞生,是对 Grails 框架的一次现代化重新构想。Grails 是少数成功的 Ruby on Rails(RoR)框架“克隆”之一,利用 Groovy 编程语言。Grails 在几年间引起了轰动,直到 Spring Boot 的兴起使其失去了关注,促使 Grails 团队寻找替代方案,最终推出了 Micronaut。从表面上看,Micronaut 提供了与 Spring Boot 类似的用户体验,因为它也允许开发人员基于现有组件和合理的默认设置来构建应用程序。
Micronaut 的一个关键区别在于使用编译时依赖注入来组装应用程序,而不是运行时依赖注入,这与目前使用 Spring Boot 组装应用程序的首选方式不同。这一看似微不足道的改变让 Micronaut 在运行时提升了速度,因为应用程序在启动时花费的时间更少;这也可以减少内存消耗,并且减少对 Java 反射的依赖,后者在直接方法调用之前的历史上速度较慢。
有几种启动 Micronaut 项目的方式,但首选的方法是浏览到 Micronaut Launch,选择您想要添加到项目中的设置和功能。默认的应用程序类型定义了构建基于 REST 的应用程序所需的最小设置,例如我们将在几分钟内讲解的内容。一旦满意您的选择,请点击“生成项目”按钮,如 图 4-2 所示,这将生成一个 ZIP 文件,可以下载到您的本地开发环境中。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0402.png
图 4-2. Micronaut 启动
与我们为 Spring Boot 所做的类似,解压 ZIP 文件并在项目根目录运行./mvnw verify
命令会确保一个良好的起点。如果一切顺利,此命令会根据需要下载插件和依赖项;如果构建成功,几秒钟后应该看到这样的输出。添加一对额外的源文件后,项目结构应如下所示:
.
├── README.md
├── micronaut-cli.yml
├── mvnw
├── mvnw.bat
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── example
│ └── demo
│ ├── Application.java
│ ├── Greeting.java
│ └── GreetingController.java
└── resources
├── application.yml
└── logback.xml
Application.java 源文件定义了入口点,暂时不需要更新。同样,我们也不需要更改 application.yml 资源文件;该资源提供的配置属性目前不需要更改。
我们需要另外两个源文件:由Greeting.java定义的数据对象,其责任是包含发送给消费者的消息,以及由GreetingController.java定义的实际 REST 端点。控制器原型追溯到 Grails 规范所制定的约定,并几乎被所有 RoR 克隆框架所遵循。您可以根据您的领域需求更改文件名,但必须保留@Controller
注解。数据对象的源代码应如下所示:
package com.example.demo;
import io.micronaut.core.annotation.Introspected;
@Introspected
public class Greeting {
private final String content;
public Greeting(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
对于这个类,我们再次依赖于不可变设计。请注意使用的@Introspected
注解,这会告诉 Micronaut 在编译时检查类型并将其包含为依赖注入过程的一部分。通常情况下,可以省略此注解,因为 Micronaut 会自动识别出需要的类。但是,在使用 GraalVM Native Image 生成本地可执行文件时,它的使用是至关重要的;否则,可执行文件将无法完整生成。第二个文件应该是这样的:
package com.example.demo;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.QueryValue;
@Controller("/")
public class GreetingController {
private static final String template = "Hello, %s!";
@Get(uri = "/greeting")
public Greeting greeting(@QueryValue(value = "name",
defaultValue = "World") String name) {
return new Greeting(String.format(template, name));
}
}
我们可以看到控制器定义了一个映射到/greeting
的单个端点,接受一个名为name
的可选参数,并返回数据对象的一个实例。默认情况下,Micronaut 会将返回值解析为 JSON,因此无需额外配置。应用程序的运行可以通过两种方式完成。您可以调用./mvnw mn:run
,将其作为构建过程的一部分运行应用程序,或者调用./mvnw package
,它会在target目录中创建一个名为demo-0.1.jar的文件,可以像传统方式一样启动它,即java -jar target/demo-0.1.jar
。调用 REST 端点进行一些查询可能会产生类似于以下输出:
// using the default name parameter
$ curl http://localhost:8080/greeting
{"content":"Hello, World!"}
// using an explicit value for the name parameter
$ curl http://localhost:8080/greeting?name=Microservices
{"content":"Hello, Microservices!"}
任何一个命令都能快速启动应用程序。在我的本地环境中,应用程序平均在 500 毫秒内准备好处理请求,这比 Spring Boot 的相同行为速度快三倍。JAR 文件的大小也小了一点,总共为 14 MB。尽管这些数字可能令人印象深刻,但如果使用 GraalVM Native Image 将应用程序转换为本地可执行文件,我们可以获得速度提升。幸运的是,Micronaut 的方式对这种设置更友好,生成的项目中已经配置了我们需要的一切。就这样。无需更新构建文件的其他设置,一切都已准备就绪。
您确实需要安装 GraalVM 及其native-image
可执行文件,就像我们以前做的那样。创建本地可执行文件只需调用./mvnw -Dpackaging=native-image package
,几分钟后,我们应该能得到一个名为demo
的可执行文件(事实上,这是项目的artifactId
,如果您想知道的话),位于target目录下。使用本地可执行文件启动应用程序平均启动时间为 20 毫秒,比 Spring Boot 快三分之一。可执行文件大小为 60 MB,与 JAR 文件的减小大小相对应。
让我们停止探索 Micronaut,并转向下一个框架:Quarkus。
Quarkus
尽管Quarkus在 2019 年初宣布,但其工作开始得早得多。Quarkus 与我们迄今看到的两个候选者有很多相似之处。它提供基于组件、约定大于配置和生产力工具的优秀开发体验。更重要的是,Quarkus 决定也采用像 Micronaut 一样的编译时依赖注入,使其能够获得同样的好处,如更小的二进制文件、更快的启动时间和更少的运行时魔法。同时,Quarkus 还添加了自己的风格和独特性,对某些开发人员来说可能最重要的是,Quarkus 更多地依赖于标准而不是其他两个候选者。Quarkus 实现了 MicroProfile 规范,这些规范来自于 JakartaEE(以前称为 JavaEE),并在 MicroProfile 项目的保护伞下开发了其他标准。
您可以访问Quarkus 配置您的应用程序页面开始使用 Quarkus,配置值并下载 ZIP 文件。此页面包含许多好东西,包括许多扩展选项,可用于配置特定的集成,如数据库、REST 能力、监控等。必须选择 RESTEasy Jackson 扩展,以便 Quarkus 能够无缝地将值编组为 JSON 并从 JSON 解组。单击“生成您的应用程序”按钮应提示您将 ZIP 文件保存到本地系统,其内容应与以下内容类似:
.
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│ ├── docker
│ │ ├── Dockerfile.jvm
│ │ ├── Dockerfile.legacy-jar
│ │ ├── Dockerfile.native
│ │ └── Dockerfile.native-distroless
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── demo
│ │ ├── Greeting.java
│ │ └── GreetingResource.java
│ └── resources
│ ├── META-INF
│ │ └── resources
│ │ └── index.html
│ └── application.properties
└── test
└── java
我们可以看到,Quarkus 默认添加 Docker 配置文件,因为它旨在通过容器和 Kubernetes 解决云中的微服务架构问题。但随着时间的推移,它的范围已经扩展,通过支持额外的应用程序类型和架构。GreetingResource.java文件也是默认创建的,它是一个典型的 Jakarta RESTful Web Services(JAX-RS)资源。我们需要对该资源进行一些调整,以使其能够处理Greeting.java数据对象。以下是该资源的源代码:
package com.example.demo;
public class Greeting {
private final String content;
public Greeting(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
代码与本章前面所见的基本相同。关于这个不可变数据对象没有什么新鲜或意外的。现在,在 JAX-RS 资源的情况下,事情看起来相似但又有所不同,因为我们寻求的行为与以前相同,只是我们指示框架执行其魔术的方式是通过 JAX-RS 注解。因此,代码如下:
package com.example.demo;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
@Path("/greeting")
public class GreetingResource {
private static final String template = "Hello, %s!";
@GET
public Greeting greeting(@QueryParam("name")
@DefaultValue("World") String name) {
return new Greeting(String.format(template, name));
}
}
如果您熟悉 JAX-RS,这段代码对您来说应该不会感到意外。但是如果您不熟悉 JAX-RS 注解,我们在这里所做的是用我们想要响应的 REST 路径标记资源;我们还指示greeting()
方法将处理GET
调用,并且其name
参数具有默认值。要指示 Quarkus 将返回值转换为 JSON,无需做任何其他操作,因为这将默认发生。
运行应用程序可以通过几种方式完成,其中一种是使用开发者模式作为构建的一部分。这是具有独特 Quarkus 风味的功能之一,它允许您在不手动重启应用程序的情况下自动运行应用程序并获取您所做的任何更改。您可以通过调用/.mvnw compile quarkus:dev
来激活此模式。如果您对源文件进行任何更改,您会注意到构建将自动重新编译和加载应用程序。
您也可以像之前看到的那样使用java
解释器运行应用程序,结果是一个命令,例如java -jar target/quarkus-app/quarkus-run.jar
。请注意,我们使用了一个不同的 JAR,尽管demo-1.0.0-SNAPSHOT.jar确实存在于target目录中;之所以这样做是因为 Quarkus 应用了自定义逻辑以加速启动过程,即使在 Java 模式下也是如此。
运行应用程序应该会导致平均启动时间为 600 毫秒,这几乎接近 Micronaut 的情况。此外,完整应用程序的大小在 13 MB 左右。向应用程序发送一对GET
请求,无论是否带有name
参数,都会产生类似以下输出的结果:
// using the default name parameter
$ curl http://localhost:8080/greeting
{"content":"Hello, World!"}
// using an explicit value for the name parameter
$ curl http://localhost:8080/greeting?name=Microservices
{"content":"Hello, Microservices!"}
毫无疑问,Quarkus 也支持通过 GraalVM Native Image 生成本机可执行文件,因为它面向推荐使用小二进制大小的云环境。由于这个原因,Quarkus 自带电池,就像 Micronaut 一样,并且从一开始就生成您所需的一切。无需更新构建配置即可开始使用本机可执行文件。与其他示例一样,您必须确保当前 JDK 指向 GraalVM 分发,并且native-image
可执行文件位于您的路径中。完成这一步后,剩下的就是通过调用./mvnw -Pnative package
将应用程序打包为本机可执行文件。这会激活native
配置文件,该配置文件指示 Quarkus 构建工具生成本机可执行文件。
几分钟后,构建应该已经生成了一个名为demo-1.0.0-SNAPSHOT-runner的可执行文件,该文件位于target目录内。运行这个可执行文件显示应用程序平均启动时间为 15 毫秒。可执行文件的大小接近 47 MB,这使得 Quarkus 在与以前的候选框架相比的启动速度和最小可执行文件大小方面表现最佳。
目前我们告别了 Quarkus,留下了第四个候选框架:Helidon。
Helidon
最后但并非最不重要的是,Helidon 是一个专门用于构建微服务的框架,有两种版本:SE 和 MP。MP 版本代表MicroProfile,通过利用标准的力量来构建应用程序;这个版本是 MicroProfile 规范的完整实现。另一方面,SE 版本虽然没有实现 MicroProfile,但使用不同的一组 API 提供了类似的功能。根据你想要与之交互的 API 和你对标准的偏好,选择一个版本;无论如何,Helidon 都能完成工作。
鉴于 Helidon 实现了 MicroProfile,我们可以使用另一个站点来启动 Helidon 项目。可以使用MicroProfile Starter site(见图 4-3)来为 MicroProfile 规范的所有支持实现创建项目版本。
https://github.com/OpenDocCN/ibooker-java-zh/raw/master/docs/dop-tl-java-dev/img/dtjd_0403.png
图 4-3. MicroProfile Starter
浏览到该站点,选择您感兴趣的 MP 版本,选择 MP 实现(在我们的例子中是 Helidon),并可能定制一些可用的功能。然后点击下载按钮以下载包含生成项目的 ZIP 文件。ZIP 文件包含类似于以下结构的项目结构,当然,我已经使用两个文件更新了源文件,以使应用程序按照我们想要的方式工作:
.
├── pom.xml
├── readme.md
└── src
└── main
├── java
│ └── com
│ └── example
│ └── demo
│ ├── Greeting.java
│ └── GreetingResource.java
└── resources
├── META-INF
│ ├── beans.xml
│ └── microprofile-config.properties
├── WEB
│ └── index.html
├── logging.properties
└── privateKey.pem
碰巧的是,源文件Greeting.java和GreetingResource.java与我们在 Quarkus 示例中看到的源文件完全相同。这是怎么可能的呢?首先因为代码显然是微不足道的,但更重要的是因为这两个框架都依赖于标准的力量。事实上,Greeting.java文件在所有框架中几乎完全相同,只有 Micronaut 需要额外的注解,但仅在您有兴趣生成本地可执行文件时才需要;否则,它是完全相同的。如果您决定在浏览其他内容之前直接跳到这部分,这是Greeting.java文件的样子:
package com.example.demo;
import io.helidon.common.Reflected;
@Reflected
public class Greeting {
private final String content;
public Greeting(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
它只是一个普通的不可变数据对象,只有一个访问器。定义应用程序所需的 REST 映射的GreetingResource.java文件如下:
package com.example.demo;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
@Path("/greeting")
public class GreetingResource {
private static final String template = "Hello, %s!";
@GET
public Greeting greeting(@QueryParam("name")
@DefaultValue("World") String name) {
return new Greeting(String.format(template, name));
}
}
我们很欣赏 JAX-RS 注解的使用,因为我们可以看到此时无需特定于 Helidon 的 API。运行 Helidon 应用程序的首选方法是将二进制文件打包并使用java
解释器运行它们。也就是说,虽然我们在构建工具集成方面稍有损失(目前如此),但我们仍然可以使用命令行进行迭代开发。因此,调用mvn package
后接着是java -jar/demo.jar
编译、打包并运行应用程序,嵌入式 Web 服务器监听 8080 端口。我们可以向其发送一些查询,例如:
// using the default name parameter
$ curl http://localhost:8080/greeting
{"content":"Hello, World!"}
// using an explicit value for the name parameter
$ curl http://localhost:8080/greeting?name=Microservices
{"content":"Hello, Microservices!"}
如果你查看应用程序进程运行的输出,你会看到应用程序的平均启动时间为 2.3 秒,这使其成为迄今为止最慢的候选框架,而二进制文件的大小接近 15 MB,属于所有测量值的中间位置。但正如谚语所说,不能以貌取人。Helidon 提供了更多自动配置的开箱即用功能,这解释了额外的启动时间和更大的部署大小。
如果启动速度和部署大小是问题,你可以重新配置构建,移除可能不需要的功能,并切换到本地可执行模式。幸运的是,Helidon 团队也接受了 GraalVM Native Image,并且每个像我们自己一样启动的 Helidon 项目都配备了创建本地二进制文件所需的配置。如果遵循惯例,无需调整pom.xml文件。执行mvn -Pnative-image package
命令,你会在target目录中找到一个名为demo的可执行二进制文件。这个可执行文件大约有 94 MB,迄今为止最大,平均启动时间为 50 毫秒,与之前的框架处于相同范围内。
到目前为止,我们已经初步了解了每个框架提供的功能,从基本特性到构建工具的集成。作为提醒,有几个理由可以选择一个候选框架而不是另一个。我鼓励你为每个影响你的开发需求的相关功能/方面编写一个矩阵,并对每个候选项进行评估。
无服务器
本章开始时讨论了通常由组件和层组合成的单片应用程序和架构。对特定部分的更改或更新需要更新和部署整个单体。某个特定位置的故障也可能导致整体崩溃。然后我们转向了微服务。将单体应用程序拆分为可以单独和独立更新和部署的更小块应该解决之前提到的问题,但微服务也带来了一系列其他问题。
以前,在大型服务器上托管的应用服务器内运行单体已经足够了,配备了少数副本和负载均衡器以作为补充。但这种设置存在可伸缩性问题。采用微服务的方法,我们可以根据负载的大小扩展或折叠服务的网格。这增强了弹性,但现在我们必须协调多个实例并提供运行时环境,负载均衡器变得必不可少,API 网关也是必需的,网络延迟也开始显现,我有提到分布式追踪吗?是的,这些都是需要注意和管理的许多事情。但如果不需要这些怎么办?如果有人可以处理基础设施、监控和其他运行大规模应用所需的“琐事”,那就是无服务器方法的用武之地:你可以专注于手头的业务逻辑,让无服务器提供者处理其他所有事务。
当将组件分解为较小的部分时,应该思考一件事:“我可以将这个组件转化为什么样的最小可重复使用的代码片段?” 如果你的答案是一个带有少量方法和一两个注入的协作者/服务的 Java 类,那么你已经接近了,但还不够。事实上,最小的可重复使用代码片段是一个单一的方法。想象一下,一个微服务定义为一个执行以下步骤的单一类:
-
读取输入参数并按照下一步所需的格式转换为可消耗的格式
-
执行服务所需的实际行为,例如向数据库发出查询、索引或记录日志
-
将处理后的数据转换为输出格式
现在,这些步骤中的每一个可能会被组织成单独的方法。你可能很快意识到,其中一些方法是可重复使用的或者可以参数化的。解决这个问题的典型方法是为微服务提供一个公共超类型。这会在类型之间创建强依赖关系,在某些使用情况下是可以接受的。但对于其他情况,共同代码的更新必须尽快进行,以版本化的方式进行,而不会中断当前正在运行的代码,所以我恐怕我们可能需要另一种方法。
考虑到这种情况,如果通用代码被提供为一组可以独立调用的方法,它们的输入和输出以这样一种方式组合,即您建立了一个数据转换的管道,则我们现在所知道的称为函数。像函数即服务(FaaS)这样的服务是无服务器提供者的一个常见主题。
总之,FaaS 是一种精致的方式,即您根据可能的最小部署单元组合应用程序,并让提供者为您解决所有基础设施细节。在接下来的章节中,我们将构建并部署一个简单的函数到云端。
设置中
如今,每个主要的云提供商都有一个可供使用的 FaaS 提供,其附加组件可以连接其他工具,用于监控、日志记录、灾难恢复等等;只需选择满足您需求的一个。在本章中,我们将选择 AWS Lambda,毕竟它是 FaaS 理念的发起者。我们还会选择 Quarkus 作为实现框架,因为它目前提供了最小的部署大小。请注意,这里展示的配置可能需要一些调整或可能完全过时;始终审查构建和运行代码所需工具的最新版本。目前我们将使用 Quarkus 1.13.7。
使用 Quarkus 和 AWS Lambda 设置函数需要一个 AWS 账号,在您的系统上安装 AWS CLI,以及如果您想要运行本地测试,还需要安装 AWS Serverless Application Model (SAM) CLI。
一旦你搞定了这些,下一步就是启动项目,我们会倾向于像以前一样使用 Quarkus,但是函数项目需要不同的设置。所以最好切换到使用 Maven 原型:
mvn archetype:generate \
-DarchetypeGroupId=io.quarkus \
-DarchetypeArtifactId=quarkus-amazon-lambda-archetype \
-DarchetypeVersion=1.13.7.Final
在交互模式下调用此命令将询问您一些问题,例如项目的组、artifact、版本(GAV)坐标和基础包。对于这个演示,让我们使用以下配置:
-
groupId
: com.example.demo -
artifactId
: demo -
version
: 1.0-SNAPSHOT(默认值) -
package
: com.example.demo(与groupId
相同)
这将导致一个适合构建、测试和部署到 AWS Lambda 的 Quarkus 项目的项目结构。原型创建了 Maven 和 Gradle 的构建文件,但是我们现在不需要后者;它还创建了三个函数类,但我们只需要一个。我们的目标是拥有类似于这个的文件结构:
.
├── payload.json
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── demo
│ │ ├── GreetingLambda.java
│ │ ├── InputObject.java
│ │ ├── OutputObject.java
│ │ └── ProcessingService.java
│ └── resources
│ └── application.properties
└── test
├── java
│ └── com
│ └── example
│ └── demo
│ └── LambdaHandlerTest.java
└── resources
└── application.properties
函数的要点是使用 InputObject
类型捕获输入,使用 ProcessingService
类型处理它们,然后将结果转换为另一种类型(OutputObject
)。GreetingLambda
类型将所有内容整合在一起。首先让我们先看一下输入和输出类型——毕竟,它们只是简单的数据类型,没有任何逻辑:
package com.example.demo;
public class InputObject {
private String name;
private String greeting;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGreeting() {
return greeting;
}
public void setGreeting(String greeting) {
this.greeting = greeting;
}
}
Lambda 函数期望接收两个输入值:问候语和姓名。我们马上会看到它们如何被处理服务转换:
package com.example.demo;
public class OutputObject {
private String result;
private String requestId;
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
public String getRequestId() {
return requestId;
}
public void setRequestId(String requestId) {
this.requestId = requestId;
}
}
输出对象包含转换后的数据和请求 ID 的引用。我们将使用这个字段来展示如何从运行上下文获取数据。
好了,接下来是处理服务;这个类负责将输入转换为输出。在我们的情况下,它将两个输入值连接成一个字符串,如下所示:
package com.example.demo;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ProcessingService {
public OutputObject process(InputObject input) {
OutputObject output = new OutputObject();
output.setResult(input.getGreeting() + " " + input.getName());
return output;
}
}
剩下的就是查看GreetingLambda
,用于组装函数本身的类型。这个类需要实现由 Quarkus 提供的已知接口,其依赖应该已经在使用原型创建的pom.xml文件中配置好了。这个接口使用输入和输出类型参数化。幸运的是,我们已经有了这些。每个 Lambda 必须有一个唯一的名称,并且可以访问其运行上下文,如下所示:
package com.example.demo;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import javax.inject.Inject;
import javax.inject.Named;
@Named("greeting")
public class GreetingLambda
implements RequestHandler<InputObject, OutputObject> {
@Inject
ProcessingService service;
@Override
public OutputObject handleRequest(InputObject input, Context context) {
OutputObject output = service.process(input);
output.setRequestId(context.getAwsRequestId());
return output;
}
}
所有的部分都应该得到合理安排。Lambda 定义输入和输出类型并调用数据处理服务。为了演示目的,此示例展示了依赖注入的使用,但您可以通过将ProcessingService
的行为移到GreetingLambda
中来减少代码量。我们可以通过运行本地测试命令mvn test
快速验证代码,或者如果您喜欢mvn verify
,因为它还会打包函数。
请注意,当函数被打包时,附加文件被放置在target目录中,特别是一个名为manage.sh的脚本,该脚本依赖于 AWS CLI 工具,用于在与您的 AWS 帐户关联的目标位置创建、更新和删除函数。支持这些操作需要其他文件:
function.zip
包含二进制位的部署文件
sam.jvm.yaml
使用 AWS SAM CLI 进行本地测试(Java 模式)
sam.native.yaml
使用 AWS SAM CLI 进行本地测试(本地模式)
下一步需要您配置一个执行角色,最好参考 AWS Lambda 开发指南,以防流程已经更新。该指南将向您展示如何配置 AWS CLI(如果您尚未执行此操作)并创建一个必须添加为运行 shell 的环境变量的执行角色。例如:
LAMBDA_ROLE_ARN="arn:aws:iam::1234567890:role/lambda-ex"
在这种情况下,1234567890
代表您的 AWS 帐户 ID,lambda-ex
是您选择的角色名称。我们可以继续执行函数,对于这个函数,我们有两种模式(Java、本地)和两种执行环境(本地、生产);让我们首先处理两种环境的 Java 模式,然后再处理本地模式。
在本地环境中运行函数需要使用 Docker 守护程序,现在应该是开发人员工具箱中的常见组成部分;我们还需要使用 AWS SAM CLI 来驱动执行。记住在target目录中找到的附加文件集合吗?我们将使用sam.jvm.yaml文件以及在项目启动时由原型创建的另一个文件payload.json。位于目录根目录,其内容应如下所示:
{
"name": "Bill",
"greeting": "hello"
}
该文件为函数接受的输入定义了值。鉴于函数已经打包,我们只需调用它,如下所示:
$ sam local invoke --template target/sam.jvm.yaml --event payload.json
Invoking io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
(java11)
Decompressing /work/demo/target/function.zip
Skip pulling image and use local one:
amazon/aws-sam-cli-emulation-image-java11:rapid-1.24.1.
Mounting /private/var/folders/p_/3h19jd792gq0zr1ckqn9jb0m0000gn/T/tmppesjj0c8 as
/var/task:ro,delegated inside runtime container
START RequestId: 0b8cf3de-6d0a-4e72-bf36-232af46145fa Version: $LATEST
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
[io.quarkus] (main) quarkus-lambda 1.0-SNAPSHOT on
JVM (powered by Quarkus 1.13.7.Final) started in 2.680s.
[io.quarkus] (main) Profile prod activated.
[io.quarkus] (main) Installed features: [amazon-lambda, cdi]
END RequestId: 0b8cf3de-6d0a-4e72-bf36-232af46145fa
REPORT RequestId: 0b8cf3de-6d0a-4e72-bf36-232af46145fa Init Duration: 1.79 ms
Duration: 3262.01 ms Billed Duration: 3300 ms
Memory Size: 256 MB Max Memory Used: 256 MB
{"result":"hello Bill","requestId":"0b8cf3de-6d0a-4e72-bf36-232af46145fa"}
此命令将拉取一个适合运行该函数的 Docker 镜像。请注意报告的值,这可能会因您的设置而异。在我的本地环境中,此函数的执行将花费我 3.3 秒,并占用 256 MB 的内存。这可以让您了解在运行系统作为一组函数时,您将被计费多少。然而,本地环境与生产环境不同,因此让我们将该函数部署到真实环境中。我们将使用 manage.sh 脚本来完成这一壮举,通过调用以下命令:
$ sh target/manage.sh create
$ sh target/manage.sh invoke
Invoking function
++ aws lambda invoke response.txt --cli-binary-format raw-in-base64-out
++ --function-name QuarkusLambda --payload file://payload.json
++ --log-type Tail --query LogResult
++ --output text base64 --decode
START RequestId: df8d19ad-1e94-4bce-a54c-93b8c09361c7 Version: $LATEST
END RequestId: df8d19ad-1e94-4bce-a54c-93b8c09361c7
REPORT RequestId: df8d19ad-1e94-4bce-a54c-93b8c09361c7 Duration: 273.47 ms
Billed Duration: 274 ms Memory Size: 256 MB
Max Memory Used: 123 MB Init Duration: 1635.69 ms
{"result":"hello Bill","requestId":"df8d19ad-1e94-4bce-a54c-93b8c09361c7"}
正如您所见,计费时长和内存使用量都减少了,这对我们的钱包来说是好事,尽管初始化时长增加到了 1.6,这会延迟响应,增加整个系统的总执行时间。让我们看看当我们从 Java 模式切换到本机模式时,这些数字会如何变化。您可能还记得,Quarkus 允许您将项目打包为本机可执行文件,但请记住 Lambda 需要 Linux 可执行文件,因此如果您恰好在非 Linux 环境上运行,则需要调整打包命令。以下是需要完成的操作:
# for linux
$ mvn -Pnative package
# for non-linux
$ mvn package -Pnative -Dquarkus.native.container-build=true \
-Dquarkus.native.container-runtime=docker
第二个命令在 Docker 容器内调用构建,并将生成的可执行文件放置在预期位置,而第一个命令则按原样执行构建。现在,有了本地可执行文件,我们可以在本地和生产环境中执行新函数。先看看本地环境:
$ sam local invoke --template target/sam.native.yaml --event payload.json
Invoking not.used.in.provided.runtime (provided)
Decompressing /work/demo/target/function.zip
Skip pulling image and use local one:
amazon/aws-sam-cli-emulation-image-provided:rapid-1.24.1.
Mounting /private/var/folders/p_/3h19jd792gq0zr1ckqn9jb0m0000gn/T/tmp1zgzkuhy as
/var/task:ro,delegated inside runtime container
START RequestId: 27531d6c-461b-45e6-92d3-644db6ec8df4 Version: $LATEST
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
[io.quarkus] (main) quarkus-lambda 1.0-SNAPSHOT native
(powered by Quarkus 1.13.7.Final) started in 0.115s.
[io.quarkus] (main) Profile prod activated.
[io.quarkus] (main) Installed features: [amazon-lambda, cdi]
END RequestId: 27531d6c-461b-45e6-92d3-644db6ec8df4
REPORT RequestId: 27531d6c-461b-45e6-92d3-644db6ec8df4 Init Duration: 0.13 ms
Duration: 218.76 ms Billed Duration: 300 ms Memory Size: 128 MB
Max Memory Used: 128 MB
{"result":"hello Bill","requestId":"27531d6c-461b-45e6-92d3-644db6ec8df4"}
计费时长降低了一个数量级,从 3300 毫秒降至仅 300 毫秒,并且使用的内存减半;与其 Java 对应物相比,这看起来很有希望。在生产环境中运行时,我们会得到更好的数据吗?让我们来看看:
$ sh target/manage.sh native create
$ sh target/manage.sh native invoke
Invoking function
++ aws lambda invoke response.txt --cli-binary-format raw-in-base64-out
++ --function-name QuarkusLambdaNative
++ --payload file://payload.json --log-type Tail --query LogResult --output text
++ base64 --decode
START RequestId: 19575cd3-3220-405b-afa0-76aa52e7a8b5 Version: $LATEST
END RequestId: 19575cd3-3220-405b-afa0-76aa52e7a8b5
REPORT RequestId: 19575cd3-3220-405b-afa0-76aa52e7a8b5 Duration: 2.55 ms
Billed Duration: 187 ms Memory Size: 256 MB Max Memory Used: 54 MB
Init Duration: 183.91 ms
{"result":"hello Bill","requestId":"19575cd3-3220-405b-afa0-76aa52e7a8b5"}
总计的计费时长结果提高了 30%,内存使用量不到以前的一半;但真正的赢家是初始化时间,大约是以前时间的 10%。在本机模式下运行您的函数会导致启动更快,各方面的数据更好。
现在轮到您决定哪种组合选项会给您带来最佳结果了。有时,即使是在生产环境中保持 Java 模式也足够好,或者一直采用本机模式可能会为您带来优势。无论哪种方式,测量都很关键—不要猜测!
总结
在本章中,我们涵盖了很多内容,从传统的单体架构开始,将其分解为具有可重用组件的较小部分,这些部分可以独立部署,称为微服务,一直到最小的部署单元:函数。沿途会出现权衡,因为微服务架构本质上更复杂,由于其由更多运动部分组成。网络延迟成为一个真正的问题,必须相应地加以解决。其他方面,如数据事务,由于其跨服务边界的跨度可能不同,取决于情况,会变得更加复杂。使用 Java 和本地可执行模式产生不同的结果,并且需要定制设置,每种都有其优缺点。我亲爱的读者,我的建议是评估、测量,然后选择一种组合;随时注意数字和服务级别协议(SLAs),因为您可能需要重新评估决策并进行调整。
表 4-1 总结了在我的本地环境和远程环境中以 Java 和本地映像模式运行示例应用程序时,每个候选框架的测量结果。大小列显示部署单元大小,而时间列描绘了从启动到第一个请求的时间。
表 4-1. 测量摘要
框架 | Java - 大小 | Java - 时间 | 本地 - 大小 | 本地 - 时间 |
---|---|---|---|---|
Spring Boot | 17 MB | 2200 ms | 78 MB | 90 ms |
Micronaut | 14 MB | 500 ms | 60 MB | 20 ms |
Quarkus | 13 MB | 600 ms | 47 MB | 13 ms |
Helidon | 15 MB | 2300 ms | 94 MB | 50 ms |
作为提醒,鼓励您进行自己的测量。对于托管环境、JVM 版本和设置、框架版本、网络条件和其他环境特征的更改将产生不同的结果。显示的数字应该持保留态度,永远不要视为权威值。