通往成功DevOps的六大障碍

JFrog捷蛙阅读(2303)评论(0)

一、背景

在现今的社会中,每个公司都是软件公司,无论是通过台式机、云服务还是移动设备,软件都已成为世界各地、各个公司日常工作的工具。例如,汽车是带轮子的计算机,空调是数据终端,而银行在手机中提供服务,等等。

在这个新的世界中,软件更新可以满足客户的需求。每一个交付都是您更新,或破坏,与客户间信任的机会。如何才能保证您的每个更新都以最快的速度提供一流的服务?

这就是为什么DevOps对您的公司很重要。当您加快高质量软件的交付速度时,客户会大呼过瘾,并且您可以对市场需求的变化迅速做出反应。DevOps通过减少在测试、评估和发布等各阶段之间,以及与相关干系人之间的摩擦,来加快高质量软件的交付速度。识别并解决这个过程中的痛点可以推进DevOps的成功建设。

良好的制品仓库可以促进软件在DevOps流程中的运转。它存储了过程中所有的二进制制品(artifacts,也称为工件),同时也保留了有关它们的信息,从而减少了不确定性,并使自动化工具能够自由、快速地运行。

在加速软件交付的过程中通常会遇到下述的六大障碍,而良好的制品仓库可以帮助解决它们,以实现从代码到客户的快速、持续的软件更新与发布。

二、六大障碍

障碍一,您了解您所有的构建吗?

您的开发人员团队每天都可以生成许多构建,而您能全部跟踪和掌握所有的构建吗?

如果没有全面的解决方案,那您可能会知道哪个版本是最新的,但却无法确认哪个版本才是最好的。您也无法可靠地追溯构建的历史,并掌握组成该交付版本的各个部分都是来自何处。

当构建失败时,您能够识别并回退有问题的部分吗?您如何才能查明哪些构建存在问题,以及问题出现在构建过程中哪个位置,以便您或开发人员可以快速提供修复。

解决方案:通用记录系统

为您所有的构建建造一个制品中心,作为所有在DevOps流水线中运转的制品的唯一真实来源。在中心的仓库中管理和版本化所有构建的产出,意味着您可以轻松找到功能最佳、最新的构建。

制品仓库能够跟踪制品使用的位置,及其先前的所有版本,从而提供了丰富的数据,来帮助您追溯所有构建的来源及其祖先。您可以快速查看一个版本与另一个版本之间的差异,了解每个版本的制作方式,并找到可帮助您修复错误版本的参考。

障碍二,您的过程中有人工操作吗?

在DevOps过程中,每个需要人工介入的地方都会带来风险。例如,人工的检验会增加延迟,生产环境的重复构建会带来不确定性,必须手动更改、维护和执行的用于工具管理或构建部署的脚本会浪费时间,而且容易出错。这些成本昂贵的过程,任何一个都会减慢正确的软件版本发布到最终用户的速度。

解决方案:自动化和流程管理

如上一部分所讲,保管您所有构建和制品的中央制品仓库为构建管理提供了便利。但如果同时它也可以收集有关制品的信息,这将赋予您更多的能力。您对制品的了解越多,就越能实现更好的自动化,并使您的构建工具能够做出明智的决策,从而统一并加速整个部署过程中的软件交付。

您的制品仓库应该能够为您的构建工具提供丰富的、灵活的查询和命令接口,以便它们可以不在您的干预下自动完成工作。如果它使用标准的、平台无关的访问机制,如REST API,则您可以任意选择最适合您的CI服务器。

一旦您能够实现DevOps流程的自动化,就可以更好地确保发布到生产中的每个版本都遵循相同的流程,并且符合通用标准。

障碍三,您规范管理了所有的构建依赖吗?

现在的软件应用开发的特点,一是多语言并存,二是架构在公共框架、公共库的基础之上。从而开发人员在构建过程中为这多钟开发语言和技术都拉取了大量的外部依赖,而每种语言和技术对于依赖的管理都有其自己的要求和接口。您将如何管理它们?

这些外部资源可以随时更改,并且对其质量的控制和管理程度参差不齐,有的根本就没有保证。您如何确定每个版本中的用到了哪些依赖?如何可靠地复用其中的某个依赖?如何检测悄然发生的有害更新?

而且,您的构建过程不可能跑得比访问这些远程资源的链接快,繁重的网络负载会减慢构建速度,而访问的中断会导致您原来可靠的构建失败。

解决方案:依赖管理

使用本地制品仓库来代理存储外部依赖的远程资源,进而将所有外部依赖纳入统一管理。借助制品仓库对这些外部依赖的本地缓存,可以保证始终以所需的版本、最快的速度来完成构建。

更好的是,一旦您的制品仓库掌握了这些外部依赖,它就可以像其他制品一样,为这些外部依赖保存和维护相同的信息。通过跟踪依赖的历史记录和使用过程,就能始终确认每个构建中都采用了哪个版本的依赖项。

障碍四,您是如何在DevOps流程中传递交付版本的?

许多DevOps流程中,在测试、验证和发布的每个阶段,都需要基于全部或部分源代码进行重新构建。这就导致每个新版本都需要花费更多的时间,并且可能需要每个干系人进行手动评估和触发。更糟的是,随着开发人员持续地更改共享代码,每次重新构建都会带来不确定性,不得不在每个阶段重复相同的质量检查。

一旦某个构建通过了当前检查,您如何将其实际推进到下一阶段?手动将该构建推送到下一阶段的过程很容易出错。而且,您还需要一种在整个DevOps过程中向整个团队传达该构建状态的方法。

解决方案:元数据和升级管理

如障碍二的解决方案中所述,本地制品仓库不仅管理了所有构建及其制品,还管理了制品相关的信息,也可称为制品的元数据。这些元数据可以帮助您对该制品的质量进行检验,来源进行跟踪。

在DevOps流程中,各个阶段之间交付版本的推进,推荐的最佳实践是避免重复构建,而是采用制品升级的方式。也就是说,在前一个阶段完成质量检查后,制品带着其元数据,一起升级到下一个阶段。下一个阶段首先根据元数据对该制品进行质量检查和评测,确认达到质量标准再开始本阶段的工作。

升级的方式,使得每个阶段都直接基于制品开展工作,避免重复构建,在提升效率的同时,也降低了不确定性的风险。同时,针对元数据的检测,既保证了制品在各个阶段的一致性,避免篡改,又有助于提升质量检测的自动化程度,减少人工的介入,提高效率的同时,也降低了出错风险。

障碍五,您是如何满足客户不断增长的需求的?

为了满足客户日益增长的需求,您需要今天多做,明天做得更多。这会加重许多业务团队的负担,进而可能会减慢整个开发流程。

而基础架构中的任何单点故障都可能是灾难性的。地理位置分散的团队需要始终能够以相同的速度获得相同的资源,任何业务更新或容量升级造成的服务中断都会浪费大量的生产时间。

解决方案:企业级支持

企业级支持的解决方案可提供适应您的规模及成长的能力和灵活性。

可以在云平台中工作的制品仓库可以帮助您无限地扩展存储和计算的成本。您的制品仓库可以使用的云供应商越多,您获得的控制权就越大。SaaS订阅选项可确保您的资源始终可用并且是最新的。

高可用、多活的集群配置可以确保高负载下制品仓库的响应能力。其冗余还为灾难恢复提供了容错支持,并实现了零宕机的升级和维护。

支持多站点复制同步的制品仓库可以为跨地域的分布式团队提供全球范围内DevOps过程中资源、信息的快速分享。

障碍六,您适应变更的成本有多高?

响应所有的客户意味着在多个运行系统中使用多种语言进行开发。某个部门可能用Go为云平台编写代码,而另一个部门则可能用Java为移动设备编写代码。但是每种语言和技术都有其自己的要求和支持的工具。

您将为DevOps使用哪种基础架构?现在,在您自己的数据中心中安全运行可能是最有意义的。而未来,您可能需要云平台的灵活性,或者将它们结合起来以获得各自的优势。您将可以自由选择最适合您需求的供应商,并在需求变化时灵活地进行更改。

解决方案:混合云的解决方案

支持混合云架构的制品仓库可以帮助您的交付过程自动化,无论您使用的是哪种语言或运行于何种平台。通过REST API进行访问,可以方便、灵活地与您已经在使用的工具进行对接。

作为DevOps系统的核心,您的制品仓库在云平台中的功能必须与在本地自己的服务器上的性能相同。在任何环境间都能够轻松地升级构建、推进交付的解决方案可以有效地帮助您在功能强大的混合云中实现DevOps。对所有主要提供商(例如AWS、Google Cloud、Azure、阿里云等)的集成支持,可以帮助您实现避免供应商绑定的多云策略。

您也应该能够自主地选择付款方式。您需要的解决方案应该是,无论您选择固定的许可费用还是灵活的SaaS订阅,都能够帮助您自由地构建现在和将来使用的系统。

三、总结

功能齐全的制品仓库将帮助您实现自动化的软件交付流程,并支持您采用新的工作方式。它可以为您提供对流程的控制和洞察力,从而可以解决出现的问题并不断改进您的方法。经过稳健的设计后,您的制品仓库可以灵活地适应企业的特殊需求。

同样重要的是,您需要一个可以在您的DevOps建设过程中成为良好合作伙伴的解决方案提供商。他们应该了解不同的方法和行业趋势。

JFrog的Artifactory制品仓库是端到端DevOps平台的核心,用于自动化管理、保护、分发和监视所有类型的制品。Artifactory得到了近6000家客户的信任,其中包括了世界500强中93%的客户。亚马逊、Facebook、谷歌、华为、VMware等世界顶级品牌都依靠JFrog来管理其制品,推进其DevOps进程。

希望Artifactory同样能够帮助您解决上述的六大障碍,成功建设DevOps体系,实现高质量、快速、持续的软件发布流程。

 

更多技术分享请关注      JFrog杰蛙在线课堂

12日在线课堂:《版本控制管理与最佳实践》

课堂收益:

  1. 如何使用版本控制系统
  2. 各种分支模型有什么优缺点,什么样的分支模型更适用于您的环境
  3. 版本控制应该遵循的一些标准

 

报名链接:https://www.bagevent.com/event/6309332

抽奖活动:

课堂结束前五分钟,进行抽奖活动

第一名:小爱音箱

第二名:JFrog杰蛙新版T

第三名:JFrog杰蛙新版T

Github架构师解读C/C++应用包管理的Why和How

JFrog捷蛙阅读(3065)评论(0)

一、背景

本文整理自Johannes Nicolai在JFrog 2019用户大会上的讲演《DevOps for Non-Hipsters(aka C/C++ programmers)》。

 

Johannes Nicolai是Github的解决方案架构师,主要负责德语区的用户。他和很多制造业的用户(多数使用C/C++)交流,询问他们在DevOps或持续交付方面的挑战,通常会得到如下的描述:

在嵌入式C/C++领域,花费几十个小时完成一个完整的DevOps流水线并不少见。为某一个提交运行单独的构建和测试几乎是不可能的,通常每次构建都包含了几百个同事所有的提交。而构建时间长的主要原因在于交付包包含了大量的依赖包,而每次构建这些依赖包都需要从头开始重新构建。上述的描述并不仅限于德语区,Johannes询问了美国制造业的用户,也得到了类似的反馈。

从业界的发展来看,声明式包管理能够很好的解决上述的问题。在交付包中通过声明描述所需的依赖包,在构建时根据声明从包管理系统中获取相应的依赖包,这样能够大大缩短构建时间。Java或JavaScript的开发者很熟悉这样的方式。

对于像Java或JavaScript这样的开发语言,包管理的实现相对简单,包的每一个版本只对应一个二进制文件。而在C/C++中,由于操作系统、架构、编译器等的不同,包的每一个版本会对应多个不同的二进制文件,彼此之间还并不兼容。这也就导致了C/C++的包管理一直是业界公认的难题。

当然,针对C/C++的开发,现在也出现了像Conan这样比较成熟的包管理解决方案。Johannes在本次讲演中首先分析了为什么要在DevOps中引入包管理,然后通过演示介绍了Conan如何通过方便的包管理和开发方式,帮助C/C++程序员实现简洁、高效的DevOps流水线。

二、为什么要在DevOps中引入包管理

现在业界大都在推行敏捷,而在敏捷提出的12条原则中,第一条就是:通过早期和持续型的高价值工作交付满足“客户”。

通过持续性的交付,首先能够快速发现问题,从而尽早解决问题,而不是每次发布前都要积累大量的问题,从而导致过长的修复时间和交付质量的下降。

其次,用户可能一开始并不是特别清楚自己的需求。通过持续性的交付,用户可以不断的试用来渐进明晰地明确自己的实际需求,从而保证了交付的有效性。

要实现敏捷原则所要求的持续性交付,我们必须实现持续性、可重复的DevOps流水线。而为了实现这样的DevOps,一个基本原则是要做到“只构建一次二进制文件”。

也就是说,每一个版本的交付包,我们只构建一次。获得其对应的二进制文件后,在DevOps的后续阶段、不同环境中,都应该用且只用这同一个二进制文件。

然而,针对C/C++的应用来说,各种不同的目标环境导致同一个版本的交付包,必然会对应多个互不兼容的二进制文件。在这种情况,要做到仅一次构建,无疑需要借助于良好的包管理解决方案。

通过引入包管理系统,可以为C/C++包的每一个版本预编译好多个与之对应的面向不同目标环境的二进制包,再通过语义化版本及兼容环境的描述,在构建过程中直接获取对应的二进制包,从而能够大大节省构建的时间,保证DevOps流水线的一致性和可重复性。同时,当发现某些问题,如安全漏洞或开源许可证错误时,也可以通过对依赖关系的管理,迅速定位问题的影响范围,提升问题的解决效率。

对于C/C++开发常用的子模块的方式,并不能满足上述DevOps的要求。子模块的方式不能解决构建时间长的问题,不能保证所依赖的库的不可变性,对版本的依赖关系缺乏灵活的定义和管理,对兼容性的分析和处理也缺乏内置的解决方案。

类似的,通过Git LFS来管理C/C++的包也不是一个好的方式。Git LFS缺乏对版本依赖关系的灵活定义和管理,缺乏对兼容性分析和处理的内置解决方案,同样不能解决构建时间长的问题。

因此,要提升C/C++应用的DevOps效率和质量,我们需要引入与Java、JavaScript等类似的包管理解决方案。而目前这一领域发展最快、最受业界关注的就是Conan。

三、Conan——C/C++的包管理方案

