你好,我是成富,在 Java 领域钻研 13 年有余,参与过大大小小各种类型的项目开发工作,积累了很多实际产品的开发与架构设计经验,平时乐于总结和分享。
微服务在业内的实践已经从流行走向成熟,诸多大厂(比如 Amazon、Netflix、蚂蚁金服、网易云音乐等)都已经迁移并采用了微服务架构,而随着 AWS、Google 云平台、微软 Azure、阿里云、腾讯云和华为云等云计算平台的流行,越来越多的中小型公司选择把应用迁移到云平台上运行。
微服务云原生应用,恰是结合了微服务架构和云计算平台两者的优势,成为了中小型公司开发新项目或迁移已有项目的最佳选择,微服务云原生应用开发人员的大量需求也应运而生。
但是,在这背后,我们也看到很多现实问题:
人才的供给总是具有滞后性。即便是在庞大的 Java 开发者中,了解微服务架构,知道如何在云平台上实现微服务架构的开发人员数量也很有限。这主要是因为大部分 Java 开发者都熟悉单体应用开发,对于微服务架构缺乏必要的了解。
学习本身需要投入大量时间和精力。单是微服务技术本身,其概念就非常繁杂。而且各种概念又相互关联,形成复杂的知识网络,让初学者不知何处下手,更别提如何利用理论知识来解决关键问题。
知识的理解和应用之间,需要跨越鸿沟。如果只看概念,而不进行实际操作,很难说真正理解。
在这个专栏中,我就是想把微服务架构相关的理论与具体的实例相结合,来帮助你真正掌握云原生微服务架构的应用开发。
我是如何学习和实践微服务的?
微服务架构出现之后,我意识到了它必将引领产品开发的一次革命性潮流,而 Kubernetes 和云原生技术的出现,更为微服务架构的落地找到了最佳的平台。于是,我开始使用微服务架构和云平台来设计和实现公司的下一代产品。
在实际开干之前,我先是网上搜索阅读了大量技术资料,看了不下十本微服务图书,当然还有 Kubernetes 官方文档和其他相关的视频和文字资料,我感觉对微服务架构在云平台上的实现方式有了非常清晰的认识。
但是,当我把这些知识应用到实际的产品开发中时,还是遇到了很多现实问题。比如,如何划分微服务,一个功能到底应该放在哪个微服务;如何在一个微服务中调用另外一个微服务的 API;当数据在多个微服务中分别存储时,如何保证这些微服务之间数据的一致性。我在初学微服务云原生应用开发时,和你产生过同样的困惑,有种无的放矢的感觉。但是我把握了最重要的一点,那就是“实战永远是学习新技术的不二法门”:
当你要学习一项新技术时,最有效的办法就是真正用它来开发一个实际应用。
一开始,你写的代码可能很“蠢”,但是实战的过程会加深你对技术的理解和认知。当你对技术有了更深入的理解之后,完全可以再回过头来对已有代码进行重构,甚至是推倒重来。亲身经历整个过程,可以帮助你有效地掌握开发实践和积累经验。当你之后真正需要在工作中用到这项技术时,就能少走很多不必要的弯路。
但是,这些弯路,我已经代你走过很多,你不必一一重历。
我是如何设计这个课程的?
这个课程正是我在实际产品开发中的经验总结。在开发微服务架构应用的过程中,你同样会遇到和我类似的问题,而我的这些经验不仅可以帮助你少走一些弯路,还能给你一些经过验证的最佳实践,让你的开发工作走得更稳、更高效,让产品能够更快上线,同时为以后的版本维护和更新打下坚实的基础。
当我们开始学习微服务架构和云原生应用的时候,通常第一个直观印象是它很复杂。一点没错儿,这是因为微服务架构本身所带来的挑战是多方面的,除了技术层面的挑战,还包括团队组织、项目管理和运营维护等诸多方面。实际上,在微服务架构中,微服务自身的实现反而只是最基础的部分。
当一个微服务的 API 确定之后,你可以选择任何编程语言和框架来实现它,不管是 Java、Go,还是 NodeJS。微服务架构最大的难点在于:服务之间的交互、数据一致性、错误处理,以及运维方面的挑战。
这个课程最大的优势在于,以一个完备的、贴近实战的应用做主线,真正带你走完一个产品从想法到上线的全部过程。我将借助一个类似优步、滴滴的打车应用的后台服务,带你了解微服务架构在云平台上落地的方方面面,从最初的构想到最后在阿里云部署上线,带你一步一步实践一个微服务云原生应用的开发、部署和上线。
项目初始的时候,我们将利用领域驱动设计的思想来帮助拆分微服务。微服务的划分看似简单,实则是非常重要的一部分。因为微服务一旦划分完以后,再想把某个功能从一个微服务迁移到另外一个微服务会变得很困难,通常会需要改动多个微服务的公开 API,这是你不希望看到的情况。因此,在微服务划分阶段就要仔细考虑。
接着,对于每个微服务采用最适合的技术来实现,比如有的使用数据库驱动的方式来实现,有的使用事件源技术来实现。这是因为一旦微服务的 API 确定之后,对于微服务本身的实现并没有太多的限制,我们可以根据每个服务的特征选择最适合的技术栈。
此外,除了微服务实现,安全、日志管理、性能指标、调用追踪和异常处理等非功能性需求相关的内容也涵盖在这个课程之中。这些非功能性需求可能相对于实际的业务逻辑来说并不那么重要,但却是一个健壮的应用所不可或缺的。对于一个微服务架构的应用来说,上线只是第一步,应用的运行离不开实时监控、错误追踪、故障处理和系统维护的支持。
最后,我们会通过持续集成和持续部署服务,把应用部署到阿里云平台的 Kubernetes 上。在实际产品开发中,你无法想象没有自动化单元测试和集成测试,以及自动化的持续集成和部署;在一个成熟的产品中,你不可能不考虑到服务的版本更新。和其他往往集中于过度简化的用户场景,离实际的产品开发太远的课程不同,这些容易被忽略的内容都包含在这个课程中。
此外,我在课程中还为你提供了大量贴近实际的进阶内容,如微服务之间的异步消息传递、使用 Saga 模式进行跨服务的协作和基于 Istio 的服务网格实现,这些都是在实际项目开发中会用到的技术。在介绍相关的技术时,我还会着重讲解背后的原理,真正做到让你“知其然”的同时,也能“知其所以然”。
当然,除了了解微服务架构云原生应用开发的全过程,你还将得到完整的示例代码。我在课程中,对于重要功能的实现都给出了详细的示例代码,你可以把这个示例应用作为你自己的微服务应用的基础。完整的示例代码,我会发布在 GitHub(https://github.com/alexcheng1982/happyride)上并提供更新。
希望这样一个完整的过程,能让你细致而且系统地掌握微服务云原生应用开发的全貌与细节,帮你把各种散碎的知识与概念串联起来,通过贴近实际场景的应用示例,了解真实业务的开发过程、最佳实践,真正将脑中的想法变成可运行的业务代码。
写在最后
我在实际开发和应用微服务架构时,最大的感受是:理论知识与实际的产品开发之间不可避免地存在一些差异。这是因为抽象的理论知识需要与具体的实践相结合,从中获取到实际的开发经验。
要真正了解微服务架构和云原生应用,最好的做法就是实际完成一个项目的开发。在这个过程中,你会把遇到的问题都解决一遍,把可能踩的坑都踩一遍。第一个项目的开发过程是痛苦的,一开始你认为正确的做法,可能被实践证明是不合理的,会有反复验证的过程。但只有在不断地试错过程中,才能积累到真实的经验,并可以用来指导以后的开发。
这样当你开始做第二个,第三个项目时,就会有得心应手的感觉。你已经有一整套成熟的从架构、设计、实现、测试到部署的完整流程,可以直接快速地复制。
这个课程所要提供给你的,就是这样一整套经过实际开发验证的流程,一套完备的、真实的,贴近实际的产品开发过程。除了让你快速上手,即便是遇到课程中没有提过的内容,你也可以按照同样的方式来进行实践,积累属于自己的经验。
微服务架构的云原生应用开发是一个巨大的挑战,同时也带来了机遇。可以预期的是,在未来的三到五年内,会有越来越多的公司迁移到微服务架构和云平台。这个趋势已经在国内外的大公司中得到了验证,中小企业也会逐步跟上这个趋势。学会微服务和云原生相关的技术,可以确保你能跟上这一波的技术热潮。
希望你在每次机遇来临前,都能够把握住时机。
本课时我们将介绍微服务架构。
相信你也听说过微服务(MicroService)这个词,微服务架构是一种流行的架构设计风格,通常用作单体(Monolith)架构的一种替代方案。市面上关于微服务的图书、教程和资料层出不穷,这在很大程度归功于大厂商的市场推广,微服务和很多年之前的 SOA 一样,注定会成为一个广泛讨论的话题。从好的方面来说,这保证了微服务相关的技术有足够大的市场,不会被轻易淘汰;从坏的方面来说,错综复杂的信息会让人无所适从,不知道从何入手开始学习微服务架构中与技术相关的内容。
目前,我们开发的大部分应用都是单体应用。当单体应用的复杂度增加时,会出现一系列的问题。微服务架构吸引人的地方在于它对复杂应用的开发提供了一种新的解决方法。微服务架构的核心思想是把应用按照功能划分成多个独立的服务,每个服务都是独立运行的应用。
如下图所示,外部的边框是应用的边界,不同的形状表示不同的单元。图中左侧表示的是单体应用,所有单元在同一个应用的边界内。在进行扩展时,单体应用只能整体扩展;右侧表示的是微服务架构的应用,每个单元在自己的应用边界内。在进行扩展时,微服务架构的应用以服务为单位进行扩展。不同服务在运行时的实例数量可以根据负载动态进行调整。
在介绍微服务架构之前,有必要先看一下单体应用存在的问题,从而了解微服务架构要解决的问题。
单体应用的问题
初学 Java,我们就知道 main() 方法的作用,以及如何使用 Java 虚拟机来运行一个应用。这样的应用就是单体应用。单体应用是我们最熟悉的开发应用方式,从开发、测试、部署到维护的全过程,积累了很多经验。但是当单体应用不断更新升级和扩充功能时,一些问题就会出现。
第 1 个问题是单体应用过于复杂,超出了单个开发人员的理解能力。虽然可以通过组件化把单体应用划分成多个单元,降低每个单元的复杂度,但在实际开发中,组件化的效果并不是很好。一方面是因为公共代码的存在,这些共享的代码由于被多个组件使用,难以高效更新;另一方面由于文档缺失和开发过程中的各种不规范行为,造成组件之间的接口不清晰。在 Java 代码中,我们可以很容易地调用其他组件中的接口和类的方法,从而在不同组件之间有意或无意的引入依赖。所产生的结果就是单体应用中不同组件之间的依赖关系非常复杂。
第 2 个问题是缓慢的开发速度。当应用变得复杂时,开发人员则需要花费更多的时间来理解所做的改动对已有代码的潜在影响。经常遇到的情况有,一个看似很小的改动,会对应用造成很大的影响。当这样的情况出现多次之后,开发人员由于怕承担责任,变得不情愿改进代码的质量,同时,在本地开发环境上进行开发和调试变得更加耗时。在 IDE 中修改代码之后,编译和重启应用的时间过长,本地单元测试也需要更长的时间来运行。所产生的结果就是开发人员的宝贵时间耗费在无意义的等待上。
第 3 个问题是应用的扩展变得很困难。当应用的处理能力不能满足业务需求时,需要进行扩展,扩展分为垂直扩展和水平扩展两种。垂直扩展的做法是增加单个应用实例所能使用的资源,这导致的问题是不同组件对资源需求的冲突,有些组件对内存的消耗较大,而另外一些组件对 CPU 的要求高,当无法同时满足两者时,则需要作出取舍。水平扩展的做法是增加应用的实例数量,水平扩展只能以应用为单位来进行,如前面的图片所示,应用中不同组件的负载程度是不同的,以电子商务应用为例,商品展示和订单处理相关组件的负载要大于客户服务相关的组件,以应用为单元的扩展方式无法有效的分配资源。
第 4 个问题是新版本更新上线的速度变慢。现在的应用都要求能够及时响应用户的需求,以最快的速度添加新功能和修复问题,这意味着一天可能要进行很多次的产品更新。单体应用由于复杂度高,每一次代码提交之后的持续集成所花费的时间过长。由于需要运行全部的测试用例,限制了更新的频率。
第 5 个问题是整个应用的稳定性变差。由于整个应用只有一个进程,组件之间缺少必要的隔离性,任何一个组件中出现的问题,比如内存泄漏,都会导致整个应用的崩溃。当某个组件占用大量的 CPU 和内存资源时,会导致其他组件由于资源不足而无法正常工作。
第 6 个问题是技术栈的选型和更新变得困难。单体应用通常只使用单一的技术栈,包括编程语言、所用框架、第三方库,以及数据库和消息中间件等,这就要求所有的开发人员都掌握相同的技术栈,事实上,不同组件由于需求的不同,有它最适合的技术栈。强制使用单一技术栈,无疑会对开发效率产生影响,一旦技术栈选型确定之后,要对它进行更新是一件非常困难的事情,整个应用都可能受到影响。所产生的效果就是应用的技术栈不断的老化,带来更多的问题,形成恶性循环。
如果你的单体应用遇到了上述问题,则说明该应用的架构到了需要调整的时候,微服务架构是一个值得考虑的解决方案。下面我们来看一下微服务架构到底是什么。
微服务架构的特征
对于微服务架构,并没有一种统一的定义,不同的人有不同的表达形式。Martin Fowler 是这样表述的:微服务架构风格把应用划分成若干个服务,每个服务有自己独立的进程,服务之间通过轻量级传输机制进行交互。这个表述包含了微服务架构的一些重要特征。事实上,我们关注更多的并不是微服务架构的定义,而是它所具备的特征,这些特征可以帮助我们确定是否应该选用微服务架构。
微服务架构使用服务作为组件化的单元。组件化是软件开发中的基本实践,在 Java 应用开发中,组件通常以 JAR 文件的形式出现,Maven 仓库中包含了海量的第三方库可供使用,Java 开发人员都熟悉这种使用组件的方式。在微服务架构中,组件的单元变成了服务,服务运行在独立的进程中,不能通过直接的方法调用来访问,而需要使用类似 HTTP 这样的进程间通信方式,每个服务可以独立部署,使用 API 规范来描述其公开接口。一个微服务只能通过 API 访问另外的微服务,并不能访问内部的实现代码。
如下图所示,不同服务之间存在调用关系,调用的方式可以是 REST 或 gRPC。
微服务架构的开发团队围绕业务能力来组织。单体应用的开发团队通常按照技能来划分,一个典型的 3 层应用开发团队可能分成前端开发、后端开发和数据库管理等小组。微服务架构的开发团队以服务为单元来组织,每个服务与特定的业务需求相对应。服务的开发团队规模较小,包含开发、测试和 DevOps 相关的全部人员,负责该微服务的团队对该微服务的实现可以全权负责。较小的开发团队意味着更少的沟通成本和更高的开发效率。
微服务架构使用去中心化的管理模式。单体应用的开发团队通常会对使用的技术栈做出限制,要求整个团队使用统一的技术栈。这种方式的弊端在于,没有一种技术栈适用于解决所有的问题。微服务架构中的服务都可以独立部署,这就意味着每个服务在实现时可以选择最适合的技术栈,只需要满足服务的 API 契约即可。每个团队自主管理所负责的服务,不但负责构建,还同样负责运行和维护,这在无形中提高了团队的主观能动性,同时降低了管理的开销。
如下图所示,每个微服务都有对应的团队,而每个团队中都有各种角色的人员。
微服务架构使用去中心的数据存储。单体应用通常使用单一数据库来存储数据,微服务架构中的服务通常有自己专有的数据存储,如下图所示。这些存储方式的实现可能各不相同,只包含服务所需的数据。
微服务架构强调基础设施的自动化。持续集成和持续部署都是通用的实践,单体应用由于只有一个部署单元,对自动化的要求并不高。微服务架构中的服务可以独立部署,但当服务的数量较多时,必须通过自动化的流程来完成。
微服务架构在设计时充分考虑到失败的情况。这是因为服务之间通过进程间通讯方式进行交互,这样的交互方式天生就容易失败,调用时的目标服务可能失败或负载过大。
正如《人月神话》中提到的“没有银弹”一样,微服务架构也不是解决所有应用问题的万能钥匙。下面我们看一下微服务架构带来的问题有哪些。
微服务架构的问题
微服务架构最大的问题是其实现的复杂性。从单个应用对多个应用的变化,带来的不仅仅是数量上的增加,而是交互模式的变化。使用微服务架构之后,应用变成了一个分布式系统,在分布式系统中,服务通过进程间通讯方式来交互,如使用 REST API 或 gRPC 远程方法调用。这样的通讯方式,要求调用者使用更复杂的策略来处理可能出现的错误。当需要调用一个微服务时,首先需要通过服务发现机制找到微服务的实际地址,然后再发送调用请求。目标微服务有可能处于离线状态,或者因为负载过大而延迟过高,对于不同的情况,需要有相应的处理策略。
微服务架构的复杂性还体现在架构设计时。设计时最重要的问题是如何划分微服务,划分的好坏将直接影响整个应用的可维护性。如果划分的微服务粒度过大,则随着微服务的发展,它们的复杂度可能等同于传统的单体应用;如果划分的微服务粒度过小,则过多数量的微服务不但增大了运维的成本,也会影响系统性能,增加开发的复杂度。一旦微服务划分完成之后,在服务之间转移功能将变得非常困难。如果需要把一个服务的功能转移到另外一个服务上,不仅需要对这两个服务的 API 契约进行修改,还可能会影响其他服务。
微服务架构中的服务可能使用的是不同的数据存储,包括关系型数据库和 NoSQL 数据库,要在这些数据存储之间保持数据一致性非常复杂。在单体应用中,我们可以通过数据库事务的 ACID 特性来保证数据一致性,在微服务架构中,通常的选择是保证数据的最终一致性。
微服务架构对于系统运维也提出了更高的要求。每个微服务可能有不同的实现方式,运维团队不仅需要维护种类繁多的数据库和消息中间件,还需要应对持续集成和持续部署的挑战。幸运的是,随着阿里云这样的 PaaS 平台出现,以及 Kubernetes 和 Docker 等的流行,运维团队有了更好的技术和工具来管理微服务。
微服务对开发团队的组织也有影响。在传统的单一应用中,开发团队按照组件划分成多个小团队,由于组件之间的联系比较紧密,小团队之间沟通比较多,有些改动甚至需要全部团队参与,这使得团队之间的沟通成本变大,大量的时间都花在了沟通上,降低了开发效率。微服务架构强调团队的自主性,每个团队负责一个或多个微服务的开发、测试、部署和运维,他们的规模都比较小,方便内部沟通。在开发流程上,瀑布式的开发流程并不适用于微服务,微服务开发应该采用敏捷软件开发流程。
上述这些问题是由微服务架构的特征所带来的,这些问题在实际的开发中都有相应的解决方案,本专栏也会对这些方案进行介绍。
微服务架构的实现
微服务架构所涵盖的内容非常广泛,对不同角色的人员,其意义并不相同:
从架构的角度来说,它是由多个独立服务组成的分布式系统;
从人员管理的角度来说,它要求员工组成小而完备的自主团队;
从项目管理的角度来说,它推荐使用敏捷软件开发流程,如 Scrum 或 Kanban;
从开发的角度来说,每个服务有独立的业务逻辑实现和数据存储,使用开放 API 作为边界;
从测试的角度来说,需要对服务的 API 契约进行测试;
从运维的角度来说,持续集成和持续部署对微服务架构的成功至关重要。
微服务架构的实践是一项系统化的工程,需要很多人的协同合作。作为开发人员,我们更多关注的是如何完成服务的实现,但除了每个服务本身的实现之外,还包括与其他服务的协作。
从实现的角度来说,我们需要考虑表中的这些因素。
表中列出的关于服务实现的相关内容,在大部分微服务架构的应用中都会出现。但在实际的项目开发中,你并不会从零开始实现所有相关的内容,而是使用已有的平台、框架和技术,流行的技术选择包括 Netflix OSS 栈、Spring Cloud 和 Kubernetes 等。
Netflix 是微服务架构实践中的引领者,不仅在生产系统中成功应用了微服务架构,还把相关的库和工具以开放源代码的形式共享出来,形成了 Netflix OSS 栈。
Spring Cloud 是由多个开源项目组成的开发套件,用来实现分布式系统中的常见模式,如配置管理、服务发现和断路器等,可以用来实现微服务架构的应用。它的优势在于提供了一个抽象框架,可以避免供应商锁定的问题,对于同一个模式,它可以切换底层的实现方式。Spring Cloud 本身是基于 Spring 框架的,对于一直工作在 Spring 框架上的团队来说,Spring Cloud 是一个不错的选择。
Kubernetes 是管理容器化工作负载和服务的平台,同时也是容器编排平台,微服务及其依赖的其他服务通常以容器的形式运行。Kubernetes 对表中的很多需求都提供了原生的解决方案,对于另外的一些需求则有开源实现,是运行微服务架构应用的良好平台。
本专栏包含的内容
微服务架构所包含的内容非常多,不可能在一个专栏中涵盖所有相关的内容,本专栏的侧重点在于微服务的实现,即如何实现单个服务,以及服务之间如何协作,上表中列出的与微服务架构实现相关的因素都会涉及。在具体的实现技术方面,微服务架构的应用将使用 Kubernetes 来作为底层平台,并使用 Istio 来作为 服务网格 的实现。
除了微服务实现相关的内容之外,本专栏还包含了测试和 DevOps 相关内容的简要介绍,主要是因为测试和 DevOps 与开发的关系比较密切,当你了解了一些相关的内容后,才会更有助于团队之间的合作。
总结
作为微服务课程的第一课时,着重介绍了微服务架构的基本概念。首先从单体应用的问题来阐述引入微服务架构的必要性,接着介绍了微服务架构的应用所具备的特征。微服务架构也不是解决所有问题的万能钥匙,本课时还介绍了微服务架构的问题。最后,介绍了实现微服务架构时需要考虑的因素和相关的平台、框架和技术。
本课时我将带你学习容器化技术与 Docker 的使用。
微服务架构的应用本质是一个分布式系统,分布式系统早在微服务架构之前就已经出现了。分布式系统的复杂性除了体现在开发时,对运维也提出了更高的要求,需要部署的应用数量从一个变成了几个甚至几十个。如果在运维方面没有相应的技术提升,微服务架构也不可能得到广泛的应用。
容器化技术的出现,为系统运维带来了新的可能性,微服务架构应用的部署离不开容器化技术。
下面我将首先介绍应用部署方面的技术发展。下图给出了应用部署发展的阶段,即从传统部署到硬件虚拟化,再到容器化。这里我们首先介绍其中的硬件虚拟化技术。
应用部署技术的发展
硬件虚拟化技术
在早期的时候,应用都是直接被安装在操作系统中的。在进行扩展时,我们需要在新的物理机器上安装操作系统,再安装应用,这种方式的问题在于当多个应用共享物理资源时,一个应用可能占用过多的资源,从而影响其他应用的性能。另外,这种方式进行扩展的速度也很慢,需要从物理机器开始,无法快速响应业务的需求。
硬件虚拟化技术的出现,为扩展提供了新的解决方案,硬件虚拟化指的是对计算机的虚拟化,虚拟化对用户隐藏了计算平台的物理特征,仅提供一个抽象的计算平台。
用来控制虚拟化的程序称为 Hypervisor,它可以创建和运行虚拟机。在虚拟机上我们可以安装不同类型的操作系统,包括 Windows、Linux 和 MacOS,虚拟机实例共享虚拟化的硬件资源。Hypervisor 通常分成两类:第一类 Hypervisor 直接在硬件上运行,如 Xen 和微软的 Hyper-V;第二类 Hypervisor 运行在已有的操作系统上,如 VMware Workstation、VMware Player、VirtualBox 和 QEMU。
硬件虚拟化使得我们可以更充分的利用硬件资源,在创建集群时,可以用少数大型服务器替换掉数量较多的小型服务器。在这些服务器上运行 Hypervisor,并根据需要创建和运行虚拟机;在虚拟机上运行操作系统,而在操作系统上运行应用。
在创建虚拟机时我们可以限制虚拟机的 CPU、内存和硬盘等资源,硬件虚拟化可以更好的支持扩展。Hypervisor 可以从镜像文件中快速创建出虚拟机实例,当需要增加应用实例时,我们可以从保存的镜像中创建虚拟机并运行。处理应用的失败也变得简单,只需要创建新的虚拟机实例替换掉出错的即可。
硬件虚拟化的不足之处在于只能以操作系统为单位来进行扩展,操作系统本身也需要占用资源。当虚拟机的数量增加时,很多资源都被虚拟机中的操作系统占用。操作系统级别的虚拟化,也就是容器化,可以在隔离的容器中运行程序。容器中运行的程序只能访问操作系统的部分资源,包括 CPU、内存、文件系统和网络等。目前最流行的容器化实现是 Docker,除此之外,还有 LXC 和 Container Linux 等其他实现。
容器化技术
随着 Docker 的流行,容器化的实现也得到了广泛的应用。相对于基于 Hypervisor 的硬件虚拟化,容器化有很多优势。
传统部署流程的问题
在早期的软件开发实践中,开发和运维团队的职责划分并不清晰。开发团队的成员在自己的本地环境上开发,通过持续集成环境构建出可部署的工件(Artifact),部署的工作由运维团队来完成。根据开发团队提供的文档,在生产环境上安装应用及其依赖的外部服务,比如数据库和消息中间件等。
这样的开发部署流程最大的问题在于,无法保证开发时和运行时环境的一致性。经常出现的问题是,应用在开发人员的开发环境上可以正常工作,到了生产环境中则会出现各种问题。有可能开发人员在本地环境上为应用添加了一个新的参数,但是忘了更新安装文档,导致运维团队安装的生产环境应用出现问题。
下图是传统的部署方式,可以看出,最大的问题在于手动维护安装文档,任何手动维护的文档从根本上来说都是不可信的,文档可能与代码失去同步。
传统部署方式
开发和生产环境的这种不一致性,会随着应用的复杂性而加剧。从单体应用迁移到微服务架构,对应用部署的要求更高,需要部署的应用数量从一个变成多个,而且每个服务所使用的技术栈和依赖的外部服务都可能千差万别。如果以硬件虚拟化来实现,还需要为每个服务创建独立的操作系统镜像,这些镜像的管理、更新和维护都是一个巨大的挑战。
容器化的优势
容器化技术提供了一种更简洁的方式来描述可运行的应用,可运行对运维来说至关重要。以 Java 应用为例,开发人员在本地环境上进行开发和调试,通过持续集成构建出可部署的 JAR 文件。但对运维团队来说,这些 JAR 文件并不是可运行的,因为它们还缺少所依赖的运行时支持,最基本的运行时依赖是 JDK,Java 应用对依赖的 JDK 版本是有要求的,除此之外,Java 应用启动时还可能需要额外的参数,这些信息并不包含在 JAR 文件中。因此运维团队需要从开发团队中获取这些信息,开发团队通常使用文档来说明如何运行应用,而文档本身很容易与代码产生不一致。
使用容器化技术所创建的镜像包含了应用所依赖的全部内容。一个 Java 应用的镜像,除了包含应用本身的 JAR 文件之外,还包含所需的 JDK 和如何启动应用的信息。容器的镜像是自包含的,同时也是可运行的。运维团队所做的仅仅是从镜像中创建容器并运行。这就进一步明确开发和运维团队的职责,开发团队负责创建应用对应的镜像,而运维团队只负责管理基础设施和容器的运行。
如下图所示,在容器化的部署方式中,应用镜像是开发团队和运维团队的唯一交集。
图 3 容器化部署方式
容器镜像的最大优势是不可变,不可变性在运维中的作用巨大,这一点和虚拟机镜像类似,但是容器镜像更加轻量级。在进行版本更新时,如果出现未预期的问题,只需要用上一个版本的镜像重新运行容器,就可以快速回退。当在生产环境中发现问题时,开发人员可以在本地环境上运行同样版本的镜像来重现问题。
容器化解决了不同环境之间的一致性问题,开发团队产出的应用镜像,经过测试团队的测试之后,被部署到生产环境中。开发、测试和生产环境使用的是同样的不可变镜像,这样的一致性对于应用的更新至关重要。
虽然容器对所运行的进程数量没有限制,但是一般容器只运行一个进程。为了运行应用,除了应用本身的容器之外,应用所需的其他服务也运行在各自的容器中,这就要求协调不同容器的运行。容器编排工具的作用就是解决这个问题,常见的容器编排工具包括 Kubernetes、Docker Swarm 和 Apache Mesos。
在本专栏中将使用 Kubernetes 作为容器编排平台,但在开发中,会需要用到 Docker。
Docker 的使用
在众多容器化技术中,Docker 是最流行的一个,采用客户端 — 服务器的架构。服务器端是 Docker 后台程序,负责构建、运行和分发容器;客户端则通过 REST API 与 Docker 后台程序交互。
Docker 中两个最重要的概念是镜像和容器,镜像是创建容器的只读模板,可以从 Docker 注册表中下载,也可以创建自定义镜像。Docker Hub 是默认的镜像注册表,包含了非常多可用的镜像,企业内部也可以搭建自己私有的注册表。镜像虽然是不可变的,但是可以在已有的镜像上进行定制,得到新的镜像,这也是通常创建镜像的方式。容器是镜像的可运行实例,从镜像中创建出来的容器,可以被启动、暂停、停止和删除。
安装
Docker 的安装很简单。在本地开发环境中,Windows 和 MacOS 可以安装 Docker Desktop,对于 Docker Desktop 不支持的 Windows 版本,可以安装 Docker Toolbox;在 Linux 上则需要安装 Docker Engine。
安装完成之后,相关的操作可通过 docker 命令来执行。下面我们就一起来运行容器。
运行容器
当使用 docker run 命令运行容器时需要指定镜像的名称。下面的代码是运行 Nginx 对应的镜像,nginx 是镜像的名称;镜像名称前面没有注册表的地址,默认从 Docker Hub 获取,镜像名称之后的 1.17 是镜像的标签,用来区分不同的版本;--name 参数用来指定容器的名称。
$ docker run --name nginx nginx:1.17
docker run 运行的容器默认在前台运行,我们也可以使用 -d 参数让容器在后台运行。容器启动之后,可通过 docker ps 命令来查看运行容器的状态,容器运行之后,我们可以使用 docker exec 在容器中执行命令。下面的命令在名为 nginx的 容器中执行 hostname 命令。
$ docker exec nginx hostname
上述命令在执行 hostname 之后就会退出。在开发中,可能需要在运行的容器上执行很多命令,这个时候可以运行一个交互式的 shell。
$ docker exec -it nginx sh
我们分别使用 docker start、docker pause、docker stop 和 docker rm 来启动、暂停、停止和删除容器。Docker Desktop 提供了图形化界面来管理容器,如下图所示。
Docker Desktop
容器通常可以对外提供服务,为了在本地访问容器中的服务,我们需要把容器中开放的端口暴露到本地机器上。Nginx 容器暴露了 80 端口,可以使用 -p 参数来暴露端口。在下面的代码中,我们使用 -p 参数把容器的 80 端口暴露在本地机器上的 10080 端口,再使用 curl 来访问。
$ docker run --name nginx -p 10080:80 nginx:1.17 $ curl http://localhost:10080
对于运行的容器,则可以通过 docker logs 命令来查看日志,如 docker logs nginx。
有些镜像在创建时提供了可以进行配置的环境变量。在运行容器时,可以使用 -e 参数来传递环境变量。下面的命令运行的是 MySQL 8 容器,并指定了 root 用户的密码和数据库名称。
$ docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=myrootpassword -e MYSQL_DATABASE=demo mysql:8
运行容器的另外一个常见需求是在本地机器和容器中共享文件,我们可以使用 -v 参数来指定本机目录到容器目录的绑定关系。下面代码中的命令使用的是 OpenJDK 8 的镜像来编译当前目录下的 Java 源文件:
使用 -v 参数把本机上的当前目录绑定到容器上的 /tmp 路径上;
-w 参数设置容器中的工作目录;
--rm 参数的作用是在容器退出时自动删除该容器。
当该命令运行结束之后,可以在本机的当前目录下看到编译之后的 class 文件。
$ docker run --rm -v `pwd`:/tmp -w /tmp openjdk:8 javac Hello.java
创建 Docker 镜像
在应用开发中,我们通常需要从已有的镜像中创建自定义的镜像,镜像的创建方式可通过 Dockerfile 文件来描述。以 Java 应用为例,我们需要以 OpenJDK 的镜像为基础,把 JAR 文件作为镜像的一部分,并设置正确的启动参数。
以 Spring Boot 应用为例,下面的 Dockerfile 文件被用来创建该应用的镜像。在 Dockerfile 中:
FROM 声明了基础的镜像名称;
ADD 用来添加应用的 JAR 文件到指定目录;
CMD 声明了容器启动时执行的命令。
FROM openjdk:8 ADD target/*.jar /opt/app.jar CMD java -jar /opt/app.jar
完成之后我们再通过 docker build 命令构建镜像,-t 参数为创建的镜像指定名称和标签,在构建时需要提供 Dockerfile 所在的目录,命令中的“.”表示当前目录。
$ docker build -t myapp:1.0 .
镜像创建完成之后,我们通过 docker run 命令来运行:
$ docker run myapp:1.0
Docker Compose 的使用
Docker Compose 是 Docker 提供的容器管理工具,相对于其他编排工具,该工具使用简单,适用于本地开发,可以同时启动多个容器,并定义容器之间的关联关系。
以一个使用 Spring Data JPA 的 Spring Boot 应用为例,该应用在运行时依赖数据库服务,而且应用需要通过网络访问该数据库服务。我们可以用 Docker Compose 来同时启动两个容器,并定义其中的关联。Docker Compose 使用的是 YAML 文件来声明容器。
下面代码中的 docker-compose.yml 文件是用来启动应用和 MySQL 服务器的。在 services 中定义了两个服务:db 服务使用的是 MySQL 8 镜像,同时声明了所需的环境变量;app 服务没有使用已有的镜像,而是要求通过 docker build 命令来构建出镜像。app 服务也有同样的环境变量,其中 MYSQL_HOST 的值是 db,是 MySQL 服务的名称。这是因为 Docker Compose 会用服务名称来作为容器的主机名,当 2 个容器出现在同一个网络中,这时 app 服务就可以访问到 MySQL 服务器了。depends_on 声明了 app 服务对 db 服务的依赖关系,这样可以保证正确的容器启动顺序。
version: '3' services: db: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: myrootpassword MYSQL_USER: mysqluser MYSQL_PASSWORD: mysqlpassword MYSQL_DATABASE: demo app: build: . environment: MYSQL_HOST: db MYSQL_USER: mysqluser MYSQL_PASSWORD: mysqlpassword MYSQL_DATABASE: demo depends_on: - db
在 Spring Boot 应用的 application.properties 文件中,我们直接引用环境变量来配置数据库连接。
spring.datasource.url=jdbc:mysql://${MYSQL_HOST}:3306/${MYSQL_DATABASE} spring.datasource.username=${MYSQL_USER} spring.datasource.password=${MYSQL_PASSWORD}
使用 docker-compose up 命令可以运行 docker-compose.ym 文件描述的全部容器。
在上面代码的 docker-compose.yml 文件中,MySQL 相关的环境变量在两个服务的定义中重复出现。为了避免代码重复,我们可以把相同的环境变量放在一个文件中,如下面代码所示的 db.env 文件。
MYSQL_USER=mysqluser MYSQL_PASSWORD=mysqlpassword MYSQL_DATABASE=demo
接着我们可以在 docker-compose.yml 文件中使用 env_file 来引用该文件。
version: '3' services: db: image: mysql:8 environment: MYSQL_ROOT_PASSWORD: myrootpassword env_file: - db.env app: build: . environment: MYSQL_HOST: db env_file: - db.env depends_on: - db
Docker 和 Docker Compose 是本专栏中会用到的重要工具。在此我们对这两个工具做了简要介绍,这对于本课时中示例应用的开发已经足够了。
总结
容器化技术对微服务架构云原生应用的部署至关重要。本课时首先介绍了硬件虚拟化技术;接着介绍了容器化技术,以及容器化技术对于微服务应用部署的重要意义,Docker 是容器化技术的主流实现,本课时对 Docker 的使用做了简要的介绍;最后介绍了在开发环境中常用的容器编排工具 Docker Compose。