CICD搭建完成之后又迎来新的问题,链路追踪、日志、监控告警、在线调试、服务更新策略,先从链路追踪说起。
链路追踪
链路追踪的话有很多选项,比如zipkin、skywalking等,由于我们公司是基于java的技术路线的,所以我选择了skywalking来做链路追踪,通过sidecar的方式将其无感的注入到服务中。
skywalking可以通过helm直接安装,具体安装流程就不详细说明了,我是把监控告警这些工具全都装在了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
这样就可以在对代码零侵入的情况下实现对链路的追踪以及运行指标的收集。
apisix也支持skywalking插件,需要在配置文件中进行如下设置,然后针对每个需要收集的路由单独启用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也加了进去。我使用的网关是Apisix,它同样支持skywalking插件,可以收集链路以及日志信息(skywalking日志收集有版本要求,需要apisix版本>=2.12)。但说句实在话,skywalking的日志展示功能实在有点一言难尽,只能说能用,而且收集的日志全都是base64编码的,这个可以说是非常的坑,即使想要用kibana来进行展示也会有问题,虽然kibana支持base64解码,但是解码出来的不支持关键字搜索,毕竟skywalking的强项是链路追踪和收集指标,所以也不能要求太多。我建议只收集apisix的日志就可以了,服务的日志通过fluentd这类的工具来收集,然后通过kibana来展示。
我是用的是kube-fluentd-operator,vm出品,直接通过helm进行安装即可,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 那样的查看日志,还支持按照服务名、实例进行过滤等功能,非常好用,但是要注意kibana、es以及logtrail插件的版本不能差一个大版本,我是用的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"
}
}
}
]
}
监控告警
这个其实没什么好多选的,就prometheus+grafana+alertmanager即可,绝对是不二之选,这个我就不想描述太多了,主要讲一下如何把apisix集成到prometheus里面。首先在apisix里面设置一下metric的暴露地址,并且把prefer_name设置为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的指标信息。
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中导入一个apisix的dashboard就可以查看到从apisix进入集群的所有请求指标了。
在线调试
相信还是有很多人想要在线调试代码的,虽然这种操作不适合在生产环境中进行,但是在开发以及测试环境还是可以的。针对java技术栈的人,我提供下面两种在线调试的思路供大家参考:
容器启动的时候添加-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005,然后通过Lens把pod的5005端口暴露到本地,IDEA直接attach上去进行调试。
通过telepherence工具,把容器的流量劫持到本地进行调试。
第一种方式比较简单,具体的启动脚本如下:
#!/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
通过获取环境变量的方式决定是否启动debug端口。把这个sh文件打包到镜像里面,然后通过这个脚本去启动jar即可。
第二种方式是通过telepherence工具的方式,这个就需要用到config文件,下面是对应的用户需要的权限
---
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
具体的操作指令可以从官网上去查。之前版本的telepherence用起来蛮简单,现在感觉越来越难用了,有兴趣的可以去深入研究一下。
服务更新策略
一般来说服务上线之后我们都不希望停机维护,而是无感升级,下面提供两种情况下的升级思路
- 在线升级
特点:可以无缝升级,升级的同时不影响实际的使用,适合一些小版本的迭代以及hotfix
要求:服务在线升级采用金丝雀方案,要求新的版本一定要兼容旧的版本
- 离线升级
特点:升级的时候需要停止线上的服务,无法对外提供服务,适合大版本更新
要求:需要在网关上把对应服务的流量断开,整体升级完成并测试没有什么大问题之后重新开放流量
在线升级更新步骤
1、更新对应服务的deployment-canary.yml文件,把版本号更新到新的版本,并将replica设置成1,跟已经在运行的容器比例大概会是1:10的比例,server会把不到10%的流量导入到新的服务。
2、等服务启动之后需要等服务稳定运行至少1天,期间可以适当的增加replica数量,每次递增的时间间隔保证在6h以上,每次递增数量以1个为准。
3、升级期间需要增加对链路和日志的观测,链路一旦出现问题立刻把replica设置成0,停止升级,待解决问题之后从第1步重新开始。
4、等服务稳定运行之后更新deployment.yml中的版本号,等待服务慢慢滚动升级,目前设置了一次只升级一个pod,如果升级途中链路出现任何问题或者日志有错误则马上进行回滚操作,待解决问题之后从第1步开始重新升级。
5、等deployment.yml中的pod全部升级完成之后,将deployment-canary.yml中的replica设置为0。
离线升级更新步骤
1、对于需要大版本升级的服务,需要先评估其本身的影响以及与其相关的服务影响,明确影响范围。
2、定好升级的时间点,提前告知用户。
3、在网关中找到对应的服务,启用ip-restriction插件,设置IP白名单,所有白名单以外的流量全部拦截,只返回固定的结果。
4、开始升级对应服务的deployment.yml文件,如果涉及到数据库表结构变更的需要提前备份数据库。
5、全部升级完成之后用白名单进行整体测试,跑对应的test case,如果遇到严重问题确认无法马上解决的,马上进行版本的回滚以及数据库数据的回滚。
6、如果test case跑下来没有问题,则把consumer-restriction插件停用,把服务向大众开放。
7、如果有一些小的问题,则参考在线升级的方式进行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
总结
至此,包括K8s集群、网络、存储、CICD、日志、监控告警、调试、服务更新,基本上涵盖了DevOps所有涉及到的内容,涉及到了非常的组件,其中不乏要自己写一些代码来把各个组件粘合起来,所以要把K8s顺畅的用起来并非一件简单的事情。
暂时只是把工作中的一些大致的东西记录了下来,但由于最近公司996赶项目,空闲时间不是很多,写的比较粗,实际上还有很多的细节没有写,后续有时间会慢慢补上。
后面还会介绍几个基于K8s的数据库方案,包括mysql和pgsql相关的内容。后续等工作没那么忙了也考虑把一些组件进行整合,目标是整合成一个完整的DevOps平台,就是不知道什么时候能得偿所望了。