Cassandra: How Not to Die if You Only Know Oracle

Hello, Habr.

My name is Misha Butrimov, and I would like to share a bit about Cassandra. My story will be useful for those who have never encountered NoSQL databases — it has many unique implementation features and pitfalls you need to be aware of. If you've only seen Oracle or any other relational database, these insights will save your life.

What Makes Cassandra Great? It’s a NoSQL database designed without a single point of failure that scales well. If you need to add a couple of terabytes for some database, you simply add nodes to the ring. Want to expand it to another data center? Add nodes to the cluster. Need to increase the processed RPS? Add nodes to the cluster. It works the other way around as well.

Cassandra: How Not to Die if You Only Know Oracle

What else is it good at? Processing a high volume of requests. But how many is a lot? 10, 20, 30, 40 thousand requests per second is just a start. 100 thousand requests per second on write is also manageable. There are companies claiming to handle 2 million requests per second. We might need to believe them.

Essentially, Cassandra has one significant difference from relational data — it doesn't resemble them at all. This is very important to remember.

Not everything that looks the same works the same.

Once, a colleague came to me and asked, "Here's SQL, the Cassandra query language. It has select statements, it has where, it has and. I'm typing the letters, and it’s not working. Why?" If you treat Cassandra like a relational database, that’s a perfect way to end up in a cruel dead end. And I’m not advocating that; it's prohibited in Russia. You simply end up designing something incorrectly.

For example, a client comes to us and says, "Let's build a database for series or a database for a recipe directory. We’ll have dishes with ingredients or a list of series and their actors." We joyfully respond, "Sure!" It's just two bytes to send, a couple of tables, and everything will be up and running very quickly and reliably. And everything is great until clients come back and say housewives are also aiming to solve the reverse problem: they have a list of ingredients and want to know which dish they want to cook. You're dead.

This is because Cassandra is a hybrid database: it is both a key-value store and it stores data in wide columns. If we were to describe this in Java or Kotlin, it would look like this:

Map<RowKey, SortedMap>

So, it's a map, inside which there is also a sorted map. The first key to this map is the Row key or Partition key — the partitioning key. The second key, which is the key to the already sorted map, is the Clustering key.

To illustrate the distribution of the database, let's draw three nodes. Now we need to understand how to distribute the data across the nodes. Because if we were to shove everything into one (there can be a thousand, two thousand, five — as many as you want), that wouldn’t really be about distribution. Therefore, we need a mathematical function that will return a number. Just a number, a long integer, which will fall into some range. And one node will be responsible for one range, the second for another, and the n-th for the n-th.

Cassandra: How Not to Die if You Only Know Oracle

This number is obtained using a hash function, which is applied to what we call the Partition key. This is the column specified in the Primary key directive, and it is the column that will be the first and primary key of the map. It determines which data will go to which node. A table in Cassandra is created with almost the same syntax as in SQL:

CREATE TABLE users (
	user_id uuid,
	name text,
	year int,
	salary float,
	PRIMARY KEY(user_id)

)

The primary key in this case consists of one column, which is also the partitioning key.

How will our users be distributed? Some will go to one node, some to another, and some to the third. It results in a regular hash table, also known as a map, which in Python is a dictionary, a simple key-value structure from which we can read all values and both read and write by key.

Cassandra: How Not to Die if You Only Know Oracle

Select: when allow filtering turns into a full scan, or how not to do it

Let's write some select statement: select * from users where userid = It works somewhat like in Oracle: we write select, specify the conditions, and everything works, users are fetched. But if we choose, for example, a user with a specific birth year, Cassandra complains that it cannot execute the query. This is because it knows nothing about how our data on birth years is distributed — it only has one column specified as a key. Then it says: 'Alright, I can still execute this query. Add allow filtering.' We add the directive, everything works. And at that moment, something terrible happens.

When we run on test data, everything is fine. But when you execute the query in production, where we have, for example, 4 million records, things don't go well. Because allow filtering is a directive that allows Cassandra to gather all the data from this table across all nodes, and only then filter it. data centers (if there are many in this cluster), and only after that will it filter. This is akin to a Full Scan, and it's unlikely that anyone would be thrilled about it.

