Skip to content

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 ETag và Cache-Control giú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.

http
GET /api/products/123

HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
ETag: "a3f8b2c1"
http
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ông

Hai cơ chế bổ sung nhau:

Cơ chếCách hoạt độngKhi nào dùng
Cache-ControlTrình duyệt không gửi request trong thời hạnDữ liệu ít đổi, biết trước thời hạn
ETagVẫn gửi request nhưng chỉ nhận 304 nếu không đổiDữ 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:

http
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.

dockerfile
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ào

Cầ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 IPv4Thấp — hạ tầng lo
TLS 1.3Bắt tay nhanh hơn, bỏ thuật toán yếuThấp — bật mặc định
Load balancerPhân phối request cho nhiều serverTrung bình
CDNCache tài nguyên tĩnh gần người dùngTrung bình
DNS nâng caoBản ghi CNAME, A, MX, TXT; TTLTrung bình
gRPCGiao tiếp nhị phân trên HTTP/2Trung 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ênChủ đềVì sao
CaoCaching HTTPRẻ, hiệu quả rõ, dùng hàng ngày
CaoContainer cơ bảnMọi hệ thống hiện đại đều dùng
Trung bìnhHTTP/2, load balancer, DNSHiểu để chẩn đoán, ít khi tự cấu hình
Trung bìnhWebSocketChỉ cần khi có tính năng thời gian thực
ThấpIPv6, TLS chi tiếtHạ 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:

csharp
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.

bash
curl -s -D - -o /dev/null http://127.0.0.1:5288/api/leads/8842

Chạy thật trên .NET 10:

text
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"
bash
curl -s -D - -o /dev/null -H 'If-None-Match: "6bef8b82de92cea1"' \
  http://127.0.0.1:5288/api/leads/8842
text
HTTP/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:

text
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ì:

text
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ại

Và đây là phần quan trọng hơn — ETag KHÔNG tiết kiệm cái gì:

text
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ỏ qua

Hai đoạn dưới đây dùng EF Core (bài 8.10), chỉ cần đọc ý tưởng:

csharp
// 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ông

Muố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:

csharp
// 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ần

Entity 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:

csharp
// Đặ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ý.

text
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á JSON
SQL 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:

csharp
[Timestamp] public byte[] Version { get; set; } = [];

var etag = $"\"{Convert.ToHexString(version)}\"";

Hai kiểu ETag, và cách chọn:

KiểuCách tínhDùng khi
Mạnh — "abc123"Băm nội dung, hoặc cột phiên bảnByte phải giống hệt nhau
Yếu — W/"abc123"Theo phiên bản logicNội dung "tương đương về mặt ngữ nghĩa" là đủ
text
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.

csharp
ctx.Response.Headers.CacheControl = "private, max-age=60";
text
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):

text
Chrome DevTools -> Network -> chuột phải hàng tiêu đề -> tích "Protocol"
text
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 ms

Kiểm tra bằng dòng lệnh, không cần trình duyệt:

bash
curl -sI --http2 https://example.com | head -1
curl -sI --http3 https://cloudflare.com | head -1

Dò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.1HTTP/2HTTP/3
Tầng vận chuyểnTCPTCPQUIC trên UDP
Nhiều request trên một kết nốiKhông (xếp hàng)CóCó
Đầu dòng bị chặnỞ tầng HTTPỞ tầng TCPKhông có
Nén headerKhôngHPACKQPACK
Đổi mạng giữa chừngMất kết nốiMất kết nốiGiữ được

Vì sao HTTP/2 không giúp gì cho một API chỉ gọi một request:

text
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ể.
text
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:

text
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ờ
text
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:

csharp
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:

text
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.

text
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:

dockerfile
# 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:

bash
docker build -t crm-api:thu-nghiem .
docker run --rm -p 8080:8080 crm-api:thu-nghiem
curl -s http://localhost:8080/      # ở terminal khác

Kế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.

text
Đ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 publish
text
Và đâ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.

dockerfile
# 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
dockerfile
# ĐÚ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-restore
text
Docker 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.

text
# .dockerignore
bin/
obj/
.git/
**/node_modules
*.user
text
Khô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 container

2. Chạy bằng root.

dockerfile
USER $APP_UID
text
Image .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ể.

dockerfile
# 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.13

Ghim 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.

text
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ạy

Kiểm chứng image đã gọn chưa:

bash
docker images crm-api:thu-nghiem --format "{{.Size}}"
docker history crm-api:thu-nghiem
text
docker 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ục

Nố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:

  1. Caching HTTP và container nên học sớm — cả hai dùng hàng ngày.
  2. HTTP/2 giải một vấn đề cụ thể, và API trả JSON thường không có vấn đề đó.
  3. 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 ​

Phát hành theo giấy phép MIT.