模块,单甘油和微服务

2021-02-24 22:01:30

最近,有人问我什么时候微服务是个好主意。在解释世界的系统设计中,我谈到了诸如第二系统效果,创新者的困境之类的大问题。系统设计可以回答微服务问题吗?

您可以在Internet上找到各种定义。这是我的:微服务是对整体的最大抵制。

当您将整个应用程序所需的所有内容链接到一个大型程序并将其部署为一个大型Blob时,就会发生独石。 Monoliths历史悠久,可以追溯到CGI,Django,Rails和PHP等框架。

立即让我们放弃这样的假设,即单一的服务和一组微服务是仅有的两个选择。一项可以完成所有任务的巨型服务提供了广泛而细微的连续体到无限微小的服务,每个服务几乎什么都不做。

如果您追随潮流,那么您至少会构建一个整体(无论是有意使用还是因为传统框架鼓励您这样做),然后发现整体存在一些问题,然后听说微服务是答案,然后开始将所有内容重新配置为微服务。

但是不要追随时尚。在这些极端之间有很多要点。其中之一可能很适合您。更好的方法从要放置接口的位置开始。

接口是模块之间的连接。模块是相关代码的集合。在系统设计中,我们谈论的是“框和箭”。工程:模块是盒子,界面是箭头。

那么,更深层的问题是:盒子有多大?每个盒子多少钱?我们如何确定何时将一个大盒子分成两个小盒子?连接盒子的最佳方法是什么?所有这些都有很多方法。没有人知道最好的。这是软件体系结构中最困难的问题之一。

在过去的几十年中,我们经历了许多种类的盒子。 Goto陈述被认为是有害的主要是因为它们根本阻止了任何等级制度。然后,我们添加了功能或过程;这些是非常简单的盒子,它们之间具有接口(参数和返回码)。

然后根据递归的编程分支发现递归函数,静态函数原型,库(静态或运行时链接),对象(OOP),协程,受保护的虚拟内存,进程,线程,JIT,名称空间,沙箱,chroot ,监狱,容器,虚拟机,主管,管理程序,微内核和unikernel。

就是盒子了!一旦盒子彼此隔离,则需要用箭头将它们连接起来。为此,我们有ABI,API,系统调用,套接字,RPC,文件系统,数据库,消息传递系统和“虚拟化硬件”。

如果您试图绘制现代Unix系统的完整方框图(我赢了),那将是很疯狂的:函数位于用户空间内的容器内部,内核之下, VM,在由编排系统捆绑在一起的云提供商的数据中心机架中的硬件上运行,依此类推。

每个抽象层上的这些框中的每个框都以某种方式与相同或其他层的其他框隔离,然后连接到其他框。有些在其他里面。您无法在仅二维的情况下绘制出真实的这张照片,而没有线条纵横交错的希望。

这一切都在数十年间演变。花哨的人称其为“路径依赖”。我称之为混乱。而且要弄清楚:大多数混乱不再提供太多价值。

而不是专注于患有非常丑陋的进化结果的东西,而是谈论他们在发明所有这些东西时努力做的事情。

升级,降级并缩放一些位,而无需同时升级所有其他位。

计算机行业花费绝对巨大的时间乱搞,试图找到所有这些模块化问题的完美平衡,同时仍然试图使开发成为无痛和轻松。

到目前为止,我们'在最糟糕的是#1,隔离。如果我们可以真正和有效地隔离另一个代码,另一个目标大多落在地上。但我们根本不知道如何。

隔离是一个超硬问题。善良知道人们已经尝试过。然而,浏览器沙箱逃脱仍然是经常发生的,每个操作系统都证明未检测到的特权升级攻击,iOS仍然会定期越狱,DRM永远不会工作(无论好坏),虚拟机和容器定期发现漏洞,以及K8S这样的系统默认情况下,将其包含不安全配置的容器。

甚至可以通过在Internet上向它们发送井定时数据包来弄清楚远程服务器上的加密密钥。同时,近期内存中最壮观的隔离失败是崩溃和幽灵攻击,它允许计算机上的任何程序,甚至是Web浏览器中的JavaScript应用程序,即使是沙箱或跨越沙箱虚拟机。

每一个新的隔离技术都会通过以下循环,从乐观到绝望:

(用户抱怨说,这比我们最后尝试的还要慢,而且更乏味。)

但是我们也永远不能淘汰这种隔离方法,因为现在有太多人依赖它。

例如,此时安全人员根本不相信以下任何一种(每一种都是当时可用的最好的技术)都是绝对安全的:

当允许远程执行代码(对于安全人员来说是RCE)时,OS进程之间的特权分离。

据我所知,最好的隔离状态是Chrome沙箱或gVisor。大型浏览器供应商和云提供商均使用此类工具。这些工具仍然不完善,但是提供商确实会尽快追捕每一个新的漏洞,而且新漏洞的发生速度也相当慢。

隔离比以往任何时候都要好。。。如果您将所有隔离都放在虚拟机(VM)级别,以便您的云提供商可以做到这一点,因为没人能知道或经常更新。

如果您信任云提供商的VM隔离,则可以希望所有已知问题都得到缓解。但是我们有充分的理由认为会发现更多的问题。

考虑到所有因素,这实际上还不错。至少我们有一些可行的方法。

好吧,等等。为每个小模块启动一个隔离的虚拟机是很痛苦的。模块有多大?

很久以前,当Java首次面世时,梦想是每个对象中每个函数的每一行都可以强制执行权限,即使是在同一应用程序二进制文件中的对象之间也是如此,因此不需要CPU强制的内存保护。没有人再相信他们可以做到这一点。以及诸如“云功能”之类的营销主张除了,没有人真的认为您应该尝试。

当前已知的隔离方法都无法完美地发挥作用,但是它们中的每一种都可以近似地工作。越来越熟练的攻击者或越来越有价值的目标,需要更好和更烦人的隔离。我们目前所知道的最佳隔离是第1层云提供商提供的VM间沙箱。最坏的是,它下降到零。

我们还跳过了证据,假设大多数系统是如此紧密地耦合在一起,以至于熟练的攻击者可以在模块之间横向突破。因此,例如,如果有人可以将恶意库链接到您的Go或C ++程序中,那么他们很可能可以控制整个程序。

同样,如果您的程序具有对数据库的写访问权,则攻击者可能会使其在数据库中的任何位置进行写操作。如果可以联系网络,他们可能可以联系网络中的任何地方。如果它可以执行任意的Unix命令或系统调用,则它们可能可以获得Unix根访问权限。如果在容器中,它们可能会从该容器中破裂并进入其他容器。如果恶意数据可能导致png解码器崩溃,则它们可能会使它执行解码器程序允许执行的其他任何操作。等等。

一种特别强大的攻击形式是提交代码的能力,因为该代码最终将在开发人员机器上运行,并且某些开发人员或生产机器可能在某处有权执行您想做的事情。

上面的内容可能有点过于悲观,但是做出这些假设可以帮助避免在不提高实际安全性的情况下使系统过于复杂。 Daniel J. Bernstein在qmail 1.0十年后对安全性的一些思考中指出(如果我可能要大量解释的话),他在qmail中添加了许多防御措施,特别是使用chroot和不同的Unix uid将不同的组件彼此隔离,不值得,也从未获得回报。

无论如何,让我们认为具有执行代码能力的攻击者通常可以"对于几乎所有的模块隔离技术,都可以在耦合模块之间横向跳转。这意味着只有两种模块边界:

可信赖的:两个模块相互信任的边界不是恶意的,因此可以使用弱隔离。

不可信:模块之间不相互信任的边界,因此它们必须使用强隔离。

我在这里没有说什么非常有见地的东西。围绕这种区别的流行现代平台已经建立。

例如,由于网页不可信任,因此Chrome在高度隔离的沙箱VM中运行随机的Web javascript。

大多数OS都将本机应用程序作为纯粹的进程(没有沙箱),共享文件系统,网络名称空间等运行,因为我们曾经认为它们相对值得信赖。 (这就是病毒的发生方式。)

专家不再信任多用户的unix系统,因为事实证明进程隔离很弱。云虚拟机默认为无密码sudo,因为事实证明根与非根隔离很弱,所以为什么还要打扰。

(我们仍然让人们输入sudo来帮助减少删除所有文件或其他内容时人为错误的影响。)

来自多个供应商的共享库和DLL被链接到其他供应商的应用程序中,因为所有代码都被认为是可信赖的。 (这为通过开放源代码库供应商进行供应链攻击开辟了道路。令我感到惊讶的是,这种情况不会经常发生。在我愤世嫉俗的时刻,我想也许是这样,而且他们很少检测到。)

手机操作系统会越狱,因为应用商店限制本应使应用沙箱足够值得信赖,但是隔离仍然总是太弱。

Kubernetes和Docker在单个机器或VM中运行多个隔离程度不高的容器,因为这些容器隐式地被认为是值得信任的。他们强烈建议您不要尝试运行多租户。 Kubernetes集群(不可信的应用程序代表单独的用户而不是相互信任的用户运行),因为容器隔离实际上很弱。

哦,即使对每个服务都使用gVisor虚拟机之类的强隔离,如果代码本身不是使用强隔离的工具链构建的,那也无济于事。如果一组人可以更新一个库,然后将其链接到一组应用程序中,那么这些应用程序实际上并不是彼此隔离的,无论它们如何运行。

历史大部分如果我们放弃了大多数这些层,那么安全性不会受到太大影响,并且简单性将得到改善。我希望随着时间的推移会发生这种情况。我们已经看到了这一趋势。多用户的Unix系统几乎已经不存在了。 "无服务器"服务器会丢弃除最强类型之外的所有隔离类型,并在您在那里时尝试将您锁定在云提供商中。

