Rob Reid - Practical CockroachDB - Building Fault-Tolerant Distributed SQL Databases-Apress (2022)
Rob Reid - Practical CockroachDB - Building Fault-Tolerant Distributed SQL Databases-Apress (2022)
Rob Reid
Practical CockroachDB: Building Fault-Tolerant Distributed SQL Databases
Rob Reid
Liss, Hampshire, UK
Introduction�����������������������������������������������������������������������������������������������������������xvii
v
Table of Contents
CockroachDB Serverless/Dedicated������������������������������������������������������������������������������������������� 29
Creating a Cluster������������������������������������������������������������������������������������������������������������������ 29
Connecting to Your Cluster���������������������������������������������������������������������������������������������������� 29
Summary������������������������������������������������������������������������������������������������������������������������������������ 30
Chapter 3: Concepts����������������������������������������������������������������������������������������������� 31
Database Objects������������������������������������������������������������������������������������������������������������������������ 31
Data Types����������������������������������������������������������������������������������������������������������������������������������� 33
UUID��������������������������������������������������������������������������������������������������������������������������������������� 33
ARRAY������������������������������������������������������������������������������������������������������������������������������������ 34
BIT����������������������������������������������������������������������������������������������������������������������������������������� 37
BOOL�������������������������������������������������������������������������������������������������������������������������������������� 38
BYTES������������������������������������������������������������������������������������������������������������������������������������ 39
DATE�������������������������������������������������������������������������������������������������������������������������������������� 39
ENUM������������������������������������������������������������������������������������������������������������������������������������� 40
DECIMAL�������������������������������������������������������������������������������������������������������������������������������� 41
FLOAT������������������������������������������������������������������������������������������������������������������������������������� 43
INET��������������������������������������������������������������������������������������������������������������������������������������� 44
INTERVAL������������������������������������������������������������������������������������������������������������������������������� 45
JSONB����������������������������������������������������������������������������������������������������������������������������������� 46
SERIAL����������������������������������������������������������������������������������������������������������������������������������� 48
STRING����������������������������������������������������������������������������������������������������������������������������������� 49
TIME/TIMETZ�������������������������������������������������������������������������������������������������������������������������� 50
TIMESTAMP/TIMESTAMPTZ��������������������������������������������������������������������������������������������������� 51
GEOMETRY����������������������������������������������������������������������������������������������������������������������������� 53
Functions������������������������������������������������������������������������������������������������������������������������������������ 55
Geo-partitioned Data������������������������������������������������������������������������������������������������������������������� 58
REGION BY ROW�������������������������������������������������������������������������������������������������������������������� 59
REGION BY TABLE������������������������������������������������������������������������������������������������������������������ 63
vi
Table of Contents
vii
Table of Contents
viii
Table of Contents
ix
About the Author
Rob Reid is a software developer from London, England. In his career, he has written
back-end, front-end, and messaging software for the police, travel, finance, commodity,
sports betting, telecom, retail, and aerospace industries. He is an avid user of
CockroachDB and has worked with the Cockroach Labs team in recent years to promote
the database and embed it into development teams in the United States and the UK.
xi
About the Technical Reviewer
Fernando Ipar has been working on and with open source databases since 2000,
focusing on performance, scaling, and high availability. He currently works as a
Database Reliability Engineer at Life360. Before that, he has worked at Perceptyx,
Pythian, and Percona, among other places. When not working, Fernando enjoys going
to plant nurseries with his wife and playing music with their children while being a good
service employee for the family’s cat.
xiii
Acknowledgments
I’m incredibly grateful to the following people. Their contributions to this book have
been invaluable to me.
Kai Niemi (Solutions Engineer (EMEA) at Cockroach Labs) – I met Kai when he
was a customer of Cockroach Labs and have witnessed him transition from being a
CockroachDB expert at one company to an expert the global CockroachDB community
can be grateful to have.
Daniel Holt (Director, Sales Engineering, International (EMEA and APAC), at
Cockroach Labs) – I worked very closely with Daniel from the moment he joined
Cockroach Labs and have often marvelled at his comprehensive knowledge of the
database.
Katarina Vetrakova (Privacy Programme Manager at GoCardless) – Katarina is quite
possibly the most enthusiastic data privacy specialist you could ever hope to meet. She’s
completely dedicated to the art, and since working with her at Lush, her passion and
knowledge have been inspiring to me.
Jonathan Gennick (Assistant Editorial Director of Databases at Apress) – I’d like
to thank Jonathan Gennick for approaching me to write this book. Without him, this
amazing (and terrifying) opportunity wouldn’t have found me. He has been amazing
throughout the process of writing this book, and his patient knowledge sharing allowed
this first-time author to really find his feet and enjoyment in writing.
The Cockroach Labs team – The Cockroach Labs team is among the smartest people
I’ve ever met. They’re incredibly dedicated to their database and its customers and
are a big reason for my affection toward CockroachDB. I’d like to thank the following
people from Cockroach Labs (past and present) for their help, inspiration, hospitality,
and friendship: Jim Walker, Jeff Miller, Carolyn Parrish, Jordan Lewis, Bram Gruneir,
Kai Niemi, Daniel Holt, Glenn Fawcett, Tim Veil, Jessica Edwards, Dan Kelly, Lakshmi
Kannan, Spencer Kimball, Peter Mattis, Ben Darnell, Nate Stewart, Jesse Seldess,
Andy Woods, Meagan Goldman, Megan Mueller, Andrew Deally, Isaac Wong, Vincent
Giacomazza, Maria Toft, Tom Hannon, Mikael Austin, Eric Goldstein, Amruta Ranade,
Armen Kopoyan, Robert Lee, Charles Sutton, Kevin Maro, James Weitzman, and anyone
I’ve failed to mention.
xv
Introduction
Every so often, the technology community is blessed with truly disruptive technology.
We’ve seen the likes of Kubernetes for orchestration, Kafka for streaming, gRPC for
Remote Procedure Call, and Terraform for infrastructure. CockroachDB does what these
technologies have done for their respective use cases; it’s a game changer for data.
I first discovered CockroachDB in 2016, where I used it to create rapid prototypes
during company Hackathons at my then employer. It immediately felt familiar and as if
it had been designed for a developer to build reliable and scalable software without an
army of database specialists to help them.
In this book, I’ll share my excitement for this database and the experience I’ve gained
from using it for many different use cases.
xvii
Introduction
The book aims to remain practical, so it hovers above the database’s internal details.
To continue your journey, I recommend reading the excellent documentation and blog
posts available on the Cockroach Labs website: www.cockroachlabs.com.
Contacts
CockroachDB
[email protected]
53 W 23rd Street
8th Floor
New York, NY
10010
www.cockroachlabs.com
https://forum.cockroachlabs.com
Rob Reid
[email protected]
https://robreid.io
https://twitter.com/robreid_io
https://github.com/codingconcepts
www.linkedin.com/in/rob-reid
xviii
CHAPTER 1
The Reason
for CockroachDB
Databases are a critical part of almost every stateful system. However, correctly running
them can be challenging, especially where price, availability, and shifting international
data privacy regulations are concerned. CockroachDB makes navigating this tricky
landscape not only easier but enjoyable.
In this chapter, we’ll explore the why of CockroachDB – why it came to be, and why
you might want to consider using it for your data.
What Is CockroachDB?
CockroachDB is a cloud-native Relational Database Management System (RDBMS). It
falls into the class of “NewSQL” or “DistSQL” (Distributed SQL) databases, which aim
to provide the scalability of NoSQL databases while giving users the experience and
features of a traditional SQL database. It is wire-compatible with Postgres, which means
you can connect to a CockroachDB cluster with most Postgres tools and drivers. To find
out which drivers are available, visit www.cockroachlabs.com/docs/stable/install-
client-drivers.html.
On paper, CockroachDB is “CP,” which, in terms of the CAP Theorem, means it favors
data Consistency over Availability in the face of network Partitions. In reality, however,
CockroachDB – as its name suggests – has been built with availability in mind as well.
In practice, if you were to lose the majority of replicas in a CockroachDB cluster, rather
than compromising the integrity of your data, requests will block until the nodes have
recovered. Sizing your cluster to ensure CockroachDB can continue to function in the
event of lost nodes is, therefore, an essential constituent to availability.
1
© Rob Reid 2022
R. Reid, Practical CockroachDB, https://doi.org/10.1007/978-1-4842-8224-3_1
Chapter 1 The Reason for CockroachDB
CockroachDB’s Architecture
CockroachDB uses the Raft Consensus algorithm to give users a multiactive system,
which means every node is the same. There are no special “active,” “leader,” or “write”
nodes. Every node can receive a portion of read and write requests for different shards
(or “ranges”) of data, helping CockroachDB to scale horizontally.
Under the hood, CockroachDB is a distributed key/value store. But thanks to its
layered architecture of SQL > Transactions > Distribution > Replication > Storage, you
can interact with data as richly as you would in any other RDBMS.
2
Chapter 1 The Reason for CockroachDB
In Figure 1-2, you’ll see that when we add a node, data in the cluster will be
rebalanced across all four nodes, resulting in shares of 30GB per node of the 120GB total.
3
Chapter 1 The Reason for CockroachDB
4
CHAPTER 2
Installing CockroachDB
One of CockroachDB’s many strengths is the ease with which you can install it. In this
chapter, we’ll explore CockroachDB’s licensing model and the various ways you can
install it.
Licensing
We’ll start with a tour of CockroachDB’s licensing options to familiarize ourselves with
what each option provides. There are both free and paid-for models, and your choice of
model will depend on your requirements and the features available in each.
Free
There are two free options available. One is an on-premises installation of the
CockroachDB Core functionality, and the other is a cloud offering:
5
© Rob Reid 2022
R. Reid, Practical CockroachDB, https://doi.org/10.1007/978-1-4842-8224-3_2
Chapter 2 Installing CockroachDB
Paid For
To unlock everything that CockroachDB has to offer (and to support Cockroach Labs
in continuing to build the database into the future), the following paid-for models are
available:
CockroachDB Core
Beginning in version 19.2, CockroachDB Core is subject to multiple licenses. Some
functionality retains the original Apache 2.0 license, meaning it is fully Open Source. Other
features are subject to a Cockroach Community License, which protects Cockroach Labs
from companies using their code to build products that do not benefit Cockroach Labs.
In short, if you plan on using CockroachDB Core’s free features to power your own
databases and do not intend to sell CockroachDB as a service to others, CockroachDB
Core is a good choice for you.
Local Installation
In this section, I’ll show you how to install CockroachDB on your local machine. For
each installation method, we’ll run CockroachDB with insecure configuration for brevity
for each installation method, which means no authentication or encryption. This
is acceptable for a local development database but not for anything else. Once we’ve
installed and tested local insecure deployments, we’ll move on to some real-world
secure implementations.
6
Chapter 2 Installing CockroachDB
Binary Install
You can install the cockroach binary in several ways, depending on your
operating system.
For Linux users, run the following commands to get started:
$ curl https://binaries.cockroachdb.com/cockroach-v21.1.7.linux-amd64.tgz
| tar -xz
$ curl -o cockroach-v21.1.7.windows-6.2-amd64.zip
https://binaries.cockroachdb.com/cockroach-v21.1.7.windows-6.2-amd64.zip
We’ve just started a single node, insecure cluster, listening on localhost with the
default 26527 port for SQL connections and the 8080 port for HTTP connections.
To test that CockroachDB is up and running, use the cockroach sql command to
enter the CockroachDB SQL shell. Note that the --host argument can be omitted, as the
default value assumes a locally running node using the default port:
7
Chapter 2 Installing CockroachDB
Docker Install
To install CockroachDB with Docker, first, make sure you have Docker installed on
your machine. If you don’t have Docker installed, visit the Docker installation website
https://docs.docker.com/get-docker for instructions.
You can start an instance of CockroachDB in Docker with the following command:
$ docker run \
--rm -it \
--name=cockroach \
-p 26257:26257 \
-p 8080:8080 \
cockroachdb/cockroach:v21.1.7 start-single-node \
--insecure
This command will pull the CockroachDB Docker image and run it with port 26257
for client connections and port 8080 for HTTP connections exposed. The command will
block until the process terminates, at which point, the CockroachDB Docker container
will be stopped and removed.
To test that CockroachDB is up and running, use the cockroach sql command to
enter the CockroachDB SQL shell:
8
Chapter 2 Installing CockroachDB
Kubernetes Install
To install CockroachDB with Kubernetes, make sure you have the following prerequisites
installed:
• kind
(https://kind.sigs.k8s.io/docs/user/quick-start)
• minikube
(https://minikube.sigs.k8s.io/docs/start)
• k3s
(https://rancher.com/docs/k3s/latest/en/installation)
I will be using kind to create a local Kubernetes cluster, and at the time of writing,
this installs Kubernetes version 1.21.
9
Chapter 2 Installing CockroachDB
https://github.com/cockroachdb/cockroach/blob/master/cloud/kubernetes/
cockroachdb-statefulset.yaml
1_pod-disruption-budget.yaml
This file creates the PodDisruptionBudget resource for our StatefulSet. A Pod Disruption
Budget provides Kubernetes with a tolerance for pod failures against a given application
(identified by the selector “cockroachdb”). It ensures that there are never more than
maxUnavailable pods unavailable in the cluster at any one time. By setting this to 1, we’ll
prevent Kubernetes from removing more than 1 CockroachDB node during operations
like rolling updates, etc.
If you’re configuring a cluster of 5 nodes in Kubernetes, consider setting this to 2, as
you’ll still have a 3-node cluster if 2 of your nodes are temporarily unavailable.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: cockroachdb-budget
labels:
app: cockroachdb
spec:
selector:
matchLabels:
app: cockroachdb
maxUnavailable: 1
10
Chapter 2 Installing CockroachDB
2_stateful-set.yaml
Now we’re getting into the guts of our Kubernetes configuration. It’s time to create our
StatefulSet. A StatefulSet is like a deployment that provides additional guarantees around
pod scheduling to ensure that pods have a persistent disk.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: cockroachdb
spec:
serviceName: "cockroachdb"
replicas: 3
selector:
matchLabels:
app: cockroachdb
template:
metadata:
labels:
app: cockroachdb
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cockroachdb
topologyKey: kubernetes.io/hostname
containers:
- name: cockroachdb
image: cockroachdb/cockroach:v21.1.7
11
Chapter 2 Installing CockroachDB
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: "1"
memory: "1Gi"
limits:
cpu: "1"
memory: "1Gi"
ports:
- containerPort: 26257
name: grpc
- containerPort: 8080
name: http
readinessProbe:
httpGet:
path: "/health?ready=1"
port: http
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 2
volumeMounts:
- name: datadir
mountPath: /cockroach/cockroach-data
env:
- name: COCKROACH_CHANNEL
value: kubernetes-insecure
- name: GOMAXPROCS
valueFrom:
resourceFieldRef:
resource: limits.cpu
divisor: "1"
command:
- "/bin/bash"
- "-ecx"
- exec
12
Chapter 2 Installing CockroachDB
/cockroach/cockroach
start
--logtostderr
--insecure
--advertise-host $(hostname -f)
--http-addr 0.0.0.0
--join cockroachdb-0.cockroachdb,cockroachdb-1.
cockroachdb,cockroachdb-2.cockroachdb
--cache 25%
--max-sql-memory 25%
terminationGracePeriodSeconds: 60
volumes:
- name: datadir
persistentVolumeClaim:
claimName: datadir
podManagementPolicy: Parallel
updateStrategy:
type: RollingUpdate
volumeClaimTemplates:
- metadata:
name: datadir
spec:
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: 1Gi
Whether you’re a seasoned Kubernetes user or not, a lot is going on here. I’ll take
you through some of the less obvious configuration blocks to further demystify the
configuration.
13
Chapter 2 Installing CockroachDB
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- cockroachdb
topologyKey: kubernetes.io/hostname
You can ask the Kubernetes scheduler to give pods an affinity or an antiaffinity to
other pods. By doing this, you’re either asking them to be placed together or placed away
from one another, respectively.
To highlight the importance of this configuration, let’s assume that our
Kubernetes cluster is running in the cloud and each node is running in a
different availability zone (AZ). This podAntiAffinity rule asks Kubernetes to
launch each cockroachdb pod on a different node (identified by its kubernetes.
io/hostname”). Depending on your requirements, you may prefer to use the
requiredDuringSchedulingIgnoredDuringExecution rule, which guarantees that
each pod schedules onto a different node. This makes sense for clusters with multiple
Kubernetes nodes but not for our local, single-node Kubernetes cluster.
env:
- name: COCKROACH_CHANNEL
value: kubernetes-insecure
- name: GOMAXPROCS
valueFrom:
resourceFieldRef:
resource: limits.cpu
divisor: "1"
In this block, we’re setting the environment variables consumed during both
initialization and runtime. The COCKROACH_CHANNEL variable will help us identify
how we installed the cluster (e.g., installed securely on Kubernetes or installed via
14
Chapter 2 Installing CockroachDB
helm, etc.). The GOMAXPROCS variable is passed to CockroachDB and used by the Go
runtime to limit the CPU cores allocated to the CockroachDB process.
command:
- "/bin/bash"
- "-ecx"
- exec
/cockroach/cockroach
start
--logtostderr
--insecure
--advertise-host $(hostname -f)
--http-addr 0.0.0.0
--join cockroachdb-0.cockroachdb,cockroachdb-1.
cockroachdb,cockroachdb-2.cockroachdb
--cache 25%
--max-sql-memory 25%
In this block, we’re passing some arguments to the CockroachDB executable. These
arguments tell CockroachDB how to discover other nodes and how to be discovered
by other nodes and set memory constraints for the database. These are useful when
running on machines where resources are limited.
volumeClaimTemplates:
- metadata:
name: datadir
spec:
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: 1Gi
In this block, we’re asking Kubernetes for 1 gibibyte1 of disk storage. There are
various access modes available, but I’m using ReadWriteOnce as we only need to
allocate disk space for each node once.
1
1 gibibyte = 230 = 1,073,741,824 bytes
15
Chapter 2 Installing CockroachDB
3_private-service.yaml
This file creates the Kubernetes service that will be used both by the CockroachDB nodes
to discover one another and by other resources in the Kubernetes cluster. It will not be
available outside of the cluster, as it doesn’t expose a cluster IP.
Exposing port 26257 will allow the CockroachDB nodes to communicate with one
another, and exporting port 8080 will allow services like Terraform to obtain metrics.
apiVersion: v1
kind: Service
metadata:
name: cockroachdb
labels:
app: cockroachdb
spec:
ports:
- name: tcp
port: 26257
targetPort: 26257
- name: http
port: 8080
targetPort: 8080
publishNotReadyAddresses: true
clusterIP: None
selector:
app: cockroachdb
4_public-service.yaml
This file creates a Kubernetes LoadBalancer Service that will be used for external
connections to the CockroachDB nodes. We’ll use this service to connect to the
database later.
16
Chapter 2 Installing CockroachDB
Exposing port 26257 will allow us to connect to the database, and exposing port 8080
will allow us to view its dashboard. The following code creates a Kubernetes service that
exposes the ports defined in the StatefulSet.
apiVersion: v1
kind: Service
metadata:
name: cockroachdb-public
labels:
app: cockroachdb
spec:
ports:
- name: tcp
port: 26257
targetPort: 26257
- name: http
port: 8080
targetPort: 8080
selector:
app: cockroachdb
type: LoadBalancer
5_init.yaml
This file initializes the CockroachDB cluster. When run, it will connect to the first node
in the cluster to perform initialization. In our case, we could have specified any of the
nodes to start the initialization, as they are all configured to connect to the other nodes
via the --join argument.
17
Chapter 2 Installing CockroachDB
apiVersion: batch/v1
kind: Job
metadata:
name: init
labels:
app: cockroachdb
spec:
template:
spec:
containers:
- name: init
image: cockroachdb/cockroach:v21.1.7
imagePullPolicy: IfNotPresent
command:
- "/cockroach/cockroach"
- "init"
- "--insecure"
- "--host=cockroachdb-0.cockroachdb"
restartPolicy: Never
If you’ve got to this point, you’ll have a working CockroachDB cluster running in
Kubernetes! There are a lot of moving parts in this example, so your mileage may vary.
If your local Kubernetes cluster is running in Docker and you’re experiencing issues,
make sure that you’ve given Docker enough memory to run this example. 6GB should
be plenty.
To connect to the cluster’s HTTP endpoint, create a port-forward to the public
service and open http://localhost:8080:
18
Chapter 2 Installing CockroachDB
To connect to the cluster’s SQL endpoint, create a port-forward to the public service:
Then use the cockroach command as if your cluster was running locally:
If you’d rather connect to the CockroachDB cluster from within Kubernetes, run the
following command to create a Kubernetes pod that opens a SQL shell:
19
Chapter 2 Installing CockroachDB
Multinode Clusters
Up to this point, we’ve only created single-node, local clusters, which are not advised
for real-world use cases, as such clusters cannot provide resilience. Instead, it’s better
to create clusters of at least three nodes to survive a failure of one node, or at least five
nodes to survive a failure of two nodes.
The CockroachDB documentation provides guidance on the topologies you’ll need
to use in order to survive everything from single-node to multiregion failures:
www.cockroachlabs.com/docs/stable/disaster-recovery.html
There are plenty of ways to install a multinode CockroachDB cluster. A Kubernetes
StatefulSet deployment is one of the more accessible options. It takes care of the manual
work involved in managing multiple application instances such as service discovery
and rolling updates. In the previous section on installing via Kubernetes, had we been
working with a multinode/region cluster, our StatefulSet would have deployed one
CockroachDB node to each of them.
In this section, we’ll simulate a three-node CockroachDB cluster with the
cockroach binary.
First, start the three nodes with the following commands:
$ cockroach start \
--insecure \
--store=node1 \
--listen-addr=localhost:26257 \
--http-addr=localhost:8080 \
--join=localhost:26257,localhost:26258,localhost:26259
$ cockroach start \
--insecure \
--store=node2 \
--listen-addr=localhost:26258 \
--http-addr=localhost:8081 \
--join=localhost:26257,localhost:26258,localhost:26259
$ cockroach start \
--insecure \
--store=node3 \
--listen-addr=localhost:26259 \
20
Chapter 2 Installing CockroachDB
--http-addr=localhost:8082 \
--join=localhost:26257,localhost:26258,localhost:26259
Next, initialize the cluster by executing the init command against one of the nodes:
Finally, connect to the cluster by executing the sql command against one of
the nodes:
If you’re creating a cluster on a known set of machines and you’d rather avoid
orchestration tools like Kubernetes, this might be a good option for you.
Multiregion Clusters
For many use cases, installing CockroachDB into multiple availability zones (AZs) within
a cloud provider region will provide a suitable level of resilience for your database.
If you require more resiliency than a single-region deployment can provide,
CockroachDB has you covered again. In this section, we’ll simulate a multiregion
deployment using the cockroachdb binary. This cluster will survive not only AZ failures
but also complete cloud provider region failures.
21
Chapter 2 Installing CockroachDB
Multiregion Deployment
In the previous example, we were operating within a cloud provider region. A single AZ
failure would have removed one CockroachDB node from the cluster, leaving two nodes
to continue working until it was available again. From a latency perspective, this would
have been acceptable, thanks to the proximity of the other nodes.
On a map, our cluster may have looked similar to the cluster in Figure 2-1. Note that
the map view for nodes is only available for Enterprise clusters:
All nodes are located within a given cloud provider region (e.g., us-east1, as shown
previously).
In the following example, we’re operating across cloud provider regions. This means
the distance (and latency) between regions may be significant. Creating a cluster with
one node in each region would therefore be a bad idea. Consider Figure 2-2, where we’ve
taken our three-node, single-region cluster and have installed it across regions instead
of AZs. If we lose one node, the cluster will continue to work with two. However, as
mentioned, the latency between nodes will now be much greater.
22
Chapter 2 Installing CockroachDB
Outages aside, our cluster will be spending a lot of time replicating data between
nodes. When writing to CockroachDB in a three-node replicated cluster, consensus
between the leaseholder node (the primary write node for a range of data) and at least
one of its follower nodes will need to be achieved. This will take time in a cross-region
cluster, making writes very slow.
We need a new topology.
In this example, we’ll partition our data by region so that database consumers (users
and applications, etc.) will access their closest region for all write operations. For a
cluster like this to provide adequate regional and zonal resilience, we’ll now need nine
nodes. Use the following commands to create these:
$ cockroach start \
--insecure \
--store=node1 \
--listen-addr=localhost:26257 \
--http-addr=localhost:8080 \
--locality=region=us-east1,zone=us-east1a \
--join='localhost:26257, localhost:26258, localhost:26259'
23
Chapter 2 Installing CockroachDB
$ cockroach start \
--insecure \
--store=node2 \
--listen-addr=localhost:26258 \
--http-addr=localhost:8081 \
--locality=region=us-east1,zone=us-east1b \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node3 \
--listen-addr=localhost:26259 \
--http-addr=localhost:8082 \
--locality=region=us-east1,zone=us-east1c \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node4 \
--listen-addr=localhost:26260 \
--http-addr=localhost:8083 \
--locality=region=us-central1,zone=us-central1a \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node5 \
--listen-addr=localhost:26261 \
--http-addr=localhost:8084 \
--locality=region=us-central1,zone=us-central1b \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node6 \
--listen-addr=localhost:26262 \
--http-addr=localhost:8085 \
24
Chapter 2 Installing CockroachDB
--locality=region=us-central1,zone=us-central1c \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node7 \
--listen-addr=localhost:26263 \
--http-addr=localhost:8086 \
--locality=region=us-west1,zone=us-west1a \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node8 \
--listen-addr=localhost:26264 \
--http-addr=localhost:8087 \
--locality=region=us-west1,zone=us-west1b \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node9 \
--listen-addr=localhost:26265 \
--http-addr=localhost:8088 \
--locality=region=us-west1,zone=us-west1c \
--join='localhost:26257, localhost:26258, localhost:26259'
Next, initialize the cluster by executing the init command against one of the nodes:
To partition the database in this way, we’ll need an Enterprise license. You can
obtain a free 30-day trial license from the Cockroach Labs website. Visit the
www.cockroachlabs.com/get-cockroachdb/enterprise/ page and grab your trial
Enterprise license now.
25
Chapter 2 Installing CockroachDB
It’s time to convert the cluster to Enterprise! Connect to one of the nodes in the
cluster and run the following commands, substituting the cluster.organization and
enterprise.license values with the ones you received in the Enterprise trial email:
Visit the Cluster Console for one of the nodes in the cluster (e.g., http://
localhost:8080) and switch the Node List to Node Map. Figure 2-3 shows what you’ll
see with an Enterprise Cluster in the Cluster Map view this time.
26
Chapter 2 Installing CockroachDB
CockroachDB allows you to decide how the cluster should behave in the event of
zonal or regional failure. These are configured as “Survival Goals” options, and your
decision of which one to use should be based on your resilience and performance
requirements.
27
Chapter 2 Installing CockroachDB
By running the following command, we’ll see that the number of replicas is set to 3,
the default value for CockroachDB with or without explicitly configuring survival goals.
This database is now configured to survive region failures instead of zone failures.
Now run the following command to set the number of replicas to 5.
28
Chapter 2 Installing CockroachDB
CockroachDB Serverless/Dedicated
If you prefer to use Software-as-a-Service (SaaS) tools, CockroachDB Serverless and
CockroachDB Dedicated (collectively referred to as “CockroachDB Serverless”) are a
great way to get started with CockroachDB. Both services allow you to spin up a secure
CockroachDB cluster in a matter of seconds. Depending on your requirements, it
provides everything from free single-core/5GB/multitenant instances of CockroachDB to
multiregion Enterprise clusters hosted on your choice of cloud provider.
Creating a Cluster
Let’s create and connect to a free-tier CockroachDB Serverless instance now.
First, head to https://cockroachlabs.cloud and register for an account if you don’t
already have one. Cockroach Labs will not ask for you any billing information.
Next, choose the Free plan, select your preferred cloud provider, choose a
deployment region, provide a name, and create your cluster. At the time of writing, you
can choose to host your Free Tier instance in either GCP’s europe-west1, us-central1,
and asia-southeast1 regions or AWS’s eu-west-1, us-west-2, and ap-southeast-1 regions.
After approximately 20 seconds, your cluster will be ready for connections.
Next, connect to your database via the SQL shell using the following command. The
emboldened text will be provided by the UI when you click on Connect:
29
Chapter 2 Installing CockroachDB
Summary
In this chapter, we’ve covered a lot of ground. Let’s recap on the things we’ve learned:
30
CHAPTER 3
Concepts
It’s time to dive deeper into CockroachDB! This chapter explores some of CockroachDB’s
building blocks, including data types, indexes, and geo-partitioning.
First, let’s explore the top-level objects that make up CockroachDB.
Database Objects
As Figure 3-1 shows, objects in CockroachDB are arranged hierarchically.
31
© Rob Reid 2022
R. Reid, Practical CockroachDB, https://doi.org/10.1007/978-1-4842-8224-3_3
Chapter 3 Concepts
At the top level of the CockroachDB object hierarchy, there are databases. Databases
contain schemas that, in turn, contain schema objects like tables and views.
Every CockroachDB cluster will contain the following three databases when it starts:
32
Chapter 3 Concepts
Underneath schemas are CockroachDB’s third and final-level objects. These include
indexes, sequences, tables, views, and temporary objects (objects like temporary tables
that are not persisted).
Data Types
CockroachDB boasts all of the data types you’ll need to build a rich database. In this
section, I’ll show you how and where to use these data types.
UUID
The UUID data type stores a 128-bit UUID value. Values stored in this column can be any
UUID version1 but will all be formatted using RFC 4122 standards. Let’s create a table
with a UUID column to learn more about it.
First, let’s create a contrived table that includes a UUID column:
1
https://en.wikipedia.org/wiki/Universally_unique_identifier
33
Chapter 3 Concepts
Next, we’ll insert some data into it; note that UUIDs are valid with or without curly
braces, as Uniform Resource Names (URNs), or as 16-byte strings:
Selecting the UUIDs out of the table reveals their stored representation, even though
we inserted them differently:
UUID columns are a great choice when you need unique IDs for tables. Rather than
providing the UUIDs ourselves, let’s ask CockroachDB to generate them for us on-insert:
CockroachDB will only generate a default value if you don’t provide one, so it’s still
possible to provide values yourself.
ARRAY
The ARRAY data type stores a flat (or one-dimensional) collection of another data type.
It’s indexable using inverted indexes designed to work with tokenizable data, such as
the values of an array or the key-value pairs in a JSON object. Let’s create a table with an
ARRAY column to learn more about it.
34
Chapter 3 Concepts
First, we’ll create a table with an ARRAY column. Arrays are created in the TYPE[]
syntax or the TYPE ARRAY syntax as follows:
INSERT
INTO person (pets)
VALUES (ARRAY['Max', 'Duke']),
(ARRAY['Snowball']),
(ARRAY['Gidgit']),
(ARRAY['Chloe']);
Selecting the values back out of the table reveals their representation in
CockroachDB:
There are many operations you can perform against ARRAY columns. We’ll cover just
the most common.
35
Chapter 3 Concepts
To return rows that contain a particular value in an ARRAY column, we can use the
“contains” operator. The following returns the ID of any person with a pet called Max:
To return rows whose ARRAY column is within a given array, we can use the “is
contained by” operator. The following returns the ID of any person whose complete list
of pets is contained within a given array:
If you know the name of one of a person’s pets but not all of them, you can use
the overlap operator to find the ID of any person who has a pet contained within a
given array:
Add an element to an array (note that for inserts, you can either use array_append or
the append operator ||):
UPDATE person
SET pets = array_append(pets, 'Duke')
WHERE id = '59220317-cc79-4689-b05f-c21886a7986d';
UPDATE person
SET pets = array_remove(pets, 'Duke')
WHERE id = '59220317-cc79-4689-b05f-c21886a7986d';
36
Chapter 3 Concepts
To get the most out of an ARRAY column, you’ll need to use an inverted index, as without
it, CockroachDB will have to perform a full table scan, as highlighted in the following:
• filter
│ estimated row count: 0
│ filter: pets @> ARRAY['Max']
│
└── • scan
estimated row count: 4 (100% of the table; stats collected 16
minutes ago)
table: person@primary
spans: FULL SCAN
BIT
The BIT data type stores a bit array. BIT columns can store varying numbers of bits and
can either contain an exact or a variable number of bits:
37
Chapter 3 Concepts
any_size VARBIT,
up_to_64 VARBIT(64)
);
Values can be inserted into BIT columns as follows (note that the preceding B for
each of the values denotes a binary string):
INSERT
INTO bits (exactly_1, exactly_64, any_size, up_to_64)
VALUES (
B'1',
B'101010101010101010101010101010101010101010101010101
0101010101010',
B'10101',
B'10101010101'
);
BOOL
The BOOL or BOOLEAN data type stores a true or false value and is created as follows:
Values are provided to BOOL columns with Boolean literals or via type casting from
integers:
INSERT
INTO person (wants_marketing_emails)
VALUES
(1::BOOL), -- True (any non-zero number)
(true), -- Literal true
(12345::BOOL), -- True (any non-zero number)
(0::BOOL), -- False (zero value)
(false); -- Literal false
38
Chapter 3 Concepts
BYTES
The BYTES, BYTEA, or BLOB data type stores the byte array equivalent of TEXT strings and
can be created as follows:
You can insert BYTES values in several ways. TEXT strings will automatically cast to
BYTES, and CockroachDB supports various encoding methods for fine-grained control of
insert values:
INSERT
INTO person (password)
VALUES
('password'), -- String value
(b'password'), -- Byte array literal
(x'70617373776f7264'), -- Hex literal
(b'\x70\x61\x73\x73\x77\x6f\x72\x64'); -- Hex characters
Every resulting row from the preceding insert will have an identical password
column value.
DATE
The DATE data type stores a day, month, and year value and is created as follows:
39
Chapter 3 Concepts
INSERT
INTO person (date_of_birth)
VALUES
('1941-09-09'), -- String literal
(DATE '1941-09-09'), -- Interpreted literal
('1941-09-09T01:02:03.456Z'), -- Timestamp (will be truncated)
(CAST(-10341 AS DATE)); -- Number of days since the epoch
Every resulting row from the preceding insert will have an identical date_of_birth
column value.
ENUM
The ENUM data type provides an enumeration that is validated upon insert and is created
as follows:
40
Chapter 3 Concepts
As with many of CockroachDB’s data types, ENUM columns are castable from string
literals, interpreted literals, or strings with direct casts:
INSERT
INTO person (favourite_planet)
VALUES
('neptune'), -- String literal
(planet 'earth'), -- Interpreted literal
(CAST('saturn' AS planet)); -- Cast
DECIMAL
The DECIMAL, DEC, or NUMERIC data type stores exact, fixed-point2 numbers of variable
size and is created either with or without a precision.
Let’s start by creating a DECIMAL column without specifying a precision and scale:
Inserting some values into this table will reveal that only the digits required to
represent the number are used:
INSERT
INTO person (bitcoin_balance)
VALUES
(0.000030),
(0.80),
(147.50);
2
Meaning the number of digits after the decimal point is fixed.
41
Chapter 3 Concepts
Now, let’s recreate the table and provide a precision and scale for the DECIMAL
column this time. The DECIMAL column type now takes two arguments: the first defines
the precision of the value, and the second defines the scale. The precision argument tells
CockroachDB the maximum number of integral digits (digits to the left of the decimal
point) and fractional digits (digitals to the right of the decimal point):
Inserting some values into this table will reveal that all eight of the fractional digits
are used:
INSERT
INTO person (bitcoin_balance)
VALUES
(0.000030),
(0.80),
(147.50);
42
Chapter 3 Concepts
It is possible to insert infinite and NAN (not a number) values in a DECIMAL column
as follows:
INSERT
INTO person (bitcoin_balance)
VALUES
('inf'), ('infinity'), ('+inf'), ('+infinity'),
('-inf'), ('-infinity'),
('nan');
FLOAT
The FLOAT, FLOAT4 (REAL), and FLOAT8 (DOUBLE PRECISION) data types store inexact,
floating-point numbers with up to 17 digits of precision and are created as follows:
INSERT
INTO person (latitude, longitude)
VALUES
(38.908200917747095, -77.03236828895616),
(52.382578005867906, 4.855332269875395),
(51.46112343492288, -0.11128454244911225),
(51.514018690098645, -0.1267503331073194);
43
Chapter 3 Concepts
INET
The INET data type stores valid IPv4 and IPv6 addresses and CIDRs (Classless Inter-
Domain Routing) and is created as follows:
Inserting some values into this table will reveal that CockroachDB understands the
IP addresses being inserted and removes any redundant masks:
INSERT
INTO person (ip)
VALUES
('10.0.1.0/24'),
('10.0.1.1/32'),
('229a:d983:f190:75ef:5f06:a5a8:f5c2:8500/128'),
('229a:d983:f190:75ef:5f06:a5a8:f5c2:853f/100');
44
Chapter 3 Concepts
Notice that the 10.0.1.0/24 address keeps its mask because a /24 mask for this
address includes addresses from 10.0.1.0 to 10.0.1.255. On the other hand, the
10.0.1.1/32 address has a mask of /32, which means “this IP address only,” meaning we
have a specific IP address, and the mask is superfluous. The same holds for the IPv6
addresses, where a /128 means “this IP address only.”
INTERVAL
The INTERVAL data type stores durations ranging from microseconds to years and is
created as follows:
You can provide durations as ISO 8601 strings, time strings, and seconds. Selecting
the rows out of the table reveals how the values are stored and how you can cast them to
seconds:
INSERT
INTO rodeo_records (duration)
VALUES
('10:45:00.0'), -- Time string
('1h30m'), -- ISO 8601 string
(30::INTERVAL); -- Seconds
45
Chapter 3 Concepts
JSONB
The JSONB, JSON data type stores arbitrary JSON objects and is ideal for storing semi-
structured data (data that has a structure that doesn’t fit or may outgrow your relational
database tables). JSONB columns can be created as follows:
If you plan on performing complex queries against a JSON column, you’ll want to
create an inverted index for the column. I’ll cover these in a subsequent chapter on
performance.
To insert data into a JSONB column, simply provide a JSON string as follows:
INSERT
INTO song (details)
VALUES
('{"label": "Century Media", "release_date": "2004-09-20"}'),
('{"label": "Season of Mist", "release_date": "2010-02-15"}'),
('{"label": "Season of Mist", "release_date": "2016-02-12"}');
JSON columns are very flexible. You can query their fields and select specific values
to return. Let’s do both now. The following query returns the release dates of songs
released under the “Season of Mist” record label as string values:
46
Chapter 3 Concepts
SELECT
details->>'release_date' AS release_date
FROM
song
WHERE
details @> '{"label": "Season of Mist"}';
release_date
----------------
2010-02-15
2016-02-12
To access specific JSON fields as regular columns, create indexes or even create
PRIMARY KEYs from JSON fields within JSON documents; this is also possible with the
JSON data type.
The following CREATE statement recreates the table, but this time, its id and label
columns are from fields in the details JSON column:
INSERT
INTO song (details)
VALUES
('{"id":"60d28ed4-ee97-43d5-98a7-ba42d478f4c7", "label": "Century
Media", "release_date": "2004-09-20"}'),
('{"id":"4b158ac6-386d-4143-8281-ca6f0f9c9a93", "label": "Season of
Mist", "release_date": "2010-02-15"}'),
('{"id":"c82dc39d-f310-45a4-9a31-805d923f1c8e", "label": "Season of
Mist", "release_date": "2016-02-12"}');
47
Chapter 3 Concepts
We can now treat the id and label columns exactly as we’d treat any other
database column:
SELECT
id,
label,
details->>'release_date' release_date
FROM
song
WHERE
label = 'Season of Mist';
SERIAL
The SERIAL data type is not strictly a data type but rather an INT, INT2, INT4, or INT8
with a default value applied, resulting in an auto-incrementing number. SERIAL was
introduced to CockroachDB to provide Postgres compatibility. As such, you should
consider using the UUID data type with a default value of gen_random_uuid() instead of
the SERIAL data type, as this provides 128 bits of randomness vs. SERIAL’s maximum of
64 bits.
SERIAL columns can be created as follows:
Inserting data into this table will reveal that close attention to the SERIAL
documentation3 is required to fully understand the nuances of this data type:
3
www.cockroachlabs.com/docs/stable/serial.html
48
Chapter 3 Concepts
INSERT
INTO random
DEFAULT VALUES;
As we can see from the values generated, regardless of the integer size provided by
the data type definition, the default mode for generating these values (unique_rowid())
will always result in a 64-bit integer.
STRING
The STRING, TEXT, CHARACTER, CHAR, or VARCHAR data type stores either fixed or variable-
length strings of Unicode characters and is created as follows:
By showing the columns of our table, we can see what the data types resolve to
behind the scenes:
49
Chapter 3 Concepts
column_name | data_type
--------------+--------------
id | UUID
first_name | STRING
last_name | VARCHAR(50)
grade | CHAR
Note that VARCHAR(n) is, in fact, an alias of STRING(n), but for Postgres compatibility,
it is still represented as VARCHAR(n) here, as Postgres does not have a STRING data type.
Inserting data into this table will reveal that CockroachDB faithfully represents
Unicode characters:
As in Postgres, you’ll receive an error from CockroachDB if you try to insert data that
will not fit into a fixed-length or limited variable-length column.
T IME/TIMETZ
The TIME and TIMEZ data types store time (minus date) values in UTC and zoned
representations, respectively. They can store time values to various levels of precision,
ranging from second precision down to microsecond precision.
Cockroach Labs recommends using the TIME variant and converting to local time in
the front-end.
50
Chapter 3 Concepts
name | time_of_day
---------------------+--------------
TIMESTAMP/TIMESTAMPTZ
The TIMESTAMP and TIMESTAMPTZ data types store timestamps in UTC and display the
values in UTC and zoned representations, respectively.
Cockroach Labs recommends using the TIMESTAMPTZ data type over the TIMESTAMP
data type for explicitness, so I’ll demonstrate the use of the TIMESTAMPTZ data type, which
is created as follows:
51
Chapter 3 Concepts
As you can see, the TIMESTAMP data types allow for an optional precision to be
provided. This precision accepts a value between zero (representing second precision)
and six (representing microsecond precision). The default precision for TIMESTAMP data
types is 6, so for most cases, you can omit the precision entirely.
Let’s insert some data into the table to show its representation to users in different
time zones:
INSERT
INTO episode_schedule (name, next_show_time)
VALUES
('South Park - The Tail of Sc...', '2021-12-10 22:00:00+00:00'),
('South Park - Grounded Vinda...', '2021-12-10 22:30:00+00:00'),
('South Park - Make Love, Not...', '2021-12-10 23:00:00+00:00');
Let’s select the rows out of the table now, once for users in a UTC time zone and once
for users in the Eastern Daylight Time (EDT) time zone:
As you can see, users in the UTC time zone see the data as it was inserted into the
database, while users in the EDT time zone see the data in their local time as a -05:00
offset from UTC.
52
Chapter 3 Concepts
GEOMETRY
The GEOMETRY and GEOGRAPHY data types store spatial objects on variable-plane4
geometry and earth positions as spheroids, respectively, and are created as follows:
Both the GEOMETRY and GEOGRAPHY data types can store the following spatial objects:
Let’s insert some GEOMETRY data into our table and perform some basic operations on
it. In this scenario, let’s assume we provide a service that allows users to look up stores.
We hold the store locations, and users draw search areas to find them.
INSERT
INTO property (location)
VALUES
('POINT(-0.16197244907496533 51.50186005364136)'),
4
2D, 3D, etc.
53
Chapter 3 Concepts
('POINT(-0.16139087662866003 51.498542352748814)'),
('POINT(-0.17528813494181622 51.48604279157454)');
A particularly wealthy user wishes to search for property in the Knightsbridge area of
London and provides the following coordinates:
Their query returns just the two properties in Knightsbridge, leaving the equally
lavish Chelsea property for another fabulously wealthy user.
A very neat feature of Postgres and CockroachDB is the ability to convert GEOMETRY
data into GeoJSON; let’s convert the search area provided by the user into GeoJSON and
visualize it on https://geojson.io:
{"type":"Polygon","coordinates":[[[-0.164215565,51.49697505],
[-0.153422356,51.49697505],[-0.153422356,51.503440155],
[-0.164215565,51.503440155],[-0.164215565,51.49697505]]]}
Pasting this data into the GeoJSON website yields the user’s search area as shown in
Figure 3-2.
54
Chapter 3 Concepts
Functions
CockroachDB provides built-in functions to make certain operations easier, and there
are functions for working with most of the data types introduced in this chapter.
Built-in functions5 either provide functionality for working with data types or expose core
system functionality such as working with stream ingestion and getting system information.
I will demonstrate the use of a small selection of built-in functions, but for brevity,
I will leave the exploration of the remaining functions as an exercise to the reader. The
following code example shows some of the more commonly used functions, and the
comment preceding each provides a brief explanation.
5
www.cockroachlabs.com/docs/stable/functions-and-operators.html#built-in-functions
55
Chapter 3 Concepts
-- Decodes a value from its encoded representation (out of hex, escape, and
base64).
SELECT decode('636f636b726f6163686462', 'hex');
--> cockroachdb
56
Chapter 3 Concepts
There are many more functions to try; for a complete list of available functions, visit
www.cockroachlabs.com/docs/stable/functions-and-operators.html.
57
Chapter 3 Concepts
Geo-partitioned Data
Geo-partitioning is one of CockroachDB’s most powerful Enterprise features, and as of
v21.1.10, working with geo-partitioned data has become much more straightforward.
To get started with geo-partitioned data, you first need to decide on a partitioning
strategy. For example, will all of the rows within a table need to remain in one location,
or will specific rows within that table need to be separated by location? If you need all of
the rows within a table to remain in one location, the REGION BY TABLE table locality is
what you need. On the other hand, if you need to pin rows to different places, the REGION
BY ROW table locality is a good choice.
Let’s create some database tables to demonstrate geo-partitioning for both locality
strategies.
Before we start, let’s create a cluster that spans multiple continents. Start by creating
a temporary Enterprise cluster using CockroachDB’s demo command:
$ cockroach demo \
--no-example-database \
--nodes 9 \
--insecure \
--demo-locality=region=us-east1,az=a:region=us-east1,az=b:region=us-
east1,az=c:region=asia-northeast1,az=a:region=asia-
northeast1,az=b:region=asia-northeast1,az=c:region=europe-
west1,az=a:region=europe-west1,az=b:region=europe-west1,az=c
This command creates an insecure, empty cluster with nine nodes. The --demo-
locality argument allows us to specify the region and availability zones for the nodes to be
distributed across: nine availability zones, one for each of our nine nodes. The syntax of this
argument’s value looks tricky but is actually very simple; for each of the requested localities,
we simply provide a colon-separated collection of region and availability zone pairs.
In order for CockroachDB to show a map of your cluster in its Node Map view, we need
to run a SQL command in the CockroachDB shell. At the time of writing, only a handful of
locations are configured in the system.locations table; both the United States and Europe
are configured, but Asia is not. We need to tell CockroachDB where to place the asia-
northeast1 cluster on its Node Map, and the following statement does just that:
58
Chapter 3 Concepts
As discussed in Chapter 2, it’s vital you correctly set regional or zonal survival goals
for your databases. While it is not essential to geo-partitioning, it is helpful to know how
your database will function in the event of an outage.
REGION BY ROW
The REGION BY ROW table locality ensures that individual rows within a table are
pinned to specific locations, depending on values set for a specific field. In human
speak, a location can be a continent, a country, a state, or even a specific machine. In
infrastructure speak, a location can be a region, an availability zone, or a node.
59
Chapter 3 Concepts
First, we’ll create a database for this example and make it region-aware. The
following code statements create a database and attach regions and a primary region via
ALTER statements:
USE my_app;
We don’t need to manually set a survival goal in this example because for now, we’ll
stick with the default configuration, which allows CockroachDB to survive zonal failures.
Next, we’ll create a table. This table will have two columns that allow us to geo-
partition data:
The following code creates a table called “person” with the aforementioned geo-
partitioning columns. It ensures that locality rules for the table are set to REGIONAL BY
ROW, meaning each row will be pinned to a different region:
60
Chapter 3 Concepts
Next, we’ll insert some data into the person table. We’ll provide different country
codes to ensure that data is stored across each of our three regions.
Selecting the values back out of the person table reveals that the crdb_region column
is populated as expected:
By default, CockroachDB will use a replication factor of five for the person table. You
can confirm this with a query for the table’s RANGES (I’ve modified the formatting to
make it easier to read):
61
Chapter 3 Concepts
region=asia-northeast1,az=c
{3,4,5,6,7} {"region=us-east1,az=c","region=asia-
northeast1,az=a","region=asia-northeast1,az=b","region=asia-
northeast1,az=c","region=europe-west1,az=a"}
region=europe-west1,az=a
{3,6,7,8,9}
{"region=us-east1,az=c","region=asia-northeast1,az=c","region=europe-
west1,az=a","region=europe-west1,az=b","region=europe-west1,az=c"}
region=us-east1,az=b
{1,2,3,5,7}
{"region=us-east1,az=a","region=us-east1,az=b","region=us-
east1,az=c","region=asia-northeast1,az=b","region=europe-west1,az=a"}
From the preceding output, you’ll see that data in the person table is shared across
five nodes, as per the default replication factor in CockroachDB. A replication factor of
five provides additional robustness if a region becomes unavailable.
I’ll now drop the replication factor from five to three to show how CockroachDB
will keep geo-partitioned data pinned to the regions specified in crdb_internal_region
columns (in our case, the crdb_region column):
region=asia-northeast1,az=c
{4,5,6}
62
Chapter 3 Concepts
{"region=asia-northeast1,az=a","region=asia-northeast1,az=b",
"region=asia-northeast1,az=c"}
region=europe-west1,az=a
{7,8,9}
{"region=europe-west1,az=a","region=europe-west1,az=b",
"region=europe-west1,az=c"}
region=us-east1,az=b
{1,2,3}
{"region=us-east1,az=a","region=us-east1,az=b","region=us-east1,az=c"}
REGION BY TABLE
The REGION BY TABLE table locality ensures that all of the rows within a table remain
in a particular location. In human speak, a location can be a continent, a country, a
state, or even a specific machine. In infrastructure speak, a location can be a region, an
availability zone, or a node.
The REGION BY TABLE table locality is the default configuration for tables in
CockroachDB. As a result, access to regional tables will be faster when accessed from the
database’s primary region.
Use this option if you wish to pin all data within a table to one region. In the
following example, we’ll create a regional table.
First, we’ll create a table. Note that in this example, I’m not providing any regional
information to the rows of the table; this is because the table itself is regional.
63
Chapter 3 Concepts
('Grace', 'Hopper'),
('Barbara', 'McClintock'),
('Rachel', 'Carson');
Assuming the database’s primary region is still set to europe-west1 from the REGION
BY ROW example, if we were to select the table’s ranges at this point, we’d see that a
European-based leaseholder currently manages our American scientists:
region=europe-west1,az=a
{3,4,7,8,9}
{"region=us-east1,az=c","region=asia-northeast1,az=a","region=europe-
west1,az=a","region=europe-west1,az=b","region=europe-west1,az=c"}
To move this table into the us-east1 region, we need to run a couple of simple
statements. First, set the database’s primary region to us-east1, which will update the
leaseholder for the data but will not physically locate all replicas to the United States.
CockroachDB sets a default number of replicas for a new table to five, so as we have only
three nodes in the us-east1 region, CockroachDB will use nodes from other regions to
bring the replica count up to five:
region=us-east1,az=c
{1,2,3,4,8}
{"region=us-east1,az=a","region=us-east1,az=b","region=us-
east1,az=c","region=asia-northeast1,az=a","region=europe-west1,az=b"}
Next, drop the replica count down to three to ensure that all data moves across to the
three nodes in the us-east1 region:
64
Chapter 3 Concepts
region=us-east1,az=c
{1,2,3}
{"region=us-east1,az=a","region=us-east1,az=b","region=us-east1,az=c"}
In the previous step, we reduced the number of replicas for the american_scientists
table from five to three. Doing so will impact the resilience of the cluster, given that
CockroachDB will now replicate data across fewer nodes. In later chapters, I share some
cluster size and replication number recommendations for production environments.
65
CHAPTER 4
Managing CockroachDB
from the Command Line
As we discovered in Chapter 2, CockroachDB can be installed in many ways. A great way
to create databases during development is to use the cockroach binary. In this chapter,
we’ll explore the cockroach binary and the features it provides.
67
© Rob Reid 2022
R. Reid, Practical CockroachDB, https://doi.org/10.1007/978-1-4842-8224-3_4
Chapter 4 Managing CockroachDB from the Command Line
Figure 4-1. A tree that represents the commands under the cockroach binary
Navigate the command tree by entering a subcommand and using the --help
argument at each level. For example:
$ cockroach -h
$ cockroach start -h
$ cockroach demo bank
68
Chapter 4 Managing CockroachDB from the Command Line
The start command requires a --join argument that takes the addresses of other
nodes in the cluster, forcing you to think about the basic topology of your cluster and
how the nodes will find one another. Cockroach Labs recommends configuring between
three and five nodes in the --join argument to ensure the startup performance of
your node.
After starting your nodes, you run the init command to initialize the cluster nodes.
In a nutshell, the init command initializes the database engine on each node and
ensures they are running the same version of CockroachDB.
To start a single-node CockroachDB cluster, use the start-single-node command.
This command starts a node and initializes it, meaning you don’t have to run the init
command as you would with a multinode cluster.
Unlike the start command, the start-single-node command does not allow you
to pass a --join argument, forcing your cluster to remain as a single node.
69
Chapter 4 Managing CockroachDB from the Command Line
• startrek – A sample database with two related tables: one that stores
Star Trek episodes and one that stores the quotes from them
For most use cases, you’ll want to start an empty database. I do just this in Chapter 3
as follows:
$ cockroach demo \
--no-example-database \
--nodes 9 \
--demo-locality=region=us-east1,az=a:region=us-east1,az=b:region=us-
east1,az=c:region=asia-northeast1,az=a:region=asia-
northeast1,az=b:region=asia-northeast1,az=c:region=europe-
west1,az=a:region=europe-west1,az=b:region=europe-west1,az=c
1
www.tpc.org/tpcc
2
https://github.com/brianfrankcooper/YCSB
70
Chapter 4 Managing CockroachDB from the Command Line
All three types of certificates (and any related keys) are listed in the response, along
with their expiry dates and accompanying notes.
This command omits the user, port, and database elements of the URL; the following
command will do exactly the same as the above:
To connect to a different database, either change the database portion of the URL or
provide a -d or --database argument as follows:
$ cockroach sql \
-d defaultdb \
--url "postgresql://root@localhost:26257?sslmode=disable"
71
Chapter 4 Managing CockroachDB from the Command Line
You may wish to run a command against a CockroachDB cluster without keeping a
SQL shell open. This can be achieved with the --execute/-e argument:
You can harness the --execute argument to perform any database operation against
a cluster. It’s even possible to extract data by piping the output from a statement into
another command. The following statements use the --execute argument to create a
table, insert data into it, and extract the data into a JSON file, all without opening a long-
lived SQL shell:
$ cat names.csv
first_name,last_name
Ben,Darnell
Peter,Mattis
Spencer,Kimball
If you’d like to watch for changes, you can pass the --watch argument in conjunction
with the --execute argument. The following command will get the current database
time once per second:
72
Chapter 4 Managing CockroachDB from the Command Line
Time: 1ms
now
---------------------------------
2021-10-31 19:45:35.882499+00
(1 row)
Time: 1ms
$ cockroach start \
--insecure \
--store=node1 \
--listen-addr=localhost:26257 \
--http-addr=localhost:8080 \
--join=localhost:26257,localhost:26258,localhost:26259
$ cockroach start \
--insecure \
--store=node2 \
--listen-addr=localhost:26258 \
--http-addr=localhost:8081 \
--join=localhost:26257,localhost:26258,localhost:26259
73
Chapter 4 Managing CockroachDB from the Command Line
$ cockroach start \
--insecure \
--store=node3 \
--listen-addr=localhost:26259 \
--http-addr=localhost:8082 \
--join=localhost:26257,localhost:26258,localhost:26259
$ cockroach start \
--insecure \
--store=node4 \
--listen-addr=localhost:26260 \
--http-addr=localhost:8083 \
--join=localhost:26257,localhost:26258,localhost:26259
$ cockroach start \
--insecure \
--store=node5 \
--listen-addr=localhost:26261 \
--http-addr=localhost:8084 \
--join=localhost:26257,localhost:26258,localhost:26259
Next, let’s get some more information from each node using the status
subcommand. Note that because there’s a lot more information being returned, I’m not
using the default value for --format and I’m instead returning the results as records:
74
Chapter 4 Managing CockroachDB from the Command Line
-[ RECORD 1 ]
id | 1
address | localhost:26257
sql_address | localhost:26257
build | v21.1.7
started_at | 2021-11-01 15:13:11.166798
updated_at | 2021-11-01 15:15:39.681848
locality |
is_available | true
is_live | true
-[ RECORD 2 ]
...
The status subcommand provides more than just top-level information; use the
following commands to expose additional node information:
For each of the preceding commands, you can also display status information for
individual nodes by passing a node ID as follows:
75
Chapter 4 Managing CockroachDB from the Command Line
In the event that we need to take a node out of a cluster to perform maintenance, the
drain subcommand will prevent new clients from connecting to the node and rebalance
its range leases to other nodes in the cluster:
Figure 4-2 shows that the node we drained is now showing as “dead” in the admin
console, indicating that it is not a functioning node in the cluster anymore:
Once drained, a node is safe to decommission. This can be achieved with the
decommission subcommand as follows; note that the command is followed by the ID of
the node you wish to decommission:
The output of this command will show the replicas being reduced until there are no
replicas remaining on the node. The node is then removed from the cluster and will no
longer appear in the admin console.
Figure 4-3 shows how the state of the decommissioned node transitions to
“DECOMMISSIONING” during the process of being decommissioned.
76
Chapter 4 Managing CockroachDB from the Command Line
In the event that a node is fully decommissioned, you can start the node again by
first deleting its old storage directory and reissuing the start command as follows:
$ rm -rf node4
$ cockroach start \
--insecure \
--store=node4 \
--listen-addr=localhost:26260 \
--http-addr=localhost:8083 \
--join=localhost:26257,localhost:26258,localhost:26259
77
Chapter 4 Managing CockroachDB from the Command Line
The resulting node will have a new node ID, reflecting that this is not simply a
recommissioning of an old node.
$ cockroach sqlfmt \
-e "SELECT first_name, last_name, date_of_birth FROM person WHERE id
= '1c448ac9-73a9-47c5-9e4d-769f8aab27fd';"
SELECT
first_name, last_name, date_of_birth
FROM
person
WHERE
id = '1c448ac9-73a9-47c5-9e4d-769f8aab27fd'
78
Chapter 4 Managing CockroachDB from the Command Line
As the statement contains 160 characters (100 more than the default print width of
60), sqlfmt reformats the command so that the print width of the statement remains
below 60 characters. To change the print width, pass a value to the --print-width
argument.
If you prefer your SQL logic to stay on the same line as the keywords (FROM, etc.), pass
the --align argument:
$ cockroach sqlfmt \
-e "SELECT first_name, last_name, date_of_birth FROM person WHERE id
= '1c448ac9-73a9-47c5-9e4d-769f8aab27fd';" \
--align
If you SQL statement contains quotes, you can wrap it in triple quotes as follows:
$ cockroach sqlfmt \
-e """SELECT p."first_name", p."last_name", p."date_of_birth",
a."name" FROM "person" p JOIN "animal" a on p.id = a."owner_id" WHERE
p."id" = '1c448ac9-73a9-47c5-9e4d-769f8aab27fd';"""
SELECT
p.first_name, p.last_name, p.date_of_birth, a.name
FROM
person AS p JOIN animal AS a ON p.id = a.owner_id
WHERE
p.id = '1c448ac9-73a9-47c5-9e4d-769f8aab27fd'
Note that the sqlfmt command does not consider the quotes to be necessary, so
it removes them before outputting the result. It will also remove other superfluous
characters from the input statement such as unnecessary brackets:
$ cockroach sqlfmt \
-e "SELECT first_name, last_name, date_of_birth FROM person WHERE
id IN (('1c448ac9-73a9-47c5-9e4d-769f8aab27fd'),('652cfbbc-52a9-42be-
a73f-32fc7604b7e9'));" \
--align \
--use-spaces
79
Chapter 4 Managing CockroachDB from the Command Line
80
Chapter 4 Managing CockroachDB from the Command Line
_elapsed___errors__ops/sec(inst)___ops/sec(cum)__p50(ms)__p95(ms)__p99(ms)_
pMax(ms)
1.0s 0 69.7
70.0 35.7 436.2 872.4 973.1 transfer
2.0s 0 169.0
119.5 46.1 385.9 1610.6 1879.0 transfer
3.0s 0 227.5
155.6 41.9 151.0 738.2 2281.7 transfer
4.0s 0 247.2
178.4 44.0 159.4 805.3 872.4 transfer
...
Figure 4-4 shows load being generated against the bank database in the
CockroachDB admin console.
By default, the load generator will run 16 concurrent workers. This can be changed
depending on your requirements using the --concurrency flag. If you would like to
simulate higher load, increase the concurrency.
81
CHAPTER 5
Interacting with
CockroachDB
We’ve covered a lot of conceptual ground, and it’s now time to start using CockroachDB
as an end user. We’ve created clusters and tables; now, let’s connect to them and put
them to work.
Connecting to CockroachDB
When presented with a new database, the first thing you might want to do is connect
to it and start querying! In this section, we’ll connect to self-hosted and cloud-based
CockroachDB clusters with tools and from code.
Connecting with Tools
There are plenty of off-the-shelf tools you can use to connect to a CockroachDB cluster.
Some are free, and some you’ll pay to use.
Here are some of the popular off-the-shelf tools for interacting with CockroachDB. I
will be using DBeaver Community, but both DataGrip and TablePlus have excellent
support for CockroachDB:
• DBeaver – https://dbeaver.com
• DataGrip – www.jetbrains.com/datagrip
• TablePlus – https://tableplus.com
83
© Rob Reid 2022
R. Reid, Practical CockroachDB, https://doi.org/10.1007/978-1-4842-8224-3_5
Chapter 5 Interacting with CockroachDB
Let’s create a CockroachDB cluster with the following command and use DBeaver
to connect to it. Note that without the --insecure flag, CockroachDB generates a
username and password. I’ll use these in DBeaver. Please note that I’ve omitted a lot of
the command output for brevity:
Now it’s time to open DBeaver and connect to the database. Take note of the
username and password in the preceding output. Figure 5-1 shows the configuration
used to connect to this cluster using these values.
84
Chapter 5 Interacting with CockroachDB
Note that port 26257 is the default port used by CockroachDB for database
connections. As I’ve not altered the default port, I’m using 26257 as the connection port
number here.
Now, let’s create a table, insert some data into it, and select the data out to see how
this looks in DBeaver. Figure 5-2 shows what working with data looks like using DBeaver.
85
Chapter 5 Interacting with CockroachDB
The database we’ve connected to is basic and does not use client certificates. I’ll now
connect to a Cockroach Cloud database to show you how to do this with DBeaver.
Unlike a local demo database, a free-tier Cockroach Cloud database requires a
certificate and an argument to tell Cockroach Cloud which cluster to connect to (it’s
multitenant, meaning clusters for different users are co-hosted).
Figure 5-3 shows the additional configuration values required by a Cockroach Cloud
database. Namely, the host field has been updated to point to the Cockroach Cloud
instance, and the database field now includes the information Cockroach Cloud requires
to locate your cluster.
86
Chapter 5 Interacting with CockroachDB
In addition to the basic settings, you’ll need to help DBeaver find the certificate that
will authenticate your connection. Figure 5-4 shows the two additional configuration
values you’ll need to set.
87
Chapter 5 Interacting with CockroachDB
Connecting Programmatically
You can connect to CockroachDB from many different programming languages.
Cockroach Labs lists the available drivers on their website1 along with example
applications for each.2
To give you a good introduction to working with CockroachDB programmatically, I’ll
perform some basic operations against a database using some popular drivers now.
1
www.cockroachlabs.com/docs/stable/third-party-database-tools#drivers
2
www.cockroachlabs.com/docs/stable/example-apps
88
Chapter 5 Interacting with CockroachDB
The examples that follow show how to connect to CockroachDB and get results using
several common programming languages. In each case, I’ve chosen succinctness over
completeness in the interest of brevity.
Go Example
In this example, I’ll connect to the “defaultdb” database and perform an INSERT
and SELECT against the “person” table. I’ll be using a popular Go driver for Postgres
called pgx.
First, let’s initialize the environment:
$ mkdir go_example
$ cd go_example
$ go mod init go_example
Next, we’ll create a simple application in a main.go file. I’ll share the key building
blocks of the application here, as the complete code listing will be available on GitHub.
First, fetch and import the pgx package:
$ go get github.com/jackc/pgx/v4
import "github.com/jackc/pgx/v4/pgxpool"
Next, connect to the database and ensure that the connection is closed after use:
89
Chapter 5 Interacting with CockroachDB
postgresql://<USERNAME>:<PASSWORD>@<HOST>:<PORT>/<DB_NAME>?sslmode=verify-
full&sslrootcert=<PATH_TO_CERT>&options=--cluster%3D<CLUSTER_NAME>
Now we’re ready to insert data. The following statement inserts three names into the
“people” table:
90
Chapter 5 Interacting with CockroachDB
$ go run main.go
2021/11/11 09:54:46 2917ee7b-3e26-4999-806a-ba799bc515b3 Wang Zhenyi
2021/11/11 09:54:46 5339cd29-77d5-4e01-966f-38ac7c6f9fbc Annie Easley
2021/11/11 09:54:46 85288df9-25fe-439a-a670-a8e8ea70c7db Valentina
Tereshkova
Python Example
Next, we’ll create a Python application to do the same thing, connect to the “defaultdb”
database, INSERT into, and SELECT out of.
First, we’ll initialize the environment:
$ mkdir python_example
$ python3 -m pip install --no-binary :all: psycopg2
With the psycopg2 dependencies installed, we’re ready to write our application. Let’s
import the dependency and create a connection to the database:
import psycopg2
conn = psycopg2.connect(
dbname="defaultdb",
user="demo",
password="demo9315",
port=26257,
host="localhost",
)
It’s a matter of preference whether you use the connect function that takes a
connection string/Data Source Name (DSN) or separate variables for each of the
connection elements.
Next, we’ll insert some data:
91
Chapter 5 Interacting with CockroachDB
psycopg2’s fetchall command returns a list of tuples, one for each row returned. We
can access the column values for each tuple by their index.
Ruby Example
Onto Ruby! Let’s initialize the environment:
$ mkdir ruby_example
$ cd ruby_example
$ gem install pg
We’re ready to create an application. First, we’ll bring in the pg gem and create a
connection to the database:
require 'pg'
conn = PG.connect(
user: 'demo',
password: 'demo9315',
dbname: 'defaultdb',
host: 'localhost',
92
Chapter 5 Interacting with CockroachDB
port: "26257",
sslmode: 'require'
)
Next, let’s insert some data. We’ll reuse the person table:
conn.transaction do |tx|
tx.exec_params(
'INSERT INTO person (name) VALUES ($1), ($2), ($3)',
[['Ada Lovelace'], ['Alice Ball'], ['Rosalind Franklin']],
)
end
Queries that take parameters should always be parameterized. In Ruby, you pass
parameters into a query using the exec_params function. I then pass a multidimensional
array as a parameter, with each array containing the fields of a row to insert.
Let’s read the data out. The exec/exec_params function can also be used to return
data. In the following data, I pass a block into which the results of the query are available
as rows. For each of the rows returned, I fetch the “id” and “name” columns:
conn.transaction do |tx|
tx.exec('SELECT id, name FROM person') do |result|
result.each do |row|
puts row.values_at('id', 'name')
end
end
end
$ ruby main.rb
2bef0b0a-3b57-4f85-b0a2-58cf5f6ab7e4
["Ada Lovelace"]
5096e818-3236-4697-b919-8695fde1581d
["Rosalind Franklin"]
cf5b1f2d-f4af-4aab-8c67-3a4ced6f6c07
["Alice Ball"]
93
Chapter 5 Interacting with CockroachDB
Crystal Example
Crystal’s turn! I’ve been using Crystal for a number of personal projects recently
(including projects backed by CockroachDB), so I wanted to make sure I covered it.
Let’s prepare the environment:
dependencies:
pg:
github: will/crystal-pg
With the dependency in our shard files, the shards command will know what to
fetch. Invoke it now to bring in our database driver:
$ shards install
require "db"
require "pg"
db = PG.connect "postgresql://demo:demo43995@localhost:26257/
defaultdb?auth_methods=cleartext"
94
Chapter 5 Interacting with CockroachDB
C# Example
Onto C#. Let’s prepare the environment. For this example, I’ll be using .NET 6 with C# 10:
Next, we’ll bring in some NuGet package dependencies. Note that Dapper is not a
required dependency for working with CockroachDB; it’s just a preference in this case:
Now for the code. I’m omitting exception handling to keep the code succinct:
using Dapper;
using System.Data.SqlClient;
using Npgsql;
95
Chapter 5 Interacting with CockroachDB
$ dotnet run
4e363f35-4d3b-49dd-b647-7136949b1219 Christine Lagarde
5eecb866-a07f-47b4-9b45-86628864e778 Jacinda Ardern
7e466bd2-1a19-465f-9c70-9fb5f077fe79 Jacinda Ardern
Designing Databases
The database schemas we’ve designed thus far have remained purposefully simple to
help demonstrate specific database features like data types. The database schemas we’ll
create in this section will be a more accurate reflection of what you’ll create in the wild.
Database Design
We’ll start by looking at CockroachDB’s topmost object: the database. Up to this point,
we’ve created tables against the defaultdb for simplicity. It’s now time to create our own
database.
An important decision to make before creating your database is where it will live.
Will it be located in a single region, or will it span multiple regions?
If your database is to remain within a single region, it can be created as follows:
96
Chapter 5 Interacting with CockroachDB
The preceding command will return an error if a database called “db_name” already
exists. The following command will create a database only if a database with the same
name does not already exist:
$ cockroach demo \
--no-example-database \
--nodes 12 \
--insecure \
--demo-locality=region=us-east1,az=a:region=us-east1,az=b:region=us-
east1,az=c:region=europe-north1,az=a:region=europe-
north1,az=b:region=europe-north1,az=c:region=europe-
west1,az=a:region=europe-west1,az=b:region=europe-west1,az=c:region=europe-
west3,az=a:region=europe-west3,az=b:region=europe-west3,az=c
Fetching the regions confirms that we created our expected cluster topology:
Let’s create a database that spans the European regions and see how that affects our
regions view:
The results of SHOW REGIONS confirm that our “db_name” database is running in the
European regions and has a primary region of “europe-west1”, just as we configured.
The SHOW REGIONS command takes additional arguments to further narrow down
our search. The following command shows just the regions for the “db_name” database:
Schema Design
If you need to logically separate database objects such as tables, then creating a user-
defined schema is a good choice. In the following example, we’ll assume that we’ve
made the decision to use user-defined schemas to logically separate our database
objects. The use of custom schemas is optional. If you don’t specify one yourself, the
default “public” schema will be used.
In this example, we’re building a database to support a simple online retail business.
Supporting this online business are the following business areas:
In a business like this, schemas make a lot of sense, as it’s feasible that each of the
business areas will need an “orders” table. The retail team will need to capture customer
orders, the manufacturing team will need to capture orders for raw materials, and
finance would like to capture transactions made on company cards as orders.
98
Chapter 5 Interacting with CockroachDB
Let’s create a database with some schemas to see how we might harness schema
design to give us a flexible system. First, we’ll create a database:
Next, we’ll create some users to represent people in each of the three business areas:
Finally, we’ll create some schemas. The following statements create three schemas,
one for each business area. For the retail and manufacturing schemas, we’ll give access
to the finance user, as they will need to view data from tables in both schemas:
With users, schemas, and grants created, let’s double-check the grants using the SHOW
GRANTS command:
Let’s connect to the database as the retail_user and create an orders table in the retail
schema. Note that this is the only schema we’ll be able to do this in while connected as
the retail_user:
$ cockroach sql \
--url "postgres://retail_user@localhost:26257/acme?sqlmode=disable" \
--insecure
CREATE TABLE retail.orders(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
reference TEXT NOT NULL
);
$ cockroach sql \
--url "postgres://manufacturing_user@localhost:26257/
acme?sqlmode=disable" \
--insecure
CREATE TABLE manufacturing.orders(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
reference TEXT NOT NULL
);
$ cockroach sql \
--url "postgres://finance_user@localhost:26257/
acme?sqlmode=disable" \
--insecure
CREATE TABLE finance.orders(
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
reference TEXT NOT NULL
);
100
Chapter 5 Interacting with CockroachDB
Let’s reconnect to the database as the root user and view the tables we’ve created:
$ cockroach sql \
--url "postgres://root@localhost:26257/acme?sqlmode=disable" \
--insecure
Table Design
There are plenty of considerations to make when creating tables in any database, and
CockroachDB is no exception. Among other things, badly named columns, incorrect use
of data types, missing (or redundant) indexes, and badly configured primary keys can
hurt readability and performance.
In this section, we’ll take a look at some table design best practices.
Naming
It’s important to know where CockroachDB will create a table, so it’s vitally important
that you provide a name that instructs CockroachDB exactly where to place it.
Where do you think CockroachDB will place the following table?
If you said any variation of ¯\_(ツ)_/¯, you’re not far off. The following outcomes
could result:
101
Chapter 5 Interacting with CockroachDB
In short, creating a table like this is ill-advised when working with production-scale
systems. Providing the fully qualified name of the table is so important. In the following
example, we’ve removed any uncertainty about where the table should live:
Always give your tables descriptive names. The choice of singular vs. plural table
names and whether you decide to PascalCase, camelCase, snake_case, or SPoNgEBOb
case is yours to make. However, whatever you decide, it’s always a good idea to remain
consistent with your naming convention.
Here’s a list of potential issues I can identify in the design of this table:
• The ID column uses the SERIAL data type, which only exists for
Postgres compatibility. In most scenarios, it’s better to use the UUID
data type, whose values distribute more evenly across ranges.
102
Chapter 5 Interacting with CockroachDB
• The is_admin column can also store a large number, but the
main problem with this column being of type INT is that there
exists a better data type to convey the Boolean semantics of this
column: BOOL.
• An ENUM data type for the primary_role column may better fit, given
we’d have a finite range of values.
Here’s the table again but this time, with the issues I’ve identified resolved:
In summary:
• There’s usually a data type that is well suited to the data you’d like
to store.
103
Chapter 5 Interacting with CockroachDB
Indexes
Use indexes to improve the performance of SELECT queries by giving CockroachDB
hints as to where it should look in a dataset for the value(s) you’re requesting.
CockroachDB distinguishes between two types of indexes:
In the following example, I’m creating a table with no primary or secondary indexes:
Let’s run a query to see how our table looks like in CockroachDB:
104
Chapter 5 Interacting with CockroachDB
As you can see from the aforementioned, CockroachDB has generated a fourth
column called rowid as a substitute for our missing primary key column. Values for this
column are auto-incrementing but not necessarily sequential. As can be seen with a call
to unique_rowid():
difference
--------------
32768
To see what CockroachDB has generated for us, I’ll run another command to
generate the table’s CREATE statement:
table_name | create_statement
-------------+-----------------------------------------------------------
person | CREATE TABLE public.person (
| id UUID NOT NULL DEFAULT gen_random_uuid(),
| date_of_birth TIMESTAMP NOT NULL,
| pets STRING[] NULL,
| rowid INT8 NOT VISIBLE NOT NULL DEFAULT unique_rowid(),
| CONSTRAINT "primary" PRIMARY KEY (rowid ASC),
| FAMILY "primary" (id, date_of_birth, pets, rowid)
| )
Let’s create the table again but this time, provide a primary key of our own:
Now that the ID column in our table is the primary key, CockroachDB has not
created its own primary key for us:
With the table in place, let’s insert some data into it and see how CockroachDB
searches the table when queried:
distribution: full
vectorized: true
• sort
│ estimated row count: 0
│ order: +date_of_birth
│
└── • filter
│ estimated row count: 0
│ filter: ((date_of_birth >= '1979-01-01 00:00:00') AND (date_of_
birth <= '1997-12-31 00:00:00')) AND (pets <@ ARRAY['Mr. Dog'])
│
└── • scan
106
Chapter 5 Interacting with CockroachDB
Uh oh! CockroachDB has had to perform a full scan of the entire person table
to fulfill our query. Queries will get linearly slower with every new row added to the
database.
Secondary indexes to the rescue! Let’s recreate the table with indexes on the columns
we’ll filter and sort on. In our case, that’s the date_of_birth and the pets column:
INDEX (date_of_birth),
INVERTED INDEX (pets)
);
distribution: local
vectorized: true
• filter
│ filter: pets <@ ARRAY['Mr. Dog']
│
└── • index join
│ table: person@primary
│
└── • scan
missing stats
table: person@person_date_of_birth_idx
spans: [/'1979-01-01 00:00:00' - /'1997-12-31 00:00:00']
107
Chapter 5 Interacting with CockroachDB
Success! Our query no longer requires a full table scan. Note what happens, however,
if we order the results in descending order of date_of_birth:
distribution: full
vectorized: true
• sort
│ order: -date_of_birth
│
└── • filter
│ filter: pets <@ ARRAY['Mr. Dog']
│
└── • index join
│ table: person@primary
│
└── • scan
missing stats
table: person@person_date_of_birth_idx
spans: [/'1979-01-01 00:00:00' - /'1997-12-31 00:00:00']
CockroachDB has had to sort the results manually for us before returning them
because the indexes on the table sort in ascending order by default and we’ve asked for
them in descending order.
Suppose you know that CockroachDB may return the results of a query in either
ascending or descending order by a column. In that case, it’s worth considering adding
indexes for both scenarios as follows:
108
Chapter 5 Interacting with CockroachDB
With this in place, CockroachDB knows how to return query results that are
ordered by date_of_birth in ascending or descending order without having to manually
sort them.
If you’re indexing columns containing sequential data, such as TIMESTAMP or
incrementing INT values, you may want to consider using hash-sharded indexes.3
Hash-sharded indexes ensure that indexed data is distributed evenly across ranges,
which prevent single-range hotspots. Single-range hotspots occur when multiple similar
values are queried and exist in the same range.
At the time of writing, hash-sharded indexes are still experimental, so they need to
be enabled as follows:
Under the covers, the values in the index will be an FNV hash representation of the
data you’re indexing. Any small changes to the input data may result in a different hash
output and a different index bucket. The table can now scale more evenly across the
nodes in your cluster, resulting in increased performance.
View Design
A view is simply a SELECT query that you assign a name to ask CockroachDB to
remember. CockroachDB supports three types of views:
3
www.cockroachlabs.com/docs/stable/hash-sharded-indexes
109
Chapter 5 Interacting with CockroachDB
your table, for query performance, and if the data in that table does
not update frequently. If data frequently updates, your results might
be stale.
If you need to restrict table data to specific users or have complex queries you’d like
to expose as simpler ones, views are a good choice. Let’s create some views to see what
they can offer us.
Simplify Queries
Suppose you have a query whose complexity you’d like to hide (or simply not have to
rewrite). You can create a view over this data to expose a simpler query.
Let’s assume we’re running an online shop and would like a query to return the
number of purchases by day. Granted, this is a straightforward query, but for the sake of
argument, we’ll wrap this in a view to demonstrate the concept.
First, we’ll create the tables:
INDEX(email)
);
110
Chapter 5 Interacting with CockroachDB
INDEX(sku)
);
Next, we’ll insert some data into the tables to give us something to work with:
Finally, we’ll create the view. This view will be a simple selection on the purchase
table, along with a grouping over the extracted day-of-week value of the checkout_
at column:
111
Chapter 5 Interacting with CockroachDB
With this in place, we can now work against the view, which hides the complexity of
the date manipulation:
$ mkdir certs
$ mkdir keys
$ cockroach start-single-node \
--certs-dir=certs \
--store=node1 \
--listen-addr=localhost:26257 \
--http-addr=localhost:8080
112
Chapter 5 Interacting with CockroachDB
First, we’ll create the products table and insert some products into it:
Next, we’ll create a view to return all products for customers in the UK:
Finally, we’ll create a user and grant them access to the view. We’ll assume the
following user will serve UK customers:
Let’s connect to the database as the new user and see what we can (and
can’t) access:
$ cockroach sql \
--certs-dir=certs \
--url "postgres://user_gbr:some_password@localhost:26257/defaultdb"
113
Chapter 5 Interacting with CockroachDB
We’re in business! Our user_gbr user has no access to query the product table
directly but does have access to its data via the product_gbr view, which exposes only the
products we’d like them to see.
Moving Data
In this section, we’ll explore some use cases that involve moving data into and out of
databases via the IMPORT and EXPORT statements and CDC (Change Data Capture).
With a cluster in place, we’ll create a database with two tables: one table to export
data out of and another table to import data into. We’ll also insert some data to export:
114
Chapter 5 Interacting with CockroachDB
Next, we’ll need somewhere to export our data. CockroachDB supports cloud
provider destinations such as Amazon S3, Azure Storage, and Google Cloud Storage and
allows you to export to self-hosted destinations.
For this example, I’ve created a basic HTTP server that I’m hosting in replit.com
under https://replit.com/@robreid/importexport. You can sign up for a free Replit
account and fork this Repl to test your own exports/imports.
With a server up and running and available at https://importexport.robreid.
repl.co, I can export data as follows (you will have a different URL in the form of
https://importexport.YOUR_USERNAME.repl.co):
filename
| rows | bytes
-------------------------------------------------------------------+-----
-+-------
export16b4ae42664118980000000000000001-n707964976657694721.0.csv
| 3 | 6
We can confirm that the file has been successfully ingested by the server but joining
the address of the running Repl with the file name in the preceding response:
$ curl https://importexport.robreid.repl.co/export16b4ae42664118980000000
000000001-n707964976657694721.0.csv
A
B
C
115
Chapter 5 Interacting with CockroachDB
'https://importexport.robreid.repl.co/export16b4ae426641189800000000
00000001-n707964976657694721.0.csv'
);
CockroachDB also supports importing data from Avro files, TSV and other delimited
files, and CockroachDB, MySQL, and Postgres dump files. I recommend using the
IMPORT statement in the following scenarios:
Kafka Sink
The Kafka CDC sink was the first to appear in CockroachDB, making its first appearance
in v2.1.
In this example, I’ll use Redpanda, a streaming platform that’s wire-compatible with
Kafka (in the same way that CockroachDB is wire-compatible with Postgres). Let’s start
an instance of Redpanda, create a topic, and consume from it:
$ docker run \
--rm -it \
116
Chapter 5 Interacting with CockroachDB
--name redpanda \
-p 9092:9092 \
-p 9644:9644 \
docker.vectorized.io/vectorized/redpanda:latest \
redpanda start \
--overprovisioned \
--smp 1 \
--memory 1G \
--reserve-memory 0M \
--node-id 0 \
--check=false
Moving back to Cockroach, let’s create a simple insecure demo cluster with an
example table to stream data from:
With the table in place, let’s create a Kafka Changefeed to start publishing changes:
Note that in order to create a Changefeed for multiple tables, simply provide a
comma-separated string containing the tables you’d like to monitor as follows:
117
Chapter 5 Interacting with CockroachDB
The only thing required to get data into Kafka now is to use your tables as normal. All
changes to the data in our example table will be automatically published to Kafka. Let’s
INSERT, UPDATE, and DELETE some data now and see what CockroachDB publishes to
the cdc_example topic:
UPDATE example
SET value = 'b'
WHERE id = '4c0ebb98-7f34-436e-9f6b-7ea1888327d9';
You’ll start to see the events received from CockroachDB via the Redpanda
consumer at this point. The first message received represents the change introduced by
the INSERT statement, the second message was published after the UPDATE statement,
and the last message shows the row being deleted as a result of the DELETE statement:
{
"topic": "cdc_example",
"key": "[\"4c0ebb98-7f34-436e-9f6b-7ea1888327d9\"]",
"value": "{\"after\": {\"id\": \"4c0ebb98-7f34-436e-9f6b-7ea1888327d9\",
\"value\": \"a\"}, \"updated\": \"1637755793947605000.0000000000\"}",
"timestamp": 1637755794706,
"partition": 0,
"offset": 44
}
{
"topic": "cdc_example",
"key": "[\"4c0ebb98-7f34-436e-9f6b-7ea1888327d9\"]",
"value": "{\"after\": {\"id\": \"4c0ebb98-7f34-436e-9f6b-7ea1888327d9\",
\"value\": \"b\"}, \"updated\": \"1637755826616641000.0000000000\"}",
"timestamp": 1637755826791,
"partition": 0,
118
Chapter 5 Interacting with CockroachDB
"offset": 45
}
{
"topic": "cdc_example",
"key": "[\"4c0ebb98-7f34-436e-9f6b-7ea1888327d9\"]",
"value": "{\"after\": null, \"updated\": \"163775601102495500
0.0000000000\"}",
"timestamp": 1637756011195,
"partition": 0,
"offset": 46
}
To stop a Changefeed, you need to locate and delete the job that’s running it. Let’s
find the job and cancel it now:
job_id | job_type | description
---------------------+------------+--------------------------------------
713311406086291457 | CHANGEFEED | CREATE CHANGEFEED FOR TABLE example
INTO 'kafka://localhost:9092?topic_name=cdc_example' WITH updated
Webhook Sink
Let’s redirect our CDC events to a web server now. Owing to the potentially sensitive
nature of the data in your tables, CockroachDB will only send data to HTTPS-enabled
servers. Like the previous export/import example, I’ll reuse replit.com to create and run
a free HTTPS server. Let’s create and run a simple HTTPS server:
package main
import (
"io/ioutil"
"log"
"net/http"
)
119
Chapter 5 Interacting with CockroachDB
func main() {
http.HandleFunc("/", cdc)
log.Fatal(http.ListenAndServe(":9090", nil))
}
log.Println(string(event))
}
Finally, let’s generate some events with INSERT, UPDATE, and DELETE statements:
120
Chapter 5 Interacting with CockroachDB
Switching back to replit.com, our server is now receiving events for CockroachDB
changes:
121
CHAPTER 6
Data Privacy
Data privacy should be a concern for businesses great and small, regardless of where
they are in the world. While the laws affecting the penalties for poorly implemented
security can vary from country to country, the safeguarding of users is of paramount
importance.
In this topic, we’ll take a closer look at how CockroachDB can support you in
creating a secure and compliant infrastructure for your data.
Global Regulations
There is no one data privacy law that governs the entire world. Each country has its own
legislation, which it can revise over time. This makes for a forever-changing landscape
that we as engineers need to keep up and remain compliant with. The DLA Piper law
firm maintains an excellent site that provides a country-by-country overview of this
landscape: www.dlapiperdataprotection.com. I will be using DLA Piper’s research as
my starting point for the content in this section.
What I present in this section should not be considered legal advice. Before creating
a production CockroachDB cluster for customer data, please consult a data privacy
specialist.
Here are a few headlines to give you a sense of the complexity of this compliance
challenge:
123
© Rob Reid 2022
R. Reid, Practical CockroachDB, https://doi.org/10.1007/978-1-4842-8224-3_6
Chapter 6 Data Privacy
• The GDPR sets the age of consent for data collection and processing
at 16 but allows European member states to lower that if they wish. As
a result, there is no one definition of a child regarding data protection
in Europe.2
The list of regulations and their differences is beyond the scope of this book.
However, I hope that by sharing the preceding points, I’ve highlighted the importance
of understanding the data privacy laws in any country where you are operating or from
which you will have users whose data you will collect or process.
Your responsibilities and liabilities will change, depending on whether you are
considered a processor, a controller, or a joint controller of data, so it’s important to
understand the difference between the two. In the UK, the Information Commissioner's
Office (ICO) has a useful checklist to help you identify your role.3 In short, however, if
you have decided to collect data and know how and why you are processing it, you are a
controller.
1
www.cookiebot.com/en/ccpa-vs-gdpr-compliance-with-cookiebot-cmp
2
www.clarip.com/data-privacy/gdpr-child-consent
3
https://ico.org.uk/for-organisations/guide-to-data-protection/
guide-to-the-general-data-protection-regulation-gdpr/key-definitions/
controllers-and-processors/
124
Chapter 6 Data Privacy
Your responsibilities and liabilities will also change depending on the type of
data you are processing. If you store healthcare data (e.g., patient names and health
conditions), you will be subject to more scrutiny than if you store customer data (e.g.,
customer email and physical addresses) in an online retail setting.
As a rule of thumb, if you are operating across multiple countries, it’s wise to comply with
the country with the most stringent data privacy laws and apply this across all countries.
Additionally, if two countries have conflicting rules, you may need to implement rules
for each country on a customer-by-customer basis, depending on their home country/state.
Although navigating the complex waters of data privacy remains an important
consideration for all database users, CockroachDB’s encryption in transit, encryption at
rest, and geo-partitioning functionally make the complexity easier to manage.
Location-Specific Considerations
In this section, we’ll take a closer look at the considerations you’ll need to make when
operating across geographies and under different circumstances that will affect your
compliance responsibilities.
4
https://ec.europa.eu/commission/presscorner/detail/en/ip_21_3183
125
Chapter 6 Data Privacy
• Process all customer data in the EU – For the same reason that
housing all data within a single UK region will keep costs down and
infrastructure simple, housing all data within a single EU region can
also be a good option.
126
Chapter 6 Data Privacy
Each of these options has benefits and trade-offs. I would either run a single geo-
partitioned cluster or multiple isolated clusters in this scenario.
5
www.nytimes.com/wirecutter/blog/state-of-privacy-laws-in-us
6
www.brabners.com/blogs/why-does-it-matter-which-country-i-store-my-data
127
Chapter 6 Data Privacy
States, it would be acceptable to take an anonymized copy of EU customer data and store
that in the United States.
There are many ways we could design a compliant database infrastructure for this
scenario. Let’s review two options:
7
www.wired.co.uk/article/china-personal-data-law
8
https://digichina.stanford.edu/work/translation-outbound-data-transfer-security-
assessment-measures-draft-for-comment-oct-2021/
9
www.alibabacloud.com/icp
128
Chapter 6 Data Privacy
Encryption
With global regulations and PII considered, the next line of defense for data is
encryption. Properly implemented, encryption makes it all but impossible for a bad
actor to understand and use your data for malicious purposes. In this section, we’ll cover
data encryption in CockroachDB.
At a high level, there are two areas to focus on when using encryption:
129
Chapter 6 Data Privacy
In Transit
By default, CockroachDB will attempt to start securely and look for certificates and keys
to use on startup. If you try to run the following command without generating certificates
or keys, CockroachDB will fail to start:
$ cockroach start-single-node
*
* ERROR: ERROR: cannot load certificates.
* Check your certificate settings, set --certs-dir, or use --insecure for
insecure clusters.
*
* failed to start server: problem using security settings: no certificates
found; does certs dir exist?
*
...
$ mkdir certs
$ mkdir keys
Next, we’ll ask CockroachDB to generate a Certificate Authority (CA) certificate and a
private key. We’ll use these files to create certificates for nodes in subsequent steps:
130
Chapter 6 Data Privacy
$ ls certs
ca.crt
$ ls keys
ca.key
Next, we’ll ask CockroachDB to generate a certificate and a public/private key pair
for the cluster nodes:
$ ls certs
ca.crt node.crt node.key
$ ls keys
ca.key
The “cert -> create-node” command earlier takes a variadic set of host arguments to
aid in node discovery. I’m running everything locally in this example, so passing a single
value of “localhost” is sufficient.
Now let’s start some CockroachDB nodes using the certificate and key we generated.
Previously, CockroachDB returned an error when we attempted to do this, but now we
have the files it requires to start securely, so it will start without issue:
$ cockroach start \
--certs-dir=certs \
--store=node1 \
--listen-addr=localhost:26257 \
--http-addr=localhost:8080 \
--join=localhost:26257,localhost:26258,localhost:26259
$ cockroach start \
--certs-dir=certs \
--store=node2 \
--listen-addr=localhost:26258 \
--http-addr=localhost:8081 \
--join=localhost:26257,localhost:26258,localhost:26259
131
Chapter 6 Data Privacy
$ cockroach start \
--certs-dir=certs \
--store=node3 \
--listen-addr=localhost:26259 \
--http-addr=localhost:8082 \
--join=localhost:26257,localhost:26258,localhost:26259
Let’s connect to our cluster. As this is a secure cluster, we’ll need to generate and
provide a certificate to authenticate users. We’ll generate a client certificate for the
database root user and use that to connect. First, generate the root client certificate:
We can now initialize the nodes and finish the cluster setup:
Finally, connect to the cluster as the root user using the client certificate we
generated:
132
Chapter 6 Data Privacy
At Rest
Encryption at rest is an Enterprise feature of CockroachDB. It will encrypt data on-disk
transparently, meaning a user with proper access to the data will never be aware that
encryption took place.
CockroachDB uses the Advanced Encryption Standard (AES) algorithm to encrypt
data with a key size of either 128, 192, or 256 bits.
To generate an encryption key, simply invoke the gen command of the cockroach binary.
The following command generates a 256 AES key that we’ll use for the rest of this chapter:
You must manage this key securely. If it gets into the wrong hands, your data will be
available to anyone with access to your CockroachDB nodes. Cockroach Labs has the
following recommendations for managing encryption keys:
• Only the UNIX user running the CockroachDB process should have
access to encryption keys.
• You should not store encryption keys in the same place as your
CockroachDB data. Something like HashiCorp’s Vault would be a
good candidate for this.
Database Encryption
Database encryption involves encrypting all of the tables within a database. If you are
unsure whether specific tables need encrypting or not, this is a sensible option, as it
ensures everything is encrypted.
First, let’s create some nodes. With one difference, these commands are similar to
the commands we used to start the nodes in the encryption in-transit section. In the
following commands, we provide an additional --enterprise-encryption argument
that tells CockroachDB that we’ll be using Enterprise encryption features.
The --enterprise-encryption argument uses key-value pairs to configure
encryption-specific configurations:
• path – The directory that will store your CockroachDB data. This
value corresponds to the value provided to the --store argument.
The following commands create three nodes with encryption enabled. For
consistency, I will reuse the certificates generated in the previous section:
$ cockroach start \
--certs-dir=certs \
--store=node1 \
--listen-addr=localhost:26257 \
--http-addr=localhost:8080 \
--join=localhost:26257,localhost:26258,localhost:26259 \
--enterprise-encryption=path=node1,key=keys/enc.key,old-key=plain
$ cockroach start \
--certs-dir=certs \
--store=node2 \
--listen-addr=localhost:26258 \
--http-addr=localhost:8081 \
--join=localhost:26257,localhost:26258,localhost:26259 \
--enterprise-encryption=path=node2,key=keys/enc.key,old-key=plain
$ cockroach start \
--certs-dir=certs \
--store=node3 \
--listen-addr=localhost:26259 \
--http-addr=localhost:8082 \
--join=localhost:26257,localhost:26258,localhost:26259 \
--enterprise-encryption=path=node3,key=keys/enc.key,old-key=plain
134
Chapter 6 Data Privacy
If you open the CockroachDB admin console and navigate to the following URLs,
you’ll see that each of the nodes has one encrypted data store, encrypted with the key we
generated, as is shown in Figure 6-1:
• Node 1 – https://localhost:8080/#/reports/stores/1
• Node 2 – https://localhost:8080/#/reports/stores/2
• Node 3 – https://localhost:8080/#/reports/stores/3
Table Encryption
CockroachDB also allows you to encrypt specific tables rather than encrypting an entire
cluster. For example, suppose your database has some huge tables that don’t contain
sensitive information. In that case, this might be a good option for you, as you won’t pay
the performance penalty of decrypting encrypted data.
Creating a cluster that supports table encryption is similar to creating an encrypted
cluster. However, there are two small changes you need to make to the startup
commands:
135
Chapter 6 Data Privacy
The following commands create three nodes with partial encryption enabled. I will
continue securing my cluster for consistency:
$ cockroach start \
--certs-dir=certs \
--store=path=node1/encrypted,attrs=encrypted \
--store=path=node1/unencrypted,attrs=unencrypted \
--listen-addr=localhost:26257 \
--http-addr=localhost:8080 \
--join=localhost:26257,localhost:26258,localhost:26259 \
--enterprise-encryption=path=node1/encrypted,key=keys/enc.key,old-
key=plain
$ cockroach start \
--certs-dir=certs \
--store=path=node2/encrypted,attrs=encrypted \
--store=path=node2/unencrypted,attrs=unencrypted \
--listen-addr=localhost:26258 \
--http-addr=localhost:8081 \
--join=localhost:26257,localhost:26258,localhost:26259 \
--enterprise-encryption=path=node2/encrypted,key=keys/enc.key,old-
key=plain
$ cockroach start \
--certs-dir=certs \
--store=path=node3/encrypted,attrs=encrypted \
--store=path=node3/unencrypted,attrs=unencrypted \
--listen-addr=localhost:26259 \
--http-addr=localhost:8082 \
--join=localhost:26257,localhost:26258,localhost:26259 \
--enterprise-encryption=path=node3/encrypted,key=keys/enc.key,old-
key=plain
With the cluster initialized, we can now connect to it using the root user client
certificate we created previously:
It’s time to create some encrypted and unencrypted tables. We do this by first
identifying the tables we need to and don’t need to encrypt. In the following example,
I’m creating a customer table containing Personally Identifiable Information (PII), which
should be encrypted, and a product table containing no PII, which does not need to be
encrypted.
Let’s create the customer and product tables now:
After creating each table in the example shown previously, I’m setting zone
configurations to mark a table as being either +encrypted (needs encrypting) or
+unencrypted (does not need encrypting). These constraints coincide with the attributes
we used when defining the stores earlier.
If you open the CockroachDB admin console and navigate to the data source URLs
again, you’ll see that each of the nodes has an encrypted store and an unencrypted store,
as is shown in Figure 6-2.
137
Chapter 6 Data Privacy
138
CHAPTER 7
Deployment Topologies
In this chapter, we’ll explore some common cluster deployment topologies. Each will
make sense in different circumstances, depending on your environment, latency, and
survivability requirements.
I’ll create an example cluster for many of the topologies listed in this chapter to
show you how it’s done. To balance clarity with succinctness, I will manually create
CockroachDB nodes for all of the examples in this chapter. Depending on your
infrastructure and preferred deployment and orchestration technologies, you may
prefer to deploy with Kubernetes or use Cockroach Cloud. Note also that in this chapter,
I aim to show just the relationships between nodes. I won’t be deploying geographically
separate nodes.
I’ll reuse some generated certificates and keys for all clusters to reduce repetition.
I’ll create these with the following commands:
$ mkdir certs
$ mkdir keys
139
© Rob Reid 2022
R. Reid, Practical CockroachDB, https://doi.org/10.1007/978-1-4842-8224-3_7
Chapter 7 Deployment Topologies
Single-Region Topologies
We’ll start with the single-region topologies. These are recommended for small-scale
clusters that serve users from a single location. For example, these topologies will work
for you if you’re a UK business with only UK customers. Let’s explore them.
Development
You’ll likely want to keep costs to a minimum during the course of development. For
this reason, Cockroach Labs acknowledges that a single-node cluster works well for this
scenario, if you are prepared to prioritize cost-savings and simplicity over resiliency. The
Development Topology defines exactly this: a CockroachDB cluster with a single node.
I would give a single-node cluster the following characteristics:
Read latency ★★★★★ The gateway node is also the leaseholder node for all data,
meaning no internode hops required.
Write latency ★★★★★ No replication latencies incurred.
Resiliency ★ A single-node cluster offers no protection against machine or
network failures.
Ease of setup ★★★★★ Can be installed onto a machine on the local network or an E2C
server, etc.
Basic Production
Once your application has users who depend on an available database, a single-
node cluster is no longer a viable topology. The Basic Production topology defines a
CockroachDB cluster with nodes in multiple availability zones (AZs) in a single region.
The number of nodes you choose to deploy is up to you, but in order to survive an
outage of a single AZ, you’ll need at least three nodes with a replication factor of three.
In order to survive an outage of two AZs, you’ll need at least five nodes with a replication
factor of five.
140
Chapter 7 Deployment Topologies
Let’s look at some of the benefits and drawbacks of a cluster using this topology:
Read latency ★★★★★ The gateway node is in the same region as the leaseholder nodes.
Write latency ★★★★★ All replication occurs within the same region, so consensus is
achieved quickly.
Resiliency ★★★ Can survive single/multiple AZ outages depending on your
architecture, node count, and replication strategy.
Ease of setup ★★★★ All nodes exist within a single region, so they can be easily
orchestrated with Kubernetes with little effort.
Now, let’s create a cluster to simulate the Basic Production topology. Let’s create a
five-node cluster that can handle an outage of two AZs:
$ cockroach start \
--certs-dir=certs \
--store=node1 \
--listen-addr=localhost:26257 \
--http-addr=localhost:8080 \
--locality=region=us-east1,zone=a \
--join=localhost:26257,localhost:26258,localhost:26259,localhost:26260,
localhost:262561
$ cockroach start \
--certs-dir=certs \
--store=node2 \
--listen-addr=localhost:26258 \
141
Chapter 7 Deployment Topologies
--http-addr=localhost:8081 \
--locality=region=us-east1,zone=b \
--join=localhost:26257,localhost:26258,localhost:26259,localhost:26260,
localhost:262561
$ cockroach start \
--certs-dir=certs \
--store=node3 \
--listen-addr=localhost:26259 \
--http-addr=localhost:8082 \
--locality=region=us-east1,zone=c \
--join=localhost:26257,localhost:26258,localhost:26259,localhost:26260,
localhost:262561
$ cockroach start \
--certs-dir=certs \
--store=node4 \
--listen-addr=localhost:26260 \
--http-addr=localhost:8083 \
--locality=region=us-east1,zone=d \
--join=localhost:26257,localhost:26258,localhost:26259,localhost:26260,
localhost:262561
$ cockroach start \
--certs-dir=certs \
--store=node5 \
--listen-addr=localhost:26261 \
--http-addr=localhost:8084 \
--locality=region=us-east1,zone=e \
--join=localhost:26257,localhost:26258,localhost:26259,localhost:26260,
localhost:262561
Next, we’ll connect to the cluster and run some commands to ensure CockroachDB
replicates our data across the five nodes.
142
Chapter 7 Deployment Topologies
The increase in replicas from three to five can take a minute or two to take effect,
but once it has, you’ll notice that CockroachDB uses all five of the nodes as replicas for
data in the example table. If we lose two of the five nodes, the remaining three will still
manage to achieve quorum.
Multiregion Topologies
Multiregion topologies are good for applications requiring a higher degree of failure
tolerance. I’d recommend these topologies for clusters that need to span larger
geographic areas, with customers in multiple locations. I’d also recommend these
topologies if your survival goals include the outage of an entire cloud region, regardless
of where your customers are situated. For example, if you’re an EU-based business with
customers in Europe and Asia, these topologies will work well. Let’s explore them.
Regional Tables
The Regional Tables topology is a good choice for scenarios where you need to pin data
to specific regions. In this configuration, you’ll ensure that data for users in a given
region is always close to them, making for fast local region reads and writes.
As multiregion clusters span not just AZs but whole regions, the number of nodes
will typically be higher. For a cluster capable of surviving not just AZ but also region
failures, the node count will jump from a minimum of between three and five nodes to
between nine and fifteen. Figure 7-2 shows a multiregion cluster capable of surviving
regional outages but not AZ outages. In an AZ outage, customers closest to the now
unavailable region will route to another region, increasing latency.
143
Chapter 7 Deployment Topologies
Let’s look at some of the benefits and drawbacks of a cluster using this topology:
Read ★★★★ If data is pinned to a single region, reads from that region will be quick. If
latency data is not pinned and comes from multiple regions, you’ll incur interregion
latency.
Write ★★★★ If data is pinned to a single region, writes to that region will be quick. If data
latency is replicated across regions (for higher resiliency), you’ll incur interregion
replication latency.
Resiliency ★★★★ If you replicate data across regions, you’ll achieve resilience against regional
failures. If you pin data to regions and have nodes in different AZs within that
region, you’ll achieve resilience against AZ failure.
Ease of ★★★ Setting up a multiregion cluster is not without its pains. If you’re
setup orchestrating your cluster in Kubernetes, you’ll need a cluster for each
region, and their networks will need to be peered.
144
Chapter 7 Deployment Topologies
Now let’s create a cluster to simulate the Regional Tables topology. In this example,
we’ll create a nine-node cluster that’s capable of surviving regional outages. We’ll reuse
the start commands from the “Multiregion Clusters” section in Chapter 2 to save
repetition.
Next, we’ll set up our database and regions (note that an Enterprise license is
required for geo-partitioning):
With the cluster up and running, let’s create two tables: one that’s completely
regional (i.e., all data remains in one region) and another whose rows will be pinned to
different regions. First, we’ll create the regional table and insert a single row into it:
Next, we’ll create the table that’s regional by row and insert three rows into it, one for
each region:
145
Chapter 7 Deployment Topologies
region=us-central1,zone=us-central1b
{1,2,4,6,8}
{
"region=us-east1,zone=us-east1a",
"region=us-east1,zone=us-east1b",
"region=us-central1,zone=us-central1c",
"region=us-central1,zone=us-central1b",
"region=us-west1,zone=us-west1b"
}
146
Chapter 7 Deployment Topologies
region=us-central1,zone=us-central1b
{1,3,4,6,8}
{
"region=us-east1,zone=us-east1a",
"region=us-east1,zone=us-east1c",
"region=us-central1,zone=us-central1c",
"region=us-central1,zone=us-central1b",
"region=us-west1,zone=us-west1b"
}
region=us-east1,zone=us-east1a
{1,3,5,7,8}
{
"region=us-east1,zone=us-east1a",
"region=us-east1,zone=us-east1c",
"region=us-central1,zone=us-central1a",
"region=us-west1,zone=us-west1a",
"region=us-west1,zone=us-west1b"
}
region=us-west1,zone=us-west1b
{1,4,6,7,8}
{
"region=us-east1,zone=us-east1a",
"region=us-central1,zone=us-central1c",
"region=us-central1,zone=us-central1b",
"region=us-west1,zone=us-west1a",
"region=us-west1,zone=us-west1b"
}
The default replication factor is five, so you can see that nodes from all regions
have been used to store the data. This is because CockroachDB does not know why
you’re geo-partitioning data, so it tries to replicate data in as resilient a way as possible,
given the default survival goal of REGION. This is acceptable when data can flow freely
between regions, but not acceptable when it needs to be pinned for regulatory reasons.
Note that the example_regional_table has two nodes within us-central1, and its
leaseholder is also in us-central1.
147
Chapter 7 Deployment Topologies
Note that in the example_regional_rows table (a table whose rows are region
specific), there are two nodes for each of the geo-partitioned regions used by the tables.
Like the example_regional_table example, the range leaseholder also exists within
that region.
If you require data to exist in a single region, you’ll need to change the survival goal
of the database from REGION to ZONE and update the replicator factor to reflect the
number of nodes in that region. This cluster has three regions and three nodes in each of
those regions, so the commands to reflect this are
After a minute or so, check table ranges again. This time, you’ll notice that because
we’ve explicitly removed our requirement to survive regional failures, all data now exists
in the region we configured for the tables:
region=us-central1,zone=us-central1a
{4,6,9}
{
"region=us-central1,zone=us-central1c",
"region=us-central1,zone=us-central1a",
"region=us-central1,zone=us-central1b"
}
148
Chapter 7 Deployment Topologies
region=us-central1,zone=us-central1c
{4,6,9}
{
"region=us-central1,zone=us-central1c",
"region=us-central1,zone=us-central1a",
"region=us-central1,zone=us-central1b"
}
region=us-east1,zone=us-east1a
{1,2,3}
{
"region=us-east1,zone=us-east1a",
"region=us-east1,zone=us-east1b",
"region=us-east1,zone=us-east1c"
}
region=us-west1,zone=us-west1b
{5,7,8}
{
"region=us-west1,zone=us-west1b",
"region=us-west1,zone=us-west1a",
"region=us-west1,zone=us-west1c"
}
Global Tables
The Global Tables topology is a good choice for read-heavy scenarios where write
latencies can be tolerated. Reads will be managed locally to a region and therefore fast,
whereas writes will replicate across regions, so they will be much slower.
149
Chapter 7 Deployment Topologies
Let’s look at some of the benefits and drawbacks of a cluster using this topology:
Read ★★★★ If data is pinned to a single region, reads from that region will be quick.
latency If data is not pinned and comes from multiple regions, you’ll incur
interregion latency.
Write ★★ Data is replicated across regions before write statements return.
latency
Resiliency ★★★★★ If you replicate data across regions, you’ll achieve resilience against
regional failures. If you pin data to regions and have nodes in different
AZs within that region, you’ll achieve resilience against AZ failure.
Ease of ★★★ Setting up a Global Tables cluster is similar to setting up a Region Tables
setup cluster.
To enable the Global Tables topology pattern, we simply need to update the
configuration of a table to make it global. For this example, I’ll update our example_
regional_table to be global:
You may notice that the replication count has gone from three to five. This is the
default replication count for global tables, and it allows CockroachDB to scale the table
beyond the existing us-central1 region:
region=us-east1,zone=us-east1a
{1,2,3,5,9}
{
"region=us-east1,zone=us-east1a",
"region=us-east1,zone=us-east1b",
"region=us-east1,zone=us-east1c",
"region=us-west1,zone=us-west1b",
"region=us-central1,zone=us-central1b"
}
150
Chapter 7 Deployment Topologies
Follower Reads
The Follower Reads topology is a good choice for read-heavy scenarios where reads
for stale data and write latencies can be tolerated. The stale tolerance afforded by
this pattern reduces read and write contention for the same data, as the data can be
requested for slightly different time periods. The level of read staleness will depend on
your requirements and can be one of the following:
Let’s look at some of the benefits and drawbacks of a cluster using this topology:
Read latency ★★★★★ Reads are very fast because latency is prioritized over consistency.
Write latency ★★ Data is replicated across regions before write statements return.
Resiliency ★★★★★ The cluster can tolerate failures of AZs and regions.
Ease of setup ★★★ The configuration for Follower Reads is provided by clients
during reads, so this topology is no more difficult than any other
multiregion cluster.
To enable the Follower Reads topology, we simply need to make SELECT queries that
allow for staleness. The following query is a follower-read query against the example_
regional_rows table:
id | city | crdb_region
---------------------------------------+------------+--------------
7b65d3fa-a0e1-4cd6-b125-05654a6fbce8 | Portland | us-west1
d39ec728-cfc0-47e8-914c-fff493064071 | Des Moines | us-central1
e6a0c79c-2026-4877-b24f-6d4d9dd86848 | Charleston | us-east1
151
Chapter 7 Deployment Topologies
It’s also possible to enable follower reads within transactions using the
following syntax:
BEGIN;
SET TRANSACTION AS OF SYSTEM TIME follower_read_timestamp();
SELECT * FROM example_regional_rows;
COMMIT;
Follow-the-Workload
The Follow-the-Workload topology is the default topology for clusters that do not specify
a table locality (e.g., regional by rows). They are a good choice for scenarios where read
latencies in the current most active region must be low.
In a cluster using the Follow-the-Workload topology, CockroachDB will move data to
ensure that the busiest region for reads gets the required data.
Let’s look at some of the benefits and drawbacks of a cluster using this topology:
Read latency ★★★★ Reads for data within the current busiest region will be fast, whereas
reads from outside that region will be slower.
Write latency ★★ Data is replicated across regions before write statements return.
Resiliency ★★★★★ The cluster can tolerate failures of AZs and regions.
Ease of ★★★ This topology is the default topology for multiregion clusters, so no
setup more difficult than any other multiregion cluster.
Being the default topology, there’s nothing we need to do to explicitly enable it.
Antipatterns
Considering antipatterns and the performance implications of a poorly designed cluster
is as critical as considering the recommended topology patterns. In this section, we’ll
create a cluster using the demo command that seems well designed but will have poor
performance characteristics.
152
Chapter 7 Deployment Topologies
First, we’ll spin up the cluster. This cluster will be a nine-node, multiregion
deployment, with nodes in us-west1, us-east1, and europe-west1.
Next, we’ll create some database objects. The following statements will
• Update the table’s zone configuration to ensure that data stays within
those regions
153
Chapter 7 Deployment Topologies
What we’ve essentially created with these commands is a globally distributed follow-
the-workload cluster. If the most active region is Europe in this example, queries against
the antipattern_table will have to fetch data from the States and vice versa.
After a short while, run the following statement to ensure that data has been
distributed equally between the nodes:
region=europe-west1,az=c
{7,8,9}
{
"region=europe-west1,az=b",
"region=europe-west1,az=c",
"region=europe-west1,az=d"
}
region=us-east1,az=d
{1,2,3}
{
"region=us-east1,az=b",
"region=us-east1,az=c",
"region=us-east1,az=d"
}
154
Chapter 7 Deployment Topologies
region=us-west1,az=b
{4,5,6}
{
"region=us-west1,az=a",
"region=us-west1,az=b",
"region=us-west1,az=c"
}
id | city | crdb_region
---------------------------------------+---------------+---------------
3c24dee9-f006-4c2a-b556-0d7d6d0a7f6d | Portland | us-west1
8aaab90d-2ff3-471b-ac5a-39eec13b97bd | San Francisco | us-west1
9489216e-95f2-4796-98bd-ba654f85b688 | Seattle | us-west1
421f2184-99d7-4d2a-823c-e330ccbf5a87 | Madrid | europe-west1
48b68338-93b2-4761-832b-4a3a91ba77e5 | Berlin | europe-west1
731b6876-d745-47f3-822e-d2fe2867d639 | Paris | europe-west1
a1542138-f0ff-41ba-bfa7-1da3a5226103 | New York | us-east1
c66acd43-e23f-47ce-9965-6b09d6c4e502 | Chicago | us-east1
c73e23a1-65c1-4063-8f93-51094e0fe475 | Boston | us-east1
(9 rows)
If you thought that 14ms for CockroachDB to gather data from the east and west
coast of the States and west Europe was impressive, you’d be right! Unfortunately, this
highlights an important oversight in the configuration of this database. Running the demo
command by itself does not simulate interregion latencies. We need to add the --global
flag to enable this.
This time, we’ll run the demo command as follows:
155
Chapter 7 Deployment Topologies
If you run the same SQL commands against the database this time, you see different
results, once the data has replicated into the configured regions:
Subsequent queries will perform faster because of caching, but the design of
our cluster topology is not currently optimal. There are various ways this can be
improved upon:
Summary
You’ll need to carefully design your production cluster to get the best out of it. The
following points will help you cover the fundamentals:
• Testing – The best way to test your cluster and any applied
topology patterns is to spin up a real-world cluster and monitor
its performance. If this is not possible, you can simulate globally
distributed clusters with the --demo command or by using Docker
and the HAProxy container.1
1
www.cockroachlabs.com/blog/simulate-cockroachdb-cluster-localhost-docker
157
CHAPTER 8
Testing
In this chapter, we’ll explore some techniques for testing your database, whether it’s on
your machine, in your CI/CD pipeline, ready for production, or already in production.
It’s easy to overlook the database during application testing. The test areas to neglect
during development time are numerous. I’ll share a real-world example with you to
highlight the importance of having a test strategy when a database is involved.
While developing my first database-backed application, I created a dataset with
all row combinations I thought my application would need to support. Whenever I
learned of a new requirement, I updated the dataset, and everything seemed to work
as expected. Right up to the point where I deployed to production and saw production
amounts of data. My application was sluggish and grew increasingly sluggish over the
days that followed its deployment.
In this example, I’d neglected to test one significant thing in my database: indexes.
With my small (but seemingly comprehensive) dataset, I’d developed a false sense of
confidence that my application and database were ready for production. The moment
it saw production amounts of data, the missing index reared its ugly head, and my
application grew steadily slower.
There are many things you’ll want to get your application and database ready for
before they come anywhere near your users. There are three high-level types of database
tests you’ll want to consider to ensure comprehensive test coverage:
159
© Rob Reid 2022
R. Reid, Practical CockroachDB, https://doi.org/10.1007/978-1-4842-8224-3_8
Chapter 8 Testing
Before we get onto testing, let’s create a basic (and very contrived) database and
work through some testing scenarios together to assert its correctness. First, we’ll create
a local cluster to more or less mirror a three-node Basic Production topology cluster:
$ mkdir certs
$ mkdir keys
$ cockroach start \
--certs-dir=certs \
--store=node1 \
--listen-addr=localhost:26257 \
--http-addr=localhost:8080 \
--locality=region=us-east1,zone=a \
--join=localhost:26257,localhost:26258,localhost:26259
$ cockroach start \
--certs-dir=certs \
--store=node2 \
--listen-addr=localhost:26258 \
--http-addr=localhost:8081 \
--locality=region=us-east1,zone=b \
--join=localhost:26257,localhost:26258,localhost:26259
$ cockroach start \
--certs-dir=certs \
--store=node3 \
--listen-addr=localhost:26259 \
--http-addr=localhost:8082 \
--locality=region=us-east1,zone=c \
--join=localhost:26257,localhost:26258,localhost:26259
160
Chapter 8 Testing
$ cockroach init \
--certs-dir=certs \
--host=localhost:26257
Structural Testing
One of the goals of structural testing is to assert whether the proposed structure of the
database and its objects will satisfy business requirements. So rather than creating a
bunch of tables up front, I’ll go object by object and detail my design decisions for each
database object. Remember that this business scenario (and all database objects) is
contrived and not production-ready; the resulting database will be bare-bones.
Let’s start by creating the database. The company is called Bean About Town (totally
fictional, although a quick Google reveals that at least three cunning London-based
companies have had the same idea). For the commands that follow, we’ll use the root
user. Let’s open a CockroachDB shell and create the database bean_about_town. We will
arrange all of our database objects under that:
$ cockroach sql \
--certs-dir=certs
There are to be three primary user groups for the database: customers, members of
the coffee roasting department, and members of the accounting department. Let’s create
three users with simple passwords to ensure sensible isolation of grants and privileges.
We’ll have a retail user with access to customer-facing tables, a roasting user with access
to roasting-related tables, and a finance user with access to accounts-related tables:
161
Chapter 8 Testing
Across the business, we’ll be making and taking orders for different things:
• Roasting – Making orders for raw materials and taking orders from
the retail side of the business (e.g., a warehouse that fulfills online
orders needs roasted coffee beans). We’ll only create tables for
making orders in this example for simplicity.
Knowing that we’ll need to conceptualize multiple order types, it’s clear that creating
schemas will simplify this. Let’s create some schemas and database objects now. I won’t
create the roasting or finance schemas to keep things simple, as we can cover various
types of testing by having just the retail schema.
Let’s create the retail schema now. The retail schema will hold all of the database
objects that are related to the retail operations of the business:
• Customer names – Not everyone has a first and last name, so rather
than asking for a separate first and last name, consider asking for a
customer’s full name instead.
Rather than storing prices alongside products, we’ll create a way of allowing product
prices to change over time. For example, a customer might purchase a product while on
sale. If they ask for a refund later, the business can’t afford to lose money by refunding
the product at a nonsale price.
The following table is a basic implementation of historical prices:
163
Chapter 8 Testing
can control how decimal values get rounded. As I won’t perform any
calculations, I’ve decided to keep things simple and use decimal.
Each order can contain multiple products, and each can appear in multiple orders.
We’ll resolve this many-to-many relationship with a third table:
Following a conversation with the finance department, we’ve learned that members
of their team will need to have access to objects in the retail schema for generating e nd-
of-day reports, etc. Currently, if they try to access the objects of the retail schema, they
won’t see anything:
164
Chapter 8 Testing
$ cockroach sql \
--certs-dir=certs \
--url "postgres://finance_user:a02308ce58c92131@localhost:26257/bean_
about_town" \
--execute "SELECT COUNT(*) FROM retail.order"
ERROR: user finance_user does not have SELECT privilege on relation order
Granting them USAGE and SELECT permissions on the retail schema objects will
rectify this:
Functional Testing
Functional testing asserts that the database satisfies business requirements and use
cases. We can run functional tests from either a user perspective (Black Box) or with a
deeper understanding and direct access to the database (White Box). We’ll go through
each type of testing now.
To keep complexity from spiralling, I’ll limit the tests to cover the retail schema.
165
Chapter 8 Testing
Next, bring in the dependencies we’ll need for this application by adding them to the
shard.yml file and installing them:
dependencies:
kemal:
github: kemalcr/kemal
pg:
github: will/crystal-pg
$ shards install
With the dependencies installed, we’re ready to write our API. As with the example
code from Chapter 5, this code is nowhere near production quality and is provided to act
as a mechanism for Black Box testing.
Let’s start by importing some modules:
require "kemal"
require "db"
require "pg"
require "json"
require "uuid"
require "uuid/json"
Next, we’ll create a connection to the database. Note that this connection will be in
cleartext, which is not what you’d want for production use cases:
db = PG.connect "postgres://retail_user:7efd7222dfdfb107@localhost:26257/
bean_about_town?auth_methods=cleartext"
166
Chapter 8 Testing
Now we’re ready to wire up our API handlers. Let’s start with a filter middleware
handler that will attach a JSON Content-Type header to all responses. This will run
before all of our handers execute and will add the correct Content-Type header to all
responses:
before_all do |env|
env.response.content_type = "application/json"
end
Next, we’ll add a POST request handler to insert customers. This handler will
• Read full_name and email string values from a JSON request body
id, join_date =
db.query_one "INSERT INTO retail.customer (full_name, email)
VALUES ($1, $2)
RETURNING id, join_date", full_name, email, as: {
UUID, Time }
Next, we’ll add a GET request handler to select customers by their IDs. This
handler will
167
Chapter 8 Testing
Now we’ll move onto products. Let’s add a POST request endpoint to insert a product
and its prices. This handler will
• Insert the prices for that product into the produce_price table
As we’re reading an object from the request, I’ve created two classes: a Product class
that will hold the top-level information of the product and a ProductPrice class that will
contain a single, optionally expirable price for that product.
class Product
include JSON::Serializable
property id : UUID?
property name : String
property sku : String
property prices : Array(ProductPrice)
end
class ProductPrice
include JSON::Serializable
168
Chapter 8 Testing
property id : UUID?
property amount : Float64
property start_date : Time?
property end_date : Time?
end
id = UUID.empty
db.transaction do |tx|
id = tx.connection.query_one "INSERT INTO retail.product (name, sku)
VALUES ($1, $2)
RETURNING id", request.name, request.sku,
as: { UUID }
request.prices.each do |price|
tx.connection.exec "INSERT INTO retail.product_price (product_id,
amount, start_date, end_date)
VALUES ($1, $2, $3, $4)", id, price.amount,
price.start_date, price.end_date
end
end
As with the customer handler, we’ll add a GET request handler for products too. This
handler will
• Select the lowest active price for each product. This is defined as the
lowest price for a product that has a start_date that’s not in the future
and either an end_date in the future or no end_date.
• Build up a collection of products found.
169
Chapter 8 Testing
products.to_json
end
The next handler we’ll create is a POST request handler to insert orders. As with the
product POST handler, we’ll capture a JSON request from the caller, so we will introduce
a new class to handle the serialization. This handler will
170
Chapter 8 Testing
• Insert the products and paid price into the product_order table
class Order
include JSON::Serializable
property id : UUID?
property customer_id : UUID
property delivery_instructions : String?
property products : Hash(UUID, UUID)
end
id = UUID.empty
ref = ('A'..'Z').to_a.shuffle(Random::Secure)[0, 8].join
db.transaction do |tx|
id = tx.connection.query_one "INSERT INTO retail.order (reference,
customer_id, delivery_instructions)
VALUES ($1, $2, $3)
RETURNING id", ref, request.customer_
id, request.delivery_instructions, as:
{ UUID }
The last handler we’ll create is a GET request handler to select orders for a given
customer ID. This handler will
class Order
include JSON::Serializable
property id : UUID
property reference : String
property customer_id : UUID
property date : Time
property delivery_instructions : String
property products : Hash(String, Float64)
end
172
Chapter 8 Testing
unless orders.has_key? id
orders[id] = Order.new(id, ref, customer_id, date, instructions)
end
orders[id].products[product] = amount.to_f
end
end
orders.values.to_json
end
All that’s left to do now is to start the API server listening. This code will
Kemal.run
With the code in place, we can start the server with the following command:
Test the Application
Let’s write some cURL commands to test the API (and, in doing so, the database). During
this phase of testing, we’ll need to assert not only that the database behaves as expected
when given expected data but also that it behaves as expected when given unexpected
data. These are commonly referred to as Boundary Tests.
In this section, we’ll go through each of our use cases to assert the database can
support them. Let’s start by creating a customer:
This covers the happy path: a sensibly sized name and email address. Does our
database support unicode characters? Let’s find out:
Seems to! How about long names? Does the database reject full names that are
greater than the maximum 255 length we defined?
The server is absent of any error handling, but the logs confirm that our attempt to
store a customer with a 256-character-long full_name failed:
174
Chapter 8 Testing
"email": "[email protected]"
}'
{"id":"d93bedb4-3e59-4bd5-b018-b6d89d688904","join_
date":"2022-01-20T18:26:53Z"}
Ignoring email addresses for now, our create customer use case is looking good, and
it seems that our database is satisfying the basics of this use case. Let’s move onto the get
customer use case now.
First, we’ll make a request to fetch an existing customer:
This returns the data we originally inserted for this user. There are obviously no
security checks around who are allowed to make requests for specific users, but from
the perspective of the database, this is our happy path test satisfied. How about unhappy
paths? Let’s try omitting the customer ID in the URL path, searching for a customer by a
nonexistent ID, and searching for a customer with a malformatted UUID. For these tests,
I’ll limit the response to just the headers, as without proper error handling, we’ll receive
an HTML page response.
In the response to a request without an ID in the path, we receive a 404 because the
path is expecting /customers/{CUSTOMER_ID}. This test has not reached the database,
which is expected.
175
Chapter 8 Testing
Connection: keep-alive
X-Powered-By: Kemal
Content-Type: text/html
Transfer-Encoding: chunked
Exception: error in argument for $1: could not parse string "a" as uuid
(PQ::PQError)
Onto the creation of products. This scenario provides our first test of a
multistatement transaction. In this transaction, we insert a product and then insert some
prices using the previously inserted product ID. Let’s make a request:
176
Chapter 8 Testing
We’ve given two prices to the product we’ve just inserted: a full price of 12.00 and a
half price of 6.00. The half price offer is set to expire one hour after it becomes available,
so at the time of testing, I would expect the price for this product to be 12.00 for every
request.
We also want to assert that if there are multiple active prices, we always returned the
cheapest:
Rather than run a suite of explorative tests against this endpoint, let’s skip ahead to
fetching the product and its prices:
177
Chapter 8 Testing
In the response body (which I’ve formatted slightly for readability), we can see that
our two products have been returned and that neither the expired sale price nor the
more expensive active price appears.
Next, let’s place an order and check the database behavior with our next
multistatement transaction call. In this scenario, we’ll create an order and then use its ID
to insert rows into the product_order table:
Let’s check that the order was inserted successfully by making a request the endpoint
that returns a customer’s orders:
In the response body, we can see that all of the order fields are included, along with
the correct products and prices.
178
Chapter 8 Testing
Let’s begin! We’ll start by preparing a directory to house our tests with a few
commands:
$ mkdir db_tests
$ cd db_tests
$ go mod init dbtests
Next, we’ll pull in the database dependency that will enable database interaction:
$ go get -u github.com/jackc/pgx
Next, we’ll create the Go code that will connect to the database and run some unit
tests. As this file lives in isolation of any other Go code, there are no dependencies to
other self-managed Go modules. In this code, we will
• Read a flag from the command line, indicating whether database
tests should be included or not (defaults to false)
• If database tests are flagged to run:
179
Chapter 8 Testing
• Run unit tests and capture a status code indicating whether the tests
passed or failed.
• Exit the application with the status code received from the test run.
package main
import (
"context"
"flag"
"log"
"os"
"testing"
"time"
"github.com/jackc/pgx/v4/pgxpool"
)
var (
dbTests *bool
db *pgxpool.Pool
)
if *dbTests {
connStr, ok := os.LookupEnv("CONN_STR")
if !ok {
log.Fatal("connection string env var not found")
}
180
Chapter 8 Testing
code := m.Run()
if *dbTests {
db.Close()
}
os.Exit(code)
}
return err
}
Note that at this point, we’ll be able to run all tests that don’t require a database
connection with the following command:
$ go test ./... -v
=== RUN TestADatabaseInteraction
--- SKIP: TestADatabaseInteraction (0.00s)
181
Chapter 8 Testing
=== RUN TestANonDatabaseInteraction
main_test.go:65: running non-database test
--- PASS: TestANonDatabaseInteraction (0.00s)
PASS
ok
Next, we’ll package up the application into a Docker image, so it can be executed
alongside CockroachDB in a Docker Compose environment. This can be very simple
because we’ll be executing a Linux-compatible executable that will exist in the /app
directory.
FROM alpine:latest
COPY . /app
WORKDIR /app
With our code (and this Dockerfile) in place, we can build a Docker image ready
to use in Docker Compose. The first command will build a test executable for a Linux
machine called “crdb-test”. The second command will bundle our current working
directory (including the test executable) into the /app directory:
The last file we’ll create will be for Docker Compose. This file will create two services:
one for CockroachDB and one for our test suite. In this file, we
• Create a cockroach_test service using the v21.2.4 version of the
CockroachDB Docker image that will
• Customize its startup command to run just a single node, so it
doesn’t wait for an initialization command
• Not expose CockroachDB’s default port of 26257, as there’s no
need to access CockroachDB from the host machine
• Suppress logs, meaning our test suite logs are the only
output we see
182
Chapter 8 Testing
We’ll run both of the containers in a Docker bridge network called app-network so
that they can talk to each other.
version: '3'
services:
cockroach_test:
image: cockroachdb/cockroach:v21.2.4
command: start-single-node --insecure
logging:
driver: none
networks:
- app-network
app_test:
image: crdb-test:latest
environment:
CONN_STR: postgres://root@cockroach_test:26257/
defaultdb?sslmode=disable
command: ./crdb-test -db -test.v
183
Chapter 8 Testing
working_dir: /app
depends_on:
- cockroach_test
networks:
- app-network
networks:
app-network:
driver: bridge
With everything in place, we can kick off the full database test with the following
command:
As we can see, both the database and the nondatabase test ran as expected. After
the app_test service container finishes, both the app_test and the cockroach_test
containers exit.
184
Chapter 8 Testing
Without these reference checks, we would have created orphaned data in the
referencing tables. It’s important to note that without proper permissions, we could still
inadvertently delete data from the retail.customer and retail.product_order tables. Still,
we can rest assured that checks are in place for the remaining tables.
185
Chapter 8 Testing
• scan
estimated row count: 1 (33% of the table; stats collected 28
minutes ago)
table: customer@primary
spans: [/'85f05ac3-931f-4417-a6ae-1403468a2c10' - /'85f05ac3-931f-4417-
a6ae-1403468a2c10']
Of the three items in the table, we’ve scanned just the one with the ID we’re
interested in (or 33% of the table). The ID is the table’s primary key, so it’s already
indexed and nice and efficient. Note that while I don’t need to include the LIMIT 1
expression (as there’ll only be one instance of this ID in the table), I think it’s a safe and
sensible habit if you further restrict your queries to the number of expected rows you’d
like returned.
186
Chapter 8 Testing
187
Chapter 8 Testing
│
└── • scan
estimated row count: 5 (100% of the table;
stats collected 33 minutes ago)
table: product_price@primary
spans: FULL SCAN
Oh dear! From the FULL SCAN, it’s clear we’re missing some indexes. CockroachDB
has highlighted the start_date <= filter, the end_date >= filter, and the end_date IS
NULL filter as candidates for this issue. Unless fixed, the performance of this query will
continue to degrade as the table grows.
Let’s add some indexes to these columns to make our query more efficient.
As the start_date filter checks that start_date column values are less than or equal to
a given value, I’ve provided the default ASC sort order (this could be omitted but I’ve kept
it in to make it obvious). As the end_date filter checks that end_date column values are
greater than or equal to a given value, I’ve provided the DESC sort order.
Let’s try the query again with these indexes in place:
188
Chapter 8 Testing
190
Chapter 8 Testing
Another full table scan! The reason for this full table scan, however, is the multiple
table joins with missing indexes, rather than a single table with a missing index. Let’s add
indexes to the columns that are participating in the join and rerun the explain:
191
Chapter 8 Testing
Much better. It’s important to note that when writes occur against a table, locking
occurs, leading to contention. To view where your database contention occurs, you can
visit the /sql-activity?tab=Statements page of the Admin Console or run the following
commands in the SQL shell. The first highlights contented for tables and the second
highlights contended for indexes:
192
Chapter 8 Testing
From this information, we can determine the contention hotspots in our database
and deal with them. One way to reduce contention is to split operations into separate
statements. Our API runs every statement to create products and orders in the same
transaction, creating contention, especially if many statements execute. I prefer
multirow DML1 (Data Manipulation Language) over executing multiple statements in a
more production-friendly environment. Consider the following INSERT statements. The
first uses single-row DML to insert three records, while the second harnesses multirow
DML. The outcome of using DML is fewer network round trips, less parsing of SQL
statements, and less time for a locked row to increase contention on a table or index.
Single-row DML:
Multirow DML:
1
www.cockroachlabs.com/blog/multi-row-dml
2
www.cockroachlabs.com/docs/stable/build-a-java-app-with-cockroachdb.html
193
Chapter 8 Testing
In the real world, I would keep the transaction and harness multirow DML to
ensure that changes resulting from the insertion of products and prices get committed
together, and if any error occurred, they are rolled back together, with no side effects or
orphaned rows.
The API we’ve created is tiny and contains just a handful of queries, making this kind
of White Box testing quick and painless. What if you performed hundreds or thousands
of queries and didn’t have time to check them all individually?
CockroachDB’s SHOW FULL TABLE SCANS statement returns every statement that
resulted in a full table scan and is available to admin users. Run this tool after a suite of
automated or Black Box tests (and in production), and you’ll see which queries you need
to tune.
Nonfunctional Testing
Nonfunctional testing asserts that the database satisfies the ‘ilities: reliability, scalability,
etc. In this section, we’ll cover some of the most common types of nonfunctional testing
you might come across when integrating CockroachDB.
Performance Testing
To get value from a performance test, we must first understand how the application will
use our database. Which tables will be the most frequently used? What is the impact of
calling them? Are there any client-side interactions that result in many database calls?
Let’s start this section by understanding the client-side interactions and how they
result in database calls. We’ll do this by listing the interactions in descending order of
expected call frequency:
194
Chapter 8 Testing
I’ll use k6,3 an open source load testing tool written in Go, and JavaScript scriptable,
to simulate these transactions.
To keep things light, I’ll implement just two k6 scripts. For the sake of brevity, I won’t
spend time simulating accurate user behavior:
First, let’s create the products.js script to simulate a user creating products. This
script will
• Fail if after running all of the test iterations, the error rate was greater
than 1% or the request duration for the 95% percentile was greater
than 100ms
3
https://k6.io
195
Chapter 8 Testing
196
Chapter 8 Testing
Now, let’s create the customers.js script to simulate a customer using the database
via the API. This script will
• Fail if the response status from creating the customer is not 200
(success)
• Capture the customer’s ID from the response body JSON and pass it
into the iteration loop. Within the iteration loop, we
• Make a third GET request to the /products endpoint but this time,
capturing an array of products
• Fail if after running all of the test iterations, the error rate was greater
than 1% or the request duration for the 95% percentile was greater
than 200ms
197
Chapter 8 Testing
http_req_duration: ['p(95)<200'],
}
};
return createResponse.json('id');
}
198
Chapter 8 Testing
Time to run our simulations. First, let’s kick off the products.js script and ask it to run
for ten seconds. As we won’t have many people entering products, I’ll simulate just one
user entering products (at five products per second; I didn’t say they were an ordinary
person).
199
Chapter 8 Testing
With that running, we’ll kick off the customers.js script and ask it to run for ten
seconds. I’ll simulate just two customers for now, and they’ll complete the full scenario
as quickly as possible:
█ setup
200
Chapter 8 Testing
test environment and introduce load balancing, I’ll now create a local Dockerized
cluster that sits behind an HAProxy, a container courtesy of the brilliant Tim Veil of
Cockroach Labs.
Create a Docker Compose configuration file called docker-compose.yml with the
following content. This file will
• Create an HAProxy load balancer that will forward requests from the
host machine to each of the CockroachDB nodes
services:
node1:
image: cockroachdb/cockroach:v21.2.4
hostname: node1
container_name: node1
command: start --insecure --join=node1,node2,node3
networks:
- app-network
node2:
image: cockroachdb/cockroach:v21.2.4
hostname: node2
container_name: node2
command: start --insecure --join=node1,node2,node3
networks:
- app-network
node3:
image: cockroachdb/cockroach:v21.2.4
hostname: node3
container_name: node3
command: start --insecure --join=node1,node2,node3
networks:
- app-network
haproxy:
201
Chapter 8 Testing
hostname: haproxy
image: timveil/dynamic-haproxy:latest
ports:
- "26257:26257"
- "8080:8080"
- "8081:8081"
environment:
- NODES=node1 node2 node3
links:
- node1
- node2
- node3
networks:
- app-network
networks:
app-network:
driver: bridge
To start the containers running, initialize the cluster, and create a shell to one of the
CockroachDB containers, run the following commands:
$ docker compose up -d
$ docker exec -it node1 cockroach init --insecure
$ docker exec -it node1 /bin/bash
Now that you’re connected to the container, run the following command to connect
to the CockroachDB SQL shell:
You can now create the database and its objects exactly as you did before and access
all of the nodes in the cluster via localhost:26257, a change that will be transparent to the
running API.
202
Chapter 8 Testing
Resilience Testing
Resilience testing aims to ensure that our database remains available in the event of
node outages. We’ll use the Docker Compose example that preceded this section as it’s
load balanced, meaning we can remove each node from the cluster one by one without
having to connect to another. In our case, we have a three-node cluster, so it can tolerate
up to a maximum of one unavailable node.
First, let’s check that our nodes are running. We’ll do this by running the docker ps
command that only shows running containers and output their IDs and names:
Before we start removing nodes from the cluster, we’ll check the range status of the
cluster nodes. In the following output, I’ve kept only the relevant range information:
Let’s remove one of the nodes from the cluster. We’ll start by removing node1, seeing
how cluster replication is affected, and then bring it back into the cluster before moving
onto the next node:
203
Chapter 8 Testing
We can see that node1 is now unavailable and has no leaseholders. Ranges in
the remaining nodes now show as underreplicated because replication is no longer
occurring over all three nodes. Let’s bring node1 back up and see how this affects the
replication:
We’re back up to three nodes and our underreplication issue is resolved. I’ll now
repeat for node2. Note that I’m leaving a couple of minutes between stopping and
starting a node before running the node status requests:
Having finished our single-node shutdown tests, our server is back up to full
capacity, having experienced no outages. Let’s try removing two nodes from the
cluster now:
205
Chapter 8 Testing
EOF
Failed running "node status"
Observe that after we stopped two nodes, the cluster was no longer available. This is
expected, because the maximum number of nodes we can tolerate being unavailable in a
three-node cluster is one.
Also observe that after running the node status request immediately after restarting
the nodes, none of them have any leaseholders. It’s not until the restarted nodes (node1
and node2) become fully available that leaseholder leaders are reelected and the server
returns to normal operation.
To finish our resilience tests, we’ll clean things up:
206
CHAPTER 9
Production
In the last chapter of this book, we’ll cover some practices and tools that will ensure our
cluster is ready for whatever a production environment can throw at it:
1
https://cloud.google.com/customers/lush
207
© Rob Reid 2022
R. Reid, Practical CockroachDB, https://doi.org/10.1007/978-1-4842-8224-3_9
Chapter 9 Production
Best Practices
The best practices documentation2 on the Cockroach Labs website is comprehensive
and a great reference for anyone wishing to get the best out of their CockroachDB clusters.
We’ll put these best practices to work in this section and see what happens if we don’t.
Unless stated, many of the examples that follow in this chapter will use the three-
node, load-balanced cluster I created in Chapter 8.
SELECT Performance
In this section, we’ll look at some performance best practices concerning SELECT
statements and what configuration values you can tweak to enjoy the best possible
performance.
If you find that SELECT queries are running slowly and you’ve ruled out indexes as the
culprit, your database might lack cache memory. By default, each node will have a cache
size of 128MiB (~134MB), which stores information required by queries. This size works
well as a default for local database development, but you may find that increasing it will
make for faster SELECT performance. Cockroach Labs recommends setting the cache size
to at least 25% of the machine’s available memory for production deployments.
To update this setting, pass a value to the --cache argument when you start
your node:
2
www.cockroachlabs.com/docs/stable/performance-best-practices-overview
208
Chapter 9 Production
To update this setting, pass a value to the --max-sql-memory argument when you
start your node:
For a full list of memory recommendations, visit the flags section3 of the start
command documentation and the production checklist.4
INSERT Performance
In this section, we’ll look at some performance best practices and why you’ll want to
follow them. We’ll start by performing some INSERT statements to see why it’s important
to use multirow DML statements over single-row DML statements.
First, we’ll create a database and a table:
Next, we’ll write a script to simulate the bulk insertion of 1000 rows and compare the
performance of doing it via single and multirow DML statements.
I’ll use Go for these tests, so my imports and main function look as follows:
package main
import (
"context"
"fmt"
3
www.cockroachlabs.com/docs/stable/cockroach-start#flags
4
www.cockroachlabs.com/docs/stable/recommended-production-settings.html
209
Chapter 9 Production
"log"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v4/pgxpool"
)
func main() {
connStr := "postgres://root@localhost:26257/best_
practices?sslmode=disable"
db, err := pgxpool.Connect(context.Background(), connStr)
if err != nil {
log.Fatalf("error connecting to db: %v", err)
}
defer db.Close()
...
}
Next, we’ll implement the single-row DML INSERT code. This code will make 1000
separate requests (involving 1000 separate network hops) to insert a row into the sensor_
reading table and time how long the complete operation takes:
start := time.Now()
for i := 0; i < rows; i++ {
if _, err := db.Exec(context.Background(), stmt, uuid.New(),
1); err != nil {
return 0, fmt.Errorf("inserting row: %w", err)
}
}
return time.Since(start), nil
}
210
Chapter 9 Production
Next, we’ll perform the single-row INSERT statements in batches. This will send
multiple INSERT statements to CockroachDB at the same time and execute them in a
single transaction. It’s important to consider breaking large inserts into smaller ones
when bulk-inserting data because the more data you’re inserting, the longer you’ll lock
the table you’re inserting into. The following function allows me to execute batch inserts
in small, concurrent chunks:
start := time.Now()
eg, _ := errgroup.WithContext(context.Background())
for c := 0; c < chunks; c++ {
eg.Go(func() error {
batch := &pgx.Batch{}
for i := 0; i < rows/chunks; i++ {
batch.Queue(stmt, uuid.New(), i)
}
return nil
})
}
211
Chapter 9 Production
Note that this implementation is still executed as single-row DML in the database.
The difference between this implementation and the first, however, is that there are now
far fewer network hops, making for faster end-to-end queries.
If we really want to see performance gains from our queries, we’ll need to execute
multirow DML statements. The following does just that, with the help of two additional
functions. The first helper function is called counter, and its job is to simply return an
incremented number every time it’s called:
The second helper function is called argPlaceholders, and its job is to manually
construct the syntax used in a multirow DML statement.
212
Chapter 9 Production
}
return builder.String()
}
Here are some examples to help you understand how argPlaceholders works:
start := time.Now()
eg, _ := errgroup.WithContext(context.Background())
for c := 0; c < chunks; c++ {
eg.Go(func() error {
argPlaceholders := argPlaceholders(rows/chunks, 2, 1)
stmt := fmt.Sprintf(stmtFmt, argPlaceholders)
213
Chapter 9 Production
So how fast are our queries? I’ll run them each five times and take an average:
It’s clear to see that by executing multirow DML, our execution times are a fraction
of the single-row DML equivalents. There’s a good reason multirow DML statements for
bulk operations are considered best practice.
UPDATE Performance
If you need to bulk update a table’s data, it’s important to consider the following:
214
Chapter 9 Production
Let’s compare the performance of an UPDATE statement that updates data on the
fly vs. an UPDATE statement that uses the multipass approach. As with the INSERT
examples, I’ll provide code for both scenarios before running them.
Before we begin, we’ll insert some random data into the table again, this time,
shortcutting the Go code and going straight to CockroachDB with the help of its
generate_series function. This statement will insert 1000 random entries into the table
for the past 1000 or so days and will be the basis for our UPDATE statements:
Let’s assume our sensor readings in the 1990s were high by 0.001 (no one said this
wouldn’t get hideously contrived) and need fixing. We’ll fix the data using UPDATE
statements.
First, we’ll create update on-the-fly solution:
start := time.Now()
if _, err := db.Exec(context.Background(), stmt); err != nil {
return 0, fmt.Errorf("updating rows: %w", err)
}
Now, we’ll create a multipass solution. There’s more going on in this code than there
was in the on-the-fly example, simply because we’re performing more operations to
achieve a faster query that results in less index contention. For this example, I present
three functions.
First, a function to select IDs out of the table (note that you can select whatever
column(s) will allow you to most efficiently fetch your data). This function
• Selects the IDs of any rows matching the selection criteria using a
time-travel6 query (a query that tolerates slightly stale data in return
for reduced transaction contention and increased performance)
args := []interface{}{}
selectStmt := selectStmtFmt
if lastID != "" {
selectStmt = fmt.Sprintf(selectStmtFmt, "AND sensor_id > $2")
args = append(args, selectLimit, lastID)
} else {
5
This is frequently referred to as “cursor pagination.”
6
www.cockroachlabs.com/docs/stable/as-of-system-time.html
216
Chapter 9 Production
The next function will update any rows matching the IDs we fetched from the
previous query. This function
217
Chapter 9 Production
updateIDs := ids
for {
idCount := min(len(updateIDs), limit)
if idCount == 0 {
return nil
}
updateIDs = updateIDs[idCount:]
}
}
• Fetches a batch of IDs that have not been processed via the select
function and passes them to the update function.
• If at any point we’ve run out of IDs to process, the function returns.
218
Chapter 9 Production
for {
ids, err := updateMultiPassSelect(db, lastID, selectLimit)
if err != nil {
return 0, fmt.Errorf("fetching ids to update: %w", err)
}
if len(ids) == 0 {
break
}
lastID = ids[len(ids)-1]
}
Now for the results. The difference between the two methods isn’t as obvious as the
INSERT example, but for the reduced table locking, an additional performance bump
is great:
updateOnTheFly 3.69s
updateMultiPass (selectLimit = 10,000, updateLimit = 1,000) 2.04s
Check out the Cockroach Labs site for a great example of a multipass bulk-update
operation using Python.7
7
www.cockroachlabs.com/docs/stable/bulk-update-data
219
Chapter 9 Production
Cluster Maintenance
The cluster you originally deploy to production on day one is unlikely to resemble the
cluster you have running after year one (and especially after year five). If you’re planning
your CockroachDB cluster years ahead, you
• Are potentially paying for resources you’re not making full use of
We’ll start with scaling the cluster. If your CockroachDB deployment is running via
Cockroach Cloud, this scenario is handled for you; all cluster scaling is automatic. If your
cluster is hosted in Kubernetes, it’s as simple as follows:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: cockroachdb
spec:
serviceName: "cockroachdb"
replicas: 3 # => 5
...
For the examples in this chapter, we’ll spin up a cluster manually, so we have full
control over each of the nodes. Let’s start with three nodes in three separate command-
line terminal sessions and then initialize it with a fourth terminal. Note that I’m using
different ports and stores in this example because the cluster is running on one machine.
In reality, this cluster will be running across multiple machines, so the only difference
between the node start command will be the join addresses:
$ cockroach start \
--insecure \
--store=node1 \
220
Chapter 9 Production
--listen-addr=localhost:26257 \
--http-addr=localhost:8080 \
--join=localhost:26257,localhost:26258,localhost:26259
$ cockroach start \
--insecure \
--store=node2 \
--listen-addr=localhost:26258 \
--http-addr=localhost:8081 \
--join=localhost:26257,localhost:26258,localhost:26259
$ cockroach start \
--insecure \
--store=node3 \
--listen-addr=localhost:26259 \
--http-addr=localhost:8082 \
--join=localhost:26257,localhost:26258,localhost:26259
Time to scale it. CockroachDB was designed to run anywhere, with scalability and
survivability being its raison d'etre. As such, scaling a CockroachDB cluster is almost
comically simple:
$ cockroach start \
--insecure \
--store=node4 \
--listen-addr=localhost:26260 \
--http-addr=localhost:8083 \
--join=localhost:26257,localhost:26258,localhost:26259
In the preceding command, we start a new node in the exact same way we started
the three initial (or “seed”) nodes. Every node in the cluster will use the gossip protocol8
to communicate with one other to organize scaling. The three seed nodes are all it takes
8
https://en.wikipedia.org/wiki/Gossip_protocol
221
Chapter 9 Production
to create the basis for this gossip network, and they don’t need to be aware of additional
nodes. This allows you to continue to scale your cluster without making any changes to
the configuration of the seed nodes.
Running the command starts a fourth node, which immediately joins the cluster,
expanding its capacity.
Before moving onto the next example, stop the nodes and remove their data
directories (e.g., node1, node2, and node3).
Scaling a cluster into a different region requires a little more configuration but is
about as straightforward as scaling a single-region cluster. First, we’ll create a cluster in
eu-central-1 (Frankfurt) before scaling into eu-west-3 (Paris):
$ cockroach start \
--insecure \
--store=node1 \
--listen-addr=localhost:26257 \
--http-addr=localhost:8080 \
--locality=region=eu-central-1,zone=eu-central-1a \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node2 \
--listen-addr=localhost:26258 \
--http-addr=localhost:8081 \
--locality=region=eu-central-1,zone=eu-central-1a \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node3 \
--listen-addr=localhost:26259 \
--http-addr=localhost:8082 \
--locality=region=eu-central-1,zone=eu-central-1a \
--join='localhost:26257, localhost:26258, localhost:26259'
222
Chapter 9 Production
With the nodes in Frankfurt up and running, let’s use the CockroachDB shell to
interrogate the regions and zones:
SHOW regions;
region | zones | database_names | primary_region_of
---------------+-----------------+----------------+--------------------
eu-central-1 | {eu-central-1a} | {} | {}
Now, we’ll spin up the nodes in our Paris cluster and have them join the nodes in the
existing Frankfurt cluster:
$ cockroach start \
--insecure \
--store=node4 \
--listen-addr=localhost:26260 \
--http-addr=localhost:8083 \
--locality=region=eu-west-3,zone=eu-west-3a \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node5 \
--listen-addr=localhost:26261 \
--http-addr=localhost:8084 \
--locality=region=eu-west-3,zone=eu-west-3a \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node6 \
--listen-addr=localhost:26262 \
--http-addr=localhost:8085 \
--locality=region=eu-west-3,zone=eu-west-3a \
--join='localhost:26257, localhost:26258, localhost:26259'
223
Chapter 9 Production
With the nodes in Paris up and running, let’s run the regions query again to see how
our cluster looks now:
SHOW regions;
region | zones | database_names | primary_region_of
---------------+-----------------+----------------+--------------------
eu-central-1 | {eu-central-1a} | {} | {}
eu-west-3 | {eu-west-3a} | {} | {}
Next, we’ll upgrade the version of CockroachDB on all of our cluster nodes. Starting
with just the nodes of the Frankfurt cluster for simplicity, we’ll update each of the nodes
in turn, with zero downtime.
On my machine, the Frankfurt nodes are currently running on v21.2.0 of
CockroachDB:
$ cockroach version
Build Tag: v21.2.0
Build Time: 2021/11/15 14:00:58
Distribution: CCL
Platform: darwin amd64 (x86_64-apple-darwin19)
Go Version: go1.16.6
C Compiler: Clang 10.0.0
Build Commit ID: 79e5979416cb426092a83beff0be1c20aebf84c6
Build Type: release
$ cockroach version
Build Tag: v21.2.5
Build Time: 2022/02/07 21:04:05
Distribution: CCL
Platform: darwin amd64 (x86_64-apple-darwin19)
Go Version: go1.16.6
C Compiler: Clang 10.0.0
Build Commit ID: 5afb632f77eee9f09f2adfa2943e1979ec4ebedf
Build Type: release
224
Chapter 9 Production
apiVersion: apps/v1
kind: StatefulSet
...
containers:
- name: cockroachdb
image: cockroachdb/cockroach:v21.2.0 # => cockroachdb/cockroach:v21.2.5
imagePullPolicy: IfNotPresent
Kubernetes will perform a rolling upgrade of your nodes, without any downtime, and
will remove each node from the load balancer before replacing it.
Cockroach Labs has some best practices9 to consider when performing an upgrade
between versions (including minor versions like the update we’re about to do). A key
thing to be aware of is auto-finalization and whether it’s enabled or not before you
upgrade your nodes.
If there’s any chance that by upgrading your cluster nodes you could inadvertently
corrupt your database (e.g., when upgrading between versions with breaking changes),
it’s important to disable auto-finalization. This can be achieved as follows:
Before we begin, I’ll run a statement from a previous chapter to get basic information
about the nodes in the cluster:
9
www.cockroachlabs.com/docs/stable/upgrade-cockroach-version.html
225
Chapter 9 Production
As we can see, all of our nodes are running v21.2.0 as expected. Let’s perform our
rolling upgrade now, starting with node one. First, we’ll stop the node (just ctrl-c
the process for now) and check that it has become unavailable. Note that we’ll have to
connect to another node to check the status while node one is down:
As there are no breaking changes between v21.2.0 and v21.2.5, we can simply run the
start command again once we’ve obtained v21.2.5 of the binary; we don’t need to delete
the node’s store directory.
Starting the node again, we can see that the node is available again and its version
has incremented. We can also run the node command against node one again:
We’ll repeat the steps for nodes two and three now. Note that because our cluster has
only three nodes, it’s critical that we perform the upgrade on one node at a time. If we
remove two nodes from the cluster at the same time, the cluster will be unavailable.
226
Chapter 9 Production
Moving a Cluster
Let’s assume that in the last part of this section, we need to move a cluster from one
location to another (e.g., for a cloud provider migration or simply onto newer hardware
in an on-premise scenario). I won’t discuss topics like load balancing or DNS here but
provide a pattern for the migration of nodes.
We’ll start a brand new cluster for this example, starting with a three-node cluster
in AWS’s eu-central-1 (Frankfurt) region and moving them into GCP’s europe-west1 (St.
Ghislain) region. Everything will be running locally, so note that no cloud migration is
actually taking place.
Before we begin, it’s important to discuss the order in which we’ll add and remove
nodes. We have a three-node cluster, with a replication factor of three (as can be seen
by running SHOW ZONE CONFIGURATION FROM DATABASE defaultdb), so we should keep
a three-node cluster available at all times. This means bringing up a node before taking
another node down and waiting for all replicas to be rebalanced onto the new node
before starting work on the next node.
First, we’ll start node1, node2, and node3 in the Frankfurt region and initialize the
cluster:
$ cockroach start \
--insecure \
--store=node1 \
--listen-addr=localhost:26257 \
--http-addr=localhost:8080 \
--locality=region=eu-central-1,zone=eu-central-1a \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node2 \
--listen-addr=localhost:26258 \
--http-addr=localhost:8081 \
--locality=region=eu-central-1,zone=eu-central-1a \
--join='localhost:26257, localhost:26258, localhost:26259'
$ cockroach start \
--insecure \
--store=node3 \
227
Chapter 9 Production
--listen-addr=localhost:26259 \
--http-addr=localhost:8082 \
--locality=region=eu-central-1,zone=eu-central-1a \
--join='localhost:26257, localhost:26258, localhost:26259'
Next, we’ll start node4, node5, and node6 in the St. Ghislain region and have them
join the cluster. Once any underreplicated ranges have been rebalanced and resolved,
we’ll start tearing down the original cluster nodes:
$ cockroach start \
--insecure \
--store=node4 \
--listen-addr=localhost:26260 \
--http-addr=localhost:8083 \
--locality=region=europe-west1,zone=europe-west1b \
--join='localhost:26257, localhost:26258, localhost:26259,
localhost:26260, localhost:26261, localhost:26262'
$ cockroach start \
--insecure \
--store=node5 \
--listen-addr=localhost:26261 \
--http-addr=localhost:8084 \
--locality=region=europe-west1,zone=europe-west1c \
--join='localhost:26257, localhost:26258, localhost:26259,
localhost:26260, localhost:26261, localhost:26262'
$ cockroach start \
--insecure \
--store=node6 \
--listen-addr=localhost:26262 \
--http-addr=localhost:8085 \
--locality=region=europe-west1,zone=europe-west1d \
--join='localhost:26257, localhost:26258, localhost:26259,
localhost:26260, localhost:26261, localhost:26262'
228
Chapter 9 Production
Note that I’ve included all of the new and existing hosts to the --join argument; this
will prevent the cluster from becoming unavailable once I start removing the old nodes.
With the new nodes in place and all underreplicated ranges resolved, let’s run the
node command to see how our cluster is looking:
-[ RECORD 1 ]
id | 1
address | localhost:26257
locality | region=eu-central-1,zone=eu-central-1a
is_available | true
is_live | true
-[ RECORD 2 ]
id | 2
address | localhost:26259
locality | region=eu-central-1,zone=eu-central-1a
is_available | true
is_live | true
-[ RECORD 3 ]
id | 3
address | localhost:26258
locality | region=eu-central-1,zone=eu-central-1a
is_available | true
is_live | true
-[ RECORD 4 ]
id | 4
address | localhost:26260
locality | region=europe-west1,zone=europe-west1b
is_available | true
is_live | true
229
Chapter 9 Production
-[ RECORD 5 ]
id | 5
address | localhost:26261
locality | region=europe-west1,zone=europe-west1c
is_available | true
is_live | true
-[ RECORD 6 ]
id | 6
address | localhost:26262
locality | region=europe-west1,zone=europe-west1d
is_available | true
is_live | true
Everything’s looking good. All six nodes are operational. Once all remaining
underreplicated ranges are resolved (see Figure 9-1 for an example of healthy
replication) and you’ve taken any backups required, it’s safe to remove the original three
nodes from the cluster and finish the move.
Let’s start by decommissioning node1, node2, and node3. This process will ensure
that all ranges are replicated away from these nodes and then remove them from the
cluster. Note that I’m connected to node6 but I could have connected to any node in the
cluster to perform this operation:
230
Chapter 9 Production
Once the nodes are decommissioned, check that the replication status in the
dashboard still looks like Figure 9-1 and then shut the nodes down.
You’ve just moved your cluster from AWS’s Frankfurt region to GCP’s St.
Ghislain region!
• Full backups – Full backups contain all of the data in your cluster
(without replication). Say, for example, you replicate 1GB of data
across five nodes; your backup will contain 1GB of data, not 5GB. Full
backups are available for all clusters.
10
www.cockroachlabs.com/docs/v21.2/take-full-and-incremental-backups
231
Chapter 9 Production
either the latest data or data from a previous point in time. These are
available for Enterprise clusters.
Let’s run through each of the backup methods to see how they work for different
use cases. I’ll run each of the following examples in a demo cluster (which enables
Enterprise features). To enable Enterprise features in a regular cluster, configure the
following settings:
Before we create any backups, I’ll create a small database and table to prove that our
backup and restore operations are successful. For this, I’ll create a new sensor_reading
table that will work nicely for all of the backup methods:
Just one more step before we’re ready to backup. Unlike the IMPORT statement,
which can use an HTTP endpoint, backups and restorations need to use cloud provider
blob storage (e.g., S3, Google Cloud Storage, and Azure Storage). Let’s create a few S3
buckets for our backup examples:
232
Chapter 9 Production
{
"Location": "http://practical-cockroachdb-backups.s3.amazonaws.com/"
}
{
"Location": "http://practical-cockroachdb-backups-us-west.
s3.amazonaws.com/"
}
{
"Location": "/practical-cockroachdb-backups-us-east"
}
Full Backups
With the database, table, and bucket in place, we’re ready to begin! Let’s start by running
a full backup and restore of the data:
233
Chapter 9 Production
Note that in our backup request, we tell CockroachDB to back up the database as it
was ten seconds ago. This means that it’s not trying to back up live data while it’s being
served to clients. This is a performance recommendation from Cockroach Labs, and ten
seconds is the minimum recommended period; depending on your garbage collection
window (the default is 25 hours), you may want to set this to be further in the past.
Let’s check that S3 has our backup:
{
"Contents": [
{
"Key": "2022/03/06-190437.13/BACKUP-
CHECKPOINT-742270852339073025-CHECKSUM",
"LastModified": "2022-03-06T19:04:48+00:00",
"ETag": "\"79f98a6fd4b39f02b7727c91707b71cd\"",
"Size": 4,
"StorageClass": "STANDARD"
}
...
TRUNCATE sensor_reading;
234
Chapter 9 Production
With the data gone (and the department panicking), it’s time to run our restore. Pay
attention to the backups objects’ directory structure, we’ll need them now:
ERROR: full cluster restore can only be run on a cluster with no tables or
databases but found 4 descriptors: [sensors crdb_internal_region _crdb_
internal_region sensor_reading]
Oh no! It seems we can’t restore unless we have an empty cluster. What if we had
other databases or tables that are not in need of restoration? Happily, CockroachDB has
alternative RESTORE commands you can use, depending on your situation. All of which
can be performed from the full backup we just took:
There’s one more step we need to perform before our table can be restored. We need
to DROP or RENAME it11 before running the restore. As my sensor_reading table is empty,
there’s nothing I need to archive, so DROP works best for me:
11
www.cockroachlabs.com/docs/stable/restore.html#restore-a-cluster
235
Chapter 9 Production
Incremental Backups
A full backup and restore works in this case, as the database and objects are small. If your
cluster is many gigabytes in size, incremental backups may be a more efficient option for
you. Let’s explore incremental backups to see how they work.
First, we’ll insert some new data into our restored sensor_reading table to simulate
data being added to the table over time. This will create a delta between the original data
we inserted at the start of the backups section to now. If CockroachDB does not detect
a change between an incremental backup and the previous backup (whether full or
incremental), you’ll see an empty backup directory:
236
Chapter 9 Production
The number of rows indicates the number of rows backed up by the last backup
operation. Note that because we have not yet backed up our restored dataset, the delta
includes all rows in the table. If you run again now, you’ll see zero rows as there’s nothing
fresh to back up:
Note that because we’re running an incremental backup of the whole cluster,
changes to any other database object will be picked up (and backed up). To restore our
table, we simply need to rerun the restore operation as before. All incremental backups
exist in S3 and are automatically picked up:
Encrypted Backups
To encrypt a backup, you’ll need to create your own encryption key and use that to
encrypt the backup before it’s persisted in cloud storage. Let’s create an encryption key
and use it to create an encrypted backup now:
To use the encryption key for backups and restores, simply pass an argument to the
BACKUP/RESTORE command as follows:
If you attempt to restore the backup without a KMS key, you’ll receive an error, as
CockroachDB is aware that this backup has been manually encrypted with a KMS key:
238
Chapter 9 Production
Restoring from a backup with the use of a KMS key performs the restore as expected:
239
Chapter 9 Production
Note that the number of rows restored is less than the number of rows backed up.
This is because a backup was made across the whole cluster, while the restore targets just
the sensor_reading table.
If you’d rather not use keys stored in your cloud provider for encrypting backups,
you can provide your own password to use for encryption. The process of encrypting
backups in this manner is very similar to encrypt backups with cloud keys:
Locality-Aware Backups
To make a backup locality-aware, we simply need to pass some additional configuration
to the BACKUP command to make sure that backups remain in their respective regions.
Let’s create a new cluster using the demo command to simulate a multiregion cluster.
In the following example, we specify that by default, CockroachDB should back up to
the us-west bucket, except for nodes with the us-east-1 locality, which will back up to the
us-east bucket:
BACKUP INTO (
's3://practical-cockroachdb-backups-us-west?AWS_ACCESS_KEY_ID=****&AWS_
SECRET_ACCESS_KEY=****&COCKROACH_LOCALITY=default',
240
Chapter 9 Production
's3://practical-cockroachdb-backups-us-east?AWS_ACCESS_KEY_ID=****&AWS_
SECRET_ACCESS_KEY=****&COCKROACH_LOCALITY=region%3Dus-east-1'
);
KMS keys and encryption passphrases can be used in conjunction with regional
backups, making for flexible backup and restores with CockroachDB.
Scheduled Backups
It’s safe to assume that you won’t want to manually back up your cluster at midnight
every night. That’s where CockroachDB’s backup schedules come in. The following
statement creates two backup schedules:
The output of this statement will contain information for two newly created schedules:
• Weekly full backup:
• ID: 742844058827390977
• Status: ACTIVE
241
Chapter 9 Production
• ID: 742844058837024769
Once the initial daily backup has finished, its status will go from PAUSED to
ACTIVE. This can be seen with a call to SHOW SCHEDULES:
-[ RECORD 1 ]
id | 742844058837024769
label | cluster_backup
schedule_status | ACTIVE
next_run | 2022-03-13 00:00:00+00
recurrence | @weekly
-[ RECORD 3 ]
id | 742844058827390977
label | cluster_backup
schedule_status | ACTIVE
next_run | 2022-03-09 00:00:00+00
recurrence | @daily
Our daily backup job will next execute tomorrow at midnight, whereas our weekly
backup job will next execute on Sunday.
We asked CockroachDB to start our first daily backup immediately. This will
unlikely be your desired behavior if creating the schedule in the middle of the day. To
set a different time for the first run to start, simply replace the first_run value with a
timestamp. For example:
242
Chapter 9 Production
Cluster Design
When designing your cluster, there are numerous configurations you’ll need to decide
between to get optimal performance and resilience. In this section, we’ll focus on these
decisions and their trade-offs.
Cluster Sizing
If you think about your CockroachDB cluster as a collection of vCPUs (Virtual CPUs) that
are distributed across nodes, the process of architecting your cluster becomes simpler.
There are trade-offs between having a cluster with a small number of large nodes
and a cluster with a large number of smaller nodes.
In a small cluster of large nodes, the additional computing power of each machine
and fewer network hops required to satisfy large queries make for a more stable cluster.
In a large cluster of small nodes, the additional nodes to distribute data and
parallelize distributed operations like backups and restores make for a more resilient
cluster.
The decision between stability and resilience is yours to make, but as a rule-of-
thumb, Cockroach Labs recommends that you meet in the middle and distribute your
vCPUs across as few nodes as possible while still achieving resilience. Their Production
Checklist12 offers a detailed breakdown of each scenario.
Node Sizing
Cockroach Labs provides a simple set of recommendations for sizing your cluster
nodes based on their vCPU count. Let’s go through each of the recommendations
against the minimum and recommended vCPU counts. Note that these are just general
recommendations. Depending on your unique performance requirements, you may
require different ratios:
12
www.cockroachlabs.com/docs/stable/recommended-production-settings.html
243
Chapter 9 Production
It’s often said that “premature optimization is the root of all evil.” The same applies
when creating the architecture your CockroachDB cluster will live in. Back in 2018, I
was creating a CockroachDB cluster and asked the Cockroach Labs team whether they
recommended immediately placing an external cache in front of my CockroachDB
cluster to cater for read-heavy workloads. Their response was to start by harnessing
CockroachDB’s own cache. This would result in fewer network hops and would provide
query atomicity, whereas a combination of database and cache would not.
With CockroachDB, you have the freedom to not only scale the number of nodes
in your cluster but also the nodes themselves. So design to support a sensible initial
capacity and grow from there.
Monitoring
An important aspect of running any software safely in production is monitoring. In this
section, we’ll configure monitoring for a CockroachDB cluster and raise alerts against a
simple metric.
For this example, we’ll use Prometheus, a popular open source monitoring system
that CockroachDB has great support for.
First, we’ll create a cluster to monitor:
244
Chapter 9 Production
$ cockroach start \
--insecure \
--store=node1 \
--listen-addr=localhost:26257 \
--http-addr=localhost:8080 \
--join=localhost:26257,localhost:26258,localhost:26259
$ cockroach start \
--insecure \
--store=node2 \
--listen-addr=localhost:26258 \
--http-addr=localhost:8081 \
--join=localhost:26257,localhost:26258,localhost:26259
$ cockroach start \
--insecure \
--store=node3 \
--listen-addr=localhost:26259 \
--http-addr=localhost:8082 \
--join=localhost:26257,localhost:26258,localhost:26259
Next, we’ll create an instance of Prometheus to monitor our three nodes. We’ll create
prometheus.yml and alert_rules.yml configuration files and point the Prometheus
instance at those.
The prometheus.yml file configures the basic properties of Prometheus. It dictates
how frequently Prometheus will scrape for metrics, which hosts to scrape metrics from,
and which URLs those hosts are serving metrics on. Note that because I’ll be running
Prometheus using Docker, I use the host.docker.internal DNS name, which resolves
out of the container to my host machine:
global:
scrape_interval: 10s
rule_files:
- alert_rules.yml
245
Chapter 9 Production
scrape_configs:
- job_name: cockroachdb
metrics_path: /_status/vars
static_configs:
- targets:
- host.docker.internal:8080
- host.docker.internal:8081
- host.docker.internal:8082
The alert_rules.yml file configures alert groups containing metric rules. If any
metric rules breach configured thresholds, an alert will be raised for that group. For
this example, I create an alert that will fire if CockroachDB detects that a node has been
offline for one minute:
groups:
- name: node_down
rules:
- alert: NodeDown
expr: up == 0
for: 1m
labels:
severity: critical
Next, we’ll create an instance of Alertmanager. This will receive alerts from
Prometheus and direct them to a receiver. With the following configuration, I use a
simple HTTP receiver to send notifications to https://httpbin.org:
global:
resolve_timeout: 5m
route:
group_by: ['alertname']
group_wait: 5s
group_interval: 5s
repeat_interval: 1h
receiver: api_notify
246
Chapter 9 Production
receivers:
- name: api_notify
webhook_configs:
- url: https://httpbin.org/post
$ docker run \
--name prometheus \
--rm -it \
-p 9090:9090 \
-v ${PWD}/prometheus.yml:/etc/prometheus/prometheus.yml \
-v ${PWD}/alert_rules.yml:/etc/prometheus/alert_rules.yml \
prom/prometheus
$ docker run \
--name alertmanager \
--rm -it \
-p 9093:9093 \
-v ${PWD}/alertmanager.yml:/etc/alertmanager/alertmanager.yml \
prom/alertmanager
If you visit http://localhost:9090/alerts, you’ll see that the NodeDown alert is active
and reporting that no nodes are currently down:
If we kill a node now, the alert will enter a “pending” state before “firing.” I’ll
terminate the cockroach process for node3 to demonstrate these alert statuses.
247
Chapter 9 Production
After a short while, the alert will enter the pending state:
If we restart node3 and wait a short while, the alert will clear and report that our
cluster is once again healthy.
248
Index
A serialization, 170
testing with application code, 179–184
Antipatterns, 152–156
argPlaceholders, 212
Availability zones (AZs), 21, 140, 141, 151 C
The California Consumer Privacy Act
(CCPA), 124, 126–128
B Certificate Authority (CA), 70, 130
Backing up and restoring data Change Data Capture (CDC), 6, 114, 116,
backups with revision history, 231 119, 120, 156
encrypted backups, 231, 237–240 Changefeeds, 116
full backups, 231, 233–235 Cloud provider blob storage, 232
incremental backups, 231, 236 Cluster design
locality-aware backups, 232 cluster sizing, 243
scheduled backups, 241, 242 node sizing, 243, 244
Backup methods, 232 Cluster maintenance
Backups with revision history, 231–232 cluster-wide operations, 220
Basic Production topology, 140, 141, 160 Frankfurt cluster, 223
Bean About Town, 161 gossip protocol, 221
Black Box testing Kubernetes users, 225
API server listening, 173 node start command, 220
application, 166 scaling, 221, 222
application, testing, 173 Cockroach binary, 67
database’s internal structure, 165 cockroach cert command, 70, 71
error handling, 174 command tree, 68
GET request handler, 167, 169, 172 demo commands, 69
malformatted customer ID, 176 import command, 78
mechanism, 166 node command, 73–76
multistatement transaction, 176 recommission command, 77
POST request endpoint, 168 sample databases, 69, 70
POST request handler, 167 sqlfmt command, 78, 79
Product class, 168 start and start-single-node
request for nonexistent ID, 176 commands, 68, 69
249
© Rob Reid 2022
R. Reid, Practical CockroachDB, https://doi.org/10.1007/978-1-4842-8224-3
INDEX
250
INDEX
251
INDEX
252
INDEX
253
INDEX
V Z
v21.2.0, 226
Zone Failure Goal, 27–28
v21.2.5, 224, 226
254