一个经过优化的微服务架构案例

ahkb6784 8年前
   <h2>前言</h2>    <p>大家都知道,基于单体(Monolith)和微服务(Microservice)架构的争论已经存在多年,正如我们对胖客户端、瘦客户端孰好孰坏的争论一样,有必然的历史演化,也有各自的优缺点。架构师们总是在考虑,我们是要一个中心化、全能多才的单体,还是百花齐放、各自为政的微服务群体,各种基于成本、交互、部署等等的讨论应该不会停下脚步。这里,我们不做过多的深入探讨和介绍,而本文正是这些讨论中的一个很好的例子,供大家思考。</p>    <p>Joakim Tengstrand 近期在他的推特上提到,看到 Robert C. Martin(Bob 大叔)多年前描述的基于 JAR 包 <a href="/misc/goto?guid=4958869581161680310" rel="nofollow,noindex">微服务</a> 的不足之处,于是写下了这篇有趣的称为 <strong>微单体</strong> (micro monolith)的文章,并在Git上发布了基于 Java 和 Clojure 的两种源码包供下载。接下来将逐一介绍微单体的概念、工作方式、各自的优缺点、示例、实践等环节。</p>    <h2>微单体是什么</h2>    <p>用成千上万或数百万行代码来编写高质量的软件,可能是开发人员所能承担的最具挑战性和最复杂的任务之一。Tengstrand 利用软件模块化这种非常简单的方式,实现了一个方案并期望能有助于完成这项挑战,提供了基于 Java 和 Clojure 的 <a href="/misc/goto?guid=4959737060675773533" rel="nofollow,noindex">代码</a> 供参考。</p>    <p>随着系统变得越来越大,最终会达到一个临界点,作为一个 <strong>单体</strong> ( <a href="/misc/goto?guid=4959737060766467655" rel="nofollow,noindex">monolith</a> )它变得难以管理。每一行代码的添加,都会让系统变得更加难以理解、变更和重用。虽然 <strong>微服务</strong> ( <a href="/misc/goto?guid=4958974292835080211" rel="nofollow,noindex">microservice</a> )试图解决这些问题,但也带来了额外的复杂性以及集成的成本。</p>    <p>微单体架构的核心原则是保持硬件、软件和数据紧密地结合在“一个地方”。这样处理可以简化事情,摆脱不必要的协调工作。如果我们从同一个地方直接访问数据,性能也会得到改善。当设计系统时,可以像微服务一样使用小的、孤立的、可组合的构建块(building blocks),又可以像单体一样通过一个地方执行它们,就能从两方面都达到最优。</p>    <p>从上面的介绍可以看出,单体就像一个胖服务,微单体利用微服务架构的优势将处理工作从逻辑上分拆出去,在此基础上增加一层调度(编排服务)来管理所有的微服务。这样一来,使得业务的完整性和一致性得到了较好的保证,也解决了跨服务集成的问题。用一个简单的例子来描述,单体就好像把所有的文件放在了一个文件夹里,微服务则试图将它们分类并放在不同文件夹中,而微单体的方法是生成了一个叫做 development 的文件夹,里面保存了所有文件的快捷方式(shortcut),这样更易于根据不同的业务场景来管理和访问这些“文件”。</p>    <h2>如何工作</h2>    <p>在版本控制系统里为每个服务生成一个项目,这样可以得到各自的 JAR 包(假设在 JVM 上运行,或者类似平台)。它们就是构成系统的构建块,并最终组成整个生产系统。生成一个 development 项目,通过服务把所有源代码连接起来,这样就可以在里面直接运行源代码,就像是只有这一个项目一样。</p>    <p>接下来介绍微单体架构的优缺点:</p>    <p>优点</p>    <ul>     <li>简单性(关注点分离,代码直接调用,消除了网络 API 的复杂度)</li>     <li>卓越的性能(没有访问服务的网络调用)</li>     <li>模块化、可组合的服务(在不同的系统中重复利用)</li>     <li>跨服务事务的数据一致性(无需考虑最终一致性)</li>     <li>减少 DevOps 和硬件/托管的费用(在单机上运行的系统)</li>     <li>易于测试(可以对整个系统进行一体化测试)</li>     <li>更快和更有效的开发体验(跨服务导航、重构和调试 + 变更无需重建服务)</li>    </ul>    <p>缺点</p>    <ul>     <li>必须在所有服务中使用相同的编程语言(*)</li>     <li>development 项目需要一些额外的设置(创建符号链接 symbolic links)</li>     <li>操作系统必须支持符号链接 symbolic links</li>     <li>共享相同路径的资源在所有服务中必须具有唯一的名称(它们有不同的内容)</li>     <li>IDE 内置的版本控制失效(因为微单体架构可能不支持 IDE) <p>(*) 并没有强迫只使用一种编程语言,目标是要让 development 项目发挥其优势(例如跨服务的重构和调试),这时最好的选择是使用一种语言。其次是混合使用,可以使用在同一个平台(比如 JVM)上运行的语言,像 Java、Scala、JRuby、 Clojure(如果使用本地接口 JNI ,还可以选择 C),但如果一个服务做了变更,就需要生成一个新的 JAR 包并共享给其他服务。</p> </li>    </ul>    <p>上面介绍了微单体方法的概念,还没有提到它是如何在实践中工作的,现在让我们通过 Java 和 Clojure 的两个示例来进行演示。所有示例的代码可以在 这里 找到。注:在实际系统中,它们被存储在单独的资源库里并且彼此隔离,这里为了方便起见,它们被存储在同一个资源库中。</p>    <p>下面 Java 和 Clojure 的例子会实现相同的“解决方案”,利用一个假造的 REST API 来编排一些服务,并暴露findaddresses,douserstuff 和 domoreuserstuff 供调用。</p>    <h2>Java - 示例代码</h2>    <p>Java 是一种流行语言,这里将展示在面向对象语言里的微单体架构是什么样子的。</p>    <p>在处理 development 项目时,你在大部分时间里会是一个开发人员。虽然所有的服务都被存为各自独立的项目( Git ),这里我们通过一个技巧,利用 符号链接 把所有源码“放到”一个单独的项目中。IDE 并不关心地址是“真正的”还是一个链接,都采用箭头来标记它们(至少在 这个例子 里是这样的):</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/e3b212feb6c57c46391fa5e866d58291.png"></p>    <p>项目在本地check out后,这些链接在 Linux 或 Unix 上 马上就能工作。如果是其它平台,可以参考 <a href="/misc/goto?guid=4959737060874171190" rel="nofollow,noindex">这样</a> 类似的脚本,手动地创建 development 项目。</p>    <p>建立了这个项目,通常意义的甚至跨服务的开发环境所具备的调试、重构和搜索等等方面的好处,我们都可以实现。这一点非常强大并且节省了时间,每次代码变更不需要重建服务,这样使得工作流程非常高效和快乐。</p>    <h2>依赖性</h2>    <p>在设计系统时,需要决定是否允许服务获得外部库(external libraries)具体实现的信息。在本文的例子里,我们选择信息透明,最好在所有服务中使用相同版本的外部库。</p>    <p>另一种选择是通过在内部服务与外部库之间添加接口来集成,这样一个服务就不用知道具体库的信息,比如 log4j-1.2.17.jar,只需要生成一个接口 log4j-api,这样编排服务(orchestrator service)就能把它注入到所需的服务中去了。</p>    <h2>编排服务(The orchestrator service)</h2>    <p>编排服务就是把所有服务都放到一起的那个服务。一个系统可以有多个编排服务,这里的例子只有一个 RestService ,它依赖地址,电子邮件和用户这几个服务,并在 pom.xml 里进行指定。</p>    <p>如果服务 A 需要调用服务 B 里的函数 f,可以通过编排服务把函数 f 注入到服务 A 中。这里没有强制要求一次只注入一个函数,但是这样做可以降低服务间的耦合,增加可变性(changeability)和可测性(testability)。我们将使用“微注入”这个术语,特指一次只注入一个函数的意思。</p>    <h2>测试</h2>    <p>微单体架构鼓励让测试变得简单、容易,就像微服务一样,要让每个服务的独立测试更方便。相比于微服务, 微单体就像一体化部署在一台机器上一样,它让测试变得更加的简单(比如本例的 REST API )。</p>    <p>例子里包含了一个测试数据生成器,它帮助我们在已知的状态下建立数据库。可以存在一个用户表和一个地址表相关联,然后就得到一个 UserService 和一个 AddressService。 测试数据生成器 可以方便地设置数据库的已知状态,这有助于编写集成测试。这可以在某个服务中完成并且实现跨服务调用,例如 AddressServiceTest 和 UserServiceTest 。</p>    <h2>Clojure - 示例代码</h2>    <p>Clojure 是一个功能强大的语言,能在 JVM 上运行。下面将展示在 Clojure 这样的函数式语言中如何使用微单体架构。所有 Clojure 代码可以在 这里 找到。</p>    <p>它的 development 项目看起来是这样的:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/db307b8b165b8de98bc324923d94b207.png"></p>    <p>Clojure 版本的结构基本类似 Java 版本,但函数都存储在不同命名空间而不是类中,也不需要像Java一样为地址和电子邮件服务添加额外的 API 层。Clojure 版本更加简洁,可以用大约 200 行代码实现 Java 里 400 行代码能完成的工作。</p>    <p>Clojure 里的微注入会更简单,可以用宏注入(inject macro)的方式来注入函数。这里的例子里,在命名空间 rest.service 的第8行,就是用函数 email/send-pdf-email! 替换了原来的 user.service/send-pdf-email! 。</p>    <h2>实践经验</h2>    <p>Tengstrand 和他的团队已经把微单体架构应用到了一个真正的生产系统。他们搭建了这样一个架构,每一个服务在 Git上 有各自的资源库。迁移到微单体是非常顺利的,要做的就是 <strong>丢弃 30% 的代码</strong> ,并把所有 REST 服务调用替换成简单的函数调用。这个过程中消失的不仅仅是 REST 部分,还有很多复杂的状态和错误处理。</p>    <p>从一开始,就像生产环境一样设置开发环境,每个服务就是一个 Java 存档(JAR 文件)。缺点是每次的服务变更,必须重建这个 JAR 文件,以便它可以供其他服务使用。另一个缺点是,我们必须重新启动 REPL (是的,使用的是Clojure!),这样做耗费了时间并把在 REPL 上工作的一些快乐也带走了。</p>    <p>于是,Tengstrand 的团队想出了新的方法来设置 development 项目,只需启动一次 REPL ,然后可以继续工作而不会被打断,这样一来开发人员就幸福多了。另一件事是,他们意识到服务里有一些灰色标记的死掉(dead)的代码,现在也可以去掉了。</p>    <p>另一个设计上的选择是采用 Datomic 数据库,它真的与微单体架构很适用,既简单又强大。你可以在 这里 了解它的更多架构细节。</p>    <p>他们使用测试数据生成器来处理几乎所有的服务,让集成测试更简单。之前他们发现可以改进服务中的一些变量和函数的命名,但更改之前必须手动搜索和替换所有的服务,这样做费时又容易出错。最后他们并没有做这些小的改动,而是通过development 项目,利用 IDE 对重构的支持瞬间就做到了变量和函数的重命名。</p>    <h2>总结</h2>    <p>微单体提出了如何构建系统的一个简单模式,虽然和微服务竞争但并不能完全取代它,因为后者肯定有它的位置。如果有需要的话,随时可以同时使用它们。</p>    <p>最后 Tengstrand 建议,如果在构建系统时非常关注简单性和可组合性,那么一定要尝试下微单体架构,享受这个架构带来的高效,以及测试、生产环境的简洁。</p>    <p> </p>    <p>来自:http://www.infoq.com/cn/articles/an-optimized-micro-service-architecture-case</p>    <p> </p>