avatarTony

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

12801

Abstract

containerPort:</span> <span class="hljs-number">2181</span> <span class="hljs-attr">name:</span> <span class="hljs-string">client</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">2888</span> <span class="hljs-attr">name:</span> <span class="hljs-string">server</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">3888</span> <span class="hljs-attr">name:</span> <span class="hljs-string">leader-election</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">7000</span> <span class="hljs-attr">name:</span> <span class="hljs-string">prometheus</span> <span class="hljs-comment"># See those links for proper startup settings:</span> <span class="hljs-comment"># https://github.com/kow3ns/kubernetes-zookeeper/blob/master/docker/scripts/start-zookeeper</span> <span class="hljs-comment"># https://clickhouse.yandex/docs/en/operations/tips/#zookeeper</span> <span class="hljs-comment"># https://github.com/ClickHouse/ClickHouse/issues/11781</span> <span class="hljs-attr">command:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">bash</span> <span class="hljs-bullet">-</span> <span class="hljs-string">-x</span> <span class="hljs-bullet">-</span> <span class="hljs-string">-c</span> <span class="hljs-bullet">-</span> <span class="hljs-string">| SERVERS=1 && HOST=hostname -s && DOMAIN=hostname -d && CLIENT_PORT=2181 && SERVER_PORT=2888 && ELECTION_PORT=3888 && PROMETHEUS_PORT=7000 && ZOO_DATA_DIR=/var/lib/zookeeper/data && ZOO_DATA_LOG_DIR=/var/lib/zookeeper/datalog && { echo "clientPort={CLIENT_PORT}" echo 'tickTime=2000' echo 'initLimit=300' echo 'syncLimit=10' echo 'maxClientCnxns=2000' echo 'maxSessionTimeout=60000000' echo "dataDir={ZOO_DATA_DIR}" echo "dataLogDir={ZOO_DATA_LOG_DIR}" echo 'autopurge.snapRetainCount=10' echo 'autopurge.purgeInterval=1' echo 'preAllocSize=131072' echo 'snapCount=3000000' echo 'leaderServes=yes' echo 'standaloneEnabled=false' echo '4lw.commands.whitelist=*' echo 'metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider' echo "metricsProvider.httpPort={PROMETHEUS_PORT}" } > /conf/zoo.cfg && { echo "zookeeper.root.logger=CONSOLE" echo "zookeeper.console.threshold=WARN" echo "log4j.rootLogger=${zookeeper.root.logger}" echo "log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender" echo "log4j.appender.CONSOLE.Threshold=${zookeeper.console.threshold}" echo "log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout" echo "log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n" } > /conf/log4j.properties && echo 'JVMFLAGS="-Xms128M -Xmx1G -XX:+UseG1GC -XX:+CMSParallelRemarkEnabled"' > /conf/java.env && 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 && mkdir -p {ZOO_DATA_DIR} &amp;&amp; mkdir -p {ZOO_DATA_LOG_DIR} && export MY_ID=((ORD+1)) &amp;&amp; echo MY_ID > ZOO_DATA_DIR/myid &amp;&amp; for (( i=1; i&lt;=SERVERS; i++ )); do echo "server.i=NAME-((i-1)).DOMAIN:SERVER_PORT:ELECTION_PORT" >> /conf/zoo.cfg; done && chown -Rv zookeeper "ZOO_DATA_DIR" "ZOO_DATA_LOG_DIR" "ZOO_LOG_DIR" "ZOO_CONF_DIR" && zkServer.sh start-foreground </span> <span class="hljs-attr">readinessProbe:</span> <span class="hljs-attr">exec:</span> <span class="hljs-attr">command:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">bash</span> <span class="hljs-bullet">-</span> <span class="hljs-string">-c</span> <span class="hljs-bullet">-</span> <span class="hljs-string">"OK=(echo ruok | nc 127.0.0.1 2181); if [[ \"OK" == "imok" ]]; then exit 0; else exit 1; fi"</span> <span class="hljs-attr">initialDelaySeconds:</span> <span class="hljs-number">10</span> <span class="hljs-attr">timeoutSeconds:</span> <span class="hljs-number">5</span> <span class="hljs-attr">livenessProbe:</span> <span class="hljs-attr">exec:</span> <span class="hljs-attr">command:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">bash</span> <span class="hljs-bullet">-</span> <span class="hljs-string">-c</span> <span class="hljs-bullet">-</span> <span class="hljs-string">"OK=(echo ruok | nc 127.0.0.1 2181); if [[ \"OK" == "imok" ]]; then exit 0; else exit 1; fi"</span> <span class="hljs-attr">initialDelaySeconds:</span> <span class="hljs-number">10</span> <span class="hljs-attr">timeoutSeconds:</span> <span class="hljs-number">5</span> <span class="hljs-attr">volumeMounts:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">data-volume-ch</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/var/lib/zookeeper</span> <span class="hljs-comment"># Run as a non-privileged user</span> <span class="hljs-attr">securityContext:</span> <span class="hljs-attr">runAsUser:</span> <span class="hljs-number">1000</span> <span class="hljs-attr">fsGroup:</span> <span class="hljs-number">1000</span> <span class="hljs-attr">volumeClaimTemplates:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">metadata:</span> <span class="hljs-attr">name:</span> <span class="hljs-string">data-volume-ch</span> <span class="hljs-attr">spec:</span> <span class="hljs-attr">accessModes:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">ReadWriteOnce</span> <span class="hljs-attr">resources:</span> <span class="hljs-attr">requests:</span> <span class="hljs-attr">storage:</span> <span class="hljs-string">20Gi</span></pre></div><p id="be4c">To deploy this manifest we will create another script:</p><div id="564f"><pre><span class="hljs-meta">#!/bin/bash</span>

ZK_NAMESPACE=<span class="hljs-string">"<span class="hljs-variable">${ZK_NAMESPACE:-ch-zookeeper}</span>"</span>

CUR_DIR=<span class="hljs-string">"<span class="hljs-subst">(cd <span class="hljs-string">"<span class="hljs-subst">(dirname <span class="hljs-string">"<span class="hljs-variable">${BASH_SOURCE[0]}</span>"</span>)</span>"</span> >/dev/null 2>&1 && pwd)</span>"</span>

kubectl create namespace <span class="hljs-string">"<span class="hljs-variable">{ZK_NAMESPACE}</span>"</span></pre></div><div id="638e"><pre><span class="hljs-attribute">kubectl</span> --namespace=<span class="hljs-string">"<span class="hljs-variable">{ZK_NAMESPACE}</span>"</span> apply -f <span class="hljs-string">"<span class="hljs-variable">${CUR_DIR}</span>/zookeeper-1-nodes.yaml"</span></pre></div><p id="c95a">Save this file as <code>create-zookeeper-1-nodes.sh</code> . The script will take care of creating a namespace (in our case <b>ch-zookeeper</b>) and apply this manifest there. Simply run the following in the directory of the file:</p><div id="f457"><pre>./create-zookeeper-1-nodes.sh</pre></div><h2 id="1de5">2.2) Check the Zookeeper Cluster</h2><p id="a63e">If the pod status is running, the Zookeeper deployment is successful. The status of the pods can be viewed through the following command:</p><div id="8def"><pre>kubectl <span class="hljs-keyword">get</span> <span class="hljs-keyword">all</span> -n ch-zookeeper</pre></div><figure id="3da7"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*gqfWHD8mC5EqeKT4Ef0SBg.png"><figcaption></figcaption></figure><p id="2d43">You will see a Service and a Pod for the Zookeeper. The importance of these two is not very evident with just one Zookeeper pod; but imagine having a 3 node cluster, you can use the Service host and port and Kubernetes will take care of redirecting you to any available Zookeeper pod in the cluster.</p><h2 id="36bd">2.3) Deploy the clickhouse cluster</h2><p id="4ced">Having the ClickHouse Operator & Zookeeper Cluster up and running the next step is to define the ClickHouse Cluster Installation. This is defined in the <code>clickhouse-cluster.yaml</code>. Some important parts to note are in bold below (since most of the configuration is handled by the operator):</p><div id="d73e"><pre><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">"clickhouse.altinity.com/v1"</span> <span class="hljs-attr">kind:</span> <span class="hljs-string">"ClickHouseInstallation"</span> <span class="hljs-attr">metadata:</span> <span class="hljs-attr">name:</span> <span class="hljs-string">"medium"</span> <span class="hljs-attr">spec:</span> <span class="hljs-comment"># Templates are specified for all clusters ... if you want explicitly for each cluster then apply below</span> <span class="hljs-attr">defaults:</span> <span class="hljs-attr">templates:</span> <span class="hljs-attr">podTemplate:</span> <span class="hljs-string">clickhouse</span> <span class="hljs-attr">dataVolumeClaimTemplate:</span> <span class="hljs-string">data-volume-clickhouse</span> <span class="hljs-attr">logVolumeClaimTemplate:</span> <span class="hljs-string">data-volume-clickhouse</span> <span class="hljs-attr">configuration:</span> <span class="hljs-attr">clusters:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">"medium-cluster"</span> <span class="hljs-attr">layout:</span> <span class="hljs-attr">shardsCount:</span> <span class="hljs-number">2</span> <span class="hljs-attr">replicasCount:</span> <span class="hljs-number">2</span> <span class="hljs-attr">zookeeper:</span> <span class="hljs-attr">nodes:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">host:</span> <span class="hljs-string">zookeeper.ch-zookeeper</span> <span class="hljs-attr">port:</span> <span class="hljs-number">2181</span> <span class="hljs-attr">templates:</span> <span class="hljs-attr">podTemplates:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">clickhouse</span> <span class="hljs-attr">spec:</span> <span class="hljs-attr">securityContext:</span> <span class="hljs-attr">runAsUser:</span> <span class="hljs-number">101</span> <span class="hljs-attr">runAsGroup:</span> <span class="hljs-number">101</span> <span class="hljs-attr">fsGroup:</span> <span class="hljs-number">101</span> <span class="hljs-attr">containers:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">clickhouse</span> <span class="hljs-attr">image:</span> <span class="hljs-string">yandex/clickhouse-server:21.10</span> <span class="hljs-attr">ports:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">http</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">8123</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">client</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">9000</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">

