+52

Từ server truyền thống tới Docker và scaling cùng K8s

Mở đầu

Bạn đã bao giờ thắc mắc là các máy chủ web hoạt động như thế nào, có khác với lúc mình chạy ở local không, rồi cài các phần mềm vào máy chủ ra sao?

Rồi người ta triển khai (deploy) và quản lý các hệ thống kiểu gì? Tại sao các website lớn lại có thể xử lý hàng triệu tới hàng tỷ request mỗi giây trong khi cấu hình vật lý của máy tính là có hạn?

Hãy cùng mình đi qua hành trình từ webserver, tới containerization, scaling horizontal và orchestration để có một cái nhìn tổng quan về cách các hệ thống hoạt động, cũng như tự tìm câu trả lời cho bản thân nhé.

⚠️ Cảnh báo: bài viết rất dài. Vui lòng ngồi xuống, uống ly bánh, ăn miếng nước và thong thả đọc.

First things first

Again, mình là Kiên Đinh (mà shipper vẫn hay gọi lộn thành Kiên Định 😦), một chàng developer thích viết, thích chia sẻ để có động lực học và nghiên cứu.

Hôm trước mình đã đề cập việc sử dụng docker để tạo nhanh 1 database postgres sử dụng cho việc test hiệu năng của connection pooling. Hôm nay mình sẽ giữ lời hứa viết 1 bài để cho những anh em còn chưa rõ về docker có thể có một cái nhìn tổng quan hơn.

Nếu đây là lần đầu bạn đọc bài viết của mình, thì xin giới thiệu đây là bài viết thuộc series backend nâng cao, chuỗi bài viết có hàm lượng kiến thức cao mà mình muốn chia sẻ cho mọi backend developer để có thể tiến sâu hơn về mặt technical.

Mô hình client-server

Giới thiệu

Từ lâu, khoảng những năm 80 của thế kỷ trước, một mô hình thiết kế phần mềm mang tên client-server bắt đầu xuất hiện, và nó còn phổ biến đến tận ngày nay. Không nói đâu xa, khi bạn đang lướt web để đọc bài viết này, thì trình duyệt của bạn cũng là một thành phần trong mô hình này.

Mình nhớ đâu đó năm lớp 10 hay lớp 11, trong môn tin học chúng ta đã được học về nó rồi, tuy nhiên chắc không còn ai nhớ, trả chữ cho thầy hết 💔.

01_client-server-diagram.png

Thành phần

Đúng như tên gọi, mô hình client-server có 2 thành phần:

  • Client-side: Hay còn gọi là phía khách, là một ứng dụng chạy ở máy tính của người dùng cuối (end-users). Ứng dụng này cung cấp giao diện để user tương tác, cũng có thể lưu trữ và xử lý một vài thông tin tạm thời (local-storage, cookies …). Giao tiếp với server bằng cách gửi yêu cầu, và nhận lại phản hồi.
  • Server-side: Phía máy chủ, hay ta quen gọi là server. Là phía xử lý yêu cầu từ client gửi lên, thực hiện các tác vụ logic và gửi lại phản hồi cho client. Phía server còn chứa thêm cả database, giúp lưu trữ thông tin của người dùng một cách lâu dài hơn.

Việc phân chia mô hình thành 2 phía này giúp cho việc xây dựng giao diện phía người dùng được dễ dàng hơn [1], client có thể là laptop, iphone, hoặc có thể là điện thoại android hay smart TV.

💡 Có thể bạn đã biết: Điện thoại (trừ cục gạch) và smart TV, thậm chí máy đọc sách cũng là một chiếc máy tính, vì nó có đầy đủ thành phần chính cấu tạo nên máy tính: Main Board; CPU; RAM; ROM; Ổ cứng;

Kiến trúc client-server không chỉ xuất hiện khi chúng ta duyệt web, mà trong máy tính của các bạn cũng có thể đang có vài phần mềm sử dụng nó ví dụ như:

  • X11 window system trên Linux
  • Language server, Gopls chạy trên VSCode
  • SSH, Git…

