Category Archive : K8S相关

traefik暴露kubernetes里的http服务和tcp服务

traeifk使用说明

traefik使用helm安装,搭配metalLB使用,由metalLB分配IP地址给traefik的loadbalancer

helm repo add traefik https://helm.traefik.io/traefik
helm upgrade -i traefik traefik/traefik --version 9.11.0 -f traefik/values.yaml

traefik暴露http服务的例子

配置如下http-ingress.yaml文件,暴露一个nginx,浏览器通过nginx.demo.test.local访问

#http-ingress.yaml
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: simpleingressroute
  namespace: default
spec:
  entryPoints:
    - web
  routes:
  - match: Host(`nginx.demo.test.local`)
    kind: Rule
    services:
    - name: test-demo-service
      port: 80

解释:

  1. metadata.name不能重复
  2. namespace是暴露的svc对应的namespace
  3. Host(nginx.demo.test.local)表示接受到浏览器访问nginx.aimp.sferetest.local的时候,转到test-demo-service服务的80端口
  4. entryPoints对应的是我们安装traefik时,values.yaml里的ports参数下面的名称,如web的8000是内部端口,80是对外提供访问的端口
#values.yaml
ports:
  # The name of this one can't be changed as it is used for the readiness and
  # liveness probes, but you can adjust its config to your liking
  web:
    port: 8000
    # hostPort: 8000
    expose: true
    exposedPort: 80
    # The port protocol (TCP/UDP)
    protocol: TCP

traefik暴露tcp服务的例子

配置如下tcp-ingress.yaml文件,暴露一个redis,通过loadbalancerIP:6851访问

# tcp-ingress.yaml
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRouteTCP
metadata:
  name: redisingleressroute
  namespace: test
spec:
  entryPoints:
    - redis
  routes:
  - match: HostSNI(`*`)
    kind: Rule
    services:
    - name: redis
      port: 6379

解释:

  1. kind得是IngressRouteTCP
  2. match必须是HostSNI(*)
  3. entryPoints对应的是我们安装traefik时,values.yaml里的ports参数下面的名称,如redis的6379是内部端口,6851是对外提供访问的端口
#values.yaml
  redis:
    port: 6379
    # hostPort: 8000
    expose: true
    exposedPort: 6851
    # The port protocol (TCP/UDP)
    protocol: TCP

timescaledb升级

背景

使用docker安装的timescaledb与postgresQL数据库。目前我们需要升级docker镜像以及目前正在使用的数据库。单纯的更换docker镜像是行不通的,请按照如下方式进行升级。
原版本是timescale/timescaledb-postgis:1.4.0-pg11
新版本是timescale/timescaledb-postgis:1.7.4-pg11

操作步骤

1.拉取最新的镜像

docker pull timescale/timescaledb:1.7.4-pg11

2.检查老容器挂载的数据目录

$ docker inspect timescaledb --format='{{range .Mounts }}{{.Source}}{{end}}'
/path/to/data

3.删除老容器

docker stop timescaledb
docker rm timescaledb

4.使用挂载的数据目录和新拉取的镜像,创建新容器

docker run -v /path/to/data:/var/lib/postgresql/data -d --name timescaledb -p 5432:5432 timescale/timescaledb

5.更新template1的timescaledb插件(重要)
如果不更新template1的话,后续创建的所有database还是老的1.4.0插件

docker exec -it timescaledb bash
su postgres
psql template1
ALTER EXTENSION timescaledb UPDATE;

6.更新已经存在的database的timescaledb插件(重要)
对所有“已经存在的数据库”进行插件更新,不然会导致无法连接,报错如下:
ERROR: could not access file “$libdir/timescaledb-1.4.2”: No such file or directory

docker exec -it timescaledb bash
su postgres
psql 已经存在的数据库
ALTER EXTENSION timescaledb UPDATE;

在K8S里使用filebeat作为sidecar收集nginx日志

简介

