原本生活网(benlai.com)是一家生鲜电商平台,提供蔬菜、水果、海鲜等优质生鲜果蔬食材食品网购服务。当今容器技术被普遍关注,原本生活网在经历了一年的技术革命后,基本完成了容器化所需的基础设施建设。本文介绍原本生活网在 Kubernetes 落地过程当中的实践和经验。
容器化背景git
原本生活网是一家生鲜电商平台,公司很早就中止了烧钱模式,开始追求盈利。既然要把利润最大化,那就要开源节流,做为技术能够在省钱的方面想一想办法。咱们的生产环境是由 IDC 机房的 100 多台物理机所组成,占用率高达 95%,闲置资源比较多,因而咱们考虑借助 Kubernetes 来重构咱们的基础设施,提升咱们资源的利用率。github
容器化项目团队最初加上我就只有三我的,同时咱们还有各自的工做任务要作,留给容器化的时间较少,所以咱们要考虑如何快速的搭建容器平台,避免走所有自研这条路,这对咱们来讲是个巨大的挑战。在经历了一年的容器化之旅后,分享下咱们这一年所踩过的坑和得到的经验。数据库
面临的问题服务器
在搭建 Kubernetes 集群前,有不少问题摆在咱们面前:网络
人手不足,时间也不充裕,不能有太多自研的需求
咱们目前的发布是由测试人员完成的,不可能要求他们去写一个 yaml 或执行 kubectl 作发布,这个学习成本过高也容易出错,所以咱们必须构建一个用户体验良好的可视化平台给发布人员使用
咱们有大量的 .NET 项目,而 .NET 环境又依赖 Windows
ConfigMap/Secret 不支持版本控制,同时用来存业务配置也不是很方便
Kubernetes 集群和外部的通讯如何打通架构
容器平台负载均衡
做为小团队去构建一个容器平台,自研的工做量太大了。前期咱们调研过不少可视化平台,好比 Kubernetes Dashboard 和 Rancher 等等,可是要操做这些平台得须要专业的 Kubernetes 运维知识,这对咱们的测试人员不太友好。后来咱们尝试了 KubeSphere(kubesphere.io) 平台,各方面都比较符合咱们的需求,因而决定使用该平台做为咱们容器平台的基础,在这之上构建咱们本身的发布流程。运维
.NET 项目ide
咱们的项目有 Java 也有 .NET 的,.NET 项目占了 80% 以上。要支持 .NET 意味着要支持 Windows。在咱们去年开始启动项目的时候,Kubernetes 刚升级到 1.14 版本,是支持 Windows 节点的第一个版本,同时功能也比较弱。通过实验,咱们成功对 .NET Framework 的程序进行了容器化,在不改代码的前提下运行在了 Windows 服务器上,并经过 Kubernetes 进行管理。不过咱们也遇到了一些比较难处理的问题,使用下来的总结以下:微服务
Kubernetes 版本必须是 1.14 版本或以上
大多数 Linux 容器若不作处理会自动调度到 Windows 节点上
Windows 基础镜像体积广泛比较大
必须使用 Windows Server 2019 及以上版本
Kubernetes 关键性组件以 Windows 服务形式运行,而非 Pod
存储和网络的支持有局限性
部署步骤复杂,易出错
咱们调研了一段时间后决定放弃使用 Linux 和 Windows 的混合集群,由于这些问题会带来巨大的运维成本,并且也没法避免 Windows 的版权费。
咱们也考虑过把这些项目转换成 Java,但其中包含大量的业务逻辑代码,把这些重构为 Java 会消耗巨大的研发和测试的人力成本,显然这对咱们来讲也是不现实的。那么有没有一种方案是改动不多的代码,却又能支持 Linux 的呢?答案很明显了,就是把 .NET 转 .NET Core。咱们采用了这种方案,而且大多数状况能很顺利的转换一个项目而不须要修改一行业务逻辑代码。
固然这个方案也有它的局限性,好比遇到以下状况就须要修改代码没法直接转换:
项目里使用了依赖 Windows API 的代码
引用的第三方库无 .NET Core 版本
WCF 和 Web Forms 的代码
这些修改的成本是可控的,也是咱们能够接受的。到目前为止咱们已经成功转换了许多 .NET 项目,而且已运行在 Kubernetes 生产环境之上。
集群暴露
因为咱们是基于物理机部署也就是裸金属(Bare Metal)环境,因此不管基于什么平台搭建,最终仍是要考虑如何暴露 Kubernetes 集群这个问题。
暴露方式
LoadBalancer,是 Kubernetes 官方推荐的暴露方式,很惋惜官方支持的方式都须要部署在云上。咱们公司所有是裸机环境部署,没法使用云方案。
NodePort,端口范围通常是 30000 以上,每一个端口只能对应一种服务。若是应用愈来愈多,那端口可能就不够用了。它最大的问题是若是你暴露某一个节点给外部访问,那么这个节点会成为单点。若是你要作高可用,这几个节点都暴露出去,前面同样也要加一个负载均衡,这样事情就复杂了。
Ingress,能够解决 NodePort 端口复用的问题,它通常工做在 7 层上能够复用 80 和 443 端口。使用 Ingress 的前提是必需要有 Ingress Controller 配合,而 Ingress Controller 一样会出现你须要暴露端口并公开的问题。这时候若是你用 HostNetwork 或 HostPort 把端口暴露在当前的节点上,就存在单点问题;若是你是暴露多个节点的话,一样须要在前面再加一个LB。
HostNetwork/HostPort,这是更暴力的方式,直接把 Pod 的端口绑定到宿主机的端口上。这时候端口冲突会是一个很大的问题,同时单点问题依旧存在。
裸金属方案
咱们分别试用了两套方案 MetalLB(metallb.universe.tf)和 Porter(github.com/kubesphere/porter),这两个都是以 LoadBalancer 方式暴露集群的。咱们测试下来都能知足需求。Porter 是 KubeSphere 的子项目,和 KubeSphere 平台兼容性更好,可是目前 Porter 没有如何部署高可用的文档,我在这里简单分享下:
前置条件
首先你的路由器,必须是三层交换机,须要支持 BGP 协议。如今大多数路由设备都会支持这个协议,因此这个条件通常都能知足
其次集群节点上不能有创建 BGP 通讯的其余服务。举例来讲,当使用 Calico 时,开启了BGP模式。它的 BGP 端口运行在每一个集群节点,Calico 和 Porter 同时启用 BGP 协议,会有部分节点同时运行两个组件与交换机创建 BGP 协议形成冲突。而 KubeSphere 默认安装的 Calico 是 IPIP 模式的,因此咱们没有遇到冲突问题
最后必定要有网络运维人员支持,配合你完成路由器配置以及了解整个网络拓扑结构。了解网络拓扑结构是很是重要的,不然会遇到不少问题
配置和部署
架构和逻辑
Porter有两个插件:Porter-Manager 和 Porter-Agent。
Porter-Manager 是使用 Deployment 部署到 Master 节点上的,但默认只部署1个副本,它负责同步 BGP 路由到物理交换机(单向 BGP 路由,只需将 Kubernetes 私有路由发布给交换机便可,无需学习交换机内物理网络路由)。还有一个组件,Porter-Agent,它以 DaemonSet 的形式在全部节点都部署一个副本,功能是维护引流规则。
高可用架构
部署好后,你可能会有疑问:
单个路由器挂了怎么办
单个 porter-manager 挂了怎么办
porter-manager 和路由器网络断了怎么办
EIP 下一跳地址所在的节点挂了怎么办
某个 EIP 流量忽然飙升,一个节点扛不住怎么办
通常路由器或交换机都会准备两台作 VSU(Virtual Switching Unit)实现高可用,这个是网络运维擅长的,这里不细讲了。主要说下其余几点怎么解决:
确保一个 EIP 有多条 BGP 路由规则,交换机和 Manager 是多对多部署的
确保交换机开启等价路由(ECMP)
要作好故障演练和压力测试
MetalLB 的高可用部署也是相似思路,虽然架构稍有不一样,好比它和路由器进行 BGP 通讯的组件是 Speaker,对应 Porter 的 Manager;它与 Porter 的区别在于高可用目前作的比较好;可是 Local 引流规划不如 Porter,EIP 的下一跳节点必须是 BGP 对等体(邻居)。
配置中心
Kubernetes 的 ConfigMap 和 Secret 在必定程度上解决了配置的问题,咱们能够很轻松的使用它们进行更改配置而不须要从新生成镜像,但咱们在使用的过程当中仍是会遇到一些痛点:
不支持版本控制,Deployment 和 StatefulSet 都有版本控制,咱们可使用 rollout 把应用回滚到老的版本,可是 ConfigMap/Secret 却没有版本控制,这会产生一个问题就是当咱们的 Deployment 要进行回滚时,使用的 ConfigMap 仍是最新的,咱们必须把 ConfigMap 改回上一个 Deployment 版本所使用的 ConfigMap 内容,再进行回滚,可是这样的操做是很危险的,假设改完 ConfigMap 后有个 Pod 出了问题自动重启了,又或者 ConfigMap 以文件形式挂载到 Pod 中,都会形成程序读取到了错误的配置。一个折中的作法是每一个版本都关联一个单独的 ConfigMap。
不太适合管理业务配置,一个应用有时候会须要加不少业务配置,在维护大量业务配置时,咱们可能须要搜索关键字、查看某个 key 的备注、对 value 的格式进行校验、批量更新配置等等,可是若是使用了 ConfigMap ,咱们就不得再也不作一些工具来知足这些需求,而这些需求对于配置的维护效率是有很是大的影响。
热更新,咱们知道若是 ConfigMap 是以文件形式进行挂载,那么当修改了 ConfigMap 的值后,过一会全部的 Pod 里相应的文件都会变动,但是若是是以环境变量的方式挂载却不会更新。为了让咱们的程序支持热更新,咱们须要把 ConfigMap 都挂载成文件,而当配置有不少时麻烦就来了,若是 Key 和文件是一对一挂载,那么 Pod 里会有不少小文件;若是全部 Key 都放到一个文件里,那么维护配置就成了噩梦。
配置大小限制,ConfigMap 自己没有大小限制,可是 etcd 有,默认状况下一个 ConfigMap 限制为 1MB,咱们估算了下,有个别应用的配置加起来真有可能突破这个限制,为了绕过这个大小限制,目前也只有切割成多个 ConfigMap 的方法了。
为了解决这些痛点,咱们综合考虑了不少方案,最终决定仍是使用一套开源的配置中心做为配置的源,先经过 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 每一个实例都是有状态的,而且根据功能不一样,相应的配置也会不一样,所以每一个实例的配置是须要不同的,不能简单的挂载 ConfigMap 来解决
CAT 每一个实例须要绑定一个 IP 给客户端使用,不能使用 Service 作负载均衡
CAT 每一个实例必须事先知道全部实例的 IP 地址
CAT 每一个实例会在代码层面获取本身的 IP,这时候获取到的是可变的 Pod IP,与绑定的 IP 不一致,这就会致使集群内部通讯出问题
为了把 CAT 部署成一个 StatefulSet 而且支持扩容,咱们参考了 Kafka 的 Helm 部署方式,作了如下的工做:
咱们为每一个 Pod 建立了一个 Service,而且启用 LoadBalancer 模式绑定了 IP,使每一个 Pod 都会有一个独立的对外 IP 地址,称它为 EIP;
咱们把全部实例的信息都保存在配置中心,而且为每一个实例生成了不一样的配置,好比有些实例是跑 Job,有些实例是跑监控的;
咱们会预先规划好 EIP,并把这些 EIP 列表经过动态生成配置的方式传给每一个实例;
最后咱们给每一个实例里塞了一个特殊的文件,这个文件里存的是当前这个实例所绑定的 EIP 地址。接着咱们修改了 CAT 的代码,把全部获取本地 IP 地址的代码改为了读取这个特定文件里的 IP 地址,以此欺骗每一个实例认为这个 EIP 就是它本身的本地 IP。
扩容很简单,只须要在配置中内心添加一条实例信息,从新部署便可。
CI/CD
因为 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 升级到一个稳定的版本也是颇有必要的。
将来展望
咱们目前对将来的规划是这样的:
支持多集群部署
支持容器调试
微服务与 Istio 的结合
应用管理界面
Q&A
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 是能够预先分配的。