这一篇算是接在上一篇Build a super fast on demand local PyPi mirror的后面吧~
这里会以 docker-compose 的方式为例子,详细写一下~不使用docker-compose的话,则也仅仅需要手动指定 pypicache 与需要这个服务的 container 到同一个 docker 网络中,这样就可以不用去找 pypicache 的 IP 地址,对最终用户透明化,不用增加额外的 pip 安装参数,即可轻松享受本地高速缓存,特别是对于大一点的文件效果更明显~
理论分析加速比
以典型的 JupyterHub(https://github.com/jupyterhub/jupyterhub-deploy-docker) 为例子,先借用它的图说明一下,橙色部分是我们想要增加的 pypicache 服务~
假设某个 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}
那么理论上能省下的时间呢?比如 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-docker与pip-cache (local branch)
git clone https://github.com/jupyterhub/jupyterhub-deploy-docker cd jupyterhub-deploy-docker git clone -b local #/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的相对路径是在上两层的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的相对路径是在上两层的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 的高速缓存服务了~就像下图这样~
发现做博客的人都是有钱人啊,都是吃苹果的