หลาย RUN เทียบกับ RUN แบบโซ่เดียวใน Dockerfile อะไรจะดีกว่ากัน?


134

Dockerfile.1ดำเนินการหลายรายการRUN:

FROM busybox
RUN echo This is the A > a
RUN echo This is the B > b
RUN echo This is the C > c

Dockerfile.2 เข้าร่วมกับพวกเขา:

FROM busybox
RUN echo This is the A > a &&\
    echo This is the B > b &&\
    echo This is the C > c

แต่ละRUNเลเยอร์จะสร้างเลเยอร์ขึ้นมาดังนั้นฉันจึงคิดเสมอว่าเลเยอร์ที่น้อยกว่านั้นดีกว่าและDockerfile.2ดีกว่า

เห็นได้ชัดว่าเป็นจริงเมื่อRUNลบบางสิ่งที่เพิ่มโดยก่อนหน้านี้RUN(เช่นyum install nano && yum clean all) แต่ในกรณีที่ทุกRUNอย่างเพิ่มบางสิ่งมีบางประเด็นที่เราต้องพิจารณา:

  1. เลเยอร์ควรจะเพิ่มความแตกต่างเหนือเลเยอร์ก่อนหน้าดังนั้นหากเลเยอร์ในภายหลังไม่ได้ลบสิ่งที่เพิ่มในก่อนหน้านี้ไม่ควรมีข้อได้เปรียบในการประหยัดพื้นที่ดิสก์มากนักระหว่างทั้งสองวิธี ...

  2. เลเยอร์จะถูกดึงแบบขนานจาก Docker Hub ดังนั้นDockerfile.1แม้ว่าอาจจะใหญ่กว่าเล็กน้อย แต่ในทางทฤษฎีจะดาวน์โหลดได้เร็วขึ้น

  3. หากเพิ่มประโยคที่ 4 (เช่นecho This is the D > d) และสร้างขึ้นใหม่ภายในเครื่องDockerfile.1จะสร้างได้เร็วขึ้นด้วยแคช แต่Dockerfile.2จะต้องเรียกใช้คำสั่งทั้ง 4 คำสั่งอีกครั้ง

ดังนั้นคำถาม: วิธีไหนดีกว่าในการทำ Dockerfile?


1
โดยทั่วไปไม่สามารถตอบได้เนื่องจากขึ้นอยู่กับสถานการณ์และการใช้ภาพ (ปรับให้เหมาะสมกับขนาดความเร็วในการดาวน์โหลดหรือความเร็วในการสร้าง)
Henry

คำตอบ:


102

ถ้าเป็นไปได้ฉันมักจะรวมคำสั่งที่สร้างไฟล์ด้วยคำสั่งที่ลบไฟล์เดียวกันเหล่านั้นเป็นRUNบรรทัดเดียว เนื่องจากแต่ละRUNบรรทัดเพิ่มเลเยอร์ให้กับรูปภาพผลลัพธ์จึงเป็นการเปลี่ยนแปลงของระบบไฟล์ที่คุณสามารถดูได้docker diffจากคอนเทนเนอร์ชั่วคราวที่สร้างขึ้น หากคุณลบไฟล์ที่สร้างในเลเยอร์อื่นระบบไฟล์ยูเนี่ยนทั้งหมดจะลงทะเบียนการเปลี่ยนแปลงระบบไฟล์ในเลเยอร์ใหม่ไฟล์จะยังคงอยู่ในเลเยอร์ก่อนหน้านี้และถูกจัดส่งผ่านเครือข่ายและเก็บไว้ในดิสก์ ดังนั้นหากคุณดาวน์โหลดซอร์สโค้ดแตกไฟล์คอมไพล์เป็นไบนารีแล้วลบ tgz และซอร์สไฟล์ในตอนท้ายคุณต้องการให้ทำทั้งหมดนี้ในเลเยอร์เดียวเพื่อลดขนาดภาพ

ต่อไปฉันเองแยกเลเยอร์ตามศักยภาพในการนำมาใช้ซ้ำในรูปภาพอื่น ๆ และการใช้แคชที่คาดหวัง หากฉันมีภาพ 4 ภาพทั้งหมดที่มีอิมเมจพื้นฐานเหมือนกัน (เช่นเดเบียน) ฉันอาจดึงคอลเล็กชันยูทิลิตี้ทั่วไปของรูปภาพเหล่านั้นส่วนใหญ่มาเป็นคำสั่งรันครั้งแรกเพื่อให้รูปภาพอื่น ๆ ได้รับประโยชน์จากการแคช

