(ClickHouse at Segment)

Alan Braithwaite

@caust1c

A Modern Database
with
Modern Orchestration

What is Segment?

Collect

Standardize

Synthesize

Dispatch

Roadmap

Background &
​Motivation

Clickhouse &
​Kubernetes

Performance &
​Observability

Background & Motivation

Once upon a time...

Hack Week 2018

  • Legacy Aggregation System
  • Redis and Go based
  • Complex and Inflexible to change
  • High performance
  • Easy to operate
  • Cost equal or less than Redis solution

Legacy Counters on Redis

MySQL (Aurora)

Google BigQuery

Snowflake

Spark

Flink

Druid

Legacy Counters on Redis

MySQL (Aurora)

Google BigQuery

Snowflake

Spark

Flink

Druid

Clickhouse & Kubernetes

Amazon's Elastic Container Service

If Kubernetes had an annoying younger sibling, ECS would be it

Used i3 nodes with local NVME disks

Started 1 node for POC

Lasted longer than we wanted it to

Struggled to get Distributed Config working

EKS Ready for use internally — April 19

 

New i3en Instances Announced — May 8

Design inspired by LogHouse

https://github.com/flant/loghouse

github.com/yandex/ClickHouse/issues/5287
Initial Report — May 15
Root Cause Identified — May 20
Fix Implemented — June 5
Shipped in release — June 24

9 * i3en.3xlarge nodes

 

7.5TB NVME SSDs, 96GB Memory, 12 vCPUs

Clickhouse has full node usage


Kubernetes mounts NVME disk inside container


Cross Replication using default databases trick

 

Set env vars for macros in configmap script directory

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: counters
spec:
  selector:
    matchLabels:
      app: clickhouse
  updateStrategy:
    type: OnDelete
  podManagementPolicy: Parallel
  serviceName: "clickhouse"
  replicas: 9
  template:
    metadata:
      labels:
        app: clickhouse
    spec:
      terminationGracePeriodSeconds: 10
      nodeSelector:
        segment.com/pool: clickhouse
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - clickhouse
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: clickhouse
        image: "<segment-private-docker-repo>/clickhouse-server:eff47da"
        volumeMounts:
        - name: datadir
          mountPath: /var/lib/clickhouse
        - name: configdir
          mountPath: /etc/clickhouse-server/config.d
      - name: clickhouse-mon
        image: "<segment-private-docker-repo>/clickhouse-mon:587ea6b"
      volumes:
        - name: datadir
          hostPath:
            path: /data
            type: Directory
        - name: configdir
          configMap:
            name: clickhouse-config
FROM yandex/clickhouse-server:19.9.2.4

RUN apt-get update && apt-get install -y iptables ca-certificates
COPY start-clickhouse.sh /usr/local/bin/start-clickhouse

ENTRYPOINT /usr/local/bin/start-clickhouse
#!/usr/bin/env bash -xe
HOST=`hostname -s`
if [[ $HOST =~ (.*)-([0-9]+)$ ]]; then
    NAME=${BASH_REMATCH[1]}
    ORD=${BASH_REMATCH[2]}
else
    echo "Failed to parse name and ordinal of Pod"
    exit 1
fi
# set the environment before we exec
. /etc/clickhouse-server/config.d/$ORD.sh
exec /entrypoint.sh

Dockerfile

start-clickhouse.sh

<yandex>
  <macros replace="replace">
    <r0shard from_env="CH_SHARD_R0"/>
    <r0replica from_env="CH_REPLICA_R0"/>
    <r1shard from_env="CH_SHARD_R1"/>
    <r1replica from_env="CH_REPLICA_R1"/>
    <cluster>counters_cluster</cluster>
  </macros>
</yandex>

macros.xml

$ ls config.d
0.sh   1.sh   2.sh
3.sh   4.sh   5.sh
6.sh   7.sh   8.sh
$ cat config.d/1.sh
export CH_SHARD_R0=01
export CH_REPLICA_R0=00
export CH_SHARD_R1=02
export CH_REPLICA_R1=01

Cross Replication

Cross Replication

ENGINE = Distributed(
  'counters_cluster',
  -- Database name: use the default database of whichever node is connected to.
  -- This will either be r0 or r1, depending on the (shard, node) combination.
  -- This mapping is configured in `locals.xml`.
  '',
  -- Table name: the table name within r0/r1 that will be used to perform the query.
  global_events_with_properties,
  -- Sharding key: randomly choose a shard to write to for every insertion.
  rand()
);
  <remote_servers>
    <counters_cluster>
      <shard>
        <internal_replication>true</internal_replication>
        <replica>
          <default_database>r0</default_database>
          <host>counters-0.clickhouse.default.svc.cluster.local</host>
          <port>9000</port>
        </replica>
        <replica>
          <default_database>r1</default_database>
          <host>counters-1.clickhouse.default.svc.cluster.local</host>
          <port>9000</port>
        </replica>
      </shard>
      <!-- ... -->
    </counters_cluster>
  <remote_servers>

Performance & Observability

Insertion Performance

 

Hitting wall when inserting 100k batches into distributed table (over raw data)

 

Batches became ~11k each (100k / 9 nodes)

Active Part Count Climbed
 

Due to async write nature, no backpressure

Solution: insert directly into shards

Drawback: client doesn't try second replica for shard

If there's a failure of a node,
that shard will stop getting data

Merges were now able to keep up
Active Part count went down

Future

Convert more existing systems to Clickhouse

 

Better Observability features for Segment Customers

Open Source
Kubernetes Chart &
"clickhouse-mon"

Alan Braithwaite

@caust1c

Thanks

Segment Clickhouse

By Alan Braithwaite

Segment Clickhouse

  • 4,272