Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Báo cáo trình bày một số phương pháp cho phép giám sát hiệu suất của các truy vấn SQL khi có hàng triệu truy vấn mỗi ngàyvà có hàng trăm máy chủ PostgreSQL được giám sát.

Giải pháp kỹ thuật nào cho phép chúng tôi xử lý hiệu quả lượng thông tin như vậy và làm thế nào điều này giúp cuộc sống của một nhà phát triển bình thường trở nên dễ dàng hơn?


Ai có hứng thú nào? phân tích các vấn đề cụ thể và các kỹ thuật tối ưu hóa khác nhau Truy vấn SQL và giải quyết các vấn đề DBA điển hình trong PostgreSQL - bạn cũng có thể đọc một loạt bài viết về chủ đề này.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)
Tên tôi là Kirill Borovikov, tôi đại diện Công ty Tenor. Cụ thể, tôi chuyên làm việc với cơ sở dữ liệu trong công ty của chúng tôi.

Hôm nay tôi sẽ cho bạn biết cách chúng tôi tối ưu hóa các truy vấn, khi bạn không cần phải “chọn lọc” hiệu suất của một truy vấn mà giải quyết được nhiều vấn đề. Khi có hàng triệu yêu cầu và bạn cần tìm một số các cách tiếp cận giải pháp vấn đề lớn này.

Nói chung, Tensor dành cho một triệu khách hàng của chúng tôi là VLSI là ứng dụng của chúng tôi: mạng xã hội doanh nghiệp, các giải pháp truyền thông video, lưu chuyển tài liệu nội bộ và bên ngoài, hệ thống kế toán kế toán và kho bãi,... Tức là một “siêu tổ hợp” quản lý kinh doanh tích hợp, trong đó có hơn 100 tính năng khác nhau các dự án nội bộ.

Để đảm bảo rằng tất cả đều hoạt động và phát triển bình thường, chúng tôi có 10 trung tâm phát triển trên khắp cả nước và nhiều hơn nữa. 1000 nhà phát triển.

Chúng tôi đã làm việc với PostgreSQL từ năm 2008 và đã tích lũy được một lượng lớn những gì chúng tôi xử lý - dữ liệu khách hàng, dữ liệu thống kê, phân tích, dữ liệu từ các hệ thống thông tin bên ngoài - hơn 400TB. Chỉ riêng có khoảng 250 máy chủ đang được sản xuất và tổng cộng có khoảng 1000 máy chủ cơ sở dữ liệu mà chúng tôi giám sát.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

SQL là một ngôn ngữ khai báo. Bạn mô tả không phải “cách” một thứ gì đó sẽ hoạt động mà là “điều” bạn muốn đạt được. DBMS biết rõ hơn cách tạo THAM GIA - cách kết nối các bảng của bạn, những điều kiện nào cần áp đặt, những gì sẽ đi qua chỉ mục, những gì sẽ không...

Một số DBMS chấp nhận gợi ý: “Không, hãy kết nối hai bảng này theo hàng đợi như vậy,” nhưng PostgreSQL không thể thực hiện điều này. Đây là quan điểm có ý thức của các nhà phát triển hàng đầu: “Chúng tôi muốn hoàn thiện trình tối ưu hóa truy vấn hơn là cho phép các nhà phát triển sử dụng một số loại gợi ý”.

Tuy nhiên, mặc dù thực tế là PostgreSQL không cho phép “bên ngoài” tự kiểm soát, nhưng nó hoàn toàn cho phép xem điều gì đang xảy ra bên trong anh ấykhi bạn chạy truy vấn của mình và nó gặp sự cố ở đâu.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Nói chung, nhà phát triển [đối với DBA] thường gặp phải những vấn đề kinh điển nào? “Ở đây chúng tôi đã đáp ứng được yêu cầu và mọi thứ đều chậm chạp với chúng tôi, mọi thứ đang treo lơ lửng, có chuyện gì đó đang xảy ra ... Rắc rối nào đó!

Những lý do hầu như luôn giống nhau:

  • thuật toán truy vấn không hiệu quả
    Nhà phát triển: “Bây giờ tôi đang đưa cho anh ấy 10 bảng trong SQL thông qua THAM GIA…” - và hy vọng rằng các điều kiện của anh ấy sẽ được “cởi trói” một cách thần kỳ một cách hiệu quả và anh ấy sẽ hiểu được mọi thứ một cách nhanh chóng. Nhưng điều kỳ diệu không xảy ra, và bất kỳ hệ thống nào có khả năng thay đổi như vậy (10 bảng trong một TỪ) luôn gây ra một số loại lỗi. [bài viết]
  • số liệu thống kê không liên quan
    Điểm này đặc biệt phù hợp với PostgreSQL, khi bạn “đổ” một tập dữ liệu lớn lên máy chủ, đưa ra yêu cầu và nó “sexcanits” máy tính bảng của bạn. Bởi vì ngày hôm qua có 10 bản ghi trong đó, hôm nay có 10 triệu bản ghi, nhưng PostgreSQL vẫn chưa biết về điều này và chúng ta cần thông báo cho nó về điều đó. [bài viết]
  • "cắm" vào tài nguyên
    Bạn đã cài đặt một cơ sở dữ liệu lớn và được tải nặng trên một máy chủ yếu không có đủ hiệu suất đĩa, bộ nhớ hoặc bộ xử lý. Và chỉ vậy thôi... Đâu đó có một mức trần hiệu suất mà trên đó bạn không thể nhảy được nữa.
  • chặn lại
    Đây là một điểm khó, nhưng chúng phù hợp nhất với các truy vấn sửa đổi khác nhau (CHÈN, CẬP NHẬT, XÓA) - đây là một chủ đề lớn riêng biệt.

