mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
354 lines
12 KiB
Python
354 lines
12 KiB
Python
import os
|
|
import subprocess
|
|
import uuid
|
|
import time
|
|
import logging
|
|
import pytest
|
|
|
|
from helpers.cluster import ClickHouseCluster
|
|
|
|
cluster = ClickHouseCluster(__file__)
|
|
|
|
|
|
def _is_sanitizer_build():
|
|
"""Detect sanitizer builds before starting the cluster, to avoid creating
|
|
containers that are slow to shut down under sanitizers."""
|
|
binary = os.environ.get(
|
|
"CLICKHOUSE_TESTS_SERVER_BIN_PATH", "/usr/bin/clickhouse"
|
|
)
|
|
try:
|
|
result = subprocess.run(
|
|
[
|
|
binary,
|
|
"local",
|
|
"--query",
|
|
"SELECT value FROM system.build_options WHERE name = 'CXX_FLAGS'",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
return "-fsanitize=" in result.stdout
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
is_sanitizer = _is_sanitizer_build()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def started_cluster():
|
|
try:
|
|
cluster.add_instance(
|
|
"node1",
|
|
main_configs=["configs/conf.xml"],
|
|
with_minio=True,
|
|
mem_limit="20g",
|
|
cpu_limit=7,
|
|
)
|
|
# Don't create node_smol in sanitizer builds: the ASan leak checker at
|
|
# exit is very slow with high max_server_memory_usage, causing Docker
|
|
# teardown to hang for minutes.
|
|
if not is_sanitizer:
|
|
cluster.add_instance(
|
|
"node_smol",
|
|
main_configs=["configs/conf.xml", "configs/smol.xml"],
|
|
with_minio=True,
|
|
mem_limit="20g",
|
|
cpu_limit=7,
|
|
)
|
|
cluster.start()
|
|
|
|
yield cluster
|
|
finally:
|
|
cluster.shutdown()
|
|
|
|
|
|
def test_basics(started_cluster):
|
|
# Write ~8 MB of data.
|
|
node = cluster.instances["node1"]
|
|
node.query(
|
|
"create table a (k Int64 CODEC(NONE)) engine MergeTree order by k settings storage_policy = 's3';"
|
|
"system stop merges a;"
|
|
"insert into a select * from numbers(1000000);"
|
|
)
|
|
|
|
# Check that page cache is initially ~empty.
|
|
assert (
|
|
int(
|
|
node.query(
|
|
"select value from system.metrics where metric = 'PageCacheBytes'"
|
|
)
|
|
)
|
|
< 4000000
|
|
)
|
|
|
|
# Cold read, should miss cache. (Populating cache on write is not implemented.)
|
|
query_id = uuid.uuid4().hex
|
|
node.query(
|
|
"select sum(k) from a settings use_page_cache_for_disks_without_file_cache=1",
|
|
query_id=query_id,
|
|
)
|
|
node.query("system flush logs")
|
|
# Don't check that number of hits is zero - it's usually not.
|
|
assert (
|
|
int(
|
|
node.query(
|
|
f"select ProfileEvents['PageCacheMisses'] from system.query_log where query_id='{query_id}' and type = 'QueryFinish'"
|
|
)
|
|
)
|
|
> 0
|
|
)
|
|
assert (
|
|
int(
|
|
node.query(
|
|
"select value from system.metrics where metric = 'PageCacheBytes'"
|
|
)
|
|
)
|
|
> 4000000
|
|
)
|
|
|
|
# Repeat read, should hit cache.
|
|
query_id = uuid.uuid4().hex
|
|
node.query(
|
|
"select sum(k) from a settings use_page_cache_for_disks_without_file_cache=1",
|
|
query_id=query_id,
|
|
)
|
|
node.query("system flush logs")
|
|
assert (
|
|
node.query(
|
|
f"select ProfileEvents['PageCacheMisses'], ProfileEvents['PageCacheHits'] > 0 from system.query_log where query_id='{query_id}' and type = 'QueryFinish'"
|
|
)
|
|
== "0\t1\n"
|
|
)
|
|
|
|
# Drop cache and read again, should miss. Also don't write to cache.
|
|
node.query("system drop page cache")
|
|
query_id = uuid.uuid4().hex
|
|
node.query(
|
|
"select sum(k) from a settings use_page_cache_for_disks_without_file_cache=1, read_from_page_cache_if_exists_otherwise_bypass_cache=1",
|
|
query_id=query_id,
|
|
)
|
|
node.query("system flush logs")
|
|
assert (
|
|
int(
|
|
node.query(
|
|
f"select ProfileEvents['PageCacheMisses'] from system.query_log where query_id='{query_id}' and type = 'QueryFinish'"
|
|
)
|
|
)
|
|
> 0
|
|
)
|
|
assert (
|
|
int(
|
|
node.query(
|
|
"select value from system.metrics where metric = 'PageCacheBytes'"
|
|
)
|
|
)
|
|
< 4000000
|
|
)
|
|
|
|
# Repeat read, should still miss, but populate cache.
|
|
query_id = uuid.uuid4().hex
|
|
node.query(
|
|
"select sum(k) from a settings use_page_cache_for_disks_without_file_cache=1",
|
|
query_id=query_id,
|
|
)
|
|
node.query("system flush logs")
|
|
assert (
|
|
int(
|
|
node.query(
|
|
f"select ProfileEvents['PageCacheMisses'] from system.query_log where query_id='{query_id}' and type = 'QueryFinish'"
|
|
)
|
|
)
|
|
> 0
|
|
)
|
|
assert (
|
|
int(
|
|
node.query(
|
|
"select value from system.metrics where metric = 'PageCacheBytes'"
|
|
)
|
|
)
|
|
> 4000000
|
|
)
|
|
|
|
# Read again, hit the cache.
|
|
query_id = uuid.uuid4().hex
|
|
node.query(
|
|
"select sum(k) from a settings use_page_cache_for_disks_without_file_cache=1",
|
|
query_id=query_id,
|
|
)
|
|
node.query("system flush logs")
|
|
assert (
|
|
node.query(
|
|
f"select ProfileEvents['PageCacheMisses'], ProfileEvents['PageCacheHits'] > 0 from system.query_log where query_id='{query_id}' and type = 'QueryFinish'"
|
|
)
|
|
== "0\t1\n"
|
|
)
|
|
|
|
node.query("drop table a;" "system drop page cache;")
|
|
|
|
|
|
def test_cache_arena_isolation(started_cluster):
|
|
"""Test that page cache allocations land in the dedicated jemalloc cache arena
|
|
and that SYSTEM DROP PAGE CACHE reclaims arena pages."""
|
|
node = cluster.instances["node1"]
|
|
|
|
# Skip if jemalloc is not available
|
|
jemalloc_enabled = node.query(
|
|
"SELECT value IN ('ON', '1') FROM system.build_options WHERE name = 'USE_JEMALLOC'"
|
|
).strip()
|
|
if jemalloc_enabled != "1":
|
|
pytest.skip("jemalloc not available")
|
|
|
|
node.query(
|
|
"CREATE TABLE IF NOT EXISTS t_arena_page_cache (k Int64 CODEC(NONE)) "
|
|
"ENGINE MergeTree ORDER BY k SETTINGS storage_policy = 's3';"
|
|
"SYSTEM STOP MERGES t_arena_page_cache;"
|
|
"INSERT INTO t_arena_page_cache SELECT * FROM numbers(1000000);"
|
|
)
|
|
|
|
# Start clean
|
|
node.query("SYSTEM DROP PAGE CACHE")
|
|
node.query("SYSTEM DROP MARK CACHE")
|
|
node.query("SYSTEM RELOAD ASYNCHRONOUS METRICS")
|
|
|
|
# Record baseline arena state
|
|
baseline_pactive = int(
|
|
node.query(
|
|
"SELECT value FROM system.asynchronous_metrics "
|
|
"WHERE metric = 'jemalloc.cache_arena.pactive'"
|
|
)
|
|
)
|
|
|
|
# Populate page cache + mark cache via S3 read
|
|
node.query(
|
|
"SELECT sum(k) FROM t_arena_page_cache "
|
|
"SETTINGS use_page_cache_for_disks_without_file_cache=1"
|
|
)
|
|
|
|
# Verify page cache is populated
|
|
page_cache_bytes = int(
|
|
node.query(
|
|
"SELECT value FROM system.metrics WHERE metric = 'PageCacheBytes'"
|
|
)
|
|
)
|
|
assert page_cache_bytes > 0, f"Expected PageCacheBytes > 0, got {page_cache_bytes}"
|
|
|
|
node.query("SYSTEM RELOAD ASYNCHRONOUS METRICS")
|
|
|
|
# Verify cache arena has more active pages than baseline
|
|
current_pactive = int(
|
|
node.query(
|
|
"SELECT value FROM system.asynchronous_metrics "
|
|
"WHERE metric = 'jemalloc.cache_arena.pactive'"
|
|
)
|
|
)
|
|
assert current_pactive > baseline_pactive, (
|
|
f"Expected cache arena pactive to increase: "
|
|
f"baseline={baseline_pactive}, current={current_pactive}"
|
|
)
|
|
|
|
# Drop page cache and mark cache (both trigger arena purge)
|
|
node.query("SYSTEM DROP PAGE CACHE")
|
|
node.query("SYSTEM DROP MARK CACHE")
|
|
|
|
# Verify page cache is empty
|
|
page_cache_bytes_after = int(
|
|
node.query(
|
|
"SELECT value FROM system.metrics WHERE metric = 'PageCacheBytes'"
|
|
)
|
|
)
|
|
assert page_cache_bytes_after == 0, (
|
|
f"Expected PageCacheBytes = 0, got {page_cache_bytes_after}"
|
|
)
|
|
|
|
node.query("SYSTEM RELOAD ASYNCHRONOUS METRICS")
|
|
|
|
# Verify cache arena pages decreased after clearing
|
|
final_pactive = int(
|
|
node.query(
|
|
"SELECT value FROM system.asynchronous_metrics "
|
|
"WHERE metric = 'jemalloc.cache_arena.pactive'"
|
|
)
|
|
)
|
|
assert final_pactive < current_pactive, (
|
|
f"Expected cache arena pactive to decrease after drop + purge: "
|
|
f"before={current_pactive}, after={final_pactive}"
|
|
)
|
|
|
|
node.query("DROP TABLE t_arena_page_cache SYNC")
|
|
|
|
|
|
def test_size_adjustment(started_cluster):
|
|
if is_sanitizer:
|
|
pytest.skip("sanitizer build has higher memory consumption; also it is slow")
|
|
|
|
node = cluster.instances["node_smol"]
|
|
|
|
# Clean up from a possible previous failed run.
|
|
node.query("drop table if exists a;" "system drop page cache;")
|
|
|
|
# A few GB, see configs/smol.xml
|
|
memory_limit = int(node.query("select value from system.server_settings where name='max_server_memory_usage'"))
|
|
|
|
try:
|
|
# Insert more data than max_server_memory_usage.
|
|
node.query(
|
|
"create table a (k Int64 CODEC(NONE)) engine MergeTree order by k settings storage_policy = 's3';"
|
|
"system stop merges a;"
|
|
f"insert into a select * from numbers({int(memory_limit * 1.1 // 8)});"
|
|
)
|
|
|
|
# Make sure asynchronous metrics update.
|
|
time.sleep(3)
|
|
|
|
def get_metrics():
|
|
tsv = node.query("select metric, toInt64(value) as value from system.asynchronous_metrics where metric in ('CGroupMemoryTotal', 'CGroupMemoryUsed', 'OSMemoryTotal', 'MemoryResident', 'PageCacheMaxBytes') UNION ALL select metric, value from system.metrics where metric in ('PageCacheBytes', 'PageCacheCells')")
|
|
pairs = map(lambda p: p.split('\t'), tsv.strip().split('\n'))
|
|
return {k: int(v) for [k, v] in pairs}
|
|
|
|
metrics = get_metrics()
|
|
logging.info(f"server metrics before reads: {metrics}")
|
|
memory_used = metrics['MemoryResident']
|
|
os_memory_limit = metrics['OSMemoryTotal']
|
|
if 'CGroupMemoryUsed' in metrics:
|
|
memory_used = max(memory_used, metrics['CGroupMemoryUsed'])
|
|
if 'CGroupMemoryTotal' in metrics:
|
|
os_memory_limit = min(os_memory_limit, metrics['CGroupMemoryTotal'])
|
|
|
|
# Check that the test is run with a high enough memory limit in cgroups.
|
|
assert os_memory_limit >= memory_limit
|
|
|
|
# Check there's at least some free memory for page cache. If this fails, maybe server's memory
|
|
# usage bloated enough that max_server_memory_usage needs to be increased in configs/smol.xml
|
|
memory_free = min(os_memory_limit, memory_limit) - memory_used
|
|
assert memory_free > 100e6
|
|
|
|
assert metrics['PageCacheMaxBytes'] > 60e6
|
|
assert metrics['PageCacheBytes'] < 10e6
|
|
|
|
# Read with cache enabled.
|
|
node.query(
|
|
"select sum(k) from a settings use_page_cache_for_disks_without_file_cache=1;"
|
|
)
|
|
|
|
metrics = get_metrics()
|
|
logging.info(f"server metrics after first read: {metrics}")
|
|
|
|
assert metrics['PageCacheBytes'] > 50e6
|
|
|
|
# Do a query that uses lots of memory (and fails), check that the cache was shrunk to ~page_cache_min_size.
|
|
err = node.query_and_get_error(
|
|
"select groupArray(number) from numbers(10000000000)"
|
|
)
|
|
assert "MEMORY_LIMIT_EXCEEDED" in err
|
|
|
|
# (There used to be a check here that system.query_log shows high enough memory usage for the previous
|
|
# query, but it was flaky because log flush sometimes hits memory limit and fails.)
|
|
|
|
metrics = get_metrics()
|
|
logging.info(f"server metrics after second read: {metrics}")
|
|
assert metrics['PageCacheBytes'] < 50e6
|
|
finally:
|
|
node.query("drop table if exists a;" "system drop page cache;")
|