系统编程的开源开发者指南

应用程序运行在系统上,理解如何正确地对系统进行编程是开发者的一项关键技能。
4 位读者喜欢此文。
Woman sitting in front of her computer

Ray Smith

编程是一种帮助实现模型的活动。什么是模型?通常,程序员会对现实世界的情况进行建模,例如在线购物。

当您在现实世界中购物时,您会进入商店并开始浏览。当您找到想要购买的商品时,您会将它们放入购物车。购物完成后,您去收银台,收银员清点所有商品,并向您展示总额。然后您付款并带着新购买的商品离开商店。

得益于技术的进步,现在您无需前往实体店即可完成相同的购物活动。您可以通过让软件创建者团队对实际的购物活动进行建模,然后使用软件程序模拟这些活动来实现这种便利。

这些程序运行在由网络和其他计算基础设施组成的信息技术系统上。 挑战是在出现故障的情况下使系统可靠。

为什么会出现故障?

提供在线购物等虚拟功能的唯一方法是在网络(即互联网)上实现该模型。网络的一个问题是它们本质上是不可靠的。每当您计划实现网络应用程序时,都必须考虑以下普遍存在的问题

  • 网络不可靠。
  • 网络上的延迟不为零。
  • 网络上的带宽不是无限的。
  • 网络不安全。
  • 网络拓扑结构往往会发生变化。
  • 网络上的传输成本不为零。
  • 网络不是同质的。
  • “在我的机器上可以工作”并不能证明应用程序实际上可以正常运行。

从上面的列表中可以看出,在计划启动应用程序或服务时,有很多理由可以预期会出现故障。

什么是系统?

您依赖于系统来支持应用程序。那么,什么是系统?

系统是指组合在一起的东西,这意味着它是程序的组合,可以为其他程序提供服务。这种设计是松耦合的。它是分布式的和去中心化的(即,它没有全局监督/管理)。

什么是可靠的系统?

考虑构成可靠系统的属性

  • 可靠的系统是一个始终启动并运行的系统。 这样的系统能够优雅地降级,这意味着当性能开始下降时,系统不会突然停止工作。
  • 可靠的系统不仅始终启动并运行,而且还能够逐步增强。 随着对系统功能的需求增加,可靠的系统会进行扩展以满足需求。
  • 可靠的系统也易于维护,无需昂贵的更改。
  • 可靠的系统是低风险的。 部署对此类系统的更改是安全且简单的,可以通过回滚或前进的方式进行。

所有构建的东西最终都会超出理解能力

每个成功的系统都是从一个更简单的设计创建的。 随着系统被增强和修饰,它们最终会达到其复杂性无法轻易理解的地步。

考虑一个由许多运动部件组成的系统。 随着系统中运动部件数量的增加,这些运动部件之间的相互依赖程度也随之增加(图 1)。

randomness increases with complexity

(Alex Bunardzic, CC BY-SA 4.0)

只有在该系统增长的早期阶段,人们才能对系统进行正式分析。 达到一定的系统复杂性之后,人类只能通过应用统计分析来推断系统。

形式分析和统计分析之间存在差距(图 2)。

randomness increases with complexity

(Alex Bunardzic, CC BY-SA 4.0)

如何编程一个系统?

开发人员知道如何编写有用的应用程序,但他们还必须知道如何编程一个使应用程序能够在网络上运行的系统。

事实证明,似乎没有可用的系统编程语言。 尽管开发人员可能知道许多编程语言(例如 C、Java、C++、C#、Python、Ruby、JavaScript 等),但所有这些语言都专门用于对应用程序的功能进行建模和模拟。 但是如何对系统功能进行建模呢?

看看系统是如何组装的。 基本上,在一个系统中,程序之间相互通信。 他们是怎么做到的?

它们通过网络进行通信。 由于没有两个或多个程序相互通信的系统,因此很明显,编程系统的唯一方法是编程网络。

在更仔细地研究如何编程网络之前,我将检查网络的主要问题 - 故障。

故障发生在系统级别

系统中的故障是如何发生的? 一种方式是一个或多个程序突然变得不可用时。

该故障与编程错误无关。 实际上,编程错误并不是真正的错误,它们只是需要消除的错误!

网络基本上是一个链条,正如每个人都知道的那样,链条的强度取决于其最薄弱的环节。

当一个环节断裂时(即,当其中一个程序变得不可用时),防止该中断导致整个系统瘫痪至关重要。

管理员如何做到这一点? 他们提供了一个抽象边界,可以阻止错误的传播。 我现在将检查在系统内部提供这种抽象边界的方法。 这样做相当于编程一个系统。

系统编程中的最佳实践

设计程序和服务以满足机器的需求非常重要。 创建程序和服务以满足人类需求是一个常见的错误。 但是,在进行系统编程时,这种方法是不正确的。

为机器设计服务与为人类设计服务之间存在根本区别。 机器不需要操作界面。 但是,如果没有功能界面,人类就无法使用服务。

机器需要的是编程接口。 因此,在进行系统编程时,请完全专注于应用程序编程接口 (API)。 将操作界面固定在已经实现的编程接口之上会很容易,因此不要急于首先创建操作界面。

构建简单的服务也很重要。 这起初可能看起来不合理,但是一旦您了解了简单的服务很容易组合成更复杂的服务,那么它就更有意义了。

在进行系统编程时,为什么简单的服务如此重要? 通过专注于简单的服务,开发人员可以最大程度地降低陷入过早抽象化的风险。 不可能过度抽象化这种简单的服务。 结果是系统组件易于制作,推理,部署,修复和替换。

开发人员必须避免将服务变成一个整体的诱惑。 拒绝添加功能和特性来避免这样做。 此外,请勿将服务变成堆栈。 当其他用户(程序)决定使用该组件提供的服务时,他们应该可以自由选择适合消费服务的商品。

让服务用户决定使用哪个数据存储,哪个队列等。程序员绝不能将自定义堆栈强加给客户端。

服务必须完全隔离和独立。 换句话说,服务必须保持自治。

价值观的价值

在编程的上下文中,什么是值? 以下属性表征一个值

  • 没有身份
  • 短暂的
  • 无名
  • 在线

考虑一个服务的示例值,该服务返回每月服务费总额。 假设客户收到 425.00 美元作为每月服务费。 值 425.00 美元有什么特征?

  • 它没有身份。
  • 没有名称 - 它只是四百二十五美元 - 不需要单独的名称。
  • 它是短暂的 - 随着时间的推移,每月费用不断变化。
  • 它总是通过电线发送并由客户端接收。

值的短暂性质意味着流动。

系统不是面向场所的

面向场所的产品可以描述为在造船厂中建造的船舶。

系统是面向流程的

例如,汽车是在移动装配线上制造的。

值如何在系统中流动?

值会经历转换,并且会被移动,路由和记录。

  • 变换
  • 移动
  • 路由
  • 记录
  • 保持上述活动分离

值如何在系统中移动?

  • 来源 => 目的地
  • 移动器(生产者)取决于身份/可用性
  • 必须将生产者与消费者分离
  • 必须消除对身份的依赖
  • 必须消除对可用性的依赖
  • 使用队列 Pub/sub

对于值在系统中有效流动而言,避免依赖性至关重要。 脆弱的设计包括依赖于通过其身份找到某个服务或要求某个服务可用的过程。 唯一允许值在系统中流动的可靠设计是使用队列来解耦依赖关系。 建议使用发布/订阅排队模型。

主要为机器设计服务

避免将服务设计为供人类使用。 不应期望机器通过操作界面访问服务。 仅在构建了以机器为中心的服务之后,才能构建人工操作界面。

努力构建简单的服务。简单的服务易于组合。在设计简单的服务时,不会有过度抽象的风险。

不可能过度抽象一个简单的服务。

避免将服务变成一个单体应用。

避免添加功能和特性(保持超级简单)。不惜一切代价避免将服务变成一个技术栈。允许服务用户在消费服务时选择使用哪些商品。让他们决定使用哪个数据存储、哪个队列等等。不要向客户强加你自定义的技术栈。

系统故障模型是唯一的故障模型。

接下来,承认系统故障是必然会发生的!问题不在于是否会发生,而在于何时发生以及发生的频率。

什么时候会发生异常?任何时候,运行时系统不知道该做什么,结果都会是异常和系统故障。

这些故障不同于编程错误。错误发生在团队在实现处理逻辑时犯错(开发人员称这些错误为“bug”)。

无论何时系统发生故障,请注意它是部分的且不协调的。整个系统同时发生故障的可能性很小(几乎不可能发生这样的事件)。

可靠系统的最低要求。

至少,一个可靠的系统必须具备以下能力:

  • 并发性
  • 故障封装
  • 故障检测
  • 故障识别
  • 热代码升级
  • 稳定存储
  • 异步消息传递

我将逐一检查这些属性。

并发性

为了使系统能够同时处理两个或多个进程,它必须是非命令式的。系统绝不能阻止处理或对进程应用“暂停”按钮。此外,系统绝不能依赖于共享的可变状态。

在并发系统中,一切都是进程。因此,至关重要的是,可靠的系统必须具有用于创建并行进程的轻量级机制。它还必须能够有效地进行进程之间的上下文切换和消息传递。

并发系统中的任何进程都必须依赖故障检测原语来观察另一个进程。

故障封装

一个进程中发生的故障不能损害/削弱系统中的其他进程。

“进程通过不与其他进程共享状态来实现故障隔离;它与其他进程的唯一联系是通过内核消息系统传递的消息。” - Jim Gray

这是 Jim Gray 的另一句有用的话:

“与硬件一样,软件容错的关键是将大型系统分层分解为模块,每个模块都是一个服务单元和一个故障单元。一个模块的故障不会传播到模块之外。”

为了实现容错,必须只编写处理正常情况的代码。

如果发生故障,唯一推荐的行动方案是让它崩溃!修复故障并继续不是一个好的做法。不同的进程应该处理任何错误(升级错误处理模型)。

至关重要的是,要始终确保错误恢复代码和正常情况代码之间的清晰分离。这样做可以大大简化整体系统设计和系统架构。

故障检测

编程语言必须能够检测本地(在发生异常的进程中)和远程(看到非本地进程中发生了异常)的异常。

一旦组件的行为不再与其规范一致,则认为该组件存在故障。错误检测是容错的重要组成部分。

尽量保持任务简单,以增加成功的可能性。

面对失败,管理员更关心的是保护系统免受损害,而不是提供完整的服务。目标是提供可接受的服务水平,并在事情开始失败时变得不那么雄心勃勃。

尝试执行一项任务。如果你无法执行一项任务,请尝试执行一项更简单的任务。

故障识别

你应该能够识别出异常发生的原因。

热代码升级

能够在系统执行时更改代码,而无需停止系统。

稳定存储

开发人员需要一个稳定的错误日志,该日志可以在崩溃后幸存下来。以一种可以在系统崩溃后幸存下来的方式存储数据。

异步消息传递

异步消息传递应该是服务间通信的默认选择。

行为良好的程序

一个系统应该由行为良好的程序组成。这样的程序应该与规范同构。如果规范说了什么愚蠢的事情,那么程序必须忠实地重现规范中的任何错误。如果规范没有说明该怎么做,则引发异常!

避免猜测——现在不是发挥创造力的时候。

“对于安全来说,能够将不信任的程序彼此隔离,并保护宿主平台免受此类程序的侵害至关重要。在面向对象的系统中,隔离是困难的,因为对象很容易被别名化(即,至少两个或多个对象持有对一个对象的引用)” - Ciaran Bryce

任务不能直接共享对象。任务进行通信的唯一干净的方法是使用标准的复制通信机制。

总结

应用程序在系统上运行,了解如何正确地编程系统是开发人员的关键技能。系统包含可靠性和复杂性,最好通过一系列最佳实践进行管理。其中一些包括:

  • 进程是故障封装的单元。
  • 强大的隔离带来自主性。
  • 进程做他们应该做的事情,或者尽快失败(快速失败)。
  • 允许组件崩溃然后重新启动会导致更简单的故障模型和更可靠的代码。远程进程必须能够检测到故障以及故障的原因。
  • 进程不共享状态,而是通过消息传递进行通信。
标签
User profile image.
Alex 自 1990 年以来一直从事软件开发。他目前的热情是如何将软性带回软件中。他坚信,我们的行业已经达到了可以完全实现这一崇高目标(即将软性带回软件中)的成熟程度。

评论已关闭。

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.