Skip to content

Commit 01526d3

Browse files
authored
PHOENIX-7878 CDC perf improvement - skip redundant cell versions on data table scans (#2493)
1 parent 995cf71 commit 01526d3

5 files changed

Lines changed: 814 additions & 24 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.phoenix.filter;
19+
20+
import java.io.DataInput;
21+
import java.io.DataOutput;
22+
import java.io.IOException;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
import org.apache.hadoop.hbase.Cell;
26+
import org.apache.hadoop.hbase.CellUtil;
27+
import org.apache.hadoop.hbase.exceptions.DeserializationException;
28+
import org.apache.hadoop.hbase.filter.FilterBase;
29+
import org.apache.hadoop.hbase.util.Bytes;
30+
import org.apache.hadoop.hbase.util.Writables;
31+
import org.apache.hadoop.io.Writable;
32+
import org.apache.phoenix.hbase.index.util.ImmutableBytesPtr;
33+
34+
/**
35+
* Filter for CDC data table scans that prunes redundant cell versions. For each data row the filter
36+
* is given the {@code [min, max]} band of change timestamps for that row (the oldest and newest CDC
37+
* change seen in the current scan). Per column (family:qualifier), with cells delivered in
38+
* timestamp-descending order, it includes only:
39+
* <ul>
40+
* <li>cells whose timestamp is within {@code [min, max]} (the change cells), and</li>
41+
* <li>the first cell strictly below {@code min} (the pre-image for the oldest change), and</li>
42+
* <li>all DeleteFamily / DeleteFamilyVersion markers (needed for CDC deletion tracking).</li>
43+
* </ul>
44+
* Cells newer than {@code max}, and all but the first cell older than {@code min}, are skipped,
45+
* reducing network I/O, memory, and processing overhead on the data table scan.
46+
*/
47+
public class CDCVersionFilter extends FilterBase implements Writable {
48+
49+
private Map<ImmutableBytesPtr, long[]> rangeMap;
50+
51+
// Per-row state
52+
private long[] currentRange;
53+
private byte[] currentRowKey;
54+
55+
// Per-column state
56+
private byte[] prevFamily;
57+
private byte[] prevQualifier;
58+
59+
private boolean belowMinEmitted;
60+
61+
public CDCVersionFilter() {
62+
}
63+
64+
/**
65+
* @param rangeMap mapping of data row key to a 2-element {@code [min, max]} array giving the
66+
* oldest and newest change timestamp for that row
67+
*/
68+
public CDCVersionFilter(Map<ImmutableBytesPtr, long[]> rangeMap) {
69+
this.rangeMap = rangeMap;
70+
}
71+
72+
@Override
73+
public void reset() throws IOException {
74+
currentRange = null;
75+
currentRowKey = null;
76+
resetColumnState();
77+
}
78+
79+
private void resetColumnState() {
80+
prevFamily = null;
81+
prevQualifier = null;
82+
belowMinEmitted = false;
83+
}
84+
85+
private boolean isNewRow(Cell cell) {
86+
if (currentRowKey == null) {
87+
return true;
88+
}
89+
return !Bytes.equals(currentRowKey, 0, currentRowKey.length, cell.getRowArray(),
90+
cell.getRowOffset(), cell.getRowLength());
91+
}
92+
93+
private void onNewRow(Cell cell) {
94+
currentRowKey = CellUtil.cloneRow(cell);
95+
ImmutableBytesPtr rowKeyPtr = new ImmutableBytesPtr(currentRowKey);
96+
currentRange = rangeMap != null ? rangeMap.get(rowKeyPtr) : null;
97+
resetColumnState();
98+
}
99+
100+
private boolean isNewColumn(Cell cell) {
101+
if (prevFamily == null) {
102+
return true;
103+
}
104+
if (
105+
!Bytes.equals(prevFamily, 0, prevFamily.length, cell.getFamilyArray(), cell.getFamilyOffset(),
106+
cell.getFamilyLength())
107+
) {
108+
return true;
109+
}
110+
return !Bytes.equals(prevQualifier, 0, prevQualifier.length, cell.getQualifierArray(),
111+
cell.getQualifierOffset(), cell.getQualifierLength());
112+
}
113+
114+
private void trackColumn(Cell cell) {
115+
prevFamily = CellUtil.cloneFamily(cell);
116+
prevQualifier = CellUtil.cloneQualifier(cell);
117+
}
118+
119+
// No @Override for HBase 3 compatibility
120+
public ReturnCode filterKeyValue(Cell v) throws IOException {
121+
return filterCell(v);
122+
}
123+
124+
@Override
125+
public ReturnCode filterCell(Cell cell) throws IOException {
126+
if (isNewRow(cell)) {
127+
onNewRow(cell);
128+
}
129+
130+
if (currentRange == null) {
131+
return ReturnCode.INCLUDE;
132+
}
133+
134+
Cell.Type type = cell.getType();
135+
if (type == Cell.Type.DeleteFamily || type == Cell.Type.DeleteFamilyVersion) {
136+
return ReturnCode.INCLUDE;
137+
}
138+
139+
if (isNewColumn(cell)) {
140+
trackColumn(cell);
141+
belowMinEmitted = false;
142+
}
143+
144+
long cellTs = cell.getTimestamp();
145+
long minChangeTs = currentRange[0];
146+
long maxChangeTs = currentRange[1];
147+
148+
if (cellTs > maxChangeTs) {
149+
return ReturnCode.SKIP;
150+
}
151+
if (cellTs >= minChangeTs) {
152+
return ReturnCode.INCLUDE;
153+
}
154+
if (!belowMinEmitted) {
155+
belowMinEmitted = true;
156+
return ReturnCode.INCLUDE;
157+
}
158+
return ReturnCode.NEXT_COL;
159+
}
160+
161+
@Override
162+
public byte[] toByteArray() throws IOException {
163+
return Writables.getBytes(this);
164+
}
165+
166+
public static CDCVersionFilter parseFrom(byte[] pbBytes) throws DeserializationException {
167+
try {
168+
return (CDCVersionFilter) Writables.getWritable(pbBytes, new CDCVersionFilter());
169+
} catch (IOException e) {
170+
throw new DeserializationException(e);
171+
}
172+
}
173+
174+
@Override
175+
public void write(DataOutput out) throws IOException {
176+
if (rangeMap == null) {
177+
out.writeInt(0);
178+
return;
179+
}
180+
out.writeInt(rangeMap.size());
181+
for (Map.Entry<ImmutableBytesPtr, long[]> entry : rangeMap.entrySet()) {
182+
ImmutableBytesPtr key = entry.getKey();
183+
long[] range = entry.getValue();
184+
out.writeInt(key.getLength());
185+
out.write(key.get(), key.getOffset(), key.getLength());
186+
out.writeLong(range[0]);
187+
out.writeLong(range[1]);
188+
}
189+
}
190+
191+
@Override
192+
public void readFields(DataInput in) throws IOException {
193+
int numRows = in.readInt();
194+
if (numRows < 0) {
195+
throw new IOException("Invalid CDCVersionFilter row count: " + numRows);
196+
}
197+
rangeMap = new HashMap<>();
198+
for (int i = 0; i < numRows; i++) {
199+
int keyLen = in.readInt();
200+
if (keyLen < 0) {
201+
throw new IOException("Invalid CDCVersionFilter row key length: " + keyLen);
202+
}
203+
byte[] keyBytes = new byte[keyLen];
204+
in.readFully(keyBytes);
205+
long minChangeTs = in.readLong();
206+
long maxChangeTs = in.readLong();
207+
rangeMap.put(new ImmutableBytesPtr(keyBytes), new long[] { minChangeTs, maxChangeTs });
208+
}
209+
}
210+
}

phoenix-core-server/src/main/java/org/apache/phoenix/coprocessor/CDCGlobalIndexRegionScanner.java

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.Collection;
2929
import java.util.Collections;
3030
import java.util.HashMap;
31+
import java.util.HashSet;
3132
import java.util.List;
3233
import java.util.Map;
3334
import java.util.Set;
@@ -40,6 +41,8 @@
4041
import org.apache.hadoop.hbase.client.Result;
4142
import org.apache.hadoop.hbase.client.Scan;
4243
import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
44+
import org.apache.hadoop.hbase.filter.Filter;
45+
import org.apache.hadoop.hbase.filter.FilterList;
4346
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
4447
import org.apache.hadoop.hbase.regionserver.Region;
4548
import org.apache.hadoop.hbase.regionserver.RegionScanner;
@@ -49,6 +52,7 @@
4952
import org.apache.phoenix.execute.TupleProjector;
5053
import org.apache.phoenix.expression.Expression;
5154
import org.apache.phoenix.expression.SingleCellColumnExpression;
55+
import org.apache.phoenix.filter.CDCVersionFilter;
5256
import org.apache.phoenix.hbase.index.util.ImmutableBytesPtr;
5357
import org.apache.phoenix.index.CDCTableInfo;
5458
import org.apache.phoenix.index.IndexMaintainer;
@@ -123,21 +127,60 @@ protected Scan prepareDataTableScan(Collection<byte[]> dataRowKeys) throws IOExc
123127
) {
124128
return null;
125129
}
126-
// TODO: Get Timerange from the start row and end row of the index scan object
127-
// and set it in the datatable scan object.
128-
// if (scan.getStartRow().length == 8) {
129-
// startTimeRange = PLong.INSTANCE.getCodec().decodeLong(
130-
// scan.getStartRow(), 0, SortOrder.getDefault());
131-
// }
132-
// if (scan.getStopRow().length == 8) {
133-
// stopTimeRange = PLong.INSTANCE.getCodec().decodeLong(
134-
// scan.getStopRow(), 0, SortOrder.getDefault());
135-
// }
136130
Scan dataScan = prepareDataTableScan(dataRowKeys, true);
137131
if (dataScan == null) {
138132
return null;
139133
}
140-
return CDCUtil.setupScanForCDC(dataScan);
134+
CDCUtil.setupScanForCDC(dataScan);
135+
Map<ImmutableBytesPtr, long[]> changeRangeMap = buildDataRowChangeRangeMap(dataRowKeys);
136+
if (!changeRangeMap.isEmpty()) {
137+
CDCVersionFilter versionFilter = new CDCVersionFilter(changeRangeMap);
138+
Filter existingFilter = dataScan.getFilter();
139+
if (existingFilter != null) {
140+
dataScan.setFilter(
141+
new FilterList(FilterList.Operator.MUST_PASS_ALL, existingFilter, versionFilter));
142+
} else {
143+
dataScan.setFilter(versionFilter);
144+
}
145+
}
146+
return dataScan;
147+
}
148+
149+
private Map<ImmutableBytesPtr, long[]>
150+
buildDataRowChangeRangeMap(Collection<byte[]> dataRowKeys) {
151+
Set<ImmutableBytesPtr> scopedRowKeys = new HashSet<>(dataRowKeys.size());
152+
for (byte[] dataRowKey : dataRowKeys) {
153+
scopedRowKeys.add(new ImmutableBytesPtr(dataRowKey));
154+
}
155+
Map<ImmutableBytesPtr, long[]> result = new HashMap<>();
156+
for (List<Cell> indexRow : indexRows) {
157+
if (indexRow.isEmpty()) {
158+
continue;
159+
}
160+
Cell firstCell = indexRow.get(0);
161+
byte[] indexRowKey = ImmutableBytesPtr.cloneCellRowIfNecessary(firstCell);
162+
byte[] dataRowKey = indexToDataRowKeyMap.get(indexRowKey);
163+
if (dataRowKey == null) {
164+
continue;
165+
}
166+
ImmutableBytesPtr dataRowKeyPtr = new ImmutableBytesPtr(dataRowKey);
167+
if (!scopedRowKeys.contains(dataRowKeyPtr)) {
168+
continue;
169+
}
170+
long changeTs = firstCell.getTimestamp();
171+
long[] range = result.get(dataRowKeyPtr);
172+
if (range == null) {
173+
result.put(dataRowKeyPtr, new long[] { changeTs, changeTs });
174+
} else {
175+
if (changeTs < range[0]) {
176+
range[0] = changeTs;
177+
}
178+
if (changeTs > range[1]) {
179+
range[1] = changeTs;
180+
}
181+
}
182+
}
183+
return result;
141184
}
142185

143186
protected boolean getNextCoveredIndexRow(List<Cell> result) throws IOException {

0 commit comments

Comments
 (0)