微服务宏问题



在短短20年的时间里,软件开发已从具有单个数据库和集中状态的体系结构转移到微服务,在微服务中,所有内容都分布在众多容器,服务器,数据中心甚至大洲上。分布使缩放更容易,但同时也带来了全新的挑战,其中许多挑战以前是通过整体解决的。



让我们快速浏览一下网络应用程序的历史,以了解我们是如何到达今天的。然后,我们来谈谈Temporal中使用的有状态执行模型以及它如何解决面向服务的体系结构(SOA)的问题。我可能会有偏见,因为我在Temporal经营杂货部,但我相信这种方法将是未来。



简短的历史课



二十年前,开发人员几乎总是创建单片应用程序。这是一个简单且一致的模型,类似于您在本地环境中进行编程的方式。从本质上讲,整体式依赖单个数据库,也就是说,所有状态都是集中的。在单个事务中,整体可以更改其任何状态,即,给出二进制结果:是否有效。没有不一致的余地。也就是说,关于整体的奇妙之处在于,不会因事务失败而导致状态不一致。这意味着开发人员无需一直在猜测不同元素的状态就可以编写代码。



长期以来,整体设计是有意义的。连接的用户还不多,因此软件扩展需求很小。即使是最大的软件巨头,其操作系统都按现代标准微不足道。只有少数公司(例如Amazon和Google)使用了大型解决方案,但这些是例外。



以人为本软件







在过去的20年中,软件需求一直在不断增长。今天,应用程序应该从第一天开始就可以在全球市场上工作。像Twitter和Facebook这样的公司已经将24/7在线作为前提条件。应用程序不再提供任何内容,它们本身已成为用户体验。如今,每个公司都必须拥有软件产品。 “可靠性”和“可用性”不再是属性,而是要求。



不幸的是,当将“可伸缩性”和“可用性”添加到需求中时,整体式组件开始崩溃。开发人员和企业都需要找到方法来跟上爆炸性的全球增长和苛刻的用户期望。我不得不寻找替代架构,以减少与扩展相关的新出现的问题。



微服务(很好,面向服务的体系结构)就是答案。最初,它们似乎是一个很好的解决方案,因为它们使您可以将应用程序拆分为相对独立的模块,这些模块可以独立扩展。而且由于每个微服务都保持自己的状态,因此应用程序不再局限于一台机器的容量!开发人员终于能够创建可以随着连接数量的增长而扩展的程序。由于责任透明和架构分离,微服务还为团队和公司的工作提供了灵活性。





没有免费的奶酪



尽管微服务解决了阻碍软件增长的可扩展性和可用性问题,但事情并非没有万无一失。开发人员开始意识到微服务存在严重缺陷。



整体组件通常具有一个数据库和一个应用程序服务器。而且由于整体无法拆分,因此只有两种缩放方法:



  • 垂直:升级硬件以增加吞吐量或容量。这种缩放可以有效,但是很昂贵。如果您的应用程序需要持续增长,那么它肯定不会永远解决该问题。如果扩展足够,最终将没有足够的设备进行升级。
  • : , . , .


微服务是不同的,它们的价值在于具有许多“类型”的数据库,队列和其他服务的能力,这些能力彼此独立地进行扩展和管理。但是,切换到微服务时开始注意到的第一个问题正是这样一个事实,即您现在必须照顾一堆各种服务器和数据库。



长期以来,一切都留给了偶然的机会,开发人员和操作员独自摆脱困境。微服务带来的基础架构管理问题很难解决,最多只能降低应用程序的可靠性。



但是,供应是根据需求而产生的。微服务的传播越多,就越有更多的开发人员去解决基础设施问题。但是,工具肯定会慢慢出现,而Docker,Kubernetes和AWS 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);
  }
}


las,编写没有错误的代码是不可能的。代码越复杂,出现错误的可能性就越大。如您所料,与“中间件”一起使用的代码不仅很复杂,而且非常复杂。至少有一些可靠性总比没有好。因此,开发人员必须编写此类最初有漏洞的代码来维持用户体验。这花费了我们的时间和精力,并且使雇主付出了很多钱。尽管微服务可以很好地扩展,但它们却以开发人员的乐趣,生产力和应用程序可靠性为代价。



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







现在我们解决了。它没有得到Stack Overflow的认可,我们也不声称是完美的。我们只想分享我们的想法并听取您的意见。有什么比Stack更好的地方来获得改进代码的反馈?



直到今天,还没有解决方案允许您使用微服务而不解决上述问题。您可以测试和模拟崩溃状态,编写考虑到崩溃的代码,但是仍然会出现这些问题。我们相信Temporal可以解决这些问题。它是用于微服务编排的开源(麻省理工)。



Temporal具有两个主要组件:在您选择的数据库上运行的有状态后端,以及使用一种受支持语言的客户端框架。应用程序是使用客户端框架和常规的旧版代码构建的,这些代码在运行时会自动将状态更改保存在后端中。您可以像构建任何其他应用程序一样使用相同的依赖项,库和构建链。老实说,后端是高度分布式的,因此它不像J2EE 2.0。实际上,正是后端的分布允许几乎无限的水平缩放。与Docker基础架构,Kubernetes和无服务器架构一样,Temporal为应用程序层带来了一致性,简单性和可靠性。



Temporal为微服务编排提供了许多高度可靠的机制。但是最重​​要的是国家的保存。此功能使用事件发射将任何有状态的更改自动保存到正在运行的应用程序。也就是说,如果运行Temporal的计算机崩溃,该代码将自动跳转到另一台计算机,就像什么都没发生一样。这甚至适用于局部变量,执行线程和其他特定于应用程序的状态。



让我给你一个比喻。作为开发人员,您今天可能依赖SVN版本控制(即OG Git)来跟踪对代码的更改。 SVN只是保存新文件,然后链接到现有文件以避免重复。时间性类似于SVN(大致类比),用于表示正在运行的应用程序的状态历史记录。当您的代码更改应用程序的状态时,Temporal会自动保存该更改(而不是结果)而不会出错。也就是说,Temporal不仅可以还原崩溃的应用程序,还可以将其回滚,分叉并执行更多操作。因此,开发人员无需在服务器可能崩溃的情况下构建应用程序。



这就像在每个输入字符之后从手动保存文档(Ctrl + S)切换到Google文档自动云保存一样。并不是说您不再手动保存任何内容,只是不再有任何一台与此文档相关联的机器。状态持久性意味着开发人员可以编写因微服务而不得不编写的无聊的样板代码。此外,您不再需要特殊的基础结构-单独的队列,缓存和数据库。这使操作和添加新功能更加容易。这也使更新新手变得更加容易,因为他们不需要了解混乱的特定状态管理代码。



状态保留也以“持久计时器”的形式实现。这是一种可以与命令一起使用的故障安全机制Workflow.sleep。它的工作方式与完全相同 sleep。但是,Workflow.sleep可以在任何时间段内对其进行安乐死。许多临时用户已经睡了几个星期,甚至几年。这是通过将长时间运行的计时器存储在时间存储中并跟踪要唤醒的代码来实现的。同样,即使服务器崩溃(或您刚刚将其关闭),当计时器到期时,代码也会转到可用的计算机上。睡眠过程不会消耗资源,您可以以微不足道的开销拥有数百万个睡眠过程。听起来可能太抽象了,所以下面是一个有效的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语句,重试逻辑和超时来包装所有调用此API的代码。但是,如果您从某个活动中调用银行API,那么所有这些功能都是开箱即用的:如果调用失败,则会自动重试该活动。都很棒但有时您自己拥有不可靠的服务,并想保护其不受DDoS的侵害。因此,活动调用还支持超时,并由长计时器支持。也就是说,重复活动之间的停顿可能长达数小时,数天或数周。这对于需要成功运行的代码特别有用,但是您不确定它发生的速度。



该视频在两分钟内说明了时间编程模型:





Temporal的另一个优点是正在运行的应用程序具有可观察性。观察API提供了一个类似于SQL的界面,用于从任何工作流程(无论是否可执行)中查询元数据。您还可以在流程中定义和更新您的元数据值。观察API对临时操作员和开发人员非常有用,尤其是在开发过程中进行调试时。监视甚至支持对查询结果执行批处理操作。例如,您可以向与创建时间>昨天的请求匹配的所有工作进程发送终止信号。 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使微服务开发变得智能化的部分功能。



All Articles