Docker Image สร้างอย่างไรให้มีประสิทธิภาพ?

Docker Image สร้างอย่างไรให้มีประสิทธิภาพ?

การสร้าง Docker image ให้มีประสิทธิภาพเป็นส่วนสำคัญอย่างหนึ่งที่ไม่ควรมองข้ามทั้งในขั้นตอนของ development และ deployment ของระบบ ให้มีประสิทธิภาพหมายความว่าอย่างไร? ในทีนี้หมายถึงการทำให้ขนาดของ image เล็ก เคลื่อนย้ายไปมาได้ง่าย ง่ายต่อการจัดการ สามารถสร้าง (build) ได้เร็ว ตัว image ที่ได้มามีความสมบูรณ์ service ที่อยู่ข้างในสามารถรันได้อย่างไม่มีปัญหา ฯลฯ

เลือกใช้ Docker image และ base image ให้เหมาะสมกับงาน

การเลือกทำ image ไม่แนะนำว่าให้ติดตั้งทุก service ที่เราต้องการภายใน image เดียว ถึงแม้ว่าจะง่ายก็ตาม แต่สิ่งที่ตามมาคือ image เราจะมีความซับซ้อนมากขึ้น และยากต่อการจัดการในภายหลัง ถ้าอยากจะติดตั้ง WordPress ก็ใช้ image ของ Nginx, PHP, MySQL แยกกันเป็นต้น ซึ่งตรงกับ best practices ข้อหนึ่งที่ว่า Run only one process per container

สำหรับ base image เวลาเราอยากจะทำ service สัก service หนึ่งขึ้นมาแล้วใช้ Ubuntu image เป็น base ก็อาจจะดูเกินความจำเป็นไปหน่อย ถ้าทำ service ที่ใช้ Django เราก็ควรใช้ base image ของ Python จะเหมาะสมกว่า ถ้า service ไหน เราใช้ Nginx ก็ใช้ base image ของ Nginx ไปเลยดีกว่า แต่ถ้ามีความจำเป็นต้องทำ custom image ก็แนะนำให้เลือกใช้ image ที่มีขนาดเล็กๆ แทน เช่น Alpine แทน

ทำ Dockerfile ให้เรียบง่าย

ให้เรียบง่ายประมาณว่าจัดกลุ่มของคำสั่งติดตั้งต่างๆ ให้เป็นระเบียบ และทำให้มี layer น้อยๆ เข้าไว้ ยกตัวอย่างเช่น คำสั่ง RUN ก็นับเป็น 1 layer ดังนั้นคำสั่งติดตั้งคำสั่งไหนที่ควรเอามารวมกัน ก็ให้เราเอามารวมกันเป็นคำสั่งเดียว เช่นถ้าเราอยากติดตั้ง Python ก็ให้ใช้คำสั่งประมาณนี้

RUN apt-get install -y build-essential python2.7 python2.7-dev python-setuptools python-software-properties python-pip

อย่าใช้แบบนี้

RUN apt-get install -y build-essential
RUN apt-get install -y python2.7
RUN apt-get install -y python2.7-dev
RUN apt-get install -y python-setuptools
RUN apt-get install -y python-software-properties
RUN apt-get install -y python-pip

ข้อควรระวัง! เราก็ไม่ควรจะยุบรวมทุกคำสั่งเป็นคำสั่งเดียวเพื่อที่พยายามจะลดจำนวน layer นะ เพราะจะทำให้ Dockerfile เราอ่านยากเกินไป

ใช้ .dockerignore

ถ้าใครที่ใช้ Git มา คงจะคุ้นๆ กับไฟล์ .gitignore กัน สำหรับ Docker เค้ามีไฟล์ .dockerignore เช่นกัน ปกติเวลาที่เราจะเอาไฟล์เข้าไปใส่ไว้ใน image ด้วยความง่ายเราจะสั่งแบบนี้

ADD . /app/

ไฟล์ทั้งหมดจะเข้าไปอยู่ใน image ที่โฟลเดอร์ /app/ ทันที ทั้งไฟล์ Dockerfile และโฟลเดอร์ที่ซ่อนไว้อย่าง .git และอะไรอื่นๆ อีกมากมาย ซึ่งไฟล์เหล่านี้เป็นสิ่งที่ไม่จำเป็นเลย เราควรจะเอาไปใส่ไว้ใน .dockerignore ซะ ลองไปดูวิธีเขียนได้ที่ .dockerignore file ครับ (ใช้ pattern เดียวกับ .gitignore)

เข้าใจการ cache ช่วยทำให้ build ได้เร็วขึ้น

อยากให้ลองไปอ่าน Build cache กันดูด้วยครับ ยกตัวอย่างโปรเจค Django โปรเจคหนึ่ง เราจะต้องมีการติดตั้ง Python packages ต่างๆ ทีนี้ส่วนใหญ่ที่ทำกันน่าจะเป็นแบบนี้

ADD . /app/
RUN pip install -r requirements.txt

ไฟล์ requirements.txt เป็นไฟล์ที่รวม Python packages ต่างๆ ที่เราจะติดตั้งสำหรับโปรเจคนั้นๆ ซึ่งการทำแบบนี้คำสั่ง ADD จะมาตรวจสอบว่ามีไฟล์อะไรเปลี่ยนแปลงบ้างก่อน ถ้ามีการเปลี่ยนแปลง Docker จะสลาย cache ครับ (โดยปกติแล้วเราแก้ไขโค้ดเราอยู่เกือบตลอดเวลา ดังนั้นคำสั่งนี้จึงสลาย cache อยู่บ่อยครั้ง) ส่งผลทำให้คำสั่งอะไรก็ตามต่อจาก ADD นั้นไม่มี cache อยู่ Python packages ที่เราต้องการจะโดนติดตั้งใหม่ (เกือบ) ทุกรอบที่มีการ build ดังนั้นแนะนำให้ทำประมาณนี้ครับ

RUN pip install Django==1.10 django-taggit==0.21.1 requests==2.11.1
ADD . /app/
RUN pip install -r requirements.txt

คือเราเอาสิ่งที่อยู่ใน requirements.txt ออกมาอยู่ที่ Dockerfile แทน ทำให้คำสั่ง RUN จะสร้าง cache ขึ้นมา เวลาที่มาถึงคำสั่งติดตั้งที่ต้องไปอ่านไฟล์ requirements.txt จะทำงานเร็วขึ้นมากครับ เพราะไม่ต้องติดตั้งใหม่ ลองดูผลการรันตามรูปที่ 1 ประกอบครับ

Docker Caching when Build

รูปที่ 1: ผลการรันหลังจากที่เอา Python packages ออกมาไว้ข้างนอกไฟล์ requirements.txt

จากรูปจะเห็นได้ว่า Step 6 ไม่ได้มีการไปโหลด package จาก Internet ใหม่ครับ เพราะว่า Step 4 ได้ติดตั้ง package ตามที่เราต้องการไปแล้ว 😀

ข้อควรระวัง! ถ้าจำนวน packages ที่เราจะติดตั้งยิ่งไม่นิ่ง การ build ในแต่ละรอบก็จะใช้เวลาเหมือนกันนะครับ แต่ถ้านิ่งเมื่อไหร่แล้ว จะ build เร็วขึ้นเยอะ

ลบไฟล์ที่โหลดมาให้ถูก layer

ตรงนี้ต้องอาศัยความรู้เกี่ยวกับ layer เล็กน้อย ลองดูคำสั่งติดตั้ง Elasticsearch ตามนี้

RUN curl -L -O https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/2.4.0/elasticsearch-2.4.0.tar.gz
RUN tar -xvf elasticsearch-2.4.0.tar.gz
RUN rm elasticsearch-2.4.0.tar.gz

ชุดคำสั่งด้านบนนี้จะทำให้มีไฟล์ elasticsearch-2.4.0.tar.gz ค้างอยู่ใน layer ก่อนหน้าที่เราจะสั่งลบไฟล์ทิ้ง เราควรจะยุบคำสั่งให้เหลือแบบนี้แทน

RUN curl -L -O https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/tar/elasticsearch/2.4.0/elasticsearch-2.4.0.tar.gz && tar -xvf elasticsearch-2.4.0.tar.gz && rm elasticsearch-2.4.0.tar.gz

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

ปิด service ให้หมดตอนท้ายของแต่ละ Docker build

ถ้าเราไม่ปิด service ก่อนในตอนท้ายอาจจะทำให้ service เหล่านั้นโดนปิดแบบไม่ค่อยดีนัก อาจจะส่งผลทำให้ service ตอนที่เราเอา image ไปใช้รันไม่ขึ้น ยกตัวอย่างการปิด service อย่างเช่น่ ถ้าเราติดตั้ง Nginx เราก็ปิด service ตอนท้ายตามนี้

RUN service nginx stop || true

กรณีที่ไม่สามารถรัน service ได้ ผมไม่ค่อยเจอเท่าไหร่ แต่ก็ทำไว้ก็ไม่เสียหายครับ

พยายามลบสิ่งที่ไม่จำเป็นทิ้ง

เช่น /usr/share/doc หรือให้ apt-get ลบพวก packages ต่างๆ ที่ไม่จำเป็นทิ้ง ตามนี้

RUN rm -rf /usr/share/doc
RUN apt-get clean && apt-get autoclean

จบ.. สุดท้ายถ้าใครมีข้อแนะนำเพิ่มเติมอะไร คอมเม้นต์กันมาเลยครับ 😉

ขอบคุณเนื้อหาและรูปจาก


Kan Ouivirach

Kan Ouivirach

Lead Software Architect

Being interested in Agile software development, I joined an Agile team at Pronto Tools as a Research & Development Architect (as Lead Software Architect now). I am an enthusiastic architect who not only has a scientific mindset, but also a practical approach to software solutions.