解谜计算机科学

要掌握一个学科的精髓,不能从细枝末节开始。人脑的能力很大程度上受限于信念。一个人不相信自己的时候,他就做不到本来可能的事。信心是很重要的,信心却容易被挫败。如果只见树木不见森林,人会失去信心,以为要到猴年马月才能掌握一个学科。

所以我们不从“树木”开始,而是引导读者一起来探索这背后的“森林”,把计算机科学最根本的概念用浅显的例子解释,让读者领会到它们的本质。把这些概念稍作发展,你就得到逐渐完整的把握。你一开头就掌握着整个学科,而且一直掌握着它,只不过增添更多细节而已。这就像画画,先勾勒出轮廓,一遍遍的增加细节,日臻完善,却不失去对大局的把握。

一般计算机专业的学生学了很多课程,可是直到毕业都没能回答一个基础问题:什么是计算?这一章会引导你去发现这个问题的答案。不要小看这基础的问题,它经常是解决现实问题的重要线索。世界上有太多不理解它的人,他们走了很多的弯路,掉进很多的坑,制造出过度复杂或者有漏洞的理论和技术。

接下来,我们就来理解几个关键的概念,由此接触到计算的本质。

手指算术

每个人都做过计算,只是大部分人都没有理解自己在做什么。回想一下幼儿园(大概四岁)的时候,妈妈问你:“帮我算一下,4+3 等于几?” 你掰了一会手指,回答:7。当你掰手指的时候,你自己就是一台简单的计算机。

不要小看了这手指算术,它蕴含着深刻的原理。计算机科学植根于这类非常简单的过程,而不是复杂的高等数学。

现在我们来回忆一下这个过程。这里应该有一段动画,但现阶段还没有。请你对每一步发挥一下想象力,增加点“画面感”。

  1. 当妈妈问你“4+3 等于几”的时候,她是一个程序员,你是一台计算机。计算机得到程序员的输入:4,+,3。
  2. 听到妈妈的问题之后,你拿出两只手,左手伸出四个指头,右手伸出三个指头。
  3. 接着你开始自己的计算过程。一根根地数那些竖起来的手指,每数一根你就把它弯下去,表示它已经被数过了。你念道:“1,2,3,4,5,6,7。”
  4. 现在已经没有手指伸着,所以你把最后数到的那个数作为答案:7!整个计算过程就结束了。

符号和模型

(这个概念太过深入,好像不适合出现在第一章,考虑去掉)

这里的幼儿园手指算术包含着深刻的哲学问题,现在我们来初步体会一下这个问题。

当妈妈说“帮我算 4+3”的时候,4,+,3,三个字符传到你耳朵里,它们都是符号(symbol)。符号是“表面”的东西:光是盯着“4”和“3”这两个阿拉伯数字的曲线,一个像旗子,一个像耳朵,你是不能做什么的。你需要先用脑子把它们转换成对应的“模型”(model)。这就是为什么你伸出两只手,一只手表示 4,另一只表示 3。

这两只手的手势是“可操作”的。比如,你把左手再多弯曲一个手指,它就变成“3”。你再伸开一根手指,它就变成“5”。所以手指是一个相当好的机械模型,它是可以动,可操作的。把符号“4”和“3”转换成手指模型之后,你就可以开始计算了。

你怎么知道“4”和“3”对应什么样的手指模型呢?因为妈妈以前教过你。十根手指,对应着 1 到 10 十个数。这就是为什么人都用十进制数做算术。

我们现在没必要深究这个问题。我只是提示你,分清“符号”和“模型”是重要的。

计算图

在计算机领域,我们经常用一些抽象的图示来表达计算的过程,这样就能直观地看到信息的流动和转换。这种图示看起来是一些形状用箭头连接起来。我在这里把它叫做“计算图”。

对于以上的手指算术 4 + 3,我们可以用下图来表示它:

图中的箭头表示信息的流动方向。说到“流动”,你可以想象一下水的流动。首先我们看到数字 4 和 3 流进了一个圆圈,圆圈里有一个“+”号。这个圆圈就是你,一个会做手指加法的小孩。妈妈给你两个数 4 和 3,你现在把它们加起来,得到 7 作为结果。

注意圆圈的输入和输出方向是由箭头决定的,我们可以根据需要调整那些箭头的位置,只要箭头的连接关系和方向不变就行。它们不一定都是从左到右,也可能从右到左或者从上到下,但“出入关系”都一样:4 和 3 进去,结果 7 出来。比如它还可以是这样:

我们用带加号的圆圈表示一个“加法器”。顾名思义,加法器可以帮我们完成加法。在上个例子里,你就是一个加法器。我们也可以用其他装置作为加法器,比如一堆石头,一个算盘,某种电子线路…… 只要它能做加法就行。

具体要怎么做加法,就像你具体如何掰手指,很多时候我们是不关心的,我们只需要知道这个东西能做加法就行。圆圈把具体的加法操作给“抽象化”了,这个蓝色的圆圈可以代表很多种东西。抽象(abstraction)是计算机科学至关重要的思维方法,它帮助我们进行高层面的思考,而不为细节所累。

表达式

计算机科学当然不止 4 + 3 这么简单,但它的基本元素确实是如此简单。我们可以创造出很复杂的系统,然而归根结底,它们只是在按某种顺序计算像 4 + 3 这样的东西。

4 + 3 是一个很简单的表达式(expression)。你也许没听说过“表达式”这个词,但我们先不去定义它。我们先来看一个稍微复杂一些的表达式:

2 * (4 + 3)

这个表达式比 4 + 3 多了一个运算,我们把它叫做“复合表达式”。这个表达式也可以用计算图来表示:

你知道它为什么是这个样子吗?它表示的意思是,先计算 4 + 3,然后把结果(7)传送到一个“乘法器”,跟 2 相乘,得到最后的结果。那正好就是 2 * (4 + 3) 这个表达式的含义,它的结果应该是 14。

为什么要先计算 4 + 3 呢?因为当我们看到乘法器 2 * ... 的时候,其中一个输入(2)是已知的,而另外一个输入必须通过加法器的输出得到。加法器的结果是由 4 和 3 相加得到的,所以我们必须先计算 4 + 3,然后才能与 2 相乘。

小学的时候,你也许学过:“括号内的内容要先计算”。其实括号只是“符号层”的东西,它并不存在于计算图里面。我这里讲的“计算图”,其实才是本质的东西。数学的括号一类的东西,都只是表象,它们是符号或者叫“语法”。从某种意义上讲,计算图才是表达式的本质或者“模型”,而“2 * (4 + 3)”这串符号,只是对计算图的一种表示或者“编码”(coding)。

这里我们再次体会到了“符号”和“模型”的差别。符号是对模型的“表示”或者“编码”。我们必须从符号得到模型,才能进行操作。这种从符号到模型的转换过程,在计算机科学里叫做“语法分析”(parsing)。我们会在后面的章节理解这个过程。

我们现在来给表达式做一个初步的定义。这并不是完整的定义,但你应该试着理解这种定义的方式。稍后我们会逐渐补充这个定义,逐渐完善。

定义(表达式):表达式可以是如下几种东西。

  1. 数字是一个表达式。比如 1,2,4,15,……
  2. 表达式 + 表达式。两个表达式相加,也是表达式。
  3. 表达式 – 表达式。两个表达式相减,也是表达式。
  4. 表达式 * 表达式。两个表达式相乘,也是表达式。
  5. 表达式 / 表达式。两个表达式相除,也是表达式。

注意,由于我们之前讲过的符号和模型的差别,为了完全忠于我们的本质认识,这里的“表达式 + 表达式”虽然看起来是一串符号,它必须被想象成它所对应的模型。当你看到“表达式”的时候,你的脑子里应该浮现出它对应的计算图,而不是一串符号。这个计算图的画面大概是这个样子,其中左边的大方框里可以是任意两个表达式。

是不是感觉这个定义有点奇怪?因为在“表达式”的定义里,我们用到了“表达式”自己。这种定义叫做“递归定义”。所谓递归(recursion),就是在一个东西的定义里引用这个东西自己。看上去很奇怪,好像绕回去了一样。递归是一个重要的概念,我们会在将来深入理解它。

现在我们可以来验证一下,根据我们的定义,2 * (4 + 3) 确实是一个表达式:

  • 首先根据第一种形式,我们知道 4 是表达式,因为它是一个数字。3 也是表达式,因为它是一个数字。
  • 所以 4 + 3 是表达式,因为 + 的左右都是表达式,它满足表达式定义的第二种形式。
  • 所以 2 * (4 + 3) 是表达式,因为 * 的左右都是表达式,它满足表达式定义的第四种形式。

并行计算

考虑这样一个表达式:

(4 + 3) * (1 + 2)

它对应一个什么样的计算图呢?大概是这样:

如果妈妈只有你一个小孩,你应该如何用手指算出它的结果呢?你大概有两种办法。

第一种办法:先算出 4+3,结果是 7。然后算出 1+2,结果是 3。然后算 7*3,结果是 21。

第二种办法:先算出 1+2,结果是 3。然后算出 4+3,结果是 7。然后算 7*3,结果是 21。

注意到没有,你要么先算 4+3,要么先算 1+2,你不能同时算 4+3 和 1+2。为什么呢?因为你只有两只手,所以算 4+3 的时候你就没法算 1+2,反之也是这样。总之,你妈妈只有你一个加法器,所以一次只能做一个加法。

现在假设你还有一个妹妹,她跟你差不多年纪,她也会手指算术。妈妈现在就多了一些办法来计算这个表达式。她可以这样做:让你算 4+3,不等你算完,马上让妹妹算 1+2。等到你们的结果(7 和 3)都出来之后,让你或者妹妹算 7*3。

发现没有,在某一段时间之内,你和妹妹同时在做加法计算。这种时间上重叠的计算,叫做并行计算(parallel computing)。

你和妹妹同时计算,得到结果的速度可能会比你一个人算更快。如果你妈妈还有其它几个孩子,计算复杂的式子就可能快很多,这就是并行计算潜在的好处。所谓“潜在”的意思是,这种好处不一定会实现。比如,如果你的妹妹做手指算数的速度比你慢很多,你做完了 4+3,只好等着她慢慢的算 1+2。这也许比你自己依次算 4+3 和 1+2 还要慢。

