Sáu trạm, sáu cách chết: hành trình sống còn của một HTTP request

Lời mở: tại sao ta cần hiểu con đường này?
Có những đêm, ta ngồi trước màn hình lúc hai giờ sáng, nhìn một dòng timeout lạnh lùng, và tự hỏi: thằng request bé nhỏ này đã chết ở đâu, mà chết êm đến mức không kịp để lại một dòng log nào?
Alert nổ giữa đêm. API trả về timeout. Tôi mở log của service lên sạch bong. Không một dòng error, không một stack trace. Cứ như request gửi đi rồi tan vào hư không, chưa bao giờ chạm được tới nơi code thật sự chạy.
Khoảnh khắc ấy dạy tôi một điều mà sau này thành nguyên tắc sống còn: muốn debug được một hệ thống phân tán, ta phải hiểu con đường vật lý mà request đi qua không phải con đường ta tưởng tượng trong đầu.
Ta hay vẽ trong đầu một mũi tên thẳng: client → server. Nhưng thực tế, giữa hai đầu mũi tên ấy là cả một dãy cửa ải. Mỗi cửa có một người gác cổng với luật lệ, trí nhớ, và tính cách riêng. Request không "bay thẳng" nó được chuyển tiếp, được kiểm tra, được dịch địa chỉ, được cân tải, qua từng chặng một. Và ở mỗi chặng, nó có thể chết theo một cách hoàn toàn khác nhau.
Bài viết này là nơi tôi note lại toàn bộ hiểu biết về con đường ấy. Kiến trúc cụ thể ta mổ xẻ:
[Client] → [DNS] → [nginx (Allow List)] → [APISIX] → [NodePort] → [Pod backend]
Một chi tiết quan trọng cần ghim ngay từ đầu: trong kiến trúc này, cả nginx lẫn APISIX đều nằm NGOÀI cluster Kubernetes. Cluster chỉ phơi ra thế giới bên ngoài đúng một cánh cửa NodePort. Vì sao lại thiết kế như vậy, và nó kéo theo những hệ quả gì, ta sẽ thấy rõ ở trạm 5.
Trạm 1 : Client: hành trình bắt đầu trước cả khi byte đầu tiên rời máy
Chuyện gì thực sự xảy ra
Cú "à há" đầu tiên: khi bạn gọi một API, request không lập tức bay đi. Nó phải trải qua một chuỗi chuẩn bị mà phần lớn diễn ra ngay trên máy của bạn.
Bước 1 : Phân giải tên. Trước tiên client phải biết api.example.com ứng với IP nào. Nó hỏi tầng DNS (trạm 2). Nhưng trước khi hỏi ra ngoài, nó lục cache local đã.
Bước 2 : Thiết lập kết nối TCP. Có IP rồi, client mở một kết nối TCP qua "bắt tay ba bước" (SYN → SYN-ACK → ACK). Đây là lý do mỗi kết nối mới đều tốn một vòng khứ hồi mạng (round-trip) chỉ để chào hỏi, trước khi gửi được dữ liệu thật.
Bước 3 : Bắt tay TLS (nếu HTTPS). Với HTTPS, sau khi có TCP còn thêm một nghi thức nữa: trao đổi chứng chỉ, thỏa thuận thuật toán mã hóa, sinh khóa phiên. Tốn thêm một tới hai round-trip nữa.
Tại sao lại có keep-alive và connection pool?
Vì ba bước trên đắt. Nếu mỗi request đều bắt tay TCP rồi bắt tay TLS lại từ đầu, độ trễ sẽ kinh khủng. Nên client (và mọi proxy về sau) đều dùng keep-alive: giữ kết nối mở sau khi request xong, để request sau tái dùng. Một "connection pool" là một rổ các kết nối đang mở sẵn như vậy.
Đây cũng chính là mầm mống của một loại bug tinh vi: kết nối ôi thiu (stale connection). Server bên kia âm thầm đóng kết nối (timeout, restart), nhưng client chưa nhận được tín hiệu đóng. Nó vẫn tưởng kết nối còn sống, gửi request vào cái ống đã chết, rồi ngồi đợi tới timeout.
Nơi hay chết: Stale connection trong pool. Đặc biệt hay gặp sau khi backend restart mà client/proxy giữ kết nối quá lâu.
Vũ khí tại trận:
curl -v https://api.example.com/
# Đọc kỹ dòng "Re-using existing connection" (đang tái dùng)
# vs "Trying <ip>... Connected" (mở kết nối mới)
Trạm 2 : DNS: tại sao "đổi IP rồi mà vẫn vào server cũ"?
DNS làm gì, và tại sao nó phải cache
DNS dịch tên miền thành IP. Nghe đơn giản, nhưng hệ thống DNS toàn cầu phục vụ hàng nghìn tỷ truy vấn mỗi ngày. Nếu mọi truy vấn đều phải đi tới tận máy chủ gốc, hệ thống sẽ sụp. Nên DNS được thiết kế để cache ở mọi tầng và đây là cốt lõi để hiểu mọi bi kịch của nó.
TTL (Time To Live) là con số mà bản ghi DNS tự khai: "hãy nhớ tôi trong N giây, đừng hỏi lại trước khi hết hạn." Mỗi tầng cache tôn trọng con số này theo cách riêng.
Tại sao đổi IP không có hiệu lực ngay?
Hình dung bạn đổi IP backend lúc 14:00, với TTL = 300 (5 phút). Chuyện gì xảy ra?
Cache của hệ điều hành client: vẫn giữ IP cũ tới khi TTL hết.
Resolver của ISP: cũng giữ bản sao riêng, hết hạn vào một thời điểm khác (vì nó cache lúc khác).
CDN, proxy trung gian: lại một bản sao nữa.
Kết quả: trong suốt cửa sổ TTL, một phần lưu lượng đi vào IP mới, một phần vẫn chảy về IP cũ cái server bạn vừa tắt. Đây là lý do lỗi DNS thường mang tính "một nửa": nửa số request thành công, nửa thất bại. Và chính tính "một nửa" đó khiến nó cực khó chẩn đoán vì khi bạn thử lại, nhiều khi lại trúng cái nửa thành công.
Bài học vận hành
Trước khi đổi IP một dịch vụ quan trọng, hãy hạ TTL xuống thấp (vài chục giây) từ trước đó vài tiếng, đợi TTL cũ hết hạn trên toàn mạng, rồi mới đổi. Sau khi đổi xong và ổn định, nâng TTL lại để giảm tải. Đây là kỹ thuật cơ bản nhưng hay bị quên.
Nơi hay chết: Đổi server rồi mà một phần lưu lượng vẫn vào IP cũ trong suốt thời gian TTL chưa hết.
Vũ khí tại trận:
dig +short api.example.com # IP đang được phân giải
dig api.example.com | grep -A1 ANSWER # xem TTL còn lại đếm ngược về 0
Trạm 3 : nginx (Allow List): hung thủ giết người không để lại dấu
Đây là trạm tôi muốn dừng lại lâu nhất, vì đây là nơi cái request 2 giờ sáng của tôi đã chết và nó chết theo một cách mà rất ít tài liệu tiếng Việt giải thích tử tế.
nginx ở đây là lớp ngoài cùng: reverse proxy kèm allow list kiểm soát ai được qua. Có ba kiểu cấu hình allow list, và hiểu tại sao mỗi kiểu hành xử khác nhau mới là điều quan trọng.
Kiểu 1 : Lọc theo IP nguồn, và cái bẫy "nhìn nhầm người"
location /api/ {
allow 10.0.0.0/8;
deny all;
proxy_pass http://apisix_upstream;
}
Module ngx_http_access_module kiểm tra IP của kết nối TCP đang tới. Vấn đề: nếu nginx đứng sau một load balancer khác, thì kết nối TCP mà nginx nhận được là từ LB, không phải từ client. Về mặt mạng, client thật đã "biến mất" sau lưng LB.
Tại sao? Vì ở tầng TCP, mỗi hop chỉ thấy hop liền trước nó. LB mở một kết nối mới tới nginx, nên nginx chỉ thấy IP của LB. IP thật của client được LB nhét vào header X-Forwarded-For ở tầng HTTP (L7) mà allow/deny lại làm việc ở tầng kết nối, nên mặc định nó không thèm đọc header đó.
Cách sửa bảo nginx tin vào header để khôi phục IP thật:
set_real_ip_from 10.0.0.0/8; # chỉ tin XFF khi kết nối đến từ LB nội bộ
real_ip_header X-Forwarded-For;
Lưu ý set_real_ip_from rất quan trọng về bảo mật: chỉ tin header từ những nguồn bạn kiểm soát, nếu không kẻ tấn công có thể giả mạo X-Forwarded-For để vượt allow list.
Kiểu 2 : Bẫy DNS-cache: tại sao nginx ôm một IP đã chết?
Đây là phần đắt giá nhất. Mặc định, khi proxy_pass chứa một tên miền dạng tĩnh, nginx phân giải DNS đúng MỘT lần lúc load config, rồi cache cái IP đó cho đến lần reload tiếp theo.
proxy_pass http://api.internal.example.com; # ← resolve một lần, nhớ mãi
Tại sao nginx làm vậy? Vì triết lý thiết kế của nó là tốc độ tuyệt đối. Phân giải DNS cho mỗi request sẽ thêm độ trễ và phụ thuộc vào một dịch vụ ngoài. Nên nginx chọn resolve sẵn lúc khởi động để mỗi request về sau không tốn một mili-giây nào cho DNS. Đây là một đánh đổi hợp lý cho tới khi backend đổi IP.
Trong thế giới Kubernetes / cloud, IP backend đổi liên tục: Pod restart, autoscale, failover. Nginx thì vẫn ngoan cố ôm cái IP từ hồi khởi động. Triệu chứng đúng như đêm 2h sáng của tôi: 502/504 ngắt quãng, gọi vào một địa chỉ "ma" không còn ai ở đó mà config thì trông hoàn toàn đúng.
Cách chữa, và tại sao nó hiệu nghiệm:
resolver 10.0.0.10 valid=10s; # khai một DNS resolver, kết quả chỉ tin trong 10s
set $backend "api.internal.example.com";
proxy_pass http://$backend; # vì là BIẾN, nginx buộc phải resolve lúc chạy
Mấu chốt nằm ở quy tắc nội bộ của nginx: khi proxy_pass chứa biến, nginx không thể resolve sẵn lúc khởi động được nữa (vì giá trị biến chỉ biết lúc chạy). Nó buộc phải dùng resolver để hỏi DNS runtime, và tôn trọng valid=10s để định kỳ hỏi lại. Một thay đổi nhỏ về cú pháp, nhưng đảo ngược hoàn toàn hành vi cache.
Kiểu 3 : Lọc theo domain bằng map, và vì sao if nguy hiểm
map $host $allowed {
default 0;
"api.example.com" 1;
"~^.*\.internal$" 1; # regex — sai một dấu là lọt hoặc chặn nhầm
}
server {
if ($allowed = 0) { return 403; }
}
map chạy hiệu quả vì nginx dựng sẵn bảng tra cứu lúc khởi động. Nhưng if bên trong khối location thì nổi tiếng nguy hiểm ("if is evil") lý do là if trong nginx không phải câu lệnh điều kiện thông thường, mà can thiệp vào cơ chế chọn cấu hình nội bộ, dẫn tới hành vi phản trực giác khi kết hợp với các directive khác. Dùng if ở cấp server cho việc đơn giản như return thì an toàn; nhét logic phức tạp vào if trong location thì dễ vỡ.
Nơi hay chết:
403âm thầm do realip sai (kiểu 1), hoặc502/504do DNS-cache chết (kiểu 2 — kẻ giết người thầm lặng nhất).
Vũ khí tại trận:
nginx -T # dump TOÀN BỘ config đã merge — thấy allow list thật sau khi include
Mẹo vàng thêm $upstream_addr vào log để thấy nginx thật sự nối tới IP nào:
log_format up '$remote_addr -> $host -> $upstream_addr status=$status';
Nếu $upstream_addr trỏ vào một IP đã chết → bạn vừa bắt tận tay con bug DNS-cache.
Trạm 4 : APISIX: tại sao lỗi ở đây "tử tế" hơn?
nginx vs APISIX — khác nhau ở đâu?
Câu hỏi tự nhiên: đã có nginx rồi, sao còn cần APISIX? (Thú vị là APISIX cũng được xây trên nền nginx + OpenResty.) Câu trả lời nằm ở mức độ thông minh.
nginx trong vai trò trên chủ yếu làm việc tầng thấp: chuyển tiếp, lọc IP, lọc domain. APISIX là một API Gateway đầy đủ, hoạt động sâu ở tầng 7 nó đọc hiểu ngữ nghĩa HTTP và quản lý vòng đời của API: routing theo path/method/header, xác thực (JWT, key-auth, OAuth), rate limiting, biến đổi request/response, observability, qua một hệ thống plugin động cấu hình được lúc chạy (thường qua etcd) mà không cần reload.
Nói cách khác: nginx là người gác cổng, APISIX là người quản lý cả tòa nhà.
Tại sao lỗi từ APISIX dễ chẩn đoán hơn?
Vì nó là lỗi có chủ đích, có ngữ nghĩa. Khác với cái chết âm thầm của DNS-cache, APISIX thường nói rõ lý do qua mã trạng thái:
401 Unauthorized— thiếu hoặc sai thông tin xác thực.403 Forbidden— xác thực hợp lệ nhưng không đủ quyền.429 Too Many Requests— vượt rate limit.502 Bad Gateway— "tôi gọi upstream nhưng nó không trả lời tử tế."504 Gateway Timeout— "tôi gọi upstream nhưng đợi mãi không thấy hồi âm."
Phân biệt trách nhiệm kỹ năng debug quan trọng nhất ở đây
Mã lỗi | Ý nghĩa | Trách nhiệm thuộc về |
|---|---|---|
| Code xử lý request và tự ném exception | Pod backend — lỗi logic/code |
| Gateway gọi được upstream nhưng nhận phản hồi hỏng / kết nối bị từ chối | Đường APISIX → backend hoặc backend crash |
| Gateway gọi upstream nhưng hết thời gian chờ | Backend quá chậm, hoặc đường nghẽn |
Hiểu sự khác biệt này tiết kiệm hàng giờ: thấy 502/504 thì đừng ngồi soi code (code còn chẳng chạy được tới), mà soi đường mạng và sức khỏe backend. Thấy 500 mới mở code ra đọc.
Nơi hay chết: Nhầm
502(lỗi đường truyền/upstream) thành500(lỗi code) đào nhầm mộ.
Trạm 5 : NodePort: cánh cửa duy nhất, và tại sao nó vừa tiện vừa nguy hiểm
Đây là trạm mang lại cú "à há" lớn nhất, và là nơi thiết kế kiến trúc lộ ra những đánh đổi sâu sắc.
Bối cảnh: hai thế giới cách nhau một biên giới
nginx và APISIX sống ngoài cluster. Pod backend sống trong cluster. Giữa chúng là một biên giới mạng: mạng nội bộ của K8s (Pod network) thường không định tuyến được trực tiếp từ bên ngoài. Vậy làm sao APISIX (ngoài) gọi tới Pod (trong)?
Kubernetes cho vài cách phơi dịch vụ ra ngoài. Ở đây ta dùng NodePort kiểu Service mở một cổng cố định (dải 30000–32767) trên mọi node của cluster. Gọi vào <bất-kỳ-node-ip>:<nodeport> là chạm được vào dịch vụ.
APISIX khai upstream trỏ tới node-ip:31080. Thế là xong — đó là toàn bộ "service discovery" trong kiến trúc này. Đơn giản đến mức không cần Ingress Controller, không cần cơ chế khám phá động nào cả.
Đọc một dòng kubectl get svc và hiểu từng con số
NAME TYPE CLUSTER-IP PORT(S) AGE
backend-svc NodePort 10.96.12.34 80:31080/TCP 30d
Cụm 80:31080/TCP kể cả một câu chuyện:
80— cổng của ClusterIP, dành cho ai gọi từ bên trong cluster (10.96.12.34:80).31080— cổng mở trên mọi node, dành cho ai gọi từ bên ngoài (node-ip:31080).
Điều khiến người mới khựng lại: NodePort không thay thế ClusterIP, nó chồng thêm lên. Mọi Service kiểu NodePort đều ngầm có sẵn một ClusterIP. Tại sao? Vì cơ chế nội bộ của K8s vẫn cần ClusterIP để định tuyến NodePort chỉ là một lớp "mở cổng ra ngoài" gắn thêm lên trên. Hệ quả: backend của bạn có nhiều đường vào tồn tại song song, không phải chọn một.
Cú "à há" cốt lõi: NodePort là một cái phễu, không phải một địa chỉ
Đây là điểm tinh tế nhất, và là nơi cần hiểu sâu cơ chế.
Khi APISIX gọi node-A:31080, Pod xử lý thật có thể không nằm trên node A. Cơ chế bên dưới: trên mỗi node có một tiến trình kube-proxy, dựng sẵn các luật iptables (hoặc IPVS). Khi gói tin tới cổng 31080 của node A, luật này chọn một Pod đích — có thể là Pod trên node A, cũng có thể là Pod trên node B — rồi DNAT (đổi địa chỉ đích) và chuyển tiếp gói sang đó. Đây là một "hop thừa" giữa các node, và nó âm thầm xảy ra dưới lớp mạng.
Hiểu cơ chế này rồi, hai cạm bẫy lớn lộ ra:
Cạm bẫy 1 : Đừng hardcode một node IP. NodePort mở trên mọi node, nhưng nếu APISIX chỉ trỏ tới một node IP cố định, thì số phận của toàn bộ backend bị buộc vào sức khỏe của một node đó. Node đó chết → toàn bộ backend "biến mất" với APISIX, dù các Pod ở node khác vẫn sống nhăn. Đây là họ hàng gần của bẫy DNS-cache ở nginx: đừng bao giờ coi một địa chỉ duy nhất là bất tử. Giải pháp: đặt một load balancer L4 trước các node, hoặc cho APISIX biết nhiều node IP và tự health-check.
Cạm bẫy 2 : Source IP bị che mất (vì sao?). Khi gói nhảy từ node A sang node B (Pod nằm ở B), node A phải làm SNAT đổi địa chỉ nguồn thành IP của chính node A. Tại sao bắt buộc? Vì gói phản hồi từ Pod phải quay về đúng node A (nơi đã làm DNAT) để được dịch ngược địa chỉ, rồi mới trả về cho client. Không SNAT thì Pod sẽ trả thẳng về client và phá vỡ chuỗi dịch địa chỉ. Hệ quả: backend nhìn thấy IP của node, không phải IP thật của APISIX/client. Ai cần log IP thật để audit sẽ đau đầu.
Lối thoát là externalTrafficPolicy: Local chỉ route tới Pod trên chính node nhận gói, nhờ vậy không cần hop thừa, không cần SNAT, giữ được source IP. Nhưng đánh đổi: node nào không có Pod sẽ không nhận traffic, và cân tải kém đều hơn.
Đánh đổi của cả kiến trúc
Mô hình "nginx/APISIX ngoài cluster → NodePort → Pod" rất đơn giản và độc lập: không cần Ingress Controller, không cần service discovery động, gateway tách hẳn khỏi vòng đời cluster. Đổi lại, APISIX (L7 thông minh) phải giao phó việc cân tải backend cho kube-proxy (L4 mù nội dung). APISIX không biết Pod nào sống/chết, không health-check được tới từng Pod — nó chỉ biết gõ cửa NodePort và tin rằng kube-proxy sẽ dẫn đúng đường. Đây là sự đánh đổi giữa đơn giản và khả năng kiểm soát tinh vi.
Trạm 6 : Pod: tại sao "Running" không có nghĩa là "sẵn sàng"?
Sau cả hành trình, kẻ lữ hành tới đích: Pod backend nơi code thật sự chạy.
Hai trạng thái mà người mới hay nhầm
Cạm bẫy cuối cùng nằm ở sự khác biệt giữa "đang chạy" và "sẵn sàng phục vụ":
Running chỉ nghĩa là container đã khởi động và chưa chết. Nó không nói gì về việc Pod đã sẵn sàng nhận traffic chưa. Một Pod vừa Running có thể đang nạp cache, đang mở kết nối tới DB, đang khởi tạo chưa thể phục vụ.
Readiness probe trả lời câu hỏi "tôi đã sẵn sàng nhận request chưa?" Cơ chế: K8s định kỳ gọi một endpoint (ví dụ /healthz) của Pod. Chỉ khi probe này pass, Pod IP mới được thêm vào danh sách Endpoints của Service tức là mới được kube-proxy đưa vào vòng cân tải. Nếu readiness cấu hình sai (hoặc thiếu), kube-proxy có thể đẩy request vào Pod chưa sẵn sàng → lỗi chập chờn, lúc được lúc không.
Liveness probe trả lời câu hỏi "tôi còn sống không, hay đã treo?" Nếu probe này fail, K8s giết và khởi động lại Pod. Cạm bẫy: cấu hình quá nhạy (timeout quá ngắn) sẽ khiến K8s giết Pod ngay giữa lúc nó đang gồng xử lý một request nặng tự tay tạo ra sự bất ổn.
Mối liên kết quan trọng: Endpoints chính là cầu nối
Hiểu được readiness điều khiển Endpoints sẽ mở ra một công cụ debug mạnh. Endpoints là danh sách Pod IP mà Service coi là "sẵn sàng". Nếu danh sách này rỗng, mọi request đi vào Service đều rơi vào hư không dù các Pod vẫn Running.
Nơi hay chết: Readiness probe sai khiến traffic dội vào Pod chưa sẵn sàng đặc biệt hay xảy ra ngay sau deploy. Triệu chứng: deploy xong, lỗi nhấp nháy vài phút rồi tự hết (khi Pod cuối cùng cũng ready).
Vũ khí tại trận:
kubectl get endpoints backend-svc # danh sách Pod IP đang được coi là Ready
kubectl describe pod <pod-name> # xem readiness/liveness và các sự kiện gần đây
kubectl get pod <pod-name> -o wide # Pod nằm ở node nào (đối chiếu với cạm bẫy NodePort)
Khám nghiệm tử thi: bảng tra cứu cho lần sau
Triệu chứng | Trạm nghi phạm | Tại sao |
|---|---|---|
Đổi server rồi mà vẫn vào IP cũ | DNS | TTL chưa hết, cache nhiều tầng còn ôm IP cũ |
| nginx | DNS-cache chết — cần |
| nginx | allow/deny soi nhầm IP — cần realip |
| APISIX | lỗi có chủ đích — đọc kỹ thông điệp |
| NodePort | node bị trỏ tới đã chết |
Tải lệch hẳn về một node | NodePort | hardcode một node IP, không rải đều |
Backend log toàn IP của node | NodePort | SNAT che source IP — cân nhắc |
Lỗi chập chờn ngay sau deploy | Pod | readiness probe sai, Pod chưa ready đã nhận traffic |
| Pod/Service | không Pod nào pass readiness |
Còn cái request 2 giờ sáng của tôi? Hung thủ là trạm 3 nginx ôm một IP đã chết vì proxy_pass tĩnh. Một dòng config thiếu chữ resolver, đổi lấy một đêm mất ngủ và một bài học khắc cốt.
Lời cuối: chậm lại để đi nhanh hơn
Bài học lớn nhất sau cái đêm ấy không phải một mẹo kỹ thuật, mà là một thái độ.
Khi hệ thống đổ vỡ, bản năng của ta là nhảy bổ vào chỗ thân thuộc nhất — thường là code. Nhưng kẻ lữ hành đã đi qua sáu cửa ải trước khi chạm tới code. Hung thủ có thể nấp ở bất kỳ trạm nào.
Debug giỏi không phải đoán nhanh, mà là đi tuần tự, không nhảy cóc. Lần theo đúng con đường request đã đi client, DNS, nginx, APISIX, NodePort, Pod và ở mỗi trạm hỏi đúng hai câu: "Kẻ lữ hành có đi qua đây không? Và nó còn sống khi rời đi chứ?"
Hiểu sâu một hệ thống không phải là thuộc lòng từng directive, mà là hiểu tại sao mỗi tầng tồn tại, nó đánh đổi điều gì, và nó sẽ phản bội ta theo cách nào khi bị hiểu lầm. Mỗi tầng trong bài này từ TTL của DNS, tới cache của nginx, tới cái phễu NodePort đều là một quyết định đánh đổi giữa tốc độ, sự đơn giản, và khả năng kiểm soát. Hiểu được sự đánh đổi đó, ta không chỉ sửa được bug, mà còn thiết kế được hệ thống tốt hơn.
Một request, nếu ta biết lắng nghe, sẽ kể lại toàn bộ câu chuyện về cái chết của nó. Ta chỉ cần đủ kiên nhẫn để đi cùng nó suốt chặng đường như một kẻ lữ hành hoài niệm, lần theo từng dấu chân trên con đường đã cũ.
— Ghi lại trong một đêm không còn phải thức vì alert.
Bài viết liên quan
Thiết kế hệ thốngTôi viết một con DNS server để hiểu thứ mình đã tin tưởng mù quáng suốt mười năm
Có những công nghệ bạn dùng mỗi ngày mà chưa từng nhìn thẳng vào mặt nó. Với tôi, DNS là một trong số đó. Tôi gõ tên miền cả thập kỷ. Mua domain, trỏ record, chờ "propagate", chửi thề khi nó không lên, rồi vui khi nó lên — mà thật ra không hiểu tại sao nó lên. DNS với tôi giống công tắc điện trong nhà: bật là sáng, hỏng thì gọi thợ. Cho đến một đêm deploy, web chết. ping ra IP vẫn ngon, curl thẳng vào IP vẫn trả về trang. Chỉ riêng cái tên miền là im lặng. Tôi ngồi đó và nhận ra một điều hơi nhục: tôi không biết bắt đầu sửa từ đâu, vì tôi chưa bao giờ thật sự hiểu cái thứ đang hỏng. Nên tôi làm điều mà chắc chỉ dân kỹ thuật mới thấy hợp lý: thay vì đọc tài liệu cho qua, tôi ngồi viết hẳn một con DNS server bằng Rust. Project tên mini-dns. Bài này là những gì một hộp đen kể cho tôi nghe khi tôi cuối cùng cũng chịu mở nó ra.
Phát triển phần mềmNginx không "dở chứng" — bạn chỉ đang đọc config sai cách
Giới Dev có một nghịch lý vui: chúng ta sẵn sàng tranh cãi cả tuần xem Backend nên viết bằng Rust, Go hay Node.js, nhưng đến lúc đẩy sản phẩm lên production thì 90% lặng lẽ gõ apt install nginx và cắm nó ra phía trước. Nginx là "người gác cổng quốc dân" — và cũng là thứ khiến không ít người ngồi debug tới 2 giờ sáng mà vẫn không hiểu vì sao một request đơn giản lại trả về 404. Điều thú vị là: lỗi gần như không bao giờ nằm ở Nginx. Nó nằm ở chỗ chúng ta đọc file config như đọc một đoạn code chạy tuần tự từ trên xuống — trong khi Nginx hoàn toàn không hoạt động như vậy. Bài này không phải danh sách "những lỗi Nginx thường gặp" để bạn chép về dán vào server. Mục tiêu của nó là đưa cho bạn một mô hình tư duy: hiểu Nginx suy nghĩ ra sao, để những cái bẫy tưởng như thần bí trở nên đoán trước được — thay vì cứ gặp là ngớ người ra.
Phát triển phần mềmGóc Nhìn "Reverse LSP": Khi Bản Năng Sinh Tồn Buộc Bạn Phải Vi Phạm Nguyên Tắc
SOLID không phải là một tôn giáo, và các nguyên tắc thiết kế không phải là những điều răn bất di bất dịch. Đứng ở góc độ một Tech Lead thực chiến, đôi khi quyết định bẻ gãy nguyên tắc Liskov Substitution Principle (LSP) lại là lựa chọn trưởng thành để cứu sống hệ thống. Hãy cùng phân tích 4 kịch bản trade-off kinh điển và nghệ thuật khoanh vùng 'mã độc' an toàn.
Thảo luận
0 bình luận
Hãy là người đầu tiên thảo luận.