---
title: "系统时间回拨后超时变长：用单调时钟共享截止时间"
date: 2026-11-13
slug: python-monotonic-timeout-budget
canonical: https://kernelranch.com/blog/2026/11/python-monotonic-timeout-budget
markdown_url: https://kernelranch.com/blog/2026/11/python-monotonic-timeout-budget.md
tags: ["python", "time", "retries"]
---一个导出请求给总流程 5 秒，连接、读取和重试分别重新获得 5 秒，用户可能等远超过约定的时间。如果还用系统时间计算差值，校时回拨会进一步拉长等待。进入请求时用单调时钟计算一次截止时间，每进入下一阶段都从它扣减。

## 1. 不改系统时间，直接注入时钟

保存为 budget_lab.py，运行 python budget_lab.py。

```python
import time
from dataclasses import dataclass

@dataclass
class Clock:
    wall: float = 100.0
    steady: float = 200.0

def deadline(seconds, clock=time.monotonic):
    expires = clock() + seconds

    def remaining():
        return max(0.0, expires - clock())

    return remaining

clock = Clock()
wall_left = deadline(5, lambda: clock.wall)
left = deadline(5, lambda: clock.steady)
clock.wall += 2
clock.steady += 2
assert left() == 3
print("after connection:", left(), "seconds left")

clock.wall -= 60
clock.steady += 1
assert wall_left() == 63 and left() == 2
print("after simulated rollback: wall=", wall_left(), ", monotonic=", left(), sep="")

clock.steady += 2
assert left() == 0
print("before retry:", left(), "seconds left")
```

本机实测输出如下。

```text
after connection: 3.0 seconds left
after simulated rollback: wall=63.0, monotonic=2.0
before retry: 0.0 seconds left
```

## 2. 每次进入阻塞操作前都查预算

先算剩余时间，已耗尽就结束请求。否则把剩余时间和单阶段上限取最小值，再传给库。退避等待也要计入预算，醒来后重新计算。若库的 read timeout 表示两次读取之间的等待，持续流入少量数据可以一直延后结束，这时需要在上层加总时限或可取消的异步调用。

墙上时间适合日志和跨进程协议。单调时钟的参考点未定义，适合在本进程按差值计时。跨进程传预算时定义清楚时延和计时方式，避免把本地 monotonic 的绝对值写到数据库当作时间戳。

## 小结

同一次请求共享单调时钟的截止时间。验收时覆盖系统时间前跳、回拨和最后一次重试之前预算耗尽。

## 数据来源与检索时间

检索时间 2026-11-13。

| 来源 | URL | 对应内容 |
|---|---|---|
| Python time | https://docs.python.org/3.11/library/time.html#time.monotonic | 单调读数、参考点和系统校时 |

## 核验说明

- Python 3.11.9 实测注入时钟的预算分支。回拨由变量模拟，未修改机器时间。