Conan(https://conan.io)是一个开源的解决方案,为C/C++应用提供了跨平台的包管理方式。而JFrog收购Conan后,通过结合其在制品管理方面产品和技术的优势,更提升了Conan对C/C++应用的支持能力。

Conan具有良好的兼容性,能够与当前C/C++领域应用的各种构建系统和编译器配合使用。

Conan提供了完整的C/C++应用依赖关系管理能力,能够支持语义化版本描述、传递依赖的解析、依赖冲突的分析与解决,以及灵活的范围化版本描述等。

Conan还为C/C++应用的DevOps建设提供了丰富的工具支持:

  • 针对包仓库,提供了原生、开源的Conan Server,同时JFrog的Artifactory、Bintray也提供了功能更为丰富、全面的商业化产品支持;
  • Conan的客户端,与各种构建系统对接,实现基于Conan的C/C++构建;

Conan提供与Jenkins、Travis CI等工具的对接,实现C/C++应用自动化的、可重复的DevOps流水线。

在Conan的解决方案中,包的每一个版本都根据目标环境的不同,如架构、操作系统、编译器等,预编译好与之对应的二进制包。构建时,Conan客户端只下载与当前目标环境兼容的二进制包,从而在保证一致性的同时,提升了构建的效率。

对于特殊环境,还没有对应的预编译二进制包的情况,Conan通过定义包的Recipe,描述了如何构建该包的二进制包的过程,Conan客户端可以即时构建出一个新的,匹配与当前特殊环境的新二进制包,供应用构建使用。同时,这个新二进制包也可以存回包仓库当中,供后续的构建直接引用。

综上所述,与Java、JavaScript等使用的类似,Conan为C/C++开发者提供了一个成熟的、功能完整、工具完备的包管理解决方案,能够辅助C/C++的开发者创建稳定、高效、一致、可重复的DevOps流水线。

四、如何在C/C++应用中使用Conan

Johannes在演讲中还通过演示,展示了如何基于Conan,实现便捷、高效的C/C++应用的构建。

Johannes所用的例子不是简单的“Hello World”,而是github上一个真实项目:

要使用Conan,我们只需为每一个C/C++应用增加一个conanfile.txt,用以描述其依赖关系:

利用“conan remote add”命令,可以将Conan客户端和Conan的包仓库建立关联,再执行“conan install”,就可以将符合目标环境需求的所有依赖二进制包下载在本地。

在编辑构建参数,如使用CMake构建,就修改CMakeLists.txt,加入conan的配置,就可以集成下载的依赖二进制包,完成C/C++应用的构建。

除了直接引用Conan仓库中已有的包及其二进制文件,利用Conan也可以创建自己开发的Conan包作为库,供其他C/C++应用依赖。Johannes还以github上的另一个项目演示了如何创建自己的Conan包:

要创建Conan的库包,需要为项目增加conanfile.py文件,如上图中的右半部分,改py文件就对应了之前提到的Conan包的Recipe,它除了描述了该包的基本信息之外,还通过函数定义了如何构建该库包得到二进制文件的过程。

通过执行“conan create”命令,我们就可以生成自定义的Conan包作为内部库,再执行“conan upload”将其上传到Conan包仓库,就可以被其他C/C++应用引用、依赖了。

此外,Conan还可以与Jenkins等工具集成,通过自动化、并行的方式,一次性构建出同一版本包,针对不同目标环境的所有二进制文件:

最后,基于Conan的包管理方案,通过与GitHub、Jenkins、Artifactory、Bintray等工具对接,可以实现完整的C/C++应用的DevOps流水线:

通过演示可以看出,在C/C++应用中引入Conan的包管理,方式是直观、简便的,附加的工作负载并不多。而通过与各种工具的集成,可以基于Conan方便地创建C/C++应用的DevOps流水线,满足敏捷的需求。

五、总结

敏捷化是目前业界应用研发的发展方向。通过实施敏捷化,我们可以实现迅速的、持续的产品交付,从而尽早发现问题,尽早解决问题。而且,通过快速、持续的交付,我们也可以获得用户持续的反馈,渐进明细地挖掘和实现用户的真正需求。

对于C/C++应用及开发者来说,基于Conan的包管理方案,以及与DevOps领域工具的集成使用,可以创建便捷、高效、一致性、可重复的DevOps流水线,从而满足敏捷化的需求。

 

更多技术分享请关注      JFrog杰蛙在线课堂

1月2日在线课堂:《版本控制管理与最佳实践》

课堂收益:

  1. 如何使用版本控制系统
  2. 各种分支模型有什么优缺点,什么样的分支模型更适用于您的环境
  3. 版本控制应该遵循的一些标准

 

报名链接:https://www.bagevent.com/event/6309332

抽奖活动:

课堂结束前五分钟,进行抽奖活动

第一名:小爱音箱

第二名:JFrog杰蛙新版T恤

第三名:JFrog杰蛙新版T恤

火箭还是飞机?——DevOps的两种模式

JFrog捷蛙阅读(1967)评论(0)

一、背景

在当下的软件应用开发领域中,越来越多的敏捷化企业希望自己的软件开发过程能以超音速、甚至于星际穿梭的速度,来快速响应各种变化,但同时还要保证安全性。DevOps流水线无疑为这一目标提供了最佳实践。

但是,要完全满足这样的需求,我们应该如何去建立合适的DevOps流水线呢?有没有一种很好的方式,能够帮助我们去理解DevOps流水线当中CI/CD过程,以及容器技术,如Docker和Kubernetes,在其中的角色和影响呢?

其实,DevOps流水线的建设可以类比为两种模式:火箭式或飞机式。从众多客户的应用实践来看,要想运行一个完善的、可靠的DevOps流水线,火箭式的建设是远远不够的,实际遇到的困难要大得多。

二、火箭发射模式

我们通常把DevOps流水线理解为一个简单的、从左到右的线性过程:编写代码、提交、构建、测试、部署,以及作为产品发布。创建出来的软件在流水线当中就像被装船运送一样,有着清晰定义的去向。

 

在这种模式下,创建一个应用程序就像发射登陆火星的火箭一样。在允许登陆任务继续进行之前,必须要计划、审批、测试,以及验证所有的工作。为了准备发射,所有团队必须紧密地协同工作。我们只有一次机会去发射这个火箭,而且火箭一旦发射,就再没有机会进行修改和更新。从发射台到最终到达火星,火箭的功能是固定的、不可修改的。

火箭发射是一次性的。每一次测试都需要创建一个新的火箭,而这会带来一些未知的风险。新的火箭需要在确认所有系统都准备好了之后才能发射,以完成承担的任务。

这是一个清晰的、经典的工程模型。但是,这不是DevOps应有的工作方式。

三、航班运行模式

上述火箭模式中比较好的DevOps实践是在创建和运行服务时,开发和运维团队在研发生命周期的各个阶段都紧密地合作。

然而,DevOps并不是像高风险火箭发射那样简单的从左到右的线性过程。相反,它是一个频繁触发且不会终止的循环过程,而且每一次触发都会引入一些新的的风险。所以,DevOps更像是现代的航空系统。在任何时间,全球的航空公司都频繁地,事实上是连续地,发送着航班,运载着上百万对其充满信任的旅客。

 

航空公司管理其飞机和航线的流程,和DevOps流水线保证应用发布时效性和可靠性的方法是十分相像的。和软件企业一样,航空公司一样要紧跟技术的发展、快速响应安全问题,以及适应客户需求的变化,同时还要保证整个系统不中断地运行。在日常运营中,航空公司持续对每架飞机进行检查(测试)、维护(打补丁),以及安排运营时间。这个过程和DevOps中应用的持续集成、更新和交付过程非常类似的。

和火箭发射的一次性不同,飞机能够反复地执行起飞和下降,最终执行航线任务的和最初通过测试飞行的都是同一架飞机,这充分表明了两种模式的差异性。那么,怎样才能保证DevOps流水线运转得更像是一个航空公司,而不是一名火箭兵呢?

四、起飞前的清理工作

火箭和飞机都是由许多资源组成的产品,不同的生产部门分别负责结构、机械和控制系统等各个部分,而且是由不同的供应商提供大部分的组件,从最基本的如门锁、座椅、地毯等,直到复杂的如导航系统等。

类似的,每一个软件应用也是由组织内的多个团队共同创建的,而且应用运行的大部分代码,例如操作系统和语言框架等,都是以企业外部的远程仓库为来源的。

开发人员可以控制将自己开发代码的哪些部分加入构建。但是,对于从公共仓库下载的外部依赖包,如npm或maven等,又该如何控制呢?那些包可能会不定期的进行修改,而这是你无法控制的。

如果你的DevOps流水线像发射火箭一样,那么针对测试或发布的每次新构建,都会成为偷渡者潜入火箭的机会。如果无法做到持续监控,那些计划之外的东西就可能进入新的构建,从而导致每次构建都不能获得完全相同的结果。

五、前往跑道

Docker镜像就像是飞行器,不管是火箭还是飞机,通过构建而成,并封装了应用要执行的所有功能。从发射到着陆,Docker镜像的能力保持不变。

Docker引擎,结合镜像仓库,把镜像转换为容器,就像把这些飞行器推送到发射平台。而像Kubernetes这样的编排工具就进一步把这些容器发射到航线上去。

Docker引擎,结合镜像仓库,把镜像转换为容器,就像把这些飞行器推送到发射平台。而像Kubernetes这样的编排工具就进一步把这些容器发射到航线上去。

如果像处理火箭一样,每次发射一个新的,那在开发、测试,或发布阶段构建的每个Docker镜像,都有可能和前一个有一些不同。

 

而如果像飞机一样处理,就意味着对发射到空中的内容有更大的确定性。航空公司不会为每次起飞都制造一架新的飞机。他们只是测试飞机,然后在航线上可靠地运行飞机,直到飞机需要更换为止。

构建一个Docker镜像,然后在测试到发布的流水线上进行升级,而不是重新构建,能够确保这个镜像带上航线的都能每次、准时、安全地飞翔。

 

六、地面控制

正如上述分析的,真正的DevOps流水线不是简单的从左到右的线性过程,而是设计、分解和重构的复杂、迭代、网络化的过程。

为了使DevOps流水线运转得更像是航空公司,Artifactory可以作为地勤人员,来保证一切都按计划、平稳地运行。Artifactory使得开发人员能够控制从代码构建而来的Docker镜像,并通过总是在航线中运行同一架飞机来保证可靠性和速度。

首先,Artifactory解决了一个开发人员面对的关键挑战,那就是利用如npm或maven这样的外部依赖,也能进行持久、确定的构建。利用Artifactory,开发人员能够维护外部仓库的本地缓存,从而保证外部代码的变化在没有确认可用之前不会被引入到构建当中。而且,保证外部依赖的本地访问,也能帮助加速构建,提升生产力。

利用Artifactory作为Docker镜像中心,可以使得DevOps流水线中从测试到发布的各个阶段之间升级实不可变的构建产出变得更加容易,而不需要每次都重新构建。同样可以根据需要,利用Artifactory为每次构建存储的详尽信息,来创建新的确定性的构建。

飞机飞行需要燃料,而航空公司,就像所有现代化企业一样,基于数据进行运维。航空公司对他们的飞机、客户和工作人员了解得越多,就越能帮助他们最大限度地提高投资回报率。

Artifactory作为Kubernetes的Docker镜像中心,可以提供简化、安全实施运维工作所需的数据。除了容器、镜像的列表之外,Artifactory还可以可视化地展示容器里都有什么,以及这些内容是如何进入容器的。

如果再增加利用JFrog Xray对镜像进行扫描,就可以获得更多有关镜像当中代码真实安全性的信息,进一步保护企业和应用。

七、翱翔天空

不仅仅像本文说的,真正的DevOps流水线就像是现代化的航空公司,DevOps已成为大多数航空公司自身运营的关键实践。联合航空公司、西南航空公司、阿拉斯加航空公司和捷蓝航空公司就是众多航空公司的代表,它们已经为DevOps和CI/CD做了大量的投资,在网络和移动平台上为其客户提供购票、值机、登机等各种服务。这些航空公司很多都引入了Artifactory,获得了在全球范围发展DevOps的巨大收益。

正如这些高风险企业所展示的,仅仅将Docker容器推上跑道,对于真正的DevOps是不够的,需要利用Artifactory为CI/CD提供的支持,来确保它们在天空中安全地飞翔。

 

 

更多技术分享请关注      JFrog杰蛙在线课堂

1月2日在线课堂:《版本控制管理与最佳实践》

课堂收益:

  1. 如何使用版本控制系统
  2. 各种分支模型有什么优缺点,什么样的分支模型更适用于您的环境
  3. 版本控制应该遵循的一些标准

 

报名链接:https://www.bagevent.com/event/6309332

抽奖活动:

课堂结束前五分钟,进行抽奖活动

第一名:小爱音箱

第二名:JFrog杰蛙新版T恤

第三名:JFrog杰蛙新版T恤

API Gateway Kong在Rainbond上的部署

好雨云阅读(3218)评论(0)

什么是Kong

kong

Kong是一个可扩展的开源API平台(也称为API网关,API中间件或微服务服务网格)。Kong最初是由Kong Inc.(以前称为Mashape)实现的,用于为其API Marketplace维护、管理和扩展超过15,000个微服务,这些微服务每月产生数十亿个请求。

技术上讲,Kong是在Nginx中运行的Lua应用程序,并且通过lua-nginx-module实现。Kong是与OpenResty一起分发的,而不是使用此模块来编译Nginx,OpenResty已经包括lua-nginx-module。

了解更多有关Kong的事情,你需要点击了解一下

从应用市场安装

快速安装

目前我们已经将最新版本(v1.4.X)的Kong发布到了应用市场,如果你想要快速的搭建以及使用Kong,你只需要做一件事情,那就是点击一下安装:

Kong-install

等待一小段时间后,Kong就已经部署在了你的Rainbond集群中了。在这个应用中,我们已经集成了Konga作为UI管理工具,接下来的步骤,需要你访问Konga,做几步简单的设置,就可以愉快的探索Kong了。

  • 注册Konga

  • 配置Kong的连接地址,写入 http://127.0.0.1:8001即可

  • 连接成功,就可以使用Konga来管理你的Kong了

注意事项

  • 如果你所使用的Rainbond平台,是在2019年12月25日以前安装的,并且没有进行过任何升级操作,那么你可能遇到Konga连接不到Kong的问题,解决的方案如下:

    • 如果你使用了v5.1.9以前的版本,请升级到最新版本

    • 如果你现在已经在使用v5.1.9版本,那么请点击链接,打个补丁。

  • Kong的启动很消耗内存

配置Kong

环境变量

Kong支持以KONG_开头的环境变量进行配置。举例说明:

对于部署在Rainbond上的Kong来说,直接添加环境变量

即可在Kong的配置文件中生成

添加完成后,点击更新,即可使之生效。

注入Nginx配置

通过调整Kong实例的Nginx配置,可以优化其基础架构的性能。

Kong启动时,将构建一个Nginx配置文件。你可以通过Kong配置直接将自定义Nginx配置注入此文件。

注入单个Nginx配置

Kong的配置文件中的任何前缀为的条目nginx_http_nginx_proxy_nginx_admin_通过删除前缀将其转换为等效的Nginx指令,并将其添加到Nginx配置的相应部分:

  • 前缀为的条目nginx_http_将注入到整体http 块指令中。

  • 前缀为的条目nginx_proxy_将注入到server处理Kong代理端口的block指令中。

  • 前缀为的条目nginx_admin_将注入到server处理Kong的Admin API端口的block指令中。

例如,如果将以下行添加到kong.conf文件中:

它将以下指令添加到serverKong的Nginx配置的代理块中:

为了达到这个目的,你需要参考环境变量,为Kong添加以下环境变量:

通过注入的Nginx指令包含文件

对于更复杂的配置方案,例如添加整个新 server块,可以使用上述方法include向Nginx配置注入 指令,指向包含其他Nginx设置的文件。

可以通过在kong.conf文件中添加以下条目来包含目标文件:

在Rainbond上,可以通过环境变量进行配置:

Kong应用怎么制作

即点即用的Kong,使用起来非常方便。那么这个应用是怎么制作的呢?

我们只需要做到以下几点,就可以发布出可以即点即用的云市场应用:

  • 目标应用的所有组件都已经部署在Rainbond并正常运行

  • 各服务组件使用的数据库具备自动初始化数据的功能

  • 各服务组件间的依赖关系已经处理妥当,从业务层面已经正常运行

接下来,只需要参考应用分享与发布,将你的应用发布出去即可。

数据库自动初始化

Kong可以使用的数据库包括 Postgres与Cassandra,我们这里使用了前者。

首先利用docker run 命令快速部署一个Postgresql:

使用Kong的镜像,即可初始化数据库表结构,在部署初始化组件时,要记得建立从 kong-init 指向 kong-database的依赖关系:

docker run --rm \
    --name kong-init \
    --link kong-database:kong-database \
    -e "KONG_DATABASE=postgres" \
    -e "KONG_PG_HOST=127.0.0.1" \
    -e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \
    kong kong migrations bootstrap

kong-init 运行完成后会自动退出,在Rainbond上显示运行异常,不用担心,它已经完成了使命,验证下 kong-database 中已存在数据表结构,就可以删除kong-init 了。

进入 kong-database 的容器实例,通过命令行工具备份出数据库。

pg_dump -U kong -d kong > /var/lib/postgresql/data/data.sql

找到 kong-database 的数据持久化目录,得到 data.sqlkong-database 的使命也就达成了,可以被关闭删除。

自定义一个代码仓库,参考 https://github.com/dazuimao1990/pri-postgresql/tree/kongdata.sql 放到 sql 目录下,即可用这份代码创建一个可以自动初始化表结构的Postgresql了。创建之,命名为 kong-postgres 备用。

部署Kong

直接使用docker run 命令创建Kong,要记得建立从 Kong 指向 kong-postgres 的依赖关系:

docker run -d --name kong \
    --link kong-database:kong-database \
    -e "KONG_DATABASE=postgres" \
    -e "KONG_PG_HOST=127.0.0.1" \
    -e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \
    -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
    -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
    -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \
    -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
    -e "KONG_ADMIN_LISTEN=0.0.0.0:8001, 0.0.0.0:8444 ssl" \
    -p 8000:8000 \
    -p 8443:8443 \
    -p 8001:8001 \
    -p 8444:8444 \
    kong

内存至少提高至4G。

部署Konga

直接使用docker run 命令创建Konga,要记得建立从 Konga 指向 Kong 的依赖关系:

docker run -p 1337:1337 \
             --name konga \
             pantsel/konga

完成后,最终拓扑将会是这个样子的:

发布应用

点击 发布到市场,编辑它的信息,即可发布了。

解读容器 2019:把“以应用为中心”进行到底

alicloudnative阅读(10555)评论(0)

作者 | 张磊  阿里云高级技术专家、CNCF 官方大使,Kubernetes 项目资深成员和联合维护者

在下一个十年交替之际,你是否知道,这个看似波澜不惊的云原生技术生态,又在孕育和经历着哪些全新的变革呢?

前言

  • 在这一年,这个生态具有标志性意义的 KubeCon,史无前例的吸引到了一万两千人涌入圣地亚哥,整个会议的赞助商列表,多到一张十余米长的巨幅海报才堪堪放下。
  • 在这一年,Kubernetes 终于成为了广受认可的基础设施领域工业标准,而这个标准的确立,则以 AWS 的重量级投入画上了圆满的句号。
  • 在这一年,在社区头部参与者的持续推进下,“规模”与“性能”终于成为了 Kubernetes 项目的重要关键词,这不仅真正意义上打通了 Kubernetes 在企业生产环境中大规模落地的最后一公里,也让 Kubernetes 第一次成为了 “双11” 等顶级互联网规模化场景中实实在在的技术主角。

在下一个十年交替之际,你是否知道,这个看似波澜不惊的云原生技术生态,又在孕育和经历着哪些全新的变革呢?

规模:Kubernetes 项目的新名片

如果要提名 2019 年的云原生技术演进的重要节点,那么“规模”一定是其中最当仁不让的关键词。

出于设计理念上的侧重点考虑,Kubernetes 项目在过去乃至到 2019 年之前的很长一段时间内,也并不把“规模”作为整个项目演进的核心优先级来对待。这里的主要原因在于 Kubernetes 的设计思想是“以应用为中心”的基础设施,所以相比于传统作业编排调度管理项目(比如 Mesos 和 Yarn 等)关注的资源效能问题,Kubernetes 的核心竞争力则一直放在工作负载描述、服务发现、容器设计模式等更高纬度的应用基础设施构建上。当然,另一方面的原因在于,对于 Kubernetes 服务提供商,比如 GKE 来说,他们对于规模与性能诉求也是比较有限的。

这个状态,随着 2019 年初顶级互联网公司对 Kubernetes 社区的重量级投入而被打破。事实上,Kubernetes 本身的规模与性能优化并不是一个“不可解”的问题,但是整个社区长期以来欠缺大规模场景和专门的工程力量投入,最终使得发现、诊断和修复整个容器编排与管理链路中各个性能障碍成了一个遥不可及的事情。

在这条关键链路当中, Kubernetes 的规模性问题又可以细分为:以 etcd 为代表的“数据面”、以 kube-apiserver 为代表“管控面”和以 kubelet 及各种 Controller 组成的“生产 / 消费面”三个问题域。在场景的推动下,Kubernetes 以及 etcd 社区在过去的一年里,正是围绕这三个问题域进行了大量的优化,例如:

  • 数据面

通过优化 etcd 底层数据库的数据结构和算法,将 etcd 百万键值对随机写性能提升 24 倍。

  • 管控面

为 kube-apiserver 添加 Bookmark 机制,将 APIServer 重启时需要重新同步的事件降低为原来的 3%,性能提高了数十倍。

  • 生产 / 消费面

将 Kubernetes 节点向 APIServer 的心跳机制从“定时发送”修改为“按需发送”,从而大大减少了规模化场景下 kubelet 对 APIServer 带来的巨大压力,大幅提高了 Kubernetes 所能支持的节点数目上限。

除此之外,在规模化 Kubernetes 落地的具体场景中,围绕着上述三个问题域、在生产环境中经历了充分验证的规模与性能优化实践也在 KubeCon 等技术演讲中浮出水面。比如:如何让多个 kube-apiserver 更均衡的处理生产 / 消费请求、避免性能热点;如何通过合理的设置主备 Controller,让升级 Controller 时无需大量重新同步数据,从而降低 controller 恢复时对 API Server 的性能冲击等等。

这些 Kubernetes 规模与性能领域的“全链路优化”工作,几乎全部源自于真实的互联网级规模化场景,而它们最终完成的得益于顶级开源社区的协同与所有参与者的共同努力。而规模化与性能问题的逐步解决不仅仅带来了为 Kubernetes 项目带来了充足的底气,也正在迅速改变着整个基础设施领域的基本格局。

2019 年 5 月,Twitter 公司在旧金山总部正式宣布 Twitter 的基础设施将从 Mesos 全面转向 Kubernetes。 这个新闻仿佛给当时略显沉闷的技术社区里扔进了一颗重磅炸弹一般,一时间传言四起。

事实上,早在一年前,Twitter 公司的工程师就已经成为了湾区“大规模集群管理小组( CMWS, Cluster Mgmt at Web Scale)”里的重要成员和分享常客。 CMWS 是一个专门针对大规模场景下的集群管理问题而成立的闭门组织,其创始成员包括了阿里巴巴、Pinterest、Linkedin、Netflix、Google、Uber、Facebook、Apple 等一大批全球顶级技术公司。该小组成员会在每个月举行闭门 Meetup,围绕具体的议题进行深度技术分享和交流,推动小组成员更快更好的在互联网场景中落地 Kubernetes 技术体系。众所周知,出于规模和性能的考虑,湾区的互联网公司一直以来都是 Mesos 项目的重度用户,而此次 Twitter 的转变,实际上只是 CMWS 小组成员中的一位而已。

云原生的本质与误区

尽管一路高歌猛进,但其实哪怕在 2019 年,还是有很多人对“云原生”充满了疑惑甚至误解。这想必也是为何,我们一直能够在不同的场合听到关于云原生的各种不同的定义。有人说,云原生就是 Kubernetes 和容器;也有人说,云原生就是“弹性可扩展”;还有人说,云原生就是 Serverless;而后来,有人干脆做出判断:云原生本身就是“哈姆雷特”,每个人的理解都是不一样的。

实际上,自从这个关键词被 CNCF 和 Kubernetes 技术生态“借用”之初,云原生的意义和内涵,就是非常确定的。在这个生态当中,云原生的本质是一系列最佳实践的结合;更详细的说,云原生为实践者指定了一条低心智负担的、能够以可扩展、可复制的方式最大化地利用云的能力、发挥云的价值的最佳路径。

所以说,云原生并不指代某个开源项目或者某种技术,它是一套指导软件与基础设施架构设计的思想。这里关键之处在于,基于这套思想构建出来的应用和应用基础设施,将天然能够与“云”天然地集成在一起,将“云”的最大能力和价值发挥出来。

这种思想,以一言以蔽之,就是“以应用为中心”。

正是因为以应用为中心,云原生技术体系才会无限强调让基础设施能更好的配合应用、以更高效方式为应用“输送”基础设施能力,而不是反其道而行之。而相应的, Kubernetes 、Docker、Operator 等在云原生生态中起到了关键作用的开源项目,就是让这种思想落地的技术手段。

以应用为中心,是指导整个云原生生态和 Kubernetes 项目蓬勃发展至今的重要主线。

“下沉”的应用基础设施能力与 Service Mesh

带着这样一条主线,我们回过头来重新审视 2019 年云原生生态的技术演进,会稍微清晰一些。

大家可能听说过,在这次以 Kubernetes 为代表的基础设施领域的演进过程中,总是伴随着一个重要的关键词,那就是应用基础设施能力的“下沉”。

在过去,我们编写一个应用所需要的基础设施能力,比如,数据库、分布式锁、服务注册 / 发现、消息服务等等,往往是通过引入一个中间件库来解决的。这个库,其实就是专门的中间件团队为你编写的服务接入代码,使得你可以在不需要深入了解具体基础设施能力细节的前提下,以最小的代价学习和使用这些基础设施能力。这其实是一种朴素的“关注点分离”的思想。不过更确切的说,中间件体系的出现,并不单单是要让“专业的人做专业的事”,而更多是因为在过去,基础设施的能力既不强大、也不标准。这就意味着,假如没有中间件来把这些的基础设施细节给屏蔽掉、把接入方式统一掉,业务研发就必须“被迫营业”,去学习无数晦涩的基础设施 API 和调用方法,对于“生产力就是一切”的研发同学来说,这显然是不可接受的。

不过,基础设施本身的演进过程,实际上也伴随着云计算和开源社区的迅速崛起。时至今日,以云为中心、以开源社区为依托的现代基础设施体系,已经彻底的打破了原先企业级基础设施能力良莠不齐、或者只能由全世界几家巨头提供的情况。

这个变化,正是云原生技术改变传统应用中间件格局的开始。更确切的说,原先通过应用中间件提供和封装的各种基础设施能力,现在全都被 Kubernetes 项目从应用层“拽”到了基础设施层也就是 Kubernetes 本身当中。而值得注意的是,Kubernetes 本身其实也不是这些能力的直接提供者, Kubernetes 项目扮演的角色,是通过声明式 API 和控制器模式把更底层的基础设施能力对用户“暴露”出去。这些能力,或者来自于“云”(比如 PolarDB 数据库服务);或者来自于生态开源项目(比如 Prometheus 和 CoreDNS)。

这也是为什么 CNCF 能够基于 Kubernetes 这样一个种子迅速构建起来一个数百个开源项目组成的庞大生态的根本原因:Kubernetes 从来就不是一个简单的平台或者资源管理项目,它是一个分量十足的“接入层”,是云原生时代真正意义上的“操作系统”。

可是,为什么只有 Kubernetes 能做到这一点呢?

这是因为, Kubernetes 是第一个真正尝试以“应用为中心“的基础设施开源项目。

以应用为中心,使得 Kubernetes 从第一天开始,就把声明式 API 、而不是调度和资源管理作为自己的立身之本。声明式 API 最大的价值在于“把简单留给用户,把复杂留给自己”。通过声明式 API,Kubernetes 的使用者永远都只需要关心和声明应用的终态,而不是底层基础设施比如云盘或者 Nginx 的配置方法和实现细节。注意,这里应用的“终态”,不仅仅包括应用本身的运行终态,还包括了应用所需要的所有底层基础设施能力比如路由策略、访问策略、存储需求等所有应用依赖的终态。

这,正是以“应用为中心”的切实体现。

所以说,Kubernetes 并没有让中间件消失,而是把自己变成了一种“声明式的”、“语言无关的”中间件,这正是应用基础设施能力“下沉”的真实含义。

应用基础设施能力“下沉”,实际上伴随着整个云原生技术体系和 Kubernetes 项目的发展始终。比如, Kubernetes 最早提供的应用副本管理、服务发现和分布式协同能力,其实就是把构建分布式应用最迫切的几个需求,通过 Replication Controller,kube-proxy 体系和 etcd “下沉”到了基础设施当中。而 Service Mesh ,其实就更进一步,把传统中间件里至关重要的“服务与服务间流量治理”部分也“下沉”了下来。当然,这也就意味着 Service Mesh 实际上并不一定依赖于 Sidecar :只要能无感知的拦截下来服务与服务之间的流量即可(比如 API Gateway 和 lookaside 模式)。

随着底层基础设施能力的日趋完善和强大,越来越多的能力都会被以各种各样的方式“下沉”下来。而在这个过程中,CRD + Operator 的出现,更是起到了关键的推进作用。 CRD + Operator,实际上把 Kubernetes 声明式 API 驱动对外暴露了出来,从而使得任何一个基础设施“能力”的开发者,都可以轻松的把这个“能力”植入到 Kubernetes 当中。当然,这也就体现了 Operator 和自定义 Controller 的本质区别:Operator 是一种特殊的自定义 Controller,它的编写者,一定是某个“能力”对应的领域专家比如 TiDB 的开发人员,而不是 K8s 专家。遗憾的是当前的 Operator Framework 的发展并没有体现出这个深层含义:太多的 K8s Controller 细节被暴露给了 Operator 的开发者,这是不对的。

在 2019 年,Service Mesh 生态取得了长足的进步,并且从原本 Istio 的绝对统治地位,来到了更接近于“诸侯争鸣”的局面。毕竟,“中间件”这个生态,在过去也很难出现完全一家独大的状态,而 Google “一如既往”的宣布 Istio 项目暂时不捐赠给任何一个开源社区,其实也只是给这个趋势增加一个推波助澜的作用而已。其实,作为这波 Service Mesh 浪潮中应用基础设施能力“下沉”的集大成者,Istio 项目已经在跟 Kubernetes 项目本身产生越来越多的“局部冲突”(比如跟 kube-proxy 体系的关系)。在未来,具体某种“声明式中间件”的能力是 Kubernetes Core 还是 Mesh 插件来提供,可能会有更多的争论出现,而在这个过程中,我们也会看到 Istio 更多的“侵入”到 Kubernetes 的网络甚至容器运行时层面当中,这将使得基础设施变得越来越复杂、越来越“黑科技”化。

声明式应用基础设施的主旋律

所以一个不争的事实是,Kubernetes 项目的其实会越来越复杂、而不是越来越简单。

更确切地说,“声明式基础设施”的基础,就是让越来越多的“复杂”下沉到基础设施里,无论是插件化、接口化的 Kubernetes “民主化”的努力,还是容器设计模式,或者 Mesh 体系,这些所有让人兴奋的技术演进,最终的结果都是让 Kubernetes 本身变得越来越复杂。而声明式 API 的好处就在于,它能够在基础设施本身的复杂度以指数级攀升的同时,至少保证使用者的交互界面复杂度仍然以线性程度上升。否则的话,如今的 Kubernetes 恐怕早就已经重蹈了 OpenStack 的灾难而被人抛弃。

“复杂”,是任何一个基础设施项目天生的特质而不是缺点。今天的 Linux Kernel 一定比 1991 年的第一版复杂不止几个数量级;今天的 Linux Kernel 开发人员也一定没办法像十年前那样对每一个 Module 都了如指掌。这是基础设施项目演进的必然结果。

但是,基础设施本身的复杂度,并不意味着基础设施的所有使用者都应该承担所有这些复杂度本身。这就好比我们每一个人其实都在“使用” Linux Kernel,但我们并不会抱怨“Linux Kernel 实在是太复杂了”:很多时候,我们甚至并不知道 Linux Kernel 的存在。

为了更好的说明 Kubernetes 的“复杂性”问题,我们需要先阐述一下当前的 Kubernetes 体系覆盖的三类技术群体:

  • 基础设施工程师

在公司里,他们往往称作“Kubernetes 团队”或者“容器团队”。他们负责部署、维护 Kubernetes 及容器项目、进行二次开发、研发新的功能和插件以暴露更多的基础设施能力。他们是 Kubernetes 与容器领域里的专家,也是这个生态里的中坚力量。

  • 运维工程师(含运维研发和 SRE)

他们以及他们开发的工具和流水线(Pipeline),是保障关键业务稳定和正确运行的基石,而 Kubernetes 项目对他们来说,则是供给和研发运维能力的核心基础依赖。理想情况下,运维工程师是 Kubernetes 项目的最重要使用者。

  • 业务研发工程师

当前的 Kubernetes 项目用户中,业务研发的比重很小。他们本身对基础设施并不感冒,但是也有可能被 Kubernetes 的“声明式中间件”的能力被吸引,逐步接受了依赖 Kubernetes 提供的基础设施原语来编写和部署应用。

上述三种使用群体在不同的公司内可能会有重合,而 Kubernetes 与生俱来的“复杂”,却对上述三种使用者都有着深远的影响。在 2019 年,云原生生态也正在从上述三个角度出发,试图解决 Kubernetes 复杂度对不同使用者带来的问题。

Serverless Infrastructure 方兴未艾

自然的,Kubernetes 生态首先希望解决的是基础设施工程师面对的复杂度。在这里声明式 API 其实已经帮了很大的忙,但是部署和运维 Kubernetes 本身还是一个挑战。这正是类似 kubeadm 、kops 项目,以及 Kubernetes 托管项目和服务(比如 GKE,ACK,EKS 等)的重要价值点。

另一方面,在帮助基础设施工程师缓解复杂度问题的过程中,有一部分公有云提供商逐步发现了这样一个事实:Kubernetes 工程师实际上也不希望关心更底层基础设施比如网络、存储、宿主机的细节和特性。这就好比一个电器工程师,怎么会去关心发电厂里的事情呢?

所以很多公有云提供商先后推出了 Serverless Kubernetes 的服务。这种服务保留了 Kubernetes 的 Control Plane,但是把 Kubernetes 中的 kubelet 组件、以及相应的网络、存储等诸多 IaaS 相关概念去掉,然后用 Virtual Kubelet 项目把 Kubernetes Control Plane 直接对接到一个叫做 Serverless Container 的服务上来直接接入 IaaS 的能力。所谓 Serverless Container,实际上就是把虚拟机以及相应的网络存储等依赖,通过一个容器 API 暴露出来,比如 Microsoft 的 ACI,AWS 的 Fargate 以及阿里云的 ECI。由于从用户视角来看,他看到的效果是 Kubernetes 里面的 Node 被去掉了,所以这种服务方式又被称作“Nodeless”。

Nodeless 从 Kubernetes 中移除最让人头疼的节点和资源管理部分,所以它在使用上非常简单,底层资源的问题也完全不必用户操心,可谓省心省力无限弹性;而相应的折衷是 kubelet 的缺失会为 Kubernetes 的功能完整性打上折扣。

在 2019 年底,AWS 在 Re:Intent 大会上宣布的 EKS on Fargate 服务之后,迅速在业界引起了巨大的反响。EKS on Fargate 的设计跟 Serverless Kubernetes 是类似的,主要差异点在于它并没有使用 Virtual Kubelet 来直接去掉 Node 的概念,而是依然使用 kubelet 向 Fargate 服务申请 EC2 虚拟机当 Pod 用,从而更好的保留了 Kubernetes 功能完整度。这种方式,我们可以称作 Serverless Infrastructure,即:一个不需要关心底层基础设施细节的 Kubernetes。

实际上,在 2019 年中旬,阿里就已经在 Kubernetes 社区提出了 Virtual Cluster 架构。它的核心思想是,在一个“基础 Kubernetes 集群(元集群)”上,可以“虚拟出”无数个完整的 Kubernetes 集群来给不同的租户使用。可以看到,这个思路跟 EKS on Fargate 的设计不谋而合但走的更远:Fargate 目前需要通过 EC2 虚拟机和对应的虚拟机管控系统来实现容器运行时的隔离,而 Virtual Cluster 则直接通过 KataContainers 使得所有租户可以安全的共享同一个宿主机,从而大大提高了资源利用率(这也正是 KataContainers 最大的魅力所在)。不过,相信很快 EKS on Fargate 也会通过 Firecracker 逐步走向 Virtual Cluster 架构。与 Virtual Cluster 类似的另一个商业产品,就是 VMware 的 Project Pacific:它通过“魔改” kubelet,在 vSphere 之上实现了“虚拟出” 租户 Kubernetes 集群的目的。

作为开源界的 EKS on Farge 和 Project Pacific,Virtual Cluster 目前是 Kubernetes 上游多租工作组的核心孵化项目并且已经进行过多次 PoC,非常值得关注。

构建 “以应用为中心”的运维体系

从 Nodeless 到 Virtual Cluster,2019 年的云原生生态正在全力以赴的解决基础设施工程师的烦恼。不过,更加重要的运维工程师和业务研发所面临的问题,却似乎一直被忽视至今。要知道,他们其实才是抱怨“Kubernetes 复杂”的主要群体。

这里的本质问题在于,Kubernetes 项目的定位是“The Platform for Platform”,所以它的核心功能原语服务的对象是基础设施工程师,而不是运维和研发;它的声明式 API 设计、CRD Operator 体系,也是为了方便基础设施工程师接入和构建新基础设施能力而设计的。这就导致作为这些能力的使用者和终端受益者,运维工程师和业务研发实际上跟 Kubernetes 核心定位之间存在着明显的错位;而现有的运维体系和系统,跟 Kubernetes 体系之间也存在着巨大的鸿沟。

为了解决这个问题,很多公司和组织落地 Kubernetes 的时候实际上采用了“ PaaS 化”的思路,即:在 Kubernetes 之上再构建一个 PaaS 系统,通过 PaaS API(或者 UI)把 Kubernetes 跟业务运维和业务研发隔离开来。这样做的好处在于,Kubernetes 的基础设施能力真的就变成了“基础设施”:业务运维和研发真正学习和使用的是这个 PaaS,Kubernetes 的“复杂性”问题也迎刃而解了。

但这种做法从本质上来说,其实跟 Kubernetes “以应用为中心”的设计是不一致的。Kubernetes 一旦退化成了“类 IaaS 基础设施”,它的声明式 API、容器设计模式、控制器模型就根本无法发挥出原本的实力,也很难接入到更广泛的生态。在 Kubernetes 的世界里,传统 PaaS 提供的各项能力比如 CI/CD、应用打包托管、发布扩容,现在都可以通过一个个的 CRD Controller 的方式作为插件部署在 Kubernetes 当中,这跟应用基础设施“下沉”的过程其实是类似的。

当然,在这个过程中,这些插件就成了如何将 Kubernetes 与现有的运维体系进行对接和融合的关键所在。比如:如何做应用原地升级、固定 IP,如何避免 Pod 被随意驱逐,如何按照公司运维规范统一管理和发布 Sidecar 容器等等。在 2019 年 KubeCon 上海,自定义工作负载开源项目 OpenKruise 的发布,其实正是 Kubernetes 与现有运维体系成功对接的典型案例。一句话:在现代云原生技术体系当中,“运维能力”可以直接通过声明式 API 和控制器模型成为 Kubernetes 基础设施的一部分。所以在大多数情况下,其实并不需要一个运维 PaaS 的存在。

但是,即使做到了“运维能力”云原生化这一点,“以应用为中心”的基础设施其实也才刚刚起步。

把“以应用为中心”进行到底

跟传统中间件从业务研发视角出发不同,云原生乃至整个应用基础设施“下沉”的革命,是从底向上开始的。它起始于 Google Borg/Omega 这样比“云计算”还要底层的容器基础设施构建理念,然后逐层向上透出“以应用为中心”的设计思想。出于基础设施本身与生俱来的高门槛和声明式应用管理理论被接纳的速度,直到 2019 年,社区对 Kubernetes 体系的认识其实才刚刚从“类 IaaS 基础设施”、“资源管理与调度”,堪堪上升到“以应用为中心的运维能力”的维度上来。

当然,这次“以应用为中心”的技术革命,一定不会在“运维”这个节点就戛然而止。那么接下来呢?

实际上,声明式 API 和控制器化,是将底层基础设施能力和运维能力接入 Kubernetes 的手段但并非目的。Kubernetes 打造“以应用为中心”的基础设施真正希望达成的目标,是让”应用“变成基础设施中的绝对主角,让基础设施围绕“应用”构建和发挥作用,而不是反之。

具体来说,在下一代“以应用为中心”的基础设施当中,业务研发通过声明式 API 定义的不再是具体的运维配置(比如:副本数是 10 个),而是“我的应用期望的最大延时是 X ms”。接下来的事情,就全部会交给 Kubernetes 这样的应用基础设施来保证实际状态与期望状态的一致,这其中,副本数的设置只是后续所有自动化操作中的一个环节。只有让 Kubernetes 允许研发通过他自己的视角来定义应用,而不是定义 Kubernetes API 对象,才能从根本上的解决 Kubernetes 的使用者“错位”带来的各种问题。

而对于业务运维来说,下一代“以应用为中心”的基础设施,必须能够从
应用的视角对运维能力进行封装和再发现,而不是直接让运维人员去学习和操作各种底层基础设施插件。**举个例子,对于 kube-ovn 这样一个 Kubernetes 网络插件来说,运维人员拿到的不再是 kube-ovn 本身的使用文档或者 CRD,而是这个插件能够提供的一系列运维能力的声明式描述,比如:“动态 QoS 配置 CRD”、“子网隔离配置 CRD”。然后,运维人员只需要通过这些声明式 API ,为某个应用绑定它所需要的动态 QoS 和子网隔离的具体策略值即可。剩下的所有事情就全交给 Kubernetes 了。

这样,在底层基础设施层逐渐 Serverless 化、运维能力也越来越多的接入到 Kubernetes 体系当中之后,云原生的下一站将会继续朝着“把以应用为中心进行到底”的方向不断迈进。

在 2019 年 4 月,Google Cloud 发布了 Cloud Run 服务,第一次把 Serverless Application 的概念呈现给大众。2019 年 9 月,CNCF 宣布成立应用交付领域小组,宣布将“应用交付”作为云原生生态的第一等公民。 2019 年 10 月,Microsoft 发布了基于 Sidecar 的应用中间件项目 Dapr ,把“面向 localhost 编程”变成了现实。

而在 2019 年 10 月,阿里巴巴则联合 Microsoft 共同发布了 Open Application Mode(OAM)项目,第一次对“以应用为中心”的基础设施和构建规范进行了完整的阐述。在 2019 年,我们已经能够切实感受到一场新的云计算革命正在兴起。在这场革命的终态,任何一个符合“以应用为中心“规范的软件,将能够从研发而不是基础设施的视角自描述出自己的所有属性,以及自己运行所需要所有的运维能力和外部依赖。

这样的应用才能做到天生就是 Serverless Application:它可以被“自动匹配”到任何一个满足需求的“以应用为中心”的运行平台上去运行,我们不再需要关心任何基础设施层面的细节和差异性。

总结

2019 年,在 Kubernetes 技术体系的规模性问题逐渐解决之后,整个云原生生态正在积极的思考和探索如何达成云原生技术理念“以应用为中心”的最终愿景。而从 Serverless Infrastructure 到 Serverless Application,我们已经看到云原生技术栈正在迅速朝“应用”为中心聚拢和收敛。我们相信在不久的未来,一个应用要在世界上任何一个地方运行起来,唯一要做的,就是声明“我是什么”,“我要什么”。在这个时候,所有的基础设施概念包括 Kubernetes、Istio、Knative 都会“消失不见”:就跟 Linux Kernel 一样。

我们拭目以待。

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

你的DevOps中有完善的持续交付体系么?

JFrog捷蛙阅读(3805)评论(0)

背景:

DevOps已经成为软件开发领域一个炙手可热的名词。敏捷开发、持续交付、CI/CD,K8s…这些主流的开发理念、工具无一例外都与DevOps有着很强的联系。这种环境影响下,传统的运维团队均开始向DevOps进行转型。一时之间运维开发、SRE、工程效能工程师需求量大增,无论公司大小,都会开始着手DevOps的从0到1的建设。我们开始搭建工具链、部署流水线、集成自动化测试工具、开发自动化发布系统……一切的一切都是为了完善我们自动化体系,从而提高开发效率,优化产品质量。

那么问题来了,你团队所建设的DevOps体系,已经是完善的DevOps了么?

 

正文:

我们如何去评估目前DevOps中持续交付的建设情况呢,这里笔者总结了一些我们在DevOps初期应该进行的一些工作,这些工作大多数属于工具链的集成,我们可以对照下述十四条关卡,来验证一下我们的DevOps建设中持续交付这一环节是否走在了一条正确的道路上吧。

 

1.  版本控制

版本控制是指通过记录软件开发过程中的源代码、配置信息、环境、数据等,快速的恢复及访问任意一个版本。版本控制最主要的功能就是追踪文件的变更。

常用版本管理工具:git

2. 最优分支策略

分支的种类很多,大多数团队会使用到主干分支、开发分支、临时分支、功能分支、预发布分支、bug修复分支等等。那么如何选择一个最优分支策略,是我们开发团队必须去规范的一件事。分支策略与版本发布频率之间有一定的相关性,我们要根据自己团队开发模式以及项目迭代周期来选择最优的分支策略。

3. 代码静态扫描

代码静态扫描需要从下面几个维度进行检查:复杂度分布、重复代码、单元测试统计、代码规则检查、注释率、潜在BUG、结构与设计。通过在构建pipeline中加设代码静态扫描的质量关卡,确保我们的代码可以达到一个可发布的级别。

常用的代码静态扫描工具:如SonarQube。

4. 80%以上单元测试覆盖率

提高单元测试的意义最重要的一点是保证代码所对应的功能正常、而单元测试覆盖率的检查是以一种约束方式来规范开发人员使用单元测试,通过设置单元测试覆盖率的关卡来保障开发代码的正确性,并让单元测试逐渐地变成开发习惯。

5. 漏洞扫描

每个项目中,高达99.9%的代码来自于外部依赖,为了避免重复造轮子,我们引用了大量的外部开源框架及组件,这些外部依赖是否有安全,是否存在高危漏洞,开发人员一般是无法关注到的,所以我们需要一款产品可以集成到我们开发的IDE、设置成为构建流程pipeline中的质量关卡、无缝的对接到我们的制品库中,来扫描我们所有的外部依赖。

常用漏洞扫描工具:JFrog Xray

6. 制品管理

制品是构建过程的产出物。包括软件包、测试报告、应用配置文件等。制品管理是对软件研发过程中生成的产物的管理,一般作为最终交付物完成发布和交付。你所管理的制品可以统称为二进制文件,制品仓库则可以提供所有二进制文件的管理能力,提供全语言的依赖解析能力以及收集整个软件生命周期的信息与制品关联。

常用制品管理仓库:Artifactory

7. 开源协议扫描

开源协议有上百种,宽松程度不一。是否允许商用、是否可以修改、修改后是否需要继续开源等都是每种协议特有的特性。我们作为用户,作为开发者,为了提高开发效率,避免重复造轮子,难免会引用大量的外部组件及框架,那我们在DevOps建设过程中则必须注意对开源协议的管理及扫描。

常用的开源协议:

  • Apache 2:直接使用须保留原始许可声明,若对其进行修改,需向用户说明。
  • MIT:必须保留原始许可证声明
  • BSD:必须保留原始许可证声明,部分版本不得使用原作者姓名用于软件销售
  • GPL:若包涵GPL代码,整个项目则必须使用GPL许可证。

常用的协议扫描工具:JFrog Xray。

8. 不可变基础设施

不可变基础设施是指任何基础设施实例在部署后永远不会被修改。如果需要以任何方式更新,修复或修改某些内容,则会使用一批新的实例去替换,并在经过验证后,将新的基础设施实例上线,替换掉旧的实例。这种在当年在物理机或虚拟机上无法快速实现的这种不可变基础设施的理念,随着docker的普及正在飞快的发展,我们可以通过容器的方式快速的实现。这种模式可以为我们减少配置管理的负担,并使得 DevOps 更加容易实践

最佳实践方式:Docker

9. 集成测试

集成测试是在单元测试的基础上,把软件单元按照软件概要设计规格说明的规格要求,组装成模块、子系统或系统的过程中各部分工作是否达到或实现相应技术指标及要求。

10. 性能测试

性能测试方法是通过模拟生产运行的业务压力量和使用场景组合,来测试系统的性能是否满足软件的性能要求。通俗地说,这种方法就是要在特定的运行条件下验证软件系统的处理能力。

11. 变更管理

在google的SRE体系中,变更管理是DevOps体系中最为重要的一个部分。根据以往的经验,90%的线上故障是由于线上变更导致的,该变更原因包括软件、硬件、环境等所有因素。建设变更管理体系目的就是为了快速定位线上问题,止损由于变更造成的线上故障,及时通知相关人员做好故障预防工作。所以,变更管理体系也是需要我们重点去建设以及完善的。

落地方式包括但不限于下述几点:

  • 运维人员、对应的开发及测试人员、产品经理等微信通知
  • 大屏滚动播放最近的变更记录
  • 变更记录同步到监控系统

12. 功能开关

功能开关概念很容易理解,通过功能开关我们可以在运行过程中对某一功能进行启动和关闭,在敏捷开发模式下,为了快速迭代,在某些团队没有完全准备好的情况下,我们可以通过功能开关的方式将新功能上线并通过配置屏蔽该功能,直至所有团队准备就绪,整个功能涉及到的服务全部上线后,可以打开此功能开关,提供用户使用。

通过功能开关的方案,我们可以快速迭代,不必进行复杂的集成测试及大版本交付,实现真正的敏捷开发、小步快跑。

13. 较高的接口测试覆盖率

提高接口测试覆盖率就意味着我们可以提高自动化测试的覆盖率,在每次构建流水线中可以自动部署我们的项目,通过接口测试来实现基础的自动化测试。另外接口测试可以提供给运维平台一个监控服务是否稳定的依据,我们可以通过监控平台实时触发接口测试,来判断线上的服务是否依然稳定运行。通过这种接口测试,我们可以最快的速度定位到某些线上某些功能的故障。

14. 零停机发布

无论使用蓝绿部署、还是金丝雀发布,我们的目标只有一个,就是零停机发布。零停机发布是指在对用户不停止服务的前提下进行版本的迭代,修复bug、引用新功能等操作。是否拥有一个健全的零停机发布体系也将是我们DevOps建设中十分重要的一个步骤。

 

 

总结:

DevOps并不是我们建设起来工具链就可以实践的一个理念,文中所介绍的十四个关卡也仅仅是DevOps体系中小小的一部分。开发团队组织架构的合理性、安全风险管理能力、技术运营、需求管理、架构设计、工程师文化等都是我们DevOps建设过程中需要探讨的课题。

 

 

更多技术分享请关注   JFrog杰蛙在线课堂

1月2日在线课堂:《版本控制管理与最佳实践》

课堂收益:

  1. 如何使用版本控制系统
  2. 各种分支模型有什么优缺点,什么样的分支模型更适用于您的环境
  3. 版本控制应该遵循的一些标准

 

报名链接:https://www.bagevent.com/event/6309332

抽奖活动:

课堂结束前五分钟,进行抽奖活动

第一名:小爱音箱

第二名:JFrog杰蛙新版T恤

第三名:JFrog杰蛙新版T恤

Go 开发关键技术指南 | Go 面向失败编程 (内含超全知识大图)

alicloudnative阅读(2977)评论(0)

作者 | 杨成立(忘篱) 阿里巴巴高级技术专家

导读:从问题本身出发,不局限于 Go 语言,探讨服务器中常常遇到的问题,最后回到 Go 如何解决这些问题,为大家提供 Go 开发的关键技术指南。我们将以系列文章的形式推出《Go 开发的关键技术指南》,共有 4 篇文章,本文为第 2 篇。

go.png

Could Not Recover

在 C/C++ 中,

  • 最苦恼的莫过于上线后发现有野指针或内存越界,导致不可能崩溃的地方崩溃;
  • 最无语的是因为很早写的日志打印,比如 %s 把整数当字符串,突然某天执行到了崩溃;
  • 最无奈的是无论因为什么崩溃都导致服务的所有用户受到影响。

如果能有一种方案,将指针和内存都管理起来,避免用户错误访问和释放,这样虽然浪费了一部分的 CPU,但是可以在快速变化的业务中避免这些头疼的问题。在现代的高级语言中,比如 Java、Python 和 JS 的异常,以及 Go 的 panic-recover 都是这种机制。

毕竟,用一些 CPU 换得快速迭代中的不 Crash,怎么算都是划得来的。

哪些可以 Recover

Go 有 Defer, Panic, and Recover。其中 defer 一般用在资源释放或者捕获 panic。而 panic 是中止正常的执行流程,执行所有的 defer,返回调用函数继续 panic;主动调用 panic 函数,还有些运行时错误都会进入 panic 过程。最后 recover 是在 panic 时获取控制权,进入正常的执行逻辑。

注意 recover 只有在 defer 函数中才有用,在 defer 的函数调用的函数中 recover 不起作用,如下实例代码不会 recover:

go
package main

import "fmt"

func main() {
    f := func() {
        if r := recover(); r != nil {
            fmt.Println(r)
        }
    }

    defer func() {
        f()
    } ()

    panic("ok")
}

执行时依旧会 panic,结果如下:

$ go run t.go 
panic: ok

goroutine 1 [running]:
main.main()
    /Users/winlin/temp/t.go:16 +0x6b
exit status 2

有些情况是不可以被捕获,程序会自动退出,这种都是无法正常 recover。当然,一般的 panic 都是能捕获的,比如 Slice 越界、nil 指针、除零、写关闭的 chan。

下面是 Slice 越界的例子,recover 可以捕获到:

go
package main

import (
  "fmt"
)

func main() {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println(r)
    }
  }()

  b := []int{0, 1}
  fmt.Println("Hello, playground", b[2])
}