Web server

Đã đi qua định nghĩa về kiến trúc client-server rồi, hãy tiếp tục đi sâu vào web server, ta cần làm rõ nó trước khi đi tới các định nghĩa như containerization, orchestration mà mình đã đề cập.

Định nghĩa, giải ảo

Khi nhắc tới máy chủ, chúng ta thường lẫn lộn giữa server vật lý và phần mềm server, thực ra thì nói thế nào cũng đúng vì thực tế là nó như vậy 🤦.

Server có thể là:

  • Một ứng dụng có nhiệm vụ tiếp nhận requestgửi về response cho client (phần mềm server - application server).
  • Một máy tính có chạy phần mềm server.

Các phần mềm server nổi tiếng chúng ta có là: Apache, Tomcat, Nginx, IIS… Những phần mềm này hỗ trợ môi trường chạy cho các ngôn ngữ lập trình như PHP, ASP.NET, Python… Ở đây các ngôn ngữ như PHP đảm nhận nhiệm vụ xử lý logic.

Vậy có một câu hỏi: Không cần các phần mềm như IIS hay Nginx thì có thể khởi chạy 1 máy chủ hay không?

Với đa phần ngôn ngữ hiện nay, ta có thể chạy luôn mà không cần application server nhưng có thể cần thêm runtime engine, điển hình như Nodejs, .NET [2] hoặc là Go v.v. Còn với vài ngôn ngữ thông dịch như PHP thì không được (làm được nhưng cồng kềnh, không ai làm).

Điều kiện là code của chương trình phải lắng nghe 1 port nào đó (port mặc định là 80), lúc này đây chương trình đó giữ luôn vai trò application server.

Xem hình ví dụ sau để biết sự khác biệt giữa application server và server vật lý cũng như chạy trực tiếp app:

Với hình trên, máy chủ vật lý của kiendinh.space được cài PHP và phải chạy thông qua Nginx. Còn máy chủ của stackoverflow.com thì không cần, mà chạy trực tiếp process Golang. (Giống như bạn chạy yarn start -H 0.0.0.0).

Ngày nay các server không chỉ đơn thuần là webserver nữa, mà có thể là server cho nhiều dịch vụ khác.

Một webserver hoạt động thế nào?

Chúng ta biết rằng mỗi một máy tính trên mạng internet đều có một địa chỉ IP. Do đó theo lý thuyết tất cả mọi người trên thế giới đều có thể gửi request tới router wifi nhà bạn, hay thậm chí chiếc máy tính cá nhân của bạn.

Chúng ta hãy xem IP như địa chỉ nhà riêng vậy.

Bây giờ, ta có thể đưa chiếc máy tính bình thường trở thành một server web với những bước sau:

  • Có một địa chỉ IP tĩnh (gói cước đắt hơn của nhà mạng có cung cấp). Chứ nếu không sẽ giống như việc bạn đổi địa chỉ nhà mỗi tuần 1 lần, sẽ nhiều người tìm không ra nhà bạn.
  • Tắt tường lửa cho các truy cập từ bên ngoài.
  • Export cổng của server đang lắng nghe (thường là 80 cho HTTP và 443 cho HTTPS).

Client có thể truy cập trực tiếp tới IP của máy chủ, hoặc thông qua tên miền (domain name). Để biết domain nào trỏ vào IP nào, thì client phải truy cập vào DNS (Domain Name Server) để tìm (DNS xem như danh bạ điện thoại, không cần nhớ số, chỉ cần nhớ tên).

Hãy giả định như ta đang có tên miền kiendinh.space, thì trình duyệt (client) sẽ có flow từng bước như sau:

Việc deploy truyền thống

Tới đây thì ta đã rõ ràng server cũng chỉ là một chiếc máy tính như bao máy tính khác (nhưng cấu hình mạnh hơn). Thì việc deploy ứng dụng trước đây diễn ra thế nào?

