以每周一个节点,记录知识点。
2023-08-14~18
条件型 CORS 响应下因缺失 Vary: Origin 导致的缓存错乱问题
现象:
在同一个浏览器下,先打开了foo.taobao.com
上的一个页面,访问了我们的资源 https://static.taobao.com/1.jpg,这个资源被浏览器缓存了下来,和资源内容一起缓存的还有Access-Control-Allow-Origin: https://foo.taobao.com
响应头。这时又打开 bar.taobao.com
上的一个页面,这个页面也要访问那个资源 https://static.taobao.com/1.jpg,这时它会读取本地缓存,读到的 Access-Control-Allow-Origin
头是缓存下的 https://foo.taobao.com
而不是自己想要的 https://bar.taobao.com
,这时就报跨域错误了,虽然它应该是能访问到这份资源的。
解决方案:
使用 Vary: Origin 让同一个 URL 有多份缓存
有一个 HTTP 响应头叫Vary
,vary 这个单词的意思是”变化”、”不同”的意思,Vary
响应头就是让同一个 URL 根据某个请求头的不同而使用不同的缓存。比如常见的Vary: Accept-Encoding
表示客户端要根据Accept-Encoding
请求头的不同而使用不同的缓存,比如 gizp 的缓存一份,未压缩的缓存为另一份。
在 CORS 的场景下,我们需要使用Vary: Origin
来保证不同网站发起的请求使用各自的缓存。比如从foo.taobao.com
发起的请求缓存下的响应头是:
Access-Control-Allow-Origin: https://foo.taobao.com
Vary: Origin
的话,bar.taobao.com
在发起同 URL 的请求就不会使用这份缓存了,因为Origin
请求头变了。还有<img>
标签发起的非 CORS 请求缓存下的响应头是:
Vary: Origin
的话, 在使用 XHR 发起的 CORS 请求也不会使用那份缓存,因为Origin
请求头从无到有,也算是变了。
Fetch 规范中专门讲了Vary: Origin
在 CORS 响应中该如何使用 https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches,其中第一段是:
If CORS protocol requirements are more complicated than setting
Access-Control-Allow-Origin
to * or a static origin,Vary
is to be used.
翻译一下就是“如果你的 Access-Control-Allow-Origin
响应头不是简单的写死成了*
或者某一个特定的源(就是我总结的条件型 CORS 响应),那么你就应该加上Vary: Origin
响应头。
云产品的缓存配置
现在很多静态今天资源都会使用云产品的CDN做缓存,所以不仅后端要配置跨域标识,CDN也要配置。
CDN上配置
在自定义HTTP响应头页签,单击添加,根据下表中的参数含义设置自定义HTTP响应头。
Access-Control-Allow-Origin
: https://*.aliyun.com,http://*.aliyun.com
Access-Control-Allow-Methods
: GET,POST,PUT,DELETE,HEAD,OPTIONS
Access-Control-Allow-Headers
: *
Access-Control-Allow-Credentials
: true
Access-Control-Max-Age
: 300
Vary
: Origin
OSS上配置
在创建跨域规则面板,将来源设置为*.aliyun.com
,允许Methods全部勾选,允许Headers设置为*
,暴露Headers设置为ETag和x-oss-request-id,缓存时间设置为300,选中返回Vary: Origin,然后单击确定。
知识点:
无条件型 CORS 响应
将Access-Control-Allow-Origin
固定写死为*
(允许任意网站访问)、或者特定的某一个源(只允许这一个网站访问),不论请求头里的 Origin
是什么,甚至没有 Origin
也一样返回那个值。
条件型 CORS 响应
条件型 CORS 响应又分为两种情况:
- 区分对待有无
Origin
请求头
有Origin
请求头才会返回Access-Control-Allow-Origin
响应头,没有就不返回。
- 区分对待不同的
Origin
请求头
如果想允许特定的某些个网站访问自己的资源,由于Access-Control-Allow-Origin
被设计为不支持返回多个源,这就需要根据 Origin
请求头的值来动态的判断出要不要加 Access-Control-Allow-Origin
了。
比如我们想只允许 *.taobao.com
下的页面访问,当接受到的请求包含 Origin: https://foo.taobao.com
时,需要返回 Access-Control-Allow-Origin: https://foo.taobao.com
;当接受到的请求包含Origin: https://bar.taobao.com
时,需要返回 Access-Control-Allow-Origin: https://bar.taobao.com
;当接受到的请求包含 Origin: https://foo.tmall.com
时,就不返回Access-Control-Allow-Origin
头了。
其他知识
-
https://zhuanlan.zhihu.com/p/38972475
-
跨源资源共享(CORS)https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
2023-08-14~18
k8s 监控丢点现象
问题定位
当 Prometheus 按照ServiceMonitor 设置的采集频率10s去采集cAdvisor 提供的数据时,如果在此期间 cAdvisor 没有进行数据更新,则Prometheus会采集到与上次采集时间戳和值相同的情况,Prometheus 就只会记录一条数据。这就是cAdvisor “丢点”的本质。cAdvisor的刷新频率由 housekeeping相关参数 和 抖动 机制确定。
解决方案
-
cAdvisor 配置
containers:// 参数设置 - -allow_dynamic_housekeeping=false - -housekeeping_interval=1s
-
ServiceMonitor上配置忽略指标自带时间戳
spec: endpoints: - honorLabels: true // 忽略时间戳 honorTimestamps: false interval: 10s
k8s 监控调优
- 【架构高可用】:集群维度的双副本 Prometheus 采集底层exporter数据,adapter多实例自动选主实现容灾。
- 【数据持久化】:通过remoteWrite将数据存储到后端的VictoriaMetrics中进行持久化存储,Grafana使用VictoriaMetrics做为数据源展示和告警。
- 【监控统一化】:通过remoteWrite将数据交由kafka-adapter转发到Kafka,由公司基础监控等服务通过消费Kafka中的数据来进行展示和告警。
优化项:
-
过滤未使用指标
# 过滤 container_threads 开头的指标 - action: drop regex: container_threads(.*) sourceLabels: - __name__
-
过滤低优先级 pod 相关指标
# 过滤掉 monitoring namespace 下面 telegraf 这个daemosnet提供的 pod 的相关指标 - action: drop regex: monitoring@telegraf(.*) separator: '@' sourceLabels: - namespace - pod
-
均衡 Prometheus 负载
将监控 target 进行分类交由不同的组 Prometheus 采集,且每类 Prometheus 为双副本模式。
-
减少Prometheus存储数据时间
监控数据的持久化存储都放在 VictoriaMetrics 上面, 故修改Prometheus采集的数据本地存储时间从7天改为2天。
vivo 容器集群监控系统优化之道 https://mp.weixin.qq.com/s/zHir-IiCEuS_4gmyKDry7Q
2023-08-01~04
k8s ipvs 出现 1s 延迟
问题定位
通过抓包分析,发现tcp 连接在连接复用时,会丢弃一个syn包,等待1s后才重新连接。
ipvs 对端口的复用策略主要由内核参数 net.ipv4.vs.conn_reuse_mode
决定
其目的是:
当 client ip:client port 复用发生时,对于 TIME_WAIT 状态下的 ip_vs_conn,进行重新调度,使得 connection 在 rs 上的分布更均衡,以提高性能。
如果该 mode 是 0,则会复用旧 ip_vs_conn 里的 rs,使得连接更不均衡。所以当 conn_reuse_mode 为 0 表示启用 ipvs 连接复用,为 1 表示不复用.
conn_resue_mode=1 的 bug
开启这个内核参数实际就表示 ipvs 转发时不做连接复用,每次新建的连接都会重新调度 rs 并新建 ip_vs_conn,但它的实现有个问题: 在新建连接时 (SYN 包),如果 client ip:client port 匹配到了 ipvs 旧连接 (TIME_WIAT 状态),且使用了 conntrack,就会丢掉第一个 SYN 包,**等待重传后 (1s) **才能成功建连,从而导致建连性能急剧下降。
conn_resue_mode=0 引发的问题
ipvs 连接复用有两个问题:
- 系统使用 conntrack 跟踪连接,conntrack 使用三元组(即源 IP、源端口、协议)定位具体连接,当客户端产生大量短连接时,其本身可能与之前的端口复用,而 TCP 在 conntrack 中的超时时间达 900s,也就是说 conntrack 中记录的连接可能存在很长时间,客户端过来的新连接极有可能复用老连接,此时已经 TIME_WAIT 的老连接无法响应请求,从而导致超时。只要有 client ip:client port 匹配上 ip_vs_conn (发生复用),就直接转发给对应的 rs,不管 rs 当前是什么状态,即便 rs 的 weight 为 0 (通常是 TIME_WAIT 状态) 也会转发,TIME_WAIT 的 rs 通常是 Terminating 状态已销毁的 Pod,转发过去的话连接就必然异常。
- 高并发下大量复用,没有为新连接没有调度 rs,直接转发到所复用连接对应的 rs 上,导致很多新连接被 “固化” 到部分 rs 上。
业务可能遇到的问题:
-
滚动更新连接异常。 被访问的服务滚动更新时,Pod 有新建有销毁,ipvs 发生连接复用时转发到了已销毁的 Pod 导致连接异常 (no route to host)。
-
滚动更新负载不均。 由于复用时不会重新调度连接,导致新连接也被 “固化” 在某些 Pod 上了。
-
新扩容的 Pod 接收流量少。 同样也是由于复用时不会重新调度连接,导致很多新连接被 “固化” 在扩容之前的这些 Pod 上了。
v5.9 以上的内核已修复上述 bug https://github.com/kubernetes/kubernetes/issues/93297
内核两个Patch
• ipvs: allow connection reuse for unconfirmed conntrack
修改了ip_vs_conn_uses_conntrack()方法的逻辑,当使用unconfirmed conntrack时,返回false,这种修改针对了TIME_WAIT的conntrack。
• ipvs: queue delayed work to expire no destination connections if expire_nodest_conn=1
提前了expire connection的操作,在destination被删除后,便开始将expire connection操作入队列。而不是等到数据包真正发过来时,才做expire connection,以此来减少数据包的丢失。
解决方案
通过升级系统和内核来根本解决此问题。此系统内核值情况
net.ipv4.vs.conn_reuse_mode = 1
net.ipv4.vs.conntrack = 1
net.ipv4.vs.expire_nodest_conn = 1