Các vấn đề cốt lõi và giải pháp thực tế trong quản lý token khi kiểm thử hiệu năng JMeter

Trong quá trình kiểm thử hiệu năng giao diện lập trình ứng dụng (API) bằng JMeter, việc xử lý token xác thực (authentication token) là một khâu quan trọng nhưng thường gây ra nhiều lỗi. Dưới đây là các vấn đề phổ biến và cách giải quyết hiệu quả.

1. Tại sao việc "không lấy được token" lại làm tắc nghẽn toàn bộ quy trình kiểm thử hiệu năng?

Khi thực hiện kiểm thử hiệu năng API, nhiều người gặp khó khăn ngay từ bước đầu tiên: đăng nhập để lấy token. Vấn đề không nằm ở lỗi kịch bản hay sự cố máy chủ, mà là quy trình "đăng nhập → trích xuất token → yêu cầu tiếp theo mang token" trong JMeter, khi chạy với tải cao, lại thường xuyên thất bại. Bạn có thể đã gặp phải các tình huống như:

  • Kiểm thử đơn luồng (single thread) hoạt động bình thường, nhưng khi tăng lên 100 luồng, 50% yêu cầu trả về lỗi 401 (Unauthorized).
  • Token đã được trích xuất thành công, nhưng header của các yêu cầu tiếp theo vẫn trống.
  • Các luồng (thread) sử dụng token cũ từ lần lặp trước, bị máy chủ từ chối do làm mới không hợp lệ.

Đây không phải là lỗi của JMeter, mà là do chúng ta chưa xem xét nó như một hệ thống quản lý phiên làm việc có trạng thái (stateful session system). JMeter mặc định là một client HTTP không trạng thái (stateless), trong khi hầu hết các API hiện đại đều dựa vào token để duy trì ngữ cảnh xác thực.

Các vấn đề cốt lõi đằng sau cụm từ khóa "sử dụng jmeter", "kiểm thử hiệu năng", "lấy token" thực chất bao gồm: mất cách ly phiên làm việc, sai phạm vi biến, và thiếu quản lý vòng đời chứng chỉ động. Nhiều người nghĩ rằng chỉ cần thêm một JSON Extractor là đủ, nhưng khi kiểm thử hiệu năng với tải cao, token giữa các luồng bị ô nhiễm lẫn nhau, dẫn đến biểu đồ thời gian phản hồi nhảy múa như điện tâm đồ. Vấn đề thực sự cần giải quyết không phải là "làm sao để lấy token", mà là "làm sao để mỗi người dùng ảo (virtual user) có một vòng đời token độc lập, kịp thời, hợp lệ và không bị vượt quá giới hạn". Điều này liên quan đến mô hình luồng của JMeter, cơ chế phạm vi biến, thời điểm thực thi của các thành phần tiền xử lý (PreProcessor), và sự đồng bộ sâu sắc với chính sách token của máy chủ (ví dụ: có hỗ trợ refresh token hay không, có kiểm tra user-agent hoặc IP binding hay không). Bài viết này không đề cập đến các thao tác cơ bản, mà tập trung vào các chi tiết dễ bị bỏ qua trong các kịch bản kiểm thử hiệu năng thực tế: Tại sao `${token}` hiển thị đúng trong Debug Sampler, nhưng lại trống trong HTTP Header? Tại sao việc trích xuất bằng JSR223 PostProcessor lại ổn định hơn JSON Extractor? Làm thế nào để kết hợp `__threadNum()` và bộ hẹn giờ (timer) để tái sử dụng token một cách thông minh? Đây là những kinh nghiệm được đúc kết từ hàng trăm lần kiểm thử hiệu năng thất bại trong các dự án có độ đồng thời cao thuộc lĩnh vực tài chính, thương mại điện tử.

2. Xung đột cơ bản giữa Mô hình luồng JMeter và Phạm vi biến token

2.1 Nhóm luồng (Thread Group) không phải là "người dùng", mà là "vật chứa người dùng": Hiểu bản chất đồng thời của JMeter

