diff --git a/.gitignore b/.gitignore index 00320283df..323bf20ab6 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,9 @@ hits.vortex # Python venvs created by install scripts myenv +__pycache__/ + +# spark-gluten-clickhouse: from-source build tree and symlinked artifacts +gluten/ +gluten.jar +libch.so diff --git a/spark-gluten-clickhouse/README.md b/spark-gluten-clickhouse/README.md new file mode 100644 index 0000000000..013bc7bdad --- /dev/null +++ b/spark-gluten-clickhouse/README.md @@ -0,0 +1,34 @@ +This entry runs Apache Spark with the [Apache Gluten](https://gluten.apache.org/) plugin configured to use the **ClickHouse backend** ('ch'). Gluten loads `libch.so` (a fork of ClickHouse v23.1) into the Spark executor JVM and runs the columnar physical plan natively through it. See also [`spark-gluten/`](../spark-gluten/) (Velox backend), [`spark-velox/`](../spark-velox/), and the [accelerators README](../spark/README-accelerators.md). + +### Run + +`./benchmark.sh` sets a few env vars and delegates to the shared driver +[`../lib/benchmark-common.sh`](../lib/benchmark-common.sh), which runs the +per-system scripts (`install`, `load`, `query`, ...) and prints the results in +the format collected by play.clickhouse.com. `./install` builds everything from +source (no pre-built bundle is published for the CH backend). + +## Notes + +### Build + +The CH backend is not part of Apache Gluten's release tarball — only the Velox bundle is published. As a result `install` builds two things from source: + +1. **`libch.so`** — built from [Kyligence/ClickHouse](https://github.com/Kyligence/ClickHouse) at the org/branch/commit pinned in `gluten/cpp-ch/clickhouse.version`. The build uses Clang 19 / cmake / ninja (Gluten v1.4.0's CH backend requires Clang 19). Its `extern-local-engine` module links against JNI **including AWT**, so `install` uses the full `openjdk-17-jdk` (not `-headless`, which omits `jawt.h`/`libjawt.so` and makes cmake fail with `Could NOT find JNI (missing: AWT)`). +2. **The Gluten Spark plugin** — built via Maven with `-Pbackends-clickhouse -Pspark-3.5 -Pscala-2.12 -Pdelta`. The `delta` profile is required by the `backends-clickhouse` module's enforcer; `spark-3.5` pins the matching Delta version (3.2.0). JDK 8 is required at compile time (Gluten's POM); Spark itself runs under JDK 17 (see `./query`). + +Building libch.so essentially compiles ClickHouse from source: it is **memory-hungry** (Gluten's docs note that 64 GB RAM is recommended). On a c6a.4xlarge (32 GB RAM) the compile may OOM; use c6a.8xlarge or larger for a clean run. + +### Configuration + +- `spark.gluten.sql.columnar.backend.lib=ch` selects the ClickHouse backend over Velox. +- `spark.gluten.sql.columnar.libpath=` points to the native library. Gluten's wrapper cmakes into `gluten/cpp-ch/build_ch`, which drives an inner ClickHouse cmake that builds `libch.so` under `gluten/cpp-ch/build/.../extern-local-engine/`; `install` globs for it under `cpp-ch/` and symlinks it as `libch.so` in the entry directory. +- Memory is split 50/50 between Spark heap and Gluten off-heap, identical to the Velox entry — the CH backend also runs off-heap via JNI. +- `libch.so` is preloaded into the JVM via `LD_PRELOAD` (set in `query.py`). Because the library carries initial-exec-model TLS, a lazy `dlopen` from the running JVM otherwise fails with `cannot allocate memory in static TLS block`; preloading it at JVM startup — while the static TLS block still has room — avoids this. Gluten's docs use `spark.executorEnv.LD_PRELOAD` for this, but in `local[*]` mode the driver JVM is the executor and launches before that config is read, so the preload is done through the JVM's environment instead. +- Queries use ClickHouse-style regex backreferences (`\1`) rather than Spark's `$1`, since the regex evaluation happens inside libch.so. See the discussion in [`spark-gluten/README.md`](../spark-gluten/README.md) and [Gluten issue #7545](https://github.com/apache/incubator-gluten/issues/7545). + +### Links + +- [Gluten ClickHouse-backend getting started](https://gluten.apache.org/docs/get-started/ClickHouse/). +- [Gluten release page](https://gluten.apache.org/downloads/) (Velox bundles only). +- [Kyligence/ClickHouse fork](https://github.com/Kyligence/ClickHouse) (the source of libch.so). diff --git a/spark-gluten-clickhouse/benchmark.sh b/spark-gluten-clickhouse/benchmark.sh new file mode 100755 index 0000000000..fb3b4d1318 --- /dev/null +++ b/spark-gluten-clickhouse/benchmark.sh @@ -0,0 +1,9 @@ +#!/bin/bash +export BENCH_DOWNLOAD_SCRIPT="download-hits-parquet-single" +export BENCH_RESTARTABLE=no +# Single-process engine: each query forks a fresh full-machine process with no +# shared scheduler across connections, so the concurrent-QPS test only +# oversubscribes RAM rather than measuring throughput. Skip it by default; +# override BENCH_CONCURRENT_DURATION to re-enable. See issue #946. +export BENCH_CONCURRENT_DURATION="${BENCH_CONCURRENT_DURATION:-0}" +exec ../lib/benchmark-common.sh diff --git a/spark-gluten-clickhouse/check b/spark-gluten-clickhouse/check new file mode 100755 index 0000000000..45d397cda2 --- /dev/null +++ b/spark-gluten-clickhouse/check @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +# shellcheck disable=SC1091 +source myenv/bin/activate +python3 -c 'import pyspark' >/dev/null 2>&1 +[ -f gluten.jar ] +[ -f libch.so ] diff --git a/spark-gluten-clickhouse/data-size b/spark-gluten-clickhouse/data-size new file mode 100755 index 0000000000..1a34600a86 --- /dev/null +++ b/spark-gluten-clickhouse/data-size @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +du -b hits.parquet | cut -f1 diff --git a/spark-gluten-clickhouse/install b/spark-gluten-clickhouse/install new file mode 100755 index 0000000000..d242687bae --- /dev/null +++ b/spark-gluten-clickhouse/install @@ -0,0 +1,216 @@ +#!/bin/bash +set -e + +# Spark + Apache Gluten with the ClickHouse backend ('ch'). Unlike the Velox +# backend, no pre-built bundle is published for the CH backend, so we build +# both libch.so (a ClickHouse fork) and the Gluten Spark plugin from source. +# +# The libch.so compile is essentially a ClickHouse build: RAM-hungry (Gluten's +# docs recommend >= 64 GB) and slow. Run on c6a.8xlarge or larger; c6a.4xlarge +# (32 GB) may OOM. +# +# Note: Keep in sync with spark-*/install (see README-accelerators.md). + +# Track Gluten main, not a release tag: the CH backend's pinned ClickHouse fork +# is only rebased forward on main, so release tags carry an old ClickHouse. This +# commit pulls ClickHouse 25.12.10.7 (cpp-ch/clickhouse.version => +# Kyligence rebase_ch/20260425-25.12.10.7); v1.4.0 was on a ~2-year-old fork. +GLUTEN_COMMIT=8b701473f465e4d7eb05335b0529bad9327db8b3 # apache/gluten main @ 2026-07-24 +SPARK_PROFILE=spark-3.5 + +# Build prerequisites: +# - Java 8 (headless) to build Gluten via Maven (Gluten's pom requires JDK 8). +# - Java 17, the FULL jdk (not -headless), to run Spark AND to satisfy the +# libch.so build's JNI/AWT dependency: ClickHouse's extern-local-engine +# links against jawt.h/libjawt.so, which the -headless packages omit, so +# its cmake fails with `Could NOT find JNI (missing: AWT)` / +# `JAVA_AWT_INCLUDE_PATH-NOTFOUND`. openjdk-17-jdk ships jawt.h and pulls +# in the non-headless JRE that ships libjawt.so. +# - Clang 19, cmake, ninja, etc. to build libch.so (Gluten's CH backend +# requires Clang 19; see get-started/ClickHouse.md). +sudo apt-get update -y +sudo apt-get install -y python3-pip python3-venv \ + openjdk-8-jdk-headless openjdk-17-jdk \ + maven git cmake ccache ninja-build nasm yasm gawk \ + lsb-release wget software-properties-common gnupg + +# ClickHouse (libch.so) starts core-scaled thread pools at init; on a 192-core +# box the process needs far more threads and memory-map areas than the defaults +# allow, and JavaSparkContext init otherwise dies with `OutOfMemoryError: +# unable to create native thread`. Raise the kernel ceilings once here +# (best-effort; ./query also raises the matching per-process ulimits). +sudo sysctl -w kernel.threads-max=4000000 kernel.pid_max=4000000 \ + vm.max_map_count=2000000 >/dev/null 2>&1 || true + +# Install Clang 19 via apt.llvm.org. +if ! command -v clang-19 >/dev/null 2>&1; then + wget -O - https://apt.llvm.org/llvm.sh | sudo bash -s -- 19 +fi + +export CC=clang-19 +export CXX=clang++-19 + +ARCH=$(dpkg --print-architecture) + +# pyspark venv. +if [ ! -d myenv ]; then + python3 -m venv myenv +fi +# shellcheck disable=SC1091 +source myenv/bin/activate +# pyspark matches the spark-3.5 profile Gluten is built against below (Spark +# 3.5.5), so the plugin jar's Spark APIs line up with the runtime. +pip install -q pyspark==3.5.5 psutil + +# Clone Gluten and the Kyligence ClickHouse fork that the CH backend wraps. +# The CH fork org/branch/commit are pinned in Gluten's cpp-ch/clickhouse.version. +GLUTEN_DIR="$PWD/gluten" +if [ ! -d "$GLUTEN_DIR" ]; then + # Shallow-fetch the pinned commit. `git clone --branch` accepts only refs, + # not SHAs, so init + fetch-by-SHA (GitHub allows reachable-SHA fetches). + git init -q "$GLUTEN_DIR" + git -C "$GLUTEN_DIR" remote add origin https://github.com/apache/gluten.git + git -C "$GLUTEN_DIR" fetch -q --depth 1 origin "$GLUTEN_COMMIT" + git -C "$GLUTEN_DIR" checkout -q FETCH_HEAD +fi + +CH_VERSION_FILE="$GLUTEN_DIR/cpp-ch/clickhouse.version" +CH_ORG=$(grep '^CH_ORG=' "$CH_VERSION_FILE" | cut -d= -f2) +CH_BRANCH=$(grep '^CH_BRANCH=' "$CH_VERSION_FILE" | cut -d= -f2) +CH_DIR="$GLUTEN_DIR/cpp-ch/ClickHouse" +if [ ! -d "$CH_DIR" ]; then + git clone --recursive --shallow-submodules \ + --branch "$CH_BRANCH" \ + "https://github.com/${CH_ORG}/ClickHouse.git" "$CH_DIR" +fi + +# Build libch.so WITHOUT jemalloc. ClickHouse enables jemalloc by default and +# Gluten hardcodes -DENABLE_JEMALLOC=ON for the inner ClickHouse cmake in +# cpp-ch/CMakeLists.txt (there is no env hook to override it). jemalloc is only +# safe when it interposes malloc/free process-wide, which requires preloading +# libch.so. We load it lazily (System.load in query.py, via the static-TLS +# tunable) rather than LD_PRELOAD, so jemalloc does NOT interpose: the first +# time ClickHouse frees a pointer the system/JVM allocator made, jemalloc's +# radix tree has no metadata for it and SIGSEGVs in rtree_metadata_read during +# the CH backend's native init (CHListenerApi.initialize -> nativeInitNative). +# Building without jemalloc makes ClickHouse use the system allocator uniformly, +# removing the cross-allocator boundary. Patch + verify so a future Gluten bump +# that changes the flag fails loudly instead of silently re-enabling jemalloc. +CH_CMAKELISTS="$GLUTEN_DIR/cpp-ch/CMakeLists.txt" +sed -i 's/-DENABLE_JEMALLOC=ON/-DENABLE_JEMALLOC=OFF/g' "$CH_CMAKELISTS" +if grep -q -- '-DENABLE_JEMALLOC=ON' "$CH_CMAKELISTS" \ + || ! grep -q -- '-DENABLE_JEMALLOC=OFF' "$CH_CMAKELISTS"; then + echo "ERROR: could not disable jemalloc in $CH_CMAKELISTS" >&2 + exit 1 +fi + +# Scala 2.12 compat for the Delta 3.3 backend sources. On Gluten main, +# backends-clickhouse/src-delta33/.../delta/Snapshot.scala calls Option.when, +# which only exists in Scala 2.13's stdlib -- but scala-2.12 is Gluten's default +# profile and matches the pyspark wheel we run, so the CH backend module fails +# to compile ("value when is not a member of object Option"). Rather than pull +# in a whole Scala-2.13 Spark stack, drop in Option.when/unless as implicit +# extensions on the Option companion (compiled from src-delta33, so it is on the +# module's own source path) and import it into Snapshot.scala. Under a scala-2.13 +# build the real Option.when takes precedence and this shim is simply unused, so +# it is safe across versions. Only Snapshot.scala uses Option.when in +# backends-clickhouse (verified by code search), so this one file is enough. +DELTA33_SCALA="$GLUTEN_DIR/backends-clickhouse/src-delta33/main/scala" +SNAPSHOT_SCALA="$DELTA33_SCALA/org/apache/spark/sql/delta/Snapshot.scala" +if [ -f "$SNAPSHOT_SCALA" ]; then + mkdir -p "$DELTA33_SCALA/org/apache/gluten/compat" + cat > "$DELTA33_SCALA/org/apache/gluten/compat/OptionCompat.scala" <<'SCALA' +/* + * 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.gluten.compat + +// Scala 2.12 lacks Option.when/Option.unless (added in 2.13). Provide them as +// implicit extensions on the Option companion so Delta 3.3 sources compile +// under scala-2.12 (Gluten's default profile). Under scala-2.13 the real +// stdlib methods take precedence and this shim is unused. +object OptionCompat { + implicit class OptionCompanionOps(companion: Option.type) { + def when[A](cond: Boolean)(a: => A): Option[A] = if (cond) Some(a) else None + def unless[A](cond: Boolean)(a: => A): Option[A] = if (cond) None else Some(a) + } +} +SCALA + if ! grep -q 'org.apache.gluten.compat.OptionCompat' "$SNAPSHOT_SCALA"; then + sed -i '/^package /a import org.apache.gluten.compat.OptionCompat._' "$SNAPSHOT_SCALA" + fi + if ! grep -q 'org.apache.gluten.compat.OptionCompat._' "$SNAPSHOT_SCALA"; then + echo "ERROR: failed to inject the Option.when shim into Snapshot.scala" >&2 + exit 1 + fi +fi + +# Build libch.so via Gluten's own wrapper. It cmakes cpp-ch into +# cpp-ch/build_ch, whose build_ch target drives an inner ClickHouse cmake that +# builds `--target libch` into cpp-ch/build/, so the artifact lands under +# cpp-ch/build/.../extern-local-engine/. Glob for it rather than hardcoding — +# the two build dirs (build_ch wrapper vs build inner) are easy to confuse. +# Honors the CC/CXX exported above; JAVA_HOME points cmake's FindJNI at the +# full JDK 17 so it locates jawt.h/libjawt.so (see the JDK note above). +# Note: the wrapper was renamed build_clickhouse.sh -> build-clickhouse.sh +# after v1.4.0; glob so either name works across Gluten versions. +export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-${ARCH}" +LIBCH_SO=$(find "$GLUTEN_DIR/cpp-ch" -name libch.so -type f 2>/dev/null | head -n1) +if [ -z "$LIBCH_SO" ]; then + BUILD_CH_SCRIPT=$(find "$GLUTEN_DIR/ep/build-clickhouse" \ + -name 'build[-_]clickhouse.sh' -type f 2>/dev/null | head -n1) + if [ -z "$BUILD_CH_SCRIPT" ]; then + echo "ERROR: could not find the build-clickhouse wrapper script" >&2 + exit 1 + fi + bash "$BUILD_CH_SCRIPT" + LIBCH_SO=$(find "$GLUTEN_DIR/cpp-ch" -name libch.so -type f 2>/dev/null | head -n1) +fi +if [ -z "$LIBCH_SO" ]; then + echo "ERROR: libch.so not found after build" >&2 + exit 1 +fi + +# Build the Gluten Spark plugin against the CH backend. JDK 8 is required at +# compile time per Gluten's pom; Spark itself runs under JDK 17 in ./query. +# pyspark wheels ship Scala 2.12 jars, so build with scala-2.12 to match. +# The delta profile is mandatory: backends-clickhouse's pom carries an +# `enforce-delta-profile` enforcer rule that fails the build with +# `"-P delta" must be set when building Gluten with ClickHouse backend` unless +# it is active. The spark-3.5 profile pins the matching delta.version (3.3.2 on +# current main), so -Pdelta needs no extra version flag. +JAVA_HOME_8="/usr/lib/jvm/java-8-openjdk-${ARCH}" +GLUTEN_JAR=$(ls "$GLUTEN_DIR"/backends-clickhouse/target/gluten-*-spark-3.5-jar-with-dependencies.jar 2>/dev/null | head -n1) +if [ -z "$GLUTEN_JAR" ]; then + ( + cd "$GLUTEN_DIR" + export MAVEN_OPTS="-Xmx8g -XX:ReservedCodeCacheSize=2g" + JAVA_HOME="$JAVA_HOME_8" PATH="$JAVA_HOME_8/bin:$PATH" \ + mvn -B clean package \ + -Pbackends-clickhouse -P"$SPARK_PROFILE" -Pscala-2.12 -Pdelta \ + -DskipTests -Dcheckstyle.skip -Dspotless.check.skip=true + ) + GLUTEN_JAR=$(ls "$GLUTEN_DIR"/backends-clickhouse/target/gluten-*-spark-3.5-jar-with-dependencies.jar 2>/dev/null | head -n1) +fi +if [ -z "$GLUTEN_JAR" ]; then + echo "ERROR: could not locate built Gluten CH-backend jar" >&2 + ls "$GLUTEN_DIR/backends-clickhouse/target/" >&2 || true + exit 1 +fi + +# query.py expects the jar and native library as ./gluten.jar and ./libch.so. +ln -sf "$GLUTEN_JAR" gluten.jar +ln -sf "$LIBCH_SO" libch.so diff --git a/spark-gluten-clickhouse/load b/spark-gluten-clickhouse/load new file mode 100755 index 0000000000..27b3422e88 --- /dev/null +++ b/spark-gluten-clickhouse/load @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +# Nothing to load: query.py reads hits.parquet directly. Just flush writeback +# so the parquet the harness downloaded is durably on disk before queries run. +sync diff --git a/spark-gluten-clickhouse/queries.sql b/spark-gluten-clickhouse/queries.sql new file mode 100644 index 0000000000..31f65fc898 --- /dev/null +++ b/spark-gluten-clickhouse/queries.sql @@ -0,0 +1,43 @@ +SELECT COUNT(*) FROM hits; +SELECT COUNT(*) FROM hits WHERE AdvEngineID <> 0; +SELECT SUM(AdvEngineID), COUNT(*), AVG(ResolutionWidth) FROM hits; +SELECT AVG(UserID) FROM hits; +SELECT COUNT(DISTINCT UserID) FROM hits; +SELECT COUNT(DISTINCT SearchPhrase) FROM hits; +SELECT MIN(EventDate), MAX(EventDate) FROM hits; +SELECT AdvEngineID, COUNT(*) FROM hits WHERE AdvEngineID <> 0 GROUP BY AdvEngineID ORDER BY COUNT(*) DESC; +SELECT RegionID, COUNT(DISTINCT UserID) AS u FROM hits GROUP BY RegionID ORDER BY u DESC LIMIT 10; +SELECT RegionID, SUM(AdvEngineID), COUNT(*) AS c, AVG(ResolutionWidth), COUNT(DISTINCT UserID) FROM hits GROUP BY RegionID ORDER BY c DESC LIMIT 10; +SELECT MobilePhoneModel, COUNT(DISTINCT UserID) AS u FROM hits WHERE MobilePhoneModel <> '' GROUP BY MobilePhoneModel ORDER BY u DESC LIMIT 10; +SELECT MobilePhone, MobilePhoneModel, COUNT(DISTINCT UserID) AS u FROM hits WHERE MobilePhoneModel <> '' GROUP BY MobilePhone, MobilePhoneModel ORDER BY u DESC LIMIT 10; +SELECT SearchPhrase, COUNT(*) AS c FROM hits WHERE SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10; +SELECT SearchPhrase, COUNT(DISTINCT UserID) AS u FROM hits WHERE SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY u DESC LIMIT 10; +SELECT SearchEngineID, SearchPhrase, COUNT(*) AS c FROM hits WHERE SearchPhrase <> '' GROUP BY SearchEngineID, SearchPhrase ORDER BY c DESC LIMIT 10; +SELECT UserID, COUNT(*) FROM hits GROUP BY UserID ORDER BY COUNT(*) DESC LIMIT 10; +SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10; +SELECT UserID, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, SearchPhrase LIMIT 10; +SELECT UserID, extract(minute FROM EventTime) AS m, SearchPhrase, COUNT(*) FROM hits GROUP BY UserID, m, SearchPhrase ORDER BY COUNT(*) DESC LIMIT 10; +SELECT UserID FROM hits WHERE UserID = 435090932899640449; +SELECT COUNT(*) FROM hits WHERE URL LIKE '%google%'; +SELECT SearchPhrase, MIN(URL), COUNT(*) AS c FROM hits WHERE URL LIKE '%google%' AND SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10; +SELECT SearchPhrase, MIN(URL), MIN(Title), COUNT(*) AS c, COUNT(DISTINCT UserID) FROM hits WHERE Title LIKE '%Google%' AND URL NOT LIKE '%.google.%' AND SearchPhrase <> '' GROUP BY SearchPhrase ORDER BY c DESC LIMIT 10; +SELECT * FROM hits WHERE URL LIKE '%google%' ORDER BY EventTime LIMIT 10; +SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime LIMIT 10; +SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY SearchPhrase LIMIT 10; +SELECT SearchPhrase FROM hits WHERE SearchPhrase <> '' ORDER BY EventTime, SearchPhrase LIMIT 10; +SELECT CounterID, AVG(length(URL)) AS l, COUNT(*) AS c FROM hits WHERE URL <> '' GROUP BY CounterID HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25; +SELECT REGEXP_REPLACE(Referer, '^https?://(?:www\.)?([^/]+)/.*$', '\1') AS k, AVG(length(Referer)) AS l, COUNT(*) AS c, MIN(Referer) FROM hits WHERE Referer <> '' GROUP BY k HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25; +SELECT SUM(ResolutionWidth), SUM(ResolutionWidth + 1), SUM(ResolutionWidth + 2), SUM(ResolutionWidth + 3), SUM(ResolutionWidth + 4), SUM(ResolutionWidth + 5), SUM(ResolutionWidth + 6), SUM(ResolutionWidth + 7), SUM(ResolutionWidth + 8), SUM(ResolutionWidth + 9), SUM(ResolutionWidth + 10), SUM(ResolutionWidth + 11), SUM(ResolutionWidth + 12), SUM(ResolutionWidth + 13), SUM(ResolutionWidth + 14), SUM(ResolutionWidth + 15), SUM(ResolutionWidth + 16), SUM(ResolutionWidth + 17), SUM(ResolutionWidth + 18), SUM(ResolutionWidth + 19), SUM(ResolutionWidth + 20), SUM(ResolutionWidth + 21), SUM(ResolutionWidth + 22), SUM(ResolutionWidth + 23), SUM(ResolutionWidth + 24), SUM(ResolutionWidth + 25), SUM(ResolutionWidth + 26), SUM(ResolutionWidth + 27), SUM(ResolutionWidth + 28), SUM(ResolutionWidth + 29), SUM(ResolutionWidth + 30), SUM(ResolutionWidth + 31), SUM(ResolutionWidth + 32), SUM(ResolutionWidth + 33), SUM(ResolutionWidth + 34), SUM(ResolutionWidth + 35), SUM(ResolutionWidth + 36), SUM(ResolutionWidth + 37), SUM(ResolutionWidth + 38), SUM(ResolutionWidth + 39), SUM(ResolutionWidth + 40), SUM(ResolutionWidth + 41), SUM(ResolutionWidth + 42), SUM(ResolutionWidth + 43), SUM(ResolutionWidth + 44), SUM(ResolutionWidth + 45), SUM(ResolutionWidth + 46), SUM(ResolutionWidth + 47), SUM(ResolutionWidth + 48), SUM(ResolutionWidth + 49), SUM(ResolutionWidth + 50), SUM(ResolutionWidth + 51), SUM(ResolutionWidth + 52), SUM(ResolutionWidth + 53), SUM(ResolutionWidth + 54), SUM(ResolutionWidth + 55), SUM(ResolutionWidth + 56), SUM(ResolutionWidth + 57), SUM(ResolutionWidth + 58), SUM(ResolutionWidth + 59), SUM(ResolutionWidth + 60), SUM(ResolutionWidth + 61), SUM(ResolutionWidth + 62), SUM(ResolutionWidth + 63), SUM(ResolutionWidth + 64), SUM(ResolutionWidth + 65), SUM(ResolutionWidth + 66), SUM(ResolutionWidth + 67), SUM(ResolutionWidth + 68), SUM(ResolutionWidth + 69), SUM(ResolutionWidth + 70), SUM(ResolutionWidth + 71), SUM(ResolutionWidth + 72), SUM(ResolutionWidth + 73), SUM(ResolutionWidth + 74), SUM(ResolutionWidth + 75), SUM(ResolutionWidth + 76), SUM(ResolutionWidth + 77), SUM(ResolutionWidth + 78), SUM(ResolutionWidth + 79), SUM(ResolutionWidth + 80), SUM(ResolutionWidth + 81), SUM(ResolutionWidth + 82), SUM(ResolutionWidth + 83), SUM(ResolutionWidth + 84), SUM(ResolutionWidth + 85), SUM(ResolutionWidth + 86), SUM(ResolutionWidth + 87), SUM(ResolutionWidth + 88), SUM(ResolutionWidth + 89) FROM hits; +SELECT SearchEngineID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits WHERE SearchPhrase <> '' GROUP BY SearchEngineID, ClientIP ORDER BY c DESC LIMIT 10; +SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits WHERE SearchPhrase <> '' GROUP BY WatchID, ClientIP ORDER BY c DESC LIMIT 10; +SELECT WatchID, ClientIP, COUNT(*) AS c, SUM(IsRefresh), AVG(ResolutionWidth) FROM hits GROUP BY WatchID, ClientIP ORDER BY c DESC LIMIT 10; +SELECT URL, COUNT(*) AS c FROM hits GROUP BY URL ORDER BY c DESC LIMIT 10; +SELECT 1, URL, COUNT(*) AS c FROM hits GROUP BY 1, URL ORDER BY c DESC LIMIT 10; +SELECT ClientIP, ClientIP - 1, ClientIP - 2, ClientIP - 3, COUNT(*) AS c FROM hits GROUP BY ClientIP, ClientIP - 1, ClientIP - 2, ClientIP - 3 ORDER BY c DESC LIMIT 10; +SELECT URL, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND DontCountHits = 0 AND IsRefresh = 0 AND URL <> '' GROUP BY URL ORDER BY PageViews DESC LIMIT 10; +SELECT Title, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND DontCountHits = 0 AND IsRefresh = 0 AND Title <> '' GROUP BY Title ORDER BY PageViews DESC LIMIT 10; +SELECT URL, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 AND IsLink <> 0 AND IsDownload = 0 GROUP BY URL ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT TraficSourceID, SearchEngineID, AdvEngineID, CASE WHEN (SearchEngineID = 0 AND AdvEngineID = 0) THEN Referer ELSE '' END AS Src, URL AS Dst, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 GROUP BY TraficSourceID, SearchEngineID, AdvEngineID, Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT URLHash, EventDate, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 AND TraficSourceID IN (-1, 6) AND RefererHash = 3594120000172545465 GROUP BY URLHash, EventDate ORDER BY PageViews DESC LIMIT 10 OFFSET 100; +SELECT WindowClientWidth, WindowClientHeight, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-01' AND EventDate <= '2013-07-31' AND IsRefresh = 0 AND DontCountHits = 0 AND URLHash = 2868770270353813622 GROUP BY WindowClientWidth, WindowClientHeight ORDER BY PageViews DESC LIMIT 10 OFFSET 10000; +SELECT DATE_TRUNC('minute', EventTime) AS M, COUNT(*) AS PageViews FROM hits WHERE CounterID = 62 AND EventDate >= '2013-07-14' AND EventDate <= '2013-07-15' AND IsRefresh = 0 AND DontCountHits = 0 GROUP BY DATE_TRUNC('minute', EventTime) ORDER BY DATE_TRUNC('minute', EventTime) LIMIT 10 OFFSET 1000; diff --git a/spark-gluten-clickhouse/query b/spark-gluten-clickhouse/query new file mode 100755 index 0000000000..b2ca8053f9 --- /dev/null +++ b/spark-gluten-clickhouse/query @@ -0,0 +1,52 @@ +#!/bin/bash +set -e + +# shellcheck disable=SC1091 +source myenv/bin/activate + +# Spark runs under JDK 17; Gluten was built under JDK 8 in ./install. +arch=$(dpkg --print-architecture) +export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-${arch}" +export PATH="$JAVA_HOME/bin:$PATH" + +# libch.so plus Spark spin up many threads on a 192-core box; the default +# per-process limits are too low and SparkContext init dies with +# `OutOfMemoryError: unable to create native thread`. Raise the soft limits to +# the hard max (./install also bumps the kernel-wide sysctls). +ulimit -u unlimited 2>/dev/null || ulimit -u "$(ulimit -Hu)" 2>/dev/null || true +ulimit -n 1048576 2>/dev/null || ulimit -n "$(ulimit -Hn)" 2>/dev/null || true + +# The ulimit/sysctl bumps did NOT clear the native-thread error, which points to +# a cgroup pids.max (systemd TasksMax) ceiling that ulimit can't override, and/or +# a memory-overcommit gate on thread stacks. Diagnose + raise both. NOTE: the +# harness sends this script's STDOUT to /dev/null and only surfaces STDERR (on a +# non-zero exit), so ALL diagnostics must go to stderr -- hence the { ... } >&2. +{ + echo "=== LIMITS: nproc(-u)=$(ulimit -u) nofile(-n)=$(ulimit -n) stack(-s)=$(ulimit -s) KiB addrspace(-v)=$(ulimit -v) locked(-l)=$(ulimit -l) sys-threads=$(ps -eL 2>/dev/null | wc -l) ===" + echo "=== KERNEL: threads-max=$(cat /proc/sys/kernel/threads-max 2>/dev/null) pid_max=$(cat /proc/sys/kernel/pid_max 2>/dev/null) overcommit=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null) max_map_count=$(cat /proc/sys/vm/max_map_count 2>/dev/null) ===" + cg=$(awk -F: '$1=="0"{print $3}' /proc/self/cgroup 2>/dev/null) + echo "=== CGROUP=$cg pids.max=$(cat "/sys/fs/cgroup${cg}/pids.max" 2>/dev/null) pids.current=$(cat "/sys/fs/cgroup${cg}/pids.current" 2>/dev/null) ===" + # Raise pids.max on our cgroup and every ancestor up to the root (effective + # limit is the min across them). Also allow memory overcommit so thread-stack + # reservations can't trip an ENOMEM under the big -Xmx. + d="/sys/fs/cgroup${cg}" + while [ -n "$cg" ] && [ "$d" != "/sys/fs/cgroup" ] && [ "$d" != "/" ]; do + echo max | sudo tee "$d/pids.max" >/dev/null 2>&1 || true + d=$(dirname "$d") + done + echo 1 | sudo tee /proc/sys/vm/overcommit_memory >/dev/null 2>&1 || true + echo "=== after raise: pids.max=$(cat "/sys/fs/cgroup${cg}/pids.max" 2>/dev/null) overcommit=$(cat /proc/sys/vm/overcommit_memory 2>/dev/null) ===" +} >&2 +# RLIMIT_AS (address space) is the last unmeasured ceiling; a finite value would +# make a thread-stack mmap fail as "unable to create native thread" even with +# unlimited nproc/pids. Lift it (no-op if already unlimited). +ulimit -v unlimited 2>/dev/null || true + +query=$(cat) +# DIAGNOSTIC: pin the JVM (and thus libch.so) to 16 CPUs. ClickHouse sizes its +# thread pools to the visible core count (sched_getaffinity), so on the bare +# 192-core box it eagerly spawns a huge number of threads at init and dies with +# "unable to create native thread" even though every system limit is unlimited. +# If capping perceived cores clears it, that confirms thread-count as the cause +# and the real fix is a ClickHouse pool-size setting (keeping full parallelism). +printf '%s' "$query" | taskset -c 0-15 python3 query.py diff --git a/spark-gluten-clickhouse/query.py b/spark-gluten-clickhouse/query.py new file mode 100755 index 0000000000..b525cba9ba --- /dev/null +++ b/spark-gluten-clickhouse/query.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 + +""" +Spark + Apache Gluten using the ClickHouse backend ('ch'). The CH backend +loads libch.so (a fork of ClickHouse v23.1) into the Spark executor JVM and +runs the columnar physical plan natively. + +Reads SQL on stdin, runs it once, prints the result on stdout and the runtime +in fractional seconds as the LAST line on stderr. + +Note: Keep in sync with spark-*/query.py (see README-accelerators.md for details). +""" + +import faulthandler +import os +import signal +import sys +import threading +import time +import timeit + +import psutil +from pyspark.sql import SparkSession +import pyspark.sql.functions as F + + +# --- Diagnostic scaffolding (temporary) -------------------------------------- +# The CH backend is built + loaded remotely on c6a.metal and cannot be +# reproduced on the (aarch64) dev box, so each iteration costs a ~1h build. +# The harness sends query stdout to /dev/null and only surfaces stderr when a +# query *exits non-zero*; a hang therefore produces no output and burns the +# full 10h job timeout. These helpers make a hang fail fast and loud instead: +# * STEP markers on stderr pinpoint how far we got before wedging. +# * A watchdog forces a JVM thread dump (SIGQUIT) then kills the JVM so the +# captured-stderr pipe closes and the invocation exits non-zero. +# * A sentinel file short-circuits the remaining tries/queries once one hang +# is observed, so a fully-wedged backend costs one timeout, not 43x3. +# Remove once queries run cleanly. +# Generous per-query cap: any real ClickBench query finishes in well under this +# on c6a.metal, and the sentinel means a total hang costs only ONE timeout (not +# 43x3), so a large value is safe and won't false-trip a slow-but-valid query. +QUERY_TIMEOUT = int(os.environ.get("QUERY_TIMEOUT", "600")) +HANG_SENTINEL = "query_hang.sentinel" + + +def mark(msg): + print(f"=== STEP: {msg} ===", file=sys.stderr, flush=True) + + +def _dump_and_die(): + print( + f"\n=== WATCHDOG: query.py exceeded {QUERY_TIMEOUT}s; dumping stacks ===", + file=sys.stderr, + flush=True, + ) + jvms = [] + try: + for child in psutil.Process().children(recursive=True): + try: + if "java" in child.name().lower(): + jvms.append(child) + print( + f"=== sending SIGQUIT to JVM pid {child.pid} for thread dump ===", + file=sys.stderr, + flush=True, + ) + child.send_signal(signal.SIGQUIT) + except Exception as exc: # noqa: BLE001 + print(f"watchdog: {exc}", file=sys.stderr, flush=True) + except Exception as exc: # noqa: BLE001 + print(f"watchdog: could not enumerate children: {exc}", file=sys.stderr, flush=True) + + time.sleep(10) # let the JVM flush its thread dump to our stderr + + print("=== Python stacks ===", file=sys.stderr, flush=True) + faulthandler.dump_traceback(file=sys.stderr) + sys.stderr.flush() + + # Kill the JVM so the pipe the harness reads from (2>&1) closes and the + # command substitution capturing our stderr returns instead of blocking. + for child in jvms: + try: + child.kill() + except Exception: # noqa: BLE001 + pass + + try: + open(HANG_SENTINEL, "w").close() + except Exception: # noqa: BLE001 + pass + os._exit(1) + + +if os.path.exists(HANG_SENTINEL): + print( + "=== a prior query hung or crashed at init (see earlier diagnostics); " + "fast-failing ===", + file=sys.stderr, + flush=True, + ) + sys.exit(1) + +watchdog = threading.Timer(QUERY_TIMEOUT, _dump_and_die) +watchdog.daemon = True +watchdog.start() + +# Peak thread-count sampler: the except handler counts threads too late (the +# failed JVM has already torn its threads down), so sample the JVM subtree's +# thread count continuously and remember the max, to see how many threads +# actually existed at the "unable to create native thread" moment. +_peak_threads = [0] + + +def _sample_peak_threads(): + while True: + try: + total = psutil.Process().num_threads() + for child in psutil.Process().children(recursive=True): + try: + total += child.num_threads() + except Exception: # noqa: BLE001 + pass + if total > _peak_threads[0]: + _peak_threads[0] = total + except Exception: # noqa: BLE001 + pass + time.sleep(0.2) + + +_sampler = threading.Thread(target=_sample_peak_threads, daemon=True) +_sampler.start() +# ----------------------------------------------------------------------------- + + +query = sys.stdin.read() +print(query) + +# Calculate available memory to configure SparkSession (in MB). +# The CH backend runs off-heap (via JNI into libch.so), so split available +# memory between Spark's JVM heap and the off-heap pool the same way the +# Velox backend does. +# +# DIAGNOSTIC (temporary): the Velox sibling uses 0.7*available (half heap / +# half off-heap) and survives on c6a.metal, but the CH backend's JVM dies +# during JavaSparkContext construction (where libch.so initializes) with no +# hs_err banner on stderr — consistent with an external SIGKILL (earlyoom is +# enabled on the runner) if the CH engine eagerly commits its off-heap arena. +# Cap heap+off-heap far below available so an OOM kill can't be the cause; if +# this run's queries execute, the 0.7 split needs revisiting. Restore it once +# the init crash is understood. +avail_mb = int(psutil.virtual_memory().available / (1024 ** 2)) +heap = min(avail_mb // 2, 24576) # <= 24 GiB driver heap +off_heap = min(avail_mb - heap, 49152) # <= 48 GiB off-heap for the CH engine +print(f"avail={avail_mb} MB -> heap={heap} MB, off_heap={off_heap} MB", file=sys.stderr, flush=True) +print(f"SparkSession will use {heap} MB of heap and {off_heap} MB of off-heap memory") + +# Gluten's CH backend loads libch.so into the JVM lazily via JNI (System.load, +# from CHListenerApi.initialize). libch.so carries initial-exec-model TLS (from +# its statically linked deps), and glibc sizes the static TLS block at process +# startup, leaving only a small surplus — so a lazy dlopen from the running JVM +# fails with: +# java.lang.UnsatisfiedLinkError: libch.so: cannot allocate memory in static +# TLS block +# Gluten's docs suggest LD_PRELOAD=, but preloading forces libch.so's +# global constructors and (statically linked) allocator onto the whole JVM from +# process start, which deadlocked JVM startup here (query never returned). The +# cleaner fix is glibc's `rtld.optional_static_tls` tunable: it enlarges the +# per-thread static-TLS surplus reserved at startup, so the *lazy* System.load +# succeeds without preloading anything. It must be in the environment before +# the dynamic loader runs, i.e. before the JVM starts; setting it here does not +# affect this already-running Python process, but pyspark's launcher copies +# os.environ into the JVM it spawns, so the JVM starts with the enlarged +# surplus. +# +# Value matters, and the safe window is narrow. The surplus is reserved as +# native memory in *every* JVM the env touches — including pyspark's own tiny +# launcher JVM (`org.apache.spark.launcher.Main`, run at -Xmx128m by +# spark-class to build the real command). Too large and the JVM dies at +# startup with "Cannot create worker GC thread. Out of system resources" +# (native TLS collides with the compressed-oops heap region); because +# spark-class runs the launcher inside a process substitution, bash swallows +# the crash and it surfaces only as `spark-class: line 97: CMD: bad array +# subscript` -> `[JAVA_GATEWAY_EXITED] Java gateway process exited before +# sending its port number`, so all queries return null. Measured locally +# (OpenJDK 17, 192 GC threads to mimic c6a.metal): the cliff is sharp and +# machine-independent for a FEW threads. But the surplus is per-thread, so the +# real budget is surplus x thread_count: 512 KiB let the JVM's own startup +# threads through, yet init still died with "unable to create native thread" +# once ClickHouse added its threads (all system limits were proven unlimited: +# nproc, pids.max, threads-max, addrspace, overcommit all uncapped) — the same +# class of failure as the too-large surplus, just reached via thread count +# instead. Shrinking the surplus cuts each thread's native-TLS cost, so more +# threads fit before the collision. 128 KiB is 4x smaller (4x the thread +# headroom) and still ~79x the failing glibc default (1664 B), so libch.so's +# small IE-model TLS still loads. +_TLS_SURPLUS = "glibc.rtld.optional_static_tls=131072" +os.environ["GLIBC_TUNABLES"] = _TLS_SURPLUS + +builder = ( + SparkSession + .builder + .appName("ClickBench") + .config("spark.driver", "local[*]") # To ensure using all cores + .config("spark.driver.memory", f"{heap}m") + .config("spark.sql.parquet.binaryAsString", True) # Correct length/text results + + # Gluten + ClickHouse backend configuration + .config("spark.jars", "gluten.jar") + .config("spark.driver.extraClassPath", "gluten.jar") + .config("spark.plugins", "org.apache.gluten.GlutenPlugin") + .config("spark.shuffle.manager", "org.apache.spark.shuffle.sort.ColumnarShuffleManager") + .config("spark.gluten.sql.columnar.backend.lib", "ch") + .config("spark.gluten.sql.columnar.libpath", os.path.abspath("libch.so")) + # Cap ClickHouse's global/IO thread pools. On a 192-core box libch.so's + # pools are sized to the core count and their eager thread creation is the + # leading suspect for "unable to create native thread" at init (all system + # count limits are already unlimited). Passed through to ClickHouse config + # via the runtime_config prefix. 512 still far exceeds per-query + # parallelism, so this does not throttle the benchmark. + .config("spark.gluten.sql.columnar.backend.ch.runtime_config.max_thread_pool_size", "512") + .config("spark.gluten.sql.columnar.backend.ch.runtime_config.max_io_thread_pool_size", "64") + .config("spark.memory.offHeap.enabled", "true") + .config("spark.memory.offHeap.size", f"{off_heap}m") + # Cap the JVM's helper threads: on 192 cores it otherwise spawns ~150 + # GC+JIT threads which, on top of ClickHouse's native pools, exhaust the + # process thread limit during SparkContext init (OutOfMemoryError: unable + # to create native thread). These do NOT limit local[*] task parallelism. + # -XX:ErrorFile pins any JVM crash log to cwd so the except handler below + # can surface it (temporary; part of the init-crash diagnosis). + .config("spark.driver.extraJavaOptions", + "-Dio.netty.tryReflectionSetAccessible=true " + "-XX:ErrorFile=./hs_err_pid%p.log " + "-XX:ParallelGCThreads=8 -XX:ConcGCThreads=2 -XX:CICompilerCount=4") + + # Cluster-mode equivalent of the GLIBC_TUNABLES above: a no-op in local[*] + # (the driver JVM is the executor and already inherits os.environ) but kept + # so real executors get the same enlarged static-TLS surplus. + .config("spark.executorEnv.GLIBC_TUNABLES", _TLS_SURPLUS) +) + + +def _dump_crash_artifacts(): + """Surface a JVM crash log if one exists, plus the live thread count.""" + import glob + import subprocess + try: + n = subprocess.run(["ps", "-eL"], capture_output=True, text=True).stdout.count("\n") + print( + f"=== threads at failure: system={n}, JVM-subtree peak={_peak_threads[0]} ===", + file=sys.stderr, flush=True, + ) + except Exception as exc: # noqa: BLE001 + print(f"=== could not count threads: {exc} ===", file=sys.stderr, flush=True) + files = sorted(glob.glob("hs_err_pid*.log") + glob.glob("/tmp/hs_err_pid*.log")) + if not files: + print( + "=== no hs_err file: this is a caught Java OutOfMemoryError " + "(unable to create native thread), not a JVM/native crash ===", + file=sys.stderr, flush=True, + ) + return + newest = files[-1] + print(f"=== JVM crash log {newest} (first 90 lines) ===", file=sys.stderr, flush=True) + try: + with open(newest) as fh: + for i, line in enumerate(fh): + if i >= 90: + break + print(line.rstrip(), file=sys.stderr) + except OSError as exc: + print(f" (could not read {newest}: {exc})", file=sys.stderr) + sys.stderr.flush() + + +mark("building SparkSession (JVM launch + libch.so load)") +try: + spark = builder.getOrCreate() +except BaseException: + watchdog.cancel() + print("=== getOrCreate() failed; scanning for JVM crash artifacts ===", + file=sys.stderr, flush=True) + _dump_crash_artifacts() + # One init crash means every subsequent query will crash identically; drop + # the sentinel so the remaining invocations fast-fail instead of each + # rebuilding a doomed JVM (bounds the run, and one crash dump is enough). + try: + open(HANG_SENTINEL, "w").close() + except OSError: + pass + raise + +mark("SparkSession ready; reading hits.parquet") +df = spark.read.parquet("hits.parquet") +df = df.withColumn("EventTime", F.col("EventTime").cast("timestamp")) +df = df.withColumn("EventDate", F.date_add(F.lit("1970-01-01"), F.col("EventDate"))) +df.createOrReplaceTempView("hits") + +mark("temp view created; executing query") +try: + start = timeit.default_timer() + result = spark.sql(query) + result.show(100) + end = timeit.default_timer() + elapsed = end - start + mark("query complete") + print(f"Time: {elapsed}") + print(f"{elapsed:.6f}", file=sys.stderr) +except Exception as e: + print(e, file=sys.stderr) + print("Failure!", file=sys.stderr) + sys.exit(1) +finally: + watchdog.cancel() diff --git a/spark-gluten-clickhouse/start b/spark-gluten-clickhouse/start new file mode 100755 index 0000000000..06bd986563 --- /dev/null +++ b/spark-gluten-clickhouse/start @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/spark-gluten-clickhouse/stop b/spark-gluten-clickhouse/stop new file mode 100755 index 0000000000..06bd986563 --- /dev/null +++ b/spark-gluten-clickhouse/stop @@ -0,0 +1,2 @@ +#!/bin/bash +exit 0 diff --git a/spark-gluten-clickhouse/template.json b/spark-gluten-clickhouse/template.json new file mode 100644 index 0000000000..6ae1ad2873 --- /dev/null +++ b/spark-gluten-clickhouse/template.json @@ -0,0 +1,13 @@ +{ + "system": "Spark (Gluten-on-ClickHouse)", + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": [ + "Java", + "C++", + "column-oriented", + "Spark derivative", + "ClickHouse" + ] +}