分类 技术 下的文章

下文是 James Henry(@MrJamesHenry)所提交的内容。我是 ESLint 核心团队的一员,也是 TypeScript 布道师。我正在和 Todd 在 UltimateAngular 平台上合作发布 Angular 和 TypeScript 的精品课程。

本文的主旨是为了介绍我们是如何看待 TypeScript 的以及它在加强 JavaScript 开发中所起的作用。

我们也将尽可能地给出那些类型和编译方面的那些时髦词汇的准确定义。

TypeScript 强大之处远远不止这些,本篇文章无法涵盖,想要了解更多请阅读官方文档,或者学习 UltimateAngular 上的 TypeScript 课程 ,从初学者成为一位 TypeScript 高手。

背景

TypeScript 是个出乎意料强大的工具,而且它真的很容易掌握。

然而,TypeScript 可能比 JavaScript 要更为复杂一些,因为 TypeScript 可能向我们同时引入了一系列以前没有考虑过的 JavaScript 程序相关的技术概念。

每当我们谈论到类型、编译器等这些概念的时候,你会发现很快会变的不知所云起来。

这篇文章就是一篇为了解答你需要知道的许许多多不知所云的概念,来帮助你 TypeScript 快速入门的教程,可以让你轻松自如的应对这些概念。

关键知识的掌握

在 Web 浏览器中运行我们的代码这件事或许使我们对它是如何工作的产生一些误解,“它不用经过编译,是吗?”,“我敢肯定这里面是没有类型的...”

更有意思的是,上述的说法既是正确的也是不正确的,这取决于上下文环境和我们是如何定义这些概念的。

首先,我们要作的是明确这些。

JavaScript 是解释型语言还是编译型语言?

传统意义上,程序员经常将自己的程序编译之后运行出结果就认为这种语言是编译型语言。

从初学者的角度来说,编译的过程就是将我们自己编辑好的高级语言程序转换成机器实际运行的格式。

就像 Go 语言,可以使用 go build 的命令行工具编译 .go 的文件,将其编译成代码的低级形式,它可以直接执行、运行。

# We manually compile our .go file into something we can run
# using the command line tool "go build"
go build ultimate-angular.go
# ...then we execute it!
./ultimate-angular

作为一个 JavaScript 程序员(这一刻,请先忽略我们对新一代构建工具和模块加载程序的热爱),我们在日常的 JavaScript 开发中并没有编译的这一基本步骤,

我们写一些 JavaScript 代码,把它放在浏览器的 <script> 标签中,它就能运行了(或者在服务端环境运行,比如:node.js)。

好吧,因此 JavaScript 没有进行过编译,那它一定是解释型语言了,是吗?

实际上,我们能够确定的一点是,JavaScript 不是我们自己编译的,现在让我们简单的回顾一个简单的解释型语言的例子,再来谈 JavaScript 的编译问题。

解释型计算机语言的执行的过程就像人们看书一样,从上到下、一行一行的阅读。

我们所熟知的解释型语言的典型例子是 bash 脚本。我们终端中的 bash 解释器逐行读取我们的命令并且执行它。

现在我们回到 JavaScript 是解释执行还是编译执行的讨论中,我们要将逐行读取和逐行执行程序分开理解(对“解释型”的简单理解),不要混在一起。

以此代码为例:

hello();
function hello(){
    console.log("Hello")
}

这是真正意义上 JavaScript 输出 Hello 单词的程序代码,但是,在 hello() 在我们定义它之前就已经使用了这个函数,这是简单的逐行执行办不到的,因为 hello() 在第一行没有任何意义的,直到我们在第二行声明了它。

像这样在 JavaScript 是存在的,因为我们的代码实际上在执行之前就被所谓的“JavaScript 引擎”或者是“特定的编译环境”编译过,这个编译的过程取决于具体的实现(比如,使用 V8 引擎的 node.js 和 Chome 就和使用 SpiderMonkey 的 FireFox 就有所不同)。

在这里,我们不会在进一步的讲解编译型执行和解释型执行微妙之处(这里的定义已经很好了)。

请务必记住,我们编写的 JavaScript 代码已经不是我们的用户实际执行的代码了,即使是我们简单地将其放在 HTML 中的 <script> ,也是不一样的。

运行时间 VS 编译时间

现在我们已经正确理解了编译和运行是两个不同的阶段,那“ 运行阶段 Run Time ”和“ 编译阶段 Compile Time ”理解起来也就容易多了。

编译阶段,就是我们在我们的编辑器或者 IDE 当中的代码转换成其它格式的代码的阶段。

运行阶段,就是我们程序实际执行的阶段,例如:上面的 hello() 函数就执行在“运行阶段”。

TypeScript 编译器

现在我们了解了程序的生命周期中的关键阶段,接下来我们可以介绍 TypeScript 编译器了。

TypeScript 编译器是帮助我们编写代码的关键。比如,我们不需将 JavaScript 代码包含到 <script> 标签当中,只需要通过 TypeScript 编译器传递它,就可以在运行程序之前得到改进程序的建议。

我们可以将这个新的步骤作为我们自己的个人“编译阶段”,这将在我们的程序抵达 JavaScript 主引擎之前,确保我们的程序是以我们预期的方式编写的。

它与上面 Go 语言的实例类似,但是 TypeScript 编译器只是基于我们编写程序的方式提供提示信息,并不会将其转换成低级的可执行文件,它只会生成纯 JavaScript 代码。

# One option for passing our source .ts file through the TypeScript
# compiler is to use the command line tool "tsc"
tsc ultimate-angular.ts

# ...this will produce a .js file of the same name
# i.e. ultimate-angular.js

官方文档中,有许多关于将 TypeScript 编译器以各种方式融入到你的现有工作流程中的文章。这些已经超出本文范围。

动态类型与静态类型

就像对比编译程序与解释程序一样,动态类型与静态类型的对比在现有的资料中也是极其模棱两可的。

让我们先回顾一下我们在 JavaScript 中对于类型的理解。

我们的代码如下:

var name = 'James';
var sum = 1 + 2;

我们如何给别人描述这段代码?

“我们声明了一个变量 name,它被分配了一个 “James” 的字符串,然后我们又申请了一个变量 sum,它被分配了一个数字 1 和数字 2 的求和的数值结果。”

即使在这样一个简单的程序中,我们也使用了两个 JavaScript 的基本类型:StringNumber

就像上面我们讲编译一样,我们不会陷入编程语言类型的学术细节当中,关键是要理解在 JavaScript 中类型表示的是什么,并扩展到 TypeScript 的类型的理解上。

从每夜拜读的最新 ECMAScript 规范中我们可以学到(LOL, JK - “wat’s an ECMA?”),它大量引用了 JavaScript 的类型及其用法。

直接引自官方规范:

ECMAScript 语言的类型取决于使用 ECMAScript 语言的 ECMAScript 程序员所直接操作的值。

ECMAScript 语言的类型有 Undefined、Null、Boolean、String、Symbol、Number 和 Object。

我们可以看到,JavaScript 语言有 7 种正式类型,其中我们在我们现在程序中使用了 6 种(Symbol 首次在 ES2015 中引入,也就是 ES6)。

现在我们来深入一点看上面的 JavaScript 代码中的 “name 和 sum”。

我们可以把我们当前被分配了字符串“James”的变量 name 重新赋值为我们的第二个变量 sum 的当前值,目前是数字 3。

var name = 'James';
var sum = 1 + 2;

name = sum;

name 变量开始“存有”一个字符串,但现在它“存有”一个数字。这凸显了 JavaScript 中变量和类型的基本特性:

“James” 值一直是字符串类型,而 name 变量可以分配任何类型的值。和 sum 赋值的情况相同,1 是一个数字类型,sum 变量可以分配任何可能的值。

在 JavaScript 中,值是具有类型的,而变量是可以随时保存任何类型的值。

这也恰好是一个“动态类型语言”的定义。

相比之下,我们可以将“静态类型语言”视为我们可以(也必须)将类型信息与特定变量相关联的语言:

var name: string = ‘James’;

在这段代码中,我们能够更好地显式声明我们对变量 name 的意图,我们希望它总是用作一个字符串。

你猜怎么着?我们刚刚看到我们的第一个 TypeScript 程序。

当我们 反思reflect我们自己的代码(非编程方面的双关语“反射”)时,我们可以得出的结论,即使我们使用动态语言(如 JavaScript),在几乎所有的情况下,当我们初次定义变量和函数参数时,我们应该有非常明确的使用意图。如果这些变量和参数被重新赋值为与我们原先赋值不同类型的值,那么有可能某些东西并不是我们预期的那样工作的。

作为 JavaScript 开发者,TypeScript 的静态类型注释给我们的一个巨大的帮助,它能够清楚地表达我们对变量的意图。

这种改进不仅有益于 TypeScript 编译器,还可以让我们的同事和将来的自己明白我们的代码。代码是用来读的。

TypeScript 在我们的 JavaScript 工作流程中的作用

我们已经开始看到“为什么经常说 TypeScript 只是 JavaScript + 静态类型”的说法了。: string 对于我们的 name 变量就是我们所谓的“类型注释”,在编译时被使用(换句话说,当我们让代码通过 TypeScript 编译器时),以确保其余的代码符合我们原来的意图。

我们再来看看我们的程序,并添加显式注释,这次是我们的 sum 变量:

var name: string = 'James';
var sum: number = 1 + 2;

name = sum;

如果我们使用 TypeScript 编译器编译这个代码,我们现在就会收到一个在 name = sum 这行的错误: Type 'number' is not assignable to type 'string',我们的这种“偷渡”被警告,我们执行的代码可能有问题。

重要的是,如果我们想要继续执行,我们可以选择忽略 TypeScript 编译器的错误,因为它只是在将 JavaScript 代码发送给我们的用户之前给我们反馈的工具。

TypeScript 编译器为我们输出的最终 JavaScript 代码将与上述原始源代码完全相同:

var name = 'James';
var sum = 1 + 2;

name = sum;

类型注释全部为我们自动删除了,现在我们可以运行我们的代码了。

注意:在此示例中,即使我们没有提供显式类型注释的 : string: number ,TypeScript 编译器也可以为我们提供完全相同的错误 。

TypeScript 通常能够从我们使用它的方式推断变量的类型!

我们的源文件是我们的文档,TypeScript 是我们的拼写检查

对于 TypeScript 与我们的源代码的关系来说,一个很好的类比,就是拼写检查与我们在 Microsoft Word 中写的文档的关系。

这两个例子有三个关键的共同点:

  1. 它能告诉我们写的东西的客观的、直接的错误:

    • 拼写检查:“我们已经写了字典中不存在的字”
    • TypeScript:“我们引用了一个符号(例如一个变量),它没有在我们的程序中声明”
  2. 它可以提醒我们写的可能是错误的:

    • 拼写检查:“该工具无法完全推断特定语句的含义,并建议重写”
    • TypeScript:“该工具不能完全推断特定变量的类型,并警告不要这样使用它”
  3. 我们的来源可以用于其原始目的,无论工具是否存在错误:

    • 拼写检查:“即使您的文档有很多拼写错误,您仍然可以打印出来,并把它当成文档使用”
    • TypeScript:“即使您的源代码具有 TypeScript 错误,它仍然会生成您可以执行的 JavaScript 代码”

TypeScript 是一种可以启用其它工具的工具

TypeScript 编译器由几个不同的部分或阶段组成。我们将通过查看这些部分之一 The Parser(语法分析程序)来结束这篇文章,除了 TypeScript 已经为我们做的以外,它为我们提供了在其上构建其它开发工具的机会。

编译过程的“解析器步骤”的结果是所谓的抽象语法树,简称为 AST。

什么是抽象语法树(AST)?

我们以普通文本形式编写我们的程序,因为这是我们人类与计算机交互的最好方式,让它们能够做我们想要的东西。我们并不是很擅长于手工编写复杂的数据结构!

然而,不管在哪种情况下,普通文本在编译器里面实际上是一个非常棘手的事情。它可能包含程序运作不必要的东西,例如空格,或者可能存在有歧义的部分。

因此,我们希望将我们的程序转换成数据结构,将数据结构全部映射为我们所使用的所谓“标记”,并将其插入到我们的程序中。

这个数据结构正是 AST!

AST 可以通过多种不同的方式表示,我使用 JSON 来看一看。

我们从这个极其简单的基本源代码来看:

var a = 1;

TypeScript 编译器的 Parser(语法分析程序)阶段的(简化后的)输出将是以下 AST:

{
  "pos": 0,
  "end": 10,
  "kind": 256,
  "text": "var a = 1;",
  "statements": [
    {
      "pos": 0,
      "end": 10,
      "kind": 200,
      "declarationList": {
        "pos": 0,
        "end": 9,
        "kind": 219,
        "declarations": [
          {
            "pos": 3,
            "end": 9,
            "kind": 218,
            "name": {
              "pos": 3,
              "end": 5,
              "text": "a"
            },
            "initializer": {
              "pos": 7,
              "end": 9,
              "kind": 8,
              "text": "1"
            }
          }
        ]
      }
    }
  ]
}

我们的 AST 中的对象称为节点。

示例:在 VS Code 中重命名符号

在内部,TypeScript 编译器将使用 Parser 生成的 AST 来提供一些非常重要的事情,例如,发生在编译程序时的类型检查。

但它不止于此!

我们可以使用 AST 在 TypeScript 之上开发自己的工具,如代码美化工具、代码格式化工具和分析工具。

建立在这个 AST 代码之上的工具的一个很好的例子是: 语言服务器 Language Server

深入了解语言服务器的工作原理超出了本文的范围,但是当我们编写程序时,它能为我们提供一个绝对重量级别功能,就是“重命名符号”。

假设我们有以下源代码:

// The name of the author is James
var first_name = 'James';
console.log(first_name);

经过代码审查和对完美的适当追求,我们决定应该改换我们的变量命名惯例;使用驼峰式命名方式,而不是我们当前正在使用这种蛇式命名。

在我们的代码编辑器中,我们一直以来可以选择多个相同的文本,并使用多个光标来一次更改它们。

Manually select matches

当我们把程序也视作文本这样继续操作时,我们已经陷入了一个典型的陷阱中。

那个注释中我们不想修改的“name”单词,在我们的手动匹配中却被误选中了。我们可以看到在现实世界的应用程序中这样更改代码是有多危险。

正如我们在上面学到的那样,像 TypeScript 这样的东西在幕后生成一个 AST 的时候,与我们的程序不再像普通文本那样可以交互,每个标记在 AST 中都有自己的位置,而且它有很清晰的映射关系。

当我们右键单击我们的 first_name 变量时,我们可以在 VS Code 中直接“重命名符号”(TypeScript 语言服务器插件也可用于其他编辑器)。

Rename Symbol Example

非常好!现在我们的 first_name 变量是唯一需要改变的东西,如果需要的话,这个改变甚至会发生在我们项目中的多个文件中(与导出和导入的值一样)!

总结

哦,我们在这篇文章中已经讲了很多的内容。

我们把有关学术方面的规避开,围绕编译器和类型还有很多专业术语给出了通俗的定义。

我们对比了编译语言与解释语言、运行阶段与编译阶段、动态类型与静态类型,以及抽象语法树(AST)如何为我们的程序构建工具提供了更为优化的方法。

重要的是,我们提供了 TypeScript 作为我们 JavaScript 开发工具的一种思路,以及如何在其上构建更棒的工具,比如说作为重构代码的一种方式的重命名符号。

快来 UltimateAngular 平台上学习从初学者到 TypeScript 高手的课程吧,开启你的学习之旅!


via: https://toddmotto.com/typescript-the-missing-introduction

作者:James Henry 译者:MonkeyDEcho 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在恒久的迷惑与过多期待的海洋中,登上一组简单响应式设计原则的小岛。

下载 Konrad Malawski 的免费电子书《为什么选择响应式?企业应用中的基本原则》,深入了解更多响应式技术的知识与好处。

自从 2013 年一起合作写了《响应式宣言》之后,我们看着响应式从一种几乎无人知晓的软件构建技术——当时只有少数几个公司的边缘项目使用了这一技术——最后成为 中间件领域 middleware field 大佬们全平台战略中的一部分。本文旨在定义和澄清响应式各个方面的概念,方法是比较在响应式编程风格下和把响应式系统视作一个紧密整体的设计方法下编写代码的不同之处。

响应式是一组设计原则

响应式技术目前成功的标志之一是“ 响应式 reactive ”成为了一个热词,并且跟一些不同的事物与人联系在了一起——常常伴随着像“ streaming ”、“ 轻量级 lightweight ”和“ 实时 real-time ”这样的词。

举个例子:当我们看到一支运动队时(像棒球队或者篮球队),我们一般会把他们看成一个个单独个体的组合,但是当他们之间碰撞不出火花,无法像一个团队一样高效地协作时,他们就会输给一个“更差劲”的队伍。从这篇文章的角度来看,响应式是一组设计原则,一种关于系统架构与设计的思考方式,一种关于在一个分布式环境下,当 实现技术 implementation techniques 、工具和设计模式都只是一个更大系统的一部分时如何设计的思考方式。

这个例子展示了不经考虑地将一堆软件拼揍在一起——尽管单独来看,这些软件都很优秀——和响应式系统之间的不同。在一个响应式系统中,正是不同 组件 parts 间的相互作用让响应式系统如此不同,它使得不同组件能够独立地运作,同时又一致协作从而达到最终想要的结果。

一个响应式系统 是一种 架构风格 architectural style ,它允许许多独立的应用结合在一起成为一个单元,共同响应它们所处的环境,同时保留着对单元内其它应用的“感知”——这能够表现为它能够做到 放大/缩小规模 scale up/down ,负载平衡,甚至能够主动地执行这些步骤。

以响应式的风格(或者说,通过响应式编程)写一个软件是可能的;然而,那也不过是拼图中的一块罢了。虽然在上面的提到的各个方面似乎都足以称其为“响应式的”,但仅就其它们自身而言,还不足以让一个系统成为响应式的。

当人们在软件开发与设计的语境下谈论“响应式”时,他们的意思通常是以下三者之一:

  • 响应式系统(架构与设计)
  • 响应式编程(基于声明的事件的)
  • 函数响应式编程(FRP)

我们将调查这些做法与技术的意思,特别是前两个。更明确地说,我们会在使用它们的时候讨论它们,例如它们是怎么联系在一起的,从它们身上又能到什么样的好处——特别是在为多核、云或移动架构搭建系统的情境下。

让我们先来说一说函数响应式编程吧,以及我们在本文后面不再讨论它的原因。

函数响应式编程(FRP)

函数响应式编程 Functional reactive programming ,通常被称作 FRP,是最常被误解的。FRP 在二十年前就被 Conal Elliott 精确地定义过了了。但是最近这个术语却被错误地 脚注1 用来描述一些像 Elm、Bacon.js 的技术以及其它技术中的响应式插件(RxJava、Rx.NET、 RxJS)。许多的 libraries 声称他们支持 FRP,事实上他们说的并非响应式编程,因此我们不会再进一步讨论它们。

响应式编程

响应式编程 Reactive programming ,不要把它跟函数响应式编程混淆了,它是异步编程下的一个子集,也是一种范式,在这种范式下,由新信息的 有效性 availability 推动逻辑的前进,而不是让 一条执行线程 a thread-of-execution 去推动 控制流 control flow

它能够把问题分解为多个独立的步骤,这些独立的步骤可以以异步且 非阻塞 non-blocking 的方式被执行,最后再组合在一起产生一条 工作流 workflow ——它的输入和输出可能是 非绑定的 unbounded

“异步地” Asynchronous 被牛津词典定义为“不在同一时刻存在或发生”,在我们的语境下,它意味着一条消息或者一个事件可发生在任何时刻,也有可能是在未来。这在响应式编程中是非常重要的一项技术,因为响应式编程允许[ 非阻塞式 non-blocking ]的执行方式——执行线程在竞争一块共享资源时不会因为 阻塞 blocking 而陷入等待(为了防止执行线程在当前的工作完成之前执行任何其它操作),而是在共享资源被占用的期间转而去做其它工作。 阿姆达尔定律 Amdahl's Law 脚注2 告诉我们,竞争是 可伸缩性 scalability 最大的敌人,所以一个响应式系统应当在极少数的情况下才不得不做阻塞工作。

响应式编程一般是 事件驱动 event-driven ,相比之下,响应式系统则是 消息驱动 message-driven 的——事件驱动与消息驱动之间的差别会在文章后面阐明。

响应式编程库的应用程序接口(API)一般是以下二者之一:

  • 基于回调的 Callback-based —匿名的 间接作用 side-effecting 回调函数被绑定在 事件源 event sources 上,当事件被放入 数据流 dataflow chain 中时,回调函数被调用。
  • 声明式的 Declarative ——通过函数的组合,通常是使用一些固定的函数,像 mapfilterfold 等等。

大部分的库会混合这两种风格,一般还带有 基于流 stream-based 操作符 operators ,像 windowing、 counts、 triggers。

说响应式编程跟 数据流编程 dataflow programming 有关是很合理的,因为它强调的是数据流而不是控制流

举几个为这种编程技术提供支持的的编程抽象概念:

  • Futures/Promises——一个值的容器,具有 读共享/写独占 many-read/single-write 的语义,即使变量尚不可用也能够添加异步的值转换操作。
  • streams - 响应式流——无限制的数据处理流,支持异步,非阻塞式,支持多个源与目的的 反压转换管道 back-pressured transformation pipelines
  • 数据流变量#Dataflow_variables_and_declarative_concurrency)——依赖于输入、 过程 procedures 或者其它单元的 单赋值变量 single assignment variables (存储单元),它能够自动更新值的改变。其中一个应用例子是表格软件——一个单元的值的改变会像涟漪一样荡开,影响到所有依赖于它的函数,顺流而下地使它们产生新的值。

在 JVM 中,支持响应式编程的流行库有 Akka Streams、Ratpack、Reactor、RxJava 和 Vert.x 等等。这些库实现了响应式编程的规范,成为 JVM 上响应式编程库之间的 互通标准 standard for interoperability ,并且根据它自身的叙述是“……一个为如何处理非阻塞式反压异步流提供标准的倡议”。

响应式编程的基本好处是:提高多核和多 CPU 硬件的计算资源利用率;根据阿姆达尔定律以及引申的 Günther 的通用可伸缩性定律 Günther’s Universal Scalability Law 脚注3 ,通过减少 序列化点 serialization points 来提高性能。

另一个好处是开发者生产效率,传统的编程范式都尽力想提供一个简单直接的可持续的方法来处理异步非阻塞式计算和 I/O。在响应式编程中,因活动(active)组件之间通常不需要明确的协作,从而也就解决了其中大部分的挑战。

响应式编程真正的发光点在于组件的创建跟工作流的组合。为了在异步执行上取得最大的优势,把 反压 back-pressure 加进来是很重要,这样能避免过度使用,或者确切地说,避免无限度的消耗资源。

尽管如此,响应式编程在搭建现代软件上仍然非常有用,为了在更高层次上 理解 reason about 一个系统,那么必须要使用到另一个工具: 响应式架构 reactive architecture ——设计响应式系统的方法。此外,要记住编程范式有很多,而响应式编程仅仅只是其中一个,所以如同其它工具一样,响应式编程并不是万金油,它不意图适用于任何情况。

事件驱动 vs. 消息驱动

如上面提到的,响应式编程——专注于短时间的数据流链条上的计算——因此倾向于事件驱动,而响应式系统——关注于通过分布式系统的通信和协作所得到的弹性和韧性——则是消息驱动的 脚注4(或者称之为 消息式 messaging 的)。

一个拥有 长期存活的可寻址 long-lived addressable 组件的消息驱动系统跟一个事件驱动的数据流驱动模型的不同在于,消息具有固定的导向,而事件则没有。消息会有明确的(一个)去向,而事件则只是一段等着被 观察 observe 的信息。另外, 消息式 messaging 更适用于异步,因为消息的发送与接收和发送者和接收者是分离的。

响应式宣言中的术语表定义了两者之间概念上的不同

一条消息就是一则被送往一个明确目的地的数据。一个事件则是达到某个给定状态的组件发出的一个信号。在一个消息驱动系统中,可寻址到的接收者等待消息的到来然后响应它,否则保持休眠状态。在一个事件驱动系统中,通知的监听者被绑定到消息源上,这样当消息被发出时它就会被调用。这意味着一个事件驱动系统专注于可寻址的事件源而消息驱动系统专注于可寻址的接收者。

分布式系统需要通过消息在网络上传输进行交流,以实现其沟通基础,与之相反,事件的发出则是本地的。在底层通过发送包裹着事件的消息来搭建跨网络的事件驱动系统的做法很常见。这样能够维持在分布式环境下事件驱动编程模型的相对简易性,并且在某些特殊的和合理的范围内的使用案例上工作得很好。

然而,这是有利有弊的:在编程模型的抽象性和简易性上得一分,在控制上就减一分。消息强迫我们去拥抱分布式系统的真实性和一致性——像 局部错误 partial failures 错误侦测 failure detection 丢弃/复制/重排序 dropped/duplicated/reordered 消息,最后还有一致性,管理多个并发真实性等等——然后直面它们,去处理它们,而不是像过去无数次一样,藏在一个蹩脚的抽象面罩后——假装网络并不存在(例如EJB、 RPCCORBAXA)。

这些在语义学和适用性上的不同在应用设计中有着深刻的含义,包括分布式系统的 复杂性 complexity 中的 弹性 resilience 韧性 elasticity 移动性 mobility 位置透明性 location transparency 管理 management ,这些在文章后面再进行介绍。

在一个响应式系统中,特别是使用了响应式编程技术的,这样的系统中就即有事件也有消息——一个是用于沟通的强大工具(消息),而另一个则呈现现实(事件)。

响应式系统和架构

响应式系统 —— 如同在《响应式宣言》中定义的那样——是一组用于搭建现代系统——已充分准备好满足如今应用程序所面对的不断增长的需求的现代系统——的架构设计原则。

响应式系统的原则决对不是什么新东西,它可以被追溯到 70 和 80 年代 Jim Gray 和 Pat Helland 在 串级系统 Tandem System 上和 Joe aomstrong 和 Robert Virding 在 Erland 上做出的重大工作。然而,这些人在当时都超越了时代,只有到了最近 5 - 10 年,技术行业才被不得不反思当前企业系统最好的开发实践活动并且学习如何将来之不易的响应式原则应用到今天这个多核、云计算和物联网的世界中。

响应式系统的基石是 消息传递 message-passing ,消息传递为两个组件之间创建一条暂时的边界,使得它们能够在 时间 上分离——实现并发性——和 空间 space ——实现 分布式 distribution 移动性 mobility 。这种分离是两个组件完全 隔离 isolation 以及实现 弹性 resilience 韧性 elasticity 基础的必需条件。

从程序到系统

这个世界的连通性正在变得越来越高。我们不再构建 程序 ——为单个操作子来计算某些东西的端到端逻辑——而更多地在构建 系统 了。

系统从定义上来说是复杂的——每一部分都包含多个组件,每个组件的自身或其子组件也可以是一个系统——这意味着软件要正常工作已经越来越依赖于其它软件。

我们今天构建的系统会在多个计算机上操作,小型的或大型的,或少或多,相近的或远隔半个地球的。同时,由于人们的生活正变得越来越依赖于系统顺畅运行的有效性,用户的期望也变得越得越来越难以满足。

为了实现用户——和企业——能够依赖的系统,这些系统必须是 灵敏的 responsive ,这样无论是某个东西提供了一个正确的响应,还是当需要一个响应时响应无法使用,都不会有影响。为了达到这一点,我们必须保证在错误( 弹性 )和欠载( 韧性 )下,系统仍然能够保持灵敏性。为了实现这一点,我们把系统设计为 消息驱动的 ,我们称其为 响应式系统

响应式系统的弹性

弹性是与 错误下 灵敏性 responsiveness 有关的,它是系统内在的功能特性,是需要被设计的东西,而不是能够被动的加入系统中的东西。弹性是大于容错性的——弹性无关于 故障退化 graceful degradation ——虽然故障退化对于系统来说是很有用的一种特性——与弹性相关的是与从错误中完全恢复达到 自愈 的能力。这就需要组件的隔离以及组件对错误的包容,以免错误散播到其相邻组件中去——否则,通常会导致灾难性的连锁故障。

因此构建一个弹性的、 自愈 self-healing 系统的关键是允许错误被:容纳、具体化为消息,发送给其他(担当 监管者 supervisors )的组件,从而在错误组件之外修复出一个安全环境。在这,消息驱动是其促成因素:远离高度耦合的、脆弱的深层嵌套的同步调用链,大家长期要么学会忍受其煎熬或直接忽略。解决的想法是将调用链中的错误管理分离,将客户端从处理服务端错误的责任中解放出来。

响应式系统的韧性

韧性 Elasticity 是关于 欠载下的 灵敏性 responsiveness 的——意味着一个系统的吞吐量在资源增加或减少时能够自动地相应 增加或减少 scales up or down (同样能够 向内或外扩展 scales in or out )以满足不同的需求。这是利用云计算承诺的特性所必需的因素:使系统利用资源更加有效,成本效益更佳,对环境友好以及实现按次付费。

系统必须能够在不重写甚至不重新设置的情况下,适应性地——即无需介入自动伸缩——响应状态及行为,沟通负载均衡, 故障转移 failover ,以及升级。实现这些的就是 位置透明性 location transparency :使用同一个方法,同样的编程抽象,同样的语义,在所有向度中 伸缩 scaling 系统的能力——从 CPU 核心到数据中心。

如同《响应式宣言》所述:

一个极大地简化问题的关键洞见在于意识到我们都在使用分布式计算。无论我们的操作系统是运行在一个单一结点上(拥有多个独立的 CPU,并通过 QPI 链接进行交流),还是在一个 节点集群 cluster of nodes (独立的机器,通过网络进行交流)上。拥抱这个事实意味着在垂直方向上多核的伸缩与在水平方面上集群的伸缩并无概念上的差异。在空间上的解耦 [...],是通过异步消息传送以及运行时实例与其引用解耦从而实现的,这就是我们所说的位置透明性。

因此,不论接收者在哪里,我们都以同样的方式与它交流。唯一能够在语义上等同实现的方式是消息传送。

响应式系统的生产效率

既然大多数的系统生来即是复杂的,那么其中一个最重要的点即是保证一个系统架构在开发和维护组件时,最小程度地减低生产效率,同时将操作的 偶发复杂性 accidental complexity 降到最低。

这一点很重要,因为在一个系统的生命周期中——如果系统的设计不正确——系统的维护会变得越来越困难,理解、定位和解决问题所需要花费时间和精力会不断地上涨。

响应式系统是我们所知的最具 生产效率 的系统架构(在多核、云及移动架构的背景下):

  • 错误的隔离为组件与组件之间裹上舱壁(LCTT 译注:当船遭到损坏进水时,舱壁能够防止水从损坏的船舱流入其他船舱),防止引发连锁错误,从而限制住错误的波及范围以及严重性。
  • 监管者的层级制度提供了多个等级的防护,搭配以自我修复能力,避免了许多曾经在侦查(inverstigate)时引发的操作 代价 cost ——大量的 瞬时故障 transient failures
  • 消息传送和位置透明性允许组件被卸载下线、代替或 重新布线 rerouted 同时不影响终端用户的使用体验,并降低中断的代价、它们的相对紧迫性以及诊断和修正所需的资源。
  • 复制减少了数据丢失的风险,减轻了数据 检索 retrieval 和存储的有效性错误的影响。
  • 韧性允许在使用率波动时保存资源,允许在负载很低时,最小化操作开销,并且允许在负载增加时,最小化 运行中断 outgae 紧急投入 urgent investment 伸缩性的风险。

因此,响应式系统使 生成系统 creation systems 很好的应对错误、随时间变化的负载——同时还能保持低运营成本。

响应式编程与响应式系统的关联

响应式编程是一种管理 内部逻辑 internal logic 数据流转换 dataflow transformation 的好技术,在本地的组件中,做为一种优化代码清晰度、性能以及资源利用率的方法。响应式系统,是一组架构上的原则,旨在强调分布式信息交流并为我们提供一种处理分布式系统弹性与韧性的工具。

只使用响应式编程常遇到的一个问题,是一个事件驱动的基于回调的或声明式的程序中两个计算阶段的 高度耦合 tight coupling ,使得 弹性 难以实现,因此时它的转换链通常存活时间短,并且它的各个阶段——回调函数或 组合子 combinator ——是匿名的,也就是不可寻址的。

这意味着,它通常在内部处理成功与错误的状态而不会向外界发送相应的信号。这种寻址能力的缺失导致单个 阶段 stages 很难恢复,因为它通常并不清楚异常应该,甚至不清楚异常可以,发送到何处去。

另一个与响应式系统方法的不同之处在于单纯的响应式编程允许 时间 上的 解耦 decoupling ,但不允许 空间 上的(除非是如上面所述的,在底层通过网络传送消息来 分发 distribute 数据流)。正如叙述的,在时间上的解耦使 并发性 成为可能,但是是空间上的解耦使 分布 distribution 移动性 mobility (使得不仅仅静态拓扑可用,还包括了动态拓扑)成为可能的——而这些正是 韧性 所必需的要素。

位置透明性的缺失使得很难以韧性方式对一个基于适应性响应式编程技术的程序进行向外扩展,因为这样就需要分附加工具,例如 消息总线 message bus 数据网格 data grid 或者在顶层的 定制网络协议 bespoke network protocol 。而这点正是响应式系统的消息驱动编程的闪光的地方,因为它是一个包含了其编程模型和所有伸缩向度语义的交流抽象概念,因此降低了复杂性与认知超载。

对于基于回调的编程,常会被提及的一个问题是写这样的程序或许相对来说会比较简单,但最终会引发一些真正的后果。

例如,对于基于匿名回调的系统,当你想理解它们,维护它们或最重要的是在 生产供应中断 production outages 或错误行为发生时,你想知道到底发生了什么、发生在哪以及为什么发生,但此时它们只提供极少的内部信息。

为响应式系统设计的库与平台(例如 Akka 项目和 Erlang 平台)学到了这一点,它们依赖于那些更容易理解的长期存活的可寻址组件。当错误发生时,根据导致错误的消息可以找到唯一的组件。当可寻址的概念存在组件模型的核心中时, 监控方案 monitoring solution 就有了一个 有意义 的方式来呈现它收集的数据——利用 传播 propagated 的身份标识。

一个好的编程范式的选择,一个选择实现像可寻址能力和错误管理这些东西的范式,已经被证明在生产中是无价的,因它在设计中承认了现实并非一帆风顺,接受并拥抱错误的出现 而不是毫无希望地去尝试避免错误。

总而言之,响应式编程是一个非常有用的实现技术,可以用在响应式架构当中。但是记住这只能帮助管理一部分:异步且非阻塞执行下的数据流管理——通常只在单个结点或服务中。当有多个结点时,就需要开始认真地考虑像 数据一致性 data consistency 跨结点沟通 cross-node communication 协调 coordination 版本控制 versioning 编制 orchestration 错误管理 failure management 关注与责任 concerns and responsibilities 分离等等的东西——也即是:系统架构。

因此,要最大化响应式编程的价值,就把它作为构建响应式系统的工具来使用。构建一个响应式系统需要的不仅是在一个已存在的遗留下来的 软件栈 software stack 上抽象掉特定的操作系统资源和少量的异步 API 和 断路器 circuit breakers 。此时应该拥抱你在创建一个包含多个服务的分布式系统这一事实——这意味着所有东西都要共同合作,提供一致性与灵敏的体验,而不仅仅是如预期工作,但同时还要在发生错误和不可预料的负载下正常工作。

总结

企业和中间件供应商在目睹了应用响应式所带来的企业利润增长后,同样开始拥抱响应式。在本文中,我们把响应式系统做为企业最终目标进行描述——假设了多核、云和移动架构的背景——而响应式编程则从中担任重要工具的角色。

响应式编程在内部逻辑及数据流转换的组件层次上为开发者提高了生产率——通过性能与资源的有效利用实现。而响应式系统在构建 原生云 cloud native 和其它大型分布式系统的系统层次上为架构师及 DevOps 从业者提高了生产率——通过弹性与韧性。我们建议在响应式系统设计原则中结合响应式编程技术。

脚注

  1. 参考 Conal Elliott,FRP 的发明者,见这个演示
  2. Amdahl 定律揭示了系统理论上的加速会被一系列的子部件限制,这意味着系统在新的资源加入后会出现 收益递减 diminishing returns
  3. Neil Günter 的 通用可伸缩性定律 Universal Scalability Law 是理解并发与分布式系统的竞争与协作的重要工具,它揭示了当新资源加入到系统中时,保持一致性的开销会导致不好的结果。
  4. 消息可以是同步的(要求发送者和接受者同时存在),也可以是异步的(允许他们在时间上解耦)。其语义上的区别超出本文的讨论范围。

via: https://www.oreilly.com/ideas/reactive-programming-vs-reactive-systems

作者:Jonas BonérViktor Klang 译者:XLCYun 校对:wxy

本文由 LCTT 组织编译,Linux中国 荣誉推出

 title=

unikernel 实质上是一个缩减的操作系统,它可以与应用程序结合成为一个 unikernel 程序,它通常在虚拟机中运行。下载《开放云指南》了解更多。

当涉及到操作系统、容器技术和 unikernel,趋势是朝着微型化发展。什么是 unikernel?unikernel 实质上是一个缩减的操作系统(特指 “unikernel”),它可以与应用程序结合成为一个 unikernel 程序, 它通常在虚拟机中运行。它们有时被称为库操作系统,因为它包含了使应用程序能够将硬件和网络协议与一组访问控制和网络层隔离的策略相结合使用的库。

在讨论云计算和 Linux 时容器常常会被提及,而 unikernel 也在做一些变革。容器和 unikernel 都不是新事物。在 20 世纪 90 年代就有类似 unikernel 的系统,如 Exokernel,而如今流行的 unikernel 系统则有 MirageOS 和 OSv。 Unikernel 程序可以独立使用并在异构环境中部署。它们可以促进专业化和隔离化服务,并被广泛用于在微服务架构中开发应用程序。

作为 unikernel 如何引起关注的一个例子,你可以看看 Docker 收购了基于 Cambridge 的 Unikernel 系统,并且已在许多情况下在使用 unikernel。

unikernel,就像容器技术一样, 它剥离了非必需的的部分,因此它们对应用程序的稳定性、可用性以及安全性有非常积极的影响。在开源领域,它们也吸引了许多顶级,最具创造力的开发人员。

Linux 基金会最近宣布发布了其 2016 年度报告开放云指南:当前趋势和开源项目指南。这份第三年度的报告全面介绍了开放云计算的状况,并包含了一节关于 unikernel 的内容。你现在可以下载该报告。它汇总并分析研究、描述了容器、unikernel 的发展趋势,已经它们如何重塑云计算的。该报告提供了对当今开放云环境中心的各类项目的描述和链接。

在本系列文章中,我们将按类别分析指南中提到的项目,为整体类别的演变提供了额外的见解。下面, 你将看到几个重要 unikernel 项目的列表及其影响,以及它们的 GitHub 仓库的链接, 这些都是从开放云指南中收集到的:

ClickOS

ClickOS 是 NEC 的高性能虚拟化软件中间件平台,用于构建于 MiniOS/MirageOS 之上网络功能虚拟化(NFV)

Clive

Clive 是用 Go 编写的一个操作系统,旨在工作于分布式和云计算环境中。

HaLVM

Haskell 轻量级虚拟机(HaLVM)是 Glasgow Haskell 编译器工具包的移植,它使开发人员能够编写可以直接在 Xen 虚拟机管理程序上运行的高级轻量级虚拟机。

IncludeOS

IncludeOS 是在云中运行 C++ 服务的 unikernel 操作系统。它提供了一个引导加载程序、标准库以及运行服务的构建和部署系统。在 VirtualBox 或 QEMU 中进行测试,并在 OpenStack 上部署服务。

Ling

Ling 是一个用于构建超级可扩展云的 Erlang 平台,可直接运行在 Xen 虚拟机管理程序之上。它只运行三个外部库 (没有 OpenSSL),并且文件系统是只读的,以避免大多数攻击。

MirageOS

MirageOS 是在 Linux 基金会的 Xen 项目下孵化的库操作系统。它使用 OCaml 语言构建的 unikernel 可以用于各种云计算和移动平台上安全的高性能网络应用。代码可以在诸如 Linux 或 MacOS X 等普通的操作系统上开发,然后编译成在 Xen 虚拟机管理程序下运行的完全独立的专用 Unikernel。

OSv

OSv 是 Cloudius Systems 为云设计的开源操作系统。它支持用 Java、Ruby(通过 JRuby)、JavaScript(通过 Rhino 和 Nashorn)、Scala 等编写程序。它运行在 VMware、VirtualBox、KVM 和 Xen 虚拟机管理程序上。

Rumprun

Rumprun 是一个可用于生产环境的 unikernel,它使用 rump 内核提供的驱动程序,添加了 libc 和应用程序环境,并提供了一个工具链,用于将现有的 POSIX-y 程序构建为 Rumprun unikernel。它适用于 KVM 和 Xen 虚拟机管理程序和裸机,并支持用 C、C ++、Erlang、Go、Java、JavaScript(Node.js)、Python、Ruby、Rust 等编写的程序。

Runtime.js

Runtime.js 是用于在云上运行 JavaScript 的开源库操作系统(unikernel),它可以与应用程序捆绑在一起,并部署为轻量级和不可变的 VM 镜像。它基于 V8 JavaScript 引擎,并使用受 Node.js 启发的事件驱动和非阻塞 I/O 模型。KVM 是唯一支持的虚拟机管理程序。

UNIK

Unik 是 EMC 推出的工具,可以将应用程序源编译为 unikernel(轻量级可引导磁盘镜像)而不是二进制文件。它允许应用程序在各种云提供商、嵌入式设备(IoT) 以及开发人员的笔记本或工作站上安全地部署,资源占用很少。它支持多种 unikernel 类型、处理器架构、管理程序和编排工具,包括 Cloud Foundry、Docker 和 Kubernetes。Unik 的 GitHub

(题图:Pixabay)


via: https://www.linux.com/news/open-cloud-report/2016/guide-open-cloud-age-unikernel

作者:SAM DEAN 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

CoreOS,一款最新的 Linux 发行版本,支持自动升级内核软件,提供各集群间配置的完全控制。

关于使用哪个版本的 Linux 服务器系统的争论,常常是以这样的话题开始的:

你是喜欢基于 Red Hat Enterprise Linux (RHEL)CentOS 或者 Fedora,还是基于 DebianUbuntu,抑或 SUSE 呢?

但是现在,一款名叫 CoreOS 容器 Linux 的 Linux 发行版加入了这场“圣战”。这个最近在 Linode 服务器上提供的 CoreOS,和它的老前辈比起来,它使用了完全不同的实现方法。

你可能会感到不解,这里有这么多成熟的 Linux 发行版本,为什么要选择用 CoreOS ?借用 Linux 主干分支的维护者,也是 CoreOS 顾问的 Greg Kroah-Hartman 先生的一句话:

CoreOS 可以控制发行版的升级(基于 ChromeOS 代码),并结合了 Docker 和潜在的核对/修复功能,这意味着不用停止或者重启你的相关进程,就可以在线升级。测试版本已经支持此功能,这是史无前例的。

当 Greg Kroah-Hartman 做出这段评价时,CoreOS 还处于 α 测试阶段,当时也许就是在硅谷的一个车库当中,开发团队正在紧锣密鼓地开发此产品,但 CoreOS 不像最开始的苹果或者惠普,其在过去的四年当中一直稳步发展。

当我参加在旧金山举办的 2017 CoreOS 大会时,CoreOS 已经支持谷歌云、IBM、AWS 和微软的相关服务。现在有超过 1000 位开发人员参与到这个项目中,并为能够成为这个伟大产品的一员而感到高兴。

究其原因,CoreOS 从开始就是为容器而设计的轻量级 Linux 发行版,其起初是作为一个 Docker 平台,随着时间的推移, CoreOS 在容器方面走出了自己的道路,除了 Docker 之外,它也支持它自己的容器 rkt (读作 rocket )。

不像大多数其他的 Linux 发行版,CoreOS 没有包管理器,取而代之的是通过 Google ChromeOS 的页面自动进行软件升级,这样能提高在集群上运行的机器/容器的安全性和可靠性。不用通过系统管理员的干涉,操作系统升级组件和安全补丁可以定期推送到 CoreOS 容器。

你可以通过 CoreUpdate 和它的 Web 界面上来修改推送周期,这样你就可以控制你的机器何时更新,以及更新以多快的速度滚动分发到你的集群上。

CoreOS 通过一种叫做 etcd 的分布式配置服务来进行升级,etcd 是一种基于 YAML 的开源的分布式哈希存储系统,它可以为 Linux 集群容器提供配置共享和服务发现等功能。

此服务运行在集群上的每一台服务器上,当其中一台服务器需要下线升级时,它会发起领袖选举,以便服务器更新时整个Linux 系统和容器化的应用可以继续运行。

对于集群管理,CoreOS 之前采用的是 fleet 方法,这将 etcd 和 systemd 结合到分布式初始化系统中。虽然 fleet 仍然在使用,但 CoreOS 已经将 etcd 加入到 Kubernetes 容器编排系统构成了一个更加强有力的管理工具。

CoreOS 也可以让你定制其它的操作系统相关规范,比如用 cloud-config 的方式管理网络配置、用户账号和 systemd 单元等。

综上所述,CoreOS 可以不断地自行升级到最新版本,能让你获得从单独系统到集群等各种场景的完全控制。如 CoreOS 宣称的,你再也不用为了改变一个单独的配置而在每一台机器上运行 Chef 了。

假如说你想进一步的扩展你的 DevOps 控制,CoreOS 能够轻松地帮助你部署 Kubernetes

CoreOS 从一开始就是构建来易于部署、管理和运行容器的。当然,其它的 Linux 发行版,比如 RedHat 家族的原子项目也可以达到类似的效果,但是对于那些发行版而言是以附加组件的方式出现的,而 CoreOS 从它诞生的第一天就是为容器而设计的。

当前容器和 Docker 已经逐渐成为商业系统的主流,如果在可预见的未来中你要在工作中使用容器,你应该考虑下 CoreOS,不管你的系统是在裸机硬件上、虚拟机还是云上。

如果有任何关于 CoreOS 的观点或者问题,还请在评论栏中留言。如果你觉得这篇博客还算有用的话,还请分享一下~


关于博主:Steven J. Vaughan-Nichols 是一位经验丰富的 IT 记者,许多网站中都刊登有他的文章,包括 ZDNet.comPC MagazineInfoWorldComputerWorldLinux TodayeWEEK 等。他拥有丰富的 IT 知识 - 而且他曾参加过智力竞赛节目 Jeopardy !他的相关观点都是自身思考的结果,并不代表 Linode 公司,我们对他做出的贡献致以最真诚的感谢。如果想知道他更多的信息,可以关注他的 Twitter @sjvn


via: https://medium.com/linode-cube/the-what-why-and-wow-behind-the-coreos-container-linux-fa7ceae5593c

作者:Steven J. Vaughan-Nichols 译者:吴霄/toyijiu 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这篇帖子是有关 在 Ubuntu Core 开发 ROS 原型到成品 系列的补充,用来回答我收到的一个问题: “我想做一个工厂镜像,但我不想使我的 snap 公开” 当然,这个问题和回答都不只是针对于机器人技术。在这篇帖子中,我将会通过两种方法来回答这个问题。

开始之前,你需要了解一些制作 Ubuntu Core 镜像的背景知识,如果你已经看过 在 Ubuntu Core 开发 ROS 原型到成品[3 系列文章(具体是第 5 部分),你就已经有了需要的背景知识,如果没有看过的话,可以查看有关 制作你的 Ubuntu Core 镜像 的教程。

如果你已经了解了最新的情况,并且当我说 “模型定义” 或者 “模型断言” 时知道我在谈论什么,那就让我们开始通过不同的方法使用私有 sanps 来制作 Ubuntu Core 镜像吧。

方法 1: 不要上传你的 snap 到商店

这是最简单的方法了。首先看一下这个有关模型定义的例子——amd64-model.json

{
    "type": "model",
    "series": "16",
    "model": "custom-amd64",
    "architecture": "amd64",
    "gadget": "pc",
    "kernel": "pc-kernel",
    "authority-id": "4tSgWHfAL1vm9l8mSiutBDKnnSQBv0c8",
    "brand-id": "4tSgWHfAL1vm9l8mSiutBDKnnSQBv0c8",
    "timestamp": "2017-06-23T21:03:24+00:00",
    "required-snaps": ["kyrofa-test-snap"]
}

让我们将它转换成模型断言:

$ cat amd64-model.json | snap sign -k my-key-name > amd64.model
You need a passphrase to unlock the secret key for
user: "my-key-name"
4096-bit RSA key, ID 0B79B865, created 2016-01-01
...

获得模型断言:amd64.model 后,如果你现在就把它交给 ubuntu-image 使用,你将会碰钉子:

$ sudo ubuntu-image -c stable amd64.model 
Fetching core
Fetching pc-kernel
Fetching pc
Fetching kyrofa-test-snap
error: cannot find snap "kyrofa-test-snap": snap not found
COMMAND FAILED: snap prepare-image --channel=stable amd64.model /tmp/tmp6p453gk9/unpack

实际上商店中并没有名为 kyrofa-test-snap 的 snap。这里需要重点说明的是:模型定义(以及转换后的断言)只包含了一系列的 snap 的名字。如果你在本地有个那个名字的 snap,即使它没有存在于商店中,你也可以通过 --extra-snaps 选项告诉 ubuntu-image 在断言中匹配这个名字来使用它:

$ sudo ubuntu-image -c stable \
         --extra-snaps /path/to/kyrofa-test-snap_0.1_amd64.snap \
         amd64.model
Fetching core
Fetching pc-kernel
Fetching pc
Copying "/path/to/kyrofa-test-snap_0.1_amd64.snap" (kyrofa-test-snap)
kyrofa-test-snap already prepared, skipping
WARNING: "kyrofa-test-snap" were installed from local snaps
disconnected from a store and cannot be refreshed subsequently!
Partition size/offset need to be a multiple of sector size (512).
The size/offset will be rounded up to the nearest sector.

现在,在 snap 并没有上传到商店的情况下,你已经获得一个预装了私有 snap 的 Ubuntu Core 镜像(名为 pc.img)。但是这样做有一个很大的问题,ubuntu-image 会提示一个警告:不通过连接商店预装 snap 意味着你没有办法在烧录了这些镜像的设备上更新它。你只能通过制作新的镜像并重新烧录到设备的方式来更新它。

方法 2: 使用品牌商店

当你注册了一个商店账号并访问 dashboard.snapcraft.io 时,你其实是在标准的 Ubuntu 商店中查看你的 snap。如果你是在系统中新安装的 snapd,默认会从这个商店下载。虽然你可以在 Ubuntu 商店中发布私有的 snap,但是你不能将它们预装到镜像中,因为只有你(以及你添加的合作者)才有权限去使用它。在这种情况下制作镜像的唯一方式就是公开发布你的 snap,然而这并不符合这篇帖子的目的。

对于这种用例,我们有所谓的 品牌商店。品牌商店仍然托管在 Ubuntu 商店里,但是它们是针对于某一特定公司或设备的一个定制的、专门的版本。品牌商店可以继承或者不继承标准的 Ubuntu 商店,品牌商店也可以选择开放给所有的开发者或者将其限制在一个特定的组内(保持私有正是我们想要的)。

请注意,这是一个付费功能。你需要 申请一个品牌商店。请求通过后,你将可以通过访问用户名下的 “stores you can access” 看到你的新商店。

在那里你可以看到多个有权使用的商店。最少的情况下也会有两个:标准的 Ubuntu 商店以及你的新的品牌商店。选择品牌商店(红框),进去后记录下你的商店 ID(蓝框):等下你将会用到它。

在品牌商店里注册名字或者上传 snap 和标准的商店使用的方法是一样的,只是它们现在是上传到你的品牌商店而不是标准的那个。如果你将品牌商店放在 unlisted 里面,那么这些 snap 对外部用户是不可见。但是这里需要注意的是第一次上传 snap 的时候需要通过 web 界面来操作。在那之后,你可以继续像往常一样使用 Snapcraft 来操作。

那么这些是如何改变的呢?我的 “kyrofal-store” 从 Ubuntu 商店继承了 snap,并且还包含一个发布在稳定通道中的 “kyrofa-bran-test-snap” 。这个 snap 在 Ubuntu 商店里是使用不了的,如果你去搜索它,你是找不到的:

$ snap find kyrofa-branded
The search "kyrofa-branded" returned 0 snaps

但是使用我们前面记录的商店 ID,我们可以创建一个从品牌商店而不是 Ubuntu 商店下载 snap 的模型断言。我们只需要将 “store” 键添加到 JSON 文件中,就像这样:

{
    "type": "model",
    "series": "16",
    "model": "custom-amd64",
    "architecture": "amd64",
    "gadget": "pc",
    "kernel": "pc-kernel",
    "authority-id": "4tSgWHfAL1vm9l8mSiutBDKnnSQBv0c8",
    "brand-id": "4tSgWHfAL1vm9l8mSiutBDKnnSQBv0c8",
    "timestamp": "2017-06-23T21:03:24+00:00",
    "required-snaps": ["kyrofa-branded-test-snap"],
    "store": "ky<secret>ek"
}

使用方法 1 中的方式对它签名,然后我们就可以像这样很简单的制作一个预装有我们品牌商店私有 snap 的 Ubuntu Core 镜像:

$ sudo ubuntu-image -c stable amd64.model
Fetching core
Fetching pc-kernel
Fetching pc
Fetching kyrofa-branded-test-snap
Partition size/offset need to be a multiple of sector size (512).
The size/offset will be rounded up to the nearest sector.

现在,和方法 1 的最后一样,你获得了一个为工厂准备的 pc.img。并且使用这种方法制作的镜像中的所有 snap 都从商店下载的,这意味着它们将能像平常一样自动更新。

结论

到目前为止,做这个只有两种方法。当我开始写这篇帖子的时候,我想过可能还有第三种(将 snap 设置为私有然后使用它制作镜像),但最后证明是不行的

另外,我们也收到很多内部部署或者企业商店的请求,虽然这样的产品还没有公布,但是商店团队正在从事这项工作。一旦可用,我将会写一篇有关它的文章。

希望能帮助到您!


关于作者

Kyle 是 Snapcraft 团队的一员,也是 Canonical 公司的常驻机器人专家,他专注于 snaps 和 snap 开发实践,以及 snaps 和 Ubuntu Core 的机器人技术实现。


via: https://insights.ubuntu.com/2017/07/11/ubuntu-core-making-a-factory-image-with-private-snaps/

作者:Kyle Fazzari 译者:Snaplee 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Linux 中高效的备份拷贝命令

在 Linux 上能使用鼠标点来点去的图形化界面是一件很美妙的事……但是如果你喜欢的开发交互环境和编译器是终端窗口、Bash 和 Vim,那你应该像我一样经常和终端打交道。

即使是不经常使用终端的人,如果对终端环境深入了解也能获益良多。举个例子—— cp 命令,据 维基百科) 的解释,cp (意即 copy)命令是第一个版本的 Unix 系统的一部分。连同一组其它的命令 lsmvcdpwdmkdirvishsedawk ,还有提到的 cp 都是我在 1984 年接触 System V Unix 系统时所学习的命令之一。cp 命令最常见的用法是制作文件副本。像这样:

cp sourcefile destfile

在终端中执行此命令,上述命令将名为 sourcefile 的文件复制到名为 destfile 的文件中。如果在执行命令之前 destfile 文件不存在,那将会创建此文件,如果已经存在,那就会覆盖此文件。

这个命令我不知道自己用了多少次了(我也不想知道),但是我知道在我编写测试代码的时候,我经常用,为了保留当前正常的版本,而且又能继续修改,我会输入这个命令:

cp test1.py test1.bak

在过去的30多年里,我使用了无数次这个命令。另外,当我决定编写我的第二个版本的测试程序时,我会输入这个命令:

cp test1.py test2.py

这样就完成了修改程序的第一步。

我通常很少查看 cp 命令的参考文档,但是当我在备份我的图片文件夹的时候(在 GUI 环境下使用 “file” 应用),我开始思考“在 cp 命令中是否有个参数支持只复制新文件或者是修改过的文件。”果然,真的有!

高效用法 1:更新你的文件夹

比如说在我的电脑上有一个存放各种文件的文件夹,另外我要不时的往里面添加一些新文件,而且我会不时地修改一些文件,例如我手机里下载的照片或者是音乐。

假设我收集的这些文件对我而言都很有价值,我有时候会想做个拷贝,就像是“快照”一样将文件保存在其它媒体。当然目前有很多程序都支持备份,但是我想更为精确的将目录结构复制到可移动设备中,方便于我经常使用这些离线设备或者连接到其它电脑上。

cp 命令提供了一个易如反掌的方法。例子如下:

在我的 Pictures 文件夹下,我有这样一个文件夹名字为 Misc。为了方便说明,我把文件拷贝到 USB 存储设备上。让我们开始吧!

me@desktop:~/Pictures$ cp -r Misc /media/clh/4388-D5FE
me@desktop:~/Pictures$

上面的命令是我从按照终端窗口中完整复制下来的。对于有些人来说不是很适应这种环境,在我们输入命令或者执行命令之前,需要注意的是 me@mydesktop:~/Pictures 这个前缀,me 这个是当前用户,mydesktop 这是电脑名称,~/Pictures 这个是当前工作目录,是 /home/me/Pictures 完整路径的缩写。

我输入这个命令 cp -r Misc /media/clh/4388-D5FE 并执行后 ,拷贝 Misc 目录下所有文件(这个 -r 参数,全称 “recursive”,递归处理,意思为本目录下所有文件及子目录一起处理)到我的 USB 设备的挂载目录 /media/clh/4388-D5FE

执行命令后回到之前的提示,大多数命令继承了 Unix 的特性,在命令执行后,如果没有任何异常什么都不显示,在任务结束之前不会显示像 “execution succeeded” 这样的提示消息。如果想获取更多的反馈,就使用 -v 参数让执行结果更详细。

下图中是我的 USB 设备中刚刚拷贝过来的文件夹 Misc ,里面总共有 9 张图片。

 title=

假设我要在原始拷贝路径下 ~/Pictures/Misc 下添加一些新文件,就像这样:

 title=

现在我想只拷贝新的文件到我的存储设备上,我就使用 cp 的“更新”和“详细”选项。

me@desktop:~/Pictures$ cp -r -u -v Misc /media/clh/4388-D5FE
'Misc/asunder.png' -> '/media/clh/4388-D5FE/Misc/asunder.png'
'Misc/editing tags guayadeque.png' -> '/media/clh/4388-D5FE/Misc/editing tags guayadeque.png'
'Misc/misc on usb.png' -> '/media/clh/4388-D5FE/Misc/misc on usb.png'
me@desktop:~/Pictures$

上面的第一行中是 cp 命令和具体的参数(-r 是“递归”, -u 是“更新”,-v 是“详细”)。接下来的三行显示被复制文件的信息,最后一行显示命令行提示符。

通常来说,参数 -r 也可用更详细的风格 --recursive。但是以简短的方式,也可以这么连用 -ruv

高效用法 2:版本备份

回到一开始的例子中,我在开发的时候定期给我的代码版本进行备份。然后我找到了另一种更好用的 cp 参数。

假设我正在编写一个非常有用的 Python 程序,作为一个喜欢不断修改代码的开发者,我会在一开始编写一个程序简单版本,然后不停的往里面添加各种功能直到它能成功的运行起来。比方说我的第一个版本就是用 Python 程序打印出 “hello world”。这只有一行代码的程序就像这样:

print 'hello world'

然后我将这个代码保存成文件命名为 test1.py。我可以这么运行它:

me@desktop:~/Test$ python test1.py
hello world
me@desktop:~/Test$

现在程序可以运行了,我想在添加新的内容之前进行备份。我决定使用带编号的备份选项,如下:

clh@vancouver:~/Test$ cp --force --backup=numbered test1.py test1.py
clh@vancouver:~/Test$ ls
test1.py &nbsp;test1.py.~1~
clh@vancouver:~/Test$ 

所以,上面的做法是什么意思呢?

第一,这个 --backup=numbered 参数意思为“我要做个备份,而且是带编号的连续备份”。所以一个备份就是 1 号,第二个就是 2 号,等等。

第二,如果源文件和目标文件名字是一样的。通常我们使用 cp 命令去拷贝成自己,会得到这样的报错信息:

cp: 'test1.py' and 'test1.py' are the same file

在特殊情况下,如果我们想备份的源文件和目标文件名字相同,我们使用 --force 参数。

第三,我使用 ls (意即 “list”)命令来显示现在目录下的文件,名字为 test1.py 的是原始文件,名字为 test1.py.~1~ 的是备份文件

假如现在我要加上第二个功能,在程序里加上另一行代码,可以打印 “Kilroy was here.”。现在程序文件 test1.py 的内容如下:

print 'hello world'
print 'Kilroy was here'

看到 Python 编程多么简单了吗?不管怎样,如果我再次执行备份的步骤,结果如下:

clh@vancouver:~/Test$ cp --force --backup=numbered test1.py test1.py
clh@vancouver:~/Test$ ls
test1.py test1.py.~1~ test1.py.~2~
clh@vancouver:~/Test$

现在我有有两个备份文件: test1.py.~1~ 包含了一行代码的程序,和 test1.py.~2~ 包含两行代码的程序。

这个很好用的功能,我考虑做个 shell 函数让它变得更简单。

最后总结

第一,Linux 手册页,它在大多数桌面和服务器发行版都默认安装了,它提供了更为详细的使用方法和例子,对于 cp 命令,在终端中输入如下命令:

man cp

对于那些想学习如何使用这些命令,但不清楚如何使用的用户应该首先看一下这些说明,然后我建议创建一个测试目录和文件来尝试使用命令和选项。

第二,兴趣是最好的老师。在你最喜欢的搜索引擎中搜索 “linux shell tutorial”,你会获得很多有趣和有用的资源。

第三,你是不是在想,“为什么我要用这么麻烦的方法,图形化界面中有相同的功能,只用点击几下岂不是更简单?”,关于这个问题我有两个理由。首先,在我们工作中需要中断其他工作流程以及大量使用点击动作时,点击动作可就不简单了。其次,如果我们要完成流水线般的重复性工作,通过使用 shell 脚本和 shell 函数以及 shell 重命名等功能就能很轻松的实现。

你还知道关于 cp 命令其他更棒的使用方式吗?请在留言中积极回复哦~

(题图:stonemaiergames.com)


作者简介:

Chris Hermansen - 1978 年毕业于英国哥伦比亚大学后一直从事计算机相关职业,我从 2005 年开始一直使用 Linux、Solaris、SunOS,在那之前我就是 Unix 系统管理员了,在技术方面,我的大量的职业生涯都是在做数据分析,尤其是空间数据分析,我有大量的编程经验与数据分析经验,熟练使用 awk、Python、PostgreSQL、PostGIS 和 Groovy。


via: https://opensource.com/article/17/7/two-great-uses-cp-command

作者:Chris Hermansen 译者:bigdimple 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出