Migration of Cassandra to Kubernetes: features and solutions

Migration of Cassandra to Kubernetes: features and solutions

With Apache Cassandra as a database and the need to operate it within a Kubernetes-based infrastructure, we face this challenge regularly. In this article, we will share our insights on the necessary steps, criteria, and existing solutions (including an overview of operators) for migrating Cassandra to K8s.

“Who can manage a woman can also manage a state”

So, who is Cassandra? It is a distributed storage system designed to handle large volumes of data while ensuring high availability without a single point of failure. The project hardly needs an extensive introduction, so I will only outline the key features of Cassandra that will be relevant in the context of this article:

  • Cassandra is written in Java.
  • The Cassandra topology includes several levels:
    • Node — a single deployed instance of Cassandra;
    • Rack — a group of Cassandra instances that are combined by some criteria and located within one data center;
    • Datacenter — a collection of all groups of Cassandra instances located within one data center;
    • Cluster — a collection of all data centers.
  • Cassandra identifies nodes using IP addresses.
  • To speed up read and write operations, Cassandra stores some data in memory.

Now — to the potential migration to Kubernetes.

Check-list for migration

When it comes to migrating Cassandra to Kubernetes, we hope that managing it will become easier with the move. What will be required for this? What can help?

1. Data Storage

As previously mentioned, Cassandra stores some data in memory — in the Memtable. But there is also another part of the data that is saved to disk, — in the form of SSTable. This data is complemented by the entity Commit Log — records of all transactions that are also saved to disk.

Migration of Cassandra to Kubernetes: features and solutions
Schema of write transactions in Cassandra

In Kubernetes, we can use PersistentVolume for data storage. Thanks to well-established mechanisms, working with data in Kubernetes is becoming easier every year.

Migration of Cassandra to Kubernetes: features and solutions
We will allocate a separate PersistentVolume for each pod with Cassandra.

It's important to note that Cassandra by itself implies data replication, offering built-in mechanisms for this purpose. Therefore, if you are assembling a Cassandra cluster from a large number of nodes, there is no need to use distributed systems like Ceph or GlusterFS for data storage. In this case, it would be logical to store data on the node's disk using local persistent disks or mounting hostPath.

Another question is if you want to create a separate environment for each feature branch for developers. In this case, the correct approach would be to spin up a single Cassandra node and store data in distributed storage; i.e., the aforementioned Ceph and GlusterFS would become your option. Then developers will be confident that they won't lose test data even if one of the nodes in the Kubernetes cluster fails.

2. Monitoring

The practically undisputed choice for implementing monitoring in Kubernetes is Prometheus (we covered this in detail in the corresponding report). How is Cassandra doing with metric exporters for Prometheus? And, more importantly, with suitable dashboards for Grafana?

Migration of Cassandra to Kubernetes: features and solutions
An example of the visualization of graphs in Grafana for Cassandra

There are only two exporters: jmx_exporter and cassandra_exporter.

We chose the first one because:

  1. JMX Exporter is growing and evolving, while Cassandra Exporter has not received adequate community support. Cassandra Exporter still does not support most versions of Cassandra.
  2. It can be run as a javaagent by adding the flag -javaagent:\/cassandra-exporter.jar=--listen=:9180.
  3. There is an adequate dashboard, which is incompatible with Cassandra Exporter.

3. Choosing Kubernetes primitives

According to the above structure of the Cassandra cluster, let's try to translate everything described there into Kubernetes terminology:

  • Cassandra Node → Pod
  • Cassandra Rack → StatefulSet
  • Cassandra Datacenter → pool of StatefulSets
  • Cassandra Cluster → ???

It turns out that there is a need for some additional entity to manage the entire Cassandra cluster at once. But if something is missing, we can create it! In Kubernetes, there is a mechanism for defining custom resources for this purpose— Custom Resource Definitions.

Migration of Cassandra to Kubernetes: features and solutions
Declaration of additional resources for logs and alerts

But the Custom Resource itself means nothing on its own: it needs controller. You may need to enlist the help of a Kubernetes operator…

4. Identification of pods

As mentioned earlier, we agreed that one Cassandra node will equal one pod in Kubernetes. However, the IP addresses of the pods will differ every time. Node identification in Cassandra is based specifically on the IP address... This means that after each pod deletion, the Cassandra cluster will add a new node.

There is a way out, and not just one:

  1. We can track by host identifiers (UUIDs, which uniquely identify Cassandra instances) or by IP addresses and store everything in some structures/tables. This method has two main drawbacks:
    • The risk of a race condition if two nodes fail simultaneously. Once restored, the Cassandra nodes will simultaneously request an IP address from the table and compete for the same resource.
    • If a Cassandra node loses its data, we can no longer identify it.
  2. The second solution seems like a bit of a hack, but nonetheless: we can create a Service with ClusterIP for each Cassandra node. The issues with this implementation are:
    • If there are many nodes in the Cassandra cluster, we will have to create a very large number of Services.
    • The ClusterIP functionality is implemented via iptables. This can become a problem if there are many nodes in the Cassandra cluster (1000… or even 100?). Although balancing based on IPVS can solve this problem.
  3. The third solution is to use the node network for Cassandra nodes instead of a dedicated pod network by enabling the setting hostNetwork: true. This method imposes certain limitations:
    • On node replacement. It is required that the new node must have the same IP address as the previous one (this is practically impossible to achieve in clouds like AWS, GCP);
    • Using the cluster node network, we start competing for network resources. Consequently, deploying more than one Cassandra pod on a single cluster node will be problematic.

5. Backups

