UUBlog

UUBlog

引言

老东家的大佬基础架构方面做得比较好,日志打印都比较规范,还封装了日志组件,打印的日志不落盘,直接往 Kafka 送。

这样日志收集就省了很多事情了,性能还好。

健康160就没这么好条件了,因为项目庞大,建设时间跨度很长,技术架构迭代过几次,导致有很多不同语言,不同框架的服务。日志打印也是五花八门。

在开始推进Kubernetes容器化的时候,无可避免地要涉及日志的收集,真正收集的时候,才感觉这五花八门的日志采集起来,有点难受。

我们虚拟机方案是每个服务都配置一下 Logstash 来采集和过滤消息,然后推送到 Kafka 消费。

虚拟机日志流向

每个服务都要修改一下,来适配这个服务,这种刀耕火种的方式,感觉已经和时代脱轨了。我所期待的,是服务发到 k8s 后,就自动完成了日志的采集,不用配置 Logstash 了。加上 Logstash 是 Java 服务,资源占用不小,所以就把眼光瞄向基于 Fluent Bit的 日志Operator。

初探基于 FluentBit 的日志采集Operator

容器化的服务,我们会把日志输出到 stdout,而这些日志,会被容器收集起来放在/var/log/containers/*.log,所以我们只需要收集容器目录的日志就好。

基于 Fluent Bit 来做的 Kubernetes 日志采集的方案中,以 Fluent Operator 和 Logging Operator 最好。

它们默认都是用 Fluentbit 以 Deamonset形式部署,每个容器运行起来后,把宿主机的 /var/log/containers/ 目录挂进来,用 File Input 插件对该目录下的文件进行采集。

当时两个都支持 Fluent Bit 和 fluentd,都差不多。反而 Fluent Operator 的开箱即用做得更好些,基本 helm 简单配置下 values,安装就能用,Logging Operator 还需要一些额外的资源定义的配置才行。

于是就有了这篇文章:K8S日志收集 Fluent Operator 动态索引名的实现 当时还提了两个 PR 来支持这个改动 #1119 #1121

这时候采集大概是这样的,没有中间商赚差价。

k8s 日志流向 1

一开始,小日子看起来还是可以的,fluentbit 负载并不高,常年内存使用在 50mb 左右。

初遇挑战

异常堆栈多行文本合并问题

直到开发反馈说多行堆栈的内容没有合并到同个日志开始,幸福的日子就开始告一段落了。

堆栈日志

于是开始动手,增加了Java MultilineParser 来匹配日志 见这个 PR #1138 ,并顺手修复了没有用匿名结构体,导致生成的资源配置和 CRD 定义的资源不一致的bug MultilineParser achieve an effect similar to embedding by using anonymous structs #1133

经过折腾,发现能采集日志,而且多行采集也没问题,就应用了这个改动。

image-20240719192108605

日志采集暂停问题

没过几天,发现有些节点的日志采集停了,服务没日志了。再看该节点上的 Fluent Bit,相关节点内存占用都比较高,都到了分配的内存最高位。

看 Fluent Bit 的日志,发现

1
[ warn] [input] emitter.59 paused (mem buf overlimit)

当时 buffer 我用的是 memory,而不是 file,其实生产应该用 file 更好,可以分配更大的 buffer,而不用占用昂贵的内存。

想着内存不够,那就加多点,毕竟有时候输入过快,输出速度跟不上是很正常的,先提高内存来解燃眉之急。

回过头来,梳理了下,Fluent Bit buffer 消耗过多,猜测可能会以下几种情况:

  1. 高峰期消息生产实在太快,需要缓存来削峰,停过这段时间即可。
  2. multiline 匹配行还没遇到结束的 flag 的时候,要内存来保存,而大量的多行导致内存占用升高。
  3. 输出到 Elasticsearch 的速度过慢,无法及时消耗 input 的内容,导致内存堆高。

通过iotop -Po 只看进程,并且有 io 的进程,然后箭头左右控制,切换到 DISK WRITE 列 来观察,锁定一些输出高的进程。类似下面这样。

image-20240722113001898

发现在出问题的几台机器,有新容器化项目在运行,输出的日志量是比较多,明显大于其它的机器。

而 multiline 插件的memory buffer 确实会导致内存升高。而且我们多语言环境,有些语言框架输出的多行文本和别人的不太一样,很难有一套正则能涵盖所有语言的多行的起始匹配,可能会导致超长 multiline 的产生。

当时用的 fluent-bit 2.2.2版本,不会因为 buffer 满,而限流输入端,可能还导致日志丢失。见 Issue https://github.com/fluent/fluent-bit/issues/8198 , 3.0.2之后是会限流输入端的。

再看 Fluent Bit 的日志,会发现一些推送到 Elasticsearch 429 状态的日志,基本就是 Elasticsearch 太过于繁忙,日志推送被 reject了,需要重试。

调整方案

至此,问题四面八方都有,四面楚歌,痛定思痛,唯有重新梳理流程,调整日志采集的架构的路子可以走了。

需求整理

整理了下需求如下:

  1. 能监控采集的服务,及时发现采集端的问题。
  2. 能图表化看到采集组件的各项指标,好确定调整是否有效果。
  3. 比较方便地分类服务,按类别应用不同的语言的采集规则。
  4. 提升推送吞吐量,避免日志积压在推送端。

采集流程调整

参考之前虚拟机的方案,调整后的架构如下:

新的日志架构

为什么搞了一个 fluentd 组出来?

主要是想减轻 FluentBit agent 的负载,FluentBit将其作为一个 demonset 的类型服务,一个节点有一个,如果它承担 Input 之外,还承担Filter 和 Output 的职责,很容易出现性能问题。而这个时候不好横向扩张来解决。

虽然可以做到为每个不同服务分组都建立一个 daemonset 的 FluentBit agent,来实现每台机有多个 agent 来处理,但是在现阶段下,一个节点一个 agent 来处理所有的日志 Input是没问题的。

Fluentbit 采集的日志,简单的 Parser 处理后,就直接 Forward 给 Fluentd 的service。fluentd 在这里用的是 StatefulSet类型,因为 buffer,cache之类的持久化存储,为了性能,使用的是 hostPath。

在 filter 和 output 遇到瓶颈的时候,是可以比较容易扩容来实现更高的数据处理的能力的。

日志采集Operator再选型

有了大致的方案和需求,就重新对社区的相关日志方案选型

能力 Fluent Operator Logging Operator
支持 fluent+fluentd方案
有开箱即用的 grafana 面板
比较方便地分类服务,使用不同的采集规则

两个其实差别不大,但是在我额外关注的点上,Logging Operator 要做得更好点。有 CNCF 的孵化,还是有后劲更足些。

Logging Operator有个抽象得不错的资源叫 Flow,可以比较轻松来根据 label 匹配不同的 pod 的日志进行采集。这在后续我区分不同的服务来用不同的规则的时候,起到很大的便利。

举个例子,我要匹配出label language=python 的服务进行使用detectExceptions 插件、record_transformer插件、record_modify 插件处理后,output 到 kafka 的相关 output 的时候,大概如下:

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
apiVersion: logging.banzaicloud.io/v1beta1
kind: Flow
metadata:
name: python-kafka-flow
namespace: prod
spec:
match:
- select:
labels:
language: "python"
# - exclude:
# labels:
# swck-java-agent-injected: "true"
filters:
# - tag_normaliser: 这标签和detectExceptions 互斥
# format: ${kubernetes["labels"]["app"]}
- detectExceptions:
multiline_flush_interval: "0.1"
force_line_breaks: true
languages:
- python
# keep keys
- record_transformer:
enable_ruby: true
records:
- app_name: ${record["kubernetes"]["labels"]["app"]}
- ip: ${record["kubernetes"]["annotations"]["cni.projectcalico.org/podIP"]}
# clean keys
- record_modifier:
remove_keys: stream,logtag,kubernetes

localOutputRefs:
- kafka-output

有了 match,就可以针对不同的服务,使用不同的规则来精准匹配日志。

这里值得提醒的是,Fluentd的 filter 使用的插件越多,处理速度就越慢。比如上面的 detectExceptions 服务就迫使 Fluentd 只能工作在单 worker 模式下,性能大打折扣。

改动小结

让我们再总结一下。

之前的方案,所有的日志都用同一套采集规则,不够灵活。经过整改后我按照语言的维度来划分不同的 Flow,来应用不同的采集规则。

Buffer 和 Cache 从 Memory 改为 File,使用 hostPath 挂载的本地盘,保障写入的性能。

另外 Fluentbit 只承担 Input 和简单的 Parser任务,复杂和性能损耗比较大的 Filter 和耗时的推送交给 可以横向扩容的Fluentd。

Output 原先是直接推送到 ES集群,现在改为推送到 Kafka 集群,我对 Kafka 集群进行了基准测试,写入能力大概在 200mb/s 这个大大超出了我们的output 的流量,哪怕是高峰时期。

另外我们对 Fluentbit 和 Fluentd 的指标进行采集,并通过 Grafana 展示,容易一目了然观测过去和当下的日志采集情况。

图表示例

这样一来,职责清晰,也有较大的弹性来应对未来的流量暴涨。

掐指一算,妥妥的,然而…

新的挑战

Output Buffer 存满

然而在我认为最不可能发生问题的环节,偏偏发生了。Output 到 Kafka 的吞吐量,居然还不如之前的 Elasticsearch!

Output Buffer 很快就又堆满了。

经过排查,发现是 Logging Operator Fluentd内置的 Kafka 插件,默认用的是 ruby-kafka,性能非常差。可以改为用 rdkafka,这个是 c 语言实现的插件,性能比 ruby-Kafka 好很多。

我用的 Logging Operator 当前最新版本是 4.8.0,其用的 fluend:v1.16-4.8-full 镜像没有打包 rdkafka,另外 Logging Operator 并不支持 rdkafka 客户端的相关配置。

问题解决

知道了问题,就好解决了,我从新打包了一个镜像,把 rdkafka 打包进去,并修改了下 Logging Operator Kafka output支持该插件配置,并兼容之前的配置。

相关改动,我也提交到了社区,并被采纳了,不出意外的话,在 4.9.0 版本就可以体验。

具体 PR 见fluentd-images#/142logging-operator#1780

启用rdkafka client也非常简单,在原来的Kafka output 配置上增加 use_rdkafka: true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: logging.banzaicloud.io/v1beta1
kind: Output
metadata:
name: kafka-output
namespace: mirr
spec:
kafka:
use_rdkafka: true #这句开启 rdkafka
brokers: 10.1.2.206:9092
default_topic: k8s-logs
format:
type: json
buffer:
tags: k8s-logs
timekey: 1m
timekey_wait: 30s
timekey_use_utc: true

效果

效果也是非常好,如下图 Output Buffer 的图表,之前 Buffer 到接近 4g(我分配的 Cache 大小就是 4g),在改动应用后,迅速就被消费完了。

截止今日,一个多星期了,依然没有遇到性能瓶颈。

image-20240710103011905

总结

我考察了基于Fluent Bit的Kubernetes日志采集方案,最终在Fluent Operator和Logging Operator之间,选择了Fluent Operator,因为它的开箱即用体验做得更好。

初次实施后,尽管Fluent Bit表现稳定,但我遇到了多行日志合并和日志采集暂停的问题,这让我不得不重新思考解决方案。

我决定将日志处理流程分解,让Fluentbit仅负责日志的采集和简单解析,而将过滤和输出任务交给Fluentd处理,同时通过Kafka作为中介,大幅提高了日志处理的效率和系统的稳定性。

在进一步优化过程中,我重新审视了日志框架的选择,最终决定采用Logging Operator,因为它在我关注的几个方面表现更佳,比如服务个性化配置方面和监控能力。

我根据服务的语言维度来划分不同的Flow,并应用不同的采集规则,这使我能更灵活地管理日志采集流程。

然而,当我将日志输出到Kafka时,又遇到了新的挑战:内置的Kafka插件性能不佳。

为解决这个问题,我自定义了镜像,将Fluentd的Kafka插件替换成性能更强的rdkafka,并成功地向Logging Operator社区提交了这一改进。

这一改动大大提高了日志推送到 Kafka的效率,让整个架构运行得更加顺畅。

最后,希望大家不要忽略了基础架构的标准化建设,统一有时候确实能在方方面面提升效率。

关注公众号 尹安灿

集群Service IP 段变更后(从 10.96.0.0/16 变为 10.17.0.0/16),导致 kubernetes.default.svc 的ClusterIP IP (10.96.0.1)和段范围不一样,对于这个情况,需要重建该 svc。

重建方法很简单,删除该 Service,集群会自动创建一个新的,ClusterIP 为 10.17.0.1 的 service。但是如果你和我一样直接这样操作,那大概率会遇到和我一样的坑。

这个是 集群内的服务访问apiserver 的主要方式。这个时候,几乎集群内需要和 apiserver 交互的功能和服务都不能正常使用。因为它们还是要和以前的 Service IP 进行通讯。

比如 calico-node 就开始出现问题、如创建销毁 pod的过程 要和calico-node通讯。一连串地炸了。

重建 apiserver 和 calico-node 后,发现 calico-node 起不来,发现它还是连接到老的 ClusterIP,于是修改它的环境变量指定正确的 Apiserver IP。

可以通过加入环境变量到 calico-node demonset 中,指定 IP 和端口

1
2
3
KUBERNETES_SERVICE_HOST:是k8s的kubernetes服务的serviceIP
KUBERNETES_SERVICE_PORT: 是k8s的kubernetes服务的端口号
KUBERNETES_SERVICE_PORT_HTTPS:是k8s的kubernetes服务的https端口号

于是编辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kubectl edit daemonset calico-node -n kube-system
# 发现存在以下映射自 configmap 的环境变量kubernetes-services-endpoint,但是 configmap 并不存在
- configMapRef:
name: kubernetes-services-endpoint
# 我就没直接加到demonset里面,创建了对应的configmap
# 创建 ep.yaml 内容如下
apiVersion: v1
data:
KUBERNETES_SERVICE_HOST: 10.1.15.121
KUBERNETES_SERVICE_PORT: "6443"
KUBERNETES_SERVICE_PORT_HTTPS: "6443"
kind: ConfigMap
metadata:
name: kubernetes-services-endpoint
namespace: kube-system

kubectl apply -f ep.yaml

提交后calico-node 能顺利起来了,但是其它服务还是有问题。

上面 10.1.15.121 是我的 apiserver master 的 IP,为什么不写集群内的 Service ClusterIP 呢?当时改过为正确的新 IP,但是还是无法启动,因为证书里没含有新的集群 IP,然后就写这个了。证书当时每个 master 的 api 和负载均衡的域名都加进去了。

它正常后,pod 创建是没问题了,但是一些核心的服务还是有问题的。因为通过 Service 拿到的新 IP 10.17.0.1 不在 apiserver 的证书里面。

查看证书 SANs 命令

1
for i in $(find /etc/kubernetes/pki -type f -name "*.crt");do echo ${i} && openssl x509 -in ${i} -text | grep 'DNS:';done

于是到这一步就需要重新生成集群证书并把新 IP 加进去了。

以下是基于集群是 kubeadm 安装的前提

  1. 编辑 cm 加入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    kubectl edit cm -n kube-system kubeadm-config
    ClusterConfiguration: |
    apiServer:
    certSANs:
    - 127.0.0.1
    - apiserver.cluster.local
    - 10.103.97.2
    - 10.1.15.121
    - 10.1.15.122
    - 10.1.15.123
    - 10.17.0.1
  2. 备份和删除本地证书

    1
    2
    3
    cp -r /etc/kubernetes /etc/kubernetes-bak
    rm -rf /etc/kubernetes/pki/{apiserver*,front-proxy-client*}
    rm -rf /etc/kubernetes/pki/etcd/{healthcheck*,peer*,server*}
  3. 生成新的证书

    1
    kubeadm init phase certs all --config kubeadmin-config.yaml
  4. 在每个 master node 重复 2~3 步骤

  5. 完成后,重建 apiserver 后 集群就正常了

总结

换 IP 段后,要及时生成新的 apiserver 的 Service ClusterIP 到证书 SANs 里。然后再重建 kubernetes.default.svc 。最后我把 kubernetes-services-endpoint configmap 的 IP 改为正确的 Service cluster IP 并重建了 calico-node。

关注公众号 尹安灿

解决 DNS conntrack 塞满 导致查询超时的问题

这个其实除了能解决 dns 塞满,还可以解决 ipv6 和 ipv4 并发解析, 落到同一个 coredns 的时候 conntrack 冲突导致丢包的问题。

IPVS 模式配置 node-local-dns

我们kube-proxy 用的是 IPVS mode

保存该文件 https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml

内容如下:

registry.k8s.io/dns/k8s-dns-node-cache:1.23.0 建议保存替换成自己仓库的

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
# Copyright 2018 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

apiVersion: v1
kind: ServiceAccount
metadata:
name: node-local-dns
namespace: kube-system
labels:
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
---
apiVersion: v1
kind: Service
metadata:
name: kube-dns-upstream
namespace: kube-system
labels:
k8s-app: kube-dns
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/name: "KubeDNSUpstream"
spec:
ports:
- name: dns
port: 53
protocol: UDP
targetPort: 53
- name: dns-tcp
port: 53
protocol: TCP
targetPort: 53
selector:
k8s-app: kube-dns
---
apiVersion: v1
kind: ConfigMap
metadata:
name: node-local-dns
namespace: kube-system
labels:
addonmanager.kubernetes.io/mode: Reconcile
data:
Corefile: |
__PILLAR__DNS__DOMAIN__:53 {
errors
cache {
success 9984 30
denial 9984 5
}
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
health __PILLAR__LOCAL__DNS__:8080
}
in-addr.arpa:53 {
errors
cache 30
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
ip6.arpa:53 {
errors
cache 30
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__CLUSTER__DNS__ {
force_tcp
}
prometheus :9253
}
.:53 {
errors
cache 30
reload
loop
bind __PILLAR__LOCAL__DNS__ __PILLAR__DNS__SERVER__
forward . __PILLAR__UPSTREAM__SERVERS__
prometheus :9253
}
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-local-dns
namespace: kube-system
labels:
k8s-app: node-local-dns
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
spec:
updateStrategy:
rollingUpdate:
maxUnavailable: 10%
selector:
matchLabels:
k8s-app: node-local-dns
template:
metadata:
labels:
k8s-app: node-local-dns
annotations:
prometheus.io/port: "9253"
prometheus.io/scrape: "true"
spec:
priorityClassName: system-node-critical
serviceAccountName: node-local-dns
hostNetwork: true
dnsPolicy: Default # Don't use cluster DNS.
tolerations:
- key: "CriticalAddonsOnly"
operator: "Exists"
- effect: "NoExecute"
operator: "Exists"
- effect: "NoSchedule"
operator: "Exists"
containers:
- name: node-cache
image: registry.k8s.io/dns/k8s-dns-node-cache:1.23.0
resources:
requests:
cpu: 25m
memory: 5Mi
args: [ "-localip", "__PILLAR__LOCAL__DNS__,__PILLAR__DNS__SERVER__", "-conf", "/etc/Corefile", "-upstreamsvc", "kube-dns-upstream" ]
securityContext:
capabilities:
add:
- NET_ADMIN
ports:
- containerPort: 53
name: dns
protocol: UDP
- containerPort: 53
name: dns-tcp
protocol: TCP
- containerPort: 9253
name: metrics
protocol: TCP
livenessProbe:
httpGet:
host: __PILLAR__LOCAL__DNS__
path: /health
port: 8080
initialDelaySeconds: 60
timeoutSeconds: 5
volumeMounts:
- mountPath: /run/xtables.lock
name: xtables-lock
readOnly: false
- name: config-volume
mountPath: /etc/coredns
- name: kube-dns-config
mountPath: /etc/kube-dns
volumes:
- name: xtables-lock
hostPath:
path: /run/xtables.lock
type: FileOrCreate
- name: kube-dns-config
configMap:
name: kube-dns
optional: true
- name: config-volume
configMap:
name: node-local-dns
items:
- key: Corefile
path: Corefile.base
---
# A headless service is a service with a service IP but instead of load-balancing it will return the IPs of our associated Pods.
# We use this to expose metrics to Prometheus.
apiVersion: v1
kind: Service
metadata:
annotations:
prometheus.io/port: "9253"
prometheus.io/scrape: "true"
labels:
k8s-app: node-local-dns
name: node-local-dns
namespace: kube-system
spec:
clusterIP: None
ports:
- name: metrics
port: 9253
targetPort: 9253
selector:
k8s-app: node-local-dns

安装:

169.254.20.10 写死用这个杜撰的 IP即可,生产和镜像都一样

1
2
3
4
5
kubedns=`kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}`
domain="cluster.local"
localdns="169.254.20.10"
sed -i "s/__PILLAR__LOCAL__DNS__/$localdns/g; s/__PILLAR__DNS__DOMAIN__/$domain/g; s/,__PILLAR__DNS__SERVER__//g; s/__PILLAR__CLUSTER__DNS__/$kubedns/g" nodelocaldns.yaml
kubectl apply -f nodelocaldns.yaml

验证:

进入一个 pod,把/etc/resolv.conf 的 name server 改为169.254.20.10。然后 nslookup 看服务和其它域名解析正常即可。

配置 DNS优先使用 node-local-dns

主要有两种:

1. 全局生效,修改 kubelet 指定的 cluster dns

1
2
3
4
5
6
7
8
9
10
11
12
kubedns=`kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}`
sed -i "s/$kubedns/169.254.20.10/g" /var/lib/kubelet/config.yaml
# --
# 验证 改为 169.254.20.10 了
# cat /var/lib/kubelet/config.yaml|grep -A 3 DNS
#clusterDNS:
#- 169.254.20.10
#clusterDomain: cluster.local
#configMapAndSecretChangeDetectionStrategy: Watch
# ---
systemctl restart kubelet
systemctl status -l kubelet

2.在 pod 的 dnsConfig 指定

1
2
3
4
5
6
7
8
9
10
11
dnsConfig:
nameservers:
- 169.254.20.10
searches:
- test.svc.cluster.local
- svc.cluster.local
- cluster.local
- nykjsrv.cn
options:
- name: ndots
value: '5'

参考

关注公众号 尹安灿

在单台 Nginx 负载能力冗余的情况,没必要申请更多资源做负载均衡,只需要保障高可用即可。 此配置为了应对 nginx单台机器 崩溃,或者重启 导致服务不可用的时候,能自动切换到备用的 nginx。两台 nginx 理应部署在不同的物理机,避免物理机重启的时候彻底不可用。

根据目前医院情况,Nginx 有两台,分别是 100.100.100.245 和 100.100.100.247,那寻找一个还未被使用的同网段 ip,这里假设是 100.100.100.246 还未被使用。 未禁止 ping 的时候 ping 100.100.100.246 是没有回应的。

那目前我们掌握的IP信息大概如下:

- Nginx master: 100.100.100.245

- Nginx slave: 100.100.100.247

- Virtual IP(VIP): 100.100.100.246

配置Keepalived

在Nginx master: 100.100.100.245 配置

执行以下命令安装 keepalived

1
yum install -y keepalived

配置 nginx 健康检查脚本。该脚本主要实现以下功能

  1. 如果不存在 nginx 进程,则尝试启动 nginx 进程。这里我们是用 systemd 管理的 nginx,所以用 systemctl start nginx ,如果是别的方式启动,请写具体的启动命令。
  2. 如果依然启动失败 则杀死 keepalived 进程 使得 VIP 立马飘走。不用等待权重慢慢减少再飘走。
1
2
3
4
5
6
7
8
9
10
11
cat <<EOF> /usr/local/src/nginx_check.sh
#! /bin/bash
A=`ps -C nginx --no-header | wc -l`
if [ $A -eq 0 ];then
systemctl start nginx
sleep 2
if [ `ps -C nginx --no-header| wc -l` -eq 0 ];then
killall keepalived
fi
fi
EOF

配置 keepalived

以下脚本注意修改以下几个点

  • 网卡设备名 我这里是 eth0 ,可以使用 ip a 这个命令查看,如果不是 eth0,则修改 interface 后面的 eth0 为你正确的设备名。
  • VIP 我们前面选定的 VIP 是 100.100.100.246
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
cat <<EOF> /etc/keepalived/keepalived.conf 
global_defs {
router_id LVS_Main
}

vrrp_script chk_nginx {
script "/usr/local/src/nginx_check.sh"
interval 2
weight 2
}

vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 160
priority 100
advert_int 1
authentication {
auth type PASS
auth pass 1111
}
track_script {
chk_nginx
}
virtual_ipaddress {
100.100.100.246/24
}
}
EOF

启动 keepalived 并设置开机启动

1
systemctl enable --now keepalived

在 Nginx slave: 100.100.100.247 配置

执行以下命令安装 keepalived

1
yum install -y keepalived

配置 nginx 健康检查脚本

1
2
3
4
5
6
7
8
9
10
11
cat <<EOF> /usr/local/src/nginx_check.sh
#! /bin/bash
A=`ps -C nginx --no-header | wc -l`
if [ $A -eq 0 ];then
systemctl start nginx
sleep 2
if [ `ps -C nginx --no-header| wc -l` -eq 0 ];then
killall keepalived
fi
fi
EOF

配置 keepalived

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
cat <<EOF> /etc/keepalived/keepalived.conf 
global_defs {
router_id LVS_Slave
}

vrrp_script chk_nginx {
script "/usr/local/src/nginx_check.sh"
interval 2
weight 2
}

vrrp_instance VI_1 {
state SLAVE
interface eth0
virtual_router_id 160
priority 10
advert_int 1
authentication {
auth type PASS
auth pass 1111
}
track_script {
chk_nginx
}
virtual_ipaddress {
100.100.100.246/24
}
}
EOF

启动 keepalived 并设置开机启动

1
systemctl enable --now keepalived

检查配置效果

确认两台机的 nginx 和 keepalived 都启动

1
2
systemctl status nginx
systemctl status keepalived

检查当前 VIP 是否绑定

目前 master (100.100.100.245)的priority 比较高,所以用命令 ip a 查看的时候,能看到VIP(100.100.100.246) 被绑在对应的网卡

1
ip a

nginx 停止的时候能被 keepalived 再度拉起

1
2
3
systemctl stop nginx
# 过几秒再查看 nginx 是否有启动
systemctl status nginx

测试 VIP 是否能飘走

1
2
3
4
5
6
7
# 在master
systemctl stop keepalived
# 停止后 看VIP是否没有了
ip a

# 在slave上查看 VIP 是否绑定
ip a

注意事项

  • 修改配置后一定要 ·nginx -t· 测试没问题后再 reload
  • 理应注意两台机器的配置保持同步。

简单点的话,在配置了 ssh master 上免密登录 slave 后。可以采用 rsync 命令同步配置

1
2
3
rsync -avz --delete /etc/nginx/ user@100.100.100.247:/etc/nginx/
# 重载 slave nginx 配置
ssh root@100.100.100.247 'systemctl reload nginx'

关注公众号 尹安灿

兄弟们,我又来亏钱了

今天介绍亏钱工具 freqtrade macos 下的安装

下载 freqtrade

1
git clone https://github.com/freqtrade/freqtrade.git

安装并激活python 环境

1
2
conda create --name freqtrade python=3.11
conda activate freqtrade

安装依赖

1
2
3
4
./build_helpers/install_ta-lib.sh
python3 -m pip install --upgrade pip
python3 -m pip install -r requirements.txt
python3 -m pip install -e .

测试

1
2
3
4
5
> freqtrade create-userdir --userdir user_data
2024-02-28 18:28:45,361 - freqtrade - INFO - freqtrade 2024.3-dev-d1028b8ca
2024-02-28 18:28:45,362 - freqtrade.configuration.directory_operations - INFO - Created user-data directory: user_data

# 能看到这个输出 基本问题不大 可以准备亏钱之旅了

安装文档

https://www.freqtrade.io/en/stable/installation/#freqtrade-install-conda-environment

关注公众号 尹安灿

近日有需求对 es 进行基准测试,来和华为云的产品来做个横向对比。简单记录下过程,以便未来能复用。

本次测试主要横向对比本地的机器、华为云 CSS、华为云上自建 ES 集群的性能。

系统均基于 centos7.x

Esrally初始化

安装 python3.9

1
2
3
4
5
6
7
yum install conda
conda create -n py39 python=3.9
conda init bash
## conda 初始化 bash 后 要 relogin 一次
conda activate py39
yum install pbzip
python3 -m pip install --user esrally

安装高版本git

esrally 有些操作依赖 git 来 fetch 它自己的一些资源

1
2
3
yum remove git-*
yum install https://packages.endpointdev.com/rhel/7/os/x86_64/endpoint-repo.x86_64.rpm
yum install git

离线下载好要准备的 track

为了不在测试开始的时候 在网络上耗费太多时间在等待下载测试数据,所以推荐先离线下载好相关数据。

1
2
3
4
5
6
7
8
9
10
esrally list tracks
# 列出可用的 tracks
这里选华为云给出的 benchmark 数据所用的数据集 geonames
curl -O https://raw.githubusercontent.com/elastic/rally-tracks/master/download.sh
chmod a+x download.sh
./download.sh geonames
tar -xf rally-track-data-geonames.tar
# 这就离线好了
ll ~/.rally/benchmarks/tracks/default/geonames/
# 可以查看track 的内容

esrally一些实用的用法

1
2
3
4
5
6
7
8
9
10
# 对已存在的集群进行 benchmark 并调整测试的索引分片数为 6 保存结果到 race.md --track-params 可以修改 track 参数 覆盖默认值
nohup esrally race --track=geonames --pipeline=benchmark-only --target-hosts=10.8.58.101:9200,10.8.58.102:9200,10.8.58.103:9200 --track-params="number_of_shards:6" --challenge=append-no-conflicts --on-error=abort --report-file=~/race.md > race.log &

# 对比两次 race 的结果
esrally list races
esrally compare --baseline a64a2e46-9cf9-4961-9cee-d793608c9de8 --contender ee79d74e-83ad-4124-9a2b-e902d742b387

# 如果在不同的机器进行的基准测试,想在一台机上对比
ll .rally/benchmarks/races/
把这个目录下对应的 race 的文件夹 拷贝到同个机器这个目录就可以了。

常见问题

有时候结果中并没有出现 ·index-append· 的数据,可以更改 ~.rallly/benchmarks/tracks/default/<对应的 track>/challenges/default.json index-append warmup-time-period 的值,从120 调整为 20

如 geonames track 的

1
2
3
4
5
6
7
8
vim ~/.rally/benchmarks/tracks/default/geonames/challenges/default.json
...
{
"operation": "index-append",
"warmup-time-period": 20,
"clients": {{bulk_indexing_clients | default(8)}},
"ignore-response-error-level": "{{error_level | default('non-fatal')}}"
},
Read more »

关注公众号 尹安灿

日志收集,考虑到我们没有主动上报日志,格式也不太统一,容器化的时候,对于我们而言采集容器标准输出的日志是更快捷达到目的的方式。

大致对比了filebeat、fluentd、fluentbit、vector、logstash后,锁定了flientbit。

虽然它功能不是最强大的,但是基本功能能满足我们的业务需求,采集输入端多样化,也能做简单的filter,支持output的端也很丰富。其次,它吃资源比较少,性能也不错。

我们只需要它收集标准输出的日志,输出到es即可。

而在k8s中,有kubesphere共献给fluent的fluent-operator。该opeator提供了fluentbit only、fluentd only、fluentbit+fluentd 这几种采集方式。对于我们而言,就fluentbit 就够了。

我选择了helm方式安装,下载了它chart (https://github.com/fluent/fluent-operator/releases/download/v2.2.0/fluent-operator.tgz)包,解压到`/app/fluent-operator/ `,直接修改里面values.yaml,这样好方便未来管理自己的部署配置。

简单按照文档,修改了fluentbit的input和output后,直接安装即可

1
2
helm upgrade --install fluent-operator --create-namespace -n fluent /app/fluent-operator/ --set containerRuntime=containerd

按照服务名区分索引名

日志是能收集了,但是所有的服务的日志,当天都放到一个索引,类似 “k8s-logs-<yyyy.mm.dd>” 这样的索引中。本地还好,如果生产环境的话,那一天单个索引就十分庞大了。而且对于不同服务日志保留天数做差异化保留的时候也不好处理,也不好直观展示各个服务日志大小。

所以能按照k8s-<service-name>-<yyyy.mm.dd>格式的话,对我们会更为理想点。截止目前的版本,没有现成的配置可以实现这个。经过研究,可以利用fluentbit的 logstash_prefix_key 这个来实现。logstash_prefix是固定的前缀,logstash_prefix_key则可以动态读取 key来作为索引名。

1.修改fluent-operator,让其支持logstash_prefix_key

经过测试,fluent-operator当前版本的模板并没有对该字段进行处理,所以要进行修改让它支持这个字段。

我需要output到es,所以我就修改了es相关的output,其它输出源的可以自己检查下。

修改 ./templates/fluentbit-output-elasticsearch.yaml

在Values.fluentbit.output.es.logstashPrefix之后增加 .Values.fluentbit.output.es.logstashPrefixKey 这段的配置,如下:

1
2
3
4
5
6
{{- if .Values.fluentbit.output.es.logstashPrefix }}
logstashPrefix: {{ .Values.fluentbit.output.es.logstashPrefix | default "ks-logstash-log" | quote }}
{{- end }}
{{- if .Values.fluentbit.output.es.logstashPrefixKey }}
logstashPrefixKey: {{ .Values.fluentbit.output.es.logstashPrefixKey | default "ks-logstash-log-key" | quote }}
{{- end }}

2.让它能获取app name作为服务名

./values.yaml 修改fluentbit部分,开启kubernetes labels,因为labels的app标签就是我们的服务名。

1
2
3
4
5
6
filter:
kubernetes:
enable: true
labels: true


3. 拼接索引名

由于logstash_prefix_key只能接受 key,且不支持嵌套的对象的key。

举个直观的例子,假设你的对象是这样的:

1
2
3
4
5
6
7
8
{
"file": "systemd.log",
"kubernetes": {
"labels": {
"app": "demo-service"
}
}
}

k8s-$kubernets["labels"]["app"] 和 像 $kubernets["labels"]["app"] 这样的值,是取不到任何内容的。像上面的例子,只接受 $file 这个值。

所以这里要实现我们的目标,我们得自己拼接出一个新的key,作为索引名。类似下面:

1
2
3
4
5
6
7
8
9
{
"file": "systemd.log",
"app_name": "k8s-demo-service",
"kubernetes": {
"labels": {
"app": "demo-service"
}
}
}

我是这样实现的,利用lua的filter,通过取出kubernetes的labels,拼接新的值。

我修改的是 cat ./templates/fluentbit-containerd-config.yaml 增加了一个 add_k8s_app_name_field 函数

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
{{- if .Values.Kubernetes -}}
{{- if .Values.fluentbit.enable -}}
{{- if .Values.fluentbit.filter.containerd.enable -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: fluent-bit-containerd-config
data:
containerd.lua: |
function containerd( tag, timestamp, record)
if(record["logtag"]~=nil)
then
timeStr = os.date("!*t", timestamp["sec"])
t = string.format("%4d-%02d-%02dT%02d:%02d:%02d.%sZ",
timeStr["year"], timeStr["month"], timeStr["day"],
timeStr["hour"], timeStr["min"], timeStr["sec"],
timestamp["nsec"]);
record["time"] = t;
record["log"] = record["message"];
record["message"] = nil;
return 1, timestamp, record
else
return 0,timestamp,record
end
end

function add_k8s_app_name_field(tag, timestamp, record)
retcode = 0
prefix = 'k8s'
app_name = record['kubernetes']['labels']['app']
if app_name ~= nil then
app_name = prefix .. '-' .. app_name

if app_name ~= nil then
record['app_name'] = app_name
retcode = 2
end
end

return retcode, timestamp, record
end
{{- end }}
{{- end }}
{{- end }}

修改 templates/fluentbit-clusterfilter-kubernetes.yaml 增加新增的lua filter函数 对kubernetes标签进行处理

cat templates/fluentbit-clusterfilter-kubernetes.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
{{- if .Values.Kubernetes -}}
{{- if .Values.fluentbit.enable -}}
{{- if .Values.fluentbit.filter.kubernetes.enable -}}
apiVersion: fluentbit.fluent.io/v1alpha2
kind: ClusterFilter
metadata:
name: kubernetes
labels:
fluentbit.fluent.io/enabled: "true"
fluentbit.fluent.io/component: logging
spec:
match: kube.*
filters:
- kubernetes:
kubeURL: https://kubernetes.default.svc:443
kubeCAFile: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
kubeTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
{{- $params := omit .Values.fluentbit.filter.kubernetes "enable" }}
{{- if .Values.fluentbit.output.stdout.enable }}
{{- $_ := set $params "k8sLoggingExclude" true -}}
{{- end }}
{{- with $params }}
{{- . | toYaml | nindent 6 }}
{{- end }}
- lua:
script:
key: containerd.lua
name: fluent-bit-containerd-config
call: add_k8s_app_name_field
timeAsTable: true
- nest:
operation: lift
nestedUnder: kubernetes
addPrefix: kubernetes_
- modify:
rules:
- remove: stream
- remove: kubernetes_pod_id
- remove: kubernetes_docker_id
- remove: kubernetes_container_hash
- remove: kubernetes_labels
- nest:
operation: nest
wildcard:
- kubernetes_*
nestUnder: kubernetes
removePrefix: kubernetes_
{{- end }}
{{- end }}
{{- end }}

主要增加了

1
2
3
4
5
6
- lua:
script:
key: containerd.lua
name: fluent-bit-containerd-config
call: add_k8s_app_name_field
timeAsTable: true

到这里基本就满足所有的条件了,为了不影响systemd的日志收集和归类,也给它的lua filter增加app_name字段.

./templates/fluentbit-lua-config.yaml

1
new_record["app_name"] = "systemd"

经过前面的修改后,只要在values.yaml 设置 fluentbit.output.es.logstashPrefixKey=”$app_name” 即可

4. 应用变更

我修改过的values.yaml,去除了无关部分后主要如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Kubernetes: true
fluentbit:
crdsEnable: true
enable: true
image:
repository: "hub.xxxx.com/library/fluent-bit"
tag: "v2.0.11"
input:
tail:
enable: true
refreshIntervalSeconds: 10
memBufLimit: 50MB
path: "/var/log/containers/*.log"
skipLongLines: false
systemd:
enable: true
path: "/var/log/journal"
includeKubelet: true
output:
es:
enable: true
# 如果多个host的话,用hosts
host: "es.xxx.local"
port: 9200
logstashFormat: true
logstashPrefixKey: "$app_name"
filter:
kubernetes:
enable: true
labels: true
annotations: false
k8sLoggingExclude: true
containerd:
enable: true
systemd:
enable: true

再来helm更新一下,搞定

1
helm upgrade --install fluent-operator --create-namespace -n fluent /app/fluent-operator/ --set containerRuntime=containerd

效果如下:

1
2
3
4
yellow open k8s-kiali-2023.05.10                iWEHK0gKR6GoPKjO3gH6Eg 1 1      30      0 105.9kb 105.9kb
yellow open k8s-nginx-deploy-2023.05.10 OCzPIKWgRneSn27J-o4k9g 1 1 16 0 76.3kb 76.3kb
yellow open k8s-istiod-2023.05.10 GlA1dI7aQDqPFD-5ZMX6iw 1 1 797 0 329.5kb 329.5kb
yellow open k8s-reviews-2023.05.10 41Ifiq3cTQGBKdJ4Fn2MJQ 1 1 18 0 86.6kb 86.6kb

关注公众号 尹安灿

背景是我们希望能在k8s中通过DNS方式,访问服务的FQDN来调用虚拟机注册到nacos的服务。

我们vm和k8s的网段配置了相关路由能相互访问

之前nacos有维护了一个同步去coredns的项目,但是年久失修,支持的nacos版本和coredns版本都不高。后面在官方文档找资料的时候,发现nacos是支持istio MCP协议的 Pilot MCP协议介绍,于是采取这个方案来完成目标。《Nacos 1.1.4发布,业界率先支持Istio MCP协议

环境:

istio: 1.10

nacos: 2.1.0

配置nacos开启MCP Server

进入nacos配置目录,执行以下命令,把 nacos.istio.mcp.server.enabled 值设置为 true。重启nacos server 让它运行MCP Server。

1
[root@dev_10.1.10.209 nacos]#sed -i 's/nacos.istio.mcp.server.enabled=false/nacos.istio.mcp.server.enabled=true/g' conf/application.properties

重启后nacos MCP server会监听 18848 端口

1
2
3
[root@dev_10.1.10.209 nacos]#lsof -i:18848
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 98108 root 171u IPv4 834348182 0t0 TCP *:18848 (LISTEN)

配置istio添加MCP server sources

我测试的nacos server所在的服务器IP10.1.10.209

nacos MCP server监听的端口 18848

编辑istio的configmap,添加以下配置

1
kubectl edit -n istio-system cm istio
1
2
configSources:
- address: xds://10.1.10.209:18848

添加后的配置大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
data:
mesh: |-
accessLogFile: /dev/stdout
configSources:
- address: xds://10.1.10.209:18848
defaultConfig:
discoveryAddress: istiod.istio-system.svc:15012
proxyMetadata: {}
tracing:
zipkin:
address: zipkin.istio-system:9411
enablePrometheusMerge: true
rootNamespace: istio-system
trustDomain: cluster.local
meshNetworks: 'networks: {}'

重启下istiod 连接MCP server同步信息

1
kubectl rollout restart -n istio-system deployment istiod
Read more »

关注公众号 尹安灿

Istio EnvoyFilter+Lua 简单实现动态路由转发

因为种种原因,我们需要实现一个将服务名和subset携带在http请求的header里面,根据这个来实现将服务转发去特定的istio的服务的subset。

比如以下example:

  • 携带 service: msg-group tag: gray 的话,将其路由去msg-group 的gray subset。

    该版本返回数据: Call Read from msg-group,By New version!!!!

  • 携带 service: msg-group tag: default 的话,将其路由去msg-group 的default subset。

    改版本返回数据: Call Read from msg-group

用istio的Virtual Service来实现

大概如下:

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
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: onroute-com
namespace: develop
spec:
gateways:
- msggroup-gateway # 这个gateway - '*'
hosts:
- onroute.com
http:
- match:
- headers:
service:
exact: msg-group
tag:
exact: gray
route:
- destination:
host: msg-group.develop.svc.cluster.local
subset: gray
headers:
response:
set:
test: This value added By static route to gray
- match:
- headers:
service:
exact: msg-group
route:
- destination:
host: msg-group.develop.svc.cluster.local
subset: default
headers:
response:
set:
test: This value added By static route default

请求构造及结果如下:

注:10.99.20.74 是istio-ingressgateway的IP

请求 msg-group 的 gray 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# curl -v -H "Host: onroute.com" -H "service: msg-group" -H "tag: gray" 10.99.20.74/read
* Trying 10.99.20.74:80...
* Connected to 10.99.20.74 (10.99.20.74) port 80 (#0)
> GET /read HTTP/1.1
> Host: onroute.com
> User-Agent: curl/7.77.0
> Accept: */*
> service: msg-group
> tag: gray
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-type: text/plain;charset=UTF-8
< content-length: 43
< date: Fri, 16 Sep 2022 07:36:35 GMT
< x-envoy-upstream-service-time: 7
< server: istio-envoy
< test: This value added By static route to gray
<
* Connection #0 to host 10.99.20.74 left intact
Call Read from msg-group,By New version!!!!%

请求 msg-group 的 default 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
curl -v -H "Host: onroute.com" -H "service: msg-group" -H "tag: default" 10.99.20.74/read
* Trying 10.99.20.74:80...
* Connected to 10.99.20.74 (10.99.20.74) port 80 (#0)
> GET /read HTTP/1.1
> Host: onroute.com
> User-Agent: curl/7.77.0
> Accept: */*
> service: msg-group
> tag: default
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-type: text/plain;charset=UTF-8
< content-length: 24
< date: Fri, 16 Sep 2022 07:38:41 GMT
< x-envoy-upstream-service-time: 12
< server: istio-envoy
< test: This value added By static route default
<
* Connection #0 to host 10.99.20.74 left intact
Call Read from msg-group%

能实现问题,但是有几个小问题

  1. 配置不够灵活,如果面对几百个服务,配置是个巨大的工作量。
  2. 不同的namespace要配置不同的规则。

用Envoy Filter + Lua来实现

针对上面提到的问题,如果用EnvoyFiltter来实现动态的路由的话,就可以解决这个问题。

解决思路都是围绕config.route.v3.RouteAction cluster_header 这个字段来的

(string) Envoy will determine the cluster to route to by reading the value of the HTTP header named by cluster_header from the request headers. If the header is not found or the referenced cluster does not exist, Envoy will return a 404 response.

  1. 设置route的cluster_header字段为 x-service
  2. 从header读取service和tag的值组合出upstream cluster格式,并将x-service头替换为它
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
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: onroute-routing
namespace: istio-system
spec:
workloadSelector:
labels:
istio: ingressgateway
configPatches:
- applyTo: VIRTUAL_HOST
match:
context: GATEWAY
patch:
operation: ADD
value:
name: dynamic_routing
domains:
- 'onroute.com'
- 'onroute.com:*'
routes:
- name: "path-matching"
match:
prefix: "/"
route:
cluster_header: "x-service"
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: "envoy.http_connection_manager"
subFilter:
name: "envoy.router"
patch:
operation: INSERT_BEFORE
value:
name: envoy.service-helper
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
inlineCode: |
function envoy_on_request(request_handle)
local headers = request_handle:headers()
local mesh_service = headers:get("service")
local tag = headers:get("tag")
if mesh_service ~= nil then
if tag ~= nil then
request_handle:headers():replace("x-service", "outbound|80|" .. tag .. "|" .. mesh_service .. ".svc.cluster.local")
end
end
end

function envoy_on_response(response_handle)
response_handle:headers():add("test", "from envoy filter response")
end

下面是测试结果:

请求 msg-group的 gray 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
curl -v -H "Host: onroute.com" -H "service: msg-group.develop" -H "tag: gray" 10.99.20.74/read
* Trying 10.99.20.74:80...
* Connected to 10.99.20.74 (10.99.20.74) port 80 (#0)
> GET /read HTTP/1.1
> Host: onroute.com
> User-Agent: curl/7.77.0
> Accept: */*
> service: msg-group.develop
> tag: gray
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-type: text/plain;charset=UTF-8
< content-length: 43
< date: Fri, 16 Sep 2022 08:07:29 GMT
< x-envoy-upstream-service-time: 16
< server: istio-envoy
< test: from envoy filter response
<
* Connection #0 to host 10.99.20.74 left intact
Call Read from msg-group,By New version!!!!%

请求 msg-group的 default 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
curl -v -H "Host: onroute.com" -H "service: msg-group.develop" -H "tag: default" 10.99.20.74/read
* Trying 10.99.20.74:80...
* Connected to 10.99.20.74 (10.99.20.74) port 80 (#0)
> GET /read HTTP/1.1
> Host: onroute.com
> User-Agent: curl/7.77.0
> Accept: */*
> service: msg-group.develop
> tag: default
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-type: text/plain;charset=UTF-8
< content-length: 24
< date: Fri, 16 Sep 2022 08:12:40 GMT
< x-envoy-upstream-service-time: 14
< server: istio-envoy
< test: from envoy filter response
<
* Connection #0 to host 10.99.20.74 left intact
Call Read from msg-group%

参考资料

关注公众号 尹安灿

优秀的容量治理 不但能解决当前问题,还能防患于未然

扩容

  • 扩容避免盲目,可以作为应急手段,避免滥用和无脑用
  • 考虑对底层资源的影响 (中间件、数据库、缓存这类、连接数、长连接数之类的)
  • 建立谨慎扩容风气
  • 优化程序为主(异步、读写分离、增加缓存、代码和SQL调优等等)

限流

常见限流策略

  • 流量整形: 指不管流量到达的速率多么不稳定,在接收流量后,都将其匀速输出的过程,即“乱进齐出”。
  • 容忍突发流量: 指的是限流策略允许流量在短时间内突增,且在突增结束后不会影响后续流量的正常限流。
  • 平滑限流: 指的是在限流周期内流量分布均匀,比如限制 10 秒内请求次数不超过 1000,平滑限流应做到分摊到每秒不超过 100 次请求。反之,不平滑限流有可能在第 1 秒就请求了 1000 次,后面 9 秒无法再发出任何请求。
限流策略 流量整形 容忍突发流量 平滑限流 实现复杂度
固定窗口 不支持 不支持 不支持
滑动窗口 不支持 不支持 不支持
漏桶算法 支持 不支持 支持
令牌桶算法 支持 支持 支持

限流应当采用分治的思想

按位置划分的话,可以分为:

  1. 网关层限流|入口限流 静态资源走CDN,仅放过合法的请求,针对IP或者域名粗放式进行限流。
  2. 接入层限流 | 粒度比网关细,主要手段有负载均衡和限流措施,分摊服务压力。
  3. 应用层限流 | 有自己或者单机的限流, 常用的微服务相关的容量治理主要是在这个环节,针对自己,集群,第三方调用的治理。
  4. 数据库层限流 | 主要采取集群、读写分离等方法分摊压力。

和搞安全类似的想法,“一切输入都是有害的”,不信任上层,要有自己的一套治理方法。

Read more »

关注公众号 尹安灿

0%