下面是 nil 指针被引用的例子,recover 可以捕获到:

go
package main

import (
  "bytes"
  "fmt"
)

func main() {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println(r)
    }
  }()

  var b *bytes.Buffer
  fmt.Println("Hello, playground", b.Bytes())
}

下面是除零的例子,recover 可以捕获到:

go
package main

import (
  "fmt"
)

func main() {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println(r)
    }
  }()

  var v int
  fmt.Println("Hello, playground", 1/v)
}

下面是写关闭的 chan 的例子,recover 可以捕获到:

go
package main

import (
  "fmt"
)

func main() {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println(r)
    }
  }()

  c := make(chan bool)
  close(c)
  c <- true
}

Recover 最佳实践

一般 recover 后会判断是否 err,有可能需要处理特殊的 error,一般也需要打印日志或者告警,给一个 recover 的例子:

package main

import (
    "fmt"
)

type Handler interface {
    Filter(err error, r interface{}) error
}

type Logger interface {
    Ef(format string, a ...interface{})
}

// Handle panic by hdr, which filter the error.
// Finally log err with logger.
func HandlePanic(hdr Handler, logger Logger) error {
    return handlePanic(recover(), hdr, logger)
}

type hdrFunc func(err error, r interface{}) error

func (v hdrFunc) Filter(err error, r interface{}) error {
    return v(err, r)
}

