第二十七夜 需求与响应协议,电网里的应答哲学
上一夜我们把这门协议的语言史讲完了。XML 从出生到搬家,二十年换了一口口音,可乡音底下说的还是那些话。这一夜要拆的,就是那些话本身。名字先掰开。OpenADR,Open 是开放的,剩下三个字母各挑一件事。A,Automated,自动化的。D,Demand,需求。R,Response,响应。合起来,开放自动化需求响应。
先说 D,需求是谁。
第23夜我陪你看过多功能插座的清单,也看过工厂配电室的电表。需求不是别的,就是这些。你家里墙上正开着的空调,几百瓦到1000瓦;工厂里嗡嗡转的马达,一台几十千瓦起步。一户人家是零头,可千万户加在一起,就是第23夜那座城的晚峰,18867 万千瓦。需求的第一个特点是分散,分散到千家万户;第二个特点是大,大到逼近2亿千瓦;第三个特点是它自己会做主,谁家开空调,电网说了不算。
再说 R,响应是什么。
电不够用的时候,老办法是打电话。调度员一个个打给大工厂,问下午6点到7点能不能让一让。电话打通了,这一让就是响应;打不通,人不在,记错了钟点,响应就断了。新办法是把电话换成机器。高峰时让1万个空调各把设定温度调高2度,让电池放电,让工厂把马达挪一个班次。
这笔生意有多大,可以粗粗算一下。一台空调调高2度,少出一两百瓦,1万台凑在一起是1000到2000千瓦,差不多一家中型工厂一个车间的量。一家大厂错峰半小时,几万千瓦就出来了。把这些零头全部聚合起来,就是一门国家规模的生意。中国给虚拟电厂定的目标是 2030 年做到 5000 万千瓦,这个量级相当于几十座百万千瓦的大型机组。不用建新电厂,先凑出一座看不见的电厂。需求响应卖的,就是这些让出来的几千万千瓦的电量。
最后是 A,自动化。这三个字是人肉和机器的分界线。打电话给厂长,那是需求的响应;把几点、让多少、谁参与写成报文送进设备的收件箱,设备自己读、自己执行、自己回话,这才是 OpenADR。名字拆到这里,这个协议是干嘛用的就说完了。它管的是电网和1万台设备之间怎么开口喊话、怎么应答。
可是当年我打开那份几百页的规范时,看见的不是这四个字母,是几十个报文名。oadrDistributeEvent,oadrCreateReport,oadrCancelOpt,oadrRequestReregistration,一个比一个长,名字摞着名字,翻不到头。后来我给自己立了个法,别背名字,先分家。几十个报文,按亲戚关系分成四家,外加一个站在四家之外的常客。这一夜,就把这一家人一个个认过去。

