Cách tôi vận hành máy chủ (2022)
(blog.wesleyac.com)- Các dịch vụ cá nhân được tự vận hành trực tiếp trên VM DigitalOcean 5 USD/tháng và Debian 10; với các dịch vụ quy mô nhỏ, cách cài phần mềm máy chủ lên VM cũng đủ đơn giản và hợp lý
- Ứng dụng máy chủ được tạo dưới dạng binary tĩnh Rust, giữ artifact triển khai chỉ là một file duy nhất, bao gồm cả HTML, CSS, cấu hình và secret
- Việc quản lý chạy dịch vụ do systemd đảm nhiệm; xử lý HTTPS được giao cho reverse proxy nginx và Let’s Encrypt/certbot để ứng dụng không phải trực tiếp xử lý TLS
- Các dịch vụ cần dữ liệu dùng một file SQLite duy nhất, kết hợp backup hằng ngày bằng Tarsnap và backup streaming lên DigitalOcean Spaces bằng Litestream
- Khi đặt nhiều dịch vụ trên một VM, cô lập bằng Unix user riêng cho từng dịch vụ; nếu cần bảo mật mạnh hơn thì chọn VM riêng, systemd-nspawn hoặc firejail
Cấu hình cơ bản cho máy chủ và triển khai
- Nhiều dịch vụ như thoughts.page, hanabi.site, cgmserver, phonebridge được vận hành gần với cấu hình này
- Ứng dụng chạy trên VM DigitalOcean, gói là tier 5 USD/tháng và hệ điều hành là Debian 10
- Một số dịch vụ dùng chung cùng VM, một số được tách sang VM khác
- Phần mềm máy chủ được viết bằng Rust
- Được liên kết tĩnh
- HTML, CSS, cấu hình, secret, v.v. được compile vào binary
- Sử dụng rust-musl-builder và rust-embed
- Với cách này, việc triển khai được đơn giản hóa thành thao tác copy một file duy nhất lên máy chủ
- Có thể áp dụng cách tương tự với Go, C++, v.v.
- Với các ngôn ngữ không dễ triển khai dưới dạng một binary duy nhất, có thể dùng Docker container làm artifact build thay thế
Chạy dịch vụ, proxy và lưu giữ dữ liệu
- Việc quản lý khởi động dịch vụ do systemd đảm nhiệm
- Đảm bảo binary chạy cùng khi máy chủ khởi động
- Hầu hết các file systemd unit là file đơn giản 9 dòng
- Bản thân systemd phức tạp, nhưng khi chỉ dùng để khởi động máy chủ lúc boot thì hầu như không phải đối mặt với phần phức tạp đó
- Triển khai được xử lý bằng simple deploy script
- Copy binary lên máy chủ và khởi động lại máy chủ
- Có thể rollback, và đảm bảo luôn có một phiên bản hợp lệ đang chạy ngay cả khi mất kết nối trong lúc triển khai
- Các chương trình cần cơ sở dữ liệu dùng SQLite
- Toàn bộ trạng thái ứng dụng nằm trong một file duy nhất
- Mỗi ngày tạo backup bằng lệnh SQLite
.backupvà lưu vào Tarsnap - Script backup được chạy bằng cron
- Dùng Litestream để stream bản sao cơ sở dữ liệu theo từng giây lên storage DigitalOcean Spaces, và tạo snapshot mỗi 6 giờ
- Tất cả máy chủ chạy phía sau reverse proxy nginx
- nginx xử lý TLS termination nên ứng dụng không cần bận tâm đến HTTPS
- Chứng chỉ HTTPS được cấp bằng Let’s Encrypt và certbot, đồng thời được tự động gia hạn
- Ví dụ cấu hình nginx của hanabi.site có trong gist riêng
- File tĩnh có thể được phục vụ bằng nginx, chỉ cần copy lên máy chủ bằng
scphoặcrsync
Bảo trì và cô lập dịch vụ
- Cấu hình này hướng tới một đường vận hành đơn giản và vững chắc
- Ngoài bản thân ứng dụng, các phần mềm trên đường phục vụ đều là những thành phần đã được kiểm chứng qua hàng chục năm sử dụng
- Miễn là tiếp tục trả phí DigitalOcean, về bản chất việc vận hành site gần như không cần bảo trì
- Vấn đề duy nhất mà hệ thống monitoring phát hiện là sự cố mạng DigitalOcean tạm thời
- Đôi khi cần cập nhật hệ điều hành và bảo mật
- Các bản phát hành Debian được hỗ trợ 5 năm, nên khoảng 2 năm rưỡi nữa sẽ phải nâng cấp lên Debian 11
- Nếu lại xảy ra sự kiện như Heartbleed thì sẽ cần vá lỗi
- Những sự kiện như vậy khá hiếm
- Chi phí dùng VM 5 USD/tháng cho từng dịch vụ có thể là gánh nặng, nhưng có thể chạy nhiều dịch vụ trên cùng một VM
- Việc cô lập được cung cấp bằng cách tạo tài khoản Unix user riêng cho từng dịch vụ
- Cách cô lập này đã tồn tại từ những ngày đầu của Unix nên có vẻ vững chắc
- Nếu cần cô lập mạnh hơn, có thể dùng systemd-nspawn hoặc firejail
- Nếu thật sự quan trọng về mặt bảo mật, thì trả thêm 5 USD/tháng và chạy trên VM riêng
- Quy trình thiết lập dự án mới khá ngắn
- Tạo user mới
- Thêm virtual host nginx và cấp chứng chỉ HTTPS bằng certbot
- Thêm systemd unit
- Commit deploy script vào repository và chạy
- Ban đầu có nhiều thứ phải học, nhưng hạ tầng này thay đổi chậm hơn hạ tầng cloud rất nhiều
- Định dạng cấu hình nginx về cơ bản gần như không đổi trong 10 năm qua
- Thay đổi lớn gần nhất trong quản trị hệ thống Debian là việc chuyển sang systemd gần 10 năm trước
- Ít bị phơi nhiễm trước tình huống nhà cung cấp cloud khai tử dịch vụ hoặc âm thầm thay đổi hành vi
- Phụ thuộc duy nhất là nhà cung cấp VPS, và máy chủ là một commodity phổ dụng nên có thể chuyển sang nhà cung cấp khác
1 bình luận
Ý kiến Hacker News
Nếu chạy mỗi dịch vụ bằng một người dùng Unix riêng để cách ly, tính năng DynamicUser của systemd có thể giúp tiết kiệm thời gian
Nó sẽ cấp phát UID và tạo thư mục log/trạng thái với quyền phù hợp
https://0pointer.net/blog/dynamic-users-with-systemd.html
Chỉ cần đặt một tệp văn bản nhỏ trong
/etc/sysusers.d/chứa thông tin như tên người dùng, thư mục home, rồi chạy lệnh hoặc dịch vụsysusersDạo này tôi thích nhất là hàm đám mây kích hoạt bằng HTTP
Nếu cẩn thận tránh các bẫy riêng của từng nhà cung cấp, nó giảm độ phức tạp đi rất nhiều, và là lớp trừu tượng cloud-native duy nhất cho tôi cảm giác như “hình thái cuối cùng”
Chỉ riêng một ứng dụng tôi đã triển khai hơn 2000 lần, mà chưa từng có lần nào môi trường thực thi bị hỏng đến mức phải liên hệ đội hỗ trợ hay gõ các lệnh console khó hiểu
Hiệu năng cũng rất tốt theo tiêu chuẩn App Service plan cô lập của Azure, và sự phản đối mang tính ý thức hệ với mô hình tính phí theo từng request giờ cũng không còn nữa
Sở hữu cả bất động sản đặt máy chủ thì có thể rẻ hơn, nhưng để tự triển khai những thuộc tính như tuân thủ và kiểm toán mà một hàm HTTP đơn giản mang lại, trên thực tế gần như phải lập ra một công ty khổng lồ và tuyển cả đống người
Lập luận về phụ thuộc nhà cung cấp cũng không còn thuyết phục tôi nữa. Giao diện nhận HTTP request và trả về HTTP response là rất tự nhiên; khác biệt giữa các nhà cung cấp chủ yếu chỉ là ngữ cảnh phụ như method signature của trigger hay các claim OIDC/SAML, nên nếu muốn tạo ra một cấu trúc không thể refactor sang nhà cung cấp khác trong vòng một tuần thì có khi còn phải cố tình làm vậy
Tôi hiểu là với blog cá nhân, mua một VM của Hetzner rồi chăm chút như thợ thủ công sẽ vui hơn. Nếu là ngành hoàn toàn không bị quản lý, thì việc cân đo biên lợi nhuận và độ phức tạp kỹ càng thay vì thuê ngoài toàn bộ máy chủ cũng là một quan điểm có thể được bảo vệ mạnh hơn
Không hiểu họ nghĩ trước chuyển dịch lên đám mây thì mọi người đã làm thế nào
Hiện tôi đang phụ trách nhiều team trong một công ty công nghiệp lớn và mở rộng, triển khai các ứng dụng đổi mới trên Azure, nhưng chi phí đám mây thì lớn đến vô lý so với số người dùng
Ngày trước chúng tôi từng vận hành các ứng dụng có số người dùng gấp 10 lần với chi phí chỉ bằng 1/100 và độ phức tạp còn thấp hơn
Khoản chi này được tính sang các bộ phận khác đã đưa ra những lựa chọn đáng ngờ như vậy nên cá nhân tôi không bị ảnh hưởng, nhưng tôi khó hiểu thái độ mù quáng tin vào đám mây
Với chúng tôi, tự vận hành phần cứng là lựa chọn tốt nhất, và do mức độ bảo mật kiểu không được chụp ảnh trong văn phòng, khả năng giao bất cứ thứ gì cho nhà cung cấp đám mây là 0%
Nhưng để ứng dụng trở nên hữu ích thì còn cần những thành phần khác như cơ sở dữ liệu, và chẳng phải chi phí sẽ tăng ở đó sao?
Không biết đó là request handler của máy chủ HTTP, điện toán đám mây dạng function-as-a-service, hay chỉ là RPC kiểu cũ
Nếu có script kiddie nào đó bắt đầu gửi request 10 lần mỗi phút, thì sẽ mất bao lâu để chi phí của hàm vượt qua VPS?
Tôi dùng một cấu hình tương tự cho website cá nhân và các site dự án
VM Linode tôi dùng là gói 5 USD/tháng, chạy Debian GNU/Linux, và phần mềm được viết bằng Common Lisp
Website cá nhân hay blog thì tôi tạo site tĩnh bằng chương trình Common Lisp, còn dịch vụ trực tuyến hay web app thì được làm bằng chương trình Common Lisp dùng Hunchentoot để xử lý và phản hồi HTTP request
Tôi dùng file unit systemd để site/dịch vụ tự động khởi chạy khi VM khởi động hoặc khởi động lại; đa số chỉ khoảng 10–15 dòng
Cấu hình ban đầu của VM được mã hóa bằng shell script: https://github.com/susam/dotfiles/blob/main/linode.sh
Cấu hình theo từng dự án và từng dịch vụ được quản lý bằng Makefile riêng: https://github.com/susam/susam.net/blob/main/Makefile, https://github.com/susam/mathb/blob/main/Makefile
Tôi không dùng container. Các site này đã chạy nhiều năm từ trước khi container trở nên phổ biến, và đến giờ thì script khởi tạo cùng Makefile vẫn là đủ
Tôi cũng dùng Nginx. Nó phục vụ file tĩnh và, khi có dịch vụ backend, đóng vai trò reverse proxy; ngoài việc kết thúc TLS, nó còn có lợi ích như giới hạn tốc độ request hay cấu hình danh sách cho phép HTTP header để bảo vệ backend
Trong playbook cá nhân nhỏ của tôi có vài lệnh như
curl LINK -o linode.sh && sh linode.sh,git clone LINK && cd PROJECT && sudo make setup httpsCác target của
makexử lý những việc cần thiết để chạy site như cài đặt công cụ Nginx, certbot, sbcl, cấu hình Nginx, cấu hình chứng chỉ, v.v.; khi lệnh kết thúc thì website sẽ được public ngayNhìn vào Makefile thì có vẻ là SBCL, nên tôi cũng muốn biết việc dùng bộ nhớ có từng là vấn đề không
Tôi đang dùng VPS 512MB RAM, mà một instance SBCL đã ngốn khoảng 100MB nên chỉ có thể chạy đồng thời vài dịch vụ
Tôi từng nghĩ đến việc chuyển các dịch vụ có lưu lượng thấp nhất sang CLISP, nhưng nó lại thiếu một tính năng tôi đang dùng là package-local aliases
Sau vài năm liên tục tinh chỉnh cấu hình, giờ đã ổn định với việc chạy mọi thứ trong container Docker
Dùng docker-compose thay cho systemd làm bộ điều phối, và dùng Caddy thay cho nginx làm proxy
Giống bài gốc, tôi cũng viết script triển khai cho từng dự án cần chạy nên nhìn chung khá tương tự
Một trong nhiều ưu điểm của Docker là có thể chạy cả phần mềm bên thứ ba với cùng một cấu hình
Tôi đã dùng cấu hình này nhiều năm rồi và thấy rất tốt; giống như bài gốc, nó vừa vững chắc vừa đủ linh hoạt để thay đổi theo ý muốn khi cần
Điểm đau duy nhất hiện tại là triển khai rolling. Khi phần mềm lớn dần, việc mỗi lần triển khai đều có vài giây downtime đang trở thành vấn đề, và tuy chưa có lời giải đơn giản, có lẽ docker swarm là hướng đi đúng
Để giảm downtime, có thể thử
health_uri /health,lb_try_duration 30sVí dụ, nếu cấu hình như
reverse_proxy api:8089 { health_uri /health lb_try_duration 30s }, Caddy sẽ đệm các yêu cầu trong lúc triển khai phiên bản mới và cho dịch vụ mới tối đa 30 giây để khởi độngLý tưởng nhất là sau khi phiên bản mới đạt trạng thái khỏe mạnh, Caddy bắt đầu dùng nó rồi mới giết container cũ
Tôi đã xem https://github.com/Wowu/docker-rollout và https://github.com/lucaslorentz/caddy-docker-proxy nhưng vẫn chưa ưu tiên được việc này
Không hoàn hảo, nhưng thay vì trình duyệt nhận lỗi kết nối thì nó sẽ chỉ tải trong vài giây
https://mrsk.dev/ cũng dùng cùng kỹ thuật này
Production khởi đầu bằng compose, rồi sau đó được cải tiến thành một pipeline triển khai liên tục tự động nâng cấp stack
Khi công ty và tệp người dùng tăng lên, vấn đề downtime khi khởi động lại hoặc triển khai bắt đầu xuất hiện, và mỗi khi muốn chạy thêm ứng dụng mới lại phải chuẩn bị một môi trường triển khai mới
Tôi từng sợ đến ngày phải dùng Kubernetes, vì đã tận mắt thấy độ phức tạp của nó và không muốn dành phần lớn một ngày chỉ để dỗ dành cluster
Vì vậy chúng tôi chuyển sang chế độ Swarm, và đó là một hành trình lúc thì Jekyll, lúc thì Hyde
Có những bug không ai muốn sửa, một số phần của đặc tả Docker không được triển khai nhưng cũng chẳng ai báo trước, những lựa chọn triển khai khiến bạn chỉ muốn bứt tóc, và cảm giác như nhân viên Docker Inc không nói chuyện với nhau hoặc không thể tập trung đến cùng
Dù vậy, nó cũng có nhiều mặt đẹp. Stack compose vẫn hoạt động nguyên vẹn và có thể mở rộng đúng chỗ cần; nếu cấu hình đúng thì triển khai không gián đoạn, nâng cấp, cân bằng tải và rollback đều chạy tốt
Raft cũng đáng tin cậy trong việc duy trì cluster như ở những nơi khác, và chỉ cần bỏ thêm chút công sức là có thể xây dựng một nền tảng tự phục vụ linh hoạt, an toàn, tự động phân tán với chỉ một phần ngân sách bảo trì K8s
Tuy vậy, cần chuẩn bị để làm script triển khai cho tử tế. Tôi đã tốn khá nhiều thời gian để viết một công cụ Python chuyển file compose hợp lệ theo đặc tả Docker sang đặc tả Swarm, làm mới và dọn dẹp secret, rồi mở rộng biến môi trường
Tùy nhà cung cấp VPS, bạn cũng phải bảo đảm cấu hình đúng MTU mạng. Việc này chắc đã làm tôi giảm tuổi thọ kha khá
Ít nhất là cho đến khi số lượng khách hàng hoặc người dùng tăng đủ để vấn đề mở rộng thực sự xuất hiện
docker savevàdocker importTôi tò mò không biết bạn có tự vận hành registry không
Trên máy chủ vật lý, tôi dùng Podman Pod để đặt cơ sở dữ liệu PostgreSQL và ứng dụng vào trong đó, tất cả đều chạy trên các cổng localhost lớn hơn 5000, còn Caddy hoạt động như một reverse proxy trên cổng 443
Dùng systemd Timer để dump toàn bộ cơ sở dữ liệu vào một thư mục mỗi ngày lúc 4:55 chiều
Sau đó DejaDup [1] tự động sao lưu
$HOMEsang HDD gắn ngoài mỗi ngày lúc 5 giờ chiều; loại trừ file cache nhưng bao gồm cả bản dump cơ sở dữ liệuHệ điều hành là Debian cài GNOME Core [2], và các quy tắc firewalld chỉ cho phép 80, 443 và cổng SSH tùy chỉnh
SSH dùng khóa và đã tắt xác thực bằng mật khẩu
Đây là cách làm nhàm chán nhất, nhưng nó cứ thế mà hoạt động tốt
1 - https://flathub.org/apps/org.gnome.DejaDup
2 - https://packages.debian.org/bookworm/gnome-core
Dạo này tôi cài Dokku lên một máy chủ duy nhất để dùng
Quản lý dễ, lập trình viên có thể triển khai chỉ bằng
git pushtheo kiểu Heroku, và có rất nhiều plugin nên việc thêm cơ sở dữ liệu hoặc cấu hình HTTPS cũng chỉ mất tối đa 10 giâygit pushthực sự rất tiệnViệc thêm ứng dụng, thêm cơ sở dữ liệu, quản lý biến môi trường và quản lý domain đều cực kỳ trực quan
Tôi thích cấu hình đơn giản nên cách này có vẻ khá thú vị để làm việc
Có khả năng là đủ cho 99% dịch vụ
Có lẽ tôi sẽ dùng Ansible cho cấu hình máy chủ và script triển khai
Như vậy máy chủ có thể được tài liệu hóa và script triển khai cũng có thể đơn giản hơn
Tuy vậy, tôi sẽ không tránh việc đăng nhập trực tiếp vào máy chủ để debug hoặc kiểm tra
Script của tôi gần như không thay đổi suốt 7 năm qua, trong khi Ansible chậm và có xu hướng buộc phải duy trì nhiều tệp liên tục
Về chuyện debug, cũng có thể dùng nginx để mở log ứng dụng qua một endpoint được bảo vệ bằng mật khẩu
Có vẻ mọi người đôi khi quên rằng ngay cả trước thời cloud, CI/CD và quản lý máy chủ hiệu quả cũng đã là thực tiễn phổ biến
Về cách viết phần mềm máy chủ bằng Rust, liên kết tĩnh rồi biên dịch HTML, CSS, cấu hình, secret... tất cả vào trong binary, gần đây tôi đang làm như vậy với Go và cực kỳ thích vì nó khiến việc viết và triển khai phần mềm phụ thuộc vào tệp tĩnh trở nên rất dễ dàng
Dùng biến môi trường hoặc cấu hình cho secret gần với chuẩn hơn, và dù có thêm một bước trước khi chạy, nó giúp tránh tình huống chia sẻ binary rồi quên mất bên trong có secret
Giải nén binary ra để tìm chuỗi là việc khá dễ
Còn việc gói kèm asset frontend tĩnh và cấu hình mặc định thì là một lợi thế lớn
Cross-compilation với Rust, đặc biệt từ Mac sang Linux, tương đối đau đầu, trong khi với Go thì rất đơn giản và đã được tích hợp sẵn trong công cụ
goNó thực sự giúp giảm ma sát khi hoàn thành và triển khai các side project
Nhưng tôi có một câu hỏi. Trong bài có nói “nhận chứng chỉ Let’s Encrypt bằng certbot và cũng xử lý tự động gia hạn”, còn tôi thì dùng cấu hình đa vùng liên khu vực với hai máy chủ và geo-DNS, nên certbot chỉ chạy trên máy chủ ở Mỹ và tôi phải sao chép thủ công chứng chỉ sang máy chủ ở châu Âu
Có cách nào giải quyết việc này không?
Phần mô tả về ClickHouse Playground ở đây: https://ghe.clickhouse.tech/
Tôi phục vụ cả ứng dụng web từ cùng một dịch vụ biên dịch tĩnh đang cung cấp backend API
Chạy
npm buildtrong CI rồi nhúng kết quả vào giúp việc test local hoặc dựng demo instance trở nên cực kỳ dễ dàng