+9

🧩🧠 Service Mesh: khi các Service không cần tin nhau - Microservice Architecture P13

Service Mesh: khi các Service không cần tin nhau

1. Buổi họp căng thẳng lúc 8 giờ tối

Hãy tưởng tượng kịch bản này: Hệ thống microservices của bạn đang chạy êm đẹp với khoảng 5 services viết bằng Java. Đột nhiên, một yêu cầu bảo mật mới xuất hiện từ phía ban quản trị: Toàn bộ traffic giao tiếp nội bộ giữa các service (thường gọi là east-west traffic) bắt buộc phải được mã hóa bằng mTLS (mutual TLS) để đáp ứng tiêu chuẩn bảo mật PCI-DSS. Đồng thời, do hệ thống bắt đầu phản hồi chậm chạp, bạn được yêu cầu tích hợp giải pháp Distributed Tracing để giám sát latency của từng request đi qua hệ thống.

Quyết định nhanh chóng được đưa ra: Phân bổ việc này cho các team tự tích hợp.

  • Team Java bắt đầu cấu hình KeyStore, TrustStore trong Spring Boot và viết cấu hình Resilience4j làm circuit breaker.
  • Team Go tự thiết lập kết nối TLS bằng thư viện crypto/tls và tích hợp OpenTelemetry SDK.
  • Team Node.js cũng làm tương tự, loay hoay với việc chuyển giao trace context qua các callback lồng nhau và quản lý vòng đời của chứng chỉ SSL.

Ba tháng trôi qua. Thay vì có một hệ thống bảo mật và dễ quan sát như kỳ vọng, bạn nhận lại một "bãi chiến trường" vận hành:

  • Tracing liên tục bị đứt đoạn: Chỉ vì một lập trình viên Node.js quên chuyển tiếp HTTP header traceparent trong một lần refactor code, toàn bộ trace chain cho 40% số request đi qua service đó bị biến mất.
  • Xoay vòng chứng chỉ (Certificate Rotation) trở thành thảm họa: Mỗi lần thay thế chứng chỉ hết hạn, toàn bộ các service phải được build lại và deploy lại để nhận file cert mới. Một service Java cấu hình sai định dạng truststore dẫn đến downtime kéo dài 2 tiếng trên production.
  • Tự sát bằng Retry Storm: Hàng loạt lỗi quá tải dây chuyền xảy ra khi cấu hình retry của team Go quá hung hãn (thử lại ngay lập tức mà không có độ trễ tăng dần), vô tình tạo ra một cuộc tấn công từ chối dịch vụ (DoS) nội bộ vào database khi hệ thống gặp quá tải nhẹ.

Đây chính là lúc bạn nhận ra: Đẩy logic quản lý mạng vào code ứng dụng là một sai lầm kiến trúc nghiêm trọng. Và đó là lý do Service Mesh ra đời.


2. Niềm tin phổ biến: "Muốn bảo mật và quản lý mạng, cứ import SDK là xong"

Trong những ngày đầu của kiến trúc microservices, bộ thư viện Netflix OSS (với Eureka, Hystrix, Ribbon) tích hợp trong Spring Cloud là tiêu chuẩn vàng. Lập trình viên chỉ cần thêm vài annotation như @EnableDiscoveryClient hay @HystrixCommand là đã có ngay tính năng service discovery và circuit breaking.

Cách làm này tạo ra một niềm tin phổ biến và ăn sâu vào tư duy của nhiều thế hệ lập trình viên: Quản lý giao tiếp mạng là việc của code ứng dụng và các thư viện (SDK).

Lý thuyết này nghe rất hợp lý vì:

  • Quyền tự quyết của lập trình viên: Dev có toàn quyền kiểm soát cách ứng dụng xử lý khi mạng lỗi. Logic retry hay timeout nằm ngay trong code, dễ đọc, dễ debug trên môi trường local.
  • Tiện lợi trong các hệ thống đơn ngôn ngữ: Nếu toàn bộ hệ thống của bạn chỉ viết bằng một ngôn ngữ duy nhất (ví dụ Java/Spring Boot), việc sử dụng một bộ SDK dùng chung rất mượt mà. Mọi thứ thống nhất từ cấu hình cho đến cách vận hành.

Nhưng thực tế production luôn phức tạp hơn lý thuyết. Khi hệ thống lớn lên, công nghệ phân hóa (polyglot) với sự xuất hiện của Go, Node.js, Python, mô hình SDK-based bắt đầu bộc lộ những điểm gãy chết người.


3. Tại sao mô hình SDK-based gãy khi microservices phình to

Khi số lượng service tăng từ 5 lên 50, và ngôn ngữ lập trình không còn đồng nhất, việc dùng SDK sẽ biến thành một cơn ác mộng vận hành vì các nguyên nhân sau:

  1. Gánh nặng bảo trì đa ngôn ngữ (Polyglot Hell): Bạn không thể duy trì một "thư viện dùng chung" cho tất cả các team nếu họ dùng các ngôn ngữ khác nhau. Bạn sẽ phải duy trì 3-4 phiên bản của cùng một logic mạng (mTLS, retry, circuit breaker) trên Java, Go, Node.js. Mỗi thư viện lại có những bug riêng, cơ chế threading riêng, và đòi hỏi nỗ lực bảo trì khổng lồ.
  2. Khó cập nhật và đồng bộ cấu hình (Configuration Drift): Nếu muốn đổi cấu hình timeout từ 2 giây lên 5 giây trên toàn hệ thống, bạn phải sửa file cấu hình của từng service, mở Pull Request, review, chạy CI/CD và deploy lại 50 service đó. Mạng là thứ thay đổi liên tục, nhưng việc thay đổi nó lại bị ràng buộc chặt chẽ vào vòng đời release của ứng dụng.
  3. Rủi ro bảo mật từ con người: Làm sao bạn chắc chắn 100% rằng một lập trình viên junior không vô tình tắt xác thực SSL để bypass lỗi chứng chỉ ở môi trường staging rồi đẩy thẳng lên production? Không thể, trừ khi bạn có một quy trình audit code cực kỳ khắt khe và tốn kém.
  4. Sự không đồng nhất về mặt hành vi: Thư viện circuit breaker của Java hoạt động khác với thư viện của Node.js. Cùng một trạng thái lỗi mạng, service Java có thể ngắt kết nối ngay lập tức, trong khi service Node.js tiếp tục gửi request và làm nghẽn hệ thống.

4. Góc nhìn mới: Chuyển độ phức tạp mạng xuống hạ tầng

Để giải quyết tận gốc bài toán này, chúng ta cần thay đổi góc nhìn về kiến trúc.

Hãy nghĩ về cách hệ điều hành và phần cứng xử lý giao tiếp mạng. Khi ứng dụng của bạn gửi một request HTTP, code của bạn không hề quan tâm đến thuật toán kiểm soát tắc nghẽn TCP (TCP congestion control), cách chia nhỏ gói tin IP, hay cách mã hóa bit trên đường truyền vật lý. Tất cả những việc đó được đảm nhiệm hoàn toàn bởi nhân hệ điều hành (Kernel) và card mạng. Ứng dụng chỉ đơn giản là viết dữ liệu vào một socket.

Vậy tại sao các tính năng như mTLS, retry, timeout, circuit breaker, và tracing — vốn dĩ cũng là các mối bận tâm về mặt truyền tin — lại phải nằm trong code ứng dụng?

Chúng nên được đẩy xuống tầng hạ tầng (infrastructure layer). Đây chính là triết lý cốt lõi của Sidecar PatternService Mesh.

Thay vì nhồi nhét thư viện mạng vào trong code, chúng ta đặt một proxy siêu nhẹ, hiệu năng cao chạy song song bên cạnh mỗi service. Trong môi trường Kubernetes, proxy này chạy như một container riêng biệt (gọi là Sidecar) nằm chung trong cùng một Pod với ứng dụng của bạn.

Vì chạy chung Pod, ứng dụng và proxy Sidecar chia sẻ cùng một Network Namespace (nghĩa là chúng dùng chung địa chỉ IP và giao tiếp với nhau cực kỳ nhanh qua localhost). Bằng cách cấu hình các luật điều hướng mạng (iptables), chúng ta ép buộc toàn bộ traffic đi vào và đi ra khỏi container ứng dụng phải đi qua proxy Sidecar một cách hoàn toàn trong suốt (transparent interception). Ứng dụng không hề biết đến sự tồn tại của proxy; nó chỉ nghĩ rằng nó đang nói chuyện trực tiếp với thế giới bên ngoài.


5. Bóc tách Sidecar Pattern: Envoy hoạt động như thế nào?

