Chapter 13. Implementation of Document Store

As you might have guessed, this use case again utilizes most if not all of what we have seen before—that is, replication for backup, Lily and Solr for real-time indexing and search, Spark for fast processing, and of course the Java API. The only thing that we don’t really have look at is the MOB aspect. Therefore, we will focus on that over the next sections. Because consistency is also one important aspect of the use case, we will also address it.

As you will see next, there is not that much to do on the implementation side, but we will mostly focus on the key concepts to keep in mind when designing your own application.

MOBs

HBase works very well with small to average size cell values, but bigger ones, because of the compactions, create write amplification. MOBs have been implemented to avoid this.

Warning

MOBs require HFiles format v3. You will have to make sure your HBase version is configured for that. The Apache version of HBase has HFile configured to v3 since version 1.0. However, for compatibility purposes, some distributions still keep the v2. To make sure your version is configured to v3, add hfile.format.version parameter to your config file and set it to 3.1

In the following section, we will see how to configure a table to use the MOB feature and how to push some data into it. You will need to decide the cut off point for when data will be considered a regular cell, or if the entry is too large and needs to be moved into a MOB region.

For testing purposes, we will keep this value pretty small:

create 'mob_test', {NAME => 'f', IS_MOB => true, MOB_THRESHOLD => 104857}
Tip

MOB have been implemented as part of HBASE-11339, which has been commited into the HBase 2.0 branch. Is had not been backported into the 1.0 branch by the community. Therefore, if you want to try the MOB feature, you have to use a 2.0+ version of HBase or a distribution where this has been backported. Because the Cloudera QuickStart VM includes this implementation, we will use it for testing in this chapter.

This command will create a table called mod_test with a single column family called f where all cells over tens of a megabyte will be considered to be a MOB. From the client side, there is no specific operation or parameter to add. Therefore, the Put command will transfer the entire cell content to the RegionServer. Once there, the RegionServer will decide whether to store the document as a normal cell or a MOB region.

The following two puts against our mob_test table are very similar from a client point of view, but will be stored differently on the server side:

byte[] rowKey = Bytes.toBytes("rowKey");
byte[] CF = Bytes.toBytes("f");
byte[] smallCellCQ = Bytes.toBytes("small");
byte[] bigCellCQ = Bytes.toBytes("big");
byte[] smallCellValue = new byte(1024);
byte[] bigCellValue = new byte(110000);
Put smallPut = new Put(rowKey);
smallPut.addColumn(CF, smallCellCQ, smallCellValue);
table.put (smallPut);

Put bigPut = new Put(rowKey);
bigPut.addColumn(CF, bigCellCQ, bigCellValue);
table.put (bigPut);

Those two puts for the same row key of the same table will both go into the same region. However, after flushing the memstore, the small one will go in a regular HFile while the big one will go as a separate MOB file.

Warning

Being pretty new, MOBs are a bit sensitive to the version of HBase you are running. Indeed, building the preceding example with CDH5.7, it will not run against CHD5.5 nor against HBase master branch. Make sure you adjust your POM file to compile with the same version of the HBase you will run.

Storage

In Part I, we saw how HBase stores the data into HFile. Each table is split into regions, each region will be a directory on HDFS, and each column family is a subdirectory of the region directory. For the table we’re looking at here, after everything has been flushed from memory, if we consider we did not use any specific namespace, the following directory structure will be created on HDFS:

├── data
│   ├── default
│   │   └── mob_test
│   │       └── 4ef02a664043021a511f4b4585b8cba1
│   │           ├── f
│   │           │   └── adb1f3f22fa34dfd9b04aa273254c773
│   │           └── recovered.edits
│   │               └── 2.seqid

This looks exactly like what we had for any other regular table. However, we now also have a MOB directory created under the HBase folder:

├── mobdir
│   └── data
│       └── default
│           └── mob_test
│               └── 0660f699de69e8d4c52e4a037e00a732
│                   └── f
│                       └── d41d8cd98f00b204e9800998ecf8427e20160328fa75df3f...0

As you can see here, we still have our regular HFile that contains the small cells that are not filtered as MOB, but we also have an additional file that contains the MOB values. This new file will not be handled the same way as the regular files. Indeed, when HBase has to compact files together, it has to read the entire HFiles and create a new one containing the merged content. The goal of the compaction is to reduce the number of small files and create bigger ones. Because MOB cells are already big, storing them in separate will reduce the read and write amplification caused by the compactions. Only references to those MOB cells will be read and rewritten each time there is a compaction.

MOB files still have to be compacted. You might have deleted a value that was stored as a MOB. There is a shell command for that, but HBase will also automatically trigger those compactions when required.

If most of what you are doing with HBase is writes of big cells and almost no deletes, MOB will give you a very good improvement compared to the regular format.

Usage

At this point, you might be wondering what constitutes a big cell versus a small one. HBase performs very well with cells up to 1 MB. It is also able to process cells up to 5 MB. However, above that point, regular write path will start to have some performance challenges. The other thing you have to consider is how many times you are going to write big cells versus regular ones. MOBs required some extra work for HBase. When you are reading a cell, we first need to lookup into the related HFile and then into the MOB file. Because of this extra call, latency will be slightly impacted. Now, if your MOB is 50 MB, the time to transfer it to the client versus the extra read access on HBase might not be significant. The extra read access on the HBase side will add few milliseconds to your call. But transferring 50 MB of data to your client might take more than that and might just hide the extra milliseconds for the additional read.

The main question you will need to ask yourself is how often are you going to insert big cells into HBase. If you expect to do so only, once in a while, and not more than once per row, a cell that might be a big bigger than the others, like up to 5 MB, you might just be able to use the regular put API. That will save you the extra management of the MOB files and HBase will be able to handle that. Because there will be only a few big cells, the read and write amplification will be limited. However, if you are planning to have multiple big cells per row, if some of your cells are going to be bigger than 5 MB, or if most of your puts are big, you might want to consider using the MOBs.

Warning

Keep in mind that even if MOB reduces the rewrite amplification of the regular HFiles, if you have to perform many puts and deletes of big cells, you will still have to perform many compactions of the MOB files.

Too Big

As we have just seen, it’s fine for regular HBase cells to occasionally grow up to 5 MB cells. MOBs will allow you to easily handle cells up 20 MB. Even if it is not necessarily recommended, some people have pushed it a bit further and are using MOBs with 50 MB cells.

Now, if your use case is to store only very big cells, like 100 MB cells or more, you are better off using a different approach.

There are two best practices around this. First, if your cells are bigger than 100 MB (and you know for sure that they will continue to be over that limit), a better approach is to store the content as a file on HDFS and store the reference of this file into HBase. Keep in mind that it’s inadvisable to store millions of files in the same directory, so you should devise a directory structure based on your use case (e.g., you might create a new folder every month or every day, or for every source).

The second approach is the one described in the previous chapter. If only some of your cells will be very big, but some will be smaller, and you don’t know in advance the distribution of the size, think about splitting your cell into multiple smaller cells. Then when writing the data, check its size and split it if required, and when reading it, perform the opposite operation and merge it back.

The best design to do this is to abstract that from the client application. Indeed, if the client has to split the content itself, it will add complexity and might hide some business logic. However, if you extend the existing HBase API to parse the Puts before they are sent to HBase, you can silently and easily perform the split operations. The same thing is true for the Get where you can, again, parse the content retrieved from HBase and merge it back to one single object.

Example 13-1 gives you an example of how to perform that.

To keep things simple and illustrate how it should work, we are not extending the client API.

Example 13-1. Split one HBase cell to many pieces
  public static void putBigCell (Table table, byte[] key, byte[] value)
               throws IOException {
    if (value.length < 10 * MB) {
      // The value is small enough to be handled as a single HBase cell.
      // There is no need to split it in pieces.
      Put put = new Put(key).addColumn(columnFamily, columnQualifierOnePiece, value);
      table.put(put);
    } else {
      byte[] buffer = new byte[10*MB];
      int index = 0;
      int piece = 1;
      Put put = new Put(key);
      while (index < value.length) {
        int length = Math.min((value.length - index), buffer.length);
        byte[] columnQualifier = Bytes.toBytes(piece);
        System.arraycopy(value, index, buffer, 0, length); 1
        KeyValue kv = new KeyValue(key, 0, key.length, 2
                                   columnFamily, 0, columnFamily.length,
                                   columnQualifier, 0, columnQualifier.length,
                                   put.getTimeStamp(), KeyValue.Type.Put,
                                   buffer, 0, length);
        put.add(kv);
        index += length + 1;
        piece ++;
      }
      table.put(put);
    }
  }
