容器踩坑集

之前没有做记录的习惯,技术上踩的许多坑都散落在公司wiki、在线笔记、复盘纪要里……匆匆6年过去了,对很多没来得及总结的问题,在这里简单记上一笔。

容器与编排初尝

容器内进程的优雅关闭问题

  • 问题:docker stop containerId 后容器内进程未正常关闭

    • 原因:容器内进程以shell脚本方式启动,pid=1的进程是bash 因此业务进程未感知到SIGTERM,而是直接被SIGKILL
    • 解决:脚本中从java -jar启动改为:exec java -jar xx ;并确保dockerfile中启动方式用args模式(不调用shell即执行命令)
  • 问题:mesos默认关闭docker容器为非优雅关闭

    • 原因:mesos关闭容器时调用docker命令:docker stop containerId -t ${ docker_stop_timeout }
    • 解决:设置对应参数,否则默认为0,则跳过SIGTERM,直接发SIGKILL
    • 参考:https://github.com/mesosphere/marathon/issues/712 https://issues.apache.org/jira/browse/MESOS-4279
  • 问题:注册在eureka中的应用重启期间较长时间不可用。最长有3分钟以上

    • 原因:默认30秒更新间隔,3分钟以上说明client没来得及告知eureka下线,发现client的exit code是137(即128+9)被kill导致未能优雅关闭,继而导致eureka启用保护,在最长超时时间后才会将其下线。
    • 解决:解决上面的优雅关闭问题即可。

微服务与容器编排

服务发现的冲突

  • 问题:容器滚动升级期间,比如新启动3个新实例,注册到Eureka上后,老实例一直未能下线。

    • 原因:Eureka的自我保护机制:当网络出现抖动或Eureka出现故障的时候,大量服务在短期内会频繁的注册与取消注册,从而引起不可用。为避免上述情况,Eureka会默认启用自我保护模式——即如果某注册服务在15分钟内可用度低于85%,已注册的服务信息不会被删除,依旧可被访问。但在Docker环境或小规模应用中,每次上线都通过新建同等规模的实例去替换旧的实例,很容易触发自我保护,这种保护模式会让不可用的服务成为资源池的一部分,造成部分访问失败。按照这个思路,要么在Eureka的控制界面上通过 /refresh强制刷新,要么重启整个Eureka集群,这两种方式在大规模的服务中是不现实的,因此我们选择关闭Eureka的自我保护模式。
    • 解决:eureka.server.enable-self-preservation=false
  • 问题:容器滚动升级期间,Eureka非实时更新,部分流量依然走到老的已停止的实例上导致不可用。

    • 原因:Zuul根据配置定时(默认30秒)向Eureka获取相关信息。但当某个服务实例关闭后并从Eureka取消注册,如果Zuul还未更新,在此期间,用户经由Zuul发起的请求是不可达的。
    • 解决:使用ingress(k8s)或haproxy,nixy,mlb(marathon)等方案,替代zuul+eureka。

低版本内核的问题

  • mesos集群在低版本内核(3.10)上发生过大范围的文件系统崩溃现象,同时期高版本(4.14)没问题
  • k8s集群在3.10版本内核发生过数次假死现象,异常日志都伴随:”SLUB Unable to allocate memory in node“,意思cgroup内存泄漏bug,升级到4.10之后好转。

镜像的制作

  • 背景:为了降低大家使用容器的成本、尽快实现容器化编排,我们将用户端的操作如打包、制作镜像环节大幅简化。
  • 实现:marathon具有一个fetcher功能(类似prestart),能够在容器启动前先去远端拉取一个包并自动解压到特定目录,利用这个功能,可以让所有同一语言的应用都使用一个基础镜像即可,对应的yaml中设置一个包下载路径,便可下载、解压、执行启动脚本(配合环境变量识别)完成发布过程。
  • 问题:这样减少了镜像私服的压力,但在整个环节中新增了一个包下载中心,以及后续引入k8s时较为麻烦,而且不符合docker一次构件,任意运行的原则,更无法分享给外部
    • 解决:取消此类做法,引入harbor优化私服能力。
  • 效果:用户输入git地址、项目名、子目录层级(如果有)、环境名(dev,test,beta,prod)。我们基于这些信息实现自动打包、制作镜像、推送到特定私服仓库、并跑测试。后来发现这个和openshift的source2image一样。

docker registry的GC

docker私服在删除镜像时并未真正将空间释放,需要在GC的时候才能真正删除,腾出空间。但GC的时候仓库只可读不可写,需做好紧急方案,应对只读期间的上线。(备机方案)

flannel若干问题

k8s最大节点数限制

  • 问题:由于初期同事flannel的网段设置为FLANNEL_NETWORK为B类地址,FLANNEL_SUBNET是C类地址,这导致只有一个C的地址即256个可以分配个各node。
  • 解决:
    1. 将FLANNEL_NETWORK修改为A类地址,但前缀不要改,如11.0.0.0/16 改为11.0.0.0/8 如下:
    etcdctl set  /coreos.com/network/config  '{"Network":"11.0.0.0/8"}'
    
    1. 新节点启动后自动获取到相关网段信息
    2. 老节点在重启flannel服务后,也会更新为A类配置
    3. 在此期间pod跨主机通信不受影响,因为在创建网段的时候,flannel生成一个网段,然后向etcd中提交,如果同名网段etcd中已经存在,就换一个网段重试,从而避免了网段冲突。只要每个node上的子网前缀是不变的,原先的node分配的网段不会发生变化,新增node的网段不会与其它node的网段重叠。

版本升级并将模式从udp到vxlan

  • 先升级版本,再切换模式
  • 先master,再node 平滑升级:
  1. 检查各节点docker ps状态,将有异常的节点先剔除避免后续卡住。
  2. 检查flannel service的被动依赖,确保flanneld.server中的RequireBy=docker.service 被注释。
  3. 检查docker的主动依赖:清除/etc/systemd/system/docker.service.requires/
  4. 备份配置:/usr/bin/flanneld /usr/lib/systemd/system/flanneld.service /etc/sysconfig/flanneld
  5. 分发新版本的二进制包和配置文件
  6. 再次检查依赖关系取消
  7. daemon-reload
  8. 重启flanneld
  9. 测试
  10. 异常回滚

切换模式

  1. 检查当前etcd中的flannel配置:get /atomic.io/network/config {“Network”:“10.12.0.0/16”,“SubnetLen”:26,“Backend”:{“Type”:“udp”}}
  2. 新建/atomic.io/networkvxlan/config {“Network”:“10.12.0.0/16”,“SubnetLen”:26,“Backend”:{“Type”:“vxlan”}}
  3. 备份各节点的flannel配置:/etc/sysconfig/flanneld主要是修改ETCD_PREFIX为新的/atomic.io/networkvxlan
  4. 停止flanneld服务
  5. 清除udp模式下生成的各类网络信息,如
ip neigh flush
ip link set flannel0 down
ip link del dev flannel0
  1. 启动flanneld服务
  2. 检查iptables规则,确保forward为accept策略
iptables -P FORWARD ACCEPT
  1. 测试
  2. 异常回滚

编排的选择和转换

选择
  • 在使用mesos+marathon的初期,结合微服务用的挺好,但是随着越来越多功能诉求的挖掘,我发现marathon已经很吃力,并且其自身bug的解决效率也很低。而由于在初期我们就考虑要兼容k8s,因此将marathon、k8s的接口之上再封装一层,这对于3-5人的团队,同时维护两套编排系统已经非常吃力,伴随着大量的推广、维稳成本,压力非常之大。
  • marathon的提供方mesosphere开始力推他们的DCOS,这个半商业产品也给我们带来很大的不确定性。在试用过程中遇到的问题很多。
  • 而与此同时,虽然k8s的问题也很多,bug不断,但是其一直确定的开源立场,频次极高的更新状态,以及越来越好的社区活跃度,更优雅全面的设计,这些都足够吸引人。

于是,我们确定全面转向k8s。

转换
  1. 第一步是新应用停止再上marathon,而是直接走k8s,减少后续迁移成本
  2. 第二步是已有应用的迁移功能开发,一方面让用户适配k8s的部分情况,如日志标准化、服务注册要脱离eureka都走ingress(早先marathon日志走docker的gelf,注册既支持eureka也支持haproxy,nixy等)
  3. 第三步是宣讲,告知用户我们的转换意图,给他们带来的后续好处以及需要他们的哪些支持。
  4. 第四步,资源回收,写脚本不断统计闲置节点、资源使用极低的节点做及时回收。
  5. 第五步,k8s集群的资源自动扩容,监控配合脚本,在集群资源不足时自动创建机器加入集群。

总结,下线marathon的过程持续了一年,从生产到测试各套环境,从新业务、边缘业务到核心业务,最终mesos集群全部转到k8s集群。