Blog

23 Mar
Tối ưu build với Cache trong Docker
March 23, 2023

Tối ưu build với Cache trong Docker

   
Hi mọi người, Tứn văn vở đã xuất hiện và muốn chia sẻ một vài cái vui vui khi vọc lại docker.
Cơ duyên:
Lâu rồi hông sử dụng docker vì tự tin máy mình ok (dùng mac mà lo chi hiệu suất) nêu cái gì mình cũng cài local chơi hết cho tiện, đột nhiên dạo gần đây lap mình nó có dấu hiệu ngộp thở lúc này mình nhớ ra có bài seminar về docker khá hay của Qui & Thơm nên mình cấp máy thở cho nó (docker), cứ nghĩ dùng docker sẽ tốt hơn cho hiệu suất NHƯNG nó cũng vậy khó thở hơn =)), vấn đề này xảy ra giống với câu hỏi của Qui và Thơm trong bài chia sẻ về optimize docker nên mình cố gắng tìm câu trả lời chính xác cho vấn đề này, sau một thời gian mò mẩm thì mình cũng có được câu trả lời tạm tạm cho câu hỏi này.
  • Câu hỏi:
Vì sao trong dockerfile phải dùng 2 lần lệnh COPY :

+ COPY package*.json ./

+ COPY . . Trong khi "COPY . . " nó đã copy tất cả vào container?
  • Câu trả lời:

Mình có 1 Dockerfile như dưới:

FROM node:14.16.0-alpine3.13 
RUN addgroup app && adduser -S -G app app 
USER app WORKDIR /app COPY . . 
RUN npm install EXPOSE 3000 CMD ["npm", "start"]

Đầu tiên chúng ta hiểu được mỗi lệnh trong file Dockerfile là 1 instruction, thì trong Dockerfile của mình có 8 instructions. Khi tiến hành lệnh build (docker build -t [App_Name] [Path to Dockerfile]) thì docker sẽ check từng instruction xem thử có thay đổi với lần build trước đó hay không (ở lần đầu tiên docker sẽ tiến hành build tất cả và lưu Cache), ví dụ:
với lần build đầu tiên chúng ta có:

> docker build --no-cache --pull -t react-app . 
[+] Building 60.9s (10/10) FINISHED 
=> CACHED [1/5] FROM docker.io/library/node:14.16.0-alpine3.13@sha256:2c51dc462a02f15621e7486774d36d048a27225d581374b00 0.0s => [2/5] RUN addgroup app && adduser -S -G app app
0.4s 
=> [3/5] WORKDIR /app
0.0s => [4/5] COPY . .
0.1s => [5/5] RUN npm install
53.1s => exporting to image
5.5s => => exporting layers
5.5s => => writing image sha256:8f65c5d42bd9b6b4b692ce90a9f5896aa317658e870517696de7dcc728dce199
0.0s => => naming to docker.io/library/react-app
0.0s

Mọi người để ý lần build đầu tiên docker sẽ chạy lênh RUN npm install trong vòng ~1 phút, ở lần build này docker sẽ cache tất cả  instructions để tối ưu hoá cho lần build tiếp theo, việc cache và reuse sẽ có một chút khác cho các lần build sau:

1. Đối với các commands FROM, RUN, USER, WORKDIR,… Khi build lại thì docker sẽ check xem thử các commands này thay đổi hay không, nếu thay đổi nó sẽ rebuild lại instructions đó chứ không reuse từ cache, ngược lại nếu không thay đổi thì sẽ reuse lại từ cache

a) Ví dụ: ở lần build thứ 2 mình có thay đổi ở file Dockerfile dòng 2, chuyển từ app sang app2 (RUN addgroup app2 && adduser -S -G app2 app) thì lúc này docker sẽ rebuild lại chứ không reuse từ cache.

2. Đối với instruction COPY sẽ có một chút khác biệt, với instruction này docker không thể biết được instruction này có change hay không, nên docker bắt buộc phải kiểm tra content của từng file trong source code của chúng ta, vì vậy nếu có 1 chút thay đổi nào đó trong source code thì docker sẽ rebuild lại, lúc này chúng ta có 2 trường hợp cần lưu ý:

a) Thử không thay đổi gì cả ở source code và chạy lệnh build lại, như chúng ta biết thì vì đã cache lần đầu nên docker không rebuild lại gì cả, tất cả đều reuse từ cache, CACHED [5/5] RUN npm install 0.0s quá tuyệt zời kaka, mọi thử lúc nào cũng vậy thì cuộc đời luôn luôn đẹp sao =)))

> docker build -t react-app .
[+] Building 3.3s (11/11) FINISHED 
=> [1/5] FROM docker.io/library/node:14.16.0-
alpine3.13@sha256:2c51dc462a02f15621e7486774d36d048a27225d581374b002b8477a 0.0s 
=> CACHED [2/5] RUN addgroup app && adduser -S -G app app 
0.0s 
=> CACHED [3/5] WORKDIR /app 
0.0s 
=> CACHED [4/5] COPY . . 
0.0s 
=> CACHED [5/5] RUN npm install 
0.0s 
=> exporting to image 
0.0s 
=> => exporting layers 
0.0s 
=> => writing image sha256:8f65c5d42bd9b6b4b692ce90a9f5896aa317658e870517696de7dcc728dce199 
0.0s 
=> => naming to docker.io/library/react-app
0.0s

b) Tuy nhiên mọi người cũng biết cuộc đời đa số sẽ đi ngược lại với những gì mình suy nghĩ (đúng nhận sai cải =)))), nên ở trường hợp này mình sẽ thay đổi một chút code ở file README.md , file này chắc mn đều biết nhỉ (File này để mô tả dự án, cũng như hướng dẫn setup source code), cụ thể mình thêm Hello docker vào content của file này, lúc này sẽ xảy ra vấn đề của Qui và Thơm nêu ra trong bài seminar, vì chúng ta thay đổi source code nên docker sẽ rebuild lại instruction COPY tuy nhiên nếu vậy thì không có gì xảy ra (chỉ rebuild lại mỗi thèn COPY thôi mà lo quái gì đâu =)))) NHƯNG vâng lại NHƯNG  trong docker nếu 1 instruction rebuild thì tất cả instruction sau đó cũng sẽ rebuild lại =>>>> đauuuuuu ở đây  này , đau vì chúng ta biết sau thèn COPY  instruction RUNnpm install, lúc này docker sẽ install lại tất cả packages lần nữa, mặc dù chúng ta không thay đổi instruction RUN npm install và cả file package.json hay package-lock.json cũng không thay đổi. Bên dưới là lệnh sau khi mình chạy run build lại, vâng lại ~1 phút cho lệnh RUN npm install  (Đan mạch =)))

> docker build -t react-app . 
[+] Building 71.1s (10/10) FINISHED 
=> [1/5] FROM docker.io/library/node:14.16.0-
alpine3.13@sha256:2c51dc462a02f15621e7486774d36d048a27225d581374b002b8477a 0.0s 
=> CACHED [2/5] RUN addgroup app && adduser -S -G app app 
0.0s 
=> CACHED [3/5] WORKDIR /app 
0.0s 
=> [4/5] COPY . . 0.1s => [5/5] RUN npm install 
63.8s 
=> exporting to image 
5.6s 
=> => exporting layers 
5.5s 
=> => writing image sha256:2b0261b5580e4ee7b57e81c1ef449b5d57f36e44746191835f1bb9de4b735ab7 
0.0s 
=> => naming to docker.io/library/react-app 
0.0s

Vì vấn đề này nên mình nhớ ra solution trong bài seminar và cũng nghĩ nên hạn chế việc rebuild lại lệnh npm install bằng cách tách file package*.json  npm install ra khỏi lệnh COPY . . và cho nó chạy trước, nếu vậy khi chúng ta thay đổi gì đó trong source code (không phải là file package*.json) thì docker không cần install lại các packages một lần nữa. mình sẽ đổi content của Dockerfile như bên dưới:
FROM node:14.16.0-alpine3.13 
RUN addgroup app && adduser -S -G app app 
USER app WORKDIR /app COPY package*.json . 
RUN npm install 
COPY . . 
EXPOSE 3000 
CMD ["npm", "start"]
Sau đó mình tiến hành build one more time: docker build -t react-app . Và kết quả như bên dưới:
=> [1/6] FROM docker.io/library/node:14.16.0-alpine3.13@sha256:2c51dc462a02f15621e748677  0.0s> 
=> CACHED [2/6] RUN addgroup app && adduser -S -G app app 0.0s => CACHED [3/6] WORKDIR /app 0.0s> 
=> [4/6] COPY package*.json . 0.0s 
=> [5/6] RUN npm install 66.3s> 
=> [6/6] COPY . . 0.1s 
=> exporting to image 5.8s 
=> => exporting layers 5.8s 
=> => writing image sha256:71f9321fb1d13abcf8fd5d46f8c80d8485e4325eea8a7ae4eb33c97c04d18 0.0s 
=> => naming to docker.io/library/react-app 0.0s
ỦA ALOOOOOOO  RUN npm install 66.3s  (đan mạch lần 2) vậy là những suy đoán của mình sai , với bản tính lì lợm không chịu bỏ cuộc của một coder mình lại thay đổi content của README.md  run docker build lại một lần nữa, thìiiiiiiii AHAAA nó chạy goài các bạn ạ, như một phép màu lệnh npm install đã đc cache  reuse lại RUN npm install 0.0s. Từ đây có thể suy ra vì chúng ta thay đổi Dockerfile (thêm mới COPY package và npm install) nên docker sẽ build lại 2 lệnh này.
> docker build -t react-app .
[+] Building 4.0s (12/12) FINISHED 
=> [1/6] FROM docker.io/library/node:14.16.0-alpine3.13@sha256:2c51dc462a02f15621e7486774d36d048a2722 0.0s 
=> [internal] load build context                                                                      0.0s 
=> => transferring context: 17.18kB                                                                   0.0s 
=> CACHED [2/6] RUN addgroup app && adduser -S -G app app                                             0.0s 
=> CACHED [3/6] WORKDIR /app                                                                          0.0s 
=> CACHED [4/6] COPY package*.json .                                                                  0.0s 
=> CACHED [5/6] RUN npm install                                                                       0.0s 
=> [6/6] COPY . .                                                                                     0.1s 
=> exporting to image                                                                                 0.0s 
=> => exporting layers                                                                                0.0s 
=> => writing image sha256:0849fe2f1490dab751e3257b814acde385abacdbffffb450e35549a0f06f6618           0.0s 
=> => naming to docker.io/library/react-app                                                           0.0s
  • Bài học rút ra: Cuối cùng mình cũng hiểu hơn một chút về dọcker (cách cache, cách reuse), hy vọng một chút chia sẻ của mình về cầu trả lời tối ưu lệnh COPY trong docker sẽ giúp mọi người hiểu hơn về docker và cách hoạt động của nó. Sau đây mình liệt kê một vào case study cho mọi người tham khảo:
    • Những instructions không thay đổi thường xuyên nên ở trên cùng, và những instructions nào hay thay đổi thì nằm ở dưới,
    • Nên tách các lệnh install ra khỏi câu lệnh COPY. 
    • Khi thêm mới instructions vào Dockerfile thì nó sẽ build lại lệnh đó.

    Cảm ơn mọi người đã bỏ chút thời gian ra đọc bài viết nhảm nhảm nhảm của mình trong một ngày cuối tuần không có người yêu bên cạnh 
    Thank you.