StreamPark获取Flink中信息的两种方式源码解读
经过对SP的源码解读,发现SP对Flink的Metric等信息的监控分为两种实现方式,在K8S模式下获取的方式和其他方式有所区别,且K8S模式下为scala代码实现,其他模式为java代码实现,不过最终都是从Flink Web UI中获取数据。
一、当作业启动在yarn、独立、远程模式下时,SP获取flink信息
1、该模式下对Flink监控的入口类为FlinkRESTAPIWatcher.java,通过start方法作为监控Flink信息的入口,由代码分析可以得知,在以下两种条件下会执行。
(1)当程序启动或页面操作的任务,如启动/停止,需要立即返回状态。(频率1秒一次,连续10秒(10次))
(2)正常信息获取,每5秒获取一次
@Scheduled(fixedDelay = 1000)
public void start() {
// The application has been started at the first time, or the front-end is operating start/stop,
// need to return status info immediately.
if (lastWatchingTime == null || !OPTIONING.isEmpty()) {
doWatch();
} else if (System.currentTimeMillis() - lastOptionTime <= OPTION_INTERVAL) {
// The last operation time is less than option interval.(10 seconds)
doWatch();
} else if (System.currentTimeMillis() - lastWatchingTime >= WATCHING_INTERVAL) {
// Normal information obtain, check if there is 5 seconds interval between this time and the
// last time.(once every 5 seconds)
doWatch();
}
}
2、接下来是doWatch方法,该方法主要是通过线程池执行监控任务,可以看到这一段代码中主要获取状态的方法分为getFromFlinkRestApi和getFromYarnRestApi,该程序以getFromFlinkRestApi为主,当通过这个方法获取失败时,会通过getFromYarnRestApi方法进行获取。
private void doWatch() {
lastWatchingTime = System.currentTimeMillis();
for (Map.Entry<Long, Application> entry : WATCHING_APPS.entrySet()) {
EXECUTOR.execute(
() -> {
long key = entry.getKey();
Application application = entry.getValue();
final StopFrom stopFrom =
STOP_FROM_MAP.getOrDefault(key, null) == null
? StopFrom.NONE
: STOP_FROM_MAP.get(key);
final OptionState optionState = OPTIONING.get(key);
try {
// query status from flink rest api
Utils.required(application.getId() != null);
getFromFlinkRestApi(application, stopFrom);
} catch (Exception flinkException) {
// query status from yarn rest api
try {
getFromYarnRestApi(application, stopFrom);
} catch (Exception yarnException) {
/*
Query from flink's restAPI and yarn's restAPI both failed.
In this case, it is necessary to decide whether to return to the final state depending on the state being operated
*/
if (optionState == null || !optionState.equals(OptionState.STARTING)) {
// non-mapping
if (application.getState() != FlinkAppState.MAPPING.getValue()) {
log.error(
"FlinkRESTAPIWatcher getFromFlinkRestApi and getFromYarnRestApi error,job failed,savePoint expired!");
if (StopFrom.NONE.equals(stopFrom)) {
savePointService.expire(application.getId());
application.setState(FlinkAppState.LOST.getValue());
alertService.alert(application, FlinkAppState.LOST);
} else {
application.setState(FlinkAppState.CANCELED.getValue());
}
}
/*
This step means that the above two ways to get information have failed, and this step is the last step,
which will directly identify the mission as cancelled or lost.
Need clean savepoint.
*/
application.setEndTime(new Date());
cleanSavepoint(application);
cleanOptioning(optionState, key);
doPersistMetrics(application, true);
FlinkAppState appState = FlinkAppState.of(application.getState());
if (appState.equals(FlinkAppState.FAILED)
|| appState.equals(FlinkAppState.LOST)) {
alertService.alert(application, FlinkAppState.of(application.getState()));
if (appState.equals(FlinkAppState.FAILED)) {
try {
applicationService.start(application, true);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
}
}
}
});
}
}
3、通过分析getFromFlinkRestApi方法得知,该方法主要调用的方法为httpJobsOverview、httpCheckpoints,通过这两个方法来获取Flink信息
private void getFromFlinkRestApi(Application application, StopFrom stopFrom) throws Exception {
FlinkCluster flinkCluster = getFlinkCluster(application);
JobsOverview jobsOverview = httpJobsOverview(application, flinkCluster);
Optional<JobsOverview.Job> optional;
ExecutionMode execMode = application.getExecutionModeEnum();
if (ExecutionMode.YARN_APPLICATION.equals(execMode)
|| ExecutionMode.YARN_PER_JOB.equals(execMode)) {
optional =
jobsOverview.getJobs().size() > 1
? jobsOverview.getJobs().stream()
.filter(a -> StringUtils.equals(application.getJobId(), a.getId()))
.findFirst()
: jobsOverview.getJobs().stream().findFirst();
} else {
optional =
jobsOverview.getJobs().stream()
.filter(x -> x.getId().equals(application.getJobId()))
.findFirst();
}
if (optional.isPresent()) {
JobsOverview.Job jobOverview = optional.get();
FlinkAppState currentState = FlinkAppState.of(jobOverview.getState());
if (!FlinkAppState.OTHER.equals(currentState)) {
try {
// 1) set info from JobOverview
handleJobOverview(application, jobOverview);
} catch (Exception e) {
log.error("get flink jobOverview error: {}", e.getMessage(), e);
}
try {
// 2) CheckPoints
handleCheckPoints(application);
} catch (Exception e) {
log.error("get flink jobOverview error: {}", e.getMessage(), e);
}
// 3) savePoint obsolete check and NEED_START check
OptionState optionState = OPTIONING.get(application.getId());
if (currentState.equals(FlinkAppState.RUNNING)) {
handleRunningState(application, optionState, currentState);
} else {
handleNotRunState(application, optionState, currentState, stopFrom);
}
}
}
}
4、通过分析httpOverview、httpJobsOverview、httpCheckpoints方法,得知SP是通过调用Flink的WebUI界面的http请求,来获取Flink相关信息。
private Overview httpOverview(Application application, FlinkCluster flinkCluster)
throws IOException {
String appId = application.getAppId();
if (appId != null) {
if (application.getExecutionModeEnum().equals(ExecutionMode.YARN_APPLICATION)
|| application.getExecutionModeEnum().equals(ExecutionMode.YARN_PER_JOB)) {
String reqURL;
if (StringUtils.isEmpty(application.getJobManagerUrl())) {
String format = "proxy/%s/overview";
reqURL = String.format(format, appId);
} else {
String format = "%s/overview";
reqURL = String.format(format, application.getJobManagerUrl());
}
return yarnRestRequest(reqURL, Overview.class);
}
}
return null;
}
private JobsOverview httpJobsOverview(Application application, FlinkCluster flinkCluster)
throws Exception {
final String flinkUrl = "jobs/overview";
ExecutionMode execMode = application.getExecutionModeEnum();
if (ExecutionMode.YARN_PER_JOB.equals(execMode)
|| ExecutionMode.YARN_APPLICATION.equals(execMode)) {
String reqURL;
if (StringUtils.isEmpty(application.getJobManagerUrl())) {
String format = "proxy/%s/" + flinkUrl;
reqURL = String.format(format, application.getAppId());
} else {
String format = "%s/" + flinkUrl;
reqURL = String.format(format, application.getJobManagerUrl());
}
return yarnRestRequest(reqURL, JobsOverview.class);
} else if (ExecutionMode.REMOTE.equals(execMode)
|| ExecutionMode.YARN_SESSION.equals(execMode)) {
if (application.getJobId() != null) {
String remoteUrl = flinkCluster.getAddress() + "/" + flinkUrl;
JobsOverview jobsOverview = httpRestRequest(remoteUrl, JobsOverview.class);
if (jobsOverview != null) {
List<JobsOverview.Job> jobs =
jobsOverview.getJobs().stream()
.filter(x -> x.getId().equals(application.getJobId()))
.collect(Collectors.toList());
jobsOverview.setJobs(jobs);
}
return jobsOverview;
}
}
return null;
}
private CheckPoints httpCheckpoints(Application application, FlinkCluster flinkCluster)
throws IOException {
final String flinkUrl = "jobs/%s/checkpoints";
ExecutionMode execMode = application.getExecutionModeEnum();
if (ExecutionMode.YARN_PER_JOB.equals(execMode)
|| ExecutionMode.YARN_APPLICATION.equals(execMode)) {
String reqURL;
if (StringUtils.isEmpty(application.getJobManagerUrl())) {
String format = "proxy/%s/" + flinkUrl;
reqURL = String.format(format, application.getAppId(), application.getJobId());
} else {
String format = "%s/" + flinkUrl;
reqURL = String.format(format, application.getJobManagerUrl(), application.getJobId());
}
return yarnRestRequest(reqURL, CheckPoints.class);
} else if (ExecutionMode.REMOTE.equals(execMode)
|| ExecutionMode.YARN_SESSION.equals(execMode)) {
if (application.getJobId() != null) {
String remoteUrl =
flinkCluster.getAddress() + "/" + String.format(flinkUrl, application.getJobId());
return httpRestRequest(remoteUrl, CheckPoints.class);
}
}
return null;
}
二、当作业启动在K8S模式下时,SP获取flink信息
1、该模式下对Flink监控的入口类为FlinkK8sWatcherWrapper.java,通过registerFlinkK8sWatcher方法作为监控Flink信息的入口,由代码分析可以得知,该方法主要做的是加载配置文件、加载监控类。
@Bean(destroyMethod = "close")
public FlinkK8sWatcher registerFlinkK8sWatcher() {
// lazy start tracking monitor
FlinkK8sWatcher flinkK8sWatcher =
FlinkK8sWatcherFactory.createInstance(FlinkTrackConfig.fromConfigHub(), true);
initFlinkK8sWatcher(flinkK8sWatcher);
/* Dev scaffold: watch flink k8s tracking cache,
see org.apache.streampark.flink.kubernetes.helper.KubernetesWatcherHelper for items.
Example:
KubernetesWatcherHelper.watchTrackIdsCache(flinkK8sWatcher);
KubernetesWatcherHelper.watchJobStatusCache(flinkK8sWatcher);
KubernetesWatcherHelper.watchAggClusterMetricsCache(flinkK8sWatcher);
KubernetesWatcherHelper.watchClusterMetricsCache(flinkK8sWatcher);
*/
return flinkK8sWatcher;
}
2、通过分析TrackConfig.scala中的**FlinkTrackConfig.fromConfigHub()**方法得知,获取作业状态默认模式下为每5秒获取一次,获取集群Metric信息为每10秒获取一次,超时时间为120秒
object JobStatusWatcherConfig {
def defaultConf: JobStatusWatcherConfig = JobStatusWatcherConfig(
requestTimeoutSec = 120,
requestIntervalSec = 5,
silentStateJobKeepTrackingSec = 60)
def debugConf: JobStatusWatcherConfig = JobStatusWatcherConfig(
requestTimeoutSec = 120,
requestIntervalSec = 2,
silentStateJobKeepTrackingSec = 5)
}
object MetricWatcherConfig {
def defaultConf: MetricWatcherConfig = MetricWatcherConfig(
requestTimeoutSec = 120,
requestIntervalSec = 10)
def debugConf: MetricWatcherConfig = MetricWatcherConfig(
requestTimeoutSec = 120,
requestIntervalSec = 2)
}
3、通过分析initFlinkK8sWatcher方法得知,注册了一个名为FlinkK8sChangeEventListener监听器,将此监听器注册到了eventBus
private void initFlinkK8sWatcher(@Nonnull FlinkK8sWatcher trackMonitor) {
// register change event listener
trackMonitor.registerListener(flinkK8sChangeEventListener);
// recovery tracking list
List<TrackId> k8sApp = getK8sWatchingApps();
k8sApp.forEach(trackMonitor::doWatching);
}
4、该监听器的主要作用是捕获FlinkJobStatusChangeEvent,然后将其永久存储到数据库中。实际更新
@Subscribe
public void subscribeJobStatusChange(FlinkJobStatusChangeEvent event) {
JobStatusCV jobStatus = event.jobStatus();
TrackId trackId = event.trackId();
// get pre application record
Application app = applicationService.getById(trackId.appId());
if (app == null) {
return;
}
// update application record
setByJobStatusCV(app, jobStatus);
applicationService.persistMetrics(app);
// email alerts when necessary
FlinkAppState state = FlinkAppState.of(app.getState());
if (FlinkAppState.FAILED.equals(state)
|| FlinkAppState.LOST.equals(state)
|| FlinkAppState.RESTARTING.equals(state)
|| FlinkAppState.FINISHED.equals(state)) {
IngressController.deleteIngress(app.getClusterId(), app.getK8sNamespace());
executor.execute(() -> alertService.alert(app, state));
}
}
5、通过查看DefaultFlinkK8sWatcher.scala源码得知,其中创建了用于存储跟踪结果的缓存池、用于更改事件的eventBus、Flink几种指标监视器的实现类FlinkK8sEventWatcher.scala、FlinkJobStatusWatcher.scala、FlinkMetricWatcher.scala、FlinkCheckpointWatcher.scala
class DefaultFlinkK8sWatcher(conf: FlinkTrackConfig = FlinkTrackConfig.defaultConf) extends FlinkK8sWatcher {
// cache pool for storage tracking result
implicit val watchController: FlinkK8sWatchController = new FlinkK8sWatchController()
// eventBus for change event
implicit lazy val eventBus: ChangeEventBus = {
val eventBus = new ChangeEventBus()
eventBus.registerListener(new BuildInEventListener)
eventBus
}
// remote server tracking watcher
val k8sEventWatcher = new FlinkK8sEventWatcher()
val jobStatusWatcher = new FlinkJobStatusWatcher(conf.jobStatusWatcherConf)
val metricsWatcher = new FlinkMetricWatcher(conf.metricWatcherConf)
val checkpointWatcher = new FlinkCheckpointWatcher(conf.metricWatcherConf)
private[this] val allWatchers = Array[FlinkWatcher](k8sEventWatcher, jobStatusWatcher, metricsWatcher, checkpointWatcher)
6、通过查看FlinkJobStatusWatcher.scala源码可以得知,该模式也是通过Flink的WEB UI界面的接口获取FLink的相关信息,只不过实现方式不同
private def callJobsOverviewsApi(restUrl: String): Option[JobDetails] = {
val jobDetails = JobDetails.as(
Request.get(s"$restUrl/jobs/overview")
.connectTimeout(Timeout.ofSeconds(KubernetesRetriever.FLINK_REST_AWAIT_TIMEOUT_SEC))
.responseTimeout(Timeout.ofSeconds(KubernetesRetriever.FLINK_CLIENT_TIMEOUT_SEC))
.execute.returnContent().asString(StandardCharsets.UTF_8))
jobDetails
}
private[kubernetes] object JobDetails {
@transient
implicit lazy val formats: DefaultFormats.type = org.json4s.DefaultFormats
def as(json: String): Option[JobDetails] = {
Try(parse(json)) match {
case Success(ok) =>
ok \ "jobs" match {
case JNothing | JNull => None
case JArray(arr) =>
val details = arr.map(x => {
val task = x \ "tasks"
JobDetail(
(x \ "jid").extractOpt[String].orNull,
(x \ "name").extractOpt[String].orNull,
(x \ "state").extractOpt[String].orNull,
(x \ "start-time").extractOpt[Long].getOrElse(0),
(x \ "end-time").extractOpt[Long].getOrElse(0),
(x \ "duration").extractOpt[Long].getOrElse(0),
(x \ "last-modification").extractOpt[Long].getOrElse(0),
JobTask(
(task \ "total").extractOpt[Int].getOrElse(0),
(task \ "created").extractOpt[Int].getOrElse(0),
(task \ "scheduled").extractOpt[Int].getOrElse(0),
(task \ "deploying").extractOpt[Int].getOrElse(0),
(task \ "running").extractOpt[Int].getOrElse(0),
(task \ "finished").extractOpt[Int].getOrElse(0),
(task \ "canceling").extractOpt[Int].getOrElse(0),
(task \ "canceled").extractOpt[Int].getOrElse(0),
(task \ "failed").extractOpt[Int].getOrElse(0),
(task \ "reconciling").extractOpt[Int].getOrElse(0),
(task \ "initializing").extractOpt[Int].getOrElse(0)))
}).toArray
Some(JobDetails(details))
case _ => None
}
case Failure(_) => None
}
}
}
三、两种模式下获取到的Flink信息
1、通过分析代码得知,可获取到的信息有
(1)集群信息:
{
"taskmanagers":1,
"slots-total":3,
"slots-available":2,
"jobs-running":1,
"jobs-finished":0,
"jobs-cancelled":0,
"jobs-failed":0,
"flink-version":"1.16.0",
"flink-commit":"af6eff8"
}
(2)作业信息:
{
"jobs":[
{
"jid":"34cfd6635b35505f9365460f8920e551",
"name":"Flink Streaming Job",
"state":"RUNNING",
"start-time":1682231940127,
"end-time":-1,
"duration":143155,
"last-modification":1682231940664,
"tasks":{
"total":2,
"created":0,
"scheduled":0,
"deploying":0,
"running":2,
"finished":0,
"canceling":0,
"canceled":0,
"failed":0,
"reconciling":0,
"initializing":0
}
}
]
}
(3)checkpoint信息:
{
"counts":{
"restored":0,
"total":13,
"in_progress":0,
"completed":13,
"failed":0
},
"summary":{
"checkpointed_size":{
"min":1878,
"max":1878,
"avg":1878,
"p50":1878,
"p90":1878,
"p95":1878,
"p99":1878,
"p999":1878
},
"state_size":{
"min":1878,
"max":1878,
"avg":1878,
"p50":1878,
"p90":1878,
"p95":1878,
"p99":1878,
"p999":1878
},
"end_to_end_duration":{
"min":11,
"max":79,
"avg":21,
"p50":18,
"p90":55.79999999999998,
"p95":79,
"p99":79,
"p999":79
},
"alignment_buffered":{
"min":0,
"max":0,
"avg":0,
"p50":0,
"p90":0,
"p95":0,
"p99":0,
"p999":0
},
"processed_data":{
"min":0,
"max":0,
"avg":0,
"p50":0,
"p90":0,
"p95":0,
"p99":0,
"p999":0
},
"persisted_data":{
"min":0,
"max":0,
"avg":0,
"p50":0,
"p90":0,
"p95":0,
"p99":0,
"p999":0
}
},
"latest":{
"completed":{
"className":"completed",
"id":13,
"status":"COMPLETED",
"is_savepoint":false,
"trigger_timestamp":1682232181234,
"latest_ack_timestamp":1682232181246,
"checkpointed_size":1878,
"state_size":1878,
"end_to_end_duration":12,
"alignment_buffered":0,
"processed_data":0,
"persisted_data":0,
"num_subtasks":2,
"num_acknowledged_subtasks":2,
"checkpoint_type":"CHECKPOINT",
"tasks":{
},
"external_path":"file:/Users/wangkeshuai/Documents/checkpoint/34cfd6635b35505f9365460f8920e551/chk-13",
"discarded":false
},
"savepoint":null,
"failed":null,
"restored":null
},
"history":[
{
"className":"completed",
"id":13,
"status":"COMPLETED",
"is_savepoint":false,
"trigger_timestamp":1682232181234,
"latest_ack_timestamp":1682232181246,
"checkpointed_size":1878,
"state_size":1878,
"end_to_end_duration":12,
"alignment_buffered":0,
"processed_data":0,
"persisted_data":0,
"num_subtasks":2,
"num_acknowledged_subtasks":2,
"checkpoint_type":"CHECKPOINT",
"tasks":{
},
"external_path":"file:/Users/wangkeshuai/Documents/checkpoint/34cfd6635b35505f9365460f8920e551/chk-13",
"discarded":false
},
{
"className":"completed",
"id":12,
"status":"COMPLETED",
"is_savepoint":false,
"trigger_timestamp":1682232161230,
"latest_ack_timestamp":1682232161243,
"checkpointed_size":1878,
"state_size":1878,
"end_to_end_duration":13,
"alignment_buffered":0,
"processed_data":0,
"persisted_data":0,
"num_subtasks":2,
"num_acknowledged_subtasks":2,
"checkpoint_type":"CHECKPOINT",
"tasks":{
},
"external_path":"file:/Users/wangkeshuai/Documents/checkpoint/34cfd6635b35505f9365460f8920e551/chk-12",
"discarded":true
},
{
"className":"completed",
"id":11,
"status":"COMPLETED",
"is_savepoint":false,
"trigger_timestamp":1682232141233,
"latest_ack_timestamp":1682232141247,
"checkpointed_size":1878,
"state_size":1878,
"end_to_end_duration":14,
"alignment_buffered":0,
"processed_data":0,
"persisted_data":0,
"num_subtasks":2,
"num_acknowledged_subtasks":2,
"checkpoint_type":"CHECKPOINT",
"tasks":{
},
"external_path":"file:/Users/wangkeshuai/Documents/checkpoint/34cfd6635b35505f9365460f8920e551/chk-11",
"discarded":true
},
{
"className":"completed",
"id":10,
"status":"COMPLETED",
"is_savepoint":false,
"trigger_timestamp":1682232121237,
"latest_ack_timestamp":1682232121255,
"checkpointed_size":1878,
"state_size":1878,
"end_to_end_duration":18,
"alignment_buffered":0,
"processed_data":0,
"persisted_data":0,
"num_subtasks":2,
"num_acknowledged_subtasks":2,
"checkpoint_type":"CHECKPOINT",
"tasks":{
},
"external_path":"file:/Users/wangkeshuai/Documents/checkpoint/34cfd6635b35505f9365460f8920e551/chk-10",
"discarded":true
},
{
"className":"completed",
"id":9,
"status":"COMPLETED",
"is_savepoint":false,
"trigger_timestamp":1682232101236,
"latest_ack_timestamp":1682232101247,
"checkpointed_size":1878,
"state_size":1878,
"end_to_end_duration":11,
"alignment_buffered":0,
"processed_data":0,
"persisted_data":0,
"num_subtasks":2,
"num_acknowledged_subtasks":2,
"checkpoint_type":"CHECKPOINT",
"tasks":{
},
"external_path":"file:/Users/wangkeshuai/Documents/checkpoint/34cfd6635b35505f9365460f8920e551/chk-9",
"discarded":true
},
{
"className":"completed",
"id":8,
"status":"COMPLETED",
"is_savepoint":false,
"trigger_timestamp":1682232081238,
"latest_ack_timestamp":1682232081254,
"checkpointed_size":1878,
"state_size":1878,
"end_to_end_duration":16,
"alignment_buffered":0,
"processed_data":0,
"persisted_data":0,
"num_subtasks":2,
"num_acknowledged_subtasks":2,
"checkpoint_type":"CHECKPOINT",
"tasks":{
},
"external_path":"file:/Users/wangkeshuai/Documents/checkpoint/34cfd6635b35505f9365460f8920e551/chk-8",
"discarded":true
},
{
"className":"completed",
"id":7,
"status":"COMPLETED",
"is_savepoint":false,
"trigger_timestamp":1682232061239,
"latest_ack_timestamp":1682232061257,
"checkpointed_size":1878,
"state_size":1878,
"end_to_end_duration":18,
"alignment_buffered":0,
"processed_data":0,
"persisted_data":0,
"num_subtasks":2,
"num_acknowledged_subtasks":2,
"checkpoint_type":"CHECKPOINT",
"tasks":{
},
"external_path":"file:/Users/wangkeshuai/Documents/checkpoint/34cfd6635b35505f9365460f8920e551/chk-7",
"discarded":true
},
{
"className":"completed",
"id":6,
"status":"COMPLETED",
"is_savepoint":false,
"trigger_timestamp":1682232041240,
"latest_ack_timestamp":1682232041259,
"checkpointed_size":1878,
"state_size":1878,
"end_to_end_duration":19,
"alignment_buffered":0,
"processed_data":0,
"persisted_data":0,
"num_subtasks":2,
"num_acknowledged_subtasks":2,
"checkpoint_type":"CHECKPOINT",
"tasks":{
},
"external_path":"file:/Users/wangkeshuai/Documents/checkpoint/34cfd6635b35505f9365460f8920e551/chk-6",
"discarded":true
},
{
"className":"completed",
"id":5,
"status":"COMPLETED",
"is_savepoint":false,
"trigger_timestamp":1682232021241,
"latest_ack_timestamp":1682232021260,
"checkpointed_size":1878,
"state_size":1878,
"end_to_end_duration":19,
"alignment_buffered":0,
"processed_data":0,
"persisted_data":0,
"num_subtasks":2,
"num_acknowledged_subtasks":2,
"checkpoint_type":"CHECKPOINT",
"tasks":{
},
"external_path":"file:/Users/wangkeshuai/Documents/checkpoint/34cfd6635b35505f9365460f8920e551/chk-5",
"discarded":true
},
{
"className":"completed",
"id":4,
"status":"COMPLETED",
"is_savepoint":false,
"trigger_timestamp":1682232001238,
"latest_ack_timestamp":1682232001257,
"checkpointed_size":1878,
"state_size":1878,
"end_to_end_duration":19,
"alignment_buffered":0,
"processed_data":0,
"persisted_data":0,
"num_subtasks":2,
"num_acknowledged_subtasks":2,
"checkpoint_type":"CHECKPOINT",
"tasks":{
},
"external_path":"file:/Users/wangkeshuai/Documents/checkpoint/34cfd6635b35505f9365460f8920e551/chk-4",
"discarded":true
}
]
}