Fork me on GitHub

分类 工具技巧 下的文章

使用 Arthas 排查线上问题

Arthas 是阿里开源的一款 Java 应用诊断工具,可以在线排查问题,动态跟踪 Java 代码,以及实时监控 JVM 状态。这个工具的大名我早有耳闻,之前一直听别人推荐,却没有使用过。最近在线上遇到了一个问题,由于开发人员在异常处理时没有将线程堆栈打印出来,只是简单地抛出了一个系统错误,导致无法确定异常的具体来源;因为是线上环境,如果要修改代码重新发布,流程会非常漫长,所以只能通过分析代码来定位,正当我看着繁复的代码一筹莫展的时候,突然想到了 Arthas 这个神器,于是尝试着使用 Arthas 来排查这个问题,没想到轻松几步就定位到了原因,上手非常简单,着实让我很吃惊。正所谓 “工欲善其事,必先利其器”,这话果真不假,于是事后花了点时间对 Arthas 的各种用法学习了一番,此为总结。

快速入门

如果你处于联网环境,可以直接使用下面的命令下载并运行 Arthas:

$ wget https://arthas.aliyun.com/arthas-boot.jar
$ java -jar arthas-boot.jar

程序会显示出系统中所有正在运行的 Java 进程,Arthas 为每个进程分配了一个序号:

[INFO] JAVA_HOME: C:\Program Files\Java\jdk1.8.0_351\jre
[INFO] arthas-boot version: 3.7.1
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 9400 .\target\demo-0.0.1-SNAPSHOT.jar
  [2]: 13964 org.eclipse.equinox.launcher_1.6.500.v20230717-2134.jar
  [3]: 6796 org.springframework.ide.vscode.boot.app.BootLanguageServerBootApp

从这个列表中找到出问题的那个 Java 进程,并输入相应的序号,比如这里我输入 1,然后按下回车,Arthas 就会自动下载完整的包,并 Attach 到目标进程,输出如下:

[INFO] Start download arthas from remote server: https://arthas.aliyun.com/download/3.7.1?mirror=aliyun
[INFO] Download arthas success.
[INFO] arthas home: C:\Users\aneasystone\.arthas\lib\3.7.1\arthas
[INFO] Try to attach process 9400
[INFO] Attach process 9400 success.
[INFO] arthas-client connect 127.0.0.1 3658
  ,---.  ,------. ,--------.,--.  ,--.  ,---.   ,---.  
 /  O  \ |  .--. ''--.  .--'|  '--'  | /  O  \ '   .-' 
|  .-.  ||  '--'.'   |  |   |  .--.  ||  .-.  |`.  `-. 
|  | |  ||  |\  \    |  |   |  |  |  ||  | |  |.-'    |
`--' `--'`--' '--'   `--'   `--'  `--'`--' `--'`-----' 

wiki       https://arthas.aliyun.com/doc
tutorials  https://arthas.aliyun.com/doc/arthas-tutorials.html
version    3.7.1
main_class
pid        9400                                                                 
time       2023-09-06 07:16:31

[arthas@9400]$

下载的 Arthas 包位于 ~/.arthas 目录,如果你没有联网,需要提前下载完整的包。

Arthas 偶尔会出现 Attach 不上目标进程的情况,可以查看 ~/logs/arthas 目录下的日志进行排查。

查看所有命令

使用 help 可以查看 Arthas 支持的 所有子命令

[arthas@9400]$ help
 NAME         DESCRIPTION
 help         Display Arthas Help
 auth         Authenticates the current session
 keymap       Display all the available keymap for the specified connection.
 sc           Search all the classes loaded by JVM
 sm           Search the method of classes loaded by JVM
 classloader  Show classloader info
 jad          Decompile class
 getstatic    Show the static field of a class
 monitor      Monitor method execution statistics, e.g. total/success/failure count, average rt, fail rate, etc.
 stack        Display the stack trace for the specified class and method
 thread       Display thread info, thread stack
 trace        Trace the execution time of specified method invocation.
 watch        Display the input/output parameter, return object, and thrown exception of specified method invocation
 tt           Time Tunnel
 jvm          Display the target JVM information
 memory       Display jvm memory info.
 perfcounter  Display the perf counter information.
 ognl         Execute ognl expression.
 mc           Memory compiler, compiles java files into bytecode and class files in memory.
 redefine     Redefine classes. @see Instrumentation#redefineClasses(ClassDefinition...)
 retransform  Retransform classes. @see Instrumentation#retransformClasses(Class...)
 dashboard    Overview of target jvm's thread, memory, gc, vm, tomcat info.
 dump         Dump class byte array from JVM
 heapdump     Heap dump
 options      View and change various Arthas options
 cls          Clear the screen
 reset        Reset all the enhanced classes
 version      Display Arthas version
 session      Display current session information
 sysprop      Display and change the system properties.
 sysenv       Display the system env.
 vmoption     Display, and update the vm diagnostic options.
 logger       Print logger info, and update the logger level
 history      Display command history
 cat          Concatenate and print files
 base64       Encode and decode using Base64 representation
 echo         write arguments to the standard output
 pwd          Return working directory name
 mbean        Display the mbean information
 grep         grep command for pipes.
 tee          tee command for pipes.
 profiler     Async Profiler. https://github.com/jvm-profiling-tools/async-profiler
 vmtool       jvm tool
 stop         Stop/Shutdown Arthas server and exit the console.

这些命令根据功能大抵可以分为以下几类:

  • 与 JVM 相关的命令
  • 与类加载、类、方法相关的命令
  • 统计和观测命令
  • 类 Linux 命令
  • 其他命令

与 JVM 相关的命令

这些命令主要与 JVM 相关,用于查看或修改 JVM 的相关属性,查看 JVM 线程、内存、CPU、GC 等信息:

  • jvm - 查看当前 JVM 的信息;
  • sysenv - 查看 JVM 的环境变量;
  • sysprop - 查看 JVM 的系统属性;
  • vmoption - 查看或修改 JVM 诊断相关的参数;
  • memory - 查看 JVM 的内存信息;
  • heapdump - 将 Java 进程的堆快照导出到某个文件中,方便我们对堆内存进行分析;
  • thread - 查看所有线程的信息,包括线程名称、线程组、优先级、线程状态、CPU 使用率、堆栈信息等;
  • dashboard - 查看当前系统的实时数据面板,包括了线程、内存、GC 和 Runtime 等信息;可以把它看成是 threadmemoryjvmsysenvsysprop 几个命令的综合体;
  • perfcounter - 查看当前 JVM 的 Perf Counter 信息;
  • logger - 查看应用日志信息,支持动态更新日志级别;
  • mbean - 查看或实时监控 Mbean 的信息;
  • vmtool - 利用 JVMTI 接口,实现查询内存对象,强制 GC 等功能;

与类加载、类、方法相关的命令

这些命令主要与类加载、类或方法相关,比如在 JVM 中搜索类或类的方法,查看类的静态属性,编译或反编译,对类进行热更新等:

  • classloader - 查看 JVM 中所有的 Classloader 信息;
  • dump - 将指定类导出成 .class 字节码文件;
  • jad - 将指定类反编译成 Java 源码;
  • mc - 内存编译器,将 Java 源码编译成 .class 字节码文件;
  • redefine / retransform - 这两个命令都可以对已加载的类进行热更新,但是 redefinejad / watch / trace / monitor / tt 等命令会冲突,而且 redefine 后的原来的类不能恢复,所以推荐使用 retransform 命令,关于 JDK 中 Redefine 和 Retransform 机制的区别可以参考 这里
  • sc - Search Class,搜索 JVM 中的类;
  • sm - Search Method,搜索 JVM 中的类的方法;
  • getstatic - 查看类的静态属性;
  • ognl - 执行 ognl 表达式;ognl 非常灵活,可以实现很多功能,比如上面的查看或修改系统属性,查看类的静态属性都可以通过 ognl 实现;

统计和观测

这些命令可以对类方法的执行情况进行统计和监控,是排查线上问题的利器:

  • monitor - 对给定的类方法进行监控,统计其调用次数,调用耗时以及成功率等;
  • stack - 查看一个方法的执行调用堆栈;
  • trace - 对给定的类方法进行监控,输出该方法的调用耗时,和 monitor 的区别在于,它还能跟踪一级方法的调用链路和耗时,帮助快速定位性能问题;
  • watch - 观测指定方法的执行数据,包括方法的入参、返回值、抛出的异常等;
  • tt - 和 watch 命令一样,tt 也可以观测指定方法的执行数据,但 tt 是将每次的执行情况都记录下来,然后再针对每次调用进行排查和分析,所以叫做 Time Tunnel;
  • reset - 上面这些与统计观测相关的命令都是通过 字节码增强技术 来实现的,会在指定类的方法中插入一些切面代码,因此在生产环境诊断结束后,记得执行 reset 命令重置增强过的类(或执行 stop 命令);
  • profiler - 使用 async-profiler 对应用采样,并将采样结果生成火焰图;
  • jfr - 动态开启关闭 JFR 记录,生成的 jfr 文件可以通过 JDK Mission Control 进行分析;

Arthas 命令与 JDK 工具的对比

细数 JDK 自带的那些调试和诊断工具 这篇笔记中我总结了很多 JDK 自带的诊断工具,其实有很多 Arthas 命令和那些 JDK 工具的功能是类似的,只是 Arthas 在输出格式上做了优化,让输出的内容更加美观和易读,而且在功能上做了增强。

Arthas 命令JDK 工具对比
syspropjinfo -sysprops都可以查看 JVM 的系统属性,但是 syspropjinfo 强的是,它还能修改系统属性
vmoptionjinfo -flag都可以查看 JVM 参数,但是 vmoption 只显示诊断相关的参数,比如 HeapDumpOnOutOfMemoryErrorPrintGC
memoryjmap -heap都可以查看 JVM 的内存信息,但是 memory 以表格形式显示,方便用户阅读
heapdumpjmap -dump都可以导出进程的堆内存,只是它在使用上更加简洁
threadjstack都可以列出 JVM 的所有线程,但是 thread 以表格形式显示,方便用户阅读,而且增加了 CPU 使用率的功能,可以方便我们快速找出当前最忙的线程
perfcounterjcmd PerfCounter.print都可以查看 JVM 进程的性能统计信息
classloaderjmap -clstats都可以查看 JVM 的 Classloader 统计信息,但是 classloader 命令还支持以树的形式查看,另外它还支持查看每个 Classloader 实际的 URL,通过 Classloader 查找资源等
jfrjcmd JFR.start都可以开启或关闭 JFR 记录,并生成的 jfr 文件

类 Linux 命令

除了上面那些用于问题诊断的命令,Arthas 还提供了一些类 Linux 命令,方便我们在 Arthas 终端中使用,比如:

  • base64 - 执行 base64 编码和解码;
  • cat - 打印文件内容;
  • cls - 清空当前屏幕区域;
  • echo - 打印参数;
  • grep - 使用字符串或正则表达式搜索文本,并输出匹配的行;
  • history - 输出历史命令;
  • pwd - 输出当前的工作目录;
  • tee - 从 stdin 读取数据,并同时输出到 stdout 和文件;
  • wc - 暂时只支持 wc -l,统计输出的行数;

此外,Arthas 还支持在后台运行任务,仿照 Linux 中的相关命令,我们可以使用 & 在后台运行任务,使用 jobs 列出所有后台任务,使用 Ctrl + Z 暂停任务,使用 bgfg 将暂停的任务转到后台或前台继续运行,使用 kill 终止任务。具体内容可以参考 Arthas 后台异步任务

其他命令

还有一些与 Arthas 本身相关的命令,比如查看 Arthas 的版本号、配置、会话等信息:

  • version - 查看 Arthas 版本号;
  • options - 查看或修改 Arthas 全局配置;
  • keymap - 查看当前所有绑定的快捷键,可以通过 ~/.arthas/conf/inputrc 文件自定义快捷键;
  • session - 查看当前会话信息;
  • auth - 验证当前会话;
  • quit - 退出当前 Arthas 客户端,其他 Arthas 客户端不受影响;
  • stop - 关闭 Arthas 服务端,所有 Arthas 客户端全部退出;这个命令会重置掉所有的增强类(除了 redefine 的类);

线上问题排查

了解了 Arthas 的命令之后,接下来总结一些使用 Arthas 对常见问题的排查思路。

使用 watch 监听方法出入参和异常

相信不少人见过类似下面这样的代码,在遇到异常情况时直接返回系统错误,而没有将异常信息和堆栈打印出来:

@PostMapping("/add")
public String add(@RequestBody DemoAdd demoAdd) {
  try {
    Integer result = demoService.add(demoAdd);
    return String.valueOf(result);
  } catch (Exception e) {
    return "系统错误!";
  }
}

有时候只打印了异常信息 e.getMessage(),但是一看日志全是 NullPointerException,一旦出现异常,根本不知道是哪行代码出了问题。这时,Arthas 的 watch 命令就可以派上用场了:

$ watch com.example.demo.service.DemoService add -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 143 ms, listenerId: 1

我们对 demoService.add() 方法进行监听,当遇到正常请求时:

$ curl -X POST -H "Content-Type: application/json" -d '{"x":1,"y":2}' http://localhost:8080/add
3

watch 的输出如下:

method=com.example.demo.service.DemoService.add location=AtExit
ts=2023-09-11 08:00:46; [cost=1.4054ms] result=@ArrayList[
    @Object[][
        @DemoAdd[DemoAdd(x=1, y=2)],
    ],
    @DemoService[
    ],
    @Integer[3],
]

location=AtExit 表示这个方法正常结束,result 表示方法在结束时的变量值,默认只监听方法的入参、方法所在的实例对象、以及方法的返回值。

当遇到异常请求时:

$ curl -X POST -H "Content-Type: application/json" -d '{"x":1}' http://localhost:8080/add
系统错误!

watch 的输出如下:

method=com.example.demo.service.DemoService.add location=AtExceptionExit
ts=2023-09-11 08:05:20; [cost=0.1402ms] result=@ArrayList[
    @Object[][
        @DemoAdd[DemoAdd(x=1, y=null)],
    ],
    @DemoService[
    ],
    null,
]

可以看到 location=AtExceptionExit 表示这个方法抛出了异常,同样地,result 默认只监听方法的入参、方法所在的实例对象、以及方法的返回值。那么能不能拿到具体的异常信息呢?当然可以,通过自定义观察表达式可以轻松实现。

默认情况下,watch 命令使用的观察表达式为 {params, target, returnObj},所以输出结果里并没有异常信息,我们将观察表达式改为 {params, target, returnObj, throwExp} 重新监听:

$ watch com.example.demo.service.DemoService add "{params, target, returnObj, throwExp}" -x 2

此时就可以输出具体的异常信息了:

method=com.example.demo.service.DemoService.add location=AtExceptionExit
ts=2023-09-11 08:11:19; [cost=0.0961ms] result=@ArrayList[
    @Object[][
        @DemoAdd[DemoAdd(x=1, y=null)],
    ],
    @DemoService[
    ],
    null,
    java.lang.NullPointerException
        at com.example.demo.service.DemoService.add(DemoService.java:11)
        at com.example.demo.controller.DemoController.add(DemoController.java:20)
    ,
]

观察表达式其实是一个 ognl 表达式,可以观察的维度也比较多,参考 表达式核心变量

从上面的例子可以看到,使用 watch 命令有一个很不方便的地方,我们需要提前写好观察表达式,当忘记写表达式或表达式写得不对时,就有可能没有监听到我们的调用,或者虽然监听到调用却没有得到我们想要的内容,这样我们就得反复调试。所以 Arthas 又推出了一个 tt 命令,名为 时空隧道(Time Tunnel)

使用 tt 命令时大多数情况下不用太关注观察表达式,直接监听类方法即可:

$ tt -t com.example.demo.service.DemoService add

tt 会自动地将所有调用都保存下来,直到用户按下 Ctrl+C 结束监听;注意如果方法的调用非常频繁,记得用 -n 参数限制记录的次数,防止记录太多导致内存爆炸:

$ tt -t com.example.demo.service.DemoService add -n 10

当监听结束后,使用 -l 参数查看记录列表:

$ tt -l
 INDEX  TIMESTAMP            COST(ms)  IS-RET  IS-EXP  OBJECT       CLASS                    METHOD                   
------------------------------------------------------------------------------------------------------------
 1000   2023-09-15 07:51:10  0.8111     true   false  0x62726348   DemoService              add
 1001   2023-09-15 07:51:16  0.1017     false  true   0x62726348   DemoService              add

其中 INDEX 列非常重要,我们可以使用 -i 参数指定某条记录查看它的详情:

$ tt -i 1000
 INDEX          1000
 GMT-CREATE     2023-09-15 07:51:10
 COST(ms)       0.8111
 OBJECT         0x62726348
 CLASS          com.example.demo.service.DemoService
 METHOD         add
 IS-RETURN      true
 IS-EXCEPTION   false
 PARAMETERS[0]  @DemoAdd[
                    x=@Integer[1],
                    y=@Integer[2],
                ]
 RETURN-OBJ     @Integer[3]
Affect(row-cnt:1) cost in 0 ms.

从输出中可以看到方法的入参和返回值,如果方法有异常,异常信息也不会丢了:

$ tt -i 1001
 INDEX            1001                                                                                      
 GMT-CREATE       2023-09-15 07:51:16
 COST(ms)         0.1017
 OBJECT           0x62726348
 CLASS            com.example.demo.service.DemoService                                                      
 METHOD           add
 IS-RETURN        false
 IS-EXCEPTION     true
 PARAMETERS[0]    @DemoAdd[
                      x=@Integer[1],                                                                        
                      y=null,
                  ]
                        at com.example.demo.service.DemoService.add(DemoService.java:21)
                        at com.example.demo.controller.DemoController.add(DemoController.java:21)
                        ...
Affect(row-cnt:1) cost in 13 ms.

tt 命令记录了所有的方法调用,方便我们回溯,所以被称为时空隧道,而且,由于它保存了当时调用的所有现场信息,所以我们还可以主动地对一条历史记录进行重做,这在复现某些不常见的 BUG 时非常有用:

$ tt -i 1000 -p
 RE-INDEX       1000
 GMT-REPLAY     2023-09-15 07:52:31
 OBJECT         0x62726348
 CLASS          com.example.demo.service.DemoService
 METHOD         add
 PARAMETERS[0]  @DemoAdd[
                    x=@Integer[1],
                    y=@Integer[2],
                ]
 IS-RETURN      true
 IS-EXCEPTION   false
 COST(ms)       0.1341
 RETURN-OBJ     @Integer[3]
Time fragment[1000] successfully replayed 1 times.

另外,由于 tt 保存了当前环境的对象引用,所以我们甚至可以通过这个对象引用来调用它的方法:

$ tt -i 1000 -w 'target.properties()' -x 2
@DemoProperties[
    title=@String[demo title],
]
Affect(row-cnt:1) cost in 148 ms.

使用 logger 动态更新日志级别

Spring Boot 生产就绪特性 Actuator 这篇笔记中,我们学习过 Spring Boot Actuator 内置了一个 /loggers 端点,可以查看或修改 logger 的日志等级,比如下面这个 POST 请求将 com.example.demo 的日志等级改为 DEBUG

$ curl -s -X POST -d '{"configuredLevel": "DEBUG"}' \
  -H "Content-Type: application/json" \
  http://localhost:8080/actuator/loggers/com.example.demo

使用这种方法修改日志级别可以不重启目标程序,这在线上问题排查时非常有用,但是有时候我们会遇到一些没有开启 Actuator 功能的 Java 程序,这时就可以使用 Arthas 的 logger 命令,实现类似的效果。

直接输入 logger 命令,查看程序所有的 logger 信息:

$ logger
 name                    ROOT
 class                   ch.qos.logback.classic.Logger
 classLoader             org.springframework.boot.loader.LaunchedURLClassLoader@6433a2
 classLoaderHash         6433a2
 level                   INFO
 effectiveLevel          INFO
 additivity              true
 codeSource              jar:file:/D:/demo/target/demo-0.0.1-SNAPSHOT.jar!/BOOT
                         -INF/lib/logback-classic-1.2.10.jar!/
 appenders               name            CONSOLE
                         class           ch.qos.logback.core.ConsoleAppender
                         classLoader     org.springframework.boot.loader.LaunchedURLClassLoader@6433a2
                         classLoaderHash 6433a2
                         target          System.out

默认情况下只会打印有 appender 的 logger 信息,可以加上 --include-no-appender 参数打印所有的 logger 信息,不过这个输出会很长,通常使用 -n 参数打印指定 logger 的信息:

$ logger -n com.example.demo
 name                    com.example.demo
 class                   ch.qos.logback.classic.Logger
 classLoader             org.springframework.boot.loader.LaunchedURLClassLoader@6433a2
 classLoaderHash         6433a2
 level                   null
 effectiveLevel          INFO
 additivity              true
 codeSource              jar:file:/D:/demo/target/demo-0.0.1-SNAPSHOT.jar!/BOOT 
                         -INF/lib/logback-classic-1.2.10.jar!/

可以看到 com.example.demo 的日志级别是 null,说明并没有设置,我们可以使用 -l 参数来修改它:

$ logger -n com.example.demo -l debug
Update logger level fail. Try to specify the classloader with the -c option. 
Use `sc -d CLASSNAME` to find out the classloader hashcode.

需要注意的是,默认情况下,logger 命令会在 SystemClassloader 下执行,如果应用是传统的 war 应用,或者是 Spring Boot 的 fat jar 应用,那么需要指定 classloader。在上面执行 logger -n 时,输出中的 classLoader 和 classLoaderHash 这两行很重要,我们可以使用 -c <classLoaderHash> 来指定 classloader:

$ logger -n com.example.demo -l debug -c 6433a2
Update logger level success.

也可以直接使用 --classLoaderClass <classLoader> 来指定 classloader:

$ logger -n com.example.demo -l debug --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader
Update logger level success.

使用 ognl 查看系统属性和应用配置

有时候我们会在线上环境遇到一些莫名奇妙的问题:比如明明数据库地址配置得好好的,但是程序却报数据库连接错误;又或者明明在配置中心对配置进行了修改,但是程序中却似乎始终不生效;这时我们不确定到底是程序本身逻辑的问题,还是程序没有加载到正确的配置,如果能将程序加载的配置信息打印出来,这个问题就很容易排查了。

如果程序使用了 System.getenv() 来获取环境变量,我们可以使用 sysenv 来进行确认:

$ sysenv JAVA_HOME
 KEY                          VALUE 
---------------------------------------------------------------
 JAVA_HOME                    C:\Program Files\Java\jdk1.8.0_351

如果程序使用了 System.getProperties() 来获取系统属性,我们可以使用 sysprop 来进行确认:

$ sysprop file.encoding
 KEY                          VALUE  
-----------------------------------
 file.encoding                GBK

如果发现系统属性的值有问题,可以使用 sysprop 对其动态修改:

$ sysprop file.encoding UTF-8
Successfully changed the system property.
 KEY                          VALUE  
-----------------------------------
 file.encoding                UTF-8

实际上,无论是 sysenv 还是 sysprop,我们都可以使用 ognl 命令实现:

$ ognl '@System@getenv("JAVA_HOME")'
@String[C:\Program Files\Java\jdk1.8.0_351]
$ 
$ ognl '@System@getProperty("file.encoding")'
@String[UTF-8]

OGNL 是 Object Graphic Navigation Language 的缩写,表示对象图导航语言,它是一种表达式语言,用于访问对象属性、调用对象方法等,它被广泛集成在各大框架中,如 Struts2、MyBatis、Thymeleaf、Spring Web Flow 等。

除了环境变量和系统属性,应用程序本身的配置文件也常常需要排查,在 Spring Boot 程序中,应用配置非常灵活,当存在多个配置文件时,往往搞不清配置是否生效了。这时我们也可以通过 ognl 命令来查看配置,不过使用 ognl 有一个限制,它只能访问静态方法,所以我们在代码中要实现一个 SpringUtils.getBean() 静态方法,这个方法通过 ApplicationContext 来获取 Spring Bean:

@Component
public class SpringUtils implements ApplicationContextAware {

    private static ApplicationContext CONTEXT;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        CONTEXT = applicationContext;
    }
    
    public static Object getBean(String beanName) {
        return CONTEXT.getBean(beanName);
    }
}

这样我们就可以通过 ognl 来查看应用程序的配置类了:

$ ognl '@com.example.demo.utils.SpringUtils@getBean("demoProperties")'
@DemoProperties[
    title=@String[demo title],
]

那么如果我们的代码中没有 SpringUtils.getBean() 这样的静态方法怎么办呢?

在上面我们学到 Arthas 里有一个 tt 命令,可以记录方法调用的所有现场信息,并可以使用 ognl 表达式对现场信息进行查看;这也就意味着我们可以调用监听目标对象的方法,如果监听目标对象有类似于 getBean()getApplicationContext() 这样的方法,那么我们就可以间接地获取到 Spring Bean。在 Spring MVC 程序中,RequestMappingHandlerAdapter 就是这样绝佳的一个监听对象,每次处理请求时都会调用它的 invokeHandlerMethod() 方法,我们对这个方法进行监听:

$ tt -t org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter invokeHandlerMethod
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 43 ms, listenerId: 2
 INDEX  TIMESTAMP            COST(ms)  IS-RET  IS-EXP  OBJECT       CLASS                    METHOD
