Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

内核缺陷触发的NodePort服务63秒延迟问题

14
Aug
2020

内核缺陷触发的NodePort服务63秒延迟问题

By Alex
/ in PaaS
3 Comments
现象

我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容器网络,集群由三个二层互通的Master节点 10.0.0.11、 10.0.0.12、 10.0.0.13组成。在访问宿主机端口为 30153的NodePort类型的Service时,出现了很有趣的现象:

  1. 在节点 10.0.0.11、 10.0.0.13节点上 curl http://localhost:30153,有50%几率卡住
  2. 在节点 10.0.0.12上 curl http://localhost:30153,100%几率卡住
  3. 从集群内部,访问非本节点的30153端口,畅通
  4. 从集群外部,访问任意节点的30153端口,畅通

三个节点本身并无差异,卡住几率不同,可能和服务的端点(Endpoint,即Pod)的分布情况有关。

NodePort服务的定义如下:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Service
metadata:
  name: kube-dns-nodeport
  namespace: kube-system
spec:
  externalTrafficPolicy: Cluster
  ports:
  - name: metrics
    nodePort: 30153
    port: 9153
    protocol: TCP
    targetPort: 9153
  selector:
    k8s-app: kube-dns
  sessionAffinity: None
  type: NodePort

该服务的端点有两个:

Shell
1
2
3
4
kubectl -n kube-system get pod -l k8s-app=kube-dns -o wide
NAME                      READY   STATUS    RESTARTS   AGE    IP           NODE        NOMINATED NODE   READINESS GATES
coredns-bbc9b5888-r72zd   1/1     Running   0          140m   172.29.0.2   10.0.0.11   <none>           <none>
coredns-bbc9b5888-v6wx6   1/1     Running   0          10m    172.29.2.3   10.0.0.13   <none>           <none>

可以看到,端点在10.0.0.11、10.0.0.13上分别有一个。假设容器网络存在问题,只能访问本机的Pod,则能解释前面的卡住现象 —— 10.0.0.12上没有端点,因此一直卡住。10.0.0.11、10.0.0.13分别占有50%端点,因此50%几率卡住。

但是,我们在任意节点直接访问Pod,发现都是畅通的:

Shell
1
2
3
4
5
curl http://172.29.0.2:9153
404 page not found
 
curl http://172.29.2.3:9153
404 page not found

这说明故障和容器网络没有直接关系。 

分析
内层封包

我们在10.0.0.11向localhost:30153发起请求,并且抓取卡住时的封包:

Shell
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
# 经过iptables时,DNAT为POD_IP:9153,SNAT为宿主机eth0地址
curl http://127.0.0.1:30153
 
tcpdump -ttttt -nn -vvv -i any 'tcp port 9153'
 
# 请求端
# SYN 0
00:00:00.000000 IP (tos 0x0, ttl 64, id 42199, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0x5a6c (incorrect -> 0xd480), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19658463 ecr 0,nop,wscale 9], length 0
# SYN 1
00:00:01.000549 IP (tos 0x0, ttl 64, id 42200, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0x5a6c (incorrect -> 0xd097), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19659464 ecr 0,nop,wscale 9], length 0
# SYN 2
00:00:03.005510 IP (tos 0x0, ttl 64, id 42201, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0x5a6c (incorrect -> 0xc8c2), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19661469 ecr 0,nop,wscale 9], length 0
# SYN 3
00:00:07.008579 IP (tos 0x0, ttl 64, id 42202, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0x5a6c (incorrect -> 0xb91f), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19665472 ecr 0,nop,wscale 9], length 0
# SYN 4
00:00:15.024516 IP (tos 0x0, ttl 64, id 42203, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0x5a6c (incorrect -> 0x99cf), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19673488 ecr 0,nop,wscale 9], length 0
# SYN 5
00:00:31.072562 IP (tos 0x0, ttl 64, id 42204, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0x5a6c (incorrect -> 0x5b1f), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19689536 ecr 0,nop,wscale 9], length 0
# SYN 6   63秒
00:01:03.136526 IP (tos 0x0, ttl 64, id 42205, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0x5a6c (incorrect -> 0xddde), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19721600 ecr 0,nop,wscale 9], length 0
# SYN+ACK 通讯建立
00:01:03.137188 IP (tos 0x0, ttl 63, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.2.3.9153 > 172.29.0.0.40233: Flags [S.], cksum 0xbdbe (correct), seq 4208932479, ack 1769165321, win 27960, options [mss 1410,sackOK,TS val 19735883 ecr 19721600,nop,wscale 9], length 0
 
 
# 服务端
 
# 这个报文在63秒后才收到
# SYN 6
00:00:00.000000 IP (tos 0x0, ttl 64, id 42205, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0xddde (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19721600 ecr 0,nop,wscale 9], length 0
00:00:00.000025 IP (tos 0x0, ttl 63, id 42205, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0xddde (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19721600 ecr 0,nop,wscale 9], length 0
00:00:00.000065 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.2.3.9153 > 172.29.0.0.40233: Flags [S.], cksum 0x5a6c (incorrect -> 0xbdbe), seq 4208932479, ack 1769165321, win 27960, options [mss 1410,sackOK,TS val 19735883 ecr 19721600,nop,wscale 9], length 0

可以注意到:

  1. 当Service负载均衡到本机的Pod时畅通,负载均衡到其它节点的Pod时卡住。这就是50%卡住的原因
  2. 并非彻底卡死,在63秒后,SYN成功
iptables规则

从上面的抓包结果分析,我们初步判断故障和iptables没有关系。iptables导致的问题可能是无限卡死直到超时(静默的丢弃了报文)、ICMP错误、TCP RST等,通常不会出现过了一段时间自动恢复的情况。

然后,这个故障很特别,它的确是由iptables规则所触发的。我们是后来查找资料才发现的这一事实,这里先列出相关的规则。其中PREROUTING阶段的规则如下:

Shell
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
# iptables -L -n -v -t nat
 
Chain PREROUTING (policy ACCEPT 1 packets, 60 bytes)
pkts bytes target                prot opt in  out  source     destination        
# 所有封包都要这经过这个链
# kubernetes service portals
46185 2817K KUBE-SERVICES         all  --  *   *    0.0.0.0/0  0.0.0.0/0            
 
Chain KUBE-SERVICES (2 references)
pkts bytes target                prot opt in  out  source      destination        
# 这些会匹配ClusterIP,和本场景无关
# kube-system/kube-dns:metrics cluster IP
# 0  0 KUBE-SVC-JD5MR3NA4I4DYORP  tcp  --  *  *     0.0.0.0/0  172.29.255.10  tcp dpt:9153
#kube-system/kube-dns-nodeport:metrics cluster IP                  
# 0  0 KUBE-SVC-CZA6AQQ7F4S64XIF  tcp  --  *  *     0.0.0.0/0  172.29.255.56  tcp dpt:9153
# default/kubernetes:https cluster IP                            
# 0  0 KUBE-SVC-NPX46M4PTMTKRN6Y  tcp  --  *  *     0.0.0.0/0  172.29.255.1   tcp dpt:443
# kube-system/kube-dns:dns cluster IP                          
# 0  0 KUBE-SVC-TCOU7JCQXEZGVUNU  udp  --  *  *     0.0.0.0/0  172.29.255.10  udp dpt:53
#kube-system/kube-dns:dns-tcp cluster IP                        
# 0  0 KUBE-SVC-ERIFXISQEP7F7OF4  tcp  --  *  *     0.0.0.0/0  172.29.255.10  tcp dpt:53
# 不是访问ClusterIP的、目的地址是本机绑定地址的封包,都要经过这个链
# kubernetes service nodeports; NOTE: this must be the last rule in this chain
  678 40680 KUBE-NODEPORTS        all  --  *  *     0.0.0.0/0  0.0.0.0/0  ADDRTYPE match dst-type LOCAL
 
 
Chain KUBE-NODEPORTS (1 references)
pkts bytes target                prot opt in out   source     destination        
# 匹配本场景(目标端口30153),会给封包打标记,因此不会终止规则链遍历
# kube-system/kube-dns-nodeport:metrics
    0     0 KUBE-MARK-MASQ        tcp  --  *  *     0.0.0.0/0  0.0.0.0/0  tcp dpt:30153
# 匹配本场景(目标端口30153),跳转到NodePort的目标服务的专属规则链
# kube-system/kube-dns-nodeport:metrics
    0     0 KUBE-SVC-CZAXXX       tcp  --  *  *     0.0.0.0/0  0.0.0.0/0  tcp dpt:30153
 
 
Chain KUBE-MARK-MASQ (8 references)
pkts bytes target                prot opt in out   source     destination        
# 封包会被打上 0x4000标记
    0     0 MARK                  all  --  *  *     0.0.0.0/0  0.0.0.0/0  MARK or 0x4000
 
 
# 这个是NodePort的目标服务的专属规则链,随机转发给某个服务端点
Chain KUBE-SVC-CZAXXX (2 references)
pkts bytes target                prot opt in out   source     destination    
# kube-system/kube-dns-nodeport:metrics    
    0     0 KUBE-SEP-DZXXXX       all  --  *  *     0.0.0.0/0  0.0.0.0/0  statistic mode random probability 0.50000000000
# kube-system/kube-dns-nodeport:metrics
    0     0 KUBE-SEP-COSXXX       all  --  *  *     0.0.0.0/0  0.0.0.0/0          
 
 
# 这是NodePort服务的某个端点的专属规则链
Chain KUBE-SEP-DZXXXX (1 references)
pkts bytes target                prot opt in out   source     destination        
# kube-system/kube-dns-nodeport:metrics
    0     0 KUBE-MARK-MASQ        all  --  *  *     172.29.2.3 0.0.0.0/0            
# 匹配本场景,进行DNAT,将目的地址从本机地址转为服务端点地址,如果端点不在本机,报文会从flannel.1接口发出
# kube-system/kube-dns-nodeport:metrics
    0     0 DNAT                  tcp  --  *  *     0.0.0.0/0  0.0.0.0/0  tcp to:172.29.2.3:9153

我们可以看到,如果服务端点不在本机,发往localhost:30153的封包,会被先打上0x4000标记,然后DNAT到服务端点的IP:PORT(例如172.29.2.3:9153),这会保证封包从flannel.1发出。

POSTROUTING阶段的规则如下:

Shell
1
2
3
4
5
6
7
8
9
Chain POSTROUTING (policy ACCEPT 2 packets, 120 bytes)
pkts bytes target   prot opt in  out  source     destination        
# kubernetes postrouting rules
83159 5015K KUBE-POSTROUTING all  --  *   *    0.0.0.0/0  0.0.0.0/0            
 
Chain KUBE-POSTROUTING (1 references)
pkts bytes target prot opt in out source destination  
# kubernetes service traffic requiring SNAT      
0   0 MASQUERADE  all  --  *  *  0.0.0.0/0 0.0.0.0/0  mark match 0x4000/0x4000 random-fully

可以看到,这里做了SNAT,任何具有0x4000标记的封包,都被SNAT,确保使用flannel.1的地址作为源IP。

63秒现象

经过反复测试, 发现卡住时,总是会消耗63秒左右,然后接收到响应。

63秒这个数字,和TCP默认的SYN重试机制有关。SYN如果没有收到ACK,发送端会自动重发SYN,每次重试的延迟时间指数增长,依次为1, 2, 4, 8, 16, 32,这会引发合计63秒的总延迟。

令人费解的是,为什么63秒之后,不是超时,而是连接成功?

外层封包

从上文抓取的TCP封包看,服务端的Pod网卡没有收到前面6次SYN,这些封包应该在链路的某个位置被丢弃了。

在VXLAN模式下,上面抓的TCP封包,会封装在UDP报文中,并通过节点物理网卡的8472端口发出。我们从外层报文的角度分析一下

Shell
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
# tcpdump -ttttt -n -v -i eth0 'udp port 8472'
# 畅通时,没有输出,因为访问本机的Pod时不走VXLAN
 
# 卡住时,请求端封包
00:00:00.000000 IP (tos 0x0, ttl 64, id 43516, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.60142 > 10.0.0.13.8472: [bad udp cksum 0xffff -> 0x4b80!] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42199, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0xd480 (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19658463 ecr 0,nop,wscale 9], length 0
00:00:01.000542 IP (tos 0x0, ttl 64, id 44011, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.60142 > 10.0.0.13.8472: [bad udp cksum 0xffff -> 0x4b80!] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42200, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0xd097 (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19659464 ecr 0,nop,wscale 9], length 0
00:00:03.005505 IP (tos 0x0, ttl 64, id 45443, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.60142 > 10.0.0.13.8472: [bad udp cksum 0xffff -> 0x4b80!] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42201, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0xc8c2 (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19661469 ecr 0,nop,wscale 9], length 0
00:00:07.008579 IP (tos 0x0, ttl 64, id 46574, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.60142 > 10.0.0.13.8472: [bad udp cksum 0xffff -> 0x4b80!] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42202, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0xb91f (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19665472 ecr 0,nop,wscale 9], length 0
00:00:15.024518 IP (tos 0x0, ttl 64, id 50068, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.60142 > 10.0.0.13.8472: [bad udp cksum 0xffff -> 0x4b80!] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42203, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0x99cf (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19673488 ecr 0,nop,wscale 9], length 0
00:00:31.072564 IP (tos 0x0, ttl 64, id 65085, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.60142 > 10.0.0.13.8472: [bad udp cksum 0xffff -> 0x4b80!] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42204, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0x5b1f (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19689536 ecr 0,nop,wscale 9], length 0
00:01:03.136538 IP (tos 0x0, ttl 64, id 19809, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.50024 > 10.0.0.13.8472: [no cksum] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42205, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0xddde (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19721600 ecr 0,nop,wscale 9], length 0
00:01:03.137105 IP (tos 0x0, ttl 64, id 63229, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.13.50017 > 10.0.0.11.8472: [no cksum] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 63, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.2.3.9153 > 172.29.0.0.40233: Flags [S.], cksum 0xbdbe (correct), seq 4208932479, ack 1769165321, win 27960, options [mss 1410,sackOK,TS val 19735883 ecr 19721600,nop,wscale 9], length 0
 
 
# 卡住时,服务端封包
# SYN 0
00:00:00.000000 IP (tos 0x0, ttl 64, id 43516, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.60142 > 10.0.0.13.8472: [bad udp cksum 0xffff -> 0x4b80!] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42199, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0xd480 (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19658463 ecr 0,nop,wscale 9], length 0
# SYN 1
00:00:01.000543 IP (tos 0x0, ttl 64, id 44011, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.60142 > 10.0.0.13.8472: [bad udp cksum 0xffff -> 0x4b80!] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42200, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0xd097 (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19659464 ecr 0,nop,wscale 9], length 0
# SYN 2
00:00:03.005514 IP (tos 0x0, ttl 64, id 45443, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.60142 > 10.0.0.13.8472: [bad udp cksum 0xffff -> 0x4b80!] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42201, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0xc8c2 (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19661469 ecr 0,nop,wscale 9], length 0
# SYN 3
00:00:07.008577 IP (tos 0x0, ttl 64, id 46574, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.60142 > 10.0.0.13.8472: [bad udp cksum 0xffff -> 0x4b80!] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42202, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0xb91f (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19665472 ecr 0,nop,wscale 9], length 0
# SYN 4
00:00:15.024575 IP (tos 0x0, ttl 64, id 50068, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.60142 > 10.0.0.13.8472: [bad udp cksum 0xffff -> 0x4b80!] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42203, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0x99cf (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19673488 ecr 0,nop,wscale 9], length 0
# SYN 5
00:00:31.072593 IP (tos 0x0, ttl 64, id 65085, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.60142 > 10.0.0.13.8472: [bad udp cksum 0xffff -> 0x4b80!] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42204, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0x5b1f (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19689536 ecr 0,nop,wscale 9], length 0
# SYN 6 63秒,可以看到这次没有UDP封包没有chksum了,服务端也收到SYN了
00:01:03.136659 IP (tos 0x0, ttl 64, id 19809, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.11.50024 > 10.0.0.13.8472: [no cksum] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 64, id 42205, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.0.0.40233 > 172.29.2.3.9153: Flags [S], cksum 0xddde (correct), seq 1769165320, win 43690, options [mss 65495,sackOK,TS val 19721600 ecr 0,nop,wscale 9], length 0
00:01:03.136830 IP (tos 0x0, ttl 64, id 63229, offset 0, flags [none], proto UDP (17), length 110)
    10.0.0.13.50017 > 10.0.0.11.8472: [no cksum] OTV, flags [I] (0x08), overlay 0, instance 1
IP (tos 0x0, ttl 63, id 0, offset 0, flags [DF], proto TCP (6), length 60)
    172.29.2.3.9153 > 172.29.0.0.40233: Flags [S.], cksum 0xbdbe (correct), seq 4208932479, ack 1769165321, win 27960, options [mss 1410,sackOK,TS val 19735883 ecr 19721600,nop,wscale 9], length 0

可以看到,请求端/服务端的UDP报文相互呼应, 至少可以说,请求端的全部报文都送到了服务端。

但是,前面5次重试的UDP报文都被标注了bad udp cksum,最后一次UDP报文没有chksum,连接成功建立。有理由怀疑,故障和chksum有关系。

通过查阅VXLAN的RFC,在VXLAN Frame Format一章中,关于UDP封包的Checksum,具有如下说明:

UDP Checksum应该以零传递。接收端接收到零Checksum的UDP包后,它必须接受,用于解包(decapsulation)。但是,如果发送端的确提供了非零Checksum,那么它必须是正确的、基于整个封包进行计算的 —— 包括IP头、UDP头、VXLAN头,以及最里层的MAC帧。接收端可以对非零Checksum进行校验,或者不去校验。但是,如果进行了校验,且校验结果不正确,则必须丢弃UDP封包

RFC说的很明确,如果Checksum是错误的,并且进行了校验,则封包会被丢弃。带入我们的场景中,可以推测,服务端内核丢弃了那些bad udp cksum的封包,因而服务端的Pod网卡一直没有收到SYN。

那么,Checksum为什么会错了呢?根源应该在内核。

内核缺陷

现代操作系统都支持某些形式的Network Offloading,将某些工作委托给网卡完成,从而减轻CPU的负担。从内核代码的演变情况来看,这种Offloading的种类越来越丰富。

Checksum就可以Offload给网卡来完成,这样,IP、TCP和UDP的Checksum,会在报文即将从网络接口发送出去的时候进行计算。Offloading需要内核的TCP/IP栈、设备驱动、硬件正确的配合才能完成。

通过查阅资料,我们了解到,内核中存在一个和VXLAN处理有关的缺陷,该缺陷会导致Checksum Offloading不能正确完成。这个缺陷仅仅在很边缘的场景下才会表现出来。

在VXLAN的UDP头被NAT过(见下文的二次SNAT问题)的前提下,如果:

  1. VXLAN设备禁用(这是RFC的建议)了UDP Checksum
  2. VXLAN设备启用了Tx Checksum Offloading

就会导致生成错误的UDP Checksum。

二次SNAT

前面提到内核缺陷必须在VXLAN的UDP封包被NAT时,才会触发。那么,在源、目标地址都是宿主机网段的情况下,为什么还对UDP封包进行NAT呢?

在上文的iptables分析中我们看到,访问localhost:30153的封包,会被:

  1. DNAT到服务端Pod的地址,这保证封包能够通过flannel.1发出
  2. 打上0x4000标记,这个标记会在随后的POSTROUTING阶段,用于进行SNAT。使用flannel.1的地址作为源地址

被DNAT+SNAT后的内层TCP报文,进入flannel.1接口,进而在内核的VXLAN驱动中处理,封装为UDP报文。需要注意,iptables打标记,我们期望是针对内层报文的。然而,内层封包被VXLAN处理后包裹了外层UDP,重新进入网络栈,内核自动将0x4000标记关联到外层UDP报文上,这导致了额外的一次SNAT:

Shell
1
2
3
4
5
6
7
8
iptables -t nat -I  KUBE-POSTROUTING 1 -j LOG --log-prefix "0x4000-marked: " -m mark --mark 0x4000/0x4000
 
dmesg -wH
 
# 第一次NAT,针对内层报文,我们期望将127.0.0.1 SNAT为 flannel.1的地址
[  +3.851027] 0x4000-marked: IN= OUT=flannel.1 SRC=127.0.0.1 DST=172.29.2.3 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=44704 DF PROTO=TCP SPT=43326 DPT=9153 WINDOW=43690 RES=0x00 SYN URGP=0 MARK=0x4000
# 第二次NAT,针对外层报文,我们并没有期望这次SNAT,因为源地址本来就是eth0的地址了
[  +0.000019] 0x4000-marked: IN= OUT=eth0 SRC=10.0.0.12 DST=10.0.0.11 LEN=110 TOS=0x00 PREC=0x00 TTL=64 ID=9697 PROTO=UDP SPT=60211 DPT=8472 LEN=90 MARK=0x4000

在Kubernetes 1.16.0之前的版本,Kube Proxy做SNAT( -j MASQUERADE)时,没有使用 --random-fully参数。这意味着第二次SNAT不会有任何效果,因为内核会在Masquerading时尝试保持源端口不变,与此同时,源端口已经是期望的地址了。

但是,使用了--random-fully参数后,情况变得不同。该参数会强制的进行随机的源端口映射。这就触发了上文提到的内核缺陷。

random-fully

这是SNAT目标的一个参数,它会使用伪随机数生成器,自动产生一个端口,来替换NAT前的端口。根据文档,它需要内核3.14+才能支持。

然而,我们用的是CentOS 7,内核版本是 3.10.0-1127.13.1.el7.x86_64,照理说应该不支持这个特性。

在宿主机上,用iptables-save导出规则,也是看不到--random-fully的。但是,从Kube Proxy容器里面导出规则,却能看见:

Shell
1
2
3
4
5
# iptables-save | egrep '\-A\sKUBE-POSTROUTING'
-A KUBE-POSTROUTING  -m mark --mark 0x4000/0x4000 -j MASQUERADE
 
# kubectl -n kube-system exec kube-proxy-7qtzm -- iptables-save | egrep '\-A\sKUBE-POSTROUTING'
-A KUBE-POSTROUTING  -m mark --mark 0x4000/0x4000 -j MASQUERADE --random-fully

原因可能是两个iptables的版本不同。有一点可以明确,--random-fully在我们的环境下的确产生了影响,因为禁用该参数后,问题就消失了。

解决
临时方案
关闭Offloading

既然故障的根源是内核中,和Offloading有关的缺陷,因此,禁用Offloading是最直接的手段:

Shell
1
ethtool --offload flannel.1 rx off tx off

这个命令执行的时机很重要,如果主机重启,Flannel创建网卡后,才能执行该命令,否则会提示找不到设备。

防止二次SNAT

有两种方式防止对VXLAN的UDP封包进行SNAT。第一种是禁用--random-fully参数。这种做法印证了上文关于此参数的猜测。

Shell
1
iptables -t nat -R KUBE-POSTROUTING  1  -m mark --mark 0x4000/0x4000 -j MASQUERADE 

第二种,将发往8472端口的UDP封包,做一个重置标记的操作。Kubernetes社区就是这种做法。

Shell
1
iptables -A OUTPUT -p udp -m udp --dport 8472 -j MARK --set-mark 0x0  
永久方案
升级K8S版本

查看Kubernetes v1.18.5的Changelog,可以发现PR 92035修复了这个故障。这个PR会在不需要0x4000标记时,将其清除。

在Kubelet初始化期间,会在NAT表创建KUBE-MARK-MASQ、KUBE-MARK-DROP、KUBE-POSTROUTING等链,并添加一些规则。该PR对这部分的逻辑进行了修改:

pkg/kubelet/kubelet_network_linux.go
Go
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
func (kl *Kubelet) syncNetworkUtil() {
    // ...
    if _, err := kl.iptClient.EnsureRule(utiliptables.Append, utiliptables.TableNAT,
    // 这里将原先有缺陷的--set-xmark 0x4000/0x4000 改为 --xor-mark
      KubeMarkMasqChain, "-j", "MARK", "--or-mark", masqueradeMark); err != nil {
        klog.Errorf("Failed to ensure marking rule for %v: %v", KubeMarkMasqChain, err)
        return
    }
    // ...
    // 这里是关键的修改,在KUBE-POSTROUTING中添加以下规则:
    // 如果封包没有0x4000标记位,则不做处理
    // iptables -t NAT -A KUBE-POSTROUTING -m mark ! --mark=0x4000/0x4000 -j RETRUN
    if _, err := kl.iptClient.EnsureRule(utiliptables.Append, utiliptables.TableNAT, KubePostroutingChain,
        "-m", "mark", "!", "--mark", fmt.Sprintf("%s/%s", masqueradeMark, masqueradeMark),
        "-j", "RETURN"); err != nil {
        klog.Errorf("Failed to ensure filtering rule for %v: %v", KubePostroutingChain, err)
        return
    }
    // 否则,清除0x4000标记位,防止封包重新遍历网络栈时,被再次SNAT
    // 注意,在这里可以明确知道0x4000被设置,因此可以安全的用XOR将该位取消掉,不需要关心其它位
    // iptables -t NAT -A  KUBE-POSTROUTING  -j MARK --xor-mark=0x4000
    if _, err := kl.iptClient.EnsureRule(utiliptables.Append, utiliptables.TableNAT, KubePostroutingChain,
        "-j", "MARK", "--xor-mark", masqueradeMark); err != nil {
        klog.Errorf("Failed to ensure unmarking rule for %v: %v", KubePostroutingChain, err)
        return
    }
    // ...
}

此外,该PR还对Kube Proxy的iptables/ipvs相关模块进行了类似修改,这里就不张贴代码了。

升级内核

已知内核版本5.6.13, 5.4.41, 4.19.123, 4.14.181修复了上文提到的内核缺陷,但是CentOS 7何时修复未知,可能需要自行Patch。

深入
Checksum

所谓Checksum是一个固定长度的字段,网络协议使用该字段来纠正某些传输错误。

Checksum通常是基于某些报文字段来计算摘要信息,算法决定了Checksum的可靠性和计算成本。IP协议仅仅使用报文头,而大部分L4协议,同时使用报文头、报文体。

在IPv4(IPV6没有IP Checksum)中,IP Checksum是16bit字段,信息来自IP头所有字段。在任一跳发现Checksum错误,都会导致静默的丢弃,而不产生ICMP报文 —— L4协议需要考虑这种静默丢弃的可能并进行相应处理,例如TCP在ACK没有及时收到时会进行重传。

IP数据报在经过每一跳时,都需要更新Checksum,至少TTL的变化需要重新计算Checksum。除了TTL,IP头还可能因为以下原因变化:

  1. NAT导致的地址变化
  2. IP选项处理
  3. IP分片

计算IP Checksum时,报文被分隔为16bit的小段,将这些小段相加并取反(ones-complemented),就得到最后的Checksum。在Linux中,可能分隔为32bit甚至64bit的小段,以提升计算速度,但是取反操作前需要一个额外的折叠(csum_fold)操作。

由于IP Checksum仅仅牵涉到报文头,成本很低,Linux总是在CPU中进行计算,不会Offload给硬件。

L4协议的Checksum牵涉完整报文,包括L4报文头、L4报文体、以及所谓的伪头(pseudoheader)。伪头其实就是IP头中的源地址、目的地址、以及之后的32bit。

IP层在NAT等场景下,需要对IP头进行变更,这会导致L4协议计算的Checksum失效。如果没有更新失效的Checksum,则在IP报文传输的每一跳都不会发现错误,因为中间路由仅仅会校验IP Checksum。结果就是,只有目的地内核才能在L4发现这一情况。我们可以了解到Checksum算法具有可逆性,因此NAT这样导致很少字段变化的情况下,更新Checksum不需要从头计算。

Offloading

前面提到过,L4的Checksum计算涉及完整报文,成本较高。因此Linux支持将L4的Checksum委托给硬件完成,这就是Checksum Offloading。

设备能否支持Checksum Offloading,是通过 net_device->features标记传递给内核的:

  1. NETIF_F_HW_CSUM 驱动能够为任何协议组合、协议层计算IP Checksum
  2. NETIF_F_IP_CSUM 驱动支持L4(仅限于TCP/UDP over IPv4)的Checksum计算
  3. NETIF_F_IPV6_CSUM 驱动支持L4(仅限于TCP/UDP over IPv6)的Checksum计算
  4. NETIF_F_NO_CSUM 表示设备明确知道不需要计算Checksum,通常用于loopback设备
  5. NETIF_F_RXCSUM  驱动进行接收封包的Checksum Offloading,仅仅用于禁用设备的RX Checksum

skb->ip_summed字段存放了Checksum的状态,其含义在接收封包、发送封包期间有所不同。

在接收封包期间:

  1. CHECKSUM_NONE 提示设备没有对封包进行Checksum校验,可能由于缺少相关特性
  2. CHECKSUM_UNNECESSARY 提示内核不再需要对Checksum进行校验
  3. CHECKSUM_COMPLETE 提示设备已经提供了完整的L4 Checksum,L4代码只需要加上伪头即可进行校验

在发送封包期间:

  1. CHECKSUM_NONE 提示内核已经完全处理好Checksum了,设备不需要做任何事情
  2. CHECKSUM_UNNECESSARY 意义和CHECKSUM_NONE相同
  3. CHECKSUM_PARTIAL  提示内核已经完成伪头部分的Checksum,驱动必须计算从 skb->csum_start到封包结尾部分的Checksum,并且将其存放在 skb->csum_start + skb->csum_offset这个位置
  4. CHECKSUM_COMPLETE  不使用

可以看到,在发送封包时,如果skb->ip_summed的值为CHECKSUM_PARTIAL,则意味着内核要求驱动Checksum Offloading。

内核缺陷

基于上面的认识,我们可以看一下本文牵涉到的内核缺陷到底是什么了:

net/netfilter/nf_nat_proto_udp.c
C
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
// linux-3.10.y
static bool
udp_manip_pkt(struct sk_buff *skb,  // 当前操控的套接字缓冲
          const struct nf_nat_l3proto *l3proto,  // 持有NAT操作相关的若干函数指针
          unsigned int iphdroff, unsigned int hdroff,  // IP头、L4头的偏移量
          const struct nf_conntrack_tuple *tuple,  // 连接跟踪相关的信息,新旧IP端口
          enum nf_nat_manip_type maniptype)  // 是SNAT还是DNAT
{
    struct udphdr *hdr;
    __be16 *portptr, newport;
 
    if (!skb_make_writable(skb, hdroff + sizeof(*hdr)))
        return false;
 
    // 获得UDP头
    hdr = (struct udphdr *)(skb->data + hdroff);
 
    if (maniptype == NF_NAT_MANIP_SRC) {
        // NAT后的源端口
        newport = tuple->src.u.udp.port;
        // NAT前的源端口
        portptr = &hdr->source;
    } else {
        /* Get rid of dst port */
        newport = tuple->dst.u.udp.port;
        portptr = &hdr->dest;
    }
    // 如果Checksum不为零, 或者 开启了Offloading,则更新Checksum
    if (hdr->check || skb->ip_summed == CHECKSUM_PARTIAL) {
 
        //       这里调用的是 nf_nat_ipv4_csum_update
        l3proto->csum_update(skb, iphdroff, &hdr->check,
                     tuple, maniptype);
        inet_proto_csum_replace2(&hdr->check, skb, *portptr, newport,
                     0);
        if (!hdr->check)
            hdr->check = CSUM_MANGLED_0;
    }
    *portptr = newport;
    return true;
}
 
static void nf_nat_ipv4_csum_update(struct sk_buff *skb,
                    unsigned int iphdroff, __sum16 *check,
                    const struct nf_conntrack_tuple *t,
                    enum nf_nat_manip_type maniptype)
{
    struct iphdr *iph = (struct iphdr *)(skb->data + iphdroff);
    __be32 oldip, newip;
 
    if (maniptype == NF_NAT_MANIP_SRC) {
        oldip = iph->saddr;
        newip = t->src.u3.ip;
    } else {
        oldip = iph->daddr;
        newip = t->dst.u3.ip;
    }
    // 这里传入了无效的Checksum
    inet_proto_csum_replace4(check, skb, oldip, newip, 1);
}
 
void inet_proto_csum_replace4(__sum16 *sum, struct sk_buff *skb,
                  __be32 from, __be32 to, int pseudohdr)
{
    __be32 diff[] = { ~from, to };
    if (skb->ip_summed != CHECKSUM_PARTIAL) {
        *sum = csum_fold(csum_partial(diff, sizeof(diff),
                ~csum_unfold(*sum)));
        if (skb->ip_summed == CHECKSUM_COMPLETE && pseudohdr)
            skb->csum = ~csum_partial(diff, sizeof(diff),
                        ~skb->csum);
    } else if (pseudohdr)
        // 走这个分支,可以看到,更新Checksum依赖于先前的Checksum是正确值
        *sum = ~csum_fold(csum_partial(diff, sizeof(diff), csum_unfold(*sum)));
}

当VXLAN端点的UDP被NAT的情况下,上述代码会执行。如果 VXLAN设备禁用了UDP Checksum,它会将udphdr->check置零。如果同时VXLAN设备还启用了Tx Checksum Offloading,skb->ip_summed的值就会是CHECKSUM_PARTIAL。这就是我们环境下的配置。

UDP Checksum被禁用情况下,udphdr->check是个零值,显然没有包含旧的伪头的Checksum信息,因为通过伪头计算的Checksum,至少协议类型部分(UDP 0x11)是非零。

因此,判断是否需要更新Checksum,应当只VXLAN接口是否禁用了UDP Checksum,禁用了就不应该更新。

参考
  1. TCP offloading on VXLAN.calico adaptor causing 63 second delays in VXLAN communications node->nodeport or node->clusterip:port
  2. netfilter: nat: never update the UDP checksum when it's 0
  3. kubelet, kube-proxy: unmark packets before masquerading them
← Galaxy学习笔记
2020年10月拈花湾 →
3 Comments On This Topic
  1. 回复
    yanick
    2020/08/20

    这个错误的确是盲人摸象了,要深入协议侧才能明白错误原因。感觉脑壳疼

  2. 回复
    林哲緯
    2020/11/23

    hi, 感謝分享,分析得非常徹底

    想請問一下為什麼前五次都是 bad udp checksum 然而第六次卻突然 no checksum?
    第六次有做什麼變更嗎? 我查了VxLAN RFC 跟 linux networking 相關資料並沒有看到 第六次就不計算 checksum

    • 回复
      Alex
      2020/11/24

      哈哈,我也纠结于此。估计和内核中VXLAN的实现细节有关,没有去深入探寻。

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • 如何在Pod中执行宿主机上的命令
  • Kubernetes故障检测和自愈
  • Istio Mixer与Envoy的交互机制解读
  • Istio Pilot与Envoy的交互机制解读
  • 基于Helm的Kubernetes资源管理

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • Bazel学习笔记 38 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2