反向代理后面,应用如何知道自己的公开地址
卷中目录
应用跑在本机端口上时,它自己收到的连接往往来自同一台机器上的 Web 服务器。如果只看这条连接,协议会像是普通的未加密 HTTP,主机名也可能是内部名字。访客实际使用的是带证书的公开域名。中间少了哪一项说明,登录跳转、资源地址和站点地图都可能指错地方。页面有时仍能打开,因为相对链接不在乎主机名;等到一次绝对跳转,人才发现自己被送到了一个外部无法访问的地址。
这种错位会躲过“首页能打开”的那一次检查。相对链接跟着地址栏走,文章、翻页和返回都像是正常的。应用一旦要自己写出完整网址,就只能依赖它所相信的协议和主机名。那一组值若仍是内网连接上的事实,复制出去的链接、信件里的链接和站点地图就会一起指错。入口的工作,是把访客真正使用的协议和主机名交进去;应用的工作,是只在这些值来自入口时才采信。至于什么时候让入口把人送过来,要等应用自己说明已经可以接待,而不是等进程还活着。
本篇目标: 让反向代理把公开访问的协议和主机名告诉应用,并在健康检查通过之后再对外开放。

先等应用自己说准备好了
端口已经接受连接,并不等于这个站点可以交给访客。进程刚起来的时候,表可能还在建立,第一份配置可能还停在向导里,甚至数据库都还没有答应连接。这时候若把公开名字指过来,人看见的会是半成品,或者一张只有坐在机器旁边才填得明白的安装页。半成品一旦被打开,就不再只是你自己的启动问题。读者会刷新,编辑会试着登录,向导会把第一份站点地址写成当时那个临时入口。
比较稳的顺序是:先向应用自己提供的就绪地址发请求,连续几次都成功,再把入口里的站点切到它。就绪地址只给本机或只给入口用,不必出现在公网能走到的路径上。它回答的是“现在能不能接待”,不是“请把安装过程展示给路人”。切换的动作落在入口的上游配置上。公开的域名可以早就指向这台机器,只要入口在就绪之前仍把这个主机名送到原来的去处,或送到一段你写好的暂缓说明。
以一个公开书目站为例。就绪不该只是套接字通了。至少应该能读到一条已经发布的书目;若确实还没有内容,就返回你事先约定的、稳定的空架状态,而不是向导,也不是一段堆栈。你若把向导页面当成就绪,入口会很诚实地把第一批访客送进安装流程。空架状态要能和故障页分开:它是一次成功的读取,只是书架上还没有书;故障页是读取本身没有完成。这两件事用同一个宽泛的“有响应”来代表,后面就无法决定能不能接流量。
因此,就绪的合同要在接流量之前写好,并且用一次真实的读取来兑现。合同里写明路径、成功时的样子、超时算失败。不要把“进程已经听端口”写进去。听端口是更早的一步,说明请求有地方可去,并不说明书目已经能被读到。等这一份合同连续成立,再改入口。顺序反了,你就是在用访客的第一次访问来完成自己的启动。
进程还在,只说明它没有退出
看进程表时,最容易把“还在”误认为“能用”。进程没有退出,只说明监督它的程序仍握着一个编号,或者它自己还没崩溃。它可能正卡在建表,可能在等数据库接受连接,也可能在创建第一个管理人员。这些时候它往往仍然活着,日志也还在增长。增长本身只说明有人在写日志,不说明下一行会是成功。
更迷惑的是反复拉起。应用一连不上数据库就退出,监督程序立刻再启动它。你每隔几秒都能看到一个新的进程,于是像是“它在工作”。访客若在这个窗口被送进来,会赶上一次没有完成的启动。有的向导允许当时打开页面的人把站点地址和管理职责一起写下。你再想换成正式内容,就得先分辨哪些行是那次半截安装留下的,哪些是后来的人真正改过的。分辨往往做不到,因为两边写进的是同一张表。
监听也会骗人。有的程序先打开端口,再做迁移。端口能连上,请求却挂着,直到迁移结束才返回。检查若只试探端口,会在迁移中途就宣布可以接客。应当让应用在迁移和自检都结束之后,才对就绪地址返回成功。做不到这一点,就不要把“端口已开放”写进切换条件。你可以用一个很短的超时去访问就绪地址:挂住的迁移会表现为超时,而不是表现为已经可用。
所以至少要准备两种不同的问题,并且不许互相代替。第一种问监督程序:这个进程是不是已经死了,该不该被重新拉起。第二种问入口:新的访客是不是该被送进来。两个问题都用“进程还在”来回答,就会在该等的时候重启,或在不该接的时候接客。重启毁掉的是一次本来会完成的迁移;接客毁掉的是一份还没定稿的初始数据。两件事看起来都像“站点不稳定”,根子却是把两个问题合成了一个。
存活和就绪要交给不同的人
存活检查适合交给拉起进程的一方。它回答的是:这个进程是否还应该继续占用名额。若它退出、死锁,或不再响应一个极轻的探测,就可以按你写好的间隔重新拉起。这个探测要便宜,而且在漫长但正常的初始化期间也应该保持成功,否则一次本该完成的建表会被误当成崩溃,然后被杀掉重来。重来之后,建表又要花同样久,探测再次失败,机器就忙着重启,而不是忙着完成启动。
就绪检查适合交给入口。它回答的是:现在把一个真实访客送进来,会不会看见半成品。初始化没做完、依赖还不可用,或者进程自己知道正在收尾时,就绪应该失败。入口看到失败,就继续使用原来的去处,而不是把这个上游算进可服务的名单。入口不负责拉起进程,也不该因为就绪失败就去重载自己的全部站点。失败只表示“先不要把人送过来”。
两条路径可以都在本机端口上,用不同的地址分开。不要让入口去打存活地址,也不要让监督程序把一次短暂的依赖失败当成进程已死。依赖抖动时,该变化的是“送不送新访客”,不是“把进程杀掉重来”。杀掉重来会把一次短暂的数据库停顿放大成一片启动风暴,日志里全是启动句子,真正的停顿原因被挤到上面。
书目站可以这样约定。存活地址只返回一个固定的短句,表示进程还能调度自己的代码,不去碰数据库。就绪地址则确认它能完成一次对书目的读取。读取失败时,进程仍然留着,入口暂停把新请求送过来。等读取恢复,再连续成功几次,才重新纳入。固定短句不要带版本、路径或内部编号。它不是给访客看的页面,只是给监督程序的一个稳定信号。信号里的信息越少,以后越不容易因为一句说明改了措辞而误判为进程已死。
一次成功不够,要看一小串
刚启动的程序会在失败和成功之间闪一下。连接池第一次借到连接,下一次又借不到;迁移刚提交,下一次自检还看见旧的结构。若入口把第一次成功就当成可以切换,访客会正好落在下一次失败上。从外面看,这像是“刚打开就坏了”,于是人去改入口或改证书。其实只是采样采在了缝上。
可以要求连续成功,比如隔开几秒的三次。任何一次失败就把计数清掉,重新开始。次数不必大。大了只是把切换拖得很慢,并不能证明更多的事情。它的作用是滤掉单独的一次侥幸。间隔用来确认这几次成功不是同一个瞬间里的重复采样。若三次请求挤在同一秒,应用可能还在用同一份刚刚建立、马上又会丢掉的连接。
检查自己也要有短的上限。应用若已经挂住,探测不该跟着挂住,否则你关于“要不要切换”的判断会一直等。超时记作失败,计入那次连续计数的清零。探测里不要写数据,不要发信件,也不要扫描全部书目。一次全表扫描在书目变多以后会自己把就绪打失败,入口就开始无故撤掉流量。探测应当是你在合同里写过的那一次轻读取,并且每次都读同一类东西,这样慢下来时你知道慢的是这条路径,不是探测方式又变了。
切换之前看的是这一串连续成功,不是某一条日志里的“启动完成”。日志句子会改,也会被写成不同的说法。就绪地址是你和入口之间的合同。合同的正文要稳定:成功就是成功,失败就是失败,不要用一段会变的说明文字来让入口做判断。入口只认状态,人再去读日志。把这两件事绑在一起,日志一改措辞,流量策略就跟着漂。
数据库先通过自己的检查
应用依赖数据库时,数据库没准备好就先启动应用,得到的通常是一长串连接失败。监督程序若再负责重启,日志就被这些重复的失败淹没。你后来真正要找的那一行,比如目录不可写、监听的端口和你探测的不是同一个,会被卷到上面去。反复重启也不能让数据库更快准备好。它只是让应用更频繁地发现同一件尚未改变的事。
让数据库先回答它自己的检查,再允许应用的进程启动。最起码,它能接受连接,并完成一次几乎不读业务数据的询问。这一步通过,只说明数据库这个程序已经能说话。顺序要写在启动配置里,而不是靠人看着日志去点第二个程序。人会累,也会把“刚才那一次好像连上了”记成已经可以。配置里的顺序每次都一样,不依赖你当时有没有盯着屏幕。
数据库能连接,还不等于书目站需要的结构已经存在。你可以选一种做法并写死,不要两头都占。一种是单独的迁移先跑完,成功之后才启动那个会接待读者的进程。另一种是接待进程自己在启动时做迁移,但在迁移结束前不得对就绪地址返回成功。两种都能工作。混在一起就会出现这样的窗口:迁移跑了一半,就绪已经为真,入口把读者送进来,读到的是还没合上的结构。
不要用应用的重启来代替等待。等待属于启动顺序和就绪合同。应用在数据库拒绝连接时立刻退出,只适合作为很外层的保护,不适合当作每秒一次的常规节奏。常规节奏应当是:数据库的检查没过,应用根本不被拉起;应用被拉起之后,再用自己的就绪告诉入口能不能接客。三件事分成三步,日志也就分成三处,不会搅在同一个文件的开头。
启动之后依赖又断开,不要靠杀掉进程
启动顺序只约束开机的那一段。到了下午,数据库仍可能拒绝连接、慢到超过上限,或只允许读、不再允许写。这些情况不该自动变成“把应用进程杀掉”。进程往往是好的,坏的是它身后的依赖。杀掉它不会修好数据库,只会在依赖恢复之后再付一次启动的时间,并且在恢复之前制造一串无用的启动失败。
这时该改变的是就绪。读者要走的那条路径已经不能完成时,就绪失败,入口停止把新的读者送进来,可以改为一段你事先写好的、平静的暂不可用说明。说明由入口给出,避免每个应用各自吐出一段没有收拾过的错误。已经连着的请求让它们按自己的上限结束,不必为了一次短暂失败把所有人同时切断。切断看起来像是处理果断,实际是把一次依赖停顿变成全体访客的中断。
要给失败一个很小的预算,避免一次短暂重连就把站点从入口摘掉。连续失败到了预算,再标记未就绪;恢复时同样要求连续成功,和启动时用的是同一条合同。这样抖动不会变成入口上的来回切换。预算和间隔都写在入口一侧,不要散落在人的记忆里。下一次有人把间隔改得很短,站点就会随着每一次正常的停顿闪烁。
读和写可以分开看。书目站对读者主要是读。若你把一次后台写入失败也当成整体未就绪,一段整理工作就会让整个站点从入口消失。更清楚的做法是:就绪跟随你真正要接待的那条路径。写路径的失败记进日志,并让会写的那几个动作自己拒绝,而不是把读也关掉。若这个站点的每次访问都必须写,再把写放进就绪。这个选择要写下来。没有写下来的时候,人们会在一次写入失败之后,不假思索地让整个站点一起退出服务。
等待要有上限,到点就去读应用日志
连续等待看起来像谨慎。没有上限,就会把“卡住了”藏成“还在启动”。迁移若真的需要更久,应该是你看过日志、确认它在往前走,然后有意把这一次的上限提高,并记下为什么。不是每次失败都把数字加倍。加倍会变成习惯,习惯会让一个真正停住的迁移看起来仍在被认真对待。
上限到了仍未就绪,就去读应用自己的日志,不要先重载入口。入口的规则没有错时,重载只会让你以为问题出在站点文件上。你开始改主机名、改上游端口、改协议头,真正的原因却是应用根本还没准备好被问。入口被改过之后,即便应用后来就绪了,你也不再知道现在的转发是不是原来那一份。故障从一个变成两个。
日志里常见的情形要分开认。连接被拒绝,多半是数据库还没听,或应用听的端口不是你探测的那个。口令不被接受,或数据目录不能写,则是配置和权限,重启多少次都一样。同一句错误每隔几秒重复出现,就不要再加等待。句子在变化,并且明确写着正在继续迁移,才考虑把这一次的上限放宽。放宽时写下你看见的那句还在前进的话,而不是只写下“再等一会儿”。
改完应用这一侧,再重新开始那段带上限的等待。不要边改入口边等。等到就绪连续成功,再回到入口做入口该做的检查。两件事叠在同一次操作里,下次失败时你不会知道该读哪一份日志。等待本身也应该被记一笔:开始的时间、上限、最后一次失败的原因属于应用还是属于数据库。没有这一笔,人会觉得自己等了很久,其实只是在来回重载入口。
内网这一跳看不见访客用的协议
访客和入口之间可以是加密的,入口和应用之间却常常是同一台机器上的普通 HTTP。加密在入口结束,是为了让证书、续期和对外跳转集中在一处。应用若根据自己看见的这一跳来判断,它会以为所有人都在用未加密的协议访问一个内部名字。这个判断在内网里是诚实的,拿到公网的链接里就是错的。
绝对链接于是会被写成未加密的地址。需要加密连接才送出的会话信息,也可能被标成不必加密。浏览器在真正的加密页面上会丢掉这样的信息,下一次请求看起来像是没登录过。人会觉得账号有问题,去重置、去清缓存、去再注册一次。问题不在账号,而在应用对这一次访问的协议判断。它看见的是入口身后的那一跳,不是访客地址栏里的那一跳。
入口必须把外面的事实译成应用能读的说明,应用也必须被明确告知该相信这层入口。只改应用、不改入口,应用仍然无值可采。只改入口、不改应用,头已经送到,应用却继续使用内网连接上的协议和主机名。两边要一起对上。对上的标志不是配置里出现了某几行,而是应用自己报出的协议和主机名,与访客地址栏里的协议和主机名一致。
在对上之前,不要先开放流量,也不要先相信“页面已经能显示”。能显示,常常是因为页面用了相对路径。相对路径不向应用询问协议,所以它无法替你证明协议已经被交还。证明只能来自那些必须由应用自己拼出来的地址:登录之后的落地、信件里的链接、站点地图里的一条、页面源代码中的规范地址。这四样在后面各自有检查。这里先记住:内网这一跳的诚实,不能代替公开那一跳的说明。
三个头要写成同一句话
转发时常用三个字段。Host 放在入口这一跳的请求里,表示入口希望应用看见的主机名。X-Forwarded-Host 用来留下访客原来那个主机名。X-Forwarded-Proto 用来留下访客连接入口时使用的协议。应用的框架不一定读同一个字段。有的只看 Host,有的要等你打开“信任入口”之后才看后面两个。你要先知道这本书目站读的是哪一个,再让入口把那一个写对,并把另外两个写成不与之矛盾的值。
X-Forwarded-Proto 说的是访客连到代理时的协议,常见的就是加密或未加密。它不是应用和入口之间那一跳的协议。那一跳经常是未加密的,正因为加密已经在入口卸下。把这两跳混成一个值,应用就会以为公开站点也是未加密的,于是绝对链接、会话标记和资源地址一起退回去。MDN:X-Forwarded-Proto
有的入口默认不传这些字段。有的会传,但应用要单独被配置为采信,否则仍然只看套接字。所以“入口已经写了”和“应用已经信了”分成两次确认。只看入口的配置文件,会漏掉应用那一侧的开关;只在应用里打开信任,又会在入口没写头时去相信一个空值,然后退回内网事实。练习一节会让应用把采信之后的结果打印出来。在那之前,不要假设框架的默认刚好适合放在反向代理后面。
访客 --加密的公开访问--> 网站入口 --本机上的普通 HTTP--> 书目站
入口写明:主机名是访客正在用的公开名字,协议是加密的
书目站只在请求来自这层入口时,才采纳这两项
三个字段写成同一句话,是为了让不同的框架撞上同一件事实。今天这本书目站读 Host,下一次换了一种读取方式去读 X-Forwarded-Host,若两个值本来就一致,链接不会突然改道。若你只填了其中一个,另一次读取就会把内部名字或未加密的协议重新带回来。一致性不是重复劳动,是为了防止“我们明明转发过”这句话落在框架没有读的那个字段上。
主机名是留下,还是收成一个
Host 会被一些入口改成上游的地址,好让应用始终生成同一个名字。这在你已经决定只公布一个名字时有用。可如果你什么都没决定,这种改写会静默发生:地址栏还是访客输入的那个名字,应用生成的链接却全变成入口替它选的另一个名字。首页因为相对链接而看起来正常,人要等到复制链接或登录,才看见名字变了。
X-Forwarded-Host 更接近访客请求里原来的主机名。它之所以存在,就是因为 Host 经过代理之后可能已经不可靠。若应用读的是前者,而你只设置了后者,或者相反,两边都会看起来“主机名已经转发”,生成的绝对链接却仍然是错的。先确认应用采信的是哪一个,再谈政策。不确认就改,等于在两套名字之间掷骰子。
政策本身只需要一句。可以是:对外只承认一个名字,入口把相关字段都写成这个名字,需要时再让浏览器跳过去。也可以是:根域名和 www 都允许停在地址栏里,入口把访客原来的主机名原样交进去,不做改写。两种都能工作。不能工作的是两个字段各写各的,或者入口在改写,应用又按另一个字段生成链接。那样访客会在同一次浏览里见到两个名字,并且不知道哪一个才是会被记下来的。
改完之后,用两个名字各请求一次,看应用打印出来的主机名。若政策是“都留下”,两次打印应当分别等于两次地址栏。若政策是“收成一个”,两次打印应当都等于那一个名字,并且浏览器最终也停在它上面。不要只试你自己习惯输入的那个名字。习惯会掩盖另一个名字上的改写。另一个名字往往是旧链接、别人代为输入、或带 www 的那一个,它同样会进入登录和信件。
改写不会让浏览器更换地址
跳转是入口或应用告诉浏览器:请再用新的地址来一次。地址栏会变,人也看得见。改写只发生在入口和应用之间。浏览器仍显示原来的地址,应用却收到另一个主机名,于是它后面生成的链接、会话范围和规范地址,都按那个被改写的名字来。人停留的地方和被记录的地方因此分开。分开不一定是错,但必须是你想要的那一种分开。
这两种动作可以共存,不要用其中一个假装完成了另一个。你若希望地址栏最终只留下一个名字,就必须有一条浏览器能看见的跳转。你若希望地址栏保持不动,就不要在应用里再安排一条绝对跳转,把人送去基准名字。只改写、不跳转,地址栏是稳的,复制出去的链接却会集中到另一个名字。只跳转、不改写,浏览器走到了新名字,应用若仍按旧名字生成下一跳,又会把人送回去。
循环常常由此而来。入口把未加密的访问送到同一主机名的加密地址。应用却认为公开协议仍是未加密,于是再把人送回去。两条规则各自看都合理。放在一条路上,浏览器就在两个地址之间来回,直到它自己停止。这种循环不一定出现在首页。首页多是相对链接,不触发应用的绝对地址。登录之后的落地、退出之后的返回,更容易带上完整网址,循环就在那里出现。人会说“只有登录坏了”,于是去查账号,而不去查协议字段。
检查时要看最终停住的那个地址,而不是第一下的状态。第一下可能是入口正确的加密跳转,第二下才是应用错误的送回。只记录第一下,你会以为已经完成。把中途每一跳的地址都记下来,下一次循环复发时,你能看出是入口先送的,还是应用先送的。这个记录比“清一下浏览器”有用。清掉之后循环还在,只是你失去了刚才那一串地址。
路径若带有前缀,也要先说好是谁加上的。入口把某个公开路径转到应用的根上,应用却以为自己的整个世界都带那个前缀,链接就会出现两层前缀。反过来,应用生成的链接没有前缀,样式和表单会落到入口自己的根上,拿到另一份站点的页面。前缀是主机名之外的第二处合同。它和协议、主机名一样,要在入口和应用两边写成同一句,并且用一次真实的样式地址来核对,而不是只看首页的文字。
这些字段只有入口写的才算数
X-Forwarded-Proto 以及与主机名有关的转发字段,本身只是请求里的文字。任何能把请求直接送到应用的人,都可以写下同样的文字。应用若无条件采信,直连的人就可以声称这次访问是加密的,或者声称主机名是另一个站点。接下来,绝对链接、跳转位置,以及会话该不该标记为只在加密连接里送出,都会照着这句未经证实的话走。链接会指向他写的那个名字,人再从你的站点被带到别处。
所以采信的前提不是这些字段的名字看起来正式,而是不该写它们的人根本到不了应用的端口。入口是你允许的写信人。应用只采纳来自这层入口的值。入口在写的时候应当覆盖同名字段,而不是把访客已经带来的值接在后面。覆盖之后,应用看见的是单独的一个协议、单独的一个主机名,没有一串需要猜测该信哪一端的文字。接在后面看起来像是保留了全过程,实际是把判断推给应用。应用若取了访客写的那一端,入口的正确值就白写了。
若前面还有另一层你同样掌管的代理,要事先定好谁有权写下最后的值,并让书目站只相信那一个来源。没有画清链路时,不要打开“相信所有转发过来的字段”。那等于把协议和主机名的决定权交给链路上任何一个能添加文字的环节。你能掌管的通常只是自己的入口。入口覆盖,应用只信入口,链路就收成一段,而不是一串互相推诿的名字。
标准的 Forwarded 字段也表达协议和主机,规则并不因此放宽。名字换了,仍然只是一段由代理填写的说明,仍然只有在填写者是你的入口时才可信。书目站若两种字段都可能读到,就让入口把它们写成同一个协议、同一个主机名,避免一个字段说加密、另一个字段说未加密。应用里则明确只启用一种读取,免得两次生成链接用了两套事实。
应用的端口不要对公网开放
把书目站绑在本机,或绑在只有入口能够到达的那一侧。公网只需要看见入口正在听的端口。读者、编辑和偶然扫过来的连接,都不该直接打到应用自己的端口上。直接打到,就会同时绕过证书、绕过你设计的跳转,也绕过“只有入口能写协议和主机名”这一条。后面的信任设置因此全部失效。应用会把直连者写成的字段当成访客的真实地址。
绑定范围是第一道,防火墙上的收口是第二道。只靠第二道时,一次为了看向导而临时放开的口子,常常会留到第二天。绑定则是应用自己的选择:它根本不在对外的接口上听。两道都做,检查时却要分开做。绑定错了,从本机以外访问应用端口不该成功。防火墙有缺口时,即便应用听得比较宽,从公网仍然不该连上。两道都声称自己没问题,就要真的从外面试一次端口,而不是只读规则的文本。
为了看安装页面而把端口对所有接口打开,是最常见的遗留。人在自己的电脑上访问不到只听本机的端口,就把监听放开,看完向导又忘了改回去。向导应当经过入口、用最终的公开名字来完成;或者只在你已经坐在这台机器旁边时,通过本机完成。不要为了少走一步,把端口暴露到公网,再指望自己记得收回。收回之前的每一分钟,信任设置都是反过来的:谁能连上,谁就能规定应用眼中的协议和主机名。
验收用三句就够,而且三句缺一不可。从另一台机器连接应用端口,应当失败。从这台机器经由入口打开公开名字,应当成功。从这台机器直接访问应用,仍然应当成功,否则入口自己也无法把请求送进去。第三句是为了防止绑定收得太窄,窄到入口都到不了。前两句防止暴露,第三句防止你把站点修成入口也无法转发的样子。只做其中一句,都会留下一种假的安心。
同一台机器上其余已经在服务的站点,也不需要这个应用端口。它们若要链到书目站,应当走公开名字,和读者走同一条路。内部互相直连看起来省掉了入口,会养成一套不经过协议字段和主机字段的调用。等到你检查绝对地址,这些调用仍在使用内部名字,并且不会出现在你用浏览器做的那次验收里。把它们也改到公开名字上,或者明确它们只是机器内部的管理动作、永不把地址写给访客。
会被带走的链接用外部基准地址
书目站通常还要一个外部基准地址。它不随某一次请求变化,用来生成站点地图、订阅源,以及信件里那些离开了浏览器才能被打开的绝对链接。这个地址选根域名或 www 都可以,但必须和证书里覆盖的名字、以及你决定的跳转规则一起看。两个主机名都能把页面打开,并不等于每一条由应用生成的链接都会保留访客刚刚使用的那个名字。
基准地址写进应用的站点设置,而不是写进每一篇正文。正文里能用相对链接的地方继续用相对链接。必须写出完整网址的地方,才去读这一份基准。若每篇文章各自写死一个名字,改政策时你要搜完整站,而且总会漏掉一封已经发出去的信件模板。集中在一处,改的时候才知道自己改的是“被引用时使用的名字”,不是某一次浏览的地址栏。
选定之后,到页面源代码里抽查几条绝对链接,看它们是否都落到这个名字上。再打开站点地图里的前几条,以及一封测试信件里的链接。三处不一致时,说明还有第二条“隐藏的基准”:也许是向导里留下的旧值,也许是某个模板自己拼了主机名。把第二条找出来废掉,而不是在入口再加一条跳转去掩盖。跳转能把人送回来,却不能让已经复制出去的地址在对方的记录里自动改写。
基准地址里不要带本机才有的端口,也不要带只有这台机器内部才认的名字。它是给站外的人用的。带上内部端口之后,所有地图和信件都会继承这个端口,读者在自己的网络里打不开。你在服务器上点击却能打开,因为那个端口就在你身边。这种“我这边可以”会把错误留到第一位真正的读者身上。
这一次请求沿用当前主机名
和基准地址相对的,是当前这一次请求的主机名。它只服务还没离开这次访问的动作:翻到下一页、回到刚读的那一条书目、登录后仍停在读者进来时的那个名字、在根域名和 www 之间不强迫人换边。这些动作应当使用入口交进来的、并且已经被采信的当前主机名,或者干脆使用相对路径,让浏览器自己保持地址栏。
相对路径是更省事的一种。表单的提交地址、样式的引用、文中的下一篇,只要不离开这个站点,就不必由应用拼出协议和主机。应用少拼一次,就少一次把内部名字写出去的机会。绝对地址应当留给那些会被复制、被收录、被放进信件的场合。同一页里两种链接并存是正常的。不正常的是你说不清哪一种用在哪一种场合,于是把基准地址用进了翻页,或把当前主机名用进了站点地图。
若你的政策是“两个名字都可以留下”,登录和翻页就不该把人送到基准名字。读者从 www 进来,登录之后仍应停在 www;从根域名进来,就停在根域名。基准名字只出现在他复制链接、或搜索引擎读取地图的时候。若你的政策是“最终只留一个名字”,那就用浏览器看得见的跳转来完成,并且全站一致,而不是有的页面跳、有的页面不跳。半套跳转会让人觉得站点在两个地址之间不稳定。
判断某条链接该用哪一种,可以问一个问题:这个地址离开当前浏览器之后,还有没有人要打开它。要,就用基准。不要,就用相对路径或当前主机名。这个问题比“尽量写完整网址”有用。完整网址看起来整齐,却把一次普通的翻页冻成了生成那一瞬间的主机名。入口的政策若后来改了,这些冻住的链接仍指向旧决定。
没有当前请求时,只能读基准地址
有些工作根本没有访客。夜里生成站点地图、清晨寄出一封新书目的摘要、给已经发布的条目补上一条可供站外引用的图址,这些任务在运行时没有地址栏,也没有入口交进来的主机名。代码若在这种时候去读“当前请求的主机”,读到的会是空的,或者是进程启动时留下的内部名字和本机端口。
空的时候,程序常常退回到一个开发时能用的默认值。那个默认值在你自己的机器上能打开页面,写进地图和信件之后,站外的人打不开。更麻烦的是它不一定报错。任务显示完成,文件也生成了,只是里面的每一条链接都指向一个外部不存在的地方。你若只检查任务有没有失败,会以为地图已经更新。
所以这类任务只读外部基准地址,不读请求,也不读监听端口。测试时要在没有传入任何请求的情况下跑一次,然后打开它生成的文件,看前几条链接的协议和主机名。应当是加密的公开名字,不带内部端口。若跑的时候必须伪造一个请求才能得到正确链接,说明代码仍然依赖当前请求,只是被测试挡住了。把依赖去掉,而不是把伪造的请求留在定时任务里。
也别让这类任务去“自己访问首页再抄地址栏”。那会把定时任务变成一个内部的访客,它看见的主机名取决于它怎么访问自己。它若直接打应用端口,抄到的又是内部事实。基准地址就是为了让没有访客的代码不必假装自己是访客。假装一次,就会在某次运行里假装错。
初始化向导里不要填本机端口
第一次打开书目站时,向导常常要你填写站点地址。人很容易填当时用来打开向导的那个地址:本机的名字,再加一个只有这台机器上才听着的端口。向导会把这行字存进站点设置,以后的后台跳转、预览和信件都从这里取。填错的当时页面还能用,因为你确实就在本机。等到从公开名字进入后台,跳转会把你带到那个外部无法访问的位置。
应当填写最终的公开地址:加密的协议,加上你选定的那个公开名字,不带内部端口。填完先不要忙着发布书目。用公开名字走一遍登录,确认每一次跳转都还留在这个站点上,地址栏没有出现本机端口,也没有出现另一个你不认识的主机名。这一遍要在把流量正式指过来之前做完。不要等第一位真实的编辑帮你发现后台出不去。
有的向导只在第一次启动时出现。当时若填了临时地址,后来就得在站点设置里改掉同一份值,而不是再找向导。改完要确认应用是否把旧值留在内存里。若设置要等进程重新加载才生效,就在重新加载之后再走一遍登录。只改了保存按钮、没有重新加载,你会看见设置页面上已经是新地址,实际跳转仍用旧地址。两处不一致时,以跳转落到的地址为准。
同一份值还会出现在信件样例和规范链接里。设置页面改对了,若样例信件仍写出本机端口,说明信件走的是另一处配置,或者用的是生成那封样例时的旧缓存。把样例再生成一次。仍不对,就回到“没有当前请求的任务”那一节:它可能没有读你刚改的基准,而是读了向导留下的另一份副本。副本要删到只剩一份,否则下次向导不会再出现,副本却一直在。
在机器上请求自己,代替不了从外面打开
坐在服务器上用命令去访问应用,和访客从自己的网络打开公开名字,中间隔着入口、证书和解析。命令若直接打到应用端口,请求没有经过入口,应用就看不到那些由入口填写的字段。它会诚实地说:这次是未加密的,主机名是本机。这个结果可以用来对照,不能用来宣布站点已经配对。命令返回了页面,只证明进程会回答,不证明入口和应用对公开地址的理解一致。
同一台机器上还有一种请求:去问本机的入口,并带上公开的主机名,但不经过外面的域名解析。它检查的是入口规则和上游端口。证书可以暂时不参与,因为你还没有走加密那一层。这种请求能发现主机名被送到了错误的上游,或被默认站点接走。它仍然不证明外面的人解析到了这台机器,也不证明证书里有这个名字。
第三种才是从另一台网络上的机器打开公开名字。它同时经过解析、证书、入口和应用。四件事任何一件失败,表现却可能都是“打不开”或“打开了但不该是这一页”。所以不能用第三种的失败去同时改四层,也不能用第一种的成功去代替第三种。三种请求回答三个问题,要分别记下来:应用自己怎么说,入口怎么转,外面能不能走到。
还有一个容易混进去的做法,是在服务器上把公开名字临时指回本机再访问。这样绕开的是外面的解析,得到的结果接近第二种,却常常被人记成“我已经用公开名字打开过了”。记录时写上请求是从哪里发出的、有没有经过入口、有没有经过加密。不写这些,过几天你只会记得“当时能打开”,却不知道打开的是哪一种。
失败时先看停在哪一层
整条路可以分成四层:应用自己、入口规则、证书、公开名字的解析。每一层都有一个只属于它的检查。不要用下一层的失败去改上一层,也不要在还没判断层次的时候同时改两层。同时改,最容易得到一个“终于好了”的结果,却不知道是哪一次修改生效的。下一次复发,你仍然不知道该看哪里,只好把上次改过的地方再改一遍。
先用一层的检查把故障钉住,再打开那一层的记录。应用这一层失败,就读应用日志和数据库,入口文件先不动。入口这一层失败,就看站点规则和它要转发去的端口,证书先放着。证书这一层失败,就看证书里的名字、期限和入口里写的证书位置,不要去改书目站的业务设置。解析这一层失败,就看域名记录指到了哪里,不要重启应用。重启应用不会改变外面把名字送到哪一台机器。
钉住层次之后,还要防一种跨层的错觉。浏览器上的安全警告看起来像证书问题,有时是你打开的根本不是这本书目站,而是解析还指着旧地方,旧地方的证书当然对不上你预期的名字。这时去换新证书没有用。反过来,页面能开但登录跳去了本机端口,证书和解析都是好的,该看的是应用里的基准地址和它采信的主机名。症状出现在浏览器里,原因不一定在浏览器接触的那一层。
把层次写成一张可以对照的短表,放在你做变更的笔记旁边,而不是只放在记忆里。短表只有四行,每行是:这一层的检查怎么做、失败时长什么样、此时不许改哪一层。真正排障时按表走。人在着急的时候会跳层,表的作用就是让跳层变得显眼:你若在解析失败时重启了应用,笔记上会出现一行不符合表的动作,下次就能避免。
应用这一层:本机上的就绪和一页正文
应用这一层不经过入口。从这台机器直接访问就绪地址,再直接访问一页你知道内容的正文。就绪应当按合同成功。正文应当是书目站的句子,而不是入口的默认页,也不是向导。若连接被拒绝,先核对进程是否在听、听的是不是你访问的那个端口,而不是去改入口里的主机名。端口不一致时,入口以后也会把人送到一个没有人听的地方。
若就绪一直失败,就停在日志里,按前面的上限去读,不要重载入口来“促一促”。日志若显示数据库拒绝连接,回到数据库自己的检查,确认启动顺序没有被绕过。日志若显示目录不能写,或仍停在向导,则把这些做完再重新等待连续成功。这个阶段页面丑一点没有关系。丑说明你还在应用里面。一旦你为了好看去改入口,失败就会换一层,你失去现在这个清楚的定位。
直接访问时,应用报出的协议和主机名应当是内网这一跳的事实。这不是故障。故障是你期望它在没有入口的情况下仍然报出公开名字。它报不出来是对的。把这次打印留下来,当作后面经过入口时的对照。没有对照,你只看见经过入口的那一次,就不知道哪些值是入口补上的,哪些是应用自己猜测的。
这一层通过的标准要写得具体。就绪连续成功,一页正文里能找到你预先放好的那一句,打印出来的是未加密和内部主机名。三条都成立,才离开这一层。缺一条就还在应用里面。不要因为正文已经能读,就忽略就绪仍在闪烁。入口一旦接上这样的上游,闪烁会变成访客看见的间歇失败。
入口这一层:用公开主机名问本机的入口
入口这一层仍不离开这台机器,但请求要交给入口,并带上访客会使用的主机名。这样可以不依赖外面的解析。失败时看站点文件,以及它准备转发到的那个端口。端口应当就是应用正在听的端口。文件里若把这个主机名落到了别的站点,或落到了默认站点,你会得到另一个网站的正常页面。正常不等于正确。比对标题和那一句预先放好的话,才能确定走到了书目站。
这一层同时核对三个字段是否按你的政策写给了上游。政策是留下两个名字,就分别用根域名和 www 各请求一次,上游应当看到不同的主机名。政策是收成一个,两次都应当看到那一个。协议字段在你以加密方式询问入口时应当是加密;你若故意只用未加密的方式打到入口、且入口还没做跳转,就应当看到未加密。不要在同一次请求里既要求跳转又抱怨协议字段还没变成加密。跳转完成之后的下一次,才是加密。
证书在这一层可以先不管,前提是你清楚自己是在用未加密的方式询问本机入口,或是在用你已经理解的加密方式。若加密握手本身就失败,那已经不是入口规则的问题,把记录挪到证书那一层,入口规则先停止修改。握手失败时继续改上游端口,会让你在证书修好之后发现端口也被改错了。
入口这一层通过的样子是:用公开主机名问本机入口,得到书目站的那一句,而且应用打印的协议和主机名符合政策。打印仍然是最直接的证据。只看返回的正文,相对链接的页面会在字段全错的时候依然正确。所以这一层要把打印纳入通过标准,不能只看文章句子。句子证明内容对了,打印证明地址的理解对了。
证书这一层:名字、期限和入口里的位置
证书这一层从加密的公开访问看。名字要覆盖访客正在用的主机名,期限未过,入口使用的也是这一份而不是另一份同名文件。失败时看证书本身和入口配置里写明的位置。不要去改书目站的业务配置,也不要去改数据库。证书对不上时,浏览器往往在进入应用之前就停住了。你在应用里改基准地址,浏览器根本还没把那次请求送进应用,所以改动不会显示出来。
根域名和 www 若都允许停留,证书里就都要有。只有一个名字能通过加密检查时,另一个名字上的失败不是应用的跳转逻辑坏了。人会因为两个名字一个能开、一个不能开,去怀疑入口把主机名改错。先看证书里的名字列表。列表里缺谁,就先补谁。补的时候不要顺手改掉已经正确的上游和协议字段。证书更新和主机名政策是两件可以分开完成的事。
还要确认入口读到的文件就是刚放上去的那一份。机器上可以同时躺着几份旧文件。配置若仍指着旧的那份,你查看新文件会觉得名字已经齐了,握手用的却还是旧名单。核对时看一次握手里实际出现的名字,而不是只看磁盘上最新文件的名字。两者不一致,就去改入口指向的位置,然后只重做证书这一层的检查。
证书通过之后,再回到登录和绝对链接。顺序不要反。在警告还在的时候记录跳转,记录下来的是浏览器中断之前的地址,不是应用生成的地址。你会把一张不完整的记录当成应用的错。等警告消失,再看应用把人送到哪里。这样证书的问题和基准地址的问题不会写在同一行里。
解析这一层:名字有没有指到这台机器
解析回答的是:这个公开名字现在把人送到哪一台机器。它不回答应用有没有就绪,也不回答证书是否匹配。从另一台网络上的机器打开,或至少用一处不受你这台机器本地覆盖影响的查询,看名字是否指向当前这台。失败就改记录,不要重启应用,也不要重载入口。应用和入口都不会把一条指去别处的记录拉回来。
根域名和 www 要分别看。一个已经指向这台,另一个仍指向旧处时,你会看到“有时是新站、有时是旧站”。这不是应用不稳定,是两个名字的记录还不一致。在记录一致之前,不要根据这种时好时坏去调整就绪的连续次数。连续次数治的是应用的闪烁,治不了解析的分叉。把分叉记成闪烁,会让你把一条正确的就绪合同改松或改严。
记录改完不会立刻在全世界生效。旧的回答会按它自己的寿命继续被使用。在这段时间里,有的网络已经看到新机器,有的还看到旧机器。这时反复重启应用,只是让新机器上的日志更吵。应当记下记录的寿命,等过了这个时间再从之前失败的那个网络复测。复测仍指向旧处,才说明记录本身没有改到预期的机器,而不是还在等传播。
也要防止本机的解析覆盖把你骗住。你在服务器上或自己的电脑上为了测试改过名字的指向,外面的人并不享用这份修改。所以解析这一层的通过标准必须来自外面,或来自一处你确认没有被本地覆盖的查询。通过之后,再做一次从外面打开公开名字的访问,确认落到的是书目站的那一句,而不是旧机器上的同名页面。
一次只改一层,并留下四行能比较的记录
层次判断清楚之后,每次只改你钉住的那一层,然后只重复那一层的检查。检查没有变好,就把这一次的修改退回,再决定要不要看下一层。不退回就继续往下改,几层会同时处于“也许改对了”的状态。最后站点好了,你却无法把任何一层恢复到一个已知的样子,下一次也不敢动。
记录不需要长。每一次留下四行:时间、哪一层、改了什么、这一层的检查现在是什么结果。结果里写下最终停住的地址、状态,以及你核对过的那一句正文或那两行打印。不要只写“好了”或“还是不行”。这样的句子无法和明天比较。明天的失败若和今天的结果写在同一种四行里,你能看出是哪一行变了。
同时改入口、证书和应用,是最常见的把记录写废的方式。三份文件都动过,检查又用的是从外面打开的那一种全面测试,全面测试一通过,三份文件就一起被当成正确。其中也许只有一份是必要的,另两份留下了过宽的监听或一个多余的跳转。过几天多余的跳转才在登录时出现,你已经不记得它是哪一次一起改进去的。
若你发现自己已经同时改了两层,先停下来,用记录把两层拆开复测,而不是趁手再改第三层。拆开的办法是:先把一层退回到改之前,只留另一层,做这一层的检查;再对调一次。多花的时间用来换回“哪一层真正起了作用”。没有这个知识,后面的就绪、协议和基准地址都会建立在一份说不清的入口之上。说不清的入口会让打印练习失去意义,因为你不知道打印里的值是哪一版配置写出来的。
登录跳转专门用来检验地址
文章页常常全是相对链接,所以协议和主机名都错了,文章仍能往下读。登录不同。它要告诉浏览器下一次去哪一页,这个“哪一页”经常被写成完整网址。完整网址一写错,人就离开公开名字,落到本机端口、落到只有内网才存在的名字,或在根域名和 www 之间来回。内容完全正确的时候,跳转仍然可以是错的。所以登录要单独走一遍,不能用读过一篇文章来代替。
走的时候不要只看登录表单那一页。表单页本身也可能是相对的,看起来很正常。提交之后看地址栏最终停在哪里,并在还能看见响应的时候记下那一次被送往的地址。停住的地方应当仍是你的公开名字,协议是加密的,路径是你期望的后台或原来要读的那一页。若中间有一跳去了别的名字又被送回,也要记下来。被送回只说明后面还有一条补救跳转,不说明第一跳是对的。
协议判断还会影响会话要不要只在加密连接里往返。应用若以为这次是未加密的,就可能给会话做上不适合加密页面的标记。浏览器在真正的加密页面上不把这样的会话送回去,于是下一次请求像是没登录。表现是:刚进后台又回到登录页,而且地址栏一直是对的。人会去查账号是否写错。应当先看应用打印的协议是不是加密。协议仍是未加密,就回到入口有没有写上协议字段、应用有没有采信,而不是去改登录表单的文案。
退出登录、以及“登录之后回到刚才那一页”,同属这一次检验。它们同样会生成完整网址。有的站点只把登录落地写对了,退出时却用基准地址或内部名字。读者从 www 进来,退出后被放到根域名,若你的政策是两个名字都留下,这就是一次不必要的改道。把这三步都走完:登录、回到原页、退出。三步都停在政策允许的名字上,登录这一项才算通过。只通过第一步,另外两步会在真实使用的那天单独失败。
样式和图片要按它们自己的地址打开
首页的句子正常,版面却没有样式,或图片全是空白,这通常不是文章内容没到,而是这些资源走了另一条根。入口可能把样式和图片当成自己的文件去磁盘上找,没有转给书目站。书目站提供的上传图片,和主题自带的样式,还可能放在不同的位置。一条资源成功,不能证明另一条也经过了应用。
把样式或图片的地址单独打开,不要只在文章页里看它有没有加载。单独打开时,主机名应当仍是你正在用的公开名字,不应当跳到另一个根、另一个端口或入口自己的默认站点。页面源代码里看它是相对的还是绝对的。相对的会跟着当前主机名走,这通常是你想要的。绝对的若指向基准名字,要符合你写过的政策;若指向内部名字或未加密的协议,就回到字段和基准地址,而不是去把文件再复制一份到入口的目录里。
复制到入口目录能让这一次看起来修好,下一次应用生成新的资源地址时又会断。资源应当仍然由原来负责它的那一方提供。入口只在你明确决定把某类不变的文件从入口直接送出时才自己读磁盘,并且这条决定要写进站点规则。没有写明的时候,默认应当转给应用。这样主题更新或新上传的图片不用在两处各放一份。
还有一种样式失败来自协议不一致。页面是加密的,样式地址却是未加密的,浏览器会把样式拦住,版面因此像是没加载。文件其实在,只是协议不被允许混用。这不是磁盘上缺文件,而是应用在生成样式的绝对地址时没有拿到加密这个事实。打开那个被拦住的地址,看它的协议。是未加密,就去核对协议字段,不要先把样式文件改名或重新上传。重新上传会让你以为问题在文件本身,下次换一个图片仍然会被拦住。
长连接要单独放宽,不要把所有超时一起加大
管理书目时,有的界面要保持一条更久的连接,用来接收陆续到来的更新。这种连接和普通打开一页不同。它往往要先把连接升级成一条长通道,入口必须把升级所需的那些字段原样转给应用,并允许这条通道比普通页面停留得更久。字段被丢掉时,页面本身仍能打开,一进入那个持续更新的界面就断开。人会以为是界面的脚本坏了。
普通页面的超时应当保持短。短超时让一条已经没有下文的请求尽快让出位置。长通道若套用这个短超时,会在默认的时间里被掐断,表现就是更新走到一半停止,刷新之后又好一会儿。把这类路径单独写出来,只对它们放宽空闲时间。不要为了这一处,把整站的超时都改得很大。整站放宽之后,缓慢的下载、停在半路的提交,都会长时间占着入口的位置。站点能同时接待的人变少,原因却记在“我们只是为了修复更新界面”上。
升级字段和超时是两件事,要分开验证。只放宽时间、没有把升级字段转交,通道根本建立不起来,放宽不会生效。字段转交了、时间仍是普通页面的长度,通道能建立,然后在你停留时断开。测试时在那个界面停留,超过原来的短超时,看更新是否还在继续。只刷新首页无法暴露这件事。首页不走这条长通道。
还要看入口会不会把本该陆续送出的内容先攒起来再一次性交给浏览器。攒起来时,界面会一直空白,直到某一次才突然跳满,或者因为攒得太久被当成失败。长通道需要的是边到边送。若只有这一条路径需要如此,就只对这一条关掉这种攒积,普通页面仍可保持原来的方式。改动之后用同一次停留来复测,不要改完就去做别的验收,以免忘记这条路径是单独约定的。
长通道也不该绕过应用端口的收口。它仍然从入口进来,仍然只在就绪之后才被送上去。不能因为升级比较特殊,就给这条路径开一个对公网直接监听的端口。那样做会把前面关于协议字段的信任全部让开:长通道上同样可能带着主机名和协议的判断。让它走同一层入口,只是超时和升级字段不同。
用一组路径验收,而不是看一个状态码
接流量之前,按固定的一组路径走一遍,并且任何一条失败就停,不要一边失败一边叠加新的配置。顺序是:先在本机确认就绪;再分别用根域名和 www 打开首页、一篇正文、一个样式或一张图片;最后打开登录并走完落地与退出。这几条合在一起,才分别碰到进程、入口、证书、内容和跳转。少一条,就会有一种错误留到后面。
状态码成功,也可能是一页错误说明,或者是默认站点上某个本来就正常的页面。所以除了状态,还要抄下最终停住的地址,并核对页面里有没有你预先放好的那一句。地址必须符合主机名政策。句子必须来自书目站,而不是来自同一台机器上的另一个站点。把这些写成可以比较的短行:从哪个名字进入、最终停在哪个地址、看见了哪一句。下次改入口或改基准时,用同一组短行再走一遍。
静态资源那一条要写下资源自己的地址,不要只写“首页看起来正常”。登录那一条要写下提交之后的地址,不要只写“登录页能打开”。长通道若你已经启用,再加一行:在更新界面停留超过原来的短超时之后,更新是否还在。没有启用就不要为了凑数去测。验收清单跟着实际会交给访客的路径走,不测那些还不存在的功能。
本机直连应用的那一行也留着,但把它和公开访问分开写。直连时期望看见的是内部协议和内部主机名。公开访问时期望看见的是加密和公开名字。两行若写成同一种期望,你不是把直连误判为失败,就是把公开访问的错误当成了“和本机一样,所以正常”。期望不同,才是对照。
旧的、与这次无关的主机名,若已经在这台机器上服务,也打开一次,确认它的页面没有变成书目站,证书也没有被换成只含新名字的那一份。新站点的验收通过,不能以弄坏旁边的站点为代价。旁边那一次只要确认标题仍是原来的,不必把书目站的整组路径再走一遍。
做一个只打印协议和主机名的练习
在测试环境让应用只打印两行:它采信之后的协议,以及它采信之后的主机名。不要打印其他字段,不要把这个练习页放到公网上长期开放。它的价值是把抽象的转发变成看得见的两行字,而不是多一个公开的诊断入口。练习做完就关掉;若必须留下,也只留在本机,和就绪地址一样不对外。
先直接访问应用端口,记下这两行。此时应当是未加密,主机名是内部的。这是对照的底稿。再通过入口,用你准备公开的名字访问同一种打印。两行应当变成加密,以及政策要求的那个主机名。两个名字都允许停留时,分别访问两次,主机名应当跟着地址栏变。只允许一个名字时,两次都应当打印那一个,并且浏览器最终也停在它上面。
然后故意不让入口传递协议字段,再看应用生成的一条绝对链接。它应当退回未加密的地址。这说明应用确实依赖这个字段,而不是碰巧在测试里写对了链接。确认这一点之后,把协议字段加回去,绝对链接应当重新成为加密的。这一去一回比只看一次成功更有用。只看成功,你不知道成功是来自字段,还是来自某个写死的地址。
最后把应用从“所有接口都听”改成“只在本机听”。从另一台机器连接应用端口,应当失败。再通过入口访问打印页,应当仍然成功,并且两行仍是公开的协议和主机名。这一步把“入口能到”和“公网不能到”同时钉住。若改完绑定之后入口也失败了,说明入口并不是从本机去访问应用,而是绕到了对外的接口。先把入口的去路改回本机这一侧,再保持对外不可达。不要为了让入口恢复,把绑定重新放开到所有接口。
直接访问应用:未加密,主机名是内部的
经过入口:加密,主机名符合你写下的政策
故意去掉协议字段:绝对链接退回未加密
只在本机监听:外面连不上应用端口,入口仍能打出公开名字
以后在真实环境里怀疑跳转,先问同一个问题:应用以为自己正被怎样访问。答案若不等于访客地址栏里的那一行,就从入口是否覆盖了字段、应用是否只采信入口这两处查起,而不是从页面上的句子查起。句子可以全对,两行打印仍然全错。打印是地址问题的入口。没有这两行,你只能从错误的跳转结果往回猜。
绝对地址是四段拼起来的
应用写出一条完整网址时,通常是把协议、主机名、路径和查询接在一起。端口只有在不是默认端口时才插入。四段里任何一段用了内网事实,整条链接就不可用。路径和查询往往来自当前这一页,问题较少;协议和主机名来自它对“这次访问是谁”的判断,问题就集中在这里。基准地址则是在没有当前访问时,把协议和主机名整段换掉。
默认端口被写进链接,是向导或基准地址里带了本机端口之后的典型后果。加密的默认端口不必写出来。写出来不一定打不开,但会和证书、跳转、会话所属的名字形成细小的差别,有的浏览器把它们看成不同的站点。检查绝对链接时,看它有没有多出一段内部才需要的端口。有,就回到基准地址和向导留下的那份设置,而不是在入口写一条专门去掉端口的跳转。去掉的跳转是补救,设置里的端口仍会在信件和地图里再生出来。
路径被重复加前缀,是入口和应用没有商量好谁负责前缀。打印练习若只打印协议和主机名,看不出这件事。所以除了两行打印,再抄一条带路径的绝对链接,数一数前缀出现了几次。出现两次,就约定只由一边添加,然后只改那一边再数一次。查询也要保留。登录后回到原页时,若原来的页码或检索词丢了,多半是生成下一跳时只写了路径、没把查询接上。这和主机名无关,但会让人以为又是跳转把站点弄错了。分开看:主机名对不对是一回事,路径和查询有没有被截掉是另一回事。
四段都核对过的一条链接,才配被放进站点地图或信件样例。只核对主机名、不看协议,会放过未加密的地图。只核对协议、不看路径,会放过双前缀。把一条真实链接拆成四段写在验收短行旁边,比写“链接正常”更能在下次比较。下次若只有查询那一段变空,你就知道不要去动证书。
协议字段缺失时,失败要比静默退回更显眼
应用在没有采信到协议时,常见的默认是退回未加密。退回能让你在本机直连时仍然生成打得开的链接,所以作为开发时的习惯很常见。放在入口后面,这种习惯会把入口的漏配变成一批未加密的绝对链接,而且页面本身继续正常。等到浏览器拦住混合的资源、或把人从加密页面送去未加密页面,时间已经晚了。
更清楚的做法是分开两种环境。测试环境里,故意去掉协议字段时,允许它退回未加密,这样练习才能看见依赖关系。准备接待访客的环境里,若请求明显来自入口、却没有协议字段,生成绝对链接的动作应当失败得显眼:记一条日志,页面上对必须使用绝对链接的那一处给出你能认出的标记,而不是悄悄写出未加密的地址。标记要让验收的人看见。看不见的失败会被相对链接掩盖。
失败得显眼,不是把整站变成一页堆栈。堆栈既不适合访客,也可能把内部路径暴露出去。你只需要在该生成绝对地址的地方停下来,并在日志里写明“缺少来自入口的协议”。有了这句,排障就回到入口这一层,而不是去怀疑证书过期。证书过期时浏览器进不了页面;缺字段时页面能进,只是绝对地址不可信。两种症状不同,日志应当帮助你保持这个区别。
也不要在缺失时改用一个写死的加密值来“总是正确”。写死能掩盖入口漏配。哪一天你在测试环境里用未加密的方式经过入口,写死的值仍会声称这是加密的,练习就失去了对照。接待访客的环境可以要求字段必须存在;测试环境可以允许退回。两种行为写在两个地方,不要用同一个写死的值同时应付两边。
会话要附着在你允许的名字上
会话除了看协议,还要看它附着的主机名范围。范围若被写成内部名字,浏览器在公开名字上不会把会话送回,登录就会像是没有发生。范围若被写成只含 www 的名字,从根域名进来的人每次都像是新的访问者。范围若放得过宽,大到不该共享的名字也能带上同一份会话,就超出了这本书目站的需要。
政策是两个公开名字都留下时,要明确会话是否要在根域名和 www 之间共用。共用,人从一边登录之后到另一边仍保持登录,这很省事,但必须是你决定的,并且两个名字都在证书里、都由你的入口服务。不共用,则每边各有各的会话,登录跳转就更不该把人从一边送到另一边,否则他会在目的地发现自己没有登录。两种都成立,不能一半共用、一半又在跳转里假设不共用。
政策是只留一个名字时,会话就附着在那一个名字上,其余名字只负责跳转,不负责保持会话。跳转完成之前不要期待会话已经存在。有的故障是:在还没跳到最终名字时就要求会话,结果会话写在了中间那个名字上,到了最终名字又读不到。让设置会话发生在最终停住的名字上。验收时从旧名字进入,看跳转完成后会话是否仍在,而不是在跳转中途就判断登录失败。
打印练习不包含会话,这是故意的。会话的值不该出现在那个只用来看协议和主机名的页面上,也不该写进普通日志。你要看的是会话附着的名字和协议标记,不是会话里面的内容。在浏览器里看它是否只随加密连接送出、附着的名字是否符合政策,就够了。内容本身保持在原处。
站点地图和规范链接核对同一份基准
站点地图是给站外的程序看的,规范链接告诉对方哪一个地址才是这条书目的正式位置。两者都应当使用外部基准地址,不应当使用某一次生成时碰巧遇到的主机名。若地图里是根域名,规范链接里是 www,对方会以为这是两套内容。你的政策即使允许两个名字都阅读,被引用的正式名字也只能有一个。
核对时从地图里取一条,在浏览器中打开,再看这页源代码中的规范链接是否和地图里的那条一致,协议是否都是加密的,有没有内部端口。再换一条带查询或不带路径前缀的,防止只有首页是对的。首页常常被单独写过。真正容易错的是地图生成器批量拼出来的那些。批量的那一条对了,首页的手写链接也要改成同一份基准,避免手写的那条还停在向导留下的旧值上。
地图是没有当前请求的任务生成的,所以这一节是前面那条规则的验收,不是一条新规则。生成之后把文件留一份在验收记录里,至少留下第一条和最后一条的完整地址。下次只改了入口的主机名政策、忘了改基准地址时,这两条会先变错,页面却因为相对链接仍然能读。没有留下地图里的原句,你只能重新生成之后凭记忆比较。
若两个名字都允许打开页面,规范链接仍指向基准名字,这不是故障。故障是规范链接指向内部名字,或随着生成地图的那次请求在两个公开名字之间摇摆。摇摆说明生成器用了当前主机名。把它改成只读基准。改完在没有请求的情况下再生成一次,确认它不再跟随你测试时使用的那个名字。
健康检查不要被中间缓存住
就绪地址的回答应当是此刻的,而不是一小时前的。若某层把这个回答缓存起来,应用已经失败,入口却继续看到旧的成功,流量就还是送进来。反过来,一次失败被缓存很久,应用早已恢复,入口却迟迟不把流量送回。连续成功的合同会失效,因为入口数到的不是新的探测,而是同一份被重复播放的旧回答。
就绪地址不该被当成可以缓存的普通页面。入口去探测时,要明确这次探测不要取缓存。应用那一侧也不要给这个地址加上很长的可缓存标记。它不是给读者保存的内容。读者本来就不该访问它。若你发现探测的时间戳或内容在多次失败期间完全不变,先怀疑缓存,再怀疑应用真的卡死。卡死通常不会如此稳定地重复同一份成功。
外面另有一处监控时,不要让那处监控去打应用的内部端口,也不要让它代替入口的就绪判断。外面的监控从公开名字进来,检查的是整条路,适合用来发现“读者已经打不开”。入口的就绪检查从本机进来,适合用来决定“要不要把新的读者送上去”。两套数字可以同时存在,不要合成一个。合成之后,公开名字的解析故障会让你重启应用,应用的未就绪又会让你去改解析。
探测的间隔和缓存的时间若接近,还会出现一种更隐蔽的情况:每次探测都刚好落在缓存仍视为新鲜的窗口里。把间隔和“不许缓存”一起写进入口的探测配置。只写间隔,不写不许缓存,间隔再短也只是更频繁地读到同一份旧回答。验收时可以故意让就绪失败一次,看入口是否在你预期的连续失败之后停止送流量。它若毫无反应,就不是应用没失败,而是失败没有被入口看见。
和已经在跑的站点共用入口
这台机器上往往已经有别的网站。书目站只是入口上的一个新主机名,不应当改写那些旧主机名的证书、根目录和上游。新增时用单独的站点规则:这个主机名才转发到书目站的本机端口,其余主机名仍走原来的规则。先在本机用旧名字请求一次,记下标题;配完书目站再请求一次,标题应当相同。标题变了,说明新规则范围过大,把旧名字也卷进来了。
也不要为了让书目站读到样式,把入口的默认根目录改成书目站的目录。默认根目录一改,所有没有单独规则的名字都会开始读这些文件,甚至把别的站点的上传和书目站的上传混在一处。样式问题按前面的办法,由书目站自己提供,或在书目站自己的规则里写明哪些不变的文件由入口送出。不要动默认根。
证书同理。新名字需要出现在它自己使用的那份证书里,不需要把旧名字从旧证书里挪走,也不需要让所有站点突然改用同一份新文件。入口里每个名字指向自己的证书位置。更新书目站的证书时,只重载必要的配置,并复测一个旧名字。旧名字仍能握手,书目站的新名字也能握手,才算这次没有越界。
共用入口还意味着日志和超时是共享的资源。书目站的长通道单独放宽,不要把入口的全局超时改掉,否则旧站点上的长下载也会跟着变成另一种行为。你没有计划去验收旧站点的全部路径,所以就不该改变它们的行为。变化越小,复测旧名字的那一次就越有代表性。只复测标题,是因为你相信除了新主机名之外什么都没改。这个信念要靠你真的没改别的来维持。
这些条件都成立,才把流量指过来
流量指过来,可以是把公开名字的记录指到这台机器,也可以是名字早已指过来、只是入口终于把这个主机名的上游切到书目站。两种都算“开始接待”。开始之前,下面这些应当已经成立,而不是边接待边补。
数据库先通过了它自己的检查,应用才被允许启动。应用的就绪按合同连续成功,而且成功的是一次真实读取,不是“进程还在”。入口只在连续成功之后才把新请求送上来。失败时送去的是原来的去处或一段平静说明,不是半截向导。
协议、原来的主机名和转发主机名由入口覆盖着写给应用,应用只采信这层入口。应用的端口从另一台机器访问不到,入口自己却访问得到。外部基准地址是加密的公开名字,向导或站点设置里没有留下本机端口。没有请求的任务生成的链接也使用这份基准。当前这次访问的翻页和登录,则按你写下的政策使用当前主机名或相对路径。
登录的三步、一条样式或图片、以及你启用了的长通道,都按验收短行走过。根域名和 www 各自符合政策。旁边一个旧名字的标题没有变。打印的两行在经过入口时等于地址栏里该有的协议和主机名。
缺任何一条,就继续让入口把这个主机名留在原来的去处,或先不要把记录指到这里。为了证明名字已经归你,可以让名字先指到入口,由入口回答一段暂缓说明,同时上游仍不指向还没就绪的应用。这样证书和解析可以先完成,读者却不会走进向导。等上面的条件成立,再把上游切过去,并立刻用外面的网络把那组短行再走一遍。里外两次都符合,这次切换才算结束。之后若再失败,仍按四层往回退,一次只改一层。切换成功不是四层从此不必看了,只是你终于有一组可以比较的短行。
见字如晤