------------------------------------------------------------------------------------------------------------
 1002   2023-09-15 07:59:27  3.5448     true   false   0x57023e7    RequestMappingHandlerAd  invokeHandlerMethod

因为这个对象有 getApplicationContext() 方法,所以可以通过 tt -w 来调用它,从而获取配置 Bean 的内容:

$ tt -i 1002 -w 'target.getApplicationContext().getBean("demoProperties")'
@DemoProperties[
    title=@String[demo title],
]
Affect(row-cnt:1) cost in 3 ms.

使用 jad/sc/retransform 热更新代码

有时候我们排查出问题原因后,发现只需一个小小的改动就可以修复问题,可能是加一行判空处理,或者是修复一处逻辑错误;又或者问题太复杂一时排查不出结果,需要加几行调试代码来方便问题的定位;如果修改代码,再重新发布到生产环境,耗时会非常长,而且重启服务也可能会影响到当前的用户。在比较紧急的情况下,热更新功能就可以排上用场了,不用重启服务就能在线修改代码逻辑。

热更新代码一般分为下面四个步骤:

第一步,使用 jad 命令将要修改的类反编译成 .java 文件:

$ jad --source-only com.example.demo.service.DemoService > /tmp/DemoService.java

第二步,修改代码:

$ vi /tmp/DemoService.java

比如我们在 add() 方法中加入判空处理:

           public Integer add(DemoAdd demoAdd) {
                if (demoAdd.getX() == null) {
                    demoAdd.setX(0);
                }
                if (demoAdd.getY() == null) {
                    demoAdd.setY(0);
                }
/*20*/         log.debug("x = {}, y = {}", (Object)demoAdd.getX(), (Object)demoAdd.getY());
/*21*/         return demoAdd.getX() + demoAdd.getY();
           }

第三步,使用 mc 命令将修改后的 .java 文件编译成 .class 字节码文件:

$ mc /tmp/DemoService.java -d /tmp
Memory compiler output:
D:\tmp\com\example\demo\service\DemoService.class
Affect(row-cnt:1) cost in 1312 ms.

mc 命令有时会失败,这时我们可以在本地开发环境修改代码,并编译成 .class 文件,再上传到服务器上。

最后一步,使用 redefineretransform 对类进行热更新:

$ retransform /tmp/com/example/demo/service/DemoService.class
retransform success, size: 1, classes:
com.example.demo.service.DemoService

redefineretransform 都可以热更新,但是 redefinejad / watch / trace / monitor / tt 等命令冲突,所以推荐使用 retransform 命令。热更新成功后,使用异常请求再请求一次,现在不会报系统错误了:

$ curl -X POST -H "Content-Type: application/json" -d '{"x":1}' http://localhost:8080/add
1

如果要还原所做的修改,那么只需要删除这个类对应的 retransform entry,然后再重新触发 retransform 即可:

$ retransform --deleteAll
$ retransform --classPattern com.example.demo.service.DemoService

不过要注意的是,Arthas 的热更新也并非无所不能,它也有一些限制,比如不能修改、添加、删除类的字段和方法,只能在原来的方法上修改逻辑。

另外,在生产环境热更新代码并不是很好的行为,而且还非常危险,一定要严格地控制,上线规范也同样重要。

其他使用场景

Arthas 的使用非常灵活,有时候甚至还会有一些意想不到的功能,除了上面这些使用场景,Arthas 的 Issues 中还收集了一些 用户案例,其中有几个案例对我印象很深,非常有启发性,可供参考。

参考

更多

深入 Arthas 实现原理

扫描二维码,在手机上阅读!

容器运行时 containerd 学习笔记

2016 年 12 月,Docker 公司宣布将 containerd 项目从 Docker Engine 中分离出来,形成一个独立的开源项目,并捐赠给 CNCF 基金会,旨在打造一个符合工业标准的容器运行时。Docker 公司之所以做出这样的决定,是因为当时在容器编排的市场上 Docker 面临着 Kubernetes 的极大挑战,将 containerd 分离,是为了方便开展 Docker Swarm 项目,不过结果大家都知道,Docker Swarm 在 Kubernetes 面前以惨败收场。

containerd 并不是直接面向最终用户的,而是主要用于集成到更上层的系统里,比如 Docker Swarm、Kubernetes 或 Mesos 等容器编排系统。containerd 通过 unix domain docket 暴露很低层的 gRPC API,上层系统可以通过这些 API 对机器上的容器整个生命周期进行管理,包括镜像的拉取、容器的启动和停止、以及底层存储和网络的管理等。下面是 containerd 官方提供的架构图:

containerd-architecture.png

从上图可以看出,containerd 的核心主要由一堆的 Services 组成,通过 Content Store、Snapshotter 和 Runtime 三大技术底座,实现了 Containers、Content、Images、Leases、Namespaces 和 Snapshots 等的管理。

其中 Runtime 部分和容器的关系最为紧密,可以看到 containerd 通过 containerd-shim 来支持多种不同的 OCI runtime,其中最为常用的 OCI runtime 就是 runc,所以只要是符合 OCI 标准的容器,都可以由 containerd 进行管理,值得一提的是 runc 也是由 Docker 开源的。

OCI 的全称为 Open Container Initiative,也就是开放容器标准,主要致力于创建一套开放的容器格式和运行时行业标准,目前包括了 Runtime、Image 和 Distribution 三大标准。

containerd 与 Docker 和 Kubernetes 的关系

仔细观察 containerd 架构图的上面部分,可以看出 containerd 通过提供 gRPC API 来供上层应用调用,上层应用可以直接集成 containerd client 来访问它的接口,诸如 Docker Engine、BuildKit 以及 containerd 自带的命令行工具 ctr 都是这样实现的;所以从 Docker 1.11 开始,当我们执行 docker run 命令时,整个流程大致如下:

docker-to-containerd.png

Docker Client 和 Docker Engine 是典型的 CS 架构,当用户执行 docker run 命令时,Docker Client 调用 Docker Engine 的接口,但是 Docker Engine 并不负责容器相关的事情,而是调用 containerd 的 gRPC 接口交给 containerd 来处理;不过 containerd 收到请求后,也并不会直接去创建容器,因为我们在上面提到,创建容器实际上已经有一个 OCI 标准了,这个标准有很多实现,其中 runc 是最常用的一个,所以 containerd 也不用再去实现这套标准了,而是直接调用这些现成的 OCI Runtime 即可。

不过创建容器有一点特别需要注意的地方,我们创建的容器进程需要一个父进程来做状态收集、维持 stdin 等工作的,这个父进程如果是 containerd 的话,那么如果 containerd 挂掉的话,整个机器上的所有容器都得退出了,为了解决这个问题,containerd 引入了 containerd-shim 组件;shim 的意思是垫片,正如它的名字所示,它其实是一个代理,充当着容器进程和 containerd 之间的桥梁;每当用户启动容器时,都会先启动一个 containerd-shim 进程,containerd-shim 然后调用 runc 来启动容器,之后 runc 会退出,而 containerd-shim 则会成为容器进程的父进程,负责收集容器进程的状态,上报给 containerd,并在容器中 PID 为 1 的进程退出后接管容器中的子进程进行清理,确保不会出现僵尸进程。

介绍完 containerd 与 Docker 之间的关系,我们再来看看它与 Kuberntes 的关系,从历史上看,Kuberntes 和 Docker 相爱相杀多年,一直是开源社区里热门的讨论话题。

在 Kubernetes 早期的时候,由于 Docker 风头正盛,所以 Kubernetes 选择通过直接调用 Docker API 来管理容器:

kubelet-to-docker.png

后来随着容器技术的发展,出现了很多其他的容器运行时,为了让 Kubernetes 平台支持更多的容器运行时,而不仅仅是和 Docker 绑定,Google 于是联合 Red Hat 一起推出了 CRI 标准。CRI 的全称为 Container Runtime Interface,也就是容器运行时接口,它是 Kubernetes 定义的一组与容器运行时进行交互的接口,只要你实现了这套接口,就可以对接到 Kubernetes 平台上来。不过在那个时候,并没有多少容器运行时会直接去实现 CRI 接口,而是通过 shim 来适配不同的容器运行时,其中 dockershim 就是 Kubernetes 将 Docker 适配到 CRI 接口的一个实现:

kubelet-to-docker-shim.png

很显然,这个链路太长了,好在 Docker 将 containerd 项目独立出来了,那么 Kubernetes 是否可以绕过 Docker 直接与 containerd 通信呢?答案当然是肯定的,从 containerd 1.0 开始,containerd 开发了 CRI-Containerd,可以直接与 containerd 通信,从而取代了 dockershim(从 Kubernetes 1.24 开始,dockershim 已经从 Kubernetes 的代码中删除了,cri-dockerd 目前交由社区维护):

kubelet-cri-containerd.png

到了 containerd 1.1 版本,containerd 又进一步将 CRI-Containerd 直接以插件的形式集成到了 containerd 主进程中,也就是说 containerd 已经原生支持 CRI 接口了,这使得调用链路更加简洁:

kubelet-to-containerd.png

这也是目前 Kubernetes 默认的容器运行方案。不过,这条调用链路还可以继续优化下去,在 CNCF 中,还有另一个和 containerd 齐名的容器运行时项目 cri-o,它不仅支持 CRI 接口,而且创建容器的逻辑也更简单,通过 cri-o,kubelet 可以和 OCI 运行时直接对接,减少任何不必要的中间开销:

kubelet-to-crio.png

快速开始

这一节主要学习 containerd 的安装和使用。

安装 containerd

首先从 containerd 的 Release 页面 下载最新版本:

$ curl -LO https://github.com/containerd/containerd/releases/download/v1.7.2/containerd-1.7.2-linux-amd64.tar.gz

然后将其解压到 /usr/local/bin 目录:

$ tar Cxzvf /usr/local containerd-1.7.2-linux-amd64.tar.gz 
bin/
bin/containerd-shim-runc-v1
bin/containerd-shim-runc-v2
bin/containerd-stress
bin/containerd
bin/containerd-shim
bin/ctr

其中,containerd 是服务端,我们可以直接运行:

$ containerd
INFO[2023-06-18T14:28:14.867212652+08:00] starting containerd revision=0cae528dd6cb557f7201036e9f43420650207b58 version=v1.7.2
...
INFO[2023-06-18T14:28:14.922388455+08:00] serving... address=/run/containerd/containerd.sock.ttrpc
INFO[2023-06-18T14:28:14.922477258+08:00] serving... address=/run/containerd/containerd.sock
INFO[2023-06-18T14:28:14.922529910+08:00] Start subscribing containerd event
INFO[2023-06-18T14:28:14.922570820+08:00] Start recovering state
INFO[2023-06-18T14:28:14.922636858+08:00] Start event monitor
INFO[2023-06-18T14:28:14.922653276+08:00] Start snapshots syncer
INFO[2023-06-18T14:28:14.922662467+08:00] Start cni network conf syncer for default
INFO[2023-06-18T14:28:14.922671149+08:00] Start streaming server
INFO[2023-06-18T14:28:14.922689846+08:00] containerd successfully booted in 0.060348s

ctr 是客户端,运行 ctr version 确认 containerd 是否安装成功:

$ ctr version
Client:
  Version:  v1.7.2
  Revision: 0cae528dd6cb557f7201036e9f43420650207b58
  Go version: go1.20.4

Server:
  Version:  v1.7.2
  Revision: 0cae528dd6cb557f7201036e9f43420650207b58
  UUID: 9eb2cbd4-8c1d-4321-839b-a8a4fc498de8

以 systemd 方式启动 containerd

官方已经为我们准备好了 containerd.service 文件,我们只需要将其下载下来,放在 systemd 的配置目录下即可:

$ mkdir -p /usr/local/lib/systemd/system/
$ curl -L https://raw.githubusercontent.com/containerd/containerd/main/containerd.service -o /usr/local/lib/systemd/system/containerd.service

containerd.service 文件内容如下:

[Unit]
Description=containerd container runtime
Documentation=https://containerd.io
After=network.target local-fs.target

[Service]
#uncomment to enable the experimental sbservice (sandboxed) version of containerd/cri integration
#Environment="ENABLE_CRI_SANDBOXES=sandboxed"
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/containerd

Type=notify
Delegate=yes
KillMode=process
Restart=always
RestartSec=5
# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNPROC=infinity
LimitCORE=infinity
LimitNOFILE=infinity
# Comment TasksMax if your systemd version does not supports it.
# Only systemd 226 and above support this version.
TasksMax=infinity
OOMScoreAdjust=-999

[Install]
WantedBy=multi-user.target

其中有两个配置很重要,Delegate=yes 表示允许 containerd 管理自己创建容器的 cgroups,否则 systemd 会将进程移到自己的 cgroups 中,导致 containerd 无法正确获取容器的资源使用情况;默认情况下,systemd 在停止或重启服务时会在进程的 cgroup 中查找并杀死所有子进程,KillMode=process 表示让 systemd 只杀死主进程,这样可以确保升级或重启 containerd 时不影响现有的容器。

然后我们使用 systemd 守护进程的方式启动 containerd 服务:

$ systemctl enable --now containerd

这样当系统重启后,containerd 服务也会自动启动了。

安装 runc

安装好 containerd 之后,我们就可以使用 ctr 执行一些基本操作了,比如使用 ctr image pull 下载镜像:

$ ctr image pull docker.io/library/nginx:alpine
docker.io/library/nginx:alpine:                                                   resolved
index-sha256:2d194184b067db3598771b4cf326cfe6ad5051937ba1132b8b7d4b0184e0d0a6:    exists  
manifest-sha256:2d4efe74ef541248b0a70838c557de04509d1115dec6bfc21ad0d66e41574a8a: exists  
layer-sha256:768e67c521a97f2acf0382a9750c4d024fc1e541e22bab2dec1aad36703278f1:    exists  
config-sha256:4937520ae206c8969734d9a659fc1e6594d9b22b9340bf0796defbea0c92dd02:   exists  
layer-sha256:4db1b89c0bd13344176ddce2d093b9da2ae58336823ffed2009a7ea4b62d2a95:    exists  
layer-sha256:bd338968799fef766509223449d72392692f1f56802da9059ae3f0965c2885e2:    exists  
layer-sha256:6a107772494d184e0fddf5d99c877e2fa8d07d1d47b714c17b7d20eba1da01c6:    exists  
layer-sha256:9f05b0cc5f6e8010689a6331bad9ca02c62caa226b7501a64d50dcca0847dcdb:    exists  
layer-sha256:4c5efdb87c4a2350cc1c2781a80a4d3e895447007d9d8eac1e743bf80dd75c84:    exists  
layer-sha256:c8794a7158bff7f518985e76c590029ccc6b4c0f6e66e82952c3476c095225c9:    exists  
layer-sha256:8de2a93581dcb1cc62dd7b6e1620bc8095befe0acb9161d5f053a9719e145678:    exists  
elapsed: 2.8 s                                                                    total:   0.0 B (0.0 B/s)
unpacking linux/amd64 sha256:2d194184b067db3598771b4cf326cfe6ad5051937ba1132b8b7d4b0184e0d0a6...
done: 23.567287ms    

注意这里和 docker pull 的不同,镜像名称需要写全称。

不过这个时候,我们还不能运行镜像,我们不妨用 ctr run 命令运行一下试试:

$ ctr run docker.io/library/nginx:alpine nginx
ctr: failed to create shim task: 
    OCI runtime create failed: 
        unable to retrieve OCI runtime error (open /run/containerd/io.containerd.runtime.v2.task/default/nginx/log.json: no such file or directory):
            exec: "runc": executable file not found in $PATH: unknown

正如前文所述,这是因为 containerd 依赖 OCI runtime 来进行容器管理,containerd 默认的 OCI runtime 是 runc,我们还没有安装它。runc 的安装也非常简单,直接从其项目的 Releases 页面 下载最新版本:

$ curl -LO https://github.com/opencontainers/runc/releases/download/v1.1.7/runc.amd64

并将其安装到 /usr/local/sbin 目录即可:

$ install -m 755 runc.amd64 /usr/local/sbin/runc

使用 ctr container rm 删除刚刚运行失败的容器:

$ ctr container rm nginx

然后再使用 ctr run 重新运行:

$ ctr run docker.io/library/nginx:alpine nginx
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2023/06/18 07:04:52 [notice] 1#1: using the "epoll" event method
2023/06/18 07:04:52 [notice] 1#1: nginx/1.25.1
2023/06/18 07:04:52 [notice] 1#1: built by gcc 12.2.1 20220924 (Alpine 12.2.1_git20220924-r4) 
2023/06/18 07:04:52 [notice] 1#1: OS: Linux 3.10.0-1160.el7.x86_64
2023/06/18 07:04:52 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1024:1024
2023/06/18 07:04:52 [notice] 1#1: start worker processes
2023/06/18 07:04:52 [notice] 1#1: start worker process 30

可以看到此时容器正常启动了,不过目前这个容器还不具备网络能力,所以我们无法从外部访问它,可以使用 ctr task exec 进入容器:

$ ctr task exec -t --exec-id nginx nginx sh

在容器内部验证 nginx 服务是否正常:

/ # curl localhost:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

安装 CNI 插件

正如上一节所示,默认情况下 containerd 创建的容器只有 lo 网络,无法从容器外部访问,如果希望将容器内的网络端口暴露出来,我们还需要安装 CNI 插件。和 CRI 一样,CNI 也是一套规范接口,全称为 Container Network Interface,即容器网络接口,它提供了一种将容器网络插件化的解决方案。CNI 涉及两个基本概念:容器和网络,它的接口也是围绕着这两个基本概念进行设计的,主要有两个:ADD 负责将容器加入网络,DEL 负责将容器从网络中删除,有兴趣的同学可以阅读 CNI Specification 了解更具体的信息。

实战 Docker 容器网络 这篇笔记中,我们曾经学习过 Docker 的 CNM 网络模型,它和 CNI 相比要复杂一些。CNM 和 CNI 是目前最流行的两种容器网络方案,关于他俩的区别,可以参考 docker的网络-Container network interface(CNI)与Container network model(CNM)

官方提供了很多 CNI 接口的实现,比如 bridgeipvlanmacvlan 等,这些都被称为 CNI 插件,此外,很多开源的容器网络项目,比如 calicoflannelweave 等也实现了 CNI 插件。其实,CNI 插件就是一堆的可执行文件,我们可以从 CNI 插件的 Releases 页面 下载最新版本:

$ curl -LO https://github.com/containernetworking/plugins/releases/download/v1.3.0/cni-plugins-linux-amd64-v1.3.0.tgz

然后将其解压到 /opt/cni/bin 目录(这是 CNI 插件的默认目录):

$ mkdir -p /opt/cni/bin
$ tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v1.3.0.tgz 
./
./loopback
./bandwidth
./ptp
./vlan
./host-device
./tuning
./vrf
./sbr
./tap
./dhcp
./static
./firewall
./macvlan
./dummy
./bridge
./ipvlan
./portmap
./host-local

可以看到目录中包含了很多插件,这些插件按功能可以分成三大类:Main、IPAM 和 Meta:

  • Main:负责创建网络接口,支持 bridgeipvlanmacvlanptphost-devicevlan 等类型的网络;
  • IPAM:负责 IP 地址的分配,支持 dhcphost-localstatic 三种分配方式;
  • Meta:包含一些其他配置插件,比如 tuning 用于配置网络接口的 sysctl 参数,portmap 用于主机和容器之间的端口映射,bandwidth 用于限流等等。

CNI 插件是通过 JSON 格式的文件进行配置的,我们首先创建 CNI 插件的配置目录 /etc/cni/net.d

$ mkdir -p /etc/cni/net.d

然后在这个目录下新建一个配置文件:

$ vi /etc/cni/net.d/10-mynet.conf
{
    "cniVersion": "0.2.0",
    "name": "mynet",
    "type": "bridge",
    "bridge": "cni0",
    "isGateway": true,
    "ipMasq": true,
    "ipam": {
        "type": "host-local",
        "subnet": "10.22.0.0/16",
        "routes": [
            { "dst": "0.0.0.0/0" }
        ]
    }
}

其中 "name": "mynet" 表示网络的名称,"type": "bridge" 表示创建的是一个网桥网络,"bridge": "cni0" 表示创建网桥的名称,isGateway 表示为网桥分配 IP 地址,ipMasq 表示开启 IP Masquerade 功能,关于 bridge 插件的更多配置,可以参考 bridge plugin 文档

下面的 ipam 部分是 IP 地址分配的相关配置,"type": "host-local" 表示将使用 host-local 插件来分配 IP,这是一种简单的本地 IP 地址分配方式,它会从一个地址范围内来选择分配 IP,关于 host-local 插件的更多配置,可以参考 host-local 文档

除了网桥网络,我们再新建一个 loopback 网络的配置文件:

$ vi /etc/cni/net.d/99-loopback.conf
{
    "cniVersion": "0.2.0",
    "name": "lo",
    "type": "loopback"
}

CNI 项目中内置了一些简单的 Shell 脚本用于测试 CNI 插件的功能:

$ git clone https://github.com/containernetworking/cni.git
$ cd cni/scripts/
$ ls
docker-run.sh  exec-plugins.sh  priv-net-run.sh  release.sh

其中 exec-plugins.sh 脚本用于执行 CNI 插件,创建网络,并将某个容器加入该网络:

$ ./exec-plugins.sh 
Usage: ./exec-plugins.sh add|del CONTAINER-ID NETNS-PATH
  Adds or deletes the container specified by NETNS-PATH to the networks
  specified in $NETCONFPATH directory

该脚本有三个参数,第一个参数为 adddel 表示将容器添加到网络或将容器从网络中删除,第二个参数 CONTAINER-ID 表示容器 ID,一般没什么要求,保证唯一即可,第三个参数 NETNS-PATH 表示这个容器进程的网络命名空间位置,一般位于 /proc/${PID}/ns/net,所以,想要将上面运行的 nginx 容器加入网络中,我们需要知道这个容器进程的 PID,这个可以通过 ctr task list 得到:

$ ctr task ls
TASK     PID      STATUS    
nginx    20350    RUNNING

然后执行下面的命令:

$ CNI_PATH=/opt/cni/bin ./exec-plugins.sh add nginx /proc/20350/ns/net

前面的 CNI_PATH=/opt/cni/bin 是必不可少的,告诉脚本从这里执行 CNI 插件,执行之后,我们可以在主机上执行 ip addr 进行确认:

$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:7f:8e:9a brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic enp0s3
       valid_lft 72476sec preferred_lft 72476sec
    inet6 fe80::e0ae:69af:54a5:f8d0/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
3: cni0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 72:87:b6:07:19:07 brd ff:ff:ff:ff:ff:ff
    inet 10.22.0.1/16 brd 10.22.255.255 scope global cni0
       valid_lft forever preferred_lft forever
    inet6 fe80::7087:b6ff:fe07:1907/64 scope link 
       valid_lft forever preferred_lft forever
11: vethc5e583fc@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master cni0 state UP group default 
    link/ether 92:5c:c3:6a:a0:56 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::905c:c3ff:fe6a:a056/64 scope link 
       valid_lft forever preferred_lft forever

可以看出主机上多了一个名为 cni0 的网桥设备,这个就对应我们创建的网络,执行 ip route 也可以看到主机上多了一条到 cni0 的路由:

$ ip route
default via 10.0.2.2 dev enp0s3 proto dhcp metric 100 
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15 metric 100 
10.22.0.0/16 dev cni0 proto kernel scope link src 10.22.0.1

另外,我们还能看到一个 veth 设备,在 实战 Docker 容器网络 这篇笔记中我们已经学习过 veth 是一种虚拟的以太网隧道,其实就是一根网线,网线一头插在主机的 cni0 网桥上,另一头则插在容器里。我们可以进到容器里面进一步确认:

$ ctr task exec -t --exec-id nginx nginx sh
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
4: eth0@if11: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP 
    link/ether 0a:c2:11:63:ea:8c brd ff:ff:ff:ff:ff:ff
    inet 10.22.0.9/16 brd 10.22.255.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::8c2:11ff:fe63:ea8c/64 scope link 
       valid_lft forever preferred_lft forever

容器除了 lo 网卡之外,多了一张 eth0 网卡,它的 IP 地址是 10.22.0.9,这个正是我们在 10-mynet.conf 配置文件中定义的范围。这时,我们就可以在主机上通过这个 IP 来访问容器内部了:

$ curl 10.22.0.9:80

exec-plugins.sh 脚本会遍历 /etc/cni/net.d/ 目录下的所有配置来创建网络接口,我们也可以使用 cnitool 来创建特定的网络接口。

通过将容器添加到指定网络,可以让容器具备和外界通信的能力,除了这种方式之外,我们也可以直接以主机网络模式启动容器:

$ ctr run --net-host docker.io/library/nginx:alpine nginx

使用 ctr 操作 containerd

经过上面的步骤,containerd 服务已经在我们的系统中安装和配置好了,接下来我们将学习命令行工具 ctr 对 containerd 进行操作,ctr 是 containerd 部署包中内置的命令行工具,功能比较简单,一般用于 containerd 的调试,其实在上面的安装步骤中已经多次使用过它,这一节对 ctr 做个简单的总结。

ctr image 命令

