精华内容
下载资源
问答
  • 容错机制的三个标准
    2019-05-20 21:34:23

    上篇我们大体讲了Flink容错机制的处理方法,和产生checkpoint的机制;这次主要讲一些补充

    一,Flink的checkpoint形式

    checkpoint有两种特殊形式:

    Savepoint:是一种特殊的checkpoint,只不过不像checkpoint定期的从系统中去触发的,它是用户通过命令触发,
    存储格式和checkpoint也是不相同的,会将数据按照一个标准的格式存储,不管配置什么样,Flink都会从这个checkpoint恢复,是用来做版本升级一个非常好的工具;

     External Checkpoint:对已有checkpoint的一种扩展,就是说做完一次内部的一次Checkpoint后,还会在用户给定的一个目录中,多存储一份checkpoint的数据;

    二,Flink的状态划分

    Flink中包含两种基础的状态:Keyed State和Operator State。(Blink现在支持MapState)
    Keyed State

    顾名思义,就是基于KeyedStream上的状态。这个状态是跟特定的key绑定的,对KeyedStream流上的每一个key,可能都对应一个state。所以keyedState支持多种数据结构并支持动态拓展


    Operator State

    与Keyed State不同,Operator State跟一个特定operator的一个并发实例绑定,整个operator只对应一个state。相比较而言,在一个operator上,可能会有很多个key,从而对应多个keyed state。Operator State可以在任意流中使用但支持有限的数据结构(list)

    三,Flink状态持久化方式

    Flink有三种状态持久化的方式

    用户可以根据自己的需求选择,如果数据量较小,可以存放到MemoryStateBackend和FsStateBackend中,如果数据量较大,可以放到RockDB中。RockDB和FsStateBackend都支持将数据写在文件中,而文件路径传给master。

    MemoryStateBackend:state数据存储在内存中,并被保存在一个由多层java map嵌套而成的数据结构中,默认情况下,数据大小不可以超过5MB

    FsStateBackend:策略是当状态的大小小于1MB(可配置,最大1MB)时,会把状态数据直接存储在meta data file中,避免出现很小的状态文件。

    FsStateBackend另外一个成员变量就是basePath,即checkpoint的路径。它会记录状态的文件路径,然后传给master节点,一般用于记录容量比较大的状态。

    它会在本地文件系统中维护状态,KeyedStateBackend等会直接写入本地rocksdb中。同时它需要配置一个远端的filesystem uri(一般是HDFS),在做checkpoint的时候,会把本地的数据直接复制到filesystem中。fail over的时候从filesystem中恢复到本地。

    RocksDB:每个state存储在单独一个columnfamily中,存储状态的文件路径,只用于增量checkpoint。

    更新的时候会直接以key + namespace作为key,然后把具体的值更新到rocksdb中。

    做checkpoint的时候,会首先在本地对rockdb做checkpoint(rocksdb自带的checkpoint功能),这一步是同步的。然后将checkpoint异步复制到远程文件系统中。最后返回RocksDBStateHandle。

    RocksDB克服了HeapKeyedStateBackend受内存限制的缺点,同时又能够持久化到远端文件系统中,比较适合在生产中使用。

    本文章只用于理解,最后补充一下Flink实战的学习资源:

    1. 官网 https://ci.apache.org/projects/flink/flink-docs-stable/

    所有技术栈和接口都在官网文档中有介绍

    2. 数据仓库 https://www.ververica.com/flink-forward-san-francisco-2018

    各种有关于Flink前沿论坛的资料和视频

    3.阿里云flink系列 https://yq.aliyun.com/articles/225623#

    实战内容多一些,链接只是一个例子,教你如何在实战中配置使用

    4. 过往记忆 https://www.iteblog.com/archives/category/flink/

    很多大数据技术的中文文档

    5. Flink论坛 https://cwiki.apache.org/confluence/display/FLINK/Apache+Flink+Home

    可以看到很多课题和问题,以及解决方法

    6. github https://github.com/apache/flink

    源码才是王道

     

    更多相关内容
  • Flink 容错机制

    千次阅读 2022-04-04 09:02:42
    概述 ...  在 Flink 中,有一套完整的容错机制(fault tolerance)来保证故障后的恢复,其中最重要的就是检查点(checkpoint)。在前面,我们已经介绍过检查点的基本概念和用途,接下来我们就深入探

    概述

      在分布式架构中,当某个节点出现故障,其他节点基本不受影响。这时只需要重启应用, 恢复之前某个时间点的状态继续处理就可以了。这一切看似简单,可是在实时流处理中,我们不仅需要保证故障后能够重启继续运行,还要保证结果的正确性、故障恢复的速度、对处理性能的影响,这就需要在架构上做出更加精巧的设计。
      在 Flink 中,有一套完整的容错机制(fault tolerance)来保证故障后的恢复,其中最重要的就是检查点(checkpoint)。在前面,我们已经介绍过检查点的基本概念和用途,接下来我们就深入探讨一下检查点的原理和 Flink 的容错机制。

    一、检查点(Checkpoint)

      在流处理中,我们可以用存档读档的思路,把之前的计算结果做个保存,这样重启之后就可以继续处理新数据、而不需要重新计算了。进一步地,我们知道在有状态的流处理中, 任务继续处理新数据,并不需要“之前的计算结果”,而是需要任务“之前的状态”。所以我们最终的选择,就是将之前某个时间点所有的状态保存下来,这份“存档”就是所谓的“检查点”
    (checkpoint)。
      遇到故障重启的时候,我们可以从检查点中“读档”,恢复出之前的状态,这样就可以回到当时保存的一刻接着处理数据了。
      检查点是 Flink 容错机制的核心。这里所谓的“检查”,其实是针对故障恢复的结果而言的:故障恢复之后继续处理的结果,应该与发生故障前完全一致,我们需要“检查”结果的正确性。所以,有时又会把 checkpoint 叫作“一致性检查点”。

    1. 检查点的保存

      什么时候进行检查点的保存呢?最理想的情况下,我们应该“随时”保存,也就是每处理完一个数据就保存一下当前的状态;这样如果在处理某条数据时出现故障,我们只要回到上一个数据处理完之后的状态,然后重新处理一遍这条数据就可以。这样重复处理的数据最少,完全没有多余操作,可以做到最低的延迟。然而实际情况不会这么完美。

    1.1 周期性的触发保存

      “随时存档”确实恢复起来方便,可是需要我们不停地做存档操作。如果每处理一条数据就进行检查点的保存,当大量数据同时到来时,就会耗费很多资源来频繁做检查点,数据处理的速度就会受到影响。所以更好的方式是,每隔一段时间去做一次存档,这样既不会影响数据的正常处理,也不会有太大的延迟——毕竟故障恢复的情况不是随时发生的。在 Flink 中,检查点的保存是周期性触发的,间隔时间可以进行设置。
      所以检查点作为应用状态的一份“存档”,其实就是所有任务状态在同一时间点的一个“快照”(snapshot),它的触发是周期性的。具体来说,当每隔一段时间检查点保存操作被触发时,就把每个任务当前的状态复制一份,按照一定的逻辑结构放在一起持久化保存起来,就构成了检查点。

    1.2 保存的时间点

      这里有一个关键问题:当检查点的保存被触发时,任务有可能正在处理某个数据,这时该怎么办呢?
      最简单的想法是,可以在某个时刻“按下暂停键”,让所有任务停止处理数据。这样状态就不再更改,大家可以一起复制保存;保存完毕之后,再同时恢复数据处理就可以了。
      然而仔细思考就会发现这有很多问题。这种想法其实是粗暴地“停止一切来拍照”,在保存检查点的过程中,任务完全中断了,这会造成很大的延迟;我们之前为了实时性做出的所有设计就毁在了做快照上。另一方面,我们做快照的目的是为了故障恢复;现在的快照中,有些任务正在处理数据,那它保存的到底是处理到什么程度的状态呢?举个例子,我们在程序中某一步操作中自定义了一个 ValueState,处理的逻辑是:当遇到一个数据时,状态先加 1;而后经过一些其他步骤后再加 1。现在停止处理数据,状态到底是被加了 1 还是加了 2 呢?这很重要,因为状态恢复之后,我们需要知道当前数据从哪里开始继续处理。要满足这个要求,就必须将暂停时的所有环境信息都保存下来——而这显然是很麻烦的。
      为了解决这个问题,我们不应该“一刀切”把所有任务同时停掉,而是至少得先把手头正在处理的数据弄完。这样的话,我们在检查点中就不需要保存所有上下文信息,只要知道当前处理到哪个数据就可以了。
      但这样依然会有问题:分布式系统的节点之间需要通过网络通信来传递数据,如果我们保存检查点的时候刚好有数据在网络传输的路上,那么下游任务是没法将数据保存起来的;故障重启之后,我们只能期待上游任务重新发送这个数据。然而上游任务是无法知道下游任务是否收到数据的,只能盲目地重发,这可能导致下游将数据处理两次,结果就会出现错误。
      所以我们最终的选择是:当所有任务都恰好处理完一个相同的输入数据的时候,将它们的状态保存下来。首先,这样避免了除状态之外其他额外信息的存储,提高了检查点保存的效率。其次,一个数据要么就是被所有任务完整地处理完,状态得到了保存;要么就是没处理完,状态全部没保存:这就相当于构建了一个“事务”(transaction)。如果出现故障,我们恢复到之前保存的状态,故障时正在处理的所有数据都需要重新处理;所以我们只需要让源(source) 任务向数据源重新提交偏移量、请求重放数据就可以了。这需要源任务可以把偏移量作为算子状态保存下来,而且外部数据源能够重置偏移量;Kafka 就是满足这些要求的一个最好的例子, 我们会在后面详细讨论。

    1.3 保存的具体流程

      检查点的保存,最关键的就是要等所有任务将“同一个数据”处理完毕。下面我们通过一个具体的例子,来详细描述一下检查点具体的保存过程。
      回忆一下我们最初实现的统计词频的程序——WordCount。这里为了方便,我们直接从数据源读入已经分开的一个个单词,例如这里输入的就是:

    “hello”“world”“hello”“flink”“hello”“world”“hello”“flink”……
    

    对应的代码就可以简化为:

    SingleOutputStreamOperator<Tuple2<String, Long>> wordCountStream =
    
    env.addSource(...)
    	.map(word -> Tuple2.of(word, 1L))
    	.returns(Types.TUPLE(Types.STRING, Types.LONG));
    	.keyBy(t -> t.f0);
    	.sum(1);
    

      源(Source)任务从外部数据源读取数据,并记录当前的偏移量,作为算子状态(Operator State)保存下来。然后将数据发给下游的 Map 任务,它会将一个单词转换成(word, count)二元组,初始 count 都是 1,也就是(“hello”, 1)、(“world”, 1)这样的形式;这是一个无状态的算子任务。进而以word 作为键(key)进行分区,调用.sum()方法就可以对 count 值进行求和统计了; Sum 算子会把当前求和的结果作为按键分区状态(Keyed State)保存下来。最后得到的就是当前单词的频次统计(word, count),如图所示。

    在这里插入图片描述
      当我们需要保存检查点(checkpoint)时,就是在所有任务处理完同一条数据后,对状态做个快照保存下来。例如上图中,已经处理了 3 条数据:“hello”“world”“hello”,所以我们会看到 Source 算子的偏移量为 3;后面的 Sum 算子处理完第三条数据“hello”之后,此时已经有 2 个“hello”和 1 个“world”,所以对应的状态为“hello”-> 2,“world”-> 1(这里 KeyedState底层会以 key-value 形式存储)。此时所有任务都已经处理完了前三个数据,所以我们可以把当前的状态保存成一个检查点,写入外部存储中。至于具体保存到哪里,这是由状态后端的配置项“ 检查点存储 ”( CheckpointStorage )来决定的,可以有作业管理器的堆内存(JobManagerCheckpointStorage)和文件系统(FileSystemCheckpointStorage)两种选择。一般情况下,我们会将检查点写入持久化的分布式文件系统

    2. 从检查点恢复状态

      在运行流处理程序时,Flink 会周期性地保存检查点。当发生故障时,就需要找到最近一次成功保存的检查点来恢复状态。
      例如在上节的 word count 示例中,我们处理完三个数据后保存了一个检查点。之后继续运行,又正常处理了一个数据“flink”,在处理第五个数据“hello”时发生了故障,如图所示。

    在这里插入图片描述
      这里 Source 任务已经处理完毕,所以偏移量为 5;Map 任务也处理完成了。而 Sum 任务在处理中发生了故障,此时状态并未保存。
    接下来就需要从检查点来恢复状态了。具体的步骤为:
    (1) 重启应用
      遇到故障之后,第一步当然就是重启。我们将应用重新启动后,所有任务的状态会清空。

    在这里插入图片描述

    (2)读取检查点,重置状态
      找到最近一次保存的检查点,从中读出每个算子任务状态的快照,分别填充到对应的状态中。这样,Flink 内部所有任务的状态,就恢复到了保存检查点的那一时刻,也就是刚好处理完第三个数据的时候,如图所示。这里 key 为“flink”并没有数据到来,所以初始为 0。

    在这里插入图片描述
    (3) 重放数据
      从检查点恢复状态后还有一个问题:如果直接继续处理数据,那么保存检查点之后、到发生故障这段时间内的数据,也就是第 4、5 个数据(“flink”“hello”)就相当于丢掉了;这会造成计算结果的错误。
      为了不丢数据,我们应该从保存检查点后开始重新读取数据,这可以通过 Source 任务向外部数据源重新提交偏移量(offset)来实现,如图所示。

    在这里插入图片描述
    (4) 继续处理数据
      接下来,我们就可以正常处理数据了。首先是重放第 4、5 个数据,然后继续读取后面的数据,如图所示。

    在这里插入图片描述
      当处理到第 5 个数据时,就已经追上了发生故障时的系统状态。之后继续处理,就好像没有发生过故障一样;我们既没有丢掉数据也没有重复计算数据,这就保证了计算结果的正确性。在分布式系统中,这叫作实现了“精确一次”(exactly-once)的状态一致性保证。关于状态一致性的概念,我们会在本章后面继续展开。
      这里我们也可以发现,想要正确地从检查点中读取并恢复状态,必须知道每个算子任务状态的类型和它们的先后顺序(拓扑结构);因此为了可以从之前的检查点中恢复状态,我们在改动程序、修复 bug 时要保证状态的拓扑顺序和类型不变。状态的拓扑结构在 JobManager 上可以由 JobGraph 分析得到,而检查点保存的定期触发也是由 JobManager 控制的;所以故障恢复的过程需要 JobManager 的参与。

    3. 检查点算法

      我们已经知道,Flink 保存检查点的时间点,是所有任务都处理完同一个输入数据的时候。但是不同的任务处理数据的速度不同,当第一个 Source 任务处理到某个数据时,后面的 Sum 任务可能还在处理之前的数据;而且数据经过任务处理之后类型和值都会发生变化,面对着“面目全非”的数据,不同的任务怎么知道处理的是“同一个”呢?
      一个简单的想法是,当接到 JobManager 发出的保存检查点的指令后,Source 算子任务处理完当前数据就暂停等待,不再读取新的数据了。这样我们就可以保证在流中只有需要保存到检查点的数据,只要把它们全部处理完,就可以保证所有任务刚好处理完最后一个数据;这时把所有状态保存起来,合并之后就是一个检查点了。这就好比我们想要保存所有同学刚好毕业时的状态,那就在所有人答辩完成之后,集合起来拍一张毕业合照。这样做最大的问题,就是每个人的进度可能不同;先答辩完的人为了保证状态一致不能进行其他工作,只能等待。当先保存完状态的任务需要等待其他任务时,就导致了资源的闲置和性能的降低。
      所以更好的做法是,在不暂停整体流处理的前提下,将状态备份保存到检查点。在 Flink中,采用了基于 Chandy-Lamport 算法的分布式快照,下面我们就来详细了解一下。

    3.1 检查点分界线(Barrier)

      我们现在的目标是,在不暂停流处理的前提下,让每个任务“认出”触发检查点保存的那个数据。
      自然想到,如果给数据添加一个特殊标识,任务就可以准确识别并开始保存状态了。这需要在 Source 任务收到触发检查点保存的指令后,立即在当前处理的数据中插入一个标识字段, 然后再向下游任务发出。但是假如 Source 任务此时并没有正在处理的数据,这个操作就无法实现了。
      所以我们可以借鉴水位线(watermark)的设计,在数据流中插入一个特殊的数据结构, 专门用来表示触发检查点保存的时间点。收到保存检查点的指令后,Source 任务可以在当前数据流中插入这个结构;之后的所有任务只要遇到它就开始对状态做持久化快照保存。由于数据流是保持顺序依次处理的,因此遇到这个标识就代表之前的数据都处理完了,可以保存一个检查点;而在它之后的数据,引起的状态改变就不会体现在这个检查点中,而需要保存到下一个检查点。
      这种特殊的数据形式,把一条流上的数据按照不同的检查点分隔开,所以就叫作检查点的“分界线”(Checkpoint Barrier)。
      与水位线很类似,检查点分界线也是一条特殊的数据,由 Source 算子注入到常规的数据流中,它的位置是限定好的,不能超过其他数据,也不能被后面的数据超过。检查点分界线中带有一个检查点 ID,这是当前要保存的检查点的唯一标识,如图所示。
      这样,分界线就将一条流逻辑上分成了两部分:分界线之前到来的数据导致的状态更改, 都会被包含在当前分界线所表示的检查点中;而基于分界线之后的数据导致的状态更改,则会被包含在之后的检查点中。

    在这里插入图片描述
      在 JobManager 中有一个“检查点协调器”(checkpoint coordinator),专门用来协调处理检查点的相关工作。检查点协调器会定期向 TaskManager 发出指令,要求保存检查点(带着检查点 ID);TaskManager 会让所有的 Source 任务把自己的偏移量(算子状态)保存起来,并将带有检查点 ID 的分界线(barrier)插入到当前的数据流中,然后像正常的数据一样像下游传递; 之后 Source 任务就可以继续读入新的数据了。
      每个算子任务只要处理到这个 barrier,就把当前的状态进行快照;在收到 barrier 之前, 还是正常地处理之前的数据,完全不受影响。比如上图中,Source 任务收到 1 号检查点保存指令时,读取完了三个数据,所以将偏移量 3 保存到外部存储中;而后将 ID 为 1 的 barrier 注入数据流;与此同时,Map 任务刚刚收到上一条数据“hello”,而 Sum 任务则还在处理之前的第二条数据(world, 1)。下游任务不会在这时就立刻保存状态,而是等收到 barrier 时才去做快照,这时可以保证前三个数据都已经处理完了。同样地,下游任务做状态快照时,也不会影响上游任务的处理,每个任务的快照保存并行不悖,不会有暂停等待的时间。
      如果还是拿拍毕业照来类比的话,现在就不需要大家答辩完之后聚在一起排队摆 pose 了——每个人完成答辩之后只要单独照张相,就可以继续做自己的事情去了;最后由班主任老师发挥P 图技能合成合照,这样无疑就省去了大家集合等待的时间。

    3.2 分布式快照算法

      通过在流中插入分界线(barrier),我们可以明确地指示触发检查点保存的时间。在一条单一的流上,数据依次进行处理,顺序保持不变;不过对于分布式流处理来说,想要一直保持数据的顺序就不是那么容易了。
      我们先回忆一下水位线(watermark)的处理:上游任务向多个并行下游任务传递时,需要广播出去;而多个上游任务向同一个下游任务传递时,则需要下游任务为每个上游并行任务维护一个“分区水位线”,取其中最小的那个作为当前任务的事件时钟。
      那 barier 在并行数据流中的传递,是不是也有类似的规则呢?
      watermark 指示的是“之前的数据全部到齐了”,而 barrier 指示的是“之前所有数据的状态更改保存入当前检查点”:它们都是一个“截止时间”的标志。所以在处理多个分区的传递时,也要以是否还会有数据到来作为一个判断标准。
      具体实现上,Flink 使用了 Chandy-Lamport 算法的一种变体,被称为“异步分界线快照”
    (asynchronous barrier snapshotting)算法。算法的核心就是两个原则:当上游任务向多个并行下游任务发送 barrier 时,需要广播出去;而当多个上游任务向同一个下游任务传递 barrier 时, 需要在下游任务执行“分界线对齐”(barrier alignment)操作,也就是需要等到所有并行分区的 barrier 都到齐,才可以开始状态的保存。
      为了详细解释检查点算法的原理,我们对之前的 word count 程序进行扩展,考虑所有算子并行度为 2 的场景,如图所示。

    在这里插入图片描述
      我们有两个并行的Source 任务,会分别读取两个数据流(或者是一个源的不同分区)。这里每条流中的数据都是一个个的单词:“hello”“world”“hello”“flink”交替出现。此时第一条流的 Source 任务(为了方便,下文中我们直接叫它“Source 1”,其他任务类似)读取了 3个数据,偏移量为 3;而第二条流的 Source 任务(Source 2)只读取了一个“hello”数据,偏移量为 1。第一条流中的第一个数据“hello”已经完全处理完毕,所以 Sum 任务的状态中 key 为 hello 对应着值 1,而且已经发出了结果(hello, 1);第二个数据“world”经过了 Map 任务的转换,还在被Sum 任务处理;第三个数据“hello”还在被 Map 任务处理。而第二条流的第一个数据“hello”同样已经经过了 Map 转换,正在被Sum 任务处理。

    接下来就是检查点保存的算法。具体过程如下:

    (1)JobManager 发送指令,触发检查点的保存
      Source 任务保存状态,插入分界线JobManager 会周期性地向每个 TaskManager 发送一条带有新检查点 ID 的消息,通过这种方式来启动检查点。收到指令后,TaskManger 会在所有 Source 任务中插入一个分界线(barrier),并将偏移量保存到远程的持久化存储中,如图所示。

    在这里插入图片描述
      并行的 Source 任务保存的状态为 3 和 1,表示当前的 1 号检查点应该包含:第一条流中截至第三个数据、第二条流中截至第一个数据的所有状态更改。可以发现 Source 任务做这些的时候并不影响后面任务的处理,Sum 任务已经处理完了第一条流中传来的(world, 1),对应的状态也有了更改。

    (2)状态快照保存完成,分界线向下游传递
      状态存入持久化存储之后,会返回通知给 Source 任务;Source 任务就会向 JobManager确认检查点完成,然后像数据一样把 barrier 向下游任务传递。

    在这里插入图片描述
      由于 Source 和 Map 之间是一对一(forward)的传输关系(这里没有考虑算子链 operator chain),所以 barrier 可以直接传递给对应的 Map 任务。之后 Source 任务就可以继续读取新的数据了。与此同时,Sum 1 已经将第二条流传来的(hello,1)处理完毕,更新了状态。

    (3)向下游多个并行子任务广播分界线,执行分界线对齐
      Map 任务没有状态,所以直接将 barrier 继续向下游传递。这时由于进行了 keyBy 分区, 所以需要将 barrier 广播到下游并行的两个 Sum 任务,如图所示。同时,Sum 任务可能收到来自上游两个并行 Map 任务的 barrier,所以需要执行“分界线对齐”操作。

    在这里插入图片描述
      此时的 Sum 2 收到了来自上游两个 Map 任务的barrier,说明第一条流第三个数据、第二条流第一个数据都已经处理完,可以进行状态的保存了;而 Sum 1 只收到了来自 Map 2 的barrier,所以这时需要等待分界线对齐。在等待的过程中,如果分界线尚未到达的分区任务Map 1 又传来了数据(hello, 1),说明这是需要保存到检查点的,Sum 任务应该正常继续处理数据,状态更新为 3;而如果分界线已经到达的分区任务 Map 2 又传来数据,这已经是下一个检查点要保存的内容了,就不应立即处理,而是要缓存起来、等到状态保存之后再做处理。

    (4)分界线对齐后,保存状态到持久化存储
      各个分区的分界线都对齐后,就可以对当前状态做快照,保存到持久化存储了。存储完成之后,同样将barrier 向下游继续传递,并通知 JobManager 保存完毕,如图所示。

    在这里插入图片描述
      这个过程中,每个任务保存自己的状态都是相对独立的,互不影响。我们可以看到,当Sum 将当前状态保存完毕时,Source 1 任务已经读取到第一条流的第五个数据了。

    (5)先处理缓存数据,然后正常继续处理
      完成检查点保存之后,任务就可以继续正常处理数据了。这时如果有等待分界线对齐时缓存的数据,需要先做处理;然后再按照顺序依次处理新到的数据。
      当 JobManager 收到所有任务成功保存状态的信息,就可以确认当前检查点成功保存。之后遇到故障就可以从这里恢复了。
      由于分界线对齐要求先到达的分区做缓存等待,一定程度上会影响处理的速度;当出现背压(backpressure)时,下游任务会堆积大量的缓冲数据,检查点可能需要很久才可以保存完毕。为了应对这种场景,Flink 1.11 之后提供了不对齐的检查点保存方式,可以将未处理的缓冲数据(in-flight data)也保存进检查点。这样,当我们遇到一个分区 barrier 时就不需等待对齐,而是可以直接启动状态的保存了。

    4. 检查点配置

    检查点的作用是为了故障恢复,我们不能因为保存检查点占据了大量时间、导致数据处理性能明显降低。为了兼顾容错性和处理性能,我们可以在代码中对检查点进行各种配置。

    4.1 启用检查点

      默认情况下,Flink 程序是禁用检查点的。如果想要为 Flink 应用开启自动保存快照的功能,需要在代码中显式地调用执行环境的.enableCheckpointing()方法:

    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    // 每隔 1 秒启动一次检查点保存
    env.enableCheckpointing(1000);
    

      这里需要传入一个长整型的毫秒数,表示周期性保存检查点的间隔时间。如果不传参数直接启用检查点,默认的间隔周期为 500 毫秒,这种方式已经被弃用。
      检查点的间隔时间是对处理性能和故障恢复速度的一个权衡。如果我们希望对性能的影响更小,可以调大间隔时间;而如果希望故障重启后迅速赶上实时的数据处理,就需要将间隔时间设小一些。

    4.2 检查点存储(Checkpoint Storage)

      检查点具体的持久化存储位置,取决于“检查点存储”(CheckpointStorage)的设置。默认情况下,检查点存储在 JobManager 的堆(heap)内存中。而对于大状态的持久化保存,Flink 也提供了在其他存储位置进行保存的接口,这就是CheckpointStorage。
      具体可以通过调用检查点配置的.setCheckpointStorage() 来配置, 需要传入一个CheckpointStorage 的实现类。Flink 主要提供了两种 CheckpointStorage:作业管理器的堆内存
    (JobManagerCheckpointStorage)和文件系统(FileSystemCheckpointStorage)。

    // 配置存储检查点到 JobManager 堆内存
    env.getCheckpointConfig().setCheckpointStorage(new JobManagerCheckpointStorage());
    // 配置存储检查点到文件系统
    env.getCheckpointConfig().setCheckpointStorage(new FileSystemCheckpointStorage("hdfs://namenode:40010/flink/checkpoints"));
    

      对于实际生产应用,我们一般会将 CheckpointStorage 配置为高可用的分布式文件系统(HDFS,S3 等)。

    4.3 其他高级配置

    检查点还有很多可以配置的选项,可以通过获取检查点配置(CheckpointConfig)来进行设置。

    CheckpointConfig checkpointConfig = env.getCheckpointConfig();
    

    我们这里做一个简单的列举说明:
    (1) 检查点模式(CheckpointingMode)
      设置检查点一致性的保证级别,有“精确一次”(exactly-once)和“至少一次”(at-least-once)两个选项。默认级别为exactly-once,而对于大多数低延迟的流处理程序,at-least-once 就够用了,而且处理效率会更高。关于一致性级别,我们会在 10.2 节继续展开。
    (2) 超时时间(checkpointTimeout)
      用于指定检查点保存的超时时间,超时没完成就会被丢弃掉。传入一个长整型毫秒数作为参数,表示超时时间。
    (3) 最小间隔时间(minPauseBetweenCheckpoints)
      用于指定在上一个检查点完成之后,检查点协调器(checkpoint coordinator)最快等多久可以出发保存下一个检查点的指令。这就意味着即使已经达到了周期触发的时间点,只要距离上一个检查点完成的间隔不够,就依然不能开启下一次检查点的保存。这就为正常处理数据留下了充足的间隙。当指定这个参数时,maxConcurrentCheckpoints 的值强制为 1。
    (4) 最大并发检查点数量(maxConcurrentCheckpoints)
      用于指定运行中的检查点最多可以有多少个。由于每个任务的处理进度不同,完全可能出现后面的任务还没完成前一个检查点的保存、前面任务已经开始保存下一个检查点了。这个参数就是限制同时进行的最大数量。
      如果前面设置了 minPauseBetweenCheckpoints,则 maxConcurrentCheckpoints 这个参数就不起作用了。
    (5) 开启外部持久化存储(enableExternalizedCheckpoints)
      用于开启检查点的外部持久化,而且默认在作业失败的时候不会自动清理,如果想释放空间需要自己手工清理。里面传入的参数 ExternalizedCheckpointCleanup 指定了当作业取消的时候外部的检查点该如何清理。

    • DELETE_ON_CANCELLATION:在作业取消的时候会自动删除外部检查点,但是如果是作业失败退出,则会保留检查点。
    • RETAIN_ON_CANCELLATION:作业取消的时候也会保留外部检查点。

    (6) 检查点异常时是否让整个任务失败(failOnCheckpointingErrors)
      用于指定在检查点发生异常的时候,是否应该让任务直接失败退出。默认为 true,如果设置为 false,则任务会丢弃掉检查点然后继续运行。
    (7) 不对齐检查点(enableUnalignedCheckpoints)
      不再执行检查点的分界线对齐操作,启用之后可以大大减少产生背压时的检查点保存时间。这个设置要求检查点模式(CheckpointingMode)必须为 exctly-once,并且并发的检查点个数为 1。

    代码中具体设置如下:

    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
    // 启用检查点,间隔时间 1 秒
    env.enableCheckpointing(1000);
    CheckpointConfig checkpointConfig = env.getCheckpointConfig();
    // 设置精确一次模式
    checkpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
    // 最小间隔时间 500 毫秒
    checkpointConfig.setMinPauseBetweenCheckpoints(500);
    // 超时时间 1 分钟
    checkpointConfig.setCheckpointTimeout(60000);
    // 同时只能有一个检查点
    checkpointConfig.setMaxConcurrentCheckpoints(1);
    // 开启检查点的外部持久化保存,作业取消后依然保留
    checkpointConfig.enableExternalizedCheckpoints( ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION);
    // 启用不对齐的检查点保存方式
    checkpointConfig.enableUnalignedCheckpoints();
    // 设置检查点存储,可以直接传入一个 String,指定文件系统的路径
    checkpointConfig.setCheckpointStorage("hdfs://my/checkpoint/dir")
    

    5. 保存点(Savepoint)

      除了检查点(checkpoint)外,Flink 还提供了另一个非常独特的镜像保存功能——保存点
    (Savepoint)。
      从名称就可以看出,这也是一个存盘的备份,它的原理和算法与检查点完全相同,只是多了一些额外的元数据。事实上,保存点就是通过检查点的机制来创建流式作业状态的一致性镜像(consistent image)的。
      保存点中的状态快照,是以算子 ID 和状态名称组织起来的,相当于一个键值对。从保存点启动应用程序时,Flink 会将保存点的状态数据重新分配给相应的算子任务。

    5.1 保存点的用途

      保存点与检查点最大的区别,就是触发的时机。检查点是由 Flink 自动管理的,定期创建, 发生故障之后自动读取进行恢复,这是一个“自动存盘”的功能;而保存点不会自动创建,必须由用户明确地手动触发保存操作,所以就是“手动存盘”。因此两者尽管原理一致,但用途就有所差别了:检查点主要用来做故障恢复,是容错机制的核心;保存点则更加灵活,可以用来做有计划的手动备份和恢复。
      保存点可以当作一个强大的运维工具来使用。我们可以在需要的时候创建一个保存点,然后停止应用,做一些处理调整之后再从保存点重启。它适用的具体场景有:

    • 版本管理和归档存储
      对重要的节点进行手动备份,设置为某一版本,归档(archive)存储应用程序的状态。
    • 更新 Flink 版本
      目前 Flink 的底层架构已经非常稳定,所以当 Flink 版本升级时,程序本身一般是兼容的。这时不需要重新执行所有的计算,只要创建一个保存点,停掉应用、升级 Flink 后,从保存点重启就可以继续处理了。
    • 更新应用程序
      我们不仅可以在应用程序不变的时候,更新 Flink 版本;还可以直接更新应用程序。前提是程序必须是兼容的,也就是说更改之后的程序,状态的拓扑结构和数据类型都是不变的,这样才能正常从之前的保存点去加载。
      这个功能非常有用。我们可以及时修复应用程序中的逻辑 bug,更新之后接着继续处理; 也可以用于有不同业务逻辑的场景,比如 A/B 测试等等。
    • 调整并行度
      如果应用运行的过程中,发现需要的资源不足或已经有了大量剩余,也可以通过从保存点重启的方式,将应用程序的并行度增大或减小。
    • 暂停应用程序
      有时候我们不需要调整集群或者更新程序,只是单纯地希望把应用暂停、释放一些资源来处理更重要的应用程序。使用保存点就可以灵活实现应用的暂停和重启,可以对有限的集群资源做最好的优化配置。

      需要注意的是,保存点能够在程序更改的时候依然兼容,前提是状态的拓扑结构和数据类型不变。我们知道保存点中状态都是以算子 ID-状态名称这样的 key-value 组织起来的,算子ID 可以在代码中直接调用 SingleOutputStreamOperator 的.uid()方法来进行指定:

    DataStream<String> stream = env
    	.addSource(new StatefulSource())
    	.uid("source-id")
    	.map(new StatefulMapper())
    	.uid("mapper-id")
    	.print();
    

      对于没有设置 ID 的算子,Flink 默认会自动进行设置,所以在重新启动应用后可能会导致ID 不同而无法兼容以前的状态。所以为了方便后续的维护,强烈建议在程序中为每一个算子手动指定 ID。

    5.2 使用保存点

      保存点的使用非常简单,我们可以使用命令行工具来创建保存点,也可以从保存点恢复作业。
    (1) 创建保存点
    要在命令行中为运行的作业创建一个保存点镜像,只需要执行:

    bin/flink savepoint :jobId [:targetDirectory]
    

    这里 jobId 需要填充要做镜像保存的作业 ID,目标路径 targetDirectory 可选,表示保存点存储的路径。

    对于保存点的默认路径,可以通过配置文件 flink-conf.yaml 中的 state.savepoints.dir 项来设定:

    state.savepoints.dir: hdfs:///flink/savepoints
    

    当然对于单独的作业,我们也可以在程序代码中通过执行环境来设置:

    env.setDefaultSavepointDir("hdfs:///flink/savepoints");
    

      由于创建保存点一般都是希望更改环境之后重启,所以创建之后往往紧接着就是停掉作业的操作。除了对运行的作业创建保存点,我们也可以在停掉一个作业时直接创建保存点:

    bin/flink stop --savepointPath [:targetDirectory] :jobId
    

    (2) 从保存点重启应用
      我们已经知道,提交启动一个 Flink 作业,使用的命令是 flink run;现在要从保存点重启一个应用,其实本质是一样的:

    bin/flink run -s :savepointPath [:runArgs]
    

      这里只要增加一个-s 参数,指定保存点的路径就可以了,其他启动时的参数还是完全一样的。细心的读者可能还记得我们在第三章使用 web UI 进行作业提交时,可以填入的参数除了入口类、并行度和运行参数,还有一个“Savepoint Path”,这就是从保存点启动应用的配置。

    二、状态一致性

    1. 一致性的概念和级别

      在分布式系统中,一致性(consistency)是一个非常重要的概念;在事务(transaction) 中,一致性也是重要的一个特性。Flink 中一致性的概念,主要用在故障恢复的描述中,所以更加类似于事务中的表述。那到底什么是一致性呢?
      简单来讲,一致性其实就是结果的正确性。对于分布式系统而言,强调的是不同节点中相同数据的副本应该总是“一致的”,也就是从不同节点读取时总能得到相同的值;而对于事务而言,是要求提交更新操作后,能够读取到新的数据。对于 Flink 来说,多个节点并行处理不同的任务,我们要保证计算结果是正确的,就必须不漏掉任何一个数据,而且也不会重复处理同一个数据。流式计算本身就是一个一个来的,所以正常处理的过程中结果肯定是正确的;但在发生故障、需要恢复状态进行回滚时就需要更多的保障机制了。我们通过检查点的保存来保证状态恢复后结果的正确,所以主要讨论的就是“状态的一致性”。

    一般说来,状态一致性有三种级别

    • 最多一次AT-MOST-ONCE
        当任务发生故障时,最简单的做法就是直接重启,别的什么都不干;既不恢复丢失的状态, 也不重放丢失的数据。每个数据在正常情况下会被处理一次,遇到故障时就会丢掉,所以就是“最多处理一次”。
        我们发现,如果数据可以直接被丢掉,那其实就是没有任何操作来保证结果的准确性;所以这种类型的保证也叫“没有保证”。尽管看起来比较糟糕,不过如果我们的主要诉求是“快”,而对近似正确的结果也能接受,那这也不失为一种很好的解决方案。

    • 至少一次AT-LEAST-ONCE
        在实际应用中,我们一般会希望至少不要丢掉数据。这种一致性级别就叫作“至少一次”
      (at-least-once),就是说是所有数据都不会丢,肯定被处理了;不过不能保证只处理一次,有些数据会被重复处理。
        在有些场景下,重复处理数据是不影响结果的正确性的,这种操作具有“幂等性”。比如, 如果我们统计电商网站的 UV,需要对每个用户的访问数据进行去重处理,所以即使同一个数据被处理多次,也不会影响最终的结果,这时使用 at-least-once 语义是完全没问题的。当然, 如果重复数据对结果有影响,比如统计的是 PV,或者之前的统计词频 word count,使用at-least-once 语义就可能会导致结果的不一致了。
        为了保证达到 at-least-once 的状态一致性,我们需要在发生故障时能够重放数据。最常见的做法是,可以用持久化的事件日志系统,把所有的事件写入到持久化存储中。这时只要记录一个偏移量,当任务发生故障重启后,重置偏移量就可以重放检查点之后的数据了。Kafka 就是这种架构的一个典型实现。

    • 精确一次EXACTLY-ONCE
        最严格的一致性保证,就是所谓的“精确一次”(exactly-once,有时也译作“恰好一次”)。这也是最难实现的状态一致性语义。exactly-once 意味着所有数据不仅不会丢失,而且只被处理一次,不会重复处理。也就是说对于每一个数据,最终体现在状态和输出结果上,只能有一次统计。
        exactly-once 可以真正意义上保证结果的绝对正确,在发生故障恢复后,就好像从未发生过故障一样。

      很明显,要做的 exactly-once,首先必须能达到 at-least-once 的要求,就是数据不丢。所以同样需要有数据重放机制来保证这一点。另外,还需要有专门的设计保证每个数据只被处理一次。Flink 中使用的是一种轻量级快照机制——检查点(checkpoint)来保证 exactly-once 语义。

    2. 端到端的状态一致性

      我们已经知道检查点可以保证 Flink 内部状态的一致性, 而且可以做到精确一次(exactly-once)。那是不是说,只要开启了检查点,发生故障进行恢复,结果就不会有任何问题呢?
      没那么简单。在实际应用中,一般要保证从用户的角度看来,最终消费的数据是正确的。而用户或者外部应用不会直接从 Flink 内部的状态读取数据,往往需要我们将处理结果写入外部存储中。这就要求我们不仅要考虑 Flink 内部数据的处理转换,还涉及从外部数据源读取, 以及写入外部持久化系统,整个应用处理流程从头到尾都应该是正确的。
      所以完整的流处理应用,应该包括了数据源、流处理器和外部存储系统三个部分。这个完整应用的一致性,就叫作“端到端(end-to-end)的状态一致性”,它取决于三个组件中最弱的那一环。一般来说,能否达到 at-least-once 一致性级别,主要看数据源能够重放数据;而能否达到 exactly-once 级别,流处理器内部、数据源、外部存储都要有相应的保证机制。在下一节, 我们就将详细讨论端到端的 exactly-once 一致性语义如何保证。

    三、端到端精确一次(end-to-end exactly-once)

      实际应用中,最难做到、也最希望做到的一致性语义,无疑就是端到端(end-to-end)的“精确一次”(exactly-once)。我们知道,对于 Flink 内部来说,检查点机制可以保证故障恢复后数据不丢(在能够重放的前提下),并且只处理一次,所以已经可以做到 exactly-once 的一致性语义了。
      需要注意的是,我们说检查点能够保证故障恢复后数据只处理一次,并不是说之前统计过某个数据,现在就不能再次统计了;而是要看状态的改变和输出的结果,是否只包含了一次这个数据的处理。由于检查点保存的是之前所有任务处理完某个数据后的状态快照,所以重放的数据引起的状态改变一定不会包含在里面,最终结果中只处理了一次。
      所以,端到端一致性的关键点,就在于输入的数据源端和输出的外部存储端。

    1. 输入端保证

      输入端主要指的就是 Flink 读取的外部数据源。对于一些数据源来说,并不提供数据的缓冲或是持久化保存,数据被消费之后就彻底不存在了。例如 socket 文本流就是这样, socket 服务器是不负责存储数据的,发送一条数据之后,我们只能消费一次,是“一锤子买卖”。对于这样的数据源,故障后我们即使通过检查点恢复之前的状态,可保存检查点之后到发生故障期间的数据已经不能重发了,这就会导致数据丢失。所以就只能保证 at-most-once 的一致性语义,相当于没有保证。
      想要在故障恢复后不丢数据,外部数据源就必须拥有重放数据的能力。常见的做法就是对数据进行持久化保存,并且可以重设数据的读取位置。一个最经典的应用就是 Kafka。在 Flink 的 Source 任务中将数据读取的偏移量保存为状态,这样就可以在故障恢复时从检查点中读取出来,对数据源重置偏移量,重新获取数据。
      数据源可重放数据,或者说可重置读取数据偏移量,加上 Flink 的 Source 算子将偏移量作为状态保存进检查点,就可以保证数据不丢。这是达到 at-least-once 一致性语义的基本要求, 当然也是实现端到端 exactly-once 的基本要求。

    2. 输出端保证

      有了 Flink 的检查点机制,以及可重放数据的外部数据源,我们已经能做到 at-least-once了。但是想要实现 exactly-once 却有更大的困难:数据有可能重复写入外部系统。
      因为检查点保存之后,继续到来的数据也会一一处理,任务的状态也会更新,最终通过Sink 任务将计算结果输出到外部系统;只是状态改变还没有存到下一个检查点中。这时如果出现故障,这些数据都会重新来一遍,就计算了两次。我们知道对 Flink 内部状态来说,重复计算的动作是没有影响的,因为状态已经回滚,最终改变只会发生一次;但对于外部系统来说, 已经写入的结果就是泼出去的水,已经无法收回了,再次执行写入就会把同一个数据写入两次。
      所以这时,我们只保证了端到端的 at-least-once 语义。
      为了实现端到端exactly-once,我们还需要对外部存储系统、以及 Sink 连接器有额外的要求。能够保证 exactly-once 一致性的写入方式有两种:

    • 幂等写入
    • 事务写入

    2.1 幂等(idempotent)写入

      所谓“幂等”操作,就是说一个操作可以重复执行很多次,但只导致一次结果更改。也就是说,后面再重复执行就不会对结果起作用了。
      数学中一个典型的例子是,e^x 的求导下操作,无论做多少次,得到的都是自身。
      而在数据处理领域,最典型的就是对 HashMap 的插入操作:如果是相同的键值对,后面的重复插入就都没什么作用了。
      这相当于说,我们并没有真正解决数据重复计算、写入的问题;而是说,重复写入也没关系,结果不会改变。所以这种方式主要的限制在于外部存储系统必须支持这样的幂等写入:比如 Redis 中键值存储,或者关系型数据库(如 MySQL)中满足查询条件的更新操作。
      需要注意,对于幂等写入,遇到故障进行恢复时,有可能会出现短暂的不一致。因为保存点完成之后到发生故障之间的数据,其实已经写入了一遍,回滚的时候并不能消除它们。如果有一个外部应用读取写入的数据,可能会看到奇怪的现象:短时间内,结果会突然“跳回”到之前的某个值,然后“重播”一段之前的数据。不过当数据的重放逐渐超过发生故障的点的时候,最终的结果还是一致的。

    2.2 事务(transactional)写入

      如果说幂等写入对应用场景限制太多,那么事务写入可以说是更一般化的保证一致性的方式。
      之前我们提到,输出端最大的问题就是“覆水难收”,写入到外部系统的数据难以撤回。自然想到,那怎样可以收回一条已写入的数据呢?利用事务就可以做到。
      我们都知道,事务(transaction)是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所做的所有更改都会被撤消。事务有四个基本特性:原子性(Atomicity)、一致性(Correspondence)、隔离性(Isolation)和持久性(Durability),这就是著名的 ACID。
      在 Flink 流处理的结果写入外部系统时,如果能够构建一个事务,让写入操作可以随着检查点来提交和回滚,那么自然就可以解决重复写入的问题了。所以事务写入的基本思想就是: 用一个事务来进行数据向外部系统的写入,这个事务是与检查点绑定在一起的。当 Sink 任务遇到 barrier 时,开始保存状态的同时就开启一个事务,接下来所有数据的写入都在这个事务中;待到当前检查点保存完毕时,将事务提交,所有写入的数据就真正可用了。如果中间过程出现故障,状态会回退到上一个检查点,而当前事务没有正常关闭(因为当前检查点没有保存完),所以也会回滚,写入到外部的数据就被撤销了。
      具体来说,又有两种实现方式:预写日志(WAL)和两阶段提交(2PC
    (1)预写日志(write-ahead-log,WAL)
      我们发现,事务提交是需要外部存储系统支持事务的,否则没有办法真正实现写入的回撤。那对于一般不支持事务的存储系统,能够实现事务写入呢?

    预写日志(WAL)就是一种非常简单的方式。具体步骤是:
    ①先把结果数据作为日志(log)状态保存起来
    ②进行检查点保存时,也会将这些结果数据一并做持久化存储
    ③在收到检查点完成的通知时,将所有结果一次性写入外部系统。

      我们会发现,这种方式类似于检查点完成时做一个批处理,一次性的写入会带来一些性能上的问题;而优点就是比较简单,由于数据提前在状态后端中做了缓存,所以无论什么外部存储系统,理论上都能用这种方式一批搞定。在 Flink 中 DataStream API 提供了一个模板类GenericWriteAheadSink,用来实现这种事务型的写入方式。

      需要注意的是,预写日志这种一批写入的方式,有可能会写入失败;所以在执行写入动作之后,必须等待发送成功的返回确认消息。在成功写入所有数据后,在内部再次确认相应的检查点,这才代表着检查点的真正完成。这里需要将确认信息也进行持久化保存,在故障恢复时,只有存在对应的确认信息,才能保证这批数据已经写入,可以恢复到对应的检查点位置。

      但这种“再次确认”的方式,也会有一些缺陷。如果我们的检查点已经成功保存、数据也成功地一批写入到了外部系统,但是最终保存确认信息时出现了故障,Flink 最终还是会认为没有成功写入。于是发生故障时,不会使用这个检查点,而是需要回退到上一个;这样就会导致这批数据的重复写入。

    (2)两阶段提交(two-phase-commit,2PC)
      前面提到的各种实现exactly-once 的方式,多少都有点缺陷,有没有更好的方法呢?自然是有的,这就是传说中的两阶段提交(2PC)。
      顾名思义,它的想法是分成两个阶段:先做“预提交”,等检查点完成之后再正式提交。这种提交方式是真正基于事务的,它需要外部系统提供事务支持。

    具体的实现步骤为:
    ①当第一条数据到来时,或者收到检查点的分界线时,Sink 任务都会启动一个事务。
    ②接下来接收到的所有数据,都通过这个事务写入外部系统;这时由于事务没有提交,所以数据尽管写入了外部系统,但是不可用,是“预提交”的状态。
    ③当 Sink 任务收到 JobManager 发来检查点完成的通知时,正式提交事务,写入的结果就真正可用了。

      当中间发生故障时,当前未提交的事务就会回滚,于是所有写入外部系统的数据也就实现了撤回。这种两阶段提交(2PC)的方式充分利用了 Flink 现有的检查点机制:分界线的到来, 就标志着开始一个新事务;而收到来自 JobManager 的 checkpoint 成功的消息,就是提交事务的指令。每个结果数据的写入,依然是流式的,不再有预写日志时批处理的性能问题;最终提交时,也只需要额外发送一个确认信息。所以 2PC 协议不仅真正意义上实现了 exactly-once, 而且通过搭载 Flink 的检查点机制来实现事务,只给系统增加了很少的开销。
      Flink 提供了 TwoPhaseCommitSinkFunction 接口,方便我们自定义实现两阶段提交的
    SinkFunction 的实现,提供了真正端到端的 exactly-once 保证。
      不过两阶段提交虽然精巧,却对外部系统有很高的要求。这里将 2PC 对外部系统的要求列举如下:

    • 外部系统必须提供事务支持,或者 Sink 任务必须能够模拟外部系统上的事务。
    • 在检查点的间隔期间里,必须能够开启一个事务并接受数据写入。
    • 在收到检查点完成的通知之前,事务必须是“等待提交”的状态。在故障恢复的情况下,这可能需要一些时间。如果这个时候外部系统关闭事务(例如超时了),那么未提交的数据就会丢失。
    • Sink 任务必须能够在进程失败后恢复事务。
    • 提交事务必须是幂等操作。也就是说,事务的重复提交应该是无效的。

      可见,2PC 在实际应用同样会受到比较大的限制。具体在项目中的选型,最终还应该是一致性级别和处理性能的权衡考量。

    3. Flink 和 Kafka 连接时的精确一次保证

      在流处理的应用中,最佳的数据源当然就是可重置偏移量的消息队列了;它不仅可以提供数据重放的功能,而且天生就是以流的方式存储和处理数据的。所以作为大数据工具中消息队列的代表,Kafka 可以说与 Flink 是天作之合,实际项目中也经常会看到以 Kafka 作为数据源和写入的外部系统的应用。在本小节中,我们就来具体讨论一下 Flink 和Kafka 连接时,怎样保证端到端的exactly-once 状态一致性。

    3.1 整体介绍

      既然是端到端的exactly-once,我们依然可以从三个组件的角度来进行分析:
    (1) Flink 内部
    Flink 内部可以通过检查点机制保证状态和处理结果的 exactly-once 语义。
    (2) 输入端
      输入数据源端的Kafka 可以对数据进行持久化保存,并可以重置偏移量(offset)。所以我们可以在 Source 任务(FlinkKafkaConsumer)中将当前读取的偏移量保存为算子状态,写入到检查点中;当发生故障时,从检查点中读取恢复状态,并由连接器 FlinkKafkaConsumer 向Kafka 重新提交偏移量,就可以重新消费数据、保证结果的一致性了。
    (3) 输出端
      输出端保证 exactly-once 的最佳实现,当然就是两阶段提交(2PC)。作为与 Flink 天生一对的Kafka,自然需要用最强有力的一致性保证来证明自己。

      Flink 官方实现的Kafka 连接器中,提供了写入到Kafka 的 FlinkKafkaProducer,它就实现了TwoPhaseCommitSinkFunction接口:

    public class FlinkKafkaProducer<IN> extends TwoPhaseCommitSinkFunction<IN, FlinkKafkaProducer.KafkaTransactionState, FlinkKafkaProducer.KafkaTransactionContext> {
    ...
    }
    

      也就是说,我们写入 Kafka 的过程实际上是一个两段式的提交:处理完毕得到结果,写入Kafka 时是基于事务的“预提交”;等到检查点保存完毕,才会提交事务进行“正式提交”。如果中间出现故障,事务进行回滚,预提交就会被放弃;恢复状态之后,也只能恢复所有已经确认提交的操作。

    3.2 具体步骤

    为了方便说明,我们来考虑一个具体的流处理系统,由 Flink 从 Kafka 读取数据、并将处理结果写入Kafka。

    在这里插入图片描述
      这是一个 Flink 与Kafka 构建的完整数据管道,Source 任务从Kafka 读取数据,经过一系列处理(比如窗口计算),然后由 Sink 任务将结果再写入Kafka。
      Flink 与Kafka 连接的两阶段提交,离不开检查点的配合,这个过程需要 JobManager 协调各个TaskManager 进行状态快照,而检查点具体存储位置则是由状态后端(State Backend)来配置管理的。一般情况,我们会将检查点存储到分布式文件系统上。

    实现端到端exactly-once 的具体过程可以分解如下:
    (1) 启动检查点保存
      检查点保存的启动,标志着我们进入了两阶段提交协议的“预提交”阶段。当然,现在还没有具体提交的数据。

    在这里插入图片描述
      如图所示,JobManager 通知各个TaskManager 启动检查点保存,Source 任务会将检查点分界线(barrier)注入数据流。这个barrier 可以将数据流中的数据,分为进入当前检查点的集合和进入下一个检查点的集合。
    (2) 算子任务对状态做快照
      分界线(barrier)会在算子间传递下去。每个算子收到 barrier 时,会将当前的状态做个快照,保存到状态后端。

    在这里插入图片描述
      如图所示,Source 任务将 barrier 插入数据流后,也会将当前读取数据的偏移量作为状态写入检查点,存入状态后端;然后把 barrier 向下游传递,自己就可以继续读取数据了。
      接下来 barrier 传递到了内部的 Window 算子,它同样会对自己的状态进行快照保存,写入远程的持久化存储。

    (3) Sink 任务开启事务,进行预提交

    在这里插入图片描述
      如图所示,分界线(barrier)终于传到了 Sink 任务,这时 Sink 任务会开启一个事务。接下来到来的所有数据,Sink 任务都会通过这个事务来写入Kafka。这里 barrier 是检查点的分界线,也是事务的分界线。由于之前的检查点可能尚未完成,因此上一个事务也可能尚未提交;此时 barrier 的到来开启了新的事务,上一个事务尽管可能没有被提交,但也不再接收新的数据了。
      对于 Kafka 而言,提交的数据会被标记为“未确认”(uncommitted)。这个过程就是所谓的“预提交”(pre-commit)。

    (4) 检查点保存完成,提交事务
      当所有算子的快照都完成,也就是这次的检查点保存最终完成时,JobManager 会向所有任务发确认通知,告诉大家当前检查点已成功保存,如图所示。

    在这里插入图片描述
      当 Sink 任务收到确认通知后,就会正式提交之前的事务,把之前“未确认”的数据标为“已确认”,接下来就可以正常消费了。
      在任务运行中的任何阶段失败,都会从上一次的状态恢复,所有没有正式提交的数据也会回滚。这样,Flink 和Kafka 连接构成的流处理系统,就实现了端到端的 exactly-once 状态一致性。

    3.3 需要的配置

    在具体应用中,实现真正的端到端 exactly-once,还需要有一些额外的配置:
    (1) 必须启用检查点;
    (2) 在 FlinkKafkaProducer 的构造函数中传入参数 Semantic.EXACTLY_ONCE;
    (3) 配置Kafka 读取数据的消费者的隔离级别
      这里所说的Kafka,是写入的外部系统。预提交阶段数据已经写入,只是被标记为“未提交”(uncommitted),而 Kafka 中默认的隔离级别 isolation.level 是 read_uncommitted,也就是可以读取未提交的数据。这样一来,外部应用就可以直接消费未提交的数据,对于事务性的保证就失效了。所以应该将隔离级别配置
      为 read_committed,表示消费者遇到未提交的消息时,会停止从分区中消费数据,直到消息被标记为已提交才会再次恢复消费。当然,这样做的话,外部应用消费数据就会有显著的延迟。

    (4) 事务超时配置
      Flink 的Kafka 连接器中配置的事务超时时间transaction.timeout.ms 默认是1 小时,而Kafka 集群配置的事务最大超时时间 transaction.max.timeout.ms 默认是 15 分钟。所以在检查点保存时间很长时,有可能出现 Kafka 已经认为事务超时了,丢弃了预提交的数据;而 Sink 任务认为还可以继续等待。如果接下来检查点保存成功,发生故障后回滚到这个检查点的状态,这部分数据就被真正丢掉了。所以这两个超时时间,前者应该小于等于后者。

    参考资料

    Word版:https://download.csdn.net/download/mengxianglong123/85035166
    PDF版:https://download.csdn.net/download/mengxianglong123/85035172

    需要的可以私信我,免费

    展开全文
  • 代码行为异常容错机制与自我调节

    千次阅读 2020-03-29 21:32:22
    1.5、代码的容错机制与自我调节 2、设计观与方法论 2.1 设计观与代码容错机制、自我调节 2.2 问题是否能够被解决 2.2.1 意识行为是否具有虚拟性 2.2.2 思维是否具有方向性 2.3 问题与问题解决 2.4 软件与问题...

    目录

    1、吧啦吧啦吧啦

    1.1、代码自我意识

    1.2、代码功能安全

    1.3、代码保密性

    1.4、代码执行完整性

    1.5、代码的容错机制与自我调节

    2、设计观与方法论

    2.1 设计观与代码容错机制、自我调节

    2.2 问题是否能够被解决

    2.2.1 意识行为是否具有虚拟性

    2.2.2 思维是否具有方向性

    2.3 问题与问题解决

    2.4 软件与问题

    3、代码容错机制与异常

    4、一个微型实例

    4.1 业务逻辑异常

    4.2 执行控制异常

    4.3 系统级别异常

    5、代码容错机制与自我调节总结


    “行为”“自我调节”是心理学和日常生活中比较常见的用语,意义还是很明确的,那么,无论从信息加工论的认知观来看,还是从类比人的心理活动和行为、对代码构建一种新的描述方式来看,“代码行为异常”“代码自我调节”意义都是很明确的。

     

    1、吧啦吧啦吧啦

    吧啦吧啦吧啦是什么?补习时,学生模仿我讲道理的时候常常会说“吧啦吧啦吧啦”以示略过。在这里,它就算是一些表达我观点概念性词语吧。这些概念就是电影里的快镜、远景或背景旁白,虽然只有廖廖几个,却足以勾画我的道理的基本框架,成为电光石火间的一道思绪掠影。

     

    1.1、代码自我意识

    “代码是具有自我意识的。”

    “年轻人,别太高大上。”

    代码就是一篇由程序设计语言承载的“小说”,或短篇或中篇或长篇,不同于文学小说的是,代码反映的内容只有一项:这个问题怎么解决。说代码具有自我意识,这是基于既把代码当作、又是代码能够成为独立对象的。程序运行过程中对自身、对运行环境的感知与评价就是代码的自我意识。这对于大多数软件来说,是处于正常业务逻辑之外的,也是不必须的。而说到自我意识,就不得不面对终极之问:我是谁?我存在吗?这种终极之问的答案在同一个维度内和同一个形式系统内很难回答:在找到更高级的智慧生命形态之前,我思故我在可以通过感知思考过程证明客观实在的“我”,但是代码却很难从同一代码系统内的指令层面上对为用户服务的自己、虚拟环境下的自己和反编译下的自己做出绝对区分。软件漏洞问题实际上就是涉及方方面面的逻辑检测、异常捕获是否得到充分展开的问题。如果存在充分展开就会在代码层面上形成一个良好的自我意识系统。

     

    1.2、代码功能安全

    软件是为解决一定问题、实现某个业务功能而存在的,软件代码中为确保业务功能得以正常实现的、系统或局部机制下的、在常规业务逻辑流程之外的代码行为都属于代码功能安全范畴,包括代码保密机制、代码执行完整性检查、代码容错机制与自我调节,不包括业务逻辑内的身份验证、使用授权、通信加密等。软件处理的业务越重要,代码功能安全问题就越突出。代码功能安全机制很像带有独立功能的单位的秩序保障机制,有了它,软件的功能才得以平稳而有序地展开,这个世界才会有另一个精彩绝伦的故事,给世间创造一笔笔财富。对于大多数软件来说,一般只要求能实现正常的业务逻辑、一定期限内稳定运行即可,对软件功能安全的意识和要求不高。软件一般会有代码容错和自我调节机制,较高层次的软件支持执行完整性,更高一级的系统(更底层生态的系统)才可能会在前两者基础上要求代码保密。

     

    1.3、代码保密性

    不记得是谁说过,比陆地更广阔的是海洋,比海洋更广阔的是天空,比天空更广阔的是你我的心胸。可是,我就要告诉你,人的心胸再大,也装不下不应该知道的秘密。保密是一个老得掉牙的话题。底层基础的、原理的、原子的以及核心的代码往往是双重非公开,既是用户非公开,也是普通设计员或技术维护员非公开,还是形式非公开。用户眼里,软件永远只是一个个交互界面。而普通设计员或技术维护员,只能访问和修改公开性的授权代码。形式非公开,是指即便通过一定技术手段,也只能得到一行行的形式代码,即以或二进制指令、或机器指令、或字节码等形式而存在的代码,但却只是一种加密形式,非原来代码本身。这也是一种认知非公开。说到这里,我不得不赞叹一句:是谁发明了加密这种极具智慧的方法?比如说,小三(2)班只有李雷会讲英文,一天转来一位新加坡的新生露西。韩梅梅只想和露西讨论习题的时候,会偷偷地把露西拉到一个没人的地方,而当李雷只想和露西讨论的时候,则可以正大光明地于阳光之下用英文和露西交流,其他同学同样只有蒙圈的资格。

     

    1.4、代码执行完整性

    代码执行完整性问题实际上是关于“我是我吗”或“你是谁”的问题,是正常业务逻辑内或代码系统本身在“自我信息感知”这一层面上,确保自己或对方是合乎要求的自己(自然独立实体)或对方(非冒充者)的一种身份确认和保证特性。这对于基于网络传输的软件来说无疑是意义重大的。关于执行完整性,我在网上找到一些资料,一起来感受一下。

    基于完整性的执行控制设计与实现,这是在百度学术中看到的一篇论文,摘要:为防止外来漏洞程序(如来自网络或U盘)威胁系统,本文在LSM框架下构建一个基于完整性的执行控制安全模块,控制这些程序的执行。为区分这些程序,本文定义了完整级,将这些程序标记为最低完整级,普通用户不能执行这些程序,而超级用户可以在构建的沙箱中执行这些程序,从而确保系统的安全。

    从摘要看出,这种执行完整性控制是环境级别的,不是软件自身自发的,而是来自外在运行环境的监测。类似这种设计的检测与控制模式在杀毒软件或其他安全软件中用的比较多。

    一种程序执行完整性保护方法和系统 [发明],这是在专利之星系统中检索到的内容,摘要: 本发明公开一种程序执行完整性保护方法,包括:根据跳转指令,将程序分割成顺序执行的程序块,在每个程序块的起始位置插入校验值标识CK_FLAG和校验值CK_VALUE;在程序运行时,进行实时程序块校验值计算,并和预先计算好的程序块校验值进行比较,在程序块受到破坏或者没有按照预期执行程序块时给出警报信号。

    从摘要来看,这是一种比较典型的完整性检查,可以和某些类型的文件在网络传输中的校验检查做类比,主要检查程序当前的执行指令或模块是否是按照设计者最初的设计进行的。如果是,那么这种执行是完整的,否则是不完整的,即代码的当前行为不是代码本身、而是外在因素发起的,当前的代码行为环境是虚拟而非真实操作系统。执行完整性检查既是一种智慧活,需要一定的策略来确定代码的自然独立和真实性,也是一种智力活,检查策略将以系统性的代码形式体现出来,即能够被翻译成合适的计算科学技术和方法。我能通过我正在思考确定我的客观实在性,但我又是怎么知道我在思考的?很明显,自我感知。也就是说,要把策略转化为形式代码。

    以上两种方式侧重点有所不同,但可大致归类于执行完整性检查,第一种是来自代码外部的,基于代码的完整性作出安全风险评估,并给予不同的执行环境,以尽可能地减少对真实的系统带给破坏性的影响。第二种是来自代码内部的,通过基于设计流的校验码来确定当前的代码行为是发自原始的自身。

     

    1.5、代码的容错机制与自我调节

    我首先来列举一下代码有哪些异常吧。代码异常有两种级别:系统级别和软件级别。软件级别的异常又分执行控制异常和业务逻辑异常。

    系统级别异常:软件正常运行所需要的资源,如权限、内存空间、某种设备资源使用通道、所依赖的系统组件等,操作系统因某种原因无法给予正常提供而导致的访问、修改、运行异常,即代码无法正常进行当前阶段的业务处理行为,实现某个功能。

    执行控制异常:执行控制异常即行为流向异常。在当前阶段,根据不同的输入,代码的行为会按照业务处理逻辑有条不紊地指向某一个方向。也即,在控制系统的协调下数据和实现业务的功能模块有序地交互作用。当这种有条不紊和有序受到破坏,也可看作是实现业务处理或软件本体处理及相关处理要求的“规律”受到破坏,从而引发执行控制异常。

    业务逻辑异常:也可以称为问题求解异常。当实现某个功能所需的条件不成立时,求解过程或业务处理过程脱离预定逻辑,便发生业务逻辑异常。另一种情况是,数据是正确的,条件是提供的,但是解决方法不对。总之,无法实现正常的业务处理,解决问题。

    上面只是代码异常的两种级别和分类,在不同的应用情境、语境下,这些异常都可以包含非常丰富的实际内涵。

    代码行为就如学生行为,但比学生行为要可爱多一点,因为它们都可以是形式化的,界定起来要简单得多。一看就知道小明同学是学霸,可是像小明一样不迟到不早退、上课听讲认真下课作业倍儿棒地完成的同学多了去,然而考出99分的就只有小明。既然出现问题代码行为,自然要进行处理,以最低的代价使影响发生在最小范围,这就是代码的自我调节。而支撑代码自我行为调节的,就是代码容错机制。在设计代码容错机制与自我调节时,要注意的一个平衡策略是:既要能路径通畅,又不能代价太大。一般来说,要代价适中、适度地解决问题。如果解决问题的代价比问题本身的代价还要大,说明解决方法是错误的。如果两者代价相近,说明解决方法是可以更优的。如果解决问题的代价在问题本身代价一半附近甚至更低,则解决方法是较优的。我这里也只是以一种设计型的视角做的一个大致规划,有兴趣的朋友可以进一步深入研究。

    上述内容是我以一种认知的、基于计算机科学的、类比人类自身又以面向对象的设计方法或汇总或建构起来的观点,这样,下面说起来就比较容易。

     

    2、设计观与方法论

    2.1 设计观与代码容错机制、自我调节

    这里本不该用“设计”,太容易让人误解,因为一说到设计,自然以为是产品设计、程序设计、功能设计等等,实际上我这里的“设计”指的是“解决问题”设计,设计观实际上就是解决问题的策略观。因为这不是出于对许许多多的、大量的实例、事实、实验结果的总结、归纳和概括,而是一种个人观点,一种设计型的表述。也就是说,下面所说的是一种日常概念和直觉经验,是在对一些直觉经验或我自己也想不起来源的常识的归纳后做的假设演绎式推理。

    我道理中的解决问题,就是指代码中容错机制和自我调节的具体内涵。也许像我一样,可能会有这么弱弱一问:那不就是软件测试和调试吗?从开发者行为形式和软件工程角度来看,两者之间的确很像但又有本质区别。首先是对象主体不同,前者是代码自身行为,后者是软件测试者行为。其次是内涵不同,前者是体现代码容错机制和自我调节行为的设计的代码表现,是内在系统性的、策略性的,后者是对软件的一种测试与修改,是软件外部行为性的。最后,前者在代码中具体体现在逻辑检查、条件判断、异常捕获与处理的规划、潜在漏洞预测与控制以及更高要求的检查,后者则侧重对不同形态下同类业务问题是否能够正确解决的检查,包括语句、模块、功能、集成等方面的检查与修改。从这个意义上看,两者是有重叠的地方,但是对从具体中抽象出来的“问题”本质的把握,有助于解决问题时打开更广阔的视野从而找到更多通往目标的路径,以寻求更优。

     

    2.2 问题是否能够被解决

    终极之问:问题是否能够被解决?

    什么是问题?怎样才是解决问题?我们可以从几个方面来吧啦一下。

     

    2.2.1 意识行为是否具有虚拟性

    你认为网络具有虚拟性吗?

    玩过游戏的人都知道,游戏是具有虚拟性的。什么是虚拟?虚拟与现实相对,是非现实的,在认知上不把游戏中的一切和现实世界中的一切联系到一起,它们之间不具备相似性,游戏中的一切不能做为现实中任何行为的先行条件。那么网络呢?可能你出身于高教家庭,但网络的出生身份却是一种“超级工具”,它的工具性质使得其上的信息变化和现实中的事物变化能够一一对应起来,正如物理理论中的一些公式变化可以和现实中的事物变化能够一一对应起来,都是一种对应和描述关系。但是,当网络承载的非工具性功能越来越多,便充斥着大量不和现实变化对应的信息,和现实世界既非对应关系,也非描述关系,从这个意义来看,网络不也是一个大型游戏吗?网络和游戏,甚至和所有的被称为“信息系统”的东西一起,都有因建构而虚拟的一面。虚拟的程度如何,取决于网络上的信息能和实际生活有多大的联系,以及我们在现实中进行某个行为时有多以它们为决策参考依据。此外,网络本身也可看成是一个大型“信息系统”,作为人与现实世界交互的中介环节,这种中介性质是虚拟的;网络上各种类型的社区生态,以信息处理作为载体模拟现实世界人际交互的、非实质行动性精神交流,更具虚拟性。

    你认为人类文明具有虚拟性吗?

    前面说了网络的虚拟性,这种虚拟性是建立在网络上呈现的信息与现实生活中行为的联系密切程度、与现实世界变化的一致性程度上的,或者是建立在拟本体性上的。那么人类文明呢?这要从另一个角度来看一致性和本体性。

    我插一句,用自然语言来说明想法就是好,如果是在一个形式系统上讲道理,那么,对于“这要从另一个角度来看”,可能会很难进行下去。

    什么是人类文明?人类文明与自然世界相对应。放眼世界及历史发展,人类文明大致包含三部分:一是人类与人类自身特点和行为特点,二是人与人之间的关系和交互,三是人类创造出来的、能以一定方式表达和呈现的、实质性或非实质性之物,比如高楼大厦,学校,文化艺术品等。这三者组成人类文明,但是,高贵的我们会机智地从中选择一些作为人类文明象征。至于为什么要象征,往进化上说就是群体的代表必然是优质的,往情操上说就是我们人类必须是高贵的。

    什么是自然?如果对于旁观者的我们以人类视角去看动物世界、植物世界和它们所在的生态环境,很容易概括出“自然”:行为表现与意识情感的高度一致就是自然。大多数动物、植物没有意识怎么办?别啊,不能和意识情感保持一致,行为反应、生理反应、自然变化和客体自身构造对应的功能高度一致不也一样的吗?说简单一点,也行:动物、植物的蛮与野就是自然。

    自然性行为不是虚拟的,是实在客观的。

    动值物具有自然“行为”,那是不是说像动物一样就是自然?

    肯定不是的。人类和动物有差别,那么人类的“自然”必然有差别。

    人类和动物有本质的差别吗?“人和动物的本质区别是劳动”、“使用物质工具进行劳动生产,这是人和动物最本质的区别”、“由于人有了以语言为主的第二信号系统,其学习与动物有了本质区别”,这些常见的教科书式说法很容易找到出处。流传范围的宽广可大致看成是客观性的衡量指标,因而上述说法是大众认可的。精神生产工具是中介性质的,它的使用产生出作为人类社会发展结果的高级心理机能,于是人便有不同于动物的由感知觉、情感态度、意识需求、智力活动等构成的独有特性。其中,情感态度、意识需求决定着人的行为方向,智力活动是该方向的选择器和助推器。但我想说的是,人类和动物的最本质差别从外在上区分,是在所有物种的最大公因数式的生存条件下,进行以高级心理机能为依托的建构性语义行为,包括维持的和创造的。

    人类行为有着客观虚拟性和独特自然性。人类行为的独特自然性,指人类行为是意识反应的外在表现,带有主动意识的行为表现是人类行为自然性的体现。并非只有像动、植物一样反应才是自然,因为人类有着和它们的本质区别。那么客观虚拟性呢?一方面,人具有动物不具备的高级心理机能和思维活动,对于从外界接收的信息做出处理变换后才表现在行为上,这种中介性加工变换使得人类的客观世界仅仅是一种语义映像,是对真实客观世界的一种描述、加工和反映。这和我们用一段程序来解决问题的过程没什么实质性的不同。所以人类和环境交互的行为具有客观虚拟性。另一方面,从一致性来看,人类行为分三种:一是无意识行为,这种行为只是缺少某个强烈的方向做为加工信息的指引,并非没有在头脑中加工处理信息;二是有意识行为且知情意行合一,这二种行为最能代表人类行为的自然,虽然它们是客观虚拟性的;三是有意识行为但知情意行不合一,这是人类行为客观虚拟性的典型代表。之所以不合一,是因为人类意识中建构性动机而非机体功能性动机的增多。

    人与人之间的关系和交互是虚拟性的。人类行为产物既包含行为习惯,也包含人与人之间的关系及基于关系的交互作用,也就是你来我往大家一起讨论问题、做作业、交朋友和争第一名。为什么是虚拟的?并不是说人际关系和交互不是客观实在的,而是关系及基于关系的交互中的“关系”,带有浓浓的描述色彩和建构色彩。“色彩好看吗?”“好看,绿绿的太阳比红红的太阳更好看。”“太阳不是红色的吗?”“不,我只能感到绿色,只有绿色对我有意义。”关系是有意义的,所以是虚拟的。说到建构,不得不说到模仿,一种类型的模仿是:一段时间内人较原始的动机行为呈现一定方向性,随着时间推移,当人的自我意识加强或变化后,察觉到这种方向并总结成行事指导原则,指导后续行为,因而上升到意识水平,成为模仿。模仿的结果是,在一个新的层面上再次造就知、情、意、行的合一。另一种类型的模仿是意义建构:说得东式点,就是人法地,地法天,天法道,道法自然,高度建构性的“自然”还有另一层含义:使制度与规则的具体表征物像一个自然物运转。说得西式点,就是以一个客观对象为蓝本,在一代或几代人的理论钻研、技术改进的积累下,最终制造出该对象的人造之物。不管是模仿还是意义建构,都是虚拟的。

    人类行为创造物的虚拟性。基于上面提到的虚拟性,大量的人造物都是某个自然物在人类世界中的呈现与映射,因而也是虚拟性的。

    人类社会这个庞然又复杂的大物能够像一只鸟、一条鱼或者其他的有机体一样自然的运转,得益于一些制度、规则的遵循与维护行为。而这些行为都来源于人类意识。比如,学校之所以是学校,是因为从外在来说,有学校场地和相应人员、设施,从内在来说我们会把这样的场地、人员、设施“视为”学校的一种成分。即便有学校场所、有教室、有大门牌匾,但是人进去后只做一件事,找个空地栽栽树,那么“xx学校”对人类来说依旧只是一个林场,不具备“学校”的意义。那么社会或代表社会文明之物是怎么来的呢?如前所提的模仿与建构:反省、总结外加建构。而且,建构性意识行为也是有其建构依据或方向的,要么来自自身问题的改进,要么来自模仿外在物。对于人类而言,外在物很大的构成比例是动、植物及相关生态组成的自然界。因而人类社会的规则和自然界规律相比,本身具有效仿性,是一种人造规律。

    无论是从信息的加工变换来看,还是从一致性来看;无论是从个体行为来看,还是从群体行为产物及群体的自身联系来看;无论从模仿来看,还是从建构来看,人类文明有一定虚拟性。

    虚拟性并不代表人类文明的非自然。

    所以,作为人类文明原因的人类意识行为具有虚拟性。

    2.2.2 思维是否具有方向性

    人类文明基于意识的维系,是带有虚拟性质的,既模仿大自然建构人造物又有自己独有的特点。这种模仿并非说我们一定要像大自然一样运作,或者是像大自然一模一样的运作,而是说通过人与人之间的有意识的维系性行为,使得人类社会能像大自然中的一个客观存在的、独立又统一的客观对象去运转。人类社会的规则如果要成为规律,是建立在人主动遵守的基础上的。

    人造规律主要还是通过效法自然实现的。既然有效法,就一定有效法对象,因而我们的思维活动必定具有指向性,这种指向性体现在思维活动沿着思维规律向目标变化。思维规律其实就是大脑的物质性构造呈现的功能性规律在我们意识层面上的映射。这种内在规定性与意志、动机特别是建构性动机无关。无论是思维活动效仿的指向性,还是思维活动受思维规律制约,都说明思维活动是有方向、有序的。这种方向和有序便是人能解决问题的基础。

    思维活动是客观或非客观问题的本体,因而,问题不一定能被解决,但是,问题可以被解决。

    2.3 问题与问题解决

    什么是问题?一说到问题,像我或我们这种计算机科学专业的人第一反应是程序中出现的BUG,一个函数应用错误,程序运行时的异常,操作系统中出现的错误。还有一些人的第一反应是最近一段时间内最熟悉事件中出现的、导致事件无法正常完成的感觉印象或客观事实。在心理学中,关于问题是什么早就有讨论了,一百多年来认知心理学这一分支所重点关注的内容中就有解决问题这一项。用我的观点来概括就是,在一个或多个视角下,在一个可描述的系统中,无论是形式符号描述还是自然语言描述,当一个带有系统性语义对象的变化偏离它“行进”的路径即变化规律时,此时的系统状态就是“问题”。问题有三个要素:系统语义对象,系统语义变化,系统语义下的变化规律。其中,系统语义对象是“偏离”系统语义下变化规律的,而系统本身则是或描述或模仿客观自然的,或是建构的。

    自然规律的固有属性是方向、有序,对于人造规律来说也如此。当然还有变换,但这里先不把变换作为强调对象,因为这与具体功能或问题有关。一个对象之所以成为一个有机整体,是因为其各组成部分及之间的联系、交互是客观存在的,这些联系与交互是有方向的、有序进行的。这种解释同样适用人类文明中的某个对象,比如之前说的“学校”,只不过学校的构成和作为一个独立对象的行为规律都是一种人赋予的意义建构和该意义下人、物的组成与行为交互:我认为有这个意义,并始终按这个意义去做,客观世界也能形成相应方向下的变化,这个意义才是客观,我的行为才是规律的。人类的认知、思维活动也是有方向、有序的,这为问题的解决提供现实可能性。也因此,人的思维活动是应遵循也必然遵循思维的基本规范的,如同一律、矛盾律、排中律、充足理由律及其四重根等,这为形式化问题的出现和解决提供现实可能性。总之,问题能够被解决,是可能的。

    那么什么是问题解决?

    回到前面说的“问题”上来,问题解决有两种含义:系统语义对象从初始状态沿变化规律发展为目标状态的“路径”的形成,中间没有非变化规律上的,或者是变化规律上但不是变化规律预期的状态,是一种问题解决。怎么可能出现变化规律下的非预期状态?之前说过意识行为具有虚拟性,再加上认知影响,人类发现的规律并不必然与对应的自然规律保持全面和整体的一致;而人造规律就更次之,解决思维逻辑问题和保证行为一致性的策略问题后也只是达到一个模拟规律级别。问题解决的第二种含义是,在系统语义对象从初始状态发展为目标状态过程中出现非变化规律上的其他状态或变化规律上的非预期状态,即异常状态,通过系统调节或对象自我调节后,异常状态消失,重新回到变化规律上,最终到达目标状态。

    如同最开始说的,我这里的“问题”是一种设计观,因为它带有浓浓的系统性语义,客观世界是不带“意义”的,就只那么存在着,概念和命题都是意识产物,即便最接近它本身的形式系统,也因为各种概念离它渐行渐远。语义是有意识的人类独有的自然现象,但是,它是一种意识产物,对于客观世界而言无所谓问题不问题,问题是一种意识“设计”概念,是唯心(意识产物)和唯物(模仿自然)的。

    2.4 软件与问题

    什么是软件?从软件工程视角来看,对外在的用户而言,软件是一个带有服务周期可实现需求的“生命对象”;对内在的开发人员而言,和基于公理推演出来的一些物理上的、数学上的形式理论系统类似:软件是一个形式功能系统,只不过这个形式系统既有静态信息和静态性质描述,也有动态信息和动态行为描述。数学等领域的形式系统和这不同,它们是人类在探索客观规律后的一种表达型却可推演的形式系统,是静态的,表达于其上的运动规律通过几个关键性指标及其之间的关系来刻画,是简洁、无歧义的形式系统。代码无需将一个动态变化过程进行关键性地标记,当然也可以这样,而是直接把这一过程描述出来,相当于通过操作系统进行翻译的一个机器脚本。这个动态过程可以是规律性的,也可以是步骤性的。软件的代码描述和操作系统的脚本式驱动共同构造出软件这个生命实体。

    软件的方向性和有序性。作为一个带有生命周期的生命实体,软件具有功能需求方面的刚性标准,该标准的达到保障就是代码功能安全体系:代码的保密机制、执行完整性检查、代码容错机制与自我调节,无论用户强调不强调、开发者重视不重视其中一个或多个方面。软件功能的完整实现便是其代码行为方向。

    我们从以下几个方面来分析软件问题来源。

    首先,软件与系统环境。软件是在操作系统下运行的,软件与操作系统的交互——软件访问系统资源和系统查询软件信息——是软件代码行为异常的来源之一。访问的资源得不到应答时,以及操作系统局部异常或关键异常将直接导致代码行为异常。特别是访问的资源越处于系统底层,需要开发者维护的工作和职责就越多,越容易发生致命性异常。

    其次,软件和使用者。软件的功能实现和业务的正常处理,取决于各种形式的数据输入和指令输入,如数据文件的数据、命令行指令或图形界面指令等,因此,如果使用者本身是一种无序的、非理性的、非目标业务预期的,可能会出现以下结果:软件运行时没有任何问题,但是得不到正确的业务结果。或者,这些数据或指令不符合实现某个功能的代码的语义要求或形式要求,将会导致代码行为异常。通俗地说,实实在在的问题是问题,没有相应操作技能的用户也是问题。

    第三,软件定义与功能实现。软件功能的实现即是用户需求的实现,也即解决问题的过程实现,从而定义软件本身。问题求解自然是产生问题的高危地带,同时也是解决问题的必经过程。代码对业务处理的执行控制的复杂程度取决于业务复杂程度,越复杂对开发者的逻辑严密性、处理问题策略性要求越高。对于C/S架构的、支持并发方式处理业务的、带有前后端的软件,软件行为一直保持在自身闭环内很关键,即软件是在和架构内另一端的自己对话及其实现。而如果业务安全度较高,处理的信息非常机密,那么对于多种软件协作处理共同业务时,进行数据通信的各方是否属于同一个业务处理集群以及数据加密很关键。

    后面会通过一个抛研引玉的小例子就上述三个方面进行探讨。

    3、代码容错机制与异常

    首先明白一点,对于软件来说,工具性是第一位的,其他一切机制都是围绕保障这种工具性的实现展开的。因此,在设计过程中,业务逻辑和业务逻辑异常的处理是主线,对于执行控制异常和系统级别的异常是“卡关键点”。当然,对于生态平台性的系统则不是仅仅是卡关键点,机制和策略的系统性很重要。异常发生时,最佳状态是,代码行为在业务处理或问题求解上的流程虽然被暂时中断,但仍旧保持在软件自身内部。通过一番调整,为待重新求解,或撤销之前代码的处理结果回到最初状态,或保存当前的处理状态,或调节自身状态回到业务逻辑上另一“地点”,整个过程代码行为始终保持在自身可控范围,这便是代码的容错机制。

    什么是代码异常?任何在软件运行过程中出现的,业务处理及支持业务处理方面的非预期性行为或反馈都是代码异常问题,这种异常可能是操作系统级的,可能是自身执行控制系统的,也可能是业务逻辑的。

    4、一个微型实例

    问题定义:为数据文件中的每位客户建立指定路径下的工作目录

    问题分析:

    1、用户提供数据文件,文件中有客户信息,客户信息按一定结构存放,每位客户的记录结构为:客户ID,姓名,电话号码(客户相关信息代表)。

    2、用户指定客户工作目录所在路径。

    3、为每位客户建立各自的工作目录。

    4、信息提示与反馈界面。

    问题解决:接受指定路径与数据文件的输入,提取数据文件中相关客户信息(这里是客户的姓名)为其创建目录,输出结果为指定路径下创建好的每位客户的工作目录。

    任何一个能够实现需求的软件都是一个问题得到解决的结果。首先要观察问题情境,理解其各组成部分及之间的联系,按照自顶向下的原则逐层分解,当分解到一定层次后,再分析该层次下制约问题解决的各种条件,理顺它们的先后关系并根据IPO(输入-处理-输出)定义出模块。所有的原子模块和集成模块一起便构成了整个问题的解。

    在观察问题情境时,为避免不必要的业务逻辑异常,要确定好业务内和业务间的关系,以便考虑权限与可见;要确定业务的驱动形式与结果反馈,以便考虑输入、输出的表达和规范;要确定软件的性能需求、运行平台需求、质量需求,以便考虑问题能够得到解决的先行条件和约束要求。这些考虑在需求分析就要体现,形成整套系统,不向后堆积。

     

    4.1 业务逻辑异常

    什么是业务?“业务”二字在营销中出现得多,但现在可泛指一个岗位职责事务,业务能力就是实现一个岗位职责所需的事务处理能力。业务逻辑是指业务处理的信息化、形式化、代码化,实际上就是模仿和虚拟化,只不过模仿者和模仿对象从人与自然变成机器与人。在代码内部,业务逻辑是指确保所有输入都正确(语法正确、语义正确)的基础上,按照要求输出预期结果的整个处理流程。业务逻辑问题体现在业务逻辑异常上。在这个实例中,业务逻辑的异常将集中在输入、处理和输出结果的表达上。先看一种极简单的求解:

    import os
    print("当前工作目录是:{}".format(os.getcwd()))
    filename=input("请输入用户信息文件:\n")    
    with open(filename.strip(),"r") as f:
            targetDir=input("请输入要创建的各客户个人目录的父目录:\n")
            num=int(input("请输入用户名称在信息文件中的第几列(起始列为0)\n"))
            for i in f:
                cc=i.split(",")
                client=cc[num]
                os.mkdir(targetDir+"\\"+name)
    print("生成客户个人目录完成,再见!")

    整个程序流程很简单,业务逻辑也很简单,在提示当前的工作目录后,接收用户输入的数据文件、客户个人目录的父目录、客户姓名所在列号,然后处理数据,这里只是提取相关信息,并生成客户工作目录。

    这个业务逻辑是建立在非常理想状态下的,即:所有的输入都是有意义的,所有的操作都是可实现的,任何时刻程序运行的条件都是满足的,以及数据文件内容是极优质的,即不存在引发更多求解步骤的数据。

    我们可以从行为角度分析这些代码的性质:输入与输出部分,涉及到程序与用户、程序与操作系统的交互,发生异常的可能性受到用户和系统环境双重影响;输入和输出之间的业务逻辑,才是完全受程序自身决定的,对于其间可能发生的异常完全能够在业务流程内解决。

    那么,业务逻辑异常处理从哪里开始?是按输入-处理-输出,还是先实现业务逻辑和软件功能再引入异常处理机制?是一部分一部分的完成还是一起完成?这取决于我们思维能同时关注的维度的数量,也取决于程序的编写习惯,有时候也受限于熊(安全)与鱼(功能)的取舍。总之,对于普通软件来说,功能和问题求解优先,再决定其安全。技术手段无法触碰的范围要制定必要的、保障软件正常运行的软件使用管理规范。用户多做一点软件实现就容易一点,用户少做一点软件就可能要复杂很多,这涉及投入预算与软件规模。特别是带有语义的数据,用户进行语义保证是很简单的事情,若由代码提供语义识别,将复杂许多。

    这里就从业务逻辑实现上入手吧。假设数据文件不是最优质,但也不是很劣质(无序,无意义数据较多),在用户输入指定列以后,面临三种情况:

    第一种情况,指定列不存在:最好解决,交给语句语法检查与异常处理;

    第二种情况,指定列存在但不是客户姓名列:这属于语义正确性检查,较难处理,可以以输入规范和语法规则解决为主,再辅以特征检查(但不是绝对有效),最好是,用户的问题还是交给用户解决:删掉成功建立的目录(但不是预期的)重来一次。

    第三种情况,指定列存在也是客户姓名列但存在相同姓名的客户:这是个问题吗?这取决于创建目录的函数的实现过程:当存在同名客户,上面用到的os.mkdir创建目录时会认为目录已存在而引发异常。这个比较好解决,创建目录前先验证文件中客户姓名数量,若有同名同姓者则以“姓名(客户ID)”作目录名。改进代码如下:

    import os
    from sys import exc_info
    print("当前工作目录是:{}".format(os.getcwd()))
    filename=input("请输入用户信息文件:\n")   
    try:  
        with open(filename.strip(),"r") as f:
                targetDir=input("请输入要创建的各客户个人目录的父目录:\n")
                num=int(input("请输入用户名称在信息文件中的第几列(起始列为0)\n"))
                s=f.read()
                f.seek(0)  #文件指针移回文件首,以便下面的访问
                for i in f:
                    cc=i.split(",")
                    client=cc[num]
                    if s.count(client)>1:
                        sss=targetDir+"\\"+client+"(学号{})".format(cc[0])
                    else:
                        sss=targetDir+"\\"+client
                    os.mkdir(sss)
        print("生成客户个人目录完成,再见!")
    except:
        t,v,tb=exc_info()
        st=str(t)
        t=st[st.find("'")+1:st.rfind("'")]
        print("指定列内容有没有问题?以下是问题详情:\n{}:{}".format(str(t),v))

                                                                                               第一种情况

    第二种情况,我把语义问题完全交给用户解决:删除目录,再运行一次。从图中也可以看出,第三种同名情况能得到处理。

    以上业务逻辑异常类型及相应解决方式并未使运行脱离代码自身范围,这可为得到真正的、所需的“解”提供最大保障,因而是容错的。是的,对于简单实例来说很多人都能做到这一点,这不就是普通意义上的测试与调试吗?但是稍有不同的是,调试会消除程序中的已知问题,而这种代码的容错机制并不是完全这样:各种类型语句或函数抛出的异常要处理,为解决某类局部问题或改变业务的和执行的处理流程而主动抛出的异常要处理,对只在某些极特殊情况才出现但影响极大的异常要处理(这需要一双有洞察力的双眼和严谨的逻辑),某个功能实现过程中、或某个业务处理中出现的异常也要处理。对于一些不可消除的异常,各种可能性级别的异常,代码会如何调整自己的行为呢?这需要形成策略性和系统性机制。并且,当软件一旦规模变大,如何处理突然显现的潜在BUG、关于业务的平衡性策略又是如何,都值得仔细思考。

    4.2 执行控制异常

    执行控制是软件保证正常业务逻辑、业务辅助逻辑、软件本体逻辑正常运行的功能性逻辑。如果所有的业务逻辑和业务辅助逻辑能够缩小为一条语句,那整个软件系统就是一个执行控制逻辑(模块)。执行控制异常是导致软件本体完整性丢失,业务逻辑流无法正常流动,或业务逻辑无法继续进展的异常。

    例子比较简单,就说说输入输出吧。输入和输出分别是一个业务逻辑的基本出发点和归宿点,输入关系到业务逻辑的正常进展和软件功能的具体实现,输出关系到软件本身的执行控制流向,是内在业务逻辑与外在执行控制逻辑的交接点。当然,这个例子中的输入、输出相对于内部业务逻辑的起始点、归宿点来说,更侧重于代码和用户、代码和系统之间的交互。

    先看代码和用户。对于用户闭上眼睛瞎摸乱打导致的问题,全都可以交给语法解决,只要能捕捉语法问题导致的异常即可。需要费心的是语义性输入问题。例如,程序要求用户提供姓名,在用户输入后,程序怎么知道这不是一个姓名的问题。但是,这种问题的解决现在有着非常成熟策略:输入内容的规范化,即按照一定的语义性词法和语法输入。这也是让人多做一点的策略的体现,最典型的莫过于开发软件时需要程序员输入的代表问题解决过程某种程序设计语言了:虽然硬件设备只知道如何做但不知道“自己”在做什么,但是操作系统是清楚的;虽然操作系统只知道如何做但不知道“自己”在做什么,但是软件本身却是清楚的。根据问题定义,要为客户创建工作目录,需要用户提供的信息有:父目录、数据文件、姓名所在列。

    父目录:输入的路径要符合python的语法,要符合创建/检查父目录的语句或函数的使用要求。无需语义处理,完全交给语法及异常捕获解决。如果目录已存在,将忽略创建,否则将创建指定目录;如果父目录路径输入不合法,提示用户重新输入。

    数据文件:分三个层次。首先,输入的内容代表一个文件;其次,是一个数据型文件;最后,是符合业务处理逻辑“期待”的数据文件。最后一个层次实际上是对前两个层次的统合,因此以它为标准即可。是否是合法文件交由语法解决,是否是符合业务逻辑期待,我这里交由用户自己解决,即假定它是符合期待的。

    姓名所在列:内容合法性交由语法解决,语义合法性交由用户解决。

    代码对用户的要求是:形式合法,意义合法。用户对代码执行结果的要求是:符合形式,符合预期。代码运行结果是:在指定位置出现每位客户的工作目录。

    再看代码和系统。代码通过打开文件与创建目录来和操作系统交互。打开文件用于获取数据,有可能被创建的父目录用于存放各个客户工作目录,因而这两者都可以看作是一种“输入”。而将创建的客户工作目录则是代码的“输出”行为。

    打开文件:这里涉及的问题主要有路径是否合法、软件是否有权限进行这样的操作、文件打开过程中是否出错等。这些都可以交由异常捕获解决。

    创建目录:目录创建实际上也是路径合法性与语句要求问题、执行权限问题、执行过程问题。

    尽可能地对正常逻辑和语法问题之外的潜在的运行时错误或逻辑设计漏洞作应对策略,是代码容错机制的强力保证,也是代码具有较强容错性的体现。改进代码如下。

    import os
    from sys import exc_info
    from time import sleep
    while True:
        print("当前工作目录是:{}".format(os.getcwd()))
        targetDir=input("请输入要创建的各客户个人目录的父目录:\n") 
        try:   
            os.mkdir(targetDir)
            break
        except FileExistsError:
            print("父目录已存在,将略过创建!继续...")
            break
        except:
            print("路径名称输入有误或其他未知错误,3秒后请重输!")
            sleep(3)
            os.system("cls") 
    filename=input("请输入用户信息文件:\n")
    try:      
        with open(filename.strip(),"r") as f:
            num=int(input("请输入用户名称在信息文件中的第几列(起始列为0)\n"))
            s=f.read()
            f.seek(0)
            for i in f:
                cc=i.split(",")
                client=cc[num]
                if s.count(client)>1:
                    sss=targetDir+"\\"+client+"(学号{})".format(cc[0])
                else:
                    sss=targetDir+"\\"+client
                try:
                    os.mkdir(sss)
                    print("客户目录 【{}】 创建成功!".format(sss))
                except FileExistsError:
                    print("客户目录 【{}】 已存在,将略过创建!".format(sss))
            print("\n生成客户个人目录完成,再见!")
    except:  
            t,v,tb=exc_info()
            st=str(t)
            t=st[st.find("'")+1:st.rfind("'")]
            print("文件名或路径名是对的吗?指定列内容有没有问题?以下是问题详情:\n{}:{}".format(str(t),v))
    

                                                                                      父目录路径不对:为空

                                          父目录路径不对:os.mkdir要求最后一级目录前的路径是存在的,第一个test不存在。

                                                                                 父目录指定为当前目录,已存在

                                                                                     创建目录时没有权限

                                                                                              父目录成功创建

                                                                                           输入的数据文件问题

                                                                                          客户工作目录成功创建

                                                                            重复性任务,将忽略新的创建过程

                                                             运行中,指定目录权限“突然”发生变更,没有访问权限

    以上每项测试代表一个代码异常及相应处理行为,各项测试之间没有关系,测试条件分别单独设计。

    4.3 系统级别异常

    上面所说的执行控制异常中,输入、输出所涉及的权限问题也可以划分至系统级别的异常中来。由于输入、输出导致的异常或多或少地影响到正常业务处理逻辑流动,暂且先归入执行控制异常。系统级别的异常多来至软件外部操作系统环境的变更,和正常的问题求解过程无关,和语法、形式无关,和业务逻辑设计无关,和潜在的BUG、运行时动态出现的问题有关,和软件运行时的操作系统环境有关,和隐性问题的敏锐觉察与应对策略有关,和运行的时机有关。

    第一种情况,和业务逻辑相关的系统环境变化。比如,在即将创建客户工作目录时,父目录由于某种原因被删除,从而导致创建失败。这种问题是异常捕捉机制设计策略决定的。我们可以有两种方式来推进业务逻辑的向前进展:一是代码行为流向的安全职责分布到各阶段,每完成一个阶段,就表示处于该阶段的风险完全得到消除,后续阶段将假定前面阶段不再有遗留问题,不再做重复性检查。二是每次在关键操作处检查执行该操作需满足的各种条件,代码行为流向安全由局部的统一处理保证。两种方式都各有优劣。这里用的是第一种策略:创建客户工作目录时,应该具备的条件在前面都得到保证后才能从业务逻辑上行进到这里,在代码内部,此时是可以断定是没有创建失败风险的。

    第二种情况,我是我自己吗?也即代码是否是在一个真实的操作系统下运行,代码的行为是否是自发的。例如,通过专业手段,直接跳过前面各阶段的代码执行某种操作,此时,由于软件本体安全壁垒被攻破,运行时的各种临时数据可以被随意修改,既能引发很深的信息安全问题(比如说代码处理授权范围有限制或非公开的某些信息),执行该操作的各种先决条件也得不到可靠保证。这种问题只能通过执行完整性相关的检查机制解决。

    第三种情况,软件正常运行在操作系统下的刚性系统条件变更。比如,软件依赖的某个系统组件、服务出错,突然断电等。解决这种问题要把握两个方向:一个方向是在软件内部要有运行状态保存策略,以便再次运行时继续执行;或者状态清理策略,以便再次运行时在多方认同的基础上重新求解问题。另一个方向是对于软件的部署实施要有危机处理策略,比如为计算机加装一台安全电延装置,当断电发生时,能为计算机提供一段安全时间续电,并向计算机发出已断电信号;而当软件接收到已断电信号后,进行相应处理后立即结束运行。改进代码如下,这里主要解决的是第一种情况,第二、三种情况有机会、有条件时再一起探讨,但是会对第二种情况做一个模拟演示:

    import os
    from sys import exc_info
    from time import sleep
    while True:
        print("当前工作目录是:{}".format(os.getcwd()))
        targetDir=input("请输入要创建的各客户个人目录的父目录:\n") 
        try:   
            os.mkdir(targetDir)
            break
        except FileExistsError:
            print("父目录已存在,将略过创建!继续...")
            break
        except:
            print("路径名称输入有误或其他未知错误,3秒后请重输!")
            sleep(3)
            os.system("cls")  
    filename=input("请输入用户信息文件:\n")
    flag=[]
    try:      
        with open(filename.strip(),"r") as f:
            num=int(input("请输入用户名称在信息文件中的第几列(起始列为0)\n"))
            s=f.read()
            f.seek(0)
            for i in f:
                cc=i.split(",")
                client=cc[num]
                if s.count(client)>1:
                    sss=targetDir+"\\"+client+"(学号{})".format(cc[0])
                else:
                    sss=targetDir+"\\"+client
                try:
                    os.mkdir(sss)
                    print("客户目录 【{}】 创建成功!".format(sss))
                    flag.append(1)
                except FileExistsError:
                    print("客户目录 【{}】 已存在,将略过创建!".format(sss))
                    flag.append(1)
                except FileNotFoundError:
                    print("客户目录 【{}】 创建失败!请检查路径是否发生变更!".format(sss))
                    flag.append(0)
            if sum(flag):
                print("\n生成客户个人目录完成,再见!")
            else:
                print("\n生成客户个人目录失败,再见!")
    except:  
            t,v,tb=exc_info()
            st=str(t)
            t=st[st.find("'")+1:st.rfind("'")]
            print("文件名或路径名是对的吗?指定列内容有没有问题?以下是问题详情:\n{}:{}".format(str(t),v))
    

                                      假设运行前text\text目录已存在,在运行至输入指定列时将最后的text删除。

                                                        最后一级的text即父目录被删除后,创建客户工作目录失败

     

    改动程序,在flag=[]语句下加一句:haha=False,然后在try中flag.append(1)下面加一句:if client=="王五":

                        if haha==True:os._exit(-1)

    其他不变:

    import os
    from sys import exc_info
    from time import sleep
    while True:
        print("当前工作目录是:{}".format(os.getcwd()))
        targetDir=input("请输入要创建的各客户个人目录的父目录:\n")
        try:   
            os.mkdir(targetDir)
            break
        except FileExistsError:
            print("父目录已存在,将略过创建!继续...")
            break
        except:
            print("路径名称输入有误或其他未知错误,3秒后请重输!")
            sleep(3)
            os.system("cls")  
    filename=input("请输入用户信息文件:\n")
    flag=[]
    haha=False
    try:      
        with open(filename.strip(),"r") as f:
            num=int(input("请输入用户名称在信息文件中的第几列(起始列为0)\n"))
            s=f.read()
            f.seek(0)
            for i in f:
                cc=i.split(",")
                client=cc[num]
                if s.count(client)>1:
                    sss=targetDir+"\\"+client+"(学号{})".format(cc[0])
                else:
                    sss=targetDir+"\\"+client
                try:
                    os.mkdir(sss)
                    print("客户目录 【{}】 创建成功!".format(sss))
                    flag.append(1)
                    if client=="王五":
                        if haha==True:os._exit(-1)
                except FileExistsError:
                    print("客户目录 【{}】 已存在,将略过创建!".format(sss))
                    flag.append(1)
                except FileNotFoundError:
                    print("客户目录 【{}】 创建失败!请检查路径是否发生变更!".format(sss))
                    flag.append(0)
            if sum(flag):
                print("\n生成客户个人目录完成,再见!")
            else:
                print("\n生成客户个人目录失败,再见!")
    except:  
            t,v,tb=exc_info()
            st=str(t)
            t=st[st.find("'")+1:st.rfind("'")]
            print("文件名或路径名是对的吗?指定列内容有没有问题?以下是问题详情:\n{}:{}".format(str(t),v))
    

    这样改动为了在代码内部模拟一次系统环境变更导致的异常:当运行到为客户王五生成工作目录后,突然遇到某个系统性原因导致代码运行终止。

    这是一个模拟环境,模拟代码的非自发运行。实际上这是一个调试状态,通过调试环境模拟代码在虚拟环境下的非自发运行,此时可以说代码并非操作系统下真实的自己。在原有的业务逻辑中,haha一直处于False状态,但是可以通过局部变量窗口手动修改它的值为True,来改变代码的执行控制流程和业务逻辑流程,并以此来模拟操作系统环境变更导致的异常终止。此时业务处理状态为:

                                                               这是假设系统故障排除后,重新(接着)运行的结果

     

    最后,将刚才用于模拟环境异常的语句去掉,再加入界面信息,改进代码如下:

    import os
    from sys import exc_info
    from time import sleep
    while True:
        print("当前工作目录是:{}".format(os.getcwd()))
        targetDir=input("请输入要创建的各客户个人目录的父目录:\n") 
        try:   
            os.mkdir(targetDir)
            break
        except FileExistsError:
            print("父目录已存在,将略过创建!继续...")
            break
        except:
            print("路径名称输入有误或其他未知错误,3秒后请重输!")
            sleep(3)
            os.system("cls") 
    filename=input("请输入用户信息文件:\n")
    print("\n"+"*"*20)
    flag=[]
    try:      
        with open(filename.strip(),"r") as f:
            num=int(input("请输入用户名称在信息文件中的第几列(起始列为0)\n"))
            print("\n"+"*"*20)
            s=f.read()
            f.seek(0)
            for i in f:
                cc=i.split(",")
                client=cc[num]
                if s.count(client)>1:
                    sss=targetDir+"\\"+client+"(学号{})".format(cc[0])
                else:
                    sss=targetDir+"\\"+client
                try:
                    os.mkdir(sss)
                    print("客户目录 【{}】 创建成功!".format(sss))
                    flag.append(1)
                except FileExistsError:
                    print("客户目录 【{}】 已存在,将略过创建!".format(sss))
                    flag.append(1)
                except FileNotFoundError:
                    print("客户目录 【{}】 创建失败!请检查路径是否发生变更!".format(sss))
                    flag.append(0)
            print("*"*20)
            if sum(flag):
                print("\n生成客户个人目录完成,再见!\n目标文件夹:")
            print(os.listdir(targetDir))        
            print("*"*20)
    except: 
            if flag==[]:
                print("(此处空空)\n"+"*"*20)
            print("\n生成客户个人目录失败,再见!")
            t,v,tb=exc_info()
            st=str(t)
            t=st[st.find("'")+1:st.rfind("'")]
            print("文件名或路径名是对的吗?指定列内容有没有问题?以下是问题详情:\n{}:{}".format(str(t),v))
            print("*"*20)
    

                                                                                          任务成功完成

                                                                                   创建任务失败

                                                                                         任务再续成功

                                                                                            数据文件打开失败

     

    5、代码容错机制与自我调节总结

    从信息加工作视角来看,计算机和人具有“大致相仿”的宏观结构和行为,但我们仍将计算机看作是“计算机”;但是对于具有人工智能系统的机器设备,我们会进一步称之为“人”:机器人。可见,不是它真的是人,只是我们将它与人的相似性特征和它一起视为“人”。基于这种观点,从独立客体角度出发,我们可以认为软件是一个客观存在的对象,那么对于软件内部的代码,则是主体的构成。因此,代码是有自我意识的。

    软件存在的最大价值是其功能,具有工具性。如果说,以前的物质工具和精神工具还比较楚汉分明的话,那么软件的出现则模糊了这两者之间的界线。为确保软件功能得以正常实现,软件具备代码安全系统,具体来说是代码保密性、代码执行完整性的保障机制和代码容错机制与自我调节。为确保某个原子性功能,也就是在一个维度下无法再细分的功能对外是不可见、不可知的,需要将相关代码进行保密。当软件运行时其内部代码间的执令流动、数据流动都是软件本身发起的,且基于软件自身“控制机构”的“设计规律”,那么这些代码行为都是预期的。预期的代码行为可以保证信息机密、业务机密、代码机密不被非授权性访问、修改。如何保证所有的代码行为都是预期的,这是代码执行控制和完整性检查要做的事。一旦软件功能在实施过程中遇到内部代码执行困难,将触发代码容错机制,并进行自我调节,使得代码的执行控制始终保持在从软件运行到软件结束(此时不一定完全解决某个问题)这一功能实现期内,从而保证业务处理涉及的方方面面都能得到最优处理。

    在代码容错机制展开时,可能会遇到业务逻辑异常、执行控制异常和系统级别异常。业务逻辑异常大多是得到某个结果的先决条件不具备,或者进行业务处理的方法、策略不对,分类、分级的分情况处理不全面、不完善。执行控制异常往往导致代码行为偏离业务逻辑流和软件本体功能流的正常“跑道”,这个时候就要小心部署,以期能够捕获各种代码行为异常,并进行相应调节,回到软件的本次功能实现。系统级别异常是非软件自身能够掌控的,要做的是及时得到系统异常信号,最大限度的完成相关的撤销清理或状态保存工作。

    代码行为异常的几个经典原因,一是来自用户。用户在使用时要提供一定的所需数据和执行条件,这就涉及到数据的合法性、所需性与执行业务处理的合理性。我们还是遵循那个原则:过程性的问题交由技术解决,意义性的问题将由用户解决。用户操作规范少一点,代码就要复杂一点;用户操作规范多一点,代码就要精准一点。这是策略性的问题,具体问题具体分析,但是,适当的语义定义和尽可能地让用户做选择而不是提供数据是解决用户、软件之间交互问题的通用做法。说到用户和软件的使用交互,由于用户操作的不规范、业务不熟悉等原因,有时能导致用户引发的业务逻辑异常,这种异常往往是BUG(漏洞)性的。二是来自系统。软件与软件之间资源使用同步问题、业务处理同步问题,软件与操作系统交互问题,操作系统的环境突变问题,都是代码容错机制和自身调节重点关注对象,特别是涉及到操作系统底层资源,越底层关注度就应越大。上面两种代码行为异常原因侧重于策略和机制层面,实际上因为开发者对某个函数或方法等理解不够而导致代码行为异常的情况并不少见。

    代码异常发生的时机也是应当重视的。即便语法没问题,业务处理逻辑没问题,解决方法很正确,仍然可能会有想像不到的异常“蹦”出来。在设计代码时,一些问题交由语法解决,如捕获语法问题导致的异常;一些问题交由语义解决,如按一定规范分析某个对象;一些问题交由先行条件检查解决,如检查处理该业务时是否具备条件,但是,还是有一些看似相当正常的代码属于认知误区或问题解决误区之内的,认知不清晰、忽略对它们的关注(态度)、检查的忽略(思维缺陷)本身就是潜藏BUG的关键原因。这类错误往往能通过词法、语法检查和编译,但是在代码运行的时候非必然性的、偶然性地概率性地出现。一旦出现,对于软件自身来说就是致命性的,对于重要业务处理来说是代价惨重的。

    虽然代码容错机制与自我调节很重要,但也不必逐变量逐表达式地生抠死查。一是根据开发财务预算,二是根据业务的重要性完全可以采用不同等级策略。对于代价不是体现在一定数量级的人力、物力、资源上,不是体现在严格授权等级的越级上,不是体现在信息机密上,不是体现在非常关键非常重视的数据上,等等,完全可以将保障软件安全运行的重任交给用户。

    最后再看一下通过python函数装饰器实现的相似异常的统一管理。统一管理异常的方法有很多种,就不展开了。

    from os import mkdir,getcwd
    from sys import exc_info
    def Boom_and_Watch(objF):
        def wrap(*args,**kwargs):
            of=True
            try:
                of=objF(*args,**kwargs)            
            except FileExistsError:
                print("目录 【{}】 已存在,将略过创建!".format(args[0]))
                of=False
            except FileNotFoundError:
                print("系统找不到路径:{}".format(args[0]))
                of=False
            except:
                t,v,b=exc_info()
                t=str(t)
                t=t[t.find("'")+1:t.rfind("'")]
                print("{}:{}".format(t,v))
                of=False
            return of
        return wrap
    mkdir=Boom_and_Watch(mkdir)
    open=Boom_and_Watch(open)
    ################################################################
    print("当前工作目录是:{}".format(getcwd()))
    targetDir=input("请输入要创建的各客户个人目录的父目录:\n")
    mkdir(targetDir)
    filename=input("请输入用户信息文件:\n")
    num=int(input("请输入用户名称在信息文件中的第几列(起始列为0)\n"))
    print("\n"+"*"*20)
    ################################################################
    try:
        with open(filename.strip(),"r") as f:
                s=f.read()
                f.seek(0)
                for i in f:
                    cc=i.split(",")
                    client=cc[num if 0<=num<len(cc) else 1]
                    sss=targetDir+"\\"+client+"(学号{})".format(cc[0])
                    if s.count(client)==1:
                        sss=sss[0:sss.find("(")]
                    if mkdir(sss)==None:
                        print("目录 【{}】 创建成功!".format(sss))
    except:pass
    print("*"*20)
    print("\n本次创建客户个人目录任务完成,再见!")
    

     

    展开全文
  • 一文读懂区块链共识及其容错机制

    千次阅读 2019-01-14 18:30:49
    翻译 | shawn编辑 | 波波区块链是一分布式和去中心化的系统,这意味着它需要一种可以追踪当前系统官方状态的方法。由于区块链可以包括金融交易和商业协议,因此所有相关...

    640?wx_fmt=gif

    640?wx_fmt=png

    翻译 | shawn

    编辑 | 波波


    区块链是一个分布式和去中心化的系统,这意味着它需要一种可以追踪当前系统官方状态的方法。由于区块链可以包括金融交易和商业协议,因此所有相关方必须在协议条款上保持一致,这点非常重要。


    群体共识是区块链概念中最重要的一个支柱。  


    这听起来可能有点古怪,区块链确实是个相当低效的系统:因为系统内的多个节点、成千上万台计算机全都需要重复相同的工作,同时保管同一数据的相应副本。


    让计算机这样干活效率肯定很低,我们之所以能容忍这种效率,是因为它们这样做能带来我们想要的另外一种东西——共识:如果能让多数计算机就什么记录是真相达成共识,那么我们就可以建立一种计算信任的方式,前提是这些记录没有经过任何篡改或修订。


    因此,共识机制可以说是区块链的基石。


    目前,让区块链节点达成共识的方法有好多种。


    基本上,我们可以这样理解共识机制:把区块链中的每个区块看作为一张纸,每一行记录一个交易。我们每人都在自己的纸上记录交易,当纸写满后,我们作为一个集体必须要相互检查对方的记录有没有出错,然后选出大部分人都同意的某一页纸或某一版本的纸,也就是达成共识。


    共识机制指的是如何让网络上的计算机节点来达成共识的方法。它要确保网络上的所有节点都能确认交易,并对各自账本(ledger)的顺序、存在与否能够达成共识。


    在加密数字货币中,这一过程非常重要。因为它可以有效防止出现双花(Double Spending)现象或其他无效数据被写入区块链账本,也就是所有链上交易的数据库。


    在去中心化的世界中,共识机制及其重要:因为这里不存在任何信任中介,只有去信任化的价值流动,也就是真正的去中心化。


    在一个区块中达成共识的方法有好多种,不同的情况可以使用不同的解决方案。


    不同共识机制之间的主要差别是它们授权和奖励交易确认工作的方式上。


    多数区块链生态的共识机制都是不同共识机制的混合体:它们各有用处,非此即彼的选择容易出现偏颇。


    “工作量证明(Proof of Work)”是最为古老、最为常用、最为有名的共识机制


    过去十年来,工作量证明(PoW)的表现一直不错。它最初是用在比特币中,后来每一个主要公链与大多数的私链都在使用。


    但现在,我们开始意识到工作量证明的局限性,特别是其速度及可扩展性。


    目前,工作量证明的交易速度是 20-50 TPS,但相比 Mastercard 、Visa 等现代支付网络就是小巫见大巫了,后者的交易速度可以高达 7000 TPS。


    欲能跟传统技术相竞争,区块链必须要加快交易速度。


    为了实现这一点,开发者提出了很多种替代的共识方法。


    其中,Tangle 使用的是无区块解决方案,目前已投入应用。而新兴的权益证明(Proof of Stake)、活动证明(Proof of Activity)方案目前还处于试验阶段。


    深入讨论之前,我们需要重点理解共识机制的概念:


    让所有节点(可能数万个)全部重复相同的工作,然后让它们定期就大多数节点所选出的事实达成共识,这可以赋予区块链高度的可信性,使其成为一个安全的记录存储系统。


    640?wx_fmt=jpeg


    共识


    1. 去中心化区块链需要用一种方法让用户就区块链的当前状态达成一致意见。

    2. 区块链网络中的共识基于稀缺性。对稀缺资源的控制越多,对区块链操作的控制就越多。

    3. 几种不同的共识机制方案:

           常见的共识机制:

            ·  工作量证明和权益证明

           其他共识机制:

             ·  委任权益证明(Delegated Proof of Stake)

             ·  实用拜占庭容错算法 (Practical Byzantine Fault Tolerance)

             ·  有向无环图(Directed Acyclic Graph,DAG)

             ·  消逝时间量证明(Proof of Elapsed Time)


    区块链是一个共享、同步的历史账本,这意味着针对什么应该计入官方记录、什么不应该计入,它需要在某一时刻做出最终决定。


    由于区块链是去中心化的,因此不存在可以批准和最终确定区块链区块内容的“高级权威”。


    比特币区块链的缔造者中本聪为实现共识所创造的方法基于稀缺性。在某种意义上,区块链共识算法可以归结为某种投票机制:用户的票数与其控制的有限资源的数量有关。根据经济学上的供求法则,收集某一种资产直到在市场上达到控制性的份额,这样该资产的价格就会上涨,要实现同等程度的控制的代价就会变得无比昂贵。


    中本聪为比特币发明了一种共识算法,名为“工作量证明”。后来,为了适用不同的用例开发者又发明了其他几种共识算法。包括:权益证明、委托权益证明、实用拜占庭容错算法、有向无环图、消逝时间量证明、权威证明、活动证明、烧毁证明(Proof of Burn)、能力证明(Proof of Capacity)等。


    最常使用的共识算法是工作量证明和权益证明。其他共识算法大部分还只是处于讨论阶段的概念或理论。


    640?wx_fmt=jpeg


    工作量证明


    计算资源:工作量证明基于计算资源的稀缺性。

    激励:工作量证明区块链上的矿工比赛找出某一加密货币问题的可接受答案

    51%安全:工作量证明假设任何人控制的网络资源不得超过总资源的一半。


    在工作量证明(PoW)中,想要生成下一个区块(赢得奖励)的区块链网络用户叫做矿工。为了赢得挖掘某一区块的权利,矿工们要比赛解决某一密码学“难”题。我们之前讨论过,这种数学“难”题只能通过随机猜测解出来。当某一矿工找到某一可接受的答案后,他们会生成一个区块,并在网络上传播,对这个区块以及其中的所有交易进行最终确定。


    工作量证明通过选出一个随机猜测解决的问题,以实现对计算资源稀缺性的利用。矿工一次可以给出很多猜测,猜测的数量没有限制。


    因此,工作量证明鼓励矿工使用尽可能多的矿机,以尽可能扩大他们第一个找到问题答案的几率。由于矿机要花钱购买和运行,用户投资挖掘设备的钱有限,因此他们对区块链网络的控制权就受到了限制。


    工作量证明的安全性基于这样一个假设:任何人控制的网络资源不得超过区块链挖掘网络总计算资源的一半。


    如果有一个矿工在解决区块链上每个区块的挖掘难题时,他第一个找出可接受答案的概率比其他人都要高,那么这个矿工对这个区块链网络就有绝对的控制权。这就破坏了区块链去中心化的特性。


    总的来说,矿工们通过解决数学难题来证明交易,确保整个网络的安全。


    矿工们用运行计算周期的GPU或专用集成电路(ASIC)芯片解决某一数学难题,希望找到预先提供给他们的一组数字 。这组数字称为“目标”,是一个前面是很多零的SHA-256 哈希算法。每产生2016个区块(大约每两周),“目标”的“难度”(比特币世界中的另一个术语)就会调整一次,以确保矿工大约每十分钟可以挖掘一个区块。


    找到这个“目标”需要三个主要素:


    • 一个nonce值(一个只使用一次的值)

    • 交易数据

    • 之前的block hash


    经过一次又一次的hash(组合),而且nonce值每一次都会改变,直到这三个要素生成的hash比“目标”提供的hash低。


    当某个矿工达到了这个“目标”,他就会获得一笔交易费和挖掘奖励(在本文发出时为12.5比特币)。这个奖励每生成210000个区块(大约要4年时间)就会减少一半。


    接下来,这个矿工要向所有其他矿工告知,他已经达到了设定的“目标”并确认了这个区块。完成这一步后,他就会挖掘下一个区块。


    640?wx_fmt=jpeg


    权益证明


    稀缺性通货:权益证明基于给定货币的稀缺性。    

    权益:下一个区块的生成者由所有拥有权益的用户伪随机选出。被选定的几率与用户所拥有权益的大小大致成比例。  

    经济可行性:权益证明假设所有用户不得对加密货币有绝大多数的控制。否则,他们就会被选中来生成大部分区块,拥有对加密货币的绝对控制权。


    权益证明(PoS)网络中的用户可以“锁定(stake)”或承诺不使用所拥有的代币。这样,他们就有机会被选中创造或“打造”新区块,并获得相应奖励。区块创造者是从所有将部分资产作为权益的用户中伪随机选出的,这个选择过程是有偏向性的,权益越大,被选中的几率就越大。


    例如,假设一个轮子被分成了四部分,每个部分的大小与权益的大小成正比。选择下一位区块创造者的方法是,旋转这个轮子看哪一部分最终转到最上面。在权益证明中,每个用户都有一个该轮子的复制品,由于所有用户都是同步的,因此每个用户都可以独立地决定选择过程,得出相同结果。所以说权益证明用的是一个伪随机选择过程,而不是随机选择过程。


    在权益证明中,攻击者需要控制足够多的权益货币,以确保被选中创造每一个区块。由于加密货币是一种有限资产,买那么多货币是非常费钱的,因此攻击权益证明系统在经济上是不可行的。


    在权益证明中,是“证明者”在“打造区块”,而非“矿工”在“挖矿”。


    在解决PoW这样的问题时,不存在计算大量的数学难题的计算循环。在PoS中,证明人在全网络发送一种特殊的交易,然后该交易被锁入到保证金(也称为“证明池”)中,这个过程叫做“锁定(staking)”。


    一旦证明人提供了一定数量加密货币的所有权,算法就会每隔一个时间段伪随机选择一个证明人(比如一个时间段可能是10秒),并赋予该证明人创造一个区块的权利。这个区块必须指向之前最长链末尾的某些区块,随着时间的推移,大多数区块会汇集成一条不断成长的链。


    当选出证明人后,为了创造一个区块,证明人需要证明一组交易。


    完成证明后,证明人就会收到之前存入的资金,以及该区块的交易费(当加密货币供应暂时价格过高时给的是奖励)。


    如果证明人不守信,为欺诈性交易提供证明,他们就会失去目前所质押的权益,并且被踢出之后的验证人池(失去创造区块的权利)。这种内置的激励机制可以确保证明人创造的是有效的交易,而不是欺诈性交易。



    特定共识机制的应用


    以太坊


    目前,以太坊的共识机制用的工作量证明。Casper是以太坊从工作量证明转向权益证明的一个计划。


    以太坊的原始设计是使用工作量证明作为共识机制,直到权益证明应用(代号Casper)出现被迫的硬分叉。这个硬分叉嵌入了以太坊协议中,随着工作量证明问题的难度的缓慢增加,一直到解决这个问题所需的时间长到了这个工作量证明不能使用的程度,这个硬分叉才会完成。权益证明不像工作量证明那样需要那么多的能量消耗,它是一种更加可持续、可扩展的共识机制。


    比特币


    比特币的共识机制使用的是中本聪发明的工作量证明。


    Hyperledger Fabric


    Hyperledger Fabric 将共识机制进行了分解,用户可以根据他们的特定用途挑选共识算法。


    Hyperledger Fabric 通过定义一个执行所有与共识有关的操作的“orderer 模块”,有意避免了将共识机制硬编码到协议中。因为这样,Hyperledger Fabric 的用户可以根据他们的特定用途挑选共识算法,无需被迫进行大规模的代码编辑。


    Corda


    每个 Corda 网络都有一个由独立方组成的公证服务,来批准使用任何适用共识算法的区块。


    标准的区块链模型将交易捆绑到区块中,然后再由网络整体地对所有交易进行最终确认。Corda 则不同,它的网络包含一个或多个由数位独立方组成的公证人节点。Corda 上的交易由公证人用使用诸如Raft算法的多方数字签名系统进行最终确认。



    区块链上的容错


    区块链是一个分布式、去中心化的系统,它始终维持一个共享的状态。共识算法的作用是使网络可以对这个状态达成共识,有时候这种共识可能实现不了。因此,容错是区块链技术的一个重要环节。


    区块链在设计上是低效且冗余的,这赋予了系统的不变性,以及极高的容错能力。


    区块链的核心运行着一个点对点网络架构,这个架构中的每个节点与其他节点都是对等的。


    和传统的客户—服务商模型不同,区块链的每个节点同时充当客户和服务商的角色。


    640?wx_fmt=png


    因此,我们在网络层面上保留了这种冗余设计,让所有节点执行相同的任务。


    和任何点对点系统一样,区块链有着非常高的容错能力。事实上,如果一个区块链系统有两个或两个以上节点在线,它仍然可以正常工作。


    当考虑到主要公链有那么大的规模时,你就会领会到内置容错的神奇之处。


    以比特币为例:


    这是一个包含3万以上节点的区块链,这些节点在每个区块上都要达成共识。只要两个或两个以上节点在线并且能够相互沟通,系统就能正常工作。


    这让系统在节点在线和离线、网络传输等问题上极高的容错能力, 同时使得区块链可以在网络和电力基础设施不理想的环境中作为一个很好的平台。网络上的节点可以一会儿离线一会儿在线,当一个节点在离线一会儿后恢复在线,这时它就要进行同步工作,从其他节点获取其离线时错过的所有数据,然后就可以像其他节点那样在线工作了。


    这跟中心化系统非常不同。


    在传统的客户—务商模型中,如果服务商处于离线状态,客户就无法获取他们所要求的数据,或者执行他们想要执行的操作。


    640?wx_fmt=jpeg


    这种情况在区块链上是不存在的。


    回顾其他点对点解决方案的历史,例如:BitTorrent 或 Napster,我们看到官方在使这些网络离线时遇到了极大的困难。


    这归咎于点对点架构的容错能力。


    事实上,我们在最近发生的阿拉伯之春(Arab Spring)中也看到了这种情况,当时埃及政府决定关闭整个国家的网络一晚上。


    在24小时之内,埃及就重新恢复了网络,通过一种名为“网状网络”的网络共享机制连接到了因特网上,核心上这种机制只是一种分享网络连接的点对点方法。


    点对点解决方案可以提供极高的容错能力和可靠性,正因如此我们才选择在它的基础上构建区块链这样的平台。


    如果你正在寻找一个可以提供极高容错能力的解决方案平台,如果你计划在基础设施不太理想的区域或者在节点频繁在线和离线的环境中部署一个解决方案,区块链可能是一个非常好的平台。



    拜占庭将军问题


    我在《 Byzantine Fault Tolerance In a Nutshell》这篇文章中进行了详细探讨。


    • 几位将军需要就一个进攻协作计划达成共识

    • 其中一位或一位以上将军可能是叛徒

    • 所有将军将会遵守多数决定规则,但是可能会影响这个规则


    区块链在设计上具备拜占庭容错


    • 所有节点都是不可信的

    • 所有节点必须就区块链的官方状态达成一个共识。


    拜占庭将军问题演示了多方在只能进行一对一沟通而且沟通是不可信的基础上达成共识的难度。在这个故事中,几位拜占庭的将军带领自己的军队共同包围了一座城池。如果他们一起进攻或一起撤退,他们就是安全的;但是如果有人进攻有人撤退,那么他们就会被消灭。


    将军们只能通过信使来进行沟通,而信使可能会遭遇拦截,被迫传递假消息,而且一位或一位以上将军可能是叛徒。他们的目标是在可能有存在叛徒和虚假消息的情况下,找到一种就进攻策略达成共识的方法。并且假设所有将军都遵守多数决定达成的共识。只要三分之二的将军不是叛徒,那么拜占庭将军问题就是可解决的。


    区块链在设计上就是为了拜占庭容错,这就意味着网络会在部分成员可能不诚实的情况下,就区块链的官方状态达成一个共识。拜占庭将军问题的解决方案是低效的,因此区块链需要在不执行完整个解决方案的条件下实现共识的可信性。


    640?wx_fmt=jpeg


    工作量证明提供了一种博弈理论式的分布式共识算法。



    • 工作量证明激励网络上的挖矿节点向计算循环的热力学界限靠近。这样做可以激励去中心化,因为相比在一个中心位置发散挖矿节点产生的热量,在两个分离的位置上的散热效果更好。注意,这种去中心化说的只是物理和网络分布上的去中心化。

    • 工作量证明实际证明了博弈力量可以应用到协议中,因为工作量证明成功地在网络上的每个可能操作上都应用了激励机制。

    • 工作量证明成功的原因在于它的无优化和无近似。


    无优化意味着无法规避挖矿协议为了担保某一区块必须进行的 hashing。


    无近似意味着拥有一个区块的过程是两极化的:有或者没有。


    权益证明提供了一种内部实验博弈理论的共识算法


    • 它依赖的是拥有用于作为权益的加密货币的节点。权益证明奖励将大多数加密货币作为权益的节点,而不是奖励计算能力最强的节点。

    • 权益证明要求每个验证节点都是可识别的。这是因为作为权益的加密货币可能会因为任何不当行为而被没收。工作量证明并不要求节点的可识别性。

    • 在权益证明中,与你竞争的是一组数量更多的节点。和工作量证明不同,权益证明的staking中不存在交易争执,前者需要购买挖矿硬件、连接因特网、提供冷却系统等。



    工作量证明 VS 权益证明(总结)


    工作量证明是最古老、最原始的共识协议,它的十周年纪念日都已经过了。它从 2009 年开始在比特币中应用,是最早投入应用的共识协议,它的使用效果非常好。


    虽然出现了一些针对在区块链上构建的智能合约和解决方案的黑客攻击和漏洞袭击,但是在创造了超过一万亿美元市值的 10 年间,没人能够成功地利用工作量证明本身获利,这足以证明这种协议的安全性和可靠性。


    但是,工作量证明本身存在的某些缺点和争议正将我们的目光引向替代的共识机制,例如权益证明。


    交易处理能力是工作量证明的缺点之一。理想状况下,工作量证明在全世界范围内每秒可以处理 10 到 20 笔交易。


    虽然听起来很多,但是和传统的交易处理系统相比仍然有很大的差距,举例来说,Visa 的支付处理系统每秒最多可以处理 7 万比交易。


    为了让区块链继续作为一个成功的解决方案平台,我们需要寻找其他的共识机制,它们的交易处理速度应该能够和传统的技术相抗衡。


    工作量证明本身还存在一些其他争议,其中一个争议就是中心化问题。


    从本文中读者可以看出,区块链的要素之一就是去中心化的理念——区块链网络中的任何中央机构、中介或参与者都不得拥有过多的权力。


    我们现在在工作量证明上看到的是军备竞争,挖矿者们为了尽可能提供挖矿效率,不惜购买各种各样的设备、专用硬件、专用挖矿装置,以获得竞争优势。其中获利最多的就是可以使用便宜电力的大型数据中心。


    现在,比特币网络几乎 80% 的处理能力集中在中国的 6 个大型数据中心。


    很多区块链支持者和纯粹主义者认为,一个地理区域不应该享有如此程度的集中(我本人也同意这个观点)。


    我们希望通过权益证明来改变这种现状,权益证明去除来群体共识的工作模块,取而代之的是一种专门形式的博弈机制。


    权益证明的设计理念是,如果不需要用专用的硬件来达成共识,使用任何设备的任何人都可以参与共识决策过程。


    这些设备可能是你书桌上很久没用过的旧笔记本电脑、智能手机、平板电脑、或 iPad。


    权益证明可以实现更广、更去中心化的设备选择范围,让更大的网络规模参与共识过程。


    交易处理速度和去中心化理念是转向权益证明的巨大驱动力。权益证明未来发展如何,我们将拭目以待。


    今年我们将看到以太坊引入 Casper,如果它被证明是一个成功的共识机制,届时更多的区块链将会追随以太坊的脚步。


    当你听到有人争论工作量证明和权益证明的优缺点时,你要理解这些争论的意义,为什么我们要从工作量证明转到权益证明。我们正试图克服工作量证明的一些大局限,尽管直至今日它表现得一直不错。


    我们正试着找到一种交易处理速度可以和传统技术相提并论的共识机制,这种机制去除了工作量证明本身存在的一些中心化特性。





    参考链接:

    • https://medium.com/coinmonks/blockchain-consensus-and-fault-tolerance-in-a-nutshell-765de83b8d03

    • https://medium.com/coinmonks/byzantine-fault-tolerance-in-a-nutshell-bc7762ffb996




    — END —


    640?wx_fmt=png


    推荐阅读:



    点击“阅读原文”,打开 APP 获取更多干货哟!


    好文!必须点好看
    展开全文
  • 拜占庭容错三个基本理论 1) CAP理论 - "如果网路发生阻断(partition)时,你只能选择资料的一致性(consistency)或可用性(availability),无法两者兼得"。论点比较真观:如果网路因阻断而分隔为二,在其中...
  • Flink状态管理和容错机制介绍

    千次阅读 2018-08-25 17:00:45
      本文来自8月11日在北京举行的 Flink Meetup会议,分享来自于施晓罡,目前在...状态管理和容错机制实现; 阿里相关工作介绍; 一.有状态的流数据处理 1.1.什么是有状态的计算    计算任务的结果不仅仅依赖于...
  • 数据和计算系统如何容错

    千次阅读 2021-12-13 16:02:56
    容错是大规模数据系统和计算系统的必备功能,不能容错的分布式系统基本没有可用性。大家可能觉得高质量的系统错误率没有那么高,实质上系统的故障率总是随着系统规模和复杂程度增加。笔者读书的时候曾经听过一位参与...
  • 版权声明:本套技术专栏是作者(秦凯新)平时工作的总结和升华,通过从真实商业环境抽取案例进行总结和...在批处理过程中,数据是划分为块分片去完成的,然后每一Task去处理一分片。当分片执行完成后,把输出...
  • 服务器运行时,如果出现故障,服务器如何保持正常运转,保证业务不会中断?这时候就需要“容错服务器...一、容错服务器和普通服务器的区别:1、“普通服务器”:我相信大家已经有基础概念,注意力放在大件:CPU\...
  • 一、简介HDFS——Hadoop分布式文件... HDFS中包含三个主要的进程:NameNode,DataNode,SecondaryNameNode。这三个进程一般是分布式不同的主机上,所以一般习惯上是用进程的名字称呼节点 二、特点 优点: 支持超大
  • HttpClient连接池及重试机制

    千次阅读 2019-07-30 16:50:47
    HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,基于标准的java语言。 功能介绍 支持HTTP和HTTPS协议 实现了HTTP的方法,...
  • 单体应用的故障影响面很大,而分布式系统中由于故障的影响面可以被隔离,所以影响面较小,但是因为服务多,出故障的...而容错设计就是面向失败设计一非常重要的环节,常见的容错设计有:流量控制、服务熔断、服务降级
  • 首先用户程序 (JobClient) 提交了一 job,job 的信息会发送到Job Tracker中,Job Tracker 是 Map-reduce 框架的中心,他需要与集群中的机器定时通信 (heartbeat), 需要管理哪些程序应该跑在哪些机器上,需要管理.....
  • 数据模型的评价标准 种事实表(设计原则,设计方法) 1.种事实表概述 2.种事实表对比 3.事实表设计 8 大原则 4.事实表设计方法 第一步:选择业务过程及确定事实表类型 第二步:声明粒度 第步:确定维度 ...
  • 在比特币区块链系统中有Nonce(难度随机数),它由若干前导零构成,前面零的个数越多,整个网络的难度系数就越大,之前也说过一节点想要获得记账的权利,则根据PoW的规则,算力最大最先算出某个hash值的节点...
  •  我们提出的弹性分布式数据集(RDDs),是一让程序员在大型集群上以容错的方式执行基于内存计算的分布式内存抽象。RDDs受启发于两类使用当前计算框架处理不高效的应用:迭代算法和交互式数据挖掘工具。这二者在...
  • 区块链之旅()智能合约与共识机制

    千次阅读 多人点赞 2021-05-09 00:00:08
    过程: 评价标准 共识机制的对比 PoW PoS DPoS Raft PBFT 场景 公链 公链、联盟链 公链、联盟链 联盟链 联盟链 去中心化 完全 完全 完全 半中性化 半中性化 记账节点 全网 全网 选出若干代表 选出一Leader 动态...
  • 到目前为止,我们已经了解了 Git 基本的运作机制和使用方式,学习了许多 Git 提供的工具简单且有效地使用它,可以高效地帮助我们工作,提升我们的效率。 如果还不清楚 Git 的基础使用流程、分支的管理、托管服务器的...
  • 我们在人工智能领域研发、开发和应用时,需要有专业性的标准标准化的指标、或评估规范等,在网上查找了一段时间,发现很多资料并不可靠,或简单介绍,让人头大呀;这里整理了一些比较专业的资料,分享给大家。 ...
  • 微服务ServiceMesh及种服务发现机制

    千次阅读 2020-01-23 14:14:39
    在模式三中,业务代码进程(相当于主驾驶)共享一代理(相当于边车),代理除了负责服务发现和负载均衡,还负责动态路由、容错限流、监控度量和安全日志等功能,这些功能是具体业务无关的,属于跨横切面关注点(Cross-...
  • 牛逼!Java 从入门到精通,超全汇总版

    万次阅读 多人点赞 2021-05-06 19:40:33
    学习 Java,我将会从三个阶段来介绍,分为初级、中级和高级 Java 基础 什么是初级 Java 的水平呢?我认为就是理解 Java 的语法规则、语言特性,这么说有点干瘪,直接上思维导图! 就这一张图,如果你能把图中内容都...
  • 摘要:本篇文章是【区块链之技术进阶】的第七篇文章,在之前的文章中咱们多多少少提及了共识算法等相关知识,但是却没有具体地更加深入地了解,本文就为大家掰一掰区块链共识机制与分布式一致性算法,两者究竟有什么...
  • 这道题没有一个标准的答案,它涉及很多的知识点,面试官会通过这道题了解你对哪一方面的知识比较擅长,然后继续追问看看你的掌握程度。当然本篇博客的分析也只是我的一些个人理解,从前端的角度出发,具体地分析从...
  • 区块链共识机制

    千次阅读 2022-03-13 16:25:30
    1、区块链的特点:去中心化、开放性、独立性、安全性 2、区块链关键技术:加密技术、分布式存储、共识机制、智能合约 3、区块链的网络特点:网络路由...6、共识机制解决的2问题:分布式网络的数据一致性,数据有效性
  • 一般而言,在介绍区块链时经常会提到两例子:一是由古老的记账模式延伸到分布式账本,二是...区块链上的共识机制主要解决由谁来构造区块,以及如何维护区块链统一的问题,该问题的理论基础是拜占庭容错(Byza...
  • 无人驾驶汽车系统入门(十二)——ROS2概述和实践入门(一) ROS可以说是目前机器人相关开源社区最流行的项目之一,它是一易用且完备的机器人开发框架、生态乃至社区,海量的机器人开源项目(涵盖感知、规划、...
  • 实用拜占庭容错(PBFT)算法详细介绍

    千次阅读 2020-03-18 11:29:54
    本文主要讲述实用拜占庭容错算法(PBFT)的算法部分。
  • 在这一章节中,我将以Aviv Zohar和Jonatan Sompolinsky设计的GHOST(Greedy Heaviest-Observed Sub-Tree,又称幽灵协议)原理在权益证明中的应用来重新叙述Casper机制诞生的故事。 我称它为“友好的Ghost”(编者按...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 38,824
精华内容 15,529
热门标签
关键字:

容错机制的三个标准

友情链接: 51+sl811.rar