Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.9k
[improvement](streamingjob) change streaming job source log lag in bytes#66409
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
646f4135fb3486509413042806c5a95bfdd7d0aa448698c82File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| // Licensed to the Apache Software Foundation (ASF) under one | ||
| // or more contributor license agreements. See the NOTICE file | ||
| // distributed with this work for additional information | ||
| // regarding copyright ownership. The ASF licenses this file | ||
| // to you under the Apache License, Version 2.0 (the | ||
| // "License"); you may not use this file except in compliance | ||
| // with the License. You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
| package org.apache.doris.job.cdc.request; | ||
| import lombok.Data; | ||
| import lombok.EqualsAndHashCode; | ||
| import lombok.NoArgsConstructor; | ||
| import java.util.Collections; | ||
| import java.util.Map; | ||
| @Data | ||
| @NoArgsConstructor | ||
| @EqualsAndHashCode(callSuper = true) | ||
| public class FetchEndOffsetRequest extends JobBaseConfig { | ||
| private Map<String, String> referenceOffset; | ||
| public FetchEndOffsetRequest( | ||
| String jobId, | ||
| String dataSource, | ||
| Map<String, String> config, | ||
| String frontendAddress, | ||
| Map<String, String> referenceOffset) { | ||
| super(jobId, dataSource, config, frontendAddress); | ||
| // Its presence tells the CDC client that this FE accepts the lag-aware response. | ||
| this.referenceOffset = referenceOffset == null ? Collections.emptyMap() : referenceOffset; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| // Licensed to the Apache Software Foundation (ASF) under one | ||
| // or more contributor license agreements. See the NOTICE file | ||
| // distributed with this work for additional information | ||
| // regarding copyright ownership. The ASF licenses this file | ||
| // to you under the Apache License, Version 2.0 (the | ||
| // "License"); you may not use this file except in compliance | ||
| // with the License. You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, | ||
| // software distributed under the License is distributed on an | ||
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| // KIND, either express or implied. See the License for the | ||
| // specific language governing permissions and limitations | ||
| // under the License. | ||
| package org.apache.doris.job.cdc.response; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Data; | ||
| import lombok.NoArgsConstructor; | ||
| import java.util.Map; | ||
| @Data | ||
| @AllArgsConstructor | ||
| @NoArgsConstructor | ||
| public class FetchEndOffsetResult { | ||
| private Map<String, String> endOffset; | ||
| private long lagBytes = -1; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1000,21 +1000,19 @@ public void onReplayCreate() throws JobException { | ||
| } | ||
| public String getLag() { | ||
| return offsetProvider != null ? offsetProvider.getLag() : ""; | ||
| return offsetProvider != null ? offsetProvider.getLag() : "-1"; | ||
| } | ||
| // Numeric lag for metrics. Returns -1 when lag is not applicable (S3, snapshot phase) | ||
| // or unparseable, so dashboards can filter N/A jobs via lag >= 0. | ||
| public long getLagSeconds() { | ||
| String lagStr = getLag(); | ||
| if (lagStr == null || lagStr.isEmpty()) { | ||
| return -1L; | ||
| } | ||
| try { | ||
| return Long.parseLong(lagStr); | ||
| } catch (NumberFormatException e) { | ||
| return -1L; | ||
| } | ||
| public long getLagBytes() { | ||
| return offsetProvider != null ? offsetProvider.getLagBytes() : -1; | ||
| } | ||
| public long getLastSourceEventTimestampSeconds() { | ||
| return offsetProvider != null ? offsetProvider.getLastSourceEventTimestampSeconds() : 0; | ||
| } | ||
| public long getLastTaskSuccessTimeSeconds() { | ||
| return lastTaskSuccessTime / 1000L; | ||
| } | ||
| /** | ||
| @@ -1077,6 +1075,7 @@ private void modifyPropertiesInternal(Map<String, String> inputProperties) throw | ||
| if (StringUtils.isNotEmpty(inputStreamProps.getOffsetProperty())) { | ||
| Offset offset = validateOffset(inputStreamProps.getOffsetProperty()); | ||
| this.offsetProvider.updateOffset(offset); | ||
| this.offsetProvider.resetLag(); | ||
| this.offsetProviderPersist = offsetProvider.getPersistInfo(); | ||
| log.info("modifyPropertiesInternal: offset updated to {}, job {}", | ||
| inputStreamProps.getOffsetProperty(), getJobId()); | ||
| @@ -1174,8 +1173,10 @@ public TRow getTvfInfo() { | ||
| ? "" : GsonUtils.GSON.toJson(failureReason))); | ||
| trow.addToColumnValue(new TCell().setStringVal(jobRuntimeMsg == null | ||
| ? "" : jobRuntimeMsg)); | ||
| trow.addToColumnValue(new TCell().setStringVal( | ||
| offsetProvider != null ? offsetProvider.getLag() : "")); | ||
| trow.addToColumnValue(new TCell().setStringVal(getLag())); | ||
JNSimba marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| long lastSourceEventTimestampSeconds = getLastSourceEventTimestampSeconds(); | ||
| trow.addToColumnValue(new TCell().setStringVal(lastSourceEventTimestampSeconds > 0 | ||
| ? String.valueOf(lastSourceEventTimestampSeconds) : "")); | ||
| trow.addToColumnValue(new TCell().setStringVal(lastTaskSuccessTime > 0 | ||
| ? TimeUtils.longToTimeString(lastTaskSuccessTime) : "")); | ||
| return trow; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -24,8 +24,10 @@ | ||
| import org.apache.doris.httpv2.rest.RestApiStatusCode; | ||
| import org.apache.doris.job.cdc.DataSourceConfigKeys; | ||
| import org.apache.doris.job.cdc.request.CompareOffsetRequest; | ||
| import org.apache.doris.job.cdc.request.FetchEndOffsetRequest; | ||
| import org.apache.doris.job.cdc.request.FetchTableSplitsRequest; | ||
| import org.apache.doris.job.cdc.request.JobBaseConfig; | ||
| import org.apache.doris.job.cdc.response.FetchEndOffsetResult; | ||
| import org.apache.doris.job.cdc.split.AbstractSourceSplit; | ||
| import org.apache.doris.job.cdc.split.BinlogSplit; | ||
| import org.apache.doris.job.cdc.split.SnapshotSplit; | ||
| @@ -106,6 +108,8 @@ public class JdbcSourceOffsetProvider implements SourceOffsetProvider { | ||
| volatile boolean hasMoreData = true; | ||
| transient volatile long lagBytes = -1; | ||
| transient volatile String cloudCluster; | ||
| // Route fetchEndOffset/compareOffset to the bound BE (synced from job, not persisted). | ||
| @@ -304,8 +308,13 @@ public void setBoundBackendId(long boundBackendId) { | ||
| @Override | ||
| public void fetchRemoteMeta(Map<String, String> properties) throws Exception { | ||
| Backend backend = StreamingJobUtils.selectBackend(cloudCluster, boundBackendId); | ||
| JobBaseConfig requestParams = | ||
| new JobBaseConfig(getJobId().toString(), sourceType.name(), sourceProperties, getFrontendAddress()); | ||
| FetchEndOffsetRequest requestParams = | ||
| new FetchEndOffsetRequest( | ||
| getJobId().toString(), | ||
| sourceType.name(), | ||
| sourceProperties, | ||
| getFrontendAddress(), | ||
| getLagReferenceOffset()); | ||
| InternalService.PRequestCdcClientRequest request = InternalService.PRequestCdcClientRequest.newBuilder() | ||
| .setApi("/api/fetchEndOffset") | ||
| .setParams(new Gson().toJson(requestParams)).build(); | ||
| @@ -322,14 +331,17 @@ public void fetchRemoteMeta(Map<String, String> properties) throws Exception { | ||
| "Failed to get end offset from backend," + result.getStatus().getErrorMsgs(0) + ", response: " | ||
| + result.getResponse()); | ||
| } | ||
| Map<String, String> newEndOffset = parseCdcResponseData( | ||
| result.getResponse(), new TypeReference<Map<String, String>>() {}); | ||
| FetchEndOffsetResult fetchResult = parseFetchEndOffsetResponse(result.getResponse()); | ||
| Map<String, String> newEndOffset = fetchResult.getEndOffset(); | ||
| synchronized (splitsLock) { | ||
| // null→value also counts as a change: upstream may have advanced while fetch was blocked. | ||
| if (endBinlogOffset == null || !endBinlogOffset.equals(newEndOffset)) { | ||
| hasMoreData = true; | ||
| } | ||
| endBinlogOffset = newEndOffset; | ||
| if (!isSnapshotOnlyMode()) { | ||
| updateLagBytes(fetchResult.getLagBytes()); | ||
JNSimba marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| } | ||
| } catch (TimeoutException te) { | ||
| log.warn("cdc_client RPC timeout api=/api/fetchEndOffset jobId={} backend={}:{} timeout_sec={}", | ||
| @@ -342,6 +354,30 @@ public void fetchRemoteMeta(Map<String, String> properties) throws Exception { | ||
| } | ||
| } | ||
| Map<String, String> getLagReferenceOffset() { | ||
| if (isSnapshotOnlyMode()) { | ||
| return null; | ||
| } | ||
| synchronized (splitsLock) { | ||
| if (currentOffset != null && !currentOffset.snapshotSplit()) { | ||
| BinlogSplit binlogSplit = (BinlogSplit) currentOffset.getSplits().get(0); | ||
| if (MapUtils.isNotEmpty(binlogSplit.getStartingOffset())) { | ||
| return new HashMap<>(binlogSplit.getStartingOffset()); | ||
| } | ||
| } | ||
| if (sourceType == DataSourceType.POSTGRES) { | ||
| // PostgreSQL can use the replication slot's confirmed flush LSN during snapshot. | ||
| return null; | ||
| } | ||
| return finishedSplits.stream() | ||
| .map(SnapshotSplit::getHighWatermark) | ||
| .filter(MapUtils::isNotEmpty) | ||
| .findFirst() | ||
JNSimba marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| .map(HashMap::new) | ||
| .orElse(null); | ||
| } | ||
| } | ||
| @Override | ||
| public boolean hasMoreDataToConsume() { | ||
| if (currentOffset == null) { | ||
| @@ -945,6 +981,23 @@ <T> T parseCdcResponseData(String response, TypeReference<T> dataType) throws Jo | ||
| } | ||
| } | ||
| FetchEndOffsetResult parseFetchEndOffsetResponse(String response) throws JobException { | ||
| JsonNode data = parseCdcResponseData(response, new TypeReference<JsonNode>() {}); | ||
| if (data == null) { | ||
| throw new JobException(response); | ||
| } | ||
| try { | ||
| if (data.has("endOffset")) { | ||
| return objectMapper.convertValue(data, FetchEndOffsetResult.class); | ||
| } | ||
| Map<String, String> endOffset = objectMapper.convertValue( | ||
| data, new TypeReference<Map<String, String>>() {}); | ||
| return new FetchEndOffsetResult(endOffset, -1); | ||
| } catch (IllegalArgumentException exception) { | ||
| throw new JobException(response); | ||
| } | ||
| } | ||
| protected boolean checkNeedSplitChunks(Map<String, String> sourceProperties) { | ||
| String startMode = sourceProperties.get(DataSourceConfigKeys.OFFSET); | ||
| if (startMode == null) { | ||
| @@ -960,45 +1013,46 @@ protected boolean isSnapshotOnlyMode() { | ||
| } | ||
| @Override | ||
| public String getLag() { | ||
| if (currentOffset == null || currentOffset.snapshotSplit()) { | ||
| return ""; | ||
| } | ||
| // Source is idle (last task consumed no data), report zero lag | ||
| if (!hasMoreData) { | ||
| return "0"; | ||
| } | ||
| BinlogSplit binlogSplit = (BinlogSplit) currentOffset.getSplits().get(0); | ||
| Map<String, String> offsetMap = binlogSplit.getStartingOffset(); | ||
| if (MapUtils.isEmpty(offsetMap)) { | ||
| return ""; | ||
| } | ||
| long eventTimeMs = extractEventTimeMs(offsetMap); | ||
| if (eventTimeMs <= 0) { | ||
| return "0"; | ||
| } | ||
| long lagSec = (System.currentTimeMillis() - eventTimeMs) / 1000; | ||
| return String.valueOf(Math.max(lagSec, 0)); | ||
| public long getLagBytes() { | ||
| return lagBytes; | ||
| } | ||
| /** | ||
| * Extract event timestamp in milliseconds from binlog offset map. | ||
| * MySQL: ts_sec (seconds), PostgreSQL: ts_usec (microseconds). | ||
| */ | ||
| protected long extractEventTimeMs(Map<String, String> offsetMap) { | ||
| try { | ||
| String tsSec = offsetMap.get("ts_sec"); | ||
| if (tsSec != null) { | ||
| return Long.parseLong(tsSec) * 1000; | ||
| @Override | ||
| public long getLastSourceEventTimestampSeconds() { | ||
| synchronized (splitsLock) { | ||
| if (currentOffset == null || currentOffset.snapshotSplit()) { | ||
| return 0; | ||
| } | ||
| String tsUsec = offsetMap.get("ts_usec"); | ||
| if (tsUsec != null) { | ||
| return Long.parseLong(tsUsec) / 1000; | ||
| BinlogSplit binlogSplit = (BinlogSplit) currentOffset.getSplits().get(0); | ||
| Map<String, String> offsetMap = binlogSplit.getStartingOffset(); | ||
| if (MapUtils.isEmpty(offsetMap)) { | ||
| return 0; | ||
| } | ||
| } catch (NumberFormatException e) { | ||
| log.warn("Failed to parse event timestamp from offset: {}", offsetMap, e); | ||
| try { | ||
| String timestampSeconds = offsetMap.get("ts_sec"); | ||
| if (timestampSeconds != null) { | ||
| return Math.max(Long.parseLong(timestampSeconds), 0); | ||
| } | ||
| String timestampMicros = offsetMap.get("ts_usec"); | ||
| if (timestampMicros != null) { | ||
| return Math.max(Long.parseLong(timestampMicros) / 1_000_000, 0); | ||
| } | ||
| } catch (NumberFormatException e) { | ||
| log.warn("Failed to parse source event timestamp from offset: {}", offsetMap, e); | ||
| } | ||
| return 0; | ||
| } | ||
| } | ||
| @Override | ||
| public void resetLag() { | ||
| lagBytes = -1; | ||
| } | ||
| void updateLagBytes(long fetchedLagBytes) { | ||
| if (fetchedLagBytes >= 0) { | ||
| lagBytes = fetchedLagBytes; | ||
| } | ||
| return -1; | ||
| } | ||
| @Override | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.