Google Borg 浅析

作者: 徐蓓

Google Borg 浅析

笔者的工作主要涉及集群资源调度和混合部署,对相关技术和论文有所研究,包括 Google Borg、Kubernetes、Firmament 和 Kubernetes Poseidon 等。尤其是这篇《Large-scale cluster management at Google with Borg》令笔者受益匪浅。下面本人就结合生产场景,尝试对 Google Borg 做些分析和延展。

Google Borg 简介

Google Borg 是一套资源管理系统,可用于管理和调度资源。在 Borg 中,资源的单位是 JobTaskJob 包含一组 TaskTask 是 Borg 管理和调度的最小单元,它对应一组 Linux 进程。熟悉 Kubernetes 的读者,可以将 JobTask 大致对应为 Kubernetes 的 ServicePod

在架构上,Borg 和 Kubernetes 类似,由 BorgMaster、Scheduler 和 Borglet 组成。

Allocs

Borg Alloc 代表一组可用于运行 Task 的资源,如 CPU、内存、IO 和磁盘空间。它实际上是集群对物理资源的抽象。Alloc set 类似 Job,是一堆 Alloc 的集合。当一个 Alloc set 被创建时,一个或多个 Job 就可以运行在上面了。

Priority 和 Quota

每个 Job 都可以设置 Priority。Priority 可用于标识 Job 的重要程度,并影响一些资源分配、调度和 Preemption 策略。比如在生产中,我们会将作业分为 Routine Job 和 Batch Job。Routine Job 为生产级的例行作业,优先级最高,它占用对应实际物理资源的 Alloc set。Batch Job 代表一些临时作业,优先级最低。当资源紧张时,集群会优先 Preempt Batch Job,将资源提供给 Routine Job 使用。这时 Preempted Batch Job 会回到调度队列等待重新调度。

Quota 代表资源配额,它约束 Job 的可用资源,比如 CPU、内存或磁盘。Quota 一般在调度之前进行检查。Job 若不满足,会立即在提交时被拒绝。生产中,我们一般依据实际物理资源配置 Routine Job Quota。这种方式可以确保 Routine Job 在 Quota 内一定有可用的资源。为了充分提升集群资源使用率,我们会将 Batch Job Quota 设置为无限,让它尽量去占用 Routine Job 的闲置资源,从而实现超卖。这方面内容后面会在再次详述。

Schedule

调度是资源管理系统的核心功能,它直接决定了系统的“好坏”。在 Borg 中,Job 被提交后,Borgmaster 会将其放入一个 Pending Queue。Scheduler 异步地扫描队列,将 Task 调度到有充足资源的机器上。通常情况下,调度过程分为两个步骤:Filter 和 Score。Filter,或是 Feasibility Checking,用于判断机器是否满足 Task 的约束和限制,比如 Schedule Preference、Affinity 或 Resource Limit。Filter 结束后,就需要 Score 符合要求的机器,或称为 Weight。上述两个步骤完成后,Scheduler 就会挑选相应数量的机器调度给 Task 运行。实际上,选择合适的调度策略尤为重要。

这里可以拿一个生产集群举例。在初期,我们的调度系统采用的 Score 策略类似 Borg E-PVM,它的作用是将 Task 尽量均匀的调度到整个集群上。从正面效果上讲,这种策略分散了 Task 负载,并在一定程度上缩小了故障域。但从反面看,它也引发了资源碎片化的问题。由于我们底层环境是异构的,机器配置并不统一,并且 Task 配置和物理配置并无对应关系。这就造成一些配置过大的 Task 无法运行,由此在一定程度上降低了资源的分配率和使用率。为了应付此类问题,我们自研了新的 Score 策略,称之为 “Best Fillup”。它的原理是在调度 Task 时选择可用资源最少的机器,也就是尽量填满。不过这种策略的缺点显而易见:单台机器的负载会升高,从而增加 Bursty Load 的风险;不利于 Batch Job 运行;故障域会增加。

这篇论文,作者采用了一种被称为 hybrid 的方式,据说比第一种策略增加 3-5% 的效率。具体实现方式还有待后续研究。

Utilization

资源管理系统的首要目标是提高资源使用率,Borg 亦是如此。不过由于过多的前置条件,诸如 Job 放置约束、负载尖峰、多样的机器配置和 Batch Job,导致不能仅选择 “average utilization” 作为策略指标。在 Borg 中,使用 Cell Compaction 作为评判基准。简述之就是:能承载给定负载的最小 Cell。

Borg 提供了一些提高 utilization 的思路和实践方法,有些是我们在生产中已经采用的,有些则非常值得我们学习和借鉴。

Cell Sharing

Borg 发现,将各种优先级的 Task,比如 prod 和 non-prod 运行在共享的 Cell 中可以大幅度的提升资源利用率。

上面(a)图表明,采用 Task 隔离的部署方式会增加对机器的需求。图(b)是对额外机器需求的分布函数。图(a)和图(b)都清楚的表明了将 prod job 和 non-prod job 分开部署会消耗更多的物理资源。Borg 的经验是大约会新增 20-30% 左右。

个中原理也很好理解:prod job 通常会为应对负载尖峰申请较大资源,实际上这部分资源在多数时间里是闲置的。Borg 会定时回收这部分资源,并将之分配给 non-prod job 使用。在 Kubernetes 中,对应的概念是 request limit 和 limit。我们在生产中,一般设置 Prod job 的 Request limit 等于 limit,这样它就具有了最高的 Guaranteed Qos。该 QoS 使得 pod 在机器负载高时不至于被驱逐和 OOM。non-prod job 则不设置 request limit 和 limit,这使得它具有 BestEffort 级别的 QoS。kubelet 会在资源负载高时优先驱逐此类 Pod。这样也达到了和 Borg 类似的效果。

Large cells

Borg 通过实验数据表明,小容量的 cell 通常比大容量的更占用物理资源。

这点对我们有和很重要的指导意义。通常情况下,我们会在设计集群时对容量问题感到犹豫不决。显而易见,小集群可以带来更高的隔离性、更小的故障域以及潜在风险。但随之带来的则是管理和架构复杂度的增加,以及更多的故障点。大集群的优缺点正好相反。在资源利用率这个指标上,我们凭直觉认为是大集群更优,但苦于无坚实的理论依据。Borg 的研究表明,大集群有利于增加资源利用率,这点对我们的决策很有帮助。

Fine-grained resource requests

Borg 对资源细粒度分配的方法,目前已是主流,在此我就不再赘述。

Resource reclamation

笔者感觉这部分内容帮助最大。熟悉 Kubernetes 的读者,应该对类似的概念很熟悉,也就是所谓的 request limit。job 在提交时需要指定 resource limit,它能确保内部的 task 有足够资源可以运行。有些用户会为 task 申请过大的资源,以应对可能的请求或计算的突增。但实际上,部分资源在多数时间内是闲置的。与其资源浪费,不如利用起来。这需要系统有较精确的预测机制,可以评估 task 对实际资源的需求,并将闲置资源回收以分配给低 priority 的任务,比如 batch job。上述过程在 Borg 中被称为 resource reclamation,对使用资源的评估则被称为 reservation。Borgmaster 会定期从 Borglet 收集 resource consumption,并执行 reservation。在初始阶段,reservation 等于 resource limit。随着 task 的运行,reservation 就变为了资源的实际使用量,外加 safety margin。

在 Borg 调度时,Scheduler 使用 resource limit 为 prod task 过滤和选择主机,这个过程并不依赖 reclaimed resource。从这个角度看,并不支持对 prod task 的资源超卖。但 non-prod task 则不同,它是占用已有 task 的 resource reservation。所以 non-prod task 会被调度到拥有 reclaimed resource 的机器上。

这种做法当然也是有一定风险的。若资源评估出现偏差,机器上的可用资源可能会被耗尽。在这种情况下,Borg 会杀死或者降级 non-prod task,prod task 则不会受到半分任何影响。

上图证实了这种策略的有效性。参照 Week 1 和 4 的 baseline,Week 2 和 3 在调整了 estimation algorithm 后,实际资源的 usage 与 reservation 的 gap 在显著缩小。在 Borg 的一个 median cell 中,有 20% 的负载是运行在 reclaimed resource 上。

相较于 Borg,Kubernetes 虽然有 resource limit 和 capacity 的概念,但却缺少动态 reclaim 机制。这会使得系统对低 priority task 的资源缺少行之有效的评估机制,从而引发系统负载问题。个人感觉这个功能对资源调度和提升资源使用率影响巨大,这部分内容也是笔者的工作重心

Isolation

这部分内容虽十分重要,但对于我们的生产集群优先级不是很高,在此先略过。有兴趣的读者可以自行研究。

参考资料

Istio 学习笔记:Istio CNI 插件

作者: 陈鹏

设计目标

当前实现将用户 pod 流量转发到 proxy 的默认方式是使用 privileged 权限的 istio-init 这个 init container 来做的(运行脚本写入 iptables),Istio CNI 插件的主要设计目标是消除这个 privileged 权限的 init container,换成利用 k8s CNI 机制来实现相同功能的替代方案

原理

  • Istio CNI Plugin 不是 istio 提出类似 k8s CNI 的插件扩展机制,而是 k8s CNI 的一个具体实现
  • k8s CNI 插件是一条链,在创建和销毁pod的时候会调用链上所有插件来安装和卸载容器的网络,istio CNI Plugin 即为 CNI 插件的一个实现,相当于在创建销毁pod这些hook点来针对istio的pod做网络配置:写入iptables,让该 pod 所在的 network namespace 的网络流量转发到 proxy 进程
  • 当然也就要求集群启用 CNI,kubelet 启动参数: --network-plugin=cni (该参数只有两个可选项:kubenet, cni

实现方式

  • 运行一个名为 istio-cni-node 的 daemonset 运行在每个节点,用于安装 istio CNI 插件
  • 该 CNI 插件负责写入 iptables 规则,让用户 pod 所在 netns 的流量都转发到这个 pod 中 proxy 的进程
  • 当启用 istio cni 后,sidecar 的自动注入或istioctl kube-inject将不再注入 initContainers (istio-init)

istio-cni-node 工作流程

  • 复制 Istio CNI 插件二进制程序到CNI的bin目录(即kubelet启动参数--cni-bin-dir指定的路径,默认是/opt/cni/bin
  • 使用istio-cni-node自己的ServiceAccount信息为CNI插件生成kubeconfig,让插件能与apiserver通信(ServiceAccount信息会被自动挂载到/var/run/secrets/kubernetes.io/serviceaccount)
  • 生成CNI插件的配置并将其插入CNI配置插件链末尾(CNI的配置文件路径是kubelet启动参数--cni-conf-dir所指定的目录,默认是/etc/cni/net.d
  • watch CNI 配置(cni-conf-dir),如果检测到被修改就重新改回来
  • watch istio-cni-node 自身的配置(configmap),检测到有修改就重新执行CNI配置生成与下发流程(当前写这篇文章的时候是istio 1.1.1,还没实现此功能)

设计提案

参考资料

istio 庖丁解牛(三) galley

作者: 钟华

今天我们来解析istio控制面组件Galley. Galley Pod是一个单容器单进程组件, 没有sidecar, 结构独立, 职责明确.

查看高清原图

前不久istio 1.1 版本正式发布, 其中istio的配置管理机制有较大的改进, 以下是1.1 release note 中部分说明:

Added Galley as the primary configuration ingestion and distribution mechanism within Istio. It provides a robust model to validate, transform, and distribute configuration states to Istio components insulating the Istio components from Kubernetes details. Galley uses the Mesh Configuration Protocol (MCP) to interact with components

Galley 原来仅负责进行配置验证, 1.1 后升级为整个控制面的配置管理中心, 除了继续提供配置验证功能外, Galley还负责配置的管理和分发, Galley 使用 网格配置协议(Mesh Configuration Protocol) 和其他组件进行配置的交互.

今天对Galley的剖析大概有以下方面:

  • Galley 演进的背景
  • Galley 配置验证功能
  • MCP 协议
  • Galley 配置管理实现浅析

Galley 演进的背景

在 k8s 场景下, 「配置(Configuration)」一词主要指yaml编写的Resource Definition, 如service、pod, 以及扩展的CRD( Custom Resource Definition), 如 istio的 VirtualService、DestinationRule 等.

本文中「配置」一词可以等同于 k8s Resource Definition + istio CRD

声明式 API 是 Kubernetes 项目编排能力“赖以生存”的核心所在, 而「配置」是声明式 API的承载方式.

Istio 项目的设计与实现,其实都依托于 Kubernetes 的声明式 API 和它所提供的各种编排能力。可以说,Istio 是在 Kubernetes 项目使用上的一位“集大成者”

Istio 项目有多火热,就说明 Kubernetes 这套“声明式 API”有多成功

k8s 内置了几十个Resources, istio 创造了50多个CRD, 其复杂度可见一斑, 所以有人说面向k8s编程近似于面向yaml编程.

早期的Galley 仅仅负责对「配置」进行运行时验证, istio 控制面各个组件各自去list/watch 各自关注的「配置」, 以下是istio早期的Configuration flow:

越来越多且复杂的「配置」给istio 用户带来了诸多不便, 主要体现在:

  • 「配置」的缺乏统一管理, 组件各自订阅, 缺乏统一回滚机制, 配置问题难以定位
  • 「配置」可复用度低, 比如在1.1之前, 每个mixer adpater 就需要定义个新的CRD.
  • 另外「配置」的隔离, ACL 控制, 一致性, 抽象程度, 序列化等等问题都还不太令人满意.

随着istio功能的演进, 可预见的istio CRD数量还会继续增加, 社区计划将Galley 强化为istio 「配置」控制层, Galley 除了继续提供「配置」验证功能外, 还将提供配置管理流水线, 包括输入, 转换, 分发, 以及适合istio控制面的「配置」分发协议(MCP).

本文对Galley的分析基于istio tag 1.1.1 (commit 2b13318)


Galley 配置验证功能

istio 庖丁解牛(二) sidecar injector中我分析了istio-sidecar-injector 如何利用 MutatingWebhook 来实现sidecar注入, Galley 使用了k8s提供的另一个Admission Webhooks: ValidatingWebhook, 来做配置的验证:

istio 需要一个关于ValidatingWebhook的配置项, 用于告诉k8s api server, 哪些CRD应该发往哪个服务的哪个接口去做验证, 该配置名为istio-galley, 简化的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
%kubectl get ValidatingWebhookConfiguration istio-galley -oyaml

apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
name: istio-galley
webhooks:
- clientConfig:
......
service:
name: istio-galley
namespace: istio-system
path: /admitpilot
failurePolicy: Fail
name: pilot.validation.istio.io
rules:
...pilot关注的CRD...
- gateways
- virtualservices
......
- clientConfig:
......
service:
name: istio-galley
namespace: istio-system
path: /admitmixer
name: mixer.validation.istio.io
rules:
...mixer关注的CRD...
- rules
- metrics
......

可以看到, 该配置将pilot和mixer关注的CRD, 分别发到了服务istio-galley的/admitpilot/admitmixer, 在Galley 源码中可以很容易找到这2个path Handler的入口:

1
2
h.HandleFunc("/admitpilot", wh.serveAdmitPilot)
h.HandleFunc("/admitmixer", wh.serveAdmitMixer)

MCP协议

MCP 提供了一套配置订阅和分发的API, 在MCP中, 可以抽象为以下模型:

  • source: 「配置」的提供端, 在Istio中Galley 即是source
  • sink: 「配置」的消费端, 在isito中典型的sink包括Pilot和Mixer组件
  • resource: source和sink关注的资源体, 也就是isito中的「配置」

当sink和source之间建立了对某些resource的订阅和分发关系后, source 会将指定resource的变化信息推送给sink, sink端可以选择接受或者不接受resource更新(比如格式错误的情况), 并对应返回ACK/NACK 给source端.

MCP 提供了gRPC 的实现, 实现代码参见: https://github.com/istio/api/tree/master/mcp/v1alpha1,

其中包括2个services: ResourceSourceResourceSink, 通常情况下, source 会作为 gRPC的server 端, 提供ResourceSource服务, sink 作为 gRPC的客户端, sink主动发起请求连接source; 不过有的场景下, source 会作为gRPC的client端, sink作为gRPC的server端提供ResourceSink服务, source主动发起请求连接sink.

以上2个服务, 内部功能逻辑都是一致的, 都是sink需要订阅source管理的resource, 区别仅仅是哪端主动发起的连接请求.

具体到istio的场景中:

  • 在单k8s集群的istio mesh中, Galley默认实现了ResourceSource service, Pilot和Mixer会作为该service的client主动连接Galley进行配置订阅.
  • Galley 可以配置去主动连接远程的其他sink, 比如说在多k8s集群的mesh中, 主集群中的Galley可以为多个集群的Pilot/Mixer提供配置管理, 跨集群的Pilot/Mixer无法主动连接主集群Galley, 这时候Galley就可以作为gRPC的client 主动发起连接, 跨集群的Pilot/Mixer作为gRPC server 实现ResourceSink服务,

两种模式的示意图如下:


Galley 配置管理实现浅析

galley 进程对外暴露了若干服务, 最重要的就是基于gRPC的mcp服务, 以及http的验证服务, 除此之外还提供了 prometheus exporter接口以及Profiling接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if serverArgs.EnableServer { // 配置管理服务
go server.RunServer(serverArgs, livenessProbeController, readinessProbeController)
}
if validationArgs.EnableValidation { // 验证服务
go validation.RunValidation(validationArgs, kubeConfig, livenessProbeController, readinessProbeController)
}

// 提供 prometheus exporter
go server.StartSelfMonitoring(galleyStop, monitoringPort)

if enableProfiling {
// 使用包net/http/pprof
// 通过http server提供runtime profiling数据
go server.StartProfiling(galleyStop, pprofPort)
}
// 开始探针更新
go server.StartProbeCheck(livenessProbeController, readinessProbeController, galleyStop)

接下来主要分析下「配置」管理服务的实现:

1
go server.RunServer(serverArgs, livenessProbeController, readinessProbeController)

下面是Galley 配置服务结构示意图:

查看高清原图

从上图可以看到, Galley 配置服务主要包括 Processor 和 负责mcp通信的grpc Server.

其中 Processor 又由以下部分组成:

  • Source: 代表Galley管理的配置的来源
  • Handler: 对「配置」事件的处理器
  • State: Galley管理的「配置」在内存中状态

Source

interface Source 代表istio关注的配置的来源, 其Start方法需要实现对特定资源的变化监听.

1
2
3
4
5
6
7
8
9
10
// Source to be implemented by a source configuration provider.
type Source interface {
// Start the source interface, provided the EventHandler. The initial state of the underlying
// config store should be reflected as a series of Added events, followed by a FullSync event.
Start(handler resource.EventHandler) error

// Stop the source interface. Upon return from this method, the channel should not be accumulating any
// more events.
Stop()
}

在Galley中, 有多个Source的实现, 主要包括

  • source/fs.source

  • source/kube/builtin.source

  • source/kube/dynamic.source
  • source/kube.aggregate

其中source/fs代表从文件系统中获取配置, 这种形式常用于开发和测试过程中, 不需要创建实际的k8s CRD, 只需要CRD文件即可, 同时source/fs也是实现了更新watch(使用https://github.com/howeyc/fsnotify)

source/kube/builtin.source处理k8s 内置的配置来源, 包括Service, Node, Pod, Endpoints等, source/kube/dynamic.source处理其他的istio 关注的CRD, source/kube.aggregate是多个Source 的聚合, 其本身也实现了Source interface:

1
2
3
4
5
6
7
8
9
10
11
12
13
type aggregate struct {
mu sync.Mutex
sources []runtime.Source
}

func (s *aggregate) Start(handler resource.EventHandler) error {
......
for _, source := range s.sources {
if err := source.Start(syncHandler); err != nil {
return err
}
}
......

source/kube/builtin.sourcesource/kube/dynamic.source本身都包含一个k8s SharedIndexInformer:

1
2
3
4
5
6
7
8
// source is a simplified client interface for listening/getting Kubernetes resources in an unstructured way.
type source struct {
......
// SharedIndexInformer for watching/caching resources
informer cache.SharedIndexInformer

handler resource.EventHandler
}

二者的Start方法的实现, 正是用到了k8s典型的 Informer+list/watch 模式, 获取关注「配置」的变化事件, 在此不再赘述.

Source 获得「配置」更新事件后, 会将其推送到Processor 的events chan 中, events 长度为1024, 通过go p.process(), Proccesorhandler会对事件进行异步处理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func (p *Processor) Start() error {
......
events := make(chan resource.Event, 1024)
err := p.source.Start(func(e resource.Event) {
events <- e
})
......
p.events = events

go p.process()
return nil
}

func (p *Processor) process() {
loop:
for {
select {
// Incoming events are received through p.events
case e := <-p.events:
p.processEvent(e)

case <-p.state.strategy.Publish:
scope.Debug("Processor.process: publish")
p.state.publish()

// p.done signals the graceful Shutdown of the processor.
case <-p.done:
scope.Debug("Processor.process: done")
break loop
}

if p.postProcessHook != nil {
p.postProcessHook()
}
}
......
}

Handler 和 State

interface Handler 代表对「配置」变化事件的处理器:

1
2
3
4
// Handler handles an incoming resource event.
type Handler interface {
Handle(e resource.Event)
}

在istio中有多个Handler的实现, 典型的有:

  • Dispatcher
  • State

Dispatcher 是多个Handler的集合:

1
2
3
type Dispatcher struct {
handlers map[resource.Collection][]Handler
}

State 是对Galley的内存中的状态, 包括了Galley 当前持有「配置」的schema、发布策略以及内容快照等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// State is the in-memory state of Galley.
type State struct {
name string
schema *resource.Schema

distribute bool
strategy *publish.Strategy
distributor publish.Distributor

config *Config

// version counter is a nonce that generates unique ids for each updated view of State.
versionCounter int64

// entries for per-message-type State.
entriesLock sync.Mutex
entries map[resource.Collection]*resourceTypeState

// Virtual version numbers for Gateways & VirtualServices for Ingress projected ones
ingressGWVersion int64
ingressVSVersion int64
lastIngressVersion int64

// pendingEvents counts the number of events awaiting publishing.
pendingEvents int64

// lastSnapshotTime records the last time a snapshot was published.
lastSnapshotTime time.Time
}

同时State 也实现了interface Handler, 最终「配置」资源将会作为快照存储到State的distributor中, distributor实际的实现是mcp包中的Cache, 实际会调用mcp中的Cache#SetSnapshot.


Distributor 、Watcher 和 Cache

在mcp包中, 有2个interface 值得特别关注: Distributor 和 Watcher

interface Distributor 定义了「配置」快照存储需要实现的接口, State 最终会调用SetSnapshot将配置存储到快照中.

1
2
3
4
5
6
// Distributor interface allows processor to distribute snapshots of configuration.
type Distributor interface {
SetSnapshot(name string, snapshot sn.Snapshot)

ClearSnapshot(name string)
}

interface Watcher 功能有点类似k8s的 list/watch, Watch方法会注册 mcp sink 的watch 请求和处理函数:

1
2
3
4
5
6
7
8
9
10
// Watcher requests watches for configuration resources by node, last
// applied version, and type. The watch should send the responses when
// they are ready. The watch can be canceled by the consumer.
type Watcher interface {
// Watch returns a new open watch for a non-empty request.
//
// Cancel is an optional function to release resources in the
// producer. It can be called idempotently to cancel and release resources.
Watch(*Request, PushResponseFunc) CancelWatchFunc
}

struct mcp/snapshot.Cache 同时实现了Distributor 和 Watcher interface:

1
2
3
4
5
6
7
8
type Cache struct {
mu sync.RWMutex
snapshots map[string]Snapshot
status map[string]*StatusInfo
watchCount int64

groupIndex GroupIndexFn
}

mcp 服务端在接口 StreamAggregatedResourcesEstablishResourceStream中, 会调用Watch方法, 注册sink连接的watch请求:

1
2
3
4
5
6
sr := &source.Request{
SinkNode: req.SinkNode,
Collection: collection,
VersionInfo: req.VersionInfo,
}
w.cancel = con.watcher.Watch(sr, con.queueResponse)

mcp/snapshot.Cache 实现了interface Distributor 的SetSnapshot方法, 该方法在State状态变化后会被调用, 该方法会遍历之前watch注册的responseWatch, 并将WatchResponse传递给各个处理方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// SetSnapshot updates a snapshot for a group.
func (c *Cache) SetSnapshot(group string, snapshot Snapshot) {
c.mu.Lock()
defer c.mu.Unlock()

// update the existing entry
c.snapshots[group] = snapshot

// trigger existing watches for which version changed
if info, ok := c.status[group]; ok {
info.mu.Lock()
defer info.mu.Unlock()

for id, watch := range info.watches {
version := snapshot.Version(watch.request.Collection)
if version != watch.request.VersionInfo {
scope.Infof("SetSnapshot(): respond to watch %d for %v @ version %q",
id, watch.request.Collection, version)

response := &source.WatchResponse{
Collection: watch.request.Collection,
Version: version,
Resources: snapshot.Resources(watch.request.Collection),
Request: watch.request,
}
watch.pushResponse(response)

// discard the responseWatch
delete(info.watches, id)

scope.Debugf("SetSnapshot(): watch %d for %v @ version %q complete",
id, watch.request.Collection, version)
}
}
}
}

提供给Watch的处理函数queueResponse会将WatchResponse放入连接的响应队列, 最终会推送给mcp sink端.

1
2
3
4
5
6
7
8
9
// Queue the response for sending in the dispatch loop. The caller may provide
// a nil response to indicate that the watch should be closed.
func (con *connection) queueResponse(resp *WatchResponse) {
if resp == nil {
con.queue.Close()
} else {
con.queue.Enqueue(resp.Collection, resp)
}
}

最后上一张Galley mcp 服务相关模型UML:

查看高清原图

Galley 源代码展示了面向抽象(interface)编程的好处, Source 是对「配置」数据源的抽象, Distributor 是「配置」快照存储的抽象, Watcher 是对「配置」订阅端的抽象. 抽象的具体实现可以组合起来使用. 另外Galley组件之间也充分解耦, 组件之间的数据通过chan/watcher等流转.

关于早期 istio 配置管理的演进计划, 可以参考2018年5月 CNCF KubeCon talk Introduction to Istio Configuration - Joy Zhang (需.翻.墙), 1.1 版本中Galley 也还未完全实现该文中的roadmap, 如 configuration pipeline 等. 未来Galley 还会继续演进.

版权归作者所有, 欢迎转载, 转载请注明出处


参考资料

istio 庖丁解牛(二) sidecar injector

作者: 钟华

今天我们分析下istio-sidecar-injector 组件:

查看高清原图

用户空间的Pod要想加入mesh, 首先需要注入sidecar 容器, istio 提供了2种方式实现注入:

「注入」本质上就是修改Pod的资源定义, 添加相应的sidecar容器定义, 内容包括2个新容器:

  • 名为istio-init的initContainer: 通过配置iptables来劫持Pod中的流量
  • 名为istio-proxy的sidecar容器: 两个进程pilot-agent和envoy, pilot-agent 进行初始化并启动envoy


1. Dynamic Admission Control

kubernetes 的准入控制(Admission Control)有2种:

  • Built in Admission Control: 这些Admission模块可以选择性地编译进api server, 因此需要修改和重启kube-apiserver
  • Dynamic Admission Control: 可以部署在kube-apiserver之外, 同时无需修改或重启kube-apiserver.

其中, Dynamic Admission Control 包含2种形式:

  • Admission Webhooks: 该controller 提供http server, 被动接受kube-apiserver分发的准入请求.
  • Initializers: 该controller主动list and watch 关注的资源对象, 对watch到的未初始化对象进行相应的改造.

    其中, Admission Webhooks 又包含2种准入控制:

  • ValidatingAdmissionWebhook

  • MutatingAdmissionWebhook

istio 使用了MutatingAdmissionWebhook来实现对用户Pod的注入, 首先需要保证以下条件满足:

  • 确保 kube-apiserver 启动参数 开启了 MutatingAdmissionWebhook
  • 给namespace 增加 label: kubectl label namespace default istio-injection=enabled
  • 同时还要保证 kube-apiserver 的 aggregator layer 开启: --enable-aggregator-routing=true 且证书和api server连通性正确设置.

另外还需要一个配置对象, 来告诉kube-apiserver istio关心的资源对象类型, 以及webhook的服务地址. 如果使用helm安装istio, 配置对象已经添加好了, 查阅MutatingWebhookConfiguration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
% kubectl get mutatingWebhookConfiguration -oyaml
- apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
name: istio-sidecar-injector
webhooks:
- clientConfig:
service:
name: istio-sidecar-injector
namespace: istio-system
path: /inject
name: sidecar-injector.istio.io
namespaceSelector:
matchLabels:
istio-injection: enabled
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods

该配置告诉kube-apiserver: 命名空间istio-system 中的服务 istio-sidecar-injector(默认443端口), 通过路由/inject, 处理v1/pods的CREATE, 同时pod需要满足命名空间istio-injection: enabled, 当有符合条件的pod被创建时, kube-apiserver就会对该服务发起调用, 服务返回的内容正是添加了sidecar注入的pod定义.


2. Sidecar 注入内容分析

查看Pod istio-sidecar-injector的yaml定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
%kubectl -n istio-system get pod istio-sidecar-injector-5f7894f54f-w7f9v -oyaml
......
volumeMounts:
- mountPath: /etc/istio/inject
name: inject-config
readOnly: true

volumes:
- configMap:
items:
- key: config
path: config
name: istio-sidecar-injector
name: inject-config

可以看到该Pod利用projected volumeistio-sidecar-injector这个config map 的config挂到了自己容器路径/etc/istio/inject/config, 该config map 内容正是注入用户空间pod所需的模板.

如果使用helm安装istio, 该 configMap 模板源码位于: https://github.com/istio/istio/blob/master/install/kubernetes/helm/istio/templates/sidecar-injector-configmap.yaml.

该config map 是在安装istio时添加的, kubernetes 会自动维护 projected volume的更新, 因此 容器 sidecar-injector只需要从本地文件直接读取所需配置.

高级用户可以按需修改这个模板内容.

1
kubectl -n istio-system get configmap istio-sidecar-injector -o=jsonpath='{.data.config}'

查看该configMap, data.config包含以下内容(简化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
policy: enabled // 是否开启自动注入
template: |- // 使用go template 定义的pod patch
initContainers:
[[ if ne (annotation .ObjectMeta `sidecar.istio.io/interceptionMode` .ProxyConfig.InterceptionMode) "NONE" ]]
- name: istio-init
image: "docker.io/istio/proxy_init:1.1.0"
......
securityContext:
capabilities:
add:
- NET_ADMIN
......
containers:
- name: istio-proxy
args:
- proxy
- sidecar
......
image: [[ annotation .ObjectMeta `sidecar.istio.io/proxyImage` "docker.io/istio/proxyv2:1.1.0" ]]
......
readinessProbe:
httpGet:
path: /healthz/ready
port: [[ annotation .ObjectMeta `status.sidecar.istio.io/port` 0 ]]
......
securityContext:
capabilities:
add:
- NET_ADMIN
runAsGroup: 1337
......
volumeMounts:
......
- mountPath: /etc/istio/proxy
name: istio-envoy
- mountPath: /etc/certs/
name: istio-certs
readOnly: true
......
volumes:
......
- emptyDir:
medium: Memory
name: istio-envoy
- name: istio-certs
secret:
optional: true
[[ if eq .Spec.ServiceAccountName "" -]]
secretName: istio.default
[[ else -]]
secretName: [[ printf "istio.%s" .Spec.ServiceAccountName ]]
......

对istio-init生成的部分参数分析:

  • -u 1337 排除用户ID为1337,即Envoy自身的流量
  • 解析用户容器.Spec.Containers, 获得容器的端口列表, 传入-b参数(入站端口控制)
  • 指定要从重定向到 Envoy 中排除(可选)的入站端口列表, 默认写入-d 15020, 此端口是sidecar的status server
  • 赋予该容器NET_ADMIN 能力, 允许容器istio-init进行网络管理操作

对istio-proxy 生成的部分参数分析:

  • 启动参数proxy sidecar xxx 用以定义该节点的代理类型(NodeType)
  • 默认的status server 端口--statusPort=15020
  • 解析用户容器.Spec.Containers, 获取用户容器的application Ports, 然后设置到sidecar的启动参数--applicationPorts中, 该参数会最终传递给envoy, 用以确定哪些端口流量属于该业务容器.
  • 设置/healthz/ready 作为该代理的readinessProbe
  • 同样赋予该容器NET_ADMIN能力

另外istio-sidecar-injector还给容器istio-proxy挂了2个volumes:

  • 名为istio-envoy的emptydir volume, 挂载到容器目录/etc/istio/proxy, 作为envoy的配置文件目录

  • 名为istio-certs的secret volume, 默认secret名为istio.default, 挂载到容器目录/etc/certs/, 存放相关的证书, 包括服务端证书, 和可能的mtls客户端证书

    1
    2
    3
    4
    % kubectl exec productpage-v1-6597cb5df9-xlndw -c istio-proxy -- ls /etc/certs/
    cert-chain.pem
    key.pem
    root-cert.pem

后续文章探究sidecar istio-proxy会对其进一步分析.


3. istio-sidecar-injector-webhook 源码分析

  • 镜像Dockerfile: istio/pilot/docker/Dockerfile.sidecar_injector
  • 启动命令: /sidecar-injector
  • 命令源码: istio/pilot/cmd/sidecar-injector

容器中命令/sidecar-injector启动参数如下:

1
2
3
4
5
6
7
8
- args:
- --caCertFile=/etc/istio/certs/root-cert.pem
- --tlsCertFile=/etc/istio/certs/cert-chain.pem
- --tlsKeyFile=/etc/istio/certs/key.pem
- --injectConfig=/etc/istio/inject/config
- --meshConfig=/etc/istio/config/mesh
- --healthCheckInterval=2s
- --healthCheckFile=/health

sidecar-injector 的核心数据模型是 Webhookstruct, 注入配置sidecarConfig包括注入模板以及注入开关和规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Webhook struct {
mu sync.RWMutex
sidecarConfig *Config // 注入配置: 模板,开关,规则
sidecarTemplateVersion string
meshConfig *meshconfig.MeshConfig

healthCheckInterval time.Duration
healthCheckFile string

server *http.Server
meshFile string
configFile string // 注入内容路径, 从启动参数injectConfig中获取
watcher *fsnotify.Watcher // 基于文件系统的notifications
certFile string
keyFile string
cert *tls.Certificate
}

type Config struct {
Policy InjectionPolicy `json:"policy"`
Template string `json:"template"`
NeverInjectSelector []metav1.LabelSelector `json:"neverInjectSelector"`
AlwaysInjectSelector []metav1.LabelSelector `json:"alwaysInjectSelector"`
}

sidecar-injector 的root cmd 会创建一个Webhook, 该struct包含一个http server, 并将路由/inject注册到处理器函数serveInject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
RunE: func(c *cobra.Command, _ []string) error {
......
wh, err := inject.NewWebhook(parameters)
......
go wh.Run(stop)
......
}

func NewWebhook(p WebhookParameters) (*Webhook, error) {
......
watcher, err := fsnotify.NewWatcher()
// watch the parent directory of the target files so we can catch
// symlink updates of k8s ConfigMaps volumes.
for _, file := range []string{p.ConfigFile, p.MeshFile, p.CertFile, p.KeyFile} {
watchDir, _ := filepath.Split(file)
if err := watcher.Watch(watchDir); err != nil {
return nil, fmt.Errorf("could not watch %v: %v", file, err)
}
}
......
h := http.NewServeMux()
h.HandleFunc("/inject", wh.serveInject)
wh.server.Handler = h
......
}

Webhook#Run方法会启动该http server, 并负责响应配置文件的更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (wh *Webhook) Run(stop <-chan struct{}) {
go func() {
wh.server.ListenAndServeTLS("", "")
......
}()
......
var timerC <-chan time.Time
for {
select {
case <-timerC:
timerC = nil
sidecarConfig, meshConfig, err := loadConfig(wh.configFile, wh.meshFile)
......
case event := <-wh.watcher.Event:
// use a timer to debounce configuration updates
if (event.IsModify() || event.IsCreate()) && timerC == nil {
timerC = time.After(watchDebounceDelay)
}
case ......
}
}
}

Webhook#Run首先会启动处理注入请求的http server, 下面的for循环主要是处理2个配置文件的更新操作, select 里使用了一个timer(并不是ticker), 咋一看像是简单的定时更新配置文件, 其实不然. 配置文件更新事件由wh.watcher进行接收, 然后才会启动timer, 这里用到了第三方库https://github.com/howeyc/fsnotify, 这是一个基于文件系统的notification. 这里使用timer限制在一个周期(watchDebounceDelay)里面最多重新加载一次配置文件, 避免在配置文件频繁变化的情况下多次触发不必要的loadConfig

use a timer to debounce configuration updates

Webhook.serveInject 会调用Webhook#inject, 最终的模板处理函数是injectionData.

istio 庖丁解牛(一) 组件概览

作者: 钟华

Istio 作为 Service Mesh 领域的集大成者, 提供了流控, 安全, 遥测等模型, 其功能复杂, 模块众多, 有较高的学习和使用门槛, 本文会对istio 1.1 的各组件进行分析, 希望能帮助读者了解istio各组件的职责、以及相互的协作关系.

1. istio 组件构成

以下是istio 1.1 官方架构图:

虽然Istio 支持多个平台, 但将其与 Kubernetes 结合使用,其优势会更大, Istio 对Kubernetes 平台支持也是最完善的, 本文将基于Istio + Kubernetes 进行展开.

如果安装了grafana, prometheus, kiali, jaeger等组件的情况下, 一个完整的控制面组件包括以下pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
% kubectl -n istio-system get pod
NAME READY STATUS
grafana-5f54556df5-s4xr4 1/1 Running
istio-citadel-775c6cfd6b-8h5gt 1/1 Running
istio-galley-675d75c954-kjcsg 1/1 Running
istio-ingressgateway-6f7b477cdd-d8zpv 1/1 Running
istio-pilot-7dfdb48fd8-92xgt 2/2 Running
istio-policy-544967d75b-p6qkk 2/2 Running
istio-sidecar-injector-5f7894f54f-w7f9v 1/1 Running
istio-telemetry-777876dc5d-msclx 2/2 Running
istio-tracing-5fbc94c494-558fp 1/1 Running
kiali-7c6f4c9874-vzb4t 1/1 Running
prometheus-66b7689b97-w9glt 1/1 Running

将istio系统组件细化到进程级别, 大概是这个样子:

查看高清原图

Service Mesh 的Sidecar 模式要求对数据面的用户Pod进行代理的注入, 注入的代理容器会去处理服务治理领域的各种「脏活累活」, 使得用户容器可以专心处理业务逻辑.

从上图可以看出, Istio 控制面本身就是一个复杂的微服务系统, 该系统包含多个组件Pod, 每个组件 各司其职, 既有单容器Pod, 也有多容器Pod, 既有单进程容器, 也有多进程容器, 每个组件会调用不同的命令, 各组件之间会通过RPC进行协作, 共同完成对数据面用户服务的管控.


2. Istio 源码, 镜像和命令

Isito 项目代码主要由以下2个git 仓库组成:

仓库地址 语言 模块
https://github.com/istio/istio Go 包含istio控制面的大部分组件: pilot, mixer, citadel, galley, sidecar-injector等,
https://github.com/istio/proxy C++ 包含 istio 使用的边车代理, 这个边车代理包含envoy和mixer client两块功能

2.1 istio/istio

https://github.com/istio/istio 包含的主要的镜像和命令:

容器名 镜像名 启动命令 源码入口
Istio_init istio/proxy_init istio-iptables.sh istio/tools/deb/istio-iptables.sh
istio-proxy istio/proxyv2 pilot-agent istio/pilot/cmd/pilot-agent
sidecar-injector-webhook istio/sidecar_injector sidecar-injector istio/pilot/cmd/sidecar-injector
discovery istio/pilot pilot-discovery istio/pilot/cmd/pilot-discovery
galley istio/galley galley istio/galley/cmd/galley
mixer istio/mixer mixs istio/mixer/cmd/mixs
citadel istio/citadel istio_ca istio/security/cmd/istio_ca

另外还有2个命令不在上图中使用:

命令 源码入口 作用
mixc istio/mixer/cmd/mixc 用于和Mixer server 交互的客户端
node_agent istio/security/cmd/node_agent 用于node上安装安全代理, 这在Mesh Expansion特性中会用到, 即k8s和vm打通.

2.2 istio/proxy

https://github.com/istio/proxy 该项目本身不会产出镜像, 它可以编译出一个name = "Envoy"的二进制程序, 该二进制程序会被ADD到istio的边车容器镜像istio/proxyv2中.

istio proxy 项目使用的编译方式是Google出品的bazel, bazel可以直接在编译中引入第三方库,加载第三方源码.

这个项目包含了对Envoy源码的引用,还在此基础上进行了扩展,这些扩展是通过Envoy filter(过滤器)的形式来提供,这样做的目的是让边车代理将策略执行决策委托给Mixer,因此可以理解istio proxy 这个项目有2大功能模块:

  1. Envoy: 使用到Envoy的全部功能
  2. mixer client: 测量和遥测相关的客户端实现, 基于Envoy做扩展,通过RPC和Mixer server 进行交互, 实现策略管控和遥测

后续我将对以上各个模块、命令以及它们之间的协作进行探究.


3. Istio Pod 概述

3.1 数据面用户Pod

数据面用户Pod注入的内容包括:

  1. initContainer istio-init: 通过配置iptables来劫持Pod中的流量, 转发给envoy

  2. sidecar container istio-proxy: 包含2个进程, 父进程pliot-agent 初始化并管控envoy, 子进程envoy除了包含原生envoy的功能外, 还加入了mixer client的逻辑.

    主要端口:

    • --statusPort status server 端口, 默认为0, 表示不启动, istio启动时通常传递为15020, 由pliot-agent监听
    • --proxyAdminPort 代理管理端口, 默认 15000, 由子进程envoy监听.

3.2 istio-sidecar-injector

包含一个单容器, sidecar-injector-webhook: 启动一个http server, 接受kube api server 的Admission Webhook 请求, 对用户pod进行sidecar注入.

进程为sidecar-injector, 主要监听端口:

  • --port Webhook服务端口, 默认443, 通过k8s serviceistio-sidecar-injector 对外提供服务.

3.3 istio-galley

包含一个单容器 galley: 提供 istio 中的配置管理服务, 验证Istio的CRD 资源的合法性.

进程为galley server ......, 主要监听端口:

  • --server-address galley gRPC 地址, 默认是tcp://0.0.0.0:9901
  • --validation-port https端口, 提供验证crd合法性服务的端口, 默认443.

  • --monitoringPort http 端口, self-monitoring 端口, 默认 15014

以上端口通过k8s serviceistio-galley对外提供服务

3.4 istio-pilot

pilot组件核心Pod, 对接平台适配层, 抽象服务注册信息、流量控制模型等, 封装统一的 API,供 Envoy 调用获取.

包含以下容器:

  1. sidecar container istio-proxy

  2. container discovery: 进程为pilot-discovery discovery ......

    主要监听端口:

    • 15010: 通过grpc 提供的 xds 获取接口
    • 15011: 通过https 提供的 xds 获取接口

    • 8080: 通过http 提供的 xds 获取接口, 兼容v1版本, 另外 http readiness 探针 /ready也在该端口

    • --monitoringPort http self-monitoring 端口, 默认 15014

    以上端口通过k8s serviceistio-pilot对外提供服务

3.5 istio-telemetry 和istio-policy

mixer 组件包含2个pod, istio-telemetry 和 istio-policy, istio-telemetry负责遥测功能, istio-policy 负责策略控制, 它们分别包含2个容器:

  1. sidecar containeristio-proxy

  2. mixer: 进程为 mixs server ……

    主要监听端口:

    • 9091: grpc-mixer
    • 15004: grpc-mixer-mtls

    • --monitoring-port: http self-monitoring 端口, 默认 15014, liveness 探针/version

3.7 istio-citadel

负责安全和证书管理的Pod, 包含一个单容器 citadel

启动命令/usr/local/bin/istio_ca --self-signed-ca ...... 主要监听端口:

  • --grpc-port citadel grpc 端口, 默认8060

  • --monitoring-port: http self-monitoring 端口, 默认 15014, liveness 探针/version

以上端口通过k8s serviceistio-citadel对外提供服务


后续将对各组件逐一进行分析.

Istio 服务网格领域的新王者

作者: 钟华

istio

今天分享的内容主要包括以下4个话题:

  • 1 Service Mesh: 下一代微服务
  • 2 Istio: 第二代 Service Mesh
  • 3 Istio 数据面
  • 4 Istio 控制面

首先我会和大家一起过一下 Service Mesh的发展历程, 并看看Istio 为 Service Mesh 带来了什么, 这部分相对比较轻松. 接下来我将和大家分析一下Istio的主要架构, 重点是数据面和控制面的实现, 包括sidecar的注入, 流量拦截, xDS介绍, Istio流量模型, 分布式跟踪, Mixer 的适配器模型等等, 中间也会穿插着 istio的现场使用demo.


1. Service Mesh: 下一代微服务

  • 应用通信模式演进
  • Service Mesh(服务网格)的出现
  • 第二代 Service Mesh
  • Service Mesh 的定义
  • Service Mesh 产品简史
  • 国内Service Mesh 发展情况

1.1 应用通信模式演进: 网络流控进入操作系统

在计算机网络发展的初期, 开发人员需要在自己的代码中处理服务器之间的网络连接问题, 包括流量控制, 缓存队列, 数据加密等. 在这段时间内底层网络逻辑和业务逻辑是混杂在一起.

随着技术的发展,TCP/IP 等网络标准的出现解决了流量控制等问题。尽管网络逻辑代码依然存在,但已经从应用程序里抽离出来,成为操作系统网络层的一部分, 形成了经典的网络分层模式.


1.2 应用通信模式演进: 微服务架构的出现

微服务架构是更为复杂的分布式系统,它给运维带来了更多挑战, 这些挑战主要包括资源的有效管理和服务之间的治理, 如:

  • 服务注册, 服务发现
  • 服务伸缩
  • 健康检查
  • 快速部署
  • 服务容错: 断路器, 限流, 隔离舱, 熔断保护, 服务降级等等
  • 认证和授权
  • 灰度发布方案
  • 服务调用可观测性, 指标收集
  • 配置管理

在微服务架构的实现中,为提升效率和降低门槛,应用开发者会基于微服务框架来实现微服务。微服务框架一定程度上为使用者屏蔽了底层网络的复杂性及分布式场景下的不确定性。通过API/SDK的方式提供服务注册发现、服务RPC通信、服务配置管理、服务负载均衡、路由限流、容错、服务监控及治理、服务发布及升级等通用能力, 比较典型的产品有:

  • 分布式RPC通信框架: COBRA, WebServices, Thrift, GRPC 等
  • 服务治理特定领域的类库和解决方案: Hystrix, Zookeeper, Zipkin, Sentinel 等
  • 对多种方案进行整合的微服务框架: SpringCloud、Finagle、Dubbox 等

实施微服务的成本往往会超出企业的预期(内容多, 门槛高), 花在服务治理上的时间成本甚至可能高过进行产品研发的时间. 另外上述的方案会限制可用的工具、运行时和编程语言。微服务软件库一般专注于某个平台, 这使得异构系统难以兼容, 存在重复的工作, 系统缺乏可移植性.

Docker 和Kubernetes 技术的流行, 为Pass资源的分配管理和服务的部署提供了新的解决方案, 但是微服务领域的其他服务治理问题仍然存在.


1.3 Sidecar 模式的兴起

Sidecar(有时会叫做agent) 在原有的客户端和服务端之间加多了一个代理, 为应用程序提供的额外的功能, 如服务发现, 路由代理, 认证授权, 链路跟踪 等等.

业界使用Sidecar 的一些先例:

  • 2013 年,Airbnb 开发了Synapse 和 Nerve,是sidecar的一种开源实现
  • 2014 年, Netflix 发布了Prana,它也是一个sidecar,可以让非 JVM 应用接入他们的 NetflixOSS 生态系统

1.4 Service Mesh(服务网格)的出现

直观地看, Sidecar 到 Service Mesh 是一个规模的升级, 不过Service Mesh更强调的是:

  • 不再将Sidecar(代理)视为单独的组件,而是强调由这些代理连接而形成的网络
  • 基础设施, 对应用程序透明

1.5 Service Mesh 定义

以下是Linkerd的CEO Willian Morgan给出的Service Mesh的定义:

A Service Mesh is a dedicated infrastructure layer for handling service-to-service communication. It’s responsible for the reliable delivery of requests through the complex topology of services that comprise a modern, cloud native application. In practice, the Service Mesh is typically implemented as an array of lightweight network proxies that are deployed alongside application code, without the application needing to be aware.

服务网格(Service Mesh)是致力于解决服务间通讯的基础设施层。它负责在现代云原生应用程序的复杂服务拓扑来可靠地传递请求。实际上,Service Mesh 通常是通过一组轻量级网络代理(Sidecar proxy),与应用程序代码部署在一起来实现,且对应用程序透明


1.6 第二代 Service Mesh

控制面板对每一个代理实例了如指掌,通过控制面板可以实现代理的访问控制和度量指标收集, 提升了服务网格的可观测性和管控能力, Istio 正是这类系统最为突出的代表.


1.7 Service Mesh 产品简史

  • 2016 年 1 月 15 日,前 Twitter 的基础设施工程师 William Morgan 和 Oliver Gould,在 GitHub 上发布了 Linkerd 0.0.7 版本,采用Scala编写, 他们同时组建了一个创业小公司 Buoyant,这是业界公认的第一个Service Mesh

  • 2016 年,Matt Klein在 Lyft 默默地进行 Envoy 的开发。Envoy 诞生的时间其实要比 Linkerd 更早一些,只是在 Lyft 内部不为人所知

  • 2016 年 9 月 29 日在 SF Microservices 上,“Service Mesh”这个词汇第一次在公开场合被使用。这标志着“Service Mesh”这个词,从 Buoyant 公司走向社区.

  • 2016 年 9 月 13 日,Matt Klein 宣布 Envoy 在 GitHub 开源,直接发布 1.0.0 版本。

  • 2016 年下半年,Linkerd 陆续发布了 0.8 和 0.9 版本,开始支持 HTTP/2 和 gRPC,1.0 发布在即;同时,借助 Service Mesh 在社区的认可度,Linkerd 在年底开始申请加入 CNCF

  • 2017 年 1 月 23 日,Linkerd 加入 CNCF。

  • 2017 年 3 月 7 日,Linkerd 宣布完成千亿次产品请求

  • 2017 年 4 月 25 日,Linkerd 1.0 版本发布

  • 2017 年 7 月 11 日,Linkerd 发布版本 1.1.1,宣布和 Istio 项目集成

  • 2017 年 9 月, nginx突然宣布要搞出一个Servicemesh来, Nginmesh: https://github.com/nginxinc/nginmesh, 可以作为istio的数据面, 不过这个项目目前处于不活跃开发(This project is no longer under active development)

  • 2017 年 12 月 5 日,Conduit 0.1.0 版本发布

Envoy 和 Linkerd 都是在数据面上的实现, 属于同一个层面的竞争, 前者是用 C++ 语言实现的,在性能和资源消耗上要比采用 Scala 语言实现的 Linkerd 小,这一点对于延迟敏感型和资源敏的服务尤为重要.

Envoy 对 作为 Istio 的标准数据面实现, 其最主要的贡献是提供了一套标准数据面API, 将服务信息和流量规则下发到数据面的sidecar中, 另外Envoy还支持热重启. Istio早期采用了Envoy v1 API,目前的版本中则使用V2 API,V1已被废弃.

通过采用该标准API,Istio将控制面和数据面进行了解耦,为多种数据面sidecar实现提供了可能性。事实上基于该标准API已经实现了多种Sidecar代理和Istio的集成,除Istio目前集成的Envoy外,还可以和Linkerd, Nginmesh等第三方通信代理进行集成,也可以基于该API自己编写Sidecar实现.

将控制面和数据面解耦是Istio后来居上,风头超过Service mesh鼻祖Linkerd的一招妙棋。Istio站在了控制面的高度上,而Linkerd则成为了可选的一种sidecar实现.

Conduit 的整体架构和 Istio 一致,借鉴了 Istio 数据平面 + 控制平面的设计,而且选择了 Rust 编程语言来实现数据平面,以达成 Conduit 宣称的更轻、更快和超低资源占用.

(参考: 敖小剑 Service Mesh年度总结:群雄逐鹿烽烟起)

1.8 似曾相识的竞争格局

Kubernetes Istio
领域 容器编排 服务网格
主要竞品 Swarm, Mesos Linkerd, Conduit
主要盟友 RedHat, CoreOS IBM, Lyft
主要竞争对手 Docker 公司 Buoyant 公司
标准化 OCI: runtime spec, image spec XDS
插件化 CNI, CRI Istio CNI, Mixer Adapter
结果 Kubernetes 成为容器编排事实标准 ?

google 主导的Kubernetes 在容器编排领域取得了完胜, 目前在服务网格领域的打法如出一辙, 社区对Istio前景也比较看好.

Istio CNI 计划在1.1 作为实验特性, 用户可以通过扩展方式定制sidecar的网络.


1.9 国内Service Mesh 发展情况

  • 蚂蚁金服开源SOFAMesh: https://github.com/alipay/sofa-mesh

    • 从istio fork
    • 使用Golang语言开发全新的Sidecar,替代Envoy
    • 为了避免Mixer带来的性能瓶颈,合并Mixer部分功能进入Sidecar
    • Pilot和Citadel模块进行了大幅的扩展和增强
    • 扩展RPC协议: SOFARPC/HSF/Dubbo
  • 华为:

  • 腾讯云 TSF:

    • 基于 Istio、envoy 进行改造
    • 支持 Kubernetes、虚拟机以及裸金属的服务
    • 对 Istio 的能力进行了扩展和增强, 对 Consul 的完整适配
    • 对于其他二进制协议进行扩展支持
  • 唯品会

    • OSP (Open Service Platform)
  • 新浪:

    • Motan: 是一套基于java开发的RPC框架, Weibo Mesh 是基于Motan

2. Istio: 第二代 Service Mesh

Istio来自希腊语,英文意思是「sail」, 意为「启航」

  • 2.1 Istio 架构
  • 2.2 核心功能
  • 2.3 Istio 演示: BookInfo

2.1 Istio 架构

Istio Architecture(图片来自Isio官网文档)
  • 数据面

    • Sidecar
  • 控制面

    • Pilot:服务发现、流量管理
    • Mixer:访问控制、遥测
    • Citadel:终端用户认证、流量加密

2.2 核心功能

  • 流量管理
  • 安全
  • 可观察性
  • 多平台支持
  • 集成和定制

下面是我对Istio架构总结的思维导图:


2.3 Istio 演示: BookInfo

以下是Istio官网经典的 BookInfo Demo, 这是一个多语言组成的异构微服务系统:

Bookinfo Application(图片来自Isio官网文档)

下面我将现场给大家进行演示, 从demo安装开始, 并体验一下istio的流控功能:

使用helm管理istio

下载istio release: https://istio.io/docs/setup/kubernetes/download-release/

安装istio:
1
2
kubectl apply -f install/kubernetes/helm/istio/templates/crds.yaml
helm install install/kubernetes/helm/istio --name istio --namespace istio-system

注意事项, 若要开启sidecar自动注入功能, 需要:

  • 确保 kube-apiserver 启动参数 开启了ValidatingAdmissionWebhook 和 MutatingAdmissionWebhook
  • 给namespace 增加 label: kubectl label namespace default istio-injection=enabled
  • 同时还要保证 kube-apiserver 的 aggregator layer 开启: --enable-aggregator-routing=true 且证书和api server连通性正确设置.
如需卸载istio:
1
2
helm delete --purge istio
kubectl delete -f install/kubernetes/helm/istio/templates/crds.yaml -n istio-system

更多安装选择请参考: https://istio.io/docs/setup/kubernetes/helm-install/

安装Bookinfo Demo:

Bookinfo 是一个多语言异构的微服务demo, 其中 productpage 微服务会调用 details 和 reviews 两个微服务, reviews 会调用ratings 微服务, reviews 微服务有 3 个版本. 关于此项目更多细节请参考: https://istio.io/docs/examples/bookinfo/

部署应用:

kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml

这将创建 productpage, details, ratings, reviews 对应的deployments 和 service, 其中reviews 有三个deployments, 代表三个不同的版本.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 % kubectl get pod
NAME READY STATUS RESTARTS AGE
details-v1-6865b9b99d-mnxbt 2/2 Running 0 1m
productpage-v1-f8c8fb8-zjbhh 2/2 Running 0 59s
ratings-v1-77f657f55d-95rcz 2/2 Running 0 1m
reviews-v1-6b7f6db5c5-zqvkn 2/2 Running 0 59s
reviews-v2-7ff5966b99-lw72l 2/2 Running 0 59s
reviews-v3-5df889bcff-w9v7g 2/2 Running 0 59s

% kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
details ClusterIP 172.18.255.240 <none> 9080/TCP 1m
productpage ClusterIP 172.18.255.137 <none> 9080/TCP 1m
ratings ClusterIP 172.18.255.41 <none> 9080/TCP 1m
reviews ClusterIP 172.18.255.140 <none> 9080/TCP 1m

对入口流量进行配置:

1
kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml

该操作会创建bookinfo-gateway 的Gateway, 并将流量发送到productpage服务

1
2
3
kubectl get gateway
NAME AGE
bookinfo-gateway 1m

此时通过bookinfo-gateway 对应的LB或者nodeport 访问/productpage 页面, 可以看到三个版本的reviews服务在随机切换

基于权重的路由

通过CRD DestinationRule创建3 个reviews 子版本:

1
kubectl apply -f samples/bookinfo/networking/destination-rule-reviews.yaml

通过CRD VirtualService 调整个 reviews 服务子版本的流量比例, 设置 v1 和 v3 各占 50%

1
kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-50-v3.yaml

刷新页面, 可以看到无法再看到reviews v2的内容, 页面在v1和v3之间切换.

基于内容路由

修改reviews CRD, 将jason 登录的用户版本路由到v2, 其他用户路由到版本v3.

1
kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-jason-v2-v3.yaml

刷新页面, 使用jason登录的用户, 将看到v2 黑色星星版本, 其他用户将看到v3 红色星星版本.

更多BookInfo 示例, 请参阅: https://istio.io/docs/examples/bookinfo/, 若要删除应用: 执行脚本 ./samples/bookinfo/platform/kube/cleanup.sh


3. Istio 数据面

  • 3.1 数据面组件
  • 3.2 sidecar 流量劫持原理
  • 3.3 数据面标准API: xDS
  • 3.4 分布式跟踪

3.1 数据面组件

Istio 注入sidecar实现:

注入Pod内容:

  • istio-init: 通过配置iptables来劫持Pod中的流量
  • istio-proxy: 两个进程pilot-agent和envoy, pilot-agent 进行初始化并启动envoy

Sidecar 自动注入实现

Istio 利用 Kubernetes Dynamic Admission Webhooks 对pod 进行sidecar注入

查看istio 对这2个Webhooks 的配置 ValidatingWebhookConfiguration 和 MutatingWebhookConfiguration:

1
2
% kubectl get ValidatingWebhookConfiguration -oyaml
% kubectl get mutatingWebhookConfiguration -oyaml

可以看出:

  • 命名空间istio-system 中的服务 istio-galley, 通过路由/admitpilot, 处理config.istio.io部分, rbac.istio.io, authentication.istio.io, networking.istio.io等资源的Validating 工作
  • 命名空间istio-system 中的服务 istio-galley, 通过路由/admitmixer, 处理其他config.istio.io资源的Validating 工作
  • 命名空间istio-system 中的服务 istio-sidecar-injector, 通过路由/inject, 处理其他v1/pods的CREATE, 同时需要满足命名空间istio-injection: enabled

istio-init

数据面的每个Pod会被注入一个名为istio-init 的initContainer, initContrainer是K8S提供的机制,用于在Pod中执行一些初始化任务.在Initialcontainer执行完毕并退出后,才会启动Pod中的其它container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
initContainers:
- image: docker.io/istio/proxy_init:1.0.5
args:
- -p
- "15001"
- -u
- "1337"
- -m
- REDIRECT
- -i
- '*'
- -x
- ""
- -b
- "9080"
- -d
- ""

istio-init ENTRYPOINT 和 args 组合的启动命令:

1
/usr/local/bin/istio-iptables.sh -p 15001 -u 1337 -m REDIRECT -i '*' -x "" -b 9080 -d ""

istio-iptables.sh 源码地址为 https://github.com/istio/istio/blob/master/tools/deb/istio-iptables.sh

1
2
3
4
5
6
7
8
9
10
11
$ istio-iptables.sh -p PORT -u UID -g GID [-m mode] [-b ports] [-d ports] [-i CIDR] [-x CIDR] [-h]
-p: 指定重定向所有 TCP 流量的 Envoy 端口(默认为 $ENVOY_PORT = 15001)
-u: 指定未应用重定向的用户的 UID。通常,这是代理容器的 UID(默认为 $ENVOY_USER 的 uid,istio_proxy 的 uid 或 1337)
-g: 指定未应用重定向的用户的 GID。(与 -u param 相同的默认值)
-m: 指定入站连接重定向到 Envoy 的模式,“REDIRECT” 或 “TPROXY”(默认为 $ISTIO_INBOUND_INTERCEPTION_MODE)
-b: 逗号分隔的入站端口列表,其流量将重定向到 Envoy(可选)。使用通配符 “*” 表示重定向所有端口。为空时表示禁用所有入站重定向(默认为 $ISTIO_INBOUND_PORTS)
-d: 指定要从重定向到 Envoy 中排除(可选)的入站端口列表,以逗号格式分隔。使用通配符“*” 表示重定向所有入站流量(默认为 $ISTIO_LOCAL_EXCLUDE_PORTS)
-i: 指定重定向到 Envoy(可选)的 IP 地址范围,以逗号分隔的 CIDR 格式列表。使用通配符 “*” 表示重定向所有出站流量。空列表将禁用所有出站重定向(默认为 $ISTIO_SERVICE_CIDR)
-x: 指定将从重定向中排除的 IP 地址范围,以逗号分隔的 CIDR 格式列表。使用通配符 “*” 表示重定向所有出站流量(默认为 $ISTIO_SERVICE_EXCLUDE_CIDR)。

环境变量位于 $ISTIO_SIDECAR_CONFIG(默认在:/var/lib/istio/envoy/sidecar.env)

istio-init 通过配置iptable来劫持Pod中的流量:

  • 参数-p 15001: Pod中的数据流量被iptable拦截,并发向15001端口, 该端口将由 envoy 监听
  • 参数-u 1337: 用于排除用户ID为1337,即Envoy自身的流量,以避免Iptable把Envoy发出的数据又重定向到Envoy, UID 为 1337,即 Envoy 所处的用户空间,这也是 istio-proxy 容器默认使用的用户, 见Sidecar istio-proxy 配置参数securityContext.runAsUser
  • 参数-b 9080 -d "": 入站端口控制, 将所有访问 9080 端口(即应用容器的端口)的流量重定向到 Envoy 代理
  • 参数-i '*' -x "": 出站IP控制, 将所有出站流量都重定向到 Envoy 代理

Init 容器初始化完毕后就会自动终止,但是 Init 容器初始化结果(iptables)会保留到应用容器和 Sidecar 容器中.

istio-proxy

istio-proxy 以 sidecar 的形式注入到应用容器所在的pod中, 简化的注入yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- image: docker.io/istio/proxyv2:1.0.5
name: istio-proxy
args:
- proxy
- sidecar
- --configPath
- /etc/istio/proxy
- --binaryPath
- /usr/local/bin/envoy
- --serviceCluster
- ratings
- --drainDuration
- 45s
- --parentShutdownDuration
- 1m0s
- --discoveryAddress
- istio-pilot.istio-system:15007
- --discoveryRefreshDelay
- 1s
- --zipkinAddress
- zipkin.istio-system:9411
- --connectTimeout
- 10s
- --proxyAdminPort
- "15000"
- --controlPlaneAuthPolicy
- NONE
env:
......
ports:
- containerPort: 15090
name: http-envoy-prom
protocol: TCP
securityContext:
runAsUser: 1337
......

istio-proxy容器中有两个进程pilot-agent和envoy:

1
2
3
4
~ % kubectl exec productpage-v1-f8c8fb8-wgmzk -c istio-proxy -- ps -ef
UID PID PPID C STIME TTY TIME CMD
istio-p+ 1 0 0 Jan03 ? 00:00:27 /usr/local/bin/pilot-agent proxy sidecar --configPath /etc/istio/proxy --binaryPath /usr/local/bin/envoy --serviceCluster productpage --drainDuration 45s --parentShutdownDuration 1m0s --discoveryAddress istio-pilot.istio-system:15007 --discoveryRefreshDelay 1s --zipkinAddress zipkin.istio-system:9411 --connectTimeout 10s --proxyAdminPort 15000 --controlPlaneAuthPolicy NONE
istio-p+ 21 1 0 Jan03 ? 01:26:24 /usr/local/bin/envoy -c /etc/istio/proxy/envoy-rev0.json --restart-epoch 0 --drain-time-s 45 --parent-shutdown-time-s 60 --service-cluster productpage --service-node sidecar~172.18.3.12~productpage-v1-f8c8fb8-wgmzk.default~default.svc.cluster.local --max-obj-name-len 189 --allow-unknown-fields -l warn --v2-config-only

可以看到:

  • /usr/local/bin/pilot-agent/usr/local/bin/envoy 的父进程, Pilot-agent进程根据启动参数和K8S API Server中的配置信息生成Envoy的初始配置文件(/etc/istio/proxy/envoy-rev0.json),并负责启动Envoy进程
  • pilot-agent 的启动参数里包括: discoveryAddress(pilot服务地址), Envoy 二进制文件的位置, 服务集群名, 监控指标上报地址, Envoy 的管理端口, 热重启时间等

Envoy配置初始化流程:

  1. Pilot-agent根据启动参数和K8S API Server中的配置信息生成Envoy的初始配置文件envoy-rev0.json,该文件告诉Envoy从xDS server中获取动态配置信息,并配置了xDS server的地址信息,即控制面的Pilot
  2. Pilot-agent使用envoy-rev0.json启动Envoy进程
  3. Envoy根据初始配置获得Pilot地址,采用xDS接口从Pilot获取到Listener,Cluster,Route等d动态配置信息
  4. Envoy根据获取到的动态配置启动Listener,并根据Listener的配置,结合Route和Cluster对拦截到的流量进行处理

查看envoy 初始配置文件:

kubectl exec productpage-v1-f8c8fb8-wgmzk -c istio-proxy -- cat /etc/istio/proxy/envoy-rev0.json


3.2 sidecar 流量劫持原理

sidecar 既要作为服务消费者端的正向代理,又要作为服务提供者端的反向代理, 具体拦截过程如下:

  • Pod 所在的network namespace内, 除了envoy发出的流量外, iptables规则会对进入和发出的流量都进行拦截,通过nat redirect重定向到Envoy监听的15001端口.

  • envoy 会根据从Pilot拿到的 XDS 规则, 对流量进行转发.

  • envoy 的 listener 0.0.0.0:15001 接收进出 Pod 的所有流量,然后将请求移交给对应的virtual listener

  • 对于本pod的服务, 有一个http listener podIP+端口 接受inbound 流量

  • 每个service+非http端口, 监听器配对的 Outbound 非 HTTP 流量

  • 每个service+http端口, 有一个http listener: 0.0.0.0+端口 接受outbound流量

整个拦截转发过程对业务容器是透明的, 业务容器仍然使用 Service 域名和端口进行通信, service 域名仍然会转换为service IP, 但service IP 在sidecar 中会被直接转换为 pod IP, 从容器中出去的流量已经使用了pod IP会直接转发到对应的Pod, 对比传统kubernetes 服务机制, service IP 转换为Pod IP 在node上进行, 由 kube-proxy维护的iptables实现.


3.3 数据面标准API: xDS

xDS是一类发现服务的总称,包含LDS,RDS,CDS,EDS以及 SDS。Envoy通过xDS API可以动态获取Listener(监听器), Route(路由),Cluster(集群),Endpoint(集群成员)以 及Secret(证书)配置

xDS API 涉及的概念:

  • Host
  • Downstream
  • Upstream
  • Listener
  • Cluster

Envoy 配置热更新: 配置的动态变更,而不需要重启 Envoy:

  1. 新老进程采用基本的RPC协议使用Unix Domain Socket通讯.
  2. 新进程启动并完成所有初始化工作后,向老进程请求监听套接字的副本.
  3. 新进程接管套接字后,通知老进程关闭套接字.
  4. 通知老进程终止自己.

xDS 调试

Pilot在9093端口提供了下述调试接口:

1
2
3
4
5
6
7
8
9
# What is sent to envoy
# Listeners and routes
curl $PILOT/debug/adsz

# Endpoints
curl $PILOT/debug/edsz

# Clusters
curl $PILOT/debug/cdsz

Sidecar Envoy 也提供了管理接口,缺省为localhost的15000端口,可以获取listener,cluster以及完整的配置数据

可以通过以下命令查看支持的调试接口:

1
kubectl exec productpage-v1-f8c8fb8-zjbhh -c istio-proxy curl http://127.0.0.1:15000/help

或者forward到本地就行调试

1
kubectl port-forward productpage-v1-f8c8fb8-zjbhh 15000

相关的调试接口:

1
2
3
4
5
http://127.0.0.1:15000
http://127.0.0.1:15000/help
http://127.0.0.1:15000/config_dump
http://127.0.0.1:15000/listeners
http://127.0.0.1:15000/clusters

使用istioctl 查看代理配置:

1
2
3
4
istioctl pc {xDS类型}  {POD_NAME} {过滤条件} {-o json/yaml}

eg:
istioctl pc routes productpage-v1-f8c8fb8-zjbhh --name 9080 -o json

xDS 类型包括: listener, route, cluster, endpoint

对xDS 进行分析: productpage 访问 reviews 服务

查看 product 的所有listener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
% istioctl pc listener  productpage-v1-f8c8fb8-zjbhh
ADDRESS PORT TYPE
172.18.255.178 15011 TCP
172.18.255.194 44134 TCP
172.18.255.110 443 TCP
172.18.255.190 50000 TCP
172.18.255.203 853 TCP
172.18.255.2 443 TCP
172.18.255.239 16686 TCP
0.0.0.0 80 TCP
172.18.255.215 3306 TCP
172.18.255.203 31400 TCP
172.18.255.111 443 TCP
172.18.255.203 8060 TCP
172.18.255.203 443 TCP
172.18.255.40 443 TCP
172.18.255.1 443 TCP
172.18.255.53 53 TCP
172.18.255.203 15011 TCP
172.18.255.105 14268 TCP
172.18.255.125 42422 TCP
172.18.255.105 14267 TCP
172.18.255.52 80 TCP
0.0.0.0 15010 HTTP
0.0.0.0 9411 HTTP
0.0.0.0 8060 HTTP
0.0.0.0 9080 HTTP
0.0.0.0 15004 HTTP
0.0.0.0 20001 HTTP
0.0.0.0 9093 HTTP
0.0.0.0 8080 HTTP
0.0.0.0 15030 HTTP
0.0.0.0 9091 HTTP
0.0.0.0 9090 HTTP
0.0.0.0 15031 HTTP
0.0.0.0 3000 HTTP
0.0.0.0 15001 TCP
172.18.3.50 9080 HTTP 这是当前pod ip 暴露的服务地址, 会路由到回环地址, 各个pod 会不一样

envoy 流量入口的listener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
% istioctl pc listener  productpage-v1-f8c8fb8-zjbhh --address 0.0.0.0 --port 15001 -o json
[
{
"name": "virtual",
"address": {
"socketAddress": {
"address": "0.0.0.0",
"portValue": 15001
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.tcp_proxy",
"config": {
"cluster": "BlackHoleCluster",
"stat_prefix": "BlackHoleCluster"
}
}
]
}
],
"useOriginalDst": true # 这意味着它将请求交给最符合请求原始目标的监听器。如果找不到任何匹配的虚拟监听器,它会将请求发送给返回 404 的 BlackHoleCluster
}
]

以下是reviews的所有pod IP

1
2
3
 % kubectl get ep reviews
NAME ENDPOINTS AGE
reviews 172.18.2.35:9080,172.18.3.48:9080,172.18.3.49:9080 1d

对于目的地址是以上ip的http访问, 这些 ip 并没有对应的listener, 因此会通过端口9080 匹配到listener 0.0.0.0 9080

查看listener 0.0.0.0 9080:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
% istioctl pc listener  productpage-v1-f8c8fb8-zjbhh --address 0.0.0.0 --port 9080 -ojson
{
"name": "0.0.0.0_9080",
"address": {
"socketAddress": {
"address": "0.0.0.0",
"portValue": 9080
}
},
......

"rds": {
"config_source": {
"ads": {}
},
"route_config_name": "9080"
},
......

查看名为9080 的 route:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
% istioctl pc routes  productpage-v1-f8c8fb8-zjbhh --name 9080 -o json

[
{
"name": "9080",
"virtualHosts": [
{
"name": "details.default.svc.cluster.local:9080",
"domains": [
"details.default.svc.cluster.local",
"details.default.svc.cluster.local:9080",
"details",
"details:9080",
"details.default.svc.cluster",
"details.default.svc.cluster:9080",
"details.default.svc",
"details.default.svc:9080",
"details.default",
"details.default:9080",
"172.18.255.240",
"172.18.255.240:9080"
],
"routes": [
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "outbound|9080||details.default.svc.cluster.local",
"timeout": "0.000s",
"maxGrpcTimeout": "0.000s"
},
......
{
"name": "productpage.default.svc.cluster.local:9080",
"domains": [
"productpage.default.svc.cluster.local",
"productpage.default.svc.cluster.local:9080",
"productpage",
"productpage:9080",
"productpage.default.svc.cluster",
"productpage.default.svc.cluster:9080",
"productpage.default.svc",
"productpage.default.svc:9080",
"productpage.default",
"productpage.default:9080",
"172.18.255.137",
"172.18.255.137:9080"
],
"routes": [ ...... ]
},
{
"name": "ratings.default.svc.cluster.local:9080",
"domains": [
"ratings.default.svc.cluster.local",
"ratings.default.svc.cluster.local:9080",
"ratings",
"ratings:9080",
"ratings.default.svc.cluster",
"ratings.default.svc.cluster:9080",
"ratings.default.svc",
"ratings.default.svc:9080",
"ratings.default",
"ratings.default:9080",
"172.18.255.41",
"172.18.255.41:9080"
],
"routes": [ ...... ]
},
{
"name": "reviews.default.svc.cluster.local:9080",
"domains": [
"reviews.default.svc.cluster.local",
"reviews.default.svc.cluster.local:9080",
"reviews",
"reviews:9080",
"reviews.default.svc.cluster",
"reviews.default.svc.cluster:9080",
"reviews.default.svc",
"reviews.default.svc:9080",
"reviews.default",
"reviews.default:9080",
"172.18.255.140",
"172.18.255.140:9080"
],
"routes": [
{
"match": {
"prefix": "/",
"headers": [
{
"name": "end-user",
"exactMatch": "jason"
}
]
},
"route": {
"cluster": "outbound|9080|v2|reviews.default.svc.cluster.local",
"timeout": "0.000s",
"maxGrpcTimeout": "0.000s"
},
......
},
{
"match": {
"prefix": "/"
},
"route": {
"cluster": "outbound|9080|v3|reviews.default.svc.cluster.local",
"timeout": "0.000s",
"maxGrpcTimeout": "0.000s"
},
.......
}
]
}
],
"validateClusters": false
}
]

可以看到, 在9080 这个route 中, 包含所有这个端口的http 路由信息, 通过virtualHosts列表进行服务域名分发到各个cluster.

查看virtualHosts reviews.default.svc.cluster.local:9080 中的routes信息, 可以看到jason 路由到了cluster outbound|9080|v2|reviews.default.svc.cluster.local

查看该cluster:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
% istioctl pc cluster productpage-v1-f8c8fb8-zjbhh --fqdn reviews.default.svc.cluster.local --subset v2 -o json
[
{
"name": "outbound|9080|v2|reviews.default.svc.cluster.local",
"type": "EDS",
"edsClusterConfig": {
"edsConfig": {
"ads": {}
},
"serviceName": "outbound|9080|v2|reviews.default.svc.cluster.local"
},
"connectTimeout": "1.000s",
"lbPolicy": "RANDOM",
"circuitBreakers": {
"thresholds": [
{}
]
}
}
]

查看其对应的endpoint:

1
2
3
 % istioctl pc endpoint productpage-v1-f8c8fb8-zjbhh --cluster 'outbound|9080|v2|reviews.default.svc.cluster.local'
ENDPOINT STATUS CLUSTER
172.18.2.35:9080 HEALTHY outbound|9080|v2|reviews.default.svc.cluster.local

该endpoint 即为 reviews 服务 V2 对应的 pod IP

XDS服务接口的最终一致性考虑

遵循 make before break 模型


3.4 分布式跟踪

以下是分布式全链路跟踪示意图:


一个典型的Trace案例(图片来自opentracing文档中文版)

Jaeger 是Uber 开源的全链路跟踪系统, 符合OpenTracing协议, OpenTracing 和 Jaeger 均是CNCF 成员项目, 以下是Jaeger 架构的示意图:

Jaeger 架构示意图(图片来自Jaeger官方文档)

分布式跟踪系统让开发者能够得到可视化的调用流程展示。这对复杂的微服务系统进行问题排查和性能优化时至关重要.

Envoy 原生支持http 链路跟踪:

  • 生成 Request ID:Envoy 会在需要的时候生成 UUID,并操作名为 [x-request-id] 的 HTTP Header。应用可以转发这个 Header 用于统一的记录和跟踪.
  • 支持集成外部跟踪服务:Envoy 支持可插接的外部跟踪可视化服务。目前支持有:
    • LightStep
    • Zipkin 或者 Zipkin 兼容的后端(比如说 Jaeger)
    • Datadog
  • 客户端跟踪 ID 连接:x-client-trace-id Header 可以用来把不信任的请求 ID 连接到受信的 x-request-id Header 上

跟踪上下文信息的传播

  • 不管使用的是哪个跟踪服务,都应该传播 x-request-id,这样在被调用服务中启动相关性的记录
  • 如果使用的是 Zipkin,Envoy 要传播的是 B3 Header。(x-b3-traceid, x-b3-spanid, x-b3-parentspanid, x-b3-sampled, 以及 x-b3-flags. x-b3-sampled)
  • 上下文跟踪并非零修改, 在调用下游服务时, 上游应用应该自行传播跟踪相关的 HTTP Header

4. Istio 控制面

  • 4.1 Pilot 架构
  • 4.2 流量管理模型
  • 4.3 故障处理
  • 4.4 Mixer 架构
  • 4.5 Mixer适配器模型
  • 4.6 Mixer 缓存机制

4.1 Pilot 架构

Pilot Architecture(图片来自Isio官网文档)
  • Rules API: 对外封装统一的 API,供服务的开发者或者运维人员调用,可以用于流量控制。
  • Envoy API: 对内封装统一的 API,供 Envoy 调用以获取注册信息、流量控制信息等。
  • 抽象模型层: 对服务的注册信息、流量控制规则等进行抽象,使其描述与平台无关。
  • 平台适配层: 用于适配各个平台如 Kubernetes、Mesos、Cloud Foundry 等,把平台特定的注册信息、资源信息等转换成抽象模型层定义的平台无关的描述。例如,Pilot 中的 Kubernetes 适配器实现必要的控制器来 watch Kubernetes API server 中 pod 注册信息、ingress 资源以及用于存储流量管理规则的第三方资源的更改

4.2 流量管理模型

  • VirtualService
  • DestinationRule
  • ServiceEntry
  • Gateway

VirtualService

VirtualService 中定义了一系列针对指定服务的流量路由规则。每个路由规则都是针对特定协议的匹配规则。如果流量符合这些特征,就会根据规则发送到服务注册表中的目标服务, 或者目标服务的子集或版本, 匹配规则中还包含了对流量发起方的定义,这样一来,规则还可以针对特定客户上下文进行定制.

Gateway

Gateway 描述了一个负载均衡器,用于承载网格边缘的进入和发出连接。这一规范中描述了一系列开放端口,以及这些端口所使用的协议、负载均衡的 SNI 配置等内容

ServiceEntry

Istio 服务网格内部会维护一个与平台无关的使用通用模型表示的服务注册表,当你的服务网格需要访问外部服务的时候,就需要使用 ServiceEntry 来添加服务注册, 这类服务可能是网格外的 API,或者是处于网格内部但却不存在于平台的服务注册表中的条目(例如需要和 Kubernetes 服务沟通的一组虚拟机服务).

EnvoyFilter

EnvoyFilter 描述了针对代理服务的过滤器,用来定制由 Istio Pilot 生成的代理配置.

Kubernetes Ingress vs Istio Gateway

  • 合并了L4-6和L7的规范, 对传统技术栈用户的应用迁入不方便
  • 表现力不足:
    • 只能对 service、port、HTTP 路径等有限字段匹配来路由流量
    • 端口只支持默认80/443

Istio Gateway:·

  • 定义了四层到六层的负载均衡属性 (通常是SecOps或NetOps关注的内容)
    • 端口
    • 端口所使用的协议(HTTP, HTTPS, GRPC, HTTP2, MONGO, TCP, TLS)
    • Hosts
    • TLS SNI header 路由支持
    • TLS 配置支持(http 自动301, 证书等)
    • ip / unix domain socket

Kubernetes, Istio, Envoy xDS 模型对比

以下是对Kubernetes, Istio, Envoy xDS 模型的不严格对比

Kubernetes Istio Envoy xDS
入口流量 Ingress GateWay Listener
服务定义 Service - Cluster+Listener
外部服务定义 - ServiceEntry Cluster+Listener
版本定义 - DestinationRule Cluster+Listener
版本路由 - VirtualService Route
实例 Endpoint - Endpoint

Kubernetes 和 Istio 服务寻址的区别:

Kubernetes:

  1. kube-dns: service domain -> service ip
  2. kube-proxy(node iptables): service ip -> pod ip

Istio:

  1. kube-dns: service domain -> service ip
  2. sidecar envoy: service ip -> pod ip

4.3 故障处理

随着微服务的拆分粒度增强, 服务调用会增多, 更复杂, 扇入 扇出, 调用失败的风险增加, 以下是常见的服务容错处理方式:

控制端 目的 实现 Istio
超时 client 保护client 请求等待超时/请求运行超时 timeout
重试 client 容忍server临时错误, 保证业务整体可用性 重试次数/重试的超时时间 retries.attempts, retries.perTryTimeout
熔断 client 降低性能差的服务或实例的影响 通常会结合超时+重试, 动态进行服务状态决策(Open/Closed/Half-Open) trafficPolicy.outlierDetection
降级 client 保证业务主要功能可用 主逻辑失败采用备用逻辑的过程(镜像服务分级, 调用备用服务, 或者返回mock数据) 暂不支持, 需要业务代码按需实现
隔离 client 防止异常server占用过多client资源 隔离对不同服务调用的资源依赖: 线程池隔离/信号量隔离 暂不支持
幂等 server 容忍client重试, 保证数据一致性 唯一ID/加锁/事务等手段 暂不支持, 需要业务代码按需实现
限流 server 保护server 常用算法: 计数器, 漏桶, 令牌桶 trafficPolicy.connectionPool

Istio 没有无降级处理支持: Istio可以提高网格中服务的可靠性和可用性。但是,应用程序仍然需要处理故障(错误)并采取适当的回退操作。例如,当负载均衡池中的所有实例都失败时,Envoy 将返回 HTTP 503。应用程序有责任实现必要的逻辑,对这种来自上游服务的 HTTP 503 错误做出合适的响应。


4.4 Mixer 架构

Mixer Topology(图片来自Isio官网文档)

Istio 的四大功能点连接, 安全, 控制, 观察, 其中「控制」和「观察」的功能主要都是由Mixer组件来提供, Mixer 在Istio中角色:

  • 功能上: 负责策略控制和遥测收集
  • 架构上:提供插件模型,可以扩展和定制

4.5 Mixer Adapter 模型

  • Attribute
  • Template
  • Adapter
  • Instance
  • Handler
  • Rule

Attribute

Attribute 是策略和遥测功能中有关请求和环境的基本数据, 是用于描述特定服务请求或请求环境的属性的一小段数据。例如,属性可以指定特定请求的大小、操作的响应代码、请求来自的 IP 地址等.

  • Istio 中的主要属性生产者是 Envoy,但专用的 Mixer 适配器也可以生成属性
  • 属性词汇表见: Attribute Vocabulary
  • 数据流向: envoy -> mixer

Template

Template 是对 adapter 的数据格式和处理接口的抽象, Template定义了:

  • 当处理请求时发送给adapter 的数据格式
  • adapter 必须实现的gRPC service 接口

每个Template 通过 template.proto 进行定义:

  • 名为Template 的一个message
  • Name: 通过template所在的package name自动生成
  • template_variety: 可选Check, Report, Quota or AttributeGenerator, 决定了adapter必须实现的方法. 同时决定了在mixer的什么阶段要生成template对应的instance:
    • Check: 在Mixer’s Check API call时创建并发送instance
    • Report: 在Mixer’s Report API call时创建并发送instance
    • Quota: 在Mixer’s Check API call时创建并发送instance(查询配额时)
    • AttributeGenerator: for both Check, Report Mixer API calls

Istio 内置的Templates: https://istio.io/docs/reference/config/policy-and-telemetry/templates/

Adapter

封装了 Mixer 和特定外部基础设施后端进行交互的必要接口,例如 Prometheus 或者 Stackdriver

  • 定义了需要处理的模板(在yaml中配置template)
  • 定义了处理某个Template数据格式的GRPC接口
  • 定义 Adapter需要的配置格式(Params)
  • 可以同时处理多个数据(instance)

Istio 内置的Adapter: https://istio.io/docs/reference/config/policy-and-telemetry/adapters/

Instance

代表符合某个Template定义的数据格式的具体实现, 该具体实现由用户配置的 CRD, CRD 定义了将Attributes 转换为具体instance 的规则, 支持属性表达式

  • Instance CRD 是Template 中定义的数据格式 + 属性转换器
  • 内置的Instance 类型(其实就是内置 Template): Templates
  • 属性表达式见: Expression Language
  • 数据流向: mixer -> adapter 实例

Handler

用户配置的 CRD, 为具体Adapter提供一个具体配置, 对应Adapter的可运行实例

Rule

用户配置的 CRD, 配置一组规则,这些规则描述了何时调用特定(通过Handler对应的)适配器及哪些Instance


结语

计算机科学中的所有问题,都可以用另一个层来解决,除了层数太多的问题

Kubernetes 本身已经很复杂, Istio 为了更高层控制的抽象, 又增加了很多概念. 复杂度堪比kubernetes.

可以看出istio 设计精良, 在处理微服务的复杂场景有很多优秀之处, 不过目前istio目前的短板还是很明显, 高度的抽象带来了很多性能的损耗, 社区现在也有很多优化的方向, 像蚂蚁金服开源的SofaMesh 主要是去精简层, 试图在sidecar里去做很多mixer 的事情, 减少sidecar和mixer的同步请求依赖, 而一些其他的sidecar 网络方案, 更多的是考虑去优化层, 优化sidecar 这一层的性能开销.

在Istio 1.0 之前, 主要还是以功能的实现为主, 不过后面随着社区的积极投入, 相信Istio的性能会有长足的提升.

笔者之前从事过多年的服务治理相关的工作, 过程中切身体会到微服务治理的痛点, 所以也比较关注 service mesh的发展, 个人对istio也非常看好, 刚好今年我们中心容器产品今年也有这方面的计划, 期待我们能在这个方向进行一些产品和技术的深耕.



参考资料:

Kubernetes 流量复制方案

作者:田小康

背景

测试环境没有真实的数据, 会导致很多测试工作难以展开, 尤其是一些测试任务需要使用生产环境来做时, 会极大影响现网的稳定性。

我们需要一个流量复制方案, 将现网流量复制到预发布/测试环境

流量复制示意

期望

  • 将线上请求拷贝一份到预发布/测试环境
  • 不影响现网请求
  • 可配置流量复制比例, 毕竟测试环境资源有限
  • 零代码改动

方案

Kubernetes 流量复制方案

  • 承载入口流量的 Pod 新增一个 Nginx 容器 接管流量
  • Nginx Mirror 模块会将流量复制一份并 proxy 到指定 URL (测试环境)
  • Nginx mirror 复制流量不会影响正常请求处理流程, 镜像请求的 Resp 会被 Nginx 丢弃
  • K8s Service 按照 Label Selector 去选择请求分发的 Pod, 意味着不同Pod, 只要有相同 Label, 就可以协同处理请求
  • 通过控制有 Mirror 功能的 Pod正常的 Pod 的比例, 便可以配置流量复制的比例

我们的部署环境为 腾讯云容器服务, 不过所述方案是普适于 Kubernetes 环境的.

实现

PS: 下文假定读者了解

Nginx 镜像

使用 Nginx 官方镜像便已经预装了 Mirror 插件

即: docker pull nginx

yum install nginx 安装的版本貌似没有 Mirror 插件的哦, 需要自己装

Nginx ConfigMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
kind: ConfigMap
metadata:
name: entrance-nginx-config
namespace: default
apiVersion: v1
data:
nginx.conf: |-
worker_processes auto;

error_log /data/athena/logs/entrance/nginx-error.log;

events {
worker_connections 1024;
}

http {
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;

server {
access_log /data/athena/logs/entrance/nginx-access.log;

listen {{ .Values.entrance.service.nodePort }};
server_name entrance;

location / {
root html;
index index.html index.htm;
}

location /entrance/ {
mirror /mirror;
access_log /data/athena/logs/entrance/nginx-entrance-access.log;
proxy_pass http://localhost:{{ .Values.entrance.service.nodePortMirror }}/;
}

location /mirror {
internal;
access_log /data/athena/logs/entrance/nginx-mirror-access.log;
proxy_pass {{ .Values.entrance.mirrorProxyPass }};
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}

其中重点部分如下:

业务方容器 + Nginx Mirror

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
{{- if .Values.entrance.mirrorEnable }}
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: entrance-mirror
spec:
replicas: {{ .Values.entrance.mirrorReplicaCount }}
template:
metadata:
labels:
name: entrance
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
podAffinityTerm:
labelSelector:
matchExpressions:
- key: "name"
operator: In
values:
- entrance
topologyKey: "kubernetes.io/hostname"
initContainers:
- name: init-kafka
image: "centos-dev"
{{- if .Values.delay }}
command: ['bash', '-c', 'sleep 480s; until nslookup athena-cp-kafka; do echo "waiting for athena-cp-kafka"; sleep 2; done;']
{{- else }}
command: ['bash', '-c', 'until nslookup athena-cp-kafka; do echo "waiting for athena-cp-kafka"; sleep 2; done;']
{{- end }}

containers:
- image: "{{ .Values.entrance.image.repository }}:{{ .Values.entrance.image.tag }}"
name: entrance
ports:
- containerPort: {{ .Values.entrance.service.nodePort }}
env:
- name: ATHENA_KAFKA_BOOTSTRAP
value: "{{ .Values.kafka.kafkaBootstrap }}"
- name: ATHENA_KAFKA_SCHEMA_REGISTRY_URL
value: "{{ .Values.kafka.kafkaSchemaRegistryUrl }}"
- name: ATHENA_PG_CONN
value: "{{ .Values.pg.pgConn }}"
- name: ATHENA_COS_CONN
value: "{{ .Values.cos.cosConn }}"
- name: ATHENA_DEPLOY_TYPE
value: "{{ .Values.deployType }}"
- name: ATHENA_TPS_SYS_ID
value: "{{ .Values.tps.tpsSysId }}"
- name: ATHENA_TPS_SYS_SECRET
value: "{{ .Values.tps.tpsSysSecret }}"
- name: ATHENA_TPS_BASE_URL
value: "{{ .Values.tps.tpsBaseUrl }}"
- name: ATHENA_TPS_RESOURCE_FLOW_PERIOD_SEC
value: "{{ .Values.tps.tpsResourceFlowPeriodSec }}"
- name: ATHENA_CLUSTER
value: "{{ .Values.cluster }}"
- name: ATHENA_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: ATHENA_HOST_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: ATHENA_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP

command: ['/bin/bash', '/data/service/go_workspace/script/start-entrance.sh', '-host 0.0.0.0:{{ .Values.entrance.service.nodePortMirror }}']

volumeMounts:
- mountPath: /data/athena/
name: athena
readOnly: false

imagePullPolicy: IfNotPresent

resources:
limits:
cpu: 3000m
memory: 800Mi
requests:
cpu: 100m
memory: 100Mi

livenessProbe:
exec:
command:
- bash
- /data/service/go_workspace/script/health-check/check-entrance.sh
initialDelaySeconds: 120
periodSeconds: 60

- image: "{{ .Values.nginx.image.repository }}:{{ .Values.nginx.image.tag }}"
name: entrance-mirror
ports:
- containerPort: {{ .Values.entrance.service.nodePort }}

volumeMounts:
- mountPath: /data/athena/
name: athena
readOnly: false
- mountPath: /etc/nginx/nginx.conf
name: nginx-config
subPath: nginx.conf

imagePullPolicy: IfNotPresent

resources:
limits:
cpu: 1000m
memory: 500Mi
requests:
cpu: 100m
memory: 100Mi

livenessProbe:
tcpSocket:
port: {{ .Values.entrance.service.nodePort }}
timeoutSeconds: 3
initialDelaySeconds: 60
periodSeconds: 60

terminationGracePeriodSeconds: 10

nodeSelector:
entrance: "true"

volumes:
- name: athena
hostPath:
path: "/data/athena/"
- name: nginx-config
configMap:
name: entrance-nginx-config

imagePullSecrets:
- name: "{{ .Values.imagePullSecrets }}"
{{- end }}

上面为真实在业务中使用的 Deployment 配置, 有些地方可以参考:

  • valueFrom.fieldRef.fieldPath 可以取到容器运行时的一些字段, 如 NodeIP, PodIP 这些可以用于全链路监控
  • ConfigMap 直接 Mount 到文件系统, 覆盖默认配置的例子
  • affinity.podAntiAffinity 亲和性调度, 使 Pod 在主机间均匀分布
  • 使用了 tcpSocketexec.command 两种健康检查方式

Helm Values

1
2
3
4
5
6
7
8
9
10
11
12
13
# entrance, Athena 上报入口模块
entrance:
enable: true
replicaCount: 3
mirrorEnable: true
mirrorReplicaCount: 1
mirrorProxyPass: "http://10.16.0.147/entrance/"
image:
repository: athena-go
tag: v1901091026
service:
nodePort: 30081
nodePortMirror: 30082

如上, replicaCount: 3 + mirrorReplicaCount: 1 = 4 个容器, 有 1/4 流量复制到 http://10.16.0.147/entrance/

内网负载均衡

流量复制到测试环境时, 尽量使用内网负载均衡, 为了成本, 安全及性能方面的考虑

LB-inner-config

总结

通过下面几个步骤, 便可以实现流量复制啦

  • 建一个内网负载均衡, 暴漏测试环境的 服务入口 Service
  • 服务入口 Service 需要有可以更换端口号的能力 (例如命令行参数/环境变量)
  • 线上环境, 新增一个 Deployment, Label 和之前的 服务入口 Service 一样, 只是端口号分配一个新的
  • 为新增的 Deployment 增加一个 Nginx 容器, 配置 nginx.conf
  • 调节有 Nginx Mirror 的 Pod 和 正常的 Pod 比例, 便可以实现按比例流量复制

Cgroup泄漏--潜藏在你的集群中

作者: 洪志国

前言

绝大多数的kubernetes集群都有这个隐患。只不过一般情况下,泄漏得比较慢,还没有表现出来而已。

一个pod可能泄漏两个memory cgroup数量配额。即使pod百分之百发生泄漏, 那也需要一个节点销毁过三万多个pod之后,才会造成后续pod创建失败。

一旦表现出来,这个节点就彻底不可用了,必须重启才能恢复。

故障表现

腾讯云SCF(Serverless Cloud Function)底层使用我们的TKE(Tencent Kubernetes Engine),并且会在节点上频繁创建和消耗容器。

SCF发现很多节点会出现类似以下报错,创建POD总是失败:

1
Dec 24 11:54:31 VM_16_11_centos dockerd[11419]: time="2018-12-24T11:54:31.195900301+08:00" level=error msg="Handler for POST /v1.31/containers/b98d4aea818bf9d1d1aa84079e1688cd9b4218e008c58a8ef6d6c3c106403e7b/start returned error: OCI runtime create failed: container_linux.go:348: starting container process caused \"process_linux.go:279: applying cgroup configuration for process caused \\\"mkdir /sys/fs/cgroup/memory/kubepods/burstable/pod79fe803c-072f-11e9-90ca-525400090c71/b98d4aea818bf9d1d1aa84079e1688cd9b4218e008c58a8ef6d6c3c106403e7b: no space left on device\\\"\": unknown"

这个时候,到节点上尝试创建几十个memory cgroup (以root权限执行 for i inseq 1 20;do mkdir /sys/fs/cgroup/memory/${i}; done),就会碰到失败:

1
mkdir: cannot create directory '/sys/fs/cgroup/memory/8': No space left on device

其实,dockerd出现以上报错时, 手动创建一个memory cgroup都会失败的。 不过有时候随着一些POD的运行结束,可能会多出来一些“配额”,所以这里是尝试创建20个memory cgroup。

出现这样的故障以后,重启docker,释放内存等措施都没有效果,只有重启节点才能恢复。

复现条件

docker和kubernetes社区都有关于这个问题的issue:

网上有文章介绍了类似问题的分析和复现方法。如:
http://www.linuxfly.org/kubernetes-19-conflict-with-centos7/?from=groupmessage

不过按照文中的复现方法,我在3.10.0-862.9.1.el7.x86_64版本内核上并没有复现出来。

经过反复尝试,总结出了必现的复现条件。 一句话感慨就是,把进程加入到一个开启了kmem accounting的memory cgroup并且执行fork系统调用

  1. centos 3.10.0-862.9.1.el7.x86_64及以下内核, 4G以上空闲内存,root权限。
  2. 把系统memory cgroup配额占满

    1
    for i in `seq 1 65536`;do mkdir /sys/fs/cgroup/memory/${i}; done

    会看到报错:

    1
    mkdir: cannot create directory ‘/sys/fs/cgroup/memory/65530’: No space left on device

    这是因为这个版本内核写死了,最多只能有65535个memory cgroup共存。 systemd已经创建了一些,所以这里创建不到65535个就会遇到报错。

    确认删掉一个memory cgroup, 就能腾出一个“配额”:

    1
    2
    rmdir /sys/fs/cgroup/memory/1
    mkdir /sys/fs/cgroup/memory/test
  3. 给一个memory cgroup开启kmem accounting

    1
    2
    3
    cd /sys/fs/cgroup/memory/test/
    echo 1 > memory.kmem.limit_in_bytes
    echo -1 > memory.kmem.limit_in_bytes
  4. 把一个进程加进某个memory cgroup, 并执行一次fork系统调用

    1
    2
    3
    4
    最简单的就是把当前shell进程加进去:
    echo $$ > /sys/fs/cgroup/memory/test/tasks
    sleep 100 &
    cat /sys/fs/cgroup/memory/test/tasks
  5. 把该memory cgroup里面的进程都挪走

    1
    2
    3
    for p in `cat /sys/fs/cgroup/memory/test/tasks`;do echo ${p} > /sys/fs/cgroup/memory/tasks; done

    cat /sys/fs/cgroup/memory/test/tasks //这时候应该为空
  6. 删除这个memory cgroup

    1
    rmdir /sys/fs/cgroup/memory/test
  7. 验证刚才删除一个memory cgroup, 所占的配额并没有释放

    1
    mkdir /sys/fs/cgroup/memory/xx

    这时候会报错:mkdir: cannot create directory ‘/sys/fs/cgroup/memory/xx’: No space left on device

什么版本的内核有这个问题

搜索内核commit记录,有一个commit应该是解决类似问题的:

1
4bdfc1c4a943: 2015-01-08 memcg: fix destination cgroup leak on task charges migration [Vladimir Davydov]

这个commit在3.19以及4.x版本的内核中都已经包含。 不过从docker和kubernetes相关issue里面的反馈来看,内核中应该还有其他cgroup泄漏的代码路径, 4.14版本内核都还有cgroup泄漏问题。

规避办法

不开启kmem accounting (以上复现步骤的第3步)的话,是不会发生cgroup泄漏的。

kubelet和runc都会给memory cgroup开启kmem accounting。所以要规避这个问题,就要保证kubelet和runc,都别开启kmem accounting。下面分别进行说明。

runc

查看代码,发现在commit fe898e7 (2017-2-25, PR #1350)以后的runc版本中,都会默认开启kmem accounting。代码在libcontainer/cgroups/fs/kmem.go: (老一点的版本,代码在libcontainer/cgroups/fs/memory.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const cgroupKernelMemoryLimit = "memory.kmem.limit_in_bytes"

func EnableKernelMemoryAccounting(path string) error {
// Ensure that kernel memory is available in this kernel build. If it
// isn't, we just ignore it because EnableKernelMemoryAccounting is
// automatically called for all memory limits.
if !cgroups.PathExists(filepath.Join(path, cgroupKernelMemoryLimit)) {
return nil
}
// We have to limit the kernel memory here as it won't be accounted at all
// until a limit is set on the cgroup and limit cannot be set once the
// cgroup has children, or if there are already tasks in the cgroup.
for _, i := range []int64{1, -1} {
if err := setKernelMemory(path, i); err != nil {
return err
}
}
return nil
}

runc社区也注意到这个问题,并做了比较灵活的修复: https://github.com/opencontainers/runc/pull/1921

这个修复给runc增加了”nokmem”编译选项。缺省的release版本没有使用这个选项。 自己使用nokmem选项编译runc的方法:

1
2
cd $GO_PATH/src/github.com/opencontainers/runc/
make BUILDTAGS="seccomp nokmem"

kubelet

kubelet在创建pod对应的cgroup目录时,也会调用libcontianer中的代码对cgroup做设置。在 pkg/kubelet/cm/cgroup_manager_linux.go的Create方法中,会调用Manager.Apply方法,最终调用vendor/github.com/opencontainers/runc/libcontainer/cgroups/fs/memory.go中的MemoryGroup.Apply方法,开启kmem accounting。

这里也需要进行处理,可以不开启kmem accounting, 或者通过命令行参数来控制是否开启。

kubernetes社区也有issue讨论这个问题:https://github.com/kubernetes/kubernetes/issues/70324

但是目前还没有结论。我们TKE先直接把这部分代码注释掉了,不开启kmem accounting。

给容器设置内核参数

作者: 洪志国

sysctl

/proc/sys/目录下导出了一些可以在运行时修改kernel参数的proc文件。

1
2
# ls /proc/sys
abi crypto debug dev fs kernel net vm

可以通过写proc文件来修改这些内核参数。例如, 要打开ipv4的路由转发功能:

1
echo 1 > /proc/sys/net/ipv4/ip_forward

也可以通过sysctl命令来完成(只是对以上写proc文件操作的简单包装):

1
sysctl -w net.ipv4.ip_forward=1

其他常用sysctl命令:

显示本机所有sysctl内核参数及当前值

1
sysctl -a

从文件(缺省使用/etc/sysctl.conf)加载多个参数和取值,并写入内核

1
sysctl -p [FILE]

另外, 系统启动的时候, 会自动执行一下”sysctl -p”。 所以,希望重启之后仍然生效的参数值, 应该写到/etc/sysctl.conf文件里面。

容器与sysctl

内核方面做了大量的工作,把一部分sysctl内核参数进行了namespace化(namespaced)。 也就是多个容器和主机可以各自独立设置某些内核参数。例如, 可以通过net.ipv4.ip_local_port_range,在不同容器中设置不同的端口范围。

如何判断一个参数是不是namespaced?

运行一个具有privileged权限的容器(参考下一节内容), 然后在容器中修改该参数,看一下在host上能否看到容器在中所做的修改。如果看不到, 那就是namespaced, 否则不是。

目前已经namespace化的sysctl内核参数:

  • kernel.shm*,
  • kernel.msg*,
  • kernel.sem,
  • fs.mqueue.*,
  • net.*.

注意, vm.*并没有namespace化。 比如vm.max_map_count, 在主机或者一个容器中设置它, 其他所有容器都会受影响,都会看到最新的值。

在docker容器中修改sysctl内核参数

正常运行的docker容器中,是不能修改任何sysctl内核参数的。因为/proc/sys是以只读方式挂载到容器里面的。

1
proc on /proc/sys type proc (ro,nosuid,nodev,noexec,relatime)

要给容器设置不一样的sysctl内核参数,有多种方式。

方法一 –privileged

1
# docker run --privileged -it ubuntu bash

整个/proc目录都是以”rw”权限挂载的

1
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)

在容器中,可以任意修改sysctl内核参赛。

注意:
如果修改的是namespaced的参数, 则不会影响host和其他容器。反之,则会影响它们。

如果想在容器中修改主机的net.ipv4.ip_default_ttl参数, 则除了–privileged, 还需要加上 –net=host。

方法二 把/proc/sys bind到容器里面

1
# docker run -v /proc/sys:/writable-sys -it ubuntu bash

然后写bind到容器内的proc文件

1
echo 62 > /writable-sys/net/ipv4/ip_default_ttl

注意: 这样操作,效果类似于”–privileged”, 对于namespaced的参数,不会影响host和其他容器。

方法三 –sysctl

1
2
# docker run -it --sysctl 'net.ipv4.ip_default_ttl=63' ubuntu sysctl net.ipv4.ip_default_ttl
net.ipv4.ip_default_ttl = 63

注意:

  • 只有namespaced参数才可以。否则会报错”invalid argument…”
  • 这种方式只是在容器初始化过程中完成内核参数的修改,容器运行起来以后,/proc/sys仍然是以只读方式挂载的,在容器中不能再次修改sysctl内核参数。

kubernetes 与 sysctl

方法一 通过sysctls和unsafe-sysctls annotation

k8s还进一步把syctl参数分为safe和unsafe。 safe的条件:

  • must not have any influence on any other pod on the node
  • must not allow to harm the node’s health
  • must not allow to gain CPU or memory resources outside of the resource limits of a pod.

非namespaced的参数,肯定是unsafe。

namespaced参数,也只有一部分被认为是safe的。

在pkg/kubelet/sysctl/whitelist.go中维护了safe sysctl参数的名单。在1.7.8的代码中,只有三个参数被认为是safe的:

  • kernel.shm_rmid_forced,
  • net.ipv4.ip_local_port_range,
  • net.ipv4.tcp_syncookies

如果要设置一个POD中safe参数,通过security.alpha.kubernetes.io/sysctls这个annotation来传递给kubelet。

1
2
3
4
metadata:
name: sysctl-example
annotations:
security.alpha.kubernetes.io/sysctls: kernel.shm_rmid_forced=1

如果要设置一个namespaced, 但是unsafe的参数,要使用另一个annotation: security.alpha.kubernetes.io/unsafe-sysctls, 另外还要给kubelet一个特殊的启动参数。

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Pod
metadata:
name: sysctl-example
annotations:
security.alpha.kubernetes.io/sysctls: kernel.shm_rmid_forced=1
security.alpha.kubernetes.io/unsafe-sysctls: net.ipv4.route.min_pmtu=1000,kernel.msgmax=1 2 3
spec:
...

kubelet 增加–experimental-allowed-unsafe-sysctls启动参数

1
kubelet --experimental-allowed-unsafe-sysctls 'kernel.msg*,net.ipv4.route.min_pmtu'

方法二 privileged POD

如果要修改的是非namespaced的参数, 如vm.*, 那就没办法使用以上方法。 可以给POD privileged权限,然后在容器的初始化脚本或代码中去修改sysctl参数。

创建POD/deployment/daemonset等对象时, 给容器的spec指定securityContext.privileged=true

1
2
3
4
5
spec:
containers:
- image: nginx:alpine
securityContext:
privileged: true

这样跟”docker run –privileged”效果一样,在POD中/proc是以”rw”权限mount的,可以直接修改相关sysctl内核参数。

ulimit

每个进程都有若干操作系统资源的限制, 可以通过 /proc/$PID/limits 来查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cat /proc/1/limits 
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 62394 62394 processes
Max open files 1024 4096 files
Max locked memory 65536 65536 bytes
Max address space unlimited unlimited bytes
Max file locks unlimited unlimited locks
Max pending signals 62394 62394 signals
Max msgqueue size 819200 819200 bytes
Max nice priority 0 0
Max realtime priority 0 0
Max realtime timeout unlimited unlimited us

在bash中有个ulimit内部命令,可以查看当前bash进程的这些限制。

跟ulimit属性相关的配置文件是/etc/security/limits.conf。具体配置项和语法可以通过man limits.conf 命令查看。

systemd给docker daemon自身配置ulimit

在service文件中(一般是/usr/lib/systemd/system/dockerd.service)中可以配置:

1
2
3
4
5
6
7
8
9
[Service]
LimitAS=infinity
LimitRSS=infinity
LimitCORE=infinity
LimitNOFILE=65536
ExecStart=...
WorkingDirectory=...
User=...
Group=...

dockerd 给容器的 缺省ulimit设置

dockerd –default-ulimit nofile=65536:65536

冒号前面是soft limit, 后面是hard limit

给容器指定ulimit设置

docker run -d –ulimit nofile=20480:40960 nproc=1024:2048 容器名

在kubernetes中给pod设置ulimit参数

有一个issue在讨论这个问题: https://github.com/kubernetes/kubernetes/issues/3595

目前可行的办法,是在镜像中的初始化程序中调用setrlimit()系统调用来进行设置。子进程会继承父进程的ulimit参数。

参考文档:

http://tapd.oa.com/CCCM/prong/stories/view/1010166561060564549

https://kubernetes.io/docs/concepts/cluster-administration/sysctl-cluster/

https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities

NodePort, svc, LB直通Pod性能测试对比

作者:郭志宏

1. 测试背景:

目前基于k8s 服务的外网访问方式有以下几种:

  1. NodePort
  2. svc(通过k8s 的clusterip 访问)
  3. 自研 LB -> Pod (比如pod ip 作为 nginx 的 upstream, 或者社区的nginx-ingress)

其中第一种和第二种方案都要经过iptables 转发,第三种方案不经过iptables,本测试主要是为了测试这三种方案的性能损耗。

2. 测试方案

为了做到测试的准确性和全面性,我们提供以下测试工具和测试数据:

  1. 2核4G 的Pod

  2. 5个Node 的4核8G 集群

  3. 16核32G 的Nginx 作为统一的LB

  4. 一个测试应用,2个静态测试接口,分别对用不同大小的数据包(4k 和 100K)

  5. 测试1个pod ,10个pod的情况(service/pod 越多,一个机器上的iptables 规则数就越多,关于iptables规则数对转发性能的影响,在“ipvs和iptables模式下性能对⽐比测试报告” 已有结论: Iptables场景下,对应service在总数为2000内时,每个service 两个pod, 性能没有明显下降。当service总数达到3000、4000时,性能下降明显,service个数越多,性能越差。)所以这里就不考虑pod数太多的情况。

  6. 单独的16核32G 机器作作为压力机,使用wrk 作为压测工具, qps 作为评估标准,

  7. 那么每种访问方式对应以下4种情况

测试用例 Pod 数 数据包大小 平均QPS
1 1 4k
2 1 100K
3 10 4k
4 10 100k
  1. 每种情况测试5次,取平均值(qps),完善上表。

3. 测试过程

  1. 准备一个测试应用(基于nginx),提供两个静态文件接口,分别返回4k的数据和100K 的数据。

    镜像地址:ccr.ccs.tencentyun.com/caryguo/nginx:v0.1

    接口:http://0.0.0.0/4k.html

    http://0.0.0.0/100k.htm

  2. 部署压测工具。https://github.com/wg/wrk

  3. 部署集群,5台Node来调度测试Pod, 10.0.4.6 这台用来独部署Nginx, 作为统一的LB, 将这台机器加入集群的目的是为了 将ClusterIP 作为nginx 的upstream .

    1
    2
    3
    4
    5
    6
    7
    8
    root@VM-4-6-ubuntu:/etc/nginx# kubectl get node
    NAME STATUS ROLES AGE VERSION
    10.0.4.12 Ready <none> 3d v1.10.5-qcloud-rev1
    10.0.4.3 Ready <none> 3d v1.10.5-qcloud-rev1
    10.0.4.5 Ready <none> 3d v1.10.5-qcloud-rev1
    10.0.4.6 Ready,SchedulingDisabled <none> 12m v1.10.5-qcloud-rev1
    10.0.4.7 Ready <none> 3d v1.10.5-qcloud-rev1
    10.0.4.9 Ready <none> 3d v1.10.5-qcloud-rev1
  4. 根据不同的测试场景,调整Nginx 的upstream, 根据不同的Pod, 调整压力,让请求的超时率控制在万分之一以内, 数据如下:

    1
    2
    ./wrk -c 200 -d 20 -t 10 http://carytest.pod.com/10k.html   单pod
    ./wrk -c 1000 -d 20 -t 100 http://carytest.pod.com/4k.html 10 pod
  5. 测试wrk -> nginx -> Pod 场景,

测试用例 Pod 数 数据包大小 平均QPS
1 1 4k 12498
2 1 100K 2037
3 10 4k 82752
4 10 100k 7743
  1. wrk -> nginx -> ClusterIP -> Pod
测试用例 Pod 数 数据包大小 平均QPS
1 1 4k 12568
2 1 100K 2040
3 10 4k 81752
4 10 100k 7824
  1. NodePort 场景,wrk -> nginx -> NodePort -> Pod
测试用例 Pod 数 数据包大小 平均QPS
1 1 4k 12332
2 1 100K 2028
3 10 4k 76973
4 10 100k 5676

压测过程中,4k 数据包的情况下,应用的负载都在80% -100% 之间, 100k 情况下,应用的负载都在20%-30%

之间,压力都在网络消耗上,没有到达服务后端。

4. 测试结论

  1. 在一个pod 的情况下(4k 或者100 数据包),3中网络方案差别不大,QPS 差距在3% 以内。
  2. 在10个pod,4k 数据包情况下,lb->pod 和 svc 差距不大,NodePort 损失近7% 左右。
  3. 10个Pod, 100k 数据包的情况下,lb->pod 和 svc 差距不大,NodePort 损失近 25%

5. 附录

  1. nginx 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
user nginx;
worker_processes 50;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
worker_connections 100000;
}

http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

include /etc/nginx/mime.types;
default_type application/octet-stream;

# Load modular configuration files from the /etc/nginx/conf.d directory.
# See http://nginx.org/en/docs/ngx_core_module.html#include
# for more information.
include /etc/nginx/conf.d/*.conf;

# pod ip
upstream panda-pod {
#ip_hash;
# Pod ip
#server 10.0.4.12:30734 max_fails=2 fail_timeout=30s;
#server 172.16.1.5:80 max_fails=2 fail_timeout=30s;
#server 172.16.2.3:80 max_fails=2 fail_timeout=30s;
#server 172.16.3.5:80 max_fails=2 fail_timeout=30s;
#server 172.16.4.6:80 max_fails=2 fail_timeout=30s;
#server 172.16.4.5:80 max_fails=2 fail_timeout=30s;
#server 172.16.3.6:80 max_fails=2 fail_timeout=30s;
#server 172.16.1.4:80 max_fails=2 fail_timeout=30s;
#server 172.16.0.7:80 max_fails=2 fail_timeout=30s;
#server 172.16.0.6:80 max_fails=2 fail_timeout=30s;
#server 172.16.2.2:80 max_fails=2 fail_timeout=30s;

# svc ip
#server 172.16.255.121:80 max_fails=2 fail_timeout=30s;

# NodePort
server 10.0.4.12:30734 max_fails=2 fail_timeout=30s;
server 10.0.4.3:30734 max_fails=2 fail_timeout=30s;
server 10.0.4.5:30734 max_fails=2 fail_timeout=30s;
server 10.0.4.7:30734 max_fails=2 fail_timeout=30s;
server 10.0.4.9:30734 max_fails=2 fail_timeout=30s;

keepalive 256;
}

server {
listen 80;
server_name carytest.pod.com;
# root /usr/share/nginx/html;
charset utf-8;

# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
proxy_pass http://panda-pod;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;

}

error_page 404 /404.html;
location = /40x.html {
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}