5
votes

Dépendances de Cache Cargo dans un volume Docker

Je construis un programme Rust dans Docker ( rust: 1.33.0 ).

À chaque changement de code temporel, il se recompile (bon), ce qui re-télécharge également toutes les dépendances (mauvais).

J'ai pensé que je pourrais mettre en cache les dépendances en ajoutant VOLUME ["/ usr / local / cargo"] . edit J'ai aussi essayé de déplacer ce répertoire avec CARGO_HOME sans chance.

Je pensais qu'en faire un volume persisterait les dépendances téléchargées, qui apparaissent pour être dans ce répertoire.

Mais cela n'a pas fonctionné, ils sont toujours téléchargés à chaque fois. Pourquoi?


Dockerfile

...
Step 4/6 : COPY Cargo.toml .
---> Using cache
---> 97f180cb6ce2
Step 5/6 : COPY src/ ./src/
---> 835be1ea0541
Step 6/6 : RUN ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]
---> Running in 551299a42907
Updating crates.io index
Downloading crates ...
Downloaded log v0.4.6
Downloaded cfg-if v0.1.6
Compiling cfg-if v0.1.6
Compiling log v0.4.6
Compiling mwe v0.1.0 (/)
Finished dev [unoptimized + debuginfo] target(s) in 17.43s
Removing intermediate container 551299a42907
---> e4626da13204
Successfully built e4626da13204

Construit avec seulement docker build. .

Cargo .toml

[package]
name = "mwe"
version = "0.1.0"
[dependencies]
log = { version = "0.4.6" }

Code: juste bonjour le monde

Sortie de la deuxième exécution après avoir changé main.rs :

FROM rust:1.33.0

VOLUME ["/output", "/usr/local/cargo"]

RUN rustup default nightly-2019-01-29

COPY Cargo.toml .
COPY src/ ./src/

RUN ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]


4 commentaires

Pouvez-vous publier votre Dockerfile et votre commande docker build ?


@JackGore Je l'ai réduit à un exemple minimal et l'ai ajouté


Alors est-ce que je comprends bien que l'exécution de cargo build -Z unstable-options --out-dir / output génère à la fois la chose et télécharge les dépendances? Et que, si le dossier de dépendance est déjà rempli, il ne les téléchargera pas à nouveau?


@ b.enoit.be Oui, cette commande télécharge et construit tout. Cela ne fonctionne pas sans code de projet, donc cela doit être soit ajouté (invalidation du cache), soit falsifié (ce qui fonctionne mais j'aimerais idéalement éviter cette complexité).


7 Réponses :


2
votes

Vous n'avez pas besoin d'utiliser un volume Docker explicite pour mettre en cache vos dépendances. Docker mettra automatiquement en cache les différentes "couches" de votre image. Fondamentalement, chaque commande du Dockerfile correspond à un calque de l'image. Le problème auquel vous êtes confronté est basé sur le fonctionnement de la mise en cache de la couche d'image Docker .

Les règles suivies par Docker pour la mise en cache des calques d'images sont répertoriées dans le documentation :

  • En commençant par une image parente qui est déjà dans le cache, la prochaine l'instruction est comparée à toutes les images enfants dérivées de cela image de base pour voir si l'un d'eux a été construit en utilisant exactement le même instruction. Sinon, le cache est invalidé.

  • Dans la plupart des cas, il suffit de comparer l'instruction dans le Dockerfile avec une des images enfant suffit. Cependant, certaines instructions nécessitent plus d'examen et d'explications.

  • Pour les instructions ADD et COPY, le contenu du (des) fichier (s) dans le l'image sont examinées et une somme de contrôle est calculée pour chaque fichier. le Les heures de dernière modification et de dernier accès des fichiers ne sont pas considéré dans ces sommes de contrôle. Pendant la recherche dans le cache, la somme de contrôle est comparé à la somme de contrôle des images existantes. Si quelque chose a changé dans le (s) fichier (s), comme le contenu et les métadonnées, puis le cache est invalidé.

  • Hormis les commandes ADD et COPY, la vérification du cache ne prend pas en compte les fichiers dans le conteneur pour déterminer une correspondance de cache. Par exemple, lors du traitement d'une commande RUN apt-get -y update, les fichiers mis à jour dans le conteneur n'est pas examiné pour déterminer si un hit de cache existe. Dans dans ce cas, seule la chaîne de commande elle-même est utilisée pour trouver une correspondance.

Une fois le cache invalidé, toutes les commandes Dockerfile suivantes générer de nouvelles images et le cache n'est pas utilisé.

Le problème vient donc du positionnement de la commande COPY src / ./src/ dans le Dockerfile . Chaque fois qu'il y a un changement dans l'un de vos fichiers source, le cache sera invalidé et toutes les commandes suivantes n'utiliseront pas le cache. Par conséquent, votre commande cargo build n'utilisera pas le cache Docker .

Pour résoudre votre problème, ce sera aussi simple que de réorganiser les commandes dans votre Fichier Docker , à ceci:

FROM rust:1.33.0

RUN rustup default nightly-2019-01-29

COPY Cargo.toml .

RUN ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]