Nhận một kế hoạch

...Và đối với mọi thứ khác, chúng tôi cần một kế hoạch! Chúng ta cần xem những gì đang xảy ra bên trong máy chủ.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Kế hoạch thực hiện truy vấn cho PostgreSQL là một cây thuật toán thực hiện truy vấn dưới dạng biểu diễn văn bản. Chính xác là thuật toán mà theo kết quả phân tích của người lập kế hoạch, được cho là hiệu quả nhất.

Mỗi nút cây là một thao tác: truy xuất dữ liệu từ một bảng hoặc chỉ mục, xây dựng bitmap, nối hai bảng, nối, giao nhau hoặc loại trừ các lựa chọn. Việc thực hiện một truy vấn bao gồm việc duyệt qua các nút của cây này.

Để có được kế hoạch truy vấn, cách dễ nhất là thực thi câu lệnh EXPLAIN. Để có được tất cả các thuộc tính thực, nghĩa là thực sự thực hiện một truy vấn trên cơ sở - EXPLAIN (ANALYZE, BUFFERS) SELECT ....

Phần tệ: khi bạn chạy nó, nó xảy ra "ở đây và bây giờ", vì vậy nó chỉ phù hợp để gỡ lỗi cục bộ. Nếu bạn lấy một máy chủ có tải cao đang chịu luồng thay đổi dữ liệu mạnh mẽ và bạn thấy: “Ồ! Ở đây chúng tôi thực hiện chậmcắm trại lời yêu cầu." Nửa giờ, một giờ trước - trong khi bạn đang chạy và nhận yêu cầu này từ nhật ký, đưa nó trở lại máy chủ, toàn bộ tập dữ liệu và số liệu thống kê của bạn đã thay đổi. Bạn chạy nó để gỡ lỗi - và nó chạy rất nhanh! Và bạn không thể hiểu tại sao, tại sao chậm.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Để hiểu chính xác điều gì đã xảy ra vào thời điểm yêu cầu được thực thi trên máy chủ, những người thông minh đã viết mô-đun auto_explain. Nó có mặt trong hầu hết các bản phân phối PostgreSQL phổ biến nhất và có thể được kích hoạt một cách đơn giản trong tệp cấu hình.

Nếu nó nhận ra rằng một số yêu cầu đang chạy lâu hơn giới hạn mà bạn đã yêu cầu, thì nó sẽ làm như vậy. “ảnh chụp nhanh” kế hoạch của yêu cầu này và ghi chúng lại với nhau vào nhật ký.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Bây giờ mọi thứ có vẻ ổn, chúng ta đến khúc gỗ và thấy ở đó... [văn bản khăn lau chân]. Nhưng chúng tôi không thể nói bất cứ điều gì về nó, ngoài thực tế rằng đó là một kế hoạch xuất sắc vì phải mất 11 mili giây để thực hiện.

Mọi thứ có vẻ ổn - nhưng không có gì rõ ràng điều gì đã thực sự xảy ra. Ngoài thời gian chung ra, chúng tôi thực sự không thấy gì cả. Bởi vì nhìn vào một “con cừu” văn bản đơn giản như vậy thường không trực quan.

Nhưng ngay cả khi nó không rõ ràng, ngay cả khi nó bất tiện, thì vẫn có những vấn đề cơ bản hơn:

  • Nút chỉ ra tổng tài nguyên của toàn bộ cây con dưới anh ta. Nghĩa là, bạn không thể chỉ biết lượng thời gian đã dành cho Quét chỉ mục cụ thể này nếu có một số điều kiện lồng nhau trong đó. Chúng ta phải linh hoạt xem xét liệu có "con" và các biến có điều kiện, CTE bên trong hay không - và loại trừ tất cả những điều này "trong tâm trí chúng ta".
  • Điểm thứ hai: thời gian được chỉ định trên nút là thời gian thực hiện nút đơn. Ví dụ: nếu nút này được thực thi do một bản ghi lặp qua bảng nhiều lần, thì số vòng lặp—chu kỳ của nút này—sẽ tăng lên trong kế hoạch. Nhưng bản thân thời gian thực hiện nguyên tử vẫn giữ nguyên về mặt kế hoạch. Nghĩa là, để hiểu tổng cộng nút này đã được thực hiện trong bao lâu, bạn cần nhân một thứ với một thứ khác - một lần nữa, “trong đầu bạn”.

Trong những tình huống như vậy, hãy hiểu “Ai là mắt xích yếu nhất?” gần như không thể. Vì vậy, ngay cả chính các nhà phát triển cũng viết trong “sách hướng dẫn” rằng “Hiểu được kế hoạch là một nghệ thuật cần phải học, phải trải nghiệm…”.

Nhưng chúng tôi có 1000 nhà phát triển và bạn không thể truyền đạt trải nghiệm này cho từng người trong số họ. Tôi, bạn, anh ấy biết, nhưng có người ở đằng kia không còn biết nữa. Có thể anh ấy sẽ học, hoặc có thể không, nhưng anh ấy cần phải làm việc ngay bây giờ - và anh ấy sẽ lấy kinh nghiệm này ở đâu?

Trực quan hóa kế hoạch

Vì vậy, chúng tôi nhận ra rằng để giải quyết những vấn đề này, chúng tôi cần hình dung tốt về kế hoạch. [bài báo]

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Lần đầu tiên chúng tôi “đi qua thị trường” - hãy cùng tìm trên Internet để xem những gì còn tồn tại.

Nhưng hóa ra có rất ít giải pháp tương đối “sống” đang phát triển ít nhiều - theo nghĩa đen, chỉ có một: giải thích.depesz.com của Hubert Lubaczewski. Khi bạn nhập vào trường “nguồn cấp dữ liệu” dưới dạng văn bản trình bày của kế hoạch, nó sẽ hiển thị cho bạn một bảng có dữ liệu được phân tích cú pháp:

  • thời gian xử lý riêng của nút
  • tổng thời gian cho toàn bộ cây con
  • số lượng hồ sơ được truy xuất theo dự kiến ​​về mặt thống kê
  • bản thân thân nút

Dịch vụ này cũng có khả năng chia sẻ kho lưu trữ các liên kết. Bạn ném kế hoạch của mình vào đó và nói: "Này, Vasya, đây là một liên kết, có gì đó không ổn ở đó."

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Nhưng cũng có những vấn đề nhỏ.

Thứ nhất, một lượng lớn “sao chép-dán”. Bạn lấy một mảnh gỗ, dán nó vào đó, và dán lại nhiều lần.

Thứ hai, không có phân tích về lượng dữ liệu đọc — cùng một bộ đệm xuất ra EXPLAIN (ANALYZE, BUFFERS), chúng tôi không thấy nó ở đây. Đơn giản là anh ta không biết cách tháo rời chúng, hiểu chúng và làm việc với chúng. Khi bạn đang đọc nhiều dữ liệu và nhận ra rằng mình có thể đang phân bổ sai bộ đệm đĩa và bộ nhớ, thông tin này rất quan trọng.

Điểm tiêu cực thứ ba là sự phát triển rất yếu của dự án này. Các cam kết rất nhỏ, thật tốt nếu sáu tháng một lần và mã ở Perl.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Nhưng đây chỉ là “lời bài hát”, bằng cách nào đó chúng ta có thể sống chung với điều này, nhưng có một điều khiến chúng tôi rời xa dịch vụ này rất nhiều. Đây là những lỗi trong phân tích Biểu thức bảng chung (CTE) và các nút động khác nhau như InitPlan/SubPlan.

Nếu bạn tin vào bức tranh này thì tổng thời gian thực hiện của từng nút riêng lẻ sẽ lớn hơn tổng thời gian thực hiện của toàn bộ yêu cầu. Thật đơn giản - thời gian tạo của CTE này không bị trừ khỏi nút Quét CTE. Do đó, chúng tôi không còn biết câu trả lời chính xác cho việc quá trình quét CTE mất bao lâu.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Sau đó, chúng tôi nhận ra rằng đã đến lúc phải viết bài của riêng mình - hoan hô! Mọi nhà phát triển đều nói: “Bây giờ chúng tôi sẽ tự viết, nó sẽ cực kỳ dễ dàng!”

Chúng tôi đã sử dụng một ngăn xếp điển hình cho các dịch vụ web: lõi dựa trên Node.js + Express, sử dụng Bootstrap và D3.js để tạo ra các sơ đồ đẹp mắt. Và kỳ vọng của chúng tôi đã hoàn toàn chính đáng - chúng tôi đã nhận được nguyên mẫu đầu tiên sau 2 tuần:

  • trình phân tích cú pháp kế hoạch tùy chỉnh
    Nghĩa là, bây giờ chúng ta có thể phân tích cú pháp bất kỳ kế hoạch nào từ những kế hoạch do PostgreSQL tạo ra.
  • phân tích chính xác các nút động - Quét CTE, InitPlan, SubPlan
  • phân tích phân phối bộ đệm - nơi các trang dữ liệu được đọc từ bộ nhớ, nơi từ bộ đệm cục bộ, nơi từ đĩa
  • có được sự rõ ràng
    Để không phải “đào” tất cả những điều này vào nhật ký mà có thể nhìn thấy ngay “mắt xích yếu nhất” trong hình.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Chúng tôi có một cái gì đó như thế này, bao gồm cả đánh dấu cú pháp. Nhưng thông thường, các nhà phát triển của chúng tôi không còn làm việc với bản trình bày đầy đủ về kế hoạch nữa mà với bản trình bày ngắn hơn. Rốt cuộc, chúng tôi đã phân tích tất cả các số và ném chúng sang trái và phải, và ở giữa chúng tôi chỉ để lại dòng đầu tiên, đó là loại nút nào: Quét CTE, tạo CTE hoặc Quét Seq theo một số dấu hiệu.

Đây là cách biểu diễn viết tắt mà chúng tôi gọi là mẫu kế hoạch.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Còn gì thuận tiện hơn nữa? Sẽ rất thuận tiện khi xem phần nào trong tổng thời gian của chúng tôi được phân bổ cho nút nào - và chỉ cần “dán nó” sang một bên biểu đồ tròn.

Chúng tôi chỉ vào nút và xem - hóa ra Seq Scan chỉ mất chưa đến một phần tư tổng thời gian và 3/4 còn lại được thực hiện bởi CTE Scan. Kinh dị! Đây là một lưu ý nhỏ về “tốc độ bắn” của CTE Scan nếu bạn chủ động sử dụng chúng trong các truy vấn của mình. Chúng không nhanh lắm - thậm chí còn kém hơn so với việc quét bảng thông thường. [bài báo] [bài báo]

Nhưng thông thường những sơ đồ như vậy thú vị hơn, phức tạp hơn, khi chúng ta chỉ ngay vào một đoạn và xem chẳng hạn như hơn một nửa thời gian một số Seq Scan đã “ăn”. Hơn nữa, bên trong có một số loại Bộ lọc, rất nhiều bản ghi đã bị loại bỏ theo nó... Bạn có thể trực tiếp ném bức ảnh này cho nhà phát triển và nói: “Vasya, mọi thứ ở đây thật tồi tệ đối với bạn! Hãy tìm ra, nhìn xem - có gì đó không ổn!”

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Đương nhiên, có một số “cào” liên quan.

Điều đầu tiên chúng tôi gặp phải là vấn đề làm tròn. Thời gian của mỗi nút riêng lẻ trong kế hoạch được biểu thị với độ chính xác 1 μs. Và khi số chu kỳ nút vượt quá, chẳng hạn như 1000 - sau khi thực thi PostgreSQL chia “trong phạm vi độ chính xác”, thì khi tính toán lại, chúng ta nhận được tổng thời gian “ở đâu đó trong khoảng từ 0.95 mili giây đến 1.05 mili giây”. Khi số đếm chuyển sang micro giây thì không sao, nhưng khi đã là [milli] giây, bạn phải tính đến thông tin này khi “gỡ” tài nguyên vào các nút của kế hoạch “ai đã tiêu thụ bao nhiêu”.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Điểm thứ hai, phức tạp hơn, là sự phân bổ tài nguyên (những vùng đệm đó) giữa các nút động. Điều này khiến chúng tôi mất 2 tuần đầu tiên sử dụng nguyên mẫu cộng thêm 4 tuần nữa.

Khá dễ gặp phải loại vấn đề này - chúng tôi thực hiện CTE và được cho là đã đọc được nội dung nào đó trong đó. Trên thực tế, PostgreSQL “thông minh” và sẽ không đọc bất cứ thứ gì trực tiếp ở đó. Sau đó, chúng tôi lấy bản ghi đầu tiên từ nó và bản ghi thứ 101 từ cùng một CTE.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Chúng tôi xem xét kế hoạch và hiểu - thật kỳ lạ, chúng tôi có 3 bộ đệm (trang dữ liệu) “được sử dụng” trong Seq Scan, 1 bộ đệm nữa trong CTE Scan và 2 bộ đệm nữa trong CTE Scan thứ hai. Nghĩa là, nếu chỉ cộng lại mọi thứ thì chúng ta sẽ nhận được 6, nhưng từ máy tính bảng chúng ta chỉ đọc được 3! CTE Scan không đọc bất cứ thứ gì từ bất cứ đâu mà hoạt động trực tiếp với bộ nhớ xử lý. Tức là có điều gì đó rõ ràng không ổn ở đây!

Trên thực tế, hóa ra đây là tất cả 3 trang dữ liệu được yêu cầu từ Seq Scan, trang đầu tiên yêu cầu Quét CTE đầu tiên, sau đó là trang thứ 1 và 1 trang nữa được đọc cho anh ấy. 2 trang đã được đọc dữ liệu, không phải 2.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Và bức tranh này giúp chúng ta hiểu rằng việc thực hiện một kế hoạch không còn là một cái cây nữa mà chỉ đơn giản là một loại biểu đồ tuần hoàn nào đó. Và chúng tôi có một sơ đồ như thế này để chúng tôi hiểu “điều gì đến từ đâu ngay từ đầu”. Nghĩa là, ở đây chúng tôi đã tạo CTE từ pg_class và yêu cầu nó hai lần và gần như toàn bộ thời gian của chúng tôi dành cho nhánh khi chúng tôi yêu cầu lần thứ hai. Rõ ràng là việc đọc mục thứ 2 đắt hơn nhiều so với việc chỉ đọc mục đầu tiên từ máy tính bảng.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Chúng tôi thở dài một lúc. Họ nói: “Neo, bạn biết kung fu rồi! Bây giờ trải nghiệm của chúng tôi ở ngay trên màn hình của bạn. Bây giờ bạn có thể sử dụng nó." [bài báo]

Hợp nhất nhật ký

1000 nhà phát triển của chúng tôi thở phào nhẹ nhõm. Nhưng chúng tôi hiểu rằng chúng tôi chỉ có hàng trăm máy chủ "chiến đấu" và tất cả việc "sao chép-dán" này từ phía các nhà phát triển không hề thuận tiện chút nào. Chúng tôi nhận ra rằng chúng tôi phải tự mình thu thập nó.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Nói chung, có một mô-đun tiêu chuẩn có thể thu thập số liệu thống kê, tuy nhiên, nó cũng cần được kích hoạt trong cấu hình - cái này mô-đun pg_stat_statements. Nhưng anh ấy không phù hợp với chúng tôi.

