Jupyter允许什么?

我们的故事始于一项看似简单的任务。 必须为数据科学专家和数据分析师建立分析工具。 零售风险和CRM部门的同事已将这项任务交给了我们,在该部门中,数据科学专家的集中度一直很高。 客户有一个简单的愿望-编写Python代码,导入高级库(xgboost,pytorch,tensorflow等),并对从hdfs集群中获得的数据运行算法。



一切似乎都很简单明了。 但是有很多陷阱,我们决定写一篇关于它的文章,并在GitHub上发布现成的解决方案。

首先,有关源基础结构的一些详细信息:

  • HDFS数据仓库(12个Oracle Big Data Appliance节点,Cloudera分发)。 仓库总共有来自银行各种内部系统的130 TB数据;也有来自外部源的异构信息。
  • 假定在其上部署分析工具的两个应用程序服务器。 值得一提的是,不仅高级分析任务在这些服务器上“旋转”,因此要求之一是使用容器化工具(Docker)来管理服务器资源,使用各种环境并对其进行配置。

作为分析师工作的主要环境,他们决定选择JupyterHub,而JupyterHub实际上已经成为处理数据和开发机器学习模型的标准之一。 在此处了解更多信息。 将来,我们已经想到了JupyterLab。

似乎一切都很简单:您需要采用并配置一堆Python + Anaconda + Spark。 在应用程序服务器上安装Jupyter Hub,与LDAP集成,以任何其他方式连接Spark或连接到hdfs中的数据,然后继续-构建模型!
如果您深入研究所有源数据和需求,那么这里是更详细的列表:

  • 在Docker中运行JupyterHub(基本操作系统-Oracle Linux 7)
  • Cloudera CDH 5.15.1 +具有Active Directory配置中的Kerberos身份验证的Spark 2.3.0集群+集群中的专用Kerberos MIT MIT(请参见具有Active Directory的集群专用MIT KDC ),Oracle Linux 6
  • Active Directory整合
  • Hadoop和Spark中的透明身份验证
  • Python 2和3支持
  • Spark 1和2(具有使用集群资源来训练模型和使用pyspark并行化数据处理的功能)
  • 限制主机资源的能力
  • 图书馆套装

这篇文章是为需要解决此类问题的IT专业人员而设计的。

解决方案说明


在Docker + Cloudera集群集成中启动


这里没有异常。 JupyterHub和Cloudera产品客户端安装在容器中(如下所示),配置文件从主机安装:

start-hub.sh

VOLUMES="-v/var/run/docker.sock:/var/run/docker.sock:Z -v/var/lib/pbis/.lsassd:/var/lib/pbis/.lsassd:Z -v/var/lib/pbis/.netlogond:/var/lib/pbis/.netlogond:Z -v/var/jupyterhub/home:/home/BANK/:Z -v/u00/:/u00/:Z -v/tmp:/host/tmp:Z -v${CONFIG_DIR}/krb5.conf:/etc/krb5.conf:ro -v${CONFIG_DIR}/hadoop/:/etc/hadoop/conf.cloudera.yarn/:ro -v${CONFIG_DIR}/spark/:/etc/spark/conf.cloudera.spark_on_yarn/:ro -v${CONFIG_DIR}/spark2/:/etc/spark2/conf.cloudera.spark2_on_yarn/:ro -v${CONFIG_DIR}/jupyterhub/:/etc/jupyterhub/:ro" docker run -p0.0.0.0:8000:8000/tcp ${VOLUMES} -e VOLUMES="${VOLUMES}" -e HOST_HOSTNAME=`hostname -f` dsai1.2 


Active Directory整合


为了与Active Directory / Kerberos Iron(不是很主机)集成,我们公司的标准是PBIS Open产品。 从技术上讲,该产品是一组与Active Directory通信的服务,客户端可以通过这些服务通过UNIX域套接字进行工作。 该产品与Linux PAM和NSS集成。

我们使用了标准的Docker方法-主机服务的unix域套接字被安装在一个容器中(套接字是通过使用lsof命令的简单操作凭经验找到的):

start-hub.sh

 VOLUMES="-v/var/run/docker.sock:/var/run/docker.sock:Z -v/var/lib/pbis/.lsassd:/var/lib/pbis/.lsassd:Z <b>-v/var/lib/pbis/.netlogond:/var/lib/pbis/.netlogond:Z -v/var/jupyterhub/home:/home/BANK/:Z -v/u00/:/u00/:Z -v/tmp:/host/tmp:Z -v${CONFIG_DIR}/krb5.conf:/etc/krb5.conf:ro </b> -v${CONFIG_DIR}/hadoop/:/etc/hadoop/conf.cloudera.yarn/:ro -v${CONFIG_DIR}/spark/:/etc/spark/conf.cloudera.spark_on_yarn/:ro -v${CONFIG_DIR}/spark2/:/etc/spark2/conf.cloudera.spark2_on_yarn/:ro -v${CONFIG_DIR}/jupyterhub/:/etc/jupyterhub/:ro" docker run -p0.0.0.0:8000:8000/tcp ${VOLUMES} -e VOLUMES="${VOLUMES}" -e HOST_HOSTNAME=`hostname -f` dsai1.2 

反过来,PBIS软件包将安装在容器内,但不执行安装后部分。 因此,我们只放置可执行文件和库,而不在容器中启动服务-这对我们来说是多余的。 PAM和NSS Linux集成命令是手动运行的。

Dockerfile:

 # Install PAM itself and standard PAM configuration packages. RUN yum install -y pam util-linux \ # Here we just download PBIS RPM packages then install them omitting scripts. # We don't need scripts since they start PBIS services, which are not used - we connect to the host services instead. && find /var/yum/localrepo/ -type f -name 'pbis-open*.rpm' | xargs rpm -ivh --noscripts \ # Enable PBIS PAM integration. && domainjoin-cli configure --enable pam \ # Make pam_loginuid.so module optional (Docker requirement) and add pam_mkhomedir.so to have home directories created automatically. && mv /etc/pam.d/login /tmp \ && awk '{ if ($1 == "session" && $2 == "required" && $3 == "pam_loginuid.so") { print "session optional pam_loginuid.so"; print "session required pam_mkhomedir.so skel=/etc/skel/ umask=0022";} else { print $0; } }' /tmp/login > /etc/pam.d/login \ && rm /tmp/login \ # Enable PBIS nss integration. && domainjoin-cli configure --enable nsswitch 

事实证明,PBIS容器的客户端与PBIS主机服务进行通信。 JupyterHub使用PAM身份验证器,并且在主机上配置了正确的PBIS之后,一切都可以直接使用。

为了防止所有用户进入AD,您可以使用将用户限制为特定AD组的设置。

config-example / jupyterhub / jupyterhub_config.py

 c.DSAIAuthenticator.group_whitelist = ['COMPANY\\domain^users'] 

Hadoop和Spark中的透明身份验证


登录到JupyterHub时,PBIS将用户的Kerberos票证缓存在/ tmp目录中的特定文件中。 对于以这种方式进行透明身份验证,将主机的/ tmp目录挂载在容器中并将KRB5CCNAME变量设置为所需值就足够了(这在我们的authenticator类中完成)。

start-hub.sh

 VOLUMES="-v/var/run/docker.sock:/var/run/docker.sock:Z -v/var/lib/pbis/.lsassd:/var/lib/pbis/.lsassd:Z -v/var/lib/pbis/.netlogond:/var/lib/pbis/.netlogond:Z -v/var/jupyterhub/home:/home/BANK/:Z -v/u00/:/u00/:Z -v/tmp:/host/tmp:Z -v${CONFIG_DIR}/krb5.conf:/etc/krb5.conf:ro -v${CONFIG_DIR}/hadoop/:/etc/hadoop/conf.cloudera.yarn/:ro -v${CONFIG_DIR}/spark/:/etc/spark/conf.cloudera.spark_on_yarn/:ro -v${CONFIG_DIR}/spark2/:/etc/spark2/conf.cloudera.spark2_on_yarn/:ro -v${CONFIG_DIR}/jupyterhub/:/etc/jupyterhub/:ro" docker run -p0.0.0.0:8000:8000/tcp ${VOLUMES} -e VOLUMES="${VOLUMES}" -e HOST_HOSTNAME=`hostname -f` dsai1.2 

资产/ jupyterhub / dsai.py

 env['KRB5CCNAME'] = '/host/tmp/krb5cc_%d' % pwd.getpwnam(self.user.name).pw_uid 

由于上面的代码,JupyterHub用户可以从Jupyter终端执行hdfs命令并运行Spark作业,而无需其他身份验证步骤。 将主机的整个/ tmp目录挂载到容器中是不安全的-我们知道此问题,但其解决方案仍在开发中。

Python版本2和3


