P2.5: биллинг — воркер+буфер вместо горутины на операцию; корректный msg_count
RecordUsage теперь кладёт запись в буферизованный канал (1024), один воркер пишет в PG — убран навал горутин, ждущих коннект пула (было 5). При переполнении буфера запись дропается с логом (биллинг жертвуем ради стабильности). Для batch-операций (SendMessageBatch/DeleteMessageBatch/ChangeMessageVisibilityBatch) msg_count теперь реальное число записей, а не всегда 1.
This commit is contained in:
+44
-13
@@ -98,23 +98,54 @@ func autoMigrate() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordUsage — записывает одну SQS-операцию в таблицу биллинга.
|
// usageRecord — одна запись биллинга, накапливается в буфере воркера.
|
||||||
// Вызывается асинхронно (горутина) чтобы не добавлять latency к SQS-ответу.
|
type usageRecord struct {
|
||||||
// Если billing отключён — no-op. Ошибки логируются, SQS-операция не ломается.
|
tenantID string
|
||||||
|
operation string
|
||||||
|
queueName string
|
||||||
|
msgCount int
|
||||||
|
msgBytes int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// usageChan — канал-буфер для асинхронных записей биллинга.
|
||||||
|
// Вместо горутины на каждую операцию (навал горутин под нагрузкой) используется
|
||||||
|
// один воркер, читающий из буферизованного канала.
|
||||||
|
var usageChan = make(chan usageRecord, 1024)
|
||||||
|
|
||||||
|
// init запускает воркер-горутину биллинга. Воркер стартует всегда; если db == nil
|
||||||
|
// (billing отключён) — он просто дропает записи.
|
||||||
|
func init() {
|
||||||
|
go usageWorker()
|
||||||
|
}
|
||||||
|
|
||||||
|
// usageWorker — единственная горутина, пишущая usage-записи в PostgreSQL.
|
||||||
|
func usageWorker() {
|
||||||
|
for rec := range usageChan {
|
||||||
|
if db == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, err := db.Exec(
|
||||||
|
`INSERT INTO sqs_usage_records (tenant_id, operation, queue_name, msg_count, msg_bytes) VALUES ($1, $2, $3, $4, $5)`,
|
||||||
|
rec.tenantID, rec.operation, rec.queueName, rec.msgCount, rec.msgBytes,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("billing: failed to record usage [%s/%s]: %v", rec.tenantID, rec.operation, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordUsage — ставит запись в очередь биллинга (асинхронно, без блокировки SQS).
|
||||||
|
// Если billing отключён — no-op. Если буфер переполнен — запись дропается (биллинг
|
||||||
|
// жертвуем ради стабильности основного пути), ошибка логируется.
|
||||||
func RecordUsage(tenantID, operation, queueName string, msgCount int, msgBytes int64) {
|
func RecordUsage(tenantID, operation, queueName string, msgCount int, msgBytes int64) {
|
||||||
if db == nil {
|
if db == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
select {
|
||||||
go func() {
|
case usageChan <- usageRecord{tenantID, operation, queueName, msgCount, msgBytes}:
|
||||||
_, err := db.Exec(
|
default:
|
||||||
`INSERT INTO sqs_usage_records (tenant_id, operation, queue_name, msg_count, msg_bytes) VALUES ($1, $2, $3, $4, $5)`,
|
log.Warnf("billing: буфер полон, запись [%s/%s] дропнута", tenantID, operation)
|
||||||
tenantID, operation, queueName, msgCount, msgBytes,
|
}
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("billing: failed to record usage [%s/%s]: %v", tenantID, operation, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enabled — возвращает true если billing подключён к PostgreSQL
|
// Enabled — возвращает true если billing подключён к PostgreSQL
|
||||||
|
|||||||
+40
-2
@@ -200,6 +200,36 @@ func metricsAuth(adminToken string, next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// countBatchEntries — подсчитывает число записей в batch-запросе SQS.
|
||||||
|
// Для query-протокола записи идут как SendMessageBatchRequestEntry.N.Id (N=1..).
|
||||||
|
// Для JSON-протокола считаем по количеству записей в массиве Entries (requestBody).
|
||||||
|
// Возвращает 0 если определить не удалось (тогда биллинг запишет msg_count=1).
|
||||||
|
func countBatchEntries(req *http.Request, action string) int {
|
||||||
|
// Query-формат: считаем уникальные N в SendMessageBatchRequestEntry.N.Id и т.п.
|
||||||
|
prefix := "SendMessageBatchRequestEntry."
|
||||||
|
if action == "DeleteMessageBatch" {
|
||||||
|
prefix = "DeleteMessageBatchRequestEntry."
|
||||||
|
} else if action == "ChangeMessageVisibilityBatch" {
|
||||||
|
prefix = "ChangeMessageVisibilityBatchRequestEntry."
|
||||||
|
}
|
||||||
|
if req.PostForm != nil || req.Form != nil {
|
||||||
|
form := req.Form
|
||||||
|
if form == nil {
|
||||||
|
form = req.PostForm
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
for key := range form {
|
||||||
|
if strings.HasPrefix(key, prefix) && strings.HasSuffix(key, ".Id") {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func actionHandler(w http.ResponseWriter, req *http.Request) {
|
func actionHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
action := extractAction(req)
|
action := extractAction(req)
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
@@ -227,8 +257,16 @@ func actionHandler(w http.ResponseWriter, req *http.Request) {
|
|||||||
if queueName == "" {
|
if queueName == "" {
|
||||||
queueName = req.FormValue("QueueName")
|
queueName = req.FormValue("QueueName")
|
||||||
}
|
}
|
||||||
// Billing: запись в PostgreSQL (async, no-op если billing выключен)
|
// Billing: запись в PostgreSQL (async, no-op если billing выключен).
|
||||||
billing.RecordUsage(t.ID, action, queueName, 1, msgBytes)
|
// msgCount для batch-операций — число сообщений в батче; для прочих — 1.
|
||||||
|
msgCount := 1
|
||||||
|
if action == "SendMessageBatch" || action == "DeleteMessageBatch" ||
|
||||||
|
action == "ChangeMessageVisibilityBatch" {
|
||||||
|
if n := countBatchEntries(req, action); n > 0 {
|
||||||
|
msgCount = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
billing.RecordUsage(t.ID, action, queueName, msgCount, msgBytes)
|
||||||
// Prometheus counters: requests + bytes
|
// Prometheus counters: requests + bytes
|
||||||
metrics.RequestsTotal.WithLabelValues(t.ID, action).Inc()
|
metrics.RequestsTotal.WithLabelValues(t.ID, action).Inc()
|
||||||
metrics.RequestBytesTotal.WithLabelValues(t.ID, action).Add(float64(msgBytes))
|
metrics.RequestBytesTotal.WithLabelValues(t.ID, action).Add(float64(msgBytes))
|
||||||
|
|||||||
Reference in New Issue
Block a user