COPY src/ ./src/

En procédant de cette façon, vos dépendances ne seront réinstallées qu'en cas de modification de votre Cargo.toml .

J'espère que cela vous aidera.


2 commentaires

Merci d'avoir répondu! J'ai exploré la mise en cache à l'intérieur de l'image, mais comme Cargo n'a pas de moyen de créer uniquement des dépendances (le plugin ne fonctionne pas pour les espaces de travail), cela ne fonctionne pas très bien (la commande de construction de votre exemple échoue pour cette raison). C'est pourquoi j'ai voulu mettre en cache les données dans un volume ...


Peut-être devrais-je simplement adapter votre exemple et ajouter un groupe de RUN mkdir -p src && echo "fn main () {}"> src / main.rs pour chaque membre de l'espace de travail. Je pensais juste que le volume allait être plus propre et plus facile ...



3
votes

Voici un aperçu des possibilités. (Faites défiler vers le bas pour ma réponse originale.)

  • Ajoutez des fichiers Cargo, créez de faux main.rs / lib.rs , puis compilez les dépendances. Ensuite, supprimez la fausse source et ajoutez les vraies. [Met en cache les dépendances, mais plusieurs faux fichiers avec des espaces de travail].
  • Ajoutez des fichiers Cargo, créez de faux main.rs / lib.rs , puis compilez les dépendances. Ensuite, créez une nouvelle couche avec les dépendances et continuez à partir de là. [Similaire à ci-dessus].
  • Montez en externe un volume pour le répertoire du cache. [Met tout en cache, repose sur l'appelant pour passer --mount ].
  • Utilisez RUN --mount = type = cache, target = / the / path cargo build dans le Dockerfile dans les nouvelles versions de Docker. [Cache tout, semble être un bon moyen, mais actuellement trop nouveau pour travailler pour moi. Exécutable ne faisant pas partie de l'image]
  • Exécutez sccache dans un autre conteneur ou sur l'hôte, puis connectez-vous à celui-ci pendant le processus de construction. Voir ce commentaire dans Cargo numéro 2644.
  • Utilisez cargo-build-deps . [Peut fonctionner pour certains, mais ne prend pas en charge les espaces de travail Cargo (en 2019)].
  • Attendez le Cargo issue 2644 . [Il y a une volonté d'ajouter cela à Cargo, mais pas encore de solution concrète].
  • L'utilisation de VOLUME ["/ the / path"] dans le Dockerfile ne fonctionne PAS , c'est par couche (par commande) uniquement.

Remarque: on peut définir CARGO_HOME et ENV CARGO_TARGET_DIR dans le Dockerfile pour contrôler où vont le cache de téléchargement et la sortie compilée.

Notez également: cargo fetch peut au moins mettre en cache le téléchargement des dépendances, mais pas en compilation.

Les espaces de travail Cargo souffrent de devoir ajouter manuellement chaque fichier Cargo, et pour certaines générer une douzaine de faux main.rs / lib.rs . Pour les projets avec un seul fichier Cargo, les solutions fonctionnent mieux.


La mise en cache fonctionne pour mon cas particulier en ajoutant

ENV CARGO_HOME /code/dockerout/cargo
ENV CARGO_TARGET_DIR /code/dockerout/target

/ code est le répertoire dans lequel je monte mon code.

Ceci est monté en externe, pas à partir du Dockerfile.

EDIT1 strong>: Je ne savais pas pourquoi cela fonctionnait, mais @ b.enoit.be et @BMitch ont expliqué que c'était parce que les volumes déclarés dans le Dockerfile ne vivaient que pour une couche (une commande).


3 commentaires

Votre idée originale ne fonctionne pas car le volume dans le docker est effacé entre chaque calque et, plus important encore, lorsque docker crée un conteneur à partir d'une image (lorsque vous docker run ou docker exec il). Donc, votre essai de création d'un volume à partir du dossier de dépendances est en fait ce qui l'empêche d'atteindre ce à quoi vous l'aviez prévu. Les conteneurs sont jetables (vous devriez pouvoir en détruire un et exécuter votre code sur un autre, peut-être même avec une autre version de rust), c'est à cela que sert le volume; avoir une couche de données cohérente lorsque / si vous souhaitez supprimer un conteneur et réutiliser vos données dans un nouveau


Cela a également été expliqué dans la réponse @BMitch btw


@ b.enoit.be Oui, c'était mon malentendu, merci à vous et à BMitch d'avoir clarifié cela. Le volume externe est une solution que je pourrais utiliser, même si je préférerais que le fichier docker soit plus autonome ...



6
votes

Un volume à l'intérieur du Dockerfile est ici contre-productif. Cela monterait un volume anonyme à chaque étape de construction, et à nouveau lorsque vous exécutez le conteneur. Le volume au cours de chaque étape de construction est supprimé une fois cette étape terminée, ce qui signifie que vous devrez télécharger à nouveau tout le contenu pour toute autre étape nécessitant ces dépendances.

Le modèle standard pour cela est de copier la spécification de votre dépendance, d'exécuter la dépendance téléchargez, copiez votre code, puis compilez ou exécutez votre code, en 4 étapes distinctes. Cela permet à docker de mettre en cache les couches de manière efficace. Je ne suis pas familier avec la rouille ou le fret en particulier, mais je pense que cela ressemblerait à:

# syntax=docker/dockerfile:experimental
FROM rust:1.33.0

VOLUME ["/output", "/usr/local/cargo"]

RUN rustup default nightly-2019-01-29

COPY Cargo.toml .
COPY src/ ./src/

RUN --mount=type=cache,target=/root/.cargo \
    ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]

Une autre option consiste à activer certaines fonctionnalités expérimentales avec BuildKit (disponible dans la version 18.09) afin que docker enregistre ces dépendances dans ce qui est similaire à un volume nommé pour votre build. Le répertoire peut être réutilisé entre les versions, mais n'est jamais ajouté à l'image elle-même, ce qui le rend utile pour des choses comme un cache de téléchargement.

FROM rust:1.33.0

RUN rustup default nightly-2019-01-29

COPY Cargo.toml .
RUN cargo fetch # this should download dependencies
COPY src/ ./src/

RUN ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]

Notez que ce qui précède suppose que la cargaison est en cache fichiers dans /root/.cargo. Vous devrez vérifier cela et ajuster le cas échéant. Je n'ai pas non plus mélangé la syntaxe de montage avec une syntaxe d'exécution json pour savoir si cette partie fonctionne. Vous pouvez en savoir plus sur les fonctionnalités expérimentales de BuildKit ici: https: //github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md

Activer BuildKit à partir de 18.09 et les versions plus récentes est aussi simple que l'exportation DOCKER_BUILDKIT = 1 puis exécutez votre compilation à partir de ce shell.


2 commentaires

La première manière problématique car Cargo n'a actuellement pas de moyen de ne construire que des dépendances. Il met également en cache uniquement des couches entières. La deuxième méthode avec BuildKit semble très prometteuse, mais je n'ai pas pu la tester car même avec l'indicateur env et /etc/docker/daemon.json ne peut pas le faire analyser sur 18.09 .3 .


J'ai eu beaucoup de réponses utiles, mais je pense que celle-ci est la plus utile: pour avoir d'abord expliqué pourquoi le volume anonyme ne fonctionnait pas, en suggérant `` récupération de fret '', et surtout pour ce qui semble être la meilleure solution de moitié un an, même si je ne peux pas le faire fonctionner aujourd'hui.



5
votes

Je dirais que la meilleure solution serait de recourir au docker multi- étape build comme indiqué ici et

De cette façon, vous pouvez créer vous-même une première image, ce serait créez à la fois votre application et vos dépendances, puis utilisez, uniquement, dans la deuxième image, le dossier des dépendances de la première

Ceci est inspiré à la fois par votre commentaire sur Réponse de @ Jack Gore et les deux commentaires sur le problème liés ci-dessus.

FROM rust:1.33.0 as dependencies

WORKDIR /usr/src/app

COPY Cargo.toml .

RUN rustup default nightly-2019-01-29 && \
    mkdir -p src && \
    echo "fn main() {}" > src/main.rs && \
    cargo build -Z unstable-options --out-dir /output

FROM rust:1.33.0 as application

# Those are the lines instructing this image to reuse the files 
# from the previous image that was aliased as "dependencies" 
COPY --from=dependencies /usr/src/app/Cargo.toml .
COPY --from=dependencies /usr/local/cargo /usr/local/cargo

COPY src/ src/

VOLUME /output

RUN rustup default nightly-2019-01-29  && \
    cargo build -Z unstable-options --out-dir /output

PS: n'en avoir qu'un run réduira le nombre de couches que vous générez; plus d'infos ici


5 commentaires

C'est aussi une astuce intelligente, merci. Mieux que de supprimer les faux fichiers source. Bien que je préfère quand même éviter le echo "fn main () {}"> src / main.rs (d'autant plus que mon vrai projet est un espace de travail avec plusieurs Cargo.toml)


Vous êtes alors un peu coincé. Si vous utilisez votre réel main.rs dans l'image dépendances , le changer invalidera toutes les couches suivantes en cache, donc la commande RUN reconstruira vos dépendances. Ceci dit: avez-vous testé la commande cargo fetch comme indiqué par @BMitch


Bon point, j'avais raté le truc d'aller chercher, en fait. Il met en cache les téléchargements, mais pas la compilation, ce qui fait gagner du temps mais c'est limité.


Ah alors en effet. Cargo n'a définitivement pas de commande cargo install alla npm, yarn, composer-php, puis


C'est vrai, probablement pas pour trop longtemps cependant :-) github.com/rust-lang/ fret / problèmes / 2644



0
votes

Je suis sûr que vous pouvez ajuster ce code pour une utilisation avec un Dockerfile, mais j'ai écrit un drop dockerized -en remplacement de cargo que vous pouvez enregistrer dans un paquet et exécuter comme ./cargo build --release . Cela fonctionne uniquement pour (la plupart) des développements (utilise rust: latest ), mais n'est pas configuré pour CI ou quoi que ce soit. cargo build , ./cargo build --release , etc

Il utilisera le répertoire de travail actuel et enregistrera le cache dans ./. cargo . (Vous pouvez ignorer le répertoire entier dans votre contrôle de version et il n'a pas besoin d'exister au préalable.)

