CloudNative - x893675/note GitHub Wiki

k8s-crio环境搭建

环境说明

  • kubernetes: v1.16.3
  • cri-o: release-v1.16.1
  • runc: runc-1.0.0-65.rc8.el7
  • calico: v3.8
  • kubeadm,kubectl,kubelet: v1.16.3
  • cni-plugin: release-0.8.3
  • kernel: 5.4.3

系统配置

  1. centos7系统升级内核为5.4.3,可参考centos7升级内核

  2. 升级系统软件版本

  3. 关闭swap分区

  4. 将selinux设置为permissive或permissive

  5. 设置时间同步

  6. 设置内核参数

    cat > /etc/sysctl.d/99-kubernetes-cri.conf <<EOF
    net.bridge.bridge-nf-call-iptables  = 1
    net.ipv4.ip_forward                 = 1
    net.bridge.bridge-nf-call-ip6tables = 1
    EOF
    
    modprobe overlay
    modprobe br_netfilter
  7. 安装ipvsadm,并设置ipvs模块自启

    yum install ipvsadm
    
    cat > /etc/sysconfig/modules/ipvs.modules << EOF
    /sbin/modinfo -F filename ip_vs > /dev/null 2>&1
    if [ $? -eq 0 ];then
    	/sbin/modprobe ip_vs
    fi
    EOF

安装kubeadm

cat > /etc/yum.repos.d/kubernetes.repo <<EOF
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg

EOF

yum --disableexcludes=kubernetes install kubelet-1.16.3 kubeadm-1.16.3 kubectl-1.16.3