ลำดับใน Dockerfile มีความสำคัญเมื่อพิจารณาการนำแคชรูปภาพมาใช้ใหม่ ฉันดูส่วนประกอบใด ๆ ที่ไม่ค่อยจะอัปเดตมากนักอาจเป็นได้ก็ต่อเมื่ออัปเดตอิมเมจพื้นฐานและเพิ่มสูงขึ้นใน Dockerfile ในตอนท้ายของ Dockerfile ฉันรวมคำสั่งใด ๆ ที่จะทำงานได้อย่างรวดเร็วและอาจเปลี่ยนแปลงบ่อยเช่นการเพิ่มผู้ใช้ด้วย UID เฉพาะของโฮสต์หรือการสร้างโฟลเดอร์และการเปลี่ยนสิทธิ์ หากคอนเทนเนอร์มีโค้ดที่ตีความ (เช่น JavaScript) ที่กำลังพัฒนาอย่างแข็งขันสิ่งนั้นจะถูกเพิ่มให้ช้าที่สุดเพื่อให้การสร้างใหม่รันเฉพาะการเปลี่ยนแปลงเดียวนั้น

ในแต่ละกลุ่มของการเปลี่ยนแปลงเหล่านี้ฉันรวบรวมให้ดีที่สุดเท่าที่จะทำได้เพื่อย่อเลเยอร์ ดังนั้นหากมีโฟลเดอร์ซอร์สโค้ดที่แตกต่างกัน 4 โฟลเดอร์โฟลเดอร์เหล่านั้นจะถูกวางไว้ในโฟลเดอร์เดียวดังนั้นจึงสามารถเพิ่มได้ด้วยคำสั่งเดียว การติดตั้งแพ็กเกจใด ๆ จากบางสิ่งเช่น apt-get จะรวมอยู่ใน RUN เดียวเมื่อเป็นไปได้เพื่อลดจำนวนค่าใช้จ่ายของตัวจัดการแพ็คเกจให้เหลือน้อยที่สุด (การอัปเดตและการล้างข้อมูล)


อัปเดตสำหรับการสร้างหลายขั้นตอน:

ฉันกังวลน้อยลงมากเกี่ยวกับการลดขนาดภาพในขั้นตอนสุดท้ายของงานสร้างหลายขั้นตอน เมื่อขั้นตอนเหล่านี้ไม่ได้ติดแท็กและส่งไปยังโหนดอื่นคุณสามารถเพิ่มโอกาสในการใช้แคชซ้ำได้มากที่สุดโดยแยกแต่ละคำสั่งออกเป็นRUNบรรทัดแยกกัน

อย่างไรก็ตามนี่ไม่ใช่วิธีแก้ปัญหาที่สมบูรณ์แบบในการบีบเลเยอร์เนื่องจากสิ่งที่คุณคัดลอกระหว่างขั้นตอนทั้งหมดคือไฟล์ไม่ใช่ส่วนที่เหลือของข้อมูลเมตาของรูปภาพเช่นการตั้งค่าตัวแปรสภาพแวดล้อมจุดเข้าใช้งานและคำสั่ง และเมื่อคุณติดตั้งแพ็กเกจในการแจกจ่าย linux ไลบรารีและการอ้างอิงอื่น ๆ อาจกระจัดกระจายไปทั่วระบบไฟล์ทำให้การคัดลอกการอ้างอิงทั้งหมดทำได้ยาก

ด้วยเหตุนี้ฉันจึงใช้การสร้างแบบหลายขั้นตอนแทนการสร้างไบนารีบนเซิร์ฟเวอร์ CI / CD ดังนั้นเซิร์ฟเวอร์ CI / CD ของฉันจะต้องมีเครื่องมือในการรันdocker buildเท่านั้นและไม่มี jdk, nodejs, go และ ติดตั้งเครื่องมือคอมไพล์อื่น ๆ


31

คำตอบอย่างเป็นทางการที่ระบุไว้ในแนวทางปฏิบัติที่ดีที่สุด (ภาพที่เป็นทางการต้องเป็นไปตามนี้)

ลดจำนวนเลเยอร์ให้น้อยที่สุด

คุณต้องหาจุดสมดุลระหว่างความสามารถในการอ่าน (และความสามารถในการบำรุงรักษาในระยะยาว) ของ Dockerfile และลดจำนวนเลเยอร์ที่ใช้ให้น้อยที่สุด ใช้กลยุทธ์และระมัดระวังเกี่ยวกับจำนวนเลเยอร์ที่คุณใช้

