标签 istio 下的文章

Go正走在成为下一个企业级编程语言的轨道上

发展演化了十年的Go语言已经被证明了是云计算时代的首选编程语言,但Go的用武之地显然不局限于此。Kevin Goslar近期在Hacker Noon发表了一篇名为:《Go is on a Trajectory to Become the Next Enterprise Programming Language》的文章,阐述了Go可能成为下一个企业编程语言的理由,这里是那篇文章的中文译文,分享给大家。

img{512x368}

摘要

Go是一种专门为大规模软件开发而设计的编程语言。它提供了强大的开发体验并避免了现有编程语言存在的许多问题。这些因素使其成为最有可能在未来替代Java主导企业软件平台的候选者之一。对于那些寻求在未来几十年内构建大规模云基础架构的安全和前瞻性技术的公司和开源计划而言,我建议它们将Go视为其主要的编程语言。Go的优势如下:

  • 基于现实世界的经验
  • 专注于大型工程
  • 专注于可维护性
  • 保持简单明了
  • 使事情显式且明显
  • 很容易学习
  • 仅提供了一种做事方式
  • 支持简单地内置并发
  • 提供面向计算的语言原语
  • 使用OO – 好的部分
  • 拥有现代化的标准库
  • 强制执行标准化格式
  • 有一个非常快的编译器
  • 使交叉编译变得容易
  • 执行得非常快
  • 需要较小的内存占用
  • 部署规模小
  • 部署完全独立
  • 支持vendor依赖
  • 提供兼容性保证
  • 鼓励提供良好的文档
  • 商业支持的开源

请继续阅读有关上述每个优势点的更多详细信息。然而,在进入Go之前,你应该注意:

  • 不成熟的库
  • 即将到来的改变
  • 没有“硬实时”支持

简介

Go是Google开发的一种编程语言,在过去几年中取得了很大的成功。大部分现代云计算,网络和DevOps平台都是Go语言编写的,例如:DockerKubernetesTerraformETCDistio等。许多公司也将它用于通用软件开发。Go所具备的功能让这些项目吸引了大量用户,而Go的易用性也使得这些项目有了很多的贡献者。

Go的优势来自于简单和经过验证的想法的结合,同时避免了其他语言中出现的许多问题。这篇博客文章概述了Go背后的一些设计原则和工程智慧,并展示它们是如何结合在一起的 – 它们使Go成为下一代大型软件开发平台的优秀候选者。许多编程语言在个别领域都比较强大,但是在将所有领域都结合起来时,没有其他语言能够如此一致地“得分”,特别是在大型软件工程方面。

基于现实世界的经验

Go是由经验丰富的软件行业资深人士创建的,他们长期以来一直感受到现有语言的缺点带来的痛苦。几十年前,Rob PikeKen Thompson在Unix,C和Unicode的发明中发挥了重要作用。在实现了用于JavaScript和Java的V8和HotSpot虚拟机之后,Robert Griesemer在编译器和垃圾收集方面拥有着数十年的经验。在太多次的不得不等待他们的谷歌规模的C++/Java代码库的编译过程的推动下,他们开始着手创建一门新的编程语言,这门语言中凝聚了他们通过编写半个世纪代码过程中所学到的一切。

专注于大型工程

几乎任何编程语言都可以成功构建小型工程项目。当成千上万的开发人员在数十年的持续时间压力下在包含数千万行代码的大量代码库上进行协作时,真正痛苦的问题就会发生。这会导致以下问题:

  • 超长的编译时长会中断开发过程
  • 代码库由几个人/团队/部门/公司拥有,混合了不同的编程风格
  • 该公司雇佣了数千名工程师,架构师,测试人员,Ops专家,审计员,实习生等,他们需要了解代码库,但需要具有广泛的编码经验
  • 依赖于许多外部库或运行时,其中一些不再以其最初的形式存在
  • 每行代码在代码库的生命周期内平均被重写了10次,留下了疤痕,瑕疵和技术偏移
  • 文档不完整

Go专注于减轻这些大规模的工程难题,有时是以使小型工程变得更加繁琐为代价,例如在这里和那里需要一些额外的代码。

专注于可维护性

Go强调尽可能多地将工作转交到自动代码维护工具中。Go工具链提供了最常用的功能,如格式化代码和自动package导入、查找符号的定义和用法、简单的重构以及代码味道的识别。由于标准化的代码格式化和单一的惯用方式,机器生成的代码更改看起来非常接近Go中人为生成的更改。并而使用类似的模式,使得人和机器的协作更加无缝。

保持简单直接

初级程序员为简单问题创建简单的解决方案。高级程序员为复杂问题创建复杂的解决方案。伟大的程序员找到复杂问题的简单解决方案。-  查尔斯康奈尔

很多人都对Go不包含他们喜欢的其他语言概念感到惊讶。Go确实是一种非常小而简单的语言,只包含最少的正交和经过验证的概念。这鼓励开发人员以最少的认知开销编写最简单的代码,以便许多其他人可以理解并使用它。

使事情显式而明显

良好的代码是显而易见的,避免聪明,模糊的语言功能,扭曲的控制流和间接性。

许多语言都致力于使编写代码变得高效。然而,在其生命周期中,人们将花费大约(100倍)的时间阅读代码,而不是首先编写所需的代码。例如,审查,理解,调试,更改,重构或重用它。在查看代码时,通常只能看到并理解它的一小部分,通常没有对整个代码库的完整理解。为了解释这一点,Go将一切都显式化了。

一个例子是错误处理。让异常在各个点中断代码并使沿着调用链处理可能会更容易。Go需要手动处理或返回每个错误。这使得它可以准确地显示代码可以被中断的位置以及如何处理或包装错误。总的来说,这使得错误处理更容易编写,但更容易理解。

简单易学

Go非常小而且简单,可以在短短几天内研究整个语言及其基本概念。根据我们的经验,经过不超过一周的培训(与其他语言的以月为单位相比),初学者可以理解Go专家编写的代码,并为此做出贡献。为了方便大量人群,Go网站提供了所需的所有教程和深入的文章。这些教程在浏览器中运行,允许人们在将Go安装到本地计算机上之前学习和使用Go。

一种做事方式

Go语言通过个人自我表达赋予团队合作能力。

在Go(和Python)中,所有语言特征都是正交的并且彼此互补,通常做某事只有一种方法。如果您要求10位Python或Go程序员解决问题,您将获得10个相对类似的解决方案。不同的程序员在彼此的代码库中感觉更有家的感觉。在查看其他人的代码时,每分钟的WTF更少,而且人们的工作更好地融合在一起,从而形成一个人人都为之骄傲并且喜欢工作的一致性。这避免了大规模的工程问题,例如:

  • 开发人员将良好的工作代码视为“混乱”,并要求在他们可以使用之前重写它,因为他们不会像原作者那样思考。
  • 不同的团队成员在该语言的不同子集中编写相同代码库的部分内容。

