8 phút

Kiểm thử tải Go trước khi tuyển kỹ sư backend đầu tiên

Dùng kiểm thử tải Go để mô phỏng lưu lượng thực tế, đo độ trễ p95, áp lực cơ sở dữ liệu, bộ nhớ và lỗi, rồi quyết định có nên tuyển người hay không.

Kiểm thử tải Go trước khi tuyển kỹ sư backend đầu tiên

Mười nghìn người dùng hoạt động hằng tháng không phải là một yêu cầu về năng lực. Đó là số liệu thanh toán hoặc phân tích. Một dịch vụ Go có thể phục vụ nhiều hơn thế rất nhiều khi yêu cầu nhẹ và phân tán trong tháng, hoặc sập với vài trăm người dùng khi mỗi phiên kéo theo các truy vấn chậm, tải tệp lên và lệnh gọi bên thứ ba.

Câu hỏi hữu ích là liệu backend được tạo có đáp ứng một mục tiêu dịch vụ đã nêu dưới lưu lượng cao điểm dự kiến hay không, với đủ dư địa cho tăng trưởng và một sự cố thông thường. Bạn có thể trả lời trước khi tuyển kỹ sư backend, nhưng chỉ khi kiểm thử tải giống sản phẩm của bạn và ghi lại đồng thời tình trạng của API, runtime Go và PostgreSQL. Biểu đồ độ trễ trung bình màu xanh hầu như không chứng minh được gì.

Đây là bài kiểm thử tôi sẽ yêu cầu trước khi nói với nhà sáng lập rằng backend Go được tạo đã sẵn sàng cho 10.000 người dùng hoạt động hằng tháng. Nó cho kết quả đạt hoặc không đạt có thể lặp lại, chỉ ra nút thắt đầu tiên và phân biệt vấn đề năng lực với vấn đề tính đúng đắn.

Người dùng hằng tháng phải được chuyển thành số yêu cầu cao điểm

Hãy chuyển dự báo người dùng thành số yêu cầu mỗi giây trước khi chọn bất kỳ mức tải nào. Người dùng hoạt động hằng tháng che giấu hai biến số quyết định backend: có bao nhiêu phiên đến trong khung giờ bận nhất và mỗi phiên tạo ra bao nhiêu công việc.

Nếu có private beta, hãy bắt đầu từ dữ liệu quan sát được. Đếm số phiên trong 15 phút bận nhất, số yêu cầu mỗi phiên và tỷ lệ các tuyến. Nếu chưa có lưu lượng, hãy ghi các giả định để mọi người có thể xem xét. Ví dụ, giả sử 10.000 người dùng hoạt động tạo tám phiên mỗi tháng, 15 yêu cầu API mỗi phiên và 20% lưu lượng hằng ngày rơi vào giờ bận nhất. Con số đó tạo ra khoảng 8 yêu cầu mỗi giây trong một ngày bận trung bình. Ra mắt, thông báo, hạn chót trả lương hoặc một múi giờ chung có thể khiến đỉnh thực tế cao hơn nhiều lần.

Đừng biến phép tính này thành độ chính xác giả tạo. Hãy dùng nó để xác định ba mức kiểm thử:

  • Đỉnh dự kiến: mức tải bận nhất mà bạn hiện dự đoán.
  • Đỉnh tăng trưởng: gấp đôi đỉnh dự kiến, trừ khi doanh nghiệp có dự báo tốt hơn.
  • Mức stress: tăng lưu lượng cho đến khi một mục tiêu dịch vụ không đạt hoặc tài nguyên bão hòa.

Các bài kiểm thử đỉnh dự kiến và tăng trưởng trả lời liệu kế hoạch ra mắt có dư địa hay không. Kiểm thử stress cho biết thứ gì hỏng trước và người dùng thấy lỗi như thế nào. Câu trả lời cuối cùng quan trọng vì một dịch vụ từ chối nhanh công việc dư thừa dễ vận hành hơn dịch vụ chiếm hết mọi kết nối cơ sở dữ liệu và làm các tuyến không liên quan bị treo.

Hãy dùng mô hình workload mở cho bài kiểm thử năng lực. Mô hình mở khởi tạo yêu cầu theo tốc độ đến cố định ngay cả khi các yêu cầu trước đó chậm lại. Mô hình đóng với số virtual user cố định thường che giấu sự sụp đổ: phản hồi chậm làm những user đó gửi ít yêu cầu mới hơn, nên tải đưa vào giảm đúng lúc dịch vụ gặp khó. Tài liệu k6 của Grafana nêu rõ khác biệt này qua arrival-rate executor và báo cáo dropped_iterations khi bộ tạo tải không thể bắt đầu công việc đã lên lịch. Hãy coi dropped iteration là lỗi của bộ tạo kiểm thử, không phải thành công của máy chủ.

Chạy mỗi mức ổn định ít nhất 30 phút sau giai đoạn làm nóng. Kiểm thử năm phút sẽ bỏ lỡ thay đổi kết nối, chu kỳ garbage collection, đẩy cache ra ngoài, tác vụ nền và bộ nhớ tăng dần. Thêm một soak test hai giờ ở đỉnh dự kiến sau khi bài ngắn hơn đạt yêu cầu.

Viết phép quy đổi thành một bảng tính nhỏ và hiển thị mọi đơn vị. Người dùng hằng tháng nhân với số phiên mỗi người dùng và số yêu cầu mỗi phiên cho ra số yêu cầu hằng tháng. Chỉ chia sau khi đã phân bổ lưu lượng cho các ngày vận hành và giờ bận nhất. Sau đó thêm lưu lượng do retry, tác vụ nền, webhook và polling mà phân tích hành vi người dùng có thể không tính. Một frontend polling mỗi mười giây có thể tạo nhiều công việc API hơn các lượt nhấp mở trang.

Hãy mô hình hóa burst tách biệt với đỉnh ổn định. Đăng nhập sau thông báo, hoàn tất import hoặc client retry sau một sự cố ngắn có thể dồn công việc vào một phút. Thêm một giai đoạn burst nhanh chóng đạt tốc độ burst dự kiến, giữ đủ lâu để lấp đầy hàng đợi rồi trở về bình thường. Dịch vụ phải phục hồi mà không có backlog tăng dần hoặc cần khởi động lại thủ công. Ghi lại thời gian phục hồi như một kết quả. Hệ thống có thể đạt bài kiểm thử ổn định nhưng vẫn không an toàn nếu một burst ngắn khiến pool hoặc worker bị kẹt.

Đừng nhân tốc độ cuối cùng với một hệ số an toàn tùy ý rồi gọi đó là thực tế. Gắn dư địa với một bất định kinh doanh: sai lệch dự báo, chiến dịch đã lên kế hoạch, một replica không khả dụng hoặc thời gian cần để bổ sung năng lực. Kiểm thử từng giả định bạn định dựa vào. Giữ bảng tính cạnh kết quả, vì một lần chạy đạt yêu cầu sẽ mất ý nghĩa khi không ai nhớ dự báo và giả định burst nào đã tạo ra mục tiêu.

Tỷ lệ lưu lượng phải giống một phiên thực tế

Một bài kiểm thử tải thực tế giữ nguyên tần suất tuyến, kích thước payload, xác thực, phân bố dữ liệu, thời gian suy nghĩ và tranh chấp ghi. Gọi /health 100 lần mỗi giây chỉ đo handler health, không đo ứng dụng.

Nếu có thể, xây dựng tỷ lệ từ access log. Nhóm tuyến theo hành động kinh doanh thay vì URL thô, vì /projects/123/projects/456 có cùng một dạng. Một tỷ lệ SaaS giai đoạn đầu có thể dành 45% cho đọc danh sách và chi tiết, 20% cho tìm kiếm, 15% cho tạo hoặc cập nhật, 10% cho đăng nhập và làm mới token, 10% cho xuất dữ liệu hoặc công việc nặng khác. Con số của bạn phải đến từ luồng sản phẩm, không phải ví dụ này.

Dùng nhiều tài khoản và bản ghi kiểm thử. Dùng lại một tài khoản có thể tạo cache nóng bất thường, tuần tự hóa cập nhật trên một dòng hoặc kích hoạt rate limit mà lưu lượng thật sẽ phân tán. Seed tenant nhỏ, vừa và lớn. Bao gồm bản ghi thiếu, đầu vào không hợp lệ và lỗi phân quyền, vì đường lỗi thường truy vấn cơ sở dữ liệu hoặc cấp phát response body khác với đường thành công.

Giữ tải tệp lớn và xuất dữ liệu dài trong kịch bản riêng nếu chúng có mục tiêu dịch vụ khác. Tuy vậy, vẫn chạy đồng thời với lưu lượng thông thường. Nếu không, bài kiểm thử sẽ bỏ lỡ đúng sự cố người dùng nhận ra: một loại export chiếm pool trong khi trang cài đặt đơn giản phải chờ phía sau.

Đừng mock PostgreSQL, object storage, queue hoặc dịch vụ bên ngoài trong lần chạy năng lực cuối cùng. Mock hữu ích để cô lập chi phí handler, nhưng nó loại bỏ các phụ thuộc dễ quyết định năng lực nhất. Hãy trỏ bài kiểm thử vào staging stack có cùng kích cỡ instance, cài đặt cơ sở dữ liệu, index, giới hạn kết nối và đường mạng như production. Dữ liệu đã làm sạch nhưng có hình dạng production tốt hơn một nghìn dòng seed giống hệt nhau.

