1、概述
FourInOne(中文名字“四不像”)是一个四合一分布式计算框架 。将Hadoop,Zookeeper,MQ,分布式缓存四大主要的分布式计算功能合为一个框架内,对复杂的分布式计算应用进行了大量简化和归纳。
在分布式协同方面
,实现了Zookeeper所有的功能,简化Zookeeper的树型结构,用domain/node两层结构取代,简化Watch回调多线程等待编程模型,用更直观的容易保证业务逻辑完整性的内容变化事件以及状态轮循取代,Zookeeper只能存储信息不大于1M的内容,FourInOne超过1M的内容会以内存映射文件存储,增强了它的存储功能,简化了Zookeeper的ACL权限功能,用更为程序员熟悉rw风格取代,简化了Zookeeper的临时节点和序列节点等类型,取代为在创建节点时是否指定保持心跳,心跳断掉时节点会自动删除。FourInOne是高可用的,没有单点问题,可以有任意多个复本,它的复制不是定时而是基于内容变更复制,有更高的性能,FourInOne实现了领导者选举算法(但不是Paxos),在领导者服务器宕机情况下,会自动不延时的将请求切换到备份服务器上,选举出新的领导者进行服务,这个过程中,心跳节点仍然能保持健壮的稳定性,迅速跟新的领导者保持心跳连接。基于FourInOne可以轻松实现分布式配置信息,集群管理,故障节点检测,分布式锁,以及淘宝configserver等等协同功能。
在分布式缓存方面
,FourInOne可以提供完整的分布式缓存功能。如果对一个中小型的互联网或者企业应用,仅仅利用domain/node进行k/v的存储即可,因为domain/node都是内存操作而且读写锁分离,同时拥有复制备份,完全满足缓存的高性能与可靠性。基于FourInOne可以轻松实现web应用的session功能,只需要将生成的key写入客户端cookie即可。
在分布式大数据量并行计算方面
,不同于复杂的hadoop,它不像hadoop的中间计算结果依赖于hdfs,它使用不同于map/reduce的全新设计模式解决问题。FourInOne有“包工头”,“农民工”,“手工仓库”的几个核心概念。“农民工”为一个计算节点,可以部署在多个机器,它由开发者自由实现,计算时,“农民工”到“手工仓库”获取输入资源,再将计算结果放回“手工仓库”返回给“包工头”。“包工头”负责承包一个复杂项目的一部分,可以理解为一个分配任务和调度程序,它由开发者自己实现,开发者可以自由控制调度过程,比如按照“农民工”的数量将源数据切分成多少份,然后远程分配给“农民工”节点进行计算处理,它处理完的中间结果数据不限制保存在hdfs里,而可以自由控制保存在分布式缓存、数据库、分布式文件里。如果需要结果数据的合并,可以新建立一个“包工头”的任务分配进行完成。多个“包工头”之间进行责任链式处理。总的来说,是将大数据的复杂分布式计算,设计为一个链式的多“包工头”环节去处理,每个环节包括利用多台“农民工”机器进行并行计算,无论是拆分计算任务还是合并结果,都可以设计为一个单独的“包工头”环节。这样做的好处是,开发者有更大能力去深入控制并行计算的过程,去保持使用并行计算实现业务逻辑的完整性,而且对各种不同类型的并行计算场景也能灵活处理,不会因为某些特殊场景被map/reduce的框架限制住思维,并且链式的每个环节也方便进行监控过程。
在MQ(消息队列)方面
,FourInOne可以当成简单的mq来使用,将domain视为mq队列,每个node为一个队列消息,监控domain的变化事件来获取队列消息。也可以将domain视为订阅主题,将每个订阅者注册到domain的node上,发布者将消息逐一更新每个node,订阅者监控每个属于自己的node的变化事件获取订阅消息,收到后删除内容等待下一个消息。但是FourInOne不实现JMS的规范,不提供JMS的消息确认和消息过滤等特殊功能,不过开发者可以基于FourInOne自己去扩充这些功能,包括mq集群,利用一个独立的domain/node建立队列或者主题的key隐射,再仿照上面分布式缓存的智能根据key定位服务器的做法实现集群管理。
FourInOne整体代码短小精悍,跟Hadoop, Zookeeper, Memcache, ActiveMq等开源产品代码上没有任何相似性,不需要任何依赖,引用一个jar包就可以嵌入式使用,良好支持window环境,可以在一台机器上模拟分布式环境,更方便开发。
2、常用模式
包工头--职介所--手工仓库--工人模式
职介所: ParkServer ,可以部署在一台独立的计算机,在每次分布式计算时给”包工头“介绍工人,当工人加入集群时首先在”职介所“登记,然后”包工头“去职介所获取可用于计算的”工人“,然后职介所会和”工人“保持松散的联系,以便有新的工程时继续介绍该”工人“ 。用于分布式协同方面
工人:MigrantWorker,计算节点,可以部署到多机器上,业务逻辑由开发者实现。计算时,”工人”到“输入仓库”获取“包工头”分配的输入资源,再将计算结果放回“输入仓库”返回给“包工头”。
包工头:Contractor,负责承包一个复杂项目的一部分,可以理解为一个分配任务和调度程序,它的业务逻辑由开发者自己实现,开发者可以自由控制调度过程,如按照“工人“的数量将源数据切分成多少份,然后远程分配给”工人“节点进行计算处理。它处理完的中间结果数据不限制保存在HDFS里,而是可以自由控制保存在分布式缓存,数据库,分布式文件里。通常情况下,输入数据多半在工人机器上,这样有利于工人本地进行计算处理和生成结果。避免通过”包工头“进行网络传送耗用。”包工头“实际上是一个任务的调度者,它通过getWaitingWorkers方法获取线上工人并行调度任务执行,”包工头“是一个并行计算应用的程序入口,不是一个服务程序,运行完成就退出。
手工仓库 WareHouse 为输入输出设计,让计算的资源独立于计算的角色(包工头,工人),包工头和工人的数据交互都是通过手工仓库,它可以当作远程请求的参数,也可以是返回结果的对象。”手工仓库“可以存放任意类型的对象,它本身就是一个Map的实现。
总结如下:
**包工头负责分配任务,开发者实现分配任务接口(giveTask(WareHouse inhouse))
农民工负责执行任务,开发者实现任务执行接口(doTask(WareHouse outhouse))
职介所负责协同一致性等处理,保证数据统一
手工仓库负责输入输出数据交换。 **
基于消息中枢的计算模式
包工头将需要工人执行的任务/命令/消息放到消息中枢,工人监听消息中枢上各自的消息队列。一旦自己的队列有新内容,便接受执行任务,各个工人以并行的方式进行 。要支持消息中枢模式,则将config.xml文件中把下图中的0改为1(0代表工头工人直接交互)
基于网状直接交互的计算模式
如图所示,工人与工头直接交互,工人之间也可以相互联系,用于并行计算的合并。以下api可提供与获取到其他工人
工人之间可以调用其它工人的receive方法向它发送数据。每个工人都要实现receive接口
并行结合串行模式
例如生产电脑,必须把主板硬盘等配件生产齐全后才能组装,这是串行。生产每个配件的过程是并行的。多个包工头可以衔接在一起,以加工链式的方式向下执行,上一个工头的输出可以成为下一个公头的输入。同时每个工人之间又是并行完成任务的
内部批处理模式
批量处理即为多个工人完成各自的任务,必须等到最慢的一个工人做完,才统一返回结果。
调用WareHouse[] doTaskBatch(WorkerLocal[] wks,WareHouse wh) 可以实现批处理,其中wks是集群工人集合,wh是任务。返回的是工人的计算结果集合
计算集群模式
上层工人可以调用下层工头,通过组件衔接可以横向扩充更过的”工人-工头“计算单元进来,也可以纵向衍生到更高级的工头,工头上面集成更多的工头组成大的计算集群
3、与Hadoop的区别
4、wordCount代码示例
输入数据:n个数据文件,每个1g大小,为了方面统计,每个文件的数据由“aaa bbb … ccc”(由空格分割的1k单词组)不断复制组成。
输出数据:输出这n*1g个数据文件中的每个单词总数
- 初始化职介所,调用BeanContext的startPark()方法 ,注意要将config.xml放在项目根目录。看到如图输出则表示初始化成功
- 初始化工人,输入不同端口初始化3个工人,实际环境中,工人应该部署在不同机器上,读取本地的文件,这里测试读取的都为同一份文件,工人的任务是每次读取文件的64个字节,统计处每个单词的个数,然后存入HashMap返回给工头
public class WordcountWK extends MigrantWorker
{
public WareHouse doTask(WareHouse inhouse)
{
//拿到文件名
String filepath = inhouse.getString("filepath");
//每次读64字节
long n=64;
long num = (new File(filepath)).length()/n;
FileAdapter fa = null;
ReadAdapter ra = null;
byte[] bts = null;
//统计每个词的数量,存入此HashMap
HashMap<String,Integer> wordcount = new HashMap<String,Integer>();
fa = new FileAdapter(filepath);
for(long i=0;i<num;i++){
ra = fa.getReader(i*n, n);
bts = ra.readAll();
StringTokenizer tokenizer = new StringTokenizer(new String(bts));
while(tokenizer.hasMoreTokens()){
String curword = tokenizer.nextToken();
//如果之前出现过这个单词,个数加一
if(wordcount.containsKey(curword))
wordcount.put(curword, wordcount.get(curword)+1);
//第一次出现,个数为1
else
wordcount.put(curword, 1);
}
}
fa.close();
//将结果返回
return new WareHouse("word", wordcount);
}
public static void main(String[] args)
{
Scanner sc=new Scanner(System.in);
String port=sc.next();
WordcountWK mw = new WordcountWK();
//初始化工人,参数为ip,端口,还有名称
mw.waitWorking("localhost",Integer.parseInt(port),"wordcount");
}
}
-初始化工头,工头设置需要读取的文件名,然后获取到等待中的工人集合,然后进行任务批处理操作。遍历结果,拿到每个工人统计的那部分,进行合并。最后得到单词总数
public class WordcountCT extends Contractor
{
public WareHouse giveTask(WareHouse inhouse)
{
//获取到等待所有名字为wordcount的工人集合
WorkerLocal[] wks = getWaitingWorkers("wordcount");
System.out.println("wks.length:"+wks.length);
//得到批处理结果
WareHouse[] hmarr = doTaskBatch(wks, inhouse);
HashMap<String,Integer> wordcount = new HashMap<String,Integer>();
//遍历结果
for(WareHouse hm:hmarr)
{
//拿到每个工人统计的那部分,进行合并
HashMap<String,Integer> wordhm = (HashMap<String,Integer>)hm.get("word");
for(Iterator<String> iter=wordhm.keySet().iterator();iter.hasNext();){
String curword = iter.next();
if(wordcount.containsKey(curword))
wordcount.put(curword, wordcount.get(curword)+wordhm.get(curword));
else
wordcount.put(curword, wordhm.get(curword));
}
}
return new WareHouse("word", wordcount);
}
public static void main(String[] args)
{
Contractor a = new WordcountCT();
long begin = (new Date()).getTime();
//往数据仓库中设置所需要读的文件名
WareHouse result = a.giveTask(new WareHouse("filepath",
"D:\\ijProject\\test\\inputdata.txt"));//"D:\\demo\\parallel\\a\\three.txt"
long end = (new Date()).getTime();
System.out.println("time:"+(end-begin)/1000+"s");
System.out.println("result:"+result);
}
}
最后得到结果如图: