以下內容均整理自網絡
1.1 Kafka與storm整合
參考這個:https://blog.csdn.net/yongge1981/article/details/79260011
1.2 案列
任務: 實時統計移動用戶在每個小區的掉話率。
項目整體架構如下:
1.2.1 啓動集羣
啓動Zookeeper集羣
zkServer.sh start
啓動Hbase(完全分佈式需要先啓動Hadoop集羣)
啓動Kafka集羣(是通過發送數據到kafka)
bin/kafka-server-start.sh config/server.properties
啓動storm
##node1上啓動Nimbus
$ storm nimbus >> ./logs/nimbus.out 2>&1 &
##後臺啓動,記得在storm主目錄
##啓動界面
$ storm ui >> ./logs/ui.out 2>&1 &
節點node2和node3啓動supervisor,按照配置,每啓動一個supervisor就有了4個slots
$ storm supervisor >> ./logs/supervisor.out 2>&1 &
1.2.2 在Hbase中創建如下表:
start-hbase.sh
hbase shell
create 'cell_monitor_table','cf'
1.2.3修改項目配置
A、cmccstormjk02項目
a) 修改cmcc.constant. Constants中對應各配置項。如:HBASE_ZOOKEEPER_LIST、KAFKA_ZOOKEEPER_LIST、BROKER_LIST、ZOOKEEPERS
B、cmcc02_hbase項目
a) 修改cmcc.hbase.dao.impl.HBaseDAOImp中Hbase的Zookeeper集羣配置。
1.2.4啓動生成測試數據的方法 – cmccstormjk02 工程
在windows上運行kafka.productor. CellProducer,隨機生成模擬數據,並將數據寫入到Kafka中。
期間出現的錯誤:無法發送數據到對應主題。原因是這個java文件沒有建立主機IP與別名的映射,所以無法訪問。可以通過修改hosts文件,添加映射解決(C:\Windows\System32\drivers\etc)。
可查看Kafka中topic列表:
bin/kafka-topics.sh --zookeeper hadoop01:2181, hadoop02:2181, hadoop03:2181 –list
查看數據:
bin/kafka-console-consumer.sh --zookeeper hadoop01:2181, hadoop02:2181, hadoop03:2181 --topic mylog_cmcc
如果程序正確,則在hadoop虛擬機上可以查看到對應的消費數據:
1.2.5 啓動storm分析-- cmccstormjk02
在windoes上運行topo. KafkaOneCellMonintorTopology,從Kafka中讀取數據,Storm分析之後,結果寫入Hbase中存放。
此時,若已經產生“掉話”情況,分析結果在Hbase當中可以查看到:
1.2.6啓動web展示項目 – cmcc02_hbase
將cmcc02_hbase添加到Tomcat中,啓動Tomcat服務,瀏覽器通過訪問以下地址查看:
http://localhost:8080/cmcc/onecellmonitor.jsp
1.2.7關鍵程序說明
生產數據併發送到kafka的主題下:
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package kafka.productor;
import java.util.Properties;
import java.util.Random;
import backtype.storm.utils.Utils;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;
import tools.DateFmt;
/***
* 模擬發送數據到kafka中
*
* @author hadoop
*
*/
public class CellProducer extends Thread {
//extends Thread 按照線程執行,這裏實際就一條線程,在調用類的對象在執行start()方法時會自動運行run方法。
//多線程簡單例子: https://www.cnblogs.com/yjd_hycf_space/p/7526608.html
// bin/kafka-topics.sh --create --zookeeper localhost:2181
// --replication-factor 3 --partitions 5 --topic cmcccdr
private final kafka.javaapi.producer.Producer<Integer, String> producer;
private final String topic;
private final Properties props = new Properties();
//final修飾的關鍵字必須在成員變量中進行初始化,或直接初始化,且只能賦值一次
// 通過使用它來定義變量,這樣不會在後面被修改了
//構造函數讀取配置信息,並和kafka連接起來
public CellProducer(String topic) {
props.put("serializer.class", "kafka.serializer.StringEncoder");// 字符串消息
props.put("metadata.broker.list", KafkaProperties.broker_list); //讀取集羣節點信息
//這個就和kafka連接了?
producer = new kafka.javaapi.producer.Producer<Integer, String>(new ProducerConfig(props));
this.topic = topic;
}
//run方法發送數據到kafka主題下
public void run() {
Random random = new Random();
//小區編號
String[] cell_num = { "29448-37062", "29448-51331", "29448-51331", "29448-51333", "29448-51343" };
// 正常0; 掉話1(信號斷斷續續); 斷話2(完全斷開)
String[] drop_num = { "0", "1", "2" };
int i = 0;
while (true) {
i++;
String testStr = String.format("%06d", random.nextInt(10) + 1);
//1-10 10位數據
// messageStr: 2494 29448-000003 2016-01-05 10:25:17 1
//
String messageStr = i + "\t" + ("29448-" + testStr) + "\t" + DateFmt.getCountDate(null, DateFmt.date_long)
+ "\t" + drop_num[random.nextInt(drop_num.length)];
System.out.println("product:" + messageStr);
//將當前數據推送到kafka對應主題上的數據
//kafka生產數據併發送:https://cwiki.apache.org/confluence/display/KAFKA/0.8.0+Producer+Example
producer.send(new KeyedMessage<Integer, String>(topic, messageStr));//不知道爲什麼命名是整形,值是String都行
Utils.sleep(1000); //推送一條休眠1000秒
// if(i == 500) {
// break;
// }
//通過學習這個函數瞭解到,一些常用的參數可以新建一個類,通過賦值成員變量來實現。如KafkaProperties.broker_list
//一些常見的方法也可以單獨新建一個類,這樣通過類名.方法實現(不實例化,通過靜態成員函數實現)。如DateFmt.getCountDate()
}
}
public static void main(String[] args) {
// topic設置
CellProducer producerThread = new CellProducer(KafkaProperties.Cell_Topic);
// 啓動線程生成數據,會自動運行 run方法
producerThread.start();
}
}
將kafka和storm連接並處理數據:
package topo;
import java.util.ArrayList;
import java.util.List;
import backtype.storm.Config;
import backtype.storm.LocalCluster;
import backtype.storm.StormSubmitter;
import backtype.storm.generated.AlreadyAliveException;
import backtype.storm.generated.InvalidTopologyException;
import backtype.storm.spout.SchemeAsMultiScheme;
import backtype.storm.topology.TopologyBuilder;
import backtype.storm.tuple.Fields;
import bolt.CellDaoltBolt;
import bolt.CellFilterBolt;
import cmcc.constant.Constants;
import kafka.productor.KafkaProperties;
import storm.kafka.KafkaSpout;
import storm.kafka.SpoutConfig;
import storm.kafka.StringScheme;
import storm.kafka.ZkHosts;
public class KafkaOneCellMonintorTopology {
/**
* @param args
*/
public static void main(String[] args) {
TopologyBuilder builder = new TopologyBuilder();
//通過這個包.類獲取主機的名字
ZkHosts zkHosts = new ZkHosts(Constants.KAFKA_ZOOKEEPER_LIST);
SpoutConfig spoutConfig = new SpoutConfig(zkHosts,
"mylog_cmcc", //消費數據主題
"/MyKafka", // 偏移量offset的根目錄
"MyTrack"); // 對應一個應用
List<String> zkServers = new ArrayList<String>();
System.out.println(zkHosts.brokerZkStr);
for (String host : zkHosts.brokerZkStr.split(",")) {
zkServers.add(host.split(":")[0]);
}
spoutConfig.zkServers = zkServers;
spoutConfig.zkPort = 2181;
// 是否從頭開始消費
spoutConfig.forceFromStart = false;
spoutConfig.socketTimeoutMs = 60 * 1000;
// String
spoutConfig.scheme = new SchemeAsMultiScheme(new StringScheme());
//spout去主題下接收數據
//spout整合kafka,可見官網配置詳解
builder.setSpout("spout", new KafkaSpout(spoutConfig), 3);
//bolt對數據進行處理://這個過濾類的作用是去掉通話編號,對時間進行格式化
builder.setBolt("cellBolt", new CellFilterBolt(), 3).shuffleGrouping("spout");
//這個bolt主要是按照小區統計電話掛話率
//"date", "cell_num", "drop_num" 時間,小區編號,掉話狀態
builder.setBolt("CellDaoltBolt", new CellDaoltBolt(), 5)
.fieldsGrouping("cellBolt", new Fields("cell_num"));
Config conf = new Config();
conf.setDebug(false);
conf.setNumWorkers(5);
if (args.length > 0) {
try {
StormSubmitter.submitTopology(args[0], conf, builder.createTopology());
} catch (AlreadyAliveException e) {
e.printStackTrace();
} catch (InvalidTopologyException e) {
e.printStackTrace();
}
} else {
System.out.println("Local running");
LocalCluster localCluster = new LocalCluster();
localCluster.submitTopology("mytopology", conf, builder.createTopology());
}
}
}
上一個程序中的數據預處理(去掉記錄編號和格式化時間):
package bolt;
import java.util.Map;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.BasicOutputCollector;
import backtype.storm.topology.IBasicBolt;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.tuple.Fields;
import backtype.storm.tuple.Tuple;
import backtype.storm.tuple.Values;
import tools.DateFmt;
public class CellFilterBolt implements IBasicBolt {
/**
*
*/
private static final long serialVersionUID = 1L;
//這個過濾類的作用是去掉通話編號,對時間進行格式化
@Override
public void execute(Tuple input, BasicOutputCollector collector) {
String logString = input.getString(0);
try {
if (input != null) { //如果不爲空,處理
String arr[] = logString.split("\\t");//按照製表符分割
// messageStr格式:消息編號\t小區編號\t時間\t狀態
// 例: 2494 29448-000003 2016-01-05 10:25:17 1
// DateFmt.date_short是yyyy-MM-dd,把2016-01-05 10:25:17格式化2016-01-05
// 發出的數據格式: 時間, 小區編號, 掉話狀態
//因爲統計掉話率,記錄編號不需要
collector.emit(new Values(DateFmt.getCountDate(arr[2], DateFmt.date_short), arr[1], arr[3]));
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("date", "cell_num", "drop_num"));
}
@Override
public Map<String, Object> getComponentConfiguration() {
return null;
}
@Override
public void cleanup() {
// TODO Auto-generated method stub
}
@Override
public void prepare(Map map, TopologyContext arg1) {
// TODO Auto-generated method stub
}
}
按照小區統計電話掛話率:
package bolt;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import backtype.storm.task.TopologyContext;
import backtype.storm.topology.BasicOutputCollector;
import backtype.storm.topology.IBasicBolt;
import backtype.storm.topology.OutputFieldsDeclarer;
import backtype.storm.tuple.Tuple;
import cmcc.hbase.dao.HBaseDAO;
import cmcc.hbase.dao.impl.HBaseDAOImp;
import tools.DateFmt;
public class CellDaoltBolt implements IBasicBolt {
private static final long serialVersionUID = 1L; //序列化ID
HBaseDAO dao = null; //這個表示馬上就到處理完畢存儲在HBASE了
long beginTime = System.currentTimeMillis(); //獲取當前系統時間,毫秒(時間戳)
long endTime = 0;
// 通話總數
Map<String, Long> cellCountMap = new HashMap<String, Long>();
// 掉話數
Map<String, Long> cellDropCountMap = new HashMap<String, Long>();
String todayStr = null;
// excute纔是每來一條數據處理一次,可以寫一個案列來測試一下
@Override
public void execute(Tuple input, BasicOutputCollector collector) {
// input爲2016-01-05,29448-000003,1
if (input != null) {
//時間 小區號 掉話狀態 按行得到通話記錄
String dateStr = input.getString(0);
String cellNum = input.getString(1);
String dropNum = input.getString(2);
// 判斷是否是當天,不是當天 就清除map 避免內存過大
// 基站數目 大概5-10萬(北京市)
// http://bbs.c114.net/thread-793707-1-1.html
//獲取當前時間,因爲null
todayStr = DateFmt.getCountDate(null, DateFmt.date_short);
// 跨天的處理,大於當天的數據來了,就清空兩個map
// 思考: 如果程序崩潰了,map清零了,如果不出問題,一直做同一個cellid的累加
// 這個邏輯不好,應該換成一個線程定期的清除map數據,而不是這裏判斷
//todayStr.compareTo(dateStr) < 0 表示未來的數據,清除
if (todayStr != dateStr && todayStr.compareTo(dateStr) < 0) {
cellCountMap.clear();
cellDropCountMap.clear();
}
// 當前cellid的通話數統計
Long cellAll = cellCountMap.get(cellNum);//拿出次數,方便疊加
if (cellAll == null) {
cellAll = 0L;
}
//小區數+出現總次數
cellCountMap.put(cellNum, ++cellAll);
// 掉話數統計,大於0就是掉話, 做法 和上面一樣
Long cellDropAll = cellDropCountMap.get(cellNum);
int t = Integer.parseInt(dropNum); //掛話狀態,解析成整數,0,1,2
if (t > 0) {
if (cellDropAll == null) {
cellDropAll = 0L;
}
//小區數+非正常掛話總次數
cellDropCountMap.put(cellNum, ++cellDropAll);
}
// 1.定時寫庫.爲了防止寫庫過於頻繁 這裏間隔一段時間寫一次
// 2.也可以檢測map裏面數據size 寫數據到 hbase
// 3.自己可以設計一些思路 ,當然 採用redis 也不錯
// 4.採用tick定時存儲也是一個思路
endTime = System.currentTimeMillis(); //是運行到這裏的此時時刻
// flume+kafka 集成
// 當前掉話數
// 1.每小時掉話數目
// 2.每小時 通話數據
// 3.每小時 掉話率
// 4.昨天的歷史軌跡
// 5.同比去年今天的軌跡(如果有數據)
// hbase 按列存儲的數據()
// 10萬
// rowkey cellnum+ day
//beginTime 最開始的值是第一次運行這個類的系統時間。
if (endTime - beginTime >= 5000) {
// 5s 寫一次庫,防止頻繁寫數據庫
if (cellCountMap.size() > 0 && cellDropCountMap.size() > 0) {
// x軸,相對於小時的偏移量,格式爲 時:分,數值 數值是時間的偏移
String arr[] = this.getAxsi();
// 當前日期
String today = DateFmt.getCountDate(null, DateFmt.date_short);
// 當前分鐘
String today_minute = DateFmt.getCountDate(null, DateFmt.date_minute);
// cellCountMap爲通話數據的map
Set<String> keys = cellCountMap.keySet();//所有小區編號
for (Iterator iterator = keys.iterator(); iterator.hasNext();) {
String key_cellnum = (String) iterator.next();
//該時刻的小區通話總數,掉話數 打印測試
System.out.println("key_cellnum: " + key_cellnum + "***"
+ arr[0] + "---"
+ arr[1] + "---"
+ cellCountMap.get(key_cellnum) + "----"
+ cellDropCountMap.get(key_cellnum));
//寫入HBase數據,樣例: {time_title:"10:45",xAxis:10.759722222222223,call_num:140,call_drop_num:91}
//按照HBASE的格式將數據存起來插入HBASE(見insert方法實HBASEDAOImp現),可以學習下HBASE的安裝,java基本使用。
// HBASE: 表名 主鍵ID 數據的列族 內容
//tableName,rowKey, family, quailifer[], value[]
dao.insert("cell_monitor_table",
key_cellnum + "_" + today,
"cf",
new String[] { today_minute },
new String[] { "{" + "time_title:\"" + arr[0] + "\",xAxis:" + arr[1] + ",call_num:"
+ cellCountMap.get(key_cellnum) + ",call_drop_num:" + cellDropCountMap.get(key_cellnum) + "}" }
);
}
}
// 需要重置初始時間,這樣才能實現每5秒記錄一次
beginTime = System.currentTimeMillis();
}
}
}
@Override
public void prepare(Map stormConf, TopologyContext context) {
// TODO Auto-generated method stub
dao = new HBaseDAOImp();
Calendar calendar = Calendar.getInstance();
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// TODO Auto-generated method stub
}
@Override
public Map<String, Object> getComponentConfiguration() {
// TODO Auto-generated method stub
return null;
}
// 獲取X座標,就是當前時間的座標,小時是單位
public String[] getAxsi() {
// 取當前時間
Calendar c = Calendar.getInstance();
int hour = c.get(Calendar.HOUR_OF_DAY);
int minute = c.get(Calendar.MINUTE);
int sec = c.get(Calendar.SECOND);
// 總秒數
int curSecNum = hour * 3600 + minute * 60 + sec;
// (12*3600+30*60+0)/3600=12.5
Double xValue = (double) curSecNum / 3600;
// 時:分,數值 數值是時間的偏移
// 時間:時分顯示 + 時間的偏移,按照小時爲密度偏移,這樣才能好做x軸。因爲一般都是按照多少小時疊加。
String[] end = { hour + ":" + minute, xValue.toString() };
return end;
}
@Override
public void cleanup() {
}
}