-
Notifications
You must be signed in to change notification settings - Fork 18
Checksums #502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 1.8
Are you sure you want to change the base?
Checksums #502
Changes from 26 commits
2c04738
1e6c6f3
33e16c5
843a72e
1849998
4a194a7
d04a32f
5784c07
209b373
b8956be
849edb7
c081051
21ebbae
738c031
6eab894
74c8ce9
7ae2956
e12f217
e6f5229
f0e08d2
6cfc819
3fe9179
3dc63fe
84acf7a
325e3a0
9426483
f0fba76
787c955
9ba4eea
65f2401
1235157
c253991
e595752
85659cc
fdd6dd3
e34c6af
dc5cf62
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| ================= | ||
| Rolling Checksums | ||
| ================= | ||
|
|
||
| Problem | ||
| ======= | ||
| If a node crashed, and failed to write some tlog entries to disk, this is not detected by Arakoon. The node announces it's in sync up to the last entry in the tlogs, even if other nodes diverged while the node was offline. | ||
|
|
||
| Example | ||
| ------- | ||
| Consider this situation: | ||
|
|
||
| +----------------------------------+------------------------------------+------------------------------------+ | ||
| | node0 | node1 | node 2 | | ||
| +==================================+====================================+====================================+ | ||
| | 0:(Vm (node0,0.000000)) | 0:(Vm (node0,0.000000)) | 0:(Vm (node0,0.000000)) | | ||
| +----------------------------------+------------------------------------+------------------------------------+ | ||
| | 1:(Vc ([Set;"a";1;"...";],false) | 1:(Vc ([Set;"a";1;"...";],false) | 1:(Vc ([Set;"a";1;"...";],false) | | ||
| +----------------------------------+------------------------------------+------------------------------------+ | ||
| | 2:(Vc ([Set;"b";1;"...";],false) | *2:(Vc ([Set;"b";1;"...";],false)* | *2:(Vc ([Set;"b";1;"...";],false)* | | ||
| +----------------------------------+------------------------------------+------------------------------------+ | ||
| | 3:(Vc ([Set;"c";1;"...";],false) | *3:(Vc ([Set;"c";1;"...";],false)* | | | ||
| +----------------------------------+------------------------------------+------------------------------------+ | ||
|
|
||
| Node1 and node2 crashed, and the last tlog entries were lost. They are restarted, while node0 is still offline. When node0 comes back, this will result in the following situation: | ||
|
|
||
| +--------------------------------------+----------------------------------+----------------------------------+ | ||
| | node0 | node1 | node 2 | | ||
| +======================================+==================================+==================================+ | ||
| | 0:(Vm (node0,0.000000)) | 0:(Vm (node0,0.000000)) | 0:(Vm (node0,0.000000)) | | ||
| +--------------------------------------+----------------------------------+----------------------------------+ | ||
| | 1:(Vc ([Set;"a";1;"...";],false) | 1:(Vc ([Set;"a";1;"...";],false) | 1:(Vc ([Set;"a";1;"...";],false) | | ||
| +--------------------------------------+----------------------------------+----------------------------------+ | ||
| | **2:(Vc ([Set;"b";1;"...";],false)** | **2:(Vm (node1,0.000000))** | **2:(Vm (node1,0.000000))** | | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, node1 lost the 2 last tlog entries, and replaced them with others. This should not happen when you have an fsync between the writing of each entry, unless your mount options, file system, hardware are wanting. So is this whole set of changes some kind of runtime detection of bad configuration or a hardware lie-detector? |
||
| +--------------------------------------+----------------------------------+----------------------------------+ | ||
| | 3:(Vc ([Set;"d";1;"...";],false) | 3:(Vc ([Set;"d";1;"...";],false) | 3:(Vc ([Set;"d";1;"...";],false) | | ||
| +--------------------------------------+----------------------------------+----------------------------------+ | ||
|
|
||
| Checksums | ||
| ========= | ||
| This problem can be solved by using a rolling checksum, computed over all the entries in the tlogs. This checksum should be the same for all nodes. The checksum is part of the value that is synced with multi-paxos. | ||
|
|
||
| 1. The client sends a request to the master node. | ||
| 2. The master computes the rolling checksum, and makes a value of this checksum and the update commands. | ||
| 3. This value is sent to the slaves in an accept request. | ||
| 4. The slaves compute the rolling checksum, and compare it with the checksum in the value. | ||
| 5. If the checksums are equal, the tlogs are in sync, the value is written to the tlogs, and the algorithm proceeds as usual. | ||
| 6. If the checksums are different, something bad happened. The node halts, and the tlogs need to be inspected manually. | ||
|
|
||
| Remark | ||
| ------ | ||
| When several consecutive entries in the tlogs have the same number, it is only the last one that is agreed upon by multi-paxos, and thus only this entry is used in the computation of the checksum. | ||
|
|
||
| Tlog Specification | ||
| ================== | ||
| * Serial number (int64) | ||
| * Crc-32 checksum of Cmd (int32) | ||
| * Cmd | ||
| - Value | ||
| - Marker, optional (string option) | ||
|
|
||
| Older value format | ||
| ------------------ | ||
| * Update | ||
| - Update type (int32 between 1 and 16) | ||
| - Update details (depends on type) | ||
| * Synced (bool) | ||
|
|
||
| Old value format | ||
| ---------------- | ||
| * 0xff (int32) | ||
| * Value type (char 'c' or 'm') | ||
| * Value details (depends on type) | ||
|
|
||
| New value format | ||
| ---------------- | ||
| * 0x100 (int32) | ||
| * Checksum (int32 if crc-32 is used) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe you want to dabble into MICs and MACs. |
||
| * Value type (char 'c' or 'm') | ||
| * Value details (depends on type) | ||
|
|
||
| Checksums in store | ||
| ================== | ||
| The current tlog index and checksum are stored in the local store. When a node starts, they are compared with the values in the tlog. | ||
| Thus, after a collapse, the checksum of the last collapsed value is saved in the head database. Therefore the rolling tlog checksum is a continuation of the checksum in the head database. | ||
|
|
||
| Upgrade Path | ||
| ============ | ||
| To upgrade Arakoon to the new version, with a new tlog format, the nodes need to be restarted. A node that restarts after the upgrade can still read the old tlogs, and the checksums of these values will be set to None. Values with checksum None will never be valuated. | ||
|
|
||
| Nodes that need to do a catchup will do this as usual. The first received values will have checksum None, and are written to the tlogs in the old format, until all values from before the upgrade are synced. All values that are created after the upgrade will get a checksum. The checksum of the first value is a normal checksum (not depending on previous values), and all following checksums are rolling. | ||
|
|
||
| New and old nodes can not and will not communicate. If the nodes are restarted one by one, the old nodes will keep going as long as possible, while the new nodes can't make progress because they don't have a majority. When the critical point is reached, the new nodes will do a catchup and take over. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -69,7 +69,7 @@ class tcp_messaging | |
| my_addresses my_cookie (drop_it: drop_function) | ||
| max_buffer_size ~stop = | ||
|
|
||
| let _MAGIC = 0xB0BAFE7L in | ||
| let _MAGIC = 0x53E7965CL in | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should keep the old magic in here as a comment
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the new magic has no magic. :( |
||
| let _VERSION = 1 in | ||
| let my_ips, my_port = my_addresses in | ||
| let my_ip = List.hd my_ips in | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ open Log_extra | |
| open Tlogcommon | ||
|
|
||
| exception StoreAheadOfTlogs of (Int64.t * Sn.t) | ||
| exception StoreChecksumError of (Sn.t * Checksum.Crc32.t option * Checksum.Crc32.t option) | ||
| exception StoreCounterTooLow of string | ||
|
|
||
| let with_connection ~tls_ctx address f = match tls_ctx with | ||
|
|
@@ -86,9 +87,11 @@ let head_saved_epilogue hfn tlog_coll = | |
| S.make_store ~lcnum:default_lcnum | ||
| ~ncnum:default_ncnum ~read_only:true hfn >>= fun store -> | ||
| let hio = S.consensus_i store in | ||
| let hcso = S.get_checksum store in | ||
| S.close store ~flush:false ~sync:false >>= fun () -> | ||
| Logger.info_ "closed head" >>= fun () -> | ||
| begin | ||
| tlog_coll # set_previous_checksum hcso; | ||
| match hio with | ||
| | None -> Lwt.return () | ||
| | Some head_i -> | ||
|
|
@@ -98,14 +101,16 @@ let head_saved_epilogue hfn tlog_coll = | |
| end | ||
| end | ||
|
|
||
|
|
||
| let stop_fuse stop = | ||
| if !stop | ||
| then | ||
| Lwt.fail Canceled | ||
| else | ||
| Lwt.return () | ||
|
|
||
| let catchup_tlog (type s) ~tls_ctx ~stop other_configs ~cluster_id mr_name ((module S : Store.STORE with type t = s),store,tlog_coll) | ||
| let catchup_tlog (type s) ~tls_ctx ~stop other_configs ~cluster_id mr_name | ||
| ((module S : Store.STORE with type t = s), store, (tlog_coll:Tlogcollection.tlog_collection)) | ||
| = | ||
| let current_i = tlog_coll # get_last_i () in | ||
| Logger.info_f_ "catchup_tlog %s" (Sn.string_of current_i) >>= fun () -> | ||
|
|
@@ -114,8 +119,26 @@ let catchup_tlog (type s) ~tls_ctx ~stop other_configs ~cluster_id mr_name ((mo | |
| let mr_addresses = Node_cfg.client_addresses mr_cfg | ||
| and mr_name = Node_cfg.node_name mr_cfg in | ||
| Logger.info_f_ "getting last_entries from %s" mr_name >>= fun () -> | ||
| let head_saved_cb hfn = | ||
| Logger.info_f_ "head_saved_cb %s" hfn >>= fun () -> | ||
|
|
||
| let r_validate = ref false in | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this start with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would expect this sort of thing to be caught by a test .. is there a test we're missing?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The tests use |
||
| let validate () = | ||
| let _validate = !r_validate in | ||
| begin | ||
| r_validate := false; | ||
| _validate | ||
| end | ||
| in | ||
|
|
||
| let f_entry (i, value) = | ||
| let validate = validate () in | ||
| tlog_coll # log_value_explicit i value ~validate:validate false None >>= fun _ -> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can write this as |
||
| stop_fuse stop | ||
| in | ||
|
|
||
| let f_head ic = | ||
| tlog_coll # save_head ic >>= fun () -> | ||
| let hfn = tlog_coll # get_head_name () in | ||
| Logger.info_f_ "head_saved %s" hfn >>= fun () -> | ||
| head_saved_epilogue hfn tlog_coll >>= fun () -> | ||
| let when_closed () = | ||
| Logger.debug_ "when_closed" >>= fun () -> | ||
|
|
@@ -126,20 +149,20 @@ let catchup_tlog (type s) ~tls_ctx ~stop other_configs ~cluster_id mr_name ((mo | |
| Lwt.return () | ||
| in | ||
|
|
||
| let f_file name length ic = | ||
| let validate = validate () in | ||
| tlog_coll # save_tlog_file ~validate:validate name length ic | ||
| in | ||
|
|
||
| let copy_tlog connection = | ||
| make_remote_nodestream cluster_id connection >>= fun (client:nodestream) -> | ||
| let f (i,value) = | ||
| tlog_coll # log_value_explicit i value false None >>= fun _ -> | ||
| stop_fuse stop | ||
| in | ||
|
|
||
| client # iterate current_i f tlog_coll ~head_saved_cb | ||
| client # iterate current_i ~f_entry ~f_head ~f_file | ||
| in | ||
|
|
||
| Lwt.catch | ||
| (fun () -> | ||
| _with_client_connection ~tls_ctx mr_addresses copy_tlog >>= fun () -> | ||
| Logger.info_f_ "catchup_tlog completed" | ||
| _with_client_connection ~tls_ctx mr_addresses copy_tlog >>= fun () -> | ||
| Logger.info_f_ "catchup_tlog completed" | ||
| ) | ||
| (fun exn -> Logger.warning_ ~exn "catchup_tlog failed") | ||
| >>= fun () -> | ||
|
|
@@ -297,9 +320,28 @@ let verify_n_catchup_store (type s) ~stop me ?(apply_last_tlog_value=false) ((mo | |
| (Sn.string_of too_far_i) (Sn.string_of current_i) (io_s si_o) >>= fun () -> | ||
| match too_far_i, si_o with | ||
| | i, None when i <= 0L -> Lwt.return () | ||
| | i, Some j when i = j -> Lwt.return () | ||
| | i, Some j when i = j -> | ||
| begin | ||
| let store_cs = S.get_checksum store in | ||
| let tlog_cs = tlog_coll # get_previous_checksum (Sn.succ i) in | ||
| if store_cs <> tlog_cs | ||
| then Lwt.fail (StoreChecksumError (i, store_cs, tlog_cs)) | ||
| else Lwt.return () | ||
| end | ||
| | i, Some j when i > j -> | ||
| catchup_store ~stop me ((module S),store,tlog_coll) too_far_i | ||
| let entry = ref None in | ||
| let check e = Lwt.return (entry := Some e) in | ||
| tlog_coll # iterate j (Sn.succ j) check >>= fun () -> | ||
| let store_cs = S.get_checksum store in | ||
| let tlog_cs = | ||
| match !entry with | ||
| | None -> None | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this case should never happen, right? |
||
| | Some e -> Value.checksum_of (Entry.v_of e) in | ||
| if store_cs <> tlog_cs | ||
| then | ||
| Lwt.fail (StoreChecksumError (j, store_cs, tlog_cs)) | ||
| else | ||
| catchup_store ~stop me ((module S),store,tlog_coll) too_far_i | ||
| | _, None -> | ||
| catchup_store ~stop me ((module S),store,tlog_coll) too_far_i | ||
| | _,_ -> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should mention here that usually it should not be possible for this to happen. Usually means: fsync set to true (which is the default) and none of the layers below (file system, hardware) lie about fsync behaviour.