DevOps学习笔记
- Published on
DevOps简介
什么是DevOps?
DevOps是"Development"(开发)和"Operations"(运维)的组合词,它不仅是一种软件开发方法,更是一种文化理念。在传统的软件开发中,开发团队和运维团队往往是分离的,这种分离导致了许多问题:开发团队追求快速变更,而运维团队追求系统稳定,这种目标的不一致常常造成效率低下和沟通障碍。
DevOps的出现就是为了解决这个问题。它提倡开发和运维的紧密协作,通过自动化工具和最佳实践,实现快速、可靠的软件交付。
DevOps生命周期
DevOps实践遵循一个持续的循环过程:
规划(Plan):
- 确定开发目标和需求
- 制定项目计划和时间表
- 分配资源和任务
编码(Code):
- 进行实际的代码编写
- 使用版本控制系统(如Git)管理代码
- 遵循编码规范和最佳实践
构建(Build):
- 编译代码
- 运行自动化测试
- 生成可部署的软件包
测试(Test):
- 执行单元测试
- 进行集成测试
- 运行性能测试
- 进行安全测试
发布(Release):
- 版本管理
- 变更控制
- 发布审批
部署(Deploy):
- 将软件部署到生产环境
- 配置管理
- 部署自动化
运营(Operate):
- 系统维护
- 性能优化
- 问题处理
监控(Monitor):
- 性能监控
- 日志分析
- 用户体验监测
反馈(Feedback):
- 收集用户反馈
- 分析系统数据
- 确定改进方向
DevOps的三大支柱
1. 拉取请求自动化(Pull Request Automation)
什么是拉取请求?
拉取请求(Pull Request,简称PR)是代码协作的核心机制。当开发者完成一个功能或修复时,他们会创建一个PR,请求将他们的代码合并到主代码库中。
自动化的关键领域:
代码质量检查
- 静态代码分析
- 代码风格检查
- 潜在问题识别
自动化测试
- 单元测试自动运行
- 集成测试自动执行
- 测试覆盖率报告生成
环境配置
- 自动创建测试环境
- 配置依赖服务
- 数据库初始化
实际应用示例:
假设一个开发团队正在开发一个网上商城:
# 示例的GitHub Actions配置
name: Pull Request Checks
on:
pull_request:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 代码风格检查
run: npm run lint
- name: 运行单元测试
run: npm run test
- name: 检查测试覆盖率
run: npm run coverage
2. 部署自动化(Deployment Automation)
自动化部署的核心概念
部署自动化是将应用程序从开发环境转移到生产环境的自动化过程。它消除了手动部署带来的人为错误,提高了部署的可靠性和效率。
关键组件:
持续集成/持续部署(CI/CD)管道
- 代码提交触发自动构建
- 自动运行测试套件
- 自动部署到目标环境
环境管理
- 开发环境
- 测试环境
- 预生产环境
- 生产环境
配置管理
- 环境变量管理
- 密钥和证书管理
- 服务配置管理
实现示例:
使用Docker和Kubernetes的简单部署配置:
# Kubernetes部署配置示例
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-application
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: web-app:latest
ports:
- containerPort: 80
3. 应用性能管理(APM)
什么是APM?
应用性能管理是监控和管理应用程序性能和可用性的实践。它帮助团队确保应用程序运行正常,并能够快速发现和解决问题。
核心功能:
指标收集
- 响应时间
- 错误率
- 资源使用率
- 用户体验数据
日志管理
- 集中式日志收集
- 日志分析和搜索
- 异常检测和告警
监控告警
- 性能阈值监控
- 自动告警通知
- 问题诊断支持
实际应用:
使用ELK Stack(Elasticsearch, Logstash, Kibana)的日志收集配置:
# Logstash配置示例
input {
beats {
port => 5044
}
}
filter {
json {
source => "message"
}
date {
match => [ "timestamp", "ISO8601" ]
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "application-logs-%{+YYYY.MM.dd}"
}
}
测试驱动开发(TDD)
TDD的基本原则
测试驱动开发是一种先写测试,后写代码的开发方法。它不仅是一种开发技术,更是一种设计方法。
TDD的开发流程
编写测试: 首先编写一个测试用例,描述期望的功能。
运行测试: 确认测试失败(因为还没有实现功能)。
编写代码: 编写最简单的代码来通过测试。
重构: 在保证测试通过的前提下优化代码。
实际示例:
开发一个简单的计算器功能:
# 测试代码
def test_calculator_add():
calculator = Calculator()
assert calculator.add(2, 3) == 5
assert calculator.add(-1, 1) == 0
assert calculator.add(0, 0) == 0
# 实现代码
class Calculator:
def add(self, a, b):
return a + b
What is CI?
Continuous integration
Developers pushing many small changes to a central git repo per day. These changes are verified by an automatic software that runs comprehensive tests to ensure no major issues are seen by customers
Benefits of CI
- CI is the first step to DevOps automation and helps with code collaboration
- CI helps improve developer speed without breaking existing code
- CI helps reduce customer churn and user satisfaction by preventing broken code from publishing
Testing
Code Coverage 代码覆盖率
定义:衡量测试过程中被执行的代码行的比例,已执行的代码行数 / 总代码行数
目标:确保测试用例覆盖到代码的每一行。
Branch Coverage 分支覆盖率
定义:衡量测试过程中代码中所有可能分支(如 if/else、switch/case)的执行情况。
公式:已执行的分支数量 / 总分支数量
目标:确保测试覆盖到代码中每个可能的分支(即每个逻辑条件的每个结果)。比代码覆盖率更深入,确保测试用例涵盖所有分支的逻辑。能发现潜在的逻辑问题和边界条件。
Linting Best Practices
Linters(Line Interpreter静态代码分析工具) are programs that look at a program's source code and find problems automatically. They are a common feature of pull request automation because they ensure that "obvious" bugs do not make it to production.
Auto formatter
Once a style guide is adopted, it is possible to configure tools to automatically format code to follow that style guide. Such tools are called autoformatters. In the programming language Go, a command such as the following would use the standard formatter to clean up all of the source files in the repository automatically:
Linting automatically in CI
it's a good idea to set up a "commitback bot" that creates a linted version of the branch automatically if necessary.
In a CI configuration, that might look like:
COPY . .
RUN if ! npm run eslint; then \
npm run eslint --fix && \
git config user.name 'lint bot' && \
git config user.email 'lintbot@example.com' && \
git add -A && \
git commit -m 'Automatically linted' && \
git checkout -b $(git branch --show-current)-linted && \
git push -f origin $(git branch --show-current)-linted; && \
echo 'lint failed!'; exit 1; \
fi
Examples of linters for various programming languages
JavaScript: ESLint
TypeScript: TypeScript ESLint
C++: Google's cpplint
Go: gofmt
Java: CheckStyle, FindBugs
Java, JavaScript, C#, TypeScript, Kotlin, Ruby, Go, Scala, Flex, Python, PHP, HTML, CSS, XML and VB.NET: SonarQube
Ephemeral Environmnet 临时环境
Ephemeral environments are temporary deployments that contain a self-contained version of your application, generally for every feature branch.
Temporary environments are overtaking traditional CI platforms as the most valuable DevOps paradigm for technical teams. Because these environments are made on every change, all stakeholders can review a change without needing a development environment.
In general, ephemeral environments lie halfway between the development environments and staging environments - at the extreme, staging is entirely replaced with ephemeral environments in a Continuous Staging workflow.
临时环境中的数据
By their nature, ephemeral environments are temporary and should be isolated from production servers or data. A reviewer should be able to try deleting a resource in a review without fear of affecting production.
In the early implementation of ephemeral environments, it might make sense to connect API servers with read-only permissions to a staging database (e.g., via IAM roles if using AWS) - however, the end goal should be to have a fresh copy of the database for every commit.
An ideal ephemeral database has three attributes:
Prepopulated - it contains representative, anonymized data - to pass security audits, all Personally-Identifiable Information (PII) must be scrubbed from databases used by ephemeral environments.
Undoable - If, in the course of a review, data is deleted or modified, it should be easy to reset the database to its original state. This is also crucial for running destructive end-to-end tests that perform actions like registration and account deletion.
Migrated - The database uses the schema currently used in production, and has proposed migrations run against it: One of the most common classes of problems uncovered by ephemeral environments are broken or non-performant database migrations.
临时环境中的数据库架构应该与生产环境完全一致。
这样可以确保在临时环境中测试的功能能够无缝适配生产环境,避免因为架构差异引发问题。
The best approach is to create an ephemeral environment for every commit, and hibernate them the second they are provisioned, then transparently wake them up as they are required. This is the interface you'll see in professionally hosted providers.
Continuous Staging
The simple idea for Continuous Staging is to merge staging, ephemeral environments, and CI pipelines into a single unified DevOps workflow.
As your ephemeral environments become more powerful and easier to create, they approach and overtake many aspects of traditional Continuous Integration / Continuous Deployment pipelines.
For example, an ephemeral environment which starts a webserver for every commit could just as easily start a container that runs visual tests against that webserver, or deploy the image used by the webserver.
At its logical conclusion, this concept becomes Continuous Staging, in which CI/CD is merged with ephemeral environments to form a unified CI/CD and review process for every commit.
VMs and Containers
The primary difference between Virtual Machines (VMs) and containers is that the former is more powerful, but the latter is faster.
How the Linux Operating System Works
In the very abstract, Linux keeps track of four things for programs that you run on it:
- Memory - used for storing local variables and the code of the program that is executing. Linux uses some of the memory itself (to store its own local variables, for example) and hands out most the rest to programs that are executing.
- CPU (processors) - used for running things in parallel. If you are playing music while browsing the internet, you might be using "120%" of a processor (meaning one full processor, and 20% of another)
- Disk - If you are reading or writing files, they are stored in the disk. In general, anything that stays around after you reboot the computer will be stored on the disk.
- Devices (GPU, network, etc) - used for various purposes like connecting to the internet and rendering graphics. Linux ensures that multiple programs can use the internet at the same time, or render to a monitor at the same time.
In a usual Linux system, programs know about each other. One program could, for example, write to the file at /home/colin/file.txt and the other could read from it. A program could kill another simply by issuing a kill
command.
This is generally fine - when you run Chrome and Spotify on your computer at the same time, you assume that one will not interfere with the other. Linux will allocate shared resources like memory and CPU fairly so that both programs can work properly, and they won't share any files in any way that breaks things.
However, there are often conflicts between shared resources in business software. If you have two programs that use the Python programming language, one might expect the file at "/usr/bin/python" to be an executable corresponding to Python v2.7, whereas another newer program might expect Python v3.6. In that case, there's no way to run both programs concurrently without changing the code of one of them.
Similarly, two webservers such as Apache and nginx might run on port 80 by default, so it'd be impossible to run both programs at once without changing their configuration files.
This is where Virtual Machines and containers are useful - they allow you to separate resources like files and ports between programs, so that programs can't "step on each others' feet"
Running a program in a VM or Container
The big change for moving programs into containers or VMs is that each will have its own version of shared resources like files and network ports. The container running Chrome might create a file at ~/.chrome/cache while the container running Notepad would not see that file at all.
Similarly, one webserver could reserve port 80 concurrently with another. They'd both be reserving their ports within the scope of their VM/container, not within the entire operating system.
From our example above, the file at "/usr/bin/python" could contain Python 2.7 in one container, but contain Python 3.7 in another. They'd be different files, but each program would "see" a different executable at the same place.
How do containers work?
Namespaces 是什么? Namespaces 是 Linux 的一种功能,用于将操作系统的资源(如进程、网络接口、文件系统等)分组并隔离。
- 每个 namespace 看起来像是一个独立的环境。
- 容器使用 namespaces 隔离自身的资源,使得容器中的进程与宿主机或其他容器的进程彼此独立。
示例解释:
容器内部:
容器中的进程只“看到”它所属的 namespace 的资源。例如,容器内部可能显示有 10 个进程运行。
root@web-6bd6bfc85b-lws8t:/# ps aux | wc -l 10
宿主机外部: 宿主机上的所有进程,包括容器内部的进程,都会显示。例如,宿主机上总共有 577 个进程。
colin@colin-desktop:/$ ps aux | wc -l 577
How do VM work?
虚拟机(VM)类似于模拟器(emulator)。
- 模拟器允许在现代硬件上运行旧的软件或游戏。
- 虚拟机则通过虚拟化技术,模拟一整套计算机硬件环境(CPU、内存、存储、网络等)。
- 示例:使用虚拟机可以在一台物理电脑上运行不同的操作系统,如 Windows、Linux,甚至是旧的硬件架构如 Apple II。
容器与虚拟机的区别
工作原理总结
容器:
- 容器中的进程被 Linux 系统“欺骗”,以为自己运行在一个独立的操作系统环境中,但实际上只是共享了宿主机的 Linux 内核。
- 容器中运行的程序必须是本来能够在 Linux 上运行的程序。
虚拟机(VMs):
- 虚拟机中运行的是一个嵌套的操作系统(如 Linux、macOS、Windows、GameCube 等),这个操作系统通常不知道自己运行在虚拟化的环境中。
- 当虚拟机向硬件写入数据(如存储到硬盘),这些写入请求实际上被发送到了宿主机中的一个文件(例如
/etc/disks/disk-1.qcow2
),而不是实际的物理硬盘。
性能差异
CPU 性能:
- 各种基准测试表明,虚拟机的 CPU 性能比容器慢 10%-20%。
存储需求:
- 虚拟机通常需要比容器多 50%-100% 的存储空间,因为虚拟机需要包含完整的操作系统文件,而容器只需要相关的依赖。
内存开销:
- 虚拟机需要 大约 200MB 的内存用于运行嵌套的操作系统,而容器几乎没有额外的内存开销。
- 为什么在某些场景下虚拟机仍然更合适?
尽管容器的性能优势显而易见,在大多数情况下是更好的选择,但以下场景中虚拟机可能是更优解:
- 运行不受信任的代码:
- 例如,用户提供的代码。如果使用容器,很难完全确保这些代码不能“逃逸”到宿主机中(即突破容器的隔离)。
- 虽然近年来容器隔离技术有了很大提升,但这一直是一个争议点。
- 运行非 Linux 系统:
- 如果需要在 Linux 服务器上运行 Windows 或 macOS 系统,必须使用虚拟机。因为容器无法模拟完整的硬件环境,也无法运行其他内核的操作系统。
- 模拟硬件设备:
- 虚拟机可以模拟硬件设备(例如显卡),但这是一个相对少见的需求,仅在特定行业场景下有用。
滚动部署
滚动部署是一种部署新版本应用程序的策略,能够在不引起停机的情况下完成更新。其工作原理是:首先创建一个新版本应用程序的实例,然后依次关闭旧版本的实例,直到所有实例都已升级为新版本。
优点
良好的支持: 滚动部署在大多数情况下实现起来相对简单,许多编排工具(如 Kubernetes 和 Amazon 的 Elastic Beanstalk)都原生支持滚动部署。
不会出现大规模流量激增: 滚动部署的一个关键好处是减少了运行的服务数量。一个简单的蓝绿部署可能会一次启动 6 个 API 服务器,从而将生产环境的负载翻倍。如果在短时间内服务数量翻倍,数据库可能会触及并发连接限制,这并不罕见。
容易回滚: 另一个好处是,滚动部署如果出现问题,通常可以很容易地回滚。若在升级过程中发现问题,通常可以“反向”滚动部署,删除新版本的应用并重新启动旧版本。
缺点
- 速度较慢: 执行上述算法可能较慢,某些部署可能会有数千个服务实例需要更新,因此如果每次只更新一个实例,滚动部署可能会花费一个小时以上的时间。可以通过增加每次开启/关闭的服务数量来缓解这一问题,这通常称为“爆发限制”(burst limit)。
- API 兼容性问题: 这是滚动部署的最大缺点。假设你在后端的 v2 版本中新增了
/api/hello
端点,并在前端 v2 中使用了该端点。如果用户的请求被发送到前端的 v2 版本,但后端仍是 v1 版本(因为旧版本的实例在部署过程中仍在运行),就可能出现不兼容的情况。可以通过更复杂的路由技术来缓解这个问题,但通常情况下最好确保 API 具有向后兼容性。
需要确保服务能够同时处理其他服务的旧版本和新版本的 API。如果这个契约被破坏,用户在部署过程中仍然可能会遇到错误。
蓝绿部署
Blue/green deployments are a strategy to deploy a new version of an application. They work by starting an entirely new instance of the application, and then routing traffic over to it.
蓝绿部署(Blue/Green Deployments)是一种部署新版本应用程序的策略。它的工作原理是启动一个全新的应用程序实例,然后将流量切换到这个新实例上。
共享资源和集群资源
要设置蓝绿部署,团队需要明确哪些服务将始终部署(集群资源),哪些服务将跨版本共享(共享资源)。
以一个MERN Stack应用为例,我们有三种类型的微服务:
- Node.js 后端 - 处理对象请求。
- React 前端 - 渲染 HTML 和 JavaScript。
- MongoDB - 持久化数据库,存储数据。
数据库服务器 是共享资源。多个版本的应用会同时连接到数据库服务器,标准部署通常不会升级或修改数据库。
其他都是集群资源 —— 它们的每个新版本都会在每次生产推送时部署。
“蓝”和“绿”
蓝绿部署之所以被称为蓝绿,是因为它保持了两个不同的集群,一个命名为“蓝”,一个命名为“绿”,这种命名方式是约定俗成的。
如果当前版本的应用部署在“蓝”集群,我们会将新版本部署到“绿”集群,并将其作为一种过渡环境,确保新版本的应用能正常工作。
在确认新版本正常工作后,我们会将生产流量从“蓝”转移到“绿”,然后以相反的方向重复这一周期。
蓝绿部署的优点和缺点
优点
- 易于理解: 蓝绿部署在概念上非常容易理解。为了设置蓝绿部署,团队只需创建两个相同的生产环境,并随着部署的进行将请求发送到其中一个或另一个。
- 强大: 对于长期运行的任务(如下载),在流量切换到新版本后,仍然可以在“旧版本”的应用中继续运行。
- 可扩展性: 蓝绿部署可以扩展到许多不同的工作流程,这一点将在文章后面进一步讨论。
缺点
- 部署速度慢: 快速部署“热修复”可能很困难(例如,撤回某个更改),因为旧集群可能正在运行一些长时间的任务,无法切换。
- 负载转移麻烦: 在两个集群之间转移负载可能比较繁琐。如果资源自动扩展并且负载同时转移,新的集群可能没有足够的资源来处理请求激增。
- 共享服务修改问题: 如果某个集群修改了共享服务(比如向数据库表中添加列),即使另一个集群没有被切换为“生产环境”,这种修改也可能影响到其他集群。
蓝绿部署的常见扩展
蓝绿部署本质上非常可扩展。许多团队会围绕核心的蓝绿概念设置更复杂的工作流程,以提高稳定性或部署速度。
彩虹部署
一些团队会保持多个集群(如蓝/绿/红/黄等),而不仅仅是两个集群。当有长时间运行的任务(如 MapReduce、网页抓取、视频编码等)无法轻易跨集群转移时,这种部署方式尤其有用。在彩虹部署中,旧集群只有在所有长时间运行的任务完成处理后才会关闭。
验收测试
验收测试(Acceptance Tests)是指在发布新版本应用程序之前,由质量保证(QA)团队和产品利益相关者执行的一系列测试,目的是确保新版本符合产品要求,并且没有重大缺陷。
一些团队依赖手动的 QA 测试而非持续部署流程,特别是那些开发桌面应用或移动应用的团队,这些应用的发布周期相对较长。对于这些团队来说,持续集成和自动化部署可能不是首选,而是采取更为传统的测试和发布方式。
在这种背景下,蓝绿部署的优势得以体现:如果当前版本部署在“蓝集群”中,表示“蓝集群”是生产环境,那么新的应用版本会先部署到“绿集群”。在完成部署后,将一个新的域名(例如 new.example.com)指向“绿集群”。这样,新的版本就可以在一个接近生产环境的环境中进行全面测试。
验收测试的核心在于,QA团队和产品利益相关者可以在真实的生产环境中对新版本进行测试,并决定是否接受新版本。若测试通过,并且没有发现问题,产品团队可以将流量切换到“绿集群”,正式将其作为生产环境。如果在测试过程中发现问题,团队可以选择回滚到“蓝集群”以避免影响用户,直到问题解决。
金丝雀部署
如果新版本的应用包含主观性的更改(如修改 UI),一次性推送给所有用户可能不明智。更改可能会破坏用户的工作流程,且需要根据用户反馈进行修改或回滚。
在蓝绿部署的背景下,“金丝雀部署”指的是随机将大约 5% 的用户路由到新版本的集群,以检查这些用户的反馈。如果这些用户没有遇到问题,流量会逐渐增加,逐步将更多用户转移到新版本的集群上。这个过程通常会一直持续到所有用户都被切换到新版本。
金丝雀部署的主要目的是在生产环境中以小范围的方式测试新版本,避免一次性将所有用户暴露给可能存在的bug或问题。通过这种方式,开发团队可以快速识别并修复问题,而不会影响大多数用户的使用体验。如果在早期阶段发现问题,可以在回滚或修复后重新开始部署过程。
Autoscaling
Autoscaling automates horizontal scaling to ensure that the number of workers is proportional to the load on the system.
例子:CI 系统
假设你正在构建一个持续集成(CI)系统——用户推送代码,你需要启动运行器来执行针对这些代码的测试。你会在用户的工作时间内看到流量的激增,而在非工作时间流量则会大幅减少。
在高峰负载时,如果有 10,000 个并发的 CI 运行,你需要至少 10,000 个运行器来避免排队。然而,在夜间(非高峰时间),这些 10,000 个运行器大多数会处于空闲状态。
高峰时所需的运行器数量可能远高于非高峰时。
在理想的情况下,你能够根据需要创建或销毁运行器——在高峰时段,你可以创建新的运行器,而在非高峰时段,你可以销毁它们。这就是自动扩展的理念。
自动扩展 vs 无服务器
自动扩展通常是在大约 1 小时的工作周期内讨论的。如果你将自动扩展的概念推向极限,你将得到无服务器(serverless):定义快速启动的资源,并在大约 100 毫秒的时间周期内使用它们。
例如,一个 Web 服务器可能根本不存在,直到一个访问者第一次请求页面。它会为这个请求专门启动,然后提供页面,再关闭。
无服务器主要用于那些启动较快且无状态的服务。你不会在无服务器框架中运行类似 CI 运行的任务,但你可能会运行一个 Web 服务器或通知服务。
自动扩展主要用于那些启动较慢或需要状态的服务。你可能会在自动扩展的虚拟机或容器中运行 CI 任务,而不是在无服务器容器中。
到 2021 年为止,这两种模型之间的区别正在变得越来越模糊——无服务器容器正在变得流行,它们通常会运行超过 1 小时。几年后,自动扩展和无服务器可能会融合成一个统一的接口。
Service Discovery
Service discovery is how different services "learn" about each-other in order to connect.
服务发现是不同服务相互“了解”以便连接的方式。
在部署中,一个关键问题是如何让服务能够找到彼此。一个数据库可能位于 10.1.1.1:6543,而 Web 服务器可能位于 10.1.1.2:8080。当你添加更多的 Web 服务器副本或添加全新的服务时,问题会变得更加复杂。
在最简单的情况下,假设你正在部署类似于 MERN(MongoDB, Express/React/Node.js)堆栈来启动一个简单的全栈 Web 应用。这里有三个服务:
- 数据库
- 后端(Express/Node.js)
- 前端(通常使用 create-react-app)
一个全栈 Web 应用通常由后端、前端和数据库组成,在最简单的形式下。
在这种情况下,有三个服务需要被发现:
- 浏览器(终端用户)必须知道 example.com 的 IP 地址(例如 10.111.1.1)
- 浏览器(终端用户)必须知道 api.example.com 的 IP 地址(例如 10.111.1.2)
- 后端(Node.js)必须知道数据库的 IP 地址(例如 10.111.1.3)
一级:没有服务发现
在最简单的配置中,一切都是手动配置的。后端和前端都有静态 IP 地址,并在 DNS 中给定主机名,后端被配置为连接到 MongoDB 的特定端口: cloudflare.com 提供了一个简单的界面来配置面向浏览器的 DNS。
// 在后端中
mongoose.connect('mongodb://' + process.env['MONGODB_IP'] + ':27017/myapp')
# 启动后端时
MONGODB_IP=10.111.1.3 node backend.js
这种配置完全适合简单的产品——它不容易出错,比较安全,且没有过于复杂。
二级:通过分布式哈希表进行服务发现
你可以在这个简单配置中走得很远。大多数产品可以在没有服务发现的情况下启动一个最简化的 MVP。
当你开始看到以下某些情况时,你就应该关注服务发现了:
- 你想要“零停机部署”或使用其他更复杂的部署策略。
- 你有多个微服务。
- 你正在部署到多个环境(例如开发/预生产/临时/生产环境),这变得很难管理。
让我们集中讨论零停机部署,因为它能说明更广泛的问题。
一个小插曲:反向代理与零停机部署
零停机部署的想法很简单:
- 启动一版后端和前端。
- 等待新版本启动,然后将流量转发给它们。
- 关闭旧版本的后端和前端。
然而,由于多种原因(如 DNS 更新可能需要几天时间),很难快速更新 DNS 提供商中的 IP 地址。
解决方案是添加一个 Web 服务器作为前端和后端的“网关”。我们可以在不造成任何停机或等待的情况下更改它指向的位置。这类 Web 服务器被称为反向代理。
最初,对于我们的零停机设置,唯一的不同就是新增了反向代理。
零停机部署的第 2 步:两个版本并行运行——流量同时路由到前端和后端的 v1 和 v2。
最后,我们关闭 v1 服务器。现在运行的就是新版本的代码。
一种直接的方法:将服务 IP 存储在哈希表中。
在这个过程中,我们隐含地假设反向代理能够知道新版本应用的 IP——这就是服务发现的普遍问题。
我们不再需要手动告诉反向代理后端和前端的 IP,而是将一切自动化。当新版本上线时,它们可以分别更新“后端”和“前端”的 IP 值,然后反向代理可以监控该表的变化,并根据变化做路由决策。
举个更具体的例子,假设你使用 nginx 作为反向代理,etcd 作为键值存储,confd 用来更新配置:
server {
server_name example.com;
location / {
proxy_pass http://{{getv "/ips/frontend"}};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
server {
server_name api.example.com;
location / {
proxy_pass http://{{getv "/ips/api"}};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
你所需要做的就是更新前端的代码,将 /ips/frontend
设置为它的 IP,后端做同样的事情。
这样,每当新版本启动时,哈希表中的键就会被更新,而 nginx 就会知道它要将请求“代理转发”到哪个版本的前端或后端。
这种方法存在的问题:
- 复杂性:我们增加了三个新的依赖项,开发人员需要学习(nginx、etcd 和 confd)。
- 容易出错:很容易忘记更新必要的键,如果你运行多个副本的服务版本,更新这些键也会很复杂。
- 需要编写自定义配置文件:通常使用默认配置会更方便,而指定自定义配置则不那么直观。
这引出了行业标准:
三级:通过 DNS 进行服务发现
基于 DNS 的服务发现是行业标准:它直观、不需要配置文件,并且通常由云提供商或编排工具提供。
在深入之前,我们需要稍微了解一下 DNS。
域名服务(DNS)
DNS 的基本思想是将主机名映射到 IP 地址。
例如,当你访问 webapp.io 时,全球 DNS 系统会将 webapp.io 映射到以下 IP 地址:
colin:~$ dig webapp.io
; <<>> DiG 9.16.6-Ubuntu <<>> webapp.io
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27212
;; ANSWER SECTION:
webapp.io. 299 IN A 104.21.79.86
webapp.io. 299 IN A 172.67.169.106
什么是日志聚合?
日志聚合是一种将来自多个服务的应用程序日志收集并标记的方法,汇总到一个易于搜索的仪表板中。它是应用性能管理(APM)系统中的重要组成部分。
为什么需要日志聚合?
- 在复杂系统中(如 Google 的基础设施),一次操作可能涉及多个服务,相关的日志分散在不同的地方。
- 日志聚合提供一个统一的视角,让开发者可以快速诊断生产问题。例如,出现意外结果时,开发者可以通过唯一的请求 ID,查看每个服务对请求的处理过程。
日志聚合系统的组成
以 ELK(ElasticSearch, Logstash, Kibana) 为例,日志聚合通常包括以下三个组件:
日志处理器(Logstash)
将日志条目解析成结构化数据。例如,将一行原始日志
1997/01/01 18:03UTC HTTP/1.1 GET /
转换为:
{"date": "01-01-1997 18:03:00", "service": "backend", "msg": "HTTP/1.1 GET /"}
- 通常暴露接口给前端,用于捕获 JavaScript 错误。
数据存储(Elasticsearch)
- 专门优化了文本搜索和写入速度的数据库。
- 支持复杂查询,如根据时间范围、服务、关键词等筛选日志。
日志前端(Kibana)
- 提供用户界面,用于搜索、过滤和可视化日志数据。
日志聚合的实际应用
- 问题诊断:当用户报告问题(如特定错误代码),可以在 Kibana 中搜索相关日志并定位问题服务。
- 安全性:通过反向代理(如 nginx)或 ELK 的 X-Pack 添加身份验证,保护敏感日志信息,仅管理员可访问。
- 测试用例验证:在 CI 流程中使用日志聚合来检查是否有测试中未捕获的错误。
什么是生产指标监控?
生产指标监控是通过收集和存储数值数据来了解生产系统运行情况的过程。它是生产监控中的第二大关键工具(仅次于日志聚合),两者可以协同工作:
- 日志聚合 主要处理文本信息。
- 指标监控 则聚焦于数值,例如执行时间、内存使用量等。
通过监控指标,团队可以及时发现性能问题和系统瓶颈。
指标监控的关键组件(以 Prometheus 为例)
Prometheus 是一个流行的开源指标监控工具,主要包括以下组件:
- 时间序列数据库(TSDB)
- 存储大量时间序列数据(如页面加载时间等)。
- 数据检索(Retrieval)
- 从各种数据源中提取信息,例如日志解析或直接测量任务执行时间。
- 告警管理器(Alert Manager)
- 当发现问题时,自动通知相关人员或更新状态页面。
- Web 用户界面(Web UI)
- 可视化和分析指标,用于深入理解问题。
重要的生产指标类型
根据产品的特点,以下几类指标通常具有通用性:
- 请求完成时间
- 用于发现系统过载或新功能引发的性能下降。
- 例如,通过日志提取 "响应时间" 字段,分析慢速页面或接口。
- 请求数量
- 监控系统的流量变化,识别潜在问题,如拒绝服务攻击(DoS) 或超载。
- 服务器资源
- 监控有限资源的使用情况,避免因资源耗尽导致的故障:
- 数据库大小及其最大容量
- Web 服务器内存使用情况
- 网络吞吐量与带宽限制
- TLS 证书过期时间
- 监控有限资源的使用情况,避免因资源耗尽导致的故障:
- 分位数分析(Quartile Analysis)
- 将数据划分为多个百分位,以识别性能问题的趋势。例如:
- 最慢的 1% 请求所需时间
- 最慢的 5% 请求所需时间
- 最慢的 25% 请求所需时间
- 这种方法可快速定位 API 或页面的潜在问题。
- 将数据划分为多个百分位,以识别性能问题的趋势。例如: