File size: 8,282 Bytes
3a08f21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# Worker Pool 饱和问题修复方案

## 问题背景

在 HuggingFace Space 部署的 stock_data_api_service 中,以下端点频繁出现 timeout / cooldown / worker_pool_saturated 错误:

- `stock_search`(`/api/v1/search`)
- `boards_flow``/api/v1/boards/flow``/boards/concepts/flow``/boards/industries/flow`- `sector_concept_hot``/api/v1/boards/concepts/flow`### 根本原因

1. **HuggingFace Space 的 Worker 限制**
   - Free tier:1-2 个并发 worker
   - Pro tier:10+ 个并发 worker
   - 每个慢请求(15-60s)会占用一个 worker,导致 worker pool 饱和

2. **无全局并发控制**
   - `source_runner.py` 中的 `call_with_timeout` 每次都创建新的 `ThreadPoolExecutor`
   - 没有全局的并发限制和请求排队机制
   - 多个慢请求同时到达,立即超出 HF 限制

3. **网络请求慢**
   - eastmoney/AKShare 接口响应时间不稳定(15-60s+)
   - 当前 `SOURCE_TIMEOUT_SECONDS = 15` 太短,但增加超时会占用更多 worker

## 修复方案

### 1. 引入全局源调用池(GlobalSourcePool)

**修改文件**: `app/services/source_runner.py`

```python
class GlobalSourcePool:
    """全局源调用池,控制并发数,防止 worker pool 饱和"""

    def __init__(self, max_concurrent: int = 2, max_workers: int = 4):
        self._max_concurrent = max_concurrent
        self._semaphore = threading.Semaphore(max_concurrent)
        self._executor = concurrent.futures.ThreadPoolExecutor(
            max_workers=max_workers,
            thread_name_prefix="source_pool"
        )
        self._stats = {
            "active": 0,
            "queued": 0,
            "completed": 0,
            "failed": 0,
            "timeout": 0,
        }
```

**关键特性**- ✅ 使用 `Semaphore` 控制最大并发数
- ✅ 线程池复用,不再每次创建新的 ThreadPoolExecutor
- ✅ 统计指标(active, queued, completed, failed, timeout)
- ✅ 懒初始化,全局单例

### 2. 添加环境变量配置

**修改文件**: `app/core/config.py`

```python
# 源调用池配置
max_concurrent_sources: int = _int_env("MAX_CONCURRENT_SOURCES", 2)  # HF Free: 2, Pro: 10
source_pool_workers: int = _int_env("SOURCE_POOL_WORKERS", 4)  # 线程池大小
```

**配置项说明**:

| 配置项 | 默认值 | HF Free | HF Pro | 说明 |
|---|---|---|---|---|
| `MAX_CONCURRENT_SOURCES` | 2 | 2 | 10 | 最大并发源调用数 |
| `SOURCE_POOL_WORKERS` | 4 | 4 | 10 | 线程池大小 |
| `SOURCE_TIMEOUT_SECONDS` | 15 | 15-30 | 15 | 单个请求超时 |

### 3. 添加监控端点

**修改文件**: `app/api/routes.py`

```python
@router.get("/admin/source-pool-stats")
def source_pool_stats():
    """获取源调用池的统计信息"""
    stats = get_source_pool_stats()
    return {
        "pool_stats": stats,
        "recommendations": {
            "worker_pool_saturated": stats.get("active", 0) >= stats.get("max_concurrent", 2),
            "suggestion": "如果 active >= max_concurrent,说明 worker pool 已饱和",
        },
    }

@router.get("/admin/health-detailed")
def health_detailed():
    """详细的健康检查"""
    pool_stats = get_source_pool_stats()
    return {
        "ok": True,
        "version": "v24-concurrent-control",
        "source_pool": pool_stats,
        "config": {...},
    }
```

## 部署指南

### 步骤 1:更新环境变量

在 HuggingFace Space 的 Settings > Repository secrets 中添加:

```
MAX_CONCURRENT_SOURCES=2    # HF Free tier 保持 2
SOURCE_POOL_WORKERS=4       # 线程池大小
SOURCE_TIMEOUT_SECONDS=15   # 可以适当增加到 30(如果响应慢)
```

### 步骤 2:提交代码

```bash
cd stock_data_api_service
git add -A
git commit -m "fix: 引入全局源调用池,防止 HF worker pool 饱和

- 添加 GlobalSourcePool 类,使用 Semaphore 控制并发数
- 配置 MAX_CONCURRENT_SOURCES 和 SOURCE_POOL_WORKERS 环境变量
- 添加 /admin/source-pool-stats 和 /admin/health-detailed 监控端点
- 复用线程池,避免每次请求创建新的 ThreadPoolExecutor
- 问题:board_flow/sector_concept_hot 端点因无并发控制导致 worker_pool_saturated"
```

### 步骤 3:验证修复

1. **监控端点检查**```bash
   curl -H "X-API-Key: YOUR_KEY" https://your-space.hf.space/api/v1/admin/health-detailed
   ```

2. **并发测试**```bash
   # 同时发起 5 个并发请求(HF Free tier 只有 2 个 worker)
   for i in {1..5}; do
     curl -H "X-API-Key: YOUR_KEY" \
       "https://your-space.hf.space/api/v1/boards/flow?category=industry&limit=10" &
   done
   wait
   ```

3. **查看统计**```bash
   curl -H "X-API-Key: YOUR_KEY" https://your-space.hf.space/api/v1/admin/source-pool-stats
   ```

## 调优指南

### 场景 1:Free tier(1-2 workers)

**配置**```env
MAX_CONCURRENT_SOURCES=2
SOURCE_POOL_WORKERS=2
SOURCE_TIMEOUT_SECONDS=20
```

**特点**- 最大 2 个并发请求,其他排队等待
- 线程池大小 2,避免线程切换开销
- 超时 20s,给上游足够时间响应

### 场景 2:Pro tier(10+ workers)

**配置**```env
MAX_CONCURRENT_SOURCES=10
SOURCE_POOL_WORKERS=10
SOURCE_TIMEOUT_SECONDS=15
```

**特点**- 最大 10 个并发请求
- 线程池大小 10
- 超时 15s

### 场景 3:上游响应慢(60s+)

**配置**```env
MAX_CONCURRENT_SOURCES=2
SOURCE_POOL_WORKERS=2
SOURCE_TIMEOUT_SECONDS=30
```

**特点**- 减少并发数,避免 worker 被长时间占用
- 增加超时时间,给上游更多时间

## 性能指标

### 修复前

-`worker_pool_saturated` 错误:多个并发请求时立即饱和
- ❌ 请求排队无限制,所有请求竞争
- ❌ 每次请求创建新的线程池,资源浪费

### 修复后

- ✅ 最多 N 个并发请求(N = MAX_CONCURRENT_SOURCES)
- ✅ 超出的请求排队等待,而不是立即饱和
- ✅ 线程池复用,资源效率提升
- ✅ 统计指标可用于监控和调优

## 常见问题

### Q1:为什么 `boards_flow` 和 `sector_concept_hot` 容易饱和?

**A1**:这些端点的特征:
- 调用 eastmoney/AKShare 的网络接口
- 响应时间不稳定(15-60s)
- 没有本地缓存(不像 `/stocks/{code}/daily` 有长期缓存)
- 并发调用时立即占用所有 worker

### Q2:如何判断 worker pool 是否饱和?

**A2**:调用 `/admin/source-pool-stats`,查看:
```json
{
  "pool_stats": {
    "active": 2,          // 当前活跃请求数
    "queued": 5,          // 排队等待的请求数
    "max_concurrent": 2,  // 最大并发数
    "completed": 100,     // 已完成请求数
    "timeout": 10,        // 超时请求数
  }
}
```

如果 `active >= max_concurrent`,说明 worker pool 已饱和。

### Q3:应该增加 MAX_CONCURRENT_SOURCES 还是增加 SOURCE_TIMEOUT_SECONDS?

**A3**:取决于你的 HF tier 和上游响应时间:

| 情况 | 建议 |
|---|---|
| HF Free tier + 上游响应快(<15s) | 保持 MAX=2, TIMEOUT=15 |
| HF Free tier + 上游响应慢(>30s) | MAX=2, TIMEOUT=30 |
| HF Pro tier + 上游响应快(<15s) | MAX=10, TIMEOUT=15 |
| HF Pro tier + 上游响应慢(>30s) | MAX=5, TIMEOUT=30 |

**关键原则**- `MAX_CONCURRENT_SOURCES` × `SOURCE_TIMEOUT_SECONDS` ≈ HF Worker 数 × 每个 worker 的时间
- Free tier:2 workers × 30s = 最多 60s 的并发时间
- Pro tier:10 workers × 15s = 最多 150s 的并发时间

## 监控和报警

### 指标

1. **pool_stats.active** - 当前活跃请求数(>max_concurrent 时饱和)
2. **pool_stats.queued** - 排队请求数(>0 说明有排队)
3. **pool_stats.timeout** - 超时请求数(持续增加说明上游响应慢)
4. **pool_stats.failed** - 失败请求数(>0 需要排查)

### 报警规则

- `active >= max_concurrent` 持续 5 分钟 → 考虑增加 `MAX_CONCURRENT_SOURCES`
- `timeout > 10` 持续 5 分钟 → 考虑增加 `SOURCE_TIMEOUT_SECONDS`
- `queued > 0` 持续 10 分钟 → 考虑优化上游或增加 worker

## 参考资料

- HuggingFace Spaces Worker Pool: https://huggingface.co/docs/hub/en/spaces-overview
- FastAPI Concurrent Requests: https://fastapi.tiangolo.com/async/
- Python ThreadPoolExecutor: https://docs.python.org/3/library/concurrent.futures.html