<?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"><channel><title><![CDATA[BoolCode]]></title><description><![CDATA[BoolCode]]></description><link>https://blog.binyamin.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1712671816541/46ccdc6b-abda-4c73-8e2f-5f68f6587507.png</url><title>BoolCode</title><link>https://blog.binyamin.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 12 Oct 2026 05:47:48 GMT</lastBuildDate><atom:link href="https://blog.binyamin.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Your own container registry]]></title><description><![CDATA[In this article we'll learn how to create our own container registry, that will be functions as a private container registry and a Docker Hub mirror altogether.
Why?
When using a containerized-first architecture, two issue may arise.

The need of hav...]]></description><link>https://blog.binyamin.dev/your-own-container-registry</link><guid isPermaLink="true">https://blog.binyamin.dev/your-own-container-registry</guid><category><![CDATA[Docker]]></category><category><![CDATA[docker-registry]]></category><category><![CDATA[containers]]></category><category><![CDATA[containerization]]></category><dc:creator><![CDATA[Binyamin Yawitz]]></dc:creator><pubDate>Tue, 14 May 2024 02:06:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715652337887/9316f901-27af-4da1-9534-ba705818a8d9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article we'll learn how to create our own container registry, that will be functions as a private container registry and a Docker Hub mirror altogether.</p>
<h3 id="heading-why">Why?</h3>
<p>When using a containerized-first architecture, two issue may arise.</p>
<ol>
<li><p>The need of having private repositories AKA private images.</p>
</li>
<li><p>The need of having fast image fetching for multiple servers inside a local network, even for public available images.</p>
</li>
</ol>
<p>To solve the first problem we need to have a private registry, and to solve the second we need to have a registry to mirror the public registry currently in use.</p>
<h3 id="heading-what">What?</h3>
<p>The good old Docker Hub registry is part the CNCF, and is publicly available under the name <a target="_blank" href="https://distribution.github.io/distribution/">Distribution</a>.</p>
<p>As of now Distribution supports one of two type of deployments, either as a mirror (proxy) or as a private registry, as <a target="_blank" href="https://distribution.github.io/distribution/about/configuration/#proxy">noted</a> in the official website</p>
<blockquote>
<p>Pushing to a registry configured as a pull-through cache is <strong>unsupported</strong></p>
</blockquote>
<p>But, we can find a way around it.</p>
<h3 id="heading-using-njs">Using NJS</h3>
<p>Nginx is one of the most popular revers-proxy out there offering resilient, reliable and fast proxy to various type of services. One of the less-known features of Nginx is NJS or Ngnix JavaScript.</p>
<p>NJS is integrated inside Nginx and can be enabled with one line. After enabling NJS, Nginx abilities are extended to the full power of programming-driven logic, you can see a lot of NJS examples <a target="_blank" href="https://github.com/nginx/njs-examples">here</a>.</p>
<p>We're going to utilize NJS for enabling two Distributions deployment act as one, offering both mirroring option and private registry option.</p>
<p>Inside the JS code we're going to check whether the request method is <code>GET</code> for mirroring and existing images, or any other method for pushing images into our private registry.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715647793924/24069771-3153-412a-b013-8b4de3acfb9a.png?width=500" alt class="image--center mx-auto" /></p>
<p>As we can see in the diagram the deployment is composed of a Nginx service standing as the entry-point to our infrastructure, separating the requests to two different Distribution deployments based on the request method.</p>
<p>Both of the Distribution services are mounting the same storage as their repo volume storage.</p>
<h3 id="heading-how">How?</h3>
<p>We'll deploy our registry using Docker Swarm using three Docker Swarm managers - with no workers as it's not required - behind load balancer, like such:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714143856350/03b0853b-8efc-4b3a-9a02-30974ef0899d.png?width=500" alt class="image--center mx-auto" /></p>
<p>Each one of the Docker Swarm node manager need to have access to the same Storage volume. For that, we can utilize either <code>nfs</code> or any S3 compatible service.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715648263389/0d1f81fd-e05b-4d73-822c-e5bc1bce3a41.png?width=500" alt class="image--center mx-auto" /></p>
<p>Our <code>nginx.conf</code> is pretty short</p>
<pre><code class="lang-nginx"><span class="hljs-attribute">upstream</span> mirror {
  <span class="hljs-attribute">server</span> registry-mirror:<span class="hljs-number">5000</span>;
}

<span class="hljs-attribute">upstream</span> private {
  <span class="hljs-attribute">server</span> registry-private:<span class="hljs-number">5000</span>;
}


<span class="hljs-attribute">js_import</span> mirror from conf.d/mirror.js;
<span class="hljs-attribute">js_set</span> <span class="hljs-variable">$mirror</span> mirror.set;

<span class="hljs-section">server</span> {
    <span class="hljs-attribute">if</span> (<span class="hljs-variable">$http_user_agent</span> <span class="hljs-regexp">~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go</span> ).*$<span class="hljs-string">" )  {
              return 404;
          }
  client_max_body_size 2G;
  listen 8080;

  location / {
    proxy_pass        http://<span class="hljs-variable">$mirror</span>;
    proxy_set_header  Host              <span class="hljs-variable">$http_host</span>;
    proxy_set_header  X-Real-IP         <span class="hljs-variable">$remote_addr</span>;
    proxy_set_header  X-Forwarded-For   <span class="hljs-variable">$proxy_add_x_forwarded_for</span>;
    proxy_set_header  X-Forwarded-Proto <span class="hljs-variable">$scheme</span>;
    proxy_read_timeout                  900;
  }
}</span>
</code></pre>
<p>We're setting two upstreams. One for the mirror, and one for the private one. the decision to which one we'll route the traffic is done using NJS as we can see in these two lines</p>
<pre><code class="lang-nginx"><span class="hljs-attribute">js_import</span> mirror from conf.d/mirror.js;
<span class="hljs-attribute">js_set</span> <span class="hljs-variable">$mirror</span> mirror.set;
</code></pre>
<p>The first line loads the file <code>conf.d/mirror.js</code> into an object named <code>mirror</code>, then it sets the value into a variable named <code>$mirror</code> using the <code>mirror.set</code> function. When the value is setup is being used as the proxy_pass value.</p>
<pre><code class="lang-nginx"><span class="hljs-attribute">proxy_pass</span>        http://<span class="hljs-variable">$mirror</span>;
</code></pre>
<p>Last thing left to investigate is the code <code>mirror.js</code> itself.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {set}

<span class="hljs-keyword">const</span> mirror      = <span class="hljs-string">'mirror'</span>
<span class="hljs-keyword">const</span> privateRepo = <span class="hljs-string">'private'</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">set</span>(<span class="hljs-params">r</span>) </span>{
    <span class="hljs-keyword">let</span> c = mirror;

    <span class="hljs-keyword">if</span> (r.method !== <span class="hljs-string">'GET'</span>)
        c = privateRepo;

    <span class="hljs-keyword">return</span> c;
}
</code></pre>
<p>Here we're exporting the function set, that checks to see if the request method is not <code>GET</code> and if so it will set the upstream to the private upstream.</p>
<p>To deploy our custom-registry we need to do the following steps:</p>
<ol>
<li><p>Create a Docker Swarm <strong>leader</strong></p>
</li>
<li><p>Join another 2 nodes as <strong>leaders</strong> as well</p>
</li>
<li><p>[in case of <code>nfs</code>, attach the <code>nfs</code> storage to the leaders]</p>
</li>
<li><p>deploy the stack using the <code>docker-compose.yml</code> file into the Swarm.</p>
</li>
</ol>
<p>Prior to all servers setup make sure you're placing all serves in the same local network using VPC.</p>
<h4 id="heading-create-a-docker-swarm-leader">Create a Docker Swarm leader</h4>
<p>Setup a Ubuntu server, and install Docker by running the following command as a root user.</p>
<pre><code class="lang-bash">curl -fsSL https://get.docker.com | sh
</code></pre>
<p>Then, init the swarm by running</p>
<pre><code class="lang-bash">docker swarm init
</code></pre>
<p>That's it you've a working docker swarm.</p>
<p>Run the following command to get the command for joining another <code>managers</code> to the Swarm.</p>
<pre><code class="lang-bash">docker swarm token join-token manager
<span class="hljs-comment"># docker swarm join --token Very-Long-Token 10.0.16.230:2377</span>
</code></pre>
<h4 id="heading-join-another-2-nodes">Join another 2 nodes</h4>
<p>Setup another 2 Ubuntu servers and install Docker in them, then run <code>join</code> command from above</p>
<pre><code class="lang-bash">docker swarm join --token Very-Long-Token 10.0.16.230:2377
</code></pre>
<p>Now, run this command from anyone of the managers to see all attached nodes (machines).</p>
<pre><code class="lang-bash">docker node ls
</code></pre>
<h4 id="heading-attach-storage-or-set-s3-credentials">Attach storage or set S3 credentials</h4>
<p>Now it's time to attach the storage, we can use one of two options (other options are of course available, but we'll focus on those two) nfs="network file system" or S3 compatible storage. We'll R2 which is S3 compatible storage provided by Cloudflare with free egress (bandwidth) usages</p>
<p>Clone the lab repo and <code>cd</code> into the lab folder, then choose either nfs or r2.</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/byawitz/labs.git
<span class="hljs-built_in">cd</span> labs/02-your-own-cr/docker
<span class="hljs-comment"># For nfs</span>
<span class="hljs-built_in">cd</span> nfs
<span class="hljs-comment"># For r2</span>
<span class="hljs-built_in">cd</span> r2
</code></pre>
<h5 id="heading-r2">R2</h5>
<p>To set an <strong>R2</strong> edit the <code>docker-compose.yml</code> file and adjust the the S3 endpoint, replace <code>ACCOUNT_ID</code> with your Cloudflare account ID.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">REGISTRY_STORAGE_S3_REGIONENDPOINT:</span> <span class="hljs-string">https://ACCOUNT_ID.r2.cloudflarestorage.com/</span>
</code></pre>
<p>Then, create admin read/write/delete token within your Cloduflare r2 settings and paste the values into <code>files/r2-access-key.txt</code> and <code>files/r2-secret-key.txt</code> accordingly. you can move to the last step "Deploy the stack".</p>
<h5 id="heading-nfs">NFS</h5>
<p>To set an NFS storage, setup another Ubuntu server and install the nfs service by running</p>
<pre><code class="lang-bash">apt update
yes | apt install nfs-kernel-server
</code></pre>
<p>Then, create a folder and grant it will all permissions.</p>
<pre><code class="lang-bash">mkdir -p /mnt/nfs/registry
chmod 777 /mnt/nfs/registry/
</code></pre>
<p>Find your local network subnet and run the following command, replacing the <code>192.168.1.0/24</code> with your current subnet.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">echo</span> <span class="hljs-string">"/mnt/nfs/registry  192.168.2.0/24(rw,sync,no_subtree_check)"</span> &gt;&gt; /etc/exports
</code></pre>
<p>This command will append a line to the file <code>/etc/exports</code> states everyone within that subnet will have a read-write access to that folder.</p>
<p>Now run these two commands to apply the exports and restart the nfs-kernal server.</p>
<pre><code class="lang-bash">exportfs -a
service nfs-kernel-server restart
</code></pre>
<p>Optional: in case you're using firewall you can run this command to let any machine with the local subnet accessing the nfs server.</p>
<pre><code class="lang-bash">ufw allow from 192.168.2.0/24 to any port nfs
</code></pre>
<p>Now for the last step, find out the nfs server internal IP by running <code>ip a</code>, then, run the following commands on each of the Swarm servers, replace <code>192.168.2.10</code> with your nfs local IP.</p>
<pre><code class="lang-bash">apt update
yes | apt install nfs-common
mkdir -p /nfs/registry
<span class="hljs-built_in">echo</span> <span class="hljs-string">"192.168.2.10:/mnt/nfs/registry    /nfs/registry/    nfs    defaults    0 0 "</span> &gt;&gt; /etc/fstab
mount -a
</code></pre>
<p>This will</p>
<ol>
<li><p>Install the <code>nfs-common</code> that will utilize the nfs client,</p>
</li>
<li><p>Create a local folder <code>/nfs/registry/</code></p>
</li>
<li><p>Add mounting point for the nfs remote directory into our newly created one.</p>
</li>
<li><p>will mount the folder, from now on it will happen automatically during system boot.</p>
</li>
</ol>
<h4 id="heading-deploy-the-stack">Deploy the stack</h4>
<p>After setting the storage we can run the stack by running</p>
<pre><code class="lang-bash">docker stack deploy -c docker-compose.yml cri
</code></pre>
<h3 id="heading-thats-it">That's it</h3>
<p>You're custom registry is now ready and can be used as a mirror a private registry, or both. Go build something awesome!</p>
<p><a target="_blank" href="https://www.freepik.com/free-vector/hand-drawn-flat-design-container-ship_19935614.htm#fromView=search&amp;page=1&amp;position=46&amp;uuid=3e481678-588a-4125-b829-8754eddaf371">Image by freepik</a></p>
]]></content:encoded></item><item><title><![CDATA[Deploy k3s cluster on your own terms]]></title><description><![CDATA[What?
Kubernetes are the industry standard for deploying scalable applications.

Kubernetes is shortened to k8s where the 8 mark the 8 characters between the k and the s

There are two general ways to use k8s:

Managed: The k8s cluster is already dep...]]></description><link>https://blog.binyamin.dev/deploy-k3s-cluster-on-your-own-terms</link><guid isPermaLink="true">https://blog.binyamin.dev/deploy-k3s-cluster-on-your-own-terms</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[k3s]]></category><category><![CDATA[k8s]]></category><category><![CDATA[self-hosted]]></category><category><![CDATA[etcd]]></category><dc:creator><![CDATA[Binyamin Yawitz]]></dc:creator><pubDate>Sun, 21 Apr 2024 21:24:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1713734750192/917efc57-83b1-4e0f-837d-8729c1c51be8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-what">What?</h3>
<p>Kubernetes are the industry standard for deploying scalable applications.</p>
<blockquote>
<p>Kubernetes is shortened to <code>k8s</code> where the <code>8</code> mark the 8 characters between the <code>k</code> and the <code>s</code></p>
</blockquote>
<p>There are two general ways to use k8s:</p>
<ol>
<li><p><strong>Managed:</strong> The k8s cluster is already deployed for you, and you only need to manage the applications life-cycle. Offered by many companies including Amazon, Azure, Google, DigitalOcean, Linode and much more. You can find the whole official list in <a target="_blank" href="https://landscape.cncf.io/?group=certified-partners-and-providers">the CNCF landscape</a> in the <strong>Certified Kubernetes - Hosted</strong> section.</p>
</li>
<li><p><strong>Self-Managed:</strong> In this way you'll also need to deploy and manage the cluster yourself. For that you have a variety of distributions to choose from which can be found on the same <a target="_blank" href="https://landscape.cncf.io/?group=certified-partners-and-providers">link</a> above in the <strong>Certified Kubernetes - Distribution</strong> section.</p>
</li>
</ol>
<p><em>Application developers (<strong><strong>CKAD</strong></strong>) mostly go with a</em> <strong><em>Managed</em></strong> <em>solution, while Kubernetes Administrator (<strong><strong>CKA</strong></strong>) will go with last one, check</em> <a target="_blank" href="https://kubernetes.io/training/"><em>here</em></a> <em>for more details about k8s certificates.</em></p>
<p>k3s is one of the official k8s distributions that meant to be a light weight version of k8s.</p>
<blockquote>
<p>Due to its light-weight nature k3s was named like such as it meant to be <strong>half</strong> of k8s.</p>
<p>k8s has 10 characters so k3s is half, 5 characters and as such the 3 marked the middle 3 characters.</p>
<p>There's no long abbreviation for k3s.</p>
</blockquote>
<h3 id="heading-why">Why?</h3>
<p>k8s is a full-feature solution, meant to cover <strong>all</strong> use-cases. As of such k8s is a complex product, and the task of deploying k8s cluster makes it complex as well.</p>
<p>k3s is a much more lighter version, and deploying a basic k3s cluster takes less then a minute. Even complex deployment of k3s don't take more the 30-90 minutes.</p>
<p>k3s achieves that by providing light, stripped-out version of k8s that lack many of the large-scale features. Because of that you can have a single-node cluster of k3s. And, due to it's lightweight nature it will consume small amount of resources while letting you deploy apps in k8s style.</p>
<p>Although k3s lack some k8s features, it's a CNCF certified distribution of k8s, meaning you can use your <code>yaml</code> file against k3s and it will work!</p>
<p>One of the biggest advantages of starting using k3s in your environments is the that you're getting use to deploy application the <strong>k8s way.</strong> This way when changing to k8s, managed or self-managed, the only thing that changes is the cluster engine, and not the way you're interacting with it.</p>
<h3 id="heading-k3s-vs-k8s">k3s vs k8s</h3>
<p>The main difference of "<strong>k3s is lighter the k8s"</strong> doesn't give us that much, will try to break the differences a bit more.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Feature</td><td>k3s</td><td>k8s</td></tr>
</thead>
<tbody>
<tr>
<td>Cloud specific</td><td>None</td><td>Included</td></tr>
<tr>
<td>Security</td><td>Basic</td><td>Advance</td></tr>
<tr>
<td>Storage driver</td><td>Limited list</td><td>All of them</td></tr>
<tr>
<td>Auto scaling</td><td>Not available</td><td>Available</td></tr>
<tr>
<td>Processes</td><td>Single binary</td><td>Multiple binaries</td></tr>
<tr>
<td>Cluster info Driver</td><td>Sqlite, etcd, external SQL</td><td>etcd, external SQL</td></tr>
</tbody>
</table>
</div><p>These features makes <code>k3s</code> less robust then <code>k8s</code>, but much more suitable for other user cases as:</p>
<ul>
<li><p>Edge computation</p>
</li>
<li><p>Home labs</p>
</li>
<li><p>Internal k8s clusters</p>
</li>
<li><p>Starting point to get use to <code>k8s</code></p>
</li>
<li><p>Any scale of application that <strong>not uses</strong> k8s as of today</p>
</li>
</ul>
<p>The last bullet-point is important, and why? leaving managed containers as ECS outside, most of today application will be deployed in one of the following approaches:</p>
<ul>
<li><p><strong>Traditional way</strong>: which you're installing and maintain the require infrastructure as the need grows. For example, when you suddnley have a need for NodeJS in your environment, you just install it.</p>
</li>
<li><p><strong>Docker &amp; Docker compose way</strong>: a single server with container management.</p>
</li>
<li><p><strong>Docker Swarm</strong>: Docker orchestration tools.</p>
</li>
</ul>
<p>The first approach is hard to maintain, while the second don't scale (easily at least) which leaves us with Docker Swarm.</p>
<p>Docker Swarm is a good approach for HA and FT applications, though in some point as your applications grows you'll find yourself in a need of k8s.</p>
<p>k3s will provide you deployment options that goes from a single server up to the HA cluster that shares the capabilities of Docker Swarm. Meaning, you can have the same benefits as you have today and starting to shift towards k8s-style deployments.</p>
<h2 id="heading-deploying">Deploying</h2>
<h3 id="heading-architecture">Architecture</h3>
<p>k3s can be deployed on a bare metal, VPS and even raspberry pie, in one of two ways as a <strong>k3s Server</strong> or as a <strong>k3s Agent.</strong></p>
<p><img src="https://docs.k3s.io/assets/images/how-it-works-k3s-revised-9c025ef482404bca2e53a89a0ba7a3c5.svg" alt /></p>
<p><em>Image form k3s</em> <a target="_blank" href="https://docs.k3s.io/architecture"><em>documentations</em></a><em>.</em></p>
<p>k3s <strong>server</strong> includes additional tools for managing the cluster, while k3s <strong>agent</strong> includes only the necessary tools needed for running pods, services and other k8s resources, and a way to communicate with the k3s server(s), the k3s supervisor.</p>
<p>As can be seeing in the architecture, k3s always uses just one-binary to handle all the is components and tools.</p>
<p>By default k3s servers will be used as an agent for when deploying resources. We can block such behavior by tainting the k3s server nodes.</p>
<blockquote>
<p>Tainting in k8s are a way to declare which resources can be deployed in a given node by declaring key value pair, for example:</p>
<pre><code class="lang-bash">kubectl taint nodes k3s-server1 taint=server:NoSchedule
</code></pre>
<p>This command will set the <code>taint=server</code> key-value pair for the <code>k3s-server</code> node. and any resource that needed to be able to deployed on that node will need to add <strong>toleration</strong> in their declaration for the key-value pair, for example</p>
<pre><code class="lang-yaml"><span class="hljs-attr">tolerations:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">key:</span> <span class="hljs-string">"taint"</span>
  <span class="hljs-attr">operator:</span> <span class="hljs-string">"Equal"</span>
  <span class="hljs-attr">value:</span> <span class="hljs-string">"server"</span>
  <span class="hljs-attr">effect:</span> <span class="hljs-string">"NoSchedule"</span>
</code></pre>
<p>The <code>NoSchedule</code> effect declares that this taint rule are from now on and don't apply for running resources.</p>
</blockquote>
<p>By default <code>k3s</code> comes with <code>traefik</code> as a built-in ingress controller. Make sure to target this ingress class in your <code>yaml</code> files. This feature - as almost everything in <code>k3s</code> - can be disabled.</p>
<p>We will cover the following deployments.</p>
<ul>
<li><p><strong>One</strong> k3s server</p>
</li>
<li><p><strong>Three</strong> k3s servers for high availability</p>
</li>
<li><p><strong>One</strong> k3s server and <strong>three</strong> k3s agents.</p>
</li>
<li><p><strong>One</strong> k3s server with <em>taint</em> and <strong>three</strong> k3s agents.</p>
</li>
<li><p><strong>Three</strong> k3s server with <em>taint</em> and <strong>three</strong> k3s agents.</p>
</li>
</ul>
<h3 id="heading-one-server"><strong>One Server</strong></h3>
<p>Will start with the basics.</p>
<p>In this example will deploy a single-node k3s cluster.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713733829422/041e3844-4b9d-4d4a-98f4-99b7e430f00e.png" alt class="image--center mx-auto" /></p>
<p>We can see that in this deployment both the end-users and the cluster-managers that uses the <code>kubectl</code> will accesse the cluster the same way.</p>
<p>To deploy the cluster, ssh into your server and run:</p>
<pre><code class="lang-bash">curl -sfL https://get.k3s.io | sh -s - server
</code></pre>
<p>The command will fetch the content of <code>https://get.k3s.io</code> and will execute using <code>sh</code>. That's it, you now have a fully working <code>k3s</code> cluster.</p>
<p>You can get your config file from <code>/etc/rancher/k3s/config.yaml</code> and copy it to your local machine to use it with <code>kubectl</code>.</p>
<h3 id="heading-three-servers"><strong>Three Servers</strong></h3>
<p>In this deployment we will achieve high-availability by deploying 3 servers into the cluster.</p>
<p>We're not deploying any agents yet as the servers are not taint and can be used as agents too.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713733886933/58f1eec8-7eb0-45cb-a956-473cf6849a9d.png" alt class="image--center mx-auto" /></p>
<p>In this deployment we will put all the servers behind load-balancer.</p>
<p>In is default settings k3s uses <code>sqlite</code> as is data-store, that won't work anymore as our cluster is now distributed. For distributed clusters k3s supports either using distributed etcd or using an external SQL db, will go with distributed etcd.</p>
<blockquote>
<p>When using etcd the servers IP must be a local one, meaning all servers must share VPC and placed in the region. for cross-region cluster deployment you'll to need use the external db option.</p>
</blockquote>
<p>ssh into the first server and run</p>
<pre><code class="lang-bash">curl -sfL https://get.k3s.io | sh -s - server --cluster-init
</code></pre>
<p>Using the <code>--cluster-init</code> flag will install k3s with etcd. <em>(you can use this command also to convert existing single-node k3s from using sqlite to use etcd).</em></p>
<p>Now we need to have the k3s token. The token location is <code>/var/lib/rancher/k3s/server/token</code> and we can get as such,</p>
<pre><code class="lang-bash">$ cat /var/lib/rancher/k3s/server/token
K107df354fe86b....::server:THIS_IS_THE_TOKEN
</code></pre>
<p>The last part after the <code>::server:</code> is the token, we will use that token and the existing server internal IP to join other servers as such:</p>
<pre><code class="lang-bash">  curl -sfL https://get.k3s.io | sh -s - server \
    --server https://10.0.0.3:6443 \
    --token <span class="hljs-string">"THIS_IS_THE_TOKEN"</span>
</code></pre>
<p>Now you can check all the nodes in the cluster as such:</p>
<pre><code class="lang-bash">:~<span class="hljs-comment"># kubectl get nodes</span>
NAME    STATUS   ROLES                       AGE     VERSION
k3s-1   Ready    control-plane,etcd,master   2m18s   v1.29.3+k3s1
k3s-3   Ready    control-plane,etcd,master   96s     v1.29.3+k3s1
k3s-2   Ready    control-plane,etcd,master   2m52s   v1.29.3+k3s1
</code></pre>
<p>You can see that each of the nodes has the <code>control-plane</code> and <code>etcd</code> roles.<br />To access the cluster run your <code>kubectl</code> against the load balancer IP.</p>
<h3 id="heading-one-server-three-agents"><strong>One Server three Agents</strong></h3>
<p>In this deployment we will deploy one server node using the default sqlite data-store and will connect 3 agents.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713733971057/f9a94348-c99f-4033-9278-57b330697524.png" alt class="image--center mx-auto" /></p>
<p>Because we haven't taint the server node it will be included in the load balancer target for the end-users.</p>
<p>In the server node will run</p>
<pre><code class="lang-bash">curl -sfL https://get.k3s.io | sh -s - server
</code></pre>
<p>That will create a single-node k3s cluster, now we can connect agents using the the server token and IP, as such:</p>
<pre><code class="lang-bash">  curl -sfL https://get.k3s.io | sh -s - agent \
    --server https://10.0.0.3:6443 \
    --token <span class="hljs-string">"THIS_IS_THE_TOKEN"</span>
</code></pre>
<p>Run this command in the server or in your local machine using <code>kubectl</code></p>
<pre><code class="lang-bash">:~<span class="hljs-comment"># kubectl get nodes</span>
NAME    STATUS   ROLES                  AGE     VERSION
k3s-1   Ready    control-plane,master   3m22s   v1.29.3+k3s1
k3s-3   Ready    &lt;none&gt;                 18s     v1.29.3+k3s1
k3s-2   Ready    &lt;none&gt;                 11s     v1.29.3+k3s1
k3s-4   Ready    &lt;none&gt;                 57s     v1.29.3+k3s1
</code></pre>
<h3 id="heading-one-tainted-server-three-agents"><strong>One Tainted Server three Agents</strong></h3>
<p>This deployment is the same as the one above, the only different is that we are going to taint the server node.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713734003187/198ce0a3-8102-43ec-97d1-b9db4b5d2140.png" alt class="image--center mx-auto" /></p>
<p>As you can see the load balancer is not pointing to server node anymore.</p>
<p>Run this command to taint the server node. (replace <code>k3s-1</code> with your server node name.</p>
<pre><code class="lang-bash">kubectl taint nodes k3s-1 manager=only:NoExecute
</code></pre>
<p>Now we can check the node taints as such:</p>
<pre><code class="lang-bash"><span class="hljs-variable">$#</span>: kubectl describe nodes k3s-1 | grep Taint
Taints:             manager=only:NoExecute
</code></pre>
<p>This will prevent placing any resources on the server node.</p>
<p><em>The</em><code>NoExecute</code><em>effect means that current services will get evicted from the node.</em></p>
<h3 id="heading-three-servers-tainted-three-agents">Three Servers (tainted) three Agents</h3>
<p>This would be the most complex deployment of them all.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713734067456/c64fbbf7-1cb4-4fb1-87a2-50eea6fd07dc.png" alt class="image--center mx-auto" /></p>
<p>In this deployment will have high-availability for the server nodes which are all going to be tainted. All of the server nodes will be behind a load-balancer which will be used by the agent nodes and the cluster manager.</p>
<p>First we run this to deploy single-node k3s cluster with etcd enabled.</p>
<pre><code class="lang-bash">curl -sfL https://get.k3s.io | sh -s - server --cluster-init
</code></pre>
<blockquote>
<p>In Hetzner for example you can create a server without external IP whatsoever which would be very useful for the server nodes.</p>
</blockquote>
<p>Now, we run this on the other servers.</p>
<pre><code class="lang-bash">  curl -sfL https://get.k3s.io | sh -s - server \
    --server https://10.0.0.2:6443 \
    --token <span class="hljs-string">"THIS_IS_THE_TOKEN"</span>
</code></pre>
<p>Now we can taint all of our servers by running</p>
<pre><code class="lang-bash">kubectl taint nodes k3s-server-1 k3s-server-2 k3s-server-3 manager=only:NoExecute
</code></pre>
<p>And we can add agents using the servers load balancer IP address</p>
<pre><code class="lang-bash">  curl -sfL https://get.k3s.io | sh -s - agent \
    --server https://10.0.0.6:6443 \
    --token <span class="hljs-string">"THIS_IS_THE_TOKEN"</span>
</code></pre>
<p>Now we just need to add another load balancer in-front of the agents.</p>
<h3 id="heading-tips-to-go">Tips to go</h3>
<p>To uninstall k3s from your server you'll have a ready to use file inside <code>/usr/local/bin</code></p>
<pre><code class="lang-bash"><span class="hljs-comment"># For server nodes</span>
/usr/<span class="hljs-built_in">local</span>/bin/k3s-uninstall.sh

<span class="hljs-comment"># For agent nodes</span>
/usr/<span class="hljs-built_in">local</span>/bin/k3s-agent-uninstall.sh
</code></pre>
<p>It's recommend to first <strong>delete</strong> the node before uninstalling it, for example</p>
<pre><code class="lang-bash"><span class="hljs-comment"># In a server node</span>
<span class="hljs-variable">$server</span>: kubectl delete nodes agent-12

<span class="hljs-comment"># In the agent node</span>
<span class="hljs-variable">$agent</span>-12: /usr/<span class="hljs-built_in">local</span>/bin/k3s-agent-uninstall.sh
</code></pre>
<p>To have a better grasp on your cluster practice how:</p>
<ul>
<li><p>Add servers and agents.</p>
</li>
<li><p>Set alarms for when a node is down.</p>
</li>
<li><p>Backup and restore the etcd or sqlite cluster database.</p>
</li>
</ul>
<p>Finally, use a firewall in-front of your cluster, specially in front of your server nodes.</p>
<p><a target="_blank" href="https://www.freepik.com/free-vector/paper-style-white-monochrome-background_15518074.htm#fromView=search&amp;page=1&amp;position=19&amp;uuid=0e24c847-3582-4206-b050-6d20499e7fd6">Image by pikisuperstar on Freepik</a></p>
]]></content:encoded></item><item><title><![CDATA[N+B+K+V+G+C = Self-Hosted Analytics]]></title><description><![CDATA[In this article will discover a event-driven-stream-based way to create analytics solutions like Google Analytics, Plausible, Aptabase and other using only existing tools.
We'll use the following tools for creating a self-hosted analytics system.



...]]></description><link>https://blog.binyamin.dev/self-hosted-analytics</link><guid isPermaLink="true">https://blog.binyamin.dev/self-hosted-analytics</guid><category><![CDATA[nginx]]></category><category><![CDATA[benthos]]></category><category><![CDATA[vector.dev]]></category><category><![CDATA[kafka]]></category><category><![CDATA[analytics]]></category><category><![CDATA[Grafana]]></category><category><![CDATA[ClickHouse]]></category><category><![CDATA[self-hosted]]></category><dc:creator><![CDATA[Binyamin Yawitz]]></dc:creator><pubDate>Thu, 18 Apr 2024 03:09:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1713404448474/2fb057b6-afcd-4b1d-a159-e284595ec19d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article will discover a event-driven-stream-based way to create analytics solutions like Google Analytics, <a target="_blank" href="https://plausible.io/">Plausible</a>, <a target="_blank" href="https://aptabase.com/">Aptabase</a> and other using only existing tools.</p>
<p>We'll use the following tools for creating a self-hosted analytics system.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Tool</td><td>Purpose</td></tr>
</thead>
<tbody>
<tr>
<td><a target="_blank" href="https://nginx.org/">Nginx</a></td><td><strong>Revers proxy</strong>: the main endpoint, and rate limiter</td></tr>
<tr>
<td><a target="_blank" href="https://www.benthos.dev/">Benthos</a></td><td><strong>Stream processor</strong>: gather analytics events</td></tr>
<tr>
<td><a target="_blank" href="https://kafka.apache.org/">Kafka</a></td><td><strong>Event store</strong>: stores all events</td></tr>
<tr>
<td><a target="_blank" href="https://vector.dev/">Vector</a></td><td><strong>Log aggregator</strong>: aggregate the logs to put into the OLAP database</td></tr>
<tr>
<td><a target="_blank" href="https://grafana.com/">Grafana</a></td><td><strong>Dashboard</strong>: view all the data in real-time and history browsing</td></tr>
<tr>
<td><a target="_blank" href="https://clickhouse.com/">Clickhouse</a></td><td><strong>OLAP database</strong>: long-term storage for the events</td></tr>
</tbody>
</table>
</div><div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can find the <code>docker-compose.yml</code> , the <code>terraform</code> files and more <a target="_blank" href="https://github.com/byawitz/labs/tree/main/01-your-own-analytics">here</a>.</div>
</div>

<p>The requirements for this article are to have general knowledge in:</p>
<ul>
<li><p>Docker &amp; Docker Swarm</p>
</li>
<li><p>Nginx</p>
</li>
<li><p>and, Stream processing in general.</p>
</li>
</ul>
<h2 id="heading-the-idea">The Idea</h2>
<p>When utilizing existing tools together we'll be able to have a production ready product. And, all what we need to do is to just update the underlying technologies being in used.</p>
<p>Here's the end results 👇</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713401750987/80874b14-3b5d-4e6d-a8a0-1d0f2526fb5e.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-why">Why?</h3>
<p>The main advantage of self-hosting solutions is to have <strong>control</strong> over the data.</p>
<p>The second, it's usually cheaper.</p>
<p>Last advantage, this type of production environment scaled easily, meaning, you need a wider throughput? just add more nodes containing the same services and put it all behind a load balancer.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🛑</div>
<div data-node-type="callout-text">ClickHouse and Kafka aren't stateless, but they scalable as well.</div>
</div>

<h3 id="heading-architecture">Architecture</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713409534584/fb5fd3d8-eae0-4672-a867-0f17bd226463.png" alt="Architecture diagram" class="image--center mx-auto" /></p>
<p>First, will use <code>nginx</code> as the only direct endpoint to the server. <code>nginx</code> will reverse proxy each request based on the domain to one of two inner endpoints:</p>
<ol>
<li><p><code>benthos</code> for event capturing</p>
</li>
<li><p><code>grafana</code> for accessing the analytics dashboard</p>
</li>
</ol>
<p><code>benthos</code> will have <a target="_blank" href="https://www.benthos.dev/docs/components/inputs/http_server"><code>http_server</code></a> input configured to handle incoming requests. Then, <code>benthos</code> will check to see if the message is indeed for the app, and will add some meta data, then it will deliver (produce) the data directly to a <code>kafka</code> topic, and response with <code>201</code> (<em>empty</em> <strong><em>successful</em></strong> <em>response</em>) to the user immediately.</p>
<p>The reason for that is to make sure the event capturing process will fast as possible, between 10-50ms total! sending the event to <code>kafka</code> is a good way to ensure just that.</p>
<p>On the other hand we have a <code>vector</code> container subscribed to the <code>kafak</code> topic for receiving the events. <code>vector</code> will process each event and will add them to OLAP database.</p>
<p>In the process <code>vector</code> will parse the user-agent, the user country and other event details.</p>
<p>Then, the management-user can access the <code>grafana</code> dashboard to view data in real-time or to filter by dates.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">It was possible to use <code>vector</code> or <code>bentos</code> for both parts of the streaming process. The use of two different services was for becoming familiar with both technologies.</div>
</div>

<h3 id="heading-deployment-steps">Deployment Steps</h3>
<blockquote>
<p>In the lab repo you can find <code>terraform</code> files for one-liner deployment of <code>N+B+K+V+G+C</code> stack to Hetzner and Cloudflare (for DNS).</p>
</blockquote>
<h4 id="heading-files-and-folders">Files and Folders</h4>
<p>Clone the lab <a target="_blank" href="https://github.com/byawitz/labs/tree/main/01-your-own-analytics">repo</a> and <code>cd</code> into <code>01-your-own-analytics/docker</code>. You will see these files.</p>
<ul>
<li><p><code>docker-compose.yml</code> The main architecture file</p>
</li>
<li><p><code>.env</code> Contains all necessary environment variables as Database passwords, etc.</p>
</li>
<li><p><code>nginx/analytics.conf</code> Contains nginx config file for analytics endpoint with rate-limit of 60 requests per second.</p>
</li>
<li><p><code>nginx/dashboard.conf</code> Contains nginx config file for Grafana dashboard endpoint.</p>
</li>
<li><p><code>bentohs/config.yml</code> - Benthos config file</p>
</li>
<li><p><code>vector/config.yml</code> - Vector config file</p>
</li>
<li><p><code>vector/mmdb</code> - MindMap GeoLite database.</p>
</li>
<li><p><code>grfana/dashboards/*.json</code> dashboards settings</p>
</li>
<li><p><code>grfana/provisioning/dashboards/clickhouse.yaml</code> Dashboards loading instructions.</p>
</li>
<li><p><code>grfana/provisioning/datasources/clickhouse.xml</code> ClickhHouse datasource settings.</p>
</li>
<li><p><code>scripts/install.ts</code> Installer script for creating ClickHouse database and table.</p>
</li>
<li><p>(Optional) <code>scripts/seed.ts</code> Seeding a year-worth of dummy data.</p>
</li>
</ul>
<p>We use <code>docker-compose.yml</code> file to describe the architecture for ease of use, and it would be suitable for most use-cases. in large environment you'll probably use Docker Swarm or a fully blown Kubernetes environments.</p>
<p>The project contains a <code>justfile</code> which its a simple script runner or "<code>just</code> is a handy way to save and run project-specific commands."</p>
<p>You can download the right one for your system <a target="_blank" href="https://just.systems/man/en/chapter_4.html">here</a>.</p>
<p>For local development with just you can run, <em>you'll be ask for password, this is for editing your</em> <code>hosts</code> <em>file, check inside the</em> <code>justfile</code> <em>for more details.</em></p>
<pre><code class="lang-bash">just install-local
</code></pre>
<p>To remove <strong>everything,</strong> run</p>
<pre><code class="lang-bash">just remove
</code></pre>
<p>Now, let's go through the deployment process.</p>
<h4 id="heading-setting-nginx-endpoints">Setting nginx endpoints</h4>
<p>First, we need to update the domains inside <code>nginx/analytics.conf</code> and <code>nginx/dashboard.conf</code> files. Here's the analytics configuration for example.</p>
<pre><code class="lang-nginx"><span class="hljs-attribute">limit_req_zone</span> <span class="hljs-variable">$binary_remote_addr</span> zone=base_limit:<span class="hljs-number">10m</span> rate=5r/s;

<span class="hljs-section">server</span> {
  ...
  <span class="hljs-attribute">server_name</span> analytics.test;

<span class="hljs-attribute">location</span> / {
    <span class="hljs-attribute">limit_req</span> zone=base_limit;
    ...
    <span class="hljs-attribute">proxy_pass</span> http://input-pipeline:4195;
   }
}
</code></pre>
<p>You can also notice that we are setting rate-limit for the analytics endpoint only, as we want to prevent abuse against the endpoint from which we are gathering data.</p>
<p>The rate limit settings are, 5 Requests per second for given IP.</p>
<p>As for the dashboard endpoint it's recommended putting it behind Firewall or a different type of blocker as CloudFlare <a target="_blank" href="https://www.cloudflare.com/zero-trust/">ZeroTrust</a> which is free up to 50 users.</p>
<h4 id="heading-env-file">Env file</h4>
<p>Inside the <code>.env</code> file you can set strong password and different usernames for the databases. The only field you might want to change is <code>PROJECTS</code></p>
<pre><code class="lang-yaml"><span class="hljs-string">PROJECTS=app,website</span>
</code></pre>
<p>This variable contains list of projects names. This project names will be in use when you will create events.</p>
<p><strong>Installation</strong></p>
<p>Now, we can start everything by running</p>
<pre><code class="lang-bash">docker compose up -d
</code></pre>
<h4 id="heading-creating-kafka-topic">Creating Kafka topic.</h4>
<p>Run this command to execute binary inside the <code>kafka</code> container to create Kafka topic named <code>analytics</code> with 7 days message retention, after that Kafka will delete them.</p>
<pre><code class="lang-bash">docker <span class="hljs-built_in">exec</span> event-store kafka-topics.sh \
--create --topic analytics \
--config retention.ms=604800000 \
--bootstrap-server localhost:9092
</code></pre>
<p>Now it's better to restart the <code>vector</code> container as the topic didn't exist till now.</p>
<pre><code class="lang-bash">docker compose restart vector
</code></pre>
<h4 id="heading-creating-clickhouse-table">Creating Clickhouse table</h4>
<p>Run this command to create Clickhouse table.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> $(grep -v <span class="hljs-string">'^#'</span> .env | xargs) \
&amp;&amp; docker run --rm  \
--env CLICKHOUSE_HOST --env CLICKHOUSE_USER --env CLICKHOUSE_PASSWORD \
--env CLICKHOUSE_PROTOCOL --env CLICKHOUSE_PORT --env CLICKHOUSE_DB \
--network=analytics \
-v ./scripts:/app oven/bun bun /app/install.ts
</code></pre>
<p>This command will run the <code>typescript</code> file using <code>bun</code> runtime.</p>
<h4 id="heading-optional-seed-some-demo-data">Optional - seed some demo data</h4>
<p>To see the dashboard with data while browsing you can run the following command to seed years worth of data into the database.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> $(grep -v <span class="hljs-string">'^#'</span> .env | xargs) \
&amp;&amp; docker run --rm  \
--env CLICKHOUSE_HOST --env CLICKHOUSE_USER --env CLICKHOUSE_PASSWORD \
--env CLICKHOUSE_PROTOCOL --env CLICKHOUSE_PORT --env CLICKHOUSE_DB \
--network=analytics \
-v ./scripts:/app oven/bun bun /app/seed.ts
</code></pre>
<h3 id="heading-create-your-first-events">Create your first event(s)</h3>
<p>Now we are ready to create our first event. we'll use <code>curl</code> to <code>POST</code> an event to our Benthos (through <code>nginx</code>) endpoint.</p>
<pre><code class="lang-bash">curl --location <span class="hljs-string">'http://analytics.test'</span> \
--header <span class="hljs-string">'X-App: app'</span> \
--header <span class="hljs-string">'Content-Type: application/json'</span> \
--data <span class="hljs-string">'{
    "type": "click",
    "user_id": "1",
    "session_id": "FaAa31",
    "timestamp": "2024-04-17T04:04:12",
    "locale": "en",
    "path": "/getting-started",
    "appVersion": "0.0.1",
    "details": {
        "meta_a": "meta_a",
        "meta_b": "meta_b"
    }
}'</span>
</code></pre>
<p>or JavaScript <code>fetch</code> for example</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> body = <span class="hljs-built_in">JSON</span>.stringify({
  <span class="hljs-string">"type"</span>: <span class="hljs-string">"click"</span>,
  <span class="hljs-string">"user_id"</span>: <span class="hljs-string">"1"</span>,
  <span class="hljs-string">"session_id"</span>: <span class="hljs-string">"FaAa31"</span>,
  <span class="hljs-string">"timestamp"</span>: <span class="hljs-string">"2024-04-17T04:04:12"</span>,
  <span class="hljs-string">"locale"</span>: <span class="hljs-string">"en"</span>,
  <span class="hljs-string">"path"</span>: <span class="hljs-string">"/getting-started"</span>,
  <span class="hljs-string">"appVersion"</span>: <span class="hljs-string">"0.0.1"</span>,
  <span class="hljs-string">"details"</span>: {
    <span class="hljs-string">"meta_a"</span>: <span class="hljs-string">"meta_a"</span>,
    <span class="hljs-string">"meta_b"</span>: <span class="hljs-string">"meta_b"</span>
  }
});

<span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"http://analytics.test"</span>, {
  <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
  <span class="hljs-attr">headers</span>: {
    <span class="hljs-string">"X-App"</span>: <span class="hljs-string">"app"</span>,
    <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>
  },
  body,
});
</code></pre>
<h4 id="heading-user-id-vs-session-id">User ID vs Session ID</h4>
<p>User ID is an identifier that you'll try to maintain as long as you can as this is the way you can different between users, for mobile you can use some device id like <a target="_blank" href="https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor">identifierForVendor</a> in iOS, and <a target="_blank" href="https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID">AndroidID</a> for Android. In web, you can store random unique ID inside the browser <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage">Local Storage</a>.</p>
<p>Session ID on the other hand is only for short period of time, for that you can create a random unique id combined with the current user ID and store it in memory. In the browser you can also store it inside <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage">Session Storage</a>.</p>
<h3 id="heading-view-the-data-in-grafana">View the data in Grafana</h3>
<p>Now access your Grafana. For the first time you'll to login using <code>admin</code> as username and password. Then, you'll be prompted to change the password.</p>
<p>You can also see events occurring in real-time by enabling Grafana auto load from the upper top right.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713402977360/a26da890-a179-4137-a580-4aa2306cf902.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-thats-it">That's it!</h3>
<p>ClickHouse is very! efficient, 119,716,577 rows weight total of <code>21.91GB</code>, but, on disk it's compressed and weight only <code>4.93GB</code>!</p>
<p>You can deploy a <code>N+B+K+V+G+C</code> stack on a € 16.18 server at <a target="_blank" href="https://www.hetzner.com/cloud/">Hetzner</a> and get</p>
<ul>
<li><p>4 vCPU</p>
</li>
<li><p>8GB RAM</p>
</li>
<li><p>160GB SSD</p>
</li>
<li><p>and 20TB! Bandwidth,</p>
</li>
</ul>
<p>To have that number of events you'll need to create 9,976,381 events a month, or 332,546 events a day, or 3-4 events in a given second.</p>
<p>This deployment of <code>N+B+K+V+G+C</code> its just simple example, and, you can of course adapt it to your needs.</p>
<p>🪄 Expelliarmus!</p>
<p><a target="_blank" href="https://www.freepik.com/free-vector/dark-background-with-dynamic-shapes_40165639.htm#fromView=search&amp;page=1&amp;position=14&amp;uuid=110c1d2e-10b4-478b-b10d-2d966afca958">Background Image by bunny on Freepik</a></p>
]]></content:encoded></item><item><title><![CDATA[Vector vs Benthos]]></title><description><![CDATA[In this article, we will contrast two stream-processing platforms: Vector - by Datadog versus Benthos - by Ashely.
TL;DRGenerally, Benthos is better suited for event processing, whereas Vector is more geared towards metrics and log aggregation. Howev...]]></description><link>https://blog.binyamin.dev/vector-vs-benthos</link><guid isPermaLink="true">https://blog.binyamin.dev/vector-vs-benthos</guid><category><![CDATA[vector.dev]]></category><category><![CDATA[kafka]]></category><category><![CDATA[streamprocessing]]></category><category><![CDATA[events]]></category><category><![CDATA[benthos]]></category><dc:creator><![CDATA[Binyamin Yawitz]]></dc:creator><pubDate>Sun, 14 Apr 2024 21:59:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1713130633289/9ab3ff88-44f7-4cef-a04f-6aa6e4b1c2a2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, we will contrast two stream-processing platforms: <a target="_blank" href="https://vector.dev/">Vector</a> - by Datadog versus <a target="_blank" href="https://www.benthos.dev/">Benthos</a> - by <a target="_blank" href="https://twitter.com/jeffail">Ashely</a>.</p>
<details><summary>TL;DR</summary><div data-type="detailsContent">Generally, Benthos is better suited for event processing, whereas Vector is more geared towards metrics and log aggregation. However, in many cases, both tools offer similar advantages. Benthos excels due to its ease of plugin development, while Vector is more readily usable in large-scale distributed systems.</div></details>

<h1 id="heading-stream-processing">Stream processing</h1>
<p>Will start with answer to the question <em>What is Stream processing at all?</em></p>
<h2 id="heading-event-driven-development">Event-Driven-Development</h2>
<p>To address that question, we need to take a step back and examine traditional development versus event-driven development.</p>
<p>In the traditional approach, also known as request-response development, the flow is linear and occurs synchronously. For instance, consider a transaction on an e-commerce website. The steps would be as follows:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713121837277/b9d59c60-1a12-41e3-b93a-a08dffe8a423.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>The user enters credit card details and submits the form.</p>
</li>
<li><p>The details are sent to the backend.</p>
</li>
<li><p>The backend charges the card via the payment gateway while the user waits.</p>
</li>
<li><p>After successfully charging the card, the backend updates the database.</p>
</li>
<li><p>The backend sends an invoice to the user.</p>
</li>
<li><p>The backend sends an email to the warehouse to initiate order fulfillment.</p>
</li>
<li><p>Finally, the backend sends a response indicating success.</p>
</li>
</ol>
<p>In many cases, the traditional approach suffices; however, there are instances where it can be considerably error-prone.</p>
<p>Firstly, the flow can diverge or alter at any point if one of the steps fails. For instance, suppose the payment charge and invoice sending succeed, but the warehouse notification fails. How do you track and recover from such failures?</p>
<p>Additionally, the end-user must wait throughout the entire process, whether it takes 2 seconds or 60. While this may be tolerable for many scenarios, it can pose a significant issue in others.</p>
<p>For instance, consider IoT applications where each device must report to a central base every <code>N</code> seconds. If the base's response time exceeds a few milliseconds, it can lead to a bottleneck of data, blocking other IoT device requests and compromising overall throughput.</p>
<p>To summarize, the issues with the request-response approach are:</p>
<ol>
<li><p>Lack of fault tolerance and inability to recover from failures.</p>
</li>
<li><p>Blocked processing until the entire process is completed.</p>
</li>
</ol>
<p>To address these challenges and handle a large volume of requests, you may consider adopting Event-Driven Development (EDD).</p>
<p>In EDD, the flow operates asynchronously. The key components of EDD are:</p>
<ol>
<li><p><strong>Event store</strong>: Such as <a target="_blank" href="https://kafka.apache.org/">Kafka</a> or <a target="_blank" href="https://www.rabbitmq.com/">RabitMQ</a>, where all events are stored</p>
</li>
<li><p><strong>Producer</strong>: Responsible for sending events to be stored in the event store, organized by topics</p>
</li>
<li><p><strong>Consumer</strong>: Registers in the event store to receive all events from specific topic(s).</p>
</li>
</ol>
<p>For instance, a producer might generate an event in the "function-execution" topic, containing execution data. Subsequently, consumers subscribed to the "function-execution" topic will receive the event data and process it accordingly.</p>
<p>This approach redefines the flow of the e-commerce example within an EDD framework.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713123051286/530d763e-8ff9-4551-a409-71d809f44dee.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>The user enters credit card details and submits the form.</p>
</li>
<li><p>The details are sent to the backend.</p>
</li>
<li><p>The backend produce a charge event and stores it in the event store.</p>
</li>
<li><p>The backend sends a success message from the <code>event store</code> (the user receives a response).</p>
</li>
<li><p>The <strong>charging consumer</strong> retrieves the event from the event store, charges the card, and sends the event back to the event store.</p>
</li>
<li><p>Then, the <strong>invoice consumer</strong> processes the event, sends it back, and continues the flow.</p>
</li>
<li><p>Then, the <strong>warehouse consumer</strong> processes the event and sends it back.</p>
</li>
<li><p>Then, the <strong>invoice consumer</strong> sends the event back to the end-user..</p>
</li>
</ol>
<p>In this example, Event-Driven Development (EDD) may appear more complex than the traditional approach. However, by introducing one more step, we gain several significant benefits:</p>
<ul>
<li><p><strong>Fast</strong>: Producing events takes mere milliseconds, ensuring that your throughput remains unblocked.</p>
</li>
<li><p><strong>Asynchronously</strong>: Event processing within consumers occurs in the background, reducing the processing load on the main endpoint.</p>
</li>
<li><p><strong>Fault tolerant</strong>: Most data stores retain all events on disk, making the system resilient to failures.</p>
</li>
<li><p><strong>Guaranteed delivery</strong>: Each event must be <strong>committed</strong>, ensuring that events are resent until processing is completed successfully.</p>
</li>
<li><p><strong>Extendible</strong>: Adding additional steps is straightforward; just add a new consumer.</p>
</li>
<li><p><strong>Scalable and distributed</strong>: Event stores like Kafka are distributed by default, making them easy to scale across multiple servers and consumers.</p>
</li>
</ul>
<h2 id="heading-stream-processing-1">Stream processing</h2>
<p>When it comes to handling messages from an event store like Kafka, stream processing plays a crucial role. Stream processing can be achieved using various SDKs or by utilizing a stream processor.</p>
<p>A stream processor is a pre-built solution that typically consists of three main components:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713124447165/8b2000fd-2c10-490b-b625-ff04c53b815e.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p><strong>Inputs</strong> or <strong>Sources</strong> - These are the origins from which the streams obtain their data. Examples include Kafka, AWS SQS, URL endpoints, and system metrics.</p>
</li>
<li><p><strong>Enrichment</strong> or <strong>Processors</strong> - Messages received from the inputs are processed here. Examples of processing tasks include filtering, removing sensitive data, type conversion, validation, and more.</p>
</li>
<li><p><strong>Outputs</strong> or <strong>Sinks</strong> - This is where the processed data is sent. It could be a database, an S3 bucket, or even another Kafka instance.</p>
</li>
</ol>
<h3 id="heading-why-choose-a-stream-processor-over-writing-code">Why choose a stream processor over writing code?</h3>
<p>While it's possible to achieve similar results by writing custom code, stream processors offer several advantages. One key advantage is that stream processors come with everything pre-configured, and they often cover most common use cases. This means you can get up and running quickly without needing to set up and fine-tune every aspect of your data processing pipeline.</p>
<h3 id="heading-stateless-vs-stateful-streaming">Stateless vs Stateful streaming</h3>
<p>In stream processing, there are two main approaches: stateless and stateful processing, each defining how incoming data is handled.</p>
<h4 id="heading-stateless">Stateless</h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712957277087/7446548e-8acf-49f2-8eef-0e21f6e1203f.jpeg" alt class="image--center mx-auto" /></p>
<p>In stateless processing, events are processed as they arrive without considering previous event processing. This means each event in the stream undergoes the same processing steps and follows the same pipeline.</p>
<p>Common examples of stateless streams include:</p>
<ul>
<li><p>Metrics gathering</p>
</li>
<li><p>Database replication</p>
</li>
<li><p>Log aggregation</p>
</li>
<li><p>General tasks that don't rely on knowledge of other events.</p>
</li>
</ul>
<p>ile it's possible to process data based on past events in a stateless approach, it typically requires manual modifications and isn't inherent to the process.</p>
<h4 id="heading-stateful">Stateful</h4>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712957519326/a809ba4c-9709-4344-bd69-42faddea0642.jpeg" alt class="image--center mx-auto" /></p>
<p>In the stateful approach, the processing pipeline can dynamically adjust based on previous data. Examples of stateful processing include:</p>
<ul>
<li><p>Infrastructure abuse monitoring: Requires past event data for effective monitoring, as gathering this information without historical context can be challenging.</p>
</li>
<li><p>Fraud detection</p>
</li>
<li><p>Search result optimization</p>
</li>
<li><p>Any event data that benefits from insights gained from previous events.</p>
</li>
</ul>
<p>Stateful processing is well-suited for scenarios where historical context or insights are crucial for accurate and meaningful data processing.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In both stateless and stateful approaches, the processing pipeline can be modified based on certain conditions. However, in both cases, these changes are typically not directly influenced by previous event data.</div>
</div>

<p>Both Vector and Benthos are <strong>stateless</strong> stream processors. While Vector does offer some stateful options, such as RAM-State, these are temporary and will be lost if Vector is terminated.</p>
<h3 id="heading-why-about-others">Why about others?</h3>
<p>Other popular solutions for stream-processing are:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Package</td><td>Goal</td><td>Languages</td></tr>
</thead>
<tbody>
<tr>
<td><a target="_blank" href="https://spark.apache.org/">Spark</a></td><td>Batch data processing</td><td>Java, Scala, Python</td></tr>
<tr>
<td><a target="_blank" href="https://samza.apache.org/">Smaza</a></td><td>Event processing.</td><td>Java</td></tr>
<tr>
<td><a target="_blank" href="https://storm.apache.org/">Storm</a></td><td>[Stateful] event streaming</td><td>Java, Scala, Python, Clojure, Ruby</td></tr>
<tr>
<td><a target="_blank" href="https://flink.apache.org/">Flink</a></td><td>Stateful event streaming</td><td>Java, Scala, Python</td></tr>
<tr>
<td><a target="_blank" href="https://kafka.apache.org/documentation/streams/">Kafka Streaming</a></td><td>Kafka-source-only event streaming.</td><td>Java</td></tr>
</tbody>
</table>
</div><blockquote>
<p><em>Maybe some day we'll do an article to explain why so many packages for the same thing.</em></p>
</blockquote>
<p>Each of those solution is a bit different then the others, but the main reasons to use Benthos or Vector is because they are language agnostic, both of them uses <code>yaml</code> with <code>toml</code> in Vector.</p>
<h2 id="heading-conceptual-differences">Conceptual differences</h2>
<h3 id="heading-idea">Idea</h3>
<p>The main difference between Vector and Benthos lies in their primary objectives:</p>
<p><strong>Vector</strong> aims to serve as a data/log/metrics aggregator, employing a stream-processing approach.</p>
<p><strong>Benthos</strong>, on the other hand, focuses on providing a stream-processing solution specifically for events.</p>
<p>Despite these differing goals, both solutions are capable of handling many stream processing tasks, which is why a comparison between them is relevant and useful.</p>
<h3 id="heading-lang">Lang</h3>
<p><strong>Vector</strong> is written in Rust, and <strong>Benthos</strong> is written in Go. Both are a very efficient general-purpose languages. And, even that some might claim that Rust is faster we will omit that advantage from Vector.</p>
<h2 id="heading-usage-differences">Usage differences</h2>
<p>Both Vector and Benthos provide a diverse range of inputs, processors, and outputs. To compare their differences effectively, we'll focus on components that are unique to each solution.</p>
<h3 id="heading-inputs-sources">Inputs - Sources</h3>
<p>The differences in the inputs is that Vector is more toward logs, and as of such it includes log inputs like:</p>
<ul>
<li><p>Docker logs</p>
</li>
<li><p>k8s logs</p>
</li>
<li><p>Datadog agent and more</p>
</li>
</ul>
<p>Benthos goes towards the events goal and haves inputs as:</p>
<ul>
<li><p>SQL Select (for <code>mysql</code>, <code>postgres</code>, <code>clickhouse</code>, <code>mssql</code>, <code>sqlite</code>, <code>oracle</code>, <code>snowflake</code>, <code>trino</code>,and <code>gocosmos)</code></p>
</li>
<li><p>Discord (new message)</p>
</li>
<li><p>and, Twitter search.</p>
</li>
</ul>
<p>Both of the solutions shared support for the most popular services as:</p>
<ul>
<li><p>AMQP</p>
</li>
<li><p>AWS Kinesis Firehose</p>
</li>
<li><p>AWS S3</p>
</li>
<li><p>AWS SQS</p>
</li>
<li><p>HTTP Client - Check for update</p>
</li>
<li><p>HTTP Server - Listen for update.</p>
</li>
<li><p>File</p>
</li>
<li><p>Kafka</p>
</li>
<li><p>Redis</p>
</li>
<li><p>Socket</p>
</li>
<li><p>GCP PubSub</p>
</li>
<li><p>Exec (getting output from a local binary)</p>
</li>
<li><p>stdin</p>
</li>
</ul>
<h3 id="heading-vector-remap-language-vrl-vs-bloblang">Vector Remap Language (VRL) vs Bloblang</h3>
<p>Voth VRL in Vector and Bloblang in Benthos serve as type-agnostic languages for remapping and processing various data formats like <code>JSON</code>, <code>XML</code>, or others. They allow you to transform and manipulate data without being tied to a specific data type, which is particularly useful in handling diverse event data.</p>
<p>Given this event data</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"events"</span>: [
    <span class="hljs-string">"1:file_uploaded"</span>,
    <span class="hljs-string">"2:alram_set_up"</span>,
    <span class="hljs-string">"3:file_found"</span>,
    <span class="hljs-string">"4:sytsem_shutdown"</span>
  ]
}
</code></pre>
<p>As we can see the events data contains the <code>:</code> to separate between the event ID and the event data, and the event data space uses <code>_</code>. we'll use both languages to enrich that event data. We'll start with <code>Bloblang</code></p>
<pre><code class="lang-javascript">root = <span class="hljs-built_in">this</span>.events.map_each(event -&gt; {
  <span class="hljs-string">"id"</span>: event.split(<span class="hljs-string">":"</span>).index(<span class="hljs-number">0</span>),
  <span class="hljs-string">"data"</span>: event.split(<span class="hljs-string">":"</span>).index(<span class="hljs-number">1</span>).split(<span class="hljs-string">"_"</span>).join(<span class="hljs-string">" "</span>)
})
</code></pre>
<p>This code will map each of the events to an <code>id</code> and <code>data</code> object and will put the results inside the <code>root</code> element, so will have something like such:</p>
<pre><code class="lang-json">[
  { <span class="hljs-attr">"data"</span>: <span class="hljs-string">"file uploaded"</span>, <span class="hljs-attr">"id"</span>: <span class="hljs-string">"1"</span> },
  { <span class="hljs-attr">"data"</span>: <span class="hljs-string">"alram set up"</span>, <span class="hljs-attr">"id"</span>: <span class="hljs-string">"2"</span> },
  { <span class="hljs-attr">"data"</span>: <span class="hljs-string">"file found"</span>, <span class="hljs-attr">"id"</span>: <span class="hljs-string">"3"</span> },
  { <span class="hljs-attr">"data"</span>: <span class="hljs-string">"sytsem shutdown"</span>, <span class="hljs-attr">"id"</span>: <span class="hljs-string">"4"</span> }
]
</code></pre>
<p><strong>TIP:</strong> Run <code>benthos blobl server</code> to get fully working REPL for practicing your Bloblang</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1713127232135/b2ea449b-d959-4c4b-bb1e-06c1a83ec4f9.png" alt class="image--center mx-auto" /></p>
<p>In Vector VRL is a bit different. (<em>Use</em> <a target="_blank" href="https://playground.vrl.dev/"><em>this</em></a> <em>website to practice on VRL</em>)</p>
<pre><code class="lang-javascript">e = []

for_each(array!(.events)) -&gt; |_index, value| {
    a, err = split(value,<span class="hljs-string">":"</span>)
    text, err2 = split(a[<span class="hljs-number">1</span>], <span class="hljs-string">"_"</span>)
    joined, err3 = join(text, <span class="hljs-string">" "</span>)
    e= append(e,[{<span class="hljs-string">"id"</span>:a[<span class="hljs-number">0</span>], <span class="hljs-string">"event"</span>:joined}])
}

. = e
</code></pre>
<p>The main difference is that VRL variables don't have methods so instead of <code>text.split</code> we must write <code>split(text)</code>, imo it gives <code>Bloblang</code> a small advantage.</p>
<h3 id="heading-output-sinks-differences">Output - Sinks differences</h3>
<p>In the outputs section we once more see the goal diffrences, while both of the solutions supports common outputs as:</p>
<ul>
<li><p>AWS Kinesis Data Firehose logs</p>
</li>
<li><p>AWS Kinesis Streams logs</p>
</li>
<li><p>AWS S3</p>
</li>
<li><p>AWS SNS</p>
</li>
<li><p>AWS SQS</p>
</li>
<li><p>Azure Blob Storage Console</p>
</li>
<li><p>GCP Cloud Storage</p>
</li>
<li><p>GCP PubSub</p>
</li>
<li><p>HTTP</p>
</li>
<li><p>Kafka</p>
</li>
<li><p>Redis</p>
</li>
</ul>
<p>Vector will go more toward the logs way as <code>loki</code> and Benthos towards the general data organizer as <code>sftp</code>.</p>
<h3 id="heading-scalability">Scalability</h3>
<p>Scalability is a key advantage of stateless tools like Benthos and Vector. They can scale easily by adding more instances of the tool. Running them in Docker containers is a recommended approach for ensuring smooth operation without interference.</p>
<p>For large deployments, Benthos offers features like a Kubernetes (k8s) <a target="_blank" href="https://github.com/benthosdev/benthos-captain">operator</a> and a Helm <a target="_blank" href="https://github.com/benthosdev/benthos-helm-chart">chart</a> for managing deployments efficiently. Similarly, Vector also provides a Helm <a target="_blank" href="https://github.com/vectordotdev/helm-charts">chart</a> for streamlined deployment.</p>
<p>One unique advantage of Vector is its agent-to-aggregator concept. This allows you to distribute workload among Vector instances and centralize stream processing, creating a single source of truth. For instance, each server can have a Vector agent gathering Docker logs, which are then sent to another Vector acting as an aggregator, responsible for filtering and enriching the data.</p>
<h3 id="heading-extending">Extending</h3>
<p>Want to create your own inputs, processors or outputs? for now it's possible only with Benthos. check this <a target="_blank" href="https://github.com/benthosdev/benthos-plugin-example">repo</a>.</p>
<h3 id="heading-example">Example</h3>
<p>Both of the tools can be run in the same way.</p>
<pre><code class="lang-bash"><span class="hljs-variable">$#</span> benthos -c config.yml
<span class="hljs-variable">$#</span> vector --config config.yml
</code></pre>
<p>The <code>config.yml</code> will contain all description of the pipeline process.</p>
<h4 id="heading-benthos">Benthos</h4>
<pre><code class="lang-yaml"><span class="hljs-attr">input:</span>
  <span class="hljs-attr">generate:</span>
    <span class="hljs-attr">mapping:</span> <span class="hljs-string">|
      root.id = uuid_v4()
      root.name = fake("name")
      root.email = fake("email")
</span>    <span class="hljs-attr">interval:</span> <span class="hljs-string">1s</span>

<span class="hljs-attr">pipeline:</span>
  <span class="hljs-attr">processors:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">mapping:</span> <span class="hljs-string">|
        root.id = this.id
        root.length = this.id.length()
        root.details = this.name + " - "+this.email
</span>    <span class="hljs-bullet">-</span> <span class="hljs-attr">mapping:</span> <span class="hljs-string">root</span> <span class="hljs-string">=</span> <span class="hljs-string">this.format_xml()</span>
<span class="hljs-attr">output:</span>
  <span class="hljs-attr">broker:</span>
      <span class="hljs-attr">outputs:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">stdout:</span> { }
        <span class="hljs-bullet">-</span> <span class="hljs-attr">file:</span>
            <span class="hljs-attr">path:</span> <span class="hljs-string">./a.txt</span>
            <span class="hljs-attr">codec:</span> <span class="hljs-string">lines</span>
</code></pre>
<p>In this example we are running the <code>generate</code> input every 1 second, and generating an object with fake ID, fake name and fake email.</p>
<p>Then, we use the <code>mapping</code> processor. First adjusting the field, and, then formatting the output as XML.</p>
<p>Then, the data will be output to the <code>stdout</code> and will be appended to a local file named <code>a.txt</code>.</p>
<h4 id="heading-vector">Vector</h4>
<pre><code class="lang-yaml"><span class="hljs-attr">sources:</span>
  <span class="hljs-attr">generate_syslog:</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">"demo_logs"</span>
    <span class="hljs-attr">format:</span> <span class="hljs-string">"json"</span>
    <span class="hljs-attr">count:</span> <span class="hljs-number">100</span>

<span class="hljs-attr">transforms:</span>
  <span class="hljs-attr">remap_syslog:</span>
    <span class="hljs-attr">inputs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"generate_syslog"</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">"remap"</span>
    <span class="hljs-attr">source:</span> <span class="hljs-string">|
      j = parse_json!(.message)
      date = parse_timestamp(j.datetime,format: "%v %R %:z") ?? now()
      dt = to_unix_timestamp(date) / 3600
      . = {"method":j.method, "host":j.host, "referer":j.referer,"date":to_int(floor(dt))}
</span>
  <span class="hljs-attr">remove_date:</span>
    <span class="hljs-attr">inputs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">remap_syslog</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">"remap"</span>
    <span class="hljs-attr">source:</span> <span class="hljs-string">|
      del(.date)
      .referer, err = replace(.referer, "https://", "")
      indexOfSlash = find(.referer, "/")
      .referer, err = slice(.referer, 0,indexOfSlash)
</span>

<span class="hljs-attr">sinks:</span>
  <span class="hljs-attr">emit_syslog:</span>
    <span class="hljs-attr">inputs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"remove_date"</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">"console"</span>
    <span class="hljs-attr">encoding:</span>
      <span class="hljs-attr">codec:</span> <span class="hljs-string">"json"</span>
</code></pre>
<p>In this example will generate 100 lines of JSON <code>demo_logs</code>, then, in the transformers sections we first remapping the data, then removing the <code>https</code> and suffix part of each domain.</p>
<p>At the end the data being output to the <code>console</code> using <code>json</code> codec.</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>A great solution could be mixing both tools. For example, gathering metrics using Vector and send it to Benthos and so on.</p>
<p>If you're main goal is to gather logs and insights from your infrastructure Vector would be the way to go. In any other case using Benthos or both of the tools together will be the best.</p>
<p><a target="_blank" href="https://www.freepik.com/free-vector/versus-vs-screen-with-two-focus-light-effect_9105979.htm#fromView=search&amp;page=1&amp;position=1&amp;uuid=2868d47d-d9ec-4ed2-9302-b6fcb160800f">Image by starline on Freepik</a></p>
]]></content:encoded></item><item><title><![CDATA[Database at scale - Introduction]]></title><description><![CDATA[In this series, we will explore the fundamentals of scaling databases. Here are the series' components.

Introduction - you’re here

Basic replication

Multiple masters

Sharding - Proxy

NoSQL CassandraDB/ScyllaDB



A database server is typically a...]]></description><link>https://blog.binyamin.dev/database-at-scale-introduction</link><guid isPermaLink="true">https://blog.binyamin.dev/database-at-scale-introduction</guid><category><![CDATA[Databases]]></category><category><![CDATA[high availability]]></category><category><![CDATA[sharding]]></category><dc:creator><![CDATA[Binyamin Yawitz]]></dc:creator><pubDate>Tue, 09 Apr 2024 14:01:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712672434253/153eb178-9c6f-478a-b4c6-772d5582f7ee.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this series, we will explore the fundamentals of scaling databases. Here are the series' components.</p>
<ol>
<li><p><strong>Introduction</strong> - you’re here</p>
</li>
<li><p>Basic replication</p>
</li>
<li><p>Multiple masters</p>
</li>
<li><p>Sharding - Proxy</p>
</li>
<li><p>NoSQL CassandraDB/ScyllaDB</p>
</li>
</ol>
<hr />
<p>A database server is typically a remote endpoint that listens for new read or write requests, which can be made through different methods such as REST, native libraries, and others.</p>
<p>In typical database cluster setups, there is a single source of truth, a server that contains the data and handles read and write requests.</p>
<p>To ensure high availability in database deployment, you should have a minimum of three sources of truth so one can take over in the event of a failure of the others.</p>
<h2 id="heading-terminology">Terminology</h2>
<p>When discussing databases at scale, it's essential to familiarize ourselves with some new terminology, which we’ll get to know in this part:</p>
<h3 id="heading-replication"><strong>Replication</strong></h3>
<p>Databases are central to managing data, and storing all your data in one place can be risky. This is where replication comes into play.</p>
<p>Replication serves as a secondary source of truth that maintains identical information as the original source.</p>
<h3 id="heading-standby-server"><strong>Standby Server</strong></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712670940498/8d3ea30d-2ffc-441e-9adc-d81da97d6ffc.png?width=400" alt="database standby topology" class="image--center mx-auto" /></p>
<p>Any data written to the main <strong>DB</strong> server is replicated to the <strong>Standby</strong> server either synchronously or asynchronously.</p>
<p>The Standby server remains inactive. When and if the main DB server crashes the Standby server takes over and becomes the main DB server.</p>
<p>When the DB servers come back it becomes the new Standby server.</p>
<h3 id="heading-master-slave"><strong>Master / Slave</strong></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712671054384/1dcc2f39-de2f-4991-9ff3-8fd92f9e8414.png?width=400" alt="master-slave topology" class="image--center mx-auto" /></p>
<p>A Master database server listens for incoming read and write requests, while a Slave database server listens for read requests only.</p>
<p>Any new data written to the master is replicated to the slaves using one of two methods:</p>
<ul>
<li><p><strong>Synchronously -</strong> data is committed to the master only when it can be inserted into all instances simultaneously, ensuring consistency at the expense of slower writing times.</p>
</li>
<li><p><strong>Asynchronously -</strong> data is inserted into the master on the spot, with the update happening in the background. This approach will speed up writing times but with the cost of inconsistency.</p>
</li>
</ul>
<h3 id="heading-multiple-masters"><strong>Multiple Masters</strong></h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712671073942/39cf836c-61b7-4e2f-8e3b-1058ca31e167.png?width=400" alt="master-master topology" class="image--center mx-auto" /></p>
<p>In this topology, each server also listens to write requests, unlike the previous one.</p>
<p>Any write request will be propagated to the rest of the servers synchronously or asynchronously.</p>
<p>The main advantage of this topology is that any endpoint can be used to write new data to the database, offering flexibility and convenience.</p>
<h3 id="heading-sharding"><strong>Sharding</strong></h3>
<p>All of the previously mentioned solutions provide redundancy but fail to address a crucial issue.</p>
<p>What happens if your data exceeds the capacity of one server? Even with multiple slaves or masters, each containing the same data, you can only scale your database's writing and reading capabilities, not its storage capacity.</p>
<p>To overcome this limitation, you can implement sharding.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712671085855/5719b493-ee9d-43a2-b0cd-a4dffdbc1ad8.png" alt="sharding topology" class="image--center mx-auto" /></p>
<p>When sharding, each new row is directed to a specific predetermined database server based on certain criteria.</p>
<p>In the example topology mentioned above, each database server contains identical tables with the same columns. For instance, when inserting an ID that starts with <code>az</code>, the data will be inserted into the first database server. Similarly, when reading an ID starting with <code>ar</code>, all the data will be fetched from the last database server.</p>
<h3 id="heading-proxy"><strong>Proxy</strong></h3>
<p>Except for the standby servers, you’ll need a method to access all of the servers as if they were one giant unit.</p>
<p>In master-master topology when all servers handle both requests a simple Level 4 load balancer can evenly distribute requests.</p>
<p>For master-slave and sharding topologies, you’ll need to have a proxy, You can set it programmatically using code or through a service proxy.</p>
<h4 id="heading-code-level">Code-level</h4>
<p>For example, you can encapsulate your database connector with a code wrapper that directs requests to the appropriate database server.</p>
<p>In master-slave topology, you can place all the slaves behind a Level 4 load balancer and separate the request by reading or writing, for example, this pseudo code.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> masterIP = <span class="hljs-string">'10.0.0.15'</span>;
<span class="hljs-keyword">const</span> loadBalancerIP = <span class="hljs-string">'10.0.0.60'</span>;

actionToServer(isWrite = <span class="hljs-literal">false</span>) {
  <span class="hljs-keyword">let</span> ip = loadBalancerIP;

  <span class="hljs-keyword">if</span>(isWrite) {
     ip = masterIP;
  }

  <span class="hljs-keyword">return</span> ip;
}
</code></pre>
<p>In a sharding topology, you would check the ID of the request to map it to the correct server. Here's an example:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> sharding = {
  <span class="hljs-string">"aa"</span>: <span class="hljs-string">"192.168.1.4"</span>,
  <span class="hljs-string">"ab"</span>: <span class="hljs-string">"192.168.1.5"</span>,
  <span class="hljs-string">"ac"</span>: <span class="hljs-string">"192.168.1.6"</span>,
  <span class="hljs-string">"ad"</span>: <span class="hljs-string">"192.168.1.7"</span>,
  <span class="hljs-string">"az"</span>: <span class="hljs-string">"192.168.1.8"</span>
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">shardKeyToServer</span>(<span class="hljs-params">id = 'aa'</span>) </span>{
  <span class="hljs-keyword">return</span> sharding[id];
}
</code></pre>
<h4 id="heading-service-proxy">Service-proxy</h4>
<p>Using a code-level proxy can be beneficial but may be more challenging to maintain. For this reason, a service proxy can be a useful alternative.</p>
<p>Two popular service proxies are:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/apache/shardingsphere">ShardingSphere</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/sysown/proxysql">ProxySQL</a></p>
</li>
</ul>
<h3 id="heading-mixing">Mixing</h3>
<p>It’s common to use a mix of the above topologies, for example:</p>
<ol>
<li><p>The first circle can be sharding topology.</p>
</li>
<li><p>Then, each sharding is deployed using a master-master topology.</p>
</li>
<li><p>Then, each of those masters has master-slave topology</p>
</li>
</ol>
<h2 id="heading-nosql-vs-yessql">NoSQL vs YesSQL</h2>
<p>While the approaches mentioned above require significant effort, they are typically associated with Relational Databases (YesSQL), as NoSQL databases often come with these functionalities built-in.</p>
<p>The reason is due to YesSQL's main advantage, joins.</p>
<p>In YesSQL you don’t write any data twice - at least you should avoid it - instead, you’re creating foreign keys to reference data from other tables.</p>
<p>However, a challenge arises when you scale your database to multiple servers. For instance, if you want to join data from server 1 to server 2, each request to the database will multiply, especially with complex joins.</p>
<p>To address this issue in YesSQL databases, strict sharding is essential. This involves ensuring that all inserted data, including foreign key values, resides on the same physical server. However, achieving this strict sharding setup is not always achievable.</p>
<h3 id="heading-going-forward">Going forward</h3>
<p>In the upcoming sections of the series, we will delve into the fundamentals of deploying these kinds of topologies.</p>
<p>To make all the examples easy to understand in the coming articles we’ll use <code>docker</code> and <code>docker-compose</code>.</p>
]]></content:encoded></item></channel></rss>