Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2c04738
Prepared implementation of rolling checksums
314eter Aug 5, 2014
1e6c6f3
Checksum validation
314eter Aug 7, 2014
33e16c5
Removed commented code
314eter Aug 7, 2014
843a72e
Changed nodenames to make tests independent
314eter Aug 7, 2014
1849998
Removed Uuused variable
314eter Aug 7, 2014
4a194a7
Raise ChecksumError
314eter Aug 7, 2014
d04a32f
Compatibility with old format (no checksums)
314eter Aug 7, 2014
5784c07
Use old tlog format for values with no checksums
314eter Aug 7, 2014
209b373
Fixed TODO: _previous_i_entry = _previous_entry at start
314eter Aug 8, 2014
b8956be
Moved checksum validation to log_value_explicit
314eter Aug 8, 2014
849edb7
Added OUnit test
314eter Aug 8, 2014
c081051
New system test and better error handling
314eter Aug 11, 2014
21ebbae
Better error msg in test
314eter Aug 11, 2014
738c031
Design document
314eter Aug 11, 2014
6eab894
collapser_test leaves head.db in root dir
314eter Aug 11, 2014
74c8ce9
Fixed wrong choice of previous_i_entry
314eter Aug 12, 2014
7ae2956
Removed options from Checksum module
314eter Aug 12, 2014
e12f217
Fixed some comments from pull request.
314eter Aug 13, 2014
e6f5229
New magic and upgrade path
314eter Aug 13, 2014
f0e08d2
Merge remote-tracking branch 'upstream/1.7' into checksum
314eter Aug 13, 2014
6cfc819
Disable checksum validation during catchup
314eter Aug 14, 2014
3fe9179
Save checksum in store, validation during catchup
314eter Aug 18, 2014
3dc63fe
Validate checksum in store
314eter Aug 19, 2014
84acf7a
Merge remote-tracking branch 'upstream/1.8' into checksum
314eter Aug 19, 2014
325e3a0
Fix merge with 1.8
314eter Aug 19, 2014
9426483
Updated design document
314eter Aug 19, 2014
f0fba76
Moved store validation to Store module
314eter Aug 20, 2014
787c955
Replaced LAST_ENTRIES and LAST_ENTRIES2 with LAST_ENTRIES3
314eter Aug 20, 2014
9ba4eea
Ensure i and checksum are updated simultaneously in store
314eter Aug 20, 2014
65f2401
Fixed some comments on pull request
314eter Aug 21, 2014
1235157
Add timeout to test power_failure
314eter Aug 21, 2014
c253991
Remove duplicate test_large_catchup_while_running
314eter Aug 21, 2014
e595752
Fix set_previous_checksum
314eter Aug 21, 2014
85659cc
Updated design document
314eter Aug 21, 2014
fdd6dd3
Validate checksums during catchup_store
314eter Aug 21, 2014
e34c6af
Use SSE4.2 in update_crc32c
314eter Aug 22, 2014
dc5cf62
Restored buildInSandbox.sh
314eter Sep 4, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions doc/design/rolling-checksums.rst
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.

Copy link
Copy Markdown
Contributor

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.


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))** |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.
4 changes: 2 additions & 2 deletions src/client/arakoon_client.mli
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class type client = object

method nop : unit -> unit Lwt.t
(** [nop ()] is a paxos no-operation.
*)
*)
method confirm: key -> value -> unit Lwt.t
(** [confirm key value] does nothing if this value was already
associated to the key, otherwise, it behaves as [set key value]
Expand Down Expand Up @@ -118,7 +118,7 @@ class type client = object
[replace key wanted] assigns the wanted value to the key,
and returns the previous assignment (if any) for that key.
If wanted is None, the binding is deleted.
*)
*)

method ping: string -> string -> string Lwt.t

Expand Down
25 changes: 9 additions & 16 deletions src/client/remote_nodestream.ml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ let section = Logger.Section.main

class type nodestream = object
method iterate:
Sn.t -> (Sn.t * Value.t -> unit Lwt.t) ->
Tlogcollection.tlog_collection ->
head_saved_cb:(string -> unit Lwt.t) -> unit Lwt.t
Sn.t -> f_entry:(Sn.t * Value.t -> unit Lwt.t) ->
f_head:(Lwt_io.input_channel -> unit Lwt.t) ->
f_file:(string -> int64 -> Lwt_io.input_channel -> unit Lwt.t) -> unit Lwt.t

method collapse: int -> unit Lwt.t

Expand All @@ -50,18 +50,13 @@ class type nodestream = object
method drop_master: unit -> unit Lwt.t
end

class remote_nodestream ((ic,oc) as conn) =
(object
method iterate (i:Sn.t) (f: Sn.t * Value.t -> unit Lwt.t)
(tlog_coll: Tlogcollection.tlog_collection)
~head_saved_cb
=
class remote_nodestream ((ic,oc) as conn) = (object
method iterate i ~f_entry ~f_head ~f_file =
let outgoing buf =
command_to buf LAST_ENTRIES2;
Sn.sn_to buf i
in
let incoming ic =
let save_head () = tlog_coll # save_head ic in
let last_seen = ref None in
let rec loop_entries () =
Sn.input_sn ic >>= fun i2 ->
Expand All @@ -78,7 +73,7 @@ class remote_nodestream ((ic,oc) as conn) =
Llio.input_int32 ic >>= fun _chksum ->
Llio.input_string ic >>= fun entry ->
let value = Value.value_from (Llio.make_buffer entry 0) in
f (i2, value) >>= fun () ->
f_entry (i2, value) >>= fun () ->
loop_entries ()
end
end
Expand All @@ -94,10 +89,8 @@ class remote_nodestream ((ic,oc) as conn) =
end
| 2 ->
begin
Logger.info_f_ "save_head" >>= fun ()->
save_head () >>= fun () ->
let hf_name = tlog_coll # get_head_name () in
head_saved_cb hf_name >>= fun () ->
Logger.info_f_ "save_head" >>= fun () ->
f_head ic >>= fun () ->
loop_parts ()
end
| 3 ->
Expand All @@ -106,7 +99,7 @@ class remote_nodestream ((ic,oc) as conn) =
Llio.input_string ic >>= fun name ->
Llio.input_int64 ic >>= fun length ->
Logger.info_f_ "got %s (%Li bytes)" name length >>= fun () ->
tlog_coll # save_tlog_file name length ic >>= fun () ->
f_file name length ic >>= fun () ->
loop_parts ()
end
| x -> Llio.lwt_failfmt "don't know what %i means" x
Expand Down
6 changes: 3 additions & 3 deletions src/client/remote_nodestream.mli
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ open Ncfg

class type nodestream = object
method iterate:
Sn.t -> (Sn.t * Value.t -> unit Lwt.t) ->
Tlogcollection.tlog_collection ->
head_saved_cb:(string -> unit Lwt.t) -> unit Lwt.t
Sn.t -> f_entry:(Sn.t * Value.t -> unit Lwt.t) ->
f_head:(Lwt_io.input_channel -> unit Lwt.t) ->
f_file:(string -> int64 -> Lwt_io.input_channel -> unit Lwt.t) -> unit Lwt.t

method collapse: int -> unit Lwt.t

Expand Down
2 changes: 1 addition & 1 deletion src/main/replay_main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ let replay_tlogs tlog_dir tlf_dir db_name end_i =
| None ->
begin
Tlc2.get_last_tlog tlog_dir tlf_dir >>= fun (_new_c,fn) ->
Tlc2._validate_one fn "" ~check_marker:false >>= fun (last, _index) ->
Tlc2._validate_one fn "" ~check_marker:false >>= fun (last, _, _index) ->
let i =
match last with
| None -> Sn.start
Expand Down
2 changes: 1 addition & 1 deletion src/msg/tcp_messaging.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Expand Down
68 changes: 55 additions & 13 deletions src/node/catchup.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ->
Expand All @@ -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 () ->
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this start with ref true ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests use mem_tlogcollection currently.

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 _ ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can write this as tlog_coll # log_value_explicit i value ~validate false None >>= fun () ->
note the ~validate and >>= fun ()

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 () ->
Expand All @@ -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 () ->
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this case should never happen, right?
so you may want to throw an exception

| 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
| _,_ ->
Expand Down
17 changes: 9 additions & 8 deletions src/node/catchup_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ let _fill tlog_coll n =
let k = Printf.sprintf "key%i" i
and v = Printf.sprintf "value%i" i in
let u = Update.Set (k,v) in
let value = Value.create_client_value [u] sync in
tlog_coll # log_value (Sn.of_int i) value >>= fun () ->
let sni = Sn.of_int i in
let value = Value.create_client_value tlog_coll sni [u] sync in
tlog_coll # log_value sni value >>= fun () ->
_loop (i+1)
end
in
Expand All @@ -58,11 +59,11 @@ let _fill2 tlog_coll n =
in
let u = Update.Set(k,v) in
let u2 = Update.Set(k2,v2) in
let value = Value.create_client_value [u] sync in
let value2 = Value.create_client_value [u2] sync in
let sni = Sn.of_int i in
tlog_coll # log_value sni value >>= fun () ->
tlog_coll # log_value sni value2 >>= fun () ->
let value = Value.create_client_value tlog_coll sni [u] sync in
let value2 = Value.create_client_value tlog_coll sni [u2] sync in
tlog_coll # log_value sni value >>= fun () ->
tlog_coll # log_value sni value2 >>= fun () ->
_loop (i+1)
end
in
Expand All @@ -84,9 +85,9 @@ let _fill3 tlog_coll n =
let u = Update.Set(k,v) in
let u2 = Update.Set(k2,v2) in
let u3 = Update.Sequence [Update.Set(k3,v3); Update.Assert_exists("nonExistingKey")] in
let value = Value.create_client_value [u; u3] sync in
let value2 = Value.create_client_value [u2; u3] sync in
let sni = Sn.of_int i in
let value = Value.create_client_value tlog_coll sni [u; u3] sync in
let value2 = Value.create_client_value tlog_coll sni [u2; u3] sync in
tlog_coll # log_value sni value >>= fun () ->
tlog_coll # log_value sni value2 >>= fun () ->
_loop (i+1)
Expand Down
Loading