Hootsuite 总部位于加拿大温哥华,是使用最广泛的 SaaS(软件即服务)社交媒体管理平台。自 2008 年 humble beginnings 以来,Hootsuite 已成长为一家价值数十亿美元的公司,在全球拥有超过 1500 万用户。
随着 Hootsuite 多年来的发展,技术堆栈也在不断发展。一个关键的变化是从 LAMP(Linux、Apache、MySQL、PHP)迁移到微服务。然而,向微服务的转变并非没有挑战。在本次圆桌会议中,我们讨论了 Scala 和 Lightbend(提供反应式应用程序开发平台)如何成为成功转型的重要组成部分。参与交流的有 Lightbend 首席技术官 Jonas Bonér;Marine Learning Systems 首席技术官 Terry Coatta;Hootsuite 高级 Scala 开发人员 Edward Steel;Hootsuite 首席软件开发人员 Yanik Berube;以及 Hootsuite 软件开发高级主管 Ken Britton。
TERRY COATTA 我很好奇 Hootsuite 在切换到微服务时最初希望解决哪些问题。您能提供一些细节吗?
EDWARD STEEL 主要是与我们向用户移动设备发送通知的能力有关,每当 Twitter 上发生相关事件时。当我们开始对我们处理这件事的方式感到担忧时,我们已经在为数十万用户提供服务,每个用户都有根据自身特定兴趣量身定制的个人订阅。我们需要的是能够保持与 Twitter 流媒体端点连接的东西。
TC 我猜想在您进行此迁移的大约同一时间,您也采取措施从 PHP 迁移到 Scala。是什么驱动了这一点?
ES 最初,这与了解其他一些组织在使用 Scala 方面取得的成功有很大关系。那是在 Twitter 决定使用 Scala 之后,这显然赋予了它很大的合法性。此外,第一个使用 Scala 的团队来自非常不同的背景。我们有些人游说使用更强类型的函数式语言——类似于 Haskell——还有一些人有 Clojure 和 Java 经验。综合考虑所有这些因素,我想 Scala 似乎满足了大多数要求。
JONAS BONÉR 您认为 Scala 的主要优势是什么?是语言本身的函数式特性吗?还是更多地与该生态系统中可用的库有关?
ES 语言本身是最大的部分。这里 Scala 的主要倡导者当时正在 Blackberry 客户端上工作,所以他有很多 JVM(Java 虚拟机)知识,但也对 Java 本身感到沮丧。我猜他只是在寻找更好的方法。我们思考的另一个方面与构建一个可以利用 Akka 作为可用库的分布式系统有关。这也是决策的重要组成部分。
实际上,我认为我们从一开始就能够使用一些 actors。那是 Akka 的一个非常早期的版本,但它仍然提供了许多我们认为有用的引人注目的功能。
JB 您当时已经在考虑微服务了吗,甚至在它作为流行语流行起来之前?还是您更多地被与共享无架构、强隔离和松耦合等相关的反应式原则所吸引?
ES 微服务始终在我们脑海中。我们已经有一些用 PHP 编写的批处理程序,当时开始运行作业。这在某种程度上奏效了,但这远非理想的做事方式。所以我认为我们已经开始渴望一个我们可以构建一些服务的系统,我们的想法是也许我们可以转向面向服务的架构。微服务的概念直到稍后才出现,这可能受到当时行业内一些流行趋势的影响。
TC 您已经多次提到了 Akka,那么您能谈谈它在这里是如何适用的吗?
ES 至少在当时,Akka 系统对 JVM 的主要吸引力在于,它为 Erlang 社区以外的人们提供了访问 actor 模型的机会。关于 actors 的一点是,它们既是基于消息的,又是高度弹性的——也就是说,即使它们崩溃了,通常也可以以有用的方式恢复。这可能解释了为什么 Erlang 经常被用来开发弹性电信系统。每当您谈论分布式系统或您希望发送大量消息的系统时,您可以期望 actor 模型真正发挥作用。
YANIK BERUBE 就这在当前基础设施中的位置而言,我们应该注意到,我们所有的微服务都由 Akka 提供支持。在内部,我们有一个服务器类型的库,通过 Akka 处理请求和响应,我们还至少有一个,如果不是更多,后端服务使用 actors 集在某些时间间隔内完成工作。
TC 当我想到 Erlang actor 系统时,除了您已经提到的独立性之外,我想到的其中一件事是它非常细粒度。所以我想知道,考虑到您专注于向用户移动设备发送通知,您是否实际上可能需要每个用户一个 actor 来处理这个问题?
ES 在我们的例子中,不需要。但当我们第一次构建系统时,肯定出现了这种情况的症状。当我们刚开始时,我们更多地了解了我们应该如何构建系统,以及 actors 应该如何真正工作。起初,我们肯定掉进了在单个 actors 中放入过多逻辑的陷阱——例如,将恢复逻辑放入每个 actor 中,而不是依赖监督层次结构,这将使我们能够编写更少的防御性代码。事实证明,最好只是拥抱“让它崩溃”的理念,因为这实际上提供了很多稳健性。
我们还了解到,每当您将关注点分离到单一用途的 actors 中时,该模型就会真正发挥作用。除了帮助澄清设计之外,它还在部署点的可扩展性和配置灵活性方面开辟了许多机会。
JB 这也适用于微服务。也就是说,关于什么甚至定义了微服务,有很多意见。对您来说,这个术语意味着什么?这在概念上如何映射到您如何看待 actors?
YB 在内部,我们仍在尝试定义什么是微服务以及它的规模应该有多大。今天,我们的大多数服务都专注于完成一组高度相关的任务。我们的目标是让每个服务都拥有我们领域模型的一部分。例如,数据服务将各自控制自己的数据,其他任何东西都无法访问该数据。要访问该数据,您必须通过数据服务本身。
然后我们还将拥有功能服务,这些服务本质上是将业务逻辑粘合到逻辑的数据部分的服务。但就这些东西的大小而言,我想说我们仍在努力弄清楚,到目前为止我们还没有提出任何硬性规定。
JB 我猜想您的每个微服务都拥有自己的数据存储。如果是这样——这些东西都是有状态的——那么您如何确保跨越中断的弹性?
ES 这些服务绝对拥有自己的数据。当涉及到替换单体系统的部分时,通常归结为处理 LAMP MySQL 数据库中的一个表或几个相关表。一般来说,就服务而言,需要考虑的空间非常小。基本上只是检索和创建数据的问题。
YB 我想说我们在数据存储方面相当异构地使用了各种技术。是的,我们确实来自 LAMP 堆栈,所以仍然严重依赖 MySQL,但我们也使用 MongoDB 和其他数据存储技术。
这些服务通常各自封装了数据的某些区域。至少在理论上,它们都应该拥有自己的数据,并依赖仅专用于它们的数据存储。因此,最近我们开始考虑在服务本身内存储更多数据,以提高效率和性能。
TC 那么,为了明确起见,是否存在一个单独的数据访问层,服务可以从中提取它们需要操作的任何信息?
ES 是的,但您不会看到多个服务访问相同的数据存储。
JB 您如何在推出更新方面管理这一点?您是否有某种机制来部署更新,以及撤下更新和回滚更新?此外,这些服务是否隔离?如果是这样,您是如何实现这一点的?如果不是,您正在做什么来最大限度地减少停机时间?
YB 目前,每个服务都使用 broker/worker 基础设施。任何服务都不能访问另一个服务,除非通过 broker 池,然后 broker 池会将请求重新分配给多个 worker。这使我们能够通过在 broker 后面放置更多 worker 来进行扩展。然后,在部署方面,我们可以对这些 worker 的目标服务器进行滚动部署。这样,我们就能够在不影响用户体验或任何其他需要使用该服务的服务体验的情况下逐步部署服务的最新版本,因为它正在重新部署。
ES 我们最近从 LAMP 端引入的另一件事已被证明对频繁推出很有用,那就是功能标记。这显然在当时更容易实现,当时我们只是使用一堆 Web 服务器,因为我们有一个用于处理它的中心位置。但最近我们将这些相同的功能迁移到 HashiCorp 的 Consul,以便为自己提供分布式、强一致性存储,这现在使我们能够在 Scala 端部署代码并关闭某些功能。
JB 回顾您在执行此操作以及维护单体应用所需的一切时,您认为迁移到微服务的最大好处是什么?
ES 就扩展团队所需的工作而言,我认为在系统中拥有明确定义的边界已被证明更容易,因为这意味着您可以与只对整体产品如何运作有大致了解的人一起工作,但要用他们个人负责的特定服务的深入知识来补充这一点。
我还认为微服务方法在操作上给了我们更多的控制权。随着这些需求的出现,扩展和替换系统的特定部分变得容易得多。
YB 这方面的一个明显例子是我的团队一直在从事的数据服务。就我们存储的数据量而言,这是一项非常高容量的服务,我们知道鉴于我们正在使用的存储技术,扩展它将具有挑战性。将所有这些数据隔离在数据服务之后,使我们更容易实施必要的更改。基本上,这只是让我们对更改我们在后台使用的持久性技术所需的措施有了更多的控制权。所以我当然认为这是一个巨大的胜利。
当 Hootsuite 工程团队开始发现有机会缩减先前在其 LAMP 堆栈(每个请求都有一个进程)上运行的底层物理和虚拟基础设施时,迁移到 Lightbend 堆栈支持的反应式微服务模型的一些好处几乎立即浮出水面。因此,很快就显而易见,在反应式微服务模型下运行将大大减轻其资源的压力。
实际上,如果有什么不同的话,Hootsuite 的工程师很快了解到,通过继续采用一些在 LAMP 堆栈中很有意义的做法,他们实际上会剥夺自己许多通过更大程度地依赖 Lightbend 堆栈而可获得的好处。例如,他们发现更大程度地使用 Lightbend 堆栈支持的模型类确实有优势,因为这些类配备了数据层知识,这些知识在动态的面向 Web 的系统中可能非常有用。
同样,他们了解到,通过使用单个 actors 来运行系统的相当大部分,而不是将这些组件分解为 actors 组,他们不知不觉地剥夺了自己 Akka 为单独调整系统部件、并行化系统部件以及然后在许多能够分担负载的不同 actors 之间有效地分配工作而提供的某些功能。
还有一些其他的事情他们也学到了...
TC 到目前为止,我们只讨论了一般性问题。现在我想听听你们更具体的一些工程挑战。
JB 我想知道的一件事是,您主要是在进行反应式扩展、预测式扩展,还是两者结合使用来优化您的硬件。
YB 至少目前是这样,我们的负载实际上并没有太大变化。或者也许我应该说它们在一整天中都在变化,但从一天到另一天都是可预测的。我们设计服务在 broker 后面运行的方式意味着我们能够根据需要启动更多 worker。结合 AWS(亚马逊网络服务)的一些出色工具,我们能够快速适应不断变化的工作负载。
ES 我们决定做的一件事是构建一个使用 ZeroMQ 的框架,以实现我们的各种 PHP 系统之间的进程通信。但后来我们发现,我们本可以很容易地将所有这些都拉入 Akka。
TC 我假设,使用 Akka,您将更容易实现您坚持 actor 范例的目标,同时还可以利用更好的恢复机制和更细粒度的控制。
ES 是的,但我认为关键在于,由于我们能够通过配置 actors 的方式来更改 actors 的特性,因此我们能够使这个核心框架适应各种服务的各种类型的负载和流量配置文件。我们可以说,“此服务正在使用阻塞数据库”,此时将提供一个大型线程池。如果该服务恰好正在处理两种不同类型的作业,我们可以将它们分成不同的执行轨道。
最近,我们在一些情况下也取得了相当大的成功,在这些情况下,由于某些第三方参与,我们遇到了一些渐进式超时。但现在我们可以直接断开连接并继续进行。这一切中的大部分都来自于 Akka 提供的所有工具。我们了解到,通过尽可能保持设计简单,我们可以更好地利用这些工具。
JB 我还发现非常有趣的一点是,Akka 和 Erlang 似乎是唯一强调拥抱和管理故障的平台或库——也就是说,它们基本上是为弹性而设计的。获得最大好处的最佳方法是,如果它们从第一天起就成为您应用程序的一部分。这样,您就可以在架构的核心完全拥抱故障。
但是,话虽如此,对于那些来自具有非常不同心态的其他环境的开发人员来说,这种新发现的对故障的拥抱在实践中是如何奏效的?这是他们能够在相当短的时间内接受并开始感到自然的事情吗?
YB 对于某些人来说,这实际上需要相当大的思维模式转变。但我认为 Akka——或者至少是 actor 模型——使人们更容易理解其好处,因为您必须非常明确地说明如何作为主管处理故障。也就是说,作为一个衍生出其他 actors 的 actor,您必须制定规则,说明如果您的一个子 actor 最终失败,应该发生什么。但是,是的,人们必须接受这方面的教育。即使这样,仍然需要一段时间才能适应。
现在,随着新开发人员的加入,我们看到他们求助于更传统的异常处理模式。但是,一旦您了解了将异常处理留给主管层次结构是多么明智,通常就不会回头了。您的代码会变得简单得多,这意味着您可以将注意力转向弄清楚每个 actor 应该负责什么。
TC 抽象地谈论 actor 框架是一回事,但这在实际应用中是什么样的呢?例如,您如何处理故障情况?
YB 您可以让 actor 失败,这意味着它基本上会死亡,然后将死亡通知发送给主管。然后,主管可以根据故障的严重程度或性质来决定如何处理这种情况——无论是意味着衍生出一个新的 actor 还是仅仅忽略故障。如果看起来问题是系统实际上应该能够处理的,它将只使用一个新的 actor 本质上再次发送相同的消息。
但关键是,actor 模型允许您将与处理特定故障情况相关的所有逻辑集中在一个地方。由于 actor 系统是层次结构,一种可能性是您最终会决定问题实际上不是原始 actor 的责任,而应该由该 actor 的主管来处理。这是因为这些层次结构创建背后的逻辑不仅决定了处理将在哪里完成,还决定了故障将在哪里处理——这不仅是一种组织代码的自然方式,而且是一种非常清楚地分离关注点的方法。
JB 我认为这真的抓住了要害。这归结为区分我们称之为错误的东西——用户负责处理的事情——和故障。然后,验证错误自然会返回给用户,而不太严重的错误会返回给创建服务的组件。这创建了一个更容易推理的模型,而不是在可能发生故障的任何地方用 try/catch 语句来污染您的代码——因为故障可能并且将会发生在分布式系统中的任何地方。
YB 从这一切中得出的一个非常棒的教训是,它让我开始思考系统实际上是如何通过消息处理故障和处理我们与之通信的外部代理的。基本上,我开始思考我们应该如何围绕处理故障的方式来处理服务之间的通信。所以现在这是我们始终思考的事情。
现实情况是,任何时候我们与外部服务对话,我们都应该预料到一些故障。它们肯定会发生。这意味着我们不应该指望某些外部服务在短时间内做出响应。我们希望显式设置超时。然后,如果我们看到服务开始非常快速地或以高频率失败,我们将知道是时候触发断路器以减轻该服务的压力,而不是让这些故障在所有服务中回响。我不得不说,当我们第一次开始使用 Akka 时,这是一个我肯定没有预料到的额外好处。
通过采用分布式微服务架构来提高系统资源利用率的效率是一回事。但这在多大程度上可能会最终将额外的负担转移到您的程序员身上?毕竟,长期以来,为异步分布式系统编写代码一直被认为是只有训练有素的绝地武士才敢涉足的领域。
对于更习惯于在同步环境范围内工作的程序员来说,可以做些什么来简化向反应式微服务环境的过渡?他们通常对资源状态所做的所有假设不会经常被违反吗?以及如何在合理的时间内让一支庞大的编码团队掌握并发学习曲线?
以下是 Hootsuite 团队学到的...
TC 让我们谈谈迁移到微服务对您的开发人员产生的影响。特别是,我认为这意味着您向他们抛出了比大多数开发人员习惯的更多的异步性。我想象他们现在可能还有更多的关于数据一致性的问题需要担心。
YB 虽然异步性问题尚未完全解决,但 Scala Futures(用于检索并发操作结果的数据结构)实际上使异步计算非常容易使用。我的意思是,仍然需要一些时间来适应任何事物都可能并且将会失败的事实。但是,使用 Scala Futures,对于相对没有经验的程序员来说,学习如何在异步世界中表达自己实际上非常容易。
ES 如果您从基于线程的并发的角度来看待这个问题,那么对于许多用例来说,它会比您从 Futures 的角度来看待它更可怕。此外,当您直接在 actors 内部工作时,即使消息是异步传输的,并且系统同时执行数千件事,您也会与同步任何状态修改所需的工作隔离开来,因为一个 actor 一次只会处理一条消息。
KEN BRITTON 我注意到,当开发人员刚开始编写 Scala 时,他们最终会得到这些深度嵌套的、控制流风格的程序。您在命令式语言中看到了很多这种情况,但这样做没有惩罚。然而,在强类型函数式语言中,通过复杂的层次结构来对齐类型要困难得多。开发人员很快就会了解到,通过编写小型函数块,然后从这些函数块中组合程序,他们会得到更好的服务。
Akka 通过鼓励您使用消息来分解逻辑,从而更进一步。我观察到一种常见的演变模式,开发人员最初会使用这些非常笨重、复杂的 actors,但后来才发现他们本可以改为将 Future 管道化到他们自己的 actor 或任何其他 actor。事实上,我目睹了许多顿悟时刻,开发人员突然意识到这些工具实际上鼓励他们构建更小的可组合软件单元。
JB 这也与我的经验相符。Actors 非常面向对象,它们封装了状态和行为——所有这些我都认为与传统方法很好地结合在一起。另一方面,Futures 更适合函数式思维——所有这些小的、无状态的、一次性的东西,您可以轻松地组合它们。但是您是否发现您可以真正使这些源自两个截然不同的宇宙的东西很好地协同工作?您是将它们混合在一起还是保持分离?
YB 我们已经并行地使用了它们,我认为它们在这种方式下运行良好。Ken 提到了生成 Futures 然后将它们管道化到您自己作为 actor 或某些其他 actors 的想法。我认为这种模式运行得非常好。它既简单又优雅。
ES 我不得不承认,我早期在这个思维转变上绊倒了一点。但是,是的,我想说我们在很大程度上已经能够成功地混合 actors 和 Futures。
JB 您是否觉得某些类型的问题更适合其中一种或另一种?
ES 在我们更简单的服务中,将请求路由到代码完全是基于 actor 的,然后实际的业务逻辑通常编写为对产生 Futures 的其他事物的调用。我想,当您考虑基础设施和管道化事物时,用 actors 来思考是非常自然的。另一方面,业务逻辑可能更容易映射到函数式观点。
KB 我们还发现,丰富的面向对象模型对我们的消息传递很有帮助。例如,我们已经开始定义更丰富的成功和失败消息,其中包含足够的细节,以便让 actor 准确地知道如何响应。因此,现在我们的消息层次结构已经扩展到封装大量信息,我们认为这很好地将面向对象的概念与 actor 模型结合起来。
TC 当您谈到您的环境时,我想到的一件事是,似乎您不仅从单体架构迁移,而且在某种意义上,也从单体技术迁移到更广泛的技术阵列。所以我想知道您现在是否发现更难以在该环境中操作,以及培训人们在该环境中工作。虽然以前可能只需要找到一些精通 PHP 的新员工就足够了,但现在您有 ZeroMQ 和 actors 以及 Futures 以及其他任何数量的东西让他们绞尽脑汁。毫无疑问,您的环境变得更加复杂。但现在在某些方面,它实际上也是一个更容易操作的地方吗?
YB 我认为将逻辑划分为许多不同的独立服务,在某种程度上更容易推理系统的工作原理。但我们还没有完成。仍然有很多工作要做,还有许多具有挑战性的领域需要继续推理。
是的,当然,环境变得有点复杂。我不得不同意这一点。但是,引入所有这些技术的好处大于缺点,因为我们现在有更多的抽象层可以利用。我们有团队通常了解大局,但主要专注于他们真正了解的少数微服务。这种方法将在我们向前扩展运营时为我们带来巨大的好处。
KB 当使用微服务时,框架和标准对于开发团队有多么重要,这一点已经变得显而易见。人们经常将微服务提供的灵活性误认为是每个服务都需要使用不同技术。与所有开发团队一样,我们仍然需要将我们使用的技术数量保持在最低限度,以便我们可以轻松地培训新人、维护我们的代码、支持团队之间的调动等等。
我们还看到了服务规模缩小的趋势。我们最初的微服务实际上更像是松散耦合的宏服务。然而,随着时间的推移,我们已将更多的部署、运行时等推送到共享库、工具等中。这确保了服务专注于逻辑而不是管道,同时还坚持团队标准。
版权所有 © 2017 归所有者/作者所有。出版权已许可给 。
最初发表于 Queue vol. 15, no. 3—
在 数字图书馆 中评论本文
Kiran Prasad、Kelly Norton、Terry Coatta - LinkedIn 的 Node:追求更薄、更轻、更快
Node.js,这个用于构建可扩展网络应用程序的服务器端基于 JavaScript 的软件平台,在过去的几年里在许多开发人员中风靡一时,尽管它的流行也激怒了一些其他人,他们发布了大量负面博客文章来指出其公认的缺点。尽管 Node 仍然很新且未经测试,但它仍在赢得更多拥护者。
Peter Christy - 评论:没有路线图的旅行
从广义上看,编程项目失败的次数远多于成功的次数。在某些情况下,失败是迈向成功的有用一步,但很多时候它只是失败。