Istio安全基础:在零可信网络运行微服务


上一篇关于Istio服务网格的博客中,我们概述了Istio的特性和功能,以及用户为什么希望在Kubernetes集群中用或不用它作为服务网格。在本篇文章中,我们将稍微深入地探讨服务网格中,Istio如何帮助改进应用程序的运行时的安全性,以及它在Kubernetes安全控制和实践的全局中处于什么位置。

完全不可信

Istio提供了大量的功能,来支持在零信任网络上安全地运行微服务的能力。

零信任模型是建立在网络和基础设施可能被恶意代码、错误配置或其他不利因素所渗透的可能性之上。这个模型仅仅是假定在用户的Kubernetes集群上,可以运行任何指定的微服务。需要说明的是:这个假定不表示所指定的微服务是合理的,或可以毫无疑问地被信任的。在一组相互调用的分布式微服务中,新服务随时可以出现,并且不需要修改配置的情况下,被其他服务使用或调用。这自然就衍生出Kubernetes集群中应用安全管理的一个关键组件,既与其他应用CI/CD和基础设施最佳实践相结合的,凭证识别和验证的零信任哲学。

在支持零信任网络的部分,Istio提供了附加的4-7层防火墙控制、传输加密、服务网格的高级服务和终端用户身份验证和授权的可选项,以及捕获详细连接日志和指标的能力。与Istio的大多数其他功能一样,这些功能都是由Envoy代理提供支持的。Envoy代理以sidecar(边车)容器的方式运行在每个应用程序的Kubernetes Pod中。

mTLS:信任与校验

mTLS(相互传输层安全性)是Istio安全工具集的一个基本部分。它不仅提供传输加密,还在服务网格中,提供服务对服务的身份验证和授权。

工作原理:网格中的每个服务都有自己的TLS证书,由Istio Citadel服务颁发和管理。在一个给定的网络连接中,客户端服务的Envoy代理将向服务端提供其证书,同时验证服务器的TLS证书的有效性。目标服务的Envoy代理将验证客户端证书,而且还可以通过客户端身份来判断该服务是否允许被连接。后者是基于Istio服务的RBAC(基于角色的访问控制)配置和策略配置来实现的。TLS加密的功能还表示即使应用本身不支持TLS加密,也不会通过网络发送明文的数据包。

Istio RBAC授权非常类似于Kubernetes原生RBAC的扩展。在服务网格中,ServiceRole可以在创建服务的端点权限的时候定义。它们可以绑定经过身份验证的实体,如Kubernetes服务帐户、使用JWT令牌验证身份的外部用户(具备服务访问权限)等。Istio RBAC的评估是在代理的sidecar容器中执行,因此处理延迟非常低。不过,对于大多数应用而言,上述所有的操作都是完全透明的,不需要对部署进行任何配置更改。

实现需求:默认情况下,未启用全局mTLS。用户可以在Istio安装时设置它。配置参数为:lobal.mtls.enabled=true,或安装后在每个命名空间或全局配置中打开它。操作方法链接参见:https://istio.io/docs/concepts ... ation

潜在问题:对于用于使用自己的客户端证书进行身份验证(内置HTTPS)而没有HTTP选项的应用,将无法适用Istio的mTLS功能,因为mTLS不支持隧道TCP,应用需要采用额外的Istio配置来绕过mTLS功能。
在只有一个服务子集或命名空开启了mTLS功能的集群中,在可行的情况下,必须使用Kubernetes网络策略隔离集群中不属于服务网格的资源。

策略检查:细粒度的身份验证和授权控制

Mixer是Istio的策略控制服务,在Istio服务网格中,它能够提供多种方式对应用添加访问控制。它不仅提供了许多开箱即用的适配器,它的可插拔适配器模型还允许用户在需要时部署和使用自己的验证机制。

虽然我们已经了解服务网格中,使用mTLS强制力和Istio RBAC处理服务到服务的身份验证和授权,但是来自集群外的传入连接通常不会提供有效的mTLS证书。Mixer支持JSON Web令牌(JWT)的授权方式,可以对最终用户或外部的上游服务进行身份验证。或者用户也可以通过一个自定义的Mixer适配器创建策略检查,来提供身份验证方法。

连接请求一旦经过身份验证,策略检查就将提供无数的选项来决定是否允许请求访问。根据请求的协议类型,Istio为策略检查提供大量的连接属性,其中非TLS HTTP服务的属性选项最为丰富,包括URI路径、请求头内容、查询参数和其他属性等。甚至还可以使用Istio RBAC授权,不过由于RBAC评估是基于静态规则,所以对于源自服务网格之外的连接,大概率上会不太有用。

工作原理:当为服务网格启用策略检查时,目标Envoy代理连接到Mixer策略服务,确认每个传入连接的请求是否被允许或拒绝。Mixer判断对服务应用哪些策略,并计算传入请求连接的各种属性。

实现需求:缺省情况下不启用Mixer检查的强制策略。可以在安装时使用global.disablePolicyChecks=false配置开启,也可以在安全完成后再开启。

潜在问题:因为目标Envoy代理必须连接到Istio的Mixer策略服务,以确认每个请求的授权,启用策略检查会给连接增加一些时延。当自定义或第三方适配器与Mixer一起使用时,必须确保代码质量、可靠性和部署模式,以防止后端服务不可用时,出现延迟和宕机,同时,还需确保身份验证和授权结果是正确的。

出口流量控制

Kubernetes通过标准化部署配置,减少基础设施资源分配的摩擦,来显著帮助开发人员加快现有应用迭代部署和新应用发布的速度。但这种加速并没有降低应用程序行为的监视需求,其中网络出口就是一个重要部分。基于审计,数据安全、恶意程序侵入容器后的损害控制等目的,了解和控制所访问第三方服务就变得非常重要。

默认情况下,Istio阻断了到发往服务网格外端点的所有流量,除非将外面服务显式地记录在白名单中。

工作原理:如果网格的出站服务策略设置为REGISTRY_ONLY,那当应用的Envoy代理检测到所连接的目标服务为服务网格以外的服务时,它将检查目标服务是否有ServiceEntry配置,如果配置存在,完成建立连接;如果配置不存在,则根据协议向源应用返回一个错误或断开连接。

通过部署Istio出口网关,并通过网关路由出口流量,可以进一步控制和监视出口流量。当与Kubernetes网络策略相结合时,这些策略不仅允许Istio的出站网关建立到集群外的连接,还可以帮助防止Kubernetes集群中的任何异常容器或进程绕过Envoy代理,自行建立外部连接。

实现需求:默认情况下,出口流量是不受限制的开放的。可以在安装时用这个配置:global.outboundTrafficPolicy.mode=REGISTRY_ONLY关闭,或后面在配置中开启。

潜在问题:从开放的出站策略管理思维转向只针对白名单的策略可能会有点困难,因为有许多内部和第三方应用程序都依赖SaaS服务,即使用户认为自己知道容器正在访问哪些外部站点,也会出现部分HTTP请求被重定向,发生出口连接失败的现象。失败后,HTTP 301 / 302重定向会返回给客户端,然后由客户端重新建立到新位置的连接。除此之外,一个HTTP GET可以很轻易就变成多个重定向层,每个重定向层都需要加到白名单。
$ curl -sL -D - -o /dev/null http://www.docker.io/ HTTP/1.1 301 Moved Permanently Content-length: 0 Location: https://www.docker.io/
HTTP/1.1 301 Moved Permanently 
Content-length: 0 Location: https://www.docker.com/
HTTP/2 200 content-type: text/html; charset=UTF-8 content-length: 86100
[…]

受限于Istio版本、协议和集群配置差异,Istio无法一致地记录失败的出口连接,导致确定出站连接的准确目标可能会比较困难。

默认情况下,强制所有出口流量通过出口网关是不现实的。Istio没有提供全局网关的配置,而且将流量引导到出口网关的VirtualService资源对目标地址的通配符处理也有限,这主要是由于Envoy代理中的限制所导致的。

是瑞士军刀,而非工具箱

虽然Istio为应用网络安全提供了一系列的控制措施,但它并不是一个全面的解决方案。它的推荐用法是与低层基础设施的网络控制和Kubernetes的常规安全最佳实践相结合使用。值得注意的一个重要的缺陷就是Istio不支持TCP之外的任何协议,因为Envoy代理目前只支持4层TCP协议。这也意味着,如果用户需要监视或控制服务网格中的UDP或SCTP流量,用户需要使用Kubernetes的网络策略实现。如果用户的Kubernetes服务提供者不支持网络策略,用户将必须查看他们是否提供其他网络控制方案。网络策略的可用和可配置,对Istio的控件是很好的补充和增强。同时,用户必须保护集群中Istio资源的安全性,以确保Istio的保护功能不会被错误地、恶意地覆盖或禁用。

由于Istio完全使用Kubernetes标准的资源定义(ConfigMaps和Secrets),以及Istio自己的Kubernetes自定义资源定义(CRDs)。任何具有集群级别RBAC管理特权的个人或服务帐户都可以修改定义配置,而且Istio使用Kubernetes命名空间对工作负载进行逻辑分组,所以,用户必须确保使用最小特权原则限制RBAC授权,并按照推荐的最佳安全实践进行部署和加强。

后续的Istio博客系列,将进一步深入介绍,并提供如何设置和利用Istio安全特性的示例。

原文链接:Istio Security: Running Microservices on Zero-Trust Networks(翻译:易理林)
继续阅读 »

上一篇关于Istio服务网格的博客中,我们概述了Istio的特性和功能,以及用户为什么希望在Kubernetes集群中用或不用它作为服务网格。在本篇文章中,我们将稍微深入地探讨服务网格中,Istio如何帮助改进应用程序的运行时的安全性,以及它在Kubernetes安全控制和实践的全局中处于什么位置。

完全不可信

Istio提供了大量的功能,来支持在零信任网络上安全地运行微服务的能力。

零信任模型是建立在网络和基础设施可能被恶意代码、错误配置或其他不利因素所渗透的可能性之上。这个模型仅仅是假定在用户的Kubernetes集群上,可以运行任何指定的微服务。需要说明的是:这个假定不表示所指定的微服务是合理的,或可以毫无疑问地被信任的。在一组相互调用的分布式微服务中,新服务随时可以出现,并且不需要修改配置的情况下,被其他服务使用或调用。这自然就衍生出Kubernetes集群中应用安全管理的一个关键组件,既与其他应用CI/CD和基础设施最佳实践相结合的,凭证识别和验证的零信任哲学。

在支持零信任网络的部分,Istio提供了附加的4-7层防火墙控制、传输加密、服务网格的高级服务和终端用户身份验证和授权的可选项,以及捕获详细连接日志和指标的能力。与Istio的大多数其他功能一样,这些功能都是由Envoy代理提供支持的。Envoy代理以sidecar(边车)容器的方式运行在每个应用程序的Kubernetes Pod中。

mTLS:信任与校验

mTLS(相互传输层安全性)是Istio安全工具集的一个基本部分。它不仅提供传输加密,还在服务网格中,提供服务对服务的身份验证和授权。

工作原理:网格中的每个服务都有自己的TLS证书,由Istio Citadel服务颁发和管理。在一个给定的网络连接中,客户端服务的Envoy代理将向服务端提供其证书,同时验证服务器的TLS证书的有效性。目标服务的Envoy代理将验证客户端证书,而且还可以通过客户端身份来判断该服务是否允许被连接。后者是基于Istio服务的RBAC(基于角色的访问控制)配置和策略配置来实现的。TLS加密的功能还表示即使应用本身不支持TLS加密,也不会通过网络发送明文的数据包。

Istio RBAC授权非常类似于Kubernetes原生RBAC的扩展。在服务网格中,ServiceRole可以在创建服务的端点权限的时候定义。它们可以绑定经过身份验证的实体,如Kubernetes服务帐户、使用JWT令牌验证身份的外部用户(具备服务访问权限)等。Istio RBAC的评估是在代理的sidecar容器中执行,因此处理延迟非常低。不过,对于大多数应用而言,上述所有的操作都是完全透明的,不需要对部署进行任何配置更改。

实现需求:默认情况下,未启用全局mTLS。用户可以在Istio安装时设置它。配置参数为:lobal.mtls.enabled=true,或安装后在每个命名空间或全局配置中打开它。操作方法链接参见:https://istio.io/docs/concepts ... ation

潜在问题:对于用于使用自己的客户端证书进行身份验证(内置HTTPS)而没有HTTP选项的应用,将无法适用Istio的mTLS功能,因为mTLS不支持隧道TCP,应用需要采用额外的Istio配置来绕过mTLS功能。
在只有一个服务子集或命名空开启了mTLS功能的集群中,在可行的情况下,必须使用Kubernetes网络策略隔离集群中不属于服务网格的资源。

策略检查:细粒度的身份验证和授权控制

Mixer是Istio的策略控制服务,在Istio服务网格中,它能够提供多种方式对应用添加访问控制。它不仅提供了许多开箱即用的适配器,它的可插拔适配器模型还允许用户在需要时部署和使用自己的验证机制。

虽然我们已经了解服务网格中,使用mTLS强制力和Istio RBAC处理服务到服务的身份验证和授权,但是来自集群外的传入连接通常不会提供有效的mTLS证书。Mixer支持JSON Web令牌(JWT)的授权方式,可以对最终用户或外部的上游服务进行身份验证。或者用户也可以通过一个自定义的Mixer适配器创建策略检查,来提供身份验证方法。

连接请求一旦经过身份验证,策略检查就将提供无数的选项来决定是否允许请求访问。根据请求的协议类型,Istio为策略检查提供大量的连接属性,其中非TLS HTTP服务的属性选项最为丰富,包括URI路径、请求头内容、查询参数和其他属性等。甚至还可以使用Istio RBAC授权,不过由于RBAC评估是基于静态规则,所以对于源自服务网格之外的连接,大概率上会不太有用。

工作原理:当为服务网格启用策略检查时,目标Envoy代理连接到Mixer策略服务,确认每个传入连接的请求是否被允许或拒绝。Mixer判断对服务应用哪些策略,并计算传入请求连接的各种属性。

实现需求:缺省情况下不启用Mixer检查的强制策略。可以在安装时使用global.disablePolicyChecks=false配置开启,也可以在安全完成后再开启。

潜在问题:因为目标Envoy代理必须连接到Istio的Mixer策略服务,以确认每个请求的授权,启用策略检查会给连接增加一些时延。当自定义或第三方适配器与Mixer一起使用时,必须确保代码质量、可靠性和部署模式,以防止后端服务不可用时,出现延迟和宕机,同时,还需确保身份验证和授权结果是正确的。

出口流量控制

Kubernetes通过标准化部署配置,减少基础设施资源分配的摩擦,来显著帮助开发人员加快现有应用迭代部署和新应用发布的速度。但这种加速并没有降低应用程序行为的监视需求,其中网络出口就是一个重要部分。基于审计,数据安全、恶意程序侵入容器后的损害控制等目的,了解和控制所访问第三方服务就变得非常重要。

默认情况下,Istio阻断了到发往服务网格外端点的所有流量,除非将外面服务显式地记录在白名单中。

工作原理:如果网格的出站服务策略设置为REGISTRY_ONLY,那当应用的Envoy代理检测到所连接的目标服务为服务网格以外的服务时,它将检查目标服务是否有ServiceEntry配置,如果配置存在,完成建立连接;如果配置不存在,则根据协议向源应用返回一个错误或断开连接。

通过部署Istio出口网关,并通过网关路由出口流量,可以进一步控制和监视出口流量。当与Kubernetes网络策略相结合时,这些策略不仅允许Istio的出站网关建立到集群外的连接,还可以帮助防止Kubernetes集群中的任何异常容器或进程绕过Envoy代理,自行建立外部连接。

实现需求:默认情况下,出口流量是不受限制的开放的。可以在安装时用这个配置:global.outboundTrafficPolicy.mode=REGISTRY_ONLY关闭,或后面在配置中开启。

潜在问题:从开放的出站策略管理思维转向只针对白名单的策略可能会有点困难,因为有许多内部和第三方应用程序都依赖SaaS服务,即使用户认为自己知道容器正在访问哪些外部站点,也会出现部分HTTP请求被重定向,发生出口连接失败的现象。失败后,HTTP 301 / 302重定向会返回给客户端,然后由客户端重新建立到新位置的连接。除此之外,一个HTTP GET可以很轻易就变成多个重定向层,每个重定向层都需要加到白名单。
$ curl -sL -D - -o /dev/null http://www.docker.io/ HTTP/1.1 301 Moved Permanently Content-length: 0 Location: https://www.docker.io/
HTTP/1.1 301 Moved Permanently 
Content-length: 0 Location: https://www.docker.com/
HTTP/2 200 content-type: text/html; charset=UTF-8 content-length: 86100
[…]

受限于Istio版本、协议和集群配置差异,Istio无法一致地记录失败的出口连接,导致确定出站连接的准确目标可能会比较困难。

默认情况下,强制所有出口流量通过出口网关是不现实的。Istio没有提供全局网关的配置,而且将流量引导到出口网关的VirtualService资源对目标地址的通配符处理也有限,这主要是由于Envoy代理中的限制所导致的。

是瑞士军刀,而非工具箱

虽然Istio为应用网络安全提供了一系列的控制措施,但它并不是一个全面的解决方案。它的推荐用法是与低层基础设施的网络控制和Kubernetes的常规安全最佳实践相结合使用。值得注意的一个重要的缺陷就是Istio不支持TCP之外的任何协议,因为Envoy代理目前只支持4层TCP协议。这也意味着,如果用户需要监视或控制服务网格中的UDP或SCTP流量,用户需要使用Kubernetes的网络策略实现。如果用户的Kubernetes服务提供者不支持网络策略,用户将必须查看他们是否提供其他网络控制方案。网络策略的可用和可配置,对Istio的控件是很好的补充和增强。同时,用户必须保护集群中Istio资源的安全性,以确保Istio的保护功能不会被错误地、恶意地覆盖或禁用。

由于Istio完全使用Kubernetes标准的资源定义(ConfigMaps和Secrets),以及Istio自己的Kubernetes自定义资源定义(CRDs)。任何具有集群级别RBAC管理特权的个人或服务帐户都可以修改定义配置,而且Istio使用Kubernetes命名空间对工作负载进行逻辑分组,所以,用户必须确保使用最小特权原则限制RBAC授权,并按照推荐的最佳安全实践进行部署和加强。

后续的Istio博客系列,将进一步深入介绍,并提供如何设置和利用Istio安全特性的示例。

原文链接:Istio Security: Running Microservices on Zero-Trust Networks(翻译:易理林) 收起阅读 »

在生产环境中运行容器的“六要、六不要和六管理”


容器技术让应用封装变得非常简单,容器将会成为未来最主流的部署方式。据权威咨询机构Gartner预测,到2022年全球超过75%的企业组织将在容器中运行应用程序,这与目前不到30%的比例相比有了显著的增长。

单从数据来看,近年来容器和Kubernetes在传统数据中心和云原生应用中得到很好运用,但是当前容器的生态系统并不完善,缺乏足够成熟的操作实践案例。容器集成、网络以及自动化部署仍然是非常棘手的问题。此外,由于云原生应用需要一个高度自动化基础设施环境以及专业的运维技能,导致容器在企业中应用仍然受到一定限制。如果你想和更多容器技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

在生产环境部署容器的注意事项

因此,在具体生产环境中运行容器仍然需要一个长期的学习过程。企业在生产环境中部署容器之前,一定要认真思考以下六个问题:
  • DevOps:是否拥有DevOps团队来做开发运维,能够启用敏捷开发和部署模型?
  • 工作负载:是否确定了专人来负责容器化的工作负载?
  • 快速集成:是否了解如何集成IT基础架构,以及拥有跨平台的集成能力?
  • 付费模型:是否了解使用哪种运行和编排引擎,以及他们的付费模式?
  • 培训技能:是否了解应学习哪种新技能以及采用哪种规则能确保容器部署成功?
  • ROI:投资回报率如何?


1.jpg

但是,很多企业组织经常会低估在生产环境中运行容器所需的工作量,想要让容器在企业中正常运作,要尽量避免下述六个错误的行为。
  • 在没有成熟DevOps实践经验的情况下就开始部署容器。
  • 选择了那些带有专有组件的容器导致被锁定。
  • 没有在企业组织中实施通用的工具和合规要求。
  • 没有为开发和运维人员提供前沿的技术培训服务。
  • 在选择工具的时候没有考虑开发者和运维者的需求。
  • 选择了依赖性、相关性非常大的复杂工作负载。


2.jpg

容器实践需要重点管理六个方面

企业在生产环境部署容器后,就应该格外重视容器本身的安全。例如Docker宿主机安全、Docker镜像安全、运行环境安全、编排安全等问题,这都意味着保护容器安全将是一项持续的挑战。在生产环境中部署容器,需要重点考虑安全合规、持续监控、数据持久性、网络安全问题、全生命周期管理、容器编排等问题。
3.png

安全合规

安全不能总是事后诸葛亮。它需要嵌入到DevOps过程中。企业组织需要考虑跨容器全生命周期安全问题,包括应用程序的构建、开发、部署和运行等不同阶段。
  1. 将镜像扫描集成到企业的CI/CD中,及时发现漏洞。在软件开发生命周期的构建和运行阶段对应用程序进行扫描。重点是扫描和验证开源组件、库和框架。
  2. 根据CIS基线检查安全配置。
  3. 建立强制性访问控制,针对SSL密钥或数据库凭据等敏感信息进行加密管理,只在运行时提供。
  4. 通过策略管理避免特权容器,以减少潜在攻击的影响。
  5. 部署提供白名单、行为监控和异常检测的安全产品,以防止恶意活动。


持续监控

开发人员主要关注容器在功能方面的应用,而不会去监控它们的运行情况。传统监控工具主要关注主机级指标,如CPU利用率、内存利用率、I/O、延迟和网络带宽。但这远远不够,还缺少容器或工作负载级别指标数据。
  1. 安全人员要将监控重点放在容器和服务级别上,实现细粒度监控“应用程序”,而不仅仅是物理主机。
  2. 优先考虑提供与容器编排(尤其是Kubernetes)深度集成的工具和供应商。
  3. 使用那些能够提供细粒度日志记录、提供自动服务发现、实时操作建议的工具。


容器存储

随着对有状态工作负载容器使用的增加,客户需要考虑物理主机之外数据的持久性,保护这些数据安全。即便容器不在了,数据必须还在。如果企业对容器的主要使用场景,是转移老旧应用程序或无状态用例,对存储的安全需求不会发生大变化。但是,如果要对应用程序进行重构,或提供一个新的、面向微服务的有状态应用程序,那么安全人员就需要一个存储平台,能够最大限度地提高工作负载的可用性、灵活性和性能。例如,为了更好的支持容器迁移和数据共享,Docker推出了Volume plugin接口机制,让第三方的存储厂商来支持Docker Volume并且在此基础上进行功能拓展。
  1. 选择与微服务体系结构原则一致的存储解决方案,要能满足支持API驱动、具有分布式架构、支持本地和公有云部署的服务。
  2. 避免使用专有插件和接口。相反,优先考虑与Kubernetes紧密集成的供应商,并支持标准接口,如容器存储接口(CSIs)。


容器网络

开发人员最关心就是软件开发的敏捷性和可移植性,希望应用程序能够跨软件开发生命周期进行移植。虽然在传统的企业网络模型中,IT人员为每个项目的开发、测试和生产等创建网络环境,即便如此仍然不一定能够与业务流保持很好一致性。在容器业务环境中,容器网络问题就更加复杂。例如,容器网络跨越多个层,如果直接在主机端口上开放服务虽然可行,但是部署多个应用的时会遇到端口冲突,加大扩展集群和更换主机的难度。

因此网络解决方案需要与Kubernetes原语和策略引擎紧密集成。安全和运维人员需要努力实现高度的网络自动化,并为开发人员提供适当的工具和足够的灵活性。
  1. 分析现有的容器即服务(CaaS)或软件定义网络(SDN)解决方案是否支持Kubernetes网络。如果没有,可以选择通过容器网络接口(CNI)来集成应用层网络和策略引擎。
  2. 确保所选择的CaaS、PaaS工具能为主机集群提供负载均衡方面控制,或者选择第三方代理服务器。
  3. 培训网络工程师对Linux网络、自动化网络工具的使用,弥补技能差距。


容器生命周期管理

对于高度自动化和无缝的应用交付管道,企业组织需要使用其他自动化工具来补充容器编排,例如Chef, Puppet, Ansible and Terraform等配置管理工具和应用发布自动化工具。尽管这些工具和CaaS产品之间有重叠之处,但是互补性远大于重合部分。
  1. 为容器基础镜像建立标准,考虑镜像大小、开发人员添加组件的灵活性和许可。
  2. 使用容器感知配置管理系统来管理容器镜像的生命周期,系统一旦感知到规则限定下的新版本镜像被推入仓库,则会立刻触发自动部署功能,来使用新镜像更新指定的容器。
  3. 将CaaS平台与应用程序自动化工具集成在一起,这样可以自动化整个应用程序工作流。


容器编排

因为容器编排工具管理着承载各类服务的容器集群。无论是 Kubernetes 社区还是第三方安全机构均针对 Kubernetes 中组件和资源的安全进行了相应改善和安全加固,包括计算资源安全、集群安全及相关组件安全等。这块需要重点考虑是隐私管理、授权管理、身份防控制、编排控制面、网络证书等都需要全面考虑。

其中,容器部署的关键是提供编排和调度能力。编排层与应用程序进行接口,使容器保持在所需的状态下运行。而容器调度系统按照编排层的要求将容器放在集群中最优的主机上。例如通过Apache Mesos提供调度,Marathon提供编排或使用单个工具Kubernetes或Docker Swarm提供编排和调度。客户在编排引擎之间或跨Kubernetes发行版之间进行决策时,需要重点考虑以下几个方面:
  1. 支持OS和容器运行时的深度和广度;
  2. 整体产品的运行时稳定性;
  3. 可扩展性;
  4. 对有状态应用程序的支持程度;
  5. 操作的简单性和供应商支持的质量;
  6. 对开源的支持和开发;
  7. 部署难易度及License费用;
  8. 支持混合多云。


容器技术与DevOps

容器和DevOps的关系就像是咖啡伴侣。容器能够快速发展,也得益于DevOps实践经验。在传统的开发环境,开发团队编写代码,QA团队测试软件应用程序,并将它们移交给运维团队进行日常管理。为了解决传统开发模式中的问题,很多企业都采用了“DevOps流程+微服务理论+使用容器和容器编排工具”。事实上,DevOps前身就是CI/CD,现在只不过是再加上一些发布、部署等标准和管理就构成了DevOps。

在云原生环境中,不仅软件开发和发布速度很快,而且平台本身也需要被当作一个产品来对待,因为它是动态的,并且在功能和规模方面不断发展。平台运营团队的目标是自动化、可伸缩和有弹性的标准化平台。平台运营团队的职责包括CaaS、PaaS产品的部署、操作、定制,标准化中间件的开发、操作,以及IaaS供应的自动化、部署、安全的启用等等。企业组织需要创建一个DevOps团队来运营容器,而不是一个个孤立的IT运营团队。

以容器安全为例,企业需要一个可集成至DevOps流程,又不会拖慢软件开发的方案。目前国内外有一些安全厂商已经在这方面做出卓越成绩,例如青藤容器安全解决方案,就可以提供对容器镜像扫描、入侵检测和合规基线实施情况等产品服务,化解容器所带来的安全挑战。这是一个以应用为中心、轻量级、保障容器静态资源及运行时安全的分布式解决方案,能够针对应用漏洞、不安全配置、入侵攻击、网络行为,并结合安全策略,提供覆盖容器全生命周期的、持续性安全防护。

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

容器技术让应用封装变得非常简单,容器将会成为未来最主流的部署方式。据权威咨询机构Gartner预测,到2022年全球超过75%的企业组织将在容器中运行应用程序,这与目前不到30%的比例相比有了显著的增长。

单从数据来看,近年来容器和Kubernetes在传统数据中心和云原生应用中得到很好运用,但是当前容器的生态系统并不完善,缺乏足够成熟的操作实践案例。容器集成、网络以及自动化部署仍然是非常棘手的问题。此外,由于云原生应用需要一个高度自动化基础设施环境以及专业的运维技能,导致容器在企业中应用仍然受到一定限制。如果你想和更多容器技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

在生产环境部署容器的注意事项

因此,在具体生产环境中运行容器仍然需要一个长期的学习过程。企业在生产环境中部署容器之前,一定要认真思考以下六个问题:
  • DevOps:是否拥有DevOps团队来做开发运维,能够启用敏捷开发和部署模型?
  • 工作负载:是否确定了专人来负责容器化的工作负载?
  • 快速集成:是否了解如何集成IT基础架构,以及拥有跨平台的集成能力?
  • 付费模型:是否了解使用哪种运行和编排引擎,以及他们的付费模式?
  • 培训技能:是否了解应学习哪种新技能以及采用哪种规则能确保容器部署成功?
  • ROI:投资回报率如何?


1.jpg

但是,很多企业组织经常会低估在生产环境中运行容器所需的工作量,想要让容器在企业中正常运作,要尽量避免下述六个错误的行为。
  • 在没有成熟DevOps实践经验的情况下就开始部署容器。
  • 选择了那些带有专有组件的容器导致被锁定。
  • 没有在企业组织中实施通用的工具和合规要求。
  • 没有为开发和运维人员提供前沿的技术培训服务。
  • 在选择工具的时候没有考虑开发者和运维者的需求。
  • 选择了依赖性、相关性非常大的复杂工作负载。


2.jpg

容器实践需要重点管理六个方面

企业在生产环境部署容器后,就应该格外重视容器本身的安全。例如Docker宿主机安全、Docker镜像安全、运行环境安全、编排安全等问题,这都意味着保护容器安全将是一项持续的挑战。在生产环境中部署容器,需要重点考虑安全合规、持续监控、数据持久性、网络安全问题、全生命周期管理、容器编排等问题。
3.png

安全合规

安全不能总是事后诸葛亮。它需要嵌入到DevOps过程中。企业组织需要考虑跨容器全生命周期安全问题,包括应用程序的构建、开发、部署和运行等不同阶段。
  1. 将镜像扫描集成到企业的CI/CD中,及时发现漏洞。在软件开发生命周期的构建和运行阶段对应用程序进行扫描。重点是扫描和验证开源组件、库和框架。
  2. 根据CIS基线检查安全配置。
  3. 建立强制性访问控制,针对SSL密钥或数据库凭据等敏感信息进行加密管理,只在运行时提供。
  4. 通过策略管理避免特权容器,以减少潜在攻击的影响。
  5. 部署提供白名单、行为监控和异常检测的安全产品,以防止恶意活动。


持续监控

开发人员主要关注容器在功能方面的应用,而不会去监控它们的运行情况。传统监控工具主要关注主机级指标,如CPU利用率、内存利用率、I/O、延迟和网络带宽。但这远远不够,还缺少容器或工作负载级别指标数据。
  1. 安全人员要将监控重点放在容器和服务级别上,实现细粒度监控“应用程序”,而不仅仅是物理主机。
  2. 优先考虑提供与容器编排(尤其是Kubernetes)深度集成的工具和供应商。
  3. 使用那些能够提供细粒度日志记录、提供自动服务发现、实时操作建议的工具。


容器存储

随着对有状态工作负载容器使用的增加,客户需要考虑物理主机之外数据的持久性,保护这些数据安全。即便容器不在了,数据必须还在。如果企业对容器的主要使用场景,是转移老旧应用程序或无状态用例,对存储的安全需求不会发生大变化。但是,如果要对应用程序进行重构,或提供一个新的、面向微服务的有状态应用程序,那么安全人员就需要一个存储平台,能够最大限度地提高工作负载的可用性、灵活性和性能。例如,为了更好的支持容器迁移和数据共享,Docker推出了Volume plugin接口机制,让第三方的存储厂商来支持Docker Volume并且在此基础上进行功能拓展。
  1. 选择与微服务体系结构原则一致的存储解决方案,要能满足支持API驱动、具有分布式架构、支持本地和公有云部署的服务。
  2. 避免使用专有插件和接口。相反,优先考虑与Kubernetes紧密集成的供应商,并支持标准接口,如容器存储接口(CSIs)。


容器网络

开发人员最关心就是软件开发的敏捷性和可移植性,希望应用程序能够跨软件开发生命周期进行移植。虽然在传统的企业网络模型中,IT人员为每个项目的开发、测试和生产等创建网络环境,即便如此仍然不一定能够与业务流保持很好一致性。在容器业务环境中,容器网络问题就更加复杂。例如,容器网络跨越多个层,如果直接在主机端口上开放服务虽然可行,但是部署多个应用的时会遇到端口冲突,加大扩展集群和更换主机的难度。

因此网络解决方案需要与Kubernetes原语和策略引擎紧密集成。安全和运维人员需要努力实现高度的网络自动化,并为开发人员提供适当的工具和足够的灵活性。
  1. 分析现有的容器即服务(CaaS)或软件定义网络(SDN)解决方案是否支持Kubernetes网络。如果没有,可以选择通过容器网络接口(CNI)来集成应用层网络和策略引擎。
  2. 确保所选择的CaaS、PaaS工具能为主机集群提供负载均衡方面控制,或者选择第三方代理服务器。
  3. 培训网络工程师对Linux网络、自动化网络工具的使用,弥补技能差距。


容器生命周期管理

对于高度自动化和无缝的应用交付管道,企业组织需要使用其他自动化工具来补充容器编排,例如Chef, Puppet, Ansible and Terraform等配置管理工具和应用发布自动化工具。尽管这些工具和CaaS产品之间有重叠之处,但是互补性远大于重合部分。
  1. 为容器基础镜像建立标准,考虑镜像大小、开发人员添加组件的灵活性和许可。
  2. 使用容器感知配置管理系统来管理容器镜像的生命周期,系统一旦感知到规则限定下的新版本镜像被推入仓库,则会立刻触发自动部署功能,来使用新镜像更新指定的容器。
  3. 将CaaS平台与应用程序自动化工具集成在一起,这样可以自动化整个应用程序工作流。


容器编排

因为容器编排工具管理着承载各类服务的容器集群。无论是 Kubernetes 社区还是第三方安全机构均针对 Kubernetes 中组件和资源的安全进行了相应改善和安全加固,包括计算资源安全、集群安全及相关组件安全等。这块需要重点考虑是隐私管理、授权管理、身份防控制、编排控制面、网络证书等都需要全面考虑。

其中,容器部署的关键是提供编排和调度能力。编排层与应用程序进行接口,使容器保持在所需的状态下运行。而容器调度系统按照编排层的要求将容器放在集群中最优的主机上。例如通过Apache Mesos提供调度,Marathon提供编排或使用单个工具Kubernetes或Docker Swarm提供编排和调度。客户在编排引擎之间或跨Kubernetes发行版之间进行决策时,需要重点考虑以下几个方面:
  1. 支持OS和容器运行时的深度和广度;
  2. 整体产品的运行时稳定性;
  3. 可扩展性;
  4. 对有状态应用程序的支持程度;
  5. 操作的简单性和供应商支持的质量;
  6. 对开源的支持和开发;
  7. 部署难易度及License费用;
  8. 支持混合多云。


容器技术与DevOps

容器和DevOps的关系就像是咖啡伴侣。容器能够快速发展,也得益于DevOps实践经验。在传统的开发环境,开发团队编写代码,QA团队测试软件应用程序,并将它们移交给运维团队进行日常管理。为了解决传统开发模式中的问题,很多企业都采用了“DevOps流程+微服务理论+使用容器和容器编排工具”。事实上,DevOps前身就是CI/CD,现在只不过是再加上一些发布、部署等标准和管理就构成了DevOps。

在云原生环境中,不仅软件开发和发布速度很快,而且平台本身也需要被当作一个产品来对待,因为它是动态的,并且在功能和规模方面不断发展。平台运营团队的目标是自动化、可伸缩和有弹性的标准化平台。平台运营团队的职责包括CaaS、PaaS产品的部署、操作、定制,标准化中间件的开发、操作,以及IaaS供应的自动化、部署、安全的启用等等。企业组织需要创建一个DevOps团队来运营容器,而不是一个个孤立的IT运营团队。

以容器安全为例,企业需要一个可集成至DevOps流程,又不会拖慢软件开发的方案。目前国内外有一些安全厂商已经在这方面做出卓越成绩,例如青藤容器安全解决方案,就可以提供对容器镜像扫描、入侵检测和合规基线实施情况等产品服务,化解容器所带来的安全挑战。这是一个以应用为中心、轻量级、保障容器静态资源及运行时安全的分布式解决方案,能够针对应用漏洞、不安全配置、入侵攻击、网络行为,并结合安全策略,提供覆盖容器全生命周期的、持续性安全防护。

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

Rainbond源码构建JAVA项目选取JDK


默认提供的JDK

Rainbond官方提供了多个版本的OpenJDK供用户使用。这些OpenJDK的安装包托管于好雨科技官方的OSS(对象存储)中。能够接入互联网的Rainbond平台,可以通过rbd-repo组件的代理获取这些资源,而不用人工干预。

用户通过WEB界面配置,或在源码根目录创建system.properties,设定java.runtime.version来指定OpenJDK版本。


WEB界面设置的值优先级高于system.properties中设定的值。
  • WEB界面指定:

  • system.properties指定方式:


bash

system.properties 目前Rainbond能识别的版本值为11,10,1.9,1.8,1.7,1.6

java.runtime.version=1.8


在不做出其他任何调整的情况下,在Rainbond执行源码构建时,会获取以下版本的OpenJDK资源:

| OpenJDK版本 | 资源地址 |
| ----------- | ------------------------------------------------------------ |
| 1.8(默认) | http://lang.goodrain.me/jdk/ce ... ar.gz |
| 1.6 | http://lang.goodrain.me/jdk/openjdk1.6.0_27.tar.gz |
| 1.7 | http://lang.goodrain.me/jdk/ce ... ar.gz |
| 1.9 | http://lang.goodrain.me/jdk/ce ... ar.gz |
| 10 | http://lang.goodrain.me/jdk/ce ... ar.gz |
| 11 | http://lang.goodrain.me/jdk/ce ... ar.gz |


特别提醒:如果maven编译过程中发生错误,请自行下载对应的JDK到自己的环境中,尝试本地构建,来确认是否由于OpenJDK版本问题导致了编译失败。

自定义JDK

多数用户希望能够自定义JDK,比如希望使用特定版本的OpenJDK,或者具备Oracle的授权使用OracleJDK。接下来就会讲解如何自定义。

Rainbond平台集成了 Jforg出品的 Artifactory 作为制品库(即rbd-repo组件)。在这里我们可以上传并存储自己的资源,包括jdk包。
  • 首先,访问 http://<管理节点IP>:8081 登录 Artifactory,默认凭证: admin/password



  • 创建自定义的本地制品仓库,并上传自定义的jdk包。





需要注意的是,Artifactory 上传文件的默认限制是不能超过 100MB。如果上传的文件超过了这个限制,可以在 Admin > General Configuration 页面中设置 File Upload Max Size 项为一个合适的值。
  • 在平台WEB界面指定自定义JDK地址,设置处为应用构建源。



像上述方式上传的jdk包,其地址为: http://<管理节点IP>:8081/artifactory/<自定义仓库名>/<文件名>


类似: http://192.168.1.1:8081/artifa ... ar.gz


至此,自定义JDK就完成了。

替换默认JDK

自定义JDK自由度更高,能适应更多的使用场景。但是其短板在于JDK获取地址在每一个新建的应用中都要设置一遍。有没有方式,可以替换Rainbond默认提供的JDK包呢?如果可以实现这个功能,那么每次构建都不用做其他设置,使用默认路径,即可安装公司内部指定版本的JDK,岂不是很方便?

默认JDK的地址,实际上指向了Rainbond官方的对象存储,这对于Artifactory而言,属于一种远程仓库(remote repository)。在Artifactory中,是没有办法通过上传文件,来替换远程仓库中的文件的。所以,我们要在本地建立另一个仓库,来替换Rainbond官方提供的远程仓库。

接下来是详细的步骤:
  • 用Rainbond官方提供的 rbd-java-buildpack 镜像作为基础,结合自需JDK包制作镜像(以默认的1.8版本为例)


bash

构建目录的文件结构如下:

.
├── Dockerfile
└── jdk-8u201-linux-x64.tar.gz #这里使用OracleJDK1.8 为例替换,用户根据自己需求自行更改


Dockerfile内容:

dockerfile
FROM rainbond/buildpack:java-v5.1.5
COPY jdk-8u201-linux-x64.tar.gz /pkg/lang/jdk/cedar-14/openjdk1.8.0_201.tar.gz


构建操作:

bash
docker build -t goodrain.me/buildpack:java-v5.1.5 .

  • 启动服务


在管理节点编辑指定配置文件:

bash

vi /opt/rainbond/conf/base.yaml

在末尾追加

- name: rbd-java-buildpack
endpoints:
- name: BUILDPACK_ENDPOINTS
protocol:
port: 2017
health:
name: rbd-java-buildpack
model: http
address: 127.0.0.1:2017/lang/
max_errors_num: 3
time_interval: 30
after:
- docker
type: simple
pre_start: docker rm rbd-java-buildpack
start: >-
docker run --name rbd-java-buildpack
--network host
-i goodrain.me/buildpack:java-v5.1.5
stop: docker stop rbd-java-buildpack
restart_policy: always


启动服务:

bash
node service update

  • 修改Artifactory中的远程仓库pkg_lang 的远程地址为 http://&lt;管理节点IP>:2017/lang





如果已经用Rainbond官方远程仓库获取过JDK包,那么要在 pkg_lang-cache中清除已缓存的记录。
至此,就已经将默认的OpenJDK1.8版本,替换成为指定的 OracleJDK1.8版本了。

总结

基于这篇文章,用户已经可以灵活掌握配置使用各种JDK的方式了。

在这篇文章之后,我们会继续发布如何灵活配置Maven环境的文章,敬请期待。
继续阅读 »

默认提供的JDK

Rainbond官方提供了多个版本的OpenJDK供用户使用。这些OpenJDK的安装包托管于好雨科技官方的OSS(对象存储)中。能够接入互联网的Rainbond平台,可以通过rbd-repo组件的代理获取这些资源,而不用人工干预。

用户通过WEB界面配置,或在源码根目录创建system.properties,设定java.runtime.version来指定OpenJDK版本。


WEB界面设置的值优先级高于system.properties中设定的值。
  • WEB界面指定:

  • system.properties指定方式:


bash

system.properties 目前Rainbond能识别的版本值为11,10,1.9,1.8,1.7,1.6

java.runtime.version=1.8


在不做出其他任何调整的情况下,在Rainbond执行源码构建时,会获取以下版本的OpenJDK资源:

| OpenJDK版本 | 资源地址 |
| ----------- | ------------------------------------------------------------ |
| 1.8(默认) | http://lang.goodrain.me/jdk/ce ... ar.gz |
| 1.6 | http://lang.goodrain.me/jdk/openjdk1.6.0_27.tar.gz |
| 1.7 | http://lang.goodrain.me/jdk/ce ... ar.gz |
| 1.9 | http://lang.goodrain.me/jdk/ce ... ar.gz |
| 10 | http://lang.goodrain.me/jdk/ce ... ar.gz |
| 11 | http://lang.goodrain.me/jdk/ce ... ar.gz |


特别提醒:如果maven编译过程中发生错误,请自行下载对应的JDK到自己的环境中,尝试本地构建,来确认是否由于OpenJDK版本问题导致了编译失败。

自定义JDK

多数用户希望能够自定义JDK,比如希望使用特定版本的OpenJDK,或者具备Oracle的授权使用OracleJDK。接下来就会讲解如何自定义。

Rainbond平台集成了 Jforg出品的 Artifactory 作为制品库(即rbd-repo组件)。在这里我们可以上传并存储自己的资源,包括jdk包。
  • 首先,访问 http://&lt;管理节点IP>:8081 登录 Artifactory,默认凭证: admin/password



  • 创建自定义的本地制品仓库,并上传自定义的jdk包。





需要注意的是,Artifactory 上传文件的默认限制是不能超过 100MB。如果上传的文件超过了这个限制,可以在 Admin > General Configuration 页面中设置 File Upload Max Size 项为一个合适的值。
  • 在平台WEB界面指定自定义JDK地址,设置处为应用构建源。



像上述方式上传的jdk包,其地址为: http://<管理节点IP>:8081/artifactory/<自定义仓库名>/<文件名>


类似: http://192.168.1.1:8081/artifa ... ar.gz


至此,自定义JDK就完成了。

替换默认JDK

自定义JDK自由度更高,能适应更多的使用场景。但是其短板在于JDK获取地址在每一个新建的应用中都要设置一遍。有没有方式,可以替换Rainbond默认提供的JDK包呢?如果可以实现这个功能,那么每次构建都不用做其他设置,使用默认路径,即可安装公司内部指定版本的JDK,岂不是很方便?

默认JDK的地址,实际上指向了Rainbond官方的对象存储,这对于Artifactory而言,属于一种远程仓库(remote repository)。在Artifactory中,是没有办法通过上传文件,来替换远程仓库中的文件的。所以,我们要在本地建立另一个仓库,来替换Rainbond官方提供的远程仓库。

接下来是详细的步骤:
  • 用Rainbond官方提供的 rbd-java-buildpack 镜像作为基础,结合自需JDK包制作镜像(以默认的1.8版本为例)


bash

构建目录的文件结构如下:

.
├── Dockerfile
└── jdk-8u201-linux-x64.tar.gz #这里使用OracleJDK1.8 为例替换,用户根据自己需求自行更改


Dockerfile内容:

dockerfile
FROM rainbond/buildpack:java-v5.1.5
COPY jdk-8u201-linux-x64.tar.gz /pkg/lang/jdk/cedar-14/openjdk1.8.0_201.tar.gz


构建操作:

bash
docker build -t goodrain.me/buildpack:java-v5.1.5 .

  • 启动服务


在管理节点编辑指定配置文件:

bash

vi /opt/rainbond/conf/base.yaml

在末尾追加

- name: rbd-java-buildpack
endpoints:
- name: BUILDPACK_ENDPOINTS
protocol:
port: 2017
health:
name: rbd-java-buildpack
model: http
address: 127.0.0.1:2017/lang/
max_errors_num: 3
time_interval: 30
after:
- docker
type: simple
pre_start: docker rm rbd-java-buildpack
start: >-
docker run --name rbd-java-buildpack
--network host
-i goodrain.me/buildpack:java-v5.1.5
stop: docker stop rbd-java-buildpack
restart_policy: always


启动服务:

bash
node service update

  • 修改Artifactory中的远程仓库pkg_lang 的远程地址为 http://&lt;管理节点IP>:2017/lang





如果已经用Rainbond官方远程仓库获取过JDK包,那么要在 pkg_lang-cache中清除已缓存的记录。
至此,就已经将默认的OpenJDK1.8版本,替换成为指定的 OracleJDK1.8版本了。

总结

基于这篇文章,用户已经可以灵活掌握配置使用各种JDK的方式了。

在这篇文章之后,我们会继续发布如何灵活配置Maven环境的文章,敬请期待。 收起阅读 »

Jenkins源码阅读指北,一文看懂Jenkins用到的java技术点


Jenkins是一个基于Java开发的,用于持续集成的工具。Jenkins的前身是Sun 公司的Hudson,第一个版本于2005年发布,在2010年11月期间,因为Oracle对Sun的收购带来了Hudson的所有权问题。2011年1月29日,该建议得到社区投票的批准,创建了Jenkins项目。

本文在学习Jenkins源码的同时,也会分析Jenkins对于java技术的运用,并对相关技术进行简单介绍。Jenkins使用的是Stapler框架,国内使用较少,这里不对该框架进行分析,而是根据场景直接分析源码。

1 doCheckJobName方法

我们在Jenkins创建一个Item的时候,当在Enteran item name输入一个名称,其实会请求checkJobName这个api,触发的方法在doCheckJobName,方法定义如下:

public FormValidation doCheckJobName(@QueryParameter String value) {
// this method can be used to check if a file existsanywhere in the file system,
// so it should be protected.
getOwner().checkPermission(Item.CREATE);

if (Util.fixEmpty(value) == null) {
    return FormValidation.ok();
}

try {
    Jenkins.checkGoodName(value);
    value = value.trim(); // why trim *after* checkGoodName? not sure, butItemGroupMixIn.createTopLevelItem does the same
    Jenkins.get().getProjectNamingStrategy().checkName(value);
} catch (Failure e){
    return FormValidation.error(e.getMessage());
}

if (getOwner().getItemGroup().getItem(value)!= null) {
    return FormValidation.error(Messages.Hudson_JobAlreadyExists(value));
}

// looks good
return FormValidation.ok();
}

比较重要的几行代码

getOwner().checkPermission(Item.CREATE);是对当前用户的权限进程检测

Jenkins.checkGoodName(value);对名称的合法性进行检测

if (getOwner().getItemGroup().getItem(value)!= null) {
return FormValidation.error(Messages.Hudson_JobAlreadyExists(value));
}

查看是否名称已经存在。

我们以getOwner().getItemGroup().getItem(value)为例,着重讲解一下。

2 getOwner方法和ViewGroup

getOwner()这个方法会返回一个ViewGroup。

ViewGroup接口:

public interface ViewGroup extendsSaveable, ModelObject,AccessControlled

在注释中描述为Containerof Views.

AccessControlled这个接口有四个方法:

@Nonnull ACL getACL();
default void checkPermission(@Nonnull Permission permission) throws AccessDeniedException {
getACL().checkPermission(permission);
}

default boolean hasPermission(@Nonnull Permission permission) {
return getACL().hasPermission(permission);
}

default boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission permission) {
if (a == ACL.SYSTEM) {
    return true;
}
return getACL().hasPermission(a, permission);
}

需要注意的是这个接口有三个方法带有default关键字,并且还带有具体的实现。自从jdk1.8以后接口中可以定义方法的具体实现,这个特性解决了interface扩展必须修改实现类的问题。

对于getOwner方法返回的实际上是个Hudson的实例,这个方法位于View抽象类中,因为AllView继承了View。Hudson继承Jenkins类,Jenkins类继承了AbstractCIBase抽象类,而AbstractCIBase抽象类实现了ViewGroup接口。

前面说到ViewGroup在注释中描述为View的容器。所以这个接口提供了Collection<View>getViews();这个获取View集合的方法。在Jenkins类中实现了这个方法:

@Exported
public Collection<View>getViews() {
return viewGroupMixIn.getViews();
}

这个方法的实现是返回ViewGroupMixIn类型的对象viewGroupMixIn执行getViews方法的返回结果。

下面我们从ViewGroupMixIn类型开始分析。

3 ViewGroupMixIn类型

首先我们看一下viewGroupMixIn是什么:
privatetransient final ViewGroupMixIn viewGroupMixIn = new ViewGroupMixIn(this) {
protected List<View> views() { return views; }
protected String primaryView() { return primaryView; }
protected void primaryView(String name) { primaryView=name; }
};


可以发现ViewGroupMixIn是一个抽象类,而viewGroupMixIn变量以内部类的形式实现了该类。我们再来看ViewGroupMixIn中getViews方法的实现

public Collection<View> getViews() {
List<View>orig = views();
List<View>copy = new ArrayList<>(orig.size());
for (View v : orig) {
    if (v.hasPermission(View.READ))
        copy.add(v);
}
copy.sort(View.SORTER);
return copy;
}

逻辑大概是:首先会调用views方法,这个方法在Jenkins类中的viewGroupMixIn内部类的实现中已经定义了。

protected List<View>views() { return views; }
views方法会返回Jenkins中的views变量,那么views变量是什么呢:

private final CopyOnWriteArrayList<View>views = new CopyOnWriteArrayList<>();
原来views是一个CopyOnWriteArrayList<View>。

4 java.util.concurrent中的CopyOnWriteArrayList

CopyOnWriteArrayList是java.util.concurrent包中的一个并发集合,可以保证线程安全,在读多写少的场景下拥有更好的性能。

假设我们要设计一个线程安全的List,首相想到的是:为了保证线程安全对于List的操作(读,写)时候需要加锁。但是如果无论读写都加锁势必对性能造成很大的浪费。毕竟读与读之间不会对数据进行修改,所以读读之间可以不考虑锁。这样只有在读写之间,写写之间需要同步等待。显然这种方式减少了读读情况下不必要的锁。比较与所有操作都需要锁已经有了性能的提升。

而CopyOnWriteArrayList使用了cow技术。详细来说就是当这个List在修改时候会复制一份新的副本来修改,而修改后对原数据进行替换。

我们来看看关键的源码:

public E get(int index) {
return get(getArray(), index);
}

可以看到CopyOnWriteArrayList在读取时候不会有锁操作。写操作就复杂一些:

publicboolean add(E e) {
final ReentrantLocklock = this.lock;
lock.lock();
try {
    Object[] elements = getArray();
    int len = elements.length;
    Object[] newElements = Arrays.copyOf(elements, len + 1);
    newElements[len] = e;
    setArray(newElements);
    return true;
} finally {
    lock.unlock();
}
}

首先需要使用ReentrantLock加锁,
Object[]newElements = Arrays.copyOf(elements, len + 1);
复制数组为一个长度加1的数组。
setArray(newElements);将数组赋值给原引用。

而我们所操作的数据private transient volatile Object[] array;array定义是一个volatile变量。

volatile关键字相当于声明这个被修饰的变量是一个需要频繁更新使用的变量,需要及时的修改。因为在没有同步的情况下,编译器处理器等可能对操作的执行顺序进行一些调整。

java内存模型允许编译器对操作顺序重新进行排序,并且将值暂时缓存到寄存器中。对于cpu操作顺序进行了重新排序。

当声明为volatile时候,这个变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取该变量时候总是最新的。

所以对于array的写操作,即使有多个线程在读取,也不会出现可见性的问题。

好了,刚才我们以及了解到ViewGroup是View的container,并且了解到Jenkins类对于View的管理方式和getViews的实现方式。

我们继续来看:

if(getOwner().getItemGroup().getItem(value) != null) {
这里我们已经知道getOwner方法返回的ViewGroup是Hudson对象,在Hudson类中没有实现getItemGroup方法,那么对于Hudson对象调用getItemGroup方法,实际是执行它继承的类中的方法或者实现的接口中默认的方法。

首先在ViewGroup接口中

default ItemGroup<?extends TopLevelItem> getItemGroup() {
return Jenkins.get();
}

但是在Jenkins类中也有定义

public Jenkins getItemGroup() {
 return this;
}

也就是说getItemGroup方法返回的是对象本身。同时AbstractCIBase也实现了ItemGroup。最后getItem方法取出一个Item。Jenkins对于Item的解释是“Basic configuration unit in Hudson.”。

在Jenkins类中对于getItem的实现:

@Override
public TopLevelItemgetItem(String name) throws AccessDeniedException {
if (name==null)   return null;
TopLevelItem item = items.get(name);
if (item==null)
    return null;
if (!item.hasPermission(Item.READ)) {
    if (item.hasPermission(Item.DISCOVER)) {
        throw new AccessDeniedException("Please login to access job " + name);
    }
    return null;
}
return item;
}

我们发现items声明是一个Map<String,TopLevelItem>

transient final Map<String,TopLevelItem> items = new CopyOnWriteMap.Tree<>(CaseInsensitiveComparator.INSTANCE);

CopyOnWriteMap.Tree是一个在hudson.util包中CopyOnWriteMap类中的内部类。

publicstatic final class Tree<K,V> extends CopyOnWriteMap<K,V>{
……
}

Tree继承了CopyOnWriteMap类,所以我们首先来看一下CopyOnWriteMap这个类。
publicabstract class CopyOnWriteMap<K,V> implements Map<K,V> {
……
}

**5 CopyOnWriteMap**

CopyOnWriteMap是一个实现了Map接口的抽象类。

CopyOnWriteMap首先声明了两个Map
protectedvolatile Map<K,V> core;
privatevolatile Map<K,V> view;


其中view是不可修改的map视图,这个会在后面介绍。

这两个变量使用了volatile关键字来修饰,volatile的作用以及可见性相关的内容在前边的CopyOnWriteArrayList已经介绍过了,这里不再介绍了。

CopyOnWriteMap有两个构造方法

protected CopyOnWriteMap(Map<K,V> core) {
update(core);
}

protected CopyOnWriteMap() {
update(Collections.emptyMap());
}

我们在前边看到实例化Tree的时候使用的是Tree这个构造方法

public Tree(Comparator<K> comparator) {
super(new TreeMap<>(comparator));
this.comparator =comparator;
}

其中super(new TreeMap<>(comparator));会调用protected CopyOnWriteMap(Map<K,V> core)并且传入一个TreeMap的实例,这里简单介绍一下TreeMap。

TreeMap是Map接口的实现类,它继承自AbstractMap抽象类,并且实现了NavigableMap接口。NavigableMap接口继承了SortedMap接口。SortedMap接口是有序Map的实现接口。

NavigableMap接口则是可导航Map接口(如小于指定值的最大值),比如NavigableMap接口中的方法:
K lowerKey(K key);
是Returns the greatest key strictly less than the givenkey, or if there is no such key.
K higherKey(K key);
Returns theleast key strictly greater than the given key, or null if there is no such key.

由此可知TreeMap是个有序并且是可导航的map,而TreeMap是基于红黑树实现的。对于红黑树,每次修改(以及增删)都可能破坏红黑树。所以对于put和remove需要更复杂的逻辑。TreeMap的Entry拥有6个属性:

K key;
V value;
Entry<K,V> left; 左节点
Entry<K,V> right; 右节点
Entry<K,V> parent; 父亲节点
boolean color = BLACK;颜色设置

TreeMap的put方法中
do {
        parent =t;
       cmp = cpr.compare(key, t.key);
        if (cmp < 0)
            t =t.left;
        else if (cmp > 0)
            t = t.right;
        else
            return t.setValue(value);
    } while (t != null);


这里的do-while会从树根开始迭代寻找key的所在位置。
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);


在插入新Entry以后,通过调用fixAfterInsertion方法来修正红黑树,因为此时红黑树可能已经遭到破坏:
private void fixAfterInsertion(Entry<K,V> x) {


   //将节点设为红色
x.color = RED;

   //循环条件x不为null,非根节点,红色节点
while (x != null && x != root && x.parent.color == RED) {

   //如果x节点的父亲节点是x的爷爷节点的左子节点
    if (parentOf(x) == leftOf(parentOf(parentOf(x)))){

   //获取x爷爷节点的右孩子(x父亲节点的兄节点(x的大爷节点))
        Entry<K,V> y = rightOf(parentOf(parentOf(x)));

   //如果x大爷节点的颜色是红色
        if (colorOf(y) == RED) {

   //设置x父亲节点为黑色
            setColor(parentOf(x), BLACK);

   //设置x的大爷节点为黑色
            setColor(y,BLACK);

   //设置x的爷爷节点为红色(红黑树红色节点的孩子必须是黑色)
            setColor(parentOf(parentOf(x)), RED);

   //x设置为x的爷爷节点
            x = parentOf(parentOf(x));

   //如果x的大爷节点不为红色(注意如果x的大爷为null,那么colorOf方法也会返回黑色)
        } else {

   //如果x是父亲节点的右子节点
            if (x == rightOf(parentOf(x))) {

   //x设置为x的父亲节点
                x= parentOf(x);

   //以x为轴左旋操作
                rotateLeft(x);
            }

   //设置x节点的父亲节点为黑色
            setColor(parentOf(x), BLACK);

   //设置x节点的爷爷节点为红色
            setColor(parentOf(parentOf(x)), RED);

   //以x的爷爷节点为轴右旋操作
            rotateRight(parentOf(parentOf(x)));
        }

//如果x的父亲节点是x爷爷节点的右子节点
    } else {

   //y为x的爷爷节点的左孩子,也就是x的叔叔节点
        Entry<K,V> y = leftOf(parentOf(parentOf(x)));

   //如果x叔叔节点为红色
        if (colorOf(y) == RED) {

   //设置x的父亲节点为黑色
            setColor(parentOf(x), BLACK);

   //设置x的叔叔节点为黑色
            setColor(y, BLACK);

   //设置x的爷爷节点为红色
            setColor(parentOf(parentOf(x)), RED);

   //设置x为x的爷爷节点
            x = parentOf(parentOf(x));

   //x的叔叔节点为黑色(null)
        } else {

   //如果x是x父亲节点的左子节点
            if (x == leftOf(parentOf(x))) {

   //x设置为父亲节点
                x= parentOf(x);

   //以x为轴右旋
                rotateRight(x);
            }

   //设置x的父亲为黑色
            setColor(parentOf(x), BLACK);

   //设置x的父亲为红色
            setColor(parentOf(parentOf(x)), RED);

   //以x的爷爷节点为轴左旋
            rotateLeft(parentOf(parentOf(x)));
        }
    }
}

   //设置根节点为黑色
root.color = BLACK;
}

putAll方法中使用了一个buildFromSorted方法,这个方法的作用是将一个SortedMap构造成一个TreeMap。这个方法是一个递归的方法。这个方法实现的算法的逻辑是:首先以一组数据的中间元素作为根(此处中间的坐标取整int mid = (lo + hi) >>> 1;).
递归的构建左子树:





Entry<K,V> left  = null;
if (lo < mid)
left =buildFromSorted(level+1, lo, mid- 1, redLevel,
                      it, str,defaultVal);


然后再构建右子树。

if (mid <hi) {
Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,
                                   it, str, defaultVal);
middle.right = right;
right.parent = middle;
}

简单的了解了红黑树以及TreeMap以后,我们来继续介绍CopyOnWriteMap以及CopyOnWriteMap.Tree。

既然是cow机制,基本与前面介绍的CopyOnWriteArrayList的形式相似。get方法(读操作没有锁操作),也没有使用视图。

public V get(Objectkey) {
return core.get(key);
}

下面着重说一下put方法(写操作)

publicsynchronized V put(K key, V value) {
Map<K,V> m = copy();
V r = m.put(key,value);
update(m);

return r;
}

Jenkins中这个put实现没有像java.util.concurrent包中的CopyOnWriteArrayList那样使用ReentrantLock,而是使用了synchronized关键字,在1.6以后对synchronized的性能有了大幅度的优化,与ReentrantLock的性能差距已经非常小了,在大部分场景这种差距可以忽略。

在synchronized修饰的方法内首先调用了copy方法,这个方法在Tree中实现:

protected Map<K,V> copy() {
TreeMap<K,V>m = new TreeMap<>(comparator);
m.putAll(core);
return m;
}

实现的逻辑非常的简单,调用了TreeMap的putAll方法将core赋予新的TreeMap m。然后在put方法将新KV put 到m中。最后执行update方法:

protectedvoid update(Map<K,V> m) {
core = m;
view = Collections.unmodifiableMap(core);
}

可以看到这个方法将新的Map赋予给了core。
view =Collections.unmodifiableMap(core);则创建了一个core的不可修改的副本。不可修改主要体现在比如UnmodifiableMap的put方法:

public V put(K key, V value) {
throw new UnsupportedOperationException();
}

在上述介绍完Jenkins中hudson.util包实现Cowmap(CopyOnWriteMap)以后,我们基本介绍完了checkJobName方法的逻辑以及关联到的java技术点。
继续阅读 »

Jenkins是一个基于Java开发的,用于持续集成的工具。Jenkins的前身是Sun 公司的Hudson,第一个版本于2005年发布,在2010年11月期间,因为Oracle对Sun的收购带来了Hudson的所有权问题。2011年1月29日,该建议得到社区投票的批准,创建了Jenkins项目。

本文在学习Jenkins源码的同时,也会分析Jenkins对于java技术的运用,并对相关技术进行简单介绍。Jenkins使用的是Stapler框架,国内使用较少,这里不对该框架进行分析,而是根据场景直接分析源码。

1 doCheckJobName方法

我们在Jenkins创建一个Item的时候,当在Enteran item name输入一个名称,其实会请求checkJobName这个api,触发的方法在doCheckJobName,方法定义如下:

public FormValidation doCheckJobName(@QueryParameter String value) {
// this method can be used to check if a file existsanywhere in the file system,
// so it should be protected.
getOwner().checkPermission(Item.CREATE);

if (Util.fixEmpty(value) == null) {
    return FormValidation.ok();
}

try {
    Jenkins.checkGoodName(value);
    value = value.trim(); // why trim *after* checkGoodName? not sure, butItemGroupMixIn.createTopLevelItem does the same
    Jenkins.get().getProjectNamingStrategy().checkName(value);
} catch (Failure e){
    return FormValidation.error(e.getMessage());
}

if (getOwner().getItemGroup().getItem(value)!= null) {
    return FormValidation.error(Messages.Hudson_JobAlreadyExists(value));
}

// looks good
return FormValidation.ok();
}

比较重要的几行代码

getOwner().checkPermission(Item.CREATE);是对当前用户的权限进程检测

Jenkins.checkGoodName(value);对名称的合法性进行检测

if (getOwner().getItemGroup().getItem(value)!= null) {
return FormValidation.error(Messages.Hudson_JobAlreadyExists(value));
}

查看是否名称已经存在。

我们以getOwner().getItemGroup().getItem(value)为例,着重讲解一下。

2 getOwner方法和ViewGroup

getOwner()这个方法会返回一个ViewGroup。

ViewGroup接口:

public interface ViewGroup extendsSaveable, ModelObject,AccessControlled

在注释中描述为Containerof Views.

AccessControlled这个接口有四个方法:

@Nonnull ACL getACL();
default void checkPermission(@Nonnull Permission permission) throws AccessDeniedException {
getACL().checkPermission(permission);
}

default boolean hasPermission(@Nonnull Permission permission) {
return getACL().hasPermission(permission);
}

default boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission permission) {
if (a == ACL.SYSTEM) {
    return true;
}
return getACL().hasPermission(a, permission);
}

需要注意的是这个接口有三个方法带有default关键字,并且还带有具体的实现。自从jdk1.8以后接口中可以定义方法的具体实现,这个特性解决了interface扩展必须修改实现类的问题。

对于getOwner方法返回的实际上是个Hudson的实例,这个方法位于View抽象类中,因为AllView继承了View。Hudson继承Jenkins类,Jenkins类继承了AbstractCIBase抽象类,而AbstractCIBase抽象类实现了ViewGroup接口。

前面说到ViewGroup在注释中描述为View的容器。所以这个接口提供了Collection<View>getViews();这个获取View集合的方法。在Jenkins类中实现了这个方法:

@Exported
public Collection<View>getViews() {
return viewGroupMixIn.getViews();
}

这个方法的实现是返回ViewGroupMixIn类型的对象viewGroupMixIn执行getViews方法的返回结果。

下面我们从ViewGroupMixIn类型开始分析。

3 ViewGroupMixIn类型

首先我们看一下viewGroupMixIn是什么:
privatetransient final ViewGroupMixIn viewGroupMixIn = new ViewGroupMixIn(this) {
protected List<View> views() { return views; }
protected String primaryView() { return primaryView; }
protected void primaryView(String name) { primaryView=name; }
};


可以发现ViewGroupMixIn是一个抽象类,而viewGroupMixIn变量以内部类的形式实现了该类。我们再来看ViewGroupMixIn中getViews方法的实现

public Collection<View> getViews() {
List<View>orig = views();
List<View>copy = new ArrayList<>(orig.size());
for (View v : orig) {
    if (v.hasPermission(View.READ))
        copy.add(v);
}
copy.sort(View.SORTER);
return copy;
}

逻辑大概是:首先会调用views方法,这个方法在Jenkins类中的viewGroupMixIn内部类的实现中已经定义了。

protected List<View>views() { return views; }
views方法会返回Jenkins中的views变量,那么views变量是什么呢:

private final CopyOnWriteArrayList<View>views = new CopyOnWriteArrayList<>();
原来views是一个CopyOnWriteArrayList<View>。

4 java.util.concurrent中的CopyOnWriteArrayList

CopyOnWriteArrayList是java.util.concurrent包中的一个并发集合,可以保证线程安全,在读多写少的场景下拥有更好的性能。

假设我们要设计一个线程安全的List,首相想到的是:为了保证线程安全对于List的操作(读,写)时候需要加锁。但是如果无论读写都加锁势必对性能造成很大的浪费。毕竟读与读之间不会对数据进行修改,所以读读之间可以不考虑锁。这样只有在读写之间,写写之间需要同步等待。显然这种方式减少了读读情况下不必要的锁。比较与所有操作都需要锁已经有了性能的提升。

而CopyOnWriteArrayList使用了cow技术。详细来说就是当这个List在修改时候会复制一份新的副本来修改,而修改后对原数据进行替换。

我们来看看关键的源码:

public E get(int index) {
return get(getArray(), index);
}

可以看到CopyOnWriteArrayList在读取时候不会有锁操作。写操作就复杂一些:

publicboolean add(E e) {
final ReentrantLocklock = this.lock;
lock.lock();
try {
    Object[] elements = getArray();
    int len = elements.length;
    Object[] newElements = Arrays.copyOf(elements, len + 1);
    newElements[len] = e;
    setArray(newElements);
    return true;
} finally {
    lock.unlock();
}
}

首先需要使用ReentrantLock加锁,
Object[]newElements = Arrays.copyOf(elements, len + 1);
复制数组为一个长度加1的数组。
setArray(newElements);将数组赋值给原引用。

而我们所操作的数据private transient volatile Object[] array;array定义是一个volatile变量。

volatile关键字相当于声明这个被修饰的变量是一个需要频繁更新使用的变量,需要及时的修改。因为在没有同步的情况下,编译器处理器等可能对操作的执行顺序进行一些调整。

java内存模型允许编译器对操作顺序重新进行排序,并且将值暂时缓存到寄存器中。对于cpu操作顺序进行了重新排序。

当声明为volatile时候,这个变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取该变量时候总是最新的。

所以对于array的写操作,即使有多个线程在读取,也不会出现可见性的问题。

好了,刚才我们以及了解到ViewGroup是View的container,并且了解到Jenkins类对于View的管理方式和getViews的实现方式。

我们继续来看:

if(getOwner().getItemGroup().getItem(value) != null) {
这里我们已经知道getOwner方法返回的ViewGroup是Hudson对象,在Hudson类中没有实现getItemGroup方法,那么对于Hudson对象调用getItemGroup方法,实际是执行它继承的类中的方法或者实现的接口中默认的方法。

首先在ViewGroup接口中

default ItemGroup<?extends TopLevelItem> getItemGroup() {
return Jenkins.get();
}

但是在Jenkins类中也有定义

public Jenkins getItemGroup() {
 return this;
}

也就是说getItemGroup方法返回的是对象本身。同时AbstractCIBase也实现了ItemGroup。最后getItem方法取出一个Item。Jenkins对于Item的解释是“Basic configuration unit in Hudson.”。

在Jenkins类中对于getItem的实现:

@Override
public TopLevelItemgetItem(String name) throws AccessDeniedException {
if (name==null)   return null;
TopLevelItem item = items.get(name);
if (item==null)
    return null;
if (!item.hasPermission(Item.READ)) {
    if (item.hasPermission(Item.DISCOVER)) {
        throw new AccessDeniedException("Please login to access job " + name);
    }
    return null;
}
return item;
}

我们发现items声明是一个Map<String,TopLevelItem>

transient final Map<String,TopLevelItem> items = new CopyOnWriteMap.Tree<>(CaseInsensitiveComparator.INSTANCE);

CopyOnWriteMap.Tree是一个在hudson.util包中CopyOnWriteMap类中的内部类。

publicstatic final class Tree<K,V> extends CopyOnWriteMap<K,V>{
……
}

Tree继承了CopyOnWriteMap类,所以我们首先来看一下CopyOnWriteMap这个类。
publicabstract class CopyOnWriteMap<K,V> implements Map<K,V> {
……
}

**5 CopyOnWriteMap**

CopyOnWriteMap是一个实现了Map接口的抽象类。

CopyOnWriteMap首先声明了两个Map
protectedvolatile Map<K,V> core;
privatevolatile Map<K,V> view;


其中view是不可修改的map视图,这个会在后面介绍。

这两个变量使用了volatile关键字来修饰,volatile的作用以及可见性相关的内容在前边的CopyOnWriteArrayList已经介绍过了,这里不再介绍了。

CopyOnWriteMap有两个构造方法

protected CopyOnWriteMap(Map<K,V> core) {
update(core);
}

protected CopyOnWriteMap() {
update(Collections.emptyMap());
}

我们在前边看到实例化Tree的时候使用的是Tree这个构造方法

public Tree(Comparator<K> comparator) {
super(new TreeMap<>(comparator));
this.comparator =comparator;
}

其中super(new TreeMap<>(comparator));会调用protected CopyOnWriteMap(Map<K,V> core)并且传入一个TreeMap的实例,这里简单介绍一下TreeMap。

TreeMap是Map接口的实现类,它继承自AbstractMap抽象类,并且实现了NavigableMap接口。NavigableMap接口继承了SortedMap接口。SortedMap接口是有序Map的实现接口。

NavigableMap接口则是可导航Map接口(如小于指定值的最大值),比如NavigableMap接口中的方法:
K lowerKey(K key);
是Returns the greatest key strictly less than the givenkey, or if there is no such key.
K higherKey(K key);
Returns theleast key strictly greater than the given key, or null if there is no such key.

由此可知TreeMap是个有序并且是可导航的map,而TreeMap是基于红黑树实现的。对于红黑树,每次修改(以及增删)都可能破坏红黑树。所以对于put和remove需要更复杂的逻辑。TreeMap的Entry拥有6个属性:

K key;
V value;
Entry<K,V> left; 左节点
Entry<K,V> right; 右节点
Entry<K,V> parent; 父亲节点
boolean color = BLACK;颜色设置

TreeMap的put方法中
do {
        parent =t;
       cmp = cpr.compare(key, t.key);
        if (cmp < 0)
            t =t.left;
        else if (cmp > 0)
            t = t.right;
        else
            return t.setValue(value);
    } while (t != null);


这里的do-while会从树根开始迭代寻找key的所在位置。
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);


在插入新Entry以后,通过调用fixAfterInsertion方法来修正红黑树,因为此时红黑树可能已经遭到破坏:
private void fixAfterInsertion(Entry<K,V> x) {


   //将节点设为红色
x.color = RED;

   //循环条件x不为null,非根节点,红色节点
while (x != null && x != root && x.parent.color == RED) {

   //如果x节点的父亲节点是x的爷爷节点的左子节点
    if (parentOf(x) == leftOf(parentOf(parentOf(x)))){

   //获取x爷爷节点的右孩子(x父亲节点的兄节点(x的大爷节点))
        Entry<K,V> y = rightOf(parentOf(parentOf(x)));

   //如果x大爷节点的颜色是红色
        if (colorOf(y) == RED) {

   //设置x父亲节点为黑色
            setColor(parentOf(x), BLACK);

   //设置x的大爷节点为黑色
            setColor(y,BLACK);

   //设置x的爷爷节点为红色(红黑树红色节点的孩子必须是黑色)
            setColor(parentOf(parentOf(x)), RED);

   //x设置为x的爷爷节点
            x = parentOf(parentOf(x));

   //如果x的大爷节点不为红色(注意如果x的大爷为null,那么colorOf方法也会返回黑色)
        } else {

   //如果x是父亲节点的右子节点
            if (x == rightOf(parentOf(x))) {

   //x设置为x的父亲节点
                x= parentOf(x);

   //以x为轴左旋操作
                rotateLeft(x);
            }

   //设置x节点的父亲节点为黑色
            setColor(parentOf(x), BLACK);

   //设置x节点的爷爷节点为红色
            setColor(parentOf(parentOf(x)), RED);

   //以x的爷爷节点为轴右旋操作
            rotateRight(parentOf(parentOf(x)));
        }

//如果x的父亲节点是x爷爷节点的右子节点
    } else {

   //y为x的爷爷节点的左孩子,也就是x的叔叔节点
        Entry<K,V> y = leftOf(parentOf(parentOf(x)));

   //如果x叔叔节点为红色
        if (colorOf(y) == RED) {

   //设置x的父亲节点为黑色
            setColor(parentOf(x), BLACK);

   //设置x的叔叔节点为黑色
            setColor(y, BLACK);

   //设置x的爷爷节点为红色
            setColor(parentOf(parentOf(x)), RED);

   //设置x为x的爷爷节点
            x = parentOf(parentOf(x));

   //x的叔叔节点为黑色(null)
        } else {

   //如果x是x父亲节点的左子节点
            if (x == leftOf(parentOf(x))) {

   //x设置为父亲节点
                x= parentOf(x);

   //以x为轴右旋
                rotateRight(x);
            }

   //设置x的父亲为黑色
            setColor(parentOf(x), BLACK);

   //设置x的父亲为红色
            setColor(parentOf(parentOf(x)), RED);

   //以x的爷爷节点为轴左旋
            rotateLeft(parentOf(parentOf(x)));
        }
    }
}

   //设置根节点为黑色
root.color = BLACK;
}

putAll方法中使用了一个buildFromSorted方法,这个方法的作用是将一个SortedMap构造成一个TreeMap。这个方法是一个递归的方法。这个方法实现的算法的逻辑是:首先以一组数据的中间元素作为根(此处中间的坐标取整int mid = (lo + hi) >>> 1;).
递归的构建左子树:





Entry<K,V> left  = null;
if (lo < mid)
left =buildFromSorted(level+1, lo, mid- 1, redLevel,
                      it, str,defaultVal);


然后再构建右子树。

if (mid <hi) {
Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,
                                   it, str, defaultVal);
middle.right = right;
right.parent = middle;
}

简单的了解了红黑树以及TreeMap以后,我们来继续介绍CopyOnWriteMap以及CopyOnWriteMap.Tree。

既然是cow机制,基本与前面介绍的CopyOnWriteArrayList的形式相似。get方法(读操作没有锁操作),也没有使用视图。

public V get(Objectkey) {
return core.get(key);
}

下面着重说一下put方法(写操作)

publicsynchronized V put(K key, V value) {
Map<K,V> m = copy();
V r = m.put(key,value);
update(m);

return r;
}

Jenkins中这个put实现没有像java.util.concurrent包中的CopyOnWriteArrayList那样使用ReentrantLock,而是使用了synchronized关键字,在1.6以后对synchronized的性能有了大幅度的优化,与ReentrantLock的性能差距已经非常小了,在大部分场景这种差距可以忽略。

在synchronized修饰的方法内首先调用了copy方法,这个方法在Tree中实现:

protected Map<K,V> copy() {
TreeMap<K,V>m = new TreeMap<>(comparator);
m.putAll(core);
return m;
}

实现的逻辑非常的简单,调用了TreeMap的putAll方法将core赋予新的TreeMap m。然后在put方法将新KV put 到m中。最后执行update方法:

protectedvoid update(Map<K,V> m) {
core = m;
view = Collections.unmodifiableMap(core);
}

可以看到这个方法将新的Map赋予给了core。
view =Collections.unmodifiableMap(core);则创建了一个core的不可修改的副本。不可修改主要体现在比如UnmodifiableMap的put方法:

public V put(K key, V value) {
throw new UnsupportedOperationException();
}

在上述介绍完Jenkins中hudson.util包实现Cowmap(CopyOnWriteMap)以后,我们基本介绍完了checkJobName方法的逻辑以及关联到的java技术点。 收起阅读 »

Selenium Grid容器化-分布式测试


本文分享技术的关键词

Selenium Grid 容器化
Demo演示(容器环境下分布式自动化测试)
基础镜像 (python+selenium依赖容器化)
本文分享的目的:实现Selenium Grid容器化,大大提高部署分布式测试环境效率,减少环境依赖。内容将介绍在容器环境中一键部署Selenium Grid,动态伸缩node节点,制作测试脚本的基础镜像。

Selenium Grid

Selenium作为web应用程序自动化测试利器之一,它支持多种客户端脚本(如:Java,Python,JavaScript等),并且可运行在多种浏览器中(如:chrome,firefox等)。Selenium经常被用于测试web应用程序在不同浏览器的兼容性。在企业级自动化测试中,常常结合Selenium组件Selenium Grid实现多节点分布式自动化测试。

Selenium Grid由中心管理服务Hub和多个node节点组成,node节点需要注册到Hub服务中。使用传统方式部署分布式测试环境比较繁琐和费时,本文将介绍使用docker-compose的方式实现一键部署Selenium Grid分布式测试环境。

s-1.jpg


如上图所示,要实现分布式测试,需要client测试脚本,HUB中心服务以及NODES执行节点。具体解释如下:
HUB: 中心节点服务,用于管理多个node节点,接受Client测试脚本的测试任务,将任务分配给多个node节点执行。
NODES: 真正执行测试任务的节点服务,支持多种浏览器进行测试,如:chrome firefox, node服务需注册到Hub中心服务,由中心服务统一分发任务。
Client: 这里是抽象概念,可理解为执行测试逻辑到客户端(代码),Client客户端需连接到Hub服务,执行测试脚本时由Hub中心服务统一分发给node节点进行执行。


docker-selenium

docker-selenium 实现了在容器环境中使用docker-compose方式快速部署Selenium Grid,伸缩node节点,下面通过详细步骤介绍部署方法。

step-1: 环境准备
docker  --version         #确认已安装docker
docker-compose  --version #确认已安装docker-compose

如未安装请参考:
https://docs.docker.com/install/         #先安装docker
https://docs.docker.com/compose/install/ #安装docker-compose


step-2: docker-compose一键拉起Hub和nodes

创建文件名为:docker-compose.yml,将如下内容填充到docker-compose.yml中
version: '2'
services:
firefox:
image: selenium/node-firefox:3.14.0-gallium  #node服务镜像,支持firefox
volumes:
  - /dev/shm:/dev/shm                        #挂在目录 /dev/shm
depends_on:
  - hub                                      #depends_on 注册到Hub服务
environment:
  HUB_HOST: hub

chrome:
image: selenium/node-chrome:3.14.0-gallium  #node服务镜像,支持chrome
volumes:
  - /dev/shm:/dev/shm                       #挂在目录 /dev/shm
depends_on:                                 #depends_on 注册到Hub服务
  - hub
environment:
  HUB_HOST: hub

hub:
image: selenium/hub:3.14.0-gallium          #中心服务hub镜像
ports:
  - "4444:4444"                             #暴露容器端口为4444,后面client代码需要访问这个端口


step-3:启动服务
docker-compose up -d  # 启动服务


step-4: 动态扩缩容node节点
docker-compose scale chrome=3    #指定支持chrome节点的实例数量
docker-compose scale firefox=3   #指定支持firefox节点的实例数量


step-5: 访问
http://localhost:4444/grid/console #访问Hub,能看到node节点都已注册上。


s-2.jpg


Client Demo演示

上面通过docker-compose已成功部署了Selenium Grid,其中心HUB服务端口为4444,测试脚本需要指向该端口。下面通过一个简单的测试Demo进行演示。
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

chrome = webdriver.Remote(
      command_executor='http://localhost:4444/wd/hub',
      desired_capabilities=DesiredCapabilities.CHROME)
firefox = webdriver.Remote(
      command_executor='http://localhost:4444/wd/hub',
      desired_capabilities=DesiredCapabilities.FIREFOX)

chrome.get('https://www.baidu.com')
print(chrome.title)

firefox.get('https://www.baidu.com')
print(firefox.title)


测试客户端代码指向Hub中心服务:http://localhost:4444/wd/hub'
将如上测试脚本另存为:test.py, 执行:python test.py 。执行成功输出:百度一下,你就知道。
selenium 测试脚本支持:Java Python JavaScript Ruby
更多请参考:https://seleniumhq.github.io/docs/site/en/

基础镜像

Demo演示需要依赖python以及selenium,才能执行测试脚本。试想如果我们把该测试脚本放到不同环境中,又需要安装一次python和seleniumyi依赖,很是繁琐。我们可以考虑将测试脚本需要的依赖做成一个基础镜像。

step-1: 编写Dockerfile
FROM python:3
COPY test.py ./
RUN pip install selenium
CMD [ "python3", "./test.py" ]


step-2: 制作image

{{{docker build -t selenium-base:latest .

在Dockerfile所在目录执行如上命令, 镜像名为:selenium-base}}}

step-3: 使用基础镜像运行测试脚本

{{{docker run -it --net=host selenium-base

执行成功输出:百度一下,你就知道}}}

总结

本文介绍了使用docker-compose一键拉起Selenium Grid分布式测试环境,简单python脚本Demo执行自动化测试,最后将python的依赖统一做成基础镜像,演示了从部署到执行脚本完全容器化到流程。例子很简单,旨在提供一种容器化思路。当部署程序步骤比较繁琐且需要重复进行,都可以考虑将其容器化。

参考文档

docker-selenium-github: https://github.com/SeleniumHQ/ ... mpose
python-dockerHub: https://hub.docker.com/_/python
selenium-grid: https://seleniumhq.github.io/d ... grid/

关于睿云智合

深圳睿云智合科技有限公司成立于2012年,总部位于深圳,并分别在成都、深圳设立了研发中心,北京、上海设立了分支机构,核心骨干人员全部为来自金融、科技行业知名企业资深业务专家、技术专家。早期专注于为中国金融保险等大型企业提供创新技术、电子商务、CRM等领域专业咨询服务。

自2016年始,在率先将容器技术引进到中国保险行业客户后,公司组建了专业的容器技术产品研发和实施服务团队,旨在帮助中国金融行业客户将容器创新技术应用于企业信息技术支持业务发展的基础能力改善与提升,成为中国金融保险行业容器技术服务领导品牌。

此外,凭借多年来在呼叫中心领域的业务经验与技术积累,睿云智合率先在业界推出基于开源软交换平台FreeSwitch的微服务架构多媒体数字化业务平台,将语音、视频、webchat、微信、微博等多种客户接触渠道集成,实现客户统一接入、精准识别、智能路由的CRM策略,并以容器化治理来支持平台的全应用生命周期管理,显著提升了数字化业务处理的灵活、高效、弹性、稳定等特性,为帮助传统企业向“以客户为中心”的数字化业务转型提供完美的一站式整体解决方案。
继续阅读 »

本文分享技术的关键词

Selenium Grid 容器化
Demo演示(容器环境下分布式自动化测试)
基础镜像 (python+selenium依赖容器化)
本文分享的目的:实现Selenium Grid容器化,大大提高部署分布式测试环境效率,减少环境依赖。内容将介绍在容器环境中一键部署Selenium Grid,动态伸缩node节点,制作测试脚本的基础镜像。

Selenium Grid

Selenium作为web应用程序自动化测试利器之一,它支持多种客户端脚本(如:Java,Python,JavaScript等),并且可运行在多种浏览器中(如:chrome,firefox等)。Selenium经常被用于测试web应用程序在不同浏览器的兼容性。在企业级自动化测试中,常常结合Selenium组件Selenium Grid实现多节点分布式自动化测试。

Selenium Grid由中心管理服务Hub和多个node节点组成,node节点需要注册到Hub服务中。使用传统方式部署分布式测试环境比较繁琐和费时,本文将介绍使用docker-compose的方式实现一键部署Selenium Grid分布式测试环境。

s-1.jpg


如上图所示,要实现分布式测试,需要client测试脚本,HUB中心服务以及NODES执行节点。具体解释如下:
HUB: 中心节点服务,用于管理多个node节点,接受Client测试脚本的测试任务,将任务分配给多个node节点执行。
NODES: 真正执行测试任务的节点服务,支持多种浏览器进行测试,如:chrome firefox, node服务需注册到Hub中心服务,由中心服务统一分发任务。
Client: 这里是抽象概念,可理解为执行测试逻辑到客户端(代码),Client客户端需连接到Hub服务,执行测试脚本时由Hub中心服务统一分发给node节点进行执行。


docker-selenium

docker-selenium 实现了在容器环境中使用docker-compose方式快速部署Selenium Grid,伸缩node节点,下面通过详细步骤介绍部署方法。

step-1: 环境准备
docker  --version         #确认已安装docker
docker-compose  --version #确认已安装docker-compose

如未安装请参考:
https://docs.docker.com/install/         #先安装docker
https://docs.docker.com/compose/install/ #安装docker-compose


step-2: docker-compose一键拉起Hub和nodes

创建文件名为:docker-compose.yml,将如下内容填充到docker-compose.yml中
version: '2'
services:
firefox:
image: selenium/node-firefox:3.14.0-gallium  #node服务镜像,支持firefox
volumes:
  - /dev/shm:/dev/shm                        #挂在目录 /dev/shm
depends_on:
  - hub                                      #depends_on 注册到Hub服务
environment:
  HUB_HOST: hub

chrome:
image: selenium/node-chrome:3.14.0-gallium  #node服务镜像,支持chrome
volumes:
  - /dev/shm:/dev/shm                       #挂在目录 /dev/shm
depends_on:                                 #depends_on 注册到Hub服务
  - hub
environment:
  HUB_HOST: hub

hub:
image: selenium/hub:3.14.0-gallium          #中心服务hub镜像
ports:
  - "4444:4444"                             #暴露容器端口为4444,后面client代码需要访问这个端口


step-3:启动服务
docker-compose up -d  # 启动服务


step-4: 动态扩缩容node节点
docker-compose scale chrome=3    #指定支持chrome节点的实例数量
docker-compose scale firefox=3   #指定支持firefox节点的实例数量


step-5: 访问
http://localhost:4444/grid/console #访问Hub,能看到node节点都已注册上。


s-2.jpg


Client Demo演示

上面通过docker-compose已成功部署了Selenium Grid,其中心HUB服务端口为4444,测试脚本需要指向该端口。下面通过一个简单的测试Demo进行演示。
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities

chrome = webdriver.Remote(
      command_executor='http://localhost:4444/wd/hub',
      desired_capabilities=DesiredCapabilities.CHROME)
firefox = webdriver.Remote(
      command_executor='http://localhost:4444/wd/hub',
      desired_capabilities=DesiredCapabilities.FIREFOX)

chrome.get('https://www.baidu.com')
print(chrome.title)

firefox.get('https://www.baidu.com')
print(firefox.title)


测试客户端代码指向Hub中心服务:http://localhost:4444/wd/hub'
将如上测试脚本另存为:test.py, 执行:python test.py 。执行成功输出:百度一下,你就知道。
selenium 测试脚本支持:Java Python JavaScript Ruby
更多请参考:https://seleniumhq.github.io/docs/site/en/

基础镜像

Demo演示需要依赖python以及selenium,才能执行测试脚本。试想如果我们把该测试脚本放到不同环境中,又需要安装一次python和seleniumyi依赖,很是繁琐。我们可以考虑将测试脚本需要的依赖做成一个基础镜像。

step-1: 编写Dockerfile
FROM python:3
COPY test.py ./
RUN pip install selenium
CMD [ "python3", "./test.py" ]


step-2: 制作image

{{{docker build -t selenium-base:latest .

在Dockerfile所在目录执行如上命令, 镜像名为:selenium-base}}}

step-3: 使用基础镜像运行测试脚本

{{{docker run -it --net=host selenium-base

执行成功输出:百度一下,你就知道}}}

总结

本文介绍了使用docker-compose一键拉起Selenium Grid分布式测试环境,简单python脚本Demo执行自动化测试,最后将python的依赖统一做成基础镜像,演示了从部署到执行脚本完全容器化到流程。例子很简单,旨在提供一种容器化思路。当部署程序步骤比较繁琐且需要重复进行,都可以考虑将其容器化。

参考文档

docker-selenium-github: https://github.com/SeleniumHQ/ ... mpose
python-dockerHub: https://hub.docker.com/_/python
selenium-grid: https://seleniumhq.github.io/d ... grid/

关于睿云智合

深圳睿云智合科技有限公司成立于2012年,总部位于深圳,并分别在成都、深圳设立了研发中心,北京、上海设立了分支机构,核心骨干人员全部为来自金融、科技行业知名企业资深业务专家、技术专家。早期专注于为中国金融保险等大型企业提供创新技术、电子商务、CRM等领域专业咨询服务。

自2016年始,在率先将容器技术引进到中国保险行业客户后,公司组建了专业的容器技术产品研发和实施服务团队,旨在帮助中国金融行业客户将容器创新技术应用于企业信息技术支持业务发展的基础能力改善与提升,成为中国金融保险行业容器技术服务领导品牌。

此外,凭借多年来在呼叫中心领域的业务经验与技术积累,睿云智合率先在业界推出基于开源软交换平台FreeSwitch的微服务架构多媒体数字化业务平台,将语音、视频、webchat、微信、微博等多种客户接触渠道集成,实现客户统一接入、精准识别、智能路由的CRM策略,并以容器化治理来支持平台的全应用生命周期管理,显著提升了数字化业务处理的灵活、高效、弹性、稳定等特性,为帮助传统企业向“以客户为中心”的数字化业务转型提供完美的一站式整体解决方案。 收起阅读 »

传统大型银行转型拥抱Kubernetes


【编者的话】Kubernetes已经成为一个标准的基础设施,就连传统大型银行(如意大利最大的银行联合圣保罗银行)已经开始转型拥抱Kubernetes了,这是一场令人兴奋的技术变革,让我们一览为快吧。

尽管大多数企业仍未在生产环境中应用Kubernetes,但这家意大利最大的银行显示出了前进的方向。

Kubernetes已经成为一个标准的基础设施API,Red Hat、Mesosphere(现在是D2IQ)和Pivotal等供应商都无法逃避其吸引。如果你从事的业务是使企业能够构建应用程序,那么你要支持Kubernetes了。

对于那些应用构建企业来说,Kubernetes的应用仍然有一定的吸引力,CNCF在2018年的一项调查中发现,生产中运行Kubernetes的企业占40%。在尚未上Kubernetes列车的60%的被调查者中你会发现有厌恶风险的银行。作为一个垂直的行业,传统银行不像对冲基金和交易兄弟公司那样不断寻求优势,除非迫不得已,否则它们不喜欢跨越技术鸿沟。这些人仍然在30年前的大型机技术上运行他们的ATM网络。

Kubernetes正在改变这一点,我听说过ING拥抱Kubernetes,但是这个用例遵循了DevOps社区中其他早期采用者的做法。容器和编排对于改进CI/CD非常有用,但是传统的银行会在这样一种年轻的技术上运行其真正的业务吗?作为Kubernetes已经变得多么炙手可热的一个标志,这家意大利最大的银行不仅会这么做,而且正在这么做。

这其中的原因以及Kubernetes所要求的文化变革令人着迷。

意大利式的数字化转型

Banca Intesa Sanpaolo银行,成立于2007年,由意大利Banca Intesa商业银行和圣保罗IMI合并而成,是意大利最大的银行,也是欧洲最大的银行,市值340亿美元。总部设在都灵,银行有5000多个分支机构,服务欧洲和中东十几个国家的约1900万客户,并在全球超过25个国家/地区提供支持。

2018年,该银行启动了一项战略性数字转型倡议,称为“通过创新实现数字架构再造工程”。其策略是采用微服务和容器体系结构,并从单体应用程序迁移到多层应用程序。其目标是加速开发周期,减少应用程序占用空间以获得更大的灵活性,并提高可伸缩性和可靠性。该银行的IT部门正在转型为一家基于现代CI/CD实践的软件公司。

该计划的核心是运行由Kubernetes管理的容器的挑战。

该银行首先在其遗留的虚拟机(VM)基础设施上运行试验容器项目。这些试点项目取得了成功,但该银行想看看能否在裸金属上运行Kubernetes和容器。它能否利用性能优势,同时避免为VM许可证付费的开销呢?

这并不是一个不起眼的决定,因为裸金属和Kubernetes通常意味着DIY。Banca Intesa Sanpaolo没有从零开始建设,而是采用Diamanti开发的一种设备方法。Diamanti系统是一个普通的x86服务器设备,预装了普通的Linux和Kubernetes,但增加了一些cards来克服网络和存储方面的I/O挑战,这些挑战可能会阻碍Kubernetes在生产环境中的部署。

扼杀老式应用程序

从软件的角度来看,这种方法意味着银行可以继续专注于Kubernetes和容器策略。同时,底层基础设施层还可以满足其对存储和网络虚拟化的所有要求,并具有高性能级别,以满足业务单元SLA。Diamanti的管理软件为银行提供了跨多区域和多站点集群的高可用性,为不同应用程序提供了不同业务临界级别的服务质量保证。

如今,该银行受理了3000多份申请。其中,目前超过120家正在使用新的微服务体系结构进行生产,其中包括对Banca Intesa Sanpaolo最关键的10家企业中的两家。

Banca Intesa Sanpaolo希望在微服务上运行哪些类型的应用程序呢?从一开始,团队就关注两类应用程序:新应用程序和单体应用程序。所有新的应用程序都立即使用微服务方法构建。

对于现有的单体应用程序,银行遵循所谓的扼杀应用程序模式。随着新功能被添加到任何遗留应用程序中,每个新功能都被添加为一个新的微服务迷你应用程序。遗留应用程序和微服务应用程序并行运行,直到最终迁移到一个新应用程序中,在这个新应用程序中,旧的单体应用在生命结束时被“扼杀”。

软件开发不再是所有参与者都坚持使用单一管道的场景,在这种场景中,单个提交可能导致构建失败,并导致开发、测试和部署过程停滞。这个过程将变成这样一个过程:每个参与者都有自己的开发流程来开发每个专用组件。

这一更改使操作更容易地扩展应用程序团队对其特定基础设施的需求,这是一个优雅的解决方案。应用程序的每个组件都依赖于一个可以水平伸缩的专用容器。通过避免失败的多米诺骨牌效应,可靠性显著提高。新方法还简化了自动化,消除了开发人员和操作人员在推出新应用程序时的许多手工步骤,这总体上提高了代码质量。

应对其他挑战

虽然向容器、Kubernetes和微服务体系结构的转变在可伸缩性、可靠性以及开发和部署的速度方面带来了数量级的改进,但Banca Intesa Sanpaolo也面临着巨大的管理挑战。
  • 分级:第一个挑战是准确分级运行微服务体系结构所需的底层基础设施,因为它基于一个新的范例。该银行过去用于传统单体应用程序的规则需要改进和更改。微服务应用程序的行为与单体不同,而且它们消耗的资源也不相同,必须掌握新的学习曲线。
  • 流程:使微服务与现有的数据中心生态系统协同工作,是该行构建和实现应用程序以及为基础基础设施提供资源的方式的根本性变化。研究小组发现,在这个过程中使用容器平台与Diamanti技术是非常有用,但从应用程序的观点看,对于新建应用程序将变得简单,但是作为微服务银行打算重写数以千计的应用程序,将需要大量的工作。
  • 文化挑战:DevOps的概念以及开发人员和操作人员之间的思想差异需要一种新的方式来思考如何创建和部署应用程序。


最后一点是最容易忽略的,但对任何企业来说,在实践中都很难做到。Banca Intesa Sanpaolo已经在路上了,但这是一段需要时间和耐心才能成功驾驭的旅程。

作者:Matt Asay是一位资深的技术专栏作家,曾为CNET、ReadWrite和其他技术媒体撰写文章。Asay还在领先的移动和大数据软件公司担任过多种管理职位。

原文链接:How hot is Kubernetes? Even traditional banks are transforming to embrace it

译者:Mr.lzc,软件工程师、DevOpsDays深圳核心组织者&志愿者,目前供职于华为,从事云存储工作,以Cloud Native方式构建云文件系统服务,专注于Kubernetes、微服务、Serverless领域。
继续阅读 »

【编者的话】Kubernetes已经成为一个标准的基础设施,就连传统大型银行(如意大利最大的银行联合圣保罗银行)已经开始转型拥抱Kubernetes了,这是一场令人兴奋的技术变革,让我们一览为快吧。

尽管大多数企业仍未在生产环境中应用Kubernetes,但这家意大利最大的银行显示出了前进的方向。

Kubernetes已经成为一个标准的基础设施API,Red Hat、Mesosphere(现在是D2IQ)和Pivotal等供应商都无法逃避其吸引。如果你从事的业务是使企业能够构建应用程序,那么你要支持Kubernetes了。

对于那些应用构建企业来说,Kubernetes的应用仍然有一定的吸引力,CNCF在2018年的一项调查中发现,生产中运行Kubernetes的企业占40%。在尚未上Kubernetes列车的60%的被调查者中你会发现有厌恶风险的银行。作为一个垂直的行业,传统银行不像对冲基金和交易兄弟公司那样不断寻求优势,除非迫不得已,否则它们不喜欢跨越技术鸿沟。这些人仍然在30年前的大型机技术上运行他们的ATM网络。

Kubernetes正在改变这一点,我听说过ING拥抱Kubernetes,但是这个用例遵循了DevOps社区中其他早期采用者的做法。容器和编排对于改进CI/CD非常有用,但是传统的银行会在这样一种年轻的技术上运行其真正的业务吗?作为Kubernetes已经变得多么炙手可热的一个标志,这家意大利最大的银行不仅会这么做,而且正在这么做。

这其中的原因以及Kubernetes所要求的文化变革令人着迷。

意大利式的数字化转型

Banca Intesa Sanpaolo银行,成立于2007年,由意大利Banca Intesa商业银行和圣保罗IMI合并而成,是意大利最大的银行,也是欧洲最大的银行,市值340亿美元。总部设在都灵,银行有5000多个分支机构,服务欧洲和中东十几个国家的约1900万客户,并在全球超过25个国家/地区提供支持。

2018年,该银行启动了一项战略性数字转型倡议,称为“通过创新实现数字架构再造工程”。其策略是采用微服务和容器体系结构,并从单体应用程序迁移到多层应用程序。其目标是加速开发周期,减少应用程序占用空间以获得更大的灵活性,并提高可伸缩性和可靠性。该银行的IT部门正在转型为一家基于现代CI/CD实践的软件公司。

该计划的核心是运行由Kubernetes管理的容器的挑战。

该银行首先在其遗留的虚拟机(VM)基础设施上运行试验容器项目。这些试点项目取得了成功,但该银行想看看能否在裸金属上运行Kubernetes和容器。它能否利用性能优势,同时避免为VM许可证付费的开销呢?

这并不是一个不起眼的决定,因为裸金属和Kubernetes通常意味着DIY。Banca Intesa Sanpaolo没有从零开始建设,而是采用Diamanti开发的一种设备方法。Diamanti系统是一个普通的x86服务器设备,预装了普通的Linux和Kubernetes,但增加了一些cards来克服网络和存储方面的I/O挑战,这些挑战可能会阻碍Kubernetes在生产环境中的部署。

扼杀老式应用程序

从软件的角度来看,这种方法意味着银行可以继续专注于Kubernetes和容器策略。同时,底层基础设施层还可以满足其对存储和网络虚拟化的所有要求,并具有高性能级别,以满足业务单元SLA。Diamanti的管理软件为银行提供了跨多区域和多站点集群的高可用性,为不同应用程序提供了不同业务临界级别的服务质量保证。

如今,该银行受理了3000多份申请。其中,目前超过120家正在使用新的微服务体系结构进行生产,其中包括对Banca Intesa Sanpaolo最关键的10家企业中的两家。

Banca Intesa Sanpaolo希望在微服务上运行哪些类型的应用程序呢?从一开始,团队就关注两类应用程序:新应用程序和单体应用程序。所有新的应用程序都立即使用微服务方法构建。

对于现有的单体应用程序,银行遵循所谓的扼杀应用程序模式。随着新功能被添加到任何遗留应用程序中,每个新功能都被添加为一个新的微服务迷你应用程序。遗留应用程序和微服务应用程序并行运行,直到最终迁移到一个新应用程序中,在这个新应用程序中,旧的单体应用在生命结束时被“扼杀”。

软件开发不再是所有参与者都坚持使用单一管道的场景,在这种场景中,单个提交可能导致构建失败,并导致开发、测试和部署过程停滞。这个过程将变成这样一个过程:每个参与者都有自己的开发流程来开发每个专用组件。

这一更改使操作更容易地扩展应用程序团队对其特定基础设施的需求,这是一个优雅的解决方案。应用程序的每个组件都依赖于一个可以水平伸缩的专用容器。通过避免失败的多米诺骨牌效应,可靠性显著提高。新方法还简化了自动化,消除了开发人员和操作人员在推出新应用程序时的许多手工步骤,这总体上提高了代码质量。

应对其他挑战

虽然向容器、Kubernetes和微服务体系结构的转变在可伸缩性、可靠性以及开发和部署的速度方面带来了数量级的改进,但Banca Intesa Sanpaolo也面临着巨大的管理挑战。
  • 分级:第一个挑战是准确分级运行微服务体系结构所需的底层基础设施,因为它基于一个新的范例。该银行过去用于传统单体应用程序的规则需要改进和更改。微服务应用程序的行为与单体不同,而且它们消耗的资源也不相同,必须掌握新的学习曲线。
  • 流程:使微服务与现有的数据中心生态系统协同工作,是该行构建和实现应用程序以及为基础基础设施提供资源的方式的根本性变化。研究小组发现,在这个过程中使用容器平台与Diamanti技术是非常有用,但从应用程序的观点看,对于新建应用程序将变得简单,但是作为微服务银行打算重写数以千计的应用程序,将需要大量的工作。
  • 文化挑战:DevOps的概念以及开发人员和操作人员之间的思想差异需要一种新的方式来思考如何创建和部署应用程序。


最后一点是最容易忽略的,但对任何企业来说,在实践中都很难做到。Banca Intesa Sanpaolo已经在路上了,但这是一段需要时间和耐心才能成功驾驭的旅程。

作者:Matt Asay是一位资深的技术专栏作家,曾为CNET、ReadWrite和其他技术媒体撰写文章。Asay还在领先的移动和大数据软件公司担任过多种管理职位。

原文链接:How hot is Kubernetes? Even traditional banks are transforming to embrace it

译者:Mr.lzc,软件工程师、DevOpsDays深圳核心组织者&志愿者,目前供职于华为,从事云存储工作,以Cloud Native方式构建云文件系统服务,专注于Kubernetes、微服务、Serverless领域。 收起阅读 »

使用Werf和现有的Dockerfiles改善你的CI/CD体验


迟到总比不到好。该故事讲关于我们因不支持使用常规的Dockerfile来构建镜像导致我们差点犯了一个重大错误。

Werf是一个GitOps工具,可以很好地集成到任何CI/CD系统中,并提供完整的应用程序生命周期管理,允许你:
  • 构建和推送镜像
  • 部署应用程序到Kubernetes中
  • 根据策略清理未使用镜像


我们工具的理念是:将低级手段组合到一个统一的系统中让DevOps工程师控制应用程序。尽可能使用现有的即用型工具(如Helm和Docker)。但是如果没有合适的任务解决方案呢?答案很简单,我们自己编写并维护工具来完成工作。

背景:自定义镜像构建器

Werf镜像构建器也发生了同样的故事。Dockerfile是描述构建镜像过程的事实标准,但我们的需求受到很大限制。这个问题在我们项目的早期阶段变得至关重要。在开发用于容器化应用的工具时,我们很快意识到Dockerfile不适合以下特定任务:
  1. 遵循构建典型小型Web应用程序的标准工作流程:a)安装系统范围的应用程序依赖项,b)安装特定于应用程序的库,c)构建资产(assets),d)最重要的部分,快速高效地更新镜像中的代码。
  2. 构建器在发生更改时应通过提交修补应用于修改的文件来创建新的镜像层。
  3. 如果某些文件已被修改,则必须重建依赖阶段。


这是我们一开始的需求列表,在今天我们的构建器有着许多额外的功能。

总而言之,我们没花多长时间就开始使用首选编程语言开发自定义DSL(见下文)。它必须满足既定目标,根据文件描述分阶段的构建过程并确定不同阶段间的依赖。它由相应的那些可将DSL转变成最终目标的构建器,即即用型Docker镜像补充。一开始我们使用Ruby实现了DSL,在切换到Golang之后我们用YAML文件形式重写了它。
01.png

Werf的Ruby的配置,这是旧的版本(此时项目被称为dapp)

02.png

Werf的YAML形式配置,这是现在的版本

构建器的概念随时间推移一直在变化。在一开始,我们只是简单地使用我们的配置动态地生成一些临时的Dockerfile,然后我们在临时容器中运行构建指令并进行提交。

注意:目前我们使用YAML配置的Stapel构建器(如上所示)已经发展成一个相当强大的工具。虽然它值得一篇文章来详细描述其本身,但你现在可以先在该文档中找到更多详细信息。

等一下!

不久之后,我们发现了一个严重的错误,那就是我们没有添加使用标准Dockerfiles构建镜像的能力,我们将它们集成到已建立的基础架构中以进行完整的应用程序管理(即用于构建,部署和删除镜像)。“我们怎么可能在没有支持Dockerfile的情况下打造为Kubernetes部署镜像的工具,这是否是一种描述大多数项目镜像的流行方式?”这个问题依旧困扰着我们。

我们没有回答该问题,而是提出了解决方案。如果你已经有了一个Dockerfile(或一组Dockerfile)且想使用werf呢?

注意:顺便问一下,你为何要使用werf?至少,它有各种很好的功能来增强和粘合你的CI/CD流程,例如:
  • 完整的应用程序管理周期,包括删除镜像
  • 在单个配置中构建多个镜像的能力
  • 改进的部署Helm兼容图表的流程


完整的功能列表请点击项目页面

因此,直至最近,如果你对使用werf感兴趣,我们还希望你将Dockerfiles移植到我们的配置格式。但是现在我们很高兴地告诉你,“让werf来构建你的Dockerfiles吧!”

用法

此功能的首次完整实现在werf的v1.0.3-beta.1版本引入。

一般流程非常简单,即用户在werf配置中指定已存在的Dockerfile的路径,然后使用werf build命令启动werf。然后就没有然后了,werf将会构建镜像。

以下是一个例子。我们在应用的根目录中定义Dockerfile
FROM ubuntu:18.04
RUN echo Building ...

然后我们定义werf.yaml它将引用上面的Dockerfile
configVersion: 1
project: dockerfile-example
---
image: ~
dockerfile: ./Dockerfile

然后我们就可以执行werf build了:
03.png

顺便说下,你也可以这样定义werf.yaml以使用多个Dockerfile同时构建镜像:
configVersion: 1
project: dockerfile-example
---
image: backend
dockerfile: ./dockerfiles/Dockerfile-backend
---
image: frontend
dockerfile: ./dockerfiles/Dockerfile-frontend

werf配置中同样支持传递额外的构建参数,例如--build-arg--add-host等。以下是完整Dockerfile镜像配置的链接

它是如何运作的?

在构建镜像期间,本地层的通用Docker缓存处于活动状态。重要的是,werf还将Dockerfile配置集成到其基础架构中。这意味着什么?
  1. 所有使用Dockerfile构建的镜像都包含一个特定的名为dockerfile的阶段。(可以在werf文档中了解“阶段”相关内容)
  2. dockerfile阶段,werf会根据Dockerfile配置中的内容计算出签名。Dockerfile配置的改变将会引起dockerfile阶段的签名改变。在这种情况下,werf使用新的Dockerfile配置启动此阶段的重建。如果签名保持不变,则werf使用缓存的镜像。
  3. 你可是使用werf publishwerf build-and-publish发布构建出来的镜像并将其部署到Kubernetes中。已推送到Docker仓库的镜像会通过常规werf机制进行清理。这意味着旧镜像(超过N天)和与不存在的Git分支相关联的镜像将被自动删除,且可以应用其他策略。


你可以在相应的文档中了解有关这些werf特性的更多信息:


提示和警告

1. 指令ADD不支持外部URL

当前ADD参数不能支持外部的URL。Werf不会启动重建过程以响应指定URL处的资源更改。我们计划很快添加此功能。

2. 不能将.git目录包含到镜像中

事实上,将.git目录添加到你的容器镜像中不是个好主意,原因如下:
  • .git目录在最终镜像中的存在违反了12要素应用理念。最终镜像必须与单个提交链接;不应允许其在任意提交上执行git checkout
  • .git目录增大了镜像的体积(Git仓库可能会因为曾经添加删除过大文件而增大)。相反地,每个特定提交对工作树大小将不依赖于Git操作的历史。此外,.git目录从最终镜像中添加以及后续的删除,文件夹将不再起作用,因为无论如何都将生成新的层次(这正是Docker的工作原理)。
  • 即使正在处理相同的提交(源自不同的工作树),Docker也可能启动不必要的重建。例如,GitLab在/home/gitlab-runner/builds/HASH/[0-N]/yourproject启用并行构建时创建单独的克隆文件夹。不必要的重建是由.git同一存储库的各种克隆版本中的文件夹的差异引起的(即使我们构建完全相同的提交)。


最后一点直接影响了werf的使用。Werf需要构建缓存来运行某些命令(例如werf deploy)。执行这些命令时,werf会为werf.yaml文件中指定的镜像计算各阶段签名,因此它们必须存在于构建缓存中,否则命令将会失败。.git内容在签名阶段的依赖意味着缓存容易受到无关文件的影响,这是werf无法容忍的错误(更多细节)。

无论如何,通过ADDCOPY指令添加特定和需要的文件仍然是一个很好的做法。它提高了创建的效率和所创建的Dockerfile的可靠性,并提高了缓存(通过上述内容构建Dockerfile)对Git中无关的更改的弹性。

总结

我们为特定需求制作自定义构建器之路是艰难的,诚实的和直接的:我们倾向于使用自定义语法开发自己的解决方案,而不是将解决方法置于默认Dockerfile之上。这种方法有其优势:Stapel构建器就做得很好!

但是,在创建自定义镜像构建器时,我们完全忽略了应用现有Dockerfiles的情况。这个漏洞现在已经解决了。在未来,我们计划加强对Dockerfiles的支持以及我们的定制Stapel构建器,用于分布式构建和在Kubernetes集群内构建镜像(即通过使用类似于kaniko的Kubernetes中的运行程序)。

所以,如果你碰巧有一些好的Dockerfiles,不要犹豫,快来尝试werf吧!

以下是一份简短的阅读清单,其内容将包含一些可用信息:


原文链接:Improve your CI/CD experience with werf and existing Dockerfiles(翻译:冯旭松)
继续阅读 »

迟到总比不到好。该故事讲关于我们因不支持使用常规的Dockerfile来构建镜像导致我们差点犯了一个重大错误。

Werf是一个GitOps工具,可以很好地集成到任何CI/CD系统中,并提供完整的应用程序生命周期管理,允许你:
  • 构建和推送镜像
  • 部署应用程序到Kubernetes中
  • 根据策略清理未使用镜像


我们工具的理念是:将低级手段组合到一个统一的系统中让DevOps工程师控制应用程序。尽可能使用现有的即用型工具(如Helm和Docker)。但是如果没有合适的任务解决方案呢?答案很简单,我们自己编写并维护工具来完成工作。

背景:自定义镜像构建器

Werf镜像构建器也发生了同样的故事。Dockerfile是描述构建镜像过程的事实标准,但我们的需求受到很大限制。这个问题在我们项目的早期阶段变得至关重要。在开发用于容器化应用的工具时,我们很快意识到Dockerfile不适合以下特定任务:
  1. 遵循构建典型小型Web应用程序的标准工作流程:a)安装系统范围的应用程序依赖项,b)安装特定于应用程序的库,c)构建资产(assets),d)最重要的部分,快速高效地更新镜像中的代码。
  2. 构建器在发生更改时应通过提交修补应用于修改的文件来创建新的镜像层。
  3. 如果某些文件已被修改,则必须重建依赖阶段。


这是我们一开始的需求列表,在今天我们的构建器有着许多额外的功能。

总而言之,我们没花多长时间就开始使用首选编程语言开发自定义DSL(见下文)。它必须满足既定目标,根据文件描述分阶段的构建过程并确定不同阶段间的依赖。它由相应的那些可将DSL转变成最终目标的构建器,即即用型Docker镜像补充。一开始我们使用Ruby实现了DSL,在切换到Golang之后我们用YAML文件形式重写了它。
01.png

Werf的Ruby的配置,这是旧的版本(此时项目被称为dapp)

02.png

Werf的YAML形式配置,这是现在的版本

构建器的概念随时间推移一直在变化。在一开始,我们只是简单地使用我们的配置动态地生成一些临时的Dockerfile,然后我们在临时容器中运行构建指令并进行提交。

注意:目前我们使用YAML配置的Stapel构建器(如上所示)已经发展成一个相当强大的工具。虽然它值得一篇文章来详细描述其本身,但你现在可以先在该文档中找到更多详细信息。

等一下!

不久之后,我们发现了一个严重的错误,那就是我们没有添加使用标准Dockerfiles构建镜像的能力,我们将它们集成到已建立的基础架构中以进行完整的应用程序管理(即用于构建,部署和删除镜像)。“我们怎么可能在没有支持Dockerfile的情况下打造为Kubernetes部署镜像的工具,这是否是一种描述大多数项目镜像的流行方式?”这个问题依旧困扰着我们。

我们没有回答该问题,而是提出了解决方案。如果你已经有了一个Dockerfile(或一组Dockerfile)且想使用werf呢?

注意:顺便问一下,你为何要使用werf?至少,它有各种很好的功能来增强和粘合你的CI/CD流程,例如:
  • 完整的应用程序管理周期,包括删除镜像
  • 在单个配置中构建多个镜像的能力
  • 改进的部署Helm兼容图表的流程


完整的功能列表请点击项目页面

因此,直至最近,如果你对使用werf感兴趣,我们还希望你将Dockerfiles移植到我们的配置格式。但是现在我们很高兴地告诉你,“让werf来构建你的Dockerfiles吧!”

用法

此功能的首次完整实现在werf的v1.0.3-beta.1版本引入。

一般流程非常简单,即用户在werf配置中指定已存在的Dockerfile的路径,然后使用werf build命令启动werf。然后就没有然后了,werf将会构建镜像。

以下是一个例子。我们在应用的根目录中定义Dockerfile
FROM ubuntu:18.04
RUN echo Building ...

然后我们定义werf.yaml它将引用上面的Dockerfile
configVersion: 1
project: dockerfile-example
---
image: ~
dockerfile: ./Dockerfile

然后我们就可以执行werf build了:
03.png

顺便说下,你也可以这样定义werf.yaml以使用多个Dockerfile同时构建镜像:
configVersion: 1
project: dockerfile-example
---
image: backend
dockerfile: ./dockerfiles/Dockerfile-backend
---
image: frontend
dockerfile: ./dockerfiles/Dockerfile-frontend

werf配置中同样支持传递额外的构建参数,例如--build-arg--add-host等。以下是完整Dockerfile镜像配置的链接

它是如何运作的?

在构建镜像期间,本地层的通用Docker缓存处于活动状态。重要的是,werf还将Dockerfile配置集成到其基础架构中。这意味着什么?
  1. 所有使用Dockerfile构建的镜像都包含一个特定的名为dockerfile的阶段。(可以在werf文档中了解“阶段”相关内容)
  2. dockerfile阶段,werf会根据Dockerfile配置中的内容计算出签名。Dockerfile配置的改变将会引起dockerfile阶段的签名改变。在这种情况下,werf使用新的Dockerfile配置启动此阶段的重建。如果签名保持不变,则werf使用缓存的镜像。
  3. 你可是使用werf publishwerf build-and-publish发布构建出来的镜像并将其部署到Kubernetes中。已推送到Docker仓库的镜像会通过常规werf机制进行清理。这意味着旧镜像(超过N天)和与不存在的Git分支相关联的镜像将被自动删除,且可以应用其他策略。


你可以在相应的文档中了解有关这些werf特性的更多信息:


提示和警告

1. 指令ADD不支持外部URL

当前ADD参数不能支持外部的URL。Werf不会启动重建过程以响应指定URL处的资源更改。我们计划很快添加此功能。

2. 不能将.git目录包含到镜像中

事实上,将.git目录添加到你的容器镜像中不是个好主意,原因如下:
  • .git目录在最终镜像中的存在违反了12要素应用理念。最终镜像必须与单个提交链接;不应允许其在任意提交上执行git checkout
  • .git目录增大了镜像的体积(Git仓库可能会因为曾经添加删除过大文件而增大)。相反地,每个特定提交对工作树大小将不依赖于Git操作的历史。此外,.git目录从最终镜像中添加以及后续的删除,文件夹将不再起作用,因为无论如何都将生成新的层次(这正是Docker的工作原理)。
  • 即使正在处理相同的提交(源自不同的工作树),Docker也可能启动不必要的重建。例如,GitLab在/home/gitlab-runner/builds/HASH/[0-N]/yourproject启用并行构建时创建单独的克隆文件夹。不必要的重建是由.git同一存储库的各种克隆版本中的文件夹的差异引起的(即使我们构建完全相同的提交)。


最后一点直接影响了werf的使用。Werf需要构建缓存来运行某些命令(例如werf deploy)。执行这些命令时,werf会为werf.yaml文件中指定的镜像计算各阶段签名,因此它们必须存在于构建缓存中,否则命令将会失败。.git内容在签名阶段的依赖意味着缓存容易受到无关文件的影响,这是werf无法容忍的错误(更多细节)。

无论如何,通过ADDCOPY指令添加特定和需要的文件仍然是一个很好的做法。它提高了创建的效率和所创建的Dockerfile的可靠性,并提高了缓存(通过上述内容构建Dockerfile)对Git中无关的更改的弹性。

总结

我们为特定需求制作自定义构建器之路是艰难的,诚实的和直接的:我们倾向于使用自定义语法开发自己的解决方案,而不是将解决方法置于默认Dockerfile之上。这种方法有其优势:Stapel构建器就做得很好!

但是,在创建自定义镜像构建器时,我们完全忽略了应用现有Dockerfiles的情况。这个漏洞现在已经解决了。在未来,我们计划加强对Dockerfiles的支持以及我们的定制Stapel构建器,用于分布式构建和在Kubernetes集群内构建镜像(即通过使用类似于kaniko的Kubernetes中的运行程序)。

所以,如果你碰巧有一些好的Dockerfiles,不要犹豫,快来尝试werf吧!

以下是一份简短的阅读清单,其内容将包含一些可用信息:


原文链接:Improve your CI/CD experience with werf and existing Dockerfiles(翻译:冯旭松) 收起阅读 »

DockOne微信分享(二二五):eBay Kubernetes集群的存储实践


【编者的话】存储是云平台最核心最基本的功能之一,如何在Kubernetes容器平台上对接不同的后端类型,并根据实际需求做选择和定制,会遇到哪些问题,怎么管理?本次分享将介绍eBay存储的实现,使用方式,并介绍我们在生产环境的一些实践案例,遇到的问题及解决方案。

如今,eBay已在内部广泛使用Kubernetes作为容器管理的平台,并自研了AZ和联邦级别的控制平面,用以负责50多个集群的创建、部署、监控、修复等工作。

我们的生产集群上,针对各种应用场景,大量使用了本地存储和网络存储,并通过原生的PV/PVC来使用。其中本地存储分为静态分区类型和基于lvm的动态类型,支持SSD,HDD,NVMe等介质。网络块存储使用Ceph RBD和ISCSI,共享文件存储使用CephFS和NFS。

本地存储

静态分区

我们最早于2016年开始做Local Volume(本地卷),当时社区还没有本地的永久存储方案,为了支持内部的NoSQL应用使用PV(Persistent Volume),开发了第一版的localvolume方案:

首先,在节点创建的时候,Provision系统根据节点池flavor定义对数据盘做分区和格式化,并将盘的信息写入系统配置文件。

同时,我们在集群内部署了daemonset localvolume-provisioner,当节点加入集群后,provisioner会从配置文件中读取配置信息并生成相应的PV,其中包含相应的path,type等信息。这样,每个PV对象也就对应着节点上的一个分区。

除此之外,我们改进了scheduler,将本地 PV/PVC的绑定(binding)延迟到scheduler里进行。这也对应现在社区的volumeScheduling feature。

现在cgroup v1不能很好地支持buffer io的限流和隔离,对于一些io敏感的应用来说,需要尽可能防止“noisy neighbor”干扰,同时对于disk io load很高的应用,应尽可能平均每块盘的负担,延长磁盘寿命。因此,我们增加了PVC的反亲和性(anti affinity)调度特性,在满足节点调度的同时,会尽可能调度到符合反亲和性规则的盘上。具体做法是,在PV中打上标签表明属于哪个节点的哪块盘,在PVC中指定反亲和性规则,如下图一所示。scheduler里增加相应的预选功能,保证声明了同类型的反亲和性的PVC,不会绑定到处在同一块盘的PV上,并在最终调度成功后,完成选定PV/PVC的绑定。
1.png

图 1

LVM动态存储

对于上述静态存储的方案,PV大小是固定的,我们同时希望volume空间能够更灵活地按需申请,即动态分配存储。

类似地,我们在节点flavor里定义一个vg作为存储池,节点创建的时候,provision系统会根据flavor做好分区和vg的创建。同时集群内部署了daemonset local-volume-dynamic-provisioner,实现CSI的相应功能。在CSI 0.4版本中,该daemonset由CSI的四个基本组件组成,即:csi-attacher,csi-provisioner,csi-registrar以及csi-driver。其中csi-attacher,csi-provisioner和csi-registrar为社区的sidecar controller。csi-driver是根据存储后端自己实现CSI接口,目前支持XFS和ext4两种主流文件系统,也支持直接挂载裸盘供用户使用。

为了支持scheduler能够感知到集群的存储拓扑,我们在csi-registrar中把从csi-driver拿到的拓扑信息同步到Kubernetes节点中存储,供scheduler预选时提取,避免了在kubelet中改动代码。如图2所示,Pod创建后,scheduler将根据vg剩余空间选择节点、local-volume-dynamic-provisioner来申请相应大小的lvm logical volume,并创建对应的PV,挂载给Pod使用。
2.png

图 2

网络存储

块存储

对于网络块存储,我们使用Ceph RBD和ISCSI作为存储后端,其中ISCSI为远端SSD,RBD为远端HDD,通过OpenStack的Cinder组件统一管理。

网络存储卷的管理主要包括provision/deletion/attach/detach等,在provision/deletion的时候,相比于Local Volume(本地卷)需要以DaemonSet的方式部署,网络存储只需要一个中心化的provisioner。我们利用了社区的cinder provisioner方案,并加以相应的定制,比如支持利用已有快照卷(snapshot volume)来创建PV,secret统一管理等。

Provisioner的基本思路是:watch PVC创建请求 → 调用cinder api创建相应类型和大小的卷,获得卷ID → 调用cinder的initialize_connection api,获取后端存储卷的具体连接信息和认证信息,映射为对应类型的PV对象 → 往apiserver发请求创建PV → PV controller负责完成PVC和PV的绑定。

Delete为逆过程。

Attach由volume plugin或csi来实现,直接建立每个节点到后端的连接,如RBD map, ISCSI会话连接,并在本地映射为块设备。这个过程是分立到每个节点上的操作,无法在controller manager里实现中心化的attach/detach,因此放到kubelet或csi daemonset来做,而controller manager主要实现逻辑上的accessmode的检查和volume接口的伪操作,通过节点的状态与kubelet实现协同管理。

Detach为逆过程。

在使用RBD的过程中,我们也遇到过一些问题:
  • RBD map hang:RBD map进程hang,然而设备已经map到本地并显示为/dev/rbdX。经分析,发现是RBD client端的代码在执行完attach操作后,会进入顺序等待udevd event的loop,分别为"subsystem=rbd" add event和"subsystem=block" add event。而udevd并不保证遵循kernel uevent的顺序,因此如果"subsystem=block" event先于 "subsystem=rbd" event, RBD client将一直等待下去。通过人为触发add event(udevadm trigger --type=devices --action=add),就可能顺利退出。这个问题后来在社区得到解决,我们反向移植(backport)到所有的生产集群上。(详情可见https://tracker.ceph.com/issues/39089
  • kernel RBD支持的RBD feature非常有限,很多后端存储的特性无法使用。
  • 当节点map了RBD device并被container使用,节点重启会一直hang住,原因是network shutdown先于RBD umount,导致kernel在cleanup_mnt()的时候kRBD连接Ceph集群失败,进程处于D状态。我们改变systemd的配置ShutdownWatchdogSec为1分钟,来避免这个问题。


除了kernel RBD模块,Ceph也支持块存储的用户态librbd实现:rbd-nbd。Kubernetes也支持使用rbd-nbd。如图3所示,我们对kRBD和rbd-nbd做了对比:
3.png

图 3

如上,rbd-nbd在使用上有16个device的限制,同时会耗费更多的CPU资源,综合考虑我们的使用需求,决定继续使用kRBD。

图4为三类块存储的性能比较:
4.png

图 4

文件存储

我们主要使用CephFS作为存储后端,CephFS可以使用kernel mount,也可以使用cephfs-fuse mount,类似于前述kRBD和librbd的区别,前者工作在内核态,后者工作在用户态。经过实际对比,发现性能上fuse mount远不如kernel mount,而另一方面,fuse能更好地支持后端的feature,便于管理。目前社区cephfs plugin里默认使用ceph fuse,为了满足部分应用的读写性能要求,我们提供了pod annotation(注解)选项,应用可自行选择使用哪类mount方式,默认为fuse mount。

下面介绍一下在使用ceph fuse的过程中遇到的一些问题(ceph mimic version 13.2.5, kernel 4.15.0-43):

1、ceph fuse internal type overflow导致mount目录不可访问

ceph fuse设置挂载目录dentry的attr_timeout为0,应用每次访问时kernel都会重新验证该dentry cache是否可用,而每次lookup会对其对应inode的reference count + 1。

经过分析,发现在kernel fuse driver里count是uint_64类型,而ceph-fuse里是int32类型。当反复访问同一路径时,ref count一直增加,如果节点内存足够大,kernel没能及时触发释放 dentry缓存,会导致ceph-fuse里ref count值溢出。针对该问题,临时的解决办法是周期性释放缓存(drop cache),这样每次会生成新的dentry,重新开始计数。同时我们存储的同事也往ceph社区提交补丁,将ceph-fuse中该值改为uint_64类型,同kernel 匹配起来。(详情可见https://tracker.ceph.com/issues/40775

2、kubelet du hang

kubelet会周期性通过du来统计emptydir volume使用情况,我们发现在部分节点上存在大量du进程hang,并且随着时间推移数量越来越多,一方面使系统load增高,另一方面耗尽pid资源,最终导致节点不响应。经分析,du会读取到cephfs路径,而cephfs不可达是导致du hang的根本原因,主要由以下两类问题导致:
  • 网络问题导致mds连接断开,如图5所示,通过ceph admin socket,可以看到存在失效链接(stale connection),原因是client端没有主动去重连,导致所有访问mount路径的操作hang在等待fuse answer上,在节点启用了client_reconnect_stale选项后,得到解决。
    5.png

    图 5
  • mds连接卡在opening状态,同样导致du hang。原因是服务端打开了mds_session_blacklist_on_evict,导致连接出现问题时客户端无法重连。


3、性能:

kernel mount性能远高于fuse性能,经过调试,发现启用了fuse_big_write后,在大块读写的场景下,fuse性能几乎和kernel差不多。

应用场景

本地数据备份还原

本地存储相比网络存储,具有成本低,性能高的优点,但是如果节点失效,将会导致数据丢失,可靠性比网络存储低。

为了保证数据可靠性,应用实现了自己的备份还原机制。使用本地PV存储数据,同时挂载RBD类型的PV,增量传输数据至远端备份集群。同时远端会根据事先定义规则,周期性地在这些RBD盘上打snapshot(快照),在还原的时候,选定特定snapshot,provision出对应PV,并挂载到节点上,恢复到本地PV。

盘加密

对于安全要求级别高的应用,如支付业务,我们使用了kata安全容器方案,同时对kata container的存储进行加密。如图6所示,我们使用了kernel dm-crypt对盘进行加密,并将生成的key对称加密存入eBay的密钥管理服务中,最后给container使用的是解密后的盘,在Pod生命周期结束后,会关闭加密盘,防止数据泄漏。
6.png

图 6

磁盘监控

对于本地存储来说,节点坏盘,丢盘等错误,都会影响到线上应用,需要实时有效的监控手段。我们基于社区的node-problem-detector项目(详情可见https://github.com/Kubernetes/ ... ector),往其中增加了硬盘监控(disk monitor)的功能。主要监控手段有三类:
  1. smart工具检测每块盘的健康状况。
  2. 系统日志中是否有坏盘信息。根据已有的模式(pattern)对日志进行匹配,如图7所示:
    7.png

    图 7
  3. 丢盘检测,对比实际检测到的盘符和节点flavor定义的盘符。


以上检测结果以metrics(指标)的形式被prometheus收集,同时也更新到自定义crd computenode的状态中,由专门的remediation controller(修复控制器)接管,如满足预定义的节点失效策略,将会进入后续修复流程。

对于有问题的盘,monitor会对相应PV标记taint,scheduler里会防止绑定到该类PV,同时对于已绑定的PV,会给绑定到的PVC发event,通知应用。

管理部署

以上提到了几类组件,local-volume-provisioner,local-volume-dynamic-provisioner,cinder-provisioner,node-problem-detector等,我们开发了gitops + salt的方案对其进行统一管理。首先把每个组件作为一个salt state,定义对应的salt state文件和pillar,写入git repo,对于key等敏感信息则存放在secret中。这些manifest文件通过AZ控制面同步到各个集群并执行。我们将所有的组件视为addon,salt会生成最终的yaml定义文件,交由kube addon manager进行apply。在需要更新的时候,只需更新相应的salt文件和pillar值即可。

后续工作

  1. 对于网络存储,将后端控制面由cinder切换到SDS,届时将会对接新的SDS api,实现新的dynamic provision controller和csi插件;
  2. 实现Kubernetes平台上的volume snapshot(卷快照)功能;
  3. 将in-tree 的volume插件全部迁移到CSI, 并将CSI升级到最新版本,方便部署和升级;
  4. 引入cgroup v2, 以实现blkio qos控制;
  5. 实现本地存储的自动扩容能力。


Q&A

Q:分布式数据库例如MongoDB在Kubernetes上有实现方案吗?
A:有的,我们内部NoSQL就是完全运行在容器云上的,Pod部署由应用自己管理,通过svc暴露服务,存储上使用local PV,并实现了backup restore。社区应该也有比较多的实现参考。

Q:由于环境,如网络因素,出现短时间暂时大规模掉Node的情况怎么处理?
A:如网络问题导致Node连不通,对于网络存储来说,需要在网络恢复之后重连,比如cephfs kernel client和fuse都实现了reconnect。

Q:etcd集群中,v2和数据和v3的数据备份方式不一样,如何备份整个etcd数据呢?
A:etcd server只能有一种版本,不会并存,所以按照各自版本的方式备份即可。

Q:PVC的anti affinity调度特性是Kubernetes原生支持的吗?自研方案有计划贡献到Kubernetes仓库吗?
A:不是,我们是在使用MongoDB的过程中,发现master pod的io load很高,所以基于此自己开发了这个功能。

Q:数据如何做容灾?
A:网络存储自己有多replica和rack awareness的分布,本地存储需要应用自己实现多拷贝,对于可靠性要求比较高的数据,需要做备份还原。

Q:本地存储能说的更清楚点么?比如registar是怎么把信息同步到kubernetesnode中的。PV的删除是csi那个组件来做的?信息有哪些信息。
A:registar在注册节点的时候会将vg的相关信息以annotation的方式写到node对象中,pv的删除由csi-provisioner sidecar完成,大体思路可参考社区的design doc。

Q:容器镜像如何存储和管理?
A:我们目前用的是quay,用swift存储镜像层。

Q:Redis集群,3主3从这种,如何跑在Kubernetes上?
A:可以用statefulset的方式,具体可以参考社区的做法。

Q:使用ceph rbd会出现multiattach error,导致新Pod一直处于creating状态,需要人工介入,有无自动处理方案?比如,kubelet挂掉。
A:如出现kubelet挂掉或者node hung导致kubelet不工作,有可能出现这种情况,需要实现节点的remediation,监控这些情况,重启或者下架节点,保证原来的连接断掉。

Q:请问日志存储是在专有的节点吗?如果不是会和业务数据存储产生影响吗?空间占用,CPU,内存方面的影响。
A:每个节点组件本身的日志和容器的日志都是通过beats来收集并上报到监控系统,不会和业务数据冲突或干扰。

Q:存储限制是怎么做的?
A:对于emptydir,我们使用xfs quota限制。对于PV/PVC,我们在controller层面做了每个namespace的quota limit。

Q:ceph rbd和本地磁盘有做过benchmark么?cg v2应该只能限制本地盘吧?
A:有的,如下:
8.png


Q:kernel network storage有没有什么好的学习材料?
A:具体是哪类存储类型,可以参见https://www.oreilly.com/librar ... ivers

Q:有没有可能通过StatfulSet 实现分布式存储?来做异地容灾。
A:异地容灾是federation层面的部署,感觉和用哪类workload api没太大关系。

Q:本地存储不需要另外的scheduler-extender么?用原有的scheduler就可以了?
A:我们是直接在原有的scheduler基础上做了修改,当时还没有extender机制,后续会考虑以extend方式放到外部。

以上内容根据2019年8月27日晚微信群分享内容整理。 分享人高文俊,eBay中国研发中心云计算工程师,负责Kubernetes平台的研发,内部host-runtime sig成员,主要负责存储,集群管理等工作。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiese,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。
继续阅读 »

【编者的话】存储是云平台最核心最基本的功能之一,如何在Kubernetes容器平台上对接不同的后端类型,并根据实际需求做选择和定制,会遇到哪些问题,怎么管理?本次分享将介绍eBay存储的实现,使用方式,并介绍我们在生产环境的一些实践案例,遇到的问题及解决方案。

如今,eBay已在内部广泛使用Kubernetes作为容器管理的平台,并自研了AZ和联邦级别的控制平面,用以负责50多个集群的创建、部署、监控、修复等工作。

我们的生产集群上,针对各种应用场景,大量使用了本地存储和网络存储,并通过原生的PV/PVC来使用。其中本地存储分为静态分区类型和基于lvm的动态类型,支持SSD,HDD,NVMe等介质。网络块存储使用Ceph RBD和ISCSI,共享文件存储使用CephFS和NFS。

本地存储

静态分区

我们最早于2016年开始做Local Volume(本地卷),当时社区还没有本地的永久存储方案,为了支持内部的NoSQL应用使用PV(Persistent Volume),开发了第一版的localvolume方案:

首先,在节点创建的时候,Provision系统根据节点池flavor定义对数据盘做分区和格式化,并将盘的信息写入系统配置文件。

同时,我们在集群内部署了daemonset localvolume-provisioner,当节点加入集群后,provisioner会从配置文件中读取配置信息并生成相应的PV,其中包含相应的path,type等信息。这样,每个PV对象也就对应着节点上的一个分区。

除此之外,我们改进了scheduler,将本地 PV/PVC的绑定(binding)延迟到scheduler里进行。这也对应现在社区的volumeScheduling feature。

现在cgroup v1不能很好地支持buffer io的限流和隔离,对于一些io敏感的应用来说,需要尽可能防止“noisy neighbor”干扰,同时对于disk io load很高的应用,应尽可能平均每块盘的负担,延长磁盘寿命。因此,我们增加了PVC的反亲和性(anti affinity)调度特性,在满足节点调度的同时,会尽可能调度到符合反亲和性规则的盘上。具体做法是,在PV中打上标签表明属于哪个节点的哪块盘,在PVC中指定反亲和性规则,如下图一所示。scheduler里增加相应的预选功能,保证声明了同类型的反亲和性的PVC,不会绑定到处在同一块盘的PV上,并在最终调度成功后,完成选定PV/PVC的绑定。
1.png

图 1

LVM动态存储

对于上述静态存储的方案,PV大小是固定的,我们同时希望volume空间能够更灵活地按需申请,即动态分配存储。

类似地,我们在节点flavor里定义一个vg作为存储池,节点创建的时候,provision系统会根据flavor做好分区和vg的创建。同时集群内部署了daemonset local-volume-dynamic-provisioner,实现CSI的相应功能。在CSI 0.4版本中,该daemonset由CSI的四个基本组件组成,即:csi-attacher,csi-provisioner,csi-registrar以及csi-driver。其中csi-attacher,csi-provisioner和csi-registrar为社区的sidecar controller。csi-driver是根据存储后端自己实现CSI接口,目前支持XFS和ext4两种主流文件系统,也支持直接挂载裸盘供用户使用。

为了支持scheduler能够感知到集群的存储拓扑,我们在csi-registrar中把从csi-driver拿到的拓扑信息同步到Kubernetes节点中存储,供scheduler预选时提取,避免了在kubelet中改动代码。如图2所示,Pod创建后,scheduler将根据vg剩余空间选择节点、local-volume-dynamic-provisioner来申请相应大小的lvm logical volume,并创建对应的PV,挂载给Pod使用。
2.png

图 2

网络存储

块存储

对于网络块存储,我们使用Ceph RBD和ISCSI作为存储后端,其中ISCSI为远端SSD,RBD为远端HDD,通过OpenStack的Cinder组件统一管理。

网络存储卷的管理主要包括provision/deletion/attach/detach等,在provision/deletion的时候,相比于Local Volume(本地卷)需要以DaemonSet的方式部署,网络存储只需要一个中心化的provisioner。我们利用了社区的cinder provisioner方案,并加以相应的定制,比如支持利用已有快照卷(snapshot volume)来创建PV,secret统一管理等。

Provisioner的基本思路是:watch PVC创建请求 → 调用cinder api创建相应类型和大小的卷,获得卷ID → 调用cinder的initialize_connection api,获取后端存储卷的具体连接信息和认证信息,映射为对应类型的PV对象 → 往apiserver发请求创建PV → PV controller负责完成PVC和PV的绑定。

Delete为逆过程。

Attach由volume plugin或csi来实现,直接建立每个节点到后端的连接,如RBD map, ISCSI会话连接,并在本地映射为块设备。这个过程是分立到每个节点上的操作,无法在controller manager里实现中心化的attach/detach,因此放到kubelet或csi daemonset来做,而controller manager主要实现逻辑上的accessmode的检查和volume接口的伪操作,通过节点的状态与kubelet实现协同管理。

Detach为逆过程。

在使用RBD的过程中,我们也遇到过一些问题:
  • RBD map hang:RBD map进程hang,然而设备已经map到本地并显示为/dev/rbdX。经分析,发现是RBD client端的代码在执行完attach操作后,会进入顺序等待udevd event的loop,分别为"subsystem=rbd" add event和"subsystem=block" add event。而udevd并不保证遵循kernel uevent的顺序,因此如果"subsystem=block" event先于 "subsystem=rbd" event, RBD client将一直等待下去。通过人为触发add event(udevadm trigger --type=devices --action=add),就可能顺利退出。这个问题后来在社区得到解决,我们反向移植(backport)到所有的生产集群上。(详情可见https://tracker.ceph.com/issues/39089
  • kernel RBD支持的RBD feature非常有限,很多后端存储的特性无法使用。
  • 当节点map了RBD device并被container使用,节点重启会一直hang住,原因是network shutdown先于RBD umount,导致kernel在cleanup_mnt()的时候kRBD连接Ceph集群失败,进程处于D状态。我们改变systemd的配置ShutdownWatchdogSec为1分钟,来避免这个问题。


除了kernel RBD模块,Ceph也支持块存储的用户态librbd实现:rbd-nbd。Kubernetes也支持使用rbd-nbd。如图3所示,我们对kRBD和rbd-nbd做了对比:
3.png

图 3

如上,rbd-nbd在使用上有16个device的限制,同时会耗费更多的CPU资源,综合考虑我们的使用需求,决定继续使用kRBD。

图4为三类块存储的性能比较:
4.png

图 4

文件存储

我们主要使用CephFS作为存储后端,CephFS可以使用kernel mount,也可以使用cephfs-fuse mount,类似于前述kRBD和librbd的区别,前者工作在内核态,后者工作在用户态。经过实际对比,发现性能上fuse mount远不如kernel mount,而另一方面,fuse能更好地支持后端的feature,便于管理。目前社区cephfs plugin里默认使用ceph fuse,为了满足部分应用的读写性能要求,我们提供了pod annotation(注解)选项,应用可自行选择使用哪类mount方式,默认为fuse mount。

下面介绍一下在使用ceph fuse的过程中遇到的一些问题(ceph mimic version 13.2.5, kernel 4.15.0-43):

1、ceph fuse internal type overflow导致mount目录不可访问

ceph fuse设置挂载目录dentry的attr_timeout为0,应用每次访问时kernel都会重新验证该dentry cache是否可用,而每次lookup会对其对应inode的reference count + 1。

经过分析,发现在kernel fuse driver里count是uint_64类型,而ceph-fuse里是int32类型。当反复访问同一路径时,ref count一直增加,如果节点内存足够大,kernel没能及时触发释放 dentry缓存,会导致ceph-fuse里ref count值溢出。针对该问题,临时的解决办法是周期性释放缓存(drop cache),这样每次会生成新的dentry,重新开始计数。同时我们存储的同事也往ceph社区提交补丁,将ceph-fuse中该值改为uint_64类型,同kernel 匹配起来。(详情可见https://tracker.ceph.com/issues/40775

2、kubelet du hang

kubelet会周期性通过du来统计emptydir volume使用情况,我们发现在部分节点上存在大量du进程hang,并且随着时间推移数量越来越多,一方面使系统load增高,另一方面耗尽pid资源,最终导致节点不响应。经分析,du会读取到cephfs路径,而cephfs不可达是导致du hang的根本原因,主要由以下两类问题导致:
  • 网络问题导致mds连接断开,如图5所示,通过ceph admin socket,可以看到存在失效链接(stale connection),原因是client端没有主动去重连,导致所有访问mount路径的操作hang在等待fuse answer上,在节点启用了client_reconnect_stale选项后,得到解决。
    5.png

    图 5
  • mds连接卡在opening状态,同样导致du hang。原因是服务端打开了mds_session_blacklist_on_evict,导致连接出现问题时客户端无法重连。


3、性能:

kernel mount性能远高于fuse性能,经过调试,发现启用了fuse_big_write后,在大块读写的场景下,fuse性能几乎和kernel差不多。

应用场景

本地数据备份还原

本地存储相比网络存储,具有成本低,性能高的优点,但是如果节点失效,将会导致数据丢失,可靠性比网络存储低。

为了保证数据可靠性,应用实现了自己的备份还原机制。使用本地PV存储数据,同时挂载RBD类型的PV,增量传输数据至远端备份集群。同时远端会根据事先定义规则,周期性地在这些RBD盘上打snapshot(快照),在还原的时候,选定特定snapshot,provision出对应PV,并挂载到节点上,恢复到本地PV。

盘加密

对于安全要求级别高的应用,如支付业务,我们使用了kata安全容器方案,同时对kata container的存储进行加密。如图6所示,我们使用了kernel dm-crypt对盘进行加密,并将生成的key对称加密存入eBay的密钥管理服务中,最后给container使用的是解密后的盘,在Pod生命周期结束后,会关闭加密盘,防止数据泄漏。
6.png

图 6

磁盘监控

对于本地存储来说,节点坏盘,丢盘等错误,都会影响到线上应用,需要实时有效的监控手段。我们基于社区的node-problem-detector项目(详情可见https://github.com/Kubernetes/ ... ector),往其中增加了硬盘监控(disk monitor)的功能。主要监控手段有三类:
  1. smart工具检测每块盘的健康状况。
  2. 系统日志中是否有坏盘信息。根据已有的模式(pattern)对日志进行匹配,如图7所示:
    7.png

    图 7
  3. 丢盘检测,对比实际检测到的盘符和节点flavor定义的盘符。


以上检测结果以metrics(指标)的形式被prometheus收集,同时也更新到自定义crd computenode的状态中,由专门的remediation controller(修复控制器)接管,如满足预定义的节点失效策略,将会进入后续修复流程。

对于有问题的盘,monitor会对相应PV标记taint,scheduler里会防止绑定到该类PV,同时对于已绑定的PV,会给绑定到的PVC发event,通知应用。

管理部署

以上提到了几类组件,local-volume-provisioner,local-volume-dynamic-provisioner,cinder-provisioner,node-problem-detector等,我们开发了gitops + salt的方案对其进行统一管理。首先把每个组件作为一个salt state,定义对应的salt state文件和pillar,写入git repo,对于key等敏感信息则存放在secret中。这些manifest文件通过AZ控制面同步到各个集群并执行。我们将所有的组件视为addon,salt会生成最终的yaml定义文件,交由kube addon manager进行apply。在需要更新的时候,只需更新相应的salt文件和pillar值即可。

后续工作

  1. 对于网络存储,将后端控制面由cinder切换到SDS,届时将会对接新的SDS api,实现新的dynamic provision controller和csi插件;
  2. 实现Kubernetes平台上的volume snapshot(卷快照)功能;
  3. 将in-tree 的volume插件全部迁移到CSI, 并将CSI升级到最新版本,方便部署和升级;
  4. 引入cgroup v2, 以实现blkio qos控制;
  5. 实现本地存储的自动扩容能力。


Q&A

Q:分布式数据库例如MongoDB在Kubernetes上有实现方案吗?
A:有的,我们内部NoSQL就是完全运行在容器云上的,Pod部署由应用自己管理,通过svc暴露服务,存储上使用local PV,并实现了backup restore。社区应该也有比较多的实现参考。

Q:由于环境,如网络因素,出现短时间暂时大规模掉Node的情况怎么处理?
A:如网络问题导致Node连不通,对于网络存储来说,需要在网络恢复之后重连,比如cephfs kernel client和fuse都实现了reconnect。

Q:etcd集群中,v2和数据和v3的数据备份方式不一样,如何备份整个etcd数据呢?
A:etcd server只能有一种版本,不会并存,所以按照各自版本的方式备份即可。

Q:PVC的anti affinity调度特性是Kubernetes原生支持的吗?自研方案有计划贡献到Kubernetes仓库吗?
A:不是,我们是在使用MongoDB的过程中,发现master pod的io load很高,所以基于此自己开发了这个功能。

Q:数据如何做容灾?
A:网络存储自己有多replica和rack awareness的分布,本地存储需要应用自己实现多拷贝,对于可靠性要求比较高的数据,需要做备份还原。

Q:本地存储能说的更清楚点么?比如registar是怎么把信息同步到kubernetesnode中的。PV的删除是csi那个组件来做的?信息有哪些信息。
A:registar在注册节点的时候会将vg的相关信息以annotation的方式写到node对象中,pv的删除由csi-provisioner sidecar完成,大体思路可参考社区的design doc。

Q:容器镜像如何存储和管理?
A:我们目前用的是quay,用swift存储镜像层。

Q:Redis集群,3主3从这种,如何跑在Kubernetes上?
A:可以用statefulset的方式,具体可以参考社区的做法。

Q:使用ceph rbd会出现multiattach error,导致新Pod一直处于creating状态,需要人工介入,有无自动处理方案?比如,kubelet挂掉。
A:如出现kubelet挂掉或者node hung导致kubelet不工作,有可能出现这种情况,需要实现节点的remediation,监控这些情况,重启或者下架节点,保证原来的连接断掉。

Q:请问日志存储是在专有的节点吗?如果不是会和业务数据存储产生影响吗?空间占用,CPU,内存方面的影响。
A:每个节点组件本身的日志和容器的日志都是通过beats来收集并上报到监控系统,不会和业务数据冲突或干扰。

Q:存储限制是怎么做的?
A:对于emptydir,我们使用xfs quota限制。对于PV/PVC,我们在controller层面做了每个namespace的quota limit。

Q:ceph rbd和本地磁盘有做过benchmark么?cg v2应该只能限制本地盘吧?
A:有的,如下:
8.png


Q:kernel network storage有没有什么好的学习材料?
A:具体是哪类存储类型,可以参见https://www.oreilly.com/librar ... ivers

Q:有没有可能通过StatfulSet 实现分布式存储?来做异地容灾。
A:异地容灾是federation层面的部署,感觉和用哪类workload api没太大关系。

Q:本地存储不需要另外的scheduler-extender么?用原有的scheduler就可以了?
A:我们是直接在原有的scheduler基础上做了修改,当时还没有extender机制,后续会考虑以extend方式放到外部。

以上内容根据2019年8月27日晚微信群分享内容整理。 分享人高文俊,eBay中国研发中心云计算工程师,负责Kubernetes平台的研发,内部host-runtime sig成员,主要负责存储,集群管理等工作。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiese,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。 收起阅读 »

优化Kubernetes集群负载的技术方案探讨


【编者的话】Kubernetes的资源编排调度使用的是静态调度,将Pod Request Resource与Node Allocatable Resource进行比较来决定Node是否有足够资源容纳该Pod。静态调度带来的问题是,集群资源很快被业务容器分配完,但是集群的整体负载非常低,各个节点的负载也不均衡。本文将介绍优化Kubernetes集群负载的多种技术方案。

Kubernetes为什么使用静态调度

静态调度,是指根据容器请求的资源进行装箱调度,而不考虑节点的实际负载。静态调度最大的优点就是调度简单高效、集群资源管理方便,最大的缺点也很明显,就是不管节点实际负载,极容易导致集群负载不高。

Kubernetes为什么会使用静态调度呢?因为要做好通用的动态调度几乎是不可能的,对,是通用的动态调度很难都满足不同企业不同业务的诉求,结果可能适得其反。那是不是我们就没必要去往动态调度做技术尝试呢?未必!平台根据托管的业务属性,可以适当的通过scheduler extender的方式扩展Kubernetes Scheduler来做一定权重的动态调度决策。

集群资源构成

以CPU资源为例,一个大规模Kubernetes集群的资源组成结构大致如下:
1.png

由以下几部分组成:
  • 每个节点的预留资源,对应kubelet的system-reserved, kube-reserved, eviction-hard配置的资源之和,Kubernetes计算Node的Allocatable资源时会减去这部分预留资源。
  • 目前我们集群的平均资源碎片大概在5%~10%左右,根据不同规格的CVM机型略有不同。这些资源碎片分散在集群中的各个节点,以1c1g, 2c2g, 3cxg为主,平台提供用户选择的容器规格都很难match到这些碎片,经常存在这这种情况:要调度某个Pod时,发现某个节点上的cpu足够,但是mem不足,或者相反。
  • 剩下的就是可以被业务Pod真正分配使用的资源了,业务在选择容器规格时带有一定的主观性和盲目性,导致业务容器的负载很低,这样的业务占比一大就容易导致集群低负载的情况,但是集群按照Kubernetes静态调度策略又无法再容纳更多的业务容器了。如上图中所示的,集群分配CPU水位线很高,但是实际CPU利用率不高的情况。


提升集群负载的方案

除了借助强大的容器监控数据做一定权重的动态调度决策之外,是否还有其他方案可以用于解决静态调度带来的集群低负载问题呢?下面我将给出一整套技术方案,从多个技术维度来尝试提升Kubernetes集群负载。
2.png

Pod分配资源压缩

前面提到,研发同学部署业务选择容器资源规格时,带有一定的盲目性,而且Kubernetes原生也不支持实时无感知的修改容器规格(虽然这可以通过Static VPA方案解决),导致业务容器负载低。为了解决这个问题,我们可以给Pod Request Resource做一定比例的压缩(Pod Limit Resource不压缩)。注意压缩Pod Request Resource只发生在Pod创建或者重建的时候,比如业务做变更发布之时,对于正常运行中的Pod不能做这一动作,否则可能导致对应Workload Controller重建Pod(取决于Workload的UpdateStrategy)对业务带来影响。

需要注意的是:
  • 每个Workload负载变动规律不同,因此Pod分配资源压缩比例也对应不一样,需要支持每个Workload自定义配置,而且这是对用户无感知的。这个压缩比,我们设置到Workload的Annotation中,比如cpu资源压缩对应Annotation stke.platform/cpu-requests-ratio;
  • 压缩比,谁去设置?自研组件(Pod-Resource-Compress-Ratio-Reconciler)基于Workload的历史监控数据,动态的/周期性去调整压缩比。比如某Workload连续7d/1M的负载持续很低,那么可以把压缩比设置的更大,以此让集群剩余可分配资源更大,容纳更多的业务容器。当然实际上压缩比的调整策略并不会这么简单,需要更多的监控数据来辅助。
  • Pod分配压缩特性一定要是可以关闭的和恢复的,通过Workload Annotation stke.platform/enable-resource-compress: "n"针对Workload级别disable,通过设置压缩比为1进行压缩恢复。
  • 何时通过压缩比去调整Pod Spec中的Request Resource?Kubernetes发展到现阶段,直接改Kubernetes代码是最愚蠢的方式,我们要充分利用Kubernetes的扩展方式。这里,我们通过kube-apiserver的Mutating Admission Webhook对Pod的Create事件进行拦截,自研webhook(pod-resource-compress-webhook)检查Pod Annotations中是否enable了压缩特性,并且配置了压缩比,如果配置了,则根据压缩比重新计算该Pod的Request Resource,Patch到APIServer。


3.png

Node资源超卖

Pod资源压缩方案,是针对每个Workload级别的资源动态调整方案,优点是细化到每个Workload,能做到有的放矢,缺点是业务不做变更发布,就没有效果,见效慢。

Node资源超卖方案是针对Node级别的资源动态调整方案,根据每个节点的真实历史负载数据,进行不同比例的资源超卖。
  • 每个节点的资源超卖比例,我们设置到Node的Annotation中,比如cpu超卖对应Annotation stke.platform/cpu-oversale-ratio。
  • 每个节点的超卖比例,谁去设置?自研组件(Node-Resource-Oversale-Ratio-Reconciler)基于节点历史监控数据,动态的/周期性的去调整超卖比例。比如某个Node连续7d/1M持续低负载并且节点已分配资源水位线很高了,那么可以把超卖比例适当调高,以此使Node能容纳更多的业务Pod。
  • Node超卖特性一定要是可以关闭和还原的,通过Node Annotation stke.platform/mutate: "false"关闭Node超卖,Node在下一个心跳会完成资源复原。
  • 何时通过压缩比去调整Node Status中的Allocatable&Capacity Resource?同样的,我们通过kube-apiserver的Mutating Admission Webhook对Node的Create和Status Update事件进行拦截,自研webhook(node-resource-oversale-webhook)检查Node Annotations中是否enable了超卖并且配置了超卖比,如果配置了,则根据安超卖比重新计算该Node的Allocatable&Capacity Resource,Patch到APIServer。


Node资源超卖,表面上看起来很简单,但实际上要考虑的细节还很多:
  • Kubelet Register Node To ApiServer的详细原理是什么,通过webhook直接Patch Node Status是否可行?
  • 当节点资源超卖后,Kubernetes对应的Cgroup动态调整机制是否能继续正常工作?
  • Node status更新太频繁,每次status update都会触发webhook,大规模集群容易对apiserver造成性能问题,怎么解决?
  • 节点资源超卖对Kubelet Eviction的配置是否也有超配效果,还是仍然按照实际Node配置和负载进行evict? 如果对Evict有影响,又该如解决?
  • 超卖比例从大往小调低时,存在节点上 Sum(pods' request resource) > node's allocatable情况出现,这里是否有风险,该如何处理?
  • 监控系统对Node的监控与Node Allocatable&Capacity Resource有关,超卖后,意味着监控系统对Node的监控不再正确,需要做一定程度的修正,如何让监控系统也能动态的感知超卖比例进行数据和视图的修正?
  • Node Allocatable和Capacity分别该如何超卖?超卖对节点预留资源的影响是如何的?


这里涉及的Kubernetes技术细节比较多,我将在下一篇文章中详细介绍。
4.png

优化AutoScale能力

提起Kubernetes的弹性伸缩,大家比较熟悉的是HPA和HNA,一个对Workload的Pod进行横向伸缩,一个对集群中Node进行横向伸缩。社区中还有一个VPA项目,用来对Pod的资源进行调整,但是需要重建Pod才能生效,VPA存在的意义就是要快速扩容,如果像HPA一样,需要重建Pod启动应用来扩容,其实已经失去了价值。

Kube-controller-manager内置的HPA-Controller存在以下问题:
  • 性能问题:一个goroutine中循环遍历集群中所有的HPA对象,针对每个HPA对象获取对应的Pod监控数据、计算新Replicas,这对于大业务是比较耗时的。
  • 核心配置不支持Workload自定义:HPA伸缩响应时间是每个业务都可能不一样的,有些业务期望能5s进行响应,有些业务觉得60s就够了。而内置HPA Controller在响应时间控制上只能配置全局的启动参数horizontal-pod-autoscaler-sync-period。还有每个业务对负载的抖动容忍是不一样的,在内置的HPA Controller中只能通过horizontal-pod-autoscaler-tolerance做全局配置,无法提供业务级的自定义。
  • Kubernetes目前对custom metrics的支持,只能注册一个后端监控服务,如果集群中有些业务通过prometheus来expose应用自定义指标,也有一些业务通过Monitor来监控应用自定义指标,这个时候就做不到All in了,这在for自研上云的场景中,是一定存在的场景。


我们自研了HPAPlus-Controller组件:
  • 每个HPA对象会启动一个goroutine协程专门负责该HPA对象的管理和计算工作,各个协程并行执行,极大的优化了性能。HPAPlus-Controller独立部署,其资源需求可以是集群规模和HPA数量进行合理调整,相比于原内置HPA-Controller有更大的灵活性。
  • HPAPlus-Controller支持各个HPA对象自定义伸缩响应时间,支持自动感应业务是否在变更发布并决定是否要禁用HPA(某些业务有这样的需求:升级时禁止触发弹性伸缩),支持基于pod resource limit为基数进行Pod资源利用率计算,从而推导出扩缩容后的期望replicas,这一点对于节点超卖和Pod资源压缩后的集群非常重要。
  • 支持业务级别对负载的抖动容忍度的个性化配置。
  • 支持基于更多维度的监控数据进行Scale决策,比如Pod历史7d/1M的CPU负载。
  • 支持CronHPA,满足规律性扩缩容的业务诉求。
  • 通过Extension APIServer的方式对接公司Monitor监控,保留Prometheus-Adaptor的方式来支持基于Prometheus的应用监控,满足基于多种应用监控系统的custom metrics进行HPA。



注意:HPAPlus-Controller与Kubernetes buit-in HPA-Controller存在功能冲突,上线前需要disable kube-controller-manager的HPA-Controller控制器。
5.png

除了HPA的优化和增强之外,我们也在进行Dynamic VPA技术研发,后续再单独文章介绍。

其他技术方案

另外,通过scheduler extender的方式开发动态调度器、基于业务级的配额动态管理组件、基于业务优先级和配额管理的在线离线业务混部能力、主动探测节点资源碎片信息并上报到控制器进行Pod的再漂移进行资源碎片管理等方案,也是我们正在进行实践的方向,对应方案及实现复杂度更高,后续再单独文章介绍。

总结

本文介绍了Kubernetes静态调度带来的集群资源分配水位线高但集群实际负载低的问题进行了技术方案上的探讨,详细介绍了Pod资源动态压缩、节点资源动态超卖、优化AutoScale的能力的技术方案,后面会再对动态调度、动态业务配额管理、在线离线业务混部方案进行介绍。所有这些集群负载提升方案,要做到动态,都强依赖于强大的容器监控系统。我们正与腾讯云监控产品团队深入合作,更好的服务于腾讯自研业务上云。

原文链接:https://cloud.tencent.com/deve ... 05214
继续阅读 »

【编者的话】Kubernetes的资源编排调度使用的是静态调度,将Pod Request Resource与Node Allocatable Resource进行比较来决定Node是否有足够资源容纳该Pod。静态调度带来的问题是,集群资源很快被业务容器分配完,但是集群的整体负载非常低,各个节点的负载也不均衡。本文将介绍优化Kubernetes集群负载的多种技术方案。

Kubernetes为什么使用静态调度

静态调度,是指根据容器请求的资源进行装箱调度,而不考虑节点的实际负载。静态调度最大的优点就是调度简单高效、集群资源管理方便,最大的缺点也很明显,就是不管节点实际负载,极容易导致集群负载不高。

Kubernetes为什么会使用静态调度呢?因为要做好通用的动态调度几乎是不可能的,对,是通用的动态调度很难都满足不同企业不同业务的诉求,结果可能适得其反。那是不是我们就没必要去往动态调度做技术尝试呢?未必!平台根据托管的业务属性,可以适当的通过scheduler extender的方式扩展Kubernetes Scheduler来做一定权重的动态调度决策。

集群资源构成

以CPU资源为例,一个大规模Kubernetes集群的资源组成结构大致如下:
1.png

由以下几部分组成:
  • 每个节点的预留资源,对应kubelet的system-reserved, kube-reserved, eviction-hard配置的资源之和,Kubernetes计算Node的Allocatable资源时会减去这部分预留资源。
  • 目前我们集群的平均资源碎片大概在5%~10%左右,根据不同规格的CVM机型略有不同。这些资源碎片分散在集群中的各个节点,以1c1g, 2c2g, 3cxg为主,平台提供用户选择的容器规格都很难match到这些碎片,经常存在这这种情况:要调度某个Pod时,发现某个节点上的cpu足够,但是mem不足,或者相反。
  • 剩下的就是可以被业务Pod真正分配使用的资源了,业务在选择容器规格时带有一定的主观性和盲目性,导致业务容器的负载很低,这样的业务占比一大就容易导致集群低负载的情况,但是集群按照Kubernetes静态调度策略又无法再容纳更多的业务容器了。如上图中所示的,集群分配CPU水位线很高,但是实际CPU利用率不高的情况。


提升集群负载的方案

除了借助强大的容器监控数据做一定权重的动态调度决策之外,是否还有其他方案可以用于解决静态调度带来的集群低负载问题呢?下面我将给出一整套技术方案,从多个技术维度来尝试提升Kubernetes集群负载。
2.png

Pod分配资源压缩

前面提到,研发同学部署业务选择容器资源规格时,带有一定的盲目性,而且Kubernetes原生也不支持实时无感知的修改容器规格(虽然这可以通过Static VPA方案解决),导致业务容器负载低。为了解决这个问题,我们可以给Pod Request Resource做一定比例的压缩(Pod Limit Resource不压缩)。注意压缩Pod Request Resource只发生在Pod创建或者重建的时候,比如业务做变更发布之时,对于正常运行中的Pod不能做这一动作,否则可能导致对应Workload Controller重建Pod(取决于Workload的UpdateStrategy)对业务带来影响。

需要注意的是:
  • 每个Workload负载变动规律不同,因此Pod分配资源压缩比例也对应不一样,需要支持每个Workload自定义配置,而且这是对用户无感知的。这个压缩比,我们设置到Workload的Annotation中,比如cpu资源压缩对应Annotation stke.platform/cpu-requests-ratio;
  • 压缩比,谁去设置?自研组件(Pod-Resource-Compress-Ratio-Reconciler)基于Workload的历史监控数据,动态的/周期性去调整压缩比。比如某Workload连续7d/1M的负载持续很低,那么可以把压缩比设置的更大,以此让集群剩余可分配资源更大,容纳更多的业务容器。当然实际上压缩比的调整策略并不会这么简单,需要更多的监控数据来辅助。
  • Pod分配压缩特性一定要是可以关闭的和恢复的,通过Workload Annotation stke.platform/enable-resource-compress: "n"针对Workload级别disable,通过设置压缩比为1进行压缩恢复。
  • 何时通过压缩比去调整Pod Spec中的Request Resource?Kubernetes发展到现阶段,直接改Kubernetes代码是最愚蠢的方式,我们要充分利用Kubernetes的扩展方式。这里,我们通过kube-apiserver的Mutating Admission Webhook对Pod的Create事件进行拦截,自研webhook(pod-resource-compress-webhook)检查Pod Annotations中是否enable了压缩特性,并且配置了压缩比,如果配置了,则根据压缩比重新计算该Pod的Request Resource,Patch到APIServer。


3.png

Node资源超卖

Pod资源压缩方案,是针对每个Workload级别的资源动态调整方案,优点是细化到每个Workload,能做到有的放矢,缺点是业务不做变更发布,就没有效果,见效慢。

Node资源超卖方案是针对Node级别的资源动态调整方案,根据每个节点的真实历史负载数据,进行不同比例的资源超卖。
  • 每个节点的资源超卖比例,我们设置到Node的Annotation中,比如cpu超卖对应Annotation stke.platform/cpu-oversale-ratio。
  • 每个节点的超卖比例,谁去设置?自研组件(Node-Resource-Oversale-Ratio-Reconciler)基于节点历史监控数据,动态的/周期性的去调整超卖比例。比如某个Node连续7d/1M持续低负载并且节点已分配资源水位线很高了,那么可以把超卖比例适当调高,以此使Node能容纳更多的业务Pod。
  • Node超卖特性一定要是可以关闭和还原的,通过Node Annotation stke.platform/mutate: "false"关闭Node超卖,Node在下一个心跳会完成资源复原。
  • 何时通过压缩比去调整Node Status中的Allocatable&Capacity Resource?同样的,我们通过kube-apiserver的Mutating Admission Webhook对Node的Create和Status Update事件进行拦截,自研webhook(node-resource-oversale-webhook)检查Node Annotations中是否enable了超卖并且配置了超卖比,如果配置了,则根据安超卖比重新计算该Node的Allocatable&Capacity Resource,Patch到APIServer。


Node资源超卖,表面上看起来很简单,但实际上要考虑的细节还很多:
  • Kubelet Register Node To ApiServer的详细原理是什么,通过webhook直接Patch Node Status是否可行?
  • 当节点资源超卖后,Kubernetes对应的Cgroup动态调整机制是否能继续正常工作?
  • Node status更新太频繁,每次status update都会触发webhook,大规模集群容易对apiserver造成性能问题,怎么解决?
  • 节点资源超卖对Kubelet Eviction的配置是否也有超配效果,还是仍然按照实际Node配置和负载进行evict? 如果对Evict有影响,又该如解决?
  • 超卖比例从大往小调低时,存在节点上 Sum(pods' request resource) > node's allocatable情况出现,这里是否有风险,该如何处理?
  • 监控系统对Node的监控与Node Allocatable&Capacity Resource有关,超卖后,意味着监控系统对Node的监控不再正确,需要做一定程度的修正,如何让监控系统也能动态的感知超卖比例进行数据和视图的修正?
  • Node Allocatable和Capacity分别该如何超卖?超卖对节点预留资源的影响是如何的?


这里涉及的Kubernetes技术细节比较多,我将在下一篇文章中详细介绍。
4.png

优化AutoScale能力

提起Kubernetes的弹性伸缩,大家比较熟悉的是HPA和HNA,一个对Workload的Pod进行横向伸缩,一个对集群中Node进行横向伸缩。社区中还有一个VPA项目,用来对Pod的资源进行调整,但是需要重建Pod才能生效,VPA存在的意义就是要快速扩容,如果像HPA一样,需要重建Pod启动应用来扩容,其实已经失去了价值。

Kube-controller-manager内置的HPA-Controller存在以下问题:
  • 性能问题:一个goroutine中循环遍历集群中所有的HPA对象,针对每个HPA对象获取对应的Pod监控数据、计算新Replicas,这对于大业务是比较耗时的。
  • 核心配置不支持Workload自定义:HPA伸缩响应时间是每个业务都可能不一样的,有些业务期望能5s进行响应,有些业务觉得60s就够了。而内置HPA Controller在响应时间控制上只能配置全局的启动参数horizontal-pod-autoscaler-sync-period。还有每个业务对负载的抖动容忍是不一样的,在内置的HPA Controller中只能通过horizontal-pod-autoscaler-tolerance做全局配置,无法提供业务级的自定义。
  • Kubernetes目前对custom metrics的支持,只能注册一个后端监控服务,如果集群中有些业务通过prometheus来expose应用自定义指标,也有一些业务通过Monitor来监控应用自定义指标,这个时候就做不到All in了,这在for自研上云的场景中,是一定存在的场景。


我们自研了HPAPlus-Controller组件:
  • 每个HPA对象会启动一个goroutine协程专门负责该HPA对象的管理和计算工作,各个协程并行执行,极大的优化了性能。HPAPlus-Controller独立部署,其资源需求可以是集群规模和HPA数量进行合理调整,相比于原内置HPA-Controller有更大的灵活性。
  • HPAPlus-Controller支持各个HPA对象自定义伸缩响应时间,支持自动感应业务是否在变更发布并决定是否要禁用HPA(某些业务有这样的需求:升级时禁止触发弹性伸缩),支持基于pod resource limit为基数进行Pod资源利用率计算,从而推导出扩缩容后的期望replicas,这一点对于节点超卖和Pod资源压缩后的集群非常重要。
  • 支持业务级别对负载的抖动容忍度的个性化配置。
  • 支持基于更多维度的监控数据进行Scale决策,比如Pod历史7d/1M的CPU负载。
  • 支持CronHPA,满足规律性扩缩容的业务诉求。
  • 通过Extension APIServer的方式对接公司Monitor监控,保留Prometheus-Adaptor的方式来支持基于Prometheus的应用监控,满足基于多种应用监控系统的custom metrics进行HPA。



注意:HPAPlus-Controller与Kubernetes buit-in HPA-Controller存在功能冲突,上线前需要disable kube-controller-manager的HPA-Controller控制器。
5.png

除了HPA的优化和增强之外,我们也在进行Dynamic VPA技术研发,后续再单独文章介绍。

其他技术方案

另外,通过scheduler extender的方式开发动态调度器、基于业务级的配额动态管理组件、基于业务优先级和配额管理的在线离线业务混部能力、主动探测节点资源碎片信息并上报到控制器进行Pod的再漂移进行资源碎片管理等方案,也是我们正在进行实践的方向,对应方案及实现复杂度更高,后续再单独文章介绍。

总结

本文介绍了Kubernetes静态调度带来的集群资源分配水位线高但集群实际负载低的问题进行了技术方案上的探讨,详细介绍了Pod资源动态压缩、节点资源动态超卖、优化AutoScale的能力的技术方案,后面会再对动态调度、动态业务配额管理、在线离线业务混部方案进行介绍。所有这些集群负载提升方案,要做到动态,都强依赖于强大的容器监控系统。我们正与腾讯云监控产品团队深入合作,更好的服务于腾讯自研业务上云。

原文链接:https://cloud.tencent.com/deve ... 05214 收起阅读 »

Prometheus基础知识介绍


【编者的话】本文会让你了解Prometheus是什么,并让你理解它在监控领域的适用场景。

Prometheus起源

很久以前,加利福尼亚州山景城有一家名为Google的公司。他们推出了大量产品,其中最著名的是广告系统和搜索引擎平台。为了运行这些不同的产品,他们建立了一个名为Borg的平台。Borg系统是“一个集群管理器,可以运行来自成千上万个不同的应用程序的成千上万个作业,它跨越多个集群,每个集群都有数万台服务器。“开源容器管理平台Kubernetes很多部分都是对Borg平台的传承。在Borg部署到Google后不久,他们意识到这种复杂性需要一个同等水平的监控系统。Google建立了这个系统并命名为Borgmon。Borgmon是一个实时的时间序列监控系统,它使用这些时间序列数据来识别问题并发出警报。如果你想和更多Prometheus技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

Prometheus的灵感来自谷歌的Borgmon。它最初由前谷歌SRE Matt T. Proud开发,并转为一个研究项目。在Proud加入SoundCloud之后,他与另一位工程师Julius Volz合作开发了Prometheus。后来其他开发人员陆续加入了这个项目,并在SoundCloud内部继续开发,最终于2015年1月公开发布。

与Borgmon一样,Prometheus主要用于提供近实时的,针对动态云环境下的和基于容器的微服务、服务和应用程序的检测监控。SoundCloud是这些架构模式的早期采用者,Prometheus的建立是为了满足这些需求。如今,Prometheus被更多的公司广泛使用,通常也是满足类似的监控需求,但也用来监控传统架构的资源。

Prometheus专注于现在正在发生的事情,而不是追踪数周或数月前的数据。它基于这样一个前提,即大多数监控查询和警报都是从最近的,通常是一天内的数据生成的。Facebook在其内部时间序列数据库Gorilla的论文中验证了这一观点。Facebook发现85%的查询是针对26小时内的数据。Prometheus假定你尝试修复的问题可能是最近出现的,因此最有价值的是最近时间的数据,这反映在强大的查询语言和通常有限的监控数据保留期上。

Prometheus是用开源编程语言Go编写的,并在Apache 2.0许可证下授权。它孵化于云原生云计算基金会(Cloud Native Computing Foundation)。

Prometheus架构

Prometheus通过抓取或拉取从应用程序中暴露的时间序列数据来工作。时间序列数据通常由应用程序本身通过客户端库,或通过称为导出器(exporter)的代理作为HTTP端点暴露。目前已经存在很多exporter和客户端库,支持多种编程语言、框架和开源应用程序,例如,Apache Web服务器和MySQL数据库等。

Prometheus还有一个推送网关(push gateway),可用于接收少量数据 - 例如,来自无法拉取的目标数据,比如临时作业或者防火墙后面的目标。
第二章图1.jpg

Prometheus架构
图文字翻译:Alert manager:Alertmanager;My Service:服务;Exporters run here:Exporter在这运行

指标收集

Prometheus称其可以抓取的指标来源为端点(endpoint)。端点通常对应于单个进程、主机、服务或应用程序。为了抓取端点数据,Prometheus定义了名为目标(target)的配置。这是执行抓取所需的信息 - 例如,如何进行连接,要应用哪些元数据,连接需要哪些身份验证,或定义抓取将如何执行的其他信息。一组目标被称为作业(job)。作业通常是具有相同角色的目标组 - 例如,负载均衡器后面的Apache服务器集群,它们实际上是一组相似的进程。

生成的时间序列数据将被收集并存储在Prometheus服务器本地,也可以设置从服务器发送数据到外部存储器或其他时间序列数据库。

服务发现

可以通过多种方式处理要监控的资源的发现,包括:
  • 用户提供的静态资源列表
  • 基于文件的发现。例如,使用配置管理工具生成在Prometheus中可以自动更新的资源列表
  • 自动发现。例如,查询Consul等数据存储,在Amazon或Google中运行实例,或使用DNS SRV记录生成资源列表


聚合和警报

服务器还可以查询和聚合时间序列数据,并创建规则来记录常用的查询和聚合。这允许你从现有的时间序列创建新的时间序列,例如计算变化率和比率或求和等聚合。这样就不必重新创建常用的聚合,例如用于调试,并且预计算可能比每次需要时运行查询性能更好。

Prometheus还可以定义警报规则。这些是为系统配置在满足条件时触发警报的标准,例如,资源时间序列开始显示异常的CPU使用率。Prometheus服务器没有内置警报工具,而是将警报从Prometheus服务器推送到名为警报管理器(Alertmanager)的单独服务器。Alertmanager可以管理、整合和分发各种警报到不同目的地 - 例如,它可以在发出警报时发送电子邮件,并能够防止重复发送。

查询数据

Prometheus服务器还提供了一套内置查询语言PromQL,一个表达式浏览器以及用于浏览服务器上数据的图形界面。
第二章图2.jpg

Prometheus表达式浏览器

自治

每个Prometheus服务器都设计为尽可能自治,旨在支持扩展到数千台主机的数百万个时间序列的规模。数据存储格式被设计尽可能降低磁盘的使用率,并在查询和聚合期间快速检索时间序列。


提示:为了速度和可靠性,建议Prometheus服务器充分使用内存(Prometheus在内存中做很多事)和SSD磁盘。关于SSD使用可以参考注释链接视频。

冗余和高可用性

冗余和高可用性侧重弹性而不是数据持久性。Prometheus团队建议将Prometheus服务器部署到特定环境和团队,而不是仅部署一个单体Prometheus服务器。如果你确实要部署高可用HA模式,则可以使用两个或多个配置相同的Prometheus服务器收集时间序列数据,并且所有生成的警报都由可消除重复警报的高可用Alertmanager集群处理。
第二章图3.jpg

Prometheus冗余架构
图文字翻译:Alert manager:Alertmanager;My Service:服务


提示:我们将在第7章中介绍如何实现此配置。

可视化

可视化通过内置表达式浏览器提供,并与开源仪表板Grafana集成。此外,Prometheus也支持其他仪表板。

Prometheus数据模型

正如之前所述,Prometheus收集时间序列数据。为了处理这些数据,它使用一个多维时间序列数据模型。这个时间序列数据模型结合了时间序列名称和被称为标签(label)的键/值对,这些标签提供了维度。每个时间序列由时间序列名称和标签的组合唯一标识。

指标名称

时间序列名称通常可以描述收集的时间序列数据的一般性质 - 例如,website_visits_total为网站访问的总数。

名称可以包含ASCII字符、数字、下划线和冒号。

指标标签

标签为Prometheus数据模型提供了维度。它们为特定时间序列添加上下文。例如,total_website_visits时间序列可以使用能够识别网站名称、请求IP或其他特殊标识的标签。Prometheus可以在一个时间序列、一组时间序列或者所有相关的时间序列上进行查询。

标签共有两大类:监控标签(instrumentation label)和目标标签(target label)。监控标签来自被监控的资源 - 例如,对于与HTTP相关的时间序列,标签可能会显示所使用的特定HTTP谓词。这些标签在被抓取之前被添加到时间序列中,例如由客户端或exporter。目标标签更多地与架构相关 - 它们可能会识别时间序列所在的数据中心。目标标签在Prometheus抓取期间和之后添加。

时间序列由名称和标签标识(尽管从技术上讲,名称本身也是名为__name__的标签)。如果你在时间序列中添加或更改标签,Prometheus会将其视为新的时间序列。


提示:你可以理解label就是键/值形式的标签,并且新的标签会创建新的时间序列。
标签名称可以包含ASCII字符、数字和下划线。


提示:带有__前缀的标签名称保留给Prometheus内部使用。

采样数据

时间序列的真实值是采样(sample)的结果,它包括两部分:
  • 一个float64类型的数值
  • 一个毫秒精度的时间戳


符号表示

结合这些元素,我们可以看到Prometheus如何将时间序列表示为符号(notation)。

代码清单2.1时间序列符号:
<time series name>{<label name>=<label value>, ...} 

例如,带有标签的total_website_visits时间序列可能如下所示。

代码清单2.2时间序列示例:
total_website_visits{site="MegaApp", location="NJ", instance="
webserver",job="web"} 

首先是时间序列名称,后面跟着一组键/值对标签。通常所有时间序列都有一个instance标签标识源主机或应用程序,以及一个job标签,包含了特定时间序列的作业名称。


注:这与OpenTSDB使用的符号大致相同,这是受到了Borgmon的影响。

保留时间

Prometheus专为短期监控和警报需求而设计。默认情况下,它在其数据库中保留15天的时间序列数据。如果要保留更长时间的数据,建议将所需数据发送到第三方平台。Prometheus能够写入外部数据存储,我们将在第7章中看到更多相关内容。

安全模型

Prometheus可以通过多种方式进行配置和部署,关于安全有以下两个假设:
  • 不受信任的用户将能够访问Prometheus服务器的HTTP API,从而访问数据库中的所有数据。
  • 只有受信任的用户才能访问Prometheus命令行、配置文件、规则文件和运行时配置。



提示:从Prometheus 2.0开始,默认情况下某些HTTP API的管理功能被禁用。
因此,Prometheus及其组件不提供任何服务器端身份验证、授权或加密。如果你在一个需要更加安全的环境中工作,则需要自己实施安全控制 - 例如,通过反向代理访问Prometheus服务器或者正向代理exporter。由于不同版本的配置潜在地会发生较大变化,本书没有记录如何执行这些操作。

Prometheus生态

Prometheus生态系统由Prometheus项目本身提供的组件以及丰富的开源工具和套件组成。生态系统的核心是Prometheus服务器,我们将在下一章中详细介绍。此外还有Alertmanager,它为Prometheus提供警报引擎并进行管理。

Prometheus项目还包括一系exporter,用于监控应用程序和服务,并在端点上公开相关指标以进行抓取。核心exporter支持常见工具,如Web服务器、数据库等。许多其他exporter都是开源的,你可以从Prometheus社区查看。

Prometheus还发布了一系列客户端库,支持监控多种语言编写的应用程序和服务。它们包括主流编程语言,如Python、Ruby、Go和Java。其他客户端库也可以从开源社区获取。

小结

在本文中,我们介绍了Prometheus基本概念,以及Prometheus架构、Prometheus数据模型以及生态系统等方面。

以上内容摘自《Prometheus监控实战》一书,经出版方授权发布。
继续阅读 »

【编者的话】本文会让你了解Prometheus是什么,并让你理解它在监控领域的适用场景。

Prometheus起源

很久以前,加利福尼亚州山景城有一家名为Google的公司。他们推出了大量产品,其中最著名的是广告系统和搜索引擎平台。为了运行这些不同的产品,他们建立了一个名为Borg的平台。Borg系统是“一个集群管理器,可以运行来自成千上万个不同的应用程序的成千上万个作业,它跨越多个集群,每个集群都有数万台服务器。“开源容器管理平台Kubernetes很多部分都是对Borg平台的传承。在Borg部署到Google后不久,他们意识到这种复杂性需要一个同等水平的监控系统。Google建立了这个系统并命名为Borgmon。Borgmon是一个实时的时间序列监控系统,它使用这些时间序列数据来识别问题并发出警报。如果你想和更多Prometheus技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

Prometheus的灵感来自谷歌的Borgmon。它最初由前谷歌SRE Matt T. Proud开发,并转为一个研究项目。在Proud加入SoundCloud之后,他与另一位工程师Julius Volz合作开发了Prometheus。后来其他开发人员陆续加入了这个项目,并在SoundCloud内部继续开发,最终于2015年1月公开发布。

与Borgmon一样,Prometheus主要用于提供近实时的,针对动态云环境下的和基于容器的微服务、服务和应用程序的检测监控。SoundCloud是这些架构模式的早期采用者,Prometheus的建立是为了满足这些需求。如今,Prometheus被更多的公司广泛使用,通常也是满足类似的监控需求,但也用来监控传统架构的资源。

Prometheus专注于现在正在发生的事情,而不是追踪数周或数月前的数据。它基于这样一个前提,即大多数监控查询和警报都是从最近的,通常是一天内的数据生成的。Facebook在其内部时间序列数据库Gorilla的论文中验证了这一观点。Facebook发现85%的查询是针对26小时内的数据。Prometheus假定你尝试修复的问题可能是最近出现的,因此最有价值的是最近时间的数据,这反映在强大的查询语言和通常有限的监控数据保留期上。

Prometheus是用开源编程语言Go编写的,并在Apache 2.0许可证下授权。它孵化于云原生云计算基金会(Cloud Native Computing Foundation)。

Prometheus架构

Prometheus通过抓取或拉取从应用程序中暴露的时间序列数据来工作。时间序列数据通常由应用程序本身通过客户端库,或通过称为导出器(exporter)的代理作为HTTP端点暴露。目前已经存在很多exporter和客户端库,支持多种编程语言、框架和开源应用程序,例如,Apache Web服务器和MySQL数据库等。

Prometheus还有一个推送网关(push gateway),可用于接收少量数据 - 例如,来自无法拉取的目标数据,比如临时作业或者防火墙后面的目标。
第二章图1.jpg

Prometheus架构
图文字翻译:Alert manager:Alertmanager;My Service:服务;Exporters run here:Exporter在这运行

指标收集

Prometheus称其可以抓取的指标来源为端点(endpoint)。端点通常对应于单个进程、主机、服务或应用程序。为了抓取端点数据,Prometheus定义了名为目标(target)的配置。这是执行抓取所需的信息 - 例如,如何进行连接,要应用哪些元数据,连接需要哪些身份验证,或定义抓取将如何执行的其他信息。一组目标被称为作业(job)。作业通常是具有相同角色的目标组 - 例如,负载均衡器后面的Apache服务器集群,它们实际上是一组相似的进程。

生成的时间序列数据将被收集并存储在Prometheus服务器本地,也可以设置从服务器发送数据到外部存储器或其他时间序列数据库。

服务发现

可以通过多种方式处理要监控的资源的发现,包括:
  • 用户提供的静态资源列表
  • 基于文件的发现。例如,使用配置管理工具生成在Prometheus中可以自动更新的资源列表
  • 自动发现。例如,查询Consul等数据存储,在Amazon或Google中运行实例,或使用DNS SRV记录生成资源列表


聚合和警报

服务器还可以查询和聚合时间序列数据,并创建规则来记录常用的查询和聚合。这允许你从现有的时间序列创建新的时间序列,例如计算变化率和比率或求和等聚合。这样就不必重新创建常用的聚合,例如用于调试,并且预计算可能比每次需要时运行查询性能更好。

Prometheus还可以定义警报规则。这些是为系统配置在满足条件时触发警报的标准,例如,资源时间序列开始显示异常的CPU使用率。Prometheus服务器没有内置警报工具,而是将警报从Prometheus服务器推送到名为警报管理器(Alertmanager)的单独服务器。Alertmanager可以管理、整合和分发各种警报到不同目的地 - 例如,它可以在发出警报时发送电子邮件,并能够防止重复发送。

查询数据

Prometheus服务器还提供了一套内置查询语言PromQL,一个表达式浏览器以及用于浏览服务器上数据的图形界面。
第二章图2.jpg

Prometheus表达式浏览器

自治

每个Prometheus服务器都设计为尽可能自治,旨在支持扩展到数千台主机的数百万个时间序列的规模。数据存储格式被设计尽可能降低磁盘的使用率,并在查询和聚合期间快速检索时间序列。


提示:为了速度和可靠性,建议Prometheus服务器充分使用内存(Prometheus在内存中做很多事)和SSD磁盘。关于SSD使用可以参考注释链接视频。

冗余和高可用性

冗余和高可用性侧重弹性而不是数据持久性。Prometheus团队建议将Prometheus服务器部署到特定环境和团队,而不是仅部署一个单体Prometheus服务器。如果你确实要部署高可用HA模式,则可以使用两个或多个配置相同的Prometheus服务器收集时间序列数据,并且所有生成的警报都由可消除重复警报的高可用Alertmanager集群处理。
第二章图3.jpg

Prometheus冗余架构
图文字翻译:Alert manager:Alertmanager;My Service:服务


提示:我们将在第7章中介绍如何实现此配置。

可视化

可视化通过内置表达式浏览器提供,并与开源仪表板Grafana集成。此外,Prometheus也支持其他仪表板。

Prometheus数据模型

正如之前所述,Prometheus收集时间序列数据。为了处理这些数据,它使用一个多维时间序列数据模型。这个时间序列数据模型结合了时间序列名称和被称为标签(label)的键/值对,这些标签提供了维度。每个时间序列由时间序列名称和标签的组合唯一标识。

指标名称

时间序列名称通常可以描述收集的时间序列数据的一般性质 - 例如,website_visits_total为网站访问的总数。

名称可以包含ASCII字符、数字、下划线和冒号。

指标标签

标签为Prometheus数据模型提供了维度。它们为特定时间序列添加上下文。例如,total_website_visits时间序列可以使用能够识别网站名称、请求IP或其他特殊标识的标签。Prometheus可以在一个时间序列、一组时间序列或者所有相关的时间序列上进行查询。

标签共有两大类:监控标签(instrumentation label)和目标标签(target label)。监控标签来自被监控的资源 - 例如,对于与HTTP相关的时间序列,标签可能会显示所使用的特定HTTP谓词。这些标签在被抓取之前被添加到时间序列中,例如由客户端或exporter。目标标签更多地与架构相关 - 它们可能会识别时间序列所在的数据中心。目标标签在Prometheus抓取期间和之后添加。

时间序列由名称和标签标识(尽管从技术上讲,名称本身也是名为__name__的标签)。如果你在时间序列中添加或更改标签,Prometheus会将其视为新的时间序列。


提示:你可以理解label就是键/值形式的标签,并且新的标签会创建新的时间序列。
标签名称可以包含ASCII字符、数字和下划线。


提示:带有__前缀的标签名称保留给Prometheus内部使用。

采样数据

时间序列的真实值是采样(sample)的结果,它包括两部分:
  • 一个float64类型的数值
  • 一个毫秒精度的时间戳


符号表示

结合这些元素,我们可以看到Prometheus如何将时间序列表示为符号(notation)。

代码清单2.1时间序列符号:
<time series name>{<label name>=<label value>, ...} 

例如,带有标签的total_website_visits时间序列可能如下所示。

代码清单2.2时间序列示例:
total_website_visits{site="MegaApp", location="NJ", instance="
webserver",job="web"} 

首先是时间序列名称,后面跟着一组键/值对标签。通常所有时间序列都有一个instance标签标识源主机或应用程序,以及一个job标签,包含了特定时间序列的作业名称。


注:这与OpenTSDB使用的符号大致相同,这是受到了Borgmon的影响。

保留时间

Prometheus专为短期监控和警报需求而设计。默认情况下,它在其数据库中保留15天的时间序列数据。如果要保留更长时间的数据,建议将所需数据发送到第三方平台。Prometheus能够写入外部数据存储,我们将在第7章中看到更多相关内容。

安全模型

Prometheus可以通过多种方式进行配置和部署,关于安全有以下两个假设:
  • 不受信任的用户将能够访问Prometheus服务器的HTTP API,从而访问数据库中的所有数据。
  • 只有受信任的用户才能访问Prometheus命令行、配置文件、规则文件和运行时配置。



提示:从Prometheus 2.0开始,默认情况下某些HTTP API的管理功能被禁用。
因此,Prometheus及其组件不提供任何服务器端身份验证、授权或加密。如果你在一个需要更加安全的环境中工作,则需要自己实施安全控制 - 例如,通过反向代理访问Prometheus服务器或者正向代理exporter。由于不同版本的配置潜在地会发生较大变化,本书没有记录如何执行这些操作。

Prometheus生态

Prometheus生态系统由Prometheus项目本身提供的组件以及丰富的开源工具和套件组成。生态系统的核心是Prometheus服务器,我们将在下一章中详细介绍。此外还有Alertmanager,它为Prometheus提供警报引擎并进行管理。

Prometheus项目还包括一系exporter,用于监控应用程序和服务,并在端点上公开相关指标以进行抓取。核心exporter支持常见工具,如Web服务器、数据库等。许多其他exporter都是开源的,你可以从Prometheus社区查看。

Prometheus还发布了一系列客户端库,支持监控多种语言编写的应用程序和服务。它们包括主流编程语言,如Python、Ruby、Go和Java。其他客户端库也可以从开源社区获取。

小结

在本文中,我们介绍了Prometheus基本概念,以及Prometheus架构、Prometheus数据模型以及生态系统等方面。

以上内容摘自《Prometheus监控实战》一书,经出版方授权发布。 收起阅读 »