作者通过在执行环境600秒超时的约束下,用自愈缓存(Self-Healing Cache)解决250个ticker全量API调用需要700秒的耗时问题,详解问题根因和解决方案。
嘿大家好啊,你们身边那位略显苍老的开发者又来了。我今年 38 岁,平时是工程师,周末则构建 AI 算法交易机器人。
有时候,当我在构建这类个人项目时,会注意到一些不对劲的地方——比如"这家伙好久没输出日志了"。这通常意味着它已经悄无声息地挂起了。而这次遇到的情况,正是如此。
任务本身很简单:定期从某个市场 API 获取大约 250 只股票的数据。理应顺顺利利的。但日志显示,它每次都在中途停下来,没有半点完成日志。
我最初的想法是网络暂时中断或 API 服务器临时故障。但多次重试之后,它总是在同一个地方失败。深入调查后发现,问题要简单得多,也基础得多。
原因很简单——就是超时。
获取 1 只股票数据的耗时:平均 2.8 秒
需要获取的股票数量:250 只
理论总耗时:2.8 秒 × 250 = 700 秒
与此同时,这个机器人运行所在的环境,超时设置是 600 秒(10 分钟)。
我想你已经看出问题了。我在一个 600 秒后强制终止的环境中,运行了一个需要 700 秒才能完成的任务。它当然永远无法完成。完全是我自己的设计失误。真蠢!
我为什么会忽略这么基础的事情?开发阶段,我测试时只用了大约 10 只股票。10 只股票只需不到 28 秒就完成了。"嗯,能跑,没问题!"我心想。然后,上线时扩展到完整的 250 只股票,我完全没去计算总共需要多少时间。
面对这类问题,首先想到的自然就是缓存。把获取到的数据存到本地,后续运行时直接复用本地数据,而不再调用 API。
但仅靠这一点,在这种情况下还不够。
为什么?因为构建缓存的第一次运行仍然需要 700 秒,依然会触发超时。当进程因超时而崩溃时,内存中所有部分获取的数据都会凭空消失。所以下次启动时,它不得不从头重新获取全部 250 只股票,再次超时……形成无限循环,缓存永远无法建立。
这就是"自愈缓存"这个想法的由来。
我做的事情非常简单:"在逻辑检查点频繁保存进度。"
具体来说,每获取 50 只股票后,我就把目前已有的结果以 pickle 格式写入文件。
有了这个方案,即使进程被 600 秒超时打断,至少也有 50 × N 条进度保存在磁盘上。
下次启动时,先加载这个缓存文件,然后只去 API 获取文件中尚未存在的那部分股票。
通过重复这个过程,无论发生多少次超时,全部 250 只股票的缓存最终都会完成。即使任务被中断,也能从断点恢复。
以下是实际代码的样子:
import pickle
import os
import time
CACHE_FILE = 'api_data_cache.pkl'
CACHE_TTL_SECONDS = 6 * 24 * 60 * 60 # Cache is valid for 6 days
def get_data_with_self_healing_cache(tickers_to_fetch):
"""
A caching mechanism that saves intermediate progress every 50 items
so the process can resume even if interrupted for a long time.
"""
cache = {}
# Load existing cache if it exists (and is within TTL)
if os.path.exists(CACHE_FILE):
if time.time() - os.path.getmtime(CACHE_FILE) < CACHE_TTL_SECONDS:
with open(CACHE_FILE, 'rb') as f:
cache = pickle.load(f)
# Create a list of tickers that truly need to be fetched via API this time
needed_tickers = [t for t in tickers_to_fetch if t not in cache]
print(f"Total: {len(tickers_to_fetch)}, Cached: {len(cache)}, To Fetch: {len(needed_tickers)}")
fetched_count = 0
for ticker in needed_tickers:
try:
# data = fetch_from_external_api(ticker) # This is the time-consuming API call
data = {'price': 1000, 'timestamp': time.time()} # Using dummy data for this example
cache[ticker] = data
fetched_count += 1
# ★★★ Core part of self-healing ★★★
# Save cache to disk every time 50 new items are fetched
if fetched_count > 0 and fetched_count % 50 == 0:
print(f"--- Saving intermediate cache progress ({fetched_count} new items) ---")
with open(CACHE_FILE, 'wb') as f:
pickle.dump(cache, f)
except Exception as e:
print(f"Error fetching {ticker}: {e}. Saving progress before exit.")
# Even if an error occurs, save progress before re-raising the exception
with open(CACHE_FILE, 'wb') as f:
pickle.dump(cache, f)
raise
# Finally, save all results
if fetched_count > 0:
print("--- Saving final cache ---")
with open(CACHE_FILE, 'wb') as f:
pickle.dump(cache, f)
# Return data for all requested tickers
return {t: cache.get(t) for t in tickers_to_fetch}
关键是 fetched_count % 50 == 0: 这部分,代码会定期将进度写入文件。同样值得注意的是,在 except 子句中也包含了保存操作。这确保了即使发生意外错误,到此为止的所有努力也不会白费。
修复之后,机器人现在稳定运行了。
不出所料,第一次运行超时了一次。但看日志,大约 200 条数据已经被正确保存到了缓存文件。第二次运行时,它只获取了剩余的 50 条,所有股票数据的缓存就顺利完成了。
而之后的运行就非常顺利了。
由于缓存命中率达到 100%,API 调用次数为零。这个之前在 600 秒内都跑不完的进程,现在平均只需 2.8 秒就完成。这不仅仅是快了 10 倍——而是数量级的提升。
我从中得到的教训是,设计时要有"任务会被中断"的假设。特别是在个人开发、云环境或家用服务器上,你永远不知道什么时候会出问题。编写长时间运行的进程时,必须把"中断后恢复"纳入设计考量。
这个"自愈"的概念不只适用于 API 获取,在很多场景下都能应用——比如重型数据分析批处理任务,或者处理数万个文件的场景。
如果你也在为"长时间任务莫名永远跑不完"而苦恼,希望这篇文章能帮到你!