---
title: "Từ cú nhấp chuột đến cơ sở dữ liệu: đường đi của một yêu cầu"
url: https://www.stacklogs.io.vn/ky-thuat/duong-di-cua-mot-yeu-cau/
language: vi
author: "Nhu Duy Thin (Thinnd)"
published: 2026-10-30
updated: 2026-10-30
section: "Kỹ thuật phần mềm"
tags: ["HTTP", "DNS", "TLS", "curl", "cơ sở dữ liệu", "hiệu năng", "người mới"]
summary: "Một yêu cầu web đi qua bảy chặng: tra địa chỉ (DNS), mở kết nối (TCP), bảo mật (TLS), gửi yêu cầu HTTP, ứng dụng xử lý, cơ sở dữ liệu trả lời, rồi phản hồi quay về. Các mốc thời gian của curl -w là cộng dồn từ lúc bắt đầu, nên muốn biết một chặng tốn bao lâu thì lấy mốc sau trừ mốc liền trước."
license: "Cite with the author name and the canonical URL above."
---

# Từ cú nhấp chuột đến cơ sở dữ liệu: đường đi của một yêu cầu

> Dành cho người mới học lập trình, người làm sản phẩm, và bất kỳ ai nhờ AI sửa một trang web chậm mà chưa biết lỗi nằm ở đâu. Không cần biết mạng máy tính. Từ nào khó cũng có một hình ảnh đời thường đi kèm, và phần tính toán được đánh dấu để bỏ qua nếu bạn không cần.
>
> Số liệu và định nghĩa lấy từ tài liệu công khai, có liệt kê cuối bài. Ví dụ ghi là "minh họa" hoặc "mô phỏng" được dựng lại, không nhắc đến người hay công ty cụ thể. Cách các hệ thống thật vận hành có nhiều chi tiết hơn bài này, nên các hình ảnh dưới đây chỉ là cách hình dung để dễ nhớ.
>
> **Nói trước điều tôi không làm được:** môi trường tôi viết bài chặn việc mở cổng mạng cục bộ và chặn truy cập https ra ngoài. Vì vậy tôi **không chạy được curl thật** vào một máy chủ nào. Phần "ví dụ chạy thật" là một mô phỏng tôi viết và chạy thật, nhưng các độ trễ do tôi đặt sẵn, không phải đo từ mạng. Các lệnh curl trong bài được ghi rõ là "chưa chạy ở đây". Bạn chạy trên máy mình để có số thật.

---

## Tóm tắt nhanh

- Khi bạn mở một trang, yêu cầu đi qua nhiều chặng: **tra địa chỉ, mở kết nối, bắt tay bảo mật, gửi yêu cầu, ứng dụng xử lý, cơ sở dữ liệu trả lời, phản hồi quay về**. Mỗi chặng có thể chậm hoặc hỏng riêng.
- Muốn biết chặng nào chậm thì **đo**, đừng đoán. Lệnh `curl` với tùy chọn `-w` in ra các mốc thời gian.
- Các mốc của curl là **cộng dồn từ lúc bắt đầu**. Thời gian một chặng bằng mốc sau trừ mốc liền trước.
- Trong mô phỏng của bài, hai trường hợp có tổng thời gian gần nhau (0,973 giây và 1,383 giây) nhưng **thủ phạm khác hẳn**: một bên là chờ cơ sở dữ liệu (chiếm khoảng 83%), một bên là tra DNS (chiếm khoảng 87%).
- Mã `200` chỉ nói yêu cầu thành công về mặt giao thức. Nó không nói nội dung đúng.
- Khi nhờ AI sửa trang chậm, hãy **đo trước và đưa số đo cho AI**. Code do AI viết có thể ẩn nhiều truy vấn nhỏ mà mắt thường khó thấy, nhưng tôi chưa tìm thấy nghiên cứu đo tần suất này.

### Sau bài này bạn giải thích được

1. Một yêu cầu web gồm những chặng nào, và vì sao mỗi chặng có thể chậm hoặc hỏng riêng.
2. Vì sao các mốc thời gian của curl phải trừ cho nhau, và cách tính từng chặng.
3. Cách khoanh vùng lỗi nằm ở mạng, ở ứng dụng hay ở cơ sở dữ liệu **trước khi** nhờ AI sửa.

---