img{512x368}
来源:https://www.osnews.com/story/19266/wtfsm

简单,内置并发

Go专为现代多核硬件而设计。

目前使用的大多数编程语言(Java,JavaScript,Python,Ruby,C,C ++)都是在20世纪80年代到2000年代设计的,当时大多数CPU只有一个计算核心。这就是为什么它们本质上是单线程的,并将并行化视为事后增加的边缘情况,通过诸如线程和同步点之类的附加组件实现,这些附加组件既麻烦又难以正确使用。第三方库提供了更简单的并发形式,如Actor模型,但总有多个选项可用,导致语言生态系统碎片化。今天的硬件拥有越来越多的计算内核,软件必须并行化才能在其上高效运行。Go是在多核CPU时代编写的,并且在语言中内置了简单,高级的CSP风格的并发特性。

面向计算的语言原语

在基础层面上,计算机系统接收数据,处理它(通常经过几个步骤),并输出结果数据。例如,Web服务器从客户端接收HTTP请求,并将其转换为一系列数据库或后端调用。一旦这些调用返回,它就会将接收到的数据转换为HTML或JSON并将其输出给调用者。Go的内置语言原语直接支持这种范例:

  • 结构体代表数据
  • reader和writer代表流式IO
  • 函数处理数据
  • goroutines提供(几乎无限制的)并发
  • 通道用于管理并发处理步骤之间的数据

由于所有计算原语都是由语言以直接的形式提供的,因此Go源代码可以更直接地表达服务器执行的操作。

OO – 好的部分

img{512x368}
在基类中改变某些东西的副作用

面向对象非常有用。这几十年OO的应用是富有成效的,并且让我们了解它的哪些部分比其他部分可以更好地扩展。基于这些认知,Go采用面向对象的新方法。它保留了封装和消息传递等优点。Go避免了继承,因为它现在被认为是有害的,Go为组合提供头等的支持

现代标准库

许多当前使用的编程语言(Java,JavaScript,Python,Ruby)是在互联网成为当今无处不在的计算平台之前设计的。因此,这些语言的标准库仅为未针对现代互联网优化的网络提供相对通用的支持。Go是十年前创建的,当时互联网已经全面展开。Go的标准库允许在没有第三方库的情况下创建更复杂的网络服务。这可以防止使用第三方库的常见问题:

  • 碎片化:实现相同功能的总有多种选择
  • 膨胀:库通常实现的不仅仅是它们的用途
  • 依赖地狱:库通常依赖于特定版本的其他库
  • 质量未知:第三方代码可能具有可疑的质量和安全性
  • 未知支持:第三方库的开发可以随时停止
  • 意外更改:第三方库通常不像标准库那样进行严格的版本管理

Russ Cox的更多背景信息。

标准化格式

Gofmt的风格是没有人喜欢的,但gofmt是每个人的最爱。 – Rob Pike

Gofmt是一种以标准化方式格式化Go代码的程序。它不是最漂亮的格式化方式,而是最简单,最不讨厌的方式。标准化的源代码格式化具有惊人的积极影响:

  • 重点讨论重要主题:它消除了围绕标签与空格,缩进深度,每行长度,空行,花括号放置等的一系列无意义的争论
  • 开发人员在彼此的代码库中感到宾至如归,因为其他代码看起来很像他们编写的代码。每个人都喜欢自由地按照自己喜欢的方式格式化代码,但如果其他人冒昧地按照他们自己喜欢的方式格式化>代码,那么每个人都讨厌它。
  • 自动代码更改不会弄乱手写代码的格式,例如通过引入意外的空白更改。

许多其他语言社区现在正在开发gofmt等价物。当构建为第三方解决方案时,通常会有几种竞争格式标准。例如,JavaScript世界提供PrettierStandardJS。可以一起使用其中之一或两者。许多JS项目都没有采用它们,因为这是一个额外的决定。Go的格式化程序内置于该语言的标准工具链中,因此只有一个标准,每个人都在使用它。

快速编译

img{512x368}
来源:https://xkcd.com/303

大型代码库的长编译时间是引发Go语言起源的一个微小的原因。Google主要使用C++和Java,与Haskell,Scala或Rust等更复杂的语言相比,它可以相对快速地编译。尽管如此,当编译大型代码库时,即使是少量的慢速也会把人激怒,编译工作流中断导致编译延迟。Go是从头开始设计的,以使编译更有效,因此编译器速度非常快,几乎没有编译延迟。这为Go开发人员提供了类似于脚本语言的即时反馈,并具有静态类型检查的额外好处。

交叉编译

由于语言运行时非常简单,因此它已被移植到许多平台,如macOS,Linux,Windows,BSD,ARM等。Go可以开箱即用于编译所有这些平台的二进制文件。这使得我们可以轻松地从一台机器来进行部署。

快速执行

Go有着接近C的速度。与JITed(即时编译)语言(Java,JavaScript,Python等)不同,Go二进制文件不需要启动或预热时间,因为它们作为已编译和完全优化的本机代码提供。Go垃圾收集器仅以微秒的指令引入可忽略的暂停。在其快速的单核性能上面,Go使得利用所有的CPU内核更容易

小内存占用

像JVM,Python或Node这样的运行时不仅仅在运行时加载程序代码。它们还会加载大型且高度复杂的基础架构,以便在每次运行时编译和优化程序。这使得它们的启动时间变慢并导致它们使用大量(数百MB)的RAM。Go进程的开销较小,因为它们已经完全编译和优化,只需要运行。Go还以非常节省内存的方式存储数据。这在内存有限且昂贵的云环境中以及在开发期间非常重要,在开发期间我们希望在单个机器上快速启动整个堆栈,同时为其他软件留下内存。

小部署规模

Go二进制文件的大小非常简洁。Go应用程序的Docker镜像通常比用Java或Node编写的等效文件小10倍,因为它不需要包含编译器,JIT,并且需要更少的运行时基础结构。这在部署大型应用程序时很重要。想象一下,将一个简单的应用程序部署到100个生产服务器上 使用Node / JVM时,我们的docker仓库必须提供100个docker镜像@ 200 MB = 20 GB(总共)。这需要镜像仓库耗费一些时间来服务。想象一下,我们希望每天部署100次。使用Go服务时,Docker镜像仓库只需提供100个Docker镜像@ 20 MB = 2 GB。可以更快,更频繁地部署大型Go应用程序,从而允许重要更新更快地实现生产。

自包含部署

Go应用程序部署为包含所有依赖项的单个可执行文件。不需要安装特定版本的JVM,Node或Python运行时。不必将库下载到生产服务器上。不需要对运行Go二进制文件的机器进行任何更改。甚至不需要将Go二进制文件包装到Docker中来共享它们。您只需将Go二进制文件拖放到服务器上,无论该服务器上运行的是什么,它都会在那里运行。上述描述的唯一例外是使用net和os/user包时的动态链接glibc库时。

vendor依赖关系

Go故意避免使用第三方库的中央存储库。Go应用程序直接链接到相应的Git存储库,并将所有相关代码下载(vendor保存)到他们自己的代码库中。这有很多好处:

  • 我们可以在使用之前查看,分析和测试第三方代码。此代码与我们自己的代码一样,是我们应用程序的一部分,应符合相同的质量,安全性和可靠性标准。
  • 无需永久访问存储依赖项的各个位置。可以一次性的从任何地方(包括私人Git仓库)获取您的第三方库,并永久拥有它们。
  • 在checkout后编译代码库不需要进一步下载依赖项。
  • 如果互联网上某处的代码存储库突然提供不同的代码,也不会造成surprises。
  • 即使软件包存储库服务性能变慢或托管软件包不再存在,部署也不会中断。

兼容性保证

Go团队承诺,现有的程序将继续适用于新版本语言。这使得即使是大型项目也可以轻松升级到更新编译器的版本,并从新版本带来的许多性能和安全性改进中受益。同时,由于Go二进制文件包含了他们需要的所有依赖项,因此可以在同一服务器计算机上并行运行使用不同版本的Go编译器编译的二进制文件,而无需进行复杂的设置多个版本的运行时或虚拟化。

文档

在大型工程中,文档对于使软件易于访问和维护非常重要。与其他功能类似,Go中的文档简单实用:

  • 它嵌入在源代码中,因此两者可以同时维护。
  • 它不需要特殊的语法 – 文档只是普通的源代码注释。
  • 可运行的单元测试通常是最好的文档形式,所以Go允许你将它们嵌入到文档中
  • 所有文档实用程序都内置在工具链中,因此每个人都使用它们。
  • Go linter需要导出元素的文档,以防止“文档债务”的积累。

商业支持的开源

当商业实体在公开场合发展时,一些最流行和最全面设计的软件就会发生。这种设置结合了商业软件开发的优势 – 一致性和优化,使系统健壮,可靠,高效 – 具有开放式开发的优势,如来自许多行业的广泛支持,来自多个大型实体和许多用户的支持,以及长期支持,即使商业支持停止。Go就是这样开发的。

缺点

当然,Go并不完美,每种技术选择总是有利有弊。在进入Go之前,这里有一小部分需要考虑的方面。

未成熟

虽然Go的标准库在支持HTTP/2服务器推送等许多新概念方面处于行业领先地位,但与JVM生态系统中存在的相比,用于外部API的第三方Go库可能还不那么成熟。

即将到来的变化

Go团队知道几乎不可能改变现有的语言元素,因此只有在完全开发后才会添加新功能。在经历了10年稳定的故意阶段后,Go团队正在考虑对语言进行一系列更大的改进,作为Go 2.0之旅的一部分。

没有硬实时

虽然Go的垃圾收集器只引入了非常短的中断,但支持硬实时需要没有垃圾收集的技术,例如Rust。

结论

这篇博客文章给出了一些明智的背景知识,但往往没有那么明显的选择进入Go的设计,以及当他们的代码库和团队成数量级增长时,他们将如何从许多痛苦中拯救大型工程项目。总的来说,他们将Go定位为寻求Java之外的现代编程语言的大型开发项目的绝佳选择。


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

使用nomad实现集群管理和微服务部署调度

“云原生”“容器化”“微服务”“服务网格”等概念大行其道的今天,一提到集群管理、容器工作负载调度,人们首先想到的是Kubernetes

Kubernetes经过多年的发展,目前已经成为了云原生计算平台的事实标准,得到了诸如谷歌、微软、红帽、亚马逊、IBM、阿里等大厂的大力支持,各大云计算提供商也都提供了专属Kubernetes集群服务。开发人员可以一键在这些大厂的云上创建k8s集群。对于那些不愿被cloud provider绑定的组织或开发人员,Kubernetes也提供了诸如Kubeadm这样的k8s集群引导工具,帮助大家在裸金属机器上搭建自己的k8s集群,当然这样做的门槛较高(如果您想学习自己搭建和管理k8s集群,可以参考我在慕课网上发布的实战课《高可用集群搭建、配置、运维与应用》)。

Kubernetes的学习曲线是公认的较高,尤其是对于应用开发人员。再加上Kubernetes发展很快,越来越多的概念和功能加入到k8s技术栈,这让人们不得不考虑建立和维护这样一套集群所要付出的成本。人们也在考虑是否所有场景都需要部署一个k8s集群,是否有轻量级的且能满足自身需求的集群管理和微服务部署调度方案呢?外国朋友Matthias Endler就在其文章《也许你不需要Kubernetes》中给出一个轻量级的集群管理方案 – 使用hashicorp开源的nomad工具

这让我想起了去年写的《基于consul实现微服务的服务发现和负载均衡》一文。文中虽然实现了基于consul的服务注册、发现以及负载均衡,但是缺少一个环节:那就是整个集群管理以及工作负载部署调度自动化的缺乏。nomad应该恰好可以补足这一短板,并且它足够轻量。本文我们就来探索和实践一下使用nomad实现集群管理和微服务部署调度。

一. 安装nomad集群

nomad是Hashicorp公司出品的集群管理和工作负荷调度器,支持多种驱动形式的工作负载调度,包括Docker容器、虚拟机、原生可执行程序等,并支持跨数据中心调度。Nomad不负责服务发现或密钥管理等 ,它将这些功能分别留给了HashiCorp的ConsulVault。HashiCorp的创始人认为,这会使得Nomad更为轻量级,调度性能更高。

nomad使用Go语言实现,因此其本身仅仅是一个可执行的二进制文件。和Hashicorp其他工具产品(诸如:consul等)类似,nomad一个可执行文件既可以以server模式运行,亦可以client模式运行,甚至可以启动一个实例,既是server,也是client。

下面是nomad集群的架构图(来自hashicorp官方):

img{512x368}

一个nomad集群至少要包含一个server,作为集群的控制平面;一个或多个client则用于承载工作负荷。通常生产环境nomad集群的控制平面至少要有5个及以上的server才能在高可用上有一定保证。

建立一个nomad集群有多种方法,包括手工建立、基于consul自动建立和基于云自动建立。考虑到后续涉及微服务的注册发现,这里我们采用基于consul自动建立nomad集群的方法,下面是部署示意图:

img{512x368}

我这里的试验环境仅有三台hosts,因此这三台host既承载consul集群,也承载nomad集群(包括server和client),即nomad的控制平面和工作负荷由这三台host一并承担了。

1. consul集群启动