安装cri-o

  1. 安装依赖软件包

    yum install -y \
      btrfs-progs-devel \
      containers-common \
      device-mapper-devel \
      git \
      glib2-devel \
      glibc-devel \
      glibc-static \
      go \
      gpgme-devel \
      libassuan-devel \
      libgpg-error-devel \
      libseccomp-devel \
      libselinux-devel \
      pkgconfig \
      runc
  2. 编译安装cri-o

    git clone https://github.com/cri-o/cri-o
    
    cd cri-o
    
    git checkout -b v1.16.1 v1.16.1
    
    make && make install
    
    make install.config
    
    make install.systemd
  3. 编译安装conmon

    git clone https://github.com/containers/conmon
    
    cd conmon
    
    make && make install
  4. 编译安装cni-plugins

    git clone https://github.com/containernetworking/plugins
    
    cd plugins
    
    git checkout -b 0.8.3 v0.8.3
    
    ./build_linux.sh
    
    mkdir -p /opt/cni/bin
    
    cp bin/* /opt/cni/bin/
  5. 修改/etc/crio/crio.config

    log_level = "info"
    
    cgroup_manager = "systemd"
  6. 启动crio服务

    systemctl daemon-reload && systemctl enable crio --now
    
    #可以使用crio-status config命令查看crio的配置
  7. 安装crio-ctl

    VERSION="v1.17.0"
    
    curl -L https://github.com/kubernetes-sigs/cri-tools/releases/download/$VERSION/crictl-${VERSION}-linux-amd64.tar.gz --output crictl-${VERSION}-linux-amd64.tar.gz
    
    tar zxvf crictl-$VERSION-linux-amd64.tar.gz -C /usr/local/bin
    
    rm -f crictl-$VERSION-linux-amd64.tar.gz
  8. 配置kubelet

    cat > /etc/sysconfig/kubelet <<EOF
    KUBELET_EXTRA_ARGS=--container-runtime=remote --cgroup-driver=systemd --container-runtime-endpoint='unix:///var/run/crio/crio.sock' --runtime-request-timeout=5m
    EOF
  9. 启动kubelet

    systemctl daemon-reload && systemctl enable kubelet --now

安装k8s

  1. 使用kubeadm初始化集群,因之后使用calico网络插件,所以cidr的值需要填写192.168.0.0/16

    kubeadm init  --pod-network-cidr=192.168.0.0/16  --kubernetes-version=1.16.3
  2. 安装网络插件

    kubectl apply -f https://docs.projectcalico.org/v3.8/manifests/calico.yaml
  3. 执行kubectl get po --all-namespaces -w查看并等待pod运行正常

  4. 去掉节点的trant标签kubectl taint nodes --all node-role.kubernetes.io/master-

  5. 集群启动正常后kube-proxy默认使用iptables,先更改成ipvs(通过kubeadm配置文件配置之后再研究)

    kubectl get cm kube-proxy -n kube-system -o yaml | sed 's/mode: ""/mode: "ipvs"/' | kubectl apply -f -
    
    for i in $(kubectl get po -n kube-system | awk '/kube-proxy/ {print $1}'); do
      kubectl delete po $i -n kube-system
    done
    
    #执行ipvsadm -l可看到ipvs规则
  6. 配置自动补全

    yum install -y bash-completion
    
    #重新进入shell
    
    source <(kubectl completion bash)

测试集群正常

测试deployment文件如下,部署后通过curl clusterIP:port访问服务,查看网络是否正常

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 80

istio搭建

使用istio版本为istio-1.4.2

  1. 下载istio

    curl -L https://istio.io/downloadIstio | sh -
  2. 将istioctl添加值环境变量

    export PATH="$PATH:/root/istio-1.4.2/bin"
  3. 安装前验证

    istioctl verify-install
  4. 安装demo

    istioctl manifest apply --set profile=demo
  5. 对namespace的pod自动注入需要加入label

    kubectl label namespace <namespace> istio-injection=enabled
  6. 手动注入

    istioctl kube-inject -f <your-app-spec>.yaml | kubectl apply -f -
  7. 卸载

    istioctl manifest generate --set profile=demo | kubectl delete -f -

微服务使用istio注意事项

  • istio通过对每个pod加入一个envoy的sidecar容器接管服务的进出流量,如果服务不是使用k8s作为注册中心,而是使用consul等注册中心,则istio不能显示服务间的调用关系及流量走向,如下图所示,使用的是consul作为注册中心的一个例子:

    而使用k8s作为注册中心,则可以清晰的的到服务间的调用关系和流量走向

  • istio目前能识别的是http1.1,http2.0以及grpc的流量,在定义k8s的service时,需要指明name,istio才能对特定的流量进行区别,例如如下service定义:

    apiVersion: v1
    kind: Service
    metadata:
      name: api
      labels:
        app: api
    spec:
      ports:
        - port: 8080
          targetPort: 8080
          name: http
      selector:
        app: api
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: auth-srv
      labels:
        app: auth
    spec:
      ports:
        - port: 8080
          targetPort: 8080
          name: grpc
      selector:
        app: auth

微服务对接istio调用链

分布式追踪

分布式追踪中的主要概念:

  • Trace: 一次完整的分布式调用跟踪链路
  • Span: 跨服务的一次调用;多个Span组合成一次Trace追踪记录

一个完整的调用链跟踪系统,包括调用链埋点,调用链数据收集,调用链数据存储和处理,调用链数据检索(除了提供检索的 APIServer,一般还要包含一个非常酷炫的调用链前端)等若干重要组件。istio现在默认使用的是jaeger作为trace系统,可以选择使用jaeger和zipkin的trace格式。

istio-trace

istio官方的介绍为:

Istio makes it easy to create a network of deployed services with load balancing, service-to-service authentication, monitoring, and more, without any changes in service code.

istio在使用时,不对代码做任何处理即可进行服务治理,但是实际使用过程中,不修改服务代码,istio的调用链总是断开的。

在 Istio 中,所有的治理逻辑的执行体都是和业务容器一起部署的 Envoy 这个 Sidecar,不管是负载均衡、熔断、流量路由还是安全、可观察性的数据生成都是在 Envoy 上。Sidecar 拦截了所有的流入和流出业务程序的流量,根据收到的规则执行执行各种动作。实际使用中一般是基于 K8S 提供的 InitContainer 机制,用于在 Pod 中执行一些初始化任务. InitContainer 中执行了一段 Iptables 的脚本。正是通过这些 Iptables 规则拦截 pod 中流量,并发送到 Envoy 上。Envoy 拦截到 Inbound 和 Outbound 的流量会分别作不同操作,执行上面配置的操作,另外再把请求往下发,对于 Outbound 就是根据服务发现找到对应的目标服务后端上;对于 Inbound 流量则直接发到本地的服务实例上。

Envoy的埋点规则为:

  • Inbound 流量:对于经过 Sidecar 流入应用程序的流量,如果经过 Sidecar 时 Header 中没有任何跟踪相关的信息,则会在创建一个根 Span,TraceId 就是这个 SpanId,然后再将请求传递给业务容器的服务;如果请求中包含 Trace 相关的信息,则 Sidecar 从中提取 Trace 的上下文信息并发给应用程序。
  • Outbound 流量:对于经过 Sidecar 流出的流量,如果经过 Sidecar 时 Header 中没有任何跟踪相关的信息,则会创建根 Span,并将该跟 Span 相关上下文信息放在请求头中传递给下一个调用的服务;当存在 Trace 信息时,Sidecar 从 Header 中提取 Span 相关信息,并基于这个 Span 创建子 Span,并将新的 Span 信息加在请求头中传递。

根据这个规则,对于一个api->A-这个简单调用,我们有如下分析:

  • 当一个请求进入api时,该请求头中没有任何trace相关的信息,对于这个inbound流量,istio会创建一个根span,并向请求头注入span信息。
  • 当api向A创建并发送rpc或http请求时,这个请求对于api的envoy来说时outbound流量,如果请求头中没有trace信息,会创建根span信息填入请求头
  • 这种情况下,在istio的jaeger页面上我们可以看到两段断裂的trace记录

结论埋点逻辑是在 Sidecar 代理中完成,应用程序不用处理复杂的埋点逻辑,但应用程序需要配合在请求头上传递生成的 Trace 相关信息

istio使用jaeger作为trace系统,格式为zipkin format。在请求头中有如下headers:

  • x-request-id
  • x-b3-traceid
  • x-b3-spanid
  • x-b3-parentspanid
  • x-b3-sampled
  • x-b3-flags
  • x-ot-span-context

注意: 在http请求中,比如使用gin框架时,这些header中的key应是首字母大写的,例如:X-Request-Id

代码示例分析

以下代码段使用的是gin作为http框架,AuthSvc是rpc客户端,使用go-micro框架

import(
	"github.com/uber/jaeger-client-go"
	ot "github.com/opentracing/opentracing-go"
	"github.com/micro/go-micro/metadata"
	"github.com/gin-gonic/gin"
)


func (a *LoginController) Login(c *gin.Context) {
	//从http头中获得根span,使用istio时,该根span由envoy注入,记为root span
	inBoundSpanCtx, err := ot.GlobalTracer().Extract(ot.HTTPHeaders, ot.HTTPHeadersCarrier(c.Request.Header))
	//由根span创建一个子span,改span为span2
	span := ot.StartSpan("controller.(*LoginController).Login", 
		ot.ChildOf(inBoundSpanCtx),
		ot.Tags{
			"kind": "function",
		})
	//将span2与当前context绑定
	ctx := ot.ContextWithSpan(context.Background(), span)
	//在testtrace中再创建一个子span3
	testTrace(ctx)

    //从当前context中得到rpc调用的metadata,因为当前调用入口为http调用,所以ok永远为false
	md, ok := metadata.FromContext(ctx)
	if ok{
		fmt.Println("metadata from context is ok")
		for k, v := range md{
			fmt.Println(k,v)
		}
	}else{
		fmt.Println("metadata from context is not ok")
		md = make(map[string]string)
        //从span2的spancontext中获取trace信息,因为istio使用的是jaeger,所以将opentracing的接口进行类型断言转换为jaeger的spancontext,将span2的trance信息填入metadata
		if sc, ok := span.Context().(jaeger.SpanContext); ok {
			md["x-request-id"] = c.GetHeader("X-Request-Id")
			md["x-b3-traceid"] = sc.TraceID().String()
			md["x-b3-spanid"] = sc.SpanID().String()
			md["x-b3-sampled"] = c.GetHeader("X-B3-Sampled")
		}else{
			md["x-request-id"] = c.GetHeader("X-Request-Id")
			md["x-b3-traceid"] = c.GetHeader("X-B3-Traceid")
			md["x-b3-spanid"] = c.GetHeader("X-B3-Spanid")
			md["x-b3-sampled"] = c.GetHeader("X-B3-Sampled")
		}
        //从创建好的metadata中创建一个新的context
		ctx = metadata.NewContext(ctx, md)
	}

	var item schema.LoginParam
	if err := ginplus.ParseJSON(c, &item); err != nil {
		ginplus.ResError(c, err)
		return
	}
    //发送rpc请求时,使用新创建的携带了rpc metadata的context,该请求经过envoy时,envoy看到该outbound流量中的trace信息,会创建一个子span,传递给下一个服务,标记该span为span4
	response, err := a.AuthSvc.Verify(ctx, &auth.LoginRequest{
		Username: item.UserName,
		Password: item.Password,
	})

	if err != nil {
		ginplus.ResError(c, err)
		return
	}
    //结束span2
	span.Finish()
	ginplus.ResSuccess(c, response)
}

func testTrace(ctx context.Context){
    //传入的ctx已经与span2绑定,再创建一个子span,标记为span3
	span, _ := ot.StartSpanFromContext(ctx,
		"testTrace",
		ot.Tags{
		string(ext.SpanKind): "function",
	})
	fmt.Println("in test Trace function...")
	//span结束上报jaeger
    span.Finish()
}

由上图的注释分析得到下列span关系:

root span --> span2 -- span3
                    -- span4

在istio中就把之前分裂的两个trace记录合并为一个了。

结论:

  • 使用istio时,我们只需要对服务间调用的header信息进行透传
  • 如果想把服务内的调用关系与istio生成的trace合并,只需以istio生成的span作为父span,生成子span即可
  • 透传header的代码大都一致,可以做成一个通用的函数调用,减少服务代码的修改
⚠️ **GitHub.com Fallback** ⚠️