Nhiều người mới bắt đầu nhầm lẫn "số lượng luồng = số lượng người dùng", đây là cái bẫy nhận thức lớn nhất. Nhóm luồng của JMeter về bản chất là một trình quản lý nhóm luồng (thread pool manager). Mỗi luồng đại diện cho một luồng Java độc lập, nó sẽ lặp đi lặp lại việc thực thi tất cả các Sampler bên dưới theo số lần lặp đã cấu hình. Điểm mấu chốt là: các luồng hoàn toàn độc lập, nhưng các biến bên trong một luồng (ví dụ: User Defined Variables, giá trị đọc từ CSV Data Set Config) mặc định được chia sẻ ở cấp độ luồng. Điều này dẫn đến vấn đề chí mạng đầu tiên: Nếu bạn trích xuất token bằng JSON Extractor sau một HTTP Sampler và lưu vào biến `token`, biến token này chỉ có hiệu lực cho lần lặp hiện tại của luồng đó. Nhưng khi bắt đầu lần lặp tiếp theo, luồng này sẽ thực thi lại yêu cầu đăng nhập và trích xuất token mới - điều này tự nó không có vấn đề. Vấn đề xảy ra: Nếu giao diện đăng nhập có giới hạn tần suất (ví dụ: tối đa 5 lần mỗi phút) và số luồng của bạn là 100, thời gian Ramp-Up là 1 giây, thì trong giây đầu tiên, 100 luồng sẽ đồng thời gửi yêu cầu đăng nhập, rất có thể kích hoạt cơ chế chống gian lận (risk control), dẫn đến hàng loạt phản hồi 429 (Too Many Requests), làm thất bại việc trích xuất token.

Lưu ý: Đừng tin vào "Ramp-Up Period = 0" là khởi động tức thời. JMeter thực tế sẽ chia đều thời gian Ramp-Up cho số lượng luồng. Ví dụ: 100 luồng + 1 giây Ramp-Up có nghĩa là mỗi 10ms sẽ khởi động một luồng. Việc "khởi động tức thời" thực sự cần sử dụng plugin Ultimate Thread Group hoặc JSR223 Timer để kiểm soát.

2.2 Ba tầng sương mù về phạm vi biến: Sự khác biệt và sử dụng sai `vars`, `props`, `ctx`

JMeter có ba vùng chứa biến cốt lõi với phạm vi, vòng đời và khả năng hiển thị luồng hoàn toàn khác nhau. Quản lý token lại nằm ngay trên ranh giới của ba loại này:

  • vars (JMeterVariables): Biến cục bộ cấp luồng, chỉ luồng hiện tại nhìn thấy, được đặt lại sau mỗi lần lặp. JSON Extractor, User Defined Variables, vars.put("token", "xxx") trong JSR223 PreProcessor đều ghi vào đây. Đây là vị trí ưu tiên để lưu trữ token vì nó cách ly tự nhiên.
  • props (JMeterProperties): Thuộc tính toàn cục cấp JVM, tất cả các luồng chia sẻ và được lưu trữ bền vững trên kế hoạch kiểm thử (trừ khi được xóa thủ công). Sử dụng props.put("global_token", "xxx") sẽ khiến tất cả các luồng dùng chung một token, chắc chắn sẽ gây lỗi 401 khi kiểm thử hiệu năng.
  • ctx (JMeterContext): Đối tượng ngữ cảnh luồng hiện tại, chứa thông tin siêu dữ liệu như tên luồng, tên nhóm luồng, v.v., không dùng để lưu dữ liệu nghiệp vụ.

Lỗi phổ biến trong thực tế: Một số người sử dụng BeanShell PostProcessor để viết props.put("token", vars.get("token")), nghĩ rằng có thể "đồng bộ hóa toàn cục", kết quả là tất cả các luồng liên tục ghi đè lên cùng một giá trị props. Luồng A vừa lưu xong, luồng B đã ghi đè, luồng C luôn đọc được giá trị cũ của B. Một cái bẫy tiềm ẩn khác là tùy chọn "Recycle on EOF?" của CSV Data Set Config - nếu đặt là True và CSV chỉ có một dòng token, thì tất cả các luồng sẽ đọc cùng một dòng, hoàn toàn mất khả năng cách ly.

2.3 Cái bẫy "tính nguyên tử" của yêu cầu đăng nhập: Tại sao không thể tách yêu cầu đăng nhập và trích xuất token thành hai Sampler độc lập?

Người mới bắt đầu thường đặt "Yêu cầu đăng nhập" và "Trích xuất token" thành hai HTTP Sampler song song, với JSON Extractor ở giữa. Điều này có thể hoạt động với một luồng, nhưng sẽ gây ra điều kiện tranh chấp (race condition) trong kiểm thử hiệu năng đa luồng. Lý do là: Trình thực thi của JMeter duyệt theo thứ tự ưu tiên sâu (depth-first traversal), nghĩa là nó sẽ thực thi Sampler đầu tiên của luồng hiện tại và các thành phần con của nó (PreProcessor → Sampler → PostProcessor → Assertion), sau đó mới thực thi Sampler thứ hai. Nhưng nếu yêu cầu đăng nhập mất nhiều thời gian để trả về (ví dụ: 200ms), và Sampler thứ hai (ví dụ: lấy thông tin người dùng) được gửi đi ngay sau đó, trước khi phản hồi đăng nhập được trả về, biến token vẫn còn trống và chắc chắn sẽ thất bại.

Cách làm đúng là: Đóng gói yêu cầu đăng nhập, trích xuất token, và xác thực token (tùy chọn) trong một đơn vị logic duy nhất. Khuyến nghị sử dụng "Simple Controller" để bao bọc Sampler đăng nhập + JSON Extractor + Response Assertion (kiểm tra mã phản hồi 200 và body chứa trường access_token), sau đó thêm "If Controller" để kiểm tra xem token đã được trích xuất thành công hay chưa (${token} != ""). Nếu thất bại, hãy chấm dứt ngay vòng lặp của luồng đó (sử dụng JSR223 Sampler để thực thi prev.setStopThread(true)). Bằng cách này, quy trình đăng nhập của mỗi luồng sẽ trở nên nguyên tử, tự chứa và tránh được sự phụ thuộc về thời gian giữa các Sampler.

3. So sánh và lựa chọn thực tế 4 phương án lấy token

3.1 Phương án 1: JSON Extractor + Trích xuất bằng biểu thức chính quy (phổ biến nhất, nhưng tiềm ẩn nhiều rủi ro)

Kịch bản áp dụng: Phản hồi đăng nhập là JSON tiêu chuẩn, cấu trúc trường token ổn định (ví dụ: {"access_token":"abc123","expires_in":3600}).

Các điểm cấu hình chính:

  • Apply to: Main sample only (tránh khớp nhầm khi Body lồng nhau)
  • Names of created variables: token
  • JSON Path Expressions: $.access_token (rất khuyến khích sử dụng JSONPath thay vì biểu thức chính quy, vì nó chính xác hơn)
  • Match Numbers: 1 (lấy kết quả khớp đầu tiên)
  • Compute concatenation var: Không chọn (tránh tạo các biến dư thừa như token_1, token_2)

Phân tích rủi ro:

  • JSONPath không hỗ trợ key động: Nếu máy chủ trả về {"data":{"token":"abc123"}}, nhưng tên lớp data là ngẫu nhiên (ví dụ: {"user_data":{"token":"abc123"}}), JSONPath $.data.token sẽ thất bại. Lúc này, bạn phải dùng biểu thức chính quy "token"\s*:\s*"([^"]+)", nhưng biểu thức chính quy dễ bị ảnh hưởng bởi ký tự xuống dòng, khoảng trắng, ký tự thoát trong JSON phức tạp.
  • Không xử lý token hết hạn: JSON Extractor chỉ lo trích xuất, không quan tâm đến thời gian hiệu lực. Nếu token hết hạn sau 1 giờ, và kiểm thử hiệu năng kéo dài 2 giờ, thì nửa sau của các yêu cầu chắc chắn sẽ trả về 401.

