This document discusses Libon's migration of contact data from an SQL database to Cassandra. It began with billions of contact records stored relationally in Oracle. Performance became unpredictable at scale. Tuning Oracle helped but new challenges like high availability and multi-datacenter support remained. The migration strategy involved writing to both databases, migrating old data, and switching fully to Cassandra while preserving no downtime and safe rollback. Business code refactoring maintained existing tests by modifying services and repositories to work with the new Cassandra data model.
2. Who are we ?
Brice Dutheil
Mockito
Java Track Lead @ Devoxx France
Independant contractor @ Libon
(Orange-Vallée)
DuyHai Doan
Achilles
Cassandra Technical Advocate
Former Java Developer @ Libon
2
#CassandraSummit @doanduyhai @BriceDutheil
3. Agenda
• Libon context
• Migration strategy
• Business code migration
• Data Modeling
• Take Away
3
#CassandraSummit @doanduyhai @BriceDutheil
9. Contact Matching
Libon User Friend
Accept link
9
#CassandraSummit @doanduyhai @BriceDutheil
10. Project Context
• Application grew over the years
10
#CassandraSummit @doanduyhai @BriceDutheil
11. Project Context
• Application grew over the years
• Already using Cassandra to handle events
• messaging / file sharing / SMS / notifications
• Cassandra R/W latencies ≈ 0,4 ms
• server response time under 10 ms
11
#CassandraSummit @doanduyhai @BriceDutheil
13. Project Context
• About contacts …
• stored as relational model in RDBMS (Oracle)
13
#CassandraSummit @doanduyhai @BriceDutheil
14. Project Context
• About contacts …
• stored as relational model in RDBMS (Oracle)
• 1 user ≈ 300 contacts
14
#CassandraSummit @doanduyhai @BriceDutheil
15. Project Context
• About contacts …
• stored as relational model in RDBMS (Oracle)
• 1 user ≈ 300 contacts
• with millions users ☞ billions of contacts to handle
15
#CassandraSummit @doanduyhai @BriceDutheil
16. Project Context
• About contacts …
• stored as relational model in RDBMS (Oracle)
• 1 user ≈ 300 contacts
• with millions users ☞ billions of contacts to handle
• query latency unpredictable
16
#CassandraSummit @doanduyhai @BriceDutheil
18. Fixing the problem
• Tune the RDBMS
18
#CassandraSummit @doanduyhai @BriceDutheil
19. Fixing the problem
• Tune the RDBMS
• indices
19
#CassandraSummit @doanduyhai @BriceDutheil
20. Fixing the problem
• Tune the RDBMS
• indices
• partitioning
20
#CassandraSummit @doanduyhai @BriceDutheil
21. Fixing the problem
• Tune the RDBMS
• indices
• partitioning
• less joins, simplified relational model
21
#CassandraSummit @doanduyhai @BriceDutheil
22. Fixing the problem
• Tune the RDBMS
• indices
• partitioning
• less joins, simplified relational model
• hardware capacity increased
22
#CassandraSummit @doanduyhai @BriceDutheil
23. Fixing the problem
• Tune the RDBMS
• indices
• partitioning
• less joins, simplified relational model
• hardware capacity increased
That worked
23
#CassandraSummit @doanduyhai @BriceDutheil
24. Fixing the problem
• Tune the RDBMS
• indices
• partitioning
• less joins, simplified relational model
• hardware capacity increased
That worked
but …
24
#CassandraSummit @doanduyhai @BriceDutheil
26. Next Challenges
• High Availability (DB failure, site failure …)
26
#CassandraSummit @doanduyhai @BriceDutheil
27. Next Challenges
• High Availability (DB failure, site failure …)
• Predictable performance at scale
27
#CassandraSummit @doanduyhai @BriceDutheil
28. Next Challenges
• High Availability (DB failure, site failure …)
• Predictable performance at scale
• Going to multi data-centers
28
#CassandraSummit @doanduyhai @BriceDutheil
29. Going for Cassandra
• Denormalize (if possible …)
29
#CassandraSummit @doanduyhai @BriceDutheil
30. Going for Cassandra
• Denormalize (if possible …)
• Know your business ☞ know your queries
30
#CassandraSummit @doanduyhai @BriceDutheil
31. Going for Cassandra
• Denormalize (if possible …)
• Know your business ☞ know your queries
• Linear scaling out
31
#CassandraSummit @doanduyhai @BriceDutheil
32. Going for Cassandra
• Denormalize (if possible …)
• Know your business ☞ know your queries
• Linear scaling out
• Consistent performance
32
#CassandraSummit @doanduyhai @BriceDutheil
39. Strategy
• 3 phases
• Write contacts to both data stores
39
#CassandraSummit @doanduyhai @BriceDutheil
40. Strategy
• 3 phases
• Write contacts to both data stores
• Old contacts migration
40
#CassandraSummit @doanduyhai @BriceDutheil
41. Strategy
• 3 phases
• Write contacts to both data stores
• Old contacts migration
• Switch to Cassandra …
• … and deprecate SQL
41
#CassandraSummit @doanduyhai @BriceDutheil
42. Migration Phase 1
Back end server
·
·
·
SQSLQ L SQL
C*
C*
C*C*
C*
Write
contactUUID
42
contacId(long) + contactUUID
contactId … contactUUID
129363
123e4567-
e89b-12d3…
834849
#CassandraSummit @doanduyhai @BriceDutheil
43. Migration Phase 1
Back end server
·
·
·
SQSLQ L SQL
C*
C*
C*C*
C*
Read
43
#CassandraSummit @doanduyhai @BriceDutheil
44. Migration Phase 2
• On live production, migrate old contacts
SQSLQ L SQL
C*
C*
C*C*
C*
For each batch of users
SELECT * FROM contacts
WHERE user_id = …
AND contact_uuid IS NULL
44
Old contacts created
before phase 1
#CassandraSummit @doanduyhai @BriceDutheil
45. Migration Phase 2
• On live production, migrate old contacts
SQSLQ L SQL
C*
Logged batches of
INSERT INTO contacts(..)
VALUES(…)
USING TIMESTAMP
now() - 1 week
C*
C*C*
C*
For each batch of users
SELECT * FROM contacts
WHERE user_id = …
AND contact_uuid IS NULL
45
Old contacts created
before phase 1
#CassandraSummit @doanduyhai @BriceDutheil
47. Migration Phase 2
• During data migration …
47
#CassandraSummit @doanduyhai @BriceDutheil
48. Migration Phase 2
• During data migration …
• … concurrent writes from the migration batch …
48
#CassandraSummit @doanduyhai @BriceDutheil
49. Migration Phase 2
• During data migration …
• … concurrent writes from the migration batch …
• … and updates from production for the same contact
49
#CassandraSummit @doanduyhai @BriceDutheil
50. Migration Phase 2
Update from production
Insert from batch
(to the past)
contact_uuid
name (now -1 week)
…
name (now)
…
Johny …
Johnny …
50
#CassandraSummit @doanduyhai @BriceDutheil
51. Migration Phase 2
Future reads pick the most up-to-date value
contact_uuid
name (now -1 week)
…
name (now)
…
Johny …
Johnny …
51
#CassandraSummit @doanduyhai @BriceDutheil
52. Migration Phase 2
"Write to the Past…
to save the Future"
Libon – 2014/10/08
52
#CassandraSummit @doanduyhai @BriceDutheil
53. Migration Phase 3
Back end server
·
·
·
❌
SQSLQ L SQL
C*
C*
C*C*
C*
Write
53
#CassandraSummit @doanduyhai @BriceDutheil
55. Code Inventory
• Written for RDBMS
55
#CassandraSummit @doanduyhai @BriceDutheil
56. Code Inventory
• Written for RDBMS
• Lots of joins (no surprise)
56
#CassandraSummit @doanduyhai @BriceDutheil
57. Code Inventory
• Written for RDBMS
• Lots of joins (no surprise)
• Designed around transactions
57
#CassandraSummit @doanduyhai @BriceDutheil
58. Code Inventory
• Written for RDBMS
• Lots of joins (no surprise)
• Designed around transactions
• Spring @Transactional everywhere
58
#CassandraSummit @doanduyhai @BriceDutheil
59. Code Inventory cont.
• Entities go through Services & Repositories
Serv ices
ContactEntity
Repositories
59
#CassandraSummit @doanduyhai @BriceDutheil
62. Which options ?
• Throw existing code …
• … and re-design from scratch for Cassandra
62
#CassandraSummit @doanduyhai @BriceDutheil
63. Which options ?
• Throw existing code …
• … and re-design from scratch for Cassandra
No way !
63
#CassandraSummit @doanduyhai @BriceDutheil
64. Code Quality
• Existing business code has…
• … ≈ 3500 unit tests
64
#CassandraSummit @doanduyhai @BriceDutheil
65. Code Quality
• Existing business code has…
• … ≈ 3500 unit tests
• and ≈600+ integration tests
65
#CassandraSummit @doanduyhai @BriceDutheil
66. Code Quality
• We are TDD aficionados …
66
#CassandraSummit @doanduyhai @BriceDutheil
67. Code Quality
• We are TDD aficionados …
• … and we love our code coverage
67
#CassandraSummit @doanduyhai @BriceDutheil
68. Code Quality
"The code coverage
is one of your most
valuable technical asset"
Libon – since beginning
68
#CassandraSummit @doanduyhai @BriceDutheil
69. Refactoring Strategy
Services
ContactMatchingServicContactSyncContactService
e
Repositories
ContactEntity
n
1
n
n
69
#CassandraSummit @doanduyhai @BriceDutheil
70. Refactoring Strategy
Services
Repositories
ContactMatchingServicContactServicee
Proxy
ContactNoSQLEntity
ContactSync
ContactEntity
n
1
n
n
70
#CassandraSummit @doanduyhai @BriceDutheil
71. Refactoring Strategy
Services
Repositories
ContactMatchingServicContactServicee
Proxy
ContactNoSQLEntity
ContactSync
ContactEntity
n
1
n
n
Denorm1Denorm2
…
DenormN
71
#CassandraSummit @doanduyhai @BriceDutheil
77. Outcome
• 5 months of 2 men work
77
#CassandraSummit @doanduyhai @BriceDutheil
78. Outcome
• 5 months of 2 men work
• Many iterations to fix bugs (thanks to IT)
78
#CassandraSummit @doanduyhai @BriceDutheil
79. Outcome
• 5 months of 2 men work
• Many iterations to fix bugs (thanks to IT)
• Lots of performance benchmarks using Gatling
79
#CassandraSummit @doanduyhai @BriceDutheil
81. Outcome
• 5 months of 2 men work
• Many iterations to fix bugs (thanks to IT)
• Lots of performance benchmarks using Gatling
☞ data model & code validation
81
#CassandraSummit @doanduyhai @BriceDutheil
82. Outcome
• 5 months of 2 men work
• Many iterations to fix bugs (thanks to IT)
• Lots of performance benchmarks using Gatling
☞ data model & code validation
• … we are almost there for production
82
#CassandraSummit @doanduyhai @BriceDutheil
84. Denormalization, the good
• Support fast reads
• 1 read ≈ 1 SELECT
• Worthy because mostly read, few updates
84
#CassandraSummit @doanduyhai @BriceDutheil
85. Denormalization, the bad
• Updating mutable data can be nightmare
• Data model bound by existing client-facing API
• Update paths very error-prone without tests
85
#CassandraSummit @doanduyhai @BriceDutheil
86. Data model in detail
Contacts_by_identifiers
Contacts_by_id
Contacts_in_profiles
Contacts_by_modification_date
Contacts_linked_user
Contacts_by_firstname_lastname
86
#CassandraSummit @doanduyhai @BriceDutheil
87. Data model in detail
Contacts_by_identifiers
user_id always component
of partition key
Contacts_by_id
Contacts_in_profiles
Contacts_by_modification_date
Contacts_linked_user
Contacts_by_firstname_lastname
87
#CassandraSummit @doanduyhai @BriceDutheil
88. Scalable design
C
n3
G
n7
88
A
n1
B
n2
D
n4
E
n5
F
n6
H
n8
user_id1
user_id2
user_id3
user_id4
user_id5
#CassandraSummit @doanduyhai @BriceDutheil
89. Scalable design
C
n3
user_id5
user_id2 user_id1
G
n7
89
A
n1
B
n2
D
n4
E
n5
F
n6
H
n8
user_id3
user_id4
#CassandraSummit @doanduyhai @BriceDutheil
90. Bloom filters in action
• For some tables, partition key = (user_id, contact_id)
☞ fast look-up, leverages Bloom filters
☞ touches 1 SSTable most of the time
90
#CassandraSummit @doanduyhai @BriceDutheil
91. Data model in detail
Contacts_by_identifiers
Contacts_by_id
Contacts_in_profiles
Contacts_by_modification_date
Contacts_linked_user
Wide partition
Bucketed
Contacts_by_firstname_lastname
91
#CassandraSummit @doanduyhai @BriceDutheil
92. A "queue" story
• contacts_by_modification_date
• queue-like pattern
92
#CassandraSummit @doanduyhai @BriceDutheil
93. A "queue" story
• contacts_by_modification_date
• queue-like pattern
☞ buckets to the rescue
date11
date12 …
… …
…
…
date35
date12 …
…
… …
…
…
93
user_id:2014-11
user_id:2014-12
…
date34
date47
#CassandraSummit @doanduyhai @BriceDutheil
94. Data model summary
• 7 tables for denormalization
94
#CassandraSummit @doanduyhai @BriceDutheil
95. Data model summary
• 7 tables for denormalization
• Normalize some tables because rare access
95
#CassandraSummit @doanduyhai @BriceDutheil
96. Data model summary
• 7 tables for denormalization
• Normalize some tables because rare access
• Read-before write in most update scenarios
96
#CassandraSummit @doanduyhai @BriceDutheil
97. Notes on contact_id
• In SQL, auto-generated long using sequence
• In Cassandra, auto-generated timeuuid
97
#CassandraSummit @doanduyhai @BriceDutheil
98. Notes on contact_id
• How to store both types ?
98
#CassandraSummit @doanduyhai @BriceDutheil
99. Notes on contact_id
• How to store both types ?
• As text ? ☞ easy solution …
99
#CassandraSummit @doanduyhai @BriceDutheil
100. Notes on contact_id
• How to store both types ?
• As text ? ☞ easy solution …
• … but waste of space !
• because encoded as UTF-8 or ASCII in Cassandra
100
#CassandraSummit @doanduyhai @BriceDutheil
101. Notes on contact_id
• Long ☞ 8 bytes
• Long as text(UTF-8: 1 byte) ☞ "digits count" bytes
101
#CassandraSummit @doanduyhai @BriceDutheil
103. Notes on contact_id
• 20 bytes wasted per contact uuid
103
#CassandraSummit @doanduyhai @BriceDutheil
104. Notes on contact_id
• 20 bytes wasted per contact uuid
• × 7 denormalizations = 140 bytes per contact uuid
104
#CassandraSummit @doanduyhai @BriceDutheil
105. Notes on contact_id
• 20 bytes wasted per contact uuid
• × 7 denormalizations = 140 bytes per contact uuid
• × 109 contacts = 140 GB wasted
not even counting replication factor …
105
#CassandraSummit @doanduyhai @BriceDutheil
106. Notes on contact_id
• ☞ just save contact id as byte[ ]
106
#CassandraSummit @doanduyhai @BriceDutheil
107. Notes on contact_id
• ☞ just save contact id as byte[ ]
• Achilles @TypeTransformer for automatic conversion
(see later)
107
#CassandraSummit @doanduyhai @BriceDutheil
108. Notes on contact_id
• ☞ just save contact id as byte[ ]
• Achilles @TypeTransformer for automatic conversion
(see later)
• Use blobAsBigInt( ) or blobAsUUID( ) to view data
108
#CassandraSummit @doanduyhai @BriceDutheil
109. Achilles
• Advanced "object mapper"
• Fluent API
• Tons of features
• TDD friendly
109
#CassandraSummit @doanduyhai @BriceDutheil
110. Achilles
• Dirty checking, why is it important ?
110
#CassandraSummit @doanduyhai @BriceDutheil
111. Achilles
• Dirty checking, why is it important ?
• 1 contact ≈ 8 mutable fields
111
#CassandraSummit @doanduyhai @BriceDutheil
112. Achilles
• Dirty checking, why is it important ?
• 1 contact ≈ 8 mutable fields
• × 7 denormalizations = 56 update combinations …
112
#CassandraSummit @doanduyhai @BriceDutheil
113. Achilles
• Dirty checking, why is it important ?
• 1 contact ≈ 8 mutable fields
• × 7 denormalizations = 56 update combinations …
• and not even counting multiple fields updates …
113
#CassandraSummit @doanduyhai @BriceDutheil
114. Achilles
• Are you going to manually generate 56+ prepared
statements for all possible updates ?
114
#CassandraSummit @doanduyhai @BriceDutheil
115. Achilles
• Are you going to manually generate 56+ prepared
statements for all possible updates ?
• Or just use dynamic plain string statements and get
some perf penalty ?
115
#CassandraSummit @doanduyhai @BriceDutheil
129. Achilles
public interface Codec<FROM, TO> {
Class<FROM> sourceType();
Class<TO> targetType();
TO encode(FROM fromJava)
FROM decode(TO fromCassandra);
}
129
#CassandraSummit @doanduyhai @BriceDutheil
130. Achilles
• Dynamic logging in action
2014-12-01 14:25:20,554 Bound statement : [INSERT INTO
contacts.contacts_by_modification_date(user_id,month_bucket,modification_date,...) VALUES
(:user_id,:month_bucket,:modification_date,...) USING TTL :ttl;] with CONSISTENCY LEVEL [LOCAL_QUORUM]
2014-12-01 14:25:20,554 bound values : [222130151, 2014-12, e13d0d50-7965-11e4-af38-90b11c2549e0, ...]
2014-12-01 14:25:20,701 Bound statement : [SELECT birthday,middlename,avatar_size,... FROM
contacts.contacts_by_modification_date WHERE user_id=:user_id AND month_bucket=:month_bucket AND
(modification_date)>=(:modification_date) ORDER BY modification_date ASC;] with CONSISTENCY LEVEL
[LOCAL_QUORUM]
2014-12-01 14:25:20,701 bound values : [222130151, 2014-10, be6bc010-6109-11e4-b385-000038377ead]
130
#CassandraSummit @doanduyhai @BriceDutheil
131. Achilles
• Dynamic logging
• runtime activation
• no need to recompile/re-deploy
• save us hours of debugging
• TRACE log level ☞ query tracing
131
#CassandraSummit @doanduyhai @BriceDutheil
133. Conditions for success
• Data modeling is crucial
133
#CassandraSummit @doanduyhai @BriceDutheil
134. Conditions for success
• Data modeling is crucial
• Double-run strategy & timestamp trick FTW
134
#CassandraSummit @doanduyhai @BriceDutheil
135. Conditions for success
• Data modeling is crucial
• Double-run strategy & timestamp trick FTW
• Data type conversion can be tricky
135
#CassandraSummit @doanduyhai @BriceDutheil
136. Conditions for success
• Data modeling is crucial
• Double-run strategy & timestamp trick FTW
• Data type conversion can be tricky
• Benchmark !
136
#CassandraSummit @doanduyhai @BriceDutheil
137. Conditions for success
• Data modeling is crucial
• Double-run strategy & timestamp trick FTW
• Data type conversion can be tricky
• Benchmark !
• Mindset shifts for the team
137
#CassandraSummit @doanduyhai @BriceDutheil