第一家,注册系,规范里叫 EiRegisterParty。家常名,认识。
任何关系都得从互相认识开始,所以第一步 VEN 要向 VTN 注册自己的信息,报上名号。我叫什么,这是设备编号 venID;我说什么方言,这是协议档位 2.0b;我走哪条路,这是传输方式;几秒拍一次门,这是通信频率。VTN 把这些记下,回一份注册应答,从此这一对算认识了。像入职第一天办工牌,办完之后没有人每天早上重新办一遍。注册是一次性的,往后除非证书换了、地址搬了,没有人天天走这道门。
有一个细节值得先记下。通信频率不是 VEN 自己拍脑袋定的,规范第 9.1.8 节写明,VTN 在注册应答里下发一个字段,指定允许的最小通信频率。记住它,这一夜收尾的时候它会回来。
第二家,事件系,规范里叫 EiEvent。家常名,派工。
这是四家里唯一一家由 VTN 主动开口的。派工单叫 oadrDistributeEvent,上面写着几点开始、动多少、持续多久、谁参与、用什么信号。第25夜学校慈善拍卖会,广播员吼的那一嗓子,走的就是这一家。VEN 收到派工单,必须表态。这一单我能干,报 optIn;这一天设备检修,报 optOut。表态乘着 oadrCreatedEvent 送回去,第25夜那声「好的」,户口就在这里。
为什么派工是唯一一个 VTN 主动的方向?因为时间的主动权在电网手里。什么时候缺电,设备猜不到,也只有电网知道。其余三家里,先开口的都是 VEN。
第三家,参与系,规范里叫 EiOpt。家常名,报备。
事件系管的是这一单你干不干,参与系管的是更早的一层。未来一段日子里,我什么时候在岗,什么时候缺席。「明天九点到十二点检修,别给我派工」,这句报备走的就是 EiOpt。报备了也能反悔,中途撤回走 cancelOpt。第25夜那个摆摊摆到一半、货卖完了主动收摊退出的人,用的就是这张单子。参与系是四家里最有人味的一家,它传的不是数据,是意愿。
第四家,报告系,规范里叫 EiReport。家常名,交卷。
电网光会喊话不够,还得知道喊完之后设备干没干。这一套走四步。VEN 先把家底报上去,我有哪些电表、哪些电池、各是什么型号,这份名册叫 METADATA。VTN 照着名册下订购单 oadrCreateReport,我要你 30 分钟一格的电表读数。VEN 按合同周期交卷 oadrUpdateReport。合同到期或者要改,收摊 oadrCancelReport。第25夜那句「八个杯子一小时已送」,户口在这里。
这一家是四家里最重的一家,名册、点单、交卷、收摊,一套完整的证据链。这台机器里面还藏着一台机器,一台当年亲手写出来的机器,那是后面几夜的正餐,这里先按下不表。
四家认完,回头看一眼菜单。
认识、派工、报备、交卷。一段受约束的长期关系,能发生的事就这四类。这四样不是 OpenADR 的发明,它的母菜单是 OASIS Energy Interoperation 标准,上面一共摆了八样菜。规范第 11.5 节白纸黑字列着没端上桌的四样,EiQuote 报价,EiEnroll 招生,EiAvail 可用性申报,EiMarketContext 市场上下文,后面跟着一条注解,not currently used,眼下不用。电不是当场讨价还价的现货,报价撮合有专门的市场去干;会员准入走电网公司的招募流程,不用协议管招生;可用性这件事,参与系加报告系合起来干得更细;市场的背景信息,事件单里自带。联盟从八样里点了四样,每一刀都有理由。结构是砍出来的。留下来的每一系,都是拿掉之后这场关系就缺一块的那一块。
四系说完了,该看它们跑在哪条路上。
2.0b 的报文走 HTTP。HTTP 有个天生的性格,只能你问我答,没有服务器主动敲门的动作。VTN 想派工,可 VEN 不开口,VTN 连门都敲不了。协议给了两条路。一条叫 PUSH,VEN 自己当收发室,开着门,VTN 想送什么直接送上门。一条叫 PULL,VEN 当客户,每过几秒去问一句,有货吗。规范第 9.1.1 节把两条路分开写了定义,先讲 PULL,因为世界上大多数设备走的是这条。
第25夜给 oadrPoll 起过家常名,看信箱。规范的说法更有意思,oadrPoll 让 PULL 模式的设备能够模拟 PUSH 模式的报文交换。翻译过来,拉,是对推的模拟。
问「有货吗」这一下,是这个模式全部的心思。要是对方回一句「有」,你再跑一趟去取,一来一回多跑冤枉路。协议的答法是邮递员的答法。他不回话,直接把信从怀里掏出来递到你手上,再给你盖一个戳,戳上两句话,有货,而且我已经送到了。问与取,一次往返,全部办完。
这套答法在行业里有个名字,piggyback,捎带。计算机界的老手艺,TCP 的确认号就常常搭在反向的数据包上捎回去,省一趟纯确认包。piggyback 不是这门协议的发明,是它把老手艺用到了报文上。
捎回来的到底是什么?规范第 8.6 节列了清单,八种报文,恰好按四系分家。事件系的派工单 oadrDistributeEvent。报告系的订购单、改单、撤单、注册单,oadrCreateReport、oadrUpdateReport、oadrCancelReport、oadrRegisterReport。注册系的注销通知与重注册请求,oadrCancelPartyRegistration、oadrRequestReregistration。外加一枚通用回执 oadrResponse。清单之外还有两条规则。第一,一拍只捎一件,信多,邮递员就多跑几趟,每趟手里只拿一封。第二,邮递员把信递到你手上,这一单生意还没完。收到派工单,你的表态得自己再跑一程送回去。规范管这个叫两阶段执行,第一程取信,第二程回话。
当年我拿着这些报文看不明白的时候,我们总经理跟我说过一句话。他说这是一个长会话,是基于 HTTP,用 pull 维持了一个长会话。后来他又补过一句。他说,我这个,作为聚合层中间商,一旦连上,除非出事,否则正常情况下应该一直保持轮询状态拉报文。
这两句话我记到今天,因为它把一个错觉说透了。那个错觉是,两台设备之间好像始终牵着一条线。其实那根线分两层。
下面一层是数据库。VEN 注册那天,VTN 就给它立了户。工牌,报名记录,报告排程,全写在 VTN 自己的记录里,不依赖任何一条连接。连接断了,户还在,货到站了照样压在柜子上。
上面一层是连接。每一拍 poll 都是一条全新的 HTTP 短连接,问完即拆。一秒钟一条,一秒钟一条生死。
中间联系双方的是请求-响应心跳。每敲一次信箱,上下两层就重新对上一次话。不敲,户还在,可你什么都收不到,会话还在数据库里,却断了气。

空说两层不够,跑一段给你看。下面这个演示件叫 PollClock,虚拟时钟一拍算一秒,9拍演完一场长会话的一生。
import java.util.ArrayDeque;
import java.util.Deque;
/**
* 第二十七夜演示件:PollClock(看信箱的钟)
*
* 演示 PULL 模式的长会话:连接条条短命,数据库长存,poll 是把两层缝在一起的那根针。
*
* 两层结构(与正文同源):
* 信道层:VEN 到 VTN 每拍一条 HTTP 短连接,问一句有没有货,用完即拆;
* 状态层:VTN 侧的数据库从注册那天起常驻(工牌、待办、排程),不信连接信户口;
* poll 每敲一次,两层重新对上一次话。
*
* 演示时钟:1 拍 = 1 秒(虚拟拍,不真睡)。真实世界通信频率 5 到 10 秒,注册那天商量好。
*/
public class PollClock {
/** 一件待办:VTN 数据库里压着的货 */
record Mail(String kind, String desc) {
@Override public String toString() { return kind + "(" + desc + ")"; }
}
// ———- VTN 侧:状态层,长存 ———-
static String venId; // 注册那天发的工牌
static int pollFreqSeconds = 1; // 注册那天商量的通信频率(演示取 1 秒)
static final Deque<Mail> pending = new ArrayDeque<>(); // 数据库:待办队列,断线不丢
// ———- VEN 侧:信道层,短命 ———-
static int clock = 0; // 拍号
static int trips = 0; // 短连接生灭计数:每拍一条,各自生死
static int delivered = 0; // 累计收到几件
/** 一次 poll:一条短连接的完整一生(发起、应答、拆线) */
static String pollOnce() {
clock++;
trips++; // 又一条短连接出生
if (networkBroken(clock)) {
return "连接拒绝(出事了)| 本拍空手 | 数据库未动,货还压着";
}
if (pending.isEmpty()) {
return "连接 OK | 邮递员捎回:无";
}
Mail m = pending.poll(); // 规范明文:一次 poll 只捎一件
delivered++;
// 两阶段执行:收到派工单,VEN 还要自己再跑一程,去 VTN 面前表态
String ack = m.kind().equals("派工单") ? " | VEN 第二程表态:好的" : "";
return "连接 OK | 邮递员捎回:" + m + ack;
}
/** 演示剧本:第 5 到 7 拍网络断了(当年日志里那段硬敲的缩影) */
static boolean networkBroken(int at) { return at >= 5 && at <= 7; }
public static void main(String[] args) {
// —- 第 0 拍之前:注册系。长会话从这天开始,但注册本身不占日常连接 —-
venId = "VEN_27";
System.out.println("[注册] 发工牌 " + venId + ",定通信频率 " + pollFreqSeconds + " 秒/拍,模式 PULL,数据库立户。");
// —- 开工前,数据库里压着一件派工单 —-
pending.add(new Mail("派工单", "18:00 起 1 小时,让空调各让两度"));
System.out.println("[数据库] 开工前压着 " + pending.size() + " 件待办。开始拍。");
for (int i = 0; i < 9; i++) {
// 第 5 拍:VTN 的数据库进了一件新订购单。进货不需要连接:
// 数据库是 VTN 自己家的柜子,你的网络断不断,它照样收货记一笔。
if (clock + 1 == 5) {
pending.add(new Mail("订购单", "订你 30 分钟一格的电表报数,明天起交"));
}
System.out.printf("第 %d 拍(第 %d 条连接) %s%n", clock + 1, trips + 1, pollOnce());
}
System.out.println();
System.out.println("[清点] 拍了 " + clock + " 拍,短连接生灭 " + trips + " 条,无一长存;"
+ "断线 3 拍,期间到站 1 件,一件未丢,恢复后第一拍补投。");
System.out.println("[结论] 连接是短命的,数据库是长存的,poll 是把两层缝在一起的针。");
}
}
跑起来是下面这样。
[注册] 发工牌 VEN_27,定通信频率 1 秒/拍,模式 PULL,数据库立户。
[数据库] 开工前压着 1 件待办。开始拍。
第 1 拍(第 1 条连接) 连接 OK | 邮递员捎回:派工单(18:00 起 1 小时,让空调各让两度) | VEN 第二程表态:好的
第 2 拍(第 2 条连接) 连接 OK | 邮递员捎回:无
第 3 拍(第 3 条连接) 连接 OK | 邮递员捎回:无
第 4 拍(第 4 条连接) 连接 OK | 邮递员捎回:无
第 5 拍(第 5 条连接) 连接拒绝(出事了)| 本拍空手 | 数据库未动,货还压着
第 6 拍(第 6 条连接) 连接拒绝(出事了)| 本拍空手 | 数据库未动,货还压着
第 7 拍(第 7 条连接) 连接拒绝(出事了)| 本拍空手 | 数据库未动,货还压着
第 8 拍(第 8 条连接) 连接 OK | 邮递员捎回:订购单(订你 30 分钟一格的电表报数,明天起交)
第 9 拍(第 9 条连接) 连接 OK | 邮递员捎回:无
[清点] 拍了 9 拍,短连接生灭 9 条,无一长存;断线 3 拍,期间到站 1 件,一件未丢,恢复后第一拍补投。
[结论] 连接是短命的,数据库是长存的,poll 是把两层缝在一起的针。
9条连接,条条短命,没有一条撑到第2拍。数据库从注册那刻常驻到底,断线那3拍里到站的订购单一件没丢,恢复后第一拍补投。这不是编排出来的童话,我在机器日志里亲眼见过同样的场面。2022 年 3 月底,认证窗口期,日志化石写着,09:06:57 起第3拍连接拒绝,之后每隔十几秒一次硬敲,73 秒里敲了7次,一次没歇;09:07:58 连接恢复,第一口气把断线期间积压的派工单补投齐了。没有丢一件货。救场的不是那段代码,是这套设计本身,货压在对方的柜子上,你只需要恢复之后继续拍门。
看到这里你多半会问一个我当年也问过的问题。这么严的规则,认证总该考一考吧,断线了怎么办,敲多快,货会不会丢?
我替你查过了,答案是不考。
物证先摆。认证过了,2022 年 2 月 27 日的提交记录里写着测试通过,项目根目录里躺着两份零字节的成考单,文件名就是收据,PULL 模式那份盖了章。而 517 条合格规则里,专门讲失败重试的那几条,没有一道专案考它。
为什么。这要从认证是什么说起。认证那张章担保的只有一件事,互操作。这台设备说的话是 OpenADR,能跟别家对话。两台都不做退避的设备,照样百分之百互操作,照样都该发章。语法考了,教养没考。
再往深一层,它其实考不了。53 道考题每题都要满足一条铁律,同样的输入,期望的输出唯一,跑一万遍结果一样。退避天生反着来,规范自己要求加随机抖动,同一场景两次跑,退避序列必然不同。要考它,考官得先动手掐线制造故障,掐完还得算恢复时机,考题的时长和可复现性全部崩掉。考卷上的题目里有一个现成的对照:过期证书有专案,证书到期本身是考题。同为故障,为什么它能考,因为它是确定的故障,时刻一到必然过期。断线退避是跟着时钟走的故障,什么时候断,什么时候好,没有唯一答案。考纲筛选的真判据不是重不重要,是可不可确定。
可是不考不等于不要紧。这个被免检的细节,攒到一定规模是要全线崩溃的。想象一台设备断网又恢复,立刻冲回去拍门,没问题,一台设备的机器对方一个人还能应付过来。如果一万台设备在同一秒断网,又在同一秒恢复,全部毫不犹豫的,瞬间齐步冲回来,每一台都在同一瞬间挤同扇门,这就是惊群,行业里叫 thundering herd。写过后端的人天天见它的表亲,秒杀开抢那一瞬间涌进来的请求,缓存集体到期那一瞬间全量回源的数据库。规范第 9.1.8 节把这场面写进了法条,VEN 必须加随机抖动,避免大量 VEN 在同一瞬间发起拉取,压垮 VTN。白纸黑字。VEN坐在聚合层中间商的位子上,底下挂的资源越多,这台报警器越响。
那正确做法是什么。规范写得很细,细到给它点了名。
【知识框】截断二进制指数退避。规范第 9.1.7 节,全册 517 条里唯一被点名出姓字的算法,truncated binary exponential back off。退避,backoff,是撞了门退回去歇一歇;静默期,quiesce,是歇的那段时间。做法,从大约 1 秒开始歇,上下各加10%随机抖动,第一次歇 0.9 到 1.1 秒;再失败,歇的时长翻倍,仍然加10%抖动,一直翻到某个上限为止。抖动让1万台设备的步子错开,翻倍让每次撞的时间间隔逐次拉长。
把三层摆在一起看。业务那层说,一直拉,别停。规范那层说,拉失败要退,越退越久。代码那层,当年那套实现是捕获异常继续下一拍,日志里 73 秒7次硬敲,间隔纹丝不动,规范要的退避一行都没有。总经理说除非出事,代码读成了出事也不停。
现在把另一条PUSH模式摆上来,跟 PULL 并排看。
PUSH 模式里方向倒过来。VEN 注册时把自己的收件地址报给 VTN,之后自己当收发室,开着门等着。事件一来,VTN 直接把报文提交到 VEN 门口,塞进门里。没有拍门,没有邮递员,没有空等,信直接上门。规范原文对两条路各有判词。讲 PUSH 那节先说这是 HTTP 上跑 OpenADR 最有效率的方式,紧跟着补一句,技术难点在于 VEN 可能躲在防火墙后面。讲 PULL 那节直接把代价列出来,延迟由每一拍的时间间隔决定,带宽被空拍的问询吃掉。
会话在两边的维持方式刚好互为镜像。PULL 是你续租,每拍敲一次门,会话续一拍。PUSH 是服务器查房,VTN 每推一次,推得动,说明门里有人;推不动,才去查你是不是搬走了。
那到底走哪条。两个判据。
第一,数据的快慢。PULL 的延迟下限是一拍的间隔,分钟级的遥测等半拍无所谓;秒级的高速数据等不起,按5秒一拍去取,取回的永远是陈货。第二,设备的地位。PUSH 要求设备配上一个可以被公开访问的地址,可真实世界的设备,恒温器,控制器,热泵,全躲在家庭路由器的地址转换(NAT)后面,只能自己往外打电话,外面进不来。推是特权,拉是本分,能被推的设备,得先配上一个随时能访问的地址。
当年那份接口设计本来就两册。低速册走 PULL,报文目录里摆着 oadrPoll,15份报告,1 到 30 分钟粒度的常规遥测与计划。高速册走 PUSH,oadrPoll 从目录里消失,只剩性能数据和状态两样,秒级的货。同一份契约,两门派各管一摊。后来重写的那套代码,两种模型都建了,测试各跑各的,互为对照。
这一夜走了很远,从四个字母走到两条路。回头看,每一件大事在注册那天就定了。工牌在注册时发,通信的频率在注册应答里约好,收件地址在注册时报上去,连你要不要当那间收发室,也是注册那天递上去的名片上写好的。四系是一场关系的四种动作,注册系是这场关系的出生证明。认识的那个时刻,决定了后面每一拍。
所谓长会话,真正持续长时间的从来不是连接,是数据库里那个一直没被注销的名字。连接管一拍,数据库管一生。
① 真伪核对
② 术语对照
| EiRegisterParty | 注册系 |
| EiEvent | 事件系 |
| EiOpt | 参与系 |
| EiReport | 报告系 |
| oadrPoll | 看信箱 |
| PULL | 拉模式 |
| PUSH | 推模式 |
| piggyback | 捎带 |
| two-phase execution | 两阶段执行 |
| quiesce | 静默期 |
| backoff | 退避 |
| truncated binary exponential back off | 截断二进制指数退避 |
| jitter | 抖动 |
| thundering herd | 惊群 |
| NAT | NAT 转换 |
③ 今夜习题
Ⅰ 退避实现。给 PollClock 补上规范第 9.1.7 节的退避,从 1 秒开始,加正负10%抖动,失败逐次翻倍,封顶 60 秒。把断线3拍那段剧本改成带退避的版本,看看恢复之后成功的那一拍落在第几秒。
Ⅱ 报文分家。把下面几个报文归到四系里去,oadrCreatedEvent,oadrCancelOpt,oadrRegisterReport,oadrRequestReregistration。藏了一道弯,oadrRequestEvent 是 VEN 主动去问有没有单子,方向朝 VTN,它归哪一系。提示,系是报文的家族,方向是这一趟谁先开口,两回事。
Ⅲ 母菜单点菜。规范第 11.5 节有四样没端上桌的菜,EiQuote,EiEnroll,EiAvail,EiMarketContext。查一查它们各自想干什么,再想想电网为什么暂时用不上。开放题,没有标准答案。
本书目录导航 上一篇:算法一千零一夜 · 第二十六夜 · 协议的母语 下一篇(载锅中) 起点:卷首语·算法童话
版权声明:本文为作者原创,受著作权法保护。未经授权禁止转载、搬运、摘编、改编及任何形式的二次创作;个人学习引用请注明作者与原文出处。转载授权请联系作者(CSDN 私信)。侵权必究。
网硕互联帮助中心





评论前必须登录!
注册