在之前的《基于consul实现微服务的服务发现和负载均衡》一文中,我对consul集群的建立做过详细地说明,因此这里只列出步骤,不详细解释了。注意:这次consul的版本升级到了consul v1.4.4了。

在每个node上分别下载consul 1.4.4:

# wget -c https://releases.hashicorp.com/consul/1.4.4/consul_1.4.4_linux_amd64.zip
# unzip consul_1.4.4_linux_amd64.zip

# cp consul /usr/local/bin

# consul -v

Consul v1.4.4
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)

启动consul集群:(每个node上创建~/.bin/consul-install目录,并进入该目录下执行)

dxnode1:

# nohup consul agent -server -ui -dns-port=53 -bootstrap-expect=3 -data-dir=~/.bin/consul-install/consul-data -node=consul-1 -client=0.0.0.0 -bind=172.16.66.102 -datacenter=dc1 > consul-1.log & 2>&1

dxnode2:

# nohup consul agent -server -ui -dns-port=53  -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-2 -client=0.0.0.0 -bind=172.16.66.103 -datacenter=dc1 -join 172.16.66.102 > consul-2.log & 2>&1

dxnode3:

nohup consul agent -server -ui -dns-port=53  -bootstrap-expect=3 -data-dir=/root/consul-install/consul-data -node=consul-3 -client=0.0.0.0 -bind=172.16.66.104 -datacenter=dc1 -join 172.16.66.102 > consul-3.log & 2>&1

consul集群启动结果查看如下:

# consul members
Node      Address             Status  Type    Build  Protocol  DC   Segment
consul-1  172.16.66.102:8301  alive   server  1.4.4  2         dc1  <all>
consul-2  172.16.66.103:8301  alive   server  1.4.4  2         dc1  <all>
consul-3  172.16.66.104:8301  alive   server  1.4.4  2         dc1  <all>

# consul operator raft list-peers
Node      ID                                    Address             State     Voter  RaftProtocol
consul-3  d048e55b-5f6a-34a4-784c-e6607db0e89e  172.16.66.104:8300  leader    true   3
consul-1  160a7a20-f177-d2f5-0765-e6d1a9a1a9a4  172.16.66.102:8300  follower  true   3
consul-2  6795cd2c-fad5-9d4f-2531-13b0a65e0893  172.16.66.103:8300  follower  true   3

2. DNS设置(可选)

如果采用基于consul DNS的方式进行服务发现,那么在每个nomad client node上设置DNS则很必要。否则如果要是基于consul service catalog的API去查找service,则可忽略这个步骤。设置步骤如下:

在每个node上,创建和编辑/etc/resolvconf/resolv.conf.d/base,填入如下内容:

nameserver {consul-1-ip}
nameserver {consul-2-ip}

然后重启resolvconf服务:

#  /etc/init.d/resolvconf restart
[ ok ] Restarting resolvconf (via systemctl): resolvconf.service.

新的resolv.conf将变成:

# cat /etc/resolv.conf
# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)
#     DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
nameserver {consul-1-ip}
nameserver {consul-2-ip}
nameserver 100.100.2.136
nameserver 100.100.2.138
options timeout:2 attempts:3 rotate single-request-reopen

这样无论是在host上,还是在新启动的container里就都可以访问到xx.xx.consul域名的服务了:

# ping -c 3 consul.service.dc1.consul
PING consul.service.dc1.consul (172.16.66.103) 56(84) bytes of data.
64 bytes from 172.16.66.103: icmp_seq=1 ttl=64 time=0.227 ms
64 bytes from 172.16.66.103: icmp_seq=2 ttl=64 time=0.158 ms
^C
--- consul.service.dc1.consul ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.158/0.192/0.227/0.037 ms

# docker run busybox ping -c 3 consul.service.dc1.consul

PING consul.service.dc1.consul (172.16.66.104): 56 data bytes
64 bytes from 172.16.66.104: seq=0 ttl=64 time=0.067 ms
64 bytes from 172.16.66.104: seq=1 ttl=64 time=0.061 ms
64 bytes from 172.16.66.104: seq=2 ttl=64 time=0.076 ms

--- consul.service.dc1.consul ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.061/0.068/0.076 ms

3. 基于consul集群引导启动nomad集群

按照之前的拓扑图,我们需先在每个node上分别下载nomad:

# wget -c https://releases.hashicorp.com/nomad/0.8.7/nomad_0.8.7_linux_amd64.zip

# unzip nomad_0.8.7_linux_amd64.zip.zip

# cp ./nomad /usr/local/bin

# nomad -v

Nomad v0.8.7 (21a2d93eecf018ad2209a5eab6aae6c359267933+CHANGES)

我们已经建立了consul集群,因为我们将采用基于consul集群引导启动nomad集群这一创建nomad集群的最Easy方式。同时,我们每个node上既要运行nomad server,也要nomad client,于是我们在nomad的配置文件中,对server和client都设置为”enabled = true”。下面是nomad启动的配置文件,每个node上的nomad均将该配置文件作为为输入:

// agent.hcl

data_dir = "/root/.bin/nomad-install/nomad.d"

server {
  enabled = true
  bootstrap_expect = 3
}

client {
  enabled = true
}

下面是在各个节点上启动nomad的操作步骤:

dxnode1:

# nohup nomad agent -config=/root/.bin/nomad-install/agent.hcl  > nomad-1.log & 2>&1

dxnode2:

# nohup nomad agent -config=/root/.bin/nomad-install/agent.hcl  > nomad-2.log & 2>&1

dxnode3:

# nohup nomad agent -config=/root/.bin/nomad-install/agent.hcl  > nomad-3.log & 2>&1

查看nomad集群的启动结果:

#  nomad server members
Name            Address        Port  Status  Leader  Protocol  Build  Datacenter  Region
dxnode1.global  172.16.66.102  4648  alive   true    2         0.8.7  dc1         global
dxnode2.global  172.16.66.103  4648  alive   false   2         0.8.7  dc1         global
dxnode3.global  172.16.66.104  4648  alive   false   2         0.8.7  dc1         global

# nomad operator raft list-peers

Node            ID                  Address             State     Voter  RaftProtocol
dxnode1.global  172.16.66.102:4647  172.16.66.102:4647  leader    true   2
dxnode2.global  172.16.66.103:4647  172.16.66.103:4647  follower  true   2
dxnode3.global  172.16.66.104:4647  172.16.66.104:4647  follower  true   2

# nomad node-status
ID        DC   Name     Class   Drain  Eligibility  Status
7acdd7bc  dc1  dxnode1  <none>  false  eligible     ready
c281658a  dc1  dxnode3  <none>  false  eligible     ready
9e3ef19f  dc1  dxnode2  <none>  false  eligible     ready

以上这些命令的结果都显示nomad集群工作正常!

nomad还提供一个ui界面(http://nomad-node-ip:4646/ui),可以让运维人员以可视化的方式直观看到当前nomad集群的状态,包括server、clients、工作负载(job)的情况:

img{512x368}

nomad ui首页

img{512x368}

nomad server列表和状态

img{512x368}

nomad client列表和状态

二. 部署工作负载

引导启动成功nomad集群后,我们接下来就要向集群中添加“工作负载”了。

Kubernetes中,我们可以通过创建deployment、pod等向集群添加工作负载;在nomad中我们也可以通过类似的声明式的方法向nomad集群添加工作负载。不过nomad相对简单许多,它仅提供了一种名为job的抽象,并给出了job的specification。nomad集群所有关于工作负载的操作均通过job描述文件和nomad job相关子命令完成。下面是通过job部署工作负载的流程示意图:

img{512x368}

从图中可以看到,我们需要做的仅仅是将编写好的job文件提交给nomad即可。

Job spec定义了:job -> group -> task的层次关系。每个job文件只有一个job,但是一个job可能有多个group,每个group可能有多个task。group包含一组要放在同一个集群中调度的task。一个Nomad task是由其驱动程序(driver)在Nomad client节点上执行的命令、服务、应用程序或其他工作负载。task可以是短时间的批处理作业(batch)或长时间运行的服务(service),例如web应用程序、数据库服务器或API。

Tasks是在用HCL语法的声明性job规范中定义的。Job文件提交给Nomad服务端,服务端决定在何处以及如何将job文件中定义的task分配给客户端节点。另一种概念化的理解是:job规范表示工作负载的期望状态,Nomad服务端创建并维护其实际状态。

通过job,开发人员还可以为工作负载定义约束和资源。约束(constraint)通过内核类型和版本等属性限制了工作负载在节点上的位置。资源(resources)需求包括运行task所需的内存、网络、CPU等。

有三种类型的job:system、service和batch,它们决定Nomad将用于此job中task的调度器。service 调度器被设计用来调度永远不会宕机的长寿命服务。batch作业对短期性能波动的敏感性要小得多,寿命也很短,几分钟到几天就可以完成。system调度器用于注册应该在满足作业约束的所有nomad client上运行的作业。当某个client加入到nomad集群或转换到就绪状态时也会调用它。

Nomad允许job作者为自动重新启动失败和无响应的任务指定策略,并自动将失败的任务重新调度到其他节点,从而使任务工作负载具有弹性。

如果对应到k8s中的概念,group更像是某种controller,而task更类似于pod,是被真实调度的实体。Job spec对应某个k8s api object的spec,具体体现在某个yaml文件中。

下面我们就来真实地在nomad集群中创建一个工作负载。我们使用之前在《基于consul实现微服务的服务发现和负载均衡》一文中使用过的那几个demo image,这里我们先使用httpbackendservice镜像来创建一个job。

下面是httpbackend的job文件:

// httpbackend-1.nomad

job "httpbackend" {
  datacenters = ["dc1"]
  type = "service"

  group "httpbackend" {
    count = 2

    task "httpbackend" {
      driver = "docker"
      config {
        image = "bigwhite/httpbackendservice:v1.0.0"
        port_map {
          http = 8081
        }
        logging {
          type = "json-file"
        }
      }

      resources {
        network {
          mbits = 10
          port "http" {}
        }
      }

      service {
        name = "httpbackend"
        port = "http"
      }
    }
  }
}

这个文件基本都是自解释的,重点提几个地方:

  • job type: service : 说明该job创建和调度的是一个service类型的工作负载;

  • count = 2 : 类似于k8s的replicas字段,期望在nomad集群中运行2个httpbackend服务实例,nomad来保证始终处于期望状态。

  • 关于port:port_map指定了task中容器的监听端口。network中的port “http” {}没有指定静态IP,因此将采用动态主机端口。service中的port则指明使用”http”这个tag的动态主机端口。这和k8s中service中port使用名称匹配的方式映射到具体pod中的port的方法类似。

我们使用nomad job子命令来创建该工作负载。正式创建之前,我们可以先通过nomad job plan来dry-run一下,一是看job文件格式是否ok;二来检查一下nomad集群是否有空余资源创建和调度新的工作负载:

# nomad job plan httpbackend-1.nomad
+/- Job: "httpbackend"
+/- Stop: "true" => "false"
    Task Group: "httpbackend" (2 create)
      Task: "httpbackend"

Scheduler dry-run:
- All tasks successfully allocated.

Job Modify Index: 4248
To submit the job with version verification run:

nomad job run -check-index 4248 httpbackend-1.nomad

When running the job with the check-index flag, the job will only be run if the
server side version matches the job modify index returned. If the index has
changed, another user has modified the job and the plan's results are
potentially invalid.

如果plan的输出结果没有问题,则可以用nomad job run正式创建和调度job:

# nomad job run httpbackend-1.nomad
==> Monitoring evaluation "40c63529"
    Evaluation triggered by job "httpbackend"
    Allocation "6b0b83de" created: node "9e3ef19f", group "httpbackend"
    Allocation "d0710b85" created: node "7acdd7bc", group "httpbackend"
    Evaluation status changed: "pending" -> "complete"
==> Evaluation "40c63529" finished with status "complete"

接下来,我们可以使用nomad job status命令查看job的创建情况以及某个job的详细状态信息:

# nomad job status
ID                  Type     Priority  Status   Submit Date
httpbackend         service  50        running  2019-03-30T04:58:09+08:00

# nomad job status httpbackend
ID            = httpbackend
Name          = httpbackend
Submit Date   = 2019-03-30T04:58:09+08:00
Type          = service
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group   Queued  Starting  Running  Failed  Complete  Lost
httpbackend  0       0         2        0       0         0

Allocations
ID        Node ID   Task Group   Version  Desired  Status    Created    Modified
6b0b83de  9e3ef19f  httpbackend  11       run      running   8m ago     7m50s ago
d0710b85  7acdd7bc  httpbackend  11       run      running   8m ago     7m39s ago

前面说过,nomad只是集群管理和负载调度,服务发现它是不管的,并且服务发现的问题早已经被consul解决掉了。所以httpbackend创建后,要想使用该服务,我们还得走consul提供的路线:

DNS方式(前面已经做过铺垫了):

# dig SRV httpbackend.service.dc1.consul

; <<>> DiG 9.10.3-P4-Ubuntu <<>> SRV httpbackend.service.dc1.consul
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7742
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 5
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;httpbackend.service.dc1.consul.    IN    SRV

;; ANSWER SECTION:
httpbackend.service.dc1.consul.    0 IN    SRV    1 1 23578 consul-1.node.dc1.consul.
httpbackend.service.dc1.consul.    0 IN    SRV    1 1 22819 consul-2.node.dc1.consul.

;; ADDITIONAL SECTION:
consul-1.node.dc1.consul. 0    IN    A    172.16.66.102
consul-1.node.dc1.consul. 0    IN    TXT    "consul-network-segment="
consul-2.node.dc1.consul. 0    IN    A    172.16.66.103
consul-2.node.dc1.consul. 0    IN    TXT    "consul-network-segment="

;; Query time: 471 msec
;; SERVER: 172.16.66.102#53(172.16.66.102)
;; WHEN: Sat Mar 30 05:07:54 CST 2019
;; MSG SIZE  rcvd: 251

# curl http://172.16.66.102:23578
this is httpbackendservice, version: v1.0.0

# curl http://172.16.66.103:22819
this is httpbackendservice, version: v1.0.0

或http api方式(可通过官方API查询服务):

# curl http://127.0.0.1:8500/v1/health/service/httpbackend

[
    {
        "Node": {"ID":"160a7a20-f177-d2f5-0765-e6d1a9a1a9a4","Node":"consul-1","Address":"172.16.66.102","Datacenter":"dc1","TaggedAddresses":{"lan":"172.16.66.102","wan":"172.16.66.102"},"Meta":{"consul-network-segment":""},"CreateIndex":7,"ModifyIndex":10},
        "Service": {"ID":"_nomad-task-5uxc3b7hjzivbklslt4yj5bpsfagibrb","Service":"httpbackend","Tags":[],"Address":"172.16.66.102","Meta":null,"Port":23578,"Weights":{"Passing":1,"Warning":1},"EnableTagOverride":false,"ProxyDestination":"","Proxy":{},"Connect":{},"CreateIndex":30727,"ModifyIndex":30727},
        "Checks": [{"Node":"consul-1","CheckID":"serfHealth","Name":"Serf Health Status","Status":"passing","Notes":"","Output":"Agent alive and reachable","ServiceID":"","ServiceName":"","ServiceTags":[],"Definition":{},"CreateIndex":7,"ModifyIndex":7}]
    },
    {
        "Node": {"ID":"6795cd2c-fad5-9d4f-2531-13b0a65e0893","Node":"consul-2","Address":"172.16.66.103","Datacenter":"dc1","TaggedAddresses":{"lan":"172.16.66.103","wan":"172.16.66.103"},"Meta":{"consul-network-segment":""},"CreateIndex":5,"ModifyIndex":5},
        "Service": {"ID":"_nomad-task-hvqnbklzqr6q5mpspqcqbnhxdil4su4d","Service":"httpbackend","Tags":[],"Address":"172.16.66.103","Meta":null,"Port":22819,"Weights":{"Passing":1,"Warning":1},"EnableTagOverride":false,"ProxyDestination":"","Proxy":{},"Connect":{},"CreateIndex":30725,"ModifyIndex":30725},
        "Checks": [{"Node":"consul-2","CheckID":"serfHealth","Name":"Serf Health Status","Status":"passing","Notes":"","Output":"Agent alive and reachable","ServiceID":"","ServiceName":"","ServiceTags":[],"Definition":{},"CreateIndex":8,"ModifyIndex":8}]
    }
]

三. 将服务暴露到外部以及负载均衡

集群内部的东西向流量可以通过consul的服务发现来实现,南北向流量则需要我们将部分服务暴露到外部才能实现流量导入。在《基于consul实现微服务的服务发现和负载均衡》一文中,我们是通过nginx实现服务暴露和负载均衡的,但是需要consul-template的协助,并且自己需要实现一个nginx的配置模板,门槛较高也比较复杂。

nomad的官方文档推荐了fabio这个反向代理和负载均衡工具。fabio最初由位于荷兰的“eBay Classifieds Group”开发,它为荷兰(marktplaats.nl),澳大利亚(gumtree.com.au)和意大利(www.kijiji.it)的一些最大网站提供支持。自2015年9月以来,它为这些站点提供23000个请求/秒的处理能力(性能应对一般中等流量是没有太大问题的),没有发现重大问题。

与consul-template+nginx的组合不同,fabio无需开发人员做任何二次开发,也不需要自定义模板,它直接从consul读取service list并生成相关路由。至于哪些服务要暴露在外部,路由形式是怎样的,是需要在服务启动时为服务设置特定的tag,fabio定义了一套灵活的路由匹配描述方法。

下面我们就来部署fabio,并将上面的httpbackend暴露到外部。

1. 部署fabio

fabio也是nomad集群的一个工作负载,因此我们可以像普通job那样部署fabio。我们先来使用nomad官方文档中给出fabio.nomad:

//fabio.nomad

job "fabio" {
  datacenters = ["dc1"]
  type = "system"

  group "fabio" {
    task "fabio" {
      driver = "docker"
      config {
        image = "fabiolb/fabio"
        network_mode = "host"
        logging {
          type = "json-file"
        }
      }

      resources {
        cpu    = 200
        memory = 128
        network {
          mbits = 20
          port "lb" {
            static = 9999
          }
          port "ui" {
            static = 9998
          }
        }
      }
    }
  }
}

这里有几点值得注意:

  1. fabio job的类型是”system”,也就是说该job会被部署到job可以匹配到(通过设定的约束条件)的所有nomad client上,且每个client上仅部署一个实例,有些类似于k8s的daemonset控制下的pod;

  2. network_mode = “host” 告诉fabio的驱动docker:fabio容器使用host网络,即与主机同网络namespace;

  3. static = 9999和static = 9998,说明fabio在每个nomad client上监听固定的静态端口而不是使用动态端口。这也要求了每个nomad client上不允许存在与fabio端口冲突的应用启动。

我们来plan和run一下这个fabio job:

# nomad job plan fabio.nomad

+ Job: "fabio"
+ Task Group: "fabio" (3 create)
  + Task: "fabio" (forces create)

Scheduler dry-run:
- All tasks successfully allocated.

Job Modify Index: 0
To submit the job with version verification run:

nomad job run -check-index 0 fabio.nomad

When running the job with the check-index flag, the job will only be run if the
server side version matches the job modify index returned. If the index has
changed, another user has modified the job and the plan's results are
potentially invalid.

# nomad job run fabio.nomad
==> Monitoring evaluation "97bfc16d"
    Evaluation triggered by job "fabio"
    Allocation "1b77dcfa" created: node "c281658a", group "fabio"
    Allocation "da35a778" created: node "7acdd7bc", group "fabio"
    Allocation "fc915ab7" created: node "9e3ef19f", group "fabio"
    Evaluation status changed: "pending" -> "complete"
==> Evaluation "97bfc16d" finished with status "complete"

查看一下fabio job的运行状态:

# nomad job status fabio

ID            = fabio
Name          = fabio
Submit Date   = 2019-03-27T14:30:29+08:00
Type          = system
Priority      = 50
Datacenters   = dc1
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group  Queued  Starting  Running  Failed  Complete  Lost
fabio       0       0         3        0       0         0

Allocations
ID        Node ID   Task Group  Version  Desired  Status   Created    Modified
1b77dcfa  c281658a  fabio       0        run      running  1m11s ago  58s ago
da35a778  7acdd7bc  fabio       0        run      running  1m11s ago  54s ago
fc915ab7  9e3ef19f  fabio       0        run      running  1m11s ago  58s ago

通过9998端口,可以查看fabio的ui页面,这个页面主要展示的是fabio生成的路由信息:

img{512x368}

由于尚未暴露任何服务,因此fabio的路由表为空。

fabio的流量入口为9999端口,不过由于没有配置路由和upstream service,因此如果此时向9999端口发送http请求,将会得到404的应答。

2. 暴露HTTP服务到外部

接下来,我们就将上面创建的httpbackend服务通过fabiolb暴露到外部,使得特定条件下通过fabiolb进入集群内部的流量可以被准确路由到集群中的httpbackend实例上面。

下面是fabio将nomad集群内部服务暴露在外部的原理图:

img{512x368}

我们看到原理图中最为关键的一点就是service tag,该信息由nomad在创建job时写入到consul集群;fabio监听consul集群service信息变更,读取有新变动的job,解析job的service tag,生成路由规则。fabio关注所有带有”urlprefix-”前缀的service tag。

fabio启动时监听的9999端口,默认是http接入。我们修改一下之前的httpbackend.nomad,为该job中的service增加tag字段:

// httpbackend.nomad

... ...

     service {
        name = "httpbackend"
        tags = ["urlprefix-mysite.com:9999/"]
        port = "http"
        check {
          name     = "alive"
          type     = "http"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }

对于上面httpbackend.nomad中service块的变更,主要有两点:

1) 增加tag:匹配的路由信息为:“mysite.com:9999/”

2) 增加check块:如果没有check设置,该路由信息将不会在fabio中生效

