项目描述
项目实现后能够分析出来的维度, 能够让决策者有哪方面的把控
技术架构. 该项目中用到的技术, 从以下几个方面进行描述
需求的理解和实现思路
项目中分析的维度. 例如有用户维度, 地域维度, 浏览器维度等
负责过哪些需求. 参与过哪些工作(包括实现需求之外的工作例如数据的对接, 清洗等 )
重要的hive表或者结果表中的字段需要记住
做过哪些优化
**日新增玩家(DNU): **当日新增加的玩家帐户数。
**日活跃玩家(DAU): ** 当日有开启过游戏的玩家数
**次日留存: ** 某日新增的玩家中,在下一日中还进行了游戏的玩家的比例
第一步: 事先准备的部分log日志
第二步: 使用logstash将准备好的数据拉取到kafka中
input {
file {
codec => plain {
charset => "UTF-8"
}
path => "/root/logserver/gamelog.txt"
discover_interval => 5
start_position => "beginning"
}
}
output {
kafka {
topic_id => "gamelogs"
codec => plain {
format => "%{message}"
charset => "UTF-8"
}
bootstrap_servers => "node01:9092,node02:9092,node03:9092"
}
}
第三步: 使用logstash从kafka中拉取数据到ES中
input {
kafka {
type => "accesslogs"
codec => "plain"
auto_offset_reset => "smallest"
group_id => "elas1"
topic_id => "accesslogs"
zk_connect => "node01:2181,node02:2181,node03:2181"
}
kafka {
type => "gamelogs"
auto_offset_reset => "smallest"
codec => "plain"
group_id => "elas2"
topic_id => "gamelogs"
zk_connect => "node01:2181,node02:2181,node03:2181"
}
}
filter {
if [type] == "accesslogs" {
json {
source => "message"
remove_field => [ "message" ]
target => "access"
}
}
if [type] == "gamelogs" {
mutate {
split => { "message" => "|" }
add_field => {
"event_type" => "%{message[0]}"
"current_time" => "%{message[1]}"
"user_ip" => "%{message[2]}"
"user" => "%{message[3]}"
}
remove_field => [ "message" ]
}
}
}
output {
if [type] == "accesslogs" {
elasticsearch {
index => "accesslogs"
codec => "json"
hosts => ["node01:9200", "node02:9200", "node03:9200"]
}
}
if [type] == "gamelogs" {
elasticsearch {
index => "gamelogs"
codec => plain {
charset => "UTF-16BE"
}
hosts => ["node01:9200", "node02:9200", "node03:9200"]
}
}
}
第四步: 上传后使用浏览器查看是否上传成功
第一步: 准备时间类型
/**
* 事件类型枚举
* 0 管理员登陆
* 1 首次登陆
* 2 上线
* 3 下线
* 4 升级
* 5 预留
* 6 装备回收元宝
* 7 元宝兑换RMB
* 8 PK
* 9 成长任务
* 10 领取奖励
* 11 神力护身
* 12 购买物品
*/
object EventType {
val REGISTER = "1"
val LOGIN = "2"
val LOGOUT = "3"
val UPGRADE = "4"
}
第二步: 准备时间工具类
package GameLogs
import java.util.Calendar
import org.apache.commons.lang3.time.FastDateFormat
object TimeUtils {
private val fastDateFormat: FastDateFormat = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss")
// 创建日期对象
private val calendar: Calendar = Calendar.getInstance()
// apply 方法: 注入方法, 起到初始化值的作用. 此时用该方法实现String类型的时间转化为Long类型
def apply(time: String): Long = {
calendar.setTime(fastDateFormat.parse(time))
calendar.getTimeInMillis
}
// 改变日期的方法 获取到几天后的时间, 返回
def updateCalendar(amout: Int): Long = {
calendar.add(Calendar.DATE, amout)
val time = calendar.getTimeInMillis
calendar.add(Calendar.DATE, -amout)
time
}
}
第三步: 准备过滤方法的工具类
package GameLogs
import org.apache.commons.lang3.time.FastDateFormat
object FilterUtils {
private val fastDateFormat: FastDateFormat = FastDateFormat.getInstance("yyyy年MM月dd日,E,HH:mm:ss")
// 按照时间进行过滤
def filterByTime(fields: Array[String], startTime: Long, endTime: Long) = {
val time = fields(1)
val time_long = fastDateFormat.parse(time).getTime
time_long >= startTime && time_long < endTime
}
// 按照时间类型进行过滤
def filterByType(fields: Array[String], eventType: String) = {
val _type = fields(0)
_type.equals(eventType)
}
// 以事件类型和时间进行过滤
def filterByTypeAndTime(fields: Array[String], eventType: String, startTime: Long, endTime: Long) = {
val b1: Boolean = filterByType(fields, eventType)
val b2: Boolean = filterByTime(fields, startTime, endTime)
b1 && b2
}
// 按照多个事件进行过滤
def filterByTypes(fields: Array[String], eventTypes: String*): Boolean = {
for (ev <- eventTypes) {
if (filterByType(fields, ev)) {
return true
}
}
return false
}
}
第四步: 统计新增用户, 活跃用户以及次日留存
package GameLogs
import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}
object GameLogAnalyze {
def main(args: Array[String]): Unit = {
val conf: SparkConf = new SparkConf()
.setAppName("GameLogAnalyze")
.setMaster("local[2]")
.set("es.nodes", "node1,node2,node3")
.set("es.port", "9200")
.set("es.index.auto.create", "true")
val sc = new SparkContext(conf)
val queryTime = "2016-02-01 00:00:00"
val startTime: Long = TimeUtils(queryTime)
val endTime: Long = TimeUtils.updateCalendar(+1)
val query =
"""
{"query":{"match_all":{}}}
""".stripMargin
val queryRdd: RDD[collection.Map[String, AnyRef]] = sc.esRDD("gamelogs", query).map(_._2)
//val queryRdd: RDD[String] = sc.textFile("E:\\BigData\\06-Spark\\sparkcoursesinfo\\project\\gameloganalyze\\GameLog.txt")
val splitRdd: RDD[Array[String]] = queryRdd.map(line => {
val et: String = String.valueOf(line.getOrElse("event_type", "-1"))
val time: String = String.valueOf(line.getOrElse("current_time", ""))
val user: String = String.valueOf(line.getOrElse("user", ""))
Array(et, time, user)
})
// TODO 日新增用户
val dnu: RDD[Array[String]] = splitRdd.filter(x => {
FilterUtils.filterByTypeAndTime(x, EventType.REGISTER, startTime, endTime)
})
// TODO 日活跃用户
val filteredTimeAndTypes: RDD[Array[String]] = splitRdd.filter(arr => {
FilterUtils.filterByTime(arr, startTime, endTime) &&
FilterUtils.filterByTypes(arr, EventType.REGISTER, EventType.LOGIN)
})
// 一些用户在一天之内登陆多次, 需要进行去重
val dau: RDD[String] = filteredTimeAndTypes.map(_ (2).distinct)
// TODO 次日留存
// 在join的时候, 数据必须是key,value形式的数据, 所以先把dnu数据调整一下
val dnuTup: RDD[(String, Int)] = dnu.map(fields => (fields(2), 1))
// 第二天的登陆数据
val day2Login: RDD[Array[String]] = splitRdd.filter(arr => {
FilterUtils.filterByTypeAndTime(arr, EventType.LOGIN, TimeUtils.updateCalendar(1), TimeUtils.updateCalendar(2))
})
// 将第二天用户登录的数据进行去重, 再将数据类型进行转换, 转换为key,value的形式
val day2Uname: RDD[(String, Int)] = day2Login.map(_ (2)).distinct.map((_, 1))
// 进行join
val morrowkeep: RDD[(String, (Int, Int))] = dnuTup.join(day2Uname)
// TODO 次日留存率: morrowkeep.count/dnu.count
// TODO 输出统计结果
println("日新增用户" + dnu.map(_ (2)).collect().toBuffer)
println("日新增用户数" + dnu.count())
println("日活跃用户数" + dau.count())
println("次日留存用户数" + morrowkeep.count())
sc.stop()
}
}
注意事项
在算子内不要new 一个对象, 避免产生大量对象 , 占用内存
SimpleDataFormat是线程不安全的, 所以最好用FaseDataFormat
常用的代码逻辑抽取方法放到一个工具类中, 起到代码重用的效果
namenode,resourcemanager, master 各独占一个节点
namenode(standby),resourcemanager(standby),master(standby) 共占用一个节点, 因为高可用的情况下, 集群并没有那么容易宕机, 所以完全可以将三个组件放在一个节点之上
datanode,nodeManager,worker,hbase放在一个节点上, 这样的节点若干
zookeeper单独一个集群, 至少三台
kafka单独一个集群, 至少三台
redis独占一个节点
ES单独一个集群, 至少三台(如果有的话)
10个节点, 每个节点10G 内存, 4个核心, 运行10T的数据, 大概需要8分钟, 这只是一个参考, 具体还要看优化的程度, 任务的复杂度.