什么是流式思维?

架构治理如同大禹治水,以疏代堵,顺势而为,大道如水,那我们的思维也要切合如水。

首先什么是流?最先联想到的是“水流”,潺潺不断,流是一种动态过程,如果你想截断水流,最简单的是堵住它,所以,流是不能被堵塞的,也就是非堵塞,只有非堵塞才会形成流。

中国有句古话:百川入海,如果水流没有被堵塞,随着时间推移,再多的水都会流入大海,如果把水看成是数据,很显然,流处理方式可以处理无限的数据,只需要消耗时间。因此,流式是对数据量开放的。

流式这种不畏惧数据量的特点在现实生活中比比皆是,比如我们看电视,这是通过流式获得大量信息的输入;比如我们逛大型超市,从入口一直溜(流)到出口,通过这种流式我们能获得大量商品信息。

那么为什么要提出一种流式思维呢?因为思维决定软件设计,设计决定系统,而我们的系统是模拟真实客观规律运行的,因此我们的思维必须采取最贴近客观规律的方式去思考,那么客观规律是什么特点?

量子力学的波粒二象性和薛定谔猫理论认为:当我们观察量子时它是一颗颗有边界的粒子,而我们不观察它时其实是连续的波。也就是说,在微观世界,客观规律隐式的其实是一种流式的波,连续而不间断。

那么为什么我们在日常生活中没有觉察到这一现象呢?那是因为我们人类自己作为观察者身在其中,所以,我们看到的都是一颗颗有边界的“粒子性”的对象。但是,身在庐山中不识庐山真面貌,别忘记客观规律可能还会有连续流式的特点,因为世界是运动的。

我个人斗胆将波粒二象性这一物理理论推广到思维哲学世界,认识世界不只是需要面向对象思维,还需要流式思维。

客观世界是一种类似波的流式系统,流式思维是最接近隐式的客观规律。

前面扯了那么多咸蛋,回归到软件领域,如果说粒子性的面向对象是采取空间上隔离的方式,那么流式思维正好相反,是一种空间上连续没有隔离堵塞的思考方式,这其实代表了面向函数的一种思维方式。面向对象和面向函数代表了我们两种思考方式,这两种思考方式不同,因此针对不同领域采取不同方式。

面向对象特点
我们前面说过,当我们观察量子时,它是粒子,因此,粒子性的面向对象方法适合从人的角度观察分析世界,比如我们看到一个实体如电视机,这个电视机是有实在大质量的物体(大粒子),因此,我们会在软件设计中建立一个对象叫电视机:


public class TV{
private String name;


}

这个电视机有名称和规格等等,其功能特点是能开机,播放,换频道,关机等。

public class TV{
private String name;

public void powerOn(){
..
}

public void play(){
..
}

public void powerOff(){
..
}

}

有了电视机的这个实体和它的功能是不够的,因为我们是为了利用它的这些功能,当然不同的人会有不同的用法,一些人用电视机是为了看电视剧;一些人是为了看新闻;一些人是为了看娱乐节目等等,如何让一个实体为这些不同的人群服务?
我们为这些不同应用场景设计不同的微服务即可,如下:

public interface DramaService

public interface NewsService

public interface EntertainmentService

然后设计一个界面列表出上面这些服务让用户选择进入。那么这个系统是否就完成了呢?很显然,还有一个很重要的问题我们没有考虑,那就是如果这些人合用一台电视机怎么办?一台电视机不可能同时播放几个频道啊,只能在某个时刻播放一个频道,这种在某个时刻的状态是我们必须关心的重点,那我们添加一个状态到TV中:

public class TV{

private ChannelState state; //当前这台电视机正在播放的频道

}

有了状态这个值对象,我们就能有序控制这台电视机为不同人服务,如果两个人想同时看不同节目,我们的业务规则会告诉他:“非法”操作。我们用面向对象的方式封装了规则和法律。

面向函数流式特点
以上是面向对象的分析设计过程,这个方法特点是我们首先建立一个TV实体对象,然后在其内部封装了体现业务规则的可变状态。而面向函数的流式方法则正好相反,我们不关心某个TV实体,更不关心实体内部是什么,我们更关心多个TV怎么办?无限个TV怎么办?比如在电视机制造厂希望制造的电视机越多越好,每个电视机内部属性特征最好是不变的,都是一样的,这时电视机生产商就会采取流水线作业来生产无限个电视机。

同样,对于电视台来说,无限个节目数据如何进入电视机播放才是最重要的,因此采取流媒体的方式进行推送无限个节目视频。

所以,流式的面向函数思维会更侧重平台制度性的建设,会忽视个体的差异,拥抱个体的大数据量。从这点来说,流式思维更适合进行大数据处理和分析,比如hadoop和Spark。

