需求:实时显示今天为止网址的点击量,实时显示从搜索引擎引流过来的点击量
数据流向:日志 -> Flume -> Kafka -> Spark Streaming -> HBase -> EChars
大概思路:编写脚本模拟生成点击日志,编写Flume配置文件,Flume source为日志文件,Flume sink为Kafka,编写Spark Streaming程序,整合Kafka,清洗数据,把统计结果写入到HBase数据库中,最后把数据展示出来
------ 实时日志 -> Flume ------
1 编写Python脚本模拟生成用户搜索数据(generate_log.py)
2 编写运行模拟生成用户搜索数据的Python程序的脚本(log_generator.sh)
3 使用crontab -e定时执行脚本生成用户搜索数据(crontab添加计划任务)
4 编写日志文件 -> Flume 的配置文件(文件source -> 控制台sink)(exec source -> memory channel -> logger sink)(streaming_project.conf)
5 启动Flume程序,查看是否有日志信息打印(Flume启动命令)
------ Flume -> Kafka ------
6 启动zookeeper(启动zookeeper命令)
7 启动Kafka(启动Kafka命令)
8 编写Flume -> Kafka的配置文件(streaming_project2.conf)
9 启动kafka消费者(启动kafka消费者命令)
10 启动Fluem(Flume启动命令)
------ Kafka -> Spark Streaming ------
11 编写Spark Streaming程序(Receiver方法)
12 启动Spark Streaming程序(启动时传入参数,zookeeper group topics 线程数)
------ 数据清洗 ------
13 编写日期转换工具类
14 编写数据清洗代码
15 编写数据清洗后的case class
------ 编写HBase操作代码 ------
16 启动Hadoop(启动Hadoop之前Kafka和Zookeeper已经启动)
17 启动HBase
18 创建HBase表
19 设计Rowkey(day_courseid)
20 使用Scala操作HBase
1 编写实体类(case class CourseClickCount)
2 编写实体类对应的DAO访问层(object CourseClickCountDAO)
1 根据rowkey,列族,列累加值
2 根据rowkey查询值
3 编写HBase的工具类(HBaseUtils)
1 设置zookeeper和hdfs
2 获取表
3 插入数据
----- 统计今天到现在为止课程的访问量 ------
21 编写统计访问量的Spark代码(ImoocStatStreamingApp)
22 启动Flume(streaming_project2.conf)(启动Flume之前要先启动Kafka)
23 hbase shell,查看hbase表数据是否有更新
------ 统计今天到现在为止从搜索引擎引流过来的实战课程的访问量 ------
描述:重点在于怎么把从搜索引擎过来的访问给筛选出来
24 HBase表设计,创建HBase表
25 设计rowkey(天 + search + 编号)
26 编写DAO代码(case class CourseSearchClickCount)(CourseSearchClickCountDAO)
1 根据rowkey,列族,列累加值
2 根据rowkey查询值
27 编写Spark中的统计模块(ImoocStatStreamingApp)
------ 将项目运行在服务器中 ------
1 打包
2 编写spark-submit
------ 实时日志 -> Flume ------
1 编写Python脚本模拟生成用户搜索数据(generate_log.py)
#coding=UTF-8
import random
import time
url_paths = [
"class/112.html",
"class/128.html",
"class/145.html",
"class/146.html",
"class/131.html",
"class/130.html",
"learn/821",
"course/list"
]
ip_slices = [132,156,124,10,29,167,143,187,30,46,55,63,72,87,98]
http_referers = [
"http://www.baidu.com/s?wd={query}",
"https://www.sogou.com/web?query={query}",
"https://cn.bing.com/search?q={query}",
"https://search.yahoo.com/search?p=={query}",
]
search_keyword = [
"Spark SQL实战",
"Hadoop基础",
"Storm实战",
"Spark Streaming实战",
"大数据面试",
]
status_codes = ["200", "404", "500"]
def sample_url():
return random.sample(url_paths, 1)[0]
def sample_ip():
slice = random.sample(ip_slices, 4)
return ".".join([str(item) for item in slice])
def sample_referer():
if random.uniform(0, 1) > 0.2:
return "-"
refer_str = random.sample(http_referers, 1)
query_str = random.sample(search_keyword, 1)
return refer_str[0].format(query=query_str[0])
def sample_status_code():
return random.sample(status_codes, 1)[0]
def generate_log(count = 10):
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
f = open("/home/hadoop/data/project/logs/access_smj.log","w+")
while count >= 1:
query_log = "{ip}\t{local_time}\t\"GET /{url} HTTP/1.1\"\t{status_code}\t{referer}".format(local_time=time_str,url=sample_url(), ip=sample_ip(), referer=sample_referer(), status_code=sample_status_code())
print query_log
f.write(query_log + "\n")
count = count - 1
if __name__ == '__main__':
generate_log(100)
2 编写运行模拟生成用户搜索数据的Python程序的脚本(log_generator.sh)
python /home/hadoop/data/project/generate_log_smj.py
3 使用crontab -e定时执行脚本生成用户搜索数据(crontab添加计划任务)
在终端运行 crontab -e 命令后添加以下语句(一分钟运行一次)
*/1 * * * * /home/hadoop/data/project/log_generator.sh
crontab在线工具: https://tool.lu/crontab
4 编写日志文件 -> Flume 的配置文件(文件source -> 控制台sink)(exec source -> memory channel -> logger sink)(streaming_project.conf)
这里只是用于测试,看看flume-source是否已经关联上了日志文件
exec-memory-logger.sources = exec-source
exec-memory-logger.sinks = logger-sink
exec-memory-logger.channels = memory-channel
exec-memory-logger.sources.exec-source.type = exec
exec-memory-logger.sources.exec-source.command = tail -F /home/hadoop/data/project/logs/access_smj.log
exec-memory-logger.sources.exec-source.shell = /bin/sh -c
exec-memory-logger.channels.memory-channel.type = memory
exec-memory-logger.sinks.logger-sink.type = logger
exec-memory-logger.sources.exec-source.channels = memory-channel
exec-memory-logger.sinks.logger-sink.channel = memory-channel
5 启动Flume程序,查看是否有日志信息打印(Flume启动命令)
这里只是用于测试,看看flume-source是否已经关联上了日志文件
flume-ng agent \
--name exec-memory-logger \
--conf $FLUME_HOME/conf \
--conf-file /home/hadoop/data/project/streaming_project.conf
-Dflume.root.logger=INFO,console
------ Flume -> Kafka ------
6 启动zookeeper(启动zookeeper命令)
zkServer.sh start
7 启动Kafka(启动Kafka命令)
我这里给kafka配置了环境变量,如果没有配的话直接cd到kafka的bin目录运行kafka-server-start.sh和指定一下properties配置文件就可以了
$KAFKA_HOME/bin/kafka-server-start.sh -daemon $KAFKA_HOME/config/server.properties
8 编写Flume -> Kafka的配置文件(streaming_project2.conf)
exec-memory-kafka.sources = exec-source
exec-memory-kafka.sinks = kafka-sink
exec-memory-kafka.channels = memory-channel
exec-memory-kafka.sources.exec-source.type = exec
exec-memory-kafka.sources.exec-source.command = tail -F /home/hadoop/data/project/logs/access_smj.log
exec-memory-kafka.sources.exec-source.shell = /bin/sh -c
exec-memory-kafka.channels.memory-channel.type = memory
exec-memory-kafka.sinks.kafka-sink.type = org.apache.flume.sink.kafka.KafkaSink
exec-memory-kafka.sinks.kafka-sink.topic = streamingtopic
exec-memory-kafka.sinks.kafka-sink.brokerList = hadoop000:9092
exec-memory-kafka.sinks.kafka-sink.requiredAcks = 1
exec-memory-kafka.sinks.kafka-sink.batchSize = 5
exec-memory-kafka.sources.exec-source.channels = memory-channel
exec-memory-kafka.sinks.kafka-sink.channel = memory-channel
9 启动kafka消费者(启动kafka消费者命令)
kafka消费者需要指定zookeeper和topic
kafka-console-consumer.sh --zookeeper hadoop000:2181 --topic streamingtopic
10 启动Fluem(Flume启动命令)
主要是注意--conf-file的路径不要写错了
flume-ng agent \
--name exec-memory-kafka \
--conf $FLUME_HOME/conf \
--conf-file /home/hadoop/data/project/streaming_project2.conf \
-Dflume.root.logger=INFO,console
------ Kafka -> Spark Streaming ------
11 编写Spark Streaming程序(Receiver方法)(ImoocStatStreamingApp)
这个例子用的是Receiver方法,但其实Receiver方法没有Direct方法好,下面翻译了一下Spark官网Receiver和Direct的对比
1 简化并行度:简化并行度不需要创建多个输入流然后再进行合并。使用Direct方法Spark Streaming创建的RDD分区数与Kafka的分区一样多,这些RDD分区都从Kafka并行读取数据。因此,Kafka分区和RDD分区存在存在一对一的映射关系,这更已于理解和调整
2 提升效率:Receiver方法为了实现数据零丢失,需要将数据存储在预写日志中,然后再进一步复制数据。这样做会导致效率很低,因为数据被复制了两次,第一次是通过Kafka复制,第二次是通过“预写日志”复制。Direct方法就没有这方面的问题,因为Direct方法没有接收器,所以不需要预写日志。只需要Kafka的保留数据时间足够长,就可以从Kafka中恢复数据。
3 恰好一次语义:Receiver方法使用Kafka的高级API将消耗的偏移量存储在Zookeeper中。这个方法与预写日志结合使用可以确保数据零丢失(即至少一次语义),但遇到故障的时候有可能导致数据被消费两次。发生这种情况是因为Spark Streaming接收的数据与Zookeeper跟踪的偏移量之间存在不一致。所以在Direct方法中,我们不使用Zookeeper保存偏移量,而是使用简单的Kafka API。Spark Streaming在检查点内跟踪偏移量,这样可以消除Spark Streaming、Zookeeper和Kafka之间的偏移量不一致问题。所以就算是出现了故障,Spark Streaming也会有效地恰好一次接收数据。为了实现一次性语义,结果保存到外部存储的操作必须是幂等的,或者是保存结果和偏移量的原子事务。
注意:Direct方法的缺点是它不会更新Zookeepr中的偏移量,所以基于Zookeeper的Kafka监视工具不会显示进度。但是我们可以在每个批次中访问Direct方法的偏移量,并更新Zookeeper。
注意:这里只是测试数据spark streaming和kafka是否整合成功,没有任何业务处理
package com.imooc.spark.project.spark
import com.imooc.spark.project.domain.ClickLog
import com.imooc.spark.project.utils.DateUtils
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}
object ImoocStatStreamingApp {
def main(args: Array[String]): Unit = {
if(args.length != 4) {
println("Usage: ImoocStatStreamingApp <zkQuorum> <groupId> <tipics> <numThreads>")
System.exit(1)
}
val Array(zkQuorum, groupId, topics, numThreads) = args
val sparkConf = new SparkConf().setAppName("ImoocStatStreamingApp").setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(60))
val topicMap = topics.split(",").map((_, numThreads.toInt)).toMap
val messages = KafkaUtils.createStream(ssc, zkQuorum, groupId, topicMap)
// 测试步骤一:测试数据接收
messages.map(_._2).count().print
ssc.start()
ssc.awaitTermination()
}
}
12 启动Spark Streaming程序(启动时传入参数,zookeeper group topics 线程数)(ImoocStatStreamingApp)
------ 数据清洗 ------
13 编写日期转换工具类(DateUtils)
我们的需求是需要把原始日志信息的日期格式做一个转换,这个很常用也很简单
package com.imooc.spark.project.utils
import java.util.Date
import org.apache.commons.lang3.time.FastDateFormat
/*日期时间工具类*/
object
DateUtils {
val YYYYMMDDHHMMSS_FORMAT = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss")
val TARGE_FORMAT = FastDateFormat.getInstance("yyyyMMddHHmmss")
def getTime(time: String) = {
YYYYMMDDHHMMSS_FORMAT.parse(time).getTime
}
def parseToMinute(time :String) = {
TARGE_FORMAT.format(new Date(getTime(time)))
}
def main(args: Array[String]): Unit = {
println(parseToMinute("2020-04-04 21:47:01"))
}
}
14 编写数据清洗代码(ImoocStatStreamingApp)
这里我们的需求是需要把课程编号从网址中抽取出来,对日期格式进行一个转换操作,然后筛选掉课程编号为0的日志,这个筛选操作可以放在课程编号抽取出来以后就进行,这样效率应该能高一些
package com.imooc.spark.project.spark
import com.imooc.spark.project.dao.{CourseClickCountDAO, CourseSearchClickCountDAO}
import com.imooc.spark.project.domain.{ClickLog, CourseClickCount, CourseSearchClickCount}
import com.imooc.spark.project.utils.DateUtils
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}
import scala.collection.mutable.ListBuffer
object ImoocStatStreamingApp {
def main(args: Array[String]): Unit = {
if(args.length != 4) {
println("Usage: ImoocStatStreamingApp <zkQuorum> <groupId> <tipics> <numThreads>")
System.exit(1)
}
val Array(zkQuorum, groupId, topics, numThreads) = args
// 运行在服务器上要把setMaster注释掉,一般在spark-submit的时候传入参数
val sparkConf = new SparkConf().setAppName("ImoocStatStreamingApp")//.setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(60))
val topicMap = topics.split(",").map((_, numThreads.toInt)).toMap
val messages = KafkaUtils.createStream(ssc, zkQuorum, groupId, topicMap)
// 测试步骤一:测试数据接收
// messages.map(_._2).count().print
// 测试步骤二:数据清洗
val logs = messages.map(_._2)
val cleanData = logs.map(line => {
val infos = line.split("\t")
// infos(2) = "GET /class/128.html HTTP/1.1"
// url = /class/128.html
val url = infos(2).split(" ")(1)
var courseId = 0
// 把实战课程的课程编号拿到了
if(url.startsWith("/class")) {
val courseIdHTML = url.split("/")(2)
courseId = courseIdHTML.substring(0, courseIdHTML.lastIndexOf(".")).toInt
}
// 156.98.46.29 2020-05-07 15:57:01 "GET /class/128.html HTTP/1.1" 500 https://www.sogou.com/web?query=Spark SQL实战
// ClickLog(156.98.46.29, 20200507155701, 128, 500, https://www.sogou.com/web?query=Spark SQL实战)
ClickLog(infos(0), DateUtils.parseToMinute(infos(1)), courseId, infos(3).toInt, infos(4))
}).filter(clicklog => clicklog.courseId != 0)
cleanData.print()
ssc.start()
ssc.awaitTermination()
}
}
15 编写数据清洗后的case class(ClickLog)
这是一个case class,case class的话常用作存储一条内容,这里是存储了一条清洗后的日志数据
package com.imooc.spark.project.domain
/*
清洗后的日志信息
ip 日志访问的ip地址
time 日志访问的时间
courseId 日志访问的实战课程编号
statusCode 日志访问的状态码
referer 日志访问的referer
*/
case class ClickLog(ip:String, time:String, courseId:Int, statusCode:Int, referer:String)
------ 编写HBase操作代码 ------
16 启动Hadoop(启动Hadoop之前Kafka和Zookeeper已经启动)
./start-dfs.sh
17 启动HBase
cd 到HBase的bin目录运行该命令
./start-hbase.sh
18 创建HBase表
注意一下建hbase表的时候表名和列族名是必须的,特别是列族,别漏了
// 这是创建统计课程页面点击量的表
create 'imooc_course_clickcount', 'info'
补充:
hbase shell启动命令
cd到hbase bin目录运行命令
./hbase shell
查看有哪些表
list
查看表详细信息
desc 'imooc_course_clickcount'
查看表内容
scan 'imooc_course_clickcount'
在运行list命令的时候我遇到过连接不到ZooKeeper,
初步分析为虚拟机的ZooKeeper不稳定,关闭ZooKeeper后重启,问题依旧,
最后把hadoop,zookeeper,hbase的进程都杀了之后再按照zookeeper,hadoop,hbase
这个顺序启动hbase恢复正常。
高可用的hadoop集群依赖zookeeper(非高可用不需要zookeeper),
而hbase集群依赖zookeeper和hdfs,所以得先启动zookeeper再启动hadoop,最后启动hbase
19 设计Rowkey(day_courseid)
这里我的rowkey设置为 日期_课程号
日期和课程号都可能产生数据倾斜,这里暂不做处理
20 使用Scala操作HBase
1 编写实体类(case class CourseClickCount)
可爱的case class又来了,按我的理解case class其实就是可以理解为一条数据的载体,这里是存储了一条课程点击量的内容,要统计课程点击量至少需要rowkey和clickcount
package com.imooc.spark.project.domain
/**
* 实战课程点击数实体类
* day_course 对应的就是HBase中的rowkey 20200413_1
* click_count 对应20200413_1的访问总数
* */
case class CourseClickCount (day_course:String, click_count:Long)
2 编写实体类对应的DAO访问层(object CourseClickCountDAO)
1 根据rowkey,列族,列累加值
2 根据rowkey查询值
这个是DAO和Spring Boot中的DAO层概念一样,它实现了对某个case class的操作,这里是把CourseClickCount这个case class进行累加,保存还有通过rowkey查询值
package com.imooc.spark.project.dao
import com.imooc.spark.project.domain.CourseClickCount
import com.imooc.spark.project.utils.HBaseUtils
import org.apache.hadoop.hbase.client.Get
import org.apache.hadoop.hbase.util.Bytes
import scala.collection.mutable.ListBuffer
/*实战课程点击数数据访问层*/
object CourseClickCountDAO {
val tableName = "imooc_course_clickcount_smj"
val cf = "info"
val qualifer = "click_count"
/**
* 保存数据到HBase
* CourseClickCount集合
*/
def save(list: ListBuffer[CourseClickCount]): Unit = {
val table = HBaseUtils.getInstance().getTable(tableName)
for(ele <- list) {
// incrementColumnValue(rowkey,列族,列,值)可以把相同的rowkey,列族,列的值加起来
table.incrementColumnValue(Bytes.toBytes(ele.day_course),
Bytes.toBytes(cf),
Bytes.toBytes(qualifer),
ele.click_count)
}
}
/**
* 根据rowkey查询值
*/
def count(day_course:String):Long = {
val table = HBaseUtils.getInstance().getTable(tableName)
// Get要传入rowkey
val get = new Get(Bytes.toBytes(day_course))
// 获取值要传入Get和列族,列
val value = table.get(get).getValue(cf.getBytes, qualifer.getBytes)
if(value == null) {
0L
} else {
Bytes.toLong(value)
}
}
def main(args: Array[String]): Unit = {
val list = new ListBuffer[CourseClickCount]
list.append(CourseClickCount("20200507_8", 8))
list.append(CourseClickCount("20200507_9", 9))
list.append(CourseClickCount("20200507_1", 100))
save(list)
println(count("20200507_8") + ":" + count("20200507_9") + ":" + count("20200507_1"))
}
}
3 编写HBase的工具类(HBaseUtils)
1 设置zookeeper和hdfs
2 获取表
3 插入数据
我们写程序的时候会经常写着写着需要把一些常用的方法抽象出来,这里就是操作HBase的工具类,注意一下,工具类的构造方法常用单例模式,这样可以减少资源的开销
package com.imooc.spark.project.utils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
/*HBase操作工具类:Java工具类建议采用单例模式封装*/
public class HBaseUtils {
HBaseAdmin admin = null;
Configuration configuration = null;
// 单例的构造方法是私有的
private HBaseUtils() {
// hbase需要设置zookeeper和hdfs路径
configuration = new Configuration();
configuration.set("hbase.zookeeper.quorum", "hadoop000:2181");
configuration.set("hbase.rootdir", "hdfs://hadoop000:8020/hbase");
try {
admin = new HBaseAdmin(configuration);
} catch (Exception e) {
e.printStackTrace();
}
}
private static HBaseUtils instance = null;
public static HBaseUtils getInstance() {
if (null == instance) {
instance = new HBaseUtils();
}
return instance;
}
/**
* 根据表名获取到HTable实例
* @param tableName
* @return org.apache.hadoop.hbase.client.HTable
* @author songminjian
* @date 2020/5/6 下午6:22
*/
public HTable getTable(String tableName) {
HTable table = null;
try {
table = new HTable(configuration, tableName);
} catch (IOException e) {
e.printStackTrace();
}
return table;
}
/**
* 添加一条记录到HBase表
* @param tableName HBase表名
* @param rowkey HBase表的rowkey
* @param cf HBase表的columnfamily
* @param column HBase表的列
* @param value 写入HBase表的值
* @return void
* @author songminjian
* @date 2020/5/6 下午6:31
*/
public void put(String tableName, String rowkey, String cf, String column, String value) {
HTable table = getTable(tableName);
Put put = new Put(Bytes.toBytes(rowkey));
put.add(Bytes.toBytes(cf), Bytes.toBytes(column), Bytes.toBytes(value));
try {
table.put(put);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// HTable table = HBaseUtils.getInstance().getTable("imooc_course_clickcount_smj");
// System.out.println(table.getName().getNameAsString());
String tableName = "imooc_course_clickcount_smj";
String rowkey = "20200506_88";
String cf = "info";
String column = "click_count";
String value = "2";
HBaseUtils.getInstance().put(tableName, rowkey, cf, column, value);
}
}
----- 统计今天到现在为止课程的访问量 ------
21 编写统计访问量的Spark代码(ImoocStatStreamingApp)
注意一下测试步骤三就可以了,其他都是上面有的代码
package com.imooc.spark.project.spark
import com.imooc.spark.project.dao.CourseClickCountDAO
import com.imooc.spark.project.domain.{ClickLog, CourseClickCount}
import com.imooc.spark.project.utils.DateUtils
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}
import scala.collection.mutable.ListBuffer
object ImoocStatStreamingApp {
def main(args: Array[String]): Unit = {
if(args.length != 4) {
println("Usage: ImoocStatStreamingApp <zkQuorum> <groupId> <tipics> <numThreads>")
System.exit(1)
}
val Array(zkQuorum, groupId, topics, numThreads) = args
val sparkConf = new SparkConf().setAppName("ImoocStatStreamingApp").setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(60))
val topicMap = topics.split(",").map((_, numThreads.toInt)).toMap
val messages = KafkaUtils.createStream(ssc, zkQuorum, groupId, topicMap)
// 测试步骤一:测试数据接收
// messages.map(_._2).count().print
// 测试步骤二:数据清洗
val logs = messages.map(_._2)
val cleanData = logs.map(line => {
val infos = line.split("\t")
// infos(2) = "GET /class/128.html HTTP/1.1"
// url = /class/128.html
val url = infos(2).split(" ")(1)
var courseId = 0
// 把实战课程的课程编号拿到了
if(url.startsWith("/class")) {
val courseIdHTML = url.split("/")(2)
courseId = courseIdHTML.substring(0, courseIdHTML.lastIndexOf(".")).toInt
}
ClickLog(infos(0), DateUtils.parseToMinute(infos(1)), courseId, infos(3).toInt, infos(4))
}).filter(clicklog => clicklog.courseId != 0)
// cleanData.print()
// 测试步骤三:统计今天到现在为止实战课程的访问量
cleanData.map(x => {
// HBase rowkey设计:20200507_88(日期_课程编号)
(x.time.substring(0, 8) + "_" + x.courseId, 1)
}).reduceByKey(_ + _).foreachRDD(rdd => {
rdd.foreachPartition(partitionRecords => {
val list = new ListBuffer[CourseClickCount]
partitionRecords.foreach(pair => {
list.append(CourseClickCount(pair._1, pair._2))
})
CourseClickCountDAO.save(list)
})
})
ssc.start()
ssc.awaitTermination()
}
}
22 启动Flume(streaming_project2.conf)(因为Flume的sink是Kafka,所以启动Flume之前要先启动Kafka,还有要确保Flume的source要有新的数据产生)
flume-ng agent \
--name exec-memory-kafka \
--conf $FLUME_HOME/conf \
--conf-file /home/hadoop/data/project/streaming_project2.conf \
-Dflume.root.logger=INFO,console
23 hbase shell,查看hbase表数据是否有更新
scan 'imooc_course_clickcount_smj'
------ 统计今天到现在为止从搜索引擎引流过来的实战课程的访问量 ------
描述:重点在于怎么把从搜索引擎过来的访问给筛选出来
24 HBase表设计,创建HBase表
// 这是创建统计从搜索引擎引流过来的点击量表
create 'imooc_course_search_clickcount','info'
// 清空表数据,测试的时候会经常清空表查看数据有没有更新
truncate 'imooc_course_search_clickcount'
25 设计rowkey(天 + search + 编号)
rowkey一定要是唯一的,而且要与业务相关,注意要防止数据热点问题
26 编写DAO代码(case class CourseSearchClickCount)(CourseSearchClickCountDAO)
1 根据rowkey,列族,列累加值
2 根据rowkey查询值
不准确地来说,可以把case class CourseSearchClickCount看做是HBase表中的一条内容,CourseSearchClickCountDAO是用来操作CourseSearchClickCount的,也就是操作HBase表的内容,把内容保存到HBase或者对内容进行其他操作
CourseSearchClickCount源码:
package com.imooc.spark.project.domain
/**
* <b><code>CourseSearchClickCount</code></b>
* <p/>
* Description
* <p/>
* <b>Creation Time:</b> 2020/5/7 下午3:29.
*
* @author songminjian
* @since sparktrain ${PROJECT_VERSION}
*/
case class CourseSearchClickCount(day_search_course:String, click_count:Long)
CourseSearchClickCountDAO源码:
package com.imooc.spark.project.dao
import com.imooc.spark.project.domain.{CourseClickCount, CourseSearchClickCount}
import com.imooc.spark.project.utils.HBaseUtils
import org.apache.hadoop.hbase.client.Get
import org.apache.hadoop.hbase.util.Bytes
import scala.collection.mutable.ListBuffer
/*实战课程点击数数据访问层*/
object CourseSearchClickCountDAO {
val tableName = "imooc_course_search_clickcount_smj"
val cf = "info"
val qualifer = "click_count"
/**
* 保存数据到HBase
* CourseSearchClickCount集合
*/
def save(list: ListBuffer[CourseSearchClickCount]): Unit = {
val table = HBaseUtils.getInstance().getTable(tableName)
for(ele <- list) {
// incrementColumnValue(rowkey,列族,列,值)可以把相同的rowkey,列族,列的值加起来
table.incrementColumnValue(Bytes.toBytes(ele.day_search_course),
Bytes.toBytes(cf),
Bytes.toBytes(qualifer),
ele.click_count)
}
}
/**
* 根据rowkey查询值
*/
def count(day_search_course:String):Long = {
val table = HBaseUtils.getInstance().getTable(tableName)
// Get要传入rowkey
val get = new Get(Bytes.toBytes(day_search_course))
// 获取值要传入Get和列族,列
val value = table.get(get).getValue(cf.getBytes, qualifer.getBytes)
if(value == null) {
0L
} else {
Bytes.toLong(value)
}
}
def main(args: Array[String]): Unit = {
val list = new ListBuffer[CourseSearchClickCount]
list.append(CourseSearchClickCount("20200507_www.baidu.com_8", 8))
list.append(CourseSearchClickCount("20200507_cn.bing.com_9", 9))
save(list)
println(count("20200507_www.baidu.com_8") + ":" + count("20200507_cn.bing.com_9"))
}
}
27 编写Spark中的统计模块(ImoocStatStreamingApp)
注意一下测试四即可,其他都是上面有的。
新增的功能是统计今天到现在为止从搜索引擎引流过来的实战课程的访问量。
这里有几个需要解决的问题:
1 需要提取出搜索引擎的host
2 需要过滤掉不是从搜索引擎引流过来的点击
3 需要拼接rowkey和把值累加起来
package com.imooc.spark.project.spark
import com.imooc.spark.project.dao.{CourseClickCountDAO, CourseSearchClickCountDAO}
import com.imooc.spark.project.domain.{ClickLog, CourseClickCount, CourseSearchClickCount}
import com.imooc.spark.project.utils.DateUtils
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka.KafkaUtils
import org.apache.spark.streaming.{Seconds, StreamingContext}
import scala.collection.mutable.ListBuffer
object ImoocStatStreamingApp {
def main(args: Array[String]): Unit = {
if(args.length != 4) {
println("Usage: ImoocStatStreamingApp <zkQuorum> <groupId> <tipics> <numThreads>")
System.exit(1)
}
val Array(zkQuorum, groupId, topics, numThreads) = args
// 运行在服务器上要把setMaster注释掉,一般在spark-submit的时候传入参数
val sparkConf = new SparkConf().setAppName("ImoocStatStreamingApp")//.setMaster("local[2]")
val ssc = new StreamingContext(sparkConf, Seconds(60))
val topicMap = topics.split(",").map((_, numThreads.toInt)).toMap
val messages = KafkaUtils.createStream(ssc, zkQuorum, groupId, topicMap)
// 测试步骤一:测试数据接收
// messages.map(_._2).count().print
// 测试步骤二:数据清洗
val logs = messages.map(_._2)
val cleanData = logs.map(line => {
val infos = line.split("\t")
// infos(2) = "GET /class/128.html HTTP/1.1"
// url = /class/128.html
val url = infos(2).split(" ")(1)
var courseId = 0
// 把实战课程的课程编号拿到了
if(url.startsWith("/class")) {
val courseIdHTML = url.split("/")(2)
courseId = courseIdHTML.substring(0, courseIdHTML.lastIndexOf(".")).toInt
}
// 156.98.46.29 2020-05-07 15:57:01 "GET /class/128.html HTTP/1.1" 500 https://www.sogou.com/web?query=Spark SQL实战
// ClickLog(156.98.46.29, 20200507155701, 128, 500, https://www.sogou.com/web?query=Spark SQL实战)
ClickLog(infos(0), DateUtils.parseToMinute(infos(1)), courseId, infos(3).toInt, infos(4))
}).filter(clicklog => clicklog.courseId != 0)
// cleanData.print()
// 测试步骤三:统计今天到现在为止实战课程的访问量
cleanData.map(x => {
// HBase rowkey设计:20200507_88(日期_课程编号)
(x.time.substring(0, 8) + "_" + x.courseId, 1)
}).reduceByKey(_ + _).foreachRDD(rdd => {
rdd.foreachPartition(partitionRecords => {
val list = new ListBuffer[CourseClickCount]
partitionRecords.foreach(pair => {
list.append(CourseClickCount(pair._1, pair._2))
})
CourseClickCountDAO.save(list)
})
})
// 测试步骤四:统计从搜索引擎过来的今天到现在为止实战课程的访问量
// https://www.sogou.com/web?query=Spark SQL实战
cleanData.map(x => {
val referer = x.referer.replaceAll("//", "/")
// splits(1)是搜索网址
val splits = referer.split("/")
var host = ""
if(splits.length > 2) {
host = splits(1)
}
// 返回元组host,courseId, time
(host, x.courseId, x.time)
}).filter(_._1 != "").map(x => {
(x._3.substring(0,8) + "_" + x._1 + "_" + x._2, 1)
}).reduceByKey(_ + _).foreachRDD(rdd => {
rdd.foreachPartition(partitionRecords => {
val list = new ListBuffer[CourseSearchClickCount]
partitionRecords.foreach(pair => {
list.append(CourseSearchClickCount(pair._1, pair._2))
})
CourseSearchClickCountDAO.save(list)
})
})
ssc.start()
ssc.awaitTermination()
}
}
------ 将项目运行在服务器中 ------
28 打包
// 直接在IDEA的终端运行打包命令
// 或者到项目的文件夹下面打开终端对项目打包
// 或者直接使用IDEA的Maven工具Lifecycle打包
// 或者通过IDEA的版本控制进行打包...
mvn clean package -DskipTests
29 编写spark-submit
编写spark-submit的时候遇到了几个报错,都是jar包没有引入导致的,kafka整合spark streaming需要引入特定的jar包,可以到spark官网找一下,注意kafka和scala还有spark的版本号,还有操作hbase也需要引入hbase的jar包,下面先放正确的spark-submit再对报错进行分析。
注意,下面这一个spark-submit是正确的,特点是--jars后面跟了一长串的jar包路径,前面加的是spark streaming整合kafka需要的jar包,后面加的是hbase的lib目录下的所有jar包,用命令给拼起来了而已
spark-submit --master local[2] \
--class com.imooc.spark.project.spark.ImoocStatStreamingApp \
--jars /home/hadoop/lib/smj/spark-streaming-kafka-0-8-assembly_2.11-2.2.0.jar,$(echo /home/hadoop/app/hbase-1.2.0-cdh5.7.0/lib/*.jar |tr ' ' ',') \
/home/hadoop/lib/smj/sparktrain-1.0.jar \
hadoop000:2181 test streamingtopic 1
说完正确的spark-submit,下面说说写错或者写少了--jars会导致哪些错误,这些错误有要怎样解决
报错一:
报错信息:
Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/spark/streaming/kafka/KafkaUtils$
at com.imooc.spark.project.spark.ImoocStatStreamingApp$.main(ImoocStatStreamingApp.scala:29)
at com.imooc.spark.project.spark.ImoocStatStreamingApp.main(ImoocStatStreamingApp.scala)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.spark.deploy.SparkSubmit$.org$apache$spark$deploy$SparkSubmit$$runMain(SparkSubmit.scala:755)
at org.apache.spark.deploy.SparkSubmit$.doRunMain$1(SparkSubmit.scala:180)
at org.apache.spark.deploy.SparkSubmit$.submit(SparkSubmit.scala:205)
at org.apache.spark.deploy.SparkSubmit$.main(SparkSubmit.scala:119)
at org.apache.spark.deploy.SparkSubmit.main(SparkSubmit.scala)
Caused by: java.lang.ClassNotFoundException: org.apache.spark.streaming.kafka.KafkaUtils$
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 11 more
关键字:
ClassNotFoundException: org.apache.spark.streaming.kafka.KafkaUtils$
报错分析:
这里是报错ClassNotFoundException找不到类,找不到KafkaUtils类,所以我们需要在spark-submit中引入Kafka整合Spark Streaming的jar包,这一些spark的官网上都有说明,我们只需要找到我们对应spark版本和对应的kafka版本即可。以下是关于spark-submit整合kafka的链接http://spark.apache.org/docs/2.2.0/streaming-kafka-0-8-integration.html
解决方法:
spark-submit中添加
--packages org.apache.spark:spark-streaming-kafka-0-8_2.11:2.2.0 \
注意:添加这一行以后可能会因为网络问题下载不了jar包而导致报错,如果要解决这个问题需要下载相应的jar包,然后用spark-submit --jars引入jar包(jar包下载地址:https://search.maven.org/search?q=a:spark-streaming-kafka-0-8-assembly_2.11%20AND%20v:2.2.0),还有这用的是0.8版本的kafka,2.11版本的scala,2.2.0版本的spark,如果用的是不同的版本这里要相应地修改一下
--jars /home/hadoop/lib/smj/spark-streaming-kafka-0-8-assembly_2.11-2.2.0.jar \
报错二:
java.lang.NoClassDefFoundError: org/apache/hadoop/hbase/client/HBaseAdmin
at com.imooc.spark.project.utils.HBaseUtils.<init>(HBaseUtils.java:26)
at com.imooc.spark.project.utils.HBaseUtils.getInstance(HBaseUtils.java:36)
at com.imooc.spark.project.dao.CourseSearchClickCountDAO$.save(CourseSearchClickCountDAO.scala:23)
at com.imooc.spark.project.spark.ImoocStatStreamingApp$$anonfun$main$8$$anonfun$apply$3.apply(ImoocStatStreamingApp.scala:91)
at com.imooc.spark.project.spark.ImoocStatStreamingApp$$anonfun$main$8$$anonfun$apply$3.apply(ImoocStatStreamingApp.scala:85)
at org.apache.spark.rdd.RDD$$anonfun$foreachPartition$1$$anonfun$apply$29.apply(RDD.scala:926)
at org.apache.spark.rdd.RDD$$anonfun$foreachPartition$1$$anonfun$apply$29.apply(RDD.scala:926)
at org.apache.spark.SparkContext$$anonfun$runJob$5.apply(SparkContext.scala:2062)
at org.apache.spark.SparkContext$$anonfun$runJob$5.apply(SparkContext.scala:2062)
at org.apache.spark.scheduler.ResultTask.runTask(ResultTask.scala:87)
at org.apache.spark.scheduler.Task.run(Task.scala:108)
at org.apache.spark.executor.Executor$TaskRunner.run(Executor.scala:335)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
关键字:
NoClassDefFoundError: org/apache/hadoop/hbase/client/HBaseAdmin
报错分析:
NoClassDefFoundError,这里也是找不到类的问题,HBase的jar包没有引入,需要把hbase的lib目录下的所有jar包引入,也可以只引入调用了的jar包
解决方法:
--jars $(echo /home/hadoop/app/hbase-1.2.0-cdh5.7.0/lib/*.jar |tr ' ' ',') \