Trải nghiệm đầu tiên của tôi khi khôi phục cơ sở dữ liệu Postgres sau khi bị lỗi (trang không hợp lệ trong khối 4123007 của cơ sở relatton/16490)

Tôi muốn chia sẻ với bạn trải nghiệm thành công đầu tiên của tôi trong việc khôi phục cơ sở dữ liệu Postgres về đầy đủ chức năng. Tôi làm quen với Postgres DBMS cách đây nửa năm, trước đó tôi hoàn toàn không có kinh nghiệm về quản trị cơ sở dữ liệu.

Trải nghiệm đầu tiên của tôi khi khôi phục cơ sở dữ liệu Postgres sau khi bị lỗi (trang không hợp lệ trong khối 4123007 của cơ sở relatton/16490)

Tôi làm kỹ sư bán DevOps trong một công ty CNTT lớn. Công ty chúng tôi phát triển phần mềm cho các dịch vụ có tải trọng cao và tôi chịu trách nhiệm về hiệu suất, bảo trì và triển khai. Tôi được giao một nhiệm vụ tiêu chuẩn: cập nhật một ứng dụng trên một máy chủ. Ứng dụng được viết bằng Django, trong quá trình di chuyển cập nhật được thực hiện (các thay đổi trong cấu trúc cơ sở dữ liệu) và trước quá trình này, chúng tôi thực hiện kết xuất cơ sở dữ liệu đầy đủ thông qua chương trình pg_dump tiêu chuẩn, để đề phòng.

Đã xảy ra lỗi không mong muốn khi thực hiện kết xuất (Postgres phiên bản 9.5):

pg_dump: Oumping the contents of table “ws_log_smevlog” failed: PQgetResult() failed.
pg_dump: Error message from server: ERROR: invalid page in block 4123007 of relatton base/16490/21396989
pg_dump: The command was: COPY public.ws_log_smevlog [...]
pg_dunp: [parallel archtver] a worker process dled unexpectedly

Bug "trang không hợp lệ trong khối" nói về các vấn đề ở cấp độ hệ thống tập tin, điều này rất tệ. Trên nhiều diễn đàn khác nhau, người ta đã đề xuất làm CHÂN KHÔNG ĐẦY ĐỦ với tùy chọn zero_damaged_pages để giải quyết vấn đề này. Nào, hãy thử...

Chuẩn bị phục hồi

Chú ý! Hãy nhớ sao lưu Postgres trước khi cố gắng khôi phục cơ sở dữ liệu của bạn. Nếu bạn có máy ảo, hãy dừng cơ sở dữ liệu và chụp ảnh nhanh. Nếu không thể chụp ảnh nhanh, hãy dừng cơ sở dữ liệu và sao chép nội dung của thư mục Postgres (bao gồm cả tệp wal) vào nơi an toàn. Điều quan trọng nhất trong công việc kinh doanh của chúng tôi là không làm mọi việc trở nên tồi tệ hơn. Đọc này.

Vì cơ sở dữ liệu thường hoạt động với tôi nên tôi giới hạn bản thân ở kết xuất cơ sở dữ liệu thông thường, nhưng loại trừ bảng có dữ liệu bị hỏng (tùy chọn -T, --exclude-table=BẢNG trong pg_dump).

Máy chủ là vật lý nên không thể chụp ảnh nhanh. Bản sao lưu đã bị xóa, hãy tiếp tục.

Kiểm tra hệ thống tập tin

Trước khi cố gắng khôi phục cơ sở dữ liệu, chúng ta cần đảm bảo rằng mọi thứ đều ổn với chính hệ thống tệp. Và trong trường hợp mắc sai lầm, hãy sửa chúng, vì nếu không bạn chỉ có thể khiến mọi việc trở nên tồi tệ hơn.

Trong trường hợp của tôi, hệ thống tệp có cơ sở dữ liệu đã được gắn vào "/srv" và loại là ext4.

Dừng cơ sở dữ liệu: systemctl dừng [email được bảo vệ] và kiểm tra xem hệ thống tập tin không được ai sử dụng và có thể được ngắt kết nối bằng lệnh lsof.:
lsof +D /srv

Tôi cũng đã phải dừng cơ sở dữ liệu redis vì nó cũng đang sử dụng "/srv". Tiếp theo tôi ngắt kết nối / srv (số lượng lớn).