Tránh kiểm thử qua content delivery cache nếu API bình thường không được cache. Ngược lại, hãy giữ cache thật trong đường đi khi production sử dụng nó. Mục đích không phải làm backend trông bận rộn. Mục đích là tái hiện công việc mà yêu cầu của người dùng thực sự gây ra.

Một bài kiểm thử k6 có thể chạy nên mã hóa hợp đồng

Đưa threshold và các giai đoạn lưu lượng vào version control để một lần chạy không trở thành ảnh chụp màn hình được diễn giải sau đó. k6 coi threshold là tiêu chí đạt hoặc không đạt và thoát với mã khác 0 khi chúng không đạt, nên kết quả phù hợp làm kiểm tra phát hành.

Khung sau tạo một phiên hỗn hợp theo arrival rate, kiểm tra ngữ nghĩa phản hồi và đặt giới hạn độ trễ riêng cho lượt đọc thông thường và export nặng. Thay các tuyến, payload và mục tiêu bằng giá trị đã thống nhất cho sản phẩm của bạn. Mã trong bài kiểm thử được cố ý viết đơn giản để một kiểm tra thất bại tương ứng với một hành động người dùng.

import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';

const businessErrors = new Rate('business_errors');

export const options = {
  scenarios: {
    expected_peak: {
      executor: 'ramping-arrival-rate',
      startRate: 5,
      timeUnit: '1s',
      preAllocatedVUs: 40,
      maxVUs: 200,
      stages: [
        { target: 10, duration: '5m' },
        { target: 10, duration: '30m' },
        { target: 20, duration: '10m' },
        { target: 20, duration: '30m' },
      ],
    },
  },
  thresholds: {
    'http_req_duration{name:project_list}': ['p(95)<300'],
    'http_req_duration{name:project_create}': ['p(95)<500'],
    'http_req_duration{name:export}': ['p(95)<2000'],
    http_req_failed: ['rate<0.01'],
    business_errors: ['rate<0.005'],
    dropped_iterations: ['count==0'],
  },
};

export function setup() {
  const response = http.post(`${__ENV.BASE_URL}/api/login`, JSON.stringify({
    email: __ENV.TEST_EMAIL,
    password: __ENV.TEST_PASSWORD,
  }), { headers: { 'Content-Type': 'application/json' } });
  check(response, { 'login succeeds': r => r.status === 200 });
  return { token: response.json('token') };
}

export default function (data) {
  const headers = { Authorization: `Bearer ${data.token}`, 'Content-Type': 'application/json' };
  const list = http.get(`${__ENV.BASE_URL}/api/projects?limit=25`, {
    headers,
    tags: { name: 'project_list' },
  });
  businessErrors.add(!check(list, {
    'list status is 200': r => r.status === 200,
    'list has items': r => Array.isArray(r.json('items')),
  }));

  if (Math.random() < 0.25) {
    const create = http.post(`${__ENV.BASE_URL}/api/projects`, JSON.stringify({
      name: `load-${__VU}-${__ITER}`,
    }), { headers, tags: { name: 'project_create' } });
    businessErrors.add(!check(create, { 'create status is 201': r => r.status === 201 }));
  }

  if (Math.random() < 0.03) {
    const runExport = http.post(`${__ENV.BASE_URL}/api/exports`, '{}', {
      headers,
      tags: { name: 'export' },
    });
    businessErrors.add(!check(runExport, { 'export accepted': r => r.status === 202 }));
  }

  sleep(Math.random() * 2 + 1);
}

Các mục tiêu mẫu là điểm khởi đầu, không phải cam kết áp dụng cho mọi nơi. Hãy đặt p95 cho từng nhóm tuyến dựa trên độ chờ mà người dùng chấp nhận được và yêu cầu riêng của sản phẩm. Đừng dùng một ngưỡng chung 300 ms để đánh giá export bất đồng bộ và yêu cầu autocomplete.

Chạy script từ máy không lưu trữ ứng dụng. Xác nhận máy tạo tải còn dư CPU và không có dropped iteration. Lưu chính xác commit, cấu hình môi trường, mã nhận diện snapshot dữ liệu, lệnh chạy và đầu ra kiểm thử thô. Nếu thiếu chúng, việc so sánh sau này chủ yếu dựa vào trí nhớ và sự lạc quan.

Hiệu chỉnh máy tạo tải trước khi tin vào một lần chạy dài. Trỏ nó vào handler nhỏ không làm việc với cơ sở dữ liệu, tăng arrival rate yêu cầu cao hơn bài kiểm thử dự kiến và kiểm tra rằng máy tạo tải duy trì được tốc độ mà không cạn CPU, socket hay mạng của chính nó. Khi k6 thêm virtual user hoặc báo dropped iteration, máy tạo tải có thể là giới hạn. Chỉ phân tán việc tạo tải sang nhiều máy khi một máy không thể tạo đủ công việc cần thiết, và giữ đồng bộ đồng hồ để biểu đồ máy chủ và client khớp nhau.

