随着Hadoop的普及,越来越多的公司在构建自己的Hadoop的集群,一赶大数据不可阻挡之趋势,虽然大数据的发展的确是不可阻挡的。随着业务的延展,有时候公司内部不同部门或团队之间就会出现归属自己的Hadoop集群,这种多集群的方式,既让不同业务板块的Hadoop集群实现个性化、差异化,以更好的为自身业务场景所服务,与此同时也会不可避免的出现需要协调多个Hadoop集群共同完成某件任务的场景。下面我们以公司经营利润的计算为例来说明。
需求说明
:
公司一年经营利润的计算,需要由采购团队计算采购的支出(
purchase
),销售团队计算销售的收入(
sell
),然后还包括其他部门费用支付的计算(other),则:
一年的:
利润(
profit
)=销售收入-采购支出-其他业务花费,
我们暂且将业务场景定义的如此简单。
从系统角度来看,采购部门要统计采购数据(海量数据),销售部门统计销售数据((海量数据),其他部门统计的其他费用支出(汇总的少量数据),最后系统计算得到当月的利润。
这里要说明的是,采购系统是单独的系统,销售是另外单独的系统,及以其他很多大大小小的系统,如何能让多个系统,配合起来做这道计算题呢??
计算方式我们采用基础的MapReduce进行,则
profit
的计算需要
purchase、
sell、
other三个任务同时完成后,才能触发。我们此处探索,使用ZooKeeper来进行任务的协调,当然还有其他很好的方式比如,使用消息总线或者
Oozie等工具。
架构设计:
- 数据存储:
- 采购数据,为海量数据,基于Hadoop存储和分析;
- 销售数据,为海量数据,基于Hadoop存储和分析;
- 其他费用支出,为少量数据,基于文件或数据库存储和分析;
2.程序设计:
设计一个同步队列,这个队列有3个条件节点,分别对应采购(purchase),销售(sell),其他费用(other)3个部分。当3个节点都被创建后,程序会自动触发计算利润,并创建利润(profit)节点。上面3个节点的创建,无顺序要求。每个节点只能被创建一次:
说明
:
- 2个独立的Hadoop集群
- 2个独立的Java应用
- 3个Zookeeper集群几点
/queue是队列的目录;
/queue/purchase是队列的采购排队节点,对应Hadoop App1完成任务后在ZK上创建;
/queue/sell 是队列的销售排队节点,对应Hadoop App2完成计算后在ZK上创建;
/queue/other 是队列的其他费用节点,对应Java程序 App3完成计算后在ZK上创建;
/queue/profit是队列的利润节点,当前三个节点都创建成功后,触发该节点的创建,完成利润计算;
创建/queue/purchase,/queue/sell,/queue/other目录时,没有前后顺序,程序提交后,/queue目录下会生成对应该子目录;
/queue/profit被创建后,zk的应用会监听到这个事件,通知应用,队列已完成。
(PS:向下的红色箭头代表,利润节点创建完成后,删除业务几点zk1、zk2、zk3释放对应Hadoop计算节点资源)
3.实验环境:
开发环境: Win7 64bit、JDK1.6、Maven3、Eclipse Luna;
Zookeeper服务器:三台服务器几点,CentOS 6.5、
zookeeper-3.4.5、JDK1.6
Hadoop集群:
CentOS 6.5、JDK1.6、Hadoop-1.2.1
提前完成Hadoop集群和Zookeep集群的搭建,并启动;
4.实验数据:
- 采购数据:purchase.csv,格式示例:
一共4列,分别对应 产品ID,产品数量,产品单价,采购日期,
1,26,1168,2013-01-082,49,779,2013-02-123,80,850,2013-02-054,69,1585,2013-01-265,88,1052,2013-01-136,84,2363,2013-01-197,64,1410,2013-01-128,53,910,2013-01-119,21,1661,2013-01-1910,53,2426,2013-02-18
- 销售数据:sell.csv,格式示例:
一共4列,分别对应 产品ID,销售数量,销售单价,销售日期 ,1,14,1236,2013-01-142,19,808,2013-03-063,26,886,2013-02-234,23,1793,2013-02-095,27,1206,2013-01-216,27,2648,2013-01-307,22,1502,2013-01-198,20,1050,2013-01-189,13,1778,2013-01-3010,20,2718,2013-03-14
其他费用数据集:
other.csv,格式示例:
一共2列,分别对应 发生日期,发生金额
2013-01-02,5522013-01-03,10922013-01-04,17942013-01-05,4352013-01-06,9602013-01-07,10662013-01-08,13542013-01-09,8802013-01-10,19922013-01-11,9315. 程序开发:
- 使用Maven构建Java Project,myZookeeper,目录机构如下:
- 项目使用Maven进行依赖包的管理,pom.xml文件引入:
< dependencies >
<
dependency
>
<
groupId
>
org.apache.hadoop
</
groupId
>
<
artifactId
>
hadoop
-core
</
artifactId
>
<
version
>
1.2.1
</
version
>
</
dependency
>
<
dependency
>
<
groupId
>
junit
</
groupId
>
<
artifactId
>
junit
</
artifactId
>
<
version
>
4.4
</
version
>
<
scope
>
test
</
scope
>
</
dependency
>
<
dependency
>
<
groupId
>
org.apache.zookeeper
</
groupId
>
<
artifactId
>
zookeeper
</
artifactId
>
<
version
>
3.4.6
</
version
>
</
dependency
>
</ dependencies >
- 类说明,总共新建6个Java类,其中:
HdfsDao.java:操作HDFS的工具类,实现本地对HDFS文件的基本操作,常规操作,有兴趣可以参考项目源码; Purchase.java:基于MapReduce的采购金额的计算:(一个简单的MR任务)package org.bd.ytg.zookeeper; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.regex.Pattern; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapred.FileInputFormat; import org.apache.hadoop.mapred.FileOutputFormat; import org.apache.hadoop.mapred.JobClient; import org.apache.hadoop.mapred.JobConf; import org.apache.hadoop.mapred.MapReduceBase; import org.apache.hadoop.mapred.Mapper; import org.apache.hadoop.mapred.OutputCollector; import org.apache.hadoop.mapred.Reducer; import org.apache.hadoop.mapred.Reporter; import org.apache.hadoop.mapred.TextInputFormat; import org.apache.hadoop.mapred.TextOutputFormat; import org.bd.ytg.hdfs.HdfsDao; /** * 计算2013年1月的采购金额 * @author gaoyongtao * * 2017年11月8日 */ public class Purchase { public static final String HDFS_HOST = "hdfs://192.168.203.10:9000/"; public static final Pattern DELIMITER = Pattern.compile("[\t,]"); public static class PurchaseMapper extends MapReduceBase implements Mapper<LongWritable, Text, Text, IntWritable>{ //一共4列,分别对应 产品ID,产品数量,产品单价,采购日期 // 1,26,1168,2013-01-08 // 2,49,779,2013-02-12 // 3,80,850,2013-02-05 // 4,69,1585,2013-01-26 // 5,88,1052,2013-01-13 public static final String MONTH = "2013-01"; // 输出的Key static Text oneMonth = new Text(MONTH); // 输出的value IntWritable money = new IntWritable(); @Override public void map(LongWritable key, Text value,OutputCollector<Text, IntWritable> outputCollector, Reporter reporter) throws IOException { // hadoop的输入 这个value是1行数据,是的;每一行数据列之间以'/t'进行分割 System.out.println("PurchaseMapper excete in map,key=:"+key+",line=:"+value.toString()); //PurchaseMapper excete in map,key=:0,line=:1,26,1168,2013-01-08 String[] datas = DELIMITER.split(value.toString()); if(datas.length>=3 && datas[3].startsWith(MONTH)){ int sum = 0; sum = Integer.parseInt(datas[1]) * Integer.parseInt(datas[2]); money.set(sum); outputCollector.collect(oneMonth, money); } } } public static class PurchaseReducer extends MapReduceBase implements Reducer<Text, IntWritable, Text, IntWritable>{ private IntWritable v = new IntWritable(); Text myKey = new Text(); private int totalMoney = 0; @Override public void reduce(Text key, Iterator<IntWritable> values,OutputCollector<Text, IntWritable> outputCollector, Reporter reporter) throws IOException { while(values.hasNext()){ int money = values.next().get(); System.out.println("PurchaseReducer excete in reduce,key=:"+key+",values.next().get()=:"+money); totalMoney += money; } //myKey.set(string); v.set(totalMoney); // outputCollector.collect(key, v); //如果此处输出带上key,则MR的输出即为2013-01,XXXX outputCollector.collect(null, v); // 不带key,则MR输出为XXXX,只有一个金额 System.out.println("Output:" + key + "," + totalMoney); } } public static void runPurchase(Map<String, String> path) throws IOException, InterruptedException, ClassNotFoundException { JobConf conf = getHadoopConfig(); String local_data = path.get("purchase"); String input = path.get("input"); String output = path.get("output"); // 初始化HDFS访问层 HdfsDao hdfs = new HdfsDao(HDFS_HOST, conf); hdfs.rmr(input); // hdfs.rmr(output); hdfs.mkdirs(input); hdfs.copyFile(local_data, input); conf.setOutputKeyClass(Text.class); conf.setOutputValueClass(IntWritable.class); conf.setMapperClass(PurchaseMapper.class); conf.setReducerClass(PurchaseReducer.class); conf.setInputFormat(TextInputFormat.class); conf.setOutputFormat(TextOutputFormat.class); FileInputFormat.setInputPaths(conf, new Path(input)); FileOutputFormat.setOutputPath(conf, new Path(output)); JobClient.runJob(conf); } public static JobConf getHadoopConfig() { JobConf conf = new JobConf(); conf.setJobName("purchaseJob"); conf.addResource("classpath:/hadoop/core-site.xml"); conf.addResource("classpath:/hadoop/hdfs-site.xml"); conf.addResource("classpath:/hadoop/mapred-site.xml"); conf.addResource("classpath:/hadoop/masters"); conf.addResource("classpath:/hadoop/slaves"); return conf; } public static Map<String,String> pathConfigMap(){ Map<String, String> path = new HashMap<String, String>(); path.put("purchase", "logfile/biz/purchase.csv");// 本地的数据文件 path.put("input", HDFS_HOST + "dataguru/hdfs/purchase/");// HDFS的目录 path.put("output", HDFS_HOST + "purchaseresult"); // 输出目录 return path; } public static void main(String[] args) throws Exception { runPurchase(pathConfigMap()); } }
Sell.java:MapReduce的销售金额的计算:Other.java :其他费用的Java App计算;ProfitCaculate.java:利润计算的Java App;ZookeeperJob.java :ZK任务调度类, 各个业务节点,在完成自身节点创建完成后,判断队列创建是否完成(/queue的子节点个数是否等于3),如果是,则触发对PROFIT 节点的创建,生成利润节点,进行利润的计算:package org.bd.ytg.zookeeper; import java.io.IOException; import java.util.List; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.ZooDefs.Ids; import org.apache.zookeeper.ZooKeeper; /** * ZooKeeper任务调度类: * 各个业务节点,在完成自身节点创建完成后,判断队列创建是否完成(/queue的子节点个数是否等于3), * 如果是,则触发对PROFIT节点的创建,生成利润节点,进行利润的计算 * @author gaoyongtao * * 2017年11月8日 */ public class ZooKeeperJob { final public static String QUEUE = "/queue"; //父節點 final public static String PURCHASE = "/queue/purchase"; final public static String SELL = "/queue/sell"; final public static String OTHER = "/queue/other"; final public static String PROFIT = "/queue/profit"; // 创建一个与服务器的连接,监控节点创建事件 public static ZooKeeper connection(String host) throws IOException { ZooKeeper zk = new ZooKeeper(host, 60000, new Watcher() { // 监控所有被触发的事件 public void process(WatchedEvent event) { if (event.getType() == Event.EventType.NodeCreated && event.getPath().equals(PROFIT)) { System.out.println("Queue has Completed!!!"); } } }); return zk; } // 初始化隊列 public static void initQueue(ZooKeeper zooKeeper) throws KeeperException, InterruptedException{ System.out.println("WATCH => " + PROFIT); // 如果這個節點存在 zooKeeper.exists(QUEUE, true); // 節點不存在,則創建該節點 if (zooKeeper.exists(QUEUE, false) == null) { System.out.println("create " + QUEUE); zooKeeper.create(QUEUE, QUEUE.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } else { System.out.println(QUEUE + " is exist!"); } } // 判斷隊列的節點是否全部創建完成 public static void isCompleted(ZooKeeper zk) throws Exception { // 共三個節點:採購、銷售、其他 int size = 3; List<String> children = zk.getChildren(QUEUE, true); int length = children.size(); System.out.println("Queue Complete:" + length + "/" + size); if (length >= size) { System.out.println("create " + PROFIT); String profit = String.valueOf(ProfitCaculate.profit()); System.out.println(profit); zk.create(PROFIT, profit.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); List<String> queueChildren = zk.getChildren(QUEUE, true); for (String child : queueChildren) { System.out.println("完成利润节点的创建,Queue的子节点有:"); System.out.print(child+" "); } try { String profitMoney = new String(zk.getData(PROFIT, null, null)); System.out.println(profitMoney); } catch (Exception e) { System.out.println("获取PROFIT值异常"); e.printStackTrace(); } /* for (String child : children) {// 清空节点,釋放服務器對該業務節點的監控,保留利润节点 if(!PROFIT.equals(QUEUE + "/" + child)){ zk.delete(QUEUE + "/" + child, -1); } }*/ } } // 如果隊列上不存在採購節點,則執行採購金額計算MR任務,并創建採購節點,完成后判斷隊列節點是否創建完成 public static void doPurchase(ZooKeeper zk) throws Exception { if (zk.exists(PURCHASE, false) == null) { Purchase.runPurchase(Purchase.pathConfigMap()); System.out.println("create " + PURCHASE); zk.create(PURCHASE, PURCHASE.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } else { System.out.println(PURCHASE + " is exist!"); } isCompleted(zk); } // 如果隊列上不存在銷售節點,則執行銷售金額計算MR任務,并創建銷售節點,完成后判斷隊列節點是否創建完成 public static void doSell(ZooKeeper zk) throws Exception { if (zk.exists(SELL, false) == null) { Sell.runSell(Sell.pathConfigMap()); System.out.println("create " + SELL); zk.create(SELL, SELL.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } else { System.out.println(SELL + " is exist!"); } isCompleted(zk); } // 如果隊列上不存在其他費用節點,則執行其他費用金額計算任務,并創建採購節點,完成后判斷隊列節點是否創建完成 public static void doOther(ZooKeeper zk) throws Exception { if (zk.exists(OTHER, false) == null) { Other.calcOther(Other.file); System.out.println("create " + OTHER); zk.create(OTHER, OTHER.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } else { System.out.println(OTHER + " is exist!"); } isCompleted(zk); } public static void doAction(int client) throws Exception { String host1 = "192.168.203.10:2181"; String host2 = "192.168.203.10:2181"; String host3 = "192.168.203.10:2181"; ZooKeeper zk = null; switch (client) { case 1: zk = connection(host1); initQueue(zk); doPurchase(zk); break; case 2: zk = connection(host2); initQueue(zk); doSell(zk); break; case 3: zk = connection(host3); initQueue(zk); doOther(zk); break; } } public static void main(String[] args) throws Exception { doAction(Integer.parseInt("1")); doAction(Integer.parseInt("2")); doAction(Integer.parseInt("3")); } }
6.程序运行:
启动Hadoop集群,启动ZK集群,检查应用进程均正常:(master既作为Hadoop的主节点,也作为Zookeeper的主节点,salve1和slave2作为从节点)
分别进行调试,完成两个MapReduce任务单独运行成功( 确保MR任务无bug,毕竟此处这不是我们的重点 );运行过MR任务后,需要对HDFS进行初始化,还原到最初的环境,人生若只如初见:
下面,运行 ZookeeperJob.java中的main方法,进行集群协调的验证,运行结果:实验数据文件由本地上传至HDFS:
采购MapReduce任务金额的计算:
销售MapReduce任务金额的计算:
其他费用为本地Java 应用计算金额:
查看 Zookeeper的queue队列: (此处为查看计算结果,暂未删除ZK节点释放资源):
查看Zookeeper上的profit节点:
Eclipse输出日志:
7.小结:
通过同步的分步式队列自动启动了计算利润的程序,并在日志中打印了2013年1月的利润为-6693765,以此模拟实现这个分布式队列的Demo完成。 当然程序中还有许多不严谨的地方,以待继续优化完善,不知情所起,一往情深。
附:完整项目代码参考:https://gitee.com/tonnygao/JiYuZooKeeperShiXianFenBuShiDuiLieXiTongShiXianMapReduceRenWuJiCheng.git