Créez un fichier nommé cargo dans le dossier de votre projet, exécutez chmod + x ./cargo dessus, et placez-y le code suivant:

#!/bin/bash

# This is a drop-in replacement for `cargo`
# that runs in a Docker container as the current user
# on the latest Rust image
# and saves all generated files to `./cargo/` and `./target/`.
#
# Be sure to make this file executable: `chmod +x ./cargo`
#
# # Examples
#
# - Running app: `./cargo run`
# - Building app: `./cargo build`
# - Building release: `./cargo build --release`
#
# # Installing globally
#
# To run `cargo` from anywhere,
# save this file to `/usr/local/bin`.
# You'll then be able to use `cargo`
# as if you had installed Rust globally.
sudo docker run \
    --rm \
    --user "$(id -u)":"$(id -g)" \
    --mount type=bind,src="$PWD",dst=/usr/src/app \
    --workdir /usr/src/app \
    --env CARGO_HOME=/usr/src/app/.cargo \
    rust:latest \
    cargo "$@"


0 commentaires

1
votes

Avec l'intégration de BuildKit dans le docker, si vous êtes en mesure de bénéficier du backend BuildKit supérieur, il est désormais possible de monter un volume de cache pendant une commande RUN , et à mon humble avis, c'est devenu le meilleur moyen de mettre en cache les constructions de cargaisons. Le volume de cache conserve les données qui y ont été écrites lors des exécutions précédentes.