ตั้งแต่นักเทียบท่า 1.10 COPY, ADDและRUNงบเพิ่มเลเยอร์ใหม่เพื่อภาพของคุณ ระมัดระวังเมื่อใช้ข้อความเหล่านี้ พยายามที่จะรวมคำสั่งเป็นเดียวRUNคำสั่ง แยกสิ่งนี้เฉพาะในกรณีที่จำเป็นสำหรับการอ่าน

ข้อมูลเพิ่มเติม: https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#/minimize-the-number-of-layers

อัปเดต: หลายขั้นตอนในนักเทียบท่า> 17.05

ด้วยการสร้างแบบหลายขั้นตอนคุณสามารถใช้หลายFROMคำสั่งใน Dockerfile ของคุณ แต่ละFROMคำสั่งเป็นเวทีและสามารถมีภาพฐานของตัวเองได้ ในขั้นตอนสุดท้ายคุณใช้อิมเมจพื้นฐานขั้นต่ำเช่นอัลไพน์คัดลอกสิ่งที่สร้างจากขั้นตอนก่อนหน้าและติดตั้งข้อกำหนดรันไทม์ ผลลัพธ์สุดท้ายของเวทีนี้คือภาพลักษณ์ของคุณ นี่คือจุดที่คุณกังวลเกี่ยวกับเลเยอร์ตามที่อธิบายไว้ก่อนหน้านี้

ตามปกตินักเทียบท่ามีเอกสารที่ยอดเยี่ยมในการสร้างหลายขั้นตอน นี่คือข้อความที่ตัดตอนมาอย่างรวดเร็ว:

ด้วยการสร้างแบบหลายขั้นตอนคุณสามารถใช้คำสั่ง FROM หลายรายการใน Dockerfile ของคุณ คำสั่ง FROM แต่ละคำสั่งสามารถใช้ฐานที่แตกต่างกันและแต่ละคำสั่งจะเริ่มขั้นตอนใหม่ของการสร้าง คุณสามารถคัดลอกอาร์ติแฟกต์จากขั้นตอนหนึ่งไปยังอีกขั้นได้โดยทิ้งทุกสิ่งที่คุณไม่ต้องการไว้ในภาพสุดท้าย

บล็อกโพสต์ดีๆเกี่ยวกับเรื่องนี้สามารถพบได้ที่นี่: https://blog.alexellis.io/mutli-stage-docker-builds/

เพื่อตอบโจทย์ของคุณ:

  1. ใช่เลเยอร์เป็นประเภทที่แตกต่างกัน ฉันไม่คิดว่าจะมีการเพิ่มเลเยอร์หากไม่มีการเปลี่ยนแปลงอย่างแน่นอน ปัญหาคือเมื่อคุณติดตั้ง / ดาวน์โหลดบางอย่างในเลเยอร์ # 2 คุณจะไม่สามารถลบออกในเลเยอร์ # 3 ได้ ดังนั้นเมื่อเขียนบางสิ่งในเลเยอร์แล้วขนาดภาพจะไม่สามารถลดลงได้อีกต่อไปโดยการลบสิ่งนั้นออกไป

  2. แม้ว่าเลเยอร์จะสามารถดึงขนานกันได้ แต่ก็ทำให้เร็วขึ้น แต่แต่ละเลเยอร์ก็เพิ่มขนาดภาพอย่างไม่ต้องสงสัยแม้ว่าจะกำลังลบไฟล์ก็ตาม

  3. ใช่การแคชมีประโยชน์หากคุณกำลังอัปเดตไฟล์นักเทียบท่า แต่มันทำงานในทิศทางเดียว หากคุณมี 10 เลเยอร์และคุณเปลี่ยนเลเยอร์ # 6 คุณจะยังต้องสร้างทุกอย่างใหม่ตั้งแต่เลเยอร์ # 6- # 10 ดังนั้นจึงไม่บ่อยเกินไปที่จะเร่งกระบวนการสร้าง แต่รับประกันได้ว่าจะเพิ่มขนาดรูปภาพของคุณโดยไม่จำเป็น


ขอบคุณ@Mohan ที่เตือนให้ฉันอัปเดตคำตอบนี้


1
สิ่งนี้ล้าสมัยแล้ว - ดูคำตอบด้านล่าง
Mohan

1
@ Mohan ขอบคุณสำหรับการเตือน! ฉันอัปเดตโพสต์เพื่อช่วยเหลือผู้ใช้
Menzo Wijmenga

19

ดูเหมือนว่าคำตอบข้างต้นจะล้าสมัย เอกสารหมายเหตุ:

ก่อน Docker 17.05 และมากกว่านั้นก่อน Docker 1.10 สิ่งสำคัญคือต้องลดจำนวนเลเยอร์ในรูปภาพของคุณ การปรับปรุงต่อไปนี้ช่วยบรรเทาความต้องการนี้:

[ ... ]

Docker 17.05 ขึ้นไปเพิ่มการรองรับสำหรับการสร้างแบบหลายขั้นตอนซึ่งช่วยให้คุณคัดลอกเฉพาะสิ่งประดิษฐ์ที่คุณต้องการลงในภาพสุดท้าย ซึ่งช่วยให้คุณสามารถรวมเครื่องมือและข้อมูลการดีบักในขั้นตอนการสร้างขั้นกลางได้โดยไม่ต้องเพิ่มขนาดของภาพสุดท้าย

https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#minimize-the-number-of-layers

และ

โปรดสังเกตว่าตัวอย่างนี้ยังบีบอัดคำสั่ง RUN สองคำสั่งเข้าด้วยกันโดยใช้ตัวดำเนินการ Bash && เพื่อหลีกเลี่ยงการสร้างเลเยอร์เพิ่มเติมในรูปภาพ นี่เป็นความล้มเหลวได้ง่ายและยากที่จะรักษา

https://docs.docker.com/engine/userguide/eng-image/multistage-build/

แนวทางปฏิบัติที่ดีที่สุดดูเหมือนจะเปลี่ยนไปใช้การสร้างแบบหลายขั้นตอนและทำให้Dockerfileสามารถอ่านได้


ในขณะที่การสร้างแบบหลายขั้นตอนดูเหมือนจะเป็นตัวเลือกที่ดีในการรักษาความสมดุล แต่การแก้ไขคำถามนี้จะเกิดขึ้นเมื่อdocker image build --squashตัวเลือกนั้นอยู่นอกเหนือการทดลอง
Yajo

2
@ ยาโจ - ฉันสงสัยเกี่ยวกับsquashการทดลองที่ผ่านมา มีลูกเล่นมากมายและเหมาะสมก่อนที่จะสร้างแบบหลายขั้นตอนเท่านั้น ด้วยการสร้างแบบหลายขั้นตอนคุณจะต้องปรับแต่งขั้นตอนสุดท้ายให้เหมาะสมซึ่งง่ายมาก
Menzo Wijmenga

1
@Yajo หากต้องการขยายนั้นมีเพียงเลเยอร์ในขั้นตอนสุดท้ายเท่านั้นที่สร้างความแตกต่างให้กับขนาดของภาพสุดท้าย ดังนั้นหากคุณใส่ gubbins ตัวสร้างทั้งหมดของคุณในขั้นตอนก่อนหน้านี้และมีขั้นตอนสุดท้ายเพียงแค่ติดตั้งแพ็คเกจและคัดลอกไฟล์จากขั้นตอนก่อนหน้าทุกอย่างจะทำงานได้อย่างสวยงามและไม่จำเป็นต้องใช้สควอช
Mohan

3

ขึ้นอยู่กับจำนวนที่คุณรวมไว้ในเลเยอร์รูปภาพของคุณ

ประเด็นสำคัญคือการแชร์เลเยอร์ให้มากที่สุด:

ตัวอย่างที่ไม่ดี:

Dockerfile.1

RUN yum install big-package && yum install package1

Dockerfile.2

RUN yum install big-package && yum install package2

ตัวอย่างที่ดี:

Dockerfile.1

RUN yum install big-package
RUN yum install package1

Dockerfile.2

RUN yum install big-package
RUN yum install package2

คำแนะนำอีกประการหนึ่งคือการลบไม่ได้มีประโยชน์เฉพาะในกรณีที่เกิดขึ้นในเลเยอร์เดียวกับการดำเนินการเพิ่ม / ติดตั้ง


2 สิ่งนี้จะแบ่งปันRUN yum install big-packageจากแคชจริงๆหรือไม่?
Yajo

ใช่พวกเขาจะแชร์เลเยอร์เดียวกันหากเริ่มจากฐานเดียวกัน
Ondra Žižka
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.