K8S中微服务踩坑分享
一、资源限制
- 我们先来谈下 “资源限制”:
我们线上目前多数为java应用,java应用对于K8S来说,资源限制特别重要,如果不做资源限制,会影响整个宿主机,然后整个宿主机资源不够会实现飘移,会转移到其他主机上,然后再异常,可能会起到一种雪崩的效应。
通常我们会通过如下设置进行对容器的资源限制,但是通常对于java应用来说这个是和jvm中设置的是不搭嘎的,且docker容器也抓不到jvm的设置;这就造成了,假如设置不合理就会导致业务启动故障。
如:如下内存我们设置了最大1G,如果jvm可以读取到limits的内存,一旦应用使用内存即将到达1G,就会自动触发堆内存回收,大量GCC。如果读取不到,且jvm内存设置的2G,这时候多数的应用会OOM;
resources:
requests:
cpu: 0.5
memory: 256Mi
limits:
cpu: 1
memory: 1Gi
当然不是没有解决方案,我们目前是使用的2种解决方案: - 默认将JVM参数通过镜像写死在镜像中,且JVM参数是根据内存自动分配大小,放在/etc/profile中如果使用就加载,不使用不加载:
export NewSize=`expr ${DAOKEMEM} / 4`
export DirectMemorySize=`expr ${DAOKEMEM} / 16`
export XM=`expr ${DAOKEMEM} - ${DirectMemorySize}`
export MetaspaceSize=`expr ${NewSize} / 8`
export MaxMetaspaceSize=`expr ${NewSize} / 4`
export JAVA_OPTS="-Duser.timezone=GMT+08 -server -Xms${XM}m -Xmx${XM}m -XX:NewSize=${NewSize}m -XX:MaxNewSize=${NewSize}m -XX:MaxDirectMemorySize=${DirectMemorySize}m -XX:MetaspaceSize=${MetaspaceSize}m -XX:MaxMetaspaceSize=${MaxMetaspaceSize}m -XX:+UseParNewGC -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=1024M -XX:+ExplicitGCInvokesConcurrent -XX:-UseGCOverheadLimit -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=65 -XX:CMSFullGCsBeforeCompaction=2 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -javaagent:/usr/local/apm_agent/apm.agent.bootstrap.jar -Xloggc:/data/logs/skynet-${DAOKEAPPUK}/${DAOKEAPPUK}_gc.log -Dapm.applicationName=${DAOKEAPPUK} -Dapm.agentId=${HOST}-${PORT0} -Dapm.env=${DAOKEENVTYPE}"
# env
HOSTNAME=ASD-18-XXX-XXX.linux.XXX.com
XM=7680
NewSize=2048
HOST=172.XX.XXX.XX
TERM=xterm
PORT0=18645
DAOKEAPPNAME=XXX.hbase.XXX.efs
APPNAME=XXX.hbase.XXX.efs
JAVA_OPTS=-Duser.timezone=GMT+08 -server -Xms7680m -Xmx7680m -XX:NewSize=2048m -XX:MaxNewSize=2048m -XX:MaxDirectMemorySize=512m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+UseParNewGC -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=1024M -XX:+ExplicitGCInvokesConcurrent -XX:-UseGCOverheadLimit -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=65 -XX:CMSFullGCsBeforeCompaction=2 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -javaagent:/usr/local/apm_agent/apm.agent.bootstrap.jar -Xloggc:/data/logs/skynet-tcbase.java.dss.hbase.infra.efs/tcbase.java.XXX.hbase.XXX.efs_gc.log -Dapm.applicationName=tcbase.java.XXX.hbase.XXX.efs -Dapm.agentId=172.18.191.95-18645 -Dapm.env=product
DirectMemorySize=512
JRE_HOME=/usr/java/default/jre
LS_COLORS=
DAOKE_REGION=cn_east
DAOKE_LOGIC_IDC=logicidc_hd1
DAOKEDOWNCMD=
DAOKEIDC=logicidc_hd1
PATH=/usr/java/default/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/usr/local/tomcat
DAOKEENV=product
JAVA_HOME=/usr/java/default
DAOKEID=73918
LANG=en_US.UTF-8
TZ=Asia/Shanghai
DAOKE_IDC=XHY
DAOKEREGION=cn_east
DAOKEENVTYPE=product
DAOKECPU=4
DAOKEWAITTIME=10
SHLVL=1
HOME=/root
MaxMetaspaceSize=512
MetaspaceSize=256
CLASSPATH=.:/usr/java/default/lib/tools.jar
LESSOPEN=||/usr/bin/lesspipe.sh %s
DAOKEIP=172.18.191.95
DAOKEMEM=8192
DAOKEAPPUK=tcbase.java.XXX.hbase.XXX.efs
_=/usr/bin/env
env:
- name: JAVA_OPTS
value: "-Duser.timezone=GMT+08 -server -Xms7680m -Xmx7680m -XX:NewSize=2048m -XX:MaxNewSize=2048m -XX:MaxDirectMemorySize=512m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+UseParNewGC -XX:+UseCMSInitiatingOccupancyOnly -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=1024M -XX:+ExplicitGCInvokesConcurrent -XX:-UseGCOverheadLimit -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=65 -XX:CMSFullGCsBeforeCompaction=2 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -javaagent:/usr/local/apm_agent/apm.agent.bootstrap.jar -Xloggc:/data/logs/XXX-tcbase.java.XXX.hbase.XXX.efs/tcbase.java.XXX.hbase.XXX.efs_gc.log -Dapm.applicationName=tcbase.java.XXX.hbase.XXX.efs -Dapm.agentId=172.18.191.95-18645 -Dapm.env=product"
当然,你也许会有疑问,那如何加载呢? 先来看一个示例的Dockerfile:
# cat product-service/product-service-biz/Dockerfile
FROM java:8-jdk-alpine
ENV JAVA_OPTS="$JAVA_OPTS"
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY ./target/product-service-biz.jar ./
COPY pinpoint /pinpoint
EXPOSE 8010
CMD java -jar -javaagent:/pinpoint/pinpoint-bootstrap-1.8.3.jar -Dpinpoint.agentId=${HOSTNAME} -Dpinpoint.applicationName=ms-product $JAVA_OPTS /product-service-biz.jar
二、滚动更新之健康检查重要性
2.1、探针机制
k8s提供了是两种探针的机制,分别为就绪探针 readinessProbe、存活探针 livenessProbe。
探针机制可以通过http接口、shell指令、tcp确认容器的状态。探针还可以配置延迟探测时间、探测间隔、探测成功或失败条件延后时间等参数。使用http接口探测时,可以配置header参数,如果响应的状态码大于等于200且小于400,则诊断被认为是成功的。
存活探针,主要用于检测pod是否异常,如果k8s通过健康探针检测到服务异常后会替换或重启容器。
就绪探针,这个探测通过时才会将其加入到service匹配的endpoint列表中,并向该容器发送http请求,否则会将pod从列表移除直到就绪探针再次通过
就绪探针和存活探针比较类似,都会持续执行检测,只是检测会导致的结果不一样,一个会导致容器重启或被替换,一个会导致http请求停止分发到容器。
readinessProbe: ##就绪检查
tcpSocket:
port: 8010
initialDelaySeconds: 60
periodSeconds: 3
livenessProbe: ##存活检查(探活)
tcpSocket:
port: 8010
initialDelaySeconds: 60 ##启动pod后的多少秒开始检查
periodSeconds: 3 ##每隔3s进行探活
如上针对单体java应用还是有必要的,因为如果没有readinessProbe(就绪检查),一旦容器启动就会有业务流量进入;但是对于微服务来说可有可无,因为有注册中心和配置中心来管控!
三、滚动更新之流量丢失
一般故障就是: - 连接拒绝 - 响应错误 - 调用不到
使用k8s发布服务默认使用的滚动发布方案,这个方案本身已经有一定机制减少发布的影响。滚动发布时发布完一个新版本的pod后才会下线一个旧的pod,并把指向sevice的请求经负载均衡指向新pod,直到所有旧的pod下线,新的pod全部发布完毕。
所以只要k8s在pod的启停时做到和微服务联动,就可以做到无感发布。关键在于探知微服务是否准备好了、通知服务将要停止、配置启停过程预留的时间。这几个方面k8s都有相关的机制,所以我们先了解这些机制,再整合得出解决思路。
一般滚动更新是关闭现有的pod,再起一新的pod,关闭现有的其实是就是删除了一个pod,然后apiserver会通知给kubelet,然后kubelet会关闭这个容器,然后从service后端摘掉;
关闭pod之后会有一个等待时间,在这个时间呢,可能还会接入一些新的流量,但是它的服务已经不再处理新的请求了,所以会导致连接拒绝,怎么去解决这个问题呢?实际上readiness探针在整个过程中并起到关键的作用,一旦endpoint收到pod 的删除事件后,这已经就与readiness探测结果不相关了。
如何解决呢?
其实之需要在关闭这个pod时加个休眠的时间,其实就可以解决这个问题了,在关闭和启动都是有一个钩子存在的,所有可以在关闭容器前,执行这个钩子,钩子这个定义一个shell,y也可以定义一个http请求,也就是支持者两种类型,也就是在container同级,因为这里休眠5秒也就是你关闭的容器不会马上退出,然后休眠5秒钟,再去关闭着应用,这5秒能够足够让kube-proxy刷新这个规则,这样的话,就不会将新加入的流量转发到这个刚关闭的pod上,增加这个钩子就能暂缓你关闭pod的时间,从而让kube-proxy增加刷新规则的时间!
lifecycle:
preStop:
httpGet:
host: 192.168.4.170
path: api/v2/devops/pkg/upload_hooks
port: 8090
或者:
lifecycle :
preStop :
exec :
command :
- sh
- -c
- “sleep 5”
3.1、terminationGracePeriodSeconds 配置延迟关闭时间
该属性默认30s,只配置terminationGracePeriodSeconds这个属性而没有配置prestop时,k8s会先发送SIGTERM信号给主进程,然后然后等待terminationGracePeriodSeconds 属性的时间,会被使用SIGKILL杀死。这个机制相对简单粗暴。
3.2、提供上述几个机制的deployment文件配置示例如下
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: review-demo
namespace: scm
labels:
app: review-demo
spec:
replicas: 3
# minReadySeconds: 60 #滚动升级时60s后认为该pod就绪
strategy:
rollingUpdate: ##由于replicas为3,则整个升级,pod个数在2-4个之间
maxSurge: 1 #滚动升级时会先启动1个pod
maxUnavailable: 1 #滚动升级时允许的最大Unavailable的pod个数
template:
metadata:
labels:
app: review-demo
spec:
terminationGracePeriodSeconds: 60 ##k8s将会给应用发送SIGTERM信号,可以用来正确、优雅地关闭应用,默认为30秒
containers:
- name: review-demo
image: library/review-demo:0.0.1-SNAPSHOT
imagePullPolicy: IfNotPresent
lifecycle:
preStop:
httpGet:
path: /prestop
port: 8080
scheme: HTTP
livenessProbe: #kubernetes认为该pod是存活的,不存活则需要重启
httpGet:
path: /health
port: 8080
scheme: HTTP
httpHeaders:
- name: Custom-Header
value: Awesome
initialDelaySeconds: 60 ## equals to the max startup time of the application + couple of seconds
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 5 # 连续失败次数
periodSeconds: 5 # 多少秒执行一次检测
readinessProbe: #kubernetes认为该pod是准备好接收http请求了的
httpGet:
path: /ifready
port: 8080
scheme: HTTP
httpHeaders:
- name: Custom-Header
value: Awesome
initialDelaySeconds: 30 #equals to min startup time of the app
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 5
periodSeconds: 5 # 多少秒执行一次检测
resources:
# keep request = limit to keep this container in guaranteed class
requests:
cpu: 50m
memory: 200Mi
limits:
cpu: 500m
memory: 500Mi
env:
- name: PROFILE
value: "test"
ports:
- name: http
containerPort: 8080
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!