微服务的宏观问题

发布于:2020-12-24 16:14:26

0

66

0

分布式系统 微服务

在短短20年的时间里,软件工程已经从设计具有单一数据库和集中式状态的整体式架构转变为微服务,在微服务中,所有内容都分布在多个容器,服务器,数据中心甚至大洲上。 分发事物解决了扩展问题,但引入了一个全新的问题世界,其中许多问题以前是由整体解决的。

在本文中,我将简要介绍网络应用程序的历史,讨论如何达到这一点。 之后,我们将讨论Temporal的状态执行模型及其如何解决面向服务的体系结构(SOA)引入的问题。 全面披露我是Temporal产品的负责人,所以我可能会有偏见,但我认为这种方法是未来。

简短的历史课

二十年前,开发人员几乎总是构建单片应用程序。 该模型简单,一致,并且类似于您在本地环境中进行编程的经验。

巨石本质上依赖于单个数据库,这意味着所有状态都是集中的。 整体可以在一次交易中更改其任何状态,这意味着它会产生二进制结果,无论它是否起作用。 不一致的空间为零。 因此,整体式为开发人员提供了很好的经验,因为这意味着没有失败的交易导致状态不一致的机会。 反过来,这意味着开发人员不必不断编写代码来猜测事物的状态。

长期以来,整体式设计才有意义。 没有大量的连接用户,这意味着软件的规模要求很小。 即使是最大的软件巨头,其经营规模如今看来也很小。 像亚马逊和谷歌这样的少数公司正在“规模”经营,但是它们是罕见的例外,而不是常规。

人们喜欢软件

在过去的20年中,对软件的需求不会停止增长。如今,预计应用程序将从第一天开始服务于全球市场。 TwitterFacebook等公司已经实现了24/7全天候在线赌注。软件不再只是幕后的力量,它已成为最终用户体验本身。现在期望每个公司都有软件产品。可靠性和可用性不再是功能,而是必要条件。

不幸的是,当规模和可用性成为需求时,整体式装置便开始崩溃。开发人员和企业需要寻找方法来跟上全球快速增长和苛刻的用户期望。他们开始寻找可减轻他们所遇到的可伸缩性问题的替代体系结构。

他们找到的答案是微服务(很好的面向服务的体系结构)。最初,微服务似乎很棒,因为它们使应用程序可以分解为相对独立的单元,这些单元可以独立扩展。而且由于每个微服务都保持其自己的状态,这意味着您的应用程序不再局限于一台计算机上适合的状态!开发人员最终可以构建满足日益连接的世界的规模需求的应用程序。微服务还为团队和公司带来了灵活性,因为它们为组织架构提供了明确的责任线和分离线。

没有免费的午餐

尽管微服务解决了从根本上阻碍软件增长的可伸缩性和可用性问题,但并非一切都很好。开发人员开始意识到微服务具有一些严重的缺陷。

对于整体而言,通常只有一个数据库实例和一台应用程序服务器。而且由于无法分解整体,因此只有两个实际的缩放选项。第一种选择是垂直扩展,这意味着升级硬件以增加吞吐量/容量。垂直扩展可以提高效率,但成本高昂,而且如果您的应用程序需求不断增长,那么绝对不是永久解决方案。如果垂直扩展足够,最终将耗尽硬件以进行升级。第二种选择是水平缩放,在整体式的情况下,这意味着仅创建其自身的副本,以便每个副本都可服务于一组特定的用户/请求等。水平缩放式的整体式会导致资源利用不足,并且在足够高的比例下,普通不会工作。微服务不是这种情况,微服务的价值来自拥有多种“类型”的数据库,队列和其他可独立扩展和操作的服务器的能力。但是人们转向微服务时注意到的第一个问题是,他们突然开始对许多不同类型的服务器和数据库负责。长期以来,微服务的这一方面一直没有得到解决,开发人员和运营商不得不自己解决。解决微服务随附的基础架构管理问题非常困难,这使应用程序充其量是不可靠的。

需求是改变的最终工具。随着微服务的采用迅速增加,开发人员变得越来越有动力来解决其潜在的基础结构问题。慢慢地但可以肯定的是,解决方案开始出现,而DockerKubernetesAWS Lambda等技术则填补了空白。这些技术中的每一种都大大减轻了运行微服务基础架构的负担。开发人员不必编写用于处理容器和资源的自定义代码,而可以依靠工具来为它们完成工作。现在,到2020年,我们终于达到了一个点,即基础架构的可用性不会破坏应用程序的可靠性。做得好!

当然,我们还没有进入完美稳定软件的乌托邦。基础架构不再是应用程序不可靠的根源;应用程序代码是。