更新一下httpbackend:

# nomad job run httpbackend-2.nomad
==> Monitoring evaluation "c83af3d3"
    Evaluation triggered by job "httpbackend"
    Allocation "6b0b83de" modified: node "9e3ef19f", group "httpbackend"
    Allocation "d0710b85" modified: node "7acdd7bc", group "httpbackend"
    Evaluation status changed: "pending" -> "complete"
==> Evaluation "c83af3d3" finished with status "complete"

查看fabio的route表,可以看到增加了两条新路由信息:

img{512x368}

我们通过fabio来访问一下httpbackend服务:

# curl http://mysite.com:9999/      --- 注意:事先已经在/etc/hosts中添加了 mysite.com的地址为127.0.0.1
this is httpbackendservice, version: v1.0.0

我们看到httpbackend service已经被成功暴露到lb的外部了。

四. 暴露HTTPS、TCP服务到外部

1. 定制fabio

我们的目标是将https、tcp服务暴露到lb的外部,nomad官方文档中给出的fabio.nomad将不再适用,我们需要让fabio监听多个端口,每个端口有着不同的用途。同时,我们通过给fabio传入适当的命令行参数来帮助我们查看fabio的详细access日志信息,并让fabio支持TRACE机制

fabio.nomad调整如下:

job "fabio" {
  datacenters = ["dc1"]
  type = "system"

  group "fabio" {
    task "fabio" {
      driver = "docker"
      config {
        image = "fabiolb/fabio"
        network_mode = "host"
        logging {
          type = "json-file"
        }
        args = [
          "-proxy.addr=:9999;proto=http,:9997;proto=tcp,:9996;proto=tcp+sni",
          "-log.level=TRACE",
          "-log.access.target=stdout"
        ]
      }

      resources {
        cpu    = 200
        memory = 128
        network {
          mbits = 20
        }
      }
    }
  }
}

