云原生小课堂|Envoy 请求流程源码解析(三):请求解析
前言
Envoy 是一款面向 Service Mesh 的高性能网络代理服务。它与应用程序并行运行,通过以平台无关的方式提供通用功能来抽象网络。当基础架构中的所有服务流量都通过 Envoy 网格时,通过一致的可观测性,很容易地查看问题区域,调整整体性能。
Envoy 也是 istio 的核心组件之一,以 sidecar 的方式与服务运行在一起,对服务的流量进行拦截转发,具有路由,流量控制等等强大特性。本系列文章,我们将不局限于 istio,envoy 的官方文档,从源码级别切入,分享 Envoy 启动、流量劫持、http 请求处理流程的进阶应用实例,深度分析 Envoy 架构。
本篇将是 Envoy 请求流程源码解析的第三篇,主要分享 Envoy 的 outbound 方向下篇,包含:接收请求、发送请求、接收响应、返回响应。注:本文中所讨论的 issue 和 pr 基于 21 年 12 月。
outbound 方向
接收请求
1、client 开始向 socket 写入请求数据
2、eventloop 在触发 read event 后,transport_socket_.doRead 中会循环读取加入 read_buffer_,直到返回 EAGAIN
3、
4、把 buffer 传入 Envoy::Http::ConnectionManagerImpl::onData 进行 HTTP 请求的处理
5、
6、如果 codec_type 是 AUTO(HTTP1,2,3 目前还不支持,在计划中)的情况下,会判断请求是否以 PRI * HTTP/2 为开始来判断是否 http2
7、利用 http_parser 进行 http 解析的 callback,ConnectionImpl::settings_静态初始化了 parse 各个阶段的 callbacks
8、
envoy 社区有讨论会将协议解析器从 http_parser 换成 llhttp
https://github.com/envoyproxy/envoy/issues/5155
https://github.com/envoyproxy/envoy/pull/15263/files 使用解析器接口,重构 http parser
https://github.com/envoyproxy/envoy/pull/15814添加 llhttp 解析器的实现,暂时还没合并
9、
10、onMessageBeginBase
11、
12、创建 ActiveStream, 保存 downstream 的信息,和对应的 route 信息对于 https,会把 TLS 握手的时候保存的 SNI 写入 ActiveStream.requested_server_name_
13、onHeaderField,onHeaderValue 迭代添加 header 到 current_header_map_中
14、解析完最后一个请求头后会执行 onHeadersComplete 把 request 中的一些字段(method, path, host )加入 headers 中
15、回调 onHeadersComplete, 依次回调 onMessageComplete,onMessageCompleteBase,ServerConnectionImpl::onMessageComplete
这个请求解码是 Envoy 上下文的,它会执行 Envoy 的核心代理逻辑 —— 遍历 HTTP 过滤器链、进行路由选择
此过滤器当中判断请求过载
通过 route 上的 cluster name 从 ThreadLocalClusterManager 中查找 cluster, 缓存在 cached_cluster_info_中
根据配置构造在 route 上的 filterChain (具体的 filter 实现是通过 registerFactory 方法注册进去,在 createFilterChain 的时候根据名称构造,比如 istio-proxy 的 stats)
如果对应 http connection manager 上有 trace 配置
request header 中有 trace,就创建子 span, sampled 跟随 parent span
如果 header 中没有 trace,就创建 root span, 并设置 sampled
16、根据 http connection manager 上配置的 filters (envoy.cors,envoy.fault,envoy.router),一个个执行 decodeHeaders
这里主要写一下和 envoy.router
(1)envoy.router
在构造 RouteMatcher 的时候会遍历 virtual_hosts 下的 domains,并根据通配符的位置和 domain 的长度分为 4 个 map<domain_len, std::unordered_map<domain, virtualHost>, std::greater<int64_t>>
default_virtual_host_`domain 就是一个通配符(只允许存在一个)
wildcard_virtual_host_suffixes_domain 中通配符在开头
wildcard_virtual_host_prefixes_domain 中通配符在结尾
virtual_hosts_不包含通配
按照 virtual_hosts_=>wildcard_virtual_host_suffixes_=>wildcard_virtual_host_prefixes_=>default_virtual_host_的顺序查找
同时按照 map 的迭代顺序(domain len 降序)查找最先除去通配符后能匹配到的 virtualhost,如果没有直接返回 404
在一个 virtualhost 上查找对应 route 和 cluster
在通过 domain 匹配到 virtualhost,会在那个 virtualhost 上匹配查找 cluster,如果没匹配上,会直接返回 404
match 可以根据配置分为 prefix,regex,path 三种 route 进行匹配
如果存在 weighted_clusters,会根据 stream_id, 和 clusters 的 weight 进行分发,stream_id 本身是每个请求独立随机生成,所以 weighted_clusters 的权重分发可以视为随机分发
(2)
没有 route 能匹配请求,返回 404no cluster match for URL
有配置 directResponseEntry,直接返回
route 上的 clustername 在 clustermanager 上找不到对应 cluster,返回配置的 clusterNotFoundResponseCode
当前处于 maintenanceMode (和主动健康检查相关)
调用 createConnPool 获取 upstream conn pool
根据 cluster 上的 features 配置和 USE_DOWNSTREAM_PROTOCOL 来确定使用 http1 还是 http2 协议向上游发送请求
在 ThreadLocalClusterManager 上根据 cluster name 查询 cluster
根据 loadbalancer 算法挑选节点(此处 worker 之间的负载均衡根据不同的负载均衡算法有的是独立的,比如 round robin,只有同一个 Worker 上的才是严格的顺序)
根据节点和协议拿到连接池 (连接池由 ThreadLocalClusterManager 管理,各个 Worker 不共享)
没有做直接 503,中止解析链
根据配置(timeout, perTryTimeout)确定本次请求的 timeout
把之前生成的 trace 写入 request header
对 request 做一些最终的修改,headers_to_remove``headers_to_add``host_rewrite``rewritePathHeader(路由的配置)
构造 retry 和 shadowing 的对象
发送请求
发送请求部分也是在 envoy.router 中的逻辑
1、查看当前 conn pool 是否有空闲 client
2、
如果存在空闲连接
根据 downstream request 和 tracing 等配置构造发往 upstream 的请求 buffer
把 buffer 一次性移入 write_buffer_, 立即触发 Write Event
ConnectionImpl::onWriteReady 随后会被触发
把 write_ buffer_的内容写入 socket 发送出去
如果不存在空闲连接
根据 max_pending_requests 和 max_connections 判断是否可以创建新的连接(此处的指标为 worker 间共享),但是每个线程会向上游最少建立一条连接,也就是极端策略可能需要和工作线程数相关根据配置设置新连接的 socket options, 使用 dispatcher.createClientConnection 创建连接上游的连接,并绑定到 eventloop 新建 PendingRequest 并加到 pending_requests_头部当连接成功建立的时候,会触发 ConnectionImpl::onFileEvent
在 onConnected 的回调中停止 connect_timer_;复用存在空闲连接时的逻辑,发送请求
3、在 onRequestComplete 里调用 maybeDoShadowing 进行流量复制
4、
shadowing 流量并不会返回错误 shadowing 流量为 asynclient 发送,不会阻塞 downstream,timeout 也为 global_timeout_
shadowing 会修改 request header 里的 host 和 authority 添加-shadow 后缀 5、根据 global_timeout_启动响应超时的定时器
接收响应
1、eventloop 触发 ClientConnectionImpl.ConnectionImpl 上的 onFileEvent 的 read ready 事件
2、经过 http_parser execute 后触发 onHeadersComplete 后执行到 UpstreamRequest::decodeHeaders
3、upstream_request_->upstream_host_->outlierDelector().putHttpResponseCode 写入 status code,更新外部检测的状态
4、
5、
6、根据返回结果、配置和 retries_remaining_判断是否应该 retry
根据 internal_redirect_action 的配置和 response 来确定是否需要 redirect 到新的 host
返回响应
1、停止 request_timer, 重置 idle_timer
2、和向 upstream 发送请求一样的逻辑,发送响应给 downstream
阅读源码总结
1、envoy 当中各种继承,模板,组合使用的非常多,子类初始化时需要关注父类的构造函数做了什么
2、可以根据请求日志的信息,通过日志的顺序再到代码走一遍大体过程
3、善用各种调试工具,例如抓包,gdb,放开指标等,个人的经验 百分之 90 的问题日志+抓包+部分源码的阅读可以解决
ASM 试用申请
Envoy 是 Istio 中的 Sidecar 官方标配,是一个面向 Service Mesh 的高性能网络代理服务。
当前 Service Mesh 是 Kubernetes 上微服务治理的最佳实践,灵雀云微服务治理平台 Alauda Service Mesh(简称:ASM)可完整覆盖微服务落地所需要的基础设施,让开发者真正聚焦业务。
如果您想深入体验 ASM,扫描下方二维码即可报名!
附录:
关于重复 header 的 rfc 规范:
https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
关于 header 大小写处理:
https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/header_casing
关于修改 header append 行为:
https://www.envoyproxy.io/docs/envoy/latest/version_history/v1.15.1
关于【云原生小课堂】
【云原生小课堂】是由灵雀云、Kube-OVN 社区、云原生技术社区联合开设的公益性技术分享类专题,将以丰富详实的精品内容和灵活多样的呈现形式,持续为您分享云原生前沿技术,带您了解更多云原生实践干货。
在数字化转型的背景下,云原生已经成为企业创新发展的核心驱动力。作为国内最早将 Kubernetes 产品化的厂商之一,灵雀云从出生便携带“云原生基因”,致力于通过革命性的技术帮助企业完成数字化转型,我们期待着云原生给这个世界带来更多改变。
关注我们,学习更多云原生知识,一起让改变发生。
版权声明: 本文为 InfoQ 作者【York】的原创文章。
原文链接:【http://xie.infoq.cn/article/e20011ed7738a7022686d7292】。文章转载请联系作者。
评论