我们进入微服务世界的旅程-以及我们从中学到的东西。

611 阅读22分钟
原文链接: mp.weixin.qq.com

本文为翻译发表,转载需要注明来自公众号EAWorld。

作者:Ignacio Salazar Williams

译者:白小白 

原题:

Our journey into the world of Microservices — and what we learned from it.

原文:

https://medium.freecodecamp.org/our-journey-into-the-world-of-microservices-and-what-we-learned-from-it-d255b9a2a654

全文7510字,阅读约需要15分钟

“旧地图上的指南针” 

by Himesh Kumar Behera on Unsplash

我知道,我知道每个人都在谈论微服务。人们谈起数字化转型,就会说微服务模式将引导我们走向架构的未来。还有人说,微服务是“巨石应用的毁灭者”,是解决我们所有架构问题的灵丹妙药。

要我说,微服务确实很了不起,但它不是说吹一口仙气然后施些魔法、一片仙气缭绕的解决我们所有的问题。我不打算从模式的角度再多讲什么,而是尽我最大的努力来讲述这个故事,我的故事。我将讨论这一概念和模式是如何在现实中、在某些情况下不断演进和变化的。我称之为微服务的世纪决战。

白小白

微服务的世纪决战,原文为Micro-Armageddon,Armageddon是基督教《圣经》所述世界末日之时善恶对决的最终战场。根据《圣经》记载,全能者会在此击败魔鬼和“天下众王”,进而引申为“伤亡惨重的战役”、“毁灭世界的大灾难”、“世界末日”等涵意。迈克尔•贝在1998年执导了同名的电影,由布鲁斯•威利斯主演。

来源:GIPHY

在我的团队里,每天总有些事情是我们力所不能及的,并导致了一些问题。但是,如果从大局着眼,以良好的心态不断改进我们的组件,总会让软件达到团队所期望的应有的质量标准。

所以,请跟我一起走过这段好坏参半、喜泪交织的旅程,并一起回顾太多的悔不当初。

小结 这篇文章有点长,但我跟你讲,如果想从他人关于微服务的错误中吸取教训,我强烈建议你通读这篇文章。当然,也可以跳着看看插画,至少乐呵乐呵,是吧。

1.一点背景


来源: Innoview 

让我们从头讲起。我呢(大家好!),是一个刚毕业的计算机科学专业的学生,刚刚受雇做一些咨询 (类似西部拓荒一类的) 工作。公司的一个客户在他们的办公室指派我进入了这个项目,我们的团队负责对他们的业务进行数字化转型。因此,需要引入微服务的理念。(随着我在这个领域的经验更加资深,我经常同时听到这两个概念一起出现。)

我们用Node.js作为后端编程语言(爽),因此我们使用Express(基于 Node.js 平台的 Web 应用开发框架)作为暴露API的默认框架。另外,很重要的是我们在项目中应用了基于敏捷理念的Scrum研发过程(你很快就会明白我为什么单独指出这一点)。

团队分工

Photo by rawpixel on Unsplash

我们被分成两个大组:我所在的第一组,是架构团队。我们负责指导团队方向并传播有关微服务的理念。第二组是开发团队,负责按照业务需求开发产品。有多个团队同时服务于不同的产品,同时也在从事微服务的工作。

架构团队的构成,类似如下:

  • 1名高级经理(我们的人)

  • 2名经理(1名我们的,1名客户的)

  • 10名架构师(6名我们的,4名客户的)

  • 2人负责大数据

  • 3人负责CI/CD

  • 3人负责安全

  • 2人负责前后端开发架构

每个开发团队的构成类似如下:

  • 1 名Scrum Master(也负责咨询)

  • 1名产品负责人(客户方)

  • 4名开发人员(2名我们的,2名客户的)

  • 1 人负责QA

  • 1人负责UX

  • 1名架构师(客户方)

我知道这听起来已经很糟糕了,一切成员角色混在一起了,但别担心,对我们来说,这正是一个改正错误的机会。

开发人员背景技能

没有人生来就是一个熟练的开发人员,我们所有人就像一只猴子,试图编译一个基本的“HelloWorld”。

——FelipeLazo

我们的团队成员有各种各样的背景,有些人对计算机一无所知,有些人来自美国宇航局。有些人使用过COBOL、Java、JavaScript、C、Python等,而其他人则完全没写过代码。

因此,很容易理解,一些团队成员不是特别擅长开发好的代码和结构,因为他们中的许多人就没有这方面的背景。同样,还有些人有一些经验。对这些不同的背景情况心中有数是有好处的,这将取决于我们如何对这些差异性进行充分利用。不应把这看作是一个弱点,而应该看作团队整体改进的机会(在敏捷环境中工作时尤其如此)。

2.目标

在这个项目里,我们的目标是把微服务作为一个后端解决方案,来集成客户所拥有的遗留组件。我们打算将它们暴露为简单的API,以便相关团队能够将它们集成到他们的应用程序中。

以下是我们对微服务的初步需求:

  • 调用SOAP服务并返回JSON结果。对大多数人(包括我)来说,这听起来会有点糟糕。但只能这样,因为微服务没有权限直连数据层,所以他们必须通过SOAP来获取数据【客户方初始需求】。

  • 将产生微服务的所有数据记录到全新的DataLake中。

  • 支持基本的认证机制。

  • 尽可能保证无故障运行。

在这些要求之外,我们还必须添加下面这些特性:

  • 通过单元测试达成期望的质量(此处有我们雄心勃勃的90%的覆盖率标准)

  • 静态代码分析

  • 性能测试

  • 一些安全检查。

所有这些都必须在本地手动检查,然后通过严格的管道(CI/CD)进行检查。说是严格的检查,但也不是一票否决式的。即使有某项要求不满足,仍然允许部署微服务。但我建议大家永远不要这样做,或者至少要了解这样做的后果。

到目前为止,我们没有遇到太多的问题。作为开发微服务所需的一个基本条件,这听起来相当不错。我们有DevOps,在同一个地方工作,我们有我们的方法和模式,我们有一个超棒的运行时(Node.js)环境,所有这一切让我们可以一步步地构建和遵循规则,使这个项目成为一个杰作。嗯,至少当时我们是这样想的…

3.完蛋,出错了

团队试图拯救微服务的精准描述

看看这张相当写实的图片,图中的架构团队试图将微服务从水深火热中拯救出来。你可能会问,为什么会发生这种事?在敏捷开发场景下,允许多个团队自由开发自己的微服务时,就会发生这种情况。微服务是什么?能干什么?为什么这么干?如何治理?重要的是,应该以怎样的颗粒度进行切分?如果不对这些问题做出充分的解释,麻烦就一定会出现。

而且,在项目开始时,除了Subversion之外,我们没有任何可靠的版本控制软件。其时,我们正试图内部部署Git。

所以,基本上基本上就会导致下面这样的场景:

  • 微服务:“客户”(由A组、B组和C组负责)

-B组厌倦了所有的合并和部署工作,也厌倦了谁来为此负责而进行的争吵。

  • 微服务:“贷款客户”(B组负责)

-B组把“客户”微服务的所有状态原封不动的复制过来。这就导致了伴随有用的endpoint暴露了越来越多的无用的endpoint,并且带来大量的维护工作。

小白

实际上微服务中的endpoint的概念类似MVC中的controller的概念,包括后面列出的代码也展示了这一点。

我们经常看到的一个问题是,许多不成熟的团队不仅没有试图扑灭这场大火,反而在不断重复着构建这类微服务,并基于此开始更多的开发工作,从而使火势进一步蔓延。这使得微服务体系越来越庞大,并且包含了无用和重复的内容。

大概情况就是这样。我们到底该如何解决所有这些(地狱一样)的问题?以下是我们的解决方案。

4.识别症状

Photo by rawpixel on Unsplash

很明显,我们不能任由这种乱相继续下去,所以,宛如疾控中心控制埃波拉病毒的漫延一样,我们穿上白大褂,消毒了房间,并对我们的现状进行了检视。就像前面所说的,只要是让各团队自由的开发自己的微服务而不做任何事先的规约,很多灾难就是不可避免了。我们识别了可以产生灾难的一系列症状,并将最重要的症状列为优先事项,并试图通过达成一些小目标,来逐渐控制局势。

建立一些小目标,不仅有助于明确问题,而且还让团队成员知道在改善日常工作方面还是可以有所作为的。

5.“微-巨石”(Mini-Monolith)

或曰

“宏服务”(Macroservices)

等一下!不是说微服务的事么…

这是宏服务?

我相信你已经对S.O.L.I.D.原则( https://hackernoon.com/solid-principles-made-easy-67b1246bcdf )烂熟于心,讲述智能、紧凑的单元应该具备著名的单一原则理念。

小白

我很怀疑有多少人能够讲得清SOLID原则,网上的文章,能够清晰明了讲清楚的也不多。有两篇文章可以参考:

https://dwz.cn/QwndtvVu

https://dwz.cn/Qx4U1ijz

前一篇的例子较好,后一篇的解释较通俗,结合在一起看,应该能了解个大概。

我们这里讲的不是这个。这也是为什么当我观察到周遭所发生的事情时,我称之为宏服务

小白

按照作者后面的描述,其实问题的产生,恰恰在于没有按照SOLID的单一职责和接口分离原则来进行微服务的拆分,才产生的大而无当的“宏服务”。

想象一下:在一个简单的领域,比如用户域中,对同一个“微服务”就有15种POST操作。所有的操作都在一个相同的领域中,但却都有着不同的目标,并使用独有的定制库。此外,我们还要在这样的场景下进行所有的单元和性能测试。这简直是一场动乱。差不多是这样的情景:

├── app --The whole MS is in here│   ├── controllers --All the controllers of the domain│   │   ├── dummies│   │   │  └── ** All the dummies for each controller **│   │   ├── xsl│   │   │   └── ** All xsl configuration for each controller **│   │   ├── Controller1.js│   │   ├── Controller2.js│   │   ├── Controller3.js│   │   ├── Controller4.js│   │   ├── Controller5.js│   │   └── **Literally 20 more controllers**│   ├── functions --All the functions of the MS│   │   ├── function1.js│   │   ├── function2.js│   │   ├── function3.js│   │   └── function4.js│   ├── properties --All the properties of the MS│   │   ├── propertie1.js│   │   └── propertie2.js│   ├── routes --All the routes of the MS│   │   ├── routes_useSecurity.js│   │   └── routes_withoutSecurity.js│   ├── services --Extra services that were consumed│   │   ├── service1.js│   │   └── service2.js│   └── xsl│      └── **A bunch of XSL to do transformations**├── config --"Global" configurations│   ├── configSOAP.js│   ├── configMS.js│   ├── environments.js│   ├── logging.js│   ├── userForBussinessA.js│   └── userForBussinessB.js├── package.json├── README.md├── test--All the tests│   ├── UnitTesting│   │   └── Controllers│   │       └── ** All the 25 tests in theory **│   └── PerformanceTest│       ├── csv_development.txt│       ├── csv_QA.txt│       ├── csv_production.txt│       ├── performance1.jmx│       └── performance2.jmx├── server.js --Express Server├── serverKey.keytab├── sonarlint.json├── encryptor├── ** Around 10 more useless files **└── Dockerfile

所以如果A组修改了Controller1,他们必须通过管道进行集成,很有可能失败(然后部署也会失败)。他们得把这个过程不断地重复,直到他们成功为止。因此,所有的队伍都争先恐后的不做最后部署那一组。因为如果有什么东西在部署中失败了,就得背锅,承认自己做错了把事情搞糟了。

这基本上就是我的惨状了

好玩吧?这对于所有开发者来说是一个多么“健康”的环境。谁不想在那里…反正不是我!

无论如何,这种现状必须停止,因为这样根本无法治理。即使为了在DEV环境中做一些测试(这是常规操作),也必须通过CI/CD管道进行构建和部署,团队为此心力交瘁。在项目的这个阶段,这显然是难言完美的。

6.是时候重新开始了

我宣布:破产!

我们需要重新开始,让事情回归正轨。把控谁在做什么,让大家各司其职。当然,我们也必须做到公平:我们不会让一个团队完全负责包含15种操作的整个领域,毕竟这里有测试、部署等太多事情。没人能一力承担。

你知道,我们是搞敏捷的,敏捷人做敏捷事。我们不需要把宝贵的时间浪费在谁该负责的事情上,更没有必要为此大打出手、指指点点、*翻白眼*。

步骤1:调整微型服务的规模

在此,我要做一个大胆断言:所有微服务的最大操作数量必须遵循CRUD标准。以这个为前提,不需要再考虑微服务应该有多大颗粒。

遵循这条规则会让你在夜晚获得心灵的宁静,因为你知道,在任何给定的时间任何子域中最多只需要有4个操作。仅此而已。

就是说:

  • POST - Create

CREATE过程是插入新数据作为微服务的持久化。

  • GET - Read

READ过程,读取客户端所需的数据。

  • PUT - Update

UPDATE过程修改记录而不覆盖。

  • DELETE – Delete

DELETE过程删除指定的数据。

采用这样的规则使我们能够开发更加紧凑、更智能和更标准的微服务。当需要划分微服务的时候,这会让我们更加从容。

假设银行域里有一个“客户”微服务,突然我发现我不仅需要表达信用卡客户而且需要贷款用户,很简单。只需将客户域划分为两个子域:“信用卡客户”和“贷款客户”,由此,你可以看到一切都是如何回归正轨。

完美!我们现在有了合适的微服务。接下来需要做的就是让客户和团队找到一种方式来了解如何拆分域,并了解它们的子域。

有没有现成的方法呢?…(敲黑板)当然有,那就是领域驱动设计(Domain Driven Design)

(地址:

 https://medium.com/withbetterco/what-is-domain-driven-design-bcf81fc4fdc1 )

步骤2:确定负责人

我们解决了一个问题,可以长出一口气,但还没完,现在的情况是,有一堆微服务,每个人都在参与开发,但出了问题却没人负责。

我的观点是:“谁写的代码,谁负责”。对这句至理名言,你可能会说:“这我早就知道,大家也都知道。”并不是这样,不仅不是每个人都知道这一点,而且这是一个常见的错误。要学聪明一点,把它作为一项规则。

(图)来自D. Keith Robinson 的文章

 Learn to love Git

只要把Git用好,可以让你安心开发 (参见上面 D. Keith Robinson 的文章链接 https://medium.com/designing-atlassian/learn-to-love-git-part-one-the-basics-90429f456ace ),因为你知道你的代码永远都是最新的。如果有人想要改进它,提出改进的建议,或者只是需要一个更新,所有这些都必须经过代码的负责人。就本文的例子来说,负责人是开发代码的DEV团队的架构师。这在敏捷场景下非常有效。

步骤3:API端点(命名)和版本控制

正确处理命名API的方式可以节省开发人员的大量时间和精力。命名API不是游戏。它可以拯救生命。

良好的命名可以令微服务增值。如果你实在不会起名字,就问问业务人员,并与你的团队一起讨论确定。设计驱动的开发在这里可能会有所帮助。

了解更多,可以看看RESTful API 设计指南与最佳实践这本书。( https://hackernoon.com/restful-api-designing-guidelines-the-best-practices-60e1d954e7c9 ) 毕竟,我不能把整篇文章都引用过来。

步骤4:重构

(图)“一个小朋友在玩Jenga积木塔的游戏” by Michał Parzuchowski  on Unsplash

建立正确的理念固然重要,但如何将理念付诸实践?

我将向您展示的下一个文件树将体现我作为微服务概念的追随者的看法。我将在服务的设计理念上遵循松耦合和高内聚的概念:

├── config│   ├── artillery.js│   ├── config.js│   ├── develpment.csv│   ├── processorArtillery.js│   ├── production.csv│   └── qa.csv├──index.js├──package.json├──package-lock.json├──README.md├──service│   ├── getLoans --Theoperation│   │  ├── getLoans.config.json --Configuration of theresource│   │  ├── getLoans.contract.js --Contract test│   │  ├── getLoans.controller.js --Controller│   │  ├── getLoans.performance.json --Performance test config│   │  ├── getLoans.scheme.js --Scheme validator│   │  ├── getLoans.spec.js --Unit Tests│   │   └── Util --Localfunctions│   │      ├── trimmer.js│   │      └── requestHandler.js│   ├── postLoans│   │  ├── postLoans.config.json│   │  ├── postLoans.contract.js│   │  ├── postLoans.controller.js│   │  ├── postLoans.performance.json│   │   ├──postLoans.scheme.js│   │  └── postLoans.spec.js│   └── notFound│       ├── notFound.js│       ├── notFound.performance.json│       └── notFound.spec.js├──Util --Global functions│   ├── headerValidator.js│   ├── bodyValidator.js│   ├── DBConnector.js│   └── BrokerConnector.js├──sonarlint.json└──sonar-project.properties

这一理念意味着在实施DDD的过程中,不仅使域或子域的概念可替换或可拆分,同时在文件和目录的层面也是如此。当然,在本例中,是一个Node.js的例子。

按照上面的设计,微服务的每个操作(就像getLoans和postLoans一样)都有满足其开发、配置、单元测试、性能测试、契约测试、方案验证要求的所有组件和控制器。当我们的微服务的体积鼓胀的过大,必须作分割时,以操作为单位来考虑有利于我们对分割过程施加控制。比如,我们只需要将整个文件夹移动到相应的新服务当中去,仅此而已,既不需要找到正确的依赖组件,也不需要调来调去以使其再次工作。

注:我们动态地生成API路由,因此每个操作都具有足够的自描述性,可以和项目的package.json一起,来建立我们所需要暴露的路由。这让我们有了我们想要的灵活性:不再手工编辑路由(这也是经常会出错的地方,必须尽量避免)。例如:

  • 动词/{Name of Artifact}}/{{Domain}}/{{Version}}/{{Subdomain}}/

-- Name of Artifact:你在暴露什么样的工件(微服务,BFF,或任何其他)?

 -- Domain:如字面含义,即操作所属的域。

 -- Version:当前可用的资源的主版本。

 -- Subdomain:微服务在CRUD的原则下执行的操作。

  • GET/Microservice/Client/v1/loan/ -- 获取所有客户所做的所有贷款。

这听起来真的很神奇,也是我强烈推荐的方式。基于这样的方法,你将发现在组织微服务时遇到的大部分问题将大大减少。

步骤5:文档

“我们要试试一个叫什么敏捷编程的东西”

“听说不用计划不写文档,就是写代码加抱怨就行了”

“还好有名字”

“培训的事就交给你了”

(图)Dilbert的精彩漫画,来自 这里

一如这漫画中的场景一旦发生,我不得不说,那真得吓我一跳。我可以想象所有的敏捷实践者,竭声尖叫着 “Scrum之魂”来为这一方法论正名的样子。但别担心,这件事我帮你搞定了。

我将介绍两个概念:第一,也是最重要的,因为我们暴露了API,让我们都来试试API优先开发方法(API First Development)

API First Development是一种策略,意味着开发一个API的首要任务是考虑目标开发者的利益,然后才在此基础上构建产品,无论是网站、移动应用程序还是SaaS软件。如果可以在基于API进行应用开发的过程中,始终考虑开发者的利益,您和您的开发人员不仅节省了大量的工作,同时为其他人在此基础上构建自己的应用奠定了基础。(参见来自Restcase的 API-优先开发方法 https://blog.restcase.com/an-api-first-development-approach/ )。

小白

API first不只是指出API的重要性,更重要的是体现了一种时间上的建议顺序。由于API编程的理念出现较晚,很多网站先是有Web接口,而后有API接口,同时两种接口并存。按照API First的理念,应该是API在先,而应用程序在后。也就是说,应用程序的功能还没完备,先要设计API,然后再基于API来构建应用程序,这样会就强迫开发者自己先“消费”自己的API,从而设计出真正开发者友好的API。By Lee Provoost From Quora

你可能会问,怎么实践这一策略呢?轮到第二个重要概念登场了:Swagger,构建API的众多工具之一。这个工具将打开以一扇以干净和有组织的方式设计和建模API的大门。

我想,这个工具会超出你的期待。Swagger不仅解决了我们通常遇到的有关文档的敏捷性问题,而且还改进了团队开发微服务的方式。这一工具为团队间提供了正确的交互工具,因此,任何进一步的迭代都将围绕文档良好的API进行。并且,也不会听到别的团队有这样的抱怨:“我的团队想要…这些输出结果,想要API有…那些特性,你看看你给我们设计了个啥?”对此,你就可以理直气壮地说:“这是我们API的文档,由架构师设计和批准,也满足业务的需求”。话题到此为止。

Swagger是为了描述一套标准的而且是和语言无关的REST API的规范。对于外部调用者来说,只需通过Swagger文档即可清楚Server端提供的服务,而不需去阅读源码或接口文档说明。关于Swagger,EAWorld此前发过一篇文章,可供参考《微服务架构实战:Swagger规范RESTful API》 

步骤6:培训

正如我早些时候所说,如何充分利用我们的开发人员和团队的成员差异性,这取决于我们自己。慢慢来,找出缺点并加以改进!

(图)李小龙

我知道每个人在训练他们的团队时都有不同的偏好,但如果是在敏捷开发的环境下,而目标是优化团队时间的话,我强烈推荐Coding Dojo(http://codingdojo.org/ )。这种培训技术使我们能够培训所有的团队,使他们在每个方面拥有相同的基本专业水平,无论是Node.js、微服务、单元测试、性能测试或是其他。这种技术也改善了信息传递给团队的方式--我们都玩过打电话的游戏,多数时候游戏的结果没有什么不同。没有人有时间从头阅读经年累月的文档。我们甚至可以将来自团队的反馈应用到我们的日常工作中。所以每个人都赢了!

关于打电话的游戏。我理解,其实就是“传话游戏”,一堆人站一排,给第一个人一个描述,让第一个人用动作表演给第二个人,往往到最后一个人的时候,最初的描述已经面目全非了。这个过程体现的是传递过程中信息的衰减和失真,每个人的表达都会丧失一部分精准性。但我也经历过这样的场景,前面给传的乱七八糟,然后到了某个人不知怎么就灵光一现的把楼给正回来了。关于coding dojo,简单讲就是一堆人在一起做一个小项目,相当于一种会议式的训练方法,大家可以取长补短并迅速获得知识的共享。这里有一篇中文介绍 https://dwz.cn/m8O5dD7w,这里还有一个在线方式参与的虚拟的线上dojo https://dwz.cn/Rx0CWRRD

7.吸取的教训和最后的话

(图)Photo by Hello I'm Nik on Unsplash

对我来说,这是关于生态系统中的个体之间是如何相互作用的话题。这也是关于学习如何对它们作出反应的过程。因为我可以向你保证,总有一天,你会前脚想出一个解决问题的办法,后脚就为了适应需求最终采取一些完全不同的方法。这就是微服务的美丽之处。微服务带来一定的灵活性,不管开始时情况看起来有多糟,只要遵循组件可替换松耦合,和高内聚的原则,相信我,一切都会好起来的。

微服务的实现是为勇士准备的旅程。他们愿意每天不断改进,也意识到哪些事情可以做得更好,他们具备大局观,也具备把事情做好的能力。

就像我之前说过的,我开始的时候并不是专家,而且也犯了错误。但这并没有阻止我做正确的事。对于你们所有的人来说,我可以告诉你们:暂停,深呼吸,做你自己的诊断和改善。做对事还不晚。


关于作者:Ignacio Salazar Williams 计算机科学家,全栈工程师, 架构师。 微服务的粉丝和从业者。