Cho mỗi lần chạy một baseline yên tĩnh. Dừng migration, import dữ liệu và tác vụ staging không liên quan, trừ khi những tác vụ đó cũng chạy vào lúc cao điểm thật. Sau đó lên lịch bài kiểm thử thứ hai với công việc nền thật được bật. Cặp bài này cho biết cả năng lực API sạch và năng lực vận hành mà người dùng sẽ nhận được. Nếu chỉ lần chạy yên tĩnh đạt, kế hoạch ra mắt đang dựa vào một staging hư cấu.

Tạo script thứ hai cho một hành trình người dùng và chạy với một virtual user trước khi thêm tải. Kiểm tra từng phản hồi, bản ghi đã tạo và thao tác dọn dẹp. Việc này bắt được token sai, một check luôn trả về true hoặc dữ liệu kiểm thử bị trùng sau vòng lặp đầu tiên. Kết quả năng lực vô nghĩa nếu script đang chạy trên trang lỗi hoặc liên tục đọc cùng một object đã cache.

p95 cần ngữ cảnh theo từng tuyến

Dùng độ trễ p95 vì trung bình che khuất một nhóm nhỏ chậm, nhưng đừng đọc p95 một mình. Với p95 là 800 ms, một trong hai mươi yêu cầu mất ít nhất từng đó thời gian, đủ khiến trang có nhiều yêu cầu luôn có cảm giác chậm. Phân vị này cũng nhiễu ở các tuyến có quá ít mẫu, nên hãy báo số lượng yêu cầu bên cạnh nó.

Ghi lại p50, p95, p99, mức tối đa, throughput và tỷ lệ lỗi cho từng hành động kinh doanh đã đặt tên. p50 cho thấy hành vi bình thường, p95 là cổng dịch vụ thực tế và p99 cho thấy phần đuôi mà không để một giá trị tối đa đơn lẻ chi phối cuộc thảo luận. Chia kết quả theo mã trạng thái. Phản hồi 500 nhanh không được làm câu chuyện về độ trễ tốt hơn.

Đo thời gian xử lý phía máy chủ cùng với thời gian client quan sát. Chênh lệch gồm thiết lập kết nối, proxy, thời gian mạng và truyền phản hồi. Nếu p95 phía client tăng trong khi p95 handler không đổi, hãy tìm nguyên nhân ngoài handler. Nếu cả hai cùng tăng và thời gian chờ cơ sở dữ liệu tăng, yêu cầu có lẽ đang xếp hàng chờ kết nối hoặc truy vấn.

Kết quả làm nóng và trạng thái ổn định phải tách riêng. Bản nhị phân Go đã triển khai không biên dịch lúc chạy, nhưng cache lạnh, kết nối cơ sở dữ liệu mới, khởi tạo lười và autoscaling có thể làm méo vài phút đầu. Người dùng vẫn trải nghiệm hành vi lạnh, vì vậy hãy giữ nó như một kết quả riêng thay vì xóa đi.

Giá trị trung bình vẫn hữu ích cho hạch toán tài nguyên. Tổng thời gian cơ sở dữ liệu chia cho số lần gọi có thể phát hiện một truy vấn chậm vừa phải nhưng xuất hiện cực nhiều. Tuy nhiên, nó không thể thay thế mục tiêu dịch vụ theo phân vị. Một khác biệt thường bị làm mờ là độ trễ so với năng lực: độ trễ mô tả thời gian công việc đã hoàn thành, còn năng lực mô tả lượng công việc đưa vào mà dịch vụ có thể duy trì không làm hàng đợi hay lỗi tăng lên. Độ trễ tốt ở tốc độ yêu cầu đạt được thấp không chứng minh năng lực.

Xác định thất bại trước khi chạy. Tôi sẽ coi bài kiểm thử ra mắt không đạt khi bất kỳ tuyến quan trọng nào bỏ lỡ mục tiêu p95, lỗi HTTP không mong đợi vượt tỷ lệ đã thống nhất, kiểm tra nghiệp vụ thất bại, lần lặp đã lên lịch bị bỏ hoặc một tài nguyên còn bão hòa. Đạt bốn trong năm cổng vẫn là lần chạy không đạt, nhưng có dữ liệu chẩn đoán hữu ích.

Thời gian chờ kết nối cho thấy hàng đợi cơ sở dữ liệu ẩn

Hoàn tác thay đổi hiệu năng kém
Snapshot và rollback tạo đường khôi phục cho các thay đổi backend được tạo tự động khi bản phát hành không đạt mục tiêu.