但是让'留下历史。我不得不介绍所有这些孤立概念,所以我可以说些简单的东西:出于安全原因,你几乎从不定义模块边界。

相反,模块边界通常遵循Conway'法律。人们根据他们想要在其团队中细分开发工作的方式分手模块,并根据团队和队友的沟通方式最终结束沟通。 (Conway'法律是迷人和真实的,但你可以在许多其他地方阅读它。让'现在跳过它。)

Chromeos有数千名开发人员,但用户收到了一个包含一个完全测试的Linux内核组合的单个更新,窗口管理器,Web浏览器等。这些模块之间的接口可能会在任何版本中更改,因为它们不会' t需要向后兼容性(除了包含硬件和Web的课程之外)。 MacOS,iOS和Android遵循类似的型号。

Debian Linux有数千名开发人员,但用户下载并安装单个包。您可以从今天的新包装中从古代Debian-Oldstable运行一个包,并从今天' s debian-unstable,最有可能的' ll工作。可能没有人曾经测试过您的特定组合,但可能是它的工作原因,因为包之间的界面非常明确。

(人们对桌面上的Linux的不可靠性笑话。"他们始终谈论第二个,利基,难以测试的类型,而不是第一个,主流,更容易 - 最佳。我不认为感知的质量差异实际上是由公司金钱与开源的。差异是部署模型。)

这两个系统都包含众多由众多开发商组织成团队的软件包(模块)。它们都有模块之间的界面。如果您画了每个系统的盒子和箭头图,它可能看起来很相似:核,驱动程序,窗口系统,沙箱,Web浏览器等。

然而,如果这些是后端云服务而不是iSS,我们将分别称为这两个模型和微服务,因为他们的部署模型。一个只有一个部署的"服务,"虽然另一个有很多,但每个都分别部署。对于相同的模块架构!什么'正在继续?

隔离:如果出于安全性目的确实需要强隔离,则现在需要单独的服务,因为推出隔离的VM的唯一方法是单独进行(尽管请注意:这更多是对我们的隔离系统的限制,而不是架构目标。作为代码的基础架构和蓝色/绿色部署尝试使这些服务再次恢复同步,因此您可以使用整体式部署模型。)

联系:遵循康威定律。模块边界倾向于遵循您团队的个人沟通模式。但是违反直觉的是,康韦定律不需要定义服务边界。

兼容性保证:向整体施加压力。如果您的整体语言是使用Go,Typescript,rust甚至C ++之类的类型安全语言编写的,则尤其如此。 (例如,Chrome是一个巨大的二进制文件。)

升级,降级和可伸缩性:这些主要决定了您的服务边界。让我们再谈谈他们。

您的整体需要长时间启动吗?这使升级变得很痛苦,因此您可能希望将较慢的部分分开,以使其他升级过程变得更快。

您是否需要正确的数据存储架构版本?有时,需要对后端的所有实例进行锁步升级/降级,以使它们位于同一架构版本上。锁步升级存在风险,并且倾向于防止回滚。您有时希望将与架构相关的部分保持尽可能小。

连续集成测试是否经常失败?如果是这样,那我有个坏消息。那些失败的测试表明您的代码已损坏。这是一个功能!分离服务并分别推出它们可能会使测试无法通过,但是随后您将在生产中遇到兼容性和版本偏斜问题。那没有帮助。

与其他部分不同的部分尺度缩放?例如,某些操作是内存重的,而其他操作则是CPU-Shive。这并不像你一样重要。如果您的所有实例正确平衡,那么负载往往以非常有效的方式自然地传播。如果负载平衡成为问题,则可以稍后测量并修复特定的粒度问题。

昂贵的请求是否需要使用较少的并行性运行?常见的微服务架构是将请求转储到消息队列中,并顺序地具有工人实例请求。但是,这比思考更频繁地出错了,想想,有更好的设计避免"队列爆炸"问题。您可以在整料中实施相同的设计。

您是否拥有不同的质量/可靠性目标服务?这可能是分裂服务的充分理由。例如,在TailScale下,我们只有几个具有非常严格的正常运行时间目标的服务:协调服务和日志捕集服务。这两个已经拆分为安全隔离,因为日志非常敏感。在那之上,我们的"实时"日志/指标处理管道可以容忍更多的停机时间,因此更具实验,所以它与高可靠性服务分开,可以具有不同的部署过程。

事实上,上述大部分的大多数都是相当具有在服务之间创造界限的不铭文的理由。它们可能是在模块或团队之间创造界限的绝佳原因!但是,在将它们重新结合成一个或多种整体之后,您可以推出模块。

请记住,Chromeos是一片巨石。 ios是一块巨石。您的团队可能比这些团队中的任何一个小得多。你只是不需要拼凑了很多微服务来获得你想要的东西。建筑物的东西简单的方式,直到你'绝对被迫这样做艰难的方式。那个'我们做了什么。