-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathsearch.xml
234 lines (234 loc) · 174 KB
/
search.xml
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[【理解 Cilium 系列文章】(二) 理解网络数据包的流转过程]]></title>
<url>%2F2021%2F08%2F27%2Fcilium02%2F</url>
<content type="text"><![CDATA[Cilium 作为近两年最火的云原生网络方案,可谓是风头无两。作为第一个通过 ebpf 实现了 kube-proxy 所有功能的网络插件,它的神秘面纱究竟是怎样的呢?本系列文章将带大家一起来慢慢揭晓 作为《理解 Cilium系列文章》的第二篇,本文主要介绍 Cilium 网络相关知识点,为后续 Cilium 的深入了解做铺垫。了解 Cilium 是如何在网络流转的路径中做拦截处理的 之前的两篇文章【25 张图,一万字,拆解 Linux 网络包发送过程】【图解Linux网络包接收过程】主要从源码层次介绍了 Linux 网络收发包的流程,感兴趣的同学可以看一下,读完上述两篇再看本篇将会更轻松 。 1. 网络分层的宏观视角想必大家都应该准备过这样一道面试题 从输入 URL 到收到请求响应,中间发生了什么事情 ,笔者当年校招时就经常被问到这个题目。 这个过程讲复杂了,恐怕讲个一天一夜也讲不完。此处咱们长话短说,简要描述下大体流程,先建立个宏观视角。 先复习下网络分层模型 如下 左图 为 OSI 的标准七层 网络模型,这套模型只是停留在概念上的,实现起来太复杂了。右边是业界标准的 TCP/IP 模型,Linux 系统中正是按照 TCP/IP 模型开发的网络协议栈。 接下来回到上文的问题 从输入 URL 到收到请求响应,中间发生了什么事情 此处简要描述下流程,限于篇幅不一一展开了,当然如果小伙伴对其中某些知识点感兴趣的话,可以自行搜索相关材料继续深入研究。 客户端发起网络请求,用户态的应用程序(浏览器)会生成 HTTP 请求报文、并通过 DNS 协议查找到对应的远端 IP 地址。 用户态的应用程序(浏览器) 会委托操作系统内核协议栈中的上半部分,也就是 TCP/UDP 协议发起连接请求。此处封装 TCP 头(或 UDP 头) 然后经由协议栈下半部分的 IP 协议进行封装,然后交给下层协议。此处封装 IP 头 经过 MAC 层处理,找到接收方的目标 MAC 地址。此处封装 MAC 头。 最终数据包在经过网卡转化成电信号经过交换机、路由器发送到服务端,服务端经过处理拿到数据,再通过各种网络协议依次把封装的头解封装,把数据响应给客户端。 客户端拿到数据进行渲染。 2. Linux 网络协议栈上面讲述了网络分层原理以及各层的封包解包流程,下面介绍下 Linux 网络协议栈,其实 Linux 网络协议栈就类似于 TCP/IP 的四层结构: 通过上图可以看到: 应用程序需要通过系统调用,来跟 Socket 层进行数据交互; Socket 层的下面就是传输层、网络层和网络接口层; 最下面的一层,则是网卡驱动程序和硬件网卡设备; 3. Linux 接收网络包的流程同样的,先来个宏观视角,然后再一一介绍,避免一开始就陷入细节无法自拔 图片取自《你不好奇 Linux 网络发包过程吗?》 可以看到上图比之前介绍的网络封包解包相比,多了下面网卡相关的内容。是的,因为咱们要介绍的是 Cilium 相关的网络基础,所以需要了解 数据包是如何穿过 network datapath 的:包括从硬件到 内核,再到用户空间。图中有 Cilium logo 的地方,都是 datapath 上 Cilium 重度使用 BPF 程序的地方。 参考 【[译] 深入理解 Cilium 的 eBPF 收发包路径(datapath)(KubeCon, 2019)】 下面将分层 介绍: 3.1 L1 -> L2(物理层 -> 数据链路层) 网卡收包简要流程: 网卡驱动初始化。 网卡获得一块物理内存,作用收发包的缓冲区(ring-buffer)。这种方式称为 DMA(直接内存访问)。 驱动向内核 NAPI(New API)注册一个轮询(poll )方法。 网卡从网络中收到一个包,通过 DMA 方式将包放到 Ring Buffer,这是一个环形缓冲区。 如果此时 NAPI 没有在执行,网卡就会触发一个硬件中断(HW IRQ),告诉处理器 DMA 区域中有包等待处理。 收到硬中断信号后,处理器开始执行 NAPI。 NAPI 执行网卡注册的 poll 方法开始收包。 关于 NAPI poll 机制: Linux 内核在 2.6 版本中引入了 NAPI 机制,它是混合「中断和轮询」的方式来接收网络包,它的核心概念就是不采用中断的方式读取数据,而是首先采用中断唤醒数据接收的服务程序,然后 poll 的方法来轮询数据。 驱动注册的这个 poll 是一个主动式 poll(active poll),执行 poll 方法的是运行在某个或者所有 CPU 上的内核线程(kernel thread),一旦执行就会持续处理 ,直到没有数据可供处理,然后进入 idle 状态。 比如,当有网络包到达时,网卡发起硬件中断,于是会执行网卡硬件中断处理函数,中断处理函数处理完需要「暂时屏蔽中断」,然后唤醒「软中断」来轮询处理数据,不断从驱动的 DMA 区域内接收数据包直到没有新数据时才恢复中断,这样一次中断处理多个网络包,于是就可以降低网卡中断带来的性能开销。 之所以会有这种机制,是因为硬件中断代价太 高了,因为它们比系统上几乎所有东西的优先级都要高。 NAPI 驱动的 poll 机制将数据从 DMA 区域读取出来,对数据做一些准备工作,然后交给比 它更上一层的内核协议栈。 3.2 L2 数据链路层此处不会过多展示驱动层做的事情,主要关注 Cilium 涉及到的流程,即 内核及以上的流程,主要包括 分配 socket buffers(skb) BPF iptables 将包送到网络栈(network stack)和用户空间 Step 1:NAPI poll 首先,NAPI poll 机制不断调用驱动实现的 poll 方法,后者处理 RX 队列内的包,并最终 将包送到正确的程序。也就是前面的公众号介绍的 XDP【Linux网络新技术基石——eBPF、XDP】。 Step 2:XDP 程序处理 XDP全称为eXpress Data Path,是Linux内核网络栈的最底层。它只存在于RX(接收数据)路径上,允许在网络设备驱动内部网络堆栈中数据来源最早的地方进行数据包处理,在特定模式下可以在操作系统分配内存(skb)之前就已经完成处理 插播一下 XDP 的工作模式 XDP 有三种工作模式,默认是 native(原生)模式,当讨论 XDP 时通常隐含的都是指这 种模式。 Native XDP XDP 程序 hook 到网络设备的驱动上,它是XDP最原始的模式,因为还是先于操作系统进行数据处理,它的执行性能还是很高的,当然需要网卡驱动支持。大部分广泛使用的 10G 及更高速的网卡都已经支持这种模式 。 Offloaded XDP XDP程序直接hook到可编程网卡硬件设备上,与其他两种模式相比,它的处理性能最强;由于处于数据链路的最前端,过滤效率也是最高的。如果需要使用这种模式,需要在加载程序时明确声明。 Generic XDP 对于还没有实现 native 或 offloaded XDP 的驱动,内核提供了一个 generic XDP 选 项,这是操作系统内核提供的通用 XDP兼容模式,它可以在没有硬件或驱动程序支持的主机上执行XDP程序。在这种模式下,XDP的执行是由操作系统本身来完成的,以模拟native模式执行。好处是,只要内核够高,人人都能玩XDP;缺点是由于是仿真执行,需要分配额外的套接字缓冲区(SKB),导致处理性能下降,跟native模式在10倍左右的差距。 对于在生产环境使用 XDP,推荐要么选择 native 要么选择 offloaded 模式。这两种模式需要网卡驱动的支持,对于那些不支持 XDP 的驱动,内核提供了 Generic XDP ,这是软件实现的 XDP,性能会低一些, 在实现上就是将 XDP 的执行上移到了核心网络栈 继续回来介绍 ,分两种情况:native/offloaded 模式、general 模式 (1) native/offloaded 模式:XDP 在内核收包函数 receive_skb() 之前 (2) Generic XDP 模式:XDP 在内核收包函数 receive_skb() 之后, XDP 程序返回一个判决结果给驱动,可以是 PASS, TRANSMIT, 或 DROP TRANSMIT 非常有用,有了这个功能,就可以用 XDP 实现一个 TCP/IP 负载均衡器。 XDP 只适合对包进行较小修改,如果是大动作修改,那这样的 XDP 程序的性能 可能并不会很高,因为这些操作会降低 poll 函数处理 DMA ring-buffer 的能力。 如果返回的是 DROP,这个包就可以直接原地丢弃了,而 无需再穿越后面复杂的协议栈然后再在某个地方被丢弃,从而节省了大量资源。在业界最出名的一个应用场景就是 Facebook 基于 XDP 实现高效的防 DDoS 攻击,其本质上就是实现尽可能早地实现「丢包」,而不去消耗系统资源创建完整的网络栈链路,即「early drop」。 如果返回是 PASS,内核会继续沿着默认路径处理包,如果是 native/offloaded 模式 ,后续到达 clean_rx() 方法;如果是 Generic XDP 模式,将导到 check_taps()下面的 Step 6 继续讲解。 Step 3:clean_rx():创建 skb 如果 XDP 返回是 PASS,内核会继续沿着默认路径处理包,到达 clean_rx() 方法。 这个方法创建一个 socket buffer(skb)对象,可能还会更新一些统计信息,对 skb 进行硬件校验和检查,然后将其交给 gro_receive() 方法。 Step 4:gro_receive() GRO (Generic Receive Offload) 是一种硬件特性的软件实现,可以简单理解成:将包交给网络协议栈之前,把相关的小包合并成一个大包。目的是减少传送给网络栈的包数,这有助于减少 CPU 的使用量,提高吞吐量。 如果 GRO 的 buffer 相比于包太小了,它可能会选择什么都不做。 如果当前包属于某个更大包的一个分片,调用 enqueue_backlog 将这个分片放到某个 CPU 的包队列。当包重组完成后,会交给 receive_skb() 方法处理。 如果当前包不是分片包,直接调用 receive_skb(),进行一些网络栈最底层的处理。 Step 5:receive_skb() receive_skb() 之后会再次进入 XDP 程序点。 3.3 L2 -> L3(数据链路层 -> 网络层)Step 6:通用 XDP 处理(gXDP) 上文说过,如果不支持 Native 或 Offloaded 模式的 XDP 的话,将通过 General XDP 来处理,就是此处的 (g)XDP。 此处 XDP 的行为 跟 Step 2 中一致,此处不再赘述。 Step 7:Tap 设备处理 图中有个 *check_taps 框,但其实并没有这个方法:receive_skb() 会轮询所有的 socket tap,将包放到正确的 tap 设备的缓冲区。 tap 设备监听的是二层协议(L2 protocols)。如果 tap 设 备存在,它就可以操作这个 skb 了。 Tun/Tap 设备: Tun 设备是一个三层设备,从/dev/net/tun字符设备上读取的是IP数据包,写入的也只能是IP数据包,因此不能进行二层操作,如发送ARP请求和以太网广播。 Tap设备是三层设备,处理的是二层 MAC 层数据帧,从/dev/net/tun字符设备上读取的是MAC 层数据帧,写入的也只能是 MAC 层数据帧。从这点来看,Tap虚拟设备和真实的物理网卡的能力更接近。 Step 8:tc(traffic classifier)处理 接下来是 TC (Traffic Control), 也就是流量控制,TC更专注于packet scheduler,所谓的网络包调度器,调度网络包的延迟、丢失、传输顺序和速度控制。和 XDP 一样,TC 的输出代表了数据包如何被处置的一种动作,最新的 Linux 内核中定义的有 9 种动作: #define TC_ACT_OK 0#define TC_ACT_RECLASSIFY 1#define TC_ACT_SHOT 2#define TC_ACT_PIPE 3#define TC_ACT_STOLEN 4#define TC_ACT_QUEUED 5#define TC_ACT_REPEAT 6#define TC_ACT_REDIRECT 7#define TC_ACT_TRAP 8 注: Cilium 控制的网络设备,至少被加载了一个 tc eBPF 程序 Step 9:Netfilter 处理 如果 tc BPF 返回 OK,包会再次进入 Netfilter。 Netfilter 也会对入向的包进行处理,这里包括 nftables 和 iptables 模块。 *def_dev_protocol 框是二层过滤器(L2 net filter),由于 Cilium 没有用到任何 L2 filter,此处就不展开了。 Step 10:L3 协议层处理:ip_rcv() 最后,如果包没有被前面丢弃,就会通过网络设备的 ip_rcv() 方法进入协议栈的三层( L3)—— 即 IP 层 —— 进行处理。 接下来看ip_rcv()(但这里需要提醒大家的是,Linux 内核也支持除了 IP 之 外的其他三层协议,它们的 datapath 会与此有些不同)。 3.3 L3 -> L4(网络层 -> 传输层)Step 11:Netfilter L4 处理 ip_rcv() 做的第一件事情是再次执行 Netfilter 过滤,因为我们现在是从四层(L4)的 视角来处理 socket buffer。因此,这里会执行 Netfilter 中的任何四层规则(L4 rules ) Step 12:ip_rcv_finish() 处理Netfilter 执行完成后,调用回调函数 ip_rcv_finish()。 ip_rcv_finish() 立即调用 ip_routing() 对包进行路由判断。 Step 13:ip_routing() 处理 ip_routing() 对包进行路由判断,例如看它是否是在 lookback 设备上,是否能 路由出去(egress),或者能否被路由,能否被 unmangle 到其他设备等等。 在 Cilium 中,如果没有使用隧道模式(tunneling),那就会用到这里的路由功能。相比 隧道模式,路由模式会的 datapath 路径更短,因此性能更高。 Step 14:目的是本机:ip_local_deliver() 处理 根据路由判断的结果,如果包的目的端是本机,会调用 ip_local_deliver() 方法。 ip_local_deliver() 会调用 xfrm4_policy()。 Step 15:xfrm4_policy() 处理 xfrm4_policy() 完成对包的封装、解封装、加解密等工作。例如,IPSec 就是在这里完成的。 最后,根据四层协议的不同,ip_local_deliver() 会将最终的包送到 TCP 或 UDP 协议 栈。这里必须是这两种协议之一,否则设备会给源 IP 地址回一个 ICMP destination unreachable 消息。 此处拿 UDP 协议作为例子,因为 TCP 状态机太复杂了,不适合这里用于理解 datapath 和数据流。但不是说 TCP 不重要,Linux TCP 状态机还是非常值得好好学习的。 3.4 L4(传输层,以 UDP 为例)Step 16:udp_rcv() 处理 udp_rcv() 对包的合法性进行验证,检查 UDP 校验和。然后,再次将包送到 xfrm4_policy() 进行处理。 Step 17:xfrm4_policy() 再次处理 这里再次对包执行 transform policies 是因为,某些规则能指定具体的四层协议,所以只 有到了协议层之后才能执行这些策略。 Step 18:将包放入 socket_receive_queue 这一步会拿端口(port)查找相应的 socket,然后将 skb 放到一个名为 socket_receive_queue 的链表。 Step 19:通知 socket 收数据:sk_data_ready() 最后,udp_rcv() 调用 sk_data_ready() 方法,标记这个 socket 有数据待收。 本质上,一个 socket 就是 Linux 中的一个文件描述符,这个描述符有一组相关的文件操 作抽象,例如 read、write 等等。 网络栈下半部分小结 以上 Step 1~19 就是 Linux 网络栈下半部分的全部内容。 接下来介绍几个内核函数,都是与进程上下文相关的。 3.5 L4 User Space下图左边是一段 socket listening 程序,这里省略了错误检查,而且 epoll 本质上也 是不需要的,因为 UDP 的 recv 方法已经在执行 poll 操作了。 事实上当我们调 用 recvmsg() 方法时,内核所做的事情就和上面这段代码差不多。对照右边的图: 首先初始化一个 epoll 实例和一个 UDP socket,然后告诉 epoll 实例我们想 监听这个 socket 上的 receive 事件,然后等着事件到来。 当 socket buffer 收到数据时,其 wait queue 会被上一节的 sk_data_ready() 方法置位(标记)。 epoll 监听在 wait queue,因此 epoll 收到事件通知后,提取事件内容,返回给用户空间。 用户空间程序调用 recv 方法,它接着调用 udp_recv_msg 方法,后者又会 调用 cgroup eBPF 程序 —— 这是本文出现的第三种 BPF 程序。Cilium 利用 cgroup eBPF 实现 socket level 负载均衡: 一般的客户端负载均衡对客户端并不是透明的,即,客户端应用必须将负载均衡逻辑内置到应用里。 有了 cgroup BPF,客户端根本感知不到负载均衡的存在。 本文介绍的最后一种 BPF 程序是 sock_ops BPF,用于 socket level 整流(traffic shaping ),这对某些功能至关重要,例如客户端级别的限速(rate limiting)。 最后,我们有一个用户空间缓冲区,存放收到的数据。 以上就是 Cilium 涉及到网络数据包在内核流转的过程, 实际上内核 datapath 要远比这里讲的要复杂。 前面只是非常简单地介绍了协议栈每个位置(Netfilter、iptables、eBPF、XDP)能执行的动作。 这些位置提供的处理能力是不同的。例如 XDP 可能是能力最受限的,因为它只是设计用来做快速丢包(fast dropping)和 非本地重定向(non-local redirecting);但另一方面,它又是最快的程序,因为 它在整个 datapath 的最前面,具备对整个 datapath 进行短路处理(short circuit the entire datapath)的能力。 tc 和 iptables 程序能方便地 mangle 数据包,而不会对原来的转发流程产生显著影响。 理解这些东西非常重要,因为这是 Cilium 乃至广义 datapath 里非常核心的东西。如 果遇到底层网络问题,或者需要做 Cilium/kernel 调优,必须要理解包的收发/转发 路径。 参考文章[1].你不好奇 Linux 网络发包过程吗: https://xie.infoq.cn/article/6ba14b756c3019cc737ed48a6 [2].【[译] 深入理解 Cilium 的 eBPF 收发包路径(datapath)(KubeCon, 2019)】: http://arthurchiao.art/blog/understanding-ebpf-datapath-in-cilium-zh [3].GRO: https://zhuanlan.zhihu.com/p/44683790 [4].Linux 内核: https://elixir.bootlin.com/linux/v5.14-rc7/source/include/uapi/linux/pkt_cls.h#L60]]></content>
<categories>
<category>cilium</category>
</categories>
<tags>
<tag>cilium</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【理解 Cilium 系列文章】(一) 初识 Cilium]]></title>
<url>%2F2021%2F08%2F17%2Fcilium01%2F</url>
<content type="text"><![CDATA[Cilium 作为近两年最火的云原生网络方案,可谓是风头无两。作为第一个通过 ebpf 实现了 kube-proxy 所有功能的网络插件,它的神秘面纱究竟是怎样的呢?本系列文章将带大家一起来慢慢揭晓 作为《理解 Cilium 系列文章》的第一篇,本文主要介绍 Cilium 的发展,相关功能以及使用,深入理解及底层原理将在后续文章中继续介绍 背景随着云原生的普及率越来越高,各大厂商基本上或多或少都实现了业务的 k8s 容器化,头部云计算厂商更是不用说。 而且随着 k8s 的 普及,当前集群逐渐呈现出以下两个特点: 容器数量越来越多,比如:k8s 官方单集群就已经支持 15000 pod Pod 生命周期越来越短,Serverless 场景下甚至短至几分钟,几秒钟 随着容器密度的增大,以及生命周期的变短,对原生容器网络带来的挑战也越来越大 当前 k8s Service 负载均衡的实现现状在 Cilium 出现之前, Service 由 kube-proxy 来实现,实现方式有 userspace,iptables,ipvs 三种模式。 Userspace当前模式下,kube-proxy 作为反向代理,监听随机端口,通过 iptables 规则将流量重定向到代理端口,再由 kube-proxy 将流量转发到 后端 pod。Service 的请求会先从用户空间进入内核 iptables,然后再回到用户空间,代价较大,性能较差。 Iptables存在的问题: 1.可扩展性差。随着 service 数据达到数千个,其控制面和数据面的性能都会急剧下降。原因在于 iptables 控制面的接口设计中,每添加一条规则,需要遍历和修改所有的规则,其控制面性能是O(n²)。在数据面,规则是用链表组织的,其性能是O(n) 2.LB 调度算法仅支持随机转发 Ipvs 模式IPVS 是专门为 LB 设计的。它用 hash table 管理 service,对service 的增删查找都是O(1)的时间复杂度。不过 IPVS 内核模块没有 SNAT 功能,因此借用了 iptables 的 SNAT 功能。 IPVS 针对报文做 DNAT 后,将连接信息保存在 nf_conntrack 中,iptables 据此接力做 SNAT。该模式是目前 Kubernetes 网络性能最好的选择。但是由于 nf_conntrack 的复杂性,带来了很大的性能损耗。腾讯针对该问题做过相应的优化【绕过conntrack,使用eBPF增强 IPVS优化K8s网络性能】 Cilium 的发展Cilium 是基于 eBpf 的一种开源网络实现,通过在 Linux 内核动态插入强大的安全性、可见性和网络控制逻辑,提供网络互通,服务负载均衡,安全和可观测性等解决方案。简单来说可以理解为 Kube-proxy + CNI 网络实现。 Cilium 位于容器编排系统和 Linux Kernel 之间,向上可以通过编排平台为容器进行网络以及相应的安全配置,向下可以通过在 Linux 内核挂载 eBPF 程序,来控制容器网络的转发行为以及安全策略执行 简单了解下 Cilium 的发展历程: 2016 Thomas Graf 创立了 Cilium, 现为 Isovalent (Cilium 背后的商业公司)的 CTO 2017 年 DockerCon 上 Cilium 第一次发布 2018 年 发布 Cilium 1.0 2019 年 发布 Cilium 1.6 版本,100% 替代 kube-proxy 2019 年 Google 全面参与 Cilium 2021 年 微软、谷歌、FaceBook、Netflix、Isovalent 在内的多家企业宣布成立 eBPF 基金会(Linux 基金会下) 功能介绍 查看官网,可以看到 Cilium 的功能主要包含 三个方面,如上图 一、网络 高度可扩展的 kubernetes CNI 插件,支持大规模,高动态的 k8s 集群环境。支持多种租网模式: Overlay 模式,支持 Vxlan 及 Geneve Unerlay 模式,通过 Direct Routing (直接路由)的方式,通过 Linux 宿主机的路由表进行转发 kube-proxy 替代品,实现了 四层负载均衡功能。LB 基于 eBPF 实现,使用高效的、可无限扩容的哈希表来存储信息。对于南北向负载均衡,Cilium 作了最大化性能的优化。支持 XDP、DSR(Direct Server Return,LB 仅仅修改转发封包的目标 MAC 地址) 多集群的连通性,Cilium Cluster Mesh 支持多集群间的负载,可观测性以及安全管控 二、可观测性 提供生产可用的可观测性工具 hubble, 通过 pod 及 dns 标识来识别连接信息 提供 L3/L4/L7 级别的监控指标,以及 Networkpolicy 的 行为信息指标 API 层面的可观测性 (http,https) Hubble 除了自身的监控工具,还可以对接像 Prometheus、Grafana 等主流的云原生监控体系,实现可扩展的监控策略 三、安全 不仅支持 k8s Network Policy,还支持 DNS 级别、API 级别、以及跨集群级别的 Network Policy 支持 ip 端口 的 安全审计日志 传输加密 总结,Cilium 不仅包括了 kube-proxy + CNI 网络实现,还包含了众多可观测性和安全方面的特性。 安装部署 linux 内核要求 4.19 及以上 可以采用 helm 或者 cilium cli,此处笔者使用的是 cilium cli(版本为 1.10.3) 下载 cilium cli wget https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gztar xzvfC cilium-linux-amd64.tar.gz /usr/local/bin 安装 cilium cilium install --kube-proxy-replacement=strict # 此处选择的是完全替换,默认情况下是 probe,(该选项下 pod hostport 特性不支持) 可视化组件 hubble(选装) cilium hubble enable --ui 等待 pod ready 后,查看 状态如下: ~# cilium status /¯¯\ /¯¯\__/¯¯\ Cilium: OK \__/¯¯\__/ Operator: OK /¯¯\__/¯¯\ Hubble: OK \__/¯¯\__/ ClusterMesh: disabled \__/DaemonSet cilium Desired: 1, Ready: 1/1, Available: 1/1Deployment cilium-operator Desired: 1, Ready: 1/1, Available: 1/1Deployment hubble-relay Desired: 1, Ready: 1/1, Available: 1/1Containers: hubble-relay Running: 1 cilium Running: 1 cilium-operator Running: 1Image versions cilium quay.io/cilium/cilium:v1.10.3: 1 cilium-operator quay.io/cilium/operator-generic:v1.10.3: 1 hubble-relay quay.io/cilium/hubble-relay:v1.10.3: 1 cilium cli 还支持 集群可用性检查(可选) [root@~]# cilium connectivity testℹ️ Single-node environment detected, enabling single-node connectivity testℹ️ Monitor aggregation detected, will skip some flow validation steps✨ [kubernetes] Creating namespace for connectivity check...✨ [kubernetes] Deploying echo-same-node service...✨ [kubernetes] Deploying same-node deployment...✨ [kubernetes] Deploying client deployment...✨ [kubernetes] Deploying client2 deployment...⌛ [kubernetes] Waiting for deployments [client client2 echo-same-node] to become ready...⌛ [kubernetes] Waiting for deployments [] to become ready...⌛ [kubernetes] Waiting for CiliumEndpoint for pod cilium-test/client-6488dcf5d4-rx8kh to appear...⌛ [kubernetes] Waiting for CiliumEndpoint for pod cilium-test/client2-65f446d77c-97vjs to appear...⌛ [kubernetes] Waiting for CiliumEndpoint for pod cilium-test/echo-same-node-745bd5c77-gr2p6 to appear...⌛ [kubernetes] Waiting for Service cilium-test/echo-same-node to become ready...⌛ [kubernetes] Waiting for NodePort 10.251.247.131:31032 (cilium-test/echo-same-node) to become ready...⌛ [kubernetes] Waiting for Cilium pod kube-system/cilium-vsk8j to have all the pod IPs in eBPF ipcache...⌛ [kubernetes] Waiting for pod cilium-test/client-6488dcf5d4-rx8kh to reach default/kubernetes service...⌛ [kubernetes] Waiting for pod cilium-test/client2-65f446d77c-97vjs to reach default/kubernetes service...ð Enabling Hubble telescope...⚠️ Unable to contact Hubble Relay, disabling Hubble telescope and flow validation: rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing dial tcp [::1]:4245: connect: connection refused"ℹ️ Expose Relay locally with: cilium hubble enable cilium status --wait cilium hubble port-forward&ð Running tests... 等 hubble 安装完成后,hubble-ui service 修改为 NodePort类型, 即可通过 NodeIP+NodePort 来登录 Hubble 界面 查看相关信息 Cilium 部署完后,有以下几个组件 operator、hubble(ui, relay),Cilium agent(Daemonset 形式,每个节点一个),其中关键组件为 cilium agent。 Cilium Agent作为整个架构中最核心的组件,通过DaemonSet的方式,以特权容器的模式,运行在集群的每个主机上。Cilium Agent作为用户空间守护程序,通过插件与容器运行时和容器编排系统进行交互,进而为本机上的容器进行网络以及安全的相关配置。同时提供了开放的API,供其他组件进行调用。 Cilium Agent在进行网络和安全的相关配置时,采用eBPF程序进行实现。Cilium Agent结合容器标识和相关的策略,生成 eBPF 程序,并将 eBPF 程序编译为字节码,将它们传递到 Linux 内核。 相关命令介绍Cilium agent 中内置了一些调试用的命令,下面介绍,agent 中的 cilium 不同与上述介绍的 cilium cli ( 虽然同为 cilium) cilium status 主要展示 cilium 的一些简单配置信息及状态,如下 [root@~]# kubectl exec -n kube-system cilium-s62h5 -- cilium statusDefaulted container "cilium-agent" out of: cilium-agent, ebpf-mount (init), clean-cilium-state (init)KVStore: Ok DisabledKubernetes: Ok 1.21 (v1.21.2) [linux/amd64]Kubernetes APIs: ["cilium/v2::CiliumClusterwideNetworkPolicy", "cilium/v2::CiliumEndpoint", "cilium/v2::CiliumNetworkPolicy", "cilium/v2::CiliumNode", "core/v1::Namespace", "core/v1::Node", "core/v1::Pods", "core/v1::Service", "discovery/v1::EndpointSlice", "networking.k8s.io/v1::NetworkPolicy"]KubeProxyReplacement: Strict [eth0 10.251.247.131 (Direct Routing)]Cilium: Ok 1.10.3 (v1.10.3-4145278)NodeMonitor: Listening for events on 8 CPUs with 64x4096 of shared memoryCilium health daemon: OkIPAM: IPv4: 68/254 allocated from 10.0.0.0/24,BandwidthManager: DisabledHost Routing: LegacyMasquerading: BPF [eth0] 10.0.0.0/24 [IPv4: Enabled, IPv6: Disabled]Controller Status: 346/346 healthyProxy Status: OK, ip 10.0.0.167, 0 redirects active on ports 10000-20000Hubble: Ok Current/Max Flows: 4095/4095 (100.00%), Flows/s: 257.25 Metrics: DisabledEncryption: DisabledCluster health: 1/1 reachable (2021-08-11T09:33:31Z) cilium service list 展示 service 的实现,使用时可通过 ClusterIP 来过滤,其中,FrontEnd 为 ClusterIP,Backend 为 PodIP [root@~]# kubectl exec -it -n kube-system cilium-vsk8j -- cilium service listDefaulted container "cilium-agent" out of: cilium-agent, ebpf-mount (init), clean-cilium-state (init)ID Frontend Service Type Backend1 10.111.192.31:80 ClusterIP 1 => 10.0.0.212:88882 10.101.111.124:8080 ClusterIP 1 => 10.0.0.81:80803 10.101.229.121:443 ClusterIP 1 => 10.0.0.24:84434 10.111.165.162:8080 ClusterIP 1 => 10.0.0.213:80805 10.96.43.229:4222 ClusterIP 1 => 10.0.0.210:42226 10.100.45.225:9180 ClusterIP 1 => 10.0.0.48:9180# 避免过多,此处不一一展示 cilium service get 通过 cilium service get < ID> -o json 来展示详情 [root@~]# kubectl exec -it -n kube-system cilium-vsk8j -- cilium service get 132 -o jsonDefaulted container "cilium-agent" out of: cilium-agent, ebpf-mount (init), clean-cilium-state (init){ "spec": { "backend-addresses": [ { "ip": "10.0.0.213", "nodeName": "n251-247-131", "port": 8080 } ], "flags": { "name": "autoscaler", "namespace": "knative-serving", "trafficPolicy": "Cluster", "type": "ClusterIP" }, "frontend-address": { "ip": "10.98.24.168", "port": 8080, "scope": "external" }, "id": 132 }, "status": { "realized": { "backend-addresses": [ { "ip": "10.0.0.213", "nodeName": "n251-247-131", "port": 8080 } ], "flags": { "name": "autoscaler", "namespace": "knative-serving", "trafficPolicy": "Cluster", "type": "ClusterIP" }, "frontend-address": { "ip": "10.98.24.168", "port": 8080, "scope": "external" }, "id": 132 } }} 还有很多有用的命令,限于篇幅,此处不一一展示,留给读者自己去探索(cilium status --help) 《理解 Cilium 系列文章》的第一篇到此就讲完了,后续文章将按照 Cilium 涉及的基础知识及相关原理进行介绍,敬请期待]]></content>
<categories>
<category>cilium</category>
</categories>
<tags>
<tag>cilium</tag>
</tags>
</entry>
<entry>
<title><![CDATA[典型的 Serverless 架构是怎样的]]></title>
<url>%2F2021%2F06%2F27%2Fserverless_arch%2F</url>
<content type="text"><![CDATA[先看下 CNCF 2020 (2021年的还没出) Serverless 调查报告显示的数据,云计算厂商中的 Serverless 产品使用率中 AWS Lambda 占 57% ;开源 Serverless 平台平台使用率中 Knative 占 27% 。 本文主要介绍 Lambda 上的 Serverless 典型架构 那么AWS Lambdas 吸引人的地方在于:自动扩缩容及按使用量计费。当然还有一个主要原因,AWS 提供的其他服务也是相当丰富(API gateway,鉴权,数据库,工作流等等)。给中小型企业充分发挥的余地,充分利用 AWS 提供的这些能力组建自己的 Serverless 形态产品架构。 本文以 Theodo 公司的一个 Web 项目为例,介绍典型的 Serverless 架构是怎样的,首先看下项目的全景图,看不懂没关系,接下来分解开来讲解。 在上述图表中, 方框代表的是存在于大多数 Serverless 架构中的典型技术领域或技术功能。 一、Serverless 实践思路此项目的目标是拥有一个强大的完全托管的系统,并提供友好的开发人员体验。 为了实现这一目标,Theodo 公司采取了以下措施: 1. 选择 AWS云技术的竞争很激烈:亚马逊云服务(AWS)、谷歌云服务(GCP)、微软云服务(Azure)、IBM 云服务、阿里云服务。这些平台都提供了自己的云计算产品,并且特性都在快速迭代中。 Ben Ellerby 在这篇文章[1]中比较了前三名的云服务提供商,而我们更青睐于 AWS 的解决方案。在 Serverless 架构方面,AWS 是最先进的。借助 AWS 的解决方案,我们可以尽可能接近 Serverless 架构。为了说明这一点,我们将在下文中详细介绍构成我们架构模块的每个 AWS 服务。 2. 使用 TypeScript 开发 Node.js 项目JavaScript 是世界上最受欢迎的编程语言之一,社区也很活跃,根据 Datadog 的调查(可参考之前发的一篇公众号文章),Serverless 架构中也是如此。尽管 Python 以 47%的占有率领先,但目前已部署的 Lambdas 中有 39% 是运行 JavaScript 的。 TypeScript 丰富了 JavaScript 的特性。最后说明下, 在绝大多数用例中, Lambdas 中的 JavaScript 都运行得很好。 3. Serverless FrameworkServerless Framework 完成了大部分基础架构即代码(Infrastructure as Code, IaC)的工作(基于 CloudFormation,CloudFormation 是 AWS 提供的一个基础设置编排工具)。定义一个对 HTTP 事件做出响应的 Lambda 函数, Serverless 架构框架将自动部署相关的 API Gateway资源、相应的路由以及新的 Lambda 函数。当需要更复杂的服务配置时,只需简单地添加一些 CloudFormation 即可 。 4. 细粒度的 Lambda 函数Lambda 是一个函数,它有自己的工作任务,并且能做得很好。比如: 项目的前端需要获得一个项目列表,那这个功能可以新建一个 Lambda 函数。 当用户注册后,我们需要发送确认电子邮件, 为这个功能也可以新建一个 Lambda 函数。 当然,某些特定的代码(例如数据实体 Entity)可以分解成小的单元,并在专用的 utilities 文件夹中共享。但一定要非常小心这些代码,因为任何更改都会影响所有相关的 Lambda 函数。而且因为每个 Lambda 是可以独立测试和部署的,因此可能会遗漏一些内容(这时 TypeScript 就派上用场了)。 5. 分解成微服务为了避免团队之间相互影响,同时避免 package.json 和 serverless.yaml 配置文件过大(CloudFormation 的资源限制数量为 200)以及过长的 CloudFormation 部署时间,同时也为了方便我们在代码库中定位,并在所有 Lambdas 函数之间明确清晰的团队职责:我们定义了微服务划分的边界。Ben Ellerby 在这篇文章中写了一个方法, EventBridge Storming[2],来帮助定义这些界限。 在我们的单体代码库中:一个微服务=一个 CloudFormation 堆栈=一个 serverless.yml + package.json。此外,微服务有只属于自己的数据实体,这些数据实体不会与其他微服务共享。 早期在项目中我们推荐只使用 JavaScript,但出于种种原因,可能想要使用另一种语言,或者可能希望逐步迁移到 JavaScript 中的 Serverless 架构。在 Serverless 架构中,微服务的优势是你可以在架构中混合多种技术栈,只要保证微服务之间的抽象接口一致即可。 6. 使用事件驱动 同时微服务之间需要完全独立,如果其中一个微服务出现事故,或者正在对另一个微服务进行重大改动,这对于系统其他部分的影响应该越小越好。为实现这个目标,Lambdas 函数仅通过 EventBridge 这个 Serverless架构的事件总线来和其他 Lambdas 函数交互。在这篇文章[3]中, Ben Ellerby 详细叙述了为什么 EventBridge 用途这么大 。 二、详解 Serverless 架构模块上面已经介绍了一些背景知识,接下来详细介绍下本文开篇的 Serverless 架构图中的每个模块。 1. 前端开发 我们优秀的无服务器后端需要以某种方式为前端提供数据。为简化与 AWS 耦合的前端开发,我们利用了 Amplify(AWS 提供的前端框架)。Amplify 囊括了几个不同的东西:一个命令行工具、一个基础架构即代码(IaC)工具、一个 SDK 和一套 UI 组件。我们利用前端的 JS 的 SDK 来加快与其他资源(比如用于认证的 Cognito)的集成,这些资源通常是通过其他 基础架构即代码工具(比如 Serverless Framework)来部署的。 2. 网站托管 如今,大多数网站是单页应用(Single Page Application,SPA),它们是功能齐全的动态应用程序,被打包在一组静态文件中。这些文件是在用户的浏览器首次访问 URL 时下载的。在 AWS 环境中,我们在 S3(AWS 的 文件存储服务)中托管这些静态文件,并通过 CloudFront(AWS 的 CDN 服务)来公开。 虽说上面场景是多数,但前端的趋势仍然在朝着诸如 Next.js 之类的服务端渲染(Server Side Rendering,即 SSR)发展。要在 Serverless 架构中运行一个 SSR 网站,我们可以利用 CloudFront 中的 Lambda @ Edge。可以使用更接近用户端的 Lambdas 函数来进行服务端渲染。 3. 域名与证书 在我们网站中,我们希望使用相比于原始自动生成的 S3 URL 更好的 URL,为了做到这一点,我们使用 Certificate Manager (AWS 证书管理服务)来生成证书,并将其绑定在 CloudFront(AWS 的 CDN 服务),并使用 Route 53 (AWS 的域名管理服务)来管理域名。 4. 业务逻辑接口 现在,我们的网站需要连接后端,以获得和推送数据。为此,我们使用 API Gateway来处理 HTTP 连接和路由,并为每个路由同步触发一个 Lambda 函数。我们的 Lambda 函数包含与 DynamoDB(AWS 的非关系数据库) 通信的业务逻辑,以便存储和使用数据。 架构如上文所示是事件驱动的,这意味着可以立即回复用户请求,同时继续在后台异步地处理请求。例如,DynamoDB (AWS 的非关系数据库)提供了流(Streams),它可以对任何数据改动作出反应,并且异步地触发 Lambda 函数。大多数 Serverless 架构的服务都有类似的功能。 5. 异步任务 我们项目的架构是事件驱动的,所以许多的 Lambda 函数都是异步的,通常是由 EventBridge 事件、S3 事件、DynamoDB 流等事件触发。例如,我们系统中有一个异步 Lambda 函数,负责在成功注册后发送欢迎电子邮件。 在分布式异步系统中,故障处理是非常重要的。所以对于异步的 Lambda 函数,我们使用它们的死信队列(Dead Letter Queue,DLQ),并且将最终的故障信息首先传递给 Simple Notification Service(SNS)(AWS 的 消息推送服务,如电子邮件,短信等),然后再传给 Simple Queue Service(SQS)(AWS 的消息队列服务)。我们现在必须这样做,因为 AWS 暂时还不支持将 SQS 直接连接到 Lambda DLQ 上。 6. 后端向前端的推送 有了异步的操作,前端不能在等待一个 XHR (xhr:XMLHttpRequest)响应时仅仅显示一个加载页面。我们需要后端的预备状态和数据推送。为此,我们利用了 API Gateway 的 WebSocket,这个 API 可以使 WebSocket 保持连接状态,并且仅在在收到消息时触发 Lambdas 函数。 这篇文章[4]深入讨论了相比较于其他解决方案,为什么我们选择了 WebSocket,以及如何实现它。 7. 文件上传 处理 Lambda 的文件上传流(Stream)可能会造成较大的开销。相较于这个方案,S3 (AWS 的文件存储服务) 还提供了一个功能,使得前端能使用由 Lambda 生成的、签名(安全)的上传 URL 来直接上传文件到 S3 (AWS 的文件存储服务)。 8. 用户与认证 Cogito( AWS 的认证服务)有我们所需要的所有东西:认证、用户管理、访问控制以及外部身份提供商集成。尽管大家知道它使用起来有些复杂,但它确实可以为我们做不少事情。和其他服务一样,它由专用的 SDK 来与 Lambda 交互,并且可以通过分发事件来触发 Lambda。本例中,API Gateway 路由绑定了原生Cogito 授权服务。同时也暴露了一个用于刷新身份验证令牌的 Lambda 函数和一个用于获取用户列表的 Lambda 函数。 9. 状态机 在某些情形下,我们的逻辑和数据流可能会非常复杂。如果直接在 Lambda 函数内部手动维护和操作这些数据流,正在运行的系统可能会难以跟踪和掌控。因此,AWS 为我们提供了专门提供了一个服务:可视化工作流服务 ( Step Functions)。 我们通过 CloudFormation (AWS 的基础设施编排工具)来声明状态机:包括每个子步骤和状态、每个希望得到的结果和不希望得到的结果,并且将一些操作(例如等待、选择)或者一个 Lambda 函数挂载在这些步骤上。然后,就饿可以通过 AWS 界面实时看到这些服务的运行状态。在其中的每个步骤中,还可以定义重试和失败处理逻辑。Ben Ellerby 在 这篇文章[5]中进一步详细介绍了该服务。 这里举一个更加具体的例子,假设我们希望通过 SaaS 发送一个电子邮件广告,并且能够确保该广告已经发送完毕: 步骤 1,Lambda:要求 SaaS 发送电子邮件广告系列并获取广告的 ID。 步骤 2,任务令牌 Lambda:从 Step Function (可视化工作流服务 ) 中获得回调令牌,将其链接到广告 ID,然后等待来自 SaaS 的回调。 步骤 3,(在任务流之外的)Lambda:在广告的状态发生改变时(待定、存档、失败、成功),从 SaaS 通过一个钩子函数调用,然后通过对应的回调令牌根据新的广告状态来继续任务流。 步骤 4,选择(Choice):基于状态选择,如果这个广告还没有成功,回到第二步。 步骤 5,(结束)Lambda:在广告发送后,更新用户。 这篇文章[6]深入讲述了任务令牌(Task Tokens )是如何工作的。 10. 安全 Identity and Access Management (IAM) (AWS 权限控制服务)可以更加细粒度地管控任何 AWS 访问,不管是开发人员的访问、持续集成与持续交付流程 , 还是 AWS 的服务调用另一个服务。IAM 的管控是非常精细的,需要平台开发者认真考虑某个特定的“消费者”被允许操作的所有行为。这意味着我们基础架构中的每一层都是受到保护的。 对于非常敏感的数据,比如 SaaS 的 API 密钥,我们将其安全地存储在 System Manager (AWS 的系统管理服务)中的 Parameter Store 中。无论是来自我们 Serverless Framework 还是 CloudFormation 文件,甚至是来自业务代码的访问,都必须通过对应的 SDK 来请求它们。值得一提的是, AWS Secrets Manager (AWS 的密钥管理软件)也可以完成类似的工作。 如果对该话题感兴趣,推荐来自 Sat G 的 这篇文章[6],关于 Serverless 中的安全问题在这篇文章中有更详细的讲解。 11. 监控 CloudWatch (AWS 的云监控服务)是监控服务的业界标准。所有 AWS 服务的基础指标和日志都可以发送到 CloudWatch。当然我们可以做更多的事情:将自定义的指标和日志发送到 CloudWatch,创建指标 DashBoard,在超过阈值后触发警报;数据分析与挖掘整理后展现在自定义的图表中。 可观测性方面还有其他选择,比如 X-Ray (AWS 分布式追踪服务),它的目标是在整个分布式系统中端到端地追踪请求,然后直观动态得展现出来。只不过现在这个追踪服务时不时会失败,因为它还不支持某些 AWS 服务,比如 EventBridge(而这在我们的架构中是重中之重)。 另一个服务,基于 X-Ray 和 CloudWatch 构建的 ServiceLens (AWS 的可视化监控服务),效果也很赞。 参考文章 https://medium.com/serverless-transformation/choosing-the-right-cloud-provider-a-serverless-cloud-atlas-eeae672076ce https://aws.amazon.com/cn/eventbridge/ https://medium.com/serverless-transformation/eventbridge-the-key-component-in-serverless-architectures-e7d4e60fca2d https://medium.com/serverless-transformation/asynchronous-client-interaction-in-aws-serverless-polling-websocket-server-sent-events-or-acf10167cc67 https://medium.com/serverless-transformation/serverless-event-scheduling-using-aws-step-functions-b4f24997c8e2 https://medium.com/@zaccharles/9df97fe8973c]]></content>
<categories>
<category>serverless</category>
</categories>
<tags>
<tag>serverless</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何利用 Google 开源工具 Ko 在 kubernetes 上构建并部署 Go 应用]]></title>
<url>%2F2021%2F04%2F23%2Fko-dev%2F</url>
<content type="text"><![CDATA[Ko 是 Google 开源的一款用于构建并部署 Go 应用的工具。 这是一款简单、快速的 Go 应用镜像构建器。并与 Kubernetes 集成,能够将应用快速部署到 Kubernetes 上。是云原生时代 Kubernetes 应用开发的一大利器。 特点: 需要构建的 Go 应用对系统镜像无太多依赖(例如,无 cgo,无 OS 软件包依赖关系),最好是只有一个 go 二进制。 构建镜像的过程不需要 Docker ,因此可以用在轻量化的 CI/CD 场景。 支持 yaml 模板,可以直接用于部署 Kubernetes 应用。 如何使用官方地址在这 https://github.com/google/ko 1. 安装 ko此处安装的是 v0.8.2 版本的 linux x86 版本,可以根据需要自行选择版本下载 Release 地址: https://github.com/google/ko/releases 手动安装 curl -L https://github.com/google/ko/releases/download/v0.8.2/ko_0.8.2_Linux_x86_64.tar.gz | tar xzf - kochmod +x ./kosudo mv ko /usr/local/bin brew 安装 brew install ko go install 安装 go install github.com/google/ko 2. 镜像仓库的认证Ko 依赖的是 Docker 的镜像仓库的认证( Docker config 文件),即 ~/.docker/config.json cat ~/.docker/config.json{ "auths": { "https://index.docker.io/v1/": {}, }, "HttpHeaders": { "User-Agent": "Docker-Client/19.03.13 (darwin)" }, "credsStore": "desktop", "experimental": "disabled", "stackOrchestrator": "swarm"} 如果镜像仓库没有登录过,需要先进行 Docker login 生成 对应的认证配置文件,比如我直接用的是 docker hub ,那么直接 docker login 即可。 3. 设置私有仓库的地址Ko 通过 环境变量 KO_DOCKER_REPO 配置私有仓库的地址, 这决定了 Ko 会将编译好的镜像推到哪个仓库。 # 这是我的 dockerhub 账号, # 这里也可以设置为 docker.io/zhaojizhuang66,不过 docker 会省略掉export KO_DOCKER_REPO = zhaojizhuang66 4. 镜像构建 Ko publish 首先代码一定要在本地的 Go path 中,可以下载示例代码 https://github.com/zhaojizhuang/http-helloworld mkdir -p $GOPATH/src/github.comcd $GOPATH/src/github.comgit clone https://github.com/zhaojizhuang/http-helloworld Ko publish 构建镜像,执行命令 ko publish github.com/http-helloworld/cmd/helloworld (main函数所在的 GoPath 路径)。 $ ko publish github.com/http-helloworld/cmd/helloworld2021/04/23 17:57:41 Using base gcr.io/distroless/static:nonroot for github.com/http-helloworld/cmd/helloworld2021/04/23 17:57:41 No matching credentials were found for "gcr.io/distroless/static", falling back on anonymous2021/04/23 17:57:45 Building github.com/http-helloworld/cmd/helloworld for linux/amd642021/04/23 17:57:46 Publishing zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648:latest2021/04/23 17:57:52 pushed blob: sha256:a3e2b18c53ecc2aab65b3b70e5c16486e48f76490febeb68d99aa18a48265b082021/04/23 17:57:52 pushed blob: sha256:72164b581b02b1eb297b403bcc8fc1bfa245cb52e103a3a525a0835a58ff58e22021/04/23 17:57:56 pushed blob: sha256:5dea5ec2316d4a067b946b15c3c4f140b4f2ad607e73e9bc41b673ee5ebb99a32021/04/23 17:58:07 pushed blob: sha256:d99fea081f251cc06ce68ce7cb224e2a0f63babd03ee9dd6bb03f6bf76dcb5a52021/04/23 17:58:08 zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648:latest: digest: sha256:21d352ec9f9079f8da4c91cfe2461df51a404c079f262390b19fff4cb2ce15a0 size: 7502021/04/23 17:58:08 Published zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648@sha256:21d352ec9f9079f8da4c91cfe2461df51a404c079f262390b19fff4cb2ce15a0zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648@sha256:21d352ec9f9079f8da4c91cfe2461df51a404c079f262390b19fff4cb2ce15a0 也支持相对路径编译,如下 cd $GOPATH/src/github.com/http-helloworld/cmd/helloworldko publish ./ 5. Ko resolve 创建文件deploy.yaml ,yaml 内容如下 apiVersion: apps/v1kind: Deploymentmetadata: name: hello-worldspec: selector: matchLabels: foo: bar replicas: 1 template: metadata: labels: foo: bar spec: containers: - name: hello-world # This is the import path for the Go binary to build and run. image: ko://github.com/http-helloworld/cmd/helloworld ports: - containerPort: 8080 然后执行 ko resolve -f deploy.yaml 结果如下,可以看到 ko://github.com/http-helloworld/cmd/helloworld 被替换成了 zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648@sha256:21d352ec9f9079f8da4c91cfe2461df51a404c079f262390b19fff4cb2ce15a0 $ ko resolve -f deploy.yaml2021/04/23 22:09:19 Using base gcr.io/distroless/static:nonroot for github.com/http-helloworld/cmd/helloworld2021/04/23 22:09:19 No matching credentials were found for "gcr.io/distroless/static", falling back on anonymous2021/04/23 22:09:23 Building github.com/http-helloworld/cmd/helloworld for linux/amd642021/04/23 22:09:24 Publishing zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648:latest2021/04/23 22:09:31 existing blob: sha256:5dea5ec2316d4a067b946b15c3c4f140b4f2ad607e73e9bc41b673ee5ebb99a32021/04/23 22:09:31 existing blob: sha256:d99fea081f251cc06ce68ce7cb224e2a0f63babd03ee9dd6bb03f6bf76dcb5a52021/04/23 22:09:32 existing blob: sha256:72164b581b02b1eb297b403bcc8fc1bfa245cb52e103a3a525a0835a58ff58e22021/04/23 22:09:32 existing blob: sha256:a3e2b18c53ecc2aab65b3b70e5c16486e48f76490febeb68d99aa18a48265b082021/04/23 22:09:32 zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648:latest: digest: sha256:21d352ec9f9079f8da4c91cfe2461df51a404c079f262390b19fff4cb2ce15a0 size: 7502021/04/23 22:09:32 Published zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648@sha256:21d352ec9f9079f8da4c91cfe2461df51a404c079f262390b19fff4cb2ce15a0apiVersion: apps/v1kind: Deploymentmetadata: name: hello-worldspec: selector: matchLabels: foo: bar replicas: 1 template: metadata: labels: foo: bar spec: containers: - name: hello-world # This is the import path for the Go binary to build and run. image: zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648@sha256:21d352ec9f9079f8da4c91cfe2461df51a404c079f262390b19fff4cb2ce15a0 ports: - containerPort: 8080 6. Ko applyko apply -f xxx.yaml 用法同 kubectl apply -f xxx.yaml ,不同的是,ko apply -f 相当于先执行 resolve 将 ko://xxx 替换成镜像地址,然后再执行 kubectl apply -f 。 $ cd $GOPATH/src/github.com/http-helloworld/config$ ko apply -f ./2021/04/23 22:18:10 Using base gcr.io/distroless/static:nonroot for github.com/http-helloworld/cmd/helloworld2021/04/23 22:18:10 No matching credentials were found for "gcr.io/distroless/static", falling back on anonymous2021/04/23 22:18:13 Building github.com/http-helloworld/cmd/helloworld for linux/amd642021/04/23 22:18:15 Publishing zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648:latest2021/04/23 22:18:24 existing blob: sha256:a3e2b18c53ecc2aab65b3b70e5c16486e48f76490febeb68d99aa18a48265b082021/04/23 22:18:24 existing blob: sha256:d99fea081f251cc06ce68ce7cb224e2a0f63babd03ee9dd6bb03f6bf76dcb5a52021/04/23 22:18:24 existing blob: sha256:72164b581b02b1eb297b403bcc8fc1bfa245cb52e103a3a525a0835a58ff58e22021/04/23 22:18:24 existing blob: sha256:5dea5ec2316d4a067b946b15c3c4f140b4f2ad607e73e9bc41b673ee5ebb99a32021/04/23 22:18:25 zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648:latest: digest: sha256:21d352ec9f9079f8da4c91cfe2461df51a404c079f262390b19fff4cb2ce15a0 size: 7502021/04/23 22:18:25 Published zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648@sha256:21d352ec9f9079f8da4c91cfe2461df51a404c079f262390b19fff4cb2ce15a0deployment.apps/hello-world configured 7. 替换基础镜像可以通过 ko 的 config 文件中的 defaultBaseImage 变量来设置基础镜像, 配置文件名为 .ko.yaml , ko 执行时默认会在当前目录下寻找 .ko.yaml 文件,也可以通过 环境变量 KO_CONFIG_PATH 来指定 .ko.yaml 的路径 # ~/.ko.yamldefaultBaseImage: ubuntu 执行如下 $ export KO_CONFIG_PATH=~/$ ko apply -f ./config2021/04/23 22:28:50 Using base ubuntu for github.com/http-helloworld/cmd/helloworld2021/04/23 22:29:00 Building github.com/http-helloworld/cmd/helloworld for linux/amd642021/04/23 22:29:01 Publishing zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648:latest2021/04/23 22:29:14 existing blob: sha256:72164b581b02b1eb297b403bcc8fc1bfa245cb52e103a3a525a0835a58ff58e22021/04/23 22:29:14 existing blob: sha256:d99fea081f251cc06ce68ce7cb224e2a0f63babd03ee9dd6bb03f6bf76dcb5a52021/04/23 22:29:14 mounted blob: sha256:a70d879fa5984474288d52009479054b8bb2993de2a1859f43b5480600cecb242021/04/23 22:29:14 mounted blob: sha256:c4394a92d1f8760cf7d17fee0bcee732c94c5b858dd8d19c7ff02beecf3b4e832021/04/23 22:29:14 mounted blob: sha256:10e6159c56c084c858f5de2416454ac0a49ddda47b764e4379c5d5a147c9bf5f2021/04/23 22:29:16 pushed blob: sha256:91d93e3477d55b71f5760478dcab690846ca5f76d92bbef874970460b3e73e5b2021/04/23 22:29:17 zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648:latest: digest: sha256:623cb60ff10751f3f6a5f6570aaf5f49aee5fb6afc1ef5cfde4dd48a8b4d57df size: 10722021/04/23 22:29:17 Published zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648@sha256:623cb60ff10751f3f6a5f6570aaf5f49aee5fb6afc1ef5cfde4dd48a8b4d57dfdeployment.apps/hello-world configured 还可以 通过.ko.yaml 文件中的 baseImageOverrides 对指定编译二进制替换基础镜像,如本示例中,设置如下: # ~/.ko.yamldefaultBaseImage: ubuntubaseImageOverrides: github.com/http-helloworld/cmd/helloworld: centos 修改 .ko.yaml 后,执行命令 $ ko apply -f config2021/04/23 22:38:14 Using base centos for github.com/http-helloworld/cmd/helloworld2021/04/23 22:38:26 Building github.com/http-helloworld/cmd/helloworld for linux/amd642021/04/23 22:38:27 Publishing zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648:latest2021/04/23 22:38:29 existing blob: sha256:72164b581b02b1eb297b403bcc8fc1bfa245cb52e103a3a525a0835a58ff58e22021/04/23 22:38:29 existing blob: sha256:d99fea081f251cc06ce68ce7cb224e2a0f63babd03ee9dd6bb03f6bf76dcb5a52021/04/23 22:38:29 mounted blob: sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec36212021/04/23 22:38:32 pushed blob: sha256:e909db5555a2c7310e605e60a47a98661c9a5c54d567f741a8d677f84c8a887f2021/04/23 22:38:32 zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648:latest: digest: sha256:dc009335be23a9eee0235e425218f18a68c3210b00adac7cfe736d9bf38f4121 size: 7522021/04/23 22:38:32 Published zhaojizhuang66/helloworld-cbcbba9849adcc25ce56a08dfd597648@sha256:dc009335be23a9eee0235e425218f18a68c3210b00adac7cfe736d9bf38f4121deployment.apps/hello-world configured]]></content>
<categories>
<category>云原生</category>
</categories>
<tags>
<tag>k8s</tag>
<tag>云原生</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【Knative系列】如何基于 Knative 开发自定义控制器]]></title>
<url>%2F2021%2F04%2F21%2Fwrite-controller%2F</url>
<content type="text"><![CDATA[1. 为什么要开发 自定义 controller?开源版本的 Knative 提供了扩缩容及事件驱动的架构,对于大部分场景的 Serverless 已经满足了,不过对于商业版本的 Serverless 平台来说,免不了要添加一些增强特性。 通常情况下,In-Tree 形式的增强不推荐,而且这种方式也会因开源版本升级带来不小的适配工作量。 Out-Of-Tree 形式的 自定义 controller 是一种很好特性增强方式,而且社区本身对于周边组件的解耦也是通过 controller 来对接的。比如: net-contour : 对接 Contour 七层负载的网络插件 net-kourier : 对接 Kourier 七层负载的网络插件 net-istio : 对接 Istio 七层负载的网络插件 上述提到的几个网络插件都是通过 自定义 Controller 结合 Kingress 这个 CRD资源来实现 2. How?对于 有过 Kubernetes operator 开发经验的同学来说,可能对 Kubebuilder 更熟悉一些,其实 Knative 自定义 控制器的开发更简单,下面一步一步介绍怎么开始 2.1 Fork 社区 Template社区 项目地址在 https://github.com/knative-sandbox/sample-controller,直接 fork 到个人仓库。 2.2 sample-controller介绍代码下载到本地,目录如下,如下(此处省略掉不重要的文件): sample-controller├── cmd│ ├── controller │ │ └── main.go # controller 的启动入口文件│ ├── schema│ │ └── main.go # 生成 CRD 资源的 工具│ └── webhook│ └── main.go # webhook 的入口文件├── config # controller 和webhook 的部署文件(deploy role clusterrole 等等,此处省略)│ ├── 300-addressableservice.yaml│ ├── 300-simpledeployment.yaml├── example-addressable-service.yaml # CR 资源的示例yaml├── example-simple-deployment.yaml # CR 资源的示例yaml├── hack│ ├── update-codegen.sh # 生成 informer,clientset,injection,lister 等│ ├── update-deps.sh│ ├── update-k8s-deps.sh│ └── verify-codegen.sh├── pkg│ ├── apis│ │ └── samples │ │ ├── register.go│ │ └── v1alpha1 # 此处需编写 CRD 资源的types│ ├── client # 执行 hack/update-codegen.sh 后自动生成的文件│ │ ├── clientset│ │ ├── informers│ │ ├── injection│ │ └── listers│ └── reconciler # 此处是控制器的主要逻辑,示例中实现了两个控制器,每个控制器包含主控制器入口(controller.go) 和对应的 reconcile 逻辑│ ├── addressableservice│ │ ├── addressableservice.go│ │ └── controller.go│ └── simpledeployment│ ├── controller.go│ └── simpledeployment.go 目录介绍: cmd: 包含 controller 和webhook 的入口 main 函数,以及生成 crd 的 schema 工具(这也是笔者的社区贡献之一) config: controller 和webhook 的部署文件(本文只关注 controller) hack:是 程序自动生成代码的脚本,其中的 update-codegen.sh 最常用,是生成 informer,clientset,injection,lister 的工具 pkg/apis: 此处是 CRD 定义的 types 文件 pkg/client: 这里是 执行 hack/update-codegen.sh 后自动生成的,包含 clienset,informers, injection(常用的是其中的 reconfiler 框架,框架中 lister 和 informer 可以从 context 中获取,这也是 injection 的含义) ,lister。 pkg/reconciler: 这里是控制器的主要逻辑,包括控制器主入口 controller.go 和对应的 reconciler逻辑 2.3 CRD 资源定义1. 确定 GKV,即资源的 Group、Kind、Version此处实例中,有两个 crd 资源,本文主要以 AddressableService 为例讲解。 Group 为samples.knative.dev , Kind 为 AddressableService (实例中有两个类型,取一个介绍), Version 为v1alpha1 2.编写 CRD types 文件目录按照 /pkg/apis/<kind 一般取 groupname 第一个逗号前的单词>/<version> Group 和 Version及其注册 # pkg/apis/samples/register.go#20package samplesconst ( GroupName = "samples.knative.dev") 在 addKnownTypes 中将 Kind 注册 # pkg/apis/samples/v1alpha1/register.go#27// SchemeGroupVersion is group version used to register these objectsvar SchemeGroupVersion = schema.GroupVersion{Group: samples.GroupName, Version: "v1alpha1"}// Adds the list of known types to Scheme.func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &AddressableService{}, &AddressableServiceList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil} 为 CRD types 编写对应的 spec 和 status, 注意其中的注解,这是 hack/update-codegen.sh 执行生成 clientset 和 reconciler 的关键 // +genclient// +genreconciler// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object # pkg/apis/samples/v1alpha1/addressable_service_types.go#32// +genclient// +genreconciler// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Objecttype AddressableService struct { metav1.TypeMeta `json:",inline"` // +optional metav1.ObjectMeta `json:"metadata,omitempty"` // Spec holds the desired state of the AddressableService (from the client). // +optional Spec AddressableServiceSpec `json:"spec,omitempty"` // Status communicates the observed state of the AddressableService (from the controller). // +optional Status AddressableServiceStatus `json:"status,omitempty"`} 3 CRD 资源的 配置可以看到,对于每个 CRD 资源,除了 xxxtypes.go 外,还有以下几个文件 xxx_validation.go: 用于 webhook 校验 _xxx__lifecycle.go: 用于status 状态的设置 xxx_defaults.go: 用于 默认值的设置 可在 xxx_types 文件中 声明如下,校验是否实现了对应的接口 // Check that AddressableService can be validated and defaulted._ apis.Validatable = (*AddressableService)(nil)_ apis.Defaultable = (*AddressableService)(nil)_ kmeta.OwnerRefable = (*AddressableService)(nil)// Check that the type conforms to the duck Knative Resource shape._ duckv1.KRShaped = (*AddressableService)(nil) 4. hack/update-codegen.sh 文件配置# 1. knative.dev/sample-controller/pkg/client 表示生成代码的目标位置# 2. knative.dev/sample-controller/pkg/apis 表示 CRD 资源定义的文件位置# 3. `samples:v1alpha1` 表示 crd 的kind 版本# 4. `deepcopy,client,informer,lister` 表示生成对应的方法${CODEGEN_PKG}/generate-groups.sh "deepcopy,client,informer,lister" \ knative.dev/sample-controller/pkg/client knative.dev/sample-controller/pkg/apis \ "samples:v1alpha1" \ --go-header-file ${REPO_ROOT_DIR}/hack/boilerplate/boilerplate.go.txtgroup "Knative Codegen"# injection 这是生成 reconciler 的关键# Knative Injection${KNATIVE_CODEGEN_PKG}/hack/generate-knative.sh "injection" \ knative.dev/sample-controller/pkg/client knative.dev/sample-controller/pkg/apis \ "samples:v1alpha1" \ --go-header-file ${REPO_ROOT_DIR}/hack/boilerplate/boilerplate.go.txt 5. 编写完毕,执行 bash hack/update-codegen.sh执行完毕没出错的话,就可以进行下一步编写控制器主逻辑了 如果是 mac 用户,这里一定要升级 bash 版本到 v4(执行 bash –version 查看),不然会出现如下问题,升级方法请自行百度 bash hack/update-codegen.shhack/../vendor/knative.dev/hack/library.sh: line 25: conditional binary operator expected 2.4 控制器逻辑介绍controller 入口文件 # cmd/controller/main.gofunc main() { sharedmain.Main("controller", addressableservice.NewController, simpledeployment.NewController, )} sharedmain.Main 函数传入 controller 的初始化方法,该方法会返回一个 controller 的实现 controller.impl ,impl 的定义如下 # https://github.com/knative/pkg # knative.dev/pkg/controller/controller.go#188type Impl struct {// 控制器的名字 Name string // Reconciler 是主要实现逻辑,实现了接口 Reconcile(ctx context.Context, key string) error // Reconciler 会调用 Reconciler Reconciler// 工作队列 workQueue *twoLaneQueue}# knative.dev/pkg/controller/controller.go#65type Reconciler interface { Reconcile(ctx context.Context, key string) error sharedmain.Main 会执行以下事情: 启动各种 informer,启动 所有 controller, knative.dev/pkg/injection/sharedmain/main.go#238 执行工作流 processNextWorkItem ,knative.dev/pkg/injection/sharedmain/main.go#468 调用 Reconciler 接口的 Reconcile(ctx context.Context,key string) err 函数 Reconcile(ctx context.Context,key string) err 函数调用 具体的 Reconciler 的实现接口 (这里就是用户自己实现的代码了)sample-controller/pkg/client/injection/reconciler/samples/v1alpha1/addressableservice/reconciler.go#181 FinalizeKind(ctx context.Context, o v1alpha1.AddressableService) reconciler.Event FinalizeKind(ctx context.Context, o v1alpha1.AddressableService) reconciler.Event 接下来就是上述第 4点说的自己实现的代码了 2.5 控制器逻辑编写代码主要在 如下两个文件: sample-controller/pkg/reconciler/addressableservice/addressableservice.go sample-controller/pkg/reconciler/addressableservice/controller.go addressableservice.go 实现 AddressableService 的 ReconcileKind 接口,如果删除 CR 资源时要做清理动作,可以实现 Finalizer 的 FinalizeKind 接口,可通过以下声明 确保接口的实现(IDE 一键生成函数框架) // Check that our Reconciler implements Interfacevar _ addressableservicereconciler.Interface = (*Reconciler)(nil)var _ addressableservicereconciler.Finalizer = (*Reconciler)(nil) controller 中 代码如下# pkg/reconciler/addressableservice/controller.go#// 借助 injection 从 context 中获取 informer addressableserviceInformer := addressableserviceinformer.Get(ctx)svcInformer := svcinformer.Get(ctx) // 实例化 addressableservice 的 Reconcilerr := &Reconciler{ ServiceLister: svcInformer.Lister(),}// 实例化 controller.impl 返回 供 controller 框架调用impl := addressableservicereconciler.NewImpl(ctx, r)r.Tracker = tracker.New(impl.EnqueueKey, controller.GetTrackerLease(ctx))logger.Info("Setting up event handlers.") // 添加 informer 的hander 函数addressableserviceInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue))svcInformer.Informer().AddEventHandler(controller.HandleAll( // Call the tracker's OnChanged method, but we've seen the objects // coming through this path missing TypeMeta, so ensure it is properly // populated. controller.EnsureTypeMeta( r.Tracker.OnChanged, corev1.SchemeGroupVersion.WithKind("Service"), ),)) handler 函数 为 informer 添加 函数除了实例中的 Informer().AddEventHandler,还可以 通过 Informer().AddEventHandlerWithResyncPeriod 确保除了 watch 之外,周期性将 CR 全量加入 工作队列中处理。 filter 函数 还可以添加如下 filter 函数,过滤进入 工作队列的 资源,(在资源数量巨大时能优化性能) domainInformer.Informer().AddEventHandlerWithResyncPeriod(cache.FilteringResourceEventHandler{ FilterFunc: controller.FilterControllerGK(v1beta1.Kind("Function")), Handler: controller.HandleAll(impl.EnqueueControllerOf),}, ControllerResyncPerion)// k8s don't allow cross namespace owerreferences, so filter resource with labelk8ssvcInformer.Informer().AddEventHandlerWithResyncPeriod(cache.FilteringResourceEventHandler{ FilterFunc: FilterLabelKeyExists(api.FuncNameLabelKey), Handler: controller.HandleAll(impl.EnqueueLabelOfNamespaceScopedResource(api.FuncNameSpaceLabelKey, api.FuncNameLabelKey)),}, ControllerResyncPerion) 2.6 Reconciler 逻辑编写 参考 sample-controller/pkg/reconciler/addressableservice/addressableservice.go 文件即可,其中注意status 在 reconciler 中调用 xxx_lifecycle.go 中的 状态设置函数可以,controller 框架会在 reconcile 流程结束后将 CR 资源的状态 通过 kube-apiserver 更新到 etcd 中# sample-controller/pkg/reconciler/addressableservice/addressableservice.go#76o.Status.MarkServiceAvailable()o.Status.Address = &duckv1.Addressable{ URL: &apis.URL{ Scheme: "http", Host: network.GetServiceHostname(o.Spec.ServiceName, o.Namespace), },} 2.7 调试1. 生成 CRD 描述文件 并 apply 到集群 在 sample-controller/cmd/schema/main.go 中注册,如下: func main() { registry.Register(&v1alpha1.AddressableService{}) if err := commands.New("knative.dev/sample-controller").Execute(); err != nil { log.Fatal("Error during command execution: ", err) }} 执行命令 go run cmd/schema/main.go dump AddressableService 将生成的 yaml 粘贴到 sample-controller/config/300-addressableservice.yaml 中的spec.versions.schema.openAPIV3Schema 下 apply crd yaml,在 k8s 集群中执行 kubectl apply -f config/300-addressableservice.yaml IDE 中 debug 如果是在 mac 中的 IDE 调试,将 k8s 集群中的 config 文件 复制一份,放在 mac 地址的 ~/.kube 目录下,window linux 类似,config 放在用户目录下的 .kube目录下:为程序添加 环境变量 SYSTEM_NAMESPACE ,主要是用于 controller 选主,不设置会 panic。 接下来,直接 debug sample-controller/cmd/controller/main.go 中的 main 函数即可 !]]></content>
<categories>
<category>k8s</category>
</categories>
<tags>
<tag>knative</tag>
<tag>controller</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【Knative系列】Knative Source 开发指南]]></title>
<url>%2F2021%2F01%2F25%2Fwrite-eventsource%2F</url>
<content type="text"><![CDATA[准备: 安装 go-licenses https://github.com/google/go-licenses 1. 相关概念的设计解耦2. API 定义3. Controller4. Reconciler5. Receive Adapter6. Example YAML7. Moving the event source to the knative-sandbox organization 注意:运行 hack/update-codegen.sh 时在 linux上,mac 上会报错,见 issue https://github.com/knative/hack/issues/50 参考 官网 doc https://knative.dev/docs/eventing/samples/writing-event-source/]]></content>
<categories>
<category>k8s</category>
</categories>
<tags>
<tag>knative</tag>
<tag>source</tag>
<tag>event</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【Knative系列】看完这篇还不懂 Knative Serving,你来打我~(史上最详细)]]></title>
<url>%2F2020%2F10%2F15%2Fknative-serving%2F</url>
<content type="text"><![CDATA[本文主要讲解 Knative serving 系统及组件: 发展历程Knative 是谷歌开源的 serverless 架构方案,旨在提供一套简单易用的 serverless 方案,把 serverless 标准化。目前参与的公司主要是 Google、Pivotal、IBM、Red Hat,2018年7月24日才刚刚对外发布,当前还处于快速发展的阶段(3.4k star, 3.3k issue)。 Knative 包含 build(已被tekton取代),serving,event三个部分,本文主要介绍serving。 先看下Knative的发展里历程 项目 时间 发起公司 k8s 2014年6月 Google (同年加入 微软、RedHat、IBM、Docker) istio 2017 年6 月8 日 Google,IBM 和 Lyft knative 2018年7月24日 Google (目前主要为 Google、Pivotal、IBM、Red Hat) 前言 有了 ”k8s,为什么还要 knative”通常情况下 Serverless = Faas + Baas,Faas 无状态(业务逻辑),Baas 有状态(通用服务:数据库,认证,消息队列)。 既然有了 k8s (paas), 为什么还需要 Knative (Serverless),下面从四个方面来进行解释:资源利用率,弹性伸缩,按比例灰度发布,用户运维复杂性 1. 资源利用率讲资源利用率之前先看下下面两个应用,左边应用 A 这个是典型的中长尾应用,中长尾应用就是那些每天大部分时间都没有流量或者有很少流量的应用。 想一下,如果用 paas(k8s) 来实现的话,对于应用 A,需按照资源占用的资源最高点来申请规格,也就是 4U10G, 而且 paas 最低实例数>=1, 长此以往, 当中长尾应用足够多时,资源利用率可想而知。有可能会出现 大部分边缘集群资源被预占,但是利用率却很低。 而 Knative,恰恰可以解决应用A的资源占用问题,因为 Knative 可以将实例缩容为0,并根据请求自动扩缩容,缩容到零可以大大增加集群的资源利用率,因为中长尾应用都是按需所取,不会过度空占用资源。 比较合理的是对应应用A 用 Knative(Serverless),对于应用 B 用 k8s(Paas) 2. 弹性伸缩大家可能会想到,k8s 也有 hpa 进行扩缩容,但是 Knative 的 kpa 和 k8s 的 hpa 有很大的不同: • Knative 支持缩容到 0 和从 0 启动,反应更迅速适合流量突发场景; • K8s HPA 不支持缩容到 0 ,反应比较保守 具体比较如下 Knative KPA k8s HPA 指标类型 可以根据 请求量扩速容 只能根据 cpu memory 等指标扩缩容(或自定义指标) 01启动 可以缩容到0和冷启动 只能缩容到1(如果缩容到0,就没有实例了,流量进不来,metrics数据永远为0,此时HPA也无能为力) 指标获取方式 Knative 指标获取有两种方式,Activator 和queue-proxy, activator的metrics 是通过websocket 主动push 给Autoscaler的,反应更迅速 k8s 只能是通过prometheus 轮询获取。 反应速度 Knative 默认会 计算 60 秒窗口内的平均并发数, 也会计算 6 秒的恐慌窗口,6s内达到目标并发的 2 倍,则会进入恐慌模式。在恐慌模式下,Autoscaler 在更短、更敏感的紧急窗口上工作 而且 HPA 本身设计比较保守,有一个稳定期(默认5min)默认在5min内没有重新扩缩容的情况下,才会触发扩缩容。当大流量突发过来时,如果正处在5min内的HPA稳定期,这个时候根据HPA的策略,会导致无法扩容。 3. 按比例灰度发布设想一下,假如通过 k8s来进行灰度发布怎么做,只能是通过两个Deployment和两个service,如果灰度升级的话只能通过修改两个 Deployment 的rs,一个逐渐增加,一个逐渐减少,如果想要按照百分比灰度,只能在外部负载均衡做文章,所以要想 Kubernetes 原生实现,至少需要一个按流量分发的网关,两个 service,两个 deployment ,两个 ingress , hpa,prometheus 等,实现起来相当复杂。 使用 Knative 就可以很简单的实现,只需一个 ksvc 即可 4. 用户运维复杂性使用 Knative 免运维,低成本:用户只关心业务逻辑,由工具和云去管理资源,复杂性由平台去做:容器镜像构建,Pod 的管控,服务的发布,相关的运维等。 k8s 本质上还是基础设施的抽象,对应pod的管控,服务的发布,镜像的构建等等需要上层的包装。 1. 相关概念介绍资源介绍: knative 资源 推荐一个工具 kubectl tree 可以查看k8s资源之间的引用关系 1. Service(ksvc) ksvc 是 Knative 中 最顶层的 CR 资源,用于定义 Knative 应用 ,包含镜像以及 traffic 百分比等等(本例配了 两个版本,流量百分比是 10%,90%), 可以接管 route 和 configuration 的配置 2. Configuration configuration 是 Knative 应用的最新配置,也就是应用目前期望的状态。configuration 更改会产生快照 revision 3. Revision revision 是 Knative 应用的快照,Knative 的设计理念中 revision 是不可更改的,可以看做是 git 的 历史 commit 记录 4. Route route 是 Knative 蓝绿发布,金丝雀发布的关键,用于声明不同版本之间流量的百分比。 5. Ingress(kingress) Knative 的流量入口网关 是通过 kingress 抽象的。详情可以看第二节 Knative 网关 6. PodAutoScaler(kpa) 7. ServerlessService(sks) 8. k8s Service (public) 注意,此处的 k8s service 没有 label selector,说明这个 service 的后端 endpoint 不是由 k8s 自动控制的,实际上这个 svc 的后端 endpoint 是 由 knative 自己来控制的,(是取 activator 的pod ip 还是 服务真实实例的pod ip) 9. k8s Service (private) private 类型 的 svc 不同于 public 类型的 svc,这个 svc 是通过label selector 来筛选后端 endpoint 的,这里后端指向的永远是 服务真实实例的 pod ip 2. Knative 网关Knative 从设计之初就考虑到了其扩展性,通过抽象出来 Knative Ingress (kingress)资源来对接不同的网络扩展:Ambassador、Contour、Gloo、Istio、Kong、Kourier 这些网络插件都是基于 Envoy 这个新生的云原生服务代理,关键特性是可以基于流量百分比进行分流。 感兴趣的可以研究下 https://www.servicemesher.com/envoy/intro/what_is_envoy.html 3. Knative 组件1. Queue-proxy Queue-proxy 是每个业务 pod 中都存在的 sidecar,每个发到业务pod的请求都会先经过 queue-proxy queue-proxy 的主要作用是 收集和限制 业务应用的并发量,比如当一个 revision 设定了并发量为 5 ,那么 queue-proxy 会保证每次到达业务容器的请求数不会大于 5. 如果多于 5 个请求到达,queue-proxy 会将请求暂存在本地队列中。 几个端口表示如下: • 8012, queue-proxy 代理的http端口,流量的入口都会到 8012• 8013, http2 端口,用于grpc流量的转发• 8022, queue-proxy 管理端口,如健康检查• 9090, queue-proxy的监控端口,暴露指标供 autoscaler 采集,用于kpa扩缩容• 9091, prometheus 应用监控指标(请求数,响应时长等)• USER_PORT, 是用户配置的容器端口,即业务实际暴露的服务端口,ksvc container port 配置的 2. Autoscaller AutoScaller 主要是 Knative 的扩缩容实现,通过request指标来决定是否扩缩容实例,指标来源有两个: • 通过获取每个 pod queue-proxy 中的指标• Activator 通过 websocket 主动上报 扩缩容算法如下: autoscaler 是基于每个 Pod(并发)的运行中请求的平均数量。系统的默认目标并发性为 100,但是我们为服务使用了 10。我们为服务加载了 50 个并发请求,因此自动缩放器创建了 5 个容器( 50 个并发请求/目标 10 = 5 个容器)。 算法中有两种模式,分别是 panic 和 stable 模式,一个是短时间,一个是长时间,为了解决短时间内请求突增的场景,需要快速扩容。 Stable Mode(稳定模式) 在稳定模式下,Autoscaler 根据每个pod期望的并发来调整Deployment的副本个数。根据每个pod在60秒窗口内的平均并发来计算,而不是根据现有副本个数计算,因为pod的数量增加和pod变为可服务和提供指标数据有一定时间间隔。 Panic Mode (恐慌模式) KPA会在 60 秒的窗口内计算平均并发性,因此系统需要一分钟时间才能稳定在所需的并发性级别。但是,自动缩放器还会计算一个 6秒 的紧急窗口,如果该窗口达到目标并发性的 2 倍,它将进入紧急模式。在紧急模式下,自动缩放器在较短,更敏感的紧急窗口上运行。一旦在 60 s秒内不再满足紧急情况,autoscaler 将返回到最初的 60 秒稳定窗口。 | Panic Target---> +--| 20 | | | <------Panic Window | | Stable Target---> +-------------------------|--| 10 CONCURRENCY | | | | <-----------Stable Window | | |--------------------------+-------------------------+--+ 0120 60 0 TIME 3. Activator Activator 的作用:流量的负载和缓存,是Knative能缩容到 0 的关键 实例为0 时(冷启动),流量会先转发到 Activator,由 Activator 通过 websocket 主动触发 Autoscaler 扩缩容。 Activator 本身的扩缩容通过 hpa 实现 [email protected]:~# kubectl get hpa -n knative-servingNAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGEactivator Deployment/activator 8%/100% 1 20 1 73d 可以看到 Activator 默认最大可以扩缩容到 20 Activator 只在 冷启动阶段是 proxy 模式,当当实例足够时,autoscaler 会更新 public service 的endpoints 指向 revision对应的pod,将请求导向真正的后端,这时候处理请求过程中 activator 不在起作用 详细步骤如下: 冷启动时 activator 的角色 Kingress 收到请求(确切的是 网关收到请求后根据kingress生成的配置做转发,如istio是virtualservice)后,将请求导至 activator activator 将请求保存在缓存中 Activator 触发 autoscaller, 触发过程中其中做了两件事:a. Activator 携带了第2步缓存的请求信息到 autoscaler;b. 触发信号促使 autoscaller 立即做出扩容决定,而不是等下个扩容周期; 考虑到有请求需要处理,但是目前实例是0,autoscaller 决定扩容出一个实例,于是设置一个新的 scale 目标 activator 在等待 autoscaler 和 Serving 准备工作的时候,activator 会去轮询 Serving 查看实例时候准备完毕 Serving 调用k8s 生成k8s 实例(deploy,pod) 当实例可用时,Activator 将请求从缓存中 proxy 到实例 Activator 中的 proxy 模块 将请求代理到实例 Activator 中的 proxy 模块 同样将 response 返回到 kingress 4. 扩缩容原理1. 正常扩缩容场景(非 0 实例) 稳定状态下的工作流程如下: 请求通过 ingress 路由到 public service ,此时 public service 对应的 endpoints 是 revision 对应的 pod Autoscaler 会定期通过 queue-proxy 获取 revision 活跃实例的指标,并不断调整 revision 实例。 请求打到系统时, Autoscaler 会根据当前最新的请求指标确定扩缩容比例。 SKS 模式是 serve, 它会监控 private service 的状态,保持 public service 的 endpoints 与 private service 一致 。 2. 缩容到 0 的场景 缩容到零过程的工作流程如下: AutoScaler 通过 queue-proxy 获取 revision 实例的请求指标 一旦系统中某个 revision 不再接收到请求(此时 Activator 和 queue-proxy 收到的请求数都为 0) AutoScaler 会通过 Decider 确定出当前所需的实例数为 0,通过 PodAutoscaler 修改 revision 对应 Deployment 的 实例数 在系统删掉 revision 最后一个 Pod 之前,会先将 Activator 加到 数据流路径中(请求先到 Activator)。Autoscaler 触发 SKS 变为 proxy 模式,此时 SKS 的 public service 后端的endpoints 变为 Activator 的IP,所有的流量都直接导到 Activator 此时,如果在冷却窗口时间内依然没有流量进来,那么最后一个 Pod 才会真正缩容到零。 3. 从 0 启动的场景 冷启动过程的工作流程如下: 当 revision 缩容到零之后,此时如果有请求进来,则系统需要扩容。因为 SKS 在 proxy 模式,流量会直接请求到 Activator 。Activator 会统计请求量并将 指标主动上报到 Autoscaler, 同时 Activator 会缓存请求,并 watch SKS 的 private service, 直到 private service 对应的endpoints产生。 Autoscaler 收到 Activator 发送的指标后,会立即启动扩容的逻辑。这个过程的得出的结论是至少一个Pod要被创造出来,AutoScaler 会修改 revision 对应 Deployment 的副本数为为N(N>0),AutoScaler 同时会将 SKS 的状态置为 serve 模式,流量会直接到导到 revision 对应的 pod上。 Activator 最终会监测到 private service 对应的endpoints的产生,并对 endpoints 进行健康检查。健康检查通过后,Activator 会将之前缓存的请求转发到健康的实例上。 最终 revison 完成了冷启动(从零扩容)。]]></content>
<categories>
<category>knative</category>
</categories>
<tags>
<tag>knative</tag>
<tag>serving</tag>
</tags>
</entry>
<entry>
<title><![CDATA[【Knative系列】理解 Knative 扩缩容系统的设计原理]]></title>
<url>%2F2020%2F09%2F30%2Fknative-autoscalling%2F</url>
<content type="text"><![CDATA[本文主要讲解 Knative 扩缩容系统的设计原理及实现细节,主要从以下三个方面进行讲解: Knative Serving 扩缩容系统的组件; 涉及的API; 扩缩容和冷启动时的 控制流和数据流的一些细节; 组件Knative Serving 是 Knative 系统的核心,而理解 Knative Serving 系统内的组件能更容易了理解 Knative Serving 系统的实现:了解其中的控制流和数据流的走向,了解其在扩缩容过程中的作用。因篇幅有限,这里只对组件进行简要描述,后续会针对每个组件进行详细的单独讲解。 1. queue-proxyqueue-proxy 是 一个伴随着用户容器运行的 Sidecar 容器,跟用户容器运行在同一个 Pod 中。每个请求到达业务容器之前都会经过 queue-proxy 容器,这也是它问什么叫 proxy 的原因。 queue-proxy 的主要作用是统计和限制到达业务容器的请求并发量,当对一个 Revision 设置了并发量之后(比如设置了5),queue-proxy 会确保不会同时有超过5个请求打到业务容器。当有超过5个请求到来时,queue-proxy会先把请求暂存在自己的队列 queue 里,(这也是为什么名字里有个 queue的缘故)。queue-proxy 同时会统计进来的请求量,同时会通过指定端口提供平均并发量和 rps(每秒请求量)的查询。 2. AutoscalerAutoscaler 是 Knative Serving 系统中一个重要的 pod,它由三部分组成: PodAutoscaler reconciler Collector Decider PodAutoscaler reconciler 会监测 PodAutoscaler(KPA)的变更,然后交由 Collector 和 Decider 处理 Collector 主要负责从应用的 queue-proxy 那里收集指标, Collector 会收集每个实例的指标,然后汇总得到整个系统的指标。为了实现扩缩容,会搜集所有应用实例的样本,并将收集到的样本反映到整个集群。 Decider 得到指标之后,来决定多少个Pod 被扩容出来。简单的计算公式如下: want = concurrencyInSystem/targetConcurrencyPerInstance 另外,扩缩容的量也会受到 Revision 中最大最小实例数的限制。同时 Autoscaler 还会计算当前系统中剩余多少突发请求容量(可扩缩容多少实例)进来决定 请求是否走 Activator 转发。 3. ActivatorActivator 是整个系统中所用应用共享的一个组件,是可以扩缩容的,主要目的是缓存请求并给 Autoscaler主动上报请求指标 Activator 主要作用在从零启动和缩容到零的过程,能根据请求量来对请求进行负载均衡。当 revision 缩容到零之后,请求先经过 Activator 而不是直接到 revision。 当请求到达时,Activator 会缓存这这些请求,同时携带请求指标(请求并发数)去触发 Autoscaler扩容实例,当实例 ready后,Activator 才会将请求从缓存中取出来转发出去。同时为了避免后端的实例过载,Activator 还会充当一个负载均衡器的作用,根据请求量决定转发到哪个实例(通过将请求分发到后端所有的Pod上,而不是他们超过设置的负载并发量)。 Knative Serving 会根据不同的情况来决定是否让请求经过 Activator,当一个应用系统中有足够多的pod实例时,Activator 将不再担任代理转发角色,请求会直接打到 revision 来降低网络性能开销。 跟 queue-proxy 不同,Activator 是通过 websocket 主动上报指标给 Autoscaler,这种设计当然是为了应用实例尽可能快的冷启动。queue-proxy 是被动的拉取:Autoscaler去 queue-proxy指定端口拉取指标。 APIPodAutoscaler (PA,KPA)API: podautoscalers.autoscaling.internal.knative.dev PodAutoscaler 是对扩缩容的一个抽象,简写是 KPA 或 PA ,每个 revision 会对应生成一个 PodAutoscaler。 可通过下面的指令查看 kubectl get kpa -n xxx ServerlessServices (SKS)API: serverlessservices.networking.internal.knative.dev ServerlessServices 是 KPA 产生的,一个 KPA 生成一个 SKS,SKS 是对 k8s service 之上的一个抽象, 主要是用来控制数据流是直接流向服务 revision(实例数不为零) 还是经过 Activator(实例数为0)。 对于每个 revision,会对应生成两个k8s service ,一个public service,一个 private service. private service 是标准的 k8s service,通过label selector 来筛选对应的deploy 产生的pod,即 svc 对应的 endpoints 由 k8s 自动管控。 public service 是不受 k8s 管控的,它没有 label selector,不会像 private service 一样 自动生成 endpoints。public service 对应的 endpoints 由 Knative SKS reconciler 来控制。 SKS 有两种模式:proxy 和 serve serve 模式下 public service 后端 endpoints 跟 private service一样, 所有流量都会直接指向 revision 对应的 pod。 proxy 模式下 public service 后端 endpoints 指向的是 系统中 Activator 对应的 pod,所有流量都会流经 Activator。 数据流下面看几种情况下的数据流向,加深对Knative 扩缩容系统机制的理解。 1. 稳定状态下的扩缩容 稳定状态下的工作流程如下: 请求通过 ingress 路由到 public service ,此时 public service 对应的 endpoints 是 revision 对应的 pod Autoscaler 会定期通过 queue-proxy 获取 revision 活跃实例的指标,并不断调整 revision 实例。请求打到系统时, Autoscaler 会根据当前最新的请求指标确定扩缩容比例。 SKS 模式是 serve, 它会监控 private service 的状态,保持 public service 的 endpoints 与 private service 一致 。 2. 缩容到零 缩容到零过程的工作流程如下: AutoScaler 通过 queue-proxy 获取 revision 实例的请求指标 一旦系统中某个 revision 不再接收到请求(此时 Activator 和 queue-proxy 收到的请求数都为 0) AutoScaler 会通过 Decider 确定出当前所需的实例数为 0,通过 PodAutoscaler 修改 revision 对应 Deployment 的 实例数 在系统删掉 revision 最后一个 Pod 之前,会先将 Activator 加到 数据流路径中(请求先到 Activator)。Autoscaler 触发 SKS 变为 proxy 模式,此时 SKS 的 public service 后端的endpoints 变为 Activator 的IP,所有的流量都直接导到 Activator 此时,如果在冷却窗口时间内依然没有流量进来,那么最后一个 Pod 才会真正缩容到零。 3. 冷启动(从零开始扩容) 冷启动过程的工作流程如下: 当 revision 缩容到零之后,此时如果有请求进来,则系统需要扩容。因为 SKS 在 proxy 模式,流量会直接请求到 Activator 。Activator 会统计请求量并将 指标主动上报到 Autoscaler, 同时 Activator 会缓存请求,并 watch SKS 的 private service, 直到 private service 对应的endpoints产生。 Autoscaler 收到 Activator 发送的指标后,会立即启动扩容的逻辑。这个过程的得出的结论是至少一个Pod要被创造出来,AutoScaler 会修改 revision 对应 Deployment 的副本数为为N(N>0),AutoScaler 同时会将 SKS 的状态置为 serve 模式,流量会直接到导到 revision 对应的 pod上。 Activator 最终会监测到 private service 对应的endpoints的产生,并对 endpoints 进行健康检查。健康检查通过后,Activator 会将之前缓存的请求转发到健康的实例上。 最终 revison 完成了冷启动(从零扩容)。]]></content>
<categories>
<category>knative</category>
</categories>
<tags>
<tag>knative</tag>
<tag>autoscalling</tag>
</tags>
</entry>
<entry>
<title><![CDATA[自行配置你的HPA扩缩容速率]]></title>
<url>%2F2020%2F05%2F19%2Fconfighpa%2F</url>
<content type="text"><![CDATA[HPA介绍HPA, Pod 水平自动伸缩(Horizontal Pod Autoscaler)特性, 可以基于CPU利用率自动伸缩 replication controller、deployment和 replica set 中的 pod 数量,(除了 CPU 利用率)也可以 基于其他应程序提供的度量指标custom metrics。 pod 自动缩放不适用于无法缩放的对象,比如 DaemonSets。 Pod 水平自动伸缩特性由 Kubernetes API 资源和控制器实现。资源决定了控制器的行为。 控制器会周期性的获取平均 CPU 利用率,并与目标值相比较后来调整 replication controller 或 deployment 中的副本数量。 官网文档 HPA 工作机制 关于 HPA 的原理可以看下 baxiaoshi的云原生学习笔记 https://www.yuque.com/baxiaoshi/tyado3/yw9deb,本文不做过多介绍,只介绍自行配置hpa特性 HPA 是由 hpacontroller 来实现的, 通过 --horizontal-pod-autoscaler-sync-period 参数 指定周期(默认值为15秒) 一个HPA的例子如下: apiVersion: autoscaling/v2beta2kind: HorizontalPodAutoscalermetadata: name: php-apachespec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: php-apache minReplicas: 1 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 50status: observedGeneration: 1 lastScaleTime: <some-time> currentReplicas: 1 desiredReplicas: 1 currentMetrics: - type: Resource resource: name: cpu current: averageUtilization: 0 averageValue: 0 背景前面已经介绍了 HPA 的相关信息,相信大家都对 HPA 有了简单的了解, 当使用 Horizontal Pod Autoscaler 管理一组副本缩放时, 有可能因为指标动态的变化造成副本数量频繁的变化,有时这被称为 抖动。为了避免这种情况,社区引入了 延迟时间的设置,该配置是针对整个集群的配置,不能对应用粒度进行配置 --horizontal-pod-autoscaler-downscale-stabilization: 这个 kube-controller-manager 的参数表示缩容冷却时间。 即自从上次缩容执行结束后,多久可以再次执行缩容,默认时间是5分钟(5m0s)。` 注意:这个配置是集群级别的,只能配置整个集群的扩缩容速率,用户不能自行配置自己的应用扩缩容速率: 考虑一下场景的应用: 对于大流量的的web应用。需要非常快的扩容速率,缓慢的缩容速率(为了迎接下一个流量高峰) 对于处理数据类的应用。需要尽快的扩容(减少处理数据的时间),并尽快缩容(降低成本) 对于处理常规流量和数据的应用,按照常规的方式配置就可以了 对于上述3种应用场景,1.17以前的集群是不能支持,这也导致了一些未解决的 issue: https://github.com/kubernetes/kubernetes/issues/39090 https://github.com/kubernetes/kubernetes/issues/65097 https://github.com/kubernetes/kubernetes/issues/69428 为此社区 在1.18引入了 能自行配置HPA速率的新特性 见 相关 keps 原理介绍为了自定义扩缩容行为,需要给单个 hpa 对象添加一个 behavior字段,如下: apiVersion: autoscaling/v2beta2kind: HorizontalPodAutoscalermetadata: name: php-apachespec: *** behavior: scaleUp: policies: - type: percent value: 900% scaleDown: policies: - type: pods value: 1 periodSeconds: 600 # (i.e., scale down one pod every 10 min) metrics: *** 其中 behavior 字段 包含 如下对象: scaleUp 配置扩容时的规则。 stabilizationWindowSeconds: -该值表示 HPA 控制器应参考的采样时间,以防止副本数量波动。 selectPolicy:可以是min或max,用于选择 policies 中的最大值还是最小值。默认为 max。 policies: 是一个数组,每个元素有如下字段: type: 可为pods或者percent(以Pod的绝对数量或当前副本的百分比表示)。 periodSeconds 规则应用的时间范围(以秒为单位),即每多少秒扩缩容一次。 value: 规则的值,设为0 表示禁止 扩容或者缩容 scaleDown 与 scaleUp 类似,指定的是缩容的规则。 用户通过控制 HPA中指定参数,来控制HPA扩缩容的逻辑 selectPolicy 字段指示应应用的策略。默认情况下为max,也就是:将使用尽可能扩大副本的最大数量,而选择尽可能减少副本的最小数量。 有点绕没关系,待会看下面的例子 场景场景1:尽可能快的扩容 这种模式适合流量增速比较快的场景 HPA的配置如下: behavior: scaleUp: policies: - type: percent value: 900% 900% 表示可以扩容的数量为当前副本数量的9倍,也就是可以扩大到当前副本数量的10倍,其他值都是默认值 如果应用一个开始的pod数为1,那么扩容过程中 pod数量如下: 1 -> 10 -> 100 -> 1000 scaleDown没有配置,表示该应用缩容将按照常规方式进行 可参考对应的扩缩容算法 场景2:尽可能快的扩容,然后慢慢缩容 这种模式适合不想快速缩容的场景 HPA的配置如下: behavior: scaleUp: policies: - type: percent value: 900% scaleDown: policies: - type: pods value: 1 periodSeconds: 600 # (i.e., 每隔10分钟缩容一个pod) 这种配置扩容场景同 场景1,缩容场景下却是不同的,这里配置的是每隔10分钟缩容一个pod 假如说扩容之后有1000个pod,那么缩容过程中pod数量如下: 1000 -> 1000 -> 1000 -> … (7 more min) -> 999 场景3:慢慢扩容,按常规缩容 这种模式适合不太激进的扩容 HPA的配置如下: behavior: scaleUp: policies: - type: pods value: 1 如果应用一个开始的pod数为1,那么扩容过程中 pod数量如下: 1 -> 2 -> 3 -> 4 同样,scaleDown没有配置,表示该应用缩容将按照常规方式进行 可参考对应的扩缩容算法 场景4: 常规扩容,禁止缩容 这种模式适用于 不允许应用缩容,或者你想单独控制应用的缩容的场景 HPA的配置如下: behavior: scaleDown: policies: - type: pods value: 0 副本数量将按照常规方式扩容,不会发生缩容 场景5:延迟缩容 这种模式适用于 用户并不想立即缩容,而是想等待更大的负载时间到来,再计算缩容情况 behavior: scaleDown: stabilizationWindowSeconds: 600 policies: - type: pods value: 5 这种配置情况下 hpa 缩容策略行为如下: 会采集最近 600s 的(默认300s)的缩容建议,类似于滑动窗口 选择最大的那个 按照不大于每秒5个pod的速率缩容 假如 CurReplicas = 10 , HPA controller 每 1min 处理一次: 前 9 min,算法只会收集扩缩容建议,而不会发生真正的扩缩容,假设 有如下 扩缩容建议: recommendations = [10, 9, 8, 9, 9, 8, 9, 8, 9] 第 10 min,我们增加一个扩缩容建议,比如说是 8 recommendations = [10, 9, 8, 9, 9, 8, 9, 8, 9,8] HPA 算法会取其中最大的一个 10,因此应用不会发生缩容,repicas 的值不变 第 11 min,我们增加一个扩缩容建议,比如 7,维持 600s 的滑动窗口,因此需要把第一个 10 去掉,如下: recommendations = [9, 8, 9, 9, 8, 9, 8, 9, 8, 7] HPA 算法会取最大的一个 9, 应用副本数量变化: 10 -> 9 场景6: 避免错误的扩容 这种模式在数据处理流中比较常见,用户想要根据队列中的数据来扩容,当数据较多时快速扩容。当指标有抖动时并不想扩容 HPA的配置如下: behavior: scaleUp: stabilizationWindowSeconds: 300 policies: - type: pods value: 20 这种配置情况下 hpa 扩容策略行为如下: 会采集最近 300s 的(默认0s)的扩容建议,类似于滑动窗口 选择最小的那个 按照不大于每秒20个pod的速率扩容 假如 CurReplicas = 2 , HPA controller 每 1min 处理一次: 前 5 min,算法只会收集扩缩容建议,而不会发生真正的扩缩容,假设 有如下 扩缩容建议: recommendations = [2, 3, 19, 10, 3] 第 6 min,我们增加一个扩缩容建议,比如说是 4 recommendations = [2, 3, 19, 10, 3, 4] HPA 算法会取其中最小的一个 2,因此应用不会发生缩容,repicas 的值不变 第 11 min,我们增加一个扩缩容建议,比如 7,维持 300s 的滑动窗口,因此需要把第一个 2 去掉,如下: recommendations = [3, 19, 10, 3, 4,7] HPA 算法会取最小的一个 3, 应用副本数量变化: 2 -> 3 算法原理算法的伪代码如下: // HPA controller 中的for循环for { desiredReplicas = AnyAlgorithmInHPAController(...) // 扩容场景 if desiredReplicas > curReplicas { replicas = []int{} for _, policy := range behavior.ScaleUp.Policies { if policy.type == "pods" { replicas = append(replicas, CurReplicas + policy.Value) } else if policy.type == "percent" { replicas = append(replicas, CurReplicas * (1 + policy.Value/100)) } } if behavior.ScaleUp.selectPolicy == "max" { scaleUpLimit = max(replicas) } else { scaleUpLimit = min(replicas) } // 这里的min可以理解为 尽量维持原状,即如果是扩容,尽量取最小的那个 limitedReplicas = min(max, desiredReplicas) } // 缩容场景 if desiredReplicas < curReplicas { for _, policy := range behaviro.scaleDown.Policies { replicas = []int{} if policy.type == "pods" { replicas = append(replicas, CurReplicas - policy.Value) } else if policy.type == "percent" { replicas = append(replicas, CurReplicas * (1 - policy.Value /100)) } if behavior.ScaleDown.SelectPolicy == "max" { scaleDownLimit = min(replicas) } else { scaleDownLimit = max(replicas) } // 这里的max可以理解为 尽量维持原状,即如果是缩容,尽量取最大的那个 limitedReplicas = max(min, desiredReplicas) } } storeRecommend(limitedReplicas, scaleRecommendations) // 选择合适的扩缩容副本 nextReplicas := applyRecommendationIfNeeded(scaleRecommendations) // 扩缩容 setReplicas(nextReplicas) sleep(ControllerSleepTime) } 默认值为了平滑得扩速容,默认值是有必要的behavior 的默认值如下: behavior.scaleDown.stabilizationWindowSeconds = 300, 缩容情况下默认等待 5 min中再缩容. behavior.scaleUp.stabilizationWindowSeconds = 0, 扩容时立即扩容,不等待 behavior.scaleUp.policies 默认值如下: 百分比策略 policy = percent periodSeconds = 60, 扩容间隔为 1min value = 100 每次最多扩容翻倍 Pod个数策略 policy = pods periodSeconds = 60, 扩容间隔为 1min value = 4 每次最多扩容4个 behavior.scaleDown.policies 默认值如下: 百分比策略 policy = percent periodSeconds = 60 缩容间隔为 1min value = 100 一次缩容最多可以把所有的示例都干掉]]></content>
<categories>
<category>k8s</category>
</categories>
<tags>
<tag>k8s</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Go 内存管理]]></title>
<url>%2F2019%2F11%2F12%2Fneicunguanliyufenpei%2F</url>
<content type="text"><![CDATA[Go这门语言抛弃了C/C++中的开发者管理内存的方式:主动申请与主动释放,增加了逃逸分析和GC,将开发者从内存管理中释放出来,让开发者有更多的精力去关注软件设计,而不是底层的内存问题。这是Go语言成为高生产力语言的原因之一 引自【 Go内存分配那些事,就这么简单!】 堆内存的分配先看下面这段代码,思考下 smallStruct 会被分配在堆上还是栈上: package maintype smallStruct struct { a, b int64 c, d float64}func main() { smallAllocation()}//go:noinlinefunc smallAllocation() *smallStruct { return &smallStruct{}} 通过 annotation //go:noinline 禁用内联函数,不然这里不会产生堆内存的分配 【逃逸分析】 Inline 内联: 是在编译期间发生的,将函数调用调用处替换为被调用函数主体的一种编译器优化手段。 将文件保存为 main.go, 并执行 go tool compile "-m" main.go ,查看Go 堆内存的分配 【逃逸分析】的过程 如果不加 annotation //go:noinline 可以用go build -gcflags '-m -l' main.go -l可以禁止内联函数,效果是一样的,下面是逃逸分析的结果: main.go:14:9: &smallStruct literal escapes to heap 再来看这段代码生成的汇编指令来详细的展示内存分配的过程, 执行下面 go tool compile -S main.go 0x001d 00029 (main.go:14) LEAQ type."".smallStruct(SB), AX0x0024 00036 (main.go:14) PCDATA $0, $00x0024 00036 (main.go:14) MOVQ AX, (SP)0x0028 00040 (main.go:14) CALL runtime.newobject(SB) runtime.newobject 是 Go 内置的申请堆内存的函数,对于堆内存的分配,Go 中有两种策略: 大内存的分配和小内存的分配 小内存的分配从 P 的 mcache 中分配对于小于 32kb的小内存,Go 会尝试在 P 的 本地缓存 mcache 中分配, mcache 保存的是各种大小的Span,并按Span class分类,小对象直接从mcache分配内存,它起到了缓存的作用,并且可以 无锁访问 每个 M 绑定一个 P 来运行一个goroutine, 在分配内存时,当前的 goroutine 在当前 P 的本地缓存 mcache 中查找对应的span, 从 span list 中来查找第一个可用的空闲 span span class 分为 8 bytes ~ 32k bytes 共 66 种类型(还有个大小为0的 size class0,并未用到,用于大对象的堆内存分配),分别对应不同的内存大小,mspan里保存对应大小的object, 1个 size class 对应2个 span class,2个 span class 的 span 大小相同,只是功能不同,1个用来存放包含指针的对象,一个用来存放不包含指针的对象,不包含指针对象的 Span 就无需 GC 扫描了。 前面的例子里,struct的大小为 (64bit/8)*4=8bit*4=32b 所以会在 span class 大小为 32bytes 的 mspan 里分配 从全局的缓存 mcentral 中分配当 mcache中没有空闲的 span 时怎么办呢,Go 还维护了一个全局的缓存 mcentral, mcentral 和 mcache 一样,都134个 span class 级别(67个 size class ),但每个级别都保存了2个span list,即2个span链表: nonempty:这个链表里的span,所有span都至少有1个空闲的对象空间。这些span是mcache释放span时加入到该链表的。 empty:这个链表里的span,所有的span都不确定里面是否有空闲的对象空间。当一个span交给mcache的时候,就会加入到empty链表。 mcache从mcentral获取和归还mspan的流程:引自【 图解Go语言内存分配|码农桃花源】 获取:加锁;从 nonempty 链表找到一个可用的 mspan ;并将其从 nonempty 链表删除;将取出的 mspan 加入到 empty 链表;将 mspan 返回给工作线程;解锁。 归还:加锁;将 mspan 从 empty 链表删除;将 mspan 加入到 nonempty 链表;解锁。 另外,GC 扫描的时候会把部分 mspan 标记为未使用,并将对应的 mspan 加入到 nonempty list 中 mcache 从 mcentral获取 过程如下: mcentral 从 heap 中分配当 mcentral 中的 nonempty list 没有可分配的对象的时候,Go会从 mheap 中分配对象,并链接到 nonempty list 上, mheap 必要时会向系统申请内存 mheap 中还有 arenas ,主要是为了大块内存需要,arena 也是用 mspan 组织的 大内存的分配大内存的分配就比较简单了,大于 32kb 的内存都会在 mheap 中直接分配 总结go 内存分配的概览 参考文章 Go内存分配那些事,就这么简单!:https://lessisbetter.site/2019/07/06/go-memory-allocation 图解Go语言内存分配|码农桃花源:https://qcrao.com/2019/03/13/graphic-go-memory-allocation 聊一聊goroutine stack:https://zhuanlan.zhihu.com/p/28409657]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
<tag>内存管理</tag>
</tags>
</entry>
<entry>
<title><![CDATA[谈谈 epoll]]></title>
<url>%2F2019%2F05%2F10%2Fepoll%2F</url>
<content type="text"><![CDATA[epollIO 多路复用目前支持I/O多路复用的系统调用有 select,pselect,poll,epoll ,I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作 select调用后select函数会阻塞,直到有描述符就绪(有数据可读、可写),或者超时,函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。 select的流程 假如程序同时监视如下图的sock1、sock2和sock3三个socket,那么在调用select之后,操作系统把进程A分别加入这三个socket的等待队列中 当任何一个socket收到数据后,中断程序将唤起进程,将进程从所有fd(socket)的等待队列中移除,再将进程加入到工作队列里面 进程A被唤醒后,它知道至少有一个socket接收了数据。程序需遍历一遍socket列表,可以得到就绪的socket 缺点: 其一,每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。 其二,进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。 poll与select一样,只是去掉了 1024的限制epollepoll 事先通过 epoll_ctl() 来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个文件描述符,当进程调用 epoll_wait() 时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。) epoll使用一个文件描述符(eventpoll)管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次 int s = socket(AF_INET, SOCK_STREAM, 0); bind(s, ...)listen(s, ...)int epfd = epoll_create(...);epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中while(1){ int n = epoll_wait(...) for(接收到数据的socket){ //处理 }} 流程:首先创建 epoll对象创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket 假设计算机中正在运行进程A和进程B,在某时刻进程A运行到了epoll_wait语句。如下图所示,内核会将进程A放入eventpoll的等待队列中,阻塞进程 当socket接收到数据,中断程序一方面修改rdlist,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态(如下图)。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。 参考文章 https://www.jianshu.com/p/dfd940e7fca2 2 罗培羽:如果这篇文章说不清epoll的本质,那就过来掐死我吧!(1) 3 罗培羽:如果这篇文章说不清epoll的本质,那就过来掐死我吧!(2) 4 罗培羽:如果这篇文章说不清epoll的本质,那就过来掐死我吧!(3)]]></content>
<tags>
<tag>linux</tag>
<tag>epoll</tag>
</tags>
</entry>
<entry>
<title><![CDATA[浅谈 Go map]]></title>
<url>%2F2019%2F04%2F02%2Fgomap%2F</url>
<content type="text"><![CDATA[map原理分析map 结构体type hmap struct { count int // 元素的个数 flags uint8 // 状态标志 B uint8 // 可以最多容纳 6.5 * 2 ^ B 个元素,6.5为装载因子 noverflow uint16 // 溢出的个数 hash0 uint32 // 哈希种子 buckets unsafe.Pointer // 桶的地址 oldbuckets unsafe.Pointer // 旧桶的地址,用于扩容 nevacuate uintptr // 搬迁进度,小于nevacuate的已经搬迁 overflow *[2]*[]*bmap }// A bucket for a Go map.type bmap struct { // 每个元素hash值的高8位,如果tophash[0] < minTopHash,表示这个桶的搬迁状态 tophash [bucketCnt]uint8 // bucketCnt是常量8,接下来是8个key、8个value,但是我们不能直接看到;为了优化对齐,go采用了key放在一起,value放在一起的存储方式,8个k,8个v得内存地址 // 再接下来是hash冲突发生时,下一个溢出桶的地址} bmap不只tophash还有两个方法 overflow 和setoverflow func (b *bmap) overflow(t *maptype) *bmap { return *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize))}func (b *bmap) setoverflow(t *maptype, ovf *bmap) { *(**bmap)(add(unsafe.Pointer(b), uintptr(t.bucketsize)-sys.PtrSize)) = ovf} hmap中的buckets中的原色bucket就是bmap,即 buckets[0],bucket[1],… bucket[2^B-1]如下图 bucket就是bmap bmap 是存放 k-v 的地方,我们把视角拉近,仔细看 bmap 的内部组成。 key 经过哈希计算后得到哈希值,共 64 个 bit 位(64位机,32位机就不讨论了,现在主流都是64位机),计算它到底要落在哪个桶时,只会用到最后 B 个 bit 位。还记得前面提到过的 B 吗?如果 B = 5,那么桶的数量,也就是 buckets 数组的长度是 2^5 = 32 例如,现在有一个 key 经过哈希函数计算后,得到的哈希结果是: 10010111 | 000011110110110010001111001010100010010110010101010 │ 01010 用最后的 5 个 bit 位,也就是 01010,值为 10,也就是 10 号桶。这个操作实际上就是取余操作,但是取余开销太大,所以代码实现上用的位操作代替。 再用哈希值的高 8 位,找到此 key 在 bucket 中的位置,这是在寻找已有的 key。最开始桶内还没有 key,新加入的 key 会找到第一个空位,放入。 buckets 编号就是桶编号,当两个不同的 key 落在同一个桶中,也就是发生了哈希冲突。冲突的解决手段是用链表法:在 bucket 中,从前往后找到第一个空位。这样,在查找某个 key 时,先找到对应的桶,再去遍历 bucket 中的 key hash冲突的两种表示方式: 开放寻址法(hash冲突时,在当前index往后查找第一个空的位置即可) 拉链法 map在写入过程会发生扩容,runtime.mapassign 函数会在以下两种情况发生时触发哈希的扩容: 装载因子已经超过 6.5;装载因子=总数量/桶的数量 哈希使用了太多溢出桶;溢出捅的数量 超过正常桶的数量 即 noverflow 大于 1<<B buckets 每次都会将桶的数量翻倍 扩容机制: 翻倍扩容:哈希在存储元素过多时状态会触发扩容操作,每次都会将桶的数量翻倍,整个扩容过程并不是原子的,而是通过 runtime.growWork 增量触发的,在扩容期间访问哈希表时会使用旧桶,向哈希表写入数据时会触发旧桶元素的分流; 等量扩容,为了解决大量写入、删除造成的内存泄漏问题,哈希引入了 sameSizeGrow这一机制,在出现较多溢出桶时会对哈希进行『内存整理』减少对空间的占用。 参考链接 https://www.jianshu.com/p/aa0d4808cbb8 https://segmentfault.com/a/1190000018387055]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Go 学习笔记]]></title>
<url>%2F2019%2F01%2F05%2Fgo%20shen%20ru%20fen%20xi%2F</url>
<content type="text"><![CDATA[Go 学习笔记go程序是如何运行的参考链接1 defer 源码分析参考链接 defer、return、返回值三者的执行逻辑应该是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出 逃逸分析 堆栈分配参考链接 go build -gcflags '-m -l' xxx.go 就可以看到逃逸分析的过程和结果 go性能大杀器 pprof参考链接1参考链接2 ### pprof中自带 web 火焰图,需要安装graphvizgo tool pprof -http=:8181 xxx,pprof 下面的语句 可以结合代码查看哪个函数用时最多go tool pprof main.go xxxx.prof 进入pprof后执行 list <函数名> 对于web开放的pprof (在http的go程序中 添加 _ "net/http/pprof"的import,会增加 debug/pprof 的endpoint),结束后将默认进入 pprof 的交互式命令模式go tool pprof http://localhost:6060/debug/pprof/profile?seconds=60go tool pprof http://localhost:6060/debug/pprof/heap go性能大杀器 trace同pprof 对于web开放的pprof (在http的go程序中 添加 _ "net/http/pprof"的import curl http://127.0.0.1:6060/debug/pprof/trace\?seconds\=20 > trace.outgo tool trace trace.out # 此处和pprof不同,不用加 -http=:8181 这里他会自动选择端口 对于后台应用,后台程序main启动时添加 trace.Start(os.Stderr)直接运行下面的命令即可 go run main.go 2> trace.out 它能够跟踪捕获各种执行中的事件,例如 Goroutine 的创建/阻塞/解除阻塞,Syscall 的进入/退出/阻止,GC 事件,Heap 的大小改变,Processor 启动/停止等等 interface 不含有任何方法的 interface type eface struct { // 16 bytes _type *_type data unsafe.Pointer} 含有 方法的 interface type iface struct { // 16 bytes tab *itab data unsafe.Pointer} 变量类型 结构体实现接口 结构体指针实现接口 结构体初始化变量 通过 不通过 结构体指针初始化变量 通过 通过 不通过的如下 type Duck interface { Quack()}type Cat struct{}func (c *Cat) Quack() { fmt.Println("meow")}func main() { var c Duck = Cat{} // 将结构体变量传到指针类型接受的函数是不行的,反过来可行 c.Quack()}$ go build interface.go./interface.go:20:6: cannot use Cat literal (type Cat) as type Duck in assignment: Cat does not implement Duck (Quack method has pointer receiver) Go中函数调用都是值拷贝,使用 c.Quack() 调用方法时都会发生值拷贝: 对于 &Cat{} 来说,这意味着拷贝一个新的 &Cat{} 指针,这个指针与原来的指针指向一个相同并且唯一的结构体,所以编译器可以隐式的对变量解引用(dereference)获取指针指向的结构体; 对于 Cat{} 来说,这意味着 Quack 方法会接受一个全新的 Cat{},因为方法的参数是*Cat,编译器不会无中生有创建一个新的指针;即使编译器可以创建新指针,这个指针指向的也不是最初调用该方法的结构体; panicpanic只会调用当前Goroutine的defer() func main() { defer println("in main") go func() { defer println("in goroutine") panic("") }() time.Sleep(1 * time.Second)}$ go run main.goin goroutinepanic: make newnew 返回的是指针,指向一个type类型内存空间的指针new等价于 var a typeA // tpyeA的零值b:=&a 但是 new不能对 chanel map slice进行初始化 ,这几个必须经过make进行结构体的初始化才能用]]></content>
<categories>
<category>Go</category>
</categories>
<tags>
<tag>linux</tag>
<tag>epoll</tag>
<tag>Go</tag>
</tags>
</entry>
<entry>
<title><![CDATA[kubernetes&容器网络(2)之iptables]]></title>
<url>%2F2018%2F12%2F05%2Fk8s-iptables%2F</url>
<content type="text"><![CDATA[iptables规则 参考 http://www.zsythink.net/archives/tag/iptables/ 用户空间,例如从pod中流出的流量就是从ouput链流出 上图表示的iptables的链,链 和表的关系如下,以PREROUTING链为例 这幅图是什么意思呢?它的意思是说,prerouting”链”只拥有nat表、raw表和mangle表所对应的功能,所以,prerouting中的规则只能存放于nat表、raw表和mangle表中。 NATNAT的三种类型: SNAT iptables -t nat -A POSTROUTING -s 10.8.0.0/255.255.255.0 -o eth0 -j SNAT --to-source192.168.5.3# 目标流向eth0,源地址是xxx的,做SNAT,源地址改为xxx DNAT iptables-t nat -A POSTROUTING -s 10.8.0.0/255.255.255.0 -o eth0 -j SNAT --to-source192.168.5.3-192.168.5.5 MASQUERADE 是SNAT的一种,可以自动获取网卡的ip来做SNAT,如果是ADSL这种动态ip的,如果用SNAT需要经常更改iptables规则 iptables-t nat -A POSTROUTING -s 10.8.0.0/255.255.255.0 -o eth0 -j MASQUERADE# 源地址是xxx,流向eth0的,流向做自动化SNAT masquerade 应为英文伪装 iptabels 常用命令iptables [-t 表名] 管理选项 [链名] [匹配条件] [-j 控制类型]# 控制类型包括 ACCETP REJECT DROP LOG 还有自定义的链(k8s的链)等iptabels -t nat(表名) -nvL POSTROUTING(链的名字) https://www.jianshu.com/p/ee4ee15d3658 分析k8s下的iptables规则以如下 service 为例 Name: testapi-smzdm-comNamespace: zhongce-v2-0Labels: <none>Selector: zdm-app-owner=testapi-smzdm-comType: LoadBalancerIP: 172.17.185.22LoadBalancer Ingress: 10.42.162.216Port: <unset> 809/TCPTargetPort: 809/TCPNodePort: <unset> 39746/TCPEndpoints: 10.42.147.255:809,10.42.38.222:809Session Affinity: NoneExternal Traffic Policy: ClusterEvents: <none> 即 cluster ip 为 172.17.185.22 后端 podip 为 10.42.147.25510.42.38.222此外还有1个 loadbalancer ip 10.42.162.216 svc的访问路径- 集群内部,通过 `clusterip` 到访问到后端 `pod - 集群外部,通过直接访问`nodeport`;或者通过 `elb` 负载均衡到 `node` 上再通过 `nodeport` 访问 cluster ip 的基本原理如果是集群内的应用访问 cluster ip,那就是从用户空间访问内核空间网络协议栈,走的是 OUTPUT 链 从OUTPUT 链开始 [root@10-42-8-102 ~]# iptables -t nat -nvL OUTPUTChain OUTPUT (policy ACCEPT 4 packets, 240 bytes) pkts bytes target prot opt in out source destination 3424K 209M KUBE-SERVICES all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service portals */ OUTPUT下的规则 直接把流量交给 KUBE-SERVICES 链 [root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-SERVICESChain KUBE-SERVICES (2 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ tcp -- * * 0.0.0.0/0 172.17.185.22 /* zhongce-v2-0/testapi-smzdm-com: cluster IP */ tcp dpt:809 0 0 KUBE-SVC-G3OM5DSD2HHDMN6U tcp -- * * 0.0.0.0/0 172.17.185.22 /* zhongce-v2-0/testapi-smzdm-com: cluster IP */ tcp dpt:809 10 520 KUBE-FW-G3OM5DSD2HHDMN6U tcp -- * * 0.0.0.0/0 10.42.162.216 /* zhongce-v2-0/testapi-smzdm-com: loadbalancer IP */ tcp dpt:809 0 0 KUBE-NODEPORTS all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTY 上述3条规则是顺序执行的: 第1条规则匹配发往 Cluster IP 172.17.185.22 的流量,跳转到了 KUBE-MARK-MASQ 链进一步处理,其作用就是打了一个 MARK ,稍后展开说明。 第2条规则匹配发往 Cluster IP 172.17.185.22 的流量,跳转到了 KUBE-SVC-G3OM5DSD2HHDMN6U 链进一步处理,稍后展开说明。 第3条规则匹配发往集群外 LB IP 的 10.42.162.216 的流量,跳转到了KUBE-FW-G3OM5DSD2HHDMN6U 链进一步处理,稍后展开说明。 第4条 KUBE-NODEPORTS的规则在末尾,只要dst ip是node 本机ip的话 ((–dst-type LOCAL),就跳转到KUBE-NODEPORTS做进一步判定:) 第2条规则要做dnat转发到后端具体的后端pod上 [root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-SVC-G3OM5DSD2HHDMN6UChain KUBE-SVC-G3OM5DSD2HHDMN6U (3 references) pkts bytes target prot opt in out source destination 18 936 KUBE-SEP-JT2KW6YUTVPLLGV6 all -- * * 0.0.0.0/0 0.0.0.0/0 statistic mode random probability 0.50000000000 21 1092 KUBE-SEP-VETLC6CJY2HOK3EL all -- * * 0.0.0.0/0 0.0.0.0/0 两条 对应 后端pod的链 [root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-SEP-JT2KW6YUTVPLLGV6Chain KUBE-SEP-JT2KW6YUTVPLLGV6 (1 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ all -- * * 10.42.147.255 0.0.0.0/0 26 1352 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp to:10.42.147.255:809[root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-SEP-VETLC6CJY2HOK3ELChain KUBE-SEP-VETLC6CJY2HOK3EL (1 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ all -- * * 10.42.38.222 0.0.0.0/0 2 104 DNAT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp to:10.42.38.222:809 流量经过路由表从eth0出去,在流量流出本机之前会经过POSTROUTING 链 在流量离开本机的时候会经过 POSTROUTING 链[root@10-42-8-102 ~]# iptables -t nat -nvL POSTROUTINGChain POSTROUTING (policy ACCEPT 274 packets, 17340 bytes) pkts bytes target prot opt in out source destination 632M 36G KUBE-POSTROUTING all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes postrouting rules */[root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-POSTROUTINGChain KUBE-POSTROUTING (1 references) pkts bytes target prot opt in out source destination 526 27352 MASQUERADE all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service traffic requiring SNAT */ mark match 0x4000/0x4000 其实直接就跳转到了 KUBE-POSTROUTING,然后匹配打过0x4000 MARK 的流量,将其做 SNAT 转换,而这个 MARK 其实就是之前没说的 KUBE-MARK-MASQ 做的事情 [root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-MARK-MASQChain KUBE-MARK-MASQ (183 references) pkts bytes target prot opt in out source destination 492 25604 MARK all -- * * 0.0.0.0/0 0.0.0.0/0 MARK or 0x4000 当流量离开本机时,src IP会被修改为node的IP,而不是发出流量的POD IP了 通过loadbalance ip进行访问最后还有一个KUBE-FW-G3OM5DSD2HHDMN6U链没有讲,从本机发往LB IP的流量要做啥事情呢? 其实也是让流量直接发往具体某个Endpoints,就别真的发往LB了,这样才能获得最佳的延迟:[root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-FW-G3OM5DSD2HHDMN6UChain KUBE-FW-G3OM5DSD2HHDMN6U (1 references) pkts bytes target prot opt in out source destination 2 104 KUBE-MARK-MASQ all -- * * 0.0.0.0/0 0.0.0.0/0 /* zhongce-v2-0/testapi-smzdm-com: loadbalancer IP */ 2 104 KUBE-SVC-G3OM5DSD2HHDMN6U all -- * * 0.0.0.0/0 0.0.0.0/0 /* zhongce-v2-0/testapi-smzdm-com: loadbalancer IP */ 0 0 KUBE-MARK-DROP all -- * * 0.0.0.0/0 0.0.0.0/0 /* zhongce-v2-0/testapi-smzdm-com: loadbalancer IP */ 通过nodeport 来访问回顾一下 KUBE_SERVICES规则 [root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-SERVICESChain KUBE-SERVICES (2 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ tcp -- * * !172.17.0.0/16 172.17.185.22 /* zhongce-v2-0/testapi-smzdm-com: cluster IP */ tcp dpt:809 0 0 KUBE-SVC-G3OM5DSD2HHDMN6U tcp -- * * 0.0.0.0/0 172.17.185.22 /* zhongce-v2-0/testapi-smzdm-com: cluster IP */ tcp dpt:809 10 520 KUBE-FW-G3OM5DSD2HHDMN6U tcp -- * * 0.0.0.0/0 10.42.162.216 /* zhongce-v2-0/testapi-smzdm-com: loadbalancer IP */ tcp dpt:809 0 0 KUBE-NODEPORTS all -- * * 0.0.0.0/0 0.0.0.0/0 /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL KUBE-NODEPORTS 是最后一条规则 [root@10-42-8-102 ~]# iptables -t nat -nvL KUBE-NODEPORTSChain KUBE-NODEPORTS (1 references) pkts bytes target prot opt in out source destination 0 0 KUBE-MARK-MASQ tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* zhongce-v2-0/testapi-smzdm-com: */ tcp dpt:39746 0 0 KUBE-SVC-G3OM5DSD2HHDMN6U tcp -- * * 0.0.0.0/0 0.0.0.0/0 /* zhongce-v2-0/testapi-smzdm-com: */ tcp dpt:39746 第1条匹配dst port如果是39746,那么就打mark。第2条匹配dst port如果是39746,那么就跳到负载均衡链做DNAT改写。 总结 KUBE-SERVICES 链的规则存在于 OUTPUT POSTROUTING PREROUTING 三个链上 对于 KUBE-SERVICES KUBE-NDOEPORTS-xxx KUBE-SEP-xxx 下都会对符合条件(匹配条件)的规则打上MARK 可以重复打MARK 在流量出node的时候做SNAT 从集群内出去的流量怎么回来出node的流量在做SNAT的时候,netfilter有个连接跟踪机制,保存在 conntrack记录中 这就是Netfilter的连接跟踪(conntrack)功能了。对于TCP协议来讲,肯定是上来先建立一个连接,可以用源/目的IP+源/目的端口 (四元组),唯一标识一条连接,这个连接会放在conntrack表里面。当时是这台机器去请求163网站的,虽然源地址已经Snat成公网IP地址了,但是 conntrack 表里面还是有这个连接的记录的。当163网站返回数据的时候,会找到记录,从而找到正确的私网IP地址。 参考文档k8s 的iptales规则详解]]></content>
<categories>
<category>k8s</category>
</categories>
<tags>
<tag>k8s</tag>
<tag>iptables</tag>
</tags>
</entry>
<entry>
<title><![CDATA[kubernetes&容器网络(1)之seivice]]></title>
<url>%2F2018%2F11%2F28%2Fk8s-svc%2F</url>
<content type="text"><![CDATA[1. Service介绍Kubernetes 中有很多概念,例如 ReplicationController、Service、Pod等。我认为 Service 是 Kubernetes 中最重要的概念,没有之一。 为什么 Service 如此重要?因为它解耦了前端用户和后端真正提供服务的 Pods 。在进一步理解 Service 之前,我们先简单地了解下 Pod , Pod 也是 Kubernetes 中很重要的概念之一。 在 Kubernetes 中,Pod 是能够创建、调度、和管理的最小部署单元,而不是单独的应用容器。Pod 是容器组,一个Pod中容器运行在一个共享的应用上下文中。这里的共享上下文是为多个 Linux Namespace 的联合。例如: PID命名空间(在同一个 Pod 中的应用可以看到其它应用的进程) Network名字空间(在同一个 Pod 中的应用可以访问同样的IP和端口空间) IPC命名空间(在同一个 Pod 中的应用可以使用SystemV IPC或者POSIX消息队列进行通信) UTS命名空间(在同一个 Pod 中的应用可以共享一个主机名称) Pod 是一个和应用相关的“逻辑主机”, Pod 中的容器共享一个网络名字空间。 Pod 为它的组件之间的数据共享、通信和管理提供了便利。 我们可以看出, Pod 在资源层面抽象了容器。 因此,在Kubernetes中,让我们暂时先忘记容器,记住 Pod 。 Kubernetes对Service的定义是:Service is an abstraction which defines a logical set of Pods and a policy by which to access them。我们下面理解下这句话。 刚开始的时候,生活其实是很简单的。一个提供特定服务的进程,运行在一个容器中,监听容器的IP地址和端口号。客户端通过<Container IP>:<ContainerPort>,或者通过使用Docker的端口映射<Host IP>:<Host Port>就可以访问到这个服务了。The simplest, the best,简单的生活很美好。 但是美好的日子总是短暂的。小伙伴们太热情了,单独由一个容器提供服务不够用了,怎么办?很简单啊,由多个容器提供服务不就可以了吗。问题似乎得到了解决。 可是那么多的容器,客户端到底访问哪个容器中提供的服务呢?访问容器的请求不均衡怎么办?假如容器所在的主机故障了,容器在另外一台主机上拉起了,这个时候容器的IP地址变了,客户端怎么维护这个容器列表呢?所以,由多个容器提供服务的情况下,一般有两种做法: 客户端自己维护提供服务的容器列表,自己做负载均衡,某个容器故障时自己做故障转移;提供一个负载均衡器,解耦用户和后端提供服务的容器。负载均衡器负责向后端容器转发流量,并对后端容器进行健康检查。客户端只要访问负载均衡器的IP和端口号就行了。我们在前面说Service解耦了前端用户和后端真正提供服务的 Pod s。从这个意义上讲,Service就是Kubernetes中 Pod 的负载均衡器。 从生命周期来说, Pod 是短暂的而不是长久的应用。 Pod 被调度到节点,保持在这个节点上直到被销毁。当节点死亡时,分配到这个节点的 Pod 将会被删掉。 但Service在其生命周期内,IP地址是稳定的。对于Kubernetes原生的应用,Kubernetes提供了一个Endpoints的对象,这个Endpoints的名字和Service的名字相同,它是一个:的列表,负责维护Service后端的Pods的变化。 总结一下,Service解耦了前端用户和后端真正提供服务的Pods,Pod在资源层面抽象了容器。由于它们的存在,使得这个简单的世界变得复杂了。 对了,Service怎么知道是哪些后端的Pods在真正提供自己定义的服务呢?在创建Pods的时候,会定义一些label;在创建Service的时候,会定义Label Selector,Kubernetes就是通过Label Selector来匹配后端真正服务于Service的后端Pods的。 2. 定义一个Service接下来就有点没意思了,我要开始翻译上面说的很重要的Services in Kubernetes了。当然,我会加入自己的理解。 Service也是Kubernetes中的一个REST对象。可以通过向apiserver发送POST请求进行创建。当然,Kubernetes为我们提供了一个强大和好用的客户端kubectl,代替我们向apiserver发送请求。但是kubectl不仅仅是一个简单的apiserver客户端,它为我们提供了很多额外的功能,例如rolling update等。 我们可以创建一个yaml或者json格式的Service规范文件,然后通过kubectl create -f <service spec file>创建之。一个Service的例子如下: { "kind": "Service", "apiVersion": "v1", "metadata": { "name": "my-service" }, "spec": { "selector": { "app": "MyApp" }, "ports": [ { "protocol": "TCP", "port": 80, "targetPort": 9376 } ] }} 以上规范创建了一个名字为my-service的Service对象,它指向任何有app=MyApp标签的、监听TCP端口9376的任何Pods。 Kubernetes会自动地创建一个和Service名字相同的Endpoints对象。Service的selector会被持续地评估哪些Pods属于这个Service,结果会被更新到相应的Endpoints对象。 当然,你也可以在定义Service的时候为其指定一个IP地址(ClusterIP,必须在kube-apiserver的--service-cluster-ip-range参数定义内,且不能冲突)。 Service会把到:的流量转发到targetPort。缺省情况下targetPort等于port的值。一个有意思的情况是,这里你可以定义targetPort为一个字符串(我们可以看到targetPort的类型为IntOrString),这意味着在后端每个Pods中实际监听的端口值可能不一样,这极大地提高了部署Service的灵活性。 Kubernetes的Service支持TCP和UDP协议,缺省是TCP。 3. Service发布服务的方式Service有三种类型:ClusterIP,NodePort和LoadBalancer。 3.1 ClusterIP关于ClusterIP: 通过Service的spec.type: ClusterIP指定;使用cluster-internal ip,即kube-apiserver的--service-cluster-ip-range参数定义的IP范围中的IP地址;缺省方式;只能从集群内访问,访问方式:<ClusterIP>:<Port>;kube-proxy会为每个Service,打开一个本地随机端口,通过iptables规则把到Service的流量trap到这个随机端口,由kube-proxy或者iptables接收,进而转发到后端Pods。 3.2 NodePort关于NodePort: 通过Service的spec.type: NodePort指定;包含ClusterIP功能;在集群的每个节点上为Service开放一个端口(默认是30000-32767,由kube-apiserver的--service-node-port-range参数定义的节点端口范围);可从集群外部通过<NodeIP>:<NodePort>访问;集群中的每个节点上,都会监听NodePort端口,因此,可以通过访问集群中的任意一个节点访问到服务。 3.3 LoadBalancer关于LoadBalancer: 通过Service的spec.type: LoadBalancer指定;包含NodePort功能;通过Cloud Provider(例如GCE)提供的外部LoadBalancer访问服务,即<LoadBalancerIP>:<Port>;Service通过集群的每个节点上的<NodeIP>:<NodePort>向外暴露;有的cloudprovider支持直接从LoadBalancer转发流量到后端Pods(例如GCE),更多的是转发流量到集群节点(例如AWS,还有HWS);你可以在Service定义中指定loadBalancerIP,但这需要cloudprovider的支持,如果不支持则忽略。真正的IP在status.loadBalancer.ingress.ip中。一个例子如下: { "kind": "Service", "apiVersion": "v1", "metadata": { "name": "my-service" }, "spec": { "selector": { "app": "MyApp" }, "ports": [ { "protocol": "TCP", "port": 80, "targetPort": 9376, "nodePort": 30061 } ], "clusterIP": "10.0.171.239", "loadBalancerIP": "78.11.24.19", "type": "LoadBalancer" }, "status": { "loadBalancer": { "ingress": [ { "ip": "146.148.47.155" } ] } }} 目前对接ELB的实现几大厂商,比如 HW 是ELB转发流量到集群节点,后面再由kube-proxy或者iptables转发到后端的Pods。 3.4 External IPsExternal IPs 不是一种Service类型,它不由Kubernetes管理,但是我们也可以通过它暴露服务。数据包通过<External IP>:<Port>到达集群,然后被路由到Service的Endpoints。一个例子如下: { "kind": "Service", "apiVersion": "v1", "metadata": { "name": "my-service" }, "spec": { "selector": { "app": "MyApp" }, "ports": [ { "name": "http", "protocol": "TCP", "port": 80, "targetPort": 9376 } ], "externalIPs" : [ "80.11.12.10" ] }} 4. 几种特殊的Service4.1 没有selector的Service上面说Kubernetes的Service抽象了到Kubernetes的Pods的访问。但是它也能抽象到其它类型的后端的访问。举几个场景: 你想接入一个外部的数据库服务;你想把一个服务指向另外一个Namespace或者集群的服务;你把部分负载迁移到Kubernetes,而另外一部分后端服务运行在Kubernetes之外。在这几种情况下,你可以定义一个没有selector的服务。如下: { "kind": "Service", "apiVersion": "v1", "metadata": { "name": "my-service" }, "spec": { "ports": [ { "protocol": "TCP", "port": 80, "targetPort": 9376 } ] }} 因为没有selector,Kubernetes不会自己创建Endpoints对象,你需要自己手动创建一个Endpoints对象,把Service映射到后端指定的Endpoints上。 { "kind": "Endpoints", "apiVersion": "v1", "metadata": { "name": "my-service" }, "subsets": [ { "addresses": [ { "IP": "1.2.3.4" } ], "ports": [ { "port": 9376 } ] } ]} 注意:Endpoint IP不能是loopback(127.0.0.1)地址、link-local(169.254.0.0/16)和link-local multicast(224.0.0.0/24)地址。 看到这里,我们似乎明白了,这不就是Kubernetes提供的外部服务接入的方式吗?和CloudFoundry的ServiceBroker的功能类似。 4.2 多端口(multi-port)的ServiceKubernetes的Service还支持多端口,比如同时暴露80和443端口。在这种情况下,你必须为每个端口定义一个名字以示区分。一个例子如下: { "kind": "Service", "apiVersion": "v1", "metadata": { "name": "my-service" }, "spec": { "selector": { "app": "MyApp" }, "ports": [ { "name": "http", "protocol": "TCP", "port": 80, "targetPort": 9376 }, { "name": "https", "protocol": "TCP", "port": 443, "targetPort": 9377 } ] }} 注意: 多端口必须指定ports.name以示区分,端口名称不能一样;如果是spec.type: NodePort,则每个端口的NodePort必须不一样,否则Kubernetes不知道一个NodePort对应的是后端哪个targetPort;协议protocol和port可以一样。 4.3 Headless services有时候你不想或者不需要Kubernetes为你的服务做负载均衡,以及一个Service的IP地址。在这种情况下,你可以创建一个headless的Service,通过指定spec.clusterIP: None。 对这类Service,不会分配ClusterIP。对这类Service的DNS查询会返回一堆A记录,即后端Pods的IP地址。另外,kube-proxy不会处理这类Service,Kubernetes不会对这类Service做负载均衡或者代理。但是Endpoints Controller还是会为此类Service创建Endpoints对象。 这允许开发者减少和kubernetes的耦合性,允许他们自己做服务发现等。我在最后讨论的一些基于Kubernetes的容器服务,除了彻底不使用Service的概念外,也可以创建这类headless的Service,自己直接通过LoadBalancer把流量转发(负载均衡和代理)到后端Pods。 5. Service的流量转发模式5.1 Proxy-mode: userspaceuserspace的代理模式是指由用户态的kube-proxy转发流量到后端Pods。如下图所示。 关于userspace: Kube-proxy通过apiserver监控(watch)Service和Endpoints的变化;Kube-proxy安装iptables规则;Kube-proxy把访问Service的流量转发到后端真正的Pods上(Round-Robin);Kubernetes v1.0只支持这种转发方式;通过设置service.spec.sessionAffinity: ClientIP支持基于ClientIP的会话亲和性。 5.2 proxy-mode: iptablesiptables的代理模式是指由内核态的iptables转发流量到后端Pods。如下图所示。 关于iptables: Kube-proxy通过apiserver监控(watch)Service和Endpoints的变化; Kube-proxy安装iptables规则; iptables把访问Service的流量转发到后端真正的Pods上(Random); Kubernetes v1.1已支持,但不是默认方式,v1.2中将会是默认方式; 通过设置service.spec.sessionAffinity: ClientIP支持基于ClientIP的会话亲和性; 需要iptables和内核版本的支持。iptables > 1.4.11,内核支持route_localnet参数(kernel >= 3.6); 相比userspace的优点: 1,数据包不需要拷贝到用户态的kube-proxy再做转发,因此效率更高、更可靠。 2,不修改Client IP。 5.3 proxy-mode: iptables kubernetes v1.8 引入, 1.11正式可用 在 ipvs 模式下,kube-proxy监视Kubernetes服务和端点,调用 netlink接口相应地创建 IPVS 规则, 并定期将 IPVS 规则与 Kubernetes 服务和端点同步。 该控制循环可确保 IPVS 状态与所需状态匹配。 访问服务时,IPVS 将流量定向到后端Pod之一。 IPVS代理模式基于类似于 iptables模式的 netfilter 挂钩函数,但是使用哈希表作为基础数据结构,并且在内核空间中工作。 这意味着,与 iptables 模式下的 kube-proxy 相比,IPVS 模式下的 kube-proxy 重定向通信的延迟要短,并且在同步代理规则时具有更好的性能。与其他代理模式相比,IPVS 模式还支持更高的网络流量吞吐量。 IPVS提供了更多选项来平衡后端Pod的流量。 这些是: rr: round-robin lc: least connection (最小连接数) dh: destination hashing(目的地址has) sh: source hashing(源地址has) 等等 5.3 userspace和iptables转发方式的主要不同点userspace和iptables转发方式的主要不同点如下: 比较项 userspace iptables 谁转发流量到Pods| kube-proxy把访问Service的流量转发到后端真正的Pods上 |iptables把访问Service的流量转发到后端真正的Pods上|转发算法 |轮询Round-Robin| 随机Random|用户态和内核态 |数据包需要拷贝到用户态的kube-proxy再做转发,因此效率低、不可靠 |数据包直接在内核态转发,因此效率更高、更可靠|是否修改Client IP |因为kube-proxy在中间做代理,会修改数据包的Client IP| 不修改数据包的Client IPiptables版本和内核支持| 不依赖| iptables > 1.4.11,内核支持route_localnet参数(kernel >= 3.6) 通过设置kube-proxy的启动参数--proxy-mode设定使用userspace还是iptables代理模式。 6. Service发现方式现在服务创建了,得让别人来使用了。别人要使用首先得知道这些服务呀,服务治理很基本的一个功能就是提供服务发现。Kubernetes为我们提供了两种基本的服务发现方式:环境变量和DNS。 6.1 环境变量当一个Pod在节点Node上运行时,kubelet会为每个活动的服务设置一系列的环境变量。它支持Docker links compatible变量,以及更简单的{SVCNAME}_SERVICE_HOST和{SVCNAME}_SERVICE_PORT变量。后者是把服务名字大写,然后把中划线(-)转换为下划线(_)。 以服务redis-master为例,它暴露TCP协议的6379端口,被分配了集群IP地址10.0.0.11,则会创建如下环境变量: REDIS_MASTER_SERVICE_HOST=10.0.0.11REDIS_MASTER_SERVICE_PORT=6379REDIS_MASTER_PORT=tcp://10.0.0.11:6379REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379REDIS_MASTER_PORT_6379_TCP_PROTO=tcpREDIS_MASTER_PORT_6379_TCP_PORT=6379REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11 这里有一个注意点是,如果一个Pod要访问一个Service,则必须在该Service之前创建,否则这些环境变量不会被注入此Pod。DNS方式的服务发现就没有此限制。 6.2 DNS虽然DNS是一个cluster add-on特性,但是我们还是强烈推荐使用DNS作为服务发现的方式。DNS服务器通过KubernetesAPI监控新的Service的生成,然后为每个Service设置一堆DNS记录。如果集群设置了DNS,则该集群中所有的Pods都能够使用DNS解析Sercice。 例如,如果在Kubernertes中的my-ns名字空间中有一个服务叫做my-service,则会创建一个my-service.my-ns的DNS记录。在同一个名字空间my-ns的Pods能直接通过服务名my-service查找到该服务。如果是其它的Namespace中的Pods,则需加上名字空间,例如my-service.my-ns。返回的结果是服务的ClusterIP。当然,对于我们上面讲的headless的Service,返回的则是该Service对应的一堆后端Pods的IP地址。 对于知名服务端口,Kubernetes还支持DNS SRV记录。例如my-service.my-ns的服务支持TCP协议的http端口,则你可以通过一个DNS SRV查询_http._tcp.my-service.my-ns来发现http的端口。 对于每个Service的DNS记录,Kubernetes还会加上一个集群域名的后缀,作为完全域名(FQDN)。这个集群域名通过svc+安装集群DNS的DNS_DOMAIN参数指定,默认是svc.cluster.local。如果不是一个标准的Kubernetes支持的安装,则启动kubelet的时候指定参数--cluster-domain,你还需要指定--cluster-dns告诉kubelet集群DNS的地址。 6.3 如何发现和使用服务?一般在创建Pod的时候,指定一个环境变量GET_HOSTS_FROM,值可以设为env或者dns。在Pod中的应用先获取这个环境变量,得到获取服务的方式。如果是env,则通过getenv获取相应的服务的环境变量,例如REDIS_SLAVE_SERVICE_HOST;如果是dns,则可以在Pod内通过标准的gethostbyname获取服务主机名。有个例外是Pod的定义中,不能设置hostNetwork: true。 获取到服务的地址,就可以通过正常方式使用服务了。 如下是Kubernetes自带的guestbook.php中的一段相关代码,供参考: $host = 'redis-slave';if (getenv('GET_HOSTS_FROM') == 'env') { $host = getenv('REDIS_SLAVE_SERVICE_HOST');}$client = new Predis\Client([ 'scheme' => 'tcp', 'host' => $host, 'port' => 6379,]); 7. 一些容器服务中的Service虽然Service在Kubernetes中如此重要,但是对一些基于Kubernetes的容器服务,并没有使用Service,或者用的是上面讨论的headless类型的Service。这种方式基本上是把容器当做VM使用的典型,LoadBalancer和Pods网络互通,通过LoadBalancer直接把流量转发到Pods上,省却了中间由kube-proxy或者iptables的转发方式,从而提高了流量转发效率,但是也由LoadBalancer自己提供对后端Pods的维护,一般需要LoadBalancer提供动态路由的功能(即后端Pods可以动态地从LoadBalancer上注册/注销)。]]></content>
<categories>
<category>k8s</category>
</categories>
<tags>
<tag>k8s</tag>
<tag>iptables</tag>
</tags>
</entry>
<entry>
<title><![CDATA[容器网络]]></title>
<url>%2F2018%2F10%2F25%2Fcontainer-network%2F</url>
<content type="text"><![CDATA[容器网络vxlanvxlan原理: VXLAN通过MAC-in-UDP的报文封装,实现了二层报文在三层网络上的透传,属于overlay网络 Flannel首先,flannel利用Kubernetes-API(这里就是取node.spec.podCIDR)或者etcd用于存储整个集群的网络配置,其中最主要的内容为设置集群的网络地址空间。例如,设定整个集群内所有容器的IP都取自网段“10.1.0.0/16”。 接着,flannel在每个主机中运行flanneld作为agent,它会为所在主机从集群的网络地址空间中,获取一个小的网段subnet,本主机内所有容器的IP地址都将从中分配。 flannel 的 UDP 模式和 Vxlan 模式 host-gw 模式 UDP 模式是 三层 overlay,即,将原始数据包的三层包(IP包)装在 UDP 包里,通过 ip+端口 传到目的地,ip为目标node ip 端口为目标节点上flanneld进程监听的8285端口,解析后传入flannel0设备进入内核网络协议栈,UDP模式下 封包解包是在 flanneld里进行的也就是用户态下 重要!!! 《深入解析kubernetes》 33章 https://time.geekbang.org/column/article/65287 VxLan 模式 是二层 overlay,即将原始Ethernet包(MAC包)封装起来,通过vtep设备发到目的vtep,vxlan是内核模块,vtep是flannneld创建的,vxlan封包解封完全是在内核态完成的 注意点 inner mac 为 目的vtep的mac outer ip为目的node的ip 这一点和UDP有区别下一跳ip对应的mac地址是ARP表里记录的,inner mac对应的arp记录是 flanneld维护的,outer mac arp表是node自学习的 host-gw 模式的工作原理,是在 节点上加路由表,其实就是将每个 Flannel 子网(Flannel Subnet,比如:10.244.1.0/24)的“下一跳”,设置成了该子网对应的宿主机的 IP 地址。这台“主机”(Host)会充当这条容器通信路径里的“网关”(Gateway)。这也正是“host-gw”的含义。 $ ip route...<目的容器IP地址段> via <网关的IP地址> dev eth0# 网关的 IP 地址,正是目的容器所在宿主机的 IP 地址 Flannel host-gw 模式必须要求集群宿主机之间是二层连通的。如果分布在不同的子网里是不行的,只是三层可达 POD IP的分配使用CNI后,即配置了 kubelet 的 --network-plugin=cni,容器的IP分配:kubelet 先创建pause容器生成network namespace调用 网络driver CNI driverCNI driver 根据配置调用具体的cni 插件cni 插件给pause 容器配置网络pod 中其他的容器都使用 pause 容器的网络 CNM模式Pod IP是docker engine分配的,Pod也是以docker0为网关,通过veth连接network namespace flannel的两种方式 CNI CNM总结CNI中,docker0的ip与Pod无关,Pod总是生成的时候才去动态的申请自己的IP,而CNM模式下,Pod的网段在docker engine启动时就已经决定。CNI只是一个网络接口规范,各种功能都由插件实现,flannel只是插件的一种,而且docker也只是容器载体的一种选择,Kubernetes还可以使用其他的, cluster IP的分配是在kube-apiserver中 `pkg/registry/core/service/ipallocator`中分配的 network policyingress:- from: - namespaceSelector: matchLabels: user: alice - podSelector: matchLabels: role: client 像上面这样定义的 namespaceSelector 和 podSelector,是“或”(OR)的关系,表示的是yaml数组里的两个元素 ingress:- from: - namespaceSelector: matchLabels: user: alice podSelector: matchLabels: role: client 像上面这样定义的 namespaceSelector 和 podSelector,是“与”(AND)的关系,yaml里表示的是一个数组元素的两个字段 Kubernetes 网络插件对 Pod 进行隔离,其实是靠在宿主机上生成 NetworkPolicy 对应的 iptable 规则来实现的。 通过NodePort来访问service的话,client的源ip会被做SNAT client \ ^ \ \ v \ node 1 <--- node 2 | ^ SNAT | | ---> v |endpoint 流程: 客户端发送数据包到 node2:nodePort node2 使用它自己的 IP 地址替换数据包的源 IP 地址(SNAT) node2 使用 pod IP 地址替换数据包的目的 IP 地址 数据包被路由到 node 1,然后交给 endpoint Pod 的回复被路由回 node2 Pod 的回复被发送回给客户端 可以将 service.spec.externalTrafficPolicy 的值为 Local,请求就只会被代理到本地 endpoints 而不会被转发到其它节点。这样就保留了最初的源 IP 地址 不会对访问NodePort的client ip做 SNAT了。如果没有本地 endpoints,发送到这个节点的数据包将会被丢弃。]]></content>
<categories>
<category>k8s</category>
</categories>
<tags>
<tag>k8s</tag>
<tag>容器网络</tag>
</tags>
</entry>
<entry>
<title><![CDATA[服务亲和性路由在华为云 k8s 中的实践【Kubernetes中的服务拓扑】]]></title>
<url>%2F2018%2F07%2F07%2Fservicetopo%2F</url>
<content type="text"><![CDATA[本文根据华为工程师DJ在LC3上的演讲整理: 1. 拓扑概念首先,我们讲一下kubernetes中的拓扑。根据kubernetes现在的设计,我觉得拓扑可以是任意的。用户可以指定任何拓扑关系,比如az(available zone可用区)、region、机架、主机、交换机,甚至发电机。Kubernetes中拓扑的概念已经在调度器中被广泛使用。 2. Kubernetes调度器中的拓扑在kubernetes中,pod是工作的基本单元,所以调度器的工作可以简化为“在哪里运行这些pod”,当然我们知道pod是运行在节点里,但是怎么选择节点,这是调度器要解决的问题。 在kubernetes中,我们通过label选择节点,从而确定pod应该放在哪个节点中。下面是kubernetes原生提供的一些label: k8s.io/hostname 此外,kubernetes允许集群管理员和云提供商自定义label,比如机架、磁盘类型等。 3.Kubernetes中基于拓扑的调度比如,如果我们要将PostgreSQL服务运行在不同的zone中,假设zone的名字分别是1a和1b,那么我们可以在podSpec中定义节点亲和,如下图所示。主服务器需要运行在zone a的节点中,备用服务器需要运行在属于zone b的节点中。 在kubernetes中,pod与node的亲和或反亲和是刚性的要求。那么我们之前的问题“在哪里运行这些pod”就可以简化成“可以在这个节点运行pod吗”,答案取决于节点的一些情况,比如节点的名字,节点所属的region,节点是否有SSD盘等。 另外,调度器还会考虑另外一个问题,“可以把pod和其他pod放在同一区域吗”,比如,PostgreSQL服务肯定不能和MySQL服务运行在同一个节点中。Kubernetes通过下面三个步骤解决这个问题: 定义“其他pod”。这个过程与选择node的过程类似,我们通过label选择pod。比如,我们可以选择带有“app=web-frontend”label的pod 定义“同一区域”。如图中的两个pod,是在同一区域吗?不是,因为它们podSpec中的topologyKey字段,它是node label中的key,用于指定拓扑区域,比如zone、rack、主机等。比如如果我们使用“k8s.io/hostname”作为topologyKey,那么同一区域就表示在同一个主机上。 我们可以使用之前提到的任何nodelabel作为“同一区域”的标识,比如在同一个zone。或者使用自定义的label,下图给出了通过自定义拓扑label创建node组。 最后是决定是否可以。这取决于亲和和反亲和(affinity/anti-affinity)。 Kubernetes中其他依赖拓扑关系的特性 上面讲到的部署pod时的拓扑感知。除了调度之外,还有许多特性会依赖拓扑关系: 工作负载:在缩容或滚动升级的时候,控制器决定先杀掉哪些pod 卷存储:卷存储会有拓扑限制,来决定可以挂在卷的node集。比如GCE的持久卷只能挂在在同一个zone的节点上,本地卷被所在节点访问。 依赖拓扑的服务亲和性路由1. Service和endpoint首先我们来了解一下kubernetes中的service和endpoint的 概念。 Kubernetes中的service是一个抽象的概念,它通过label选择一个pod的集合,并且定义了这些pod的访问策略。简言之,service指定一个虚拟IP,作为这些pod的访问入口,在4层协议上工作。 Kubernetes中的endpoint是service后端的pod的地址列表。作为使用者,我们不需要感知它们,service创建的时候endpoint会自动创建,并且会根据后端的pod自动配置好。 2. Service的工作原理Endpoints控制器会watch到创建好的service和pod,然后创建相应的endpoint。Kube-proxy会watch service和endpoint,并创建相应的proxy规则。在kubernetes1.8之前proxy是通过底层的iptables实现,但是iptables只支持随机的负载均衡策略,并且可扩展性很差。 在1.8之后,我们实现并在社区持续推动了基于ipvs的proxy,这种proxy模式相对原来的iptables模式有很多优势,比如支持很多负载均衡算法,并且在大规模场景下接近无限扩展性等。好消息是,现在kubernetes社区基于ipvs的proxy已经svc,大家可以在生产环境使用。那么问题来了,既然我们已经有IPVS加持了,为什么还需要服务亲和性路由呢? 3. 服务亲和性路由先看一下用户的使用场景,当我们将pod正确的放到了用户指定的区域之后,就会有下面的问题。 单节点通信(访问serviceIP的时候只能访问到本节点的应用) 我们使用daemonset部署fluent的时候,应用只需要与当前节点的fluent通信。 一些用户出于安全考虑,希望确保他们只能与本地节点的服务通信,因为跨节点的流量可能会携带来自其他节点的敏感信息。 限制跨zone的流量 因为跨zone的流量会收费,而同一个zone的流量则不会。而有些云提供商,比如阿里云甚至不允许跨zone的流量。 性能优势:显然,到达本区域(节点/zone等)的流量肯定比跨区访问的流量有更低的延时和更高的带宽。 4. 亲和性路由的实现需要解决的问题正如我们前边讲到的,本地意味着一定的拓扑等级,我们需要有一个可以根据拓扑选择endpoint子集的机制。 这样我们就面临着如下问题: 是软亲和还是硬亲和。硬亲和意味着只需要本地的后端,而软亲和意味着首先尝试本地的,如果本地没有则尝试更广范围的。 如果是软亲和,那么判定标准是什么呢?可能给每个拓扑区域增加权重是一个解决方案。 如果多个后端满足条件,那么选择的依据又是什么呢?随机选择还是引入概率? 5. 我们的方案我们提供了一个解决方案,引用一种新的资源“ServicePolicy”。集群管理员可以通过ServicePolicy配置“local”的选择标准,以及各种拓扑的权重。ServicePolicy是一种可选的namespace范围内的资源,有三种模式:Required/Perferred/Ignored,分别代表硬亲和/软亲和/忽略。 上图是我们引入和ServicePolicy资源和endpoint引入的字段示例。在我们的示例中,ServicePolicy会选择namespace foo中带有label app=bar的service。由于我们将hostname设置为ServicePolicy的拓扑依据,那么对这些service的访问会只路由到与kube-proxy有在同一个host的后端。 需要说明的是,service和ServicePolicy是多对多的关系,一个ServicePolicy可以通过label选择多个service,一个service也可以被多个ServicePolicy选中,有多个亲和性要求。 另外我们还希望endpoint携带节点的拓扑信息,因此我们为endpoint添加一个新的字段Topology,用于识别pod属于的拓扑区域,比如在哪个host/rack/zone/region等。 这会改变现有的逻辑。如下图所示,Endpoint控制器需要watch两种新的资源,node和ServicePolicy,它需要维护node与endpoint的对应关系,并根据node的拓扑信息更新endpoint的Topology字段。另外,kube-proxy也会相应地作一些改动。它需要过滤掉与自身不在同一个拓扑区域的endpoint,这意味着kube-proxy会在不同的节点上创建不同的规则。 下图表示从ServicePolicy到proxy规则的数据流。首先ServicePolicy通过label选择一组service,我们可以根据这些service找到它们的pod。Endpoint控制器会将pod所在节点的拓扑label放到对应的endpoint中。Kube-proxy负责仅为处在同一拓扑区域的endpoint创建proxy规则,并且当多个endpoint满足要求时提供路由策略。 [总 结] 目前我们已经在华为云的CCE服务上实现了服务亲和性路由,效果很好,欢迎大家体验。我们很乐意把这个特性开源出来,并且正在做这件事,相信它会像IPVS一样,成为kubernetes下一个版本的一个重要特性。]]></content>
<categories>
<category>k8s</category>
</categories>
<tags>
<tag>k8s</tag>
</tags>
</entry>
<entry>
<title><![CDATA[了解思科 Tetration 平台]]></title>
<url>%2F2017%2F06%2F12%2Fcisco-platform%2F</url>
<content type="text"><![CDATA[介绍 思科推出了 Tetration Analytics平台,Tetration 平台主要是对大规模数据中心和云平台上的网络流量的实时采集、存储和分析。 Tetration 平台搭配基于Cloud Scale技术的硬件设备,流动在数据中心的任何一个数据包的元信息都可以被实时记录和存储下来。 Tetration 平台可以辅助用户在应用关系梳理、应用访问策略制定、模拟和实时验证、应用云端迁移访问策略制定、白名单安全模型等方面脱离传统手工和被动的工作方式。 Tetration Analytics平台主要由 数据采集部分、数据存储部分、和数据分析部分组成。 数据采集部分:包括安装在实体服务器或者虚拟机中的软件数据采集器、以太网交换机转发芯片的硬件数据采集逻辑和第三方数据接口组成。软件数据采集器通过 libpcap(一个网络数据包捕获函数库,linux抓包工具tcpdump就是基于此的)来对数据进行采集。 存储和分析部分:由基于思科UCS计算平台的服务器集群组成。 下图是思科Tetration Analytics平台架构 存储和分析部分是该平台的精髓所在,针对万亿个数据的无监督机器学习算法的采用,为网络访问行为基线设立、网络访问异常检测、应用访问关系的动态甄别、聚类动态划分等提供了方便的工具。平台为用户提供了网络数据完善的、全面的大数据来源。 Tetration Analytics平台提供了存储和分析的接口,用户可以根据数据进行相应的网络数据分析,提供的接口包括 开放式 API、REST、推送事件、用户应用 平台特性思科 Tetration Analytics 能够分析应用行为,并准确地反映出应用之间的依赖关系。它采用机器学习技术构建动态分层策略模型,从而实现应用分段和自动策略实施 Tetration Analytics 可大幅简化零信任模式的实施。它可以针对数据中心内的任何对象实时提供可视性。它使用基于行为的应用洞察和机器学习技术来构建动态策略模型,实现自动策略实施。此外,它还通过 REST API 支持开放式访问,客户可以编写个性化应用。 遥感勘测快上加快 Tetration Analytics 使用无需监管的机器学习技术,以线速处理收集的遥感勘测数据。借助自然语言技术,搜索和浏览数百亿条数据流记录。只需不到一秒即可获得切实可行的见解。 切实可行的应用见解 依据应用组件和行为分析算法获取实时数据。标识应用组及其通信模式和服务依赖性。获取自动化白名单策略建议,实现零信任安全性。 应用分段 在本地数据中心以及公共云和私有云中实施一致的策略,实现应用分段。持续监控合规性偏差,可在几分钟内发现生产网络中的违规情况。 开放式 API 利用全面精细的遥感勘测数据,轻松打造个性化的定制应用。生成个性化的定制通知和查询。监控应用层的延迟情况并获取通知。此平台使用 REST API。 强大的可扩展性 Tetration Analytics 从数据中心的每个数据包收集遥感勘测数据。它可以在几秒钟内分析数百万个事件并从数十亿条记录中提供切实可行的见解。它可以长期保留数据,而不会丢失细节。ddd]]></content>
<categories>
<category>数据中心</category>
</categories>
<tags>
<tag> Analytics</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Docker 介绍及其应用]]></title>
<url>%2F2017%2F06%2F11%2FDocker%2F</url>
<content type="text"><![CDATA[重要链接!!!知乎上的深入浅出 1.Docker 介绍便于入题,首先用 Docker 的logo解释下: 那个大鲸鱼(或者是货轮)就是操作系统 把要交付的应用程序看成是各种货物,原本要将各种各样形状、尺寸不同的货物放到大鲸鱼上,你得为每件货物考虑怎么安放(就是应用程序配套的环境),还得考虑货物和货物是否能叠起来(应用程序依赖的环境是否会冲突)。 现在使用了集装箱(容器)把每件货物都放到集装箱里,这样大鲸鱼可以用同样地方式安放、堆叠集装了,省事省力。参考知乎 步入正题: Docker 是 PaaS 提供商 dotCloud 开源的一个基于 LXC 的高级容器引擎,源代码托管在 Github 上, 基于go语言并遵从Apache2.0协议开源。 Docker可以轻松的为任何应用创建一个轻量级的、可移植的、自给自足的容器。开发者在笔记本上编译测试通过的容器可以批量地在生产环境中部署,包括VMs(虚拟机)、bare metal、OpenStack 集群和其他的基础应用平台。 如图所示,Docker 使用客户端-服务器 (C/S) 架构模式。 Docker 客户端会与 Docker 守护进程进行通信。 Docker 守护进程会处理复杂繁重的任务,例如建立、运行、发布你的 Docker 容器。 Docker 客户端和守护进程 Daemon 可以运行在同一个系统上,当然你也可以使用 Docker 客户端去连接一个远程的 Docker 守护进程。Docker 客户端和守护进程之间通过 socket 或者 RESTful API 进行通信,就像下图。 1.1 Docker 守护进程如上图所示,Docker 守护进程运行在一台主机上。用户并不直接和守护进程进行交互,而是通过 Docker 客户端间接和其通信。 1.2 Docker 客户端Docker 客户端,实际上是 docker 的二进制程序,是主要的用户与 Docker 交互方式。它接收用户指令并且与背后的 Docker 守护进程通信,如此来回往复。 1.3 Docker 内部要理解 Docker 内部构建,需要理解以下三种部件: Docker 镜像 - Docker imagesDocker 仓库 - Docker registeriesDocker 容器 - Docker containers Docker 镜像:Docker 镜像是 Docker 容器运行时的只读模板,每一个镜像由一系列的层 (layers) 组成。Docker 使用 UnionFS 来将这些层联合到单独的镜像中。UnionFS 允许独立文件系统中的文件和文件夹(称之为分支)被透明覆盖,形成一个单独连贯的文件系统。正因为有了这些层的存在,Docker 是如此的轻量。当你改变了一个 Docker 镜像,比如升级到某个程序到新的版本,一个新的层会被创建。因此,不用替换整个原先的镜像或者重新建立(在使用虚拟机的时候你可能会这么做),只是一个新 的层被添加或升级了。现在你不用重新发布整个镜像,只需要升级,层使得分发 Docker 镜像变得简单和快速。 Docker 仓库:Docker 仓库用来保存镜像,可以理解为代码控制中的代码仓库。同样的,Docker 仓库也有公有和私有的概念。公有的 Docker 仓库名字是 Docker Hub。Docker Hub 提供了庞大的镜像集合供使用。这些镜像可以是自己创建,或者在别人的镜像基础上创建。Docker 仓库是 Docker 的分发部分。 Docker 容器:Docker 容器和文件夹很类似,一个Docker容器包含了所有的某个应用运行所需要的环境。每一个 Docker 容器都是从 Docker 镜像创建的。Docker 容器可以运行、开始、停止、移动和删除。每一个 Docker 容器都是独立和安全的应用平台,Docker 容器是 Docker 的运行部分。 2. Docker 8个的应用场景 本小节介绍了常用的8个Docker的真实使用场景,分别是简化配置、代码流水线管理、提高开发效率、隔离应用、整合服务器、调试能力、多租户环境、快速部署 一些Docker的使用场景,它为你展示了如何借助Docker的优势,在低开销的情况下,打造一个一致性的环境。 1.简化配置这是Docker公司宣传的Docker的主要使用场景。虚拟机的最大好处是能在你的硬件设施上运行各种配置不一样的平台(软件、系统),Docker在降低额外开销的情况下提供了同样的功能。它能让你将运行环境和配置放在代码中然后部署,同一个Docker的配置可以在不同的环境中使用,这样就降低了硬件要求和应用环境之间耦合度。 2. 代码流水线(Code Pipeline)管理前一个场景对于管理代码的流水线起到了很大的帮助。代码从开发者的机器到最终在生产环境上的部署,需要经过很多的中间环境。而每一个中间环境都有自己微小的差别,Docker给应用提供了一个从开发到上线均一致的环境,让代码的流水线变得简单不少。 3. 提高开发效率不同的开发环境中,我们都想把两件事做好。一是我们想让开发环境尽量贴近生产环境,二是我们想快速搭建开发环境。 理想状态中,要达到第一个目标,我们需要将每一个服务都跑在独立的虚拟机中以便监控生产环境中服务的运行状态。然而,我们却不想每次都需要网络连接,每次重新编译的时候远程连接上去特别麻烦。这就是Docker做的特别好的地方,开发环境的机器通常内存比较小,之前使用虚拟的时候,我们经常需要为开发环境的机器加内存,而现在Docker可以轻易的让几十个服务在Docker中跑起来。 4. 隔离应用有很多种原因会让你选择在一个机器上运行不同的应用,比如之前提到的提高开发效率的场景等。 我们经常需要考虑两点,一是因为要降低成本而进行服务器整合,二是将一个整体式的应用拆分成松耦合的单个服务(译者注:微服务架构)。 5. 整合服务器正如通过虚拟机来整合多个应用,Docker隔离应用的能力使得Docker可以整合多个服务器以降低成本。由于没有多个操作系统的内存占用,以及能在多个实例之间共享没有使用的内存,Docker可以比虚拟机提供更好的服务器整合解决方案。 6. 调试能力Docker提供了很多的工具,这些工具不一定只是针对容器,但是却适用于容器。它们提供了很多的功能,包括可以为容器设置检查点、设置版本和查看两个容器之间的差别,这些特性可以帮助调试Bug。 7. 多租户环境另外一个Docker有意思的使用场景是在多租户的应用中,它可以避免关键应用的重写。我们一个特别的关于这个场景的例子是为IoT(物联网)的应用开发一个快速、易用的多租户环境。这种多租户的基本代码非常复杂,很难处理,重新规划这样一个应用不但消耗时间,也浪费金钱。 使用Docker,可以为每一个租户的应用层的多个实例创建隔离的环境,这不仅简单而且成本低廉,当然这一切得益于Docker环境的启动速度和其高效的diff命令。 8. 快速部署在虚拟机之前,引入新的硬件资源需要消耗几天的时间。虚拟化技术(Virtualization)将这个时间缩短到了分钟级别。而Docker通过为进程仅仅创建一个容器而无需启动一个操作系统,再次将这个过程缩短到了秒级。这正是Google和Facebook都看重的特性。 你可以在数据中心创建销毁资源而无需担心重新启动带来的开销。通常数据中心的资源利用率只有30%,通过使用Docker并进行有效的资源分配可以提高资源的利用率。 本文的几个概念 LXC: LXC为Linux Container的简写。可以提供轻量级的虚拟化,以便隔离进程和资源,而且不需要提供指令解释机制以及全虚拟化的其他复杂性。属于操作系统层次之上的虚拟化]]></content>
<categories>
<category>docker</category>
</categories>
<tags>
<tag>docker</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Java 中的并发]]></title>
<url>%2F2017%2F06%2F10%2Fjava-create-thread%2F</url>
<content type="text"><![CDATA[Java 中的并发如何创建一个线程按 Java 语言规范中的说法,创建线程只有一种方式,就是创建一个 Thread 对象。而从 HotSpot 虚拟机的角度看,创建一个虚拟机线程 有两种方式,一种是创建 Thread 对象,另一种是创建 一个本地线程,加入到虚拟机线程中。 如果从 Java 语法的角度。有两种方法。 第一是继承 Thread 类,实现 run 方法,并创建子类对象。 public void startThreadUseSubClass() { class MyThread extends Thread { public void run() { System.out.println("start thread using Subclass of Thread"); } } MyThread thread = new MyThread(); thread.start();} 另一种是传递给 Thread 构造函数一个 Runnable 对象。 public void startThreadUseRunnalbe() { Thread thread = new Thread(new Runnable() { public void run() { System.out.println("start thread using runnable"); } }); thread.start();} 当然, Runnalbe 对象,也不是只有这一种形式,例如如果我们想要线程执行时返回一个值,就需要用到另一种 Runnalbe 对象,它 对原来的 Runnalbe 对象进行了包装。 public void startFutureTask() { FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() { public Integer call() { return 1; } }); new Thread(task).start(); try { Integer result = task.get(); System.out.println("future result " + result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); }} 结束线程wait 与 sleepsleep 会使得当前线程休眠一段时间,但并不会释放已经得到的锁。 wait 会阻塞住,并释放已经得到的锁。一直到有人调用 notify 或者 notifyAll,它会重新尝试得到锁,然后再唤醒。 线程池好处 复用 线程池中有一系列线程,这些线程在执行完任务后,并不会被销毁,而会从任务队列中取出任务,执行这些任务。这样,就避免为每个任务 都创建线程,销毁线程。 在有大量短命线程的场景下,如果创建线程和销毁线程的时间比线程执行任务的时间还长,显然是不划算的,这时候,使用线程池就会有明显 的好处。 流控 同时,可以设置线程数目,这样,线程不会增大到影响系统整体性能的程度。当任务太多时,可以在队列中排队, 如果有空闲线程,他们会从队列中取出任务执行。 使用 线程数目 那么,线程的数目要设置成多少呢?这需要根据任务类型的不同来设置,假如是大量计算型的任务,他们不会阻塞,那么可以将线程数目设置 为处理器数目。而如果任务中涉及大量IO,有些线程会阻塞住,这样就要根据阻塞线程数目与运行线程数目的比例,以及处理器数目来设置 线程总数目。例如阻塞线程数目与运行线程数目之比为n, 处理器数目为p,那么可以设置 n * (p + 1) 个线程,保证有 n 个线程处于运行 状态。 Executors JDK 的 java.util.concurrent.Executors 类提供了几个静态的方法,用于创建不同类型的线程池。 ExecutorService service = Executors.newFixedThreadPool(10);ArrayList<Future<Integer>> results = new ArrayList<>();for (int i = 0; i < 14; i++) { Future<Integer> r = service.submit(new Callable<Integer>() { public Integer call() { return new Random().nextInt(); }); results.add(r);} newFixedThreadPool 可以创建固定数目的线程,一旦创建不会自动销毁线程,即便长期没有任务。除非显式关闭线程池。如果任务队列中有任务,就取出任务执行。 另外,还可以使用 newCachedThreadPool 方法创建一个不设定固定线程数目的线程池,它有一个特性,线程完成任务后,如果一分钟之内又有新任务,就会复用这个线程执行新任务。如果超过一分钟还没有任务执行,就会自动销毁。 另外,还提供了 newSingleThreadExecutor 创建有一个工作线程的线程池。 原理JDK 中的线程池通过 HashSet 存储工作者线程,通过 BlockingQueue 来存储待处理任务。 通过核心工作者数目(corePoolSize) 和 最大工作者数目(maximumPoolSize) 来确定如何处理任务。如果当前工作者线程数目 小于核心工作者数目,则创建一个工作者线程执行这个任务。否则,将这个任务放入待处理队列。如果入队失败,再看看当前工作 者数目是不是小于最大工作者数目,如果小于,则创建工作者线程执行这个任务。否则,拒绝执行这个任务。 另外,如果待处理队列中没有任务要处理,并且工作者线程数目超过了核心工作者数目,那么,需要减少工作者线程数目。]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>java</tag>
<tag>线程</tag>
</tags>
</entry>
</search>