Viết chương trình để đối phó với lỗi I / O gây mất ghi trên Linux


138

TL; DR: Nếu nhân Linux mất ghi I / O được đệm , có cách nào để ứng dụng tìm ra không?

Tôi biết bạn phải fsync()tập tin (và thư mục mẹ của nó) cho độ bền . Câu hỏi đặt ra là nếu kernel mất bộ đệm bẩn đang chờ ghi do lỗi I / O, làm thế nào để ứng dụng có thể phát hiện ra điều này và phục hồi hoặc hủy bỏ?

Hãy nghĩ rằng các ứng dụng cơ sở dữ liệu, vv, trong đó thứ tự ghi và viết độ bền có thể rất quan trọng.

Mất viết? Làm sao?

Trong một số trường hợp, lớp khối của hạt nhân Linux có thể bị mất các yêu cầu I / O được đệm đã được gửi thành công write(), pwrite()v.v., với một lỗi như:

Buffer I/O error on device dm-0, logical block 12345
lost page write due to I/O error on dm-0

(Xem end_buffer_write_sync(...)end_buffer_async_write(...)trongfs/buffer.c ).

Trên các hạt nhân mới hơn, thay vào đó, lỗi sẽ chứa "ghi trang không đồng bộ bị mất" , như:

Buffer I/O error on dev dm-0, logical block 12345, lost async page write

Vì ứng dụng write()sẽ quay trở lại mà không có lỗi, nên dường như không có cách nào để báo cáo lỗi về ứng dụng.

Phát hiện chúng?

Tôi không quen thuộc với các nguồn kernel, nhưng tôi nghĩ rằng nó đặt AS_EIOtrên bộ đệm không được ghi ra nếu nó thực hiện ghi async:

    set_bit(AS_EIO, &page->mapping->flags);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

nhưng nó không rõ ràng với tôi nếu hoặc làm thế nào ứng dụng có thể tìm hiểu về điều này khi nó fsync()là tệp để xác nhận nó trên đĩa.

Dường như wait_on_page_writeback_range(...)trongmm/filemap.c sức bởi do_sync_mapping_range(...)trongfs/sync.c đó là rẽ gọi bằng sys_sync_file_range(...). Nó trả về -EIOnếu một hoặc nhiều bộ đệm không thể được viết.

Nếu, như tôi đoán, điều này lan truyền đến fsync()kết quả, sau đó nếu ứng dụng hoảng loạn và giải cứu nếu nó bị lỗi I / O fsync()và biết cách thực hiện lại công việc của nó khi được khởi động lại, điều đó có đủ an toàn không?

Có lẽ không có cách nào để ứng dụng biết được byte nào trong tệp tương ứng với các trang bị mất để nó có thể viết lại chúng nếu biết, nhưng nếu ứng dụng lặp lại tất cả công việc đang chờ xử lý của nó kể từ lần thành công cuối cùng fsync()của tệp và điều đó viết lại bất kỳ bộ đệm kernel bẩn nào tương ứng với ghi bị mất đối với tệp, sẽ xóa mọi cờ lỗi I / O trên các trang bị mất và cho phép tiếp theo fsync()hoàn thành - phải không?

Sau đó, có trường hợp nào khác, vô hại, nơi fsync()có thể trở lại -EIOnơi bảo lãnh và làm lại công việc sẽ quá quyết liệt?

Tại sao?

Tất nhiên lỗi như vậy không nên xảy ra. Trong trường hợp này, lỗi phát sinh từ sự tương tác đáng tiếc giữa dm-multipathmặc định của trình điều khiển và mã cảm giác được SAN sử dụng để báo cáo lỗi không phân bổ dung lượng lưu trữ được cung cấp mỏng. Nhưng đây không phải là trường hợp duy nhất có thể xảy ra - tôi cũng đã thấy các báo cáo về nó từ LVM được cung cấp mỏng chẳng hạn, như được sử dụng bởi libvirt, Docker, v.v. Một ứng dụng quan trọng như cơ sở dữ liệu nên cố gắng đối phó với các lỗi như vậy, thay vì mù quáng tiếp tục như thể tất cả đều ổn.

Nếu kernel nghĩ rằng không sao để mất ghi mà không chết với kernel, thì các ứng dụng phải tìm cách đối phó.

Tác động thực tế là tôi đã tìm thấy một trường hợp trong đó một vấn đề đa luồng với SAN gây ra mất ghi đã hạ cánh gây ra tham nhũng cơ sở dữ liệu vì DBMS không biết rằng bài viết của nó đã thất bại. Không vui.


1
Tôi e rằng điều này sẽ cần các trường bổ sung trong SystemFileTable để lưu trữ và ghi nhớ các điều kiện lỗi này. Và khả năng quá trình không gian người dùng nhận hoặc kiểm tra chúng trong các cuộc gọi tiếp theo. (làm fsync () và đóng () trả lại loại thông tin lịch sử này ?)
tham gia

@joop Cảm ơn. Tôi vừa đăng một câu trả lời với những gì tôi nghĩ đang diễn ra, hãy chú ý kiểm tra sự tỉnh táo vì bạn dường như biết nhiều hơn về những gì đang diễn ra so với những người đã đăng các biến thể rõ ràng của "write () cần close () hoặc fsync ( ) cho độ bền "mà không cần đọc câu hỏi?
Craig Ringer

BTW: Tôi nghĩ rằng bạn thực sự nên đi sâu vào các nguồn kernel. Các hệ thống tập tin tạp chí có thể sẽ gặp phải vấn đề tương tự. Chưa kể xử lý phân vùng trao đổi. Vì chúng sống trong không gian kernel, nên việc xử lý các điều kiện này có thể sẽ cứng hơn một chút. writev (), có thể nhìn thấy từ không gian người dùng, cũng có vẻ như là một nơi để tìm. [tại Craig: vâng vì tôi biết tên của bạn và tôi biết bạn không phải là một thằng ngốc hoàn toàn; -]
tham gia

1
Tôi đồng ý, tôi đã không công bằng. Than ôi câu trả lời của bạn không thỏa mãn lắm, ý tôi là không có giải pháp dễ dàng (đáng ngạc nhiên?).
Jean-Baptiste Yunès

1
@ Jean-BaptisteYunès Đúng. Đối với DBMS tôi đang làm việc, "sự cố và nhập lại" được chấp nhận. Đối với hầu hết các ứng dụng không phải là một tùy chọn và chúng có thể phải chấp nhận hiệu năng khủng khiếp của I / O đồng bộ hoặc chỉ chấp nhận hành vi được xác định kém và tham nhũng đối với các lỗi I / O.
Craig Ringer

Câu trả lời:


91

fsync()trả về -EIOnếu kernel bị mất ghi

(Lưu ý: phần đầu tham khảo các hạt nhân cũ hơn; được cập nhật bên dưới để phản ánh các hạt nhân hiện đại)

Dường như lỗi ghi bộ đệm async trong các end_buffer_async_write(...)lỗi đặt -EIOcờ trên trang bộ đệm bẩn không thành công cho tệp :

set_bit(AS_EIO, &page->mapping->flags);
set_buffer_write_io_error(bh);
clear_buffer_uptodate(bh);
SetPageError(page);

sau đó được phát hiện bởi wait_on_page_writeback_range(...)như gọi bằng do_sync_mapping_range(...)như gọi bằng sys_sync_file_range(...)như gọi bằng sys_sync_file_range2(...)để thực hiện các cuộc gọi thư viện C fsync().

Nhưng chỉ một lần!

Nhận xét này về sys_sync_file_range

168  * SYNC_FILE_RANGE_WAIT_BEFORE and SYNC_FILE_RANGE_WAIT_AFTER will detect any
169  * I/O errors or ENOSPC conditions and will return those to the caller, after
170  * clearing the EIO and ENOSPC flags in the address_space.

gợi ý rằng khi fsync()trả về -EIOhoặc (không có giấy tờ trong trang chủ) -ENOSPC, nó sẽ xóa trạng thái lỗi để tiếp theo fsync()sẽ báo cáo thành công mặc dù các trang không bao giờ được viết.

Chắc chắn đủ để wait_on_page_writeback_range(...) xóa các bit lỗi khi nó kiểm tra chúng :

301         /* Check for outstanding write errors */
302         if (test_and_clear_bit(AS_ENOSPC, &mapping->flags))
303                 ret = -ENOSPC;
304         if (test_and_clear_bit(AS_EIO, &mapping->flags))
305                 ret = -EIO;

Vì vậy, nếu ứng dụng mong đợi nó có thể thử lại fsync()cho đến khi thành công và tin tưởng rằng dữ liệu trên đĩa, thì đó là sai lầm khủng khiếp.

Tôi khá chắc chắn đây là nguồn gốc của sự hỏng dữ liệu mà tôi tìm thấy trong DBMS. Nó thử lại fsync()và nghĩ rằng tất cả sẽ tốt khi nó thành công.

Điều này có được phép không?

Các tài liệu POSIX / SuS trênfsync() không thực sự chỉ định theo cách này:

Nếu chức năng fsync () không thành công, các hoạt động I / O nổi bật không được đảm bảo đã hoàn thành.

Trang con người của Linuxfsync() không nói gì về những gì xảy ra khi thất bại.

Vì vậy, có vẻ như ý nghĩa của fsync()lỗi là "dunno những gì đã xảy ra với bài viết của bạn, có thể đã hoạt động hoặc không, tốt hơn là thử lại để chắc chắn".

Hạt nhân mới hơn

Trên 4.9 end_buffer_async_writebộ -EIOtrên trang, chỉ cần thông qua mapping_set_error.

    buffer_io_error(bh, ", lost async page write");
    mapping_set_error(page->mapping, -EIO);
    set_buffer_write_io_error(bh);
    clear_buffer_uptodate(bh);
    SetPageError(page);

Về mặt đồng bộ tôi nghĩ nó tương tự nhau, mặc dù cấu trúc bây giờ khá phức tạp để theo dõi. filemap_check_errorstrong mm/filemap.cgiờ làm:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;

trong đó có nhiều tác dụng tương tự. Kiểm tra lỗi dường như tất cả đều trải qua filemap_check_errorsmà kiểm tra và rõ ràng:

    if (test_bit(AS_EIO, &mapping->flags) &&
        test_and_clear_bit(AS_EIO, &mapping->flags))
            ret = -EIO;
    return ret;

Tôi đang sử dụng btrfstrên máy tính xách tay của mình, nhưng khi tôi tạo một ext4loopback để thử nghiệm /mnt/tmpvà thiết lập một đầu dò hoàn hảo trên nó:

sudo dd if=/dev/zero of=/tmp/ext bs=1M count=100
sudo mke2fs -j -T ext4 /tmp/ext
sudo mount -o loop /tmp/ext /mnt/tmp

sudo perf probe filemap_check_errors

sudo perf record -g -e probe:end_buffer_async_write -e probe:filemap_check_errors dd if=/dev/zero of=/mnt/tmp/test bs=4k count=1 conv=fsync

Tôi tìm thấy ngăn xếp cuộc gọi sau đây trong perf report -T:

        ---__GI___libc_fsync
           entry_SYSCALL_64_fastpath
           sys_fsync
           do_fsync
           vfs_fsync_range
           ext4_sync_file
           filemap_write_and_wait_range
           filemap_check_errors

Đọc qua gợi ý rằng yeah, hạt nhân hiện đại hành xử giống nhau.

Điều này dường như có nghĩa là nếu fsync()(hoặc có lẽ write()hoặc close()) trở lại -EIO, tệp ở trạng thái không xác định giữa thời điểm bạn thành công fsync()d hoặc close()d nó và write()mười trạng thái gần đây nhất của nó .

Kiểm tra

Tôi đã thực hiện một trường hợp thử nghiệm để chứng minh hành vi này .

Hàm ý

Một DBMS có thể đối phó với điều này bằng cách vào phục hồi sự cố. Làm thế nào trên trái đất là một ứng dụng người dùng bình thường được cho là để đối phó với điều này? Các fsync()trang người đàn ông không đưa ra cảnh báo rằng nó có nghĩa là "fsync-if-you-cảm-như-nó" và tôi hy vọng một nhiều các ứng dụng sẽ không đối phó tốt với hành vi này.

Báo cáo lỗi

đọc thêm

lwn.net đã chạm vào điều này trong bài viết "Cải thiện xử lý lỗi lớp khối" .

postgresql.org chủ đề danh sách gửi thư .


3
lxr.free-electrons.com/source/fs/buffer.c?v=2.6.26#L598 là một cuộc đua có thể, bởi vì nó chờ {I / O chờ xử lý & lên lịch, không phải cho {I / O chưa lên lịch}. Điều này rõ ràng là để tránh các chuyến đi khứ hồi thêm cho thiết bị. (Tôi cho rằng người dùng viết () không quay lại cho đến khi I / O được lên lịch, đối với mmap (), điều này khác)
tham gia

3
Có thể cuộc gọi nào đó của fsync đối với một số tệp khác trên cùng một đĩa sẽ bị trả về lỗi không?
Random832

3
@ Random832 Rất phù hợp với một DB đa xử lý như PostgreSQL, vì vậy câu hỏi hay. Có vẻ như có thể, nhưng tôi không biết mã hạt nhân đủ để hiểu. Các procs của bạn tốt hơn nên hợp tác nếu cả hai đều có cùng một tệp được mở.
Craig Ringer

1
@DavidFoerster: Các tòa nhà chọc trời thất bại khi sử dụng mã errno âm; errnohoàn toàn là một cấu trúc của thư viện C không gian người dùng. Người ta thường bỏ qua sự khác biệt về giá trị trả về giữa các tòa nhà và thư viện C như thế này (như Craig Ringer, ở trên), vì giá trị trả về lỗi xác định một cách đáng tin cậy cái nào (chức năng thư viện C hoặc tòa nhà C) đang được đề cập đến: " -1với errno==EIO"Đề cập đến chức năng thư viện C, trong khi" -EIO"đề cập đến một tòa nhà chọc trời. Cuối cùng, các trang man Linux trực tuyến là tài liệu tham khảo cập nhật nhất cho các trang man Linux.
Động vật danh nghĩa

2
@CraigRinger: Để trả lời câu hỏi cuối cùng của bạn: "Bằng cách sử dụng I / O cấp thấp và fsync()/ fdatasync()khi kích thước giao dịch là một tệp hoàn chỉnh; bằng cách sử dụng mmap()/ msync()khi kích thước giao dịch là một bản ghi được căn chỉnh theo trang và bằng cách sử dụng I cấp thấp / O, fdatasync()và nhiều mô tả tệp đồng thời (một mô tả và một luồng trên mỗi giao dịch) cho cùng một tệp khác " . Các khóa mô tả tệp mở dành riêng cho Linux ( fcntl(), F_OFD_) rất hữu ích với khóa cuối cùng.
Động vật danh nghĩa

22

Vì write () của ứng dụng sẽ được trả về mà không có lỗi, nên dường như không có cách nào để báo cáo lại lỗi cho ứng dụng.

Tôi không đồng ý. writecó thể trả về mà không có lỗi nếu ghi đơn giản là được xếp hàng, nhưng lỗi sẽ được báo cáo trong thao tác tiếp theo sẽ yêu cầu ghi thực tế trên đĩa, có nghĩa là tiếp theo fsync, có thể là ghi sau nếu hệ thống quyết định xóa bộ đệm và tại ít nhất trên tập tin cuối cùng đóng.

Đó là lý do tại sao ứng dụng cần thiết để kiểm tra giá trị trả về gần để phát hiện các lỗi ghi có thể xảy ra.

Nếu bạn thực sự cần có khả năng xử lý lỗi thông minh, bạn phải cho rằng mọi thứ được viết từ lần thành công cuối cùng fsync có thể đã thất bại và trong tất cả những điều đó ít nhất đã xảy ra.


4
Vâng, tôi nghĩ rằng móng tay đó. Điều này thực sự sẽ gợi ý rằng ứng dụng nên thực hiện lại tất cả công việc của nó kể từ lần xác nhận cuối cùng - thành công fsync()hoặc close()của tệp nếu nó nhận được -EIOtừ write(), fsync()hoặc close(). Chà, thật vui.
Craig Ringer

1

write(2) cung cấp ít hơn bạn mong đợi. Trang người đàn ông rất cởi mở về ngữ nghĩa của một write()cuộc gọi thành công :

Hoàn trả thành công từ write()không đảm bảo rằng dữ liệu đã được cam kết vào đĩa. Trong thực tế, trên một số triển khai lỗi, thậm chí không đảm bảo rằng không gian đã được dành riêng cho dữ liệu. Cách duy nhất để chắc chắn là gọi fsync(2) sau khi bạn viết xong tất cả dữ liệu của mình.

Chúng ta có thể kết luận rằng thành công write()chỉ có nghĩa là dữ liệu đã đạt đến các cơ sở đệm của kernel. Nếu vẫn tồn tại bộ đệm thất bại, lần truy cập tiếp theo vào bộ mô tả tệp sẽ trả về mã lỗi. Như là phương sách cuối cùng có thể close(). Trang man của closelệnh gọi hệ thống (2) chứa câu sau:

Hoàn toàn có thể xảy ra lỗi trong writethao tác (2) trước đó được báo cáo đầu tiên tại Final close().

Nếu ứng dụng của bạn cần duy trì dữ liệu, hãy ghi lại, nó phải sử dụng fsync/ fsyncdatathường xuyên:

fsync()chuyển ("tuôn ra") tất cả dữ liệu trong lõi đã sửa đổi của (nghĩa là các trang bộ đệm bộ đệm đã sửa đổi cho) tệp được mô tả bởi tệp mô tả tệp fd vào thiết bị đĩa (hoặc thiết bị lưu trữ vĩnh viễn khác) để có thể lấy tất cả thông tin thay đổi ngay cả sau khi hệ thống bị sập hoặc được khởi động lại. Điều này bao gồm ghi thông qua hoặc xóa bộ đệm đĩa nếu có. Các cuộc gọi chặn cho đến khi thiết bị báo cáo rằng việc chuyển đã hoàn thành.


