11. 部署规划
# 11. 部署规划
现在你的应用程序已准备就绪,你希望将其部署,以便客户能够访问。然而,你不会进行脆弱的部署,否则不可避免的硬件/软件故障会影响客户体验。你希望能够在生产环境中可靠地部署新功能并修复漏洞,同时尽量减少花费的时间和出现的错误。
本章将讨论能够实现弹性架构、可扩展性和高功能迭代速度的部署架构。首先介绍典型生产环境设置的整体情况,然后深入探讨构建持续集成和部署管道的细节。我们还将探讨以下内容:
- 现代应用程序的部署架构
- 持续集成/持续交付(Continuous Integration/Continuous Delivery,CICD)管道
- 监控解决方案
- 云平台
- 安全注意事项
# 部署架构
部署架构是将系统架构映射到物理环境的过程。这个环境由运行代码的机器、负载均衡器、数据库、网络设备等组成。将逻辑架构和软件组件映射到部署环境涉及以下方面:
- 定义所需的基础设施组件
- 定义所需的环境
- 对每个环境进行规模分析:估算高效运行环境所需资源的数量和特性
- 管理策略:定义新代码如何在环境中部署的流程
本节将讨论生产系统的典型组件、配置和环境。
# 组件
一个典型的软件系统很少仅仅由代码构成。构建复杂的系统意味着要使用各种各样的系统组件。
下图描述了现代微服务架构部署中的典型组件:
以下各节将更详细地描述每个组件。
# 计算资源
我们需要计算硬件(CPU和内存)来运行代码。本节将介绍计算资源的各种选择。
# 物理服务器
早期的架构将代码部署在物理服务器上。每台服务器与交换机和存储阵列一起安装在机架中:
机架中的组件(服务器刀片、网络交换机等)通常根据其尺寸或机架单元(Rack Unit)进行分类。这是一个用于描述安装在19英寸或23英寸机架中设备高度的度量单位,大约为44.45毫米(1.75英寸)高。设备的外形尺寸通常以机架单元的倍数来表示(1U、2U等)。
物理服务器能提供最佳的硬件性能,然而,它们难以管理,利用率往往不均衡,而且可扩展性较差(这里的扩展意味着购买新硬件) 。
# 虚拟机
虚拟化(Virtualization)是指在实际硬件之上模拟硬件(如CPU/内存)的技术,从而在实际硬件上创建多个虚拟机。底层的管理程序层会捕获特权指令和访问共享状态的指令,并为每个虚拟机模拟隔离的环境。
虚拟化有诸多优点,包括:
- 更好地利用硬件资源
- 允许保存/恢复机器状态
- 更易于维护,例如支持虚拟机从一个物理主机迁移到另一个物理主机,便于物理硬件的维护
- 允许在标准硬件上部署运行在不同操作系统上的应用程序
虚拟化供应商众多,包括:
- 微软Hyper-V
- Linux内核虚拟机(Linux Kernel Virtual Machine,KVM)、Qemu以及相关生态系统,如Openstack
- 思杰Xenserver
# 容器
虚拟机通过打包应用程序和操作系统来工作。这使得软件包(通常称为镜像)非常庞大。而且由于在实际的 “主机” 操作系统之上运行多个操作系统,效率也比较低。
容器利用以下机制在操作系统层面实现隔离:
- 资源配额分配和强制执行,例如对CPU、内存、块I/O和网络资源的分配
- 命名空间隔离功能,为每个容器提供一个隔离的操作系统环境视图,包括文件系统、进程树、用户ID等
这种隔离机制比虚拟机中的客户操作系统概念高效得多。这使得在一台计算机上可以部署大量容器。
容器最初缺乏标准的打包系统。Docker通过提供打包标准、运行时(守护进程)以及用于打包、管理和分发容器化应用程序的工具(客户端)解决了这个问题。打包格式称为镜像,它本质上是一个列出创建容器指令的模板。镜像通常基于另一个镜像,并列出相对于基础镜像的特定定制内容。
例如,可以有一个基于CentOS的镜像,该镜像安装了Nginx和应用程序。Docker还有注册表(registry)的概念,它是Docker镜像的存储库。Docker Hub和Docker Cloud是任何人都可以使用的公共注册表,但组织内部也可以运行自己的私有镜像注册表,如下图所示:
参考:https://docs.docker.com/engine/docker-overview/#docker-architecture
虽然容器很不错,但大规模管理容器需要专门的解决方案。编排容器的选项有很多,Kubernetes、Mesos和Docker Swarm是一些流行的解决方案。
# 计算属性
如前所述,计算资源可以是虚拟机或容器。计算实例的重要属性包括:
- 计算能力:CPU的数量、速度以及核心数
- RAM和缓存的容量/配置
- 持久存储(磁盘)的类型和大小
- 虚拟机/物理机或容器
除了硬件特性,我们还需要定义实例上需要运行哪些其他服务。这些辅助程序包括:
- 进程监控组件,如supervisors
- 边车组件,通过代理实现与外部服务的连接
- 日志采集器,将日志发送到集中式系统(如Splunk,见“监控”部分)的组件
# 存储
数据中心存储技术主要有三种类型:
- 直连存储(Direct Attached Storage,DAS):这是传统的存储解决方案,磁盘直接连接到服务器。访问通常由智能控制器进行仲裁。
- 网络附加存储(Network Attached Storage,NAS):这里的存储本质上是在文件系统级别,通过某种网络协议在多个服务器之间共享。远程文件系统在服务器操作系统的文件系统树中的特定位置被“挂载”。两种常见的NAS协议是NFS(网络文件系统,Network File System)和CIFS(通用互联网文件系统,Common Internet File System)。CIFS需要一个专门的独立存储服务器,其他所有服务器都要连接到该服务器才能访问数据。除了实现数据共享,NAS的主要优点如下:
- 更有效地利用可用存储容量
- 集中管理
- 存储区域网络(Storage Area Networks,SAN):与NAS一样,SAN也提供共享存储。然而,SAN提供的是块级访问,而不是共享文件系统。这意味着无法进行文件共享,但保留了高效利用存储的优点。SAN有时除了存储服务器外,还会使用专门的网络(如光纤通道)。iSCSI协议是一种SAN解决方案,它使用现有的以太网设备和IP协议,从而实现了一种经济高效的SAN解决方案。
# 网络
网络设计是数据中心架构以及部署架构的关键要素。数据中心的网络通常基于分层设计,如下图所示:
核心层(Core layer)为进出数据中心的所有流量提供高速分组交换背板。接入路由器/交换机除了进行容错路由外,还提供生成树处理、防火墙、入侵检测和SSL卸载等额外功能。接入层由机架顶交换机(Top-of-the-Rack,ToR)组成,用于将实际服务器连接到网络。可以看到,布线中有大量冗余,以避免单点故障。路由协议确保冗余链路得到有效且正确的使用。
除了硬件元素,网络元素还有逻辑分组。通常,在三层应用程序中,网络也被划分为三层(Web层、应用层和数据库层)。这允许(包括但不限于)限制跨层的网络访问。在当今环境中,这种逻辑架构通过虚拟网络(Virtual Networks,VLAN)来实现,即在相同的物理网络基础设施之上实现多个二层隔离域。安全组(Security Groups)定义了哪些端口允许进出网络段。
# 负载均衡器
负载均衡器(Load Balancer,LB)是一种将传入的API请求分发到多个计算资源(实例)的设备。它能够:
- 处理单个实例无法承担的更多工作。
- 通过冗余提高可靠性和可用性。
- 在实例中实现零停机的代码平滑升级。
负载均衡器可以在网络堆栈的传输层或应用层工作。不同的层次如下:
- HTTP:在此模式下,负载均衡器将HTTP请求路由到一组后端实例。路由逻辑通常基于URL,但也可以使用请求的其他特征(用户代理、头部信息等)。负载均衡器通常会设置一些标准头部,如
X-Forwarded-For
、X-Forwarded-Proto
和X-Forwarded-Port
,以便向后端提供有关原始请求的信息。 - HTTPS:HTTPS与HTTP非常相似,但关键区别在于,为了能够基于HTTP头部/组件进行请求路由,需要终止SSL。在对负载进行解密并检查以进行路由后,发往后端的请求既可以通过HTTPS加密,也可以通过HTTP发送。这就要求我们必须在负载均衡器上部署SSL/TLS证书。SSL和TLS协议使用X.509证书对客户端和后端应用程序进行身份验证。这些证书是一种数字身份,由证书颁发机构(CA,Certificate Authority)颁发。证书通常包含加密公钥、有效期、序列号以及颁发者的数字签名等信息。管理证书成为SSL终止过程中的一项关键维护工作。
- TCP:流量也可以在TCP层进行路由。一个典型的例子是连接到一组冗余缓存。
- 用户数据报协议(UDP,User Datagram Protocol):这与TCP负载均衡类似,但使用较少。典型的用例可能是DNS和
syslogd
等使用UDP的协议。
根据负载均衡器的工作层次,可以采用不同的路由策略,包括:
- 直接路由:本质上只是更改二层网络地址,并将数据包重定向到后端。这具有巨大的性能优势,因为负载均衡器的工作很少,并且返回流量(从后端到客户端)可以在不经过负载均衡器的情况下进行。然而,这要求负载均衡器和后端服务器位于同一二层网络中。
- 网络地址转换(NAT,Network Address Translation):在此模式下,三层地址会被重写,负载均衡器存储一个映射,以便将返回流量重定向到正确的客户端。
- 终止并连接:对于大多数应用层负载均衡器来说,在数据包级别进行操作非常困难。因此,它们会终止TCP连接,读取数据,跨数据包存储负载,然后在获取足够的信息以做出路由决策后,通过另一组套接字将数据转发到“后端”服务器。
每个服务(微服务)都有一个特定的端点,通常是一个完全限定域名(FQDN,Fully Qualified Domain Name)。域名服务器(DNS,Domain Name Server)用于识别此实例的IP地址。通常,这个IP地址是一个虚拟IP地址(VIP,Virtual IP Address),它标识一组实例(每个实例都有一个实际的IP地址)。应用层(L7)负载均衡器是为每个服务配置VIP的地方。
负载均衡器应该只将流量转发到健康的后端服务器。为了评估健康状况,通常期望后端服务公开一个端点,负载均衡器可以通过该端点查询实例的健康状态。
一个典型的生产环境设置包括结合使用四层(TCP)和七层(HTTP)负载均衡器,以及一层用于终止SSL的机器,如前面的图表所示。
# API网关
API是现代应用程序的通用语言。在基于微服务的架构中,有多个服务,它们的集合构成了应用程序的API集。API网关就是这组API的“前门”。
API网关的职责包括:
- 授权和访问控制:确保只有经过授权的应用程序才能访问API。
- 配额/限流:为特定客户端的特定API设置配额和速率限制。
- 监控。
- API版本管理。
- 减少微服务架构中的“闲聊”:使客户端能够调用一个端点,而该端点在内部可以协调多个后端微服务。
负载均衡器和API网关之间存在功能重叠。一般来说,前者更侧重于高效路由,而后者功能更丰富。我们在第7章“构建API”中已经详细介绍过API网关。下面的图表快速回顾一下:
# 反向代理
反向代理的主要目标是在请求和服务器/后端代码之间添加一些功能。这些功能可以是SSL终止(HTTPS)或缓存等。
Varnish是一个专注于缓存的HTTP代理。它从客户端获取请求,并尝试从自身缓存中进行响应。如果Varnish在缓存中找不到响应,则将请求转发到后端服务器。后端服务器响应后,在将响应转发给客户端之前,可以先进行缓存。Varnish根据Cache-Control
HTTP响应头来决定是否缓存响应。
Varnish在性能提升方面效果显著,响应时间通常可达微秒级。
# 消息代理
正如第6章“消息传递”中所讨论的,服务之间的消息交换是构建弹性、高吞吐量架构的关键组成部分。消息代理是承载消息并在生产者和消费者之间进行路由的组件。Kafka消息代理就是一个例子。
在部署消息代理时,必须准确估计每个主题每秒所需的消息数量。这使我们能够计算出消息代理的CPU/内存需求以及承载消息所需的存储等配置。
另一个常见的配置是复制因子,它定义了消息在多少个节点上进行复制。这反过来又决定了主题在多少个节点发生故障时仍能继续为消费者提供消息服务。然而,为了实现这种可靠性,节点需要跨越不同的故障域。这里的故障域是指一组共享相同电源和网络交换机的节点。
像Kafka这样的一些消息代理包含复杂的功能,如组协调器(Group Co-ordinator),它实现了高可用的消息消费。然而,为了正确使用这些功能,我们需要正确定义一些可调整的参数,如心跳超时时间(消息代理和消费者之间)。
# 环境
前面描述的各个组件组合在一起形成一个“环境”。其中最重要的是生产环境,它用于处理客户的请求。然而,为了支持多个开发工作,还需要并行的环境。
现代系统在功能/需求方面不断变化,频繁进行版本发布。在将版本发布到生产环境之前,在受控环境中进行测试非常重要。这可以确保软件错误/回归问题不会首先被客户发现。这样的环境被称为质量保证(QA,Quality Assurance)环境。一般来说,QA环境比生产环境规模小。它通常是多个服务为了实现特定功能而首次进行集成的地方。
有时,精简的QA环境不足以验证系统的非功能需求。对特定版本的性能进行评估并确保新代码不会导致性能下降非常重要。这种测试通常需要大量负载,因此需要一个接近生产环境(或进行适当缩放以预测实际生产性能)的环境。因此,性能测试通常在单独的环境中进行。为了节省成本,这个环境通常是虚拟的,在不使用时可以停用。
部署环境的基本方案有很多变体。一种流行的组合称为蓝绿部署。在这种部署方式中,同时维护两个相同的生产环境(分别称为“蓝”和“绿”)。在任何给定时间,只有一个环境(例如“蓝”环境)处理实际生产流量。新的部署会发布到另一个集群(例如“绿”环境)。一旦新部署经过审查,在负载均衡器层面切换活动/备用环境。这种技术有几个关键优点:
- 由于新部署在完全不同的集群上进行,几乎可以实现零停机。
- 如果当前活动环境出现问题,最后一个稳定的环境可以作为热备用立即投入使用。
这种策略的缺点是维护两个生产级环境的成本较高。下面的图表描述了蓝绿部署策略:
# 容量规划和规模确定
环境的容量规划(或规模确定)是确定环境中各种组件的数量和配置的过程,以便在满足所需服务水平协议(SLAs)的前提下实现系统需求和业务目标。这不是一门精确的科学,而是基于所需的SLAs、过去的设计经验、领域知识以及创造性思维进行的大致计算。
对于计算资源来说,主要的资源是CPU和内存。领域知识和系统设计可以表明处理单个请求需要多少这些资源:
- API处理是否资源密集型?如果是,可能需要更多核心的CPU。
- API处理是否受限于IO?如果是,那么像直接内存访问(DMA,Direct Memory Access)这样的辅助硬件可能会对性能产生影响(而不是更大核心的CPU)。
- 如果有大量的内存缓存或递归操作,那么内存需求可能会很大。
有了这些信息,就可以确定一个特定规格的单个实例每秒能够处理多少请求。一旦我们知道每个实例可以处理x
个请求/秒,而服务所需的SLA是y
个请求/秒,那么使用ceil(y/x)
就可以轻松计算出所需的实例总数。
如第8章“数据建模”所述,数据库有多种形式和类型。然而,在生产部署中,有一些通用的注意事项:
- 高可用性:确保没有数据库实例成为单点故障。
- 存储配置:数据库将数据写入磁盘。在大多数现代操作系统中,这个过程并不简单,需要注意确保满足持久性和性能目标。这包括文件系统中的各种缓冲区/缓存参数。
- 备份:大多数关键业务数据也需要进行备份。这涉及定期进行快照,并将快照传输到远程存档。
- CPU/内存需求:根据每秒需要支持的IO操作数量,为数据库实例分配适当的CPU和内存。
- 合规性和访问控制:需要对生产数据进行适当的保护,确保只有有限的必要用户对数据库具有读/写访问权限。出于隐私考虑,有些数据可能需要加密存储。
- 对数据库实体(表、视图、存储过程等)的创建/修改进行脚本化,并将脚本置于版本控制之下。
Go语言比Java等其他语言在资源利用上更高效。因此,即使你正在迁移现有系统,上述工作也是有意义的。
# 灾难恢复
正如第9章“反脆弱系统”中所讨论的,灾难恢复意味着在发生大规模故障(如整个数据中心瘫痪)时确保业务连续性和应用程序的可用性。通过复制,故障转移部署与主部署保持近乎同步。每个组件/服务都定义了故障转移站点中数据一致性的重要程度。在一致性方面的权衡是,将数据实时传输到故障转移端会导致性能/效率下降。实际的切换通过DNS更改来实现。下面这张来自第9章“反脆弱系统”的图表进行了快速回顾:
# 持续集成与持续交付(CICD)
持续集成、持续交付(CICD,Continuous Integration, Continuous Delivery)模型由蒂姆·菲茨(Tim Fitz)在他2010年的开创性著作《持续交付:通过构建、测试和部署自动化实现可靠的软件发布》中提出。本质上,它要求有一个自动化的管道,能够获取新代码,进行必要的检查/构建,然后将其部署到生产环境中。这样的系统是大多数现代应用程序实现高功能迭代速度开发的先决条件。
本节将描述CICD背后的概念,介绍一个简单的实现,然后深入探讨有助于构建CICD管道的Go工具。
# 概述
CICD方法倡导以下几点:
- 持续集成:持续合并开发人员的代码,实现单元测试自动化、代码打包以及与产品/组织的其他系统集成。
- 在整个开发过程中(不仅仅是在最后)对集成后的产品进行持续测试。
- 根据预定义的规则自动将构建版本推广到更高层次的环境。
- 持续发布,将推广后的构建版本自动部署到生产环境中。
CICD工作流程或管道定义了代码从开发环境到生产环境所需经过的各个阶段和关卡,并提供了使这个过程自动化的工具。整个过程的触发点是安装在版本控制系统(例如GitHub)中的一个Webhook。这个钩子触发一个管道,其中包含一系列流程,这些流程获取代码、进行打包、测试并部署,如下图所示:
# Jenkins
Jenkins是一个开源自动化服务器,由川口耕介(Kohsuke Kawaguchi)于2004年用Java编写。Jenkins可以作为端到端的持续集成持续交付(CICD)编排器,它在代码提交时被触发,能够进行构建、生成文档、测试、打包,以及执行预发布和部署等操作。
本节将概述如何使用Jenkins为托管在GitHub上的Go项目部署搭建一个简单的CICD管道。Jenkins服务器将部署在一台笔记本电脑(运行macOS系统)上,部署目标是一个Ubuntu容器。
# 示例代码
为了演示一个端到端的CICD管道,首先得有可运行的代码!在这个示例管道中,我们将使用一个基于Gin框架的简单HTTP服务器。示例代码如下:
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
fmt.Println("starting application..")
// 配置和路由
r := gin.Default()
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{
"status": "ok",
})
})
// 在0.0.0.0:9000监听并提供服务
r.Run(":9000")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们将直接从这个源(https://github.com/cookingkode/cisample)触发构建和部署操作。
# 安装Jenkins
Jenkins的官方网站是https://jenkins.io/ ,可以从那里下载Jenkins。下载完成后,可以通过java -jar Jenkins.war
命令来运行它。下载并安装Jenkins后,你可以通过网页浏览器访问Jenkins(默认地址是http://localhost:8080)。你应该会看到如下界面:
# 安装Docker
我们需要有一个部署目标。在实际应用中,这可能是某种虚拟机(VM)/容器的镜像,但在这里我们将在本地运行一个Docker容器,并在其上部署代码。要运行一个Ubuntu容器,可以使用以下命令:
docker run -p 9000:9000 -p 32:22 -it ubuntu /bin/bash
这个命令将部署一个名为Ubuntu的容器,还会进行一些端口映射,以便通过SSH连接并访问运行在9000端口的HTTP服务器。
我们将使用SSH把构建产物传输到部署目标上。因此,需要在容器中安装SSH服务器。可以使用以下命令来安装:
apt-get update
apt-get install openssh-server
service ssh restart
2
3
之后,需要在构建服务器(在这个例子中是开发用的笔记本电脑)上生成一个密钥对:
ssh-keygen -t rsa
然后,我们需要把公钥(~/.ssh/id_rsa.pub
)的内容复制到目标服务器(这里是Ubuntu容器)的~/.ssh/authorized_keys
文件中。确保你的.ssh
目录权限为700,authorized_keys
文件权限为644。
# 设置插件
接下来,我们需要安装一些插件来辅助我们的CICD管道。导航到“Manage Jenkins”(管理Jenkins)|“Manage Plugins”(管理插件),我们需要下载以下插件:
这些插件将用于编译Go代码(Go插件)、执行构建的自定义脚本(PostBuildScript插件),以及将构建产物发布到远程服务器并执行命令(Publish Over SSH插件)。插件下载完成后,我们需要对它们进行全局配置。为此,从Jenkins主仪表盘导航到“Manage Jenkins”(管理Jenkins)|“Global Tool Configuration”(全局工具配置),搜索“Go”部分并设置版本:
下一步是配置用于部署的SSH密钥。从Jenkins主仪表盘选择“Manage Jenkins”(管理Jenkins)|“Configure System”(配置系统),导航到“SSH”部分并输入以下内容:
- 密钥文件的路径,或者在“Key”文本框中粘贴私钥内容。
- 服务器详细信息。
在下面的截图中,密钥部分被隐藏了:
这里,由于我们将32端口映射到了22端口,确保你进入“SSH Servers”(SSH服务器)|“Advanced”(高级),并将“SSH Port”(SSH端口)设置为32:
完成这些设置后,建议使用“test connection”(测试连接)按钮来确保连接正常。
# 创建项目
此时,我们已准备好为我们的管道创建一个新项目。从Jenkins主仪表盘选择“New Item”(新建项目),给它起一个合适的名字,并选择“Freestyle Project”(自由风格项目)。这种类型允许我们设置自己的工作流程。项目名称很重要,因为它将是构建生成的项目二进制文件的名称。之后,设置示例项目的Git(GitHub)URL:
在这个例子中,Jenkins运行在笔记本电脑上,代码存储在GitHub上,所以我们将使用构建触发器,而不是手动触发管道。你可以将“Build Triggers”(构建触发器)部分留空。
我们需要设置Go版本:
设置好这个之后,工作流程将下载并在构建前安装该版本的Go。
接下来,我们配置实际的构建过程。我们可以分步骤进行。在这个例子中,我们只使用两个步骤——一个用于获取Go依赖,另一个用于进行构建,如下图所示:
注意,需要加上
GOOS=linux GOARCH=386
前缀,因为我的Jenkins服务器运行的是macOS系统,而我要将二进制文件部署到容器中的Ubuntu系统上。
设置好构建部分后,我们需要设置构建后操作。在这里,我们将复制(在Jenkins构建服务器上生成的)二进制文件,然后在目标容器上执行它:
最后,我们的项目就创建完成了。
# 运行构建
项目正确设置好后,你可以导航到主仪表盘并启动一次构建,如下图所示:
构建成功后,你可以通过访问http://localhost:9000/health并查看我们编写的示例响应,来测试二进制文件是否已成功部署。
# 目标配置
在示例中,我们设置了一个简单的Docker容器来托管二进制文件。在实际应用中,可能需要调整一些参数来确保程序正常运行。这包括以下内容:
GOPATH
(使用GVM时应设置为$HOME/go
)GOBIN
(应设置为$GOPATH/bin
)GOMAXPROCS
(设置为所需的核心数/所需的并行度)
还需要确保应用程序进程有足够的权限和资源限制来完成其工作。大多数操作系统会对进程可使用的各种资源设置多种限制,包括文件描述符、打开文件的数量等。在Linux系统中,可以使用ulimit
工具来检查和管理这些设置。
最后,Go进程应该设置为守护进程并作为服务运行。例如,在Ubuntu这样的操作系统中,这可能意味着使用如下配置来设置Upstart:
start on runlevel [2345]
stop on runlevel [!2345]
chdir /home/user/app
setgid app
setuid app
./app 1>>/var/logs/app.log 2>>/var/logs/apperr.log
2
3
4
5
6
# 工具
在前面的示例中,持续集成持续交付(CICD,Continuous Integration and Continuous Delivery)管道仅包含构建(go get
以及构建操作)和部署。然而,构建健壮的应用程序要求CICD管道执行更多步骤,包括测试、代码检查等。Go语言生态系统在这方面表现出色,拥有大量工具。以下是在CICD设置中有用的Go工具简要列表。
# go fmt
go fmt
工具会自动格式化Go源代码,包括缩进使用的制表符和对齐使用的空格。例如,看下面的代码片段:
package main
import "fmt"
// sample code to demo
// gofmt
var b int=2;
func main(){
a := 1;
fmt.Println(" a,b : ");
fmt.Println(a);
fmt.Println(b);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
经过go fmt
格式化后,代码会变成:
package main
import "fmt"
// sample code to demo
// gofmt
var b int = 2
func main() {
a := 1
fmt.Println(" a,b : ")
fmt.Println(a)
fmt.Println((b))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这都得益于go fmt
。
# golint
lint
工具的目的是确保代码符合《Effective Go》以及其他公开的良好编码规范所提出的代码风格。这个代码检查器并非Go工具套件的一部分,所以需要从https://github.com/golang/lint进行安装。使用方法相当简单:
$golint -set_exit_status $(go list ./...)
Go Lint
会发现以下一些问题。默认情况下,golint
只会在控制台打印信息,所以CICD框架不易判断是否存在问题。-set_exit_status
选项能使golint
的返回状态不为零,从而推断代码中存在错误。下面探讨golint
标记出的一些错误:
- 变量命名:
package errors
import fmt
var e = errors.New("error")
func main() {
fmt.Println ("this program will give a golint error")
}
2
3
4
5
6
7
8
在这里,golint
会抱怨变量e
的命名(“错误变量e
的命名应该形如errFoo
”)。
- 错误返回:
package myapp
func fetchData() (error, string) {
return nil, ""
}
2
3
4
5
- 函数中的最后一个
else
:这里,golint
会抱怨在返回多个值时,错误类型应该是最后一个。
func IsGreaterThanZero(x int) int {
if x > 0 {
return 1
} else {
return 0
}
}
2
3
4
5
6
7
在这里,golint
会建议将最后一个返回值放在else
之外。
单元测试用于验证应用程序的功能。我们已经介绍过如何编写Go单元测试以及相关最佳实践。对于每个.go
文件,都需要有一个对应的_test.go
文件来包含单元测试。
Clang有一个用于检测未初始化读取的工具,叫做内存消毒剂(MemorySanitizer,可在https://clang.llvm.org/docs/MemorySanitizer.html查看相关描述)。它可以和go test
一起使用,加上-msan
标志。CI框架应该使用类似下面的命令为所有包运行测试:
$go test -msan -short $(go list ./...)
除了单元测试的状态(是否有失败的测试),代码可靠性的一个关键指标是代码覆盖率。它表示单元测试覆盖的代码量。我们可以使用如下脚本计算代码覆盖率:
$PKGS=$(go list ./...)
$for p in ${PKGS}; do
go test -covermode=count -coverprofile "cov/${package##*/}.cov" "$p" ;
done
$tail -q -n +2 cov/*.cov >> cov/total_coverage.cov
$go tool cover -func=cov/total_coverage.cov
2
3
4
5
6
# go build
在确保代码通过所需的质量检查后,我们需要将其编译成二进制文件。Go团队为加快构建速度和实现交叉编译付出了很多努力(正如我们在上一个示例中展示的那样) 。
# 脚注
花时间构建一个健壮的CICD管道至关重要。这有助于在保持质量标准的同时,实现快速部署和功能迭代。
在示例中,我们手动触发了管道。然而,如前所述,我们应该配置网络钩子(web hooks),以便在代码推送时触发管道。在GitHub上进行设置的步骤相当简单。导航到“Webhooks & Services”选项卡,选择“Configure Services”。找到“Jenkins(GitHub插件)”选项,填入Jenkins服务器的URL,格式类似http://<Jenkins服务器名称>:8080/github-webhook/。确保勾选“Active”复选框,并使用“Test Hook”按钮检查设置是否有效:
# 监控
代码部署之后,你需要监控运行情况。这需要预先投入精力规划监控架构并完成相关设置,以免应用上线后两眼抓瞎。没有监控的话,一旦出现不可避免的故障,团队就很难了解发生了什么。这会妨碍问题排查,最终影响客户体验。
这就是为什么恰当的监控至关重要。监控包含五个方面:
- 恰当的日志记录
- 从应用程序和基础设施中准确发出所有相关且重要的指标
- 设计良好的仪表盘,用于反映应用程序的健康状况
- 针对关键指标的可操作警报系统
- 组建生产可靠性团队,可能包括站点可靠性工程(SRE,Site Reliability Engineering)团队,并为团队中的所有开发人员制定高效的值班轮换计划
我们将在以下部分详细介绍每个方面。
# 日志
日志记录是监控首先要实现的内容。在调试应用程序问题时,开发人员大多需要查看日志,梳理事件发生的时间顺序,找出问题所在。
为了最大程度发挥日志的作用,确保日志遵循特定结构并包含重要信息非常关键,这些信息包括:
- 对所记录事件的简短清晰描述。
- 相关数据,如请求ID、用户ID等。像用户名(电子邮件)、社会安全号码和客户姓名等信息不应记录(或进行掩码处理),以避免泄露客户隐私。
- 描述事件发生时间的时间戳。
- 服务实例的线程和主机标识符。
- 文件名、函数名、行号。
- 所有API的重要请求和响应详细信息。
- 用于跨微服务识别服务请求的追踪标识符。
为每条日志设定经过深思熟虑的级别也很重要。有些信息在调试环境中可能很重要,但在生产环境中,调试日志可能会过于冗长,在某些情况下会产生大量干扰信息,反而妨碍问题排查。理想情况下,应该有三个级别:
- 调试(Debug):详细信息,在非生产环境中分析程序时很有用。
- 信息(Info):在生产环境中对调试有帮助的事件。
- 严重错误(Fatal):导致程序必须退出的严重故障。
Go的标准库中有一个log
包:https://golang.org/pkg/log/ 。它本身不支持分级日志记录,但可以为每个级别创建不同的日志记录器,如下所示:
Debug = log.New(os.Stdout, "DEBUG ", log.LstdFlags)
一个常见的错误是应用程序自行管理日志文件。实际上,每个进程都应该无缓冲地(作为事件流)将日志写入标准输出(Stdout)。在开发过程中,开发人员可以在终端查看日志。在生产环境中,可以轻松将相同的日志流重定向到文件,如./my_service 2>> logfile
(Go中的默认日志记录器写入标准错误输出 - 2)。
如果标准日志库无法满足需求,还有很多日志记录器库可供选择,它们在日志基础上增加了分级等功能。
这里有一个关于在多个开发人员协作场景下,在包内进行日志记录的小提示。在包内创建日志记录器实例并不是个好主意,因为这样客户端应用程序就会与包中使用的日志记录器库耦合。更糟糕的是,如果最终的应用程序包含多个包,每个包使用不同格式的日志记录器,那么浏览不同的日志会变得很困难,更不用说日志记录器库带来的不必要的冗余。在这种情况下,从应用程序(客户端)代码获取日志记录器作为输入会更加优雅,包只需使用该日志记录器来记录事件即可。客户端可以在库提供的初始化函数中指定日志记录器。
每个实例的日志都需要汇总到一个中心位置。一种常见的架构是Elasticsearch、Logstash、Kibana(ELK)堆栈。Elasticsearch是一个基于Apache Lucene搜索引擎的倒排索引数据库服务。Logstash是一个数据采集工具,它接受来自各种来源的输入,进行转换后,将结果输出到多个目标,在这种情况下是Elasticsearch。Kibana是Elasticsearch之上的可视化层。通常,Logstash从文件中获取日志,并将其发送到Elasticsearch,在那里日志会按照logstash-YYYY.MMM.DD
的格式被索引到不同的索引中。可以使用正则表达式进行搜索,例如,要查看2018年6月的所有日志数据,可以指定索引模式logstash-2018.06*
。
需要注意的是,日志文件可能会迅速变大。如果它们占用过多磁盘空间,应用程序可能会受到影响。因此,对日志进行轮转,保留最新的日志并丢弃旧日志非常重要。像Logstash这样的数据采集工具在采集过程中可以对日志文件进行轮转设置。
# 指标
需要在每个服务、主机、基础设施以及支持组件(如数据库)级别识别并监控关键指标。所有这些指标加起来应该能够提供描述系统行为和健康状况的充分必要信息。
这些原始指标可以进行汇总和处理,形成更高级别的指标。它们应该足够细化,以便开发人员了解特定主机上特定服务的指标状态。同时,也应该能够对指标进行汇总,这样就能获取服务在所有运行主机上的指标。例如,应该能够查询到特定主机上某个服务的CPU利用率,以及该服务在所有部署主机上的CPU利用率。
以下是一些可以测量的指标:
分类 | 指标 |
---|---|
基础设施 | CPU利用率、内存利用率、磁盘利用率、存储设备的每秒输入/输出操作次数(IOPS,Input/Output Operations Per Second) |
应用程序(系统级别) | goroutine数量、堆大小、打开的文件描述符数量、数据库连接数、内核日志 |
应用程序(业务级别) | 所有API端点的服务级别协议(SLA,Service-Level Agreement)性能、API成功率、业务交易、错误、崩溃、应用程序日志 |
客户端 | 真实用户监测、所有API端点的客户端性能、崩溃、客户端日志 |
ELK堆栈还提供了一个名为Metricsbeat的组件,它主要用于将Go应用程序的指标发送到Elasticsearch。有关使用Kibana优化Go应用程序的更多详细信息,可以访问https://www.elastic.co/blog/monitor-and-optimize-golang-application-by-using-elastic-stack。
# 应用性能监测/仪表盘
仪表盘(或应用性能监测(APM,Application Performance Monitoring)工具)应该为系统健康状况提供统一视图,涵盖硬件、应用程序以及相关服务。设计良好的仪表盘能让开发人员通过直观的方式轻松检测系统健康状况和行为中的异常。
Newrelic是一款流行的应用程序监测系统,它支持Go语言。以下截图展示了使用该插件可以实时获取的重要指标:
参考:https://newrelic.com/golang
根据Newrelic的介绍,Go语言代理具有以下功能:
- 查看应用程序正在访问的数据存储调用和外部服务。
- 找出可能导致响应出现瓶颈的操作。
- 使用部署标记查看不同部署之间应用性能和运行时行为的变化。
- 编写自定义事件,并使用New Relic Insights构建自定义仪表盘。
仪表盘的一个重要功能是趋势分析。如果我们查看某个API在过去几分钟的响应时间图表,叠加该API上周同一时间的响应时间会很有帮助。这能让开发人员快速判断该API当前是否存在重大异常。
大多数问题都是由新部署的代码引起的。为了帮助开发人员快速定位新代码中的问题,在仪表盘中包含部署时间信息很有帮助。部署时间会在指标图表(随时间变化)中以清晰的可视化元素(例如垂直红线)显示。
# 警报
仅有仪表盘是不够的,我们不能指望开发人员全天候持续监控仪表盘。我们需要实时警报功能,这意味着能够为指标设置阈值,并识别关键日志/事件。在设置警报时,我们还需要确定警报的通信机制,这种机制可以从简单的电子邮件到像PagerDuty这样复杂的解决方案。
一旦指标超过这些阈值,可能会导致系统故障、延迟飙升或以某种方式影响客户体验,因此需要向相关团队发出通知,以便纠正问题。重要的是,阈值的设置应该确保在灾难性情况发生之前发出通知,这样团队就有足够的时间进行调试并解决问题。
# 团队
如果在出现问题时不采取任何补救措施,监控和警报就毫无意义。警报触发后,需要一个团队对每个警报进行分类、调试和解决。这有两个目标:
- 立即使生产环境恢复稳定状态。
- 收集所有必要的数据(如有必要,从生产集群中取出一个实例),以便进行有效的根本原因分析。
第一个目标是最优先考虑的。在生产环境中调试故障系统会延长故障时间,并且调试效率低下。
为了应对生产故障,需要有针对各种情况的详细调试步骤说明,这通常被称为值班手册(on-call runbook)。对于每个警报,工程师可以查阅值班手册,找出偏离正常情况的已知原因、纠正问题的方法以及如何进行调试/收集更多信息。这些值班手册既适用于基础设施,也适用于每个服务。
传统上,企业会有一个运维团队,手动执行各项操作。这里提到的值班手册更多是手动运行的命令。然而,随着规模、复杂性和功能迭代速度的增加,人们意识到这种流程无法扩展。大多数企业正在转向由谷歌首创的站点可靠性工程(SRE)团队。SRE团队是一组工程师,他们使用软件工具来管理应用程序的所有软件和基础设施。实际上,值班手册实现了自动化,以前手动执行的操作现在可以自动完成。
SRE团队通常会有各个服务团队的“值班”开发人员作为补充。这些值班开发人员负责生产环境中自己的服务,进行详细的调试并提供二级支持。在生产事故发生时,他们与SRE团队密切合作。
虽然我们简要介绍了如何组建一个DevOps团队,但还有很多细节需要完善。不过,这些细节超出了本书的范围。
# 云服务
云计算指的是通过互联网从外部供应商为应用程序提供计算基础设施和更高级别服务的能力。提供这些多租户计算服务的公司被称为云服务提供商,它们通常按使用量收费,从而以公用事业模式提供基础设施和服务。
按照面向服务架构的理念,云计算主要分为三大类,下面将分别介绍。
# 基础设施即服务(IaaS)
在这种模式下,云服务提供商让用户能够直接访问服务器、存储和网络等计算资源。企业利用这些资源,并在该基础设施上部署自己的技术栈。用户无需直接购买硬件,实际上是租用硬件。计算资源具有可扩展性,用户可以控制实例的类型和数量。下表列举了亚马逊网络服务(AWS)中不同类型的计算实例以及IaaS产品(EC2),展示了其丰富的种类:
类型 | 用途 | 示例配置 |
---|---|---|
T2 | 支持突发计算,本质上是用户选择一个CPU性能基线水平,且具备在基线之上进行突发计算的能力。突发能力由CPU积分控制。每个T2实例会根据自身大小,按既定速率定期获得CPU积分。实例在实际使用CPU时会消耗这些积分。这类实例适用于那些无需持续占用全部CPU资源的工作负载,如开发环境 | t2.nano:1个虚拟CPU,0.5GB内存,每小时3个CPU积分 t2.medium:2个虚拟CPU,4GB内存,每小时24个CPU积分 t2.2xlarge:8个虚拟CPU,32GB内存,每小时81个CPU积分 |
M4 | M4实例是基于定制的英特尔至强E5 - 2676 v3 Haswell处理器(专为AWS优化)的通用型实例。这些实例还配备了增强网络功能,能将网络数据包速率提高四倍,同时保证可靠的低延迟。一个典型用例是用于中间层数据库 | m4.large:2个虚拟CPU,8GB内存 m4.xlarge:4个虚拟CPU,16GB内存 m4.16xlarge:64个虚拟CPU,256GB内存 |
C4/C5 | C4实例是计算优化型实例,基于定制的2.9GHz英特尔至强E5 - 2666 v3处理器,并启用了英特尔睿频加速技术。这些实例能在给定价格下实现最高性能。它们适用于计算密集型应用,如媒体转码、游戏服务器等 | c4.2xlarge:8个虚拟CPU,15GB内存 c5.large:2个虚拟CPU,4GB内存 c5.4xlarge:16个虚拟CPU,32GB内存 c5d.large:2个虚拟CPU,4GB内存,1块50GB NVMe固态硬盘 |
X1/R4 | 这些实例最适合大规模内存型应用,在AWS EC2实例中,每GB内存的价格最低。它们旨在托管内存数据库和企业解决方案,如SAP HANA等。这些实例提供1952GB的DDR4内存,是其他任何AWS EC2实例提供内存的八倍。R4实例支持增强网络功能。X1实例提供固态硬盘存储,并且默认进行了EBS优化 | r4.large:2个虚拟CPU,15.25GB内存 r4.4xlarge:16个虚拟CPU,122GB内存 r4.16xlarge:64个虚拟CPU,488GB内存 X1.32xlarge:28个虚拟CPU,1952GB内存 |
G2 | G2实例适用于需要利用GPU的应用,例如建模、机器学习、渲染、转码任务和游戏串流。这些实例配备了高性能的NVIDIA GPU,具有4GB显存和1536个CUDA核心 | g3.8xlarge:2个GPU,32个虚拟CPU,244GB内存 |
I2, H1 | 这些是存储优化型实例,提供快速的固态硬盘支持的实例存储。这是随机读写性能方面最佳的存储选项,能以最低成本提供最大的每秒输入/输出操作次数(IOPS)。H1实例还提供增强网络功能,最适合用于MapReduce作业、分布式文件系统(如HDFS)、Apache Kafka等用例。I2实例则非常适合MongoDB、Redis等NoSQL解决方案 | t3.4xlarge:16个虚拟CPU,122GB内存 h1.2xlarge:8个虚拟CPU,32GB内存 |
# 平台即服务(PaaS)
平台即服务(PaaS)模式比IaaS的服务层级更高,在这种模式下,云服务提供商提供诸如数据库、缓存等构建模块的托管服务。这使得应用程序开发人员可以专注于业务用例,而无需在基础设施管理上耗费精力。与IaaS相比,PaaS组件在软件栈中处于更高层级。
PaaS的优势显而易见。软件开发团队可以立即开始开发工作,根据需要快速搭建实用的服务。供应商会管理其他所有事务,如软件版本、安全性、操作系统和备份等。大多数PaaS服务具有很强的弹性,可以通过管理控制台按需进行纵向扩展(例如,增加更多的IOPS)或横向扩展(增加更多数量) 。PaaS服务与IaaS相比,存在以下权衡:
- 通常比自行搭建的解决方案成本更高。
- 供应商锁定:如果为特定的云服务编写代码,将应用程序迁移到其他云平台会很困难。
- 开发设置受限:开发人员通常在云端共享开发账户,因为大多数PaaS服务无法在开发机器上部署。
下表展示了AWS提供的丰富托管服务:
分类 | 服务详情 |
---|---|
数据库 | 关系数据库服务(Relational Database Service,RDS)(https://aws.amazon.com/rds/ )是一种符合ACID规范的托管关系数据库即服务(DBaaS),数据库的弹性、扩展和维护主要由平台负责。RDS提供常见的数据库引擎,如MySQL、MariaDB、PostgreSQL、Oracle、Microsoft SQL Server,最近还推出了一种新的与MySQL兼容的内部开发引擎,名为亚马逊极光(Amazon Aurora)数据库引擎。存储类型可以配置为磁性磁盘或固态硬盘。RDS提供的一个重要功能是多可用区高可用性(跨基础设施故障域进行复制)。 DynamoDB(https://aws.amazon.com/dynamodb/)是AWS提供的一种多模型NoSQL数据库服务,可用于建模键值对、文档、图形和列数据。它在全球范围内分布,因此具备高可用性。除了其他出色的功能外,DynamoDB会根据应用程序请求量的增加或减少自动扩展或缩减容量。 AWS ElastiCache(https://aws.amazon.com/elasticache/)是一种完全托管的缓存服务,提供Redis和Memcached服务器/集群。使用这些服务,用户无需为缓存解决方案配置硬件和软件。 |
AWS Redshift(https://docs.aws.amazon.com/redshift/latest/mgmt/welcome.html)是一种完全托管的弹性数据仓库。其主要目标是作为一个数据湖,存储所有与业务相关的数据,用于长期存储,并支持多种用例,如商业智能和机器学习模型训练。用户可以创建集群,每个集群可以托管多个数据库。每个集群由一个领导节点和一组计算节点组成。计算节点实际承载数据切片,这些切片实际上是数据的分片。领导节点接收来自客户端程序的查询和命令,对其进行解析,并为每个计算节点构建执行计划。领导节点创建编译后的代码,并根据每个计算节点所存储的数据将其分发到相应的计算节点。一旦计算节点处理完相关代码,就会将结果返回给领导节点,由领导节点汇总结果。Redshift以列格式存储数据。这一点,再加上大规模并行处理(Massively Parallel Processing,MPP)功能,使其非常适合执行OLTP工作负载。计算节点切片是基于PostgreSQL的数据库。客户端应用程序可以使用标准的开源PostgreSQL JDBC和ODBC驱动程序与Redshift进行通信 | |
网络 | AWS虚拟私有云(AWS Virtual Private Cloud,https://aws.amazon.com/vpc)是云中的一个隔离的私有网络边界。用户获得一个VPC后,就可以控制自己的网络环境,包括定义IP范围、路由表、子网等。使用VPC时,通常会为Web服务器创建一个面向公共互联网的子网,为数据库或应用服务器等组件创建另一个隔离的子网。后者没有互联网访问权限。用户可以利用多层安全规则,包括安全组和网络访问控制列表,来帮助控制对子网内实例的访问。VPN网关可用于连接外部/现有网络和云VPC。有时,VPN连接无法提供网络互连所需的带宽。AWS Direct Connect(https://aws.amazon.com/directconnect/)则提供从现有场所到AWS的专用网络连接。AWS CloudFront(https://aws.amazon.com/cloudfront)是一种内容分发网络(Content Delivery Network,CDN)服务,它将音频、视频、应用程序、图像,甚至API响应等内容存储在离客户端较近的地方。这利用了AWS的全球基础设施文件。CloudFront还与其他AWS基础设施产品(如Web应用程序防火墙(Web Application Firewall,WAF)和Shield Advanced)无缝集成,有助于保护应用程序免受更多威胁,如DDoS攻击。 |
API管理 | AWS弹性负载均衡(AWS Elastic Load Balancing,https://aws.amazon.com/elasticloadbalancing/)是一个负载均衡管理器,它将传入流量分配到多个后端,如Amazon EC2实例、容器和IP地址。它具有L7(应用层)和L4(传输控制协议层)的负载均衡功能,并且可以在VPC中设置目标。 AWS API网关(https://aws.amazon.com/api-gateway)是一个托管的API网关,支持流量管理、授权和访问控制、监控、限流和版本管理等功能。 AWS Route 53(https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html)是一种DNS服务,具有额外功能,包括对后端服务的健康监测。 |
编排、管理和监控 | AWS OpsWorks(https://aws.amazon.com/opsworks/)是一个托管的Chef/Puppet服务,支持持续集成和持续交付(CICD)平台,并实现应用程序/服务器配置的自动化。 AWS CloudWatch(https://aws.amazon.com/cloudwatch/)是一个托管服务,用于监控云资源(如Amazon EC2实例、Amazon DynamoDB表和Amazon RDS数据库实例)以及应用程序级别的自定义指标和日志文件。用户还可以使用Amazon CloudWatch设置警报,并对这些指标的变化自动做出反应。 AWS X - Ray(https://aws.amazon.com/xray/)提供了请求在应用程序的不同微服务/层中传输时的端到端视图。 AWS管理控制台是一个统一的控制台,用于管理所有云基础设施和服务。 |
# 软件即服务(SaaS)
软件即服务(Software as a Service,SaaS)为你提供由服务提供商运行和管理的托管产品。使用SaaS产品时,你无需考虑服务的基础设施甚至技术栈;你只需要知道如何使用它。基于Web的电子邮件就是一个SaaS应用程序的例子,你可以在无需安装或管理自己的电子邮件服务器的情况下发送和接收电子邮件。
# 安全性
随着应用程序与人们的生活紧密交织,且周围恶意行为者越来越多,保障应用程序的安全变得至关重要。以下详细介绍一些常见的安全威胁:
- 敏感数据泄露(Sensitive data leakage):通过应用程序编程接口(API)传输并存储在数据库中的某些数据可能极其敏感(如电话号码、信用卡号等)。黑客可能会试图窃取这些数据。除了防止此类盗窃行为,通常还有隐私方面的法律法规,以确保数据在静态存储和传输过程中都进行加密。如果公司任由其平台发生数据盗窃事件,可能会面临法律禁令。
- 拒绝服务(Denial of Service):拒绝服务攻击试图通过发送虚假/伪造流量使应用程序无法使用。这类攻击中一种特别恶劣的形式是分布式拒绝服务(DDoS,Distributed Denial of Service)攻击,其流量来自多个源头,以避免被检测到流量产生的来源。
- 跨站脚本攻击(Cross-Site Scripting,XSS):在这些攻击中,恶意脚本被注入到原本安全可信的网站中,例如论坛的评论区。当用户点击这些链接时,脚本会利用浏览器中缓存的用户认证信息执行,从而导致恶意行为。
- 注入攻击(Injection attacks):诸如SQL注入、轻量级目录访问协议(LDAP,Lightweight Directory Access Protocol)注入和回车换行符(CRLF,Carriage Return Line Feed)注入等攻击,涉及攻击者在表单中发送某种脚本,当脚本运行时,会在未经适当授权的情况下执行命令。该脚本可能具有极大的破坏性,比如删除整个数据库表。
- 弱认证(Weak authentication):由于认证机制不够完善,黑客能够访问诸如银行网站等敏感应用程序。像密码这种简单的认证方式很容易被盗取或推断出来。
一些补救策略如下:
- 对所有API请求进行身份验证和授权:身份验证用于可靠地确定终端用户的身份。授权是指确定已识别用户可以访问哪些资源的过程。对于API来说,认证机制通常是一个临时访问令牌,它可以通过外部机制获取/刷新。这个令牌会随每个API请求一起发送。后端可以处理这个令牌,并可靠地推断出用户的身份。有时授权信息(如角色)会编码在令牌中。授权令牌有很多标准,JSON网络令牌(JWT,JSON Web Tokens)是一种流行的标准。它基于一个开放标准(RFC 7519),用于以JSON对象的形式在各方之间安全地传输信息。由于信息经过数字签名,所以可以被验证和信任,因此在传输过程中对信息的篡改很容易被发现。JWT可以使用密钥(采用哈希消息认证码(HMAC,Hash - based Message Authentication Code)算法)或公私钥对进行签名。https://github.com/dgrijalva/jwt-go是一个用于使用JWT的Go语言库。
- 对于像银行网站这类涉及敏感信息且供用户使用的应用程序,多因素认证(MFA,Multi - Factor Authentication)可以降低账户被攻破的风险。在这里,除了标准的用户名/密码认证外,还会使用另一种认证机制(例如向注册的电话号码发送一次性验证码)。这样,即使密码被盗,黑客也无法获得访问权限。
- 可以通过数据加密来缓解隐私问题。超文本传输安全协议(HTTPS)是安全消息传递的事实上的标准,它在内部使用传输层安全协议/安全套接字层协议(TLS/SSL,Transport Layer Security/Secure Sockets Layer)对有效负载进行加密。Go标准库中的
net/http
包有http.ListenAndServeTLS()
函数,它允许Go应用程序直接提供HTTPS服务。然而,更常用的选择是使用边车模式,搭配专门的类似HTTPS接收器的Caddy或Nginx,它们可以代理Go应用程序。我们之前已经了解过Nginx;Caddy(https://caddyserver.com/)是一个完全用Go编写的HTTPS反向代理。它有一些很酷的功能,包括:- 默认支持现代加密算法,如高级加密标准 - 伽罗瓦/计数器模式(AES - GCM,Advanced Encryption Standard - Galois/Counter Mode)、ChaCha和椭圆曲线密码学(ECC,Elliptic Curve Cryptography),在安全性和兼容性之间取得平衡。
- 中间人检测:Caddy可以检测客户端的TLS连接是否可能被代理拦截,让你能够采取相应措施。由于它是用Go编写的,所以不会受到像心脏出血漏洞(Heartbleed)这类内存安全攻击的影响。
- 相互认证:借助Caddy对TLS客户端授权的支持,你可以只允许特定客户端连接到你的服务,符合支付卡行业数据安全标准(PCI,Payment Card Industry)的规范。
- 密钥轮换:Caddy默认会轮换TLS会话票证密钥,从而有助于保持前向保密性,保护访问者隐私。
话虽如此,在撰写本文时,Nginx仍然比Caddy速度更快。若要查看两者之间的性能对比,可以访问https://ferdinand - muetsch.de/caddy - a - modern - web - server - vs - nginx.html。
- 配额和限流:如果一个普通API的用户请求频率约为每秒2次(RPS,Requests Per Second),那么来自某个用户每秒10次的请求负载就可能被视为可疑。可以使用配额来确保特定用户在每个API的每秒请求数量方面有一个设定的限制。限流还可以保护API免受拒绝服务(DOS,Denial of Service)攻击。Caddy有
http.ratelimit
结构来对来自特定IP地址的请求进行速率限制。Nginx有一个功能更丰富的速率限制器,允许对特定地理位置、请求头等等进行速率限制(在https://www.nginx.com/blog/rate - limiting - nginx/中有详细描述)。Nginx的实现使用漏桶算法进行速率限制,其中漏桶代表一种先进先出(FIFO,First - In - First - Out)的调度算法。 - 使用安全请求头:HTTP请求头包含多种与安全相关的请求头。例如,
Allowed Hosts
请求头提供了允许为你的网站提供服务的完全限定域名(FQDN,Fully Qualified Domain Name)列表。这可以防止诸如DNS缓存投毒之类的攻击。
本节旨在对保障应用程序安全时需要考虑的不同方面进行概述。安全领域涉及内容广泛,详细论述超出了本书的范围。
# 总结
部署规划比大多数人想象的要复杂得多。如果我们没有充分思考并投入足够时间,那么生产环境就可能会不稳定。本章介绍了生产环境中的常见要素,并就如何构建安全、健壮的代码交付管道提供了指导。
到目前为止,本书涵盖了用Go语言进行系统工程的各个方面,本章以部署相关内容作为结束。在下一章中,我们将通过探讨将非Go语言应用程序迁移到Go语言的各个方面来结束本书。