随着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.实验数据:
一共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
6,84,2363,2013-01-19
7,64,1410,2013-01-12
8,53,910,2013-01-11
9,21,1661,2013-01-19
10,53,2426,2013-02-18
一共4列,分别对应 产品ID,销售数量,销售单价,销售日期 ,
1,14,1236,2013-01-14
2,19,808,2013-03-06
3,26,886,2013-02-23
4,23,1793,2013-02-09
5,27,1206,2013-01-21
6,27,2648,2013-01-30
7,22,1502,2013-01-19
8,20,1050,2013-01-18
9,13,1778,2013-01-30
10,20,2718,2013-03-14
其他费用数据集:
other.csv,格式示例:
一共2列,分别对应 发生日期,发生金额
2013-01-02,552
2013-01-03,1092
2013-01-04,1794
2013-01-05,435
2013-01-06,960
2013-01-07,1066
2013-01-08,1354
2013-01-09,880
2013-01-10,1992
2013-01-11,931
5.
程序开发:
- 使用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
>
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{
//一共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 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{
private IntWritable v = new IntWritable();
Text myKey = new Text();
private int totalMoney = 0;
@Override
public void reduce(Text key, Iterator values,OutputCollector 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 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 pathConfigMap(){
Map path = new HashMap();
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 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 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