Ta có thể dễ dàng đoán được, việc này cũng giống như cài các công cụ cần thiết cho việc code ở phía local vậy:

  • Setup một máy chủ vật lý hoặc VPS (máy chủ ảo), mua luôn về nhà hoặc đa phần thuê ở đâu đó.
  • Cài hệ điều hành cho máy (Windows server hoặc Linux).
  • Truy cập vào server bằng SSH hoặc RDP nếu là windows server (nếu server do mình quản lý thì mình mở ra chạy luôn như bình thường).
  • Cài đặt các phần mềm cần thiết: Git, PHP, Nginx, Java, .NET framework, Database (nếu có) v.v.
  • Cập nhật bản code / app mới nhất và chạy command.

Thực là lắm rắc rối và phiền hà. Các công ty lớn chắc là người ta sẽ tối ưu những công việc trên bằng các shellscript, nhưng vẫn rất là bất tiện.

💡 Các ngôn ngữ như PHP có thể không cần chạy thẳng trên VPS mà đa phần chỉ chạy trên hosting, sẽ đỡ phức tạp và tiết kiệm hơn nhiều. Hosting chứa phần mềm quản lý đã được cài đặt sẵn, giúp tối ưu cho việc deploy.

Bởi vì việc deploy phức tạp và tốn thời gian như vậy, mà đặc thù của môi trường development thì thay đổi liên tục, nên chúng ta cần vài một biện pháp tối ưu hơn, và thế là người ta dùng ảo hóa và containerization.

Ảo hóa và Container hóa

Docker đúng thực là một công cụ giúp ích rất nhiều trong việc thiết lập môi trường cho hệ thống. Như bài trước mình chỉ cần vài bước đơn giản đã có thể chạy postgres ở local. Lúc trước khi còn đi học, mỗi lần cài lại win là mình tốn hơn 30p tới 1 tiếng để cài MSSQL Server 🥲.

Nhưng trước khi nói tới Docker và cơ chế containerization (container hóa, dung khí hóa), thì ta nên tìm hiểu về cơ chế ảo hóa trước, để hiểu tường tận lý do nó ra đời và lợi ích containerization mang lại.

Cơ chế ảo hóa - virtualization

Nếu từng là sinh viên IT thì bạn đã cài máy ảo không chỉ 1 lần rồi đúng không? Hoặc cài bluestack để chơi game android trên PC thì cũng là một dạng máy ảo.

Xem hình mô tả cơ chế ảo hóa [3]:

Khi deploy ứng dụng web với cơ chế ảo hóa, chúng ta có vài lợi ích sau:

  • Tận dụng tối đa sức mạnh phần cứng.
  • Cô lập các ứng dụng khác nhau trên các VM khác nhau, anh A làm việc không phụ thuộc vào anh B.
  • Chạy máy ảo vẫn sẽ nhanh hơn là cài OS thủ công và cài phần mềm ứng dụng vào đó.

Vì những ưu điểm trên, nên các nền tảng cloud, cho thuê server áp dụng ảo hóa để bán nhiều máy chủ ảo (VPS) trên cùng 1 máy vật lý để tối ưu doanh thu.

💡 Các nhà cung cấp server thường có 2 lựa chọn cho máy chủ riêng: VPS và Dedicated server. VPS chính là máy ảo, dùng chung cùng một máy vật lý với các VPS khác, còn Dedicated server là một máy vật lý riêng biệt.

Vậy điểm yếu của cơ chế này khi dùng để phát triển phần mềm là gì?

  • Thứ nhất, vẫn tốn thời gian để khởi chạy một VM.
  • Ảo hóa tốn resource (RAM, Storage), hiệu năng app chạy trên VM không tốt như chạy trực tiếp.
  • Vì kích thước của các máy ảo lớn (hàng GB), nên việc backup và nhân bản (clone) khá bất tiện.

