首页 > 代码库 > 面对软件错误构建可靠的分布式系统(阅读笔记)
面对软件错误构建可靠的分布式系统(阅读笔记)
阅读笔记
joe Armstrong
段先德 译
核心问题:如何在存在软件错误的情况下编写具有合理行为的软件 ,如何避免像死锁。死循环等问题
ERLANG的世界观,一切皆进程。将任务分离成层次化的一系列任务,强隔离的进程负责来执行每个具体化的任务,进程之间不共享状态(实际上ETS跨越了这个准则)。
只能通过消息传递来通信,必须注意进程消息的堵塞问题
工作者和监督者构成一个完整的系统,监督者的作用就是监控整个系统的运行状况。并对突发情况进行可靠的处理。
behaviour库的设计思想就是将程序的并发处理和顺序化处理完全的分开,不理解
正文:
实际上运行的软件不可避免的会存在一些错误,必须在程序有错误这一前提下进行工作来构建可靠的系统
爱立信(Ericsson)在电信方向的探索诞生出来一门语言,一门设计方法学,一个健壮的系统程序库(OTP)
语言和标准库是两种不一样的概念,其解决的问题是不一样的
一、绪论
系统存在不完善的环节,但是我们希望我们的系统可靠,矛盾的要求,满足的条件两种解决方案,语言本身或者是方法库
在ERLANG中,对于上述的问题的解决方式采用语言+库一起作用的方式来解决,满足在程序有错误的情况下,我们的代码依旧会按照合理的方式运行
本文不关注算法细节、硬件假设、软件工程方面的要求
而是关注与软件的容错性在语言、库、操作系统方面的要求,纯消息传递式的语言,其它语言可能会存在共享内存的方式来解决这样的问题
速错进程的概念,现成的错误存在互相传递的隐患,会破坏掉系统内部的坚固性
Ericsson 的计算机科学实验室CSLab ->computer science laboratory
plain old telephony service :老式的电话服务
abstract :虚拟
为了提高ERLANG的效率问题,Joe Armstrong设计了JAM,Joe‘s Abstract Machine ,其中的重要特性是加入了并行的进程,采用消息传递的机制,加入错误检测机制,省略了回溯机制,回溯机制采用回溯算法,深度遍历整个结点树,加大了系统的开销
Mike Williams 编写了JAM的C语言实现版本
Ericsson在1998年想成为技术的消费者而不是技术的产生者而在新项目中禁用ERLANG,进而导致ERLANG团队的出走和开源化
ERLANG的核心人员出走创立了新的公司叫做Bluetail,创造使互联网更为可靠的程序
监督树机制在ERLANG中对于可容错这一功能具有决定性的作用
OTP是和ERLANG一起发布的一个用于构建容错系统的应用级别的操作系统
AXD301几乎是函数式语言所编写过的最庞大的系统,其饱含了170万行的ERLANG代码,百度AXD301几乎都能关联到ERLANG相关的网站上去
二、架构模型
究竟什么可以说是软件的架构?架构师所从事的工作究竟有哪些是令人觉得具有挑战性的东西
架构是一组有关软件系统的组织方式的重要决策,重要在于决策,是对系统构成元素、元素接口以及这些元素之间的协作方式的一种选择,是把这种元素以及结构组建成更为庞大的子系统的合成方式,也是一种构建风格,在其知道下将元素,元素的接口,和元素的协作和合成组织起来。
架构就是如何去组织各个细节
架构从最高层面上来说就是一种思考世界的方式。让我想到了巴神,哈哈
从细节上来说架构就是规程和手册,相当与指导文档,如何去编写一个角色进程的模块,如何去处理外部消息等
软件的架构的描述方法
1、关注于问题领域
软件的架构一定不是通用的,而是面向某个特殊领域而进行定制设计的,必须明确该架构是为了解决什么类型的问题,游戏架构的问题是如何解决玩家和服务器的交互,游戏服务器更像一个交互服务器,而不是一个运算密集型的服务器,游戏服务器对运行的稳定性有要求,要求有足够过的日志供给运营来进行分析
2、哲学
软件构造方法背后的原理是什么,核心思想是什么。 模拟现实,简单通用
3、软件构造指南
相当于软件的说明书,在实用性方面,我们的构造指南必须要包括编程的规则集,如何去编写类似的程序,如何处理协议和数据层的关系。例子程序,提供正确可靠的模板程序,例如更新脚本中的模板。培训资料,如何去让一个新人来接手现有的项目
4、预先定义好的部件
OTP库中饱含了一个完整的现成部件集合(behaviour库),通过gen_server这种behaviour就能构建其一个client_server模型,gen_event可以构建基于事件的程序
预先定义好的部件如果经历了严格的检测,就能够帮助快速而又准确的构建更为庞大的系统
5、描述方式
必须用准确的方式来描述程序提供给外部的调用API,描述协议和系统结构,目标是产生合理而又容易理解的文档,这样才能在未来的维护中让人能够迅速接手
6、配置方式
如何去启动、停止以及更新系统的配置,如何去在工作中的系统中进行重新的配置
ERLANG这门语言所关注的问题领域:
1、系统能够应对超大规模的并发活动
2、必须在规定的时刻或规定的时间内完成任务
3、系统必须可以在多台计算机上分布式的运行
4、系统要能够控制硬件 ERLANG的os模块能够直接运行shell脚本的东西
5、软件系统往往会非常的庞大
6、系统要拥有足够强大的功能
7、系统要能够运行很多年,必须稳定
8、软件的维护应该能够在不停机的情况下进行
9、满足苛刻的质量和可靠性要求
10、必须提供容错机制,报错兼容硬件失灵和软件错误
为了满足上述的条件,必须让设计的语言和库具有一下的机制
1、并发性,必须要满足数以万计的并发需求,对于游戏而言
2、软实时性,在时间上不需要100%满足,一旦出现超时,通过再执行一遍的方式来进行处理即可,硬实时系统是需要基于硬件的准确时序的工作方式,必须保证100%的时间可靠性
3、必须满足分布式的特点,为了应对大数据量的冲击,必须让我们的系统可以扩展,单节点明显是不能满足我们需求的,因此我们的系统应该可以支持随时扩展成多节点的形式
4、硬件交互能力,在交换系统中存在大量的硬件需要控制,因此语言本身必须能提供可靠的操作硬件的方式以及上下文切换需要迅速
5、能够支撑大的软件系统运行,哪怕是上百万行的代码,也必须在语言层面上提供可靠的运行支持
6、所谓的复杂性软件原则就是软件在运行过程中会随时由于功能上的改变或者是扩展而进行在不停机的情况下热更新代码和功能
7、运行多年的能力
8、高质量性,即便发生错误也要让我们的系统能够稳定的运行,这个也需要在编码上注意
9、容错性,就是说要兼容错误
ERLANG的需求列表来源与电信系统,但是在互联网时代,Web服务器的需求,原因是ERLANG天然的适合多角色交互的环境
为了构建在软件存在错误的情况下依旧能够具有合理行为的可容错软件系统,必须要进行一下的设计
1、将软件所构成的系统分层次,复杂且重要的层次处于顶层
2、我们首先保证顶层的任务能正常的运作
3、一旦在运行过程中某一个任务出现错误,那么就不再执行它,而是切换到一个更为简单和底层的任务上去执行
必须要隔离错误,任何一个进程或者任务的错误应当不影响其它的系统,系统容错的最主要问题就是故障隔离
ERLANG采用了操作系统的进程的概念,进程提供一种保护机制,一个进程不会影响到相邻的其它进程,因此我们电脑里面一个程序的奔溃正常情况下是不会影响到整个操作系统的稳定性
在ERLANG中,进程以及并发编程属于语言本身的特性,而非依靠操作系统
这样我们就能不依靠操作系统,从而直接运行到各种操作系统上,所有的同步和通讯其实都和宿主操作系统没有关系,多一个虚拟机层次,只要我们保证虚拟机能够运行在各个服务器上就可以了
采用轻量级别的进程,ERLANG的进程创建和销毁的速度是其它语言的N倍
能够移植到各类操作系统而不用考虑各个系统的限制,方面嵌入式的迁移,但是嵌入式环境下使用ERLANG明显不科学啊
ERLANG的应用程序通过大量的互相通过消息来联系的并行进程组成
这种方式给我们的ERLANG架构提供了基础的设施,我们能够通过进程来区分系统中各个功能,形成程序族来满足各个不一样的功能
巨大的潜在效益,对于被分解的任务而言,只要降低其中的依赖性,就能够在分布式的环境下快速的运行,而游戏服务器中由于每个角色之间不会互相的依赖,因此能够天然是适应分布式的特点
这样就能够实现故障隔离,除非ERLANG虚拟机发生奔溃,否则系统中的进程就不能互相的产生影响
实际上ERLANG进程之间的进程并行是通过内置的调度程序来实现的伪并行的时分方式来实现的
故障隔离才是实现可容错系统的本质要求,必须隔离故障
隔离故障的可靠方式就是采用消息传递的方式来共享数据,一旦没有了共享数据,那么就能够在某一个进程崩溃的时候迅速隔离
ERLANG中的并发是非常重要的,这种并发机制甚至被命名为面向进程编程。
这种编程方式特别适合那些面向现实世界建模或者是与现实世界进行交互的系统
ERLANG和面向对象编程语言的相同点是提供多态和统一接口,通过函数匹配的方式能够非常方便的实现ERLANG中的多态性
对于一个问题的多个处理进程我们可以给他们传递同样的消息,他们会以同样的方式来进行处理
COPL:面向进程的编程方式,就是说分解现实社会到多个进程上来
1、从真实的世界中识别出所有的并发活动,就是说完全不会互相依赖的那些活动
2、找到这些活动之间的消息通道
3、列举这些消息通道中会传递的全部消息,必须注意的是如果存在未知的消息会导致进程信箱的阻塞而奔溃
COPL的特点:
1、每一个进程都被认为是一个自包含的虚拟机器
2、运行在同一台机器上多个进程应该是高度隔离的,他们互相之间不会出现影响的情况,除非是monitor的情况
3、每一个进程拥有唯一的标识符号,这种符号在整个ERLANG的虚拟系统中应该是独一无二的,如何在分布式系统保证这种独一无二性?
4、进程之间没有共享状态,进程只能通过消息传递的方式进行数据交流,实际上ETS表打破了这一个限制,而又与禁用进程字典的要求相背离
5、消息传递应该是不可靠的,没有传输保障,类似于UDP么?
6、一个进程应该可以知道另一个进程出现错误退出的消息,并知道其退出的原因,并以此来构建一个监督者进程
在ERLANG中进程是真正在并发运行,进程之间的消息也可以确信为异步的消息传送模式
进程隔离:
进程可以假设是运行在不一样的物理机器上,应该尽量的避免同步的调用,因为我们确信整个系统都不是绝对可靠的,消息在异步的环境下也是不可靠的,唯一可靠的就是一对消息构成的握手机制,否则认为整个系统的消息不可靠,进程之间不存在共享数据和状态,不互相影响。
进程的名字:
在erlang中要求进程的名字不可以被伪造,也就是说pid是无法被伪造的,对外公布pid的过程就叫做名字散布,越多的进程名字被散播出来,系统的安全性会下降,假定每一个进程只知道其本身的pid和其产生的子进程的pid,这样就能尽量的减少进程名的可知性扩散。
消息传递:
消息传递是原子操作,对于一条消息,可以确保要么是失败的要么是成功的,消息传递的顺序可以相信,进程应该会根据时间戳重组整个消息,通过缓存和重组,实现消息的时间有序处理,消息的传递内容必须是时间的数据的拷贝,而不能是只能,消息传递是一种类似与发送及祈祷的方式,哪怕整个网路是可靠的,也只能依靠消息对来进行消息的确认。消息的传世存在时序性,消息传递用于同步进程间的数据或者是状态
协议:
用于部件及进程之间的通讯,这些通讯属于ERLANG系统层面的东西,用于约束进程的行为,判断进程的存活状态以及进程的运行状态,是否处于即将死亡的边缘等行为
COP和程序员的协作方式类似,都是一部分进程来组成一个功能模块,通过互相之间的消息来共享状态和其它数据
6点根本性原则:并发性 错误隔离 故障检测 故障识别 代码升级 持久的存储
并发性和运行效率对于ERLANG来说非常的重要,语言本身可以提供并发,也能通过系统层面上来提供并发的功能,运行与unix上的c程序也能拥有并发的功能
对于语言层面上的要求
1、封装原语 ->提供最主要的封装机制,防止错误的扩散
2、并发性 -> 必须支持大量进程,降低进程切换的损耗以及进程不能独占cpu
3、错误检测原语 ->必须提供一种监督机制,这样才能准确的知道系统中发生的异常以及是否重启等操作
4、位置透明性,对于同意系统内的进程,不用在意其处于的机器和结点,都应该能准确的更根据进程的pid寻址到对应的进程所在
5、代码动态升级 ->能够对运行中的代码进行替换 通过加载beam文件就能够直接将原有的程序替换掉,这里是如何解决进程数据残留等问题的呢?
进程的数量不应该成为整个系统的瓶颈,那么进程数在很大的情况如何处理进程之间切换的损耗呢?
对于库的要求:
1、拥有持久存储的功能
2、有设备驱动程序,能够提供一种和外部硬件交互的方式
3、代码升级能力
4、运行基础,如何找到程序的入口,如何停止,如何配置运行参数 erl -name -hidden -setcookie
ERLANG虚拟级本省参照了操作系统的相关操作,异步通讯机制等
可容错软件的核心就是ERLANG的监督者模型,其属于ERLANG的OTP库,这叫做应用程序库,是比普通的库更为高级的存在,更为抽象的层次结构
surpervisor :监督者模型 gen_server :client-server模型 gen_event:事件驱动程序 gen_fsm :有限状态机
错误必须被强隔离,意味着在程序中降低错误被传递的机会,采用速错的概念,既然有错误就让它停下来,同时保存运行现场以便快速恢复
进程之间互相监测,以此在某个进程出现错误时就被其它进程发现,如果有错误就让错误尽快的暴露出来,这样才能减少更大的错误,进程采用拷贝消息的方式传递数据,这样就能尽可能的不互相造成不良的影响
根据软件的模块来划分层次,包错误降低到最小的模块中,游戏中个别玩家的错误应该不能影响到游戏中的其它的玩家,只允许角色进程call其它的进程,而不允许其它的进程来call角色
ERLANG中并不建议使用防护性编程的方式
软件部件隔离非常重要,应该在部件发生错误的时候就直接重启它,这样能够简化错误模型,维护系统的整体稳定性
ERLANG语言层面
ERLANG是面向消息型的语言,此类语言的特点是采用并行进程的方式来实现系统的并行
ERLANG具有以下的一些观点:
1、一切都是进程,所有的程序必然运行在一个确定的进程中
2、进程强隔离,摆脱共享内存和共享数据的束缚
3、进程的生成和销毁都是轻量级别的操作,时间消耗和内存消耗上来说都是这样的
4、消息传递是进程交互的唯一方式,引入ETS可以突破这一个限制,可以通过ETS来实现进程的的数据交互
5、每一个进程都有自己独一无二的名字
6、如果你知道进程的名字就能可以给一个进程发送消息,好奇whereis函数的实现
7、进程之间不存在共享资源,这是明确的,ETS实际上也是一组进程状态
8、错误处理要非本地化,一个进程的错误不应该由进程本身来处理,而是由它的监控者进程来进行处理,这样尽量避免错误扩散
9、进程要么好好的运行,要么就死掉,不要存在异常的进程状态
进程本身能够提供一种良好的边界条件来提供数据保护,因为存在这种边界条件,所以只能通过消息传递的方式来进行数据的交互
可容错的系统必须考虑到物理机器的安全性,需要做硬件备份,在游戏中这种备份就不是必须的,因为游戏的运行要求不是和电信系统一致的,但是在淘宝上就必须存在所谓的备份机器,以便随时可以接管系统,游戏的router机器也是,必须做一种多机器共同做一件事情的设置。
ERLANG作为函数式的并发语言,没有共享数据,没有监视或者是同步方法,从语言本身来说,编程无副作用,但是少数操作是具有副作用的,注意这些有副作用的操作
ERLANG中的import导入能够实现将外部模块的导出函数变成本身模块的内部调用函数,在编码的实际过程中不推荐这种方式,原因是这样让全局的查找变得困难起来
整数的大小只受内存的限制,几乎可以确认ERLANG中的整数为无限大
加上‘‘标签的字符串可以包含任意的字符,都会被转义成原子
引用的make_ref()用法,引用仅仅为符号而已,两个引用可以比较是否相等,全局唯一
二进制数具有高效存储的作用
Pid在ERLANG是一种数据结构,叫做进程标识符,通过spawn原语产生这样的进程标识,Pid是ERLANG进程的
端口,端口也是一种数据结构,通过open_port来完成创建,端口可以进行消息的时间偶发,消息必须遵守端口协议的规则
匿名函数 fun() -> end.可以编写匿名函数,其本质上叫做函数闭包
元组和列表,元组中的数据个数是固定的,列表是可变的
列表的第一个元素的访问时间是恒定的,不会因为列表的增长而发生变化
[Head|Tail].Head是单个列表元素,Tail是列表
单词时间:syntactic ->语法 sugar ->糖
字符串在ERLANG中只是ASCII码的列表集合速记
记录record是元组的带标记定义,用于区别元组中各个字段代表的意思
单赋值的变量模式,任何的破坏性赋值必须重新开辟内存空间,重新命名新的变量
term和pattern 项式和模式
term() 定义为原始数据|基项构成的元组|基项构成的列表
一个原始模式 primitive(原始) pattern 是其所有变量都不同的模式????
模式匹配就是一个模式和一个基项进行比较的行为,模式难以理解
模式中可以存在变量,而项式中不能存在变量,模式匹配是模式在前,项式在后,项式必须是实际的数据类型,项式和模式的匹配成功过程叫做一致化过程
保护式: guard
保护式的格式是T1 二元运算符 T2
== 等于 =:= 严格等于 保护式可以作为表达式来使用,运行的结果是true或者是false
在原始的模式中,我们要求所以ude变量都必须不同,扩展模式不要求变量不同
在实际上进行模式匹配的时候,我们首先把扩展模式转换成原始模式和保护式,然后再当作原始模式来进行匹配
{X,a,X,[B|X]}这种扩展模式被转换成原始模式加上保护式的方式就变成了 [X,a,F1,[B|F2]] when F1 == X,F2 =:= X这里没有采用完全匹配??
匿名变量的概念"_",可以与任意的一个项式来进行匹配,但是不会进行绑定,这样就能够实现模式匹配的扩展性
函数的概念:
在ERLANG语言中函数由一个或者是多个的分号分隔的字句组成
-> 函数的头是一个原子,然后是是一组用括号括起来的模式 ,模式中可以存在变量,项式中不存在变量 fun() pid port integer float atom ref binary
保护式必须要加上关键字when 来引出
函数体是一系列的用逗号分隔的表达式组成
扩展模式的概念,扩展的是原始模式 primitive
在每一次进入到函数的运算当中时变量绑定的值的作用域只有本函数
保护式在多模式匹配的函数中起到了入口保护的作用
调用函数时就是项式和模式的匹配过程,函数本身是有值的,函数的值就是函数的最后一行表达式的值
尾递归调用的定义是在函数体最后一个字句也是调用其它的函数,那么就可以说这个函数是尾递归在,而不用在乎调用的函数是其本身或者是其它的函数。
尾递归并不会降低堆栈的消耗,而只有迭代函数才会降低堆栈的消耗,因为在传统的栈式计算机中,一个函数运行完毕的结果就直接会被当成下一个函数参数传递进去,无需去开辟新的堆栈空间,从而在内存的消耗上产生极大的优势。
一般可以通过加多一个参数的方式让一个函数编程尾递归,为了防止某些函数的死循环,一定要做严格的边界检查,通过不应该将加多一个参数这样的事情暴露给外部调用程序来实现,而是可以通过再封装一层的方式将加多的参数封起来,这样就能防止外部调用的失败和参数错误。
额外参数可以叫做聚集器。
case 和if 的返回值问题 if 中不能是本地定义的函数,因为函数的返回值是不确定的,无法确定为true
函数式语言的精华就在于高阶函数,函数可以作为参数和返回值
lists:map/2函数的作用就是lists:map(F,L)
F为函数参数,map(F,[H|L]) ->F(H),map(F,L).
abstraction 抽象 control 控制
函数可以作为高阶函数的返回值,可以定义这样的行为X = fun foo/3,实际上来源自模式fun(L,F,H) -> foo(L,F,H) end.
表理解
列表解析 [表达式||生成器,过滤器]
列表解析可以用于快速排序的简单实现,效率不高,也可以用去字符组合算法
快速排序的简单实现
qsort([]) ->
[];
qsort([H|L]) ->
qsort([X||X<L,X <=H]) ++ [H] ++ qsort([X||X<-L,X > H]).
组词算法
perms([]) -> [[]];
perms(L) -> [[X|T]||X<-L,T<-perms(L --[X])].
二进制数据
设计的目的是为了存储非结构化的数据,就是说存储那些非类型化的数据,无类型的
最初的目的是存储大量的非机构化数据和高效的I/O操作,二进制数据相比较List或者是Tuple而言,其存储效率大大提升,
列表中的一个数据存储需要8个字节,而二进制数据一个字符需要1个字节就可以了,会有很小的固定开销,二进制数据只能存储小整数(0-255)
io表指的就是小整数表
list_to_binary/1,调用条件就是list必须是io表,其作用是直接扁平化list表,逆操作是binary_to_list/1
还有term_to_binary/1,能把所有的结构转换成binary数据存储 任意项式 (不存在变量的ERLANG数据集)
concatenate_binary 连接二进制数 和split_binary 分隔二进制数
对于计算机网络而言,将一个项式转换为二进制数据的重要性在于可以便捷的存储数据以及将这些数据丢到网络里面去,非常适合网络服务器的开发
直接构造二进制数据时,如X = 1,Y=256 ,Z= 1 <<X,Y,Z>> = <<1,0,1>>
二进制数是8位的,除非指定位数 <<X,Y:16,Z>> = <<1,1,0,1>>
可以通过big和litte来调整字节顺序
二进制数据的模式匹配最早是被设计用来处理网络中的二进制包数据的
通过二进制的模式匹配能够直接达到对二进制的01字串进行操作的目的
记录的存在目的就是为了让tuple的各个字段变得容易理解和提取出具体的值
ERLANG的预处理程序
包含了两个大的模块 epp和compile模块
预处理的结果就是将各种头文件导入到对应的代码中去,生成临时的文件,宏都会被展开,在这个临时文件中,所有的注释都被去除了
宏定义的作用就是在代码中用宏定义的对应参数
宏调用的参数和返回值必须是完整的表达式,而不能是表达式的分解或者部分
?LINE
?FILE
?MODULE
包含文件的两种方式
include和include_lib 怀疑我们游戏中是把所有的目录文件汇总到一起再进行编译的
并发编程的核心在于通过erlang:spawn/1,2,3等函数创建并行的进程
Pid = spawn(F). 直接就会将spawn出来的进程的独一无二的标记赋值给Pid这个变量,这叫做模式绑定项式
receive语句中可以加入保护式的概念来进行防范性编程
当向一个进程发送消息的时候,该消息就被放到进程的邮箱当中去mailbox,下一次进程对receive进行求值的时候,就会查看邮箱,试图将第一条信息与receive语句的所有模式进行匹配,如果邮箱中收到的消息没有和任何的模式匹配成功,消息就会被转移到临时的保管队列当中去,进程会被挂起,等待下一条消息,如果消息匹配成功,并且保护式也取值为真的时候,模式后面的语句就会被依次求值,临时保管的消息也会被放回到进程的邮箱中来
receive的匹配机制是如何的呢?如何进行循环的呢?
调用c:l(Module)函数以后
erlang:process_info(Pid)得到的信息会变成undefined
receive加上超时控制,可以在超时期限内没有收到可以匹配的消息,超时条件下面的表达式就会被求值
注册进程名的作用在于把atom和pid绑定起来,这样就可以通过调用atom名直接给pid发送消息
错误处理:
在ERLANG中求取一个函数的值两种返回结果,要么是返回一个值,要么是产生一个异常
程序在产生异常的时候就没有必要继续执行下去了,除非在代码中采用catch的方式显示的处理所谓的异常
ERLANG程序是被编译成虚拟机的指令然后由一个虚拟机的仿真器来执行的
共有6中类型的异常
1、值错误
2、类型错误
3、模式匹配错误
4、显示的调用错误 exit(Why)产生的一个错误
5、错误传播 进程收到exit消息时后处理
6、系统异常,内存耗尽或者是检测到一个内部表不一致中介掉一个进程
catch可以防止进程挂掉,原因在进程在出现未知的异常的时候就会退出,并且会把这个错误在连接的进程族中进行传递
catch的作用是把错误转变成ERLANG的一个项式,如果catch的模块正常返回,catch返回的就是模块的正常运行结果
catch 如果后面是项式的话就需要加上括号来进行范围限定
exit产生的显示异常会导致进程的自行退出
throw的作用是用于区分用户自己产生的异常和系统运行时所产生的异常
如果在编写代码时候就已经预估到了错误,就能编写足够多的错误纠正式子来纠正所有的错误情况
进程连接是将一组进程聚合在一起,任意的一个进程退出都会造成整个组进程的退出
进程监视是一个进程单独来监控系统中的其它进程,在设计上监督真可以捕获错误而不用退出
进程连接的相关概念
如果一个进程发生错误,并且没有捕获命令catch,那么该进程将会终止
出错的时候,该进程的出错原因将被广播到它的归属连接集中去,link(Pid) -> 某一个进程连接到其调用进程上
也可以使用spawn_link的方式去创建进程,这时候等价于先spawn后link,不过这两个表达式是一起执行的,这个语句的引入是为了规避在进程创建的过程中没有来得及link就死掉的这种罕见的错误情况
信号的概念,在ERLANG系统中信号是进程终止时候在进程之间传递的东西,信号是一个{‘EXIT‘,PID,Why}元组,任何世道的Why不是normal的退出信号的进程都会死掉
在shell中调用!命令伪造的退出信号无法让角色进程退出???
这里有一个叫系统进程的概念,系统进程收到退出信号会将其改成一个正常的进程间消息,并把这个消息加入到进程的邮箱中去。
process_flag(trap_exit,true)能把一个一般进程编程系统进程
exit/2具有伪装一个进程死亡的功能 exit(Pid,Why). 给进程Pid发送一个原因为Why的退出信号
exit(Pid,kill)具有强制性的杀死掉一个进程的作用
使用的时候将一个应用的进程都连接起来,让其中的一个进程作为监督者存在捕获错误,一旦一个进程出错,整个应该的进程都会退出,留下一个监视者进程来接收和处理群组中的进程的出错消息
监督者不用是系统进程也能使用monitor实现系统进程中消息的监控机制
ERLANG的分布式编程
spawn(Node,Fun) ->在Node结点上产生一个处理函数是Fun的进程
monitor(Node) -> 监控一整个结点的行为
在一台物理机上运行多个ERLANG结点和运行在多台物理机上的结点类似
端口机制:
每个端口都有一个与之关联的控制进程(controlling_process)
控制进程拥有该端口,从该端口收到的消息都会被发送给其控制进程,且只有控制进程才能向该端口发送消息
一把情况下默认创建该端口的进程就是端口的控制进程,但是这个进程是可以被改变的
P是端口,Con是控制者的Pid P!{Con,Command}.
可以通过这样的调用让端口足一些事情{command,Data} close {connect,Pid1} 端口可以关闭,改变控制进程,以及将io表发送给外部对象
那么如何让二进制数据进入所谓的网络通路呢?
通过端口收到的外部应用程序的数据都是用{Port,{data,D}}的消息格式传给其控制进程
消息的确切格式以及该消息是如何组帧的,都是取决与端口如何被创建的,在创建参数里面可以自定义这话总组合方式
动态代码替换
ERLANG结点上所有的进程都共享同一份的代码
在顺序话的编程语言中由于只存在一个控制线,因此如果动态的替换代码,我们只需要考虑对唯一控制线的影响。在一个顺序化的系统中,如果我们期望改变代码,通常做法就是停止该控制线,替换代码后再启动之
ERLANG系统中每个模块的代码允许存在两个不一样的版本,如果一个模块的代码被加载进去了,那么所有调用该模块的代码的新进程都会动态的连接到该模块的最新版本上去,原来的执行该模块的进程可以选择继续执行老的代码,也可以执行加载模块新的代码,这取决于该代码是如何被调用的
如果代码是通过全修饰名没调用的,就是通过ModuleName:FuncName的方式调用的模块代码,那么总是调用该模块的最新代码
在单一进程的loop循环末端,如果使用loop/0来实现循环,实际上下次调用其实使用的是旧版本的代码
只用显示的加上模块名修饰符号才会实现调用新模块代码的功能,(不知道是否生效,可以做实验)
代码存在两个版本的局限性,如果第三次试着去重新载入新的模块,那么以模块为执行对象的进程会被全部的杀死,两次reload会造成这种奇怪的问题不?
存在很多内置函数来实现代码替换的功能,其中一定有某些坑
原始数据的类型描述
init() -> 整数类型
atom() -> 原子类型
pid() -> Pid类型
ref() -> 引用类型
float() -> 浮点类型 浮点数室友大小限制的
port() -> 端口类型
bin() -> 二进制类型
可以通过类型符号来秒时函数的形式,定义类型 +type +deftype 类型定义符号
ERLANG为什么满足构建可容错系统的条件呢?
1、进程是语言的基础,所以封装层面上是满足的
2、进程本身就存在错误隔离的作用,在步连接进程的情况下,单个进程就是错误的限制作用域,因此错误无法被传播
3、进程一旦执行到错误点,这个错误没有被捕获的情况下会立即使得进程本身退出,满足速错的概念
4、一个进程的错误退出会被广播给进程的连接集,因此错误是可以被捕获的,这样就能及时的对错误进行处理
5、代码的替换更新原则满足不停机维护的需求
6、存储在语言中没有被满足,但是在ERLANG的库中被满足了DETS和Mnesia实现了数据的持久化永久存储
DETS是基于单机的基于磁盘的存储系统,Mnesia支持多结点的存储系统
halt on failure -> 出错就停止运行
Failure status property -> 错误的状态属性
Stable storage property ->稳定的存储属性
编程技术:
技术和语言是不一样的,语言提供的是一种基础
1、抽象出并发,如何转变顺序程序到并发程序上
2、抱持ERLANG的世界观,一切皆进程的概念是否能运用到实际的工作中去
3、如何在ERLANG中处理各种错误
4、显意的编程
将一个系统划分为通用部分和插件
通用部件是困难的,插件是简单的,通用部件实现并行功能,插件顺序化的执行就可以了
尽量将并发的功能封装在少量的模块中,并发必然伴随着有副作用的函数调用,以及一些环调用等死锁问题,因此通用模块必须要强大
gen_server就是一种用于资源管理业务
采用了带外消息传递的形式而不是call来实现服务器进程间消息的传递
在ERLANG边编写的程序中,更多的是对server-client的抽象,按照joe armstrong的说法是由gen_server本身来实现并发机制以及处理外部访问,抽象出来的是通用服务的部分,这样就是的程序员专注与业务上面的开发,大大的提高整个系统的稳定性和可靠性,保证并行机制不会出错,不会出现不可预知的side-effect
可容错和可以动态更新的服务器是如何实现的呢?
我们编写的gen_server实际上就是编写了插入模块,通用模块已经被实现
对于最基本的server模块而言
包含三个功能,根据传入的Pname创建一个死循环的消息receive进程负责消息的接受
提供给一个外部接口函数采用消息异步传递给以Pname为名的进程,该接口有一个receive的阻塞,用于等待进程的返回信息
存在一个保存状态的参数机制state用于存储每一次运行后整个进程的状态,作为进程局域内的变量
基本的server模块不提供处理逻辑函数,该函数必须由调用模块传入才可行
求值的调用在server模块调用
内部尾递归调用实现从而降低对内存的消耗
在外部的模块中,编写的handle_info handle_cast handle_call函数实际上都会被放入到gen_server进程中去运行
1、负责创建进程和消息传递的功能被抽线出来编写成通用模块 负责具体的启动、功能处理的函数被包含在插入模块中,两个模块的共同作用来实现整个功能,对于程序员来说各自只需要专注于自己的模块就好了,约定好API好消息结构
2、编写插入模块的程序员不需要知道任何与并发和错误处理的代码,降低了代码的编写难度,提高了工作效率
一旦插入模块中的实现函数奔溃,自然会导致通用模块所运行的进程奔溃
这时候就必须引入容错机制
如果通用模块所并发出来的通用进程运行函数发送错误,就catch住,然后直接杀死调用者的进程,因为错误是调用者引起的
此时可以加入打印日志的概念用于定位错误出现的具体位置
然后整个通用进程的state就不会发生变动,因此就是回退到调用上次奔溃代码之前的状态
1、容错的服务器首先会报告错误
2、容错服务器会调用exit来使得调用者进程退出,因为调用者已经发生错误了,让它再会下去已经没有意义了
3、通用的容错进程要么运行成功,更新状态,要么运行失败,状态保持不变
通用服务器可能会因为外部原因而被kill掉,因此调用者在等待的过程中必须设置一个超时来防范这种错误
receive 关键词可以带 after 参数加入超时的时间控制
解决这种超时的办法就是加入监控树,具体实现需要看源代码
如果服务进程发生了错误,那么不应该是客户进程来处理这种哦那个错误,而应该由监督者来处理这种错误
在可以热更新代码的服务器中,由于函数是loop循环中的,可以传递特殊的参数将该函数变成另外的函数就成功的实现了函数代码的热更新
1、远程调用的细节被存储在了服务端
2、服务端包含了并行进程,错误处理,消息转发等元素,比应用程序要复杂
3、服务端的代码可以大量被复用
将服务器的功能划分为功能性部件和非功能性部件的好处
1、并发程序的实现比较复杂,专家级别的程序员可以从事非功能性部件的开发而新手程序员可以进行功能性部件的开发,这样就能降低整体程序的开发难度
2、一旦通用服务器已经ok了,那使用ERLANG编程实际上就已经退化成了顺序化编程了!!!
3、所有的应用程序一旦都依靠同一份通用程序,从代码理解和维护的上来讲难度大大的降低了
4、通用服务器和应用服务器可以分开开进行开发和测试
5、可以通过定制通用服务器的方式让程序在各种环境下健康执行
一切都是进程,要交互,就用消息
因为web服务器和HTTP的交互会严重干扰实际逻辑的运行,因此可以抽离出来交互程序
通过这样的一个中间人工具,中间人去实际控制port,然后应用进程与中间人进程交互即可,网关层面上的
错误处理
ERLANG的错误处理与其它的语言不一样
1、让其他的进程来处理错误,这样就能实现容错和双备份机制
2、工作者不成功,便成仁
3、让它奔溃吧
4、杜绝防御式的编程方式
实际上监督者收到的EXIT消息来自其本身的实时系统,而非挂掉的进程
让其他进程来处理错误的方式可以方便的实现哪怕CPU烧坏时我们的系统也能稳定
远程错误处理的好处
1、错误处理代码和产生错误的代码运行在两个进程中
2、解决问题的代码和处理异常的代码分离,逻辑上容易理解
3、移植方便,单节点的东西可以很快的被移植到多节点的系统里去
4、测试简单,可以在单台机器上实现整个系统的测试
监督者和工作者模型
1、工作进程不用担心错误处理
2、可以哟哦那个特殊的进程来负责错误的处理
3、可以在独立的物理机器上执行监督者,从物理机制上实现容错
4、错误是具有通用性的
这种机制几乎能够包容绝大多数的错误,能够让系统长期稳定的运行
错误是指:
系统不知道如何处理的错误
程序员未知的错误
这类的错误就不要防御了,直接让它暴露出来,让进程奔溃,留下罪证来进行修复
摒弃防御的编程方式,让系统自身自灭吧,系统的错误信息也许会比我们自己写的更多
显示编程,应该直接可以通过函数的名字来判断函数的用途,而不是需要来阅读函数的具体实现
函数的命名应该是清晰的
编写正确的代码的困难之处在于难以抽象出最简单可靠的模型
分治的办法能够把大的问题分解成小的问题,降低复杂程度,但是这种分解本身却又是一中复杂的活动
操作系统提供了某些被程序员遗忘的东西,因为JAM的存在,ERLANG实际上只是调用了很少的操作系统功能
通过进程的并发和错误的检测,ERLANG实际上提供了一种简单的OS功能,就如OTP一样
异常、错误、故障是有区别的
1、如果一个系统在发生错误的情况下依旧能够正常的工作,那么该程序就是容错的
2、必须考虑到对错误处理的复杂程度所产生的系统消耗
异常来源于ERLANG的虚拟机,错误是抛出的异常被实例化,如果一个错误被捕获了,那么该错误就不是故障,一旦没有被捕获,就会成长成一个故障
故障是会被传播的,传播的范围仅限于在进程的连接进程组中
1、当你无法纠正一个错误的时候,就让他奔溃,并转向到较为简单的任务上,以保证整个系统的可靠性,对于电信系统就是你不能提供完全的服务时,你就考虑去提供有限的可靠服务
2、建立可靠的监督层级,采用监控树的方式来构建整个系统的监督机制
3、乖函数的概念,假如一个乖函数产生了错误,我们就认为这是一个故障了
假如程序不能按照规则说明来运行,就应该使其退出
能够被遇见的错误不应该引起系统的故障,所谓的捕获就是指将这一段代码在catch中求值,写下故障信息
对于电信系统而言,首先我们尽力去执行一个任务,如果这个任务发生了故障,我们就转向另一个更为简单的任务去进行处理
监督者监督的是就监督者和工作者
工作者必然都是behavior实例
监督树的概念,监督者根据其监督机制组成的树形结构区分为树形的和线性的
监督者会在工作者发出异常错误的时候,采用定义好的重启策略去修复这样的错误并且留下日志记录
通过乖函数来参数化我们的gen_server behavior就能得到一个通用的服务器模型
线性层次结构 AND/OR层次树
监督者应该能监控任意多个工作者和监督者
监督者应该能知道其监控队列中所有监督者和工作者的启动、停止、重启方式
每一个孩子结点都只有一个父亲结点
如果一个监督者被其父亲结点给终止了,那么该监督者将停止其下面所挂载的所有子进程,包括监督者和工作者
如果一个工作者发生故障,那么监督者会根据重启策略,重启单个子进程或者是重启其下面的全部子进程
系统的启动通过最顶层的监督者启动而进行启动
在与/或监督者模型中,与监督者在一个孩子挂掉的时候会重启全部的子孩子,或监督者只会重启发生故障的子孩子
监督者死掉会带动它所有的子孩子进程死掉
错误的纠正问题,可以被纠正和不可以被纠正的错误会同时存在于系统之中
按照定义,一个组件的实际工作情况与其定义的方式不一致了就可以说该组件发生了故障
必须在系统运行与规格说明相背的情况下记录下错误日志
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。