CICD搭建完成之后又迎來新的問題,鏈路追蹤、日志、監(jiān)控告警、在線調(diào)試、服務(wù)更新策略,先從鏈路追蹤說起。
鏈路追蹤
鏈路追蹤的話有很多選項,比如zipkin、skywalking等,由于我們公司是基于java的技術(shù)路線的,所以我選擇了skywalking來做鏈路追蹤,通過sidecar的方式將其無感的注入到服務(wù)中。
skywalking可以通過helm直接安裝,具體安裝流程就不詳細(xì)說明了,我是把監(jiān)控告警這些工具全都裝在了monitoring的namespace中。
## deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-backend
labels:
app: service-backend
spec:
replicas: 1
selector:
matchLabels:
app: service-backend
template:
metadata:
labels:
app: service-backend
spec:
initContainers:
- name: init-skywalking-agent
image: skywalking-java-agent:8.7.0
command: [ "/bin/sh" ]
args: [ "-c", "cp -R /skywalking/agent /agent" ]
volumeMounts:
- mountPath: /agent
name: skywalking-agent
containers:
- name: service-backend
image: service-backend:latest
env:
- name: SW_AGENT_COLLECTOR_BACKEND_SERVICES
value: skywalking-oap.monitoring.svc:11800
- name: SW_AGENT_NAME
value: service-backend
- name: SW_AGENT_INSTANCENAME
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: SW_GRPC_LOG_SERVER_HOST
value: skywalking-oap.monitoring.svc
- name: SW_GRPC_LOG_SERVER_PORT
value: '11800'
- name: SW_GRPC_LOG_MAX_MESSAGE_SIZE
value: '10485760'
- name: SW_GRPC_LOG_GRPC_UPSTREAM_TIMEOUT
value: '30'
- name: SERVER_PORT
value: '80'
- name: TZ
value: Asia/Shanghai
- name: JAVA_OPTS
value: ' -Xmx4096m -Xms2048m'
- name: JAVA_ENABLE_DEBUG
value: 'false'
ports:
- name: http
containerPort: 80
volumeMounts:
- mountPath: /opt/skywalking/
name: skywalking-agent
- name: time
mountPath: /etc/localtime
volumes:
- name: skywalking-agent
emptyDir: {}
- name: time
hostPath:
path: /etc/localtime
type: ''
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
revisionHistoryLimit: 3
progressDeadlineSeconds: 600
## 容器啟動腳本
#!/bin/sh
echo 'ready to start service'
if [ x"${JAVA_ENABLE_DEBUG}" != x ] && [ "${JAVA_ENABLE_DEBUG}" != "false" ]
then
java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 /home/*.jar
elif [ -f "/opt/skywalking/agent/skywalking-agent.jar" ]
then
java -javaagent:/opt/skywalking/agent/skywalking-agent.jar ${JAVA_OPTS} -jar /home/*.jar
else
java ${JAVA_OPTS} -jar /home/*.jar
fi
這樣就可以在對代碼零侵入的情況下實現(xiàn)對鏈路的追蹤以及運行指標(biāo)的收集。
apisix也支持skywalking插件,需要在配置文件中進(jìn)行如下設(shè)置,然后針對每個需要收集的路由單獨啟用skywalking插件即可。
...
- real-ip
- skywalking
- skywalking-logger
stream_plugins:
- mqtt-proxy
- ip-restriction
- limit-conn
plugin_attr:
skywalking:
service_name: APISIX
endpoint_addr: http://skywalking-oap.monitoring.svc:12800
service_instance_name: $hostname
skywalking-logger:
endpoint_addr: http://skywalking-oap.monitoring.svc:12800
service_name: APISIX
service_instance_name: $hostname
日志收集
由于skywalking也支持日志收集和展示,所以我把SW_GRPC_LOG_SERVER_HOST也加了進(jìn)去。我使用的網(wǎng)關(guān)是Apisix,它同樣支持skywalking插件,可以收集鏈路以及日志信息(skywalking日志收集有版本要求,需要apisix版本>=2.12)。但說句實在話,skywalking的日志展示功能實在有點一言難盡,只能說能用,而且收集的日志全都是base64編碼的,這個可以說是非常的坑,即使想要用kibana來進(jìn)行展示也會有問題,雖然kibana支持base64解碼,但是解碼出來的不支持關(guān)鍵字搜索,畢竟skywalking的強項是鏈路追蹤和收集指標(biāo),所以也不能要求太多。我建議只收集apisix的日志就可以了,服務(wù)的日志通過fluentd這類的工具來收集,然后通過kibana來展示。
我是用的是kube-fluentd-operator,vm出品,直接通過helm進(jìn)行安裝即可,github地址:https://github.com/vmware/kube-fluentd-operator
安裝完成之后,只需要在需要采集日志的namespace下面添加一個configmap即可自動采集日志,并且支持熱加載,ES可以跟skywalking公用一個ES集群。
apiVersion: v1
kind: ConfigMap
metadata:
name: fluentd-config
namespace:
data:
fluent.conf: |
<match $labels(reportlog=es)>
@type elasticsearch
host elasticsearch-master-headless.monitoring.svc
port 9200
logstash_format true
buffer_type memory
buffer_chunk_limit 1M
buffer_queue_limit 32
flush_interval 2s
max_retry_wait 30
disable_retry_limit
num_threads 16
reload_connections "true"
</match>
這里要推薦一個kibana的插件logtrail,它可以讓我們像tail -f 那樣的查看日志,還支持按照服務(wù)名、實例進(jìn)行過濾等功能,非常好用,但是要注意kibana、es以及l(fā)ogtrail插件的版本不能差一個大版本,我是用的es版本是6.8.6、kibana版本是6.8.12、logtrail插件版本是6.8.12。

這是logrtail的配置文件logtrail.json
{
"index_patterns" : [
{
"es": {
"default_index": "logs*",
"allow_url_parameter": false
},
"tail_interval_in_seconds": 10,
"es_index_time_offset_in_seconds": 0,
"display_timezone": "local",
"display_timestamp_format": "MMM DD HH:mm:ss",
"max_buckets": 500,
"default_time_range_in_days" : 0,
"max_hosts": 100,
"max_events_to_keep_in_viewer": 5000,
"fields" : {
"mapping" : {
"timestamp" : "@timestamp",
"display_timestamp" : "@timestamp",
"hostname" : "kubernetes.container_name",
"program": "kubernetes.pod_name",
"message": "log"
},
"message_format": "{{{log}}}",
"keyword_suffix" : "keyword"
},
"color_mapping" : {
"field": "loglevel",
"mapping": {
"ERROR": "#FF0000",
"WARN": "#FFEF96",
"DEBUG": "#B5E7A0",
"TRACE": "#CFE0E8",
"INFO": "#339999"
}
}
}
]
}
監(jiān)控告警
這個其實沒什么好多選的,就prometheus+grafana+alertmanager即可,絕對是不二之選,這個我就不想描述太多了,主要講一下如何把apisix集成到prometheus里面。首先在apisix里面設(shè)置一下metric的暴露地址,并且把prefer_name設(shè)置為true。
......
plugin_attr:
prometheus:
prefer_name: true
export_addr:
ip: 0.0.0.0
port: 9091
然后添加一個svc,通過svc來訪問pod的metric
apiVersion: v1
kind: Service
metadata:
name: apisix-metric-svc
namespace: apisix
labels:
app: apisix-metric-svc
spec:
ports:
- name: http-metric
protocol: TCP
port: 80
targetPort: 9091
selector:
app.kubernetes.io/instance: apisix
app.kubernetes.io/name: apisix
type: ClusterIP
接著添加一個ServiceMonitor資源,讓prometheus去獲取apisix的指標(biāo)信息。
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
app: apisix-monitor
release: prometheus
name: apisix-apisix-metric
namespace: monitoring
spec:
endpoints:
- interval: 60s
path: /apisix/prometheus/metrics
port: http-metric
namespaceSelector:
matchNames:
- apisix
selector:
matchLabels:
app: apisix-metric-svc
最后再在grafana中導(dǎo)入一個apisix的dashboard就可以查看到從apisix進(jìn)入集群的所有請求指標(biāo)了。

在線調(diào)試
相信還是有很多人想要在線調(diào)試代碼的,雖然這種操作不適合在生產(chǎn)環(huán)境中進(jìn)行,但是在開發(fā)以及測試環(huán)境還是可以的。針對java技術(shù)棧的人,我提供下面兩種在線調(diào)試的思路供大家參考:
容器啟動的時候添加-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005,然后通過Lens把pod的5005端口暴露到本地,IDEA直接attach上去進(jìn)行調(diào)試。
通過telepherence工具,把容器的流量劫持到本地進(jìn)行調(diào)試。
第一種方式比較簡單,具體的啟動腳本如下:
#!/bin/sh
echo 'ready to start service'
if [ x"${JAVA_ENABLE_DEBUG}" != x ] && [ "${JAVA_ENABLE_DEBUG}" != "false" ]
then
java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 /home/*.jar
else
java ${JAVA_OPTS} -jar /home/*.jar
fi
通過獲取環(huán)境變量的方式?jīng)Q定是否啟動debug端口。把這個sh文件打包到鏡像里面,然后通過這個腳本去啟動jar即可。
第二種方式是通過telepherence工具的方式,這個就需要用到config文件,下面是對應(yīng)的用戶需要的權(quán)限
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: test # Update value for appropriate user name
namespace: ambassador # Traffic-Manager is deployed to Ambassador namespace
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: telepresence-role
rules:
- apiGroups:
- ""
resources: ["pods"]
verbs: ["get", "list", "create", "watch", "delete"]
- apiGroups:
- ""
resources: ["services"]
verbs: ["update"]
- apiGroups:
- ""
resources: ["pods/portforward"]
verbs: ["create"]
- apiGroups:
- "apps"
resources: ["deployments", "replicasets", "statefulsets"]
verbs: ["get", "list", "update"]
- apiGroups:
- "getambassador.io"
resources: ["hosts", "mappings"]
verbs: ["*"]
- apiGroups:
- ""
resources: ["endpoints"]
verbs: ["get", "list", "watch"]
---
kind: RoleBinding # RBAC to access ambassador namespace
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: t2-ambassador-binding
namespace: ambassador
subjects:
- kind: ServiceAccount
name: test # Should be the same as metadata.name of above ServiceAccount
namespace: ambassador
roleRef:
kind: ClusterRole
name: telepresence-role
apiGroup: rbac.authorization.k8s.io
---
kind: RoleBinding # RoleBinding T2 namespace to be intecpeted
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: telepresence-test-binding # Update "test" for appropriate namespace to be intercepted
namespace: default # Update "test" for appropriate namespace to be intercepted
subjects:
- kind: ServiceAccount
name: test # Should be the same as metadata.name of above ServiceAccount
namespace: ambassador
roleRef:
kind: ClusterRole
name: telepresence-role
apiGroup: rbac.authorization.k8s.io
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: telepresence-namespace-role
rules:
- apiGroups:
- ""
resources: ["namespaces"]
verbs: ["get", "list", "watch"]
- apiGroups:
- ""
resources: ["services"]
verbs: ["get", "list", "watch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: telepresence-namespace-binding
subjects:
- kind: ServiceAccount
name: test # Should be the same as metadata.name of above ServiceAccount
namespace: ambassador
roleRef:
kind: ClusterRole
name: telepresence-namespace-role
apiGroup: rbac.authorization.k8s.io
具體的操作指令可以從官網(wǎng)上去查。之前版本的telepherence用起來蠻簡單,現(xiàn)在感覺越來越難用了,有興趣的可以去深入研究一下。
服務(wù)更新策略
一般來說服務(wù)上線之后我們都不希望停機維護(hù),而是無感升級,下面提供兩種情況下的升級思路
- 在線升級
特點:可以無縫升級,升級的同時不影響實際的使用,適合一些小版本的迭代以及hotfix
要求:服務(wù)在線升級采用金絲雀方案,要求新的版本一定要兼容舊的版本
- 離線升級
特點:升級的時候需要停止線上的服務(wù),無法對外提供服務(wù),適合大版本更新
要求:需要在網(wǎng)關(guān)上把對應(yīng)服務(wù)的流量斷開,整體升級完成并測試沒有什么大問題之后重新開放流量
在線升級更新步驟
1、更新對應(yīng)服務(wù)的deployment-canary.yml文件,把版本號更新到新的版本,并將replica設(shè)置成1,跟已經(jīng)在運行的容器比例大概會是1:10的比例,server會把不到10%的流量導(dǎo)入到新的服務(wù)。
2、等服務(wù)啟動之后需要等服務(wù)穩(wěn)定運行至少1天,期間可以適當(dāng)?shù)脑黾觬eplica數(shù)量,每次遞增的時間間隔保證在6h以上,每次遞增數(shù)量以1個為準(zhǔn)。
3、升級期間需要增加對鏈路和日志的觀測,鏈路一旦出現(xiàn)問題立刻把replica設(shè)置成0,停止升級,待解決問題之后從第1步重新開始。
4、等服務(wù)穩(wěn)定運行之后更新deployment.yml中的版本號,等待服務(wù)慢慢滾動升級,目前設(shè)置了一次只升級一個pod,如果升級途中鏈路出現(xiàn)任何問題或者日志有錯誤則馬上進(jìn)行回滾操作,待解決問題之后從第1步開始重新升級。
5、等deployment.yml中的pod全部升級完成之后,將deployment-canary.yml中的replica設(shè)置為0。
離線升級更新步驟
1、對于需要大版本升級的服務(wù),需要先評估其本身的影響以及與其相關(guān)的服務(wù)影響,明確影響范圍。
2、定好升級的時間點,提前告知用戶。
3、在網(wǎng)關(guān)中找到對應(yīng)的服務(wù),啟用ip-restriction插件,設(shè)置IP白名單,所有白名單以外的流量全部攔截,只返回固定的結(jié)果。
4、開始升級對應(yīng)服務(wù)的deployment.yml文件,如果涉及到數(shù)據(jù)庫表結(jié)構(gòu)變更的需要提前備份數(shù)據(jù)庫。
5、全部升級完成之后用白名單進(jìn)行整體測試,跑對應(yīng)的test case,如果遇到嚴(yán)重問題確認(rèn)無法馬上解決的,馬上進(jìn)行版本的回滾以及數(shù)據(jù)庫數(shù)據(jù)的回滾。
6、如果test case跑下來沒有問題,則把consumer-restriction插件停用,把服務(wù)向大眾開放。
7、如果有一些小的問題,則參考在線升級的方式進(jìn)行hotfix。
# deployment-canary.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-canary
spec:
replicas: 0
selector:
matchLabels:
app: nginx
role: canary
template:
metadata:
labels:
app: nginx
role: canary
spec:
volumes:
- name: time
hostPath:
path: /etc/localtime
type: ''
containers:
- name: nginx
image: nginx:latest
env:
- name: TZ
value: Asia/Shanghai
volumeMounts:
- name: time
mountPath: /etc/localtime
ports:
- name: http
containerPort: 80
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 15
failureThreshold: 6
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 60
periodSeconds: 30
revisionHistoryLimit: 2
# deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
role: release
spec:
replicas: 1
selector:
matchLabels:
app: nginx
role: release
template:
metadata:
labels:
app: nginx
role: release
spec:
volumes:
- name: time
hostPath:
path: /etc/localtime
type: ''
containers:
- name: nginx
image: nginx:latest
env:
- name: TZ
value: Asia/Shanghai
volumeMounts:
- name: time
mountPath: /etc/localtime
ports:
- name: http
containerPort: 80
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 15
failureThreshold: 6
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 60
periodSeconds: 30
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
revisionHistoryLimit: 3
progressDeadlineSeconds: 600
---
apiVersion: v1
kind: Service
metadata:
name: nginx-svc
labels:
app: nginx-svc
spec:
ports:
- protocol: TCP
port: 80
targetPort: 80
selector:
app: nginx
type: ClusterIP
sessionAffinity: None
總結(jié)
至此,包括K8s集群、網(wǎng)絡(luò)、存儲、CICD、日志、監(jiān)控告警、調(diào)試、服務(wù)更新,基本上涵蓋了DevOps所有涉及到的內(nèi)容,涉及到了非常的組件,其中不乏要自己寫一些代碼來把各個組件粘合起來,所以要把K8s順暢的用起來并非一件簡單的事情。
暫時只是把工作中的一些大致的東西記錄了下來,但由于最近公司996趕項目,空閑時間不是很多,寫的比較粗,實際上還有很多的細(xì)節(jié)沒有寫,后續(xù)有時間會慢慢補上。
后面還會介紹幾個基于K8s的數(shù)據(jù)庫方案,包括mysql和pgsql相關(guān)的內(nèi)容。后續(xù)等工作沒那么忙了也考慮把一些組件進(jìn)行整合,目標(biāo)是整合成一個完整的DevOps平臺,就是不知道什么時候能得償所望了。