Tại sao cơ chế ảo hóa lại có những điểm yếu trên? Đáp án là bởi vì bản thân mỗi một VM đều phải chứa luôn 1 hệ điều hành (OS), là windows hoặc họ Linux, mà đã vậy thì số lượng công việc và tài nguyên nó chiếm dụng sẽ là một số lượng lớn.

Vì vậy, cơ chế containerization tối ưu hơn ra đời đã khắc phục được 2 nhược điểm trên.

Cơ chế containerization

Mặc dù cơ chế container hóa đã có từ lâu, nhưng phải tới lúc Docker xuất hiện thì nó mới phổ biến.

Năm 2010, 1 startup mang tên Docker Inc ra đời, và sản phẩm của họ chính là một công cụ hỗ trợ container hóa - Docker, điều đặc biệt là Docker là một phần mềm open-source, và được viết bằng Go 😎.

Và đây là hình minh họa so sánh conainerization và virtualization [4]:

Nhìn vào hình mô tả trên, ta có thể dễ dàng nhận ra kiến trúc của Docker / container đã được bớt đi 1 layer, chính là layer Guest OS (VM OS). Bởi vì Docker Engine sử dụng chung luôn Kernel của máy chủ vật lý. Điều này khiến cho lượng tài nguyên mà cơ chế container hóa sử dụng ít tài nguyên hơn.

Nói một cách đơn giản, containerization chính là việc bạn đưa những gì mà application của bạn cần vào 1 cái thùng chứa, đóng gói nó lại thành 1 image (giống như ISO cài hệ điều hành vậy), tuy nhiên image này nhẹ hơn bởi vì không cần phải chứa kernel.

Docker

Hiện nay mình không biết có bao nhiêu công cụ hỗ trợ container hóa, nhưng các công cụ mà opensource thì chỉ có 2 cái phổ biến nhất: Docker và Podman.

Hãy xem câu chuyện này:

Bạn Nam bởi vì không biết tới bao lâu mới bán được 1 tỷ gói mè, nên chuyển qua mở tiệm bánh. Để mở tiệm bánh thì bạn cần chuẩn bị rất nhiều thứ: Công thức làm bánh, mua máy trộn bột, mua các thiết bị nhà bếp, mua máy đóng gói, mua sắm bàn ghế nội thất, trang trí decor, vân vân và mây mây, ti tỉ thứ việc.

Một hôm nọ, thành phố Nam ở xảy ra động đất, làm cửa tiệm hư hỏng nặng, thế là bạn phải dời qua vị trí mới. Bạn lại phải chuẩn bị lại các bước như trên, nhưng thành phố mới bạn không quen thuộc, tính riêng bộ bàn ghế thôi cũng đã có 7x7 49 loại khác nhau, không biết chọn loại nào cho phù hợp, vậy cho nên tốn cả tháng trời để khai trương trở lại.

Để lỡ có phải chuyển nhà lần nữa, Nam lưu lại một tài liệu hướng dẫn xây dựng, ghi chi tiết những thứ cần dùng cho một tiệm bánh luôn. Nam viết ra toàn bộ những bước mà lúc cửa tiệm cần vào tài liệu đó (Dockerfile):

FROM Nhà cấp 4 mái bằng
COPY Công thức làm bánh
RUN Lắp máy trộn bột ToshiSung EX203
RUN Mua bàn ghế 75x60x60
RUN Sơn tường màu xanh, ốp gạch màu trắng
RUN 77x49 bước....

~~ Khai trương ~~

Vậy là giờ đây, nếu có trường hợp gì đó không may như hỏa hoạn hay động đất, hoặc muốn nhượng quyền lại tiệm bánh, Nam chỉ cần đưa tài liệu kĩ thuật cho người ta, người ta chỉ cần dựa vào đó là xong, chỉ mất có vài ngày là đã có thể khai trương rồi!

Công cụ mà Nam sử dụng có thể xem như Docker.

Ưu điểm của Docker

Bên cạnh việc sở hữu điểm mạnh của cơ chế container hóa là tiết kiệm tài nguyên hơn ảo hóa nhờ dùng chung kernel với Host OS, Docker còn có một điểm mạnh ăn tiền chính là các image có thể được sử dụng lại (vẫn nhẹ hơn các file VM ảo hóa), và chúng ta có thể dùng dockerfile để quyết định image của application chạy có những phần mềm nào.

Ví dụ, nếu bạn muốn deploy một ứng dụng web PHP trên Ubuntu, thông thường bạn sẽ phải cài Nginx hoặc Apache lên ubuntu (1), sau đó cài đặt PHP composer (2), MySQL trên máy chủ (3), chưa kể đôi lúc phải chạy vài script để chmod, cấp quyền cho user (4) vân vân mây mây… Khá tốn thời gian.

Với Docker, việc này đơn giản hơn vì bạn có thể sử dụng lại các Docker image đã cài đặt sẵn PHP, Nginx, bạn chỉ cần cài Docker (1) và config lại file dockerfile (2) để copy lại code PHP vào image và chạy thôi.

💡 Đây chỉ là một ví dụ, thực tế sẽ không nên cài chung DB cùng 1 container với web app mà nên tách riêng ra 2 container. Còn môi trường production thì sẽ chạy DB riêng trên máy vật lý luôn, không đưa vào container.

Câu hỏi: Nếu sử dụng chung kernel, thì làm sao các máy tính windows có thể chạy được các image linux?

Trả lời: Với Docker ở các máy tính windows, bạn có thể chạy được cả 2 loại image Linux cũng như image Windows, lý do là bởi vì Windows các phiên bản mới có tính năng WSL. Tức là docker vẫn sử dụng kernel linux.

Ngược lại, bạn không thể chạy được image windows trên docker ở các máy chạy Linux, vì không có kernel Windows NT để share cho docker engine dùng [5].

Docker xử lý được vấn đề gì?

Docker xử lý được 2 điểm quan trọng:

  • Đảm bảo app của bạn chạy đúng như mong đợi trên các môi trường khác nhau (dev, UAT, PROD…). Bởi vì các package cài thêm, các script chạy đã được đóng gói sẵn vào image rồi.
  • Tối ưu quá trình, tiết kiệm thời gian thiết lập môi trường. Như các bạn thấy việc mình chạy một container postgresql hoặc MSSQL rất nhanh chỉ với vài thao tác, nhanh hơn nhiều việc cài bằng tay.

Thực tế cả 2 phương pháp containerization và virtualization vẫn được dùng song song hiện nay.

Cơ chế ảo hóa vẫn được dùng để bán các gói VPS cho bạn dùng (như AWS EC2), nhưng với deployment, người ta hầu hết sử dụng Docker hoặc tool containerization khác (Podman chẳng hạn).

Scaling và Kubernetes

Ở đầu bài mình có đề cập tới câu hỏi: “Tại sao các hệ thống lớn lại có thể xử lý được số lượng request lớn như vậy?”, trong khi ví dụ điển hình nhất là các server đăng ký học phần ở các trường ĐH chỉ có phục vụ mấy nghìn tới mấy chục nghìn sinh viên đã tèo rồi?

Đây là do các hệ thống lớn áp dụng scaling (có thể dịch scaling là mở rộng quy mô).

Scaling up và Scaling out

Quay trở lại câu chuyện kinh doanh của Nam.

Việc làm ăn của Nam ngày càng phát đạt, lượng khách mua bánh ngày càng nhiều khiến đôi lúc tiệm bánh quá tải, vì tối đa tiệm chỉ phục vụ được 100 khách hàng trong 1 giờ, khách đến sau sẽ phải chờ lâu.

