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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容