Skip to content

Latest commit

 

History

History
173 lines (120 loc) · 14.7 KB

建议.md

File metadata and controls

173 lines (120 loc) · 14.7 KB

服务注册与发现:

  • 为什么要使用服务注册与发现?

    当服务数量多到一定程序,再去写配置文件就不太合适了,那样的运维会非常麻烦,而且在K8S上POD IP是不停变化的。

  • 选型

    • etcd或zookeeper,这一类的kv存储

      本质上是将IP列表写入到一个key下面作为value。自己的程序运行一个etcd或zookeeper的客户端去watch这个key的变化,当变化后自己需要将本地缓存的IP列表刷新。

      实际上是要你自己写一个负载均衡的算法在自己的程序里面,通常可以写一个公共库来完成,比如最简单的round robing负载方法,每次发器连接请求的时候都去IP列表中找一个IP来连接,或者能自己写一个连接池最佳。

      对于TCP长连接应用是无效的,因为TCP连接打开的时候会在内核表开启一个文件描述符FD,所有的tcp数据IO都要通过这个FD来完成。一个机器上的TCP无法转移到另一个机器上,除非完全继承FD和TCP包序。VIP就特别不适合TCP长连接应用,VIP漂移后TCP也断了。

      说了这么多,实际上你要是会写kubernetes client-go(java版本不叫这个名字)的话,可以自己去用informer监听Endpoint里面的IP列表,只有Running的POD IP才会被写入到Endpoint的IP列表中。因为K8S Service的负载就是这么干的,下面会说到。

    • 基于K8S Service(IPVS方案)

      • 有头服务,即有clusterIP的Service

        假如现在有一个服务的名字叫做 nginx 它的ClusterIP是10.24.8.8,它对应的同名Endpoint里面包含了三个IP [ 10.25.1.2 , 10.25.1.3 , 10.25.1.4 ]。

        当我们的程序如 curl nginx:80 的时候,curl会去执行一次DNS查询。

          由于linux系统默认是没有DNS缓存的,如果需要性能应当考虑增加kubernetes coreDNS的数量或者在集群中运行node local dns cache。
          注意每一次DNS查询都会触发UDP报文发送,这又会触发conntrack追踪的连接过多的table爆表,而UDP实际上非常短,短到conntrack无法追踪而耗死自己导致大量连接丢失,造成网络抖动。conntrack默认的table大小是1MB
          建议使用linux 4.5以上的ebpf方案代替,如cilium作为容器网络。当你使用了这个方案的时候,可以考虑扔掉nodelocal DNS Cache,但要记得扩容coreDNS。
        

        当完成DNS查询之后,curl拿到了service的cluster IP并访问这个IP,本地的IPVS路由表里面早就有这个clusterIP的路由规则,本地的IPVS路由会执行一个round robin负载方法选择一个POD IP去作为最终的流量目标地址。

          注意这里的IPVS路由规则会在每一个主机上都有一份。也就是说当你有一万个POD的时候,每一台宿主机都有这一万个POD的IPVS路由规则,宿主机之间是可以互相访问对象的POD IP的,除非做了kubernetes NetWorkPolicy。因为这些POD IP实际上是通过CNI创建的网卡来接收网段内的流量的,注意复习一个IP哪些部分叫网络号哪些部分叫主机号。
        
      • 无头服务

        一次DNS查询会直接返回Endpoint里面的POD IP列表,而不是SERVICE的clusterIP,因为此时service也没有clusterIP,所以无头服务实际上是要你自己去做负载均衡,并且是基于DNS查询的负载均衡,你要确保自己的客户端足够的聪明,能做到DNS级别的负载均衡再考虑使用无头服务。

  • 禁止缓存服务注册系统的数据在磁盘文件上

    缓存服务注册系统的数据在文件上干嘛呢,为了增加磁盘IO么。要知道服务数据是不停变化,应当把这些服务数据缓存在程序的内存中。

    定期同步数据大概十秒,主动去服务发现系统同步数据。当然服务注册发现系统也会主动推送服务的数据给服务注册发现系统的客户端。

    再说了,缓存在磁盘上,watch文件这种事情只有不包含服务注册与发现客户端,同时能watch文件变更或热重载的软件才需要这么做。

dockerfile构建镜像

  • 禁止使用ENTRYPOINT

    这样的镜像无法使用docker run -it --rm image:tag bash 这样的方式提前进入镜像并修改配置,最后人工启动程序来做调试。(k8s 环境也会无法人工调试)

    因为ENTRYPOINT执行顺序在CMD之前,导致程序还没等我进去直接运行了。

    如果你的程序启动脚本方法是这样的

    #!/bin/bash
    set -ex;
    vps \
    -c=a.yaml \
    --name="fsdfs"

    那么建议将这段shell脚本放入一个shell文件 /run.sh , 在dockerfile构建时赋予执行权限 最后

    CMD ["/run.sh"]
  • 镜像号标准化

    务必使用v1.0.0这样的格式,如果是特定平台则 v1.0.0-windows-cubda10.0.1 这样的tag号,这样非常方便替换大版本。

    在流水线构建的时候,镜像的tag建议是github分支号外加构建次数,比如 myimage:CLOUD-100_1,这表示CLOUD-100的分支的第一次构建,这样就可以触发K8S更新了。

    但是流水线要小心statefulset的更新坑,当statefulset使用了一个正确的镜像,但下一次的镜像是错误的则会引发POD错误,然后再换成正确的镜像,此时你会发现statefulset的POD并不会更新到正确的镜像,这个issue的github卡在这里。

    • issue
    • 代码设定kubernetes v1.19.4 /pkg/controller/statefulset/stateful_set_control.go
    // If we have a Pod that has been created but is not running and ready we can not make progress.
    // We must ensure that all for each Pod, when we create it, all of its predecessors, with respect to its
    // ordinal, are Running and Ready.
    if !isRunningAndReady(replicas[i]) && monotonic {
        klog.V(4).Infof(
            "StatefulSet %s/%s is waiting for Pod %s to be Running and Ready",
            set.Namespace,
            set.Name,
            replicas[i].Name)
        return &status, nil
    }

运行在k8s内的程序要求

  • 列出自己支持哪些环境变量作为初始化自身的配置。
  • 能适应k8s环境的动态伸缩,并根据headless服务做集群成员的互相发现。因为无头服务的本质是一次DNS查询返回多个Runinng状态的POD IP
  • 能读取configmap 或者 secret,用于初始化自己
  • 要将自己的配置项放到环境变量上,方便k8s管理员管理集群配置,如果太多写死在configmap里面会引发另一个问题,即configmap的刷新时间和kubelt与kube-apiserver的同步时间一直,默认是六十秒。
  • 善用client-go informer cache来降低对kube-apiserver的访问压力。

小心K8S的无头服务 Headless service

  • 问题描述

    文献引用

    无头服务的本质是一次DNS查询会返回所有该服务对应的Endpoint里面的IP列表,即一次DNS查询会返回多个IP地址,所以就要确保客户端是否足够聪明,能将DNS中的三个地址加速自己的连接池中,如果不带连接池则一定不要使用无头服务。

    再来说说有头服务。他的本质是利用自己主机上的Round Robin功能。进行转发,我们这里不考虑iptables转发,因为没有任何人还会再去使用iptables构建容器网络。这里说说IPVS。

    假如Service的clusterIP地址是10.24.0.3,对应的同名的Endpoint 里面的IP列表有10.25.0.2,10.25.0.3。则会在主机上产生一条IPVS路由规则,负载方法K8S默认使用RR,因此IPVS转发规则也是RR。 容器访问10.24.0.3的时候会使用到本机的IPVS转发规则,因此实际上RR规则是运行在每一台主机上的。

日志规范化

  • 要求

    需要打印出抛错代码的文件路径和行号,告诉我是什么模块因为什么问题导致的,比如 /a/b/c.go 访问mysql服务失败,错误原因是 -> 里面包裹的错误信息如mysql密码错误。

  • 格式

    使用JSON { "msg": "这是一条错误", "time": "2019-11-11 10:00:09" , "username":"admin" ,"module": "user", "requestURL": "http://baidu.com" } ,尽可能地打印本地用到的访问参数注意密码等信息要脱敏,即变成六个******。

      注意JSON只能支持UTF-8编码,websocket默认的textMesage传输方法也是只能支持UTF-8编码,对于websocket要传输其他编码还请使用websocket的binaryMessage并搭配前端JS的TextDecoder对象来实现。
    

开发流程 All In Jira

  • 先决条件

    当前git主仓库包含三个分支 分别是 master,release-1.0,release-1.1 ,release-2.0

    其中git 仓库 tag 分别是 v1.0.0 v1.0.1 v1.1.0 v2.0.0 v2.0.1 v2.1.0

    当我开发的任务再jira上的fixVersion=v1.0.2的时候,我所有的代码都会合并到release-1.0的分支上,而不是合并到master分支,当我开发完v1.0.1的所有jira任务后,基于release-1.0做一个git tag=v1.0.1,然后继续开发v1.0.2的版本。同理类推v2.0.2的代码应该合并到release-2.0上面,v1.1.1的代码应当合并到release-1.1上面.

    当我开发v3.0.0的代码的时候,此时没有release-3.0说明v3.0.0是一个未来版本我需要将fixVersion=v3.0.0的代码变更都合并到master上面。

    当我开发完v3.0.0的任务之后,基于master的分支做一个git branch=release-3.0。并做一个git tag=v3.0.0。

  • 建jira卡

    新建一堆的jira任务卡,不设工期,不设修复版本,不指定执行人,写好问题复现方法在描述中,或者在描述中写好要做的事情的原因和期望结果。

  • 开会分任务

    每一个sprint开始之前,先粗略地将任务分配各模块的主要负责人或大方向的小组领导。由他们分给手下的所有开发者,并做一个内部技术评估。这些领导最好具备对自己的名下的产品的逻辑流程了解得很清楚,否则开会的时候会较难沟通。

    每一个jira sprint结束的时候,评审本期那些完成了的,那些未完成的,听取未完成的原因。

  • 任务的开始

    当我开始执行jira任务CLOUD-100的时候,在个人github上建立一个分支叫CLOUD-100,然后标记jira任务CLOUD-100的状态为inProgress。

  • 开发者之间的联动测试

    请使用kt-connect来完成服务的本地映射和替换,这玩意儿当你访问本地执行一个 mysql服务映射到k8s的时候,集群内所有的依赖mysql的服务都会将流量导向本地的mysql。当你终结本地的mysql服务映射的时候,流量又将回到集群里面原先已经存在的哪个mysql服务。

    多个开发者之间互相映射自己的服务到K8S集群即可实现本地联调。但要注意你需要一定的K8S RBAC权限才能完美运行KT CONNECT。

  • 任务的提测

    当开发者将任务开发到自测通过之后,假如我正在开发未来版本即先决条件所说的v3.0.0,执行 git rebase -i master 将更改合并为一个commit,这样方便回滚也方便审查代码。然后git push -f CLOUD-100

    当我开发v2.0.2的时候,git rebase -i release-2.0,并合并代码更改为一个commit。然后git push -f origin CLOUD-100。

      注意在rebase的时候,如果有govendor也要独立为一个commit,代码变更单独作为一个commit,你不想让审查代码的人去看govendor的巨量变更吧。
    

    然后建立git web ui pull request,并设置好测试人员和审核人员以便他们有权限在我的pull request上有标记"通过"的权力。

    此时流水线会自动根据pull request建立一个构建流水线。构建出来的镜像 如 repo:PR-100_1,如果pull request没有被关闭且被更新则仍然保留流水线,此时构建出来的镜像是 repo:PR-100_2。但要注意流水线不能主动去部署,PR产生的镜像需要测试人员去部署,只有开发者产生的镜像才可以设置为自动部署到某个环境,当然开发者愿意做本地调试则没有这个必要。

    最后更改jira状态为readyForReview,并assign给代码审查人员。代码审查人员将收到jira/git变更的通知,并自行安排什么时候去审阅。

  • 代码审阅

    审查开始的时候,需要将jira状态更改为inReview。

    必须是对本仓库的代码逻辑有着较为清晰认识的人。审查人员要注意开发是否写了注释,是否写了class/strcut 字段的取值范围,以及每个字段等于某个值的时候会发生什么(如果字段的取值范围是多个)。

    并且需要查验开发是否写清楚了这个package/namespace的总体运行逻辑,如果写上了注意事项更好,对于人员流动性高的项目组这很重要。

    最后如果觉得没问题则,更改jira状态为readyForTest,更改git web ui上的,并assign给测试人员。测试人员将收到jira/git变更的通知,并自行安排什么时候去验收。

    如果觉得有问题就将jira状态更改为fallback或issued,并且要在pull request上备注出来是哪几行因为什么原因导致的不通过,如果能给出更改建议更好。此时开发将会收到jira通知。

  • 代码测试

    如果公司存在一套DEVOPS UI,则测试人员在UI上根据PR号搜到对应的流水线,然后点击流水线界面上的部署按钮,决定部署到哪个环境里面去做测试。

    如果公司没有一套上述的UI,则开发人员需要在UI上写出本次测是的镜像地址,建议在测试的时候对POD使用imagePullPolicy: Always以保证镜像是最新的.

    测试开始后需要将jira状态标记为testing,测试通过之后则标记jira状态为tested。并在git web ui上标记测试通过,以方便仓库管理员进行合并。

    测试不通过,则在git web ui pull request下面写上测试不通过的原因。并标记jira状态为fallback或issued。

  • 代码合并

    仓库管理员需要自己多留意有哪些测试和审查都通过了还没有进行合并的PR,最好公司写一个git工具来定期扫描各个仓库的PR,并通知各个仓库的管理员去合并PR。

    在合并PR之后,仓库管理员需要去标记jira状态为resolved,当然这个操作也可以由开发者自己来做,毕竟PR被合并之后会有一个通知消息,但还是建议仓库管理员来做这个事情。

    到此 以一个jira任务的生命周期完结。

升级到K8S 1.19之后要注意的事情