我们让fabio监听三个端口:

  • 9999: http端口

  • 9997: tcp端口

  • 9996: tcp+sni端口

后续会针对这三个端口暴露的不同服务做细致说明。

我们将fabio的日志级别调低为TRACE级别,以便能查看到fabio日志中输出的trace信息,帮助我们进行路由匹配的诊断。

重新nomad job run fabio.nomad后,我们来看看TRACE的效果:

//访问后端服务,在http header中添加"Trace: abc":

# curl -H 'Trace: abc' 'http://mysite.com:9999/'
this is httpbackendservice, version: v1.0.0

//查看fabio的访问日志:

2019/03/30 08:13:15 [TRACE] abc Tracing mysite.com:9999/
2019/03/30 08:13:15 [TRACE] abc Matching hosts: [mysite.com:9999]
2019/03/30 08:13:15 [TRACE] abc Match mysite.com:9999/
2019/03/30 08:13:15 [TRACE] abc Routing to service httpbackend on http://172.16.66.102:23578/
127.0.0.1 - - [30/Mar/2019:08:13:15 +0000] "GET / HTTP/1.1" 200 44

我们可以清晰的看到fabio收到请求后,匹配到一条路由:”mysite.com:9999/”,然后将http请求转发到 172.16.66.102:23578这个httpbackend服务实例上去了。

2. https服务

接下来,我们考虑将一个https服务暴露在lb外部。

一种方案是fabiolb做ssl termination,然后再在与upstream https服务建立的ssl连接上传递数据。这种两段式https通信是比较消耗资源的,fabio要对数据进行两次加解密。

另外一种方案是fabiolb将收到的请求透传给后面的upsteam https服务,由client与upsteam https服务直接建立“安全数据通道”,这个方案我们在后续会提到。

第三种方案,那就是对外依旧暴露http,但是fabiolb与upsteam之间通过https通信。我们先来看一下这种“间接暴露https”的方案。

// httpsbackend-upstreamhttps.nomad

