Kafka+storm學習筆記

以下內容均整理自網絡

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() {
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章