6. DSO Session 生命周期和定时器
6.1. DSO Session 发起
DSO Session 按第 5.1 节所述开始.
一旦创建了 DSO Session, 客户端或服务器都可以通过该 DSO Session 发起任意数量的 DNS 操作.
当发起方有多条消息要发送时, 不应等待每个响应返回后才发送下一条消息.
响应方必须按接收顺序处理消息, 并应在请求消息的响应可用时返回响应. 响应方不应为了按对应请求的接收顺序交付响应而延迟发送响应.
DNS-over-TCP 规范 [RFC7766] 第 6.2.1.1 节对此有更详细规定.
6.2. DSO Session 超时
DSO Session 关联两个超时值: 非活动超时 (inactivity timeout) 和保活间隔 (keepalive interval). 两个值都通过同一个 TLV, 即 Keepalive TLV (第 7.1 节) 传递.
第一个超时值是非活动超时, 表示客户端可以推测性地保持一个非活动 DSO Session 打开的最长时间, 以期待将来可能还有请求要发送给该服务器.
第二个超时值是保活间隔, 表示如果客户端希望保持 DSO Session 存活, 两条消息之间允许的最大间隔.
这两个超时值彼此独立. 非活动超时可以短于, 等于或长于保活间隔, 尽管大多数情况下预期非活动超时短于保活间隔.
较短的非活动超时配合较长的保活间隔, 表示客户端不应无理由地长时间推测性保持非活动 DSO Session 打开, 但当它确实有主动理由保持 DSO Session 打开时, 无需发送过于频繁的 DSO keepalive 流量来维护该会话. 一个例子是客户端已经订阅 DNS Push notifications. 这种情况下, 客户端没有向服务器发送任何流量, 但由于存在接收 push notifications 的主动请求, 该会话并非非活动.
较长的非活动超时配合较短的保活间隔, 表示客户端可以长时间推测性保持非活动 DSO Session 打开, 但要维持该非活动 DSO Session, 它需要发送大量 DSO keepalive 流量. 这种配置预计较少见.
在通常的非活动超时短于保活间隔的情况下, 只有当客户端具有长期, 低流量操作时, 保活间隔才会发挥作用, 用来确保产生足够的残余流量以维护 NAT 和防火墙状态, 并让客户端和服务器确认彼此仍然连通.
在新的 DSO Session 上, 如果没有发生显式 DSO Keepalive 消息交换, 两个超时值的默认值都是 15 秒.
对两个超时而言, 较低的超时值会带来更高网络流量和更高服务器 CPU 负载.
6.3. 非活动 DSO Session
在服务器和客户端上, 生成或接收任何完整 DNS 消息 (包括 DNS 请求, 响应, 更新, DSO 消息等) 都会重置该 DSO Session 的两个定时器. 唯一例外是 DSO Keepalive 消息只重置保活定时器, 不重置非活动超时定时器.
此外, 只要客户端仍有未完成的操作正在进行, 非活动定时器就保持清除状态, 不会发生非活动超时.
对传统查询和更新等短生命周期 DNS 操作而言, 从请求到响应之间的时间被视为操作 "正在进行", 通常最多只有几百毫秒. 在客户端, 发送请求时清除非活动定时器, 直到收到对应响应前一直保持清除. 在服务器, 接收请求时清除非活动定时器, 直到发送对应响应前一直保持清除.
对长期 DNS Stateful Operations (例如 Push Notification subscription [Push] 或 Discovery Relay interface subscription [Relay]) 而言, 只要操作处于活动状态, 即尚未取消, 就视为 "正在进行". 这意味着一个 DSO Session 可以存在活动操作, 即使两个方向都没有消息流动, 也可以远远超过非活动超时时间. 这不是错误. 这正是需要两个独立定时器的原因: 非活动超时和保活间隔. DSO Session 长时间没有流量并不自动意味着它是 "非活动" 的, 如果它有正在等待事件的活动操作.
6.4. 非活动超时
非活动超时的目的是让服务器在建立新 DSO Session 的成本和维护非活动 DSO Session 的成本之间权衡. DSO Session 容量充足的服务器可以提供较高非活动超时, 允许客户端长时间保持推测性 DSO Session 打开, 从而节省未来与该服务器通信时建立新 DSO Session 的成本. 内存资源紧张的服务器可以提供较低非活动超时, 促使客户端在没有未完成操作时及时关闭 DSO Session, 并在之后需要时再创建新的 DSO Session.
6.4.1. 关闭非活动 DSO Session
当连接达到非活动超时时, 客户端必须开始关闭空闲连接, 但客户端并不需要一直保持空闲连接直到达到非活动超时. 客户端可以自行决定在任何时候关闭 DSO Session. 如果客户端判断当前没有且可合理预期也不会有继续使用当前非活动 DSO Session 的需要, 它应当优雅关闭该连接.
在 DSO Session 生命周期内的任何时刻, 如果非活动超时值 (默认 15 秒) 流逝且 DSO Session 上没有任何活动操作, 客户端必须优雅关闭连接.
在 DSO Session 生命周期内的任何时刻, 如果 DSO Session 上没有活动操作的时间过长, 服务器必须认为客户端失约并强制中止该 DSO Session. 这里的 "时间过长" 指 5 秒或当前非活动超时值的两倍, 取较大者. 如果非活动超时使用默认 15 秒, 则客户端在 30 秒非活动后仍未关闭连接时会被视为失约并被断开.
在此语境中, DSO Session 上的活动操作包括等待响应的查询, 等待响应的更新, 或活动的长期操作, 但不包括 DSO Keepalive 消息交换本身. DSO Keepalive 消息交换只重置保活间隔定时器, 不重置非活动超时定时器.
如果客户端希望把非活动 DSO Session 保持得比默认时长更久, 它会使用 DSO Keepalive 消息请求更长的超时值, 如第 7.1 节所述.
6.4.2. 非活动超时取值
对非活动超时值而言, 较低值会导致更频繁的 DSO Session 拆除和重建. 较高值会降低流量和服务器 CPU 负载, 但会增加维护非活动 DSO Session 状态所需的内存负担.
服务器可以为非活动超时指定任何其选择的值 (无论是在响应客户端发起的请求时, 还是在服务器发起的消息中), 包括低于一秒甚至为零的值.
非活动超时为零表示客户端不应推测性地维护空闲连接. 一旦客户端完成与该服务器相关的操作, 就应立即开始关闭该会话.
服务器会在 5 秒或非活动超时值的两倍后强制中止空闲客户端会话, 取较大者. 当非活动超时为零时, 这意味着如果客户端未关闭空闲客户端会话, 服务器会在 5 秒后强制中止该空闲会话.
非活动超时值 0xFFFFFFFF 表示 "无限", 告知客户端可以按自己意愿一直保持空闲连接打开. 注意, 即使以这种方式授予无限非活动超时, 服务器仍可在任何时候发送新的 DSO Keepalive 消息, 指定新的 Session Timeout 值来修订该非活动超时.
当前 Keepalive TLV 支持的最大有限非活动超时为 0xFFFFFFFE (2^32-2 毫秒, 约 49.7 天).
6.5. 保活间隔
保活间隔的目的是生成足够的消息, 以维护中间盒 (例如 NAT 网关或防火墙) 中的状态, 并让客户端和服务器定期验证彼此仍然连通. 这使双方能在连通性丢失时清理状态, 并在适当时建立新会话.
6.5.1. 保活间隔到期
在 DSO Session 生命周期内的任何时刻, 如果保活间隔值 (默认 15 秒) 流逝且 DSO Session 上没有发送或接收任何 DNS 消息, 客户端必须通过发送 DSO Keepalive 消息 (第 7.1 节) 来保持 DSO Session 存活. DSO Keepalive 消息交换只重置保活定时器, 不重置非活动定时器.
如果客户端突然断开网络连接而没有干净关闭其 DSO Session, 可能留下未取消的长期操作, 服务器会在未收到该客户端必需的 DSO keepalive 流量后得知这一点. 在 DSO Session 生命周期内的任何时刻, 如果保活间隔值的两倍时间 (默认 30 秒) 流逝且 DSO Session 上没有发送或接收任何 DNS 消息, 服务器应认为客户端失约并应强制中止该 DSO Session.
6.5.2. 保活间隔取值
对保活间隔值而言, 较低值会产生更多 DSO keepalive 流量. 较高保活间隔会降低流量和 CPU 负载, 但对服务器内存负担影响很小, 因为客户端保持 DSO Session 打开的时长由非活动超时决定, 与所需 DSO keepalive 流量级别无关.
客户端和服务器可以根据所在网络类型选择不同的保活间隔.
只服务内部网络客户端, 且中间没有 NAT 网关或防火墙的企业 DNS 服务器, 可以施加更长保活间隔, 因为不需要频繁的 DSO keepalive 流量.
主要服务住宅消费者客户端的公共 DNS 服务器, 由于路径上很可能有 NAT 网关, 可以施加较短保活间隔以生成更频繁的 DSO keepalive 流量.
智能客户端可以根据环境自适应. 使用私有 IPv4 地址 [RFC1918] 与私有地址块之外的 DNS 服务器通信的客户端, 可以推断路径上可能有 NAT 网关, 因而请求较短保活间隔.
默认情况下, 推荐客户端请求且服务器授予 60 分钟的保活间隔. 该保活间隔可以在客户端突然断开且未干净关闭会话时较及时地检测出来. 同时, 它也足以维护遵循 IETF 推荐 Best Current Practice 的防火墙和 NAT 网关中的状态, 即中间盒使用的 "established connection idle-timeout" 至少为 2 小时 4 分钟 [RFC5382] [RFC7857].
注意, 保活间隔越短, 客户端和服务器负载越高. 此外, 如果保活值短于传输层完成重传所需时间, 单个数据包丢失就可能导致服务器过度积极地中止连接. 例如, 一个假设且不现实的 100 ms 保活间隔值, 会导致两个方向持续产生每秒十条或更多消息 (如果当前拥塞控制窗口允许) 来保持 DSO Session 存活. 在这个极端例子中, 对于 RTT 为 100 ms 的路径, 单次重传就会在消息流中引入足够长的短暂停顿, 导致服务器中止连接.
由于这一顾虑, 服务器禁止发送保活间隔值小于 10 秒的 DSO Keepalive 消息 (无论是对客户端发起 DSO 请求的 DSO 响应, 还是服务器发起的 DSO 消息). 如果客户端收到指定保活间隔值小于 10 秒的 DSO Keepalive 消息, 这是致命错误, 客户端必须立即强制中止连接.
保活间隔值 0xFFFFFFFF 表示 "无限", 告知客户端不应生成任何 DSO keepalive 流量. 注意, 在以这种方式指示客户端不应生成 DSO keepalive 流量后, 服务器仍可随时发送新的 DSO Keepalive 消息, 指定新的 Session Timeout 值来修订该 DSO keepalive 流量要求.
当前 Keepalive TLV 支持的最大有限保活间隔为 0xFFFFFFFE (2^32-2 毫秒, 约 49.7 天).
6.6. 服务器发起的 DSO Session 终止
除了选择性取消单个长期操作 (第 5.6 节) 外, 有时服务器也需要终止一个或多个完整 DSO Session. 如果客户端存在缺陷, 或离开网络而没有关闭其 DSO Session, 可能需要终止整个 DSO Session. 当服务器过载或重新配置, 且无法选择性判断哪些操作需要取消时, 也可能需要终止 DSO Session.
本节讨论 DSO Session 可能被终止的各种原因以及执行终止的机制.
正常运行时, 关闭 DSO Session 是客户端的责任. 客户端根据自身需求和服务器指定的非活动超时值来决定何时关闭 DSO Session. 服务器只会在下列异常情况下导致 DSO Session 结束:
-
服务器应用软件或底层操作系统正在关闭或重启.
-
服务器应用软件意外终止, 例如由于 bug 崩溃, 导致底层操作系统发送 TCP RST.
-
服务器正在重新配置或维护, 且由于服务器软件实现方式, 需要断开客户端. 例如某些软件在启动时读取配置文件, 修改服务器配置需要修改配置文件, 然后杀死并重启服务器软件, 这通常会导致网络连接丢失.
-
客户端未履行生成必需 DSO keepalive 流量的义务, 或未在规定时间内关闭非活动会话 (5 秒或服务器指定时间间隔的两倍, 取较大者, 如第 6.2 节所述).
-
客户端发送严重无效或格式错误的请求, 表明客户端实现存在严重缺陷.
-
服务器容量不足, 需要卸载部分负载.
6.6.1. 服务器发起的 Retry Delay 消息
在上述服务器选择终止 DSO Session 的情况下, 它可以直接强制中止连接. 但如果这样做, 客户端很可能把它当作网络故障并立即重新连接, 从而给服务器带来更多负担.
因此, 为避免重新连接崩塌, 服务器应改为发送 Retry Delay 消息卸载客户端负载, 并带上适当 RCODE 值告知客户端 DSO Session 需要终止的原因. DSO Retry Delay TLV 的格式以及各 RCODE 值的解释见第 7.2 节. 发送 DSO Retry Delay 消息后, 服务器禁止在该 DSO Session 上发送任何进一步消息.
当短时间内发送大量 retry delay 时, 服务器可以随机化重试延迟, 以避免所有客户端同时尝试重新连接. 一般而言, 实现应避免以会导致大量客户端在精确同一指定时间重新连接的方式使用 DSO Retry Delay 消息.
客户端收到服务器的 DSO Retry Delay 消息后, 必须记录该服务器的重连延迟, 并立即优雅关闭连接.
发送 DSO Retry Delay 消息后, 服务器应给客户端 5 秒时间关闭连接. 如果 5 秒后客户端仍未关闭连接, 服务器应强制中止连接.
客户端禁止发起 DSO Retry Delay 消息. 如果服务器收到 DSO Retry Delay 消息, 这是致命错误, 服务器必须立即强制中止连接.
6.6.1.1. 未完成操作
服务器选择发起 DSO Retry Delay 消息的瞬间, 该 DSO Session 上可能已有客户端到服务器方向正在传输的 DNS 请求, 它们会在服务器发送 DSO Retry Delay 消息之后到达服务器. 服务器必须静默忽略这些传入请求, 禁止为它们生成任何响应消息. 当服务器的 DSO Retry Delay 消息到达客户端时, 客户端会判断它先前在该 DSO Session 上发送且尚未收到响应的任何 DNS 请求都必然不会再收到响应.
这类请求应视为失败, 并在适当时候重试.
如果 DSO Session 上已有操作中只有一部分变得无效 (例如服务器重新配置后不再对某些名称具有权威), 但服务器通过向所有受影响 DSO Session 发送 DSO Retry Delay 消息来整体终止它们, 则重连延迟可以为零, 表示客户端应立即尝试重新建立操作.
根据重新配置的性质, 某些尝试可能成功, 某些可能失败.
如果服务器一次终止大量 DSO Session (例如系统重启), 且不希望被同时重试洪峰淹没, 它应向每个客户端发送不同的重连延迟值. 这些调整可以随机, 伪随机或确定性选择, 例如对每个后续客户端把时间值增加十分之一秒, 从而得到重启后每秒 10 个客户端的重连速率.
6.6.2. 行为异常的客户端
服务器可能判断某个客户端没有正确遵循协议. 服务器可能无法恢复该 DSO Session, 在这种情况下会强制终止连接. 由于客户端不知道连接为什么断开, 它可能立即重新连接. 如果服务器已经确定客户端未正确遵循协议, 可以在 DSO Session 一建立就终止它, 并指定较长 retry-delay 来防止客户端立即重新连接.
6.6.3. 客户端重新连接
服务器结束 DSO Session 后 (无论是通过向客户端发送 DSO Retry Delay 消息, 还是强制中止底层传输连接), 客户端应尝试重新连接到该服务实例, 或在存在多个可用实例时连接到另一个合适的服务实例. 如果重新连接同一服务实例, 客户端必须在尝试重新连接前遵守指示的延迟 (如有). 客户端不应自行随机化该延迟; 如果需要这种行为, 服务器会对发送给每个客户端的 retry delay 值随机抖动.
如果某个服务实例只会因短暂维护而不可用, 它应指示略长于预期维护窗口的 retry delay 值. 它不应默认使用很大的延迟值, 否则客户端可能不会在服务恢复后及时尝试重新连接.
如果某个服务实例不希望客户端永远重新连接 (例如该服务实例正在退役), 它应把 retry delay 设为最大值 0xFFFFFFFF (2^32-1 毫秒, 约 49.7 天). 无法指示客户端远离超过 49.7 天. 如果 49.7 天后 DNS 或其他配置信息仍指示这是某项服务的有效服务实例, 客户端可以尝试重新连接. 实际上, 如果客户端重启或以其他方式丢失状态, 只要 DNS 或其他配置信息仍继续指示这是客户端应使用的服务实例, 它很可能在 49.7 天过去前就尝试重新连接.
6.6.3.1. 强制中止后的重新连接
如果连接因服务器不合规行为而被客户端强制中止, 客户端应把该服务实例标记为不支持 DSO. 客户端可以重新连接但不尝试使用 DSO, 或在适用时连接到不同服务实例.
6.6.3.2. 无法解释的连接断开后的重新连接
服务器也可能强制终止连接; 在这种情况下, 客户端不知道终止是协议错误导致还是网络中断导致. 当客户端注意到连接已断开时, 可以立即尝试重新连接. 但是, 如果连接再次断开, 且客户端无法成功完成其试图执行的操作, 它应把该服务器标记为不支持 DSO.
6.6.3.3. 探测可用的 DSO 支持
一旦客户端把服务器标记为不支持 DSO, 它就不应在一段时间过去前对该服务器尝试 DSO 操作. 合理的最小值是 1 小时. 由于强制中止连接是软件故障的结果, 该问题不太可能在首次遇到后的第一个小时内解决. 不过, 把重试间隔限制为 1 小时, 客户端就能在问题修复时注意到, 同时不会给服务器造成不当负担.