type loggerFunc func(format string, a ...interface{})

func (v loggerFunc) Ef(format string, a ...interface{}) {
    v(format, a...)
}

// Handle panic by hdr, which filter the error.
// Finally log err with logger.
func HandlePanicFunc(hdr func(err error, r interface{}) error,
    logger func(format string, a ...interface{}),
) error {
    var f Handler
    if hdr != nil {
        f = hdrFunc(hdr)
    }

    var l Logger
    if logger != nil {
        l = loggerFunc(logger)
    }

    return handlePanic(recover(), f, l)
}

func handlePanic(r interface{}, hdr Handler, logger Logger) error {
    if r != nil {
        err, ok := r.(error)
        if !ok {
            err = fmt.Errorf("r is %v", r)
        }

        if hdr != nil {
            err = hdr.Filter(err, r)
        }

        if err != nil && logger != nil {
            logger.Ef("panic err %+v", err)
        }

        return err
    }

    return nil
}

func main() {
    func() {
        defer HandlePanicFunc(nil, func(format string, a ...interface{}) {
            fmt.Println(fmt.Sprintf(format, a...))
        })

        panic("ok")
    }()

    logger := func(format string, a ...interface{}) {
        fmt.Println(fmt.Sprintf(format, a...))
    }
    func() {
        defer HandlePanicFunc(nil, logger)

        panic("ok")
    }()
}

对于库如果需要启动 goroutine,如何 recover 呢?

  • 如果不可能出现 panic,可以不用 recover,比如 tls.go 中的一个 goroutine:errChannel <- conn.Handshake() ;
  • 如果可能出现 panic,也比较明确的可以 recover,可以调用用户回调,或者让用户设置 logger,比如 http/server.go 处理请求的 goroutine:if err := recover(); err != nil && err != ErrAbortHandler { ;
  • 如果完全不知道如何处理 recover,比如一个 cache 库,丢弃数据可能会造成问题,那么就应该由用户来启动 goroutine,返回异常数据和错误,用户决定如何 recover、如何重试;
  • 如果完全知道如何 recover,比如忽略 panic 继续跑,或者能使用 logger 打印日志,那就按照正常的 panic-recover 逻辑处理。

哪些不能 Recover

下面看看一些情况是无法捕获的,包括(不限于):

  • Thread Limit,超过了系统的线程限制,详细参考下面的说明;
  • Concurrent Map Writers,竞争条件,同时写 map,参考下面的例子。推荐使用标准库的 sync.Map 解决这个问题。

Map 竞争写导致 panic 的实例代码如下:

go
package main

import (
    "fmt"
    "time"
)

func main() {
    m := map[string]int{}
    p := func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println(r)
            }
        }()
        for {
            m["t"] = 0
        }
    }

    go p()
    go p()
    time.Sleep(1 * time.Second)
}

注意:如果编译时加了 -race,其他竞争条件也会退出,一般用于死锁检测,但这会导致严重的性能问题,使用需要谨慎。

备注:一般标准库中通过 throw 抛出的错误都是无法 recover 的,搜索了下 Go1.11 一共有 690 个地方有调用 throw。

Go1.2 引入了能使用的最多线程数限制 ThreadLimit,如果超过了就 panic,这个 panic 是无法 recover 的。

fatal error: thread exhaustion

runtime stack:
runtime.throw(0x10b60fd, 0x11)
    /usr/local/Cellar/go/1.8.3/libexec/src/runtime/panic.go:596 +0x95
runtime.mstart()
    /usr/local/Cellar/go/1.8.3/libexec/src/runtime/proc.go:1132

默认是 1 万个物理线程,我们可以调用 runtime 的 debug.SetMaxThreads 设置最大线程数。

SetMaxThreads sets the maximum number of operating system threads that the Go program can use. If it attempts to use more than this many, the program crashes. SetMaxThreads returns the previous setting. The initial setting is 10,000 threads.

用这个函数设置程序能使用的最大系统线程数,如果超过了程序就 crash,返回的是之前设置的值,默认是 1 万个线程。

The limit controls the number of operating system threads, not the number of goroutines. A Go program creates a new thread only when a goroutine is ready to run but all the existing threads are blocked in system calls, cgo calls, or are locked to other goroutines due to use of runtime.LockOSThread.

注意限制的并不是 goroutine 的数目,而是使用的系统线程的限制。goroutine 启动时,并不总是新开系统线程,只有当目前所有的物理线程都阻塞在系统调用、cgo 调用,或者显示有调用 runtime.LockOSThread 时。

SetMaxThreads is useful mainly for limiting the damage done by programs that create an unbounded number of threads. The idea is to take down the program before it takes down the operating system.

这个是最后的防御措施,可以在程序干死系统前把有问题的程序干掉。

举一个简单的例子,限制使用 10 个线程,然后用 runtime.LockOSThread 来绑定 goroutine 到系统线程,可以看到没有创建 10 个 goroutine 就退出了(runtime 也需要使用线程)。参考下面的例子 Playground: ThreadLimit:

go
package main

import (
  "fmt"
  "runtime"
  "runtime/debug"
  "sync"
  "time"
)

func main() {
  nv := 10
  ov := debug.SetMaxThreads(nv)
  fmt.Println(fmt.Sprintf("Change max threads %d=>%d", ov, nv))

  var wg sync.WaitGroup
  c := make(chan bool, 0)
  for i := 0; i < 10; i++ {
    fmt.Println(fmt.Sprintf("Start goroutine #%v", i))

    wg.Add(1)
    go func() {
      c <- true
      defer wg.Done()
      runtime.LockOSThread()
      time.Sleep(10 * time.Second)
      fmt.Println("Goroutine quit")
    }()

    <- c
    fmt.Println(fmt.Sprintf("Start goroutine #%v ok", i))
  }

  fmt.Println("Wait for all goroutines about 10s...")
  wg.Wait()

  fmt.Println("All goroutines done")
}

运行结果如下:

bash
Change max threads 10000=>10
Start goroutine #0
Start goroutine #0 ok
......
Start goroutine #6
Start goroutine #6 ok
Start goroutine #7
runtime: program exceeds 10-thread limit
fatal error: thread exhaustion

runtime stack:
runtime.throw(0xffdef, 0x11)
    /usr/local/go/src/runtime/panic.go:616 +0x100
runtime.checkmcount()
    /usr/local/go/src/runtime/proc.go:542 +0x100
......
    /usr/local/go/src/runtime/proc.go:1830 +0x40
runtime.startm(0x1040e000, 0x1040e000)
    /usr/local/go/src/runtime/proc.go:2002 +0x180

从这次运行可以看出,限制可用的物理线程为 10 个,其中系统占用了 3 个物理线程,user-level 可运行 7 个线程,开启第 8 个线程时就崩溃了。

注意这个运行结果在不同的 go 版本是不同的,比如 Go1.8 有时候启动 4 到 5 个 goroutine 就会崩溃。

而且加 recover 也无法恢复,参考下面的实例代码。

可见这个机制是最后的防御,不能突破的底线。我们在线上服务时,曾经因为 block 的 goroutine 过多,导致触发了这个机制。

go
package main

import (
  "fmt"
  "runtime"
  "runtime/debug"
  "sync"
  "time"
)

func main() {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println("main recover is", r)
    }
  } ()

  nv := 10
  ov := debug.SetMaxThreads(nv)
  fmt.Println(fmt.Sprintf("Change max threads %d=>%d", ov, nv))

  var wg sync.WaitGroup
  c := make(chan bool, 0)
  for i := 0; i < 10; i++ {
    fmt.Println(fmt.Sprintf("Start goroutine #%v", i))

    wg.Add(1)
    go func() {
      c <- true

      defer func() {
        if r := recover(); r != nil {
          fmt.Println("main recover is", r)
        }
      } ()

      defer wg.Done()
      runtime.LockOSThread()
      time.Sleep(10 * time.Second)
      fmt.Println("Goroutine quit")
    }()

    <- c
    fmt.Println(fmt.Sprintf("Start goroutine #%v ok", i))
  }

  fmt.Println("Wait for all goroutines about 10s...")
  wg.Wait()

  fmt.Println("All goroutines done")
}

如何避免程序超过线程限制被干掉?一般可能阻塞在 system call,那么什么时候会阻塞?还有,GOMAXPROCS 又有什么作用呢?

The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit. This package’s GOMAXPROCS function queries and changes the limit.

GOMAXPROCS sets the maximum number of CPUs that can be executing simultaneously and returns the previous setting. If n < 1, it does not change the current setting. The number of logical CPUs on the local machine can be queried with NumCPU. This call will go away when the scheduler improves.

可见 GOMAXPROCS 只是设置 user-level 并行执行的线程数,也就是真正执行的线程数 。实际上如果物理线程阻塞在 system calls,会开启更多的物理线程。关于这个参数的说明,文章《Number of threads used by goroutine》解释得很清楚:

There is no direct correlation. Threads used by your app may be less than, equal to or more than 10.

So if your application does not start any new goroutines, threads count will be less than 10.

If your app starts many goroutines (>10) where none is blocking (e.g. in system calls), 10 operating system threads will execute your goroutines simultaneously.

If your app starts many goroutines where many (>10) are blocked in system calls, more than 10 OS threads will be spawned (but only at most 10 will be executing user-level Go code).

设置 GOMAXPROCS 为 10:如果开启的 goroutine 小于 10 个,那么物理线程也小于 10 个。如果有很多 goroutines,但是没有阻塞在 system calls,那么只有 10 个线程会并行执行。如果有很多 goroutines 同时超过 10 个阻塞在 system calls,那么超过 10 个物理线程会被创建,但是只有 10 个活跃的线程执行 user-level 代码。

那么什么时候会阻塞在 system blocking 呢?例子《Why does it not create many threads when many goroutines are blocked in writing》解释很清楚,虽然设置了 GOMAXPROCS 为 1,但实际上还是开启了 12 个线程,每个 goroutine 一个物理线程,具体执行下面的代码 Writing Large Block:

go
package main

import (
  "io/ioutil"
  "os"
  "runtime"
  "strconv"
  "sync"
)

func main() {
  runtime.GOMAXPROCS(1)
  data := make([]byte, 128*1024*1024)

  var wg sync.WaitGroup
  for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(n int) {
      defer wg.Done()
      for {
        ioutil.WriteFile("testxxx"+strconv.Itoa(n), []byte(data), os.ModePerm)
      }
    }(i)
  }

  wg.Wait()
}

运行结果如下:

Mac chengli.ycl$ time go run t.go
real    1m44.679s
user    0m0.230s
sys    0m53.474s

虽然 GOMAXPROCS 设置为 1,实际上创建了 12 个物理线程。

有大量的时间是在 sys 上面,也就是 system calls。

So I think the syscalls were exiting too quickly in your original test to show the effect you were expecting.

Effective Go 中的解释:

Goroutines are multiplexed onto multiple OS threads so if one should block, such as while waiting for I/O, others continue to run. Their design hides many of the complexities of thread creation and management.

由此可见,如果程序出现因为超过线程限制而崩溃,那么可以在出现瓶颈时,用 linux 工具查看系统调用的统计,看哪些系统调用导致创建了过多的线程。

Errors

错误处理是现实中经常碰到的、难以处理好的问题,下面会从下面几个方面探讨错误处理:

错误和异常

我们总会遇到非预期的非正常情况,有一种是符合预期的,比如函数返回 error 并处理,这种叫做可以预见到的错误,还有一种是预见不到的比如除零、空指针、数组越界等叫做 panic,panic 的处理主要参考 Defer, Panic, and Recover

错误处理的模型一般有两种,一般是错误码模型比如 C/C++ 和 Go,还有异常模型比如 Java 和 C#。Go 没有选择异常模型,因为错误码比异常更有优势,参考文章《Cleaner, more elegant, and wrong》以及《Cleaner, more elegant, and harder to recognize》。

看下面的代码:

try {
  AccessDatabase accessDb = new AccessDatabase();
  accessDb.GenerateDatabase();
} catch (Exception e) {
  // Inspect caught exception
}

public void GenerateDatabase()
{
  CreatePhysicalDatabase();
  CreateTables();
  CreateIndexes();
}

这段代码的错误处理有很多问题,比如如果 CreateIndexes 抛出异常,会导致数据库和表不会删除,造成脏数据。从代码编写者和维护者的角度看这两个模型,会比较清楚:

_1

错误处理不容易做好,要说容易那说明做错了;要把错误处理写对了,基于错误码模型虽然很难,但比异常模型简单。

_2

如果使用错误码模型,非常容易就能看出错误处理没有写对,也能很容易知道做得好不好;要知道是否做得非常好,错误码模型也不太容易。

如果使用异常模型,无论做的好不好都很难知道,而且也很难知道怎么做好。

 

Errors in Go

Go 官方的 error 介绍,简单一句话就是返回错误对象的方式,参考《Error handling and Go》,解释了 error 是什么?如何判断具体的错误?以及显式返回错误的好处。

文中举的例子就是打开文件错误:

func Open(name string) (file *File, err error)

Go 可以返回多个值,最后一个一般是 error,我们需要检查和处理这个错误,这就是 Go 的错误处理的官方介绍:

if err := Open("src.txt"); err != nil {
    // Handle err
}

看起来非常简单的错误处理,有什么难的呢?稍等,在 Go2 的草案中,提到的三个点 Error HandlingError Values和 Generics 泛型,两个点都是错误处理的,这说明了 Go1 中对于错误是有改进的地方。

再详细看下 Go2 的草案,错误处理:Error Handling 中,主要描述了发生错误时的重复代码,以及不能便捷处理错误的情况。比如草案中举的这个例子 No Error Handling: CopyFile,没有做任何错误处理:

package main

import (
  "fmt"
  "io"
  "os"
)

func CopyFile(src, dst string) error {
  r, _ := os.Open(src)
  defer r.Close()

  w, _ := os.Create(dst)
  io.Copy(w, r)
  w.Close()

  return nil
}

func main() {
  fmt.Println(CopyFile("src.txt", "dst.txt"))
}

还有草案中这个例子 Not Nice and still Wrong: CopyFile,错误处理是特别啰嗦,而且比较明显有问题:

package main

import (
  "fmt"
  "io"
  "os"
)

func CopyFile(src, dst string) error {
  r, err := os.Open(src)
  if err != nil {
    return err
  }
  defer r.Close()

  w, err := os.Create(dst)
  if err != nil {
    return err
  }
  defer w.Close()

  if _, err := io.Copy(w, r); err != nil {
    return err
  }
  if err := w.Close(); err != nil {
    return err
  }
  return nil
}

func main() {
  fmt.Println(CopyFile("src.txt", "dst.txt"))
}

当 io.Copy 或 w.Close 出现错误时,目标文件实际上是有问题,那应该需要删除 dst 文件的。而且需要给出错误时的信息,比如是哪个文件,不能直接返回 err。所以 Go 中正确的错误处理,应该是这个例子 Good: CopyFile,虽然啰嗦繁琐不简洁:

package main

import (
  "fmt"
  "io"
  "os"
)

func CopyFile(src, dst string) error {
  r, err := os.Open(src)
  if err != nil {
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
  }
  defer r.Close()

  w, err := os.Create(dst)
  if err != nil {
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
  }

  if _, err := io.Copy(w, r); err != nil {
    w.Close()
    os.Remove(dst)
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
  }

  if err := w.Close(); err != nil {
    os.Remove(dst)
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
  }
  return nil
}

func main() {
  fmt.Println(CopyFile("src.txt", "dst.txt"))
}

具体应该如何简洁的处理错误,可以读 Error Handling,大致是引入关键字 handle 和 check,由于本文重点侧重 Go1 如何错误处理,就不展开分享了。

明显上面每次都返回的 fmt.Errorf 信息也是不够的,所以 Go2 还对于错误的值有提案,参考 Error Values。大规模程序应该面向错误编程和测试,同时错误应该包含足够的信息。

Go1 中判断 error 具体是什么错误,有以下几种办法:

  • 直接比较,比如返回的是 io.EOF 这个全局变量,那么可以直接比较是否是这个错误;
  • 可以用类型转换 type 或 switch,尝试来转换成具体的错误类型,看是哪种错误;
  • 提供某些函数来判断是否是某个错误,比如 os.IsNotExist 判断是否是指定错误;
  • 当多个错误被糅合到一起时,只能用 error.Error() 返回的字符串匹配,看是否是某个错误。

在复杂程序中,有用的错误需要包含调用链的信息。例如,考虑一次数据库写,可能调用了 RPC,RPC 调用了域名解析,最终是没有权限读 /etc/resolve.conf 文件,那么给出下面的调用链会非常有用:

write users database: call myserver.Method: \
    dial myserver:3333: open /etc/resolv.conf: permission denied

Errors Solutions

由于 Go1 的错误值没有完整的解决这个问题,才导致出现非常多的错误处理的库,比如:

  • 2017 年 12 月, upspin.io/errors,带逻辑调用堆栈的错误库,而不是执行的堆栈,引入了 errors.Iserrors.As 和 errors.Match
  • 2015 年 12 月, github.com/pkg/errors,带堆栈的错误,引入了 %+v 来格式化错误的额外信息比如堆栈;
  • 2014 年 10 月, github.com/hashicorp/errwrap,可以 wrap 多个错误,引入了错误树,提供 Walk 函数遍历所有的错误;
  • 2014 年 2 月, github.com/juju/errgo,Wrap 时可以选择是否隐藏底层错误。和 pkg/errors 的 Cause 返回最底层的错误不同,它只反馈错误链的下一个错误;
  • 2013 年 7 月, github.com/spacemonkeygo/errors,是来源于一个大型 Python 项目,有错误的 hierarchies,自动记录日志和堆栈,还可以带额外的信息。打印错误的消息比较固定,不能自己定义;
  • 2019 年 9 月,Go1.13 标准库扩展了 error,支持了 Unwrap、As 和 Is,但没有支持堆栈信息。

Go1.13 改进了 errors,参考如下实例代码:

package main

import (
    "errors"
    "fmt"
    "io"
)

func foo() error {
    return fmt.Errorf("read err: %w", io.EOF)
}

func bar() error {
    if err := foo(); err != nil {
        return fmt.Errorf("foo err: %w", err)
    }
    return nil
}

func main() {
    if err := bar(); err != nil {
        fmt.Printf("err: %+v\n", err)
        fmt.Printf("unwrap: %+v\n", errors.Unwrap(err))
        fmt.Printf("unwrap of unwrap: %+v\n", errors.Unwrap(errors.Unwrap(err)))
        fmt.Printf("err is io.EOF? %v\n", errors.Is(err, io.EOF))
    }
}

运行结果如下:

err: foo err: read err: EOF
unwrap: read err: EOF
unwrap of unwrap: EOF
err is io.EOF? true

从上面的例子可以看出:

  • 没有堆栈信息,主要是想通过 Wrap 的日志来标识堆栈,如果全部 Wrap 一层和堆栈差不多,不过对于没有 Wrap 的错误还是无法知道调用堆栈;
  • Unwrap 只会展开第一个嵌套的 error,如果错误有多层嵌套,取不到最里面的那个 error,需要多次 Unwrap 才行;
  • 用 errors.Is 能判断出是否是最里面的那个 error。

另外,错误处理往往和 log 是容易混为一谈的,因为遇到错误一般会打日志,特别是在 C/C 中返回错误码一般都会打日志记录下,有时候还会记录一个全局的错误码比如 linux 的 errno,而这种习惯,导致 error 和 log 混淆造成比较大的困扰。考虑以前写了一个 C 的服务器,出现错误时会在每一层打印日志,所以就会形成堆栈式的错误日志,便于排查问题,如果只有一个错误,不知道调用上下文,排查会很困难:

avc decode avc_packet_type failed. ret=3001
Codec parse video failed, ret=3001
origin hub error, ret=3001

这种比只打印一条日志 origin hub error, ret=3001 要好,但是还不够好:

  • 和 Go 的错误一样,比较啰嗦,有重复的信息。如果能提供堆栈信息,可以省去很多需要手动写的信息;
  • 对于应用程序可以打日志,但是对于库,信息都应该包含在 error 中,不应该直接打印日志。如果底层的库都要打印日志,将会导致底层库都要依赖日志库,这时很多库都有日志打印函数供调用者重写;
  • 对于多线程,看不到线程信息,或者看不到业务层 ID 的信息。对于服务器来说,有时候需要知道这个错误是哪个连接的,从而查询这个连接之前的上下文信息。

改进后的错误日志变成了在底层返回,而不在底层打印在调用层打印,有调用链和堆栈,有线程切换的 ID 信息,也有文件的行数:

Error processing video, code=3001 : origin hub : codec parser : avc decoder
[100] video_avc_demux() at [srs_kernel_codec.cpp:676]
[100] on_video() at [srs_app_source.cpp:1076]
[101] on_video_imp() at [srs_app_source:2357]

从 Go2 的描述来说,实际上这个错误处理也还没有考虑完备。从实际开发来说,已经比较实用了。

总结下 Go 的 error,错误处理应该注意以下几点:

  • 凡是有返回错误码的函数,必须显式的处理错误,如果要忽略错误,也应该显式的忽略和写注释;
  • 错误必须带丰富的错误信息,比如堆栈、发生错误时的参数、调用链给的描述等等。特别要强调变量,我看过太多日志描述了一对常量,比如 “Verify the nonce, timestamp and token of specified appid failed”,而这个消息一般会提到工单中,然后就是再问用户,哪个 session 或 request 甚至时间点?这么一大堆常量有啥用呢,关键是变量呐;
  • 尽量避免重复的信息,提高错误处理的开发体验,糟糕的体验会导致无效的错误处理代码,比如拷贝和漏掉关键信息;
  • 分离错误和日志,发生错误时返回带完整信息的错误,在调用的顶层决定是将错误用日志打印,还是发送到监控系统,还是转换错误,或者忽略。

 

Best Practice

推荐用 github.com/pkg/errors 这个错误处理的库,基本上是够用的,参考 Refine: CopyFile,可以看到 CopyFile 中低级重复的代码已经比较少了:

package main

import (
  "fmt"
  "github.com/pkg/errors"
  "io"
  "os"
)

func CopyFile(src, dst string) error {
  r, err := os.Open(src)
  if err != nil {
    return errors.Wrap(err, "open source")
  }
  defer r.Close()

  w, err := os.Create(dst)
  if err != nil {
    return errors.Wrap(err, "create dest")
  }

  nn, err := io.Copy(w, r)
  if err != nil {
    w.Close()
    os.Remove(dst)
    return errors.Wrap(err, "copy body")
  }

  if err := w.Close(); err != nil {
    os.Remove(dst)
    return errors.Wrapf(err, "close dest, nn=%v", nn)
  }

  return nil
}

func LoadSystem() error {
  src, dst := "src.txt", "dst.txt"
  if err := CopyFile(src, dst); err != nil {
    return errors.WithMessage(err, fmt.Sprintf("load src=%v, dst=%v", src, dst))
  }

  // Do other jobs.

  return nil
}

func main() {
  if err := LoadSystem(); err != nil {
    fmt.Printf("err %+v\n", err)
  }
}

改写的函数中,用 errors.Wrap 和 errors.Wrapf 代替了 fmt.Errorf,我们不记录 src 和 dst 的值,因为在上层会记录这个值(参考下面的代码),而只记录我们这个函数产生的数据,比如 nn

import "github.com/pkg/errors"

func LoadSystem() error {
    src, dst := "src.txt", "dst.txt"
    if err := CopyFile(src, dst); err != nil {
        return errors.WithMessage(err, fmt.Sprintf("load src=%v, dst=%v", src, dst))
    }

    // Do other jobs.

    return nil
}

在这个上层函数中,我们用的是 errors.WithMessage 添加了这一层的错误信息,包括 src 和 dst,所以 CopyFile 里面就不用重复记录这两个数据了。同时我们也没有打印日志,只是返回了带完整信息的错误。

func main() {
    if err := LoadSystem(); err != nil {
        fmt.Printf("err %+v\n", err)
    }
}

在顶层调用时,我们拿到错误,可以决定是打印还是忽略还是送监控系统。

比如我们在调用层打印错误,使用 %+v 打印详细的错误,有完整的信息:

err open src.txt: no such file or directory
open source
main.CopyFile
    /Users/winlin/t.go:13
main.LoadSystem
    /Users/winlin/t.go:39
main.main
    /Users/winlin/t.go:49
runtime.main
    /usr/local/Cellar/go/1.8.3/libexec/src/runtime/proc.go:185
runtime.goexit
    /usr/local/Cellar/go/1.8.3/libexec/src/runtime/asm_amd64.s:2197
load src=src.txt, dst=dst.txt

但是这个库也有些小毛病:

  1. CopyFile 中还是有可能会有重复的信息,还是 Go2 的 handle 和 check 方案是最终解决;
  2. 有时候需要用户调用 Wrap,有时候是调用 WithMessage(不需要加堆栈时),这个真是非常不好用的地方(这个我们已经修改了库,可以全部使用 Wrap 不用 WithMessage,会去掉重复的堆栈)。

 

Logger

一直在码代码,对日志的理解总是不断在变,大致分为几个阶段:

  • 日志是给人看的,是用来查问题的。出现问题后根据某些条件,去查不同进程或服务的日志。日志的关键是不能漏掉信息,漏了关键日志,可能就断了关键的线索;
  • 日志必须要被关联起来,上下文的日志比单个日志更重要。长连接需要根据会话关联日志;不同业务模型有不同的上下文,比如服务器管理把服务器作为关键信息,查询这个服务器的相关日志;全链路跨机器和服务的日志跟踪,需要定义可追踪的逻辑 ID;
  • 海量日志是给机器看的,是结构化的,能主动报告问题,能从日志中分析潜在的问题。日志的关键是要被不同消费者消费,要输出不同主题的日志,不同的粒度的日志。日志可以用于排查问题,可以用于告警,可以用于分析业务情况。

Note: 推荐阅读 Kafka 对于 Log 的定义,广义日志是可以理解的消息,The Log: What every software engineer should know about real-time data’s unifying abstraction

完善信息查问题

考虑一个服务,处理不同的连接请求:

package main

import (
    "context"
    "fmt"
    "log"
    "math/rand"
    "os"
    "time"
)

type Connection struct {
    url    string
    logger *log.Logger
}

func (v *Connection) Process(ctx context.Context) {
    go checkRequest(ctx, v.url)

    duration := time.Duration(rand.Int()%1500) * time.Millisecond
    time.Sleep(duration)
    v.logger.Println("Process connection ok")
}

func checkRequest(ctx context.Context, url string) {
    duration := time.Duration(rand.Int()%1500) * time.Millisecond
    time.Sleep(duration)
    logger.Println("Check request ok")
}

var logger *log.Logger

func main() {
    ctx := context.Background()

    rand.Seed(time.Now().UnixNano())
    logger = log.New(os.Stdout, "", log.LstdFlags)

    for i := 0; i < 5; i++ {
        go func(url string) {
            connecton := &Connection{}
            connecton.url = url
            connecton.logger = logger
            connecton.Process(ctx)
        }(fmt.Sprintf("url #%v", i))
    }

    time.Sleep(3 * time.Second)
}

这个日志的主要问题,就是有了和没有差不多,啥也看不出来,常量太多变量太少,缺失了太多的信息。看起来这是个简单问题,却经常容易犯这种问题,需要我们在打印每个日志时,需要思考这个日志比较完善的信息是什么。上面程序输出的日志如下:

2019/11/21 17:08:04 Check request ok
2019/11/21 17:08:04 Check request ok
2019/11/21 17:08:04 Check request ok
2019/11/21 17:08:04 Process connection ok
2019/11/21 17:08:05 Process connection ok
2019/11/21 17:08:05 Check request ok
2019/11/21 17:08:05 Process connection ok
2019/11/21 17:08:05 Check request ok
2019/11/21 17:08:05 Process connection ok
2019/11/21 17:08:05 Process connection ok

如果完善下上下文信息,代码可以改成这样:

type Connection struct {
    url    string
    logger *log.Logger
}

func (v *Connection) Process(ctx context.Context) {
    go checkRequest(ctx, v.url)

    duration := time.Duration(rand.Int()%1500) * time.Millisecond
    time.Sleep(duration)
    v.logger.Println(fmt.Sprintf("Process connection ok, url=%v, duration=%v", v.url, duration))
}

func checkRequest(ctx context.Context, url string) {
    duration := time.Duration(rand.Int()%1500) * time.Millisecond
    time.Sleep(duration)
    logger.Println(fmt.Sprintf("Check request ok, url=%v, duration=%v", url, duration))
}

输出的日志如下:

2019/11/21 17:11:35 Check request ok, url=url #3, duration=32ms
2019/11/21 17:11:35 Check request ok, url=url #0, duration=226ms
2019/11/21 17:11:35 Process connection ok, url=url #0, duration=255ms
2019/11/21 17:11:35 Check request ok, url=url #4, duration=396ms
2019/11/21 17:11:35 Check request ok, url=url #2, duration=449ms
2019/11/21 17:11:35 Process connection ok, url=url #2, duration=780ms
2019/11/21 17:11:35 Check request ok, url=url #1, duration=1.01s
2019/11/21 17:11:36 Process connection ok, url=url #4, duration=1.099s
2019/11/21 17:11:36 Process connection ok, url=url #3, duration=1.207s
2019/11/21 17:11:36 Process connection ok, url=url #1, duration=1.257s

上下文关联

完善日志信息后,对于服务器特有的一个问题,就是如何关联上下文,常见的上下文包括:

  • 如果是短连接,一条日志就能描述,那可能要将多个服务的日志关联起来,将全链路的日志作为上下文;
  • 如果是长连接,一般长连接一定会有定时信息,比如每隔 5 秒输出这个链接的码率和包数,这样每个链接就无法使用一条日志描述了,链接本身就是一个上下文;
  • 进程内的逻辑上下文,比如代理的上下游就是一个上下文,合并回源,故障上下文,客户端重试等。

以上面的代码为例,可以用请求 URL 来作为上下文。

package main

import (
    "context"
    "fmt"
    "log"
    "math/rand"
    "os"
    "time"
)

type Connection struct {
    url    string
    logger *log.Logger
}

func (v *Connection) Process(ctx context.Context) {
    go checkRequest(ctx, v.url)

    duration := time.Duration(rand.Int()%1500) * time.Millisecond
    time.Sleep(duration)
    v.logger.Println(fmt.Sprintf("Process connection ok, duration=%v", duration))
}

func checkRequest(ctx context.Context, url string) {
    duration := time.Duration(rand.Int()%1500) * time.Millisecond
    time.Sleep(duration)
    logger.Println(fmt.Sprintf("Check request ok, url=%v, duration=%v", url, duration))
}

var logger *log.Logger

func main() {
    ctx := context.Background()

    rand.Seed(time.Now().UnixNano())
    logger = log.New(os.Stdout, "", log.LstdFlags)

    for i := 0; i < 5; i++ {
        go func(url string) {
            connecton := &Connection{}
            connecton.url = url
            connecton.logger = log.New(os.Stdout, fmt.Sprintf("[CONN %v] ", url), log.LstdFlags)
            connecton.Process(ctx)
        }(fmt.Sprintf("url #%v", i))
    }

    time.Sleep(3 * time.Second)
}

运行结果如下所示:

[CONN url #2] 2019/11/21 17:19:28 Process connection ok, duration=39ms
2019/11/21 17:19:28 Check request ok, url=url #0, duration=149ms
2019/11/21 17:19:28 Check request ok, url=url #1, duration=255ms
[CONN url #3] 2019/11/21 17:19:28 Process connection ok, duration=409ms
2019/11/21 17:19:28 Check request ok, url=url #2, duration=408ms
[CONN url #1] 2019/11/21 17:19:29 Process connection ok, duration=594ms
2019/11/21 17:19:29 Check request ok, url=url #4, duration=615ms
[CONN url #0] 2019/11/21 17:19:29 Process connection ok, duration=727ms
2019/11/21 17:19:29 Check request ok, url=url #3, duration=1.105s
[CONN url #4] 2019/11/21 17:19:29 Process connection ok, duration=1.289s

如果需要查连接 2 的日志,可以 grep 这个 url #2 关键字:

Mac:gogogo chengli.ycl$ grep 'url #2' t.log
[CONN url #2] 2019/11/21 17:21:43 Process connection ok, duration=682ms
2019/11/21 17:21:43 Check request ok, url=url #2, duration=998ms

然鹅,还是发现有不少问题:

  • 如何实现隐式标识,调用时如何简单些,不用没打一条日志都需要传一堆参数?
  • 一般 logger 是公共函数(或者是每个类一个 logger),而上下文的生命周期会比 logger 长,比如 checkRequest 是个全局函数,标识信息必须依靠人打印,这往往是不可行的;
  • 如何实现日志的 logrotate(切割和轮转),如何收集多个服务器日志。

解决办法包括:

  • 用 Context 的 WithValue 来将上下文相关的 ID 保存,在打印日志时将 ID 取出来;
  • 如果有业务特征,比如可以取 SessionID 的 hash 的前 8 个字符形成 ID,虽然容易碰撞,但是在一定范围内不容易碰撞;
  • 可以变成 json 格式的日志,这样可以将 level、id、tag、file、err 都变成可以程序分析的数据,送到 SLS 中处理;
  • 对于切割和轮转,推荐使用 lumberjack 这个库,程序的 logger 只要提供 SetOutput(io.Writer) 将日志送给它处理就可以了。

当然,这要求函数传参时需要带 context.Context,一般在自己的应用程序中可以要求这么做,凡是打日志的地方要带 context。对于库,一般可以不打日志,而返回带堆栈的复杂错误的方式,参考 Errors 错误处理部分。

点击下载《不一样的 双11 技术:阿里巴巴经济体云原生实践》

ban.jpg

本书亮点

  • 双11 超大规模 K8s 集群实践中,遇到的问题及解决方法详述
  • 云原生化最佳组合:Kubernetes+容器+神龙,实现核心系统 100% 上云的技术细节
  • 双 11 Service Mesh 超大规模落地解决方案

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

你的安卓项目编译要花10分钟,如何缩短到1分钟?

JFrog捷蛙阅读(2554)评论(0)

痛点

如果项目的代码库较大,例如大型的安卓开发项目,在构建的时候耗时较长,达到数十分钟甚至更长,分析其原因,其中一部分时间是花在构建上。在大规模开发团队中,例如上百人的开发团队,如果每个人构建一次需要花费数十分钟,那么团队每天浪费的时间是非常惊人的。

除了构建时间,执行 Gradle Build 的时候很大一部分时间是花在单元测试用例的执行上,这样的问题也困扰着大规模 Gradle 的用户。

 

方案

为了让构建提升速度,Gradle 4.0 以上版本提供了Build Cache 的功能,也就是构建缓存。注意,这里的构建指的不是构建产出物,例如 war,jar 文件,而是 Java 构建的字节码 .class文件。通过缓存每次构建产生的.class 文件,实现 Java 项目的增量编译。Gradle 项目能够在第一次构建之后,创建一个 Key-value 的键值对数据,将每个.class 文件通过一个 key 索引起来。而这些键值对以及.class 文件会上传到一个中央服务器(例如 Nginx 或者 JFrog Artifactory),当用户再次构建,或者其他成员构建时,会先将中央服务器的缓存文件下载到本地,再进行打包,这样就能大大减少编译构建时间,实现增量编译。

 

注意,这里不仅仅能够缓存软件程序的.class 文件,对应单元测试用例编译产生的.class 文件同样能够缓存。

这里以开源版 Artifactory 为例,结合 Gradle 实现增量编译:

 

  • 创建一个示例项目“gradle-cache-example”

在这个 Java 工程里只需要创建一些普通的 Java 类即可,后面我们将验证如何将这段代码对应的 class 缓存起来,节约构建时间。

为设置构建缓存前执行构建:./gradlew clean build

BUILD SUCCESSFUL in 11s

13 actionable tasks: 12 executed, 1 up-to-date

 

可以看到构建耗时 12 秒。

 

  • 在本地搭建开源版 Artifactory 作为构建缓存中央服务器。搭建开源版 Artifactory 最方便的方式是用容器启动:

 

docker run –name artifactory -d -p 8081:8081

docker.bintray.io/jfrog/artifactory-oss:latest

 

  • 设置构建缓存

在开发本地的工程文件中的 gradle.properties中设置如下配置,将构建缓存指向 Artifactory。

 

gradle.properties

artifactory_user=admin

artifactory_password=password

artifactory_url=http://localhost:8081/artifactory

org.gradle.caching=true

gradle.cache.push=false

 

设置 CI 服务器上的settings.gradle,下面是 Jenkins 的脚本:

include “shared”, “api”, “services:webservice”

ext.isPush = getProperty(‘gradle.cache.push’)

buildCache {

local {

enabled = false

}

remote(HttpBuildCache) {

url = “${artifactory_url}/gradle-cache-example/”

credentials {

username = “${artifactory_user}”

password = “${artifactory_password}”

}

push = isPush

}

}

 

在CI 服务器上执行 ./gradlew clean build -Pgradle.cache.push=true。通过设置gradle.cache.push=true,实现本地构建缓存向中央服务器的推送。

BUILD SUCCESSFUL in 1s

13 actionable tasks: 7 executed, 5 from cache, 1 up-to-date

可以看到构建时间从 12 秒缩短到 1 秒,其中 5 个任务是来自缓存。

 

来确认下我们的构建加速并不是来自本地缓存,可以查看 Artifactory 的访问日志:

 

20170526153341|3|REQUEST|127.0.0.1|admin|GET|/gradle-cache-example/6dc9bb4c16381e32ca1f600b3060616f|HTTP/1.1|200|1146

20170526153341|4|REQUEST|127.0.0.1|admin|GET|/gradle-cache-example/e5a67dca52dfaea60efd28654eb8ec97|HTTP/1.1|200|1296

 

可以看到本地缓存,均来自 Artifactory 的统一仓库。

 

  • 跨部门,地域共享构建缓存

 

在大型分布式研发团队里,构建环境往往分布在各个地域,例如北京,上海。在这种情况下,构建缓存上传到本地的 Artifactory 之后,并不能够被远程的构建服务器使用。这是需要用到 Artifactory 企业版的文件实时复制功能实现。

 

如上图所示:当本地开发者或者 CI 服务器执行第一次构建时,Artifactory 会通过 Push Replication(推送复制)的方式将本地的缓存推送到远程的 Artifactory,当远程的用户在执行 Gradle 构建时,能够受益于已有的构建缓存,从而大大加速构建的速度。

 

总结

本文展示并说明了如何使用 Gradle和 Artifactory 开源版进行构建缓存的实现,提升构建速度。使用 Artifactory 企业版,能够实现跨地域的构建缓存共享,优化公司级别的构建速度。

PouchContainer 容器技术演进助力阿里云原生升级

alicloudnative阅读(2447)评论(0)

本文节选自《不一样的 双11 技术:阿里巴巴经济体云原生实践》一书。

作者 | 杨育兵(沈陵) 阿里巴巴高级技术专家

我们从 2016 年开始在集团推广全面的镜像化容器化,今年是集团全面镜像化容器化后的第 4 个 双11,PouchContainer 容器技术已经成为集团所有在线应用运行的运行时底座和运维载体,每年 双11 都有超过百万的 PouchContainer 容器同时在线,提供电商和所有相关的在线应用平稳运行的载体,保障大促购物体验的顺滑。

我们通过 PouchContainer 容器运行时这一层标准构建了应用开发和基础设施团队的标准界面,每年应用都有新的需求、新的变化,同时基础设施也有上云/混部/神龙/存储计算分离/网络变革这些升级,两边平行演进,互不干扰。技术设施和 PouchContainer 自身都做了很大的架构演进,这些很多的架构和技术演进对应用开发者都是无感知的。

1.png

在容器技术加持的云原生形成趋势的今天,PouchContainer 容器技术支持的业务方也不再只有集团电商业务和在线业务了,我们通过标准化的演进,把所有定制功能做了插件化,适配了不同场景的需要。除了集团在线应用,还有运行在离线调度器上面的离线 job 类任务、跑在搜索调度器上面的搜索广告应用、跑在 SAE/CSE 上面的 Serverless 应用、专有云产品及公有云(ACK+CDN)等场景,都使用了 PouchContainer 提供的能力。

2.png

运行时的演进

2015 年之前,我们用的运行时是 LXC,PouchContainer 为了在镜像化后能够平滑接管原来的 T4 容器,在 LXC 中支持新的镜像组装方式,并支持交互式的 exec 和内置的网络模式。

随着云原生的进程,我们在用户无感知的情况下对运行时做了 containerd+runc 的支持,用标准化的方式加内部功能插件,实现了内部功能特性的支持和被各种标准化运维系统无缝集成的目标。

无论是 LXC 还是 runc 都是让所有容器共享 Linux 内核,利用 cgroup 和 namespace 来做隔离,对于强安全场景和强隔离场景是不适用的。为了容器这种开发和运维友好的交付形式能给更多场景带来收益,我们很早就开始探索这方面的技术,和集团 os 创新团队以及蚂蚁 os 虚拟化团队合作共建了 kata 安全容器和 gvisor 安全容器技术,在容器生态嫁接,磁盘、网络和系统调用性能优化等方面都做了很多的优化。在兼容性要求高的场景我们优先推广 kata 安全容器,已经支持了 SAE 和 ACK 安全容器场景。在语言和运维习惯确定的场景,我们也在 618 大促时上线了一些合适的电商使用了 gvisor 的运行时隔离技术,稳定性和性能都得到了验证。

为了一部分专有云场景的实施,我们今年还首次支持了 Windows 容器运行时,在容器依赖相关的部署、运维方面做了一些探索,帮助敏捷版专有云拿下了一些客户。

除了安全性和隔离性,我们的运行时演进还保证了标准性,今年最新版本的 PouchContainer 把 diskquota、lxcfs、dragonfly、DADI 这些特性都做成了可插拔的插件,不需要这些功能的场景可以完全不受这些功能代码的影响。甚至我们还对一些场景做了 containerd 发行版,支持纯粹的标准 CRI 接口和丰富的运行时。

3.png

镜像技术的演进

镜像化以后必然会引入镜像分发的效率方面的困难,一个是速度另一个是稳定性,让发布扩容流程不增加太多时间的情况下,还要保证中心节点不被压垮。
PouchContainer 在一开始就支持了使用 Dragonfly 来做 P2P 的镜像分发,就是为了应对这种问题,这是我们的第一代镜像分发方案。在研发域我们也对镜像分层的最佳实践做了推广,这样能最大程度的保证基础环境不变时每次下载的镜像层最小。镜像加速要解决的问题有:build 效率、push 效率、pull 效率、解压效率以及组装效率。第一代镜像加速方案,结合 Dockerfile 的最佳实践解决了 build 效率和 pull 效率和中心压力。

第一代镜像分发的缺点是无论用户启动过程中用了多少镜像数据,在启动容器之前就需要把所有的镜像文件都拉到本地,在很多场景下都是浪费的,特别影响的是扩容场景。所以第二代的镜像加速方案,我们调研了阿里云的盘古,盘古的打快照、mount、再打快照这种使用方式完美匹配打镜像和分发的流程;能做到秒级镜像 pull,因为 pull 镜像时只需要鉴权,下载镜像 manifest,然后 mount 盘古,也能做到镜像内容按需读取。

2018 年 双11,我们小规模上线了盘古远程镜像,也验证了我们的设计思路,这一代的镜像加速方案结合新的 overlay2 技术在第一代的基础上又解决了PouchContainer 效率/pull 效率/解压效率和组装效率。

但是也存在一些问题。首先镜像数据没有存储在中心镜像仓库中,只有 manifest 信息,这样镜像的分发范围就受限,在哪个盘古集群做的镜像,就必须在那个盘古集群所在的阿里云集群中使用这个镜像;其次没有 P2P 的能力,在大规模使用时对盘古后端的压力会很大,特别是离线场景下由于内存压力导致很多进程的可执行文件的 page cache 被清理,然后需要重新 load 这种场景,会给盘古后端带来更大的压力。基于这两个原因,我们和 ContainerFS 团队合作共建了第三代镜像分发方案:DADI(基于块设备的按需 P2P 加载技术,后面也有计划开源这个镜像技术)。

DADI 在构建阶段保留了镜像的多层结构,保证了镜像在多次构建过程中的可重用性,并索引了每个文件在每层的offset 和 length,推送阶段还是把镜像推送到中心镜像仓库中,保证在每个机房都能拉取到这个镜像。在每个机房都设置了超级节点做缓存,每一块内容在特定的时间段内,都只从镜像仓库下载一次。如果有时间做镜像预热,像 双11 这种场景,预热阶段就是从中心仓库中把镜像预热到本地机房的超级节点,后面的同机房的数据传输会非常快。镜像 pull 阶段只需要下载镜像的 manifest 文件(通常只有几 K大小),速度非常快,启动阶段 DADI 会给每个容器生成一个块设备,这个块设备的 chunk 读取是按需从超级节点或临近节点 P2P 读取的内容,这样就保证了容器启动阶段节点上只读取了需要的部分内容。为了防止容器运行过程中出现 iohang,我们在容器启动后会在后台把整个镜像的内容全部拉到 node 节点,享受超快速启动的同时最大程度地避免后续可能出现的 iohang。

使用 DADI 镜像技术后的今年 双11 高峰期,每次有人在群里面说有扩容任务,我们值班的同学去看工单时,基本都已经扩容完成了,扩容体验做到了秒级。

网络技术演进

PouchContainer 一开始的网络功能是揉合在 PouchContainer 自己的代码中的,用集成代码的方式支持了集团各个时期的网络架构,为了向标准化和云原生转型,在应用无感知的情况下,我们在 Sigma-2.0 时代使用 libnetwork 把集团现存的各种网络机架构都统一做了 CNM 标准的网络插件,沉淀了集团和专有云都在用的阿里巴巴自己的网络插件。在在线调度系统推广期间,CNM 的网络插件已经不再适用,为了不需要把所有的网络插件再重新实现一遍,我们对原来的网络插件做了包装,沉淀了 CNI 的网络插件,把 CNM 的接口转换为 CNI 的接口标准。

内部的网络插件支持的主流单机网络拓扑演进过程如下图所示:

4.png

从单机拓扑能看出来使用神龙 eni 网络模式可以避免容器再做网桥转接,但是用神龙的弹性网卡和CNI网络插件时也有坑需要避免,特别是 eni 弹性网卡是扩容容器时才热插上来的情况时。创建 eni 网卡时,udevd 服务会分配一个唯一的 id N,比如 ethN,然后容器 N 启动时会把 ethN 移动到容器 N 的 netns,并从里面改名为 eth0。容器 N 停止时,eth0 会改名为 ethN 并从容器 N 的 netns 中移动到宿主机的 netns 中。

这个过程中,如果容器 N 没有停止时又分配了一个容器和 eni 到这台宿主机上,udevd 由于看不到 ethN 了,它有可能会分配这个新的 eni 的名字为 ethN。容器 N 停止时,把 eth0 改名为 ethN 这一步能成功,但是移动到宿主机根 netns 中这一步由于名字冲突会失败,导致 eni 网卡泄漏,下一次容器 N 启动时找不到它的 eni 了。可以通过修改 udevd 的网卡名字生成规则来避免这个坑。

运维能力演进

PouchContainer 容器技术支持了百万级的在线容器同时运行,经常会有一些问题需要我们排查,很多都是已知的问题,为了解决这个困扰,我还写了 PouchContainer 的一千个细节以备用户查询,或者重复问题问过来时直接交给用户。但是 PouchContainer 和相关链路本身稳定性和运维能力的提升才是最优的方法。今年我们建设了 container-debugger 和 NodeOps 中心系统,把一些容器被用户问的问题做自动检测和修复,任何修复都做了灰度筛选和灰度部署能力,把一些经常需要答疑的问题做了用户友好的提示和修复,也减轻了我们自身的运维压力。

5.png

  1. 内部的中心化日志采集和即时分析
  2. 自带各模块的健康和保活逻辑
  3. 所有模块提供 Prometheus 接口,暴露接口成功率和耗时
  4. 提供常见问题自动巡检修复的工具
  5. 运维经验积累,对用户问题提供修复建议
  6. 提供灰度工具,任何变更通过金丝雀逐步灰度
  7. 剖析工具,流程中插入代码的能力
  8. Pouch 具备一键发布能力,快速修复

容器使用方式演进

提供容器平台给应用使用,在容器启动之前必然有很多平台相关的逻辑需要处理,这也是我们以前用富容器的原因。

  1. 安全相关:安全路由生成、安全脚本配置
  2. cpushare 化相关配置:tsar 和 nginx 配置
  3. 运维agent 更新相关:运维agent 更新相对频繁,基础镜像更新特别慢,不能依赖于基础镜像更新来更新运维agent
  4. 配置相关逻辑:同步页头页尾,隔离环境支持, 强弱依赖插件部署
  5. SN 相关:模拟写 SN 到/dev/mem,保证 dmidecode 能读到正确的 SN
  6. 运维相关的的 agent 拉起,很多运维系统都依赖于在节点上面有一个 agent,不管这个节点是容器/ecs 还是物理机
  7. 隔离相关的配置:比如 nproc 这个限制是在用户上面的,用统一个镜像的容器不能使用统一 uid 不然无法隔离 nproc
    现在随着基于 K8s 编排调度系统的推广,我们有了 Pod 能力,可以把一些预置逻辑放到前置 hook 中去执行,当然富容器可以瘦下来,还要依赖于运维 agent 可以从主容器中拆出来,那些只依赖于 volume 共享就能跑起来的 agent 可以先移动到 sidecar 里面去,这样就可以把运维容器和业务主容器分到不同的容器里面去,一个 Pod 多个容器在资源隔离上面分开,主容器是 Guaranteed 的 QOS,运维容器是 Burstable 的 QOS。同时在 kubelet 上支持 Pod 级别的资源管控,保证这个 Pod 整体是 Guaranteed 的同时,限制了整个 pod 的资源使用量不超过应用单实例的申请资源。

还有一些 agent 不是只做 volume 共享就可以放到 sidecar 的运维容器中的,比如 arthas 需要能 attach 到主容器的进程上去,还要能 load 主容器中非 volume 路径上面的 jar 文件才能正常工作。对于这种场景 PouchContainer 容器也提供了能让同 Pod 多容器做一些 ns 共享的能力,同时配合 ns 穿越来让这些 agent 可以在部署方式和资源隔离上是和主容器分离的,但是在运行过程中还可以做它原来可以做的事情。

容器技术继续演进的方向

可插拔的插件化的架构和更精简的调用链路在容器生态里面还是主流方向,kubelet 可以直接去调用 pouch-containerd 的 CRI 接口,可以减少中间一个组件的远程调用,不过 CRI 接口现在还不够完善,很多运维相关的指令都没有,logs 接口也要依赖于 container API 来实现,还有运行环境和构建环境分离,这样用户就不需要到宿主机上面执行 build。所有的运维系统也不再依赖于 container API。在这些约束下我们可以做到减少对一个中间组件的系统调用,直接用 kubelet 去调用 pouch-containerd 的 CRI 接口。

现在每个应用都有很多的 Dockerifle,怎么让 Dockerfile 更有表达能力,减少 Dockerfile 数量。构建的时候并发构建也是一个优化方向,buildkit 在这方面是可选的方案,Dockerfile 表达能力的欠缺也需要新的解决方案,buildkit 中间态的 LLB 是 go 代码,是不是可以用 go 代码来代替 Dockerfile,定义更强表达能力的 Dockerfile 替代品。

容器化是云原生的关键路径,容器技术在运行时和镜像技术逐渐趋于稳定的情况下,热点和开发者的目光开始向上层转移,K8s 和基于其上的生态成为容器技术未来能产生更多创新的领域,PouchContainer 技术也在向着更云原生、更好适配 K8s 生态的方向发展,网络、diskquota、试图隔离等 PouchContainer 的插件,在 K8s 生态系统中适配和优化也我们后面的方向之一。

点击下载《不一样的 双11 技术:阿里巴巴经济体云原生实践》
ban.jpg

本书亮点

  • 双11 超大规模 K8s 集群实践中,遇到的问题及解决方法详述
  • 云原生化最佳组合:Kubernetes+容器+神龙,实现核心系统 100% 上云的技术细节
  • 双 11 Service Mesh 超大规模落地解决方案

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

Kubernetes 下零信任安全架构分析

alicloudnative阅读(16095)评论(0)

本文节选自《不一样的 双11 技术:阿里巴巴经济体云原生实践》一书。

作者
杨宁(麟童) 阿里云基础产品事业部高级安全专家
刘梓溪(寞白) 蚂蚁金服大安全基础安全安全专家
李婷婷(鸿杉) 蚂蚁金服大安全基础安全资深安全专家

简介

零信任安全最早由著名研究机构 Forrester 的首席分析师约翰.金德维格在 2010 年提出。零信任安全针对传统边界安全架构思想进行了重新评估和审视,并对安全架构思路给出了新的建议。

其核心思想是,默认情况下不应该信任网络内部和外部的任何人/设备/系统,需要基于认证和授权重构访问控制的信任基础。诸如 IP 地址、主机、地理位置、所处网络等均不能作为可信的凭证。零信任对访问控制进行了范式上的颠覆,引导安全体系架构从“网络中心化”走向“身份中心化”,其本质诉求是以身份为中心进行访问控制。

目前落地零信任概念包括 Google BeyondCorp、Google ALTS、Azure Zero Trust Framework 等,云上零信任体系,目前还是一个新兴的技术趋势方向,同样的零信任模型也同样适用于 Kubernetes,本文重点讲解一下 Kubernetes 下零信任安全架构的技术分析。

传统零信任概念和目前落地情况

Microsoft Azure

Azure 的零信任相对来说还是比较完善的,从体系角度来看涵盖了端、云、On-Permises、SaaS 等应用,下面我们分析一下相关的组件:

  • 用户 Identity:然后通过 Identity Provider(创建、维护和管理用户身份的组件)的认证,再认证的过程中可以使用账号密码,也可以使用 MFA(Multi Factor Auth)多因素认证的方式,多因素认证包括软、硬 Token、SMS、人体特征等;
  • 设备 Identity:设备包含了公司的设备以及没有统一管理的设备,这些设备的信息,包含 IP 地址、MAC 地址、安装的软件、操作系统版本、补丁状态等存储到 Device Inventory;另外设备也会有相应的 Identity 来证明设备的身份;设备会有对应的设备状态、设备的风险进行判定;
  • Security Policy Enforcement:通过收集的用户 Identity 以及状态、设备的信息,状态以及 Identity 后,SPE 策略进行综合的判定,同时可以结合 Threat Intelligence 来增强 SPE 的策略判定的范围和准备性;策略的例子包括可以访问后面的 Data、Apps、Infrastructure、Network;
  • Data:针对数据(Emails、Documents)进行分类、标签、加密的策略;
  • Apps:可以自适应访问对应的 SaaS 应用和 On-Permises 应用;
  • Infrastructure:包括 IaaS、PaaS、Container、Serverless 以及 JIT(按需开启访问)和 GIT 版本控制软件;
  • Network:针对网络交付过程以及内部的微隔离进行策略打通。

1.png

下面这张微软的图进行了更加细化的讲解,用户(员工、合作伙伴、用户等)包括 Azure AD、ADFS、MSA、Google ID 等,设备(可信的合规设备)包括 Android、iOS、MacOS、Windows、Windows Defender ATP,客户端(客户端 APP 以及认证方式)包括浏览器以及客户端应用,位置(物理和虚拟地址)包括地址位置信息、公司网络等,利用 Microsoft 的机器学习 ML、实时评估引擎、策略等进行针对用户、客户端、位置和设备进行综合判定,来持续自适应的访问 On-Permises、Cloud SaaS Apps、Microsoft Cloud,包含的策略包括 Allow、Deny,限制访问、开启 MFA、强制密码重置、阻止和锁定非法认证等;从下图可以看出 Azure 已经打通了 On-Permises、Cloud、SaaS 等各个层面,构建了一个大而全的零信任体系。

2.png

Google BeyondCorp

Google BeyondCorp 是为了应对新型网络威胁的一种网络安全解决方案,其实 Google BeyondCorp 本身并没有太多的技术上的更新换代,而是利用了持续验证的一种思路来做的,去掉了 VPN 和不再分内外网。Google 在 2014 年之前就预测到互联网和内网的安全性是一样危险的,因为一旦内网边界被突破的话,攻击者就很容易的访问企业的一些内部应用,由于安全意识的问题导致企业会认为我的内部很安全,就对内部的应用进行低优先级别的处理,导致大量内部的安全问题存在。现在的企业越来越多的应用移动和云技术,导致边界保护越来越难。所以 Google 干脆一视同仁,不分内外部,用一样的安全手段去防御。

从攻防角度来看一下 Google 的 BeyondCorp 模型,例如访问 Google 内部应用http://blackberry.corp.google.com ,它会跳转到https://login.corp.google.com/ 也就是 Google Moma 系统,首先需要输入账号密码才能登陆,这个登录的过程中会针对设备信息、用户信息进行综合判定,账号密码正确以及设备信息通过规则引擎验证之后,会继续跳转到需要 YubiKey 登录界面,每个 Google 的员工都会有 Yubikey,通过 Yubikey 来做二次验证。Yubikey 的价值,Google 认为是可以完全杜绝钓鱼攻击的。另外类似的就是 Amazon 的 Midway-Auth 方式 ( https://midway-auth.amazon.com/login?next=%2F )。
3.png

Kubernetes 下容器零信任模型

容器下网络零信任

首先介绍一下容器下的网络零信任组件 Calico,Calico 是针对容器,虚拟机和基于主机的本机 Workload 的开源网络和网络安全解决方案产品。Calico 支持广泛的平台,包括 Kubernetes、OpenShift、Docker EE、OpenStack 和裸金属服务。零信任最大的价值就是即使攻击者通过其他各种手法破坏应用程序或基础架构,零信任网络也具有弹性。零信任架构使得使攻击者难以横向移动,针对性的踩点活动也更容易发现。

在容器网络零信任体系下,Calico+Istio 目前是比较热的一套解决方案;先来看看两者的一些差别,从差别上可以看到 Istio 是针对 Pod 层 Workload 的访问控制,以及 Calico 针对 Node 层的访问控制:

Istio Calico
Layer L3-L7 L3-L4
实现方式 用户态 内核态
策略执行点 Pod Node

下面重点讲解一下 Calico 组件和 Istio 的一些技术细节,Calico 构建了一个 3 层可路由网络,通过 Calico 的 Felix(是运行在 Node 的守护程序,它在每个 Node 资源上运行。Felix 负责编制路由和 ACL 规则以及在该 Node 主机上所需的任何其他内容,以便为该主机上的资源正常运行提供所需的网络连接)运行在每个 Node 上,主要做路由和 ACL 的策略以及搭建网络;通过运行在 Node 上的 Iptables 进行细粒度的访问控制。可以通过 Calico 设置默认 Deny 的策略,然后通过自适应的访问控制来进行最小化的访问控制策略的执行,从而构建容器下的零信任体系;Dikastes/Envoy:可选的 Kubernetes sidecars,可以通过相互 TLS 身份验证保护 Workload 到 Workload 的通信,并增加相关的控制策略;

4.png

Istio

再讲解 Istio 之前先讲一下微服务的一些安全需求和风险分析:

1、微服务被突破之后通过 Sniffer 监控流量,进而进行中间人攻击,为了解决这种风险需要对流量进行加密;
2、为了针对微服务和微服务之间的访问控制,需要双向 TLS 和细粒度的访问策略;
3、要审核谁在什么时候做了什么,需要审计工具;

分析了对应的风险之后,下面来解释一下 Istio 如何实现的零信任架构。首先很明显的一个特点就是全链路都是双向 mTLS 进行加密的,第二个特点就是微服务和微服务之间的访问也可以进行鉴权,通过权限访问之后还需要进行审计。Istio 是数据面和控制面进行分离的,控制面是通过 Pilot 将授权策略和安全命名信息分发给 Envoy,然后数据面通过 Envoy 来进行微服务的通信。在每个微服务的 Workload 上都会部署 Envoy,每个 Envoy 代理都运行一个授权引擎,该引擎在运行时授权请求。当请求到达代理时,授权引擎根据当前授权策略评估请求上下文,并返回授权结果 ALLOW 或 DENY。

5.png

微服务下的 Zero Trust API 安全

42Crunch( https://42crunch.com/ )将 API 安全从企业边缘扩展到了每个单独的微服务,并通过可大规模部署的超低延迟微 API 防火墙来进行保护。 42Crunch API 防火墙的部署模式是以 Kubernetes Pod 中以 Sidecar 代理模式部署,毫秒级别的性能响应。 这省去了编写和维护单个 API 安全策略过程,并实施了零信任安全体系结构,提升了微服务下的 API 安全性。42Crunch 的 API 安全能力包括:审核:运行 200 多个 OpenAPI 规范定义的安全审核测试,并进行详细的安全评分,以帮助开发人员定义和加强 API 安全;扫描:扫描实时 API 端点,以发现潜在的漏洞;保护:保护 API 并在应用上部署轻量级,低延迟 Micro API Firewall。

蚂蚁零信任架构体系落地最佳实践

随着 Service Mesh 架构的演进,蚂蚁已经开始在内部落地 Workload 场景下的服务鉴权能力,如何建设一套符合蚂蚁架构的 Workload 间的服务鉴权能力,我们将问题分为一下三个子问题:

1、Workload 的身份如何定义,如何能够实现一套通用的身份标识的体系;
2、Workload 间访问的授权模型的实现;
3、访问控制执行点如何选择。

Workload 身份定义 & 认证方式

蚂蚁内部使用 SPIFFE 项目中给出的 Identity 格式来描述 Workload 的身份,即:

spiffe://<domain>/cluster/<cluster>/ns/<namespace>

不过在工程落地过程中发现,这种维度的身份格式粒度不够细,并且与 K8s 对于 namespace 的划分规则有较强的耦合。蚂蚁的体量较大,场景较多,不同场景下 namespace 的划分规则都不完全一致。因此我们对格式进行了调整,在每一场景下梳理出能够标识一个 Workload 示例所须要的一组必备属性(例如应用名,环境信息等),并将这些属性携带在 Pod 的 Labels 中。调整后的格式如下:

spiffe://<domain>/cluster/<cluster>/<required_attr_1_name>/<required_attr_1_value>/<required_attr_2_name>/<required_attr_2_value>

配合这个身份格式标准,我们在 K8s API Server 中添加了 Validating Webhook 组件,对上述 Labels 中必须携带的属性信息进行校验。如果缺少其中一项属性信息,则实例 Pod 将无法创建。如下图所示:

6.png

在解决了 Workload 身份定义的问题后,剩下的就是如何将身份转换成某种可校验的格式,在 Workload 之间的服务调用链路中透传。为了支持不同的使用场景,我们选择了 X.509 证书与 JWT 这两种格式。

对于 Service Mesh 架构的场景,我们将身份信息存放在 X.509 证书的 Subject 字段中,以此来携带 Workload 的身份信息。如下图所示:

7.png

对于其他场景,我们将身份信息存放在 JWT 的 Claims 中,而 JWT 的颁发与校验,采用了 Secure Sidecar 提供服务。如下图所示:

8.png

授权模型

在项目落地的初期,使用 RBAC 模型来描述 Workload 间服务调用的授权策略。举例描述,应用 A 的某一个服务,只能被应用 B 调用。这种授权策略在大多数场景下都没有问题,不过在项目推进过程中,我们发现这种授权策略不适用于部分特殊场景。
我们考虑这样一个场景,生产网内部有一个应用 A,职责是对生产网内部的所有应用在运行时所需要使用的一些动态配置提供中心化的服务。这个服务的定义如下:A 应用 – 获取动态配置的 RPC 服务:

message FetchResourceRequest {
// The appname of invoker
string appname = 1;
// The ID of resource
string resource_id = 2;
}
message FetchResourceResponse {
string data = 1;
}
service DynamicResourceService {
rpc FetchResource (FetchResourceRequest) returns (FetchResourceResponse) {}
}

在此场景下,如果依然使用 RBAC 模型,应用 A 的访问控制策略将无法描述,因为所有应用都需要访问 A 应用的服务。但是这样会导致显而易见的安全问题,调用方应用 B 可以通过该服务获取到其它应用的资源。因此我们将 RBAC 模型升级为 ABAC 模型来解决上述问题。 我们采用 DSL 语言来描述 ABAC 的逻辑,并且集成在 Secure Sidecar 中。

访问控制执行点的选择

在执行点选择方面,考虑到 Service Mesh 架构推进需要一定的时间,我们提供了两不同的方式,可以兼容 Service Mesh 的架构,也可以兼容当前场景。

在 Service Mesh 架构场景下,RBAC Filter 和 ABAC Filter(Access Control Filter)集成在 Mesh Sidecar 中。

9.png

在当前场景下,我们目前提供了 JAVA SDK,应用需要集成 SDK 来完成所有认证和授权相关的逻辑。与 Service Mesh 架构场景类似,所有 Identity 的颁发、校验,授权与 Secure Sidecar 交互,由 Secure Sidecar 完成。

10.png

结语

零信任的核心是“Never Trust, Always Verify”,未来会继续深化零信任在整个阿里巴巴的实践,赋予不同的角色不同的身份,例如企业员工、应用、机器,并将访问控制点下沉到云原生基础设施的各个点,实现全局细粒度的控制,打造安全防护的新边界。本文从业界的零信任体系的落地最佳实践,到基于 Kubernetes 的零信任落地方式进行了简单的描述,本文只是抛砖引玉,希望能引发更多关于 Cloud Native 下的零信任架构体系的更多讨论和能看到更多的业界优秀的方案和产品能出现。

点击下载《不一样的 双11 技术:阿里巴巴经济体云原生实践》

ban.jpg

本书亮点

  • 双11 超大规模 K8s 集群实践中,遇到的问题及解决方法详述
  • 云原生化最佳组合:Kubernetes+容器+神龙,实现核心系统 100% 上云的技术细节
  • 双 11 Service Mesh 超大规模落地解决方案

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

双11 背后的全链路可观测性:阿里巴巴鹰眼在“云原生时代”的全面升级

alicloudnative阅读(3914)评论(0)

本文节选自《不一样的 双11 技术:阿里巴巴经济体云原生实践》一书。

作者:
周小帆(承嗣)  阿里云中间件技术部高级技术专家
王华锋(水彧)  阿里云中间件技术部技术专家
徐彤(绍宽)  阿里云中间件技术部技术专家
夏明(涯海)  阿里云中间件技术部技术专家

导读:作为一支深耕多年链路追踪技术 (Tracing) 与性能管理服务 (APM) 的团队,阿里巴巴中间件鹰眼团队的工程师们见证了阿里巴巴基础架构的多次升级,每一次的架构升级都会对系统可观测性能力 (Observability) 带来巨大挑战,而这次的“云原生”升级,给我们带来的新挑战又是什么呢?

云原生与可观测性

在刚刚过去的 2019 年 双11,我们再次见证了一个技术奇迹:这一次,我们花了一整年的时间,让阿里巴巴的核心电商业务全面上云,并且利用阿里云的技术基础设施顶住了 54 万笔/秒的零点交易峰值;我们的研发、运维模式,也正式步入了云原生时代。

云原生所倡导的新范式,给传统的研发和运维模式带来巨大冲击:微服务、DevOps 等理念让研发变得更高效,但带来的却是海量微服务的问题排查、故障定位的难度变得更大;容器化、Kubernetes 等容器编排技术的逐渐成熟让规模化软件交付变得容易,但带来的挑战是如何更精准地评估容量、调度资源,确保成本与稳定性的最好平衡。

今年阿里巴巴所探索的 Serverless、Service Mesh 等新技术,未来将彻底地从用户手中接管运维中间件以及 IaaS 层的工作,对于基础设施的自动化程度来讲则是一个更加巨大的挑战。

基础设施的自动化(Automation)是云原生的红利能够被充分释放的前提,而可观测性是一切自动化决策的基石

如果每个接口的执行效率、成败与否都能被精准统计、每一个用户请求的来龙去脉都能被完整追溯、应用之间以及应用与底层资源的依赖关系能被自动梳理,那我们就能基于这些信息自动判断业务的异常根因在哪?是否需要对影响业务的底层资源做迁移、扩容或是摘除?我们就能根据 双11 的峰值,自动推算出每一个应用所需准备资源是否充分且不浪费。

可观测性≠监控

许多人会问,“可观测性”是否就是“监控”换了一个说法,业界对这两件事的定义其实大相径庭。

不同于“监控”,监控更加注重问题的发现与预警,而“可观测性”的终极目标是为一个复杂分布式系统所发生的一切给出合理解释。监控更注重软件交付过程中以及交付后(Day 1 & Day 2),也就是我们常说的“事中与事后”,而“可观测性”则要为全研发与运维的生命周期负责。

回到“可观测性”本身,依旧是由老生常谈的“链路(Tracing)”“指标(Metric)”“日志(Logging)”构成,单独拉出来看都是非常成熟的技术领域。只不过这三样东西与云基础设施如何整合?它们之间如何更好地关联、融合在一起?以及它们如何更好地和云时代的在线业务做结合?是我们团队这一两年来努力探索的方向。

我们今年做了什么

今年的 双11,鹰眼团队的工程师们在四个新方向的技术探索,为集团业务全面上云、双11 的自动化备战与全局稳定性提供了强有力的保障:

面向场景化的业务可观测性

随着阿里巴巴电商业态不断的复杂与多元化,大促备战也不断趋向于精细化与场景化

以往的备战方式,是各个微服务系统的负责人根据所负责系统的自身以及上下游情况,各自为战。这样分而治之的做法虽然足够高效,却难免有疏漏,根本原因在于中台应用与实际业务场景的错位关系。以交易系统为例,一个交易系统会同时承载天猫、盒马、大麦、飞猪等多种类型的业务,而每种业务的预期调用量、下游依赖路径等均不相同,作为交易系统的负责人,很难梳理清楚每种业务的上下游细节逻辑对自身系统的影响。

今年鹰眼团队推出了一种场景化链路的能力,结合业务元数据字典,通过无侵入自动打标的手段实现流量染色,将实际的流量业务化,打通了业务与下游各中间件的数据,从以往以应用为中心的视图,转变成了以业务场景为中心,也因此更加贴近于真实的大促模型。

cs1.png

如上图所示,这是一个查询商品的案例,四个系统 A、B、C、D 分别提供“商品详情”、“商品类型”、“价格详情”和“优惠详情”的查询能力。入口应用 A 提供了一个商品查询的接口 S1,通过鹰眼,我们可以很快地发现应用 B、C、D 属于应用 A 的依赖,同时也是接口 S1 的下游,对于系统稳定性的治理而言,有这样一份链路数据已经足够。

但其实这样的视角并不具备业务的可观测性,因为在这样的一个依赖结构中包含着两种业务场景,这两种场景所对应的链路也是完全不同的:甲类商品所对应的链路是 A->B->C-D,而乙类商品对应的链路是A->B->C。假设日常态这两类商品的占比是 1:1,而大促态的占比是 1:9,那么仅从系统的角度或者业务的角度去梳理链路,是无法得到一个合理的流量预估模型的。

所以,如果我们能在系统层通过打标的方式把两种流量染色,就能很方便地梳理出两种业务场景所对应的的链路,这样一份更加精细化的视角对于保证业务的稳定性、以及更加合理的依赖梳理和限流降级策略的配置显得尤为重要。

这样的业务场景化能力在今年的 双11 备战中发挥了巨大的价值,很多业务系统都基于这样的能力梳理出了自己核心的业务链路,备战更加从容且不会有遗漏;同时,一系列的服务治理工具,在鹰眼的赋能下,进行了全面的场景化升级,例如针对场景化的流量录制和回放、场景化的故障演练工具、场景化的精准测试回归等等。配合这些更加贴合业务场景的服务治理工具,帮助整个 双11 备战的可观测性颗粒度走进了“高清时代”。

基于可观测性数据的智能根因定位

云原生时代,随着微服务等技术的引入,业务规模的增长,应用的实例数规模不断增长,核心业务的依赖也变得愈加复杂。一方面我们享受着开发效率的指数提升的红利,同时也在承受着故障定位成本居高不下的痛苦。特别是当业务出现问题的时候,如何快速发现问题和止血变得非常困难。鹰眼团队作为集团内应用性能的“守护神”,如何帮助用户快速完成故障定位成为今年的新挑战。

要完成故障定位,首先要回答,什么是你认为的故障?这背后需要运维人员对业务深层次的理解,很多维护人员喜欢使用穷举式的手段配上所有可观测性的指标,各种告警加上,显得有“安全感”,实际上当故障来临时,满屏出现指标异常、不断增加的告警短信,这样的“可观测性”看上去功能强大,实际效果却适得其反。

团队对集团内的历年故障做了一次仔细梳理,集团内的核心应用通常有四类故障(非业务自身逻辑问题):资源类、流量类、时延类、错误类。

再往下细分:

  1. 资源类:  比如 cpu、load、mem、线程数、连接池;
  2. 流量类:业务流量跌零 OR 不正常大幅度上涨下跌,中间件流量如消息提供的服务跌零等;
  3. 时延类:系统提供的服务 OR 系统依赖的服务,时延突然大幅度飙高了,基本都是系统有问题的前兆;
  4. 错误类:服务返回的错误的总数量,系统提供服务 OR 依赖服务的成功率。

有了上面这些故障分类作为抓手后,后面要做的就是“顺藤摸瓜”,可惜随着业务的复杂性,这根“藤”也来越长,以时延突增这个故障为例,其背后就隐藏着很多可能的根因:有可能是上游业务促销导致请求量突增导致,有可能是应用自身频繁 GC 导致应用整体变慢,还有可能是下游数据库负载过大导致响应变慢,以及数不胜数的其它各种原因。

鹰眼以前仅仅提供了这些指标信息,维护人员光看单条调用链数据,鼠标就要滚上好几番才能看完一条完整的 tracing 数据,更别说跨多个系统之间来回切换排查问题,效率也就无从谈起。

故障定位的本质就是一个不断排查、否定、再排查的过程,是一个“排除掉所有的不可能,剩下的就是真相”的过程。仔细想想可枚举的可能+可循环迭代的过程,这个不就是计算机最擅长的处理动作吗?故障定位智能化项目在这样的背景下诞生了。

提起智能化,很多人第一反应是把算法关联在一起,把算法过度妖魔化。其实了解机器学习的同学应该都知道:数据质量排第一,模型排第二,最后才是算法。数据采集的可靠性、完整性与领域模型建模才是核心竞争力,只有把数据化这条路走准确后,才有可能走智能化。

故障定位智能化的演进路线也是按照上面的思路来逐步完成的,但在这之前我们先得保障数据的质量:得益于鹰眼团队在大数据处理上深耕多年,数据的可靠性已经能得到非常高质量的保障,否则出现故障还得先怀疑是不是自己指标的问题。

接下来就是数据的完备性和诊断模型的建模,这两部分是智能化诊断的基石,决定了故障定位的层级,同时这两部分也是相辅相成的,通过诊断模型的构建可以对可观测性指标查漏补缺,通过补齐指标也可以增加诊断模型的深度。

主要通过以下 3 方面结合来不断地完善:

  • 第一,历史故障推演,历史故障相当于已经知道标准答案的考卷,通过部分历史故障+人工经验来构建最初的诊断模型,然后迭代推演其余的历史故障,但是这一步出来的模型容易出现过拟合现象;
  • 第二,利用混沌工程模拟常见的异常,不断修正模型;
  • 第三,线上人为打标的方式,来继续补齐可观测性指标、修正诊断模型。

经过以上三个阶段之后,这块基石基本建立完成了。接下来就要解决效率问题,从上面几步迭代出来的模型其实并不是最高效的,因为人的经验和思维是线性思维,团队内部针对现有模型做了两方面的工作:边缘诊断和智能剪枝。将定位的过程部分下沉到各个代理节点,对于一些可能对系统造成影响的现象自动保存事发现场关键信息同时上报关键事件,诊断系统自动根据各个事件权重进行定位路径智能调整。

智能根因定位上线后,累计帮助数千个应用完成故障根因定位,并取得了很高的客户满意度,基于根因定位结论为抓手,可观测性为基石,基础设施的自动化能力会得到大大提升。今年的 双11 大促备战期间,有了这样的快速故障定位功能,为应用稳定性负责人提供了更加自动化的手段。我们也相信在云原生时代,企业应用追求的运行的质量、成本、效率动态平衡不再是遥不可及,未来可期!

最后一公里问题定位能力

什么是“最后一公里”的问题定位?“最后一公里”的问题有哪些特点?为什么不是“最后一百米”、“最后一米”?

首先,我们来对齐一个概念,什么是“最后一公里”?在日常生活中,它具备以下特点:

  • 走路有点远,坐车又太近,不近不远的距离很难受;
  • 最后一公里的路况非常复杂,可能是宽阔大道,也可能是崎岖小路,甚至是宛如迷宫的室内路程(这点外卖小哥应该体会最深)。

那么分布式问题诊断领域的最后一公里又是指什么呢,它又具备哪些特征?

  • 在诊断流程上,此时已经离根因不会太远,基本是定位到了具体的应用、服务或节点,但是又无法确定具体的异常代码片段;
  • 能够定位根因的数据类型比较丰富,可能是内存占用分析,也可能是 CPU 占用分析,还可能是特定的业务日志/错误码,甚至只是单纯的从问题表象,结合诊断经验快速确定结论。

通过上面的分析,我们现在已经对最后一公里的概念有了一些共识。下面,我们就来详细介绍:如何实现最后一公里的问题定位?

首先,我们需要一种方法,可以准确的到达最后一公里的起点,也就是问题根因所在的应用、服务或是机器节点。这样可以避免根源上的无效分析,就像送外卖接错了订单。那么,如何在错综复杂的链路中,准确的定界根因范围?这里,我们需要使用 APM 领域较为常用的链路追踪(Tracing)的能力。通过链路追踪能够准确的识别、分析异常的应用、服务或机器,为我们最后一公里的定位指明方向。

然后,我们通过在链路数据上关联更多的细节信息,例如本地方法栈、业务日志、机器状态、SQL 参数等,从而实现最后一公里的问题定位,如下图所示:

cs2.png

  • 核心接口埋点: 通过在接口执行前后插桩埋点,记录的基础链路信息,包括 TraceId、RpcId(SpanId)、时间、状态、IP、接口名称等。上述信息可以还原最基础的链路形态;
  • 自动关联数据: 在调用生命周期内,可以自动记录的关联信息,包括 SQL、请求出入参数、异常堆栈等。此类信息不影响链路形态,但却是某些场景下,精准定位问题的必要条件;
  • 主动关联数据: 在调用生命周期内,需要人为主动记录的关联数据,通常是业务数据,比如业务日志、业务标识等。由于业务数据是非常个性化的,无法统一配置,但与链路数据主动关联后,可以大幅提升业务问题诊断效率;
  • 本地方法栈: 由于性能与成本限制,无法对所有方法添加链路埋点。此时,我们可以通过方法采样或在线插桩等手段实现精准的本地慢方法定位。

通过最后一公里的问题定位,能够在日常和大促备战态深度排查系统隐患,快速定位根因,下面举两个实际的应用案例:

  • 某应用在整体流量峰值时出现偶发性的 RPC 调用超时,通过分析自动记录的本地方法栈快照,发现实际耗时都是消耗在日志输出语句上,原因是 LogBack 1.2.x 以下的版本在高并发同步调用场景容易出现“热锁”,通过升级版本或调整为异步日志输出就彻底解决了该问题;
  • 某用户反馈订单异常,业务同学首先通过该用户的 UserId 检索出下单入口的业务日志,然后根据该日志中关联的链路标识 TraceId 将下游依赖的所有业务流程、状态与事件按实际调用顺序进行排列,快速定位了订单异常的原因(UID 无法自动透传到下游所有链路,但 TraceId 可以)。

监控告警往往只能反映问题的表象,最终问题的根因还需要深入到源码中去寻找答案。鹰眼今年在诊断数据的“精细采样”上取得了比较大的突破,在控制成本不膨胀的前提下,大幅提升了最后一公里定位所需数据的精细度与含金量。在整个双11 漫长的备战期中,帮助用户排除了一个又一个的系统风险源头,从而保障了大促当天的“丝般顺滑”。

全面拥抱云原生开源技术

过去一年,鹰眼团队拥抱开源技术,对业界主流的可观测性技术框架做了全面集成。我们在阿里云上发布了链路追踪(Tracing Analysis)服务,兼容 Jaeger(OpenTracing)、Zipkin、Skywalking 等主流的开源 Tracing 框架,已经使用了这些框架的程序,可以不用修改一行代码,只需要修改数据上报地址的配置文件,就能够以比开源自建低出许多的成本获得比开源 Tracing 产品强大许多的链路数据分析能力。

鹰眼团队同时也发布了全托管版的 Prometheus 服务,解决了开源版本部署资源占用过大、监控节点数过多时的写入性能问题,对于长范围、多维度查询时查询速度过慢的问题也做了优化。优化后的 Prometheus 托管集群在阿里巴巴内全面支持了 Service Mesh 的监控以及几个重量级的阿里云客户,我们也将许多优化点反哺回了社区。同样,托管版的 Prometheus 兼容开源版本,在阿里云的容器服务上也可以做到一键迁移到托管版。

可观测性与稳定性密不可分,鹰眼的工程师们今年将这些年来可观测性、稳定性建设等相关一系列的文章、工具做了整理,收录在了 Github 上,欢迎大家一起来进行共建。

ban.jpg

本书亮点

  • 双11 超大规模 K8s 集群实践中,遇到的问题及解决方法详述
  • 云原生化最佳组合:Kubernetes+容器+神龙,实现核心系统 100% 上云的技术细节
  • 双 11 Service Mesh 超大规模落地解决方案

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

关注你所关注的 – Golang社区调研报告

JFrog捷蛙阅读(2267)评论(0)

Golang语言可以说现在炙手可热,大家熟悉的Kubernates 就是使用Golang开发的。

我们在最近于伦敦和圣地亚哥举行的GopherCon大会上调查了1000多名开发者,以更好地了解Go 开发社区和对Go Module的总体看法。随着最近发布的Go 1.13版本,现在是向社区分享一些有趣数据的好时机。

BTW, 在JFrog,我们也是Go开发者(JFrog CLI和Xray都是用Go编写的)。 同时也是GoLang社区的贡献者, 并为社区维护贡献了公共注册中心 Gocenter(goproxy)

https://gocenter.io/. 加速Golang语言开发人员构建速度

以下是我们学到的一些关键的东西:

 

Go开发人员是高度投入的

绝大多数的Go开发者都在使用最新版本的GoLang。超过70%的受访者表示使用的是最新版本的Go 1.12。

同样值得注意的是,超过82%的Golang开发者使用的是1.11或更新的版本,因此能够使用Go Module。只有一小部分报告使用了Go的早期版本。

 

Go Modules 的使用率很高

几乎同样多的使用最新版本的Go开发人员也报告在他们的组织中采用了Go Module。也有相当多的人希望尽快使用Go Module,这样到2020年中期,其使用率将上升到至少80%,并可能接近100%。

 

 

GoLang被广泛应用于各个行业

虽然Go开发应用于广泛的计算领域,但DevOps仅排在Web开发之后,有近五分之一的报告称他们使用Go来实现这些目的。系统、数据库和网络应用程序的得分也很高,这表明在移动和嵌入式设备等有前途的领域还有很大的增长空间。

 

 

选择Go Module很难

当开发人员选择开源组件时,他们倾向于追求安全性和质量。但在如何判断 Go Module的风险方面,几乎没有共识。

大多数报告显示人们偏好用他人使用最多的包,以及流行度所带来的保证。同时安全漏洞也是一个问题,开发人员如何确定一个包的风险在各种各样的条件下是不同的,如内网环境和外网环境同一个漏洞的处理可能是不一样的。

 

 

如何更好更快的选择Go Module

由于开发人员在选择越来越多的Go模块时遇到了困难,GoCenter通过交互搜索帮助用户发现。一旦你有了结果,GoCenter会提供你需要的所有信息来决定哪些模块最适合你的需求,包括活动和星级。我们也正在开发一个系统,根据选择的标准来给模块打分,这将更好的帮助Golang开发人员更快的选择到合适的模块。

 

 

GoCenter

我们感谢所有对我们GoLang调查做出回应的开发者。我们从中获得的信息将帮助我们帮助您,特别是在我们继续使GoCenter成为帮助采用Go Module的有用工具的情况下。

如果您还没有听说过GoCenter,那么它提供了不可变的、版本化的模块的中心源,这些模块直接从公共源存储库中的Go项目进行处理和验证。在众多好处中,使用GoCenter可以加快GoLang应用程序的构建时间。

 GoCenter现在托管了超过260,000个版本化的Go Module,这些模块可以免费提供给Go开发者社区。

 

GoCenter与Golang 1.13的最佳实践

    1.使用Gocenter 作为Goproxy

在Golang 1.13中,Go Module的支持在默认情况下是启用的。尽管Go客户端的新安装会自动设置一个默认的google托管代理,在国内你可以覆盖它并使用你选择的Go Module代理,比如GoCenter。

要使用GoCenter作为版本化go模块的代理,请将GOPROXY环境变量设置为GoCenter URL:

Golang 1.13中的新特性允许您在GOPROXY中指定由逗号分隔的多个代理,以及直接从源代码下载模块的Direct 模式(这种模式的场景是goproxy中没有对应模块或版本是会返回404)。但是GoCenter目前支持了自动包含特性,意味着您在使用GoCenter进行代理时不需要使用这些Direct模式,当gocenter中目前没有缓存对用组件时,会自动触发到源码库中自动拉取对应组件以及版本。

https://github.com/jfrog/gocenter/blob/master/releases.md#2019-march-27-enhanced-automatic-inclusion-of-go-modules

 

  2.校验和(checksum)数据库支持

从Golang的1.13版开始,go get通过谷歌在sum.golang.org上维护的一个可审计的校验和数据库来执行模块的身份验证。版本1.13会使用GOSUMDB环境变量中默认设置这个校验和的DB URL。

GoCenter已经通过代理sum.golang.org帮助我们加速构建。如果你的GOPROXY设置为GoCenter,你不需要改变任何东西-你已经通过GoCenter验证你的模块了!

 

   3.私有Go Module 仓库

GoCenter用于代理通过公共源存储库(如Github)共享的Go Module。Golang 1.13还引入了一些环境变量,特别是GOPRIVATE,用于绕过代理和校验和验证,从私有存储库下载模块版本, 此场景比较适合企业内部有大量自研发Go Module。您可以在Golang文档中了解如何使用它们。

 

    4. 版本验证

在1.13版本中,为Go Module使用正确的后缀非常重要。go命令现在对请求的版本字符串执行额外的验证,如果模块不符合语义导入版本控制,go get将失败。因此,在Golang 1.13之前加载的模块的伪版本(pseudo version)可能会失败。