即使妹妹做算术跟你一样快,这里还有个问题。你和妹妹算出结果 7 和 3 之后,得把结果传递给下一个计算 7*3 的那个人(也许是你,也许是你妹妹)。这种“通信”会带来时间的延迟,叫做“通信开销”。如果你们其中一个说话慢,这比起一个人来做计算可能还要慢。

如何根据计算单元能力的不同和通信开销的差异,来最大化计算的效率,降低需要的时间,就成为了并行计算领域研究的内容。并行计算虽然看起来是一个“博大精深”的领域,可是你如果理解了我这里说的那点东西,就很容易理解其余的内容。

变量和赋值

如果你有一个复杂的表达式,比如

(5 - 3) * (4 + (2 * 3 - 5) * 6)

由于它有比较多的嵌套,人的眼睛是难以看清楚的,它要表达的意义也会难懂。这时候,你希望可以用一些“名字”来代表中间结果,这样表达式就更容易理解。

打个比方,这就像你有一个亲戚,他是你妈妈的表姐的女儿的丈夫。你不想每次都称他“我妈妈的表姐的女儿的丈夫”,所以你就用他的名字“叮当”来指代他,一下子就简单了。

我们来看一个例子。之前的复合表达式

2 * (4 + 3)

其实可以被转换为等价的,含有变量的代码:

{
    a = 4 + 3       // 变量 a 得到 4+3 的值
    2 * a           // 代码块的值
}

其中 a 是一个名字。a = 4 + 3 是一个“赋值语句”,它的意思是:用 a 来代表 4 + 3 的值。这种名字,计算机术语叫做变量(variable)。

这段代码的意思可以简单地描述为:计算 4 + 3,把它的结果表示为 a,然后计算 2 * a 作为最后的结果。

有些东西可能扰乱了你的视线。两根斜杠 // 后面一直到行末的文字叫做“注释”,是给人看的说明文字。它们对代码的逻辑不产生作用,执行的时候可以忽略。许多语言都有类似这种注释,它们可以帮助阅读的人,但是会被机器忽略。

这段代码执行过程会是这样:先计算 4 + 3 得到 7,用 a 记住这个中间结果 7。接着计算 2 * a ,也就是计算 2 * 7,所以最后结果是 14。很显然,这跟 2 * (4 + 3) 的结果是一样的。

a 叫做一个变量,它是一个符号,可以用来代表任意的值。除了 a,你还有许多的选择,比如 b, c, d, x, y, foo, bar, u21… 只要它不会被误解成其它东西就行。

如果你觉得这里面的“神奇”成分太多,那我们现在来做更深一层的理解……

再看一遍上面的代码。这整片代码叫做一个“代码块”(block),或者叫一个“序列”(sequence)。这个代码块包括两条语句,分别是 a = 4 + 3 和 2 * a。代码块里的语句会从上到下依次执行。所以我们先执行 a = 4 + 3,然后执行 2 * a

最后一条语句 2 * a 比较特别,它是这个代码块的“值”,也就是最后结果。之前的语句都是在为生成这个最后的值做准备。换句话说,这整个代码块的值就是 2 * a 的值。不光这个例子是这样,这是一个通用的原理:代码块的最后一条语句,总是这个代码块的值。

我们在代码块的前后加上花括号 {...} 进行标注,这样里面的语句就不会跟外面的代码混在一起。这两个花括号叫做“边界符”。我们今后会经常遇到代码块,它存在于几乎所有的程序语言里,只是语法稍有不同。比如有些语言可能用括号 (...) 或者 BEGIN...END来表示边界,而不是用花括号。

这片代码已经有点像常用的编程语言了,但我们暂时不把它具体化到某一种语言。我不想固化你的思维方式。在稍后的章节,我们会把这种抽象的表达法对应到几种常见的语言,这样一来你就能理解几乎所有的程序语言。

另外还有一点需要注意,同一个变量可以被多次赋值。它的值会随着赋值语句而改变。举个例子:

{
    a = 4 + 3
    b = a
    a = 2 * 5
    c = a
}

这段代码执行之后,b 的值是 7,而 c 的值是 10。你知道为什么吗?因为 a = 4 + 3 之后,a 的值是 7。b = a 使得 b 得到值 7。然后 a = 2 * 5 把 a 的值改变了,它现在是 10。所以 c = a 使得 c 得到 10。

对同一个变量多次赋值虽然是可以的,但通常来说这不是一种好的写法,它可能引起程序的混淆,应该尽量避免。只有当变量表示的“意义”相同的时候,你才应该对它重复赋值。

编译

一旦引入了变量,我们就可以不用复合表达式。因为你可以把任意复杂的复合表达式拆开成“单操作算术表达式”(像 4 + 3 这样的),使用一些变量记住中间结果,一步一步算下去,得到最后的结果。

举一个复杂点的例子,也就是这一节最开头的那个表达式:

(5 - 3) * (4 + (2 * 3 - 5) * 6)

它可以被转化为一串语句:

{
    a = 2 * 3
    b = a - 5
    c = b * 6
    d = 4 + c
    e = 5 - 3
    e * d
}

最后的表达式 e * d,算出来就是原来的表达式的值。你观察一下,是不是每个操作都非常简单,不包含嵌套的复合表达式?你可以自己验算一下,它确实算出跟原表达式一样的结果。

在这里,我们自己动手做了“编译器”(compiler)的工作。通常来说,编译器是一种程序,它的任务是把一片代码“翻译”成另外一种等价形式。这里我们没有写编译器,可是我们自己做了编译器的工作。我们手动地把一个嵌套的复合表达式,编译成了一系列的简单算术语句。

这些语句的结果与原来的表达式完全一致。这种保留原来语义的翻译过程,叫做编译(compile)。

我们为什么需要编译呢?原因有好几种。我不想在这里做完整的解释,但从这个例子我们可以看到,编译之后我们就不再需要复杂的嵌套表达式了。我们只需要设计很简单的,只会做单操作算术的机器,就可以算出复杂的嵌套的表达式。实际上最后这段代码已经非常接近现代处理器(CPU)的汇编代码(assembly)。我们只需要多加一些转换,它就可以变成机器指令。

我们暂时不写编译器,因为你还缺少一些必要的知识。这当然也不是编译技术的所有内容,它还包含另外一些东西。但从这一开头,你就已经初步理解了编译器是什么,你只需要在将来加深这种理解。

函数

到目前为止,我们做的计算都是在已知的数字之上,而在现实的计算中我们往往有一些未知数。比如我们想要表达一个“风扇控制器”,有了它之后,风扇的转速总是当前气温的两倍。这个“当前气温”就是一个未知数。

我们的“风扇控制器”必须要有一个“输入”(input),用于得到当前的温度 t,它是一个温度传感器的读数。它还要有一个输出,就是温度的两倍。

那么我们可以用这样的方式来表达我们的风扇控制器:

t -> t*2

不要把这想成任何一种程序语言,这只是我们自己的表达法。箭头 -> 的左边表示输入,右边表示输出,够简单吧。

你可以把 t 想象成从温度传感器出来的一根电线,它连接到风扇控制器上,风扇控制器会把它的输入(t)乘以 2。这个画面像这个样子:

我们谈论风扇控制器的时候,其实不关心它的输入是哪里来的,输出到哪里去。如果我们把温度传感器和风扇从画面里拿掉,就变成这个样子:

这幅图才是你需要认真理解的函数的计算图。你发现了吗,这幅图画正好对应了之前的风扇控制器的符号表示:t -> t*2。看到符号就想象出画面,你就得到了符号背后的模型。

像 t -> t*2 这样具有未知数作为输入的构造,我们把它叫做函数(function)。其中 t 这个符号,叫做这个函数的参数。

参数,变量和电线

你可能发现了,函数的参数和我们之前了解的“变量”是很类似的,它们都是一个符号。之前我们用了 a, b, c, d, e 现在我们有一个 t,这些名字我们都是随便起的,只要它们不要重复就好。如果名字重复的话,可能会带来混淆和干扰。

其实参数和变量这两种概念不只是相似,它们的本质就是一样的。如果你深刻理解它们的相同本质,你的脑子就可以少记忆很多东西,而且它可能帮助你对代码做出一些有趣而有益的转化。在上一节你已经看到,我用“电线”作为比方来帮助你理解参数。你也可以用同样的方法来理解变量。

比如我们之前的变量 a

{
    a = 4 + 3
    2 * a
}

它可以被想象成什么样的画面呢?

我故意把箭头方向画成从右往左,这样它就更像上面的代码。从这个图画里,你也许可以看到变量 a 和风扇控制器图里的参数 t,其实没有任何本质差别。它们都表示一根电线,那根电线进入乘法器,将会被乘以 2,然后输出。如果你把这些都看成是电路,那么变量 a 和参数 t 都代表一根电线而已。

然后你还发现一个现象,那就是你可以把 a 这个名字换成任何其它名字(比如 b),而这幅图不会产生实质的改变。

这说明什么问题呢?这说明以下的代码(把 a 换成了 b)跟之前的是等价的:

{
    b = 4 + 3
    2 * b
}

根据几乎一样的电线命名变化,你也可以对之前的函数得到一样的结论:t -> t*2 和 u -> u*2,和 x -> x*2 都是一回事。

名字是很重要的东西,但它们具体叫什么,对于机器并没有实质的意义,只要它们不要相互混淆就可以。但名字对于人是很重要的,因为人脑没有机器那么精确。不好的变量和参数名会导致代码难以理解,引起程序员的混乱和错误。所以通常说来,你需要给变量和参数起好的名字。

什么样的名字好呢?我会在后面集中讲解。

有名字的函数

既然变量可以代表“值”,那么一个自然的想法,就是让变量代表函数。所以就像我们可以写

a = 4 + 3

我们似乎也应该可以写

f = t -> t*2

对的,你可以这么做。f = t->t*2 还有一个更加传统的写法,就像数学里的函数写法:

f(t) = t*2

请仔细观察 t 的位置变化。我们在函数名字的右边写一对括号,在里面放上参数的名字。

注意,你不可以只写

f = t*2

你必须明确的指出函数的参数是什么,否则你就不会明白函数定义里的 t 是什么东西。明确指出 t 是一个“输入”,你才会知道它是函数的输入,是一个未知数,而不是在函数外面定义的其它变量

这个看似简单的道理,很多数学家都不明白,所以他们经常这样写书:

有一个函数 y = x*2