Để hiểu rõ hơn, hãy xem luồng đi của một request từ Order Service sang Payment Service khi có sự tham gia của proxy Envoy (proxy phổ biến nhất được dùng trong Istio):

                  [ Pod: Order Service ]
                  ┌──────────────────────────────────────────────┐
                  │                                              │
                  │   ┌─────────────────┐  localhost  ┌──────┐   │
                  │   │  Order Service  │◄───────────▶│Envoy │   │
                  │   │  (App Container)│             │Proxy │   │
                  │   └─────────────────┘             └──────┘   │
                  │                                      ▲       │
                  └──────────────────────────────────────┼───────┘
                                                         │ 
                                                         │ mTLS (Mã hóa & Xác thực)
                                                         │ Tracing Header Injected
                                                         │
                  [ Pod: Payment Service ]               │
                  ┌──────────────────────────────────────┼───────┐
                  │                                      ▼       │
                  │   ┌─────────────────┐  localhost  ┌──────┐   │
                  │   │ Payment Service │◄───────────▶│Envoy │   │
                  │   │  (App Container)│             │Proxy │   │
                  │   └─────────────────┘             └──────┘   │
                  │                                              │
                  └──────────────────────────────────────────────┘

Quy trình diễn ra như sau:

  1. Chặn đầu ra: Order Service gửi một request HTTP không mã hóa tới http://payment-service. Nhờ cấu hình iptables trong Pod, request này tự động bị đổi hướng vào cổng lắng nghe của Envoy Proxy cục bộ.
  2. Xử lý tại Proxy nguồn: Envoy kiểm tra cấu hình. Nó phát hiện request này hướng tới payment-service. Nó tự động thiết lập một kết nối mTLS (mutual TLS) an toàn với Envoy Proxy đích, đồng thời kiểm tra xem request đã có các trace header chưa để tự động bổ sung hoặc cập nhật.
  3. Truyền tải an toàn: Traffic đi qua mạng nội bộ giữa hai Pod dưới dạng HTTPS được mã hóa. Mọi nỗ lực nghe lén ở tầng mạng vật lý đều vô tác dụng.
  4. Chặn đầu vào & Giải mã: Envoy Proxy của Payment Service nhận request, xác thực chứng chỉ Client của Envoy gửi từ Order Service (Mutual Authentication). Nếu hợp lệ, nó giải mã traffic và chuyển tiếp request dưới dạng HTTP không mã hóa tới Payment Service thông qua cổng localhost.
  5. Ghi nhận Metric: Cả hai proxy ghi nhận thời gian phản hồi, mã lỗi (HTTP status) và gửi các metric này về hệ thống giám sát tập trung một cách không đồng bộ (asynchronously).

Ứng dụng của bạn hoàn toàn sạch bóng các đoạn code cấu hình SSL/TLS phức tạp. Giao tiếp mạng trở nên bảo mật và minh bạch mà dev không cần tốn một dòng code nào.


6. Kiến trúc hai tầng: Data Plane vs Control Plane

Để vận hành hàng trăm Sidecar proxy chạy rải rác khắp cluster, Service Mesh chia hệ thống thành hai phần riêng biệt: Data PlaneControl Plane.

┌────────────────────────────────────────────────────────┐
│                     CONTROL PLANE                      │
│                                                        │
│  - Quản lý & đồng bộ cấu hình, chính sách (Traffic)     │
│  - Cơ quan cấp phát & xoay vòng chứng chỉ CA (mTLS)    │
│  - Thu thập Telemetry (Metrics & Tracing)              │
└──────────────────────────┬─────────────────────────────┘
                           │ Pushes Config / Certificates
                           │ (qua gRPC / xDS API)
                           ▼
┌────────────────────────────────────────────────────────┐
│                       DATA PLANE                       │
│                                                        │
│  [ Pod A ]                             [ Pod B ]       │
│  ┌───────────┐                         ┌───────────┐   │
│  │ App A     │                         │ App B     │   │
│  └───┬───────┘                         └───▲───────┘   │
│      │ localhost                           │ localhost │   │
│  ┌───▼───────┐        mTLS (Envoy-to-Envoy)│           │   │
│  │ Proxy A   │========================▶│ Proxy B   │   │
│  └───────────┘                         └───────────┘   │
└────────────────────────────────────────────────────────┘

Data Plane (Mặt phẳng dữ liệu)

  • Gồm tất cả các sidecar proxy chạy cùng các service.
  • Nhiệm vụ: Thực thi việc truyền nhận gói tin, mã hóa, routing, retry, áp dụng rate limit thực tế.
  • Đặc điểm: Phải cực kỳ nhẹ, nhanh và tiêu tốn ít tài nguyên nhất có thể. Envoy được viết bằng C++ và Linkerd-proxy được viết bằng Rust để đáp ứng yêu cầu khát khe này.

Control Plane (Mặt phẳng điều khiển)

  • Là bộ não trung tâm (ví dụ istiod của Istio).
  • Nhiệm vụ: Không can thiệp trực tiếp vào từng gói tin của ứng dụng. Thay vào đó, nó đóng vai trò quản lý cấu hình. Khi bạn định nghĩa một policy, Control Plane sẽ dịch cấu hình này sang định dạng mà Envoy hiểu được và đẩy xuống toàn bộ các proxy dưới Data Plane thông qua các API chuyên dụng (xDS API).
  • Đồng thời, Control Plane hoạt động như một Certificate Authority (CA) nội bộ, tự động sinh ra chứng chỉ TLS ngắn hạn, phân phối và xoay vòng chúng đến từng sidecar proxy mà không làm gián đoạn kết nối.

7. Sức mạnh thực sự: Những bài toán Service Mesh giải quyết tự động

Khi đã có một Service Mesh hoạt động ổn định, bạn sẽ có được những siêu năng lực mà trước đây phải tốn hàng tháng trời code mới có được:

Bảo mật Zero-Trust đích thực

Bạn không còn phải tin tưởng vào mạng nội bộ của Kubernetes. Với mTLS được bật mặc định:

  • Authentication: Xác thực danh tính của service gửi yêu cầu.
  • Authorization: Định nghĩa các luật phân quyền chi tiết. Ví dụ, bạn có thể viết một cấu hình khai báo quy định service analytics chỉ được phép gọi HTTP GET tới /public-data của user-service, mọi hành vi gọi vào /admin đều bị block ngay lập tức ở tầng proxy.

Quản lý Traffic thông minh (Traffic Routing)

  • Canary Deployments: Bạn có thể định hướng 90% traffic tới phiên bản ổn định v1 và 10% tới phiên bản mới v2. Nếu v2 chạy tốt, bạn tăng dần tỷ lệ mà không cần thay đổi DNS hay cấu hình của API Gateway.
  • Request Mirroring: Bạn có thể nhân bản toàn bộ traffic thật trên production và gửi sang một service thử nghiệm để test hiệu năng dưới tải thực tế mà hoàn toàn không ảnh hưởng đến trải nghiệm của người dùng cuối.

Khả năng tự phục hồi (Resiliency)

  • Outlier Detection: Nếu một pod trong cụm payment-service bị lỗi phần cứng và liên tục trả về lỗi 503, Envoy sẽ tự động nhận diện và tạm thời cô lập pod đó ra khỏi pool load balancing.
  • Retry & Timeout policies: Thiết lập nhất quán cơ chế retry với exponential backoff để tránh tình trạng "retry storm" tự hủy hệ thống.

Khả năng quan sát toàn diện (Telemetry)

  • Tự động vẽ bản đồ phụ thuộc giữa các service (Service Dependency Graph).
  • Tự động đo lường "4 tín hiệu vàng" (Golden Signals): Latency, Traffic, Errors, và Saturation của từng kết nối mà không phụ thuộc vào code ứng dụng.

8. Cái giá phải trả: Những trade-off "đau đớn"

Nguyên lý thiết kế hệ thống luôn nhắc nhở chúng ta: Không có bữa ăn nào miễn phí. Bản chất của Service Mesh là chuyển đổi độ phức tạp từ tầng ứng dụng (application code) sang tầng vận hành hạ tầng (infrastructure operations). Bạn sẽ phải đánh đổi những điều sau:

Trễ mạng tăng thêm (Latency Overhead)

