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
- 通过yaml文件限定jvm参数:
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