Đầu tiên, nó gán cho cùng một truy vấn bằng cách sử dụng các lược đồ khác nhau trong cùng một cơ sở dữ liệu QueryId khác nhau. Nghĩa là, nếu lần đầu tiên bạn làm SET search_path = '01'; SELECT * FROM user LIMIT 1;và sau đó SET search_path = '02'; và cùng một yêu cầu, thì số liệu thống kê của mô-đun này sẽ có các bản ghi khác nhau và tôi sẽ không thể thu thập số liệu thống kê chung cụ thể trong bối cảnh của hồ sơ yêu cầu này mà không tính đến các sơ đồ.

Điểm thứ hai ngăn cản chúng tôi sử dụng nó là thiếu kế hoạch. Tức là không có kế hoạch, chỉ có bản thân yêu cầu mà thôi. Chúng tôi thấy điều gì đang chậm lại, nhưng chúng tôi không hiểu tại sao. Và ở đây chúng ta quay trở lại vấn đề về tập dữ liệu thay đổi nhanh chóng.

Và khoảnh khắc cuối cùng - thiếu “sự thật”. Nghĩa là, bạn không thể giải quyết một trường hợp thực hiện truy vấn cụ thể - không có trường hợp nào, chỉ có số liệu thống kê tổng hợp. Mặc dù có thể làm việc với điều này nhưng nó rất khó khăn.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Vì vậy, chúng tôi quyết định đấu tranh với việc sao chép-dán và bắt đầu viết người sưu tầm.

Trình thu thập kết nối qua SSH, thiết lập kết nối an toàn với máy chủ với cơ sở dữ liệu bằng chứng chỉ và tail -F “bám” vào nó trong tệp nhật ký. Vì vậy trong phiên này chúng tôi nhận được một "bản sao" hoàn chỉnh của toàn bộ tệp nhật ký, mà máy chủ tạo ra. Bản thân tải trên máy chủ là tối thiểu vì chúng tôi không phân tích cú pháp bất kỳ thứ gì ở đó mà chỉ phản ánh lưu lượng truy cập.

Vì chúng tôi đã bắt đầu viết giao diện trong Node.js nên chúng tôi tiếp tục viết bộ sưu tập trong đó. Và công nghệ này đã tự chứng minh, vì nó rất thuận tiện khi sử dụng JavaScript để làm việc với dữ liệu văn bản được định dạng yếu, tức là nhật ký. Và bản thân cơ sở hạ tầng Node.js như một nền tảng phụ trợ cho phép bạn làm việc dễ dàng và thuận tiện với các kết nối mạng và thực tế là với bất kỳ luồng dữ liệu nào.

Theo đó, chúng tôi “kéo dài” hai kết nối: kết nối đầu tiên là “lắng nghe” chính bản ghi và đưa nó về cho mình, và kết nối thứ hai là hỏi cơ sở định kỳ. “Nhưng nhật ký cho thấy rằng biển báo có oid 123 đã bị chặn,” nhưng điều này không có ý nghĩa gì đối với nhà phát triển và thật tuyệt nếu bạn hỏi cơ sở dữ liệu, “OID = 123 là gì?” Vì vậy, chúng tôi định kỳ hỏi căn cứ những gì chúng tôi chưa biết về bản thân.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

“Chỉ có một điều bạn chưa tính đến, đó là một loài ong giống voi!..” Chúng tôi bắt đầu phát triển hệ thống này khi muốn giám sát 10 máy chủ. Điều quan trọng nhất theo sự hiểu biết của chúng tôi là nơi nảy sinh một số vấn đề khó giải quyết. Nhưng trong quý đầu tiên, chúng tôi đã nhận được hàng trăm đô la để theo dõi - vì hệ thống hoạt động nên mọi người đều muốn nó, mọi người đều cảm thấy thoải mái.

Tất cả điều này cần phải được bổ sung, luồng dữ liệu lớn và hoạt động. Trên thực tế, những gì chúng tôi theo dõi, những gì chúng tôi có thể giải quyết chính là những gì chúng tôi sử dụng. Chúng tôi cũng sử dụng PostgreSQL làm nơi lưu trữ dữ liệu. Và không có gì “đổ” dữ liệu vào đó nhanh hơn toán tử COPY Chưa.

Nhưng chỉ đơn giản là “đổ” dữ liệu thì không hẳn là công nghệ của chúng tôi. Bởi vì nếu bạn có khoảng 50 nghìn yêu cầu mỗi giây trên một trăm máy chủ thì điều này sẽ tạo ra 100-150GB nhật ký mỗi ngày. Vì vậy, chúng tôi phải “cắt” phần đế một cách cẩn thận.

Đầu tiên, chúng tôi đã làm phân vùng theo ngày, bởi vì nhìn chung không ai quan tâm đến mối tương quan giữa các ngày. Những gì bạn có ngày hôm qua có gì khác biệt nếu tối nay bạn tung ra phiên bản mới của ứng dụng - và đã có một số thống kê mới.