Pour utiliser BuildKit, vous allez monter deux volumes de cache, un pour le répertoire cargo, qui met en cache les sources de crate externes, et un pour le répertoire cible, qui met en cache tous vos artefacts construits, y compris les caisses externes et les bacs et bibliothèques du projet.

Si votre image de base est rust , $ CARGO_HOME est défini sur / usr / local / cargo, donc votre commande ressemble à ceci:

RUN --mount=type=cache,target=/usr/local/cargo,from=rust,source=/usr/local/cargo \
    --mount=type=cache,target=target \
    cargo build

Si votre image de base est autre chose, vous devrez changer le bit / usr / local / cargo quelle que soit la valeur de $ CARGO_HOME , ou bien ajouter une ligne ENV CARGO_HOME = / usr / local / cargo . En remarque, la chose intelligente serait de définir littéralement target = $ CARGO_HOME et de laisser Docker faire l'expansion, mais il ne semble pas fonctionner correctement - l'expansion se produit, mais le buildkit ne conserve toujours pas le même volume entre les exécutions lorsque vous faites cela.

Autres options pour réaliser la mise en cache de la construction Cargo (y compris sccache et le cargo wharf ) sont décrits dans ce numéro de github .


3 commentaires

> Je choisirais probablement simplement un répertoire hôte séparé pour mapper la cargaison, afin de dissocier l'image de tout ce que je pourrais faire avec Rust sur l'hôte. Je ne suis pas sûr de ce que vous dites ici, mais ces volumes de cache de buildkit ne sont pas liés aux répertoires hôtes. Ils partent du vide.


Oh, tant pis alors. Je pensais que les arguments supplémentaires par rapport à la réponse de BMitch étaient pour l'hôte, mais ils sont pour étape de construction


C'est vrai, et ils sont nécessaires dans ce cas. La raison pour laquelle j'ai publié une réponse distincte ici est que tous les autres articles sur --mount sont hypothétiques, mais n'ont jamais fonctionné en pratique. Chaque argument de ma solution est nécessaire pour réaliser la mise en cache.



0
votes

J'ai trouvé comment faire fonctionner cela également avec les espaces de travail cargo, en utilisant le fork de romac de cargo- build-deps .

Cet exemple contient my_app et deux espaces de travail: utils et db .

< pré> XXX


0 commentaires