spark14--游戏项目,面试中的集群问题

一 项目

1.1 面试中需要掌握的项目流程

  • 项目描述

    项目实现后能够分析出来的维度, 能够让决策者有哪方面的把控

  • 技术架构. 该项目中用到的技术, 从以下几个方面进行描述

    1. 数据的生成
    2. 数据的采集
    3. 数据的清洗
    4. 源数据的存储
    5. 需求分析
    6. 结果的存储
  • 需求的理解和实现思路

  • 项目中分析的维度. 例如有用户维度, 地域维度, 浏览器维度等

  • 负责过哪些需求. 参与过哪些工作(包括实现需求之外的工作例如数据的对接, 清洗等 )

  • 重要的hive表或者结果表中的字段需要记住

  • 做过哪些优化

1.2 游戏运营项目

1.2.1 技术框架
  1. 用户通过推广网站进入游戏官网, 用户可能在官网下载客户端
  2. 官网通过js埋点采集用户点击流日志
  3. 日志点击流通过http发送到 logserver 中, 如果用户操作的是客户端, 可以直接将日志信息发送到logserver中
  4. 使用logstash或者flume可以将log日志拉取到kafka中
  5. 如果要做离线分析可以使用flume或logstash将日志拉取到ES集群中,然后使用spark-core进行分析. 如果是实时分析可以直接使用spark-Streaming进行分析, 无需再拉取到ES中
  6. 离线分析数据(冷数据)的结果存储到mysql中, 实时分析的数据(热数据)存储到redis中
  7. 利用可视化工具, 将数据生成图表
    在这里插入图片描述
1.2.2 需求分析

**日新增玩家(DNU): **当日新增加的玩家帐户数。

**日活跃玩家(DAU): ** 当日有开启过游戏的玩家数

**次日留存: ** 某日新增的玩家中,在下一日中还进行了游戏的玩家的比例

1.2.3 实现需求
1.2.3.1 数据采集

第一步: 事先准备的部分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"]
    } 
  }
}

第四步: 上传后使用浏览器查看是否上传成功

在这里插入图片描述

1.2.3.2 实例代码

第一步: 准备时间类型

/**
  * 事件类型枚举
  * 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

    常用的代码逻辑抽取方法放到一个工具类中, 起到代码重用的效果

二 面试中的集群问题

2.1 集群部署

namenode,resourcemanager, master 各独占一个节点

namenode(standby),resourcemanager(standby),master(standby) 共占用一个节点, 因为高可用的情况下, 集群并没有那么容易宕机, 所以完全可以将三个组件放在一个节点之上

datanode,nodeManager,worker,hbase放在一个节点上, 这样的节点若干

zookeeper单独一个集群, 至少三台

kafka单独一个集群, 至少三台

redis独占一个节点

ES单独一个集群, 至少三台(如果有的话)

2.2 集群中节点的配置

  • 一个机柜(机架)可以放8个, 10个, 12个. 机架的品牌可以直接回答不知道
  • 一个刀片机(廉价机)含有的主要组件: CPU, 内存, 硬盘
    • CPU: 可以是2个CPU, 每个是8核的, 总共16核, 也可以是4个CPU, 每个cpu 6核, 总共24核
    • 内存: 可以是32G, 64G, 128G, 256G
    • 硬盘: 8~12T

2.3 spark跑任务所需时间

10个节点, 每个节点10G 内存, 4个核心, 运行10T的数据, 大概需要8分钟, 这只是一个参考, 具体还要看优化的程度, 任务的复杂度.

2.4 每天的数据量

  • 一条数据大概在0.3k~1.5k, 按照0.8k 来计算, 大概30多个字段
  • 如果一天生成的数据是80G, 那么大概有100 000 000 条数据
  • 清洗后的数据大概是元数据的4/5~2/3
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值