微服务的另一个问题

使用整体,开发人员可以编写以二进制方式进行状态更改的代码。事情发生了或没有发生。借助微服务,世界状况分布在不同的服务器上。现在,更改应用程序状态需要同时更新不同的数据库。这引入了一种可能性,即一个数据库将成功更新,而其他数据库可能已关闭,从而使您陷入不一致的中间状态。但是,由于服务是横向可伸缩性的唯一解决方案,因此开发人员别无选择。

在服务之间分配状态的根本问题是,对外部服务的每次调用都是可用性骰子。开发人员当然可以选择忽略代码中的问题,并假定他们调用的每个外部依赖关系将始终成功。但是,如果忽略它,则意味着这些下游依赖项之一可能会在没有警告的情况下关闭应用程序。结果,开发人员被迫改编他们现有的整体时代代码,以添加检查以检查操作是否在事务中间失败的检查。在下面的代码中,开发人员必须不断从即席myDB存储中检索最后记录的状态,以避免潜在的竞争情况。不幸的是,即使采用这种实施方式,仍然存在竞赛条件。如果在不更新myDB的情况下更改了帐户状态,则存在不一致的余地。

public void transferWithoutTemporal(
  String fromId, 
  String toId, 
  String referenceId, 
  double amount,) {
  boolean withdrawDonePreviously = myDB.getWithdrawState(referenceId);
  if (!withdrawDonePreviously) {
      account.withdraw(fromAccountId, referenceId, amount);      
      myDB.setWithdrawn(referenceId);
  }
  boolean depositDonePreviously = myDB.getDepositState(referenceId);
  if (!depositDonePreviously) {
      account.deposit(toAccountId, referenceId, amount);                
      myDB.setDeposited(referenceId);
  }}

不幸的是,编写无错误的代码是不可能的,并且通常,代码越复杂,发生错误的可能性就越大。 如您所料,处理“中间”的代码不仅复杂,而且令人费解。 某些可靠性总比没有强。因此,开发人员被迫编写此固有的错误代码来维持最终用户的体验。 这不仅耗费了开发人员的时间和精力,而且使雇主付出了很多钱。 尽管微服务非常适合扩展,但它们却以开发人员的乐趣和生产力以及应用程序的可靠性为代价。

数以百万计的开发人员每天都在浪费时间来重新发明一种最创新的轮子,即可靠性样板代码。 当前使用微服务的方法根本不能反映现代应用程序对可靠性和可伸缩性的要求。

因此,现在是我们向您介绍我们的解决方案的部分。需要说明的是,Stack Overflow并不认可这一点。而且我们还没有说它是完美的。我们想分享我们的想法,并听听您的想法。有什么比Stack更好的地方获得改进代码反馈的呢?

直到今天,还没有一种解决方案使开发人员能够使用微服务而不会遇到我上面提到的这些问题。您可以测试和模拟故障状态,编写代码以预测故障,但是这些问题仍然会发生。我们相信Temporal解决了这个问题。 Temporal是一个开源(麻省理工学院,没有恶作剧)的,有状态的,微服务编排运行时。

Temporal具有两个主要组件:由您选择的数据库提供支持的有状态后端层,以及受支持的语言之一的客户端框架。应用程序是使用客户端框架和简单的旧代码构建的,该代码在代码运行时自动将状态更改保持在后端。您可以自由使用在构建任何其他应用程序时将依赖的依赖项,库和构建链。需要明确的是,后端本身是高度分布式的,因此这不是J2EE 2.0的情况。实际上,后端的分布式性质正是实现几乎无限水平缩放的原因。 Temporal旨在为应用程序层提供一致性,简单性和可靠性,就像DockerKubernetes和无服务器对基础架构所做的那样。

Temporal为编排微服务提供了许多高度可靠的机制,但是最重要的是状态保存。状态保存是一种临时功能,它使用事件源自动将正在运行的应用程序中的所有有状态更改持久化。就是说,如果运行您的Temporal工作流程功能的计算机崩溃了,该代码将自动在另一台计算机上恢复,就像从未发生崩溃一样。这甚至包括局部变量,线程和其他特定于应用程序的状态。比喻,了解此功能的工作原理的最佳方法是。作为当今的开发人员,您很可能依赖版本控制SVN(它是OG Git)来跟踪对代码所做的更改。关于SVN的事情是,它不会在您进行每次更改后就对应用程序的全面状态进行快照。 SVN的工作方式是仅存储新文件,然后引用现有文件,从而避免重复它们。对于运行应用程序的有状态历史记录,Temporal类似于SVN(再次大致类推)。只要您的代码修改了应用程序状态,Temporal就会以容错的方式自动存储所做的更改(而不是结果)。这意味着Temporal不仅可以还原崩溃的应用程序,还可以将它们回滚,分叉等等。结果是,开发人员不再需要在基础服务器可能发生故障的前提下构建应用程序。

