<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="https://www.w3.org/2005/Atom" xmlns:sy="https://purl.org/rss/1.0/modules/syndication/" xmlns:dc="https://purl.org/dc/elements/1.1/" xmlns:content="https://purl.org/rss/1.0/modules/content/" xmlns:wfw="https://wellformedweb.org/CommentAPI/" xmlns:slash="https://purl.org/rss/1.0/modules/slash/"
  version="2.0">
  <channel>
    <title>Chen’s Thoughts and Experiments</title>
    <atom:link
      href="https://docs.microsoft.com/archive/blogs/chenfucsperfthoughts/feed.xml"
      rel="self"
      type="application/rss+xml" />
    <link>https://docs.microsoft.com/archive/blogs/chenfucsperfthoughts/feed.xml</link>
    <description>on Performance and Maintainability, in composing, testing and monitoring</description>
    <lastBuildDate>Wed, 17 Apr 2019 03:07:48 GMT</lastBuildDate>
    <language>en-US</language>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    <item>
      <title>Why Pay for Sorting if You Don’t Need it? (3 of 3)</title>
      <link>https://docs.microsoft.com/archive/blogs/chenfucsperfthoughts/why-pay-for-soring-if-you-dont-need-it-3-of-3</link>
      <pubDate>Mon, 12 Sep 2016 22:51:58 GMT</pubDate>
      <dc:creator><![CDATA[Chen's thoughts and experiments]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/chenfucsperfthoughts/?p=175</guid>
      <description><![CDATA[By Chen Fu, Tanj Bennett, Ashwini Khade
In our previous posts, we discussed the high cost of data...]]></description>
      <content:encoded><![CDATA[<p style="text-align: center">By Chen Fu, Tanj Bennett, Ashwini Khade</p>
In our <a href="https://blogs.msdn.microsoft.com/chenfucsperfthoughts/2016/09/12/sorting_cost/">previous posts</a>, we discussed the high cost of data sorting. Here we introduce Venger Index, an alternative option when you need memory efficiency but not data sorting.
<h1>1. Venger Index – Balancing Cost and Performance</h1>
Hash tables do not require data sorting, and are widely used in memory based data tables. Its use in disk or flash based data stores, however, are limited due to high memory cost. A hash table requires all keys to be in memory. When the key occupies a significant portion of the key-value pair, this cost becomes prohibitive. For instance, in a web document data store, when the URLs are used as keys, the key size can be more than one hundred bytes.

Our goal is to build an index that is at least as memory efficient as the LSM tree. We achieved the goal by modifying the hash table with the following key ideas:
<ol>
 	<li>
<div><em>Key Hashing</em>. We use md5 to hash original keys to 128 bit integers, same size with <a href="https://en.wikipedia.org/wiki/Globally_unique_identifier">GUID</a>. The number of unique keys are so large that the chance of collision can be ignored. We can also extend the key size from 128b to 192b or 256b, without affecting memory cost or performance of the index.</div></li>
 	<li>
<div><em>Key Folding</em>: In the index, we don't store the full 128b key, but a hashed partial key of 27 bit. This is cheating. We will explain later about how we get away with it, and why 27.</div></li>
 	<li><em>Hash Block Spilling</em>. To further save memory, we spill most of the hash entries from memory to the SSD, and only load them to memory when needed. Unlike memory, the main media is not suited for accessing of small items such as one hash entry. Thus we group hash entries into <em>Hash Blocks</em>.</li>
</ol>
Key folding and hash block spilling will cause performance degradation comparing with standard hash table. Next we explain the structure of the index in detail, and show how we effectively reduce memory usage while keep performance degradation in check.
<h2>1.1 Index Architecture</h2>
Venger index is designed for a <a href="https://en.wikipedia.org/wiki/Partition_(database)">partitioned data store</a>, assuming each machine has 50 to several hundred partitions. Each partition has an instance of the index and a data file. Each record in the data files has a header containing the 128b full key and size of the record. The index maps a record's key to its location in the data file. Each <em>hash entry</em> takes the form <code>&lt;partial_key, size, address&gt;</code>. In our first implementation, the size of this entry is 8 bytes, with 27-bit partial key, 3-bit size and 34-bit address.

The size field is a logarithmic scale representation of sizes from 2k to 128k. For larger records, we treat the sizes as 128k. Using log scale size means that when we read a record, we have to load more data in memory than necessary, causing read amplification, but no larger than 2. For records that are bigger than 128k, we read in 128k first, then consult the size field in the data header to determine how much we missed, and use a second read operation to bring it in. Since it is unlikely the SSD can read the whole 128k in one read operation, we are not increasing read amplification here.

34-bit address can be used to address a data file up to 16GB. If we force all record sizes to align to 64-bit boundary, 34-bit can address 16G number of 64-bit numbers, that's 128GB. With 300 partitions, for example, we can manage up to 38TB of data on a single machine.

We group a fixed number of hash entries into a <em>Hash Block</em>. Each hash block is guarded by one bloom filter and one <em>Shadow Key Block</em>. Currently we fit 512 entries into a hash block that is 4KB in size. One hash block with its corresponding bloom block and shadow key block, is called a <em>Hash Page</em>.

&nbsp;

<img src="https://msdnshared.blob.core.windows.net/media/2016/09/hash_page.png" alt="hash_page" class="alignnone wp-image-205" height="288" width="798" />

<span style="color: #44546a;font-size: 9pt"><em>Figure 3 Hash Page Structure
</em></span>

Figure 3 Illustrates how to insert a new 128b full key to a hash page. Two segments of the full key are passed into two hash functions, <em>BHash<sub>32</sub></em> and <em>PHash<sub>27</sub></em>, respectively. The first result <em>BHash<sub>32</sub>(128b_key)</em> is a 32-bit number used as the bloom key, and the second result <em>PHash<sub>27</sub>(128b_key) </em>is a 27-bit number used as the partial key. The bloom key is used to populate the bloom filter, and also saved in the <em>Shadow Key Block</em>. The partial key is inserted into the hash block. Here we try to keep the bloom key and the partial key independent from each other, so as to reduce the chance of partial key collision, which will be explained later in Section 1.5. Shadow key blocks are only used during index compaction, explained in Section 1.4.

As shown in Figure 4, each index instance consists of a number of hash buckets (e.g. 1024 for 10M keys). Each bucket is a linked list of hash pages. When 10M keys are divided into 1k buckets, each bucket has 10k keys, that is 20 hash pages since each page has 512 entries.

<img src="https://msdnshared.blob.core.windows.net/media/2016/09/091316_0551_WhyPayforSo6.png" alt="" />

<span style="color: #44546a;font-size: 9pt"><em>Figure 4 In Memory Index
</em></span>

This configuration still uses too much memory comparing with LSM trees. To further save memory, we spill most of the hash pages to SSD, only keep the head page in memory, as shown in Figure 5.

<img src="https://msdnshared.blob.core.windows.net/media/2016/09/091316_0551_WhyPayforSo7.png" alt="" />

<span style="color: #44546a;font-size: 9pt"><em>Figure 5 Spilling to SSD
</em></span>
<h2>1.2 Adding a new entry:</h2>
When a new Key-value pair enters the system, we need to create a new hash entry and insert it into our index. Overwriting of an existing key-value pair is achieved by simply adding a new version. Deleting is also achieved by writing a tombstone entry with the key and empty value. It will become clear after section 1.3, that our index support versioning naturally.

Steps:
<ol>
 	<li>Write the new key-value pair into the data file</li>
 	<li>Locate the correct bucket by hashing the full 128b key.</li>
 	<li>
<div>If the first hash block of the bucket is full,</div>
<ol>
 	<li>create a new hash page, insert it into the head of the linked list.</li>
 	<li>Kick off asynchronous spilling of the second page in the list.</li>
</ol>
</li>
 	<li>Generate keys and populate the keys in the first hash block, according to Figure 3</li>
</ol>
As we always insert the new entry in to the head page of the linked list, all the entries in a bucket is sorted chronically, with youngest entries near the head of the list. This makes query of the young records faster. Based on our observation, younger records tend to be queried more often than older ones.
<h2>1.3 Querying a Key</h2>
In this operation, the input is a key, and we need to return the location of the record that matches the key, or "not found" if there is no such key in the index.

First we locate the right bucket by computing the hash of the full 128b key. Next we compute the partial key and the bloom key the same way we do in Figure 3.

Then we traverse the list of hash pages trying to locate the right hash entry. For each hash page on the linked list, we first test the bloom key against the bloom block. If the bloom test is successful, we load the hash block to memory, and then search the block for an entry that matches the partial key, and return the location if found. "Not found" is returned if we reach the end of the linked list without finding a match.

Note that since the partial key produced by <em>PHash<sub>27</sub>(128b_key)</em> has only 27 bits, there are cases where multiple full keys are hashed into the same partial key. As a result, there are <em>false matches</em>, when the index returns a result based on partial key match, but the address does not point to the key asked by the client. The caller of this operation must verify the match by reading the full key from the SSD, and ask the index to <em>retry</em> in the case of a false match.

When the caller discovers a false match, it calls the index query API again with the fully key and a "previous address". The index would scan the list of pages again as described above, but this time go pass the entry with the "previous address", and then return the next match.

As shown in Section 1.2, hash entries are ordered chronically in the index. When there are multiple versions of the same key in the index, based on our query algorithm, younger entries are always returned first. However, the client has the option to search for older versions by using the retry mechanism.

There are two things that incur read amplification and hurt performance here. They are, 1) bloom filter false positive induced unnecessary hash block scan, and 2) hash collisions from <em>PHash<sub>27</sub>(128b_key)</em>, which require us to consult the data file repeatedly. Probability of these two can be reduced by increasing memory usage in the index. We will discuss in Section 1.5 how we precisely tradeoff between performance vs memory usage.
<h2>1.4 Relocation and Eviction</h2>
Garbage collection is necessary for all key-value store to rid the system of deleted or overwritten records. During garbage collection, the data file needs to rearrange records to allocate space for new coming records. Thus our index must support moving a record from one location to another (relocation), or removing a record (eviction).

For relocation, the caller needs to provide a key, its old location and new location. For eviction, the caller needs to provide a key and its old location. It is simpler to combine these two operations as one, with the latter providing an invalid new location.

Implementation is straight forward, with the full key, we can search the index the same way we described above. However, we won't have false match, as we match not only the partial key, but also the old location. The match is unique since the location is unique.

Once we load the hash block to memory, and located the hash entry with the matching partial key and the old location, we simply update that hash entry in place. And if the new location is invalid, we set the partial key to the invalid value too. This way we get rid of the key from the index. The block needs to be flush to SSD on a later time.

Whenever a hash entry is removed from the index, it leaves an invalid entry in the index, which takes up spaces. Periodical <em>compaction</em> is needed to get rid of invalid entries in the index itself. One way to do this is to create a new linked list of hash pages, copy all the valid entries over, and switch the head pointer of the bucket to point to the new list. When we compacting hash pages, we need to regenerate new bloom filters, using the bloom key saved in the Shadow Key Block (Figure 3). The is the only time when the shadow key blocks are needed in memory.

Note that a relocation operation needs to bring a hash block to memory for updating. And index compaction needs all the hash blocks and shadow key blocks of a bucket in memory. It would be more efficient if these two can be coordinated. When a data file garbage collection starts, it would call relocation multiple times, loading many hash blocks in memory. The index can just keep these blocks in memory until garbage collection finishes, then start index compaction right way. As soon as one bucket is compacted, most hash blocks and shadow key blocks in that bucket can be spilled to SSD.

It seems that we need to load all the hash blocks in memory during garbage collection, defeating all our memory saving efforts. However, since we are in a partitioned data store with more than 50 partitions in a single machine, each partition can take turns performing garbage collection. As long as we keep the number of concurrent garbage collection low, the memory saving is not negated. We will discuss our memory cost in detail next.

We store hash blocks and shadow key blocks on SSD. Index compaction would generate new blocks and makes old ones expire. This causes write amplifications. Suppose we allow 5% of hash entries to be invalid before starting index compaction, and each compaction would rewrite all the blocks on SSD, then the write amplification is 20. On the other hand, since hash entry only accounts for a very small fraction of the data (1% with 1k records, even smaller when records are bigger), damaging effect of the write amplification in the index is very small. We will discuss write amplification of data files later.
<h2>1.5 Performance vs. Memory Cost</h2>
As described previously, we save memory by sacrificing some performance, e.g. false matching caused by key folding. To ensure a good balance between performance and memory consumption, there are several parameters in the system that can be tuned, including number of buckets, size of the bloom filter and width of the partial key.

Now we use one example setting to illustrate how this is done. Suppose we need to have an index that holds up to 10M keys. We choose the number of buckets to be 1024, thus 10K keys per bucket. 512 hash entries per hash page, so about 20 hash pages per bucket.

As seen from section 1.3, we guard the loading and searching of hash blocks with bloom filters. When searching for a key, we could test 20 bloom filters, each with a false positive rate <em>p</em>. Suppose one goal is to have less than 1% of the queries to scan a hash block that does not contain the desired key. Then we need <em>p</em> to satisfy

<img src="https://msdnshared.blob.core.windows.net/media/2016/09/binomial.png" alt="binomial" class="size-full wp-image-215 aligncenter" height="82" width="281" />And we get <em>p &lt; 0.05%</em>. We can use a 1KB bloom filter (max 512 items) to achieve this. This way, 99% of the read requests can be satisfied by at most two reads to the SSD, one read to load a hash block, a second read to load the requested data record (except records larger than 128k). 99.995% of the read requests can be satisfied by at most 3 reads.

Now consider the probability of a false matching. Assuming we can find a good quality hash function (output follow uniform distribution) to produce the partial keys, then:

<img src="https://msdnshared.blob.core.windows.net/media/2016/09/collision.png" alt="collision" class="alignnone size-full wp-image-225 aligncenter" height="54" width="375" />
where <em>α</em> is the size of the partial key, and <em>β</em> is the number of hash entries checked in a query. Since hash blocks are guarded by bloom filters, 99.995% of queries only need to query 1 or 2 hash blocks. It is safe to assume that in vast majority of the cases, <em>β</em> to be smaller than 1024 (two hash blocks), in which case an <em>α</em> of 27 leads to a false match probability less than 0.01%.

Based on this example, in an index with 10M hash entries, we have 20k hash pages. We need 20k bloom filters in memory, occupying 20MB. Accounting for all the head pages of the buckets, we have 1K hash blocks and shadow key blocks in memory, occupying 6MB. That's 2.6 byte per key memory cost.

During garbage collection, the rest of the hash blocks are brought in memory, which is 76MB. For shadow key blocks used during index compaction, we only need one bucket at a time, 38KB. If we perform garbage collection on 2% of the data files, average cost is 1.5MB. Overall, Venger Index has a memory cost of 2.8 byte per key.

Note that most of the memory cost comes from the bloom filters, which we use just the standard implementation. Replacing it with more advanced implementations such as <a href="https://homepages.dcc.ufmg.br/~nivio/papers/iadis-www-internet11.pdf">perfect hashing</a> would lead to even more memory saving without sacrificing performance.
<h1>2. Data Files</h1>
Based on the description above, the Venger Index only requires two things from the data file, 1) 34b addressable, and 2) to be notified of garbage collection start and end. It does not need to know how data are organized inside a data file, leaving a lot of room to improve write amplification. Below are two example implementations of data files.
<h2>2.1 Circular Data File</h2>
A simple layout is a logical circle. A continuous portion of the circle contains the data, as shown in Figure 6. New records are always appended at the tail end. A garbage collector reads from head of the data, discarding expired or deleted records, and rewrite the alive records at the tail end. As both the new data and the old data are appended at the tail end, the garbage collector slowly mixes new data with old, like kneading a dough. Overtime, different age records are distributed at different places on the circle.

<img src="https://msdnshared.blob.core.windows.net/media/2016/09/091316_0551_WhyPayforSo8.png" alt="" />

<span style="color: #44546a;font-size: 9pt"><em>Figure 6 Circular Data File
</em></span>

Write amplification induced by the garbage collector depends on portion of the obsolete data in the data file. If we allow 10% of obsolete data in the store, assuming the data file is 10G, then one scan of the entire file rewrites 9G of the existing data, to make room for 1G of new records, resulting a write amplification of 10. Obviously more extra space would result in lower write amplification.
<h2>2.2 Multi-Stream Data File</h2>
A problem with the circular file is mixing of records with different time to live (TTL). The garbage collector has to constantly rewrite living records while discarding dead ones. If we know the TTL of each record, then we can group data records together that are likely to die at the same time, thus further reduce write amplification.

<img src="https://msdnshared.blob.core.windows.net/media/2016/09/091316_0551_WhyPayforSo9.png" alt="" />

<span style="color: #44546a;font-size: 9pt"><em>Figure 7 Multi-Stream
</em></span>

We are implementing a new multi-stream data file, illustrated in Figure 7. Here we assume that we know the TTL for each record; delete and overwrite (operations that shorten TTL unexpectedly) are rare, or mostly happen to fresh data.

Here a single data file is split into multiple logic streams. Each stream is a list of physical SSD pages forming a logical belt that can grow and shrink dynamically. The tail page can be in memory to avoid fragmentation. The new records are appended to the tail end of the fresh stream. A garbage collector picks records from the head of the fresh stream, and append them to a stream's tail end based on the record's remaining TTL. It also discards expired records from each streams' head, freeing up spaces. Except for the last stream, where alive records are again appended to the tail.

This way, we use the fresh stream to catch deletions and overwrites to fresh records. And for records that survive fresh stream, and live shorter than two weeks, we have write amplification only 2. A long living record are rewritten once every two weeks. For data sets with no deletion or overwriting (e.g. event logs), we can skip the fresh stream and get rid of write amplification.
<h1>3. Conclusion</h1>
Here we present Venger Index, an alternative option to LSM tree for a storage system, when memory efficiency is required, but data sorting is not. Removing data sorting leads to reduced I/O cost, and prolonged SSD life. At the same time, hash based index can effetely reduce load unbalancing, a valuable property in large scale distributed storage systems.

Venger Index is implemented as part of <a href="https://github.com/Microsoft/ExaStore/">Exabyte Store</a>, open sourced Key-Value pair storage engine.]]></content:encoded>
    </item>
    <item>
      <title>Why Pay for Sorting If You Don't Need It? (2 of 3)</title>
      <link>https://docs.microsoft.com/archive/blogs/chenfucsperfthoughts/lsm_compaction</link>
      <pubDate>Mon, 12 Sep 2016 15:59:48 GMT</pubDate>
      <dc:creator><![CDATA[Chen's thoughts and experiments]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/chenfucsperfthoughts/?p=45</guid>
      <description><![CDATA[In our last blog post, we briefly discussed high cost of LSM Trees. Here we introduce how data are...]]></description>
      <content:encoded><![CDATA[In our <a href="https://blogs.msdn.microsoft.com/chenfucsperfthoughts/2016/09/12/sorting_cost/">last blog post</a>, we briefly discussed high cost of LSM Trees. Here we introduce how data are organized in LSM trees and the cost induced.
<h3>LSM Tree Compaction</h3>
In a <a href="https://en.wikipedia.org/wiki/Log-structured_merge-tree">LSM tree</a>, data are divided into multiple <em>levels</em>.  New records are inserted into a memory buffer where all records are stored in order. When the buffer is full, content of the buffer is written to SSD or hard disk as a file in Level 0 (L<sub>0</sub> for short). As a result, files in L<sub>0</sub> have overlapping key ranges.

From L<sub>1</sub> onward, each entire level is a sorted run, with files within a level having non-overlapping key ranges. Figure 1 is from a <a href="https://www.percona.com/live/data-performance-conference-2016/sites/default/files/slides/RocksDB_Siying_Dong.pdf">presentation about RocksDB</a>, illustrating leveled storage and compaction. When the total size of the data in L<sub>i</sub> exceeds a threshold, a file from L<sub>i</sub> is removed from that level and merged with multiple files in L<sub>i+1</sub> using   merge sort. This operation is called a <em>compaction</em>.  Unlike other levels, in a compaction from L<sub>0</sub> to L<sub>1</sub>, all the files in L<sub>1</sub> must participate in the compaction.

<img src="https://msdnshared.blob.core.windows.net/media/2016/09/image001-300x131.png" alt="image001" width="300" height="131" class="alignnone size-medium wp-image-75" />

<span style="color: #008080"><em>Figure 1: LSM Level Based Compaction</em></span>

Deciding how many levels to use is a delicate balance. If the number of levels is too small, then one file in L<sub>i</sub> has to be merged with too many files in L<sub>i+1</sub>. This increases the amount of computation and write amplification in a single compaction, pushing performance variance to an extreme. On the other hand, If the number of levels is too large, it hurts read performance, as a read may need to check all the levels to find a key.

<img src="https://msdnshared.blob.core.windows.net/media/2016/09/image003-281x300.png" alt="image003" width="281" height="300" class="alignnone size-medium wp-image-85" />

<span style="color: #008080"><em>Figure 2: Compaction Induced Write Amplification</em></span>

Figure 2 also comes from the same <a href="https://www.percona.com/live/data-performance-conference-2016/sites/default/files/slides/RocksDB_Siying_Dong.pdf">presentation about RocksDb</a>, giving us a rough estimate about write amplification. Each compaction has a 10 time blow up. When all levels are close to full, adding one file into L<sub>0</sub> would induce cascading compaction operations on each level, resulting in more than 40 write amplifications.

And then there are read amplifications, which means the data store reads in more data than what’s requested by the client. To serve a read request, an LSM may needs to check all the levels to find the key. This may involve reading multiple files. Using of bloom filter can reduce number of file scans, with increased memory consumption. Each file is organized as a B-tree. It may take multiple reads to load the requested record.

Moreover, there are compaction induced reads. The 40 times blow up of writes are accompanied by same number of reads. Read amplification does not hurt SSD life. However, it hogs up SSD and bus bandwidth, amplifies the performance variance, and reduces the total IOPS available to the client.

Keep in mind that all these extra read and write not only cost IOPS, but also CPU and memory. And they are caused by the need to keep data sorted.

<span style="font-size: 16px;line-height: 1.5">In the </span><a href="https://blogs.msdn.microsoft.com/chenfucsperfthoughts/2016/09/12/why-pay-for-soring-if-you-dont-need-it-3-of-3/">next blog post</a><span style="font-size: 16px;line-height: 1.5">, we explain in detail how to construct a memory efficient index, to provide an option to avoid the cost of data sorting.</span>]]></content:encoded>
    </item>
    <item>
      <title>Why Pay for Sorting If You Don't Need It? (1 of 3)</title>
      <link>https://docs.microsoft.com/archive/blogs/chenfucsperfthoughts/sorting_cost</link>
      <pubDate>Mon, 12 Sep 2016 09:34:53 GMT</pubDate>
      <dc:creator><![CDATA[Chen's thoughts and experiments]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/chenfucsperfthoughts/?p=35</guid>
      <description><![CDATA[Log-structured Merge-trees (or LSM trees) are used by many famous data stores, such as BigTable,...]]></description>
      <content:encoded><![CDATA[<a href="https://en.wikipedia.org/wiki/Log-structured_merge-tree">Log-structured Merge-tree</a>s (or LSM trees) are used by many famous data stores, such as <a href="https://en.wikipedia.org/wiki/BigTable">BigTable</a>, <a href="https://en.wikipedia.org/wiki/HBase">HBase</a>, <a href="https://rocksdb.org/">Rocks DB</a>, <a href="https://cassandra.apache.org/">Cassandra</a>, etc. LSM trees provide desirable features such as memory efficiency, quick updating, and range query, i.e. access to all the records in a key range, according to one predefined total order of the keys.

Unfortunately, LSM trees can be costly. They require periodical data sorting, which causes high write amplification that shortens SSD life.  Write Amplification means that in order to make room to store <em>k</em> bytes of data, the system moves <em>k * n</em> bytes of other data, resulting an amplification factor of <em>n+1</em>. A SSD’s <a href="https://itblog.sandisk.com/ssd-endurance-speeds-feeds-needs/">lifetime</a> is dictated by the number of write operations it can endure. As a result, higher write amplification leads to shorter replacement cycle.

Sorting may amplify write by <a href="https://www.flashmemorysummit.com/English/Collaterals/Proceedings/2014/20140805_D12_Dong.pdf">40 times</a> when space is tight. At the same time, the amplified SSD writes are accompanied by roughly same number of SSD reads, leading to total amplification of 80, leaving only 1.25% of the I/O bandwidth to clients. Over-provisioning in SSD space and I/O bandwidth is required to avoid the problem.

Furthermore, according to a <a href="https://www.usenix.org/sites/default/files/conference/protected-files/hotstorage14_slides_kang.pdf">Samsung study</a>, write patterns of the LSM tree causes <a href="https://en.wikipedia.org/wiki/Write_amplification">SSD fragmentation</a>, leading to more write amplification and performance degradation in the SSD device level.

The cost of data sorting is high. On the other hand, some may argue that sorting is necessary to achieve high memory efficiency. After all, LSM trees do not have to keep all keys in memory because data are stored in order. It seems that we have to take memory efficiency and data sorting as an all or nothing package.

Or do we?

Introducing <a href="https://blogs.msdn.microsoft.com/chenfucsperfthoughts/2016/09/12/why-pay-for-soring-if-you-dont-need-it-3-of-3/"><em>Venger Index</em></a>, sorting not required, yet memory efficient. It is a hash based index that maps a key to the corresponding record’s location on SSD. It uses less than 3 bytes of DRAM per key. More importantly, since data records do not have to be stored in order, we can experiment with different data layouts, and reduce write amplification by 70% comparing to LSM trees with similar space over provisioning. Not having to worry about record ordering, we have more freedom in taking advantage of <a href="https://events.linuxfoundation.org/sites/events/files/slides/LightNVM-Vault2015.pdf">open channel SSD</a> or <a href="https://www.samsung.com/semiconductor/global/file/insight/2015/12/0_storage-intelligence-prodoverview-2015-0.pdf">multi-stream SSD</a> interfaces, reducing write amplification to 1 to 2, and potentially removing SSD fragmentation.

Venger Index is best suited for distributed flash stores require low latency and low cost, with average record size 1KB or bigger. It does not support range query, which can be implemented by maintaining indexing data structure, e.g. B-tree, separately. However, this feature is better to be implemented by an upper layer who understands the semantics of the data, so as to avoid <a href="https://ria101.wordpress.com/2010/02/22/cassandra-randompartitioner-vs-orderpreservingpartitioner/">load balancing problems in distributed store</a>, and to avoid imposing the cost on data sets that do not need this feature.

In our next posts we explain why <a href="https://blogs.msdn.microsoft.com/chenfucsperfthoughts/2016/09/12/lsm_compaction/">LSM tree compaction is costly</a>, and how we build <a href="https://blogs.msdn.microsoft.com/chenfucsperfthoughts/2016/09/12/why-pay-for-soring-if-you-dont-need-it-3-of-3/">Venger Index</a> to achieve low cost and high performance.]]></content:encoded>
    </item>
    <item>
      <title>Threads, Events, and Mala (in C++)</title>
      <link>https://docs.microsoft.com/archive/blogs/chenfucsperfthoughts/between-threads-and-events</link>
      <pubDate>Fri, 19 Aug 2016 17:56:17 GMT</pubDate>
      <dc:creator><![CDATA[Chen's thoughts and experiments]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/chenfucsperfthoughts/?p=1</guid>
      <description><![CDATA[By Chen Fu and Tanj Bennett
When it comes to constructing high performance server applications,...]]></description>
      <content:encoded><![CDATA[<p style="text-align: center">By Chen Fu and Tanj Bennett</p>
When it comes to constructing high performance server applications, there is a decade long debate between <a href="https://www.usenix.org/legacy/events/hotos03/tech/full_papers/vonbehren/vonbehren_html/">threads</a> vs <a href="https://web.stanford.edu/~ouster/cgi-bin/papers/threads.pdf">events</a>. And it is not getting old.  Here we examine the story from both sides, and explore how back end server programmers can get the advantages from both sides, using an open source library <a href="https://github.com/Microsoft/mala">Mala</a>.
<h3>Thread-per-Request</h3>
"Threads" people usually assigns a single thread to process a client request. Accessing to shared resources, such as a remote database or a local memory table, can be wrapped behind blocking interfaces to hide the synchronization details. As a result, this approach provides an intuitive sequential programming model, and helps programmers avoid the headache of parallel programming.

The "Event" people like to accuse their "Threads" friends for being wasteful. Using blocking interfaces means a thread is suspended while waiting for completion. To keep the CPU busy, the system has to maintain a big thread pool to process lots of requests in parallel.  It is difficult to tune the size of the thread pool, since the portion of suspended threads varies. To avoid the sin of letting CPU idle and letting clients wait, programmers tend to allocate thread pools larger than necessary.

The cost of a big thread pool can not be ignored. Each thread has 2MB user space stack and more in kernel space. A context switch takes a few microseconds, a non-trivial cost, considering modern I/O devices provide response in tens of microseconds. And the cost of thread scheduling increases with the total number of threads in the system.
<h3>Events</h3>
In event driven systems, the request processing logic are broken into multiple event handlers (aka. callback functions). Examples of events include completion of disk I/O, arrival of packets over the network. Whenever an event is raised, the corresponding handler is dispatched to run in a thread pool. An event handler may raise other events, or call async I/O operations that would raise events when they finish.

Aha! So these guys also use a thread pool! But the size of this thread pool is usually equals to the number of CPU cores, since event handlers never block. Smaller thread pool leads to smaller memory overhead and less number of context switches.

On the other hand, the programming model of the event driven systems is not intuitive. It forces programmers to split naturally connected logic into multiple functions at the boundary of I/O operations, hiding explicit event sequences and reducing readability along the way. As a result, event driven systems are harder to write and maintain.

Then there is concurrency to deal with. Let's say you need to read multiple files and dump their content to the console output. So you wrote a loop that issue async read on each file and pass the same call back function <code>Output</code> that dumps the data to console, like the code below.  When you run the code, content of multiple files are mingled together, as multiple instances of <code>Output</code> function are run in parallel.
<pre><span style="color: #008000">// Dump data to console
</span><span style="color: #0000ff">void</span> Output(<span style="color: #0000ff">void</span>* <span style="color: #808080">buffer</span>, <span style="color: #0000ff">int</span> <span style="color: #808080">length</span>)
{
  <span style="color: #0000ff">char</span>* ptr = <span style="color: #3366ff">reinterpret_cast</span>&lt;<span style="color: #0000ff">char</span>*&gt;(<span style="color: #808080">buffer</span>);
  <span style="color: #0000ff">for</span> (<span style="color: #0000ff">int</span> i=0; i&lt; <span style="color: #808080">length</span>; i++){
    cout &lt;&lt; *(ptr++);
  }
} 

<span style="color: #0000ff">void</span> DumpFiles(<span style="color: #008080">vector</span>&lt;<span style="color: #008080">string</span>&gt; <span style="color: #808080">allFiles</span>)
{
  <span style="color: #0000ff">char</span> buffer[BIG_ENOUGH]; 
  <span style="color: #0000ff">for</span> (<span style="color: #0000ff">auto</span>&amp; filename : <span style="color: #808080">allFiles</span>) {
    asyncReadFile(filename, buffer, &amp;Output);
    <span style="color: #008000">// Output function will be triggered after file read complete
    // multiple file read may finish around the same time,
    // causing multiple instances of Output to run in parallel
</span>  }
}
</pre>
Looks like the low runtime overhead of the event-driven systems does not come free either.
<h3>C++ Lambda</h3>
C++11 introduced Lambda expressions. Now we can write anonymous function objects right where it is passed into an async operation as a call back. It would have been great if we could use Lambda expressions this way to specify the sequence of event directly in code layout, almost like in thread based system.

Ideally we can write something like the following, where the code of writing file content to the console is right after invocation of an async read. Semantically connected code are placed next to each other, making it easier to read.
<pre><span style="color: #0000ff">void</span> DumpFiles(<span style="color: #008080">vector</span>&lt;<span style="color: #008080">string</span>&gt; <span style="color: #808080">allFiles</span>)
{
   <span style="color: #0000ff">char</span> buffer[BIG_ENOUGH]; 
   <span style="color: #0000ff">for</span> (<span style="color: #0000ff">auto</span>&amp; filename : <span style="color: #808080">allFiles</span>) {
      asyncReadFile(filename, buffer, 
          [filename&amp;]
          (<span style="color: #0000ff">void</span>* <span style="color: #808080">buffer</span>, <span style="color: #0000ff">int</span> <span style="color: #808080">length</span>){
              <span style="color: #008000">// logic for dumping data here</span>
          });
   }
}</pre>
Unfortunately, the above code does not work. The lambda expression is a function object allocated on the stack of the function <code>DumpFiles</code>. When the async file read finishes, chances are <code>DumpFiles</code> already returned, the function object is not there anymore, causing a memory corruption problem. So Lambda expressions can not be used directly as event handlers.

Plus you still have to deal with concurrency.
<h3>Mala, strung beads</h3>
To reduce the headache of parallel programming, we want a certain set of semantically related event handlers to execute sequentially. This does not necessarily mean they have to be executed on a single thread. Sequential execution is a requirement. Single threaded execution, on the other hand, is only one implementation of this requirement, a pretty restrictive and costly one. We believe there are more efficient implementations.

Mala, or prayers beads, is a bunch of beads strung together, like a necklace. We use it to name our <a href="https://github.com/Microsoft/mala">open source thread library</a> that supports async sequential execution.

In Mala, our event handlers are called <em>Continuations</em>, each of which is a wrapper of a function object. Each <em>Activity</em> has multiple continuations. All continuations in one activity are guaranteed to execute sequentially, with no overlap. An activity acts like a string that connects multiple beads (continuations) together sequentially, forming a mala. Our implementations takes care of the memory management of lambda expressions and synchronizations, so that the programmers can write event-driven systems in a more intuitive way.

The code below illustrates how the dump files example can be implemented using Mala. The key here is function <code>lambdaContinue</code>. It takes two parameters. The first parameter is an activity, the second is a lambda expression. It returns a continuation that belongs to the activity in the first parameter. When this continuation is triggered, it executes the lambda expression.
<pre><span style="color: #0000ff">void</span> dumpFiles(<span style="color: #008080">vector</span>&lt;<span style="color: #008080">string</span>&gt; allFiles, <span style="color: #008080">Scheduler</span>&amp; scheduler)
{
   <span style="color: #008000">// create a "string" to string all the beads</span>
   <span style="color: #0000ff">auto</span> pActivity<span> = </span><span class="pl-c1">ActivityFactory</span><span>(scheduler, </span><span class="pl-s" style="color: #800000"><span class="pl-pds">"Test Activity</span><span class="pl-pds">"</span></span><span>);

   <span style="color: #008000">// context shared by all the beads</span>
</span>   <span style="color: #0000ff">auto</span> pFilesRemain = <span style="color: #0000ff">new</span> <span style="color: #0000ff">int</span>(allFiles.size());
   <span style="color: #0000ff">for</span> (<span style="color: #0000ff">auto</span>&amp; filename : allFiles){
      <span style="color: #008000">// open file</span>
      <span style="color: #0000ff">auto</span> f = AsyncFileFactory(filename, scheduler);
      <span style="color: #0000ff">char</span>* readbuffer = <span style="color: #0000ff">new char</span>[f.GetSize()];
      f-&gt;Read(0, f.GetSize(), readbuffer, <span style="color: #008000">// read entire file to buffer
</span>
         lambdaContinue(  <span style="color: #008000">// create a bead as call back</span>
            *pActivity,   <span style="color: #008000">// all the beads are sequentialized by pActivity</span>
            [pActivity, pFilesRemain, f, readBuffer]
         (<span style="color: #008080">WorkerThread</span>&amp;, <span style="color: #008080">intptr_t</span> status, <span style="color: #008080">uint32_t</span> length)
         {
            <span style="color: #0000ff">if</span> (status != S_OK){
               cerr &lt;&lt; <span style="color: #800000">"File read error " <span style="color: #333333">&lt;&lt; f.GetName() &lt;&lt;</span> " \n"</span>;
            }
            <span style="color: #0000ff">else</span> {
               cout &lt;&lt; <span style="color: #800000">"==== File Start: "</span> &lt;&lt; f.GetName() &lt;&lt; <span style="color: #800000">"====\n"</span>;
               <span style="color: #0000ff">for</span> (<span style="color: #0000ff">int</span> i=0; i &lt; length; i++){
                  cout &lt;&lt; readBuffer[i];
               }
               cout &lt;&lt; endl;
            }
            <span style="color: #0000ff">delete</span> readBuffer;
            f-&gt;Close();
            
            <span style="color: #0000ff">if</span> (--(*pFilesRmain) == 0){
               <span style="color: #0000ff">delete</span> pFilesRemain;
               pActivity-&gt;Shutdown();
            }
         })); <span style="color: #008000">// end of the bead
</span>
   } <span style="color: #008000">// end for each file</span>
}

</pre>
In the code above, <code>lambdaContinue</code> enables the lambda expression to live beyond the scope of <code>dumpFiles</code> function, and specifies all of the runtime instances execute sequentially, as they belong to the same activity.  This way, contents from different files will not be mingled together.

A continuation can also be triggered by invoking <code>pContinuation-&gt;Post()</code>. The continuation will be executed when all the previously triggered continuations in the same activity finish. This provides a way for one activity to send a notification to another activity, by posting a continuation of the second activity.

We hope Mala helps you in making event-drive code easier to write and read. We welcome your feedback to the library and invite you to contribute.]]></content:encoded>
    </item>
  </channel>
</rss>