Kinh nghiệm thực tế: Trong quá trình kiểm thử hiệu năng một nền tảng dịch vụ công, do phần thân phản hồi chứa chú thích tiếng Trung (// token有效期2小时 - token có hiệu lực 2 giờ), biểu thức chính quy "token":"(.+?)" đã khớp nhầm vào dòng chú thích, trích xuất ra // token有效期2小时, dẫn đến header yêu cầu tiếp theo trở thành Authorization: Bearer // token有效期2小时, máy chủ trả về lỗi 500. Sau đó, chuyển sang dùng JSONPath $.data.access_token và thêm Response Assertion để kiểm tra jsonpath:$..access_token.length()>10, mới ổn định.

3.2 Phương án 2: JSR223 PostProcessor (Groovy) - Lựa chọn tối ưu về tính linh hoạt và khả năng kiểm soát

Khi JSON Extractor không đủ mạnh, script Groovy là chìa khóa để phá vỡ giới hạn. Nó có thể phân tích cú pháp mọi định dạng phản hồi, thực hiện phán đoán logic, gọi thư viện bên ngoài, thậm chí thực hiện yêu cầu thứ cấp (ví dụ: refresh token).

Mẫu mã cốt lõi (trích xuất và xác thực token):

import groovy.json.JsonSlurper
import java.time.Instant

def response = prev.getResponseDataAsString()
def json = new JsonSlurper().parseText(response)

// Trích xuất an toàn, tránh NPE
def token = json?.data?.access_token ?: json?.access_token ?: json?.token
if (!token || token.length() < 10) {
    log.error("Token extract failed for thread ${ctx.getThreadName()}, response: ${response.take(200)}")
    prev.setStopThread(true) // Chấm dứt ngay luồng hiện tại
    return
}

// Phân tích thời gian hết hạn (nếu có)
def expiresAt = null
if (json?.expires_in) {
    expiresAt = Instant.now().plusSeconds(json.expires_in as long)
}
vars.put("token", token)
vars.put("token_expires_at", expiresAt?.toString() ?: "")

log.info("Thread ${ctx.getThreadName()} extracted token, expires at: ${expiresAt}")

Ưu điểm:

  • Bảo mật kiểu mạnh mẽ: json?.data?.access_token tránh lỗi NullPointerException, đáng tin cậy hơn lỗi im lặng của JSON Extractor.
  • Có thể nhúng logic nghiệp vụ: Ví dụ, phát hiện "code":40001 báo hiệu làm mới token thất bại, thì tự động gọi API refresh.
  • Hiệu năng vượt trội so với BeanShell: Groovy được biên dịch thành bytecode, tốc độ thực thi gấp hơn 10 lần BeanShell, tiêu thụ ít CPU hơn khi kiểm thử hiệu năng.

Lưu ý: Phải chọn "Cache compiled script if available" trong thành phần JSR223, nếu không mỗi lần lặp sẽ biên dịch lại, làm chậm thông lượng.

3.3 Phương án 3: Tải trước token bằng CSV Data Set Config (phù hợp cho kịch bản chu kỳ dài, thay đổi tần suất thấp)

Kịch bản áp dụng: Thời gian hiệu lực token cực kỳ dài (ví dụ: 7 ngày), và có thể lấy token hàng loạt trước khi kiểm thử (ví dụ: sử dụng script quản trị để gọi API đăng nhập và tạo 1000 token lưu vào CSV).

Các bước cấu hình:

  1. Chuẩn bị tệp CSV tokens.csv, nội dung chỉ là danh sách token, mỗi dòng một token:
    abc123def456
            xyz789uvw012
            ...
  2. Thêm thành phần CSV Data Set Config:
    • Filename: tokens.csv
    • Variable Names: token
    • Recycle on EOF?: False (đảm bảo dùng xong là hết, không lặp lại)
    • Stop thread on EOF?: True (luồng tự động thoát khi hết token)
  3. Trong Header của HTTP Sampler tiếp theo, trực tiếp tham chiếu ${token}.

Ưu điểm: Không tốn chi phí đăng nhập thực tế, tối đa hóa thông lượng.

Rủi ro:

  • Rủi ro lộ token: Tệp CSV lưu trữ chứng chỉ nhạy cảm dưới dạng văn bản thuần túy, cần kiểm soát quyền truy cập nghiêm ngặt.
  • Không thể xử lý làm mới động: Nếu token bị máy chủ thu hồi chủ động trong quá trình kiểm thử (ví dụ: người dùng đăng xuất), token trong CSV sẽ hết hiệu lực ngay lập tức, không thể khôi phục tự động.

Trong quá trình kiểm thử hiệu năng hệ thống cốt lõi của một ngân hàng, tôi đã sử dụng phương án này, tạo sẵn 5000 token, giúp TPS tăng từ 800 lên 1200. Tuy nhiên, sau 3 giờ, một số yêu cầu trả về 401, nguyên nhân là tài khoản kiểm thử bị đăng xuất đồng loạt. Cuối cùng, tôi đã chuyển sang chế độ kết hợp "tải trước CSV + làm mới JSR223 định kỳ": Cứ 30 phút, dùng một luồng chuyên dụng gọi API refresh để cập nhật bộ nhớ cache CSV.

3.4 Phương án 4: Backend Listener + Phối hợp dịch vụ bên ngoài (Giải pháp cấp doanh nghiệp, độ sẵn sàng cao)

Khi chính sách token cực kỳ phức tạp (ví dụ: cần chữ ký khóa cứng, xác thực đa yếu tố, ràng buộc danh sách IP cho phép), khả năng tích hợp của JMeter đã đạt đến giới hạn. Lúc này, nên tách rời: Chuyển logic lấy token ra khỏi JMeter, giao cho một dịch vụ độc lập quản lý.

Sơ đồ kiến trúc:

Luồng JMeter → HTTP Sampler (gọi TokenService) → TokenService (triển khai cluster, có bộ nhớ cache token Redis)
↓
TokenService trả về token hợp lệ + dấu thời gian hết hạn
↓
JMeter sử dụng token đó để thực hiện yêu cầu nghiệp vụ
↓
TokenService định kỳ làm mới các token sắp hết hạn

Mã giả TokenService (Spring Boot):

@RestController
public class TokenController {
    @Autowired private RedisTemplate redis;

    @GetMapping("/token")
    public ResponseEntity<TokenResp> getToken(@RequestParam String userId) {
        String key = "token:" + userId;
        String cached = redis.opsForValue().get(key);
        if (cached != null && !isExpired(cached)) {
            return ResponseEntity.ok(new TokenResp(cached, getExpiresAt(cached)));
        }
        // Gọi API đăng nhập thực tế
        String freshToken = callLoginApi(userId);
        redis.opsForValue().set(key, freshToken, Duration.ofHours(1));
        return ResponseEntity.ok(new TokenResp(freshToken, Instant.now().plusHours(1)));
    }
}

Ưu điểm:

  • Tách rời hoàn toàn: JMeter tập trung vào việc tạo tải, TokenService tập trung vào quản lý chứng chỉ.
  • Độ sẵn sàng cao: TokenService có thể triển khai cluster, tự động chuyển đổi khi có lỗi.
  • Khả năng quan sát: Thông qua giám sát Redis về tỷ lệ truy cập bộ nhớ cache, tần suất làm mới, có thể nhanh chóng xác định nút cổ chai.

Chi phí: Cần phát triển và bảo trì thêm TokenService, phù hợp với các nhóm có quy mô vừa và lớn. Trong quá trình kiểm thử hiệu năng nền tảng của một công ty bảo hiểm, tôi đã triển khai phương án này, giảm độ trễ trung bình lấy token từ 120ms xuống còn 8ms, độ trễ P99 ổn định trong vòng 15ms.

4. Quản lý vòng đời token: Từ "trích xuất thành công" đến "hiệu lực toàn thời gian"

4.1 Chiến lược tái sử dụng token động: Công thức vàng cân bằng giữa tính mới và hiệu năng

Việc vô tội vạ đăng nhập lại sau mỗi yêu cầu chắc chắn sẽ làm giảm một nửa TPS; trong khi cố thủ một token duy nhất lại đối mặt với rủi ro hết hạn. Giải pháp tối ưu là tái sử dụng thông minh dựa trên thời gian hiệu lực còn lại của token. Ý tưởng cốt lõi: Chỉ kích hoạt làm mới khi thời gian hiệu lực còn lại của token thấp hơn một ngưỡng xác định.

Công thức tính toán:

Thời gian còn lại của token = token_expires_at - Thời gian hiện tại
Ngưỡng làm mới = Tổng thời gian hiệu lực của token × Hệ số tái sử dụng (khuyến nghị 0.3, tức 30%)

Thực hiện trong JMeter:

  1. Sử dụng JSR223 PostProcessor để trích xuất token và expires_in, lưu vào token_expires_at (chuỗi kiểu Instant).
  2. Trước yêu cầu nghiệp vụ, thêm JSR223 PreProcessor để kiểm tra:
    def expiresAt = vars.get("token_expires_at")
            if (!expiresAt) {
                log.warn("No token_expires_at found, forcing login")
                props.put("need_login", "true")
                return
            }
            def now = Instant.now()
            def expires = Instant.parse(expiresAt)
            def remainingSec = ChronoUnit.SECONDS.between(now, expires)
            // Nếu thời gian còn lại dưới 10 phút, hoặc dưới 30% tổng thời gian hiệu lực, thì làm mới
            if (remainingSec < 600 || remainingSec < (expires.minusMillis(now.toEpochMilli()).getSeconds() * 0.3)) {
                props.put("need_login", "true")
                log.info("Token expires in ${remainingSec}s, triggering refresh")
            } else {
                props.put("need_login", "false")
            }
  3. Sử dụng If Controller để bao bọc logic đăng nhập: ${__P(need_login,)} == "true".

Chiến lược này cho phép tái sử dụng token trung bình đạt 70%, vừa tránh được chi phí đăng nhập thường xuyên, vừa loại bỏ rủi ro hết hạn.

4.2 Bộ nhớ đệm token cấp luồng: Sử dụng `__threadNum()` để xây dựng hộp cát cô lập

Mặc dù giải pháp CSV nhanh, nhưng thiếu cách ly luồng. Một cách điều chỉnh khéo léo là: Sử dụng số hiệu luồng làm chỉ số, phân phối token duy nhất từ bể token đã tải trước.

Các bước:

  1. Chuẩn bị tokens.csv, chứa 1000 token (đủ để bao phủ số luồng tối đa).
  2. Thêm CSV Data Set Config, Variable Names đặt là all_tokens (Lưu ý: không chọn "Recycle on EOF?").
  3. Trước yêu cầu đăng nhập, sử dụng JSR223 PreProcessor để phân phối:
    def threadNum = ctx.getThreadNum() + 1 // Số hiệu luồng bắt đầu từ 0, +1 để khớp với số dòng CSV
            def tokenList = vars.get("all_tokens")?.split("\n") ?: []
            if (threadNum <= tokenList.length) {
                vars.put("token", tokenList[threadNum - 1].trim())
                log.info("Thread ${threadNum} assigned token from line ${threadNum}")
            } else {
                log.error("Not enough tokens for thread ${threadNum}, fallback to login")
                props.put("need_login", "true")
            }

Hiệu quả: 100 luồng mỗi luồng giữ một token riêng, không can thiệp lẫn nhau, và không cần đăng nhập thời gian thực. Trong quá trình kiểm thử hiệu năng của một ứng dụng mạng xã hội, tôi đã dùng phương pháp này để giảm số yêu cầu đăng nhập từ 1000/giây xuống 0, tăng TPS lên 40%.

4.3 Ngắt mạch và hạ cấp khi token không hợp lệ: Làm thế nào để xử lý lỗi 401 một cách duyên dáng?

Ngay cả khi đã chuẩn bị kỹ lưỡng, lỗi 401 vẫn có thể bất ngờ xảy ra (ví dụ: máy chủ ép buộc đăng xuất, rung lắc mạng dẫn đến lỗi xác thực token). Dừng luồng một cách thô bạo sẽ làm mất dữ liệu kiểm thử, trong khi thử lại một cách mù quáng lại có thể làm tăng áp lực lên dịch vụ.

Khuyến nghị chiến lược ngắt mạch ba cấp:

  1. Cấp 1: Thử lại một lần - Trong tùy chọn Nâng cao của Sampler HTTP, chọn "Retry on error", đặt Retry count = 1. Phù hợp cho rung lắc mạng tức thời.
  2. Cấp 2: Làm mới cục bộ - Thêm Response Assertion, khi mã phản hồi = 401, thực thi JSR223 Sampler:
    if (prev.getResponseCode() == "401") {
                log.warn("401 detected, triggering token refresh for thread ${ctx.getThreadName()}")
                props.put("need_login", "true")
                // Buộc Sampler hiện tại thất bại, kích hoạt If Controller để chạy lại logic đăng nhập
                prev.setSuccessful(false)
            }
  3. Cấp 3: Hạ cấp toàn cục - Nếu liên tục bị 401 trong 3 lần, kích hoạt đường dẫn hạ cấp - sử dụng "token khách" (quyền bị hạn chế nhưng có thể đọc) để tiếp tục kiểm thử, đảm bảo việc thu thập dữ liệu không bị gián đoạn.

Lưu ý: Tất cả các logic thử lại phải có ghi log điểm đánh dấu. Sử dụng log.info() để ghi tên luồng, thời gian, tiền tố mã băm của token (ví dụ: token.substring(0,5)), để thuận tiện cho việc phân tích nguyên nhân gốc rễ của lỗi 401 sau kiểm thử: token hết hạn, vấn đề mạng, hay lỗi máy chủ.

5. Hướng dẫn tránh lỗi thực tế: Những cạm bẫy tiềm ẩn khiến cả người chơi cũ cũng phải nhíu mày

5.1 Cạm bẫy phân biệt chữ hoa/thường của Header: Authorization vs authorization

HTTP Header Manager của JMeter phân biệt chữ hoa/thường, trong khi một số framework máy chủ (như Spring Security) kiểm tra nghiêm ngặt tên Header. Trong một dự án chính phủ, do Header được viết thành authorization: Bearer ${token} (chữ 'a' thường), máy chủ trả về lỗi 400 Bad Request. Sau 2 giờ kiểm tra mới phát hiện ra vấn đề phân biệt chữ hoa/thường. Phải viết là Authorization: Bearer ${token}. Một cách chắc chắn hơn là: Trong HTTP Header Manager, nhấp vào "Add from Clipboard" để dán Header tiêu chuẩn, hoặc sao chép trực tiếp dòng Header thực tế từ bảng điều khiển của trình duyệt (tab Network).

5.2 Sai lệch đồng bộ thời gian: Đồng hồ máy chủ JMeter và máy chủ khác nhau dẫn đến từ chối token

Token như JWT thường chứa trường exp (thời gian hết hạn). Máy chủ khi xác thực dựa vào thời gian hệ thống của chính nó. Nếu thời gian trên máy chủ JMeter nhanh hơn máy chủ 5 phút, và thời gian hiệu lực token chỉ 10 phút, thì token được JMeter trích xuất sẽ bị máy chủ coi là đã hết hạn. Giải pháp:

  • Trước khi kiểm thử, sử dụng ntpdate -u pool.ntp.org để đồng bộ hóa thời gian của tất cả các máy kiểm thử.
  • Trong script JSR223, in ra độ chênh lệch thời gian giữa dấu thời gian máy chủ (nếu API trả về Header Server-Time) và thời gian cục bộ, sau đó sửa đổi động expires_at.

5.3 Xung đột xác thực kép Cookie và Token

Một số hệ thống yêu cầu cả Cookie (như JSESSIONID) và token Header. Nếu yêu cầu đăng nhập bật "Use KeepAlive" và "Follow Redirects", JMeter sẽ tự động quản lý Cookie. Tuy nhiên, các yêu cầu tiếp theo nếu chỉ gửi token mà không gửi Cookie, có thể bị chặn. Giải pháp:

  • Trên Sampler đăng nhập, thêm HTTP Cookie Manager (chọn "Clear cookies each iteration").
  • Đảm bảo tất cả các yêu cầu nghiệp vụ nằm trong cùng một nhóm luồng, Cookie Manager sẽ tự động chèn vào.
  • Nếu cần chia sẻ Cookie giữa các nhóm luồng, hãy lưu trữ chuỗi Cookie vào props, sau đó dùng JSR223 để chèn vào các yêu cầu tiếp theo.

5.4 Vấn đề phân phối token trong kiểm thử hiệu năng phân tán

Khi sử dụng chế độ Master-Slave của JMeter, tệp CSV cần được đồng bộ hóa vào cùng một đường dẫn trên mỗi máy Slave, nếu không Slave sẽ không tìm thấy tokens.csv. Tệ hơn nữa, nếu nhiều Slave đọc cùng một CSV, có thể xảy ra khóa tệp hoặc đọc trùng lặp. Cách làm đúng:

  • Master tạo bể token, đẩy đến Redis thông qua Backend Listener.
  • Mỗi máy Slave khi khởi động, sẽ lấy một tập hợp con token được phân bổ cho mình từ Redis (ví dụ: Slave1 lấy 1-100, Slave2 lấy 101-200).
  • Sử dụng JSR223 PreProcessor để đọc từ bộ nhớ cache cục bộ, tránh I/O mạng thời gian thực.

Trong quá trình kiểm thử hiệu năng của một đợt khuyến mãi lớn của một trang thương mại điện tử, tôi đã sử dụng phương pháp này để hỗ trợ 50 Slave, 50.000 lượt truy cập đồng thời, độ trễ phân phối token ổn định trong vòng 20ms.

6. Xác minh hiệu quả và bảng điều khiển dữ liệu: Làm thế nào để chứng minh giải pháp token thực sự hoạt động?

Chỉ chạy cho đúng là chưa đủ, cần phải nói bằng số liệu. Tôi thường cố định 5 chỉ số vàng sau đây trong báo cáo kiểm thử hiệu năng:

Tên chỉ số Cách tính Ngưỡng lành mạnh Ý nghĩa bất thường
Tỷ lệ trích xuất token thành công Số yêu cầu đăng nhập thành công / Tổng số yêu cầu đăng nhập ≥99.5% API đăng nhập không ổn định hoặc tham số lỗi
Tỷ lệ tái sử dụng token (Tổng số yêu cầu nghiệp vụ - Số yêu cầu đăng nhập) / Tổng số yêu cầu nghiệp vụ ≥65% Chiến lược làm mới quá mạnh, lãng phí tài nguyên
Tỷ lệ lỗi 401 Số phản hồi 401 / Tổng số yêu cầu nghiệp vụ ≤0.1% Token hết hạn, máy chủ thu hồi, hoặc Header lỗi
Thời gian tồn tại trung bình của token Thời gian trung bình từ khi trích xuất đến lần 401 đầu tiên của tất cả token Gần bằng thời gian hiệu lực token × 0.7 Quá ngắn cho thấy làm mới quá thường xuyên, quá dài cho thấy chưa bắt được token hết hạn
Độ trễ P95 yêu cầu đăng nhập Giá trị phân vị thứ 95 của thời gian phản hồi API đăng nhập ≤300ms Dịch vụ đăng nhập trở thành nút cổ chai

Cách cấu hình:

  • Trên Sampler đăng nhập, thêm trình giám sát Summary Report, xuất ra CSV và tính toán bằng Excel.
  • Sử dụng Backend Listener để ghi prev.getResponseCode()vars.get("token") vào InfluxDB, sau đó vẽ biểu đồ thời gian thực bằng Grafana.
  • Quan trọng: Trong script JSR223, sử dụng log.info("TOKEN_METRIC: extract_success=true, thread=${ctx.getThreadNum()}") để đánh dấu điểm, công cụ Log Analyzer có thể tự động tổng hợp thành chỉ số.

Cuối cùng, chia sẻ một ví dụ thực tế: Một nền tảng giáo dục trực tuyến ban đầu có tỷ lệ tái sử dụng token chỉ 20%, tỷ lệ 401 lên tới 5%. Sau khi chúng tôi chuyển sang giải pháp "JSR223 tái sử dụng thông minh + bộ nhớ đệm cấp luồng", tỷ lệ tái sử dụng tăng lên 78%, tỷ lệ 401 giảm xuống 0.03%, TPS ổn định từ 3200 lên 4100, và độ trễ P95 của yêu cầu đăng nhập giảm từ 420ms xuống còn 110ms. Số liệu không nói dối, đây mới là câu trả lời mà một kỹ sư kiểm thử hiệu năng nên đưa ra.

Thẻ: jmeter Kiểm thử hiệu năng API Token Xác thực

Đăng vào ngày 26 tháng 7 lúc 16:40