## Từ khó, nói dễ

| Thuật ngữ | Nói dễ hiểu |
|---|---|
| **DNS** (hệ thống tên miền) | Danh bạ của internet. Bạn đưa tên "example.com", nó trả địa chỉ số của máy chủ. |
| **TCP** (kết nối) | Cuộc gọi điện thoại thông báo "tôi sắp gửi hàng", bên kia nghe máy xác nhận rồi mới gửi. |
| **TLS** (bắt tay bảo mật) | Hai bên thỏa thuận một chiếc thùng có khóa trước khi gửi hàng, để người ngoài không mở xem được. Có trong địa chỉ bắt đầu bằng https. |
| **HTTP** | Ngôn ngữ của phiếu đặt hàng và phiếu trả hàng giữa trình duyệt và máy chủ. |
| **Header** (dòng tiêu đề) | Các dòng ghi chú gắn kèm phiếu: loại nội dung, ngôn ngữ, độ dài. |
| **Status code** (mã trạng thái) | Con số ngắn trong phiếu trả: 200 là xong, 404 là không tìm thấy, 500 là máy chủ gặp lỗi. |
| **Cơ sở dữ liệu** | Cái kho lưu thông tin lâu dài. Ứng dụng phải hỏi kho mới có dữ liệu để trả lời. |
| **Latency** (độ trễ) | Thời gian chờ, tính bằng giây hoặc mili giây (1 mili giây là một phần nghìn giây). |

---

## 1. Hình dung trước: gửi một bưu kiện

Hình dung bạn đặt hàng từ một cửa hàng ở thành phố khác. Trước khi nhận được hàng, có bảy việc xảy ra:

1. Bạn **tra danh bạ** để biết địa chỉ cửa hàng (DNS).
2. Bạn **gọi điện báo** có đơn và chờ họ nghe máy (kết nối TCP).
3. Hai bên thống nhất dùng **thùng có khóa** (TLS).
4. Bạn **gửi phiếu đặt hàng** ghi rõ muốn gì (yêu cầu HTTP).
5. **Lễ tân** nhận phiếu và xử lý (ứng dụng trên máy chủ).
6. Lễ tân **xuống kho** tìm hàng (cơ sở dữ liệu).
7. Hàng và phiếu giao **quay về** tay bạn (phản hồi).

Nếu hàng đến chậm, bạn không biết ngay lỗi nằm ở đâu: ở danh bạ cũ, ở đường dây bận, ở lễ tân, hay ở kho. Người đáng tin nhất là người **bấm giờ từng việc**. Máy tính cũng vậy.

Tài liệu của MDN mô tả ngắn hơn, theo ví dụ con đường: thiết bị của bạn là ngôi nhà, máy chủ là cửa hàng, DNS là cuốn sổ địa chỉ, HTTP là ngôn ngữ để đặt hàng. MDN liệt kê bốn bước: tra DNS, gửi yêu cầu, nhận phản hồi, ghép thành trang. Bài này tách chi tiết hơn để bạn thấy chỗ nào đo được.

**Điều rút ra:** "trang chậm" không phải một lỗi. Nó là tổng của bảy chặng, và thường chỉ một chặng chiếm phần lớn thời gian.

---

## 2. Bảy chặng, từng chặng một

Bảng dưới là bản đồ đơn giản hóa. Cột cuối cho biết chặng đó hay hỏng thế nào, để bạn nhận ra khi thấy thông báo lỗi.

| # | Chặng | Hình dung | Hay chậm khi | Hay hỏng thế nào |
|---|---|---|---|---|
| 1 | DNS | Tra danh bạ | Máy chủ DNS xa hoặc quá tải, tên miền mới | Không phân giải được tên: trình duyệt báo không tìm thấy máy chủ |
| 2 | Kết nối TCP | Gọi điện báo | Máy chủ ở xa, mạng mất gói | Kết nối bị từ chối hoặc hết thời gian chờ |
| 3 | TLS | Thùng có khóa | Mạng xa, thương lượng nhiều vòng | Chứng chỉ hết hạn hoặc sai tên: trình duyệt cảnh báo không an toàn |
| 4 | Gửi yêu cầu | Phiếu đặt hàng | Nội dung gửi lên lớn | Thiếu quyền (401, 403), dòng tiêu đề sai |
| 5 | Ứng dụng | Lễ tân xử lý | Code chậm, vòng lặp lớn | Lỗi 500 khi code gặp tình huống không xử lý được |
| 6 | Cơ sở dữ liệu | Xuống kho | Thiếu chỉ mục, quá nhiều truy vấn nhỏ | Hết kết nối, truy vấn quá thời gian |
| 7 | Phản hồi | Giao hàng | Nội dung lớn, chưa nén | Bị cắt giữa chừng, thiếu nội dung |

