<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Kafka Consumer｜林鼎淵 Dean Lin</title>
    <link>https://deanlin.net/tags/kafka-consumer/</link>
    <image><url>https://deanlin.net/images/author_hu_aa256fc934e9a932.jpg</url><title>Kafka Consumer｜林鼎淵 Dean Lin</title><link>https://deanlin.net/tags/kafka-consumer/</link><width>144</width><height>144</height></image>
    <description>林鼎淵（Dean Lin）的部落格：生成式 AI 企業內訓講師、外商資安公司軟體專家、7 本書作者。400 多篇文章用白話分享生成式 AI 實戰、Claude Code 與 Vibe Coding 教學、團隊合作與職場成長。</description>
    <generator>Hugo</generator>
    <language>zh-tw</language>
    <copyright>© 2020–2026 林鼎淵 Dean Lin</copyright>
    <lastBuildDate>Sun, 20 Nov 2022 22:46:53 +0800</lastBuildDate>
    <atom:link href="https://deanlin.net/tags/kafka-consumer/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>手把手帶你建立 Node.js 專案 ＆ Kafka 環境，透過實作了解 Kafka 的運行邏輯</title>
      <link>https://deanlin.net/blog/202211/nodejs-kafka-hands-on/</link>
      <pubDate>Sun, 20 Nov 2022 22:46:53 +0800</pubDate>
      <guid>https://deanlin.net/blog/202211/nodejs-kafka-hands-on/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>本篇文章將會帶大家一步步建立 Node.js 專案 ＆ Kafka 環境，透過具體實踐來了解 Kafka 的運行邏輯。</description>
      <media:content url="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-og_hu_c30b47177bb01ebe.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>在<a href="https://deanlin.net/blog/202211/what-is-kafka/">上篇文章</a>中，筆者帶大家了解 Kafka 這門技術的使用情境、優點，以及它具體是如何解決問題的。</p>
<p>而本篇文章將會帶大家一步步建立 Node.js 專案 ＆ Kafka 環境，透過具體實踐來了解 Kafka 的運行邏輯。</p>
<p><img src="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-01.webp" width="1400" height="739" alt="手把手帶你建立 Node.js 專案 ＆ Kafka 環境，透過實作了解 Kafka 的運行邏輯（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱

ㄧ、手把手帶你建立 Node.js 專案 ＆ Kafka 環境
二、啟動專案，了解 Kafka 的運行邏輯
  ➤ 1 個 Producer VS 1 個 Consumer
  ➤ Kafka 持久化驗證
  ➤ 1 個 Partition 的數據只允許 Consumer Group 中的某個 Consumer 消費
  ➤ 2 個 Partition VS 2 個 Consumer
</code></pre><h2 id="ㄧ手把手帶你建立-nodejs-專案--kafka-環境">ㄧ、手把手帶你建立 Node.js 專案 ＆ Kafka 環境<a class="h-anchor" href="#%e3%84%a7%e6%89%8b%e6%8a%8a%e6%89%8b%e5%b8%b6%e4%bd%a0%e5%bb%ba%e7%ab%8b-nodejs-%e5%b0%88%e6%a1%88--kafka-%e7%92%b0%e5%a2%83" aria-label="這個段落的連結"></a></h2>
<p><a href="https://deanlin.net/blog/202211/what-is-kafka/">上一篇文章</a>講了這麼多理論，下面就建立一個 Node.js 專案，來實際了解 Kafka 的運作邏輯吧！</p>
<blockquote>
<p>如果懶得按照步驟執行，可以直接到<a href="https://github.com/dean9703111/kafka-demo">筆者的 Github</a> Clone 一份下來。<br>筆者 local 的 Node.js 版本為 v18.12.0。</p>
</blockquote>
<p><strong>SETP 1</strong>：建立專案、安裝 kafkajs 套件。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm init -f
</span></span><span style="display:flex;"><span>npm install --save kafkajs
</span></span></code></pre></div><p><strong>SETP 2</strong>：建立儲存 Kafka 數據的資料夾（這是下一步「docker-compose.yml」裏面「volumes」的路徑）</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir -p deploy/kafkaCluster/kraft
</span></span></code></pre></div><p><strong>SETP 3</strong>：新增「docker-compose.yml」貼上如下程式。</p>
<blockquote>
<p>這裡筆者選用不依賴 zookeeper 的 Kafka 版本（KRaft），過去主流會使用 zookeeper 來保存消息，但 Kafka 官方表示在不久後就不再支援 zookeeper，故筆者做此選擇，至於 KRaft 本身有哪些優勢，感興趣的朋友可以參考<a href="https://www.cnblogs.com/smartloli/p/16430041.html">連結</a>。</p>
</blockquote>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">version</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;3&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#7ee787">services</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">kafka</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">image</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#39;bitnami/kafka:latest&#39;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">ports</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#39;9092:9092&#39;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">environment</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_ENABLE_KRAFT=yes</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_PROCESS_ROLES=broker,controller</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_BROKER_ID=1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_NODE_ID=1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">ALLOW_PLAINTEXT_LISTENER=yes</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">volumes</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#8b949e;font-style:italic"># 這是設定資料要存在哪路的路徑</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">./deploy/kafkaCluster/kraft:/bitnami/kafka:rw</span><span style="color:#6e7681">
</span></span></span></code></pre></div><p><strong>SETP 4</strong>：輸入以下指令把 Kafka 的 Docker 拉下來並執行，你可以把這個 Docker 想像成 Kafka Cluster 中的 Broker。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker-compose up -d
</span></span></code></pre></div><p><strong>SETP 5</strong>：新增「producer.js」貼上如下程式，我們會透過它每隔 2 秒產生消息到指定的 topic。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { Kafka, CompressionTypes, logLevel } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;kafkajs&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> kafka <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Kafka({
</span></span><span style="display:flex;"><span>  logLevel<span style="color:#ff7b72;font-weight:bold">:</span> logLevel.ERROR,
</span></span><span style="display:flex;"><span>  brokers<span style="color:#ff7b72;font-weight:bold">:</span> [<span style="color:#a5d6ff">`localhost:9092`</span>],
</span></span><span style="display:flex;"><span>  clientId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;example-producer&#39;</span>,
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> topic <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;topic-test&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> numPartitions <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">2</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> crateTopic <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">async</span> (topic) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> admin <span style="color:#ff7b72;font-weight:bold">=</span> kafka.admin();
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> admin.connect();
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> topics <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> admin.listTopics();
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">if</span> (<span style="color:#ff7b72;font-weight:bold">!</span>topics.includes(topic)) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> admin.createTopics({
</span></span><span style="display:flex;"><span>      topics<span style="color:#ff7b72;font-weight:bold">:</span> [{
</span></span><span style="display:flex;"><span>        topic<span style="color:#ff7b72;font-weight:bold">:</span> topic,
</span></span><span style="display:flex;"><span>        numPartitions<span style="color:#ff7b72;font-weight:bold">:</span> numPartitions
</span></span><span style="display:flex;"><span>      }],
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> fetchTopicOffsets <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> admin.fetchTopicOffsets(<span style="color:#a5d6ff">&#39;topic-test&#39;</span>)
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">`Crate topic:</span><span style="color:#a5d6ff">${</span>topic<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> successful!`</span>)
</span></span><span style="display:flex;"><span>    console.log(fetchTopicOffsets)
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> admin.disconnect();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> producer <span style="color:#ff7b72;font-weight:bold">=</span> kafka.producer()
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> getRandomNumber <span style="color:#ff7b72;font-weight:bold">=</span> () =&gt; Math.round(Math.random(<span style="color:#a5d6ff">10</span>) <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">1000</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> createMessage <span style="color:#ff7b72;font-weight:bold">=</span> (num, partition <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>) =&gt; ({
</span></span><span style="display:flex;"><span>  key<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`key-</span><span style="color:#a5d6ff">${</span>num<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>,
</span></span><span style="display:flex;"><span>  value<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`value-</span><span style="color:#a5d6ff">${</span>num<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">-</span><span style="color:#a5d6ff">${</span><span style="color:#ff7b72">new</span> Date().toISOString()<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>,
</span></span><span style="display:flex;"><span>  partition<span style="color:#ff7b72;font-weight:bold">:</span> partition
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> sendMessage <span style="color:#ff7b72;font-weight:bold">=</span> () =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> producer
</span></span><span style="display:flex;"><span>    .send({
</span></span><span style="display:flex;"><span>      topic,
</span></span><span style="display:flex;"><span>      compression<span style="color:#ff7b72;font-weight:bold">:</span> CompressionTypes.GZIP,
</span></span><span style="display:flex;"><span>      messages<span style="color:#ff7b72;font-weight:bold">:</span> Array(createMessage(getRandomNumber()))
</span></span><span style="display:flex;"><span>      <span style="color:#8b949e;font-style:italic">// 用來測試 kafka 多個 Partition 與 Consumer 的關係
</span></span></span><span style="display:flex;"><span>      <span style="color:#8b949e;font-style:italic">// messages: Array(createMessage(getRandomNumber(), 0), createMessage(getRandomNumber(), 1))
</span></span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    .then(console.log)
</span></span><span style="display:flex;"><span>    .<span style="color:#ff7b72">catch</span>(e =&gt; console.error(<span style="color:#a5d6ff">`[example/producer] </span><span style="color:#a5d6ff">${</span>e.message<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>, e))
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> run <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> crateTopic(topic);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> producer.connect()
</span></span><span style="display:flex;"><span>  setInterval(sendMessage, <span style="color:#a5d6ff">2000</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>run().<span style="color:#ff7b72">catch</span>(e =&gt; console.error(<span style="color:#a5d6ff">`[example/producer] </span><span style="color:#a5d6ff">${</span>e.message<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>, e))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> errorTypes <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#a5d6ff">&#39;unhandledRejection&#39;</span>, <span style="color:#a5d6ff">&#39;uncaughtException&#39;</span>]
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> signalTraps <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#a5d6ff">&#39;SIGTERM&#39;</span>, <span style="color:#a5d6ff">&#39;SIGINT&#39;</span>, <span style="color:#a5d6ff">&#39;SIGUSR2&#39;</span>]
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 收到無法處理錯誤時的處理
</span></span></span><span style="display:flex;"><span>errorTypes.forEach(type =&gt; {
</span></span><span style="display:flex;"><span>  process.on(type, <span style="color:#ff7b72">async</span> e =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>      console.log(<span style="color:#a5d6ff">`process.on </span><span style="color:#a5d6ff">${</span>type<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>)
</span></span><span style="display:flex;"><span>      console.error(e)
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">await</span> consumer.disconnect()
</span></span><span style="display:flex;"><span>      process.exit(<span style="color:#a5d6ff">0</span>)
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (_) {
</span></span><span style="display:flex;"><span>      process.exit(<span style="color:#a5d6ff">1</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 收到中斷訊號時的處理
</span></span></span><span style="display:flex;"><span>signalTraps.forEach(type =&gt; {
</span></span><span style="display:flex;"><span>  process.once(type, <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">await</span> consumer.disconnect()
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">finally</span> {
</span></span><span style="display:flex;"><span>      process.kill(process.pid, type)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p><strong>SETP 6</strong>：新增「consumer.js」貼上如下程式，我們透過它模擬消費者消費特定 topic 的消息。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { Kafka, logLevel } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;Kafkajs&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> Kafka <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Kafka({
</span></span><span style="display:flex;"><span>  logLevel<span style="color:#ff7b72;font-weight:bold">:</span> logLevel.INFO,
</span></span><span style="display:flex;"><span>  brokers<span style="color:#ff7b72;font-weight:bold">:</span> [<span style="color:#a5d6ff">`localhost:9092`</span>],
</span></span><span style="display:flex;"><span>  clientId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;example-consumer&#39;</span>,
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> topic <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;topic-test&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> consumer <span style="color:#ff7b72;font-weight:bold">=</span> Kafka.consumer({ groupId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;test-group&#39;</span> })
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 用來測試 Kafka 持久化
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// const consumer = Kafka.consumer({ groupId: &#39;test-group2&#39; })
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> run <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> consumer.connect()
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> consumer.subscribe({ topic, fromBeginning<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">true</span> })
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> consumer.run({
</span></span><span style="display:flex;"><span>    eachMessage<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#ff7b72">async</span> ({ topic, partition, message }) =&gt; {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">const</span> prefix <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>topic<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">[</span><span style="color:#a5d6ff">${</span>partition<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> | </span><span style="color:#a5d6ff">${</span>message.offset<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">] / </span><span style="color:#a5d6ff">${</span>message.timestamp<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>
</span></span><span style="display:flex;"><span>      console.log(<span style="color:#a5d6ff">`- </span><span style="color:#a5d6ff">${</span>prefix<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> </span><span style="color:#a5d6ff">${</span>message.key<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">#</span><span style="color:#a5d6ff">${</span>message.value<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>)
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>run().<span style="color:#ff7b72">catch</span>(e =&gt; console.error(<span style="color:#a5d6ff">`[example/consumer] </span><span style="color:#a5d6ff">${</span>e.message<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>, e))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> errorTypes <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#a5d6ff">&#39;unhandledRejection&#39;</span>, <span style="color:#a5d6ff">&#39;uncaughtException&#39;</span>]
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> signalTraps <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#a5d6ff">&#39;SIGTERM&#39;</span>, <span style="color:#a5d6ff">&#39;SIGINT&#39;</span>, <span style="color:#a5d6ff">&#39;SIGUSR2&#39;</span>]
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 收到無法處理錯誤時的處理
</span></span></span><span style="display:flex;"><span>errorTypes.forEach(type =&gt; {
</span></span><span style="display:flex;"><span>  process.on(type, <span style="color:#ff7b72">async</span> e =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>      console.log(<span style="color:#a5d6ff">`process.on </span><span style="color:#a5d6ff">${</span>type<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>)
</span></span><span style="display:flex;"><span>      console.error(e)
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">await</span> consumer.disconnect()
</span></span><span style="display:flex;"><span>      process.exit(<span style="color:#a5d6ff">0</span>)
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (_) {
</span></span><span style="display:flex;"><span>      process.exit(<span style="color:#a5d6ff">1</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 收到中斷訊號時的處理
</span></span></span><span style="display:flex;"><span>signalTraps.forEach(type =&gt; {
</span></span><span style="display:flex;"><span>  process.once(type, <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">await</span> consumer.disconnect()
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">finally</span> {
</span></span><span style="display:flex;"><span>      process.kill(process.pid, type)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><hr>
<h2 id="二啟動專案了解-kafka-的運行邏輯">二、啟動專案，了解 Kafka 的運行邏輯<a class="h-anchor" href="#%e4%ba%8c%e5%95%9f%e5%8b%95%e5%b0%88%e6%a1%88%e4%ba%86%e8%a7%a3-kafka-%e7%9a%84%e9%81%8b%e8%a1%8c%e9%82%8f%e8%bc%af" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>建議使用有分頁功能的終端機（如 iTerm2），這樣比較好觀察與操作。</p>
</blockquote>
<h3 class="h-nomark" id="-1-個-producer-vs-1-個-consumer">1 個 Producer VS 1 個 Consumer<a class="h-anchor" href="#-1-%e5%80%8b-producer-vs-1-%e5%80%8b-consumer" aria-label="這個段落的連結"></a></h3>
<p>在專案根目錄下輸入 <code>node producer.js</code> 模擬「生產者傳送消息」，並在另一個分頁輸入 <code>node consumer.js</code> 模擬「消費者消費消息」。</p>
<p>透過下面的 Gif 大家可以看到消息傳送＆消費的過程。</p>
<figure class="figure figure-video">
  <video class="gifv" muted loop playsinline controls preload="none" poster="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-02.webp" width="960" height="625" aria-label="1 個 Producer VS 1 個 Consumer（圖 2）" data-gifv>
    <source src="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-02.mp4" type="video/mp4">
  </video>
</figure>

<blockquote>
<p>確認消息可以順利生產與消費後，記得透過「Crontrol + C」將程式關閉。</p>
</blockquote>
<h3 id="-kafka-持久化驗證">Kafka 持久化驗證<a class="h-anchor" href="#-kafka-%e6%8c%81%e4%b9%85%e5%8c%96%e9%a9%97%e8%ad%89" aria-label="這個段落的連結"></a></h3>
<p>這邊我們再做一個<strong>持久化</strong>的實驗，我們把「consumer.js」裏面 groupId 的值改為「test-group2」，來確認是否可以重新消費過去的消息。</p>
<p>通過下圖我們可以得知 Kafka 的資料是持久化的，不會因為有消費者消費而消失；因此擴張的時候，只需要增加消費者來處理資料即可。</p>
<p><img src="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-03.webp" width="1400" height="659" alt="Kafka 持久化驗證（圖 3）" loading="lazy" decoding="async"></p>
<h3 class="h-nomark" id="-1-個-partition-的數據只允許-consumer-group-中的某個-consumer-消費">1 個 Partition 的數據只允許 Consumer Group 中的某個 Consumer 消費<a class="h-anchor" href="#-1-%e5%80%8b-partition-%e7%9a%84%e6%95%b8%e6%93%9a%e5%8f%aa%e5%85%81%e8%a8%b1-consumer-group-%e4%b8%ad%e7%9a%84%e6%9f%90%e5%80%8b-consumer-%e6%b6%88%e8%b2%bb" aria-label="這個段落的連結"></a></h3>
<p>接著我們來驗證，在 Producer 的 Partition 只有 1 個的情境下，是不是相同的 group_id 下只會有一個 consumer 進行消費；這次要開啟 3 個分頁，1 個模擬生產者，2 個模擬消費者。</p>
<p>透過下面的 Gif，大家可以看到在同一個 Consumer Group 中，的確只會有一個 Consumer 進行消費。</p>
<figure class="figure figure-video">
  <video class="gifv" muted loop playsinline controls preload="none" poster="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-04.webp" width="960" height="551" aria-label="1 個 Partition 的數據只允許 Consumer Group 中的某個 Consumer 消費（圖 4）" data-gifv>
    <source src="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-04.mp4" type="video/mp4">
  </video>
</figure>

<h3 class="h-nomark" id="-2-個-partition-vs-2-個-consumer">2 個 Partition VS 2 個 Consumer<a class="h-anchor" href="#-2-%e5%80%8b-partition-vs-2-%e5%80%8b-consumer" aria-label="這個段落的連結"></a></h3>
<p>先前有說到 Partition 數量最好與 Consumer 的數量相等，這樣效率才會最高（一個人一個碗）；這裡一樣是開啟 3 個分頁，1 個模擬生產者，2 個模擬消費者；同時對「producer.js」裡面的程式做微調如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 將原本的註解
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// messages: Array(createMessage(getRandomNumber())),
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 解開下面的註解，測試 Kafka 多個 Partition 與 Consumer 的關係
</span></span></span><span style="display:flex;"><span>messages<span style="color:#ff7b72;font-weight:bold">:</span> Array(createMessage(getRandomNumber(),<span style="color:#a5d6ff">0</span>),createMessage(getRandomNumber(),<span style="color:#a5d6ff">1</span>)),
</span></span></code></pre></div><p>透過下面的 Gif，可以觀察到在 2 個 Partition VS 2 個 Consumer 時，消息會均勻的分配。</p>
<figure class="figure figure-video">
  <video class="gifv" muted loop playsinline controls preload="none" poster="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-05.webp" width="960" height="551" aria-label="2 個 Partition VS 2 個 Consumer（圖 5）" data-gifv>
    <source src="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-05.mp4" type="video/mp4">
  </video>
</figure>

<hr>
<p>在文章的最後，筆者想分享一段話：「只透過文字是無法理解一門技術的，需要透過實作才有辦法拼出技術的全貌。」</p>
<p>今天的文章就分享到這裡，筆者之後再找時間分享更深入的組合應用，也歡迎讀者分享自己的使用心得，希望藉助彼此的經驗，讓我們一起在工程師的道路走得更遠。</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
