Dockerize project demo microservices của Google
Hello các bạn lại là mình đây 👋👋
Lâu rồi chưa được viết bài thêm nay lại thấy ngứa tay rồi, không biết dạo này công việc của các bạn như thế nào rồi nhỉ? 😚
Hôm nay ta lại tiếp tục với series Docker với một bài deploy full project e-commerce theo kiến trúc microservice, lấy từ demo của Google nhé. 🚀🚀
Câu chuyện là...
Câu chuyện là mình có được một người em chỉ cho xem repo này: https://github.com/GoogleCloudPlatform/microservices-demo
Ở đó ta có demo về một project e-commerce tới từ Google, với đầy đủ các service, mỗi service lại code bằng ngôn ngữ khác nhau, chạy độc lập, mình có xem tất cả những gì có trong repo đó và thấy là họ làm rất chỉn chu cho mục đích demo để ta có thể hiểu được một kiến trúc microservices khi deploy thật trông nó sẽ như thế nào.
Nhận thấy đây là một mảnh ghép còn thiếu cho series của mình, vậy là lại có ý tưởng viết bài mới liền 😝
Những thứ ta sẽ làm
Ở bài ngày hôm nay ta sẽ clone project của Google về: https://github.com/GoogleCloudPlatform/microservices-demo
Sau đó sẽ build từng service và cuối cùng là chạy project lên xem tổng thể như thế nào nhé. Thực tế bài này sẽ không dài, vì project của họ đã cho ta tất cả các file Dockerfile sẵn luôn rồi 😂
Mục đích của mình ở bài này như sau:
- showcase dockerize/deploy microservices với Docker như thật sẽ thế nào
- Flow build và run các services để giảm thiểu lỗi
- xử lý các vấn đề liên quan trong quá trình build
Tổng quan
Ở đây ta có sơ đồ kiến trúc như sau: (cái này người ta đã cho sẵn ở repo của họ luôn)
Ở trên ta có tất thảy 11 services chính, cộng cả redis thì là 12, service nào call vào đâu đều được thể hiện rõ trên diagram
Thường khi ta làm thực tế, thì việc vẽ ra được sơ đồ như này rất quan trọng, lí do là để ta có thể hiểu được sự phụ thuộc của các service với nhau, phân chia network/VPC như nào cho phù hợp, hiểu được request flow thì khi gặp lỗi sẽ dễ hơn khi debug
Không vẽ ra được thì ta cũng phải đảm bảo là viết doc cho nó ở đâu đó nha 😎
Dựa vào diagram ta có, lát nữa ta sẽ dockerize làm 2 lần:
- lần 1: các service không cần gọi vào service khác. Ví dụ:
ad, productcatalog, cart
(cái này gọi vào redis nhưng về mặt business ta cũng coi như nó không gọi vào serviec khác),... - lần 2: các service phụ thuộc vào service khác, ví dụ:
frontend, checkout, loadgenerator,...
Bên dưới ta có chi tiết từng service như sau:
Service | Ngôn ngữ | Mô tả |
---|---|---|
frontend | Go | Là 1 HTTP server. Không yêu cầu đăng ký/đăng nhập và tự động tạo session ID cho tất cả người dùng. |
cartservice | C# | Lưu trữ các sản phẩm trong giỏ hàng của người dùng trên Redis và truy xuất sau này. |
productcatalogservice | Go | Cung cấp danh sách sản phẩm từ file JSON, cho phép tìm kiếm và lấy thông tin chi tiết từng sản phẩm. |
currencyservice | Node.js | Chuyển đổi số tiền giữa các loại tiền tệ. Sử dụng tỷ giá thực tế lấy từ Ngân hàng Trung ương Châu Âu (ECB). Đây là dịch vụ có QPS (Query per second) cao nhất. |
paymentservice | Node.js | Thanh toán với thông tin thẻ tín dụng giả (mock) và số tiền nhất định, trả về ID giao dịch. |
shippingservice | Go | Dự toán chi phí vận chuyển dựa trên giỏ hàng. Gửi hàng đến địa chỉ được cung cấp (mock). |
emailservice | Python | Gửi email xác nhận đơn hàng cho người dùng (mock). |
checkoutservice | Go | Lấy giỏ hàng người dùng, chuẩn bị đơn hàng, điều phối thanh toán, vận chuyển và thông báo qua email. |
recommendationservice | Python | Đề xuất các sản phẩm khác dựa trên các mặt hàng trong giỏ hàng. |
adservice | Java | Cung cấp quảng cáo văn bản dựa trên các từ ngữ ngữ cảnh được cung cấp. |
loadgenerator | Python/Locust | Liên tục gửi các yêu cầu mô phỏng hành vi mua sắm thực tế của người dùng đến frontend. |
Ta bắt đầu nhé 🚀🚀
Setup
Bài này ta sẽ làm ở local với Docker, nên yêu cầu là máy các bạn đã cài Docker nha
Tiếp theo ta clone project từ repo của Google Cloud về nhé: https://github.com/GoogleCloudPlatform/microservices-demo
git clone https://github.com/GoogleCloudPlatform/microservices-demo.git
Sau khi clone về ta có project như sau:
Ở đây ta thấy họ đã chuẩn bị sẵn cấu hình cho nhiều các deploy khác nhau: Kubernetes với manifest hoặc kustomize, Helm chart, terraform,...
Bài này ta làm ở local nên ta sẽ build trực tiếp từ src
, ở trong đó thì họ đã tổ chức các service theo từng folder rõ ràng cho ta:
Ta bỏ qua service
shoppingassistantservice
, hiện tại (07/2024) họ chưa có doc cho việc sử dụng service này như nào
Và ở trong mỗi service họ đã có luôn cả Dockerfile cho ta luôn:
Quá toẹt vời, chứ ngồi mà đọc project xong tự viết Dockerfile cho hơn chục services chắc chớt 😜😜
Dockerize lần một
Ở lần đầu ta sẽ dockerize tất cả các service mà không cần gọi tới service khác, bao gồm 7 service: ad, productcatalog, cart, shipping, currency, payment, email
Ta bắt đầu với adservice
trước nhé, ta mở terminal ở folder src/adservice
:
docker build -t adservice .
Chờ một tí thấy như sau là oke rồi:
Tiếp theo tới productcatalogservice
, tương tự vẫn mở terminal ở folder src/productcatalogservice
và chạy:
docker build -t productcatalogservice .
Tiếp theo tới cartservice
, vì Dockerfile của service này nằm trong 1 folder src
nữa:
Nên ta phải mở terminal ở đúng folder nhé src/cartservice/src
:
docker build -t cartservice .
Tiếp theo tới các service còn lại, cái này mình làm nhanh, các bạn nhớ phải mở terminal ở đúng folder rồi mới chạy build nhé:
# src/shippingservice
docker build -t shippingservice .
# src/currencyservice
docker build -t currencyservice .
# src/paymentservice
docker build -t paymentservice .
# src/emailservice
docker build -t emailservice .
Vì tất cả các service đã được người ta viết sẵn Dockerfile hết rồi nên ta chỉ việc build lại thôi 😁
Sau khi build xong tất cả thì ta chạy lên xem có lỗi lầm gì không nhé.
Ở root folder project ta tạo file docker-compose.yml
với nội dung như sau:
services:
adservice:
image: adservice
productcatalogservice:
image: productcatalogservice
cartservice:
image: cartservice
environment:
- REDIS_ADDR=redis-cart:6379
depends_on:
- redis-cart
redis-cart:
image: redis:6-alpine
shippingservice:
image: shippingservice
currencyservice:
image: currencyservice
paymentservice:
image: paymentservice
emailservice:
image: emailservice
Ở trên ta sẽ chạy tất cả các service mà ta vừa build lên, bao gồm cả redis, chú ý rằng dựa vào diagram:
Ta thấy rằng cuối cùng chỉ cần frontend
là cần map port ra ngoài vì user sẽ gọi vào từ đó, còn lại tất cả các service khác sẽ giao tiếp container->container bên trogn Docker luôn
Ở trên với service cartservice
ta cần set biến môi trường REDIS_ADDR=redis-cart:6379
nó là địa chỉ của redis, mình biết biến này là vì xem ở file manifest K8S kubernetes-manifests/cartservice.yml
😆
Giờ ta chạy lên xem thế nào nhé:
docker compose up -d
Sau đó ta check xem các service đã Up
chưa:
docker compose ps
Thì thấy kết quả như sau:
Ủa ở đây sao lại có mỗi 5 service Up
nhỉ???? 🙄🙄
Trong khi ở docker-compose.yml
ta có tất cả 8 services (kể cả redis), tức là ở đây ta thiếu 3 services: paymentservice, currencyservice, cartservice
Giờ ta sẽ check logs các service bị lỗi xem như nào nhé:
docker compose logs paymentservice
Lỗi: server must be bound in order to start
Lạ nhỉ??? 🤨🤨
Check code của paymentservice/server.js
thì lỗi trả về từ GRPC, lọ mọ search Google thì thấy được issue này: https://github.com/grpc/grpc-node/issues/1404, có vẻ là bị lỗi IP port gì đó
Mở file manifest Kubernetes để hóng xem khi deploy với K8S thì họ có làm gì thêm không:
# kubernetes-manifests/paymentservice.yaml
...
env:
- name: PORT
value: "50051"
- name: DISABLE_PROFILER
value: "1"
Ầu, thì ra họ phải set PORT cho paymentservice
, bên cạnh đó họ cũng có DISABLE_PROFILER
luôn, vậy thì ta cứ follow và thêm vào các biến môi trường giống vậy nhé. Ta sửa lại docker-compose.yml
như sau:
services:
adservice:
image: adservice
environment:
- PORT=9555
productcatalogservice:
image: productcatalogservice
environment:
- PORT=3550
- DISABLE_PROFILER=1
cartservice:
image: cartservice
environment:
- REDIS_ADDR=redis-cart:6379
depends_on:
- redis-cart
redis-cart:
image: redis:6-alpine
shippingservice:
image: shippingservice
environment:
- PORT=50051
- DISABLE_PROFILER=1
currencyservice:
image: currencyservice
environment:
- PORT=7000
- DISABLE_PROFILER=1
paymentservice:
image: paymentservice
environment:
- PORT=50051
- DISABLE_PROFILER=1
emailservice:
image: emailservice
environment:
- PORT=8080
- DISABLE_PROFILER=1
Ở trên ta cần check lại từng service một xem chúng cần biến môi trường gì, bên Kubernetes có biến nào ta thêm hết vào nhé 😁
Sau đó ta restart lại project:
docker compose down
docker compose up -d
Sau đó ta lại tiếp tục kiểm tra xem các service đã Up
hết chưa:
docker compose ps
Các bạn tự làm phần này nhé, kiểm tra xem đủ 8 services chưa nhé, đến bước này là phải OK rồi đó, trừ các bạn dùng Macbook Apple Silicon chip (chip M) 😢
Với Macbook Apple chip
Nếu các bạn dùng Macbook Apple chip, thì ở bước này ta khi ta chạy docker compose ps
, ta mới thấy chỉ có 7 services Up
:
Thiếu mất một đó là cartservice
, check logs ta thấy như sau:
docker compose logs cartservice
Search google một chút ra đầy kết quả, lí do là vì khả năng tương thích của project C# (dotnet) trên Apple Chip, vì apple chip họ dùng architecture khác (arm
), trong khi thường sẽ là amd
Ta có thể check architecture hệ điều hành của chúng ta với command sau:
arch
-->>>
arm64
Vậy giờ thử chạy cartservice
với architecture amd
xem, ta update docker-compose.yml
cho cartservice
như sau:
cartservice:
image: cartservice
platform: linux/amd64 # ==> thêm vào
environment:
- REDIS_ADDR=redis-cart:6379
depends_on:
- redis-cart
Sau đó ta restart lại project:
docker compose down
docker compose up -d
Ta lại thấy lỗi khác 😭😭:
Lí do là vì ta muốn chạy image cartservice
với platform (architecture) khác, trong khi ở local ta không có image được build với platform như vậy nên Docker nó fallback tìm trên Dockerhub và không thấy, do vậy ta cần update Dockerfile của cartservice
như sau:
FROM mcr.microsoft.com/dotnet/sdk:8.0.302-noble@sha256:bd836d1c4a19860ee61d1202b82561f0c750edb7a635443cb001042b71d79569 as builder
WORKDIR /app
COPY cartservice.csproj .
RUN dotnet restore cartservice.csproj \
-r linux-x64
COPY . .
RUN dotnet publish cartservice.csproj \
-p:PublishSingleFile=true \
-r linux-x64 \
--self-contained true \
-p:PublishTrimmed=true \
-p:TrimMode=full \
-c release \
-o /cartservice
# https://mcr.microsoft.com/product/dotnet/runtime-deps
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0.6-noble-chiseled@sha256:55d6e41f2e7687c597daa4fdca997b07beb3e23b6283729e19bb8ceb272def1a
WORKDIR /app
COPY /cartservice .
EXPOSE 7070
ENV DOTNET_EnableDiagnostics=0 \
ASPNETCORE_HTTP_PORTS=7070
USER 1000
ENTRYPOINT ["/app/cartservice"]
Ở trên các bạn chú ý mình đã thêm --platform=amd64
vào 2 chỗ có FROM
.
Sau đó ta build lại image:
docker build -t cartservice:v1 .
Ở trên mình thêm tag v1
cho khác tránh nhầm lẫn
Nhưng lại có lỗi khác đó là build rất lâu, mãi chưa xong 😰😰:
Bị treo luôn ở đoạn RUN dotnet restore...
.
Lại lọ mọ search Google, mò một lúc thì tìm ra là có lẽ ta cần thêm ENV DOTNET_EnableWriteXorExecute=0
, thôi thì lại sửa Dockerfile tiếp:
FROM mcr.microsoft.com/dotnet/sdk:8.0.302-noble@sha256:bd836d1c4a19860ee61d1202b82561f0c750edb7a635443cb001042b71d79569 as builder
ENV DOTNET_EnableWriteXorExecute=0
WORKDIR /app
COPY cartservice.csproj .
RUN dotnet restore cartservice.csproj \
-r linux-x64
COPY . .
RUN dotnet publish cartservice.csproj \
-p:PublishSingleFile=true \
-r linux-x64 \
--self-contained true \
-p:PublishTrimmed=true \
-p:TrimMode=full \
-c release \
-o /cartservice
# https://mcr.microsoft.com/product/dotnet/runtime-deps
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0.6-noble-chiseled@sha256:55d6e41f2e7687c597daa4fdca997b07beb3e23b6283729e19bb8ceb272def1a
WORKDIR /app
COPY /cartservice .
EXPOSE 7070
ENV DOTNET_EnableDiagnostics=0 \
ASPNETCORE_HTTP_PORTS=7070
USER 1000
ENTRYPOINT ["/app/cartservice"]
Ở trên ta đã thêm ENV DOTNET_EnableWriteXorExecute=0
vào stage builder
(lần FROM
đầu tiên)
Sau đó ta build lại image với tag mới v2
:
docker build -t cartservice:v2 .
Oh ngon rồi:
Ta update cartservice
ở docker-compose.yml
trước khi chạy lại nhé:
cartservice:
image: cartservice:v2 # ==> thêm vào
platform: linux/amd64 # ==> thêm vào
environment:
- REDIS_ADDR=redis-cart:6379
depends_on:
- redis-cart
Sau đó ta restart lại project:
docker compose down
docker compose up -d
Đến giờ thì ta đã có đủ tất cả 8 service đều Up
, check logs của cartservice
cũng oke rồi:
Dockerize lần hai
Lần này ta sẽ deploy 4 service còn lại, bao gồm: recommendation, frontend, checkout, loadgenerator
Cách làm vẫn tương tự, ta mở terminal ở từng service và build:
# src/recommendationservice
docker build -t recommendationservice .
# src/frontend
docker build -t frontend .
# src/checkoutservice
docker build -t checkoutservice .
# src/loadgenerator
docker build -t loadgenerator .
Phần này thì các bạn tự chạy sẽ thấy OK nhé, nhưng vẫn trừ các bạn dùng Macbook Apple Chip 🤣🤣
Với các bạn dùng Apple Chip, khi build tới loadgenerator
sẽ gặp lỗi:
Phần này thì cách fix giống với cartservice
ở trên, ta update lại Dockerfile
của loadgenerator
như sau:
FROM python:3.12.4-slim@sha256:d3a32591680bdfd49da5773495730cf8afdb817e217435db66588b2c64db6d5e as base
FROM base as builder
COPY requirements.txt .
RUN pip install --prefix="/install" -r requirements.txt
FROM base
WORKDIR /loadgen
COPY /install /usr/local
# Add application code.
COPY locustfile.py .
# enable gevent support in debugger
ENV GEVENT_SUPPORT=True
ENTRYPOINT locust --host="http://${FRONTEND_ADDR}" --headless -u "${USERS:-10}" 2>&1
Ở trên dòng FROM
đầu tiên ta thêm vào --platform=linux/amd64
, chú ý rằng ta không cần làm vậy với 2 dòng FROM
bên dưới, vì 2 cái FROM
bên dưới chúng FROM
trực tiếp từ base
rồi (là dòng đầu tiên)
Sau khi update thì ta build lại loadgenerator
(ta dùng tag v1
cho khác nhé):
docker build -t loadgenerator:v1 .
Oke ngon rồi:
Mệt với Apple chip quá 😂😂
Sau khi đã build xong các service ta update lại docker-compose.yml
và thêm tất cả các service vào:
services:
adservice:
image: adservice
environment:
- PORT=9555
productcatalogservice:
image: productcatalogservice
environment:
- PORT=3550
- DISABLE_PROFILER=1
cartservice:
image: cartservice:v2 # ==> thêm vào
platform: linux/amd64 # ==> thêm vào
environment:
- REDIS_ADDR=redis-cart:6379
depends_on:
- redis-cart
redis-cart:
image: redis:6-alpine
shippingservice:
image: shippingservice
environment:
- PORT=50051
- DISABLE_PROFILER=1
currencyservice:
image: currencyservice
environment:
- PORT=7000
- DISABLE_PROFILER=1
paymentservice:
image: paymentservice
environment:
- PORT=50051
- DISABLE_PROFILER=1
emailservice:
image: emailservice
environment:
- PORT=8080
- DISABLE_PROFILER=1
# --->> Thêm vào từ đây
recommendationservice:
image: recommendationservice
environment:
- PORT=8080
- PRODUCT_CATALOG_SERVICE_ADDR=productcatalogservice:3550
- DISABLE_PROFILER=1
depends_on:
- productcatalogservice
checkoutservice:
image: checkoutservice
environment:
- PORT=5050
- PRODUCT_CATALOG_SERVICE_ADDR=productcatalogservice:3550
- SHIPPING_SERVICE_ADDR=shippingservice:50051
- PAYMENT_SERVICE_ADDR=paymentservice:50051
- EMAIL_SERVICE_ADDR=emailservice:8080
- CURRENCY_SERVICE_ADDR=currencyservice:7000
- CART_SERVICE_ADDR=cartservice:7070
depends_on:
- productcatalogservice
- shippingservice
- paymentservice
- emailservice
- currencyservice
- cartservice
frontend:
image: frontend
ports:
- "8000:8080"
environment:
- PORT=8080
- PRODUCT_CATALOG_SERVICE_ADDR=productcatalogservice:3550
- CURRENCY_SERVICE_ADDR=currencyservice:7000
- CART_SERVICE_ADDR=cartservice:7070
- RECOMMENDATION_SERVICE_ADDR=recommendationservice:8080
- SHIPPING_SERVICE_ADDR=shippingservice:50051
- CHECKOUT_SERVICE_ADDR=checkoutservice:5050
- AD_SERVICE_ADDR=adservice:9555
- ENABLE_PROFILER=0
- SHOPPING_ASSISTANT_SERVICE_ADDR=shoppingassistantservice:8080
depends_on:
- productcatalogservice
- currencyservice
- cartservice
- recommendationservice
- shippingservice
- checkoutservice
- adservice
loadgenerator:
image: loadgenerator:v1
platform: linux/amd64
environment:
- FRONTEND_ADDR=frontend:8080
depends_on:
- frontend
Chú ý rằng ở trên các biến môi trường mình xem từ các file manifest Kubernetes của các service tương ứng và thêm vào, mình có thêm depends_on
thể hiện sự phụ thuộc giữa các service cho đúng với diagram, như vậy thì khi Docker start container nó sẽ start các service độc lập trước, sau đó tới các service phụ thuộc
Chú ý rằng ở service frontend
mình phải thêm biến SHOPPING_ASSISTANT_SERVICE_ADDR
, vì trong code họ yêu cầu cái đó, nhưng thực tế ta không cần service đó để chạy
Giờ ta restart lại project nhé:
docker compose down
docker compose up -d
Pòm pòm chíu chíu 🥳🥳:
App của chúng ta lên rồi, các bạn thoải mái test xem tất cả flow có gì lỗi không nhé:
Với Macbook Apple Chip
Vâng, một lần nữa, lại là Apple Chip 🤣🤣
nếu ta thử restart lại project:
docker compose down
docker compose up -d
Thì rất có thể ta sẽ gặp lỗi:
Lại là cartservice
, cái service C# Dotnet củ chuối này 🤨🤨
Check log ta thấy như sau:
Sau khi search một vòng Google thì ta sẽ hiểu ra thì đây là một lỗi khá phổ biến của project C#(dotnet) khi chạy trên arm
.
Tìm mãi không ra được solution cụ thể, tắt laptop khởi động lại thì có khi được 1-2 lần lại bị.
May quá tìm ra PR này: https://github.com/GoogleCloudPlatform/microservices-demo/pull/2266/files#diff-c9094dcf2dd2b5e3bb57d27631699d50e63b9bfb3aa3665db826e031b0ab9b4c
ý tưởng của họ là update Dockerfile để build cartservice
target tới architecture khác là linux-musl-arm64
(alpine + arm64), dùng runtime identifier (RID):
linux
: hệ điều hành target là Linuxmusl
: Thư viện C làmusl
, thường được dùng trên bản phân phối Alpine.arm64
: architecture la ARM64 (cũng được biết đến là AArch64), tương thích với Appl M1/M2/... chips và các platform ARM64 khác.
Giờ ta sửa lại Dockerfile
của cartservice
như sau nhé:
FROM mcr.microsoft.com/dotnet/sdk:8.0 as builder
WORKDIR /app
COPY cartservice.csproj .
RUN dotnet restore cartservice.csproj \
-r linux-musl-arm64
COPY . .
RUN dotnet publish cartservice.csproj \
-p:PublishSingleFile=true \
-r linux-musl-arm64 \
--self-contained true \
-p:PublishTrimmed=true \
-p:TrimMode=full \
-c release \
-o /cartservice
# https://mcr.microsoft.com/product/dotnet/runtime-deps
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine3.18-arm64v8
WORKDIR /app
COPY /cartservice .
EXPOSE 7070
ENV DOTNET_EnableDiagnostics=0 \
ASPNETCORE_HTTP_PORTS=7070
USER 1000
ENTRYPOINT ["/app/cartservice"]
Ở trên các bạn để ý rằng:
- ta không cần tới biến môi trường
ENV DOTNET_EnableWriteXorExecute=0
- ở stage
builder
ta dùnglinux-musl-arm64
- ở cái
FROM
thứ 2 thì ta dùng imageruntime-deps:8.0-alpine3.18-arm64
Giờ ta build lại image cartservice
với tag v3
nhé:
docker build -t cartservice:v3 .
Sau đó sửa lại cartservice
ở docker-compose.yml
cartservice:
image: cartservice:v3 # ==> update
environment:
- REDIS_ADDR=redis-cart:6379
depends_on:
- redis-cart
Chú ý rằng ta đã bỏ đi dòng platform: linux/amd64
Cuối cùng chạy lên ta sẽ thấy OK nhé. phewwwwww Apple Chip mệt quá ta 😅
Tổng hợp và kết bài
Như các bạn thấy, dù bài này Dockerfile họ đã viết sẵn cho chúng ta rồi nhưng khi chạy lên tuỳ vào môi trường của ta là gì nó còn có thể phát sinh lỗi và ta phải mò thêm.
Nhìn chung đây là ví dụ khá là trực quan demo việc deploy microservices với Docker như thế nào. Việc vẽ được, hiểu được architecture trước giúp quá trình làm việc của ta mượt hơn nhiều, ta biết được service nào phụ thuộc vào service khác, service nào không, từ đó đưa ra kế hoạch deploy cho phù hợp.
Chúc các bạn cuối tuần vui vẻ, hẹn gặp lại các bạn ở những bài sau
All rights reserved