命令含义
ctr image list查看镜像列表
ctr image list -q查看镜像列表,只显示镜像名称
ctr image pull docker.io/library/nginx:alpine拉取镜像,注意镜像名称的前缀不能少
ctr image pull --platform linux/amd64 docker.io/library/nginx:alpine拉取指定平台的镜像
ctr image pull --all-platforms docker.io/library/nginx:alpine拉取所有平台的镜像
ctr image tag docker.io/library/nginx:alpine 192.168.1.109:5000/nginx:alpine给镜像打标签
ctr image push 192.168.1.109:5000/nginx:alpine推送镜像
ctr image push --user username:password 192.168.1.109:5000/nginx:alpine推送镜像到带认证的镜像仓库
ctr image rm 192.168.1.109:5000/nginx:alpine删除镜像
ctr image export nginx.tar docker.io/library/nginx:alpine导出镜像
ctr image import hello.tar导入镜像
ctr image import --platform linux/amd64 hello.tar导入指定平台的镜像,如果导出的镜像文件只包含一个平台,导入时可能会报错 ctr: content digest sha256:xxx: not found,必须带上 --platform 这个参数
ctr image mount docker.io/library/nginx:alpine ./nginx将镜像挂载到主机目录
ctr image unmount ./nginx将镜像从主机目录卸载

ctr container 命令

命令含义
ctr container list查看容器列表
ctr container list -q查看容器列表,只显示容器名称
ctr container create docker.io/library/nginx:alpine nginx创建容器
ctr container info nginx查看容器详情,类似于 docker inspect
ctr container rm nginx删除容器

ctr task 命令

命令含义
ctr task list查看任务列表,使用 ctr container create 创建容器时并没有运行,它只是一个静态的容器,包含了容器运行所需的资源和配置数据
ctr task start nginx启动容器
ctr task exec -t --exec-id nginx nginx sh进入容器进行操作,注意 --exec-id 参数随便写,只要唯一就行
ctr task metrics nginx查看容器的 CPU 和内存使用情况
ctr task ps nginx查看容器中的进程对应宿主机中的 PID
ctr task pause nginx暂停容器,暂停后容器状态变成 PAUSED
ctr task resume nginx恢复容器继续运行
ctr task kill nginx停止容器,停止后容器状态变成 STOPPED
ctr task rm nginx删除任务

ctr run 命令

命令含义
ctr run docker.io/library/nginx:alpine nginx创建容器并运行,相当于 ctr container create + ctr task start
ctr run --rm docker.io/library/nginx:alpine nginx退出容器时自动删除容器
ctr run -d docker.io/library/nginx:alpine nginx运行容器,运行之后从终端退出(detach)但容器不停止
ctr run --mount type=bind,src=/root/test,dst=/test,options=rbind:rw docker.io/library/nginx:alpine nginx挂载本地目录或文件到容器
ctr run --env USER=root docker.io/library/nginx:alpine nginx为容器设置环境变量
ctr run --null-io docker.io/library/nginx:alpine nginx运行容器,并将控制台输出重定向到 /dev/null
ctr run --log-uri file:///var/log/nginx.log docker.io/library/nginx:alpine nginx运行容器,并将控制台输出写到文件中
ctr run --net-host docker.io/library/nginx:alpine nginx使用主机网络运行容器
ctr run --with-ns=network:/var/run/netns/nginx docker.io/library/nginx:alpine nginx使用指定命名空间文件运行容器

命名空间

命令含义
ctr ns list查看命名空间列表
ctr ns create test创建命名空间
ctr ns rm test删除命名空间
ctr -n test image list查看特定命名空间下的镜像列表
ctr -n test container list查看特定命名空间下的容器列表

containerd 通过命名空间进行资源隔离,当没有指定命名空间时,默认使用 default 命名空间,Docker 和 Kubernetes 都可以基于 containerd 来管理容器,Docker 使用的是 moby 命名空间,Kubernetes 使用的是 k8s.io 命名空间,所以如果想查看 Kubernetes 运行的容器,可以通过 ctr -n k8s.io container list 查看。

除了上面的一些常用命令,还有一些不常用的命令,比如 pluginscontentleasessnapshotsleasesshim 等,这里就不一一介绍了,感兴趣的同学可以使用 ctrctr help 获取更多的帮助信息。

虽然使用 ctr 可以进行大部分 containerd 的日常操作,但是这些操作偏底层,对用户很不友好,比如不支持镜像构建,网络配置非常繁琐,所以 ctr 一般是供开发人员测试 containerd 用的;如果希望找一款更简单的命令行工具,可以使用 nerdctl,它的操作和 Docker 非常类似,对 Docker 用户来说会感觉非常亲近,nerdctl 相对于 ctr 来说,有着以下几点区别:

  • nerdctl 支持使用 Dockerfile 构建镜像;
  • nerdctl 支持使用 docker-compose.yaml 定义和管理多个容器;
  • nerdctl 支持在容器内运行 systemd;
  • nerdctl 支持使用 CNI 插件来配置容器网络;

除了 ctr 和 nerdctl,我们还可以使用 crictl 来操作 containerd,crictl 是 Kubernetes 提供的 CRI 客户端工具,由于 containerd 实现了 CRI 接口,所以 crictl 也可以充当 containerd 的客户端。此外,官方还提供了一份教程可以让我们 实现自己的 containerd 客户端

参考

更多

使用 ctr run --with-ns 让容器在启动时加入已存在命名空间

上面我们是通过往容器进程的网络命名空间中增加网络接口来实现的,我们也可以先创建网络命名空间:

$ ip netns add nginx

这个网络命名空间的文件位置位于 /var/run/netns/nginx

$ ls /var/run/netns
nginx

然后在这个网络命名空间中配置网络接口,可以执行 exec-plugins.sh 脚本:

$ CNI_PATH=/opt/cni/bin ./exec-plugins.sh add nginx /var/run/netns/nginx

或执行 cnitool 命令:

$ CNI_PATH=/opt/cni/bin cnitool add mynet /var/run/netns/nginx

ctr run 命令在启动容器的时候可以使用 --with-ns 参数让容器在启动时候加入到一个已存在的命名空间,所以可以通过这个参数加入到上面配置好的网络命名空间中:

$ ctr run --with-ns=network:/var/run/netns/nginx docker.io/library/nginx:alpine nginx

containerd 的配置文件

使用 containerd config default 命令生成默认配置文件:

$ mkdir -p /etc/containerd
$ containerd config default > /etc/containerd/config.toml
扫描二维码,在手机上阅读!

基于 Argo CD 的 GitOps 实践笔记

GitOps 这个概念最早是由 Weaveworks 的 CEO Alexis Richardson 在 2017 年提出的,它是一种全新的基于 Git 仓库来管理 Kubernetes 集群和交付应用程序的方式。它包含以下四个基本原则:

  1. 声明式(Declarative):整个系统必须通过声明式的方式进行描述,比如 Kubernetes 就是声明式的,它通过 YAML 来描述系统的期望状态;
  2. 版本控制和不可变(Versioned and immutable):所有的声明式描述都存储在 Git 仓库中,通过 Git 我们可以对系统的状态进行版本控制,记录了整个系统的修改历史,可以方便地回滚;
  3. 自动拉取(Pulled automatically):我们通过提交代码的形式将系统的期望状态提交到 Git 仓库,系统从 Git 仓库自动拉取并做出变更,这种被称为 Pull 模式,整个过程不需要安装额外的工具,也不需要配置 Kubernetes 的认证授权;而传统的 CI/CD 工具如 Jenkins 或 CircleCI 等使用的是 Push 模式,这种模式一般都会在 CI 流水线运行完成后通过执行命令将应用部署到系统中,这不仅需要安装额外工具(比如 kubectl),还需要配置 Kubernetes 的授权,而且这种方式无法感知部署状态,所以也就无法保证集群状态的一致性了;
  4. 持续调谐(Continuously reconciled):通过在目标系统中安装一个 Agent,一般使用 Kubernetes Operator 来实现,它会定期检测实际状态与期望状态是否一致,一旦检测到不一致,Agent 就会自动进行修复,确保系统达到期望状态,这个过程就是调谐(Reconciliation);这样做的好处是将 Git 仓库作为单一事实来源,即使集群由于误操作被修改,Agent 也会通过持续调谐自动恢复。

其实,在提出 GitOps 概念之前,已经有另一个概念 IaC (Infrastructure as Code,基础设施即代码)被提出了,IaC 表示使用代码来定义基础设施,方便编辑和分发系统配置,它作为 DevOps 的最佳实践之一得到了社区的广泛关注。关于 IaC 和 GitOps 的区别,可以参考 The GitOps FAQ

Argo CD 快速入门

基于 GitOps 理念,很快诞生出了一批 声明式的持续交付(Declarative Continuous Deployment) 工具,比如 Weaveworks 的 Flux CD 和 Intuit 的 Argo CD,虽然 Weaveworks 是 GitOps 概念的提出者,但是从社区的反应来看,似乎 Argo CD 要更胜一筹。

Argo 项目最初是由 Applatix 公司于 2017 年创建,2018 年这家公司被 Intuit 收购,Argo 项目就由 Intuit 继续维护和演进。Argo 目前包含了四个子项目:Argo WorkflowsArgo EventsArgo CDArgo Rollouts,主要用于运行和管理 Kubernetes 上的应用程序和任务,所有的 Argo 项目都是基于 Kubernetes 控制器和自定义资源实现的,它们组合在一起,提供了创建应用程序和任务的三种模式:服务模式、工作流模式和基于事件的模式。2020 年 4 月 7 日,Argo 项目加入 CNCF 开始孵化,并于 2022 年 12 月正式毕业,成为继 Kubernetes、Prometheus 和 Envoy 之后的又一个 CNCF 毕业项目。

这一节我们将学习 Argo CD,学习如何通过 Git 以及声明式描述来部署 Kubernetes 资源。

安装 Argo CD

Argo CD 提供了两种 安装形式多租户模式(Multi-Tenant)核心模式(Core)。多租户模式提供了 Argo CD 的完整特性,包括 UI、SSO、多集群管理等,适用于多个团队用户共同使用;而核心模式只包含核心组件,不包含 UI 及多租户功能,适用于集群管理员独自使用。

使用比较多的是多租户模式,官方还为其提供了两份部署配置:非高可用配置 install.yaml 和高可用配置 ha/install.yaml,生产环境建议使用高可用配置,开发和测试环境可以使用非高可用配置,下面就使用非高可用配置来安装 Argo CD。

首先,创建一个 argocd 命名空间:

$ kubectl create namespace argocd
namespace/argocd created

接着将 Argo CD 部署到该命名空间中:

$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
customresourcedefinition.apiextensions.k8s.io/applications.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/applicationsets.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/appprojects.argoproj.io created
serviceaccount/argocd-application-controller created
serviceaccount/argocd-applicationset-controller created
serviceaccount/argocd-dex-server created
serviceaccount/argocd-notifications-controller created
serviceaccount/argocd-redis created
serviceaccount/argocd-repo-server created
serviceaccount/argocd-server created
role.rbac.authorization.k8s.io/argocd-application-controller created
role.rbac.authorization.k8s.io/argocd-applicationset-controller created
role.rbac.authorization.k8s.io/argocd-dex-server created
role.rbac.authorization.k8s.io/argocd-notifications-controller created
role.rbac.authorization.k8s.io/argocd-server created
clusterrole.rbac.authorization.k8s.io/argocd-application-controller created
clusterrole.rbac.authorization.k8s.io/argocd-server created
rolebinding.rbac.authorization.k8s.io/argocd-application-controller created
rolebinding.rbac.authorization.k8s.io/argocd-applicationset-controller created
rolebinding.rbac.authorization.k8s.io/argocd-dex-server created
rolebinding.rbac.authorization.k8s.io/argocd-notifications-controller created
rolebinding.rbac.authorization.k8s.io/argocd-redis created
rolebinding.rbac.authorization.k8s.io/argocd-server created
clusterrolebinding.rbac.authorization.k8s.io/argocd-application-controller created
clusterrolebinding.rbac.authorization.k8s.io/argocd-server created
configmap/argocd-cm created
configmap/argocd-cmd-params-cm created
configmap/argocd-gpg-keys-cm created
configmap/argocd-notifications-cm created
configmap/argocd-rbac-cm created
configmap/argocd-ssh-known-hosts-cm created
configmap/argocd-tls-certs-cm created
secret/argocd-notifications-secret created
secret/argocd-secret created
service/argocd-applicationset-controller created
service/argocd-dex-server created
service/argocd-metrics created
service/argocd-notifications-controller-metrics created
service/argocd-redis created
service/argocd-repo-server created
service/argocd-server created
service/argocd-server-metrics created
deployment.apps/argocd-applicationset-controller created
deployment.apps/argocd-dex-server created
deployment.apps/argocd-notifications-controller created
deployment.apps/argocd-redis created
deployment.apps/argocd-repo-server created
deployment.apps/argocd-server created
statefulset.apps/argocd-application-controller created
networkpolicy.networking.k8s.io/argocd-application-controller-network-policy created
networkpolicy.networking.k8s.io/argocd-applicationset-controller-network-policy created
networkpolicy.networking.k8s.io/argocd-dex-server-network-policy created
networkpolicy.networking.k8s.io/argocd-notifications-controller-network-policy created
networkpolicy.networking.k8s.io/argocd-redis-network-policy created
networkpolicy.networking.k8s.io/argocd-repo-server-network-policy created
networkpolicy.networking.k8s.io/argocd-server-network-policy created

另外,还可以通过 Helm 来部署 Argo CD,这里是社区维护的 Helm Charts

通过 Web UI 访问 Argo CD

Argo CD 部署好之后,默认情况下,API Server 从集群外是无法访问的,这是因为 API Server 的服务类型是 ClusterIP

$ kubectl get svc argocd-server -n argocd
NAME            TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)                      AGE
argocd-server   ClusterIP   10.111.209.6   <none>        80/TCP,443/TCP               23h

我们可以使用 kubectl patch 将其改为 NodePortLoadBalancer

$ kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort"}}'

修改之后,Kubernetes 会为 API Server 随机分配端口:

$ kubectl get svc argocd-server -n argocd
NAME            TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)                      AGE
argocd-server   NodePort   10.111.209.6   <none>        80:32130/TCP,443:31205/TCP   23h

这时我们就可以通过 localhost:32130localhost:31205 来访问 API Server 了:

argocd-login.png

可以看到 API Server 需要登录才能访问,初始用户名为 admin,初始密码在部署时随机生成,并保存在 argocd-initial-admin-secret 这个 Secret 里:

$ kubectl get secrets argocd-initial-admin-secret -n argocd -o yaml
apiVersion: v1
data:
  password: SlRyZDYtdEpOT1JGcXI3QQ==
kind: Secret
metadata:
  creationTimestamp: "2023-05-04T00:14:19Z"
  name: argocd-initial-admin-secret
  namespace: argocd
  resourceVersion: "17363"
  uid: 0cce4b4a-ff9d-44b3-930d-48bc5530bef0
type: Opaque

密码以 BASE64 形式存储,可以使用下面的命令快速得到明文密码:

$ kubectl get secrets argocd-initial-admin-secret -n argocd --template={{.data.password}} | base64 -d
JTrd6-tJNORFqr7A

输入用户名和密码登录成功后,进入 Argo CD 的应用管理页面:

argocd-ui.png

除了修改服务类型,官方还提供了两种方法暴露 API Server:一种是 使用 Ingress 网关,另一种是使用 kubectl port-forward 命令进行端口转发:

$ kubectl port-forward svc argocd-server -n argocd 8080:443

通过 CLI 访问 Argo CD

我们也可以使用命令行客户端来访问 Argo CD,首先使用 curl 命令下载:

$ curl -LO https://github.com/argoproj/argo-cd/releases/download/v2.7.1/argocd-linux-amd64

然后使用 install 命令安装:

$ sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd

使用 argocd version 查看 Argo CD 的版本信息,验证安装是否成功:

$ argocd version
argocd: v2.7.1+5e54351
  BuildDate: 2023-05-02T16:54:25Z
  GitCommit: 5e543518dbdb5384fa61c938ce3e045b4c5be325
  GitTreeState: clean
  GoVersion: go1.19.8
  Compiler: gc
  Platform: linux/amd64
FATA[0000] Argo CD server address unspecified

由于此时还没有配置服务端,所以 argocd version 只显示了 Argo CD 客户端的版本信息,没有服务端的版本信息。通过上一节的方法将 API Server 暴露之后,就可以使用 argocd login 连接服务端:

$ argocd login localhost:32130
WARNING: server certificate had error: x509: certificate signed by unknown authority. Proceed insecurely (y/n)? y
Username: admin
Password:
'admin:login' logged in successfully
Context 'localhost:32130' updated

登录成功后,再次查看版本:

$ argocd version
argocd: v2.7.1+5e54351
  BuildDate: 2023-05-02T16:54:25Z
  GitCommit: 5e543518dbdb5384fa61c938ce3e045b4c5be325
  GitTreeState: clean
  GoVersion: go1.19.8
  Compiler: gc
  Platform: linux/amd64
argocd-server: v2.7.1+5e54351.dirty
  BuildDate: 2023-05-02T16:35:40Z
  GitCommit: 5e543518dbdb5384fa61c938ce3e045b4c5be325
  GitTreeState: dirty
  GoVersion: go1.19.6
  Compiler: gc
  Platform: linux/amd64
  Kustomize Version: v5.0.1 2023-03-14T01:32:48Z
  Helm Version: v3.11.2+g912ebc1
  Kubectl Version: v0.24.2
  Jsonnet Version: v0.19.1

部署应用

这一节我们将学习如何通过 Argo CD 来部署一个 Kubernetes 应用,官方在 argoproj/argocd-example-apps 仓库中提供了很多示例应用可供我们直接使用,这里我们将使用其中的 guestbook 应用。

通过 Web UI 部署应用

最简单的方法是通过 Argo CD 提供的可视化页面 Web UI 来部署应用,打开 Argo CD 的应用管理页面,点击 + NEW APP 按钮,弹出新建应用的对话框:

new-app.png

对话框中的选项比较多,但是我们只需要填写红框部分的内容即可,包括:

  • 通用配置

    • 应用名称:guestbook
    • 项目名称:default
    • 同步策略:Manual
  • 源配置

    • Git 仓库地址:https://github.com/argoproj/argocd-example-apps.git
    • 分支:HEAD
    • 代码路径:guestbook
  • 目标配置

    • 集群地址:https://kubernetes.default.svc
    • 命名空间:default

其他的选项暂时可以不用管,如果想了解具体内容可以参考官方文档 Sync Options,填写完成后,点击 CREATE 按钮即可创建应用:

new-app-not-sync.png

因为刚刚填写的同步策略是手工同步,所以我们能看到应用的状态还是 OutOfSync,点击应用详情:

new-app-not-sync-detail.png

可以看到 Argo CD 已经从 Git 仓库的代码中解析出应用所包含的 Kubernetes 资源了,guestbook 应用包含了一个 Deployment 和 一个 Service,点击 SYNC 按钮触发同步,Deployment 和 Service(以及它们关联的资源)被成功部署到 Kubernetes 集群中:

new-app-sync.png

测试完成后,点击 DELETE 删除应用,该应用关联的资源将会被级联删除。

通过 CLI 部署应用

我们还可以通过 Argo CD 提供的命令行工具来部署应用,经过上一节的步骤,我们已经登录了 API Server,我们只需要执行下面的 argocd app create 命令即可创建 guestbook 应用:

$ argocd app create guestbook \
  --repo https://github.com/argoproj/argocd-example-apps.git \
  --path guestbook \
  --dest-server https://kubernetes.default.svc \
  --dest-namespace default
application 'guestbook' created

也可以使用 YAML 文件声明式地创建 Argo CD 应用:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: guestbook
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/argoproj/argocd-example-apps.git
    targetRevision: HEAD
    path: guestbook
  destination:
    server: https://kubernetes.default.svc
    namespace: guestbook

我们创建一个 YAML 文件,包含以上内容,然后执行 kubectl apply -f 即可,和 Web UI 操作类似,刚创建的应用处于 OutOfSync 状态,我们可以使用 argocd app get 命令查询应用详情进行确认:

$ argocd app get guestbook
Name:               argocd/guestbook
Project:            default
Server:             https://kubernetes.default.svc
Namespace:          default
URL:                https://localhost:32130/applications/guestbook
Repo:               https://github.com/argoproj/argocd-example-apps.git
Target:
Path:               guestbook
SyncWindow:         Sync Allowed
Sync Policy:        <none>
Sync Status:        OutOfSync from  (53e28ff)
Health Status:      Missing

GROUP  KIND        NAMESPACE  NAME          STATUS     HEALTH   HOOK  MESSAGE
       Service     default    guestbook-ui  OutOfSync  Missing
apps   Deployment  default    guestbook-ui  OutOfSync  Missing

接着我们执行 argocd app sync 命令,手工触发同步:

$ argocd app sync guestbook
TIMESTAMP                  GROUP        KIND   NAMESPACE                  NAME    STATUS    HEALTH        HOOK  MESSAGE
2023-05-06T08:22:53+08:00            Service     default          guestbook-ui  OutOfSync  Missing
2023-05-06T08:22:53+08:00   apps  Deployment     default          guestbook-ui  OutOfSync  Missing
2023-05-06T08:22:53+08:00            Service     default          guestbook-ui    Synced  Healthy
2023-05-06T08:22:54+08:00            Service     default          guestbook-ui    Synced   Healthy              service/guestbook-ui created
2023-05-06T08:22:54+08:00   apps  Deployment     default          guestbook-ui  OutOfSync  Missing              deployment.apps/guestbook-ui created
2023-05-06T08:22:54+08:00   apps  Deployment     default          guestbook-ui    Synced  Progressing              deployment.apps/guestbook-ui created

Name:               argocd/guestbook
Project:            default
Server:             https://kubernetes.default.svc
Namespace:          default
URL:                https://localhost:32130/applications/guestbook
Repo:               https://github.com/argoproj/argocd-example-apps.git
Target:
Path:               guestbook
SyncWindow:         Sync Allowed
Sync Policy:        <none>
Sync Status:        Synced to  (53e28ff)
Health Status:      Progressing

Operation:          Sync
Sync Revision:      53e28ff20cc530b9ada2173fbbd64d48338583ba
Phase:              Succeeded
Start:              2023-05-06 08:22:53 +0800 CST
Finished:           2023-05-06 08:22:54 +0800 CST
Duration:           1s
Message:            successfully synced (all tasks run)

GROUP  KIND        NAMESPACE  NAME          STATUS  HEALTH       HOOK  MESSAGE
       Service     default    guestbook-ui  Synced  Healthy            service/guestbook-ui created
apps   Deployment  default    guestbook-ui  Synced  Progressing        deployment.apps/guestbook-ui created

等待一段时间后,应用关联资源就部署完成了:

$ kubectl get all
NAME                               READY   STATUS    RESTARTS   AGE
pod/guestbook-ui-b848d5d9d-rtzwf   1/1     Running   0          67s

NAME                   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/guestbook-ui   ClusterIP   10.107.51.212   <none>        80/TCP    67s
service/kubernetes     ClusterIP   10.96.0.1       <none>        443/TCP   30d

NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/guestbook-ui   1/1     1            1           67s

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/guestbook-ui-b848d5d9d   1         1         1       67s

测试完成后,执行 argocd app delete 命令删除应用,该应用关联的资源将会被级联删除:

$ argocd app delete guestbook
Are you sure you want to delete 'guestbook' and all its resources? [y/n] y
application 'guestbook' deleted

参考

更多

部署其他的应用类型

Argo CD 不仅支持原生的 Kubernetes 配置清单,也支持 Helm ChartKustomize 或者 Jsonnet 部署清单,甚至可以通过 Config Management Plugins 实现自定义配置。

Flux CD

扫描二维码,在手机上阅读!

使用 Google Colab 体验 AI 绘画

AIGC 的全称为 AI Generated Content,是指利用人工智能技术来生成内容,被认为是继 PGC(Professionally Generated Content,专业生成内容)和 UGC(User Generated Content,用户生成内容)之后的一种新型内容创作方式。目前,这种创作方式一般可分为两大派别:一个是以 OpenAIChatGPTGPT-4、Facebook 的 LLaMA、斯坦福的 Alpaca大语言模型 技术为代表的文本生成派,另一个是以 Stability AIStable DiffusionMidjourney、OpenAI 的 DALL·E 2扩散模型 技术为代表的图片生成派。

在文本生成方面,目前 AI 已经可以和用户聊天,回答各种问题,而且可以基于用户的要求进行文本创作,比如写文案、写邮件、写小说等;在图片生成方面,AI 的绘画水平也突飞猛进,目前 AI 已经可以根据用户的提示词创作出各种不同风格的绘画作品,而且可以对图片进行风格迁移、自动上色、缺损修复等,AI 生成的作品几乎可以媲美专业画师,生成作品的效率越来越高,而生成作品的成本却越来越低,这让 AI 绘画技术得以迅速普及,让普通用户也可以体验专业画师的感觉,我从小就很特别羡慕那些会画画的人,现在就可以借助 AI 技术让我实现一个画家的梦。

AI 绘画的发展历史

2014 年 10 月,Ian J. Goodfellow 等人发表了一篇论文 《Generative Adversarial Networks》,在论文中提出了一种新的深度学习算法 GAN(生成式对抗网络),这个算法包含两个模型:生成模型(Generative Model,简称 G 模型)和 判别模型(Discriminative Model,简称 D 模型),在训练过程中,G 模型的目标是尽量生成以假乱真的图片去欺骗 D 模型,而 D 模型的目标是判断 G 模型生成的图片是不是真实的,这样,G 模型和 D 模型就构成了一个动态的博弈过程,仿佛老顽童周伯通的左右手互搏一样,当 D 模型无法判断输入的图片是 G 模型生成的还是真实的时候,G 模型和 D 模型的训练就达到了平衡,这时我们得到的 G 模型就可以生成以假乱真的图片了。

不过由于 GAN 算法包含了两个模型,稳定性较差,可能出现有趣的 海奥维提卡现象(the helvetica scenario),如果 G 模型发现了一个能够骗过 D 模型的 bug,它就会开始偷懒,一直用这张图片来欺骗 D 模型,导致整个平衡的无效。在 2020 年,Jonathan Ho 等人发表论文 《Denoising Diffusion Probabilistic Models》,提出了一种新的 扩散模型(Diffusion Model),相比 GAN 来说,扩散模型的训练更稳定,而且能够生成更多样的样本,一时间扩散模型在 AI 圈里迅速起飞,2021 年 11 月 OpenAI 推出 DALL·E,2022 年 3 月,David Holz 推出 Midjourney,5 月 Google Brain 推出 Imagen,都是基于扩散模型实现的。

到了 2022 年 8 月,Stability AI 开发出 Stable Diffusion 模型,相比于之前的商业产品,Stable Diffusion 是一个完全开源的模型,无论是代码还是权重参数库都对所有人开放使用,而且 Stable Diffusion 对资源的消耗大幅降低,消费级显卡就可以驱动,大大降低了 AI 绘画的门槛,普通人也可以在他们的电脑上体验 AI 绘画的乐趣。到了 10 月,游戏设计师 Jason Allen 使用 AI 绘画工具 Midjourney 生成的一幅名为《太空歌剧院》的作品在美国科罗拉多州举办的艺术博览会上获得数字艺术类冠军,引起了一波不小的争论,也让 AI 绘画再一次成为热门话题,之后各大公司和团队纷纷入局,各种 AI 绘画工具如雨后春笋般冒了出来。

正因为如此,有人将 2022 年称为 AI 绘画元年。

选择 GPU

虽说 Stable Diffusion 的门槛已经被大大降低了,但还是有一定门槛的,因为运行 Stable Diffusion 要配备一张 GPU 显卡,可以使用 NVIDIA 卡(俗称 N 卡)或 AMD 卡(俗称 A 卡),不过主流的推理框架都使用了 NVIDIA 的 CUDA 工具包,所以一般都推荐使用 N 卡。GPU 显卡价格不菲,可以参考驱动之家的 桌面显卡性能天梯图 进行选购,除非你是资深的游戏玩家或者深度学习的爱好者,大多数家用电脑上都不具备这个条件。

也可以使用各大公有云厂商推出的 GPU 云服务器,比如 阿里云腾讯云华为云百度智能云 等,但是价格也都不便宜,比较适合中小企业,对于那些刚对深度学习感兴趣,希望尝试一些深度学习项目的小白个人用户来说,就不划算了。

好在网上有很多 白嫖 GPU 的攻略,国外的有 Google Colab 和 Kaggle,它们都提供了 V100、P100、T4 等主流显卡,可以免费使用 12 个小时,超时之后会自动清理;国内的有阿里的天池,相比来说磁盘和使用时间稍短一点,不过对于新人入门来说也足够了;另外还有百度的 AI Studio 和 趋动云 等产品,它们可以通过打卡做任务等形式赚取 GPU 算力,在 GPU 不够用时不妨一试。下面是网上找的一些使用教程,供参考:

Google Colab 入门

综合对比下来,Google Colab 的使用体验最好,Google Colab 又叫作 Colaboratory,简称 Colab,中文意思是 合作实验室,正如其名,它可以帮助用户在浏览器中编写和执行 Python 代码,无需任何配置就可以进行一些数据科学或机器学习的实验,借助 Jupyter 交互式笔记本,实验过程和结果也可以轻松分享给其他用户。很多开源项目都提供了 Colab 脚本,可以直接运行体验,这一节将以 Colab 为例,介绍它的基本使用方法。

首先在浏览器输入 colab.research.google.com 访问 Colab 首页:

colab-home.png

首页上对 Colab 有个简单的介绍,还提供了一些数据科学和机器学习的入门例子和学习资源。我们通过左上角的 文件 -> 新建笔记本 菜单项创建一个新的笔记本:

new-notebook.png

然后点击 修改 -> 笔记本设置 将硬件加速器调整为 GPU:

notebook-setting.png

然后点击右上角的 连接 按钮,Google 会动态地为我们分配计算资源,稍等片刻,我们就相当于拥有了一台 12.7 G 内存,78.2 G 磁盘,且带 GPU 显卡的主机了:

resource-info.png

Colab 的基本使用

在这个笔记本中,我们可以编写 Markdown 文档,也可以编写和执行 Python 代码:

execute-python-code.png

甚至可以在命令前加个 ! 来执行 Shell 命令:

execute-shell.png

这个环境里内置了很多常用的数据科学或机器学习的 Python 库,比如 numpy、pandas、matplotlib、scikit-learn、tensorflow 等:

pip-list.png

另外,由于这是一台 GPU 主机,我们还可以使用 nvidia-smi 来查看显卡信息:

nvidia-smi.png

可以看到,我们免费得到了一张 Tesla T4 的显卡,显存大约 15G 左右。

测试 GPU 速度

接下来,我们测试下这个 GPU 的速度。首先通过 TensorFlow 的 tf.test.gpu_device_name() 获取 GPU 设备的名称:

gpu-device-name.png

然后编写两个方法:

gpu-vs-cpu.png

这两个方法所做的事情是一样的,只不过一个使用 CPU 来运行,另一个使用 GPU 来运行。在这个方法中,先使用 tf.random.normal((100, 100, 100, 3)) 随机生成一个 100*100*100*3 的四维张量,然后使用 tf.keras.layers.Conv2D(32, 7)(random_image_cpu) 对这个张量计算卷积,卷积过滤器数量 filters 为 32,卷积窗口 kernel_size7*7,最后使用 tf.math.reduce_sum(net_cpu) 对卷积结果求和。

接下来第一次执行,并使用 timeit 来计时:

gpu-vs-cpu-first-run.png

可以看到,在 GPU 上的执行速度比 CPU 上的要慢一点,这是因为 TensorFlow 第一次运行时默认会使用 cuDNN 的 autotune 机制对计算进行预热。

我们再执行第二次:

gpu-vs-cpu-second-run.png

这时,在 GPU 上的执行速度明显快多了,相比于 CPU 来说,速度有着 50 多倍的提升。

这里是 这一节的完整代码

在 Google Colab 里运行 Stable Diffusion

2023 年 4 月 21 日,Google Colab 官方发了一份声明,由于 Stable Diffusion 太火了,消耗了 Google Colab 大量的 GPU 资源,导致预算不够,现在已经被封了,只有付费用户才能运行,免费用户运行会有警告:

colab-warning.png

对 Google Colab 有一定了解后,我们就可以免费使用它的 GPU 来做很多有趣的事情了,比如我想要运行 Stable Diffusion 来体验 AI 绘画。

camenduru/stable-diffusion-webui-colab 这个项目整理了大量 Stable Diffusion 的 Colab 脚本,基于 AUTOMATIC1111/stable-diffusion-webui 实现了可视化 Web 页面,集成了 Hugging FaceCivitai 上热门的模型和插件,我们随便选择一个,点击左侧的 stable 打开 Colab 页面执行即可:

sd-webui-colab.png

运行成功后,在控制台中可以看到打印了几个随机生成的外网链接:

running.png

随便选择一个链接打开,进入 Stable Diffusion WebUI 页面:

webui.png

接下来,开始你的 AI 绘画之旅吧!

参考

更多

文本生成派

图片生成派

扫描二维码,在手机上阅读!

使用 RSSHub 为任意网址生成订阅源

最近在学习 APISIX 时,发现它的 官方博客 有不少的干货内容,于是想着能在我的阅读器里订阅这个博客的更新,不过找了半天都没有找到这个博客的订阅入口,后来在博客的页面代码里找到了 rss.xml 和 atom.xml 两个订阅链接,不过打开一看全都是 404 Page Not Found

其实遇到这种情况,也有不少的解决方法,有很多网站提供了 RSS 生成的功能,比如 RSS.appFetchRSSfeed43 等都提供了免费的 RSS 源转换功能,不过这些工具要么使用起来不太好用,要么访问速度巨慢,要么就是有各种各样的限制。于是便想实现一个自己的 RSS 生成服务,正好前几天看到了一个叫做 RSSHub 的项目,这是一个开源、简单易用、易于扩展的 RSS 生成器,口号是 万物皆可 RSS,可以给任何奇奇怪怪的内容生成 RSS 订阅源,而且看社区也挺活跃,于是就利用周末时间折腾一下,使用 RSSHub 搭建了一个自己的 RSS 生成服务。

快速开始

RSSHub 和那些在线的 RSS 生成服务不一样,它是通过编写扩展的方式来添加新的 RSS 订阅源。不过在编写自己的扩展之前,可以先到官网上搜索一下,看看有没有其他人已经写过了,官网上目前已经适配了数百家网站的上千项内容。由于我要订阅的 APSIX 博客比较小众,目前还没有人写过,所以就只能自己动手了。

RSSHub 是基于 Node.js 实现的,所以先确保机器上已经安装了 Node.js 运行环境:

$ node -v
v16.14.2

以及包管理器 Npm 或 Yarn,我这里使用的是 Npm:

$ npm -v
8.5.0

然后,下载 RSSHub 的源码:

$ git clone https://github.com/DIYgod/RSSHub.git

进入 RSSHub 的根目录,运行以下命令安装依赖:

$ cd RSSHub
$ npm install

依赖安装成功后,运行以下命令在本地启动 RSSHub:

$ npm run dev

启动成功后,在浏览器中打开 http://localhost:1200 就可以看到 RSSHub 的首页了:

rsshub-homepage.png

新建路由

此时 RSSHub 内置的上千个订阅源,都可以在本地访问,比如通过 /bilibili/ranking/0/3/1 这个地址可以订阅 B 站三天内的排行榜。这个订阅源的格式一般分为三个部分:

/命名空间/路由/参数

新建命名空间

命名空间应该和 RSS 源网站的二级域名相同,所以 B 站的命名空间为 bilibili,而我们要新建的 APISIX 博客地址为 apisix.apache.org/zh/blog,所以命名空间应该为 apache

每个命名空间对应 lib/v2 目录下的一个子文件夹,所以我们在这个目录下创建一个 apache 子文件夹:

$ mkdir lib/v2/apache

注册路由

第二步,我们需要在命名空间子文件夹下按照 RSSHub 的 路由规范 来组织文件,一个典型的文件夹结构如下:

├───lib/v2
│   ├───furstar
│       ├─── templates
│           ├─── description.art
│       ├─── router.js
│       ├─── maintainer.js
│       ├─── radar.js
│       └─── someOtherJs.js

其中,每个文件的作用如下:

  • router.js - 注册路由
  • maintainer.js - 提供路由维护者信息
  • radar.js - 为每个路由提供对应 RSSHub Radar 规则
  • someOtherJs.js - 一些其他的代码文件,一般用于实现路由规则
  • templates - 该目录下是以 .art 结尾的模版文件,它使用 art-template 进行排版,用于渲染自定义 HTML 内容

编写 router.js 文件

其中最重要的一个文件是 router.js,它用于注册路由信息,我们创建该文件,内容如下:

module.exports = (router) => {
    router.get('/apisix/blog', require('./apisix/blog'));
};

RSSHub 使用 @koa/router 来定义路由,在上面的代码中,我们通过 router.get() 定义了一个 HTTP GET 的路由,第一个参数是路由路径,它需要符合 path-to-regexp 语法,第二个参数指定由哪个文件来实现路由规则。

在路由路径中,我们还可以使用参数,比如上面 bilibili 的路由如下:

router.get('/ranking/:rid?/:day?/:arc_type?/:disableEmbed?', require('./ranking'));

其中 :rid:days:arc_type:disableEmbed 都是路由的参数,每个参数后面的 ? 表示这是一个可选参数。路由参数可以从 ctx.params 对象中获取。

编写 maintainer.js 文件

maintainer.js 文件用于提供路由维护者信息,当用户遇到 RSS 路由的问题时,他们可以联系此文件中列出的维护者:

module.exports = {
    '/apisix/blog': ['aneasystone'],
};

编写路由规则

接下来我们就可以实现路由规则了。首先我们需要访问指定网址来获取数据,RSSHub 提供了两种方式来获取数据:

  • 对于一些简单的 API 接口或网页,可以直接使用 got 发送 HTTP 请求获取数据;
  • 对于某些反爬策略很严的网页,可能需要使用 puppeteer 模拟浏览器打开网页来获取数据。

这其实就是爬虫技术,我们获取的数据通常是 JSON 或 HTML 格式,如果是 HTML 格式,RSSHub 提供了 cheerio 方便我们进一步处理。

上面在注册路由时我们指定了路由规则文件为 ./apisix/blog,所以接下来,创建 ./apisix/blog.js 文件。路由规则实际上就是生成 ctx.state.data 对象,这个对象包含三个字段:

  • title - 源标题
  • link - 源链接
  • item - 源文章

我们先编写一个最简单的路由规则,文件内容如下:

module.exports = async (ctx) => {
    ctx.state.data = {
        title: `Blog | Apache APISIX`,
        link: `https://apisix.apache.org/zh/blog/`,
        item: [{}],
    };
};

这时虽然源文章列表还是空的,但是我们已经可以通过 http://localhost:1200/apache/apisix/blog 地址来访问我们创建的 RSS 源了:

apache-apisix-blog-rss.png

只不过源文章中的 titledescription 都是 undefined

接下来要做的事情就是如何获取源文章了,很显然,我们需要访问 APISIX 的博客,并从页面 HTML 中解析出源文章。首先使用 got 发送 HTTP 请求获取页面 HTML:

const url = 'https://apisix.apache.org/zh/blog/';
const { data: res } = await got(url);

得到 HTML 之后,使用 cheerio 对其进行解析:

const $ = cheerio.load(res);

cheerio 有点类似于 jQuery,可以通过 CSS 选择器对 HTML 进行解析和提取,我们可以很方便地在页面中提取出源文章列表:

const articles = $('section.sec_gjjg').eq(1).find('article');
const results = [];
articles.each((i, elem) => {
    const a = $(elem).find('header > a');
    const time = $(elem).find('footer').find('time').text();
    const author = $(elem).find('footer').find('img').attr('src');
    results.push({
        title: a.find('h2').text(),
        description: a.find('p').text(),
        link: a.attr('href'),
        pubDate: timezone(parseDate(time, 'YYYY年M月D日'), +8),
        author,
    });
});
return results;

每个源文章包含以下几个字段:

  • title - 文章标题
  • link - 文章链接
  • description - 文章正文
  • pubDate - 文章发布日期
  • author - 文章作者(可选)
  • category - 文章分类(可选)

至此,我们的路由规则就创建好了,可以在浏览器中对我们的路由进行验证和调试。我们这里的路由规则比较简单,稍微复杂一点的例子可以参考 RSSHub 官方文档 制作自己的 RSSHub 路由使用缓存日期处理。另外,lib/v2 目录下有很多其他人编写的路由规则,也是很好的参考资料。

其他工作

实现自己的订阅源之后,还可以编写 radar.js 文件,为每个路由提供对应 RSSHub Radar 规则。RSSHub Radar 是 RSSHub 的一款浏览器插件,方便用户查找某个网站是否存在 RSS 订阅源。最后为你的路由 添加相应的文档,一个订阅源就开发完成了。

不过如果只是自己部署使用,这些工作也可以跳过。

部署

最后,将 RSSHub 部署到自己的服务器上,官方提供了几种 部署方式,比较推荐的是 Docker 或 Docker Compose 部署。

我这里使用 Docker 来简化部署流程。由于我希望将 Redis 作为我的 RSSHub 缓存,这样可以保证每次重启 RSSHub 之后缓存不会失效。首先启动一个 Redis 实例:

$ docker run -d -p 6379:6379 redis:alpine

然后启动 RSSHub 即可:

$ docker run --name rsshub \
    -d -p 1200:1200 \
    -e NODE_ENV=production \
    -e CACHE_TYPE=redis \
    -e REDIS_URL=redis://172.18.0.1:6379/ \
    -v /root/rsshub/v2:/app/lib/v2 \
    diygod/rsshub

注意我们将 lib/v2 目录挂载进容器,这样才能让我们的订阅源生效。我制作了三个 RSS 订阅源,有需要的小伙伴可以自取:

参考

扫描二维码,在手机上阅读!

使用 Istio 和 Envoy 打造 Service Mesh 微服务架构

周志明 老师在他的 《凤凰架构》 中将分布式服务通信的演化历史分成五个阶段:

  • 第一阶段:将通信的非功能性需求视作业务需求的一部分,通信的可靠性由程序员来保障

stage-1.png

这个阶段是分布式系统发展最早期时的技术策略,这些系统一开始仅仅是通过 RPC 技术去访问远程服务,当遇到通信问题时,就在业务代码中引入相应的处理逻辑,比如服务发现、网络重试或降级等。这些通信的非功能性逻辑和业务逻辑耦合在一起,让系统变得越来越复杂。

  • 第二阶段:将代码中的通信功能抽离重构成公共组件库,通信的可靠性由专业的平台程序员来保障

stage-2.png

这个阶段人们逐渐意识到通信功能应该从业务逻辑中抽离出来,于是形成了一些公共组件库,比如 FinagleSpring Cloud 等。不过这些组件库大多是和语言绑定的,比如 Spring Cloud 技术栈只能用在 Java 项目中,遇到其他语言的项目就无能为力了。

  • 第三阶段:将负责通信的公共组件库分离到进程之外,程序间通过网络代理来交互,通信的可靠性由专门的网络代理提供商来保障

stage-3.png

为了将通信功能做成语言无关的,这个阶段发展出了专门负责可靠通信的网络代理,比如 Netflix Prana。业务系统和这类代理之间通过回环设备或 Unix 套接字进行通信,网络代理对业务系统的网络流量进行拦截,从而在代理上完成流控、重试或降级等几乎所有的分布式通信功能。

这种网络代理后来演化出了两种形态:一种是微服务网关,它位于整个分布式系统的入口,同时代理整个系统中所有服务的网络流量;另一种是边车代理,它和某个进程共享一个网络命名空间,专门针对性地代理该服务的网络流量。

  • 第四阶段:将网络代理以边车的形式注入到应用容器,自动劫持应用的网络流量,通信的可靠性由专门的通信基础设施来保障

stage-4.png

在使用上一阶段的网络代理时,我们必须在应用程序中手工指定网络代理的地址才能生效,而边车代理有一个很大的优势,它通过 iptables 等技术自动劫持代理进程的网络流量,所以它对应用是完全透明的,无需对应用程序做任何改动就可以增强应用的通信功能。目前边车代理的代表性产品有 LinkerdEnvoyMOSN 等。不过,边车代理也有一个很大的缺点,随着系统中代理服务的增多,对边车代理的维护和管理工作就成了运维最头疼的问题,于是服务网格应运而生。

  • 第五阶段:将边车代理统一管控起来实现安全、可控、可观测的通信,将数据平面与控制平面分离开来,实现通用、透明的通信,这项工作就由专门的服务网格框架来保障

服务网格(Service Mesh)一词是 Buoyant 公司的 CEO William Morgan 于 2017 年在他的一篇博客 《What's a service mesh? And why do I need one?》 中首次提出的,他是世界上第一款服务网格产品 Linkerd 的创始人之一,在博客中,William Morgan 对服务网格做了如下定义:

服务网格是一种用于管控服务间通信的的基础设施,它负责为现代云原生应用程序在复杂服务拓扑中可靠地传递请求。在实践中,服务网格通常以轻量级网络代理阵列的形式实现,这些代理与应用程序部署在一起,对应用程序来说无需感知代理的存在。

服务网格将上一阶段的边车代理联合起来形成如下所示的网格状结构:

stage-5.png

在服务网格中,主要由数据平面与控制平面组成。数据平面是所有边车代理的集合,负责拦截所有服务流入和流出的流量,并配置控制平面对流量进行管理;控制平面对数据平面进行管理,完成配置分发、服务发现、授权鉴权等功能。如上图所示,整个系统中的通信包括两个部分:实线表示数据平面之间的通信,虚线表示控制平面和数据平面之间的通信。

服务网格的概念一经提出,其价值迅速被业界所认可,业界几乎所有的云原生玩家都积极参与了进来:

  • 2016 年,Buoyant 公司 推出 Linkerd,同年,Lyft 公司 推出 Envoy
  • 2017 年,Linkerd 加入 CNCF,同年,Google、IBM 和 Lyft 共同发布 Istio,为了和 Istio 展开竞争,Buoyant 公司将自家的 Conduit 产品合并到 Linkerd 中发布了 Linkerd 2
  • 2018 年后,Google、亚马逊、微软分别推出各自的公有云版本 Service Mesh 产品,国内的阿里巴巴也推出了基于 Istio 的修改版 SOFAMesh(目前已经废弃),并开源了自己研发的 MOSN 代理

随着各巨头的参与,Istio 逐渐超过了 Linkerd 的地位,几乎成了原云生环境下服务网格中控制平面的事实标准,而 Envoy 凭借其卓越的性能和强大的动态配置功能,成为了服务网格中数据平面的不二选择,下图是使用 Istio 作为服务网格方案后的典型架构:

istio-envoy.png

Istio 服务网格的数据平面由 Envoy 代理实现,在 Envoy 学习笔记 中我们学习过 Envoy 的基本用法,这是一个用 C++ 开发的高性能代理,用于协调和控制微服务之间的网络流量,并为这些服务透明地提供了许多 Envoy 内置的功能特性:

  • 动态服务发现
  • 负载均衡
  • TLS 终端
  • HTTP/2 与 gRPC 代理
  • 熔断器
  • 健康检查
  • 基于百分比流量分割的分阶段发布
  • 故障注入
  • 丰富的指标

不仅如此,这些服务同时还具备了 Istio 所提供的功能特性:

  • 流量控制特性:通过丰富的 HTTP、gRPC、WebSocket 和 TCP 流量路由规则来执行细粒度的流量控制;
  • 网络弹性特性:重试设置、故障转移、熔断器和故障注入;
  • 安全性和身份认证特性:执行安全性策略,并强制实行通过配置 API 定义的访问控制和速率限制;
  • 基于 WebAssembly 的可插拔扩展模型,允许通过自定义策略执行和生成网格流量的遥测。

Istio 服务网格的控制平面由 Istiod 实现,它提供了诸如服务发现(Pilot),配置(Galley),证书生成(Citadel)和可扩展性(Mixer)等功能;它通过 Envoy API 实现了对数据平面的管理,所以 Istio 的数据平面并不仅限于 Envoy,其他符合 Envoy API 规范的代理都可以作为 Istio 的数据平面。

快速开始

这篇笔记将以 Istio 的官方示例来学习如何使用 Istio 和 Envoy 打造一个基于服务网格的微服务架构。

安装 Istio

首先从 Istio 的 Release 页面 找到最新版本的安装包并下载和解压:

$ curl -LO https://github.com/istio/istio/releases/download/1.17.1/istio-1.17.1-linux-amd64.tar.gz
$ tar zxvf istio-1.17.1-linux-amd64.tar.gz

解压后的目录中包含几个文件和目录:

$ tree -L 1 istio-1.17.1
istio-1.17.1
├── LICENSE
├── README.md
├── bin
├── manifest.yaml
├── manifests
├── samples
└── tools

4 directories, 3 files

其中,bin 目录下包含了 istioctl 可执行文件,我们可以将这个目录添加到 PATH 环境变量,配置之后,就可以使用 istioctl install 命令安装 Istio 了:

$ istioctl install --set profile=demo
This will install the Istio 1.17.1 demo profile with ["Istio core" "Istiod" "Ingress gateways" "Egress gateways"] components into the cluster. Proceed? (y/N) y
✔ Istio core installed
✔ Istiod installed
✔ Ingress gateways installed
✔ Egress gateways installed
✔ Installation complete

Istio 内置了几种 不同的安装配置

$ istioctl profile list
Istio configuration profiles:
    ambient
    default
    demo
    empty
    external
    minimal
    openshift
    preview
    remote

所有的配置都位于 manifests/profiles 目录下,你也可以 定制自己的配置。这里为了演示和体验,我们使用了 demo 配置,它相对于默认的 default 配置来说,开启了一些用于演示的特性,日志级别也比较高,性能会有一定影响,所以不推荐在生产环境使用。这些配置通过统一的 IstioOperator API 来定义,我们可以通过 istioctl profile dump 查看配置详情:

$ istioctl profile dump demo

或者通过 istioctl profile diff 对比两个配置之间的差异:

$ istioctl profile diff default demo

执行 istioctl 命令时会创建了一个名为 installed-state 的自定义资源,内容就是上面所看到的 demo 配置:

$ kubectl get IstioOperator installed-state -n istio-system -o yaml

从安装的输出结果可以看到,demo 配置安装内容包括下面四个部分:

  • Istio core
  • Istiod
  • Ingress gateways
  • Egress gateways

可以使用 kubectl get deployments 来看看 Istio 具体安装了哪些服务:

$ kubectl get deployments -n istio-system
NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
istio-egressgateway    1/1     1            1           20m
istio-ingressgateway   1/1     1            1           20m
istiod                 1/1     1            1           21m

除此之外,istioctl 命令还生成了很多其他的 Kubernetes 资源,我们可以使用 istioctl manifest generate 生成最原始的 Kubernetes YAML 定义:

$ istioctl manifest generate --set profile=demo > demo.yaml

生成上面的 YAML 后,我们甚至可以通过执行 kubectl apply -f 来安装 Istio,不过这种安装方式有很多 需要注意的地方,官方并不推荐这种做法,但这也不失为一种深入了解 Istio 原理的好方法。

除了 使用 Istioctl 安装,官方还提供了很多 其他的安装方式,比如 使用 Helm 安装虚拟机安装使用 Istio Operator 安装 等,各个云平台 也对 Istio 提供了支持。

如果要卸载 Istio,可以执行 istioctl uninstall 命令:

$ istioctl uninstall -y --purge

一个简单的例子

为了充分发挥 Istio 的所有特性,网格中的每个应用服务都必须有一个边车代理,这个边车代理可以拦截应用服务的所有出入流量,这样我们就可以利用 Istio 控制平面为应用提供服务发现、限流、可观测性等功能了。

边车代理的注入 一般有两种方法:自动注入和手动注入。上面在解压 Istio 安装包时,可以看到有一个 samples 目录,这个目录里包含了一些官方的示例程序,用于体验 Istio 的不同功能特性,其中 samples/sleep 目录下是一个简单的 sleep 应用,这一节就使用这个简单的例子来演示如何在我们的应用服务中注入一个边车代理。

首先,执行下面的命令部署 sleep 应用:

$ kubectl apply -f samples/sleep/sleep.yaml
serviceaccount/sleep created
service/sleep created
deployment.apps/sleep created

默认情况下,一个 Pod 里只有一个容器(从下面的 READY 字段是 1/1 可以看出来):

$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
sleep-78ff5975c6-hw2lv   1/1     Running   0          23s

接下来,我们给 default 命名空间打上 istio-injection=enabled 标签:

$ kubectl label namespace default istio-injection=enabled
namespace/default labeled

这个标签可以让 Istio 部署应用时自动注入 Envoy 边车代理,这个过程是在 Pod 创建时自动完成的,Istio 使用了 Kubernetes 的 准入控制器(Admission Controllers),通过 MutatingAdmissionWebhook 可以对创建的 Pod 进行修改,从而将边车代理容器注入到原始的 Pod 定义中。

我们将刚刚的 Pod 删除,这样 Deployment 会尝试创建一个新的 Pod,从而触发自动注入的过程:

$ kubectl delete pod sleep-78ff5975c6-hw2lv
pod "sleep-78ff5975c6-hw2lv" deleted

再次查看 Pod 列表,READY 字段已经变成了 2/2,说明每个 Pod 里变成了两个容器:

$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
sleep-78ff5975c6-v6qg4   2/2     Running   0          12s

也可以使用 kubectl describe 查看 Pod 详情:

$ kubectl describe pod sleep-78ff5975c6-v6qg4
Name:             sleep-78ff5975c6-v6qg4
Namespace:        default
Priority:         0
Service Account:  sleep
Init Containers:
  istio-init:
    Container ID:  docker://c724b0c29ffd828e497cfdff45706061855b1b8c93073f9f037acc112367bceb
    Image:         docker.io/istio/proxyv2:1.17.1
Containers:
  sleep:
    Container ID:  docker://44f35a4934841c5618eb68fb5615d75ea5dd9c5dd826cb6a99a6ded6efaa6707
    Image:         curlimages/curl
  istio-proxy:
    Container ID:  docker://10a9ff35f45c7f07c1fcf88d4f8daa76282d09ad96912e026e59a4d0a99f02cf
    Image:         docker.io/istio/proxyv2:1.17.1

Events:
  Type     Reason     Age    From               Message
  ----     ------     ----   ----               -------
  Normal   Created    4m22s  kubelet            Created container istio-init
  Normal   Started    4m21s  kubelet            Started container istio-init
  Normal   Created    4m20s  kubelet            Created container sleep
  Normal   Started    4m20s  kubelet            Started container sleep
  Normal   Created    4m20s  kubelet            Created container istio-proxy
  Normal   Started    4m20s  kubelet            Started container istio-proxy

可以看到除了原始的 sleep 容器,多了一个 istio-proxy 容器,这就是边车代理,另外还多了一个 istio-init 初始化容器,它使用 iptables 将网络流量自动转发到边车代理,对应用程序完全透明。

另一种方法是手工注入边车代理,先将 default 命名空间的 istio-injection 标签移除:

$ kubectl label namespace default istio-injection-
namespace/default unlabeled

同时删除 sleep 应用:

$ kubectl delete -f samples/sleep/sleep.yaml
serviceaccount "sleep" deleted
service "sleep" deleted
deployment.apps "sleep" deleted

然后再重新部署 sleep 应用,并使用 istioctl kube-inject 命令手工注入边车代理:

$ istioctl kube-inject -f samples/sleep/sleep.yaml | kubectl apply -f -
serviceaccount/sleep created
service/sleep created
deployment.apps/sleep created

默认情况下,Istio 使用集群中的默认配置模版来生成边车配置,这个默认配置模版保存在名为 istio-sidecar-injector 的 ConfigMap 中:

# 配置模版
$ kubectl -n istio-system get configmap istio-sidecar-injector -o=jsonpath='{.data.config}'

# 配置值
$ kubectl -n istio-system get configmap istio-sidecar-injector -o=jsonpath='{.data.values}'

除了官方提供的默认模版,还可以通过在 Pod 中添加一个 istio-proxy 容器来自定义注入内容,或者通过 inject.istio.io/templates 注解来设置自定义模版,更多内容可以 参考官方文档

体验 Bookinfo 示例应用

这一节我们来部署一个更复杂的例子,在 samples/bookinfo 目录下是名为 Bookinfo 的示例应用,我们就使用这个应用来体验 Istio 的功能。为了方便起见,我们还是给 default 命名空间打上 istio-injection=enabled 标签开启自动注入功能,然后,执行下面的命令部署 Bookinfo 示例应用:

$ kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml
service/details created
serviceaccount/bookinfo-details created
deployment.apps/details-v1 created
service/ratings created
serviceaccount/bookinfo-ratings created
deployment.apps/ratings-v1 created
service/reviews created
serviceaccount/bookinfo-reviews created
deployment.apps/reviews-v1 created
deployment.apps/reviews-v2 created
deployment.apps/reviews-v3 created
service/productpage created
serviceaccount/bookinfo-productpage created
deployment.apps/productpage-v1 created

这是一个简单的在线书店应用,包含了四个微服务,这些微服务由不同的语言编写:

  • productpage - 产品页面,它会调用 detailsreviews 两个服务;
  • details - 书籍详情服务;
  • reviews - 书籍评论服务,它有三个不同的版本,v1 版本不包含评价信息,v2 和 v3 版本会调用 ratings 服务获取书籍评价,不过 v2 显示的是黑色星星,v3 显示的是红色星星;
  • ratings - 书籍评价服务;

部署时 Istio 会对每个微服务自动注入 Envoy 边车代理,我们可以通过 kubectl get pods 命令进行确认,确保每个 Pod 里都有两个容器在运行,其中一个是真实服务,另一个是代理服务:

$ kubectl get pods
NAME                             READY   STATUS    RESTARTS   AGE
details-v1-5ffd6b64f7-r6pr5      2/2     Running   0          14m
productpage-v1-979d4d9fc-gdmzh   2/2     Running   0          14m
ratings-v1-5f9699cfdf-trqj6      2/2     Running   0          14m
reviews-v1-569db879f5-gd9st      2/2     Running   0          14m
reviews-v2-65c4dc6fdc-5lph4      2/2     Running   0          14m
reviews-v3-c9c4fb987-kzjfk       2/2     Running   0          14m

部署之后整个系统的架构图如下所示:

bookinfo.png

这个时候从外部还不能访问该服务,只能在集群内访问,进入 ratings 服务所在的 Pod,验证 productpage 服务能否正常访问:

$ kubectl exec -it ratings-v1-5f9699cfdf-trqj6 -- sh
$ curl -s productpage:9080/productpage | grep "<title>"
    <title>Simple Bookstore App</title>

为了能从外部访问该服务,我们需要创建一个入站网关:

$ kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml
gateway.networking.istio.io/bookinfo-gateway created
virtualservice.networking.istio.io/bookinfo created

这个 YAML 中包含两个部分,第一部分定义了名为 bookinfo-gateway网关(Gateway)

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: bookinfo-gateway
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"

注意这里使用了 istio-ingressgateway 作为网关,这在我们安装 Istio 的时候已经自动安装好了。

第二部分定义了名为 bookinfo虚拟服务(Virtual Service)

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: bookinfo
spec:
  hosts:
  - "*"
  gateways:
  - bookinfo-gateway
  http:
  - match:
    - uri:
        exact: /productpage
    - uri:
        prefix: /static
    - uri:
        exact: /login
    - uri:
        exact: /logout
    - uri:
        prefix: /api/v1/products
    route:
    - destination:
        host: productpage
        port:
          number: 9080

将这个虚拟服务和上面的网关关联起来,这样做的好处是,我们可以像管理网格中其他数据平面的流量一样去管理网关流量。

有了这个网关后,我们就可以在浏览器中输入 http://localhost/productpage 访问我们的在线书店了:

bookinfo-productpage.png

因为这里我们部署了三个版本的 reviews 服务,所以多刷新几次页面,可以看到页面上会随机展示 reviews 服务的不同版本的效果(可能不显示星星,也可能显示红色或黑色的星星)。

配置请求路径

由于部署了三个版本的 reviews 服务,所以每次访问应用页面看到的效果都不一样,如果我们想访问固定的版本,该怎么做呢?

检查上面的 bookinfo.yaml 文件,之所以每次访问的效果不一样,是因为 reviews 服务的 Service 使用了 app=reviews 选择器,所以请求会负载到所有打了 app=reviews 标签的 Pod:

apiVersion: v1
kind: Service
metadata:
  name: reviews
  labels:
    app: reviews
    service: reviews
spec:
  ports:
  - port: 9080
    name: http
  selector:
    app: reviews

reviews-v1reviews-v2reviews-v3 三个版本的 Deployment 都使用了 app=reviews 标签:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: reviews-v1
  labels:
    app: reviews
    version: v1
spec:
  ...
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: reviews-v2
  labels:
    app: reviews
    version: v2
spec:
  ...
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: reviews-v3
  labels:
    app: reviews
    version: v3
spec:
  ...

所以请求会负载到所有版本的 reviews 服务,这是 Kubernetes Service 的默认行为。不过要知道,现在所有 Pod 的流量都在我们的边车代理管控之下,我们当然可以使用 Istio 来改变请求的路径。首先,我们注意到这三个版本的 reviews 服务虽然 app=reviews 标签都一样,但是 version 标签是不一样的,这个特点可以用来定义 reviews目标规则(Destination Rules)

$ kubectl apply -f samples/bookinfo/networking/destination-rule-reviews.yaml
destinationrule.networking.istio.io/reviews created

destination-rule-reviews.yaml 文件内容如下:

apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: reviews
spec:
  host: reviews
  trafficPolicy:
    loadBalancer:
      simple: RANDOM
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
  - name: v3
    labels:
      version: v3

在 Istio 里,目标规则和虚拟服务一样,是一个非常重要的概念。在目标规则中可以定义负载策略,并根据标签来将应用划分成不同的服务子集(subset),在上面的例子中,我们将 reviews 服务划分成了 v1v2v3 三个服务子集。接下来再为 reviews 定义一个虚拟服务:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-v3.yaml
virtualservice.networking.istio.io/reviews created

virtual-service-reviews-v3.yaml 文件内容如下:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v3

在这个虚拟服务中我们指定了 v3 服务子集,稍等片刻,再次刷新页面,可以发现每次显示的都是红色的星星,我们成功的通过 Istio 控制了服务之间的流量。

基于匹配条件的路由

虚拟服务如果只能指定固定的服务子集,那么和 Kubernetes Service 也就没什么区别了。之所以在 Istio 引入虚拟服务的概念,就是为了 将客户端请求的目标地址与真实响应请求的目标工作负载进行解耦,这使得 A/B 测试很容易实现,比如我们可以让指定用户路由到特定的服务子集:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-jason-v2-v3.yaml
virtualservice.networking.istio.io/reviews configured

virtual-service-reviews-jason-v2-v3.yaml 文件内容如下:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
  - reviews
  http:
  - match:
    - headers:
        end-user:
          exact: jason
    route:
    - destination:
        host: reviews
        subset: v2
  - route:
    - destination:
        host: reviews
        subset: v3

在这个虚拟服务中,不仅定义了一个默认路由,目标是 reviews 的 v3 服务子集,而且还定义了一个目标是 v2 服务子集的路由,但是这个路由必须满足条件 headers.end-user.exact = jason,也就是 HTTP 请求头中必须包含值为 jasonend-user 字段,为了实现这一点,我们使用 jason 用户登录即可,这时看到的书籍评价就是黑色星星:

bookinfo-productpage-jason.png

退出登录后,看到的仍然是红色的星星,这样我们就实现了 A/B 测试的功能。

除了将 headers 作为匹配条件,我们还可以使用 urimethodqueryParams 等参数,具体内容可参考 HTTPMatchRequest

基于权重的路由

在上面的例子中我们实现了指定用户的 A/B 测试,一般来说,我们的服务上线后,先通过指定的测试账号进行验证,验证没有问题后就可以开始迁移流量了,我们可以先迁移 10% 的流量到新版本:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-90-10.yaml
virtualservice.networking.istio.io/reviews configured

virtual-service-reviews-90-10.yaml 文件内容如下:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: reviews
spec:
  hosts:
    - reviews
  http:
  - route:
    - destination:
        host: reviews
        subset: v1
      weight: 90
    - destination:
        host: reviews
        subset: v2
      weight: 10

在这个虚拟服务中,通过 weight 参数指定不同服务子集的权重。如果运行一段时间后没有问题,我们可以继续迁移 20% 的流量到新版本:

$ kubectl apply -f samples/bookinfo/networking/virtual-service-reviews-80-20.yaml
virtualservice.networking.istio.io/reviews configured

然后继续迁移 50%,80%,直到 100% 的流量,至此服务升级就完成了,这种升级方式也被称为 金丝雀部署

其他流量管理功能

上面演示了如何使用 Istio 对不同版本的微服务进行路由配置,这是 Istio 流量管理的基本功能。除此之外,Istio 还提供了很多其他的 流量管理功能

可观测性

可观测性是服务网格的另一个重要特性,Istio 对很多开源组件提供了集成,比如使用 Prometheus 收集指标,使用 GrafanaKiali 对指标进行可视化,使用 JaegerZipkinApache SkyWalking 进行分布式追踪。

samples/addons 目录下提供了 Prometheus、Grafana、Kiali 和 Jaeger 这几个组件的精简版本,我们通过下面的命令安装:

$ kubectl apply -f samples/addons
serviceaccount/grafana created
configmap/grafana created
service/grafana created
deployment.apps/grafana created
configmap/istio-grafana-dashboards created
configmap/istio-services-grafana-dashboards created
deployment.apps/jaeger created
service/tracing created
service/zipkin created
service/jaeger-collector created
serviceaccount/kiali created
configmap/kiali created
clusterrole.rbac.authorization.k8s.io/kiali-viewer created
clusterrole.rbac.authorization.k8s.io/kiali created
clusterrolebinding.rbac.authorization.k8s.io/kiali created
role.rbac.authorization.k8s.io/kiali-controlplane created
rolebinding.rbac.authorization.k8s.io/kiali-controlplane created
service/kiali created
deployment.apps/kiali created
serviceaccount/prometheus created
configmap/prometheus created
clusterrole.rbac.authorization.k8s.io/prometheus created
clusterrolebinding.rbac.authorization.k8s.io/prometheus created
service/prometheus created
deployment.apps/prometheus created

安装完成后,使用下面的命令打开 Prometheus UI:

$ istioctl dashboard prometheus

在 Expression 对话框中输入指标名称即可查询相应的指标,比如查询 istio_requests_total 指标:

prometheus.png

使用下面的命令打开 Grafana UI:

$ istioctl dashboard grafana

Istio 内置了几个常用的 Grafana Dashboard 可以直接使用:

grafana.png

比如查看 Istio Control Plane Dashboard,这里显示的是控制平面 Istiod 的一些指标图:

grafana-ctl-plane-dashboard.png

要注意的是 Istio 默认的采样率为 1%,所以这时去查看 Istio Mesh Dashboard 还看不到数据,可以通过下面的脚本模拟 100 次请求:

$ for i in `seq 1 100`; do curl -s -o /dev/null http://localhost/productpage; done

使用下面的命令打开 Kiali UI:

$ istioctl dashboard kiali

选择左侧的 Graph 菜单,然后在 Namespace 下拉列表中选择 default,就可以看到 Bookinfo 示例应用的整体概览,以及各个服务之间的调用关系:

kiali.png

最后,使用下面的命令打开 Jaeger UI:

$ istioctl dashboard jaeger

从仪表盘左边面板的 Service 下拉列表中选择 productpage.default,并点击 Find Traces 按钮查询该服务的所有 Trace,随便点击一个 Trace,查看它的详细信息:

jaeger.png

上面对几种常见的可观测性组件做了一个大概的介绍,如果想进一步学习各个组件的高级特性,可以参考 Istio 的官方文档 以及各个组件的官方文档。

参考

更多

其他官方学习文档

Kubernetes Gateway API

在最早的版本中,Istio 使用 Kubernetes 提供的 Ingress API 来进行流量管理,但是这个 API 在管理大型应用系统或非 HTTP 协议服务时存在一定的缺点和限制,Istio 在 2018 年 推出了新的流量管理 API v1alpha3,在新的 API 中定义了下面这些资源:

  • Gateway
  • VirtualService
  • DestinationRule
  • ServiceEntry

关于这几个概念的意义,可以通过 Istio 的 官方文档 来学习。

2022 年 7 月,Kubernetes 发布了 Gateway API 的 Beta 版本,Istio 立即宣布了对 Kubernetes Gateway API 的支持,Istio API 也跟着升级到了 Beta 版本,并表示 Gateway API 将作为未来所有 Istio 流量管理的默认 API。

目前,用户可以选择 Kubernetes Ingress、Istio classic 和 Gateway API 三种方式来管理网格中的流量,相关内容可以参考 Kubernetes IngressIstio GatewayKubernetes Gateway API

扫描二维码,在手机上阅读!

实战 APISIX 服务发现

APISIX 使用小记 中,我们通过 APISIX 官方提供的入门示例学习了 APISIX 的基本概念,并使用 Admin API 和 Dashboard 两种方法创建路由。在创建路由时,我们必须明确地知道服务的 IP 和端口,这给运维人员带来了一定的负担,因为服务的重启或扩缩容都可能会导致服务的 IP 和端口发生变化,当服务数量非常多的时候,维护成本将急剧升高。

APISIX 集成了多种服务发现机制来解决这个问题,通过服务注册中心,APISIX 可以动态地获取服务的实例信息,这样我们就不用在路由中写死固定的 IP 和端口了。

如下图所示,一个标准的服务发现流程大致包含了三大部分:

discovery-cn.png

  1. 服务启动时将自身的一些信息,比如服务名、IP、端口等信息上报到注册中心;各个服务与注册中心使用一定机制(例如心跳)通信,如果注册中心与服务长时间无法通信,就会注销该实例;当服务下线时,会删除注册中心的实例信息;
  2. 网关会准实时地从注册中心获取服务实例信息;
  3. 当用户通过网关请求服务时,网关从注册中心获取的实例列表中选择一个进行代理;

目前市面上流行着很多注册中心,比如 Eureka、Nacos、Consul 等,APISIX 内置了下面这些服务发现机制:

基于 Eureka 的服务发现

Eureka 是 Netflix 开源的一款注册中心服务,它也被称为 Spring Cloud Netflix,是 Spring Cloud 全家桶中的核心成员。本节将演示如何让 APISIX 通过 Eureka 来实现服务发现,动态地获取下游服务信息。

启动 Eureka Server

我们可以直接运行官方的示例代码 spring-cloud-samples/eureka 来启动一个 Eureka Server:

$ git clone https://github.com/spring-cloud-samples/eureka.git
$ cd eureka && ./gradlew bootRun

或者也可以直接使用官方制作好的镜像:

$ docker run -d -p 8761:8761 springcloud/eureka

启动之后访问 http://localhost:8761/ 看看 Eureka Server 是否已正常运行。

启动 Eureka Client

如果一切顺利,我们再准备一个简单的 Spring Boot 客户端程序,引入 spring-cloud-starter-netflix-eureka-client 依赖,再通过 @EnableEurekaClient 注解将服务信息注册到 Eureka Server:

@EnableEurekaClient
@SpringBootApplication
@RestController
public class EurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }

    @RequestMapping("/")
    public String home() {
        return String.format("Hello, I'm eureka client.");
    }
}

在配置文件中设置服务名称和服务端口:

spring.application.name=eureka-client
server.port=8081

默认注册的 Eureka Server 地址是 http://localhost:8761/eureka/,可以通过下面的参数修改:

eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

默认情况下,Eureka Client 是将该服务所在的主机名注册到 Eureka Server,这在某些情况下可能会导致其他服务调不通该服务,我们可以通过下面的参数,让 Eureka Client 注册 IP 地址:

eureka.instance.prefer-ip-address=true
eureka.instance.ip-address=192.168.1.40

启动后,在 Eureka 页面的实例中可以看到我们注册的服务:

eureka-instances.png

APISIX 集成 Eureka 服务发现

接下来,我们要让 APISIX 通过 Eureka Server 找到我们的服务。首先,在 APISIX 的配置文件 config.yaml 中添加如下内容:

discovery:
  eureka:
    host:
      - "http://192.168.1.40:8761"
    prefix: /eureka/

然后重启 APISIX,接着向 APISIX 中添加如下路由:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/11 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/eureka",
    "plugins": {
        "proxy-rewrite" : {
            "regex_uri": ["/eureka", "/"]
        }
    },
    "upstream": {
        "type": "roundrobin",
        "discovery_type": "eureka",
        "service_name": "EUREKA-CLIENT"
    }
}'

之前创建路由时,我们在 upstream 中通过 nodes 参数表示上游服务器节点,这里我们不再需要写死服务器节点信息,而是通过 "discovery_type": "eureka""service_name": "EUREKA-CLIENT" 来让 APISIX 使用 eureka 服务发现机制,上游的服务名称为 EUREKA-CLIENT

值得注意的是,虽然上面的 Eureka Client 的 spring.application.name 是小写,但是注册到 Eureka Server 的服务名称是大写,所以这里的 service_name 参数必须是大写。此外,这里我们还使用了 proxy-rewrite 插件,它相当于 Nginx 中的路径重写功能,当多个上游服务的接口地址相同时,通过路径重写可以将它们区分开来。

访问 APISIX 的 /eureka 地址验证一下:

$ curl http://127.0.0.1:9080/eureka
Hello, I'm eureka client.

我们成功通过 APISIX 访问到了我们的服务。

关于 APISIX 集成 Eureka 的更多信息,可以参考官方文档 集成服务发现注册中心 和官方博客 API 网关 Apache APISIX 集成 Eureka 作为服务发现

基于 Nacos 的服务发现

Nacos 是阿里开源的一款集服务发现、配置管理和服务管理于一体的管理平台,APISIX 同样支持 Nacos 的服务发现机制。

启动 Nacos Server

首先,我们需要准备一个 Nacos Server,Nacos 官网提供了多种部署方式,可以 通过源码或安装包安装通过 Docker 安装通过 Kubernetes 安装,我们这里直接使用 docker 命令启动一个本地模式的 Nacos Server:

$ docker run -e MODE=standalone -p 8848:8848 -p 9848:9848 -d nacos/nacos-server:v2.2.0

不知道为什么,有时候启动会报这样的错误:com.alibaba.nacos.api.exception.runtime.NacosRuntimeException: errCode: 500, errMsg: load derby-schema.sql error,多启动几次又可以了。

启动成功后,访问 http://localhost:8848/nacos/ 进入 Nacos 管理页面,默认用户名和密码为 nacos/nacos

nacos-home.png

启动 Nacos Client

接下来,我们再准备一个简单的 Spring Boot 客户端程序,引入 nacos-discovery-spring-boot-starter 依赖,并通过它提供的 NameService 将服务信息注册到 Nacos Server:

@SpringBootApplication
@RestController
public class NacosApplication implements CommandLineRunner {

    @Value("${spring.application.name}")
    private String applicationName;

    @Value("${server.port}")
    private Integer serverPort;
    
    @NacosInjected
    private NamingService namingService;
    
    public static void main(String[] args) {
        SpringApplication.run(NacosApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        namingService.registerInstance(applicationName, "192.168.1.40", serverPort);
    }

    @RequestMapping("/")
    public String home() {
        return String.format("Hello, I'm nacos client.");
    }
}

在配置文件中设置服务名称和服务端口:

spring.application.name=nacos-client
server.port=8082

以及 Nacos Server 的地址:

nacos.discovery.server-addr=127.0.0.1:8848

启动后,在 Nacos 的服务管理页面中就可以看到我们注册的服务了:

nacos-service-management.png

APISIX 集成 Nacos 服务发现

接下来,我们要让 APISIX 通过 Nacos Server 找到我们的服务。首先,在 APISIX 的配置文件 config.yaml 中添加如下内容:

discovery:
  nacos:
    host:
      - "http://192.168.1.40:8848"
    prefix: "/nacos/v1/"

然后重启 APISIX,接着向 APISIX 中添加如下路由:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/22 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/nacos",
    "plugins": {
        "proxy-rewrite" : {
            "regex_uri": ["/nacos", "/"]
        }
    },
    "upstream": {
        "type": "roundrobin",
        "discovery_type": "nacos",
        "service_name": "nacos-client"
    }
}'

和上面 Eureka 服务发现的例子一样,我们也使用 proxy-rewrite 插件实现了路径重写功能,访问 APISIX 的 /nacos 地址验证一下:

$ curl http://127.0.0.1:9080/nacos
Hello, I'm nacos client.

我们成功通过 APISIX 访问到了我们的服务。

关于 APISIX 集成 Nacos 的更多信息,可以参考官方文档 基于 Nacos 的服务发现 和官方博客 Nacos 在 API 网关中的服务发现实践

基于 Consul 的服务发现

Consul 是由 HashiCorp 开源的一套分布式系统的解决方案,同时也可以作为一套服务网格解决方案,提供了丰富的控制平面功能,包括:服务发现、健康检查、键值存储、安全服务通信、多数据中心等。

启动 Consul Server

Consul 使用 Go 语言编写,安装和部署都非常简单,官方提供了 Consul 的多种安装方式,包括 二进制安装Kubernetes 安装HCP 安装。这里我们使用最简单的二进制安装方式,这种方式只需要执行一个可执行文件即可,首先,我们从 Install Consul 页面找到对应操作系统的安装包并下载:

$ curl -LO https://releases.hashicorp.com/consul/1.15.1/consul_1.15.1_linux_amd64.zip
$ unzip consul_1.15.1_linux_amd64.zip

下载并解压之后,Consul 就算安装成功了,使用 consul version 命令进行验证:

$ ./consul version
Consul v1.15.1
Revision 7c04b6a0
Build Date 2023-03-07T20:35:33Z
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)

Consul 安装完成后,就可以启动 Consul Agent 了,Consul Agent 有 -server-client 两种模式,-client 一般用于服务网格等场景,这里我们通过 -server 模式启动:

$ ./consul agent -server -ui -bootstrap-expect=1 -node=agent-one -bind=127.0.0.1 -client=0.0.0.0 -data-dir=./data_dir
==> Starting Consul agent...
              Version: '1.15.1'
           Build Date: '2023-03-07 20:35:33 +0000 UTC'
              Node ID: '8c1ccd5a-69b3-4c95-34c1-f915c19a3d08'
            Node name: 'agent-one'
           Datacenter: 'dc1' (Segment: '<all>')
               Server: true (Bootstrap: true)
          Client Addr: [0.0.0.0] (HTTP: 8500, HTTPS: -1, gRPC: -1, gRPC-TLS: 8503, DNS: 8600)
         Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302)
    Gossip Encryption: false
     Auto-Encrypt-TLS: false
            HTTPS TLS: Verify Incoming: false, Verify Outgoing: false, Min Version: TLSv1_2
             gRPC TLS: Verify Incoming: false, Min Version: TLSv1_2
     Internal RPC TLS: Verify Incoming: false, Verify Outgoing: false (Verify Hostname: false), Min Version: TLSv1_2

==> Log data will now stream in as it occurs:

其中 -ui 表示开启内置的 Web UI 管理界面,-bootstrap-expect=1 表示服务器希望以 bootstrap 模式启动,-node=agent-one 用于指定节点名称,-bind=127.0.0.1 这个地址用于 Consul 集群内通信,-client=0.0.0.0 这个地址用于 Consul 和客户端之间的通信,包括 HTTP 和 DNS 两种通信方式,-data-dir 参数用于设置数据目录。关于 consul agent 更多的命令行参数,可以参考 Agents OverviewAgents Command-line Reference

简单起见,我们也可以使用 -dev 参数以开发模式启动 Consul Agent:

$ ./consul agent -dev

如果 Consul Agent 启动成功,访问 http://localhost:8500/ 进入 Consul 的管理页面,在服务列表可以看到 consul 这个服务:

consul-services.png

在节点列表可以看到 agent-one 这个节点:

consul-nodes.png

启动 Consul Client

让我们继续编写 Consul Client 程序,引入 spring-cloud-starter-consul-discovery 依赖,并通过 @EnableDiscoveryClient 注解将服务信息注册到 Consul Server:

@EnableDiscoveryClient
@SpringBootApplication
@RestController
public class ConsulApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(ConsulApplication.class, args);
    }

    @RequestMapping("/")
    public String home() {
        return String.format("Hello, I'm consul client.");
    }
}

可以看到和 Eureka Client 的代码几乎是完全一样的,不过有一点要注意,我们还需要在 pom.xml 文件中引入 spring-boot-starter-actuator 依赖,开启 Actuator 端点,因为 Consul 默认是通过 /actuator/health 接口来对程序做健康检查的。

在配置文件中设置服务名称和服务端口:

spring.application.name=consul-client
server.port=8083

以及 Consul 相关的配置:

spring.cloud.consul.host=127.0.0.1
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.service-name=${spring.application.name}
spring.cloud.consul.discovery.prefer-ip-address=true
spring.cloud.consul.discovery.ip-address=192.168.1.40

启动后,在 Consul 的服务管理页面中就可以看到我们注册的服务了:

consul-service-detail.png

APISIX 集成 Consul 服务发现

接下来,我们要让 APISIX 通过 Consul Server 找到我们的服务。首先,在 APISIX 的配置文件 config.yaml 中添加如下内容:

discovery:
  consul:
    servers:
      - "http://192.168.1.40:8500"

然后重启 APISIX,接着向 APISIX 中添加如下路由:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/33 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/consul",
    "plugins": {
        "proxy-rewrite" : {
            "regex_uri": ["/consul", "/"]
        }
    },
    "upstream": {
        "type": "roundrobin",
        "discovery_type": "consul",
        "service_name": "consul-client"
    }
}'

访问 APISIX 的 /consul 地址验证一下:

$ curl http://127.0.0.1:9080/consul
Hello, I'm consul client.

关于 APISIX 集成 Consul 的更多信息,可以参考官方文档 基于 Consul 的服务发现

基于 Consul KV 的服务发现

Consul 还提供了分布式键值数据库的功能,这个功能和 Etcd、ZooKeeper 类似,主要用于存储配置参数和元数据。基于 Consul KV 我们也可以实现服务发现的功能。

首先准备 consul-kv-client 客户端程序,它的地址为 192.168.1.40:8084,我们通过 Consul KV 的 HTTP API 手工注册服务:

$ curl -X PUT http://127.0.0.1:8500/v1/kv/upstreams/consul-kv-client/192.168.1.40:8084 -d ' {"weight": 1, "max_fails": 2, "fail_timeout": 1}'

其中,/v1/kv/ 后的路径按照 {Prefix}/{Service Name}/{IP}:{Port} 的格式构成。可以在 Consul 的 Key/Value 管理页面看到我们注册的服务:

consul-key-value.png

然后在 APISIX 的配置文件 config.yaml 中添加如下内容:

discovery:
  consul_kv:
    servers:
      - "http://192.168.1.40:8500"
    prefix: "upstreams"

然后重启 APISIX,接着向 APISIX 中添加如下路由:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/44 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/consul_kv",
    "plugins": {
        "proxy-rewrite" : {
            "regex_uri": ["/consul_kv", "/"]
        }
    },
    "upstream": {
        "type": "roundrobin",
        "discovery_type": "consul_kv",
        "service_name": "http://192.168.1.40:8500/v1/kv/upstreams/consul-kv-client/"
    }
}'

注意这里的 service_name 参数需要设置为 KV 的 URL 路径,访问 APISIX 的 /consul_kv 地址验证一下:

$ curl http://127.0.0.1:9080/consul_kv
Hello, I'm consul_kv client.

另一点需要注意的是,这种方式注册的服务没有健康检查机制,服务退出后需要手工删除对应的 KV:

$ curl -X DELETE http://127.0.0.1:8500/v1/kv/upstreams/consul-kv-client/192.168.1.40:8084

关于 APISIX 集成 Consul KV 的更多信息,可以参考官方文档 基于 Consul KV 的服务发现 和官方博客 Apache APISIX 集成 Consul KV,服务发现能力再升级

基于 DNS 的服务发现

Consul 不仅支持 HTTP API,而且还支持 DNS API,它内置了一个小型的 DNS 服务器,默认端口为 8600,我们以上面的 consul-client 为例,介绍如何在 APISIX 中集成 DNS 的服务发现。

注册到 Consul 中的服务默认会在 Consul DNS 中添加一条 <服务名>.service.consul 这样的域名记录,使用 dig 命令可以查询该域名的信息:

$ dig @192.168.1.40 -p 8600 consul-client.service.consul

; <<>> DiG 9.11.3-1ubuntu1.17-Ubuntu <<>> @192.168.1.40 -p 8600 consul-client.service.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 32989
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;consul-client.service.consul.  IN      A

;; ANSWER SECTION:
consul-client.service.consul. 0 IN      A       192.168.1.40

;; Query time: 4 msec
;; SERVER: 192.168.1.40#8600(192.168.1.40)
;; WHEN: Tue Mar 21 07:17:40 CST 2023
;; MSG SIZE  rcvd: 73

上面的查询结果中只包含 A 记录,A 记录中只有 IP 地址,没有服务端口,如果用 A 记录来做服务发现,服务的端口必须得固定;好在 Consul 还支持 SRV 记录,SRV 记录中包含了服务的 IP 和端口信息:

$ dig @192.168.1.40 -p 8600 consul-client.service.consul SRV

; <<>> DiG 9.11.3-1ubuntu1.17-Ubuntu <<>> @192.168.1.40 -p 8600 consul-client.service.consul SRV
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 41141
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 3
;; WARNING: recursion requested but not available

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

;; ANSWER SECTION:
consul-client.service.consul. 0 IN      SRV     1 1 8083 c0a80128.addr.dc1.consul.

;; ADDITIONAL SECTION:
c0a80128.addr.dc1.consul. 0     IN      A       192.168.1.40
agent-one.node.dc1.consul. 0    IN      TXT     "consul-network-segment="

;; Query time: 3 msec
;; SERVER: 192.168.1.40#8600(192.168.1.40)
;; WHEN: Tue Mar 21 07:18:22 CST 2023
;; MSG SIZE  rcvd: 168

我们在 APISIX 的配置文件 config.yaml 中添加如下内容:

discovery:
  dns:
    servers:
      - "192.168.1.40:8600"

然后重启 APISIX,接着向 APISIX 中添加如下路由:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/55 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/dns",
    "plugins": {
        "proxy-rewrite" : {
            "regex_uri": ["/dns", "/"]
        }
    },
    "upstream": {
        "type": "roundrobin",
        "discovery_type": "dns",
        "service_name": "consul-client.service.consul"
    }
}'

访问 APISIX 的 /dns 地址验证一下:

$ curl http://127.0.0.1:9080/dns
Hello, I'm consul client.

关于 Consul DNS 的更多信息,可以参考官方文档 Discover services with DNS,除了 Consul DNS,我们也可以使用其他的 DNS 服务器来做服务发现,比如 CoreDNS 就是 Kubernetes 环境下的服务发现默认实现。

关于 APISIX 集成 DNS 的更多信息,可以参考官方文档 基于 DNS 的服务发现 和官方博客 API 网关 Apache APISIX 携手 CoreDNS 打开服务发现新大门

基于 APISIX-Seed 架构的控制面服务发现

上面所介绍的所有服务发现机制都是在 APISIX 上进行的,我们需要修改 APISIX 的配置,并重启 APISIX 才能生效,这种直接在网关上实现的服务发现也被称为 数据面服务发现,APISIX 还支持另一种服务发现机制,称为 控制面服务发现

控制面服务发现不直接对 APISIX 进行修改,而是将服务发现结果保存到 Etcd 中,APISIX 实时监听 Etcd 的数据变化,从而实现服务发现:

control-plane-service-discovery.png

APISIX 通过 APISIX-Seed 项目实现了控制面服务发现,这样做有下面几个好处:

  • 简化了 APISIX 的网络拓扑,APISIX 只需要关注 Etcd 的数据变化即可,不再和每个注册中心保持网络连接;
  • APISIX 不用额外存储注册中心的服务数据,减小内存占用;
  • APISIX 的配置变得简单,更容易管理;

虽然如此,目前 APISIX-Seed 还只是一个实验性的项目,从 GitHub 上的活跃度来看,官方似乎对它的投入并不是很高,目前只支持 ZooKeeperNacos 两种服务发现,而且官方也没有提供 APISIX-Seed 安装包的下载,需要我们自己通过源码来构建:

$ git clone https://github.com/api7/apisix-seed.git
$ make build

构建完成后,可以得到一个 apisix-seed 可执行文件,然后我们以上面的 Nacos 为例,介绍如何通过 APISIX-Seed 来实现控制面服务发现。

首先,我们将 APISIX 的配置文件中所有服务发现相关的配置都删掉,并重启 APISIX,接着打开 conf/conf.yaml 配置文件,文件中已经提前配置好了 Etcd、ZooKeeper、Nacos 等相关的配置,我们对其做一点裁剪,只保留下面这些信息:

etcd:
  host:
    - "http://127.0.0.1:2379"
  prefix: /apisix
  timeout: 30
    
log:
  level: warn
  path: apisix-seed.log
  maxage: 168h
  maxsize: 104857600
  rotation_time: 1h

discovery:
  nacos:
    host:
      - "http://127.0.0.1:8848"
    prefix: /nacos

然后启动 apisix-seed

$ ./apisix-seed
panic: no discoverer with key: dns

goroutine 15 [running]:
github.com/api7/apisix-seed/internal/discoverer.GetDiscoverer(...)
        D:/code/apisix-seed/internal/discoverer/discovererhub.go:42
        D:/code/apisix-seed/internal/core/components/watcher.go:84 +0x1d4
created by github.com/api7/apisix-seed/internal/core/components.(*Watcher).Init
        D:/code/apisix-seed/internal/core/components/watcher.go:48 +0x2b6
panic: no discoverer with key: consul

goroutine 13 [running]:
github.com/api7/apisix-seed/internal/discoverer.GetDiscoverer(...)
        D:/code/apisix-seed/internal/discoverer/discovererhub.go:42
github.com/api7/apisix-seed/internal/core/components.(*Watcher).handleQuery(0x0?, 0xc000091200, 0x0?)
        D:/code/apisix-seed/internal/core/components/watcher.go:84 +0x1d4
created by github.com/api7/apisix-seed/internal/core/components.(*Watcher).Init
        D:/code/apisix-seed/internal/core/components/watcher.go:48 +0x2b6

不过由于上面我们在路由中添加了 dns、consul 这些服务发现类型,这些 APISIX-Seed 是不支持的,所以启动会报错,我们需要将这些路由删掉:

$ curl -X DELETE http://127.0.0.1:9180/apisix/admin/routes/11 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
$ curl -X DELETE http://127.0.0.1:9180/apisix/admin/routes/33 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
$ curl -X DELETE http://127.0.0.1:9180/apisix/admin/routes/44 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
$ curl -X DELETE http://127.0.0.1:9180/apisix/admin/routes/55 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'

只保留一条 /nacos 的路由,然后重启 apisix-seed 即可:

$ ./apisix-seed
2023-03-22T07:49:53.849+0800    INFO    naming_client/push_receiver.go:80       udp server start, port: 55038

访问 APISIX 的 /nacos 地址验证一下:

$ curl http://127.0.0.1:9080/nacos
Hello, I'm nacos client.

关于 APISIX-Seed 的更多信息,可以参考官方文档 基于 APISIX-Seed 架构的控制面服务发现APISIX-Seed 项目文档

基于 Kubernetes 的服务发现

我们的服务还可能部署在 Kubernetes 集群中,这时,不用依赖外部的服务注册中心也可以实现服务发现,因为 Kubernetes 提供了强大而丰富的监听资源的接口,我们可以通过监听 Kubernetes 集群中 Services 或 Endpoints 等资源的实时变化来实现服务发现,APISIX 就是这样做的。

我们以 Kubernetes 使用小记 中的 kubernetes-bootcamp 为例,体验一下 APISIX 基于 Kubernetes 的服务发现。

首先在 Kubernetes 集群中创建对应的 Deployment 和 Service:

$ kubectl create deployment kubernetes-bootcamp --image=jocatalin/kubernetes-bootcamp:v1
deployment.apps/kubernetes-bootcamp created

$ kubectl expose deployment/kubernetes-bootcamp --type="NodePort" --port 8080
service/kubernetes-bootcamp exposed

通过 kubectl get svc 获取 NodePort 端口,并验证服务能正常访问:

$ kubectl get svc
NAME                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
kubernetes            ClusterIP   10.96.0.1       <none>        443/TCP          115d
kubernetes-bootcamp   NodePort    10.101.31.128   <none>        8080:32277/TCP   59s

$ curl http://192.168.1.40:32277
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-857b45f5bb-jtzs4 | v=1

接下来,为了让 APISIX 能查询和监听 Kubernetes 的 Endpoints 资源变动,我们需要创建一个 ServiceAccount

kind: ServiceAccount
apiVersion: v1
metadata:
 name: apisix-test
 namespace: default

以及一个具有集群级查询和监听 Endpoints 资源权限的 ClusterRole

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: apisix-test
rules:
- apiGroups: [ "" ]
  resources: [ endpoints ]
  verbs: [ get,list,watch ]

再将这个 ServiceAccountClusterRole 关联起来:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: apisix-test
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: apisix-test
subjects:
  - kind: ServiceAccount
    name: apisix-test
    namespace: default

然后我们需要获取这个 ServiceAccount 的 token 值,如果 Kubernetes 是 v1.24 之前的版本,可以通过下面的方法获取 token 值:

$ kubectl get secrets | grep apisix-test
$ kubectl get secret apisix-test-token-c64cv -o jsonpath={.data.token} | base64 -d

Kubernetes 从 v1.24 版本开始,不能再通过 kubectl get secret 获取 token 了,需要使用 TokenRequest API 来获取,首先开启代理:

$ kubectl proxy --port=8001
Starting to serve on 127.0.0.1:8001

然后调用 TokenRequest API 生成一个 token:

$ curl 'http://127.0.0.1:8001/api/v1/namespaces/default/serviceaccounts/apisix-test/token' \
  -H "Content-Type:application/json" -X POST -d '{}'
{
  "kind": "TokenRequest",
  "apiVersion": "authentication.k8s.io/v1",
  "metadata": {
    "name": "apisix-test",
    "namespace": "default",
    "creationTimestamp": "2023-03-22T23:57:20Z",
    "managedFields": [
      {
        "manager": "curl",
        "operation": "Update",
        "apiVersion": "authentication.k8s.io/v1",
        "time": "2023-03-22T23:57:20Z",
        "fieldsType": "FieldsV1",
        "fieldsV1": {
          "f:spec": {
            "f:expirationSeconds": {}
          }
        },
        "subresource": "token"
      }
    ]
  },
  "spec": {
    "audiences": [
      "https://kubernetes.default.svc.cluster.local"
    ],
    "expirationSeconds": 3600,
    "boundObjectRef": null
  },
  "status": {
    "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImtLdHRyVzFmNTRHWGFVUjVRS3hrLVJMSElNaXM4aENLMnpfSGk1SUJhbVkifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjc5NTMzMDQwLCJpYXQiOjE2Nzk1Mjk0NDAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImFwaXNpeC10ZXN0IiwidWlkIjoiMzVjZWJkYTEtNGZjNC00N2JlLWIxN2QtZDA4NWJlNzU5ODRlIn19LCJuYmYiOjE2Nzk1Mjk0NDAsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmFwaXNpeC10ZXN0In0.YexM_VoumpdwZNbSkwh6IbEu59PCtZrG1lkTnCqG24G-TC0U1sGxgbXf6AnUQ5ybh-CHWbJ7oewhkg_J4j7FiSAnV_yCcEygLkaCveGIQbWldB3phDlcJ52f8YDpHFtN2vdyVTm79ECwEInDsqKhn4n9tPY4pgTodI6D9j-lcK0ywUdbdlL5VHiOw9jlnS7b60fKWBwCPyW2uohX5X43gnUr3E1Wekgpo47vx8lahTZQqnORahTdl7bsPsu_apf7LMw40FLpspVO6wih-30Ke8CNBxjpORtX2n3oteE1fi2vxYHoyJSeh1Pro_Oykauch0InFUNyEVI4kJQ720glOw",
    "expirationTimestamp": "2023-03-23T00:57:20Z"
  }
}

默认的 token 有效期只有一个小时,可以通过参数改为一年:

$ curl 'http://127.0.0.1:8001/api/v1/namespaces/default/serviceaccounts/apisix-test/token' \
  -H "Content-Type:application/json" -X POST \
  -d '{"kind":"TokenRequest","apiVersion":"authentication.k8s.io/v1","metadata":{"name":"apisix-test","namespace":"default"},"spec":{"audiences":["https://kubernetes.default.svc.cluster.local"],"expirationSeconds":31536000}}'

我们在 APISIX 的配置文件 config.yaml 中添加如下内容( 将上面生成的 token 填写到 token 字段 ):

discovery:
  kubernetes:
    service:
      schema: https
      host: 127.0.0.1
      port: "6443"
    client:
      token: ...

这里有一个比较坑的地方,port 必须是字符串,否则会导致 APISIX 启动报错:

invalid discovery kubernetes configuration: object matches none of the required

然后重启 APISIX,接着向 APISIX 中添加如下路由:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/66 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/kubernetes",
    "plugins": {
        "proxy-rewrite" : {
            "regex_uri": ["/kubernetes", "/"]
        }
    },
    "upstream": {
        "type": "roundrobin",
        "discovery_type": "kubernetes",
        "service_name": "kubernetes-bootcamp"
    }
}'

访问 APISIX 的 /kubernetes 地址验证一下:

$ curl http://127.0.0.1:9080/kubernetes

不过,如果你的 APISIX 运行在 Kubernetes 集群之外,大概率是访问不通的,因为 APISIX 监听的 Endpoints 地址是 Kubernetes 集群内的 Pod 地址:

$ kubectl describe endpoints/kubernetes-bootcamp
Name:         kubernetes-bootcamp
Namespace:    default
Labels:       app=kubernetes-bootcamp
Annotations:  endpoints.kubernetes.io/last-change-trigger-time: 2023-03-25T00:31:43Z
Subsets:
  Addresses:          10.1.5.12
  NotReadyAddresses:  <none>
  Ports:
    Name     Port  Protocol
    ----     ----  --------
    <unset>  8080  TCP

Events:  <none>

所以想使用基于 Kubernetes 的服务发现,最佳做法是将 APISIX 部署在 Kubernetes 中,或者使用 APISIX Ingress,关于 APISIX 集成 Kubernetes 的更多信息,可以参考官方文档 基于 Kubernetes 的服务发现 和官方博客 借助 APISIX Ingress,实现与注册中心的无缝集成

参考

更多

实现自定义服务发现

https://apisix.apache.org/zh/docs/apisix/discovery/

扫描二维码,在手机上阅读!

实战 Docker 容器网络

我们知道,容器技术出现的初衷是对容器之间以及容器和宿主机之间的进程、用户、网络、存储等进行隔离,提供一种类似沙盒的虚拟环境,容器网络是这个虚拟环境的一部分,它能让应用从宿主机操作系统的网络环境中独立出来,形成容器自有的网络设备、IP 协议栈、端口套接字、IP 路由表、防火墙等模块。但是网络作为一种特殊的通信机制,我们有时候又希望容器之间、容器和宿主机之间甚至容器和远程主机之间能够互相通信,既要保证容器网络的隔离性,又要实现容器网络的连通性,这使得在容器环境下,网络的问题变得异常复杂。

Docker 是目前最流行的容器技术之一,它提供了一套完整的网络解决方案,不仅可以解决单机网络问题,也可以实现跨主机容器之间的通信。

容器网络模型

在学习各种不同的容器网络解决方案之前,我们首先来了解下 CNM 的概念。CNM(Container Network Model) 是 Docker 提出并主推的一种容器网络架构,这是一套抽象的设计规范,主要包含三个主要概念:

  • Sandbox - 提供了容器的虚拟网络栈,即端口套接字、IP 路由表、防火墙、DNS 配置等内容,主要用于隔离容器网络与宿主机网络,形成了完全独立的容器网络环境,一般通过 Linux 中的 Network Namespace 或类似的技术实现。一个 Sandbox 中可以包含多个 Endpoint。
  • Endpoint - 就是虚拟网络的接口,就像普通网络接口一样,它的主要职责是创建 Sandbox 到 Network 之间的连接,一般使用 veth pair 之类的技术实现。一个已连接的 Endpoint 只能归属于一个 Sandbox 和 一个 Network。
  • Network - 提供了一个 Docker 内部的虚拟子网,一个 Network 可以包含多个 Endpoint,同一个 Network 内的 Endpoint 之间可以互相通讯,一般使用 Linux bridge 或 VLAN 来实现。

这三个概念之间的关系如下图所示:

cnm.jpeg