Vì mọi request giờ đây phải đi qua hai proxy trung gian (Proxy nguồn -> Proxy đích), hệ thống sẽ bị cộng thêm một khoảng trễ nhỏ. Dù Envoy được tối ưu hóa đến mức cực hạn (thường dưới 1ms cho mỗi hop), nhưng nếu kiến trúc của bạn có chuỗi gọi nhau quá dài (ví dụ: Gateway -> Service A -> Service B -> Service C -> Service D), tổng latency cộng dồn có thể lên tới 5-10ms. Đối với các hệ thống yêu cầu độ trễ cực thấp, đây là một cái giá rất đắt.

Tiêu tốn tài nguyên kinh khủng (Resource Cost)

Hãy làm một bài toán số học đơn giản: Mỗi sidecar proxy Envoy tiêu tốn khoảng 50MB RAM và 0.1 CPU core để duy trì kết nối và lưu trữ bảng định tuyến. Nếu hệ thống của bạn có 100 service, mỗi service scale trung bình 5 bản sao (replica), bạn sẽ chạy tổng cộng 500 pods. Số tài nguyên hao phí chỉ dành riêng cho việc chạy proxy sẽ là:

  • RAM: 500 * 50MB = 25GB RAM.
  • CPU: 500 * 0.1 = 50 CPU cores. Đối với nhiều doanh nghiệp vừa và nhỏ, chi phí hạ tầng để nuôi các proxy này thậm chí còn lớn hơn chi phí chạy code ứng dụng thực tế.

Độ phức tạp khi debug (Troubleshooting Nightmare)

Khi một request thất bại với lỗi 503 Service Unavailable hoặc Connection Timeout, việc tìm ra nguyên nhân gốc rễ trở nên khó khăn hơn gấp nhiều lần. Lỗi nằm ở code của ứng dụng A, Envoy của ứng dụng A cấu hình sai cổng, luật iptables chặn nhầm traffic, Envoy của ứng dụng B bị quá tải, hay do Control Plane chưa đồng bộ xong cấu hình mới? Platform team lúc này bắt buộc phải có kiến thức chuyên sâu về mạng Linux, lệnh tcpdump, cách đọc log định dạng Envoy, và cơ chế hoạt động của iptables.


9. Failure Case: Khi Service Mesh giết chết hệ thống nhỏ

Một trong những lỗi kinh điển nhất của các kỹ sư là lạm dụng Service Mesh quá sớm.

Hãy nghe câu chuyện thực tế của một dự án nọ: Một startup xây dựng một sản phẩm thương mại điện tử nhỏ. Ở giai đoạn đầu, họ chỉ có 4 microservices chạy trên một cụm Kubernetes gồm 3 node (mỗi node có cấu hình 4 vCPUs và 16GB RAM). Với mong muốn đi tắt đón đầu và áp dụng "best practices", họ quyết định cài đặt Istio ngay từ ngày đầu tiên để quản lý mTLS và thu thập tracing.

Hậu quả là:

  1. Kiệt quệ tài nguyên: Bản thân Control Plane của Istio (istiod) và các sidecar proxy đã ngấu nghiến gần 40% dung lượng CPU và RAM khả dụng của cả cluster. Các pod ứng dụng liên tục rơi vào trạng thái bị bóp hiệu năng (CPU throttling) và thiếu bộ nhớ (OOMKilled).
  2. Downtime dây chuyền: Một đêm nọ, cụm Kubernetes gặp sự cố mạng nhẹ, khiến istiod không thể giao tiếp với các sidecar proxy. Khi các pod ứng dụng restart, sidecar proxy không thể khởi tạo vì không lấy được cấu hình từ Control Plane. Kết quả là toàn bộ hệ thống bị tê liệt hoàn toàn, không thể tự phục hồi.
  3. Lãng phí thời gian: Thay vì tập trung viết tính năng để kiểm thử thị trường, đội ngũ phát triển phải dành 50% thời gian để debug các file YAML cấu hình phức tạp của Istio như VirtualService, DestinationRule, và Gateway.

Bài học rút ra: Nếu hệ thống của bạn chỉ có dưới 10 services và hầu hết được viết bởi cùng một ngôn ngữ, hãy tránh xa Service Mesh. Bạn có thể giải quyết các bài toán bảo mật và tracing bằng những cách đơn giản hơn rất nhiều: dùng API Gateway ở rìa hệ thống, tận dụng mạng overlay mặc định của Kubernetes, hoặc sử dụng các thư viện tracing gọn nhẹ. Đừng trả thuế vận hành của một chiếc tàu sân bay khi bạn chỉ cần đi qua một khúc sông nhỏ.