建设一个面向流式处理的平台如同铺设自来水水管的建设,更注重管道的规划和有序合理性,防止管道之间汇聚发生堵塞,典型的就是道路这种管道建设,如果两条路交叉,那么在两条路上的车辆就会发生碰撞堵塞,红绿灯设置只是临时之举,正如CPU的线程上下文切换一样,CPU就是线程的红绿灯,可以暂停某条线程,先执行其他线程,然后再唤醒这条线程继续执行。

流式处理最重要的基本要素是事件,事件是一个一旦发生就不再改变的值对象,每个事件结构基本一样,我们不关心事件内部的规则和状态,而是关心无数个事件的处理,因此事件是一个流式系统的基本要素,有了无数个事件就需要有事件的源和目的,掌握这三点就容易帮助我们理解流式系统。

流式思维在大数据架构上应用
比如Storm这个大数据处理框架,其有几个好像陌生的概念:Spout Tuple Bolt,如果单从字面上理解是“嘴” “元组” “螺栓”等意思,但是你从流式角度去思考就很容易理解,Spout嘴是产生事件流的源,而Tuple元组代表一个个小事件,多个事件就是Tuples了,Tuples代表了事件流,而Bolt代表流过程中一个处理器,如同将两个水管接在一起的接头。http://www.jdon.com/bigdata/storm.html

流式总是和集合概念相对应的,因为集合一般是很多数据个体的总称,流式不像粒子性面向对象那样注重个体,流式是注重集体集合整体概念的,集合一般是流式的源泉,比如Java中集合处理,以前我们是将连续的波流人为地切断为一个粒子点,就像把线看成是点组成一样,那么就有下面代码:


Collection values = …;
for (Object o;values) {
...
o.xxx();
}

我们使用for循环实际就是将values集合切分点一个个个体的点,然后逐点处理。这是一种粒子性的面向对象方法,用在这里显得笨拙,因为我们并不关注某个对象内部静态特征和属性,而是关注很多个这些对象,这些对象都有一个统一的特征,都是一样的,熟悉访问者模式的人可能会联想到,访问者模式正是通过强制给集合中对象强加一个统一的accept方法。

熟悉访问者模式说明你已经一只腿迈入流式思维的大门中,我们看看使用Java 8的面向函数范式如何实现这个案例的流式处理:
values.parallelStream().filter (..).forEach(..);

流式思维在微观上应用
流式思维不但可以应用在大数据处理上,也能使用在微观的CPU使用上,前面我们谈到CPU类似红绿灯调度员,调度两条线程(水管)的协调,如果我们采取流式思维,设计出不会交汇的多条水管,那么CPU就不会去做红绿灯这种看似高效率实际是低效率的事情。比如我们在安卓编程中,可以采取如下流式编程:


apiService.login() //后端线程
.getUserData()
//后端线程
.saveUserData()
//后端线程
.subscribe(Subscriber {
//UI线程
void onComplete(){}
void onError(Exception e){}
void onNext(Object o){}
});

我们在这个代码中相当于启动了四条不会交汇的流,实际是四个线程,这四个线程正好利用四核CPU,这样避免了一个CPU做红绿灯。详细进入


流式思维还可以巧妙避开资源争夺问题,比如回到前面TV 这个案例,这个案例是在家庭应用这个上下文场景下设计的,试图解决一台电视机供不同人作为不同用途的问题,电视机在这里是一个资源,我们通过引入可变的当前的频道状态来协调多人看不同频道的问题,这实际也是类似红绿灯,某个时刻电视机要么播放娱乐频道,要么播放新闻频道,或者电视剧,只能有一个状态,但是如果有两个人同时换频道,电视机到底听谁的呢?代码如下:


public class TV{

private ChannelState state; //当前这台电视机正在播放的频道

public void changeChannel(changeCommand cmd){
state = cmd.getState();
//修改当前状态为新的状态
}
}

这里就发生了资源争夺问题,传统解决办法也是使用红绿灯方式,也就是加入锁,如引入同步synchronized:

public void synchronized changeChannel(changeCommand cmd){
state = cmd.getState(); //修改当前状态为新的状态
}

这样某个时刻只有一个线程能操作这个方法,这种拍拖式的红绿灯其实是一种堵塞方式,在大量用户同时操作时会产生让浏览器用户产生网络无响应,或者操作失败等各种问题,甚至引起死锁后当机,不少网站系统在承受大量用户访问后瘫痪有不少原因是因此引起的。

相反,流式思维就能我们巧妙回避了资源争夺问题,我们在changeChannel方法调用之前加入一个管道队列Queue,同时无序的混乱操作变成了有序的流式处理,这种性能成本要远远小于同步锁带来的成本。单写原理

同样,如果我们在changeChannel 方法中需要再操作其他资源,特别是慢的资源操作容易引发堵塞,就像免费高速通行时总是堵塞在收费口,虽然收费口不设置收费了,但是车辆进入收费口时是需要减速的,同理车流也会堵塞在大桥上面,因为大桥是限速的。比如这里我们会将状态保存到数据库中,数据库操作需要连接池等慢处理:


public void changeChannel(changeCommand cmd){
state = cmd.getState(); //修改当前状态为新的状态
repository.save(state);
//这个慢速会成为整个方法处理过程的瓶颈。
}

解决这个问题通过引入流式处理,流式方法的特点是事件流,那么我们这里引入事件:

public void changeChannel(changeCommand cmd){
state = cmd.getState(); //修改当前状态为新的状态
producer.send(changeEvent en);
//触发一个changeEvent事件,没有堵塞
}

持续不断地调用changeChannel就会产生持续不断的事件流,在事件流的终点,我们再进行慢速处理,这样不会影响原来的流程,这是在一个水流管道上再分流出一个管道,从而不影响原来水流的操作方式,正如南水北调工程是在长江这个水流中分支出一个通道流向北方。

这种流式处理正是LMAX架构的核心,依靠Disruptor这种管道队列,LMAX能做到每秒处理600万订单的吞吐量,正如设计一个无堵塞的港口,其处理能力只有时间的限制,没有被处理对象数量的限制一样。

上述案例,我们巧妙将慢速的数据库操作转移他处,但是要真正解决数据库的资源争夺,我们还是可以使用流式思维,数据库就不要存储可变的状态数据,因为对同一个数据表记录会存在多个修改的引发的争夺现象,我们只要向数据库或文件中增加新数据,而不是修改原来的数据,永远只是追加Append操作,这种Append是典型的流式操作,我们可以将事件流不断追加到数据库或文件系统等慢速设备上,需要时再通过事件流加载再次播放到指定的状态。这是Event Sourcing/CQRS的基本思路。

总之,流式思维应用在架构设计上大有好处,架构治理如同治水,流式思维类似大禹治水,以疏替堵,顺势而为,面向对象 面向函数 异步编程 并发编程等都只是手段方法而已。

[该贴被banq于2014-05-24 10:47修改过]

总结上面流式思维几个特点:

1. 流式擅长处理大数据
流式来源是集合而不是个体。

3. 流式以疏导替代红绿灯式的堵塞
流式管道不可交汇,避免拥挤
流式管道可分支,缓解拥挤。

流式架构的两个案例:
基于大型Node.js的流式架构开源项目:
Arch-orchestrator是Node.js流式架构指挥家

使用Apache kafka消息系统与Storm组成的分布式流式处理架构:
kafka + storm 集成源码案例
总结kafka+storm原理的流式大数据模式: http://www.jdon.com/45698

[该贴被banq于2014-05-26 10:11修改过]

打个比方:2个人去看病。
A情况(阻塞):2个人争抢医生看病,争抢去验血,争抢。。。一个人看病时,另一个人就要等待。病人都堆在医院里,有限的椅子、有限的医生、有限的医疗设备都是他们争抢的资源,而这些资源没有在有限的时间里最大合理化的利用。
B情况(非阻塞):提前安排,病人甲8:00去医院看病,病人乙8:30去医院看病;病人乙8:30到医院面见医生时,病人甲可能已经去验血拍片了。。。资源规划+事件通知,病人不用全部一个时间呼啦都跑到医院,也不用在一个诊室、验血窗口、CT室等等没完没了的排队。

这些环节中,其实那些费时费资源?诊断过程、验血化验、拍片子等。那些不太费时,预约、诊室到下一个诊室、通知等。

比如就4个医生(好比4个CPU):
1、首先病人预约,应该有个请求队列和响应队列,请求受理,响应通知。
2、然后病人按照预定时间去看病。
3、进出诊室都要经过请求-响应这一过程,病人看病的过程都被合理的安排进行,4个医生充分利用,不会出现A医生面前4个病人,其它医生面前没有病人而闲着。

我觉得是服务思维,如何提供优质的服务,一定要有通知,好比ajax,我为你服务,然后加上回调通知,你不用等待。其实就是把你的服务订单放入我的请求队列,然后你可以干别的去,你不用在这等着(阻塞)。我这里把事情做完了,把我的结果放入响应队列,然后由客服(比如UI线程)发通知给你就行了。
[该贴被oceannut于2014-05-26 17:12修改过]

其实阻塞或不阻塞,都是针对用户的感受而言;不管哪种模式,服务可能还是要干同样多的事情。但是非阻塞可以在时空上更充分合理的利用好有限资源,并给用户提供一个优质的服务感受。

我觉得不管什么设计思想总要解决2个问题:"一“和”协同".
“一”是强调单一性、纯粹性,不能重复。面向对象主要是解决一的问题,但并没有全部解决。
“协同”是指“一"之间如何有层次地交互。在我看来,流式思维可能更侧重”协同“。

其实,最佳的设计思想是要解决”一“和”协同"达到一种均衡,而不只是侧重一方。

//exports.start = start;