Options

name:</span> <span class="hljs-string">interserver</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">9009</span> <span class="hljs-attr">volumeMounts:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">data-volume-clickhouse</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/var/lib/clickhouse</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">data-volume-clickhouse</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/var/log/clickhouse-server</span> <span class="hljs-attr">volumeClaimTemplates:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">data-volume-clickhouse</span> <span class="hljs-attr">reclaimPolicy:</span> <span class="hljs-string">Retain</span> <span class="hljs-attr">spec:</span> <span class="hljs-attr">accessModes:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">ReadWriteOnce</span> <span class="hljs-attr">resources:</span> <span class="hljs-attr">requests:</span> <span class="hljs-attr">storage:</span> <span class="hljs-string">150Gi</span></pre></div><p id="cc64">The name <b>medium </b>will be the installation name. While the name <b>medium-cluster</b> is the name of the cluster within the installation. We can have multiple clusters but only 1 installation. The number of shards and replicas for that cluster is defined by <b>shardsCount </b>and<b> replicasCount. </b>We also need to specify the endpoints of the zookeeper we just created <b>zookeeper.ch-zookeeper:2181</b>.</p><p id="3c5e">Running the <code>./create-clickhouse-cluster.sh</code> will create a number of servers to access those ClickHouse pods as well, but the most important one would be the LoadBalancer services which will take care of redirecting you to the appropriate shard, rather than you specifying which shard. Again we define the <code>create-clickhouse-cluster.sh</code> file:</p><div id="c307"><pre><span class="hljs-meta">#!/bin/bash</span>

CH_NAMESPACE=<span class="hljs-string">"<span class="hljs-variable">${CH_NAMESPACE:-clickhouse-operator}</span>"</span>

CUR_DIR=<span class="hljs-string">"<span class="hljs-subst">(cd <span class="hljs-string">"<span class="hljs-subst">(dirname <span class="hljs-string">"<span class="hljs-variable">${BASH_SOURCE[0]}</span>"</span>)</span>"</span> >/dev/null 2>&1 && pwd)</span>"</span>

kubectl create namespace <span class="hljs-string">"<span class="hljs-variable">${CH_NAMESPACE}</span>"</span>

kubectl apply -n <span class="hljs-string">"<span class="hljs-variable">{CH_NAMESPACE}</span>"</span> -f <span class="hljs-string">"<span class="hljs-variable">{CUR_DIR}</span>/clickhouse-cluster.yaml"</span></pre></div><p id="e7bb">Run the script</p><div id="8965"><pre>./<span class="hljs-keyword">create</span>-clickhouse-<span class="hljs-keyword">cluster</span>.sh</pre></div><h2 id="a92d">2.4) Check the Clickhouse Cluster</h2><p id="07cb">If the pod shards (<b>0–0–0 </b>and <b>1–0–0</b>) and the respective replicas (<b>0–1–0</b> and <b>1–1–0</b>) status is running, the ClickHouse Cluster deployment is successful. The status of the pods can be viewed through the following command:</p><figure id="e499"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*_Qn1VzGj34zNO4g7YrXpug.png"><figcaption></figcaption></figure><p id="a219">Each pod will be associated with a service so you can access that shard/replica directly. The most important one from the services when you have a replicated and distributed cluster is the <b>LoadBalancer, </b>so you are directed to less heavily loaded and readily available ClickHouse pods.</p><h2 id="0171">2.5) Connect to the cluster</h2><p id="f84a">Since we are trying to access the LoadBalancer service outside of the Kubernetes cluster we need to perform port forwarding:</p><div id="ddcf"><pre>kubectl port-<span class="hljs-keyword">forward</span> svc/clickhouse-medium-test <span class="hljs-number">8123</span>:<span class="hljs-number">8123</span><span class="hljs-keyword">namespace</span> clickhouse-<span class="hljs-keyword">operator</span></pre></div><p id="16ca">This command essentially redirects any commands from <b>localhost:8123</b> to http://clickhouse-medium-test.clickhouse-operator.svc.cluster.local:8123</p><p id="f984">Now you can use any IDE (DBeaver is a free solution) or even the clickhouse-client to access your cluster. In my case, I am a big fan of DataGrip:</p><figure id="4e2c"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*WWA268WSMj933Hvp1CRdxQ.png"><figcaption></figcaption></figure><p id="1a1f">Note that for test purposes the username and password were set as default and null password respectively. Make sure to change those from inside the clickhouse operator yaml config:</p><figure id="bf24"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*YsLJ28KDFvLWvRVVdB-Sqw.png"><figcaption></figcaption></figure><h2 id="e2c7">2.6) Built-in macros</h2><p id="b00b">The Clickhouse Operator provides a set of macros, which are:</p><ol><li><code>{installation}</code> -- ClickHouse Installation name</li><li><code>{cluster}</code> -- primary cluster name</li><li><code>{replica}</code> -- replica name in the cluster, maps to pod service name</li><li><code>{shard}</code> -- shard id</li></ol><p id="7bf4">ClickHouse also supports internal macros <code>{database}</code> and <code>{table}</code> that maps to the current database and table respectively.</p><p id="aa97">The following code shows the macro automatically created by the current cluster, which we can use when creating a table.</p><div id="719d"><pre><yandex> <macros> <installation>medium-test</installation> <<span class="hljs-keyword">all</span>-sharded-shard><span class="hljs-number">0</span></<span class="hljs-keyword">all</span>-sharded-shard> <<span class="hljs-keyword">cluster</span>>medium-<span class="hljs-keyword">cluster</span></<span class="hljs-keyword">cluster</span>> <shard><span class="hljs-number">0</span></shard> <<span class="hljs-keyword">replica</span>>chi-medium-test-medium-<span class="hljs-keyword">cluster</span><span class="hljs-number">-0</span><span class="hljs-number">-0</span></<span class="hljs-keyword">replica</span>> </macros> </yandex></pre></div><h1 id="899b">3) Test the cluster</h1><p id="a677">Since you are connected to the cluster, let's run some commands and view the output.</p><h2 id="29fa">3.1) Create a set of tables</h2><div id="4145"><pre><span class="hljs-keyword">CREATE</span> TABLE events_local <span class="hljs-keyword">on</span> <span class="hljs-keyword">cluster</span> <span class="hljs-string">'{cluster}'</span> ( event_date Date, event_type Int32, article_id Int32, <span class="hljs-built_in">title</span> <span class="hljs-keyword">String</span> ) engine=ReplicatedMergeTree(<span class="hljs-string">'/clickhouse/{installation}/{cluster}/tables/{shard}/{database}/{table}'</span>, <span class="hljs-string">'{replica}'</span>, event_date, (event_type, article_id), <span class="hljs-number">8192</span>);</pre></div><div id="069b"><pre><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> events <span class="hljs-keyword">on</span> <span class="hljs-keyword">cluster</span> <span class="hljs-string">'{cluster}'</span> <span class="hljs-keyword">AS</span> events_local ENGINE = Distributed(<span class="hljs-string">'{cluster}'</span>, <span class="hljs-keyword">default</span>, events_local, rand());</pre></div><figure id="d5b8"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*lH891x6T4Oq7zEqokAxaGA.png"><figcaption></figcaption></figure><h2 id="758f">3.2) Insert data</h2><div id="29d3"><pre><span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> events <span class="hljs-keyword">SELECT</span> today(), rand()%<span class="hljs-number">3</span>, <span class="hljs-keyword">number</span>, <span class="hljs-string">'my title'</span> <span class="hljs-keyword">FROM</span> numbers(<span class="hljs-number">100</span>);</pre></div><h2 id="cf58">3.3) View data</h2><div id="3eb3"><pre><span class="hljs-keyword">SELECT</span> <span class="hljs-built_in">count</span>() <span class="hljs-keyword">FROM</span> events; <span class="hljs-keyword">SELECT</span> <span class="hljs-built_in">count</span>() <span class="hljs-keyword">FROM</span> events_local;</pre></div><div id="6553"><pre><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> events;</pre></div><figure id="ea9d"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*FGFdpb80CFis6ZnZjIeSVQ.png"><figcaption></figcaption></figure><p id="99ee">You will notice that the count in <code>events_local</code> is lower than <code>events</code> and that’s because the cluster is taking care of distributing data to the other shards. So always make sure to query the <code>events</code> table since that’s the distributed Engine that will take care to join the data from all shards</p><h1 id="9e8f">4) Check Local Persistent Volume</h1><p id="e22e">The yaml configuration above for the Zookeeper & ClickHouse Installation also creates persistent volume. This is needed to make sure data is independent of the <b>life-cycle</b> of the pod that uses it, meaning that even if the pod shuts down, the data in the volume is not erased. You can check those by running:</p><div id="c80d"><pre>kubectl <span class="hljs-built_in">get</span> pv — <span class="hljs-attribute">sort-by</span>=.spec.capacity.storage</pre></div><figure id="bfe2"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*JAWgi5hZ03sUy8jd6C8sxA.png"><figcaption></figcaption></figure><h1 id="5eb0">5) cluster monitoring</h1><p id="ba01">There are many ways to monitor a Kubernetes deployment but the two best opensource options I found useful for me are Prometheus & Grafana</p><h2 id="230d">5.1) Prometheus</h2><p id="b02f">I will be creating a separate article for Prometheus but in summary, the ClickHouse Operator creates a metrics service. Prometheus can use this address to grab metrics:</p><div id="b6d7"><pre>http://clickhouse-<span class="hljs-keyword">operator</span>-metrics.clickhouse-<span class="hljs-keyword">operator</span>.svc.<span class="hljs-keyword">cluster</span>.<span class="hljs-keyword">local</span>:<span class="hljs-number">8888</span>/metrics</pre></div><h2 id="6bfd">5.2) Grafana Dashboard</h2><p id="bfb7">With the Prometheus and ClickHouse Cluster set up, we can have a combination of dashboards to monitor the environment. Another separate article will be created for Grafana.</p><h1 id="e8ee">6) Conclusion</h1><p id="9432">The ClickHouse operator is simple to install and can handle life-cycle operations for many ClickHouse installations running in a single Kubernetes cluster. It is designed for use cases ranging from quick tests to production data warehouses. So if it’s not clear from the above in summary the operator handles the following tasks:</p><ul><li>Setting up ClickHouse installations</li><li>Changing the configuration of nodes, for example, to increase memory</li><li>Adding and removing shards</li><li>Adding and removing replicas</li><li>Upgrading cluster nodes</li><li>Monitoring cluster status</li><li>Removing ClickHouse installations</li><li>Provides Cluster Metrics for monitoring purposes</li></ul><p id="b270">Altinity are also working on a Dashboard for the ClickHouse Operator that aims to make ClickHouse deployment in Kubernetes easy, painless, and successful. I recommend looking into this as well:</p><div id="6964" class="link-block"> <a href="https://altinity.com/blog/introducing-the-altinity-dashboard-a-ui-for-the-clickhouse-operator"> <div> <div> <h2>Introducing the Altinity Dashboard, a UI for the ClickHouse Operator</h2> <div><h3>The Altinity Dashboard is a new Altinity open source project aimed at making ClickHouse deployment in Kubernetes easy…</h3></div> <div><p>altinity.com</p></div> </div> <div> <div style="background-image: url(https://miro.readmedium.com/v2/resize:fit:320/0*O-fLHeOPm_PmRRQ0)"></div> </div> </div> </a> </div></article></body>

Data Warehouse on Kubernetes

ClickHouse Operator to the rescue

I won’t go into too much detail on why you should consider ClickHouse as your data warehouse since there are a number of articles out there. You can also check my previous articles Setting up a local datawarehouse and Build your own data platform if you want to get acquainted.

The scope of this article is for you to deploy a production-ready ClickHouse environment inside Kubernetes with persistent storage. You will find a lot of detailed information provided by Altinity since they are the creators of this operator. This article will allow you to have a production-ready data warehouse.

So to be able to follow the below steps make sure you have a Kubernetes environment available. In my case, I am using a minikube environment. At the end of the below steps you will end up with the following files:

1) Deploy clickhouse-operator

Instead of pasting a huge yaml file in this article (since the clickhouse-operator is pretty huge) then copy the contents found in the below link and name this file clickhouse-operator-install.yaml:

To deploy this manifest we will create another script:

#!/bin/bash

CH_NAMESPACE="${CH_NAMESPACE:-clickhouse-operator}"

CUR_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"

kubectl create namespace "${CH_NAMESPACE}"

kubectl apply -n "${CH_NAMESPACE}" -f "${CUR_DIR}/clickhouse-cluster.yaml"

Save this file as create-clickhouse-operator-install.sh . The script will take care of creating a namespace (in our case clickhouse-operator) and apply this manifest there. Simply run the following in the directory of the file:

./create-clickhouse-operator-install.sh

1.1) Check the clickhouse-operator

If the pod status is running, the clickhouse operator deployment is successful. The status of the pods can be viewed through the following command:

kubectl get all -n clickhouse-operator

To add with the above yaml file, essentially there are two ConfigMaps that contain CH settings that are important to us being the etc-clickhouse-operator-usersd-files & etc-clickhouse-operator-configd-files.

These interact directly with the actual ClickHouse settings which would be specified in ClickHouse/config.xml or ClickHouse/users.xml respectively. Here the operator takes care of propagating those changes.

Once the operator is running it will listen to ClickHouseInstallation manifest and create an instance based on the specs. Also if a specific cluster is changed or scaled up/down, the operator will notice the change in ConfigMap and apply it to the cluster

2) Deploy the cluster

If you take a close look at the figure of the Data Warehouse, you will notice a cluster of 2 shards and 2 replicas. The storage of each pod uses the way of local persistent volume.

2.1) Deploy Zookeeper

Together with this ClickHouse cluster, a Zookeeper cluster is required since we are going to use Replication. If only shards are going to be used, Zookeeper can be skipped.

You can decide how many replicas you want. To keep things simple we will go with 1 replica so only 1 Zookeeper pod (I don’t recommend this manifest to be in production ideally more replicas are added) … You can use any image or managed service of your liking, as long as you have a zookeeper host and port that can be used by the ClickHouse Cluster

So copy the contents found below and paste them into a file named zookeeper-1-nodes.yaml:

# Setup Service to provide access to Zookeeper for clients
apiVersion: v1
kind: Service
metadata:
  # DNS would be like zookeeper.zoons
  name: zookeeper
  labels:
    app: zookeeper
spec:
  ports:
    - port: 2181
      name: client
    - port: 7000
      name: prometheus
  selector:
    app: zookeeper
    what: node
---
# Setup Headless Service for StatefulSet
apiVersion: v1
kind: Service
metadata:
  # DNS would be like zookeeper-0.zookeepers.etc
  name: zookeepers
  labels:
    app: zookeeper
spec:
  ports:
    - port: 2888
      name: server
    - port: 3888
      name: leader-election
  clusterIP: None
  selector:
    app: zookeeper
    what: node
---
# Setup max number of unavailable pods in StatefulSet
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: zookeeper-pod-disruption-budget
spec:
  selector:
    matchLabels:
      app: zookeeper
  maxUnavailable: 1
---
# Setup Zookeeper StatefulSet
# Possible params:
# 1. replicas
# 2. memory
# 3. cpu
# 4. storage
# 5. storageClassName
# 6. user to run app
apiVersion: apps/v1
kind: StatefulSet
metadata:
  # nodes would be named as zookeeper-0, zookeeper-1, zookeeper-2
  name: zookeeper
spec:
  selector:
    matchLabels:
      app: zookeeper
  serviceName: zookeepers
  replicas: 1
  updateStrategy:
    type: RollingUpdate
  podManagementPolicy: Parallel
  template:
    metadata:
      labels:
        app: zookeeper
        what: node
      annotations:
        prometheus.io/port: '7000'
        prometheus.io/scrape: 'true'
    spec:
      containers:
        - name: kubernetes-zookeeper
          imagePullPolicy: IfNotPresent
          image: "docker.io/zookeeper:3.6.3"
#          resources:
#            requests:
#              memory: "128M"
#              cpu: "1"
#            limits:
#              memory: "1Gi"
#              cpu: "2"
          ports:
            - containerPort: 2181
              name: client
            - containerPort: 2888
              name: server
            - containerPort: 3888
              name: leader-election
            - containerPort: 7000
              name: prometheus
          # See those links for proper startup settings:
          # https://github.com/kow3ns/kubernetes-zookeeper/blob/master/docker/scripts/start-zookeeper
          # https://clickhouse.yandex/docs/en/operations/tips/#zookeeper
          # https://github.com/ClickHouse/ClickHouse/issues/11781
          command:
            - bash
            - -x
            - -c
            - |
              SERVERS=1 &&
              HOST=`hostname -s` &&
              DOMAIN=`hostname -d` &&
              CLIENT_PORT=2181 &&
              SERVER_PORT=2888 &&
              ELECTION_PORT=3888 &&
              PROMETHEUS_PORT=7000 &&
              ZOO_DATA_DIR=/var/lib/zookeeper/data &&
              ZOO_DATA_LOG_DIR=/var/lib/zookeeper/datalog &&
              {
                echo "clientPort=${CLIENT_PORT}"
                echo 'tickTime=2000'
                echo 'initLimit=300'
                echo 'syncLimit=10'
                echo 'maxClientCnxns=2000'
                echo 'maxSessionTimeout=60000000'
                echo "dataDir=${ZOO_DATA_DIR}"
                echo "dataLogDir=${ZOO_DATA_LOG_DIR}"
                echo 'autopurge.snapRetainCount=10'
                echo 'autopurge.purgeInterval=1'
                echo 'preAllocSize=131072'
                echo 'snapCount=3000000'
                echo 'leaderServes=yes'
                echo 'standaloneEnabled=false'
                echo '4lw.commands.whitelist=*'
                echo 'metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider'
                echo "metricsProvider.httpPort=${PROMETHEUS_PORT}"
              } > /conf/zoo.cfg &&
              {
                echo "zookeeper.root.logger=CONSOLE"
                echo "zookeeper.console.threshold=WARN"
                echo "log4j.rootLogger=\${zookeeper.root.logger}"
                echo "log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender"
                echo "log4j.appender.CONSOLE.Threshold=\${zookeeper.console.threshold}"
                echo "log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout"
                echo "log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} [myid:%X{myid}] - %-5p [%t:%C{1}@%L] - %m%n"
              } > /conf/log4j.properties &&
              echo 'JVMFLAGS="-Xms128M -Xmx1G -XX:+UseG1GC -XX:+CMSParallelRemarkEnabled"' > /conf/java.env &&
              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 &&
              mkdir -p ${ZOO_DATA_DIR} &&
              mkdir -p ${ZOO_DATA_LOG_DIR} &&
              export MY_ID=$((ORD+1)) &&
              echo $MY_ID > $ZOO_DATA_DIR/myid &&
              for (( i=1; i<=$SERVERS; i++ )); do
                  echo "server.$i=$NAME-$((i-1)).$DOMAIN:$SERVER_PORT:$ELECTION_PORT" >> /conf/zoo.cfg;
              done &&
              chown -Rv zookeeper "$ZOO_DATA_DIR" "$ZOO_DATA_LOG_DIR" "$ZOO_LOG_DIR" "$ZOO_CONF_DIR" &&
              zkServer.sh start-foreground
          readinessProbe:
            exec:
              command:
                - bash
                - -c
                - "OK=$(echo ruok | nc 127.0.0.1 2181); if [[ \"$OK\" == \"imok\" ]]; then exit 0; else exit 1; fi"
            initialDelaySeconds: 10
            timeoutSeconds: 5
          livenessProbe:
            exec:
              command:
                - bash
                - -c
                - "OK=$(echo ruok | nc 127.0.0.1 2181); if [[ \"$OK\" == \"imok\" ]]; then exit 0; else exit 1; fi"
            initialDelaySeconds: 10
            timeoutSeconds: 5
          volumeMounts:
            - name: data-volume-ch
              mountPath: /var/lib/zookeeper
      # Run as a non-privileged user
      securityContext:
        runAsUser: 1000
        fsGroup: 1000
  volumeClaimTemplates:
    - metadata:
        name: data-volume-ch
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 20Gi

To deploy this manifest we will create another script:

#!/bin/bash

ZK_NAMESPACE="${ZK_NAMESPACE:-ch-zookeeper}"

CUR_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"

kubectl create namespace "${ZK_NAMESPACE}"
kubectl --namespace="${ZK_NAMESPACE}" apply -f "${CUR_DIR}/zookeeper-1-nodes.yaml"

Save this file as create-zookeeper-1-nodes.sh . The script will take care of creating a namespace (in our case ch-zookeeper) and apply this manifest there. Simply run the following in the directory of the file:

./create-zookeeper-1-nodes.sh

2.2) Check the Zookeeper Cluster

If the pod status is running, the Zookeeper deployment is successful. The status of the pods can be viewed through the following command:

kubectl get all -n ch-zookeeper

You will see a Service and a Pod for the Zookeeper. The importance of these two is not very evident with just one Zookeeper pod; but imagine having a 3 node cluster, you can use the Service host and port and Kubernetes will take care of redirecting you to any available Zookeeper pod in the cluster.

2.3) Deploy the clickhouse cluster

Having the ClickHouse Operator & Zookeeper Cluster up and running the next step is to define the ClickHouse Cluster Installation. This is defined in the clickhouse-cluster.yaml. Some important parts to note are in bold below (since most of the configuration is handled by the operator):

apiVersion: "clickhouse.altinity.com/v1"
kind: "ClickHouseInstallation"
metadata:
  name: "medium"
spec:
  # Templates are specified for all clusters ... if you want explicitly for each cluster then apply below
  defaults:
    templates:
      podTemplate: clickhouse
      dataVolumeClaimTemplate: data-volume-clickhouse
      logVolumeClaimTemplate: data-volume-clickhouse
  configuration:
    clusters:
      - name: "medium-cluster"
        layout:
          shardsCount: 2
          replicasCount: 2
    zookeeper:
      nodes:
        - host: zookeeper.ch-zookeeper
          port: 2181
  templates:
    podTemplates:
      - name: clickhouse
        spec:
          securityContext:
            runAsUser: 101
            runAsGroup: 101
            fsGroup: 101
          containers:
            - name: clickhouse
              image: yandex/clickhouse-server:21.10
              ports:
                - name: http
                  containerPort: 8123
                - name: client
                  containerPort: 9000
                - name: interserver
                  containerPort: 9009
              volumeMounts:
                - name: data-volume-clickhouse
                  mountPath: /var/lib/clickhouse
                - name: data-volume-clickhouse
                  mountPath: /var/log/clickhouse-server
    volumeClaimTemplates:
      - name: data-volume-clickhouse
        reclaimPolicy: Retain
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 150Gi

The name medium will be the installation name. While the name medium-cluster is the name of the cluster within the installation. We can have multiple clusters but only 1 installation. The number of shards and replicas for that cluster is defined by shardsCount and replicasCount. We also need to specify the endpoints of the zookeeper we just created zookeeper.ch-zookeeper:2181.

Running the ./create-clickhouse-cluster.sh will create a number of servers to access those ClickHouse pods as well, but the most important one would be the LoadBalancer services which will take care of redirecting you to the appropriate shard, rather than you specifying which shard. Again we define the create-clickhouse-cluster.sh file:

#!/bin/bash

CH_NAMESPACE="${CH_NAMESPACE:-clickhouse-operator}"

CUR_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"

kubectl create namespace "${CH_NAMESPACE}"

kubectl apply -n "${CH_NAMESPACE}" -f "${CUR_DIR}/clickhouse-cluster.yaml"

Run the script

./create-clickhouse-cluster.sh

2.4) Check the Clickhouse Cluster

If the pod shards (0–0–0 and 1–0–0) and the respective replicas (0–1–0 and 1–1–0) status is running, the ClickHouse Cluster deployment is successful. The status of the pods can be viewed through the following command:

Each pod will be associated with a service so you can access that shard/replica directly. The most important one from the services when you have a replicated and distributed cluster is the LoadBalancer, so you are directed to less heavily loaded and readily available ClickHouse pods.

2.5) Connect to the cluster

Since we are trying to access the LoadBalancer service outside of the Kubernetes cluster we need to perform port forwarding:

kubectl port-forward svc/clickhouse-medium-test 8123:8123namespace clickhouse-operator

This command essentially redirects any commands from localhost:8123 to http://clickhouse-medium-test.clickhouse-operator.svc.cluster.local:8123

Now you can use any IDE (DBeaver is a free solution) or even the clickhouse-client to access your cluster. In my case, I am a big fan of DataGrip:

Note that for test purposes the username and password were set as default and null password respectively. Make sure to change those from inside the clickhouse operator yaml config:

2.6) Built-in macros

The Clickhouse Operator provides a set of macros, which are:

  1. {installation} -- ClickHouse Installation name
  2. {cluster} -- primary cluster name
  3. {replica} -- replica name in the cluster, maps to pod service name
  4. {shard} -- shard id

ClickHouse also supports internal macros {database} and {table} that maps to the current database and table respectively.

The following code shows the macro automatically created by the current cluster, which we can use when creating a table.

<yandex>
    <macros>
        <installation>medium-test</installation>
        <all-sharded-shard>0</all-sharded-shard>
        <cluster>medium-cluster</cluster>
        <shard>0</shard>
        <replica>chi-medium-test-medium-cluster-0-0</replica>
    </macros>
</yandex>

3) Test the cluster

Since you are connected to the cluster, let's run some commands and view the output.

3.1) Create a set of tables

CREATE TABLE events_local on cluster '{cluster}' (
    event_date  Date,
    event_type  Int32,
    article_id  Int32,
    title       String
) engine=ReplicatedMergeTree('/clickhouse/{installation}/{cluster}/tables/{shard}/{database}/{table}', '{replica}', event_date, (event_type, article_id), 8192);
CREATE TABLE events on cluster '{cluster}' AS events_local
ENGINE = Distributed('{cluster}', default, events_local, rand());

3.2) Insert data

INSERT INTO events SELECT today(), rand()%3, number, 'my title' FROM numbers(100);

3.3) View data

SELECT count() FROM events;
SELECT count() FROM events_local;
SELECT * FROM events;

You will notice that the count in events_local is lower than events and that’s because the cluster is taking care of distributing data to the other shards. So always make sure to query the events table since that’s the distributed Engine that will take care to join the data from all shards

4) Check Local Persistent Volume

The yaml configuration above for the Zookeeper & ClickHouse Installation also creates persistent volume. This is needed to make sure data is independent of the life-cycle of the pod that uses it, meaning that even if the pod shuts down, the data in the volume is not erased. You can check those by running:

kubectl get pv — sort-by=.spec.capacity.storage

5) cluster monitoring

There are many ways to monitor a Kubernetes deployment but the two best opensource options I found useful for me are Prometheus & Grafana

5.1) Prometheus

I will be creating a separate article for Prometheus but in summary, the ClickHouse Operator creates a metrics service. Prometheus can use this address to grab metrics:

http://clickhouse-operator-metrics.clickhouse-operator.svc.cluster.local:8888/metrics

5.2) Grafana Dashboard

With the Prometheus and ClickHouse Cluster set up, we can have a combination of dashboards to monitor the environment. Another separate article will be created for Grafana.

6) Conclusion

The ClickHouse operator is simple to install and can handle life-cycle operations for many ClickHouse installations running in a single Kubernetes cluster. It is designed for use cases ranging from quick tests to production data warehouses. So if it’s not clear from the above in summary the operator handles the following tasks:

  • Setting up ClickHouse installations
  • Changing the configuration of nodes, for example, to increase memory
  • Adding and removing shards
  • Adding and removing replicas
  • Upgrading cluster nodes
  • Monitoring cluster status
  • Removing ClickHouse installations
  • Provides Cluster Metrics for monitoring purposes

Altinity are also working on a Dashboard for the ClickHouse Operator that aims to make ClickHouse deployment in Kubernetes easy, painless, and successful. I recommend looking into this as well:

Data Science
Programming
Technology
Machine Learning
Artificial Intelligence
Recommended from ReadMedium