Stream car usage aggregation

This commit is contained in:
seo
2026-06-26 01:34:24 +09:00
parent ec8adbd56d
commit 2e9c4eaf4a
2 changed files with 117 additions and 88 deletions
+1
View File
@@ -117,5 +117,6 @@ Worker 교체나 예외 종료로 `processing`, `ui_timeout_pending` 상태가
- 명령별 rate limit과 감사 로그를 유지합니다.
- 통신사 기준 데이터 사용량 보정값을 주기적으로 확인합니다. 2026-06-26 01:10 T world 실측 102.4MB(104,834KB) 기준 2026-06 보정값은 통신사 누적 107,350,016 bytes, 로컬 보정 전 누적 54,104,011 bytes, 배율 1.984142입니다.
- 직전 배율 1.984648 기준 현재값은 107,377,429 bytes로 계산되어 T world보다 27,413 bytes(약 0.026MB) 높았고, 새 배율은 직전보다 0.000507 낮아졌습니다.
- 데이터 사용량 집계는 성공 응답 평균을 SQL로 먼저 구하고, 월간/일간 행은 PHP에서 한 줄씩 스트리밍 처리합니다. 기존 보정 조건은 유지하면서 `fetchAll()` 메모리 사용을 제거해 PHP-FPM `memory_limit=128M`에서도 동작하도록 합니다.
- 4.95초 timeout 직후에는 사용량 기록이 즉시 생기지 않을 수 있으며, 최대 60초 뒤 늦은 정산 행이 추가될 수 있습니다.
- `systemctl status car-tcp-worker.service``journalctl -u car-tcp-worker.service`로 worker 상태를 확인합니다.
+82 -54
View File
@@ -204,24 +204,10 @@ function monthly_data_usage(PDO $pdo): array
$monthStart = $now->modify('first day of this month')->setTime(0, 0, 0);
$nextMonthStart = $monthStart->modify('first day of next month');
$stmt = $pdo->prepare(""
. "SELECT ts, source, cmd, sent_bytes, received_bytes, total_bytes, tcp_ok, tcp_error "
. "FROM car_data_usage "
. "WHERE id='car' AND ts >= :month_start AND ts < :next_month_start "
. "ORDER BY ts ASC"
);
$stmt->execute([
':month_start' => $monthStart->format('Y-m-d H:i:s'),
':next_month_start' => $nextMonthStart->format('Y-m-d H:i:s'),
]);
$usageRows = $stmt->fetchAll();
$usage = calibrated_usage_rows($usageRows);
$todayStart = $now->setTime(0, 0, 0);
$tomorrowStart = $todayStart->modify('+1 day');
$todayUsage = calibrated_usage_rows(array_values(array_filter($usageRows, function (array $row) use ($todayStart, $tomorrowStart): bool {
$ts = strtotime((string)($row['ts'] ?? ''));
return $ts !== false && $ts >= $todayStart->getTimestamp() && $ts < $tomorrowStart->getTimestamp();
})));
$usage = calibrated_usage_range($pdo, $monthStart, $nextMonthStart);
$todayUsage = calibrated_usage_range($pdo, $todayStart, $tomorrowStart);
$sentBytes = $usage['sent_bytes'];
$receivedBytes = $usage['received_bytes'];
@@ -339,23 +325,90 @@ function floor_krw_10(float $value): int
return (int)floor(max(0.0, $value) / 10) * 10;
}
function calibrated_usage_rows(array $rows): array
function usage_success_averages(PDO $pdo, DateTimeInterface $start, DateTimeInterface $end): array
{
$successfulTotalBytes = 0;
$successfulCount = 0;
$avgByCmd = [];
$stmt = $pdo->prepare(""
. "SELECT cmd, SUM(total_bytes) AS total_bytes, COUNT(*) AS sample_count "
. "FROM car_data_usage "
. "WHERE id='car' AND ts >= :start_ts AND ts < :end_ts "
. "AND tcp_ok = 1 AND total_bytes > 0 "
. "GROUP BY cmd"
);
$stmt->execute([
':start_ts' => $start->format('Y-m-d H:i:s'),
':end_ts' => $end->format('Y-m-d H:i:s'),
]);
while ($row = $stmt->fetch()) {
$cmd = (string)($row['cmd'] ?? '');
$total = (int)($row['total_bytes'] ?? 0);
$count = (int)($row['sample_count'] ?? 0);
if ($count <= 0) {
continue;
}
$successfulTotalBytes += $total;
$successfulCount += $count;
$avgByCmd[$cmd] = (int)round($total / $count);
}
$globalAverage = $successfulCount > 0 ? (int)round($successfulTotalBytes / $successfulCount) : 0;
return [
'avg_by_cmd' => $avgByCmd,
'global_average' => $globalAverage,
'success_count' => $successfulCount,
];
}
function calibrated_usage_range(PDO $pdo, DateTimeInterface $start, DateTimeInterface $end): array
{
$averages = usage_success_averages($pdo, $start, $end);
$avgByCmd = $averages['avg_by_cmd'];
$globalAverage = $averages['global_average'];
$sentBytes = 0;
$receivedBytes = 0;
$totalBytes = 0;
$successfulTotalBytes = 0;
$successfulCount = 0;
$successByCmd = [];
$firstUsageTs = null;
$lastUsageTs = null;
foreach ($rows as $row) {
$adjustedTotalBytes = 0;
$adjustedFailureBytes = 0;
$connectedFailureCount = 0;
$connectFailureCount = 0;
$receiveGapExcludedCount = 0;
$lastSuccessfulTs = null;
$sampleCount = 0;
$stmt = null;
$previousBuffered = $pdo->getAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY);
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
try {
$stmt = $pdo->prepare(""
. "SELECT ts, cmd, sent_bytes, received_bytes, total_bytes, tcp_ok "
. "FROM car_data_usage "
. "WHERE id='car' AND ts >= :start_ts AND ts < :end_ts "
. "ORDER BY ts ASC"
);
$stmt->execute([
':start_ts' => $start->format('Y-m-d H:i:s'),
':end_ts' => $end->format('Y-m-d H:i:s'),
]);
while ($row = $stmt->fetch()) {
$sampleCount++;
$cmd = (string)($row['cmd'] ?? '');
$sent = (int)($row['sent_bytes'] ?? 0);
$received = (int)($row['received_bytes'] ?? 0);
$total = (int)($row['total_bytes'] ?? 0);
$tcpOk = (int)($row['tcp_ok'] ?? 0) === 1;
$rowTs = strtotime((string)($row['ts'] ?? ''));
$sentBytes += $sent;
$receivedBytes += $received;
@@ -368,37 +421,6 @@ function calibrated_usage_rows(array $rows): array
$lastUsageTs = (string)$row['ts'];
}
if ($tcpOk && $total > 0) {
$successfulTotalBytes += $total;
$successfulCount++;
if (!isset($successByCmd[$cmd])) {
$successByCmd[$cmd] = ['total' => 0, 'count' => 0];
}
$successByCmd[$cmd]['total'] += $total;
$successByCmd[$cmd]['count']++;
}
}
$globalAverage = $successfulCount > 0 ? (int)round($successfulTotalBytes / $successfulCount) : 0;
$avgByCmd = [];
foreach ($successByCmd as $cmd => $stat) {
$avgByCmd[$cmd] = $stat['count'] > 0 ? (int)round($stat['total'] / $stat['count']) : $globalAverage;
}
$adjustedTotalBytes = 0;
$adjustedFailureBytes = 0;
$connectedFailureCount = 0;
$connectFailureCount = 0;
$receiveGapExcludedCount = 0;
$lastSuccessfulTs = null;
foreach ($rows as $row) {
$cmd = (string)($row['cmd'] ?? '');
$sent = (int)($row['sent_bytes'] ?? 0);
$total = (int)($row['total_bytes'] ?? 0);
$tcpOk = (int)($row['tcp_ok'] ?? 0) === 1;
$rowTs = strtotime((string)($row['ts'] ?? ''));
if ($tcpOk) {
$adjustedTotalBytes += $total;
if ($rowTs !== false) {
@@ -433,6 +455,12 @@ function calibrated_usage_rows(array $rows): array
$adjustedTotalBytes += $estimatedBytes;
$adjustedFailureBytes += $estimatedBytes;
}
} finally {
if ($stmt instanceof PDOStatement) {
$stmt->closeCursor();
}
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $previousBuffered);
}
return [
'sent_bytes' => $sentBytes,
@@ -443,8 +471,8 @@ function calibrated_usage_rows(array $rows): array
'connected_failure_count' => $connectedFailureCount,
'connect_failure_count' => $connectFailureCount,
'receive_gap_excluded_count' => $receiveGapExcludedCount,
'sample_count' => count($rows),
'success_count' => $successfulCount,
'sample_count' => $sampleCount,
'success_count' => $averages['success_count'],
'success_average_bytes' => $globalAverage,
'first_usage_ts' => $firstUsageTs,
'last_usage_ts' => $lastUsageTs,