If we only needed users by identifiers, that would suit us. But sometimes we need to write other queries and impose other constraints on the selection. So we remember: this is all a map, which has a partitioning key, but inside it is a sorted map.

And it also has a key, which we call the Clustering Key. This key consists of columns that we will choose, with which Cassandra understands how its data will be physically sorted and laid out on each node. That is, for a certain Partition key, the Clustering key will tell how exactly to insert the data into this tree, what place they will occupy.

It's really a tree; a comparator is simply invoked, to which we pass a certain set of columns in the form of an object, and it is also defined in the form of an enumeration of columns.

CREATE TABLE users_by_year_salary_id (
	user_id uuid,
	name text,
	year int,
	salary float,
	PRIMARY KEY((year), salary, user_id)

Note the Primary key directive; its first argument (in our case, the year) is always the Partition key. It can consist of one or more columns; that doesn't matter. If there are multiple columns, it needs to be enclosed in parentheses again so the language preprocessor understands that this refers to the Primary key, followed by all other columns as the Clustering key. They will be passed in the comparator in the order they appear. Thus, the first column is more significant, the second less so, and so on. As we write in data classes, for example, fields equals: we list the fields and indicate which are more significant and which are less. In Cassandra, this is, conditionally speaking, the fields of the data class to which the written equals will be applied.

We set the sorting, applying constraints.

It is essential to remember that the sorting order (descending, ascending, it doesn't matter) is defined at the same moment the key is created, and it cannot be changed later. It physically determines how the data will be sorted and how it will be stored. If we need to change the Clustering key or the sorting order, we will have to create a new table and transfer the data into it. This is not possible with an existing one.

Cassandra: How Not to Die if You Only Know Oracle

We populated our table with users and saw that they were arranged in a ring first by year of birth and then, within each node, by salary and user ID. Now we can select, applying constraints.

Our working one appears again. where, and, and we obtain users, and everything is fine again. However, if we attempt to use only part of the Clustering key, specifically the less significant part, Cassandra will immediately complain that it cannot find a place in our map where this object, which has null for these comparator fields, and this one that was just specified — where it lies. I will have to pull all the data from this node again and filter it. This is analogous to a Full Scan within a node, which is undesirable.

In any uncertain situation, create a new table.

If we want to be able to retrieve users by ID, age, or salary, what should we do? Nothing. Just use two tables. If we need to retrieve users in three different ways, there will be three tables. The days of saving space on disks are over. Disk space is the cheapest resource. It costs much less than response time, which can be detrimental to users. It is far more pleasant for users to receive something in a second rather than in ten minutes.

We trade off excess space taken up by denormalized data for the ability to scale well and operate reliably. After all, a cluster made up of three data centers, each with five nodes, can survive the complete loss of one data center while maintaining an acceptable level of data preservation (ensuring nothing is lost). Moreover, with two nodes remaining in each of the other two data centers, only then will problems start to arise. This is quite good redundancy and only costs a few extra SSDs and processors. Therefore, in order to use Cassandra, which is not SQL and has no relationships or foreign keys, you need to understand some simple rules.

We design everything from the query. The focus is not on the data, but on how the application will interact with that data. If it needs to retrieve different data in various ways or the same data in different ways, we must organize it in a way that is convenient for the application. Otherwise, we will fall into a Full Scan, and Cassandra will not provide us with any advantages.

Denormalizing data is the norm. We forget about normal forms; we no longer have relational databases. If we store something 100 times, it will take up space 100 times. It's still cheaper than delaying processes.

We choose partitioning keys so that they are evenly distributed. We do not want the hash of our keys to fall into a narrow range. Thus, the year of birth in the example above is a bad example. Rather, it is a good example if our users are evenly distributed by year of birth, and a poor one if we are talking about fifth-grade students, where partitioning will not work well.

The sorting is chosen only once at the Clustering Key creation stage. If it needs to be changed, we will have to reprocess our table with a different key.

And the most important thing: if we need to access the same data in 100 different ways, we will end up with 100 different tables.

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster