Wednesday, February 17, 2016

HBase - Compression vs Block Encoding

By Lars Hofhansl

(Updated on February 28th, 2016, to correct spelling errors and clarifications, and to add section about single threaded scanning)

HBase has many options to encode or compress that data on disk.

In this excellent blog post Doug Meil and Thomas Murphy outline the effects of block encoding and compression on the storage footprint.

In this post I will explore the effects of encoding and compression options on (read) performance. I performed the Scan performance tests with the excellent Apache Phoenix  the relational layer over HBase. The Get tests use a straight HBase client, without Phoenix.

Background


In short:
Block encoding refers to ability to encode KeyValues or Cells on-the-fly as blocks are written or read, exploiting duplicate information between consecutive Cells. The main advantage is that the blocks can be cached in encoded form and decoded as needed in a streaming way. The encodings used most commonly in HBase are FAST_DIFF and (to a lesser extent) PREFIX encoding.

Compression here means full compression of HBase blocks using SNAPPY, GZIP, and others. Newer versions of HBase have the ability cache block in compressed form, but I did not test this here.

(Compression and block encoding for HBase are described in more detail here.)

The most important property to note is that the cost of decompression is proportional to the number of blocks that have to be decompressed, whereas the cost of decoding is proportional to the number of Cells visited. Keep that in mind when looking at the numbers below.


The Setup


For my test I created Phoenix tables of with the following schema:

create table test (pk integer primary key, v1 float, v2 float);

Then I seed the pk from a sequence, and v1/v2 with random values like so:

upsert into test values(next value for pkey, rand(), rand());

followed by a few (doubling the size of the table each time):

upsert into test select next value for pkey, rand(), rand() from test;

This data set, except for the monotonically increasing key, would not compress well.

With that I created tables with 32m rows, or 128m Cells altogether (including the one that Phoenix creates for book-keeping), on a single machine.

Let's first look at the footprint on disk:

 595MB GZIP
 875MB FAST_DIFF
 993MB SNAPPY
1739MB PREFIX
2922MB NONE

So, the first take away: You have to use some kind of compression or encoding. Since every column needs to be stored as its own cell it clocks in with about 24 bytes per column uncompressed - or about 100 bytes per "row". (In a relational database with a dense row format that entire row would only take about 12 bytes.)

I tested the following four scenarios:
  • 12 core machine, 3GB ram, 4 DDR3 memory channels
  • from disk (7200rpm drive, 8ms seek time or about 125 IOPS, about 120MB/s)
  • from SSD (about 800MB/s, about 2500 IOPS)
  • from OS cache
  • from HBase cache

The region server was configured with:
-Xmn2g -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCompressedOops -XX:+UseCMSInitiatingOccupancyOnly -XX:MaxGCPauseMillis=5000 -XX:GCTimeLimit=90 -XX:GCHeapFreeLimit=10

Short Circuit Reads were enabled for HDFS.

Now let's do some scanning


In order to avoid caching in HBase I used the NO_CACHE hint in Phoenix that I added a while ago (PHOENIX-13)

 select /*+ NO_CACHE */ count(*) from test;
   (This will actually issue 11 scan requests to HBase in parallel)

I cleared the OS Cache with

 echo 3 | sudo tee /proc/sys/vm/drop_caches



We can see that reading from spinning disk is (not surprisingly) dominated by the amount of data to be read. Extra CPU time consumed by compression is negligible, we are clearly IO bound.
We also see that in all cases - even GZIP - HBase was able to push the drive close to it's limit. In fact in all cases I saw 90-95MB/s read from disk, the performance gain from compression is close to proportional to the compression ratio.

Let's look at the same data without the HDD numbers to see better what's going on:



Here we see the fact the decoders incur per Cell overhead, the HBase block cache does not help for FAST_DIFF or PREFIX. For PREFIX encoding the HBase block cache actually does worse than the OS Cache, that is something I need to investigate further.

We also see that for Scans the block cache has no/little advantage over the OS cache, or even reading straight form SSD - and in fact HBase wasn't able to keep the SSD fully busy. In other words for SSDs HBase scans are CPU bound. This is interesting, I will also investigate this part further.

All schemes except FAST_DIFF/PREFIX store the blocks in uncompressed form (note that newer version of HBase have the option to cache blocks in their compressed form, I did not test that). In this case the uncompressed data fits in the block cache, so it's not entirely fair to FAST_DIFF and PREFIX.

So use FAST_DIFF only when you expect your working set to fit in the aggregate block cache when compressed, but not uncompressed.

We can now also see the relative CPU cost of the compression. FAST_DIFF is actually worst(!), followed by GZIP, PREFIX, and then SNAPPY, which is most efficient.
GZIP consumed to most CPU to decompress a block, but after decompressing a block scanning goes through the block without any extra work.

Since for scans the HBase block cache generally does not buy much over Linux' disk block caching ("OS Cache" in the graph) as we're CPU bound anyway; in these situations it might be better to disable cacheBlocks (setCacheBlocks(false)) on the Scan object - even when the entire scanned data would fit into the HBase block cache.

February 28, 2016

Note that Phoenix uses parallel scans across regions and uses equal-width "guide posts" to guide how Phoenix should parallelize the work across HBase region servers. In the Scan examples Phoenix chose 11-way parallelization to execute the queries. I have since added a SERIAL hint to Phoenix to force serial execution of a query. See PHOENIX-2697.

How does performance look like when all scanning is single threaded?
Now this is interesting. Without compression we still run into the spinning disk limit throughput limit. Everything else is mostly CPU bound.

Side note, memory bandwidth:

In the next chart I mapped the scan throughput out of the OS cache seen by 1 thread and by 11 threads. (I used the OS cache, and not the HBase block cache, as this shows the internal end-to-end performance of HBase.):

MB/s is the actual throughput seen out of the OS cache. "raw" indicates the throughput in terms of uncompressed data. We see that the throughput in terms of raw uncompressed bytes is very similar across all schemes.

We run into some internal HBase/Phoenix limit around 710MB/s.

Now look at the single threaded performance!
The fastest is (of course) uncompressed scanning, which utilizes about 168MB/s of the available memory bandwidth. It seems we need throw a lot of cores at the problem to utilize the available memory bandwidth. Luckily Apache Phoenix does that automatically where possible; but this is still something to look in HBase land.

In order to put this into context... According to spec this machine has 4 DDR memory channels for a total maximum bandwidth of 51.2 GB/s.

I wrote a simple bandwidth tester in C++. It allocates 1GB of memory and then accesses it sequentially as bytes or as 64bit longs. (for good measure I wrote the same in Java, and came to the same numbers, within 10%). Please note that I am not hardware guy.

Here's what I measured (bandwidth, GB/s):
Measured B/W1 process4 processes
long13.342.6
byte2.249.1

So the HBase numbers are quite a bit lower. We see that the maximum throughput (710MB/s) is close 4x the single threaded throughput (168MB/s), indicating that this is probably related to memory throughput and not CPU cycles (we should have seen an 11x speed up, the machine has enough cores).

Now, this is from the Linux block cache, so we need account for the fact that the blocks are copied at least 2 times (into a direct byte buffer, from there into a byte[]), but since the block cache is only marginally better, this looks to be all internal Phoenix and HBase friction.

OK. That was scanning. Let's try some random gets.


I wanted to explore the decompression cost more. Here I wrote a little tool that issues 1000 random gets. Now each get will incur a block read followed by decompression (if not in the block cache), or a seek + decoding.

The numbers from HDDs are absolutely dominated by the seek time. In all cases this was close to 6.2s, which is close to what you would expect from doing 1000 seeks in 7200rpm drives (actually you would expect 8s for 1000 Gets, presumably it's a little faster, since some gets might hit the same blocks again).
Use SSDs for random Gets, or make sure that your working set fits into the OS cache or block cache. Otherwise each Get will require HBase to perform a disk seek (about 4ms for 15k drive, 8ms for 7.2k dive, 12ms for 5k drive).




So here we see the cost of the decompression again. This time HBase needs to load and decompress a block for each get - unless that block is found in the HBase block cache. We can estimate that the "seek time" on the SSD is about 0.4ms, or that the drive can do about 2500 IOPS (which does seems low).

We also see that FAST_DIFF is not doing as well with the data in block cache, again because it's the only scheme that stores blocks in their compressed form - and the cost of decoding is proportional to the number of Cells traversed.

When the data is on SSD or in the OS Cache, we do see that encoders are doing slightly better. They do not have to decode the entire block, but only the portion needed to decode the Cells we're interested in.

Most prominently for Gets we do see the value of the OS Cache and especially the HBase block cache - assuming the working set will likely fit into the aggregate block cache across the cluster.

TL;DR:

  1. Not using compression or encoding is not an option. I think HBase should not even allow that. Both SNAPPY and FAST_DIFF are good all around options.
  2. If you regularly scan large data sets from spinning disk, you're best of with GZIP. (but watch write speed, which I haven't tested here)
  3. For large scans, consider not using the HBase block cache. (Scan.setCacheBlocks(false)), even if the whole scan could fit into the block cache.
  4. If you mostly perform large scans you might even want to consider running HBase with a much smaller heap and size the block cache down, to only rely on the OS Cache. This will alleviate some garbage collection related issues.
  5. We need to throw a lot of cores at a Scan to utilize the available memory bandwidth. When spec'ing machines for HBase, do not skimp on cores, HBase needs those. Apache Phoenix makes it easy to utilize more cores to increase scan performance.
  6. If there is a chance that your data set would fit into the block cache, FAST_DIFF is a good option. It will allow more data to fit into the block cache, since the data is cached in its encoded form.
  7. While for Scans the HBase block cache shows fairly little advantage, for Gets it is quite important to have your data set cached.
  8. If you do lots of random gets, make sure you use SSDs, or that your working set fits into RAM (either OS cache or the HBase block cache), or performance will be truly terrible.
What I didn't test:
  1. CPU constrained environments.
  2. Write performance
What I also didn't test:
  • Multiple different scans in parallel. I'd expect the cached scenarios would behave similarly w.r.t. each other. Obviously scanning and Gets from rotating disks will do much worse.

Friday, January 22, 2016

HBaseCon 2016 is on!

HBaseCon 2016 is on!

I am lucky enough to be on the Program Committee again this year.

HBaseCon will take place May 24, 2016 at
The Village
969 Market St.
San Francisco, CA 94103

The Call For Papers is open. If you have anything interesting to say about HBase, what you do with HBase, what other technologies you use with HBase, how you solved XYZ with HBase, how you hacked HBase, what we should improve in HBase, etc, etc, and want to talk about it at HBaseCon please let us know.

The CFP closes Feb 28th.

Monday, December 21, 2015

Yet more on HBase GC tuning with CMS

By Lars Hofhansl
  
Some of my previous articles delve into gc tuning and
more tuning for scanning.

We have since performed more tests with heavy write loads and I need revise my previous recommendation based on the findings.

I wrote a simple test tool that generates 5 million random keys of approximately 200 bytes; then it starts 50 threads that each pick 100k random batches of these keys and write them to HBase. I then start multiple of these and point them to an HBase cluster.
The result is that we see a lot of churn in HBase as new versions of Cells are written but the overall size of the data is kept more or less constant with compactions - so I can test very large write loads with limited disk space.

We find that for these kinds of loads a young generation of 512MB that I had recommended before is hopelessly undersized. We're seeing lots of premature promotions into tenured space, followed by minute long full pauses that eventually cause the region servers to shut down.

For the tests I ran with a 16G heap and found a young gen of 2G is good.

Note that in CMS the size of the young gen is one of the knobs to trade latency for throughput. The smaller the young gen is size the smaller the pauses tend to be... If all short lived garbage fits into the young gen, that is. If it does not, short lived objects get promoted into the tenured space and that will eventually lead to very long (minutes with 30G heaps) full collection pauses.

So the goal is to size the young gen just large enough to fit the short lived objects. In HBase we do some guide posts for this. 40% of the heap (by default) is dedicated to the memstores, another 40% (again by default) to the block cache, and the rest (20%) to "day-to-day" garbage.

With I now recommend the following for the young generation:
  • at least 512MB
  • after that 10-15% of the heap
  • but at most 3GB

For most setups the following should cover all the bases:

-Xmn2g - small eden space (or even -Xmn3g for very large heaps)
-XX:+UseParNewGC - collect eden in parallel
-XX:+UseConcMarkSweepGC - use the non-moving CMS collector
-XX:CMSInitiatingOccupancyFraction=70 - start collecting when 70% of the tenured gen are full to avoid collection under pressure
-XX:+UseCMSInitiatingOccupancyOnly - do not try to adjust CMS setting


In the end you have to test with your own work loads. Start with the smallest young gen size that you think you can get away with. Then watch the GC log. If you see lot's of "promotion failed" type messages, you need to increase eden (-Xmn), do that until the promotion failures stop.

We'll be doing testing with G1GC soon.

Friday, May 8, 2015

My HBaseCon talk about HBase Performance Tuning

By Lars Hofhansl

HBaseCon 2015 was a blast as always. All the presentations and videos will be online soon.

In the meanwhile my presentation on "HBase Performance Tuning" can be found on SlideShare.

Monday, April 20, 2015

HBaseCon 2015

Don't forget to come to HBaseCon, the yearly get-together for all things HBase in San Francisco. May 7th, 2015.

We have great collection of sessions this year:

  • A highly-trafficked HBase cluster with an uptime of sixteen months
  • An HBase deploy that spans three datacenters doing master-master replication between thousands of HBase nodes in each
  • Some nuggets on how Bigtable does it (and HBase could too)
  • How HBase is being used to record patient telemetry 24/7 on behalf of the Michael J. Fox Foundation to enable breakthroughs in Parkinson Disease research
  • How Pinterest and Microsoft are doing it in the cloud, how FINRA and Bloomberg are doing it out east, and how Rocketfuel, Yahoo! and Flipboard, etc., are representing the west

