您是否始终需要Docker,微服务和反应式编程?



DataArt解决方案架构师Denis Tsyplakov发布

在DataArt中,我有两种工作方式。 首先,我会帮助人们修复由于多种原因以一种或另一种方式损坏的系统。 在第二篇文章中,我将帮助设计新的系统,以使它们在将来不会损坏,或者更现实的是,要破坏它们比较困难。

如果您没有做根本上没有新的事情,例如,世界上第一个控制核导弹发射的互联网搜索引擎或人工智能,那么创建一个好的系统设计就非常简单。 充分考虑所有需求,研究相似系统的设计并进行相同的操作,而不会犯任何严重的错误就足够了。 听起来这似乎过于简化了,但让我们回想起在院子里是2019年,几乎所有东西的系统设计都有“标准配方”。 企业可以处理复杂的技术任务-例如,处理一百万个异构PDF文件并从中删除费用表-但是系统架构很少是非常原始的。 这里的主要目的是在确定我们要构建的系统时不要犯错误,也不要错过技术选择。

上段经常会出现典型的错误,我将在一篇文章中讨论其中的一些错误。

选择技术堆栈的困难是什么? 在项目中添加任何技术都会使其变得更加困难,并带来一些限制。 因此,仅当该工具比有害工具有用时,才应添加新工具(框架,库)。 在与团队成员讨论添加库和框架的过程中,我经常开玩笑地使用以下技巧:“如果要向项目添加新的依赖项,请为团队放一盒啤酒。 如果您认为这种依赖一箱啤酒的做法不值得,那就不要添加它。”

假设我们用Java创建了一个特定的应用程序,然后将TimeMagus库添加到项目中以操纵日期(示例是虚构的)。 该库非常出色,它为我们提供了许多标准类库中没有的功能。 这样的决定怎么会有害? 让我们看一下可能的情况:

  1. 并非所有开发人员都知道非标准库,因此新开发人员的入门门槛将更高。 使用未知库处理日期时,新开发人员会犯错的机会增加了。
  2. 分布的大小正在增加。 当Spring Boot的平均应用程序大小可以轻松增加到100 MB时,这绝不是一件容易的事。 我看到了出于一种方法而将30 MB的库放入分发工具包的情况。 他们用这种方式证明了这一点:“我在上一个项目中使用了这个库,并且那里有一个方便的方法。”
  3. 根据库的不同,开始时间可能会大大增加。
  4. 库开发人员可以放弃自己的想法,然后库将开始与Java的新版本冲突,或者将在其中检测到错误(例如,由于更改时区而引起的错误),并且不会发布任何补丁。
  5. 库许可证有时会与您的产品许可证冲突(您是否检查所有使用的产品的许可证?)。
  6. 哎呀-TimeMagus库需要最新版本的SuperCollections库,然后几个月后,您需要连接该库以与第三方API集成,该第三方API不适用于最新版本的SuperCollections,仅适用于2.x版。 您无法连接API。没有其他库可以使用该API。

另一方面,标准库为我们提供了操作日期的便捷工具,例如,如果您不需要维护某种奇特的日历或计算从今天到“飞鹰升起的前一年的第三次新月的第二天”的天数,也许避免使用第三方库。 即使它非常完美并且在项目规模上,它也可以为您节省多达50行代码。

所考虑的示例非常简单,我认为做出决定很容易。 但是,有很多技术在每个人的耳边都广为流传,并且使用起来很明显,这使得选择更加困难-它们确实为开发人员提供了巨大的优势。 但这并不总是需要将它们拖到您的项目中的机会。 让我们看看其中的一些。

码头工人


在这种非常酷的技术出现之前,在部署系统时,存在许多与版本冲突和模糊的依赖性相关的令人不快和复杂的问题。 Docker允许您打包系统状态的快照,将其滚动到生产环境中并在其中运行。 这可以避免上述冲突,这当然是很大的。

以前,这是通过某种怪异的方式完成的,有些任务根本无法解决。 例如,您有一个使用ImageMagick库处理图像的PHP应用程序,您的应用程序还需要特定的php.ini设置,并且该应用程序本身是使用Apache httpd托管的。 但是有一个问题:一些常规例程是通过运行cron的Python脚本实现的,这些脚本使用的库与应用程序中使用的库的版本冲突。 Docker允许您将整个应用程序以及设置,库和HTTP服务器打包到一个在端口80上处理请求的容器中,并将例程打包到另一个容器中。 所有这些将完美地工作,并且您可以忘记库的冲突。

我应该使用Docker打包每个应用程序吗? 我的意见:不,不值得。 该图显示了部署在AWS中的dockerized应用程序的典型组成。 此处的矩形表示我们拥有的绝缘层。



最大的矩形是物理机器。 接下来是物理机的操作系统。 然后-Amazonian虚拟器,然后-虚拟机的操作系统,然后是docker容器,然后是容器OS,JVM,然后是Servlet容器(如果它是Web应用程序),并且您的应用程序代码已经在其中。 也就是说,我们已经看到了很多绝缘层。

如果我们查看缩写词JVM,情况将更加糟糕。 奇怪的是,JVM是Java虚拟机,也就是说,实际上,我们在Java中始终至少有一个虚拟机。 首先,在这里添加额外的Docker容器通常不会带来明显的优势,因为JVM本身已经使我们与外部环境完全隔离,其次,这并非没有代价。

我从两年前的IBM研究中得出的数据,如果没有弄错的话。 简而言之,如果我们谈论的是磁盘操作,处理器使用或内存访问,则Docker几乎不会增加开销(字面上的一小部分),但是如果我们谈论的是网络延迟,则延迟非常明显。 它们并不是巨大的,但是取决于您所使用的应用程序,它们可能会让您不愉快。



另外,Docker会占用额外的磁盘空间,占用部分内存,并增加启动时间。 对于大多数系统而言,所有这三点都不重要-通常会有大量的磁盘空间和内存。 通常,启动时间也不是关键问题,主要是应用程序启动。 但是仍然存在内存可能用完的情况,并且由二十个从属服务组成的系统的总启动时间已经非常大。 此外,这会影响托管成本。 而且,如果您从事任何高频交易,则Docker绝对不适合您。 在一般情况下,最好不要对任何对网络延迟不超过250–500 ms敏感的应用程序进行泊坞。

此外,使用docker,分析网络协议中的问题非常复杂,不仅延迟增加,而且所有时间安排都不同。

Docker何时真正需要?


当我们有不同版本的JRE时,最好拖着JRE。 有时您需要运行特定版本的Java(不是“最新的Java 8”,而是更具体的版本)。 在这种情况下,最好将JRE与应用程序一起打包并作为容器运行。 原则上,很明显,由于JAVA_HOME等原因,可以在目标系统上放置不同版本的Java。但是从这个意义上讲,Docker更加方便,因为您知道JRE的确切版本,所有内容打包在一起,并且与另一个JRE一起使用,应用程序甚至不会意外启动。 。

如果您依赖某些二进制库(例如,用于图像处理),则Docker也很有必要。 在这种情况下,最好将所有必需的库与Java应用程序本身打包在一起。

以下情况涉及一种系统,该系统是用各种语言编写的各种服务的复杂组合。 您有一个关于Node.js的文章,一个Java的一部分,Go的一个库,以及Python的某种机器学习。 必须对整个动物园进行仔细仔细的调整,以使它们的元素互相看到。 依赖关系,路径,IP地址-所有这些都必须在生产中绘制并仔细提出。 当然,在这种情况下,Docker将为您提供很多帮助。 而且,在没有他帮助的情况下这样做简直是痛苦的。

当您需要在命令行上指定许多不同的参数来启动应用程序时,Docker可以提供一些便利。 另一方面,bash脚本通常在一行中就可以很好地做到这一点。 决定使用哪个更好。

立即想到的最后一件事是使用Kubernetes时的情况,您需要进行系统编排,即,提出一定数量的不同微服务,这些微服务会根据某些规则自动扩展。

在所有其他情况下,Spring Boot足以将所有内容打包到一个jar文件中。 而且,原则上,springboot jar是Docker容器的一个很好的比喻。 当然,这不是同一回事,但是就易于部署而言,它们实际上是相似的。

Kubernetes


如果我们使用Kubernetes怎么办? 首先,这项技术允许您将大量的微服务部署到不同的计算机上,对其进行管理,进行自动缩放等。但是,有许多应用程序可以使您控制业务流程,例如Puppet,CF引擎,SaltStack等。 Kubernetes本身当然很好,但是它会增加可观的开销,并不是每个项目都可以使用。

我最喜欢的工具是Ansible,在需要时与Terraform结合使用。 Ansible是一个相当简单的声明性轻量级工具。 它不需要安装特殊代理,并且具有可理解的配置文件语法。 如果您熟悉Docker compose,将立即看到重叠的部分。 而且,如果您使用Ansible,则无需进行预定义-您可以使用更传统的方法来部署系统。

显然,这些都是不同的技术,但是在某些任务中它们是可以互换的。 认真的设计方法需要分析哪种技术更适合正在开发的系统。 以及几年后将如何更好地匹配它。

例如,如果系统上不同服务的数量很少并且它们的配置相对简单,例如,您只有一个jar文件,并且看不到任何突然的,爆炸性的复杂性增长,则可以通过经典的部署机制来解决。

这就提出了一个问题,“等等,一个jar文件如何?”。 该系统应包含尽可能多的原子微服务! 让我们看看使用微服务的系统应该由谁组成。

微服务


首先,微服务允许实现更大的灵活性和可伸缩性,并允许对系统各个部分进行灵活的版本控制。 假设我们已经将某种应用程序投入生产多年。 功能正在增长,但是我们不能无休止地进行广泛的开发。 举个例子

我们在Spring Boot 1和Java 8中都有一个应用程序。这是一个极好的,稳定的组合。 但是这一年是2019年,无论我们是否喜欢,我们都需要朝着Spring Boot 2和Java 12迈进。即使是从大型系统向Spring Boot的新版本的相对简单的过渡也可能非常费力,但是要想将深渊从Java 8过渡到Java 12我不想说话。 也就是说,从理论上讲,一切都很简单:我们进行迁移,更正出现的问题,测试所有内容并在生产环境中运行它们。 实际上,这可能意味着几个月的工作不会为企业带来新的功能。 如您所知,对Java 12的一些改动也不起作用。 在这里,微服务架构可以为我们提供帮助。

我们可以将应用程序的一些紧凑功能组分配到一个单独的服务中,将该功能组迁移到新的技术堆栈中,然后在相对较短的时间内投入生产。 逐步重复该过程,直到旧技术完全用尽。

同样,当一个下降的组件不会破坏整个系统时,微服务可以提供故障隔离。

微服务使我们拥有灵活的技术堆栈,也就是说,不要以一种语言和一种版本将所有内容全部编写成一个整体,并且在必要时对各个组件使用不同的技术堆栈。 当然,使用统一的技术堆栈会更好,但这并不总是可能的,在这种情况下,微服务可以提供帮助。

微服务还提供了一种技术方法来解决许多管理问题。 例如,当您的大型团队由在不同公司工作的不同小组组成(位于不同时区并且使用不同的语言)。 微服务通过将要单独开发的组件帮助隔离这种组织多样性。 团队一部分的问题将保留在一项服务中,并且不会遍及整个应用程序。

但是微服务并不是解决这些问题的唯一方法。 奇怪的是,几十年前,对于其中的一半,人们想出了类,后来又提出了-组件和控制反转模式。

如果我们看一下Spring,就会发现它实际上是Java进程内部的微服务架构。 我们可以声明一个组件,从本质上讲,它是一种服务。 我们可以通过@Autowired进行查找,还有一些工具可以管理组件的生命周期,还可以从十几种不同的来源分别配置组件。 原则上,我们几乎可以获得微服务所拥有的所有内容-仅在一个流程内,从而显着降低了成本。 常规Java类是相同的API协定,还允许您隔离实现细节。

严格来说,在Java世界中,微服务与OSGi最相似-除了几乎可以在不同服务器上使用不同的编程语言和代码执行之外,我们几乎可以完全复制微服务中的所有内容。 但是,即使不影响Java类的功能,我们也有一个功能强大的工具来解决大量隔离问题。

即使在团队隔离的“管理”方案中,我们也可以创建一个单独的存储库,其中包含一个带有明确的外部合同和一组测试的单独的Java模块。 这将大大降低一个团队无意中使另一团队的生命复杂化的能力。



我再三听到,没有微服务就不可能隔离实现细节。 但是我可以回答,整个软件行业都只是隔离实现。 为此,首先创建了子例程(上个世纪50年代),然后是函数,过程,类和后来的微服务。 但是,本系列中的微服务最后出现的事实并不能使它们成为开发的最高点,也没有迫使我们始终求助于它们的帮助。

使用微服务时,还应考虑到它们之间的调用会花费一些时间。 这通常并不重要,但是我看到一种情况,客户需要调整系统3秒的响应时间。 连接到第三方系统是合同义务。 调用链通过数十个原子微服务,并且进行HTTP调用的开销无法在3秒内缩减。 通常,您需要了解,将整体代码划分为许多服务不可避免地会影响系统的整体性能。 仅仅因为不能在“进程”和服务器之间“免费”传送数据。

什么时候需要微服务?


在什么情况下,单片应用程序确实需要分为几个微服务? 首先,当功能区域中的资源使用不平衡时。

例如,我们有一组API调用来执行需要大量处理器时间的计算。 有一组API调用可以非常快速地执行,但是需要将繁琐的64 GB数据结构存储在内存中。 对于第一组,我们需要一组计算机,总共有32个处理器,对于第二组计算机就足够了(好吧,让两台计算机具有容错能力),并具有64 GB的内存。 如果我们具有整体应用程序,则每台计算机上将需要64 GB的内存,这会增加每台计算机的成本。 如果将这些功能分为两个单独的服务,则可以通过针对特定功能优化服务器来节省资源。 服务器配置可能如下所示:



微服务是必需的,如果我们需要认真扩展一些狭窄的功能区域。 例如,一百个API方法被称为每秒10次,例如,四个API方法被称为每秒10,000次。 通常不需要扩展整个系统,也就是说,我们当然可以将所有100种方法乘以许多服务器,但是通常,这比扩展一组狭窄的方法更加昂贵和复杂。 我们可以将这四个调用分成一个单独的服务,并将其仅扩展到大量服务器。

同样很明显,如果我们已经编写了一个单独的功能区域(例如,使用Python),则可能需要微服务。 因为某些库(例如,用于机器学习的库)原来仅在Python中可用,所以我们希望将其分成单独的服务。 如果系统的某些部分容易出现故障,那么进行微服务也很有意义。 当然,编写代码以使原则上没有故障是件好事,但原因可能是外部的。 没有人能够避免自己的错误。 在这种情况下,可以将错误隔离在单独的进程中。

如果您的应用程序不具备以上任何条件,并且在可预见的将来也不期望使用此应用程序,则整体应用程序很可能最适合您。 唯一的事情-我建议编写它,以使彼此不相关的功能区域在代码中不相互依赖。 因此,如有必要,可以将未互连的功能区域彼此分开。 但是,这始终是一个不错的建议,它可以提高内部一致性并教会您精心制定模块合同。

响应式架构和响应式编程


被动方法是一个相对较新的事物。 它的出现的时刻可以被认为是2014年,即《反应宣言》出版的一年。 宣言发表后的两年,他是众所周知的。 这是系统设计的真正革命性方法。 , , , , .



, . , , : « , !?» , , , , «». , 100% , , .

— , — . .

? , .

- , - . - -, , HTTP-. , . , . , , , .

? , HTTP- , ( callback) ( ) . , - ( , HTTP-) .

— . . . . 3 Ghz , , . . . , Java-, HTTP- — 5-10%. , , , , 100 50 $/ — $500 . , , .

, ? .

, . , , , , , , . , , . .

- . , JDBC ( . ADA, R2DBC, ). 90 % , . — HTTP- , . , .



?


, , , ( ) . — - , . , , , HTTP.

, , , , , , .

. , « , » , , , . , , , 10 11 , , , .

结论


, . , , , .

Source: https://habr.com/ru/post/zh-CN436346/


All Articles