本来生活网(benlai.com)是一家生鲜电商平台,提供蔬菜、水果、海鲜等优质生鲜果蔬食材食品网购服务。当今容器技术被广泛关注,本来生活网在经历了一年的技术革命后,基本完成了容器化所需的基础设施建设。本文介绍本来生活网在 Kubernetes 落地过程中的实践和经验。
本来生活网是一家生鲜电商平台,公司很早就停止了烧钱模式,开始追求盈利。既然要把利润最大化,那就要开源节流,作为技术可以在省钱的方面想想办法。我们的生产环境是由 IDC 机房的 100 多台物理机所组成,占用率高达 95%,闲置资源比较多,于是我们考虑借助 Kubernetes 来重构我们的基础设施,提高我们资源的利用率。
容器化项目团队最初加上我就只有三个人,同时我们还有各自的工作任务要做,留给容器化的时间较少,因此我们要考虑如何快速的搭建容器平台,避免走全部自研这条路,这对我们来说是个巨大的挑战。在经历了一年的容器化之旅后,分享下我们这一年所踩过的坑和获得的经验。
在搭建 Kubernetes 集群前,有很多问题摆在我们面前:
作为小团队去构建一个容器平台,自研的工作量太大了。前期我们调研过很多可视化平台,比如 Kubernetes Dashboard 和 Rancher 等等,但是要操作这些平台得需要专业的 Kubernetes 运维知识,这对我们的测试人员不太友好。后来我们尝试了 KubeSphere(kubesphere.io) 平台,各方面都比较符合我们的需求,于是决定使用该平台作为我们容器平台的基础,在这之上构建我们自己的发布流程。
我们的项目有 Java 也有 .NET 的,.NET 项目占了 80% 以上。要支持 .NET 意味着要支持 Windows。在我们去年开始启动项目的时候,Kubernetes 刚升级到 1.14 版本,是支持 Windows 节点的第一个版本,同时功能也比较弱。经过实验,我们成功对 .NET Framework 的程序进行了容器化,在不改代码的前提下运行在了 Windows 服务器上,并通过 Kubernetes 进行管理。不过我们也遇到了一些比较难处理的问题,使用下来的总结如下:
我们调研了一段时间后决定放弃使用 Linux 和 Windows 的混合集群,因为这些问题会带来巨大的运维成本,而且也无法避免 Windows 的版权费。
我们也考虑过把这些项目转换成 Java,但其中包含大量的业务逻辑代码,把这些重构为 Java 会消耗巨大的研发和测试的人力成本,显然这对我们来说也是不现实的。那么有没有一种方案是改动很少的代码,却又能支持 Linux 的呢?答案很明显了,就是把 .NET 转 .NET Core。我们采用了这种方案,并且大多数情况能很顺利的转换一个项目而不需要修改一行业务逻辑代码。
当然这个方案也有它的局限性,比如遇到如下情况就需要修改代码无法直接转换:
这些修改的成本是可控的,也是我们可以接受的。到目前为止我们已经成功转换了许多 .NET 项目,并且已运行在 Kubernetes 生产环境之上。
由于我们是基于物理机部署也就是裸金属(Bare Metal)环境,所以无论基于什么平台搭建,最终还是要考虑如何暴露 Kubernetes 集群这个问题。
我们分别试用了两套方案 MetalLB(metallb.universe.tf)和 Porter(github.com/kubesphere/porter),这两个都是以 LoadBalancer 方式暴露集群的。我们测试下来都能满足需求。Porter 是 KubeSphere 的子项目,和 KubeSphere 平台兼容性更好,但是目前 Porter 没有如何部署高可用的文档,我在这里简单分享下:
前置条件
配置和部署
架构和逻辑
Porter有两个插件:Porter-Manager 和 Porter-Agent。
Porter-Manager 是使用 Deployment 部署到 Master 节点上的,但默认只部署1个副本,它负责同步 BGP 路由到物理交换机(单向 BGP 路由,只需将 Kubernetes 私有路由发布给交换机即可,无需学习交换机内物理网络路由)。还有一个组件,Porter-Agent,它以 DaemonSet 的形式在所有节点都部署一个副本,功能是维护引流规则。
高可用架构
部署好后,你可能会有疑问:
一般路由器或交换机都会准备两台做 VSU(Virtual Switching Unit)实现高可用,这个是网络运维擅长的,这里不细讲了。主要说下其他几点怎么解决:
MetalLB 的高可用部署也是类似思路,虽然架构稍有不同,比如它和路由器进行 BGP 通信的组件是 Speaker,对应 Porter 的 Manager;它与 Porter 的区别在于高可用目前做的比较好;但是 Local 引流规划不如 Porter,EIP 的下一跳节点必须是 BGP 对等体(邻居)。
Kubernetes 的 ConfigMap 和 Secret 在一定程度上解决了配置的问题,我们可以很轻松的使用它们进行更改配置而不需要重新生成镜像,但我们在使用的过程中还是会遇到一些痛点:
为了解决这些痛点,我们综合考虑了很多方案,最终决定还是使用一套开源的配置中心作为配置的源,先通过 Jenkins 把配置的源转换成 ConfigMap,以文件形式挂载到 Pod 中进行发布,这样以上的问题都可以迎刃而解。
我们选择了携程的 Apollo(github.com/ctripcorp/apollo) 作为配置中心,其在用户体验方面还是比较出色的,能满足我们日常维护配置的需求。
在迁移微服务到 Kubernetes 集群的时候基本都会遇到一个问题,服务注册的时候会注册成 Pod IP,在集群内的话没有问题,在集群外的服务可就访问不到了。
我们首先考虑了是否要将集群外部与 Pod IP 打通,因为这样不需要修改任何代码就能很平滑的把服务迁移过来,但弊端是这个一旦放开,未来是很难收回来的,并且集群内部的 IP 全部可访问的话,等于破坏了 Kubernetes 的网络设计,思考再三觉得这不是一个好的方法。
我们最后选择了结合集群暴露的方式,把一个微服务对应的 Service 设置成 LoadBalancer,这样得到的一个 EIP 作为服务注册后的 IP,手动注册的服务只需要加上这个 IP 即可,如果是自动注册的话就需要修改相关的代码。
这个方案有个小小的问题,因为一个微服务会有多个 Pod,而在迁移的灰度过程中,外部集群也有这个微服务的实例在跑,这时候服务调用的权重会不均衡,因为集群内的微服务只有一个 IP,通常会被看作是一个实例。因此如果你的业务对负载均衡比较敏感,那你需要修改这里的逻辑。
我们一直使用的是点评的 CAT(github.com/dianping/cat) 作为我们的调用链监控,但是要把 CAT 部署到 Kubernetes 上比较困难,官方也无相关文档支持。总结部署 CAT 的难点主要有以下几个:
为了把 CAT 部署成一个 StatefulSet 并且支持扩容,我们参考了 Kafka 的 Helm 部署方式,做了以下的工作:
扩容很简单,只需要在配置中心里添加一条实例信息,重新部署即可。
由于 KubeSphere 平台集成了 Jenkins ,因此我们基于 Jenkins 做了很多 CI/CD 的工作。
起初我们为每个应用都写了一个 Jenkinsfile,里面的逻辑有拉取代码、编译应用、上传镜像到仓库和发布到 Kubernetes 集群等。接着我为了结合现有的发布流程,通过 Jenkins 的动态参数实现了完全发布、制作镜像、发布配置、上线应用、回滚应用这样五种流程。
由于前面提到了 ConfigMap 不支持版本控制,因此配置中心拉取配置生成 ConfigMap 的事情就由 Jenkins 来实现了。我们会在 ConfigMap 名称后加上当前应用的版本号,将该版本的 ConfigMap 关联到 Deployment 中。这样在执行回滚应用时 ConfigMap 也可以一起回滚。同时 ConfigMap 的清理工作也可以在这里完成。
随着应用的增多,Jenkinsfile 也越来越多,如果要修改一个部署逻辑将会修改全部的 Jenkinsfile,这显然是不可接受的,于是我们开始优化 Jenkinsfile。
首先我们为不同类型的应用创建了不同的 yaml 模板,并用模板变量替换了里面的参数。接着我们使用了 Jenkins Shared Library 来编写通用的 CI/CD 逻辑,而 Jenkinsfile 里只需要填写需要执行什么逻辑和相应的参数即可。这样当一个逻辑需要变更时,我们直接修改通用库里的代码就全部生效了。
随着越来越多的应用接入到容器发布中,不可避免的要对这些应用的发布及部署上线的发布效率、失败率、发布次数等指标进行分析;其次我们当前的流程虽然实现了 CI/CD 的流程代码复用,但是很多参数还是要去改对应应用的 Jenkinsfile 进行调整,这也很不方便。于是我们决定将所有应用的基本信息、发布信息、版本信息、编译信息等数据存放在指定的数据库中,然后提供相关的 API,Jenkinsfile 可以直接调用对应的发布接口获取应用的相关发布信息等;这样后期不管是要对这些发布数据分析也好,还是要查看或者改变应用的基本信息、发布信息、编译信息等都可以游刃有余;甚至我们还可以依据这些接口打造我们自己的应用管理界面,实现从研发到构建到上线的一体化操作。
我们在构建我们的测试环境的时候,由于服务器资源比较匮乏,我们使用了线上过保的机器作为我们的测试环境节点。在很长一段时间里,服务器不停的宕机,起初我们以为是硬件老化引起的,因为在主机告警屏幕看到了硬件出错信息。直到后来我们生产环境很新的服务器也出现了频繁宕机的问题,我们就开始重视了起来,并且尝试去分析了原因。
后来我们把 CentOS 7 的内核版本升级到最新以后就再也没发生过宕机了。所以说内核的版本与 Kubernetes 和 Docker 的稳定性是有很大的关系。同样把 Kubernetes 和 Docker 升级到一个稳定的版本也是很有必要的。
我们目前对未来的规划是这样的:
Q:Kubernetes 在生产上部署,推荐二进制还是 kubeadm 安装?kubeadm 安装除了提高运维难度,在生产上还有什么弊端?
A:我们使用了 kubeadm 部署 Kubernetes;建议选你们运维团队较熟悉的那种方式部署。
Q:我们这用的是 Dubbo,Pod 更新的时候,比如说已经进来的流量,我如何去优雅处理,我这 Pod 号更新的时候,有依赖这个服务的应用就会报 Dubbo 超时了。
A:这个我们也在优化中,目前的方案是在进程收到 SIGTERM 信号后,先禁止所有新的请求(可以使 readinessProbe 失败),然后等待所有请求处理完毕,根据业务特性设置等待时间,默认可以为 30 秒,超时后自动强制停止。
Q:Jar 包启动时加 JVM 限制吗?还是只做 request 和 limit 限制?
A:我的理解是 request 和 limit 只是对整个容器的资源进行控制;而 JVM 的相关参数是对容器内部的应用做限制,这两者并不冲突,可以同时使用,也可以单独使用 request 和 limit;只不过 JVM 的限制上限会受到 limit 的制约。
Q:.NET Core应用部署在 Kubernetes 中相比 Java 操作复杂吗?想了解下具体如何从 NET 转到 .NET Core。
A:实际在 Kubernetes 内部署 .NET Core 和部署 Java 都一样,选择好对应的 .NET Core 基础镜像版本;然后以该版本的为基础制作应用的镜像后部署到 Kubernetes 即可,只不过在选择 .NET Core 的基础镜像时,我建议直接选择 SDK 正常版本。我们试过 runtime、sdk-alpine 等版本,虽然这些版本占用空闲小,但是你不知道它里面会少哪些基础库的东西,我们在这个上面踩了很多坑。现在选择的基础镜像是:mcr.microsoft.com/dotnet/core/sdk:3.1。转换过程根据程序的复杂度决定,有些应用没修改业务逻辑,而有些改的很厉害。
Q:镜像 tag 和代码版本是怎样的对应关系?
A:我们的镜像 tag = 源码的分支: [develop|master] + 日期 + Jenkins 的编译任务序号,如:master-202004-27这样。当这个 tag 的镜像在线下都测试完毕时准备上线了,那么就以这个镜像的 tag 作为应用代码的 tag 编号,这样就能够通过镜像的 tag 追溯到应用代码的 tag 版本。
Q:CentOS 7的系统版本和内核版本号,能说明下么?
A:内核版本:3.10.0-1062.12.1.el7.x86_64,这个对应的是 CentOS 7.7,具体可参见 https://access.redhat.com/articles/3078
Q:本来生活的日志和监控方案是怎样设计的?
A:由于 KubeSphere 的日志在老版本有延时,因此我们线上是采集到 Kafka 然后通过现有的 Kibana 进行查询,也就是 ELK,监控是基于 KubeSphere 自带的 Prometheus,没做太多修改。
Q:你们 yaml 模版复用是怎么使用的,Helm 有用到吗?
A:我们把 yaml 文件内一些应用相关的数据抽取成变量,使之成为应用 yaml 文件的基础模板;然后在 Pipeline 构建时通过接口获取到对应应用的相关参数,将这些参数结合 yaml 文件模板自动填充后生成对应应用的 yaml 文件;然后进行部署操作。Helm 没有在应用部署中使用,但中间件有。
Q:Pod绑定了 SVC,使用 LoadBalancer 的 IP 自动注册注册中心,这块如何实现的?
A:我们的微服务是手动注册 IP,如果自动的话需要与 Pipeline 结合,EIP 是可以预先分配的。
如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!
赞0
添加新评论0 条评论