diff --git a/ravel/.gitignore b/ravel/.gitignore new file mode 100644 index 0000000000..3ac91f9c5d --- /dev/null +++ b/ravel/.gitignore @@ -0,0 +1,13 @@ +# Artefacts fetched or built by ./install, and run output. +bin/ +src/ +ravel-commit.txt +server.log +server.log.prev +server.pid +*.log +hits.parquet +cache/ +# The local RustFS ./install sets up: binary, data directory, pid, log and the +# generated credentials. +rustfs/ diff --git a/ravel/README.md b/ravel/README.md new file mode 100644 index 0000000000..f9c015ef1c --- /dev/null +++ b/ravel/README.md @@ -0,0 +1,150 @@ +# Ravel + +[Ravel](https://github.com/NOFireAI/ravel) is an object-storage-native telemetry +database. Logs, metrics and traces are ingested into immutable objects in +S3-compatible storage, which is the only durable backend: there is no local-disk +storage mode and no local state a restart depends on. ClickBench's `hits` table +is loaded as the logs signal, with each column declared as a typed attribute +column, and queried over SQL. + +For this benchmark the object store is a single-node +[RustFS](https://github.com/rustfs/rustfs) that `./install` downloads, starts +and provisions on the machine's own disk, so the entry runs like the other +local-disk systems in this repository and needs no cloud account. Run it the +usual way: + +```sh +./benchmark.sh +``` + +## What this entry needs + +Nothing from the operator. `./install` fetches a pinned RustFS release from +GitHub, verifies it against the release's `SHA256SUMS`, generates a credential +pair into a file readable by the current user only (`rustfs/credentials.env`), +starts RustFS on loopback with its data directory under `rustfs/data`, creates +the bucket, and runs `ravel-cli store qualify` once, the bucket's one-time +conformance check (conditional writes, read-after-write and list-after-write +consistency, listing order). The first `./start` bootstraps the bucket's +`sys/tenancy` marker to the unkeyed tenant-hash derivation: the server's own +default is the keyed derivation, which refuses to start without a deployment +key file, and this benchmark holds no secret and needs a derivable prefix for +`./data-size`. + +Credentials reach RustFS and Ravel through the environment, never a command +line; `ravel-env.sh` reads them from the generated file. The same file holds +the key the server tokenizes query text with on its audit trail +(`RAVEL_AUDIT_TOKEN_KEY`, generated by `./install` too): a keyed deployment +derives that key, and this entry runs unkeyed, so it supplies one of its own. +RustFS's web console is disabled, so the entry opens two loopback ports, the +store's and the database's. `./stop` stops the database only. RustFS stays up +across the driver's restart cycle and `./rustfs-start` restarts it if it is +ever found down, so the drop of the page cache before each cold run reaches +RustFS's files the same way it reaches any local-disk engine's. + +To run the same scripts against another S3-compatible store, set +`RAVEL_S3_ENDPOINT`, `RAVEL_S3_BUCKET`, `RAVEL_S3_ACCESS_KEY` and +`RAVEL_S3_SECRET_KEY` before `./install`; a non-loopback endpoint skips the +RustFS setup. `RAVEL_S3_REGION` defaults to `us-east-1`. + +Optional: `RAVEL_TENANT` (default `clickbench`), `RAVEL_SHARDS` (default 4, +must match between `./load` and `./start`), `RAVEL_VERSION` (default the +released version `./install` downloads), `RAVEL_REF` (build that ref from +source instead), `RUSTFS_VERSION` (the pinned RustFS release). + +## How the run is configured + +`./install` downloads the released `ravel-server` and `ravel-cli` for the host +architecture and verifies them against the release's `SHA256SUMS`; those are the +binaries extracted from the signed container images, so what runs here is what +the published image runs. If the release is unreachable it falls back to +building the pinned ref from source with `--release`. + +`./start` passes **no performance flags**. The server resolves its query +budgets at startup: fetch concurrency from the core count, the read caches and +the SQL memory ceilings as shares of one memory budget derived from usable +memory (`MemTotal`, capped by the cgroup memory limit when it runs in a +container, less a fixed reserve), with a fixed segment cap and engine deadline. +Every resolved value and its source is logged on a `performance default +resolved` line in `server.log`; a published result should record those lines, +because they are the configuration the numbers were measured at. On +c6a.4xlarge the read cache resolves to 7.7 GB and the per-query SQL pool to +8.2 GB. `./start` also raises the process's open-file soft limit to the hard +limit, since the server has exited with `EMFILE` at startup under the common +default of 1024. + +`./load` declares the typed attribute columns and then loads the Parquet file. +Object size is set at ingest by `--batch-rows`: one batch becomes one object per +involved shard, so 150,000 rows over 4 shards gives ~4 MB objects and roughly +2,600 objects for the 100M-row dataset. **There is no post-load step**: no +compaction, no catalog fold, no VACUUM equivalent, so the layout the queries +run against is the layout ingest produced. + +`./data-size` reports the tenant's whole durable footprint in the bucket (data +objects, commit records, manifests, catalog snapshots), which is where all of +Ravel's state lives. + +### What the cold figures measure here + +The reference machine's disk is a 500 GB gp2 volume. Ravel holds no data on +local disk, so a cold run (server restarted, page cache dropped) fetches what it +needs from RustFS, and RustFS reads it from that volume. The stock fetch policy +reads whole objects, so a statement that touches the table reads the whole +11.2 GB dataset from the volume, whatever the statement computes. Measured on +v0.16.1, 40 of the 42 statements that return a number take 42.6 to 46.6 s +cold, consistent with reading the dataset at about 250 MB/s each time; the +other two (q1 and q7) take about 0.5 s. + +The stock warm runs re-read every object as well. Ravel holds no data on local +disk, so a warm run is served from the read cache or not at all, and the +derived cache (7.7 GB here) is smaller than the dataset; on this machine the +re-read comes from RustFS through the page cache. + +### The tuned configuration + +This directory publishes one result, the stock one. A tuned run belongs in its +own top-level entry the way the other tuned entries in this repository do, and +that is left for a follow-up. The tuned configuration is three server flags +passed through `RAVEL_TUNED_ARGS` in `./start`: + +``` +RAVEL_TUNED_ARGS="--logs-fetch-policy latency-first --fetch-concurrency 256 --sql-max-query-bytes 12884901888" ./benchmark.sh +``` + +`latency-first` is a named policy, not a tuning constant: it says spend +requests to save wall time, and resolves the byte quantities exactly as +`byte-minimal` does, so a logs read takes ranged reads wherever they save +bytes. It only pays off once fetch concurrency is raised with it, which is what +the second flag does (it sets the object-store GET permits, the SQL partition +count and the PromQL fan-out together). The third flag lifts the per-query +memory pool from the derived 8.2 GB to 12 GiB, which is what lets the widest +`GROUP BY` in the set (q33) complete instead of being refused. The trade is +more object-store requests for less cold wall-clock. It has not been measured +against RustFS; the real-S3 figures below are the reference for it. + +### Reference: the same binaries on real S3 + +Measured by us on the same machine type against an S3 bucket in the instance's +region, credentials from the instance role, with the same driver and the same +true-cold protocol, on the released v0.16.1 binaries and a freshly loaded +tenant. One pass each. Not reproducible by this harness, which does not run +entries on real S3, and therefore not a results file. Cold and hot are the sums +of the first and third run over the statements that returned a number. + +| configuration | load | cold | hot | statements | +|---|---|---|---|---| +| stock, real S3 | 1,191 s | 483 s | 430 s | 42 of 43, q33 refused | +| tuned, real S3 | (same tenant) | 201 s | 99 s | 43 of 43 | +| stock, local RustFS (this result) | 1,236 s | 1,720 s | 272 s | 42 of 43, q33 refused | + +## Notes + +- Queries go over the HTTP SQL endpoint on loopback, one process per query, the + same shape as the other daemon entries. +- Ravel is a telemetry database rather than a general-purpose warehouse: the + `hits` columns are modelled as typed attribute columns on log records, and + every query states a time window covering the dataset because the SQL + endpoint's window defaults to the last hour. +- The heaviest whole-table aggregates can exceed the derived per-query memory + pool on a small instance and are reported as errors rather than being run + with a raised limit; in the stock result that is q33. diff --git a/ravel/benchmark.sh b/ravel/benchmark.sh new file mode 100755 index 0000000000..ebd6651935 --- /dev/null +++ b/ravel/benchmark.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Ravel: object-storage-native telemetry database, queried over SQL. +# +# Ravel keeps every durable byte in S3-compatible object storage; there is no +# local-disk storage mode. For this benchmark the store is a single-node RustFS +# that ./install downloads, starts and provisions on this machine's disk, with +# credentials it generates into a file readable by the current user only, so +# nothing is required from the operator and no key is stored in this +# repository. See README.md. +export BENCH_DOWNLOAD_SCRIPT="download-hits-parquet-single" + +# The server is a daemon whose data survives a restart (it is in object +# storage), so the driver's defaults are right: restartable, durable, and the +# concurrent-QPS test applies. +# +# First start has to reach the object store, resolve the tenant and open the +# catalog, which is slower than a local-disk engine's start; give ./check room +# rather than failing a run on a cold control-plane round trip. +export BENCH_CHECK_TIMEOUT="${BENCH_CHECK_TIMEOUT:-600}" + +exec ../lib/benchmark-common.sh diff --git a/ravel/check b/ravel/check new file mode 100755 index 0000000000..f7f209eb3d --- /dev/null +++ b/ravel/check @@ -0,0 +1,53 @@ +#!/bin/bash +# Succeeds only when the server this harness started is the one answering. +# +# The driver (lib/benchmark-common.sh) treats ./check as the authoritative +# readiness signal and tolerates non-zero exits from ./start and ./stop. So a +# stale server left running by a failed ./stop would, with a plain HTTP probe, +# make the next "cold" try pass warm and nothing would notice. This check +# therefore requires all of: the pidfile ./start wrote exists, that pid is +# alive and is a ravel-server, that pid is the process listening on the HTTP +# port, and SELECT 1 succeeds against it. A server answering on the port that +# this harness did not launch fails the check, and the driver's check loop then +# times out loudly instead of measuring the wrong process. +# +# `ss -p` shows the listening pid only for the same user or root. The +# benchmark runs as root; run as another user the owner is invisible and this +# fails closed with the message below rather than passing. +set -eu +. ./ravel-env.sh + +pidfile="$RAVEL_PIDFILE" +if [ ! -f "$pidfile" ]; then + echo "check: no pidfile at $pidfile; nothing this harness started is running" >&2 + exit 1 +fi +pid=$(cat "$pidfile" 2>/dev/null || true) +case "$pid" in + ''|*[!0-9]*) echo "check: $pidfile holds no pid" >&2; exit 1 ;; +esac +if ! kill -0 "$pid" 2>/dev/null; then + echo "check: pid $pid from $pidfile is not running" >&2 + exit 1 +fi +if ! grep -qs 'ravel-server' "/proc/$pid/cmdline" 2>/dev/null; then + echo "check: pid $pid is not a ravel-server" >&2 + exit 1 +fi + +port="${RAVEL_HTTP##*:}" +owner=$(ss -ltnpH "( sport = :$port )" 2>/dev/null | sed -n 's/.*pid=\([0-9][0-9]*\).*/\1/p' | head -n 1) +if [ -z "$owner" ]; then + echo "check: no visible pid is listening on :$port (needs root or the same user)" >&2 + exit 1 +fi +if [ "$owner" != "$pid" ]; then + echo "check: :$port is held by pid $owner, not the pid $pid this harness started" >&2 + exit 1 +fi + +curl -sSf -X POST \ + -H 'Content-Type: application/json' \ + -H "x-ravel-tenant: $RAVEL_TENANT" \ + --data-binary '{"query":"SELECT 1","start":0,"end":4102444800}' \ + "http://${RAVEL_HTTP}/api/v1/sql" >/dev/null diff --git a/ravel/data-size b/ravel/data-size new file mode 100755 index 0000000000..7d38a2571b --- /dev/null +++ b/ravel/data-size @@ -0,0 +1,43 @@ +#!/bin/bash +# Ravel keeps every durable byte in object storage, so the VM's local disk +# footprint is not the data size. This sums the bytes stored under this +# tenant's own prefix in the bucket: data objects, commit records, manifests +# and catalog snapshots, which is the tenant's whole durable footprint. +set -eu +. ./ravel-env.sh + +# The tenant id is hashed under the bucket's pinned scheme, so the prefix is not +# derivable from the id and no command prints it (NOFireAI/ravel#1180). Take it +# from the key `catalog inspect` reports it addressed, which it names whether or +# not a catalog HEAD has been published yet. +hash=$("$RAVEL_CLI" --store s3 --s3-auth static catalog inspect \ + --tenant "$RAVEL_TENANT" --signal logs 2>&1 \ + | grep -oE 't/[0-9a-f]+/' | head -1 | cut -d/ -f2) + +# Fallback: a bucket dedicated to this benchmark holds one tenant, so its single +# prefix is unambiguous. Anything else is ambiguous and gets no guess. +if [ -z "$hash" ]; then + prefixes=$(aws s3 ls "s3://${RAVEL_S3_BUCKET}/t/" | awk '$1 == "PRE" { print $2 }' | tr -d /) + count=$(printf '%s\n' "$prefixes" | awk 'NF' | wc -l | tr -d ' ') + if [ "$count" = 1 ]; then + hash=$(printf '%s\n' "$prefixes" | awk 'NF') + echo "data-size: resolved the prefix as the bucket's only tenant" >&2 + fi +fi + +if [ -z "$hash" ]; then + echo "data-size: could not resolve the object prefix for tenant '$RAVEL_TENANT'" >&2 + exit 1 +fi + +bytes=$(aws s3 ls --summarize --recursive "s3://${RAVEL_S3_BUCKET}/t/${hash}/" \ + | awk '/Total Size:/ { print $3 }') + +# A tenant that measured zero means the prefix resolved to the wrong place, or +# the load did not land. Either way it is not a data size. +if [ -z "$bytes" ] || [ "$bytes" -eq 0 ]; then + echo "data-size: prefix t/${hash}/ holds no objects" >&2 + exit 1 +fi + +echo "$bytes" diff --git a/ravel/hits.mapping.toml b/ravel/hits.mapping.toml new file mode 100644 index 0000000000..9ebaf15486 --- /dev/null +++ b/ravel/hits.mapping.toml @@ -0,0 +1,586 @@ +# ClickBench `hits` schema mapping for `ravel-cli load --parquet` (ADR-0100, issue #430). +# +# Source of truth for the column names and their upstream SQL types: +# https://github.com/ClickHouse/ClickBench/blob/main/postgresql/create.sql +# (105 columns). This mapping is checked in so a ClickBench load is repeatable; +# it is consumed twice, with the SAME key/type information: +# 1. `ravel-cli load --parquet --mapping hits.mapping.toml` writes the records. +# 2. `ravel-cli typed-attr-column set --from-mapping hits.mapping.toml` declares +# the typed attribute columns the SQL layer accelerates (ADR-0100 decision 2). +# +# Type mapping (upstream SQL type -> loader ColType -> ravel-sql DeclaredType): +# BIGINT / INTEGER / SMALLINT -> i64 -> I64 +# TEXT / VARCHAR(n) / CHAR -> str -> Str +# Date / TIMESTAMP -> i64 -> I64 (see EventTime / secondary-time note) +# +# EventTime is the primary event-time column: it rides the native typed `ts` +# path (ts_column below) and is NOT declared as an attribute. Every OTHER column +# becomes a typed attribute with its ColType. +# +# F64 note (ADR-0101 / #431): ColType::F64 attributes CANNOT be declared as typed +# columns -- ravel-sql `DeclaredType` has no F64 variant (Str/I64/Bool/Bytes only), +# so `typed-attr-column set --from-mapping` skips an f64 entry with a stderr warning +# and a float attribute stays queryable only through `attrs['']`. This mapping +# declares NO f64 column: the `hits` schema is entirely integer, string, and +# date/time columns (77 integer/date/time, 28 text; 0 float), so the F64 gap affects +# 0 of the 105 columns here. The note is kept because a different dataset would hit it. +# +# Secondary time columns EventDate, ClientEventTime, LocalEventTime are declared i64. +# EventDate is a Date column: the loader stores it as its native unit, epoch DAYS +# (Date32 = days since 1970-01-01), NOT nanoseconds (load.rs read_i64). A date-literal +# comparison in a query therefore becomes an epoch-DAY integer comparison; the corpus +# flags those statements `modified` (ADR-0100 decision 3). ClientEventTime and +# LocalEventTime are TIMESTAMP in the schema: the loader `i64` path accepts integer +# and Date32/Date64 columns but NOT a native Arrow Timestamp column, so these two load +# only if the operator's hits.parquet encodes them as an integer/date column. None of +# the 43 ClickBench statements reference them, so this does not affect the suite. + +ts_column = "EventTime" +# EventTime in the original data is a Unix-seconds datetime. If the operator's +# hits.parquet stores EventTime as a native Arrow Timestamp, the loader uses the +# column's own unit and this ts_unit is ignored (load.rs read_ts); set it to match +# the parquet only when EventTime is stored as an integer. +ts_unit = "seconds" + +# CounterID (issue #519): the one column in `hits` that acts as a per-entity +# key -- ClickBench's "counter" is the site/tag being measured, so CounterID +# is the natural stream boundary. Declared as a resource_attribute rather than +# an attribute so it becomes part of stream identity (ADR-0029 log_stream_id): +# `shard_for_log` hashes resource attributes, so this is what actually spreads +# ingest across `--shards`. In the pre-#519 mapping, CounterID was a record +# attribute: every row's resource-attribute set was empty, so every row hashed +# to the same stream and 100% of writes landed on one shard regardless of +# --shards -- one core did all merge+encode work no matter how many shards +# were provisioned. +# +# `ravel-cli load` runs with a fixed `target_bytes: 1` (one Strict flush per +# batch), so raising --shards without raising --batch-rows to compensate +# produces MORE, SMALLER objects: a batch's rows now split across shards by +# CounterID hash, and each shard's slice flushes immediately as its own +# object, before it can reach the `block_target_records` target (8192). See +# docs/guides/clickbench.md step 2 for the --batch-rows this requires. +# +# The other 103 non-EventTime columns stay record attributes (typed values in +# `attrs`, not part of stream identity). 104 declared columns total (103 +# attribute + 1 resource_attribute) sit far under the per-object dynamic-column +# budget of 1000 (RlogConfig::max_dynamic_columns). + +[[resource_attribute]] +key = "CounterID" +column = "CounterID" +type = "i64" + +[[attribute]] +key = "WatchID" +column = "WatchID" +type = "i64" + +[[attribute]] +key = "JavaEnable" +column = "JavaEnable" +type = "i64" + +[[attribute]] +key = "Title" +column = "Title" +type = "str" + +[[attribute]] +key = "GoodEvent" +column = "GoodEvent" +type = "i64" + +[[attribute]] +key = "EventDate" +column = "EventDate" +type = "i64" + +[[attribute]] +key = "ClientIP" +column = "ClientIP" +type = "i64" + +[[attribute]] +key = "RegionID" +column = "RegionID" +type = "i64" + +[[attribute]] +key = "UserID" +column = "UserID" +type = "i64" + +[[attribute]] +key = "CounterClass" +column = "CounterClass" +type = "i64" + +[[attribute]] +key = "OS" +column = "OS" +type = "i64" + +[[attribute]] +key = "UserAgent" +column = "UserAgent" +type = "i64" + +[[attribute]] +key = "URL" +column = "URL" +type = "str" + +[[attribute]] +key = "Referer" +column = "Referer" +type = "str" + +[[attribute]] +key = "IsRefresh" +column = "IsRefresh" +type = "i64" + +[[attribute]] +key = "RefererCategoryID" +column = "RefererCategoryID" +type = "i64" + +[[attribute]] +key = "RefererRegionID" +column = "RefererRegionID" +type = "i64" + +[[attribute]] +key = "URLCategoryID" +column = "URLCategoryID" +type = "i64" + +[[attribute]] +key = "URLRegionID" +column = "URLRegionID" +type = "i64" + +[[attribute]] +key = "ResolutionWidth" +column = "ResolutionWidth" +type = "i64" + +[[attribute]] +key = "ResolutionHeight" +column = "ResolutionHeight" +type = "i64" + +[[attribute]] +key = "ResolutionDepth" +column = "ResolutionDepth" +type = "i64" + +[[attribute]] +key = "FlashMajor" +column = "FlashMajor" +type = "i64" + +[[attribute]] +key = "FlashMinor" +column = "FlashMinor" +type = "i64" + +[[attribute]] +key = "FlashMinor2" +column = "FlashMinor2" +type = "str" + +[[attribute]] +key = "NetMajor" +column = "NetMajor" +type = "i64" + +[[attribute]] +key = "NetMinor" +column = "NetMinor" +type = "i64" + +[[attribute]] +key = "UserAgentMajor" +column = "UserAgentMajor" +type = "i64" + +[[attribute]] +key = "UserAgentMinor" +column = "UserAgentMinor" +type = "str" + +[[attribute]] +key = "CookieEnable" +column = "CookieEnable" +type = "i64" + +[[attribute]] +key = "JavascriptEnable" +column = "JavascriptEnable" +type = "i64" + +[[attribute]] +key = "IsMobile" +column = "IsMobile" +type = "i64" + +[[attribute]] +key = "MobilePhone" +column = "MobilePhone" +type = "i64" + +[[attribute]] +key = "MobilePhoneModel" +column = "MobilePhoneModel" +type = "str" + +[[attribute]] +key = "Params" +column = "Params" +type = "str" + +[[attribute]] +key = "IPNetworkID" +column = "IPNetworkID" +type = "i64" + +[[attribute]] +key = "TraficSourceID" +column = "TraficSourceID" +type = "i64" + +[[attribute]] +key = "SearchEngineID" +column = "SearchEngineID" +type = "i64" + +[[attribute]] +key = "SearchPhrase" +column = "SearchPhrase" +type = "str" + +[[attribute]] +key = "AdvEngineID" +column = "AdvEngineID" +type = "i64" + +[[attribute]] +key = "IsArtifical" +column = "IsArtifical" +type = "i64" + +[[attribute]] +key = "WindowClientWidth" +column = "WindowClientWidth" +type = "i64" + +[[attribute]] +key = "WindowClientHeight" +column = "WindowClientHeight" +type = "i64" + +[[attribute]] +key = "ClientTimeZone" +column = "ClientTimeZone" +type = "i64" + +[[attribute]] +key = "ClientEventTime" +column = "ClientEventTime" +type = "i64" + +[[attribute]] +key = "SilverlightVersion1" +column = "SilverlightVersion1" +type = "i64" + +[[attribute]] +key = "SilverlightVersion2" +column = "SilverlightVersion2" +type = "i64" + +[[attribute]] +key = "SilverlightVersion3" +column = "SilverlightVersion3" +type = "i64" + +[[attribute]] +key = "SilverlightVersion4" +column = "SilverlightVersion4" +type = "i64" + +[[attribute]] +key = "PageCharset" +column = "PageCharset" +type = "str" + +[[attribute]] +key = "CodeVersion" +column = "CodeVersion" +type = "i64" + +[[attribute]] +key = "IsLink" +column = "IsLink" +type = "i64" + +[[attribute]] +key = "IsDownload" +column = "IsDownload" +type = "i64" + +[[attribute]] +key = "IsNotBounce" +column = "IsNotBounce" +type = "i64" + +[[attribute]] +key = "FUniqID" +column = "FUniqID" +type = "i64" + +[[attribute]] +key = "OriginalURL" +column = "OriginalURL" +type = "str" + +[[attribute]] +key = "HID" +column = "HID" +type = "i64" + +[[attribute]] +key = "IsOldCounter" +column = "IsOldCounter" +type = "i64" + +[[attribute]] +key = "IsEvent" +column = "IsEvent" +type = "i64" + +[[attribute]] +key = "IsParameter" +column = "IsParameter" +type = "i64" + +[[attribute]] +key = "DontCountHits" +column = "DontCountHits" +type = "i64" + +[[attribute]] +key = "WithHash" +column = "WithHash" +type = "i64" + +[[attribute]] +key = "HitColor" +column = "HitColor" +type = "str" + +[[attribute]] +key = "LocalEventTime" +column = "LocalEventTime" +type = "i64" + +[[attribute]] +key = "Age" +column = "Age" +type = "i64" + +[[attribute]] +key = "Sex" +column = "Sex" +type = "i64" + +[[attribute]] +key = "Income" +column = "Income" +type = "i64" + +[[attribute]] +key = "Interests" +column = "Interests" +type = "i64" + +[[attribute]] +key = "Robotness" +column = "Robotness" +type = "i64" + +[[attribute]] +key = "RemoteIP" +column = "RemoteIP" +type = "i64" + +[[attribute]] +key = "WindowName" +column = "WindowName" +type = "i64" + +[[attribute]] +key = "OpenerName" +column = "OpenerName" +type = "i64" + +[[attribute]] +key = "HistoryLength" +column = "HistoryLength" +type = "i64" + +[[attribute]] +key = "BrowserLanguage" +column = "BrowserLanguage" +type = "str" + +[[attribute]] +key = "BrowserCountry" +column = "BrowserCountry" +type = "str" + +[[attribute]] +key = "SocialNetwork" +column = "SocialNetwork" +type = "str" + +[[attribute]] +key = "SocialAction" +column = "SocialAction" +type = "str" + +[[attribute]] +key = "HTTPError" +column = "HTTPError" +type = "i64" + +[[attribute]] +key = "SendTiming" +column = "SendTiming" +type = "i64" + +[[attribute]] +key = "DNSTiming" +column = "DNSTiming" +type = "i64" + +[[attribute]] +key = "ConnectTiming" +column = "ConnectTiming" +type = "i64" + +[[attribute]] +key = "ResponseStartTiming" +column = "ResponseStartTiming" +type = "i64" + +[[attribute]] +key = "ResponseEndTiming" +column = "ResponseEndTiming" +type = "i64" + +[[attribute]] +key = "FetchTiming" +column = "FetchTiming" +type = "i64" + +[[attribute]] +key = "SocialSourceNetworkID" +column = "SocialSourceNetworkID" +type = "i64" + +[[attribute]] +key = "SocialSourcePage" +column = "SocialSourcePage" +type = "str" + +[[attribute]] +key = "ParamPrice" +column = "ParamPrice" +type = "i64" + +[[attribute]] +key = "ParamOrderID" +column = "ParamOrderID" +type = "str" + +[[attribute]] +key = "ParamCurrency" +column = "ParamCurrency" +type = "str" + +[[attribute]] +key = "ParamCurrencyID" +column = "ParamCurrencyID" +type = "i64" + +[[attribute]] +key = "OpenstatServiceName" +column = "OpenstatServiceName" +type = "str" + +[[attribute]] +key = "OpenstatCampaignID" +column = "OpenstatCampaignID" +type = "str" + +[[attribute]] +key = "OpenstatAdID" +column = "OpenstatAdID" +type = "str" + +[[attribute]] +key = "OpenstatSourceID" +column = "OpenstatSourceID" +type = "str" + +[[attribute]] +key = "UTMSource" +column = "UTMSource" +type = "str" + +[[attribute]] +key = "UTMMedium" +column = "UTMMedium" +type = "str" + +[[attribute]] +key = "UTMCampaign" +column = "UTMCampaign" +type = "str" + +[[attribute]] +key = "UTMContent" +column = "UTMContent" +type = "str" + +[[attribute]] +key = "UTMTerm" +column = "UTMTerm" +type = "str" + +[[attribute]] +key = "FromTag" +column = "FromTag" +type = "str" + +[[attribute]] +key = "HasGCLID" +column = "HasGCLID" +type = "i64" + +[[attribute]] +key = "RefererHash" +column = "RefererHash" +type = "i64" + +[[attribute]] +key = "URLHash" +column = "URLHash" +type = "i64" + +[[attribute]] +key = "CLID" +column = "CLID" +type = "i64" diff --git a/ravel/install b/ravel/install new file mode 100755 index 0000000000..1b3c515587 --- /dev/null +++ b/ravel/install @@ -0,0 +1,174 @@ +#!/bin/bash +set -eu +. ./ravel-env.sh + +# A server left over from a previous run holds the binaries this script is about +# to replace, and ./start is idempotent through ./check, so a survivor would go +# on serving and the run would measure the old build. +./stop + +# ClickBench standardises on Ubuntu, so apt is the primary path; the dnf branch +# keeps the entry runnable on the Amazon Linux images some of these VMs ship +# with rather than failing at the first line. +if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update -qq + # awscli is deliberately not in this list: Ubuntu 24.04 ships no awscli + # package ("has no installation candidate"), so it is installed from + # Amazon's own archive below on every distribution. + sudo apt-get install -y --no-install-recommends jq curl ca-certificates unzip +elif command -v dnf >/dev/null 2>&1; then + # curl is deliberately absent: Amazon Linux ships curl-minimal, which + # provides the binary and conflicts with the full curl package. + sudo dnf install -y -q jq ca-certificates unzip +else + echo "install: no supported package manager (apt-get or dnf)" >&2 + exit 1 +fi + +# Bucket provisioning below and ./data-size (which sums the tenant's bytes in +# the bucket) need the AWS CLI. Installed from Amazon's archive rather than +# from a distribution package so the entry does not depend on which +# distributions still carry one. +if ! command -v aws >/dev/null 2>&1; then + case "$(uname -m)" in + x86_64|amd64) awsarch=x86_64 ;; + aarch64|arm64) awsarch=aarch64 ;; + *) echo "install: unsupported arch $(uname -m) for the AWS CLI" >&2; exit 1 ;; + esac + curl -fsSL --retry 3 "https://awscli.amazonaws.com/awscli-exe-linux-${awsarch}.zip" -o /tmp/awscliv2.zip + rm -rf /tmp/aws + unzip -q /tmp/awscliv2.zip -d /tmp + sudo /tmp/aws/install --update +fi +aws --version + +RAVEL_VERSION="${RAVEL_VERSION:-v0.17.0}" +RAVEL_REPO="${RAVEL_REPO:-https://github.com/NOFireAI/ravel}" + +case "$(uname -m)" in + x86_64|amd64) arch=amd64 ;; + aarch64|arm64) arch=arm64 ;; + *) echo "install: unsupported arch $(uname -m)" >&2; exit 1 ;; +esac + +mkdir -p "$RAVEL_BIN_DIR" + +# Preferred path: the released binaries, which are the ones extracted from the +# signed container images, so a downloaded binary is byte-identical to what the +# published image runs (ADR-0086). Checksums are verified against the release's +# own SHA256SUMS. +base="${RAVEL_REPO}/releases/download/${RAVEL_VERSION}" +if curl -fsSL --retry 3 -o "$RAVEL_BIN_DIR/SHA256SUMS" "${base}/SHA256SUMS"; then + # Downloaded under a temporary name and moved into place only once the + # checksum passes, so an unverified binary never sits at the path ./start + # runs. The rename also replaces a binary that is still executing, which + # writing to it in place does not: curl fails with ETXTBSY. + for b in ravel-server ravel-cli; do + curl -fsSL --retry 3 -o "$RAVEL_BIN_DIR/${b}.new" "${base}/${b}-linux-${arch}" + done + ( cd "$RAVEL_BIN_DIR" && for b in ravel-server ravel-cli; do + want=$(awk -v n="${b}-linux-${arch}" '$2 == n || $2 == "*"n {print $1}' SHA256SUMS | head -1) + [ -n "$want" ] || { echo "install: ${b}-linux-${arch} absent from SHA256SUMS" >&2; exit 1; } + have=$(sha256sum "${b}.new" | awk '{print $1}') + [ "$want" = "$have" ] || { echo "install: checksum mismatch for $b" >&2; exit 1; } + chmod +x "${b}.new" + mv -f "${b}.new" "$b" + done ) +else + # Fallback: build at a ref. Used when the release is unreachable, or to + # benchmark an unreleased commit via RAVEL_REF. + echo "install: release ${RAVEL_VERSION} not reachable, building from source" >&2 + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get install -y --no-install-recommends git build-essential pkg-config libssl-dev protobuf-compiler + else + sudo dnf install -y -q git gcc gcc-c++ make pkgconfig openssl-devel protobuf-compiler + fi + if ! cargo --version >/dev/null 2>&1; then + if command -v rustup >/dev/null 2>&1; then rustup default stable + else curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal; fi + fi + # shellcheck disable=SC1091 + [ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env" + [ -d src ] || git clone --filter=blob:none "${RAVEL_REPO}.git" src + git -C src fetch origin --quiet + git -C src checkout -q --detach "${RAVEL_REF:-$RAVEL_VERSION}" + git -C src rev-parse HEAD > ravel-commit.txt + # The SQL surface sits behind a cargo feature that is off by default. + # --release matters: the gate profile is opt-level 0 and loads ~7x slower. + cargo build --manifest-path src/Cargo.toml --release -p ravel-server --features sql + cargo build --manifest-path src/Cargo.toml --release -p ravel-cli + cp src/target/release/ravel-server src/target/release/ravel-cli "$RAVEL_BIN_DIR/" +fi + +# What will run, by content rather than by a version string. +sha256sum "$RAVEL_SERVER" "$RAVEL_CLI" + +# The object store: a single-node RustFS on this machine's disk, so the entry +# is comparable with the local-disk systems in this repository and needs no +# cloud account. A pinned release from GitHub, verified against the release's +# own SHA256SUMS. A loopback RAVEL_S3_ENDPOINT means the RustFS this entry +# manages; anything else is an external S3-compatible store the operator +# provisioned, and this section is skipped. +RUSTFS_VERSION="${RUSTFS_VERSION:-1.0.0}" +case "$RAVEL_S3_ENDPOINT" in + http://127.0.0.1:*|http://localhost:*) local_store=yes ;; + *) local_store=no ;; +esac +if [ "$local_store" = yes ]; then + mkdir -p "$RUSTFS_DIR" + if ! [ -x "$RUSTFS_DIR/rustfs" ] || ! "$RUSTFS_DIR/rustfs" --version 2>/dev/null | grep -q "^rustfs ${RUSTFS_VERSION}\$"; then + case "$arch" in + amd64) rarch=x86_64 ;; + arm64) rarch=aarch64 ;; + esac + zip="rustfs-linux-${rarch}-gnu-v${RUSTFS_VERSION}.zip" + rbase="https://github.com/rustfs/rustfs/releases/download/${RUSTFS_VERSION}" + curl -fsSL --retry 3 -o "$RUSTFS_DIR/$zip" "${rbase}/${zip}" + want=$(curl -fsSL --retry 3 "${rbase}/SHA256SUMS" | awk -v n="$zip" '$2 == n {print $1}') + have=$(sha256sum "$RUSTFS_DIR/$zip" | awk '{print $1}') + [ -n "$want" ] && [ "$want" = "$have" ] || { echo "install: checksum mismatch for $zip" >&2; exit 1; } + rm -rf "$RUSTFS_DIR/unpack" + unzip -q -o "$RUSTFS_DIR/$zip" -d "$RUSTFS_DIR/unpack" + chmod +x "$RUSTFS_DIR/unpack/rustfs" + # A running RustFS keeps serving from the replaced inode; ./rustfs-start + # only launches a new one when the health check fails. + mv -f "$RUSTFS_DIR/unpack/rustfs" "$RUSTFS_DIR/rustfs" + rm -rf "$RUSTFS_DIR/unpack" "$RUSTFS_DIR/$zip" + fi + "$RUSTFS_DIR/rustfs" --version | head -n 1 + # Credentials: generated once, readable by this user only, handed to + # RustFS and to Ravel through the environment. Nothing in this repository + # or in a process argument list carries a key. + if [ ! -f "$RUSTFS_DIR/credentials.env" ]; then + ( umask 077 + printf 'RUSTFS_ACCESS_KEY=%s\nRUSTFS_SECRET_KEY=%s\n' \ + "ravel-$(head -c 6 /dev/urandom | od -An -tx1 | tr -d ' \n')" \ + "$(head -c 30 /dev/urandom | base64 | tr -d '/+=\n')" \ + > "$RUSTFS_DIR/credentials.env" ) + fi + # The server records query text on its audit trail tokenized under a + # key. A keyed deployment derives it; this entry runs unkeyed (see + # ./start), so it needs one of its own. Generated once, kept beside the + # RustFS credentials, handed to the server through the environment. + if ! grep -q '^RAVEL_AUDIT_TOKEN_KEY=' "$RUSTFS_DIR/credentials.env"; then + ( umask 077 + printf 'RAVEL_AUDIT_TOKEN_KEY=%s\n' \ + "$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')" \ + >> "$RUSTFS_DIR/credentials.env" ) + fi + # Re-source so the keys just written are exported for the steps below. + . ./ravel-env.sh + ./rustfs-start +fi + +# The bucket. Everything Ravel writes lands under a t// prefix in +# it, so an existing bucket is fine; measure in one that holds nothing else. +if ! aws s3api head-bucket --bucket "$RAVEL_S3_BUCKET" >/dev/null 2>&1; then + aws s3api create-bucket --bucket "$RAVEL_S3_BUCKET" >/dev/null +fi + +# One-time bucket provisioning (ADR-0050 section 6): the server refuses to start +# against a store that has never been qualified. It lives here rather than in +# ./load because it qualifies the bucket rather than loading the dataset, and +# ./load's wall clock is the reported Load time. +"$RAVEL_CLI" --store s3 --s3-auth static store qualify diff --git a/ravel/load b/ravel/load new file mode 100755 index 0000000000..435482ec5d --- /dev/null +++ b/ravel/load @@ -0,0 +1,52 @@ +#!/bin/bash +# Loads hits.parquet into Ravel. The driver times this script and prints the +# "Load time:" line itself, so nothing here prints one. +# +# Everything this writes lands in object storage. There is no post-load step: +# no compaction, no catalog fold, no VACUUM equivalent. Object sizing is done +# at ingest instead, by the geometry below. +set -eu +. ./ravel-env.sh + +PARQUET="${RAVEL_PARQUET:-hits.parquet}" +MAPPING="${RAVEL_MAPPING:-hits.mapping.toml}" +[ -f "$PARQUET" ] || { echo "load: $PARQUET not found (the driver's download step should have fetched it)" >&2; exit 1; } +[ -f "$MAPPING" ] || { echo "load: $MAPPING not found" >&2; exit 1; } + +# `ravel-cli load` drives the ingest router in-process and writes to object +# storage itself, so the server takes no part in the load. A server left running +# through it does no work for the load while its startup cache warmup and its +# background catalog folds compete for the same memory the loader needs. That is +# not a small effect: on a 32 GB box the read cache alone resolves to a 26.3 GB +# ceiling and the kernel OOM-killed a 24.1 GB ravel-server six seconds before the +# last row landed (NOFireAI/ravel#1170). Stop it here, start it again at the end. +./stop + +# Declare the typed attribute columns before the first row lands. The mapping +# names each hits column and its type; declaring them lets ingest stamp exact +# per-column statistics on the commit records, which is what lets aggregates be +# answered without reading data objects. +"$RAVEL_CLI" --store s3 --s3-auth static \ + typed-attr-column set "$RAVEL_TENANT" --from-mapping "$MAPPING" + +# Load geometry. One batch becomes one object per involved shard, so +# --batch-rows sets the object size directly: 150,000 rows over 4 shards is +# ~37.5k rows (~4 MB) per object and ~2,600 objects for the 100M-row dataset. +# That is the whole object-sizing mechanism; --target-bytes stays at its +# default of 1 (flush every write) because accumulating instead makes each +# batch's acknowledgement wait for a later batch and the load runs several +# times longer for the same layout. +"$RAVEL_CLI" --store s3 --s3-auth static load \ + --parquet "$PARQUET" \ + --tenant "$RAVEL_TENANT" \ + --mapping "$MAPPING" \ + --shards "$RAVEL_SHARDS" \ + --read-cursors "${RAVEL_READ_CURSORS:-16}" \ + --batch-rows "${RAVEL_BATCH_ROWS:-150000}" \ + --target-bytes 1 + +sync + +# Serving again for ./check and the query phase. ./start waits for the listener, +# so this returns only once the server answers. +./start diff --git a/ravel/queries.sql b/ravel/queries.sql new file mode 100644 index 0000000000..1dedee62a9 --- /dev/null +++ b/ravel/queries.sql @@ -0,0 +1,43 @@ +SELECT COUNT(*) FROM logs; +SELECT COUNT(*) FROM logs WHERE "AdvEngineID" <> 0; +SELECT SUM("AdvEngineID"), COUNT(*), AVG("ResolutionWidth") FROM logs; +SELECT AVG("UserID") FROM logs; +SELECT COUNT(DISTINCT "UserID") FROM logs; +SELECT COUNT(DISTINCT "SearchPhrase") FROM logs; +SELECT MIN("EventDate"), MAX("EventDate") FROM logs; +SELECT "AdvEngineID", COUNT(*) FROM logs WHERE "AdvEngineID" <> 0 GROUP BY "AdvEngineID" ORDER BY COUNT(*) DESC; +SELECT "RegionID", COUNT(DISTINCT "UserID") AS u FROM logs GROUP BY "RegionID" ORDER BY u DESC LIMIT 10; +SELECT "RegionID", SUM("AdvEngineID"), COUNT(*) AS c, AVG("ResolutionWidth"), COUNT(DISTINCT "UserID") FROM logs GROUP BY "RegionID" ORDER BY c DESC LIMIT 10; +SELECT "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM logs WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhoneModel" ORDER BY u DESC LIMIT 10; +SELECT "MobilePhone", "MobilePhoneModel", COUNT(DISTINCT "UserID") AS u FROM logs WHERE "MobilePhoneModel" <> '' GROUP BY "MobilePhone", "MobilePhoneModel" ORDER BY u DESC LIMIT 10; +SELECT "SearchPhrase", COUNT(*) AS c FROM logs WHERE "SearchPhrase" <> '' GROUP BY "SearchPhrase" ORDER BY c DESC LIMIT 10; +SELECT "SearchPhrase", COUNT(DISTINCT "UserID") AS u FROM logs WHERE "SearchPhrase" <> '' GROUP BY "SearchPhrase" ORDER BY u DESC LIMIT 10; +SELECT "SearchEngineID", "SearchPhrase", COUNT(*) AS c FROM logs WHERE "SearchPhrase" <> '' GROUP BY "SearchEngineID", "SearchPhrase" ORDER BY c DESC LIMIT 10; +SELECT "UserID", COUNT(*) FROM logs GROUP BY "UserID" ORDER BY COUNT(*) DESC LIMIT 10; +SELECT "UserID", "SearchPhrase", COUNT(*) FROM logs GROUP BY "UserID", "SearchPhrase" ORDER BY COUNT(*) DESC LIMIT 10; +SELECT "UserID", "SearchPhrase", COUNT(*) FROM logs GROUP BY "UserID", "SearchPhrase" LIMIT 10; +SELECT "UserID", date_part('minute', ts) AS m, "SearchPhrase", COUNT(*) FROM logs GROUP BY "UserID", m, "SearchPhrase" ORDER BY COUNT(*) DESC LIMIT 10; +SELECT "UserID" FROM logs WHERE "UserID" = 435090932899640449; +SELECT COUNT(*) FROM logs WHERE "URL" LIKE '%google%'; +SELECT "SearchPhrase", MIN("URL"), COUNT(*) AS c FROM logs 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 logs WHERE "Title" LIKE '%Google%' AND "URL" NOT LIKE '%.google.%' AND "SearchPhrase" <> '' GROUP BY "SearchPhrase" ORDER BY c DESC LIMIT 10; +SELECT * FROM logs WHERE "URL" LIKE '%google%' ORDER BY ts LIMIT 10; +SELECT "SearchPhrase" FROM logs WHERE "SearchPhrase" <> '' ORDER BY ts LIMIT 10; +SELECT "SearchPhrase" FROM logs WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10; +SELECT "SearchPhrase" FROM logs WHERE "SearchPhrase" <> '' ORDER BY ts, "SearchPhrase" LIMIT 10; +SELECT "CounterID", AVG(length("URL")) AS l, COUNT(*) AS c FROM logs 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 logs 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 logs; +SELECT "SearchEngineID", "ClientIP", COUNT(*) AS c, SUM("IsRefresh"), AVG("ResolutionWidth") FROM logs WHERE "SearchPhrase" <> '' GROUP BY "SearchEngineID", "ClientIP" ORDER BY c DESC LIMIT 10; +SELECT "WatchID", "ClientIP", COUNT(*) AS c, SUM("IsRefresh"), AVG("ResolutionWidth") FROM logs WHERE "SearchPhrase" <> '' GROUP BY "WatchID", "ClientIP" ORDER BY c DESC LIMIT 10; +SELECT "WatchID", "ClientIP", COUNT(*) AS c, SUM("IsRefresh"), AVG("ResolutionWidth") FROM logs GROUP BY "WatchID", "ClientIP" ORDER BY c DESC LIMIT 10; +SELECT "URL", COUNT(*) AS c FROM logs GROUP BY "URL" ORDER BY c DESC LIMIT 10; +SELECT 1, "URL", COUNT(*) AS c FROM logs GROUP BY 1, "URL" ORDER BY c DESC LIMIT 10; +SELECT "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3, COUNT(*) AS c FROM logs GROUP BY "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3 ORDER BY c DESC LIMIT 10; +SELECT "URL", COUNT(*) AS PageViews FROM logs WHERE "CounterID" = 62 AND "EventDate" >= 15887 AND "EventDate" <= 15917 AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "URL" <> '' GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10; +SELECT "Title", COUNT(*) AS PageViews FROM logs WHERE "CounterID" = 62 AND "EventDate" >= 15887 AND "EventDate" <= 15917 AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "Title" <> '' GROUP BY "Title" ORDER BY PageViews DESC LIMIT 10; +SELECT "URL", COUNT(*) AS PageViews FROM logs WHERE "CounterID" = 62 AND "EventDate" >= 15887 AND "EventDate" <= 15917 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 logs WHERE "CounterID" = 62 AND "EventDate" >= 15887 AND "EventDate" <= 15917 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 logs WHERE "CounterID" = 62 AND "EventDate" >= 15887 AND "EventDate" <= 15917 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 logs WHERE "CounterID" = 62 AND "EventDate" >= 15887 AND "EventDate" <= 15917 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', ts) AS M, COUNT(*) AS PageViews FROM logs WHERE "CounterID" = 62 AND "EventDate" >= 15900 AND "EventDate" <= 15901 AND "IsRefresh" = 0 AND "DontCountHits" = 0 GROUP BY DATE_TRUNC('minute', ts) ORDER BY DATE_TRUNC('minute', ts) LIMIT 10 OFFSET 1000; diff --git a/ravel/query b/ravel/query new file mode 100755 index 0000000000..94dabcfb29 --- /dev/null +++ b/ravel/query @@ -0,0 +1,32 @@ +#!/bin/bash +# Reads one SQL statement from stdin and runs it against ravel-server. +# Stdout: query result. +# Stderr: query runtime in fractional seconds on the last line. +# Exit non-zero on error, which the driver records as `null`. +set -e +. ./ravel-env.sh + +query=$(cat) + +# The SQL endpoint's `start`/`end` default to the last hour. ClickBench's hits +# rows carry 2013 EventTime values, so every query has to state a window that +# spans the whole dataset or it would legitimately match nothing. +body=$(jq -n --arg q "$query" '{query: $q, start: 0, end: 4102444800, timeout: 3600}') + +t1=$(date +%s%3N) +out=$(curl -sS --fail-with-body -X POST \ + -H 'Content-Type: application/json' \ + -H "x-ravel-tenant: $RAVEL_TENANT" \ + --data-binary "$body" \ + "http://${RAVEL_HTTP}/api/v1/sql") && exit_code=0 || exit_code=$? +t2=$(date +%s%3N) + +if [ "$exit_code" -ne 0 ]; then + printf '%s\n' "$out" >&2 + exit "$exit_code" +fi + +printf '%s\n' "$out" + +duration=$((t2 - t1)) +awk -v d="$duration" 'BEGIN { printf "%.3f\n", d / 1000 }' >&2 diff --git a/ravel/ravel-env.sh b/ravel/ravel-env.sh new file mode 100644 index 0000000000..8087716edf --- /dev/null +++ b/ravel/ravel-env.sh @@ -0,0 +1,72 @@ +# Shared environment for the Ravel ClickBench entry. Sourced by every script. +# +# Ravel keeps every durable byte in S3-compatible object storage; there is no +# local-disk storage mode (see README.md). For this benchmark the store is a +# single-node RustFS that ./install downloads, starts and provisions on this +# machine's own disk, so the entry needs no cloud bucket and no credentials +# from the operator. To run against another S3-compatible store instead, set +# RAVEL_S3_ENDPOINT, RAVEL_S3_BUCKET, RAVEL_S3_ACCESS_KEY and +# RAVEL_S3_SECRET_KEY before ./install. +# shellcheck shell=bash + +export RAVEL_S3_ENDPOINT="${RAVEL_S3_ENDPOINT:-http://127.0.0.1:9000}" +export RAVEL_S3_BUCKET="${RAVEL_S3_BUCKET:-clickbench}" +export RAVEL_S3_REGION="${RAVEL_S3_REGION:-us-east-1}" + +# Where ./install keeps RustFS: its binary, its data directory (this entry's +# "data on local disk"), its pid file, its log and the credentials it +# generated. ./rustfs-start brings it up from here. +export RUSTFS_DIR="${RUSTFS_DIR:-$PWD/rustfs}" + +# Static keys. ./install generates them once into a file readable by this +# user only, and every script reads them from there, so no key appears in a +# process argument list. The server and the CLI take RAVEL_S3_ACCESS_KEY and +# RAVEL_S3_SECRET_KEY; the AWS CLI (bucket provisioning and ./data-size) takes +# its own names plus the endpoint. +export RAVEL_S3_AUTH=static +if [ -z "${RAVEL_S3_ACCESS_KEY:-}" ] && [ -f "$RUSTFS_DIR/credentials.env" ]; then + # shellcheck disable=SC1091 + . "$RUSTFS_DIR/credentials.env" + export RAVEL_S3_ACCESS_KEY="$RUSTFS_ACCESS_KEY" + export RAVEL_S3_SECRET_KEY="$RUSTFS_SECRET_KEY" +fi +# The audit-trail tokenization key ./install generated; an unkeyed server +# refuses to start without one. +if [ -z "${RAVEL_AUDIT_TOKEN_KEY:-}" ] && [ -f "$RUSTFS_DIR/credentials.env" ]; then + RAVEL_AUDIT_TOKEN_KEY="$(sed -n 's/^RAVEL_AUDIT_TOKEN_KEY=//p' "$RUSTFS_DIR/credentials.env")" +fi +export RAVEL_AUDIT_TOKEN_KEY="${RAVEL_AUDIT_TOKEN_KEY:-}" +unset RAVEL_S3_SESSION_TOKEN AWS_SESSION_TOKEN +export AWS_ACCESS_KEY_ID="${RAVEL_S3_ACCESS_KEY:-}" +export AWS_SECRET_ACCESS_KEY="${RAVEL_S3_SECRET_KEY:-}" +export AWS_ENDPOINT_URL="$RAVEL_S3_ENDPOINT" +export AWS_DEFAULT_REGION="$RAVEL_S3_REGION" + +export RAVEL_TENANT="${RAVEL_TENANT:-clickbench}" +# Must match what ./load provisioned: a server configured for a different shard +# count refuses to resolve rather than answering over a subset (ADR-0050 s5). +export RAVEL_SHARDS="${RAVEL_SHARDS:-4}" + +# Where ./install puts the binaries. +RAVEL_BIN_DIR="${RAVEL_BIN_DIR:-$PWD/bin}" +export RAVEL_BIN_DIR +export RAVEL_SERVER="$RAVEL_BIN_DIR/ravel-server" +export RAVEL_CLI="$RAVEL_BIN_DIR/ravel-cli" + +# ADR-0046 read cache, local-disk tier. Empty (the default) means the RAM tier +# only, which is what the published result uses. With the store on this same +# volume the tier is not a faster disk; it keeps the objects the server already +# fetched once, and it survives a server restart. It is bounded by the same +# resolved ceiling as the RAM tier. +export RAVEL_CACHE_DIR="${RAVEL_CACHE_DIR:-}" + +# Loopback only: --dev-insecure-tenant-header refuses to enable unless +# --listen-http binds a loopback address, so this stays a single-node local +# benchmark and does not weaken the server's auth posture on any reachable +# interface. +export RAVEL_HTTP="${RAVEL_HTTP:-127.0.0.1:9080}" + +# ./start records the server's pid here and ./stop reads it. Stopping by pid +# rather than by command-line pattern keeps ./stop from matching, and killing, +# an unrelated process whose argv merely contains the server's flags. +export RAVEL_PIDFILE="${RAVEL_PIDFILE:-$PWD/server.pid}" diff --git a/ravel/results/20260908/c6a.4xlarge.json b/ravel/results/20260908/c6a.4xlarge.json new file mode 100644 index 0000000000..a524fde7d1 --- /dev/null +++ b/ravel/results/20260908/c6a.4xlarge.json @@ -0,0 +1,60 @@ +{ + "system": "Ravel", + "date": "2026-09-08", + "machine": "c6a.4xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","stateless","S3"], + "load_time": 1202, + "data_size": 11238388947, + "concurrent_qps": 0.498, + "concurrent_error_ratio": 0.057, + "result": [ + [0.196, 0.129, 0.116], + [42.32, 2.77, 2.817], + [42.245, 2.885, 2.857], + [42.22, 2.802, 2.771], + [42.323, 3.451, 3.408], + [42.355, 3.618, 3.504], + [0.184, 0.139, 0.128], + [42.346, 2.841, 2.78], + [42.494, 3.879, 3.811], + [42.6, 4.258, 4.302], + [42.379, 3.084, 3.084], + [42.405, 3.252, 3.248], + [42.394, 3.417, 3.399], + [42.719, 4.031, 3.972], + [42.413, 3.647, 3.607], + [42.352, 3.509, 3.498], + [42.47, 4.672, 4.641], + [42.371, 4.577, 4.618], + [42.579, 5.335, 5.374], + [44.312, 6.362, 4.156], + [42.349, 4.332, 4.331], + [42.353, 4.824, 4.83], + [42.387, 7.293, 7.233], + [42.667, 4.848, 4.795], + [42.304, 3.115, 3.108], + [42.255, 3.026, 3.021], + [42.285, 3.11, 3.067], + [42.414, 5.546, 5.534], + [42.704, 7.412, 7.379], + [42.303, 2.756, 2.755], + [42.424, 4.325, 4.357], + [42.47, 4.701, 4.635], + [null, null, null], + [42.99, 7.043, 7.169], + [42.985, 7.055, 7.101], + [42.358, 3.408, 3.401], + [42.46, 6.27, 5.746], + [42.425, 6.36, 5.787], + [42.43, 6.115, 6.063], + [42.534, 6.211, 6.071], + [42.421, 5.965, 5.949], + [42.415, 6.232, 5.924], + [42.416, 6.055, 5.629] +] + } + \ No newline at end of file diff --git a/ravel/results/20260925/c8g.metal-48xl.json b/ravel/results/20260925/c8g.metal-48xl.json new file mode 100644 index 0000000000..11a2c0e2e5 --- /dev/null +++ b/ravel/results/20260925/c8g.metal-48xl.json @@ -0,0 +1,60 @@ +{ + "system": "Ravel", + "date": "2026-09-25", + "machine": "c8g.metal-48xl", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","stateless","S3"], + "load_time": 820, + "data_size": 11238648900, + "concurrent_qps": 5.437, + "concurrent_error_ratio": 0, + "result": [ + [0.373, 0.337, 0.333], + [42.482, 0.537, 0.518], + [42.415, 0.504, 0.492], + [42.399, 0.496, 0.48], + [42.534, 0.709, 0.726], + [42.571, 0.688, 0.668], + [0.376, 0.355, 0.373], + [42.573, 0.633, 0.61], + [42.669, 0.825, 0.849], + [42.72, 0.97, 0.941], + [42.702, 0.852, 0.868], + [42.827, 0.804, 0.802], + [42.619, 0.701, 0.708], + [42.852, 0.917, 0.947], + [42.642, 0.761, 0.697], + [42.548, 0.778, 0.738], + [42.617, 1.013, 0.944], + [42.671, 0.863, 0.907], + [42.71, 1.428, 1.498], + [42.487, 0.566, 0.553], + [42.542, 0.719, 0.661], + [42.653, 0.92, 0.878], + [42.658, 1.168, 1.106], + [42.887, 1.055, 0.995], + [42.484, 0.588, 0.563], + [42.468, 0.576, 0.541], + [42.519, 0.69, 0.66], + [42.658, 0.975, 0.942], + [42.804, 1.42, 1.271], + [42.504, 0.638, 0.615], + [42.744, 0.977, 0.837], + [42.753, 0.873, 0.826], + [42.93, 1.818, 1.799], + [42.656, 1.348, 1.34], + [42.665, 1.348, 1.369], + [42.572, 0.697, 0.705], + [42.532, 0.608, 0.604], + [42.531, 0.612, 0.61], + [42.527, 0.611, 0.602], + [42.61, 0.674, 0.67], + [42.517, 0.608, 0.607], + [42.53, 0.609, 0.615], + [42.522, 0.605, 0.598] +] + } + \ No newline at end of file diff --git a/ravel/results/20260926/c6a.2xlarge.json b/ravel/results/20260926/c6a.2xlarge.json new file mode 100644 index 0000000000..d874e3cba7 --- /dev/null +++ b/ravel/results/20260926/c6a.2xlarge.json @@ -0,0 +1,60 @@ +{ + "system": "Ravel", + "date": "2026-09-26", + "machine": "c6a.2xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","stateless","S3"], + "load_time": 1405, + "data_size": 11238648951, + "concurrent_qps": 0.132, + "concurrent_error_ratio": 0.347, + "result": [ + [0.578, 0.52, 0.512], + [42.746, 34.258, 32.82], + [42.663, 35.946, 33.689], + [42.638, 35.726, 33.521], + [42.837, 35.558, 33.212], + [42.913, 36.164, 32.993], + [0.593, 0.551, 0.526], + [42.744, 36.101, 32.947], + [42.881, 36.053, 33.306], + [43.176, 36.535, 33.306], + [42.804, 35.401, 33.09], + [42.813, 35.94, 33.41], + [42.855, 35.59, 33.201], + [43.435, 37.157, 33.78], + [42.88, 36.344, 33.706], + [42.88, 36.452, 33.301], + [43.109, 36.575, 33.857], + [42.962, 35.02, 32.945], + [null, null, null], + [73.328, 45.445, 19.375], + [42.762, 35.635, 33.438], + [42.789, 35.588, 33.176], + [42.955, 35.297, 33.726], + [43.097, 35.931, 34.008], + [42.65, 35.61, 33.142], + [42.602, 36.03, 33.793], + [42.648, 36.001, 33.288], + [42.874, 35.722, 33.666], + [43.335, 36.783, 34.044], + [42.722, 35.536, 32.899], + [42.887, 36.072, 33.97], + [42.994, 36.56, 33.569], + [null, null, null], + [null, null, null], + [null, null, null], + [42.815, 35.102, 33.16], + [45.22, 33.472, 31.509], + [44.927, 34.038, 32.38], + [45.27, 34.111, 31.531], + [45.317, 34.295, 32.271], + [44.936, 33.593, 32.08], + [45.111, 33.491, 31.607], + [45.255, 33.7, 31.727] +] + } + \ No newline at end of file diff --git a/ravel/results/20260926/c6a.4xlarge.json b/ravel/results/20260926/c6a.4xlarge.json new file mode 100644 index 0000000000..b3acf52289 --- /dev/null +++ b/ravel/results/20260926/c6a.4xlarge.json @@ -0,0 +1,60 @@ +{ + "system": "Ravel", + "date": "2026-09-26", + "machine": "c6a.4xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","stateless","S3"], + "load_time": 1187, + "data_size": 11238648901, + "concurrent_qps": 0.46, + "concurrent_error_ratio": 0.058, + "result": [ + [0.538, 0.441, 0.456], + [42.669, 5.502, 5.538], + [42.573, 5.615, 5.635], + [42.618, 5.542, 5.527], + [42.673, 5.927, 5.914], + [42.689, 5.91, 5.915], + [0.521, 0.474, 0.46], + [42.668, 5.564, 5.503], + [42.69, 6.283, 6.286], + [42.919, 6.418, 6.469], + [42.694, 5.692, 5.671], + [42.691, 5.832, 5.842], + [42.702, 5.86, 5.906], + [43.042, 6.517, 6.451], + [42.744, 6.112, 6.085], + [42.69, 5.984, 5.989], + [42.819, 6.988, 6.949], + [42.72, 6.859, 6.882], + [42.886, 7.393, 7.386], + [46.45, 8.845, 6.659], + [42.668, 6.838, 6.876], + [42.679, 7.146, 7.196], + [42.732, 9.207, 9.125], + [43.036, 7.262, 7.294], + [42.623, 5.692, 5.633], + [42.585, 5.606, 5.633], + [42.64, 5.675, 5.689], + [42.745, 7.854, 7.853], + [42.971, 8.854, 8.931], + [42.634, 5.487, 5.478], + [42.765, 6.647, 6.518], + [42.778, 6.869, 6.905], + [null, null, null], + [43.302, 9.318, 9.362], + [43.3, 9.281, 9.273], + [42.678, 5.838, 5.855], + [42.91, 7.075, 7.558], + [42.88, 6.997, 7.501], + [42.86, 7.21, 7.414], + [42.978, 7.292, 7.365], + [42.826, 6.917, 7.245], + [42.831, 7.709, 7.063], + [42.855, 7.201, 6.968] +] + } + \ No newline at end of file diff --git a/ravel/results/20260926/c6a.metal.json b/ravel/results/20260926/c6a.metal.json new file mode 100644 index 0000000000..e7ff1ccd10 --- /dev/null +++ b/ravel/results/20260926/c6a.metal.json @@ -0,0 +1,60 @@ +{ + "system": "Ravel", + "date": "2026-09-26", + "machine": "c6a.metal", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","stateless","S3"], + "load_time": 1157, + "data_size": 11238648876, + "concurrent_qps": 3.695, + "concurrent_error_ratio": 0, + "result": [ + [0.462, 0.401, 0.399], + [42.655, 0.836, 0.811], + [42.723, 0.835, 0.729], + [42.755, 0.851, 0.783], + [43.109, 0.983, 0.9], + [42.963, 1.008, 0.917], + [0.67, 0.615, 0.598], + [42.99, 0.893, 0.867], + [43.174, 1.221, 1.139], + [43.277, 1.307, 1.299], + [43.173, 1.134, 1.103], + [43.184, 1.167, 1.12], + [43.047, 1.067, 0.946], + [43.227, 1.309, 1.244], + [43.049, 1.102, 1.018], + [43.006, 1.002, 0.934], + [43.099, 1.27, 1.222], + [42.989, 1.203, 1.105], + [43.208, 1.696, 1.624], + [42.751, 0.837, 0.839], + [42.846, 1.054, 0.94], + [43.003, 1.196, 1.105], + [43.018, 1.644, 1.614], + [43.188, 1.402, 1.357], + [42.797, 0.874, 0.791], + [42.777, 0.834, 0.829], + [42.812, 0.848, 0.766], + [43.087, 1.467, 1.348], + [43.309, 1.8, 1.609], + [42.784, 0.797, 0.729], + [43.09, 1.183, 1.091], + [43.074, 1.203, 1.159], + [43.594, 2.285, 2.247], + [43.121, 1.845, 1.811], + [43.165, 1.77, 1.746], + [42.978, 1.072, 0.942], + [42.938, 0.996, 0.886], + [42.88, 0.925, 0.854], + [42.818, 0.899, 0.889], + [43.089, 1.038, 1.003], + [42.777, 0.909, 0.927], + [42.866, 0.98, 0.951], + [42.949, 0.936, 0.96] +] + } + \ No newline at end of file diff --git a/ravel/results/20260926/c6a.xlarge.json b/ravel/results/20260926/c6a.xlarge.json new file mode 100644 index 0000000000..dbf64407a2 --- /dev/null +++ b/ravel/results/20260926/c6a.xlarge.json @@ -0,0 +1,60 @@ +{ + "system": "Ravel", + "date": "2026-09-26", + "machine": "c6a.xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","stateless","S3"], + "load_time": 2561, + "data_size": 11238648919, + "concurrent_qps": 0.058, + "concurrent_error_ratio": 0.386, + "result": [ + [0.611, 0.541, 0.502], + [42.805, 31.83, 31.706], + [42.811, 33.802, 33.314], + [42.638, 33.933, 33.092], + [42.983, 33.68, 33.339], + [42.976, 33.942, 33.176], + [0.553, 0.495, 0.508], + [42.752, 33.455, 32.984], + [43.148, 34.097, 33.526], + [43.402, 34.88, 34.209], + [42.804, 34.218, 33.554], + [42.831, 32.789, 32.156], + [42.924, 34.574, 33.619], + [44.051, 36.823, 36.811], + [42.938, 34.353, 33.733], + [43.073, 34.637, 33.907], + [null, null, null], + [null, null, null], + [null, null, null], + [71.484, 58.306, 58.061], + [42.795, 33.972, 33.569], + [42.829, 34.153, 33.756], + [43.873, 50.855, 45.779], + [43.251, 44.183, 44.13], + [42.728, 34.235, 33.484], + [42.898, 33.824, 33.007], + [42.972, 34.325, 33.519], + [42.978, 33.17, 32.844], + [44.041, 38.005, 38.865], + [42.724, 34.053, 33.464], + [42.945, 34.457, 33.691], + [43.275, 34.559, 34.419], + [null, null, null], + [null, null, null], + [null, null, null], + [42.885, 33.987, 33.505], + [44.52, 32.788, 32.066], + [44.444, 32.911, 32.042], + [44.373, 32.869, 32.263], + [44.524, 33.512, 32.519], + [44.373, 32.65, 32.487], + [44.296, 32.695, 32.256], + [44.254, 32.611, 32.151] +] + } + \ No newline at end of file diff --git a/ravel/results/20260926/c7a.metal-48xl.json b/ravel/results/20260926/c7a.metal-48xl.json new file mode 100644 index 0000000000..af12d96222 --- /dev/null +++ b/ravel/results/20260926/c7a.metal-48xl.json @@ -0,0 +1,60 @@ +{ + "system": "Ravel", + "date": "2026-09-26", + "machine": "c7a.metal-48xl", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","stateless","S3"], + "load_time": 1260, + "data_size": 11238648924, + "concurrent_qps": 5.148, + "concurrent_error_ratio": 0, + "result": [ + [0.54, 0.472, 0.491], + [42.748, 0.716, 0.677], + [42.729, 0.708, 0.656], + [42.677, 0.712, 0.672], + [42.803, 0.879, 0.87], + [42.911, 0.9, 0.804], + [0.634, 0.572, 0.577], + [42.887, 0.857, 0.812], + [43.012, 1.105, 1.064], + [43.137, 1.155, 1.177], + [43.118, 1.043, 1.002], + [43.083, 1.02, 0.996], + [42.934, 0.966, 0.933], + [43.17, 1.174, 1.083], + [42.898, 0.975, 0.921], + [42.885, 0.976, 0.87], + [43.003, 1.149, 1.042], + [42.88, 1.09, 1.009], + [43.073, 1.601, 1.515], + [42.666, 0.753, 0.761], + [42.778, 0.885, 0.801], + [42.925, 1.014, 0.954], + [42.941, 1.322, 1.205], + [43.149, 1.222, 1.205], + [42.746, 0.815, 0.752], + [42.703, 0.79, 0.783], + [42.745, 0.796, 0.772], + [43.041, 1.147, 1.078], + [43.402, 1.647, 1.621], + [42.758, 0.743, 0.68], + [42.937, 1.065, 1.019], + [42.987, 1.115, 0.973], + [43.429, 1.987, 1.888], + [43.074, 1.422, 1.351], + [43.039, 1.468, 1.38], + [42.892, 0.978, 0.883], + [42.879, 0.913, 0.857], + [42.834, 0.899, 0.837], + [42.945, 0.87, 0.842], + [42.923, 0.916, 0.902], + [42.779, 0.889, 0.869], + [42.851, 0.894, 0.854], + [42.813, 0.883, 0.843] +] + } + \ No newline at end of file diff --git a/ravel/results/20260926/c8g.4xlarge.json b/ravel/results/20260926/c8g.4xlarge.json new file mode 100644 index 0000000000..73942a0337 --- /dev/null +++ b/ravel/results/20260926/c8g.4xlarge.json @@ -0,0 +1,60 @@ +{ + "system": "Ravel", + "date": "2026-09-26", + "machine": "c8g.4xlarge", + "cluster_size": 1, + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": ["Rust","column-oriented","stateless","S3"], + "load_time": 784, + "data_size": 11238648894, + "concurrent_qps": 0.57, + "concurrent_error_ratio": 0.083, + "result": [ + [0.468, 0.435, 0.425], + [42.585, 1.907, 1.84], + [42.523, 1.956, 1.944], + [42.502, 1.92, 1.866], + [42.559, 2.211, 2.217], + [42.606, 2.321, 2.314], + [0.483, 0.441, 0.451], + [42.587, 1.874, 1.847], + [42.602, 2.506, 2.478], + [42.753, 2.911, 2.858], + [42.624, 2.131, 2.11], + [42.617, 2.291, 2.274], + [42.594, 2.218, 2.22], + [42.798, 2.575, 2.566], + [42.654, 2.379, 2.33], + [42.608, 2.269, 2.262], + [42.69, 2.919, 2.868], + [42.63, 2.867, 2.84], + [42.758, 3.259, 3.234], + [43.662, 4.206, 3.214], + [42.583, 2.714, 2.749], + [42.6, 3.021, 2.965], + [42.64, 4.961, 4.802], + [42.892, 2.974, 3.002], + [42.586, 2.105, 2.091], + [42.528, 2.093, 2.058], + [42.552, 2.134, 2.063], + [42.66, 3.274, 3.217], + [42.796, 5.004, 5.062], + [42.575, 1.906, 1.865], + [42.658, 2.89, 2.849], + [42.665, 2.965, 2.987], + [null, null, null], + [42.836, 3.812, 3.838], + [42.832, 3.822, 3.877], + [42.581, 2.231, 2.198], + [42.63, 3.911, 3.925], + [42.623, 3.789, 4.077], + [42.603, 4.065, 3.918], + [42.659, 3.995, 3.93], + [42.59, 3.895, 3.867], + [42.598, 3.775, 4.065], + [42.592, 3.831, 3.932] +] + } + \ No newline at end of file diff --git a/ravel/rustfs-start b/ravel/rustfs-start new file mode 100755 index 0000000000..5c9ca5fff9 --- /dev/null +++ b/ravel/rustfs-start @@ -0,0 +1,45 @@ +#!/bin/bash +# Brings up the local RustFS this entry runs against, if it is not already up. +# Idempotent: ./install calls it after downloading RustFS, and ./start calls it +# before every server start, so a RustFS that died between queries is restarted +# rather than turning the rest of the run into errors. Its data directory is +# on this machine's disk; ./stop leaves RustFS running on purpose, the driver's +# restart cycle is for the database, and the page-cache drop before each cold +# run reaches RustFS's files the same as any local-disk engine's. +set -eu +. ./ravel-env.sh + +# Unauthenticated, so a 200 cannot be confused with a credential problem. +health="${RAVEL_S3_ENDPOINT}/health" +if curl -fsS -o /dev/null "$health" 2>/dev/null; then + exit 0 +fi + +[ -x "$RUSTFS_DIR/rustfs" ] || { echo "rustfs-start: $RUSTFS_DIR/rustfs missing; run ./install first" >&2; exit 1; } +[ -f "$RUSTFS_DIR/credentials.env" ] || { echo "rustfs-start: $RUSTFS_DIR/credentials.env missing; run ./install first" >&2; exit 1; } +mkdir -p "$RUSTFS_DIR/data" + +# Credentials reach RustFS through its environment, never its argument list. +# The console is disabled explicitly: left unset, RustFS still opens its +# console listener on :9001 on every interface. Nothing here uses it, and the +# entry should open exactly two loopback ports, the store's and the database's. +( + set -a + # shellcheck disable=SC1091 + . "$RUSTFS_DIR/credentials.env" + set +a + export RUSTFS_ADDRESS="${RAVEL_S3_ENDPOINT#http://}" + export RUSTFS_CONSOLE_ENABLE=false + nohup "$RUSTFS_DIR/rustfs" server "$RUSTFS_DIR/data" \ + > "$RUSTFS_DIR/rustfs.log" 2>&1 & + echo $! > "$RUSTFS_DIR/rustfs.pid" +) + +for _ in $(seq 1 60); do + if curl -fsS -o /dev/null "$health" 2>/dev/null; then + exit 0 + fi + sleep 1 +done +echo "rustfs-start: RustFS did not answer $health within 60s; see $RUSTFS_DIR/rustfs.log" >&2 +exit 1 diff --git a/ravel/start b/ravel/start new file mode 100755 index 0000000000..5946594f2a --- /dev/null +++ b/ravel/start @@ -0,0 +1,109 @@ +#!/bin/bash +set -eu +. ./ravel-env.sh + +# Idempotent: ./check succeeds only for the server this harness launched (it +# verifies the pidfile's pid owns the listening port), so "already serving" +# here means ours is up, never a stale one. +if ./check >/dev/null 2>&1; then + exit 0 +fi + +# Nothing of ours is serving. Before launching, the listen ports must be free: +# a killed server holds them for a while after its pid is gone, and a server +# this harness did not start may hold them indefinitely. Launching into either +# fails with EADDRINUSE, and the driver would then time out its check loop +# without saying why. Wait for the transient case, refuse the other. +http_port="${RAVEL_HTTP##*:}" +ports_busy() { + ss -ltnH "( sport = :$http_port or sport = :4317 )" 2>/dev/null | grep -q . +} +for _ in $(seq 1 30); do + ports_busy || break + sleep 1 +done +if ports_busy; then + echo "start: :$http_port or :4317 is still held after 30s by a process this harness did not start:" >&2 + ss -ltnpH "( sport = :$http_port or sport = :4317 )" 2>/dev/null | sed 's/^/start: /' >&2 + echo "start: refusing to launch behind it; stop that server first" >&2 + exit 1 +fi + +# No performance flags. Since Ravel 0.13.0 the server resolves its query +# budgets at startup: fetch concurrency from the core count, the read caches and +# the two SQL memory ceilings from usable memory (MemTotal, capped by the cgroup +# limit in a container), with a fixed segment cap and engine deadline. The +# resolved values and their sources are on the "performance default resolved" +# lines in server.log, which is what a published result should record. +# +# --shards is not a budget: it must match what ./load provisioned, or the server +# refuses to resolve rather than answering over a subset of shards. +# --tenant-hash-unkeyed is a scheme choice, not a performance flag. A fresh +# bucket has no sys/tenancy marker and the server bootstraps one on its first +# start; the default is the keyed derivation, which refuses to start without a +# deployment key file. This benchmark keeps no secret, so it pins the bucket to +# the unkeyed (v1) derivation, which is what makes a tenant's prefix derivable +# and ./data-size possible. It is written once and read on every later start. + +# One log per start, named by UTC time, with server.log a symlink to the +# current one. The driver restarts the server before every cold try, so a +# single rotated .prev kept exactly one prior log and a server that died two +# tries ago had lost its evidence. Readers of server.log (the README's +# "performance default resolved" lines) still find the live server there. +log="server.$(date -u +%Y%m%dT%H%M%SZ).log" +ln -sfn "$log" server.log + +# The store must be up before the server: a RustFS that died between queries +# would otherwise turn every later statement into an error. No-op when it is +# already serving. +./rustfs-start + +# Each in-flight object-store request holds a socket, and at startup the +# server has exited with EMFILE under the common 1024 soft limit. Raise the +# soft limit to the hard one. +ulimit -n "$(ulimit -Hn)" 2>/dev/null || true + +# The disk cache tier is opt-in and off by default; see RAVEL_CACHE_DIR in +# ravel-env.sh for what it does against a local store. +cache_args=() +[ -n "${RAVEL_CACHE_DIR:-}" ] && cache_args=(--cache-dir "$RAVEL_CACHE_DIR") + +# The tuned entry passes its server flags through RAVEL_TUNED_ARGS so the +# setting is reproducible from this file rather than from a hand-edited +# start script. Empty for the stock entry. Word-split on purpose. +# shellcheck disable=SC2206 +tuned_args=(${RAVEL_TUNED_ARGS:-}) + +nohup "$RAVEL_SERVER" \ + --store s3 \ + --s3-auth static \ + --tenant-hash-unkeyed \ + --shards "$RAVEL_SHARDS" \ + --listen-http "$RAVEL_HTTP" \ + --dev-insecure-tenant-header \ + ${cache_args[@]+"${cache_args[@]}"} \ + ${tuned_args[@]+"${tuned_args[@]}"} \ + > "$log" 2>&1 & +pid=$! +echo "$pid" > "$RAVEL_PIDFILE" +disown + +# Wait for it to serve, so that this script returning means the next one can use +# the server. ./check now also confirms that the answering process is this pid, +# so readiness here cannot be satisfied by anything else. The listener opens +# after a startup cache warmup, and the driver is not the only caller: ./load +# restarts the server itself. +for _ in $(seq 1 180); do + if ./check >/dev/null 2>&1; then + exit 0 + fi + if ! kill -0 "$pid" 2>/dev/null; then + echo "start: the server exited during startup, last lines of $log:" >&2 + tail -n 20 "$log" >&2 + exit 1 + fi + sleep 1 +done + +echo "start: the server did not begin serving within 180 seconds; see $log" >&2 +exit 1 diff --git a/ravel/stop b/ravel/stop new file mode 100755 index 0000000000..d14ea2fb82 --- /dev/null +++ b/ravel/stop @@ -0,0 +1,83 @@ +#!/bin/bash +# Stops the server started by ./start, by pid, and does not return until its +# listen ports are free. +# +# This deliberately does not pgrep for the server's command line. The driver +# calls ./stop before every cold try, and a pattern like "ravel-server.*--listen" +# also matches any shell whose own argv happens to contain it — including the +# ssh command of whoever is driving the run, which is a remarkably effective way +# to kill your own session instead of the server. +# +# It also deliberately does not source ravel-env.sh. That file requires +# RAVEL_S3_BUCKET and aborts the shell that sources it when the variable is +# unset, which turned ./stop into a silent no-op that returned 1 and left the +# server running. Stopping a process needs its pid and nothing else; the port +# comes from RAVEL_HTTP if set, else the default the env file would give. +# +# Waiting for the ports, not only the pid, is the part that matters for a cold +# try. A killed server holds its listen sockets during exit for a while after +# `kill -0` stops seeing it; a ./start issued in that window fails with +# EADDRINUSE and the driver, which swallows ./start's exit code, then times out +# its check loop without a cause. A trend runner built on the same pattern lost +# 27 consecutive cold starts to exactly this before it waited on the ports. +set -u +PIDFILE="${RAVEL_PIDFILE:-server.pid}" +http_port="${RAVEL_HTTP:-127.0.0.1:9080}" +http_port="${http_port##*:}" + +ports_busy() { + ss -ltnH "( sport = :$http_port or sport = :4317 )" 2>/dev/null | grep -q . +} +wait_ports_free() { + for _ in $(seq 1 30); do + ports_busy || return 0 + sleep 1 + done + echo "stop: :$http_port or :4317 still held 30s after the server was gone:" >&2 + ss -ltnpH "( sport = :$http_port or sport = :4317 )" 2>/dev/null | sed 's/^/stop: /' >&2 + return 1 +} + +if [ ! -f "$PIDFILE" ]; then + # Nothing of ours is recorded. If the ports are nonetheless held, a server + # this harness does not own is serving, and returning 0 would let the + # driver's next ./start find it and the next "cold" try run warm. Say so + # and fail; the driver's cold cycle tolerates the exit, but ./check will + # refuse the foreign server and the run stops there instead of silently. + if ports_busy; then + echo "stop: no pidfile at $PIDFILE but a server is listening; this harness does not own it:" >&2 + ss -ltnpH "( sport = :$http_port or sport = :4317 )" 2>/dev/null | sed 's/^/stop: /' >&2 + exit 1 + fi + exit 0 +fi + +pid=$(cat "$PIDFILE" 2>/dev/null || true) +case "$pid" in + ''|*[!0-9]*) rm -f "$PIDFILE"; wait_ports_free; exit $? ;; +esac + +# Only signal it if it is still the server: a recycled pid belonging to +# something else must not be killed. +if ! grep -qs 'ravel-server' "/proc/$pid/cmdline" 2>/dev/null; then + rm -f "$PIDFILE" + wait_ports_free + exit $? +fi + +kill "$pid" 2>/dev/null || true +for _ in $(seq 1 30); do + if ! kill -0 "$pid" 2>/dev/null; then + rm -f "$PIDFILE" + wait_ports_free + exit $? + fi + sleep 1 +done +kill -9 "$pid" 2>/dev/null || true +for _ in $(seq 1 10); do + kill -0 "$pid" 2>/dev/null || break + sleep 1 +done +rm -f "$PIDFILE" +wait_ports_free diff --git a/ravel/template.json b/ravel/template.json new file mode 100644 index 0000000000..ac8d3f7d09 --- /dev/null +++ b/ravel/template.json @@ -0,0 +1,12 @@ +{ + "system": "Ravel", + "proprietary": "no", + "hardware": "cpu", + "tuned": "no", + "tags": [ + "Rust", + "column-oriented", + "stateless", + "S3" + ] +}