Nam suy nghĩ có 2 phương án để phục vụ được nhiều khách hơn:

  • Mở rộng diện tích của cửa hàng (A), mua thêm bàn ghế, tuyển thêm nhân viên.
  • Mở thêm một nhà bếp (B) phía bên cạnh, và dùng một quầy order để nhận đơn hàng, quầy order sẽ quyết định nhà bếp A hay B sẽ tiến hành làm bánh cho đơn đó.

Tương tự trong thế giới máy tính, một máy chủ vật lý thì có sức mạnh hữu hạn, khi một server chịu tải đến một mức độ nào đó người ta sẽ có 2 lựa chọn để có thể xử lý được nhiều request hơn:

  • Nâng cấp cấu hình: CPU, RAM (scaling up hoặc vertical scale)
  • Gắn thêm một server nữa, sử dụng load balancer để 2 em cùng gánh lượng request lớn hơn (scaling out hay horizontal scaling).

Nhưng với docker thì chúng ta chỉ cần tạo thêm các container khác ở một máy tính khác là xong đúng không? Vậy làm sao chúng ta quản lý chuyện đó, quy định số lượng container là bao nhiêu vân vân…?

Chúng ta cần một orchestration tool để làm việc này. Phổ biến nhất chính là Kubernetes, được viết tắt là K8s. (Nếu dịch containerization theo Hán Việt là dung khí hóa, thì orchestration có thể dịch là phối khí 🤣).

Orchestration tool - Kubernetes

Hình dưới đây minh họa kiến trúc của kubernetes [6]:

K8s sẽ quản lý các worker, trong đó có các pod có thể được coi như là một application server thu nhỏ. Khi một pod nào đó bị crash do lỗi, thì có thể dễ dàng điều phối request qua pod khác để xử lý.

K8s cũng hỗ trợ tính năng auto-scale, tức là lượng request càng lớn, thì ta có thể tạo thêm nhiều pod để tăng tải.

Trong khi docker đảm bảo vấn đề chạy đúng trên các môi trường, thì K8s có vai trò đảm bảo các ứng dụng luôn chạy (tính có sẵn - Availability). Khi mỗi "server" trong K8s không còn đơn lẻ nữa là mà 1 cụm các pod.

Kết hợp giữa containerization và orchestration, chúng ta sẽ có một ứng dụng chạy ổn định, phục vụ được nhiều người dùng hơn.

Để có thể chạy k8s ở local, chúng ta có thể sử dụng kind. (Kinh nghiệm của mình thì RAM dưới 32GB chạy rất đuối khoản này 😄).

Thường thì việc setup và quản lý K8s sẽ là của devops, nhưng developer thì cũng nên hiểu cách nó hoạt động để có thể design được một hệ thống hoạt động trơn tru, và tất nhiên, lên trình, tăng lương, cưới vợ, mua nhà, mua xe 😆.

Wrapping up

Chắc là bạn đã ăn xong ly nước, uống xong miếng bánh trước khi đọc hết bài này rồi. Mục đích bài này là để giới thiệu một cách tổng quan, để mọi người có cái nhìn overview hơn về backend, để tự trả lời được những câu hỏi như scale up là gì? docker là gì? bla bla nữa.

Hi vọng sẽ giúp ích được các bạn, nhớ để lại bình luận nếu bạn có thắc mắc, feel free to ask!

Bài viết gốc: Từ server truyền thống tới Docker và scaling cùng K8s

Footnotes:

  1. https://madooei.github.io/cs421_sp20_homepage/client-server-app/
  2. https://learn.microsoft.com/en-us/aspnet/web-api/overview/older-versions/self-host-a-web-api
  3. https://documentation.suse.com/smart/virtualization-cloud/html/concept-virtualization/index.html
  4. https://www.netapp.com/blog/containers-vs-vms/
  5. https://stackoverflow.com/questions/42158596/can-windows-containers-be-hosted-on-linux
  6. https://www.cncf.io/blog/2019/08/19/how-kubernetes-works/

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí