1 điểm bởi GN⁺ 2024-07-13 | 1 bình luận | Chia sẻ qua WhatsApp
  • Container image thường được đưa lên các registry chuyên dụng, nhưng nếu phơi bày qua HTTP một bucket S3 và đặt tệp vào các đường dẫn đã định, có thể dùng nó như đích docker pull
  • Trong trình build image tùy chỉnh, việc tạo image có thể pull được trong vài giây đã trở thành nút thắt, trong đó thời gian push layer chiếm tỷ trọng lớn
  • Trong benchmark upload layer 198 MiB, ECR được đo ở mức 24–28 MiB/s, S3 ở mức 115–190 MiB/s, tức S3 nhanh hơn tối đa 8 lần
  • Khác biệt này xuất phát từ việc S3 có thể upload song song các chunk của một layer đơn, trong khi ECR dựa trên OCI Distribution Spec phải xử lý các chunk tuần tự
  • Cách này không thay thế được các tính năng registry như docker push, kiểm tra tính hợp lệ của image, quét bảo mật, hay kiểm soát truy cập kho private, nên cần xem đây là một tối ưu hóa mang tính thử nghiệm

Điều kiện để docker pull từ bucket S3

  • Để dùng S3 như container registry, cần phơi bày qua HTTP bucket và upload các tệp image vào đúng đường dẫn mà Docker kỳ vọng
  • Nếu đáp ứng điều kiện, có thể lấy image bằng docker pull như với registry thông thường
  • Image demo chạy cowsay, và hoạt động bằng cách dùng URL bucket như tên image với docker run --rm .../cowsay
  • Demo dùng Cloudflare R2
    • Lý do chọn là egress miễn phí
    • R2 và S3 tương thích API, nên tác giả dùng AWS SDK để upload image lên R2

Vì sao cân nhắc S3 thay vì registry chuyên dụng

  • Container image thường được host trên các registry chuyên dụng như DockerHub, GitHub Container Registry, ECR
  • Mục tiêu của trình build image tùy chỉnh là đi từ yêu cầu đến một image có thể pull được chỉ trong vài giây
  • Trong môi trường AWS, ECR là lựa chọn dễ nhất, nhưng tốc độ upload thực tế cho thấy chênh lệch giữa S3 và ECR rất lớn
  • Khi thêm trace thực thi vào mã để tối ưu hiệu năng, thời gian push layer lên container registry được xác định là nút thắt lớn

Kết quả upload layer 198 MiB

  • Benchmark nhỏ này upload riêng một layer 198 MiB lên ECR và S3 để so sánh thời gian cần thiết và thông lượng
  • Tốc độ quan sát được như sau
    • ECR: tối thiểu 24 MiB/s, 8,2 giây
    • ECR: tối đa 28 MiB/s, 7,0 giây
    • S3: tối thiểu 115 MiB/s, 1,7 giây
    • S3: tối đa 190 MiB/s, 1,0 giây
  • Theo kết quả, S3 nhanh hơn ECR tối đa khoảng 8 lần
  • Mã thử nghiệm được chạy trên AWS, còn S3 và ECR được kết nối nội bộ qua VPC, không đi qua Internet công cộng
    • Điều kiện này cung cấp độ trễ và băng thông tốt trong khả năng có thể

Chênh lệch tốc độ do upload chunk song song

  • Upload lên S3 có thể upload song song các chunk của một layer đơn
  • Nếu có đủ băng thông, upload chunk song song sẽ làm tăng mạnh thông lượng
  • Tài liệu AWS cũng khuyến nghị upload chunk song song để tối đa hóa việc sử dụng băng thông
  • ECR triển khai OCI Distribution Spec
    • Đặc tả này là tiêu chuẩn giúp docker pulldocker push hoạt động trên nhiều registry
    • Việc push layer phải diễn ra tuần tự; ngay cả khi upload theo chunk, chunk trước phải hoàn tất rồi mới chuyển sang chunk tiếp theo
  • Khi thử upload tuần tự trên S3, thông lượng giảm xuống mức tương tự ECR

Cấu trúc request thực tế của docker pull

  • Các request nội bộ của docker pull gồm nhiều request HEADGET
  • Luồng ví dụ như sau
    • Kiểm tra sự tồn tại của manifest image: HEAD /v2/my-image/manifests/latest
    • Tải manifest image: GET /v2/my-image/manifests/latest
    • Tải lại bằng hash của manifest: GET /v2/my-image/manifests/sha256:...
    • Tải blob metadata của image: GET /v2/my-image/blobs/sha256:...
    • Tải blob layer của image: GET /v2/my-image/blobs/sha256:...
  • Cuối cùng, docker pull gần giống một quá trình tải các tệp cần thiết qua HTTP
  • Nếu một máy chủ tệp tĩnh đặt các tệp cần thiết ở đúng đường dẫn kỳ vọng và thiết lập header Content-Type phù hợp cho từng request, có thể pull container image
  • Bucket S3 có thể đáp ứng hai điều kiện này, nên nếu cấu hình cẩn thận, nó sẽ hoạt động như một container registry

Hạn chế của cách tiếp cận thử nghiệm

  • Cách này vẫn mang tính thử nghiệm, và khó đưa ra kết luận mạnh trước khi khảo sát thêm
  • S3 không phải container registry theo nghĩa nghiêm ngặt
    • Không thể docker push
    • Việc docker pull hoạt động là kết quả của sự khớp nhau giữa cấu trúc request HTTP và cách phục vụ tệp tĩnh
  • Các container registry hiện có cung cấp nhiều tính năng hơn so với cách upload tệp lên bucket
    • Có thể tin rằng image được upload bằng cách push chuẩn thực sự hợp lệ
    • Có thể cung cấp quét bảo mật tự động và cảnh báo cho các layer
    • Có thể chỉ định quyền truy cập kho private theo cách native
  • Nếu hoạt động như kỳ vọng, việc host container image public trên Cloudflare R2 cũng có thể trở nên khả thi

1 bình luận

 
GN⁺ 2024-07-13
Ý kiến trên Hacker News
  • Thật đáng tiếc vì OCI Distribution Spec đọc không giống một đặc tả được thiết kế tốt
    Theo đặc tả, việc push layer phải được thực hiện tuần tự, nên dù upload theo chunk thì cũng phải chờ từng chunk kết thúc mới chuyển sang chunk tiếp theo được. Theo thử nghiệm trên DockerHub và GHCR, bản thân upload theo chunk dù sao cũng đã bị hỏng, và các client thường upload nguyên cả blob/layer. Đặc tả còn khuyến nghị định dạng giá trị Content-Range không khớp với định dạng RFC7233
    Tất nhiên có song song hóa ở cấp blob, nhưng không có song song hóa bên trong blob. Một điểm không hài lòng nữa là đã bỏ lỡ cơ hội chuẩn hóa phân trang cho danh sách tag. Do phần câu chữ liên quan trong tiêu chuẩn bị xóa nhầm [1], mỗi registry lại triển khai theo một kiểu
    [1] https://github.com/opencontainers/distribution-spec/issues/4...

    • Docker và các công nghệ xoay quanh container nhìn chung là như vậy. Docker với tư cách trải nghiệm người dùng thì tuyệt vời, nhưng với tư cách công nghệ thì gần như là một mớ hỗn độn
      Nói vậy không phải là muốn chỉ trích hoàn toàn. Thực tế nó mang tính cách mạng, giúp việc dùng Linux namespace dễ hơn trước rất nhiều, và đã thay đổi thế giới theo hướng tốt hơn. Chỉ là nó luôn ưu tiên trải nghiệm người dùng hơn mức độ hoàn thiện kỹ thuật, và bản thân điều đó cũng không hẳn là xấu. Giống như có nhiều công ty nhàm chán giải quyết các vấn đề đắt đỏ bằng Perl hay CSV gửi qua FTP, công nghệ nhàm chán, thậm chí tệ, nếu được đóng gói tốt thì vẫn có giá trị lớn
      Dù vậy, đôi khi vẫn thấy tiếc vì lẽ ra nó đã có thể tốt hơn hiện tại rất nhiều
    • Thêm vào đó, không rõ đây là vấn đề của đặc tả OCI hay do AWS khác thường, nhưng khác với GitLab hay Nexus, AWS ECR không hỗ trợ tự động tạo thư mục
      Ví dụ, dạng như .dkr.ecr..amazonaws.com/foo/bar/baz:tag không dùng được, chỉ có thể lưu dạng phẳng, khiến tên image hoặc tag trở nên quá dài. Về lý thuyết có thể tạo các đối tượng repository ECR trong Terraform để mô phỏng tương tự, nhưng với các pipeline có đường dẫn image kết quả là động thì không phù hợp lắm. Phải cấp quá nhiều quyền đến mức khó chịu cho IAM role của CI pipeline, và tôi cũng không thích việc tài nguyên AWS được quản lý bên ngoài kho Terraform trung tâm
      [1] https://stackoverflow.com/questions/64232268/storing-images-...
  • Cloudflare từng công bố mã nguồn mở một server container registry dùng R2
    Không biết có ai đã dùng thử chưa
    [1]: https://github.com/cloudflare/serverless-registry

    • Trông có vẻ ổn. Tuy nhiên có ghi là giới hạn 500MB cho mỗi layer
      Với một số mục đích sử dụng thì có thể không phải vấn đề lớn, nhưng trong trường hợp khác có thể là điều kiện loại ngay lập tức
  • Tôi là tác giả bài viết. Nếu ai biết vì sao trong đặc tả OCI việc push layer phải diễn ra tuần tự thì mong được chỉ giáo
    Tôi tò mò không biết đây chỉ là ngẫu nhiên lịch sử đơn thuần, hay có lý do ẩn nào đó. Nói rõ hơn, nhiều layer dĩ nhiên có thể được push song song; ở đây tôi đang nói đến phần bắt buộc phải push tuần tự nội dung của một layer đơn lẻ

    • Có thể là vì nó làm cho việc dọn dẹp đơn giản hơn. Nếu chưa tới được chunk “cuối cùng” thì rõ ràng sau N+Timeout đó là một upload chưa hoàn tất, nên chỉ cần xóa đi
      Nói cách khác, nó đơn giản hóa chi tiết triển khai về cách xử lý upload một phần. Nếu không, ở cuối mỗi chunk sẽ phải kiểm tra xem tất cả các chunk khác đã có đủ chưa rồi xử lý hoàn tất. Dù vậy đây là chi tiết triển khai, và tôi nghi ngờ liệu nó có phải là một thiết kế có ý nghĩa hoặc có chủ đích hay không. Cách tiếp cận bằng S3 có vẻ sẽ hoạt động tốt, và trước đây ở một công ty triển khai các image lớn, chúng tôi từng làm điều tương tự. Mức 0,10 đô la mỗi GB mỗi tháng tích lại thành con số khá lớn
      Bạn sẽ mất các tính năng bổ sung của ECR, nhưng cá nhân tôi thấy những tính năng đó khá hạn chế
    • Tôi chưa từng xử lý push, nhưng cách tiếp cận này thật đáng mừng. Thời kỳ đầu của Docker không có container registry riêng tư nào dùng được, nên chúng tôi từng đặt image sau nginx rồi pull, vì thế tôi đọc bài này rất thích thú
    • Tôi từng triển khai một registry tương thích OCI [1], và vì đặc tả phức tạp nên phần lớn chúng tôi đi theo hành vi của triển khai tham chiếu [2] hơn là theo đặc tả
      Khi client kết thúc upload blob, nó phải cung cấp digest của toàn bộ blob. Yêu cầu này có vẻ nhằm cho phép server xác minh tính toàn vẹn của các byte đã nhận. Nếu server chỉ bắt đầu kiểm tra digest ở HTTP request cuối cùng, nó sẽ phải đọc lại toàn bộ nội dung blob đã ghi vào storage từ các HTTP request trước đó. Với các layer lớn, độ trễ này có thể khó chấp nhận. Do yêu cầu của một client cụ thể, chúng tôi đã xác minh hệ thống hoạt động tới blob 150GiB
      Thay vào đó, trong triển khai của mình, chúng tôi duy trì việc tính digest xuyên suốt toàn bộ chuỗi request. Khi nhận dữ liệu blob theo từng chunk, chúng tôi đồng thời tính digest và stream vào blob storage. Giữa mỗi request, trạng thái tính digest được serialize vào URL upload trả lại cho client trong header Location. Đại khái được xử lý trong đoạn mã này: https://github.com/sapcc/keppel/blob/7e43d1f6e77ca72f0020645...
      Theo tôi biết, triển khai tham chiếu cũng dùng cách tiếp cận tương tự. Vì việc tính digest chỉ có thể thực hiện tuần tự, nên upload cũng phải diễn ra tuần tự
      [1] https://github.com/sapcc/keppel
      [2] https://github.com/distribution/distribution
    • Cảm ơn vì bài blog. Bạn có nói “trong 4 tháng qua đã hợp tác với Outerbounds để phát triển một trình build container image tùy chỉnh”; dù bạn nói đây là chủ đề cho một bài riêng, tôi vẫn tò mò liệu bạn có thể chia sẻ chút chi tiết nào không
      Nếu có link kho GitHub thì càng tốt. Bối cảnh là tôi đang tìm cách tạo OCI image bằng lập trình bên trong $PROGRAMMING_LANGUAGE, hoặc đang nghĩ đến việc tự triển khai. Kiểu như Buildah, nhưng thay vì giao diện dòng lệnh thì là API thực sự cho ngôn ngữ lập trình. Tất nhiên cũng có thể gọi Buildah như tiến trình con, nhưng như vậy hơi phiền, còn phải để ý tới việc tương tác với trạng thái nội bộ của Buildah hoặc dọn dẹp, và hiện tại Buildah cũng không hỗ trợ Mac
    • Tôi chưa nghĩ ra lý do rõ ràng nào ngay, cũng có thể là do tải
      Tôi nhớ trước đây mình từng thêm push song song vào Docker, nhưng cũng có thể tôi đang nhầm giữa pull và push. Sau này xem lại thì công việc của tôi là về song song hóa kiểm tra, chứ không phải push cuối cùng. Nếu phải chỉ rõ layer nào nằm “trên” layer nào, thì ID được tham chiếu phải tồn tại sẵn, nên có thể là vì vậy
  • Đây là một use case khá hay
    Cá nhân tôi chỉ dùng Nexus. Nó hoạt động đủ tốt và hỗ trợ từ OCI image đến package apt, các repository Maven tùy chỉnh, NuGet, npm, v.v. Tuy nhiên việc cấu hình và mức dùng tài nguyên hơi phiền, đặc biệt là phần chính sách dọn dẹp: https://www.sonatype.com/products/sonatype-nexus-repository
    Dù vậy, việc docker pull “chỉ đơn giản” là một loạt request HEADGET thật sự rất tuyệt. Tôi muốn thấy nhiều công nghệ hơn đưa ra những quyết định hợp lẽ như thế này: dùng nguyên thứ vốn đã hoạt động tốt từ lâu, đừng làm nó phức tạp vô ích. Thật ngạc nhiên là không có nhiều kho container đơn giản hơn có cả xác thực và chức năng dọn dẹp. Nexus và Harbor đều khá phức tạp để dùng trong thực tế

    • Tôi chỉ dùng Gitea cho package. Nó xử lý Docker, npm, Python, v.v.
      Tôi ngạc nhiên là không ai trong thread này nhắc đến nó
  • Distribution của CNCF, trước đây là Docker Registry, có tính năng hỗ trợ registry bằng URL ký CloudFront lấy từ S3 [1]
    https://distribution.github.io/distribution/storage-drivers/...

  • Tôi tò mò không biết https://github.com/distribution/distribution có vấn đề gì

    • Trước đây tôi chưa thấy cái này, và đúng là nó hỗ trợ S3. Nhưng tôi tò mò liệu nó phục vụ download trực tiếp từ S3 cho client, hay chỉ dùng S3 làm backend lưu trữ của chính nó, để khi pull thì về thực chất hoạt động như một proxy
  • Cách này trông có vẻ rất đắt, và có lẽ bài viết nên đề cập cả chi phí. Tôi tò mò về cả S3 lẫn R2

    • Tầng S3 Standard, xét theo chi phí trên mỗi GB lưu trữ, chỉ bằng một phần năm ECR
      Chi phí lưu lượng đi ra Internet miễn phí thì như nhau, nhưng kho ECR công khai có ngoại lệ là miễn phí lưu lượng đi ra khi sử dụng nội bộ trong AWS
    • Chi phí rốt cuộc vẫn là chi phí S3. Tùy theo region và tầng lưu trữ, chi phí lưu trữ trên mỗi GB, chi phí GET/PUT, và chi phí băng thông có thể xem trên trang web AWS: https://aws.amazon.com/s3/pricing/
  • Ngoài công cụ phát triển ra thì tôi không dùng Docker nhiều, nhưng tôi chưa từng hiểu vì sao cần tồn tại container registry riêng tư
    Nó chỉ trông như một dạng thu địa tô. Tôi tò mò thực tế nó có ưu điểm gì so với việc tự tạo một thứ như file image do mình quản lý rồi dùng theo ý muốn

    • Không nhất thiết phải dùng. Có thể dùng docker savedocker import
      docker save alpine:3.19 > alpine.tar
      docker load < alpine.tar
      Nhưng giờ bạn phải quản lý file tar đó, và mọi hệ thống đều phải biết vị trí cũng như cách truy cập nó. Hoặc không cần phát minh lại bánh xe, cứ dùng cách Docker đã cung cấp sẵn
    • Rất có khả năng bạn có các image không muốn công khai với cả thế giới. Những image đó thường cần được truy cập từ hạ tầng như cụm k8s hoặc runner CI/CD
      Vì vậy bạn phải tự dựng registry hoặc trả tiền cho ai đó vận hành hộ. Tất nhiên nếu chỉ dùng image cho phát triển thì những thứ này đều không có ý nghĩa, chỉ cần lưu image trên máy dev là được
    • Cũng giống lý do ta không gửi file qua email thay cho kho mã nguồn
      Vì cần một kho trung tâm có đầy đủ các phiên bản trước và nhiều bên tiêu thụ có thể truy cập dễ dàng. Sau khi build ứng dụng, bạn không muốn đẩy thủ công nó vào mọi nơi có thể chạy. Chỉ cần build một lần, push lên kho trung tâm, rồi để mọi nơi tham chiếu tới kho đó
      Cũng không nhất thiết phải trả tiền để host kho riêng tư. Có rất nhiều công cụ có thể tự host
    • Registry đám mây riêng tư rất hữu ích trong các dự án có yêu cầu xác thực/phân quyền bắt buộc liên quan đến Docker image
      Có thể cấu hình toàn bộ theo từng môi trường bằng Terraform, Bicep, Pulumi
    • Vấn đề còn là quản lý nó như thế nào. Nếu muốn dùng cùng bộ công cụ như với image công khai, chỉ cần vận hành một container registry
  • ECR thực sự trông như được thiết kế để có thể upload image layer thành nhiều phần
    Các API ECR liên quan gồm InitiateLayerUpload API, được gọi khi bắt đầu upload từng image layer; UploadLayerPart API, được gọi cho mỗi chunk của layer (tối đa 20MB); và PutImage API, dùng để push image manifest chứa tham chiếu tới image layer sau khi upload layer xong. Điều kỳ lạ là phải upload chunk của layer dưới dạng mã hóa base64, khiến dữ liệu tăng khoảng 33%

    • Tôi đã trực tiếp dùng API đó, và đáng tiếc là ở đó cũng yêu cầu upload theo thứ tự
  • Ý tưởng dùng bố trí đường dẫn file làm phương tiện kiểm soát endpoint khá thú vị
    Tuy nhiên tôi tò mò không biết sẽ xử lý header Docker-Content-Digest như thế nào. Nó không bắt buộc, nhưng được khuyến nghị đưa vào phản hồi; nhiều client kỳ vọng có header này và có thể từ chối layer không có header. Ngoài ra có thể sẽ bỏ lỡ các tính năng như referrers API trong đặc tả OCI 1.1. Có vẻ việc triển khai sẽ khá khó