We want to keep a complete version of data from one Cassandra node on a schedule. Kubernetes provides a convenient option using CronJob, but here Cassandra itself throws a wrench into the works.

I would like to remind you that part of the data in Cassandra is stored in memory. To perform a complete backup, the data from memory (Memtables) needs to be transferred to disk (SSTables). At that moment, the Cassandra node stops accepting connections, becoming completely inactive in the cluster.

After that, a backup is taken (snapshot) and the schema is saved (keyspace). It turns out that a simple backup doesn't help us: we need to preserve the data identifiers that the Cassandra node was responsible for — these are special tokens.

Migration of Cassandra to Kubernetes: features and solutions
Token distribution for identifying which data is managed by Cassandra nodes

An example script for taking a Cassandra backup from Google in Kubernetes can be found at this link. The only thing the script does not account for is resetting the data on the node before taking the snapshot. This means the backup is not for the current state, but for a slightly earlier state. However, this helps keep the node operational, which seems very logical.

set -eu

if [[ -z "$1" ]]; then
  info "Please provide a keyspace"
  exit 1
fi

KEYSPACE="$1"

result=$(nodetool snapshot "${KEYSPACE}")

if [[ $? -ne 0 ]]; then
  echo "Error while making snapshot"
  exit 1
fi

timestamp=$(echo "$result" | awk '/Snapshot directory: / { print $3 }')

mkdir -p /tmp/backup

for path in $(find "/var/lib/cassandra/data/${KEYSPACE}" -name $timestamp); do
  table=$(echo "${path}" | awk -F "[/-]" '{print $7}')
  mkdir /tmp/backup/$table
  mv $path /tmp/backup/$table
done


tar -zcf /tmp/backup.tar.gz -C /tmp/backup .

nodetool clearsnapshot "${KEYSPACE}"

An example bash script for taking a backup from a single Cassandra node

Ready-made solutions for Cassandra in Kubernetes

What is currently used for deploying Cassandra in Kubernetes, and which of these fits the specified requirements best?

1. Solutions based on StatefulSets or Helm charts

Using the basic features of StatefulSets to run a Cassandra cluster is a good option. With a Helm chart and Go templates, a flexible interface can be provided to users for deploying Cassandra.

Usually, this works fine… until something unexpected happens — for example, a node failure. Standard Kubernetes tools simply cannot account for all the aforementioned nuances. Moreover, this approach is very limited in terms of how it can be extended for more complex use cases: node replacement, backup, recovery, monitoring, etc.

Representatives:

Both charts are equally good but are subject to the issues described above.

2. Solutions based on Kubernetes Operator

Such options are more interesting because they provide extensive cluster management capabilities. For designing a Cassandra operator, like any other database, a good pattern looks like Sidecar Controller CRD:

Migration of Cassandra to Kubernetes: features and solutions
Node management scheme in a well-designed Cassandra operator

Let's consider existing operators.

1. Cassandra Operator by Instaclustr

  • GitHub
  • Status: Alpha
  • License: Apache 2.0
  • Implemented in: Java

This is indeed a very promising and actively developing project from a company that offers managed Cassandra deployments. It uses a sidecar container that accepts commands via HTTP as described above. Written in Java, it sometimes lacks the advanced functionality of the client-go library. Additionally, the operator does not support different Racks within a single Datacenter.

However, the operator has advantages such as monitoring support, high-level cluster management using CRDs, and even documentation for backup procedures.

2. Navigator by Jetstack

  • GitHub
  • Status: Alpha
  • License: Apache 2.0
  • Implemented in: Golang

An operator designed for deploying Database-as-a-Service. Currently supports two databases: Elasticsearch and Cassandra. It includes interesting features like database access control via RBAC (which involves launching a separate navigator-apiserver). It's an intriguing project worth considering, although the last commit was made a year and a half ago, which clearly diminishes its potential.

3. Cassandra Operator by vgkowski

  • GitHub
  • Status: Alpha
  • License: Apache 2.0
  • Implemented in: Golang

It was not taken seriously since the last commit to the repository was over a year ago. The operator's development has been abandoned: the last supported Kubernetes version claimed is 1.9.

4. Cassandra Operator by Rook

  • GitHub
  • Status: Alpha
  • License: Apache 2.0
  • Implemented in: Golang

An operator that is not progressing as quickly as one would hope. It has a well-thought-out CRD structure for managing the cluster and addresses the node identification issue using Service with ClusterIP (the so-called 'hack')... but that's currently about it. Monitoring and backups are not available out of the box (by the way, we took care of monitoring ourselves). We handled it ourselves.). An interesting point is that this operator can also be used to deploy ScyllaDB.

NB: We used this operator with minor modifications in one of our projects. No issues were observed during its operation (~4 months of use).

5. CassKop by Orange

  • GitHub
  • Status: Alpha
  • License: Apache 2.0
  • Implemented in: Golang

The youngest operator on the list: the first commit was made on May 23, 2019. It already has a significant number of features from our list, which can be found in the project repository. The operator is built on the popular operator-sdk and supports monitoring out of the box. The main distinction from other operators is its use of CassKop plugin, implemented in Python and used for communication between Cassandra nodes.

Conclusions

The number of approaches and possible options for migrating Cassandra to Kubernetes speaks for itself: this topic is in demand.

At this stage, experimenting with any of the above is at your own risk: no developer guarantees 100% functionality of their solution in a production environment. However, many products already look promising enough to try using them in development environments.

I think in the future this woman on the ship will come in handy!

P.S.

Also read in our blog:

Source: habr.com

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