Kubernetes最佳实践之腾讯云TKE 集群组建


作者陈鹏,腾讯工程师,负责腾讯云 TKE 的售中、售后的技术支持,根据客户需求输出合理技术方案与最佳实践,为客户业务保驾护航。

使用 TKE 来组建 Kubernetes 集群时,会面对各种配置选项,本文将介绍几个比较重要的功能选型,给出对比与选型建议,让大家少走弯路。

Kubernetes 版本

Kubernetes 版本迭代比较快,新版本通常包含许多 bug 修复和新功能,旧版本逐渐淘汰,建议创建集群时选择当前 TKE 支持的最新版本,后续出新版本后也是可以支持 Master 和节点的版本升级的。

网络模式: GlobalRouter vs VPC-CNI

PIC1.png


GlobalRouter 模式架构:

•基于 CNI 和 网桥实现的容器网络能力,容器路由直接通过 VPC 底层实现;

•容器与节点在同一网络平面,但网段不与 VPC 网段重叠,容器网段地址充裕。

VPC-CNI 模式架构:

PIC2.png


•基于 CNI 和 VPC 弹性网卡实现的容器网络能力,容器路由通过弹性网卡,性能相比 Global Router 约提高 10%;

•容器与节点在同一网络平面,网段在 VPC 网段内;

•支持 Pod 固定 IP。

网络模式对比:

PIC3.png


支持三种使用方式:

•创建集群时指定 GlobalRouter 模式;

•创建集群时指定 VPC-CNI 模式,后续所有 Pod 都必须使用 VPC-CNI 模式创建;

•创建集群时指定 GlobalRouter 模式,在需要使用 VPC-CNI 模式时为集群启用 VPC-CNI 的支持,即两种模式混用。

选型建议:

•绝大多数情况下应该选择 GlobalRouter,容器网段地址充裕,扩展性强,能适应规模较大的业务;

•如果后期部分业务需要用到 VPC-CNI 模式,可以在 GlobalRouter 集群再开启 VPC-CNI 支持,也就是 GlobalRouter 与 VPC-CNI 混用,仅对部分业务使用 VPC-CNI 模式;

•如果完全了解并接受 VPC-CNI 的各种限制,并且需要集群内所有 Pod 都用 VPC-CNI 模式,可以创建集群时选择 VPC-CNI 网络插件。

参考官方文档 《如何选择容器服务网络模式》: https://cloud.tencent.com/docu ... 41636

运行时: Docker vs Containerd

Docker 作为运行时的架构:

PIC4.png


•kubelet 内置的 dockershim 模块帮傲娇的 docker 适配了 CRI 接口,然后 kubelet 自己调自己的 dockershim (通过 socket 文件),然后 dockershim 再调 dockerd 接口 (Docker HTTP API),接着 dockerd 还要再调 docker-containerd (gRPC) 来实现容器的创建与销毁等。

•为什么调用链这么长?Kubernetes 一开始支持的就只是 Docker,后来引入了 CRI,将运行时抽象以支持多种运行时,而 Docker 跟 Kubernetes 在一些方面有一定的竞争,不甘做小弟,也就没在 dockerd 层面实现 CRI 接口,所以 kubelet 为了让 dockerd 支持 CRI,就自己为 dockerd 实现了 CRI。docker 本身内部组件也模块化了,再加上一层 CRI 适配,调用链肯定就长了。

Containerd 作为运行时的架构:

PIC5.png


•containerd 1.1 之后,支持 CRI Plugin,即 containerd 自身这里就可以适配 CRI 接口。

•相比 Docker 方案,调用链少了 dockershim 和 dockerd。

运行时对比:

•containerd 方案由于绕过了 dockerd,调用链更短,组件更少,占用节点资源更少,绕过了 dockerd 本身的一些 bug,但 containerd 自身也还存在一些 bug (已修复一些,灰度中)。

•docker 方案历史比较悠久,相对更成熟,支持 docker api,功能丰富,符合大多数人的使用习惯。

选型建议:

•Docker 方案 相比 containerd 更成熟,如果对稳定性要求很高,建议 docker 方案;

•以下场景只能使用 docker:

◾Docker in docker (通常在 CI 场景)

◾节点上使用 docker 命令

◾调用 docker API

•没有以上场景建议使用 containerd。

参考官方文档 《如何选择 Containerd 和 Docker》:https://cloud.tencent.com/docu ... 35747

Service 转发模式: iptables vs ipvs

先看看 Service 的转发原理:

PIC6.jpg


•节点上的 kube-proxy 组件 watch apiserver,获取 Service 与 Endpoint,根据转发模式将其转化成 iptables 或 ipvs 规则并写到节点上;

•集群内的 client 去访问 Service (Cluster IP),会被 iptable/ipvs 规则负载均衡到 Service 对应的后端 pod。

转发模式对比:

•ipvs 模式性能更高,但也存在一些已知未解决的 bug;

•iptables 模式更成熟稳定。

选型建议:

•对稳定性要求极高且 service 数量小于 2000,选 iptables;

•其余场景首选 ipvs。

集群类型: 托管集群 vs 独立集群

托管集群:

•Master 组件用户不可见,由腾讯云托管

•很多新功能也是会率先支持托管的集群

•Master 的计算资源会根据集群规模自动扩容

•用户不需要为 Master 付费

独立集群:

•Master 组件用户可以完全掌控

•用户需要为 Master 付费购买机器

选型建议:

•一般推荐托管集群

•如果希望能能够对 Master 完全掌控,可以使用独立集群 (比如对 Master 进行个性化定制实现高级功能)

节点操作系统

PIC7.png


TKE 主要支持 Ubuntu 和 CentOS 两类发行版,带 “TKE-Optimized” 后缀用的是 TKE 定制优化版的内核,其它的是 linux 社区官方开源内核:

TKE-Optimized 的优势:

•基于内核社区长期支持的 4.14.105 版本定制

•针对容器和云场景进行优化

•计算、存储和网络子系统均经过性能优化

•对内核缺陷修复支持较好

•完全开源:https://github.com/Tencent/TencentOS-kernel

选型建议:

•推荐 “TKE-Optimized”,稳定性和技术支持都比较好

•如果需要更高版本内核,选非 “TKE-Optimized”版本的操作系统

节点池

此特性当前正在灰度中,可申请开白名单使用。主要可用于批量管理节点:

•节点 Label 与 Taint

•节点组件启动参数

•节点自定义启动脚本

•操作系统与运行时 (暂未支持)

产品文档:https://cloud.tencent.com/docu ... 43719

适用场景:

•异构节点分组管理,减少管理成本

•让集群更好支持复杂的调度规则 (Label, Taint)

•频繁扩缩容节点,减少操作成本

•节点日常维护(版本升级)

用法举例:

部分IO密集型业务需要高IO机型,为其创建一个节点池,配置机型并统一设置节点 Label 与 Taint,然后将 IO 密集型业务配置亲和性,选中 Label,使其调度到高 IO 机型的节点 (Taint 可以避免其它业务 Pod 调度上来)。

随着时间的推移,业务量快速上升,该 IO 密集型业务也需要更多的计算资源,在业务高峰时段,HPA 功能自动为该业务扩容了 Pod,而节点计算资源不够用,这时节点池的自动伸缩功能自动扩容了节点,扛住了流量高峰。

启动脚本

组件自定义参数

此特性当前也正在灰度中,可申请开白名单使用。

  1. 创建集群时,可在集群信息界面“高级设置”中自定义 Master 组件部分启动参数:

  2. 添加节点时,可在云服务器配置界面的“高级设置”中自定义 kubelet 部分启动参数:


节点启动配置

  1. 新建集群时,可在云服务器配置界面的“节点启动配置”选项处添加节点启动脚本:

  2. 添加节点时,可在云服务器配置界面的“高级设置”中通过自定义数据配置节点启动脚本 (可用于修改组件启动参数、内核参数等):
继续阅读 »

作者陈鹏,腾讯工程师,负责腾讯云 TKE 的售中、售后的技术支持,根据客户需求输出合理技术方案与最佳实践,为客户业务保驾护航。

使用 TKE 来组建 Kubernetes 集群时,会面对各种配置选项,本文将介绍几个比较重要的功能选型,给出对比与选型建议,让大家少走弯路。

Kubernetes 版本

Kubernetes 版本迭代比较快,新版本通常包含许多 bug 修复和新功能,旧版本逐渐淘汰,建议创建集群时选择当前 TKE 支持的最新版本,后续出新版本后也是可以支持 Master 和节点的版本升级的。

网络模式: GlobalRouter vs VPC-CNI

PIC1.png


GlobalRouter 模式架构:

•基于 CNI 和 网桥实现的容器网络能力,容器路由直接通过 VPC 底层实现;

•容器与节点在同一网络平面,但网段不与 VPC 网段重叠,容器网段地址充裕。

VPC-CNI 模式架构:

PIC2.png


•基于 CNI 和 VPC 弹性网卡实现的容器网络能力,容器路由通过弹性网卡,性能相比 Global Router 约提高 10%;

•容器与节点在同一网络平面,网段在 VPC 网段内;

•支持 Pod 固定 IP。

网络模式对比:

PIC3.png


支持三种使用方式:

•创建集群时指定 GlobalRouter 模式;

•创建集群时指定 VPC-CNI 模式,后续所有 Pod 都必须使用 VPC-CNI 模式创建;

•创建集群时指定 GlobalRouter 模式,在需要使用 VPC-CNI 模式时为集群启用 VPC-CNI 的支持,即两种模式混用。

选型建议:

•绝大多数情况下应该选择 GlobalRouter,容器网段地址充裕,扩展性强,能适应规模较大的业务;

•如果后期部分业务需要用到 VPC-CNI 模式,可以在 GlobalRouter 集群再开启 VPC-CNI 支持,也就是 GlobalRouter 与 VPC-CNI 混用,仅对部分业务使用 VPC-CNI 模式;

•如果完全了解并接受 VPC-CNI 的各种限制,并且需要集群内所有 Pod 都用 VPC-CNI 模式,可以创建集群时选择 VPC-CNI 网络插件。

参考官方文档 《如何选择容器服务网络模式》: https://cloud.tencent.com/docu ... 41636

运行时: Docker vs Containerd

Docker 作为运行时的架构:

PIC4.png


•kubelet 内置的 dockershim 模块帮傲娇的 docker 适配了 CRI 接口,然后 kubelet 自己调自己的 dockershim (通过 socket 文件),然后 dockershim 再调 dockerd 接口 (Docker HTTP API),接着 dockerd 还要再调 docker-containerd (gRPC) 来实现容器的创建与销毁等。

•为什么调用链这么长?Kubernetes 一开始支持的就只是 Docker,后来引入了 CRI,将运行时抽象以支持多种运行时,而 Docker 跟 Kubernetes 在一些方面有一定的竞争,不甘做小弟,也就没在 dockerd 层面实现 CRI 接口,所以 kubelet 为了让 dockerd 支持 CRI,就自己为 dockerd 实现了 CRI。docker 本身内部组件也模块化了,再加上一层 CRI 适配,调用链肯定就长了。

Containerd 作为运行时的架构:

PIC5.png


•containerd 1.1 之后,支持 CRI Plugin,即 containerd 自身这里就可以适配 CRI 接口。

•相比 Docker 方案,调用链少了 dockershim 和 dockerd。

运行时对比:

•containerd 方案由于绕过了 dockerd,调用链更短,组件更少,占用节点资源更少,绕过了 dockerd 本身的一些 bug,但 containerd 自身也还存在一些 bug (已修复一些,灰度中)。

•docker 方案历史比较悠久,相对更成熟,支持 docker api,功能丰富,符合大多数人的使用习惯。

选型建议:

•Docker 方案 相比 containerd 更成熟,如果对稳定性要求很高,建议 docker 方案;

•以下场景只能使用 docker:

◾Docker in docker (通常在 CI 场景)

◾节点上使用 docker 命令

◾调用 docker API

•没有以上场景建议使用 containerd。

参考官方文档 《如何选择 Containerd 和 Docker》:https://cloud.tencent.com/docu ... 35747

Service 转发模式: iptables vs ipvs

先看看 Service 的转发原理:

PIC6.jpg


•节点上的 kube-proxy 组件 watch apiserver,获取 Service 与 Endpoint,根据转发模式将其转化成 iptables 或 ipvs 规则并写到节点上;

•集群内的 client 去访问 Service (Cluster IP),会被 iptable/ipvs 规则负载均衡到 Service 对应的后端 pod。

转发模式对比:

•ipvs 模式性能更高,但也存在一些已知未解决的 bug;

•iptables 模式更成熟稳定。

选型建议:

•对稳定性要求极高且 service 数量小于 2000,选 iptables;

•其余场景首选 ipvs。

集群类型: 托管集群 vs 独立集群

托管集群:

•Master 组件用户不可见,由腾讯云托管

•很多新功能也是会率先支持托管的集群

•Master 的计算资源会根据集群规模自动扩容

•用户不需要为 Master 付费

独立集群:

•Master 组件用户可以完全掌控

•用户需要为 Master 付费购买机器

选型建议:

•一般推荐托管集群

•如果希望能能够对 Master 完全掌控,可以使用独立集群 (比如对 Master 进行个性化定制实现高级功能)

节点操作系统

PIC7.png


TKE 主要支持 Ubuntu 和 CentOS 两类发行版,带 “TKE-Optimized” 后缀用的是 TKE 定制优化版的内核,其它的是 linux 社区官方开源内核:

TKE-Optimized 的优势:

•基于内核社区长期支持的 4.14.105 版本定制

•针对容器和云场景进行优化

•计算、存储和网络子系统均经过性能优化

•对内核缺陷修复支持较好

•完全开源:https://github.com/Tencent/TencentOS-kernel

选型建议:

•推荐 “TKE-Optimized”,稳定性和技术支持都比较好

•如果需要更高版本内核,选非 “TKE-Optimized”版本的操作系统

节点池

此特性当前正在灰度中,可申请开白名单使用。主要可用于批量管理节点:

•节点 Label 与 Taint

•节点组件启动参数

•节点自定义启动脚本

•操作系统与运行时 (暂未支持)

产品文档:https://cloud.tencent.com/docu ... 43719

适用场景:

•异构节点分组管理,减少管理成本

•让集群更好支持复杂的调度规则 (Label, Taint)

•频繁扩缩容节点,减少操作成本

•节点日常维护(版本升级)

用法举例:

部分IO密集型业务需要高IO机型,为其创建一个节点池,配置机型并统一设置节点 Label 与 Taint,然后将 IO 密集型业务配置亲和性,选中 Label,使其调度到高 IO 机型的节点 (Taint 可以避免其它业务 Pod 调度上来)。

随着时间的推移,业务量快速上升,该 IO 密集型业务也需要更多的计算资源,在业务高峰时段,HPA 功能自动为该业务扩容了 Pod,而节点计算资源不够用,这时节点池的自动伸缩功能自动扩容了节点,扛住了流量高峰。

启动脚本

组件自定义参数

此特性当前也正在灰度中,可申请开白名单使用。

  1. 创建集群时,可在集群信息界面“高级设置”中自定义 Master 组件部分启动参数:

  2. 添加节点时,可在云服务器配置界面的“高级设置”中自定义 kubelet 部分启动参数:


节点启动配置

  1. 新建集群时,可在云服务器配置界面的“节点启动配置”选项处添加节点启动脚本:

  2. 添加节点时,可在云服务器配置界面的“高级设置”中通过自定义数据配置节点启动脚本 (可用于修改组件启动参数、内核参数等):
收起阅读 »

如何为云原生应用带来稳定高效的部署能力?


作者 | 酒祝  阿里云技术专家、墨封  阿里云开发工程师

直播完整视频回顾:https://www.bilibili.com/video/BV1mK4y1t7WS/

关注“阿里巴巴云原生”公众号,后台回复 “528” 即可下载 PPT

5 月 28 日,我们发起了第 3 期 SIG Cloud-Provider-Alibaba 网研会直播。本次直播主要介绍了阿里经济体大规模应用上云过程中遇到的核心部署问题、采取的对应解决方案,以及这些方案沉淀为通用化能力输出开源后,如何帮助阿里云上的用户提升应用部署发布的效率与稳定性。

本文汇集了此次直播完整视频回顾及资料下载,并整理了直播过程中收集的问题和解答,希望能够对大家有所帮助~

讲师图片.jpg


前言

随着近年来 Kubernetes 逐渐成为事实标准和大量应用的云原生化,我们往往发现 Kubernetes 的原生 workload 对大规模化应用的支持并不十分“友好”。如何在 Kubernetes 上为应用提供更加完善、高效、灵活的部署发布能力,成为了我们探索的目标。

本文将会介绍在阿里经济体全面接入云原生的过程中,我们在应用部署方面所做的改进优化、实现功能更加完备的增强版 workload、并将其开源到社区,使得现在每一位 Kubernetes 开发者和阿里云上的用户都能很便捷地使用上阿里巴巴内部云原生应用所统一使用的部署发布能力。


阿里应用场景与原生 workloads

阿里巴巴容器化道路的起步在国内外都是比较领先的。容器这个技术概念虽然出现得很早,但一直到 2013 年 Docker 产品出现后才逐渐为人所熟知。而阿里巴巴早在 2011 年就开始发展了基于 LXC 的容器技术,经过了几代的系统演进,如今阿里巴巴有着超过百万的容器体量,这个规模在世界范围内都是顶尖的。

随着云技术发展和云原生应用的兴起,我们近两年间逐步将过去的容器迁到了基于 Kubernetes 的云原生环境中。而在这其中,我们遇到了不少应用部署方面的问题。首先对于应用开发者来说,他们对迁移到云原生环境的期望是:
  • 面向丰富业务场景的策略功能
  • 极致的部署发布效率
  • 运行时的稳定性和容错能力


阿里的应用场景非常复杂,基于 Kubernetes 之上生长着很多不同的 PaaS 二层,比如服务于电商业务的运维中台、规模化运维、中间件、Serverless、函数计算等,而每个平台都对部署、发布要求各有不同。

我们再来看一下 Kubernete 原生所提供的两种常用 workload 的能力:

1.png


简单来说,Deployment 和 StatefulSet 在一些小规模的场景下是可以 work 的;但到了阿里巴巴这种应用和容器的规模下,如果全量使用原生 workload 则是完全不现实的。目前阿里内部容器集群上的应用数量超过十万、容器数量达到百万,有部分重点核心应用甚至单个应用下就有上万的容器。再结合上图的问题,我们会发现不仅针对单个应用的发布功能不足,而且当发布高峰期大量应用同时在升级时,超大规模的 Pod 重建也成为一种“灾难”。

阿里自研的扩展 workloads

针对原生 workload 远远无法满足应用场景的问题,我们从各种复杂的业务场景中抽象出共通的应用部署需求,据此开发了多种扩展 workload。在这些 workload 中我们做了大幅的增强和改进,但同时也会严格保证功能的通用化、不允许将业务逻辑耦合进来。

这里我们重点介绍一下 CloneSet 与 Advanced StatefulSet。在阿里内部云原生环境下,几乎全量的电商相关应用都统一采用 CloneSet 做部署发布,而中间件等有状态应用则使用了 Advanced StatefulSet 管理。

2.png


Advanced StatefulSet 顾名思义,是原生 StatefulSet 的增强版,默认行为与原生完全一致,在此之外提供了原地升级、并行发布(最大不可用)、发布暂停等功能。而 CloneSet 则对标原生 Deployment,主要服务于无状态应用,提供了最为全面丰富的部署发布策略。

原地升级

CloneSet、Advanced StatefulSet 均支持指定 Pod 升级方式:
  1. ReCreate:重建 Pod 升级,和原生 Deployment/StatefulSet 一致;
  2. InPlaceIfPossible:如果只修改 image 和 metadata 中的 labels/annotations 等字段,则触发 Pod 原地升级;如果修改了其他 template spec 中的字段,则退化到 Pod 重建升级;
  3. InPlaceOnly:只允许修改 image 和 metadata 中的 labels/annotations 等字段,只会使用原地升级。


所谓原地升级,就是在升级 template 模板的时候,workload 不会把原 Pod 删除、新建,而是直接在原 Pod 对象上更新对应的 image 等数据。

3.png


如上图所示,在原地升级的时候 CloneSet 只会更新 Pod spec 中对应容器的 image,而后 kubelet 看到 Pod 中这个容器的定义发生了变化,则会把对应的容器停掉、拉取新的镜像、并使用新镜像创建启动容器。另外可以看到在过程中,这个 Pod 的 sandbox 容器以及其他本次未升级的容器还一直处于正常运行状态,只有需要升级的容器会受到影响。

原地升级给我们带来的好处实在太多了:
  • 首先就是发布效率大大提升了,根据非完全统计数据,在阿里环境下原地升级至少比完全重建升级提升了 80% 以上的发布速度:不仅省去了调度、分配网络、分配远程盘的耗时,连拉取新镜像的时候都得益于 node 上已有旧镜像、只需要拉取较少的增量 layer);
  • IP 不变、升级过程 Pod 网络不断,除本次升级外的其他容器保持正常运行;
  • Volume 不变,完全复用原容器的挂载设备;
  • 保障了集群确定性,使排布拓扑能通过大促验证。


后续我们将会有专文讲解阿里在 Kubernetes 之上做的原地升级,意义非常重大。如果没有了原地升级,阿里巴巴内部超大规模的应用场景几乎是无法在原生 Kubernetes 环境上完美落地的,我们也鼓励每一位 Kubernetes 用户都应该“体验”一下原地升级,它给我们带来了不同于 Kubernetes 传统发布模式的变革。

流式+分批发布

前一章我们提到了,目前 Deployment 支持 maxUnavailable/maxSurge 的流式升级,而 StatefulSet 支持 partition 的分批升级。但问题在于,Deployment 无法灰度分批,而 StatefulSet 则只能一个一个 Pod 串行发布,没办法并行的流式升级。

首先要说的是,我们将 maxUnavailable 引入了 Advanced StatefulSet。原生 StatefulSet 的 one by one 发布,大家其实可以理解为一个强制 maxUnavailable=1 的过程,而 Advanced StatefulSet 中如果我们配置了更大的 maxUnavailable,那么就支持并行发布更多的 Pod 了。

然后我们再来看一下 CloneSet,它支持原生 Deployment 和 StatefulSet 的全部发布策略,包括 maxUnavailable、maxSurge、partition。那么 CloneSet 是如何把它们结合在一起的呢?我们来看一个例子:

yaml
apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet

...

spec:
replicas: 5 # Pod 总数为 5
updateStrategy:
type: InPlaceIfPossible
maxSurge: 20% # 多扩出来 5 * 20% = 1 个 Pod (rounding up)
maxUnavailable: 0 # 保证发布过程 5 - 0 = 5 个 Pod 可用
partition: 3 # 保留 3 个旧版本 Pod (只发布 5 - 3 = 2 个 Pod)


针对这个副本数为 5 的 CloneSet,如果我们修改了 template 中的 image,同时配置:maxSurge=20%  maxUnavailable=0  partition=3。当开始发布后:
  1. 先扩出来 1 个新版本的 Pod,5 个存量 Pod 保持不动;
  2. 新 Pod ready 后,逐步把旧版本 Pod 做原地升级;
  3. 直到剩余 3 个旧版本 Pod 时,因为满足了 partition 终态,会把新版本 Pod 再删除 1 个;
  4. 此时 Pod 总数仍然为 5,其中 3 个旧版本、1 个新版本。


如果我们接下来把 partition 调整为 0,则 CloneSet 还是会先扩出 1 个额外的新版 Pod,随后逐渐将所有 Pod 升级到新版,最终再次删除一个 Pod,达到 5 个副本全量升级的终态。

发布顺序可配置

对于原生的 Deployment 和 StatefulSet,用户是无法配置发布顺序的。Deployment 下的 Pod 发布顺序完全依赖于它修改 ReplicaSet 后的扩缩顺序,而 StatefulSet 则严格按照 order 的反序来做一一升级。

但在 CloneSet 和 Advanced StatefulSet 中,我们增加了发布顺序的可配置能力,使用户可以定制自己的发布顺序。目前可以通过以下两种发布优先级和一种发布打散策略来定义顺序:
  • 优先级(1):按给定 label key,在发布时根据 Pod labels 中这个 key 对应的 value 值作为权重:


yaml
apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
spec:
# ...
updateStrategy:
priorityStrategy:
orderPriority:
- orderedKey: some-label-key

  • 优先级(2):按 selector 匹配计算权重,发布时根据 Pod 对多个 weight selector 的匹配情况计算权重总和:


yaml
apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
spec:
# ...
updateStrategy:
priorityStrategy:
weightPriority:
- weight: 50
matchSelector:
matchLabels:
test-key: foo
- weight: 30
matchSelector:
matchLabels:
test-key: bar

  • 打散:将匹配 key-value 的 Pod 打散到不同批次中发布:


yaml
apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
spec:
# ...
updateStrategy:
scatterStrategy:
- key: some-label-key
value: foo


可能有同学会问为什么要配置发布顺序呢?比如 zookeeper 这类应用在发布时,需要先把所有非主节点升级,最后再升级主节点,这样才能保证在整个发布过程中只会发生一次切主。这时用户就可以通过流程打标、或者写一个 operator 自动为 zookeeper 的 Pod 打上节点职责的标签,而后配置非主节点的发布权重较大,使得发布时能够尽量减少切主的次数。

sidecar 容器管理

轻量化容器也是阿里巴巴在云原生阶段的一次重大改革,过去阿里的容器绝大多数都是以“富容器”的方式运行的,所谓“富容器”即在一个容器中既运行业务、也跑着各种各样的插件和守护进程。而在云原生时代,我们在逐渐把原先“富容器”中的旁路插件拆分到独立的 sidecar 容器中,使主容器逐渐回归业务自身。

这里对于拆分的好处就不赘述了,我们来看下另一个问题,就是拆分之后这些 sidecar 容器如何做管理呢?最直观的方式是在每个应用的 workload 中显示去定义 Pod 中需要的 sidecar,但这样带来的问题很多:
  1. 当应用和 workload 数量众多时,我们很难统一的 sidecar 增减管理;
  2. 应用开发者不知道(甚至也不关心)自己的应用需要配置哪些 sidecar 容器;
  3. 当 sidecar 镜像需要升级时,要把所有应用的 workload 全部升级一遍,很不现实。


因此,我们设计了 SidecarSet,将 sidecar 容器的定义与应用 workload 解耦。应用开发者们不再需要再关心自己的 workload 中需要写哪些 sidecar 容器,而通过原地升级, sidecar 维护者们也可以自主地管理和升级 sidecar 容器。

4.png


开放能力应用

到了这里,大家是不是对阿里巴巴的应用部署模式有了一个基本的了解呢?其实上述的能力都已经开源到了社区,我们的项目就叫做 OpenKruise,目前它已经提供了 5 种扩展 workload:
  • CloneSet:提供了更加高效、确定可控的应用管理和部署能力,支持优雅原地升级、指定删除、发布顺序可配置、并行/灰度发布等丰富的策略,可以满足更多样化的应用场景;
  • Advanced StatefulSet:基于原生 StatefulSet 之上的增强版本,默认行为与原生完全一致,在此之外提供了原地升级、并行发布(最大不可用)、发布暂停等功能;
  • SidecarSet:对 sidecar 容器做统一管理,在满足 selector 条件的 Pod 中注入指定的 sidecar 容器;
  • UnitedDeployment:通过多个 subset workload 将应用部署到多个可用区;
  • BroadcastJob:配置一个 job,在集群中所有满足条件的 Node 上都跑一个 Pod 任务。


此外,我们还有更多的扩展能力还在开源的路上!近期,我们会将内部的 Advanced DaemonSet 开放到 OpenKruise 中,它在原生 DaemonSet 的 maxUnavailable 之上,额外提供了如分批、selector 等发布策略,分批的功能使 DaemonSet 在发布的时候能够只升级其中部分 Pod,而 selector 更是允许发布的时候指定先在符合某些标签的 node 上升级,这为我们在大规模集群中升级 DaemonSet 带来了灰度能力和稳定性的保障。

而后续,我们还计划将阿里巴巴内部扩展的 HPA、调度插件等通用化能力开放出来,让每一位 Kubernetes 开发者和阿里云上的用户都能很便捷地使用上阿里内部开发应用的云原生增强能力。

最后,我们也欢迎每一位云原生爱好者来共同参与 OpenKruise 的建设。与其他一些开源项目不同,OpenKruise 并不是阿里内部代码的复刻;恰恰相反,OpenKruise Github 仓库是阿里内部代码库的 upstream。因此,每一行你贡献的代码,都将运行在阿里内部的所有 Kubernetes 集群中、都将共同支撑了阿里巴巴全球顶尖规模的应用场景!

Q & A

Q1:目前阿里最大规模的业务 pod 数量有多少,发布一次需要多少时间?
A1:这个只能透露数量目前最大规模的单个应用下数量是以万为单位的,一次发布时间要看具体分批灰度的时长了。如果分批较多、观察时间较长的话,可能是会持续一两周的。

Q2:pod 的资源 request 和 limit 是怎么配置的?request 和 limit 是什么比例来配置?过多的 request 造成浪费,过少可能会导致热点 node 负载超高。
A2:这个主要还是根据应用的需求来定的,目前大部分在线应用都是 1:1 的关系,部分离线和job 类型的会配置 request>limit。

Q3:kruise 升级问题,升级 kurise apiversion 版本的情况下,原有的版本的部署如何升级?
A3:目前 kruise 中资源的 apiVersion 还都是统一的。我们计划在今年下半年将部分较为成熟的 workload 进行版本升级,用户在自己的 K8s 集群内升级后,存量的旧版本资源会自动通过 conversion 升级到新版本。

Q4:OpenKruise 有提供 go-client 吗?
A4:目前提供两个方式:1. 引入 github.com/openkruise/kruise/pkg/client 包,下面有生成好的 clientset / informer / lister 等工具;2. 使用 controller-runtime 的用户(包括 kubebuilder、operator-sdk),直接引入 github.com/openkruise/kruise-api 轻量化依赖,然后加到 scheme 里就能直接用了。

Q5:阿里 K8s 版本升级是如何做的?
A5:阿里集团内部使用 Kube-On-Kube 的架构进行大规模的 Kubernetes 集群管理,用一个元 K8s 集群管理成百上千个业务 K8s 集群。其中元集群版本较为稳定,业务集群会进行频繁升级,业务集群的升级流程事实上就是对元集群中的 workloads(原生 workloads 以及 kruise workloads) 进行版本或配置升级,与正常情况我们对业务 workloads 的升级流程相似。

Q6:这个灰度之后,流量是怎么切的?
A6:在原地升级前,kruise 会先通过 readinessGate 将 Pod 置为 not-ready,此时 endpoint 等控制器会感知到并把 Pod 从端点摘掉。然后 kruise 更新 pod image 触发容器重建,完成后再把 Pod 改为 ready。

Q7:daemonset 的分批是通过类似 deployment 的暂停功能实现的么?统计已经发布数量然后暂停,然后继续,然后再暂停。
A7:总体过程上类似,升级过程中对新旧版本进行统计并判断是否已达到指定终态。但相比 deployment,daemonset 需要处理比较复杂的边界情况(例如初次发布时集群中并没有指定的 Pod),具体细节可以持续关注我们即将开源的代码。

Q8:多集群发布页面上怎么开始发布的?
A8:直播中演示的是一个 demo 的发布系统结合 Kruise Workloads 的例子,从交互上是通过用户选择对应的集群,点击开始执行进行发布;从实现上实际是对新版本的 YAML 与集群中的 YAML 计算 diff 后 Patch 进集群,再操作 DaemonSet 的控制字段(partition / paused 等),控制灰度进程。

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”
继续阅读 »

作者 | 酒祝  阿里云技术专家、墨封  阿里云开发工程师

直播完整视频回顾:https://www.bilibili.com/video/BV1mK4y1t7WS/

关注“阿里巴巴云原生”公众号,后台回复 “528” 即可下载 PPT

5 月 28 日,我们发起了第 3 期 SIG Cloud-Provider-Alibaba 网研会直播。本次直播主要介绍了阿里经济体大规模应用上云过程中遇到的核心部署问题、采取的对应解决方案,以及这些方案沉淀为通用化能力输出开源后,如何帮助阿里云上的用户提升应用部署发布的效率与稳定性。

本文汇集了此次直播完整视频回顾及资料下载,并整理了直播过程中收集的问题和解答,希望能够对大家有所帮助~

讲师图片.jpg


前言

随着近年来 Kubernetes 逐渐成为事实标准和大量应用的云原生化,我们往往发现 Kubernetes 的原生 workload 对大规模化应用的支持并不十分“友好”。如何在 Kubernetes 上为应用提供更加完善、高效、灵活的部署发布能力,成为了我们探索的目标。

本文将会介绍在阿里经济体全面接入云原生的过程中,我们在应用部署方面所做的改进优化、实现功能更加完备的增强版 workload、并将其开源到社区,使得现在每一位 Kubernetes 开发者和阿里云上的用户都能很便捷地使用上阿里巴巴内部云原生应用所统一使用的部署发布能力。


阿里应用场景与原生 workloads

阿里巴巴容器化道路的起步在国内外都是比较领先的。容器这个技术概念虽然出现得很早,但一直到 2013 年 Docker 产品出现后才逐渐为人所熟知。而阿里巴巴早在 2011 年就开始发展了基于 LXC 的容器技术,经过了几代的系统演进,如今阿里巴巴有着超过百万的容器体量,这个规模在世界范围内都是顶尖的。

随着云技术发展和云原生应用的兴起,我们近两年间逐步将过去的容器迁到了基于 Kubernetes 的云原生环境中。而在这其中,我们遇到了不少应用部署方面的问题。首先对于应用开发者来说,他们对迁移到云原生环境的期望是:
  • 面向丰富业务场景的策略功能
  • 极致的部署发布效率
  • 运行时的稳定性和容错能力


阿里的应用场景非常复杂,基于 Kubernetes 之上生长着很多不同的 PaaS 二层,比如服务于电商业务的运维中台、规模化运维、中间件、Serverless、函数计算等,而每个平台都对部署、发布要求各有不同。

我们再来看一下 Kubernete 原生所提供的两种常用 workload 的能力:

1.png


简单来说,Deployment 和 StatefulSet 在一些小规模的场景下是可以 work 的;但到了阿里巴巴这种应用和容器的规模下,如果全量使用原生 workload 则是完全不现实的。目前阿里内部容器集群上的应用数量超过十万、容器数量达到百万,有部分重点核心应用甚至单个应用下就有上万的容器。再结合上图的问题,我们会发现不仅针对单个应用的发布功能不足,而且当发布高峰期大量应用同时在升级时,超大规模的 Pod 重建也成为一种“灾难”。

阿里自研的扩展 workloads

针对原生 workload 远远无法满足应用场景的问题,我们从各种复杂的业务场景中抽象出共通的应用部署需求,据此开发了多种扩展 workload。在这些 workload 中我们做了大幅的增强和改进,但同时也会严格保证功能的通用化、不允许将业务逻辑耦合进来。

这里我们重点介绍一下 CloneSet 与 Advanced StatefulSet。在阿里内部云原生环境下,几乎全量的电商相关应用都统一采用 CloneSet 做部署发布,而中间件等有状态应用则使用了 Advanced StatefulSet 管理。

2.png


Advanced StatefulSet 顾名思义,是原生 StatefulSet 的增强版,默认行为与原生完全一致,在此之外提供了原地升级、并行发布(最大不可用)、发布暂停等功能。而 CloneSet 则对标原生 Deployment,主要服务于无状态应用,提供了最为全面丰富的部署发布策略。

原地升级

CloneSet、Advanced StatefulSet 均支持指定 Pod 升级方式:
  1. ReCreate:重建 Pod 升级,和原生 Deployment/StatefulSet 一致;
  2. InPlaceIfPossible:如果只修改 image 和 metadata 中的 labels/annotations 等字段,则触发 Pod 原地升级;如果修改了其他 template spec 中的字段,则退化到 Pod 重建升级;
  3. InPlaceOnly:只允许修改 image 和 metadata 中的 labels/annotations 等字段,只会使用原地升级。


所谓原地升级,就是在升级 template 模板的时候,workload 不会把原 Pod 删除、新建,而是直接在原 Pod 对象上更新对应的 image 等数据。

3.png


如上图所示,在原地升级的时候 CloneSet 只会更新 Pod spec 中对应容器的 image,而后 kubelet 看到 Pod 中这个容器的定义发生了变化,则会把对应的容器停掉、拉取新的镜像、并使用新镜像创建启动容器。另外可以看到在过程中,这个 Pod 的 sandbox 容器以及其他本次未升级的容器还一直处于正常运行状态,只有需要升级的容器会受到影响。

原地升级给我们带来的好处实在太多了:
  • 首先就是发布效率大大提升了,根据非完全统计数据,在阿里环境下原地升级至少比完全重建升级提升了 80% 以上的发布速度:不仅省去了调度、分配网络、分配远程盘的耗时,连拉取新镜像的时候都得益于 node 上已有旧镜像、只需要拉取较少的增量 layer);
  • IP 不变、升级过程 Pod 网络不断,除本次升级外的其他容器保持正常运行;
  • Volume 不变,完全复用原容器的挂载设备;
  • 保障了集群确定性,使排布拓扑能通过大促验证。


后续我们将会有专文讲解阿里在 Kubernetes 之上做的原地升级,意义非常重大。如果没有了原地升级,阿里巴巴内部超大规模的应用场景几乎是无法在原生 Kubernetes 环境上完美落地的,我们也鼓励每一位 Kubernetes 用户都应该“体验”一下原地升级,它给我们带来了不同于 Kubernetes 传统发布模式的变革。

流式+分批发布

前一章我们提到了,目前 Deployment 支持 maxUnavailable/maxSurge 的流式升级,而 StatefulSet 支持 partition 的分批升级。但问题在于,Deployment 无法灰度分批,而 StatefulSet 则只能一个一个 Pod 串行发布,没办法并行的流式升级。

首先要说的是,我们将 maxUnavailable 引入了 Advanced StatefulSet。原生 StatefulSet 的 one by one 发布,大家其实可以理解为一个强制 maxUnavailable=1 的过程,而 Advanced StatefulSet 中如果我们配置了更大的 maxUnavailable,那么就支持并行发布更多的 Pod 了。

然后我们再来看一下 CloneSet,它支持原生 Deployment 和 StatefulSet 的全部发布策略,包括 maxUnavailable、maxSurge、partition。那么 CloneSet 是如何把它们结合在一起的呢?我们来看一个例子:

yaml
apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet

...

spec:
replicas: 5 # Pod 总数为 5
updateStrategy:
type: InPlaceIfPossible
maxSurge: 20% # 多扩出来 5 * 20% = 1 个 Pod (rounding up)
maxUnavailable: 0 # 保证发布过程 5 - 0 = 5 个 Pod 可用
partition: 3 # 保留 3 个旧版本 Pod (只发布 5 - 3 = 2 个 Pod)


针对这个副本数为 5 的 CloneSet,如果我们修改了 template 中的 image,同时配置:maxSurge=20%  maxUnavailable=0  partition=3。当开始发布后:
  1. 先扩出来 1 个新版本的 Pod,5 个存量 Pod 保持不动;
  2. 新 Pod ready 后,逐步把旧版本 Pod 做原地升级;
  3. 直到剩余 3 个旧版本 Pod 时,因为满足了 partition 终态,会把新版本 Pod 再删除 1 个;
  4. 此时 Pod 总数仍然为 5,其中 3 个旧版本、1 个新版本。


如果我们接下来把 partition 调整为 0,则 CloneSet 还是会先扩出 1 个额外的新版 Pod,随后逐渐将所有 Pod 升级到新版,最终再次删除一个 Pod,达到 5 个副本全量升级的终态。

发布顺序可配置

对于原生的 Deployment 和 StatefulSet,用户是无法配置发布顺序的。Deployment 下的 Pod 发布顺序完全依赖于它修改 ReplicaSet 后的扩缩顺序,而 StatefulSet 则严格按照 order 的反序来做一一升级。

但在 CloneSet 和 Advanced StatefulSet 中,我们增加了发布顺序的可配置能力,使用户可以定制自己的发布顺序。目前可以通过以下两种发布优先级和一种发布打散策略来定义顺序:
  • 优先级(1):按给定 label key,在发布时根据 Pod labels 中这个 key 对应的 value 值作为权重:


yaml
apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
spec:
# ...
updateStrategy:
priorityStrategy:
orderPriority:
- orderedKey: some-label-key

  • 优先级(2):按 selector 匹配计算权重,发布时根据 Pod 对多个 weight selector 的匹配情况计算权重总和:


yaml
apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
spec:
# ...
updateStrategy:
priorityStrategy:
weightPriority:
- weight: 50
matchSelector:
matchLabels:
test-key: foo
- weight: 30
matchSelector:
matchLabels:
test-key: bar

  • 打散:将匹配 key-value 的 Pod 打散到不同批次中发布:


yaml
apiVersion: apps.kruise.io/v1alpha1
kind: CloneSet
spec:
# ...
updateStrategy:
scatterStrategy:
- key: some-label-key
value: foo


可能有同学会问为什么要配置发布顺序呢?比如 zookeeper 这类应用在发布时,需要先把所有非主节点升级,最后再升级主节点,这样才能保证在整个发布过程中只会发生一次切主。这时用户就可以通过流程打标、或者写一个 operator 自动为 zookeeper 的 Pod 打上节点职责的标签,而后配置非主节点的发布权重较大,使得发布时能够尽量减少切主的次数。

sidecar 容器管理

轻量化容器也是阿里巴巴在云原生阶段的一次重大改革,过去阿里的容器绝大多数都是以“富容器”的方式运行的,所谓“富容器”即在一个容器中既运行业务、也跑着各种各样的插件和守护进程。而在云原生时代,我们在逐渐把原先“富容器”中的旁路插件拆分到独立的 sidecar 容器中,使主容器逐渐回归业务自身。

这里对于拆分的好处就不赘述了,我们来看下另一个问题,就是拆分之后这些 sidecar 容器如何做管理呢?最直观的方式是在每个应用的 workload 中显示去定义 Pod 中需要的 sidecar,但这样带来的问题很多:
  1. 当应用和 workload 数量众多时,我们很难统一的 sidecar 增减管理;
  2. 应用开发者不知道(甚至也不关心)自己的应用需要配置哪些 sidecar 容器;
  3. 当 sidecar 镜像需要升级时,要把所有应用的 workload 全部升级一遍,很不现实。


因此,我们设计了 SidecarSet,将 sidecar 容器的定义与应用 workload 解耦。应用开发者们不再需要再关心自己的 workload 中需要写哪些 sidecar 容器,而通过原地升级, sidecar 维护者们也可以自主地管理和升级 sidecar 容器。

4.png


开放能力应用

到了这里,大家是不是对阿里巴巴的应用部署模式有了一个基本的了解呢?其实上述的能力都已经开源到了社区,我们的项目就叫做 OpenKruise,目前它已经提供了 5 种扩展 workload:
  • CloneSet:提供了更加高效、确定可控的应用管理和部署能力,支持优雅原地升级、指定删除、发布顺序可配置、并行/灰度发布等丰富的策略,可以满足更多样化的应用场景;
  • Advanced StatefulSet:基于原生 StatefulSet 之上的增强版本,默认行为与原生完全一致,在此之外提供了原地升级、并行发布(最大不可用)、发布暂停等功能;
  • SidecarSet:对 sidecar 容器做统一管理,在满足 selector 条件的 Pod 中注入指定的 sidecar 容器;
  • UnitedDeployment:通过多个 subset workload 将应用部署到多个可用区;
  • BroadcastJob:配置一个 job,在集群中所有满足条件的 Node 上都跑一个 Pod 任务。


此外,我们还有更多的扩展能力还在开源的路上!近期,我们会将内部的 Advanced DaemonSet 开放到 OpenKruise 中,它在原生 DaemonSet 的 maxUnavailable 之上,额外提供了如分批、selector 等发布策略,分批的功能使 DaemonSet 在发布的时候能够只升级其中部分 Pod,而 selector 更是允许发布的时候指定先在符合某些标签的 node 上升级,这为我们在大规模集群中升级 DaemonSet 带来了灰度能力和稳定性的保障。

而后续,我们还计划将阿里巴巴内部扩展的 HPA、调度插件等通用化能力开放出来,让每一位 Kubernetes 开发者和阿里云上的用户都能很便捷地使用上阿里内部开发应用的云原生增强能力。

最后,我们也欢迎每一位云原生爱好者来共同参与 OpenKruise 的建设。与其他一些开源项目不同,OpenKruise 并不是阿里内部代码的复刻;恰恰相反,OpenKruise Github 仓库是阿里内部代码库的 upstream。因此,每一行你贡献的代码,都将运行在阿里内部的所有 Kubernetes 集群中、都将共同支撑了阿里巴巴全球顶尖规模的应用场景!

Q & A

Q1:目前阿里最大规模的业务 pod 数量有多少,发布一次需要多少时间?
A1:这个只能透露数量目前最大规模的单个应用下数量是以万为单位的,一次发布时间要看具体分批灰度的时长了。如果分批较多、观察时间较长的话,可能是会持续一两周的。

Q2:pod 的资源 request 和 limit 是怎么配置的?request 和 limit 是什么比例来配置?过多的 request 造成浪费,过少可能会导致热点 node 负载超高。
A2:这个主要还是根据应用的需求来定的,目前大部分在线应用都是 1:1 的关系,部分离线和job 类型的会配置 request>limit。

Q3:kruise 升级问题,升级 kurise apiversion 版本的情况下,原有的版本的部署如何升级?
A3:目前 kruise 中资源的 apiVersion 还都是统一的。我们计划在今年下半年将部分较为成熟的 workload 进行版本升级,用户在自己的 K8s 集群内升级后,存量的旧版本资源会自动通过 conversion 升级到新版本。

Q4:OpenKruise 有提供 go-client 吗?
A4:目前提供两个方式:1. 引入 github.com/openkruise/kruise/pkg/client 包,下面有生成好的 clientset / informer / lister 等工具;2. 使用 controller-runtime 的用户(包括 kubebuilder、operator-sdk),直接引入 github.com/openkruise/kruise-api 轻量化依赖,然后加到 scheme 里就能直接用了。

Q5:阿里 K8s 版本升级是如何做的?
A5:阿里集团内部使用 Kube-On-Kube 的架构进行大规模的 Kubernetes 集群管理,用一个元 K8s 集群管理成百上千个业务 K8s 集群。其中元集群版本较为稳定,业务集群会进行频繁升级,业务集群的升级流程事实上就是对元集群中的 workloads(原生 workloads 以及 kruise workloads) 进行版本或配置升级,与正常情况我们对业务 workloads 的升级流程相似。

Q6:这个灰度之后,流量是怎么切的?
A6:在原地升级前,kruise 会先通过 readinessGate 将 Pod 置为 not-ready,此时 endpoint 等控制器会感知到并把 Pod 从端点摘掉。然后 kruise 更新 pod image 触发容器重建,完成后再把 Pod 改为 ready。

Q7:daemonset 的分批是通过类似 deployment 的暂停功能实现的么?统计已经发布数量然后暂停,然后继续,然后再暂停。
A7:总体过程上类似,升级过程中对新旧版本进行统计并判断是否已达到指定终态。但相比 deployment,daemonset 需要处理比较复杂的边界情况(例如初次发布时集群中并没有指定的 Pod),具体细节可以持续关注我们即将开源的代码。

Q8:多集群发布页面上怎么开始发布的?
A8:直播中演示的是一个 demo 的发布系统结合 Kruise Workloads 的例子,从交互上是通过用户选择对应的集群,点击开始执行进行发布;从实现上实际是对新版本的 YAML 与集群中的 YAML 计算 diff 后 Patch 进集群,再操作 DaemonSet 的控制字段(partition / paused 等),控制灰度进程。

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”
收起阅读 »

Kubernetes安全矩阵


【编者的话】MITRE ATT&CK的目标是创建网络攻击中使用的已知对抗战术和技术的详尽列表。简单来说,ATT&CK是MITRE提供的“对抗战术、技术和常识”框架,它按照一种易于理解的格式将所有已知的战术和技术进行排列。攻击战术展示在矩阵顶部,每列下面列出了单独的技术。仿效ATT&CK,微软发布云安全攻击矩阵。下文是ILDC Azure安全中心安全研究软件工程师Yossi Weizman关于Kubernetes攻击矩阵的一篇介绍文章。

Kubernetes是目前最流行的容器编制系统,同时它也是历史上成长最快的开源项目之一,它已经成为许多公司计算系统交付的基石。由于容器的灵活性和可伸缩性,众多开发者开始将工作迁移至Kubernetes。Kubernetes优势众多,但是容器的安全挑战还是接踵而至,这应该引起我们的重视。因此,了解容器化环境中存在的各种安全风险是至关重要的,特别是Kubernetes。

MITRE ATT&CK®框架是涉及网络攻击的已知战术和技术的知识库。从Windows和Linux,MITRE ATT&CK矩阵模型涵盖了涉及网络攻击的各个阶段(战术),并详细阐述了每个阶段的已知方法(技术)。这些矩阵可以帮助企业了解其环境中的攻击面,并确保可以充分预检和排除各种风险。

MITRE ATT&CK®框架策略包括:
  • 初始访问
  • 执行
  • 持久化
  • 权限提升
  • 防御绕过
  • 凭据访问
  • 发现
  • 横向运动
  • 影响


当在Azuer安全中心开始绘制Kubernetes蓝图时我们发现,尽管针对Linux或Windows平台的攻击技术有所差异,但根本策略是相似的。例如,对前四个策略的转换来看:
  1. “对计算机的初始访问”变为“对群集的初始访问”;
  2. “计算机上的恶意代码”变为“容器上的恶意活动”;
  3. “保持对计算机的访问”变为“保持对群集的访问”;
  4. “在计算机上获得更高的权限”变为“在群集中获得更高的权限”。


基于此,我们改造了第一个工具矩阵为Kubernetes版本ATT&CK的威胁攻击矩阵,其中包括与容器编排安全相关的主要技术。
k8s-matrix.png

从上图可以看出,矩阵包含了列出的9个策略。每一个策略又都包含多项技术,攻击者可以使用这些技术来实现不同的目标。下面对每一个策略展开具体的介绍。

1. 初始访问

初始访问策略包括用于获得对资源访问权限的技术。 在容器化环境中,这些技术使你可以首先访问群集。

此访问可以直接通过群集管理层进行,也可以通过获取对部署在群集上的恶意或易受攻击资源的访问来实现。

使用云的凭证

如果Kubernetes集群部署在公共云(例如,Azure中的AKS、GCP中的GKE或AWS中的EKS),云凭据的泄露可能会导致集群被接管。有权访问云帐户凭证的攻击者可以访问整个集群的管理层。

恶意镜像

在集群中运行损坏的镜像可能会损害集群。访问私有注册表的攻击者可以在注册表中植入自己的非法代码或者木马等供用户拉取使用。此外,用户获取来自公共镜像管理库(例如Docker Hub)中未经验证的镜像,这些镜像可能是恶意镜像。

基于不可信的基础镜像构建的镜像也会导致类似的结果。

Kubeconfig文件

Kubeconfig文件也用于kubectl,其中包含关于Kubernetes集群的详细信息,包括它们的分区和凭据。如果集群托管为云服务(如AKS或GKE),则通过云命令将该文件下载到客户端(例如,AKS的“az AKS get-credential”或GKE的“gcloud容器集群get-credentials”)。

如果攻击者窃取到这个文件,他们就可以使用它来访问集群。

有漏洞的应用程序

在集群中运行有公开漏洞的应用程序可引起集群的可访问权限暴露。可能会触发远程代码执行突破(RCE)攻击容器。如果服务帐户安装在容器中(Kubernetes的默认行为),则攻击者可以窃取服务帐户并对API服务器发送请求。

暴露的仪表板

Kubernetes仪表板是一个基于Web的用户界面,支持监视和管理Kubernetes集群。默认情况下,仪表板只能内网访问(ClusterIP服务)。如果仪表板是在外网公开,则可以允许进行身份验证和部署远程管理。

2. 执行攻击

执行策略包括攻击者在集群中运行其代码的技术。

容器中执行

具有权限的攻击者可以使用exec命令(“kubectl exec”)在集群中的容器中运行恶意命令。在这种方法中,攻击者可以使用合法的镜像,例如OS镜像(例如Ubuntu)作为后门,并通过使用“kubectl exec”远程运行恶意代码。

新建容器

攻击者可能尝试通过新建容器在集群中运行其代码。有权在群集中部署Pod或控制器的攻击者(例如DaemonSet\ReplicaSet\Deployment)则可以创建用于代码执行的新容器。

利用应用程序

部署在集群中的应用程序容易受到远程代码执行漏洞(或最终允许代码执行的漏洞)的攻击,从而使攻击者能够在集群中运行代码。

如果将服务帐户挂载到容器(Kubernetes中的默认行为),攻击者将能够使用此服务帐户凭据向API服务器发送请求。

在容器内运行的SSH服务器

在容器内运行的SSH服务器可能被攻击者使用。如果攻击者获得到容器的有效凭证,无论是通过暴力尝试还是通过其他方法(如钓鱼),他们都可以使用它通过SSH获得对容器的远程访问。

3. 持久化

持久化策略包括一些技术,攻击者使用这些技术来保持对集群的访问,以防失去最初的立足点。

后门容器

攻击者在集群的容器中运行恶意代码。通过使用诸如DaemonSet或Deployments之类的Kubernetes控制器,攻击者可以确保在集群中的一个或者多个节点中运行容器。

可写的hostPath挂载

hostPath卷将目录或文件从主机加载到容器。具有在集群中创建新容器权限的攻击者可以创建一个具有可写hostPath卷的容器,并执行持久化。例如,可以通过在主机上创建cron作业来实现。

Kubernetes计划

Kubernetes Job可以用于运行为批处理作业执行有限任务的容器。Kubernetes Job是一个控制器,它创建一个或多个Pod,并确保指定数量的Pod终端。Kubernetes CronJob用于计划作业。攻击者可能使用Kubernetes CronJob来调度集群容器执行恶意代码。

4. 权限提升

权限提升策略由攻击者用来在环境中获取比其当前拥有的更高特权的技术组成。在容器化环境中,权限提升技术包括从容器访问节点,在集群中获得更高的特权,甚至获得对云资源的访问。

高权限容器

高权限容器是具有主机的所有功能的容器,它消除了常规容器的所有限制。实际上,这意味着特权容器可以执行几乎所有可以直接在主机上执行的操作。攻击者可以访问特权容器,或者拥有创建新的高权限容器的权限(例如,通过使用窃取的Pod的服务帐户),可以访问主机的资源。

Cluster-admin绑定

基于角色的访问控制(RBAC)是Kubernetes中的一个关键安全特性。RBAC可以限制集群中各种角色操作。Cluster-admin是Kubernetes中的内置高特权角色,有权在群集中创建绑定和群集绑定的攻击者可以创建到群集管理员ClusterRole或其他高特权角色的绑定。

hostPath挂载

攻击者可以使用hostPath挂载来访问基础主机,从而从容器破坏主机。

访问云资源

如果Kubernetes集群部署在云上,在某些情况下,攻击者可以利用对单个容器的访问来访问集群之外的其他云资源。例如,在AKS中,每个节点都包含服务principal凭证,该凭证存储在/etc/kubernetes/azure.json中。AKS使用该服务principal来创建和管理群集操作所需的Azure资源。
默认情况下,服务主体在集群的资源组中具有contributor权限。访问这个服务主体文件(例如,通过hostPath mount)的攻击者可以使用它的凭证来访问或修改云资源。

5. 防御绕过

防御绕过策略由攻击者用来避免检测并隐藏其活动的技术组成。

清除容器日志

攻击者可能会攻陷容器上的应用程序或系统日志,以防止检测到其活动。

删除Kubernetes事件

Kubernetes事件是一个Kubernetes对象,它记录集群中资源的状态变化和故障。示例事件包括容器创建、镜像获取或节点上的Pod调度。

Kubernetes事件对于识别集群更新非常有用。因此,攻击者可能想要删除这些事件记录(例如,通过使用:“kubectl delete event - all”),以避免检测到他们在集群中的活动。

Pod/容器名混淆

由诸如Deploymen或DaemonSet之类的控制器创建的Pod在其名称中具有随机后缀。攻击者可以利用此规律,并为后门容器命名,因为它们是由现有控制器创建的。例如,攻击者可能创建一个名为coredns-{随机字串后缀}的恶意容器,该容器看上去与CoreDNS部署有关。

同样,攻击者可以将其容器部署在管理容器所在的kube系统名称空间。

从代理服务器连接

攻击者可能使用代理服务器来隐藏他们的原始IP。具体来说,攻击者经常使用匿名网络(如TOR)进行攻击活动。这可以用于与应用程序本身或与API服务器进行通信。

6. 凭据访问

凭据访问策略由攻击者用来窃取凭据的技术组成。在容器化的环境中,这包括正在运行的应用程序的凭据、身份、存储在集群中的密码或云凭据。

列出Kubernetes secrets

Kubernetes secret是一个对象,它允许用户存储和管理敏感信息,比如集群中的密码和连接字符串。可以通过Pod配置中的引用来使用secret。
有权从API服务器搜索机密的攻击者(例如,通过使用Pod服务帐户)可以访问敏感信息,其中可能包括各种服务的凭据。

挂载服务主体

当集群部署在云中时,在某些情况下,攻击者可以利用他们对集群中的容器的访问来获得云证书。例如,在AKS中,每个节点都包含服务主体凭据。

访问容器服务帐户

服务帐户(SA)在Kubernetes中表示一个应用程序标识。默认情况下,SA挂载到集群中创建的每个Pod。使用SA,Pod中的容器可以将请求发送到Kubernetes API服务器。访问Pod的攻击者可以访问SA令牌(位于/var/run/secrets/kubernet .io/serviceaccount/token),并根据SA权限在集群中执行操作。如果没有启用RBAC,那么SA在集群中具有无限的权限。如果启用了RBAC,它的权限由其关联角色绑定\ClusterRoleBindings确定。

配置文件中的应用程序凭据

开发人员将Kubernetes secret存储在Kubernetes配置文件中,例如Pod配置中的环境变量。这种行为在Azure Security Center监控的集群中很常见。通过查询API服务器或访问开发人员终端上的文件,攻击者如果可以访问到此文件,则可以窃取存储的secret并使用它们。

7. 发现

发现策略由攻击者用来探测可以访问的环境的技术组成。这些技术有助于攻击者进行横向移动并获得更多资源。

访问Kubernetes API服务器

Kubernetes API服务器是群集的网关。通过向RESTful API发送各种请求来执行集群中的操作。API服务器可以检索群集的状态,其中包括部署在群集上的所有组件。攻击者可能会发送API请求来探测集群,并获取有关集群中的容器,secrets和其他资源的信息。

访问Kubelet API

Kubelet是安装在每个节点上的Kubernetes代理。Kubelet负责正确执行分配给该节点的Pod。具有网络访问主机权限的攻击者(例如,通过攻陷容器上运行代码)可以请求访问通过Kubelet公开的只读API服务(TCP端口10255)访问Kubelet API。

通过访问[NODE IP]:10255/pods /检索节点上正在运行的Pod。

通过访问[NODE IP]:10255/spec/检索有关节点本身的信息,例如CPU和内存消耗。

网络映射

攻击者有可能尝试对集群网络探测以获取有关正在运行的应用程序的信息,包括扫描已知漏洞。默认情况下,在Kubernetes中对Pod通讯没有任何限制。因此,获得单个容器访问权限的攻击者可能会使用它来探测网络。

访问Kubernetes仪表板

Kubernetes仪表板是用于监视和管理Kubernetes集群的Web UI。仪表板允许用户使用其服务帐户(kubernetes-dashboard)在群集中执行操作,该权限由该服务帐户的绑定或群集绑定确定。获得对群集中容器的访问权限的攻击者可以使用其对仪表板容器的网络访问权限。因此,攻击者可能会使用仪表板的身份来检索有关群集中各种资源的信息。

实例化元数据API

云提供者提供了实例元数据服务,用于检索有关虚拟机的信息,例如网络配置,磁盘和SSH公钥。虚拟机通过不可路由的IP地址访问此服务,该IP地址只能从虚拟机内部访问。获得容器访问权限的攻击者可以查询元数据API服务,以获取有关基础节点的信息。例如,在AWS中,以下请求将检索实例的所有元数据信息:http:///metadata/instance?api-version=2019-06-01

8. 横向运动

横向运动策略攻击者在攻击目标环境中获取不同环境权限的技术。在容器化环境中,这包括从对一个容器的给定访问中获得对群集中各种资源的访问权,从容器中对基础节点的访问权或对云环境的访问权。

访问云资源

攻击者可能会从已经入侵的容器扩展到云环境。

容器服务帐户

获得对群集中容器的访问权限的攻击者可以使用已安装的服务帐户令牌请求访问API服务器,并获得对群集中其他资源的访问权限。

集群内部网络

Kubernetes网络允许集群中Pod之间的通信作为默认行为。获得单个容器访问权的攻击者可以使用该容器访问集群另一个容器。

配置文件中的应用程序凭据

例如,开发人员将secret存储在Kubernetes配置文件中,作为Pod配置中的环境变量。使用这些凭证的攻击者可以访问集群内外的其他资源。

可写卷在主机上挂载

攻击者可能试图从已被入侵的容器访问底层主机。

访问Kubernetes仪表板

可以访问Kubernetes仪表板的攻击者可以管理集群资源,还可以使用仪表板的内置“exec”功能在集群中的各种容器上运行他们的代码。

访问Tiller端点

Helm是CNCF为Kubernetes维护的一个流行的包管理器。Tiller是Helm到版本2的服务器端组件。

Tiller公开集群中的内部gRPC端点,监听端口44134。默认情况下,此端点不需要身份验证。攻击者可以在tiller的服务可以访问的任何容器上运行代码,并使用tiller的服务帐户在集群中执行操作,该帐户通常具有很高的特权。

9. 影响

影响策略包括攻击者用来破坏,滥用或破坏环境正常行为的技术。

数据破坏

攻击者可能试图破坏集群中的数据和资源。这包括删除部署、配置、存储和计算资源。

资源劫持

攻击者可能会滥用受损害的资源来运行任务。一种常见的滥用是使用受损害的资源来挖矿(数字货币挖掘)。能够访问集群中的容器或具有创建新容器的权限的攻击者可以将它们用于此类活动。

拒绝服务

攻击者可能试图执行拒绝服务攻击,从而使合法用户无法使用服务。在容器集群中,这包括试图阻止容器本身、底层节点或API服务器的可用性。

了解容器化环境的攻击策略是构建容器安全解决方案的第一步。上面介绍的矩阵可以帮助我们识别针对Kubernetes的不同威胁以及安全抵御水平,补全安全威胁短板和瓶颈,提高集群的安全性。

理解容器化环境的攻击面是为这些环境构建安全解决方案的第一步。上面介绍的矩阵可以帮助组织识别当前针对针对Kubernetes的不同威胁的防御覆盖率的缺口。Azure安全中心可以帮助您保护容器环境。了解更多关于Azure安全中心对容器安全的支持。

原文链接:Threat matrix for Kubernetes(翻译:张亚龙)
继续阅读 »

【编者的话】MITRE ATT&CK的目标是创建网络攻击中使用的已知对抗战术和技术的详尽列表。简单来说,ATT&CK是MITRE提供的“对抗战术、技术和常识”框架,它按照一种易于理解的格式将所有已知的战术和技术进行排列。攻击战术展示在矩阵顶部,每列下面列出了单独的技术。仿效ATT&CK,微软发布云安全攻击矩阵。下文是ILDC Azure安全中心安全研究软件工程师Yossi Weizman关于Kubernetes攻击矩阵的一篇介绍文章。

Kubernetes是目前最流行的容器编制系统,同时它也是历史上成长最快的开源项目之一,它已经成为许多公司计算系统交付的基石。由于容器的灵活性和可伸缩性,众多开发者开始将工作迁移至Kubernetes。Kubernetes优势众多,但是容器的安全挑战还是接踵而至,这应该引起我们的重视。因此,了解容器化环境中存在的各种安全风险是至关重要的,特别是Kubernetes。

MITRE ATT&CK®框架是涉及网络攻击的已知战术和技术的知识库。从Windows和Linux,MITRE ATT&CK矩阵模型涵盖了涉及网络攻击的各个阶段(战术),并详细阐述了每个阶段的已知方法(技术)。这些矩阵可以帮助企业了解其环境中的攻击面,并确保可以充分预检和排除各种风险。

MITRE ATT&CK®框架策略包括:
  • 初始访问
  • 执行
  • 持久化
  • 权限提升
  • 防御绕过
  • 凭据访问
  • 发现
  • 横向运动
  • 影响


当在Azuer安全中心开始绘制Kubernetes蓝图时我们发现,尽管针对Linux或Windows平台的攻击技术有所差异,但根本策略是相似的。例如,对前四个策略的转换来看:
  1. “对计算机的初始访问”变为“对群集的初始访问”;
  2. “计算机上的恶意代码”变为“容器上的恶意活动”;
  3. “保持对计算机的访问”变为“保持对群集的访问”;
  4. “在计算机上获得更高的权限”变为“在群集中获得更高的权限”。


基于此,我们改造了第一个工具矩阵为Kubernetes版本ATT&CK的威胁攻击矩阵,其中包括与容器编排安全相关的主要技术。
k8s-matrix.png

从上图可以看出,矩阵包含了列出的9个策略。每一个策略又都包含多项技术,攻击者可以使用这些技术来实现不同的目标。下面对每一个策略展开具体的介绍。

1. 初始访问

初始访问策略包括用于获得对资源访问权限的技术。 在容器化环境中,这些技术使你可以首先访问群集。

此访问可以直接通过群集管理层进行,也可以通过获取对部署在群集上的恶意或易受攻击资源的访问来实现。

使用云的凭证

如果Kubernetes集群部署在公共云(例如,Azure中的AKS、GCP中的GKE或AWS中的EKS),云凭据的泄露可能会导致集群被接管。有权访问云帐户凭证的攻击者可以访问整个集群的管理层。

恶意镜像

在集群中运行损坏的镜像可能会损害集群。访问私有注册表的攻击者可以在注册表中植入自己的非法代码或者木马等供用户拉取使用。此外,用户获取来自公共镜像管理库(例如Docker Hub)中未经验证的镜像,这些镜像可能是恶意镜像。

基于不可信的基础镜像构建的镜像也会导致类似的结果。

Kubeconfig文件

Kubeconfig文件也用于kubectl,其中包含关于Kubernetes集群的详细信息,包括它们的分区和凭据。如果集群托管为云服务(如AKS或GKE),则通过云命令将该文件下载到客户端(例如,AKS的“az AKS get-credential”或GKE的“gcloud容器集群get-credentials”)。

如果攻击者窃取到这个文件,他们就可以使用它来访问集群。

有漏洞的应用程序

在集群中运行有公开漏洞的应用程序可引起集群的可访问权限暴露。可能会触发远程代码执行突破(RCE)攻击容器。如果服务帐户安装在容器中(Kubernetes的默认行为),则攻击者可以窃取服务帐户并对API服务器发送请求。

暴露的仪表板

Kubernetes仪表板是一个基于Web的用户界面,支持监视和管理Kubernetes集群。默认情况下,仪表板只能内网访问(ClusterIP服务)。如果仪表板是在外网公开,则可以允许进行身份验证和部署远程管理。

2. 执行攻击

执行策略包括攻击者在集群中运行其代码的技术。

容器中执行

具有权限的攻击者可以使用exec命令(“kubectl exec”)在集群中的容器中运行恶意命令。在这种方法中,攻击者可以使用合法的镜像,例如OS镜像(例如Ubuntu)作为后门,并通过使用“kubectl exec”远程运行恶意代码。

新建容器

攻击者可能尝试通过新建容器在集群中运行其代码。有权在群集中部署Pod或控制器的攻击者(例如DaemonSet\ReplicaSet\Deployment)则可以创建用于代码执行的新容器。

利用应用程序

部署在集群中的应用程序容易受到远程代码执行漏洞(或最终允许代码执行的漏洞)的攻击,从而使攻击者能够在集群中运行代码。

如果将服务帐户挂载到容器(Kubernetes中的默认行为),攻击者将能够使用此服务帐户凭据向API服务器发送请求。

在容器内运行的SSH服务器

在容器内运行的SSH服务器可能被攻击者使用。如果攻击者获得到容器的有效凭证,无论是通过暴力尝试还是通过其他方法(如钓鱼),他们都可以使用它通过SSH获得对容器的远程访问。

3. 持久化

持久化策略包括一些技术,攻击者使用这些技术来保持对集群的访问,以防失去最初的立足点。

后门容器

攻击者在集群的容器中运行恶意代码。通过使用诸如DaemonSet或Deployments之类的Kubernetes控制器,攻击者可以确保在集群中的一个或者多个节点中运行容器。

可写的hostPath挂载

hostPath卷将目录或文件从主机加载到容器。具有在集群中创建新容器权限的攻击者可以创建一个具有可写hostPath卷的容器,并执行持久化。例如,可以通过在主机上创建cron作业来实现。

Kubernetes计划

Kubernetes Job可以用于运行为批处理作业执行有限任务的容器。Kubernetes Job是一个控制器,它创建一个或多个Pod,并确保指定数量的Pod终端。Kubernetes CronJob用于计划作业。攻击者可能使用Kubernetes CronJob来调度集群容器执行恶意代码。

4. 权限提升

权限提升策略由攻击者用来在环境中获取比其当前拥有的更高特权的技术组成。在容器化环境中,权限提升技术包括从容器访问节点,在集群中获得更高的特权,甚至获得对云资源的访问。

高权限容器

高权限容器是具有主机的所有功能的容器,它消除了常规容器的所有限制。实际上,这意味着特权容器可以执行几乎所有可以直接在主机上执行的操作。攻击者可以访问特权容器,或者拥有创建新的高权限容器的权限(例如,通过使用窃取的Pod的服务帐户),可以访问主机的资源。

Cluster-admin绑定

基于角色的访问控制(RBAC)是Kubernetes中的一个关键安全特性。RBAC可以限制集群中各种角色操作。Cluster-admin是Kubernetes中的内置高特权角色,有权在群集中创建绑定和群集绑定的攻击者可以创建到群集管理员ClusterRole或其他高特权角色的绑定。

hostPath挂载

攻击者可以使用hostPath挂载来访问基础主机,从而从容器破坏主机。

访问云资源

如果Kubernetes集群部署在云上,在某些情况下,攻击者可以利用对单个容器的访问来访问集群之外的其他云资源。例如,在AKS中,每个节点都包含服务principal凭证,该凭证存储在/etc/kubernetes/azure.json中。AKS使用该服务principal来创建和管理群集操作所需的Azure资源。
默认情况下,服务主体在集群的资源组中具有contributor权限。访问这个服务主体文件(例如,通过hostPath mount)的攻击者可以使用它的凭证来访问或修改云资源。

5. 防御绕过

防御绕过策略由攻击者用来避免检测并隐藏其活动的技术组成。

清除容器日志

攻击者可能会攻陷容器上的应用程序或系统日志,以防止检测到其活动。

删除Kubernetes事件

Kubernetes事件是一个Kubernetes对象,它记录集群中资源的状态变化和故障。示例事件包括容器创建、镜像获取或节点上的Pod调度。

Kubernetes事件对于识别集群更新非常有用。因此,攻击者可能想要删除这些事件记录(例如,通过使用:“kubectl delete event - all”),以避免检测到他们在集群中的活动。

Pod/容器名混淆

由诸如Deploymen或DaemonSet之类的控制器创建的Pod在其名称中具有随机后缀。攻击者可以利用此规律,并为后门容器命名,因为它们是由现有控制器创建的。例如,攻击者可能创建一个名为coredns-{随机字串后缀}的恶意容器,该容器看上去与CoreDNS部署有关。

同样,攻击者可以将其容器部署在管理容器所在的kube系统名称空间。

从代理服务器连接

攻击者可能使用代理服务器来隐藏他们的原始IP。具体来说,攻击者经常使用匿名网络(如TOR)进行攻击活动。这可以用于与应用程序本身或与API服务器进行通信。

6. 凭据访问

凭据访问策略由攻击者用来窃取凭据的技术组成。在容器化的环境中,这包括正在运行的应用程序的凭据、身份、存储在集群中的密码或云凭据。

列出Kubernetes secrets

Kubernetes secret是一个对象,它允许用户存储和管理敏感信息,比如集群中的密码和连接字符串。可以通过Pod配置中的引用来使用secret。
有权从API服务器搜索机密的攻击者(例如,通过使用Pod服务帐户)可以访问敏感信息,其中可能包括各种服务的凭据。

挂载服务主体

当集群部署在云中时,在某些情况下,攻击者可以利用他们对集群中的容器的访问来获得云证书。例如,在AKS中,每个节点都包含服务主体凭据。

访问容器服务帐户

服务帐户(SA)在Kubernetes中表示一个应用程序标识。默认情况下,SA挂载到集群中创建的每个Pod。使用SA,Pod中的容器可以将请求发送到Kubernetes API服务器。访问Pod的攻击者可以访问SA令牌(位于/var/run/secrets/kubernet .io/serviceaccount/token),并根据SA权限在集群中执行操作。如果没有启用RBAC,那么SA在集群中具有无限的权限。如果启用了RBAC,它的权限由其关联角色绑定\ClusterRoleBindings确定。

配置文件中的应用程序凭据

开发人员将Kubernetes secret存储在Kubernetes配置文件中,例如Pod配置中的环境变量。这种行为在Azure Security Center监控的集群中很常见。通过查询API服务器或访问开发人员终端上的文件,攻击者如果可以访问到此文件,则可以窃取存储的secret并使用它们。

7. 发现

发现策略由攻击者用来探测可以访问的环境的技术组成。这些技术有助于攻击者进行横向移动并获得更多资源。

访问Kubernetes API服务器

Kubernetes API服务器是群集的网关。通过向RESTful API发送各种请求来执行集群中的操作。API服务器可以检索群集的状态,其中包括部署在群集上的所有组件。攻击者可能会发送API请求来探测集群,并获取有关集群中的容器,secrets和其他资源的信息。

访问Kubelet API

Kubelet是安装在每个节点上的Kubernetes代理。Kubelet负责正确执行分配给该节点的Pod。具有网络访问主机权限的攻击者(例如,通过攻陷容器上运行代码)可以请求访问通过Kubelet公开的只读API服务(TCP端口10255)访问Kubelet API。

通过访问[NODE IP]:10255/pods /检索节点上正在运行的Pod。

通过访问[NODE IP]:10255/spec/检索有关节点本身的信息,例如CPU和内存消耗。

网络映射

攻击者有可能尝试对集群网络探测以获取有关正在运行的应用程序的信息,包括扫描已知漏洞。默认情况下,在Kubernetes中对Pod通讯没有任何限制。因此,获得单个容器访问权限的攻击者可能会使用它来探测网络。

访问Kubernetes仪表板

Kubernetes仪表板是用于监视和管理Kubernetes集群的Web UI。仪表板允许用户使用其服务帐户(kubernetes-dashboard)在群集中执行操作,该权限由该服务帐户的绑定或群集绑定确定。获得对群集中容器的访问权限的攻击者可以使用其对仪表板容器的网络访问权限。因此,攻击者可能会使用仪表板的身份来检索有关群集中各种资源的信息。

实例化元数据API

云提供者提供了实例元数据服务,用于检索有关虚拟机的信息,例如网络配置,磁盘和SSH公钥。虚拟机通过不可路由的IP地址访问此服务,该IP地址只能从虚拟机内部访问。获得容器访问权限的攻击者可以查询元数据API服务,以获取有关基础节点的信息。例如,在AWS中,以下请求将检索实例的所有元数据信息:http:///metadata/instance?api-version=2019-06-01

8. 横向运动

横向运动策略攻击者在攻击目标环境中获取不同环境权限的技术。在容器化环境中,这包括从对一个容器的给定访问中获得对群集中各种资源的访问权,从容器中对基础节点的访问权或对云环境的访问权。

访问云资源

攻击者可能会从已经入侵的容器扩展到云环境。

容器服务帐户

获得对群集中容器的访问权限的攻击者可以使用已安装的服务帐户令牌请求访问API服务器,并获得对群集中其他资源的访问权限。

集群内部网络

Kubernetes网络允许集群中Pod之间的通信作为默认行为。获得单个容器访问权的攻击者可以使用该容器访问集群另一个容器。

配置文件中的应用程序凭据

例如,开发人员将secret存储在Kubernetes配置文件中,作为Pod配置中的环境变量。使用这些凭证的攻击者可以访问集群内外的其他资源。

可写卷在主机上挂载

攻击者可能试图从已被入侵的容器访问底层主机。

访问Kubernetes仪表板

可以访问Kubernetes仪表板的攻击者可以管理集群资源,还可以使用仪表板的内置“exec”功能在集群中的各种容器上运行他们的代码。

访问Tiller端点

Helm是CNCF为Kubernetes维护的一个流行的包管理器。Tiller是Helm到版本2的服务器端组件。

Tiller公开集群中的内部gRPC端点,监听端口44134。默认情况下,此端点不需要身份验证。攻击者可以在tiller的服务可以访问的任何容器上运行代码,并使用tiller的服务帐户在集群中执行操作,该帐户通常具有很高的特权。

9. 影响

影响策略包括攻击者用来破坏,滥用或破坏环境正常行为的技术。

数据破坏

攻击者可能试图破坏集群中的数据和资源。这包括删除部署、配置、存储和计算资源。

资源劫持

攻击者可能会滥用受损害的资源来运行任务。一种常见的滥用是使用受损害的资源来挖矿(数字货币挖掘)。能够访问集群中的容器或具有创建新容器的权限的攻击者可以将它们用于此类活动。

拒绝服务

攻击者可能试图执行拒绝服务攻击,从而使合法用户无法使用服务。在容器集群中,这包括试图阻止容器本身、底层节点或API服务器的可用性。

了解容器化环境的攻击策略是构建容器安全解决方案的第一步。上面介绍的矩阵可以帮助我们识别针对Kubernetes的不同威胁以及安全抵御水平,补全安全威胁短板和瓶颈,提高集群的安全性。

理解容器化环境的攻击面是为这些环境构建安全解决方案的第一步。上面介绍的矩阵可以帮助组织识别当前针对针对Kubernetes的不同威胁的防御覆盖率的缺口。Azure安全中心可以帮助您保护容器环境。了解更多关于Azure安全中心对容器安全的支持。

原文链接:Threat matrix for Kubernetes(翻译:张亚龙) 收起阅读 »

基于DDD的Golang实现


本篇文章分享基于DDD的Golang实现,DDD即领域驱动设计,该模式也算是比较热门的话题了。希望通过本篇文章,大家能够掌握DDD模式,能对大家有所帮助。

领域驱动设计(DDD)是一种软件开发方法,通过将实现与不断演变的模型相连接,简化了开发人员面临的复杂性。

本文不会重点去解释Golang中实现DDD的相关理念,而是作者根据自己的研究对DDD的理解。

什么是DDD?

以下是考虑使用DDD的原因:
  • 提供解决困难问题的原则和模式
  • 将复杂的设计基于领域模型
  • 在技术和领域专家之间发起创造性的协作,以迭代地完善解决领域问题的概念模型。


DDD包含4个层:
  • Domain:这是定义应用程序的域和业务逻辑的地方
  • Infrastructure:此层包含独立于我们的应用程序而存在的所有内容:外部库,数据库引擎等。
  • Application:该层用作域和界面层之间的通道。将请求从接口层发送到域层,由域层处理请求并返回响应。
  • Interface:该层包含与其他系统交互的所有内容,例如Web服务,RMI接口或Web应用程序以及批处理前端。


1.png

开始

我们将构建一个食物推荐API。

首先要做的是初始化依赖关系管理。我们将使用go.mod。在根目录(路径:food-app /)中,初始化go.mod:
go mod init food-app

项目的组织结构:
2.png

在该应用中,我们将使用postgres和redis数据库持久化数据。先定义一个含有连接信息的.env文件。

.env文件内容:
#Postgres
APP_ENV=local
API_PORT=8888
DB_HOST=127.0.0.1
DB_DRIVER=postgres
ACCESS_SECRET=98hbun98h
REFRESH_SECRET=786dfdbjhsb
DB_USER=steven
DB_PASSWORD=password
DB_NAME=food-app
DB_PORT=5432

#Mysql
#DB_HOST=127.0.0.1
#DB_DRIVER=mysql
#DB_USER=steven
#DB_PASSWORD=here
#DB_NAME=food-app
#DB_PORT=3306


#Postgres Test DB
TEST_DB_DRIVER=postgres
TEST_DB_HOST=127.0.0.1
TEST_DB_PASSWORD=password
TEST_DB_USER=steven
TEST_DB_NAME=food-app-test
TEST_DB_PORT=5432

#Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=

该文件应位于根目录中(路径:food-app /)。

Domain 层

我们将首先考虑领域。

该域具有几种模式。其中一些是:实体,值,存储库,服务等。

由于我们在此处构建的应用比较简单,因此我们仅考虑两种域模式:实体和存储库。

实体

这是我们定义“Schema”的地方。

例如,我们可以定义用户的结构。将该实体视为域的蓝图。
package entity

import (
"food-app/infrastructure/security"
"github.com/badoux/checkmail"
"html"
"strings"
"time"
)

type User struct {
ID        uint64     `gorm:"primary_key;auto_increment" json:"id"`
FirstName string     `gorm:"size:100;not null;" json:"first_name"`
LastName  string     `gorm:"size:100;not null;" json:"last_name"`
Email     string     `gorm:"size:100;not null;unique" json:"email"`
Password  string     `gorm:"size:100;not null;" json:"password"`
CreatedAt time.Time  `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time  `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
}

type PublicUser struct {
ID        uint64 `gorm:"primary_key;auto_increment" json:"id"`
FirstName string `gorm:"size:100;not null;" json:"first_name"`
LastName  string `gorm:"size:100;not null;" json:"last_name"`
}

//BeforeSave is a gorm hook
func (u *User) BeforeSave() error {
hashPassword, err := security.Hash(u.Password)
if err != nil {
    return err
}
u.Password = string(hashPassword)
return nil
}

type Users []User

//So that we dont expose the user's email address and password to the world
func (users Users) PublicUsers() []interface{} {
result := make([]interface{}, len(users))
for index, user := range users {
    result[index] = user.PublicUser()
}
return result
}

//So that we dont expose the user's email address and password to the world
func (u *User) PublicUser() interface{} {
return &PublicUser{
    ID:        u.ID,
    FirstName: u.FirstName,
    LastName:  u.LastName,
}
}

func (u *User) Prepare() {
u.FirstName = html.EscapeString(strings.TrimSpace(u.FirstName))
u.LastName = html.EscapeString(strings.TrimSpace(u.LastName))
u.Email = html.EscapeString(strings.TrimSpace(u.Email))
u.CreatedAt = time.Now()
u.UpdatedAt = time.Now()
}

func (u *User) Validate(action string) map[string]string {
var errorMessages = make(map[string]string)
var err error

switch strings.ToLower(action) {
case "update":
    if u.Email == "" {
        errorMessages["email_required"] = "email required"
    }
    if u.Email != "" {
        if err = checkmail.ValidateFormat(u.Email); err != nil {
            errorMessages["invalid_email"] = "email email"
        }
    }

case "login":
    if u.Password == "" {
        errorMessages["password_required"] = "password is required"
    }
    if u.Email == "" {
        errorMessages["email_required"] = "email is required"
    }
    if u.Email != "" {
        if err = checkmail.ValidateFormat(u.Email); err != nil {
            errorMessages["invalid_email"] = "please provide a valid email"
        }
    }
case "forgotpassword":
    if u.Email == "" {
        errorMessages["email_required"] = "email required"
    }
    if u.Email != "" {
        if err = checkmail.ValidateFormat(u.Email); err != nil {
            errorMessages["invalid_email"] = "please provide a valid email"
        }
    }
default:
    if u.FirstName == "" {
        errorMessages["firstname_required"] = "first name is required"
    }
    if u.LastName == "" {
        errorMessages["lastname_required"] = "last name is required"
    }
    if u.Password == "" {
        errorMessages["password_required"] = "password is required"
    }
    if u.Password != "" && len(u.Password) < 6 {
        errorMessages["invalid_password"] = "password should be at least 6 characters"
    }
    if u.Email == "" {
        errorMessages["email_required"] = "email is required"
    }
    if u.Email != "" {
        if err = checkmail.ValidateFormat(u.Email); err != nil {
            errorMessages["invalid_email"] = "please provide a valid email"
        }
    }
}
return errorMessages


在上面的文件中,定义了包含用户信息的用户结构,我们还添加了帮助程序功能,这些功能将验证和清理输入。调用了一种哈希方法,该方法用于哈希密码。这是在基础结构层中定义的。

定义food实体时采用相同的方法。

存储库

存储库定义了基础结构实现的方法的集合。这描绘了与给定数据库或第三方API交互的方法数量。

user存储库如下所示:
package repository

import (
"food-app/domain/entity"
)

type UserRepository interface {
SaveUser(*entity.User) (*entity.User, map[string]string)
GetUser(uint64) (*entity.User, error)
GetUsers() ([]entity.User, error)
GetUserByEmailAndPassword(*entity.User) (*entity.User, map[string]string)


方法在接口中定义。这些方法稍后将在基础结构层中实现。

food库几乎相同。

Infrastructure层

该层实现存储库中定义的方法。这些方法与数据库或第三方API交互。本文中仅考虑数据库交互。
3.png

我们可以看到user存储库实现如下所示:
package persistence

import (
"errors"
"food-app/domain/entity"
"food-app/domain/repository"
"food-app/infrastructure/security"
"github.com/jinzhu/gorm"
"golang.org/x/crypto/bcrypt"
"strings"
)

type UserRepo struct {
db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepo {
return &UserRepo{db}
}
//UserRepo implements the repository.UserRepository interface
var _ repository.UserRepository = &UserRepo{}

func (r *UserRepo) SaveUser(user *entity.User) (*entity.User, map[string]string) {
dbErr := map[string]string{}
err := r.db.Debug().Create(&user).Error
if err != nil {
    //If the email is already taken
    if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "Duplicate") {
        dbErr["email_taken"] = "email already taken"
        return nil, dbErr
    }
    //any other db error
    dbErr["db_error"] = "database error"
    return nil, dbErr
}
return user, nil
}

func (r *UserRepo) GetUser(id uint64) (*entity.User, error) {
var user entity.User
err := r.db.Debug().Where("id = ?", id).Take(&user).Error
if err != nil {
    return nil, err
}
if gorm.IsRecordNotFoundError(err) {
    return nil, errors.New("user not found")
}
return &user, nil
}

func (r *UserRepo) GetUsers() ([]entity.User, error) {
var users []entity.User
err := r.db.Debug().Find(&users).Error
if err != nil {
    return nil, err
}
if gorm.IsRecordNotFoundError(err) {
    return nil, errors.New("user not found")
}
return users, nil
}

func (r *UserRepo) GetUserByEmailAndPassword(u *entity.User) (*entity.User, map[string]string) {
var user entity.User
dbErr := map[string]string{}
err := r.db.Debug().Where("email = ?", u.Email).Take(&user).Error
if gorm.IsRecordNotFoundError(err) {
    dbErr["no_user"] = "user not found"
    return nil, dbErr
}
if err != nil {
    dbErr["db_error"] = "database error"
    return nil, dbErr
}
//Verify the password
err = security.VerifyPassword(user.Password, u.Password)
if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
    dbErr["incorrect_password"] = "incorrect password"
    return nil, dbErr
}
return &user, nil


可以看到我们实现了存储库中定义的方法。使用实现了UserRepository接口的UserRepo结构可以做到这一点,如下行所示:
//UserRepo implements the repository.UserRepository interface
var _ repository.UserRepository = &UserRepo{} 

因此,我们通过创建包含以下内容的db.go文件来配置数据库:
package persistence

import (
"fmt"
"food-app/domain/entity"
"food-app/domain/repository"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
)

type Repositories struct {
User repository.UserRepository
Food repository.FoodRepository
db   *gorm.DB
}

func NewRepositories(Dbdriver, DbUser, DbPassword, DbPort, DbHost, DbName string) (*Repositories, error) {
DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", DbHost, DbPort, DbUser, DbName, DbPassword)
db, err := gorm.Open(Dbdriver, DBURL)
if err != nil {
    return nil, err
}
db.LogMode(true)

return &Repositories{
    User: NewUserRepository(db),
    Food: NewFoodRepository(db),
    db:   db,
}, nil
}

//closes the  database connection
func (s *Repositories) Close() error {
return s.db.Close()
}

//This migrate all tables
func (s *Repositories) Automigrate() error {
return s.db.AutoMigrate(&entity.User{}, &entity.Food{}).Error


在上面的文件中,我们定义了Repositories结构,该结构保存了应用中的所有存储库。我们有user和food库。该存储库还具有一个db实例,该实例被传递给user和food(即NewUserRepository和NewFoodRepository)的“constructors”。

Application层

我们已经在域中定义了API业务逻辑。该层连接domain和interfaces层。

以下是user的应用层:
package application

import (
"food-app/domain/entity"
"food-app/domain/repository"
)

type userApp struct {
us repository.UserRepository
}

//UserApp implements the UserAppInterface
var _ UserAppInterface = &userApp{}

type UserAppInterface interface {
SaveUser(*entity.User) (*entity.User, map[string]string)
GetUsers() ([]entity.User, error)
GetUser(uint64) (*entity.User, error)
GetUserByEmailAndPassword(*entity.User) (*entity.User, map[string]string)
}

func (u *userApp) SaveUser(user *entity.User) (*entity.User, map[string]string) {
return u.us.SaveUser(user)
}

func (u *userApp) GetUser(userId uint64) (*entity.User, error) {
return u.us.GetUser(userId)
}

func (u *userApp) GetUsers() ([]entity.User, error) {
return u.us.GetUsers()
}

func (u *userApp) GetUserByEmailAndPassword(user *entity.User) (*entity.User, map[string]string) {
return u.us.GetUserByEmailAndPassword(user)


上面有保存和检索用户数据的方法。UserApp结构具有UserRepository接口,从而可以调用用户存储库方法。

Interfaces层

接口是处理HTTP请求和响应的层。这里我们收到身份验证,与用户相关的内容和与食品相关的内容的传入请求。
4.png

用户处理

我们定义了保存用户,获取所有用户和获取特定用户的方法。这些可以在user_handler.go文件中找到。
package interfaces

import (
"food-app/application"
"food-app/domain/entity"
"food-app/infrastructure/auth"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)

//Users struct defines the dependencies that will be used
type Users struct {
us application.UserAppInterface
rd auth.AuthInterface
tk auth.TokenInterface
}

//Users constructor
func NewUsers(us application.UserAppInterface, rd auth.AuthInterface, tk auth.TokenInterface) *Users {
return &Users{
    us: us,
    rd: rd,
    tk: tk,
}
}

func (s *Users) SaveUser(c *gin.Context) {
var user entity.User
if err := c.ShouldBindJSON(&user); err != nil {
    c.JSON(http.StatusUnprocessableEntity, gin.H{
        "invalid_json": "invalid json",
    })
    return
}
//validate the request:
validateErr := user.Validate("")
if len(validateErr) > 0 {
    c.JSON(http.StatusUnprocessableEntity, validateErr)
    return
}
newUser, err := s.us.SaveUser(&user)
if err != nil {
    c.JSON(http.StatusInternalServerError, err)
    return
}
c.JSON(http.StatusCreated, newUser.PublicUser())
}

func (s *Users) GetUsers(c *gin.Context) {
users := entity.Users{} //customize user
var err error
//us, err = application.UserApp.GetUsers()
users, err = s.us.GetUsers()
if err != nil {
    c.JSON(http.StatusInternalServerError, err.Error())
    return
}
c.JSON(http.StatusOK, users.PublicUsers())
}

func (s *Users) GetUser(c *gin.Context) {
userId, err := strconv.ParseUint(c.Param("user_id"), 10, 64)
if err != nil {
    c.JSON(http.StatusBadRequest, err.Error())
    return
}
user, err := s.us.GetUser(userId)
if err != nil {
    c.JSON(http.StatusInternalServerError, err.Error())
    return
}
c.JSON(http.StatusOK, user.PublicUser())


观察到返回用户时,我们仅返回一个公共用户(在实体中定义)。公共用户没有敏感的用户详细信息,例如电子邮件和密码。

授权处理

login_handler负责登录,注销和刷新令牌方法。在各自文件中定义的某些方法在此文件中被调用。最好在它们的文件路径之后在存储库中检出它们。
package interfaces

import (
"fmt"
"food-app/application"
"food-app/domain/entity"
"food-app/infrastructure/auth"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"net/http"
"os"
"strconv"
)

type Authenticate struct {
us application.UserAppInterface
rd auth.AuthInterface
tk auth.TokenInterface
}

//Authenticate constructor
func NewAuthenticate(uApp application.UserAppInterface, rd auth.AuthInterface, tk auth.TokenInterface) *Authenticate {
return &Authenticate{
    us: uApp,
    rd: rd,
    tk: tk,
}
}

func (au *Authenticate) Login(c *gin.Context) {
var user *entity.User
var tokenErr = map[string]string{}

if err := c.ShouldBindJSON(&user); err != nil {
    c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
    return
}
//validate request:
validateUser := user.Validate("login")
if len(validateUser) > 0 {
    c.JSON(http.StatusUnprocessableEntity, validateUser)
    return
}
u, userErr := au.us.GetUserByEmailAndPassword(user)
if userErr != nil {
    c.JSON(http.StatusInternalServerError, userErr)
    return
}
ts, tErr := au.tk.CreateToken(u.ID)
if tErr != nil {
    tokenErr["token_error"] = tErr.Error()
    c.JSON(http.StatusUnprocessableEntity, tErr.Error())
    return
}
saveErr := au.rd.CreateAuth(u.ID, ts)
if saveErr != nil {
    c.JSON(http.StatusInternalServerError, saveErr.Error())
    return
}
userData := make(map[string]interface{})
userData["access_token"] = ts.AccessToken
userData["refresh_token"] = ts.RefreshToken
userData["id"] = u.ID
userData["first_name"] = u.FirstName
userData["last_name"] = u.LastName

c.JSON(http.StatusOK, userData)
}

func (au *Authenticate) Logout(c *gin.Context) {
//check is the user is authenticated first
metadata, err := au.tk.ExtractTokenMetadata(c.Request)
if err != nil {
    c.JSON(http.StatusUnauthorized, "Unauthorized")
    return
}
//if the access token exist and it is still valid, then delete both the access token and the refresh token
deleteErr := au.rd.DeleteTokens(metadata)
if deleteErr != nil {
    c.JSON(http.StatusUnauthorized, deleteErr.Error())
    return
}
c.JSON(http.StatusOK, "Successfully logged out")
}

//Refresh is the function that uses the refresh_token to generate new pairs of refresh and access tokens.
func (au *Authenticate) Refresh(c *gin.Context) {
mapToken := map[string]string{}
if err := c.ShouldBindJSON(&mapToken); err != nil {
    c.JSON(http.StatusUnprocessableEntity, err.Error())
    return
}
refreshToken := mapToken["refresh_token"]

//verify the token
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
    //Make sure that the token method conform to "SigningMethodHMAC"
    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
    }
    return []byte(os.Getenv("REFRESH_SECRET")), nil
})
//any error may be due to token expiration
if err != nil {
    c.JSON(http.StatusUnauthorized, err.Error())
    return
}
//is token valid?
if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
    c.JSON(http.StatusUnauthorized, err)
    return
}
//Since token is valid, get the uuid:
claims, ok := token.Claims.(jwt.MapClaims)
if ok && token.Valid {
    refreshUuid, ok := claims["refresh_uuid"].(string) //convert the interface to string
    if !ok {
        c.JSON(http.StatusUnprocessableEntity, "Cannot get uuid")
        return
    }
    userId, err := strconv.ParseUint(fmt.Sprintf("%.f", claims["user_id"]), 10, 64)
    if err != nil {
        c.JSON(http.StatusUnprocessableEntity, "Error occurred")
        return
    }
    //Delete the previous Refresh Token
    delErr := au.rd.DeleteRefresh(refreshUuid)
    if delErr != nil { //if any goes wrong
        c.JSON(http.StatusUnauthorized, "unauthorized")
        return
    }
    //Create new pairs of refresh and access tokens
    ts, createErr := au.tk.CreateToken(userId)
    if createErr != nil {
        c.JSON(http.StatusForbidden, createErr.Error())
        return
    }
    //save the tokens metadata to redis
    saveErr := au.rd.CreateAuth(userId, ts)
    if saveErr != nil {
        c.JSON(http.StatusForbidden, saveErr.Error())
        return
    }
    tokens := map[string]string{
        "access_token":  ts.AccessToken,
        "refresh_token": ts.RefreshToken,
    }
    c.JSON(http.StatusCreated, tokens)
} else {
    c.JSON(http.StatusUnauthorized, "refresh token expired")
}


运行程序

我们测试一下该应用。我们将连接路由,连接到数据库并启动应用程序。

在根目录中定义的main.go文件中完成。
package main

import (
"food-app/infrastructure/auth"
"food-app/infrastructure/persistence"
"food-app/interfaces"
"food-app/interfaces/fileupload"
"food-app/interfaces/middleware"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"log"
"os"
)

func init() {
//To load our environmental variables.
if err := godotenv.Load(); err != nil {
    log.Println("no env gotten")
}
}

func main() {

dbdriver := os.Getenv("DB_DRIVER")
host := os.Getenv("DB_HOST")
password := os.Getenv("DB_PASSWORD")
user := os.Getenv("DB_USER")
dbname := os.Getenv("DB_NAME")
port := os.Getenv("DB_PORT")

//redis details
redis_host := os.Getenv("REDIS_HOST")
redis_port := os.Getenv("REDIS_PORT")
redis_password := os.Getenv("REDIS_PASSWORD")


services, err := persistence.NewRepositories(dbdriver, user, password, port, host, dbname)
if err != nil {
    panic(err)
}
defer services.Close()
services.Automigrate()

redisService, err := auth.NewRedisDB(redis_host, redis_port, redis_password)
if err != nil {
    log.Fatal(err)
}

tk := auth.NewToken()
fd := fileupload.NewFileUpload()

users := interfaces.NewUsers(services.User, redisService.Auth, tk)
foods := interfaces.NewFood(services.Food, services.User, fd, redisService.Auth, tk)
authenticate := interfaces.NewAuthenticate(services.User, redisService.Auth, tk)

r := gin.Default()
r.Use(middleware.CORSMiddleware()) //For CORS

//user routes
r.POST("/users", users.SaveUser)
r.GET("/users", users.GetUsers)
r.GET("/users/:user_id", users.GetUser)

//post routes
r.POST("/food", middleware.AuthMiddleware(), middleware.MaxSizeAllowed(8192000), foods.SaveFood)
r.PUT("/food/:food_id", middleware.AuthMiddleware(), middleware.MaxSizeAllowed(8192000), foods.UpdateFood)
r.GET("/food/:food_id", foods.GetFoodAndCreator)
r.DELETE("/food/:food_id", middleware.AuthMiddleware(), foods.DeleteFood)
r.GET("/food", foods.GetAllFood)

//authentication routes
r.POST("/login", authenticate.Login)
r.POST("/logout", authenticate.Logout)
r.POST("/refresh", authenticate.Refresh)


//Starting the application
app_port := os.Getenv("PORT") //using heroku host
if app_port == "" {
    app_port = "8888" //localhost
}
log.Fatal(r.Run(":"+app_port))


其中的中间件也是定义在interfaces层。
package middleware

import (
"bytes"
"food-app/infrastructure/auth"
"github.com/gin-gonic/gin"
"io/ioutil"
"net/http"
)

func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
    err := auth.TokenValid(c.Request)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{
            "status": http.StatusUnauthorized,
            "error":  err.Error(),
        })
        c.Abort()
        return
    }
    c.Next()
}
}

func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
    c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
    c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
    c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
    c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE")

    if c.Request.Method == "OPTIONS" {
        c.AbortWithStatus(204)
        return
    }
    c.Next()
}
}

//Avoid a large file from loading into memory
//If the file size is greater than 8MB dont allow it to even load into memory and waste our time.
func MaxSizeAllowed(n int64) gin.HandlerFunc {
return func(c *gin.Context) {
    c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, n)
    buff, errRead := c.GetRawData()
    if errRead != nil {
        //c.JSON(http.StatusRequestEntityTooLarge,"too large")
        c.JSON(http.StatusRequestEntityTooLarge, gin.H{
            "status":     http.StatusRequestEntityTooLarge,
            "upload_err": "too large: upload an image less than 8MB",
        })
        c.Abort()
        return
    }
    buf := bytes.NewBuffer(buff)
    c.Request.Body = ioutil.NopCloser(buf)
}


我们现在可以使用以下命令运行该应用:
go run main.go

总结

希望通过构建该golang应用,帮助大家了解如何使用DDD。如果有什么疑问或意见,可以在下方留言。

原文链接:https://mp.weixin.qq.com/s/dVK64_v60qEUAX7kQnfSxw
继续阅读 »

本篇文章分享基于DDD的Golang实现,DDD即领域驱动设计,该模式也算是比较热门的话题了。希望通过本篇文章,大家能够掌握DDD模式,能对大家有所帮助。

领域驱动设计(DDD)是一种软件开发方法,通过将实现与不断演变的模型相连接,简化了开发人员面临的复杂性。

本文不会重点去解释Golang中实现DDD的相关理念,而是作者根据自己的研究对DDD的理解。

什么是DDD?

以下是考虑使用DDD的原因:
  • 提供解决困难问题的原则和模式
  • 将复杂的设计基于领域模型
  • 在技术和领域专家之间发起创造性的协作,以迭代地完善解决领域问题的概念模型。


DDD包含4个层:
  • Domain:这是定义应用程序的域和业务逻辑的地方
  • Infrastructure:此层包含独立于我们的应用程序而存在的所有内容:外部库,数据库引擎等。
  • Application:该层用作域和界面层之间的通道。将请求从接口层发送到域层,由域层处理请求并返回响应。
  • Interface:该层包含与其他系统交互的所有内容,例如Web服务,RMI接口或Web应用程序以及批处理前端。


1.png

开始

我们将构建一个食物推荐API。

首先要做的是初始化依赖关系管理。我们将使用go.mod。在根目录(路径:food-app /)中,初始化go.mod:
go mod init food-app

项目的组织结构:
2.png

在该应用中,我们将使用postgres和redis数据库持久化数据。先定义一个含有连接信息的.env文件。

.env文件内容:
#Postgres
APP_ENV=local
API_PORT=8888
DB_HOST=127.0.0.1
DB_DRIVER=postgres
ACCESS_SECRET=98hbun98h
REFRESH_SECRET=786dfdbjhsb
DB_USER=steven
DB_PASSWORD=password
DB_NAME=food-app
DB_PORT=5432

#Mysql
#DB_HOST=127.0.0.1
#DB_DRIVER=mysql
#DB_USER=steven
#DB_PASSWORD=here
#DB_NAME=food-app
#DB_PORT=3306


#Postgres Test DB
TEST_DB_DRIVER=postgres
TEST_DB_HOST=127.0.0.1
TEST_DB_PASSWORD=password
TEST_DB_USER=steven
TEST_DB_NAME=food-app-test
TEST_DB_PORT=5432

#Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=

该文件应位于根目录中(路径:food-app /)。

Domain 层

我们将首先考虑领域。

该域具有几种模式。其中一些是:实体,值,存储库,服务等。

由于我们在此处构建的应用比较简单,因此我们仅考虑两种域模式:实体和存储库。

实体

这是我们定义“Schema”的地方。

例如,我们可以定义用户的结构。将该实体视为域的蓝图。
package entity

import (
"food-app/infrastructure/security"
"github.com/badoux/checkmail"
"html"
"strings"
"time"
)

type User struct {
ID        uint64     `gorm:"primary_key;auto_increment" json:"id"`
FirstName string     `gorm:"size:100;not null;" json:"first_name"`
LastName  string     `gorm:"size:100;not null;" json:"last_name"`
Email     string     `gorm:"size:100;not null;unique" json:"email"`
Password  string     `gorm:"size:100;not null;" json:"password"`
CreatedAt time.Time  `gorm:"default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time  `gorm:"default:CURRENT_TIMESTAMP" json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
}

type PublicUser struct {
ID        uint64 `gorm:"primary_key;auto_increment" json:"id"`
FirstName string `gorm:"size:100;not null;" json:"first_name"`
LastName  string `gorm:"size:100;not null;" json:"last_name"`
}

//BeforeSave is a gorm hook
func (u *User) BeforeSave() error {
hashPassword, err := security.Hash(u.Password)
if err != nil {
    return err
}
u.Password = string(hashPassword)
return nil
}

type Users []User

//So that we dont expose the user's email address and password to the world
func (users Users) PublicUsers() []interface{} {
result := make([]interface{}, len(users))
for index, user := range users {
    result[index] = user.PublicUser()
}
return result
}

//So that we dont expose the user's email address and password to the world
func (u *User) PublicUser() interface{} {
return &PublicUser{
    ID:        u.ID,
    FirstName: u.FirstName,
    LastName:  u.LastName,
}
}

func (u *User) Prepare() {
u.FirstName = html.EscapeString(strings.TrimSpace(u.FirstName))
u.LastName = html.EscapeString(strings.TrimSpace(u.LastName))
u.Email = html.EscapeString(strings.TrimSpace(u.Email))
u.CreatedAt = time.Now()
u.UpdatedAt = time.Now()
}

func (u *User) Validate(action string) map[string]string {
var errorMessages = make(map[string]string)
var err error

switch strings.ToLower(action) {
case "update":
    if u.Email == "" {
        errorMessages["email_required"] = "email required"
    }
    if u.Email != "" {
        if err = checkmail.ValidateFormat(u.Email); err != nil {
            errorMessages["invalid_email"] = "email email"
        }
    }

case "login":
    if u.Password == "" {
        errorMessages["password_required"] = "password is required"
    }
    if u.Email == "" {
        errorMessages["email_required"] = "email is required"
    }
    if u.Email != "" {
        if err = checkmail.ValidateFormat(u.Email); err != nil {
            errorMessages["invalid_email"] = "please provide a valid email"
        }
    }
case "forgotpassword":
    if u.Email == "" {
        errorMessages["email_required"] = "email required"
    }
    if u.Email != "" {
        if err = checkmail.ValidateFormat(u.Email); err != nil {
            errorMessages["invalid_email"] = "please provide a valid email"
        }
    }
default:
    if u.FirstName == "" {
        errorMessages["firstname_required"] = "first name is required"
    }
    if u.LastName == "" {
        errorMessages["lastname_required"] = "last name is required"
    }
    if u.Password == "" {
        errorMessages["password_required"] = "password is required"
    }
    if u.Password != "" && len(u.Password) < 6 {
        errorMessages["invalid_password"] = "password should be at least 6 characters"
    }
    if u.Email == "" {
        errorMessages["email_required"] = "email is required"
    }
    if u.Email != "" {
        if err = checkmail.ValidateFormat(u.Email); err != nil {
            errorMessages["invalid_email"] = "please provide a valid email"
        }
    }
}
return errorMessages


在上面的文件中,定义了包含用户信息的用户结构,我们还添加了帮助程序功能,这些功能将验证和清理输入。调用了一种哈希方法,该方法用于哈希密码。这是在基础结构层中定义的。

定义food实体时采用相同的方法。

存储库

存储库定义了基础结构实现的方法的集合。这描绘了与给定数据库或第三方API交互的方法数量。

user存储库如下所示:
package repository

import (
"food-app/domain/entity"
)

type UserRepository interface {
SaveUser(*entity.User) (*entity.User, map[string]string)
GetUser(uint64) (*entity.User, error)
GetUsers() ([]entity.User, error)
GetUserByEmailAndPassword(*entity.User) (*entity.User, map[string]string)


方法在接口中定义。这些方法稍后将在基础结构层中实现。

food库几乎相同。

Infrastructure层

该层实现存储库中定义的方法。这些方法与数据库或第三方API交互。本文中仅考虑数据库交互。
3.png

我们可以看到user存储库实现如下所示:
package persistence

import (
"errors"
"food-app/domain/entity"
"food-app/domain/repository"
"food-app/infrastructure/security"
"github.com/jinzhu/gorm"
"golang.org/x/crypto/bcrypt"
"strings"
)

type UserRepo struct {
db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepo {
return &UserRepo{db}
}
//UserRepo implements the repository.UserRepository interface
var _ repository.UserRepository = &UserRepo{}

func (r *UserRepo) SaveUser(user *entity.User) (*entity.User, map[string]string) {
dbErr := map[string]string{}
err := r.db.Debug().Create(&user).Error
if err != nil {
    //If the email is already taken
    if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "Duplicate") {
        dbErr["email_taken"] = "email already taken"
        return nil, dbErr
    }
    //any other db error
    dbErr["db_error"] = "database error"
    return nil, dbErr
}
return user, nil
}

func (r *UserRepo) GetUser(id uint64) (*entity.User, error) {
var user entity.User
err := r.db.Debug().Where("id = ?", id).Take(&user).Error
if err != nil {
    return nil, err
}
if gorm.IsRecordNotFoundError(err) {
    return nil, errors.New("user not found")
}
return &user, nil
}

func (r *UserRepo) GetUsers() ([]entity.User, error) {
var users []entity.User
err := r.db.Debug().Find(&users).Error
if err != nil {
    return nil, err
}
if gorm.IsRecordNotFoundError(err) {
    return nil, errors.New("user not found")
}
return users, nil
}

func (r *UserRepo) GetUserByEmailAndPassword(u *entity.User) (*entity.User, map[string]string) {
var user entity.User
dbErr := map[string]string{}
err := r.db.Debug().Where("email = ?", u.Email).Take(&user).Error
if gorm.IsRecordNotFoundError(err) {
    dbErr["no_user"] = "user not found"
    return nil, dbErr
}
if err != nil {
    dbErr["db_error"] = "database error"
    return nil, dbErr
}
//Verify the password
err = security.VerifyPassword(user.Password, u.Password)
if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
    dbErr["incorrect_password"] = "incorrect password"
    return nil, dbErr
}
return &user, nil


可以看到我们实现了存储库中定义的方法。使用实现了UserRepository接口的UserRepo结构可以做到这一点,如下行所示:
//UserRepo implements the repository.UserRepository interface
var _ repository.UserRepository = &UserRepo{} 

因此,我们通过创建包含以下内容的db.go文件来配置数据库:
package persistence

import (
"fmt"
"food-app/domain/entity"
"food-app/domain/repository"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/postgres"
)

type Repositories struct {
User repository.UserRepository
Food repository.FoodRepository
db   *gorm.DB
}

func NewRepositories(Dbdriver, DbUser, DbPassword, DbPort, DbHost, DbName string) (*Repositories, error) {
DBURL := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable password=%s", DbHost, DbPort, DbUser, DbName, DbPassword)
db, err := gorm.Open(Dbdriver, DBURL)
if err != nil {
    return nil, err
}
db.LogMode(true)

return &Repositories{
    User: NewUserRepository(db),
    Food: NewFoodRepository(db),
    db:   db,
}, nil
}

//closes the  database connection
func (s *Repositories) Close() error {
return s.db.Close()
}

//This migrate all tables
func (s *Repositories) Automigrate() error {
return s.db.AutoMigrate(&entity.User{}, &entity.Food{}).Error


在上面的文件中,我们定义了Repositories结构,该结构保存了应用中的所有存储库。我们有user和food库。该存储库还具有一个db实例,该实例被传递给user和food(即NewUserRepository和NewFoodRepository)的“constructors”。

Application层

我们已经在域中定义了API业务逻辑。该层连接domain和interfaces层。

以下是user的应用层:
package application

import (
"food-app/domain/entity"
"food-app/domain/repository"
)

type userApp struct {
us repository.UserRepository
}

//UserApp implements the UserAppInterface
var _ UserAppInterface = &userApp{}

type UserAppInterface interface {
SaveUser(*entity.User) (*entity.User, map[string]string)
GetUsers() ([]entity.User, error)
GetUser(uint64) (*entity.User, error)
GetUserByEmailAndPassword(*entity.User) (*entity.User, map[string]string)
}

func (u *userApp) SaveUser(user *entity.User) (*entity.User, map[string]string) {
return u.us.SaveUser(user)
}

func (u *userApp) GetUser(userId uint64) (*entity.User, error) {
return u.us.GetUser(userId)
}

func (u *userApp) GetUsers() ([]entity.User, error) {
return u.us.GetUsers()
}

func (u *userApp) GetUserByEmailAndPassword(user *entity.User) (*entity.User, map[string]string) {
return u.us.GetUserByEmailAndPassword(user)


上面有保存和检索用户数据的方法。UserApp结构具有UserRepository接口,从而可以调用用户存储库方法。

Interfaces层

接口是处理HTTP请求和响应的层。这里我们收到身份验证,与用户相关的内容和与食品相关的内容的传入请求。
4.png

用户处理

我们定义了保存用户,获取所有用户和获取特定用户的方法。这些可以在user_handler.go文件中找到。
package interfaces

import (
"food-app/application"
"food-app/domain/entity"
"food-app/infrastructure/auth"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)

//Users struct defines the dependencies that will be used
type Users struct {
us application.UserAppInterface
rd auth.AuthInterface
tk auth.TokenInterface
}

//Users constructor
func NewUsers(us application.UserAppInterface, rd auth.AuthInterface, tk auth.TokenInterface) *Users {
return &Users{
    us: us,
    rd: rd,
    tk: tk,
}
}

func (s *Users) SaveUser(c *gin.Context) {
var user entity.User
if err := c.ShouldBindJSON(&user); err != nil {
    c.JSON(http.StatusUnprocessableEntity, gin.H{
        "invalid_json": "invalid json",
    })
    return
}
//validate the request:
validateErr := user.Validate("")
if len(validateErr) > 0 {
    c.JSON(http.StatusUnprocessableEntity, validateErr)
    return
}
newUser, err := s.us.SaveUser(&user)
if err != nil {
    c.JSON(http.StatusInternalServerError, err)
    return
}
c.JSON(http.StatusCreated, newUser.PublicUser())
}

func (s *Users) GetUsers(c *gin.Context) {
users := entity.Users{} //customize user
var err error
//us, err = application.UserApp.GetUsers()
users, err = s.us.GetUsers()
if err != nil {
    c.JSON(http.StatusInternalServerError, err.Error())
    return
}
c.JSON(http.StatusOK, users.PublicUsers())
}

func (s *Users) GetUser(c *gin.Context) {
userId, err := strconv.ParseUint(c.Param("user_id"), 10, 64)
if err != nil {
    c.JSON(http.StatusBadRequest, err.Error())
    return
}
user, err := s.us.GetUser(userId)
if err != nil {
    c.JSON(http.StatusInternalServerError, err.Error())
    return
}
c.JSON(http.StatusOK, user.PublicUser())


观察到返回用户时,我们仅返回一个公共用户(在实体中定义)。公共用户没有敏感的用户详细信息,例如电子邮件和密码。

授权处理

login_handler负责登录,注销和刷新令牌方法。在各自文件中定义的某些方法在此文件中被调用。最好在它们的文件路径之后在存储库中检出它们。
package interfaces

import (
"fmt"
"food-app/application"
"food-app/domain/entity"
"food-app/infrastructure/auth"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"net/http"
"os"
"strconv"
)

type Authenticate struct {
us application.UserAppInterface
rd auth.AuthInterface
tk auth.TokenInterface
}

//Authenticate constructor
func NewAuthenticate(uApp application.UserAppInterface, rd auth.AuthInterface, tk auth.TokenInterface) *Authenticate {
return &Authenticate{
    us: uApp,
    rd: rd,
    tk: tk,
}
}

func (au *Authenticate) Login(c *gin.Context) {
var user *entity.User
var tokenErr = map[string]string{}

if err := c.ShouldBindJSON(&user); err != nil {
    c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
    return
}
//validate request:
validateUser := user.Validate("login")
if len(validateUser) > 0 {
    c.JSON(http.StatusUnprocessableEntity, validateUser)
    return
}
u, userErr := au.us.GetUserByEmailAndPassword(user)
if userErr != nil {
    c.JSON(http.StatusInternalServerError, userErr)
    return
}
ts, tErr := au.tk.CreateToken(u.ID)
if tErr != nil {
    tokenErr["token_error"] = tErr.Error()
    c.JSON(http.StatusUnprocessableEntity, tErr.Error())
    return
}
saveErr := au.rd.CreateAuth(u.ID, ts)
if saveErr != nil {
    c.JSON(http.StatusInternalServerError, saveErr.Error())
    return
}
userData := make(map[string]interface{})
userData["access_token"] = ts.AccessToken
userData["refresh_token"] = ts.RefreshToken
userData["id"] = u.ID
userData["first_name"] = u.FirstName
userData["last_name"] = u.LastName

c.JSON(http.StatusOK, userData)
}

func (au *Authenticate) Logout(c *gin.Context) {
//check is the user is authenticated first
metadata, err := au.tk.ExtractTokenMetadata(c.Request)
if err != nil {
    c.JSON(http.StatusUnauthorized, "Unauthorized")
    return
}
//if the access token exist and it is still valid, then delete both the access token and the refresh token
deleteErr := au.rd.DeleteTokens(metadata)
if deleteErr != nil {
    c.JSON(http.StatusUnauthorized, deleteErr.Error())
    return
}
c.JSON(http.StatusOK, "Successfully logged out")
}

//Refresh is the function that uses the refresh_token to generate new pairs of refresh and access tokens.
func (au *Authenticate) Refresh(c *gin.Context) {
mapToken := map[string]string{}
if err := c.ShouldBindJSON(&mapToken); err != nil {
    c.JSON(http.StatusUnprocessableEntity, err.Error())
    return
}
refreshToken := mapToken["refresh_token"]

//verify the token
token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) {
    //Make sure that the token method conform to "SigningMethodHMAC"
    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
    }
    return []byte(os.Getenv("REFRESH_SECRET")), nil
})
//any error may be due to token expiration
if err != nil {
    c.JSON(http.StatusUnauthorized, err.Error())
    return
}
//is token valid?
if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
    c.JSON(http.StatusUnauthorized, err)
    return
}
//Since token is valid, get the uuid:
claims, ok := token.Claims.(jwt.MapClaims)
if ok && token.Valid {
    refreshUuid, ok := claims["refresh_uuid"].(string) //convert the interface to string
    if !ok {
        c.JSON(http.StatusUnprocessableEntity, "Cannot get uuid")
        return
    }
    userId, err := strconv.ParseUint(fmt.Sprintf("%.f", claims["user_id"]), 10, 64)
    if err != nil {
        c.JSON(http.StatusUnprocessableEntity, "Error occurred")
        return
    }
    //Delete the previous Refresh Token
    delErr := au.rd.DeleteRefresh(refreshUuid)
    if delErr != nil { //if any goes wrong
        c.JSON(http.StatusUnauthorized, "unauthorized")
        return
    }
    //Create new pairs of refresh and access tokens
    ts, createErr := au.tk.CreateToken(userId)
    if createErr != nil {
        c.JSON(http.StatusForbidden, createErr.Error())
        return
    }
    //save the tokens metadata to redis
    saveErr := au.rd.CreateAuth(userId, ts)
    if saveErr != nil {
        c.JSON(http.StatusForbidden, saveErr.Error())
        return
    }
    tokens := map[string]string{
        "access_token":  ts.AccessToken,
        "refresh_token": ts.RefreshToken,
    }
    c.JSON(http.StatusCreated, tokens)
} else {
    c.JSON(http.StatusUnauthorized, "refresh token expired")
}


运行程序

我们测试一下该应用。我们将连接路由,连接到数据库并启动应用程序。

在根目录中定义的main.go文件中完成。
package main

import (
"food-app/infrastructure/auth"
"food-app/infrastructure/persistence"
"food-app/interfaces"
"food-app/interfaces/fileupload"
"food-app/interfaces/middleware"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"log"
"os"
)

func init() {
//To load our environmental variables.
if err := godotenv.Load(); err != nil {
    log.Println("no env gotten")
}
}

func main() {

dbdriver := os.Getenv("DB_DRIVER")
host := os.Getenv("DB_HOST")
password := os.Getenv("DB_PASSWORD")
user := os.Getenv("DB_USER")
dbname := os.Getenv("DB_NAME")
port := os.Getenv("DB_PORT")

//redis details
redis_host := os.Getenv("REDIS_HOST")
redis_port := os.Getenv("REDIS_PORT")
redis_password := os.Getenv("REDIS_PASSWORD")


services, err := persistence.NewRepositories(dbdriver, user, password, port, host, dbname)
if err != nil {
    panic(err)
}
defer services.Close()
services.Automigrate()

redisService, err := auth.NewRedisDB(redis_host, redis_port, redis_password)
if err != nil {
    log.Fatal(err)
}

tk := auth.NewToken()
fd := fileupload.NewFileUpload()

users := interfaces.NewUsers(services.User, redisService.Auth, tk)
foods := interfaces.NewFood(services.Food, services.User, fd, redisService.Auth, tk)
authenticate := interfaces.NewAuthenticate(services.User, redisService.Auth, tk)

r := gin.Default()
r.Use(middleware.CORSMiddleware()) //For CORS

//user routes
r.POST("/users", users.SaveUser)
r.GET("/users", users.GetUsers)
r.GET("/users/:user_id", users.GetUser)

//post routes
r.POST("/food", middleware.AuthMiddleware(), middleware.MaxSizeAllowed(8192000), foods.SaveFood)
r.PUT("/food/:food_id", middleware.AuthMiddleware(), middleware.MaxSizeAllowed(8192000), foods.UpdateFood)
r.GET("/food/:food_id", foods.GetFoodAndCreator)
r.DELETE("/food/:food_id", middleware.AuthMiddleware(), foods.DeleteFood)
r.GET("/food", foods.GetAllFood)

//authentication routes
r.POST("/login", authenticate.Login)
r.POST("/logout", authenticate.Logout)
r.POST("/refresh", authenticate.Refresh)


//Starting the application
app_port := os.Getenv("PORT") //using heroku host
if app_port == "" {
    app_port = "8888" //localhost
}
log.Fatal(r.Run(":"+app_port))


其中的中间件也是定义在interfaces层。
package middleware

import (
"bytes"
"food-app/infrastructure/auth"
"github.com/gin-gonic/gin"
"io/ioutil"
"net/http"
)

func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
    err := auth.TokenValid(c.Request)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{
            "status": http.StatusUnauthorized,
            "error":  err.Error(),
        })
        c.Abort()
        return
    }
    c.Next()
}
}

func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
    c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
    c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
    c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
    c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, PATCH, DELETE")

    if c.Request.Method == "OPTIONS" {
        c.AbortWithStatus(204)
        return
    }
    c.Next()
}
}

//Avoid a large file from loading into memory
//If the file size is greater than 8MB dont allow it to even load into memory and waste our time.
func MaxSizeAllowed(n int64) gin.HandlerFunc {
return func(c *gin.Context) {
    c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, n)
    buff, errRead := c.GetRawData()
    if errRead != nil {
        //c.JSON(http.StatusRequestEntityTooLarge,"too large")
        c.JSON(http.StatusRequestEntityTooLarge, gin.H{
            "status":     http.StatusRequestEntityTooLarge,
            "upload_err": "too large: upload an image less than 8MB",
        })
        c.Abort()
        return
    }
    buf := bytes.NewBuffer(buff)
    c.Request.Body = ioutil.NopCloser(buf)
}


我们现在可以使用以下命令运行该应用:
go run main.go

总结

希望通过构建该golang应用,帮助大家了解如何使用DDD。如果有什么疑问或意见,可以在下方留言。

原文链接:https://mp.weixin.qq.com/s/dVK64_v60qEUAX7kQnfSxw 收起阅读 »

Docker入门看这一篇就够了


容器简介

什么是Linux容器?

Linux容器是与系统其他部分隔离开的一系列进程,从另一个镜像运行,并由该镜像提供支持进程所需的全部文件。容器提供的镜像包含了应用的所有依赖项,因而在从开发到测试再到生产的整个过程中,它都具有可移植性和一致性。
1.png

更加详细地来说,请您假定您在开发一个应用。您使用的是一台笔记本电脑,而且您的开发环境具有特定的配置。其他开发人员身处的环境配置可能稍有不同。您正在开发的应用依赖于您当前的配置,还要依赖于某些特定文件。与此同时,您的企业还拥有标准化的测试和生产环境,且具有自身的配置和一系列支持文件。您希望尽可能多在本地模拟这些环境,而不产生重新创建服务器环境的开销。

因此,您要如何确保应用能够在这些环境中运行和通过质量检测,并且在部署过程中不出现令人头疼的问题,也无需重新编写代码和进行故障修复?答案就是使用容器。容器可以确保您的应用拥有必需的配置和文件,使得这些应用能够在从开发到测试、再到生产的整个流程中顺利运行,而不出现任何不良问题。这样可以避免危机,做到皆大欢喜。

虽然这只是简化的示例,但在需要很高的可移植性、可配置性和隔离的情况下,我们可以利用 Linux 容器通过很多方式解决难题。无论基础架构是在企业内部还是在云端,或者混合使用两者,容器都能满足您的需求。

容器不就是虚拟化吗?

是,但也不竟然。我们用一种简单方式来思考一下:

虚拟化使得许多操作系统可同时在单个系统上运行。

容器则可共享同一个操作系统内核,将应用进程与系统其他部分隔离开。
2.png

普通虚拟化技术和Docker的对比

这意味着什么?首先,让多个操作系统在单个虚拟机监控程序上运行以实现虚拟化,并不能达成和使用容器同等的轻量级效果。事实上,在仅拥有容量有限的有限资源时,您需要能够可以进行密集部署的轻量级应用。Linux 容器可从单个操作系统运行,在所有容器中共享该操作系统,因此应用和服务能够保持轻量级,并行快速运行。

容器发展简史

3.png

我们现在称为容器技术的概念最初出现在2000年,当时称为FreeBSD Jail,这种技术可将FreeBSD系统分区为多个子系统(也称为Jail)。Jail是作为安全环境而开发的,系统管理员可与企业内部或外部的多个用户共享这些Jail。

Jail的目的是让进程在经过修改的chroot环境中创建,而不会脱离和影响整个系统——在chroot环境中,对文件系统、网络和用户的访问都实现了虚拟化。尽管Jail在实施方面存在局限性,但最终人们找到了脱离这种隔离环境的方法。

但这个概念非常有吸引力。

2001年,通过Jacques Gélinas的VServer项目,隔离环境的实施进入了Linux领域。正如Gélinas所说,这项工作的目的是“在高度独立且安全的单一环境中运行多个通用Linux服务器[sic]。”在完成了这项针对Linux中多个受控制用户空间的基础性工作后,Linux容器开始逐渐成形并最终发展成了现在的模样。

什么是 Docker?

“Docker”一词指代多种事物,包括开源社区项目、开源项目使用的工具、主导支持此类项目的公司Docker Inc.以及该公司官方支持的工具。技术产品和公司使用同一名称,的确让人有点困惑。

我们来简单说明一下:
  • IT 软件中所说的“Docker” ,是指容器化技术,用于支持创建和使用Linux容器。
  • 开源Docker社区致力于改进这类技术,并免费提供给所有用户,使之获益。
  • Docker Inc.公司凭借Docker社区产品起家,它主要负责提升社区版本的安全性,并将改进后的版本与更广泛的技术社区分享。此外,它还专门对这些技术产品进行完善和安全固化,以服务于企业客户。


借助Docker ,您可将容器当做重量轻、模块化的虚拟机使用。同时,您还将获得高度的灵活性,从而实现对容器的高效创建、部署及复制,并能将其从一个环境顺利迁移至另一个环境。

Docker如何工作?

Docker技术使用Linux内核和内核功能(例如Cgroups和namespaces)来分隔进程,以便各进程相互独立运行。这种独立性正是采用容器的目的所在;它可以独立运行多种进程、多个应用程序,更加充分地发挥基础设施的作用,同时保持各个独立系统的安全性。

容器工具(包括Docker)可提供基于镜像的部署模式。这使得它能够轻松跨多种环境,与其依赖程序共享应用或服务组。Docker还可在这一容器环境中自动部署应用程序(或者合并多种流程,以构建单个应用程序)。

此外,由于这些工具基于Linux容器构建,使得Docker既易于使用,又别具一格——它可为用户提供前所未有的高度应用程访问权限、快速部署以及版本控制和分发能力。

Docker技术是否与传统的Linux容器相同?

否。Docker技术最初是基于LXC技术构建(大多数人都会将这一技术与“传统的”Linux容器联系在一起),但后来它逐渐摆脱了对这种技术的依赖。

就轻量级虚拟化这一功能来看,LXC非常有用,但它无法提供出色的开发人员或用户体验。除了运行容器之外,Docker技术还具备其他多项功能,包括简化用于构建容器、传输镜像以及控制镜像版本的流程。
4.png

传统的Linux容器使用init系统来管理多种进程。这意味着,所有应用程序都作为一个整体运行。与此相反,Docker技术鼓励应用程序各自独立运行其进程,并提供相应工具以实现这一功能。这种精细化运作模式自有其优势。

Docker的目标

Docker的主要目标是“Build, Ship and Run any App, Angwhere",构建,运输,处处运行。
  • 构建:做一个Docker镜像
  • 运输:docker pull
  • 运行:启动一个容器


每一个容器,他都有自己的文件系统rootfs。

安装Docker

环境说明:
# 需要两台几点进行安装
[root@docker01 ~]# cat /etc/redhat-release 
CentOS Linux release 7.2.1511 (Core) 
[root@docker01 ~]# uname  -r 
3.10.0-327.el7.x86_64
[root@docker01 ~]# hostname -I
10.0.0.100 172.16.1.100 
[root@docker02 ~]# hostname -I
10.0.0.101 172.16.1.101

在两个节点上都进行操作:
wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo
sed -i 's#download.docker.com#mirrors.ustc.edu.cn/docker-ce#g' /etc/yum.repos.d/docker-ce.repo
yum install docker-ce -y

修改在docker01配置:
# 修改启动文件,监听远程端口
vim /usr/lib/systemd/system/docker.service
ExecStart=/usr/bin/dockerd -H unix:///var/run/docker.sock -H tcp://10.0.0.100:2375
systemctl daemon-reload
systemctl enable docker.service 
systemctl restart docker.service
# ps -ef检查进行,是否启动

在docker02测试:
[root@docker02 ~]# docker -H 10.0.0.100 info
Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 17.12.0-ce
Storage Driver: devicemapper
···

Docker基础命令操作

查看Docker相关信息:
[root@docker01 ~]#  docker version  
Client:
Version:    17.12.0-ce
API version:    1.35
Go version:    go1.9.2
Git commit:    c97c6d6
Built:    Wed Dec 27 20:10:14 2017
OS/Arch:    linux/amd64
Server:
Engine:
Version:    17.12.0-ce
API version:    1.35 (minimum version 1.12)
Go version:    go1.9.2
Git commit:    c97c6d6
Built:    Wed Dec 27 20:12:46 2017
OS/Arch:    linux/amd64
Experimental:    false

配置Docker镜像加速:
vi /etc/docker/daemon.json
{
"registry-mirrors": ["https://registry.docker-cn.com"]
}     

启动第一个容器

[root@docker01 ~]# docker run -d -p 80:80 nginx
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
e7bb522d92ff: Pull complete 
6edc05228666: Pull complete 
cd866a17e81f: Pull complete 
Digest: sha256:285b49d42c703fdf257d1e2422765c4ba9d3e37768d6ea83d7fe2043dad6e63d
Status: Downloaded newer image for nginx:latest
8d8f81da12b5c10af6ba1a5d07f4abc041cb95b01f3d632c3d638922800b0b4d
# 容器启动后,在浏览器进行访问测试

参数说明:
5.png

Docker镜像生命周期

6.png

Docker镜像相关操作

搜索官方仓库镜像

[root@docker01 ~]#  docker search centos
NAME                      DESCRIPTION                    STARS    OFFICIAL               AUTOMATED
centos                    The official build of CentOS.  3992     [OK]      
ansible/centos7-ansible   Ansible on Centos7             105                              [OK]

列表说明:
7.png

获取镜像

根据镜像名称拉取镜像:
[root@docker01 ~]# docker pull centos
Using default tag: latest
latest: Pulling from library/centos
af4b0a2388c6: Downloading  34.65MB/73.67MB

查看当前主机镜像列表:
[root@docker01 ~]# docker image list 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos              latest              ff426288ea90        3 weeks ago         207MB
nginx               latest              3f8a4339aadd        5 weeks ago         108MB

拉第三方镜像方法:
docker pull index.tenxcloud.com/tenxcloud/httpd

导出镜像

[root@docker01 ~]# docker image list 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos              latest              ff426288ea90        3 weeks ago         207MB
nginx               latest              3f8a4339aadd        5 weeks ago         108MB
# 导出
[root@docker01 ~]# docker image save centos > docker-centos.tar.gz

删除镜像

[root@docker01 ~]# docker image rm centos:latest
[root@docker01 ~]# docker image list 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              3f8a4339aadd        5 weeks ago         108MB

导入镜像

[root@docker01 ~]# docker image load -i docker-centos.tar.gz  
e15afa4858b6: Loading layer  215.8MB/215.8MB
Loaded image: centos:latest
[root@docker01 ~]# docker image list 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos              latest              ff426288ea90        3 weeks ago         207MB
nginx               latest              3f8a4339aadd        5 weeks ago         108MB

查看镜像的详细信息

[root@docker01 ~]# docker image inspect centos

容器的日常管理

容器的起/停

最简单的运行一个容器:
[root@docker01 ~]# docker run nginx

创建容器,两步走(不常用):
[root@docker01 ~]# docker create centos:latest  /bin/bash
bb7f32368ecf0492adb59e20032ab2e6cf6a563a0e6751e58930ee5f7aaef204
[root@docker01 ~]# docker start stupefied_nobel
stupefied_nobel

快速启动容器方法:
[root@docker01 ~]# docker run  centos:latest  /usr/bin/sleep 20;

容器内的第一个进程必须一直处于运行的状态,否则这个容器,就会处于退出状态!

查看正在运行的容器:
[root@docker01 ~]# docker container ls

[root@docker01 ~]# docker ps 
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
8708e93fd767        nginx               "nginx -g 'daemon of…"   6 seconds ago       Up 4 seconds        80/tcp              keen_lewin

查看你容器详细信息/ip:
[root@docker01 ~]# docker container  inspect  容器名称/id

查看你所有容器(包括未运行的):
[root@docker01 ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS               NAMES
8708e93fd767        nginx               "nginx -g 'daemon of…"   4 minutes ago       Exited (0) 59 seconds ago                       keen_lewin
f9f3e6af7508        nginx               "nginx -g 'daemon of…"   5 minutes ago       Exited (0) 5 minutes ago                        optimistic_haibt
8d8f81da12b5        nginx               "nginx -g 'daemon of…"   3 hours ago         Exited (0) 3 hours ago                          lucid_bohr

停止容器:
[root@docker01 ~]# docker stop 容器名称/id 

[root@docker01 ~]# docker container  kill  容器名称/id

进入容器方法

启动时进去方法:
[root@docker01 ~]# docker run -it #参数:-it 可交互终端
[root@docker01 ~]# docker run -it nginx:latest  /bin/bash
root@79241093859e:/#

退出/离开容器:
ctrl+p & ctrl+q

启动后进入容器的方法:

启动一个Docker:
[root@docker01 ~]# docker run -it centos:latest 
[root@1bf0f43c4d2f /]# ps -ef 
UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 15:47 pts/0    00:00:00 /bin/bash
root         13      1  0 15:47 pts/0    00:00:00 ps -ef

attach进入容器,使用pts/0 ,会让所用通过此方法进如放入用户看到同样的操作:
[root@docker01 ~]# docker attach 1bf0f43c4d2f
[root@1bf0f43c4d2f /]# ps -ef 
UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 15:47 pts/0    00:00:00 /bin/bash
root         14      1  0 15:49 pts/0    00:00:00 ps -ef

自命名启动一个容器--name:
[root@docker01 ~]# docker attach 1bf0f43c4d2f
[root@1bf0f43c4d2f /]# ps -ef 
UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 15:47 pts/0    00:00:00 /bin/bash
root         14      1  0 15:49 pts/0    00:00:00 ps -ef

exrc进入容器方法(推荐使用):
[root@docker01 ~]# docker exec -it clsn1  /bin/bash 
[root@b20fa75b4b40 /]# 重新分配一个终端
[root@b20fa75b4b40 /]# ps -ef 
UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 16:11 pts/0    00:00:00 /bin/bash
root         13      0  0 16:14 pts/1    00:00:00 /bin/bash
root         26     13  0 16:14 pts/1    00:00:00 ps -ef

删除所有容器

[root@docker01 ~]# docker rm -f  `docker ps -a -q`
# -f 强制删除

启动时进行端口映射

-p参数端口映射:
[root@docker01 ~]# docker run -d -p 8888:80  nginx:latest 
287bec5c60263166c03e1fc5b0b8262fe76507be3dfae4ce5cd2ee2d1e8a89a9

不同指定映射方法:
8.png

随机映射:
docker run -P (大P)# 需要镜像支持

Docker数据卷的管理

挂载时创建卷

挂载卷:
[root@docker01 ~]# docker run -d -p 80:80 -v /data:/usr/share/nginx/html nginx:latest
079786c1e297b5c5031e7a841160c74e91d4ad06516505043c60dbb78a259d09

容器内站点目录:/usr/share/nginx/html。

在宿主机写入数据,查看:
[root@docker01 ~]# echo "http://www.nmtui.com" >/data/index.html
[root@docker01 ~]# curl 10.0.0.100
http://www.nmtui.com

设置共享卷,使用同一个卷启动一个新的容器:
[root@docker01 ~]# docker run -d -p 8080:80 -v /data:/usr/share/nginx/html nginx:latest 
351f0bd78d273604bd0971b186979aa0f3cbf45247274493d2490527babb4e42
[root@docker01 ~]# curl 10.0.0.100:8080
http://www.nmtui.com

查看卷列表:
[root@docker01 ~]# docker volume ls
DRIVER              VOLUME NAME

创建卷后挂载

创建一个卷:
[root@docker01 ~]# docker volume create 
f3b95f7bd17da220e63d4e70850b8d7fb3e20f8ad02043423a39fdd072b83521
[root@docker01 ~]# docker volume ls 
DRIVER              VOLUME NAME
local               f3b95f7bd17da220e63d4e70850b8d7fb3e20f8ad02043423a39fdd072b83521

指定卷名:
[root@docker01 ~]# docker volume ls 
DRIVER              VOLUME NAME
local               clsn
local               f3b95f7bd17da220e63d4e70850b8d7fb3e20f8ad02043423a39fdd072b83521

查看卷路径:
[root@docker01 ~]# docker volume inspect clsn 
[
{
    "CreatedAt": "2018-02-01T00:39:25+08:00",
    "Driver": "local",
    "Labels": {},
    "Mountpoint": "/var/lib/docker/volumes/clsn/_data",
    "Name": "clsn",
    "Options": {},
    "Scope": "local"
}
]

使用卷创建:
[root@docker01 ~]# docker run -d -p 9000:80 -v clsn:/usr/share/nginx/html nginx:latest 
1434559cff996162da7ce71820ed8f5937fb7c02113bbc84e965845c219d3503
# 宿主机测试
[root@docker01 ~]# echo 'blog.nmtui.com' >/var/lib/docker/volumes/clsn/_data/index.html 
[root@docker01 ~]# curl 10.0.0.100:9000
blog.nmtui.com

设置卷:
[root@docker01 ~]# docker run  -d  -P  --volumes-from 079786c1e297 nginx:latest 
b54b9c9930b417ab3257c6e4a8280b54fae57043c0b76b9dc60b4788e92369fb

查看使用的端口:
[root@docker01 ~]# netstat -lntup 
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1400/sshd           
tcp        0      0 10.0.0.100:2375         0.0.0.0:*               LISTEN      26218/dockerd       
tcp6       0      0 :::9000                 :::*                    LISTEN      32015/docker-proxy  
tcp6       0      0 :::8080                 :::*                    LISTEN      31853/docker-proxy  
tcp6       0      0 :::80                   :::*                    LISTEN      31752/docker-proxy  
tcp6       0      0 :::22                   :::*                    LISTEN      1400/sshd           
tcp6       0      0 :::32769                :::*                    LISTEN      32300/docker-proxy  
[root@docker01 ~]# curl 10.0.0.100:32769
http://www.nmtui.com

手动将容器保存为镜像

本次是基于Docker官方CentOS 6.8镜像创建。

官方镜像列表:https://hub.docker.com/explore/

启动一个CentOS 6.8的镜像:
[root@docker01 ~]# docker pull  centos:6.8
[root@docker01 ~]# docker run -it -p 1022:22 centos:6.8  /bin/bash
# 在容器种安装sshd服务,并修改系统密码
[root@582051b2b92b ~]# yum install  openssh-server -y 
[root@582051b2b92b ~]# echo "root:123456" |chpasswd
[root@582051b2b92b ~]#  /etc/init.d/sshd start

启动完成后镜像ssh连接测试。

将容器提交为镜像:
[root@docker01 ~]# docker commit brave_mcclintock  centos6-ssh

使用新的镜像启动容器:
[root@docker01 ~]# docker run -d  -p 1122:22  centos6-ssh:latest  /usr/sbin/sshd -D 
5b8161fda2a9f2c39c196c67e2eb9274977e7723fe51c4f08a0190217ae93094

在容器安装httpd服务:
[root@5b8161fda2a9 /]#  yum install httpd -y

编写启动脚本脚本:
[root@5b8161fda2a9 /]# cat  init.sh 
#!/bin/bash 
/etc/init.d/httpd start 
/usr/sbin/sshd -D
[root@5b8161fda2a9 /]# chmod +x init.sh 
# 注意执行权限

再次提交为新的镜像:
[root@docker01 ~]# docker commit  5b8161fda2a9 centos6-httpd 
sha256:705d67a786cac040800b8485cf046fd57b1828b805c515377fc3e9cea3a481c1

启动镜像,做好端口映射。并在浏览器中测试访问:
[root@docker01 ~]# docker run -d -p 1222:22 -p 80:80  centos6-httpd /init.sh 
46fa6a06644e31701dc019fb3a8c3b6ef008d4c2c10d46662a97664f838d8c2c

Dockerfile自动构建Docker镜像

官方构建Dockerffile文件参考:https://github.com/CentOS/CentOS-Dockerfiles

Dockerfile指令集

Dockerfile主要组成部分:
  • 基础镜像信息:FROM centos:6.8
  • 制作镜像操作指令:RUN yum insatll openssh-server -y
  • 容器启动时执行指令:CMD ["/bin/bash"]


Dockerfile常用指令:
  • FROM:这个镜像的妈妈是谁(指定基础镜像)
  • MAINTAINER:告诉别人,谁负责养它(指定维护者信息,可以没有)
  • RUN:你想让它干啥(在命令前面加上RUN即可)
  • ADD:给它点创业资金(COPY文件,会自动解压)
  • WORKDIR:我是cd,今天刚化了妆(设置当前工作目录)
  • VOLUME:给它一个存放行李的地方(设置卷,挂载主机目录)
  • EXPOSE:它要打开的门是啥(指定对外的端口)
  • CMD:奔跑吧,兄弟!(指定容器启动后的要干的事情)


Dockerfile其他指令:
  • COPY:复制文件
  • ENV:环境变量
  • ENTRYPOINT:容器启动后执行的命令


创建一个Dockerfile

创建第一个Dockerfile文件:
# 创建目录
[root@docker01 base]# cd /opt/base
# 创建Dcokerfile文件,注意大小写
[root@docker01 base]# vim Dockerfile
FROM centos:6.8
RUN yum install openssh-server -y 
RUN echo "root:123456" |chpasswd
RUN /etc/init.d/sshd start 
CMD ["/usr/sbin/sshd","-D"]

构建Docker镜像:
[root@docker01 base]# docker image build  -t centos6.8-ssh . 
-t 为镜像标签打标签, . 表示当前路径

使用自构建的镜像启动:
[root@docker01 base]# docker run  -d -p 2022:22 centos6.8-ssh-b 
dc3027d3c15dac881e8e2aeff80724216f3ac725f142daa66484f7cb5d074e7a

使用Dcokerfile安装KodExplorer

Dockerfile文件内容:
FROM centos:6.8
RUN yum install wget unzip php php-gd php-mbstring -y && yum clean all
# 设置工作目录,之后的操作都在这个目录中
WORKDIR /var/www/html/
RUN wget -c http://static.kodcloud.com/update/download/kodexplorer4.25.zip
RUN unzip kodexplorer4.25.zip && rm -f kodexplorer4.25.zip
RUN chown -R apache.apache .
CMD ["/usr/sbin/apachectl","-D","FOREGROUND"]

更多的Dockerfile可以参考官方方法。

Docker中的镜像分层

参考文档:http://www.maiziedu.com/wiki/cloud/dockerimage/

Docker支持通过扩展现有镜像,创建新的镜像。实际上,Docker Hub中99%的镜像都是通过在base镜像中安装和配置需要的软件构建出来的。
9.png

从上图可以看到,新镜像是从base镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。

Docker镜像为什么分层

镜像分层最大的一个好处就是共享资源。

比如说有多个镜像都从相同的base镜像构建而来,那么Docker Host只需在磁盘上保存一份base镜像;同时内存中也只需加载一份base镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享。

如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如/etc下的文件,这时其他容器的/etc是不会被修改的,修改只会被限制在单个容器内。这就是容器Copy-on-Write特性。

可写的容器层

当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。
10.png

所有对容器的改动——无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。

容器层的细节说明

镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如/a,上层的/a会覆盖下层的/a,也就是说用户只能访问到上层中的文件/a。在容器层中,用户看到的是一个叠加之后的文件系统。

文件操作的说明:
11.png

只有当需要修改时才复制一份数据,这种特性被称作Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。

这样就解释了我们前面提出的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。

使用Docker运行zabbix-server

容器间的互联

在运行Zabbix之前务必要了解容器间互联的方法:
# 创建一个Nginx容器
docker run -d -p 80:80 nginx
# 创建容器,做link,并进入容器中
docker run -it --link quirky_brown:web01 centos-ssh /bin/bash
# 在容器中访问Nginx容器可以ping通
ping web01    

命令执行过程:
# 启动apache容器
[root@docker01 ~]# docker run -d httpd:2.4  
3f1f7fc554720424327286bd2b04aeab1b084a3fb011a785b0deab6a34e56955
^[[A[root@docker01 docker ps -a
CONTAINER ID        IMAGE               COMMAND              CREATED             STATUS              PORTS               NAMES
3f1f7fc55472        httpd:2.4           "httpd-foreground"   6 seconds ago       Up 5 seconds        80/tcp              determined_clarke
# 拉取一个busybox 镜像
[root@docker01 ~]# docker pull busybox 
# 启动容器
[root@docker01 ~]# docker run -it  --link determined_clarke:web busybox:latest   /bin/sh 
/ # 
# 使用新的容器访问最初的web容器
/ # ping web 
PING web (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.058 ms
^C
--- web ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.058/0.058/0.058 ms

启动Zabbix容器

启动一个MySQL的容器:
docker run --name mysql-server -t \
  -e MYSQL_DATABASE="zabbix" \
  -e MYSQL_USER="zabbix" \
  -e MYSQL_PASSWORD="zabbix_pwd" \
  -e MYSQL_ROOT_PASSWORD="root_pwd" \
  -d mysql:5.7 \
  --character-set-server=utf8 --collation-server=utf8_bin

启动java-gateway容器监控Java服务:
docker run --name zabbix-java-gateway -t \
  -d zabbix/zabbix-java-gateway:latest

启动zabbix-mysql容器使用link连接MySQL与java-gateway:
docker run --name zabbix-server-mysql -t \
  -e DB_SERVER_HOST="mysql-server" \
  -e MYSQL_DATABASE="zabbix" \
  -e MYSQL_USER="zabbix" \
  -e MYSQL_PASSWORD="zabbix_pwd" \
  -e MYSQL_ROOT_PASSWORD="root_pwd" \
  -e ZBX_JAVAGATEWAY="zabbix-java-gateway" \
  --link mysql-server:mysql \
  --link zabbix-java-gateway:zabbix-java-gateway \
  -p 10051:10051 \
  -d zabbix/zabbix-server-mysql:latest

启动Zabbix Web显示,使用link连接zabbix-mysql与MySQL:
docker run --name zabbix-web-nginx-mysql -t \
  -e DB_SERVER_HOST="mysql-server" \
  -e MYSQL_DATABASE="zabbix" \
  -e MYSQL_USER="zabbix" \
  -e MYSQL_PASSWORD="zabbix_pwd" \
  -e MYSQL_ROOT_PASSWORD="root_pwd" \
  --link mysql-server:mysql \
  --link zabbix-server-mysql:zabbix-server \
  -p 80:80 \
  -d zabbix/zabbix-web-nginx-mysql:latest

关于Zabbix API

关于Zabbix API可以参考官方文档:https://www.zabbix.com/documen ... l/api

获取token方法:
# 获取token
[root@docker02 ~]# curl -s -X POST -H 'Content-Type:application/json' -d '
{
"jsonrpc": "2.0",
"method": "user.login",
"params": {
"user": "Admin",
"password": "zabbix"
},
"id": 1
}' http://10.0.0.100/api_jsonrpc.php
{"jsonrpc":"2.0","result":"d3be707f9e866ec5d0d1c242292cbebd","id":1} 

Docker仓库(Registry)

创建一个普通仓库

创建仓库:
docker run -d -p 5000:5000 --restart=always --name registry -v /opt/myregistry:/var/lib/registry  registry 

修改配置文件,使之支持http:
[root@docker01 ~]# cat  /etc/docker/daemon.json 
{
"registry-mirrors": ["https://registry.docker-cn.com"],
"insecure-registries": ["10.0.0.100:5000"]


重启Docker让修改生效:
[root@docker01 ~]# systemctl restart  docker.service

修改镜像标签:
[root@docker01 ~]# docker tag  busybox:latest  10.0.0.100:5000/clsn/busybox:1.0
[root@docker01 ~]# docker images
REPOSITORY                      TAG                 IMAGE ID            CREATED             SIZE
centos6-ssh                     latest              3c2b1e57a0f5        18 hours ago        393MB
httpd                           2.4                 2e202f453940        6 days ago          179MB
10.0.0.100:5000/clsn/busybox    1.0                 5b0d59026729        8 days ago          1.15MB

将新打标签的镜像上传镜像到仓库:
[root@docker01 ~]# docker push   10.0.0.100:5000/clsn/busybox

带basic认证的仓库

安装加密工具:
[root@docker01 clsn]# yum install httpd-tools  -y

设置认证密码:
mkdir /opt/registry-var/auth/ -p
htpasswd  -Bbn clsn 123456  > /opt/registry-var/auth/htpasswd

启动容器,在启动时传入认证参数:
docker run -d -p 5000:5000 -v /opt/registry-var/auth/:/auth/ -e "REGISTRY_AUTH=htpasswd" -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd registry

使用验证用户测试:
# 登陆用户
[root@docker01 ~]# docker login 10.0.0.100:5000 
Username: clsn  
Password: 123456
Login Succeeded
# 推送镜像到仓库
[root@docker01 ~]# docker push 10.0.0.100:5000/clsn/busybox 
The push refers to repository [10.0.0.100:5000/clsn/busybox]
4febd3792a1f: Pushed 
1.0: digest: sha256:4cee1979ba0bf7db9fc5d28fb7b798ca69ae95a47c5fecf46327720df4ff352d size: 527
#认证文件的保存位置
[root@docker01 ~]# cat .docker/config.json 
{
"auths": {
    "10.0.0.100:5000": {
        "auth": "Y2xzbjoxMjM0NTY="
    },
    "https://index.docker.io/v1/": {
        "auth": "Y2xzbjpIenNAMTk5Ng=="
    }
},
"HttpHeaders": {
    "User-Agent": "Docker-Client/17.12.0-ce (linux)"
}


至此,一个简单的Docker镜像仓库搭建完成。

docker-compose编排工具

安装docker-compose

安装docker-compose:
# 下载pip软件
yum install -y python2-pip
# 下载 docker-compose
pip install docker-compose

国内开启pip下载加速:http://mirrors.aliyun.com/help/pypi
mkdir ~/.pip/
cat > ~/.pip/pip.conf <<'EOF'
[global]
index-url = https://mirrors.aliyun.com/pypi/simple/
[install]
trusted-host=mirrors.aliyun.com
EOF

编排启动镜像

创建文件目录:
[root@docker01 ~]# mkdir /opt/my_wordpress/
[root@docker01 ~]# cd /opt/my_wordpress/

编写编排文件:
[root@docker01 my_wordpress]# vim docker-compose.yml
version: '3'
services:
db:
 image: mysql:5.7
 volumes:
   - /data/db_data:/var/lib/mysql
 restart: always
 environment:
   MYSQL_ROOT_PASSWORD: somewordpress
   MYSQL_DATABASE: wordpress
   MYSQL_USER: wordpress
   MYSQL_PASSWORD: wordpress
wordpress:
 depends_on:
   - db
 image: wordpress:latest
 volumes:
   - /data/web_data:/var/www/html
 ports: 
   - "8000:80"
 restart: always
 environment:
   WORDPRESS_DB_HOST: db:3306
   WORDPRESS_DB_USER: wordpress
   WORDPRESS_DB_PASSWORD: wordpress

启动:
[root@docker01 my_wordpress]# docker-compose up
  #启动方法:docker-compose up
  #后台启动方法:docker-compose up -d

浏览器上访问http://10.0.0.100:8000

进行wordpress的安装即可。

HAProxy代理后端Docker容器

修改编排脚本:
[root@docker01 my_wordpress]# cat docker-compose.yml 
version: '3'
services:
db:
 image: mysql:5.7
 volumes:
   - /data/db_data:/var/lib/mysql
 restart: always
 environment:
   MYSQL_ROOT_PASSWORD: somewordpress
   MYSQL_DATABASE: wordpress
   MYSQL_USER: wordpress
   MYSQL_PASSWORD: wordpress
wordpress:
 depends_on:
   - db
 image: wordpress:latest
 volumes:
   - /data/web_data:/var/www/html
 ports: 
   - "80"
 restart: always
 environment:
   WORDPRESS_DB_HOST: db:3306
   WORDPRESS_DB_USER: wordpress
   WORDPRESS_DB_PASSWORD: wordpress

同时启动两台wordpress:
[root@docker01 my_wordpress]# docker-compose scale wordpress=2 
WARNING: The scale command is deprecated. Use the up command with the --scale flag instead.
Starting mywordpress_wordpress_1 ... done
Creating mywordpress_wordpress_2 ... done

安装HAProxy:
[root@docker01 ~]# yum install haproxy -y

修改HAProxy配置文件。

关于配置文件的详细说明,参考:https://www.cnblogs.com/MacoLee/p/5853413.html
[root@docker01 ~]#cp /etc/haproxy/haproxy.cfg{,.bak}
[root@docker01 ~]# vim /etc/haproxy/haproxy.cfg
global
log         127.0.0.1 local2
chroot      /var/lib/haproxy
pidfile     /var/run/haproxy.pid
maxconn     4000
user        haproxy
group       haproxy
daemon
stats socket /var/lib/haproxy/stats level admin  #支持命令行控制
defaults
mode                    http
log                     global
option                  httplog
option                  dontlognull
option http-server-close
option forwardfor       except 127.0.0.0/8
option                  redispatch
retries                 3
timeout http-request    10s
timeout queue           1m
timeout connect         10s
timeout client          1m
timeout server          1m
timeout http-keep-alive 10s
timeout check           10s
maxconn                 3000
listen stats
mode http
bind 0.0.0.0:8888
stats enable
stats uri     /haproxy-status 
stats auth    admin:123456
frontend frontend_www_example_com
bind 10.0.0.100:8000
mode http
option httplog
log global
default_backend backend_www_example_com
backend backend_www_example_com
option forwardfor header X-REAL-IP
option httpchk HEAD / HTTP/1.0
balance roundrobin
server web-node1  10.0.0.100:32768 check inter 2000 rise 30 fall 15
server web-node2  10.0.0.100:32769 check inter 2000 rise 30 fall 15

启动HAProxy:
systemctl start haproxy
systemctl enable haproxy

使用浏览器访问HAProxy监听的8000端口可以看到负载的情况:
12.png

使用浏览器访问http://10.0.0.100:8888/haproxy-status可以看到后端节点的监控状况:
13.png

安装Socat直接操作socket控制HAProxy

安装软件:
yum install socat.x86_64 -y

查看帮助:
[root@docker01 web_data]# echo "help"|socat stdio /var/lib/haproxy/stats

下线后端节点:
echo "disable server backend_www_example_com/web-node2"|socat stdio /var/lib/haproxy/stats

上线后端节点:
echo "enable server backend_www_example_com/web-node3"|socat stdio /var/lib/haproxy/stats

编写PHP测试页,放到/data/web_data下,在浏览器中访问可以查看当前的节点:
[root@docker01 web_data]# vim check.php
<html>
<head>
    <title>PHP测试</title>
</head>
<body>
    <?php  echo '<p>Hello World </p>'; ?>
    <?php  echo "访问的服务器地址是:"."<fontcolor=red>".$_SERVER['SERVER_ADDR']."</font>"."<br>";
    echo"访问的服务器域名是:"."<fontcolor=red>".$_SERVER['SERVER_NAME']."</font>"."<br>";
    ?>
</body>
</html>

重启Docker服务,容器全部退出的解决办法

在启动是指定自动重启

docker run  --restart=always

修改Docker默认配置文件

# 添加上下面这行
"live-restore": true
docker server配置文件/etc/docker/daemon.json参考

[root@docker02 ~]# cat  /etc/docker/daemon.json 
{
"registry-mirrors": ["https://registry.docker-cn.com"],
"graph": "/opt/mydocker", # 修改数据的存放目录到/opt/mydocker/,原/var/lib/docker/
"insecure-registries": ["10.0.0.100:5000"],
"live-restore": true


重启生效,只对在此之后启动的容器生效:
[root@docker01 ~]# systemctl restart  docker.service

Docker网络类型

14.png

Docker的网络类型

15.png

Bridge默认Docker网络隔离基于网络命名空间,在物理机上创建Docker容器时会为每一个Docker容器分配网络命名空间,并且把容器IP桥接到物理机的虚拟网桥上。

不为容器配置网络功能

此模式下创建容器是不会为容器配置任何网络参数的,如:容器网卡、IP、通信路由等,全部需要自己去配置。
[root@docker01 ~]# docker run  -it --network none busybox:latest  /bin/sh 
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue 
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
   valid_lft forever preferred_lft forever

与其他容器共享网络配置(Container)

此模式和Host模式很类似,只是此模式创建容器共享的是其他容器的IP和端口而不是物理机,此模式容器自身是不会配置网络和端口,创建此模式容器进去后,你会发现里边的IP是你所指定的那个容器IP并且端口也是共享的,而且其它还是互相隔离的,如进程等。
[root@docker01 ~]# docker run  -it --network container:mywordpress_db_1  busybox:latest  /bin/sh 
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue 
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
   valid_lft forever preferred_lft forever
105: eth0@if106: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.3/16 brd 172.18.255.255 scope global eth0
   valid_lft forever preferred_lft forever

使用宿主机网络

此模式创建的容器没有自己独立的网络命名空间,是和物理机共享一个Network Namespace,并且共享物理机的所有端口与IP,并且这个模式认为是不安全的。
[root@docker01 ~]# docker run  -it --network host  busybox:latest  /bin/sh

查看网络列表

[root@docker01 ~]# docker network list 
NETWORK ID          NAME                  DRIVER              SCOPE
b15e8a720d3b        bridge                bridge              local
345d65b4c2a0        host                  host                local
bc5e2a32bb55        mywordpress_default   bridge              local
ebf76eea91bb        none                  null                local

用PIPEWORK为Docker容器配置独立IP

参考文档:http://blog.csdn.net/design321 ... 64825

官方网站:https://github.com/jpetazzo/pipework

宿主环境:CentOS 7.2

安装pipework:
wget https://github.com/jpetazzo/pipework/archive/master.zip
unzip master.zip 
cp pipework-master/pipework  /usr/local/bin/
chmod +x /usr/local/bin/pipework

配置桥接网卡。

安装桥接工具:
yum install bridge-utils.x86_64 -y

修改网卡配置,实现桥接:
# 修改eth0配置,让br0实现桥接
[root@docker01 ~]# cat /etc/sysconfig/network-scripts/ifcfg-eth0 
TYPE=Ethernet
BOOTPROTO=static
NAME=eth0
DEVICE=eth0
ONBOOT=yes
BRIDGE=br0
[root@docker01 ~]# cat /etc/sysconfig/network-scripts/ifcfg-br0 
TYPE=Bridge
BOOTPROTO=static
NAME=br0
DEVICE=br0
ONBOOT=yes
IPADDR=10.0.0.100
NETMASK=255.255.255.0
GATEWAY=10.0.0.254
DNS1=223.5.5.5
# 重启网络
[root@docker01 ~]# /etc/init.d/network restart

运行一个容器镜像测试:
pipework br0 $(docker run -d -it -p 6880:80 --name  httpd_pw httpd) 10.0.0.220/24@10.0.0.254

在其他主机上测试端口及连通性:
[root@docker01 ~]# curl 10.0.0.220
<html><body><h1>It works!</h1></body></html>
[root@docker01 ~]# ping 10.0.0.220 -c 1
PING 10.0.0.220 (10.0.0.220) 56(84) bytes of data.
64 bytes from 10.0.0.220: icmp_seq=1 ttl=64 time=0.043 ms

再运行一个容器,设置网路类型为none:
pipework br0 $(docker run -d -it --net=none --name test httpd:2.4) 10.0.0.221/24@10.0.0.254

进行访问测试:
[root@docker01 ~]# curl 10.0.0.221
<html><body><h1>It works!</h1></body></html>

重启容器后需要再次指定:
pipework br0 testduliip  172.16.146.113/24@172.16.146.1
pipework br0 testduliip01  172.16.146.112/24@172.16.146.1

Dcoker跨主机通信之overlay可以参考:http://www.cnblogs.com/CloudMan6/p/7270551.html

Docker跨主机通信之Macvlan

创建网络:
[root@docker01 ~]# docker network  create --driver macvlan  --subnet 10.1.0.0/24 --gateway 10.1.0.254 -o parent=eth0  macvlan_1
33a1f41dcc074f91b5bd45e7dfedabfb2b8ec82db16542f05213839a119b62ca

设置网卡为混杂模式:
ip link set eth0 promisc on

创建使用Macvlan网络容器:
[root@docker02 ~]# docker run  -it --network macvlan_1  --ip=10.1.0.222 busybox /bin/sh

Docker企业级镜像仓库Harbor

容器管理:
[root@docker01 harbor]# pwd
/opt/harbor
[root@docker01 harbor]# docker-compose stop

安装Docker、docker-compose。

下载Harbor:
cd /opt && https://storage.googleapis.com/harbor-releases/harbor-offline-installer-v1.3.0.tgz
tar xf harbor-offline-installer-v1.3.0.tgz

修改主机及Web界面密码:
[root@docker01 harbor]# vim harbor.cfg 
···
hostname = 10.0.0.100
harbor_admin_password = Harbor12345
···

执行安装脚本:
[root@docker01 harbor]# ./install.sh

浏览器访问 http://10.0.0.11
16.png

添加一个项目:
17.png

镜像推送到仓库的指定项目:
[root@docker02 ~]# docker  tag centos:6.8  10.0.0.100/clsn/centos6.8:1.0
[root@docker02 ~]#  
[root@docker02 ~]# docker images 
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
busybox                     latest              5b0d59026729        8 days ago          1.15MB
10.0.0.100/clsn/centos6.8   1.0                 6704d778b3ba        2 months ago        195MB
centos                      6.8                 6704d778b3ba        2 months ago        195MB
[root@docker02 ~]# docker login 10.0.0.100
Username: admin
Password: 
Login Succeeded

推送镜像:
[root@docker02 ~]# docker push 10.0.0.100/clsn/centos6.8 
The push refers to repository [10.0.0.100/clsn/centos6.8]
e00c9229b481: Pushing  13.53MB/194.5MB

在Web界面里查看:
18.png

使用容器的建议

  1. 不要以拆分方式进行应用程序发布
  2. 不要创建大型镜像
  3. 不要在单个容器中运行多个进程
  4. 不要再镜像内保存凭证,不要依赖IP地址
  5. 以非root用户运行进程
  6. 不要使用“最新”标签
  7. 不要利用运行中的容器创建镜像
  8. 不要使用单层镜像
  9. 不要将数据存放在容器内


关于Docker容器的监控

容器的基本信息:包括容器的数量、ID、名称、镜像、启动命令、端口等信息。

容器的运行状态:统计各状态的容器的数量,包括运行中、暂停、停止及异常退出。

容器的用量信息:统计容器的CPU使用率、内存使用量、块设备I/O使用量、网络使用情况等资源的使用情况。

原文链接:https://www.cnblogs.com/clsn/p/8410309.html
继续阅读 »

容器简介

什么是Linux容器?

Linux容器是与系统其他部分隔离开的一系列进程,从另一个镜像运行,并由该镜像提供支持进程所需的全部文件。容器提供的镜像包含了应用的所有依赖项,因而在从开发到测试再到生产的整个过程中,它都具有可移植性和一致性。
1.png

更加详细地来说,请您假定您在开发一个应用。您使用的是一台笔记本电脑,而且您的开发环境具有特定的配置。其他开发人员身处的环境配置可能稍有不同。您正在开发的应用依赖于您当前的配置,还要依赖于某些特定文件。与此同时,您的企业还拥有标准化的测试和生产环境,且具有自身的配置和一系列支持文件。您希望尽可能多在本地模拟这些环境,而不产生重新创建服务器环境的开销。

因此,您要如何确保应用能够在这些环境中运行和通过质量检测,并且在部署过程中不出现令人头疼的问题,也无需重新编写代码和进行故障修复?答案就是使用容器。容器可以确保您的应用拥有必需的配置和文件,使得这些应用能够在从开发到测试、再到生产的整个流程中顺利运行,而不出现任何不良问题。这样可以避免危机,做到皆大欢喜。

虽然这只是简化的示例,但在需要很高的可移植性、可配置性和隔离的情况下,我们可以利用 Linux 容器通过很多方式解决难题。无论基础架构是在企业内部还是在云端,或者混合使用两者,容器都能满足您的需求。

容器不就是虚拟化吗?

是,但也不竟然。我们用一种简单方式来思考一下:

虚拟化使得许多操作系统可同时在单个系统上运行。

容器则可共享同一个操作系统内核,将应用进程与系统其他部分隔离开。
2.png

普通虚拟化技术和Docker的对比

这意味着什么?首先,让多个操作系统在单个虚拟机监控程序上运行以实现虚拟化,并不能达成和使用容器同等的轻量级效果。事实上,在仅拥有容量有限的有限资源时,您需要能够可以进行密集部署的轻量级应用。Linux 容器可从单个操作系统运行,在所有容器中共享该操作系统,因此应用和服务能够保持轻量级,并行快速运行。

容器发展简史

3.png

我们现在称为容器技术的概念最初出现在2000年,当时称为FreeBSD Jail,这种技术可将FreeBSD系统分区为多个子系统(也称为Jail)。Jail是作为安全环境而开发的,系统管理员可与企业内部或外部的多个用户共享这些Jail。

Jail的目的是让进程在经过修改的chroot环境中创建,而不会脱离和影响整个系统——在chroot环境中,对文件系统、网络和用户的访问都实现了虚拟化。尽管Jail在实施方面存在局限性,但最终人们找到了脱离这种隔离环境的方法。

但这个概念非常有吸引力。

2001年,通过Jacques Gélinas的VServer项目,隔离环境的实施进入了Linux领域。正如Gélinas所说,这项工作的目的是“在高度独立且安全的单一环境中运行多个通用Linux服务器[sic]。”在完成了这项针对Linux中多个受控制用户空间的基础性工作后,Linux容器开始逐渐成形并最终发展成了现在的模样。

什么是 Docker?

“Docker”一词指代多种事物,包括开源社区项目、开源项目使用的工具、主导支持此类项目的公司Docker Inc.以及该公司官方支持的工具。技术产品和公司使用同一名称,的确让人有点困惑。

我们来简单说明一下:
  • IT 软件中所说的“Docker” ,是指容器化技术,用于支持创建和使用Linux容器。
  • 开源Docker社区致力于改进这类技术,并免费提供给所有用户,使之获益。
  • Docker Inc.公司凭借Docker社区产品起家,它主要负责提升社区版本的安全性,并将改进后的版本与更广泛的技术社区分享。此外,它还专门对这些技术产品进行完善和安全固化,以服务于企业客户。


借助Docker ,您可将容器当做重量轻、模块化的虚拟机使用。同时,您还将获得高度的灵活性,从而实现对容器的高效创建、部署及复制,并能将其从一个环境顺利迁移至另一个环境。

Docker如何工作?

Docker技术使用Linux内核和内核功能(例如Cgroups和namespaces)来分隔进程,以便各进程相互独立运行。这种独立性正是采用容器的目的所在;它可以独立运行多种进程、多个应用程序,更加充分地发挥基础设施的作用,同时保持各个独立系统的安全性。

容器工具(包括Docker)可提供基于镜像的部署模式。这使得它能够轻松跨多种环境,与其依赖程序共享应用或服务组。Docker还可在这一容器环境中自动部署应用程序(或者合并多种流程,以构建单个应用程序)。

此外,由于这些工具基于Linux容器构建,使得Docker既易于使用,又别具一格——它可为用户提供前所未有的高度应用程访问权限、快速部署以及版本控制和分发能力。

Docker技术是否与传统的Linux容器相同?

否。Docker技术最初是基于LXC技术构建(大多数人都会将这一技术与“传统的”Linux容器联系在一起),但后来它逐渐摆脱了对这种技术的依赖。

就轻量级虚拟化这一功能来看,LXC非常有用,但它无法提供出色的开发人员或用户体验。除了运行容器之外,Docker技术还具备其他多项功能,包括简化用于构建容器、传输镜像以及控制镜像版本的流程。
4.png

传统的Linux容器使用init系统来管理多种进程。这意味着,所有应用程序都作为一个整体运行。与此相反,Docker技术鼓励应用程序各自独立运行其进程,并提供相应工具以实现这一功能。这种精细化运作模式自有其优势。

Docker的目标

Docker的主要目标是“Build, Ship and Run any App, Angwhere",构建,运输,处处运行。
  • 构建:做一个Docker镜像
  • 运输:docker pull
  • 运行:启动一个容器


每一个容器,他都有自己的文件系统rootfs。

安装Docker

环境说明:
# 需要两台几点进行安装
[root@docker01 ~]# cat /etc/redhat-release 
CentOS Linux release 7.2.1511 (Core) 
[root@docker01 ~]# uname  -r 
3.10.0-327.el7.x86_64
[root@docker01 ~]# hostname -I
10.0.0.100 172.16.1.100 
[root@docker02 ~]# hostname -I
10.0.0.101 172.16.1.101

在两个节点上都进行操作:
wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo
sed -i 's#download.docker.com#mirrors.ustc.edu.cn/docker-ce#g' /etc/yum.repos.d/docker-ce.repo
yum install docker-ce -y

修改在docker01配置:
# 修改启动文件,监听远程端口
vim /usr/lib/systemd/system/docker.service
ExecStart=/usr/bin/dockerd -H unix:///var/run/docker.sock -H tcp://10.0.0.100:2375
systemctl daemon-reload
systemctl enable docker.service 
systemctl restart docker.service
# ps -ef检查进行,是否启动

在docker02测试:
[root@docker02 ~]# docker -H 10.0.0.100 info
Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 0
Server Version: 17.12.0-ce
Storage Driver: devicemapper
···

Docker基础命令操作

查看Docker相关信息:
[root@docker01 ~]#  docker version  
Client:
Version:    17.12.0-ce
API version:    1.35
Go version:    go1.9.2
Git commit:    c97c6d6
Built:    Wed Dec 27 20:10:14 2017
OS/Arch:    linux/amd64
Server:
Engine:
Version:    17.12.0-ce
API version:    1.35 (minimum version 1.12)
Go version:    go1.9.2
Git commit:    c97c6d6
Built:    Wed Dec 27 20:12:46 2017
OS/Arch:    linux/amd64
Experimental:    false

配置Docker镜像加速:
vi /etc/docker/daemon.json
{
"registry-mirrors": ["https://registry.docker-cn.com"]
}     

启动第一个容器

[root@docker01 ~]# docker run -d -p 80:80 nginx
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
e7bb522d92ff: Pull complete 
6edc05228666: Pull complete 
cd866a17e81f: Pull complete 
Digest: sha256:285b49d42c703fdf257d1e2422765c4ba9d3e37768d6ea83d7fe2043dad6e63d
Status: Downloaded newer image for nginx:latest
8d8f81da12b5c10af6ba1a5d07f4abc041cb95b01f3d632c3d638922800b0b4d
# 容器启动后,在浏览器进行访问测试

参数说明:
5.png

Docker镜像生命周期

6.png

Docker镜像相关操作

搜索官方仓库镜像

[root@docker01 ~]#  docker search centos
NAME                      DESCRIPTION                    STARS    OFFICIAL               AUTOMATED
centos                    The official build of CentOS.  3992     [OK]      
ansible/centos7-ansible   Ansible on Centos7             105                              [OK]

列表说明:
7.png

获取镜像

根据镜像名称拉取镜像:
[root@docker01 ~]# docker pull centos
Using default tag: latest
latest: Pulling from library/centos
af4b0a2388c6: Downloading  34.65MB/73.67MB

查看当前主机镜像列表:
[root@docker01 ~]# docker image list 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos              latest              ff426288ea90        3 weeks ago         207MB
nginx               latest              3f8a4339aadd        5 weeks ago         108MB

拉第三方镜像方法:
docker pull index.tenxcloud.com/tenxcloud/httpd

导出镜像

[root@docker01 ~]# docker image list 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos              latest              ff426288ea90        3 weeks ago         207MB
nginx               latest              3f8a4339aadd        5 weeks ago         108MB
# 导出
[root@docker01 ~]# docker image save centos > docker-centos.tar.gz

删除镜像

[root@docker01 ~]# docker image rm centos:latest
[root@docker01 ~]# docker image list 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              3f8a4339aadd        5 weeks ago         108MB

导入镜像

[root@docker01 ~]# docker image load -i docker-centos.tar.gz  
e15afa4858b6: Loading layer  215.8MB/215.8MB
Loaded image: centos:latest
[root@docker01 ~]# docker image list 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos              latest              ff426288ea90        3 weeks ago         207MB
nginx               latest              3f8a4339aadd        5 weeks ago         108MB

查看镜像的详细信息

[root@docker01 ~]# docker image inspect centos

容器的日常管理

容器的起/停

最简单的运行一个容器:
[root@docker01 ~]# docker run nginx

创建容器,两步走(不常用):
[root@docker01 ~]# docker create centos:latest  /bin/bash
bb7f32368ecf0492adb59e20032ab2e6cf6a563a0e6751e58930ee5f7aaef204
[root@docker01 ~]# docker start stupefied_nobel
stupefied_nobel

快速启动容器方法:
[root@docker01 ~]# docker run  centos:latest  /usr/bin/sleep 20;

容器内的第一个进程必须一直处于运行的状态,否则这个容器,就会处于退出状态!

查看正在运行的容器:
[root@docker01 ~]# docker container ls

[root@docker01 ~]# docker ps 
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
8708e93fd767        nginx               "nginx -g 'daemon of…"   6 seconds ago       Up 4 seconds        80/tcp              keen_lewin

查看你容器详细信息/ip:
[root@docker01 ~]# docker container  inspect  容器名称/id

查看你所有容器(包括未运行的):
[root@docker01 ~]# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS               NAMES
8708e93fd767        nginx               "nginx -g 'daemon of…"   4 minutes ago       Exited (0) 59 seconds ago                       keen_lewin
f9f3e6af7508        nginx               "nginx -g 'daemon of…"   5 minutes ago       Exited (0) 5 minutes ago                        optimistic_haibt
8d8f81da12b5        nginx               "nginx -g 'daemon of…"   3 hours ago         Exited (0) 3 hours ago                          lucid_bohr

停止容器:
[root@docker01 ~]# docker stop 容器名称/id 

[root@docker01 ~]# docker container  kill  容器名称/id

进入容器方法

启动时进去方法:
[root@docker01 ~]# docker run -it #参数:-it 可交互终端
[root@docker01 ~]# docker run -it nginx:latest  /bin/bash
root@79241093859e:/#

退出/离开容器:
ctrl+p & ctrl+q

启动后进入容器的方法:

启动一个Docker:
[root@docker01 ~]# docker run -it centos:latest 
[root@1bf0f43c4d2f /]# ps -ef 
UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 15:47 pts/0    00:00:00 /bin/bash
root         13      1  0 15:47 pts/0    00:00:00 ps -ef

attach进入容器,使用pts/0 ,会让所用通过此方法进如放入用户看到同样的操作:
[root@docker01 ~]# docker attach 1bf0f43c4d2f
[root@1bf0f43c4d2f /]# ps -ef 
UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 15:47 pts/0    00:00:00 /bin/bash
root         14      1  0 15:49 pts/0    00:00:00 ps -ef

自命名启动一个容器--name:
[root@docker01 ~]# docker attach 1bf0f43c4d2f
[root@1bf0f43c4d2f /]# ps -ef 
UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 15:47 pts/0    00:00:00 /bin/bash
root         14      1  0 15:49 pts/0    00:00:00 ps -ef

exrc进入容器方法(推荐使用):
[root@docker01 ~]# docker exec -it clsn1  /bin/bash 
[root@b20fa75b4b40 /]# 重新分配一个终端
[root@b20fa75b4b40 /]# ps -ef 
UID         PID   PPID  C STIME TTY          TIME CMD
root          1      0  0 16:11 pts/0    00:00:00 /bin/bash
root         13      0  0 16:14 pts/1    00:00:00 /bin/bash
root         26     13  0 16:14 pts/1    00:00:00 ps -ef

删除所有容器

[root@docker01 ~]# docker rm -f  `docker ps -a -q`
# -f 强制删除

启动时进行端口映射

-p参数端口映射:
[root@docker01 ~]# docker run -d -p 8888:80  nginx:latest 
287bec5c60263166c03e1fc5b0b8262fe76507be3dfae4ce5cd2ee2d1e8a89a9

不同指定映射方法:
8.png

随机映射:
docker run -P (大P)# 需要镜像支持

Docker数据卷的管理

挂载时创建卷

挂载卷:
[root@docker01 ~]# docker run -d -p 80:80 -v /data:/usr/share/nginx/html nginx:latest
079786c1e297b5c5031e7a841160c74e91d4ad06516505043c60dbb78a259d09

容器内站点目录:/usr/share/nginx/html。

在宿主机写入数据,查看:
[root@docker01 ~]# echo "http://www.nmtui.com" >/data/index.html
[root@docker01 ~]# curl 10.0.0.100
http://www.nmtui.com

设置共享卷,使用同一个卷启动一个新的容器:
[root@docker01 ~]# docker run -d -p 8080:80 -v /data:/usr/share/nginx/html nginx:latest 
351f0bd78d273604bd0971b186979aa0f3cbf45247274493d2490527babb4e42
[root@docker01 ~]# curl 10.0.0.100:8080
http://www.nmtui.com

查看卷列表:
[root@docker01 ~]# docker volume ls
DRIVER              VOLUME NAME

创建卷后挂载

创建一个卷:
[root@docker01 ~]# docker volume create 
f3b95f7bd17da220e63d4e70850b8d7fb3e20f8ad02043423a39fdd072b83521
[root@docker01 ~]# docker volume ls 
DRIVER              VOLUME NAME
local               f3b95f7bd17da220e63d4e70850b8d7fb3e20f8ad02043423a39fdd072b83521

指定卷名:
[root@docker01 ~]# docker volume ls 
DRIVER              VOLUME NAME
local               clsn
local               f3b95f7bd17da220e63d4e70850b8d7fb3e20f8ad02043423a39fdd072b83521

查看卷路径:
[root@docker01 ~]# docker volume inspect clsn 
[
{
    "CreatedAt": "2018-02-01T00:39:25+08:00",
    "Driver": "local",
    "Labels": {},
    "Mountpoint": "/var/lib/docker/volumes/clsn/_data",
    "Name": "clsn",
    "Options": {},
    "Scope": "local"
}
]

使用卷创建:
[root@docker01 ~]# docker run -d -p 9000:80 -v clsn:/usr/share/nginx/html nginx:latest 
1434559cff996162da7ce71820ed8f5937fb7c02113bbc84e965845c219d3503
# 宿主机测试
[root@docker01 ~]# echo 'blog.nmtui.com' >/var/lib/docker/volumes/clsn/_data/index.html 
[root@docker01 ~]# curl 10.0.0.100:9000
blog.nmtui.com

设置卷:
[root@docker01 ~]# docker run  -d  -P  --volumes-from 079786c1e297 nginx:latest 
b54b9c9930b417ab3257c6e4a8280b54fae57043c0b76b9dc60b4788e92369fb

查看使用的端口:
[root@docker01 ~]# netstat -lntup 
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1400/sshd           
tcp        0      0 10.0.0.100:2375         0.0.0.0:*               LISTEN      26218/dockerd       
tcp6       0      0 :::9000                 :::*                    LISTEN      32015/docker-proxy  
tcp6       0      0 :::8080                 :::*                    LISTEN      31853/docker-proxy  
tcp6       0      0 :::80                   :::*                    LISTEN      31752/docker-proxy  
tcp6       0      0 :::22                   :::*                    LISTEN      1400/sshd           
tcp6       0      0 :::32769                :::*                    LISTEN      32300/docker-proxy  
[root@docker01 ~]# curl 10.0.0.100:32769
http://www.nmtui.com

手动将容器保存为镜像

本次是基于Docker官方CentOS 6.8镜像创建。

官方镜像列表:https://hub.docker.com/explore/

启动一个CentOS 6.8的镜像:
[root@docker01 ~]# docker pull  centos:6.8
[root@docker01 ~]# docker run -it -p 1022:22 centos:6.8  /bin/bash
# 在容器种安装sshd服务,并修改系统密码
[root@582051b2b92b ~]# yum install  openssh-server -y 
[root@582051b2b92b ~]# echo "root:123456" |chpasswd
[root@582051b2b92b ~]#  /etc/init.d/sshd start

启动完成后镜像ssh连接测试。

将容器提交为镜像:
[root@docker01 ~]# docker commit brave_mcclintock  centos6-ssh

使用新的镜像启动容器:
[root@docker01 ~]# docker run -d  -p 1122:22  centos6-ssh:latest  /usr/sbin/sshd -D 
5b8161fda2a9f2c39c196c67e2eb9274977e7723fe51c4f08a0190217ae93094

在容器安装httpd服务:
[root@5b8161fda2a9 /]#  yum install httpd -y

编写启动脚本脚本:
[root@5b8161fda2a9 /]# cat  init.sh 
#!/bin/bash 
/etc/init.d/httpd start 
/usr/sbin/sshd -D
[root@5b8161fda2a9 /]# chmod +x init.sh 
# 注意执行权限

再次提交为新的镜像:
[root@docker01 ~]# docker commit  5b8161fda2a9 centos6-httpd 
sha256:705d67a786cac040800b8485cf046fd57b1828b805c515377fc3e9cea3a481c1

启动镜像,做好端口映射。并在浏览器中测试访问:
[root@docker01 ~]# docker run -d -p 1222:22 -p 80:80  centos6-httpd /init.sh 
46fa6a06644e31701dc019fb3a8c3b6ef008d4c2c10d46662a97664f838d8c2c

Dockerfile自动构建Docker镜像

官方构建Dockerffile文件参考:https://github.com/CentOS/CentOS-Dockerfiles

Dockerfile指令集

Dockerfile主要组成部分:
  • 基础镜像信息:FROM centos:6.8
  • 制作镜像操作指令:RUN yum insatll openssh-server -y
  • 容器启动时执行指令:CMD ["/bin/bash"]


Dockerfile常用指令:
  • FROM:这个镜像的妈妈是谁(指定基础镜像)
  • MAINTAINER:告诉别人,谁负责养它(指定维护者信息,可以没有)
  • RUN:你想让它干啥(在命令前面加上RUN即可)
  • ADD:给它点创业资金(COPY文件,会自动解压)
  • WORKDIR:我是cd,今天刚化了妆(设置当前工作目录)
  • VOLUME:给它一个存放行李的地方(设置卷,挂载主机目录)
  • EXPOSE:它要打开的门是啥(指定对外的端口)
  • CMD:奔跑吧,兄弟!(指定容器启动后的要干的事情)


Dockerfile其他指令:
  • COPY:复制文件
  • ENV:环境变量
  • ENTRYPOINT:容器启动后执行的命令


创建一个Dockerfile

创建第一个Dockerfile文件:
# 创建目录
[root@docker01 base]# cd /opt/base
# 创建Dcokerfile文件,注意大小写
[root@docker01 base]# vim Dockerfile
FROM centos:6.8
RUN yum install openssh-server -y 
RUN echo "root:123456" |chpasswd
RUN /etc/init.d/sshd start 
CMD ["/usr/sbin/sshd","-D"]

构建Docker镜像:
[root@docker01 base]# docker image build  -t centos6.8-ssh . 
-t 为镜像标签打标签, . 表示当前路径

使用自构建的镜像启动:
[root@docker01 base]# docker run  -d -p 2022:22 centos6.8-ssh-b 
dc3027d3c15dac881e8e2aeff80724216f3ac725f142daa66484f7cb5d074e7a

使用Dcokerfile安装KodExplorer

Dockerfile文件内容:
FROM centos:6.8
RUN yum install wget unzip php php-gd php-mbstring -y && yum clean all
# 设置工作目录,之后的操作都在这个目录中
WORKDIR /var/www/html/
RUN wget -c http://static.kodcloud.com/update/download/kodexplorer4.25.zip
RUN unzip kodexplorer4.25.zip && rm -f kodexplorer4.25.zip
RUN chown -R apache.apache .
CMD ["/usr/sbin/apachectl","-D","FOREGROUND"]

更多的Dockerfile可以参考官方方法。

Docker中的镜像分层

参考文档:http://www.maiziedu.com/wiki/cloud/dockerimage/

Docker支持通过扩展现有镜像,创建新的镜像。实际上,Docker Hub中99%的镜像都是通过在base镜像中安装和配置需要的软件构建出来的。
9.png

从上图可以看到,新镜像是从base镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。

Docker镜像为什么分层

镜像分层最大的一个好处就是共享资源。

比如说有多个镜像都从相同的base镜像构建而来,那么Docker Host只需在磁盘上保存一份base镜像;同时内存中也只需加载一份base镜像,就可以为所有容器服务了。而且镜像的每一层都可以被共享。

如果多个容器共享一份基础镜像,当某个容器修改了基础镜像的内容,比如/etc下的文件,这时其他容器的/etc是不会被修改的,修改只会被限制在单个容器内。这就是容器Copy-on-Write特性。

可写的容器层

当容器启动时,一个新的可写层被加载到镜像的顶部。这一层通常被称作“容器层”,“容器层”之下的都叫“镜像层”。
10.png

所有对容器的改动——无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的。

容器层的细节说明

镜像层数量可能会很多,所有镜像层会联合在一起组成一个统一的文件系统。如果不同层中有一个相同路径的文件,比如/a,上层的/a会覆盖下层的/a,也就是说用户只能访问到上层中的文件/a。在容器层中,用户看到的是一个叠加之后的文件系统。

文件操作的说明:
11.png

只有当需要修改时才复制一份数据,这种特性被称作Copy-on-Write。可见,容器层保存的是镜像变化的部分,不会对镜像本身进行任何修改。

这样就解释了我们前面提出的问题:容器层记录对镜像的修改,所有镜像层都是只读的,不会被容器修改,所以镜像可以被多个容器共享。

使用Docker运行zabbix-server

容器间的互联

在运行Zabbix之前务必要了解容器间互联的方法:
# 创建一个Nginx容器
docker run -d -p 80:80 nginx
# 创建容器,做link,并进入容器中
docker run -it --link quirky_brown:web01 centos-ssh /bin/bash
# 在容器中访问Nginx容器可以ping通
ping web01    

命令执行过程:
# 启动apache容器
[root@docker01 ~]# docker run -d httpd:2.4  
3f1f7fc554720424327286bd2b04aeab1b084a3fb011a785b0deab6a34e56955
^[[A[root@docker01 docker ps -a
CONTAINER ID        IMAGE               COMMAND              CREATED             STATUS              PORTS               NAMES
3f1f7fc55472        httpd:2.4           "httpd-foreground"   6 seconds ago       Up 5 seconds        80/tcp              determined_clarke
# 拉取一个busybox 镜像
[root@docker01 ~]# docker pull busybox 
# 启动容器
[root@docker01 ~]# docker run -it  --link determined_clarke:web busybox:latest   /bin/sh 
/ # 
# 使用新的容器访问最初的web容器
/ # ping web 
PING web (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.058 ms
^C
--- web ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.058/0.058/0.058 ms

启动Zabbix容器

启动一个MySQL的容器:
docker run --name mysql-server -t \
  -e MYSQL_DATABASE="zabbix" \
  -e MYSQL_USER="zabbix" \
  -e MYSQL_PASSWORD="zabbix_pwd" \
  -e MYSQL_ROOT_PASSWORD="root_pwd" \
  -d mysql:5.7 \
  --character-set-server=utf8 --collation-server=utf8_bin

启动java-gateway容器监控Java服务:
docker run --name zabbix-java-gateway -t \
  -d zabbix/zabbix-java-gateway:latest

启动zabbix-mysql容器使用link连接MySQL与java-gateway:
docker run --name zabbix-server-mysql -t \
  -e DB_SERVER_HOST="mysql-server" \
  -e MYSQL_DATABASE="zabbix" \
  -e MYSQL_USER="zabbix" \
  -e MYSQL_PASSWORD="zabbix_pwd" \
  -e MYSQL_ROOT_PASSWORD="root_pwd" \
  -e ZBX_JAVAGATEWAY="zabbix-java-gateway" \
  --link mysql-server:mysql \
  --link zabbix-java-gateway:zabbix-java-gateway \
  -p 10051:10051 \
  -d zabbix/zabbix-server-mysql:latest

启动Zabbix Web显示,使用link连接zabbix-mysql与MySQL:
docker run --name zabbix-web-nginx-mysql -t \
  -e DB_SERVER_HOST="mysql-server" \
  -e MYSQL_DATABASE="zabbix" \
  -e MYSQL_USER="zabbix" \
  -e MYSQL_PASSWORD="zabbix_pwd" \
  -e MYSQL_ROOT_PASSWORD="root_pwd" \
  --link mysql-server:mysql \
  --link zabbix-server-mysql:zabbix-server \
  -p 80:80 \
  -d zabbix/zabbix-web-nginx-mysql:latest

关于Zabbix API

关于Zabbix API可以参考官方文档:https://www.zabbix.com/documen ... l/api

获取token方法:
# 获取token
[root@docker02 ~]# curl -s -X POST -H 'Content-Type:application/json' -d '
{
"jsonrpc": "2.0",
"method": "user.login",
"params": {
"user": "Admin",
"password": "zabbix"
},
"id": 1
}' http://10.0.0.100/api_jsonrpc.php
{"jsonrpc":"2.0","result":"d3be707f9e866ec5d0d1c242292cbebd","id":1} 

Docker仓库(Registry)

创建一个普通仓库

创建仓库:
docker run -d -p 5000:5000 --restart=always --name registry -v /opt/myregistry:/var/lib/registry  registry 

修改配置文件,使之支持http:
[root@docker01 ~]# cat  /etc/docker/daemon.json 
{
"registry-mirrors": ["https://registry.docker-cn.com"],
"insecure-registries": ["10.0.0.100:5000"]


重启Docker让修改生效:
[root@docker01 ~]# systemctl restart  docker.service

修改镜像标签:
[root@docker01 ~]# docker tag  busybox:latest  10.0.0.100:5000/clsn/busybox:1.0
[root@docker01 ~]# docker images
REPOSITORY                      TAG                 IMAGE ID            CREATED             SIZE
centos6-ssh                     latest              3c2b1e57a0f5        18 hours ago        393MB
httpd                           2.4                 2e202f453940        6 days ago          179MB
10.0.0.100:5000/clsn/busybox    1.0                 5b0d59026729        8 days ago          1.15MB

将新打标签的镜像上传镜像到仓库:
[root@docker01 ~]# docker push   10.0.0.100:5000/clsn/busybox

带basic认证的仓库

安装加密工具:
[root@docker01 clsn]# yum install httpd-tools  -y

设置认证密码:
mkdir /opt/registry-var/auth/ -p
htpasswd  -Bbn clsn 123456  > /opt/registry-var/auth/htpasswd

启动容器,在启动时传入认证参数:
docker run -d -p 5000:5000 -v /opt/registry-var/auth/:/auth/ -e "REGISTRY_AUTH=htpasswd" -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd registry

使用验证用户测试:
# 登陆用户
[root@docker01 ~]# docker login 10.0.0.100:5000 
Username: clsn  
Password: 123456
Login Succeeded
# 推送镜像到仓库
[root@docker01 ~]# docker push 10.0.0.100:5000/clsn/busybox 
The push refers to repository [10.0.0.100:5000/clsn/busybox]
4febd3792a1f: Pushed 
1.0: digest: sha256:4cee1979ba0bf7db9fc5d28fb7b798ca69ae95a47c5fecf46327720df4ff352d size: 527
#认证文件的保存位置
[root@docker01 ~]# cat .docker/config.json 
{
"auths": {
    "10.0.0.100:5000": {
        "auth": "Y2xzbjoxMjM0NTY="
    },
    "https://index.docker.io/v1/": {
        "auth": "Y2xzbjpIenNAMTk5Ng=="
    }
},
"HttpHeaders": {
    "User-Agent": "Docker-Client/17.12.0-ce (linux)"
}


至此,一个简单的Docker镜像仓库搭建完成。

docker-compose编排工具

安装docker-compose

安装docker-compose:
# 下载pip软件
yum install -y python2-pip
# 下载 docker-compose
pip install docker-compose

国内开启pip下载加速:http://mirrors.aliyun.com/help/pypi
mkdir ~/.pip/
cat > ~/.pip/pip.conf <<'EOF'
[global]
index-url = https://mirrors.aliyun.com/pypi/simple/
[install]
trusted-host=mirrors.aliyun.com
EOF

编排启动镜像

创建文件目录:
[root@docker01 ~]# mkdir /opt/my_wordpress/
[root@docker01 ~]# cd /opt/my_wordpress/

编写编排文件:
[root@docker01 my_wordpress]# vim docker-compose.yml
version: '3'
services:
db:
 image: mysql:5.7
 volumes:
   - /data/db_data:/var/lib/mysql
 restart: always
 environment:
   MYSQL_ROOT_PASSWORD: somewordpress
   MYSQL_DATABASE: wordpress
   MYSQL_USER: wordpress
   MYSQL_PASSWORD: wordpress
wordpress:
 depends_on:
   - db
 image: wordpress:latest
 volumes:
   - /data/web_data:/var/www/html
 ports: 
   - "8000:80"
 restart: always
 environment:
   WORDPRESS_DB_HOST: db:3306
   WORDPRESS_DB_USER: wordpress
   WORDPRESS_DB_PASSWORD: wordpress

启动:
[root@docker01 my_wordpress]# docker-compose up
  #启动方法:docker-compose up
  #后台启动方法:docker-compose up -d

浏览器上访问http://10.0.0.100:8000

进行wordpress的安装即可。

HAProxy代理后端Docker容器

修改编排脚本:
[root@docker01 my_wordpress]# cat docker-compose.yml 
version: '3'
services:
db:
 image: mysql:5.7
 volumes:
   - /data/db_data:/var/lib/mysql
 restart: always
 environment:
   MYSQL_ROOT_PASSWORD: somewordpress
   MYSQL_DATABASE: wordpress
   MYSQL_USER: wordpress
   MYSQL_PASSWORD: wordpress
wordpress:
 depends_on:
   - db
 image: wordpress:latest
 volumes:
   - /data/web_data:/var/www/html
 ports: 
   - "80"
 restart: always
 environment:
   WORDPRESS_DB_HOST: db:3306
   WORDPRESS_DB_USER: wordpress
   WORDPRESS_DB_PASSWORD: wordpress

同时启动两台wordpress:
[root@docker01 my_wordpress]# docker-compose scale wordpress=2 
WARNING: The scale command is deprecated. Use the up command with the --scale flag instead.
Starting mywordpress_wordpress_1 ... done
Creating mywordpress_wordpress_2 ... done

安装HAProxy:
[root@docker01 ~]# yum install haproxy -y

修改HAProxy配置文件。

关于配置文件的详细说明,参考:https://www.cnblogs.com/MacoLee/p/5853413.html
[root@docker01 ~]#cp /etc/haproxy/haproxy.cfg{,.bak}
[root@docker01 ~]# vim /etc/haproxy/haproxy.cfg
global
log         127.0.0.1 local2
chroot      /var/lib/haproxy
pidfile     /var/run/haproxy.pid
maxconn     4000
user        haproxy
group       haproxy
daemon
stats socket /var/lib/haproxy/stats level admin  #支持命令行控制
defaults
mode                    http
log                     global
option                  httplog
option                  dontlognull
option http-server-close
option forwardfor       except 127.0.0.0/8
option                  redispatch
retries                 3
timeout http-request    10s
timeout queue           1m
timeout connect         10s
timeout client          1m
timeout server          1m
timeout http-keep-alive 10s
timeout check           10s
maxconn                 3000
listen stats
mode http
bind 0.0.0.0:8888
stats enable
stats uri     /haproxy-status 
stats auth    admin:123456
frontend frontend_www_example_com
bind 10.0.0.100:8000
mode http
option httplog
log global
default_backend backend_www_example_com
backend backend_www_example_com
option forwardfor header X-REAL-IP
option httpchk HEAD / HTTP/1.0
balance roundrobin
server web-node1  10.0.0.100:32768 check inter 2000 rise 30 fall 15
server web-node2  10.0.0.100:32769 check inter 2000 rise 30 fall 15

启动HAProxy:
systemctl start haproxy
systemctl enable haproxy

使用浏览器访问HAProxy监听的8000端口可以看到负载的情况:
12.png

使用浏览器访问http://10.0.0.100:8888/haproxy-status可以看到后端节点的监控状况:
13.png

安装Socat直接操作socket控制HAProxy

安装软件:
yum install socat.x86_64 -y

查看帮助:
[root@docker01 web_data]# echo "help"|socat stdio /var/lib/haproxy/stats

下线后端节点:
echo "disable server backend_www_example_com/web-node2"|socat stdio /var/lib/haproxy/stats

上线后端节点:
echo "enable server backend_www_example_com/web-node3"|socat stdio /var/lib/haproxy/stats

编写PHP测试页,放到/data/web_data下,在浏览器中访问可以查看当前的节点:
[root@docker01 web_data]# vim check.php
<html>
<head>
    <title>PHP测试</title>
</head>
<body>
    <?php  echo '<p>Hello World </p>'; ?>
    <?php  echo "访问的服务器地址是:"."<fontcolor=red>".$_SERVER['SERVER_ADDR']."</font>"."<br>";
    echo"访问的服务器域名是:"."<fontcolor=red>".$_SERVER['SERVER_NAME']."</font>"."<br>";
    ?>
</body>
</html>

重启Docker服务,容器全部退出的解决办法

在启动是指定自动重启

docker run  --restart=always

修改Docker默认配置文件

# 添加上下面这行
"live-restore": true
docker server配置文件/etc/docker/daemon.json参考

[root@docker02 ~]# cat  /etc/docker/daemon.json 
{
"registry-mirrors": ["https://registry.docker-cn.com"],
"graph": "/opt/mydocker", # 修改数据的存放目录到/opt/mydocker/,原/var/lib/docker/
"insecure-registries": ["10.0.0.100:5000"],
"live-restore": true


重启生效,只对在此之后启动的容器生效:
[root@docker01 ~]# systemctl restart  docker.service

Docker网络类型

14.png

Docker的网络类型

15.png

Bridge默认Docker网络隔离基于网络命名空间,在物理机上创建Docker容器时会为每一个Docker容器分配网络命名空间,并且把容器IP桥接到物理机的虚拟网桥上。

不为容器配置网络功能

此模式下创建容器是不会为容器配置任何网络参数的,如:容器网卡、IP、通信路由等,全部需要自己去配置。
[root@docker01 ~]# docker run  -it --network none busybox:latest  /bin/sh 
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue 
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
   valid_lft forever preferred_lft forever

与其他容器共享网络配置(Container)

此模式和Host模式很类似,只是此模式创建容器共享的是其他容器的IP和端口而不是物理机,此模式容器自身是不会配置网络和端口,创建此模式容器进去后,你会发现里边的IP是你所指定的那个容器IP并且端口也是共享的,而且其它还是互相隔离的,如进程等。
[root@docker01 ~]# docker run  -it --network container:mywordpress_db_1  busybox:latest  /bin/sh 
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue 
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
   valid_lft forever preferred_lft forever
105: eth0@if106: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.3/16 brd 172.18.255.255 scope global eth0
   valid_lft forever preferred_lft forever

使用宿主机网络

此模式创建的容器没有自己独立的网络命名空间,是和物理机共享一个Network Namespace,并且共享物理机的所有端口与IP,并且这个模式认为是不安全的。
[root@docker01 ~]# docker run  -it --network host  busybox:latest  /bin/sh

查看网络列表

[root@docker01 ~]# docker network list 
NETWORK ID          NAME                  DRIVER              SCOPE
b15e8a720d3b        bridge                bridge              local
345d65b4c2a0        host                  host                local
bc5e2a32bb55        mywordpress_default   bridge              local
ebf76eea91bb        none                  null                local

用PIPEWORK为Docker容器配置独立IP

参考文档:http://blog.csdn.net/design321 ... 64825

官方网站:https://github.com/jpetazzo/pipework

宿主环境:CentOS 7.2

安装pipework:
wget https://github.com/jpetazzo/pipework/archive/master.zip
unzip master.zip 
cp pipework-master/pipework  /usr/local/bin/
chmod +x /usr/local/bin/pipework

配置桥接网卡。

安装桥接工具:
yum install bridge-utils.x86_64 -y

修改网卡配置,实现桥接:
# 修改eth0配置,让br0实现桥接
[root@docker01 ~]# cat /etc/sysconfig/network-scripts/ifcfg-eth0 
TYPE=Ethernet
BOOTPROTO=static
NAME=eth0
DEVICE=eth0
ONBOOT=yes
BRIDGE=br0
[root@docker01 ~]# cat /etc/sysconfig/network-scripts/ifcfg-br0 
TYPE=Bridge
BOOTPROTO=static
NAME=br0
DEVICE=br0
ONBOOT=yes
IPADDR=10.0.0.100
NETMASK=255.255.255.0
GATEWAY=10.0.0.254
DNS1=223.5.5.5
# 重启网络
[root@docker01 ~]# /etc/init.d/network restart

运行一个容器镜像测试:
pipework br0 $(docker run -d -it -p 6880:80 --name  httpd_pw httpd) 10.0.0.220/24@10.0.0.254

在其他主机上测试端口及连通性:
[root@docker01 ~]# curl 10.0.0.220
<html><body><h1>It works!</h1></body></html>
[root@docker01 ~]# ping 10.0.0.220 -c 1
PING 10.0.0.220 (10.0.0.220) 56(84) bytes of data.
64 bytes from 10.0.0.220: icmp_seq=1 ttl=64 time=0.043 ms

再运行一个容器,设置网路类型为none:
pipework br0 $(docker run -d -it --net=none --name test httpd:2.4) 10.0.0.221/24@10.0.0.254

进行访问测试:
[root@docker01 ~]# curl 10.0.0.221
<html><body><h1>It works!</h1></body></html>

重启容器后需要再次指定:
pipework br0 testduliip  172.16.146.113/24@172.16.146.1
pipework br0 testduliip01  172.16.146.112/24@172.16.146.1

Dcoker跨主机通信之overlay可以参考:http://www.cnblogs.com/CloudMan6/p/7270551.html

Docker跨主机通信之Macvlan

创建网络:
[root@docker01 ~]# docker network  create --driver macvlan  --subnet 10.1.0.0/24 --gateway 10.1.0.254 -o parent=eth0  macvlan_1
33a1f41dcc074f91b5bd45e7dfedabfb2b8ec82db16542f05213839a119b62ca

设置网卡为混杂模式:
ip link set eth0 promisc on

创建使用Macvlan网络容器:
[root@docker02 ~]# docker run  -it --network macvlan_1  --ip=10.1.0.222 busybox /bin/sh

Docker企业级镜像仓库Harbor

容器管理:
[root@docker01 harbor]# pwd
/opt/harbor
[root@docker01 harbor]# docker-compose stop

安装Docker、docker-compose。

下载Harbor:
cd /opt && https://storage.googleapis.com/harbor-releases/harbor-offline-installer-v1.3.0.tgz
tar xf harbor-offline-installer-v1.3.0.tgz

修改主机及Web界面密码:
[root@docker01 harbor]# vim harbor.cfg 
···
hostname = 10.0.0.100
harbor_admin_password = Harbor12345
···

执行安装脚本:
[root@docker01 harbor]# ./install.sh

浏览器访问 http://10.0.0.11
16.png

添加一个项目:
17.png

镜像推送到仓库的指定项目:
[root@docker02 ~]# docker  tag centos:6.8  10.0.0.100/clsn/centos6.8:1.0
[root@docker02 ~]#  
[root@docker02 ~]# docker images 
REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
busybox                     latest              5b0d59026729        8 days ago          1.15MB
10.0.0.100/clsn/centos6.8   1.0                 6704d778b3ba        2 months ago        195MB
centos                      6.8                 6704d778b3ba        2 months ago        195MB
[root@docker02 ~]# docker login 10.0.0.100
Username: admin
Password: 
Login Succeeded

推送镜像:
[root@docker02 ~]# docker push 10.0.0.100/clsn/centos6.8 
The push refers to repository [10.0.0.100/clsn/centos6.8]
e00c9229b481: Pushing  13.53MB/194.5MB

在Web界面里查看:
18.png

使用容器的建议

  1. 不要以拆分方式进行应用程序发布
  2. 不要创建大型镜像
  3. 不要在单个容器中运行多个进程
  4. 不要再镜像内保存凭证,不要依赖IP地址
  5. 以非root用户运行进程
  6. 不要使用“最新”标签
  7. 不要利用运行中的容器创建镜像
  8. 不要使用单层镜像
  9. 不要将数据存放在容器内


关于Docker容器的监控

容器的基本信息:包括容器的数量、ID、名称、镜像、启动命令、端口等信息。

容器的运行状态:统计各状态的容器的数量,包括运行中、暂停、停止及异常退出。

容器的用量信息:统计容器的CPU使用率、内存使用量、块设备I/O使用量、网络使用情况等资源的使用情况。

原文链接:https://www.cnblogs.com/clsn/p/8410309.html 收起阅读 »

Arthas | 定位线上 Dubbo 线程池满异常


作者 | 徐靖峰  阿里云高级开发工程师

前言

Dubbo 线程池满异常应该是大多数 Dubbo 用户都遇到过的一个问题,本文以 Arthas 3.1.7 版本为例,介绍如何针对该异常进行诊断,主要使用到 dashboard / thread 两个指令。

推荐使用 Arthas



Cloud Toolkit 是阿里云发布的免费本地 IDE 插件,帮助开发者更高效地开发、测试、诊断并部署应用。通过插件,可以将本地应用一键部署到任意服务器,甚至云端(ECS、EDAS、ACK、ACR 和 小程序云等);并且还内置了 Arthas 诊断、Dubbo工具、Terminal 终端、文件上传、函数计算 和 MySQL 执行器等工具。不仅仅有 IntelliJ IDEA 主流版本,还有 Eclipse、Pycharm、Maven 等其他版本。


Dubbo 线程池满异常介绍

理解线程池满异常需要首先了解 Dubbo 线程模型,官方文档:http://dubbo.apache.org/zh-cn/ ... .html

简单概括下 Dubbo 默认的线程模型:Dubbo 服务端每次接收到一个 Dubbo 请求,便交给一个线程池处理,该线程池默认有 200 个线程,如果 200 个线程都不处于空闲状态,则客户端会报出如下异常:

Caused by: java.util.concurrent.ExecutionException: org.apache.dubbo.remoting.RemotingException: Server side(192.168.1.101,20880) threadpool is exhausted ...

服务端会打印 WARN 级别的日志:

[DUBBO] Thread pool is EXHAUSTED!

引发该异常的原因主要有以下几点:
  • 客户端/服务端超时时间设置不合理,导致请求无限等待,耗尽了线程数;
  • 客户端请求量过大,服务端无法及时处理,耗尽了线程数;
  • 服务端由于 fullgc 等原因导致处理请求较慢,耗尽了线程数;
  • 服务端由于数据库、Redis、网络 IO 阻塞问题,耗尽了线程数;


原因可能很多,但究其根本,都是因为业务上出了问题,导致 Dubbo 线程池资源耗尽了。所以出现该问题,首先要做的是:排查业务异常。

紧接着针对自己的业务场景对 Dubbo 进行调优:
  • 调整 Provider 端的 dubbo.provider.threads 参数大小,默认 200,可以适当提高。多大算合适?至少 700 不算大;不建议调的太小,容易出现上述问题;
  • 调整 Consumer 端的 dubbo.consumer.actives 参数,控制消费者调用的速率。这个实践中很少使用,仅仅一提;
  • 客户端限流;
  • 服务端扩容;
  • Dubbo 目前不支持给某个 Service 单独配置一个隔离的线程池,用于保护服务,可能在以后的版本中会增加这个特性。


另外,不止 Dubbo 如此设计线程模型,绝大多数服务治理框架、 HTTP 服务器都有业务线程池的概念,所以理论上它们都会有线程池满异常的可能,解决方案也类似。

那既然问题都解释清楚了,我们还需要排查什么呢?

一般在线上,有很多运行中的服务,这些服务都是共享一个 Dubbo 服务端线程池,可能因为某个服务的问题,导致整个应用被拖垮,所以需要排查是不是集中出现在某个服务上,再针对排查这个服务的业务逻辑;需要定位到线程堆栈,揪出导致线程池满的元凶。

定位该问题,我的习惯一般是使用 Arthas 的 dashboardthread 命令,而在介绍这两个命令之前,我们先人为构造一个 Dubbo 线程池满异常的例子。

复现 Dubbo 线程池满异常

配置服务端线程池大小

dubbo.protocol.threads=10

默认大小是 200,不利于重现该异常。

模拟服务端阻塞

```
@Service(version = "1.0.0")
public class DemoServiceImpl implements DemoService {

@Override
public String sayHello(String name) {
sleep();
return "Hello " + name;
}

private void sleep() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
```

sleep 方法模拟了一个耗时操作,主要是为了让服务端线程池耗尽。

客户端多线程访问

for (int i = 0; i &lt; 20; i++) {
new Thread(() -> {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
demoService.sayHello(&quot;Provider&quot;);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}


问题复现

客户端

1.png


(客户端异常)

服务端

2.png


(服务端异常)

问题得以复现,保留该现场,并假设我们并不知晓 sleep 的耗时逻辑,使用 Arthas 来进行排查。

dashboard 命令介绍

$ dashboard

执行效果:

3.png


(dashboard)

可以看到如上所示的面板,显示了一些系统的运行信息,这里主要关注 THREAD 面板,介绍一下各列的含义:
  • ID: Java 级别的线程 ID,注意这个 ID 不能跟 jstack 中的 nativeID 一一对应;
  • NAME: 线程名;
  • GROUP: 线程组名;
  • PRIORITY: 线程优先级, 1~10 之间的数字,越大表示优先级越高;
  • STATE: 线程的状态;
  • CPU%: 线程消耗的 CPU 占比,采样 100ms,将所有线程在这 100ms 内的 CPU 使用量求和,再算出每个线程的 CPU 使用占比;
  • TIME: 线程运行总时间,数据格式为分:秒
  • INTERRUPTED: 线程当前的中断位状态;
  • DAEMON: 是否是 daemon 线程。


在空闲状态下线程应该是处于 WAITING 状态,而因为 sleep 的缘故,现在所有的线程均处于 TIME_WAITING 状态,导致后来的请求被处理时,抛出了线程池满的异常。

在实际排查中,需要抽查一定数量的 Dubbo 线程,记录他们的线程编号,看看它们到底在处理什么服务请求。使用如下命令可以根据线程池名筛选出 Dubbo 服务端线程:

dashboard | grep &quot;DubboServerHandler&quot;

thread 命令介绍

使用 dashboard 筛选出个别线程 id 后,它的使命就完成了,剩下的操作交给 thread 命令来完成。其实,dashboard 中的 thread 模块,就是整合了 thread 命令,但是 dashboard 还可以观察内存和 GC 状态,视角更加全面,所以我个人建议,在排查问题时,先使用 dashboard 纵观全局信息。

thread 使用示例:
  • 查看当前最忙的前 n 个线程


$ thread -n 3

4.png


(thread -n)
  • 显示所有线程信息


$ thread

dashboard 中显示一致。
  • 显示当前阻塞其他线程的线程


$ thread -b
No most blocking thread found!
Affect(row-cnt:0) cost in 22 ms.


这个命令还有待完善,目前只支持找出 synchronized 关键字阻塞住的线程, 如果是 java.util.concurrent.Lock, 目前还不支持。
  • 显示指定状态的线程


$ thread --state TIMED_WAITING

5.png


(thread --state)

线程状态一共有 [RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, NEW, TERMINATED] 6 种。
  • 查看指定线程的运行堆栈


$ thread 46

6.png


(thread ${thread_id})

介绍了几种常见的用法,在实际排查中需要针对我们的现场做针对性的分析,也同时考察了我们对线程状态的了解程度。我这里列举了几种常见的线程状态:
  • 初始(NEW)


新创建了一个线程对象,但还没有调用 start() 方法。
  • 运行(RUNNABLE)


Java 线程将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
  • 阻塞(BLOCKED)


线程阻塞于锁。
  • 等待(WAITING)


进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断):
  1. Object#wait() 且不加超时参数
  2. Thread#join() 且不加超时参数
  3. LockSupport#park()
    • 超时等待(TIMED_WAITING)


该状态不同于 WAITING,它可以在指定的时间后自行返回。
  1. Thread#sleep()
  2. Object#wait() 且加了超时参数
  3. Thread#join() 且加了超时参数
  4. LockSupport#parkNanos()
  5. LockSupport#parkUntil()‘
    • 终止(TERMINATED)


标识线程执行完毕。

状态流转图

7.jpeg


(线程状态)

问题分析

分析线程池满异常并没有通法,需要灵活变通,我们对下面这些 case 一个个分析:
  • 阻塞类问题。例如数据库连接不上导致卡死,运行中的线程基本都应该处于 BLOCKED 或者 TIMED_WAITING 状态,我们可以借助 thread --state 定位到;
  • 繁忙类问题。例如 CPU 密集型运算,运行中的线程基本都处于 RUNNABLE 状态,可以借助于 thread -n 来定位出最繁忙的线程;
  • GC 类问题。很多外部因素会导致该异常,例如 GC 就是其中一个因素,这里就不能仅仅借助于 thread 命令来排查了;
  • 定点爆破。还记得在前面我们通过 grep 筛选出了一批 Dubbo 线程,可以通过 thread ${thread_id} 定向的查看堆栈,如果统计到大量的堆栈都是一个服务时,基本可以断定是该服务出了问题,至于说是该服务请求量突然激增,还是该服务依赖的某个下游服务突然出了问题,还是该服务访问的数据库断了,那就得根据堆栈去判断了。


总结

本文以 Dubbo 线程池满异常作为引子,介绍了线程类问题该如何分析,以及如何通过 Arthas 快速诊断线程问题。有了 Arthas,基本不再需要 jstack 将 16 进制转来转去了,大大提升了诊断速度。

Arthas 第二期征文活动火热进行中

Arthas 官方举行了征文活动,第二期征文活动于 5 月 8 日 - 6 月 8 日举办,如果你有:
  • 使用 Arthas 排查过的问题
  • 对 Arthas 进行源码解读
  • 对 Arthas 提出建议
  • 不限,其它与 Arthas 有关的内容


 欢迎参加征文活动,还有奖品拿哦~点击了解详情
继续阅读 »

作者 | 徐靖峰  阿里云高级开发工程师

前言

Dubbo 线程池满异常应该是大多数 Dubbo 用户都遇到过的一个问题,本文以 Arthas 3.1.7 版本为例,介绍如何针对该异常进行诊断,主要使用到 dashboard / thread 两个指令。

推荐使用 Arthas



Cloud Toolkit 是阿里云发布的免费本地 IDE 插件,帮助开发者更高效地开发、测试、诊断并部署应用。通过插件,可以将本地应用一键部署到任意服务器,甚至云端(ECS、EDAS、ACK、ACR 和 小程序云等);并且还内置了 Arthas 诊断、Dubbo工具、Terminal 终端、文件上传、函数计算 和 MySQL 执行器等工具。不仅仅有 IntelliJ IDEA 主流版本,还有 Eclipse、Pycharm、Maven 等其他版本。


Dubbo 线程池满异常介绍

理解线程池满异常需要首先了解 Dubbo 线程模型,官方文档:http://dubbo.apache.org/zh-cn/ ... .html

简单概括下 Dubbo 默认的线程模型:Dubbo 服务端每次接收到一个 Dubbo 请求,便交给一个线程池处理,该线程池默认有 200 个线程,如果 200 个线程都不处于空闲状态,则客户端会报出如下异常:

Caused by: java.util.concurrent.ExecutionException: org.apache.dubbo.remoting.RemotingException: Server side(192.168.1.101,20880) threadpool is exhausted ...

服务端会打印 WARN 级别的日志:

[DUBBO] Thread pool is EXHAUSTED!

引发该异常的原因主要有以下几点:
  • 客户端/服务端超时时间设置不合理,导致请求无限等待,耗尽了线程数;
  • 客户端请求量过大,服务端无法及时处理,耗尽了线程数;
  • 服务端由于 fullgc 等原因导致处理请求较慢,耗尽了线程数;
  • 服务端由于数据库、Redis、网络 IO 阻塞问题,耗尽了线程数;


原因可能很多,但究其根本,都是因为业务上出了问题,导致 Dubbo 线程池资源耗尽了。所以出现该问题,首先要做的是:排查业务异常。

紧接着针对自己的业务场景对 Dubbo 进行调优:
  • 调整 Provider 端的 dubbo.provider.threads 参数大小,默认 200,可以适当提高。多大算合适?至少 700 不算大;不建议调的太小,容易出现上述问题;
  • 调整 Consumer 端的 dubbo.consumer.actives 参数,控制消费者调用的速率。这个实践中很少使用,仅仅一提;
  • 客户端限流;
  • 服务端扩容;
  • Dubbo 目前不支持给某个 Service 单独配置一个隔离的线程池,用于保护服务,可能在以后的版本中会增加这个特性。


另外,不止 Dubbo 如此设计线程模型,绝大多数服务治理框架、 HTTP 服务器都有业务线程池的概念,所以理论上它们都会有线程池满异常的可能,解决方案也类似。

那既然问题都解释清楚了,我们还需要排查什么呢?

一般在线上,有很多运行中的服务,这些服务都是共享一个 Dubbo 服务端线程池,可能因为某个服务的问题,导致整个应用被拖垮,所以需要排查是不是集中出现在某个服务上,再针对排查这个服务的业务逻辑;需要定位到线程堆栈,揪出导致线程池满的元凶。

定位该问题,我的习惯一般是使用 Arthas 的 dashboardthread 命令,而在介绍这两个命令之前,我们先人为构造一个 Dubbo 线程池满异常的例子。

复现 Dubbo 线程池满异常

配置服务端线程池大小

dubbo.protocol.threads=10

默认大小是 200,不利于重现该异常。

模拟服务端阻塞

```
@Service(version = "1.0.0")
public class DemoServiceImpl implements DemoService {

@Override
public String sayHello(String name) {
sleep();
return "Hello " + name;
}

private void sleep() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}
```

sleep 方法模拟了一个耗时操作,主要是为了让服务端线程池耗尽。

客户端多线程访问

for (int i = 0; i &lt; 20; i++) {
new Thread(() -> {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
demoService.sayHello(&quot;Provider&quot;);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}


问题复现

客户端

1.png


(客户端异常)

服务端

2.png


(服务端异常)

问题得以复现,保留该现场,并假设我们并不知晓 sleep 的耗时逻辑,使用 Arthas 来进行排查。

dashboard 命令介绍

$ dashboard

执行效果:

3.png


(dashboard)

可以看到如上所示的面板,显示了一些系统的运行信息,这里主要关注 THREAD 面板,介绍一下各列的含义:
  • ID: Java 级别的线程 ID,注意这个 ID 不能跟 jstack 中的 nativeID 一一对应;
  • NAME: 线程名;
  • GROUP: 线程组名;
  • PRIORITY: 线程优先级, 1~10 之间的数字,越大表示优先级越高;
  • STATE: 线程的状态;
  • CPU%: 线程消耗的 CPU 占比,采样 100ms,将所有线程在这 100ms 内的 CPU 使用量求和,再算出每个线程的 CPU 使用占比;
  • TIME: 线程运行总时间,数据格式为分:秒
  • INTERRUPTED: 线程当前的中断位状态;
  • DAEMON: 是否是 daemon 线程。


在空闲状态下线程应该是处于 WAITING 状态,而因为 sleep 的缘故,现在所有的线程均处于 TIME_WAITING 状态,导致后来的请求被处理时,抛出了线程池满的异常。

在实际排查中,需要抽查一定数量的 Dubbo 线程,记录他们的线程编号,看看它们到底在处理什么服务请求。使用如下命令可以根据线程池名筛选出 Dubbo 服务端线程:

dashboard | grep &quot;DubboServerHandler&quot;

thread 命令介绍

使用 dashboard 筛选出个别线程 id 后,它的使命就完成了,剩下的操作交给 thread 命令来完成。其实,dashboard 中的 thread 模块,就是整合了 thread 命令,但是 dashboard 还可以观察内存和 GC 状态,视角更加全面,所以我个人建议,在排查问题时,先使用 dashboard 纵观全局信息。

thread 使用示例:
  • 查看当前最忙的前 n 个线程


$ thread -n 3

4.png


(thread -n)
  • 显示所有线程信息


$ thread

dashboard 中显示一致。
  • 显示当前阻塞其他线程的线程


$ thread -b
No most blocking thread found!
Affect(row-cnt:0) cost in 22 ms.


这个命令还有待完善,目前只支持找出 synchronized 关键字阻塞住的线程, 如果是 java.util.concurrent.Lock, 目前还不支持。
  • 显示指定状态的线程


$ thread --state TIMED_WAITING

5.png


(thread --state)

线程状态一共有 [RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, NEW, TERMINATED] 6 种。
  • 查看指定线程的运行堆栈


$ thread 46

6.png


(thread ${thread_id})

介绍了几种常见的用法,在实际排查中需要针对我们的现场做针对性的分析,也同时考察了我们对线程状态的了解程度。我这里列举了几种常见的线程状态:
  • 初始(NEW)


新创建了一个线程对象,但还没有调用 start() 方法。
  • 运行(RUNNABLE)


Java 线程将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
  • 阻塞(BLOCKED)


线程阻塞于锁。
  • 等待(WAITING)


进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断):
  1. Object#wait() 且不加超时参数
  2. Thread#join() 且不加超时参数
  3. LockSupport#park()
    • 超时等待(TIMED_WAITING)


该状态不同于 WAITING,它可以在指定的时间后自行返回。
  1. Thread#sleep()
  2. Object#wait() 且加了超时参数
  3. Thread#join() 且加了超时参数
  4. LockSupport#parkNanos()
  5. LockSupport#parkUntil()‘
    • 终止(TERMINATED)


标识线程执行完毕。

状态流转图

7.jpeg


(线程状态)

问题分析

分析线程池满异常并没有通法,需要灵活变通,我们对下面这些 case 一个个分析:
  • 阻塞类问题。例如数据库连接不上导致卡死,运行中的线程基本都应该处于 BLOCKED 或者 TIMED_WAITING 状态,我们可以借助 thread --state 定位到;
  • 繁忙类问题。例如 CPU 密集型运算,运行中的线程基本都处于 RUNNABLE 状态,可以借助于 thread -n 来定位出最繁忙的线程;
  • GC 类问题。很多外部因素会导致该异常,例如 GC 就是其中一个因素,这里就不能仅仅借助于 thread 命令来排查了;
  • 定点爆破。还记得在前面我们通过 grep 筛选出了一批 Dubbo 线程,可以通过 thread ${thread_id} 定向的查看堆栈,如果统计到大量的堆栈都是一个服务时,基本可以断定是该服务出了问题,至于说是该服务请求量突然激增,还是该服务依赖的某个下游服务突然出了问题,还是该服务访问的数据库断了,那就得根据堆栈去判断了。


总结

本文以 Dubbo 线程池满异常作为引子,介绍了线程类问题该如何分析,以及如何通过 Arthas 快速诊断线程问题。有了 Arthas,基本不再需要 jstack 将 16 进制转来转去了,大大提升了诊断速度。

Arthas 第二期征文活动火热进行中

Arthas 官方举行了征文活动,第二期征文活动于 5 月 8 日 - 6 月 8 日举办,如果你有:
  • 使用 Arthas 排查过的问题
  • 对 Arthas 进行源码解读
  • 对 Arthas 提出建议
  • 不限,其它与 Arthas 有关的内容


 欢迎参加征文活动,还有奖品拿哦~点击了解详情收起阅读 »

用敏捷的DevOps拳打研发低效、脚踢管控不足


在为客户进行DevOps咨询和提供解决方案时,除了“又快又好”的发布之外,我们发现客户通常还有两大方面需求:开发测试管理问题和运行管理问题。以某大型金融企业为例,该企业开发测试问题主要表现为研发过程的管控不足,这带来了开发效率低、版本质量差、环境交付慢等一系列问题。另外,整个运行环境缺乏统一管理,资源申请和获取周期长,资源利用率低也是当前运行管理中频繁出现的问题。

微信截图_20200525110806.png


问题1
研发过程无法清晰度量、查看和分析
流程规范不标准,各项目各自为战
缺少统一的研发管理支撑工具,已有的流程规范无法有效落实

解决方案

流程的平台化、固化,自动驱动流程运转

不同的公司有划分阶段不同,同一个公司也有可能会分为不同的阶段。在整个流程体系下,每个角色、每个人要做什么是DevOps落地非常重要的一个关键点。通过平台去把所有的规范固化,需要在流程的每个阶段,把每个人的工作职责,需要遵守的规范,甚至是考核指标、度量数据都确认下来。这样每个人能清晰的了解自己的职责,按图索骥去完成自己的工作。

微信截图_20200525111250.png


规范的落地是DevOps能够正常运转的一个重点。从立项到整个需求任务、开发测试、发布上线的过程里,大概50%能规范化并落地到平台中,另外还有些工作例如会议需要人员管控。每个企业中可能有1-2套标准流程,不仅要匹配不同客户的需求,和同一企业中不同项目的需求,还有随着企业对DevOps认识的不断提升,流程也会随着认知不断演进。流程的自定义和灵活性就显得非常重要。

如何自动驱动流程也是一个关键。博云为该企业设计了自定义工作流引擎,整个流程的固化,从前期的准备到后面的需求、研发、不同的测试阶段,都可以通过devops平台上企业自定义的研发工作流来驱动。它不仅能展现当前标准流程的进度,同时企业能够用标准流程去驱动底层的工具链与整个研发管理过程。不管从需求、评审或是运维等角度,整个底层、研发流程的状态变化能够驱动流程和工作引擎的状态变化。例如从需求待开发进入开发阶段后,整个版本也从待规划阶段自动变化到开发阶段。

问题2
开发测试环境搭建交付慢,耗时长
同一业务系统跨多平台部署,效率低,耗时长,管理复杂
物理机、虚拟机、容器各自管理,没有统一视图

解决方案

多视角的环境管理

环境管理既重要又很困难。首先底层环境有很多种,可能需要与虚机环境,跟云平台、云管平台对接,还有容器化环境等不同的环境。其次,环境管理有时候是有项目视角的,有时候为这个项目分配了多少环境,同时项目里还有应用、服务、系统等概念,项目中可能有三套系统,并给这三套系统分配了不同的环境。还有一种是按最小粒度去分配,例如有5个服务,每个服务有不同的环境。这几个情况都有可能会出现。从环境角度来看,应用服务是个核心视角,也有项目视角,所以我们为客户设计了树状结构的多视角环境管理支持。这个树状结构有项目—应用—服务三级视角。

现在的研发管理流程中,包含非常多的对象。从项目视角来看,它注重的是开发阶段,需求任务缺陷和非常重要的研发管理的项。从代码提交开始,它的管理对象是应用和服务,比如代码是和哪个服务关联,制品是和哪个服务关联,pipeline和部署也是与应用服务关联。也就是说项目/版本,应用/服务是我们两个核心的管理对象,那如何才能实现兼容这两种方式?

我们在实践中发现,以应用服务视角去统一或以项目视角去统一这两点都是需要的。在代码之前需要的是项目视角,在代码之后最核心的是应用服务视角。通过应用服务去关联代码、环境、pipeline、制品,这是最自然的也是最好用的一套逻辑。还有一个就是人员,除了角色对象这些东西外,它核心也会体现在所有绩效、工作量和团队。

微信截图_20200525112441.png


环境管理的落地流程从整个环境规划开始。在流程的前端,一开始就给项目申请环境与配额,最后环境细化的分配到各个项目和应用中去。然后通过pipeline的自动化把环境部署起来,由环境申请到环境释放这样一个过程,底层与各种各样的资源平台去对接。

整个资源环境管理的设计能够兼容各种各样的需求。应用本身是可能有很多环境的,我们实现了一个应用能随时创建一套自己可用的环境,在整个资源平台与不同的容器平台、虚机环境、云管平台上创建的项目相匹配,并进行多个关联。

问题3

缺乏开发测试发布环境的质量管控
缺乏开发规范或者规范难落地,代码质量差
编译打包部署自动化程度低,效率低,易出错
解决方案

以版本为核心的过程管理和追溯

以版本为核心的过程管理和追溯,我们理解是整个DevOps管理设计或是研发管理过程的核心的需求。从需求开始,代码、制品、包括线上的实例,都是能与版本进行关联。但一直以来,很多企业的研发管理过程是做不到这点的。

我们通过DevOps平台的整体自动化驱动,以版本为核心,从整个的从需求到代码、制品、构建、部署和实例的管理过程给支撑起来,将它与关联项进行匹配,使信息实现同步。这样能实现线上运行的版本对应它的需求,能够清晰的了解制品关联哪些需求和版本。这样能够解决整个线上版本和线下版本或不同测试环境里的版本不一致的问题。

微信截图_20200525113744.png


这里关键的核心点是研发提测过程。整个研发过程中,研发提测把需求圈出来告诉测试,圈完之后就会走自己的pipeline,pipeline最后生成制品,这样就实现了制品与需求的关联。平台还有提测单,提测单中不仅有制品和需求的信息,还会有质量检查的信息。这个信息会一直跟着流转到整个的版本测试报告中,也就是说我们不但知道这次测试的报告,还能知道所有中间过程中安全检查和自动化测试的结果,是清晰可见的,这是一个关联和信息匹配的过程。

图片1.png


另外一个就是以版本为桥梁打通开发测试和生产环境。在开发测试和生产的过渡过程中,版本包含提测单、需求,也包含制品、配置、脚本这些所有的信息,这样从开发测试转化生产时,不仅是把制品转过去,也包含了整个配置和脚本信息,这样不仅能实现制品包移植,它的配置信息、脚本信息与开发测试环境也是一致的。

问题4

企业或项目个性化的问题和需求如何满足

解决方案

强大的配置能力,适应当前与未来

不管是驱动团队运转的自定义工作流,还是自定义的度量指标等功能,DevOps平台都能够随着客户内部团队的演进适应当前的需求还有未来的变化。另外,前台的自定义DevOps门户能够把整个DevOps流程驱动起来并可视化展现出来。

对开发人员来讲,接收到的需求能在界面上看到,做代码也能在同一个界面上看到,制品、部署发布上线,需要处理的流程的工作,都能在门户中心看到。这样就不需要用许多工具和每天登录十多个系统,就按看到的流程走。

整个工具的驱动也是一个重点与难点。工具较多,每个客户使用的工具也不一样。不管是对接还是纳管,通过工具链和门户的能力,把各种不同的工具驱动和管理起来,为开发人员和管理人员带来价值。

问题5

平台可用性差

解决方案

灵活与可用

目前市场上开源的或一些工具最大的特点就是能力很强,不管是插件化还是配置化,能力很强但是可用性比较差。所以在这个方面,我们花了巨大精力考虑如何实现产品可用性和灵活性的平衡,让可用性强但又很灵活,适应客户的不同需求及特性。

整个DevOps实施落地包含三个关键因素——人、流程、工具,博云从团队协作,流程再造和工具集成三个方面,帮助某金融企业实现了业务驱动型团队和标准化规范的敏捷流程,形成持续完善能力。由点及面,循环迭代完成研发测试运维的持续交付转型,打造能够真正落地的DevOps研发运维体系。

将原有的团队转变为以产品组和能力组共同组成的业务驱动型团队;

落实敏捷支撑的标准化规范,简化流程从而实现快速迭代;

打通应用全生命周期工具集成,形成DevOps的工具链。

通过一套敏捷作业管理框架,一组IT工具链和一个能展示所有环节和过程的可视化平台,博云帮助企业实现面向需求-开发-测试-上线等全流程的端到端自动化流水线,驱动整个研发测试管理流程运转,提高企业内部IT资产能力和开发质量管控水平,从而解决开发测试及运行管理中的种种问题,最终使得研发运维团队业务支撑水平能力得到提升。

随着企业数字化转型的加速推进,DevOps将持续发展,BoCloud博云将继续探索用户真实需求,提升产品能力,为用户提供更加灵活、可用性更强的DevOps平台,帮助企业真正建立DevOps文化,推进业务部门与开发团队深度配合,提升研发运维效能,专注创造价值。
继续阅读 »

在为客户进行DevOps咨询和提供解决方案时,除了“又快又好”的发布之外,我们发现客户通常还有两大方面需求:开发测试管理问题和运行管理问题。以某大型金融企业为例,该企业开发测试问题主要表现为研发过程的管控不足,这带来了开发效率低、版本质量差、环境交付慢等一系列问题。另外,整个运行环境缺乏统一管理,资源申请和获取周期长,资源利用率低也是当前运行管理中频繁出现的问题。

微信截图_20200525110806.png


问题1
研发过程无法清晰度量、查看和分析
流程规范不标准,各项目各自为战
缺少统一的研发管理支撑工具,已有的流程规范无法有效落实

解决方案

流程的平台化、固化,自动驱动流程运转

不同的公司有划分阶段不同,同一个公司也有可能会分为不同的阶段。在整个流程体系下,每个角色、每个人要做什么是DevOps落地非常重要的一个关键点。通过平台去把所有的规范固化,需要在流程的每个阶段,把每个人的工作职责,需要遵守的规范,甚至是考核指标、度量数据都确认下来。这样每个人能清晰的了解自己的职责,按图索骥去完成自己的工作。

微信截图_20200525111250.png


规范的落地是DevOps能够正常运转的一个重点。从立项到整个需求任务、开发测试、发布上线的过程里,大概50%能规范化并落地到平台中,另外还有些工作例如会议需要人员管控。每个企业中可能有1-2套标准流程,不仅要匹配不同客户的需求,和同一企业中不同项目的需求,还有随着企业对DevOps认识的不断提升,流程也会随着认知不断演进。流程的自定义和灵活性就显得非常重要。

如何自动驱动流程也是一个关键。博云为该企业设计了自定义工作流引擎,整个流程的固化,从前期的准备到后面的需求、研发、不同的测试阶段,都可以通过devops平台上企业自定义的研发工作流来驱动。它不仅能展现当前标准流程的进度,同时企业能够用标准流程去驱动底层的工具链与整个研发管理过程。不管从需求、评审或是运维等角度,整个底层、研发流程的状态变化能够驱动流程和工作引擎的状态变化。例如从需求待开发进入开发阶段后,整个版本也从待规划阶段自动变化到开发阶段。

问题2
开发测试环境搭建交付慢,耗时长
同一业务系统跨多平台部署,效率低,耗时长,管理复杂
物理机、虚拟机、容器各自管理,没有统一视图

解决方案

多视角的环境管理

环境管理既重要又很困难。首先底层环境有很多种,可能需要与虚机环境,跟云平台、云管平台对接,还有容器化环境等不同的环境。其次,环境管理有时候是有项目视角的,有时候为这个项目分配了多少环境,同时项目里还有应用、服务、系统等概念,项目中可能有三套系统,并给这三套系统分配了不同的环境。还有一种是按最小粒度去分配,例如有5个服务,每个服务有不同的环境。这几个情况都有可能会出现。从环境角度来看,应用服务是个核心视角,也有项目视角,所以我们为客户设计了树状结构的多视角环境管理支持。这个树状结构有项目—应用—服务三级视角。

现在的研发管理流程中,包含非常多的对象。从项目视角来看,它注重的是开发阶段,需求任务缺陷和非常重要的研发管理的项。从代码提交开始,它的管理对象是应用和服务,比如代码是和哪个服务关联,制品是和哪个服务关联,pipeline和部署也是与应用服务关联。也就是说项目/版本,应用/服务是我们两个核心的管理对象,那如何才能实现兼容这两种方式?

我们在实践中发现,以应用服务视角去统一或以项目视角去统一这两点都是需要的。在代码之前需要的是项目视角,在代码之后最核心的是应用服务视角。通过应用服务去关联代码、环境、pipeline、制品,这是最自然的也是最好用的一套逻辑。还有一个就是人员,除了角色对象这些东西外,它核心也会体现在所有绩效、工作量和团队。

微信截图_20200525112441.png


环境管理的落地流程从整个环境规划开始。在流程的前端,一开始就给项目申请环境与配额,最后环境细化的分配到各个项目和应用中去。然后通过pipeline的自动化把环境部署起来,由环境申请到环境释放这样一个过程,底层与各种各样的资源平台去对接。

整个资源环境管理的设计能够兼容各种各样的需求。应用本身是可能有很多环境的,我们实现了一个应用能随时创建一套自己可用的环境,在整个资源平台与不同的容器平台、虚机环境、云管平台上创建的项目相匹配,并进行多个关联。

问题3

缺乏开发测试发布环境的质量管控
缺乏开发规范或者规范难落地,代码质量差
编译打包部署自动化程度低,效率低,易出错
解决方案

以版本为核心的过程管理和追溯

以版本为核心的过程管理和追溯,我们理解是整个DevOps管理设计或是研发管理过程的核心的需求。从需求开始,代码、制品、包括线上的实例,都是能与版本进行关联。但一直以来,很多企业的研发管理过程是做不到这点的。

我们通过DevOps平台的整体自动化驱动,以版本为核心,从整个的从需求到代码、制品、构建、部署和实例的管理过程给支撑起来,将它与关联项进行匹配,使信息实现同步。这样能实现线上运行的版本对应它的需求,能够清晰的了解制品关联哪些需求和版本。这样能够解决整个线上版本和线下版本或不同测试环境里的版本不一致的问题。

微信截图_20200525113744.png


这里关键的核心点是研发提测过程。整个研发过程中,研发提测把需求圈出来告诉测试,圈完之后就会走自己的pipeline,pipeline最后生成制品,这样就实现了制品与需求的关联。平台还有提测单,提测单中不仅有制品和需求的信息,还会有质量检查的信息。这个信息会一直跟着流转到整个的版本测试报告中,也就是说我们不但知道这次测试的报告,还能知道所有中间过程中安全检查和自动化测试的结果,是清晰可见的,这是一个关联和信息匹配的过程。

图片1.png


另外一个就是以版本为桥梁打通开发测试和生产环境。在开发测试和生产的过渡过程中,版本包含提测单、需求,也包含制品、配置、脚本这些所有的信息,这样从开发测试转化生产时,不仅是把制品转过去,也包含了整个配置和脚本信息,这样不仅能实现制品包移植,它的配置信息、脚本信息与开发测试环境也是一致的。

问题4

企业或项目个性化的问题和需求如何满足

解决方案

强大的配置能力,适应当前与未来

不管是驱动团队运转的自定义工作流,还是自定义的度量指标等功能,DevOps平台都能够随着客户内部团队的演进适应当前的需求还有未来的变化。另外,前台的自定义DevOps门户能够把整个DevOps流程驱动起来并可视化展现出来。

对开发人员来讲,接收到的需求能在界面上看到,做代码也能在同一个界面上看到,制品、部署发布上线,需要处理的流程的工作,都能在门户中心看到。这样就不需要用许多工具和每天登录十多个系统,就按看到的流程走。

整个工具的驱动也是一个重点与难点。工具较多,每个客户使用的工具也不一样。不管是对接还是纳管,通过工具链和门户的能力,把各种不同的工具驱动和管理起来,为开发人员和管理人员带来价值。

问题5

平台可用性差

解决方案

灵活与可用

目前市场上开源的或一些工具最大的特点就是能力很强,不管是插件化还是配置化,能力很强但是可用性比较差。所以在这个方面,我们花了巨大精力考虑如何实现产品可用性和灵活性的平衡,让可用性强但又很灵活,适应客户的不同需求及特性。

整个DevOps实施落地包含三个关键因素——人、流程、工具,博云从团队协作,流程再造和工具集成三个方面,帮助某金融企业实现了业务驱动型团队和标准化规范的敏捷流程,形成持续完善能力。由点及面,循环迭代完成研发测试运维的持续交付转型,打造能够真正落地的DevOps研发运维体系。

将原有的团队转变为以产品组和能力组共同组成的业务驱动型团队;

落实敏捷支撑的标准化规范,简化流程从而实现快速迭代;

打通应用全生命周期工具集成,形成DevOps的工具链。

通过一套敏捷作业管理框架,一组IT工具链和一个能展示所有环节和过程的可视化平台,博云帮助企业实现面向需求-开发-测试-上线等全流程的端到端自动化流水线,驱动整个研发测试管理流程运转,提高企业内部IT资产能力和开发质量管控水平,从而解决开发测试及运行管理中的种种问题,最终使得研发运维团队业务支撑水平能力得到提升。

随着企业数字化转型的加速推进,DevOps将持续发展,BoCloud博云将继续探索用户真实需求,提升产品能力,为用户提供更加灵活、可用性更强的DevOps平台,帮助企业真正建立DevOps文化,推进业务部门与开发团队深度配合,提升研发运维效能,专注创造价值。 收起阅读 »

阿里开源分布式限流框架 - Sentinel Go 0.3.0 发布,支持熔断降级能力


作者 | 宿何  阿里巴巴高级开发工程师

Sentinel 是阿里巴巴开源的,面向分布式服务架构的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统自适应保护等多个维度来帮助开发者保障微服务的稳定性。Sentinel 承接了阿里巴巴近 10 年的 双11 大促流量的核心场景,例如秒杀、冷启动、消息削峰填谷、集群流量控制、实时熔断下游不可用服务等,是保障微服务高可用的利器,原生支持 Java/Go/C++ 等多种语言,并且提供 Istio/Envoy 全局流控支持来为 Service Mesh 提供高可用防护的能力。

近期,Sentinel Go 0.3.0 正式发布,带来了熔断降级特性支持,可以针对 Go 服务中的不稳定调用进行自动熔断,避免出现级联错误/雪崩,是保障服务高可用重要的一环。结合 Sentinel Go 已经提供的 gRPC、Gin、Dubbo 等框架组件的适配模块,开发者可以快速在 Web、RPC 调用层面配置熔断降级规则来保护自身服务的稳定性。同时 0.3.0 版本也带来了 etcd 动态数据源模块,开发者可以方便地通过 etcd 来动态调整熔断降级策略。

Sentinel Go 项目地址:https://github.com/alibaba/sentinel-golang

为什么需要熔断降级

一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。

1.png


现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的服务进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。

Sentinel Go 熔断降级特性基于熔断器模式的思想,在服务出现不稳定因素(如响应时间变长,错误率上升)的时候暂时切断服务的调用,等待一段时间再进行尝试。一方面防止给不稳定服务“雪上加霜”,另一方面保护服务的调用方不被拖垮。Sentinel 支持两种熔断策略:基于响应时间(慢调用比例)和基于错误(错误比例/错误数),可以有效地针对各种不稳定的场景进行防护。

2.png


下面我们介绍一下 Sentinel 流控降级的一些最佳实践。

流控降级最佳实践

在服务提供方(Service Provider)的场景下,我们需要保护服务提供方不被流量洪峰打垮。我们通常根据服务提供方的服务能力进行流量控制,或针对特定的服务调用方进行限制。为了保护服务提供方不被激增的流量拖垮影响稳定性,我们可以结合前期的容量评估,通过 Sentinel 配置 QPS 模式的流控规则,当每秒的请求量超过设定的阈值时,会自动拒绝多余的请求。

在服务调用端(Service Consumer)的场景下,我们需要保护服务调用方不被不稳定的依赖服务拖垮。借助 Sentinel 的信号量隔离策略(并发数流控规则),限制某个服务调用的并发量,防止大量慢调用挤占正常请求的资源;同时,借助熔断降级规则,当异常比率或业务慢调用比例超过某个阈值后将调用自动熔断,直到一段时间过后再尝试恢复。熔断期间我们可以提供默认的处理逻辑(fallback),熔断期间的调用都会返回 fallback 的结果,而不会再去尝试本已非常不稳定的服务。需要注意的是,即使服务调用方引入了熔断降级机制,我们还是需要在 HTTP 或 RPC 客户端配置请求超时时间,来做一个兜底的保护。

同时 Sentinel 还提供全局维度的系统自适应保护能力,结合系统的 Load、CPU 使用率以及服务的入口 QPS、响应时间和并发量等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。系统规则可以作为整个服务的一个兜底防护策略,保障服务不挂。

3.png


Let's start hacking!

Sentinel Go 版本正在快速演进中,我们非常欢迎感兴趣的开发者参与贡献,一起来主导未来版本的演进。Sentinel Go 版本的演进离不开社区的贡献。若您有意愿参与贡献,欢迎联系我们加入 Sentinel 贡献小组一起成长(Sentinel 开源讨论钉钉群:30150716)。

同时,一年一度的阿里巴巴编程之夏(Alibaba Summer of Code)开始啦!如果你是在校的学生,有兴趣参与 Sentinel 项目的开发和演进,不要错过此次机会,欢迎 pick 感兴趣的 issue 提交提案:https://github.com/alibaba/Sentinel/issues/1497

Now let's start hacking!

第 3 期云原生网络研讨会邀您参加

今晚 19:00 - 20:00,阿里云技术专家将为大家带来《如何为云原生应用带来稳定高效的部署能力?》,届时将会介绍阿里经济体大规模应用上云过程中遇到的核心部署问题、采取的对应解决方案,以及这些方案沉淀为通用化能力输出开源后,如何帮助阿里云上的用户提升应用部署发布的效率与稳定性。

听众可获取以下收益:

• 了解阿里经济体大规模应用上云的实践经验,如何解决原生 K8s workload 不满足场景需求的问题;
• 作为外部用户,如何体验和使用上阿里经济体上云所沉淀下来的应用部署发布能力;
• 演示阿里巴巴针对大规模 K8s 集群如何做到 DaemonSet 高可用的灰度升级(即将开源!)

点击链接即可预约直播:https://yq.aliyun.com/live/2898

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”
继续阅读 »

作者 | 宿何  阿里巴巴高级开发工程师

Sentinel 是阿里巴巴开源的,面向分布式服务架构的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统自适应保护等多个维度来帮助开发者保障微服务的稳定性。Sentinel 承接了阿里巴巴近 10 年的 双11 大促流量的核心场景,例如秒杀、冷启动、消息削峰填谷、集群流量控制、实时熔断下游不可用服务等,是保障微服务高可用的利器,原生支持 Java/Go/C++ 等多种语言,并且提供 Istio/Envoy 全局流控支持来为 Service Mesh 提供高可用防护的能力。

近期,Sentinel Go 0.3.0 正式发布,带来了熔断降级特性支持,可以针对 Go 服务中的不稳定调用进行自动熔断,避免出现级联错误/雪崩,是保障服务高可用重要的一环。结合 Sentinel Go 已经提供的 gRPC、Gin、Dubbo 等框架组件的适配模块,开发者可以快速在 Web、RPC 调用层面配置熔断降级规则来保护自身服务的稳定性。同时 0.3.0 版本也带来了 etcd 动态数据源模块,开发者可以方便地通过 etcd 来动态调整熔断降级策略。

Sentinel Go 项目地址:https://github.com/alibaba/sentinel-golang

为什么需要熔断降级

一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。

1.png


现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的服务进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。

Sentinel Go 熔断降级特性基于熔断器模式的思想,在服务出现不稳定因素(如响应时间变长,错误率上升)的时候暂时切断服务的调用,等待一段时间再进行尝试。一方面防止给不稳定服务“雪上加霜”,另一方面保护服务的调用方不被拖垮。Sentinel 支持两种熔断策略:基于响应时间(慢调用比例)和基于错误(错误比例/错误数),可以有效地针对各种不稳定的场景进行防护。

2.png


下面我们介绍一下 Sentinel 流控降级的一些最佳实践。

流控降级最佳实践

在服务提供方(Service Provider)的场景下,我们需要保护服务提供方不被流量洪峰打垮。我们通常根据服务提供方的服务能力进行流量控制,或针对特定的服务调用方进行限制。为了保护服务提供方不被激增的流量拖垮影响稳定性,我们可以结合前期的容量评估,通过 Sentinel 配置 QPS 模式的流控规则,当每秒的请求量超过设定的阈值时,会自动拒绝多余的请求。

在服务调用端(Service Consumer)的场景下,我们需要保护服务调用方不被不稳定的依赖服务拖垮。借助 Sentinel 的信号量隔离策略(并发数流控规则),限制某个服务调用的并发量,防止大量慢调用挤占正常请求的资源;同时,借助熔断降级规则,当异常比率或业务慢调用比例超过某个阈值后将调用自动熔断,直到一段时间过后再尝试恢复。熔断期间我们可以提供默认的处理逻辑(fallback),熔断期间的调用都会返回 fallback 的结果,而不会再去尝试本已非常不稳定的服务。需要注意的是,即使服务调用方引入了熔断降级机制,我们还是需要在 HTTP 或 RPC 客户端配置请求超时时间,来做一个兜底的保护。

同时 Sentinel 还提供全局维度的系统自适应保护能力,结合系统的 Load、CPU 使用率以及服务的入口 QPS、响应时间和并发量等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。系统规则可以作为整个服务的一个兜底防护策略,保障服务不挂。

3.png


Let's start hacking!

Sentinel Go 版本正在快速演进中,我们非常欢迎感兴趣的开发者参与贡献,一起来主导未来版本的演进。Sentinel Go 版本的演进离不开社区的贡献。若您有意愿参与贡献,欢迎联系我们加入 Sentinel 贡献小组一起成长(Sentinel 开源讨论钉钉群:30150716)。

同时,一年一度的阿里巴巴编程之夏(Alibaba Summer of Code)开始啦!如果你是在校的学生,有兴趣参与 Sentinel 项目的开发和演进,不要错过此次机会,欢迎 pick 感兴趣的 issue 提交提案:https://github.com/alibaba/Sentinel/issues/1497

Now let's start hacking!

第 3 期云原生网络研讨会邀您参加

今晚 19:00 - 20:00,阿里云技术专家将为大家带来《如何为云原生应用带来稳定高效的部署能力?》,届时将会介绍阿里经济体大规模应用上云过程中遇到的核心部署问题、采取的对应解决方案,以及这些方案沉淀为通用化能力输出开源后,如何帮助阿里云上的用户提升应用部署发布的效率与稳定性。

听众可获取以下收益:

• 了解阿里经济体大规模应用上云的实践经验,如何解决原生 K8s workload 不满足场景需求的问题;
• 作为外部用户,如何体验和使用上阿里经济体上云所沉淀下来的应用部署发布能力;
• 演示阿里巴巴针对大规模 K8s 集群如何做到 DaemonSet 高可用的灰度升级(即将开源!)

点击链接即可预约直播:https://yq.aliyun.com/live/2898

阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”
收起阅读 »

Apache Flink on Kubernetes:四种运行模式,我该选择哪种?


前言

Apache Flink 是一个分布式流处理引擎,它提供了丰富且易用的API来处理有状态的流处理应用,并且在支持容错的前提下,高效、大规模的运行此类应用。通过支持事件时间(event-time)、计算状态(state)以及恰好一次(exactly-once)的容错保证,Flink迅速被很多公司采纳,成为了新一代的流计算处理引擎。

2020 年 2 月 11 日,社区发布了 Flink 1.10.0 版本,该版本对性能和稳定性做了很大的提升,同时引入了 native Kubernetes 的特性。对于 Flink 的下一个稳定版本,社区在 2020 年 4 月底冻结新特性的合入,预计在 2020 年 5 月中旬会推出 Flink 1.11,在新版本中将重点引入新特性,以扩容 Flink 的使用场景。

Flink 为什么选择 Kubernetes

Kubernetes 项目源自 Google 内部 Borg 项目,基于 Borg 多年来的优秀实践和其超前的设计理念,并凭借众多豪门、大厂的背书,时至今日,Kubernetes 已经成长为容器管理领域的事实标准。在大数据及相关领域,包括 Spark,Hive,Airflow,Kafka 等众多知名产品正在迁往 Kubernetes,Apache Flink 也是其中一员。

Flink 选择 Kubernetes 作为其底层资源管理平台,原因包括两个方面:
  • Flink 特性:流式服务一般是常驻进程,经常用于电信网质量监控、商业数据即席分析、实时风控和实时推荐等对稳定性要求比较高的场景;
  • Kubernetes 优势:为在线业务提供了更好的发布、管理机制,并保证其稳定运行,同时 Kubernetes 具有很好的生态优势,能很方便的和各种运维工具集成,如 prometheus 监控,主流的日志采集工具等;同时 Kubernetes 在资源弹性方面提供了很好的扩缩容机制,很大程度上提高了资源利用率。


Flink on Kubernetes 的发展历史

在 Flink 的早期发行版 1.2 中,已经引入了 Flink Session 集群模式,用户得以将 Flink 集群部署在 Kubernetes 集群之上。

随着 Flink 的逐渐普及,越来越多的 Flink 任务被提交在用户的集群中,用户发现在 session 模式下,任务之间会互相影响,隔离性比较差,因此在 Flink 1.6 版本中,推出了 Per Job 模式,单个任务独占一个 Flink 集群,很大的程度上提高了任务的稳定性。

在满足了稳定性之后,用户觉得这两种模式,没有做到资源按需创建,往往需要凭用户经验来事先指定 Flink 集群的规格,在这样的背景之下,native session 模式应用而生,在 Flink 1.10 版本进入 Beta 阶段,我们增加了 native per job 模式,在资源按需申请的基础上,提高了应用之间的隔离性。

本文根据 Flink 在 Kubernetes 集群上的运行模式的趋势,依次分析了这些模式的特点,并在最后介绍了 Flink operator 方案及其优势。

Flink运行模式

本文首先分析了 Apache Flink 1.10 在 Kubernetes 集群上已经GA(生产可用)的两种部署模式,然后分析了处于 Beta 版本的 native session 部署模式和即将在 Flink 1.11 发布的 native per-job 部署模式,最后根据这些部署模式的利弊,介绍了当前比较 native kubernetes 的部署方式,flink-operator。

我们正在使用的 Flink 版本已经很好的支持了 native session 和 native per-job 两种模式,在 flink-operator 中,我们也对这两种模式也做了支持。

接下来将按照以下顺序分析了 Flink 的运行模式,读者可以结合自身的业务场景,考量适合的 Flink 运行模式。
  • Flink session 模式
  • Flink per-job 模式
  • Flink native session 模式
  • Flink native per-job 模式


这四种部署模式的优缺点对比,可以用如下表格来概括,更多的内容,请参考接下来的详细描述。
1.png

Session Cluster 模式

原理简介

Session 模式下,Flink 集群处于长期运行状态,当集群的Master组件接收到客户端提交的任务后,对任务进行分析并处理。用户将Flink集群的资源描述文件提交到 Kubernetes 之后,Flink 集群的 FlinkMaster 和 TaskManager 会被创建出来,如下图所示,TaskManager 启动后会向 ResourceManager 模块注册,这时 Flink Session 集群已经准备就绪。当用户通过 Flink Clint 端提交了 Job 任务时,Dispatcher 收到该任务请求,将请求转发给 JobMaster,由 JobMaster 将任务分配给具体的 TaskManager。
2.png

特点分析

这种类型的 Flink 集群,FlinkMaster 和 TaskManager 是以Kubernetes deployment的形式长期运行在 Kubernetes 集群中。在提交作业之前,必须先创建好 Flink session 集群。多个任务可以同时运行在同一个集群内,任务之间共享 K8sResourceManager 和 Dispatcher,但是 JobMaster 是单独的。这种方式比较适合运行短时作业、即席查询、任务提交频繁、或者对任务启动时长比较敏感的场景。

优点:作业提交的时候,FlinkMaster 和 TaskManager已经准备好了,当资源充足时,作业能够立即被分配到 TaskManager 执行,无需等待 FlinkMaster,TaskManager,Service 等资源的创建。

缺点:
  • 需要在提交 Job 任务之前先创建 Flink 集群,需要提前指定 TaskManager 的数量,但是在提交任务前,是难以精准把握具体资源需求的,指定的多了,会有大量 TaskManager 处于闲置状态,资源利用率就比较低,指定的少了,则会有任务分配不到资源,只能等集群中其他作业执行完成后,释放了资源,下一个作业才会被正常执行。
  • 隔离性比较差,多个 Job 任务之间存在资源竞争,互相影响;如果一个 Job 异常导致 TaskManager crash 了,那么所有运行在这个 TaskManager 上的 Job 任务都会被重启;进而,更坏的情况是,多个 Jobs 任务的重启,大量并发的访问文件系统,会导致其他服务的不可用;最后一点是,在 Rest interface 上是可以看到同一个 session 集群里其他人的 Job 任务。


Per Job Cluster 模式

顾名思义,这种方式会专门为每个 Job 任务创建一个单独的 Flink 集群,当资源描述文件被提交到 Kubernetes 集群,Kubernetes 会依次创建 FlinkMaster Deployment、TaskManagerDeployment 并运行任务,任务完成后,这些 Deployment 会被自动清理。
3.png

特点分析

优点:隔离性比较好,任务之间资源不冲突,一个任务单独使用一个 Flink 集群;相对于 Flink session 集群而且,资源随用随建,任务执行完成后立刻销毁资源,资源利用率会高一些。

缺点:需要提前指定 TaskManager 的数量,如果 TaskManager 指定的少了会导致作业运行失败,指定的多了仍会降低资源利用率;资源是实时创建的,用户的作业在被运行前,需要先等待以下过程:
  • Kubernetes scheduler为FlinkMaster和 TaskManager 申请资源并调度到宿主机上进行创建;
  • Kubernetes kubelet拉取FlinkMaster、TaskManager 镜像,并创建出FlinkMaster、TaskManager容器;
  • TaskManager启动后,向Flink ResourceManager 注册。


这种模式比较适合对启动时间不敏感、且长时间运行的作业。不适合对任务启动时间比较敏感的场景。

Native Session Cluster 模式

原理分析
4.png

  1. Flink提供了 Kubernetes 模式的入口脚本 kubernetes-session.sh,当用户执行了该脚本之后,Flink 客户端会生成 Kubernets 资源描述文件,包括 FlinkMaster Service,FlinkMasterDeloyment,Configmap,Service并设置了owner reference,在 Flink 1.10 版本中,是将 FlinkMaster Service 作为其他资源的 Owner,也就意味着在删除 Flink 集群的时候,只需要删除 FlinkMaster service,其他资源则会被以及联的方式自动删除;
  2. Kubernetes 收到来自 Flink 的资源描述请求后,开始创建FlinkMaster Service,FlinkMaster Deloyment,以及 Configmap 资源,从图中可以看到,伴随着 FlinkMaster 的创建,Dispatch 和 K8sResMngr 组件也同时被创建了,这里的 K8sResMngr 就是 Native 方式的核心组件,正是这个组件去和 Kubernetes API server 进行通信,申请 TaskManager 资源;当前,用户已经可以向 Flink 集群提交任务请求了;
  3. 用户通过 Flink client 向 Flink 集群提交任务,flink client 会生成 Job graph,然后和 jar 包一起上传;当任务提交成功后,JobSubmitHandler 收到了请求并提交给 Dispatcher并生成 JobMaster,JobMaster 用于向 KubernetesResourceManager 申请 task 资源;
  4. Kubernetes-Resource-Manager 会为 taskmanager 生成一个新的配置文件,包含了 service 的地址,这样当 Flink Master 异常重建后,能保证 taskmanager 通过 Service 仍然能连接到新的 Flink Master;
  5. TaskManager 创建成功后注册到 slotManager,这时 slotManager向TaskManager 申请 slots,TaskManager 提供自己的空闲 slots,任务被部署并运行。


特点分析

之前我们提到的两种部署模式,在 Kubernetes 上运行 Flink 任务是需要事先指定好 TaskManager 的数量,但是大部分情况下,用户在任务启动前是无法准确的预知该任务所需的 TaskManager 数量和规格。

指定的多了会资源浪费,指定的少了会导致任务的执行失败。最根本的原因,就是没有 Native 的使用 Kubernetes 资源,这里的 Native,可以理解为 Flink 直接与 Kuberneter 通信来申请资源。

这种类型的集群,也是在提交任务之前就创建好了,不过只包含了 FlinkMaster 及其 Entrypoint(Service),当任务提交的时候,Flink client 会根据任务计算出并行度,进而确定出所需 TaskManager 的数量,然后 Flink 内核会直接向 Kubernetes API server 申请 taskmanager,达到资源动态创建的目的。

优点:相对于前两种集群而言,taskManager 的资源是实时的、按需进行的创建,对资源的利用率更高,所需资源更精准。

缺点:taskManager 是实时创建的,用户的作业真正运行前,与 Per Job集群一样,仍需要先等待 taskManager 的创建,因此对任务启动时间比较敏感的用户,需要进行一定的权衡。

Native Per Job 模式

在当前的 Apache Flink 1.10 版本里,Flink native per-job 特性尚未发布,预计在后续的 Flink 1.11 版本中提供,我们可以提前一览 native per job 的特性。

原理分析
5.png

当任务被提交后,同样由 Flink 来向 Kubernetes 申请资源,其过程与之前提到的 native session 模式相似,不同之处在于:
  1. Flink Master是随着任务的提交而动态创建的;
  2. 用户可以将 Flink、作业 Jar 包和 classpath 依赖打包到自己的镜像里;
  3. 作业运行图由 Flink Master 生成,所以无需通过 RestClient 上传 Jar 包(图 2 步骤 3)。


特点分析

native per-job cluster 也是任务提交的时候才创建 Flink 集群,不同的是,无需用户指定 TaskManager 资源的数量,因为同样借助了 Native 的特性,Flink 直接与 Kubernetes 进行通信并按需申请资源。

优点:资源按需申请,适合一次性任务,任务执行后立即释放资源,保证了资源的利用率。

缺点:资源是在任务提交后开始创建,同样意味着对于提交任务后对延时比较敏感的场景,需要一定的权衡;。

Flink-operator

简介

分析以上四种部署模式,我们发现,对于 Flink 集群的使用,往往需要用户自行维护部署脚本,向 Kubernetes 提交各种所需的底层资源描述文件(Flink Master,TaskManager,配置文件,Service)。

在 session cluster 下,如果集群不再使用,还需要用户自行删除这些的资源,因为这类集群的资源使用了 Kubernetes 的垃圾回收机制 owner reference,在删除 Flink 集群的时候,需要通过删除资源的 Owner 来进行及联删除,这对于不熟悉 Kubernetes 的 Flink 用户来说,就显得不是很友好了。

而通过 Flink-operator,我们可以把 Flink 集群描述成 yaml 文件,这样,借助 Kubernetes 的声明式特性和协调控制器,我们可以直接管理 Flink 集群及其作业,而无需关注底层资源如 Deployment,Service,ConfigMap 的创建及维护。

当前 Flink 官方还未给出 flink-operator 方案,不过 GoogleCloudPlatform 提供了一种基于 kubebuilder 构建的 flink-operator方案。接下来,将介绍 flink-operator 的安装方式和对 Flink 集群的管理示例。

Flink-operator 原理及优势

当 Fink operator 部署至 Kubernetes 集群后, FlinkCluster 资源和 Flink Controller 被创建。其中 FlinkCluster 用于描述 Flink 集群,如 JobMaster 规格、TaskManager 和 TaskSlot 数量等;Flink Controller 实时处理针对 FlinkCluster 资源的 CRUD 操作,用户可以像管理内置 Kubernetes 资源一样管理 Flink 集群。

例如,用户通过 yaml 文件描述期望的 Flink 集群并向 Kubernetes 提交,Flink controller 分析用户的 yaml,得到 FlinkCluster CR,然后调用 API server 创建底层资源,如JobMaster Service,JobMaster Deployment,TaskManager Deployment。
6.png

通过使用 Flink Operator,有如下优势:

1. 管理 Flink 集群更加便捷

flink-operator 更便于我们管理 Flink 集群,我们不需要针对不同的 Flink 集群维护 Kubenretes 底层各种资源的部署脚本,唯一需要的,就是 FlinkCluster 的一个自定义资源的描述文件。创建一个 Flink session 集群,只需要一条 kubectl apply 命令即可,下图是 Flink Session集群的 yaml 文件,用户只需要在该文件中声明期望的 Flink 集群配置,flink-operator 会自动完成 Flink 集群的创建和维护工作。如果创建 Per Job 集群,也只需要在该 yaml 中声明 Job 的属性,如 Job 名称,Jar 包路径即可。通过 flink-operator,上文提到的四种 Flink 运行模式,分别对应一个 yaml 文件即可,非常方便。
apiVersion: flinkoperator.k8s.io/v1beta1
kind: FlinkCluster
metadata:
name: flinksessioncluster-sample
spec:
image:
name: flink:1.10.0
pullPolicy: IfNotPresent
jobManager:
accessScope: Cluster
ports:
  ui: 8081
resources:
  limits:
    memory: "1024Mi"
    cpu: "200m"
taskManager:
replicas: 1
resources:
  limits:
    memory: "2024Mi"
    cpu: "200m"
volumes:
  - name: cache-volume
    emptyDir: {}
volumeMounts:
  - mountPath: /cache
    name: cache-volume
envVars:
- name: FOO
  value: bar
flinkProperties:
taskmanager.numberOfTaskSlots: "1"

2. 声明式

通过执行脚本命令式的创建 Flink 集群各个底层资源,需要用户保证资源是否依次创建成功,往往伴随着辅助的检查脚本。借助 flink operator 的控制器模式,用户只需声明所期望的 Flink 集群的状态,剩下的工作全部由 Flink operator 来保证。在 Flink 集群运行的过程中,如果出现资源异常,如 JobMaster 意外停止甚至被删除,Flink operator 都会重建这些资源,自动的修复 Flink 集群。

3. 自定义保存点

用户可以指定 autoSavePointSeconds 和保存路径,Flink operator 会自动为用户定期保存快照。

4. 自动恢复

流式任务往往是长期运行的,甚至 2-3 年不停止都是常见的。在任务执行的过程中,可能会有各种各样的原因导致任务失败。用户可以指定任务重启策略,当指定为 FromSavePointOnFailure,Flink operator 自动从最近的保存点重新执行任务。

5. sidecar containers

sidecar 容器也是 Kubernetes 提供的一种设计模式,用户可以在 TaskManager Pod 里运行 sidecar 容器,为 Job 提供辅助的自定义服务或者代理服务。

6. Ingress 集成

用户可以定义 Ingress 资源,flink operator 将会自动创建 Ingress 资源。云厂商托管的 Kubernetes 集群一般都有 Ingress 控制器,否则需要用户自行实现 Ingress controller。

7. Prometheus 集成

通过在 Flink 集群的 yaml 文件里指定 metric exporter 和 metric port,可以与 Kubernetes 集群中的 Prometheus 进行集成。

最后

通过本文,我们了解了 Flink 在 Kubernetes 上运行的不同模式,其中 Native 模式在资源按需申请方面比较突出,借助 Kubernetes Operator,我们可以将 Flink 集群当成Kubernetes原生的资源一样进行 CRUD 操作。限于篇幅,本文主要分析了 Flink 在 Kubernetes 上的运行模式的区别,后续将会有更多的文章来对 Flink 在 Kubernetes 上的最佳实践进行描述,敬请期待。

原文链接:https://mp.weixin.qq.com/s/61AQTG-LjkSThcHN4gSrNQ
继续阅读 »

前言

Apache Flink 是一个分布式流处理引擎,它提供了丰富且易用的API来处理有状态的流处理应用,并且在支持容错的前提下,高效、大规模的运行此类应用。通过支持事件时间(event-time)、计算状态(state)以及恰好一次(exactly-once)的容错保证,Flink迅速被很多公司采纳,成为了新一代的流计算处理引擎。

2020 年 2 月 11 日,社区发布了 Flink 1.10.0 版本,该版本对性能和稳定性做了很大的提升,同时引入了 native Kubernetes 的特性。对于 Flink 的下一个稳定版本,社区在 2020 年 4 月底冻结新特性的合入,预计在 2020 年 5 月中旬会推出 Flink 1.11,在新版本中将重点引入新特性,以扩容 Flink 的使用场景。

Flink 为什么选择 Kubernetes

Kubernetes 项目源自 Google 内部 Borg 项目,基于 Borg 多年来的优秀实践和其超前的设计理念,并凭借众多豪门、大厂的背书,时至今日,Kubernetes 已经成长为容器管理领域的事实标准。在大数据及相关领域,包括 Spark,Hive,Airflow,Kafka 等众多知名产品正在迁往 Kubernetes,Apache Flink 也是其中一员。

Flink 选择 Kubernetes 作为其底层资源管理平台,原因包括两个方面:
  • Flink 特性:流式服务一般是常驻进程,经常用于电信网质量监控、商业数据即席分析、实时风控和实时推荐等对稳定性要求比较高的场景;
  • Kubernetes 优势:为在线业务提供了更好的发布、管理机制,并保证其稳定运行,同时 Kubernetes 具有很好的生态优势,能很方便的和各种运维工具集成,如 prometheus 监控,主流的日志采集工具等;同时 Kubernetes 在资源弹性方面提供了很好的扩缩容机制,很大程度上提高了资源利用率。


Flink on Kubernetes 的发展历史

在 Flink 的早期发行版 1.2 中,已经引入了 Flink Session 集群模式,用户得以将 Flink 集群部署在 Kubernetes 集群之上。

随着 Flink 的逐渐普及,越来越多的 Flink 任务被提交在用户的集群中,用户发现在 session 模式下,任务之间会互相影响,隔离性比较差,因此在 Flink 1.6 版本中,推出了 Per Job 模式,单个任务独占一个 Flink 集群,很大的程度上提高了任务的稳定性。

在满足了稳定性之后,用户觉得这两种模式,没有做到资源按需创建,往往需要凭用户经验来事先指定 Flink 集群的规格,在这样的背景之下,native session 模式应用而生,在 Flink 1.10 版本进入 Beta 阶段,我们增加了 native per job 模式,在资源按需申请的基础上,提高了应用之间的隔离性。

本文根据 Flink 在 Kubernetes 集群上的运行模式的趋势,依次分析了这些模式的特点,并在最后介绍了 Flink operator 方案及其优势。

Flink运行模式

本文首先分析了 Apache Flink 1.10 在 Kubernetes 集群上已经GA(生产可用)的两种部署模式,然后分析了处于 Beta 版本的 native session 部署模式和即将在 Flink 1.11 发布的 native per-job 部署模式,最后根据这些部署模式的利弊,介绍了当前比较 native kubernetes 的部署方式,flink-operator。

我们正在使用的 Flink 版本已经很好的支持了 native session 和 native per-job 两种模式,在 flink-operator 中,我们也对这两种模式也做了支持。

接下来将按照以下顺序分析了 Flink 的运行模式,读者可以结合自身的业务场景,考量适合的 Flink 运行模式。
  • Flink session 模式
  • Flink per-job 模式
  • Flink native session 模式
  • Flink native per-job 模式


这四种部署模式的优缺点对比,可以用如下表格来概括,更多的内容,请参考接下来的详细描述。
1.png

Session Cluster 模式

原理简介

Session 模式下,Flink 集群处于长期运行状态,当集群的Master组件接收到客户端提交的任务后,对任务进行分析并处理。用户将Flink集群的资源描述文件提交到 Kubernetes 之后,Flink 集群的 FlinkMaster 和 TaskManager 会被创建出来,如下图所示,TaskManager 启动后会向 ResourceManager 模块注册,这时 Flink Session 集群已经准备就绪。当用户通过 Flink Clint 端提交了 Job 任务时,Dispatcher 收到该任务请求,将请求转发给 JobMaster,由 JobMaster 将任务分配给具体的 TaskManager。
2.png

特点分析

这种类型的 Flink 集群,FlinkMaster 和 TaskManager 是以Kubernetes deployment的形式长期运行在 Kubernetes 集群中。在提交作业之前,必须先创建好 Flink session 集群。多个任务可以同时运行在同一个集群内,任务之间共享 K8sResourceManager 和 Dispatcher,但是 JobMaster 是单独的。这种方式比较适合运行短时作业、即席查询、任务提交频繁、或者对任务启动时长比较敏感的场景。

优点:作业提交的时候,FlinkMaster 和 TaskManager已经准备好了,当资源充足时,作业能够立即被分配到 TaskManager 执行,无需等待 FlinkMaster,TaskManager,Service 等资源的创建。

缺点:
  • 需要在提交 Job 任务之前先创建 Flink 集群,需要提前指定 TaskManager 的数量,但是在提交任务前,是难以精准把握具体资源需求的,指定的多了,会有大量 TaskManager 处于闲置状态,资源利用率就比较低,指定的少了,则会有任务分配不到资源,只能等集群中其他作业执行完成后,释放了资源,下一个作业才会被正常执行。
  • 隔离性比较差,多个 Job 任务之间存在资源竞争,互相影响;如果一个 Job 异常导致 TaskManager crash 了,那么所有运行在这个 TaskManager 上的 Job 任务都会被重启;进而,更坏的情况是,多个 Jobs 任务的重启,大量并发的访问文件系统,会导致其他服务的不可用;最后一点是,在 Rest interface 上是可以看到同一个 session 集群里其他人的 Job 任务。


Per Job Cluster 模式

顾名思义,这种方式会专门为每个 Job 任务创建一个单独的 Flink 集群,当资源描述文件被提交到 Kubernetes 集群,Kubernetes 会依次创建 FlinkMaster Deployment、TaskManagerDeployment 并运行任务,任务完成后,这些 Deployment 会被自动清理。
3.png

特点分析

优点:隔离性比较好,任务之间资源不冲突,一个任务单独使用一个 Flink 集群;相对于 Flink session 集群而且,资源随用随建,任务执行完成后立刻销毁资源,资源利用率会高一些。

缺点:需要提前指定 TaskManager 的数量,如果 TaskManager 指定的少了会导致作业运行失败,指定的多了仍会降低资源利用率;资源是实时创建的,用户的作业在被运行前,需要先等待以下过程:
  • Kubernetes scheduler为FlinkMaster和 TaskManager 申请资源并调度到宿主机上进行创建;
  • Kubernetes kubelet拉取FlinkMaster、TaskManager 镜像,并创建出FlinkMaster、TaskManager容器;
  • TaskManager启动后,向Flink ResourceManager 注册。


这种模式比较适合对启动时间不敏感、且长时间运行的作业。不适合对任务启动时间比较敏感的场景。

Native Session Cluster 模式

原理分析
4.png

  1. Flink提供了 Kubernetes 模式的入口脚本 kubernetes-session.sh,当用户执行了该脚本之后,Flink 客户端会生成 Kubernets 资源描述文件,包括 FlinkMaster Service,FlinkMasterDeloyment,Configmap,Service并设置了owner reference,在 Flink 1.10 版本中,是将 FlinkMaster Service 作为其他资源的 Owner,也就意味着在删除 Flink 集群的时候,只需要删除 FlinkMaster service,其他资源则会被以及联的方式自动删除;
  2. Kubernetes 收到来自 Flink 的资源描述请求后,开始创建FlinkMaster Service,FlinkMaster Deloyment,以及 Configmap 资源,从图中可以看到,伴随着 FlinkMaster 的创建,Dispatch 和 K8sResMngr 组件也同时被创建了,这里的 K8sResMngr 就是 Native 方式的核心组件,正是这个组件去和 Kubernetes API server 进行通信,申请 TaskManager 资源;当前,用户已经可以向 Flink 集群提交任务请求了;
  3. 用户通过 Flink client 向 Flink 集群提交任务,flink client 会生成 Job graph,然后和 jar 包一起上传;当任务提交成功后,JobSubmitHandler 收到了请求并提交给 Dispatcher并生成 JobMaster,JobMaster 用于向 KubernetesResourceManager 申请 task 资源;
  4. Kubernetes-Resource-Manager 会为 taskmanager 生成一个新的配置文件,包含了 service 的地址,这样当 Flink Master 异常重建后,能保证 taskmanager 通过 Service 仍然能连接到新的 Flink Master;
  5. TaskManager 创建成功后注册到 slotManager,这时 slotManager向TaskManager 申请 slots,TaskManager 提供自己的空闲 slots,任务被部署并运行。


特点分析

之前我们提到的两种部署模式,在 Kubernetes 上运行 Flink 任务是需要事先指定好 TaskManager 的数量,但是大部分情况下,用户在任务启动前是无法准确的预知该任务所需的 TaskManager 数量和规格。

指定的多了会资源浪费,指定的少了会导致任务的执行失败。最根本的原因,就是没有 Native 的使用 Kubernetes 资源,这里的 Native,可以理解为 Flink 直接与 Kuberneter 通信来申请资源。

这种类型的集群,也是在提交任务之前就创建好了,不过只包含了 FlinkMaster 及其 Entrypoint(Service),当任务提交的时候,Flink client 会根据任务计算出并行度,进而确定出所需 TaskManager 的数量,然后 Flink 内核会直接向 Kubernetes API server 申请 taskmanager,达到资源动态创建的目的。

优点:相对于前两种集群而言,taskManager 的资源是实时的、按需进行的创建,对资源的利用率更高,所需资源更精准。

缺点:taskManager 是实时创建的,用户的作业真正运行前,与 Per Job集群一样,仍需要先等待 taskManager 的创建,因此对任务启动时间比较敏感的用户,需要进行一定的权衡。

Native Per Job 模式

在当前的 Apache Flink 1.10 版本里,Flink native per-job 特性尚未发布,预计在后续的 Flink 1.11 版本中提供,我们可以提前一览 native per job 的特性。

原理分析
5.png

当任务被提交后,同样由 Flink 来向 Kubernetes 申请资源,其过程与之前提到的 native session 模式相似,不同之处在于:
  1. Flink Master是随着任务的提交而动态创建的;
  2. 用户可以将 Flink、作业 Jar 包和 classpath 依赖打包到自己的镜像里;
  3. 作业运行图由 Flink Master 生成,所以无需通过 RestClient 上传 Jar 包(图 2 步骤 3)。


特点分析

native per-job cluster 也是任务提交的时候才创建 Flink 集群,不同的是,无需用户指定 TaskManager 资源的数量,因为同样借助了 Native 的特性,Flink 直接与 Kubernetes 进行通信并按需申请资源。

优点:资源按需申请,适合一次性任务,任务执行后立即释放资源,保证了资源的利用率。

缺点:资源是在任务提交后开始创建,同样意味着对于提交任务后对延时比较敏感的场景,需要一定的权衡;。

Flink-operator

简介

分析以上四种部署模式,我们发现,对于 Flink 集群的使用,往往需要用户自行维护部署脚本,向 Kubernetes 提交各种所需的底层资源描述文件(Flink Master,TaskManager,配置文件,Service)。

在 session cluster 下,如果集群不再使用,还需要用户自行删除这些的资源,因为这类集群的资源使用了 Kubernetes 的垃圾回收机制 owner reference,在删除 Flink 集群的时候,需要通过删除资源的 Owner 来进行及联删除,这对于不熟悉 Kubernetes 的 Flink 用户来说,就显得不是很友好了。

而通过 Flink-operator,我们可以把 Flink 集群描述成 yaml 文件,这样,借助 Kubernetes 的声明式特性和协调控制器,我们可以直接管理 Flink 集群及其作业,而无需关注底层资源如 Deployment,Service,ConfigMap 的创建及维护。

当前 Flink 官方还未给出 flink-operator 方案,不过 GoogleCloudPlatform 提供了一种基于 kubebuilder 构建的 flink-operator方案。接下来,将介绍 flink-operator 的安装方式和对 Flink 集群的管理示例。

Flink-operator 原理及优势

当 Fink operator 部署至 Kubernetes 集群后, FlinkCluster 资源和 Flink Controller 被创建。其中 FlinkCluster 用于描述 Flink 集群,如 JobMaster 规格、TaskManager 和 TaskSlot 数量等;Flink Controller 实时处理针对 FlinkCluster 资源的 CRUD 操作,用户可以像管理内置 Kubernetes 资源一样管理 Flink 集群。

例如,用户通过 yaml 文件描述期望的 Flink 集群并向 Kubernetes 提交,Flink controller 分析用户的 yaml,得到 FlinkCluster CR,然后调用 API server 创建底层资源,如JobMaster Service,JobMaster Deployment,TaskManager Deployment。
6.png

通过使用 Flink Operator,有如下优势:

1. 管理 Flink 集群更加便捷

flink-operator 更便于我们管理 Flink 集群,我们不需要针对不同的 Flink 集群维护 Kubenretes 底层各种资源的部署脚本,唯一需要的,就是 FlinkCluster 的一个自定义资源的描述文件。创建一个 Flink session 集群,只需要一条 kubectl apply 命令即可,下图是 Flink Session集群的 yaml 文件,用户只需要在该文件中声明期望的 Flink 集群配置,flink-operator 会自动完成 Flink 集群的创建和维护工作。如果创建 Per Job 集群,也只需要在该 yaml 中声明 Job 的属性,如 Job 名称,Jar 包路径即可。通过 flink-operator,上文提到的四种 Flink 运行模式,分别对应一个 yaml 文件即可,非常方便。
apiVersion: flinkoperator.k8s.io/v1beta1
kind: FlinkCluster
metadata:
name: flinksessioncluster-sample
spec:
image:
name: flink:1.10.0
pullPolicy: IfNotPresent
jobManager:
accessScope: Cluster
ports:
  ui: 8081
resources:
  limits:
    memory: "1024Mi"
    cpu: "200m"
taskManager:
replicas: 1
resources:
  limits:
    memory: "2024Mi"
    cpu: "200m"
volumes:
  - name: cache-volume
    emptyDir: {}
volumeMounts:
  - mountPath: /cache
    name: cache-volume
envVars:
- name: FOO
  value: bar
flinkProperties:
taskmanager.numberOfTaskSlots: "1"

2. 声明式

通过执行脚本命令式的创建 Flink 集群各个底层资源,需要用户保证资源是否依次创建成功,往往伴随着辅助的检查脚本。借助 flink operator 的控制器模式,用户只需声明所期望的 Flink 集群的状态,剩下的工作全部由 Flink operator 来保证。在 Flink 集群运行的过程中,如果出现资源异常,如 JobMaster 意外停止甚至被删除,Flink operator 都会重建这些资源,自动的修复 Flink 集群。

3. 自定义保存点

用户可以指定 autoSavePointSeconds 和保存路径,Flink operator 会自动为用户定期保存快照。

4. 自动恢复

流式任务往往是长期运行的,甚至 2-3 年不停止都是常见的。在任务执行的过程中,可能会有各种各样的原因导致任务失败。用户可以指定任务重启策略,当指定为 FromSavePointOnFailure,Flink operator 自动从最近的保存点重新执行任务。

5. sidecar containers

sidecar 容器也是 Kubernetes 提供的一种设计模式,用户可以在 TaskManager Pod 里运行 sidecar 容器,为 Job 提供辅助的自定义服务或者代理服务。

6. Ingress 集成

用户可以定义 Ingress 资源,flink operator 将会自动创建 Ingress 资源。云厂商托管的 Kubernetes 集群一般都有 Ingress 控制器,否则需要用户自行实现 Ingress controller。

7. Prometheus 集成

通过在 Flink 集群的 yaml 文件里指定 metric exporter 和 metric port,可以与 Kubernetes 集群中的 Prometheus 进行集成。

最后

通过本文,我们了解了 Flink 在 Kubernetes 上运行的不同模式,其中 Native 模式在资源按需申请方面比较突出,借助 Kubernetes Operator,我们可以将 Flink 集群当成Kubernetes原生的资源一样进行 CRUD 操作。限于篇幅,本文主要分析了 Flink 在 Kubernetes 上的运行模式的区别,后续将会有更多的文章来对 Flink 在 Kubernetes 上的最佳实践进行描述,敬请期待。

原文链接:https://mp.weixin.qq.com/s/61AQTG-LjkSThcHN4gSrNQ 收起阅读 »

DockOne微信分享(二六一):浅谈一家全球电商在Kubernetes环境上的CI/CD落地与实践


【编者的话】云原生技术生态近几年狂飙猛进,现已成为互联网公司的主流服务端技术栈。公司要快速响应市场变化和需求变更,就离不开自动化流水线进行编译、打包和部署,如何基于Kubernetes落地CI/CD就是DevOps团队需要解决的首要问题之一,同时也是衡量公司DevOps能力成熟度的重要指标之一。本文主要分享iHerb在Kubernetes技术栈中CI/CD落地的情况和实施过程中的一些经验总结。

背景

本人目前就职于一家全球电商公司,公司总部设在美国, 自1997年开办公司发展到现在,已经面向包括中国在内全球170多个国家和地区开放了线上购物电商业务。

对于这样的业务体量,在国内云应用才刚起步的时候公司早以开始使用一些大型云平台诸如AWS和GCP等来部署生产环境了。

而当Kubernetes问世之后,公司就开始尝试着使用Kubernetes来部署应用,并在这几年将一些原本用VM部署的应用迁移到了Kubernetes上,以适应更快的市场变化,以及以更快的速度开辟新的市场。

对于Kubernetes的特性、优点和优势在此就不做赘述了,本文着重论要论述的是我们在Kubernetes环境上的CI/CD落地和实践。

持续集成与持续交付

首先我们来温习一下CI/CD的理念。

持续集成是一种编码理念和一系列实践,可以促使开发团队实施小的更新并经常将代码检入版本控制存储库。由于大多数现代应用程序需要在不同平台和工具中开发代码,因此团队需要一种机制来集成和验证其更改。

CI的技术目标是建立一致的自动化方法来构建,打包和测试应用程序。通过集成流程的一致性,团队更有可能更频繁地提交代码更改, 更频繁、更早的集成意味着更早的发现问题。通过持续集成,可以及时发现和解决代码故障,提高代码质量,减少故障处理成本等等,从而实现更好的协作,提高软件交付质量。

CD则是在从持续集成结束的地方开始处理制品的持续交付,负责自动将应用程序交付到选定的基础架构环境。大多数团队都会使用多个非生产环境(例如开发和测试环境),所以需要CD流水线确保有一种自动化的方式将制品包推送到目标环境上。然后,CD自动化脚本可能需要重新启动Web服务器,数据库或其他服务,并执行一些必要的服务调用,或者在部署应用程序时遵循其他过程。

持续集成/持续交付(CI/CD)是一种软件开发实践。在流水线中若再加上如自动回归、性能测试等自动化测试环节,则可以帮助团队成员频繁、快速得集成代码,以测试他们的工作成果,从而尽快发现集成后产生的错误,最终为用户提供高质量的应用程序和代码。

持续集成(CI)和持续交付(CD)体现了一种文化,一系列操作原则和实践集合,使应用程序开发团队能够更频繁,更可靠地交付代码更新。所以CI/CD流水线是DevOps团队实施的最佳实践之一,也是衡量公司DevOps技术成熟度最重要的指标之一。

虽然大部分公司在使用Kubernetes之前就已经搭建了CI/CD流水线,而在业务迁移到Kubernetes环境上后,如何在Kubernetes上搭建CI/CD流水线就成了必须尽快解决的一个重要问题。

公司的Infra架构

首先给各位看下图:
1.png

Infra架构图

如上图所示是当前公司Infrastructure架构,最底层是多云平台,这样可以节省机房的运维成本。在云平台上同时在使用VM机和云厂商的Kuberneters集群,通过VPN将办公网络与云平台网络连接起来。

在每个Kubernetes集群之内,搭建了一些公用基础设施和应用比如nginx-ingress、Istio、Datadog、Promethues等等,再上面一层就是按照业务逻辑拆分成了各个namespace,以实现业务层的相互隔离。

CI

在看完了Infra架构后,我们现在来看下怎样搭建CI流程。

CI架构

目前我们在CI流程中用到的平台和工具包括:
  • Bitbucket:用来管理源码和yaml配置
  • Jenkins:CI Pipeline引擎
  • JFrog:中间制品仓库,以节省自建和维护制品仓库的成本
  • SonarQube:存储和管理Unit test和Code analysis的报表(现已换成SonarCloud)
  • Allure report:另一个使用比较多的测试工具
  • Helm:打Helm包用


架构图如下所示:
2.png

CI架构图

CI流程

我们在编写CI流程的时候,给开发提供了一份yaml文件(build.yaml),用来让开发人员可以按照不同项目的需求来配置流水线,以实现高度定制的需求。具体流程可参考下图,其中开发可自行删减和调整任一节点。
3.png

CI流程图

首先我们看下CI任务里面包含了Pre-Executions、Code Scan、Artifacts、Helm Pacakge、Finish Executions五大节点,在大的步骤节点内可由开发人员自由配置组合需要执行的任务和需要调用的组件。
  • Pre-Executions主要内容为拉源码包,执行shell脚本,执行编译脚本,执行unit test 或其它test 脚本。
  • Code Scan环节主要执行些第三方的代码扫描工具并提交报告到相关平台上。
  • Artifacts环节以打docker image为主,将编译的结果打包到镜像里然后提交到如JFrog这样的制品仓库上。
  • Helm Package环节则是通过之前生成好的docker image自动填入Helm包并将打好的helm package提交到JFrog上。
  • Finish是CI的最后一个阶段,用于在流水线结束前执行些自定义命令,或是发送消息给Chat、Email或其它相关平台。 如果用户需要自动发布到测试环境,也是从该节点发出请求转到CD 环节。


总之,在走完CI pipeline后所有的产出都会统一保存到JFrog上。与CD交互也是靠制品仓库做连接,这样可以保证CI与CD之间的隔离。

CD

公司早期是使用Harness来进行CD部署的,但因后来发现有些部署问题以及安全漏洞,不太适合公司环境,所以打算用另外的方案来代替Harness。

我们在做CD选型方案的时候团队以及几位领导希望在资源和人手有限的情况下能快速、安全、稳定地搭建CD Pipeline,所以多方比较之下还是选择了用Jenkins + Helm的方案来做CD。

CD架构

我们在CD流程中用到的工具及作用如下:
  • JFrog:用于分发helm chart和docker image
  • Bitbucket:用来管理helm values config配置信息
  • Helm:部署helm chart用
  • Jenkins:CD Pipeline引擎,以及用来存储Kubernetes clusters的Token。我们公司在CD搭建的过程中将Jenkins拆分成了两个,一个专供CI之用,另一个只用来处理CD操作,这样实现了CI/CD相互隔离,互不影响。


CD流程

CD的核心就是通过输入各种参数最终自动生成所需要的Helm指令,其间还需要拿到各个cluster的config values yaml文件,以便根据不同的cluster来配置不同的程序运行环境设置。具体流程图可以参考下图:
4.png

CD流程图

在CD中我们就没有再用yaml来控制流水线,因为CD的Pipeline相对来说会简单些,不过另外有用到config file mangement来管理CD的一些app相关配置项。

经验总结

实施中的一些经验之谈

在Kubernetes上的CI/CD与以前在VM servers上的发布有了本质的区别,抽象出来的配置项多了很多,不仅要学习Kubernetes的yaml配置,也要学习Helm的配置,甚至还得看Istio的一些配置,所以对于开发来说也有个学习的过程,并且会占用他们一部分时间去适应和调整这些配置。

虽然DevOps team可以通过自动化脚本及UI界面将这些yaml配置封装起来,提升Pipeline的易用性,但这样做不仅会大量占用DevOps团队的人力成本和时间,同时也剥夺了开发接触Kubernetes和Helm的机会,所以就算有些开发对于Kubernetes或Helm完全是0基础但还是允许他们可以自行配置helm chart,并在不断试错中成长,毕竟helm chart只有在配置正确的情况下才会发布成功,虽然这会占用开发的一些时间。

Troubleshooting

另外关于CI/CD的Troubleshooting问题,因为Jenkins的console log功能比较强大,能把很多过程和日志输出来让用户直接在浏览器中就可以查看,在加上我们在Pipeline中埋入的很多输出点,开发完全可以全程自行Troubleshooting查找问题,同时这样也可以帮助开发理解和掌握Kubernetes及Helm的相关知识。

安全问题

在搭建Pipeline的时候在安全方面需要注意一下,不能将credential信息以明文方式输出到console log,且也不能在source code和config仓库中出现明文的账密信息,如若需要配置账密,我们的做法是将密码保存在config repo的secrets.yaml文件中,并用helm secret + aws key的方案对其进行加密操作,以弥补Kubernetes secret机制本身的一些不足。

后记

虽然目前在业界CI/CD方案中Jenkins的热度一直很高,不仅系统稳定,功能强大,插件资源丰富,而且社区支持较好,相关资料也比较多;但是考虑到其单点master部署以及JVM的执行效率问题,我们还是希望将来能使用云原生基于Golang开发的CI CD解决方案。

我们拿了目前云原生开源CI/CD多个产品进行了横向比较,结论是目前CI这块还是Jenkins做得最好,不过CD阶段可以考虑用其它云原生的开源软件比如Spinnaker、Tekton以及Knative等来代替。但万变不离其宗的一点是我们不仅要把系统搭建起来,还得让它变得适合给开发人员使用,同时即要保证稳定性也要提供扩展性,以应对复杂多变的需求。

所以我们也在开始尝试自主开发公司的CD Portal,并逐步把一些功能往上面迁移,希望将来能用CD Portal将所有的开源软件都封装起来,从而提升公司所有开发人员编译部署以及管理线上系统配置和版本发布的效率。

Q&A

Q:deploy阶段是如何保证线上服务稳定性的?
A:因为是用的Helm在Kubernetes cluster上进行安装,所以通过Helm 的版本管理机制+ Kubernetes自带的滚动升级机制,再加上cluster可切换流量的机制,通过多重保护机制来保证服务的稳定性。另外就是要通过监控的手段如大家耳熟能的Prometheus、ELK、Grafana等来监控服务的稳定性。

Q:API版本管理是如何做的?如果出现不向后兼容的API改动,Rolling Update应该不好用了,如果要保证服务的可获得性(无down time),这种情况应该如何发布?
A:1. 对开发团队来说需要在代码层面处理好API的兼容性问题,并通过各种测试手段来做确保API的可用性。2. 对Infra team来说,会通过如Istio这样的Service Mesh平台,通过与Istio结合实现Canary发布,再加上监控系统来确保服务的可用性。

Q:Jenkins是跑在Kubernetes上吗?Jenkins X?
A:是的,我们是将Jenkins用Helm安装到Kubernetes上,并给到每个Jenkins独立的Node资源,因为Jenkins在执行job的时候大多会起slave pod,再加上Jenkins master本身也比较吃内存,所以对资源的要求会比较高。Jenkins X以前有做过调研,问题1是很难将现在在Jenkins上已建好的项目迁移到Jenkins X;2是有安全问题;3是不符合我们公司的情况,因为Jenkins X是将CI/CD整条线都放在一套系统内,而我们公司实际的需求是希望CI/CD分开,并且需要方便在中间插入安检与测试环节。所以就没有选择JenkinsX 方案。

Q:Helm Chart是跟服务垂直打包发布吗(单独项目)?
A:如果你指的是通过Helm umbrella模式发布的话,目前我们实际应用中几乎没有这样的案例。目前是每个team自行发布application的Helm包,不做依赖发布。这样好处是实现了互相解耦,况且现在用到umbrella的情况也不多。如果真有需要依赖发布的话我们也是支持的。

Q:DevOps的发布过程你们运行哪些自动化测试?
A:CI阶段支持各种语言自带的unit test,然后是第三方的如Cobertura、SonarQube、JaCoCo等,而且我们也开发了功能支持在CI过程中的mock test。CD之后,主要是在test环境,可支持allure + pytest、kubejob test、Selenium/Zalenium、performance test等,这部分主要由QA团队来主导,我们负责在Pipeline中加入对test的支持。

Q:代码和软件配置是分开的吗?一般环境运行的配置改变不需要重新编译打包项目,这一步你们怎么实现的?
A:是分开的,配置项目前是另外在bitbucket上开了config repo来实现的,在CD环节会将config导入进来,最终整合到Helm命令里。

Q:docker image的tag管理是如何进行的?如何和代码commit hash以及部署环境对应起来?Kubernetes的service/deployments/pods都会打哪些tags和annotations?
A:一般tag我们是在CI Pipeline中自动打上去的,tag里会加上commit hash前7位字符,在image上打完tag后,会由程序自动填入到helm package里。Kubernetes的service/deployments/pods的 tags和annotations一般由开发团队按需求自行配置,一般开发最关心的deployment的配置还是在于namespace、replicas、resources、Liveness 和Readiness。 我们自行写了一些helm template给开发用,这样也算是简化了Helm的使用

Q:为何又有Prometheus之后,架构图里还有Datadog,能讲讲两者区别么,什么场景下,两者都用了。什么原因呢?
A:Prometheus是Kubernetes技术栈必用的监控方案,后续的Kibana和Grafana都要用到Prometheus的数据,开发人员在Helm包里也会加上很多Prometheus rules用于监控服务。Datadog则是买的第三方服务,是给审计、安全和NOC团队用的,对于这些专业团队会在Prometheus的基础上结合两边的数据一起看。Datadog目前提供的是数据聚合和监控功能,算是相对来说比较便宜的APM,如要实现数据分析则需另寻它物。

Q:GitLab CI/CD不是很好用更适合程序员吗?为啥一定要用Jenkins?
A:GitLab CI/CD比较适合小团队来使用,如果要涉及到多种Plugins的支持以及多平台的接入,和面向公司级别的CI/CD系统,目前来说还是Jenkins做得比较好。

Q:开发人员如果想要登陆Kubernetes集群debug,这一块针对Kubernetes的权限控制是什么样的?
A:我们是用Rancher来实现Kubernetes权限管理和控制的,开发可以通过Rancher登录上去看Pod的log或做Debug, 不过大部分问题都可以在CD Job的console log直接看到,这样也省去了登录的麻烦。

Q:CD情景下应用热升级是什么样方案做的?
A:Helm + Kubernetes + Istio Canary + cluster 热备。

Q:看到贵公司有用到Rancher,请问你们使用的RKE吗?还是自己搭建Kubernetes然后import进去?有没有遇到什么坑,可以分享下,谢谢~
A:我们使用的都是云平台的Kubernetes,然后在此基础上装了Rancher agent,Rancher是美国Infra团队搭建的。

Q:可以分享一下贵公司CI/CD的Git流以及环境流转吗?
A:Git流看业务team需求,有用标准git flow的,也有用Feature分支工作流的。 环境流转就是test -> preprod -> prd流转,都是用的云上的Kubernetes cluster。

Q:可以谈谈贵公司蓝绿发布是如何做的吗?
A:之前是直接使用Helm发布的,用的是Kubernetes自带的蓝绿发布。现在支持用Istio Canary模式发布,这块内容比较多,都可以另外起个topic了,不适合在QA里讲。

Q;您这一块把CI/CD分开主要是基于什么考虑,CD是由CI自动触发然后自动部署的吗?跨Jenkins的调用是如何实现的呢?
A:之所以要分开,1是因为希望资源互不干扰,另外还有安全上的考虑; 2是因为团队需要,因为他们并不希望CI后就直接CD,而且也希望将两边的job分开,这样也好排查问题。3是因为CD的时候需要对config进行调整和试错,这样就不用把Pipeline拉得太长还得从CI开始走。4是希望将来方便在CD这块不用Jenkins而是改用别的系统。CD 是手动或webhook触发的。跨Jenkins调用也可以通过webhook互相调用,在这里我们选用的是“Generic Webhook Trigger”插件。

Q:可以谈谈分支管理,以及中间件变更的管理吗?
A:关于分支管理如果是指代码层的,建议走git flow ,如果您指的是分支环境的部署,建议是Canary方案部署,然后用Istio导流,并通过API参数来判断调用哪个分支。中间件变更如果是如npm包或是nuget package这样的包管理的话,CI是支持自动build和提交的,对于开发来说只要配好包的仓库(我们是用的JFrog来管的),配置好包的version就可以了。

Q:a. 当系统有多个子系统,每个子系统都有一套自己的charts和value时,在贵CI系统中是如何将各子系统集成来做整体测试的呢?b. 想了解下贵集群的扩容和缩容机制,在既加快新增部署又减少浪费方面有什么好的实践可以分享么?c. CI中部署系统成功后,是如何进行测试的呢?
A:a. 在CI中要通过docker-compose.yaml来mock出这套环境,因为安全问题所以CI环境是无法连到DB以及cluster的。或者在UT中开发由开发做mock 测试,相对来说会简单点。b. 直接买云上的Kubernetes,AWS/GKE等大厂云都早已解决了这个问题,里面有关于弹性伸缩的配置。c. 部署成功后会由QA编写和执行测试脚本来进行测试,我们在CD的配置项里做了这个功能,就是CD之后自动触发QA job并开始执行测试代码。

Q:生产环境如何使用的CI/CD?这套CI/CD 有没有快速搭建的脚本?
A:我们这套不是开箱即用的,只能一点点搭上去,而且还有开发和推广的工作在里面。如果想要开箱即用的可以试下Jenkins X。

Q:配置文件管理是怎么做的?不同环境的构建参数不同,如何在CI中体现,CD之前要重新构建吗,还是复用CI的image?
A:1. 配置数据都在helm values.yaml里,然后我们把config repo里的values.yaml拉下来到jenkins job workspace,再通过Helm整合进行发布;2. CD的时候是复用CI的image 的。

Q:测试多环境如何在CI/CD中复用配置,比如有四套测试环境,不会每个环境否配置套新的CI/CD吧?
A:只要在config repo中跟据cluster配置不同的values.yaml,比如values.cluster1.yaml,values.cluster2.yaml,这样就可以了,然后只要执行CD即可。

Q:Ingress使用了没,太长的域名,不太合适生产使用吧?
A:有用到Ingress,域名由开发团队自己定,长域名也支持的。

Q:如何工程化调试项目,比如有500个应用,每个都有4套以上环境,量很大,怎么快速调试?
A:关于您说的这个需求在我们公司还没有尝试过。但要如果要实现这样的场景,从理论上来说,首先Kubernetes cluster可以自动生成出来,其次在部署之前config repo也要按cluster环境配好,然后可以用helm umbrella来实现整个集群应用的自动化搭建部署,最后test script覆盖率要达到一个很高的百分比才行。在这里自动化测试是个关键因素,不过我不是QA专家,所以还是得请教QA的人看是否能实现这样的测试量级。

以上内容根据2020年5月26日晚微信群分享内容整理。 分享人黄劲(Victor Huang), iHerb DevOps工程师,目前是公司的CI/CD相关技术和Jenkins平台负责人。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesf,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。
继续阅读 »

【编者的话】云原生技术生态近几年狂飙猛进,现已成为互联网公司的主流服务端技术栈。公司要快速响应市场变化和需求变更,就离不开自动化流水线进行编译、打包和部署,如何基于Kubernetes落地CI/CD就是DevOps团队需要解决的首要问题之一,同时也是衡量公司DevOps能力成熟度的重要指标之一。本文主要分享iHerb在Kubernetes技术栈中CI/CD落地的情况和实施过程中的一些经验总结。

背景

本人目前就职于一家全球电商公司,公司总部设在美国, 自1997年开办公司发展到现在,已经面向包括中国在内全球170多个国家和地区开放了线上购物电商业务。

对于这样的业务体量,在国内云应用才刚起步的时候公司早以开始使用一些大型云平台诸如AWS和GCP等来部署生产环境了。

而当Kubernetes问世之后,公司就开始尝试着使用Kubernetes来部署应用,并在这几年将一些原本用VM部署的应用迁移到了Kubernetes上,以适应更快的市场变化,以及以更快的速度开辟新的市场。

对于Kubernetes的特性、优点和优势在此就不做赘述了,本文着重论要论述的是我们在Kubernetes环境上的CI/CD落地和实践。

持续集成与持续交付

首先我们来温习一下CI/CD的理念。

持续集成是一种编码理念和一系列实践,可以促使开发团队实施小的更新并经常将代码检入版本控制存储库。由于大多数现代应用程序需要在不同平台和工具中开发代码,因此团队需要一种机制来集成和验证其更改。

CI的技术目标是建立一致的自动化方法来构建,打包和测试应用程序。通过集成流程的一致性,团队更有可能更频繁地提交代码更改, 更频繁、更早的集成意味着更早的发现问题。通过持续集成,可以及时发现和解决代码故障,提高代码质量,减少故障处理成本等等,从而实现更好的协作,提高软件交付质量。

CD则是在从持续集成结束的地方开始处理制品的持续交付,负责自动将应用程序交付到选定的基础架构环境。大多数团队都会使用多个非生产环境(例如开发和测试环境),所以需要CD流水线确保有一种自动化的方式将制品包推送到目标环境上。然后,CD自动化脚本可能需要重新启动Web服务器,数据库或其他服务,并执行一些必要的服务调用,或者在部署应用程序时遵循其他过程。

持续集成/持续交付(CI/CD)是一种软件开发实践。在流水线中若再加上如自动回归、性能测试等自动化测试环节,则可以帮助团队成员频繁、快速得集成代码,以测试他们的工作成果,从而尽快发现集成后产生的错误,最终为用户提供高质量的应用程序和代码。

持续集成(CI)和持续交付(CD)体现了一种文化,一系列操作原则和实践集合,使应用程序开发团队能够更频繁,更可靠地交付代码更新。所以CI/CD流水线是DevOps团队实施的最佳实践之一,也是衡量公司DevOps技术成熟度最重要的指标之一。

虽然大部分公司在使用Kubernetes之前就已经搭建了CI/CD流水线,而在业务迁移到Kubernetes环境上后,如何在Kubernetes上搭建CI/CD流水线就成了必须尽快解决的一个重要问题。

公司的Infra架构

首先给各位看下图:
1.png

Infra架构图

如上图所示是当前公司Infrastructure架构,最底层是多云平台,这样可以节省机房的运维成本。在云平台上同时在使用VM机和云厂商的Kuberneters集群,通过VPN将办公网络与云平台网络连接起来。

在每个Kubernetes集群之内,搭建了一些公用基础设施和应用比如nginx-ingress、Istio、Datadog、Promethues等等,再上面一层就是按照业务逻辑拆分成了各个namespace,以实现业务层的相互隔离。

CI

在看完了Infra架构后,我们现在来看下怎样搭建CI流程。

CI架构

目前我们在CI流程中用到的平台和工具包括:
  • Bitbucket:用来管理源码和yaml配置
  • Jenkins:CI Pipeline引擎
  • JFrog:中间制品仓库,以节省自建和维护制品仓库的成本
  • SonarQube:存储和管理Unit test和Code analysis的报表(现已换成SonarCloud)
  • Allure report:另一个使用比较多的测试工具
  • Helm:打Helm包用


架构图如下所示:
2.png

CI架构图

CI流程

我们在编写CI流程的时候,给开发提供了一份yaml文件(build.yaml),用来让开发人员可以按照不同项目的需求来配置流水线,以实现高度定制的需求。具体流程可参考下图,其中开发可自行删减和调整任一节点。
3.png

CI流程图

首先我们看下CI任务里面包含了Pre-Executions、Code Scan、Artifacts、Helm Pacakge、Finish Executions五大节点,在大的步骤节点内可由开发人员自由配置组合需要执行的任务和需要调用的组件。
  • Pre-Executions主要内容为拉源码包,执行shell脚本,执行编译脚本,执行unit test 或其它test 脚本。
  • Code Scan环节主要执行些第三方的代码扫描工具并提交报告到相关平台上。
  • Artifacts环节以打docker image为主,将编译的结果打包到镜像里然后提交到如JFrog这样的制品仓库上。
  • Helm Package环节则是通过之前生成好的docker image自动填入Helm包并将打好的helm package提交到JFrog上。
  • Finish是CI的最后一个阶段,用于在流水线结束前执行些自定义命令,或是发送消息给Chat、Email或其它相关平台。 如果用户需要自动发布到测试环境,也是从该节点发出请求转到CD 环节。


总之,在走完CI pipeline后所有的产出都会统一保存到JFrog上。与CD交互也是靠制品仓库做连接,这样可以保证CI与CD之间的隔离。

CD

公司早期是使用Harness来进行CD部署的,但因后来发现有些部署问题以及安全漏洞,不太适合公司环境,所以打算用另外的方案来代替Harness。

我们在做CD选型方案的时候团队以及几位领导希望在资源和人手有限的情况下能快速、安全、稳定地搭建CD Pipeline,所以多方比较之下还是选择了用Jenkins + Helm的方案来做CD。

CD架构

我们在CD流程中用到的工具及作用如下:
  • JFrog:用于分发helm chart和docker image
  • Bitbucket:用来管理helm values config配置信息
  • Helm:部署helm chart用
  • Jenkins:CD Pipeline引擎,以及用来存储Kubernetes clusters的Token。我们公司在CD搭建的过程中将Jenkins拆分成了两个,一个专供CI之用,另一个只用来处理CD操作,这样实现了CI/CD相互隔离,互不影响。


CD流程

CD的核心就是通过输入各种参数最终自动生成所需要的Helm指令,其间还需要拿到各个cluster的config values yaml文件,以便根据不同的cluster来配置不同的程序运行环境设置。具体流程图可以参考下图:
4.png

CD流程图

在CD中我们就没有再用yaml来控制流水线,因为CD的Pipeline相对来说会简单些,不过另外有用到config file mangement来管理CD的一些app相关配置项。

经验总结

实施中的一些经验之谈

在Kubernetes上的CI/CD与以前在VM servers上的发布有了本质的区别,抽象出来的配置项多了很多,不仅要学习Kubernetes的yaml配置,也要学习Helm的配置,甚至还得看Istio的一些配置,所以对于开发来说也有个学习的过程,并且会占用他们一部分时间去适应和调整这些配置。

虽然DevOps team可以通过自动化脚本及UI界面将这些yaml配置封装起来,提升Pipeline的易用性,但这样做不仅会大量占用DevOps团队的人力成本和时间,同时也剥夺了开发接触Kubernetes和Helm的机会,所以就算有些开发对于Kubernetes或Helm完全是0基础但还是允许他们可以自行配置helm chart,并在不断试错中成长,毕竟helm chart只有在配置正确的情况下才会发布成功,虽然这会占用开发的一些时间。

Troubleshooting

另外关于CI/CD的Troubleshooting问题,因为Jenkins的console log功能比较强大,能把很多过程和日志输出来让用户直接在浏览器中就可以查看,在加上我们在Pipeline中埋入的很多输出点,开发完全可以全程自行Troubleshooting查找问题,同时这样也可以帮助开发理解和掌握Kubernetes及Helm的相关知识。

安全问题

在搭建Pipeline的时候在安全方面需要注意一下,不能将credential信息以明文方式输出到console log,且也不能在source code和config仓库中出现明文的账密信息,如若需要配置账密,我们的做法是将密码保存在config repo的secrets.yaml文件中,并用helm secret + aws key的方案对其进行加密操作,以弥补Kubernetes secret机制本身的一些不足。

后记

虽然目前在业界CI/CD方案中Jenkins的热度一直很高,不仅系统稳定,功能强大,插件资源丰富,而且社区支持较好,相关资料也比较多;但是考虑到其单点master部署以及JVM的执行效率问题,我们还是希望将来能使用云原生基于Golang开发的CI CD解决方案。

我们拿了目前云原生开源CI/CD多个产品进行了横向比较,结论是目前CI这块还是Jenkins做得最好,不过CD阶段可以考虑用其它云原生的开源软件比如Spinnaker、Tekton以及Knative等来代替。但万变不离其宗的一点是我们不仅要把系统搭建起来,还得让它变得适合给开发人员使用,同时即要保证稳定性也要提供扩展性,以应对复杂多变的需求。

所以我们也在开始尝试自主开发公司的CD Portal,并逐步把一些功能往上面迁移,希望将来能用CD Portal将所有的开源软件都封装起来,从而提升公司所有开发人员编译部署以及管理线上系统配置和版本发布的效率。

Q&A

Q:deploy阶段是如何保证线上服务稳定性的?
A:因为是用的Helm在Kubernetes cluster上进行安装,所以通过Helm 的版本管理机制+ Kubernetes自带的滚动升级机制,再加上cluster可切换流量的机制,通过多重保护机制来保证服务的稳定性。另外就是要通过监控的手段如大家耳熟能的Prometheus、ELK、Grafana等来监控服务的稳定性。

Q:API版本管理是如何做的?如果出现不向后兼容的API改动,Rolling Update应该不好用了,如果要保证服务的可获得性(无down time),这种情况应该如何发布?
A:1. 对开发团队来说需要在代码层面处理好API的兼容性问题,并通过各种测试手段来做确保API的可用性。2. 对Infra team来说,会通过如Istio这样的Service Mesh平台,通过与Istio结合实现Canary发布,再加上监控系统来确保服务的可用性。

Q:Jenkins是跑在Kubernetes上吗?Jenkins X?
A:是的,我们是将Jenkins用Helm安装到Kubernetes上,并给到每个Jenkins独立的Node资源,因为Jenkins在执行job的时候大多会起slave pod,再加上Jenkins master本身也比较吃内存,所以对资源的要求会比较高。Jenkins X以前有做过调研,问题1是很难将现在在Jenkins上已建好的项目迁移到Jenkins X;2是有安全问题;3是不符合我们公司的情况,因为Jenkins X是将CI/CD整条线都放在一套系统内,而我们公司实际的需求是希望CI/CD分开,并且需要方便在中间插入安检与测试环节。所以就没有选择JenkinsX 方案。

Q:Helm Chart是跟服务垂直打包发布吗(单独项目)?
A:如果你指的是通过Helm umbrella模式发布的话,目前我们实际应用中几乎没有这样的案例。目前是每个team自行发布application的Helm包,不做依赖发布。这样好处是实现了互相解耦,况且现在用到umbrella的情况也不多。如果真有需要依赖发布的话我们也是支持的。

Q:DevOps的发布过程你们运行哪些自动化测试?
A:CI阶段支持各种语言自带的unit test,然后是第三方的如Cobertura、SonarQube、JaCoCo等,而且我们也开发了功能支持在CI过程中的mock test。CD之后,主要是在test环境,可支持allure + pytest、kubejob test、Selenium/Zalenium、performance test等,这部分主要由QA团队来主导,我们负责在Pipeline中加入对test的支持。

Q:代码和软件配置是分开的吗?一般环境运行的配置改变不需要重新编译打包项目,这一步你们怎么实现的?
A:是分开的,配置项目前是另外在bitbucket上开了config repo来实现的,在CD环节会将config导入进来,最终整合到Helm命令里。

Q:docker image的tag管理是如何进行的?如何和代码commit hash以及部署环境对应起来?Kubernetes的service/deployments/pods都会打哪些tags和annotations?
A:一般tag我们是在CI Pipeline中自动打上去的,tag里会加上commit hash前7位字符,在image上打完tag后,会由程序自动填入到helm package里。Kubernetes的service/deployments/pods的 tags和annotations一般由开发团队按需求自行配置,一般开发最关心的deployment的配置还是在于namespace、replicas、resources、Liveness 和Readiness。 我们自行写了一些helm template给开发用,这样也算是简化了Helm的使用

Q:为何又有Prometheus之后,架构图里还有Datadog,能讲讲两者区别么,什么场景下,两者都用了。什么原因呢?
A:Prometheus是Kubernetes技术栈必用的监控方案,后续的Kibana和Grafana都要用到Prometheus的数据,开发人员在Helm包里也会加上很多Prometheus rules用于监控服务。Datadog则是买的第三方服务,是给审计、安全和NOC团队用的,对于这些专业团队会在Prometheus的基础上结合两边的数据一起看。Datadog目前提供的是数据聚合和监控功能,算是相对来说比较便宜的APM,如要实现数据分析则需另寻它物。

Q:GitLab CI/CD不是很好用更适合程序员吗?为啥一定要用Jenkins?
A:GitLab CI/CD比较适合小团队来使用,如果要涉及到多种Plugins的支持以及多平台的接入,和面向公司级别的CI/CD系统,目前来说还是Jenkins做得比较好。

Q:开发人员如果想要登陆Kubernetes集群debug,这一块针对Kubernetes的权限控制是什么样的?
A:我们是用Rancher来实现Kubernetes权限管理和控制的,开发可以通过Rancher登录上去看Pod的log或做Debug, 不过大部分问题都可以在CD Job的console log直接看到,这样也省去了登录的麻烦。

Q:CD情景下应用热升级是什么样方案做的?
A:Helm + Kubernetes + Istio Canary + cluster 热备。

Q:看到贵公司有用到Rancher,请问你们使用的RKE吗?还是自己搭建Kubernetes然后import进去?有没有遇到什么坑,可以分享下,谢谢~
A:我们使用的都是云平台的Kubernetes,然后在此基础上装了Rancher agent,Rancher是美国Infra团队搭建的。

Q:可以分享一下贵公司CI/CD的Git流以及环境流转吗?
A:Git流看业务team需求,有用标准git flow的,也有用Feature分支工作流的。 环境流转就是test -> preprod -> prd流转,都是用的云上的Kubernetes cluster。

Q:可以谈谈贵公司蓝绿发布是如何做的吗?
A:之前是直接使用Helm发布的,用的是Kubernetes自带的蓝绿发布。现在支持用Istio Canary模式发布,这块内容比较多,都可以另外起个topic了,不适合在QA里讲。

Q;您这一块把CI/CD分开主要是基于什么考虑,CD是由CI自动触发然后自动部署的吗?跨Jenkins的调用是如何实现的呢?
A:之所以要分开,1是因为希望资源互不干扰,另外还有安全上的考虑; 2是因为团队需要,因为他们并不希望CI后就直接CD,而且也希望将两边的job分开,这样也好排查问题。3是因为CD的时候需要对config进行调整和试错,这样就不用把Pipeline拉得太长还得从CI开始走。4是希望将来方便在CD这块不用Jenkins而是改用别的系统。CD 是手动或webhook触发的。跨Jenkins调用也可以通过webhook互相调用,在这里我们选用的是“Generic Webhook Trigger”插件。

Q:可以谈谈分支管理,以及中间件变更的管理吗?
A:关于分支管理如果是指代码层的,建议走git flow ,如果您指的是分支环境的部署,建议是Canary方案部署,然后用Istio导流,并通过API参数来判断调用哪个分支。中间件变更如果是如npm包或是nuget package这样的包管理的话,CI是支持自动build和提交的,对于开发来说只要配好包的仓库(我们是用的JFrog来管的),配置好包的version就可以了。

Q:a. 当系统有多个子系统,每个子系统都有一套自己的charts和value时,在贵CI系统中是如何将各子系统集成来做整体测试的呢?b. 想了解下贵集群的扩容和缩容机制,在既加快新增部署又减少浪费方面有什么好的实践可以分享么?c. CI中部署系统成功后,是如何进行测试的呢?
A:a. 在CI中要通过docker-compose.yaml来mock出这套环境,因为安全问题所以CI环境是无法连到DB以及cluster的。或者在UT中开发由开发做mock 测试,相对来说会简单点。b. 直接买云上的Kubernetes,AWS/GKE等大厂云都早已解决了这个问题,里面有关于弹性伸缩的配置。c. 部署成功后会由QA编写和执行测试脚本来进行测试,我们在CD的配置项里做了这个功能,就是CD之后自动触发QA job并开始执行测试代码。

Q:生产环境如何使用的CI/CD?这套CI/CD 有没有快速搭建的脚本?
A:我们这套不是开箱即用的,只能一点点搭上去,而且还有开发和推广的工作在里面。如果想要开箱即用的可以试下Jenkins X。

Q:配置文件管理是怎么做的?不同环境的构建参数不同,如何在CI中体现,CD之前要重新构建吗,还是复用CI的image?
A:1. 配置数据都在helm values.yaml里,然后我们把config repo里的values.yaml拉下来到jenkins job workspace,再通过Helm整合进行发布;2. CD的时候是复用CI的image 的。

Q:测试多环境如何在CI/CD中复用配置,比如有四套测试环境,不会每个环境否配置套新的CI/CD吧?
A:只要在config repo中跟据cluster配置不同的values.yaml,比如values.cluster1.yaml,values.cluster2.yaml,这样就可以了,然后只要执行CD即可。

Q:Ingress使用了没,太长的域名,不太合适生产使用吧?
A:有用到Ingress,域名由开发团队自己定,长域名也支持的。

Q:如何工程化调试项目,比如有500个应用,每个都有4套以上环境,量很大,怎么快速调试?
A:关于您说的这个需求在我们公司还没有尝试过。但要如果要实现这样的场景,从理论上来说,首先Kubernetes cluster可以自动生成出来,其次在部署之前config repo也要按cluster环境配好,然后可以用helm umbrella来实现整个集群应用的自动化搭建部署,最后test script覆盖率要达到一个很高的百分比才行。在这里自动化测试是个关键因素,不过我不是QA专家,所以还是得请教QA的人看是否能实现这样的测试量级。

以上内容根据2020年5月26日晚微信群分享内容整理。 分享人黄劲(Victor Huang), iHerb DevOps工程师,目前是公司的CI/CD相关技术和Jenkins平台负责人。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesf,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。 收起阅读 »