### 2.1 Chặng 1: DNS, tra danh bạ

**Hình dung:** hỏi tổng đài số điện thoại của cửa hàng. Bạn biết tên, chưa biết số.

Máy tính chỉ nói chuyện với nhau bằng địa chỉ số. Trình duyệt hỏi một máy chủ DNS để đổi tên "example.com" thành địa chỉ số. Kết quả thường được **nhớ tạm (cache)** một thời gian, nên lần sau nhanh hơn. Vì thế lần đo đầu tiên thường chậm hơn các lần sau.

### 2.2 Chặng 2 và 3: kết nối và bắt tay bảo mật

**Hình dung:** gọi điện báo rồi hai bên mua chung một chiếc thùng có khóa.

Theo tài liệu MDN, HTTP chạy trên TCP và thường được mã hóa bằng TLS. HTTP cần một đường truyền đáng tin cậy, nên nó mở kết nối trước. Với địa chỉ https, có thêm bước bắt tay TLS để hai bên thỏa thuận khóa. HTTP/1.0 cũ mở một kết nối mới cho mỗi cặp yêu cầu và phản hồi, còn các phiên bản sau **dùng lại kết nối**, nên yêu cầu thứ hai đến cùng máy chủ thường bỏ qua chặng 1 đến 3.

### 2.3 Chặng 4: gửi yêu cầu HTTP

**Hình dung:** tờ phiếu đặt hàng. Có tên món (đường dẫn), cách đặt (phương thức) và vài dòng ghi chú (header).

MDN mô tả một yêu cầu gồm phương thức (như `GET` để lấy, `POST` để gửi), đường dẫn tài nguyên, phiên bản HTTP, các header tùy chọn và có thể có phần thân. Ví dụ theo mẫu của MDN:

```http
GET / HTTP/1.1
Host: developer.mozilla.org
Accept-Language: fr
```

Phản hồi gồm phiên bản, mã trạng thái kèm vài chữ giải thích, header và có thể có thân (nội dung trang):

```http
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 29769
```

Năm nhóm mã trạng thái theo MDN: **1xx** thông tin, **2xx** thành công, **3xx** chuyển hướng, **4xx** lỗi từ phía bên gửi yêu cầu, **5xx** lỗi từ phía máy chủ. Vài mã hay gặp: 200 thành công, 301 đã chuyển vĩnh viễn sang địa chỉ khác, 304 nội dung chưa đổi nên dùng bản đã lưu, 404 không tìm thấy, 500 lỗi chung của máy chủ, 503 tạm thời không phục vụ được.

### 2.4 Chặng 5 và 6: ứng dụng và cơ sở dữ liệu

**Hình dung:** lễ tân đọc phiếu rồi xuống kho. Mỗi lần xuống kho mất thời gian đi và về.

Với một trang động, thời gian thường dồn nhiều vào hai chặng này. Ứng dụng chạy code của bạn, và nếu cần dữ liệu thì hỏi cơ sở dữ liệu. Mỗi lần hỏi là một chuyến đi và về, nhất là khi cơ sở dữ liệu nằm ở máy khác. **Hỏi 100 lần nhỏ thường chậm hơn hỏi một lần lớn**, vì phải trả tiền chuyến đi 100 lần. Phần 6 có ví dụ tính được.

### 2.5 Chặng 7: phản hồi quay về

Nội dung trả về càng lớn thì càng lâu, nhất là khi chưa được nén. Đây thường là chặng ngắn nhất với trang chữ, nhưng có thể dài với ảnh lớn hoặc tệp tải xuống.

**Điều rút ra:** mỗi chặng có "dấu hiệu lỗi" riêng. Thông báo lỗi là manh mối nên đọc kỹ trước khi nhờ AI đoán.

---

## 3. Đo từng chặng bằng curl

**Hình dung:** đặt một đồng hồ bấm giờ ở mỗi cửa trên đường đi của bưu kiện.

curl là công cụ dòng lệnh để gửi yêu cầu HTTP. Hai tùy chọn quan trọng với bài này:

- **`-v` (verbose, nói nhiều):** in toàn bộ quá trình. Theo tài liệu của curl, dòng bắt đầu bằng `*` là thông tin của chính curl (tra tên, kết nối), dòng bắt đầu bằng `>` là phần curl **gửi đi**, dòng bắt đầu bằng `<` là phần curl **nhận về**.
- **`-w` (write-out, in thông số sau khi xong):** in các mốc thời gian theo mẫu bạn đặt.

Năm mốc cần biết. Tất cả đều tính bằng giây và đều **đếm từ lúc bắt đầu**, theo tài liệu libcurl:

| Mốc | Nghĩa |
|---|---|
| `time_namelookup` | Tra DNS xong |
| `time_connect` | Kết nối TCP xong |
| `time_appconnect` | Bắt tay TLS xong (tài liệu libcurl không nói rõ giá trị khi không dùng TLS và tôi cũng chưa tự kiểm chứng, nên với địa chỉ http hãy bỏ qua mốc này) |
| `time_starttransfer` | Nhận byte đầu tiên của phản hồi, đã gồm cả thời gian máy chủ xử lý |
| `time_total` | Xong toàn bộ |

Lệnh mẫu (**chưa chạy ở đây**, vì môi trường của tôi chặn truy cập https ra ngoài; bạn chạy trên máy mình):

```bash
curl -s -o /dev/null -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} first_byte=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' https://example.com/
```

Tôi có chạy lệnh dạng này với curl 8.7.1 và curl nhận đúng các tên biến, nhưng kết nối bị chặn nên mã trả về là 000 và các số không có ý nghĩa, tôi không dùng chúng.

### 3.1 Công thức trừ

Vì các mốc cộng dồn, thời gian từng chặng là:

```text
DNS             = time_namelookup
Kết nối TCP     = time_connect       - time_namelookup
Bắt tay TLS     = time_appconnect    - time_connect
Chờ máy chủ     = time_starttransfer - time_appconnect
Tải về          = time_total         - time_starttransfer
```

Khi địa chỉ là http (không có TLS), bỏ dòng TLS và dùng `time_connect` thay cho `time_appconnect` ở dòng "Chờ máy chủ" (cách tính xấp xỉ).

**Giới hạn quan trọng:** khoảng "chờ máy chủ" gồm cả ứng dụng (chặng 5) và cơ sở dữ liệu (chặng 6). curl **không tách được** hai phần này. Muốn tách cần log truy vấn hoặc công cụ theo dõi chuyên dụng.

---

## 4. Ví dụ chạy thật: mô phỏng bảy chặng (có thể bỏ qua phần tính)

Đây là mô phỏng tôi viết bằng Python 3.14.8 và **chạy thật** trong môi trường của mình. Mỗi chặng là một lệnh chờ do tôi đặt sẵn. Chương trình in các mốc cộng dồn giống như curl, rồi tính từng chặng bằng phép trừ. Các độ trễ là **số tôi chọn**, không phải đo từ mạng. Điều đo được là phép trừ có cho kết quả đúng như đã đặt hay không.

Ca A: cơ sở dữ liệu chậm (DNS 0,040 giây, kết nối 0,030, TLS 0,060, chờ máy chủ 0,800, tải về 0,020). Ca B: DNS chậm (DNS 1,200, kết nối 0,030, TLS 0,060, chờ máy chủ 0,050, tải về 0,020).

Đầu ra thật của lần chạy:

```text
A: slow database
  time_namelookup      0.045   leg =  0.045
  time_connect         0.080   leg =  0.035
  time_appconnect      0.144   leg =  0.064
  time_starttransfer   0.949   leg =  0.805
  time_total           0.973   leg =  0.024
  longest leg: time_starttransfer (0.805 s)
B: slow DNS, fast database
  time_namelookup      1.205   leg =  1.205
  time_connect         1.240   leg =  0.035
  time_appconnect      1.305   leg =  0.065
  time_starttransfer   1.360   leg =  0.055
  time_total           1.383   leg =  0.023
  longest leg: time_namelookup (1.205 s)
```

Đọc kết quả:

- Ca A: chặng chờ máy chủ là 0,805 giây trên tổng 0,973, tức khoảng 83%. Con số 0,805 hơi lớn hơn 0,800 đã đặt vì lệnh chờ thật luôn dôi ra vài mili giây.
- Ca B: chặng DNS là 1,205 giây trên tổng 1,383, tức khoảng 87%. Tổng thời gian của hai ca tương đương nhau (dưới 1,4 giây), nhưng **chỗ cần sửa hoàn toàn khác**.
- Nếu chỉ nhìn `time_total`, bạn không phân biệt được hai ca. Sửa cơ sở dữ liệu ở ca B sẽ không giúp gì.

Lưu ý rõ: đây là mô phỏng với độ trễ do tôi đặt, nó chứng minh **cách tính**, không chứng minh mạng thật trông thế nào. Số thật trên máy bạn sẽ khác.

### 4.1 Truy vấn thừa: đếm thật, thời gian giả định

Ví dụ thứ hai cũng chạy thật, bằng Python và SQLite 3.53.4 trong bộ nhớ. Có 50 khách hàng và 200 đơn. Cách một: hỏi danh sách khách rồi hỏi từng khách một truy vấn riêng. Cách hai: một truy vấn có phép nối (join) và gom nhóm.

```text
same answer: True
queries, one per row: 51
queries, one join   : 1
waiting for the database at 5 ms per query: 255 ms vs 5 ms
```

Hai cách ra **cùng kết quả**, nhưng một cách tốn 51 truy vấn và cách kia tốn 1. Con số đếm là thật. Con số 5 mili giây mỗi lượt đi về là **giả định của tôi** cho một cơ sở dữ liệu ở máy khác (SQLite trong bộ nhớ gần như không có độ trễ mạng). Với giả định đó, 51 truy vấn chờ 255 mili giây còn 1 truy vấn chờ 5 mili giây. Kiểu này gọi là **N+1** (một truy vấn lấy danh sách, rồi N truy vấn cho từng dòng).

**Điều rút ra:** khi chặng "chờ máy chủ" dài, câu hỏi đầu tiên không phải "truy vấn nào chậm" mà là "trang này gửi bao nhiêu truy vấn".

---

## 5. Bài tập có đáp án

Bài tập này dùng một máy chủ giả chạy trên máy bạn. **Tôi chưa chạy được máy chủ này** vì môi trường của tôi chặn việc mở cổng mạng (lệnh mở cổng báo "Operation not permitted"). Tôi chỉ kiểm tra được code biên dịch được. Đáp án bên dưới suy ra từ cách tôi viết code, nên bạn hãy chạy thật và so với đáp án.

### 5.1 Máy chủ giả

Lưu thành `slow_app.py`:

```python
import time
from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/slow":
            time.sleep(0.8)          # giả lập truy vấn chậm
            body = b"orders: 3\n"
        elif self.path == "/missing":
            self.send_response(404)
            self.send_header("Content-Length", "0")
            self.end_headers()
            return
        else:
            body = b"hello\n"
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)
    def log_message(self, *a):
        pass

HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()
```

Chạy `python3 slow_app.py` ở một cửa sổ terminal, và dùng cửa sổ khác cho các lệnh dưới đây.

### 5.2 Bài 1: tìm chặng chậm

```bash
curl -s -o /dev/null -w 'tcp=%{time_connect} first_byte=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' http://127.0.0.1:8765/slow
curl -s -o /dev/null -w 'tcp=%{time_connect} first_byte=%{time_starttransfer} total=%{time_total} code=%{http_code}\n' http://127.0.0.1:8765/
```

Câu hỏi: ở mỗi lệnh, chặng nào chiếm gần hết thời gian? Tính bằng phép trừ.

**Đáp án (suy ra từ code, hãy so với số của bạn):** lệnh đầu có `first_byte` trừ `tcp` xấp xỉ **0,8 giây** (cộng sai số vài mili giây), vì code chờ 0,8 giây trước khi trả lời. Phần còn lại gần như bằng 0 vì cả hai đầu cùng một máy. Lệnh thứ hai không có chờ, nên mọi số đều rất nhỏ. Vì địa chỉ là http, không có TLS, bạn dùng `time_connect` thay cho `time_appconnect`.

### 5.3 Bài 2: mã 404 và thời gian