在这里,似乎一切都很简单:您需要安装必要的Python版本并将其与Jupyter集成,以创建必要的内核。 这个问题已经在许多地方讨论过了。 Conda用于管理Python环境。 下一节将阐明为什么所有简单性仅是显而易见的。 Python 3.6的内核示例(此文件不在git中-所有内核文件均由代码生成):

/opt/cloudera/parcels/Anaconda-5.3.1-dsai1.0/envs/python3.6.6/share/jupyter/kernels/python3.6.6/kernel.json

 {   "argv": [      "/opt/cloudera/parcels/Anaconda-5.3.1-dsai1.0/envs/python3.6.6/bin/python",       "-m",       "ipykernel_launcher",       "-f",      "{connection_file}"   ],   "display_name": "Python 3",   "language": "python" } 

火花1和2


要与SPARK客户集成,还需要创建内核。 Python 3.6和SPARK 2的内核示例。

/opt/cloudera/parcels/Anaconda-5.3.1-dsai1.0/envs/python3.6.6/share/jupyter/kernels/python3.6.6-pyspark2/kernel.json

 {   "argv": [       "/opt/cloudera/parcels/Anaconda-5.3.1-dsai1.0/envs/python3.6.6/bin/python",       "-m",       "ipykernel_launcher",       "-f",      "{connection_file}"   ],   "display_name": "Python 3 + PySpark 2",   "language": "python",   "env": {       "JAVA_HOME": "/usr/java/default/",       "SPARK_HOME": "/opt/cloudera/parcels/SPARK2/lib/spark2/",       "PYTHONSTARTUP": "/opt/cloudera/parcels/SPARK2/lib/spark2/python/pyspark/shell.py",       "PYTHONPATH": "/opt/cloudera/parcels/SPARK2/lib/spark2/python/:/opt/cloudera/parcels/SPARK2/lib/spark2/python/lib/py4j-0.10.7-src.zip",       "PYSPARK_PYTHON": "/opt/cloudera/parcels/Anaconda-5.3.1-dsai1.0/envs/python3.6.6/bin/python"   } } 

只需注意,具有Spark 1支持的要求在历史上就已发展。 但是,某人可能会面临类似的限制-例如,您无法在集群中安装Spark 2。 因此,我们在这里描述在实施过程中遇到的陷阱。
首先,Spark 1.6.1 不适用于Python 3.6。 有趣的是,在CDH 5.12.1中,此问题已修复,但在5.15.1中已修复-出于某种原因未解决)。 首先,我们想通过简单地应用适当的补丁来解决此问题。 但是,将来必须放弃这种想法,因为这种方法需要在集群中安装经过修改的Spark,这对我们来说是不可接受的。 在使用Python 3.5创建单独的Conda环境时找到了解决方案。

第二个问题阻止Spark 1在Docker内部运行。 Spark驱动程序打开一个特定的端口,Worker通过该端口连接到驱动程序-为此,驱动程序向其发送其IP地址。 对于Docker Worker,它会尝试通过容器的IP连接到驱动程序,而当使用network = bridge时,它并不是很自然地工作。

显而易见的解决方案是不发送容器的IP,而是发送主机的IP,这是在Spark 2中通过添加适当的配置设置实现的。 该修补程序经过创造性地重新设计,并应用于Spark1。以这种方式修改的Spark不需要放置在群集主机上,因此不会出现与Python 3.6不兼容的问题。

不管Spark的版本是什么,就其功能而言,群集中必须具有与容器中相同的Python版本。 要直接绕过Cloudera Manager安装Anaconda,我们必须学习做两件事:

  • 使用Anaconda和所有合适的环境构建包裹
  • 将其安装在Docker中(出于一致性考虑)

组装包裹蟒蛇


原来这是一个相当简单的任务。 您需要做的是:

  1. 通过安装所需版本的Anaconda和Python环境来准备包裹内容
  2. 创建元数据文件并将其放在meta目录中
  3. 用简单的tar创建包裹
  4. 从Cloudera验证宗地实用程序

该过程在GitHub上有更详细的描述,那里还有一个验证器代码。 我们在Cloudera官方Anaconda官方包裹中借用了元数据,对它进行了创造性的重新制作。

在Docker中安装包裹


事实证明,这种做法很有用,原因有两个:

  • 确保Spark的可操作性-无法将Anaconda放在没有包裹的集群中
  • Spark 2仅以包裹形式分发-您当然可以将其安装为以jar文件的形式在容器中,但是这种方法被拒绝了