这是错误的,因为他没有明确指出“x 是函数 y 的参数”。如果这句话之前他们又定义过 x,你就会疑惑这是不是之前那个 x。很多人就是因为这些糊里糊涂的写法而看不懂数学书。这不怪他们,只怪数学家自己对于语言不严谨。

函数调用

有了函数,我们可以给它起名字,可是我们怎么使用它的值呢?

由于函数里面有未知数(参数),所以你必须告诉它这些未知数,它里面的代码才会执行,给你结果。比如之前的风扇控制器函数

f(t) = t*2

它需要一个温度作为输入,才会给你一个输出。于是你就这样给它一个输入:

f(2)

你把输入写在函数名字后面的括号里。那么你就会得到输出:4。也就是说 f(2) 的值是 4。

如果你没有调用一个函数,函数体是不会被执行的。因为它不知道未知数是什么,所以什么事也做不了。那么我们定义函数的时候,比如

f(t) = t*2

当看到这个定义的时候,机器应该做什么呢?它只是记录下:有这么一个函数,它的参数是 t,它需要计算 t*2,它的名字叫 f。但是机器不会立即计算 t*2,因为它不知道 t 是多少。

分支

直到现在,我们的代码都是从头到尾,闷头闷脑地执行,不问任何问题。我们缺少一种“问问题”的方法。比如,如果我想表达这样一个“食物选择器”:如果气温低于 22 度,就返回 “hotpot” 表示今天吃火锅,否则返回 “ice cream” 表示今天吃冰激凌。

我们可以把它图示如下:

中间这种判断结构叫做“分支”(branching),它一般用菱形表示。为什么叫分支呢?你想象一下,代码就像一条小溪,平时它沿着一条路线流淌。当它遇到一个棱角分明的大石头,就分成两个支流,分开流淌。

我们的判断条件 t < 22 就像一块大石头,我们的“代码流”碰到它就会分开成两支,分别做不同的事情。跟溪流不同的是,这种分支不是随机的,而是根据条件来决定,而且分支之后只有一支继续执行,而另外一边不会被执行。

我们现在看到的都是图形化表示的模型,为了书写方便,现在我们要从符号的层面来表示这个模型。我们需要一种符号表示法来表达分支,我们把它叫做 if(如果)。我们的饮料选择器代码可以这样写:

t -> if (t < 22) 
     {
       "hotpot"
     }
     else 
     {
       "ice cream"
     }

它是一个函数,输入是一个温度。if 后面的括号里放我们的判断条件。后面接着条件成立时执行的代码块,然后是一个 else,然后是条件不成立时执行的代码。它说:如果温度低于 22 度,我们就吃火锅,否则就吃冰激凌。

其中的 else 是一个特殊的符号,它表示“否则”。看起来不知道为什么 else 要在那里?对的,它只是一个装饰品。我们已经有足够的表达力来分辨两个分支,不过有了 else 似乎更加好看一些。很多语言里面都有 else 这个标记词在那里,所以我也把它放在那里。

这只是一个最简单的例子,其实那两个代码块里面不止可以写一条语句。你可以有任意多的语句,就像这样:

t ->
if (t < 22)
{
    a = 4 + 3
    b = a * 2
    "hotpot"
}
else
{
    x = "ice cream"
    x
}

这段代码和之前是等价的,你知道为什么吗?

字符串

上面一节出现了一种我们之前没见过的东西,我为了简洁而没有介绍它。这两个分支的结果,也就是加上引号的 “hotpot” 和 “ice cream”,它们并不是数字,也不是其它语言构造,而是一种跟数字处于几乎同等地位的“数据类型”,叫做字符串(string)。字符串是我们在计算机里面表示人类语言的基本数据类型。

关于字符串,在这里我不想讲述更加细节的内容,我把对它的各种操作留到以后再讲,因为虽然字符串对于应用程序很重要,它却并不是计算机科学最关键最本质的内容。

很多计算机书籍一开头就讲很多对字符串的操作,导致初学者费很大功夫去做很多打印字符串的练习,结果几个星期之后还没学到“函数”之类最根本的概念。这是非常可惜的。

布尔值

我们之前的 if 语句的条件 t < 22 其实也是一个表达式,它叫做“布尔表达式”。你可以把小于号 < 看成是跟加法一类的“操作符”。它的输入是两个数值,输出是一个“布尔值”。什么是布尔值呢?布尔值只有两个:true 和 false,也就是“真”和“假”。

举个例子,如果 t 的值是 15,那么 t < 22 是成立的,那么它的值就是 true。如果 t 的值是 23,那么 t < 22 就不成立,那么它的值就是 false。是不是很好理解呢?

我们为什么需要“布尔值”这种东西呢?因为它的存在可以简化我们的思维。对于布尔值也有一些操作,这个我也不在这一章赘述,放到以后细讲。

计算的要素

好了,现在你已经掌握了计算机科学的几乎所有基本要素。每一个编程语言都包括这些构造:

  1. 基础的数值。比如整数,字符串,布尔值等。
  2. 表达式。包括基本的算术表达式,嵌套的表达式。
  3. 变量和赋值语句。
  4. 分支语句。
  5. 函数和函数调用。

你也许可以感觉到,我是把这些构造按照“从小到大”的顺序排列的。这也许可以帮助你的理解。

现在你可以回想一下你对它们的印象。每当学习一种新的语言或者系统,你只需要在里面找到对应的构造,而不需要从头学习。这就是掌握所有程序语言的秘诀。这就像学开车一样,一旦你掌握了油门,刹车,换挡器,方向盘,速度表的功能和用法,你就学会了开所有的汽车,不管它是什么型号的汽车。

我们在这一章不仅理解了这些要素,而且为它们定义了一种我们自己的“语言”。显然这个语言只能在我们的头脑里运行,因为我们没有实现这个语言的系统。在后面的章节,我会逐渐的把我们这种语言映射到现有的多种语言里面,然后你就能掌握这些语言了。

但是请不要以为掌握了语言就学会了编程或者学会了计算机科学。掌握语言就像学会了各种汽车部件的工作原理。几分钟之内,初学者就能让车子移动,转弯,停止。可是完了之后你还需要学习交通规则,你需要许许多多的实战练习和经验,掌握各种复杂情况下的策略,才能成为一个合格的驾驶员。如果你想成为赛车手,那就还需要很多倍的努力。

但是请不要被我这些话吓到了,你没有那么多的竞争者。现在的情况是,世界上就没有很多合格的计算机科学驾驶员,更不要说把车开得流畅的赛车手。绝大部分的“程序员”连最基本的引擎,油门,刹车,方向盘的工作原理都不明白,思维方式就不对,所以根本没法独自上路,一上路就出车祸。很多人把过错归结在自己的车身上,以为换一辆车马上就能成为好的驾驶员。这是一种世界范围的计算机教育的失败。

在后面的章节,我会引导你成为一个合格的驾驶员,随便拿一辆车就能开好。

什么是计算

现在你掌握了计算所需要的基本元素,可是什么是计算呢?我好像仍然没有告诉你。这是一个很哲学的问题,不同的人可能会告诉你不同的结果。我试图从最广义的角度来告诉你这个问题的答案。

当你小时候用手指算 4+3,那是计算。如果后来你学会了打算盘,你用算盘算 4+3,那也是计算。后来你从我这里学到了表达式,变量,函数,调用,分支语句…… 在每一新的构造加入的过程中,你都在了解不同的计算。

所以从最广义来讲,计算就是“机械化的信息处理”。所谓机械化,你可以用手指算,可以用算盘,可以用计算器,或者计算机。这些机器里面可以有代码,也可以没有代码,全是电子线路,甚至可以是生物活动或者化学反应。不同的机器也可以有不同的计算功能,不同的速度和性能……

有这么多种计算的事实不免让人困惑,总害怕少了点什么,其实你可以安心。如果你掌握了上一节的“计算要素”,那么你就掌握了几乎所有类型的计算系统所需要的东西。你在后面所需要做的只是加深这种理解,并且把它“对应”到现实世界遇到的各种计算机器里面。

为什么你可以相信计算机科学的精华就只有这些呢?因为计算就是处理信息,信息有它诞生的位置(输入设备,固定数值),它传输的方式(赋值,函数调用,返回值),它被查看的地方(分支)。你想不出对于信息还有什么其它的操作,所以你就很安心的相信了,这就是计算机科学这种“棋类游戏”的全部规则。

from:http://www.yinwang.org/blog-cn/2018/04/13/computer-science

中国芯片差在哪?这篇讲全了

在半导体这个领域,中国需要挑战的是,西方上百年积累起来的工业体系。

中国半导体一直是在冒着敌人的炮火匍匐前进,如今,敌人的炮火越来越凶猛。围追堵截中,谁让我“芯”痛?

美国的惊人统治力

1957 年,晶体管之父肖克利的八个门徒,在硅谷创立仙童半导体公司,并开发出人类历史上第一块集成电路,硅谷因此成为全世界半导体技术的发源地,一直延续至今。

期间,尽管发生过几次产业转移,七八十年代,半导体制造大量转移至日本;90 年代后,转移至韩国和中国台湾。但美国至今依旧保留着在诸多核心领域的统治力。

以生产设备为例,全球三大巨头应用材料、泛林和 ASML,美国独占前两席,而且应用材料在除光刻机以外的几乎所有领域都领先,包括蚀刻、薄膜沉积等。

更恐怖的是,全球三大 EDA 软件(用于芯片设计)巨头铿腾、明导和新思,均为美国企业,全世界几乎所有芯片设计和制造企业都离不开它们。

高端芯片方面,中兴事件暴露出来的众多短板,包括 ADC/DAC(数模转换)、FPGA、高速光通信接口等芯片,目前也都依赖美国厂商,包括德州仪器、赛灵思、亚德诺等。

美国的惊人统治力还体现在生态系统上。

目前,三种主流的芯片架构 X86、MIPS 和 ARM,前两种都是美国血统。其中,英特尔的 X86 架构,与微软的 Windows 系统结盟,称霸台式机市场。ARM 架构虽然是英国血统,却离不开安卓和 iOS 系统的支持,两者合计占有全球 95% 以上的手机市场。

而且,ARM 其实诞生于苹果的一款失败产品。

如今,在全球 20 大半导体公司中,美国依旧独占八席,处于绝对的霸主地位,并且基本都是卡住核心的关键性公司。

中国 VS 整个产业链

半导体是一个庞大的产业,从大类上讲,包括集成电路(IC)、光电子、分离器和传感器等,其中 IC 的规模占 80% 以上。

