美文网首页
104.Kubernetes实现微服务和RPC服务支持

104.Kubernetes实现微服务和RPC服务支持

作者: doublegao | 来源:发表于2018-06-01 18:20 被阅读0次

    1.背景

    目前公司的paas平台由mesos的方案逐步想k8s转移,对于以前跑在mesos上的应用大致可以分为几类:

    • 单纯的负载均衡应用
    • 有明显Master和Slave之分的中间件:如redis,dubbo
    • 角色可转换的应用,如可以自动选主的应用

    2.思路

    根据物理位置的不同,我主要已两个思路为主:

    • 让客户端能够直接连接podIP
    这是最直接的方法,让客户端和服务端的网络呢能够处于同一个平面上,该方式在不通的网络插件中实现也有所不同,同时也有局限性。
    如果是Flannel(隧道) ,那么我们可以在上层的网关或者路由中,增加PODIP的相关路由,这种方式回牵扯到上层设备的修改,节点的变化需要和上层设别进行同步。
    如果是calico(BGP),一方面可以将内部路由同步到上层设备,另一方面也可以将路由同步到目标主机,但受到客户端主机的位置是否在同一个平面网络的影响。
    
    • 让每个podIP和端口映射到主机上,然后让你注册的时候注册主机的IP和端口,见方案4
      以dubbo为例子,我可以改变其注册的地址主机,而不是PODID
    apiVersion: extensions/v1beta1
    kind: Deployment
    metadata:
      name: dubbo-deployment
      namespace: default
    spec:
      replicas: 1
      template:
        spec:
          containers:
            - name: dubbo-app
              image: [your image]
              imagePullPolicy: Always
              env:
                  - name: DUBBO_IP_TO_REGISTRY  //或者在修改的源代码中加入该环境变量
                    valueFrom:
                      fieldRef:
                        fieldPath:  status.hostIP
                  - name: DUBBO_PORT_TO_REGISTRY  //此时数据可在编排时自动加入
                    value: "30011" 
              ports:
                - name: DUBBO_PORT_TO_REGISTRY  
                - containerPort: 30011
          imagePullSecrets:
          - name: harbor-key
    

    将名称为“DUBBO_PORT_TO_REGISTRY ”的端口写到环境变量中

      env:
          - name: DUBBO_PORT_TO_REGISTRY  //此时数据可在编排时自动加入
            value: "30011" 
    

    在动态分配完端口后覆盖分配的端口

        // added by gaogao start
        podSandbox,boxErr := ds.client.InspectContainer(podSandboxID)
        if boxErr!=nil {
            panic(boxErr.Error())
        }
        oldEnv := createConfig.Config.Env
        rpcContainerPort,_:=labels["DUBBO_PORT_TO_REGISTRY"]
        for key,value := range podSandbox.NetworkSettings.Ports {
            for index,v := range value{
                env :=""
                if (rpcContainerPort+"/tcp") == key.Port() {
                    oldEnv = append(oldEnv,"DUBBO_PORT_TO_REGISTRY="+strings.ToUpper(v.HostPort))
                }
                if index == 0{
                    env = "IPS_PORT_"+strings.ToUpper(strings.Replace(string(key),"/","_",-1))+"="+strings.ToUpper(v.HostPort)
                }else{
                    env = "IPS_PORT"+strconv.Itoa(index)+"_"+strings.ToUpper(strings.Replace(string(key),"/","_",-1))+"="+strings.ToUpper(v.HostPort)
                }
                oldEnv = append(oldEnv,env)
            }
        }
        createConfig.Config.Env = oldEnv
        // added by gaogao end
    

    3.场景

    第一种场景:mesos中主要是借助consul做服务发现;kubernetes中通过本身的svc和边缘路由我们采用nginx做对外转发。此处不做过多说明;

    第二种场景:mesos依然通过consul;kubernetes中为master和slave通过两个svc就可以实现;

    第三种场景:也是本次要着重说明的,对于mesos的方案,mesos会自动把动态分配的端口添加到环境变量中,在容器内的应用程序需要注册到其他服务器的时候,直接拿到对应的环境变量即可;但是对于kuebernetes中,如果通过svc去暴露端口(其实通过nodePort的方式就是这样),那么svc下的pod在被路由的时候很可能会被路由到非active节点,如果通过Pod本省和主机进行端口一一映射,那只能预先指定好hostport端口,这给端口的维护带来恨到麻烦,同时,指定端口的话同一类pod一台主机只能跑一个,受到很大限制。

    4.方案

    根据上述的描述,很多人可能会想到使用动态端口,这也是我的想法,于是将hostport设置成0,希望能让集群自己分配端口,但是结果并不想我想象的那样,不会在主机上映射任何端口,后来通过分析kubelet的代码发现,当hostport设置成0,kubelet不会做任何端口暴露,当中间要通过svc做跳转,实际上在kubelet的源码中的处理是,只有指定HostPort不为0的情况下,才能通过docker的exposedPorts把端口映射到主机。

    exteriorPort := port.HostPort
    //此处不会去设置exposedPorts
     if exteriorPort == 0 {
        // No need to do port binding when HostPort is not specified
        continue
     }      
    interiorPort := port.ContainerPort
    
    • 尝试1:把HostPort设置0的方案不行,那么在想能不能在kubelet调用docker创建容器(run一个容器本身有create和start两个动作)的时候吧HostPort设置成0,找到createContainer的过程进行修改docker_container.go。
    //containerPortsLabel = "io.kubernetes.container.ports"
    //annotationPrefix = annotation
    labelKey := annotationPrefix + containerPortsLabel
    if content, ok := labels[labelKey]; ok {
        var portMappings []IPSPortMapping
        json.Unmarshal([]byte(content), &portMappings)
        portSet := nat.PortSet{}
        mapping := struct{}{}
        for _, portMapping := range portMappings {
            if portMapping.HostPort == "0" || portMapping.HostPort == "" {
                ctnPort := nat.Port(portMapping.ContainerPort)
                portSet[ctnPort] = mapping
                portMapping[""] = port
            }
        }
        createConfig.Config.ExposedPorts = portSet
    }
    

    在尝试过程中,想到如果自动分配了,那动态的端口也无法写到容器的环境变量中,内部跑的容器势必也是无法拿到从而进行注册的,于是放弃。

    • 尝试2:既然无法提前拿到分配的动态端口写入环境变量,那么再想是否内提前分配端口,这样就可以拿到端口并写到环境变量,于是再次修改代码,指定PortBindings和环境变量。
       // 定义端口映射的结构体
       type IPSPortMapping struct {
          Protocol      string `json:"protocol"`
          ContainerPort string `json:"containerPort"`
          HostPort      string `json:"hostPort"`
          Name          string `json:"name"`
       }
       // 定义端口缓存
       var portCache map[string]string
       //定义并发锁
       var lock sync.Mutex
    
       ····省略其他原有代码
       // 创建时修改代码
       var portCache map[string]string
       // portCache 没有实例化先实例化
       if portCache == nil {
        portCache = make(map[string]string)
        }
        //containerPortsLabel = "io.kubernetes.container.ports"
        //annotationPrefix = annotation
        labelKey := annotationPrefix + containerPortsLabel
        ports := []string{}
        if content, ok := labels[labelKey]; ok {
        var portMappings []IPSPortMapping
        json.Unmarshal([]byte(content), &portMappings)
        portMap := nat.PortMap{}
        
        defer listen.Close()
        // 开启锁
        lock.Lock()
        defer lock.Unlock()
        for _, portMapping := range portMappings {
            if portMapping.HostPort == "0" || portMapping.HostPort == "" {
                // 获取随机端口,并且不在端口缓存中
                listen, _ := net.Listen("tcp", ":0") // listen on localhost
                port := strconv.Itoa(listen.Addr().(*net.TCPAddr).Port)
                for {
                    if _, ok := portCache[port]; ok{
                        port = strconv.Itoa(listen.Addr().(*net.TCPAddr).Port)
                    }else{
                        break
                    }
                }
                ctnPort := nat.Port(portMapping.ContainerPort +"/tcp")
                portBindings := []nat.PortBinding{}
                portBindings = append(portBindings, nat.PortBinding{"0.0.0.0", port})
                portMap[ctnPort] = portBindings
                // 记录随机端口到缓存,先将值设置为0,等创建完容器拿到ContainerID后更新,目的是在启动完容器后从换portCache中清楚
                portCache[port] = "0" 
                ports = append(ports,port)
            }
        }
        createConfig.HostConfig.PortBindings = portMap
        // added by gaogao end
    
         ····省略其他原有代码
    
        if createResp != nil {
            //更新ContainerID
            for _, port := range ports {
                portCache[port] = createResp.ID
            }
            return createResp.ID, err
        }
    
         ····省略其他原有代码
         // 启动时修改代码
        // 启动后从portCache中删除,防止以后容器停止,仍然占用端口
        lock.Lock()
        defer lock.Unlock()
        for k, value := range portCache {
            if strings.EqualFold(value,containerID){
                delete(portCache,k)
            }
        }
    

    上述代码都修改完成,满怀期待开始测试,但结果并不如意,报如下错误:

      Conflicting options: port publishing and the container type network mode
    

    其实惭愧看到现在我在知道原来kubernetes的应用容器原来采用的是Container网络模式,那么势必无法直接指定端口。此时也正好想起kubernetes中sandbox的概念和伴随业务容器的pause容器,遂查阅相关概念,找到下图,一图惊醒梦中人。


    image.png

    原来所谓的沙箱(sandbox)是这个意思,Kubernetes在启动Pod的时候先会启动pause容器,而pod中的其他容器会通过Container的方式挂到该容器上,这样pause容器和应用的容器就会在一个虚拟主机(POD)上公用一个IP。

      docker run -d --net=container:pause --ipc=container:pause --pid=container:pause tomcat
    

    看到这里,在想如果可以先启动一个位于同一IP( POD)上的pause容器,而且这个pause容器本身部署不是container的网络模式,那么是不是可以在pause容器中将相关的端口都暴露出去,由于pause容器时先于应用系统的容器启动的,那么在启动应用系统的容器时,我可以根据pause容器ID拿到对应的网络映射关系(动态分配的端口和容器内端口的关系),然后写入的应用系统的容器的环境变量中,应用在注册时就可以拿到宿主机的端口进行注册。

    此时基本的想法已经形成,但是还有一个问题没有解决,就是前面提到的即便将hostPort设置成0,kubelet也没有设置exposedPorts,所以也不会自动分配端口,所以要找到kubelet对应的位置进行修改。遂进行第三次尝试。

    尝试3:①在启动sandbox时,让docker能自动的分配端口;②在启动应用系统容器时从pause容器中拿到NetworkSettings拿到内外端口的映射关系以环境变量的形式写入到应用系统容器。③让scheduler调度是不校验该端口

    针对①,主要涉及kubelet的修改,其实此时做了扩展,当hostport设置成1时动态分配端口,设置成0是保留以前的处理动作(毕竟有好多端口不需要暴露,此方式个人认为有必要保留),其次使用1其实只是作为标志使用,不会占用端口(毕竟端口1也是敏感端口)

    k8s.io/kubernetes/pkg/kubelet/dockershim/helpers.go中173行左右

       // added by gaogao start
       if exteriorPort == 1 {
           exteriorPort = 0
       }
       // added by gaogao end
    

    针对②,主要涉及kubelet,在启动应用容器时,从沙箱容器(pause容器中)获取到动态端口的映射关系,写到应用容器的环境变量中。

    在文件k8s.io/kubernetes/pkg/kubelet/dockershim/docker_container.go中143行左右

      podSandbox,boxErr := ds.client.InspectContainer(podSandboxID)
      if boxErr!=nil {
          panic(boxErr.Error())
      }
      oldEnv := createConfig.Config.Env
      for key,value := range podSandbox.NetworkSettings.Ports {
          for index,v := range value{
              env :=""
              if index == 0{
                  env = "IPS_PORT_"+strings.ToUpper(strings.Replace(string(key),"/","_",-1))+"="+strings.ToUpper(v.HostPort)
              }else{
                  env = "IPS_PORT"+strconv.Itoa(index)+"_"+strings.ToUpper(strings.Replace(string(key),"/","_",-1))+"="+strings.ToUpper(v.HostPort)
              }
              oldEnv = append(oldEnv,env)
          }
      }
      createConfig.Config.Env = oldEnv
    

    针对③,主要调整插件中的调度算法,主要涉及scheduler和kubelet:

    在文件k8s.io/kubernetes/plugin/pkg/scheduler/algorithm/predicates/predicates.go ,在859行左右,当wport 为1,调度时跳过。

      for wport := range wantPorts {
            if wport != 0 && wport != 1 && existingPorts[wport] {
                return false, []algorithm.PredicateFailureReason{ErrPodNotFitsHostPorts}, nil
            }
        }
    

    经过上述编译后,测试一切如愿,达到mesos中的效果,当然对于后续的处理方式可能会采用其他的方式,比如和应用相关的istio等。
    综上,对于kubernetes中实现容器端口的动态分配,暴露,写入应用系统环境变量已实现。

    实际上把需要映射的端口写写在主街上更好,这样就不需要修修改调度器的代码

    相关文章

      网友评论

          本文标题:104.Kubernetes实现微服务和RPC服务支持

          本文链接:https://www.haomeiwen.com/subject/gvbhsftx.html