fix: 优化腾讯云账单同步限频
- 全局限制腾讯云API调用频率为每秒4次以内 - 对RequestLimitExceeded增加指数退避重试 - 补充限频回归测试并更新账单同步支持说明
This commit is contained in:
1 parent
7bc416d22a
commit
3a3f77ea62
5 files changed
+95
-7
No files matched your search
@@ -3,17 +3,39 @@ package cloud
|
|||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TencentProvider 通过腾讯云 TC3-HMAC-SHA256 API 同步 CVM 资源。 // Logan.Li
|
// TencentProvider 通过腾讯云 TC3-HMAC-SHA256 API 同步 CVM 资源。 // Logan.Li
|
||||||
type TencentProvider struct{ accessKey, secretKey string }
|
type TencentProvider struct{ accessKey, secretKey string }
|
||||||
|
|
||||||
|
const (
|
||||||
|
tencentRequestInterval = 260 * time.Millisecond
|
||||||
|
tencentMaxAttempts = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
var tencentLimiter = struct {
|
||||||
|
sync.Mutex
|
||||||
|
next time.Time
|
||||||
|
}{}
|
||||||
|
|
||||||
|
// tencentAPIError 保留腾讯云错误码,便于对限频错误做退避重试。 // Logan.Li
|
||||||
|
type tencentAPIError struct {
|
||||||
|
Code string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *tencentAPIError) Error() string {
|
||||||
|
return fmt.Sprintf("腾讯云 API 错误 %s: %s", e.Code, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
// tencentInstancesResponse 描述腾讯云 CVM 实例分页响应。 // Logan.Li
|
// tencentInstancesResponse 描述腾讯云 CVM 实例分页响应。 // Logan.Li
|
||||||
type tencentInstancesResponse struct {
|
type tencentInstancesResponse struct {
|
||||||
Response struct {
|
Response struct {
|
||||||
@@ -56,6 +78,44 @@ func (p *TencentProvider) callInRegion(region, action string, payload interface{
|
|||||||
|
|
||||||
// callService 使用 TC3-HMAC-SHA256 调用腾讯云服务 API。 // Logan.Li
|
// callService 使用 TC3-HMAC-SHA256 调用腾讯云服务 API。 // Logan.Li
|
||||||
func (p *TencentProvider) callService(service, host, version, action, region string, payload interface{}) ([]byte, error) {
|
func (p *TencentProvider) callService(service, host, version, action, region string, payload interface{}) ([]byte, error) {
|
||||||
|
var lastErr error
|
||||||
|
for attempt := 0; attempt < tencentMaxAttempts; attempt++ {
|
||||||
|
waitTencentRequestSlot()
|
||||||
|
data, err := p.callServiceOnce(service, host, version, action, region, payload)
|
||||||
|
if err == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
delay, retry := tencentRetryDelay(err, attempt)
|
||||||
|
if !retry {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
time.Sleep(delay)
|
||||||
|
}
|
||||||
|
return nil, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitTencentRequestSlot 将进程内所有腾讯云请求统一限速在每秒 4 次以内。 // Logan.Li
|
||||||
|
func waitTencentRequestSlot() {
|
||||||
|
tencentLimiter.Lock()
|
||||||
|
defer tencentLimiter.Unlock()
|
||||||
|
if delay := time.Until(tencentLimiter.next); delay > 0 {
|
||||||
|
time.Sleep(delay)
|
||||||
|
}
|
||||||
|
tencentLimiter.next = time.Now().Add(tencentRequestInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tencentRetryDelay 对 RequestLimitExceeded 使用指数退避,其他错误立即返回。 // Logan.Li
|
||||||
|
func tencentRetryDelay(err error, attempt int) (time.Duration, bool) {
|
||||||
|
var apiErr *tencentAPIError
|
||||||
|
if !errors.As(err, &apiErr) || !strings.HasPrefix(apiErr.Code, "RequestLimitExceeded") || attempt >= tencentMaxAttempts-1 {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return (500 * time.Millisecond) * time.Duration(1<<attempt), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// callServiceOnce 执行一次腾讯云 TC3-HMAC-SHA256 签名请求。 // Logan.Li
|
||||||
|
func (p *TencentProvider) callServiceOnce(service, host, version, action, region string, payload interface{}) ([]byte, error) {
|
||||||
body, err := json.Marshal(payload)
|
body, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("编码腾讯云请求失败: %w", err)
|
return nil, fmt.Errorf("编码腾讯云请求失败: %w", err)
|
||||||
@@ -93,6 +153,9 @@ func (p *TencentProvider) callService(service, host, version, action, region str
|
|||||||
return nil, fmt.Errorf("读取腾讯云响应失败: %w", err)
|
return nil, fmt.Errorf("读取腾讯云响应失败: %w", err)
|
||||||
}
|
}
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
if resp.StatusCode == http.StatusTooManyRequests {
|
||||||
|
return nil, &tencentAPIError{Code: "RequestLimitExceeded.HTTP429", Message: string(data)}
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("腾讯云 API 返回 HTTP %d: %s", resp.StatusCode, string(data))
|
return nil, fmt.Errorf("腾讯云 API 返回 HTTP %d: %s", resp.StatusCode, string(data))
|
||||||
}
|
}
|
||||||
var envelope struct {
|
var envelope struct {
|
||||||
@@ -104,7 +167,7 @@ func (p *TencentProvider) callService(service, host, version, action, region str
|
|||||||
} `json:"Response"`
|
} `json:"Response"`
|
||||||
}
|
}
|
||||||
if json.Unmarshal(data, &envelope) == nil && envelope.Response.Error != nil {
|
if json.Unmarshal(data, &envelope) == nil && envelope.Response.Error != nil {
|
||||||
return nil, fmt.Errorf("腾讯云 API 错误 %s: %s", envelope.Response.Error.Code, envelope.Response.Error.Message)
|
return nil, &tencentAPIError{Code: envelope.Response.Error.Code, Message: envelope.Response.Error.Message}
|
||||||
}
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FetchDailyBill 拉取指定日期的腾讯云日账单,并按产品和地域汇总。 // Logan.Li
|
// FetchDailyBill 拉取指定日期的腾讯云日账单,并按产品和地域汇总。 // Logan.Li
|
||||||
@@ -68,8 +67,6 @@ func (p *TencentProvider) FetchDailyBill(billingDate string) ([]CloudBillingReco
|
|||||||
if len(response.Response.DetailSet) < pageSize || (response.Response.Total > 0 && offset+pageSize >= response.Response.Total) {
|
if len(response.Response.DetailSet) < pageSize || (response.Response.Total > 0 && offset+pageSize >= response.Response.Total) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// 腾讯云账单接口频率上限为每秒 5 次,分页请求限速为每秒 4 次。 // Logan.Li
|
|
||||||
time.Sleep(250 * time.Millisecond)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
keys := make([]aggregationKey, 0, len(aggregation))
|
keys := make([]aggregationKey, 0, len(aggregation))
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package cloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestTencentRetryDelay 验证限频错误指数退避与最大重试次数。 // Logan.Li
|
||||||
|
func TestTencentRetryDelay(t *testing.T) {
|
||||||
|
rateLimitErr := &tencentAPIError{Code: "RequestLimitExceeded", Message: "frequency limit"}
|
||||||
|
delay, retry := tencentRetryDelay(rateLimitErr, 0)
|
||||||
|
if !retry || delay != 500*time.Millisecond {
|
||||||
|
t.Fatalf("首次限频退避错误: retry=%v delay=%s", retry, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
delay, retry = tencentRetryDelay(rateLimitErr, 2)
|
||||||
|
if !retry || delay != 2*time.Second {
|
||||||
|
t.Fatalf("第三次限频退避错误: retry=%v delay=%s", retry, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, retry = tencentRetryDelay(rateLimitErr, tencentMaxAttempts-1); retry {
|
||||||
|
t.Fatal("达到最大次数后不应继续重试")
|
||||||
|
}
|
||||||
|
if _, retry = tencentRetryDelay(errors.New("鉴权失败"), 0); retry {
|
||||||
|
t.Fatal("非限频错误不应重试")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ var (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Code: "cloud_billing_sync", Name: "云账单同步",
|
Code: "cloud_billing_sync", Name: "云账单同步",
|
||||||
Description: "从云账号 AK 拉取昨日账单(按产品+地域),写入云账单管理;当前支持阿里云",
|
Description: "从云账号 AK 拉取昨日账单(按产品+地域),写入云账单管理;当前支持阿里云和腾讯云", // Logan.Li
|
||||||
Schedule: "每天 10:30", Category: "finops", Runnable: true, Enabled: true,
|
Schedule: "每天 10:30", Category: "finops", Runnable: true, Enabled: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -237,4 +237,4 @@ func findDef(code string) (Def, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Def{}, false
|
return Def{}, false
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ export function CloudBilling() {
|
|||||||
if (activeTab === 'alerts') loadRules();
|
if (activeTab === 'alerts') loadRules();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 从云账号 AK 同步昨日账单(当前支持阿里云) // Logan.Li
|
// 从云账号 AK 同步昨日账单(当前支持阿里云和腾讯云) // Logan.Li
|
||||||
const handleSyncYesterday = async () => {
|
const handleSyncYesterday = async () => {
|
||||||
setSyncing(true);
|
setSyncing(true);
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in new issue
Block a user