Hệ thống tập tin đã được kiểm tra bằng tiện ích e2fsck với công tắc -f (Buộc kiểm tra ngay cả khi hệ thống tập tin được đánh dấu là sạch):

Trải nghiệm đầu tiên của tôi khi khôi phục cơ sở dữ liệu Postgres sau khi bị lỗi (trang không hợp lệ trong khối 4123007 của cơ sở relatton/16490)

Tiếp theo, sử dụng tiện ích bãi rác2fs (sudo dumpe2fs /dev/mapper/gu2—sys-srv | đã kiểm tra grep) bạn có thể xác minh rằng việc kiểm tra đã thực sự được thực hiện:

Trải nghiệm đầu tiên của tôi khi khôi phục cơ sở dữ liệu Postgres sau khi bị lỗi (trang không hợp lệ trong khối 4123007 của cơ sở relatton/16490)

e2fsck nói rằng không tìm thấy vấn đề gì ở cấp hệ thống tệp ext4, điều đó có nghĩa là bạn có thể tiếp tục cố gắng khôi phục cơ sở dữ liệu hoặc quay lại chân không đầy đủ (tất nhiên, bạn cần gắn lại hệ thống tập tin và khởi động cơ sở dữ liệu).

Nếu bạn có máy chủ vật lý, hãy đảm bảo kiểm tra trạng thái của các ổ đĩa (thông qua smartctl -a /dev/XXX) hoặc bộ điều khiển RAID để đảm bảo rằng sự cố không nằm ở cấp độ phần cứng. Trong trường hợp của tôi, RAID hóa ra là “phần cứng”, vì vậy tôi đã yêu cầu quản trị viên địa phương kiểm tra trạng thái của RAID (máy chủ cách tôi vài trăm km). Anh ấy nói rằng không có sai sót nào, điều đó có nghĩa là chúng tôi chắc chắn có thể bắt đầu khôi phục.

Lần thử 1: zero_damaged_pages

Chúng tôi kết nối với cơ sở dữ liệu qua psql bằng tài khoản có quyền siêu người dùng. Chúng ta cần một siêu người dùng, bởi vì... lựa chọn zero_damaged_pages chỉ có anh ấy mới có thể thay đổi. Trong trường hợp của tôi, đó là postgres:

psql -h 127.0.0.1 -U postgres -s [tên cơ sở dữ liệu]

Lựa chọn zero_damaged_pages cần thiết để bỏ qua lỗi đọc (từ trang web postgrespro):

Khi PostgreSQL phát hiện tiêu đề trang bị hỏng, nó thường báo lỗi và hủy giao dịch hiện tại. Nếu zero_damaged_pages được bật, hệ thống sẽ đưa ra cảnh báo, loại bỏ trang bị hỏng trong bộ nhớ và tiếp tục xử lý. Hành vi này sẽ hủy dữ liệu, cụ thể là tất cả các hàng trong trang bị hỏng.

Chúng tôi kích hoạt tùy chọn và cố gắng thực hiện hút chân không toàn bộ các bảng:

VACUUM FULL VERBOSE

Trải nghiệm đầu tiên của tôi khi khôi phục cơ sở dữ liệu Postgres sau khi bị lỗi (trang không hợp lệ trong khối 4123007 của cơ sở relatton/16490)
Thật không may, xui xẻo.

Chúng tôi gặp phải một lỗi tương tự:

INFO: vacuuming "“public.ws_log_smevlog”
WARNING: invalid page in block 4123007 of relation base/16400/21396989; zeroing out page
ERROR: unexpected chunk number 573 (expected 565) for toast value 21648541 in pg_toast_106070

pg_toast – một cơ chế lưu trữ “dữ liệu dài” trong Poetgres nếu nó không vừa trên một trang (theo mặc định là 8kb).

Nỗ lực 2: reindex

Lời khuyên đầu tiên từ Google không giúp ích được gì. Sau vài phút tìm kiếm, tôi tìm thấy mẹo thứ hai - tạo giới thiệu lại bàn bị hư. Tôi đã thấy lời khuyên này ở nhiều nơi nhưng nó không tạo được sự tự tin. Hãy lập chỉ mục lại:

reindex table ws_log_smevlog