Hãy instrument database/sql trước bài kiểm thử, vì độ trễ ứng dụng không thể cho biết PostgreSQL chậm hay ứng dụng đang chờ để kết nối. DB.Stats() của Go báo OpenConnections, InUse, Idle, WaitCountWaitDuration, cùng các bộ đếm đóng kết nối. Xuất chúng sang hệ thống metrics vài giây một lần.

func recordDBStats(ctx context.Context, db *sql.DB, g GaugeSet) {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            s := db.Stats()
            g.Set("db_open_connections", float64(s.OpenConnections))
            g.Set("db_in_use_connections", float64(s.InUse))
            g.Set("db_idle_connections", float64(s.Idle))
            g.Set("db_wait_count_total", float64(s.WaitCount))
            g.Set("db_wait_seconds_total", s.WaitDuration.Seconds())
        }
    }
}

Tính thay đổi của các bộ đếm tích lũy trong khoảng ổn định. WaitCount tăng nghĩa là yêu cầu đã phải chờ kết nối rảnh. Lấy thay đổi của WaitDuration chia cho thay đổi của WaitCount sẽ ra thời gian chờ pool trung bình trong khoảng đó. Vẽ InUse so với MaxOpenConnections; đường phẳng ở giới hạn cùng thời gian chờ tăng là pool đã bão hòa.

Đừng phản ứng bằng cách tăng SetMaxOpenConns cho đến khi biểu đồ trông đẹp hơn. Cách sửa phổ biến này chuyển hàng đợi vào PostgreSQL và có thể làm tăng tranh chấp, dùng bộ nhớ và độ trễ truy vấn. Trước hết, hãy tìm lý do kết nối bận lâu: truy vấn chậm, transaction bị giữ trong lúc gọi mạng, đọc từng dòng một hoặc quên gọi Rows.Close(). Sau đó xác định kích cỡ pool trong ngân sách kết nối của cơ sở dữ liệu trên mọi replica ứng dụng và worker.

Tài liệu Go nói giá trị SetMaxOpenConns không dương khiến pool không có giới hạn. Không giới hạn là mặc định production nguy hiểm khi vài replica có thể cùng mở kết nối. Hãy đặt giới hạn rõ ràng, chủ động thiết lập hành vi idle và lifetime, đồng thời dành năng lực cơ sở dữ liệu cho migration, quản trị và công việc nền.

Theo dõi riêng thời lượng transaction. Một handler có thể trả về trong 200 ms nhưng dọn dẹp trì hoãn hoặc transaction bị rò rỉ vẫn giữ kết nối lâu hơn nhiều. Metrics pool cho thấy áp lực, nhưng trace hoặc thời gian transaction mới chỉ ra chủ sở hữu.

Bằng chứng truy vấn chậm phải đến từ PostgreSQL

Bật pg_stat_statements trong môi trường kiểm thử và chụp snapshot trước, sau mỗi lần chạy. Tài liệu PostgreSQL mô tả nó theo dõi thống kê lập kế hoạch và thực thi cho các statement đã chuẩn hóa. View của nó có số lần gọi, số dòng, tổng và trung bình thời gian thực thi, hoạt động block và hoạt động block tạm. Bằng chứng này đáng tin hơn nhiều so với đoán từ truy vấn xuất hiện trong một trace.

Dùng phần chênh lệch giữa các snapshot vì view có tính tích lũy. Chỉ reset trong cơ sở dữ liệu kiểm thử tách biệt vì reset sẽ phá hủy bằng chứng của công việc khác. Truy vấn này tìm các statement tiêu tốn nhiều thời gian thực thi nhất trong một khoảng kiểm thử sạch:

SELECT
  queryid,
  calls,
  round(total_exec_time::numeric, 1) AS total_ms,
  round(mean_exec_time::numeric, 2) AS mean_ms,
  rows,
  shared_blks_read,
  temp_blks_written
FROM pg_stat_statements
WHERE calls > 20
ORDER BY total_exec_time DESC
LIMIT 20;

Tổng thời gian tìm các truy vấn thường xuyên chi phối công việc cơ sở dữ liệu. Thời gian trung bình tìm các statement chậm riêng lẻ. Cả hai đều không cho p95 của truy vấn vì pg_stat_statements tổng hợp các lần gọi; hãy dùng trace hoặc histogram thời lượng khi độ trễ đuôi của một statement quan trọng. Đây là một khác biệt khác thường bị làm mờ: truy vấn chậm có thể là thời gian thực thi trung bình cao, thời gian đuôi cao hoặc tổng chi phí rất lớn do gọi thường xuyên. Mỗi trường hợp cần cách sửa khác nhau.

