多用户 Docker 环境下 PyPi 源按需加速

这一篇算是接在上一篇Build a super fast on demand local PyPi mirror的后面吧~

这里会以 docker-compose 的方式为例子,详细写一下~不使用docker-compose的话,则也仅仅需要手动指定 pypicache 与需要这个服务的 container 到同一个 docker 网络中,这样就可以不用去找 pypicache 的 IP 地址,对最终用户透明化,不用增加额外的 pip 安装参数,即可轻松享受本地高速缓存,特别是对于大一点的文件效果更明显~

jupyterhub-docker
jupyterhub-docker

理论分析加速比


以典型的 JupyterHub(https://github.com/jupyterhub/jupyterhub-deploy-docker) 为例子,先借用它的图说明一下,橙色部分是我们想要增加的 pypicache 服务~

jupyterhub-docker
jupyterhub-docker

假设某个 Python 包的为大小为 $m$ MB时,在有 $n$ 个 Docker 服务都需要这个包的时候,则需要从清华源走 $n\times m$ MB,平均下载速度约 $t$ MB/s,那么一共是 $t_1=\frac{nm}{t}$ 秒;应用高速缓存的话,则只需要在第一次访问时走外网,耗时为 $\frac{m}{t}$ 秒,之后都可以通过高速缓存分发,此时传输速率为 $d$ MB/s,耗时 $\frac{(n-1)m}{d}$ 秒,共计 $t_2=\frac{(n-1)m}{d}+\frac{m}{t}$ 秒~

那么加速比的计算如下

\begin{equation}
    \begin{aligned}
        &\left\{
            \begin{aligned}
            t_1&=\frac{nm}{t}\\
            t_2&=\frac{(n-1)m}{d}+\frac{m}{t}
            \end{aligned}
        \right.
        &\\
        \frac{t_1-t_2}{t_1}&=\frac{\frac{nm}{t}-\frac{(n-1)m}{d}-\frac{m}{t}}{\frac{nm}{t}}\\
        &=\frac{nm-\frac{(n-1)mt}{d}-m}{nm}\\
        &=1-\frac{(n-1)t}{nd}-\frac{1}{n}
    \end{aligned}
\end{equation}

在不同网络环境、硬件环境下 $t, d$ 值各不相同,在我们平台上典型的传输速度是 $t=5, d=200$,那么继续上面的计算

\begin{equation}
    \begin{aligned}
        =&1-\frac{5(n-1)}{200n}-\frac{1}{n}\\
        =&1-\frac{1}{40}+\frac{1}{40n}-\frac{1}{n}\\
        =&\frac{39}{40}-\frac{39}{40n}\\
        =&\frac{39n-39}{40n}
    \end{aligned}
\end{equation}

于是从百分比上来看,有如下结果

\begin{equation}
    \begin{aligned}
        n&=2, &\frac{t_1-t_2}{t_1}&=48.75\%\\
        n&=3, &\frac{t_1-t_2}{t_1}&=65.0\%\\
        n&=5, &\frac{t_1-t_2}{t_1}&=78.0\%\\
        n&=10, &\frac{t_1-t_2}{t_1}&=87.75\%
    \end{aligned}
\end{equation}

analysis

那么理论上能省下的时间呢?比如 face_recognition 及其依赖的包大小共 200 MB 左右,那么 1 次下载的时间为 40 左右,在不使用缓存的情况下,10 次下载则是 400 秒左右,如果启用缓存的话,则可以节省大约 87.75% 的时间,也就是 351 秒上下,并且用的人越多,下载的同样的包越多,这个时间可以省的也越多~

当然,用的人多到一定程度之后就可以集成到基础 Docker 镜像中了~因为基础的 Docker 镜像我们不会频繁更新,此外,有时在 github 上的开源代码也会提供 pip 的安装方式,而这些项目通常也不会第一时间放到基础的 Docker 镜像里。

JupyterHub的例子


这里为了找个干净的环境,于是是在一台 Vultr 的 VPS 上实验的,512MB 内存,20G SSD,于是磁盘的读写 performance 就很一般般啦,但是根据结果来看,加速效果还是很不错的~

首先,克隆jupyterhub/jupyterhub-deploy-dockerBlueCocoa/pip-cache (local branch)

git clone https://github.com/jupyterhub/jupyterhub-deploy-docker
cd jupyterhub-deploy-docker
git clone -b local https://github.com/BlueCocoa/pip-cache

那么要做到上图那样的话,需要在 jupyterhub-deploy-docker 的 docker-compose.yml 中增加如下高亮部分的内容~这样一来spawn出来的每个 jupyter-user 就都是跟 pypicache 是在同一 Docker 网络下了,并且我们为 pypicache 设置了 container name,那么可以直接通过 container name 在该 Docker 网络下直接通过 http://pypicache 访问,无需手动设置 IP 地址。

# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

# JupyterHub docker-compose configuration file
version: "2"

services:
  pypicache:
    image: pypicache
    restart: always
    build:
      context: pip-cache
      dockerfile: Dockerfile
      args:
        PYPI_MIRROR_URL: ${PYPI_MIRROR_URL}
        PYPI_PACKAGE_LOC: ${PYPI_PACKAGE_LOC}
    container_name: pypicache
    volumes:
      - "pypicache:${PYPI_CACHE_CONTAINER}"
  hub-db:
    image: postgres:9.5
    container_name: jupyterhub-db
    restart: always
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      PGDATA: ${DB_VOLUME_CONTAINER}
    env_file:
      - secrets/postgres.env
  volumes:
    - "db:${DB_VOLUME_CONTAINER}"
  hub:
    depends_on:
      - pypicache
      - hub-db
    build:
      context: .
      dockerfile: Dockerfile.jupyterhub
      args:
        JUPYTERHUB_VERSION: ${JUPYTERHUB_VERSION}
      restart: always
      image: jupyterhub
      container_name: jupyterhub
      volumes:
        # Bind Docker socket on the host so we can connect to the daemon from
        # within the container
        - "/var/run/docker.sock:/var/run/docker.sock:rw"
        # Bind Docker volume on host for JupyterHub database and cookie secrets
        - "data:${DATA_VOLUME_CONTAINER}"
      ports:
        - "443:443"
      links:
        - pypicache
        - hub-db
      environment:
        # All containers will join this network
        DOCKER_NETWORK_NAME: ${DOCKER_NETWORK_NAME}
        # JupyterHub will spawn this Notebook image for users
        DOCKER_NOTEBOOK_IMAGE: ${LOCAL_NOTEBOOK_IMAGE}
        # Notebook directory inside user image
        DOCKER_NOTEBOOK_DIR: ${DOCKER_NOTEBOOK_DIR}
        # Using this run command (optional)
        DOCKER_SPAWN_CMD: ${DOCKER_SPAWN_CMD}
        # Postgres db info
        POSTGRES_DB: ${POSTGRES_DB}
        POSTGRES_HOST: hub-db
      env_file:
        - secrets/postgres.env
        - secrets/oauth.env
      command: >
        jupyterhub -f /srv/jupyterhub/jupyterhub_config.py

volumes:
  data:
    external:
      name: ${DATA_VOLUME_HOST}
  db:
    external:
      name: ${DB_VOLUME_HOST}
  pypicache:
    external:
     name: ${PYPI_CACHE_HOST}

networks:
  default:
    external:
      name: ${DOCKER_NETWORK_NAME}

设置需要加速的镜像源


在修改好 docker-compose.yml 之后,还有 4 个环境变量需要在 jupyterhub-deploy-docker 的 .env 文件中增加~

PYPI_CACHE_HOST=pypicache
PYPI_CACHE_CONTAINER=/srv/pypicache/data
PYPI_MIRROR_URL=https://pypi.tuna.tsinghua.edu.cn/simple
PYPI_PACKAGE_LOC=/packages

第一个 PYPI_CACHE_HOST 则是在物理机上的 Docker Volume 的名字,随后会在 docker-compose.yml 中被重新映射/命名为 pypicache 的 volume。这个重映射/命名后的volume则会被挂载到container中的指定位置上。

这个指定位置就是第二个 PYPI_CACHE_CONTAINER。pip-cache默认是将镜像保存到 /srv/pypicache/data 的,因此我们让这个 volume 挂载到这个地方,就可以持久化缓存数据了。

接下来是 PYPI_MIRROR_URL。这里我写的是清华大学的 pip 镜像源,这是一个全量的镜像,而我们现在部署的则只会高速缓存 Docker 用户使用的那些 Python 软件包。这里可以根据偏好写上需要的镜像源的URL,需要写完整~

那么最后就是 PYPI_PACKAGE_LOC。这个则是该镜像源保存软件包的URI路径~比如清华大学的 PyPi 镜像源的话,我们打开 keras 的 index~https://pypi.tuna.tsinghua.edu.cn/simple/keras/

Packages@TUNA PyPi
[email protected] PyPi

可以在上图看到packages的相对路径是在上两层的packages下,也就是https://pypi.tuna.tsinghua.edu.cn/simple/keras/../../packages,将相对路径去掉之后,重新计算路径则是https://pypi.tuna.tsinghua.edu.cn/packages,那么URI也就是/packages了~

再以 USTC 的 PyPi 源作为例子,它的镜像地址是https://mirrors.ustc.edu.cn/pypi/web/simple,那么 PYPI_MIRROR_URL 就写 https://mirrors.ustc.edu.cn/pypi/web/simple

同样的,我们打开 keras 的 index 来看看 packages 都保存在哪儿~

Packages@USTC PyPi
[email protected] PyPi

packages的相对路径是在上两层的packages下,也就是https://mirrors.ustc.edu.cn/pypi/web/keras/../../packages,将相对路径去掉之后,重新计算路径则是https://mirrors.ustc.edu.cn/pypi/web/packages,那么URI也就是/pypi/web/packages,所以 PYPI_PACKAGE_LOC 就写 /pypi/web/packages

对于其他的 PyPi 源也可以用同样的方法来找 packages 的保存的 URI~

最后,在 build 给最终用户实际使用的 Docker 镜像时,只需要配置一下 pip 的默认 index URL 就可以了~

mkdir -p ~/.pip && echo "[global]" > ~/.pip/pip.conf && \
    echo "trusted-host=pypicache" >> ~/.pip/pip.conf && \
    echo "index-url=http://pypicache/simple" >> ~/.pip/pip.conf

Have fun and save time~


这样分发给用户之后,用户不需要做任何其他设置,就能享受 pypicache 的高速缓存服务了~就像下图这样~

Cattery + pypicache
Cattery + pypicache
声明: 本文为0xBBC原创, 转载注明出处喵~

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.