订阅快照改为变更即推,并加低频保活
原先主站每 60 秒无条件重建并推送整份快照:实测 12 账号时传输约 346 KiB/次、 约 486 MiB/天,节点每次都要 base64 解码、SHA-256、gunzip、JSON 校验后判 APPLY_OLD 丢弃。而 revision 是内容寻址的,订阅不常变动时这些推送纯属浪费。 改为: - 内容变化即推:各处已有的 requestSubscriptionSync()(刷新成功、绑定/改绑、 重置 Key、增删停用账号、过滤开关变更)保持原样,延迟仍在数秒内。 - 低频保活:新增 scheduledSubscriptionKeepalive(),默认 6 小时一次,仅用于刷新 节点「最近收到快照」的时刻。节点的过期判定依据该时刻(见 storageNode 侧修复), 因此保活周期必须远小于节点 SubscriptionMaxStaleSeconds(默认 7 天)。 - 节点上线即推:initChannel 保持推送一次,覆盖节点刚重启、本地尚无快照的情况。 空闲流量约 486 MiB/天 -> 约 1.4 MiB/天(约三个数量级)。 测试:432 项全过。新增 SubscriptionKeepaliveTest 锁定不变量——保活入口必须存在, 且周期小于节点有效期的 1/4、初始延迟小于一个周期;否则节点可能在两次保活之间判过期。
This commit is contained in:
@@ -179,6 +179,8 @@ public class RemoteService {
|
||||
|
||||
//子节点上线时,发送未完成的任务
|
||||
resetUndone();
|
||||
// 节点刚上线时可能没有任何快照(例如刚重启),必须推一次;
|
||||
// 这同时会刷新节点「最近收到快照」的时刻,避免内容未变时被判过期。
|
||||
requestSubscriptionSync();
|
||||
return true;
|
||||
}catch (Exception e){
|
||||
@@ -252,8 +254,19 @@ public class RemoteService {
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${subscription.standby.retry-interval-ms:60000}")
|
||||
void scheduledSubscriptionSync() {
|
||||
/**
|
||||
* 低频保活推送。
|
||||
*
|
||||
* <p>快照推送改为「订阅内容变化时立即推」之后,内容长期不变时节点将收不到任何推送,
|
||||
* 而节点的过期判定依据「最近一次收到快照的时刻」——于是它会在一周后永久判过期。
|
||||
* 这个保活用于把「主站在线」这件事周期性告诉节点,刷新其新鲜度。
|
||||
*
|
||||
* <p>周期远小于节点 `SubscriptionMaxStaleSeconds`(默认 7 天):默认 6 小时一次,
|
||||
* 约 1.4 MiB/天,相比原先每 60 秒一次的约 486 MiB/天 降低约三个数量级。
|
||||
*/
|
||||
@Scheduled(fixedDelayString = "${subscription.standby.keepalive-interval-ms:21600000}",
|
||||
initialDelayString = "${subscription.standby.keepalive-initial-delay-ms:600000}")
|
||||
void scheduledSubscriptionKeepalive() {
|
||||
requestSubscriptionSync();
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,10 @@ subscription:
|
||||
standby:
|
||||
sync-enabled: "${SUBSCRIPTION_STANDBY_SYNC_ENABLED:false}"
|
||||
sync-secret: "${SUBSCRIPTION_SYNC_SECRET:}"
|
||||
retry-interval-ms: 60000
|
||||
# 快照改为「订阅内容变化时立即推送」,另加低频保活刷新节点新鲜度。
|
||||
# 周期须远小于节点 SubscriptionMaxStaleSeconds(默认 7 天),默认 6 小时。
|
||||
keepalive-interval-ms: 21600000
|
||||
keepalive-initial-delay-ms: 600000
|
||||
|
||||
bot:
|
||||
token: "5222939329:AAHa6l9ZuVVdNSDLPI_H-c8O_VgeOEw5plA"
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.lion.lionwebsite.Service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* 备机快照推送时机与保活周期。
|
||||
*
|
||||
* <p>快照推送是「订阅内容变化时立即推」(由各处 requestSubscriptionSync 触发)。
|
||||
* 但节点按「最近一次收到快照的时刻」判过期,因此内容长期不变时必须有低频保活,
|
||||
* 否则节点会在一个有效期后永久判过期——这是本类要锁住的不变量。
|
||||
*/
|
||||
class SubscriptionKeepaliveTest {
|
||||
|
||||
/** 节点侧 SubscriptionMaxStaleSeconds 的默认值(7 天),保活周期必须远小于它。 */
|
||||
private static final long NODE_MAX_STALE_MILLIS = 7L * 24 * 3600 * 1000;
|
||||
|
||||
/**
|
||||
* 保活必须存在、且周期远小于节点有效期。
|
||||
*
|
||||
* <p>回归背景:曾一度改成「只在内容变化时推送」,结果存储节点重启后收不到任何推送,
|
||||
* 状态直接退化为 expired。保活就是为这个场景兜底的。
|
||||
*/
|
||||
@Test
|
||||
void keepaliveIsScheduledWellWithinNodeStaleWindow() throws Exception {
|
||||
Method method = RemoteService.class.getDeclaredMethod("scheduledSubscriptionKeepalive");
|
||||
Scheduled scheduled = method.getAnnotation(Scheduled.class);
|
||||
|
||||
assertNotNull(scheduled, "必须存在保活推送的定时入口");
|
||||
long interval = parseDefaultMillis(scheduled.fixedDelayString());
|
||||
assertTrue(interval > 0, "保活周期必须为正: " + scheduled.fixedDelayString());
|
||||
assertTrue(interval < NODE_MAX_STALE_MILLIS / 4,
|
||||
"保活周期(" + interval + "ms)必须远小于节点有效期(" + NODE_MAX_STALE_MILLIS + "ms),"
|
||||
+ "否则节点可能在两次保活之间判过期");
|
||||
|
||||
long initialDelay = parseDefaultMillis(scheduled.initialDelayString());
|
||||
assertTrue(initialDelay >= 0, "初始延迟不能为负");
|
||||
assertTrue(initialDelay < interval, "初始延迟应小于一个周期,避免启动后长时间不刷新节点新鲜度");
|
||||
}
|
||||
|
||||
/** 从 `${key:default}` 形式的占位符里取出默认毫秒值。 */
|
||||
private static long parseDefaultMillis(String placeholder) {
|
||||
int colon = placeholder.indexOf(':');
|
||||
String value = colon >= 0 ? placeholder.substring(colon + 1) : placeholder;
|
||||
value = value.replace("}", "").trim();
|
||||
return Long.parseLong(value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user