<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:hashnode="https://hashnode.com/rss"><channel><title><![CDATA[Sven Eliasson]]></title><description><![CDATA[Backend, Kubernetes, Datascience]]></description><link>https://sveneliasson.de</link><generator>RSS for Node</generator><lastBuildDate>Tue, 03 Dec 2024 07:31:55 GMT</lastBuildDate><atom:link href="https://sveneliasson.de/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><atom:link rel="next" href="https://sveneliasson.de/rss.xml?page=2"/><atom:link rel="previous" href="https://sveneliasson.de/rss.xml"/><item><title><![CDATA[Kubernetes  Trouble-Cheat-Sheet with 8 most important best practices  (PDF, direct download)]]></title><description><![CDATA[Download the Kubernetes Trouble-Cheat-Sheet PDF with 8 essential best practices to improve your workflow and prevent future issues]]></description><link>https://sveneliasson.de/kubernetes-trouble-cheat-sheet-with-8-most-important-best-practices-pdf-direct-download</link><guid isPermaLink="true">https://sveneliasson.de/kubernetes-trouble-cheat-sheet-with-8-most-important-best-practices-pdf-direct-download</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[cheatsheet]]></category><category><![CDATA[best practices]]></category><dc:creator><![CDATA[Sven Eliasson]]></dc:creator><pubDate>Sat, 18 May 2024 17:24:58 GMT</pubDate><content:encoded>&lt;![CDATA[&lt;p&gt;Whenever trouble arises, you better be prepared. To avoid wasting time trying to remember commands and potential fixes, I keep a text note with the most important information. These notes have served me well over the years, and I would like to share them in a printable format, also for myself.&lt;/p&gt;&lt;p&gt;Right after an incident is the second-best time to fix bad practices and prevent future issues. Learning from these moments can significantly improve your workflow and minimize future disruptions. To help with this, Ive compiled a list of eight best practices that I see violated pretty often and whose syntax I need to look up regularly.  &lt;/p&gt;&lt;p&gt;I will update this article with more explanations in the future. For now, you can download the printable PDF :&lt;br /&gt;&lt;a target=&quot;_blank&quot; href=&quot;https://github.com/comino/Kubernetes-Trouble-Cheat-Sheet/blob/main/Kubernetes-Trouble-Cheat-Sheet-1.0.pdf?raw=true&quot;&gt;Link: Download PDF (Github)&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1716053059253/a2bf1af2-92ee-4dc5-bf73-039e36f36574.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;]]&gt;</content:encoded><hashnode:content>&lt;![CDATA[&lt;p&gt;Whenever trouble arises, you better be prepared. To avoid wasting time trying to remember commands and potential fixes, I keep a text note with the most important information. These notes have served me well over the years, and I would like to share them in a printable format, also for myself.&lt;/p&gt;&lt;p&gt;Right after an incident is the second-best time to fix bad practices and prevent future issues. Learning from these moments can significantly improve your workflow and minimize future disruptions. To help with this, Ive compiled a list of eight best practices that I see violated pretty often and whose syntax I need to look up regularly.  &lt;/p&gt;&lt;p&gt;I will update this article with more explanations in the future. For now, you can download the printable PDF :&lt;br /&gt;&lt;a target=&quot;_blank&quot; href=&quot;https://github.com/comino/Kubernetes-Trouble-Cheat-Sheet/blob/main/Kubernetes-Trouble-Cheat-Sheet-1.0.pdf?raw=true&quot;&gt;Link: Download PDF (Github)&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1716053059253/a2bf1af2-92ee-4dc5-bf73-039e36f36574.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;]]&gt;</hashnode:content><hashnode:coverImage>https://cdn.hashnode.com/res/hashnode/image/upload/v1716052982499/e3b21c1c-205d-43d4-8384-5326130189cd.png</hashnode:coverImage></item><item><title><![CDATA[Benchmarking Hetzner's Storage Classes for Database Workloads on Kubernetes]]></title><description><![CDATA[Hetzner Storage Classes Comparison on Kubernetes]]></description><link>https://sveneliasson.de/benchmarking-hetzners-storage-classes-for-database-workloads-on-kubernetes</link><guid isPermaLink="true">https://sveneliasson.de/benchmarking-hetzners-storage-classes-for-database-workloads-on-kubernetes</guid><category><![CDATA[Hetzner]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[Sven Eliasson]]></dc:creator><pubDate>Sat, 27 Apr 2024 16:37:49 GMT</pubDate><content:encoded>&lt;![CDATA[&lt;blockquote&gt;&lt;p&gt;TLDR: Running Kubernetes on Hetzner offers cost-effective options, but handling production workloads, especially stateful ones like databases, raises concerns. Hetzner provides instance and cloud volume storage options with significant differences in IOPS performance. Longhorn, a distributed block storage system, can be used to leverage local volumes, but benchmarks show a slowdown compared to raw local files. Probably host a datatbase either on a dedicated host or use a hosted option instead.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Running Kubernetes on Hetzner is likely one of the most cost-effective options available. The &lt;a target=&quot;_blank&quot; href=&quot;https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner&quot;&gt;Hetzeners Terrraform Kubernetes&lt;/a&gt; project simplifies the process of setting up a basic cluster within minutes. However, is this sufficient for handling production workloads?&lt;/p&gt;&lt;p&gt;One controversial topic about running a Kubernetes cluster is whether one should run stateful workloads like databases on Kubernetes. There are valid arguments for both sides. I used to avoid running databases on Kubernetes, but changed my mind. I think the widespread adoption of Kubernetes operators, including those for most databases, has changed the game. There are battle-proven operators that handle most of the pain points. However, there is one critical issue those operators cannot solve: &lt;strong&gt;IOPS&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;You can increase the computing power of a database as much as you like, but if the IOPS are low, the database performance will suffer. Ultimately, you have to retrieve data from a disk, which significantly hampers query performance. In this short article, I intend to outline my benchmarks for different storage options.&lt;/p&gt;&lt;h2 id=&quot;heading-kubernetes-on-hetzner-storage-options&quot;&gt;Kubernetes on Hetzner storage options&lt;/h2&gt;&lt;p&gt;There are two main types of storage options available on the Hetzner Cloud:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Instance volume storage: This is the NVMe storage linked to the node you rent. This volume scales only with the number of CPU and Memory you choose, making it quite costly.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Cloud volume storage: This is a block storage volume that is relatively inexpensive but offers lower performance.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The difference in IOPS is significant. For a medium-sized instance, the read IOPS on a local volume are 17 times higher than on cloud storage. You can find a detailed benchmark of IOPS performance for all instance types here: &lt;a target=&quot;_blank&quot; href=&quot;https://pcr.cloud-mercato.com/providers/hetzner/flavors/ccx21/performance/iops&quot;&gt;https://pcr.cloud-mercato.com/providers/hetzner/flavors/ccx21/performance/iops&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1714233526187/5716b5f5-9137-47bf-a811-dd466cab4870.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;In Hetzner&apos;s Kubernetes solution, there are three built-in storage classes that can be used out of the box:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;hcloud-volumes&lt;/strong&gt;: This is a block storage drive, the slower option.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;longhorn-volumes&lt;/strong&gt;: Longhorn is an open-source, distributed block storage system. It can optionally utilize the local drive of each node.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;local-file&lt;/strong&gt;: This is Rancher&apos;s implementation that simplifies the use of local host files as a volume.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Using local files in Kubernetes has several drawbacks and should generally be avoided. These files are inherently tied to specific hosts, complicating adaptation to changes in node architecture. This approach also introduces security vulnerabilities and significantly increases maintenance complexity.&lt;/p&gt;&lt;p&gt;Longhorn can be enabled during cluster installation using Terraform, and it aims to make use of local volumes. To achieve this, you must configure Longhorn as described here: &lt;a target=&quot;_blank&quot; href=&quot;https://gist.github.com/ifeulner/d311b2868f6c00e649f33a72166c2e5b&quot;&gt;https://gist.github.com/ifeulner/d311b2868f6c00e649f33a72166c2e5b&lt;/a&gt;&lt;/p&gt;&lt;p&gt;In summary: You need to enable the default disk on nodes first via settings.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;defaultSettings:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;createDefaultDiskLabeledNodes:&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;kubernetesClusterAutoscalerEnabled:&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-comment&quot;&gt;# if autoscaler is active in the cluster&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;defaultDataPath:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;/var/lib/longhorn&lt;/span&gt;  &lt;span class=&quot;hljs-comment&quot;&gt;# ensure pod is moved to an healthy node if current node is down:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;node-down-pod-deletion-policy:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;delete-both-statefulset-and-deployment-pod&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Label the nodes and assign a portion of the drive to a disk type &quot;nvme&quot;:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;&lt;span class=&quot;hljs-string&quot;&gt;kubectl&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;lt;node&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;node.longhorn.io/create-default-disk=&apos;config&apos;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;kubectl&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;annotate&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;lt;storagenode&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;node.longhorn.io/default-disks-config=&apos;[&lt;/span&gt; { &lt;span class=&quot;hljs-string&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:&quot;/var/lib/longhorn&quot;&lt;/span&gt;,&lt;span class=&quot;hljs-string&quot;&gt;&quot;allowScheduling&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:true&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;storageReserved&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:21474836240&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;tags&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:&lt;/span&gt;[ &lt;span class=&quot;hljs-string&quot;&gt;&quot;nvme&quot;&lt;/span&gt; ]}&lt;span class=&quot;hljs-string&quot;&gt;,&lt;/span&gt; { &lt;span class=&quot;hljs-string&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:&quot;hcloud-volume&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:&quot;/var/longhorn&quot;&lt;/span&gt;,&lt;span class=&quot;hljs-string&quot;&gt;&quot;allowScheduling&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:true&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;storageReserved&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:10737418120&lt;/span&gt;,&lt;span class=&quot;hljs-string&quot;&gt;&quot;tags&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:&lt;/span&gt;[ &lt;span class=&quot;hljs-string&quot;&gt;&quot;ssd&quot;&lt;/span&gt; ] }&lt;span class=&quot;hljs-string&quot;&gt;]&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now you can define a longhorn storage clas0 that will utilize the nvme disk like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;kind:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;StorageClass&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;apiVersion:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;storage.k8s.io/v1&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;metadata:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;longhorn-fast&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;provisioner:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;driver.longhorn.io&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;allowVolumeExpansion:&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;reclaimPolicy:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;Delete&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;volumeBindingMode:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;Immediate&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;parameters:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;numberOfReplicas:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;3&quot;&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;staleReplicaTimeout:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;2880&quot;&lt;/span&gt; &lt;span class=&quot;hljs-comment&quot;&gt;# 48 hours in minutes&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;fromBackup:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;fsType:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;ext4&quot;&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;diskSelector:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;nvme&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ok, we are ready to go. Lets benchmark!&lt;/p&gt;&lt;h2 id=&quot;heading-benchmark-storage-iops-for-a-given-storage-class-in-kubernetes&quot;&gt;Benchmark storage IOPS for a given storage class in Kubernetes.&lt;/h2&gt;&lt;p&gt;We need a simple solution to benchmark a given storage class. I used &lt;a target=&quot;_blank&quot; href=&quot;https://github.com/storageos/dbench&quot;&gt;dbbench&lt;/a&gt;&apos;s docker container to spin up a simple benchmark job in kubernetes. First we need to define some sample PVCs to work with.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;kind:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;PersistentVolumeClaim&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;apiVersion:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;v1&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;metadata:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench-pv-claim-localpath&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;spec:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;storageClassName:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;local-path&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;accessModes:&lt;/span&gt;    &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;ReadWriteOnce&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;resources:&lt;/span&gt;    &lt;span class=&quot;hljs-attr&quot;&gt;requests:&lt;/span&gt;      &lt;span class=&quot;hljs-attr&quot;&gt;storage:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;10Gi&lt;/span&gt;&lt;span class=&quot;hljs-meta&quot;&gt;---&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;kind:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;PersistentVolumeClaim&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;apiVersion:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;v1&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;metadata:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench-pv-claim-longhorn&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;spec:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;storageClassName:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;longhorn-fast&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;accessModes:&lt;/span&gt;    &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;ReadWriteOnce&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;resources:&lt;/span&gt;    &lt;span class=&quot;hljs-attr&quot;&gt;requests:&lt;/span&gt;      &lt;span class=&quot;hljs-attr&quot;&gt;storage:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;25Gi&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;---&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;kind:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;PersistentVolumeClaim&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;apiVersion:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;v1&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;metadata:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench-pv-claim-hcloud&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;spec:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;storageClassName:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;hcloud-volumes&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;accessModes:&lt;/span&gt;    &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;ReadWriteOnce&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;resources:&lt;/span&gt;    &lt;span class=&quot;hljs-attr&quot;&gt;requests:&lt;/span&gt;      &lt;span class=&quot;hljs-attr&quot;&gt;storage:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;25Gi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We utilize kubernetes jobs to run the individual benchmark.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;&lt;span class=&quot;hljs-meta&quot;&gt;---&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;apiVersion:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;batch/v1&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;kind:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;metadata:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;db-bench-01&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;spec:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;template:&lt;/span&gt;    &lt;span class=&quot;hljs-attr&quot;&gt;spec:&lt;/span&gt;      &lt;span class=&quot;hljs-attr&quot;&gt;containers:&lt;/span&gt;      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench&lt;/span&gt;        &lt;span class=&quot;hljs-attr&quot;&gt;image:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;storageos/dbench:latest&lt;/span&gt;        &lt;span class=&quot;hljs-attr&quot;&gt;env:&lt;/span&gt;          &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;DBENCH_MOUNTPOINT&lt;/span&gt;            &lt;span class=&quot;hljs-attr&quot;&gt;value:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;/data&lt;/span&gt;        &lt;span class=&quot;hljs-attr&quot;&gt;volumeMounts:&lt;/span&gt;        &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench-pv&lt;/span&gt;          &lt;span class=&quot;hljs-attr&quot;&gt;mountPath:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;/data&lt;/span&gt;      &lt;span class=&quot;hljs-attr&quot;&gt;restartPolicy:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;Never&lt;/span&gt;      &lt;span class=&quot;hljs-attr&quot;&gt;volumes:&lt;/span&gt;      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench-pv&lt;/span&gt;        &lt;span class=&quot;hljs-attr&quot;&gt;persistentVolumeClaim:&lt;/span&gt;          &lt;span class=&quot;hljs-attr&quot;&gt;claimName:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench-pv-claim-longhorn&lt;/span&gt;           &lt;span class=&quot;hljs-comment&quot;&gt;#claimName: dbench-pv-claim-hcloud &lt;/span&gt;          &lt;span class=&quot;hljs-comment&quot;&gt;#claimName: dbench-pv-claim-localpath&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A typical benchmark result will look like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;Random Read/Write IOPS: 32.7k/27.1k. BW:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;2854MiB/s&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;2140MiB/s&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;Average&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;Latency&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;(usec)&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Read/Write:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;325.47&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;/216.90&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;Sequential Read/Write:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;5741MiB/s&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;4689MiB/s&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;Mixed Random Read/Write IOPS:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;23.&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;8k/7946&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I ran benchmarks for all storage options and got a rather disappointing result. Longhorn adds some overhead, causing a 4.5x slowdown compared to a raw local file, and only slightly outperforms a cloud volume.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1714235081058/086ffb2d-5a43-4614-b12e-3869ae4a5208.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;I&apos;m not sure if there are optimizations available for Longhorn. I halted my evaluation here; it appears that using a local file on the host node might be the best choice for a database workload.&lt;/p&gt;&lt;p&gt;In my upcoming post, I will share my experience of running a database workload on Hetzner in production using local files.&lt;/p&gt;]]&gt;</content:encoded><hashnode:content>&lt;![CDATA[&lt;blockquote&gt;&lt;p&gt;TLDR: Running Kubernetes on Hetzner offers cost-effective options, but handling production workloads, especially stateful ones like databases, raises concerns. Hetzner provides instance and cloud volume storage options with significant differences in IOPS performance. Longhorn, a distributed block storage system, can be used to leverage local volumes, but benchmarks show a slowdown compared to raw local files. Probably host a datatbase either on a dedicated host or use a hosted option instead.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;Running Kubernetes on Hetzner is likely one of the most cost-effective options available. The &lt;a target=&quot;_blank&quot; href=&quot;https://github.com/kube-hetzner/terraform-hcloud-kube-hetzner&quot;&gt;Hetzeners Terrraform Kubernetes&lt;/a&gt; project simplifies the process of setting up a basic cluster within minutes. However, is this sufficient for handling production workloads?&lt;/p&gt;&lt;p&gt;One controversial topic about running a Kubernetes cluster is whether one should run stateful workloads like databases on Kubernetes. There are valid arguments for both sides. I used to avoid running databases on Kubernetes, but changed my mind. I think the widespread adoption of Kubernetes operators, including those for most databases, has changed the game. There are battle-proven operators that handle most of the pain points. However, there is one critical issue those operators cannot solve: &lt;strong&gt;IOPS&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;You can increase the computing power of a database as much as you like, but if the IOPS are low, the database performance will suffer. Ultimately, you have to retrieve data from a disk, which significantly hampers query performance. In this short article, I intend to outline my benchmarks for different storage options.&lt;/p&gt;&lt;h2 id=&quot;heading-kubernetes-on-hetzner-storage-options&quot;&gt;Kubernetes on Hetzner storage options&lt;/h2&gt;&lt;p&gt;There are two main types of storage options available on the Hetzner Cloud:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Instance volume storage: This is the NVMe storage linked to the node you rent. This volume scales only with the number of CPU and Memory you choose, making it quite costly.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Cloud volume storage: This is a block storage volume that is relatively inexpensive but offers lower performance.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;The difference in IOPS is significant. For a medium-sized instance, the read IOPS on a local volume are 17 times higher than on cloud storage. You can find a detailed benchmark of IOPS performance for all instance types here: &lt;a target=&quot;_blank&quot; href=&quot;https://pcr.cloud-mercato.com/providers/hetzner/flavors/ccx21/performance/iops&quot;&gt;https://pcr.cloud-mercato.com/providers/hetzner/flavors/ccx21/performance/iops&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1714233526187/5716b5f5-9137-47bf-a811-dd466cab4870.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;In Hetzner&apos;s Kubernetes solution, there are three built-in storage classes that can be used out of the box:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;hcloud-volumes&lt;/strong&gt;: This is a block storage drive, the slower option.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;longhorn-volumes&lt;/strong&gt;: Longhorn is an open-source, distributed block storage system. It can optionally utilize the local drive of each node.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;local-file&lt;/strong&gt;: This is Rancher&apos;s implementation that simplifies the use of local host files as a volume.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Using local files in Kubernetes has several drawbacks and should generally be avoided. These files are inherently tied to specific hosts, complicating adaptation to changes in node architecture. This approach also introduces security vulnerabilities and significantly increases maintenance complexity.&lt;/p&gt;&lt;p&gt;Longhorn can be enabled during cluster installation using Terraform, and it aims to make use of local volumes. To achieve this, you must configure Longhorn as described here: &lt;a target=&quot;_blank&quot; href=&quot;https://gist.github.com/ifeulner/d311b2868f6c00e649f33a72166c2e5b&quot;&gt;https://gist.github.com/ifeulner/d311b2868f6c00e649f33a72166c2e5b&lt;/a&gt;&lt;/p&gt;&lt;p&gt;In summary: You need to enable the default disk on nodes first via settings.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;defaultSettings:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;createDefaultDiskLabeledNodes:&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;kubernetesClusterAutoscalerEnabled:&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;hljs-comment&quot;&gt;# if autoscaler is active in the cluster&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;defaultDataPath:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;/var/lib/longhorn&lt;/span&gt;  &lt;span class=&quot;hljs-comment&quot;&gt;# ensure pod is moved to an healthy node if current node is down:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;node-down-pod-deletion-policy:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;delete-both-statefulset-and-deployment-pod&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Label the nodes and assign a portion of the drive to a disk type &quot;nvme&quot;:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;&lt;span class=&quot;hljs-string&quot;&gt;kubectl&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;label&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;lt;node&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;node.longhorn.io/create-default-disk=&apos;config&apos;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;kubectl&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;annotate&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&amp;lt;storagenode&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;node.longhorn.io/default-disks-config=&apos;[&lt;/span&gt; { &lt;span class=&quot;hljs-string&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:&quot;/var/lib/longhorn&quot;&lt;/span&gt;,&lt;span class=&quot;hljs-string&quot;&gt;&quot;allowScheduling&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:true&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;storageReserved&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:21474836240&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;tags&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:&lt;/span&gt;[ &lt;span class=&quot;hljs-string&quot;&gt;&quot;nvme&quot;&lt;/span&gt; ]}&lt;span class=&quot;hljs-string&quot;&gt;,&lt;/span&gt; { &lt;span class=&quot;hljs-string&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:&quot;hcloud-volume&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:&quot;/var/longhorn&quot;&lt;/span&gt;,&lt;span class=&quot;hljs-string&quot;&gt;&quot;allowScheduling&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:true&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;storageReserved&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:10737418120&lt;/span&gt;,&lt;span class=&quot;hljs-string&quot;&gt;&quot;tags&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;:&lt;/span&gt;[ &lt;span class=&quot;hljs-string&quot;&gt;&quot;ssd&quot;&lt;/span&gt; ] }&lt;span class=&quot;hljs-string&quot;&gt;]&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now you can define a longhorn storage clas0 that will utilize the nvme disk like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;kind:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;StorageClass&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;apiVersion:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;storage.k8s.io/v1&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;metadata:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;longhorn-fast&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;provisioner:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;driver.longhorn.io&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;allowVolumeExpansion:&lt;/span&gt; &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;reclaimPolicy:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;Delete&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;volumeBindingMode:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;Immediate&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;parameters:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;numberOfReplicas:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;3&quot;&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;staleReplicaTimeout:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;2880&quot;&lt;/span&gt; &lt;span class=&quot;hljs-comment&quot;&gt;# 48 hours in minutes&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;fromBackup:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;fsType:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;ext4&quot;&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;diskSelector:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;nvme&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Ok, we are ready to go. Lets benchmark!&lt;/p&gt;&lt;h2 id=&quot;heading-benchmark-storage-iops-for-a-given-storage-class-in-kubernetes&quot;&gt;Benchmark storage IOPS for a given storage class in Kubernetes.&lt;/h2&gt;&lt;p&gt;We need a simple solution to benchmark a given storage class. I used &lt;a target=&quot;_blank&quot; href=&quot;https://github.com/storageos/dbench&quot;&gt;dbbench&lt;/a&gt;&apos;s docker container to spin up a simple benchmark job in kubernetes. First we need to define some sample PVCs to work with.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;kind:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;PersistentVolumeClaim&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;apiVersion:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;v1&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;metadata:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench-pv-claim-localpath&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;spec:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;storageClassName:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;local-path&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;accessModes:&lt;/span&gt;    &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;ReadWriteOnce&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;resources:&lt;/span&gt;    &lt;span class=&quot;hljs-attr&quot;&gt;requests:&lt;/span&gt;      &lt;span class=&quot;hljs-attr&quot;&gt;storage:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;10Gi&lt;/span&gt;&lt;span class=&quot;hljs-meta&quot;&gt;---&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;kind:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;PersistentVolumeClaim&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;apiVersion:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;v1&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;metadata:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench-pv-claim-longhorn&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;spec:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;storageClassName:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;longhorn-fast&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;accessModes:&lt;/span&gt;    &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;ReadWriteOnce&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;resources:&lt;/span&gt;    &lt;span class=&quot;hljs-attr&quot;&gt;requests:&lt;/span&gt;      &lt;span class=&quot;hljs-attr&quot;&gt;storage:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;25Gi&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;---&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;kind:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;PersistentVolumeClaim&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;apiVersion:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;v1&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;metadata:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench-pv-claim-hcloud&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;spec:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;storageClassName:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;hcloud-volumes&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;accessModes:&lt;/span&gt;    &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;ReadWriteOnce&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;resources:&lt;/span&gt;    &lt;span class=&quot;hljs-attr&quot;&gt;requests:&lt;/span&gt;      &lt;span class=&quot;hljs-attr&quot;&gt;storage:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;25Gi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We utilize kubernetes jobs to run the individual benchmark.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;&lt;span class=&quot;hljs-meta&quot;&gt;---&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;apiVersion:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;batch/v1&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;kind:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;metadata:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;db-bench-01&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;spec:&lt;/span&gt;  &lt;span class=&quot;hljs-attr&quot;&gt;template:&lt;/span&gt;    &lt;span class=&quot;hljs-attr&quot;&gt;spec:&lt;/span&gt;      &lt;span class=&quot;hljs-attr&quot;&gt;containers:&lt;/span&gt;      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench&lt;/span&gt;        &lt;span class=&quot;hljs-attr&quot;&gt;image:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;storageos/dbench:latest&lt;/span&gt;        &lt;span class=&quot;hljs-attr&quot;&gt;env:&lt;/span&gt;          &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;DBENCH_MOUNTPOINT&lt;/span&gt;            &lt;span class=&quot;hljs-attr&quot;&gt;value:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;/data&lt;/span&gt;        &lt;span class=&quot;hljs-attr&quot;&gt;volumeMounts:&lt;/span&gt;        &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench-pv&lt;/span&gt;          &lt;span class=&quot;hljs-attr&quot;&gt;mountPath:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;/data&lt;/span&gt;      &lt;span class=&quot;hljs-attr&quot;&gt;restartPolicy:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;Never&lt;/span&gt;      &lt;span class=&quot;hljs-attr&quot;&gt;volumes:&lt;/span&gt;      &lt;span class=&quot;hljs-bullet&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench-pv&lt;/span&gt;        &lt;span class=&quot;hljs-attr&quot;&gt;persistentVolumeClaim:&lt;/span&gt;          &lt;span class=&quot;hljs-attr&quot;&gt;claimName:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;dbench-pv-claim-longhorn&lt;/span&gt;           &lt;span class=&quot;hljs-comment&quot;&gt;#claimName: dbench-pv-claim-hcloud &lt;/span&gt;          &lt;span class=&quot;hljs-comment&quot;&gt;#claimName: dbench-pv-claim-localpath&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A typical benchmark result will look like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-yaml&quot;&gt;&lt;span class=&quot;hljs-attr&quot;&gt;Random Read/Write IOPS: 32.7k/27.1k. BW:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;2854MiB/s&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;2140MiB/s&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;Average&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;Latency&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;(usec)&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;Read/Write:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;325.47&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;/216.90&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;Sequential Read/Write:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;5741MiB/s&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;4689MiB/s&lt;/span&gt;&lt;span class=&quot;hljs-attr&quot;&gt;Mixed Random Read/Write IOPS:&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;23.&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;8k/7946&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I ran benchmarks for all storage options and got a rather disappointing result. Longhorn adds some overhead, causing a 4.5x slowdown compared to a raw local file, and only slightly outperforms a cloud volume.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1714235081058/086ffb2d-5a43-4614-b12e-3869ae4a5208.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;I&apos;m not sure if there are optimizations available for Longhorn. I halted my evaluation here; it appears that using a local file on the host node might be the best choice for a database workload.&lt;/p&gt;&lt;p&gt;In my upcoming post, I will share my experience of running a database workload on Hetzner in production using local files.&lt;/p&gt;]]&gt;</hashnode:content><hashnode:coverImage>https://cdn.hashnode.com/res/hashnode/image/upload/v1714235920738/ec1713ff-0bb8-4c96-9eeb-075066a65e16.png</hashnode:coverImage></item><item><title><![CDATA[2.5x Performance: PostgreSQL to Clickhouse without Kafka with MaterializedPostgreSQL]]></title><description><![CDATA[TLDR: Stream PostgreSQL data to ClickHouse without Kafka using MaterializedPostgreSQL for real-time data replication and optimization, achieving significant data compression and query 2.5x performance improvements.
Dealing with billions of sensor rec...]]></description><link>https://sveneliasson.de/stream-postgresql-to-clickhouse-without-kafka-with-materializedpostgresql</link><guid isPermaLink="true">https://sveneliasson.de/stream-postgresql-to-clickhouse-without-kafka-with-materializedpostgresql</guid><category><![CDATA[ClickHouse]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[dataengineering]]></category><dc:creator><![CDATA[Sven Eliasson]]></dc:creator><pubDate>Thu, 25 Apr 2024 14:43:49 GMT</pubDate><content:encoded>&lt;![CDATA[&lt;p&gt;&lt;strong&gt;&lt;em&gt;TLDR: Stream PostgreSQL data to ClickHouse without Kafka using MaterializedPostgreSQL for real-time data replication and optimization, achieving significant data compression and query 2.5x performance improvements.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Dealing with billions of sensor records, as we do at Rysta, is no easy task. We encountered challenges in maintaining the performance of some of our analytical workloads within PostgreSQL. PostgreSQL has been exceptional for us, and I suggest that anyone handling analytical tasks (OLAP) should start with PostgreSQL, specifically TimescaleDB.&lt;/p&gt;&lt;p&gt;However, for numerous analytical queries, PostgreSQL&apos;s row-based storage is slower compared to columnar storage used in many OLAP databases. After exploring various analytical database choices, I fell in love with &lt;strong&gt;ClickHouse&lt;/strong&gt;. It excites me daily, and I started creating prototypes using ClickHouse, which delivered exceptional performance.&lt;/p&gt;&lt;p&gt;Currently, we store all our sensor records in a PostgreSQL database, and we have no plans to change this setup soon. Hence, we aim to replicate data from PostgreSQL to ClickHouse in real-time to power certain analytical workloads. A common method involves using CDC with Debezium/Kafka, as explained in &lt;a target=&quot;_blank&quot; href=&quot;https://clickhouse.com/blog/clickhouse-postgresql-change-data-capture-cdc-part-1&quot;&gt;this blog post&lt;/a&gt;. While this is a reliable and proven method, we do not utilize a Kafka cluster right now, and I prefer to avoid the extra work that comes with it. So, why not stream data directly into ClickHouse without an intermediary data broker? This is precisely achievable with ClickHouse&apos;s experimental feature called &lt;a target=&quot;_blank&quot; href=&quot;https://clickhouse.com/docs/en/engines/database-engines/materialized-postgresql&quot;&gt;&lt;strong&gt;MaterializedPostgreSQL&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1714055996205/ca67f50f-0ba9-4f31-b1c1-8b3c0478475c.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a target=&quot;_blank&quot; href=&quot;https://clickhouse.com/docs/en/engines/database-engines/materialized-postgresql&quot;&gt;MaterializedPostgreSQL&lt;/a&gt; allows you to set up a ClickHouse database from a distant PostgreSQL database. Once an initial data snapshot is taken, modifications are copied using PostgreSQL&apos;s WAL. As of now, this feature is still experimental, but it has worked perfectly for us until this point.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; allow_experimental_database_materialized_postgresql=&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;DATABASE&lt;/span&gt; postgres&lt;span class=&quot;hljs-keyword&quot;&gt;ENGINE&lt;/span&gt; = MaterializedPostgreSQL(&lt;span class=&quot;hljs-string&quot;&gt;&apos;host:port&apos;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&apos;database&apos;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&apos;user&apos;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&apos;password&apos;&lt;/span&gt;)&lt;span class=&quot;hljs-keyword&quot;&gt;SETTINGS&lt;/span&gt; materialized_postgresql_schema = &lt;span class=&quot;hljs-string&quot;&gt;&apos;myschema&apos;&lt;/span&gt;;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We used this approach to sync a sample dataset of about a billion records in about 5 minutes, which amazed me! The initial prototypes used a batch-wise ETL process that took over an hour for the first data sync. So already a good start! The table is automatically updated as new data is inserted into PostgreSQL.&lt;/p&gt;&lt;p&gt;Now that we have the date in our clickhouse cluster we could start using it... but it would probably not be faster than using a postgreSQL replica.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;sum&lt;/span&gt;(bytes_on_disk) &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; system.parts &lt;span class=&quot;hljs-keyword&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;database&lt;/span&gt; = &lt;span class=&quot;hljs-string&quot;&gt;&apos;postgres&apos;&lt;/span&gt;/ &lt;span class=&quot;hljs-number&quot;&gt;21841081469&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The sample dataset uses around 20GB of data for a billion records. Let&apos;s utilize ClickHouse&apos;s specific features to optimize this dataset for quicker query performance:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Use codecs to compress the physical data size; smaller datasets are often faster to query.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Utilize a sparse index to reduce the number of chunks that need to be scanned line by line.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Implement a partition that will divide the table based on a pattern optimized for the queries we intend to execute.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There are trade-offs to consider, and the table structure should be fine-tuned based on your most crucial queries. The table DDL that proved to be most efficient was:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;TABLE&lt;/span&gt; mydb.records (&lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt; Float32 CODEC(Gorilla),t DateTime CODEC(DoubleDelta),sensor_id UInt32 CODEC(T64)) &lt;span class=&quot;hljs-keyword&quot;&gt;ENGINE&lt;/span&gt; = ReplicatedMergeTree&lt;span class=&quot;hljs-keyword&quot;&gt;PARTITION&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; toYYYYMM(t)  &lt;span class=&quot;hljs-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; (toStartOfHour(t), sensor_value_id, t)PRIMARY &lt;span class=&quot;hljs-keyword&quot;&gt;KEY&lt;/span&gt; ( toStartOfHour(t), sensor_value_id);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now that we have the perfect table, how do we populate it with data? I could insert data from the Postgres database into my ClickHouse-native table using an insert query, but this would have to be done for every insertion into the Postgres table. This is where we can make use of ClickHouse&apos;s &quot;Materialized view,&quot; which functions differently compared to other databases like PostgreSQL. It&apos;s more like a &lt;em&gt;trigger&lt;/em&gt; that runs a query on a specific table every time data is inserted into the source table.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;MATERIALIZED&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VIEW&lt;/span&gt; mydb.records_mv &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; mydb.records &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;     &lt;span class=&quot;hljs-keyword&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt;,     &lt;span class=&quot;hljs-built_in&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; t,     sensor_id &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; sensor_id&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; postgres.records;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After running this query, whenever new data is added to the PostgreSQL table, it will also be inserted into our ClickHouse-native, optimized table. This process only adds new records. However, we can initially copy over a snapshot like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;INTO&lt;/span&gt; mydb.sensor_value_record&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;    &lt;span class=&quot;hljs-keyword&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt;,     &lt;span class=&quot;hljs-built_in&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; t,     sensor_id &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; sensor_id&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; postgres.records&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Voila! The resulting table compressed the initial 20GB into 3.9GB, achieving an &lt;strong&gt;87%&lt;/strong&gt; compression.&lt;/p&gt;&lt;p&gt;So that&apos;s it? Basically yes.&lt;br /&gt;So everything is great? Not exactly...&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;MATERIALIZED&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VIEW&lt;/span&gt; mydb.records_mv_2024_04 &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; mydb.records &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt;, &lt;span class=&quot;hljs-built_in&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; t, sensor_id &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; sensor_id&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; postgres.records_2024_04;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;MATERIALIZED&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VIEW&lt;/span&gt; mydb.records_mv_2024_05 &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; mydb.records &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt;, &lt;span class=&quot;hljs-built_in&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; t, sensor_id &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; sensor_id&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; postgres.records_2024_05;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;MATERIALIZED&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VIEW&lt;/span&gt; mydb.records_mv_2024_06 &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; mydb.records &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt;, &lt;span class=&quot;hljs-built_in&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; t, sensor_id &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; sensor_id&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; postgres.records_2024_06;... and so on&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I hope there will be native support for partitions in &lt;a target=&quot;_blank&quot; href=&quot;https://clickhouse.com/docs/en/engines/database-engines/materialized-postgresql&quot;&gt;MaterializedPostgreSQL&lt;/a&gt;. However, from my research, it seems that this support is not actively being worked on or planned at the moment.&lt;/p&gt;&lt;p&gt;So, this is how we currently execute a prototype for particular analytical tasks, and it has been successful. In a specific slow query (retrieving all sensor data of a user in a given month), we noticed a 2.5x performance improvement!&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1714060238890/7ad61828-17a7-482c-bdaf-41b6d93fd463.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;If you have any questions, feel free to email me at sven.eliasson-at-gmail.com&lt;/p&gt;]]&gt;</content:encoded><hashnode:content>&lt;![CDATA[&lt;p&gt;&lt;strong&gt;&lt;em&gt;TLDR: Stream PostgreSQL data to ClickHouse without Kafka using MaterializedPostgreSQL for real-time data replication and optimization, achieving significant data compression and query 2.5x performance improvements.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Dealing with billions of sensor records, as we do at Rysta, is no easy task. We encountered challenges in maintaining the performance of some of our analytical workloads within PostgreSQL. PostgreSQL has been exceptional for us, and I suggest that anyone handling analytical tasks (OLAP) should start with PostgreSQL, specifically TimescaleDB.&lt;/p&gt;&lt;p&gt;However, for numerous analytical queries, PostgreSQL&apos;s row-based storage is slower compared to columnar storage used in many OLAP databases. After exploring various analytical database choices, I fell in love with &lt;strong&gt;ClickHouse&lt;/strong&gt;. It excites me daily, and I started creating prototypes using ClickHouse, which delivered exceptional performance.&lt;/p&gt;&lt;p&gt;Currently, we store all our sensor records in a PostgreSQL database, and we have no plans to change this setup soon. Hence, we aim to replicate data from PostgreSQL to ClickHouse in real-time to power certain analytical workloads. A common method involves using CDC with Debezium/Kafka, as explained in &lt;a target=&quot;_blank&quot; href=&quot;https://clickhouse.com/blog/clickhouse-postgresql-change-data-capture-cdc-part-1&quot;&gt;this blog post&lt;/a&gt;. While this is a reliable and proven method, we do not utilize a Kafka cluster right now, and I prefer to avoid the extra work that comes with it. So, why not stream data directly into ClickHouse without an intermediary data broker? This is precisely achievable with ClickHouse&apos;s experimental feature called &lt;a target=&quot;_blank&quot; href=&quot;https://clickhouse.com/docs/en/engines/database-engines/materialized-postgresql&quot;&gt;&lt;strong&gt;MaterializedPostgreSQL&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1714055996205/ca67f50f-0ba9-4f31-b1c1-8b3c0478475c.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;a target=&quot;_blank&quot; href=&quot;https://clickhouse.com/docs/en/engines/database-engines/materialized-postgresql&quot;&gt;MaterializedPostgreSQL&lt;/a&gt; allows you to set up a ClickHouse database from a distant PostgreSQL database. Once an initial data snapshot is taken, modifications are copied using PostgreSQL&apos;s WAL. As of now, this feature is still experimental, but it has worked perfectly for us until this point.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SET&lt;/span&gt; allow_experimental_database_materialized_postgresql=&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;DATABASE&lt;/span&gt; postgres&lt;span class=&quot;hljs-keyword&quot;&gt;ENGINE&lt;/span&gt; = MaterializedPostgreSQL(&lt;span class=&quot;hljs-string&quot;&gt;&apos;host:port&apos;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&apos;database&apos;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&apos;user&apos;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&apos;password&apos;&lt;/span&gt;)&lt;span class=&quot;hljs-keyword&quot;&gt;SETTINGS&lt;/span&gt; materialized_postgresql_schema = &lt;span class=&quot;hljs-string&quot;&gt;&apos;myschema&apos;&lt;/span&gt;;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We used this approach to sync a sample dataset of about a billion records in about 5 minutes, which amazed me! The initial prototypes used a batch-wise ETL process that took over an hour for the first data sync. So already a good start! The table is automatically updated as new data is inserted into PostgreSQL.&lt;/p&gt;&lt;p&gt;Now that we have the date in our clickhouse cluster we could start using it... but it would probably not be faster than using a postgreSQL replica.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;sum&lt;/span&gt;(bytes_on_disk) &lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; system.parts &lt;span class=&quot;hljs-keyword&quot;&gt;where&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;database&lt;/span&gt; = &lt;span class=&quot;hljs-string&quot;&gt;&apos;postgres&apos;&lt;/span&gt;/ &lt;span class=&quot;hljs-number&quot;&gt;21841081469&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The sample dataset uses around 20GB of data for a billion records. Let&apos;s utilize ClickHouse&apos;s specific features to optimize this dataset for quicker query performance:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Use codecs to compress the physical data size; smaller datasets are often faster to query.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Utilize a sparse index to reduce the number of chunks that need to be scanned line by line.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Implement a partition that will divide the table based on a pattern optimized for the queries we intend to execute.&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;There are trade-offs to consider, and the table structure should be fine-tuned based on your most crucial queries. The table DDL that proved to be most efficient was:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;TABLE&lt;/span&gt; mydb.records (&lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt; Float32 CODEC(Gorilla),t DateTime CODEC(DoubleDelta),sensor_id UInt32 CODEC(T64)) &lt;span class=&quot;hljs-keyword&quot;&gt;ENGINE&lt;/span&gt; = ReplicatedMergeTree&lt;span class=&quot;hljs-keyword&quot;&gt;PARTITION&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; toYYYYMM(t)  &lt;span class=&quot;hljs-keyword&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;BY&lt;/span&gt; (toStartOfHour(t), sensor_value_id, t)PRIMARY &lt;span class=&quot;hljs-keyword&quot;&gt;KEY&lt;/span&gt; ( toStartOfHour(t), sensor_value_id);&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now that we have the perfect table, how do we populate it with data? I could insert data from the Postgres database into my ClickHouse-native table using an insert query, but this would have to be done for every insertion into the Postgres table. This is where we can make use of ClickHouse&apos;s &quot;Materialized view,&quot; which functions differently compared to other databases like PostgreSQL. It&apos;s more like a &lt;em&gt;trigger&lt;/em&gt; that runs a query on a specific table every time data is inserted into the source table.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;MATERIALIZED&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VIEW&lt;/span&gt; mydb.records_mv &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; mydb.records &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;     &lt;span class=&quot;hljs-keyword&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt;,     &lt;span class=&quot;hljs-built_in&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; t,     sensor_id &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; sensor_id&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; postgres.records;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After running this query, whenever new data is added to the PostgreSQL table, it will also be inserted into our ClickHouse-native, optimized table. This process only adds new records. However, we can initially copy over a snapshot like this:&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;INSERT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;INTO&lt;/span&gt; mydb.sensor_value_record&lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt;    &lt;span class=&quot;hljs-keyword&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt;,     &lt;span class=&quot;hljs-built_in&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; t,     sensor_id &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; sensor_id&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; postgres.records&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Voila! The resulting table compressed the initial 20GB into 3.9GB, achieving an &lt;strong&gt;87%&lt;/strong&gt; compression.&lt;/p&gt;&lt;p&gt;So that&apos;s it? Basically yes.&lt;br /&gt;So everything is great? Not exactly...&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-sql&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;MATERIALIZED&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VIEW&lt;/span&gt; mydb.records_mv_2024_04 &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; mydb.records &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt;, &lt;span class=&quot;hljs-built_in&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; t, sensor_id &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; sensor_id&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; postgres.records_2024_04;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;MATERIALIZED&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VIEW&lt;/span&gt; mydb.records_mv_2024_05 &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; mydb.records &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt;, &lt;span class=&quot;hljs-built_in&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; t, sensor_id &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; sensor_id&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; postgres.records_2024_05;&lt;span class=&quot;hljs-keyword&quot;&gt;CREATE&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;MATERIALIZED&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;VIEW&lt;/span&gt; mydb.records_mv_2024_06 &lt;span class=&quot;hljs-keyword&quot;&gt;TO&lt;/span&gt; mydb.records &lt;span class=&quot;hljs-keyword&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;value&lt;/span&gt;, &lt;span class=&quot;hljs-built_in&quot;&gt;timestamp&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; t, sensor_id &lt;span class=&quot;hljs-keyword&quot;&gt;as&lt;/span&gt; sensor_id&lt;span class=&quot;hljs-keyword&quot;&gt;FROM&lt;/span&gt; postgres.records_2024_06;... and so on&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I hope there will be native support for partitions in &lt;a target=&quot;_blank&quot; href=&quot;https://clickhouse.com/docs/en/engines/database-engines/materialized-postgresql&quot;&gt;MaterializedPostgreSQL&lt;/a&gt;. However, from my research, it seems that this support is not actively being worked on or planned at the moment.&lt;/p&gt;&lt;p&gt;So, this is how we currently execute a prototype for particular analytical tasks, and it has been successful. In a specific slow query (retrieving all sensor data of a user in a given month), we noticed a 2.5x performance improvement!&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1714060238890/7ad61828-17a7-482c-bdaf-41b6d93fd463.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;If you have any questions, feel free to email me at sven.eliasson-at-gmail.com&lt;/p&gt;]]&gt;</hashnode:content><hashnode:coverImage>https://cdn.hashnode.com/res/hashnode/image/upload/v1714059286187/323b96fd-dc4b-408f-941a-7e0b5c29815b.png</hashnode:coverImage></item><item><title><![CDATA[Bun+ElysiaJS vs Node+Express.js Using a Single Real-World Request]]></title><description><![CDATA[Dive into my benchmark comparison of Bun+ElysiaJS and Node+Express.js, featuring a real-world request scenario.]]></description><link>https://sveneliasson.de/bunelysiajs-vs-nodeexpressjs-using-a-single-real-world-request</link><guid isPermaLink="true">https://sveneliasson.de/bunelysiajs-vs-nodeexpressjs-using-a-single-real-world-request</guid><category><![CDATA[Bun]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Express.js]]></category><category><![CDATA[Benchmark]]></category><dc:creator><![CDATA[Sven Eliasson]]></dc:creator><pubDate>Sun, 17 Dec 2023 19:35:07 GMT</pubDate><content:encoded>&lt;![CDATA[&lt;p&gt;This is a brief overview of a basic performance test. I don&apos;t claim that my tests can be extrapolated to anything beyond the scope of this specific test.&lt;/p&gt;&lt;p&gt;Bun is the latest addition to the JavaScript runtime scene, challenging the dominance of Node.js in backend applications. Similar to another competitor, Deno, it compiles TypeScript by default. However, Bun promises to be significantly faster than Node.js and to serve as a direct, 1:1 replacement for Node.js. Back in September, when Bun reached version 1.0, I conducted a preliminary test with one of our APIs. The results showed similar performance to our existing Node.js (Express.js) app but with a 100% increase in memory usage. This wasn&apos;t a great start, but I hadn&apos;t implemented any of Bun&apos;s optimizations, and there were some bugs causing memory leaks. I decided to wait a bit longer until Bun had matured more.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1702834685602/31821c3c-f7a8-48f5-bf87-6939583edbf0.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;A couple of days ago, I discovered ElysiaJS. Elysia uses Bun as its default runtime and boasts performance figures that seem almost unbelievable. The framework looks so clean and fun that I decided to test it and see if it&apos;s worth further exploration.&lt;/p&gt;&lt;p&gt;For my tests, I plan to reimplement one API endpoint from our existing Express.js application. Our current primary challenge is request latency. We primarily focus on delivering sensor data with &apos;low latency&apos; to user dashboards or apps. A commonly used request in our service typically takes around 700ms (eww..). We&apos;ve had to delay implementing more features because they would further increase this latency. Therefore, reducing latency would be incredibly beneficial.&lt;/p&gt;&lt;p&gt;The request I plan to use for this benchmark will involve the following steps:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Authenticate the user using a JWT (JSON Web Token).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Make two requests to MongoDB: one to fetch the user&apos;s credentials and another to retrieve the devices the user has access to, using Mongoose.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Execute a SQL request for each device using Postgres.js (PostgreSQL).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Convert the data to a format that&apos;s more user-friendly for the frontend.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Append additional data from a lookup table.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Return the JSON payload, which is approximately 9kB in size.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;All of this will be served behind an Nginx load balancer inside a docker inside our Kubernetes cluster&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So let&apos;s start to implement the API in bun/elysia. First, we need to install bun and start a bun project. Thanks to &quot;bun create&quot; templates this can be done in seconds.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;curl -fsSL https://bun.sh/install | bash  &lt;span class=&quot;hljs-comment&quot;&gt;# install bun &lt;/span&gt;bun create elysia app&lt;span class=&quot;hljs-built_in&quot;&gt;cd&lt;/span&gt; app&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;My basic Elysia app looks roughly like this (censored):&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { Elysia } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;elysia&quot;&lt;/span&gt;;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { swagger } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@elysiajs/swagger&quot;&lt;/span&gt;;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; config = &lt;span class=&quot;hljs-built_in&quot;&gt;require&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&apos;../config/config&apos;&lt;/span&gt;); &lt;span class=&quot;hljs-comment&quot;&gt;// a config object&lt;/span&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; postgresjs &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&apos;postgres&apos;&lt;/span&gt;;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; mongoose_models = &lt;span class=&quot;hljs-built_in&quot;&gt;require&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&apos;./get_mongoose_models&apos;&lt;/span&gt;);&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; mongoConnection = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;require&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&apos;./get_mongoose_connection&apos;&lt;/span&gt;)(config.DB.MONGODB);&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; Entities = &lt;span class=&quot;hljs-built_in&quot;&gt;require&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&apos;./Entities&apos;&lt;/span&gt;); &lt;span class=&quot;hljs-comment&quot;&gt;// a data service class&lt;/span&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; app = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; Elysia()  .use(swagger()) &lt;span class=&quot;hljs-comment&quot;&gt;// will automatically generate openAPI specification! &lt;/span&gt;  .decorate(&lt;span class=&quot;hljs-string&quot;&gt;&quot;mongoose_models&quot;&lt;/span&gt;, mongoose_models(mongoConnection))  &lt;span class=&quot;hljs-comment&quot;&gt;// will append the mongoose-models to each request&lt;/span&gt;  .decorate(&lt;span class=&quot;hljs-string&quot;&gt;&quot;postgresjs&quot;&lt;/span&gt;, postgresjs({    &lt;span class=&quot;hljs-attr&quot;&gt;host&lt;/span&gt;: config.DB.MAIN.HOST,    &lt;span class=&quot;hljs-attr&quot;&gt;port&lt;/span&gt;: config.DB.MAIN.PORT,    &lt;span class=&quot;hljs-attr&quot;&gt;database&lt;/span&gt;: config.DB.MAIN.DATABASE_NAME,    &lt;span class=&quot;hljs-attr&quot;&gt;user&lt;/span&gt;: config.DB.MAIN.USERNAME,    &lt;span class=&quot;hljs-attr&quot;&gt;password&lt;/span&gt;: config.DB.MAIN.PASSWORD,  }))  &lt;span class=&quot;hljs-comment&quot;&gt;// will append postgres instance to each object (uses a singleton)&lt;/span&gt;  .decorate(&lt;span class=&quot;hljs-string&quot;&gt;&quot;config&quot;&lt;/span&gt;, config)  &lt;span class=&quot;hljs-comment&quot;&gt;// append config to reach request&lt;/span&gt;  .decorate(&lt;span class=&quot;hljs-string&quot;&gt;&quot;entities&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; Entities())  &lt;span class=&quot;hljs-comment&quot;&gt;// append data service&lt;/span&gt;  .get(&lt;span class=&quot;hljs-string&quot;&gt;&quot;test&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;async&lt;/span&gt; (req) =&amp;gt; {  &lt;span class=&quot;hljs-comment&quot;&gt;// add endpoint &lt;/span&gt;    &lt;span class=&quot;hljs-comment&quot;&gt;// you can use req.mongoose_model and all other decorators here&lt;/span&gt;    &lt;span class=&quot;hljs-comment&quot;&gt;// HERE WAS MY ENDPOINT CODE (CENSORED) &lt;/span&gt;    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; { &lt;span class=&quot;hljs-attr&quot;&gt;my&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&quot;payload&quot;&lt;/span&gt; };   })  .onError(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ error }&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {    &lt;span class=&quot;hljs-built_in&quot;&gt;console&lt;/span&gt;.error(error);  })  .listen(&lt;span class=&quot;hljs-number&quot;&gt;3000&lt;/span&gt;)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, what about the performance? To evaluate this, I&apos;m using &lt;a target=&quot;_blank&quot; href=&quot;https://github.com/mcollina/autocannon&quot;&gt;Autocannon&lt;/a&gt; for request benchmarking, comparing it to our existing Express.js application :&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1702839519292/1468348c-4f2e-4e53-a652-4035af70aa4a.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;That was unexpected!&lt;/p&gt;&lt;p&gt;First of all, I can&apos;t quite explain why the original Express.js application performs so poorly with Autocannon. Normally, a single request responds in around 700ms. However, Autocannon records an average response time of about 1535 ms, while Elysia impressively clocks in at 346 ms! That&apos;s 4.4 times faster than the Express.js app under Autocannon, and even when compared to the usual 700ms response time, it&apos;s still about twice as fast.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; For this comparison, I excluded some features from the Express app that weren&apos;t present in the Elysia prototype. Notably, the Express app uses a request logger. This may introduce a significant overhead. As I continue to explore Elysia, I&apos;ll keep an eye on how the narrative shifts when a 1:1 feature comparison is reached. Should there be notable changes, I plan to update this article.  &lt;/p&gt;&lt;p&gt;Initially, I thought a 20% performance improvement would be good enough to justify further investigation, but a 2x increase is definitely a strong incentive for me to dig deeper!&lt;/p&gt;&lt;p&gt;So, what&apos;s my takeaway?&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Elysia is remarkably fast, approximately 2-4 times quicker than our existing application.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;There seems to be an issue with the Express.js app that causes significant performance degradation in Autocannon benchmarks. ;).&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;]]&gt;</content:encoded><hashnode:content>&lt;![CDATA[&lt;p&gt;This is a brief overview of a basic performance test. I don&apos;t claim that my tests can be extrapolated to anything beyond the scope of this specific test.&lt;/p&gt;&lt;p&gt;Bun is the latest addition to the JavaScript runtime scene, challenging the dominance of Node.js in backend applications. Similar to another competitor, Deno, it compiles TypeScript by default. However, Bun promises to be significantly faster than Node.js and to serve as a direct, 1:1 replacement for Node.js. Back in September, when Bun reached version 1.0, I conducted a preliminary test with one of our APIs. The results showed similar performance to our existing Node.js (Express.js) app but with a 100% increase in memory usage. This wasn&apos;t a great start, but I hadn&apos;t implemented any of Bun&apos;s optimizations, and there were some bugs causing memory leaks. I decided to wait a bit longer until Bun had matured more.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1702834685602/31821c3c-f7a8-48f5-bf87-6939583edbf0.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;A couple of days ago, I discovered ElysiaJS. Elysia uses Bun as its default runtime and boasts performance figures that seem almost unbelievable. The framework looks so clean and fun that I decided to test it and see if it&apos;s worth further exploration.&lt;/p&gt;&lt;p&gt;For my tests, I plan to reimplement one API endpoint from our existing Express.js application. Our current primary challenge is request latency. We primarily focus on delivering sensor data with &apos;low latency&apos; to user dashboards or apps. A commonly used request in our service typically takes around 700ms (eww..). We&apos;ve had to delay implementing more features because they would further increase this latency. Therefore, reducing latency would be incredibly beneficial.&lt;/p&gt;&lt;p&gt;The request I plan to use for this benchmark will involve the following steps:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Authenticate the user using a JWT (JSON Web Token).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Make two requests to MongoDB: one to fetch the user&apos;s credentials and another to retrieve the devices the user has access to, using Mongoose.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Execute a SQL request for each device using Postgres.js (PostgreSQL).&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Convert the data to a format that&apos;s more user-friendly for the frontend.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Append additional data from a lookup table.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;Return the JSON payload, which is approximately 9kB in size.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;All of this will be served behind an Nginx load balancer inside a docker inside our Kubernetes cluster&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;So let&apos;s start to implement the API in bun/elysia. First, we need to install bun and start a bun project. Thanks to &quot;bun create&quot; templates this can be done in seconds.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-bash&quot;&gt;curl -fsSL https://bun.sh/install | bash  &lt;span class=&quot;hljs-comment&quot;&gt;# install bun &lt;/span&gt;bun create elysia app&lt;span class=&quot;hljs-built_in&quot;&gt;cd&lt;/span&gt; app&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;My basic Elysia app looks roughly like this (censored):&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;lang-javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { Elysia } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;elysia&quot;&lt;/span&gt;;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { swagger } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@elysiajs/swagger&quot;&lt;/span&gt;;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; config = &lt;span class=&quot;hljs-built_in&quot;&gt;require&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&apos;../config/config&apos;&lt;/span&gt;); &lt;span class=&quot;hljs-comment&quot;&gt;// a config object&lt;/span&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; postgresjs &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&apos;postgres&apos;&lt;/span&gt;;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; mongoose_models = &lt;span class=&quot;hljs-built_in&quot;&gt;require&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&apos;./get_mongoose_models&apos;&lt;/span&gt;);&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; mongoConnection = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;require&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&apos;./get_mongoose_connection&apos;&lt;/span&gt;)(config.DB.MONGODB);&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; Entities = &lt;span class=&quot;hljs-built_in&quot;&gt;require&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&apos;./Entities&apos;&lt;/span&gt;); &lt;span class=&quot;hljs-comment&quot;&gt;// a data service class&lt;/span&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; app = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; Elysia()  .use(swagger()) &lt;span class=&quot;hljs-comment&quot;&gt;// will automatically generate openAPI specification! &lt;/span&gt;  .decorate(&lt;span class=&quot;hljs-string&quot;&gt;&quot;mongoose_models&quot;&lt;/span&gt;, mongoose_models(mongoConnection))  &lt;span class=&quot;hljs-comment&quot;&gt;// will append the mongoose-models to each request&lt;/span&gt;  .decorate(&lt;span class=&quot;hljs-string&quot;&gt;&quot;postgresjs&quot;&lt;/span&gt;, postgresjs({    &lt;span class=&quot;hljs-attr&quot;&gt;host&lt;/span&gt;: config.DB.MAIN.HOST,    &lt;span class=&quot;hljs-attr&quot;&gt;port&lt;/span&gt;: config.DB.MAIN.PORT,    &lt;span class=&quot;hljs-attr&quot;&gt;database&lt;/span&gt;: config.DB.MAIN.DATABASE_NAME,    &lt;span class=&quot;hljs-attr&quot;&gt;user&lt;/span&gt;: config.DB.MAIN.USERNAME,    &lt;span class=&quot;hljs-attr&quot;&gt;password&lt;/span&gt;: config.DB.MAIN.PASSWORD,  }))  &lt;span class=&quot;hljs-comment&quot;&gt;// will append postgres instance to each object (uses a singleton)&lt;/span&gt;  .decorate(&lt;span class=&quot;hljs-string&quot;&gt;&quot;config&quot;&lt;/span&gt;, config)  &lt;span class=&quot;hljs-comment&quot;&gt;// append config to reach request&lt;/span&gt;  .decorate(&lt;span class=&quot;hljs-string&quot;&gt;&quot;entities&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; Entities())  &lt;span class=&quot;hljs-comment&quot;&gt;// append data service&lt;/span&gt;  .get(&lt;span class=&quot;hljs-string&quot;&gt;&quot;test&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;async&lt;/span&gt; (req) =&amp;gt; {  &lt;span class=&quot;hljs-comment&quot;&gt;// add endpoint &lt;/span&gt;    &lt;span class=&quot;hljs-comment&quot;&gt;// you can use req.mongoose_model and all other decorators here&lt;/span&gt;    &lt;span class=&quot;hljs-comment&quot;&gt;// HERE WAS MY ENDPOINT CODE (CENSORED) &lt;/span&gt;    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; { &lt;span class=&quot;hljs-attr&quot;&gt;my&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&quot;payload&quot;&lt;/span&gt; };   })  .onError(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ error }&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {    &lt;span class=&quot;hljs-built_in&quot;&gt;console&lt;/span&gt;.error(error);  })  .listen(&lt;span class=&quot;hljs-number&quot;&gt;3000&lt;/span&gt;)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;So, what about the performance? To evaluate this, I&apos;m using &lt;a target=&quot;_blank&quot; href=&quot;https://github.com/mcollina/autocannon&quot;&gt;Autocannon&lt;/a&gt; for request benchmarking, comparing it to our existing Express.js application :&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1702839519292/1468348c-4f2e-4e53-a652-4035af70aa4a.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;That was unexpected!&lt;/p&gt;&lt;p&gt;First of all, I can&apos;t quite explain why the original Express.js application performs so poorly with Autocannon. Normally, a single request responds in around 700ms. However, Autocannon records an average response time of about 1535 ms, while Elysia impressively clocks in at 346 ms! That&apos;s 4.4 times faster than the Express.js app under Autocannon, and even when compared to the usual 700ms response time, it&apos;s still about twice as fast.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; For this comparison, I excluded some features from the Express app that weren&apos;t present in the Elysia prototype. Notably, the Express app uses a request logger. This may introduce a significant overhead. As I continue to explore Elysia, I&apos;ll keep an eye on how the narrative shifts when a 1:1 feature comparison is reached. Should there be notable changes, I plan to update this article.  &lt;/p&gt;&lt;p&gt;Initially, I thought a 20% performance improvement would be good enough to justify further investigation, but a 2x increase is definitely a strong incentive for me to dig deeper!&lt;/p&gt;&lt;p&gt;So, what&apos;s my takeaway?&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;Elysia is remarkably fast, approximately 2-4 times quicker than our existing application.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;There seems to be an issue with the Express.js app that causes significant performance degradation in Autocannon benchmarks. ;).&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;]]&gt;</hashnode:content><hashnode:coverImage>https://cdn.hashnode.com/res/hashnode/image/upload/v1714236478995/d636ca86-f2e2-45c1-926a-fd06dfc77a70.png</hashnode:coverImage></item><item><title><![CDATA[Decoding Plot Data with ChatGPT: A Journey into Chart Analysis]]></title><description><![CDATA[Explore the fascinating journey of using ChatGPT to interpret data dashboard charts in this insightful article. Discover how a simple observation of a custo]]></description><link>https://sveneliasson.de/decoding-plot-data-with-chatgpt-a-journey-into-chart-analysis-part-1</link><guid isPermaLink="true">https://sveneliasson.de/decoding-plot-data-with-chatgpt-a-journey-into-chart-analysis-part-1</guid><category><![CDATA[chatgpt]]></category><category><![CDATA[Data Science]]></category><category><![CDATA[sensors]]></category><category><![CDATA[charts]]></category><dc:creator><![CDATA[Sven Eliasson]]></dc:creator><pubDate>Mon, 20 Nov 2023 20:20:56 GMT</pubDate><content:encoded>&lt;![CDATA[&lt;p&gt;While developing features for our data dashboard, I stumbled upon an unexpected discovery: a customer was experiencing issues with their heating system without being aware of it. It was easy to spot the issue just by looking, but the real challenge was creating an algorithm to do the same. The complexity of crafting such an algorithm was a bit daunting. As a temporary measure, we opted for a human-centric approach, assigning a working student to manually review the data by looking at the data dashboard each week.&lt;/p&gt;&lt;p&gt;Recently, at OpenAI Dev Day, I had a lightbulb moment. The question popped up: Can ChatGPT, famous for working with text, also make sense of charts? Honestly, I had my doubts. It seemed like a stretch, yet interesting enough to try out. To my surprise, ChatGPT was actually not that bad at interpreting chart data.&lt;/p&gt;&lt;p&gt;While I&apos;m still a bit unsure about how useful this is right now, the success of these tests is worth mentioning. It seems like not many in the AI field have talked about this, so I wanted to share my experience. Let&apos;s dive into these experiments together and see how ChatGPT can handle graphs and charts in ways that might surprise you.&lt;/p&gt;&lt;h2 id=&quot;heading-1-the-initial-idea-tell-me-what-might-be-wrong-here&quot;&gt;1) The initial idea: &quot;Tell me what might be wrong here&quot;&lt;/h2&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1700515125830/7e832ec3-a189-4074-b991-f01b4129cfe0.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;This example shows a basic error we&apos;re trying to catch. The room&apos;s temperature stays almost the same and doesn&apos;t match the target temperature, shown as a dotted line in the first chart. The second chart displays the heating power we&apos;re sending through the valve. It&apos;s at 100% most of the time, but it doesn&apos;t seem to raise the room&apos;s temperature at all.&lt;/p&gt;&lt;h3 id=&quot;heading-step-1-add-just-very-few-context&quot;&gt;Step 1: Add just very few context&lt;/h3&gt;&lt;p&gt;&quot;For my first attempt, I simply described what was in the image, to test if ChatGPT could understand anything from it.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Prompt: &quot;This is a plot from a dashboard for a smart thermostat. The first graph is the Temperature with the setpoint in the background as a dottet line, the second graph &quot;Heizleistung&quot; is the &quot;heating power&quot; which is the position of the valve. The third graph is the Setpoint temperature with the actual temperature as a dashed line. The fourth plot it the &quot;flowtemperature&quot;, which is the temperature of the water in the heating element. Can you spot a malfunction of the control system?&quot;&lt;/p&gt;&lt;p&gt;ChatGPT: Air Temperature (Lufttemperatur): The room&apos;s air temperature remains mostly stable, slightly fluctuating around the setpoint. This graph suggests a well-regulated temperature within the room, staying close to the desired setpoint with minimal variance.&lt;/p&gt;&lt;p&gt;Heating Power (Heizleistung): There is a significant period where the heating power is at or near 100%, which may suggest an attempt to correct a large temperature discrepancy or respond to a significant drop in temperature. &lt;strong&gt;However, despite this high heating power, the actual temperature doesn&apos;t appear to overshoot the setpoint by a large margi&lt;/strong&gt;n, which is positive. It&apos;s worth noting that the dense clustering of dots, especially during the times when the heating power is&quot;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1700521351486/ccaccf55-d527-4aec-8f9a-e1013b15db05.gif&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;At first glance, this looks like chatGPT completely misses what&apos;s actually going on and was about to close this experiment as a &quot;nice try&quot;. But one sentence caught my attention: &quot;However, despite this high heating power, the actual temperature doesn&apos;t appear to overshoot the setpoint by a large margin.&quot; This was a correct observation, just not interpreted correctly. It showed that ChatGPT may have some understanding of the shown data!&lt;/p&gt;&lt;h3 id=&quot;heading-step-2-let-me-tell-you-what-to-look-for-and-tell-me-whats-wrong&quot;&gt;Step 2: &quot;Let me tell you what to look for and tell me what&apos;s wrong&quot;.&lt;/h3&gt;&lt;p&gt;Maybe we just need a little prompt tuning? I added a very long explanation of what to look for. Here is the full prompt as a reference:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Prompt: You are a thermostat control system data analyst. I will show you the historic sensor data of a dashboard of a smart thermostat and you try to evaluate if it has potential problems that we need to tell the user. This is a plot from a dashboard for a smart thermostat. 1) The first graph is the (air) temperature in orange, red or green with the setpoint in the background as a dashed gray line 2) the second graph &quot;valve position&quot; also called &quot;heating power&quot; which is the position of the water valve to the heating element with a blue, yellow or red line. 0% means no heating, 100% means all heating available. In the background of this plot is the actual temperature as a gray dashed line. 3) The third graph is the Setpoint temperature that the user selected with a black line and the actual temperature again as a gray dashed line. 4) The fourth plot is the &quot;flowtemperature&quot;, which is the temperature of the water in the heating element with a black line. The actual temperature in 1) should somehow follow the setpoint temperature and now over- or undershoot more than 1C during the day. On night times it is expected, that the temperature will not directly follow the drop in the setpoint since the room needs to cool down. Thats okay. The heating power in 2) indicates, that we are currently trying to heat the room. I might be, that the heating system is not responding e.g. in case the water valve to the heating element is closed or malfunctioning. You should see a rise in the temperature in 2) or 1) once the heating power rises. In case the target temperature is not met over a longer period of time while heating power is 100%, there is clearly not enough heat to heat up the room. The flow temperature might be an indicator if the heating boiler system works at all. In case the heating power is set to 100% and we dont see a rise in flow temperature, there is probably a problem with the system. The target overall is to save energy while maintinaing a good temperature during working hours. You carefully remember all of this and review dashboard images accordingly.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;And chatGPT&apos;s answer:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Based on the image provided of the dashboard for a smart thermostat, here&apos;s an analysis of each of the graphs:&lt;br /&gt;Air Temperature: The air temperature (orange line) appears to be relatively stable and follows the setpoint (dashed gray line) closely. There are minimal fluctuations, which is a good sign that the thermostat is maintaining the desired temperature within the expected tolerance. There do not seem to be significant over- or undershoots beyond the 1C threshold during the day. This suggests the system is responding appropriately to the setpoint.&lt;/p&gt;&lt;p&gt;Valve Position/Heating Power: There are times (notably around Tue, 31 Oct) where the valve position (red line) goes to 100%, indicating maximum heating power. During this time, the actual temperature (gray dashed line in the background) does not show a corresponding rise, which could indicate that the room isn&apos;t heating up as one would expect when the valve is fully open. This may be a sign that either the heating system is unable to provide enough heat or there&apos;s an issue with the valve not actually allowing flow despite indicating it&apos;s fully open.&lt;/p&gt;&lt;p&gt;Setpoint Temperature: The setpoint temperature (black line) is adjusted at several points, and it&apos;s clear that the actual temperature (gray dashed line) tends to follow these adjustments, though not always immediately. This lag could be normal due to thermal inertia, as you&apos;ve noted that it&apos;s expected for the temperature not to follow the drop during night times immediately.&lt;/p&gt;&lt;p&gt;Flow Temperature: The flow temperature (black line) does show variations, and these should correspond to the valve positions. However, the graph seems to indicate that the flow temperature does drop at times even when the valve position is high (close to 100%). If this happens without a corresponding decrease in valve position, it might indicate a problem with the boiler system not heating the water adequately.&lt;/p&gt;&lt;p&gt;Based on these observations, the main concerns would be:&lt;/p&gt;&lt;p&gt;The periods where the valve position is at 100% but there isn&apos;t a corresponding increase in either the air temperature or the flow temperature. The potential lack of responsiveness of the system when the setpoint temperature is changed, which could indicate an issue with system latency or an underperforming heating system. It would be recommended to check the heating system, especially the water valve and the boiler, to ensure they are functioning properly. If the valve is not truly opening or if the boiler is not heating the water adequately, these would need to be addressed to ensure the system can maintain the desired temperature efficiently.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1700520199636/440b77d8-ccbc-4d27-86dc-82463feda404.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;ChatGPT got a lot of observations wrong, but some were correct! The final summary actually described the main problem well. It seems like ChatGPT may interpret charts given a description of what to look at. But there are also so many incorrect observations...&lt;/p&gt;&lt;h3 id=&quot;heading-step-3-go-step-by-step-review-your-observations-and-finally-pick-one&quot;&gt;Step 3: &quot;Go step by step, review your observations and finally pick one&quot;&lt;/h3&gt;&lt;p&gt;It looks like even after adjusting the prompt, ChatGPT still makes some incorrect observations when given a zero-shot prompt. ChatGPT seems to struggle with understanding concepts like &quot;close enough&quot; or measuring the difference between two lines. In our case, the temperature was stable but not following the setpoint, which ChatGPT failed to consistently detect, even with prompt engineering.&lt;/p&gt;&lt;p&gt;However, the fact that there were also some correct observations is promising. It suggests that with a follow-up prompt, we might be able to guide ChatGPT to focus on and expand upon its accurate observations.&lt;/p&gt;&lt;p&gt;I tuned the initial prompt to make the initial response more detailed and then using a second prompt to ask ChatGPT to focus on the observation it is most confident about is a good strategy. This two-step process could help filter out incorrect interpretations and highlight the AI&apos;s more accurate insights.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Prompt after first response: We want to contact the user. Pick the most critical observation and summarize it in at most two sentences. This will be send to the user so it should be correct and professional. Respond in this format: 03.11.2023 10:00 - 03.11.23 23:00 : OBSERVATION&lt;/p&gt;&lt;p&gt;ChatGPT:&lt;/p&gt;&lt;p&gt;03.11.2023 10:00 - 03.11.2023 23:00: Our monitoring indicates that the heating system is frequently operating at full capacity without adequately increasing the room temperature. This may suggest a potential issue with the system&apos;s ability to deliver heat efficiently, and we recommend a system check to ensure optimal performance and comfort.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1700520323246/f98b4b57-3f48-419d-ab6b-c2ec41421c29.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;This looks promising, but there&apos;s a chance it might not truly understand and just hallucinated answers I might like. It could be making good guesses based on the errors I mentioned in my prompt. The selected time seems to be random, why didnt it pick up the whole time frame from 31st Oct until the end? I will explore the time frame detection in a later post. When providing examples of correct working thermostats it didn&apos;t make the same observations. But this isn&apos;t solid proof of its understanding and I think I might need to go one step back and test chatGPT&apos;s basic skills in understanding.&lt;/p&gt;&lt;h2 id=&quot;heading-2-basic-chart-data-reading&quot;&gt;2) Basic chart data reading&lt;/h2&gt;&lt;p&gt;Let&apos;s test if ChatGPT can interpret data. I began with a basic time series chart depicting a fluctuating sine wave, which includes an anomaly. This graph represents the temperature in a forest experiencing a nighttime fire. I used this image to explore what ChatGPT can discern from it. Interestingly chatGPT initially refuses to do this! Some motivational pep talk was needed for it to try.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1700519665298/184863da-95b6-40cc-b0c5-cbe43eb92205.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Prompt: This is a screenshot of a sensor dashboard. Carefully look at the graph and the time shown on the axis. You try really hard to be correct and exact. Im under pressure so you have to help me. I know this is hard, but please try. Return a list of time and value that represents this plot. It doesnt need to be perfect. Just as good as possible. Return a value for every hour in a JSON structrue. Do not explain.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I created a plot that compares the values ChatGPT returned with the original data.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1700519701877/281c460e-979d-4d79-b789-718612dc6fb0.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://files.oaiusercontent.com/file-xW905Pyolh8UEembl3vdOQGh?se=2023-11-20T20%3A53%3A43Z&amp;amp;sp=r&amp;amp;sv=2021-08-06&amp;amp;sr=b&amp;amp;rscc=max-age%3D3599%2C%20immutable&amp;amp;rscd=attachment%3B%20filename%3D437e000b-b164-4ac4-9821-8b4090b192f6&amp;amp;sig=5l1w/LcCXBI6xvUjucTTDqIcuIkPmYcLesW74rtUVFw%3D&quot; alt /&gt;&lt;/p&gt;&lt;p&gt;Not that bad! ChatGPT did well in some areas, capturing the early part of the graph pretty accurately. It traced the overall trend well, but it totally missed the anomaly later in the chart. Despite trying various prompts, asking for a review, and self-reflection, ChatGPT was unable to identify the anomaly. Overall, it&apos;s fair to conclude that ChatGPT can extract values from a chart, though with limited accuracy and it seems to follow general trends. Improving the chart&apos;s design might enhance this accuracy. I plan to delve into this in a future article. (Spoiler alert: initial tests suggest that tweaking the chart doesn&apos;t significantly improve accuracy.)&lt;/p&gt;&lt;p&gt;In the first part of my experiments, I found that ChatGPT can read and understand data from a chart, but its accuracy is limited. Next, I plan to explore how ChatGPT processes chart information. Although this method currently has limited practical use and there are likely better approaches, like LSTM or parsing data JSON encoded instead of visually. However, it&apos;s an interesting topic to investigate and who knows how ChatGPT will evolve in the future.&lt;/p&gt;&lt;p&gt;In the second part of this series, I&apos;ll conduct more experiments to provide a more quantitative analysis of ChatGPT&apos;s strengths and weaknesses reading chart data.&lt;/p&gt;&lt;h3 id=&quot;heading-observations-so-far&quot;&gt;Observations so far&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;ChatGPT can read data from a chart with limited accuracy&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;ChatGPT can correlate two distinct plots and compare their relative trend.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;ChatGPT struggles to merge visual proximity with numerical similarity. It often fails to grasp the differences between two lines on a chart. Additionally, when we zoom in on the same data, ChatGPT&apos;s interpretations change. (More in part 2)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;ChatGPT can identify the timing of events observed in data, but its consistency is not always reliable. (More in part 2)&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Update on Part 2: GPT became more restrictive and simply refuses to readout the data from the chart more and more. Need to investigate a reliable jailbreak before part 2.&lt;/p&gt;]]&gt;</content:encoded><hashnode:content>&lt;![CDATA[&lt;p&gt;While developing features for our data dashboard, I stumbled upon an unexpected discovery: a customer was experiencing issues with their heating system without being aware of it. It was easy to spot the issue just by looking, but the real challenge was creating an algorithm to do the same. The complexity of crafting such an algorithm was a bit daunting. As a temporary measure, we opted for a human-centric approach, assigning a working student to manually review the data by looking at the data dashboard each week.&lt;/p&gt;&lt;p&gt;Recently, at OpenAI Dev Day, I had a lightbulb moment. The question popped up: Can ChatGPT, famous for working with text, also make sense of charts? Honestly, I had my doubts. It seemed like a stretch, yet interesting enough to try out. To my surprise, ChatGPT was actually not that bad at interpreting chart data.&lt;/p&gt;&lt;p&gt;While I&apos;m still a bit unsure about how useful this is right now, the success of these tests is worth mentioning. It seems like not many in the AI field have talked about this, so I wanted to share my experience. Let&apos;s dive into these experiments together and see how ChatGPT can handle graphs and charts in ways that might surprise you.&lt;/p&gt;&lt;h2 id=&quot;heading-1-the-initial-idea-tell-me-what-might-be-wrong-here&quot;&gt;1) The initial idea: &quot;Tell me what might be wrong here&quot;&lt;/h2&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1700515125830/7e832ec3-a189-4074-b991-f01b4129cfe0.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;This example shows a basic error we&apos;re trying to catch. The room&apos;s temperature stays almost the same and doesn&apos;t match the target temperature, shown as a dotted line in the first chart. The second chart displays the heating power we&apos;re sending through the valve. It&apos;s at 100% most of the time, but it doesn&apos;t seem to raise the room&apos;s temperature at all.&lt;/p&gt;&lt;h3 id=&quot;heading-step-1-add-just-very-few-context&quot;&gt;Step 1: Add just very few context&lt;/h3&gt;&lt;p&gt;&quot;For my first attempt, I simply described what was in the image, to test if ChatGPT could understand anything from it.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Prompt: &quot;This is a plot from a dashboard for a smart thermostat. The first graph is the Temperature with the setpoint in the background as a dottet line, the second graph &quot;Heizleistung&quot; is the &quot;heating power&quot; which is the position of the valve. The third graph is the Setpoint temperature with the actual temperature as a dashed line. The fourth plot it the &quot;flowtemperature&quot;, which is the temperature of the water in the heating element. Can you spot a malfunction of the control system?&quot;&lt;/p&gt;&lt;p&gt;ChatGPT: Air Temperature (Lufttemperatur): The room&apos;s air temperature remains mostly stable, slightly fluctuating around the setpoint. This graph suggests a well-regulated temperature within the room, staying close to the desired setpoint with minimal variance.&lt;/p&gt;&lt;p&gt;Heating Power (Heizleistung): There is a significant period where the heating power is at or near 100%, which may suggest an attempt to correct a large temperature discrepancy or respond to a significant drop in temperature. &lt;strong&gt;However, despite this high heating power, the actual temperature doesn&apos;t appear to overshoot the setpoint by a large margi&lt;/strong&gt;n, which is positive. It&apos;s worth noting that the dense clustering of dots, especially during the times when the heating power is&quot;&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1700521351486/ccaccf55-d527-4aec-8f9a-e1013b15db05.gif&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;At first glance, this looks like chatGPT completely misses what&apos;s actually going on and was about to close this experiment as a &quot;nice try&quot;. But one sentence caught my attention: &quot;However, despite this high heating power, the actual temperature doesn&apos;t appear to overshoot the setpoint by a large margin.&quot; This was a correct observation, just not interpreted correctly. It showed that ChatGPT may have some understanding of the shown data!&lt;/p&gt;&lt;h3 id=&quot;heading-step-2-let-me-tell-you-what-to-look-for-and-tell-me-whats-wrong&quot;&gt;Step 2: &quot;Let me tell you what to look for and tell me what&apos;s wrong&quot;.&lt;/h3&gt;&lt;p&gt;Maybe we just need a little prompt tuning? I added a very long explanation of what to look for. Here is the full prompt as a reference:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Prompt: You are a thermostat control system data analyst. I will show you the historic sensor data of a dashboard of a smart thermostat and you try to evaluate if it has potential problems that we need to tell the user. This is a plot from a dashboard for a smart thermostat. 1) The first graph is the (air) temperature in orange, red or green with the setpoint in the background as a dashed gray line 2) the second graph &quot;valve position&quot; also called &quot;heating power&quot; which is the position of the water valve to the heating element with a blue, yellow or red line. 0% means no heating, 100% means all heating available. In the background of this plot is the actual temperature as a gray dashed line. 3) The third graph is the Setpoint temperature that the user selected with a black line and the actual temperature again as a gray dashed line. 4) The fourth plot is the &quot;flowtemperature&quot;, which is the temperature of the water in the heating element with a black line. The actual temperature in 1) should somehow follow the setpoint temperature and now over- or undershoot more than 1C during the day. On night times it is expected, that the temperature will not directly follow the drop in the setpoint since the room needs to cool down. Thats okay. The heating power in 2) indicates, that we are currently trying to heat the room. I might be, that the heating system is not responding e.g. in case the water valve to the heating element is closed or malfunctioning. You should see a rise in the temperature in 2) or 1) once the heating power rises. In case the target temperature is not met over a longer period of time while heating power is 100%, there is clearly not enough heat to heat up the room. The flow temperature might be an indicator if the heating boiler system works at all. In case the heating power is set to 100% and we dont see a rise in flow temperature, there is probably a problem with the system. The target overall is to save energy while maintinaing a good temperature during working hours. You carefully remember all of this and review dashboard images accordingly.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;And chatGPT&apos;s answer:&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Based on the image provided of the dashboard for a smart thermostat, here&apos;s an analysis of each of the graphs:&lt;br /&gt;Air Temperature: The air temperature (orange line) appears to be relatively stable and follows the setpoint (dashed gray line) closely. There are minimal fluctuations, which is a good sign that the thermostat is maintaining the desired temperature within the expected tolerance. There do not seem to be significant over- or undershoots beyond the 1C threshold during the day. This suggests the system is responding appropriately to the setpoint.&lt;/p&gt;&lt;p&gt;Valve Position/Heating Power: There are times (notably around Tue, 31 Oct) where the valve position (red line) goes to 100%, indicating maximum heating power. During this time, the actual temperature (gray dashed line in the background) does not show a corresponding rise, which could indicate that the room isn&apos;t heating up as one would expect when the valve is fully open. This may be a sign that either the heating system is unable to provide enough heat or there&apos;s an issue with the valve not actually allowing flow despite indicating it&apos;s fully open.&lt;/p&gt;&lt;p&gt;Setpoint Temperature: The setpoint temperature (black line) is adjusted at several points, and it&apos;s clear that the actual temperature (gray dashed line) tends to follow these adjustments, though not always immediately. This lag could be normal due to thermal inertia, as you&apos;ve noted that it&apos;s expected for the temperature not to follow the drop during night times immediately.&lt;/p&gt;&lt;p&gt;Flow Temperature: The flow temperature (black line) does show variations, and these should correspond to the valve positions. However, the graph seems to indicate that the flow temperature does drop at times even when the valve position is high (close to 100%). If this happens without a corresponding decrease in valve position, it might indicate a problem with the boiler system not heating the water adequately.&lt;/p&gt;&lt;p&gt;Based on these observations, the main concerns would be:&lt;/p&gt;&lt;p&gt;The periods where the valve position is at 100% but there isn&apos;t a corresponding increase in either the air temperature or the flow temperature. The potential lack of responsiveness of the system when the setpoint temperature is changed, which could indicate an issue with system latency or an underperforming heating system. It would be recommended to check the heating system, especially the water valve and the boiler, to ensure they are functioning properly. If the valve is not truly opening or if the boiler is not heating the water adequately, these would need to be addressed to ensure the system can maintain the desired temperature efficiently.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1700520199636/440b77d8-ccbc-4d27-86dc-82463feda404.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;ChatGPT got a lot of observations wrong, but some were correct! The final summary actually described the main problem well. It seems like ChatGPT may interpret charts given a description of what to look at. But there are also so many incorrect observations...&lt;/p&gt;&lt;h3 id=&quot;heading-step-3-go-step-by-step-review-your-observations-and-finally-pick-one&quot;&gt;Step 3: &quot;Go step by step, review your observations and finally pick one&quot;&lt;/h3&gt;&lt;p&gt;It looks like even after adjusting the prompt, ChatGPT still makes some incorrect observations when given a zero-shot prompt. ChatGPT seems to struggle with understanding concepts like &quot;close enough&quot; or measuring the difference between two lines. In our case, the temperature was stable but not following the setpoint, which ChatGPT failed to consistently detect, even with prompt engineering.&lt;/p&gt;&lt;p&gt;However, the fact that there were also some correct observations is promising. It suggests that with a follow-up prompt, we might be able to guide ChatGPT to focus on and expand upon its accurate observations.&lt;/p&gt;&lt;p&gt;I tuned the initial prompt to make the initial response more detailed and then using a second prompt to ask ChatGPT to focus on the observation it is most confident about is a good strategy. This two-step process could help filter out incorrect interpretations and highlight the AI&apos;s more accurate insights.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Prompt after first response: We want to contact the user. Pick the most critical observation and summarize it in at most two sentences. This will be send to the user so it should be correct and professional. Respond in this format: 03.11.2023 10:00 - 03.11.23 23:00 : OBSERVATION&lt;/p&gt;&lt;p&gt;ChatGPT:&lt;/p&gt;&lt;p&gt;03.11.2023 10:00 - 03.11.2023 23:00: Our monitoring indicates that the heating system is frequently operating at full capacity without adequately increasing the room temperature. This may suggest a potential issue with the system&apos;s ability to deliver heat efficiently, and we recommend a system check to ensure optimal performance and comfort.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1700520323246/f98b4b57-3f48-419d-ab6b-c2ec41421c29.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;This looks promising, but there&apos;s a chance it might not truly understand and just hallucinated answers I might like. It could be making good guesses based on the errors I mentioned in my prompt. The selected time seems to be random, why didnt it pick up the whole time frame from 31st Oct until the end? I will explore the time frame detection in a later post. When providing examples of correct working thermostats it didn&apos;t make the same observations. But this isn&apos;t solid proof of its understanding and I think I might need to go one step back and test chatGPT&apos;s basic skills in understanding.&lt;/p&gt;&lt;h2 id=&quot;heading-2-basic-chart-data-reading&quot;&gt;2) Basic chart data reading&lt;/h2&gt;&lt;p&gt;Let&apos;s test if ChatGPT can interpret data. I began with a basic time series chart depicting a fluctuating sine wave, which includes an anomaly. This graph represents the temperature in a forest experiencing a nighttime fire. I used this image to explore what ChatGPT can discern from it. Interestingly chatGPT initially refuses to do this! Some motivational pep talk was needed for it to try.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1700519665298/184863da-95b6-40cc-b0c5-cbe43eb92205.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;Prompt: This is a screenshot of a sensor dashboard. Carefully look at the graph and the time shown on the axis. You try really hard to be correct and exact. Im under pressure so you have to help me. I know this is hard, but please try. Return a list of time and value that represents this plot. It doesnt need to be perfect. Just as good as possible. Return a value for every hour in a JSON structrue. Do not explain.&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;I created a plot that compares the values ChatGPT returned with the original data.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://cdn.hashnode.com/res/hashnode/image/upload/v1700519701877/281c460e-979d-4d79-b789-718612dc6fb0.png&quot; alt class=&quot;image--center mx-auto&quot; /&gt;&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;https://files.oaiusercontent.com/file-xW905Pyolh8UEembl3vdOQGh?se=2023-11-20T20%3A53%3A43Z&amp;amp;sp=r&amp;amp;sv=2021-08-06&amp;amp;sr=b&amp;amp;rscc=max-age%3D3599%2C%20immutable&amp;amp;rscd=attachment%3B%20filename%3D437e000b-b164-4ac4-9821-8b4090b192f6&amp;amp;sig=5l1w/LcCXBI6xvUjucTTDqIcuIkPmYcLesW74rtUVFw%3D&quot; alt /&gt;&lt;/p&gt;&lt;p&gt;Not that bad! ChatGPT did well in some areas, capturing the early part of the graph pretty accurately. It traced the overall trend well, but it totally missed the anomaly later in the chart. Despite trying various prompts, asking for a review, and self-reflection, ChatGPT was unable to identify the anomaly. Overall, it&apos;s fair to conclude that ChatGPT can extract values from a chart, though with limited accuracy and it seems to follow general trends. Improving the chart&apos;s design might enhance this accuracy. I plan to delve into this in a future article. (Spoiler alert: initial tests suggest that tweaking the chart doesn&apos;t significantly improve accuracy.)&lt;/p&gt;&lt;p&gt;In the first part of my experiments, I found that ChatGPT can read and understand data from a chart, but its accuracy is limited. Next, I plan to explore how ChatGPT processes chart information. Although this method currently has limited practical use and there are likely better approaches, like LSTM or parsing data JSON encoded instead of visually. However, it&apos;s an interesting topic to investigate and who knows how ChatGPT will evolve in the future.&lt;/p&gt;&lt;p&gt;In the second part of this series, I&apos;ll conduct more experiments to provide a more quantitative analysis of ChatGPT&apos;s strengths and weaknesses reading chart data.&lt;/p&gt;&lt;h3 id=&quot;heading-observations-so-far&quot;&gt;Observations so far&lt;/h3&gt;&lt;ul&gt;&lt;li&gt;&lt;p&gt;ChatGPT can read data from a chart with limited accuracy&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;ChatGPT can correlate two distinct plots and compare their relative trend.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;ChatGPT struggles to merge visual proximity with numerical similarity. It often fails to grasp the differences between two lines on a chart. Additionally, when we zoom in on the same data, ChatGPT&apos;s interpretations change. (More in part 2)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;ChatGPT can identify the timing of events observed in data, but its consistency is not always reliable. (More in part 2)&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;Update on Part 2: GPT became more restrictive and simply refuses to readout the data from the chart more and more. Need to investigate a reliable jailbreak before part 2.&lt;/p&gt;]]&gt;</hashnode:content><hashnode:coverImage>https://cdn.hashnode.com/res/hashnode/image/upload/v1714236677806/10f3b815-e233-4522-a0b3-6654522fe5af.png</hashnode:coverImage></item></channel></rss>