作为解决上述问题的一项奖励,我们收到了:

  • 轻松设置Hadoop和Spark客户端-在Docker和集群中安装相同的包裹时,集群和容器中的路径相同
  • 易于在容器和群集中维护统一的环境-更新群集时,仅使用与群集中安装的相同宗地重新构建Docker映像。

要在Docker中安装包裹,首先要从RPM软件包中安装Cloudera Manager。 对于实际的包裹安装,使用Java代码。 Java中的客户端知道Python中的客户端无法执行的操作,因此我不得不使用Java并失去一些统一性),这会调用API。

资产/ install-parcels / src / InstallParcels.java

 ParcelsResourceV5 parcels = clusters.getParcelsResource(clusterName); for (int i = 1; i < args.length; i += 2) {   result = installParcel(api, parcels, args[i], args[i + 1], pause);   if (!result) {       System.exit(1);   } } 

主机资源限制


为了管理主机资源,使用了DockerSpawner的组合(一个在单独的Docker容器中运行Jupyter最终用户的组件)和cgroups (一种Linux中的资源管理机制)。 DockerSpawner使用Docker API,该API允许您设置容器的父cgroup。 常规DockerSpawner中没有这种可能性,因此我们编写了简单的代码,使我们可以在配置中设置AD实体和父cgroup之间的对应关系。

资产/ jupyterhub / dsai.py

 def set_extra_host_config(self):       extra_host_config = {}       if self.user.name in self.user_cgroup_parent:           cgroup_parent = self.user_cgroup_parent[self.user.name]       else:           pw_name = pwd.getpwnam(self.user.name).pw_name           group_found = False           for g in grp.getgrall():               if pw_name in g.gr_mem and g.gr_name in self.group_cgroup_parent:                   cgroup_parent = self.group_cgroup_parent[g.gr_name]                   group_found = True                   break           if not group_found:               cgroup_parent = self.cgroup_parent extra_host_config['cgroup_parent'] = cgroup_parent 

还引入了一个小的修改,该修改从启动JupyterHub的同一映像启动Jupyter。 因此,不需要使用多个图像。

资产/ jupyterhub / dsai.py

 current_container = None host_name = socket.gethostname() for container in self.client.containers():   if container['Id'][0:12] == host_name:       current_container = container       break self.image = current_container['Image'] 

在容器脚本(Jupyter或JupyterHub)中确切运行的内容由启动脚本中的环境变量确定:

资产/ jupyterhub / dsai.py

 #!/bin/bash ANACONDA_PATH="/opt/cloudera/parcels/Anaconda/" DEFAULT_ENV=`cat ${ANACONDA_PATH}/envs/default` source activate ${DEFAULT_ENV} if [ -z "${JUPYTERHUB_CLIENT_ID}" ]; then   while true; do       jupyterhub -f /etc/jupyterhub/jupyterhub_config.py   done else   HOME=`su ${JUPYTERHUB_USER} -c 'echo ~'`   cd ~   su ${JUPYTERHUB_USER} -p -c "jupyterhub-singleuser --KernelSpecManager.ensure_native_kernel=False --ip=0.0.0.0" fi 

从JupyterHub Docker容器启动Jupyter Docker容器的能力是通过将Docker守护进程套接字安装在JupyterHub容器中来实现的。

start-hub.sh

 VOLUMES="-<b>v/var/run/docker.sock:/var/run/docker.sock:Z -v/var/lib/pbis/.lsassd:/var/lib/pbis/.lsassd:Z</b> -v/var/lib/pbis/.netlogond:/var/lib/pbis/.netlogond:Z -v/var/jupyterhub/home:/home/BANK/:Z -v/u00/:/u00/:Z -v/tmp:/host/tmp:Z -v${CONFIG_DIR}/krb5.conf:/etc/krb5.conf:ro -v${CONFIG_DIR}/hadoop/:/etc/hadoop/conf.cloudera.yarn/:ro -v${CONFIG_DIR}/spark/:/etc/spark/conf.cloudera.spark_on_yarn/:ro -v${CONFIG_DIR}/spark2/:/etc/spark2/conf.cloudera.spark2_on_yarn/:ro -v${CONFIG_DIR}/jupyterhub/:/etc/jupyterhub/:ro" docker run -p0.0.0.0:8000:8000/tcp ${VOLUMES} -e VOLUMES="${VOLUMES}" -e HOST_HOSTNAME=`hostname -f` dsai1.2 

将来,计划放弃该决定,而推荐使用ssh。

当将DockerSpawner与Spark结合使用时,会出现另一个问题:Spark驱动程序打开随机端口,Workers通过这些端口建立外部连接。 我们可以通过在Spark配置中设置端口号的范围来控制从中选择随机端口号的范围。 但是,这些范围对于不同的用户必须是不同的,因为我们无法使用相同的已发布端口运行Jupyter容器。 为了解决这个问题,编写了代码,该代码仅通过JupyterHub数据库中的用户ID生成端口范围,并使用适当的配置启动Docker容器和Spark:

资产/ jupyterhub / dsai.py

 def set_extra_create_kwargs(self):       user_spark_driver_port, user_spark_blockmanager_port, user_spark_ui_port, user_spark_max_retries = self.get_spark_ports()       if user_spark_driver_port == 0 or user_spark_blockmanager_port == 0 or user_spark_ui_port == 0 or user_spark_max_retries == 0:           return       ports = {}       for p in range(user_spark_driver_port, user_spark_driver_port + user_spark_max_retries):           ports['%d/tcp' % p] = None       for p in range(user_spark_blockmanager_port, user_spark_blockmanager_port + user_spark_max_retries):           ports['%d/tcp' % p] = None       for p in range(user_spark_ui_port, user_spark_ui_port + user_spark_max_retries):           ports['%d/tcp' % p] = None self.extra_create_kwargs = { 'ports' : ports } 

该解决方案的缺点是,当您使用JupyterHub重新启动容器时,由于数据库丢失,一切都会停止工作。 因此,当您重新启动JupyterHub以进行例如配置更改时,我们不会触摸容器本身,而只会重新启动其中的JupyterHub进程。

重新启动-hub.sh

 #!/bin/bash docker ps | fgrep 'dsai1.2' | fgrep -v 'jupyter-' | awk '{ print $1; }' | while read ID; do docker exec $ID /bin/bash -c "kill \$( cat /root/jupyterhub.pid )"; done 

Cgroup本身是由标准Linux工具创建的,配置中AD实体和cgroup之间的对应关系如下所示。

 <b>config-example/jupyterhub/jupyterhub_config.py</b> c.DSAISpawner.user_cgroup_parent = {   'bank\\user1'    : '/jupyter-cgroup-1', # user 1   'bank\\user2'    : '/jupyter-cgroup-1', # user 2   'bank\\user3'    : '/jupyter-cgroup-2', # user 3 } c.DSAISpawner.cgroup_parent = '/jupyter-cgroup-3' 

Git代码


我们的解决方案可在GitHub上公开获得: https//github.com/DS-AI/dsai/ (DSAI-数据科学和人工智能)。 所有代码都按顺序排列在目录中-每个后续目录中的代码都可以使用上一个目录中的工件。 来自最后一个目录的代码结果将是Docker映像。

每个目录包含文件:

  • asset.sh-创建组装所需的工件(从Internet下载或从先前步骤的目录复制)
  • build.sh-构建
  • clean.sh-组装所需的清洁工件

为了完全重建Docker映像,有必要根据目录的序列号运行clean.sh,assets.sh,build.sh。

为了进行组装,我们使用一台装有Linux RedHat 7.4,Docker 17.05.0-ce的机器。 该计算机具有8个内核,32GB RAM和250GB磁盘空间。 强烈建议不要使用RAM和HDD设置最差的主机来构建它。

这是所用名称的帮助:

  • 01-spark-patched-RPM Spark 1.6.1,应用了两个补丁SPARK-4563和SPARK-19019。
  • 02-验证程序-包裹验证程序
  • 03-anaconda-dsai-parcel-1.0-带有正确Python(2、3.5和3.6)的Anaconda包裹
  • 04-cloudera-manager-api-Cloudera Manager API库
  • 05-dsai1.2-offline-最终图像

,程序集可能由于无法修复的原因而崩溃(例如,在组装程序包过程中tar掉落了。在这种情况下,通常,您只需要重新启动程序集即可,但这并不能总是有帮助(例如,Spark程序集取决于外部资源) Cloudera,可能不再可用等)。

另一个缺点是包裹组件无法复制。 由于库是不断更新的,因此重复执行程序集可能会得到与前一个结果不同的结果。

大结局


现在,用户已经成功使用了这些工具,其数量已经超过几十个,并且还在继续增长。 将来,我们计划尝试JupyterLab并考虑将GPU连接到群集,因为现在两个功能强大的应用程序服务器的计算资源已不再足够。

Source: https://habr.com/ru/post/zh-CN443294/


All Articles