Among many others!

I'll be talking about HBase Tuning and have a brief cameo in the HBase 2.0 panel, talking abount semantic versioning. Feel free to find me afterwards.

Sunday, March 8, 2015

HBase Scanning Internals - SEEK vs SKIP

By Lars Hofhansl, March 8th, 2015

Recently we ran into a problem where a mapper that scanned a region of about 10GB with a timerange that did not include any Cell (KeyValue) took upwards of 20 minutes to finish; it processed only about 8MB/s.

It turns out this was a known problem that has eluded a fix for while: Deciding at the scanner level whether to SEEK ahead past potentially many Cells or whether to power through and repeatedly SKIP the next Cell until the next useful Cell is reached.

Background

Scanning forward through a file, HBase has no knowledge about how many columns are to follow for the current row or how many versions there are for the current column (remember that every version of every column in HBase has its own Cell in the HFiles).

In order to deal with many columns or versions, HBase can issue SEEKs to the next row (seeking past all versions for all remaining columns for the row) or the next column (seeking past all remaining versions). HBase errs on the side of SEEK'ing frequently since SKIP'ing potentially 1000' or 100000's of times can be disastrous for performance (imagine a row with 100 columns and 10000 versions each - unlikely, but possible).

The problem is: SEEK'ing is about 10x as expensive as a single SKIP - depending on how many seek pointers into HFiles have to be reset.

Yet, in many cases we have rows with only a few or even just one column and just one version each. Now the SEEKs will cause a significant slowdown.

After much trying finally there is a proposed solution:
HBASE-13109 Make better SEEK vs SKIP decisions during scanning
(0.98.12 and later)

How does it work?

HFiles are organized like B-Trees, and it is possible to determine the start key of the next block in each file.

A heuristic is now:
Will the SEEK we are about to execute get us into the next block of the HFile that is at top of the heap used for the merge sorting between the HFiles?

If so, we will definitely benefit from seeking (the repeated SKIPs would eventually exhaust the current block and load the next one anyway).

If not, we'll likely benefit from repeated SKIP'ing. This is a heuristic only, the SEEK might allow us to seek past manys Cell in HFiles not at the top of the heap, but that is unlikely.

In all tests I did this performs equal to or (much) better than the current behavior.

The upshot is that now the HBase plumbing (filters, coprocessors, delete marker handling, skipping columns, etc) can continue to issue SEEKs where that is logically possible, and then at the scanner level it can decide whether to act upon the SEEK or to keep SKIP'ing inside the current block, with almost optimal performance.

TL;DR:
HBASE-13109 Allows many queries against HBase to execute 2-3x faster. Especially those that select specific columns, or those that filter on timeranges, or where many deleted columns for column families are encountered.
Queries that request all columns and that do not filter the data in any way will not benefit from this change.

You do need to do anything to get that benefit (other than upgrading your HBase to at least the upcoming 0.98.12).

Monday, January 12, 2015

More HBase GC tuning

By Lars Hofhansl

My article on hbase-gc-tuning-observations explores how to configure the garbage collector for HBase.

There is actually a bit more to it, especially when block encoding is enabled for a column family and the predominant access is via the scan API with row caching.

Block encoding currently requires HBase to materialize each KeyValue after decoding during scanning, and hence this has the potential to produce a lot of garbage for each scan RPC, especially when the scan response is large as might be the case when scanner caching is set to larger value (see Scan.getCaching())

My experiments show that in that case it is better to run with a larger young gen of 512MB (-Xmn512m) and - crucially - make sure that all per RPC garbage across all handlers actively performing scans fits into the survivor space. (Note that this statement is true whether or not block encoding is used. Block encoding just produces a lot more garbage).

HBase actually has a way to limit the size of an individual scan response by setting hbase.client.scanner.max.result.size.

 

Quick recap:

The Hotspot JVM divides the heap into PermGen, Tenured Gen, and the YoungGen. YoungGen itself is divided into Eden and two survivor spaces.
  
By default the survivor ratio is 8 (i.e. each survivor space is 1/8 of Eden; together their sizes add up to the configured young gen size) 

 

What to do?

With -Xmn512m this comes to ~51MB for each of the two survivor spaces.
hbase.client.scanner.max.result.size = 2097152 (in hbase-site.xml)


Update, January 31st, 2015
Since HBase versions 0.98 and later produce a little bit more garbage than 0.94 due to using protobuf, I am now generally recommending a young gen of 512mb for those versions of HBase.

And the same reasoning goes for writes, when batching writes make sure the batch sizes are around 2MB, so that they can temporarily fir into the survivor generation.