java 服务编排_使用Java编排机器人群

java 服务编排

重要要点

  • Ocado Technology使用Java成功开发需要高性能的应用程序。
  • 离散事件模拟的使用使开发团队可以长时间分析性能,而无需等待结果。
  • 确定性软件对于高效调试至关重要。 实时系统本质上是不确定的,因此Ocado Technology努力解决了这种不一致问题。
  • 洋葱体系结构强调应用程序内关注点的分离。 使用这种架构,Ocado Technology可以相对轻松地不断调整和更改其应用程序。

在Ocado Technology,我们使用最先进的机器人技术为高度自动化的履行中心提供动力,而在我们最大的在线杂货自动化仓库 Erith的站点上,我们最终将招募3,500台机器人来处理每个订单220,000个订单。周。 如果您还没有看到我们的机器人在起作用,请在我们的YouTube频道上查看它们。

我们的机器人以4m / s的速度彼此相距5mm! 为了编排机器人群并最大程度地发挥仓库的效率,我们开发了类似于空中交通管制系统的控制系统。

在开始开发任何应用程序时,我们将逐步介绍您需要做出的三个典型决定,并且将解释为控制系统做出的语言,开发原理和体系结构选择。

语言选择

并不是每个人都可以纯粹根据其技术优点和对特定问题的适用性来选择使用的编程语言。 微服务和容器化的一个常被提及的好处是采用多语言开发环境的能力,但是在许多组织中,还必须考虑其他因素,例如:

  • 现有经验和专业知识
  • 招聘注意事项
  • 工具链支持
  • 企业战略

在Ocado Technology,我们在Java上投入了大量资金-我们的控制系统是用Java开发的。 我们听到的一个常见问题(经常问自己!)是为什么我们使用Java,而不是使用C ++或最近的Rust这样的语言。 答案-我们不仅在优化控制系统,而且还在优化开发人员的生产力,这种权衡取舍不断地导致我们使用Java。 我们选择使用Java是因为它的性能,开发速度,不断发展的平台和招聘。 让我们依次查看每个因素。

性能

有人认为Java比用C或C ++编写的可比程序“慢”,但这实际上是一个谬误。 有一些用Java编写的高性能应用程序的著名示例,这些示例证明了用Java实现的功能,例如LMAX Disruptor 。 比较语言时,还应考虑许多应用程序性能因素,例如可执行文件大小,启动时间,内存占用量和原始运行时速度。 同样,跨两种语言比较特定应用程序的性能本质上是困难的,除非您能够用两种语言比较地编写该应用程序。

尽管在使用Java开发高性能应用程序时有许多推荐的软件实践,但在JVM中,与其他语言相比,即时(JIT)编译器可能是提高应用程序性能的最重要的单个概念。 通过对运行中的字节代码进行概要分析并在运行时将适当的字节代码编译为本机代码,Java应用程序的性能可以非常接近本机应用程序。 此外,由于JIT编译器在最后可能的时刻运行,因此它具有AOT编译器无法获得的可用信息,主要是应用程序在其上运行的确切芯片组以及有关实际应用程序的统计信息。 有了这些信息,JIT编译器就可以执行AOT编译器无法保证安全的优化,因此在某些情况下JIT编译器实际上可以胜过AOT编译器。

发展速度

许多因素使Java开发比其他语言更快:

因为Java是一种类型化的高级语言,所以开发人员可以专注于业务问题并尽早发现错误。
现代IDE为开发人员提供了许多工具,可以在第一时间编写正确的代码。
Java具有成熟的生态系统,并且几乎所有内容都有库和框架。 中间件技术几乎无处不在对Java的支持。

不断发展的平台

Java架构师Mark Reinhold 表示 ,二十年来,JVM开发的两个最大推动力一直是开发人员生产力和应用程序性能的提高。 因此,随着时间的流逝,仅依靠不断发展和改进的语言和平台,我们就能够从前两个问题(性能和开发速度)中受益。 例如,在Java 8和Java 11之间观察到的性能改进之一是G1垃圾收集器的性能,它使我们的控制系统有更多的应用程序时间来执行计算密集型计算。

招募

最后,但对于成长中的公司而言,绝对重要的是,能够轻松招募开发人员至关重要。 在包括TiobeGitHubStackOverflowITJobsWatch在内的每种流行语言的索引中,Java始终处于顶部或顶部。 这个职位意味着我们拥有庞大的全球开发人员库,可以从中招聘最优秀的人才。

发展原则

选择语言之后,我们在系统中做出的第二个关键决定是我们作为团队开发应用程序时所采用的开发原则或实践。 这里讨论的决策规模类似于杰夫·贝佐斯(Jeff Bezos)的著名决策,即使Amazon内部面向服务。 与是否使用结对编程等决定不同,这些决定不容易更改。

在Ocado Technology,我们使用三个主要原则来开发我们的控制系统:

  • 广泛地模拟测试和研究
  • 确保我们的所有代码都可以在研发期间确定性地运行,并且相同的代码也可以在实时上下文中运行
  • 避免过早的优化

模拟

维基百科上有关仿真的文章将其描述为:

模拟是对流程或系统操作的近似模仿; 首先模拟的行为需要开发一个模型。

在机器人仓库的环境中,我们可以模拟许多流程和系统,例如自动化硬件,执行业务流程的仓库操作员,甚至其他软件系统。

模拟我们仓库的这些方面有两个主要好处:

  • 我们增强了信心,新的仓库设计将提供我们为其设计的吞吐量。
  • 我们能够测试和验证软件中的算法更改,而无需在物理硬件上进行测试。

为了在上述两个模拟场景中获得有意义的结果,我们通常需要运行许多天或数周的仓库运行模拟。 我们可以选择实时运行我们的系统,并等待数天或数周才能完成模拟,但这效率非常低,使用离散事件模拟 (DES)可以做得更好。

DES在系统状态仅在事件处理后才发生更改的假设下工作。 在这种假设下,DES可以维护要处理的事件列表,并且在事件处理之间可以及时跳转到下一个事件的时间。 在大多数情况下,正是这种“时间旅行”使DES的运行速度比等效的实时代码快得多。 对我们的开发人员和仓库设计团队的快速反馈提高了我们的生产力。

值得一提的是,要能够使用离散事件模拟,我们必须将控制系统设计为基于事件的,并确保状态不会随着时间的流逝而改变。 这种架构要求导致了我们使用的下一个开发原理-确定性。

决定论

本质上,实时系统是不确定的。 除非您的系统使用提供严格调度保证的实时操作系统,否则大部分不确定性行为可能源于操作系统,事件的不可控调度以及事件的不可预知的处理时间。

确定性在控制系统的研发过程中(即运行仿真时)非常重要。 在没有确定性的情况下,如果发生不确定性错误,开发人员通常不得不采用日志拖网和临时测试的组合来尝试重现错误,而不能保证能够真正重现该错误。 这会消耗开发人员的时间和动力。

由于实时系统永远不会是确定性的,因此我们面临的挑战是生产可以在DES期间确定性地运行并且还可以实时地不确定性地运行的软件。 我们通过使用自己的抽象-时间和调度来做到这一点。

下面的代码片段显示了时间抽象,它是为了控制时间的流逝而引入的:

@FunctionalInterface
public interface TimeProvider {
    long getTime();
}

使用这种抽象,我们可以提供一个实现,使我们可以在离散事件模拟中“时间旅行”:

public class AdjustableTimeProvider implements TimeProvider {
    private long currentTime;

    @Override
    public long getTime() {
        return this.currentTime;
    }
    
    public void setTime(long time) {
        this.currentTime = time;
    }
}

在我们的实时生产环境中,我们可以用依赖于标准系统调用的实现方式来代替此实现以节省时间:

public class SystemTimeProvider implements TimeProvider {
    @Override
    public long getTime() {
        return System.currentTimeMillis();
    }
}

对于调度,我们还介绍了我们自己的抽象和实现,而不是依赖Java中的ExecutorExecutorService接口。 我们这样做是因为Java执行程序接口无法提供我们所需的确定性保证。 我们将在本文后面探讨原因的原因:

public interface Event {
    void run();
    void cancel();
    long getTime();
}

public interface EventQueue {
    Event getNextEvent();
}

public interface EventScheduler {
    Event doNow(Runnable r);
    Event doAt(long time, Runnable r);
}

public abstract class DiscreteEventScheduler implements EventScheduler {
    private final AdjustableTimeProvider timeProvider;
    private final EventQueue queue;

    public DiscreteEventScheduler(AdjustableTimeProvider timeProvider, EventQueue queue) {
        this.timeProvider = timeProvider;
        this.queue = queue;
    }

    private void executeEvents() {
        Event nextEvent = queue.getNextEvent();
        while (nextEvent != null) {
            timeProvider.setTime(nextEvent.getTime());
            nextEvent.run();
            nextEvent = queue.getNextEvent();
        }
    }
}

public abstract class RealTimeEventScheduler implements EventScheduler {
    private final TimeProvider timeProvider = new AdjustableTimeProvider();
    private final EventQueue queue;

    public RealTimeEventScheduler(EventQueue queue) {
        this.queue = queue;
    }

    private void executeEvents() {
        Event nextEvent = queue.getNextEvent();
        while (true) {
            if (nextEvent.getTime() <= timeProvider.getTime()) {
                nextEvent.run();
                nextEvent = queue.getNextEvent();
            }
        }
    }
}

在我们的DiscreteEventScheduler中,您可以观察到timeProvider.setTime(nextEvent.getTime())这行,它表示上述时间旅行。

我们的RealTimeEventScheduler是一个忙循环的示例。 通常不建议使用此技术,因为它会浪费CPU时间进行无用的活动。 那么,为什么要在控制系统中使用繁忙循环调度程序呢? 接下来,我们将进行探讨。

优化

每个软件开发人员肯定都熟悉Donald Knuth的报价:

“过早的优化是万恶之源。”

但是,有多少人知道此报价的完整内容:

“我们应该忘记效率低下的问题,大约有97%的时间是这样: 过早的优化是万恶之源 。但是,我们不应该在这3%的临界水平上放弃机会。”

在我们的仓库控制系统中,我们追捕了3%的机会,这些机会使我们的系统尽可能地发挥最佳性能! 以前的繁忙循环调度程序就是这些机会之一。

由于系统的实时性,我们对事件调度程序有以下要求:

  • 需要为特定时间安排活动。
  • 个别事件不能任意延迟。
  • 系统不允许事件任意备份。

最初,我们选择基于ScheduledThreadPoolExecutor实现最简单,最惯用的Java解决方案。 本质上,此解决方案满足第一个要求。 为了确定它是否满足我们的第二和第三要求,我们使用了仿真功能来对性能进行全面的性能测试。 通过仿真,我们可以在很多天的满仓库容量下运行控制系统,以测试应用程序的行为-通常在任何仓库真正满负荷运行之前就可以了。 该测试表明,基于ScheduledThreadPoolExecutor的解决方案无法支持必要的仓库数量。 为了理解为什么该解决方案不够充分,我们转向对控制系统进行性能分析,该系统突出了两个需要重点关注的领域:

  • 安排活动的那一刻
  • 准备执行事件的那一刻

从安排事件的时间开始, ThreadPoolExecutor JavaDoc列出了三种排队策略:

  • 直接交接
  • 无限队列
  • 有界队列

通过查看ScheduledThreadPoolExecutor的JavaDoc内部结构,可以看到正在使用一个自定义的,无界的队列,从ThreadPoolExecutor JavaDoc中我们可以看到:

尽管这种排队方式对于消除短暂的请求突发很有用,但它承认当命令平均到达的速度比处理命令的速度快时,工作队列就会无限增长。

这告诉我们,由于事件可以在无限制的工作队列中备份,因此我们的第三项要求可能会被违反。

当准备好要执行新事件时,我们再次转向JavaDocs以了解线程池的行为。 根据您的线程池配置,可能会创建一个新线程来执行该事件。再次,从ThreadPoolExecutor JavaDoc:

如果正在运行的线程少于corePoolSize线程,则将创建一个新线程来处理请求,即使其他工作线程处于空闲状态也是如此。 否则,如果正在运行的线程少于maximumPoolSize线程,则仅当队列已满时,才会创建一个新线程来处理请求。

线程创建需要花费时间,这意味着我们的第二个要求也可能被违反。

从理论上讲,您的应用程序可能会出什么问题,但是直到您对它进行了彻底的测试,您才会知道所选解决方案的性能是否足够。 通过重新运行同一组模拟测试,我们可以观察到繁忙的循环为我们降低了单个事件的延迟:从<5ms降低到有效0,这使事件吞吐量提高了3倍,并且满足了所有条件我们的活动安排要求中的三个。

建筑

我们的最终决定“体系结构”对不同的人意味着不同的事物。

在某些情况下,体系结构指的是实现选择,例如:

  • 整体或微服务
  • ACID事务或最终的一致性(或更简单地说,SQL vs NoSQL)
  • EventSourcing或CQRS
  • REST或GraphQL

在应用程序生命周期开始时做出的实现决策通常在该时间点是有效的。 但是,随着应用程序的不断发展,功能的增加和复杂性的不可避免地增加,这些决定必须一次又一次地重新审视。

对于其他人,体系结构与如何构建代码和应用程​​序有关。 如果您承认这些实施决策将发生变化,那么良好的体系结构将确保可以尽可能轻松地进行这些更改。 实现这一目标的一种方法是遵循Onion Architecture ,它强调了应用程序中关注点的分离。

开发原则通常会影响您选择的体系结构。 我们的开发原则以多种方式指导了我们的体系结构:

  • 离散事件模拟要求我们实施基于事件的系统。
  • 强制确定性使我们实现了自己的抽象,而不是依靠标准的Java抽象。
  • 通过避免过早的优化并简单地启动,我们的应用程序从一个可部署的人工制品开始了生命。 经过多年的发展,该应用程序已成长为一个整体,仍然为我们提供了良好的服务。 我们不断评估“现在”是否是优化和重构为不同结构的时候。

考虑更改系统设计

如果您是负责决定使用哪种编程语言实现高性能系统的系统设计师或软件架构师,那么本文可证明Java是对抗诸如C,C ++或Rust之类的“显而易见”语言的主要竞争者。 如果您是Java程序员,那么本文将向您展示Java语言可以实现的示例。

下次设计系统时,请考虑在项目开始时所做的原则和决策,这些原则和决策将极其困难或不可能更改。 对我们来说,这些是我们对模拟的使用以及对确定性的关注。 对于可能发生变化的系统方面,请选择一种体系结构,例如Onion Architecture,该体系结构可以使更改的可能性变得开放且容易。

翻译自: https://www.infoq.com/articles/java-robot-swarms/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

java 服务编排

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值