4
Vâng, tôi biết rằng đó fsync()là bắt buộc. Nhưng trong trường hợp cụ thể mà kernel mất các trang do lỗi I / O sẽ fsync()thất bại? Trong hoàn cảnh nào sau đó nó có thể thành công?
Craig Ringer

Tôi cũng không biết nguồn kernel. Giả sử fsync()lợi nhuận của -EIOcác vấn đề I / O (Điều gì sẽ tốt cho mặt khác?). Vì vậy, cơ sở dữ liệu biết một số lần ghi trước đó không thành công và có thể chuyển sang chế độ phục hồi. Đây không phải là những gì bạn muốn? Động lực của câu hỏi cuối cùng của bạn là gì? Bạn có muốn biết ghi nào thất bại hoặc khôi phục bộ mô tả tệp để sử dụng thêm không?
fzgregor

Lý tưởng nhất là DBMS sẽ không muốn vào phục hồi sự cố (khởi động tất cả người dùng và tạm thời không thể truy cập hoặc ít nhất là chỉ đọc) nếu có thể tránh được. Nhưng ngay cả khi hạt nhân có thể cho chúng ta biết "byte 4096 đến 8191 của fd X", thật khó để tìm ra những gì (viết lại) ở đó mà không cần thực hiện quá trình khôi phục sự cố. Vì vậy, tôi đoán câu hỏi chính là liệu có bất kỳ hoàn cảnh vô tội nhiều nơi fsync()có thể trở lại -EIOnơi mà nó an toàn để thử lại, và nếu nó có thể biết sự khác biệt.
Craig Ringer

Chắc chắn phục hồi sự cố là biện pháp cuối cùng. Nhưng như bạn đã nói những vấn đề này dự kiến ​​sẽ rất rất hiếm. Do đó, tôi không thấy có vấn đề gì trong việc phục hồi -EIO. Nếu mỗi mô tả tệp chỉ được sử dụng bởi một luồng tại một thời điểm, thì luồng này có thể quay lại lần cuối fsync()và thực hiện lại các write()cuộc gọi. Tuy nhiên, nếu những người đó write()chỉ viết một phần của một lĩnh vực thì phần không được sửa đổi vẫn có thể bị hỏng.
fzgregor

1
Bạn đúng rằng sẽ đi vào phục hồi sự cố có thể hợp lý. Đối với các phần bị hỏng một phần, DBMS (PostgreSQL) lưu trữ hình ảnh của toàn bộ trang ngay lần đầu tiên chạm vào nó sau bất kỳ điểm kiểm tra nào vì lý do đó, vì vậy nó sẽ ổn :)
Craig Ringer

0

Sử dụng cờ O_SYNC khi bạn mở tệp. Nó đảm bảo dữ liệu được ghi vào đĩa.

Nếu điều này không làm bạn hài lòng, sẽ không có gì.


17
O_SYNClà một cơn ác mộng cho hiệu suất. Điều đó có nghĩa là ứng dụng không thể làm gì khác trong khi I / O đĩa xảy ra trừ khi nó sinh ra các luồng I / O. Bạn cũng có thể nói rằng giao diện I / O được đệm là không an toàn và mọi người nên sử dụng AIO. Chắc chắn viết bị mất âm thầm có thể được chấp nhận trong I / O được đệm?
Craig Ringer

3
( O_DATASYNCchỉ tốt hơn một chút về vấn đề đó)
Craig Ringer

@CraigRinger Bạn nên sử dụng AIO nếu bạn có nhu cầu này và cần bất kỳ loại hiệu suất nào. Hoặc chỉ sử dụng DBMS; nó xử lý mọi thứ cho bạn
Demi

10
@Demi Ứng dụng ở đây là một dbms (postgresql). Tôi chắc rằng bạn có thể tưởng tượng rằng việc viết lại toàn bộ ứng dụng để sử dụng AIO thay vì I / O được đệm là không thực tế. Cũng không cần thiết.
Craig Ringer

-5

Kiểm tra giá trị trả về của đóng. đóng có thể thất bại trong khi ghi đệm xuất hiện để thành công.


8
Chà, chúng tôi hầu như không muốn được open()ing và close()ing tập tin cứ sau vài giây. đó là lý do tại sao chúng ta có fsync()...
Craig Ringer
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.