```bash
curl -s -o /dev/null -w 'code=%{http_code} total=%{time_total}\n' http://127.0.0.1:8765/missing
```

**Đáp án:** `code=404` và thời gian rất nhỏ. Bài học: một phản hồi lỗi vẫn có thể **nhanh**. Thời gian ngắn không có nghĩa là đúng.

### 5.4 Bài 3: một lần đo là chưa đủ

Chạy lệnh của bài 1 (đường `/slow`) năm lần rồi ghi các giá trị `first_byte`. Lấy số ở giữa khi xếp từ nhỏ đến lớn (trung vị).

**Đáp án:** các số nên gần 0,8 giây nhưng lệch nhau vài mili giây. Trên mạng thật, độ lệch lớn hơn nhiều, nên đừng kết luận từ một lần đo.

### 5.5 Thử thêm với https (chưa chạy ở đây)

```bash
curl -v https://example.com/ -o /dev/null
curl -s -o /dev/null -w 'dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} first_byte=%{time_starttransfer} total=%{time_total}\n' https://example.com/
```

Trong lệnh `-v`, tìm các dòng `*` (kết nối và bắt tay), dòng `>` (phiếu đặt hàng curl gửi) và dòng `<` (phiếu trả hàng). Với lệnh `-w`, áp dụng công thức ở phần 3.1. Lần chạy đầu thường có DNS chậm hơn, lần hai thường nhanh hơn do bộ nhớ tạm.

---

## 6. Chỗ dễ nhầm

Hình dung kiểm tra bưu kiện: biết hàng đến chậm chưa đủ, phải biết chậm ở cửa nào.

| Tình huống | Vì sao nhầm | Cách bắt được |
|---|---|---|
| Trang chậm nên sửa cơ sở dữ liệu ngay | Có thể DNS hoặc TLS mới là chặng dài (như ca B ở trên) | Đo trước, trừ mốc, nhìn chặng lớn nhất |
| Chỉ nhìn mã `200` | 200 chỉ nói yêu cầu thành công về giao thức, thân phản hồi vẫn có thể rỗng hoặc chứa thông báo lỗi do ứng dụng viết | Kiểm cả nội dung trả về, không chỉ mã |
| Lần hai nhanh nên coi như đã sửa | Bộ nhớ tạm (DNS, trình duyệt, máy chủ) làm lần sau nhanh. Mã 304 là cố ý cho dùng bản đã lưu | Đo cả lần đầu lẫn lần sau, ghi rõ lần nào |
| Dùng `time_total` làm thời gian một chặng | Các mốc cộng dồn | Luôn lấy mốc sau trừ mốc trước |
| Một lần đo | Mạng và máy chủ dao động | Đo nhiều lần, lấy trung vị |
| Có chuyển hướng (3xx) | Mỗi yêu cầu được theo thêm cũng tốn đủ các chặng. Theo tài liệu libcurl, thời gian của các yêu cầu theo sau được cộng lại | Xem từng yêu cầu bằng `-v`, tránh chuyển hướng không cần thiết |
| Đo từ máy mình rồi suy ra cho người dùng | Người dùng ở nơi khác, mạng khác | Đo từ nhiều nơi nếu có thể, ghi rõ nơi đo |

---

## 7. Minh họa: một buổi sáng với trang báo cáo chậm

Đây là tình huống **minh họa**, không nhắc đến người hay sản phẩm thật. Tên, số lượng và thời gian là dựng lại.

- **09:30.** Người dùng báo trang báo cáo mất khoảng 3 giây để hiện.
- **09:40.** Dev nhờ agent: "Làm cho truy vấn nhanh hơn." Agent thêm một chỉ mục và báo "đã tối ưu".
- **10:05.** Trang vẫn chậm. Dev chạy curl với `-w` và thấy `tcp=0,02`, `first_byte=3,05`, `total=3,08`. Chờ máy chủ chiếm gần 3 giây. DNS, kết nối và tải về đều không đáng kể.
- **10:15.** Dev bật log truy vấn: một lần tải trang gửi **601 truy vấn** (1 truy vấn lấy danh sách và 600 truy vấn cho từng dòng). Mỗi truy vấn chờ khoảng 5 mili giây vì cơ sở dữ liệu ở máy khác. Phép tính: 601 nhân 5 mili giây bằng 3.005 mili giây, khớp với khoảng 3 giây.
- **10:50.** Dev nhờ agent gộp thành một truy vấn có phép nối, kèm yêu cầu in số truy vấn trước và sau. `first_byte` giảm xuống còn khoảng 0,1 giây (số giả định của ví dụ).

Việc nên làm: **đo trước, rồi đưa số đo cho AI**. Nếu chỉ nói "làm nhanh hơn", AI có thể sửa sai chỗ, như thêm chỉ mục cho một truy vấn vốn không phải thủ phạm, và vẫn báo xong. Hãy đòi bằng chứng như trong bài [Làm cho kết quả của AI tự chứng minh](https://www.stacklogs.io.vn/ky-thuat/ket-qua-ai-tu-chung-minh/): số truy vấn và số `first_byte` trước và sau.

Minh họa này không chứng minh điều gì về tần suất. Bằng chứng nằm ở chỗ khác: phần 4.1 đếm thật 51 truy vấn so với 1 cho cùng một kết quả. Còn việc code do AI viết có hay gây N+1 hay không thì tôi **chưa tìm thấy nghiên cứu đo trực tiếp**. Một nghiên cứu năm 2025 (Unveiling Inefficiencies in LLM-Generated Code) phân tích 492 đoạn code nhỏ do ba mô hình mã nguồn mở sinh ra trên bộ HumanEval+ và thấy lỗi logic và hiệu năng là hai nhóm kém hiệu quả thường gặp, nhưng đó là bài toán thuật toán nhỏ, không phải ứng dụng web có cơ sở dữ liệu. Hãy coi nguy cơ N+1 là có thể xảy ra và tự đo, đừng coi là chắc chắn. Về bảo mật của code do AI viết, xem thêm bài [OWASP Top 10 cho ứng dụng dùng AI](https://www.stacklogs.io.vn/ky-thuat/owasp-top-10-ung-dung-ai/).

---

## 8. Số liệu và tài liệu đã dùng

| Nguồn | Nói gì | Cách đọc | Lưu ý |
|---|---|---|---|
| MDN, How the web works | Bốn bước: tra DNS, gửi yêu cầu, nhận phản hồi, ghép thành trang | Bản đồ cơ bản nhất | Rất đơn giản hóa, không nhắc TLS |
| MDN, tổng quan HTTP | Mô hình yêu cầu và phản hồi, không lưu trạng thái, chạy trên TCP (thường có TLS), dùng lại kết nối | Căn cứ cho chặng 2, 3, 4 | Là tài liệu tham khảo, không có số đo |
| MDN, mã trạng thái | Năm nhóm 1xx đến 5xx và nghĩa của 200, 301, 304, 404, 500, 503 | Căn cứ cho phần mã trạng thái | Nghĩa theo giao thức |
| libcurl, STARTTRANSFER_TIME và APPCONNECT_TIME | Thời gian đếm từ lúc bắt đầu, gồm thời gian máy chủ xử lý, cộng dồn qua chuyển hướng | Căn cứ cho công thức trừ | Tài liệu không nói rõ giá trị khi không có TLS |
| Everything curl, phần verbose | Ý nghĩa `*`, `>`, `<` | Căn cứ cho cách đọc `-v` | Là tài liệu hướng dẫn |
| Mô phỏng của tôi | Cách tính từng chặng bằng phép trừ | Minh họa phương pháp | Độ trễ do tôi đặt, không phải đo mạng |
| Đếm truy vấn của tôi | 51 truy vấn so với 1 cho cùng kết quả | Minh họa N+1 | 5 mili giây mỗi lượt là giả định |
| Nghiên cứu về lỗi kém hiệu quả trong code do mô hình sinh (2025) | Lỗi logic và hiệu năng thường gặp trên 492 đoạn code | Gợi ý cần đo, không kết luận | Code thuật toán nhỏ, ba mô hình mã nguồn mở, không phải ứng dụng web |

**Đọc cho đúng:** các tài liệu ủng hộ cách chia chặng và cách tính. Tôi **chưa đo trên mạng thật** và chưa tìm thấy nghiên cứu đo N+1 trong code do AI viết.

---

## 9. Danh sách kiểm: tám câu hỏi khi trang chậm

1. Tôi đã đo bằng số, hay đang đoán?
2. Tôi đã lấy các mốc và trừ cho nhau chưa, hay đang dùng `time_total`?
3. Chặng nào lớn nhất: DNS, kết nối, TLS, chờ máy chủ hay tải về?
4. Tôi đo lần đầu hay lần sau (có bộ nhớ tạm)? Đã đo nhiều lần chưa?
5. Mã trạng thái là gì, và nội dung trả về có đúng không?
6. Nếu chặng "chờ máy chủ" dài, trang này gửi bao nhiêu truy vấn?
7. Khi nhờ AI sửa, tôi đã đưa số đo cho nó và đòi số đo sau khi sửa chưa?
8. Tôi đo từ đâu, và người dùng có ở nơi tương tự không?

---

## 10. Nhìn cân bằng: bản đồ này đơn giản hóa điều gì

**Làm được:** cho bạn một cách chia vấn đề, một bộ công thức đo và một thói quen kiểm trước khi nhờ AI.

**Chưa làm được:**

- Đây là **bản đồ đơn giản hóa** của HTTP/1.1 chạy trên TCP có TLS. Trong thực tế có thêm CDN (mạng phân phối nội dung), proxy, bộ cân tải. Mỗi cái thêm một chặng hoặc cắt bớt một chặng.
- HTTP/3 theo RFC 9114 chạy trên QUIC, một giao thức dùng UDP, và gộp phần kết nối với phần bắt tay bảo mật. Theo các nguồn thứ cấp tôi đọc, điều này giảm số vòng đi về lúc mở kết nối. Khi đó các mốc `time_connect` và `time_appconnect` có thể không còn tách bạch như bảng ở phần 3. Hãy đọc bản gốc RFC nếu cần chính xác.
- curl đo **từ một nơi, một lần**. Một lần đo là nhiễu. Công cụ theo dõi chuyên dụng (APM) thấy nhiều hơn, như tách ứng dụng với cơ sở dữ liệu.
- Mô phỏng ở phần 4 chỉ minh họa phép tính, và bài tập ở phần 5 chưa được tôi chạy thật.

**Đừng thổi phồng:** biết bảy chặng không biến bạn thành chuyên gia hạ tầng, nhưng đủ để hỏi đúng câu. **Đừng coi nhẹ:** sửa sai chỗ vẫn tốn thời gian và vẫn có người báo "đã xong".

---

## 11. Kết luận

"Trang chậm" là tổng của nhiều chặng, và thường một chặng chiếm gần hết. Hãy đo bằng `curl -w`, lấy mốc sau trừ mốc trước, rồi nhìn chặng lớn nhất. Nếu đó là chờ máy chủ, đếm số truy vấn trước khi đoán. Và khi nhờ AI sửa, hãy đưa số đo cho nó, đòi số đo sau khi sửa, rồi tự mở số đó ra xem.

---

## Nguồn tham khảo

Định nghĩa lấy từ tài liệu công khai tính đến tháng 10/2026. Phần về HTTP/3 dựa trên nguồn thứ cấp, hãy mở bản gốc để kiểm tra trước khi trích dẫn.

- MDN, [How the web works](https://developer.mozilla.org/en-US/docs/Learn_web_development/Getting_started/Web_standards/How_the_web_works)
- MDN, [Tổng quan HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Overview) và [mã trạng thái phản hồi HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status)
- curl, [CURLINFO_STARTTRANSFER_TIME](https://curl.se/libcurl/c/CURLINFO_STARTTRANSFER_TIME.html) và [CURLINFO_APPCONNECT_TIME](https://curl.se/libcurl/c/CURLINFO_APPCONNECT_TIME.html)
- Everything curl, [Verbose](https://everything.curl.dev/usingcurl/verbose/index.html) và [trang man của curl](https://curl.se/docs/manpage.html) (tôi chưa đọc được phần -w của trang này, tên biến được xác nhận bằng việc chạy curl 8.7.1)
- IETF, [RFC 9114 (HTTP/3)](https://www.rfc-editor.org/rfc/rfc9114.html) và [RFC 9000 (QUIC)](https://www.rfc-editor.org/rfc/rfc9000.html)
- Unveiling Inefficiencies in LLM-Generated Code: Toward a Comprehensive Taxonomy, [arXiv 2503.06327](https://arxiv.org/abs/2503.06327) (2025)