Thứ hai, chúng tôi đã học được (bị ép buộc) rất, rất nhanh để viết bằng cách sử dụng COPY. Tức là không chỉ COPYbởi vì anh ấy nhanh hơn INSERT, và thậm chí còn nhanh hơn.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Điểm thứ ba - tôi phải từ bỏ trình kích hoạt tương ứng và khóa ngoại. Tức là chúng ta không có tính toàn vẹn tham chiếu nào cả. Bởi vì nếu bạn có một bảng có một cặp FK và bạn nói trong cấu trúc cơ sở dữ liệu rằng “đây là một bản ghi nhật ký được FK tham chiếu, chẳng hạn như một nhóm bản ghi”, thì khi bạn chèn nó, PostgreSQL không còn gì ngoài cách chấp nhận và làm điều đó một cách trung thực SELECT 1 FROM master_fk1_table WHERE ... với mã định danh mà bạn đang cố gắng chèn - chỉ để kiểm tra xem bản ghi này có hiện diện ở đó hay không, rằng bạn không “phá vỡ” Khóa ngoại này khi chèn vào.

Thay vì một bản ghi vào bảng mục tiêu và các chỉ mục của nó, chúng ta có thêm lợi ích khi đọc tất cả các bảng mà nó tham chiếu đến. Nhưng chúng tôi hoàn toàn không cần điều này - nhiệm vụ của chúng tôi là ghi lại càng nhiều càng tốt và nhanh nhất có thể với mức tải ít nhất. Vì vậy FK - xuống!

Điểm tiếp theo là tổng hợp và băm. Ban đầu, chúng tôi đã triển khai chúng trong cơ sở dữ liệu - xét cho cùng, thật thuận tiện khi có bản ghi đến ngay lập tức, hãy thực hiện việc đó trong một loại máy tính bảng nào đó "cộng một" ngay trong trình kích hoạt. Chà, điều đó rất tiện lợi, nhưng cũng có điều tệ hại - bạn chèn một bản ghi nhưng buộc phải đọc và viết nội dung khác từ một bảng khác. Hơn nữa, bạn không chỉ đọc và viết mà còn làm điều đó mọi lúc.

Bây giờ hãy tưởng tượng rằng bạn có một bảng trong đó bạn chỉ cần đếm số lượng yêu cầu đã chuyển qua một máy chủ cụ thể: +1, +1, +1, ..., +1. Và về nguyên tắc, bạn không cần điều này - tất cả đều có thể tổng hợp trong bộ nhớ trên bộ sưu tập và gửi đến cơ sở dữ liệu trong một lần +10.

Có, trong trường hợp xảy ra một số vấn đề, tính toàn vẹn logic của bạn có thể “bị hỏng”, nhưng đây là một trường hợp gần như không thực tế - bởi vì bạn có một máy chủ bình thường, nó có pin trong bộ điều khiển, bạn có nhật ký giao dịch, nhật ký trên hệ thống tập tin... Nói chung là không đáng. Việc giảm năng suất mà bạn nhận được khi chạy trình kích hoạt/FK không đáng với chi phí bạn phải chịu.

Việc băm cũng vậy. Một yêu cầu nào đó được gửi đến bạn, bạn tính toán một số nhận dạng nhất định từ nó trong cơ sở dữ liệu, ghi nó vào cơ sở dữ liệu và sau đó thông báo cho mọi người. Mọi thứ vẫn ổn cho đến khi, tại thời điểm ghi, một người thứ hai đến gặp bạn và người muốn ghi lại điều tương tự - và bạn bị chặn, và điều này đã tệ rồi. Do đó, nếu bạn có thể chuyển việc tạo một số ID cho máy khách (liên quan đến cơ sở dữ liệu), thì tốt hơn nên làm điều này.

Thật hoàn hảo khi chúng tôi sử dụng MD5 từ văn bản - yêu cầu, kế hoạch, mẫu,... Chúng tôi tính toán nó ở phía người thu thập và “đổ” ID tạo sẵn vào cơ sở dữ liệu. Độ dài của MD5 và khả năng phân vùng hàng ngày cho phép chúng ta không phải lo lắng về những xung đột có thể xảy ra.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Nhưng để ghi lại tất cả những điều này một cách nhanh chóng, chúng tôi cần phải sửa đổi quy trình ghi.

Bạn thường ghi dữ liệu bằng cách nào? Chúng tôi có một số loại tập dữ liệu, chúng tôi chia nó thành nhiều bảng và sau đó SAO CHÉP nó - đầu tiên vào bảng đầu tiên, sau đó vào bảng thứ hai, sang bảng thứ ba... Thật bất tiện, vì dường như chúng tôi đang viết một luồng dữ liệu theo ba bước một cách tuần tự. Khó chịu. Nó có thể được thực hiện nhanh hơn? Có thể!

Để làm được điều này, chỉ cần phân tách các luồng này song song với nhau là đủ. Hóa ra là chúng tôi có lỗi, yêu cầu, mẫu, chặn, ... bay trong các luồng riêng biệt - và chúng tôi viết tất cả song song. Đủ cho việc này giữ kênh SAO CHÉP liên tục mở cho từng bảng mục tiêu riêng lẻ.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Nghĩa là, tại nhà sưu tập luôn có một dòng suối, vào đó tôi có thể ghi dữ liệu tôi cần. Nhưng để cơ sở dữ liệu nhìn thấy dữ liệu này và ai đó không bị mắc kẹt khi chờ dữ liệu này được ghi, COPY надо прерывать с определенной периодичностью. Đối với chúng tôi, khoảng thời gian hiệu quả nhất là khoảng 100 mili giây - chúng tôi đóng nó lại và ngay lập tức mở lại vào cùng một bảng. Và nếu chúng ta không có đủ một luồng trong một số thời điểm cao điểm, thì chúng ta sẽ gộp lại đến một giới hạn nhất định.

