在 Dockone 的分享 - 暴走漫画容器实践系列 Part1

大家好,我是 Michael Ding,来自暴走漫画。

暴走漫画是一家文化传媒公司。公司除了有若干视频娱乐节目,还有相应的社区网站及 App。流量 UV 200w/天 左右,PV 千万。
为了更加有效地运营以及推荐用户个性化,2015年成立了数据部,负责暴漫的数据分析和数据挖掘相关服务。

暴漫没有自己的服务器,是使用的国内某云服务。暴漫的后端主要是基于 Ruby 开发。也有基于 go, python 的一些micro service。
Docker 在暴漫中的应用主要包括:

  • 开发环境的 service 搭建

  • 代码托管,持续集成,docker 镜像,等若干 support 服务

  • 部分 micro service 以及整个数据服务系统

所以今天的内容是一些中小规模以及国内云服务下的 docker 实践的相关心得,主要包括在数据服务的架构及 docker 化的部署。

1. 简单介绍下开发环境以及 support 服务的 docker 应用

由于开发环境主要是 Mac,也有少量 Ubuntu 和 Windows,所以主要采用 Vagrant + docker 方式。
将 micro service 做成 image,在 Vagrant 中起相应的container,把端口暴露给 Host(Vagrant)。本地跑 Ruby(on Rails)

support 服务的话,其他都很简单,只有持续集成介绍下。我们用的 gitlab ci。gitlab ci 支持将 task 跑在 docker container 里面
所以我们为不同的项目准备不同的测试环境(image)以及外部依赖(eg. mysql, redis),然后在对应的 container 里面跑测试。
关于部署的话,我们平时的开发在 develop 分支,一旦向 master 分支合并后,会触发部署的 task。
部署的 task 跑在特定的 container 里面,这个 container 共享了 Host 的 docker unix sock 文件,可以执行 docker build, push 等命令

关于开发环境和 support 服务的 docker 应用,因为不是今天的重点,并且前面也有很多朋友做过类似的介绍,所以先简单介绍到这里。

2. micro service 和 数据服务系统的 docker 应用

今年我们做了很多 micro service 的尝试,例如消息推送,推荐系统,反垃圾系统,数据分析系统,视频抓取等等若干子系统的拆分上线。
虽然过程是痛苦的,但是结果却是令人欣慰的。这些 micro service,几乎都是基于 docker 的。

2.1 Rails + docker 化的 micro service

整体来说,我们是个混合的架构,Rails 是正常的跑在云主机中的,micro service 跑在 docker 中。为了协调好各方,我们对基础服务做了一点小小的调整。

这里不得不说说我做架构的一点心得。好的架构除了能满足业务需求,还要是与特定的团队,特定的资源所配套的。
在暴漫,由于技术力量有限,开发排期满,所以我都是尽量采用“非侵入式”的方案,这在后面的数据服务的构建中也有体现。

首先,我们给所有的机器都装上了 docker
其次,我们搭建了一个 etcd 集群,将所有的云主机都纳入了 etcd 集群。而 etcd 也是跑在 docker 里的。
为了方便的跑起来 etcd,我们写了个一套 bash + python 的脚本(Python 的脚本也是跑在 docker 里的)
然后,所有的机器直接访问本机 IP 可以 access etcd。

这里插一句,我们没有去折腾如何让docker跨主机组网,而是直接采用映射到 host的方式。一方面国内云主机只能这么干。另一方面,我们之前使用云主机也是单个主机特定用途的。
另外,在生产环境中,我们大量的使用了 shell + etcd 来启动 docker container 的方式。可以给大家看个 etcd 的启动 script。这个 script 放到最初的机器上就可以方便地启动起来etcd 集群。

#!/bin/bash

check_non_empty() {
  # $1 is the content of the variable in quotes e.g. "$FROM_EMAIL"
  # $2 is the error message
  if [[ $1 == "" ]]; then
  echo "ERROR: specify $2"
  exit -1
  fi
}

check_exec_success() {
  # $1 is the content of the variable in quotes e.g. "$FROM_EMAIL"
  # $2 is the error message
  if [[ $1 != "0" ]]; then
  echo "ERROR: $2 failed"
  echo "$3"
  exit -1
  fi
}

up() {

  # create ${EtcdData}
  mkdir -p ${EtcdData}

  # pull pycsa docker image
  docker pull private/pycsa:latest

  check_exec_success "$?" "pulling 'pycsa' image"

  # pull etcd docker image
  docker pull quay.io/coreos/etcd:latest

  check_exec_success "$?" "pulling 'etcd' image"

  # build cluster nodes list for `-initial-cluster`
  cwd=$(pwd)
  ClusterNodes=$(docker run --rm \
    -v ${cwd}:/data \
    private/pycsa:latest \
    python up.py cluster-nodes ${1} ${ETCD_NAME} ${HostIP})

    check_exec_success "$?" ${ClusterNodes}

    case "$1" in
    "-a")
    ${BaseCmd} -initial-cluster ${ClusterNodes} \
    -initial-cluster-state existing
    ;;
    "")
    ${BaseCmd} -initial-cluster ${ClusterNodes} \
    -initial-cluster-token bzetcd-cluster -initial-cluster-state new
    ;;
    *)
    echo "Usage: ./etcd.sh up [-a]"
    exit 1
    ;;
    esac
  }

  start() {
    docker kill etcd 2>/dev/null
    docker rm etcd 2>/dev/null
    ${BaseCmd}
  }

  stop() {
    docker stop etcd
    docker rm etcd
  }


  ##################
  # Start of script
  ##################

  # source env
  . /etc/default/etcd

  check_non_empty "${ETCD_NAME}" "ETCD_NAME"

  # get host ip
  HostIP=$(ifconfig eth0 | awk '/\/ { print $2}' | sed 's/addr://g')

  # set data dir
  EtcdData=/data/etcd/data

  # create etcd container base cmd
  BaseCmd="docker run -d \
  -v /usr/share/ca-certificates/:/etc/ssl/certs \
  -v ${EtcdData}:/data \
  -p 4001:4001 -p 2380:2380 -p 2379:2379 \
  --name etcd quay.io/coreos/etcd:latest \
  -name ${ETCD_NAME} \
  -data-dir /data \
  -advertise-client-urls http://${HostIP}:2379,http://${HostIP}:4001 \
  -listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001 \
  -initial-advertise-peer-urls http://${HostIP}:2380 \
  -listen-peer-urls http://0.0.0.0:2380"

  case "$1" in
  up) up "$2" ;;
  start) start ;;
  stop) stop ;;
  restart)
  stop
  start
  ;;
  *)
  echo "Usage: ./etcd.sh start|stop|restart or ./etcd.sh up [-a]"
  exit 1
  ;;
  esac

  exit 0

解释下, up.py 是个 python 的脚本,跑在一个 pycsa 的容器里,这个容器有 python 环境以及相关的 package

这样原来的服务几乎不受任何影响,我们可以利用 etcd + docker + shell script 来组建新的服务。

2.2 数据服务

我们的数据服务包括数据分析和数据挖掘两大块。数据分析主要是为了给运营提供量化的效果评估以及指导。数据挖掘则包括推荐,反垃圾等。

数据服务的基础是数据流,即:数据收集->数据分发->数据处理<->数据存储

先给大家看个整体的架构图,由于本人不擅作图,所以直接用手画的,还请见谅。。

首先数据收集部分,就像之前说的,我尽量采用“非侵入式”的方案,所以,我们的整个数据收集都是基于日志的。
我们在每个应用服务器上装了 logstash (跑在 docker 中) 来收集各个应用服务器的日志,然后打到 kafka (跑在 docker 中) 里,给不同的用途使用。

一份COPY 直接由kafka 一端的 logstash 存储到 elasticsearch(跑在 docker 中) 中
一份COPY 经过 spark (跑在 docker 中) stream 做实时处理(包括一些特定日志的提取),然后将处理的结果存储在 elasticsearch 里
还有一份 COPY 直接存储到 HDFS (由云服务商提供)

这里有个小问题,比如有些数据本身日志里并没有,比如用户的点击行为。这个时候,我们专门开发了一些 "ping" 接口,这些接口通过 Nginx 直接返回 200,并记录相关日志

此外还有一部分数据,例如一些比较需要“较严格的完备”的,例如用于推荐系统,反垃圾系统学习的数据,我们存储在 SQL 数据库中

下面我做些稍微详细的介绍

2.2.1 数据分析

数据分析有两种:实时数据分析和离线数据分析

实时数据分析从 kafka 到 spark stream,处理结果进 elasticsearch,离线分析是定时任务,从 HDFS 到 spark,处理结果进 elasticsearch。一般来说,离线的结果会逐步包含实时的结果,
同时实时的结果领先于离线分析的结果。

这里的分析有些抽象,我来举个例子:

Q: 统计某个板块同时在线人数的变化趋势
A: 用户每次访问都有日志,日志里包括访问内容以及用户标识。首先 spark stream 从日志里抽取出特定板块不同用户的访问事件,以秒为单位合并相同用户事件。这就是分析结果:时间戳:人数

然后这个结果怎么用?

elasticsearch 有很强大的 agg 接口。你可以以1秒,10秒,1分等等各种时间间隔单位聚合这段时间内的在线人数,聚合方式用 '平均'或'最大'

2.2.2 数据挖掘

我们主要做了2个具体的数据挖掘系统:推荐+反垃圾

今天主要讲下架构。

这两个系统基本上步骤是一样的,分为2步:训练(train) 和 服务(serve)

在 train 阶段,定时起一个 spark job,从训练数据集中读取数据,学习出 model,然后将 model 存储成文件
在 serve 阶段,起一个带 serve 的 spark job,load 之前学习出来的model 文件进内存,然后接受外部api 调用,返回结果。

关于服务的开发这部分因为涉及到太多额外的知识,我就不多说了。

这里讲个难点:spark 的 docker 化。

2.2.3 Spark 的 docker 化

Spark 的 docker 化分为两个部分:

  • docker 化的 spark 集群

  • docker 化的 spark 调用

Spark 和我们一般用的服务不太一样,它的集群不是提供运算服务的,而是一种资源分配的调度器。
让 Spark 跑 Job,其实是起的一个 Spark 的本地程序,这个本地程序会向 cluster 要资源(其他机器),cluster 分配资源以后,这个 spark 程序就把一些工作放在这些资源当中运行(进程)

所以 Spark 的 docker 化分为两个部分。

对于 spark 调用,也就是启动 spark 的本地程序,我们就是在跑程序的 image 中集成 java 环境,spark 程序

对于 spark 集群,稍微复杂一些。spark 支持三种集群:mesos, yard,还有 spark 自己的一个 standalone
我们搭建的 spark standalone 集群,这还是考虑到我们自身的资源与需求。

由于没找到官方的 spark docker image,我们自己做了一个,就是 java 环境 + spark 程序
然后利用 script + etcd 以不同的姿势(master 或 slave)在不同的云主机上启动 spark container

官方推荐要起3个 master, 用 zookeeper 做 quorum,这个我们最近正在搞,还没上线,就不分享。我们现在线上跑的是 1 master + 7 slave

谢谢

--

更多文章欢迎关注自由风暴博客

你可能感兴趣的:(容器技术,分布式)