日增1000万数据QPS1000+TP99小于100毫秒-海量数据查询架构设计(字节面试)
2025/12/30大约 7 分钟面试题面试题
海量数据查询架构设计:日千万级数据的月度查询解决方案
场景分析 (Situation)
业务背景
面对日产生1000万条数据,需要支持过去一个月时间范围内的数据查询场景,这是一个典型的大数据OLAP(在线分析处理)问题。一个月的数据量约为3亿条记录,数据规模庞大,对查询性能、存储成本、系统可用性都提出了极高要求。

技术挑战
- 存储挑战:3亿条/月的数据存储与索引设计
- 查询性能:复杂查询的毫秒级响应要求
- 并发压力:支持高并发的查询请求
- 成本控制:存储和计算资源的成本优化
- 数据一致性:实时写入与查询的一致性保证

挑战与约束 (Challenge)
性能约束
- QPS要求:支持1000+ QPS的查询并发
- 响应时间:P99 < 100ms,P95 < 50ms
- 数据时效性:准实时查询,延迟 < 5分钟
资源约束
- 存储成本:控制在合理范围内
- 计算资源:支持弹性扩缩容
- 网络带宽:优化数据传输效率
业务约束
- 查询模式:支持多维度组合查询
- 数据保留:热数据30天,温数据90天,冷数据1年
- 可用性要求:99.9%的服务可用性
解决方案 (Response)
整体架构设计

核心技术栈选型
1. 数据存储引擎:ClickHouse
选择理由:
- 列式存储,压缩比高达10:1
- 原生支持分区和分片
- 优秀的聚合查询性能
- 支持准实时数据写入
配置方案:
-- 建表语句示例
CREATE TABLE user_behavior_local ON CLUSTER '{cluster}'
(
event_time DateTime64(3),
user_id UInt64,
event_type LowCardinality(String),
page_id UInt32,
session_id String,
device_type LowCardinality(String),
channel LowCardinality(String),
province LowCardinality(String),
city LowCardinality(String),
metrics_value Float64,
create_date Date DEFAULT toDate(event_time)
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/user_behavior_local', '{replica}')
PARTITION BY toYYYYMM(event_time) -- 按月分区
ORDER BY (create_date, user_id, event_time)
SETTINGS index_granularity = 8192;
-- 分布式表
CREATE TABLE user_behavior_dist ON CLUSTER '{cluster}' AS user_behavior_local
ENGINE = Distributed('{cluster}', default, user_behavior_local, rand());2. 实时计算引擎:Apache Flink
架构设计:
@Component
public class DataStreamProcessor {
public DataStream<UserBehavior> buildProcessingPipeline(
StreamExecutionEnvironment env) {
return env.addSource(new FlinkKafkaConsumer<>(
"user_behavior_topic",
new UserBehaviorDeserializationSchema(),
kafkaProps))
.name("kafka-source")
.uid("kafka-source-uid")
.assignTimestampsAndWatermarks(
WatermarkStrategy.<UserBehavior>forBoundedOutOfOrderness(
Duration.ofSeconds(30))
.withTimestampAssigner((event, timestamp) ->
event.getEventTime()))
.keyBy(UserBehavior::getUserId)
.window(TumblingEventTimeWindows.of(Duration.ofMinutes(1)))
.aggregate(new UserBehaviorAggregator())
.name("windowed-aggregation")
.uid("windowed-aggregation-uid");
}
}3. 缓存策略:多层缓存架构
@Service
public class QueryCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private CaffeineCache localCache;
// L1缓存:本地缓存(热点查询)
@Cacheable(value = "hotQueries", unless = "#result == null")
public QueryResult getFromLocalCache(QueryRequest request) {
return executeQuery(request);
}
// L2缓存:分布式缓存(常用查询)
public QueryResult getFromDistributedCache(QueryRequest request) {
String cacheKey = generateCacheKey(request);
QueryResult cached = (QueryResult) redisTemplate
.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
}
QueryResult result = executeQuery(request);
if (result != null) {
// 根据数据时效性设置不同TTL
int ttl = calculateTTL(request.getTimeRange());
redisTemplate.opsForValue()
.set(cacheKey, result, ttl, TimeUnit.SECONDS);
}
return result;
}
private int calculateTTL(TimeRange timeRange) {
// 越新的数据TTL越短,避免缓存不一致
long hoursAgo = Duration.between(timeRange.getEndTime(),
Instant.now()).toHours();
if (hoursAgo < 2) return 60; // 2小时内数据:1分钟TTL
else if (hoursAgo < 24) return 300; // 1天内数据:5分钟TTL
else return 1800; // 历史数据:30分钟TTL
}
}分区与分片策略
1. 时间分区策略
-- 按月分区,便于历史数据管理
ALTER TABLE user_behavior_local
DROP PARTITION '202409'; -- 删除过期分区2. 分片策略
@Configuration
public class ShardingConfiguration {
// 基于用户ID哈希分片
public String calculateShard(Long userId) {
return "shard_" + (userId.hashCode() % SHARD_COUNT);
}
// 时间范围查询的分片路由
public List<String> getShardsByTimeRange(TimeRange timeRange) {
// 根据查询时间范围确定涉及的分片
return Arrays.asList("shard_0", "shard_1", "shard_2");
}
}查询优化策略
1. 索引设计
-- 主索引:支持时间范围查询
ORDER BY (create_date, user_id, event_time)
-- 跳数索引:优化枚举类型查询
ALTER TABLE user_behavior_local
ADD INDEX idx_event_type event_type TYPE set(100) GRANULARITY 1;
-- 布隆过滤器:优化存在性查询
ALTER TABLE user_behavior_local
ADD INDEX idx_session_id session_id TYPE bloom_filter(0.01) GRANULARITY 1;2. SQL查询优化
@Service
public class OptimizedQueryService {
public QueryResult executeOptimizedQuery(QueryRequest request) {
StringBuilder sql = new StringBuilder();
sql.append("SELECT ");
// 根据查询类型选择合适的聚合函数
if (request.needsApproximation() && request.isLargeDataSet()) {
sql.append("uniqHLL12(user_id) as unique_users, "); // 近似去重
} else {
sql.append("uniq(user_id) as unique_users, ");
}
sql.append("count(*) as total_events ")
.append("FROM user_behavior_dist ")
.append("WHERE create_date >= ? AND create_date <= ? ");
// 利用分区剪枝
if (request.hasSpecificEventTypes()) {
sql.append("AND event_type IN (");
sql.append(String.join(",",
request.getEventTypes().stream()
.map(type -> "'" + type + "'")
.collect(Collectors.toList())));
sql.append(") ");
}
// 添加采样以提高查询速度
if (request.isApproximationAcceptable()) {
sql.append("SAMPLE 0.1 "); // 10%采样
}
sql.append("GROUP BY event_type ")
.append("ORDER BY total_events DESC ");
return executeQuery(sql.toString(), request.getParameters());
}
}数据生命周期管理
1. 冷热数据分层存储
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public class DataLifecycleManager {
public void archiveOldData() {
// 热数据 (0-30天): 保留在ClickHouse
// 温数据 (30-90天): 转移至对象存储
String archiveDate = LocalDate.now().minusDays(30)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
// 导出到对象存储
String exportSql = String.format(
"INSERT INTO FUNCTION s3('https://bucket.s3.region.amazonaws.com/archive/%s.parquet', " +
"'parquet') SELECT * FROM user_behavior_dist WHERE create_date = '%s'",
archiveDate, archiveDate
);
clickHouseClient.execute(exportSql);
// 删除本地分区
String dropSql = String.format(
"ALTER TABLE user_behavior_local DROP PARTITION '%s'",
archiveDate.replace("-", "")
);
clickHouseClient.execute(dropSql);
}
}2. 自动化运维
# Kubernetes CronJob 配置
apiVersion: batch/v1
kind: CronJob
metadata:
name: clickhouse-maintenance
spec:
schedule: "0 1 * * *" # 每天凌晨1点
jobTemplate:
spec:
template:
spec:
containers:
- name: maintenance
image: clickhouse-maintenance:latest
command:
- /bin/sh
- -c
- |
# 优化表结构
clickhouse-client --query "OPTIMIZE TABLE user_behavior_local FINAL"
# 更新统计信息
clickhouse-client --query "ANALYZE TABLE user_behavior_local"监控与告警体系
1. 关键指标监控
@Component
public class QueryMetricsCollector {
private final MeterRegistry meterRegistry;
private final Timer queryTimer;
private final Counter queryCounter;
@PostConstruct
public void initMetrics() {
queryTimer = Timer.builder("query.execution.time")
.tag("service", "data-query")
.register(meterRegistry);
queryCounter = Counter.builder("query.total.count")
.tag("service", "data-query")
.register(meterRegistry);
}
@EventListener
public void handleQueryEvent(QueryExecutedEvent event) {
queryTimer.record(event.getExecutionTime(), TimeUnit.MILLISECONDS);
queryCounter.increment(
Tags.of(
"status", event.getStatus(),
"query_type", event.getQueryType()
)
);
// 慢查询告警
if (event.getExecutionTime() > SLOW_QUERY_THRESHOLD) {
alertService.sendSlowQueryAlert(event);
}
}
}2. 自动扩缩容策略
# HPA配置
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: query-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: query-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
- type: Pods
pods:
metric:
name: queries_per_second
target:
type: AverageValue
averageValue: "100"最佳实践与优化建议
1. 查询语句优化
- 使用列式存储优势,只查询必要字段
- 合理使用采样查询处理大数据集
- 利用物化视图预计算常用指标
- 避免SELECT * 和不必要的ORDER BY
2. 架构优化
- 实现读写分离,写入走实时链路,查询走OLAP引擎
- 采用异步处理,避免阻塞主链路
- 实现熔断降级,保证服务稳定性
- 使用连接池复用数据库连接
3. 成本优化
- 根据查询模式设计合理的数据生命周期
- 使用压缩算法减少存储成本
- 实现智能缓存,提高缓存命中率
- 采用弹性计算资源,按需扩缩容
总结
本方案基于ClickHouse + Flink + Redis的技术栈,通过分层存储、多级缓存、智能分区等策略,有效解决了日千万级数据的月度查询挑战。该方案具备以下优势:
- 高性能:通过列式存储和索引优化,实现毫秒级查询响应
- 高可用:采用集群部署和多副本策略,保证99.9%可用性
- 可扩展:支持水平扩展,可应对数据量增长
- 成本可控:通过数据分层和资源弹性,优化成本结构
该架构方案已在多个大厂生产环境中得到验证,能够有效支撑大规模数据查询业务需求。