Trải nghiệm đầu tiên của tôi khi khôi phục cơ sở dữ liệu Postgres sau khi bị lỗi (trang không hợp lệ trong khối 4123007 của cơ sở relatton/16490)

giới thiệu lại hoàn thành mà không có vấn đề.

Tuy nhiên, điều này không giúp ích được gì, CHÂN KHÔNG ĐẦY ĐỦ gặp sự cố với một lỗi tương tự. Vì tôi đã quen với những thất bại nên tôi bắt đầu tìm kiếm thêm lời khuyên trên Internet và tình cờ thấy một điều khá thú vị. Bài viết.

Nỗ lực 3: CHỌN, GIỚI HẠN, BẮT ĐẦU

Bài viết trên đề xuất xem xét từng hàng trong bảng và loại bỏ dữ liệu có vấn đề. Đầu tiên chúng ta cần xem xét tất cả các dòng:

for ((i=0; i<"Number_of_rows_in_nodes"; i++ )); do psql -U "Username" "Database Name" -c "SELECT * FROM nodes LIMIT 1 offset $i" >/dev/null || echo $i; done

Trong trường hợp của tôi, bảng chứa 1 628 991 dòng! Nó là cần thiết để chăm sóc tốt phân vùng dữ liệu, nhưng đây là một chủ đề dành cho một cuộc thảo luận riêng. Hôm đó là thứ bảy, tôi chạy lệnh này trong tmux và đi ngủ:

for ((i=0; i<1628991; i++ )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog LIMIT 1 offset $i" >/dev/null || echo $i; done

Đến sáng tôi quyết định kiểm tra xem mọi việc diễn ra như thế nào. Thật ngạc nhiên, tôi phát hiện ra rằng sau 20 giờ, chỉ có 2% dữ liệu được quét! Tôi không muốn đợi 50 ngày. Lại một thất bại hoàn toàn nữa.

Nhưng tôi không bỏ cuộc. Tôi tự hỏi tại sao quá trình quét lại mất nhiều thời gian như vậy. Từ tài liệu (một lần nữa trên postgrespro) tôi phát hiện ra:

OFFSET chỉ định bỏ qua số lượng hàng được chỉ định trước khi bắt đầu xuất các hàng.
Nếu cả hai OFFSET và LIMIT đều được chỉ định, trước tiên hệ thống sẽ bỏ qua các hàng OFFSET và sau đó bắt đầu đếm các hàng cho ràng buộc LIMIT.

Khi sử dụng LIMIT, điều quan trọng là phải sử dụng mệnh đề ORDER BY để các hàng kết quả được trả về theo một thứ tự cụ thể. Nếu không, các tập hợp con hàng không thể đoán trước sẽ được trả về.

Rõ ràng lệnh trên sai: thứ nhất, không có đặt bởi, kết quả có thể sai. Thứ hai, Postgres trước tiên phải quét và bỏ qua các hàng OFFSET, và với tốc độ ngày càng tăng OFFSET năng suất sẽ còn giảm hơn nữa.

Nỗ lực 4: kết xuất ở dạng văn bản

Sau đó, một ý tưởng có vẻ tuyệt vời nảy ra trong đầu tôi: lấy một bản kết xuất ở dạng văn bản và phân tích dòng được ghi cuối cùng.

Nhưng trước tiên, chúng ta hãy xem cấu trúc của bảng. ws_log_smevlog:

Trải nghiệm đầu tiên của tôi khi khôi phục cơ sở dữ liệu Postgres sau khi bị lỗi (trang không hợp lệ trong khối 4123007 của cơ sở relatton/16490)

Trong trường hợp của chúng tôi, chúng tôi có một cột "Tôi", chứa mã định danh duy nhất (bộ đếm) của hàng. Kế hoạch là như thế này:

  1. Chúng tôi bắt đầu thực hiện kết xuất ở dạng văn bản (dưới dạng lệnh sql)
  2. Tại một thời điểm nhất định, quá trình kết xuất sẽ bị gián đoạn do lỗi, nhưng tệp văn bản vẫn được lưu trên đĩa
  3. Chúng ta nhìn vào cuối file văn bản, từ đó tìm ra mã định danh (id) của dòng cuối cùng đã được xóa thành công

Tôi bắt đầu lấy một kết xuất ở dạng văn bản:

pg_dump -U my_user -d my_database -F p -t ws_log_smevlog -f ./my_dump.dump

Kết xuất, như mong đợi, đã bị gián đoạn với cùng một lỗi:

pg_dump: Error message from server: ERROR: invalid page in block 4123007 of relatton base/16490/21396989

Hơn nữa thông qua đuôi Tôi nhìn vào cuối bãi rác (đuôi -5 ./my_dump.dump) phát hiện ra rằng kết xuất bị gián đoạn trên dòng có id 186 525. “Vậy vấn đề là ở dòng id 186 526, nó bị hỏng, cần phải xóa đi!” - Tôi đã nghĩ. Tuy nhiên, thực hiện truy vấn tới cơ sở dữ liệu:
«chọn * từ ws_log_smevlog trong đó id=186529"Hóa ra mọi thứ đều ổn với dòng này... Các hàng có chỉ số 186 - 530 cũng hoạt động mà không gặp vấn đề gì. Một “ý tưởng tuyệt vời” khác đã thất bại. Sau này tôi mới hiểu tại sao điều này lại xảy ra: khi xóa và thay đổi dữ liệu khỏi một bảng, chúng không bị xóa về mặt vật lý mà được đánh dấu là “bộ dữ liệu chết”, sau đó xuất hiện máy hút tự động và đánh dấu những dòng này là đã bị xóa và cho phép những dòng này được sử dụng lại. Để hiểu rõ, nếu dữ liệu trong bảng thay đổi và tính năng tự động hút chân không được bật thì dữ liệu đó sẽ không được lưu trữ tuần tự.

Nỗ lực 5: CHỌN, TỪ, WHERE id=

Thất bại làm chúng ta mạnh mẽ hơn. Bạn không bao giờ nên bỏ cuộc, bạn cần phải đi đến cùng và tin tưởng vào bản thân cũng như khả năng của mình. Vì vậy, tôi quyết định thử một lựa chọn khác: chỉ cần xem qua từng bản ghi trong cơ sở dữ liệu. Biết cấu trúc bảng của tôi (xem ở trên), chúng tôi có trường id là duy nhất (khóa chính). Chúng tôi có 1 hàng trong bảng và id theo thứ tự, có nghĩa là chúng ta có thể đi qua từng cái một:

for ((i=1; i<1628991; i=$((i+1)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done

Nếu ai không hiểu thì lệnh này hoạt động như sau: quét từng hàng trong bảng và gửi thiết bị xuất chuẩn tới / dev / null, nhưng nếu lệnh SELECT không thành công thì văn bản lỗi sẽ được in (stderr được gửi đến bàn điều khiển) và một dòng chứa lỗi sẽ được in (nhờ ||, nghĩa là phần chọn có vấn đề (mã trả về của lệnh không phải là 0)).

Tôi thật may mắn, tôi đã tạo được các chỉ mục trên sân id:

Trải nghiệm đầu tiên của tôi khi khôi phục cơ sở dữ liệu Postgres sau khi bị lỗi (trang không hợp lệ trong khối 4123007 của cơ sở relatton/16490)

Điều này có nghĩa là việc tìm một dòng có id mong muốn sẽ không mất nhiều thời gian. Về lý thuyết nó sẽ hoạt động. Nào, hãy chạy lệnh trong tmux và chúng ta hãy đi ngủ thôi.

Đến sáng, tôi thấy có khoảng 90 bài viết đã được xem, tức là chỉ hơn 000%. Một kết quả tuyệt vời khi so sánh với phương pháp trước đó (5%)! Nhưng tôi không muốn đợi 2 ngày...

Lần thử 6: CHỌN, TỪ, WHERE id >= và id

Khách hàng có một máy chủ tuyệt vời dành riêng cho cơ sở dữ liệu: bộ xử lý kép Intel Xeon E5 2697 v2, có tới 48 chủ đề ở vị trí của chúng tôi! Tải trên máy chủ ở mức trung bình, chúng tôi có thể tải xuống khoảng 20 luồng mà không gặp vấn đề gì. Cũng có đủ RAM: lên tới 384 gigabyte!

Do đó, lệnh cần được song song:

for ((i=1; i<1628991; i=$((i+1)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done

Ở đây có thể viết một đoạn script đẹp và thanh lịch, nhưng tôi đã chọn phương pháp song song nhanh nhất: chia thủ công phạm vi 0-1628991 thành các khoảng 100 bản ghi và chạy riêng 000 lệnh có dạng:

for ((i=N; i<M; i=$((i+1)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done

Nhưng đó không phải là tất cả. Về lý thuyết, việc kết nối với cơ sở dữ liệu cũng tốn một chút thời gian và tài nguyên hệ thống. Kết nối 1 không thông minh lắm, bạn sẽ đồng ý. Do đó, hãy truy xuất 628 hàng thay vì kết nối một đối một. Kết quả là nhóm đã biến đổi thành thế này:

for ((i=N; i<M; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done

Mở 16 cửa sổ trong phiên tmux và chạy các lệnh:

1) for ((i=0; i<100000; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done
2) for ((i=100000; i<200000; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done
…
15) for ((i=1400000; i<1500000; i=$((i+1000)) )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done
16) for ((i=1500000; i<1628991; i=$((i+1000)) )); do psql -U my_user -d my_database  -c "SELECT * FROM ws_log_smevlog where id>=$i and id<$((i+1000))" >/dev/null || echo $i; done

Một ngày sau tôi nhận được kết quả đầu tiên! Cụ thể (các giá trị XXX và ZZZ không còn được giữ nguyên):

ERROR:  missing chunk number 0 for toast value 37837571 in pg_toast_106070
829000
ERROR:  missing chunk number 0 for toast value XXX in pg_toast_106070
829000
ERROR:  missing chunk number 0 for toast value ZZZ in pg_toast_106070
146000

Điều này có nghĩa là ba dòng có lỗi. Id của bản ghi sự cố thứ nhất và thứ hai nằm trong khoảng từ 829 đến 000, id của bản ghi sự cố thứ ba nằm trong khoảng từ 830 đến 000. Tiếp theo, chúng tôi chỉ cần tìm giá trị id chính xác của bản ghi sự cố. Để làm điều này, chúng tôi xem xét phạm vi của chúng tôi với các bản ghi có vấn đề với bước 146 và xác định id:

for ((i=829000; i<830000; i=$((i+1)) )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done
829417
ERROR:  unexpected chunk number 2 (expected 0) for toast value 37837843 in pg_toast_106070
829449
for ((i=146000; i<147000; i=$((i+1)) )); do psql -U my_user -d my_database -c "SELECT * FROM ws_log_smevlog where id=$i" >/dev/null || echo $i; done
829417
ERROR:  unexpected chunk number ZZZ (expected 0) for toast value XXX in pg_toast_106070
146911

Kết thúc có hậu

Chúng tôi đã tìm thấy những dòng có vấn đề. Chúng tôi đi vào cơ sở dữ liệu thông qua psql và cố gắng xóa chúng:

my_database=# delete from ws_log_smevlog where id=829417;
DELETE 1
my_database=# delete from ws_log_smevlog where id=829449;
DELETE 1
my_database=# delete from ws_log_smevlog where id=146911;
DELETE 1

Thật ngạc nhiên, các mục đã bị xóa mà không gặp vấn đề gì ngay cả khi không có tùy chọn zero_damaged_pages.

Sau đó tôi kết nối với cơ sở dữ liệu, đã làm CHÂN KHÔNG ĐẦY ĐỦ (Tôi nghĩ không cần thiết phải làm điều này) và cuối cùng tôi đã xóa thành công bản sao lưu bằng cách sử dụng pg_dump. Kết xuất đã được thực hiện mà không có bất kỳ lỗi nào! Vấn đề đã được giải quyết một cách ngu ngốc như vậy. Niềm vui không có giới hạn, sau bao lần thất bại chúng tôi đã tìm ra được giải pháp!

Lời cảm ơn và kết luận

Đây là trải nghiệm đầu tiên của tôi về việc khôi phục cơ sở dữ liệu Postgres thực sự. Tôi sẽ nhớ trải nghiệm này rất lâu.

Và cuối cùng, tôi muốn gửi lời cảm ơn tới PostgresPro vì đã dịch tài liệu sang tiếng Nga và vì khóa học trực tuyến hoàn toàn miễn phí, điều này đã giúp ích rất nhiều trong quá trình phân tích vấn đề.

Nguồn: www.habr.com

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