job "httpsbackend" {
  datacenters = ["dc1"]
  type = "service"

  group "httpsbackend" {
    count = 2
    restart {
      attempts = 2
      interval = "30m"
      delay = "15s"
      mode = "fail"
    }

    task "httpsbackend" {
      driver = "docker"
      config {
        image = "bigwhite/httpsbackendservice:v1.0.0"
        port_map {
          https = 7777
        }
        logging {
          type = "json-file"
        }
      }

      resources {
        network {
          mbits = 10
          port "https" {}
        }
      }

      service {
        name = "httpsbackend"
        tags = ["urlprefix-mysite-https.com:9999/ proto=https tlsskipverify=true"]
        port = "https"
        check {
          name     = "alive"
          type     = "tcp"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }
    }
  }
}

我们将创建名为httpsbackend的job,job中Task对应的tag为:”urlprefix-mysite-https.com:9999/ proto=https tlsskipverify=true”。解释为:路由mysite-https.com:9999/,上游upstream服务为https服务,fabio不验证upstream服务的公钥数字证书。

我们创建该job:

# nomad job run httpsbackend-upstreamhttps.nomad
==> Monitoring evaluation "ba7af6d4"
    Evaluation triggered by job "httpsbackend"
    Allocation "3127aac8" created: node "7acdd7bc", group "httpsbackend"
    Allocation "b5f1b7a7" created: node "9e3ef19f", group "httpsbackend"
    Evaluation status changed: "pending" -> "complete"
==> Evaluation "ba7af6d4" finished with status "complete"

我们来通过fabiolb访问一下httpsbackend这个服务:

# curl -H "Trace: abc"  http://mysite-https.com:9999/
this is httpsbackendservice, version: v1.0.0

// fabiolb 日志

2019/03/30 09:35:48 [TRACE] abc Tracing mysite-https.com:9999/
2019/03/30 09:35:48 [TRACE] abc Matching hosts: [mysite-https.com:9999]
2019/03/30 09:35:48 [TRACE] abc Match mysite-https.com:9999/
2019/03/30 09:35:48 [TRACE] abc Routing to service httpsbackend on https://172.16.66.103:29248
127.0.0.1 - - [30/Mar/2019:09:35:48 +0000] "GET / HTTP/1.1" 200 45

3. 基于tcp代理暴露https服务

上面的方案虽然将https暴露在外面,但是client到fabio这个环节的数据传输不是在安全通道中。上面提到的方案2:fabiolb将收到的请求透传给后面的upsteam https服务,由client与upsteam https服务直接建立“安全数据通道”似乎更佳。fabiolb支持tcp端口的反向代理,我们基于tcp代理来暴露https服务到外部。

我们建立httpsbackend-tcp.nomad文件,考虑篇幅有限,我们仅列出差异化的部分:

job "httpsbackend-tcp" {

 ... ...

    service {
        name = "httpsbackend-tcp"
        tags = ["urlprefix-:9997 proto=tcp"]
        port = "https"
        check {
          name     = "alive"
          type     = "tcp"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }

... ...

}

从httpsbackend-tcp.nomad文件,我们看到我们在9997这个tcp端口上暴露服务,tag为:“urlprefix-:9997 proto=tcp”,即凡是到达9997端口的流量,无论应用协议类型是什么,都转发到httpsbackend-tcp上,且通过tcp协议转发。

我们创建并测试一下该方案:

# nomad job run httpsbackend-tcp.nomad

# curl -k https://localhost:9997   //由于使用的是自签名证书,所有告诉curl不校验server端公钥数字证书
this is httpsbackendservice, version: v1.0.0

4. 多个https服务共享一个fabio端口

上面的基于tcp代理暴露https服务的方案还有一个问题,那就是每个https服务都要独占一个fabio listen的端口。那是否可以实现多个https服务使用一个fabio端口,并通过host name route呢?fabio支持tcp+sni的route策略。

SNI, 全称Server Name Indication,即服务器名称指示。它是一个扩展的TLS计算机联网协议。该协议允许在握手过程开始时通过客户端告诉它正在连接的服务器的主机名称。这允许服务器在相同的IP地址和TCP端口号上呈现多个证书,也就是允许在相同的IP地址上提供多个安全HTTPS网站(或其他任何基于TLS的服务),而不需要所有这些站点使用相同的证书。

接下来,我们就来看一下如何在fabio中让多个后端https服务共享一个Fabio服务端口(9996)。我们建立两个job:httpsbackend-sni-1和httpsbackend-sni-2。

//httpsbackend-tcp-sni-1.nomad

job "httpsbackend-sni-1" {

... ...

    service {
        name = "httpsbackend-sni-1"
        tags = ["urlprefix-mysite-sni-1.com/ proto=tcp+sni"]
        port = "https"
        check {
          name     = "alive"
          type     = "tcp"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }

.... ...

}

//httpsbackend-tcp-sni-2.nomad

job "httpsbackend-sni-2" {

... ...

   task "httpsbackend-sni-2" {
      driver = "docker"
      config {
        image = "bigwhite/httpsbackendservice:v1.0.1"
        port_map {
          https = 7777
        }
        logging {
          type = "json-file"
        }
    }

    service {
        name = "httpsbackend-sni-2"
        tags = ["urlprefix-mysite-sni-2.com/ proto=tcp+sni"]
        port = "https"
        check {
          name     = "alive"
          type     = "tcp"
          path     = "/"
          interval = "10s"
          timeout  = "2s"
        }
      }

.... ...

}

我们看到与之前的server tag不同的是:这里proto=tcp+sni,即告诉fabio建立sni路由。httpsbackend-sni-2 task与httpsbackend-sni-1不同之处在于其使用image为bigwhite/httpsbackendservice:v1.0.1,为的是能通过https的应答结果,将这两个服务区分开来。

除此之外,我们还看到tag中并不包含端口号了,而是直接采用host name作为路由匹配标识。

创建这两个job:

# nomad job run httpsbackend-tcp-sni-1.nomad
==> Monitoring evaluation "af170d98"
    Evaluation triggered by job "httpsbackend-sni-1"
    Allocation "8ea1cc8d" modified: node "7acdd7bc", group "httpsbackend-sni-1"
    Allocation "e16cdc73" modified: node "9e3ef19f", group "httpsbackend-sni-1"
    Evaluation status changed: "pending" -> "complete"
==> Evaluation "af170d98" finished with status "complete"

# nomad job run httpsbackend-tcp-sni-2.nomad
==> Monitoring evaluation "a77d3799"
    Evaluation triggered by job "httpsbackend-sni-2"
    Allocation "32df450c" modified: node "c281658a", group "httpsbackend-sni-2"
    Allocation "e1bf4871" modified: node "7acdd7bc", group "httpsbackend-sni-2"
    Evaluation status changed: "pending" -> "complete"
==> Evaluation "a77d3799" finished with status "complete"

我们来分别访问这两个服务:

# curl -k https://mysite-sni-1.com:9996/
this is httpsbackendservice, version: v1.0.0

# curl -k https://mysite-sni-2.com:9996/
this is httpsbackendservice, version: v1.0.1

从返回的结果我们看到,通过9996,我们成功暴露出两个不同的https服务。

五. 小结

到这里,我们实现了我们的既定目标:

  1. 使用nomad实现了工作负载的创建和调度;

  2. 东西向流量通过consul机制实现;

  3. 通过fabio实现了http、https(through tcp)、多https(though tcp+sni)的服务暴露和负载均衡。

后续我们将进一步探索基于nomad实现负载的多种场景的升降级操作(滚动、金丝雀、蓝绿部署)、对非host网络的支持(比如weave network)等。

本文涉及到的源码文件在这里可以下载。

六. 参考资料

  1. 使用Nomad构建弹性基础设施:nomad调度
  2. 使用Nomad构建弹性基础设施:重启任务
  3. 使用Nomad构建弹性基础设施: job生命周期
  4. 使用Nomad构建弹性基础设施:容错和自我修复
  5. fabio参考指南

我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats