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

2018-06-01  本文已影响0人  doublegao

1.背景

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

2.思路

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

这是最直接的方法,让客户端和服务端的网络呢能够处于同一个平面上,该方式在不通的网络插件中实现也有所不同,同时也有局限性。
如果是Flannel(隧道) ,那么我们可以在上层的网关或者路由中,增加PODIP的相关路由,这种方式回牵扯到上层设备的修改,节点的变化需要和上层设别进行同步。
如果是calico(BGP),一方面可以将内部路由同步到上层设备,另一方面也可以将路由同步到目标主机,但受到客户端主机的位置是否在同一个平面网络的影响。
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
//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
}

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

   // 定义端口映射的结构体
   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中实现容器端口的动态分配,暴露,写入应用系统环境变量已实现。

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

上一篇下一篇

猜你喜欢

热点阅读