所谓芯片,就是内含集成电路的硅片,它分为几十个大类,上千个小类。制造一块小小的芯片,涉及 50 多个学科、数千道工序,包括设计、制造和封装三大环节。

在这个产业链上,国内企业的差距是全方位的。

首先看设计,华为海思和紫光展锐分列国内前两名。目前,两家公司在不少领域已是世界领先水平,但一个巨大的问题是,其架构授权的核心都被外人掌握。

目前,国内仅有中科院的龙芯和总参谋部的申威拥有自主架构,前者用于北斗导航,后者用于神威超级计算机,民用领域基本是空白。

设备和材料是又一大短板。制造芯片的三大设备光刻机、蚀刻机和薄膜沉积,国内仅中微半导体的介质蚀刻机能跟上行业节奏,其 7 纳米设备已入围台积电名单。

此外,北方华创在氧化炉和薄膜沉积设备上成绩不俗,但基本还处于 28 纳米级别。其他设备,如离子注入机、抛光机和清洗机,也差不多。

差距最大的是光刻机。光刻机用于将设计好的电路图曝光在硅片上,蚀刻机则负责微观雕刻,刻出沟槽或接触孔。目前 ASML 最先进的 EUV 光刻机,即将投入三星、台积电的 7 纳米工艺,而国内上海微电子的光刻机,仍停留在 90 纳米量产的水平。

材料方面,日本是全球领先者。

在制造芯片的 19 种主要材料中,日本有 14 种位居全球第一,总份额超过 60%。全球近七成的硅晶圆产自日本,那是芯片制造的根基。

反观中国,硅晶圆几乎是空白,8 英寸国产率不足 10%,12 英寸依赖进口,打破垄断的希望还在张汝京创办的新昇半导体,今年即将量产。他也是中芯国际的创始人。

除了硅晶圆,国内企业还在溅射靶材、研磨液等材料上有所突破,并实现了国产化。前者用于制作金属导线,后者用于芯片研磨抛光。

以上均为单点突破,距离整个行业的崛起还比较远。

芯片制造,国内最先进的是中芯国际和厦门联芯,目前能做到 28 纳米量产。而它们的竞争对手,三星、台积电等巨头即将在今年量产 7 纳米,相差两三代。

最后是封测。这是目前大陆最接近国际水平的领域,长电科技收购新加坡星科金朋后,跻身全球第三。但全球封测中心在中国台湾,以日月光为首的台湾企业,拥有 50% 以上的市场份额。

在这样一个超长的产业链中,全球通力合作必不可少。以光刻机为例,荷兰 ASML 一骑绝尘,但它的成功得益于各国的鼎力合作,镜头来自德国蔡司、光源来自美国,这几乎是西方近百年工业的技术结晶。

但中国在这个产业链上处于不利地位,经常面对不友好的产业环境。这次的中兴事件只不过是浮上台面的斗争与封锁,而台面下的争斗几十年来则是一直没有消停。

巨头封锁与追杀

芯片制造是人类历史上最复杂的工艺,加工精度为头发丝的几千分之一,需要上千个步骤才能完成。其难度,堪比两弹一星。

如此复杂的工艺,需要巨额的投资。例如,建一个芯片工厂,就动辄需要上百亿美元。这样的投资规模,只有跨国巨头乃至国家才能完成。

这让巨头企业在产业中更具优势,也直接导致了行业的不断集中。

过去 40 年,半导体行业呈现加速垄断趋势。1995 年,全球七大半导体企业投资占比 24%,如今这个数字已飙升至 80% 以上。40 年前,全球有几十家主要的设备制造商,如今只剩下三四家。

不仅如此,巨头们不但自身可以使出很多种手段惩戒后来者,甚至还组建产业联盟扼杀后来者。

手段之一是低价倾销。这里面都是套路:一开始你没有,它通过垄断积累暴利;等你做出来,它马上降价倾销,让你越做越亏,暗无天日,最终断了产业化的念想。

当年液晶大战的惨痛就这样。三星、夏普等液晶巨头,一开始不愿在华建厂,而等深圳市政府组织国内彩电巨头,以及京东方要展开反攻时,他们却又主动跳出来求合作。结果导致长虹动摇并撤出,京东方则被晾在一边,国产计划泡汤。

享受这一“待遇”的,还有 MOCVD 设备商。MOCVD 是制造 LED 芯片的设备,在国产化之前,美、德两家巨头凭借垄断,每台设备卖 2000 万。而等国内厂商开始介入时,售价立刻暴跌至 600 万。结果,数十个国内玩家,如今只剩下中微、中晟光电等少数几家。

去年,高通在华推出重磅计划,与大唐电信旗下的联芯科技成立领盛科技,联手进军中低端芯片市场。关心国内芯片产业的很多人士都一致认为,这是高通要“借刀杀人”,要以领盛科技用价格战的方式绞杀正在向中高端芯片市场进军的紫光展讯。而 5 月 4 日的最新消息显示,这家合资企业已正式获批。

手段之二是发动专利战,拖住对手,打击下游客户信心。
手段之二是发动专利战,拖住对手,打击下游客户信心。

2000 年,张汝京出走台湾,在上海创建中芯国际。之后,他从台积电四处挖人,使得中芯短时间内迅速崛起。但厄运随之而来,老冤家张忠谋很快就发起了专利战。这场持续近七年的战争,最终的结果是中芯割地赔款,张汝京黯然出局。

台积电通过此举,成功拖住了中芯。而当初引进张汝京的江上舟,也在两年后辞世,死前一直惦记着中芯的未来。

同样的一幕发生在中微身上。2007 年,尹志尧领衔的中微刚推出自己的蚀刻机,就被老东家美国应用材料告上法庭。紧接着,另一巨头泛林火上浇油。好在中微从一开始就小心规避专利陷阱,最终赢得了诉讼。

但结果却不甚理想,中微不但赔上巨额的诉讼费,还赔上了下游客户的信心。再加上适逢经济危机,不得不暂时砍掉 MOCVD 业务。

很不友好的环境

事实上,中国很早就重视了半导体的大战。最早可追溯至上世纪 60 年代。但在后来的发展中,由于自身路径,国内产研环境,以及不友好的产业环境等多种原因,逐渐掉队。

中国半导体一直是在冒着敌人的炮火匍匐地前进。而今,敌人的炮火更是越来越凶猛,越来越密集。

西方一直有一个针对出口管制的制度安排,最早是 1949 年成立的巴黎统筹委员会,之后在 1996 年演变为瓦森纳协定。该协定包含军用、民用两份控制清单,目的是限制向相关国家出口敏感产品和技术,中国就属于被限制的对象。

过去几十年,国家一直在努力突破这种封锁。90 年代,先后批复 908/909 工程,时任领导人表态:砸锅卖铁也要把半导体搞上去。国务院则用财政赤字拨款。

然而,作为两项工程的产物,华晶、华虹等企业到国际上采购设备却遭到抵制,最终发展受限,一直未有大的突破。

2006 年以后,国家又搞了 01 和 02 专项。前者剑指核心电子器件、高端通用芯片和基础软件,俗称核高基;后者剑指 IC 制造和成套工艺。

近年来,两个专项相继开花结果,例如中微的蚀刻机已看齐世界水平,中芯国际的工艺已挺进到 28 纳米,但受限于不友好的产业环境,水平还是差强人意。

以光刻机为例,ASML 的 EUV 光刻机即将投入 7 纳米工艺,而国内最先进的量产水平是 90 纳米。之所以差距惊人,原因之一是买不到高水平的镜头和光源,这是光刻机的核心部件,而国内缺乏相关的技术。

坊间一直传说,瓦森纳协定禁止向中国出口高端光刻机。这种说法后来遭到 ASML 公司的否认,该公司声称,最快将于 2019 年在中国晶圆厂见到 EUV 光刻机。

但业内人士透露,在瓦森纳协定中,确实是有光刻机限售条款的,只不过每隔几年,条款就会作相应的调整。之所以调的原因也是,国产水平在不断提高,所谓的调整,顶多也就是“敌人”不断地根据我方的进展调整炮火的轻重与射程。

一个有意思的细节是,国内研究机构在相关技术上取得突破不久,ASML 公司就否认了禁售的传闻。

产业环境并不是唯一的障碍,来自国家层面的干预更加要命。

美国政府曾多次否决中国企业针对美企的收购行为,包括著名的紫光并购美光计划。最近更是制裁了中兴通讯,断供其芯片。

2016 年,国内某基金收购德国爱思强时,连 FBI 都跳出来施压,最终迫使德方放弃了交易。而据中兴员工透露,在中兴断芯事件前,FBI 就入驻了公司内部。

为钱一把辛酸泪

半导体是一个烧钱的行业。上世纪 90 年代,中央在财政非常拮据的情况下,特批了 40 亿元搞半导体。但这点钱只是杯水车薪。

向民间要投资,更遭到冷遇。

这个行业不但烧钱,而且周期长,技术更新快,你刚研发出来,别人已经开始打价格战。这意味着,前期要不断砸钱,还见不到水花。对民营资本而言,这是无法承受之痛。

中微董事长尹志尧 2004 年满腔热血回国创业,就遇到了这个难题。

为造蚀刻机,中微在短时间内,烧光了地方政府和自筹资金,只好四处筹钱续命。当时民间资本对这个行业缺乏了解,也缺乏意愿,尹的一腔热血只能碰一鼻子灰。

万般无奈之下,只好赴硅谷融资。两周内,十几家风投踏破门槛,愿意提供 5000 万美元。此情此景,令报国心切的尹志尧百感交集:难道只有美国造得出蚀刻机?

苦心积虑,只为国产化,绝不能大权旁落。在拿回几笔续命钱后,尹志尧继续寻找国内投资人。后来,在江上舟的引荐下,终于有国开行为中微背书。

跟中微有类似遭遇的,还有京东方。

做液晶面板 20 年,京东方一直伴随各种非议。尤其是它越亏越投的做法,更是被人质疑为绑架政府,因为其大部分资金来自银行和地方政府。

在资本市场上,由于不断增发,京东方背上了圈钱的骂名。但这些钱,大部分来自国资背景。其间,京东方曾多次拉私募基金入股,均被拒绝,理由是投资大,短期难见利。

幸运的是,苦熬多年后,京东方靓丽崛起,勇夺五个全球第一,让当初的坚持者赚得盆满钵满。

