2.10 - Đào sâu: cache HTTP, container, HTTP/2 và HTTP/3
Tóm tắt: Những chủ đề nối tiếp Module 2, kèm mức độ ưu tiên. Hai mục nên học sớm vì ảnh hưởng trực tiếp tới công việc backend hàng ngày: caching HTTP (biết
ETagvàCache-Controlgiúp giảm tải rõ rệt mà gần như không tốn công) và container (mọi hệ thống hiện đại đều triển khai bằng Docker). Hai mục còn lại — HTTP/2, HTTP/3 và WebSocket — hữu ích để hiểu nhưng ít khi bạn phải cấu hình trực tiếp, vì reverse proxy hoặc framework đã lo phần lớn. Và một điểm hay bị hiểu nhầm: HTTP/2 không tự động làm ứng dụng nhanh hơn — nó giải quyết một vấn đề rất cụ thể, và với API trả JSON thì vấn đề đó thường không tồn tại.
Mục tiêu bài học
Sau bài này bạn có thể:
- Biết những chủ đề tiếp theo và thứ tự ưu tiên.
- Hiểu HTTP/2 và HTTP/3 giải quyết vấn đề gì.
- Dùng caching HTTP để giảm tải.
- Biết khi nào cần WebSocket.
Cần biết trước: 2.4 — HTTP/HTTPS (mục ETag và Cache-Control) và 2.8 — Hosting và Cloud cơ bản (image, container). Bài 3 cần Docker.
Nội dung bài học
2.10.1 — Caching HTTP: học sớm
Ưu tiên cao. Rẻ để làm, hiệu quả rõ ràng.
GET /api/products/123
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
ETag: "a3f8b2c1"GET /api/products/123
If-None-Match: "a3f8b2c1"
HTTP/1.1 304 Not Modified <- KHÔNG có body, tiết kiệm băng thôngHai cơ chế bổ sung nhau:
| Cơ chế | Cách hoạt động | Khi nào dùng |
|---|---|---|
Cache-Control | Trình duyệt không gửi request trong thời hạn | Dữ liệu ít đổi, biết trước thời hạn |
ETag | Vẫn gửi request nhưng chỉ nhận 304 nếu không đổi | Dữ liệu có thể đổi bất cứ lúc nào |
ETag tiết kiệm băng thông nhưng vẫn tốn một lượt đi về; Cache-Control tiết kiệm cả lượt đi về nhưng có rủi ro dữ liệu cũ.
Hai cơ chế dùng chung trên một tài nguyên:
Chỉ thị quan trọng nhất cần phân biệt:
Cache-Control: public # CDN và proxy được phép cache
Cache-Control: private # CHỈ trình duyệt của người dùng
Cache-Control: no-store # KHÔNG được lưu ở đâu cảNhầm private thành public cho dữ liệu riêng của người dùng là lỗ hổng thật: CDN có thể phục vụ dữ liệu của người này cho người khác.
ASP.NET Core có sẵn output caching (bài 9.7).
2.10.2 — Container: học sớm
Ưu tiên cao. Mọi hệ thống hiện đại triển khai bằng container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY ./publish .
ENTRYPOINT ["dotnet", "Crm.Api.dll"]Container giải quyết một vấn đề rất cụ thể: "máy tôi chạy được mà máy anh không". Nó đóng gói ứng dụng cùng runtime và mọi phụ thuộc thành một ảnh chạy giống hệt nhau ở mọi nơi.
Ba khái niệm đủ để bắt đầu: image (bản đóng gói), container (một thực thể đang chạy của image), volume (nơi lưu dữ liệu bền, vì container mất dữ liệu khi bị xoá).
Đây là chủ đề của Module 15.
2.10.3 — HTTP/2 và HTTP/3
Ưu tiên trung bình. Hiểu để biết, ít khi phải tự cấu hình.
Vấn đề HTTP/2 giải quyết:
HTTP/1.1: mỗi kết nối TCP xử lý MỘT request tại một thời điểm
-> trình duyệt mở 6 kết nối song song
-> tải trang có 50 file phải chờ theo đợt
HTTP/2: MỘT kết nối, NHIỀU request song song (multiplexing)
+ nén headerĐặc tả HTTP/2 còn có server push (server đẩy trước tài nguyên mà trình duyệt chưa xin), nhưng Chrome và Firefox đã bỏ hỗ trợ tính năng này, nên đừng tính nó vào lợi ích.
Điểm hay bị hiểu nhầm: lợi ích của HTTP/2 chủ yếu dành cho trang web có nhiều tài nguyên nhỏ (ảnh, CSS, JavaScript). Với một API trả về JSON theo từng request riêng lẻ, vấn đề "50 file phải chờ theo đợt" không tồn tại, nên cải thiện là không đáng kể.
HTTP/3 thay TCP bằng QUIC trên UDP, giải quyết vấn đề còn lại của HTTP/2: khi một gói tin bị mất, TCP chặn mọi luồng cho tới khi gói đó được gửi lại. QUIC chỉ chặn luồng bị ảnh hưởng — lợi ích rõ trên mạng di động hay mất gói.
Thực tế: bạn hiếm khi cấu hình trực tiếp. Reverse proxy hoặc CDN nói HTTP/2 với client và HTTP/1.1 với backend, và mọi thứ hoạt động.
gRPC dùng HTTP/2 làm nền, và đó là nơi bạn gặp nó rõ nhất (bài 18.4).
2.10.4 — WebSocket
Ưu tiên trung bình. Cần khi có tính năng thời gian thực.
HTTP: Client hỏi -> Server trả lời -> kết thúc
WebSocket: Kết nối MỞ liên tục, CẢ HAI bên gửi bất cứ lúc nàoCần khi: chat, thông báo đẩy, bảng giá cập nhật liên tục, hiển thị ai đang online.
Không cần khi dữ liệu đổi vài phút một lần — khi đó gọi lại API định kỳ đơn giản hơn nhiều và không phải lo kết nối đứt, scale nhiều instance, hay xử lý kết nối lại.
Trên .NET, SignalR bọc WebSocket và tự chuyển sang cơ chế khác nếu WebSocket không dùng được (Module 11).
Cái bẫy khi scale: với nhiều instance, tin nhắn gửi từ instance A không tới client đang kết nối vào instance B, trừ khi có backplane (bài 11.10).
2.10.5 — Những chủ đề khác
| Chủ đề | Nội dung | Ưu tiên |
|---|---|---|
| IPv6 | Địa chỉ 128 bit, đang dần thay IPv4 | Thấp — hạ tầng lo |
| TLS 1.3 | Bắt tay nhanh hơn, bỏ thuật toán yếu | Thấp — bật mặc định |
| Load balancer | Phân phối request cho nhiều server | Trung bình |
| CDN | Cache tài nguyên tĩnh gần người dùng | Trung bình |
| DNS nâng cao | Bản ghi CNAME, A, MX, TXT; TTL | Trung bình |
| gRPC | Giao tiếp nhị phân trên HTTP/2 | Trung bình |
Trong bảng này, DNS nâng cao là mục hay dùng bất ngờ: hiểu TTL giúp bạn biết vì sao đổi bản ghi DNS mà nửa số người dùng vẫn vào địa chỉ cũ trong vài giờ.
2.10.6 — Thứ tự nên học
| Ưu tiên | Chủ đề | Vì sao |
|---|---|---|
| Cao | Caching HTTP | Rẻ, hiệu quả rõ, dùng hàng ngày |
| Cao | Container cơ bản | Mọi hệ thống hiện đại đều dùng |
| Trung bình | HTTP/2, load balancer, DNS | Hiểu để chẩn đoán, ít khi tự cấu hình |
| Trung bình | WebSocket | Chỉ cần khi có tính năng thời gian thực |
| Thấp | IPv6, TLS chi tiết | Hạ tầng lo, mặc định đã đúng |
Sau Module 2, bước tiếp theo là Module 3 — Git và Developer Workflow.
2.10.7 — Những gì cần nắm sau bài
Danh sách rà soát sau Module 2
- Phân biệt được Cache-Control public, private và no-store.
- Dữ liệu riêng của người dùng không bao giờ dùng Cache-Control public.
- Biết ETag tiết kiệm băng thông nhưng vẫn tốn một lượt đi về.
- Hiểu ba khái niệm image, container và volume.
- Biết HTTP/2 không tự động làm API nhanh hơn.
- Chỉ dùng WebSocket khi thật sự cần thời gian thực.
- Biết SignalR nhiều instance cần backplane.
Bài tập áp dụng
Bài 1 — Thử ETag
Gọi một endpoint hai lần, lần hai kèm If-None-Match, và xác nhận nhận được 304 không có body.
Tiêu chí hoàn thành: bạn thấy 304 và body rỗng, và giải thích được ETag tiết kiệm cái gì — cũng như không tiết kiệm cái gì.
Gợi ý và lời giải — Bài 1
Gợi ý. curl -D - in ra header phản hồi, đủ để thấy toàn bộ cơ chế mà không cần trình duyệt.
Lời giải — endpoint có ETag:
Tạo project dotnet new web, thay Program.cs:
using System.Security.Cryptography;
using System.Text;
var app = WebApplication.Create(args);
var content = """{"id":8842,"fullName":"Nguyễn Văn An","status":"InProgress"}""";
var etag = "\"" + Convert.ToHexString(
SHA256.HashData(Encoding.UTF8.GetBytes(content)))[..16].ToLowerInvariant() + "\"";
app.MapGet("/api/leads/8842", (HttpContext ctx) =>
{
var clientEtag = ctx.Request.Headers.IfNoneMatch.ToString();
ctx.Response.Headers.ETag = etag;
ctx.Response.Headers.CacheControl = "private, max-age=60";
if (clientEtag == etag)
return Results.StatusCode(StatusCodes.Status304NotModified);
return Results.Content(content, "application/json");
});
app.Run();Chạy bằng dotnet run --urls http://127.0.0.1:5288.
curl -s -D - -o /dev/null http://127.0.0.1:5288/api/leads/8842Chạy thật trên .NET 10:
HTTP/1.1 200 OK
Content-Length: 63
Content-Type: application/json
Date: Sat, 17 Oct 2026 14:36:49 GMT
Server: Kestrel
Cache-Control: private, max-age=60
ETag: "6bef8b82de92cea1"curl -s -D - -o /dev/null -H 'If-None-Match: "6bef8b82de92cea1"' \
http://127.0.0.1:5288/api/leads/8842HTTP/1.1 304 Not Modified
Date: Sat, 17 Oct 2026 14:36:49 GMT
Server: Kestrel
Cache-Control: private, max-age=60
ETag: "6bef8b82de92cea1"Đo số byte thân phản hồi bằng curl -s -o /dev/null -w "%{size_download}" cho hai lần gọi:
Kích thước body lần 1: 63 byte
Kích thước body lần 2: 0 byte
Và phản hồi 304 KHÔNG có Content-Length — vì không có gì để gửi.ETag tiết kiệm cái gì:
Băng thông: toàn bộ body không được gửi
-> với một danh sách JSON 200 KB, tiết kiệm gần như trọn vẹn
Thời gian: ít byte đi qua mạng -> phản hồi về nhanh hơn
Pin và dữ liệu di động: đáng kể với ứng dụng điện thoạiVà đây là phần quan trọng hơn — ETag KHÔNG tiết kiệm cái gì:
Vẫn có một vòng khứ hồi mạng đầy đủ
-> độ trễ mạng không giảm chút nào
Máy chủ VẪN phải tính ra ETag
-> nếu ETag được tính từ nội dung, máy chủ phải DỰNG nội dung đó trước
-> truy vấn database vẫn chạy
-> chỉ khâu gửi đi là được bỏ quaHai đoạn dưới đây dùng EF Core (bài 8.10), chỉ cần đọc ý tưởng:
// Kiểu này KHÔNG giảm tải cho database chút nào
var lead = await db.Leads.FindAsync(id, ct); // vẫn truy vấn
var etag = ComputeEtag(lead); // vẫn tuần tự hoá
if (ctx.Request.Headers.IfNoneMatch == etag)
return Results.StatusCode(304); // chỉ tiết kiệm băng thôngMuốn tiết kiệm cả công việc của máy chủ, ETag phải tính được mà KHÔNG dựng nội dung:
// Lấy MỘT cột thay vì cả bản ghi
var version = await db.Leads
.Where(l => l.Id == id)
.Select(l => (uint?)l.Version)
.FirstOrDefaultAsync(ct);
if (version is null) return Results.NotFound();
var etag = $"\"{version}\"";
if (ctx.Request.Headers.IfNoneMatch == etag)
return Results.StatusCode(304); // chưa dựng DTO, chưa tuần tự hoá
var lead = await db.Leads.FindAsync(id, ct); // chỉ dựng khi thật sự cầnEntity Lead có cột phiên bản. Với PostgreSQL, EF Core ánh xạ thuộc tính [Timestamp] uint vào cột hệ thống xmin. Khai báo kiểu đặt ở cuối file, sau mọi câu lệnh:
// Đặt cuối file
public class Lead
{
public int Id { get; set; }
public string FullName { get; set; } = "";
[Timestamp] public uint Version { get; set; }
}Chạy thử với PostgreSQL 16: đọc Version của một lead, sửa tên rồi đọc lại, giá trị đổi từ 796 sang 797 (con số cụ thể tuỳ database). ETag vì thế tự đổi mỗi khi bản ghi bị sửa, không cần tự quản lý.
Truy vấn một cột phiên bản: rất rẻ
So với: nạp cả entity, ánh xạ sang DTO, tuần tự hoá JSONSQL Server
SQL Server không có xmin. Dùng cột kiểu rowversion, khai báo trong entity là mảng byte, rồi đổi sang chuỗi hex để làm ETag:
[Timestamp] public byte[] Version { get; set; } = [];
var etag = $"\"{Convert.ToHexString(version)}\"";Hai kiểu ETag, và cách chọn:
| Kiểu | Cách tính | Dùng khi |
|---|---|---|
Mạnh — "abc123" | Băm nội dung, hoặc cột phiên bản | Byte phải giống hệt nhau |
Yếu — W/"abc123" | Theo phiên bản logic | Nội dung "tương đương về mặt ngữ nghĩa" là đủ |
ETag yếu hữu ích khi phản hồi có phần thay đổi mà không quan trọng,
ví dụ một dấu thời gian sinh ra lúc trả về.Và một cạm bẫy cần tránh: Cache-Control: private so với public.
ctx.Response.Headers.CacheControl = "private, max-age=60";private -> chỉ trình duyệt của người dùng được lưu
public -> proxy và CDN trung gian cũng được lưu
Với dữ liệu riêng của từng người dùng, dùng public là một lỗ hổng:
một proxy có thể trả dữ liệu của người A cho người B.Với API trả dữ liệu theo người dùng, mặc định nên là private — và nếu có phân quyền theo tenant, thêm cả Vary: Authorization để bộ nhớ đệm không trộn lẫn phản hồi của hai token khác nhau.
Bài 2 — So sánh giao thức
Mở DevTools tab Network, cột Protocol, và xem trang nào dùng h2 hay h3.
Tiêu chí hoàn thành: bạn quan sát được cả ba phiên bản, và giải thích được vì sao HTTP/2 không giúp gì cho một API chỉ gọi một request.
Gợi ý và lời giải — Bài 2
Gợi ý. Cột Protocol không hiện mặc định — bấm chuột phải vào hàng tiêu đề của bảng để bật nó.
Lời giải — quan sát trong DevTools (bảng dưới là ví dụ minh họa, trang bạn mở sẽ khác):
Chrome DevTools -> Network -> chuột phải hàng tiêu đề -> tích "Protocol"Name Protocol Size Time
------------------ -------- ------- -------
index.html h2 12,4 kB 142 ms
main.css h2 8,1 kB 18 ms
app.js h2 284,0 kB 96 ms
api/leads?page=1 h2 4,2 kB 68 ms
fonts/inter.woff2 h3 38,0 kB 24 ms
tracking.js http/1.1 14,2 kB 180 msKiểm tra bằng dòng lệnh, không cần trình duyệt:
curl -sI --http2 https://example.com | head -1
curl -sI --http3 https://cloudflare.com | head -1Dòng đầu cho HTTP/2 200 nếu máy chủ hỗ trợ HTTP/2. Lệnh thứ hai chỉ chạy được với bản curl được build kèm HTTP/3; curl có sẵn trên Windows và nhiều bản Linux chưa có, khi đó lệnh báo lỗi tuỳ chọn không được hỗ trợ.
Ba phiên bản khác nhau ở đâu:
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Tầng vận chuyển | TCP | TCP | QUIC trên UDP |
| Nhiều request trên một kết nối | Không (xếp hàng) | Có | Có |
| Đầu dòng bị chặn | Ở tầng HTTP | Ở tầng TCP | Không có |
| Nén header | Không | HPACK | QPACK |
| Đổi mạng giữa chừng | Mất kết nối | Mất kết nối | Giữ được |
Vì sao HTTP/2 không giúp gì cho một API chỉ gọi một request:
Lợi ích chính của HTTP/2 là ghép nhiều request vào MỘT kết nối.
Một request -> không có gì để ghép.
Thời gian của một lời gọi API:
bắt tay TCP + TLS ~40 ms (giống nhau ở cả hai)
gửi request ~1 ms
máy chủ xử lý ~150 ms (phần lớn thời gian)
nhận phản hồi ~5 ms
-> HTTP/2 tiết kiệm được vài byte header. Không đáng kể.HTTP/2 có ích rõ khi: một trang tải 40–80 tài nguyên
-> HTTP/1.1 mở 6 kết nối và xếp hàng
-> HTTP/2 dùng một kết nối cho tất cảVà điều HTTP/3 giải quyết mà HTTP/2 không giải quyết được — đầu dòng bị chặn ở tầng TCP:
HTTP/2: nhiều luồng trên MỘT kết nối TCP
-> một gói tin TCP bị mất -> TCP dừng MỌI luồng để chờ gửi lại
-> ba request còn lại vẫn phải đợi, dù dữ liệu của chúng đã tới
HTTP/3: mỗi luồng độc lập trong QUIC
-> mất gói của luồng 1 -> chỉ luồng 1 chờKhác biệt này gần như không thấy được trên mạng dây ổn định.
Nó rõ rệt trên mạng di động, nơi tỉ lệ mất gói cao hơn hẳn.Bật HTTP/3 trong ASP.NET Core 10:
using Microsoft.AspNetCore.Server.Kestrel.Core;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(o =>
{
o.ListenAnyIP(443, l =>
{
l.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
l.UseHttps();
});
});Trình duyệt không biết trước máy chủ có HTTP/3, nên request đầu tiên luôn đi qua TCP. Máy chủ báo "tôi có HTTP/3" bằng header Alt-Svc, và trình duyệt chuyển sang HTTP/3 cho những request sau. Kestrel tự gửi header này khi bật HTTP/3, không cần viết thêm middleware. Chạy thật trên .NET 10 (Windows 11), cổng 7443:
HTTP/1.1 405 Method Not Allowed
Date: Sat, 17 Oct 2026 14:53:07 GMT
Server: Kestrel
Allow: GET
Alt-Svc: h3=":7443"; ma=86400(Dòng đầu là 405 vì lệnh thử là curl -I, tức HEAD, như đã gặp ở bài 2.4.) Chỉ khi ứng dụng đứng sau một reverse proxy lo phần HTTP/3 thì proxy mới là nơi phải gửi Alt-Svc.
HTTP/3 trong .NET dựa trên thư viện MsQuic: Windows 11 và Windows Server 2022 có sẵn, còn trên Linux phải cài gói libmsquic.
Và một điều cần biết trước khi bật: HTTP/3 dùng UDP cổng 443.
Nhiều tường lửa doanh nghiệp chặn UDP 443
-> trình duyệt thử HTTP/3, thất bại, rồi quay về HTTP/2
-> thêm độ trễ cho lần thử đầu
Nên bật HTTP/3 là một cải thiện có điều kiện, không phải mặc định tốt hơn.Với phần lớn API backend, thứ tự ưu tiên đúng là: giảm số request, giảm kích thước phản hồi, rồi mới tới chọn phiên bản giao thức. Một endpoint trả 200 KB JSON không cần thiết sẽ chậm trên mọi phiên bản.
Bài 3 — Dựng container
Đóng gói một ứng dụng .NET nhỏ thành image và chạy nó bằng Docker.
Tiêu chí hoàn thành: container chạy được và trả lời request, và bạn giải thích được vì sao Dockerfile cần nhiều tầng thay vì một.
Gợi ý và lời giải — Bài 3
Gợi ý. Thứ tự các lệnh trong Dockerfile quyết định lần build thứ hai mất 3 giây hay 3 phút.
Lời giải — Dockerfile nhiều giai đoạn:
# Giai đoạn 1: build — image có SDK
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Sao chép RIÊNG file project trước, rồi restore
COPY ["Crm.Api/Crm.Api.csproj", "Crm.Api/"]
RUN dotnet restore "Crm.Api/Crm.Api.csproj"
# Rồi mới sao chép toàn bộ mã nguồn
COPY . .
RUN dotnet publish "Crm.Api/Crm.Api.csproj" -c Release -o /app/publish --no-restore
# Giai đoạn 2: chạy — chỉ có runtime, nhẹ hơn nhiều
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
# Không chạy bằng root
USER $APP_UID
EXPOSE 8080
ENTRYPOINT ["dotnet", "Crm.Api.dll"]Thử với một project vừa tạo bằng dotnet new web -n Crm.Api -o Crm.Api, đặt Dockerfile ở thư mục cha của Crm.Api:
docker build -t crm-api:thu-nghiem .
docker run --rm -p 8080:8080 crm-api:thu-nghiem
curl -s http://localhost:8080/ # ở terminal khácKết quả chạy thật: Hello World!. Kiểm tra tiến trình trong container không chạy bằng root: docker exec <tên container> id cho uid=1654(app).
Vì sao cần nhiều tầng — hai lý do khác nhau:
1. Nhiều GIAI ĐOẠN (FROM ... AS) để giảm kích thước image.
Đo bằng docker image ls trên Docker Desktop (image 10.0.13):
Image sdk:10.0: 1,3 GB
Image cuối (aspnet:10.0 + ứng dụng): 340 MB
Image cuối KHÔNG cần: trình biên dịch, NuGet, mã nguồn, tệp trung gian
-> chỉ cần thư mục publishVà đây không chỉ là chuyện dung lượng:
- image nhỏ hơn -> deploy nhanh hơn, khởi động pod nhanh hơn
- ít phần mềm hơn -> ít lỗ hổng bảo mật hơn
- không có mã nguồn trong image -> không rò rỉ nếu image bị lộ2. Nhiều TẦNG (mỗi lệnh COPY, RUN) để tận dụng bộ nhớ đệm build.
# SAI — sửa một dòng code cũng phải restore lại toàn bộ gói
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish# ĐÚNG — restore chỉ chạy lại khi file .csproj đổi
COPY ["Crm.Api/Crm.Api.csproj", "Crm.Api/"]
RUN dotnet restore "Crm.Api/Crm.Api.csproj"
COPY . .
RUN dotnet publish -c Release -o /app/publish --no-restoreDocker lưu đệm theo tầng. Một tầng đổi -> mọi tầng SAU nó phải chạy lại.
File .csproj đổi vài tuần một lần.
Mã nguồn đổi vài chục lần mỗi ngày.
-> đặt phần ít đổi TRƯỚC phần hay đổi
-> lần build thứ hai: restore lấy từ đệm, chỉ publish chạy lạiĐây là thứ tự chung cho mọi Dockerfile, không riêng .NET: ít thay đổi ở trên, hay thay đổi ở dưới.
Bốn lỗi thường gặp:
1. Không có .dockerignore.
# .dockerignore
bin/
obj/
.git/
**/node_modules
*.userKhông có nó: COPY . . sao chép cả bin, obj và .git
-> ngữ cảnh build phình từ vài MB lên hàng trăm MB
-> chậm, và tệ hơn: bin/obj của máy bạn có thể ghi đè kết quả build trong container2. Chạy bằng root.
USER $APP_UIDImage .NET chính thức có sẵn biến APP_UID (người dùng không đặc quyền).
Chạy bằng root nghĩa là: nếu ứng dụng bị chiếm quyền, kẻ tấn công có root
trong container — và từ đó dễ đi xa hơn.3. Ghim thẻ không đủ cụ thể.
# Thẻ 10.0 trỏ tới bản vá mới nhất, nội dung đổi theo thời gian
FROM mcr.microsoft.com/dotnet/aspnet:10.0
# Ghim tới bản vá cụ thể: build lại tháng sau vẫn ra đúng image đó
FROM mcr.microsoft.com/dotnet/aspnet:10.0.13Ghim cụ thể thì lặp lại được, nhưng phải tự nhớ nâng phiên bản khi có bản vá bảo mật.
4. Quên đặt cổng cho Kestrel.
Image .NET 8 trở lên mặc định lắng nghe cổng 8080 (không phải 80, đặt bằng biến ASPNETCORE_HTTP_PORTS),
vì người dùng không đặc quyền không mở được cổng dưới 1024.
-> EXPOSE 8080, và map đúng cổng đó khi chạyKiểm chứng image đã gọn chưa:
docker images crm-api:thu-nghiem --format "{{.Size}}"
docker history crm-api:thu-nghiemdocker history cho thấy từng tầng chiếm bao nhiêu
-> một tầng bất thường lớn thường là dấu hiệu quên .dockerignore,
hoặc sao chép nhầm thư mụcNối với mục 2.10.2 ở trên: container giải quyết vấn đề "chạy trên máy tôi thì được" bằng cách đóng gói cả môi trường, không chỉ ứng dụng. Nhưng nó chỉ giữ được lời hứa đó khi image thật sự tự chứa — nên mọi phụ thuộc bên ngoài (chuỗi kết nối, biến môi trường, tệp cấu hình) phải được truyền vào lúc chạy, không nướng vào image.
Tự kiểm tra
Cache-Control và ETag khác nhau thế nào?
Cache-Control cho trình duyệt không gửi request trong thời hạn nên tiết kiệm cả lượt đi về, nhưng có rủi ro dữ liệu cũ. ETag vẫn gửi request nhưng chỉ nhận 304 không có body nếu dữ liệu chưa đổi, nên tiết kiệm băng thông mà luôn đúng.
Vì sao nhầm private thành public là lỗ hổng?
Vì public cho phép CDN và proxy lưu phản hồi, nên dữ liệu riêng của một người dùng có thể được phục vụ cho người khác. Dữ liệu cá nhân phải dùng private hoặc no-store.
Vì sao HTTP/2 không tự động làm API nhanh hơn?
Vì lợi ích chính của nó là multiplexing nhiều request trên một kết nối, giải quyết vấn đề trang web có nhiều tài nguyên nhỏ. Với API trả JSON theo từng request riêng lẻ, vấn đề đó không tồn tại.
HTTP/3 giải quyết vấn đề gì của HTTP/2?
Khi một gói tin TCP bị mất, HTTP/2 bị chặn mọi luồng cho tới khi gói đó được gửi lại. HTTP/3 dùng QUIC trên UDP nên chỉ luồng bị ảnh hưởng phải chờ, lợi ích rõ trên mạng di động hay mất gói.
Khi nào không cần WebSocket?
Khi dữ liệu chỉ đổi vài phút một lần. Gọi lại API định kỳ đơn giản hơn nhiều và không phải lo kết nối đứt, scale nhiều instance hay xử lý kết nối lại.
Vì sao hiểu TTL của DNS lại hữu ích?
Vì nó giải thích tại sao sau khi đổi bản ghi DNS, một phần người dùng vẫn vào địa chỉ cũ trong vài giờ: các resolver còn giữ giá trị cũ cho tới khi hết TTL.
Kết luận
Ba điều đáng nhớ nhất:
- Caching HTTP và container nên học sớm — cả hai dùng hàng ngày.
- HTTP/2 giải một vấn đề cụ thể, và API trả JSON thường không có vấn đề đó.
- WebSocket chỉ cần khi thật sự cần thời gian thực. Gọi lại định kỳ đơn giản hơn nhiều.
Tham khảo
Điều hướng
- Bài trước: 2.9 — .NET Runtime và Ecosystem
- Bài tiếp theo: 2.11 — Ca chẩn đoán: đăng nhập được nhưng mọi trang báo 401
- Về module: Trang mục lục