## 主要更改 ### 1. 数据库表结构增强 - **t_device_message_v2**: 新增7个关键字段 - priority_level: 优先级数值(1-5) - channels: 分发渠道JSON - require_ack: 是否需要确认 - metadata: 扩展元数据JSON - target_count: 目标用户总数 - version: 乐观锁版本号 - **t_device_message_detail_v2**: 新增8个关键字段 - distribution_id: 分发唯一ID - device_sn: 设备序列号 - user_id: 用户ID(核心查询字段) - response_duration: 响应耗时 - delivery_details: 分发详情JSON - client_info: 客户端信息JSON - location_info: 位置信息JSON - last_retry_time: 最后重试时间 ### 2. 索引优化重构 (基于user_id) - 删除原有device_sn相关索引 - 新建5个user_id高性能复合索引: - idx_customer_user_time: 客户+用户+时间 - idx_customer_user_status: 客户+用户+状态 - idx_customer_user_type: 客户+用户+类型 - idx_user_org_priority: 用户+组织+优先级 - idx_user_status_time: 用户+状态+时间 - detail表新建4个user_id相关索引: - uk_message_user_target: 唯一约束 - idx_customer_user_status: 客户+用户+状态 - idx_user_channel_time: 用户+渠道+时间 - idx_user_delivery_status: 用户+分发状态 ### 3. 数据迁移和初始化 - 自动生成distribution_id - 根据urgency设置priority_level - 初始化默认channels配置 - 保持现有数据完整性 ### 4. 性能提升预期 - 用户消息查询: 预计10-20倍性能提升 - 确认状态查询: 支持快速人员清单检索 - 存储空间: JSON字段替代冗余VARCHAR - 查询覆盖: 所有user_id查询都能命中索引 ### 5. 文档和脚本 - 完整的V2优化SQL脚本 - 消息机制设计文档 - 数据库备份(22MB) - 性能验证查询 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1050 lines
38 KiB
Markdown
1050 lines
38 KiB
Markdown
# 健康数据批处理系统聚焦优化方案
|
|
|
|
## 📋 优化范围
|
|
基于ROI分析,选择实施两个核心优化项:
|
|
1. **多队列并行处理** (预期收益2-4倍)
|
|
2. **监控和可观测性** (运维能力大幅提升)
|
|
|
|
## 🎯 优化目标
|
|
- **性能提升**: 整体处理能力提升200-400%
|
|
- **并发能力**: 支持4倍以上的并发处理
|
|
- **可观测性**: 建立完整的性能监控体系
|
|
- **稳定性**: 提供实时的系统健康监控
|
|
|
|
---
|
|
|
|
## 1. 多队列并行处理实施方案
|
|
|
|
### 1.1 设计思路
|
|
|
|
#### 核心理念
|
|
- **分片策略**: 基于设备SN进行哈希分片,确保同一设备的数据顺序性
|
|
- **并行处理**: 多个独立的批处理器并行工作,避免单点瓶颈
|
|
- **负载均衡**: 自动分配设备到不同队列,实现负载均衡
|
|
- **隔离性**: 单个队列异常不影响其他队列处理
|
|
|
|
#### 架构对比
|
|
```
|
|
# 当前架构 (单队列)
|
|
设备A,B,C,D... → 单一队列 → 单个批处理器 → 数据库
|
|
|
|
# 优化架构 (多队列)
|
|
设备A,E,I... → 队列0 → 批处理器0 ↘
|
|
设备B,F,J... → 队列1 → 批处理器1 → 数据库
|
|
设备C,G,K... → 队列2 → 批处理器2 ↗
|
|
设备D,H,L... → 队列3 → 批处理器3 ↙
|
|
```
|
|
|
|
### 1.2 实现方案
|
|
|
|
#### 1.2.1 ShardedBatchProcessor 核心类
|
|
```python
|
|
import hashlib
|
|
from typing import List, Dict, Optional
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
import threading
|
|
import queue
|
|
import time
|
|
|
|
class ShardedBatchProcessor:
|
|
"""分片批处理器 - 多队列并行处理"""
|
|
|
|
def __init__(self, shard_count: int = 4):
|
|
"""
|
|
初始化分片批处理器
|
|
|
|
Args:
|
|
shard_count: 分片数量,建议设置为CPU核心数的1/2到1倍
|
|
"""
|
|
self.shard_count = shard_count
|
|
self.running = True
|
|
|
|
# 为每个分片创建独立队列
|
|
self.shard_queues = []
|
|
for i in range(shard_count):
|
|
shard_queue = queue.Queue(maxsize=1250) # 总容量5000/4
|
|
self.shard_queues.append(shard_queue)
|
|
|
|
# 分片统计信息
|
|
self.shard_stats = [
|
|
{'processed': 0, 'errors': 0, 'duplicates': 0}
|
|
for _ in range(shard_count)
|
|
]
|
|
|
|
# 启动分片处理器
|
|
self.processor_threads = []
|
|
for shard_id in range(shard_count):
|
|
thread = threading.Thread(
|
|
target=self._shard_processor,
|
|
args=(shard_id,),
|
|
name=f"BatchProcessor-Shard-{shard_id}",
|
|
daemon=True
|
|
)
|
|
thread.start()
|
|
self.processor_threads.append(thread)
|
|
|
|
logger.info(f"🚀 ShardedBatchProcessor启动: {shard_count}个分片处理器")
|
|
|
|
def _get_shard_id(self, device_sn: str) -> int:
|
|
"""
|
|
基于设备SN计算分片ID
|
|
使用一致性哈希确保同一设备总是分配到同一分片
|
|
"""
|
|
if not device_sn:
|
|
return 0
|
|
|
|
# 使用MD5哈希确保分布均匀
|
|
hash_value = int(hashlib.md5(device_sn.encode()).hexdigest(), 16)
|
|
return hash_value % self.shard_count
|
|
|
|
def add_data(self, data_item: Dict, device_sn: str) -> bool:
|
|
"""
|
|
将数据添加到对应分片队列
|
|
|
|
Args:
|
|
data_item: 批处理数据项
|
|
device_sn: 设备序列号
|
|
|
|
Returns:
|
|
bool: 是否成功添加到队列
|
|
"""
|
|
if not self.running:
|
|
return False
|
|
|
|
shard_id = self._get_shard_id(device_sn)
|
|
|
|
try:
|
|
# 添加分片ID到数据项中,便于处理时识别
|
|
data_item['shard_id'] = shard_id
|
|
self.shard_queues[shard_id].put(data_item, timeout=1)
|
|
|
|
print(f"📦 数据已添加到分片{shard_id}: device_sn={device_sn}")
|
|
return True
|
|
|
|
except queue.Full:
|
|
logger.error(f'分片{shard_id}队列已满,数据丢弃: {device_sn}')
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f'添加数据到分片{shard_id}失败: {e}')
|
|
return False
|
|
|
|
def _shard_processor(self, shard_id: int):
|
|
"""
|
|
分片处理器主循环
|
|
每个分片独立运行批处理逻辑
|
|
"""
|
|
shard_queue = self.shard_queues[shard_id]
|
|
batch_data = []
|
|
last_flush = time.time()
|
|
batch_size = max(50, min(250, 500 // self.shard_count)) # 动态批次大小
|
|
batch_timeout = 2.0
|
|
|
|
print(f"🔄 分片{shard_id}处理器启动: batch_size={batch_size}")
|
|
|
|
while self.running:
|
|
try:
|
|
# 计算超时时间
|
|
timeout = max(0.1, batch_timeout - (time.time() - last_flush))
|
|
|
|
# 从队列获取数据
|
|
try:
|
|
item = shard_queue.get(timeout=timeout)
|
|
batch_data.append(item)
|
|
print(f"📥 分片{shard_id}收到数据: device_sn={item.get('device_sn')}")
|
|
|
|
except queue.Empty:
|
|
# 超时,继续检查是否需要flush
|
|
pass
|
|
|
|
# 检查是否需要flush批次
|
|
should_flush = (
|
|
len(batch_data) >= batch_size or
|
|
(batch_data and (time.time() - last_flush) >= batch_timeout)
|
|
)
|
|
|
|
if should_flush and batch_data:
|
|
print(f"🔄 分片{shard_id}开始flush批次: size={len(batch_data)}")
|
|
|
|
# 执行批处理
|
|
success_count = self._flush_shard_batch(shard_id, batch_data)
|
|
|
|
# 更新统计
|
|
self.shard_stats[shard_id]['processed'] += success_count
|
|
|
|
# 重置批次
|
|
batch_data = []
|
|
last_flush = time.time()
|
|
|
|
except Exception as e:
|
|
self.shard_stats[shard_id]['errors'] += 1
|
|
logger.error(f'分片{shard_id}处理异常: {e}', exc_info=True)
|
|
# 发生异常时也要重置批次,避免数据堆积
|
|
batch_data = []
|
|
last_flush = time.time()
|
|
|
|
def _flush_shard_batch(self, shard_id: int, batch_data: List[Dict]) -> int:
|
|
"""
|
|
执行分片批次数据库操作
|
|
复用现有的_flush_batch逻辑,但添加分片标识
|
|
"""
|
|
try:
|
|
print(f"💾 分片{shard_id}数据库操作开始: {len(batch_data)}条数据")
|
|
|
|
# 分离不同类型的数据
|
|
main_records = []
|
|
daily_records = []
|
|
weekly_records = []
|
|
|
|
for item in batch_data:
|
|
if 'main_data' in item:
|
|
main_records.append(item['main_data'])
|
|
if 'daily_data' in item and item['daily_data']:
|
|
daily_records.append(item['daily_data'])
|
|
if 'weekly_data' in item and item['weekly_data']:
|
|
weekly_records.append(item['weekly_data'])
|
|
|
|
# 执行数据库操作 (复用现有逻辑)
|
|
success_count = self._execute_database_operations(
|
|
shard_id, main_records, daily_records, weekly_records
|
|
)
|
|
|
|
# 异步处理Redis和告警 (使用专用线程池)
|
|
self._execute_async_operations(shard_id, batch_data)
|
|
|
|
print(f"✅ 分片{shard_id}批次处理完成: {success_count}条成功")
|
|
return success_count
|
|
|
|
except Exception as e:
|
|
self.shard_stats[shard_id]['errors'] += 1
|
|
logger.error(f'分片{shard_id}批次处理失败: {e}', exc_info=True)
|
|
return 0
|
|
|
|
def _execute_database_operations(self, shard_id: int, main_records: List,
|
|
daily_records: List, weekly_records: List) -> int:
|
|
"""
|
|
执行数据库操作 (复用现有逻辑)
|
|
可以直接调用现有的数据库插入逻辑
|
|
"""
|
|
# 这里可以直接复用现有的数据库操作逻辑
|
|
# 为了避免重复代码,建议将现有的_flush_batch中的数据库操作
|
|
# 提取为独立方法,然后在这里调用
|
|
|
|
# 临时实现:返回处理的记录数
|
|
return len(main_records)
|
|
|
|
def _execute_async_operations(self, shard_id: int, batch_data: List[Dict]):
|
|
"""
|
|
执行异步操作 (Redis更新和告警检测)
|
|
使用专用线程池避免阻塞批处理器
|
|
"""
|
|
for item in batch_data:
|
|
try:
|
|
# 提交到异步线程池
|
|
if hasattr(self, 'async_executor'):
|
|
self.async_executor.submit(self._process_async_item, shard_id, item)
|
|
else:
|
|
# 如果没有异步线程池,直接处理
|
|
self._process_async_item(shard_id, item)
|
|
|
|
except Exception as e:
|
|
logger.error(f'分片{shard_id}异步操作提交失败: {e}')
|
|
|
|
def _process_async_item(self, shard_id: int, item: Dict):
|
|
"""
|
|
处理单个数据项的异步操作
|
|
"""
|
|
try:
|
|
device_sn = item.get('device_sn', 'unknown')
|
|
|
|
# Redis操作
|
|
if 'redis_data' in item:
|
|
redis.hset_data(f"health_data:{device_sn}", mapping=item['redis_data'])
|
|
redis.publish(f"health_data_channel:{device_sn}", device_sn)
|
|
|
|
# 告警检测
|
|
if item.get('enable_alerts', True) and 'redis_data' in item:
|
|
generate_alerts(item['redis_data'], item.get('health_data_id'))
|
|
|
|
except Exception as e:
|
|
logger.error(f'分片{shard_id}异步处理失败 device_sn={device_sn}: {e}')
|
|
|
|
def get_stats(self) -> Dict:
|
|
"""获取所有分片的统计信息"""
|
|
total_stats = {'processed': 0, 'errors': 0, 'duplicates': 0}
|
|
shard_details = {}
|
|
|
|
for shard_id, stats in enumerate(self.shard_stats):
|
|
total_stats['processed'] += stats['processed']
|
|
total_stats['errors'] += stats['errors']
|
|
total_stats['duplicates'] += stats['duplicates']
|
|
|
|
shard_details[f'shard_{shard_id}'] = {
|
|
'queue_size': self.shard_queues[shard_id].qsize(),
|
|
**stats
|
|
}
|
|
|
|
return {
|
|
'total': total_stats,
|
|
'shards': shard_details,
|
|
'shard_count': self.shard_count
|
|
}
|
|
|
|
def shutdown(self):
|
|
"""优雅关闭所有分片处理器"""
|
|
print("🛑 开始关闭ShardedBatchProcessor...")
|
|
self.running = False
|
|
|
|
# 等待所有线程结束
|
|
for thread in self.processor_threads:
|
|
thread.join(timeout=5)
|
|
|
|
print("✅ ShardedBatchProcessor已关闭")
|
|
```
|
|
|
|
#### 1.2.2 集成到现有系统
|
|
```python
|
|
# 修改HealthDataOptimizer类
|
|
class HealthDataOptimizer:
|
|
def __init__(self):
|
|
# ... 现有初始化代码 ...
|
|
|
|
# 使用分片批处理器替代单一批处理器
|
|
cpu_cores = psutil.cpu_count(logical=True)
|
|
shard_count = max(2, min(8, cpu_cores // 2)) # 2-8个分片
|
|
|
|
self.sharded_processor = ShardedBatchProcessor(shard_count)
|
|
|
|
# 专用异步处理线程池
|
|
self.async_executor = ThreadPoolExecutor(
|
|
max_workers=max(4, min(16, cpu_cores)),
|
|
thread_name_prefix="async-ops"
|
|
)
|
|
self.sharded_processor.async_executor = self.async_executor
|
|
|
|
logger.info(f"🚀 多队列批处理器初始化: {shard_count}个分片")
|
|
|
|
def add_data(self, raw_data, device_sn, enable_alerts=True):
|
|
"""修改add_data方法使用分片处理器"""
|
|
print(f"🔧 优化器添加数据: device_sn={device_sn}")
|
|
|
|
try:
|
|
# ... 现有的数据处理逻辑 ...
|
|
# 构建数据项
|
|
item = {
|
|
'device_sn': device_sn,
|
|
'main_data': main_data,
|
|
'daily_data': daily_data,
|
|
'weekly_data': weekly_data,
|
|
'redis_data': redis_data,
|
|
'enable_alerts': enable_alerts,
|
|
'config_info': config_info
|
|
}
|
|
|
|
# 使用分片处理器
|
|
success = self.sharded_processor.add_data(item, device_sn)
|
|
|
|
if success:
|
|
return {'success': True, 'reason': 'queued', 'message': '数据已加入分片处理队列'}
|
|
else:
|
|
return {'success': False, 'reason': 'queue_full', 'message': '队列已满,数据被丢弃'}
|
|
|
|
except Exception as e:
|
|
logger.error(f'分片处理器添加数据失败: {e}')
|
|
return {'success': False, 'reason': 'error', 'message': f'数据处理失败: {str(e)}'}
|
|
|
|
def get_stats(self):
|
|
"""获取包含分片信息的统计数据"""
|
|
base_stats = {
|
|
'processed': self.stats['processed'],
|
|
'batches': self.stats['batches'],
|
|
'errors': self.stats['errors'],
|
|
'duplicates': self.stats['duplicates']
|
|
}
|
|
|
|
# 合并分片统计
|
|
shard_stats = self.sharded_processor.get_stats()
|
|
|
|
return {
|
|
**base_stats,
|
|
'sharded_processing': shard_stats,
|
|
'performance_window_size': len(self.performance_window)
|
|
}
|
|
```
|
|
|
|
### 1.3 实施步骤
|
|
|
|
#### 阶段1: 准备工作 (1天)
|
|
1. **代码备份**: 备份现有的健康数据处理逻辑
|
|
2. **测试环境**: 在测试环境中部署新的分片处理器
|
|
3. **配置调优**: 根据服务器配置调整分片数量
|
|
|
|
#### 阶段2: 核心实现 (3-5天)
|
|
1. **实现ShardedBatchProcessor类** (2天)
|
|
2. **集成到HealthDataOptimizer** (1天)
|
|
3. **数据库操作逻辑适配** (1-2天)
|
|
|
|
#### 阶段3: 测试验证 (2-3天)
|
|
1. **单元测试**: 验证分片逻辑正确性
|
|
2. **压力测试**: 验证并发处理能力
|
|
3. **数据一致性测试**: 确保数据不丢失不重复
|
|
|
|
#### 阶段4: 生产部署 (1天)
|
|
1. **灰度发布**: 先在部分设备上启用
|
|
2. **监控观察**: 实时监控性能指标
|
|
3. **全量切换**: 确认无问题后全量启用
|
|
|
|
---
|
|
|
|
## 2. 监控和可观测性实施方案
|
|
|
|
### 2.1 核心指标体系
|
|
|
|
#### 2.1.1 业务指标监控
|
|
```python
|
|
from dataclasses import dataclass
|
|
from typing import Dict, List, Optional
|
|
import time
|
|
import threading
|
|
import json
|
|
|
|
@dataclass
|
|
class BatchProcessingMetrics:
|
|
"""批处理性能指标"""
|
|
batch_id: str
|
|
shard_id: int
|
|
batch_size: int
|
|
processing_time: float
|
|
db_insert_time: float
|
|
redis_update_time: float
|
|
alert_check_time: float
|
|
success_count: int
|
|
duplicate_count: int
|
|
error_count: int
|
|
timestamp: float
|
|
|
|
def to_dict(self) -> Dict:
|
|
return {
|
|
'batch_id': self.batch_id,
|
|
'shard_id': self.shard_id,
|
|
'batch_size': self.batch_size,
|
|
'processing_time': self.processing_time,
|
|
'db_insert_time': self.db_insert_time,
|
|
'redis_update_time': self.redis_update_time,
|
|
'alert_check_time': self.alert_check_time,
|
|
'success_count': self.success_count,
|
|
'duplicate_count': self.duplicate_count,
|
|
'error_count': self.error_count,
|
|
'timestamp': self.timestamp,
|
|
'success_rate': self.success_count / self.batch_size if self.batch_size > 0 else 0,
|
|
'throughput': self.batch_size / self.processing_time if self.processing_time > 0 else 0
|
|
}
|
|
|
|
class MetricsCollector:
|
|
"""指标收集器"""
|
|
|
|
def __init__(self):
|
|
self.metrics_buffer = []
|
|
self.buffer_lock = threading.Lock()
|
|
self.running = True
|
|
|
|
# 启动指标输出线程
|
|
self.output_thread = threading.Thread(target=self._metrics_output_worker, daemon=True)
|
|
self.output_thread.start()
|
|
|
|
logger.info("📊 MetricsCollector已启动")
|
|
|
|
def record_batch_metrics(self, metrics: BatchProcessingMetrics):
|
|
"""记录批次处理指标"""
|
|
with self.buffer_lock:
|
|
self.metrics_buffer.append(metrics)
|
|
|
|
# 如果缓冲区太大,触发立即输出
|
|
if len(self.metrics_buffer) > 100:
|
|
self._flush_metrics()
|
|
|
|
def _metrics_output_worker(self):
|
|
"""指标输出工作线程,每30秒输出一次汇总指标"""
|
|
while self.running:
|
|
try:
|
|
time.sleep(30) # 30秒间隔
|
|
self._flush_metrics()
|
|
except Exception as e:
|
|
logger.error(f"指标输出异常: {e}")
|
|
|
|
def _flush_metrics(self):
|
|
"""输出并清空指标缓冲区"""
|
|
with self.buffer_lock:
|
|
if not self.metrics_buffer:
|
|
return
|
|
|
|
current_metrics = self.metrics_buffer.copy()
|
|
self.metrics_buffer.clear()
|
|
|
|
try:
|
|
# 计算汇总指标
|
|
summary = self._calculate_summary(current_metrics)
|
|
|
|
# 输出到日志
|
|
logger.info(f"📈 批处理性能汇总: {json.dumps(summary, ensure_ascii=False)}")
|
|
|
|
# 输出到监控系统 (如Prometheus、InfluxDB等)
|
|
self._send_to_monitoring_system(summary, current_metrics)
|
|
|
|
except Exception as e:
|
|
logger.error(f"指标汇总计算失败: {e}")
|
|
|
|
def _calculate_summary(self, metrics: List[BatchProcessingMetrics]) -> Dict:
|
|
"""计算指标汇总"""
|
|
if not metrics:
|
|
return {}
|
|
|
|
total_batches = len(metrics)
|
|
total_records = sum(m.batch_size for m in metrics)
|
|
total_success = sum(m.success_count for m in metrics)
|
|
total_errors = sum(m.error_count for m in metrics)
|
|
total_duplicates = sum(m.duplicate_count for m in metrics)
|
|
|
|
processing_times = [m.processing_time for m in metrics if m.processing_time > 0]
|
|
throughputs = [m.batch_size / m.processing_time for m in metrics if m.processing_time > 0]
|
|
|
|
# 按分片统计
|
|
shard_stats = {}
|
|
for m in metrics:
|
|
shard_id = m.shard_id
|
|
if shard_id not in shard_stats:
|
|
shard_stats[shard_id] = {'batches': 0, 'records': 0, 'success': 0}
|
|
|
|
shard_stats[shard_id]['batches'] += 1
|
|
shard_stats[shard_id]['records'] += m.batch_size
|
|
shard_stats[shard_id]['success'] += m.success_count
|
|
|
|
return {
|
|
'period_summary': {
|
|
'total_batches': total_batches,
|
|
'total_records': total_records,
|
|
'success_rate': total_success / total_records if total_records > 0 else 0,
|
|
'error_rate': total_errors / total_records if total_records > 0 else 0,
|
|
'duplicate_rate': total_duplicates / total_records if total_records > 0 else 0
|
|
},
|
|
'performance_summary': {
|
|
'avg_processing_time': sum(processing_times) / len(processing_times) if processing_times else 0,
|
|
'max_processing_time': max(processing_times) if processing_times else 0,
|
|
'min_processing_time': min(processing_times) if processing_times else 0,
|
|
'avg_throughput': sum(throughputs) / len(throughputs) if throughputs else 0,
|
|
'max_throughput': max(throughputs) if throughputs else 0
|
|
},
|
|
'shard_distribution': shard_stats,
|
|
'timestamp': time.time()
|
|
}
|
|
|
|
def _send_to_monitoring_system(self, summary: Dict, raw_metrics: List[BatchProcessingMetrics]):
|
|
"""发送指标到监控系统"""
|
|
try:
|
|
# 这里可以集成Prometheus、InfluxDB、CloudWatch等监控系统
|
|
# 示例:发送到自定义监控端点
|
|
|
|
monitoring_data = {
|
|
'service': 'ljwx-bigscreen-batch-processor',
|
|
'summary': summary,
|
|
'detailed_metrics': [m.to_dict() for m in raw_metrics[-10:]] # 只发送最近10条详细数据
|
|
}
|
|
|
|
# 可以通过HTTP POST发送到监控系统
|
|
# requests.post('http://monitoring-system/metrics', json=monitoring_data)
|
|
|
|
# 或者写入文件供外部监控系统采集
|
|
with open('/tmp/batch_processor_metrics.json', 'w') as f:
|
|
json.dump(monitoring_data, f, ensure_ascii=False, indent=2)
|
|
|
|
except Exception as e:
|
|
logger.error(f"发送监控数据失败: {e}")
|
|
```
|
|
|
|
#### 2.1.2 系统资源监控
|
|
```python
|
|
import psutil
|
|
from typing import Dict
|
|
|
|
class SystemResourceMonitor:
|
|
"""系统资源监控器"""
|
|
|
|
def __init__(self, optimizer_instance):
|
|
self.optimizer = optimizer_instance
|
|
self.process = psutil.Process()
|
|
|
|
def get_resource_metrics(self) -> Dict:
|
|
"""获取系统资源指标"""
|
|
try:
|
|
# 进程级指标
|
|
memory_info = self.process.memory_info()
|
|
cpu_percent = self.process.cpu_percent()
|
|
|
|
# 系统级指标
|
|
system_memory = psutil.virtual_memory()
|
|
system_cpu = psutil.cpu_percent()
|
|
|
|
# 批处理器相关指标
|
|
batch_stats = self.optimizer.get_stats()
|
|
|
|
return {
|
|
'process_metrics': {
|
|
'memory_usage_mb': memory_info.rss / 1024 / 1024,
|
|
'memory_percent': self.process.memory_percent(),
|
|
'cpu_percent': cpu_percent,
|
|
'threads_count': self.process.num_threads(),
|
|
'open_files': len(self.process.open_files())
|
|
},
|
|
'system_metrics': {
|
|
'total_memory_mb': system_memory.total / 1024 / 1024,
|
|
'available_memory_mb': system_memory.available / 1024 / 1024,
|
|
'memory_percent': system_memory.percent,
|
|
'cpu_percent': system_cpu,
|
|
'cpu_count': psutil.cpu_count()
|
|
},
|
|
'batch_processor_metrics': {
|
|
'total_queue_size': sum(q.qsize() for q in self.optimizer.sharded_processor.shard_queues),
|
|
'shard_count': self.optimizer.sharded_processor.shard_count,
|
|
'processed_total': batch_stats.get('sharded_processing', {}).get('total', {}).get('processed', 0),
|
|
'error_total': batch_stats.get('sharded_processing', {}).get('total', {}).get('errors', 0)
|
|
},
|
|
'timestamp': time.time()
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"获取资源指标失败: {e}")
|
|
return {'error': str(e), 'timestamp': time.time()}
|
|
```
|
|
|
|
### 2.2 健康检查API
|
|
|
|
#### 2.2.1 批处理器健康检查端点
|
|
```python
|
|
from flask import jsonify
|
|
|
|
@app.route('/api/batch_processor/health', methods=['GET'])
|
|
def batch_processor_health():
|
|
"""批处理器健康检查端点"""
|
|
try:
|
|
# 获取批处理器统计信息
|
|
batch_stats = optimizer.get_stats()
|
|
|
|
# 获取系统资源信息
|
|
resource_monitor = SystemResourceMonitor(optimizer)
|
|
resource_metrics = resource_monitor.get_resource_metrics()
|
|
|
|
# 计算健康状态
|
|
health_status = _determine_health_status(batch_stats, resource_metrics)
|
|
|
|
response_data = {
|
|
'status': health_status['status'],
|
|
'timestamp': time.time(),
|
|
'checks': {
|
|
'batch_processor': {
|
|
'status': health_status['batch_processor_status'],
|
|
'details': batch_stats
|
|
},
|
|
'system_resources': {
|
|
'status': health_status['resource_status'],
|
|
'details': resource_metrics
|
|
},
|
|
'queue_health': {
|
|
'status': health_status['queue_status'],
|
|
'details': _get_queue_health_details(batch_stats)
|
|
}
|
|
},
|
|
'summary': {
|
|
'total_processed': batch_stats.get('sharded_processing', {}).get('total', {}).get('processed', 0),
|
|
'error_rate': _calculate_error_rate(batch_stats),
|
|
'queue_utilization': _calculate_queue_utilization(batch_stats),
|
|
'memory_usage_percent': resource_metrics.get('process_metrics', {}).get('memory_percent', 0)
|
|
}
|
|
}
|
|
|
|
status_code = 200 if health_status['status'] == 'healthy' else 503
|
|
return jsonify(response_data), status_code
|
|
|
|
except Exception as e:
|
|
logger.error(f"健康检查失败: {e}")
|
|
return jsonify({
|
|
'status': 'error',
|
|
'timestamp': time.time(),
|
|
'error': str(e)
|
|
}), 500
|
|
|
|
def _determine_health_status(batch_stats: Dict, resource_metrics: Dict) -> Dict:
|
|
"""确定系统健康状态"""
|
|
|
|
# 错误率检查
|
|
error_rate = _calculate_error_rate(batch_stats)
|
|
batch_processor_status = 'healthy' if error_rate < 0.05 else 'degraded' if error_rate < 0.1 else 'unhealthy'
|
|
|
|
# 资源使用检查
|
|
memory_percent = resource_metrics.get('process_metrics', {}).get('memory_percent', 0)
|
|
cpu_percent = resource_metrics.get('process_metrics', {}).get('cpu_percent', 0)
|
|
resource_status = 'healthy' if memory_percent < 80 and cpu_percent < 80 else 'degraded' if memory_percent < 90 and cpu_percent < 90 else 'unhealthy'
|
|
|
|
# 队列状态检查
|
|
queue_utilization = _calculate_queue_utilization(batch_stats)
|
|
queue_status = 'healthy' if queue_utilization < 0.7 else 'degraded' if queue_utilization < 0.9 else 'unhealthy'
|
|
|
|
# 综合健康状态
|
|
statuses = [batch_processor_status, resource_status, queue_status]
|
|
if 'unhealthy' in statuses:
|
|
overall_status = 'unhealthy'
|
|
elif 'degraded' in statuses:
|
|
overall_status = 'degraded'
|
|
else:
|
|
overall_status = 'healthy'
|
|
|
|
return {
|
|
'status': overall_status,
|
|
'batch_processor_status': batch_processor_status,
|
|
'resource_status': resource_status,
|
|
'queue_status': queue_status
|
|
}
|
|
|
|
def _calculate_error_rate(batch_stats: Dict) -> float:
|
|
"""计算错误率"""
|
|
total_stats = batch_stats.get('sharded_processing', {}).get('total', {})
|
|
processed = total_stats.get('processed', 0)
|
|
errors = total_stats.get('errors', 0)
|
|
|
|
if processed == 0:
|
|
return 0.0
|
|
|
|
return errors / (processed + errors)
|
|
|
|
def _calculate_queue_utilization(batch_stats: Dict) -> float:
|
|
"""计算队列使用率"""
|
|
shard_details = batch_stats.get('sharded_processing', {}).get('shards', {})
|
|
|
|
total_capacity = 0
|
|
total_used = 0
|
|
|
|
for shard_id, shard_data in shard_details.items():
|
|
total_capacity += 1250 # 每个分片队列容量
|
|
total_used += shard_data.get('queue_size', 0)
|
|
|
|
if total_capacity == 0:
|
|
return 0.0
|
|
|
|
return total_used / total_capacity
|
|
|
|
def _get_queue_health_details(batch_stats: Dict) -> Dict:
|
|
"""获取队列健康详情"""
|
|
shard_details = batch_stats.get('sharded_processing', {}).get('shards', {})
|
|
|
|
queue_details = {}
|
|
for shard_id, shard_data in shard_details.items():
|
|
queue_size = shard_data.get('queue_size', 0)
|
|
processed = shard_data.get('processed', 0)
|
|
errors = shard_data.get('errors', 0)
|
|
|
|
queue_details[shard_id] = {
|
|
'queue_size': queue_size,
|
|
'utilization': queue_size / 1250,
|
|
'processed': processed,
|
|
'errors': errors,
|
|
'error_rate': errors / max(processed + errors, 1)
|
|
}
|
|
|
|
return queue_details
|
|
```
|
|
|
|
#### 2.2.2 详细指标查询端点
|
|
```python
|
|
@app.route('/api/batch_processor/metrics', methods=['GET'])
|
|
def batch_processor_metrics():
|
|
"""获取详细的批处理器指标"""
|
|
try:
|
|
# 获取查询参数
|
|
include_details = request.args.get('details', 'false').lower() == 'true'
|
|
shard_id = request.args.get('shard_id', type=int)
|
|
|
|
# 获取统计信息
|
|
batch_stats = optimizer.get_stats()
|
|
resource_metrics = SystemResourceMonitor(optimizer).get_resource_metrics()
|
|
|
|
response_data = {
|
|
'timestamp': time.time(),
|
|
'batch_processing': batch_stats,
|
|
'system_resources': resource_metrics,
|
|
'performance_indicators': {
|
|
'error_rate': _calculate_error_rate(batch_stats),
|
|
'queue_utilization': _calculate_queue_utilization(batch_stats),
|
|
'processing_rate': _calculate_processing_rate(batch_stats)
|
|
}
|
|
}
|
|
|
|
# 如果请求特定分片的详细信息
|
|
if shard_id is not None:
|
|
shard_details = batch_stats.get('sharded_processing', {}).get('shards', {})
|
|
if f'shard_{shard_id}' in shard_details:
|
|
response_data['shard_detail'] = shard_details[f'shard_{shard_id}']
|
|
else:
|
|
return jsonify({'error': f'Shard {shard_id} not found'}), 404
|
|
|
|
# 如果不需要详细信息,简化响应
|
|
if not include_details:
|
|
response_data = {
|
|
'timestamp': response_data['timestamp'],
|
|
'summary': {
|
|
'total_processed': batch_stats.get('sharded_processing', {}).get('total', {}).get('processed', 0),
|
|
'total_errors': batch_stats.get('sharded_processing', {}).get('total', {}).get('errors', 0),
|
|
'shard_count': batch_stats.get('sharded_processing', {}).get('shard_count', 0),
|
|
**response_data['performance_indicators']
|
|
}
|
|
}
|
|
|
|
return jsonify(response_data)
|
|
|
|
except Exception as e:
|
|
logger.error(f"获取指标失败: {e}")
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
def _calculate_processing_rate(batch_stats: Dict) -> float:
|
|
"""计算处理速率(记录数/秒)"""
|
|
# 这里需要基于时间窗口计算,简化实现返回0
|
|
# 实际实现中可以基于performance_window计算
|
|
return 0.0
|
|
```
|
|
|
|
### 2.3 告警规则配置
|
|
|
|
#### 2.3.1 基于阈值的告警检查
|
|
```python
|
|
class AlertRuleEngine:
|
|
"""告警规则引擎"""
|
|
|
|
def __init__(self):
|
|
self.alert_rules = [
|
|
{
|
|
'name': 'high_error_rate',
|
|
'condition': lambda stats: _calculate_error_rate(stats) > 0.1,
|
|
'message': '批处理错误率超过10%',
|
|
'severity': 'critical'
|
|
},
|
|
{
|
|
'name': 'queue_overflow_risk',
|
|
'condition': lambda stats: _calculate_queue_utilization(stats) > 0.8,
|
|
'message': '队列使用率超过80%,存在溢出风险',
|
|
'severity': 'warning'
|
|
},
|
|
{
|
|
'name': 'high_memory_usage',
|
|
'condition': lambda stats: self._check_memory_usage() > 80,
|
|
'message': '内存使用率超过80%',
|
|
'severity': 'warning'
|
|
},
|
|
{
|
|
'name': 'processing_lag',
|
|
'condition': lambda stats: self._check_processing_lag(stats) > 300,
|
|
'message': '批处理延迟超过5分钟',
|
|
'severity': 'critical'
|
|
}
|
|
]
|
|
|
|
self.alert_history = []
|
|
self.last_check_time = time.time()
|
|
|
|
def check_alerts(self, batch_stats: Dict) -> List[Dict]:
|
|
"""检查告警条件"""
|
|
current_alerts = []
|
|
current_time = time.time()
|
|
|
|
for rule in self.alert_rules:
|
|
try:
|
|
if rule['condition'](batch_stats):
|
|
alert = {
|
|
'rule_name': rule['name'],
|
|
'message': rule['message'],
|
|
'severity': rule['severity'],
|
|
'timestamp': current_time,
|
|
'stats_snapshot': batch_stats
|
|
}
|
|
current_alerts.append(alert)
|
|
|
|
# 记录告警历史
|
|
self.alert_history.append(alert)
|
|
|
|
# 发送告警通知
|
|
self._send_alert_notification(alert)
|
|
|
|
except Exception as e:
|
|
logger.error(f"告警规则检查失败 {rule['name']}: {e}")
|
|
|
|
# 清理过期告警历史
|
|
cutoff_time = current_time - 3600 # 保留1小时历史
|
|
self.alert_history = [a for a in self.alert_history if a['timestamp'] > cutoff_time]
|
|
|
|
return current_alerts
|
|
|
|
def _check_memory_usage(self) -> float:
|
|
"""检查内存使用率"""
|
|
try:
|
|
process = psutil.Process()
|
|
return process.memory_percent()
|
|
except:
|
|
return 0.0
|
|
|
|
def _check_processing_lag(self, stats: Dict) -> float:
|
|
"""检查处理延迟(秒)"""
|
|
# 基于队列大小和处理速率估算延迟
|
|
# 这是简化实现,实际可以基于时间戳计算
|
|
shard_details = stats.get('sharded_processing', {}).get('shards', {})
|
|
total_queue_size = sum(s.get('queue_size', 0) for s in shard_details.values())
|
|
|
|
# 假设平均处理速率为100记录/秒
|
|
estimated_processing_rate = 100
|
|
if estimated_processing_rate > 0:
|
|
return total_queue_size / estimated_processing_rate
|
|
else:
|
|
return 0.0
|
|
|
|
def _send_alert_notification(self, alert: Dict):
|
|
"""发送告警通知"""
|
|
try:
|
|
# 可以发送到不同的通知渠道
|
|
alert_message = f"[{alert['severity'].upper()}] {alert['message']}"
|
|
|
|
# 记录到日志
|
|
if alert['severity'] == 'critical':
|
|
logger.critical(f"🚨 批处理器告警: {alert_message}")
|
|
else:
|
|
logger.warning(f"⚠️ 批处理器告警: {alert_message}")
|
|
|
|
# 可以集成更多通知方式:
|
|
# - 发送邮件
|
|
# - 发送微信消息
|
|
# - 发送到Slack
|
|
# - 调用告警系统API
|
|
|
|
except Exception as e:
|
|
logger.error(f"发送告警通知失败: {e}")
|
|
```
|
|
|
|
#### 2.3.2 定期告警检查任务
|
|
```python
|
|
def start_alert_monitoring():
|
|
"""启动告警监控任务"""
|
|
alert_engine = AlertRuleEngine()
|
|
|
|
def alert_check_worker():
|
|
while True:
|
|
try:
|
|
# 每60秒检查一次
|
|
time.sleep(60)
|
|
|
|
# 获取当前统计信息
|
|
batch_stats = optimizer.get_stats()
|
|
|
|
# 检查告警条件
|
|
current_alerts = alert_engine.check_alerts(batch_stats)
|
|
|
|
if current_alerts:
|
|
logger.warning(f"检测到 {len(current_alerts)} 个告警")
|
|
|
|
except Exception as e:
|
|
logger.error(f"告警监控任务异常: {e}")
|
|
|
|
# 启动告警监控线程
|
|
alert_thread = threading.Thread(target=alert_check_worker, daemon=True)
|
|
alert_thread.start()
|
|
|
|
logger.info("🚨 告警监控任务已启动")
|
|
```
|
|
|
|
### 2.4 实施步骤
|
|
|
|
#### 阶段1: 基础指标收集 (2天)
|
|
1. **实现MetricsCollector类** (1天)
|
|
2. **集成到现有批处理器** (0.5天)
|
|
3. **测试指标收集功能** (0.5天)
|
|
|
|
#### 阶段2: 健康检查API (1天)
|
|
1. **实现健康检查端点** (0.5天)
|
|
2. **实现详细指标查询端点** (0.5天)
|
|
|
|
#### 阶段3: 告警系统 (2天)
|
|
1. **实现告警规则引擎** (1天)
|
|
2. **实现告警通知机制** (0.5天)
|
|
3. **测试告警功能** (0.5天)
|
|
|
|
#### 阶段4: 监控面板集成 (1天)
|
|
1. **配置日志输出格式** (0.5天)
|
|
2. **文档编写和部署指南** (0.5天)
|
|
|
|
---
|
|
|
|
## 3. 预期收益和风险评估
|
|
|
|
### 3.1 预期收益
|
|
|
|
#### 3.1.1 性能收益
|
|
- **并发处理能力**: 提升2-4倍 (4个分片并行处理)
|
|
- **响应延迟**: 降低50-70% (消除单队列瓶颈)
|
|
- **系统吞吐量**: 整体TPS提升200-400%
|
|
- **资源利用率**: CPU和内存利用率更均衡
|
|
|
|
#### 3.1.2 运维收益
|
|
- **可观测性**: 100%的关键指标可视化
|
|
- **故障诊断**: 问题定位时间缩短80%
|
|
- **预防性维护**: 通过告警提前发现问题
|
|
- **系统稳定性**: 运行状态实时监控
|
|
|
|
### 3.2 风险评估和应对
|
|
|
|
#### 3.2.1 技术风险
|
|
| 风险 | 概率 | 影响 | 应对措施 |
|
|
|------|------|------|----------|
|
|
| 分片逻辑错误导致数据丢失 | 低 | 高 | 充分测试,灰度发布 |
|
|
| 多线程并发问题 | 中 | 中 | 加强线程安全检查 |
|
|
| 监控数据过多影响性能 | 低 | 低 | 限制监控频率和数据量 |
|
|
|
|
#### 3.2.2 业务风险
|
|
| 风险 | 概率 | 影响 | 应对措施 |
|
|
|------|------|------|----------|
|
|
| 短期性能下降 | 中 | 中 | 保留回滚方案 |
|
|
| 数据处理延迟增加 | 低 | 中 | 优化分片算法 |
|
|
| 现有业务逻辑兼容性 | 低 | 高 | 详细测试验证 |
|
|
|
|
### 3.3 成功指标
|
|
|
|
#### 3.3.1 性能指标
|
|
- 批处理QPS: 从20提升到50+ batches/s
|
|
- 数据处理TPS: 从800提升到2000+ records/s
|
|
- P95延迟: 从5-8秒降低到2-3秒
|
|
- 成功率: 维持在99.9%以上
|
|
|
|
#### 3.3.2 监控指标
|
|
- 系统健康状态实时可见
|
|
- 关键异常告警响应时间<5分钟
|
|
- 性能指标数据完整性>95%
|
|
- 故障诊断时间缩短到原来的20%
|
|
|
|
---
|
|
|
|
## 4. 实施时间表
|
|
|
|
### 总体时间安排: 2周
|
|
|
|
#### 第1周: 多队列并行处理
|
|
- **周一-周二**: ShardedBatchProcessor实现和集成
|
|
- **周三-周四**: 测试和调优
|
|
- **周五**: 生产环境部署和验证
|
|
|
|
#### 第2周: 监控和可观测性
|
|
- **周一-周二**: 指标收集和健康检查API
|
|
- **周三-周四**: 告警系统和监控面板
|
|
- **周五**: 完整测试和文档完善
|
|
|
|
## 总结
|
|
|
|
这个聚焦优化方案通过实施**多队列并行处理**和**监控可观测性**,可以以最小的风险获得最大的收益:
|
|
|
|
1. **性能提升显著**: 2-4倍的并发处理能力提升
|
|
2. **实施风险可控**: 核心业务逻辑不变,主要是架构优化
|
|
3. **运维能力增强**: 完善的监控体系大幅提升系统可维护性
|
|
4. **投入产出比高**: 2周的开发时间获得长期的性能和稳定性收益
|
|
|
|
建议按照时间表执行,重点关注测试验证环节,确保生产环境的平滑升级。 |