<?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/">
  <channel>
    <title>Deep-Dive on Vallari Agrawal</title>
    <link>https://vallariagrawal.com/tags/deep-dive/</link>
    <description>Recent content in Deep-Dive on Vallari Agrawal</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>contact@vallariagrawal.com (Vallari Agrawal)</managingEditor>
    <webMaster>contact@vallariagrawal.com (Vallari Agrawal)</webMaster>
    <copyright>Vallari Agrawal © 2025</copyright>
    <lastBuildDate>Sat, 20 Jun 2026 22:12:03 +0530</lastBuildDate>
    <atom:link href="https://vallariagrawal.com/tags/deep-dive/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>What is NVMe and NVMeoF?</title>
      <link>https://vallariagrawal.com/blog/nvmeof/</link>
      <pubDate>Sat, 20 Jun 2026 22:12:03 +0530</pubDate><author>contact@vallariagrawal.com (Vallari Agrawal)</author>
      <guid>https://vallariagrawal.com/blog/nvmeof/</guid>
      <description>&lt;h1 id=&#34;nvme&#34;&gt;NVMe&lt;/h1&gt;&#xA;&lt;p&gt;First, let&amp;rsquo;s understand some terminology:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://vallariagrawal.com/images/nvmeof/SSD.png&#34; alt=&#34;[SSD]&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;Fig.1: SSD key components (&lt;a href=&#34;https://www.oscoo.com/news/what-is-an-ssd-the-complete-guide/&#34;&gt;source&lt;/a&gt;)&lt;/p&gt;&#xA;&lt;p&gt;A storage device has many parts, here we focus on the controller and the physical connector used to communicate with the host.&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Physical Interface&lt;/strong&gt;: It is the physical layer which includes the connectors, cable, motherboard slots. This determines how fast bits can physically move and what shape the plug is. Examples: SATA, SAS, PCIe.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;Protocol Interface&lt;/strong&gt;: It includes the language and rules used to send commands over that physical connection. This determines how the host CPU and storage device communicate - what commands exist, how many queues, how errors are reported. Examples: ATA, SCSI, NVMe.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="nvme">NVMe</h1>
<p>First, let&rsquo;s understand some terminology:</p>
<p><img src="/images/nvmeof/SSD.png" alt="[SSD]"></p>
<p>Fig.1: SSD key components (<a href="https://www.oscoo.com/news/what-is-an-ssd-the-complete-guide/">source</a>)</p>
<p>A storage device has many parts, here we focus on the controller and the physical connector used to communicate with the host.</p>
<ol>
<li>
<p><strong>Physical Interface</strong>: It is the physical layer which includes the connectors, cable, motherboard slots. This determines how fast bits can physically move and what shape the plug is. Examples: SATA, SAS, PCIe.</p>
</li>
<li>
<p><strong>Protocol Interface</strong>: It includes the language and rules used to send commands over that physical connection. This determines how the host CPU and storage device communicate - what commands exist, how many queues, how errors are reported. Examples: ATA, SCSI, NVMe.</p>
</li>
</ol>
<p>A storage solution is often a combination of both a physical interface and a protocol. For example, a &ldquo;SATA storage device&rdquo; uses ATA commands over a serial connection (hence, Serial ATA), while a &ldquo;SAS storage device&rdquo; uses SCSI commands over a serial connection (hence, Serial Attached SCSI).</p>
<p>TL;DR: The storage device has a controller chip which implements the protocol interface. On the host, the operating system has driver software to communicate using the same protocol. And the physical interface is the physical connector between the host and storage device.</p>
<h2 id="a-brief-history-of-storage-interfaces">A brief history of storage interfaces</h2>
<p>Early storage systems were designed around mechanical hard drives.</p>
<p>On a mechanical hard drives, when working with multiple files in different locations, the drive head moved at high speed (across various parts of the drive) to read and write data on disk. They could effectively only seek to one location/file at a time.</p>
<p>Consumer storage used ATA (also called IDE or Parallel ATA) and enterprise systems used SCSI. These interfaces defined both the command set and the controller architecture.</p>
<p>In the early 2000s, these two evolved to use serial links:</p>
<ul>
<li>ATA evolved into SATA (Serial ATA)</li>
<li>SCSI evolved into SAS (Serial Attached SCSI)</li>
</ul>
<p>But both new interfaces continued to use their predecessor&rsquo;s command set. SATA continued using ATA commands, while SAS continued using SCSI commands, but transmitted them over modern serial connections.</p>
<p>When SATA was introduced, the industry wanted a standard way for operating systems to talk to SATA controllers. Intel developed <a href="https://en.wikipedia.org/wiki/SATA#Advanced_Host_Controller_Interface">AHCI (Advanced Host Controller Interface)</a>, which has become a de facto standard.</p>
<p>Since hard drives couldn&rsquo;t support multiple command queues, AHCI was built with 1 command queue, with a maximum of 32 commands per queue. This was sufficient for hard drives, but not for newer flash storage.</p>
<h2 id="why-was-nvme-needed">Why was NVMe needed?</h2>
<p>With flash storage used in SSDs, the physical limitation no longer existed (as they don&rsquo;t have a drive head and used technology such as NAND) and can service many requests in parallel.</p>
<p>By late 2010, mainstream consumer SSDs were being bottlenecked by the comparatively limited throughput granted by SATA (~6Gb/s - speed limitation inherent to SATA). In the enterprise market, this limitation was overcome by creating SSDs that connected via PCI Express.</p>
<p>Now, these SSDs were being bottlenecked by AHCI&rsquo;s random I/O performance. Aternatively, SSDs came with custom interface specification which varied a lot between vendors (and even within models of same vendors). This variation made it difficult to support vendor-specific software drivers.</p>
<p>So NVM Express was introduced as an industry standard interface for PCIe-connected SSDs to use, with a common driver specification (eliminating the need for vendor-specific drivers).</p>
<p>NVMe specification has a flash-first philosophy and offered 65k command queues with up-to 65k commands per queue - leading to higher performance.</p>
<blockquote>
<p>The NVM Express Base Specification defines a protocol for host software to communicate with non-volatile memory subsystems over a variety of memory-based transports (PCIe) and message-based transports (RDMA, TCP). It is the industry standard for solid state drives (SSDs) in all form factors (U.2, M.2, AIC, EDSFF).</p>
<p>Source: <a href="https://nvmexpress.org/">https://nvmexpress.org/</a></p></blockquote>
<h2 id="why-is-nvme-faster">Why is NVMe faster?</h2>
<ol>
<li>
<p><strong>Multiple independent I/O queues</strong></p>
<p>What is a queue? When your CPU wants to read or write data, it doesn&rsquo;t talk to the drive directly - it puts a command in a queue and the drive&rsquo;s controller picks it up and executes it.</p>
<p>AHCI provides 1 command queues with 32 commands per queue. So all CPU cores share that one queue, which means there&rsquo;s <strong>&ldquo;locking&rdquo;</strong> - one core has to wait for another core to finish writing its command before it can add its own to the queue. It was fine with hard disks because the disk could only physically seek to one location at a time.</p>
<p>NVMe supports 64 thousand queues and each queue can handle 64k commands. So, each CPU core can have it&rsquo;s own IO queue who can all go simultaneously and independently, and they don&rsquo;t have to wait for each other.</p>
<p>We will learn more about how queues work in a later section.</p>
</li>
<li>
<p><strong>Direct communication over PCIe</strong>:
With SATA and SAS, an HBA (Host Bus Adaptor) sits between the CPU and storage - it translates PCIe signals into SATA/SAS signals, and runs the AHCI/SCSI command protocol. But with NVMe, the drive sits directly on the PCIe bus and speaks PCIe natively - the HBA is gone, and so is the protocol translation layer.</p>
</li>
<li>
<p><strong>Lower command overhead</strong>:
AHCI fetches each command in two RAM reads - a pointer to the command, then the actual command. NVMe puts the full command directly in the submission queue, so the controller fetches it in one.</p>
</li>
</ol>
<h2 id="form-factors">Form Factors</h2>
<p>Form factor is a hardware design that defines the size, shape, connector type of a storage device - independent of the protocol it speaks internally.</p>
<p>Traditional hard drives have generally come in 3.5″ and 2.5″ form factors, and SSDs have typically been available as 2.5″ or mSATA cards. NVMe drives have a variety of form factors– the most common is the M.2 format, which is 22mm wide with lengths of 30, 42, 80, or less often 110 millimeters. (<a href="https://www.techrepublic.com/article/nvme-a-cheat-sheet/">source</a>)</p>
<p><img src="/images/nvmeof/form-factors.png" alt="[Form Factor]"></p>
<p>Picture: Form Factors (<a href="https://www.itechtics.com/ssd-types/">source</a>)</p>
<h2 id="nvme-architecture">NVMe architecture</h2>
<p><img src="/images/nvmeof/nvme-arch.png" alt="[NVMe Architecture]"></p>
<p>Picture: NVMe Architecture</p>
<p>Linux natively supports <strong>NVMe Driver</strong> through the nvme kernel module, it exposes devices:</p>
<ul>
<li>/dev/nvmeX -&gt; the physical NVMe hardware <strong>controllers</strong></li>
<li>/dev/nvmeXnY -&gt; <strong>namespaces</strong>, which are logical storage volumes</li>
</ul>
<h3 id="nvme-driver-software">NVMe Driver (Software)</h3>
<p>The NVMe driver (<code>nvme</code> kernel module) sits between the OS and the NVMe hardware. When an NVMe SSD is plugged in, the driver detects it as a PCIe endpoint and sets up the communication channel: the submission and completion queues in host memory, and the doorbell registers it uses to signal the controller.</p>
<p>NVMe Driver supports two kind of commands - admin commands and I/O commands.</p>
<h3 id="nvme-controller-hardware">NVMe Controller (Hardware)</h3>
<p>The NVMe controller is a dedicated processor built directly onto the SSD, responsible for managing communication between the host system and the storage media.</p>
<p>It receives commands from the driver, executes them against the flash storage, and sends results back. Under the hood it also handles the messier parts of managing flash - wear leveling, garbage collection, and error correction - so the host never has to think about them.</p>
<p>In Linux, the controller is exposed as /dev/nvmeX. Each physical SSD has one controller, and namespaces live under it - which is why the device naming is nvme0n1, nvme0n2: controller 0, namespace 1 and 2. More about this in Namespaces section.</p>
<h3 id="queues-and-its-workflow">Queues and its workflow</h3>
<blockquote>
<p>NVMe’s performance benefits stem from its queue-based architecture.</p></blockquote>
<p><img src="/images/nvmeof/nvme-queues.png" alt="[NVMe Submission and Completion Queue]"></p>
<p>Picture: NVMe admin and multiple I/O queue pair (<a href="https://nvmexpress.org/wp-content/uploads/NVMe_Overview.pdf">source</a>)</p>
<p><strong>Queue Pairs (Submission Queue + Completion Queue) &amp; Doorbell registers</strong></p>
<p>Every queue in NVMe is a queue pair - a Submission Queue (SQ) and a Completion Queue (CQ):</p>
<ul>
<li>SQ: The NVMe driver submits commands on the <strong>submission queue</strong> for controller to execute.</li>
<li>CQ: The controllers posts the results of executed commands on the <strong>completion queue</strong>.</li>
</ul>
<p>These queues are <strong>fixed-size</strong> <strong>circular buffers</strong> in <strong>host memory</strong>. The driver writes commands at the tail of the SQ, and the controller reads from the head. The controller writes results at the tail of the CQ, and the driver reads from the head. When either reaches the end of the buffer, it wraps back to position 0.</p>
<p>There&rsquo;s one more part: <strong>doorbell registers</strong> - tiny registers on the NVMe controller hardware, used as a notification mechanism to let the controller or the driver know that the SQ/CQ are updated.</p>
<p>How they work together:</p>
<ol>
<li>Once the driver places a command in the submission queue, the host updates the &ldquo;submission queue tail doorbell register&rdquo; to notify the controller.</li>
<li>The controller fetches the command from submission queue and executes it.</li>
<li>The controller then writes result into the completion queue. Then the controller updates the &ldquo;completion queue head doorbell register&rdquo; to inform the host that new completion entries are available.</li>
</ol>
<p><img src="/images/nvmeof/nvme-ioqueue.png" alt="[NVMe queue pair workflow]"></p>
<p>Picture: NVMe queue pair workflow (<a href="https://www.osr.com/nt-insider/2014-issue4/introduction-nvme-technology/">source</a>)</p>
<p><strong>The admin queue and I/O queues</strong>: There are two kinds of queue pair to handle two kinds of commands:</p>
<p><strong>1. The Admin queue</strong></p>
<p>It is the queue pair (SQ + CQ) in host RAM that handle <strong>admin commands</strong>. These include commands for identifying devices, managing namespaces, and fetching healthcheck information.</p>
<p>Every queue pair has a unique identifier (QID): <strong>QID 0 is always the admin queue</strong>, QID 1 onwards are IO queues.</p>
<p>The admin queue is separate from IO queues so management commands always have a clear path, even when the drive is under heavy IO load.</p>
<p><strong>2. I/O queues</strong></p>
<p>They are queue pairs (SQ + CQ) in host RAM that handle <strong>I/O commands</strong>. These commands enable the data operations on the device, including <strong>read/write operations</strong>.</p>
<p>NVMe theoretically supports up to 64K queues, with each queue capable of holding up to 64K commands. However, most real-world NVMe devices support significantly fewer queues and commands.</p>
<h3 id="namespaces">Namespaces</h3>
<p>Namespaces provide logical segmentation within an NVMe device. NVMe namespaces are <strong>logically isolated</strong> set of Logical Block Addresses (LBAs). The host sees a contiguous address space - block 0 to block N (regardless of how the data is physically laid out on the flash). Read <a href="https://vallariagrawal.com/blog/ssd/">here</a> to understand more about LBAs.</p>
<p>In Linux, each namespace is exposed as a block device (e.g., /dev/nvme0n1), where <strong>nvme0 refers to the controller and n1 refers to the namespace</strong>. Each namespace looks like a distinct storage target to the host.</p>
<p>Example, namespaces across multiple controllers/drives:</p>





<pre tabindex="0"><code>$ nvme list
Node                  Generic               SN                   Model                                    Namespace  Usage                      Format           FW Rev
--------------------- --------------------- -------------------- ---------------------------------------- ---------- -------------------------- ---------------- --------
/dev/nvme0n1          /dev/ng0n1            012510B00426         MTFDKCC1T9TGP-1BK1JABYY                  0x1        141.53  GB /   1.92  TB    512   B +  0 B   E3MQ000
/dev/nvme1n1          /dev/ng1n1            012510B00440         MTFDKCC1T9TGP-1BK1JABYY                  0x1        139.12  GB /   1.92  TB    512   B +  0 B   E3MQ000
/dev/nvme2n1          /dev/ng2n1            25174FCD463C         Micron_7450_MTFDKBA960TFR                0x1         18.29  GB / 960.20  GB    512   B +  0 B   E2MU200 </code></pre><p>In this example,</p>
<ul>
<li>/dev/nvme0n1 is the block device - the one that gets mounted, formatted with a filesystem, and used for IO.</li>
<li>/dev/ng0n1 is a character device - a direct unbuffered channel to the controller used for admin commands like health checks or firmware updates.</li>
<li>&ldquo;Format&rdquo; is the logical block size (512 bytes here).</li>
</ul>
<p>Namespaces are accessible by the host through a unique <strong>Namespace Identifier (NSID)</strong>. NSID are named local to the controllers. So in above example, all three namespaces have NSID 1 on their respective controllers.</p>
<p>A single controller can expose multiple namespaces - each appearing as an independent block device to the host:</p>





<pre tabindex="0"><code>One controller, multiple namespaces:
/dev/nvme0n1   ← controller 0, namespace 1
/dev/nvme0n2   ← controller 0, namespace 2
/dev/nvme0n3   ← controller 0, namespace 3</code></pre><p>This helps to isolate workloads on the same drive. So, each namespace can have its own encryption key (so two users cannot read each other&rsquo;s data on same hardware), and its own write protection, etc.</p>
<h3 id="subsystem">Subsystem</h3>
<p>An NVMe subsystem is a top-level abstraction that encapsulates:</p>
<ul>
<li>one or more controllers</li>
<li>one or more namespaces within the controllers</li>
<li>one or more paths (i.e. physical or logical access points - PCIe ports for local NVMe or network ports for NVMe-oF)</li>
</ul>
<p><img src="/images/nvmeof/nvme-subsystem.png" alt="[Subsystem]"></p>
<p>Fig: An example of a subsystem as shown in NVMe Base Spec (<a href="https://nvmexpress.org/wp-content/uploads/NVM-Express-Base-Specification-Revision-2.3-2025.08.01-Ratified.pdf">source</a>)</p>
<p>Each NVMe subsystem is exposed under <code>/sys/class/nvme</code> and initialilzed by the <code>nvme-core</code> and <code>nvme</code> driver.</p>
<p>Example, 3 local NVMe SSDs, each as its own subsystem with one PCIe path:</p>





<pre tabindex="0"><code>$ nvme list-subsys --output-format=json
[
  {
    &#34;HostNQN&#34;:&#34;nqn.2014-08.org.nvmexpress:uuid:b8b85908-723a-45a9-922d-a912639f1461&#34;,
    &#34;HostID&#34;:&#34;b8b85908-723a-45a9-922d-a912639f1461&#34;,
    &#34;Subsystems&#34;:[
      {
        &#34;Name&#34;:&#34;nvme-subsys0&#34;,
        &#34;NQN&#34;:&#34;nqn.2016-08.com.micron:nvme:nvm-subsystem-sn-012510B00426&#34;,
        &#34;Paths&#34;:[
          {
            &#34;Name&#34;:&#34;nvme0&#34;,
            &#34;Transport&#34;:&#34;pcie&#34;,
            &#34;Address&#34;:&#34;0000:03:00.0&#34;,
            &#34;State&#34;:&#34;live&#34;
          }
        ]
      },
      {
        &#34;Name&#34;:&#34;nvme-subsys1&#34;,
        &#34;NQN&#34;:&#34;nqn.2016-08.com.micron:nvme:nvm-subsystem-sn-012510B00440&#34;,
        &#34;Paths&#34;:[
          {
            &#34;Name&#34;:&#34;nvme1&#34;,
            &#34;Transport&#34;:&#34;pcie&#34;,
            &#34;Address&#34;:&#34;0000:04:00.0&#34;,
            &#34;State&#34;:&#34;live&#34;
          }
        ]
      },
      {
        &#34;Name&#34;:&#34;nvme-subsys2&#34;,
        &#34;NQN&#34;:&#34;nqn.2016-08.com.micron:nvme:nvm-subsystem-sn-25174FCD463C&#34;,
        &#34;Paths&#34;:[
          {
            &#34;Name&#34;:&#34;nvme2&#34;,
            &#34;Transport&#34;:&#34;pcie&#34;,
            &#34;Address&#34;:&#34;0000:02:00.0&#34;,
            &#34;State&#34;:&#34;live&#34;
          }
        ]
      }
    ]
  }
]</code></pre><p>Here, <code>&quot;Transport&quot;: &quot;pcie&quot;</code> means local NVMe device. In NVMe-oF, the same structure appears but with <code>&quot;Transport&quot;: &quot;tcp&quot;</code> and a network address.</p>
<h3 id="nqn-nvme-qualified-name">NQN (NVMe Qualified Name)</h3>
<p>NQN (NVMe Qualified Name) is a unique identifier for a subsystem or a host. Every subsystem has one, every host has one.</p>
<ol>
<li>Host NQN is used to uniquely identify the host/client system.</li>
<li>Subsystem NQN is used to uniquely identify the storage device.</li>
</ol>
<p>From the above <code>nvme list-subsys</code> output, we can see a host NQN and subsystem NQN:</p>





<pre tabindex="0"><code>[
  {
    // host NQN 
    &#34;HostNQN&#34;:&#34;nqn.2014-08.org.nvmexpress:uuid:b8b85908-723a-45a9-922d-a912639f1461&#34;,
    &#34;HostID&#34;:&#34;b8b85908-723a-45a9-922d-a912639f1461&#34;,
    &#34;Subsystems&#34;:[
      {
        &#34;Name&#34;:&#34;nvme-subsys0&#34;,
        // subsystem NQN
        &#34;NQN&#34;:&#34;nqn.2016-08.com.micron:nvme:nvm-subsystem-sn-012510B00426&#34;,
        &#34;Paths&#34;:[
           ... 
          ]
      },  
    ]
  }
]</code></pre><p>Host NQN is stored at <code>/etc/nvme/hostnqn</code>:</p>





<pre tabindex="0"><code>$ cat /etc/nvme/hostnqn
nqn.2014-08.org.nvmexpress:uuid:6def1fb3-a3a2-447b-9938-59856fc4805f</code></pre><p>Now, let&rsquo;s observe the format of these NQN:</p>





<pre tabindex="0"><code>Format:  nqn.{YYYY-MM}.{reverse.domain}:{unique-name}</code></pre><p>The reverse domain (com.micron, org.nvmexpress) is the naming authority. It ensures two different vendors can&rsquo;t accidentally create the same NQN. The unique name after the colon is whatever makes it distinct - a serial number for drives, a UUID for hosts.</p>
<hr>
<h1 id="nvme-of">NVMe-oF</h1>
<p>Storage can be accessed through 3 approaches: <strong>Direct-attached storage (DAS)</strong>, <strong>NAS (network-attached storage)</strong>, and <strong>SAN (storage area networks)</strong>.</p>
<ol>
<li><strong>DAS</strong> systems have the storage devices directly attached to the client systems.</li>
</ol>
<p>The other two are remote storage systems (accessed over a network):</p>
<ol start="2">
<li>
<p><strong>NAS</strong> systems <strong>serves files over the network</strong> using protocols such as NFS (network file system) and SMB (server message block) to access the file system. The storage server manages the filesystem and clients access files and directories rather than raw storage devices. It is often used within small teams or household file servers.</p>
</li>
<li>
<p><strong>SAN</strong> systems are used to <strong>access block devices over the network</strong>. The storage server provides raw blocks, while the client is responsible for creating and managing filesystems.</p>
</li>
</ol>
<p>Since SAN clients operate directly on block devices, they need a protocol to issue storage admin/io commands to a remote storage.</p>
<h2 id="a-brief-history-of-remote-block-storage-protocols">A brief history of remote block storage protocols</h2>
<p>Before NVMe, the industry standardized on the SCSI command set for many enterprise storage. Originally designed for locally attached disks, SCSI became the standard way for hosts to communicate with storage devices and was later reused in technologies such as SAS. Then the industry extended SCSI beyond local connections.</p>
<p>Instead of creating a new protocol, SCSI commands were encapsulated and transported across a network. This became known as <strong>iSCSI</strong>. This allowed SCSI commands to be exchanged between initiator (iSCSI client) and target (iSCSI server with storage).</p>
<h2 id="nvme-over-fabric">NVMe over Fabric</h2>
<p>Similar to how iSCSI made it possible to send SCSI commands over a network, NVMe-oF (part of NVMe Base specifications) <strong>allows encapsulation of NVMe commands over a network</strong>. Also like iSCSI, NVMeoF also requires an initiator (client) and a target (server) to establish a connection and exchange commands.</p>
<p>The key advantage of NVMe-oF is that it keeps the performance benefits of NVMe:</p>
<ol>
<li>Parallel I/O operations: In NVMe-oF, the queues allow multiple I/O requests to be processed concurrently between the host and storage target.</li>
<li>Reduced CPU overhead: Because NVMe allows different CPU cores to use separate queues, they can process I/O requests independently without frequently coordinating access to a shared queue. This reduces CPU overhead on both the host and storage target.</li>
<li>Fewer protocol interactions: Since NVMe requires fewer protocol interactions (one round trip and two interactions) to complete an I/O request than traditional SCSI-based protocols (two round trips and four interactions), it results in NVMeoF providing lower latency.</li>
</ol>
<h2 id="transport-types">Transport types</h2>
<p>NVMeoF standard is not fabric-specific, instead there is a separate <strong>Transport Binding</strong> spec for each transport layer. From host&rsquo;s perspective, NVMe commands are issued identically regardless of which transport layer carries them underneath.</p>
<p>Three transport bindings are currently standardized:</p>
<ol>
<li>NVMe/TCP</li>
<li>NVMe/RDMA</li>
<li>NVMe/FC</li>
</ol>
<h2 id="nvme-of-architecture">NVMe-oF Architecture</h2>
<p>NVMeoF has the client/server model: <strong>The initiator / host</strong> is the client system that needs to access remote storage, and runs the NVMe-oF initiator driver (<code>nvme-fabrics</code> kernel module on Linux). <strong>The target</strong> is the storage server that exposes subsystems with namespaces to the network.</p>
<p><img src="/images/nvmeof/nvmeof-architecture.png" alt="[NVMe-oF Architecture]"></p>
<p>Picture: NVMe-oF Architecture (diagram inspired by <a href="https://www.smartx.com/blog/2023/02/zbs-nvmeof-en/">smartx blog</a>)</p>
<p>The initiator connected to a remote subsystem (on target hosts) and once connected, the remote namespace appears exactly like a local NVMe namespace - eg, /dev/nvme0n1 - on the initiator system.</p>
<h3 id="nvme-of-subsystems">NVMe-oF Subsystems</h3>
<p>NVMe-oF subsystem is similar to local NVMe - it&rsquo;s still the top-level container with controllers and namespaces, identified with an NQN. The difference is it now exists on the target machine and is reachable over the network.</p>
<h4 id="1-nvmeof-controllers">1. NVMeoF Controllers</h4>
<p>The spec defines two controller models: the <strong>static controller model</strong> and the <strong>dynamic controller model</strong>.</p>
<p>In local NVMe, the controller is a static controller model because is a hardware chip and always present. In NVMe-oF, the spec defines a dynamic controller - it is instantiated by the target when an initiator successfully connects, and destroyed when the initiator disconnects. And this exclusive relationship between a controller and a host is called an <strong>association</strong>.</p>
<p>First, the initiator requests connection to a remote subsystem using the <code>connect</code> command:</p>





<pre tabindex="0"><code>$ nvme connect \
   --transport tcp \
   --traddr 10.243.64.86 \
   --trsvcid 4420 \
   --nqn nqn.2016-06.io.spdk:cnode1
connecting to device: nvme0</code></pre><p>Then, the target instantiates a dynamic controller for that host. This controller owns the admin queue and I/O queue pairs for that host connection, just like a local NVMe controller. Only difference is that these commands are carried over TCP (here) instead of PCIe.</p>
<p>After connecting, we can see NVMeoF subsystem from the initiator like this:</p>





<pre tabindex="0"><code>$ nvme list-subsys --output-format=json
[
  {
    &#34;HostNQN&#34;:&#34;nqn.2014-08.org.nvmexpress:uuid:6def1fb3-a3a2-447b-9938-59856fc4805f&#34;,
    &#34;HostID&#34;:&#34;6def1fb3-a3a2-447b-9938-59856fc4805f&#34;,
    &#34;Subsystems&#34;:[
      {
        &#34;Name&#34;:&#34;nvme-subsys0&#34;,
        &#34;NQN&#34;:&#34;nqn.2016-06.io.spdk:cnode1.mygroup1&#34;,  // subsystem NQN
        &#34;Paths&#34;:[
          {
            &#34;Name&#34;:&#34;nvme0&#34;,
            &#34;Transport&#34;:&#34;tcp&#34;,
            &#34;Address&#34;:&#34;traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88&#34;,
            &#34;State&#34;:&#34;live&#34;
          }
        ]
      }
    ]
  }
]</code></pre><p>Here, the output shows that the initiator (&ldquo;HostNQN&rdquo;) is connected to one remote subsystem (<code>nqn.2016-06.io.spdk:cnode1.mygroup1</code>) through a single TCP path. The path connects the initiator (<code>10.243.64.88</code>) to the target (<code>10.243.64.86:4420</code>), and the dynamic controller created for this connection appears on the initiator as nvme0. &ldquo;<code>State&quot;: &quot;live</code>&rdquo; indicates that the connection is active and able to do I/O.</p>
<p>When the host disconnects, the association ends and the target destroys the dynamic controller.</p>
<h4 id="2-nvmeof-namespace">2. NVMeoF Namespace</h4>
<p>Here, Namespaces are same as local NVMe: they are LBA exposed as block devices and identified by NSID. In fact, remote namespaces looks just like local NVMe namespaces to the initiator. Example: <code>/dev/nvme0n1</code> (controller nvme0, namespace 1).</p>
<p>Example of a NVMeoF target with a subsystem and 3 namespaces:</p>





<pre tabindex="0"><code>$ nvme show-topology
nvme-subsys0 - NQN=nqn.2016-06.io.spdk:cnode1.mygroup1
               hostnqn=nqn.2014-08.org.nvmexpress:uuid:6def1fb3-a3a2-447b-9938-59856fc4805f
               iopolicy=numa
\
 +- ns 1
 \
  +- nvme0 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized
 +- ns 3
 \
  +- nvme0 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized
 +- ns 5
 \
  +- nvme0 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized</code></pre><p>This output shows a single dynamic controller (<code>nvme0</code>) connected to a subsystem that exposes three namespaces (NSIDs 1, 3, and 5), and all are accessible through the same port. The &ldquo;live optimized&rdquo; state is the ANA state, indicating this is the optimal path for each namespace. We&rsquo;ll cover ANA in a later section.</p>
<p>Multiple initiator hosts: Since each host connection creates a dynamic controller, multiple hosts can connect to same namespaces through their own controllers.</p>
<p>Bonus: NVMe spec also defines <strong>&ldquo;shared namespaces&rdquo;</strong> where a namespace can be attached to two or more controllers concurrently.</p>
<h4 id="3-allowed-host-list--access-control">3. Allowed Host List / Access control</h4>
<p>In local NVMe, physical access to the device is the only access control. Over a network, you need explicit control over which hosts are allowed to connect.</p>
<p>Each NVMe-oF subsystem maintains an Allowed Host List which is a list of Host NQNs that are allowed access to connect to that subsystem. When a host sends a <code>connect</code> command, it presents its host NQN. The subsystem checks it against this list before allowing the session. If the NQN isn&rsquo;t on the list, the connection is rejected.</p>
<h3 id="nvme-of-discovery">NVMe-oF Discovery</h3>
<p>Before a host can connect to a subsystem, it needs to know what subsystems exist and where to reach them. This is what the Discovery Service is for.</p>
<p><strong>Discovery Service</strong> is a subsystem that hosts only Discovery controllers. It allows hosts to automatically find available NVMeoF targets and subsystems on the network.</p>
<p><strong>Discovery controller</strong> is a special controller type that exposes no namespaces and serves no I/O - its sole purpose is to return a Discovery Log Page: a list of all subsystems accessible to this host, along with the transport address needed to reach each one.</p>
<p><strong>How it works?</strong></p>
<ol>
<li>
<p>The host queries a target gateway at the well-known discovery address (port 8009 for &ldquo;centralized discovery&rdquo; - i.e. a discovery controller that maintains the full target list).</p>
<p>Note: this port is separate from 4420, which is used for actual I/O subsystems.</p>
</li>
<li>
<p>It sends a &ldquo;Get Log Page&rdquo; command to the Discovery Controller, which responds with a Discovery Log Page (see <code>nvme discover</code> example below).</p>
</li>
</ol>





<pre tabindex="0"><code># Discover all NVMe-oF targets at a given address
nvme discover -t tcp -a 10.243.64.86 -s 8009

# Connect to a discovered subsystem by NQN
nvme connect -t tcp -a 10.243.64.86 -s 4420 -n nqn.2016-06.io.spdk:cnode1.mygroup1

# Connect to all discovered subsystems at once
nvme connect-all -t tcp -a 10.243.64.86</code></pre><p>So the discovery only works after a host already knows the address of at least one Discovery controller. <code>nvme connect-all</code> does same discovery internally by sending the same &ldquo;Get Log Page&rdquo; request to a Discovery controller, then automatically issues a <code>connect</code> command for every entry returned.</p>
<p>Here is an example of discovery of two NVMeoF target gateways (for multipath):</p>





<pre tabindex="0"><code># nvme discover -t tcp -a 10.243.64.86 -s 8009

Discovery Log Number of Records 8, Generation counter 6
=====Discovery Log Entry 0======
trtype:  tcp                                  // transport type
adrfam:  ipv4
subtype: nvme subsystem
treq:    not required
portid:  0
trsvcid: 4420
subnqn:  nqn.2016-06.io.spdk:cnode1.mygroup1    // subsystem NQN
traddr:  10.243.64.86
eflags:  none
sectype: none
=====Discovery Log Entry 1======
trtype:  tcp
adrfam:  ipv4
subtype: nvme subsystem
treq:    not required
portid:  1
trsvcid: 4420
subnqn:  nqn.2016-06.io.spdk:cnode1.mygroup1
traddr:  10.243.64.87
eflags:  none
sectype: none
....
....
.... (more entries)</code></pre><p>The Discovery Service also helps to discover multiple paths to a subsystem.</p>
<p><strong>Multipath</strong>:</p>
<p>In above <code>nvme discover</code> output, both of the entries have the same &ldquo;subnqn&rdquo; but different &ldquo;traddr&rdquo; values - these are two paths to the same subsystem (i.e. 2 subsystem ports on two different target gateways). This is how the host discovers that multipath is available: multiple Discovery Log entries for the same NQN, each pointing to a different address.</p>
<h3 id="ana-asymmetric-namespace-access">ANA (Asymmetric Namespace Access)</h3>
<p>A NVMe-oF subsystem can often be reached through multiple paths. For example, a distributed storage system can expose the same subsystem through multiple target gateways. This gives the host more than one way to reach the same namespaces.</p>
<p>If every path were treated equally, the host would have no way of knowing which path provides the best access to a namespace. Some paths may provide direct access to the storage media, while some other path may require requests to be forwarded internally to another controller. Even though all paths can access the same data, one path might offer lower latency or higher throughput than the other.</p>
<p><strong>Core Idea - the why and how</strong></p>
<p>Each namespace belongs to an <strong>ANA Group</strong>. Instead of tracking the state of every namespace individually, ANA state is tracked at the group level, so all namespaces in the same ANA Group transition state together. This makes state changes much more efficient. Rather than updating thousands of namespaces individually during a controller failover, the target only updates <em>the state</em> of the affected ANA Group.</p>





<pre tabindex="0"><code>$ nvme id-ns /dev/nvme1n1
NVME Identify Namespace 1:
..
anagrpid: 1
..

$ nvme id-ns /dev/nvme1n2
NVME Identify Namespace 2:
...
anagrpid: 2
...

$ nvme id-ns /dev/nvme1n3
NVME Identify Namespace 3:
..
anagrpid: 1
..</code></pre><p>The host gets the ANA states by reading the &ldquo;<strong>ANA Log Page</strong>&rdquo;, which maps each ANA Group to its current ANA state. Since each namespace belongs to an ANA Group, the host immediately knows which paths should be used for every namespace.</p>
<p><strong>ANA states</strong></p>
<p>ANA group can have one of these 5 states:</p>
<ol>
<li>Optimized: The preferred path for accessing the namespace.</li>
<li>Non-Optimized: The namespace is accessible through this controller, but another controller provides better performance.</li>
<li>Inaccessible: The namespace cannot currently be accessed through this controller.</li>
<li>Persistent Loss: The controller has permanently lost access to the namespace.</li>
<li>Change: The ANA state is changing. The host should retrieve an updated ANA Log Page.</li>
</ol>
<p><strong>Example</strong>:</p>
<p>After connecting to a 2 NVMeoF gateway setup, a subsystem with 6 namespaces looks like this from the host:</p>





<pre tabindex="0"><code>$ nvme show-topology
nvme-subsys1 - NQN=nqn.2016-06.io.spdk:cnode1.mygroup1
               hostnqn=nqn.2014-08.org.nvmexpress:uuid:6def1fb3-a3a2-447b-9938-59856fc4805f
               iopolicy=numa
\
 +- ns 1
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420,src_addr=10.243.64.88 live inaccessible
 +- ns 2
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live inaccessible
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420,src_addr=10.243.64.88 live optimized
 +- ns 3
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420,src_addr=10.243.64.88 live inaccessible
 +- ns 4
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live inaccessible
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420,src_addr=10.243.64.88 live optimized
 +- ns 5
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420,src_addr=10.243.64.88 live inaccessible
 +- ns 6
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live inaccessible
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420,src_addr=10.243.64.88 live optimized</code></pre><p>Here, namespaces 1, 3, 5 are in one ANA Group - optimized through controller nvme1 (gateway 10.243.64.86), inaccessible through controller nvme2 (gateway 10.243.64.87). Namespaces 2, 4, 6 are in a second ANA Group - optimized through nvme2, inaccessible through nvme1.</p>





<pre tabindex="0"><code>Subsystem (nqn.2016-06.io.spdk:cnode1.mygroup1)
├── Controller 1 (nvme1)
│   ├── ANA Group 1 → Optimized
│   └── ANA Group 2 → Inaccessible
│
├── Controller 2 (nvme2)
│   ├── ANA Group 1 → Inaccessible
│   └── ANA Group 2 → Optimized
│
└── ANA Groups
    ├── ANA Group 1
    │   ├── Namespace 1
    │   ├── Namespace 3
    │   └── Namespace 5
    │
    └── ANA Group 2
        ├── Namespace 2
        ├── Namespace 4
        └── Namespace 6</code></pre><p><strong>What Happens During Failover?</strong></p>
<p>If second gateway (10.243.64.87) goes down, nvme2 becomes unreachable (path becomes from &ldquo;live&rdquo; to &ldquo;connecting&rdquo;). The first surviving gateway sends an &ldquo;Asymmetric Namespace Access Change Notice (AEN)&rdquo; to the host controller, indicating that the ANA state has changed. That triggers the host to issue &ldquo;Get Log Page&rdquo; command to get updated ANA Log Page.</p>
<p>With the updated ANA information, the surviving controller (nvme1) becomes the optimized path for both ANA Groups:</p>





<pre tabindex="0"><code>// after second gateway (10.243.64.87) goes down:

$ nvme show-topology
nvme-subsys1 - NQN=nqn.2016-06.io.spdk:cnode1.mygroup1
               hostnqn=nqn.2014-08.org.nvmexpress:uuid:6def1fb3-a3a2-447b-9938-59856fc4805f
               iopolicy=numa
\
 +- ns 1
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420 connecting inaccessible
 +- ns 2
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420 connecting optimized
 +- ns 3
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420 connecting inaccessible
 +- ns 4
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420 connecting optimized
 +- ns 5
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420 connecting inaccessible
 +- ns 6
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420 connecting optimized</code></pre><p>If we focus on namespace 2, the <code>nvme1</code> path changes from &ldquo;inaccessible&rdquo;:</p>





<pre tabindex="0"><code>// 2 gateways up:
 +- ns 2
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live inaccessible
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420,src_addr=10.243.64.88 live optimized</code></pre><p>to &ldquo;optimized&rdquo;:</p>





<pre tabindex="0"><code>// then after 1 gateway goes down:
 +- ns 2
 \
  +- nvme1 tcp traddr=10.243.64.86,trsvcid=4420,src_addr=10.243.64.88 live optimized
  +- nvme2 tcp traddr=10.243.64.87,trsvcid=4420 connecting optimized</code></pre><h1 id="references">References</h1>
<ul>
<li><a href="https://sniansfblog.org/a-beginners-guide-to-nvme/">https://sniansfblog.org/a-beginners-guide-to-nvme/</a></li>
<li><a href="https://blogs.oracle.com/linux/overview-of-nvme-architecture">https://blogs.oracle.com/linux/overview-of-nvme-architecture</a></li>
</ul>
<p>pcie:</p>
<ul>
<li><a href="https://www.synopsys.com/glossary/what-is-pci-express.html">https://www.synopsys.com/glossary/what-is-pci-express.html</a></li>
<li><a href="https://computer.howstuffworks.com/pci-express.htm">https://computer.howstuffworks.com/pci-express.htm</a></li>
</ul>
<p>nvme:</p>
<ul>
<li>Official NVMe Base spec: <a href="https://nvmexpress.org/wp-content/uploads/NVM-Express-Base-Specification-Revision-2.3-2025.08.01-Ratified.pdf">https://nvmexpress.org/wp-content/uploads/NVM-Express-Base-Specification-Revision-2.3-2025.08.01-Ratified.pdf</a></li>
<li><a href="https://www.techrepublic.com/article/nvme-a-cheat-sheet/">https://www.techrepublic.com/article/nvme-a-cheat-sheet/</a></li>
<li><a href="https://www.techtarget.com/searchstorage/feature/NVMe-market-and-technology-rapidly-evolve">https://www.techtarget.com/searchstorage/feature/NVMe-market-and-technology-rapidly-evolve</a></li>
<li><a href="https://www.everpuredata.com/knowledge/what-is-nvme.html">https://www.everpuredata.com/knowledge/what-is-nvme.html</a></li>
</ul>
<p>nvmeof:</p>
<ul>
<li><a href="https://codilime.com/blog/understanding-nvme-over-fabrics-nvmeof/">https://codilime.com/blog/understanding-nvme-over-fabrics-nvmeof/</a></li>
<li><a href="https://techbuzzonline.com/nvme-over-fabrics-nvme-of-networking/">https://techbuzzonline.com/nvme-over-fabrics-nvme-of-networking/</a></li>
<li><a href="https://nvme-storage.com/nvme-of/">https://nvme-storage.com/nvme-of/</a></li>
<li><a href="https://nvme-storage.com/glossary/nvme-of-discovery/">https://nvme-storage.com/glossary/nvme-of-discovery/</a></li>
<li><a href="https://simplyblock.io/blog/nvme-over-tcp-vs-iscsi/">https://simplyblock.io/blog/nvme-over-tcp-vs-iscsi/</a></li>
</ul>
<hr>
]]></content:encoded>
    </item>
  </channel>
</rss>