近年来,国家加大了对半导体行业的投入。大基金一期投入 1300 亿,已收尾;二期预计超过 2000 亿。乍一看,钱不少,但需要投资的项目也很多,涵盖芯片设计、制造、封测、设备等诸多领域。以一期为例,累计投资 62 个项目,涉及 23 家上市公司。

这样平均下来,每家获得的投资额并不多,跟撒胡椒面一样。

而纯靠市场手段去募集资金的难度同样非常大。以紫光为例,真正的大规模投资还没开始,市场就已不乏圈钱的质疑声,跟京东方当年恶战面板产业时所遭遇的挑战几乎没有两样。

显然,这样的投资强度是不够的。再看一组数据,就更能看出差距。

全球芯片三巨头,三星、英特尔、台积电,每年的投资都在百亿美元级别,而中芯国际不到对方的十分之一。

设备三巨头,应用材料、泛林、东京电子,每年在研发上投入5—10 亿美元不等,而中微半导体直到去年,收入才破 10 亿,还是人民币。

人才的切肤之痛

搞好半导体,主要靠三件事:一个是钱,一个是人,外加一个政策。钱的事好说,毕竟这些年国家不差钱;人的事很难搞,因为非一朝一夕之功。

数据显示,我国未来需要 70 万半导体人才,目前只有不到 30 万,缺口 40 万。

我们尤其缺行业的领军人物。这些年,01/02 专项取得的重大突破,很多都是海归创造的,他们长期任职于欧美半导体公司,拥有丰富的行业经验。

张汝京,德州仪器工作 20 年,在全球盖过 20 座芯片工厂。回国后,创办了中芯国际,以及国内第一家 12 寸硅晶圆厂,被誉为中国半导体之父。

尹志尧,闯荡硅谷 20 年,先后任职于英特尔、泛林和应用材料。回国后,创办中微半导体,几乎以一己之力,将国内介质蚀刻机带到了世界水平。

此外,曾任职于霍尼韦尔的姚力军,回国后做出了高纯度溅射靶材;美国留学归来的王淑敏,研发出国内第一款研磨液,打破了国外垄断。

日本、韩国和中国台湾地区,也是半导体人才的重要来源。大陆两大代工厂,中芯国际和厦门联芯,都有台湾背景,很多技术人员也来自台湾。

这几年,国内存储器的跨越式发展,也离不开日本、韩国、中国台湾技术人员的贡献。日本厂商尔必达破产后,大批日本人赴中国寻找机会,包括前社长坂本幸雄。

以紫光为例,外界看到其董事长赵伟国在资本和产业上动作频频,而事实上,他用心同样多的也是找人。其总投资预计 1000 亿美元左右的长江储存的执行董事长高启全,便是他费尽心思从台湾争抢过来的世界级半导体产业猛人。

目前,长江存储的 3D NAND 闪存已经获得第一笔订单,总计 10776 颗芯片,将用于 8GB USD 存储卡产品。今年 10 月,我国首批拥有完全自主知识产权的 32 层三维 NAND 闪存芯片将在这里实现量产,这也是中国集成电路闪存芯片产业规模化发展“零”的突破。

然而,引进人才毕竟不是长久之计。国内半导体行业要想大发展,必须立足于培养本土人才。

一方面,外来人才和本土人才,在利益、观念等方面是有冲突的。中芯国际在江上舟离世后,就陷入外来人员和本土人员的派系之争,一度影响到公司的发展。

另一方面,半导体是微加工行业,工艺很关键。很多外国技术人员之所以牛,是他们一辈子只干一件事积累起来的。

2002 年,上海微电子总经理赴德国考察,有工程师告诉他:“给你们全套图纸,也做不出来。”开始他不服,后来明白了。那里的抛光工人,祖孙三代干着同样一件事,“同样一个镜片,不同工人去磨,光洁度相差十倍。”

这恰是中国半导体行业的一个切肤之痛。

我们不缺设计人员,但缺工艺工程师,而这类人才很难靠引进来满足。

中芯国际之所以在制程上落后2-3 代,除了光刻机等设备受限外,工艺上的经验欠缺才是更重要的原因。

遗憾的是,目前国内不少高校的人才培养,与现实脱节。大多数学生跑去做软件,做应用,却不愿搞更基础的计算机系统和底层结构。

不容乐观的生态链

在自然界,动植物要生存,必须融入生物链。

做企业也一样。只不过,在企业这个生态链中,先行者有成本优势,再加上稳定可靠的供应链,使得他们能够持续盈利,进而支撑着技术的不断进步。

这对后来者而言,如同一道不可逾越的壁垒。

这些年,中国半导体产业面临的一大难题,就是如何融入这个生态链。

龙芯是一个很好的例子。这款中科院计算所自主研发的芯片,尽管性能不俗,但一直游离在民用市场外。原因很简单,市场上有更成熟、性价比更高的处理器。

龙芯的遭遇并非个案。大部分芯片制造厂,在采购装备时,一定考虑的是进口设备,因为国产设备刚起步,质量不稳定、一致性差。

当年,LED 芯片刚在国内兴起时,各大芯片厂均只认美、德设备,而地方政府的补贴也只给进口设备。

内忧外患,将国内 MOCVD 设备商逼到了绝境,“客户不太愿意用……因为不信任,需要重新验证,这又要花钱。”中晟光电负责人陈爱华说。

直到后来,工信部为每台国产设备提供 2000 万补贴,形势才开始好转。

新产品研制出来,没有人用,就不可能盈利;没有盈利,就没钱搞研发。结果只能是恶性循环,胎死腹中。

这个时候,需要来自生态链的支持。国内半导体行业近年来的进步,尤其是设备和材料领域,很大程度上得益于中芯国际、厦门联芯等晶圆制造厂的带动。

但这种机会,不是国外厂商所能提供的。那种一切交给市场的想法,不能说幼稚,至少也是罔顾事实的。

几年前,韩国 SK 海力士曾采购过中微半导体的蚀刻机,后来放弃了。表面上是因为性能不及预期,实际是担心泄露核心工艺的秘密。

一个有意思的细节是,尽管中国民用芯片九成依赖进口,但军用芯片却基本能自给自足,甚至还有出口。比如,龙芯就稳定运行在北斗导航系统上。

另一款自主芯片,来自总参 56 所的申威,则撑起了我们的太湖之光超级计算机。

军用很出彩,民用却卖不出去?问题就在生态链上。

军用市场是一个封闭的小圈子,产品追求稳定性和抗干扰,对性能并不敏感。龙芯和申威在这里能找到自己的位置。

反观民用市场,性能为王,技术迭代快,龙芯和申威很难融入这样的生态链。

结束语

通过以上梳理,我们看到,国内造不好高端芯片,有外部因素,也有自身原因。

形势看似悲观,前景却很光明。

一方面,半导体行业向中国转移的大趋势不会改变。另一方面,摩尔定律在工艺上逐渐趋近极限,客观上给了国内企业追赶的机会,而国家也正进一步加大支持和投入。

最近,我国国家领导人在武汉考察时,就特别到长江存储作了考察与指示。受此鼓舞的赵伟国则表示,公司将尽快在全球集成电路产业占据重要地位,用 5 到 10 年时间成为全球三维闪存主要供应商之一。

在国家的支持和企业的自身努力下,国内半导体产业链正在出现由点到面的突破,而在三大历史性机遇的支持下,我们也必须迎头赶上。

否则,后面的仗将会越来越难打,因为半导体产业不光是现代高科技产业的基础,更是支撑和保障国家安全的战略性、基础性和先导性产业,而且重要性会越来越大。

from:https://news.cnblogs.com/n/613348/

从 0 到 1 再到 100, 搭建、编写、构建一个前端项目

1. 选择现成的项目模板还是自己搭建项目骨架

搭建一个前端项目的方式有两种:选择现成的项目模板、自己搭建项目骨架。

选择一个现成项目模板是搭建一个项目最快的方式,模板已经把基本的骨架都搭建好了,你只需要向里面填充具体的业务代码,就可以通过内置的工具与命令构建代码、部署到服务器等。

一般来说,一个现成的项目模板会预定义一定的目录结构、书写方式,在编写项目代码时需要遵循相应的规范;也会内置必要的工具,比如 .editorconfigeslintstylelintprettierhuskylint-staged 等;也会内置必要的命令(package.json | scripts),比如 本地开发:npm run dev本地预览:npm run start构建:npm run build部署:npm run deploy等。

社区比较好的项目模板:

这些模板的使用又分为两种:使用 git 直接克隆到本地、使用命令行创建。

(使用现有模板构建的项目,可以跳过第 2 ~ 7 步)

1.1 使用 git 直接克隆到本地

这是一种真正意义上的模板,可以直接到模板项目的 github 主页,就能看到整个骨架,比如 react-boilerplateant-design-provue-element-adminreact-starter-kit

以 react-boilerplate 为例:

克隆到本地:

git clone --depth=1 https://github.com/react-boilerplate/react-boilerplate.git <你的项目名字>

切换到目录下:

cd <你的项目名字>

一般来说,接下来运行 npm run install 安装项目的依赖后,就可以运行;有些模板可能有内置的初始化命令,比如 react-boilerplate

npm run setup

启动应用:

npm start

这时,就可以在浏览器中预览应用了。

1.2 使用命令行创建

这种方式需要安装相应的命令,然后由命令来创建项目。

以 create-react-app 为例:

安装命令:

npm install -g create-react-app

创建项目:

create-react-app my-app

运行应用:

cd my-app
npm start

1.3 自己搭建项目骨架

如果你需要定制化,可以选择自己搭建项目的骨架,但这需要开发者对构建工具如 webpacknpmnode 及其生态等有相当的了解与应用,才能完美的把控整个项目。

下面将会一步一步的说明如何搭建一个定制化的项目骨架。

2. 选择合适的规范来写代码

js 模块化的发展大致有这样一个过程 iife => commonjs/amd => es6,而在这几个规范中:

  • iifejs 原生支持,但一般不会直接使用这种规范写代码
  • amdrequirejs 定义的加载规范,但随着构建工具的出现,便一般不会用这种规范写代码
  • commonjsnode 的模块加载规范,一般会用这种规范写 node 程序
  • es6ECMAScript2015 定义的模块加载规范,需要转码后浏览器才能运行

这里推荐使用 es6 的模块化规范来写代码,然后用工具转换成 es5 的代码,并且 es6 的代码可以使用 Tree shaking 功能。

参考:

3. 选择合适的构建工具

对于前端项目来说,构建工具一般都选用 webpackwebpack 提供了强大的功能和配置化运行。如果你不喜欢复杂的配置,可以尝试 parcel

参考:

4. 确定是单页面应用(SPA)还是多页面应用

因为单页面应用与多页面应用在构建的方式上有很大的不同,所以需要从项目一开始就确定,使用哪种模式来构建项目。

4.1 多页面应用

传统多页面是由后端控制一个 url 对应一个 html 文件,页面之间的跳转需要根据后端给出的 url 跳转到新的 html 上。比如:

http://www.example.com/page1 -> path/to/page1.html
http://www.example.com/page2 -> path/to/page2.html
http://www.example.com/page3 -> path/to/page3.html

这种方式的应用,项目里会有多个入口文件,搭建项目的时候就需要对这种多入口模式进行封装。另外,也可以选择一些封装的多入口构建工具,如 lila

4.2 单页面应用

单页面应用(single page application),就是只有一个页面的应用,页面的刷新和内部子页面的跳转完全由 js 来控制。

一般单页面应用都有以下几个特点:

  • 本地路由,由 js 定义路由、根据路由渲染页面、控制页面的跳转
  • 所有文件只会加载一次,最大限度重用文件,并且极大提升加载速度
  • 按需加载,只有真正使用到页面的时候,才加载相应的文件

这种方式的应用,项目里只有一个入口文件,便无需封装。

参考:

5. 选择合适的前端框架与 UI 库

一般在搭建项目的时候就需要定下前端框架与 UI 库,因为如果后期想更换前端框架和 UI 库,代价是很大的。

比较现代化的前端框架:

一些不错的组合:

参考:

6. 定好目录结构

一个好的目录结构对一个好的项目而言是非常必要的。

一个好的目录结构应当具有以下的一些特点:

  1. 解耦:代码尽量去耦合,这样代码逻辑清晰,也容易扩展
  2. 分块:按照功能对代码进行分块、分组,并能快捷的添加分块、分组
  3. 编辑器友好:需要更新功能时,可以很快的定位到相关文件,并且这些文件应该是很靠近的,而不至于到处找文件

比较推荐的目录结构:

多页面应用

|-- src/ 源代码目录

    |-- page1/ page1 页面的工作空间(与这个页面相关的文件都放在这个目录下)
        |-- index.html html 入口文件
        |-- index.js js 入口文件
        |-- index.(css|less|scss) 样式入口文件
        |-- html/ html 片段目录
        |-- (css|less|scss)/ 样式文件目录
        |-- mock/ 本地 json 数据模拟
        |-- images/ 图片文件目录
        |-- components/ 组件目录(如果基于 react, vue 等组件化框架)
        |-- ...
        
    |-- sub-dir/ 子目录
        |-- page2/ page2 页面的工作空间(内部结构参考 page1)
            |-- ...
        
    |-- ...
    
|-- html/ 公共 html 片段
|-- less/ 公共 less 目录
|-- components/ 公共组件目录
|-- images/ 公共图片目录
|-- mock/ 公共 api-mock 文件目录
|-- ...

单页面应用

|-- src/ 源代码目录
    |-- page1/ page1 页面的工作空间
        |-- index.js 入口文件
        |-- services/ service 目录
        |-- models/ model 目录
        |-- mock/ 本地 json 数据模拟
        |-- images/ 图片文件目录
        |-- components/ 组件目录(如果基于 react, vue 等组件化框架)
        |-- ...
        
    |-- module1/ 子目录
        |-- page2/ page2 页面的工作空间(内部结构参考 page1)
        
    |-- ...
    
|-- images/ 公共图片目录
|-- mock/ 公共 api-mock 文件目录
|-- components/ 公共组件目录   
|-- ... 

参考:

7. 搭建一个好的脚手架

搭建一个好的脚手架,能够更好的编写代码、构建项目等。

可以查看 搭建自己的前端脚手架 了解一些基本的脚手架文件与工具。

比如:

|-- /                              项目根目录
    |-- src/                       源代码目录
    |-- package.json               npm 项目文件
    |-- README.md                  项目说明文件
    |-- CHANGELOG.md               版本更新记录
    |-- .gitignore                 git 忽略配置文件
    |-- .editorconfig              编辑器配置文件
    |-- .npmrc                     npm 配置文件
    |-- .npmignore                 npm 忽略配置文件
    |-- .eslintrc                  eslint 配置文件
    |-- .eslintignore              eslint 忽略配置文件
    |-- .stylelintrc               stylelint 配置文件
    |-- .stylelintignore           stylelint 忽略配置文件
    |-- .prettierrc                prettier 配置文件
    |-- .prettierignore            prettier 忽略配置文件
    
    |-- .babelrc                   babel 配置文件
    |-- webpack.config.js          webpack 配置文件
    |-- rollup.config.js           rollup 配置文件
    |-- gulpfile.js                gulp 配置文件
    
    |-- test/                      测试目录
    |-- docs/                      文档目录
    |-- jest.config.js             jest 配置文件
    |-- .gitattributes             git 属性配置
  • .editorconfig: 用这个文件来统一不同编辑器的一些配置,比如 tab 转 2 个空格、自动插入空尾行、去掉行尾的空格等,http://editorconfig.org
  • eslintstylelintprettier: 规范化代码风格、优化代码格式等
  • huskylint-staged: 在 git 提交之前对代码进行审查,否则不予提交
  • .gitlab-ci.ymlgitlab ci 持续集成服务

参考:

=================================================

到这里为止,一个基本的项目骨架就算搭建好了。

8. 使用版本控制系统管理源代码(git)

项目搭建好后,需要一个版本控制系统来管理源代码。

比较常用的版本管理工具有 gitsvn,现在一般都用 git

一般开源的项目可以托管到 http://github.com,私人的项目可以托管到 https://gitee.comhttps://coding.net/,而企业的项目则需要自建版本控制系统了。

自建版本控制系统主要有 gitlabgogsgiteagitlab 是由商业驱动的,比较稳定,社区版是免费的,一般建议选用这个;gogs, gitea 是开源的项目,还不太稳定,期待进一步的更新。

所以,git + gitlab 是不错的配合。

9. 编写代码

编写代码时,js 选用 es6 的模块化规范来写(如果喜欢用 TypeScript,需要加上 ts-loader),样式可以用 lessscsscss 来写。

写 js 模块文件时,注释可以使用 jsdoc 的规范来写,如果配置相应的工具,可以将这些注释导出接口文档。

因为脚手架里有 huskylint-staged 的配合,所以每次提交的代码都会进行代码审查与格式优化,如果不符合规范,则需要把不规范的代码进行修改,然后才能提交到代码仓库中。

比如 console.log(haha.hehe); 这段代码就会遇到错误,不予提交:

这个功能定义在 package.json 中:

{
  "devDependencies": {             工具依赖
    "babel-eslint": "^8.2.6",
    "eslint": "^4.19.1",
    "husky": "^0.14.3",
    "lint-staged": "^7.2.0",
    "prettier": "^1.14.0",
    "stylelint": "^9.3.0",
    "eslint-config-airbnb": "^17.0.0",
    "eslint-config-prettier": "^2.9.0",
    "eslint-plugin-babel": "^5.1.0",
    "eslint-plugin-import": "^2.13.0",
    "eslint-plugin-jsx-a11y": "^6.1.0",
    "eslint-plugin-prettier": "^2.6.2",
    "eslint-plugin-react": "^7.10.0",
    "stylelint-config-prettier": "^3.3.0",
    "stylelint-config-standard": "^18.2.0"
  },
  "scripts": {                     可以添加更多命令
    "precommit": "npm run lint-staged",
    "prettier": "prettier --write \"./**/*.{js,jsx,css,less,sass,scss,md,json}\"",
    "eslint": "eslint .",
    "eslint:fix": "eslint . --fix",
    "stylelint": "stylelint \"./**/*.{css,less,sass,scss}\"",
    "stylelint:fix": "stylelint \"./**/*.{css,less,sass,scss}\" --fix",
    "lint-staged": "lint-staged"
  },
  "lint-staged": {                 对提交的代码进行检查与矫正
    "**/*.{js,jsx}": [
      "eslint --fix",
      "prettier --write",
      "git add"
    ],
    "**/*.{css,less,sass,scss}": [
      "stylelint --fix",
      "prettier --write",
      "git add"
    ],
    "**/*.{md,json}": [
      "prettier --write",
      "git add"
    ]
  }
}
  • 如果你想禁用这个功能,可以把 scripts 中 "precommit" 改成 "//precommit"
  • 如果你想自定 eslint 检查代码的规范,可以修改 .eslintrc, .eslintrc.js 等配置文件
  • 如果你想自定 stylelint 检查代码的规范,可以修改 .stylelintrc, .stylelintrc.js 等配置文件
  • 如果你想忽略某些文件不进行代码检查,可以修改 .eslintignore, .stylelintignore 配置文件

参考:

10. 组件化

当项目拥有了一定量的代码之后,就会发现,有些代码是很多页面共用的,于是把这些代码提取出来,封装成一个组件,供各个地方使用。

当拥有多个项目的时候,有些组件需要跨项目使用,一种方式是复制代码到其他项目中,但这种方式会导致组件代码很难维护,所以,一般是用另一种方式:组件化。

组件化就是将组件独立成一个项目,然后在其他项目中安装这个组件,才能使用。

一般组件化会配合私有 npm 仓库一起用。

|-- project1/ 项目1
    |-- package.json
    
|-- project2/ 项目2
    |-- package.json    

|-- component1/ 组件1
    |-- package.json

|-- component2/ 组件2
    |-- package.json

在 project1 中安装 component1, component2 组件:

# package.json
{
  "dependencies": {
    "component1": "^0.0.1",
    "component2": "^0.0.1"
  }
}
import compoennt1 from 'compoennt1';
import compoennt2 from 'compoennt2';

如果想要了解怎样写好一个组件(npm package),可以参考 从 1 到完美,写一个 js 库、node 库、前端组件库

参考:

11. 测试

测试的目的在于能以最少的人力和时间发现潜在的各种错误和缺陷,这在项目更新、重构等的过程中尤其重要,因为每当更改一些代码后,你并不知道这些代码有没有问题、会不会影响其他的模块。如果有了测试,运行一遍测试用例,就知道更改的代码有没有问题、会不会产生影响。

一般前端测试分以下几种:

  1. 单元测试:模块单元、函数单元、组件单元等的单元块的测试
  2. 集成测试:接口依赖(ajax)、I/O 依赖、环境依赖(localStorage、IndexedDB)等的上下文的集成测试
  3. 样式测试:对样式的测试
  4. E2E 测试:端到端测试,也就是在实际生产环境测试整个应用

一般会用到下面的一些工具:

另外,可以参考 聊聊前端开发的测试

12. 构建

一般单页面应用的构建会有 npm run build 的命令来构建项目,然后会输出一个 html 文件,一些 js/css/images ... 文件,然后把这些文件部署到服务器就可以了。

多页面应用的构建要复杂一些,因为是多入口的,所以一般会封装构建工具,然后通过参数传入多个入口:

npm run build -- page1 page2 dir1/* dir2/all --env test/prod
  • page1, page2 确定构建哪些页面;dir1/*, dir2/all 某个目录下所有的页面;all, * 整个项目所有的页面
  • 有时候可能还会针对不同的服务器环境(比如测试机、正式机)做出不同的构建,可以在后面加参数
  • -- 用来分割 npm 本身的参数与脚本参数,参考 npm – run-script 了解详情

多页面应用会导出多个 html 文件,需要注意这些导出的 html 不要相冲突了。

当然,也可以用一些已经封装好的工具,如 lila

13. 部署

在构建好项目之后,就可以部署到服务器了。

传统的方式,可以用 ftp, sftp 等工具,手动传到服务器,但这种方式比较笨拙,不够自动化。

自动化的,可以用一些工具部署到服务器,如 gulpgulp-ssh,当然也可以用一些封装的工具,如 md-synclila 等

以 md-sync 为例:

npm install md-sync --save-dev

md-sync.config.js 配置文件:

module.exports = [
  {
    src: './build/**/*',
    remotePath: 'remotePath',
    server: {
      ignoreErrors: true,
      sshConfig: {
        host: 'host',
        username: 'username',
        password: 'password'
      }
    },
  },
  {
    src: './build/**/*.html',
    remotePath: 'remotePath2',
    server: {
      ignoreErrors: true,
      sshConfig: {
        host: 'host',
        username: 'username',
        password: 'password'
      }
    },
  },
];

在 package.json 的 scripts 配置好命令:

"scripts": {
  "deploy": "md-sync"
}
npm run deploy

另外,一般大型项目会使用持续集成 + shell 命令(如 rsync)部署。

14. 持续集成测试、构建、部署

一般大型工程的的构建与测试都会花很长的时间,在本地做这些事情的话就不太实际,这就需要做持续集成测试、构建、部署了。

持续集成工具用的比较多的:

jenkins 是通用型的工具,可以与 githubbitbucketgitlab 等代码托管服务配合使用,优点是功能强大、插件多、社区活跃,但缺点是配置复杂、使用难度较高。

gitlab ci 是 gitlab 内部自带的持续集成功能,优点是使用简单、配置简单,但缺点是不及 jenkins 功能强大、绑定 gitlab 才能使用。

以 gitlab 为例(任务定义在 .gitlab-ci.yml 中):

stages:
  - install
  - test
  - build
  - deploy

# 安装依赖
install:
  stage: install
  only:
    - dev
    - master
  script:
    - npm install

# 运行测试用例
test:
  stage: test
  only:
    - dev
    - master
  script:
    - npm run test

# 编译
build:
  stage: build
  only:
    - dev
    - master
  script:
    - npm run clean
    - npm run build

# 部署服务器
deploy:
  stage: deploy
  only:
    - dev
    - master
  script:
    - npm run deploy

以上配置表示只要在 dev 或 master 分支有代码推送,就会进行持续集成,依次运行:

  • npm install
  • npm run test
  • npm run clean
  • npm run build
  • npm run deploy

最终完成部署。如果中间某个命令失败了,将停止接下的命令的运行,并将错误报告给你。

这些操作都在远程机器上完成。

=================================================

到这里为止,基本上完成了一个项目的搭建、编写、构建。

15. 清理服务器上过期文件

现在前端的项目基本上都会用 webpack 打包代码,并且文件名(html 文件除外)都是 hash 化的,如果需要清除过期的文件而又不想把服务器上文件全部删掉然后重新构建、部署,可以使用 sclean 来清除过期文件。

16. 收集前端错误反馈

当用户在用线上的程序时,怎么知道有没有出 bug;如果出 bug 了,报的是什么错;如果是 js 报错,怎么知道是那一行运行出了错?

所以,在程序运行时捕捉 js 脚本错误,并上报到服务器,是非常有必要的。

这里就要用到 window.onerror 了:

window.onerror = (errorMessage, scriptURI, lineNumber, columnNumber, errorObj) => {
  const data = {
    title: document.getElementsByTagName('title')[0].innerText,
    errorMessage,
    scriptURI,
    lineNumber,
    columnNumber,
    detailMessage: (errorObj && errorObj.message) || '',
    stack: (errorObj && errorObj.stack) || '',
    userAgent: window.navigator.userAgent,
    locationHref: window.location.href,
    cookie: window.document.cookie,
  };

  post('url', data); // 上报到服务器
};

线上的 js 脚本都是压缩过的,需要用 sourcemap 文件与 source-map 查看原始的报错堆栈信息,可以参考 细说 js 压缩、sourcemap、通过 sourcemap 查找原始报错信息 了解详细信息。

参考:

后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

from:https://segmentfault.com/a/1190000017158444

微服务在微信的架构实践

微服务的理念与腾讯一直倡导的“大系统小做”有很多相通之处,本文将分享微信后台架构的服务发现、通信机制、集群管理等基础能力与其上层服务划分原则、代码管理规则等。
背景介绍

首先,我们需要敏捷开发。过去几年,微信都是很敏捷地在开发一些业务。所以我们的底层架构需要支撑业务的快速发展,会有一些特殊的需求。

另外,目前整个微信团队已经有一千多人了,开发人员也有好几百。整个微信底层框架是统一的,微信后台有千级模块的系统。比如说某某服务,有上千个微服务在跑,而集群机器数有几万台,那么在这样的规模下,我们会有怎么样的挑战呢?

我们一直在说“大系统小做”,联想一下,微服务与腾讯的理念有哪些相同与不同的地方呢?通过对比,最终发现还是有许多相通的地方。所以我挑出来讲讲我们的实践。

看过过去几个会议的内容,可能大家会偏向于讲整一个大的框架,比如整个云的架构。但是我这边主要讲的是几个特殊的点。

概览详情

开始看一下我们的结构。全球都有分布,主要有上海、深圳、香港、加拿大几个数据中心。

其中上海服务国内北方的用户,深圳负责南方用户,加拿大服务北美、南美和欧洲,香港服务东南亚、中东和非洲地区。

然后来看看我们的架构:

  • 最上边是我们的产品;
  • 然后有一个号称几亿在线的长连接和短连接的服务;
  • 中间有一个逻辑层,后台框架讲的主要是逻辑层往后这块,包括我们的 RPC、服务分组、过载保护与数据存储;
  • 底层有个 PaxosStore 存储平台。

整套就是这么个体系。微服务很容易去构建,但是规模变大后有哪些问题,需要哪些能力?这里挑出三个点来讲一下:

一、敏捷

希望你的服务很快实现,不太多去考虑。像我们早期互联网业务,甚至包括 QQ 等,我们很注重架构师的一个能力,他需要把握很多的东西。他设置每个服务的时候,要先算好很多资源,算好容灾怎么做。容灾这个问题直接影响业务怎么去实现的,所以有可能你要做一个具体逻辑的时候要考虑很多问题,比如接入服务、数据同步、容灾等等每个点都要考虑清楚,所以节奏会慢。

二、容错

当你的机器到了数万台,那每天都有大量机器会有故障。再细一点,可以说是每一个盘的故障更频繁一点。

三、高并发

基础架构

接下来看看我们的基础架构。

整个微服务的架构上,我们通常分成这些部分:

  • 服务布局
  • 服务之间怎么做一些远程调用
  • 容错(主要讲一下过载保护)
  • 部署管理
服务布局

分两层,一个是城市间。城市之间的数据是相对独立的,除了少数账号全球同步,大部分业务都希望做成电子邮件式的服务,各自有自身的环境在跑,之间使用类似于电子邮件的通信。所以我们选择让每个城市自治,它们之间有一个 200-400ms 的慢速网络,国内会快点,30ms。

而城市内部,就是每个园区是一套独立的系统,可以互相为对方提供备份。要求独立的电源与网络接入。

城市内部会有整套的划分,终端 –>接入层 –>逻辑层 –>存储层 都是完全独立的一套系统。

远程调用

看到很多框架,竟然是没有协程的,这很诧异。早年我们 QQ 邮箱、微信、图像压缩、反垃圾都是一个 web 服务,只有存储层会独立到后面去,甚至用 web 直连 MySQL。因为它早期比较小,后来变大之后就用微服务架构。

每个东西都变成一个小的服务,他们是跨机的。你可以想象一下,每天我们很多人买早餐的时候,掏出手机做一个微信支付,这一个动作在后台会引起上百次的调用。这有一个复杂的链路。在 2014 年之前,我们微信就是没有做异步的,都是同步的,在这么多调用里,A 服务调用 B,那要先等它返回,这样就占住了一条进程或者线程。所以其实 13 年的时候,我们发生了大大小小的故障,很大一部分原因就在这里。

然后 13 年底的时候,这个问题太严重了,严重到,比如发消息的时候,你去拿一个头像之类的,它只要抖动,就可能引发整一条调用链的问题,并且因为过程保护的不完善,它会把整个消息发送的曲线掉下去,这是我们很痛苦的时间。

然后当时我们就去考虑这些方案,13 年的时候抽出 3 个人重新做了一个完整的库 libco。(两千行),实现时间轮盘与事件处理链、常用网络编程模式、同步原语等。它分为三大块,事件驱动、网络 HOOK 和协程机制。

早期是多进程为主,当年切多线程的时候,也遇到一大波修改,后来线程里有了一个线程变量就好多了。如果没有这个东西,你可能要把许多变量改成参数再一层一层传递下去。有了线程变量就好多了。现在我们的协程变量也是这个意义,效果就像写一个宏一样。