1

We do not want to create a new 10 MB object each time we read a new slice, so we keep reusing the same buffer array.

2

The HBase Put object doesn’t allow you to give a byte array or to specify the length of your value, so we have to make use of the KeyValue object.

Warning

To make sure you don’t create rows that are too big, HBase validates the size of the cells you are trying to store. If you are trying to start 10 MB cells, you might want to make sure hbase.client.keyvalue.maxsize is not set to a lower number. This is a relatively unknown setting that can cause lots of headaches when dealing with large cells.

Consistency

Let’s take a small example to really get the sense of what is the consistency in HBase. Imagine you are writing a website order into HBase. Let’s consider you are writing all the entries one by one with a different key formed by the order ID and the entry ID. Each line in the order will have a different entry ID and so will be a different HBase row. Most of the time, it will all go well and at once into HBase. But what if this is split over two different regions hosted on two different servers? And what if the second server just failed before storing the entries? Some will make it, and some others will have to be retried until the region is reassigned and the data finally makes it in. If a client application tries to read the entire order while this is happening, it will retrieve a partial view of the order. If we use Lily to index the data coming into this table, a query to the index will return partial results.

This is where consistency awareness is important.

Let’s see how this applies to the current use case.

Documents are received by the application where a modified HBase client split them in 10 MB slices and sends all the slices plus the metadata into HBase. All the slices and the metadata have to go together. If you send the slices as different HBase rows, it may take some time for each part to make its way into HBase and therefore an application trying to read it might get only a partial file. Because it is important for all the slices to be written in a single write and be read at once, they will have to be stored into a single row. Metadata also needs to be part of the consistency equation. Because it will be indexed and used for the document retrieval, the metadata is tightly coupled to the file. It will have to go with the same row, at the same time.

There are cases where consistency is not an issue. As an example, if you receive a list of tweets from a user, all those tweets are not linked together. Therefore, retrieving just a subset of them might not impact your results.

Consistency is a very important concept in HBase. It’s not always required to have consistency within an operation. But if you have to, it is very important to keep your data together to make sure it makes it together into HBase, or not at all.

Warning

Stay away from cross-references. Let’s imagine you have a row A where you store the key and the value of a reference called row B When you will update the value of row B, you will also have to update the value of row A. However, they can be on different regions, and consistency of those update operations cannot be guaranteed. If you need to get the value of row B when reading row A, you are much better off keeping row B key in row A values and doing another call based on that. This will require an extra call to HBase, but will save you the extra burden of having to rebuild your entire table after a few failures.

Going Further

If you want to extend the examples presented in this chapter, the following list offers some options you can try based on our discussions from this chapter:

Single cell

Try to push HBase to its maximum capability. Instead of splitting cells into 10 MB chunks, try to create bigger and bigger chunks and see how HBase behaves. Try to read and write those giant cells and validate their content.

Deletes impacts

To gain a better understanding of how deletes are handled with MOBs, update the example to generate some big MOB cells, flush them onto disk, and then execute a delete. Then look at the underlying filesystem storage. Later, flush the table again and run a compaction to see what happens.

Read

This chapter illustrated how to split a huge cell into a smaller one and write it to HBase. But we leave it to you to write the read path. Based on how we have split the cell into multiple pieces, build a method that will read it, back and re-create the initial document. To help you get started, you will have to find the last column, read it and get its size. The size of the document will be the number of columns minus one, multiplied by 10 MB plus the size of the last one. As an example, if you have three columns and the last one is 5 MB, the document size will be (3 - 1) * 10 MB + 5 MB, which is 25 MB.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.111.197