回“老赵”关于“Erlang中最大的问题”
活跃在博客园的“老赵”,是一位研究 .NET 非常深入的同学(因为我本人也是老赵——jackyz.zhao,所以,特地加了引号)。他最近很关注“在 .NET 下实现 Erlang 语言特性”的课题,并为此写了一系列的技术文章,相当不错,我一直都在关注。他自己写了一个名为 ActorLite 的小东西,此前做过介绍,是个不错的尝试。
最近“老赵”同学写了一篇《一种适合C# Actor的消息执行方式(上)》,其中提到“(在消息执行上) Erlang 中最大的问题”。这是一个很有意思的观点,而且因为富于代表性因而显得很有价值,很有必要拿出来和大家探讨。
其中提到:
Erlang的优势与缺陷
Erlang在消息执行方式上的优势在于灵活。Erlang是弱类型语言,在实现的时候可以任意调整消息的内容,或是模式的要求。在 Erlang进行模式匹配时往往有种约定:使用“原子”来表示“做什么”,而使用“绑定”来获取操作所需要的“数据”,这种方式避免了冗余的cast和赋值,在使用的时候颇为灵活。然而,世上没有完美的事物,Erlang的消息执行方式也有缺陷,而且是较为明显的缺陷。
首先,Erlang的数据抽象能力实在太弱。如果编写一个略显复杂的应用程序,您会发现程序里充斥着复杂的元组。您可能会疲于应对那些拥有7、 8个单元(甚至跟多)的元组,一个一个数过来到底某个绑定匹配的是第几项,它的含义究竟是什么——一旦搞错,程序便会出错,而且想要调试都较为困难。因此,也有人戏称Erlang是一门“天生会损害人视力的语言”(令人惊讶的是,那篇文章居然搜不到了,我们只能从搜索引擎上看出点痕迹了)。
而我认为,这并不是Erlang语言中最大的问题,Erlang中最大的问题也是其“弱类型”特性。例如,现在有一个公用的Service Locator服务,任意类型的Actor都会像SL发送一个消息用于请求某个Service的位置,SL会在得到请求之后,向请求方发送一条消息表示应答。试想,如果SL的功能需要有所修改,作为回复的消息结构产生了变化,那么我们势必要修改每一个请求方中所匹配的模式。由于消息的发送方和接受方在实际上完全分离,没有基于任何协议,因此静态检查几乎无从做起。一旦遇到这种需要大规模的修改的情况,Erlang程序便很容易产生差错。因为一旦有所遗漏,系统便无法正常执行下去了。
这的确是对于动态类型语言很常见到的担心,而且,确实,如果不加注意会成为严重的问题。这种困扰确实是因为 Erlang 的“动态类型”和“基于消息”而造成的。但,这并非无解,实际上,在 Erlang 编程规范之中,已经给出了解决方案。
通常而言,Erlang 中的消息应该是以“控制流”为主,在消息的数据结构表达上,不同的消息通常都对应着不同的处理流程,在这里“控制”是消息中最为关注的内容。为此我们可以简单的引入一个 atom 来予以表达,这就是 Tag Messages 的最佳实践:
5.7 Tag messages
All messages should be tagged. This makes the order in the receive statement less important and the implementation of new messages easier.
Don’t program like this:
loop(State) ->
receive
...
{Mod, Funcs, Args} -> % Don't do this
apply(Mod, Funcs, Args},
loop(State);
...
end.The new message {get_status_info, From, Option} will introduce a conflict if it is placed below the {Mod, Func, Args} message.
If messages are synchronous, the return message should be tagged with a new atom, describing the returned message. Example: if the incoming message is tagged get_status_info, the returned message could be tagged status_info. One reason for choosing different tags is to make debugging easier.
This is a good solution:
loop(State) ->
receive
...
{execute, Mod, Funcs, Args} -> % Use a tagged message.
apply(Mod, Funcs, Args},
loop(State);
{get_status_info, From, Option} ->
From ! {status_info, get_status_info(Option, State)},
loop(State);
...
end.
可以相信,一段庞杂的代码,在经过一番这样对 Message 本身的设计和重构之后,最终都能让其恢复“秩序与美感”。而消息中的其他参数,如果其处理逻辑非常复杂的话,那么带有模式匹配的子函数,又或着是 Tuple/Record 正是其用武之地。
上面我们提到了“对 Message 的设计和重构”,实际上,这只是问题的一个方面。另外一个方面是:无论 Message 的“协议”设计得有多精巧,其对外的接口都不应该用消息来表达,而应该是“接口函数”的形式。
Erlang 系统之中的消息是极度灵活的系统,但它并不适合用来作为模块之间的接口,因为它过于灵活。更加适合这一角色的设施是“函数”。用来充当这样角色的函数就被称作“接口函数”,在其实现代码中,除了“按照约定的格式发消息以外”什么别的也不做——因为它包装的正是以消息为载体的交互“协议”。它是函数,有着严格的语法检查,而且可以在多个模块之间重用。一旦需要修改消息协议,起隔离作用的“接口函数”就会体现出其存在的巨大价值。
5.10 Interface functions
Use functions for interfaces whenever possible, avoid sending messages directly. Encapsulate message passing into interface functions. There are cases where you can’t do this.
The message protocol is internal information and should be hidden to other modules.
Example of interface function:
-module(fileserver).
-export([start/0, stop/0, open_file/1, ...]).
open_file(FileName) ->
fileserver ! {open_file_request, FileName},
receive
{open_file_response, Result} -> Result
end.
...code...
Erlang 是动态语言,对其进行静态检查比较困难(并非不能 R13 已经有所动作)。但,并不是说没有静态检查就会寸步难行。Erlang 同样重要的语法特性是它还是函数式语言,遇到有疑惑的地方,不妨以函数的角度来思考。
感谢 Arbow 的分享。感谢 JeffreyZhao 的写作。
Erlang 语言素有“难学”的名声,其中一个原因就是因为虽然其语法十分简单,但常会让人心生 “就这样了,然后呢?” 之惑——因为过于灵活而无所适从,而且也不存在着显而易见的正确用法。这种困扰通常要在有了一定的实践经验之后才会渐渐消散。换句话说,在“学会”和“用好”之间存在着一堵模模糊糊的墙,而这一“未知区域”又需要一定的耐心和经验方可穿越。
@Jeffrey Zhao
Erlang里面也是可以传递引用的,比如:对于大的binary,并且你也可以使用ETS之类的东西避免拷贝;但是会在一定程度上违反Erlang分布式透明的哲学,跨机器时不是还得拷贝。
另外,是不是有性能问题,得测试说了算,不能猜,Erlang在消息传递方面是做了很多优化的。
再次,最根本的问题还是软件容错,你一个actor传个引用给另外一个actor,然后你这个actor死了,这时会出现啥状况。
只要大家都静下心来,好好想想如果让你做一个软件容错的系统,你会怎么考虑,就会更好的理解Erlang的设计。
说实话,比起解决那些真正复杂的问题,语言的语法真不算个啥。当然好的语法用起来还是很爽的。
》process/actor模型本身就是为了让设计和流程控制简单,
这个只是一个方面
》提升性能倒是有各种各样的办法。
process/actor模型可以用作并行计算提升性能。
》好的设计要兼顾简单和性能,actor/process模型对于并发/并行就是一种
》这样的好的设计。怎么反倒成了设计错了。
呵呵,我是说把process/actor用错了。
>>Erlang的数据抽象能力实在太弱。如果编写一个略显复杂的应用程序,您会发现程序里充斥着复杂的元组。您可能会>>疲于应对那些拥有7、 8个单元(甚至跟多)的元组..
你写c/c++,.net里面不是也有一对的指针,对象,数组,列表么?复杂的结构,逻辑必然不是一眼就能看清。你用什么语言写出btree来,不熟悉也是要折腾半天才能搞明白。复杂的东西有喜欢复杂的人去搞明白。
至于erlang的代码之精简,相信各位就不要在争辩了。
>>关于erlang中的类型
erlang是动态语言,强类型。这个灵活当然有代价。
写出一个好的应用,需要很多精力,需要不断的进行修复完善。
c/c++, java应用去除bug可能是这样:
编译期 50% 运行时 50%
而erlang可能是:
编译期 30% 运行时 70%
但是一个好的应用,总是要投入100%的精力,至于什么样的方式不重要。
通过erlang的type, spec(eep-8),通过dialyzer,我们的erlang程序在编译期可以更安全一些。
千万别抱怨erlang太脆弱,erlang不是超级强大,但是至少不必你玩过的其他语言脆弱。
>> 任务调度(process, thread..)机制
erlang中的调度器,更加接近os级别,其追求process的机会均等。
某些语言中,你可以保证某个process具有超强的优先级,可以获取更多的cpu。
在erlang中,这个也不是缺点,为了完成你的需要,可以有多种解决方案:
1,为某些资源占用密集的process,分配多个process实例,以多达到这个目的(有点蠢)
2,调整某个process的优先级(erlang中的节点间connect process就是设置priority为max,保证连接能够快速建立),还是不行?
3,启动一个独立的erlang node,你自己去独享一个vm,想怎么玩你就怎么玩.
最后,还是尽量让你的应用处于一个平衡的状态,不要让某个process做太多的事情。把80%的任务交给某几个进程去做,肯定不是好主意。
erlang系统重构的代价,就我自己的经历感觉微不足道,轻而易举。
erlang还在不断的优化完善,不断引入新的特性,目的不是要成为一个全能冠军,只是在自己擅长的路上做得更好!
另外顶下dsun!
@Jeffrey Zhao
Scala的Actor可以看作一个轻量的数据结构(在这一点上与Erlang类似),Scala会根据系统资源和任务处理时间自动地管理一个JVM的thread pool,然后把队列或者mailbox中的Actor代理给thread pool中的空闲thread实际执行,执行完毕这个被征用的thread可以还给pool并让下一个actor使用。
问题是,当正在工作的threads的数量超过CPU的数量时,就一定会导致threads的上下文切换,因为它们通常都需要分配一定的运行机会。
与Erlang不同的地方是,这个机会的分配,在Scala中,仅在有限数量的工作threads (workers)之间(非所有的actors时间)展开;而Erlang,则在所有的processes(actors)之间展开。
erlang在底层做了很多我们想想不到的优化.
binary小于64byte是在ower process保存的,这个在作为message发送时,需要copy。大于64byte的binary就是通过reference来进行了。
至于其他的情况,erlang的老大们很聪明,也很执着。他们知道什么时候使用copy,什么时候仅仅需要一个地址。
刚才最后一段话,表达地完整一点:
与Erlang不同的地方是,这个机会的分配,在Scala中,仅在有限数量的工作threads (workers)之间(非所有的actors之间)展开;而Erlang,则在所有的processes(actors)之间展开。
另一方面,Scala的上下文切换发生在JVM的thread级,Erlang的上下文切换,发生在轻量的process级,Erlang vm的process切换效率比JVM的thread要好些,这也是为什么我们看到thread-ring的测试中:
http://shootout.alioth.debian.org/gp4/benchmark.php?test=threadring&lang=all
Erlang的成绩最好的原因。
但要注意到,这个成绩的取得,和这个任务本身只是作一个简单的消息传递(!)是相关的,因为简单地传递一个短消息的时间与任务切换的时间是一个数量级的。但如果让每个ring传完信息后再计算一个很大的Fibbonacci数,那Erlang肯定输给Scala,因为这时主要的时间消耗在计算Fib数上,任务切换的代价这时反倒不显著了,这当然,也是因为Erlang vm的计算性能比JVM差。
对于Distribution的系统来说,加入Static Typing 最为麻烦的事情是如何传输类型如何进行类型的推导.
T={people,Age=1,Score=2,Id=3}.
P!T.
revieve
{people,int:Age,int:Score,int:Id}->
…..
那么势必要求,T在传输的时候是要这样吗?{people,(Int)1,(Int)2,(Int)3}.
如果某个filed是个非常复杂的继承结构呢?
如果我传输的是一个List串呢?每个Item都要标示类型码?(如果List的类型系统可以容纳不同的Item类型)。
Ok.这些东西都解决了.那么Recvieve以后我们如何分派类型呢?要知道Static Type的类型是编译期生成的.而recieve的数据是运行时得到结果的.
Recieve
{people,int:Age,int:Score,int:Id}->
在编译完成后就无法更改了.按照上面某楼说的,如果协议变了,变成了
{people,int:Age,float:Score,int:Id}.那么接收方该怎么办呢?类型推导吗?
如果你想类型推导,那么问题就来了.
如果我们写成这样
Recieve
{people,var:Age,var:Score,var:Id}->
do(Score).
如果此时var类型是一个新类型.而do(Score)函数在编译期对这种类型完全没有展开过.那么我们在执行do(Score)之前是不是要对var类型做一次新的编译推导?可以设想一下do(Score)内部使用的类型推导像Boost库里面某些变态代码那么复杂,那么运行时编译一次的代价又有多高呢?
我觉得有两个东西还是需要分清楚的:
1、操作系统的线程切换
2、Actor模型的线程切换
我们说的,Scala中一个Thread执行完一个Actor的消息再去执行另一个Actor的消息,其中发生的是第2种线程切换,而在实现时,它可能只是重新拿出一个Actor来再获取一个消息,再执行,期间根本没有发生第1种切换。
如果执行一个很大的Fib计算,那么在这里不会发生第2种切换,而第1种切换是必然会发生的。因为第1种切换由操作系统管理,例如分配时间片,计算时间超过时间片了,于是换一个操作系统线程。
即使是Scala这种使用线程池,有限个Thread,例如100个worker,它执行时也是在处理例如10000个Actor。
而Erlang,我不知道它的调度方式。据你的描述,它似乎不是由(相对)少数个worker来处理大量Actor,而是像操作系统一样,为所有的process均匀分配计算资源?
@Trustno1
首先,静态类型的传递,再Scala的Actor模型中,pattern match除了可以match List,Array,Tuple,也可以直接针对类型,特别地case class:
case class People(age:Int, score:Number, id:Int, a:Array[Any])
react {
case People(age, score:Int, id:Int, _) => do(score)
case People(age, score:Float, id:Int, _) => do(score)
case People(age, score:Number, id:Int, Array(a1:Int, a2:String, _)) => do(score)
case _ =>
}
其次,如果接口变化了,源码修改了,那么得先静态编译一遍,调用者和被调用者的代码都得通过静态类型检查,然后就是类型安全的代码了。至于怎么动态reload到运行环境中,如果系统中没有共享的状态,简单些,否则,设计迁移方案。
@Trustno1 这视角,独了~赞!
如果加入静态类型:
P ! {people,int:Age,int:Score,int:Id} % version 1
P ! {people,int:Age,float:Score,int:Id} % version 2
receive
{people,int:Age,int:Score,int:Id}-> % version 1
do(Score)
….
{people,int:Age,float:Score,int:Id}-> % verson 2
…
do(Score)
end
do(int:Score) ->
…;
do(float:Score) ->
….
Erlang的风格:
P ! {people,Age,Score,Id} % Score is integer or float, version 1 = version 2
receive
{people,Age,Score,Id} ->
do(Score)
end
do(Score) when is_integer(Score) ->
…;
do(Score) when is_float(Score) ->
….
Erlang的方法有什么不好么?
Erlang內建分布式支持,Erlang的哲学是非防御性编程,有错误就抛出来。所以静态类型不会给erlang带来太多的好处,相反是复杂罗嗦的代码。
静态类型所做的工作,不会100%保证你的代码质量,你还是需要test. erlang知道这个道理,让代码简介,让开发速度加快,就成了目标。
@Caoyuan
》1、在Erlang中,guard函数最初的目的是简单的运行时类型检查和分发约定,-spec, -type是试图加上编译时类型检查;两种机制现在没发合并到一块,或者说,相加上这些时有点晚了,因此有人提出应该写成这样:sub_string(String::list(), Start::integer(), Stop::integer()) -> … end.
这样就统一了,当然这样也就跟Scala等一样了。
你说的“两种机制现在没法合并到一块”,我觉得这倒还真是一个问题。
现在 -spec 方式的一个严重问题是,同样的信息你必须要表达两次,一次是在写代码的时候来表达,另外一次是在写 -spec 的时候表达。而且,在这个两次表达之中,还凭空多出了一个新的来源。那么,如果这两者发生了冲突,应该怎么办?又比如说,如果我更新了程序代码,却还没有更新 -spec 的内容,岂不是会出现 spec 与 code 的不一致,又要由谁通过什么机制来维护这两者的一致呢?如果这必须要依靠人的自觉性,那和编程的约定和规范又有多大的分别?
我觉得,在这里应该遵循“单一信息来源”的原则。因为 Erlang 已经将“参数的位置及其对应类型”(实际上还包含了对应的值)作为函数模式匹配的语法设施提供给了编程者,让其在运行时中使用,那么编程者的程序本身就已经完整的包含了“参数位置及其对应类型”的全部信息,而且是对这一信息最为精确的也是最终的描述(再邪乎那么一点就有“程序即文档”的意思)。也就是说,现在需要由我们手工来添加的 -spec 信息如果能从程序逻辑之中“抽取”出来,就会是最为精确的而且能够保证同步。
但现在的状况是,你不仅需要手工的方式来重建这些信息,而且,其表达能力还足够强大,简直就是另外一个语言,以至于可以从中发展出一套复杂的类型系统。然而这套类型系统却又还不是编译过程中的必须,如此折腾为哪般?
在我自己的实践中,目前仍然以 @spec (R12的文档注释)语法为主。我就是将其当作外挂的注释规范和文档生成——一个不是编译阶段所必须的类型系统,注定会是一个外挂的半吊子类型系统,我对它没有太高的期望。
》2、即便是有这些约定了,表达更复杂的类型时怎么办?现在Erlang定义复杂类型的主要手段是基本类型加List、Tuple化扩展。但是,这未必是够的。有人可能认为这样就够了,那也没关系,至少,我处理的问题域中,Scala的完整、一致、包括协变的类型体系很有帮助,它可以在很大程度上帮我分析、设计整个系统。
谈到参数的复杂类型,显然,标准的做法就是在 .hrl 文件中定义公用的 record 以作为多个模块之间的共享。但是我现在对于这么做的必要性开始有一点点疑惑——我们真的需要那么复杂的参数类型么?
尤其是在表达接口的时候。在这个层次,我个人更加关注的是,这个参数表达的是什么(貌似名称更重要),而不是它是怎么构成的。在我的编程实践中,很少碰到需要像 C 一样“需要定义一个复杂的 Struct 来作为输入输出的约定”的情况,大部分的情况下 tuple 和 list 都已经足够。当然,这是我个人的编程实践,也许并不具有普遍性。或许只有拿个具有代表性的例子出来才有讨论的基础。
》unit test,类型系统,各司其职,有什么不好吗?
如果不将复杂的业务逻辑通过几乎同样复杂的类型系统来进行表述(比如,回退到 Erlang 的几种基本类型),那么这样的类型检查系统还有那么大的必要么?
申明:以上回帖,个人观点居多,小心取舍。
》我们说的,Scala中一个Thread执行完一个Actor的消息再去执行另一个Actor的消息,
》其中发生的是第2种线程切换,而在实现时,它可能只是重新拿出一个Actor来再获
》取一个消息,再执行,期间根本没有发生第1种切换
可以这么理解,但在执行一个Actor的任务**期间**有可能发生第1种切换。
》如果执行一个很大的Fib计算,那么在这里不会发生第2种切换,
》而第1种切换是必然会发生的。
也可以这么理解,但如果处在“工作”状态的actors们此时正好有计算完成的,会发生第2种切换:新的actor被代理给这个空出来的thread。
》即使是Scala这种使用线程池,有限个Thread,例如100个worker,
》它执行时也是在处理例如10000个Actor。
那一个简化的情况作例子,假设这100个workers总是能在同样的时间里完成任务,那么你会看到输出/打印的结果是:计算头100个,需要时间,输出100个的结果,然后下100个…,每100个一批。
同样的假设下,Erlang则可能是:10000个process在同时计算,等了很久很久,然后看到10000个结果同时出来了。
@Caoyuan
经过你的解释,我感觉我的确理解了Erlang和Scala在调度上的区别了,呵呵。在某些情况下的确是Scala较为有优势(我以前的C# Actor模型也是类似方式)。
不过话说回来,Erlang的调度方式,需要能够中断任务,保存上下文,才能把计算能力分配给其他任务。而使用Java或.NET这种高级平台,进一步抽象出来的Actor模型,基本上也只能选择Scala这种方式了。
呃讨论的本体都有点歪,不是 弱类型是否足够用,也不是在erl中如何对付复杂业务最好;
应该是 “erl 的最大问题” 本身:
- 问题成立嘛?
- 真是是最大的问题?
咔咔咔,就俺看,这种讨论已经被Erlang 开发的各种大型商用软件长年运行的现实反驳了,
是否最佳实现?
用OTP 就保证了开发的规范和运行的稳健了,
具体内在机制是否合理…
只能说,没有完美的语言,在对口的领域,可以解决大多数问题,就是好语言
》同样的假设下,Erlang则可能是:10000个process在同时计算,等了很久很久,然
》后看到10000个结果同时出来了。
这是Erlang特定设计的结果,不像其他平台完全没有规章。如果“计算头100个,需要时间,输出100个的结果,然后下100个…,每100个一批”是你需要的功能,那为何不通过设计来明确规范地表达,而非得依靠底层平台的在某个时间的某种特定实现逻辑呢?
计算能力的分配是个设计问题。
@Jeffrey Zhao
其实现在各种语言互相借鉴的趋势很明显,ErlangVM说不定哪天也会支持Scala方式的调度,而JVM下的Thread实现也越来越轻量,哪天也能在必要的时候达到Erlang的调度效果。
但在目前而言,除了大量、软实时、短信息交换的应用,Scala适用领域应该更大些。这也是为什么在写了进10万行Erlang代码后,我开始更多地用Scala,因为我碰到的问题域都是业务逻辑比较复杂的。但Erlang的process模型和function style让我获益多多,用到Scala里也是得心应手。
@dsun
总得先搞清楚不同平台下特定的结果吧,这样在出现某些特定故障的时候才能知所以然。我可能比较笨,直到去年才想清楚这两种调度机制的不同。
至于设计,Erlang下也能设计出同样的结果(每100个一批),Scala下也能实现10000个同时,但具体实现的复杂程度各不相同。
例如在传统场景中,thread per request(对应到Erlang便是process per message),这样虽然可以利用所有计算资源,但是随着message数量提高,Response Time增加,吞吐量会下降。而Scala的做法,吞吐量不变,虽然Response Time也增加了。
这个就是SEDA架构解决的问题。在Erlang来说,如何实现Scala的要求?是不是需要开启有限数量个节点来计算大量任务?如果是较为复杂的做法,我觉得我在这方面还是更倾向于Scala。
》其实现在各种语言互相借鉴的趋势很明显,ErlangVM说不定哪天也会支持Scala方
》式的调度,而JVM下的Thread实现也越来越轻量,哪天也能在必要的时候达到
》Erlang的调度效果。
不明设计目标的借鉴,对语言来说就是灾难。Scala的调度方式,目前来说,其实就是为了实现高并发在JVM上打的补丁。
》但在目前而言,除了大量、软实时、短信息交换的应用,Scala适用领域应该更大
》些。这也是为什么在写了进10万行Erlang代码后,我开始更多地用 Scala,因为
》我碰到的问题域都是业务逻辑比较复杂的。但Erlang的process模型和function
》style让我获益多多,用到Scala里也是得心应手。
不知道您在解决复杂的业务逻辑时,有没有考虑过软件容错问题。
》例如在传统场景中,thread per request(对应到Erlang便是process per
》message),这样虽然可以利用所有计算资源,但是随着message数量提
》高,Response Time增加,吞吐量会下降。而Scala的做法,吞吐量不变,虽然
》Response Time也增加了。
》这个就是SEDA架构解决的问题。在Erlang来说,如何实现Scala的要求?是不是需要
》开启有限数量个节点来计算大量任务?如果是较为复杂的做法,我觉得我在这方面还
》是更倾向于Scala。
你这个例子举得挺好的,这种场景正是Erlang的强项,取得的效果正好和你描述的相反,自己google吧。
@Jeffrey Zhao
对。
Scala中,可以设计成per request per actor,然后让Scala去调度需要的实际threads数目。Scala在这方面还有一个值得称道的地方,那就是会根据系统的容量先估计一个需要的threads数量,然后根据实际运行时任务完成的速度自动调节threads的数量,比如,如果任务很快完成了,而等待的actor又很多,就适当增加threads的数量,反之,就减少。
》总得先搞清楚不同平台下特定的结果吧,这样在出现某些特定故障的时候才能知所以
》然。
对应用开发者来说,这个还是不要搞清楚的好,搞清楚了在设计上就会偷懒,然后就会出莫名其妙的故障。
@dsun
》Scala的调度方式,目前来说,其实就是为了实现高并发在JVM上打的补丁。
我认为不能叫补丁,而应该是非常合理的模式:Service good first.
》不知道您在解决复杂的业务逻辑时,有没有考虑过软件容错问题。
当然要。在不同的层次用不同的手段。
》当然要。在不同的层次用不同的手段。
能不能举个实例,简单谈谈?
想请教一下什么是设计?是指艺术设计吗?
这也是SEDA特性了,使用可调节的线程池和控制器进行控制。
不过dsun兄弟认为我举的这个例子正好是Erlang的强项,希望您可以解答一下。想听听同时有Erlang和Scala背景的程序员的看法。:)
》想请教一下什么是设计?是指艺术设计吗?
呵呵,真会开玩笑。比如基于优先级别来进行计算能力的分配就是在偷懒,@litaocheng 给出的方法就是设计。
@dsun
在网络层次,比如,股票数据,除了卫星大锅,还有交易所附近的光纤连接的主站,以及分布在各地的各个点到主站的连接;
在主机层次,主机本身可能需要是双电源的,硬盘是要raid好的;
在数据库层次,要cluster好的;
在应用服务器层次,也要cluster好的;
在笔记本电脑这边,要保证电池充足了电,然后联在电源上,同时还有一部直线电话和手机可以下单。
》在网络层次,比如,股票数据,除了卫星大锅,还有交易所附近的光纤连接的主站,
》以及分布在各地的各个点到主站的连接;。。。。
您举得是系统容错方面的措施,我说的是软件容错。比如,软件中有个bug,在处理某条消息时,会导致系统崩溃,虽然应用服务器做了集群,因为是同样的软件版本,就可能导致整个集群崩溃,这种情况咋解决。
其实,我赞成static type对健壮性和开发效率上的优势.但是这只能局限于单机系统.在分布式环境下,静态类型其实帮不了多少忙.比如说上面某楼提出的openfile修改的问题.在分布式环境下,无论是否引入static type或者OO都无法解决问题.
react {
case People(age, score:Int, id:Int, _) => do(score)
case People(age, score:Float, id:Int, _) => do(score)
case People(age, score:Number, id:Int, Array(a1:Int, a2:String, _)) => do(score)
case _ =>
}
像caoyuan给出的这个方案.无法避免全面性的更动.
这一点上,Haskell社区其实研究了很多年了.对于分布式的类型系统基本上目前没有什么好方法.所以那个以模仿Erlang为目标的Distribution Haskell很早就夭折了.
dsun的这个结论我也看不明白了,怎么结果正好相反呢?我认为随着请求量的不断增加,erlang和scala都会遇到响应时间变长的情况。只是erlang可能会让所有请求都超时,而scala让一部分请求超时。到了这个时候不管用哪种方案都该扩容了。
erlang的系统设计最重要的一点就是公平。IO公平 经常调度公平 bif执行公平一切都是在公平的基础上的。这个公平在我来看, 对于大型的系统来讲至关重要的,就好比我们要构筑现实的公平的社会一样。你想多利用资源,你的靠人数(进程数取胜),等于告诉系统,我的人头多,我的票数就多,分的资源就多。这是erlang的哲学。
》dsun的这个结论我也看不明白了,怎么结果正好相反呢?我认为随着请求量的不断
》增加,erlang和scala都会遇到响应时间变长的情况。只是erlang可能会让所有请求
》都超时,而scala让一部分请求超时。到了这个时候不管用哪种方案都该扩容了。
呵呵,没表述清楚。我的意思是Erlang的优雅降级能力要优于Scala,而且伸缩起来也更容易一些。
》erlang中的调度器,更加接近os级别,其追求process的机会均等。
》某些语言中,你可以保证某个process具有超强的优先级,可以获取更多的cpu。
》在erlang中,这个也不是缺点,为了完成你的需要,可以有多种解决方案:
》1,为某些资源占用密集的process,分配多个process实例,以多达到这个目的(有点蠢)
》2,调整某个process的优先级(erlang中的节点间connect process就是设置priority为max,
》保证连接能够快速建》立),还是不行?
》3,启动一个独立的erlang node,你自己去独享一个vm,想怎么玩你就怎么玩.
》最后,还是尽量让你的应用处于一个平衡的状态,不要让某个process做太多的事情。
》把80%的任务交给某几个进程去做,肯定不是好主意。
好吧,我们看看这个设计。
1、如果某个任务不能分解为多个processes并行算,那么方案1就不行了;
2、那么给它个优先级,比如加一级,在系统有4个CPU时,我们的测试发现在有其它400个processes时似乎够了。然后,由于业务的需要,甚至,由于CPU增加到了8个,我们的系统中同时运行的processes数现在增加到了800个,这时可能发生什么呢?
假设这个特别process的优先级导致它能10倍于普通的processes获得CPU的时间,那么在400个其它processes运行时,它的机会是:
10 / (10 + 400) = 2.44%,
而在800个其它processes运行时,它的机会是:
10 / (10 + 800) = 1.23%,
就是说,系统的CPU数增加了,它的运行机会反倒下降了,这时你会发现数据全堵在它这了,Erlang最后crash掉。
以上就是我在用erlang中的log函数时遇到的情况,如果不了解Erlang的调度机制,你能找到问题所在么?或者,你能在一开始时就知道方案1和方案2的设计可能不合适吗?
其实对于类型方面的事情 我想举的例子是 linux内核 只要2中数据 数组和list 可以对付几百万行的系统的需求。erlang的设计定位是类似电话系统这样的应用,对于这类应用来讲,目前的数据结构和表达方式,个人感觉是够用了。但是如果把它应用于其他领域,超过了设计者的预期,所以会很不顺,这是事实!
其实erlang的优先级没用的。进程的优先级确保了进程能够优先调度,但是记住一个原则就是公平原则。你只是调度,并不说明你可以多获取cpu时间,你的cpu时间每次还是3000redution. 所以对于cpu密集的应用 唯一的办法就是多开进程实例,相对于其他进程的数量优势来获取计算力的优势。
@Caoyuan
我是说@litaocheng至少在设计上考虑这个问题了,没有偷懒,方案2还是基于优先级的。他不是还有方案3的吗。
这种情况下,Erlang照样可能垮掉,就算superviser救起来,过一会照样垮。见我blog中log导致erlang crash的例子。
JVM下,某个actor处理某条消息时出错也同样可以catch住,特别地,如果这个actor也跟其它actors sharing nothing,垮掉就垮掉,其它的actors也可以平安无事。
@Caoyuan
R13B以后支持多进程队列的调度,CPU的利用率问题提高了非常多,cpu在调度的时候考虑到负载均衡了, 但是在设计的时候还是要避免单进程设计。
我在实际编写erlang程序的时候感觉到,如果record要是能支持继承关系,那么就能表达更复杂的数据结构,也就能承载更复杂的业务逻辑了。
@Caoyuan
erlang的otp设计者意识到这个问题了,你看crypt 应用的设计就不是单进程的设计了。这个logserver的问题最好是在一个独立的节点里面开。
@Caoyuan
恩,你说的问题,我也遇到过。
erlang vm因为某个process累计了上百万的消息而crash。
这个问题,我们使用1的蠢办法解决,虽然不是很漂亮。
其实归根结底是因为我们的系统不平衡,这个负载的“热点”区域应该隔离到一个独立的系统中。比如独立的log server。如果再次出现“热点”,那明显是系统需要升级了。
先澄清一下,我好像没有说要用优先级来达到特定的目标,我想我说的**即使**可以利用优先级来达到目标,也还是会出错。
那么说说方案3。这在很多情况下是好的设计,好在比较彻底的解偶。实际上我们的系统中,最后的设计是Erlang快速处理前端请求,Scala的另一台独立的机器上完成大量的XML文本解析和查询任务,两者之间通过JInterface通讯。
@dsun
各位争论得正酣,我问个题外的话,其他大拿的大名我都有所闻,但是恕我孤陋寡闻,dsun大拿有没有博客?
@fsong,建议你先去阅读大拿翻译的《Joe Thesis》
至于前面说的log问题,后来的方案是将异步log调用改为同步处理,即在同一个process中等待log process完成并返回后在继续,这样的好处是每个请求的任务在一个process中同步完成,不需要去计算分解到不同任务的process后平衡的问题,这样,简单地scale-out(加机器)就可以实现横向扩展。
对,这个很重要。记得第一次跟他们提这个问题时,他们还争辩是我log方法用错了:-)
确实不知道还有人翻译了,汗.学习.
接着讨论,呵呵。
》这种情况下,Erlang照样可能垮掉,就算superviser救起来,过一会照样垮。见我
》blog中log导致erlang crash的例子。
Erlang中处理这个消息的进程是会垮掉,但是系统对于其他消息照样能处理,所以这就是软件容错。
》JVM下,某个actor处理某条消息时出错也同样可以catch住,特别地,如果这个
》actor也跟其它actors sharing nothing,垮掉就垮掉,其它的actors也可以平
》安无事。
这个就加重应用程序设计者的负担了,而且还不容易做好。这就是工程化和非工程化的区别。