另一个是,我们支持 CGI,早期库在 CGI 上遇到问题,所以没有推广。因为一个标准 CGI 服务是基于一些古老的接口的,像 getENV、setENV,就是说你的 coreString 是通过 ENV 来得到的,那么这个我们也把它给 HOOK 掉了,它会根据你的协程去分派。

最难的一个是 gethostbyname 方法,我发现很多人就连在异步编程里,处理 hostbyname 也可能是用了一套独立线程去做,或者你很辛苦地把整个代码抠出来重新写一遍,这个肯定是有很多问题的。所以我们 libco 就把这个 gethostbyname 给完整地支持了。

最后如果你还不爽,说一般业务逻辑可以这么干,那我还有很多后台代码怎么办呢?很多有经验的老的程序员可能要拿着他们那一堆很复杂的异步编程的代码来质疑我们,他们不认为他们的代码已经完全可以被协程所取代了。

他们有如下两个质疑:

  • 质疑性能:协程有很多切换,会不会带来更大开销?
  • 你可能处理几万并发就好,消耗个 1G 内存就行,但是我们这里是处理千万并发哦,这么大的规模,我不信任你这个东西。

这样我们其实是面临了一个问题,因为一些老代码,越是高级的人写的,它的技术栈越深,稍微改动一点代码,就出 BUG 了。

所以我们后来做了两个东西,一个是实际修改了相对简单的异步代码到 libco 里,然后性能更好了。因为在做异步编程的时候,你需要自己去维护很多的数据结构,做你的状态保存,它们的生存期有可能需要很久,你自然地会分配许多内存给它,当然你会用一些内存池去优化它,但是这些是有限的。

但是你用协程的话,很多变量就自然在一个连续的内存里了,相当于一个小的内存池,就比如 if……else……这个你没有必要去 new 一个东西保存状态的,直接放在栈里就行了,所以它的性能更好了。

第二个是,它要求很高的并发。由于协程要一个栈,我们一般开 128k,如果你对这个代码掌控得比较好,可能开 16k,就算是这样,你要开 1 万个协程,还是要 100 多 M 的内存。所以我们后来就在这基础上做了一个可以支持千万连接的协程模式。

Libco 是一个底层库,让你很方便开发,但是大部分开发人员不是直接面对 libco 的,我们花了一年时间把整个微信后台绝大部分逻辑服务、存储服务改成基于 libco,整个配置就直接通过配一台机器上的并发数配 10 倍甚至 20、30 倍,这样子就一下子把整个问题解决了。

过载保护

并发数上去后容易引发另一个问题,早期的时候,后端服务性能高,逻辑服务性能相对弱,很容易被 hold,不可能给后端发起很多连接,不具有“攻击性”,但修改完成后,整个前端变得很强,那可能对后端产生很大的影响。这个时候就要来考虑一下过载保护了。

一般会提到几个点。

1.轻重分离:

就是一个服务里边不要又有重的操作,又有轻的,这样过载的时候,大量的请求都被某些小请求拦截掉了,资源被占满了。

2.队列:

过载保护一般是说系统内部服务在做过去的事情,做无用功。它们可能待在某个队列里边,比如服务时间要求 100ms,但它们总是在做 1s 以前的任务,所以整个系统会崩溃。所以老的架构师会注重说配好每一个服务的队列长度,估算好。但是在繁忙的开发中,是很难去控制的。

3.组合命令式:

后端服务并不是只有一个,上边这个图中的例子,想要调用很多服务,然后 AB 都过载,它们每一个其实都只是过载一点,通过率可达到 80%,但是前端需要这两个服务的组合服务,那么这里就可能只能达到 60% 的通过率。然后后边如果是更多的服务,那么每个服务的一点点过载,到了前端就是很严重的问题。怎么解决呢?

这本书在 12、13 年的时候很火,里边提到了两个对我们有用的点。

  • 一个是“希望系统是分布式的,去中心化”,指系统过载保护依赖每一个节点自身的情况去做,而不是下达一个统一的中心指令。
  • 二是“希望整个控制是基于反馈的”,它举了一些例子,像抽水马桶,像过去炼钢铁的参数很难配,但是只要有一个反馈机制就好解决了。

于是我们构建了一套看起来有点复杂的过载保护系统。

整个系统基于反馈,然后它把整个拒绝的信息全程传递了。看到最右边,有几个典型的服务,从一个 CGI 调用一个后台服务,再调用另一个后台服务,它会在 CGI 层面就把它的重要程度往下传。回到刚才那个前端调用 A、B 服务的例子,使用这样的一种重要程度传递,就可以直接拒绝那些相同用户的 20% 的请求,这样就解决了这个问题。

 怎么配队列?

这个只是反映了生产者和服务者处理能力的差异,观察这个差异,就可以得到一个好的拒绝的数。你不需要去配它多长,只需要去看一个请求在队列里待的平均时间是否可以接受,是一个上涨趋势还是一个下降趋势。这样我们就可以决定要不要去拒绝。那这样几乎是全自动的。你只要配得相对大一点就行了,可以抗一些抖动。在接入之前就评估它,在过去一段时间内平均队列耗时多长,如果超过预支,我们就往下调。这样就把整个系统的过载能力提升了很多。

这是一个具体的做法,我们会考虑两个维度,一个是后台服务,可能服务很多不同的前端,它可能来源于一个支付的请求,经过层层调用,到达后台;或者是一个发消息的服务;它也可能是一个不重要的小服务,如果这个账户服务过载的时候,那么我们可以根据这个表来自动地优先去拒绝一些不那么重要的服务请求,使得我们核心服务能力可以更好地提供。这样整个系统就可以做到很好的过载保护。

数据存储

上边提到一个数据层,那我们是怎么去做数据的呢?

在过去很多年里,我们可能是尽可能去事务化、不追求强一致,一般是采用主备同步的方法。但我们的目标还是强一致的存储。

强一致是说,写一个数据之后,服务器的返回成功不会因为单机故障而丢失。早年我们用的是自己设计的协议,严格来证明的话,没有 Paxos 这么严谨,所以我们在过去一年多的时间内,重新做了一个 Paxos 存储。

它是一个同步复制的数据存储,支持各个园区之间的数据一致性,并且是可以多组多写的,就是说任何一个园区接入,它都可以进行数据的强制读写。另外它并不只是 key-value 模式,它支持 key-value、list、表。在微信这边很少会说完全依赖 key-value 的,因为很多业务都是有列表、表格等的请求,所以很多年前就开始用表格的存储。

Paxos 可用性很高,所以我们就敢做单表有亿行的设计,这样像公众号粉丝等需要很大的,几千万甚至几亿行的记录,就不用考虑自己去分表。并且这个存储可以使用类 SQL 的语句去做,它是完全保证事务的。

它还是插件化系统,不仅支持 LSM,还支持其它存储引擎。

然后它低成本,后台 CPU 有 E3-1230V3,也有 E5-2670 型号的,内存,CPU 与 ssd 之间有一些能力用不上,所以我们系统是可以灵活组合很多不同存储介质的。

这个系统是跑在同城的,也就是上海内部、深圳内部、加拿大内部和香港内部。它们之间的延迟相对较低,几毫秒的级别。这是一个非租约的,没有 leader,不存在切换的不可用期,随时都可以切换任何一个园区。负载均衡这一块我们沿用 kb64 架构,6 台机为一组。因为园区故障少,平时单机时,分摊 25% 的流量,整体比较稳定。6 台为一组时,整个作为一个 set,有很多 set 之间的适用一致性要去做,会有一个很细粒度的伸缩性,比如它可以 100 组扩展到 101 组。

 为什么用这么重的方式呢?

因为希望应用是 简单快速 的,不用假设一个数据写完之后还可能被回退掉,这样只会有很多额外的开销,会有很多问题。比如公众号,他们有很多素材库之类的很重要的存储,如果数据突然丢了,或者说回退了,没有了,那用户投诉是会很严重的。微信账号这边也是这样,如果一个账户注册了,但是这个数据回退了,那也是很严重的问题。

另一个原因是 可用性。在一个传统的主备系统里面,当主机挂掉,面临切不切备机的抉择,然后你会层层请示,说明目前的同步状况,甚至你不知道当前的同步状况,经过很多流程来请示是否切换备机。

而另外,它也不是一个高成本的方案。

 为什么不用 Raft 呢?

Raft 的开源很有价值,它把互联网后台的数据一致性能力提升了很多,就算是一个很小的团队,它也能直接用 Raft 获得一个强一致能力,而这可能就已经超过了许多互联网后台的强一致能力,因为很多后台都是用了很古老的架构,比如长期用到主机架构。

 Raft 与 Paxos 的区别是什么呢?

其实 Raft 和 Paxos 不是一个层面的概念,这个图就是典型的通过一个 log 变更 db 的架构,通过三条 log 一致性做到数据持久强一致性。那 Paxos 在哪里?在一个 log 的某一个 entry 那边,三个点构成一个常量。

那 Raft 是什么呢?它是整一个二维的东西,就是说,基于一个 Paxos 强一致协议做的一条 log,它整个就是一个 Raft。所以我们可以认为 Raft 其实是 Paxos(log)的一种选择。如果你允许绿色部分不存在,那它就不是 Raft。因为 Raft 的设计是你自己做的,它与 Paxos 没关系。

整个 PaxosStore 架构如图:

它包含了很多层,包括缓存和汇聚层、同步复制的组件等。

这一套方案是在线上用了好几千台的,是一个非租约的方案。存储引擎可以自由定制。如果想用大表,那可以用 leveldb。如果想用更强的 LSM,也可以选择。然后我们也有很多 Bitcask 的模型,更适合于内存的 key-value。

由于有几万台机,所以变很重要,我们也基于 BT 做了一套存储方案。它会以园区为根据地,通常一个变更,会以 BT 协议发送到每个园区里,然后园区内部把同机架机器分成一个分组,然后分组内再互传。就我了解,Facebook 和 Twitter、Ebay 都是这样做的。

作者介绍

许家滔,2005 年加入腾讯,见证 QQ 邮箱从百万到数亿用户的整个敏捷开发过程以及架构变迁。2011 年起负责微信后台基础架构,包括分布式存储平台和后台服务框架等,覆盖微信账号 / 消息 / 朋友圈核心存储等,并为公众号 / 微信支付 / 微信企业号等等业务提供组件支持,近两年专注于后台服务质量提升和高性能架构,在数千台机器上面构建了海量高并发 Paxos 存储系统,同时是开源软件 Tencent/libco 负责人。