[ 
https://issues.apache.org/jira/browse/FLINK-41450?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Weiqing Yang updated FLINK-41450:
---------------------------------
    Summary: Release Testing: Verify FLIP-485: Add UDF Metrics  (was:  Release 
Testing: Verify FLIP-485: Add UDF Metrics)

> Release Testing: Verify FLIP-485: Add UDF Metrics
> -------------------------------------------------
>
>                 Key: FLINK-41450
>                 URL: https://issues.apache.org/jira/browse/FLINK-41450
>             Project: Flink
>          Issue Type: Sub-task
>            Reporter: Weiqing Yang
>            Priority: Blocker
>              Labels: release-testing
>             Fix For: 2.4.0
>
>
> [FLIP-485|https://cwiki.apache.org/confluence/display/FLINK/FLIP-485%3A+Add+UDF+Metrics]
>  adds per-function operator metrics for SQL/Table user-defined functions. 
> They are off by default and enabled with {{table.exec.udf-metric-enabled}}. 
> Each function gets two metrics on the operator that runs it, under 
> {{<operator scope>.udf.<udf_name>}}:
> * {{udfProcessingTime}}: a histogram in nanoseconds. Only one call in every 
> {{table.exec.udf-metric.sample-interval}} (default 100) is timed. For 
> asynchronous functions it spans dispatch to completion.
> * {{udfExceptionCount}}: a counter of exceptions escaping the function, 
> counted on every call. For asynchronous functions it counts exceptional 
> completions.
> Docs: [UDF 
> metrics|https://nightlies.apache.org/flink/flink-docs-release-2.4/docs/ops/metrics/#udf-metrics]
>  and the 
> [UDF|https://nightlies.apache.org/flink/flink-docs-release-2.4/docs/ops/metrics/#udf]
>  section of the system metrics. Unit and IT tests cover the code paths. This 
> ticket is about seeing the metrics on a real cluster for each kind of 
> function: synchronous and asynchronous, scalar and table.
> h3. Known issues (already filed, fixes in progress)
> These came up in pre-testing and are tracked for 2.4.0. There is no need to 
> file them again. If you see different behavior, comment on the ticket.
> * FLINK-41447: with metrics enabled, a 
> {{table.exec.udf-metric.sample-interval}} below 1 is accepted when the query 
> is submitted, and the job then restarts in a loop instead of being rejected.
> * FLINK-41448: the metric group is named after the spelling used in the 
> query, so {{myFunc(x)}} and {{MYFUNC(x)}} on one function create two groups.
> h3. 0. Setup
> Build {{release-2.4}}, or use the 2.4.0 RC. The steps read metrics through 
> the REST API with {{curl}} and {{jq}}. If you want the Web UI as well, drop 
> {{\-Pskip-webui-build}}.
> {code:bash}
> ./mvnw clean install -DskipTests -Dfast -Pskip-webui-build -T1C
> cd build-target
> mkdir -p /tmp/flip485/src/flip485 /tmp/flip485/classes
> {code}
> In {{conf/config.yaml}}, change {{numberOfTaskSlots: 1}} to {{10}} and the 
> TaskManager {{process.size: 1728m}} to {{4096m}} (steps 1 to 7 run nine small 
> jobs side by side). Then append the Slf4j reporter, which is only needed for 
> steps 5 and 6:
> {code:yaml}
> metrics.reporter.slf4j.factory.class: 
> org.apache.flink.metrics.slf4j.Slf4jReporterFactory
> metrics.reporter.slf4j.interval: 10 SECONDS
> {code}
> Create the test functions and compile them against the distribution. Run this 
> from {{build-target}}:
> {code:bash}
> cat > /tmp/flip485/src/flip485/SlowUpper.java <<'EOF'
> package flip485;
> import org.apache.flink.table.functions.ScalarFunction;
> /** Synchronous scalar function that takes about 2 ms per call. */
> public class SlowUpper extends ScalarFunction {
>     public String eval(String s) throws InterruptedException {
>         Thread.sleep(2);
>         return s == null ? null : s.toUpperCase();
>     }
> }
> EOF
> cat > /tmp/flip485/src/flip485/Twice.java <<'EOF'
> package flip485;
> import org.apache.flink.table.functions.TableFunction;
> /** Synchronous table function that emits every input twice. */
> public class Twice extends TableFunction<Long> {
>     public void eval(Long x) { collect(x); collect(x); }
> }
> EOF
> cat > /tmp/flip485/src/flip485/AsyncLen.java <<'EOF'
> package flip485;
> import org.apache.flink.table.functions.AsyncScalarFunction;
> import org.apache.flink.table.functions.FunctionContext;
> import java.util.concurrent.*;
> /** Asynchronous scalar function that completes 50 ms after it is called. */
> public class AsyncLen extends AsyncScalarFunction {
>     private transient ScheduledExecutorService executor;
>     @Override public void open(FunctionContext context) { executor = 
> Executors.newSingleThreadScheduledExecutor(); }
>     @Override public void close() { if (executor != null) { 
> executor.shutdownNow(); } }
>     public void eval(CompletableFuture<Integer> future, String s) {
>         executor.schedule(() -> future.complete(s.length()), 50, 
> TimeUnit.MILLISECONDS);
>     }
> }
> EOF
> cat > /tmp/flip485/src/flip485/AsyncTwice.java <<'EOF'
> package flip485;
> import org.apache.flink.table.functions.AsyncTableFunction;
> import org.apache.flink.table.functions.FunctionContext;
> import java.util.*;
> import java.util.concurrent.*;
> /** Asynchronous table function that emits every input twice, 50 ms after it 
> is called. */
> public class AsyncTwice extends AsyncTableFunction<Long> {
>     private transient ScheduledExecutorService executor;
>     @Override public void open(FunctionContext context) { executor = 
> Executors.newSingleThreadScheduledExecutor(); }
>     @Override public void close() { if (executor != null) { 
> executor.shutdownNow(); } }
>     public void eval(CompletableFuture<Collection<Long>> future, Long x) {
>         executor.schedule(() -> future.complete(Arrays.asList(x, x)), 50, 
> TimeUnit.MILLISECONDS);
>     }
> }
> EOF
> cat > /tmp/flip485/src/flip485/AsyncFlaky.java <<'EOF'
> package flip485;
> import org.apache.flink.table.functions.AsyncScalarFunction;
> import org.apache.flink.table.functions.FunctionContext;
> import java.util.Set;
> import java.util.concurrent.*;
> /** The first attempt fails for every id divisible by 10; the operator's 
> retry then succeeds. */
> public class AsyncFlaky extends AsyncScalarFunction {
>     private transient ScheduledExecutorService executor;
>     private transient Set<Long> failedOnce;
>     @Override public void open(FunctionContext context) {
>         executor = Executors.newSingleThreadScheduledExecutor();
>         failedOnce = ConcurrentHashMap.newKeySet();
>     }
>     @Override public void close() { if (executor != null) { 
> executor.shutdownNow(); } }
>     public void eval(CompletableFuture<Long> future, Long id) {
>         executor.schedule(() -> {
>             if (id % 10 == 0 && failedOnce.add(id)) {
>                 future.completeExceptionally(new RuntimeException("first 
> attempt fails"));
>             } else {
>                 future.complete(id);
>             }
>         }, 10, TimeUnit.MILLISECONDS);
>     }
> }
> EOF
> javac -cp "lib/*" -d /tmp/flip485/classes /tmp/flip485/src/flip485/*.java
> jar cf /tmp/flip485/flip485-udfs.jar -C /tmp/flip485/classes .
> {code}
> Save the following as {{/tmp/flip485/init.sql}}. Keep {{fields.id.end}} 
> small: the datagen sequence generator holds its whole range in memory.
> {code:sql}
> ADD JAR '/tmp/flip485/flip485-udfs.jar';
> CREATE TEMPORARY SYSTEM FUNCTION slow_upper AS 'flip485.SlowUpper';
> CREATE TEMPORARY SYSTEM FUNCTION twice AS 'flip485.Twice';
> CREATE TEMPORARY SYSTEM FUNCTION async_len AS 'flip485.AsyncLen';
> CREATE TEMPORARY SYSTEM FUNCTION async_twice AS 'flip485.AsyncTwice';
> CREATE TEMPORARY SYSTEM FUNCTION async_flaky AS 'flip485.AsyncFlaky';
> CREATE TABLE src (id BIGINT, name STRING) WITH (
>   'connector' = 'datagen', 'rows-per-second' = '20',
>   'fields.id.kind' = 'sequence', 'fields.id.start' = '1', 'fields.id.end' = 
> '100000',
>   'fields.name.length' = '8');
> CREATE TABLE bh_str (s STRING) WITH ('connector' = 'blackhole');
> CREATE TABLE bh_str2 (a STRING, b STRING) WITH ('connector' = 'blackhole');
> CREATE TABLE bh_long (x BIGINT) WITH ('connector' = 'blackhole');
> CREATE TABLE bh_int (n INT) WITH ('connector' = 'blackhole');
> {code}
> Save this helper as {{/tmp/flip485/udf-metrics.sh}} and {{chmod +x}} it. It 
> prints every UDF metric of every running job:
> {code:bash}
> #!/usr/bin/env bash
> REST=${REST:-http://localhost:8081}
> for jid in $(curl -s $REST/jobs | jq -r '.jobs[] | select(.status=="RUNNING") 
> | .id'); do
>   job=$(curl -s $REST/jobs/$jid)
>   echo "== $(echo "$job" | jq -r .name)"
>   for vid in $(echo "$job" | jq -r '.vertices[].id'); do
>     ids=$(curl -sg "$REST/jobs/$jid/vertices/$vid/metrics" | jq -r '.[].id' | 
> grep '\.udf\.' | paste -sd, -)
>     [ -z "$ids" ] && continue
>     curl -sg "$REST/jobs/$jid/vertices/$vid/metrics?get=$ids" | jq -r '.[] | 
> "   \(.id) = \(.value)"' | sort
>   done
> done
> {code}
> Start the cluster with {{./bin/start-cluster.sh}} and the SQL Client with 
> {{./bin/sql-client.sh \-i /tmp/flip485/init.sql}}. Run all statements below 
> in that session. Each {{INSERT INTO}} starts a detached job, and 
> {{pipeline.name}} names it so the helper output is easy to read.
> The REST API serves metrics from a store that refreshes every 10 seconds. If 
> a job that just started prints no metric lines, run the helper again.
> h3. 1. Metrics are off by default
> {code:sql}
> SET 'pipeline.name' = 'flip485-off';
> INSERT INTO bh_str SELECT slow_upper(name) FROM src;
> {code}
> *Expected:* {{/tmp/flip485/udf-metrics.sh}} prints the {{== flip485-off}} 
> header and no metric lines.
> h3. 2. Synchronous scalar and table functions
> {code:sql}
> SET 'table.exec.udf-metric-enabled' = 'true';
> SET 'table.exec.udf-metric.sample-interval' = '1';
> SET 'pipeline.name' = 'flip485-sync-scalar';
> INSERT INTO bh_str SELECT slow_upper(name) FROM src;
> SET 'pipeline.name' = 'flip485-sync-table';
> INSERT INTO bh_long SELECT t.x FROM src, LATERAL TABLE(twice(id)) AS t(x);
> {code}
> *Expected:* the {{udfProcessingTime}} statistics ({{_min}}, {{_max}}, 
> {{_mean}}, {{_median}}, {{_stddev}}, {{_p75}} to {{_p999}}) and 
> {{udfExceptionCount}} appear for both jobs, for example:
> {noformat}
> == flip485-sync-scalar
>    0.Calc[5].udf.slow_upper.udfExceptionCount = 0
>    0.Calc[5].udf.slow_upper.udfProcessingTime_mean = 2995844.07
>    0.Calc[5].udf.slow_upper.udfProcessingTime_min = 2191334
> == flip485-sync-table
>    0.Correlate[8].udf.twice.udfExceptionCount = 0
>    0.Correlate[8].udf.twice.udfProcessingTime_mean = 923.15
> {noformat}
> {{slow_upper}} sleeps 2 ms, so its {{_min}} is at least 2,000,000 ns. 
> {{twice}} does no work, so it stays in the microsecond range. The operator 
> ids in brackets will differ.
> h3. 3. Asynchronous scalar and table functions
> {code:sql}
> SET 'pipeline.name' = 'flip485-async-scalar';
> INSERT INTO bh_int SELECT async_len(name) FROM src;
> SET 'pipeline.name' = 'flip485-async-table';
> INSERT INTO bh_long SELECT t.x FROM src, LATERAL TABLE(async_twice(id)) AS 
> t(x);
> {code}
> *Expected:* metrics on the {{AsyncCalc}} and {{AsyncCorrelate}} operators, 
> under {{udf.async_len}} and {{udf.async_twice}}. Both functions complete 50 
> ms after the call, so {{udfProcessingTime_min}} is at least 50,000,000 ns 
> (larger values may print in E-notation, such as {{5.67E7}}). This shows the 
> timing covers dispatch to completion, not just the {{eval}} call.
> h3. 4. Failed asynchronous calls are counted and the job keeps running
> {code:sql}
> SET 'pipeline.name' = 'flip485-async-flaky';
> INSERT INTO bh_long SELECT async_flaky(id) FROM src;
> {code}
> *Expected:* {{udf.async_flaky.udfExceptionCount}} grows by about one per ten 
> input rows (about two per second), and the job stays {{RUNNING}}. Every id 
> divisible by 10 fails its first attempt, and the retry 
> ({{table.exec.async-scalar.max-attempts}}, default 3) succeeds.
> h3. 5. Sampling
> {code:sql}
> RESET 'table.exec.udf-metric.sample-interval';
> SET 'pipeline.name' = 'flip485-sampled-default';
> INSERT INTO bh_str SELECT slow_upper(name) FROM src;
> {code}
> The REST API shows histogram statistics but not how many samples were taken, 
> so use the Slf4j reporter's {{count}}, which it logs every 10 seconds:
> {code:bash}
> grep -oE '[^ ]*(flip485-sync-scalar|flip485-sampled-default|flip485-shared)[^ 
> ]*udfProcessingTime: count=[0-9]+' log/flink-*-taskexecutor-*.log | tail -9
> {code}
> *Expected:* for {{flip485-sampled-default}} (default interval 100, 20 rows 
> per second) the count grows by about 2 per report, one sample every 5 
> seconds. For {{flip485-sync-scalar}} (interval 1) it reaches 128 within 
> seconds and stays there, because the histogram keeps the latest 128 samples.
> h3. 6. The same function used twice in one operator shares one metric group
> Still at the default interval from step 5:
> {code:sql}
> SET 'pipeline.name' = 'flip485-shared';
> INSERT INTO bh_str2 SELECT slow_upper(name), slow_upper(CAST(id AS STRING)) 
> FROM src;
> {code}
> *Expected:* the helper shows a single {{udf.slow_upper}} group on this job's 
> {{Calc}}. Both call sites feed it, so in the grep from step 5 its count grows 
> about twice as fast as {{flip485-sampled-default}}'s: about 4 per report 
> instead of 2. Use different arguments as above. With identical arguments the 
> planner computes the call only once. Spell the function the same way at both 
> call sites. Different spellings currently create separate groups 
> (FLINK-41448).
> h3. 7. Batch mode
> The two options also apply to batch jobs, although the configuration 
> reference labels them as streaming options. In the same session, with metrics 
> still enabled:
> {code:sql}
> CREATE TEMPORARY TABLE bsrc (name STRING) WITH (
>   'connector' = 'datagen', 'rows-per-second' = '20', 'number-of-rows' = 
> '1200',
>   'fields.name.length' = '8');
> SET 'execution.runtime-mode' = 'batch';
> SET 'pipeline.name' = 'flip485-batch';
> INSERT INTO bh_str SELECT slow_upper(name) FROM bsrc;
> SET 'execution.runtime-mode' = 'streaming';
> {code}
> *Expected:* the job runs for about a minute, and while it runs the helper 
> shows {{udf.slow_upper}} on its {{Calc}}.
> h3. Exploratory (optional)
> * Turn {{table.exec.udf-metric-enabled}} off and on for the same query and 
> check the query results do not change.
> * The docs list what is not covered: Python functions, {{PROCESS_TABLE}} 
> functions, aggregate functions, functions registered through the deprecated 
> {{registerFunction}}, and the functions inside lookup joins, model prediction 
> and vector search. Using them with metrics enabled should register nothing 
> and work as before.
> * Run with parallelism > 1 and check that each subtask reports its own 
> metrics.
> h3. Known behavior (not bugs)
> * An exception escaping a synchronous function still fails the task. It is 
> counted, but the task restarts with fresh metrics, so on a cluster the 
> counter is mainly useful for asynchronous functions whose failed attempts are 
> retried.
> * The REST API and Web UI show the histogram's statistics, not its sample 
> count. The histogram keeps the latest 128 samples.
> * Metrics are scoped by the function's name, {{udf.<udf_name>}}, on each 
> operator that calls it.
> * The group uses the function's simple name. Functions with the same name 
> from different catalogs or databases, such as {{db1.f}} and {{db2.f}}, share 
> one {{udf.f}} group on an operator that calls both.
> * In the REST API and Web UI, dots and spaces in a function name become 
> {{_}}, so functions named {{my.func}} and {{my func}} appear there as one 
> {{udf.my_func}} entry. The Slf4j reporter keeps them apart.
> * A compiled plan does not store the metric settings. The values set when the 
> plan runs ({{EXECUTE PLAN}}) decide, not the values at {{COMPILE PLAN}} time.
> * Not from this FLIP: calling two functions with the same name from different 
> databases on the same arguments in one query returns wrong results, with or 
> without metrics (FLINK-41449).
> h3. Reporting
> Comment with the commit or RC and give pass or fail per section. File new 
> bugs as linked issues with Fix Version 2.4.0.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to