10. Tổng kết: Khi các service không cần tin nhau

Bản chất của Service Mesh không phải là một công nghệ thần kỳ giúp ứng dụng chạy nhanh hơn. Ngược lại, nó làm hệ thống chạy chậm hơn một chút và tốn tài nguyên hơn. Nhưng giá trị tối thượng của nó là mang lại sự kiểm soát và tính đồng nhất.

Bằng cách chuyển giao nhiệm vụ quản lý mạng từ tầng ứng dụng xuống tầng hạ tầng thông qua mô hình Sidecar, Service Mesh giúp:

  • Developer: Giải phóng hoàn toàn khỏi các logic mạng phức tạp, chỉ tập trung vào business logic.
  • Platform/Security Team: Có một điểm kiểm soát duy nhất (Control Plane) để thực thi chính sách bảo mật Zero-Trust, phân luồng traffic, và giám sát toàn diện hệ thống mà không cần xin phép hay làm phiền đến các đội phát triển code.

Khi thiết kế hệ thống microservices, câu hỏi không phải là "Chúng ta có dùng Service Mesh hay không?", mà là "Khi nào chúng ta sẵn sàng trả phí vận hành để đổi lấy sự tự do cho code ứng dụng?"


Lời kết: Khi hạ tầng mạng đã kiên cố, làm sao để đảm bảo tính toàn vẹn của API?

Service Mesh đã giải quyết hoàn hảo bài toán truyền tin an toàn và tin cậy giữa các service. Tuy nhiên, hạ tầng mạng kiên cố không có nghĩa là hệ thống sẽ không bị lỗi nếu các team tự ý thay đổi cấu trúc dữ liệu trả về (API Payload) mà các team khác phụ thuộc vào. Làm thế nào để đảm bảo code của các team khác nhau khi ráp nối vẫn khớp giao tiếp, không bị vỡ API mà không cần phải chạy các bài kiểm thử tích hợp (Integration Test) cồng kềnh và chậm chạp?

Tập tiếp theo của series sẽ mang đến câu trả lời với Contract Testing – phương pháp kiểm thử niềm tin thực sự trong thế giới microservices.


Góc nhìn thêm: Service Mesh chỉ đáng giá khi network policy đã trở thành sản phẩm nội bộ

Service Mesh thường được quảng bá như lớp hạ tầng giúp retry, mTLS, traffic policy, observability "miễn phí". Nhưng cái bẫy nằm ở chữ miễn phí. Không có gì miễn phí cả. Bạn chỉ đang dời complexity khỏi application code sang platform layer.

Nếu tổ chức đã đủ lớn để policy giao tiếp, security posture và traffic routing trở thành bài toán lặp đi lặp lại, việc gom chúng về platform có thể rất đáng giá. Ngược lại, với một hệ còn nhỏ, service mesh rất dễ trở thành một bộ máy phức tạp mà ít người thật sự hiểu sâu.

Nhìn thêm: Hỏi gì trước khi đưa mesh vào?

  • team có lặp lại cùng một concern network ở rất nhiều service chưa?
  • platform team có đủ năng lực sở hữu lớp hạ tầng đó lâu dài không?
  • chi phí debug khi có thêm sidecar, control plane và nhiều hop policy có đang vượt quá lợi ích không?

Nhiều đội ngũ đưa mesh vào vì "đó là cách Big Tech làm". Nhưng Big Tech có bài toán ở quy mô khác và đội vận hành khác. Kiến trúc đúng luôn phụ thuộc vào năng lực tổ chức cụ thể.

🔍 Đi sâu hơn cùng TechCraft

Microservices chỉ thực sự phát huy giá trị khi bạn hiểu rõ system boundary, communication pattern, failure mode và các trade-off vận hành phía sau nó.

Nếu bạn muốn tiếp tục rèn tư duy thiết kế hệ thống theo hướng thực chiến hơn, Dev Insider là nơi TechCraft tập trung các series đào sâu vào backend, database, distributed systems và production thinking.

🚀 Dev Insider https://www.patreon.com/techcraft_official/posts/vi-sao-dev-ra-161163881?collection=2220113

📘 Facebook https://www.facebook.com/techcraft.official

🎥 YouTube https://www.youtube.com/@techcraft.official

🎵 TikTok https://www.tiktok.com/@techcraft.official

Hiểu trade-off. Thiết kế tốt hơn.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.