Skip to content

Commit a2d5127

Browse files
authored
[hbase2] HBase server side value filtering for long SCAN operations (#1462)
1 parent ff4ecbd commit a2d5127

3 files changed

Lines changed: 165 additions & 15 deletions

File tree

hbase2/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ Following options can be configurable using `-p`.
7171
* `clientbuffering`: Whether or not to use client side buffering and batching of write operations. This can significantly improve performance and defaults to true.
7272
* `writebuffersize`: The maximum amount, in bytes, of data to buffer on the client side before a flush is forced. The default is 12MB. Only used when `clientbuffering` is true.
7373
* `durability`: Whether or not writes should be appended to the WAL. Bypassing the WAL can improve throughput but data cannot be recovered in the event of a crash. The default is true.
74+
* `hbase.usescanvaluefiltering` : If true, the HBase scan operations will be configured to apply server-side filtering on the values during Scan operations. This means that only those records will be returned from HBase, where the values (byte arrays) are greater/less/etc. than the byte array defined in the `hbase.scanfiltervalue` parameter. The type of the filtering can be set in the `hbase.scanfilteroperator` parameter. This feature is disabled by default.
75+
* `hbase.scanfilteroperator`: specifying the server-side filter operator to use during scan operations. One of the following strings: less_or_equal, greater_or_equal, greater, less, not_equal, equal. The default value is less_or_equal. This parameter is only used, if `hbase.usescanvaluefiltering` is set to true.
76+
* `hbase.scanfiltervalue`: specifying the server-side filter value to use during scan operations. It is defined as a hexadecimal string, will be translated into a byte array. This parameter is only used if `hbase.usescanvaluefiltering` is set to true. By default it is a 200 long string "7FFFFFF...", as the core workload is defining 100 bytes long random byte arrays as values. Using the default `hbase.scanfiltervalue` and default `hbase.scanfilteroperator` will result in the filtering of approximately half of the values.
7477

7578
Additional HBase settings should be provided in the `hbase-site.xml` file located in your `/HBASE-HOME-DIR/conf` directory. Typically this will be `/etc/hbase/conf`.
7679

hbase2/src/main/java/site/ycsb/db/hbase2/HBaseClient2.java

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515

1616
package site.ycsb.db.hbase2;
1717

18+
import org.apache.hadoop.hbase.CompareOperator;
19+
import org.apache.hadoop.hbase.filter.BinaryComparator;
20+
import org.apache.hadoop.hbase.filter.ByteArrayComparable;
21+
import org.apache.hadoop.hbase.filter.FilterList;
22+
import org.apache.hadoop.hbase.filter.ValueFilter;
1823
import site.ycsb.ByteArrayByteIterator;
1924
import site.ycsb.ByteIterator;
2025
import site.ycsb.DBException;
@@ -61,9 +66,9 @@
6166
*/
6267
public class HBaseClient2 extends site.ycsb.DB {
6368
private static final AtomicInteger THREAD_COUNT = new AtomicInteger(0);
64-
69+
6570
private Configuration config = HBaseConfiguration.create();
66-
71+
6772
private boolean debug = false;
6873

6974
private String tableName = "";
@@ -101,6 +106,17 @@ public class HBaseClient2 extends site.ycsb.DB {
101106
private boolean clientSideBuffering = false;
102107
private long writeBufferSize = 1024 * 1024 * 12;
103108

109+
/**
110+
* If true, we will configure server-side value filtering during scans.
111+
*/
112+
private boolean useScanValueFiltering = false;
113+
private CompareOperator scanFilterOperator;
114+
private static final String DEFAULT_SCAN_FILTER_OPERATOR = "less_or_equal";
115+
private ByteArrayComparable scanFilterValue;
116+
private static final String DEFAULT_SCAN_FILTER_VALUE = // 200 hexadecimal chars translated into 100 bytes
117+
"7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" +
118+
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF";
119+
104120
/**
105121
* Initialize any state for this DB. Called once per DB instance; there is one
106122
* DB instance per client thread.
@@ -126,8 +142,8 @@ public void init() throws DBException {
126142
UserGroupInformation.setConfiguration(config);
127143
}
128144

129-
if ((getProperties().getProperty("principal")!=null)
130-
&& (getProperties().getProperty("keytab")!=null)) {
145+
if ((getProperties().getProperty("principal") != null)
146+
&& (getProperties().getProperty("keytab") != null)) {
131147
try {
132148
UserGroupInformation.loginUserFromKeytab(getProperties().getProperty("principal"),
133149
getProperties().getProperty("keytab"));
@@ -165,9 +181,17 @@ public void init() throws DBException {
165181
debug = true;
166182
}
167183

168-
if ("false"
169-
.equals(getProperties().getProperty("hbase.usepagefilter", "true"))) {
170-
usePageFilter = false;
184+
usePageFilter = isBooleanParamSet("hbase.usepagefilter", usePageFilter);
185+
186+
187+
if (isBooleanParamSet("hbase.usescanvaluefiltering", false)) {
188+
useScanValueFiltering=true;
189+
String operator = getProperties().getProperty("hbase.scanfilteroperator");
190+
operator = operator == null || operator.trim().isEmpty() ? DEFAULT_SCAN_FILTER_OPERATOR : operator;
191+
scanFilterOperator = CompareOperator.valueOf(operator.toUpperCase());
192+
String filterValue = getProperties().getProperty("hbase.scanfiltervalue");
193+
filterValue = filterValue == null || filterValue.trim().isEmpty() ? DEFAULT_SCAN_FILTER_VALUE : filterValue;
194+
scanFilterValue = new BinaryComparator(Bytes.fromHex(filterValue));
171195
}
172196

173197
columnFamily = getProperties().getProperty("columnfamily");
@@ -331,9 +355,11 @@ public Status scan(String table, String startkey, int recordcount,
331355
// HBase has no record limit. Here, assume recordcount is small enough to
332356
// bring back in one call.
333357
// We get back recordcount records
358+
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
359+
334360
s.setCaching(recordcount);
335361
if (this.usePageFilter) {
336-
s.setFilter(new PageFilter(recordcount));
362+
filterList.addFilter(new PageFilter(recordcount));
337363
}
338364

339365
// add specified fields or else all fields
@@ -345,6 +371,13 @@ public Status scan(String table, String startkey, int recordcount,
345371
}
346372
}
347373

374+
// define value filter if needed
375+
if (useScanValueFiltering){
376+
filterList.addFilter(new ValueFilter(scanFilterOperator, scanFilterValue));
377+
}
378+
379+
s.setFilter(filterList);
380+
348381
// get results
349382
ResultScanner scanner = null;
350383
try {
@@ -523,6 +556,11 @@ public Status delete(String table, String key) {
523556
void setConfiguration(final Configuration newConfig) {
524557
this.config = newConfig;
525558
}
559+
560+
private boolean isBooleanParamSet(String param, boolean defaultValue){
561+
return Boolean.parseBoolean(getProperties().getProperty(param, Boolean.toString(defaultValue)));
562+
}
563+
526564
}
527565

528566
/*

hbase2/src/test/java/site/ycsb/db/hbase2/HBaseClient2Test.java

Lines changed: 116 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
package site.ycsb.db.hbase2;
1717

18+
import static org.junit.Assert.assertArrayEquals;
1819
import static site.ycsb.workloads.CoreWorkload.TABLENAME_PROPERTY;
1920
import static site.ycsb.workloads.CoreWorkload.TABLENAME_PROPERTY_DEFAULT;
2021
import static org.junit.Assert.assertEquals;
@@ -39,13 +40,13 @@
3940
import org.apache.hadoop.hbase.util.Bytes;
4041
import org.junit.After;
4142
import org.junit.AfterClass;
42-
import org.junit.Before;
4343
import org.junit.BeforeClass;
4444
import org.junit.Ignore;
4545
import org.junit.Test;
4646

4747
import java.nio.ByteBuffer;
4848
import java.util.ArrayList;
49+
import java.util.Collections;
4950
import java.util.HashMap;
5051
import java.util.List;
5152
import java.util.Properties;
@@ -70,7 +71,6 @@ private static boolean isWindows() {
7071

7172
/**
7273
* Creates a mini-cluster for use in these tests.
73-
*
7474
* This is a heavy-weight operation, so invoked only once for the test class.
7575
*/
7676
@BeforeClass
@@ -93,16 +93,19 @@ public static void tearDownClass() throws Exception {
9393
}
9494

9595
/**
96-
* Sets up the mini-cluster for testing.
97-
*
98-
* We re-create the table for each test.
96+
* Re-create the table for each test. Using default properties.
9997
*/
100-
@Before
10198
public void setUp() throws Exception {
99+
setUp(new Properties());
100+
}
101+
102+
/**
103+
* Re-create the table for each test. Using custom properties.
104+
*/
105+
public void setUp(Properties p) throws Exception {
102106
client = new HBaseClient2();
103107
client.setConfiguration(new Configuration(testingUtil.getConfiguration()));
104108

105-
Properties p = new Properties();
106109
p.setProperty("columnfamily", COLUMN_FAMILY);
107110

108111
Measurements.setProperties(p);
@@ -124,6 +127,7 @@ public void tearDown() throws Exception {
124127

125128
@Test
126129
public void testRead() throws Exception {
130+
setUp();
127131
final String rowKey = "row1";
128132
final Put p = new Put(Bytes.toBytes(rowKey));
129133
p.addColumn(Bytes.toBytes(COLUMN_FAMILY),
@@ -142,6 +146,7 @@ public void testRead() throws Exception {
142146

143147
@Test
144148
public void testReadMissingRow() throws Exception {
149+
setUp();
145150
final HashMap<String, ByteIterator> result = new HashMap<String, ByteIterator>();
146151
final Status status = client.read(tableName, "Missing row", null, result);
147152
assertEquals(Status.NOT_FOUND, status);
@@ -150,6 +155,7 @@ public void testReadMissingRow() throws Exception {
150155

151156
@Test
152157
public void testScan() throws Exception {
158+
setUp();
153159
// Fill with data
154160
final String colStr = "row_number";
155161
final byte[] col = Bytes.toBytes(colStr);
@@ -183,8 +189,48 @@ public void testScan() throws Exception {
183189
}
184190
}
185191

192+
@Test
193+
public void testScanWithValueFilteringUsingDefaultProperties() throws Exception {
194+
testScanWithValueFiltering(null, null, 100, new byte[][] {
195+
Bytes.fromHex("0000"), Bytes.fromHex("1111"), Bytes.fromHex("2222"), Bytes.fromHex("3333"),
196+
Bytes.fromHex("4444"), Bytes.fromHex("5555"), Bytes.fromHex("6666"), Bytes.fromHex("7777"),
197+
});
198+
}
199+
200+
@Test
201+
public void testScanWithValueFilteringOperationLessOrEqual() throws Exception {
202+
testScanWithValueFiltering("less_or_equal", "3333", 100, new byte[][] {
203+
Bytes.fromHex("0000"), Bytes.fromHex("1111"), Bytes.fromHex("2222"), Bytes.fromHex("3333"),
204+
});
205+
}
206+
207+
@Test
208+
public void testScanWithValueFilteringOperationEqual() throws Exception {
209+
testScanWithValueFiltering("equal", "AAAA", 100, new byte[][]{
210+
Bytes.fromHex("AAAA")
211+
});
212+
}
213+
214+
@Test
215+
public void testScanWithValueFilteringOperationNotEqual() throws Exception {
216+
testScanWithValueFiltering("not_equal", "AAAA", 100 , new byte[][]{
217+
Bytes.fromHex("0000"), Bytes.fromHex("1111"), Bytes.fromHex("2222"), Bytes.fromHex("3333"),
218+
Bytes.fromHex("4444"), Bytes.fromHex("5555"), Bytes.fromHex("6666"), Bytes.fromHex("7777"),
219+
Bytes.fromHex("8888"), Bytes.fromHex("9999"), Bytes.fromHex("BBBB"),
220+
Bytes.fromHex("CCCC"), Bytes.fromHex("DDDD"), Bytes.fromHex("EEEE"), Bytes.fromHex("FFFF")
221+
});
222+
}
223+
224+
@Test
225+
public void testScanWithValueFilteringAndRowLimit() throws Exception {
226+
testScanWithValueFiltering("greater", "8887", 3, new byte[][] {
227+
Bytes.fromHex("8888"), Bytes.fromHex("9999"), Bytes.fromHex("AAAA")
228+
});
229+
}
230+
186231
@Test
187232
public void testUpdate() throws Exception{
233+
setUp();
188234
final String key = "key";
189235
final HashMap<String, String> input = new HashMap<String, String>();
190236
input.put("column1", "value1");
@@ -209,5 +255,68 @@ public void testUpdate() throws Exception{
209255
public void testDelete() {
210256
fail("Not yet implemented");
211257
}
258+
259+
private void testScanWithValueFiltering(String operation, String filterValue, int scanRowLimit,
260+
byte[][] expectedValuesReturned) throws Exception {
261+
Properties properties = new Properties();
262+
properties.setProperty("hbase.usescanvaluefiltering", String.valueOf(true));
263+
if(operation != null) {
264+
properties.setProperty("hbase.scanfilteroperator", operation);
265+
}
266+
if(filterValue != null) {
267+
properties.setProperty("hbase.scanfiltervalue", filterValue);
268+
}
269+
270+
// setup the client and fill two columns with data
271+
setUp(properties);
272+
setupTableColumnWithHexValues("col_1");
273+
setupTableColumnWithHexValues("col_2");
274+
275+
Vector<HashMap<String, ByteIterator>> result = new Vector<>();
276+
277+
// first scan the whole table (both columns)
278+
client.scan(tableName, "00000", scanRowLimit, null, result);
279+
280+
assertEquals(expectedValuesReturned.length, result.size());
281+
for(int i = 0; i < expectedValuesReturned.length; i++) {
282+
final HashMap<String, ByteIterator> row = result.get(i);
283+
assertEquals(2, row.size());
284+
assertTrue(row.containsKey("col_1") && row.containsKey("col_2"));
285+
assertArrayEquals(expectedValuesReturned[i], row.get("col_1").toArray());
286+
assertArrayEquals(expectedValuesReturned[i], row.get("col_2").toArray());
287+
}
288+
289+
// now scan only a single column (the filter should work here too)
290+
result = new Vector<>();
291+
client.scan(tableName, "00000", scanRowLimit, Collections.singleton("col_1"), result);
292+
293+
assertEquals(expectedValuesReturned.length, result.size());
294+
for(int i = 0; i < expectedValuesReturned.length; i++) {
295+
final HashMap<String, ByteIterator> row = result.get(i);
296+
assertEquals(1, row.size());
297+
assertTrue(row.containsKey("col_1"));
298+
assertArrayEquals(expectedValuesReturned[i], row.get("col_1").toArray());
299+
}
300+
}
301+
302+
private void setupTableColumnWithHexValues(String colStr) throws Exception {
303+
final byte[] col = Bytes.toBytes(colStr);
304+
final byte[][] values = {
305+
Bytes.fromHex("0000"), Bytes.fromHex("1111"), Bytes.fromHex("2222"), Bytes.fromHex("3333"),
306+
Bytes.fromHex("4444"), Bytes.fromHex("5555"), Bytes.fromHex("6666"), Bytes.fromHex("7777"),
307+
Bytes.fromHex("8888"), Bytes.fromHex("9999"), Bytes.fromHex("AAAA"), Bytes.fromHex("BBBB"),
308+
Bytes.fromHex("CCCC"), Bytes.fromHex("DDDD"), Bytes.fromHex("EEEE"), Bytes.fromHex("FFFF")
309+
};
310+
final List<Put> puts = new ArrayList<>(16);
311+
for(int i = 0; i < 16; i++) {
312+
final byte[] key = Bytes.toBytes(String.format("%05d", i));
313+
final byte[] value = values[i];
314+
final Put p = new Put(key);
315+
p.addColumn(Bytes.toBytes(COLUMN_FAMILY), col, value);
316+
puts.add(p);
317+
}
318+
table.put(puts);
319+
}
320+
212321
}
213322

0 commit comments

Comments
 (0)