Stream car usage aggregation
This commit is contained in:
@@ -117,5 +117,6 @@ Worker 교체나 예외 종료로 `processing`, `ui_timeout_pending` 상태가
|
|||||||
- 명령별 rate limit과 감사 로그를 유지합니다.
|
- 명령별 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입니다.
|
- 통신사 기준 데이터 사용량 보정값을 주기적으로 확인합니다. 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 낮아졌습니다.
|
- 직전 배율 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초 뒤 늦은 정산 행이 추가될 수 있습니다.
|
- 4.95초 timeout 직후에는 사용량 기록이 즉시 생기지 않을 수 있으며, 최대 60초 뒤 늦은 정산 행이 추가될 수 있습니다.
|
||||||
- `systemctl status car-tcp-worker.service`와 `journalctl -u car-tcp-worker.service`로 worker 상태를 확인합니다.
|
- `systemctl status car-tcp-worker.service`와 `journalctl -u car-tcp-worker.service`로 worker 상태를 확인합니다.
|
||||||
|
|||||||
+82
-54
@@ -204,24 +204,10 @@ function monthly_data_usage(PDO $pdo): array
|
|||||||
$monthStart = $now->modify('first day of this month')->setTime(0, 0, 0);
|
$monthStart = $now->modify('first day of this month')->setTime(0, 0, 0);
|
||||||
$nextMonthStart = $monthStart->modify('first day of next month');
|
$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);
|
$todayStart = $now->setTime(0, 0, 0);
|
||||||
$tomorrowStart = $todayStart->modify('+1 day');
|
$tomorrowStart = $todayStart->modify('+1 day');
|
||||||
$todayUsage = calibrated_usage_rows(array_values(array_filter($usageRows, function (array $row) use ($todayStart, $tomorrowStart): bool {
|
$usage = calibrated_usage_range($pdo, $monthStart, $nextMonthStart);
|
||||||
$ts = strtotime((string)($row['ts'] ?? ''));
|
$todayUsage = calibrated_usage_range($pdo, $todayStart, $tomorrowStart);
|
||||||
return $ts !== false && $ts >= $todayStart->getTimestamp() && $ts < $tomorrowStart->getTimestamp();
|
|
||||||
})));
|
|
||||||
|
|
||||||
$sentBytes = $usage['sent_bytes'];
|
$sentBytes = $usage['sent_bytes'];
|
||||||
$receivedBytes = $usage['received_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;
|
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;
|
$sentBytes = 0;
|
||||||
$receivedBytes = 0;
|
$receivedBytes = 0;
|
||||||
$totalBytes = 0;
|
$totalBytes = 0;
|
||||||
$successfulTotalBytes = 0;
|
|
||||||
$successfulCount = 0;
|
|
||||||
$successByCmd = [];
|
|
||||||
$firstUsageTs = null;
|
$firstUsageTs = null;
|
||||||
$lastUsageTs = 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'] ?? '');
|
$cmd = (string)($row['cmd'] ?? '');
|
||||||
$sent = (int)($row['sent_bytes'] ?? 0);
|
$sent = (int)($row['sent_bytes'] ?? 0);
|
||||||
$received = (int)($row['received_bytes'] ?? 0);
|
$received = (int)($row['received_bytes'] ?? 0);
|
||||||
$total = (int)($row['total_bytes'] ?? 0);
|
$total = (int)($row['total_bytes'] ?? 0);
|
||||||
$tcpOk = (int)($row['tcp_ok'] ?? 0) === 1;
|
$tcpOk = (int)($row['tcp_ok'] ?? 0) === 1;
|
||||||
|
$rowTs = strtotime((string)($row['ts'] ?? ''));
|
||||||
|
|
||||||
$sentBytes += $sent;
|
$sentBytes += $sent;
|
||||||
$receivedBytes += $received;
|
$receivedBytes += $received;
|
||||||
@@ -368,37 +421,6 @@ function calibrated_usage_rows(array $rows): array
|
|||||||
$lastUsageTs = (string)$row['ts'];
|
$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) {
|
if ($tcpOk) {
|
||||||
$adjustedTotalBytes += $total;
|
$adjustedTotalBytes += $total;
|
||||||
if ($rowTs !== false) {
|
if ($rowTs !== false) {
|
||||||
@@ -433,6 +455,12 @@ function calibrated_usage_rows(array $rows): array
|
|||||||
$adjustedTotalBytes += $estimatedBytes;
|
$adjustedTotalBytes += $estimatedBytes;
|
||||||
$adjustedFailureBytes += $estimatedBytes;
|
$adjustedFailureBytes += $estimatedBytes;
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if ($stmt instanceof PDOStatement) {
|
||||||
|
$stmt->closeCursor();
|
||||||
|
}
|
||||||
|
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $previousBuffered);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'sent_bytes' => $sentBytes,
|
'sent_bytes' => $sentBytes,
|
||||||
@@ -443,8 +471,8 @@ function calibrated_usage_rows(array $rows): array
|
|||||||
'connected_failure_count' => $connectedFailureCount,
|
'connected_failure_count' => $connectedFailureCount,
|
||||||
'connect_failure_count' => $connectFailureCount,
|
'connect_failure_count' => $connectFailureCount,
|
||||||
'receive_gap_excluded_count' => $receiveGapExcludedCount,
|
'receive_gap_excluded_count' => $receiveGapExcludedCount,
|
||||||
'sample_count' => count($rows),
|
'sample_count' => $sampleCount,
|
||||||
'success_count' => $successfulCount,
|
'success_count' => $averages['success_count'],
|
||||||
'success_average_bytes' => $globalAverage,
|
'success_average_bytes' => $globalAverage,
|
||||||
'first_usage_ts' => $firstUsageTs,
|
'first_usage_ts' => $firstUsageTs,
|
||||||
'last_usage_ts' => $lastUsageTs,
|
'last_usage_ts' => $lastUsageTs,
|
||||||
|
|||||||
Reference in New Issue
Block a user