通过sidecar方法进行接入,与提供日志的容器部署在同一个pod里,主要是配置statefulset里的containers和configmap里的filebeat.yaml
1.把nginx的日志文件挂载在access_log这个volume里,同时在filebeat这个pod里也挂载access_log这个volume
2.filebeat通过subpath的方法挂载单独一个filebeat.yml到/usr/share/filebeat/filebeat.yml。注意,如果不用subpath挂载单个文件的话,是会覆盖掉/usr/share/filebeat/目录的

3.configmap里设置elasticsearch的地址和index,指定日志文件

 

statefulset.yaml

containers:
  - image: nginx:latest
    name: nginx
    ports:
        - containerPort: 80
    volumeMounts:
        - name: access-log #日志同时挂载在nginx和filebeat中
          mountPath: /var/log/nginx/
  - image: docker.elastic.co/beats/filebeat:6.8.12
    imagePullPolicy: Always
    name: filebeat
    volumeMounts:
        - name: access-log #日志同时挂载在nginx和filebeat中
          mountPath: /log
        - name: filebeat-config
          mountPath: /usr/share/filebeat/filebeat.yml
          subPath: filebeat.yml
  volumes:
    - name: filebeat-config
      configMap:
        name: filebeat-config
        items:
        - key: filebeat.yml
          path: filebeat.yml

configmap.yaml

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: filebeat-config
data:
  filebeat.yml: |
    filebeat.inputs:
    - type: log
      paths:
        - "/log/access.log"
    setup.template.name: "filebeat"
    setup.template.pattern: "filebeat-*"
    output.elasticsearch:
      hosts: ["{{ .Values.elastricsearch.addr }}"]
      index: "frontend-filebeat"

 


架构图

[docker]通过rsyslog记录日志并转发nginx日志到python程序

记录我是如何把rsyslog做成docker镜像,获取nginx的accesslog并且转发到python的

关键点1 nginx日志配置

nginx日志要设置成json格式输出,nginx.conf如下图所示,这个可以在docker镜像中通过volume把nginx.conf挂载进去,然后把/var/log/nginx/access.log挂载到本地

user  root;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';
    log_format main '{"time_local": "$time_local", '
   '"path": "$request_uri", '
   '"ip": "$remote_addr", '
   '"time": "$time_iso8601", '
   '"user_agent": "$http_user_agent", '
   '"user_id_got": "$uid_got", '
   '"user_id_set": "$uid_set", '
   '"remote_user": "$remote_user", '
   '"request": "$request", '
   '"status": "$status", '
   '"body_bytes_sent": "$body_bytes_sent", '
   '"request_time": "$request_time", '
   '"http_referrer": "$http_referer" }';
    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    #gzip  on;
    include /etc/nginx/conf.d/*.conf;
}

关键点2 rsyslog配置与Dockerfile

编写一个51-nginx-forward.conf文件放置在/etc/rsyslog.d/下即可

module(load="imfile")
input(type="imfile"
      File="/var/log/nginx/access.log"
      Tag="mywebsite:")
# omfwd module for forwarding the logs to another tcp server
if( $syslogtag == 'mywebsite:')  then {
  action(type="omfwd" target="python服务器IP地址" port="6000" protocol="tcp"
            action.resumeRetryCount="100"
            queue.type="linkedList" queue.size="10000")
}

我们可以用一个Dockerfile来运行rsyslog,docker run的时候注意日志的挂载

FROM ubuntu:16.04
RUN apt-get update && apt-get install -y rsyslog; \
    rm -rf /var/lib/apt/lists/*
ADD 51-nginx-forward.conf /etc/rsyslog.d/.
# RUN cat /dev/null> /var/log/mail.log
CMD service rsyslog start && tail -f /var/log/syslog

关键点3 python程序通过tcp的方式读取rsyslog

python程序与rsyslog建立tcp连接,可以实时的进行数据库的插入语句

import asyncio
import json
import time
import database_init
class LogAnalyser:
    def __init__(self):
        pass
    def process(self, str_input):
        # print(str_input)
        str_input = str_input.decode("utf-8", errors="ignore")
        # Add your processing steps here
        # ...
        try:
            # Extract created_at from the log string
            str_splits = str_input.split("{", 1)
            json_text = "{" + str_splits[1]
            data = json.loads(json_text)
            created_at = data["time"]
            request_all = data["request"].split(" /", 1)
            http_type = request_all[0]
            path = data["path"]
            request_time = data["request_time"]
            if PREFIX in data["path"]:
                path = data["path"]
                return http_type, path, created_at,request_time  # The order is relevant for INSERT query params
        except Exception as e:
            print("error in read_rsylog.py,Class LogAnalyser,function process")
            print(e)
        return None
@asyncio.coroutine
def handle_echo(reader, writer):
    log_filter = LogAnalyser()
    while True:
        line = yield from reader.readline()
        if not line:
            break
        params = log_filter.process(line)
        if params:
            # 进行一堆操作,例如进行数据库的插入
            # execute_sql(params=params)
if __name__ == '__main__':
    CURSOR = database_init.DBConnect().CURSOR
    CONN = database_init.DBConnect().CONN
    PREFIX = database_init.DBConnect().CONFIG["TEST_SWAGGER"]["PREFIX"]
    database_init.DBConnect().create_table()
    loop = asyncio.get_event_loop()
    coro = asyncio.start_server(handle_echo, None, 6000, loop=loop)
    server = loop.run_until_complete(coro)
    # Serve requests until Ctrl+C is pressed
    print('Serving on {}'.format(server.sockets[0].getsockname()))
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        pass
    # Close the server
    print("Closing the server.")
    server.close()
    loop.run_until_complete(server.wait_closed())
    loop.close()
    CURSOR.close()
    CONN.close()

helm更新latest镜像

有不少朋友跟我说,helm更新statefullset或者deployment时,使用latest镜像,无法更新,其实这个问题很好解决的,可以使用git-hash来解决,参考文章https://www.yinyubo.cn/?p=535
也可以使用我们本篇文章里的办法,添加环境变量来解决
helm更新的原理是,yaml文件没有变更,则不会更新,我们要想使用latest镜像先terminating老的pod,再running一个新的pod,只要使我们的statefullset或者deployment的yaml文件发生变更即可。下面贴出解决代码

containers:
        - image: '镜像名:latest'
          imagePullPolicy: Always
          env:
            - name: upgrade_time
              value: {{ date "2006-01-02-150405" .Release.Time }}

镜像使用latest,拉取策略使用alway pull的策略。在环境变量里添加一个upgrade_time升级时间,该时间使用helm的date功能生成,这样我们的yaml就能做到每次helm upgrade都发生变更,每次都能去拉取最新的镜像并且升级。并且我们可以在部署之后,通过kubectl exec -it “pod名字” sh 进入容器,检查env里的upgrade_time看看是否更新。
很简单吧,快试试吧

在K8S里使用NFS做storageclass

参考github:https://github.com/kubernetes-incubator/external-storage/tree/master/nfs-client


1.使用helm安装nfs-client,注意填写nfs服务器的地址和暴露路径

$ helm install stable/nfs-client-provisioner --set nfs.server=x.x.x.x --set nfs.path=/exported/path

2.编写storageclass.yaml并使其生效

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: managed-nfs-storage
provisioner: fuseim.pri/ifs

3.设置这个storageclass为默认storageclass

kubectl patch storageclass nfs-client -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

4.注意这边下载quay.azk8s.cn/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11这个镜像可能很慢,可以调整成国内源下载,再重命名。例如下面的命令

docker pull quay.azk8s.cn/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11
docker tag quay.azk8s.cn/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11 quay.io/external_storage/nfs-client-provisioner:v3.1.0-k8s1.11

K8S重新加入master节点,避免etcd错误

我们有时候会有删除,再重新加入master节点的需求,比如master机器改名。这里注意重新加入时,经常会出现etcd报错,如下

[check-etcd] Checking that the etcd cluster is healthy
error execution phase check-etcd: etcd cluster is not healthy: failed to dial endpoint https://192.168.0.92:2379 with maintenance client: context deadline exceeded

这个时候,就需要去还没有停止的master节点里的etcd的pod里去,删除该老master节点对应的etcd信息。


kubernetes节点信息

master03是我们将要删除重新加入的节点

# root@master01:~# kubectl get nodes
NAME       STATUS   ROLES    AGE   VERSION
master01   Ready    master   64d   v1.17.1
master02   Ready    master   95s   v1.17.1
master03   Ready    master   18h   v1.17.1
slaver01   Ready    <none>   64d   v1.17.1
slaver04   Ready    <none>   13d   v1.17.1
slaver05   Ready    <none>   13d   v1.17.1

删除master

在master01机器上执行

kubectl drain master03
kubectl delete node master03

在master03机器上执行

kubeadm reset
rm -rf /etc/kubernetes/manifests/

删除etcd信息

在master01节点上执行命令,进入etcd的容器里

kubectl exec -it etcd-master01 sh

输入命令

etcdctl --endpoints 127.0.0.1:2379 --cacert /etc/kubernetes/pki/etcd/ca.crt --cert /etc/kubernetes/pki/etcd/server.crt --key /etc/kubernetes/pki/etcd/server.key member list

检查返回值

7d39fc3ab8790afc, started, master03, https://192.168.0.93:2380, https://192.168.0.93:2379, false
b54177b91845ab93, started, master01, https://192.168.0.91:2380, https://192.168.0.91:2379, false
bc771924f2f5445f, started, master02, https://192.168.0.92:2380, https://192.168.0.92:2379, false

因为我们的master03机器对应的hash是7d39fc3ab8790afc。我们下一步就是根据hash删除etcd信息,执行如下命令

etcdctl --endpoints 127.0.0.1:2379 --cacert /etc/kubernetes/pki/etcd/ca.crt --cert /etc/kubernetes/pki/etcd/server.crt --key /etc/kubernetes/pki/etcd/server.key member remove 12637f5ec2bd02b8

获取添加master的命令


在master01上输入命令

kubeadm init phase upload-certs --upload-certs

返回34f76df3029230ca3136f5ff689ed54b1af6501a59fb0ea728ff8fed31ad52b4
再在master01上输入命令

kubeadm token create --print-join-command

返回 kubeadm join cluster.kube.com:16443 –token f4amr0.c2nc87swc7jbybut –discovery-token-ca-cert-hash sha256:2c45bcc43dad9cf43c3b7e610c0cdb7d588213d4258fc060e7384276e664922e
通过组合上面的“蓝色字体部分“+“–control-plane –certificate-key“ +“红色字体部分”,获得加入master的完整命令
kubeadm join cluster.kube.com:16443 –token uerys4.h8z3lfo2j3zf8g2u –discovery-token-ca-cert-hash sha256:2c45bcc43dad9cf43c3b7e610c0cdb7d588213d4258fc060e7384276e664922e –control-plane –certificate-key 34f76df3029230ca3136f5ff689ed54b1af6501a59fb0ea728ff8fed31ad52b4


添加Master节点

执行命令

kubeadm join cluster.kube.com:16443 --token uerys4.h8z3lfo2j3zf8g2u --discovery-token-ca-cert-hash sha256:2c45bcc43dad9cf43c3b7e610c0cdb7d588213d4258fc060e7384276e664922e --control-plane --certificate-key 34f76df3029230ca3136f5ff689ed54b1af6501a59fb0ea728ff8fed31ad52b4

ceph集群无法初始化osd问题

安装ceph的osd时.运行清空磁盘命令

ceph-deploy disk zap node3-ceph /dev/sdb

有时候会遇到这样的报错。

[node3-ceph][WARNIN]  stderr: wipefs: error: /dev/sdb: probing initialization failed: Device or resource busy
[node3-ceph][WARNIN] --> failed to wipefs device, will try again to workaround probable race condition

遇到这种报错时,只能上这台机器,手动进行dd命令清空磁盘并重启

dd if=/dev/zero of=/dev/sdb bs=512K count=1
reboot

重启完成后,再进入ceph-admin的主机进行ceph-deploy disk zap node3-ceph /dev/sdb 就能够清理磁盘成功了

使用helm动态更新k8s里的docker镜像关键点

需求分析

1.我们使用helm来实现应用程序的更新。
2.应用程序更新的关键就是镜像。每次我们的代码合入develop分支之后,都会产生一个新的docker镜像。
3.我们需要让helm知道我们使用了最新的docker镜像。这样部署的应用才是最新的。
4.helm包是通过nexus上传的,从设计上来说,不适合每一次cd流程就产生一个helm包并上传,helm本身也没有提供上传接口。helm包的设计是希望一个helm包能一直通用于某一类程序。
5.综上所述,我们的cd流程的关键是,在helm包不更新的情况下,让helm包能每次cd流程后,通过developmengt.yaml使用最新的docker镜像在K8S上进行部署


使用误区

误以为developmengt.yaml里配置container.image的tag为latest,imagePullPolicy为always,就能每次部署的时候拉取最新的镜像。
接下来,我们看看默认配置


我们经常会错误的配置Chart.Appversion为latest,如下图

helm upgrade [RELEASE] [CHART] [flags]
例如: helm upgrade lizhenwei nginx-chart

原因是:对于helm来说,参数没有任何变化,chart版本也没有变化,所以是不会去更新的


正确用法

我们的chart不会去不停的更新,所以我们要做的是参数的更新。
所以我们要让docker的镜像tag动起来
1.每次的CICD流程除了上传latest镜像以外,还要再上传一个带tag号的镜像,tag号可以用git的commitHash值来区分,一般来说,有个8位的hash值就够了
2.helm的development.yaml里的镜像tag要进行对应的修改,如下

image: "{{ .Values.image.repository }}:{{ .Values.commitHash }}"

Values.commitHash是来自values.yaml文件中的commitHash参数

3.helm的upgrade命令增加comitHash参数。升级命令里增加set参数

helm upgrade -i dev-ui sfere/cloudview-ui --set commitHash=$commitHash

因为commitHash参数的改变,所以对于helm来说,产生了参数变化,helm是会去进行更新操作,这个时候就会去把参数填入development.yaml的image选项里,从而触发拉取新代码,更新应用

在Kubernetes里使用gradle缓存加速编译和docker in docker例子

需求

1.我们的代码编译需要用到gradle6.2版本,jdk13版本,docker in docker策略
2.因为是在CI环境中使用,所以gradle容器会因为流水线的触发,不停的启动和删除。下载jar包会非常消耗时间,我们需要持久化这些gradle缓存。
3.挂载这些gradle缓存文件到机器上,可以用ceph集群和NFS,这里我偷懒,先用NFS做,后期资源充足再换成ceph。
4.因为我们有并行流水线的可能,所以gradle容器可能一次不止一个,而gradle的caches一次只能被一个进程占用,为了避免多容器占用同一个gradle的caches,我们需要有策略。


已经踏过的坑

1.不能使用apline来制作gradle容器,因为我们的代码里有用到protoc,他会在gradle里安装protoc-3.10.1-linux-x86_64.exe,但是这个程序并不兼容alpine系统,最终会导致报错。报错信息如下图

2.不要使用adoptopenjdk/openjdk13:latest镜像来制作Dockerfile,这个镜像用的jdk版本是jdk13 ea版本。这个ea版本和正常的jdk13有一些细微的差点,这会导致gradle最后编译失败,报错信息如下图

3.不要直接把gradle缓存目录直接挂载进去,这会导致多任务时阻塞,同时编译失败,如下图


解决过程

1.创建NFS

NFS的搭建是比较简单的。在网上可以很容易搜索到教程,这里需要注意节点机器上也需要安装NFS工具,apt-get install nfs-common,不然会报错,导致K8S在这个节点上创建不了容器,因为挂载volume就失败了
我们在NFS服务器上创建一个目录,用于存放gradle的缓存

vi /etc/exports
添加
/nfsdata/gradle-cache/ *(rw,sync,no_root_squash,no_subtree_check)

2.创建gradle镜像,用于k8s中编译使用

使用Dockerfile,内容如下

FROM gradle:6.2.0-jdk13
# 因为需要docker in docker 所以这里用阿里云一键安装docker
RUN  curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
# 随意创建一个文件
RUN echo "test" > test.log
# 因为使用了私有仓库,所以之间拷贝docker配置文件进去
COPY daemon.json /etc/docker/daemon.json
# 原来的gradle命令是"jshell"这里我们替换掉,避免容器启动后,程序结束就消失
ENTRYPOINT ["tail","-f","test.log"]

docker build -t gradle_lzw .

3.使用这个gradle镜像运行起来,进行一次编译,把gradle caches拷贝出来,放到NFS中

#运行容器
docker run -d -v /var/run/docker.sock:/var/run/docker.sock gradle_lzw
# 进入容器编译代码, docker exec  -it gradle_lzw gradle XXXXX(gradle编译命令)
# 复制出缓存文件 docker cp gradle:/home/gradle/.gradle /home/temp/local/.gradle

关于gradle的缓存可以看这个https://github.com/keeganwitt/docker-gradle

4.把上一步获得的gradle缓存文件上传到NFS服务器上

scp -r root@{有gradle缓存的机器IP}:/home/temp/local/.gradle /nfsdata/gradle-cache/.

5.为了避免多容器并发占用gradle缓存目录,我们只能绕开直接挂载。我们先把缓存文件挂载到一个无用的目录中,然后再从这个目录复制到gradle指定的缓存目录中l

项目流水线配置

Jenkinsfile内容参考:

pipeline {
    agent {
        kubernetes {
            //label使用项目名称,因为不同的项目,build方式是不同的,如果错误的使用了相同的label。Jenkins就不会去读取BuildPod.yaml
            label 'jnlp-项目名称'
            //
            yamlFile 'BuildPod.yaml'
        }
    }
    stages {
        stage('build') {
         //使用gradle容器
		 container('gradle'){
		 sh '''
         //复制缓存文件
         cp -rf /opt/.gradle/caches /home/gradle/.gradle/caches
         gradle {build 命令}
          '''
          }
		}
	}
}

BuildPod.yaml配置,gradle-cache是缓存目录,dind是docker in docker的必要容器

kind: Pod
metadata:
  labels:
    some-label: some-label-value
spec:
  containers:
  - name: jnlp
    image: jenkins/jnlp-slave
    tty: true
    volumeMounts:
    - name: workspace-volume
      mountPath: /home/jenkins
  - name: gradle
    image: gradle_lzw:latest
    tty: true
    volumeMounts:
    - name: workspace-volume
      mountPath: /home/jenkins
    - name: gradle-cache
      mountPath: /opt/.gradle
    env:
    - name: DOCKER_HOST
      value: tcp://localhost:2375
  - name: dind
    image: docker:18.05-dind
    securityContext:
      privileged: true
    volumeMounts:
      - name: dind-storage
        mountPath: /var/lib/docker
  imagePullSecrets:
    - name: repok8s
  volumes:
  - name: gradle-cache
    nfs:
      server: {NFS服务器IP}
      path: "/nfsdata/gradle-cache/.gradle"
  - name: workspace-volume # pod中有一个volume让其中的多个容器共享
    emptyDir: {}
  - name: dind-storage
    emptyDir: {}


苏ICP备18047533号-1