Ngoài ra, chúng tôi phát hiện ra rằng đối với một hồ sơ tải như vậy, bất kỳ sự tổng hợp nào khi các bản ghi được thu thập theo đợt đều là không tốt. Cái ác cổ điển là INSERT ... VALUES và hơn 1000 hồ sơ. Bởi vì tại thời điểm đó, bạn đạt đỉnh điểm ghi trên phương tiện và những người khác đang cố gắng ghi nội dung nào đó vào đĩa sẽ chờ đợi.

Để loại bỏ những điều bất thường như vậy, đơn giản là đừng tổng hợp bất cứ thứ gì, không đệm gì cả. Và nếu việc đệm vào đĩa xảy ra (may mắn thay, API Stream trong Node.js cho phép bạn tìm hiểu) - hãy hoãn kết nối này. Khi bạn nhận được một sự kiện cho biết nó lại miễn phí, hãy ghi vào sự kiện đó từ hàng đợi tích lũy. Và trong khi nó bận, hãy lấy cái miễn phí tiếp theo từ hồ bơi và viết thư cho nó.

Trước khi giới thiệu phương pháp ghi dữ liệu này, chúng tôi đã có khoảng 4K hoạt động ghi và bằng cách này, chúng tôi đã giảm tải xuống 4 lần. Bây giờ họ đã tăng thêm 6 lần nữa nhờ cơ sở dữ liệu được giám sát mới - lên tới 100MB/s. Và bây giờ chúng tôi lưu trữ nhật ký trong 3 tháng qua với dung lượng khoảng 10-15TB, hy vọng rằng chỉ trong ba tháng, bất kỳ nhà phát triển nào cũng có thể giải quyết được mọi vấn đề.

Chúng tôi hiểu những vấn đề

Nhưng chỉ thu thập tất cả dữ liệu này là tốt, hữu ích, phù hợp, nhưng chưa đủ - nó cần phải được hiểu rõ. Bởi vì đây là hàng triệu kế hoạch khác nhau mỗi ngày.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Nhưng hàng triệu người không thể quản lý được, trước tiên chúng ta phải làm “nhỏ hơn”. Và trước hết, bạn cần phải quyết định xem mình sẽ tổ chức việc “nhỏ hơn” này như thế nào.

Chúng tôi đã xác định được ba điểm chính:

  • ai đã gửi yêu cầu này
    Đó là, nó đã “đến” từ ứng dụng nào: giao diện web, chương trình phụ trợ, hệ thống thanh toán hay thứ gì khác.
  • đâu nó đã xảy ra
    Trên máy chủ cụ thể nào? Bởi vì nếu bạn có một số máy chủ trong một ứng dụng và đột nhiên một máy chủ "trở nên ngu ngốc" (vì "đĩa bị hỏng", "bộ nhớ bị rò rỉ", một số vấn đề khác), thì bạn cần phải giải quyết cụ thể máy chủ đó.
  • như vấn đề đã thể hiện theo cách này hay cách khác

Để hiểu “ai” đã gửi yêu cầu cho chúng tôi, chúng tôi sử dụng một công cụ tiêu chuẩn - đặt biến phiên: SET application_name = '{bl-host}:{bl-method}'; — chúng tôi gửi tên của máy chủ logic nghiệp vụ nơi yêu cầu được gửi đến và tên của phương thức hoặc ứng dụng đã khởi tạo yêu cầu đó.

Sau khi chúng tôi đã chuyển “chủ sở hữu” của yêu cầu, nó phải được xuất ra nhật ký - để làm điều này, chúng tôi định cấu hình biến log_line_prefix = ' %m [%p:%v] [%d] %r %a'. Đối với những người quan tâm, có thể nhìn vào hướng dẫn sử dụngTất cả nó có nghĩa gì. Hóa ra chúng ta thấy trong nhật ký:

  • thời gian
  • định danh quá trình và giao dịch
  • tên cơ sở dữ liệu
  • IP của người gửi yêu cầu này
  • và tên phương thức

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Sau đó, chúng tôi nhận ra rằng việc xem xét mối tương quan giữa một yêu cầu giữa các máy chủ khác nhau không thú vị lắm. Không hiếm khi bạn gặp phải trường hợp một ứng dụng gặp trục trặc ở chỗ này và chỗ kia. Nhưng ngay cả khi nó giống nhau, hãy nhìn vào bất kỳ máy chủ nào trong số này.

Vì vậy đây là phần cắt "một máy chủ - một ngày" hóa ra là đủ để chúng tôi thực hiện bất kỳ phân tích nào.

Phần phân tích đầu tiên cũng vậy "vật mẫu" - một dạng trình bày viết tắt của kế hoạch, không có tất cả các chỉ số bằng số. Lần cắt thứ hai là ứng dụng hoặc phương pháp và lần cắt thứ ba là nút kế hoạch cụ thể đã gây ra sự cố cho chúng tôi.

Khi chúng tôi chuyển từ các phiên bản cụ thể sang mẫu, chúng tôi có được hai lợi thế cùng một lúc:

  • giảm nhiều số lượng đối tượng để phân tích
    Chúng tôi phải phân tích vấn đề không còn bằng hàng nghìn truy vấn hoặc kế hoạch mà bằng hàng tá mẫu.
  • mốc thời gian
    Nghĩa là, bằng cách tóm tắt các “sự thật” trong một phần nhất định, bạn có thể hiển thị diện mạo của chúng trong ngày. Và ở đây bạn có thể hiểu rằng nếu bạn có một kiểu mẫu nào đó xảy ra, chẳng hạn như mỗi giờ một lần, nhưng nó sẽ xảy ra mỗi ngày một lần, bạn nên nghĩ xem điều gì đã xảy ra - ai đã gây ra nó và tại sao, có lẽ nó nên ở đây không nên. Đây là một phương pháp phân tích phi số, hoàn toàn trực quan.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Các phương pháp còn lại dựa trên các chỉ số mà chúng tôi trích xuất từ ​​kế hoạch: mô hình như vậy xảy ra bao nhiêu lần, tổng thời gian và trung bình, lượng dữ liệu được đọc từ đĩa và bao nhiêu từ bộ nhớ...

Bởi vì, chẳng hạn, bạn đến trang phân tích dành cho máy chủ, hãy nhìn xem - có thứ gì đó đang bắt đầu đọc quá nhiều trên đĩa. Đĩa trên máy chủ không thể xử lý được - ai đọc từ nó?

Và bạn có thể sắp xếp theo bất kỳ cột nào và quyết định xem bạn sẽ giải quyết vấn đề gì ngay bây giờ - tải trên bộ xử lý hoặc ổ đĩa hoặc tổng số yêu cầu... Chúng tôi đã sắp xếp nó, xem xét những cái "trên cùng", sửa nó và tung ra phiên bản mới của ứng dụng.
[video bài giảng]

Và ngay lập tức bạn có thể thấy các ứng dụng khác nhau có cùng một mẫu từ một yêu cầu như SELECT * FROM users WHERE login = 'Vasya'. Giao diện người dùng, phụ trợ, xử lý... Và bạn thắc mắc tại sao quá trình xử lý lại đọc được người dùng nếu anh ta không tương tác với mình.

Cách ngược lại là xem ngay ứng dụng nó làm gì. Ví dụ: giao diện người dùng là cái này, cái này, cái này và cái này mỗi giờ một lần (dòng thời gian sẽ giúp ích). Và câu hỏi ngay lập tức được đặt ra: có vẻ như công việc của frontend không phải là làm điều gì đó mỗi giờ một lần...

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Sau một thời gian, chúng tôi nhận ra rằng chúng tôi thiếu sự tổng hợp thống kê theo nút kế hoạch. Chúng tôi chỉ tách biệt khỏi kế hoạch những nút thực hiện điều gì đó với dữ liệu của các bảng (đọc/ghi chúng theo chỉ mục hoặc không). Trên thực tế, chỉ có một khía cạnh được thêm vào so với hình ảnh trước đó - nút này đã mang lại cho chúng ta bao nhiêu bản ghi?và số lượng bị loại bỏ (Hàng bị xóa bởi bộ lọc).

Bạn không có chỉ mục phù hợp trên đĩa, bạn yêu cầu thì nó bay qua chỉ mục, rơi vào Seq Scan... bạn đã lọc hết bản ghi trừ một bản ghi. Tại sao bạn cần 100 triệu bản ghi được lọc mỗi ngày, cuộn chỉ mục lại không tốt hơn sao?

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Sau khi phân tích tất cả các kế hoạch theo từng nút, chúng tôi nhận ra rằng có một số cấu trúc điển hình trong các kế hoạch rất có thể trông đáng ngờ. Và sẽ rất vui nếu nói với nhà phát triển: “Bạn ơi, ở đây trước tiên bạn đọc theo chỉ mục, sau đó sắp xếp và cắt bỏ” - theo quy định, có một bản ghi.

Mọi người viết truy vấn có lẽ đã gặp phải mẫu này: “Hãy cho tôi đơn đặt hàng cuối cùng cho Vasya, ngày của nó.” Và nếu bạn không có chỉ mục theo ngày hoặc không có ngày nào trong chỉ mục bạn đã sử dụng, thì bạn sẽ bước vào chính xác cùng một "cào".

Nhưng chúng tôi biết rằng đây là một trò “cào cào” - vậy tại sao không nói ngay cho nhà phát triển biết anh ta nên làm gì. Theo đó, khi mở một kế hoạch ngay bây giờ, nhà phát triển của chúng tôi ngay lập tức nhìn thấy một bức tranh đẹp kèm theo các mẹo, ngay lập tức họ nói với anh ta: “Bạn gặp vấn đề chỗ này chỗ kia, nhưng chúng được giải quyết theo cách này và cách khác”.

Kết quả là, lượng kinh nghiệm cần thiết để giải quyết vấn đề lúc đầu và bây giờ đã giảm đáng kể. Đây là loại công cụ chúng tôi có.

Tối ưu hóa hàng loạt các truy vấn PostgreSQL. Kirill Borovikov (Tensor)

Nguồn: www.habr.com

Thêm một lời nhận xét