作为开发人员,此功能就像从键入每个字母后手动保存(ctrl-s)文档到使用Google文档在云中自动保存一样。不仅仅是您不再手动保存文件,而且不再有与该文档关联的机器。状态保存意味着开发人员编写的微服务最初所编写的繁琐的样板代码要少得多。这也意味着不再需要临时基础结构(如独立队列,缓存和数据库)。这样可以减少操作负担和添加新功能的开销。这也使新员工的入职变得更加容易,因为他们不再需要增加混乱且特定于域的状态管理代码。

状态保存也以另一种形式出现:“耐用计时器”。耐用计时器是开发人员通过Workflow.sleep命令利用的容错机制。通常,Workflow.sleep函数与语言本机sleep命令完全一样。但是,有了Workflow.sleep,无论多长时间,您都可以安全地睡眠任何时间。有许多Temporal用户,其工作流程休眠了数周甚至数年。为此,Temporal服务在基础数据存储区中保留持久性计时器,并跟踪何时需要恢复相应的代码。同样,即使底层服务器崩溃(或您只是将其关闭),当打算触发计时器时,代码也会在可用的计算机上恢复。睡眠工作流不会消耗资源,因此您可以拥有数百万个睡眠工作流,而开销却可以忽略不计。这看起来似乎非常抽象,所以这里是一个有效的临时代码示例:

public class SubscriptionWorkflowImpl implements SubscriptionWorkflow {
  private final SubscriptionActivities activities =
      Workflow.newActivityStub(SubscriptionActivities.class);
  public void execute(String customerId) {
    activities.onboardToFreeTrial(customerId);
    try {
      Workflow.sleep(Duration.ofDays(180));
      activities.upgradeFromTrialToPaid(customerId);
      while (true) {
        Workflow.sleep(Duration.ofDays(30));
        activities.chargeMonthlyFee(customerId);
      }
    } catch (CancellationException e) {
      activities.processSubscriptionCancellation(customerId);
    }
  }
}

除了保留状态外,Temporal还提供了一套用于构建可靠应用程序的机制。活动功能是从工作流中调用的,但是活动中运行的代码不是有状态的。尽管它们不是有状态的,但活动具有自动重试,超时和心跳的功能。活动对于封装可能失败的代码非常有用。例如,假设您的应用程序依赖于通常不可用的银行的API。对于传统的应用程序,您将需要使用大量try / catch语句,重试逻辑和超时来包装调用bank API的所有代码。但是,如果您在活动中调用银行API,那么所有这些东西都是开箱即用的,这意味着如果调用失败,则将自动重试该活动。重试非常好,但是有时不可靠的服务是您拥有的服务,因此您希望避免DDoSing。因此,活动调用还支持超时,该超时由持久计时器支持。这意味着您可以在重试尝试之间让活动等待数小时,数天或数周。这对于需要成功但无需担心代码执行速度如何的代码特别有用。

Temporal的另一个强大方面是它对正在运行的应用程序提供的可见性。 可见性API提供了类似于SQL的界面,可从任何工作流程(正在运行或以其他方式)查询元数据。 也可以直接在工作流程中定义和更新自定义元数据值。 可见性API非常适合Temporal操作员和开发人员,尤其是在开发过程中进行调试时。 可见性甚至支持将批处理操作应用于查询结果。 例如,您可以向与您的创建时间查询>昨天匹配的所有工作流发送终止信号。 Temporal还支持同步获取功能,该功能使开发人员可以在运行的实例中获取本地工作流变量的值。 这有点像您的IDE中的调试器适用于生产应用。 例如,可以在以下代码的运行实例中获得greeting的值:

public static class GreetingWorkflowImpl implements GreetingWorkflow {  
  private String greeting;   
   @Override
    public void createGreeting(String name) {
      greeting = "Hello " + name + "!";
      Workflow.sleep(Duration.ofSeconds(2));
      greeting = "Bye " + name + "!";
    }    @Override
    public String queryGreeting() {      
       return greeting;
    }
  }

结论

微服务很棒,但是开发人员和企业为使用微服务付出的代价是生产力和可靠性。 Temporal旨在通过提供一种为开发人员支付微服务税的环境来解决此问题。 状态保留,自动重试失败的呼叫以及开箱即用的可见性只是Temporal提供的使开发微服务合理化的一些基本要素。