Hành trình đếm sao – 5. SQL Server đang làm gì?

Hành trình đếm sao – 5. SQL Server đang làm gì?

Lưu ý: Bài viết này được đăng lại từ kho lưu trữ cũ nên một số hình ảnh minh họa đã bị mất. Xin lỗi bạn đọc vì sự bất tiện này.

Giám sát truy vấn đang chạy: Cách tìm kiếm "Thủ phạm" làm chậm SQL Server

Công việc quản trị SQL Server luôn mang lại cho tôi rất nhiều trải nghiệm thú vị về "thời gian". Những khi hệ thống vận hành trơn tru, chúng ta có thể thong thả lướt blog của các chuyên gia để tích lũy thêm kiến thức. Thế nhưng, chỉ cần một vài truy vấn khóa (block) lẫn nhau, tình trạng tranh chấp tài nguyên khiến lượng request giảm đột ngột, hay processor time chạm ngưỡng 100%... thực tại khốc liệt sẽ lập tức kéo chúng ta trở lại.
Nhiều khi, SQL Server đột nhiên trở nên ì ạch dù các thông số tài nguyên phần cứng trông vẫn rất bình thường. Những tình huống khẩn cấp đó chẳng khác nào việc bạn phải gấp rút bê những tảng đá lớn ra khỏi đường ray khi đoàn tàu đang lao đến sát nút — lúc ấy, từng giây từng phút đều vô cùng quý giá.
Trong những khoảnh khắc căng thẳng như vậy, việc đầu tiên bạn cần làm là xác định: Điều gì đang thực sự diễn ra? SQL Server đang xử lý những gì? Có ai vừa thay đổi thiết lập trên môi trường Production hay không? Và phản xạ tự nhiên nhất, hiệu quả nhất của một DBA là: Tìm xem SQL Server đang chạy những câu lệnh nào.

Khảo sát các Request đang thực thi với DMV

Ở bài viết trước, chúng ta đã biết cách xem lịch sử chạy của một truy vấn thông qua Dynamic Management View (DMV) sys.dm_exec_query_stats. Còn đối với các câu lệnh đang thực thi — dù là từ người dùng qua SSMS, từ ứng dụng khách, hay thậm chí là các tiến trình hệ thống (system tasks) của SQL Server — chúng đều được lưu trữ trong DMV sys.dm_exec_requests.
Vì chỉ chứa các request đang chạy, đây chính là nơi bạn "săn tìm" những câu lệnh đang ngốn tài nguyên và thời gian của hệ thống. Để lọc ra các truy vấn của người dùng (bỏ qua các tiến trình hệ thống của SQL Server vốn có session_id từ 1 đến 50), bạn có thể dùng câu lệnh sau:
SQL

SELECT 
    r.session_id, 
    r.status,
    r.command,
    r.cpu_time,
    r.total_elapsed_time,
    r.wait_type, 
    r.wait_resource, 
    r.wait_time 
FROM sys.dm_exec_requests r
WHERE session_id > 50;

Nếu chạy câu lệnh này khi hệ thống rảnh rỗi, kết quả thường chỉ hiển thị duy nhất một session_id — chính là session của SSMS mà bạn đang dùng để thực thi câu lệnh trên.
Trong DMV này, mỗi request gửi đến SQL Server sẽ được gán một session_id. Cột status biểu thị trạng thái của câu lệnh (như running, runnable hoặc sleeping). Cột cpu_time (tính bằng mili-giây) cho biết thời gian CPU đã xử lý, trong khi wait_time, wait_type và wait_resource sẽ tiết lộ session đó đang phải chờ đợi tài nguyên nào và trong bao lâu. Bạn có thể tham khảo thêm tài liệu của Microsoft để hiểu chi tiết ý nghĩa từng cột của DMV này.
Để biết chính xác nội dung câu lệnh mà session_id đang chạy là gì, chúng ta cần kết hợp với Dynamic Management Function (DMF) sys.dm_exec_sql_text thông qua toán tử CROSS APPLY:
SQL

SELECT 
    r.session_id,
    t.text, 
    r.status,
    r.command, 
    r.cpu_time,
    r.total_elapsed_time,
    r.wait_type, 
    r.wait_resource, 
    r.wait_time 
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
WHERE session_id > 50;

Kết quả trả về bây giờ sẽ có thêm cột text — hiển thị chi tiết nội dung câu lệnh T-SQL đang chạy.

Giả lập tải hệ thống (Workload) với SQLQueryStress

Để thấy rõ sức mạnh của câu lệnh trên, chúng ta sẽ giả lập một lượng tải (workload) từ ứng dụng thực tế bằng SQLQueryStress — một công cụ miễn phí tuyệt vời giúp bạn chạy một truy vấn hàng ngàn lần từ nhiều session song song một cách dễ dàng. Bạn có thể tải phiên bản mới nhất kèm hướng dẫn từ trang chủ của dự án, hoặc sử dụng các phiên bản cũ hơn (không yêu cầu .NET Core) đã được chia sẻ sẵn trên mạng.
Giao diện của SQLQueryStress rất thân thiện:
[Hình ảnh: Giao diện chính của SQLQueryStress]
Cách thiết lập rất đơn giản:

  1. Chọn Server và Database để kết nối.
  2. Nhập đoạn script T-SQL bạn muốn chạy thử.
  3. Chọn số luồng (threads) cần giả lập song song và số lần lặp (iterations).
  4. Nhấn GO để bắt đầu.

Ở đây, tôi kết nối tới SQL Server trên máy cá nhân (local) và chọn database phù hợp:
[Hình ảnh: Chọn kết nối cơ sở dữ liệu]
Lưu ý:
Ở trên tôi dùng Windows Authentication vì tài khoản Windows đang đăng nhập có quyền truy cập SQL Server. Bạn hoàn toàn có thể dùng SQL Server Authentication (User/Password) tùy thuộc vào cấu hình hệ thống của bạn.
Tiếp theo, hãy giả lập một ứng dụng có 4 threads truy cập đồng thời, mỗi thread lặp lại 10 lần, chạy một câu truy vấn có điều kiện tìm kiếm ở mệnh đề HAVING để xem hệ thống xử lý ra sao:
[Hình ảnh: Giả lập 4 threads, mỗi thread lặp 10 lần]
Sau khi nhấn GO, trong lúc công cụ đang hoạt động, hãy nhanh tay quay lại SSMS và chạy lại câu lệnh kiểm tra DMV của chúng ta ở phần trước.
Kết quả sẽ hiển thị rõ: ngoài session 53 hiện tại của bạn, có thêm 4 sessions khác đang chạy song song với nội dung chính là câu lệnh SELECT có mệnh đề HAVING mà chúng ta vừa kích hoạt từ SQLQueryStress.
Đây là bước đầu tiên cực kỳ quan trọng giúp bạn khoanh vùng sự cố: khi CPU tăng cao, ổ đĩa quá tải hay server phản hồi chậm, bạn sẽ ngay lập tức biết "ai" đang làm gì trên hệ thống. Trên máy cá nhân của tôi (4 cores, 8 threads), workload này mất gần 58 giây để hoàn tất.
[Hình ảnh: Kết quả sau khi chạy xong]
Sau khi hoàn thành, ứng dụng sẽ thống kê tổng số lần lặp (40 lần), cùng các giá trị trung bình về CPU time, Elapsed time và Logical reads để bạn dễ dàng so sánh, đánh giá hiệu năng.

Xử lý tình huống Stored Procedure

Mọi chuyện sẽ phức tạp hơn một chút nếu câu lệnh đang chạy là một Stored Procedure (SP). Một SP có thể chứa rất nhiều câu lệnh SELECT chạy tuần tự. Tại một thời điểm cụ thể, chỉ có một statement (câu lệnh đơn lẻ) bên trong SP được thực thi. Nếu chỉ nhìn vào cột text của cả SP, bạn sẽ không biết hệ thống đang "kẹt" ở dòng nào.
Hãy thử tạo một Stored Procedure nhận vào ID để chạy các câu truy vấn khác nhau:
SQL

CREATE PROCEDURE sp_app_query_data
(
    @queryID INT = 1
)
AS
BEGIN
    IF @queryID = 1
    BEGIN
        --- Điều kiện tìm kiếm ở mệnh đề HAVING
        SELECT location, COUNT(*) AS cnt
        FROM Users u
        INNER JOIN Posts p ON u.Id = p.OwnerUserId
        WHERE PostTypeId = 2 
        GROUP BY location
        HAVING location LIKE '%vietnam%'
            AND COUNT(*) > 10
        ORDER BY cnt
        OPTION (MAXDOP 1);
    END
    ELSE IF @queryID = 2
    BEGIN
        --- Điều kiện tìm kiếm ở mệnh đề WHERE
        SELECT location, COUNT(*) AS cnt
        FROM Users u
        INNER JOIN Posts p ON u.Id = p.OwnerUserId
        WHERE PostTypeId = 2 AND location LIKE '%vietnam%'
        GROUP BY location
        HAVING COUNT(*) > 10
        ORDER BY cnt
        OPTION (MAXDOP 1);
    END
END;

Bây giờ, chúng ta dùng SQLQueryStress để gọi SP này với tham số @queryID = 1:
[Hình ảnh: Thay thế ad-hoc query bằng stored procedure]
Nhấn GO, rồi chạy câu lệnh giám sát nâng cấp dưới đây:
SQL

SELECT 
    r.session_id,
    t.text,
    [current_stmt] = SUBSTRING(
        t.text, 
        r.statement_start_offset / 2 + 1,
        (CASE 
            WHEN r.statement_end_offset = -1 THEN DATALENGTH(t.text) 
            ELSE r.statement_end_offset 
         END - r.statement_start_offset) / 2 + 1
    ),
    r.status,
    r.command, 
    r.cpu_time,
    r.total_elapsed_time,
    r.wait_type, 
    r.wait_resource, 
    r.wait_time 
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
WHERE session_id > 50;

Lưu ý về thuật toán phân tách: Mỗi khi SQL Server xử lý đến một statement cụ thể trong SP, nó sẽ cập nhật hai cột statement_start_offset và statement_end_offset trong sys.dm_exec_requests. Dựa vào việc cắt chuỗi bằng hàm SUBSTRING, chúng ta có thể trích xuất chính xác câu lệnh con đang thực thi trong toàn bộ đoạn mã dài của SP. Kết quả sẽ chỉ rõ câu lệnh SELECT ở mệnh đề HAVING đang hoạt động, chứ không chỉ hiển thị tên SP chung chung.

Giải pháp thay thế "mì ăn liền": sp_whoisactive

Sẽ là một thiếu sót lớn nếu nói về việc giám sát "SQL Server đang làm gì" mà không nhắc tới sp_whoisactive — một stored procedure huyền thoại được viết bởi Adam Machanic (SQL Server MVP).
sp_whoisactive tổng hợp và trình bày cực kỳ chi tiết tất cả các thông tin xử lý sự cố hiệu năng (troubleshooting performance) một cách nhanh chóng, trực quan. Bạn có thể tìm hiểu và tải về từ trang chủ của sp_whoisactive.(Lưu ý: Bạn cần quyền sysadmin trên SQL Server để thực thi SP này, vì vậy hãy ưu tiên thử nghiệm và làm quen ở môi trường Dev trước khi áp dụng trên Production).

Lời kết

Biết cách xác định những gì đang diễn ra trên SQL Server là bước đi đầu tiên và quan trọng nhất để cứu hệ thống khỏi những pha "đứng hình" bất ngờ. Thông qua hai "vũ khí" lợi hại là sys.dm_exec_requests và sys.dm_exec_sql_text, bạn đã có thể nắm trọn trong tay thông tin về trạng thái, câu lệnh cụ thể, lượng CPU đã dùng và các tài nguyên đang bị nghẽn.
Chúc các bạn áp dụng thành công và luôn làm chủ được hệ thống SQL Server của mình!