Với các statement hàng đầu, chạy EXPLAIN (ANALYZE, BUFFERS) với tham số an toàn, đại diện ngoài bài kiểm thử tải có tính thời gian. ANALYZE thực thi statement, nên hãy bọc statement thay đổi dữ liệu trong transaction rồi rollback, hoặc kiểm tra trên bản sao có thể hủy. Tìm các estimate chênh lệch mạnh với số dòng thực, vòng lặp lặp lại, sequential scan trên bảng lớn nhưng điều kiện chọn lọc, sort tràn ra đĩa và nhiều shared block được đọc.

Mã được tạo thường tạo mẫu N+1 trông vẫn ổn với dữ liệu seed: lấy 25 project, rồi tạo một truy vấn owner và một truy vấn count cho mỗi project. Ở mười yêu cầu mỗi giây, một endpoint đó có thể tạo hơn 500 statement cơ sở dữ liệu mỗi giây trước khi bất kỳ tuyến nào khác chạy. Cách sửa có thể là join, WHERE id = ANY($1) theo lô hoặc count tính sẵn. Tăng giới hạn pool vẫn giữ nguyên lãng phí.

Hãy ghi lại cả lock wait. Một truy vấn nhanh khi tách riêng có thể bị treo khi cập nhật đồng thời cùng tenant, tài khoản hoặc sequence. Khi độ trễ chỉ tăng trong kịch bản ghi, hãy kiểm tra wait đang hoạt động và ranh giới transaction thay vì phản xạ thêm index.

Bộ nhớ phải ổn định dưới tải đều

Xuất cả những phần ngoài luồng lý tưởng
Mang mã nguồn Go theo bạn khi truy vấn chậm hoặc bộ nhớ bị giữ lại cần được chẩn đoán trực tiếp.

Đánh giá bộ nhớ qua hình dạng theo thời gian, không phải một đỉnh duy nhất. Một tiến trình Go tăng trong giai đoạn làm nóng rồi dao động quanh mức ổn định khác với tiến trình có đường nền sau garbage collection tăng liên tục suốt soak test hai giờ.

Ghi resident memory của tiến trình, heap allocation của Go, heap object, số goroutine, tần suất garbage collection, thời gian pause và tốc độ cấp phát. Bộ nhớ container quan trọng vì hệ điều hành giết tiến trình theo giới hạn của nó, không phải chỉ theo Go heap. So sánh bộ nhớ sau garbage collection ở mức tải tương tự. Cách này loại bỏ phần lớn dạng răng cưa bình thường và làm phần bị giữ lại lộ rõ.

Hiển thị standard Go runtime metrics hoặc endpoint profiling được bảo vệ trong staging. Lấy heap profile gần đầu và cuối soak test, sau đó so sánh các vị trí cấp phát được giữ lại bằng go tool pprof. Lấy cả goroutine profile. Số goroutine tăng có thể cho thấy yêu cầu bị kẹt trên channel, response body chưa đóng hoặc công việc nền được bắt đầu mà không có cancellation.

Đừng đặt GOMEMLIMIT bằng đúng giới hạn container. Tiến trình còn cần bộ nhớ cho goroutine stack, executable mapping, buffer của database driver và các cấp phát ngoài heap khác. Hãy chừa dư địa và chứng minh điều đó với payload lớn nhất. Bài kiểm thử chỉ có JSON body nhỏ nói rất ít về endpoint đọc tệp tải lên 20 MB vào bộ nhớ.

Ép chạy các tình huống khó: request body tối đa được chấp nhận, kết quả truy vấn lớn, client hủy, timeout và export lặp lại. Xác nhận bộ nhớ quay về sau khi công việc hoàn tất. Đồng thời theo dõi CPU, vì garbage collection nặng có thể giữ bộ nhớ dưới giới hạn trong khi làm độ trễ tệ đi.

Điều kiện đạt thực tế kết hợp trần và xu hướng. Yêu cầu resident memory thấp hơn an toàn giới hạn triển khai ở đỉnh tăng trưởng, rồi yêu cầu đường nền sau collection và số goroutine ngừng tăng trong soak test. Không có tỷ lệ an toàn chung. Hãy chọn dư địa theo cách nền tảng khởi động lại, biến động lưu lượng và khả năng replica khác gánh một lần khởi động lại.

Lỗi phải gồm cả kết quả sai và hành vi quá tải

Biến yêu cầu thành Go
Mô tả các ràng buộc về độ trễ, thời gian chờ và cơ sở dữ liệu trong cuộc trò chuyện khi Koder.ai xây dựng mã nguồn backend.

Đếm riêng lỗi truyền tải, lỗi mã trạng thái HTTP, timeout, panic và phản hồi sai. http_req_failed bắt yêu cầu HTTP thất bại theo response callback của k6, nhưng phản hồi 200 với danh sách trống, tính phí trùng hoặc thiếu bản ghi vẫn là lỗi. Đó là lý do mẫu phát business_errors từ các kiểm tra nội dung.

Gắn thẻ các từ chối dự kiến như đầu vào không hợp lệ hoặc rate limit có chủ đích để chúng không làm bẩn tỷ lệ lỗi không mong đợi. Sau đó khẳng định hợp đồng của chúng: mã trạng thái đúng, body có giới hạn và từ chối đến nhanh. Hệ thống quá tải không nên mất 30 giây trước khi trả về 503.

Theo dõi log máy chủ để tìm panic recovery, lỗi context deadline, chậm lấy kết nối, lỗi serialization PostgreSQL và truy vấn bị hủy. Nhóm lỗi theo nguyên nhân ổn định, không theo toàn bộ thông báo, để mã định danh không tạo ra hàng nghìn nhóm. Lưu trace đại diện cho lần xuất hiện đầu tiên và phần đuôi có độ trễ cao.

Chạy một bài kiểm thử suy giảm sau bài năng lực sạch. Giảm số kết nối cơ sở dữ liệu sẵn có, thêm độ trễ có kiểm soát cho một phụ thuộc bên ngoài hoặc khởi động lại một replica ứng dụng trong khi lưu lượng vẫn tiếp tục. Chỉ làm điều này trong môi trường kiểm thử tách biệt. Mục tiêu là xác nhận timeout, cancellation và health check cô lập lỗi thay vì để hàng đợi chiếm hết tài nguyên.

Cấu hình HTTP server một cách rõ ràng. Tài liệu net/http của Go nói giá trị bằng 0 hoặc âm của ReadTimeout, WriteTimeoutIdleTimeout có thể có nghĩa là không timeout, tùy trường. Dịch vụ được tạo thường gọi http.ListenAndServe bằng mặc định và không bao giờ đưa ra quyết định này. http.Server tùy chỉnh, deadline theo request và kích thước body có giới hạn ngăn client chậm và phụ thuộc bị kẹt giữ tài nguyên mãi mãi.

Xem lại tính đúng đắn sau lần chạy. Đếm bản ghi đã tạo, xác minh idempotency ở nơi client retry, kiểm tra tác vụ nền chỉ hoàn tất một lần và xác nhận không có trạng thái dở dang tồn tại sau yêu cầu thất bại. Trong các dự án của tôi, kiểm thử tải đã phát hiện nhiều lỗi công việc trùng lặp hơn bất kỳ buổi review mã kỹ lưỡng nào.

Quyết định tuyển dụng đến từ giới hạn đầu tiên

Bạn có thể ra mắt mà chưa có kỹ sư backend khi hệ thống nhiều lần đạt bài kiểm thử đỉnh dự kiến và đỉnh tăng trưởng, soak test đạt đường nền bộ nhớ ổn định, hàng đợi cơ sở dữ liệu vẫn được kiểm soát và nhóm giải thích được lỗi stress đầu tiên. Một lần chạy xanh may mắn không phải bằng chứng. Chạy cùng commit ít nhất ba lần và điều tra mọi biến động lớn.

Giữ bản ghi kết quả ngắn gọn cho mỗi lần chạy:

  • Commit và môi trường, gồm kích cỡ replica và cấu hình cơ sở dữ liệu.
  • Quy mô dataset, tỷ lệ lưu lượng, arrival rate và thời lượng kiểm thử.
  • p95 và p99 theo tuyến, throughput đạt được, lỗi nghiệp vụ và dropped iteration.
  • CPU và bộ nhớ đỉnh, xu hướng bộ nhớ sau collection và xu hướng goroutine.
  • Thời gian chờ pool, SQL hàng đầu theo tổng thời gian, lock wait và điểm gãy quan sát được.

Hãy tuyển hoặc thuê hỗ trợ backend trước khi ra mắt khi không ai giải thích được pool wait tăng, bộ nhớ bị giữ lại, lock contention hoặc ghi dữ liệu không nhất quán. Hãy tuyển khi người duy nhất có thể chạy bài kiểm thử không thể thay đổi mã được tạo một cách an toàn. Đó là khoảng trống về quyền sở hữu, không phải ngưỡng yêu cầu mỗi giây.

Bài kiểm thử không đạt không tự động biện minh cho một nhân sự toàn thời gian. Thiếu index, truy vấn N+1 hoặc export không giới hạn có thể là sửa chữa giới hạn. Các lỗi lặp lại trong thiết kế transaction, khả năng quan sát, cancellation và hành vi triển khai cho thấy cần công việc kỹ thuật liên tục. Khác biệt nằm ở chỗ bạn tìm thấy một lỗi hay phát hiện rằng không ai sở hữu hành vi của hệ thống.

Koder.ai có thể tạo và xuất backend Go, triển khai nó và lưu snapshot để rollback, nhưng việc tạo mã không loại bỏ nhu cầu lập kế hoạch năng lực. Hãy giữ script kiểm thử tải và các thay đổi về khả năng quan sát cùng mã nguồn để mỗi thay đổi backend đáng kể phải đạt cùng hợp đồng.

Đừng hứa với doanh nghiệp rằng 10.000 người dùng hằng tháng là an toàn. Hãy cam kết một arrival rate đã đo, tỷ lệ tuyến, mục tiêu độ trễ, ngân sách lỗi và phạm vi tài nguyên. Khi sản phẩm thay đổi, hãy thay các đầu vào đó và chạy lại bài kiểm thử.

Câu hỏi thường gặp

Backend Go có thể phục vụ 10.000 người dùng hoạt động hằng tháng không?

Thường là có, nhưng người dùng hoạt động hằng tháng không mô tả tải backend. Hãy chuyển dự báo thành số yêu cầu mỗi giây vào giờ cao điểm và tỷ lệ các tuyến, rồi kiểm thử khối lượng đó với các giới hạn rõ ràng về độ trễ, lỗi, cơ sở dữ liệu và bộ nhớ.

10.000 người dùng hoạt động hằng tháng tương đương bao nhiêu yêu cầu mỗi giây?

Không có quy đổi cố định. Bạn cần biết số phiên trên mỗi người dùng, số yêu cầu trong mỗi phiên, tỷ trọng lưu lượng trong khung giờ bận nhất và mọi sự kiện khiến người dùng truy cập cùng lúc.

Độ trễ p95 nào chấp nhận được cho API Go?

Hãy đặt mục tiêu theo hành động của người dùng thay vì theo ngôn ngữ hay framework. Các thao tác đọc tương tác có thể cần vài trăm mili giây, còn tác vụ nền đã được chấp nhận có thể có mục tiêu khác, nhưng nhóm phải chọn các con số trước khi xem kết quả kiểm thử.

Kiểm thử tải backend nên chạy trong bao lâu?

Giữ mỗi mức tải ổn định ít nhất 30 phút sau giai đoạn làm nóng, rồi chạy soak test dài hơn ở mức đỉnh dự kiến. Kiểm thử ngắn có thể bỏ sót việc thay đổi kết nối, bộ nhớ bị giữ lại, tác vụ nền và hàng đợi tăng dần.

Nên dùng virtual user hay arrival rate để kiểm thử tải?

Hãy dùng mô hình arrival rate khi muốn khẳng định năng lực vì nó vẫn tiếp tục đưa công việc vào khi dịch vụ chậm đi. Mô hình số lượng virtual user cố định có thể làm giảm tốc độ yêu cầu khi chậm và che giấu điểm hàng đợi bắt đầu tăng.

Làm sao phát hiện cạn connection pool cơ sở dữ liệu trong Go?

Xuất DB.Stats() và theo dõi InUse, OpenConnections, WaitCountWaitDuration. Bộ đếm chờ tăng trong khi số kết nối đang dùng giữ ở mức tối đa đã cấu hình cho thấy các yêu cầu đang xếp hàng chờ pool.

Có nên tăng SQL connection pool của Go khi yêu cầu phải chờ không?

Chưa nên, nếu bạn chưa biết vì sao kết nối bị giữ lâu và PostgreSQL còn bao nhiêu năng lực. Pool lớn hơn có thể chuyển hàng đợi vào cơ sở dữ liệu và làm tình trạng tranh chấp tệ hơn.

Làm sao tìm truy vấn PostgreSQL chậm trong lúc kiểm thử tải?

Chụp snapshot pg_stat_statements trước và sau, rồi xếp hạng phần chênh lệch truy vấn theo tổng thời gian và thời gian thực thi trung bình. Dùng trace hoặc histogram đại diện để xem độ trễ đuôi, vì view tổng hợp không cung cấp p95 cho từng truy vấn.

Làm sao biết dịch vụ Go có rò rỉ bộ nhớ?

Chạy soak test ở tải ổn định và so sánh bộ nhớ sau các đợt garbage collection tại mức tải tương tự. Đường nền cứ tăng, đặc biệt khi heap object hoặc goroutine tăng, cần được kiểm tra bằng cách so sánh heap profile và goroutine profile.

Khi nào startup nên tuyển kỹ sư backend?

Hãy tuyển khi các lỗi hiệu năng cho thấy cần người sở hữu lâu dài về thiết kế cơ sở dữ liệu, khả năng quan sát, đồng thời, tính đúng đắn hoặc vận hành. Một lỗi index hay truy vấn đơn lẻ có thể không cần vị trí toàn thời gian, nhưng hành vi không giải thích được dưới tải thì có.

Related posts