libnetwork 是一个 Go 语言编写的开源库,它是 CNM 规范的标准实现,Docker 就是通过 libnetwork 库来实现 CNM 规范中的三大概念,此外它还实现了本地服务发现、基于 Ingress 的容器负载均衡、以及网络控制层和管理层功能。

除了控制层和管理层,我们还需要实现网络的数据层,这部分 Docker 是通过 驱动(Driver) 来实现的。驱动负责处理网络的连通性和隔离性,通过不同的驱动,我们可以扩展 Docker 的网络栈,实现不同的网络类型。

Docker 内置如下这些驱动,通常被称作 原生驱动 或者 本地驱动

第三方也可以通过 Network plugins 实现自己的网络驱动,这些驱动也被称作 远程驱动,比如 calicoflannelweave 等。

network-drivers.png

基于 Docker 网络的这种可插拔的设计,我们通过切换不同的网络驱动,就可以实现不同的容器网络解决方案。

单机容器网络方案

让我们从最简单的单机网络方案开始。Docker 在安装时,默认会在系统上创建三个网络,可以通过 docker network ls 命令查看:

$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
04eb1fccf2a8   bridge    bridge    local
189dfa3e3e64   host      host      local
1e63120a4e7a   none      null      local

这三个网络分别是 bridgehostnone,可以看到这三个网络都是 local 类型的。

Bridge 网络

Bridge 网络是目前使用最广泛的 Docker 网络方案,当我们使用 docker run 启动容器时,默认使用的就是 Bridge 网络,我们也可以通过命令行参数 --network=bridge 显式地指定使用 Bridge 网络:

$ docker run --rm -it --network=bridge busybox
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:9 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:782 (782.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

可以看到,使用 Bridge 网络的容器里除了 lo 这个本地回环网卡外,还有一个 eth0 以太网卡,那么这个 eth0 网卡是连接到哪里呢?我们可以从 /sys/class/net/eth0 目录下的 ifindexiflink 文件中一探究竟:

/ # cat /sys/class/net/eth0/ifindex
74
/ # cat /sys/class/net/eth0/iflink
75

其中 ifindex 表示网络设备的全局唯一 ID,而 iflink 主要被隧道设备使用,用于标识隧道另一头的设备 ID。也可以直接使用 ip link 命令查看:

/ # ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
74: eth0@if75: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff

如果 ifindexiflink 相同,表示这是一个真实的设备,ip link 中直接显示设备名,比如上面的 lo 网卡;如果 ifindexiflink 不相同,表示这是一个隧道设备,ip link 中显示格式为 ifindex: eth0@iflink,比如上面的 eth0 网卡。

所以容器中的 eth0 是一个隧道设备,它的 ID 为 74,连接它的另一头设备 ID 为 75,那么这个 ID 为 75 的设备又在哪里呢?答案在宿主机上。

我们在宿主机上运行 ip link 命令,很快就可以找到这个 ID 为 75 的设备:

# ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:c1:96:99 brd ff:ff:ff:ff:ff:ff
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default 
    link/ether 02:42:43:d2:12:6f brd ff:ff:ff:ff:ff:ff
75: vethe118873@if74: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default 
    link/ether 96:36:59:73:43:cc brd ff:ff:ff:ff:ff:ff link-netnsid 0

设备的名称为 vethe118873@if74,也暗示着另一头连接着容器里的 ID 为 74 的设备。像上面这样成对出现的设备,被称为 veth pairveth 表示虚拟以太网(Virtual Ethernet),而 veth pair 就是一个虚拟的以太网隧道(Ethernet tunnel),它可以实现隧道一头接收网络数据后,隧道另一头也能立即接收到,可以把它想象成一根网线,一头插在容器里,另一头插在宿主机上。那么它又插在宿主机的什么位置呢?答案是 docker0 网桥。

使用 brctl show 查看宿主机的网桥:

# brctl show
bridge name    bridge id        STP enabled    interfaces
docker0        8000.024243d2126f    no        vethe118873

可以看到一个名为 docker0 的网桥设备,并且能看到 vethe118873 接口就插在这个网桥上。这个 docker0 网桥是 Docker 在安装时默认创建的,当我们不指定 --network 参数时,创建的容器默认都挂在这个网桥上,它也是实现 Bridge 网络的核心部分。

使用下面的命令可以更直观的看出该网桥的详细信息:

# docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "ea473e613c26e31fb6d51edb0fb541be0d11a73f24f81d069e3104eef97b5cfc",
        "Created": "2022-06-03T11:56:33.258310522+08:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "33c8609c8cbe8e99f63fd430c926bd05167c2c92b64f97e4a06cfd0892f7e74c": {
                "Name": "suspicious_ganguly",
                "EndpointID": "3bf4b3d642826ab500f50eec4d5c381d20c75393ab1ed62a8425f8094964c122",
                "MacAddress": "02:42:ac:11:00:02",
                "IPv4Address": "172.17.0.2/16",
                "IPv6Address": ""
            }
        },
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1500"
        },
        "Labels": {}
    }
]

其中 IPAM 部分表明这个网桥的网段为 172.17.0.0/16,所以上面看容器的 IP 地址为 172.17.0.2;另外网关的地址为 172.17.0.1,这个就是 docker0 的地址:

# ip addr show type bridge
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:43:d2:12:6f brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:43ff:fed2:126f/64 scope link 
       valid_lft forever preferred_lft forever

docker0 网段地址可以在 Docker Daemon 的配置文件 /etc/docker/daemon.json 中进行修改:

$ vi /etc/docker/daemon.json
{
  "bip": "172.100.0.1/24"
}

修改之后重启 Docker Daemon:

$ systemctl restart docker

验证配置是否生效:

$ ip addr show docker0
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:43:d2:12:6f brd ff:ff:ff:ff:ff:ff
    inet 172.100.0.1/24 brd 172.100.0.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:43ff:fed2:126f/64 scope link 
       valid_lft forever preferred_lft forever

下图是 Bridge 网络大致的网络拓扑结构:

bridge-network.png

自定义 Bridge 网络

从 Bridge 的网络拓扑结构中我们可以看到,所有的容器都连接到同一个 docker0 网桥,它们之间的网络是互通的,这存在着一定的安全风险。为了保证我们的容器和其他的容器之间是隔离的,我们可以创建自定义的网络:

$ docker network create --driver=bridge test
5266130b7e5da2e655a68d00b280bc274ff4acd9b76f46c9992bcf0cd6df7f6a

创建之后,再次查看系统中的网桥列表:

$ brctl show
bridge name    bridge id        STP enabled    interfaces
br-5266130b7e5d        8000.0242474f9085    no        
docker0        8000.024243d2126f    no    

brctl show 的运行结果中可以看出多了一个新的网桥,网桥的名字正是我们创建的网络 ID,Docker 会自动为网桥分配子网和网关,我们也可以在创建网络时通过 --subnet--gateway 参数手工设置:

$ docker network create --driver=bridge --subnet=172.200.0.0/24 --gateway=172.200.0.1 test2
18f549f753e382c5054ef36af28b6b6a2208e4a89ee710396eb2b729ee8b1281

创建好自定义网络之后,就可以在启动容器时通过 --network 指定:

$ docker run --rm -it --network=test2 busybox
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
80: eth0@if81: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:c8:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.200.0.2/24 brd 172.200.0.255 scope global eth0
       valid_lft forever preferred_lft forever

由于 test2 的网段为 172.200.0.0/24,所以 Docker 为我们的容器分配的 IP 地址是 172.200.0.2,这个网段和 docker0 是隔离的,我们 ping 之前的容器 IP 是 ping 不同的:

/ # ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
^C
--- 172.17.0.2 ping statistics ---
4 packets transmitted, 0 packets received, 100% packet loss

自定义 Bridge 网络的拓扑结构如下图所示:

custom-network.png

虽然通过上面的实验我们发现不同网桥之间的网络是隔离的,不过仔细思考后会发现这里有一个奇怪的地方,自定义网桥和 docker0 网桥实际上都连接着宿主机上的 eth0 网卡,如果有对应的路由规则它们之间按理应该是可以通信的,为什么会网络不通呢?这就要从 iptables 中寻找答案了。

Docker 在创建网络时会自动添加一些 iptables 规则,用于隔离不同的 Docker 网络,可以通过下面的命令查看 iptables 规则:

$ iptables-save | grep ISOLATION
:DOCKER-ISOLATION-STAGE-1 - [0:0]
:DOCKER-ISOLATION-STAGE-2 - [0:0]
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-5266130b7e5d ! -o br-5266130b7e5d -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-18f549f753e3 ! -o br-18f549f753e3 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN
-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o br-5266130b7e5d -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o br-18f549f753e3 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN

前两行以 : 开头,表示创建两条空的 规则链(Rule Chain),在 iptables 中,规则链是由一系列规则(Rule)组成的列表,每个规则可以根据指定的条件来匹配或过滤数据包,并执行特定的动作。规则链可以包含其他规则链,从而形成一个规则链树。iptables 中有五个默认的规则链,分别是:

  • INPUT:用于处理进入本地主机的数据包;
  • OUTPUT:用于处理从本地主机发出的数据包;
  • FORWARD:用于处理转发到其他主机的数据包,这个规则链一般用于 NAT;
  • PREROUTING:用于处理数据包在路由之前的处理,如 DNAT;
  • POSTROUTING:用于处理数据包在路由之后的处理,如 SNAT。

第三行 -A FORWARD -j DOCKER-ISOLATION-STAGE-1 表示将在 FORWARD 规则链中添加一个新的规则链 DOCKER-ISOLATION-STAGE-1,而这个新的规则链包含下面定义的几条规则:

-A DOCKER-ISOLATION-STAGE-1 -i docker0 ! -o docker0 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-5266130b7e5d ! -o br-5266130b7e5d -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -i br-18f549f753e3 ! -o br-18f549f753e3 -j DOCKER-ISOLATION-STAGE-2
-A DOCKER-ISOLATION-STAGE-1 -j RETURN

第一条规则中的 -i docker0 ! -o docker0 表示数据包想进入 docker0 接口并且不是从 docker0 接口发出的,-i 表示 输入接口(input interface)-o 表示 输出接口(output interface),后面的 -j DOCKER-ISOLATION-STAGE-2 表示当匹配该规则时,跳转到 DOCKER-ISOLATION-STAGE-2 规则链继续处理。后面的规则都非常类似,每一条对应一个 Docker 创建的网桥的名字。最后一条表示当所有的规则都不满足时,那么这个数据包将被直接返回到原始的调用链,也就是被防火墙规则调用链中的下一条规则处理。

在上面的实验中,我们的容器位于 test2 网络中(对应的网桥是 br-18f549f753e3),要 ping 的 IP 地址属于 docker0 网络中的容器,很显然,数据包是发往 docker0 接口的(输入接口),并且是来自 br-18f549f753e3 接口的(输出接口),所以数据包满足 DOCKER-ISOLATION-STAGE-1 规则链中的第一条规则,将进入 DOCKER-ISOLATION-STAGE-2 规则链:

-A DOCKER-ISOLATION-STAGE-2 -o docker0 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o br-5266130b7e5d -j DROP
-A DOCKER-ISOLATION-STAGE-2 -o br-18f549f753e3 -j DROP
-A DOCKER-ISOLATION-STAGE-2 -j RETURN

DOCKER-ISOLATION-STAGE-2 规则链的第三条规则是,如果数据包从 br-18f549f753e3 接口发出则被直接丢弃。这就是我们为什么在自定义网络中 ping 不通 docker0 网络的原因。

我们可以通过 iptables -R 命令,将规则链中的规则修改为 ACCEPT:

$ iptables -R DOCKER-ISOLATION-STAGE-2 1 -o docker0 -j ACCEPT
$ iptables -R DOCKER-ISOLATION-STAGE-2 2 -o br-5266130b7e5d -j ACCEPT
$ iptables -R DOCKER-ISOLATION-STAGE-2 3 -o br-18f549f753e3 -j ACCEPT

此时再测试网络的连通性发现可以正常 ping 通了:

/ # ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=63 time=0.123 ms
64 bytes from 172.17.0.2: seq=1 ttl=63 time=0.258 ms
64 bytes from 172.17.0.2: seq=2 ttl=63 time=0.318 ms

测试完记得将规则还原回来:

$ iptables -R DOCKER-ISOLATION-STAGE-2 1 -o docker0 -j DROP
$ iptables -R DOCKER-ISOLATION-STAGE-2 2 -o br-5266130b7e5d -j DROP
$ iptables -R DOCKER-ISOLATION-STAGE-2 3 -o br-18f549f753e3 -j DROP

docker network connect

上面介绍了 Docker 如何通过 iptables 隔离不同网桥之间的通信,但是在一些现实场景中,我们可能需要它们之间互相通信,这可以通过 docker network connect 命令来实现:

$ docker network connect test2 8f358828cec2

这条命令的意思是将 8f358828cec2 容器连接到 test2 网络,相当于在容器里添加了一张网卡,并使用一根网线连接到 test2 网络上。我们在容器里可以看到多了一张新网卡 eth1

/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
8: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:c8:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/24 brd 172.17.0.255 scope global eth0
       valid_lft forever preferred_lft forever
30: eth1@if31: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:c8:00:03 brd ff:ff:ff:ff:ff:ff
    inet 172.200.0.3/24 brd 172.200.0.255 scope global eth1
       valid_lft forever preferred_lft forever

eth1@if31 这个名字可以看出,它也是一个 veth pair 隧道设备,设备的一头插在容器里的 eth1 网卡上,另一头插在自定义网桥上:

docker-network-connect.png

此时,容器之间就可以互相通信了,不过要注意的是,连接在 docker0 的容器现在有两个 IP,我们只能 ping 通 172.200.0.3172.17.0.2 这个 IP 仍然是 ping 不通的:

/ # ping 172.200.0.3
PING 172.200.0.3 (172.200.0.3): 56 data bytes
64 bytes from 172.200.0.3: seq=0 ttl=64 time=0.109 ms
64 bytes from 172.200.0.3: seq=1 ttl=64 time=0.106 ms
^C
--- 172.200.0.3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.106/0.107/0.109 ms
/ #
/ # ping 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
^C
--- 172.17.0.2 ping statistics ---
3 packets transmitted, 0 packets received, 100% packet loss

Container 网络

上面学习了两种容器间通信的方式,一种是通过 --network 参数在创建容器时指定自定义网络,另一种是 docker network connect 命令将已有容器加入指定网络,这两种方式都可以理解为:在容器内加一张网卡,并用网线连接到指定的网桥设备。

Docker 还提供了另一种容器间通信的方式:container 网络(也被称为 joined 网络),它可以让两个容器共享一个网络栈。我们通过 --network=container:xx 参数创建一个容器:

$ docker run --rm -it --network=container:9bd busybox

这个新容器将共享 9bd 这个容器的网络栈,容器的网络信息如下:

/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

我们进入 9bd 容器并查看该容器的网络信息:

# docker exec -it 9bd sh
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
6: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

可以发现新容器和 9bd 两个容器具有完全相同的网络信息,不仅 IP 地址一样,连 MAC 地址也一样,这两个容器之间可以通过 127.0.0.1 来通信。这种网络模式的拓扑图如下所示:

container-network.png

Container 网络一般用于多个服务的联合部署,比如有些应用需要运行多个服务,使用 Container 网络可以将它们放在同一个网络中,从而让它们可以互相访问,相比于自定义网桥的方式,这种方式通过 loopback 通信更高效。此外 Container 网络也非常适合边车模式,比如对已有的容器进行测试或监控等。

Host 网络

通过 --network=host 参数可以指定容器使用 Host 网络,使用 Host 网络的容器和宿主机共用同一个网络栈,也就是说,在容器里面,端口套接字、IP 路由表、防火墙、DNS 配置都和宿主机完全一样:

> docker run --rm -it --network=host busybox
/ # ifconfig
docker0   Link encap:Ethernet  HWaddr 02:42:07:3E:52:7E
          inet addr:172.17.0.1  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

eth0      Link encap:Ethernet  HWaddr 0A:3E:D1:F7:58:33
          inet addr:192.168.65.4  Bcast:0.0.0.0  Mask:255.255.255.255
          inet6 addr: fe80::83e:d1ff:fef7:5833/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:1280 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1271 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:2752699 (2.6 MiB)  TX bytes:107715 (105.1 KiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:709688 errors:0 dropped:0 overruns:0 frame:0
          TX packets:709688 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:149025485 (142.1 MiB)  TX bytes:149025485 (142.1 MiB)

veth2fbdd331 Link encap:Ethernet  HWaddr 46:B7:0B:D6:16:C2
          inet6 addr: fe80::44b7:bff:fed6:16c2/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:3876 errors:0 dropped:0 overruns:0 frame:0
          TX packets:3830 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:370360 (361.6 KiB)  TX bytes:413700 (404.0 KiB)

veth4d3538f6 Link encap:Ethernet  HWaddr 56:C0:72:ED:10:21
          inet6 addr: fe80::54c0:72ff:feed:1021/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:10934 errors:0 dropped:0 overruns:0 frame:0
          TX packets:12400 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:2197702 (2.0 MiB)  TX bytes:3897932 (3.7 MiB)

在容器里使用 ifconfig 命令,输出的结果其实是我们宿主机的网卡,容器里的 hostname 也和宿主机一样,所以我们可以在容器里使用 localhost 访问宿主机。

由于直接使用宿主机的网络,少了一层网络转发,所以 Host 网络具有非常好的性能,如果你的应用对网络传输效率有较高的要求,则可以选择使用 Host 网络。另外,使用 Host 网络还可以在容器里直接对宿主机网络进行配置,比如管理 iptables 或 DNS 配置等。

需要注意的是,Host 网络的主要优点是提高了网络性能,但代价是容器与主机共享网络命名空间,这可能会带来安全隐患。因此,需要仔细考虑使用 Host 网络的场景,并采取适当的安全措施,以确保容器和主机的安全性。

下图是 Host 网络大致的网络拓扑结构:

host-network.png

None 网络

None 网络是一种特殊类型的网络,顾名思义,它表示容器不连接任何网络。None 网络的容器是一个完全隔绝的环境,它无法访问任何其他容器或宿主机。我们在创建容器时通过 --network=none 参数使用 None 网络:

$ docker run --rm -it --network=none busybox
/ # ifconfig
lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

在使用 None 网络的容器里,除了 lo 设备之外,没有任何其他的网卡设备。这种与世隔离的环境非常适合一些安全性工作或测试工作,比如,对恶意软件进行逆向分析时,我们不希望恶意软件访问外部网络,使用 None 网络可以避免它对宿主机或其他服务造成影响;或者在单元测试时,通过 None 网络可以模拟网络隔离的效果,这样我们可以测试和验证没有网络情况下的程序表现。

下图是 None 网络大致的网络拓扑结构:

none-network.png

跨主机容器网络方案

在前一节中我们学习了 Docker 的几种网络模式,它们都是用于解决单个主机上的容器通信问题,那么如何实现不同主机上的容器通信呢?实际上,Docker 提供了几种原生的跨主机网络解决方案,包括:

这些跨主机网络方案需要具备一定的网络知识,比如 overlay 网络是基于 VxLAN (Virtual eXtensible LAN) 技术实现的,这是一种 隧道技术,将二层数据封装到 UDP 进行传输,这种网络技术我们将其称为 overlay 网络,指的是建立在其他网络上的网络;macvlan 是一种网卡虚拟化技术,它本身是 linux kernel 的一个模块,可以为同一个物理网卡配置多个 MAC 地址,每个 MAC 地址对应一个虚拟接口,由于它直接使用真实网卡通信,所以性能非常好,不过每个虚拟接口都有自己的 MAC 地址,而网络接口和交换机支持的 MAC 地址个数是有限的,MAC 地址过多时会造成严重的性能损失;ipvlan 解决了 MAC 地址过多的问题,它和 macvlan 类似,也是 linux kernel 的一个模块,但是它和 macvlan 不一样的是,macvlan 是为同一个网卡虚拟出多个 MAC 地址,而 ipvlan 是为同一个 MAC 地址虚拟多个 IP 地址;macvlan 和 ipvlan 不需要对包进行封装,这种网络技术又被称为 underlay 网络

除此之外,还有很多第三方网络解决方案,它们通过 Docker 网络的插件机制实现,包括:

参考

  1. Networking overview | Docker Documentation
  2. 每天5分钟玩转Docker容器技术
  3. Docker 网络模式详解及容器间网络通信
  4. 网络 - Docker — 从入门到实践
  5. 花了三天时间终于搞懂 Docker 网络了
  6. docker的网络-Container network interface(CNI)与Container network model(CNM)
  7. Docker容器网络互联
  8. linux 网络虚拟化: network namespace 简介
  9. Linux 虚拟网络设备 veth-pair 详解,看这一篇就够了
  10. 从宿主机直接进入docker容器的网络空间
  11. Deep dive into Linux Networking and Docker — Bridge, vETH and IPTables
  12. Container Networking: What You Should Know

更多

Kubernetes 网络

  1. 数据包在 Kubernetes 中的一生(1)
  2. 从零开始入门 K8s:理解 CNI 和 CNI 插件
  3. IPVS从入门到精通kube-proxy实现原理
  4. Kubernetes(k8s)kube-proxy、Service详解
扫描二维码,在手机上阅读!

使用 Helm 部署 Kubernetes 应用

Kubernetes 使用小记 中我们学习了如何通过 Deployment 部署一个简单的应用服务并通过 Service 来暴露它,在真实的场景中,一套完整的应用服务可能还会包含很多其他的 Kubernetes 资源,比如 DaemonSetIngressConfigMapSecret 等,当用户部署这样一套完整的服务时,他就不得不关注这些底层的 Kubernetes 概念,这对用户非常不友好。

我们知道,几乎每个操作系统都内置了一套软件包管理器,用来方便用户安装、配置、卸载或升级各种系统软件和应用软件,比如 Debian 和 Ubuntu 使用的是 DEB 包管理器 dpkgapt,CentOS 使用的是 RPM 包管理器 yum,Mac OS 有 Homebrew,Windows 有 WinGetChocolatey 等,另外很多系统还内置了图形界面的应用市场方便用户安装应用,比如 Windows 应用商店、Apple Store、安卓应用市场等,这些都可以算作是包管理器。

使用包管理器安装应用大大降低了用户的使用门槛,他只需要执行一句命令或点击一个安装按钮即可,而不用去关心这个应用需要哪些依赖和哪些配置。那么在 Kubernetes 下能不能也通过这种方式来部署应用服务呢?Helm 作为 Kubernetes 的包管理器,解决了这个问题。

Helm 简介

Helm 是 Deis 团队 于 2015 年发布的一个 Kubernetes 包管理工具,当时 Deis 团队还没有被微软收购,他们在一家名为 Engine Yard 的公司里从事 PaaS 产品 Deis Workflow 的开发,在开发过程中,他们需要将众多的微服务部署到 Kubernetes 集群中,由于在 Kubernetes 里部署服务需要开发者手动编写和维护数量繁多的 Yaml 文件,并且服务的分发和安装也比较繁琐,Matt Butcher 和另外几个同事于是在一次 Hackthon 团建中发起了 Helm 项目。

Helm 的取名非常有意思,Kubernetes 是希腊语 “舵手” 的意思,而 Helm 是舵手操作的 “船舵”,用来控制船的航行方向。

Helm 引入了 Chart 的概念,它也是 Helm 所使用的包格式,可以把它理解成一个描述 Kubernetes 相关资源的文件集合。开发者将自己的应用配置文件打包成 Helm chart 格式,然后发布到 ArtifactHub,这和你使用 docker push 将镜像推送到 DockerHub 镜像仓库一样,之后用户安装你的 Kubernetes 应用只需要一条简单的 Helm 命令就能搞定,极大程度上解决了 Kubernetes 应用维护、分发、安装等问题。

Helm 在 2018 年 6 月加入 CNCF,并在 2020 年 4 月正式毕业,目前已经是 Kubernetes 生态里面不可或缺的明星级项目。

快速开始

这一节我们将通过官方的入门示例快速掌握 Helm 的基本用法。

安装 Helm

我们首先从 Helm 的 Github Release 页面找到最新版本,然后通过 curl 将安装包下载下来:

$ curl -LO https://get.helm.sh/helm-v3.11.1-linux-amd64.tar.gz

然后解压安装包,并将 helm 安装到 /usr/local/bin 目录:

$ tar -zxvf helm-v3.11.1-linux-amd64.tar.gz
$ sudo mv linux-amd64/helm /usr/local/bin/helm

这样 Helm 就安装好了,通过 helm version 检查是否安装成功:

$ helm version
version.BuildInfo{Version:"v3.11.1", GitCommit:"293b50c65d4d56187cd4e2f390f0ada46b4c4737", GitTreeState:"clean", GoVersion:"go1.18.10"}

使用 helm help 查看 Helm 支持的其他命令和参数。

一般来说,直接下载 Helm 二进制文件就可以完成安装,不过官方也提供了一些其他方法来安装 Helm,比如通过 get_helm.sh 脚本来自动安装,或者通过 yumapt 这些操作系统的包管理器来安装,具体内容可参考官方的 安装文档

使用 Helm

Helm 安装完成之后,我们就可以使用 Helm 在 Kubernetes 中安装应用了。对于新手来说,最简单的方法是在 ArtifactHub 上搜索要安装的应用,然后按照文档中的安装步骤来操作即可。比如我们想要部署 Nginx,首先在 ArtifactHub 上进行搜索:

search-nginx.png

注意左侧的 KIND 勾选上 Helm charts,搜索出来的结果会有很多条,这些都是由不同的组织或个人发布的,可以在列表中看出发布的组织或个人名称,以及该 Charts 所在的仓库。Bitnami 是 Helm 中最常用的仓库之一,它内置了很多常用的 Kubernetes 应用,于是我们选择进入 第一条搜索结果

bitnami-nginx.png

这里可以查看关于 Nginx 应用的安装步骤、使用说明、以及支持的配置参数等信息,我们可以点击 INSTALL 按钮,会弹出一个对话框,并显示该应用的安装步骤:

nginx-install.png

我们按照它的提示,首先使用 helm repo add 将 Bitnami 仓库添加到我们的电脑:

$ helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories

然后使用 helm install 安装 Nginx 应用:

$ helm install my-nginx bitnami/nginx --version 13.2.23
NAME: my-nginx
LAST DEPLOYED: Sat Feb 11 08:58:10 2023
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: nginx
CHART VERSION: 13.2.23
APP VERSION: 1.23.3

** Please be patient while the chart is being deployed **
NGINX can be accessed through the following DNS name from within your cluster:

    my-nginx.default.svc.cluster.local (port 80)

To access NGINX from outside the cluster, follow the steps below:

1. Get the NGINX URL by running these commands:

  NOTE: It may take a few minutes for the LoadBalancer IP to be available.
        Watch the status with: 'kubectl get svc --namespace default -w my-nginx'

    export SERVICE_PORT=$(kubectl get --namespace default -o jsonpath="{.spec.ports[0].port}" services my-nginx)
    export SERVICE_IP=$(kubectl get svc --namespace default my-nginx -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    echo "http://${SERVICE_IP}:${SERVICE_PORT}"

稍等片刻,Nginx 就安装好了,我们可以使用 kubectl 来验证:

$ kubectl get deployments
NAME       READY   UP-TO-DATE   AVAILABLE   AGE
my-nginx   1/1     1            1           12m
$ kubectl get svc
NAME             TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
kubernetes       ClusterIP      10.96.0.1        <none>        443/TCP          75d
my-nginx         LoadBalancer   10.111.151.137   localhost     80:31705/TCP     12m

访问 localhost:80 可以看到 Nginx 已成功启动:

nginx.png

卸载和安装一样也很简单,使用 helm delete 命令即可:

$ helm delete my-nginx
release "my-nginx" uninstalled

我们还可以通过 --set 选项来改写 Nginx 的一些参数,比如默认情况下创建的 Service 端口是 80,使用下面的命令将端口改为 8080:

$ helm install my-nginx bitnami/nginx --version 13.2.23 \
    --set service.ports.http=8080

更多的参数列表可以参考安装文档中的 Parameters 部分。

另外,helm 命令和它的子命令还支持一些其他选项,比如上面的 --version--set 都是 helm install 子命令的选项,我们可以使用 helm 命令的 --namespace 选项将应用部署到指定的命名空间中:

$ helm install my-nginx bitnami/nginx --version 13.2.23 \
    --set service.ports.http=8080 \
    --namespace nginx --create-namespace

常用的 Helm 命令

通过上一节的学习,我们大致了解了 Helm 中三个非常重要的概念:

  • Repository
  • Chart
  • Release

Repository 比较好理解,就是存放安装包的仓库,可以使用 helm env 查看 HELM_REPOSITORY_CACHE 环境变量的值,这就是仓库的本地地址,用于缓存仓库信息以及已下载的安装包:

$ helm env | grep HELM_REPOSITORY_CACHE
HELM_REPOSITORY_CACHE="/home/aneasystone/.cache/helm/repository"

当我们执行 helm repo add 命令时,会将仓库信息缓存到该目录;当我们执行 helm install 命令时,也会将安装包下载并缓存到该目录。查看该目录,可以看到我们已经添加的 bitnami 仓库信息,还有已下载的 nginx 安装包:

$ ls /home/aneasystone/.cache/helm/repository
bitnami-charts.txt  bitnami-index.yaml  nginx-13.2.23.tgz

这个安装包就被称为 Chart,是 Helm 特有的安装包格式,这个安装包中包含了一个 Kubernetes 应用的所有资源文件。而 Release 就是安装到 Kubernetes 中的 Chart 实例,每个 Chart 可以在集群中安装多次,每安装一次,就会产生一个 Release。

明白了这三个基本概念,我们就可以这样理解 Helm 的用途:它先从 Repository 中下载 Chart,然后将 Chart 实例化后以 Release 的形式部署到 Kubernetes 集群中,如下图所示(图片来源):

helm-overview.png

而绝大多数的 Helm 命令,都是围绕着这三大概念进行的。

Repository 相关命令

helm repo

该命令用于添加、删除或更新仓库等操作。

将远程仓库添加到本地:

$ helm repo add bitnami https://charts.bitnami.com/bitnami

查看本地已安装仓库列表:

$ helm repo list
NAME    URL
bitnami https://charts.bitnami.com/bitnami

更新本地仓库:

$ helm repo update bitnami
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈

读取某个仓库目录,根据找到的 Chart 生成索引文件:

$ helm repo index SOME_REPO_DIR
# cat SOME_REPO_DIR/index.yaml

删除本地仓库:

$ helm repo remove bitnami
"bitnami" has been removed from your repositories

helm search

该命令用于在 ArtifactHub 或本地仓库中搜索 Chart。

在 ArtifactHub 中搜索 Chart:

$ helm search hub nginx
URL                                                     CHART VERSION   APP VERSION          DESCRIPTION
https://artifacthub.io/packages/helm/cloudnativ...      3.2.0           1.16.0               Chart for the nginx server
https://artifacthub.io/packages/helm/bitnami/nginx      13.2.23         1.23.3               NGINX Open Source is a web server that can be a...

要注意搜索出来的 Chart 链接可能不完整,不能直接使用,根据 stackoverflow 这里的解答,我们可以使用 -o json 选项将结果以 JSON 格式输出:

$ helm search hub nginx -o json | jq .
[
  {
    "url": "https://artifacthub.io/packages/helm/cloudnativeapp/nginx",
    "version": "3.2.0",
    "app_version": "1.16.0",
    "description": "Chart for the nginx server",
    "repository": {
      "url": "https://cloudnativeapp.github.io/charts/curated/",
      "name": "cloudnativeapp"
    }
  },
  {
    "url": "https://artifacthub.io/packages/helm/bitnami/nginx",
    "version": "13.2.23",
    "app_version": "1.23.3",
    "description": "NGINX Open Source is a web server that can be also used as a reverse proxy, load balancer, and HTTP cache. Recommended for high-demanding sites due to its ability to provide faster content.",
    "repository": {
      "url": "https://charts.bitnami.com/bitnami",
      "name": "bitnami"
    }
  }
}

在本地配置的所有仓库中搜索 Chart:

$ helm search repo nginx
NAME                                    CHART VERSION   APP VERSION     DESCRIPTION
bitnami/nginx                           13.2.23         1.23.3          NGINX Open Source is a web server that can be a...
bitnami/nginx-ingress-controller        9.3.28          1.6.2           NGINX Ingress Controller is an Ingress controll...
bitnami/nginx-intel                     2.1.15          0.4.9           DEPRECATED NGINX Open Source for Intel is a lig...

Chart 相关命令

helm create

该命令用于创建一个新的 Chart 目录:

$ helm create demo
Creating demo

创建的 Chart 目录是下面这样的结构:

$ tree demo
demo
├── Chart.yaml
├── charts
├── templates
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   └── tests
│       └── test-connection.yaml
└── values.yaml

3 directories, 10 files

helm package

将 Chart 目录(必须包含 Chart.yaml 文件)打包成 Chart 归档文件:

$ helm package nginx
Successfully packaged chart and saved it to: /home/aneasystone/helm/nginx-13.2.23.tgz

helm lint

验证 Chart 是否存在问题:

$ helm lint ./nginx
==> Linting ./nginx

1 chart(s) linted, 0 chart(s) failed

helm show

该命令用于显示 Chart 的基本信息,包括:

  • helm show chart - 显示 Chart 定义,实际上就是 Chart.yaml 文件的内容
  • helm show crds - 显示 Chart 的 CRD
  • helm show readme - 显示 Chart 的 README.md 文件中的内容
  • helm show values - 显示 Chart 的 values.yaml 文件中的内容
  • helm show all - 显示 Chart 的所有信息

helm pull

从仓库中将 Chart 安装包下载到本地:

$ helm pull bitnami/nginx
$ ls
nginx-13.2.23.tgz

helm push

将 Chart 安装包推送到远程仓库:

$ helm push [chart] [remote]

Release 相关命令

helm install

将 Chart 安装到 Kubernetes 集群:

$ helm install my-nginx bitnami/nginx --version 13.2.23

安装时可以通过 --set 选项修改配置参数:

$ helm install my-nginx bitnami/nginx --version 13.2.23 \
    --set service.ports.http=8080

其中 bitnami/nginx 是要安装的 Chart,这种写法是最常用的格式,被称为 Chart 引用,一共有六种不同的 Chart 写法:

  • 通过 Chart 引用:helm install my-nginx bitnami/nginx
  • 通过 Chart 包:helm install my-nginx ./nginx-13.2.23.tgz
  • 通过未打包的 Chart 目录:helm install my-nginx ./nginx
  • 通过 Chart URL:helm install my-nginx https://example.com/charts/nginx-13.2.23.tgz
  • 通过仓库 URL 和 Chart 引用:helm install --repo https://example.com/charts/ my-nginx nginx
  • 通过 OCI 注册中心:helm install my-nginx --version 13.2.23 oci://example.com/charts/nginx

helm list

显示某个命名空间下的所有 Release:

$ helm list --namespace default
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART           APP VERSION
my-nginx        default         1               2023-02-11 09:35:09.4058393 +0800 CST   deployed        nginx-13.2.23   1.23.3

helm status

查询某个 Release 的状态信息:

$ helm status my-nginx

helm get

获取某个 Release 的扩展信息,包括:

  • helm get hooks - 获取 Release 关联的钩子信息
  • helm get manifest - 获取 Release 的清单信息
  • helm get notes - 获取 Release 的注释
  • helm get values - 获取 Release 的 values 文件
  • helm get all - 获取 Release 的所有信息

helm upgrade

将 Release 升级到新版本的 Chart:

$ helm upgrade my-nginx ./nginx
Release "my-nginx" has been upgraded. Happy Helming!

升级时可以通过 --set 选项修改配置参数:

$ helm upgrade my-nginx ./nginx \
    --set service.ports.http=8080

helm history

查看某个 Release 的版本记录:

$ helm history my-nginx
REVISION        UPDATED                         STATUS          CHART           APP VERSION     DESCRIPTION
1               Sun Feb 12 11:14:18 2023        superseded      nginx-13.2.23   1.23.3          Install complete
2               Sun Feb 12 11:15:53 2023        deployed        nginx-13.2.23   1.23.3          Upgrade complete

helm rollback

将 Release 回滚到某个版本:

$ helm rollback my-nginx 1
Rollback was a success! Happy Helming!

再查看版本记录可以看到多了一条记录:

$ helm history my-nginx
REVISION        UPDATED                         STATUS          CHART           APP VERSION     DESCRIPTION
1               Sun Feb 12 11:14:18 2023        superseded      nginx-13.2.23   1.23.3          Install complete
2               Sun Feb 12 11:15:53 2023        superseded      nginx-13.2.23   1.23.3          Upgrade complete
3               Sun Feb 12 11:20:27 2023        deployed        nginx-13.2.23   1.23.3          Rollback to 1

helm uninstall

卸载 Release:

$ helm uninstall my-nginx
release "my-nginx" uninstalled

参考

  1. Helm | 快速入门指南
  2. Helm | 使用Helm
  3. Helm | 项目历史
  4. 微软 Deis Labs 的传奇故事
  5. 使用Helm管理kubernetes应用

更多

其他 Helm 命令

除了本文介绍的 Helm 三大概念以及围绕这三大概念的常用命令,也还有一些其他的命令:

制作自己的 Helm Chart

可视化管理工具

扫描二维码,在手机上阅读!

APISIX 使用小记

Apache APISIX 是基于 Nginx/OpenResty + Lua 方案打造的一款 动态实时高性能云原生 API 网关,提供了负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。APISIX 由国内初创公司 支流科技 于 2019 年 6 月开源,并于 7 月纳入 CNCF 全景图,10 月进入 Apache 孵化器,次年 7 月 毕业,成为国内唯一一个由初创公司贡献的项目,也是中国最快毕业的 Apache 顶级项目。

入门示例初体验

学习一门技术最好的方法就是使用它。这一节,我们将通过官方的入门示例,对 APISIX 的概念和用法有个基本了解。

首先,我们下载 apisix-docker 仓库:

git clone https://github.com/apache/apisix-docker.git

这个仓库主要是用来指导用户如何使用 Docker 部署 APISIX 的,其中有一个 example 目录,是官方提供的入门示例,我们可以直接使用 docker-compose 运行它:

$ cd apisix-docker/example
$ docker-compose up -d
[+] Running 8/8
 - Network example_apisix                Created                         0.9s
 - Container example-web2-1              Started                         5.1s
 - Container example-web1-1              Started                         4.0s
 - Container example-prometheus-1        Started                         4.4s
 - Container example-grafana-1           Started                         5.8s
 - Container example-apisix-dashboard-1  Started                         6.0s
 - Container example-etcd-1              Started                         5.1s
 - Container example-apisix-1            Started                         7.5s

可以看到创建了一个名为 example_apisix 的网络,并在这个网络里启动了 7 个容器:

  • etcd - APISIX 使用 etcd 作为配置中心,它通过监听 etcd 的变化来实时更新路由
  • apisix - APISIX 网关
  • apisix-dashboard - APISIX 管理控制台,可以在这里对 APISIX 的 Route、Upstream、Service、Consumer、Plugin、SSL 等进行管理
  • prometheus - 这个例子使用了 APISIX 的 prometheus 插件,用于暴露 APISIX 的指标,Prometheus 服务用于采集这些指标
  • grafana - Grafana 面板以图形化的方式展示 Prometheus 指标
  • web1 - 测试服务
  • web2 - 测试服务

部署之后可以使用 APISIX 的 Admin API 检查其是否启动成功:

$ curl http://127.0.0.1:9180/apisix/admin/routes \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
{"list":[],"total":0}

目前我们还没有创建任何路由,所以 /apisix/admin/routes 接口返回的结果为空。我们可以使用 Admin API 和 Dashboard 两种方式来创建路由。

使用 Admin API 创建路由

路由( Route ) 是 APISIX 中最基础和最核心的资源对象,APISIX 通过路由定义规则来匹配客户端请求,根据匹配结果加载并执行相应的插件,最后将请求转发到指定的上游服务。一条路由主要包含三部分信息:

  • 匹配规则:比如 methodsurihost 等,也可以根据需要自定义匹配规则,当请求满足匹配规则时,才会执行后续的插件,并转发到指定的上游服务;
  • 插件配置:这是可选的,但也是 APISIX 最强大的功能之一,APISIX 提供了非常丰富的插件来实现各种不同的访问策略,比如认证授权、安全、限流限速、可观测性等;
  • 上游信息:路由会根据配置的负载均衡信息,将请求按照规则转发到相应的上游。

所有的 Admin API 都采用了 Restful 风格,路由资源的请求地址为 /apisix/admin/routes/{id},我们可以通过不同的 HTTP 方法来查询、新增、编辑或删除路由资源(官方示例):

  • GET /apisix/admin/routes - 获取资源列表;
  • GET /apisix/admin/routes/{id} - 获取资源;
  • PUT /apisix/admin/routes/{id} - 根据 id 创建资源;
  • POST /apisix/admin/routes - 创建资源,id 将会自动生成;
  • DELETE /apisix/admin/routes/{id} - 删除指定资源;
  • PATCH /apisix/admin/routes/{id} - 标准 PATCH,修改指定 Route 的部分属性,其他不涉及的属性会原样保留;
  • PATCH /apisix/admin/routes/{id}/{path} - SubPath PATCH,通过 {path} 指定 Route 要更新的属性,全量更新该属性的数据,其他不涉及的属性会原样保留。

下面的例子将入门示例中的 web1 服务添加到路由中:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/1 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/web1",
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "web1:80": 1
        }
    }
}'

其中 X-API-KEY: edd1c9f034335f136f87ad84b625c8f1 是 Admin API 的访问 Token,可以在 APISIX 的配置文件 apisix_conf/config.yaml 中找到:

deployment:
  admin:
    allow_admin:
      - 0.0.0.0/0
    admin_key:
      - name: "admin"
        key: edd1c9f034335f136f87ad84b625c8f1
        role: admin
      - name: "viewer"
        key: 4054f7cf07e344346cd3f287985e76a2
        role: viewer

如果路由创建成功,将返回下面的 201 Created 信息:

HTTP/1.1 201 Created
Connection: close
Transfer-Encoding: chunked
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: *
Access-Control-Max-Age: 3600
Content-Type: application/json
Date: Tue, 31 Jan 2023 00:19:03 GMT
Server: APISIX/3.1.0
X-Api-Version: v3

{"key":"\/apisix\/routes\/1","value":{"create_time":1675124057,"uri":"\/web1","status":1,"upstream":{"pass_host":"pass","scheme":"http","nodes":{"web1:80":1},"hash_on":"vars","type":"roundrobin"},"priority":0,"update_time":1675124057,"id":"1"}}

这个路由的含义是当请求的方法是 GET 且请求的路径是 /web1 时,APISIX 就将请求转发到上游服务 web1:80。我们可以通过这个路径来访问 web1 服务:

$ curl http://127.0.0.1:9080/web1
hello web1

如果上游信息需要在不同的路由中复用,我们可以先创建一个 上游(Upstream)

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/upstreams/1 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "type": "roundrobin",
    "nodes": {
        "web1:80": 1
    }
}'

然后在创建路由时直接使用 upstream_id 即可:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/1 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/web1",
    "upstream_id": "1"
}'

另外,你可以使用下面的命令删除一条路由:

$ curl -X DELETE http://127.0.0.1:9180/apisix/admin/routes/1 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'

使用 Dashboard 创建路由

APISIX 提供了一套图形化 Dashboard 用来对网关的路由、插件、上游等进行管理,在入门示例中已经自带部署了 Dashboard,通过浏览器 http://localhost:9000 即可访问:

dashboard-login.png

默认的用户名和密码可以在 dashboard_conf/conf.yaml 文件中进行配置:

authentication:
  secret:
    secret     
  expire_time: 3600
  users:
    - username: admin
      password: admin
    - username: user
      password: user

登录成功后进入路由页面:

dashboard-routes.png

然后点击 “创建” 按钮创建一个路由:

dashboard-create-route.png

看上去这里的路由信息非常复杂,但是实际上我们只需要填写 名称路径HTTP 方法 即可,其他的维持默认值,当我们对 APISIX 的路由理解更深刻的时候可以再回过头来看看这些参数。

点击 “下一步” 设置上游信息:

dashboard-create-route-2.png

同样的,我们只关心目标节点的 主机名端口 两个参数即可。

然后再点击 “下一步” 进入插件配置,这里暂时先跳过,直接 “下一步” 完成路由的创建。路由创建完成后,访问 /web2 来验证路由是否生效:

$ curl http://127.0.0.1:9080/web2
hello web2

使用 APISIX 插件

通过上面的示例,我们了解了 APISIX 的基本用法,学习了如何通过 Admin API 或 Dashboard 来创建路由,实现了网关最基础的路由转发功能。APISIX 不仅具有高性能且低延迟的特性,而且它强大的插件机制为其高扩展性提供了无限可能。我们可以在 APISIX 插件中心 查看所有官方已经支持的插件,也可以 使用 lua 语言开发自己的插件,如果你对 lua 不熟悉,还可以使用其他语言 开发 External Plugin,APISIX 支持通过 Plugin Runner 以 sidecar 的形式来运行你的插件,APISIX 和 sidecar 之间通过 RPC 通信,不过这种方式对性能有一定的影响,如果你比较关注性能问题,那么可以使用你熟悉的语言开发 WebAssembly 程序,APISIX 也支持 运行 wasm 插件

external-plugin.png

这一节我们将通过几个官方插件来实现一些常见的网关需求。

在上面的学习中我们知道,一个路由是由匹配规则、插件配置和上游信息三个部分组成的,但是为了学习的递进性,我们有意地避免了插件配置部分。现在我们可以重新创建一个路由,并为其加上插件信息:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/3 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/web3",
    "plugins": {
        "limit-count": {
            "count": 2,
            "time_window": 60,
            "rejected_code": 503,
            "key": "remote_addr"
        },
        "prometheus": {}
    },
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "web1:80": 1
        }
    }
}'

上面的命令创建了一个 /web3 路由,并配置了两个插件:

  • limit-count - 该插件使用 固定窗口算法(Fixed Window algorithm) 对该路由进行限流,每分钟仅允许 2 次请求,超出时返回 503 错误码;
  • prometheus - 该插件将路由请求相关的指标暴露到 Prometheus 端点;

我们连续访问 3 次 /web3 路由:

$ curl http://127.0.0.1:9080/web3
hello web1
$ curl http://127.0.0.1:9080/web3
hello web1
$ curl http://127.0.0.1:9080/web3
<html>
<head><title>503 Service Temporarily Unavailable</title></head>
<body>
<center><h1>503 Service Temporarily Unavailable</h1></center>
<hr><center>openresty</center>
<p><em>Powered by <a href="https://apisix.apache.org/">APISIX</a>.</em></p></body>
</html>

可以看到 limit-count 插件的限流功能生效了,第 3 次请求被拒绝,返回了 503 错误码。另外,可以使用下面的命令查看 Prometheus 指标:

$ curl -i http://127.0.0.1:9091/apisix/prometheus/metrics

这个 Prometheus 指标地址可以在 apisix_conf/config.yaml 文件的 plugin_attr 中配置:

plugin_attr:
  prometheus:
    export_uri: /apisix/prometheus/metrics
    export_addr:
      ip: "0.0.0.0"
      port: 9091

APISIX 的插件可以动态的启用和禁用、自定义错误响应、自定义优先级、根据条件动态执行,具体内容可以参考 官方的 Plugin 文档。此外,如果一个插件需要在多个地方复用,我们也可以创建一个 Plugin Config

$ curl http://127.0.0.1:9180/apisix/admin/plugin_configs/1 \
  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
    "desc": "enable limit-count plugin",
    "plugins": {
        "limit-count": {
            "count": 2,
            "time_window": 60,
            "rejected_code": 503
        }
    }
}'

然后在创建路由时,通过 plugin_config_id 关联:

$ curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/1 \
    -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -i -d '
{
    "methods": ["GET"],
    "uri": "/web1",
    "upstream_id": "1",
    "plugin_config_id": "1"
}'

在 APISIX 的插件中心,我们可以看到 APISIX 将插件分成了下面几个大类:

  • General - 通用功能,比如 gzip 压缩配置、重定向配置等;
  • Transformation - 这类插件会对请求做一些转换操作,比如重写请求响应、gRPC 协议转换等;
  • Authentication - 提供一些常见的认证授权相关的功能,比如 API Key 认证、JWT 认证、Basic 认证、CAS 认证、LDAP 认证等;
  • Security - 安全相关的插件,比如开启 IP 黑白名单、开启 CORS、开启 CSRF 等;
  • Traffic - 这些插件对流量进行管理,比如限流、限速、流量镜像等;
  • Observability - 可观测性插件,支持常见的日志(比如 File-Logger、Http-Logger、Kafka-Logger、Rocketmq-Logger 等)、指标(比如 Prometheus、Datadog 等)和链路跟踪(比如 Skywalking、Zipkin、Opentelemetry 等)系统;
  • Serverless - 对接常见的 Serverless 平台,实现函数计算功能,比如 AWS Lambda、Apache OpenWhisk、CNCF Function 等;
  • Other Protocols - 这些插件用于支持 Dubbo、MQTT 等其他类型的协议;

参考

  1. 快速入门指南 | Apache APISIX® -- Cloud-Native API Gateway
  2. API 网关策略的二三事
  3. 从 Apache APISIX 来看 API 网关的演进
  4. 云原生时代的中外 API 网关之争

更多

APISIX 的部署模式

APISIX 支持多种不同的 部署模式,上面的示例中使用的是最常用的一种部署模式:traditional 模式,在这个模式下 APISIX 的控制平台和数据平面在一起:

deployment-traditional.png

我们也可以将 APISIX 部署两个实例,一个作为数据平面,一个作为控制平面,这被称为 decoupled 模式,这样可以提高 APISIX 的稳定性:

deployment-decoupled.png

上面两种模式都依赖于从 etcd 中监听和获取配置信息,如果我们不想使用 etcd,我们还可以将 APISIX 部署成 standalone 模式,这个模式使用 conf/apisix.yaml 作为配置文件,并且每间隔一段时间自动检测文件内容是否有更新,如果有更新则重新加载配置。不过这个模式只能作为数据平面,无法使用 Admin API 等管理功能(这是因为 Admin API 是基于 etcd 实现的):

deployment:
  role: data_plane
  role_data_plane:
    config_provider: yaml

将 APISIX 扩展为服务网格的边车

集成服务发现注册中心

扫描二维码,在手机上阅读!