bleem http://mritd.com/ Upward, not Northward. Wed, 22 Sep 2021 11:05:00 GMT http://hexo.io/ 从 oh-my-zsh 到 prezto http://mritd.com/2021/09/22/migrate-from-oh-my-zsh-to-prezto/ http://mritd.com/2021/09/22/migrate-from-oh-my-zsh-to-prezto/ Wed, 22 Sep 2021 11:05:00 GMT 本文介绍了从 oh-my-zsh 切换到 prezto 的过程以及一些配置调整过程。 一、为什么切换?

在一开始接触 oh-my-zsh 的时候说实话只是因为它的主题非常漂亮,例如 powerlevel10k 主题;这对于一个常年在终端上锻炼左右手的人来说确实是非常 “Sexy”。后来随着逐渐深度使用,oh-my-zsh 深度集成的这种一体化插件方案等确实带来了极大便利;例如简单的命令行搜索、git、docker、kuebctl 等各种插件的快速提示等。

但是当终端使用久了以后突然发现,其实像 powerlevel10k 这种花哨的终端主题并不适合我;当逐渐切换回简洁的一些主题,并在 Kubernetes 等大项目的目录下左右横跳时,oh-my-zsh 极慢的响应速度开始展露弊端;仅仅在 Kubernetes 的 Git 仓库目录下,按住回车键终端都能卡出动画…

所以当忍受不了终端这种拉垮的响应速度时,我感觉是时候换一个了。

二、Prezto 介绍

Prezto 官方仓库的介绍很简单,简单到只说 Prezto 是一个 zsh 配置框架,集成了一些主题、插件等。但是如果细说的话,其实 Prezto 最早应该是 oh-my-zsh 的 fork 版本,然后 Prezto 被一点点重写,现在已经基本看不到 oh-my-zsh 的影子了。不过唯一可以肯定的是,性能以及易用性上比 oh-my-zsh 好得多。

Prezto has been rewritten by the author who wanted to achieve a good zsh setup by ensuring all the scripts are making use of zsh syntax. It has a few more steps to install but should only take a few minutes extra. —- John Stevenson

三、Prezto 安装

Prezto 安装按照仓库文档的方法安装即可:

3.1、zsh 安装

首先确定已经安装了 zsh,如果没有安装则需要通过相应系统的包管理器等工具进行安装:

# macOS(最新版本的 macOS 已经默认安装了 zsh)brew install zsh# Ubuntuapt install zsh -y

3.2、克隆仓库

在仓库进行克隆时一般分为两种情况,一种默认克隆到 "${ZDOTDIR:-$HOME}/.zprezto" 目录(标准安装):

git clone --recursive http://github.com/sorin-ionescu/prezto.git "${ZDOTDIR:-$HOME}/.zprezto"

另一种高级用户可能使用 XDG_CONFIG_HOME 配置:

# 克隆仓库git clone --recursive http://github.com/sorin-ionescu/prezto.git "${ZDOTDIR:-${XDG_CONFIG_HOME:-$HOME/.config}/zsh}/.zprezto"# 调整 Prezto 的 XDG_CONFIG_HOME 配置# 该配置需要写入到 $HOME/.zshenv 中export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:=$HOME/.config}"export ZDOTDIR="${ZDOTDIR:=$XDG_CONFIG_HOME/zsh}"source "$ZDOTDIR/.zshenv"

3.3、创建软连接

Prezto 的安装方式比较方便定制化,在主仓库克隆完成后,只需要将相关的初始化加载配置软连接到 $HOME 目录即可:

setopt EXTENDED_GLOBfor rcfile in "${ZDOTDIR:-$HOME}"/.zprezto/runcoms/^README.md(.N); do    ln -s "$rcfile" "${ZDOTDIR:-$HOME}/.${rcfile:t}"done

不过需要注意的是上面的命令在某些 shell 脚本里直接写可能会有兼容性问题,在这种情况下可以直接通过命令进行简单处理:

for rcfile in $(ls ${ZDOTDIR:-$HOME}/.zprezto/runcoms/* | xargs -n 1 basename | grep -v README); do    target="${ZDOTDIR:-$HOME}/.${rcfile:t}"    ln -s "${ZDOTDIR:-$HOME}/.zprezto/runcoms/${rcfile}" "${target}"done

至此,Prezto 算是安装完成,重新登录 shell 即可看到效果。

四、细节调整

4.1、更换主题

默认情况下 Prezto 使用 sorin 这个主题,如果对默认主题不满意可以通过 prompt 命令切换:

# 列出当前支持的主题prompt -l# 直接在命令行上展示所有主题样式(预览)prompt -p# 临时试用某个主题prompt 主题名称# 保存该主题到配置中(使用)prompt -s 主题名称

4.2、grep 高亮

默认情况下 Prezto 在执行 grep 时会对结果进行高亮处理,在某些终端主题上可能会很影响观感:

grep 高亮是在 utility 插件中被开启的,可以通过在 ~/.zpreztorc 中增加以下配置关闭:

zstyle ':prezto:module:utility:grep' color 'no'

4.3、命令、语法高亮

Prezto 通过 syntax-highlighting 插件提供了各种语法高亮配置,通过解开以下配置的注释开启更多的自动高亮:

# Set syntax highlighters.# By default, only the main highlighter is enabled.zstyle ':prezto:module:syntax-highlighting' highlighters \  'main' \  'brackets' \  'pattern' \  'line' \  'cursor' \  'root'

需要注意的是,默认 root 高亮开启后,root 用户所有执行命令都会高亮,这样可能在主题配色上导致看不清输入的命令,可以简单的移除 root 高亮配置即可。

4.4、自定义命令高亮

syntax-highlighting 插件中启用了 pattern 高亮后,可以通过以下配置设置一些自定义的命令高亮配置,例如 rm -rf 等:

# Set syntax pattern styles.zstyle ':prezto:module:syntax-highlighting' pattern \  'rm*-rf*' 'fg=white,bold,bg=red'

4.5、历史命令搜索

oh-my-zsh 通过上下箭头按键来快速搜索历史命令是一个非常实用的功能,在切换到 Perzto 后会发现上下箭头的搜索变成了全命令的模糊匹配;例如输入 vim 然后上下翻页会匹配到位于命令中间带有 vim 字样的历史命令:

解决这个问题需要将 history-substring-search 插件依赖的 zsh-history-substring-search 切换到 master 分支并增加 HISTORY_SUBSTRING_SEARCH_PREFIXED 变量配置:

# 切换 zsh-history-substring-search 到 master 分支(cd ~/.zprezto/modules/history-substring-search/external && git checkout master)# 在 ~/.zshrc 中增加环境变量配置export HISTORY_SUBSTRING_SEARCH_PREFIXED=true

同时历史搜索里还有一个问题是同样的命令如果出现多次会被多次匹配,解决这个问题需要增加以下变量:

export HISTORY_SUBSTRING_SEARCH_ENSURE_UNIQUE=true

五、其他插件

更多可以使用的插件请参考 modules 目录下每个插件的文档,以及如何开启和配置。

为了方便自己使用,我在我的 init 项目下创建了快速初始化脚本,以上这些调整将自动完成:

curl http://raw.githubusercontent.com/mritd/init/master/prezto/init.sh | bash
]]>
Linux ohmyzsh prezto http://mritd.com/2021/09/22/migrate-from-oh-my-zsh-to-prezto/#disqus_thread
从 Nginx 切换到 Caddy http://mritd.com/2021/08/20/switching-rrom-nginx-too-caddy/ http://mritd.com/2021/08/20/switching-rrom-nginx-too-caddy/ Fri, 20 Aug 2021 13:42:00 GMT 记录一些测试环境 Nginx 切换到 Caddy 的一些小细节。

这几天把公司测试环境 Nginx 切换到了 Caddy,在实际切换过程中还是有一点小问题,但是目前感觉良好,这里记录一些细节。

一、为什么要切换

大部分情况我们的生产环境使用一个域名,为了保证隔离性我们会在测试环境采用另一个域名(偷偷透露一下,测试环境买 *.link 域名,国内能备案还贼便宜);然而我们不太舍得掏钱去给测试域名再买个证书,所以一直 ACME 大法。

众所周知这个玩意的证书 3 个月需要续签一次,脚本式续签然后 nginx reload 有时候还不太靠谱,总之内部环境复杂下脚本式操作还是有点风险,所以最后决定 Caddy 一把梭一劳永逸了。

二、切换中涉及到的细节

2.1、规则匹配

在某个站点中我们采用了 Nginx 判断 User-Agent 来处理访问到底是移动端还是桌面端,说实话我比较讨厌这种骚这种东西:

map $http_user_agent $is_desktop {    default 0;    ~*linux.*android|windows\s+(?:ce|phone) 0; # exceptions to the rule    ~*spider|crawl|slurp|bot 1; # bots    ~*windows|linux|os\s+x\s*[\d\._]+|solaris|bsd 1; # OSes}map $is_desktop $is_mobile {    1 0;    0 1;}server {    # reverse proxy    location / {        if ($is_mobile) {            rewrite ^ http://$host/h5 redirect;            break;        }        proxy_pass http://backend;        include conf.d/common/proxy.conf;    }}

一开始通过查找 Caddy 文档发现 Caddy 也是支持 map 的:

map {host}             {my_placeholder}  {magic_number} {example.com        "some value"      3foo.example.com    "another value"(.*)\.example.com  "${1} subdomain"  5~.*\.net$          -                 7~.*\.xyz$          -                 15default            "unknown domain"  42}

在实际配置时发现其实这个问题只需要用自定义规则匹配器判断一下是不是移动端即可:

@mobile {    header_regexp User-Agent (?i)linux.*android|windows\s+(?:ce|phone)    not path_regexp ^.+\.(?:css|cur|js|jpe?g|gif|htc|ico|png|html|xml|otf|ttf|eot|woff|woff2|svg)$    not path /web/*}rewrite @mobile /h5/{path}?{query}

在后续编写匹配规则时发现 Caddy 的匹配规则确实是非常强大,在官方的 Request Matchers 文档页面上可以找到基本上满足所有需求的匹配器,从请求头到请求方法、协议、请求路径,从标准匹配到通配符、正则匹配基本上样样俱全,甚至支持代码式的 CEL (Common Expression Language) 表达式匹配;多个匹配还可以自定义命名作为业务相关的匹配器使用。

2.2、规则重写

在 Nginx 中 rewrite 指令是多种行为的,比如可以进行 URL 隐式改写,也可以返回 301、307 等重定向代码;但是在 Caddy 中这两种行为被划分为两个指令:

  • rewrite: 内部重写,对 URL、参数等进行内部替换,浏览器地址将保持不变
  • redir: 重定向,返回 HTTP 状态码让客户端自行重定向到新页面

2.2.1、rewrite

针对于地址的隐式重写 rewrite 指令其语法规则如下:

rewrite [<matcher>] <to>

匹配器就是全局标准的匹配器定义,可以使用内置的,也可以组合内置匹配器为自定义匹配器,这个匹配器比 Nginx 强大太多;to 中分为三种情况:

  • 只替换 PATH: rewrite /abc /bcd:

这种情况下,rewrite 根据 “匹配器” 确定匹配路径,然后完全替换为最后一个路径;最后面的路径可以使用 {path} 占位符引用原始路径。

  • 只替换 请求参数: rewrite /api ?a=b:

这种情况下,Caddy 以 ? 作为分隔符,如果 ? 后面有内容就意味着将请求参数替换为后面的请求参数;最后面的请求参数可以通过 {query} 引用原始请求参数。

  • 全部替换: rewrite /abc /bcd?{query}&custom=1:

这种情况下,Caddy 根据 “匹配器” 匹配会即替换请求路径也替换请求参数,当然两个占位符也都是可用的。

需要注意的是: rewrite 只做重写,不会中断请求链,这意味着最终返回结果根据后续的请求匹配来决定。

2.2.2、redir

redir 用于向客户端声明显式的重定向,即返回特定重定向状态码,其语法如下:

redir [<matcher>] <to> [<code>]

匹配器就不说了,全都一样;**<to> 这个参数会作为 Location 头部值返回,其中可以使用占位符引用原始变量:**

redir * http://example.com{uri}

code 部分分为四种情况:

  • 一个 3xx 的自定义状态码
  • temporary: 返回 302 临时重定向
  • permanent: 返回 301 永久重定向
  • html: 使用 HTML 文档方式重定向

例如将所有请求永久重定向到新站点:

redir http://example.com{uri} permanent

这里面 HTML 方式是比较难理解的,这起源于一个规范,具体如下:

HTTP 协议中重定向机制是应该优先采用的创建重定向映射的方式,但是有时候 Web 开发者对于服务器没有控制权,或者无法对其进行配置。针对这些特定的应用情景,Web 开发者可以在精心制作的 HTML 页面的 部分添加一个 元素,并将其 http-equiv 属性的值设置为 refresh 。当显示页面的时候,浏览器会检测该元素,然后跳转到指定的页面。

在源码中如果使用了 html 重定向方式,Caddy 会返回一个 HTML 页面以满足上述方式的情况下让浏览器自行刷新:

var body stringswitch code {case "permanent":code = "301"case "temporary", "":code = "302"case "html":// Script tag comes first since that will better imitate a redirect in the browser's// history, but the meta tag is a fallback for most non-JS clients.const metaRedir = `<!DOCTYPE html><html><head><title>Redirecting...</title><script>window.location.replace("%s");</script><meta http-equiv="refresh" content="0; URL='%s'"></head><body>Redirecting to <a href="%s">%s</a>...</body></html>`safeTo := html.EscapeString(to)body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)code = "302"default:codeInt, err := strconv.Atoi(code)if err != nil {return nil, h.Errf("Not a supported redir code type or not valid integer: '%s'", code)}if codeInt < 300 || codeInt > 399 {return nil, h.Errf("Redir code not in the 3xx range: '%v'", codeInt)}}

2.2.3、uri

uri 指令是一个特殊指令,它与 rewrite 类似,不同的是它用于对 URI 重写更加方便,其语法如下:

uri [<matcher>] strip_prefix|strip_suffix|replace|path_regexp \<target> \[<replacement> [<limit>]]

语法中第二个参数为一个动词,用来定义如何替换 URI:

  • strip_prefix: 从路径中去除前缀
  • strip_suffix: 从路径中去除后缀
  • replace: 在整个 URI 路径中执行子替换(例如 /a/b/c/d 替换为 /a/1/2/d)
  • path_regexp: 在路径中进行正则替换

以下为一些样例:

# 去除 "/api/v1" 前缀uri strip_prefix /api/v1# 去除 ".html" 后缀uri strip_suffix .html# 子路径替换 "/v1" => "/v2"uri replace /v1/ /v2/# 正则替换 "/api/v数字" => "/api"uri path_regexp /api/v\d /api

其中在使用 replace 时最后面可以跟一个数字,代表从 URI 中找找替换多少次,默认为 -1 即全部替换。

2.3、WebSocket 代理

在 Nginx 配置中,如果想要代理 WebSocket 链接,我们需要增加以下设置:

proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection "upgrade";

但是在 Caddy 中一切变得更加简单… 简单到就是我们啥也不用干,自动支持:

Websocket proxying “just works” in v2; there is no need to “enable” websockets like in v1.

2.4、URL 编码

在使用路径匹配器时,URL 默认是被解码的,例如:

# 中文已经被解码,需要直接写解码后的字符串才能匹配到redir /2016/03/22/Java-内存之直接内存 http://mritd.com/2016/03/22/java-memory-direct-memory permanent

至于反向代理 reverse_proxy 传出时的编码暂时还没有遇到,还需要测试一下。

2.5、强制 HTTP

有些站点可能默认就是 HTTP 的,我们也不期望以 http 方式访问;但是 Caddy 默认会为站点进行 ACME 证书申请,而申请不下来证书时又访问不了;这种情况下只需要在站点地址上强制写明 HTTP 协议即可:

http://example.com {    reverse_proxy ...}

2.6、代理 http

如果想要代理 http 服务,那么只需要在 reverse_proxy 中填写 http 地址即可;不过与 Nginx 不同,Caddy 的 TLS 校验默认是开着的,所以如果后端 http 证书过期等情况可能导致 Caddy 返回 502 错误; 这种情况可以通过 transport 进行关闭:

reg.example.com {    handle {        request_body {            max_size 1G        }        reverse_proxy {            to http://172.16.11.40:443            transport http {                # SNI                tls_server_name reg.example.link                # 关闭后端 TLS 验证                tls_insecure_skip_verify            }        }    }}

2.7、自定义证书

如果已经有自己的证书,而不期望 Caddy 自动申请,那么只需要在 tls 指令后加上证书即可:

reg.example.com {    handle {        ...    }        # 使用自定义证书    tls cert.pem key.pem}

2.8、日志打印

Caddy 的日志系统与 Nginx 完全不同,Caddy 日志按照 Namespace 划分,在站点配置中默认为只可以打印当前站点的请求日志,如果需要打印例如反向代理的上游地址等需要在全局日志配置中配置。 日志这一块一句两句说不清,推荐直接看官方文档以及日志实现逻辑,如果懂 go 的话可以看看 uber-go/zap 这个日志框架;下面是按文件分开打印请求日志和上游日志的样例:

# Global options{    # 打印反向代理 upstream 信息日志(upstream 这个位置随便起名)    log upstream {        level DEBUG        format json {            time_format "iso8601"        }        output file /data/logs/upstream.log {            roll_size 100mb            roll_keep 3            roll_keep_for 7d        }        # 需要指定 Namespace 才能打印        include "http.handlers.reverse_proxy"    }}example.com {    # 打印站点请求日志    log {        format json {            time_format "iso8601"        }        output file "/data/logs/example.com.log" {            roll_size 100mb            roll_keep 3            roll_keep_for 7d        }    }}

2.9、TLS 版本不支持

很不幸的是我们有一个 TLS 1.1 兼容的服务,当切换到 Caddy 后 TLS 1.1 已经不被支持,目前 Caddy 的 TLS 兼容性最小为 TLS 1.2,最大为 TLS 1.3:

protocols <min> [<max>]

protocols specifies the minimum and maximum protocol versions. Default min: tls1.2. Default max: tls1.3.

三、切换总结

总结一句话: 匹配器舒服,配置行为明确,配置引用少写一万行,其他的坑继续踩。

]]>
Caddy Caddy http://mritd.com/2021/08/20/switching-rrom-nginx-too-caddy/#disqus_thread
使用 GoReplay 进行 HTTP 流量复制 http://mritd.com/2021/08/03/use-goreplay-to-record-your-live-traffic/ http://mritd.com/2021/08/03/use-goreplay-to-record-your-live-traffic/ Tue, 03 Aug 2021 10:23:00 GMT GoReplay 是一个实时流量抓取工具,可以将生产环境流量实时抓取并重放到测试环境;本文主要记录 GoReplay 的相关使用。 一、安装

GoReplay 采用 Go 编写,其只有一个单独的可执行文件,在官方 Release 页下载后将其放到 PATH 目录即可。

wget http://github.com/buger/goreplay/releases/download/v1.2.0/gor_v1.2.0_x64.tar.gztar -zxvf gor_v1.2.0_x64.tar.gzmv gor /usr/local/bin

二、基本使用

GoReplay 命令行整体使用方式为指定输入端和输入端,然后 GoReplay 从输入端将流量复制到输出端。

2.1、实时流量复制

GoReplay 输入端可以指定一个 tcp 地址,然后 GoReplay 将该端口流量复制到输出端;下面样例展示从 127.0.0.0:8000 复制流量并输出到控制台的样例。

首先启动一个 HTTP Server,这里直接使用 python 的 HTTP Server

接着再让 gor 监听同样的端口,--output-stdout 指定输出端为控制台

此时通过 curl 访问 python 的 HTTP Server 可以看到 gor 将 HTTP 请求复制并输出到了控制台

同样如果我们通过 --output-http 选项将输出端指定为另一个 HTTP Server,那么 gor 会将请求同步复制并发送到输出端 HTTP Server。

2.2、流量抓取与重放

2.2.1、基本使用

GoReplay 可以将输出端指定为文件,从而将流量保存到文件中,然后 GoReplay 读取该保存的流量文件并重放到指定的 HTTP Server 中。

首先通过 --outpu-file 选项将请求保存到文件中

使用 --input-file 选项读取流量信息,然后通过 --output-http 选项重放到目标服务器

2.2.2、扩展选项

在将流量保存到文件时,默认情况下 GoReplay 以块形式写入文件,并且每个块将生成一个独立的文件名(test_0.gor),如果想要将所有块的流量全部写入一个文件中,可以设置 --output-file-appendtrue

同时 GoReplay 输出文件名支持日期占位符,例如 --output-file %Y%m%d.gor 会生成 20210801.gor 这种文件名;所有可用的日期占位符如下:

  • %Y: year including the century (at least 4 digits)
  • %m: month of the year (01..12)
  • %d: Day of the month (01..31)
  • %H: Hour of the day, 24-hour clock (00..23)
  • %M: Minute of the hour (00..59)
  • %S: Second of the minute (00..60)

请求比较多时,将流量保存到文件可能会导致文件很大,这时候可以使用 .gz 结尾作为文件名,GoReplay 读取到 .gz 后缀后会自动进行 GZip 压缩处理。

gor --input-raw :8000 --output-file test.gor.gz

如果需要对多个文件进行聚合重放,只需要指定多个文件即可,重放过程中 GoReplay 会自动保持请求顺序:

gor --input-file *.gor --output-http http://127.0.0.1:8080

在使用文件输入时,GoReplay 还支持压力测试,通过 test.gor|200% 这种方式指定的文件名,GoReplay 会以两倍的速率进行请求重放:

# Replay from file on 2x speed gor --input-file "requests.gor|200%" --output-http "staging.com"

2.3、数据丢失与缓冲区

GoReplay 采用比较底层的数据包拦截技术,当一个 TCP 数据包到达时内核 GoReplay 会进行拦截;然而数据包可以乱序到达,接下来内核需要重建 TCP 流来保证上层应用能以正确的顺序读取 TCP 数据包,这时候内核就会有一个数据包的缓冲区;默认情况下 Linux 系统的缓冲区为 2M,Windiws 为 1M,当特定的 HTTP 请求数据包超过缓冲区时,GoReplay 就无法正确的拦截(因为 GoReplay 需要一个完整的 HTTP 请求数据包用于保存到文件或者重放),同时可能会导致请求丢失、请求损坏等问题。

为了解决这种问题,GoReplay 提供了 --input-raw-buffer-size 选项用于调整缓冲区大小,例如 --input-raw-buffer-size 10485760 选项会将缓冲区调整为 10M。

2.4、速率限制

某些情况下可能为了方便调试,我们在生产环境抓取流量并镜像到测试环境进行重放;但是可能由于生产环境流量比较大,我们并不需要如此大的请求速率,这时候可以通过速率限制让 GoReplay 帮我们控制请求数量。

绝对数量限制: 使用 --output-http "ADDRESS|N" 形式的参数时,GoReplay 会保证镜像的流量请求每秒不会超过 “N” 个。

# staging.server will not get more than ten requests per secondgor --input-tcp :28020 --output-http "http://staging.com|10"

百分比限制限制: 使用 --output-http "ADDRESS|N%" 形式的参数时,GoReplay 会保证镜像的流量维持在总流量的 “N%”。

2.5、请求过滤

在某些时候我们只期望把生产环境的特定流量重放到测试环境,或者禁止一些流量重放到测试环境,这时候我们可以使用 GoReplay 的过滤功能;GoReplay 提供以下选项来提供过滤功能:

  • --http-allow-header: 允许重放的 HTTP 头(支持正则)
  • --http-allow-method: 允许重放的 HTTP 方法
  • --http-allow-url: 允许重放的 URL(支持正则)
  • --http-disallow-header: 不允许的 HTTP 头(支持正则)
  • --http-disallow-url: 不允许的 HTTP URL(支持正则)

以下是官方给出的命令样例:

# only forward requests being sent to the /api endpointgor --input-raw :8080 --output-http staging.com --http-allow-url /api# only forward requests NOT being sent to the /api... endpointgor --input-raw :8080 --output-http staging.com --http-disallow-url /api# only forward requests with an api version of 1.0xgor --input-raw :8080 --output-http staging.com --http-allow-header api-version:^1\.0\d# only forward requests NOT containing User-Agent header value "Replayed by Gor"gor --input-raw :8080 --output-http staging.com --http-disallow-header "User-Agent: Replayed by Gor"gor --input-raw :80 --output-http "http://staging.server" \    --http-allow-method GET \    --http-allow-method OPTIONS

2.6、请求重写

有时候可能测试环境的 URL 路径与生产环境完全不同,此时如果直接把生产环境的流量在测试环境重放可能会导致请求路径错误等情况;为此 GoReplay 提供了 URL 重写、参数设置、请求头设置等功能。

通过 --http-rewrite-url 选项进行 URL 重写

# Rewrites all `/v1/user/<user_id>/ping` requests to `/v2/user/<user_id>/ping`gor --input-raw :8080 --output-http staging.com --http-rewrite-url /v1/user/([^\\/]+)/ping:/v2/user/$1/ping

设置 URL 参数

gor --input-raw :8080 --output-http staging.com --http-set-param api_key=1

设置请求头

gor --input-raw :80 --output-http "http://staging.server" \    --http-header "User-Agent: Replayed by Gor" \    --http-header "Enable-Feature-X: true"

Host 头是一个特殊的请求头,默认情况下 GoReplay 会将其自动设置为目标重放地址的域名,如果想关闭这种默认行为请使用 --http-original-host 选项。

三、其他高级配置

3.1、中继服务器

GoReplay 可以使用中继服务器从而实现链式的流量传递,使用中继服务器时只需要将输出端设置为 TCP 模式,然后中继服务器输入端也设置为 TCP 模式即可:

# Run on servers where you want to catch traffic. You can run it on each `web` machine.gor --input-raw :80 --output-tcp replay.local:28020# Replay server (replay.local).gor --input-tcp replay.local:28020 --output-http http://staging.com

如果有多个中继服务器,可以使用 --split-output 选项让每个抓取流量的 GoReplay 使用轮询算法向每个中继服务器发送流量:

gor --input-raw :80 --split-output --output-tcp replay1.local:28020 --output-tcp replay2.local:28020

3.2、输出到 ElasticSearch

GoReplay 支持将输出端设置为 ElasticSearch:

./gor --input-raw :8000 --output-http http://staging.com --output-http-elasticsearch localhost:9200/gor

输出到 ES 时不需要预先创建索引,GoReplay 会自动完成,输出到 ES 后其数据结构如下:

type ESRequestResponse struct {ReqURL               string `json:"Req_URL"`ReqMethod            string `json:"Req_Method"`ReqUserAgent         string `json:"Req_User-Agent"`ReqAcceptLanguage    string `json:"Req_Accept-Language,omitempty"`ReqAccept            string `json:"Req_Accept,omitempty"`ReqAcceptEncoding    string `json:"Req_Accept-Encoding,omitempty"`ReqIfModifiedSince   string `json:"Req_If-Modified-Since,omitempty"`ReqConnection        string `json:"Req_Connection,omitempty"`ReqCookies           string `json:"Req_Cookies,omitempty"`RespStatus           string `json:"Resp_Status"`RespStatusCode       string `json:"Resp_Status-Code"`RespProto            string `json:"Resp_Proto,omitempty"`RespContentLength    string `json:"Resp_Content-Length,omitempty"`RespContentType      string `json:"Resp_Content-Type,omitempty"`RespTransferEncoding string `json:"Resp_Transfer-Encoding,omitempty"`RespContentEncoding  string `json:"Resp_Content-Encoding,omitempty"`RespExpires          string `json:"Resp_Expires,omitempty"`RespCacheControl     string `json:"Resp_Cache-Control,omitempty"`RespVary             string `json:"Resp_Vary,omitempty"`RespSetCookie        string `json:"Resp_Set-Cookie,omitempty"`Rtt                  int64  `json:"RTT"`Timestamp            time.Time}

3.3、Kafka 对接

除了输出到 ES 以外,GoReplay 还支持输出到 Kafka 以及从 Kafka 中读取数据:

gor --input-raw :8080 --output-kafka-host '192.168.0.1:9092,192.168.0.2:9092' --output-kafka-topic 'kafka-log'gor --input-kafka-host '192.168.0.1:9092,192.168.0.2:9092' --input-kafka-topic 'kafka-log' --output-stdout
]]>
Linux GoReplay http://mritd.com/2021/08/03/use-goreplay-to-record-your-live-traffic/#disqus_thread
k0s 折腾笔记 http://mritd.com/2021/07/29/test-the-k0s-cluster/ http://mritd.com/2021/07/29/test-the-k0s-cluster/ Thu, 29 Jul 2021 06:13:00 GMT 发现一个宿主机二进制部署 Kubernetes 的好工具 -> k0s

最近两年一直在使用 kubeadm 部署 kubernetes 集群,总体来说配合一些自己小脚本还有一些自动化工具还算是方便;但是全容器化稳定性确实担忧,也遇到过莫名其妙的证书过期错误,最后重启大法解决这种问题;所以也在探索比较方便的二进制部署方式,比如这个 k0s。

一、k0s 介绍

The Simple, Solid & Certified Kubernetes Distribution.

k0s 可以认为是一个下游的 Kubernetes 发行版,与原生 Kubernetes 相比,k0s 并未阉割大量 Kubernetes 功能;k0s 主要阉割部分基本上只有树内 Cloud provider,其他的都与原生 Kubernetes 相同。

k0s 自行编译 Kubernetes 源码生成 Kubernetes 二进制文件,然后在安装后将二进制文件释放到宿主机再启动;这种情况下所有功能几乎与原生 Kubernetes 没有差异。

二、k0sctl 使用

k0sctl 是 k0s 为了方便快速部署集群所提供的工具,有点类似于 kubeadm,但是其扩展性要比 kubeadm 好得多。在多节点的情况下,k0sctl 通过 ssh 链接目标主机然后按照步骤释放文件并启动 Kubernetes 相关服务,从而完成集群初始化。

2.1、k0sctl 安装集群

安装过程中会自动下载相关镜像,需要保证所有节点可以扶墙,如何离线安装后面讲解。安装前保证目标机器的 hostname 为非域名形式,否则可能会出现一些问题。以下是一个简单的启动集群示例:

首先安装 k0sctl

# 安装 k0sctlwget http://github.com/k0sproject/k0sctl/releases/download/v0.9.0/k0sctl-linux-x64chmod +x k0sctl-linux-x64mv k0sctl-linux-x64 /usr/local/bin/k0sctl

然后编写 k0sctl.yaml 配置文件

apiVersion: k0sctl.k0sproject.io/v1beta1kind: Clustermetadata:  name: k0s-clusterspec:  hosts:  - ssh:      address: 10.0.0.11      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: controller+worker  - ssh:      address: 10.0.0.12      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: controller+worker  - ssh:      address: 10.0.0.13      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: controller+worker  - ssh:      address: 10.0.0.14      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: worker  - ssh:      address: 10.0.0.15      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: worker  k0s:    version: 1.21.2+k0s.1    config:      apiVersion: k0s.k0sproject.io/v1beta1      kind: Cluster      metadata:        name: k0s      spec:        api:          address: 10.0.0.11          port: 6443          k0sApiPort: 9443          sans:          - 10.0.0.11          - 10.0.0.12          - 10.0.0.13        storage:          type: etcd          etcd:            peerAddress: 10.0.0.11        network:          kubeProxy:            disabled: false            mode: ipvs

最后执行 apply 命令安装即可,安装前确保你的操作机器可以 ssh 免密登陆所有目标机器:

➜  tmp k0sctl apply -c bak.yaml⠀⣿⣿⡇⠀⠀⢀⣴⣾⣿⠟⠁⢸⣿⣿⣿⣿⣿⣿⣿⡿⠛⠁⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀█████████ █████████ ███⠀⣿⣿⡇⣠⣶⣿⡿⠋⠀⠀⠀⢸⣿⡇⠀⠀⠀⣠⠀⠀⢀⣠⡆⢸⣿⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀███          ███    ███⠀⣿⣿⣿⣿⣟⠋⠀⠀⠀⠀⠀⢸⣿⡇⠀⢰⣾⣿⠀⠀⣿⣿⡇⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀███          ███    ███⠀⣿⣿⡏⠻⣿⣷⣤⡀⠀⠀⠀⠸⠛⠁⠀⠸⠋⠁⠀⠀⣿⣿⡇⠈⠉⠉⠉⠉⠉⠉⠉⠉⢹⣿⣿⠀███          ███    ███⠀⣿⣿⡇⠀⠀⠙⢿⣿⣦⣀⠀⠀⠀⣠⣶⣶⣶⣶⣶⣶⣿⣿⡇⢰⣶⣶⣶⣶⣶⣶⣶⣶⣾⣿⣿⠀█████████    ███    ██████████k0sctl 0.0.0 Copyright 2021, k0sctl authors.Anonymized telemetry of usage will be sent to the authors.By continuing to use k0sctl you agree to these terms:http://k0sproject.io/licenses/eulaINFO ==> Running phase: Connect to hostsINFO [ssh] 10.0.0.15:22: connectedINFO [ssh] 10.0.0.11:22: connectedINFO [ssh] 10.0.0.12:22: connectedINFO [ssh] 10.0.0.14:22: connectedINFO [ssh] 10.0.0.13:22: connectedINFO ==> Running phase: Detect host operating systemsINFO [ssh] 10.0.0.11:22: is running Ubuntu 20.04.2 LTSINFO [ssh] 10.0.0.12:22: is running Ubuntu 20.04.2 LTSINFO [ssh] 10.0.0.14:22: is running Ubuntu 20.04.2 LTSINFO [ssh] 10.0.0.13:22: is running Ubuntu 20.04.2 LTSINFO [ssh] 10.0.0.15:22: is running Ubuntu 20.04.2 LTSINFO ==> Running phase: Prepare hostsINFO ==> Running phase: Gather host factsINFO [ssh] 10.0.0.11:22: discovered ens33 as private interfaceINFO [ssh] 10.0.0.13:22: discovered ens33 as private interfaceINFO [ssh] 10.0.0.12:22: discovered ens33 as private interfaceINFO ==> Running phase: Download k0s on hostsINFO [ssh] 10.0.0.11:22: downloading k0s 1.21.2+k0s.1INFO [ssh] 10.0.0.13:22: downloading k0s 1.21.2+k0s.1INFO [ssh] 10.0.0.12:22: downloading k0s 1.21.2+k0s.1INFO [ssh] 10.0.0.15:22: downloading k0s 1.21.2+k0s.1INFO [ssh] 10.0.0.14:22: downloading k0s 1.21.2+k0s.1......

稍等片刻后带有三个 Master 和两个 Node 的集群将安装完成:

# 注意: 目标机器 hostname 不应当为域名形式,这里的样例是已经修复了这个问题k1.node ➜ ~ k0s kubectl get node -o wideNAME      STATUS   ROLES    AGE   VERSION       INTERNAL-IP   EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIMEk1.node   Ready    <none>   10m   v1.21.2+k0s   10.0.0.11     <none>        Ubuntu 20.04.2 LTS   5.4.0-77-generic   containerd://1.4.6k2.node   Ready    <none>   10m   v1.21.2+k0s   10.0.0.12     <none>        Ubuntu 20.04.2 LTS   5.4.0-77-generic   containerd://1.4.6k3.node   Ready    <none>   10m   v1.21.2+k0s   10.0.0.13     <none>        Ubuntu 20.04.2 LTS   5.4.0-77-generic   containerd://1.4.6k4.node   Ready    <none>   10m   v1.21.2+k0s   10.0.0.14     <none>        Ubuntu 20.04.2 LTS   5.4.0-77-generic   containerd://1.4.6k5.node   Ready    <none>   10m   v1.21.2+k0s   10.0.0.15     <none>        Ubuntu 20.04.2 LTS   5.4.0-77-generic   containerd://1.4.6

2.2、k0sctl 的扩展方式

与 kubeadm 不同,k0sctl 几乎提供了所有安装细节的可定制化选项,其通过三种行为来完成扩展:

  • 文件上传: k0sctl 允许定义在安装前的文件上传,在安装之前 k0sctl 会把已经定义的相关文件全部上传到目标主机,包括不限于 k0s 本身二进制文件、离线镜像包、其他安装文件、其他辅助脚本等。
  • Manifests 与 Helm: 当将特定的文件上传到 master 节点的 /var/lib/k0s/manifests 目录时,k0s 在安装过程中会自动应用这些配置,类似 kubelet 的 static pod 一样,只不过 k0s 允许全部资源(包括不限于 deployment、daemonset、namespace 等);同样也可以直接在 k0sctl.yaml 添加 Helm 配置,k0s 也会以同样的方式帮你管理。
  • 辅助脚本: 可以在每个主机下配置 hooks 选项来实现执行一些特定的脚本(文档里没有,需要看源码),以便在特定情况下做点骚操作。

2.3、k0sctl 使用离线镜像包

基于上面的扩展,k0s 还方便的帮我们集成了离线镜像包的自动导入,我们只需要定义一个文件上传,将镜像包上传到 /var/lib/k0s/images/ 目录后,k0s 会自定将其倒入到 containerd 中而无需我们手动干预:

apiVersion: k0sctl.k0sproject.io/v1beta1kind: Clustermetadata:  name: k0s-clusterspec:  hosts:  - ssh:      address: 10.0.0.11      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: controller+worker    # files 配置将会在安装前将相关文件上传到目标主机    files:    - name: image-bundle      src: /Users/bleem/tmp/bundle_file      # 在该目录下的 image 压缩包将会被自动导入到 containerd 中      dstDir: /var/lib/k0s/images/      perm: 0755......

关于 image 压缩包(bundle_file)如何下载以及自己自定义问题请参考官方 Airgap install 文档。

2.4、切换 CNI 插件

默认情况下 k0s 内部集成了两个 CNI 插件: calico 和 kube-router;如果我们使用其他的 CNI 插件例如 flannel,我们只需要将默认的 CNI 插件设置为 custom,然后将 flannel 的部署 yaml 上传到一台 master 的 /var/lib/k0s/manifests 目录即可,k0s 会自动帮我门执行 apply -f xxxx.yaml 这种操作。

下面是切换到 flannel 的样例,需要注意的是 flannel 官方镜像不会帮你安装 CNI 的二进制文件,我们需要借助文件上传自己安装(CNI GitHub 插件下载地址):

apiVersion: k0sctl.k0sproject.io/v1beta1kind: Clustermetadata:  name: k0s-clusterspec:  hosts:  - ssh:      address: 10.0.0.11      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: controller+worker    files:    # 将 flannel 的 yaml 放到 manifests 里(需要单独创建一个目录)    - name: flannel      src: /Users/bleem/tmp/kube-flannel.yaml      dstDir: /var/lib/k0s/manifests/flannel      perm: 0644    # 自己安装一下 CNI 插件    - name: cni-plugins      src: /Users/bleem/tmp/cni-plugins/*      dstDir: /opt/cni/bin/      perm: 0755  k0s:    version: v1.21.2+k0s.1    config:      apiVersion: k0s.k0sproject.io/v1beta1      kind: Cluster      metadata:        name: k0s      spec:        api:          address: 10.0.0.11          port: 6443          k0sApiPort: 9443          sans:          - 10.0.0.11          - 10.0.0.12          - 10.0.0.13        storage:          type: etcd        network:          podCIDR: 10.244.0.0/16          serviceCIDR: 10.96.0.0/12          # 这里指定 CNI 为 custom 自定义类型,这样          # k0s 就不会安装 calico/kube-router 了          provider: custom

2.5、上传 k0s 二进制文件

除了普通文件、镜像压缩包等,默认情况下 k0sctl 在安装集群时还会在目标机器上下载 k0s 二进制文件;当然在离线环境下这一步也可以通过一个简单的配置来实现离线上传:

apiVersion: k0sctl.k0sproject.io/v1beta1kind: Clustermetadata:  name: k0s-clusterspec:  hosts:  - ssh:      address: 10.0.0.11      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: controller+worker    # 声明需要上传二进制文件    uploadBinary: true    # 指定二进制文件位置    k0sBinaryPath: /Users/bleem/tmp/k0s    files:    - name: flannel      src: /Users/bleem/tmp/kube-flannel.yaml      dstDir: /var/lib/k0s/manifests/flannel      perm: 0644......

2.6、更换镜像版本

默认情况下 k0s 版本号与 Kubernetes 保持一致,但是如果期望某个组件使用特定的版本,则可以直接配置这些内置组件的镜像名称:

apiVersion: k0sctl.k0sproject.io/v1beta1kind: Clustermetadata:  name: k0s-clusterspec:  hosts:  - ssh:      address: 10.0.0.11      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: controller+worker    uploadBinary: true    k0sBinaryPath: /Users/bleem/tmp/k0s    files:    - name: flannel      src: /Users/bleem/tmp/kube-flannel.yaml      dstDir: /var/lib/k0s/manifests/flannel      perm: 0644......  k0s:    version: v1.21.2+k0s.1    config:      apiVersion: k0s.k0sproject.io/v1beta1      kind: Cluster      metadata:        name: k0s      spec:        api:          address: 10.0.0.11          port: 6443          k0sApiPort: 9443          sans:          - 10.0.0.11          - 10.0.0.12          - 10.0.0.13        # 指定内部组件的镜像使用的版本        images:          #konnectivity:          #  image: us.gcr.io/k8s-artifacts-prod/kas-network-proxy/proxy-agent          #  version: v0.0.21          #metricsserver:          #  image: gcr.io/k8s-staging-metrics-server/metrics-server          #  version: v0.3.7          kubeproxy:            image: k8s.gcr.io/kube-proxy            version: v1.21.3          #coredns:          #  image: docker.io/coredns/coredns          #  version: 1.7.0          #calico:          #  cni:          #    image: docker.io/calico/cni          #    version: v3.18.1          #  node:          #    image: docker.io/calico/node          #    version: v3.18.1          #  kubecontrollers:          #    image: docker.io/calico/kube-controllers          #    version: v3.18.1          #kuberouter:          #  cni:          #    image: docker.io/cloudnativelabs/kube-router          #    version: v1.2.1          #  cniInstaller:          #    image: quay.io/k0sproject/cni-node          #    version: 0.1.0          default_pull_policy: IfNotPresent          #default_pull_policy: Never

2.7、调整 master 组件参数

熟悉 Kubernetes 的应该清楚,master 上三大组件: apiserver、controller、scheduler 管控整个集群;在 k0sctl 安装集群的过程中也允许自定义这些组件的参数,这些调整通过修改使用的 k0sctl.yaml 配置文件完成。

  • spec.api.extraArgs: 用于自定义 kube-apiserver 的自定义参数(kv map)
  • spec.scheduler.extraArgs: 用于自定义 kube-scheduler 的自定义参数(kv map)
  • spec.controllerManager.extraArgs: 用于自定义 kube-controller-manager 自定义参数(kv map)
  • spec.workerProfiles: 用于覆盖 kubelet-config.yaml 中的配置,该配置最终将于默认的 kubelet-config.yaml 合并

除此之外在 Host 配置中还有一个 InstallFlags 配置用于传递 k0s 安装时的其他配置选项。

三、k0s HA 搭建

其实上面的第二部分主要都是介绍 k0sctl 一些基础功能,为的就是给下面这部分 HA 生产级部署做铺垫。

就目前来说,k0s HA 仅支持独立负载均衡器的 HA 架构;即外部需要有一个高可用的 4 层负载均衡器,其他所有 Node 节点链接这个负载均衡器实现 master 的高可用。在使用 k0sctl 命令搭建 HA 集群时很简单,只需要添加一个外部负载均衡器地址即可;以下是一个完整的,全离线状态下的 HA 集群搭建配置。

3.1、外部负载均衡器

在搭建之前我们假设已经有一个外部的高可用的 4 层负载均衡器,且负载均衡器已经负载了以下端口:

  • 6443(for Kubernetes API): 负载均衡器 6443 负载所有 master 节点的 6443
  • 9443 (for controller join API): 负载均衡器 9443 负载所有 master 节点的 9443
  • 8132 (for Konnectivity agent): 负载均衡器 8132 负载所有 master 节点的 8132
  • 8133 (for Konnectivity server): 负载均衡器 8133 负载所有 master 节点的 8133

以下为一个 nginx 4 层代理的样例:

error_log syslog:server=unix:/dev/log notice;worker_processes auto;events {multi_accept on;use epoll;worker_connections 1024;}stream {    upstream kube_apiserver {        least_conn;        server 10.0.0.11:6443;        server 10.0.0.12:6443;        server 10.0.0.13:6443;    }    upstream konnectivity_agent {        least_conn;        server 10.0.0.11:8132;        server 10.0.0.12:8132;        server 10.0.0.13:8132;    }    upstream konnectivity_server {        least_conn;        server 10.0.0.11:8133;        server 10.0.0.12:8133;        server 10.0.0.13:8133;    }    upstream controller_join_api {        least_conn;        server 10.0.0.11:9443;        server 10.0.0.12:9443;        server 10.0.0.13:9443;    }        server {        listen        0.0.0.0:6443;        proxy_pass    kube_apiserver;        proxy_timeout 10m;        proxy_connect_timeout 1s;    }    server {        listen        0.0.0.0:8132;        proxy_pass    konnectivity_agent;        proxy_timeout 10m;        proxy_connect_timeout 1s;    }    server {        listen        0.0.0.0:8133;        proxy_pass    konnectivity_server;        proxy_timeout 10m;        proxy_connect_timeout 1s;    }    server {        listen        0.0.0.0:9443;        proxy_pass    controller_join_api;        proxy_timeout 10m;        proxy_connect_timeout 1s;    }}

3.2、搭建 HA 集群

以下为 k0sctl 的 HA + 离线部署样例配置:

apiVersion: k0sctl.k0sproject.io/v1beta1kind: Clustermetadata:  name: k0s-clusterspec:  hosts:  - ssh:      address: 10.0.0.11      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    # role 支持的值    # 'controller' 单 master    # 'worker' 单 worker    # 'controller+worker' master 和 worker 都运行     role: controller+worker        # 从本地 上传 k0s bin 文件,不要在目标机器下载    uploadBinary: true    k0sBinaryPath: /Users/bleem/tmp/k0s        # 上传其他文件    files:    # 上传 flannel 配置,使用自定的 flannel 替换内置的 calico    - name: flannel      src: /Users/bleem/tmp/kube-flannel.yaml      dstDir: /var/lib/k0s/manifests/flannel      perm: 0644        # 上传打包好的 image 镜像包,k0s 会自动导入到 containerd    - name: image-bundle      src: /Users/bleem/tmp/bundle_file      dstDir: /var/lib/k0s/images/      perm: 0755        # 使用 flannel 后每个机器要上传对应的 CNI 插件    - name: cni-plugins      src: /Users/bleem/tmp/cni-plugins/*      dstDir: /opt/cni/bin/      perm: 0755  - ssh:      address: 10.0.0.12      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: controller+worker    uploadBinary: true    k0sBinaryPath: /Users/bleem/tmp/k0s    files:    - name: image-bundle      src: /Users/bleem/tmp/bundle_file      dstDir: /var/lib/k0s/images/      perm: 0755    - name: cni-plugins      src: /Users/bleem/tmp/cni-plugins/*      dstDir: /opt/cni/bin/      perm: 0755  - ssh:      address: 10.0.0.13      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: controller+worker    uploadBinary: true    k0sBinaryPath: /Users/bleem/tmp/k0s    files:    - name: image-bundle      src: /Users/bleem/tmp/bundle_file      dstDir: /var/lib/k0s/images/      perm: 0755    - name: cni-plugins      src: /Users/bleem/tmp/cni-plugins/*      dstDir: /opt/cni/bin/      perm: 0755  - ssh:      address: 10.0.0.14      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: worker    uploadBinary: true    k0sBinaryPath: /Users/bleem/tmp/k0s    files:    - name: image-bundle      src: /Users/bleem/tmp/bundle_file      dstDir: /var/lib/k0s/images/      perm: 0755    - name: cni-plugins      src: /Users/bleem/tmp/cni-plugins/*      dstDir: /opt/cni/bin/      perm: 0755  - ssh:      address: 10.0.0.15      user: root      port: 22      keyPath: /Users/bleem/.ssh/id_rsa    role: worker    uploadBinary: true    k0sBinaryPath: /Users/bleem/tmp/k0s    files:    - name: image-bundle      src: /Users/bleem/tmp/bundle_file      dstDir: /var/lib/k0s/images/      perm: 0755    - name: cni-plugins      src: /Users/bleem/tmp/cni-plugins/*      dstDir: /opt/cni/bin/      perm: 0755  k0s:    version: v1.21.2+k0s.1    config:      apiVersion: k0s.k0sproject.io/v1beta1      kind: Cluster      metadata:        name: k0s      spec:        api:          # 此处填写外部的负载均衡器地址,所有 kubelet 会链接这个地址          externalAddress: 10.0.0.20          # 不要忘了为外部负载均衡器添加 api 证书的 SAN          sans:          - 10.0.0.11          - 10.0.0.12          - 10.0.0.13          - 10.0.0.20        # 存储类型使用 etcd,etcd 集群由 k0s 自动管理        storage:          type: etcd        network:          podCIDR: 10.244.0.0/16          serviceCIDR: 10.96.0.0/12          # 网络插件使用 custom,然后让 flannel 接管          provider: custom          kubeProxy:            disabled: false            # 开启 kubelet 的 ipvs 模式            mode: ipvs        # 不发送任何匿名统计信息        telemetry:          enabled: false        images:          default_pull_policy: IfNotPresent

最后只需要执行 k0sctl apply -c k0sctl.yaml 稍等几分钟集群就搭建好了,安装过程中可以看到相关文件的上传流程:

3.3、证书续签和管理

kubeadm 集群默认证书有效期是一年,到期要通过 kubeadm 重新签署;k0s 集群也差不多一样,但是不同的是 k0s 集群更加暴力;只要 CA(默认 10年) 不丢,k0s 每次重启都强行重新生成一年有效期的证书,所以在 HA 的环境下,快到期时重启一下 k0s 服务就行。

k0sctl 安装完的集群默认只有一个 k0scontroller.service 服务,master、node 上所有服务都由这个服务启动,所以到期之前 systemctl restart k0scontroller.service 一下就行。

四、集群备份和恢复

k0sctl 提供了集群备份和恢复功能,默认情况下只需要执行 k0sctl backup 即可完成集群备份,该命令会在当前目录下生成一个 k0s_backup_TIMESTAMP.tar.gz 备份文件。

需要恢复集群时使用 k0sctl apply --restore-from k0s_backup_TIMESTAMP.tar.gz 命令进行恢复即可;需要注意的是恢复命令等同于在新机器重新安装集群,所以有一定风险。

经过连续两天的测试,感觉这个备份恢复功能并不算靠谱,还是推荐使用 Velero 备份集群。

五、其他高级功能

5.1、Etcd 替换

在小规模集群场景下可能并不需要特别完善的 Etcd 作为存储,k0s 借助于 kine 库可以实现使用 SQLite 或 MySQL 等传统数据库作为集群存储;如果想要切换存储只需要调整 k0sctl.yaml 配置即可:

apiVersion: k0s.k0sproject.io/v1beta1kind: Clustermetadata:  name: k0sspec:  storage:    type: kine    kine:      dataSource: "sqlite:///var/lib/k0s/db/state.db?more=rwc&_journal=WAL&cache=shared"

5.2、集群用户管理

使用 k0sctl 搭建的集群通过 k0s 命令可以很方便的为集群添加用户,以下是添加样例:

k0s kubeconfig create --groups "system:masters" testUser > k0s.config

5.3、Containerd 配置

在不做配置的情况下 k0s 集群使用默认的 Containerd 配置,如果需要自己定义特殊配置,可以在安装时通过文件上传方式将 Containerd 配置文件上传到 /etc/k0s/containerd.toml 位置,该配置将会被 k0s 启动的 Containerd 读取并使用。

六、总结

k0s 是个不错的项目,对于二进制宿主机部署 Kubernetes 集群很方便,由于其直接采用 Kubernetes 二进制文件启动,所以基本没有功能阉割,而 k0sctl 又为自动化安装提供了良好的扩展性,所以值得一试。不过目前来说 k0s 在细节部分还有一定瑕疵,比如 konnectivity 服务在安装时无法选择性关闭等;k0s 综合来说是个不错的工具,也推荐看看源码,里面很多设计很新颖也比较利于了解集群引导过程。

]]>
Kubernetes Kubernetes k0s http://mritd.com/2021/07/29/test-the-k0s-cluster/#disqus_thread
Caddy2 file server 自动重定向问题 http://mritd.com/2021/07/02/fix-caddy2-fileserver-auto-redirect/ http://mritd.com/2021/07/02/fix-caddy2-fileserver-auto-redirect/ Fri, 02 Jul 2021 08:44:00 GMT Google Search Console 上看到好多无效链接,说我博客很多链接自动重定向了,研究半天发现是 Caddy2 的 file_server 问题,折腾两天顺手 PR 一下。 一、事情起因

自打很多年前开始使用静态博客工具来发布博客,现在基本上博客源码编译后就是一堆 html 等静态文件;一开始使用 nginx 作为静态文件服务器,后来切换到的 Caddy2;不过最近在 Google Search Console 中发现了大量的无效链接,给出的提示是 “网页会自动重定向”。

经过测试后发现这些链接地址在访问时都会重定向一下,然后在结尾加上 /;没办法我就开始探索这个 / 是怎么来的了。

二、源码分析

没办法,也不知道那个配置影响的,只能去翻 file server 的源码,在几经查找之后找到了以下代码(而且还带着注释):

从代码逻辑上看,只要 *fsrv.CanonicalURIs 这个变量为 true,那么就会触发自动重定像,并在 “目录” 尾部补上 /;注释里也说的很清楚是为了目录规范化,如果想看详细讨论可以参考那两个 issue。

三、解决方案

3.1、Admin API

翻了这个 *fsrv.CanonicalURIs 变量以后,突然发现 Caddyfile 里其实是不支持这个配置的;所以比较 low 的办法就是利用 Admin API,先把 json 弄出来,然后加上配置再。POST 回去:

{"apps": {"http": {"servers": {"srv0": {"listen": [":80"],"routes": [{"handle": [{+"canonical_uris": false,"handler": "file_server","hide": ["./Caddyfile"]}]}]}}}}}
curl -XPOST http://localhost:2019/load -H "Content-Type: application/json" -d @caddy.json

3.2、升级版本

现在可以直接从 master 构建 Caddy,或者等待 v2.4.4 版本发布,这两种方式产生的 Caddy 二进制文件已经支持了这个配置选项,配置样例如下:

:80file_server {disable_canonical_uris}
]]>
Golang Caddy Caddy http://mritd.com/2021/07/02/fix-caddy2-fileserver-auto-redirect/#disqus_thread
Caddyfile 语法浅析 http://mritd.com/2021/06/30/understand-caddyfile-syntax/ http://mritd.com/2021/06/30/understand-caddyfile-syntax/ Wed, 30 Jun 2021 09:28:00 GMT 发现大部分人在切换 Caddy 时遇到的比较大的困难就是这个 Caddyfile 不知道怎么写,一开始我也是很懵逼的状态,今天决定写写这个 Caddyfile 配置语法,顺便自己也完整的学学。

发现大部分人在切换 Caddy 时遇到的比较大的困难就是这个 Caddyfile 不知道怎么写,一开始我也是很懵逼的状态,今天决定写写这个 Caddyfile 配置语法,顺便自己也完整的学学。

一、Caddy 配置体系

在 Caddy1 时代,Caddy 自创了一种被称之为 Caddyfile 的配置文件格式,当然可以理解为创造了一种语法,这里面深入的说就涉及到了编译原理相关知识,这里不再展开细谈(因为我也不会);Caddyfile 由内部的语法解析器进行语法、词法分析最后 “序列化” 到 Go 的配置结构体中。

随着 Caddy 壮大,到了 Caddy2 时代人们已经并不满足于单纯的 Caddyfile 配置,因为学习 Caddyfile 是有代价的,负载均衡器选型的切换本身就代价很大,还要去花心思学习 Caddyfile 语法,这无异非常痛苦。所以 Caddy2 在经过取舍过后决定使用 json 作为内部标准配置,然后其他类型的配置通过 Config Adapters 将其转换为 json 再使用,而 Caddyfile 的 Adapter 作为官方支持的内置 Adapter 存在。

最终要说明的是: Caddyfile 里支持哪些指令是由 Caddyfile 的 Adapter 决定的,内部的 json 配置对应的指令名称可能跟 Caddyfile 不同,也可能内部 json 支持一些指令,而 Caddyfile 根本不支持。

二、Caddyfile 基本结构

开局一张图,文章全靠编(下面是官方的语法结构图)

2.1、全局选项

在一个 Caddyfile 内(空白文本文件),如果仅以两个大括号括起来的配置就是全局配置项,例如下面的配置:

{debughttp_port  8080http_port 8443}

那么一共有哪些全局配置项呢?当然是看 官方文档:

{# General Optionsdebughttp_port  <port>http_port <port>order <dir1> first|last|[before|after <dir2>]storage <module_name> {<options...>}storage_clean_interval <duration>admin   off|<addr> {origins <origins...>enforce_origin}log [name] {output  <writer_module> ...format  <encoder_module> ...level   <level>include <namespaces...>exclude <namespaces...>}grace_period <duration># TLS Optionsauto_http off|disable_redirects|ignore_loaded_certsemail <yours>default_sni <name>local_certsskip_install_trustacme_ca <directory_url>acme_ca_root <pem_file>acme_eab <key_id> <mac_key>acme_dns <provider> ...on_demand_tls {ask      <endpoint>interval <duration>burst    <n>}key_type ed25519|p256|p384|rsa2048|rsa4096cert_issuer <name> ...ocsp_stapling offpreferred_chains [smallest] {root_common_name <common_names...>any_common_name  <common_names...>}# Server Optionsservers [<listener_address>] {listener_wrappers {<listener_wrappers...>}timeouts {read_body   <duration>read_header <duration>write       <duration>idle        <duration>}max_header_size <size>protocol {allow_h2cexperimental_http3strict_sni_host}}}

这些全局配置具体都什么意思这里就不细说了,请自行查阅文档;当然文档也可能并不一定准确,有些兴趣的可以去查看 Caddy 源码,这些都在源码中定义了 caddyconfig/httpcaddyfile/options.go:28

2.2、代码块

叫代码块可能不太恰当,也可以叫做配置块或配置片段;这是 Caddyfile 比较棒的一个功能,配置片段可以实现类似代码这种引用使用,方便组合配置文件;配置片段的语法如下:

(配置片段名字) {    # 这里写配置片段的内容}

下面是一个配置片段示例(不能运行,只是举例):

# 定义一个叫 TLS_INTERMEDIATE 的配置片段(TLS_INTERMEDIATE) {    protocols tls1.2 tls1.3    ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256}www.mritd.com {    # 重定向    redir http://mritd.com{uri}    # 这里引用上面的 TLS_INTERMEDIATE 配置    import TLS_INTERMEDIATE}

这种写法与下面的配置等价,目的就是增加配置的重用和规范化:

www.mritd.com {    # 重定向    redir http://mritd.com{uri}    protocols tls1.2 tls1.3    ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256}

2.3、站点配置

站点配置是 Caddyfile 的核心中的核心,从开局的图上也可以看到,能在 “Top Level” 上存在的只有三种配置,其中就包含了这个站点配置块,站点配置块格式如下:

站点域名 {    # 其他配置}

以下是两个合法的站点配置示例:

example1.com {root * /www/example.comfile_server}example2.com {reverse_proxy localhost:9000}

2.4、自定义匹配器

请求匹配器是 Caddy 内置的一种针对请求的过滤工具,有点类似于 nginx 配置中的 location /api {...},只不过 Caddyfile 中的匹配器更加强大;标准的请求匹配器列表如下:

自定义命名匹配器的作用是组合多个标准匹配器,然后实现复用,自定义命名匹配器语法如下:

# @ 后面跟一个自定义名称@api {    # 标准匹配器组合    path /api/*    host example.com}

然后这个自定义的命名匹配器可以在其他位置引用:

example.com {    @api {        # 标准匹配器组合        path /api/*        host example.com    }        reverse_proxy @api 127.0.0.1:9000}

三、Caddyfile 语法细节

3.1、Blocks

Caddyfile 中的配置块可以理解为代码中的作用域,其包含两个大括号范围内的所有配置:

... {    ...}

当 Caddyfile 中只有一个站点配置,且不需要其他全局配置等信息时,Blocks 可以被省略,例如:

example.com {    reverse_proxy /api/* localhost:9001}

这个配置可以直接简写为:

example.comreverse_proxy /api/* localhost:9001

这么做的目的是方便单站点快速配置,但是一般不常用也不推荐使用。在同一个 Caddyfile 中可以包含多个站点配置,只要地址不同即可:

example.com {    ...}abcd.com {    ...}

3.2、Directives

指令是指描述站点配置的一些关键字,例如下面的站点配置文件:

example.com {    reverse_proxy /api/* localhost:9001}

在这个配置文件中 reverse_proxy 就是一个指令,同时指令还可能包含子指令(Subdirectives),下面的配置中 lb_policy 就是 reverse_proxy 的一个子指令:

example.com {    reverse_proxy localhost:9000 localhost:9001 {        lb_policy first    }}

3.3、Tokens and quotes

在 Caddyfile 被 Caddy 读取后,Caddy 会将配置文件解析为一个个的 Token;Caddyfile 中所有 Token 都认为是空格分割,所以如果某些指令需要传递参数时我们需要通过合理的空格和引号来确保 Token 正确解析:

example.com {    # 这里 localhost:9000 localhost:9001 空格分割就认为是两个 Token    reverse_proxy localhost:9000 localhost:9001}

如果某些参数需要包含空格,那么需要使用双引号包裹:

example.com {    file_server {        # 双引号包裹住有空格的参数        root "/data/Application Data/html"    }}

如果这个参数里需要包含双引号,只需要通过反斜线转义即可,例如 "\"a b\"";如果有太多的双引号或者空格,可以使用 Go 语言中类似的反引号来定义 “绝对字符串”:

example.com {    file_server {        # 反引号包裹        root `/data/Application Data/html`    }}

3.4、Addresses

Caddyfile 中的地址其实是一种很宽泛的格式,在上面讲站点配置时其实前面的字符串并不一定是域名,准确的说应该是地址:

地址 {    # 站点具体配置}

在 Caddyfile 中以下格式全部都是合法的地址:

  • localhost
  • example.com
  • :443
  • http://example.com
  • localhost:8080
  • 127.0.0.1
  • [::1]:2015
  • example.com/foo/*
  • *.example.com
  • http://

需要注意的是: 自动 http 是 Caddy 服务器的一个重要特性,但是自动 http 会隐式进行,除非在地址中明确的写明 http://example.com 这种格式时 Caddy 才会单纯监听 HTTP 协议,否则域名格式的地址 Caddy 都会进行 http 证书申请。

如果地址中指定了域名,那么只有匹配到域名的请求才会接受;例如地址为 localhost 的站点不会响应 127.0.0.1 方式的访问请求。同时地址中可以采用 * 作为通配符,通配符作用域仅在域名的英文句号 . 之内,意思就是说 *.example.com 会匹配 test.example.com 但不会匹配 abc.test.example.com

如果多个域名/地址共享一个站点配置,可以采用英文逗号分隔的方式写在一起:

example.com,www.example.com,localhost,127.0.0.1:8080 {    file_server {        root /data/html    }}

3.5、Matchers

匹配器其实在第一部分已经介绍过,这里仅做一下简单说明;匹配器一般紧跟在指令之后,其大致格式分为以下三种:

  • *: 匹配所有请求(通配符)
  • /path: 匹配特定路径
  • @name: 自定义命名匹配器

匹配器的用法样例如下:

# 自定义一个叫 websockets 的匹配器@websockets {    # 匹配请求头 Connection 中包含 Upgrade 的请求    header Connection *Upgrade*        # 匹配请求头 Upgrade 为 websocket 的请求    header Upgrade    websocket}# 反向代理时使用 @websockets 匹配器reverse_proxy @websockets localhost:6001

具体更细节的官方匹配器使用限于篇幅这里不再详细说明,请自行阅读 官方文档

3.6、Placeholders

占位符可以理解为 Caddyfile 内部的变量替换符号,占位符同样以大括号包裹,同时支持转义:

# 标准占位符{system.hostname}# 避免冲突可进行转义\{system.hostname\}

Caddyfile 内部可用的占位符有很多,但是并非在所有情况下都可用,比如 HTTP 相关的占位符仅在处理 HTTP 请求相关配置中才可用;同时占位符也支持简写,下面是官方目前支持的占位符列表:

3.7、Snippets

片段上面也介绍过了,这里说一下片段更高级的用法: 支持参数传递;下面是定义一个通用日志格式,然后通过参数引用实现不同站点使用不同日志文件的配置:

(LOG_COMMON) {    log {        format formatted "[{ts}] {request>remote_addr} {request>proto} {request>method} <- {status} -> {request>host} {request>uri} {request>headers>User-Agent>[0]}"  {            time_format "iso8601"        }                # {args.0} 声明引用传入的第一个参数        output file "{args.0}" {            roll_size 100mb            roll_keep 3            roll_keep_for 7d        }    }}example.com {    # 此时 /data/log/example.com.log 作为 "{args.0}" 被传入    import LOG_COMMON /data/log/example.com.log}

3.8、Comments

注释没啥好说的,以 # 作为开头就行了。

3.9、Environment variables

环境变量和占位符类似,不同的是占位符是 Caddyfile 内置的变量,而环境变量是引用系统环境变量;环境变量的使用格式如下(推荐全大写):

# 引用一个叫 SITE_ADDRESS 的环境变量{$SITE_ADDRESS} {    # 站点具体配置...}

上面的配置在 Caddy 启动时会读取 SITE_ADDRESS 作为监听地址,如果 SITE_ADDRESS 读取不到则会报错退出;如果想要为 SITE_ADDRESS 设置默认值,那么只需要使用如下格式即可:

{$SITE_ADDRESS:localhost} {    # 站点具体配置...}

四、其他补充

Caddyfile 并不是万能的,但是 Caddyfile 因为更易于编写和维护所以使用比较广泛;在第一部分介绍 Caddy 的配置文件体系时已经说明了,实际上 Caddy 内部是使用 json 作为配置的;这时就可能出现一些极端情况,比如说真的某个配置只能通过 json 配置,那么这时候可以考虑先通过 json 管理 API 进行动态修改,然后再去向官方发 issue,有能力也可以直接 PR;API 动态修改的流程如下:

首先假设你已经有一个能够正常启动的 Caddyfile,但是某个配置选项不支持,这时候你可以通过 API 获取内部的 json 配置:

# 结尾一定要有 /➜ ~ curl localhost:2019/config/{"apps":{"http":{"servers":{"srv0":{"listen":[":80"],"routes":[{"handle":[{"canonical_uris":false,"handler":"file_server","hide":["./Caddyfile"]}]}]}}}}}

得到这个配置以后,你可以通过格式化工具格式化 json,然后添加特定选项,再将其保存到一个配置文件中,然后重新 load 回去即可:

# 假设修改后的 json 文件叫 caddy.json➜ ~ curl -XPOST http://localhost:2019/load -H "Content-Type: application/json" -d @caddy.json
]]>
Caddy Caddy Caddyfile http://mritd.com/2021/06/30/understand-caddyfile-syntax/#disqus_thread
Golang Context 源码分析 http://mritd.com/2021/06/27/golang-context-source-code/ http://mritd.com/2021/06/27/golang-context-source-code/ Sun, 27 Jun 2021 07:57:00 GMT 好久之前就想仔细看看这个 Context,最近稍微有点时间就分析了一手

本文所有源码分析基于 Go 1.16.4,阅读时请自行切换版本。

一、Context 介绍

标准库中的 Context 是一个接口,其具体实现有很多种;Context 在 Go 1.7 中被添加入标准库,主要用于跨多个 Goroutine 设置截止时间、同步信号、传递上下文请求值等。

由于需要跨多个 Goroutine 传递信号,所以多个 Context 往往需要关联到一起,形成类似一个树的结构。这种树状的关联关系需要有一个根(root) Context,然后其他 Context 关联到 root Context 成为它的子(child) Context;这种关联可以是多级的,所以在角色上 Context 分为三种:

  • root(根) Context
  • parent(父) Context
  • child(子) Context

二、Context 类型

2.1、Context 的创建

标准库中定义的 Context 创建方法大致如下:

  • context.Background(): 该方法用于创建 root Context,且不可取消
  • context.TODO(): 该方法同样用于创建 root Context(不准确),也不可取消,TODO 通常代表不知道要使用哪个 Context,所以后面可能有调整
  • context.WithCancel(parent Context): 从 parent Context 创建一个带有取消方法的 child Context,该 Context 可以手动调用 cancel
  • context.WithDeadline(parent Context, d time.Time): 从 parent Context 创建一个带有取消方法的 child Context,不同的是当到达 d 时间后该 Context 将自动取消
  • context.WithTimeout(parent Context, timeout time.Duration): 与 WithDeadline 类似,只不过指定的是一个从当前时间开始的超时时间
  • context.WithValue(parent Context, key, val interface{}): 从 parent Context 创建一个 child Context,该 Context 可以存储一个键值对,同时这是一个不可取消的 Context

2.2、Context 内部类型

在阅读源码后会发现,Context 各种创建方法其实主要只使用到了 4 种类型的 Context 实现:

2.2.1、emptyCtx

emptyCtx 实际上就是个 int,其对 Context 接口的主要实现(DeadlineDoneErrValue)全部返回了 nil,也就是说其实是一个 “啥也不干” 的 Context;它通常用于创建 root Context,标准库中 context.Background()context.TODO() 返回的就是这个 emptyCtx

// An emptyCtx is never canceled, has no values, and has no deadline. It is not// struct{}, since vars of this type must have distinct addresses.type emptyCtx intfunc (*emptyCtx) Deadline() (deadline time.Time, ok bool) {return}func (*emptyCtx) Done() <-chan struct{} {return nil}func (*emptyCtx) Err() error {return nil}func (*emptyCtx) Value(key interface{}) interface{} {return nil}func (e *emptyCtx) String() string {switch e {case background:return "context.Background"case todo:return "context.TODO"}return "unknown empty Context"}var (background = new(emptyCtx)todo       = new(emptyCtx))// Background returns a non-nil, empty Context. It is never canceled, has no// values, and has no deadline. It is typically used by the main function,// initialization, and tests, and as the top-level Context for incoming// requests.func Background() Context {return background}// TODO returns a non-nil, empty Context. Code should use context.TODO when// it's unclear which Context to use or it is not yet available (because the// surrounding function has not yet been extended to accept a Context// parameter).func TODO() Context {return todo}

2.2.2、cancelCtx

cancelCtx 内部包含一个 Context 接口实例,还有一个 children map[canceler]struct{};这两个变量的作用就是保证 cancelCtx 可以在 parent Context 和 child Context 两种角色之间转换:

  • 作为其他 Context 实例的 parent Context 时,将其他 Context 实例存储在 children map[canceler]struct{} 中建立关联关系
  • 作为其他 Context 实例的 child Context 时,将其他 Context 实例存储在 “Context” 变量里建立关联

cancelCtx 被定义为一个可以取消的 Context,而由于 Context 的树形结构,当作为 parent Context 取消时需要同步取消节点下所有 child Context,这时候只需要遍历 children map[canceler]struct{} 然后逐个取消即可。

// A cancelCtx can be canceled. When canceled, it also cancels any children// that implement canceler.type cancelCtx struct {Contextmu       sync.Mutex            // protects following fieldsdone     chan struct{}         // created lazily, closed by first cancel callchildren map[canceler]struct{} // set to nil by the first cancel callerr      error                 // set to non-nil by the first cancel call}func (c *cancelCtx) Value(key interface{}) interface{} {if key == &cancelCtxKey {return c}return c.Context.Value(key)}func (c *cancelCtx) Done() <-chan struct{} {c.mu.Lock()if c.done == nil {c.done = make(chan struct{})}d := c.donec.mu.Unlock()return d}func (c *cancelCtx) Err() error {c.mu.Lock()err := c.errc.mu.Unlock()return err}type stringer interface {String() string}func contextName(c Context) string {if s, ok := c.(stringer); ok {return s.String()}return reflectlite.TypeOf(c).String()}func (c *cancelCtx) String() string {return contextName(c.Context) + ".WithCancel"}// cancel closes c.done, cancels each of c's children, and, if// removeFromParent is true, removes c from its parent's children.func (c *cancelCtx) cancel(removeFromParent bool, err error) {if err == nil {panic("context: internal error: missing cancel error")}c.mu.Lock()if c.err != nil {c.mu.Unlock()return // already canceled}c.err = errif c.done == nil {c.done = closedchan} else {close(c.done)}for child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err)}c.children = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c)}}

2.2.3、timerCtx

timerCtx 实际上是在 cancelCtx 之上构建的,唯一的区别就是增加了计时器和截止时间;有了这两个配置以后就可以在特定时间进行自动取消,WithDeadline(parent Context, d time.Time)WithTimeout(parent Context, timeout time.Duration) 方法返回的都是这个 timerCtx

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to// implement Done and Err. It implements cancel by stopping its timer then// delegating to cancelCtx.cancel.type timerCtx struct {cancelCtxtimer *time.Timer // Under cancelCtx.mu.deadline time.Time}func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {return c.deadline, true}func (c *timerCtx) String() string {return contextName(c.cancelCtx.Context) + ".WithDeadline(" +c.deadline.String() + " [" +time.Until(c.deadline).String() + "])"}func (c *timerCtx) cancel(removeFromParent bool, err error) {c.cancelCtx.cancel(false, err)if removeFromParent {// Remove this timerCtx from its parent cancelCtx's children.removeChild(c.cancelCtx.Context, c)}c.mu.Lock()if c.timer != nil {c.timer.Stop()c.timer = nil}c.mu.Unlock()}

2.2.4、valueCtx

valueCtx 内部同样包含了一个 Context 接口实例,目的也是可以作为 child Context,同时为了保证其 “Value” 特性,其内部包含了两个无限制变量 key, val interface{}在调用 valueCtx.Value(key interface{}) 会进行递归向上查找,但是这个查找只负责查找 “直系” Context,也就是说可以无限递归查找 parent Context 是否包含这个 key,但是无法查找兄弟 Context 是否包含。

// A valueCtx carries a key-value pair. It implements Value for that key and// delegates all other calls to the embedded Context.type valueCtx struct {Contextkey, val interface{}}// stringify tries a bit to stringify v, without using fmt, since we don't// want context depending on the unicode tables. This is only used by// *valueCtx.String().func stringify(v interface{}) string {switch s := v.(type) {case stringer:return s.String()case string:return s}return "<not Stringer>"}func (c *valueCtx) String() string {return contextName(c.Context) + ".WithValue(type " +reflectlite.TypeOf(c.key).String() +", val " + stringify(c.val) + ")"}func (c *valueCtx) Value(key interface{}) interface{} {if c.key == key {return c.val}return c.Context.Value(key)}

三、cancelCtx 源码分析

3.1、cancelCtx 是如何被创建的

cancelCtx 在调用 context.WithCancel 方法时创建(暂不考虑其他衍生类型),创建方法比较简单:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {if parent == nil {panic("cannot create context from nil parent")}c := newCancelCtx(parent)propagateCancel(parent, &c)return &c, func() { c.cancel(true, Canceled) }}

newCancelCtx 方法就是将 parent Context 设置到内部变量中,值得分析的是 propagateCancel(parent, &c) 方法和被其调用的 parentCancelCtx(parent Context) (*cancelCtx, bool) 方法,这两个方法保证了 Context 链可以从顶端到底端的及联 cancel,关于这两个方法的分析如下:

// propagateCancel arranges for child to be canceled when parent is.// propagateCancel 这个方法主要负责保证当 parent Context 被取消时,child Context 也会被及联取消func propagateCancel(parent Context, child canceler) {// 针对于 context.Background()/TODO() 创建的 Context(emptyCtx),其 done channel 将永远为 nil// 对于其他的标准的可取消的 Context(cancelCtx、timerCtx) 调用 Done() 方法将会延迟初始化 done channel(调用时创建)// 所以 done channel 为 nil 时说明 parent context 必然永远不会被取消,所以就无需及联到 child Contextdone := parent.Done()if done == nil {return // parent is never canceled}// 如果 done channel 不是 nil,说明 parent Context 是一个可以取消的 Context// 这里需要立即判断一下 done channel 是否可读取,如果可以读取说明上面无锁阶段// parent Context 已经被取消了,那么应该立即取消 child Contextselect {case <-done:// parent is already canceledchild.cancel(false, parent.Err())returndefault:}// parentCancelCtx 用于获取 parent Context 的底层可取消 Context(cancelCtx)//// 如果 parent Context 本身就是 *cancelCtx 或者是标准库中基于 cancelCtx 衍生的 Context 会返回 true// 如果 parent Context 已经取消/或者根本无法取消 会返回 false// 如果 parent Context 无法转换为一个 *cancelCtx 也会返回 false// 如果 parent Context 是一个自定义深度包装的 cancelCtx(自己定义了 done channel) 则也会返回 falseif p, ok := parentCancelCtx(parent); ok { // ok 为 true 说明 parent Context 为 标准库的 cancelCtx 或者至少可以完全转换为 *cancelCtx// 先对 parent Context 加锁,防止更改p.mu.Lock()// 因为 ok 为 true 就已经确定了 parent Context 一定为 *cancelCtx,而 cancelCtx 取消时必然设置 err// 所以并发加锁情况下如果 parent Context 的 err 不为空说明已经被取消了if p.err != nil {// parent has already been canceled// parent Context 已经被取消,则直接及联取消 child Contextchild.cancel(false, p.err)} else {// 在 ok 为 true 时确定了 parent Context 一定为 *cancelCtx,此时 err 为 nil// 这说明 parent Context 还没被取消,这时候要在 parent Context 的 children map 中关联 child Context// 这个 children map 在 parent Context 被取消时会被遍历然后批量调用 child Context 的取消方法if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()} else { // ok 为 false,说明: "parent Context 已经取消" 或 "根本无法取消" 或 "无法转换为一个 *cancelCtx" 或 "是一个自定义深度包装的 cancelCtx"atomic.AddInt32(&goroutines, +1)// 由于代码在方法开始时就判断了 parent Context "已经取消"、"根本无法取消" 这两种情况// 所以这两种情况在这里不会发生,因此 <-parent.Done() 不会产生 panic// // 唯一剩下的可能就是 parent Context "无法转换为一个 *cancelCtx" 或 "是一个被覆盖了 done channel 的自定义 cancelCtx"// 这种两种情况下无法通过 parent Context 的 children map 建立关联,只能通过创建一个 Goroutine 来完成及联取消的操作go func() {select {case <-parent.Done():child.cancel(false, parent.Err())case <-child.Done():}}()}}// parentCancelCtx returns the underlying *cancelCtx for parent.// It does this by looking up parent.Value(&cancelCtxKey) to find// the innermost enclosing *cancelCtx and then checking whether// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx// has been wrapped in a custom implementation providing a// different done channel, in which case we should not bypass it.)// parentCancelCtx 负责从 parent Context 中取出底层的 cancelCtxfunc parentCancelCtx(parent Context) (*cancelCtx, bool) {// 如果 parent context 的 done 为 nil 说明不支持 cancel,那么就不可能是 cancelCtx// 如果 parent context 的 done 为 可复用的 closedchan 说明 parent context 已经 cancel 了// 此时取出 cancelCtx 没有意义(具体为啥没意义后面章节会有分析)done := parent.Done()if done == closedchan || done == nil {return nil, false}// 如果 parent context 属于原生的 *cancelCtx 或衍生类型(timerCtx) 需要继续进行后续判断// 如果 parent context 无法转换到 *cancelCtx,则认为非 cancelCtx,返回 nil,faslep, ok := parent.Value(&cancelCtxKey).(*cancelCtx)if !ok {return nil, false}p.mu.Lock()// 经过上面的判断后,说明 parent context 可以被转换为 *cancelCtx,这时存在多种情况://   - parent context 就是 *cancelCtx//   - parent context 是标准库中的 timerCtx//   - parent context 是个自己自定义包装的 cancelCtx//// 针对这 3 种情况需要进行判断,判断方法就是: //   判断 parent context 通过 Done() 方法获取的 done channel 与 Value 查找到的 context 的 done channel 是否一致// // 一致情况说明 parent context 为 cancelCtx 或 timerCtx 或 自定义的 cancelCtx 且未重写 Done(),// 这种情况下可以认为拿到了底层的 *cancelCtx// // 不一致情况说明 parent context 是一个自定义的 cancelCtx 且重写了 Done() 方法,并且并未返回标准 *cancelCtx 的// 的 done channel,这种情况需要单独处理,故返回 nil, falseok = p.done == donep.mu.Unlock()if !ok {return nil, false}return p, true}

3.2、cancelCtx 是如何取消的

在上面的 cancelCtx 创建源码中可以看到,cancelCtx 内部跨多个 Goroutine 实现信号传递其实靠的就是一个 done channel;如果要取消这个 Context,那么就需要让所有 <-c.Done() 停止阻塞,这时候最简单的办法就是把这个 channel 直接 close 掉,或者干脆换成一个已经被 close 的 channel,事实上官方也是怎么做的。

// cancel closes c.done, cancels each of c's children, and, if// removeFromParent is true, removes c from its parent's children.func (c *cancelCtx) cancel(removeFromParent bool, err error) {    // 首先判断 err 是不是 nil,如果不是 nil 则直接 panic    // 这么做的目的是因为 cancel 方法是个私有方法,标准库内任何调用 cancel    // 的方法保证了一定会传入 err,如果没传那就是非正常调用,所以可以直接 panicif err == nil {panic("context: internal error: missing cancel error")}// 对 context 加锁,防止并发更改c.mu.Lock()// 如果加锁后有并发访问,那么二次判断 err 可以防止重复 cancel 调用if c.err != nil {c.mu.Unlock()return // already canceled}// 这里设置了内部的 err,所以上面的判断 c.err != nil 与这里是对应的// 也就是说加锁后一定有一个 Goroutine 先 cannel,cannel 后 c.err 一定不为 nilc.err = err// 判断内部的 done channel 是不是为 nil,因为在 context.WithCancel 创建 cancelCtx 的// 时候并未立即初始化 done channel(延迟初始化),所以这里可能为 nil// 如果 done channel 为 nil,那么就把它设置成共享可重用的一个已经被关闭的 channelif c.done == nil {c.done = closedchan} else { // 如果 done channel 已经被初始化,则直接 close 它close(c.done)}// 如果当前 Context 下面还有关联的 child Context,且这些 child Context 都是// 可以转换成 *cancelCtx 的 Context(见上面的 propagateCancel 方法分析),那么// 直接遍历 childre map,并且调用 child Context 的 cancel 即可// 如果关联的 child Context 不能转换成 *cancelCtx,那么由 propagateCancel 方法// 中已经创建了单独的 Goroutine 来关闭这些 child Contextfor child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err)}// 清除 c.children map 并解锁c.children = nilc.mu.Unlock()    // 如果 removeFromParent 为 true,那么从 parent Context 中清理掉自己if removeFromParent {removeChild(c.Context, c)}}

3.3、parentCancelCtx 为什么不取出已取消的 cancelCtx

在上面的 3.1 章节中分析 parentCancelCtx 方法时有这么一段:

func parentCancelCtx(parent Context) (*cancelCtx, bool) {// 如果 parent context 的 done 为 nil 说明不支持 cancel,那么就不可能是 cancelCtx// 如果 parent context 的 done 为 可复用的 closedchan 说明 parent context 已经 cancel 了// 此时取出 cancelCtx 没有意义(具体为啥没意义后面章节会有分析)done := parent.Done()if done == closedchan || done == nil {return nil, false}// ...... 省略}

现在来仔细说明一下 “为什么没有意义?” 这个问题:

首先是调用 parentCancelCtx 方法的位置,在 context 包中只有两个位置调用了 parentCancelCtx 方法;一个是在创建 cancelCtx 的 func WithCancel(parent Context)propagateCancel(parent, &c) 方法中,另一个就是 cancel 方法的 removeChild(c.Context, c) 调用中;下面分析一下这两个方法的目的。

3.3.1、propagateCancel(parent, &c)

propagateCancel 负责保证当 parent cancelCtx 在取消时能正确传递到 child Context;那么它需要通过 parentCancelCtx 来确定 parent Context 是否是一个 cancelCtx,如果是那就把 child Context 加到 parent Context 的 children map 中,然后 parent Context 在 cancel 时会自动遍历 map 调用 child Context 的 cancel;如果不是那就开 Goroutine 阻塞读 parent Context 的 done channel然后再调用 child Context 的 cancel。

if p, ok := parentCancelCtx(parent); ok {    p.mu.Lock()    if p.err != nil {        // parent has already been canceled        child.cancel(false, p.err)    } else {        if p.children == nil {            p.children = make(map[canceler]struct{})        }        p.children[child] = struct{}{}    }    p.mu.Unlock()} else {    atomic.AddInt32(&goroutines, +1)    go func() {        select {        case <-parent.Done():            child.cancel(false, parent.Err())        case <-child.Done():        }    }()}

所以在这个方法调用时,如果 parentCancelCtx 取出一个已取消的 cancelCtx,那么 parent Context 的 children map 在 cancel 时已经清空了,这时要是再给设置上就有问题了,同样业务需求中 propagateCancel 为了就是控制传播,明明 parent Context 已经 cancel 了,再去传播就没意义了。

3.3.2、removeChild(c.Context, c)

同上面的 3.3.1 一样,**removeChild(c.Context, c) 目的是在 cancel 时断开与 parent Context 的关联,同样是为了处理 children map 的问题;此时如果 parentCancelCtx 也取出一个已经 cancel 的 parent Context,由于 parent Context 在 cancel 时已经清空了 childre map,这里再尝试 remove 也没有任何意义。**

// removeChild removes a context from its parent.func removeChild(parent Context, child canceler) {p, ok := parentCancelCtx(parent)if !ok {return}p.mu.Lock()if p.children != nil {delete(p.children, child)}p.mu.Unlock()}

四、timerCtx 源码分析

4.1、timerCtx 是如何创建的

timerCtx 的创建主要通过 context.WithDeadline 方法,同时 context.WithTimeout 实际上也是调用的 context.WithDeadline:

// WithDeadline returns a copy of the parent context with the deadline adjusted// to be no later than d. If the parent's deadline is already earlier than d,// WithDeadline(parent, d) is semantically equivalent to parent. The returned// context's Done channel is closed when the deadline expires, when the returned// cancel function is called, or when the parent context's Done channel is// closed, whichever happens first.//// Canceling this context releases resources associated with it, so code should// call cancel as soon as the operations running in this Context complete.func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {    // 与 cancelCtx 一样先检查一下 parent Contextif parent == nil {panic("cannot create context from nil parent")}        // 判断 parent Context 是否支持 Deadline,如果支持的话需要判断 parent Context 的截止时间    // 假设 parent Context 的截止时间早于当前设置的截止时间,那就意味着 parent Context 肯定会先    // 被 cancel,同样由于 parent Context 的 cancel 会导致当前这个 child Context 也会被 cancel    // 所以这时候直接返回一个 cancelCtx 就行了,计时器已经没有必要存在了if cur, ok := parent.Deadline(); ok && cur.Before(d) {// The current deadline is already sooner than the new one.return WithCancel(parent)}    // 创建一个 timerCtxc := &timerCtx{cancelCtx: newCancelCtx(parent),deadline:  d,}    // 与 cancelCtx 一样的传播操作propagateCancel(parent, c)    // 判断当前时间已经已经过了截止日期,如果超过了直接 canceldur := time.Until(d)if dur <= 0 {c.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(false, Canceled) }}    // 所有 check 都没问题的情况下,创建一个定时器,在到时间后自动 cancelc.mu.Lock()defer c.mu.Unlock()if c.err == nil {c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }}

4.2、timerCtx 是如何取消的

了解了 cancelCtx 的取消流程以后再来看 timerCtx 的取消就相对简单的多,主要就是调用一下里面的 cancelCtx 的 cancel,然后再把定时器停掉:

func (c *timerCtx) cancel(removeFromParent bool, err error) {c.cancelCtx.cancel(false, err)if removeFromParent {// Remove this timerCtx from its parent cancelCtx's children.removeChild(c.cancelCtx.Context, c)}c.mu.Lock()if c.timer != nil {c.timer.Stop()c.timer = nil}c.mu.Unlock()}

五、valueCtx 源码分析

相对于 cancelCtx 还有 timerCtx,valueCtx 实在是过于简单,因为它没有及联的取消逻辑,也没有过于复杂的 kv 存储:

// WithValue returns a copy of parent in which the value associated with key is// val.//// Use context Values only for request-scoped data that transits processes and// APIs, not for passing optional parameters to functions.//// The provided key must be comparable and should not be of type// string or any other built-in type to avoid collisions between// packages using context. Users of WithValue should define their own// types for keys. To avoid allocating when assigning to an// interface{}, context keys often have concrete type// struct{}. Alternatively, exported context key variables' static// type should be a pointer or interface.// WithValue 方法负责创建 valueCtxfunc WithValue(parent Context, key, val interface{}) Context {    // parent 检测if parent == nil {panic("cannot create context from nil parent")}    // key 检测if key == nil {panic("nil key")}    // key 必须是可比较的if !reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}}// A valueCtx carries a key-value pair. It implements Value for that key and// delegates all other calls to the embedded Context.type valueCtx struct {Contextkey, val interface{}}// stringify tries a bit to stringify v, without using fmt, since we don't// want context depending on the unicode tables. This is only used by// *valueCtx.String().func stringify(v interface{}) string {switch s := v.(type) {case stringer:return s.String()case string:return s}return "<not Stringer>"}func (c *valueCtx) String() string {return contextName(c.Context) + ".WithValue(type " +reflectlite.TypeOf(c.key).String() +", val " + stringify(c.val) + ")"}func (c *valueCtx) Value(key interface{}) interface{} {    // 先判断当前 Context 里有没有这个 keyif c.key == key {return c.val}    // 如果没有递归向上查找return c.Context.Value(key)}

六、结尾

分析 Context 源码断断续续经历了 3、4 天,说心里话发现里面复杂情况有很多,网上其他文章很多都是只提了一嘴,但是没有深入具体逻辑,尤其是 cancelCtx 的相关调用;我甚至觉得我有些地方可能理解的也不完全正确,目前就先写到这里,如果有不对的地方欢迎补充。

]]>
Golang Golang Context http://mritd.com/2021/06/27/golang-context-source-code/#disqus_thread
Caddy2 的 ListenerWrapper http://mritd.com/2021/06/15/caddy2-listenerwrapper/ http://mritd.com/2021/06/15/caddy2-listenerwrapper/ Tue, 15 Jun 2021 06:04:00 GMT 最近分析了一下 Caddy2 的 ListenerWrapper,觉得很厉害索性写下来

本文所有源码分析基于 Caddy2 v2.4.2 版本进行,未来版本可能源码会有变化,阅读本文时请自行将源码切换到 v2.4.2 版本。

一、这玩意是什么?

Caddy2 对配置文件中的 listener_wrappers 配置有以下描述:

Allows configuring listener wrappers, which can modify the behaviour of the base listener. They are applied in the given order.

同时对于 tls 这个 listener_wrappers 还做了一下说明:

There is a special no-op tls listener wrapper provided as a standard module which marks where TLS should be handled in the chain of listener wrappers. It should only be used if another listener wrapper must be placed in front of the TLS handshake.

综上所述,简单的理解就是 listener_wrappers 在 Caddy2 中用于改变链接行为,这个行为可以理解为我们可以自定义接管链接,这些 “接管” 更偏向于底层,比如在 TLS 握手之前做点事情或者在 TLS 握手之后做点事情,这样我们就可以实现一些魔法操作。

二、加载与初始化

在 Caddy2 启动时首先会进行配置文件解析,例如解析 Caddyfile、json 等格式的配置文件,listener_wrappers 在配置文件中被定义为一个 ServerOption:

caddyconfig/httpcaddyfile/serveroptions.go:47

该配置最终会被注入到 Server 的 listenerWrappers 属性中(先解析为 ListenerWrappersRaw 然后再实例化)

modules/caddyhttp/server.go:132

最后在 App 的启动过程中遍历 listenerWrappers 并逐个应用,在应用 listenerWrappers 时有个比较重要的顺序处理:

首先 Caddy2 会尝试在 net.Listener 上应用一部分 listenerWrappers,当触及到 tls 这个 token 的 listenerWrappers 之后终止应用;终止前已被应用的这部分 listenerWrappers 被认为是 TLS 握手之前的自定义处理,然后在 TLS 握手之后再次应用剩下的 listenerWrappers,后面这部分被认为是 TLS 握手之后的自定义处理。

modules/caddyhttp/app.go:318

最终对 ListenerWrapper 加载流程分析如下:

  • 首先解析配置文件,并将配置转化为 Server 的 ListenerWrappersRaw []json.RawMessage
  • 然后通过 ctx.LoadModule(srv, "ListenerWrappersRaw") 实例化 ListenerWrapper
  • ctx.LoadModule 时,如果发现了 tls 指令则按照配置文件顺序排序 ListenerWrapper 切片,否则将 tls 这个特殊的 ListenerWrapper 放在首位;这意味着在配置中不写 tls 时,所有 ListenerWrapper 永远处于 TLS 握手之后
  • 最后在 App 启动时按照切片顺序应用 ListenerWrapper,需要注意的是 ListenerWrapper 接口针对的是 net.Listener 的处理,其底层是 net.Conn这意味着 ListenerWrapper 不会对 UDP(net.PacketConn) 做处理,代码中也可以看到 ListenerWrapper 并未对 HTTP3 处理

三、具体实际应用

说了半天,也分析了源码,那么最终回到问题原点: ListenerWrapper 能干什么?答案就是自定义协议,例如神奇的 caddy-trojan 插件。

caddy-trojan 插件实现了 ListenerWrapper,在 App 启动时通过源码可以看到,TLS 握手完成后原始的 TCP 链接将交由这个 ListenerWrapper 处理:

// finish wrapping listener where we left off before TLSfor i := lnWrapperIdx; i < len(srv.listenerWrappers); i++ {ln = srv.listenerWrappers[i].WrapListener(ln)}

该插件对 WrapListener 方法的实现如下:

// WrapListener implements caddy.ListenWrapperfunc (m *ListenerWrapper) WrapListener(l net.Listener) net.Listener {ln := NewListener(l, m.upstream, m.logger)// 异步后台捕获新链接go ln.loop()return ln}

所以这个 wrapper 核心处理在 loop() 中:

// loop is ...func (l *Listener) loop() {for {conn, err := l.Listener.Accept()if err != nil {select {case <-l.closed:returndefault:l.logger.Error(fmt.Sprintf("accept net.Conn error: %v", err))}continue}go func(c net.Conn, lg *zap.Logger, up *Upstream) {b := make([]byte, HeaderLen+2)if _, err := io.ReadFull(c, b); err != nil {if errors.Is(err, io.EOF) {lg.Error(fmt.Sprintf("read prefix error: read tcp %v -> %v: read: %v", c.RemoteAddr(), c.LocalAddr(), err))} else {lg.Error(fmt.Sprintf("read prefix error: %v", err))}c.Close()return}// check the net.Connif ok := up.Validate(ByteSliceToString(b[:HeaderLen])); !ok {select {case <-l.closed:c.Close()default:l.conns <- &rawConn{Conn: c, r: bytes.NewReader(b)}}return}defer c.Close()lg.Info(fmt.Sprintf("handle trojan net.Conn from %v", c.RemoteAddr()))nr, nw, err := Handle(io.Reader(c), io.Writer(c))if err != nil {lg.Error(fmt.Sprintf("handle net.Conn error: %v", err))}up.Consume(ByteSliceToString(b[:HeaderLen]), nr, nw)}(conn, l.logger, l.upstream)}}

可以看到,当新链接进入时,首先对包头做检测 if ok := up.Validate(ByteSliceToString(b[:HeaderLen]));如果检测通过那么这个链接就完全插件自己处理后续逻辑了;如果不通过则将此链接返回给 Caddy2,让 Caddy2 继续处理。

这里面涉及到一个一开始让我不解的问题: “链接不可重复读”,后来看源码才明白作者处理方式很简单: 包装一个 rawConn,在验证部分由于已经读了一点数据,如果验证不通过就把它存起来,然后让下一个读操作先读这个 buffer,从而实现原始数据组装。

// rawConn is ...type rawConn struct {net.Connr *bytes.Reader}// Read is ...func (c *rawConn) Read(b []byte) (int, error) {if c.r == nil {return c.Conn.Read(b)}n, err := c.r.Read(b)if errors.Is(err, io.EOF) {c.r = nilreturn n, nil}return n, err}

四、思考和总结

ListenerWrapper 是 Caddy2 一个强大的扩展能力,在 ListenerWrapper 基础上我们可以实现对 TCP 链接自定义处理,我们因此可以创造一些奇奇怪怪的协议。同时我们通过让链接重新交由 Caddy2 处理又能做到完美的伪装: 当你去尝试访问时,如果密码学验证不通过,那么后续行为就与标准 Caddy2 表现一致,主动探测基本无效。对任何自己创造的 ListenerWrapper 来说,如果开启了类似 AEAD 这种加密,探测行为本身就会被转接到对抗密码学原理上。

]]>
Golang Caddy Caddy http://mritd.com/2021/06/15/caddy2-listenerwrapper/#disqus_thread
JetBrains 常用插件 http://mritd.com/2021/06/06/jetbrains-plugins/ http://mritd.com/2021/06/06/jetbrains-plugins/ Sun, 06 Jun 2021 05:23:00 GMT 分享一些好玩的和有用的 JetBrains 系列 IDE 插件

所谓工欲善其事,必先利其器;这篇文章分享一些日常 Coding 中常用 JetBrains 系列 IDE 插件(本文所有插件可直接从 Marketplace 搜索并安装)。

One Dark theme

上来先整点没用的吧,主题配色这个东西根据个人喜好;我比较喜欢花花绿绿的感觉,在防止眼疲劳的同时还有点 RMB 的感觉(RMB 也花花绿绿的),毕竟 Coding 的时候要 “酷一点”,然后才能心情愉悦的写 BUG(代码和我只要有一个能跑就行)。

Gopher

也是啥用没有的插件,唯一的效果就是在各种 Loading 的时候进度条变成了 Go 的吉祥物(Golang 天下第一,嘶吼)…当然还有个同款,Gopher 变成了彩虹猫,需要的自己搜吧。

Extra Icons

这个插件为项目里一些特殊文件增加 Icon,比如 .gitignore 文件、CI 配置等,可以让人看着更舒服一些。

GitToolBox

GitToolBox 会在光标定位到某一行代码时显示其最近改动等提交信息,方便在甩锅时精确定位到背锅侠。

String Manipulation

这个就牛逼了,非常有实用价值的一个插件;当某些规范下你必须进行 “驼峰/下划线/短横线/全大写/全小写/大写加下划线/小写加下划线…” 疯狂转换时,String Manipulation 只需要选中右键即可完成。

Randomness

Randomness 在需要测试数据时很有用,它可以快速生成一些随机性的垃圾数据填充进来;例如写示例配置时。

Translation

英文渣必备插件,除了能翻译一些单词之外,还能自动识别文档进行翻译,甚至还带单词本。

carbon-now-sh

当你想把你的代码发到某个群里装X,或者写博客不想让别人复制只想展示时,carbon-now-sh 可以帮你把选中的代码传输到 http://carbon.now.sh/ 来生成漂亮的图片。

]]>
Golang JetBrains http://mritd.com/2021/06/06/jetbrains-plugins/#disqus_thread
寥寥浮生,莫问前程 http://mritd.com/2021/06/04/liao-liao-fu-sheng-mo-wen-qian-cheng/ http://mritd.com/2021/06/04/liao-liao-fu-sheng-mo-wen-qian-cheng/ Fri, 04 Jun 2021 13:41:00 GMT 月下映双鱼,白首雁归去,自古无愁难成句。 --- 双鱼 Your browser does not support the audio element.

月下映双鱼,白首雁归去,自古无愁难成句。 --- 双鱼

给岁月以青春,而不是给青春以岁月

给时光以生命,而不是给生命以时光。

每当谈起岁月,总显得是那么残忍,残忍到能真切的感受到 “岁月如刀”,然后一刀又一刀;今天莫名其妙的看了一下我的生日(是的,已经忘记了年龄),然后得到了一个很有意思的答案:

在这个世界上生活了 9684 天,生命进度条以 80 岁算正好 33%

所以我的青春已经开始向我挥手告别,或许有些艰难,或许有点感慨,但终究都已不再重要。我庆幸的是我的青春不会像大多数人一样无趣,虽然充满血腥却显得美丽;但同样也会留下许多遗憾,缺了孩子气的天真,没体会过关心,没了热血冲动的经历。

以前每当回想起来的时候,总是带着那么一点悲伤;今天突然想起一句话: “给时光以生命,而不是给生命以时光。”,所以当我们回首青春,应当给岁月以青春,而不是给青春以岁月;这段青春,感谢你砥砺前行,让岁月不在苦涩。

勇敢的质疑,勇敢的被质疑

世上没有绝对的真理,这就是我要对你们说的话。

质疑事物的能力,看似简单,我却缺少了太多;父母曾经无数次对我说过 “你应该怎样怎样…”,小到吃饭喝水,大到人生规划,最后事实证明基本没有对的;有些看似亘古不变的东西让我忘却了它存在的意义,以至于我选择了随波逐流,却从未质疑过一个基本的对与错。

同样,这个世界不只有黑与白,还有很多灰。别人的对我质疑是否真正重要?这是否是自己放弃的理由?…让质疑我的人去质疑吧,这是他的权利,但我活不成他的样子。

这世界上从来就没有绝对的真理,浩瀚星河里我太过渺小,而命运又太过伟大;我抱着敬畏之心质疑一切,也接受这个世界对我的质疑。

对未知的结论永远应该是未知

我们用眼睛观察,而不全然接受。

我曾不止一次对只是看到表象的事物作出定论,以为笑容背后就是开心,以为乌云密布就会下雨。我把太多的归因算到我所看到或是模糊看到的东西上,当 “墨菲定律” 和 “幸存者偏差” 双重作用时,悲观的人会更悲观,快乐的人会更快乐。

现在人到中年,终于学会了放弃;我所看到的不一定是真的,我没看清的也不一定是假的,对待未知的结论永远应该是未知,所以赌不赌看自己,莫要让整个世界买单。

成功无法复制,经验或许没用

世界上没有完全相同的两片树叶。

太多的人喜欢对别人讲述 “自己的经验”,一件事怎么做才能成功,甚至认为自己是 “苦口婆心”。我曾经也做过类似的傻事,直到前些年遇到了一个出租车司机;

他以前是一个搞自动化测试的,几年前跟我一样一个人来到北京打拼,我们有着差不多相似的经历,但结果却并不相同也不相似;直到现在我还记得那一段路上他所诉说的艰苦和努力,还有命运的不公。

我那时才意识到: 成功根本无法复制,经验也或许没用,别人的一生之所以那样只是命运垂青,或是开了个玩笑。怎么做事,我无法说服别人,也没人可以教导我。

对抗随机性等于向既定轨道回归

已经在开往地狱的路上,那就应该和魔鬼一起笑着去。

这是前两天在 Twitter 上看到老刘(@Yachen Liu)发的感悟:

对于多数人,大概在初中至大学阶段,思维方式、思维能力、性格等终身基本不变的特征就已经定型了,与此对应的人生未来轨道也已经基本确定,之后的时间不过是不断的对抗随机性向既定轨道回归。

我很认同这句话,环境会改变人的,很多性格、习惯其实都是那个懵懵懂懂的年纪养成的;所以现在开始不去尝试对抗随机性,给自己一点机会,我期望我短暂的生命中会有些其他的东西,因为我不想回到以前的轨道上。

一切达观,都是对悲苦的省略

写到最后,以大雪将至里的话结尾吧:

和所有的人一样,在他的一生里,也曾怀有过自己的想象和梦想,其中的一些是他自己实现的,有一些是命运赠予给他的,很多是从来都无法实现的,或者是刚刚得到,就又被从手中掠夺走的。但是他一直还活着。

他想不起来,他是从哪儿来的,最终他也不知道,他将要去向何方。但是,这生来死去之间的时光,他的一生,他可以不含遗憾地去回看,用一个戛然而止的微笑,然后就只是巨大的惊讶。

BGM 取自 “石进 - 夜的钢琴曲“ 系列第九曲。

]]>
随笔 浮生 http://mritd.com/2021/06/04/liao-liao-fu-sheng-mo-wen-qian-cheng/#disqus_thread
搓一个 Telegram Bot http://mritd.com/2021/06/03/make-a-telegram-bot/ http://mritd.com/2021/06/03/make-a-telegram-bot/ Thu, 03 Jun 2021 04:20:00 GMT Telegram 确实好用,搓个 Bot 也好玩

在一个月光如雪的晚上和 PMheart 在 Telegram 闲聊,突然发现群里一个人的昵称是当前时间,然后观察一会儿发现还在不停变化… 最可气的是他还弄个 “东半球🌏最准报时” 的头衔,我一开始以为是 Telegram 又出的什么 “高级功能”(毕竟微信炸💩都是 Telegram 好久之前玩剩下的),几经 Google 我发现其实就是自己写个 Bot,然后定时 rename;那我要不整个 “东半球最浪漫诗人” 岂不是太面了🤔。

一、Telegram Bot 介绍

在 Telegram 官方的文档描述中,其 Bot Api 实质上分为两种,这两种 Api 用途也各不相同:

1.1、标准 Bot

由用户自行联系 BotFather (人如其名)交互式创建,该 Bot 是官方所认为的标准 Bot,其主要目的就是作为一个真正的 Bot;我们可以通过一个 Token 调用 Telegram Api 来控制它,玩法很多,包括不限于发送告警、作为群管机器人、交互式的帮你做各种自动化等等;同时这个 Bot 具有严格的隐私权限控制,比如拉到群里可以控制 Bot 对群消息是否可见等等(Telegram 这点做的非常 OJBK);借助于这类 Bot,也有些脑洞大开的大哥在浪的边缘疯狂试探,比如下面的扫雷机器人:

I3IcQn

还有算命的:
pIZYSS

Github 真正干活的:
9wPxeI

我的证书告警:
HAvQny

当然肯定有高铁动车组的(🔞我是正经人):
1zmI2m

1.2、客户端 Bot

准确的官方介绍是 TDLib – build your own Telegram,从这个介绍可以看出,这一个 “Bot” Api 本质上并不是让你写 Bot,而是作为开发一个第三方 Telegram 客户端用的;所以这个 Api 的权限很大,可以完整的模拟一个用户;目前我发现被滥用最多的就是用这个 Api 作为恶意拉人、发广告等,简直是币圈割韭菜御用。

TDLib 本质上是一个 C++ 的 lib,官方提供了引导页面来帮助你用主流语言跨语言调用来使用它:

yTAC3k

二、标准 Bot 使用

标准 Bot 使用相对简单,按照官方文档跟 BotFather 聊天创建一个即可:

gPhI8O

当创建完成后在 Bot 设置界面你可以获取一个 Token,使用这个 Token 连接 Bot Api 地址就可以开始控制你的 Bot;Golang 开发可以考虑使用 http://github.com/tucnak/telebot 这个库,用法相当简单:

package mainimport ("log""time"tb "gopkg.in/tucnak/telebot.v2")func main() {b, err := tb.NewBot(tb.Settings{// You can also set custom API URL.// If field is empty it equals to "http://api.telegram.org".URL: "http://195.129.111.17:8012",Token:  "TOKEN_HERE",Poller: &tb.LongPoller{Timeout: 10 * time.Second},})if err != nil {log.Fatal(err)return}b.Handle("/hello", func(m *tb.Message) {b.Send(m.Sender, "Hello World!")})b.Start()}

基于这个库,我为了方便使用写了一个命令行小工具,方便我发送告警信息等: tgsend

BCSMIV

warOjJ

三、客户端 Api 使用

标准 Bot Api 很丰富,日常干活啥的也完全能满足,但是!人如果不会装X那和咸鱼有什么区别?我的 “东半球最浪漫诗人” 得提上日程。

3.1、TDLib 构建

关于 Api 易用性,开发生态环境,这一点说实话,Telegram 能把所有国内 IM 按在地上摩擦,就像这样:

iI1jz8

Telegram 官方提供了完整的 “点一点” 构建 TDLib 引导页面: http://tdlib.github.io/td/build.html

勾选好自己的语言、操作系统、系统版本、甚至是编译的内存大小等设置后,无脑复制下面的命令执行就行:

DnZdvD

3.2、Telegram API HASH

TDLib 构建完成后,需要自行申请一个 API_HASH,API_HASH 类似一个让 Telegram 识别你的客户端的 “合法标识”;API_HASH 申请需要登陆 http://my.telegram.org/,然后选择 API development tools:

qp2BjK

然后填写相关信息,最后 Telegram 就会为你生成好 API_HASH:

QGoh8Q

3.3、TDLib 使用

TDLib 构建好了,API HASH 也有了,那么根据自己选择的语言找一个靠谱的 SDK 使用即可;比如 Golang 开发,我选择了 http://github.com/Arman92/go-tdlib,这个库使用相当简单:

package mainimport ("fmt""github.com/Arman92/go-tdlib")func main() {tdlib.SetLogVerbosityLevel(1)tdlib.SetFilePath("./errors.txt")// Create new instance of clientclient := tdlib.NewClient(tdlib.Config{APIID:               "187786",APIHash:             "e782045df67ba48e441ccb105da8fc85",SystemLanguageCode:  "en",DeviceModel:         "Server",SystemVersion:       "1.0.0",ApplicationVersion:  "1.0.0",UseMessageDatabase:  true,UseFileDatabase:     true,UseChatInfoDatabase: true,UseTestDataCenter:   false,DatabaseDirectory:   "./tdlib-db",FileDirectory:       "./tdlib-files",IgnoreFileNames:     false,})for {currentState, _ := client.Authorize()if currentState.GetAuthorizationStateEnum() == tdlib.AuthorizationStateWaitPhoneNumberType {fmt.Print("Enter phone: ")var number stringfmt.Scanln(&number)_, err := client.SendPhoneNumber(number)if err != nil {fmt.Printf("Error sending phone number: %v", err)}} else if currentState.GetAuthorizationStateEnum() == tdlib.AuthorizationStateWaitCodeType {fmt.Print("Enter code: ")var code stringfmt.Scanln(&code)_, err := client.SendAuthCode(code)if err != nil {fmt.Printf("Error sending auth code : %v", err)}} else if currentState.GetAuthorizationStateEnum() == tdlib.AuthorizationStateWaitPasswordType {fmt.Print("Enter Password: ")var password stringfmt.Scanln(&password)_, err := client.SendAuthPassword(password)if err != nil {fmt.Printf("Error sending auth password: %v", err)}} else if currentState.GetAuthorizationStateEnum() == tdlib.AuthorizationStateReadyType {fmt.Println("Authorization Ready! Let's rock")break}}// Main loopfor update := range client.RawUpdates {// Show all updatesfmt.Println(update.Data)fmt.Print("\n\n")}}

由于 Telegram 是允许多客户端登陆的(跟我一起喊:微信垃圾、张小龙垃圾),所以使用 TDLib 我们可以完全控制我们的账户行为;那么 “东半球最浪漫诗人” 实现就相对简单:

核心代码就这几行:

// 一个定时任务工具cn := cron.New()// 默认 30s 执行一次_, err = cn.AddFunc(c.String("cron"), func() {    rand.Seed(time.Now().Unix())    // 随机取一句诗    name := data[rand.Intn(len(data)-1)]    logger.Infof("update name to [%s]...", name)    // 调用 TDLib 改名    _, err := client.SetName(name, "")    if err != nil {        logger.Error(err)    }})

效果嘛,就这样:

A15b2P

3MXSCI

]]>
Golang Golang Telegram http://mritd.com/2021/06/03/make-a-telegram-bot/#disqus_thread
nerdctl 初试 http://mritd.com/2021/06/01/nerdctl-test/ http://mritd.com/2021/06/01/nerdctl-test/ Tue, 01 Jun 2021 13:46:00 GMT 今天突然想起了 nerdctl,装一个玩玩... 自从 Containerd 发布 1.5 以后,nerdctl 工具配合 Containerd 的情况下基本已经可以替换掉 Docker 和 Docker Compose;由于天下苦 Docker 久已,没忍住今天试了试。

一、nerdctl 安装

nerdctl 官方发布包包含两个安装版本:

  • Minimal: 仅包含 nerdctl 二进制文件以及 rootless 模式下的辅助安装脚本
  • Full: 看名字就能知道是个全量包,其包含了 Containerd、CNI、runc、BuildKit 等完整组件

这时候用脚趾头想我都要一把梭,在一把梭之前先卸载以前安装的 Docker 以及 Containerd 等组件(以下以 Ubuntu 20.04 为例):

apt purge docker.io containerd -y

然后下载安装包解压启动即可(一把梭真香):

# 下载压缩包wget http://github.com/containerd/nerdctl/releases/download/v0.8.2/nerdctl-full-0.8.2-linux-amd64.tar.gz# 解压安装tar Cxzvvf /usr/local nerdctl-full-0.8.2-linux-amd64.tar.gz# 启动 containerd 和 buildkitdsystemctl enable --now containerdsystemctl enable --now buildkit

二、使用

启动完成后就可以通过 ctrcrictl 命令测试 containerd 是否工作正常了;没问题的话继续折腾 nerdctl

2.1、Docker CLI 兼容

Docker CLI 的兼容具体情况可以从 http://github.com/containerd/nerdctl#command-reference 中查看相关说明;既然是为了兼容 Docker CLI,那么在运行时只需要把 docker 命令换成 nerdctl 命令即可:

vm.node ➜ ~ nerdctl run -d --name test -p 8080:80 nginx:alpine80342ff329574ab290c212b2b786b52dd0c3f3209ee8e9e06878259dd1186879vm.node ➜  ~ nerdctl psCONTAINER ID    IMAGE                             COMMAND                   CREATED          STATUS    PORTS                   NAMES80342ff32957    docker.io/library/nginx:alpine    "/docker-entrypoint.…"    3 seconds ago    Up        0.0.0.0:8080->80/tcp    testvm.node ➜ ~ curl 10.0.0.5:8080<!DOCTYPE html><html><head><title>Welcome to nginx!</title><style>    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 andworking. 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>

唯一需要注意的是部分命令选项还是有一定不兼容,比如 run 的时候 -d-t 不能一起用,--restart 策略不支持等,但是通过列表可以看到大部分 cli 都已经完成了。

2.2、Docker Compose 兼容

由于环境不同吧,说实话 Docker Compose 兼容才是吸引最大的一点;因为现实环境中很少有直接 docker run... 这么干的,大部分不重要服务都是通过 docker-compose 启动的;而目前来说 nerdctl 配合 CNI 等已经完成了大部分 Compose 的兼容:

docker-compose.yaml

version: '3.7'services:  cloudreve:    image: mritd/cloudreve:relativepath    container_name: cloudreve    restart: always    ports:    - "5212:5212"    - "5443:5443"    volumes:    - ./config:/etc/cloudreve    - data:/data    - shared:/downloads    command: ["-c","/etc/cloudreve/conf.ini"]volumes:  shared:  data:

运行测试:

vm.node ➜ nerdctl compose up -dINFO[0000] Creating network test_defaultINFO[0000] Ensuring image mritd/cloudreve:relativepathINFO[0000] Creating container cloudreve

不过目前比较尴尬的是 compose 还不支持 ps 命令,同时如果 volume 了宿主机目录,如果目录不存在也不会自动创建;logs 命令似乎也有 BUG。

三、总结

nerdctl 目前还有很多不完善的地方,比如 cp 等命令不支持,compose 命令不完善,BuildKit 还不支持多平台交叉编译等;所以简单玩玩倒是可以,距离生产使用还需要一些时间,但是总体来说未来可期,相信不久以后我们会离 Docker 越来越远。

]]>
Containerd nerdctl http://mritd.com/2021/06/01/nerdctl-test/#disqus_thread
Golang 监控 http 证书过期时间 http://mritd.com/2021/05/31/golang-check-certificate-expiration-time/ http://mritd.com/2021/05/31/golang-check-certificate-expiration-time/ Mon, 31 May 2021 04:47:00 GMT 迫于需要节省成本(穷),一直使用 Let's Encrypt 签发的证书,为了防止证书过期,自己撮了一个小工具 一、业务需求

由于近几年 Let’s Encrypt 的兴起以及 http 的普及,个人用户终于可以免费 “绿” 一把了;但是 Let’s Encrypt ACME 申请的证书目前只有 3 个月,过期就要更换,最尴尬的是某些比较重要的东西(比如扶墙服务)证书一旦过期会耽误大事;而不同环境下自动更换证书工具也不一定靠谱,极端时候还是需要自己手动更换,所以催生了我想写个证书过期时间检测的小玩具的想法。

二、http 证书链

了解证书加密体系的应该知道,TLS 证书是链式信任的,所以中间任何一个证书过期、失效都会导致整个信任链断裂,不过单纯的 Let’s Encrypt ACME 证书检测可能只关注末端证书即可,除非哪天 Let’s Encrypt 倒下…

三、Go 的 HTTP 请求

Go 在发送 HTTP 请求后,在响应体中会包含一个 TLS *tls.ConnectionState 结构体,该结构体中目前存放了服务端返回的整个证书链:

// ConnectionState records basic TLS details about the connection.type ConnectionState struct {// Version is the TLS version used by the connection (e.g. VersionTLS12).Version uint16// HandshakeComplete is true if the handshake has concluded.HandshakeComplete bool// DidResume is true if this connection was successfully resumed from a// previous session with a session ticket or similar mechanism.DidResume bool// CipherSuite is the cipher suite negotiated for the connection (e.g.// TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_AES_128_GCM_SHA256).CipherSuite uint16// NegotiatedProtocol is the application protocol negotiated with ALPN.NegotiatedProtocol string// NegotiatedProtocolIsMutual used to indicate a mutual NPN negotiation.//// Deprecated: this value is always true.NegotiatedProtocolIsMutual bool// ServerName is the value of the Server Name Indication extension sent by// the client. It's available both on the server and on the client side.ServerName string// PeerCertificates are the parsed certificates sent by the peer, in the// order in which they were sent. The first element is the leaf certificate// that the connection is verified against.//// On the client side, it can't be empty. On the server side, it can be// empty if Config.ClientAuth is not RequireAnyClientCert or// RequireAndVerifyClientCert.PeerCertificates []*x509.Certificate// VerifiedChains is a list of one or more chains where the first element is// PeerCertificates[0] and the last element is from Config.RootCAs (on the// client side) or Config.ClientCAs (on the server side).//// On the client side, it's set if Config.InsecureSkipVerify is false. On// the server side, it's set if Config.ClientAuth is VerifyClientCertIfGiven// (and the peer provided a certificate) or RequireAndVerifyClientCert.VerifiedChains [][]*x509.Certificate// SignedCertificateTimestamps is a list of SCTs provided by the peer// through the TLS handshake for the leaf certificate, if any.SignedCertificateTimestamps [][]byte// OCSPResponse is a stapled Online Certificate Status Protocol (OCSP)// response provided by the peer for the leaf certificate, if any.OCSPResponse []byte// TLSUnique contains the "tls-unique" channel binding value (see RFC 5929,// Section 3). This value will be nil for TLS 1.3 connections and for all// resumed connections.//// Deprecated: there are conditions in which this value might not be unique// to a connection. See the Security Considerations sections of RFC 5705 and// RFC 7627, and http://mitls.org/pages/attacks/3SHAKE#channelbindings.TLSUnique []byte// ekm is a closure exposed via ExportKeyingMaterial.ekm func(label string, context []byte, length int) ([]byte, error)}

根据源码注释可以看到,PeerCertificates 包含了服务端所有证书,那么如果需要检测证书过期时间只需要遍历这个证书切片即可。

四、代码实现

基本需求确定,且确立代码可行性后直接开始 coding:

func checkSSL(beforeTime time.Duration) error {client := &http.Client{Transport: &http.Transport{// 注意如果证书已过期,那么只有在关闭证书校验的情况下链接才能建立成功TLSClientConfig: &tls.Config{InsecureSkipVerify: true},},// 10s 超时后认为服务挂了Timeout: 10 * time.Second,}resp, err := client.Get("http://mritd.com")if err != nil {return err}defer func() { _ = resp.Body.Close() }()// 遍历所有证书for _, cert := range resp.TLS.PeerCertificates {// 检测证书是否已经过期if !cert.NotAfter.After(time.Now()) {return NewWebSiteError(fmt.Sprintf("Website [http://mritd.com] certificate has expired: %s", cert.NotAfter.Local().Format("2006-01-02 15:04:05")))}// 检测证书距离当前时间 是否小于 beforeTime// 例如 beforeTime = 7d,那么在证书过期前 6d 开始就发出警告if cert.NotAfter.Sub(time.Now()) < beforeTime {return NewWebSiteError(fmt.Sprintf("Website [http://mritd.com] certificate will expire, remaining time: %fh", cert.NotAfter.Sub(time.Now()).Hours()))}}return nil}

五、整合告警

基本检测逻辑完成后,可以尝试集成告警服务,例如 Email、Telegram、微信通知等;告警的实现暂时不在本文讨论范围内,具体完整实现可以参考 http://github.com/mritd/certmonitor,certmonitor 集成了 Telegram,最终效果如下:

ixjtRl

六、其他改进

有些情况下某些服务不一定是完全基于 http 的,所以协议上可以后续去尝试使用 tls 客户端直接链接,还可能需要考虑未来基于 QUIC 的 HTTP3 等,复杂点也要支持文件证书检测… 给我时间我能给自己提一万个需求(今天就先码到这)…

]]>
Golang Golang http://mritd.com/2021/05/31/golang-check-certificate-expiration-time/#disqus_thread
Kubernetes 切换到 Containerd http://mritd.com/2021/05/29/use-containerd-with-kubernetes/ http://mritd.com/2021/05/29/use-containerd-with-kubernetes/ Sat, 29 May 2021 15:01:00 GMT 换到 Containerd 小半年了,没事写写。 一、环境准备
  • Ubuntu 20.04 x5
  • Etcd 3.4.16
  • Kubernetes 1.21.1
  • Containerd 1.3.3

1.1、处理 IPVS

由于 Kubernetes 新版本 Service 实现切换到 IPVS,所以需要确保内核加载了 IPVS modules;以下命令将设置系统启动自动加载 IPVS 相关模块,执行完成后需要重启。

# Kernel modulescat > /etc/modules-load.d/50-kubernetes.conf <<EOF# Load some kernel modules needed by kubernetes at bootnf_conntrackbr_netfilterip_vsip_vs_lcip_vs_wlcip_vs_rrip_vs_wrrip_vs_lblcip_vs_lblcrip_vs_dhip_vs_ship_vs_foip_vs_nqip_vs_sedEOF# sysctlcat > /etc/sysctl.d/50-kubernetes.conf <<EOFnet.ipv4.ip_forward=1net.bridge.bridge-nf-call-iptables=1net.bridge.bridge-nf-call-ip6tables=1fs.inotify.max_user_watches=525000EOF

重启完成后务必检查相关 module 加载以及内核参数设置:

# check ipvs modules➜ ~ lsmod | grep ip_vsip_vs_sed              16384  0ip_vs_nq               16384  0ip_vs_fo               16384  0ip_vs_sh               16384  0ip_vs_dh               16384  0ip_vs_lblcr            16384  0ip_vs_lblc             16384  0ip_vs_wrr              16384  0ip_vs_rr               16384  0ip_vs_wlc              16384  0ip_vs_lc               16384  0ip_vs                 155648  22 ip_vs_wlc,ip_vs_rr,ip_vs_dh,ip_vs_lblcr,ip_vs_sh,ip_vs_fo,ip_vs_nq,ip_vs_lblc,ip_vs_wrr,ip_vs_lc,ip_vs_sednf_conntrack          139264  1 ip_vsnf_defrag_ipv6         24576  2 nf_conntrack,ip_vslibcrc32c              16384  5 nf_conntrack,btrfs,xfs,raid456,ip_vs# check sysctl➜ ~ sysctl -a | grep ip_forwardnet.ipv4.ip_forward = 1net.ipv4.ip_forward_update_priority = 1net.ipv4.ip_forward_use_pmtu = 0➜ ~ sysctl -a | grep bridge-nf-callnet.bridge.bridge-nf-call-arptables = 1net.bridge.bridge-nf-call-ip6tables = 1net.bridge.bridge-nf-call-iptables = 1

1.2、安装 Containerd

Containerd 在 Ubuntu 20 中已经在默认官方仓库中包含,所以只需要 apt 安装即可:

# 其他软件包后面可能会用到,所以顺手装了apt install containerd bridge-utils nfs-common tree -y

安装成功后可以通过执行 ctr images ls 命令验证,本章节不会对 Containerd 配置做说明,Containerd 配置文件将在 Kubernetes 安装时进行配置。

二、安装 kubernetes

2.1、安装 Etcd 集群

Etcd 对于 Kubernetes 来说是核心中的核心,所以个人还是比较喜欢在宿主机安装;宿主机安装情况下为了方便我打包了一些 *-pack 的工具包,用于快速处理:

安装 CFSSL 和 ETCD

# 下载安装包wget http://github.com/mritd/etcd-pack/releases/download/v3.4.16/etcd_v3.4.16.runwget http://github.com/mritd/cfssl-pack/releases/download/v1.5.0/cfssl_v1.5.0.run# 安装 cfssl 和 etcdchmod +x *.run./etcd_v3.4.16.run install./cfssl_v1.5.0.run install

安装完成后,自行调整 /etc/cfssl/etcd/etcd-csr.json 相关 IP,然后执行同目录下 create.sh 生成证书。

➜ ~ cat /etc/cfssl/etcd/etcd-csr.json{    "key": {        "algo": "rsa",        "size": 2048    },    "names": [        {            "O": "etcd",            "OU": "etcd Security",            "L": "Beijing",            "ST": "Beijing",            "C": "CN"        }    ],    "CN": "etcd",    "hosts": [        "127.0.0.1",        "localhost",        "*.etcd.node",        "*.kubernetes.node",        "10.0.0.11",        "10.0.0.12",        "10.0.0.13"    ]}# 复制到 3 台 master➜ ~ for ip in `seq 1 3`; do scp /etc/cfssl/etcd/*.pem root@10.0.0.1$ip:/etc/etcd/ssl; done

证书生成完成后调整每台机器的 Etcd 配置文件,然后修复权限启动。

# 复制配置for ip in `seq 1 3`; do scp /etc/etcd/etcd.cluster.yaml root@10.0.0.1$ip:/etc/etcd/etcd.yaml; done# 修复权限for ip in `seq 1 3`; do ssh root@10.0.0.1$ip chown -R etcd:etcd /etc/etcd; done# 每台机器启动systemctl start etcd

启动完成后通过 etcdctl 验证集群状态:

# 稳妥点应该执行 etcdctl endpoint health➜ ~ etcdctl member list55fcbe0adaa45350, started, etcd3, http://10.0.0.13:2380, http://10.0.0.13:2379, falsecebdf10928a06f3c, started, etcd1, http://10.0.0.11:2380, http://10.0.0.11:2379, falsef7a9c20602b8532e, started, etcd2, http://10.0.0.12:2380, http://10.0.0.12:2379, false

2.2、安装 kubeadm

kubeadm 国内用户建议使用 aliyun 的安装源:

# kubeadmapt-get install -y apt-transport-httpcurl http://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add -cat <<EOF >/etc/apt/sources.list.d/kubernetes.listdeb http://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial mainEOFapt update# ebtables、ethtool kubelet 可能会用,具体忘了,反正从官方文档上看到的apt install kubelet kubeadm kubectl ebtables ethtool -y

2.3、安装 kube-apiserver-proxy

kube-apiserver-proxy 是我自己编译的一个仅开启四层代理的 Nginx,其主要负责监听 127.0.0.1:6443 并负载到所有的 Api Server 地址(0.0.0.0:5443):

wget http://github.com/mritd/kube-apiserver-proxy-pack/releases/download/v1.20.0/kube-apiserver-proxy_v1.20.0.runchmod +x *.run./kube-apiserver-proxy_v1.20.0.run install

安装完成后根据 IP 地址不同自行调整 Nginx 配置文件,然后启动:

➜ ~ cat /etc/kubernetes/apiserver-proxy.conferror_log syslog:server=unix:/dev/log notice;worker_processes auto;events {        multi_accept on;        use epoll;        worker_connections 1024;}stream {    upstream kube_apiserver {        least_conn;        server 10.0.0.11:5443;        server 10.0.0.12:5443;        server 10.0.0.13:5443;    }    server {        listen        0.0.0.0:6443;        proxy_pass    kube_apiserver;        proxy_timeout 10m;        proxy_connect_timeout 1s;    }}systemctl start kube-apiserver-proxy

2.4、安装 kubeadm-config

kubeadm-config 是一系列配置文件的组合以及 kubeadm 安装所需的必要镜像文件的打包,安装完成后将会自动配置 Containerd、ctrictl 等:

wget http://github.com/mritd/kubeadm-config-pack/releases/download/v1.21.1/kubeadm-config_v1.21.1.runchmod +x *.run# --load 选项用于将 kubeadm 所需镜像 load 到 containerd 中./kubeadm-config_v1.21.1.run install --load

2.4.1、containerd 配置

Containerd 配置位于 /etc/containerd/config.toml,其配置如下:

version = 2# 指定存储根目录root = "/data/containerd"state = "/run/containerd"# OOM 评分oom_score = -999[grpc]  address = "/run/containerd/containerd.sock"[metrics]  address = "127.0.0.1:1234"[plugins]  [plugins."io.containerd.grpc.v1.cri"]    # sandbox 镜像    sandbox_image = "k8s.gcr.io/pause:3.4.1"    [plugins."io.containerd.grpc.v1.cri".containerd]      snapshotter = "overlayfs"      default_runtime_name = "runc"      [plugins."io.containerd.grpc.v1.cri".containerd.runtimes]        [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]          runtime_type = "io.containerd.runc.v2"          # 开启 systemd cgroup          [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]            SystemdCgroup = true

2.4.2、crictl 配置

在切换到 Containerd 以后意味着以前的 docker 命令将不再可用,containerd 默认自带了一个 ctr 命令,同时 CRI 规范会自带一个 crictl 命令;crictl 命令配置文件存放在 /etc/crictl.yaml 中:

runtime-endpoint: unix:///run/containerd/containerd.sockimage-endpoint: unix:///run/containerd/containerd.sockpull-image-on-create: true

2.4.3、kubeadm 配置

kubeadm 配置目前分为 2 个,一个是用于首次引导启动的 init 配置,另一个是用于其他节点 join 到 master 的配置;其中比较重要的 init 配置如下:

# /etc/kubernetes/kubeadm.yamlapiVersion: kubeadm.k8s.io/v1beta2kind: InitConfiguration# kubeadm token createbootstrapTokens:- token: "c2t0rj.cofbfnwwrb387890"nodeRegistration:  # CRI 地址(Containerd)  criSocket: unix:///run/containerd/containerd.sock  kubeletExtraArgs:    runtime-cgroups: "/system.slice/containerd.service"    rotate-server-certificates: "true"localAPIEndpoint:  advertiseAddress: "10.0.0.11"  bindPort: 5443# kubeadm certs certificate-keycertificateKey: 31f1e534733a1607e5ba67b2834edd3a7debba41babb1fac1bee47072a98d88b---apiVersion: kubeadm.k8s.io/v1beta2kind: ClusterConfigurationclusterName: "kuberentes"kubernetesVersion: "v1.21.1"certificatesDir: "/etc/kubernetes/pki"# Other components of the current control plane only connect to the apiserver on the current host.# This is the expected behavior, see: http://github.com/kubernetes/kubeadm/issues/2271controlPlaneEndpoint: "127.0.0.1:6443"etcd:  external:    endpoints:    - "http://10.0.0.11:2379"    - "http://10.0.0.12:2379"    - "http://10.0.0.13:2379"    caFile: "/etc/etcd/ssl/etcd-ca.pem"    certFile: "/etc/etcd/ssl/etcd.pem"    keyFile: "/etc/etcd/ssl/etcd-key.pem"networking:  serviceSubnet: "10.66.0.0/16"  podSubnet: "10.88.0.1/16"  dnsDomain: "cluster.local"apiServer:  extraArgs:    v: "4"    alsologtostderr: "true"#    audit-log-maxage: "21"#    audit-log-maxbackup: "10"#    audit-log-maxsize: "100"#    audit-log-path: "/var/log/kube-audit/audit.log"#    audit-policy-file: "/etc/kubernetes/audit-policy.yaml"    authorization-mode: "Node,RBAC"    event-ttl: "720h"    runtime-config: "api/all=true"    service-node-port-range: "30000-50000"    service-cluster-ip-range: "10.66.0.0/16"#    insecure-bind-address: "0.0.0.0"#    insecure-port: "8080"    # The fraction of requests that will be closed gracefully(GOAWAY) to prevent    # HTTP/2 clients from getting stuck on a single apiserver.    goaway-chance: "0.001"#  extraVolumes:#  - name: "audit-config"#    hostPath: "/etc/kubernetes/audit-policy.yaml"#    mountPath: "/etc/kubernetes/audit-policy.yaml"#    readOnly: true#    pathType: "File"#  - name: "audit-log"#    hostPath: "/var/log/kube-audit"#    mountPath: "/var/log/kube-audit"#    pathType: "DirectoryOrCreate"  certSANs:  - "*.kubernetes.node"  - "10.0.0.11"  - "10.0.0.12"  - "10.0.0.13"  timeoutForControlPlane: 1mcontrollerManager:  extraArgs:    v: "4"    node-cidr-mask-size: "19"    deployment-controller-sync-period: "10s"    experimental-cluster-signing-duration: "8670h"    node-monitor-grace-period: "20s"    pod-eviction-timeout: "2m"    terminated-pod-gc-threshold: "30"scheduler:  extraArgs:    v: "4"---apiVersion: kubelet.config.k8s.io/v1beta1kind: KubeletConfigurationfailSwapOn: falseoomScoreAdj: -900cgroupDriver: "systemd"kubeletCgroups: "/system.slice/kubelet.service"nodeStatusUpdateFrequency: 5srotateCertificates: trueevictionSoft:  "imagefs.available": "15%"  "memory.available": "512Mi"  "nodefs.available": "15%"  "nodefs.inodesFree": "10%"evictionSoftGracePeriod:  "imagefs.available": "3m"  "memory.available": "1m"  "nodefs.available": "3m"  "nodefs.inodesFree": "1m"evictionHard:  "imagefs.available": "10%"  "memory.available": "256Mi"  "nodefs.available": "10%"  "nodefs.inodesFree": "5%"evictionMaxPodGracePeriod: 30imageGCLowThresholdPercent: 70imageGCHighThresholdPercent: 80kubeReserved:  "cpu": "500m"  "memory": "512Mi"  "ephemeral-storage": "1Gi"---apiVersion: kubeproxy.config.k8s.io/v1alpha1kind: KubeProxyConfiguration# kube-proxy specific options hereclusterCIDR: "10.88.0.1/16"mode: "ipvs"oomScoreAdj: -900ipvs:  minSyncPeriod: 5s  syncPeriod: 5s  scheduler: "wrr"

init 配置具体含义请自行参考官方文档,相对于 init 配置,join 配置比较简单,不过需要注意的是如果需要 join 为 master 则需要 controlPlane 这部分,否则请注释掉 controlPlane

# /etc/kubernetes/kubeadm-join.yamlapiVersion: kubeadm.k8s.io/v1beta2kind: JoinConfigurationcontrolPlane:  localAPIEndpoint:    advertiseAddress: "10.0.0.12"    bindPort: 5443  certificateKey: 31f1e534733a1607e5ba67b2834edd3a7debba41babb1fac1bee47072a98d88bdiscovery:  bootstrapToken:    apiServerEndpoint: "127.0.0.1:6443"    token: "c2t0rj.cofbfnwwrb387890"    # Please replace with the "--discovery-token-ca-cert-hash" value printed    # after the kubeadm init command is executed successfully    caCertHashes:    - "sha256:97590810ae34a82501717e33acfca76f16044f1a365c5ad9a1c66433c386c75c"nodeRegistration:  criSocket: unix:///run/containerd/containerd.sock  kubeletExtraArgs:    runtime-cgroups: "/system.slice/containerd.service"    rotate-server-certificates: "true"

2.5、拉起 master

在调整好配置后,拉起 master 节点只需要一条命令:

kubeadm init --config /etc/kubernetes/kubeadm.yaml --upload-certs --ignore-preflight-errors=Swap

拉起完成后记得保存相关 Token 以便于后续使用。

2.6、拉起其他 master

在第一个 master 启动完成后,使用 join 命令让其他 master 加入即可;需要注意的是 kubeadm-join.yaml 配置中需要替换 caCertHashes 为第一个 master 拉起后的 discovery-token-ca-cert-hash 的值。

kubeadm join 127.0.0.1:6443 --config /etc/kubernetes/kubeadm-join.yaml --ignore-preflight-errors=Swap

2.7、拉起其他 node

node 节点拉起与拉起其他 master 节点一样,唯一不同的是需要注释掉配置中的 controlPlane 部分。

# /etc/kubernetes/kubeadm-join.yamlapiVersion: kubeadm.k8s.io/v1beta2kind: JoinConfiguration#controlPlane:#  localAPIEndpoint:#    advertiseAddress: "10.0.0.12"#    bindPort: 5443#  certificateKey: 31f1e534733a1607e5ba67b2834edd3a7debba41babb1fac1bee47072a98d88bdiscovery:  bootstrapToken:    apiServerEndpoint: "127.0.0.1:6443"    token: "c2t0rj.cofbfnwwrb387890"    # Please replace with the "--discovery-token-ca-cert-hash" value printed    # after the kubeadm init command is executed successfully    caCertHashes:    - "sha256:97590810ae34a82501717e33acfca76f16044f1a365c5ad9a1c66433c386c75c"nodeRegistration:  criSocket: unix:///run/containerd/containerd.sock  kubeletExtraArgs:    runtime-cgroups: "/system.slice/containerd.service"    rotate-server-certificates: "true"
kubeadm join 127.0.0.1:6443 --config /etc/kubernetes/kubeadm-join.yaml --ignore-preflight-errors=Swap

2.8、其他处理

由于 kubelet 开启了证书轮转,所以新集群会有大量 csr 请求,批量允许即可:

kubectl get csr | grep Pending | awk '{print $1}' | xargs kubectl certificate approve

同时为了 master 节点也能负载 pod,需要调整污点:

kubectl taint nodes --all node-role.kubernetes.io/master-

后续 CNI 等不在本文内容范围内。

三、Containerd 常用操作

# 列出镜像ctr images ls# 列出 k8s 镜像ctr -n k8s.io images ls# 导入镜像ctr -n k8s.io images import xxxx.tar# 导出镜像ctr -n k8s.io images export kube-scheduler.tar k8s.gcr.io/kube-scheduler:v1.21.1

四、资源仓库

本文中所有 *-pack 仓库地址如下:

]]>
Kubernetes Linux Kubernetes http://mritd.com/2021/05/29/use-containerd-with-kubernetes/#disqus_thread
MySQL 表结构对比 http://mritd.com/2021/05/29/mysql-schema-diff/ http://mritd.com/2021/05/29/mysql-schema-diff/ Sat, 29 May 2021 10:12:00 GMT 最近在疯狂码业务代码,由于开发频繁导致生产库结构与测试库不一样,改动太多不想手动搞,记录一下这个小工具。 一、安装 mysql-schema-diff

Ubuntu 20.04 系统使用如下命令安装:

apt install libmysql-diff-perl -y

安装完成后使用 --help 应该能看到相关提示

➜ ~ mysql-schema-diff --helpUsage: mysql-schema-diff [ options ] <database1> <database2>Options:  -?,  --help                show this help  -A,  --apply               interactively patch database1 to match database2  -B,  --batch-apply         non-interactively patch database1 to match database2  -d,  --debug[=N]           enable debugging [level N, default 1]  -l,  --list-tables         output the list off all used tables  -o,  --only-both           only output changes for tables in both databases  -k,  --keep-old-tables     don't output DROP TABLE commands  -c,  --keep-old-columns    don't output DROP COLUMN commands  -n,  --no-old-defs         suppress comments describing old definitions  -t,  --table-re=REGEXP     restrict comparisons to tables matching REGEXP  -i,  --tolerant            ignore DEFAULT, AUTO_INCREMENT, COLLATE, and formatting changes  -S,  --single-transaction  perform DB dump in transaction  -h,  --host=...            connect to host  -P,  --port=...            use this port for connection  -u,  --user=...            user for login if not current user  -p,  --password[=...]      password to use when connecting to server  -s,  --socket=...          socket to use when connecting to serverfor <databaseN> only, where N == 1 or 2,       --hostN=...           connect to host       --portN=...           use this port for connection       --userN=...           user for login if not current user       --passwordN[=...]     password to use when connecting to server       --socketN=...         socket to use when connecting to serverDatabases can be either files or database names.If there is an ambiguity, the file will be preferred;to prevent this prefix the database argument with `db:'.

二、生成差异 SQL

安装完成后可直接使用该工具生成差异 SQL 文件mysql-schema-diff 工具使用如下:

Usage: mysql-schema-diff [ options ] <database1> <database2>Options:  -?,  --help                show this help  -A,  --apply               interactively patch database1 to match database2  -B,  --batch-apply         non-interactively patch database1 to match database2  -d,  --debug[=N]           enable debugging [level N, default 1]  -l,  --list-tables         output the list off all used tables  -o,  --only-both           only output changes for tables in both databases  -k,  --keep-old-tables     don't output DROP TABLE commands  -c,  --keep-old-columns    don't output DROP COLUMN commands  -n,  --no-old-defs         suppress comments describing old definitions  -t,  --table-re=REGEXP     restrict comparisons to tables matching REGEXP  -i,  --tolerant            ignore DEFAULT, AUTO_INCREMENT, COLLATE, and formatting changes  -S,  --single-transaction  perform DB dump in transaction  -h,  --host=...            connect to host  -P,  --port=...            use this port for connection  -u,  --user=...            user for login if not current user  -p,  --password[=...]      password to use when connecting to server  -s,  --socket=...          socket to use when connecting to serverfor <databaseN> only, where N == 1 or 2,       --hostN=...           connect to host       --portN=...           use this port for connection       --userN=...           user for login if not current user       --passwordN[=...]     password to use when connecting to server       --socketN=...         socket to use when connecting to server

通俗的说,通过 --hostN 等参数指定两个数据库地址,例如 --password1 指定第一个数据库密码,--password2 指定第二个数据库密码;然后最后仅跟 数据库1[.表名] 数据库2[.表名],表名如果不写则默认对比两个数据库。以下为样例命令:

mysql-schema-diff \    --host1 127.0.0.1 --port1 3300 \    --user1 bleem --password1=Bleem77965badf \    --host2 127.0.0.1 --port2 3306 \    --user2 bleem --password2=asnfskdf667asd8 \    testdb testdb

注意,首次运行后请根据生成的 SQL 判断对比是否正确,比如说想把比较新的测试库更改同步到生产库,那么 SQL 里全是DROP 字样的删除动作,这说明 --hostN 等参数指定反了(变成了生产库同步测试库),此时只需要将 --hostN 参数调换一下即可(1改成2,2改成1),这样生成的 SQL 就会变为 ADD 字样的添加动作。

三、网络问题

mysql-schema-diff 工具需要在运行时能同时连接两个数据库,常规情况下可以通过 SSH 打洞来临时解决访问问题;如果实在无法打通网络环境,mysql-schema-diff 还支持文件对比,以下为一些文件对比的示例:

# compare table definitions in two filesmysql-schema-diff db1.mysql db2.mysql# compare table definitions in a file 'db1.mysql' with a database 'db2'mysql-schema-diff db1.mysql db2# interactively upgrade schema of database 'db1' to be like the# schema described in the file 'db2.mysql'mysql-schema-diff -A db1 db2.mysql# compare table definitions in two databases on a remote machinemysql-schema-diff --host=remote.host.com --user=myaccount db1 db2# compare table definitions in a local database 'foo' with a# database 'bar' on a remote machine, when a file foo already# exists in the current directorymysql-schema-diff --host2=remote.host.com --password=secret db:foo bar
]]>
Linux Linux MySQL http://mritd.com/2021/05/29/mysql-schema-diff/#disqus_thread
Longhorn 微服务化存储初试 http://mritd.com/2021/03/06/longhorn-storage-test/ http://mritd.com/2021/03/06/longhorn-storage-test/ Sat, 06 Mar 2021 05:40:22 GMT Longhorn 是 Rancher Labs 开源的一个轻量级云原生微服务化存储方案,本篇文章将详细介绍 Longhorn 安装使用以及其设计架构 一、Longhorn 安装

1.1、准备工作

Longhorn 官方推荐的最小配置如下,如果数据并不算太重要可适当缩减和调整,具体请自行斟酌:

  • 3 Nodes
  • 4 vCPUs per Node
  • 4 GiB per Node
  • SSD/NVMe or similar performance block device on the node for storage(We don’t recommend using spinning disks with Longhorn, due to low IOPS.)

本次安装测试环境如下:

  • Ubuntu 20.04(8c16g)
  • Disk 200g
  • Kubernetes 1.20.4(kubeadm)
  • Longhorn 1.1.0

1.2、安装 Longhorn(Helm)

安装 Longhorn 推荐使用 Helm,因为在卸载时 kubectl 无法直接使用 delete 卸载,需要进行其他清理工作;helm 安装命令如下:

# add Longhorn repohelm repo add longhorn http://charts.longhorn.io# updatehelm repo update# create namespacekubectl create namespace longhorn-system# installhelm install longhorn longhorn/longhorn --namespace longhorn-system --values longhorn-values.yaml

其中 longhorn-values.yaml 请从 Charts 仓库 下载,本文仅修改了以下两项:

defaultSettings:  # 默认存储目录(默认为 /var/lib/longhorn)  defaultDataPath: "/data/longhorn"  # 默认副本数量  defaultReplicaCount: 2

安装完成后 Pod 运行情况如下所示:

M5ZYyb1614931770247

此后可通过集群 Ingress 或者 NodePort 等方式暴露 service longhorn-frontend 的 80 端口来访问 Longhorn UI;注意,Ingress 等负载均衡其如果采用 http 访问请确保向 Longhorn UI 传递了 X-Forwarded-Proto: http 头,否则可能导致 Websocket 不安全链接以及跨域等问题,后果就是 UI 出现一些神奇的小问题(我排查了好久…)。

vIFU281614931897632

1.3、卸载 Longhorn

如果在安装过程中有任何操作错误,或想重新安装验证相关设置,可通过以下命令卸载 Longhorn:

# 卸载helm uninstall longhorn -n longhorn-system

二、Longhorn 架构

2.1、Design

Longhorn 总体设计分为两层: 数据平面和控制平面;Longhorn Engine 是一个存储控制器,对应数据平面;Longhorn Manager 对应控制平面。

2.1.1、Longhorn Manager

Longhorn Manager 使用 Operator 模式,作为 Daemonset 运行在每个节点上;Longhorn Manager 负责接收 Longhorn UI 以及 Kubernetes Volume 插件的 API 调用,然后创建和管理 Volume;

Longhorn Manager 在与 kubernetes API 通信并创建 Longhorn Volume CRD(heml 安装直接创建了相关 CRD,查看代码后发现 Manager 里面似乎也会判断并创建一下),此后 Longhorn Manager watch 相关 CRD 资源和 Kubernetes 原生资源(PV/PVC…),一但集群内创建了 Longhorn Volume 则 Longhorn Manager 负责创建物理 Volume。

当 Longhorn Manager 创建 Volume 时,Longhorn Manager 首先会在 Volume 所在节点创建 Longhorn Engine 实例(对比实际行为后发现所谓的 “实例” 其实只是运行了一个 Linux 进程,并非创建 Pod),然后根据副本数量在所需放置副本的节点上创建对应的副本。

2.1.2、Longhorn Engine

Longhorn Engine 始终与其使用 Volume 的 Pod 在同一节点上,它跨存储在多个节点上的多个副本同步复制卷;同时数据的多路径保证 Longhorn Volume 的 HA,单个副本或者 Engine 出现问题不会影响到所有副本或 Pod 对 Volume 的访问。

下图中展示了 Longhorn 的 HA 架构,每个 Kubernetes Volume 将会对应一个 Longhorn Engine,每个 Engine 管理 Volume 的多个副本,Engine 与 副本实质都会是一个单独的 Linux 进程运行:

Yxljct1614932095536

注意: 图中的 Engine 并非是单独的一个 Pod,而是每一个 Volume 会对应一个 golang exec 出来的 Linux 进程。

2.2、CSI Plugin

CSI 部分不做过多介绍,具体参考 如何编写 CSI 插件;以下为简要说明:

  • Kubernetes CSI 被抽象为具体的 CSI 容器并通过 gRPC 调用目标 plugin
  • Longhorn CSI Plugin 负责接收标准 CSI 容器发起的 gRPC 调用
  • Longhorn CSI Plugin 将 Kubernetes CSI gRPC 调用转换为自己的 Longhorn API 调用,并将其转发到 Longhorn Manager 控制平面
  • Longhorn 某些功能使用了 iSCSI,所以可能需要在节点上安装 open-iscsi 或 iscsiadm

2.3、Longhorn UI

Longhorn UI 向外暴露一个 Dashboard,并用过 Longhorn API 与 Longhorn Manager 控制平面交互;Longhorn UI 在架构上类似于 Longhorn CSI Plugin 的替代者,只不过一个是通过 Web UI 转化为 Longhorn API,另一个是将 CSI gRPC 转换为 Longhorn API。

2.4、Replicas And Snapshots

在 Longhorn 微服务架构中,副本也作为单独的进程运行,其实质存储文件采用 Linux 的稀释文件方式;每个副本均包含 Longhorn Volume 的快照链,快照就像一个 Image 层,其中最旧的快照用作基础层,而较新的快照位于顶层。如果数据会覆盖旧快照中的数据,则仅将其包含在新快照中;整个快照链展示了数据的当前状态。

在进行快照时,Longhorn 会创建差异磁盘(differencing disk)文件,每个差异磁盘文件被看作是一个快照,当 Longhorn 读取文件时从上层开始依次查找,其示例图如下:

tOvsJ91614932116138

为了提高读取性能,Longhorn 维护了一个读取索引,该索引记录了每个 4K 存储块中哪个差异磁盘包含有效数据;读取索引会占用一定的内存,每个 4K 块占用一个字节,字节大小的读取索引意味着每个卷最多可以拍摄 254 个快照,在大约 1TB 的卷中读取索引大约会消耗256MB 的内存。

2.5、Backups and Secondary Storage

由于数据大小、网络延迟等限制,跨区域同步复制无法做到很高的时效性,所以 Longhorn 提供了称之为 Secondary Storage 的备份方案;Secondary Storage 依赖外部的 NFS、S3 等存储设施,一旦在 Longhorn 中配置了 Backup Storage,Longhorn 将会通过卷的指定版本快照完成备份;备份过程中 Longhorn 将会抹平快照信息,这意味着快照历史变更将会丢失,相同的原始卷备份是增量的,通过不断的应用差异磁盘文件完成;为了避免海量小文件带来的性能瓶颈,Longhorn 采用 2MB 分块进行备份,任何边界内 4k 块变动都会触发 2MB 块的备份行为;Longhorn 的备份功能为跨集群、跨区域提供完善的灾难恢复机制。Longhorn 备份机制如下图所示:

6EShuz1614932147884

2.6、Longhorn Pods

上面的大部分其实来源于对官方文档 Architecture and Concepts 的翻译;在翻译以及阅读文档过程中,通过对比文档与实际行为,还有阅读源码发现了一些细微差异,这里着重介绍一下这些 Pod 都是怎么回事:

1EUF4y-1615008575-QcVGUt

2.6.1、longhorn-manager

longhorn-manager 与文档描述一致,其通过 Helm 安装时直接以 Daemonset 方式 Create 出来,然后 longhorn-manager 开启 HTTP API(9500) 等待其他组件请求;同时 longhorn-manager 还会使用 Operator 模式监听各种资源,包括不限于 Longhorn CRD 以及集群的 PV(C) 等资源,然后作出对应的响应。

2.6.2、longhorn-driver-deployer

Helm 安装时创建了 longhorn-driver-deployer Deployment,longhorn-driver-deployer 实际上也是 longhorn-manager 镜像启动,只不过启动后会沟通 longhorn-manager HTTP API,然后创建所有 CSI 相关容器,包括 csi-provisionercsi-snapshotterlonghorn-csi-plugin 等。

2.6.3、instance-manager-e

上面所说的每个 Engine 对应一个 Linux 进程其实就是通过这个 Pod 完成的,instance-manager-e 由 longhorn-manager 创建,创建完成后 instance-manager-e 监听 gRPC 8500 端口,其只要职责就是接收 gRPC 请求,并启动 Engine 进程;从上面我们 Engine 介绍可以得知 Engine 与 Volume 绑定,所以理论上集群内 Volume 被创建时有某个 “东西” 创建了 CRD engines.longhorn.io,然后又有人 watch 了 engines.longhorn.io 并通知 instance-manager-e 启动 Engine 进程;这里不负责任的推测是 longhorn-manager 干的,但是没看代码不敢说死…

hfk9uQ1614943191222

同理 instance-manager-r 是负责启动副本的 Linux 进程的,工作原理与 instance-manager-e 相同,通过简单的查看代码(IDE 没打开…哈哈哈)推测,instance-manager-e/-r 应该是 longhorn-manager Operator 下的产物,其维护了一个自己的 “Daemonset”,但是 kubectl 是看不到的。

2.6.4、longhorn-ui

longhorn-ui 很简单,就是个 UI 界面,然后 HTTP API 沟通 longhorn-manager,这里不再做过多说明。

三、Longhorn 使用

3.1、常规使用

默认情况下 Helm 安装完成后会自动创建 StorageClass,如果集群中只有 Longhorn 作为存储,那么 Longhorn 的 StorageClass 将作为默认 StorageClass。关于 StorageClass、PV、PVC 如果使用这里不做过多描述,请参考官方 Example 文档;

需要注意的是 Longhorn 作为块存储仅支持 ReadWriteOnce 模式,如果想支持 ReadWriteMany 模式,则需要在节点安装 nfs-common,Longhorn 将会自动创建 share-manager 容器然后通过 NFSV4 共享这个 Volume 从而实现 ReadWriteMany具体请参考 Support for ReadWriteMany (RWX) workloads

3.2、添加删除磁盘

如果出现磁盘损坏重建或者添加删除磁盘,请直接访问 UI 界面,通过下拉菜单操作即可;在操作前请将节点调整到维护模式并驱逐副本,具体请参考 Evicting Replicas on Disabled Disks or Nodes

0tedBl1614945096177

需要注意的是添加新磁盘时,磁盘挂载的软连接路径不能工作,请使用原始挂载路径或通过 mount --bind 命令设置新路径。

Ps8QRR1614945234562

3.3、创建快照及回滚

当创建好 Volume 以后可以用过 Longhorn UI 在线对 Volume 创建快照,但是回滚快照过程需要 Workload(Pod) 离线,同时 Volume 必须以维护模式 reattach 到某一个 Host 节点上,然后在 Longhorn UI 进行调整;以下为快照创建回滚测试:

test.pvc.yaml

apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: longhorn-simple-pvcspec:  accessModes:    - ReadWriteOnce  storageClassName: longhorn  resources:    requests:      storage: 1Gi

test.po.yaml

apiVersion: v1kind: Podmetadata:  name: longhorn-simple-pod  namespace: defaultspec:  restartPolicy: Always  containers:    - name: volume-test      image: nginx:stable-alpine      imagePullPolicy: IfNotPresent      livenessProbe:        exec:          command:            - ls            - /data/lost+found        initialDelaySeconds: 5        periodSeconds: 5      volumeMounts:        - name: volv          mountPath: /data      ports:        - containerPort: 80  volumes:    - name: volv      persistentVolumeClaim:        claimName: longhorn-simple-pvc

3.3.1、创建快照

首先创建相关资源:

kubectl create -f test.pvc.yamlkubectl create -f test.po.yaml

创建完成后在 Longhorn UI 中可以看到刚刚创建出的 Volume:

L3hnYi-1615000011-UBMIEE

点击 Name 链接进入到 Volume 详情,然后点击 Take Snapshot 按钮即可拍摄快照;有些情况下 UI 响应缓慢可能导致 Take Snapshot 按钮变灰,刷新两次即可恢复。

TkKNCz-1615001806-jjaakH

快照在回滚后仍然可以进行交叉创建

ykQs66-1615002223-B2Z9Sa

3.3.2、回滚快照

回滚快照时必须停止 Pod:

# 停止kubectl delete -f test.po.yaml

然后重新将 Volume Attach 到宿主机:

uuAFT8-1615001937-bGA3Sq

注意要开启维护模式

bFcbW3-1615002020-Q8nG15

稍等片刻等待所有副本 “Running” 然后 Revert 即可

xLZdSP-1615002098-p8sueg

回滚完成后,需要 Detach Volume,以便供重新创建的 Pod 使用

JYZZLe-1615002351-x6A6TE

3.3.3、定时快照

除了手动创建快照之外,Longhorn 还支持定时对 Volume 进行快照处理;要使用定时任务,请进入 Volume 详情页面,在 Recurring Snapshot and Backup Schedule 选项卡下新增定时任务即可:

iUpPnA-1615006838-wM4tBA

如果不想为内核 Volume 都手动设置自动快照,可以用过调整 StorageClass 来实现为每个自动创建的 PV 进行自动快照,具体请阅读 Set up Recurring Jobs using a StorageClass 文档。

3.4、Volume 扩容

Longhorn 支持对 Volume 进行扩容,扩容方式和回滚快照类似,都需要 Deacth Volume 并开启维护模式。

首先停止 Workload

➜ ~ kubectl exec -it longhorn-simple-pod -- df -hFilesystem                Size      Used Available Use% Mounted onoverlay                 199.9G      4.7G    195.2G   2% /tmpfs                    64.0M         0     64.0M   0% /devtmpfs                     7.8G         0      7.8G   0% /sys/fs/cgroup/dev/longhorn/pvc-1c9e23f4-af29-4a48-9560-87983267b8d3                        975.9M      2.5M    957.4M   0% /data/dev/sda4                60.0G      7.7G     52.2G  13% /etc/hosts/dev/sda4                60.0G      7.7G     52.2G  13% /dev/termination-log/dev/sdc1               199.9G      4.7G    195.2G   2% /etc/hostname/dev/sdc1               199.9G      4.7G    195.2G   2% /etc/resolv.confshm                      64.0M         0     64.0M   0% /dev/shmtmpfs                     7.8G     12.0K      7.8G   0% /run/secrets/kubernetes.io/serviceaccounttmpfs                     7.8G         0      7.8G   0% /proc/acpitmpfs                    64.0M         0     64.0M   0% /proc/kcoretmpfs                    64.0M         0     64.0M   0% /proc/keystmpfs                    64.0M         0     64.0M   0% /proc/timer_listtmpfs                    64.0M         0     64.0M   0% /proc/sched_debugtmpfs                     7.8G         0      7.8G   0% /proc/scsitmpfs                     7.8G         0      7.8G   0% /sys/firmware➜ ~ kubectl delete -f test.po.yamlpod "longhorn-simple-pod" deleted

然后直接使用 kubectl 编辑 PVC,调整 spec.resources.requests.storage

LT9aSo-1615006002-L1veZr

保存后可以从 Longhorn UI 中看到 Volume 在自动 resize

zG4ugo-1615006070-f7POdb

重新创建 Workload 可以看到 Volume 已经扩容成功

➜ ~ kubectl create -f test.po.yamlpod/longhorn-simple-pod created➜ ~ kubectl exec -it longhorn-simple-pod -- df -hFilesystem                Size      Used Available Use% Mounted onoverlay                 199.9G      6.9G    193.0G   3% /tmpfs                    64.0M         0     64.0M   0% /devtmpfs                     7.8G         0      7.8G   0% /sys/fs/cgroup/dev/longhorn/pvc-1c9e23f4-af29-4a48-9560-87983267b8d3                          4.9G      4.0M      4.9G   0% /data/dev/sda4                60.0G      7.6G     52.4G  13% /etc/hosts/dev/sda4                60.0G      7.6G     52.4G  13% /dev/termination-log/dev/sdc1               199.9G      6.9G    193.0G   3% /etc/hostname/dev/sdc1               199.9G      6.9G    193.0G   3% /etc/resolv.confshm                      64.0M         0     64.0M   0% /dev/shmtmpfs                     7.8G     12.0K      7.8G   0% /run/secrets/kubernetes.io/serviceaccounttmpfs                     7.8G         0      7.8G   0% /proc/acpitmpfs                    64.0M         0     64.0M   0% /proc/kcoretmpfs                    64.0M         0     64.0M   0% /proc/keystmpfs                    64.0M         0     64.0M   0% /proc/timer_listtmpfs                    64.0M         0     64.0M   0% /proc/sched_debugtmpfs                     7.8G         0      7.8G   0% /proc/scsitmpfs                     7.8G         0      7.8G   0% /sys/firmware

Volume 扩展过程中 Longhorn 会自动处理文件系统相关调整,但是并不是百分百会处理,一般 Longhorn 仅在以下情况做自动处理:

  • 扩展后大小大约当前大小(进行扩容)
  • Longhorn Volume 中存在一个 Linux 文件系统
  • Longhorn Volume 中的 Linux 文件系统为 ext4 或 xfs
  • Longhorn Volume 使用 block device 作为 frontend

非这几种情况外,如还原到更小容量的 Snapshot,可能需要手动调整文件系统,具体请参考 Filesystem expansion 章节文档。

四、总结

总体来说目前 Longhorn 是一个比较清量级的存储解决方案,微服务化使其更加可靠,同时官方文档完善社区响应也比较迅速;最主要的是 Longhorn 采用的技术方案不会过于复杂,通过文档以及阅读源码至少可以比较快速的了解其背后实现,而反观一些其他大型存储要么文档不全,要么实现技术复杂,普通用户很难窥视其核心;综合来说在小型存储选择上比较推荐 Longhorn,至于稳定性么,很不负责的说我也不知道,毕竟我也是新手,备份还没折腾呢…

]]>
Kubernetes Kubernetes CSI Longhorn http://mritd.com/2021/03/06/longhorn-storage-test/#disqus_thread
Caddy2 简明教程 http://mritd.com/2021/01/07/lets-start-using-caddy2/ http://mritd.com/2021/01/07/lets-start-using-caddy2/ Thu, 07 Jan 2021 09:44:30 GMT 最近网站证书又过期了...... 终于痛下决心(以前太懒)切换到了 Caddy2,这里记录一下 Caddy2 简单使用方式,包括从零开始编译以及配置调整。 Caddy 是一个 Go 编写的 Web 服务器,类似于 Nginx,Caddy 提供了更加强大的功能,随着 v2 版本发布 Caddy 已经可以作为中小型站点 Web 服务器的另一个选择;相较于 Nginx 来说使用 Caddy 的优势如下:

  • 自动的 http 证书申请(ACME HTTP/DNS 挑战)
  • 自动证书续期以及 OCSP stapling 等
  • 更高的安全性包括但不限于 TLS 配置以及内存安全等
  • 友好且强大的配置文件支持
  • 支持 API 动态调整配置(有木有人可以搞个 Dashboard?)
  • 支持 HTTP3(QUIC)
  • 支持动态后端,例如连接 Consul、作为 k8s ingress 等
  • 后端多种负载策略以及健康检测等
  • 本身 Go 编写,高度模块化的系统方便扩展(CoreDNS 基于 Caddy1 开发)
  • ……

就目前来说,Caddy 对于我个人印象唯一的缺点就是性能没有 Nginx 高,但是这是个仁者见仁智者见智的问题;相较于提供的这些便利性,在性能可接受的情况下完全有理由切换到 Caddy。

一、编译 Caddy2

注意: 在 Caddy1 时代,Caddy 官方发布的预编译二进制文件是不允许进行商业使用的,Caddy2 以后已经全部切换到 Apache 2.0 License,具体请参考 issue#2786

在默认情况下 Caddy2 官方提供了预编译的二进制文件,以及自定义 build 下载页面,不过对于需要集成一些第三方插件时,我们仍需采用官方提供的 xcaddy 来进行自行编译;以下为具体的编译过程:

1.1、Golang 环境安装

本部分编译环境默认为 Ubuntu 20.04 系统,同时使用 root 用户,其他环境请自行调整相关目录以及配置;编译时自行处理好科学上网相关配置,也可以直接用国外 VPS 服务器编译。

首先下载 go 语言的 SDK 压缩包,其他平台可以从 http://golang.org/dl/ 下载对应的压缩包:

wget http://golang.org/dl/go1.15.6.linux-amd64.tar.gz

下载完成后解压并配置相关变量:

# 解压tar -zxvf go1.15.6.linux-amd64.tar.gz# 移动到任意目录mkdir -p /opt/devtoolsmv go /opt/devtools/go# 创建 go 相关目录mkdir -p ${HOME}/gopath/{src,bin,pkg}# 调整变量配置,将以下变量加入到 shell 初始化配置中# bash 用户请编辑 ~/.bashrc# zsh 用户请编辑 ~/.zshrcexport GOROOT='/opt/devtools/go'export GOPATH="${HOME}/gopath"export GOPROXY='http://goproxy.cn' # 如果已经解决了科学上网问题,GOPROXY 变量可以删除,否则可能会起反作用export PATH="${GOROOT}/bin:${GOPATH}/bin:${PATH}"# 让配置生效# bash 用户替换成 ~/.basrc# 重新退出登录也可以source ~/.zshrc

配置完成后,应该在命令行执行 go version 有成功返回:

bleem ➜ ~ go versiongo version go1.15.6 linux/amd64

1.2、安装 xcaddy

按照官方文档直接命令行执行 go get -u github.com/caddyserver/xcaddy/cmd/xcaddy 安装即可:

bleem ➜ ~ go get -u github.com/caddyserver/xcaddy/cmd/xcaddygo: downloading github.com/caddyserver/xcaddy v0.1.7go: found github.com/caddyserver/xcaddy/cmd/xcaddy in github.com/caddyserver/xcaddy v0.1.7go: downloading github.com/Masterminds/semver/v3 v3.1.0go: github.com/Masterminds/semver/v3 upgrade => v3.1.1go: downloading github.com/Masterminds/semver/v3 v3.1.1.....

安装完成后应当在命令行可以直接执行 xcaddy 命令:

# xcaddy 并没有提供完善的命令行支持,所以 `--help` 报错很正常bleem ➜  ~ xcaddy --helpgo: cannot match "all": working directory is not part of a module2021/01/07 12:15:56 [ERROR] exec [go list -m -f={{if .Replace}}{{.Path}} => {{.Replace}}{{end}} all]: exit status 1:

1.3、编译 Caddy2

编译之前系统需要安装 jqcurlgit 命令,没有的请使用 apt install -y curl git jq 命令安装;

自行编译的目的是增加第三方插件方便使用,其中官方列出的插件可以从 Download 页面获取到:

XkJZ2R1609993722272

其他插件可以从 GitHub 上寻找或者自行编写,整理好这些插件列表以后只需要使用 xcaddy 编译即可:

# 获取最新版本号,其实直接去 GitHub realse 页复制以下就行# 这里转化为脚本是为了方便自动化export version=$(curl -s "http://api.github.com/repos/caddyserver/caddy/releases/latest" | jq -r .tag_name)# 使用 xcaddy 编译xcaddy build ${version} --output ./caddy_${version} \        --with github.com/abiosoft/caddy-exec \        --with github.com/caddy-dns/cloudflare \        --with github.com/caddy-dns/dnspod \        --with github.com/caddy-dns/duckdns \        --with github.com/caddy-dns/gandi \        --with github.com/caddy-dns/route53 \        --with github.com/greenpau/caddy-auth-jwt \        --with github.com/greenpau/caddy-auth-portal \        --with github.com/greenpau/caddy-trace \        --with github.com/hairyhenderson/caddy-teapot-module \        --with github.com/kirsch33/realip \        --with github.com/porech/caddy-maxmind-geolocation \        --with github.com/caddyserver/format-encoder \        --with github.com/mholt/caddy-webdav

编译过程日志如下所示,稍等片刻后将会生成编译好的二进制文件:

Kr2tG61609993987722

编译成功后可以通过 list-modules 子命令查看被添加的插件是否成功编译到了 caddy 中:

bleem ➜  ~ ./caddy_v2.3.0 list-modulesadmin.api.loadadmin.api.metricscaddy.adapters.caddyfilecaddy.listeners.tlscaddy.logging.encoders.consolecaddy.logging.encoders.filtercaddy.logging.encoders.filter.deletecaddy.logging.encoders.filter.ip_maskcaddy.logging.encoders.formattedcaddy.logging.encoders.jsoncaddy.logging.encoders.logfmtcaddy.logging.encoders.single_fieldcaddy.logging.writers.discardcaddy.logging.writers.filecaddy.logging.writers.netcaddy.logging.writers.stderrcaddy.logging.writers.stdoutcaddy.storage.file_systemdns.providers.cloudflaredns.providers.dnspoddns.providers.duckdnsdns.providers.gandidns.providers.route53exechttphttp.authentication.hashes.bcrypthttp.authentication.hashes.scrypthttp.authentication.providers.http_basichttp.authentication.providers.jwt......

二、安装 Caddy2

2.1、宿主机安装

宿主机安装 Caddy2 需要使用 systemd 进行守护,幸运的是 Caddy2 官方提供了各种平台的安装包以及 systemd 配置文件仓库;目前推荐的方式是直接采用包管理器安装标准版本的 Caddy2,然后替换自编译的可执行文件:

# 安装标准版本 Caddy2sudo apt install -y debian-keyring debian-archive-keyring apt-transport-httpcurl -1sLf 'http://dl.cloudsmith.io/public/caddy/stable/cfg/gpg/gpg.155B6D79CA56EA34.key' | sudo apt-key add -curl -1sLf 'http://dl.cloudsmith.io/public/caddy/stable/cfg/setup/config.deb.txt?distro=debian&version=any-version' | sudo tee -a /etc/apt/sources.list.d/caddy-stable.listsudo apt updatesudo apt install caddy# 替换二进制文件systemctl stop caddyrm -f /usr/bin/caddymv ./caddy_v2.3.0 /usr/bin/caddy

2.2、Docker 安装

Docker 用户可以通过 Dockerfile 自行编译 image,目前我编写了一个基于 xcaddy 的 Dockerfile,如果有其他插件需要集成自行修改重新编译即可;当前 Dockerfile 预编译的镜像已经推送到了 Docker Hub 中,镜像名称为 mritd/caddy

三、配置 Caddy2

Caddy2 的配置文件核心采用 json,但是 json 可读性不强,所以官方维护了一个转换器,抽象出称之为 Caddyfile 的新配置格式;关于 Caddyfile 的完整语法请查看官方文档 http://caddyserver.com/docs/caddyfile,本文仅做一些基本使用的样例。

3.1、配置片段

Caddyfile 支持类似代码中 function 一样的配置片段,这些配置片段可以在任意位置被 import,同时可以接受参数,以下为配置片断示例:

# 括号内为片段名称,可以自行定义(TLS) {    protocols tls1.2 tls1.3    ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256}# 在任意位置可以引用此片段从而达到配置复用import TLS

3.2、配置模块化

import 指令除了支持引用配置片段以外,还支持引用外部文件,同时支持通配符,有了这个命令以后我们就可以方便的将配置文件进行模块化处理:

# 引用外部的 /etc/caddy/*.caddyimport /etc/caddy/*.caddy

3.3、站点配置

针对于站点域名配置,Caddyfile 比较自由化,其格式如下:

地址 {    站点配置}

关于这个 “地址” 接受多种格式,以下都为合法的地址格式:

  • localhost
  • example.com
  • :443
  • http://example.com
  • localhost:8080
  • 127.0.0.1
  • [::1]:2015
  • example.com/foo/*
  • *.example.com
  • http://

3.4、环境变量

Caddyfile 支持直接引用系统环境变量,通过此功能可以将一些敏感信息从配置文件中剔除:

# 引用环境变量 GANDI_API_TOKENdns gandi {$GANDI_API_TOKEN}

3.5、配置片段参数支持

针对于配置片段,Caddyfile 还支持类似于函数代码的参数支持,通过参数支持可以让外部引用时动态修改配置信息:

(LOG) {    log {        format json  {            time_format "iso8601"        }        # "{args.0}" 引用传入的第一个参数,此处用于动态传入日志文件名称        output file "{args.0}" {            roll_size 100mb            roll_keep 3            roll_keep_for 7d        }    }}# 引用片段import LOG "/data/logs/mritd.com.log"

3.6、自动证书申请

在启动 Caddy2 之前,如果目标域名(例如: www.example.com)已经解析到了本机,那么 Caddy2 启动后会尝试自动通过 ACME HTTP 挑战申请证书;如果期望使用 DNS 的方式申请证书则需要其他 DNS 插件支持,比如上面编译的 --with github.com/caddy-dns/gandi 为 gandi 服务商的 DNS 插件;关于使用 DNS 挑战的配置编写方式需要具体去看其插件文档,目前 gandi 的配置如下:

tls {dns gandi {env.GANDI_API_TOKEN}}

配置完成后 Caddy2 会通过 ACME DNS 挑战申请证书,值得注意的是即使通过 DNS 申请证书默认也不会申请泛域名证书,如果想要调整这种细节配置请使用 json 配置或管理 API。

3.7、完整模块化配置样例

了解了以上基础配置信息,我们就可以实际编写一个站点配置了;以下为本站的 Caddy 配置样例:

目录结构:

caddy├── Caddyfile├── mritd.com.caddy└── pureyukon.com.caddy

3.7.1、Caddyfile

Caddyfile 主要包含一些通用的配置,并将其抽到配置片段中,类似与 nginx 的 nginx.conf 主配置;在最后部分通过 import 关键字引入其他具体站点配置,类似 nginx 的 vhost 配置。

(LOG) {    log {        # 日志格式参考 http://github.com/caddyserver/format-encoder 插件文档        format formatted "[{ts}] {request>remote_addr} {request>proto} {request>method} <- {status} -> {request>host} {request>uri} {request>headers>User-Agent>[0]}"  {            time_format "iso8601"        }        output file "{args.0}" {            roll_size 100mb            roll_keep 3            roll_keep_for 7d        }    }}(TLS) {    # TLS 配置采用 http://mozilla.github.io/server-side-tls/ssl-config-generator/ 生成,SSL Labs 评分 A+    protocols tls1.2 tls1.3    ciphers TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256}(HSTS) {    # HSTS (63072000 seconds)    header / Strict-Transport-Security "max-age=63072000"}(ACME_GANDI) {    # 从环境变量获取 GANDI_API_TOKEN    dns gandi {$GANDI_API_TOKEN}}# 聚合上面的配置片段为新的片段(COMMON_CONFIG) {    # 压缩支持    encode zstd gzip    # TLS 配置    tls {        import TLS        import ACME_GANDI    }    # HSTS    import HSTS}# 开启 HTTP3 实验性支持{    servers :443 {        protocol {            experimental_http3        }    }}# 引入其他具体的站点配置import /etc/caddy/*.caddy

3.7.2、mritd.com.caddy

mritd.com.caddy 为主站点配置,主站点配置内主要编写一些路由规则,TLS 等都从配置片段引入,这样可以保持统一。

www.mritd.com {    # 重定向到 mritd.com(默认 302)    redir http://mritd.com{uri}    # 日志    import LOG "/data/logs/mritd.com.log"    # TLS、HSTS、ACME 等通用配置    import COMMON_CONFIG}mritd.com {    # 路由    route /* {        reverse_proxy mritd_com:80    }    # 日志    import LOG "/data/logs/mritd.com.log"    # TLS、HSTS、ACME 等通用配置    import COMMON_CONFIG}

3.7.3、pureyukon.com.caddy

pureyukon.com.caddy 为老站点配置,目前主要将其 301 到新站点即可。

www.pureyukon.com {    # 重定向到 mritd.com    # 最后的 "code" 支持三种参数    # temporary => 302    # permanent => 301    # html => HTML document redirect    redir http://mritd.com{uri} permanent    # 日志    import LOG "/data/logs/mritd.com.log"    # TLS、HSTS、ACME 等通用配置    import COMMON_CONFIG}pureyukon.com {    # 重定向    redir http://mritd.com{uri} permanent    # 日志    import LOG "/data/logs/mritd.com.log"    # TLS、HSTS、ACME 等通用配置    import COMMON_CONFIG}

四、启动与重载

配置文件编写完成后,通过 systemctl start caddy 可启动 caddy 服务器;每次配置修改后可以通过 systemctl reload caddy 进行配置重载,重载期间 caddy 不会重启(实际上调用 caddy reload 命令),当配置文件书写错误时,重载只会失败,不会影响正在运行的 caddy 服务器。

五、总结

本文只是列举了一些简单的 Caddy 使用样例,在强大的插件配合下,Caddy 可以实现各种 “神奇” 的功能,这些功能依赖于复杂的 Caddy 配置,Caddy 配置需要仔细阅读官方文档,关于 Caddyfile 的每个配置段在文档中都有详细的描述。

值得一提的是 Caddy 本身内置了丰富的插件,例如内置 “file_server”、内置各种负载均衡策略等,这些插件组合在一起可以实现一些复杂的功能;Caddy 是采用 go 编写的,官方也给出了详细的开发文档,相较于 Nginx 来说通过 Lua 或者 C 来开发编写插件来说,Caddy 的插件开发上手要容易得多;Caddy 本身针对数据存储、动态后端、配置文件转换等都内置了扩展接口,这为有特定需求的扩展开发打下了良好基础。

最终总结,综合来看目前 Caddy2 的性能损失可接受的情况下,相较于 Nginx 绝对是个绝佳选择,各种新功能都能够满足现代化 Web 站点的需求,真香警告。

]]>
Linux Caddy http://mritd.com/2021/01/07/lets-start-using-caddy2/#disqus_thread
Skywalking 初试 http://mritd.com/2020/11/27/how-to-deploy-skywalking-on-kubernetes/ http://mritd.com/2020/11/27/how-to-deploy-skywalking-on-kubernetes/ Fri, 27 Nov 2020 07:50:32 GMT 在 Skywalking 刚发布的时候就开始关注这个玩意了,一直没有时间去测试;最近正好新项目上线,顺手把 Skywalking 搞起来了,下面简单记录一下 Kubernetes 下的安装使用。

在 Skywalking 刚发布的时候就开始关注这个玩意了,一直没有时间去测试;最近正好新项目上线,顺手把 Skywalking 搞起来了,下面简单记录一下 Kubernetes 下的安装使用。

一、先决条件

确保有一套运行正常的 Kubernetes 集群,本文默认为使用 Elasticsearch7 作为后端存储;如果想把 ES 放到 Kubernetes 集群里那么还得确保集群配置了正确的存储,譬如默认的 StorageClass 可用等。本文为了方便起见(其实就是穷)采用外部 ES 存储且使用 docker-compose 单节点部署,所以不需要集群的分布式存储;最后确保你本地的 kubectl 能够正常运行。

二、基本架构

Skywalking 在大体上(不准确)分为四大部分:

  • oap-server: 无状态服务后端,主要负责处理核心逻辑,可以简单理解为一个标准 java web 项目。
  • skywalking-ui: UI 前端,通过 graphql 连接 oap-server 提供用户查询等 UI 展示。
  • agent: 各种语言实现的 agent 负责抓取应用运行数据并上报给 oap-server,核心的指标上报来源。
  • DB: 各种数据库,负责存储 Skywalking 的指标数据,生产环境推荐 ES、TiDB、MySQL。

三、部署 Skywalking

3.1、部署 Elasticsearch

Elasticsearch 当前使用 7.9.2 版本,由于只是初次尝试还处于测试阶段所以直接 docker-compose 启动一个单点:

# 如果有需要可以进入 es 容器使用以下命令设置密码# elasticsearch-setup-passwords interactiveversion: '3.8'services:  elasticsearch:    container_name: elasticsearch    image: docker.elastic.co/elasticsearch/elasticsearch:7.9.2    restart: always    network_mode: "host"    volumes:      - data:/data/elasticsearch    environment:      - http.host=172.16.11.43      - http.port=9200      - transport.tcp.port=172.16.11.43      - transport.tcp.port=9300      - cluster.name=skyes      - node.name=skyes      - discovery.type=single-node      - xpack.security.enabled=true      - xpack.monitoring.enabled=true      - "ES_JAVA_OPTS=-Xms4096m -Xmx7168m"volumes:  data:

3.2、安装 Helm

由于 Skywalking 官方给出的 Kubernetes 安装方式为 Helm 安装,所以需要本地先安装 Helm;Helm 安装方式非常简单,根据官方文档在网络没问题的情况下直接执行以下命令即可:

curl http://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash

如果网络不是那么 OK 的情况下请参考官方文档的包管理器方式安装或直接下载二进制文件安装。

3.3、克隆仓库初始化 Helm

Helm 部署之前按照官方文档提示需要先初始化 Helm 仓库:

# clone helm 仓库git clone http://github.com/apache/skywalking-kubernetescd skywalking-kubernetes/chart# 即使使用外部 ES 也要添加这个 repo,否则会导致依赖错误helm repo add elastic http://helm.elastic.cohelm dep up skywalking# change the release name according to your scenarioexport SKYWALKING_RELEASE_NAME=skywalking# 如果修改了 NAMESPACE,后续部署则需要先通过 kuebctl 创建该 NAMESPACE# change the namespace according to your scenarioexport SKYWALKING_RELEASE_NAMESPACE=default

3.4、安装 Skywalking

Helm 初始化完成后需要自行调整配置文件,配置 oap-server 使用外部 ES

values-my-es.yaml

oap:  image:    tag: 8.1.0-es7      # Set the right tag according to the existing Elasticsearch version  storageType: elasticsearch7ui:  image:    tag: 8.1.0elasticsearch:  enabled: false  config:               # For users of an existing elasticsearch cluster,takes effect when `elasticsearch.enabled` is false    host: 172.16.11.43    port:      http: 9200    user: "elastic"    password: "elastic"

调整好配置后只需要使用 Helm 安装即可:

helm install "${SKYWALKING_RELEASE_NAME}" skywalking -n "${SKYWALKING_RELEASE_NAMESPACE}" \  -f ./skywalking/values-my-es.yaml --set oap.image.tag=8.2.0-es7 --set ui.image.tag=8.2.0

如果安装出错或者其他问题可以使用以下命令进行卸载:

helm uninstall "${SKYWALKING_RELEASE_NAME}" skywalking -n "${SKYWALKING_RELEASE_NAMESPACE}"

安装成功后应该在 ${SKYWALKING_RELEASE_NAMESPACE} 下看到相关 Pod:

k8s21 ➜  ~ kubectl get pod -o wide -n skywalkingNAME                              READY   STATUS      RESTARTS   AGE   IP             NODE    NOMINATED NODE   READINESS GATESskywalking-es-init-xw6tx          0/1     Completed   0          32h   10.30.0.62     k8s21   <none>           <none>skywalking-oap-64c65cc6bb-lnq82   1/1     Running     0          32h   10.30.0.61     k8s21   <none>           <none>skywalking-oap-64c65cc6bb-q7zj8   1/1     Running     0          32h   10.30.32.103   k8s22   <none>           <none>skywalking-ui-695ff9d69d-wqcm8    1/1     Running     0          32h   10.30.161.42   k8s25   <none>           <none>

在确认 Pod 都运行正常后可以通过 kubectl port-forward 命令来查看 UI 界面:

# 执行以下命令,访问 127.0.0.1:8080 即可访问到 skywalking-uikubectl port-forward -n ${SKYWALKING_RELEASE_NAMESPACE} service/skywalking-ui 8080:80

在生产环境可能需要配置正确的 Ingress 或者 NodePort 等方式暴露 skywalking-ui 服务,具体取决于生产集群服务暴露方式,请自行调整。

四、Agent 配置

由于目前仅在 Java 项目上测试,所以以下 Agent 配置仅仅对 Java 项目有效。

Skywalking 在简单使用时不需要侵入代码,对于 jar 包启动的项目只需要在启动时增加 -javaagent 选项即可。

4.1、Agent 获取

javaagent 可以通过下载对应的 skywalking release 安装包获取,将此 agent 目录解压到任意位置,稍后将添加到 java 启动参数。

agent_dir

4.2、Agent 配置

Agent 主配置文件存放在 config/agent.config 配置文件中,配置文件内支持环境变量读取,可以自行添加其他配置和引用其他变量;通常这个配置文件在容器化时有两种选择,一种是创建 ConfigMap,然后通过 ConfigMap 挂载到容器里进行覆盖;另一种是在默认配置里引用各种变量,在容器启动时通过环境变量注入。这里暂时使用环境变量注入的方式:

agent.config

agent.config

deployment.yml

deployment.yml

调整完成后,应用运行一段时间后应该能在 UI 中看到数据

skwalking-ui

五、注意事项

  • 默认情况下 Helm 相关命令执行缓慢,可能需要设置 http(s)_proxy ...( _ _)ノ|壁(自行体会这个表情)
  • Skywalking 镜像一般比较大,下载缓慢,推荐预先拉取好然后 load 到每个节点
  • ES 如果设置了密码,不要忘记在 Helm 安装时调整好密码配置
  • jar 包启动时 -javaagent 不能放在 -jar 选项之后,否则可能不生效
  • 集群内连接 oap-server 推荐通过 skywalking-oap.skywalking.svc.cluster.local 域名服务发现方式寻址
]]>
Kubernetes Skywalking http://mritd.com/2020/11/27/how-to-deploy-skywalking-on-kubernetes/#disqus_thread
利用 etcdhosts 插件搭建分布式 CoreDNS http://mritd.com/2020/11/17/set-up-coredns-ha-clsuter-by-etcdhosts/ http://mritd.com/2020/11/17/set-up-coredns-ha-clsuter-by-etcdhosts/ Tue, 17 Nov 2020 02:01:28 GMT 目前宿主机上全部采用的 dnsmasq 作为 DNS 管理,其中有一个很大的问题是需要进行 DNS 冗余,dnsmasq 每次修改都要多台机器同步,所以自己写了一个插件配合 CoreDNS 实现分布式部署,如果想了解插件编写方式请参考 [Writing Plugin for Coredns](http://mritd.com/2019/11/05/writing-plugin-for-coredns/)。

目前宿主机上全部采用的 dnsmasq 作为 DNS 管理,其中有一个很大的问题是需要进行 DNS 冗余,dnsmasq 每次修改都要多台机器同步,所以自己写了一个插件配合 CoreDNS 实现分布式部署,如果想了解插件编写方式请参考 Writing Plugin for Coredns

一、etcdhosts 插件简介

etcdhosts 顾名思义,就是将 hosts 文件存储在 Etcd 中,然后多个 CoreDNS 共享一份 hosts 文件;得益于 Etcd 提供的 watch 功能,当 Etcd 中的 hosts 文件更新时,每台 CoreDNS 服务器都会接到推送,同时完成热重载;etcdhosts 基本架构如下:

+-----------------------------------------------------------------------------+|                                                                             ||   +-----------+                                                             ||   |           |                                                             ||   |  CoreDNS  +---------------------+                                       ||   |           |                     |                                       ||   +-----------+                     |                +------------------+   ||                                     |                |                  |   ||                            +--------v---------+      |                  |   ||   +-----------+            |                  |      |                  |   ||   |           |            |                  |      | dnsctl or        |   ||   |  CoreDNS  +------------>   Etcd Cluster   <------+ other etcd tool  |   ||   |           |            |                  |      |                  |   ||   +-----------+            |                  |      |                  |   ||                            +---------^--------+      |                  |   ||                                      |               |                  |   ||   +-----------+                      |               +------------------+   ||   |           |                      |                                      ||   |  CoreDNS  +----------------------+                                      ||   |           |                                                             ||   +-----------+                                                             ||                                                                             ||                                                                             |+-----------------------------------------------------------------------------+

二、编译 CoreDNS

etcdhosts release 页已经提供部分版本的预编译文件,可以直接下载使用。

etcdhosts 作为一个 CoreDNS 扩展插件采用直接偶合的方式编写(未采用 gRPC 是因为考虑性能影响),这意味着需要重新编译 CoreDNS 来集成插件,以下为 CoreDNS 编译过程(使用 docker):

# clone source codegit clone http://github.com/ytpay/etcdhosts.git# buildcd etcdhosts && ./build v1.8.0

编译完成后将在 build 目录下生成各个平台的二进制文件压缩包。

三、搭建 Etcd 集群

Etcd 集群搭建将直接采用 deb 安装包,具体细节这里不再阐述,本次搭建系统为 Ubuntu 20,以下为搭建步骤。

2.1、安装软件包

# 下载 cfssl 安装包,用于签署证书wget http://github.com/mritd/etcd-deb/releases/download/v3.4.13/cfssl_1.4.1_amd64.deb# 下载 etcd 安装包wget http://github.com/mritd/etcd-deb/releases/download/v3.4.13/etcd_3.4.13_amd64.deb# 执行安装dpkg -i cfssl_1.4.1_amd64.deb etcd_3.4.13_amd64.deb

2.2、创建证书

创建证书需要先修改证书配置文件(etcd-csr.json)然后借助 cfssl 工具来创建证书

/etc/etcd/cfssl/etcd-csr.json

{    "key": {        "algo": "rsa",        "size": 2048    },    "names": [        {            "O": "etcd",            "OU": "etcd Security",            "L": "Beijing",            "ST": "Beijing",            "C": "CN"        }    ],    "CN": "etcd",    "hosts": [        "127.0.0.1",        "localhost",        "*.etcd.node",        "*.kubernetes.node",+       "172.16.11.71",+       "172.16.11.72",+       "172.16.11.73"    ]}

通过脚本创建证书

cd /etc/etcd/cfssl./create.shcp *.pem /etc/etcd/ssl

证书创建完成后需要分发到其他两台机器上,保证三台节点的 /etc/etcd/ssl 目录证书相同。

# 复制证书scp /etc/etcd/ssl/*.pem root@NODE2:/etc/etcd/sslscp /etc/etcd/ssl/*.pem root@NODE3:/etc/etcd/ssl# 修复权限(三台都要修复)chown -R etcd:etcd /etc/etcd/

2.3、调整集群配置

证书签署完成后,简单的调整每台机器上的集群节点配置即可

/etc/etcd/etcd.conf

# [member]+ # 节点号自行修改,推荐格式: etcd+节点IP,例如 etcd21+ ETCD_NAME=etcd1ETCD_DATA_DIR="/var/lib/etcd/data"ETCD_WAL_DIR="/var/lib/etcd/wal"ETCD_SNAPSHOT_COUNT="100"+ # 修改为当前机器 IP+ ETCD_LISTEN_PEER_URLS="http://172.16.11.71:2380"+ # 修改为当前机器 IP+ ETCD_LISTEN_CLIENT_URLS="http://172.16.11.71:2379,http://127.0.0.1:2379"ETCD_QUOTA_BACKEND_BYTES="8589934592"ETCD_MAX_REQUEST_BYTES="10485760"# [cluster]+ # 修改为当前机器 IP+ ETCD_INITIAL_ADVERTISE_PEER_URLS="http://172.16.11.71:2380"# if you use different ETCD_NAME (e.g. test), set ETCD_INITIAL_CLUSTER value for this name, i.e. "test=http://..."+ # 三台机器都要按照格式写好+ ETCD_INITIAL_CLUSTER="etcd1=http://172.16.11.71:2380,etcd2=http://172.16.11.72:2380,etcd3=http://172.16.11.73:2380"ETCD_INITIAL_CLUSTER_STATE="new"ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"+ # 修改为当前机器 IP+ ETCD_ADVERTISE_CLIENT_URLS="http://172.16.11.71:2379"ETCD_AUTO_COMPACTION_MODE="revision"ETCD_AUTO_COMPACTION_RETENTION="16"ETCD_QUOTA_BACKEND_BYTES="5368709120"# [security]ETCD_CERT_FILE="/etc/etcd/ssl/etcd.pem"ETCD_KEY_FILE="/etc/etcd/ssl/etcd-key.pem"ETCD_TRUSTED_CA_FILE="/etc/etcd/ssl/etcd-root-ca.pem"ETCD_CLIENT_CERT_AUTH="true"ETCD_AUTO_TLS="true"ETCD_PEER_CERT_FILE="/etc/etcd/ssl/etcd.pem"ETCD_PEER_KEY_FILE="/etc/etcd/ssl/etcd-key.pem"ETCD_PEER_CLIENT_CERT_AUTH="true"ETCD_PEER_TRUSTED_CA_FILE="/etc/etcd/ssl/etcd-root-ca.pem"ETCD_PEER_AUTO_TLS="true"

最后每台机器执行 systemctl start etcd 启动即可,验证集群是否健康可以使用如下命令测试:

etcdctl endpoint health --cert /etc/etcd/ssl/etcd.pem --key /etc/etcd/ssl/etcd-key.pem --cacert /etc/etcd/ssl/etcd-root-ca.pem --endpoints http://172.16.11.71:2379,http://172.16.11.72:2379,http://172.16.11.73:2379http://172.16.11.71:2379 is healthy: successfully committed proposal: took = 33.07493mshttp://172.16.11.72:2379 is healthy: successfully committed proposal: took = 32.132266mshttp://172.16.11.73:2379 is healthy: successfully committed proposal: took = 40.745291ms

三、搭建 CoreDNS 集群

3.1、CoreDNS 安装

系统级 CoreDNS 安装推荐直接使用 systemd 管理,官方目前提供了 systemd 相关配置文件: http://github.com/coredns/deployment/tree/master/systemd

# 安装二进制文件tar -zxvf coredns_1.8.0_linux_amd64.tgzmv coredns /usr/bin/coredns# 安装 systemd 配置wget http://raw.githubusercontent.com/coredns/deployment/master/systemd/coredns-sysusers.conf -O /usr/lib/sysusers.d/coredns-sysusers.confwget http://raw.githubusercontent.com/coredns/deployment/master/systemd/coredns-tmpfiles.conf -O /usr/lib/tmpfiles.d/coredns-tmpfiles.confwget http://raw.githubusercontent.com/coredns/deployment/master/systemd/coredns.service -O /usr/lib/systemd/system/coredns.service# reloadsystemctl daemon-reload# 初始化用户systemd-sysusers# 初始化临时目录systemd-tmpfiles --create# 创建配置目录mkdir -p /etc/coredns/ssl

3.2、etcdhosts 配置

etcdhosts 的配置类似官方的 etcd 插件,其配置格式如下:

etcdhosts [ZONES...] {    [INLINE]    ttl SECONDS    no_reverse    fallthrough [ZONES...]    key ETCD_KEY    endpoint ETCD_ENDPOINT...    credentials ETCD_USERNAME ETCD_PASSWORD    tls ETCD_CERT ETCD_KEY ETCD_CACERT    timeout ETCD_TIMEOUT}

以下是一个简单的可启动的样例配置:

/etc/coredns/Corefile

. {    # 绑定接口地址    bind 172.16.11.71    # cache    cache 30 . {        success 4096    }    # etcdhosts 配置    etcdhosts . {        fallthrough .        key /etcdhosts        timeout 5s        tls /etc/coredns/ssl/etcd.pem /etc/coredns/ssl/etcd-key.pem /etc/coredns/ssl/etcd-root-ca.pem        endpoint http://172.16.11.71:2379 http://172.16.11.72:2379 http://172.16.11.73:2379    }    # 上游 DNS 配置    forward . 114.114.114.114:53 {        max_fails 2        expire 20s        policy random        health_check 0.2s    }    # 日志配置    errors    log . "{remote}:{port} - {>id} \"{type} {class} {name} {proto} {size} {>do} {>bufsize}\" {rcode} {>rflags} {rsize} {duration}"}

由于 etcdhosts 插件需要连接 etcd 集群,所以需要将证书复制到 Corefile 指定的位置:

# 实际生产环境 coredns 与 etcd 一般不在一台机器上,请自行 scpcp /etc/etcd/ssl/*.pem /etc/coredns/ssl# 修复权限chown -R coredns:coredns /etc/coredns

最后直接启动即可(首次启动会出现 [ERROR] plugin/etcdhosts: invalid etcd response: 0 错误,属于正常情况):

# 启动systemctl start coredns# 测试dig @172.16.11.71 baidu.com; <<>> DiG 9.16.1-Ubuntu <<>> @172.16.11.71 baidu.com; (1 server found);; global options: +cmd;; Got answer:;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 35323;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1;; OPT PSEUDOSECTION:; EDNS: version: 0, flags:; udp: 4096; COOKIE: 8e3137531ed0b57a (echoed);; QUESTION SECTION:;baidu.com.                     IN      A;; ANSWER SECTION:baidu.com.              30      IN      A       220.181.38.148baidu.com.              30      IN      A       39.156.69.79;; Query time: 8 msec;; SERVER: 172.16.11.71#53(172.16.11.71);; WHEN: Mon Nov 16 20:18:25 CST 2020;; MSG SIZE  rcvd: 100

最后在多台机器上通过同样的配置启动 CoreDNS 即可,此时所有 CoreDNS 服务器通过 Etcd 提供一致性的记录解析。

四、记录调整

所有 CoreDNS 启动成功后,默认 etcdhosts 插件将会读取 Etcd 中的 /etcdhosts key 作为 hosts 文件载入;载入成功后将会在内存级进行 Cache,多次查询不会造成疯狂的 Etcd 请求,只有当触发 reload 时(包括 Etcd 更新)才会重新查询 Etcd。所以此时只需要向 Etcd 的 /etcdhosts key 写入一个 hosts 文件即可;写入 Etcd 可以使用 etcdctl 以及其他的开源工具,甚至自己开发都可以,记录更改只需要跟 Etcd 打交道,不需要理会 CoreDNS;由于本人实在是比较菜,前端页面写不出来,所以弄了一个命令行版本的工具: dnsctl

dnsctl 只有一个可执行文件,默认情况下 dnsctl 读取 $HOME/.dnsctl.yaml 配置文件来沟通 Etcd,配置文件格式如下:

# etcd 中 etcdhosts 插件的 keydnskey: /etcdhosts# etcd 集群配置etcd:  cert: /etc/etcd/ssl/etcd.pem  key: /etc/etcd/ssl/etcd-key.pem  ca: /etc/etcd/ssl/etcd-root-ca.pem  endpoints:    - http://172.16.11.71:2379    - http://172.16.11.72:2379    - http://172.16.11.73:2379

dnsctl 提供如下命令

dnsctl for etcdhosts pluginUsage:  dnsctl [flags]  dnsctl [command]Available Commands:  config      show example config  dump        dump hosts  edit        edit hosts  help        Help about any command  upload      upload hosts from file  version     show hosts versionFlags:      --config string   config file (default is $HOME/.dnsctl.yaml)  -h, --help            help for dnsctl  -v, --version         version for dnsctlUse "dnsctl [command] --help" for more information about a command.

其中 edit 命令将会打开系统默认编辑器(例如 vim),然后编辑完保存后会自动上传到 Etcd 中,此后 CoreDNS 的 etcdhosts 插件将会立即重载;**dump 命令用于将 Etcd 中的 hosts 文件保存到本地用于备份,upload 命令可以将已有的 hosts 文件上传到 Etcd 用于恢复。**

]]>
Golang CoreDNS etcdhosts http://mritd.com/2020/11/17/set-up-coredns-ha-clsuter-by-etcdhosts/#disqus_thread
GIGABYTE Z370 AORUS Gaming 5 关闭 CFG 锁 http://mritd.com/2020/10/16/gigabyte-z370-aorus-gaming-5-disable-cfg-lock/ http://mritd.com/2020/10/16/gigabyte-z370-aorus-gaming-5-disable-cfg-lock/ Thu, 15 Oct 2020 18:51:00 GMT 最近在狂折腾黑苹果,从以前的 Clover 换成了 OC,迫于主板 CFG Lock 导致没法继续优化,折腾好久找到了解决方案。 一、解锁原理

由于主板不支持设置 CFG Lock,所以只能借助第三方工具强行解锁;首要前提是需要知道 CFG Lock 的设置地址,不同型号主板甚至不同版本的 BIOS 都不一定相同,所以 CFG Lock 地址不要照搬;本次操作用到的工具如下:

  • 主板的 BIOS 文件(自行去官网下载,并且版本要和当前一致)
  • UEFITool: 用于读取 BIOS 文件并搜索 CFG Lock 所在 Section
  • ifrextract: 用于将对应 Section 的 efi 文件转换为纯文本
  • modGRUBShell.efi: 修改版的 grub UEFI Shell 提供 setup_var_3 命令来修改 CFG Lock

二、获取 CFG Lock 地址

2.1、提取 BIOS Section

mac 下打开 UEFITool,选择 File > Open image file,文件选择框点击选项按钮切换成 All files 模式否则由于文件扩展名不同可能无法选中,然后选择主板 BIOS 文件。

OVx3zw-1602786074-uLkuRk

4OWosE-1602786158-wwWXdf

然后 command + F 切换到 Text 模式搜索 CFG Lock,接着双击下面的搜索结果会定位到对应的 Section。

miL7Qf-1602786270-thU0US

接下来右键 Extract body 导出到桌面等任意文件夹既可。

qfbB5v-1602786419-Ys0a4P

2.2、转换为 Text 文本

提取到 [Section Name].efi 文件后命令行执行 ifrextract [Section Name].efi cfg.txt 导出为文本。

TfPsbJ-1602786563-uLZS4A

2.3、确定 CFG Lock 位置

导出文本后通过编辑器搜索 CFG Lock 字符串,其中 VarStoreInfo 后面的地址就是 CFG Lock 设置地址,请记录这个地址(最好用手机拍照)。

eCjJwX-1602786663-eyW0vz

三、关闭 CFG Lock

得到了 CFG Lock 地址以后一切都简单了,创建一个启动 U 盘然后执行命令既可;首先将 U 盘格式化为 GUID 分区表,然后挂载 EFI 分区,modGRUBShell.efi 重命名为 BOOTX64.efi 并放入 EFI/BOOT 目录。

YCHA7V-1602787150-1fqjYt

最后重启系统 BIOS 选择使用 U 盘启动,并在 grub shell 内执行 setup_var_3 0x529 0x0 然后重启即完成解锁;注意: 0x529 请替换为上面找到的实际地址,实际地址 0x*** 后面的 *** 如果有大写字母请保持大写;这部份就不上图了,懒得拍照。

]]>
Hackintosh CFG Lock http://mritd.com/2020/10/16/gigabyte-z370-aorus-gaming-5-disable-cfg-lock/#disqus_thread
网站切换到 Hexo http://mritd.com/2020/10/08/switch-jekyll-to-hexo/ http://mritd.com/2020/10/08/switch-jekyll-to-hexo/ Thu, 08 Oct 2020 11:04:00 GMT 坚持写博客大约有 5 年多的时间了,以前的博客一直采用 jekyll 框架,由于一直缺少搜索等功能,而自己又不会前端,最近干脆直接切换到 Hexo 了;这里记录一下折腾过程。 一、Hexo 安装

Hexo 安装根据官方文档直接操作即可,安装前提是需要先安装 Nodejs(这里不再阐述直接略过)

npm install -g hexo-cli

Hexo 命令行工具安装完成后可以直接初始化一个样例项目,init 过程会 clone http://github.com/hexojs/hexo-starter.git 到本地,同时自动安装好相关依赖

# mritd.com 为目录名,个人习惯直接使用网站域名作为目录名称hexo init mritd.com

进入目录启动样例站点

# 进入目录cd mritd.com# 启动本地服务器进行预览hexo serve

hexo_demo

二、主题设置

基本的样例博客启动完成后就需要选择一个主题,主题实质上才决定博客功能,这里目前使用了 Fluid 主题,这个主题目前兼具了个人博客所需的所有功能,而且作者提交比较活跃,文档也比较全面。

# 下载主题git clone http://github.com/fluid-dev/hexo-theme-fluid.git themes/fluid# 切换到最新版本(cd themes/fluid && git checkout -b v1.8.3 v1.8.3)

接下来修改 _config.yml 配置切换主题即可

# Extensions## Plugins: http://hexo.io/plugins/## Themes: http://hexo.io/themes/theme: fluid

然后重新启动博客进行预览: hexo cl && hexo s

fluid_demo

关于主题其他配置可自行阅读 官方文档,文档有时可能更新不及时,可同时参考仓库内的 _config.yml 配置。

三、文章导入

关于 jekyll 博客的文章如何导入到 Hexo 中网上有很多脚本;但是实际上两个静态博客框架都是支持标准的 Markdown 语法书写的文章进行渲染,唯一区别就是每篇文章上的 “头”。

---catalog: truecategories:  - [Kubernetes]  - [Golang]date: 2018-11-25 11:11:28excerpt: 最近在看 kubeadm 的源码,不过有些东西光看代码还是没法太清楚,还是需要实际运行才能看到具体代码怎么跑的,还得打断点 debug;无奈的是本机是 mac,debug 得在 Linux 下,so 研究了一下 remote debugkeywords: kubeadm,debugmultilingual: falsetags:  - Golang  - Kubernetestitle: 远程 Debug kubeadmindex_img: img/remote_debug.jpg---具体文章内容......

所以直接复制 jekyll 的 md 文件到 source/_posts 目录,并修改文档头部即可。

四、自动更新

目前博客部署在自己的 VPS 上,以前都是将博客生成的静态直接使用 nginx 发布出去的;但是面临的问题就是每次博客更新都要手动去 VPS 更新,虽然可以写一些 CI 脚本但是并不算智能;得益于 Golang 官方完善的标准库支持,这次直接几行代码写一个静态服务器,同时拦截特定 URL 来更新博客:

package mainimport ("fmt""net/http""os""os/exec""path")func main() {http.Handle("/", fileServerWithCustom404(http.Dir("/data")))http.HandleFunc("/update", update)fmt.Println("Updating WebSite...")_, err := gitPull()if err != nil {fmt.Printf("WebSite update failed: %s", err)}fmt.Println("HTTP Server Listen at [:8080]...")_ = http.ListenAndServe(":8080", nil)}// POST 请求 /update 触发 git pull 更新博客func update(w http.ResponseWriter, r *http.Request) {if r.Method != http.MethodPost {w.WriteHeader(http.StatusBadRequest)_, _ = w.Write([]byte("only support POST method.\n"))return}bs, err := gitPull()if err != nil {w.WriteHeader(http.StatusInternalServerError)_, _ = w.Write([]byte(err.Error()))return}w.WriteHeader(http.StatusOK)_, _ = w.Write(bs)}// 包装一下 404 状态码,返回自定义的 404 页面func fileServerWithCustom404(fs http.FileSystem) http.Handler {fsh := http.FileServer(fs)return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {_, err := fs.Open(path.Clean(r.URL.Path))if os.IsNotExist(err) {r.URL.Path = "/404.html"}fsh.ServeHTTP(w, r)})}func gitPull() (msg []byte, err error) {cmd := exec.Command("git", "pull")cmd.Dir = "/data"return cmd.CombinedOutput()}

五、Docker 化

有了上面的静态服务器,写个 Dockerfile 将 Hexo 生成的静态文件打包即可:

FROM golang:1.15-alpine3.12 AS builderENV GO111MODULE onCOPY goserver /go/src/github.com/mritd/hexo/goserverWORKDIR /go/src/github.com/mritd/hexo/goserverRUN set -e \    && go installFROM alpine:3.12 AS distLABEL maintainer="mritd <mritd@linux.com>"ENV TZ Asia/ShanghaiENV REPO http://github.com/mritd/mritd.com.gitCOPY --from=builder /go/bin/goserver /usr/local/bin/goserverRUN set -e \    && apk upgrade \    && apk add bash tzdata git \    && git clone ${REPO} /data \    && ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime \    && echo ${TZ} > /etc/timezone \    && rm -rf /var/cache/apk/*WORKDIR /dataCMD ["goserver"]

镜像运行后将使用 /data 目录最为静态文件目录进行发布,Hexo 生成的静态文件(public 目录)也会完整的 clone 到当前目录,此后使用 POST 请求访问 /update 即可触发从 Github 更新博客内容。

六、Travis CI 集成

所有就绪以后在主仓库增加 .travis.yml 配置来联动 travis ci;由于每次 push 到 Github 的内容实际上已经是本地生成的 public 目录,所以 CI 只需要通知服务器更新即可;强迫症又加了一个 Telegram 通知,每次触发更新完成后 Telegram 再给自己推送一下:

language: gogit:  quiet: truescript:- curl -X POST ${CALLBACK}after_script:- curl -X POST http://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage -d chat_id=${TELEGRAM_CHAT_ID} -d "text=mritd.com deployed."

七、gulp 优化

由于目前一些配图啥的还是存储在服务器本地,所以图片等比较大的静态文件仍然是访问瓶颈,这时候可以借助 gulp 来压缩并进行优化:

# 安装 gulpnpm install -g gulp# 安装 gulp 插件npm install gulp-htmlclean gulp-htmlmin gulp-minify-css gulp-uglify-es gulp-imagemin --save# 重新 link 一下npm link gulp

接下来编写 gulpfile.js 指定相关的优化任务

var gulp = require('gulp');var minifycss = require('gulp-minify-css');var uglify = require('gulp-uglify-es').default;var htmlmin = require('gulp-htmlmin');var htmlclean = require('gulp-htmlclean');var imagemin = require('gulp-imagemin');// 压缩htmlgulp.task('minify-html', function() {    return gulp.src('./public/**/*.html')        .pipe(htmlclean())        .pipe(htmlmin({            removeComments: true,            minifyJS: true,            minifyCSS: true,            minifyURLs: true,        }))        .pipe(gulp.dest('./public'))});// 压缩cssgulp.task('minify-css', function() {    return gulp.src('./public/css/*.css')        .pipe(minifycss({            compatibility: '*'        }))        .pipe(gulp.dest('./public/css'));});// 压缩jsgulp.task('minify-js', function() {    return gulp.src('./public/js/*.js', '!./public/js/*.min.js')        .pipe(uglify())        .pipe(gulp.dest('./public/js'));});// 压缩图片gulp.task('minify-images', function() {    return gulp.src('./public/img/*.*')        .pipe(imagemin(        [imagemin.gifsicle({'optimizationLevel': 3}),        imagemin.mozjpeg({'progressive': true}),        imagemin.optipng({'optimizationLevel': 7}),        imagemin.svgo()],        {'verbose': true}))        .pipe(gulp.dest('./public/img'))});// 默认任务// 这里默认没有运行 minify-js,因为我发现 js 压缩以后 PageSpeed 评分// 莫明其妙的降低了,目前只优先考虑桌面浏览器的性能,暂不考虑移动端gulp.task('default', gulp.parallel(    'minify-html','minify-css','minify-images'));

最后在每次部署时执行一下 gulp 命令即可完成优化: hexo cl && hexo g && gulp

]]>
Linux Hexo http://mritd.com/2020/10/08/switch-jekyll-to-hexo/#disqus_thread
编写一个动态准入控制来实现自动化 http://mritd.com/2020/08/19/write-a-dynamic-admission-control-webhook/ http://mritd.com/2020/08/19/write-a-dynamic-admission-control-webhook/ Wed, 19 Aug 2020 06:35:00 GMT 前段时间弄了一个 imgsync 的工具把 gcr.io 的镜像搬运到了 Docker Hub,但是即使这样我每次还是需要编辑 yaml 配置手动改镜像名称;所以我萌生了一个想法: 能不能自动化这个过程? 一、准入控制介绍

在 Kubernetes 整个请求链路中,请求通过认证和授权之后、对象被持久化之前需要通过一连串的 “准入控制拦截器”;这些准入控制器负载验证请求的合法性,必要情况下也可以对请求进行修改;默认准入控制器编写在 kube-apiserver 的代码中,针对于当前 kube-apiserver 默认启用的准入控制器你可以通过以下命令查看:

kube-apiserver -h | grep enable-admission-plugins

具体每个准入控制器的作用可以通过 Using Admission Controllers 文档查看。在这些准入控制器中有两个特殊的准入控制器 MutatingAdmissionWebhookValidatingAdmissionWebhook这两个准入控制器以 WebHook 的方式提供扩展能力,从而我们可以实现自定义的一些功能。当我们在集群中创建相关 WebHook 配置后,我们配置中描述的想要关注的资源在集群中创建、修改等都会触发 WebHook,我们再编写具体的应用来响应 WebHook 即可完成特定功能。

二、动态准入控制

动态准入控制实际上指的就是上面所说的两个 WebHook,在使用动态准入控制时需要一些先决条件:

  • 确保 Kubernetes 集群版本至少为 v1.16 (以便使用 admissionregistration.k8s.io/v1 API)或者 v1.9 (以便使用 admissionregistration.k8s.io/v1beta1 API)。
  • 确保启用 MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook 控制器。
  • 确保启用 admissionregistration.k8s.io/v1admissionregistration.k8s.io/v1beta1 API。

如果要使用 Mutating Admission Webhook,在满足先决条件后,需要在系统中 create 一个 MutatingWebhookConfiguration:

apiVersion: admissionregistration.k8s.io/v1kind: MutatingWebhookConfigurationmetadata:  name: "mutating-webhook.pureyukon.com"  namespace: kube-addonswebhooks:  - name: "mutating-webhook.pureyukon.com"    rules:      - apiGroups:   [""]        apiVersions: ["v1"]        operations:  ["CREATE","UPDATE"]        resources:   ["pods"]        scope:       "Namespaced"    clientConfig:      service:        name: "mutating-webhook"        namespace: "kube-addons"        path: /print      caBundle: ${CA_BUNDLE}    admissionReviewVersions: ["v1", "v1beta1"]    sideEffects: None    timeoutSeconds: 5    failurePolicy: Ignore    namespaceSelector:      matchLabels:        mutating-webhook.pureyukon.com: "true"

同样要使用 Validating Admission Webhook 也需要类似的配置:

apiVersion: admissionregistration.k8s.io/v1kind: ValidatingWebhookConfigurationmetadata:  name: "validating-webhook.pureyukon.com"webhooks:  - name: "validating-webhook.pureyukon.com"    rules:      - apiGroups:   [""]        apiVersions: ["v1"]        operations:  ["CREATE","UPDATE"]        resources:   ["pods"]        scope:       "Namespaced"    clientConfig:      service:        name: "validating-webhook"        namespace: "kube-addons"        path: /print      caBundle: ${CA_BUNDLE}    admissionReviewVersions: ["v1", "v1beta1"]    sideEffects: None    timeoutSeconds: 5    failurePolicy: Ignore    namespaceSelector:      matchLabels:        validating-webhook.pureyukon.com: "true"

从配置文件中可以看到,webhooks.rules 段落中具体指定了我们想要关注的资源及其行为,webhooks.clientConfig 中指定了 webhook 触发后将其发送到那个地址以及证书配置等,这些具体字段的含义可以通过官方文档 Dynamic Admission Control 来查看。

值得注意的是 Mutating Admission Webhook 会在 Validating Admission Webhook 之前触发;Mutating Admission Webhook 可以修改用户的请求,比如自动调整镜像名称、增加注解等,而 Validating Admission Webhook 只能做校验(true or false),不可以进行修改操作。

三、编写一个 WebHook

郑重提示: 本部分文章请结合 goadmission 框架源码进行阅读。

3.1、大体思路

在编写之前一般我们先大体了解一下流程并制订方案再去实现,边写边思考适合在细节实现上,对于整体的把控需要提前作好预习。针对于这个准入控制的 WebHook 来说,根据其官方文档大致总结重点如下:

  • WebHook 接收者就是一个标准的 HTTP Server,请求方式是 POST + JSON
  • 请求响应都是一个 AdmissionReview 对象
  • 响应时需要请求时的 UID(request.uid)
  • 响应时 Mutating Admission Webhook 可以包含对请求的修改信息,格式为 JSONPatch

有了以上信息以后便可以知道编写 WebHook 需要的东西,根据这些信息目前我作出的大体方案如下:

  • 最起码我们要有个 HTTP Server,考虑到后续可能会同时处理多种 WebHook,所以需要一个带有路径匹配的 HTTP 框架,Gin 什么的虽然不错但是太重,最终选择简单轻量的 gorilla/mux
  • 应该做好适当的抽象,因为对于响应需要包含的 UID 等限制在每个请求都有可以提取出来自动化完成。
  • 针对于 Mutating Admission Webhook 响应的 JSONPatch 可以弄个结构体然后直接反序列化。

3.2、AdmissionReview 对象

基于 3.1 部分的分析可以知道,WebHook 接收和响应都是一个 AdmissionReview 对象,在查看源码以后可以看到 AdmissionReview 结构如下:

AdmissionReview

从代码的命名中可以很清晰的看出,在请求发送到 WebHook 时我们只需要关注内部的 AdmissionRequest(实际入参),在我们编写的 WebHook 处理完成后只需要返回包含有 AdmissionResponse(实际返回体) 的 AdmissionReview 对象即可;总的来说 AdmissionReview 对象是个套壳,请求是里面的 AdmissionRequest,响应是里面的 AdmissionResponse

3.3、Hello World

有了上面的一些基础知识,我们就可以简单的实行一个什么也不干的 WebHook 方法(本地无法直接运行,重点在于思路):

// printRequest 接收 AdmissionRequest 对象并将其打印到到控制台,接着不做任何处理直接返回一个 AdmissionResponse 对象func printRequest(request *admissionv1.AdmissionRequest) (*admissionv1.AdmissionResponse, error) {bs, err := jsoniter.MarshalIndent(request, "", "    ")if err != nil {return nil, err}logger.Infof("print request: %s", string(bs))return &admissionv1.AdmissionResponse{Allowed: true,Result: &metav1.Status{Code:    http.StatusOK,Message: "Hello World",},}, nil}

上面这个 printRequest 方法最细粒度的控制到只面向我们的实际请求和响应;而对于 WebHook Server 来说其接到的是 http 请求,所以我们还需要在外面包装一下,将 http 请求转换为 AdmissionReview 并提取 AdmissionRequest 再调用上面的 printRequest 来处理,最后将返回结果重新包装为 AdmissionReview 重新返回;整体的代码如下

// 通用的错误返回方法func responseErr(handlePath, msg string, httpCode int, w http.ResponseWriter) {logger.Errorf("handle func [%s] response err: %s", handlePath, msg)review := &admissionv1.AdmissionReview{Response: &admissionv1.AdmissionResponse{Allowed: false,Result: &metav1.Status{Message: msg,},},}bs, err := jsoniter.Marshal(review)if err != nil {logger.Errorf("failed to marshal response: %v", err)w.WriteHeader(http.StatusInternalServerError)_, _ = w.Write([]byte(fmt.Sprintf("failed to marshal response: %s", err)))}w.WriteHeader(httpCode)_, err = w.Write(bs)logger.Debugf("write err response: %d: %v: %v", httpCode, review, err)}// printRequest 接收 AdmissionRequest 对象并将其打印到到控制台,接着不做任何处理直接返回一个 AdmissionResponse 对象func printRequest(request *admissionv1.AdmissionRequest) (*admissionv1.AdmissionResponse, error) {bs, err := jsoniter.MarshalIndent(request, "", "    ")if err != nil {return nil, err}logger.Infof("print request: %s", string(bs))return &admissionv1.AdmissionResponse{Allowed: true,Result: &metav1.Status{Code:    http.StatusOK,Message: "Hello World",},}, nil}// http server 的处理方法func headler(w http.ResponseWriter, r *http.Request) {defer func() { _ = r.Body.Close() }()w.Header().Set("Content-Type", "application/json")// 读取 body,出错直接返回reqBs, err := ioutil.ReadAll(r.Body)if err != nil {responseErr(handlePath, err.Error(), http.StatusInternalServerError, w)return}if reqBs == nil || len(reqBs) == 0 {responseErr(handlePath, "request body is empty", http.StatusBadRequest, w)return}logger.Debugf("request body: %s", string(reqBs))// 将 body 反序列化为 AdmissionReviewreqReview := admissionv1.AdmissionReview{}if _, _, err := deserializer.Decode(reqBs, nil, &reqReview); err != nil {responseErr(handlePath, fmt.Sprintf("failed to decode req: %s", err), http.StatusInternalServerError, w)return}if reqReview.Request == nil {responseErr(handlePath, "admission review request is empty", http.StatusBadRequest, w)return}// 提取 AdmissionRequest 并调用 printRequest 处理resp, err := printRequest(reqReview.Request)if err != nil {responseErr(handlePath, fmt.Sprintf("admission func response: %s", err), http.StatusForbidden, w)return}if resp == nil {responseErr(handlePath, "admission func response is empty", http.StatusInternalServerError, w)return}// 复制 AdmissionRequest 中的 UID 到 AdmissionResponse 中(必须进行,否则会导致响应无效)resp.UID = reqReview.Request.UID// 复制 reqReview.TypeMeta 到新的响应 AdmissionReview 中respReview := admissionv1.AdmissionReview{TypeMeta: reqReview.TypeMeta,Response: resp,}// 重新序列化响应并返回respBs, err := jsoniter.Marshal(respReview)if err != nil {responseErr(handlePath, fmt.Sprintf("failed to marshal response: %s", err), http.StatusInternalServerError, w)logger.Errorf("the expected response is: %v", respReview)return}w.WriteHeader(http.StatusOK)_, err = w.Write(respBs)logger.Debugf("write response: %d: %s: %v", http.StatusOK, string(respBs), err)}

3.4、抽象出框架

编写了简单的 Hello World 以后可以看出,真正在编写时我们需要实现的都是处理 AdmissionRequest 并返回 AdmissionResponse 这部份(printRequest);外部的包装为 AdmissionReview、复制 UID、复制 TypeMeta 等都是通用的方法,所以基于这一点我们可以进行适当的抽象:

3.4.1、AdmissionFunc

针对每一个贴合业务的 WebHook 来说,其大致有三大属性:

  • WebHook 的类型(Mutating/Validating)
  • WebHook 拦截的 URL 路径(/print_request)
  • WebHook 核心的处理逻辑(处理 Request 和返回 Response)

我们将其抽象为 AdmissionFunc 结构体以后如下所示

// WebHook 类型const (Mutating   AdmissionFuncType = "Mutating"Validating AdmissionFuncType = "Validating")// 每一个对应到我们业务的 WebHook 抽象的 structtype AdmissionFunc struct {Type AdmissionFuncTypePath stringFunc func(request *admissionv1.AdmissionRequest) (*admissionv1.AdmissionResponse, error)}

3.4.2、HandleFunc

我们知道 WebHook 是基于 HTTP 的,所以上面抽象出的 AdmissionFunc 还不能直接用在 HTTP 请求代码中;如果直接偶合到 HTTP 请求代码中,我们就没法为 HTTP 代码再增加其他拦截路径等等特殊的底层设置;所以站在 HTTP 层面来说还需要抽象一个 “更高层面的且包含 AdmissionFunc 全部能力的 HandleFunc” 来使用;HandleFunc 抽象 HTTP 层面的需求:

  • HTTP 请求方法
  • HTTP 请求路径
  • HTTP 处理方法

以下为 HandleFunc 的抽象:

type HandleFunc struct {Path   stringMethod stringFunc   func(w http.ResponseWriter, r *http.Request)}

3.5、goadmission 框架

有了以上两个角度的抽象,再结合 命令行参数解析、日志处理、配置文件读取等等,我揉合出了一个 goadmission 框架,以方便动态准入控制的快速开发。

3.5.1、基本结构

.├── main.go└── pkg    ├── adfunc    │   ├── adfuncs.go    │   ├── adfuncs_json.go    │   ├── func_check_deploy_time.go    │   ├── func_disable_service_links.go    │   ├── func_image_rename.go    │   └── func_print_request.go    ├── conf    │   └── conf.go    ├── route    │   ├── route_available.go    │   ├── route_health.go    │   └── router.go    └── zaplogger        ├── config.go        └── logger.go5 directories, 13 files
  • main.go 为程序运行入口,在此设置命令行 flag 参数等
  • pkg/conf 为框架配置包,所有的配置读取只读取这个包即可
  • pkg/zaplogger zap log 库的日志抽象和处理(copy 自 operator-sdk)
  • pkg/route http 级别的路由抽象(HandleFunc)
  • pkg/adfunc 动态准入控制 WebHook 级别的抽(AdmissionFunc)

3.5.2、增加动态准入控制

由于框架已经作好了路由注册等相关抽象,所以只需要新建 go 文件,然后通过 init 方法注册到全局 WebHook 组中即可,新编写的 WebHook 对已有代码不会有任何侵入:

add_adfunc

需要注意的是所有 validating 类型的 WebHook 会在 URL 路径前自动拼接 /validating 路径,mutating 类型的 WebHook 会在 URL 路径前自动拼接 /mutating 路径;这么做是为了避免在更高层级的 HTTP Route 上添加冲突的路由。

auto_fix_url

3.5.3、实现 image 自动修改

所以一切准备就绪以后,就需要 “不忘初心”,撸一个自动修改镜像名称的 WebHook:

package adfuncimport ("fmt""net/http""strings""sync""time""github.com/mritd/goadmission/pkg/conf"jsoniter "github.com/json-iterator/go"corev1 "k8s.io/api/core/v1"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1""github.com/mritd/goadmission/pkg/route"admissionv1 "k8s.io/api/admission/v1")// 只初始化一次 renameMapvar renameOnce sync.Once// renameMap 保存镜像名称的替换规则,目前粗略实现为纯文本替换var renameMap map[string]stringfunc init() {route.Register(route.AdmissionFunc{Type: route.Mutating,Path: "/rename",Func: func(request *admissionv1.AdmissionRequest) (*admissionv1.AdmissionResponse, error) {// init rename rules maprenameOnce.Do(func() {renameMap = make(map[string]string, 10)// 将镜像重命名规则初始化到 renameMap 中,方便后续读取// rename rule example: k8s.gcr.io/=gcrxio/k8s.gcr.io_for _, s := range conf.ImageRename {ss := strings.Split(s, "=")if len(ss) != 2 {logger.Fatalf("failed to parse image name rename rules: %s", s)}renameMap[ss[0]] = ss[1]}})// 这个准入控制的 WebHook 只针对 Pod 处理,非 Pod 类请求直接返回错误switch request.Kind.Kind {case "Pod":// 从 request 中反序列化出 Pod 实例var pod corev1.Poderr := jsoniter.Unmarshal(request.Object.Raw, &pod)if err != nil {errMsg := fmt.Sprintf("[route.Mutating] /rename: failed to unmarshal object: %v", err)logger.Error(errMsg)return &admissionv1.AdmissionResponse{Allowed: false,Result: &metav1.Status{Code:    http.StatusBadRequest,Message: errMsg,},}, nil}// 后来我发现带有下面这个注解的 Pod 是没法更改成功的,这种 Pod 是由 kubelet 直接// 启动的 static pod,在 api server 中只能看到它的 "mirror",不能改的// skip static podfor k := range pod.Annotations {if k == "kubernetes.io/config.mirror" {errMsg := fmt.Sprintf("[route.Mutating] /rename: pod %s has kubernetes.io/config.mirror annotation, skip image rename", pod.Name)logger.Warn(errMsg)return &admissionv1.AdmissionResponse{Allowed: true,Result: &metav1.Status{Code:    http.StatusOK,Message: errMsg,},}, nil}}// 遍历所有 Pod,然后生成 JSONPatch// 注意: 返回结果必须是 JSONPatch,k8s api server 再将 JSONPatch 应用到 Pod 上 // 由于有多个 Pod,所以最终会产生一个补丁数组var patches []Patchfor i, c := range pod.Spec.Containers {for s, t := range renameMap {if strings.HasPrefix(c.Image, s) {patches = append(patches, Patch{// 指定 JSONPatch 动作为 replace Option: PatchOptionReplace,// 打补丁的绝对位置Path:   fmt.Sprintf("/spec/containers/%d/image", i),// replace 为处理过的镜像名Value:  strings.Replace(c.Image, s, t, 1),})// 为了后期调试和留存历史,我们再为修改过的 Pod 加个注解patches = append(patches, Patch{Option: PatchOptionAdd,Path:   "/metadata/annotations",Value: map[string]string{fmt.Sprintf("rename-mutatingwebhook-%d.pureyukon.com", time.Now().Unix()): fmt.Sprintf("%d-%s-%s", i, strings.ReplaceAll(s, "/", "_"), strings.ReplaceAll(t, "/", "_")),},})break}}}// 将所有 JSONPatch 序列化成 json,然后返回即可patch, err := jsoniter.Marshal(patches)if err != nil {errMsg := fmt.Sprintf("[route.Mutating] /rename: failed to marshal patch: %v", err)logger.Error(errMsg)return &admissionv1.AdmissionResponse{Allowed: false,Result: &metav1.Status{Code:    http.StatusInternalServerError,Message: errMsg,},}, nil}logger.Infof("[route.Mutating] /rename: patches: %s", string(patch))return &admissionv1.AdmissionResponse{Allowed:   true,Patch:     patch,PatchType: JSONPatch(),Result: &metav1.Status{Code:    http.StatusOK,Message: "success",},}, nildefault:errMsg := fmt.Sprintf("[route.Mutating] /rename: received wrong kind request: %s, Only support Kind: Pod", request.Kind.Kind)logger.Error(errMsg)return &admissionv1.AdmissionResponse{Allowed: false,Result: &metav1.Status{Code:    http.StatusForbidden,Message: errMsg,},}, nil}},})}

四、总结

  • 动态准入控制其实就是个 WebHook,我们弄个 HTTP Server 接收 AdmissionRequest 响应 AdmissionResponse 就行。
  • Request、Response 会包装到 AdmissionReview 中,我们还需要做一些边缘处理,比如复制 UID、TypeMeta 等
  • MutatingWebHook 想要修改东西时,要返回描述修改操作的 JSONPatch 补丁
  • 单个 WebHook 很简单,写多个的时候要自己抽好框架,尽量优雅的作好复用和封装
]]>
Kubernetes Admission http://mritd.com/2020/08/19/write-a-dynamic-admission-control-webhook/#disqus_thread
使用 etcdadm 三分钟搭建 etcd 集群 http://mritd.com/2020/08/19/use-etcdadm-to-build-etcd-cluster-in-3-minutes/ http://mritd.com/2020/08/19/use-etcdadm-to-build-etcd-cluster-in-3-minutes/ Wed, 19 Aug 2020 06:20:00 GMT 本文介绍一下 etcd 宿主机部署的新玩具 etcdadm,类似 kubeadm 一样可以快速的在宿主机搭建 Etcd 集群。 一、介绍

在搭建 Kubernetes 集群的过程中首先要搞定 Etcd 集群,虽然说 kubeadm 工具已经提供了默认和 master 节点绑定的 Etcd 集群自动搭建方式,但是我个人一直是手动将 Etcd 集群搭建在宿主机;因为这个玩意太重要了,毫不夸张的说 kubernetes 所有组件崩溃我们都能在一定时间以后排查问题恢复,但是一旦 Etcd 集群没了那么 Kubernetes 集群也就真没了。

在很久以前我创建了 edep 工具来实现 Etcd 集群的辅助部署,再后来由于我们的底层系统偶合了 Ubuntu,所以创建了 etcd-deb 项目来自动打 deb 包来直接安装;最近逛了一下 Kubernetes 的相关项目,发现跟我的 edep 差不多的项目 etcdadm,试了一下 “真香”。

二、安装

etcdadm 项目是使用 go 编写的,所以很明显只有一个二进制下载下来就能用:

wget http://github.com/kubernetes-sigs/etcdadm/releases/download/v0.1.3/etcdadm-linux-amd64chmod +x etcdadm-linux-amd64

三、使用

3.1、启动引导节点

类似 kubeadm 一样,etcdadm 也是先启动第一个节点,然后后续节点直接 join 即可;第一个节点启动只需要执行 etcdadm init 命令即可:

k1.node ➜  ~ ./etcdadm-linux-amd64 initINFO[0000] [install] extracting etcd archive /var/cache/etcdadm/etcd/v3.3.8/etcd-v3.3.8-linux-amd64.tar.gz to /tmp/etcd664686683INFO[0001] [install] verifying etcd 3.3.8 is installed in /opt/bin/INFO[0001] [certificates] creating PKI assetsINFO[0001] creating a self signed etcd CA certificate and key files[certificates] Generated ca certificate and key.INFO[0001] creating a new server certificate and key files for etcd[certificates] Generated server certificate and key.[certificates] server serving cert is signed for DNS names [k1.node] and IPs [127.0.0.1 172.16.10.21]INFO[0002] creating a new certificate and key files for etcd peering[certificates] Generated peer certificate and key.[certificates] peer serving cert is signed for DNS names [k1.node] and IPs [172.16.10.21]INFO[0002] creating a new client certificate for the etcdctl[certificates] Generated etcdctl-etcd-client certificate and key.INFO[0002] creating a new client certificate for the apiserver calling etcd[certificates] Generated apiserver-etcd-client certificate and key.[certificates] valid certificates and keys now exist in "/etc/etcd/pki"INFO[0006] [health] Checking local etcd endpoint healthINFO[0006] [health] Local etcd endpoint is healthyINFO[0006] To add another member to the cluster, copy the CA cert/key to its certificate dir and run:INFO[0006]      etcdadm join http://172.16.10.21:2379

从命令行输出可以看到不同阶段 etcdadm 的相关日志输出;在 init 命令时可以指定一些特定参数来覆盖默认行为,比如版本号、安装目录等:

k1.node ➜  ~ ./etcdadm-linux-amd64 init --helpInitialize a new etcd clusterUsage:  etcdadm init [flags]Flags:      --certs-dir string                    certificates directory (default "/etc/etcd/pki")      --disk-priorities stringArray         Setting etcd disk priority (default [Nice=-10,IOSchedulingClass=best-effort,IOSchedulingPriority=2])      --download-connect-timeout duration   Maximum time in seconds that you allow the connection to the server to take. (default 10s)  -h, --help                                help for init      --install-dir string                  install directory (default "/opt/bin/")      --name string                         etcd member name      --release-url string                  URL used to download etcd (default "http://github.com/coreos/etcd/releases/download")      --server-cert-extra-sans strings      optional extra Subject Alternative Names for the etcd server signing cert, can be multiple comma separated DNS names or IPs      --skip-hash-check                     Ignore snapshot integrity hash value (required if copied from data directory)      --snapshot string                     Etcd v3 snapshot file used to initialize member      --version string                      etcd version (default "3.3.8")Global Flags:  -l, --log-level string   set log level for output, permitted values debug, info, warn, error, fatal and panic (default "info")

3.2、其他节点加入

在首个节点启动完成后,将集群 ca 证书复制到其他节点然后执行 etcdadm join ENDPOINT_ADDRESS 即可:

# 复制 ca 证书k1.node ➜  ~ rsync -avR /etc/etcd/pki/ca.* 172.16.10.22:/root@172.16.10.22's password:sending incremental file list/etc/etcd//etc/etcd/pki//etc/etcd/pki/ca.crt/etc/etcd/pki/ca.keysent 2,932 bytes  received 67 bytes  856.86 bytes/sectotal size is 2,684  speedup is 0.89# 执行 joink2.node ➜  ~ ./etcdadm-linux-amd64 join http://172.16.10.21:2379INFO[0000] [certificates] creating PKI assetsINFO[0000] creating a self signed etcd CA certificate and key files[certificates] Using the existing ca certificate and key.INFO[0000] creating a new server certificate and key files for etcd[certificates] Generated server certificate and key.[certificates] server serving cert is signed for DNS names [k2.node] and IPs [172.16.10.22 127.0.0.1]INFO[0000] creating a new certificate and key files for etcd peering[certificates] Generated peer certificate and key.[certificates] peer serving cert is signed for DNS names [k2.node] and IPs [172.16.10.22]INFO[0000] creating a new client certificate for the etcdctl[certificates] Generated etcdctl-etcd-client certificate and key.INFO[0001] creating a new client certificate for the apiserver calling etcd[certificates] Generated apiserver-etcd-client certificate and key.[certificates] valid certificates and keys now exist in "/etc/etcd/pki"INFO[0001] [membership] Checking if this member was addedINFO[0001] [membership] Member was not addedINFO[0001] Removing existing data dir "/var/lib/etcd"INFO[0001] [membership] Adding memberINFO[0001] [membership] Checking if member was startedINFO[0001] [membership] Member was not startedINFO[0001] [membership] Removing existing data dir "/var/lib/etcd"INFO[0001] [install] extracting etcd archive /var/cache/etcdadm/etcd/v3.3.8/etcd-v3.3.8-linux-amd64.tar.gz to /tmp/etcd315786364INFO[0003] [install] verifying etcd 3.3.8 is installed in /opt/bin/INFO[0006] [health] Checking local etcd endpoint healthINFO[0006] [health] Local etcd endpoint is healthy

四、细节分析

4.1、默认配置

在目前 etcdadm 尚未支持配置文件,目前所有默认配置存放在 constants.go 中,这里面包含了默认安装位置、systemd 配置、环境变量配置等,限于篇幅请自行查看代码;下面简单介绍一些一些刚须的配置:

4.1.1、etcdctl

etcdctl 默认安装在 /opt/bin 目录下,同时你会发现该目录下还存在一个 etcdctl.sh 脚本,这个脚本将会自动读取 etcdctl 配置文件(/etc/etcd/etcdctl.env),所以推荐使用这个脚本来替代 etcdctl 命令。

4.1.2、数据目录

默认的数据目录存储在 /var/lib/etcd 目录,目前 etcdadm 尚未提供任何可配置方式,当然你可以自己改源码。

4.2.3、配置文件

配置文件总共有两个,一个是 /etc/etcd/etcdctl.env 用于 /opt/bin/etcdctl.sh 读取;另一个是 /etc/etcd/etcd.env 用于 systemd 读取并启动 etcd server。

4.2、Join 流程

其实很久以前由于我自己部署方式导致了我一直以来理解的一个错误,我一直以为 etcd server 证书要包含所有 server 地址,当然这个想法是怎么来的我也不知道,但是当我看了以下 Join 操作源码以后突然意识到 “为什么要包含所有?包含当前 server 不就行了么。”;当然对于 http 证书的理解一直是明白的,但是很奇怪就是不知道怎么就产生了这个想法(哈哈,我自己都觉的不可思议)…

  • 由于预先拷贝了 ca 证书,所以 join 开始前 etcdadm 使用这个 ca 证书会签发自己需要的所有证书。
  • 接下来 etcdadmin 通过 etcdctl-etcd-client 证书创建 client,然后调用 MemberAdd 添加新集群
  • 最后老套路下载安装+启动就完成了

4.3、目前不足

目前 etcdadm 虽然已经基本生产可用,但是仍有些不足的地方:

  • 不支持配置文件,很多东西无法定制
  • join 加入集群是在内部 api 完成,并未持久化到物理配置文件,后续重建可能忘记节点 ip
  • 集群证书目前不支持自动续期,默认证书为 1 年很容易过期
  • 下载动作调用了系统命令(curl)依赖性有点强
  • 日志格式有点不友好,比如 level 和日期
]]>
Kubernetes etcd http://mritd.com/2020/08/19/use-etcdadm-to-build-etcd-cluster-in-3-minutes/#disqus_thread
如何编写 CSI 插件 http://mritd.com/2020/08/19/how-to-write-a-csi-driver-for-kubernetes/ http://mritd.com/2020/08/19/how-to-write-a-csi-driver-for-kubernetes/ Wed, 19 Aug 2020 06:10:00 GMT 本篇文章详细介绍 CSI 插件,同时涉及到的源码比较多,主要倾向于使用 go 来开发 CSI 驱动。 一、为什么需要 CSI

在 Kubernetes 以前的版本中,其所有受官方支持的存储驱动全部在 Kubernetes 的主干代码中,其他第三方开发的自定义插件通过 FlexVolume 插件的形势提供服务;相对于 kubernetes 的源码树来说,内置的存储我们称之为 “树内存储”,外部第三方实现我们称之为 “树外存储”;在很长一段时间里树内存储和树外存储并行开发和使用,但是随着时间推移渐渐的就出现了很严重的问题:

  • 想要添加官方支持的存储必须在树内修改,这意味着需要 Kubernetes 发版
  • 如果树内存储出现问题则也必须等待 Kubernetes 发版才能修复

为了解决这种尴尬的问题,Kubernetes 必须抽象出一个合适的存储接口,并将所有存储驱动全部适配到这个接口上,存储驱动最好与 Kubernetes 之间进行 RPC 调用完成解耦,这样就造就了 CSI(Container Storage Interface)。

二、CSI 基础知识

2.1、CSI Sidecar Containers

在开发 CSI 之前我们最好熟悉一下 CSI 开发中的一些常识;了解过 Kubernetes API 开发的朋友应该清楚,所有的资源定义(Deployment、Service…)在 Kubernetes 中其实就是一个 Object,此时可以将 Kubernetes 看作是一个 Database,无论是 Operator 还是 CSI 其核心本质都是不停的 Watch 特定的 Object,一但 kubectl 或者其他客户端 “动了” 这个 Object,我们的对应实现程序就 Watch 到变更然后作出相应的响应;对于 CSI 编写者来说,这些 Watch 动作已经不必自己实现 Custom Controller,官方为我们提供了 CSI Sidecar Containers并且在新版本中这些 Sidecar Containers 实现极其完善,比如自动的多节点 HA(Etcd 选举)等。

所以到迄今为止,所谓的 CSI 插件开发事实上并非面向 Kubernetes API 开发,而是面向 Sidecar Containers 的 gRPC 开发,Sidecar Containers 一般会和我们自己开发的 CSI 驱动程序在同一个 Pod 中启动,然后 Sidecar Containers Watch API 中 CSI 相关 Object 的变动,接着通过本地 unix 套接字调用我们编写的 CSI 驱动:

CSI_Sidecar_Containers

目前官方提供的 Sidecar Containers 如下:

每个 Sidecar Container 的作用可以通过对应链接查看,需要注意的是 cluster-driver-registrar 已经停止维护,请改用 node-driver-registrar。

2.2、CSI 处理阶段

在理解了 CSI Sidecar Containers 以后,我们仍需要大致的了解 CSI 挂载过程中的大致流程,以此来针对性的实现每个阶段所需要的功能;CSI 整个流程实际上大致分为以下三大阶段:

2.2.1、Provisioning and Deleting

Provisioning and Deleting 阶段实现与外部存储供应商协调卷的创建/删除处理,简单地说就是需要实现 CreateVolume 和 DeleteVolume;假设外部存储供应商为阿里云存储那么此阶段应该完成在阿里云存储商创建一个指定大小的块设备,或者在用户删除 volume 时完成在阿里云存储上删除这个块设备;除此之外此阶段还应当响应存储拓扑分布从而保证 volume 分布在正确的集群拓扑上(此处描述不算清晰,推荐查看设计文档)。

2.2.2、Attaching and Detaching

Attaching and Detaching 阶段实现将外部存储供应商提供好的卷设备挂载到本地或者从本地卸载,简单地说就是实现 ControllerPublishVolume 和 ControllerUnpublishVolume;同样以外部存储供应商为阿里云存储为例,在 Provisioning 阶段创建好的卷的块设备,在此阶段应该实现将其挂载到服务器本地或从本地卸载,在必要的情况下还需要进行格式化等操作。

2.2.3、Mount and Umount

这个阶段在 CSI 设计文档中没有做详细描述,在前两个阶段完成后,当一个目标 Pod 在某个 Node 节点上调度时,kubelet 会根据前两个阶段返回的结果来创建这个 Pod;同样以外部存储供应商为阿里云存储为例,此阶段将会把已经 Attaching 的本地块设备以目录形式挂载到 Pod 中或者从 Pod 中卸载这个块设备。

2.3、CSI gRPC Server

CSI 的三大阶段实际上更细粒度的划分到 CSI Sidecar Containers 中,上面已经说过我们开发 CSI 实际上是面向 CSI Sidecar Containers 编程,针对于 CSI Sidecar Containers 我们主要需要实现以下三个 gRPC Server:

2.3.1、Identity Server

在当前 CSI Spec v1.3.0 中 IdentityServer 定义如下:

// IdentityServer is the server API for Identity service.type IdentityServer interface {GetPluginInfo(context.Context, *GetPluginInfoRequest) (*GetPluginInfoResponse, error)GetPluginCapabilities(context.Context, *GetPluginCapabilitiesRequest) (*GetPluginCapabilitiesResponse, error)Probe(context.Context, *ProbeRequest) (*ProbeResponse, error)}

从代码上可以看出 IdentityServer 主要负责像 Kubernetes 提供 CSI 插件名称可选功能等,所以此 Server 是必须实现的。

2.3.2、Node Server

同样当前 CSI v1.3.0 Spec 中 NodeServer 定义如下:

// NodeServer is the server API for Node service.type NodeServer interface {NodeStageVolume(context.Context, *NodeStageVolumeRequest) (*NodeStageVolumeResponse, error)NodeUnstageVolume(context.Context, *NodeUnstageVolumeRequest) (*NodeUnstageVolumeResponse, error)NodePublishVolume(context.Context, *NodePublishVolumeRequest) (*NodePublishVolumeResponse, error)NodeUnpublishVolume(context.Context, *NodeUnpublishVolumeRequest) (*NodeUnpublishVolumeResponse, error)NodeGetVolumeStats(context.Context, *NodeGetVolumeStatsRequest) (*NodeGetVolumeStatsResponse, error)NodeExpandVolume(context.Context, *NodeExpandVolumeRequest) (*NodeExpandVolumeResponse, error)NodeGetCapabilities(context.Context, *NodeGetCapabilitiesRequest) (*NodeGetCapabilitiesResponse, error)NodeGetInfo(context.Context, *NodeGetInfoRequest) (*NodeGetInfoResponse, error)}

在最小化的实现中,NodeServer 中仅仅需要实现 NodePublishVolumeNodeUnpublishVolumeNodeGetCapabilities 三个方法,在 Mount 阶段 kubelet 会通过 node-driver-registrar 容器调用这三个方法。

2.3.3、Controller Server

在当前 CSI Spec v1.3.0 ControllerServer 定义如下:

// ControllerServer is the server API for Controller service.type ControllerServer interface {CreateVolume(context.Context, *CreateVolumeRequest) (*CreateVolumeResponse, error)DeleteVolume(context.Context, *DeleteVolumeRequest) (*DeleteVolumeResponse, error)ControllerPublishVolume(context.Context, *ControllerPublishVolumeRequest) (*ControllerPublishVolumeResponse, error)ControllerUnpublishVolume(context.Context, *ControllerUnpublishVolumeRequest) (*ControllerUnpublishVolumeResponse, error)ValidateVolumeCapabilities(context.Context, *ValidateVolumeCapabilitiesRequest) (*ValidateVolumeCapabilitiesResponse, error)ListVolumes(context.Context, *ListVolumesRequest) (*ListVolumesResponse, error)GetCapacity(context.Context, *GetCapacityRequest) (*GetCapacityResponse, error)ControllerGetCapabilities(context.Context, *ControllerGetCapabilitiesRequest) (*ControllerGetCapabilitiesResponse, error)CreateSnapshot(context.Context, *CreateSnapshotRequest) (*CreateSnapshotResponse, error)DeleteSnapshot(context.Context, *DeleteSnapshotRequest) (*DeleteSnapshotResponse, error)ListSnapshots(context.Context, *ListSnapshotsRequest) (*ListSnapshotsResponse, error)ControllerExpandVolume(context.Context, *ControllerExpandVolumeRequest) (*ControllerExpandVolumeResponse, error)ControllerGetVolume(context.Context, *ControllerGetVolumeRequest) (*ControllerGetVolumeResponse, error)}

从这些方法上可以看出,大部分的核心逻辑应该在 ControllerServer 中实现,比如创建/销毁 Volume,创建/销毁 Snapshot 等;在一般情况下我们自己编写的 CSI 都会实现 CreateVolumeDeleteVolume,至于其他方法根据业务需求以及外部存储供应商实际情况来决定是否进行实现。

2.3.4、整体部署加构图

CSI Deploy Mechanism

从这个部署架构图上可以看出在实际上 CSI 部署时,Mount and Umount 阶段(对应 Node Server 实现)以 Daemonset 方式保证其部署到每个节点,当 Volume 创建完成后由其挂载到 Pod 中;其他阶段(Provisioning and Deleting 和 Attaching and Detaching) 只要部署多个实例保证 HA 即可(最新版本的 Sidecar Containers 已经实现了多节点自动选举);每次 PV 创建时首先由其他两个阶段的 Sidecar Containers 做处理,处理完成后信息返回给 Kubernetes 再传递到 Node Driver(Node Server) 上,然后 Node Driver 将其 Mount 到 Pod 中。

三、编写一个 NFS CSI 插件

3.1、前置准备及分析

根据以上文档的描述,针对于需要编写一个 NFS CSI 插件这个需求,大致我们可以作出如下分析:

  • 三大阶段中我们只需要实现 Provisioning and Deleting 和 Mount and Umount;因为以 NFS 作为外部存储供应商来说我们并非是块设备,所以也不需要挂载到宿主机(Attaching and Detaching)。
  • Provisioning and Deleting 阶段我们需要实现 CreateVolumeDeleteVolume 逻辑,其核心逻辑应该是针对每个 PV 在 NFS Server 目录下执行 mkdir,并将生成的目录名称等信息返回给 Kubernetes。
  • Mount and Umount 阶段需要实现 Node Server 的 NodePublishVolumeNodeUnpublishVolume 方法,然后将上一阶段提供的目录名称等信息组合成挂载命令 Mount 到 Pod 即可。

在明确了这个需求以后我们需要开始编写 gRPC Server,当然不能盲目的自己乱造轮子,因为这些 gRPC Server 需要是 NonBlocking 的,所以最佳实践就是参考官方给出的样例项目 csi-driver-host-path,这是一名合格的 CCE 必备的技能(CCE = Ctrl C + Ctrl V + Engineer)。

3.2、Hostpath CSI 源码分析

针对官方给出的 CSI 样例,首先把源码弄到本地,然后通过 IDE 打开;这里默认为读者熟悉 Go 语言相关语法以及 go mod 等依赖配置,开发 IDE 默认为 GoLand

source tree

从源码树上可以看到,hostpath 的 CSI 实现非常简单;首先是 cmd 包下的命令行部分,main 方法在这里定义,然后就是 pkg/hostpath 包的具体实现部分,CSI 需要实现的三大 gRPC Server 全部在此。

3.2.1、命令行解析

cmd 包下主要代码就是一些命令行解析,方便从外部传入一些参数供 CSI 使用;针对于 NFS CSI 我们需要从外部传入 NFS Server 地址、挂载目录等参数,如果外部存储供应商为其他云存储可能就需要从命令行传入 AccessKey、AccessToken 等参数。

flag_parse

目前 go 原生的命令行解析非常弱鸡,所以更推荐使用 cobra 命令行库完成解析。

3.2.2、Hostpath 结构体

从上面命令行解析的图中可以看到,在完成命令行解析后交由 handle 方法处理;handle 方法很简单,通过命令行拿到的参数创建一个 hostpath 结构体指针,然后 Run 起来就行了,所以接下来要着重看一下这个结构体

hostpath_struct

从代码上可以看到,hostpath 结构体内有一系列的字段用来存储命令行传入的特定参数,然后还有三个 gRPC Server 的引用;命令行参数解析完成后通过 NewHostPathDriver 方法设置到 hostpath 结构体内,然后通过调用结构体的 Run 方法创建三个 gRPC Server 并运行

hostpath_run

3.2.3、代码分布

经过这么简单的一看,基本上一个最小化的 CSI 代码分布已经可以出来了:

  • 首先需要做命令行解析,一般放在 cmd
  • 然后需要一个一般与 CSI 插件名称相同的结构体用来承载参数
  • 结构体内持有三个 gRPC Server 引用,并通过适当的方法使用内部参数还初始化这个三个 gRPC Server
  • 有了这些 gRPC Server 以后通过 server.go 中的 NewNonBlockingGRPCServer 方法将其启动(这里也可以看出 server.go 里面的方法我们后面可以 copy 直接用)

3.3、创建 CSI 插件骨架

项目骨架已经提交到 Github mritd/csi-archetype 项目,可直接 clone 并使用。

大致的研究完 Hostpath 的 CSI 源码,我们就可以根据其实现细节抽象出一个项目 CSI 骨架:

csi_archetype

在这个骨架中我们采用 corba 完成命令行参数解析,同时使用 logrus 作为日志输出库,这两个库都是 Kubernetes 以及 docker 比较常用的库;我们创建了一个叫 archetype 的结构体作为 CSI 的主承载类,这个结构体需要定义一些参数(parameter1…)方便后面初始化相关 gRPC Server 实现相关调用。

type archetype struct {name     stringnodeID   stringversion  stringendpoint string// Add CSI plugin parameters hereparameter1 stringparameter2 intparameter3 time.Durationcap   []*csi.VolumeCapability_AccessModecscap []*csi.ControllerServiceCapability}

与 Hostpath CSI 实现相同,我们创建一个 NewCSIDriver 方法来返回 archetype 结构体实例,在 NewCSIDriver 方法中将命令行解析得到的相关参数设置进结构体中并添加一些 AccessModesServiceCapabilities 方便后面 Identity Server 调用。

func NewCSIDriver(version, nodeID, endpoint, parameter1 string, parameter2 int, parameter3 time.Duration) *archetype {logrus.Infof("Driver: %s version: %s", driverName, version)// Add some check hereif parameter1 == "" {logrus.Fatal("parameter1 is empty")}n := &archetype{name:     driverName,nodeID:   nodeID,version:  version,endpoint: endpoint,parameter1: parameter1,parameter2: parameter2,parameter3: parameter3,}// Add access modes for CSI heren.AddVolumeCapabilityAccessModes([]csi.VolumeCapability_AccessMode_Mode{csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER,})// Add service capabilities for CSI heren.AddControllerServiceCapabilities([]csi.ControllerServiceCapability_RPC_Type{csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT,})return n}

整个骨架源码树中,命令行解析自己重构使用一些更加方便的命令行解析、日志输出库;结构体部分参考 Hostpath 结构体自己调整,server.go 用来创建 NonBlocking 的 gRPC Server(直接从 Hotspath 样例项目 copy 即可);然后就是三大 gRPC Server 的实现,由于是 “项目骨架” 所以相关方法我们都返回未实现,后续我们主要来实现这些方法就能让自己写的这个 CSI 插件 work。

Unimplemented_gRPC_Server

3.4、创建 NFS CSI 插件骨架

有了 CSI 的项目骨架以后,我们只需要简单地修改名字将其重命名为 NFS CSI 插件即可;由于这篇文章是先实现好了 NFS CSI(已经 work) 再来写的,所以 NFS CSI 的源码可以直接参考 Gozap/csi-nfs 即可,下面的部分主要介绍三大 gRPC Server 的实现

csi-nfs

3.5、实现 Identity Server

Identity Server

Identity Server 实现相对简单,总共就三个接口;GetPluginInfo 接口返回插件名称版本即可(注意版本号好像只能是 1.1.1 这种,v1.1.1 好像会报错);Probe 接口用来做健康检测可以直接返回空 response 即可,当然最理想的情况应该是做一些业务逻辑判活;GetPluginCapabilities 接口看起来简单但是要清楚返回的 Capabilities 含义,由于我们的 NFS 插件必然需要响应 CreateVolume 等请求(实现 Controller Server),所以 cap 必须给予 PluginCapability_Service_CONTROLLER_SERVICE,除此之外如果节点不支持均匀的创建外部存储供应商的 Volume,那么应当同时返回 PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS 以表示 CSI 处理时需要根据集群拓扑作调整;具体的可以查看 gRPC 注释:

const (PluginCapability_Service_UNKNOWN PluginCapability_Service_Type = 0// CONTROLLER_SERVICE indicates that the Plugin provides RPCs for// the ControllerService. Plugins SHOULD provide this capability.// In rare cases certain plugins MAY wish to omit the// ControllerService entirely from their implementation, but such// SHOULD NOT be the common case.// The presence of this capability determines whether the CO will// attempt to invoke the REQUIRED ControllerService RPCs, as well// as specific RPCs as indicated by ControllerGetCapabilities.PluginCapability_Service_CONTROLLER_SERVICE PluginCapability_Service_Type = 1// VOLUME_ACCESSIBILITY_CONSTRAINTS indicates that the volumes for// this plugin MAY NOT be equally accessible by all nodes in the// cluster. The CO MUST use the topology information returned by// CreateVolumeRequest along with the topology information// returned by NodeGetInfo to ensure that a given volume is// accessible from a given node when scheduling workloads.PluginCapability_Service_VOLUME_ACCESSIBILITY_CONSTRAINTS PluginCapability_Service_Type = 2)

3.6、实现 Controller Server

Controller Server 实际上对应着 Provisioning and Deleting 阶段;换句话说核心的创建/删除卷、快照等都应在此做实现,针对于本次编写的 NFS 插件仅做最小实现(创建/删除卷);需要注意的是除了核心的创建删除卷要实现以外还需要实现 ControllerGetCapabilities 方法,该方法返回 Controller Server 的 cap:

ControllerGetCapabilities

ControllerGetCapabilities 返回的实际上是在创建驱动时设置的 cscap:

n.AddControllerServiceCapabilities([]csi.ControllerServiceCapability_RPC_Type{csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,csi.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT,})

ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME 表示这个 Controller Server 支持创建/删除卷,ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT 表示支持创建/删除快照(快照功能是后来闲的没事加的);应该明确的是我们返回了特定的 cap 那就要针对特定方法做实现,因为你一旦声明了这些 cap Kubernetes 就认为有相应请求可以让你处理(你不能吹完牛逼然后关键时刻掉链子)。针对于可以返回哪些 cscap 可以通过这些 gRPC 常量来查看:

const (ControllerServiceCapability_RPC_UNKNOWN                  ControllerServiceCapability_RPC_Type = 0ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME     ControllerServiceCapability_RPC_Type = 1ControllerServiceCapability_RPC_PUBLISH_UNPUBLISH_VOLUME ControllerServiceCapability_RPC_Type = 2ControllerServiceCapability_RPC_LIST_VOLUMES             ControllerServiceCapability_RPC_Type = 3ControllerServiceCapability_RPC_GET_CAPACITY             ControllerServiceCapability_RPC_Type = 4// Currently the only way to consume a snapshot is to create// a volume from it. Therefore plugins supporting// CREATE_DELETE_SNAPSHOT MUST support creating volume from// snapshot.ControllerServiceCapability_RPC_CREATE_DELETE_SNAPSHOT ControllerServiceCapability_RPC_Type = 5ControllerServiceCapability_RPC_LIST_SNAPSHOTS         ControllerServiceCapability_RPC_Type = 6// Plugins supporting volume cloning at the storage level MAY// report this capability. The source volume MUST be managed by// the same plugin. Not all volume sources and parameters// combinations MAY work.ControllerServiceCapability_RPC_CLONE_VOLUME ControllerServiceCapability_RPC_Type = 7// Indicates the SP supports ControllerPublishVolume.readonly// field.ControllerServiceCapability_RPC_PUBLISH_READONLY ControllerServiceCapability_RPC_Type = 8// See VolumeExpansion for details.ControllerServiceCapability_RPC_EXPAND_VOLUME ControllerServiceCapability_RPC_Type = 9// Indicates the SP supports the// ListVolumesResponse.entry.published_nodes fieldControllerServiceCapability_RPC_LIST_VOLUMES_PUBLISHED_NODES ControllerServiceCapability_RPC_Type = 10// Indicates that the Controller service can report volume// conditions.// An SP MAY implement `VolumeCondition` in only the Controller// Plugin, only the Node Plugin, or both.// If `VolumeCondition` is implemented in both the Controller and// Node Plugins, it SHALL report from different perspectives.// If for some reason Controller and Node Plugins report// misaligned volume conditions, CO SHALL assume the worst case// is the truth.// Note that, for alpha, `VolumeCondition` is intended be// informative for humans only, not for automation.ControllerServiceCapability_RPC_VOLUME_CONDITION ControllerServiceCapability_RPC_Type = 11// Indicates the SP supports the ControllerGetVolume RPC.// This enables COs to, for example, fetch per volume// condition after a volume is provisioned.ControllerServiceCapability_RPC_GET_VOLUME ControllerServiceCapability_RPC_Type = 12)

当声明了 ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME 以后针对创建删除卷方法 CreateVolumeDeleteVolume 做实现即可;这两个方法实现就是常规的业务逻辑层面没什么技术含量,对于外部存储供应商是 NFS 来说无非就是接到一个 CreateVolumeRequest ,然后根据 request 给的 volume name 啥的信息自己执行一下在 NFS Server 上 mkdir ,删除卷处理就是反向的 rm -rf dir;在两个方法的处理中可能额外掺杂一些校验等其他的辅助实现。

CreateVolume

DeleteVolume

最后有几点需要注意的地方:

  • 幂等性: Kubernetes 可能由于一些其他原因会重复发出请求(比如超时重试),此时一定要保证创建/删除卷实现的幂等性,简单地说 Kubernetes 连续两次调用同一个卷创建 CSI 插件应当实现自动去重过滤,不能调用两次返回两个新卷。
  • 数据回写: 要明白的是 Controller Server 是 Provisioning and Deleting 阶段,此时还没有真正挂载到 Pod,所以就本地使用 NFS 作为存储后端来说 mkdir 以后要把目录、NFS Server 地址等必要信息通过 VolumeContext 返回给 Kubernetes,Kubernetes 接下来会传递给 Node Driver(Mount/Umount)用。
  • 预挂载: 当然这个问题目前只存在在 NFS 作为存储后端中,问题核心在于在创建卷进行 mkdir 之前,NFS 应该已经确保 mount 到了 Controller Server 容器本地,所以目前的做法就是启动 Controller Server 时就执行 NFS 挂载;如果用其他的后端存储比如阿里云存储时也要考虑在创建卷之前相关的 API Client 是否可用。

3.7、实现 Node Server

Node Server 实际上就是 Node Driver,简单地说当 Controller Server 完成一个卷的创建,并且已经 Attach 到 Node 以后(当然这里的 NFS 不需要 Attach),Node Server 就需要实现根据给定的信息将卷 Mount 到 Pod 或者从 Pod Umount 掉卷;同样的 Node Server 也许要返回一些信息来告诉 Kubernetes 自己的详细情况,这部份由两个方法完成 NodeGetInfoNodeGetCapabilities

NodeGetInfo_NodeGetCapabilities

NodeGetInfo 中返回节点的常规信息,比如 Node ID、最大允许的 Volume 数量、集群拓扑信息等;NodeGetCapabilities 返回这个 Node 的 cap,由于我们的 NFS 是真的啥也不支持,所以只好返回 NodeServiceCapability_RPC_UNKNOWN,至于其他的 cap 如下(含义自己看注释):

const (NodeServiceCapability_RPC_UNKNOWN              NodeServiceCapability_RPC_Type = 0NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME NodeServiceCapability_RPC_Type = 1// If Plugin implements GET_VOLUME_STATS capability// then it MUST implement NodeGetVolumeStats RPC// call for fetching volume statistics.NodeServiceCapability_RPC_GET_VOLUME_STATS NodeServiceCapability_RPC_Type = 2// See VolumeExpansion for details.NodeServiceCapability_RPC_EXPAND_VOLUME NodeServiceCapability_RPC_Type = 3// Indicates that the Node service can report volume conditions.// An SP MAY implement `VolumeCondition` in only the Node// Plugin, only the Controller Plugin, or both.// If `VolumeCondition` is implemented in both the Node and// Controller Plugins, it SHALL report from different// perspectives.// If for some reason Node and Controller Plugins report// misaligned volume conditions, CO SHALL assume the worst case// is the truth.// Note that, for alpha, `VolumeCondition` is intended to be// informative for humans only, not for automation.NodeServiceCapability_RPC_VOLUME_CONDITION NodeServiceCapability_RPC_Type = 4)

剩下的核心方法 NodePublishVolumeNodeUnpublishVolume 挂载/卸载卷同 Controller Server 创建删除卷一样都是业务处理,没啥可说的,按步就班的调用一下 Mount 上就行;唯一需要注意的点就是这里也要保证幂等性,同时由于要操作 Pod 目录,所以要把宿主机的 /var/lib/kubelet/pods 目录挂载到 Node Server 容器里。

3.8、部署测试 NFS 插件

NFS 插件写完以后就可以实体环境做测试了,测试方法不同插件可能并不相同,本 NFS 插件可以直接使用源码项目的 deploy 目录创建相关容器做测试(需要根据自己的 NFS Server 修改一些参数)。针对于如何部署下面做一下简单说明:

三大阶段笼统的其实对应着三个 Sidecar Container:

  • Provisioning and Deleting: external-provisioner
  • Attaching and Detaching: external-attacher
  • Mount and Umount: node-driver-registrar

我们的 NFS CSI 插件不需要 Attach,所以 external-attacher 也不需要部署;external-provisioner 只响应创建删除卷请求,所以通过 Deployment 部署足够多的复本保证 HA 就行;由于 Pod 不一定会落到那个节点上,理论上任意 Node 都可能有 Mount/Umount 行为,所以 node-driver-registrar 要以 Daemonset 方式部署保证每个节点都有一个。

四、其他说明

4.1、前期调试

在前期代码编写时一般都是 “盲狙”,就是按照自己的理解无脑实现,这时候可能离实际部署还很远,但是只是单纯的想知道某个 Request 里面到底是什么个东西,这时候你可以利用 mritd/socket2tcp 容器模拟监听 socket 文件,然后将请求转发到你的 IDE 监听端口上,然后再进行 Debug。

可能有人会问: “我直接在 Sidecar Containers 里写个 tcp 地址不就行了,还转发毛线,这不是脱裤子放屁多此一举么?”,但是这里我友情提醒一下,Sidecar Containers 指定 CSI 地址时填写非 socket 类型的地址是不好使的,会直接启动失败。

4.2、后期调试

等到代码编写到后期其实就开始 “真机” 调试了,这时候其实不必使用原始的打日志调试方法,NFS CSI 的项目源码中的 Dockerfile.debug 提供了使用 dlv 做远程调试的样例;具体怎么配合 IDE 做远程调试请自行 Google。

4.3、其他功能实现

其他功能根据需要可以自己酌情实现,比如创建/删除快照功能;对于 NFS 插件来说 NFS Server 又没有 API,所以最简单最 low 的办法当然是 tar -zcvf 了(哈哈哈(超大声)),当然性能么就不要提了。

五、总结

CSI 开发其实是针对 Kubernetes CSI Sidecar Containers 的 gRPC 开发,根据自己需求实现三大阶段中对应三大 gRPC Server 相应方法即可;相关功能要保证幂等性,cap 要看文档根据实际情况返回。

六、参考文档

]]>
Kubernetes CSI http://mritd.com/2020/08/19/how-to-write-a-csi-driver-for-kubernetes/#disqus_thread
树莓派4 Manjaro 系统定制 http://mritd.com/2020/08/19/make-a-custom-manjaro-image-for-rpi4/ http://mritd.com/2020/08/19/make-a-custom-manjaro-image-for-rpi4/ Wed, 19 Aug 2020 06:05:00 GMT 最近入手了新玩具 "吃灰派4",这一代性能提升真的很大,所以买回来是真的没办法 "吃灰" 了;但是由于目前 64bit 系统比较难产,所以只能自己定义一下 Manjaro 了。 一、目前的系统现状

截止本文编写时间,树莓派4 官方系统仍然不支持 64bit;但是当我在 3b+ 上使用 arch 64bit 以后我发现 32bit 系统和 64bit 系统装在同一个树莓派上在使用时那就是两个完全不一样的树莓派…所以对于这个新的 rpi4 那么必需要用 64bit 的系统;而当前我大致查看到支持 64bit 的系统只有 Ubuntu20、Manjaro 两个,Ubuntu 对我来说太重了(虽然服务器上我一直是 Ubuntu,但是 rpi 上我选择说 “不”),Manjaro 基于 Arch 这种非常轻量的系统非常适合树莓派这种开发板,所以最终我选择了 Manjaro。但是万万没想到的是 Manjaro 都是带 KDE 什么的图形化的,而我的树莓派只想仍在角落里跑东西,所以说图形化这东西对我来说也没啥用,最后迫于无奈只能自己通过 Manjaro 的工具自己定制了。

二、manjaro-arm-tools

经过几经查找各种 Google,发现了 Manjaro 官方提供了自定义创建 arm 镜像的工具 manjaro-arm-tools,这个工具简单使用如下:

  • 首先准备一个 Manjaro 系统(虚拟机 x86 即可)
  • 然后安装 manjaro-arm-tool 所需依赖工具
  • 添加 Manjaro 的软件源
  • 安装 manjaro-arm-tool sudo pacman -Syyu manjaro-strit-keyring && sudo pacman -S manjaro-arm-tools-git

当工具都准备完成后,只需要执行 sudo buildarmimg -d rpi4 -e minimal 即可创建 manjaro 的 rpi4 最小镜像。

三、系统定制

在使用 manjaro-arm-tool 创建系统以后发现一些细微的东西需要自己调整,比如网络设置常用软件包等,而 manjaro-arm-tool 工具又没有提供太好的自定义处理的一些 hook,所以最后萌生了自己根据 manjaro-arm-tool 来创建自己的 rpi4 系统定制工具的想法。

3.1、常用软件包安装

在查看了 manjaro-arm-tool 的源码后可以看到实际上软件安装就是利用 systemd-nspawn 进入到 arm 系统执行 pacman 安装,自己依葫芦画瓢增加一些常用的软件包安装:

systemd-nspawn -q --resolv-conf=copy-host --timezone=off -D ${ROOTFS_DIR} pacman -Syyu zsh htop vim wget which git make net-tools dnsutils inetutils iproute2 sysstat nload lsof --noconfirm

3.2、pacman 镜像

在安装软件包时发现安装速读奇慢,研究以后发现是没有使用国内的镜像源,故增加了国内镜像源的处理:

systemd-nspawn -q --resolv-conf=copy-host --timezone=off -D ${ROOTFS_DIR} pacman-mirrors -c China

3.3、网络处理

3.3.1、有线连接

默认的 manjaro-arm-tool 创建的系统网络部分采用 dhspcd 做 dhcp 处理,但是我个人感觉一切尽量精简统一还是比较好的;所以准备网络部分完全由 systemd 接管处理,即直接使用 systemd-networkd 和 systemd-resolved;systemd-networkd 处理相对简单,编写一个配置文件然后 enable systemd-networkd 服务即可:

/etc/systemd/network/10-eth-dhcp.network

[Match]Name=eth*[Network]DHCP=yes

让 systemd-networkd 开机自启动

systemd-nspawn -q --resolv-conf=copy-host --timezone=off -D ${ROOTFS_DIR} systemctl enable systemd-networkd.service

一开始以为 systemd-resolved 同样 enable 一下就行,后来发现每次开机初始化以后 systemd-resolved 都会被莫明其妙的 disable 掉;经过几经寻找和开 issue 问作者,发现这个操作是被 manjaro-arm-oem-install 包下的脚本执行的,作者的回复意思是大部分带有图形化的版本网络管理工具都会与 systemd-resolved 冲突,所以默认关闭了,这时候我们就要针对 manjaro-arm-oem-install 单独处理一下:

systemd-nspawn -q --resolv-conf=copy-host --timezone=off -D ${ROOTFS_DIR} systemctl enable systemd-resolved.servicesed -i 's@systemctl disable systemd-resolved.service 1> /dev/null 2>&1@@g' ${ROOTFS_DIR}/usr/share/manjaro-arm-oem-install/manjaro-arm-oem-install

3.3.2、无限连接

有线连接只要 systemd-networkd 处理好就能很好的工作,而无线连接目前有很多方案,我一开始想用 netctl,后来发现这东西虽然是 Arch 亲儿子,但是在系统定制时采用 systemd-nspawn 调用不兼容(因为里面调用了 systemd 的一些命令,这些命令一般只有在开机时才可用),而且只用 netctl 来管理 wifi 还感觉怪怪的,后来我的想法是要么用就全都用,要么就纯手动不要用这些东西,所以最后的方案是 wpa_supplicant + systemd-networkd 一把梭:

/etc/systemd/network/10-wlan-dhcp.network.example

# 1. Generate wifi configuration (don't modify the name of wpa_supplicant-wlan0.conf file)# $ wpa_passphrase MyNetwork SuperSecretPassphrase > /etc/wpa_supplicant/wpa_supplicant-wlan0.conf## 2. Connect to wifi automatically after booting# $ systemctl enable wpa_supplicant@wlan0## 3.Systemd automatically makes dhcp request# $ cp /etc/systemd/network/10-wlan-dhcp.network.example /etc/systemd/network/10-wlan-dhcp.network[Match]Name=wlan*[Network]DHCP=yes

3.4、内核调整

在上面的一些调整完成后我就启动系统实体机测试了,测试过程中发现安装 docker 以后会有两个警告,大致意思就是不支持 swap limit 和 cpu limit;查询资料以后发现是内核有两个参数没开启(CONFIG_MEMCG_SWAPCONFIG_CFS_BANDWIDTH)…当然我这种强迫症是不能忍的,没办法就自己在 rpi4 上重新编译了内核(后来我想想还不如用 arch 32bit 然后自己编译 64bit 内核了):

git clone http://github.com/mritd/linux-rpi4.gitcd linux-rpi4MAKEFLAGS='-j4' makepkg

3.5、外壳驱动

由于我的 rpi4 配的是 ARGON ONE 的外壳,所以电源按钮还有风扇需要驱动才能完美工作,没办法我又编译了 ARGON ONE 外壳的驱动:

git clone http://github.com/mritd/argonone.gitcd argononemakepkg

四、定制脚本

综合以上的各种修改以后,我从 manjaro-arm-tool 提取出了定制化的 rpi4 的编译脚本,该脚本目前存放在 mritd/manjaro-rpi4 仓库中;目前使用此脚本编译的系统镜像默认进行了以下处理:

  • 调整 pacman mirror 为中国
  • 安装常用软件包(zsh htop vim wget which…)
  • 有线网络完全的 systemd-networkd 接管,resolv.conf 由 systemd-resolved 接管
  • 无线网络由 wpa_supplicant 和 systemd-networkd 接管
  • 安装自行编译的内核以消除 docker 警告(自编译内核不影响升级,升级/覆盖安装后自动恢复)

至于 ARGON ONE 的外壳驱动只在 resources 目录下提供了安装包,并未默认安装到系统。

]]>
Linux Linux Manjaro http://mritd.com/2020/08/19/make-a-custom-manjaro-image-for-rpi4/#disqus_thread
如何在 Filebeat 端进行日志处理 http://mritd.com/2020/08/19/how-to-modify-filebeat-source-code-to-processing-logs/ http://mritd.com/2020/08/19/how-to-modify-filebeat-source-code-to-processing-logs/ Wed, 19 Aug 2020 06:01:00 GMT 本文主要介绍在 ELK 日志系统中,日志切割处理直接在 filebeat 端实现的一些方式;其中包括 filebeat processor 的扩展以及 module 扩展等。 一、起因

目前某项目组日志需要做切割处理,针对日志信息进行分割并提取 k/v 放入 es 中方便查询。这种需求在传统 ELK 中应当由 logstash 组件完成,通过 gork 等操作对日志进行过滤、切割等处理。不过很尴尬的是我并不会 ruby,logstash pipeline 的一些配置我也是极其头疼,而且还不想学…更不凑巧的是我会写点 go,那么理所应当的此时的我对 filebeat 源码产生了一些想法,比如我直接在 filebeat 端完成日志处理,然后直接发 es/logstash,这样似乎更方便,而且还能分摊 logstash 的压力,我感觉这个操作并不过分😂…

二、需求

目前某项目组 java 日志格式如下:

2020-04-30 21:56:30.117$$api-test-65c8c7cf7f-lng7h$$http-nio-8080-exec-3$$INFO$$com.example.api.common.filter.GlobalDataFilter$$GlobalDataFilter.java$$95$$testbuild commonData from header :{"romVersion":"W_V2.1.4","softwareVersion":"15","token":"aFxANNM3pnRYpohvLMSmENydgFSfsmFMgCbFWAosIE="}$$$$

目前开发约定格式为日志通过 $$ 进行分割,日志格式比较简单,但是 logstash 共用(nginx 等各种日志都会往这个 logstash 输出),不想去折腾 logstash 配置的情况下,只需要让 filebeat 能够直接切割并设置好 k/v 对应既可。

三、filebeat module

module 部份只做简介,以为实际上依托 es 完成,意义不大。

当然在考虑修改 filebeat 源码后,我第一想到的是 filebeat 的 module,这个 module 在官方文档中是个很神奇的东西;通过开启一个 module 就可以对某种日志直接做处理,这种东西似乎就是我想要的;比如我写一个 “项目名” module,然后 filebeat 直接开启这个 module,这个项目的日志就直接自动处理好(听起来就很 “上流”)…

针对于自定义 module,官方给出了文档: Creating a New Filebeat Module

按照文档操作如下(假设我们的项目名为 cdm):

# 克隆源码git clone git@github.com:elastic/beats.git# 切换到稳定分支cd bests && git checkout -b v7.6.2 v7.6.2-module# 创建 module,GO111MODULE 需要设置为 off# 在 7.6.2 版本官方尚未开始支持 go modcd filebeatGO111MODULE=off make create-module MODULE=cdm

创建完成后目录结构如下

➜  filebeat git:(v7.6.2-module) ✗ tree module/cdmmodule/cdm├── _meta│   ├── config.yml│   ├── docs.asciidoc│   └── fields.yml└── module.yml1 directory, 4 files

这几个文件具体作用官方文档都有详细的描述;但是根据文档描述光有这几个文件是不够的,module 只是一个处理集合的定义,尚未包含任何处理,针对真正的处理需要继续创建 fileset,fileset 简单的理解就是针对具体的一组文件集合的处理;例如官方 nginx module 中包含两个 fileset: accesserror,这两个一个针对 access 日志处理一个针对 error 日志进行处理;在 fileset 中可以设置默认文件位置、处理方式。

But… 我翻了 nginx module 的样例配置才发现,module 这个东西实质上只做定义和存储处理表达式,具体的切割处理实际上交由 es 的 Ingest Node 处理;表达式里仍需要定义 grok 等操作,而且这东西最终会编译到 go 静态文件里;此时的我想说一句 “MMP”,本来我是不像写 grok 啥的才来折腾 filebeat,结果这个 module 折腾一圈还是要写 grok 啥的,而且这东西直接借助 es 完成导致压力回到了 es 同时每次修改还得重新编译 filebeat… 所以折腾到这我就放弃了,这已经违背了当初的目的,有兴趣的可以参考以下文档继续折腾:

四、filebeat processors

经历了 module 的失望以后,我把目光对准了 processors;processors 是 filebeat 一个强大的功能,顾名思义它可以对 filbeat 收集到的日志进行一些处理;从官方 Processors 页面可以看到其内置了大量的 processor;这些 processor 大部份都是直接对日志进行 “写” 操作,所以理论上我们自己写一个 processor 就可以 “为所欲为+为所欲为=为所欲为”。

不过不幸的是关于 processor 的开发官方并未给出文档,官方认为这是一个 high level 的东西,不过也找到了一个 issue 对其做了相关回答: How do I write a processor plugin by myself;所以最好的办法就是直接看已有 processor 的源码抄一个。

理所应当的找了一个软柿子捏: add_host_metadata,add_host_metadata processor 顾名思义在每个日志事件(以下简称为 event)中加入宿主机的信息,比如 hostname 啥的;以下为 add_host_metadata processor 的文件结构(processors 代码存储在 libbeat/processors 目录下)。

dir_tree

通过阅读源码和 issue 的回答可以看出,我们自定义的 processor 只需要实现 Processor interface 既可,这个接口定义如下:

Processor interface

通过查看 add_host_metadata 的源码,String() string 方法只需要返回这个 processor 名称既可(可以包含必要的配置信息);Run(event *beat.Event) (*beat.Event, error) 方法表示在每一条日志被读取后都会转换为一个 event 对象,我们在方法内进行处理然后把 event 返回既可(其他 processor 可能也要处理)。

add_host_metadata source

有了这些信息就简单得多了,毕竟作为一名合格的 CCE(Ctrl C + Ctrl V + Engineer) 抄这种操作还是很简单的,直接照猫画虎写一个就行了

config.go

package cmd// Config for cdm processor.type Config struct {Name           string          `config:"name"`}func defaultConfig() Config {return Config{}}

cdm.go

package cmdimport ("strings""github.com/elastic/beats/libbeat/logp""github.com/pkg/errors""github.com/elastic/beats/libbeat/beat""github.com/elastic/beats/libbeat/common""github.com/elastic/beats/libbeat/processors"jsprocessor "github.com/elastic/beats/libbeat/processors/script/javascript/module/processor")func init() {processors.RegisterPlugin("cdm", New)jsprocessor.RegisterPlugin("CDM", New)}type cdm struct {config Configfields []stringlog    *logp.Logger}const (processorName = "cdm"logName       = "processor.cdm")// New constructs a new cdm processor.func New(cfg *common.Config) (processors.Processor, error) {// 配置文件里就一个 Name 字段,结构体留着以后方便扩展config := defaultConfig()if err := cfg.Unpack(&config); err != nil {return nil, errors.Wrapf(err, "fail to unpack the %v configuration", processorName)}p := &cdm{config: config,// 待分割的每段日志对应的 keyfields: []string{"timestamp", "hostname", "thread", "level", "logger", "file", "line", "serviceName", "traceId", "feTraceId", "msg", "exception"},log:    logp.NewLogger(logName),}return p, nil}// 真正的日志处理逻辑// 为了保证后面的 processor 正常处理,这里面没有 return 任何 error,只是简单的打印func (p *cdm) Run(event *beat.Event) (*beat.Event, error) {// 尝试获取 message,理论上这一步不应该出现问题msg, err := event.GetValue("message")if err != nil {p.log.Error(err)return event, nil}message, ok := msg.(string)if !ok {p.log.Error("failed to parse message")return event, nil}// 分割日志fieldsValue := strings.Split(message, "$$")p.log.Debugf("message fields: %v", fieldsVaule)// 为了保证不会出现数组越界需要判断一下(万一弄出个格式不正常的日志过来保证不崩)if len(fieldsValue) < len(p.fields) {p.log.Errorf("incorrect field length: %d, expected length: %d", len(fieldsValue), len(p.fields))return event, nil}// 这里遍历然后赛会到 event 既可data := common.MapStr{}for i, k := range p.fields {_, _ = event.PutValue(k, strings.TrimSpace(fieldsValue[i]))}event.Fields.DeepUpdate(data)return event, nil}func (p *cdm) String() string {return processorName}

写好代码以后就可以编译一个自己的 filebeat 了(开心ing)

cd filebeat# 如果想交叉编译 linux 需要增加 GOOS=linux 变量 GO111MODULE=off make

然后编写配置文件进行测试,日志相关字段已经成功塞到了 event 中,这样我直接发到 es 或者 logstash 就行了。

filebeat.inputs:- type: log  enabled: true  paths:    - /Users/natural/tmp/cdm.log  processors:    - cdm: ~  multiline.pattern: ^\d{4}-\d{1,2}-\d{1,2}  multiline.match: after  multiline.negate: true  multiline.timeout: 5s

五、script processor

在我折腾完源码以后,反思一下其实这种方式需要自己编译 filebeat,而且每次规则修改也很不方便,唯一的好处真的就是用代码可以 “为所欲为”;反过来一想 “filebeat 有没有 processor 的扩展呢?脚本热加载那种?” 答案是使用 script processor,script processor 虽然名字上是个 processor,实际上其包含了完整的 ECMA 5.1 js 规范实现;结论就是我们可以写一些 js 脚本来处理日志,然后 filebeat 每次启动后加载这些脚本既可。

script processor 的使用方式很简单,js 文件中只需要包含一个 function process(event) 方法既可,与自己用 go 实现的 processor 类似,每行日志也会形成一个 event 对象然后调用这个方法进行处理;目前 event 对象可用的 api 需要参考官方文档需要注意的是 script processor 目前只支持 ECMA 5.1 语法规范,超过这个范围的语法是不被支持;实际上其根本是借助了 http://github.com/dop251/goja 这个库来实现的。同时为了方便开发调试,script processor 也增加了一些 nodejs 的兼容 module,比如 console.log 等方法是可用的;以下为 js 处理上面日志的逻辑:

var console = require('console');var fileds = new Array("timestamp", "hostname", "thread", "level", "logger", "file", "line", "serviceName", "traceId", "feTraceId", "msg", "exception")function process(event) {    var message = event.Get("message");    if (message == null || message == undefined || message == '') {        console.log("failed to get message");        return    }    var fieldValues = message.split("$$");    if (fieldValues.length<fileds.length) {        console.log("incorrect field length");        return    }    for (var i = 0; i < fileds.length; ++i) {        event.Put(fileds[i],fieldValues[i].trim())    }}

写好脚本后调整配置测试既可,如果 js 编写有问题,可以通过 console.log 来打印日志进行不断的调试

filebeat.inputs:- type: log  enabled: true  paths:    - /Users/natural/tmp/cdm.log  processors:    - script:        lang: js        id: cdm        file: cdm.js  multiline.pattern: ^\d{4}-\d{1,2}-\d{1,2}  multiline.match: after  multiline.negate: true  multiline.timeout: 5s

需要注意的是目前 lang 的值只能为 javascriptjs(官方文档写的只能是 javascript);根据代码来看后续 script processor 有可能支持其他脚本语言,个人认为主要取决于其他脚本语言有没有纯 go 实现的 runtime,如果有的话未来很有可能被整合到 script processor 中。

script processor

六、其他 processor

研究完 script processor 后我顿时对其他 processor 也产生了兴趣,随着更多的查看processor 文档,我发现其实大部份过滤分割能力已经有很多 processor 进行了实现,其完善程度外加可扩展的 script processor 实际能力已经足矣替换掉 logstash 的日志分割过滤处理了。比如上面的日志切割其实使用 dissect processor 实现更加简单(这个配置并不完善,只是样例):

processors:  - dissect:      field: "message"      tokenizer: "%{timestamp}$$%{hostname}$$%{thread}$$%{level}$$%{logger}$$%{file}$$%{line}$$%{serviceName}$$%{traceId}$$%{feTraceId}$$%{msg}$$%{exception}$$"

除此之外还有很多 processor,例如 drop_eventdrop_fieldstimestamp 等等,感兴趣的可以自行研究。

七、总结

基本上折腾完以后做了一个总结:

  • filebeat module: 这就是个华而不实的东西,每次修改需要重新编译且扩展能力几近于零,最蛋疼的是实际逻辑通过 es 来完成;我能想到的是唯一应用场景就是官方给我们弄一些 demo 来炫耀用的,比如 nginx module;实际生产中 nginx 日志格式保持原封不动的人我相信少之又少。
  • filebeat custom processor: 每次修改也需要重新编译且需要会 go 语言还有相关工具链,但是好处就是完全通过代码实现真正的为所欲为;扩展性取决于外部是否对特定位置做了可配置化,比如预留可以配置切割用正则表达式的变量等,最终取决于代码编写者(怎么为所欲为的问题)。
  • filebeat script processor: 完整 ECMA 5.1 js 规范支持,代码化对日志进行为所欲为,修改不需要重新编译;普通用户我个人觉得是首选,当然同时会写 go 和 js 的就看你想用哪个了。
  • filebeat other processor: 基本上实现了很多 logstash 的功能,简单用用很舒服,复杂场景还是得撸代码;但是一些特定的 processor 很实用,比如加入宿主机信息的 add_host_metadata processor 等。
]]>
Golang Kubernetes Golang http://mritd.com/2020/08/19/how-to-modify-filebeat-source-code-to-processing-logs/#disqus_thread
如何不通过 docker 下载 docker image http://mritd.com/2020/03/31/how-to-download-docker-image-without-docker/ http://mritd.com/2020/03/31/how-to-download-docker-image-without-docker/ Tue, 31 Mar 2020 15:52:38 GMT 这是一个比较骚的动作,但是事实上确实有这个需求,折腾半天找工具看源码,这里记录一下(不想看源码分析啥的请直接跳转到第五部份)。

这是一个比较骚的动作,但是事实上确实有这个需求,折腾半天找工具看源码,这里记录一下(不想看源码分析啥的请直接跳转到第五部份)。

一、起因

由于最近某个爬虫业务需要抓取微信公众号的一些文章,某开发小伙伴想到了通过启动安卓虚拟机然后抓包的方式实现;经过几番寻找最终我们选择采用 docker 的方式启动安卓虚拟机,docker 里安卓虚拟机比较成熟的项目我们找到了 http://github.com/budtmo/docker-android 这个项目;但是由于众所周知的原因这个 2G+ 的镜像国内拉取是非常慢的,于是我想到了通过国外 VPS 拉取然后 scp 回来… 由于贫穷的原因,当我实际操作的时候遇到了比较尴尬的问题: **VPS 磁盘空间 25G,镜像拉取后解压接近 10G,我需要 docker save 成 tar 包再进行打包成 tar.gz 格式 scp 回来,这个时候空间不够用了…**所以我当时就在想有没有办法让 docker daemon 拉取镜像时不解压?或者说自己通过 HTTP 下载镜像直接存储为 tar?

二、尝试造轮子

当出现了上面的问题后,我第一反应就是:

三、猜测源码

当我查看 containers/image README 文档时发现其提到了 skopeo 项目,并且很明确的说了

The containers/image project is only a library with no user interface; you can either incorporate it into your Go programs, or use the skopeo tool:
The skopeo tool uses the containers/image library and takes advantage of many of its features, e.g. skopeo copy exposes the containers/image/copy.Image functionality.

那么也就是说镜像下载这块很大可能应该调用 containers/image/copy.Image 完成,随即就看了下源码文档

很明显,types.ImageReferenceOptions 里面的属性啥的我完全看不懂… 😂😂😂

四、看 skopeo 源码

containers/image 源码看不懂时,突然想到 skopeo 调用的是这个玩意,那么依葫芦画瓢看 skopeo 源码应该能行;接下来常规操作 clone skopeo 源码然后编译运行测试;编译后 skopeo 支持命令如下

NAME:   skopeo - Various operations with container images and container image registriesUSAGE:   skopeo [global options] command [command options] [arguments...]VERSION:   0.1.42-dev commit: 018a0108b103341526b41289c434b59d65783f6fCOMMANDS:   copy               Copy an IMAGE-NAME from one location to another   inspect            Inspect image IMAGE-NAME   delete             Delete image IMAGE-NAME   manifest-digest    Compute a manifest digest of a file   sync               Synchronize one or more images from one location to another   standalone-sign    Create a signature using local files   standalone-verify  Verify a signature using local files   list-tags          List tags in the transport/repository specified by the REPOSITORY-NAME   help, h            Shows a list of commands or help for one commandGLOBAL OPTIONS:   --debug                     enable debug output   --policy value              Path to a trust policy file   --insecure-policy           run the tool without any policy check   --registries.d DIR          use registry configuration files in DIR (e.g. for container signature storage)   --override-arch ARCH        use ARCH instead of the architecture of the machine for choosing images   --override-os OS            use OS instead of the running OS for choosing images   --override-variant VARIANT  use VARIANT instead of the running architecture variant for choosing images   --command-timeout value     timeout for the command execution (default: 0s)   --help, -h                  show help   --version, -v               print the version

我掐指一算调用 copy 命令应该是我要找的那个它,所以常规操作打开源码直接看

copy_cmd

通过继续追踪 alltransports.ParseImageName 方法最终可以得知 copy 命令的 SOURCE-IMAGEDESTINATION-IMAGE 都支持哪些写法

tp_register

每一个 Transport 的实现都提供了 Name 方法,其名称即为 src 或 dest 镜像名称的前缀,例如 docker://nginx:1.17.6

tp_docker

经过测试不同的 Transport 格式并不完全一致(具体看源码),比如 docker://nginx:1.17.6dir:/tmp/nginx;同时这些 Transport 并非完全都适用与 src 与 dest,比如 tarball:/tmp/nginx.tar 支持 src 而不支持 dest;其判断核心依据为 ImageReference.NewImageSourceImageReference.NewImageDestination 方法实现是否返回 error

NewImageDestination

当我看了一会各种 Transport 源码后我发现一件事: 这特么不就是我要造的轮子么!😱😱😱

五、skopeo copy 使用

5.1、不借助 docker 下载镜像

skopeo --insecure-policy copy docker://nginx:1.17.6 docker-archive:/tmp/nginx.tar

--insecure-policy 选项用于忽略安全策略配置文件,该命令将会直接通过 http 下载目标镜像并存储为 /tmp/nginx.tar,此文件可以直接通过 docker load 命令导入

5.2、从 docker daemon 导出镜像

skopeo --insecure-policy copy docker-daemon:nginx:1.17.6 docker-archive:/tmp/nginx.tar

该命令将会从 docker daemon 导出镜像到 /tmp/nginx.tar;为什么不用 docker save?因为我是偷懒 dest 也是 docker-archive,实际上 skopeo 可以导出为其他格式比如 ocioci-archiveostree

5.3、其他命令

skopeo 还有一些其他的实用命令,比如 sync 可以在两个位置之间同步镜像(😂早知道我还写个鸡儿 gcrsync),inspect 可以查看镜像信息等,迫于本人太懒,剩下的请自行查阅文档、--help 以及源码(没错,整篇文章都没写 skopeo 怎么安装)。

]]>
Docker Linux Docker http://mritd.com/2020/03/31/how-to-download-docker-image-without-docker/#disqus_thread
kubeadm 证书期限调整 http://mritd.com/2020/01/21/how-to-extend-the-validity-of-your-kubeadm-certificate/ http://mritd.com/2020/01/21/how-to-extend-the-validity-of-your-kubeadm-certificate/ Tue, 21 Jan 2020 04:43:36 GMT 最近 kubeadm HA 的集群折腾完了,发现集群证书始终是 1 年有效期,然后自己还有点子担心;无奈只能研究一下源码一探究竟了... 一、证书管理

kubeadm 集群安装完成后,证书管理上实际上大致是两大类型:

  • 自动滚动续期
  • 手动定期续期

自动滚动续期类型的证书目前从我所阅读文档和实际测试中目前只有 kubelet 的 client 证书;kubelet client 证书自动滚动涉及到了 TLS bootstrapping 部份,其核心由两个 ClusterRole 完成(system:certificates.k8s.io:certificatesigningrequests:nodeclientsystem:certificates.k8s.io:certificatesigningrequests:selfnodeclient),针对这两个 ClusterRole kubeadm 在引导期间创建了 bootstrap token 来完成引导期间证书签发(该 Token 24h 失效),后续通过预先创建的 ClusterRoleBinding(kubeadm:node-autoapprove-bootstrapkubeadm:node-autoapprove-certificate-rotation) 完成自动的 node 证书续期;kubelet client 证书续期部份涉及到 TLS bootstrapping 太多了,有兴趣的可以仔细查看(最后还是友情提醒: 用 kubeadm 一定要看看 Implementation details)。

手动续期的证书目前需要在到期前使用 kubeadm 命令自行续期,这些证书目前可以通过以下命令列出

# 不要在意我的证书过期时间是 10 年,下面会说k1.node ➜ kubeadm alpha certs check-expiration[check-expiration] Reading configuration from the cluster...[check-expiration] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'CERTIFICATE                EXPIRES                  RESIDUAL TIME   CERTIFICATE AUTHORITY   EXTERNALLY MANAGEDadmin.conf                 Dec 06, 2029 20:58 UTC   9y                                      noapiserver                  Dec 06, 2029 20:59 UTC   9y              ca                      noapiserver-kubelet-client   Dec 06, 2029 20:59 UTC   9y              ca                      nocontroller-manager.conf    Dec 06, 2029 20:59 UTC   9y                                      nofront-proxy-client         Dec 06, 2029 20:59 UTC   9y              front-proxy-ca          noscheduler.conf             Dec 06, 2029 20:59 UTC   9y                                      noCERTIFICATE AUTHORITY   EXPIRES                  RESIDUAL TIME   EXTERNALLY MANAGEDca                      Jan 13, 2030 08:45 UTC   9y              nofront-proxy-ca          Jan 13, 2030 08:45 UTC   9y              no

二、证书期限调整

上面已经提到了,手动管理部份的证书需要自己用命令续签(kubeadm alpha certs renew all),而且你会发现续签以后有效期还是 1 年;kubeadm 的初衷是 **”为快速创建 kubernetes 集群的最佳实践”**,当然最佳实践包含确保证书安全性,毕竟 Let’s Encrypt 的证书有效期只有 3 个月的情况下 kubeadm 有效期有 1 年已经很不错了;但是对于最佳实践来说,我们公司的集群安全性并不需要那么高,一年续期一次无疑在增加运维人员心智负担(它并不最佳),所以我们迫切需要一种 “一劳永逸” 的解决方案;当然我目前能想到的就是找到证书签发时在哪设置的有效期,然后想办法改掉它。

2.1、源码分析

目前通过宏观角度看整个 kubeadm 集群搭建过程,其中涉及到证书签署大致有两大部份: init 阶段和后期 renew,下面开始分析两个阶段的源码

2.1.1、init 阶段

由于 kubernetes 整个命令行都是通过 cobra 库构建的,那么根据这个库的习惯首先直接从 cmd 包开始翻,而 kubernetes 源码组织的又比较清晰进而直接定位到 kubeadm 命令包下面;接着打开 app 目录一眼就看到了 phasesphases 顾名思义啊,整个 init 都是通过不同的 phases 完成的,那么直接去 phases 包下面找证书阶段的源码既可

init_source

进入到这个 certs.go 里面,直接列出所有方法,go 的规范里只有首字母大写才会被暴露出去,那么我们直接查看这些方法名既可;从名字上很轻松的看到了这个方法…基本上就是它了

certs.go

通过这个方法的代码会发现最终还是调用了 certSpec.CreateFromCA(cfg, caCert, caKey),那么接着看看这个方法

pkiutil.NewCertAndKey

通过这个方法继续往下翻发现调用了 pkiutil.NewCertAndKey(caCert, caKey, cfg),这个方法里最终调用了 NewSignedCert(config, key, caCert, caKey)

NewSignedCert

NewSignedCert 方法里看到证书有效期实际上是个常量,那也就意味着我改了这个常量 init 阶段的证书有效期八九不离十的就变了,再通过包名看这个是个 pkiutilxxxxxutil 明显是公共的,所以推测改了它 renew 阶段应该也会变

CertificateValidity

2.1.2、renew 阶段

renew 阶段也是老套路,不过稳妥点先从 cmd 找起来,所以先看 alpha 包下的 certs.go;这时候方法名语义清晰就很有好处,一下就能找到 newCmdCertsRenewal 方法

alpha_certs.go

而这个 newCmdCertsRenewal 方法实际上没啥实现,所以目测实现是从 getRenewSubCommands 实现的

getRenewSubCommands

看了 getRenewSubCommands 以后发现上面全是命令行库、配置文件参数啥的处理,核心在 renewCert 上,从这个方法里发现还有意外收获: renew 时实际上分两种情况处理,一种是使用了 --use-api 选项,另一种是未使用;当然根据上面的命令来说我们没使用,那么看 else 部份就行了(没看源码之前我特么居然没看 --help 不知道有这个选项)

renewCert

else 部份源码最终还是调用了 RenewUsingLocalCA 方法,这个方法一直往下跟会有一个 Renew 方法

Renew

这个方法一点进去… 我上面的想法是对的

FileRenewer_Renew

2.1.3、其他推测

根据刚刚查看代码可以看到在 renew 阶段判断了 --use-api 选项是否使用,通过跟踪源码发现最终会调用到 RenewUsingCSRAPI 方法上,RenewUsingCSRAPI 会调用集群 CSR Api 执行证书签署

RenewUsingCSRAPI

有了这个发现后基本上可以推测出这一步通过集群完成,那么按理说是应该受到 kube-controller-manager 组件的 --experimental-cluster-signing-duration 影响。

2.2、测试验证

2.2.1、验证修改源码

想验证修改源码是否有效只需要修改源码重新 build 出 kubeadm 命令,然后使用这个特定版本的 kubeadm renew 证书测试既可,源码调整的位置如下

update_source

然后命令行下执行 make cross 进行跨平台交叉编译(如果过你在 linux amd64 平台下则直接 make 既可)

➜  kubernetes git:(v1.17.4) ✗ make crossgrep: /proc/meminfo: No such file or directorygrep: /proc/meminfo: No such file or directory+++ [0116 23:43:19] Multiple platforms requested and available 64G >= threshold 40G, building platforms in parallel+++ [0116 23:43:19] Building go targets for {linux/amd64 linux/arm linux/arm64 linux/s390x linux/ppc64le} in parallel (output will appear in a burst when complete):    cmd/kube-proxy    cmd/kube-apiserver    cmd/kube-controller-manager    cmd/kubelet    cmd/kubeadm    cmd/kube-scheduler    vendor/k8s.io/apiextensions-apiserver    cluster/gce/gci/mounter+++ [0116 23:43:19] linux/amd64: build started+++ [0116 23:47:24] linux/amd64: build finished+++ [0116 23:43:19] linux/arm: build started+++ [0116 23:47:23] linux/arm: build finished+++ [0116 23:43:19] linux/arm64: build started+++ [0116 23:47:23] linux/arm64: build finished+++ [0116 23:43:19] linux/s390x: build started+++ [0116 23:47:24] linux/s390x: build finished+++ [0116 23:43:19] linux/ppc64le: build started+++ [0116 23:47:24] linux/ppc64le: build finishedgrep: /proc/meminfo: No such file or directorygrep: /proc/meminfo: No such file or directory+++ [0116 23:47:52] Multiple platforms requested and available 64G >= threshold 40G, building platforms in parallel+++ [0116 23:47:52] Building go targets for {linux/amd64 linux/arm# ... 省略编译日志

编译完成后能够在 _output/local/bin/linux/amd64 下找到刚刚编译成功的 kubeadm 文件,将编译好的 kubeadm scp 到已经存在集群上执行 renew,然后查看证书时间

kubeadm_renew

经过测试后确认源码修改方式有效

2.2.2、验证调整 CSR API

根据推测当使用 --use-api 会受到 kube-controller-manager 组件的 --experimental-cluster-signing-duration 影响,从而从集群中下发证书;所以首先在启动集群时需要将 --experimental-cluster-signing-duration 调整为 10 年,然后再进行测试

controllerManager:  extraArgs:    v: "4"    node-cidr-mask-size: "19"    deployment-controller-sync-period: "10s"    # 在 kubeadm 配置文件中设置证书有效期为 10 年    experimental-cluster-signing-duration: "86700h"    node-monitor-grace-period: "20s"    pod-eviction-timeout: "2m"    terminated-pod-gc-threshold: "30"

然后使用 --use-api 选项进行 renew

kubeadm alpha certs renew all --use-api

此时会发现日志中打印出 [certs] Certificate request "kubeadm-cert-kubernetes-admin-648w4" created 字样,接下来从 kube-system 的 namespace 中能够看到相关 csr

list_csr

这时我们开始手动批准证书,每次批准完成一个 csr,紧接着 kubeadm 会创建另一个 csr

approve_csr

当所有 csr 被批准后,再次查看集群证书发现证书期限确实被调整了

success

三、总结

总结一下,调整 kubeadm 证书期限有两种方案;第一种直接修改源码,耗时耗力还得会 go,最后还要跑跨平台编译(很耗时);第二种在启动集群时调整 kube-controller-manager 组件的 --experimental-cluster-signing-duration 参数,集群创建好后手动 renew 一下并批准相关 csr。

两种方案各有利弊,修改源码方式意味着在 client 端签发处理,不会对集群产生永久性影响,也就是说哪天你想 “反悔了” 你不需要修改集群什么配置,直接用官方 kubeadm renew 一下就会变回一年期限的证书;改集群参数实现的方式意味着你不需要懂 go 代码,只需要常规的集群配置既可实现,同时你也不需要跑几十分钟的交叉编译,不需要为编译过程中的网络问题而烦恼;所以最后使用哪种方案因人因情况而定吧。

]]>
Kubernetes Kubernetes http://mritd.com/2020/01/21/how-to-extend-the-validity-of-your-kubeadm-certificate/#disqus_thread
kubeadm 集群升级 http://mritd.com/2020/01/21/how-to-upgrade-kubeadm-cluster/ http://mritd.com/2020/01/21/how-to-upgrade-kubeadm-cluster/ Tue, 21 Jan 2020 04:41:46 GMT 真是不巧,刚折腾完 kubeadm 搭建集群(v1.17.0),第二天早上醒来特么的 v1.17.1 发布了;这我能忍么,肯定不能忍,然后就开始了集群升级之路... 一、升级前准备
  • 确保你的集群是 kubeadm 搭建的(等同于废话)
  • 确保当前集群已经完成 HA(多个 master 节点)
  • 确保在夜深人静的时候(无大量业务流量)
  • 确保集群版本大于 v1.16.0
  • 确保已经仔细阅读了目标版本 CHANGELOG
  • 确保做好了完整地集群备份

二、升级注意事项

  • 升级后所有集群组件 Pod 会重启(hash 变更)
  • 升级时 kubeadm 版本必须大于或等于目标版本
  • 升级期间所有 kube-proxy 组件会有一次全节点滚动更新
  • 升级只支持顺次进行,不支持跨版本升级(You only can upgrade from one MINOR version to the next MINOR version, or between PATCH versions of the same MINOR. That is, you cannot skip MINOR versions when you upgrade. For example, you can upgrade from 1.y to 1.y+1, but not from 1.y to 1.y+2.)

关于升级版本问题…虽然是这么说的,但是官方文档样例代码里是从 v1.16.0 升级到 v1.17.0;可能是我理解有误,跨大版本升级好像官方没提,具体啥后果不清楚…

三、升级 Master

事实上所有升级工作主要是针对 master 节点做的,所以整个升级流程中最重要的是如何把 master 升级好。

3.1、升级 kubeadm、kubectl

首先由于升级限制,必须先将 kubeadmkubectl 升级到大于等于目标版本

# replace x in 1.17.x-00 with the latest patch versionapt-mark unhold kubeadm kubectlapt-get updateapt-get install -y kubeadm=1.17.x-00 kubectl=1.17.x-00apt-mark hold kubeadm kubectl

当然如果你之前没有 hold 住这几个软件包的版本,那么就不需要 unhold;我的做法可能比较极端…一般为了防止后面的误升级安装完成后我会直接 rename 掉相关软件包的 apt source 配置(从根本上防止手贱)。

3.2、升级前准备

3.2.1、配置修改

对于高级玩家一般安装集群时都会自定义很多组件参数,此时不可避免的会采用配置文件;所以安装完新版本的 kubeadm 后就要着手修改配置文件中的 kubernetesVersion 字段为目标集群版本,当然有其他变更也可以一起修改。

3.2.2、节点驱逐

如果你的 master 节点也当作 node 在跑一些工作负载,则需要执行以下命令驱逐这些 pod 并使节点进入维护模式(禁止调度)。

# 将 NODE_NAME 换成 Master 节点名称kubectl drain NODE_NAME --ignore-daemonsets

3.2.3、查看升级计划

完成节点驱逐以后,可以通过以下命令查看升级计划;升级计划中列出了升级期间要执行的所有步骤以及相关警告,一定要仔细查看。

k8s16.node ➜  ~ kubeadm upgrade plan --config /etc/kubernetes/kubeadm.yamlW0115 10:59:52.586204     983 validation.go:28] Cannot validate kube-proxy config - no validator is availableW0115 10:59:52.586241     983 validation.go:28] Cannot validate kubelet config - no validator is available[upgrade/config] Making sure the configuration is correct:W0115 10:59:52.605458     983 common.go:94] WARNING: Usage of the --config flag for reconfiguring the cluster during upgrade is not recommended!W0115 10:59:52.607258     983 validation.go:28] Cannot validate kube-proxy config - no validator is availableW0115 10:59:52.607274     983 validation.go:28] Cannot validate kubelet config - no validator is available[preflight] Running pre-flight checks.[upgrade] Making sure the cluster is healthy:[upgrade] Fetching available versions to upgrade to[upgrade/versions] Cluster version: v1.17.0[upgrade/versions] kubeadm version: v1.17.1External components that should be upgraded manually before you upgrade the control plane with 'kubeadm upgrade apply':COMPONENT   CURRENT   AVAILABLEEtcd        3.3.18    3.4.3-0Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':COMPONENT   CURRENT       AVAILABLEKubelet     5 x v1.17.0   v1.17.1Upgrade to the latest version in the v1.17 series:COMPONENT            CURRENT   AVAILABLEAPI Server           v1.17.0   v1.17.1Controller Manager   v1.17.0   v1.17.1Scheduler            v1.17.0   v1.17.1Kube Proxy           v1.17.0   v1.17.1CoreDNS              1.6.5     1.6.5You can now apply the upgrade by executing the following command:        kubeadm upgrade apply v1.17.1_____________________________________________________________________

3.3、执行升级

确认好升级计划以后,只需要一条命令既可将当前 master 节点升级到目标版本

kubeadm upgrade apply v1.17.1 --config /etc/kubernetes/kubeadm.yaml

升级期间会打印很详细的日志,在日志中可以实时观察到升级流程,建议仔细关注升级流程;在最后一步会有一条日志 [addons] Applied essential addon: kube-proxy,这意味着集群开始更新 kube-proxy 组件,该组件目前是通过 daemonset 方式启动的;这会意味着此时会造成全节点的 kube-proxy 更新;理论上不会有很大影响,但是升级是还是需要注意一下这一步操作,在我的观察中似乎 kube-proxy 也是通过滚动更新完成的,所以问题应该不大。

3.4、升级 kubelet

在单个 master 上升级完成后,只会升级本节点的 master 相关组件和全节点的 kube-proxy 组件;由于 kubelet 是在宿主机安装的,所以需要通过包管理器手动升级 kubelet

# replace x in 1.17.x-00 with the latest patch versionapt-mark unhold kubeletapt-get install -y kubelet=1.17.x-00apt-mark hold kubelet

更新完成后执行 systemctl restart kubelet 重启,并等待启动成功既可;最后不要忘记解除当前节点的维护模式(uncordon)。

3.5、升级其他 Master

当其中一个 master 节点升级完成后,其他的 master 升级就会相对简单的多;首先国际惯例升级一下 kubeadmkubectl 软件包,然后直接在其他 master 节点执行 kubeadm upgrade node 既可。由于 apiserver 等组件配置已经在升级第一个 master 时上传到了集群的 configMap 中,所以事实上其他 master 节点只是正常拉取然后重启相关组件既可;这一步同样会输出详细日志,可以仔细观察进度,最后不要忘记升级之前先进入维护模式,升级完成后重新安装 kubelet 并关闭节点维护模式。

四、升级 Node

node 节点的升级实际上在升级完 master 节点以后不需要什么特殊操作,node 节点唯一需要升级的就是 kubelet 组件;首先在 node 节点执行 kubeadm upgrade node 命令,该命令会拉取集群内的 kubelet 配置文件,然后重新安装 kubelet 重启既可;同样升级 node 节点时不要忘记开启维护模式。针对于 CNI 组件请按需手动升级,并且确认好 CNI 组件的兼容版本。

五、验证集群

所有组件升级完成后,可以通过 kubectl describe POD_NAME 的方式验证 master 组件是否都升级到了最新版本;通过 kuebctl version 命令验证 api 相关信息(HA rr 轮训模式下可以多执行几遍);还有就是通过 kubectl get node -o wide 查看相关 node 的信息,确保 kubelet 都升级成功,同时全部节点维护模式都已经关闭,其他细节可以参考官方文档

]]>
Kubernetes Kubernetes http://mritd.com/2020/01/21/how-to-upgrade-kubeadm-cluster/#disqus_thread
kubeadm 搭建 HA kubernetes 集群 http://mritd.com/2020/01/21/set-up-kubernetes-ha-cluster-by-kubeadm/ http://mritd.com/2020/01/21/set-up-kubernetes-ha-cluster-by-kubeadm/ Tue, 21 Jan 2020 04:40:36 GMT 距离上一次折腾 kubeadm 大约已经一两年了(记不太清了),在很久一段时间内一直采用二进制部署的方式来部署 kubernetes 集群,随着 kubeadm 的不断稳定,目前终于可以重新试试这个不错的工具了 一、环境准备

搭建环境为 5 台虚拟机,每台虚拟机配置为 4 核心 8G 内存,虚拟机 IP 范围为 172.16.10.21~25,其他软件配置如下

  • os version: ubuntu 18.04
  • kubeadm version: 1.17.0
  • kubernetes version: 1.17.0
  • etcd version: 3.3.18
  • docker version: 19.03.5

二、HA 方案

目前的 HA 方案与官方的不同,官方 HA 方案推荐使用类似 haproxy 等工具进行 4 层代理 apiserver,但是同样会有一个问题就是我们还需要对这个 haproxy 做 HA;由于目前我们实际生产环境都是多个独立的小集群,所以单独弄 2 台 haproxy + keeplived 去维持这个 apiserver LB 的 HA 有点不划算;所以还是准备延续老的 HA 方案,将外部 apiserver 的 4 层 LB 前置到每个 node 节点上;目前是采用在每个 node 节点上部署 nginx 4 层代理所有 apiserver,nginx 本身资源消耗低而且请求量不大,综合来说对宿主机影响很小;以下为 HA 的大致方案图

ha

三、环境初始化

3.1、系统环境

由于个人操作习惯原因,目前已经将常用的初始化环境整理到一个小脚本里了,脚本具体参见 mritd/shell_scripts 仓库,基本上常用的初始化内容为:

  • 设置 locale(en_US.utf-8)
  • 设置时区(Asia/Shanghai)
  • 更新所有系统软件包(system update)
  • 配置 vim(vim8 + 常用插件、配色)
  • ohmyzsh(别跟我说不兼容 bash 脚本,我就是喜欢)
  • docker
  • ctop(一个 docker 的辅助工具)
  • docker-compose

在以上初始化中,实际对 kubernetes 安装产生影响的主要有三个地方:

  • docker 的 cgroup driver 调整为 systemd,具体参考 docker.service
  • docker 一定要限制 conatiner 日志大小,防止 apiserver 等日志大量输出导致磁盘占用过大
  • 安装 conntrackipvsadm,后面可能需要借助其排查问题

3.2、配置 ipvs

由于后面 kube-proxy 需要使用 ipvs 模式,所以需要对内核参数、模块做一些调整,调整命令如下:

cat >> /etc/sysctl.conf <<EOFnet.ipv4.ip_forward=1net.bridge.bridge-nf-call-iptables=1net.bridge.bridge-nf-call-ip6tables=1EOFsysctl -pcat >> /etc/modules <<EOFip_vsip_vs_lcip_vs_wlcip_vs_rrip_vs_wrrip_vs_lblcip_vs_lblcrip_vs_dhip_vs_ship_vs_foip_vs_nqip_vs_sedip_vs_ftpEOF

配置完成后切记需要重启,重启完成后使用 lsmod | grep ip_vs 验证相关 ipvs 模块加载是否正常,本文将主要使用 ip_vs_wrr,所以目前只关注这个模块既可。

ipvs_mode

四、安装 Etcd

4.1、方案选择

官方对于集群 HA 给出了两种有关于 Etcd 的部署方案:

  • 一种是深度耦合到 control plane 上,即每个 control plane 一个 etcd
  • 另一种是使用外部的 Etcd 集群,通过在配置中指定外部集群让 apiserver 等组件连接

在测试深度耦合 control plane 方案后,发现一些比较恶心的问题;比如说开始创建第二个 control plane 时配置写错了需要重建,此时你一旦删除第二个 control plane 会导致第一个 control plane 也会失败,原因是创建第二个 control plane 时 kubeadm 已经自动完成了 etcd 的集群模式,当删除第二个 control plane 的时候由于集群可用原因会导致第一个 control plane 下的 etcd 发现节点失联从而也不提供服务;所以综合考虑到后续迁移、灾备等因素,这里选择了将 etcd 放置在外部集群中;同样也方便我以后各种折腾应对一些极端情况啥的。

4.2、部署 Etcd

确定了需要在外部部署 etcd 集群后,只需要开干就完事了;查了一下 ubuntu 官方源已经有了 etcd 安装包,但是版本比较老,测试了一下 golang 的 build 版本是 1.10;所以我还是选择了从官方 release 下载最新的版本安装;当然最后还是因为懒,我自己打了一个 deb 包… deb 包可以从这个项目 mritd/etcd-deb 下载,担心安全性的可以利用项目脚本自己打包,以下是安装过程:

# 下载软件包wget http://github.com/mritd/etcd-deb/releases/download/v3.3.18/etcd_3.3.18_amd64.debwget http://github.com/mritd/etcd-deb/releases/download/v3.3.18/cfssl_1.4.1_amd64.deb# 安装 etcd(至少在 3 台节点上执行)dpkg -i etcd_3.3.18_amd64.deb cfssl_1.4.1_amd64.deb

既然自己部署 etcd,那么证书签署啥的还得自己来了,证书签署这里借助 cfssl 工具,cfssl 目前提供了 deb 的 make target,但是没找到 deb 包,所以也自己 build 了(担心安全性的可自行去官方下载);接着编辑一下 /etc/etcd/cfssl/etcd-csr.json 文件,用 /etc/etcd/cfssl/create.sh 脚本创建证书,并将证书复制到指定目录

# 创建证书cd /etc/etcd/cfssl && ./create.sh# 复制证书mv /etc/etcd/cfssl/*.pem /etc/etcd/ssl

最后在 3 台节点上修改配置,并将刚刚创建的证书同步到其他两台节点启动既可;下面是单台节点的配置样例

# /etc/etcd/etcd.conf# [member]ETCD_NAME=etcd1ETCD_DATA_DIR="/var/lib/etcd/data"ETCD_WAL_DIR="/var/lib/etcd/wal"ETCD_SNAPSHOT_COUNT="100"ETCD_HEARTBEAT_INTERVAL="100"ETCD_ELECTION_TIMEOUT="1000"ETCD_LISTEN_PEER_URLS="http://172.16.10.21:2380"ETCD_LISTEN_CLIENT_URLS="http://172.16.10.21:2379,http://127.0.0.1:2379"ETCD_MAX_SNAPSHOTS="5"ETCD_MAX_WALS="5"#ETCD_CORS=""# [cluster]ETCD_INITIAL_ADVERTISE_PEER_URLS="http://172.16.10.21:2380"# if you use different ETCD_NAME (e.g. test), set ETCD_INITIAL_CLUSTER value for this name, i.e. "test=http://..."ETCD_INITIAL_CLUSTER="etcd1=http://172.16.10.21:2380,etcd2=http://172.16.10.22:2380,etcd3=http://172.16.10.23:2380"ETCD_INITIAL_CLUSTER_STATE="new"ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"ETCD_ADVERTISE_CLIENT_URLS="http://172.16.10.21:2379"#ETCD_DISCOVERY=""#ETCD_DISCOVERY_SRV=""#ETCD_DISCOVERY_FALLBACK="proxy"#ETCD_DISCOVERY_PROXY=""#ETCD_STRICT_RECONFIG_CHECK="false"ETCD_AUTO_COMPACTION_RETENTION="24"# [proxy]#ETCD_PROXY="off"#ETCD_PROXY_FAILURE_WAIT="5000"#ETCD_PROXY_REFRESH_INTERVAL="30000"#ETCD_PROXY_DIAL_TIMEOUT="1000"#ETCD_PROXY_WRITE_TIMEOUT="5000"#ETCD_PROXY_READ_TIMEOUT="0"# [security]ETCD_CERT_FILE="/etc/etcd/ssl/etcd.pem"ETCD_KEY_FILE="/etc/etcd/ssl/etcd-key.pem"ETCD_CLIENT_CERT_AUTH="true"ETCD_TRUSTED_CA_FILE="/etc/etcd/ssl/etcd-root-ca.pem"ETCD_AUTO_TLS="true"ETCD_PEER_CERT_FILE="/etc/etcd/ssl/etcd.pem"ETCD_PEER_KEY_FILE="/etc/etcd/ssl/etcd-key.pem"ETCD_PEER_CLIENT_CERT_AUTH="true"ETCD_PEER_TRUSTED_CA_FILE="/etc/etcd/ssl/etcd-root-ca.pem"ETCD_PEER_AUTO_TLS="true"# [logging]#ETCD_DEBUG="false"# examples for -log-package-levels etcdserver=WARNING,security=DEBUG#ETCD_LOG_PACKAGE_LEVELS=""# [performance]ETCD_QUOTA_BACKEND_BYTES="5368709120"ETCD_AUTO_COMPACTION_RETENTION="3"

注意: 其他两台节点请调整 ETCD_NAME 为不重复的其他名称,调整 ETCD_LISTEN_PEER_URLSETCD_LISTEN_CLIENT_URLSETCD_INITIAL_ADVERTISE_PEER_URLSETCD_ADVERTISE_CLIENT_URLS 为其他节点对应的 IP;同时生产环境请将 ETCD_INITIAL_CLUSTER_TOKEN 替换为复杂的 token

# 同步证书scp -r /etc/etcd/ssl 172.16.10.22:/etc/etcd/sslscp -r /etc/etcd/ssl 172.16.10.23:/etc/etcd/ssl# 修复权限(3台节点都要执行)chown -R etcd:etcd /etc/etcd# 最后每个节点依次启动既可systemctl start etcd

启动完成后可以通过以下命令测试是否正常

# 查看集群成员k1.node ➜ etcdctl member list3cbbaf77904c6153, started, etcd2, http://172.16.10.22:2380, http://172.16.10.22:23798eb7652b6bd99c30, started, etcd1, http://172.16.10.21:2380, http://172.16.10.21:237991f4e10726460d8c, started, etcd3, http://172.16.10.23:2380, http://172.16.10.23:2379# 检测集群健康状态k1.node ➜ etcdctl endpoint health --cacert /etc/etcd/ssl/etcd-root-ca.pem --cert /etc/etcd/ssl/etcd.pem --key /etc/etcd/ssl/etcd-key.pem --endpoints http://172.16.10.21:2379,http://172.16.10.22:2379,http://172.16.10.23:2379http://172.16.10.21:2379 is healthy: successfully committed proposal: took = 16.632246mshttp://172.16.10.23:2379 is healthy: successfully committed proposal: took = 21.122603mshttp://172.16.10.22:2379 is healthy: successfully committed proposal: took = 22.592005ms

五、部署 Kubernetes

5.1、安装 kueadm

安装 kubeadm 没什么好说的,国内被墙用阿里的源既可

apt-get install -y apt-transport-httpcurl http://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add -cat <<EOF >/etc/apt/sources.list.d/kubernetes.listdeb http://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial mainEOFapt update# ebtables、ethtool kubelet 可能会用,具体忘了,反正从官方文档上看到的apt install kubelet kubeadm kubectl ebtables ethtool -y

5.2、部署 Nginx

从上面的 HA 架构图上可以看到,为了维持 apiserver 的 HA,需要在每个机器上部署一个 nginx 做 4 层的 LB;为保证后续的 node 节点正常加入,需要首先行部署 nginx;nginx 安装同样喜欢偷懒,直接 docker 跑了…毕竟都开始 kubeadm 了,那么也没必要去纠结 docker 是否稳定的问题了;以下为 nginx 相关配置

apiserver-proxy.conf

error_log stderr notice;worker_processes auto;events {multi_accept on;use epoll;worker_connections 1024;}stream {    upstream kube_apiserver {        least_conn;        # 后端为三台 master 节点的 apiserver 地址        server 172.16.10.21:5443;        server 172.16.10.22:5443;        server 172.16.10.23:5443;    }        server {        listen        0.0.0.0:6443;        proxy_pass    kube_apiserver;        proxy_timeout 10m;        proxy_connect_timeout 1s;    }}

kube-apiserver-proxy.service

[Unit]Description=kubernetes apiserver docker wrapperWants=docker.socketAfter=docker.service[Service]User=rootPermissionsStartOnly=trueExecStart=/usr/bin/docker run -p 6443:6443 \                          -v /etc/kubernetes/apiserver-proxy.conf:/etc/nginx/nginx.conf \                          --name kube-apiserver-proxy \                          --net=host \                          --restart=on-failure:5 \                          --memory=512M \                          nginx:1.17.6-alpineExecStartPre=-/usr/bin/docker rm -f kube-apiserver-proxyExecStop=/usr/bin/docker rm -rf kube-apiserver-proxyRestart=alwaysRestartSec=15sTimeoutStartSec=30s[Install]WantedBy=multi-user.target

启动 nginx 代理(每台机器都要启动,包括 master 节点)

cp apiserver-proxy.conf /etc/kubernetescp kube-apiserver-proxy.service /lib/systemd/systemsystemctl daemon-reloadsystemctl enable kube-apiserver-proxy.service && systemctl start kube-apiserver-proxy.service

5.3、启动 control plane

5.3.1、关于 Swap

目前 kubelet 为了保证内存 limit,需要在每个节点上关闭 swap;但是说实话我看了这篇文章 In defence of swap: common misconceptions 以后还是不想关闭 swap;更确切的说其实我们生产环境比较 “富”,pod 都不 limit 内存,所以下面的部署我忽略了 swap 错误检测

5.3.2、kubeadm 配置

当前版本的 kubeadm 已经支持了完善的配置管理(当然细节部分还有待支持),以下为我目前使用的配置,相关位置已经做了注释,更具体的配置自行查阅官方文档

kubeadm.yaml

apiVersion: kubeadm.k8s.io/v1beta2kind: InitConfigurationlocalAPIEndpoint:  # 第一个 master 节点 IP  advertiseAddress: "172.16.10.21"  # 6443 留给了 nginx,apiserver 换到 5443  bindPort: 5443# 这个 token 使用以下命令生成# kubeadm alpha certs certificate-keycertificateKey: 7373f829c733b46fb78f0069f90185e0f00254381641d8d5a7c5984b2cf17cd3 ---apiVersion: kubeadm.k8s.io/v1beta2kind: ClusterConfiguration# 使用外部 etcd 配置etcd:  external:    endpoints:    - "http://172.16.10.21:2379"    - "http://172.16.10.22:2379"    - "http://172.16.10.23:2379"    caFile: "/etc/etcd/ssl/etcd-root-ca.pem"    certFile: "/etc/etcd/ssl/etcd.pem"    keyFile: "/etc/etcd/ssl/etcd-key.pem"# 网络配置networking:  serviceSubnet: "10.25.0.0/16"  podSubnet: "10.30.0.1/16"  dnsDomain: "cluster.local"kubernetesVersion: "v1.17.0"# 全局 apiserver LB 地址,由于采用了 nginx 负载,所以直接指向本地既可controlPlaneEndpoint: "127.0.0.1:6443"apiServer:  # apiserver 的自定义扩展参数  extraArgs:    v: "4"    alsologtostderr: "true"    # 审计日志相关配置    audit-log-maxage: "20"    audit-log-maxbackup: "10"    audit-log-maxsize: "100"    audit-log-path: "/var/log/kube-audit/audit.log"    audit-policy-file: "/etc/kubernetes/audit-policy.yaml"    authorization-mode: "Node,RBAC"    event-ttl: "720h"    runtime-config: "api/all=true"    service-node-port-range: "30000-50000"    service-cluster-ip-range: "10.25.0.0/16"  # 由于自行定义了审计日志配置,所以需要将宿主机上的审计配置  # 挂载到 kube-apiserver 的 pod 容器中  extraVolumes:  - name: "audit-config"    hostPath: "/etc/kubernetes/audit-policy.yaml"    mountPath: "/etc/kubernetes/audit-policy.yaml"    readOnly: true    pathType: "File"  - name: "audit-log"    hostPath: "/var/log/kube-audit"    mountPath: "/var/log/kube-audit"    pathType: "DirectoryOrCreate"  # 这里是 apiserver 的证书地址配置  # 为了防止以后出特殊情况,我增加了一个泛域名  certSANs:  - "*.kubernetes.node"  - "172.16.10.21"  - "172.16.10.22"  - "172.16.10.23"  timeoutForControlPlane: 5mcontrollerManager:  extraArgs:    v: "4"    # 宿主机 ip 掩码    node-cidr-mask-size: "19"    deployment-controller-sync-period: "10s"    experimental-cluster-signing-duration: "87600h"    node-monitor-grace-period: "20s"    pod-eviction-timeout: "2m"    terminated-pod-gc-threshold: "30"scheduler:  extraArgs:    v: "4"certificatesDir: "/etc/kubernetes/pki"# gcr.io 被墙,换成微软的镜像地址imageRepository: "gcr.azk8s.cn/google_containers"clusterName: "kuberentes"---apiVersion: kubelet.config.k8s.io/v1beta1kind: KubeletConfiguration# kubelet specific options here# 此配置保证了 kubelet 能在 swap 开启的情况下启动failSwapOn: falsenodeStatusUpdateFrequency: 5s# 一些驱逐阀值,具体自行查文档修改evictionSoft:  "imagefs.available": "15%"  "memory.available": "512Mi"  "nodefs.available": "15%"  "nodefs.inodesFree": "10%"evictionSoftGracePeriod:  "imagefs.available": "3m"  "memory.available": "1m"  "nodefs.available": "3m"  "nodefs.inodesFree": "1m"evictionHard:  "imagefs.available": "10%"  "memory.available": "256Mi"  "nodefs.available": "10%"  "nodefs.inodesFree": "5%"evictionMaxPodGracePeriod: 30imageGCLowThresholdPercent: 70imageGCHighThresholdPercent: 80kubeReserved:  "cpu": "500m"  "memory": "512Mi"  "ephemeral-storage": "1Gi"rotateCertificates: true---apiVersion: kubeproxy.config.k8s.io/v1alpha1kind: KubeProxyConfiguration# kube-proxy specific options hereclusterCIDR: "10.30.0.1/16"# 启用 ipvs 模式mode: "ipvs"ipvs:  minSyncPeriod: 5s  syncPeriod: 5s  # ipvs 负载策略  scheduler: "wrr"

关于这个配置配置文件的文档还是很不完善,对于不懂 golang 的人来说很难知道具体怎么配置,以下做一下简要说明(请确保你已经拉取了 kubernetes 源码和安装了 Goland)

kubeadm 配置中每个配置段都会有个 kind 字段,kind 实际上对应了 go 代码中的 struct 结构体;同时从 apiVersion 字段中能够看到具体的版本,比如 v1alpha1 等;有了这两个信息事实上你就可以直接在源码中去找到对应的结构体

struct_search

在结构体中所有的配置便可以一目了然

struct_detail

关于数据类型,如果是 string 的类型,那么意味着你要在 yaml 里写 "xxxx" 带引号这种,当然有些时候不写能兼容,有些时候不行比如 extraArgs 字段是一个 map[string]string 如果 value 不带引号就报错;如果数据类型为 metav1.Duration(实际上就是 time.Duration),那么你看着它是个 int64 但实际上你要写 1h2m3s 这种人类可读的格式,这是 go 的特色…

audit-policy.yaml

# Log all requests at the Metadata level.apiVersion: audit.k8s.io/v1kind: Policyrules:- level: Metadata

可能 Metadata 级别的审计日志比较多,想自行调整审计日志级别的可以参考官方文档

5.3.3、拉起 control plane

有了完整的 kubeadm.yamlaudit-policy.yaml 配置后,直接一条命令拉起 control plane 既可

# 先将审计配置放到目标位置(3 台 master 都要执行)cp audit-policy.yaml /etc/kubernetes# 拉起 control planekubeadm init --config kubeadm.yaml --upload-certs --ignore-preflight-errors=Swap

control plane 拉起以后注意要保存屏幕输出,方便后续添加其他集群节点

Your Kubernetes control-plane has initialized successfully!To start using your cluster, you need to run the following as a regular user:  mkdir -p $HOME/.kube  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config  sudo chown $(id -u):$(id -g) $HOME/.kube/configYou should now deploy a pod network to the cluster.Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:  http://kubernetes.io/docs/concepts/cluster-administration/addons/You can now join any number of the control-plane node running the following command on each as root:  kubeadm join 127.0.0.1:6443 --token r4t3l3.14mmuivm7xbtaeoj \    --discovery-token-ca-cert-hash sha256:06f49f1f29d08b797fbf04d87b9b0fd6095a4693e9b1d59c429745cfa082b31d \    --control-plane --certificate-key 7373f829c733b46fb78f0069f90185e0f00254381641d8d5a7c5984b2cf17cd3Please note that the certificate-key gives access to cluster sensitive data, keep it secret!As a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use"kubeadm init phase upload-certs --upload-certs" to reload certs afterward.Then you can join any number of worker nodes by running the following on each as root:kubeadm join 127.0.0.1:6443 --token r4t3l3.14mmuivm7xbtaeoj \    --discovery-token-ca-cert-hash sha256:06f49f1f29d08b797fbf04d87b9b0fd6095a4693e9b1d59c429745cfa082b31d

根据屏幕提示配置 kubectl

mkdir -p $HOME/.kubesudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/configsudo chown $(id -u):$(id -g) $HOME/.kube/config

5.4、部署 CNI

关于网络插件的选择,以前一直喜欢 Calico,因为其性能确实好;到后来 flannel 出了 host-gw 以后现在两者性能也差不多了;但是 flannel 好处是一个工具通吃所有环境(云环境+裸机2层直通),坏处是 flannel 缺乏比较好的策略管理(当然可以使用两者结合的 Canal);后来思来想去其实我们生产倒是很少需要策略管理,所以这回怂回到 flannel 了(逃…)

Flannel 部署非常简单,根据官方文档下载配置,根据情况调整 backend 和 pod 的 CIDR,然后 apply 一下既可

# 下载配置文件wget http://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml# 调整 backend 为 host-gw(测试环境 2 层直连)k1.node ➜  grep -A 35 ConfigMap kube-flannel.ymlkind: ConfigMapapiVersion: v1metadata:  name: kube-flannel-cfg  namespace: kube-system  labels:    tier: node    app: flanneldata:  cni-conf.json: |    {      "name": "cbr0",      "cniVersion": "0.3.1",      "plugins": [        {          "type": "flannel",          "delegate": {            "hairpinMode": true,            "isDefaultGateway": true          }        },        {          "type": "portmap",          "capabilities": {            "portMappings": true          }        }      ]    }  net-conf.json: |    {      "Network": "10.30.0.0/16",      "Backend": {        "Type": "host-gw"      }    }# 调整完成后 apply 一下kubectl apply -f kube-flannel.yml

5.5、启动其他 control plane

为了保证 HA 架构,还需要在另外两台 master 上启动 control plane;在启动之前请确保另外两台 master 节点节点上 /etc/kubernetes/audit-policy.yaml 审计配置已经分发完成,确保 127.0.0.1:6443 上监听的 4 层 LB 工作正常(可尝试使用 curl -k http://127.0.0.1:6443 测试);根据第一个 control plane 终端输出,其他 control plane 加入命令如下

kubeadm join 127.0.0.1:6443 --token r4t3l3.14mmuivm7xbtaeoj \    --discovery-token-ca-cert-hash sha256:06f49f1f29d08b797fbf04d87b9b0fd6095a4693e9b1d59c429745cfa082b31d \    --control-plane --certificate-key 7373f829c733b46fb78f0069f90185e0f00254381641d8d5a7c5984b2cf17cd3

由于在使用 kubeadm join 时相关选项(--discovery-token-ca-cert-hash--control-plane)无法与 --config 一起使用,这也就意味着我们必须增加一些附加指令来提供 kubeadm.yaml 配置文件中的一些属性;最终完整的 control plane 加入命令如下,在其他 master 直接执行既可(--apiserver-advertise-address 的 IP 地址是目标 master 的 IP)

kubeadm join 127.0.0.1:6443 --token r4t3l3.14mmuivm7xbtaeoj \    --discovery-token-ca-cert-hash sha256:06f49f1f29d08b797fbf04d87b9b0fd6095a4693e9b1d59c429745cfa082b31d \    --control-plane --certificate-key 7373f829c733b46fb78f0069f90185e0f00254381641d8d5a7c5984b2cf17cd3 \    --apiserver-advertise-address 172.16.10.22 \    --apiserver-bind-port 5443 \    --ignore-preflight-errors=Swap

所有 control plane 启动完成后应当通过在每个节点上运行 kubectl get cs 验证各个组件运行状态

k2.node ➜ kubectl get csNAME                 STATUS    MESSAGE             ERRORscheduler            Healthy   okcontroller-manager   Healthy   oketcd-1               Healthy   {"health":"true"}etcd-0               Healthy   {"health":"true"}etcd-2               Healthy   {"health":"true"}k2.node ➜ kubectl get node -o wideNAME      STATUS   ROLES    AGE   VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIMEk1.node   Ready    master   28m   v1.17.0   172.16.10.21   <none>        Ubuntu 18.04.3 LTS   4.15.0-74-generic   docker://19.3.5k2.node   Ready    master   10m   v1.17.0   172.16.10.22   <none>        Ubuntu 18.04.3 LTS   4.15.0-74-generic   docker://19.3.5k3.node   Ready    master   3m    v1.17.0   172.16.10.23   <none>        Ubuntu 18.04.3 LTS   4.15.0-74-generic   docker://19.3.5

5.6、启动 Node

node 节点的启动相较于 master 来说要简单得多,只需要增加一个防止 swap 开启拒绝启动的参数既可

kubeadm join 127.0.0.1:6443 --token r4t3l3.14mmuivm7xbtaeoj \    --discovery-token-ca-cert-hash sha256:06f49f1f29d08b797fbf04d87b9b0fd6095a4693e9b1d59c429745cfa082b31d \    --ignore-preflight-errors=Swap

启动成功后在 master 上可以看到所有 node 信息

k1.node ➜ kubectl get node -o wideNAME      STATUS   ROLES    AGE     VERSION   INTERNAL-IP    EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIMEk1.node   Ready    master   32m     v1.17.0   172.16.10.21   <none>        Ubuntu 18.04.3 LTS   4.15.0-74-generic   docker://19.3.5k2.node   Ready    master   14m     v1.17.0   172.16.10.22   <none>        Ubuntu 18.04.3 LTS   4.15.0-74-generic   docker://19.3.5k3.node   Ready    master   6m35s   v1.17.0   172.16.10.23   <none>        Ubuntu 18.04.3 LTS   4.15.0-74-generic   docker://19.3.5k4.node   Ready    <none>   72s     v1.17.0   172.16.10.24   <none>        Ubuntu 18.04.3 LTS   4.15.0-74-generic   docker://19.3.5k5.node   Ready    <none>   66s     v1.17.0   172.16.10.25   <none>        Ubuntu 18.04.3 LTS   4.15.0-74-generic   docker://19.3.5

5.7、调整及测试

集群搭建好以后,如果想让 master 节点也参与调度任务,需要在任意一台 master 节点执行以下命令

# node 节点报错属于正常情况k1.node ➜ kubectl taint nodes --all node-role.kubernetes.io/master-node/k1.node untaintednode/k2.node untaintednode/k3.node untaintedtaint "node-role.kubernetes.io/master" not foundtaint "node-role.kubernetes.io/master" not found

最后创建一个 deployment 和一个 service,并在不同主机上 ping pod IP 测试网络联通性,在 pod 内直接 curl service 名称测试 dns 解析既可

test-nginx.deploy.yaml

apiVersion: apps/v1kind: Deploymentmetadata:  name: test-nginx  labels:    app: test-nginxspec:  replicas: 3  selector:    matchLabels:      app: test-nginx  template:    metadata:      labels:        app: test-nginx    spec:      containers:      - name: test-nginx        image: nginx:1.17.6-alpine        ports:        - containerPort: 80

test-nginx.svc.yaml

apiVersion: v1kind: Servicemetadata:  name: test-nginxspec:  selector:    app: test-nginx  ports:    - protocol: TCP      port: 80      targetPort: 80

六、后续处理

说实话使用 kubeadm 后,我更关注的是集群后续的扩展性调整是否能达到目标;搭建其实很简单,大部份时间都在测试后续调整上

6.1、Etcd 迁移

由于我们采用的是外部的 Etcd,所以迁移起来比较简单怎么折腾都行;需要注意的是换 IP 的时候注意保证老的 3 个节点至少有一个可用,否则可能导致集群崩溃;调整完成后记得分发相关 Etcd 节点的证书,重启时顺序一个一个重启,不要并行操作

6.2、Master 配置修改

如果需要修改 conrol plane 上 apiserver、scheduler 等配置,直接修改 kubeadm.yaml 配置文件(所以集群搭建好后务必保存好),然后执行 kubeadm upgrade apply --config kubeadm.yaml 升级集群既可,升级前一定作好相关备份工作;我只在测试环境测试这个命令工作还可以,生产环境还是需要谨慎

6.3、证书续期

目前根据我测试的结果,controller manager 的 experimental-cluster-signing-duration 参数在 init 的签发证书阶段似乎并未生效;目前根据文档描述 kubelet client 的证书会自动滚动,其他证书默认 1 年有效期,需要自己使用命令续签;续签命令如下

# 查看证书过期时间k1.node ➜ kubeadm alpha certs check-expiration[check-expiration] Reading configuration from the cluster...[check-expiration] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'CERTIFICATE                EXPIRES                  RESIDUAL TIME   CERTIFICATE AUTHORITY   EXTERNALLY MANAGEDadmin.conf                 Jan 11, 2021 10:06 UTC   364d                                    noapiserver                  Jan 11, 2021 10:06 UTC   364d            ca                      noapiserver-kubelet-client   Jan 11, 2021 10:06 UTC   364d            ca                      nocontroller-manager.conf    Jan 11, 2021 10:06 UTC   364d                                    nofront-proxy-client         Jan 11, 2021 10:06 UTC   364d            front-proxy-ca          noscheduler.conf             Jan 11, 2021 10:06 UTC   364d                                    noCERTIFICATE AUTHORITY   EXPIRES                  RESIDUAL TIME   EXTERNALLY MANAGEDca                      Jan 09, 2030 10:06 UTC   9y              nofront-proxy-ca          Jan 09, 2030 10:06 UTC   9y              no# 续签证书k1.node ➜ kubeadm alpha certs renew all[renew] Reading configuration from the cluster...[renew] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'certificate embedded in the kubeconfig file for the admin to use and for kubeadm itself renewedcertificate for serving the Kubernetes API renewedcertificate for the API server to connect to kubelet renewedcertificate embedded in the kubeconfig file for the controller manager to use renewedcertificate for the front proxy client renewedcertificate embedded in the kubeconfig file for the scheduler manager to use renewed

6.4、Node 重加入

默认的 bootstrap token 会在 24h 后失效,所以后续增加新节点需要重新创建 token,重新创建 token 可以通过以下命令完成

# 列出 tokenk1.node ➜ kubeadm token listTOKEN                     TTL         EXPIRES                     USAGES                   DESCRIPTION                                                EXTRA GROUPSr4t3l3.14mmuivm7xbtaeoj   22h         2020-01-13T18:06:54+08:00   authentication,signing   <none>                                                     system:bootstrappers:kubeadm:default-node-tokenzady4i.57f9i2o6zl9vf9hy   45m         2020-01-12T20:06:53+08:00   <none>                   Proxy for managing TTL for the kubeadm-certs secret        <none># 创建新 tokenk1.node ➜ kubeadm token create --print-join-commandW0112 19:21:15.174765   26626 validation.go:28] Cannot validate kube-proxy config - no validator is availableW0112 19:21:15.174836   26626 validation.go:28] Cannot validate kubelet config - no validator is availablekubeadm join 127.0.0.1:6443 --token 2dz4dc.mobzgjbvu0bkxz7j     --discovery-token-ca-cert-hash sha256:06f49f1f29d08b797fbf04d87b9b0fd6095a4693e9b1d59c429745cfa082b31d

如果忘记了 certificate-key 可以通过一下命令重新 upload 并查看

k1.node ➜ kubeadm init --config kubeadm.yaml phase upload-certs --upload-certsW0112 19:23:06.466711   28637 validation.go:28] Cannot validate kubelet config - no validator is availableW0112 19:23:06.466778   28637 validation.go:28] Cannot validate kube-proxy config - no validator is available[upload-certs] Storing the certificates in Secret "kubeadm-certs" in the "kube-system" Namespace[upload-certs] Using certificate key:7373f829c733b46fb78f0069f90185e0f00254381641d8d5a7c5984b2cf17cd3

6.5、调整 kubelet

node 节点一旦启动完成后,kubelet 配置便不可再修改;如果想要修改 kubelet 配置,可以通过调整 /etc/systemd/system/kubelet.service.d/10-kubeadm.conf 配置文件完成

七、其他

本文参考了许多官方文档,以下是一些个人认为比较有价值并且在使用 kubeadm 后应该阅读的文档

]]>
Kubernetes Kubernetes http://mritd.com/2020/01/21/set-up-kubernetes-ha-cluster-by-kubeadm/#disqus_thread
云服务器下 Ubuntu 18 正确的 DNS 修改 http://mritd.com/2020/01/21/how-to-modify-dns-on-ubuntu18-server/ http://mritd.com/2020/01/21/how-to-modify-dns-on-ubuntu18-server/ Tue, 21 Jan 2020 04:35:46 GMT 博客服务器换成了阿里云香港,个人还偶尔看美剧,所以做了一下 Netflix 分流;分流过程主要是做 DNS 解析 SNI 代理,调了半天记录一下 一、起因

Netflix DNS 分流实际上我目前的方案是通过 CoreDNS 作为主 DNS Server,然后在 CoreDNS 上针对 Netflix 全部域名解析 forward 到一台国外可以解锁 Netflix 机器上;如果直接将 CoreDNS 暴露在公网,那么无疑是在作死,为 DNS 反射 DDos 提供肉鸡;所以想到的方案是自己编写一个不可描述的工具,本地 Client 到 Server 端以后,Server 端再去设置到 CoreDNS 做分流;其中不可避免的需要调整 Server 端默认 DNS。

二、已废弃修改方式

目前大部份人还是习惯修改 /etc/resolv.conf 配置文件,这个配置文件上面已经明确标注了不要去修改它;因为自 Systemd 一统江山以后,系统 DNS 已经被 systemd-resolved 服务接管;一但修改了 /etc/resolv.conf,机器重启后就会被恢复;所以根源解决方案还是需要修改 systemd-resolved 的配置。

三、netplan 的调整

在调整完 systemd-resolved 配置后其实有些地方仍然是不生效的;原因是 Ubuntu 18 开始网络已经被 netplan 接管,所以问题又回到了如何修改 netplan;由于云服务器初始化全部是由 cloud-init 完成的,netplan 配置里 IP 全部是由 DHCP 完成;那么直接修改 netplan 为 static IP 理论上可行,但是事实上还是不够优雅;后来研究了一下其实更优雅的方式是覆盖掉 DHCP 的某些配置,比如 DNS 配置;在阿里云上配置如下(/etc/netplan/99-netcfg.yaml)

network:  version: 2  renderer: networkd  ethernets:    eth0:      dhcp4: yes      dhcp4-overrides:        use-dns: no      dhcp6: no      nameservers:        search: [local,node]        # 我自己的 CoreDNS 服务器        addresses: [172.17.3.17]

修改完成后执行 netplan try 等待几秒钟,如果屏幕的读秒倒计时一直在动,说明修改没问题,接着回车既可(尽量不要 netplan apply,一旦修改错误你就再也连不上了…)

四、DNS 分流

顺便贴一下 CoreDNS 配置吧,可能有些人也需要;第一部分的域名是目前我整理的 Netflix 全部访问域名,针对这些域名的流量转发到自己其他可解锁 Netflix 的机器既可

netflix.com nflxext.com nflximg.net nflxso.net nflxvideo.net {    bind 172.17.3.17    cache 30 . {        success 4096    }    forward . 158.1.1.1 {        max_fails 2        prefer_udp        expire 20s        policy random        health_check 0.2s    }    errors    log . "{remote}:{port} - {>id} \"{type} {class} {name} {proto} {size} {>do} {>bufsize}\" {rcode} {>rflags} {rsize} {duration}"}.:53 {    bind 172.17.3.17    cache 30 . {        success 4096    }    forward . 8.8.8.8 1.1.1.1 {        except netflix.com nflxext.com nflximg.net nflxso.net nflxvideo.net        max_fails 2        expire 20s        policy random        health_check 0.2s    }    errors    log . "{remote}:{port} - {>id} \"{type} {class} {name} {proto} {size} {>do} {>bufsize}\" {rcode} {>rflags} {rsize} {duration}"

五、关于 docker

当 netplan 修改完成后,只需要重启 docker 既可保证 docker 内所有容器 DNS 请求全部发送到自己定义的 DNS 服务器上;请不要尝试将自己的 CoreDNS 监听到 127.* 或者 ::1 上,这两个地址会导致 docker 中的 DNS 无效,因为在 libnetwork 中针对这两个地址做了过滤,并且 FilterResolvDNS 方法在剔除这两种地址时不会给予任何警告日志

]]>
Linux Linux http://mritd.com/2020/01/21/how-to-modify-dns-on-ubuntu18-server/#disqus_thread
Percona MySQL 搭建 http://mritd.com/2020/01/20/set-up-percona-server/ http://mritd.com/2020/01/20/set-up-percona-server/ Mon, 20 Jan 2020 11:45:46 GMT 最近被拉去折腾 MySQL 了,Kuberntes 相关的文章停更了好久... MySQL 折腾完了顺便记录一下折腾过程,值得注意的是本篇文章从实际生产环境文档中摘录,部分日志和数据库敏感信息已被胡乱替换,所以不要盲目复制粘贴。 一、版本信息

目前采用 MySQL fork 版本 Percona Server 5.7.28,监控方面选择 Percona Monitoring and Management 2.1.0,对应监控 Client 版本为 2.1.0

二、Percona Server 安装

为保证兼容以及稳定性,MySQL 宿主机系统选择 CentOS 7,Percona Server 安装方式为 rpm 包,安装后由 Systemd 守护

2.1、下载安装包

安装包下载地址为 http://www.percona.com/downloads/Percona-Server-5.7/LATEST/,下载时选择 Download All Packages Together,下载后是所有组件全量的压缩 tar 包。

2.2、安装前准备

针对 CentOS 7 系统,安装前升级所有系统组件库,执行 yum update 既可;大部份 CentOS 7 安装后可能会附带 mariadb-libs 包,这个包会默认创建一些配置文件,导致后面的 Percona Server 无法覆盖它(例如 /etc/my.cnf),所以安装 Percona Server 之前需要卸载它 yum remove mariadb-libs

针对于数据存储硬盘,目前统一为 SSD 硬盘,挂载点为 /data,挂载方式可以采用 fstabsystemd-mount,分区格式目前采用 xfs 格式。

SSD 优化有待补充…

2.3、安装 Percona Server

Percona Server tar 包解压后会有 9 个 rpm 包,实际安装时只需要安装其中 4 个既可

yum install Percona-Server-client-57-5.7.28-31.1.el7.x86_64.rpm Percona-Server-server-57-5.7.28-31.1.el7.x86_64.rpm Percona-Server-shared-57-5.7.28-31.1.el7.x86_64.rpm Percona-Server-shared-compat-57-5.7.28-31.1.el7.x86_64.rpm

2.4、安装后调整

2.4.1、硬盘调整

目前 MySQL 数据会统一存放到 /data 目录下,所以需要将单独的数据盘挂载到 /data 目录;如果是 SSD 硬盘还需要调整系统 I/O 调度器等其他优化。

2.4.2、目录预创建

Percona Server 安装完成后,由于配置调整原因,还会用到一些其他的数据目录,这些目录可以预先创建并授权

mkdir -p /var/log/mysql /data/mysql_tmpchown -R mysql:mysql /var/log/mysql /data/mysql_tmp

/var/log/mysql 目录用来存放 MySQL 相关的日志(不包括 binlog),/data/mysql_tmp 用来存放 MySQL 运行时产生的缓存文件。

2.4.3、文件描述符调整

由于 rpm 安装的 Percona Server 会采用 Systemd 守护,所以如果想修改文件描述符配置应当调整 Systemd 配置文件

vim /usr/lib/systemd/system/mysqld.service# Sets open_files_limit# 注意 infinity = 65536LimitCORE = infinityLimitNOFILE = infinityLimitNPROC = infinity

然后执行 systemctl daemon-reload 重载既可。

2.4.4、配置文件调整

Percona Server 安装完成后也会生成 /etc/my.cnf 配置文件,不过不建议直接修改该文件;修改配置文件需要进入到 /etc/percona-server.conf.d/ 目录调整相应配置;以下为配置样例(生产环境 mysqld 配置需要优化调整)

mysql.cnf

[mysql]auto-rehashdefault_character_set=utf8mb4

mysqld.cnf

# Percona Server template configuration[mysqld]## Remove leading # and set to the amount of RAM for the most important data# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.# innodb_buffer_pool_size = 128M## Remove leading # to turn on a very important data integrity option: logging# changes to the binary log between backups.# log_bin## Remove leading # to set options mainly useful for reporting servers.# The server defaults are faster for transactions and fast SELECTs.# Adjust sizes as needed, experiment to find the optimal values.# join_buffer_size = 128M# sort_buffer_size = 2M# read_rnd_buffer_size = 2Mport=3306datadir=/data/mysqlsocket=/data/mysql/mysql.sockpid_file=/data/mysql/mysqld.pid# 服务端编码character_set_server=utf8mb4# 服务端排序collation_server=utf8mb4_general_ci# 强制使用 utf8mb4 编码集,忽略客户端设置skip_character_set_client_handshake=1# 日志输出到文件log_output=FILE# 开启常规日志输出general_log=1# 常规日志输出文件位置general_log_file=/var/log/mysql/mysqld.log# 错误日志位置log_error=/var/log/mysql/mysqld-error.log# 记录慢查询slow_query_log=1# 慢查询时间(大于 1s 被视为慢查询)long_query_time=1# 慢查询日志文件位置slow_query_log_file=/var/log/mysql/mysqld-slow.log# 临时文件位置tmpdir=/data/mysql_tmp# 线程池缓存(refs http://my.oschina.net/realfighter/blog/363853)thread_cache_size=30# The number of open tables for all threads.(refs http://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_table_open_cache)table_open_cache=16384# 文件描述符(此处修改不生效,请修改 systemd service 配置) # refs http://www.percona.com/blog/2017/10/12/open_files_limit-mystery/# refs http://www.cnblogs.com/wxxjianchi/p/10370419.html#open_files_limit=65535# 表定义缓存(5.7 以后自动调整)# refs http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_table_definition_cache# refs http://mysql.taobao.org/monthly/2015/08/10/#table_definition_cache=16384sort_buffer_size=1Mjoin_buffer_size=1M# MyiSAM 引擎专用(内部临时磁盘表可能会用)read_buffer_size=1Mread_rnd_buffer_size=1M# MyiSAM 引擎专用(内部临时磁盘表可能会用)key_buffer_size=32M# MyiSAM 引擎专用(内部临时磁盘表可能会用)bulk_insert_buffer_size=16M# myisam_sort_buffer_size 与 sort_buffer_size 区别请参考(http://stackoverflow.com/questions/7871027/myisam-sort-buffer-size-vs-sort-buffer-size)myisam_sort_buffer_size=64M# 内部内存临时表大小tmp_table_size=32M# 用户创建的 MEMORY 表最大大小(tmp_table_size 受此值影响)max_heap_table_size=32M# 开启查询缓存query_cache_type=1# 查询缓存大小query_cache_size=32M# sql modesql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'########### Network ############ 最大连接数(该参数受到最大文件描述符影响,如果不生效请检查最大文件描述符设置)# refs http://stackoverflow.com/questions/39976756/the-max-connections-in-mysql-5-7max_connections=1500# mysql 堆栈内暂存的链接数量# 当短时间内链接数量超过 max_connections 时,部分链接会存储在堆栈内,存储数量受此参数控制back_log=256# 最大链接错误,针对于 client 主机,超过此数量的链接错误将会导致 mysql server 针对此主机执行锁定(禁止链接 ERROR 1129 )# 此错误计数仅在 mysql 链接握手失败才会计算,一般出现问题时都是网络故障# refs http://www.cnblogs.com/kerrycode/p/8405862.htmlmax_connect_errors=100000# mysql server 允许的最大数据包大小max_allowed_packet=64M# 交互式客户端链接超时(30分钟自动断开)interactive_timeout=1800# 非交互式链接超时时间(10分钟)# 如果客户端有连接池,则需要协商此参数(refs http://database.51cto.com/art/201909/603519.htm)wait_timeout=600# 跳过外部文件系统锁定# If you run multiple servers that use the same database directory (not recommended), # each server must have external locking enabled.# refs http://dev.mysql.com/doc/refman/5.7/en/external-locking.htmlskip_external_locking=1# 跳过链接的域名解析(开启此选项后 mysql 用户授权的 host 方式失效)skip_name_resolve=0# 禁用主机名缓存,每次都会走 DNShost_cache_size=0########### REPL ############ 开启 binloglog_bin=mysql-bin# 作为从库时,同步信息依然写入 binlog,方便此从库再作为其他从库的主库log_slave_updates=1# server id,默认为 ipv4 地址去除第一段# eg: 172.16.10.11 => 161011server_id=161011# 每次次事务 binlog 刷新到磁盘# refs http://liyangliang.me/posts/2014/03/innodb_flush_log_at_trx_commit-and-sync_binlog/sync_binlog=100# binlog 格式(refs http://zhuanlan.zhihu.com/p/33504555)binlog_format=row# binlog 自动清理时间expire_logs_days=10# 开启 relay-log,一般作为 slave 时开启relay_log=mysql-replay# 主从复制时跳过 test 库replicate_ignore_db=test# 每个 session binlog 缓存binlog_cache_size=4M# binlog 滚动大小max_binlog_size=1024M# GTID 相关(refs http://keithlan.github.io/2016/06/23/gtid/)#gtid_mode=1#enforce_gtid_consistency=1########### InnoDB ############ 永久表默认存储引擎default_storage_engine=InnoDB# 系统表空间数据文件大小(初始化为 1G,并且自动增长)innodb_data_file_path=ibdata1:1G:autoextend# InnoDB 缓存池大小# innodb_buffer_pool_size 必须等于 innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances,或者是其整数倍# refs http://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool-resize.html# refs http://zhuanlan.zhihu.com/p/60089484innodb_buffer_pool_size=7680Minnodb_buffer_pool_instances=10innodb_buffer_pool_chunk_size=128M# InnoDB 强制恢复(refs http://www.askmaclean.com/archives/mysql-innodb-innodb_force_recovery.html)innodb_force_recovery=0# InnoDB buffer 预热(refs http://www.dbhelp.net/2017/01/12/mysql-innodb-buffer-pool-warmup.html)innodb_buffer_pool_dump_at_shutdown=1innodb_buffer_pool_load_at_startup=1# InnoDB 日志组中的日志文件数innodb_log_files_in_group=2# InnoDB redo 日志大小# refs http://www.percona.com/blog/2017/10/18/chose-mysql-innodb_log_file_size/innodb_log_file_size=256MB# 缓存还未提交的事务的缓冲区大小innodb_log_buffer_size=16M# InnoDB 在事务提交后的日志写入频率# refs http://liyangliang.me/posts/2014/03/innodb_flush_log_at_trx_commit-and-sync_binlog/innodb_flush_log_at_trx_commit=2# InnoDB DML 操作行级锁等待时间# 超时返回 ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction# refs http://ningyu1.github.io/site/post/75-mysql-lock-wait-timeout-exceeded/innodb_lock_wait_timeout=30# InnoDB 行级锁超时是否回滚整个事务,默认为 OFF 仅回滚上一条语句# 此时应用程序可以接受到错误后选择是否继续提交事务(并没有违反 ACID 原子性)# refs http://www.cnblogs.com/hustcat/archive/2012/11/18/2775487.html#innodb_rollback_on_timeout=ON# InnoDB 数据写入磁盘的方式,具体见博客文章# refs http://www.cnblogs.com/gomysql/p/3595806.htmlinnodb_flush_method=O_DIRECT# InnoDB 缓冲池脏页刷新百分比# refs http://dbarobin.com/2015/08/29/mysql-optimization-under-ssdinnodb_max_dirty_pages_pct=50# InnoDB 每秒执行的写IO量# refs http://www.centos.bz/2016/11/mysql-performance-tuning-15-config-item/#10.INNODB_IO_CAPACITY,%20INNODB_IO_CAPACITY_MAXinnodb_io_capacity=500innodb_io_capacity_max=1000# 请求并发 InnoDB 线程数# refs http://www.cnblogs.com/xinysu/p/6439715.html#_lab2_1_0innodb_thread_concurrency=60# 再使用多个 InnoDB 表空间时,允许打开的最大 ".ibd" 文件个数,不设置默认 300,# 并且取与 table_open_cache 相比较大的一个,此选项独立于 open_files_limit# refs http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_open_filesinnodb_open_files=65535# 每个 InnoDB 表都存储在独立的表空间(.ibd)中innodb_file_per_table=1# 事务级别(可重复读,会出幻读)transaction_isolation=REPEATABLE-READ# 是否在搜索和索引扫描中使用间隙锁(gap locking),不建议使用未来将删除innodb_locks_unsafe_for_binlog=0# InnoDB 后台清理线程数,更大的值有助于 DML 执行性能,>= 5.7.8 默认为 4innodb_purge_threads=4

mysqld_safe.cnf

## The Percona Server 5.7 configuration file.## One can use all long options that the program supports.# Run program with --help to get a list of available options and with# --print-defaults to see which it would actually understand and use.## For explanations see# http://dev.mysql.com/doc/mysql/en/server-system-variables.html[mysqld_safe]pid-file = /var/run/mysqld/mysqld.pidsocket   = /var/run/mysqld/mysqld.socknice     = 0

mysqldump.cnf

[mysqldump]quickdefault-character-set=utf8mb4max_allowed_packet=256M

2.5、启动

配置文件调整完成后启动既可

systemctl start mysqld

启动完成后默认 root 密码会自动生成,通过 grep 'temporary password' /var/log/mysql/* 查看默认密码;获得默认密码后可以通过 mysqladmin -S /data/mysql/mysql.sock -u root -p password 修改 root 密码。

三、Percona Monitoring and Management

数据库创建成功后需要增加 pmm 监控,后续将会通过监控信息来调优数据库,所以数据库监控必不可少。

3.1、安装前准备

pmm 监控需要使用特定用户来监控数据信息,所以需要预先为 pmm 创建用户

USE mysql;GRANT ALL PRIVILEGES ON *.* TO 'pmm'@'%' IDENTIFIED BY 'pmm12345' WITH GRANT OPTION;FLUSH PRIVILEGES;

3.2、安装 PMM Server

pmm server 端推荐直接使用 docker 启动,以下为样例 docker compose

version: '3.7'services:  pmm:    image: percona/pmm-server:2.1.0    container_name: pmm    restart: always    volumes:      - data:/srv    ports:      - "80:80"      - "443:443"volumes:  data:

如果想要自定义证书,请将证书复制到 volume 内的 nginx 目录下,自定义证书需要以下证书文件

pmmserver.node ➜ tree.├── ca-certs.pem├── certificate.conf  # 此文件是 pmm 默认生成自签证书的配置文件,不需要关注├── certificate.crt├── certificate.key└── dhparam.pem

pmm server 启动后访问 http(s)://IP_ADDRESS 既可进入 granafa 面板,默认账户名和密码都是 admin

3.3、安装 PMM Client

PMM Client 同样采用 rpm 安装,下载地址 http://www.percona.com/downloads/pmm2/,当前采用最新的 2.1.0 版本;rpm 下载完成后直接 yum install 既可。

rpm 安装完成后使用 pmm-admin 命令配置服务端地址,并添加当前 mysql 实例监控

# 配置服务端地址pmm-admin config --server-url http://admin:admin@pmm.mysql.node 172.16.0.11 generic mysql# 配置当前 mysql 实例pmm-admin add mysql --username=pmm --password=pmm12345 mysql 172.16.0.11:3306

完成后稍等片刻既可在 pmm server 端的 granafa 中看到相关数据。

四、数据导入

从原始数据库 dump 相关库,并导入到新数据库既可

# dumpmysqldump -h 172.16.1.10 -u root -p --master-data=2 --routines --triggers --single_transaction --databases DATABASE_NAME > dump.sql# loadmysql -S /data/mysql/mysql.sock -u root -p < dump.sql

数据导入后重建业务用户既可

USE mysql;GRANT ALL PRIVILEGES ON *.* TO 'test_user'@'%' IDENTIFIED BY 'test_user' WITH GRANT OPTION;FLUSH PRIVILEGES;

五、数据备份

5.1、安装 xtrabackup

目前数据备份采用 Perconra xtrabackup 工具,xtrabackup 可以实现高速、压缩带增量的备份;xtrabackup 安装同样采用 rpm 方式,下载地址为 http://www.percona.com/downloads/Percona-XtraBackup-2.4/LATEST/,下载完成后执行 yum install 既可

5.2、备份工具

目前备份工具开源在 GitHub 上,每次全量备份会写入 .full-backup 文件,增量备份会写入 .inc-backup 文件

5.3、配置 systemd

为了使备份自动运行,目前将定时任务配置到 systemd 中,由 systemd 调度并执行;以下为相关 systemd 配置文件

mysql-backup-full.service

[Unit]Description=mysql full backupAfter=network.target[Service]Type=simpleRestart=on-failureExecStart=/usr/local/bin/mybak --backup-dir /data/mysql_backup --prefix mysql full[Install]WantedBy=multi-user.target

mysql-backup-inc.service

[Unit]Description=mysql incremental backupAfter=network.target[Service]Type=simpleRestart=on-failureExecStart=/usr/local/bin/mybak --backup-dir /data/mysql_backup --prefix mysql inc[Install]WantedBy=multi-user.target

mysql-backup-compress.service

[Unit]Description=mysql backup compressAfter=network.target[Service]Type=simpleRestart=on-failureExecStart=/usr/local/bin/mybak --backup-dir /data/mysql_backup --prefix mysql compress --clean[Install]WantedBy=multi-user.target

mysql-backup-full.timer

[Unit]Description=mysql weekly full backup# 备份之前依赖相关目录的挂载After=data.mountAfter=data-mysql_backup.mount[Timer]# 目前每周日一个全量备份OnCalendar=Sun *-*-* 3:00Persistent=true[Install]WantedBy=timers.target

mysql-backup-inc.timer

[Unit]Description=mysql weekly full backupAfter=data.mountAfter=data-mysql_backup.mount[Timer]# 每天三个增量备份OnCalendar=*-*-* 9:00OnCalendar=*-*-* 13:00OnCalendar=*-*-* 18:00Persistent=true[Install]WantedBy=timers.target

mysql-backup-compress.timer

[Unit]Description=mysql weekly backup compress# 备份之前依赖相关目录的挂载After=data.mountAfter=data-mysql_backup.mount[Timer]# 目前每周日一个全量备份,自动压缩后同时完成清理OnCalendar=Sun *-*-* 5:00Persistent=true[Install]WantedBy=timers.target

创建好相关文件后启动相关定时器既可

cp *.timer *.service /lib/systemd/systemsystemctl daemon-reloadsystemctl start mysql-backup-full.timer mysql-backup-inc.timer mysql-backup-compress.timersystemctl enable mysql-backup-full.timer mysql-backup-inc.timer mysql-backup-compress.timer

六、数据恢复

6.1、全量备份恢复

针对于全量备份,只需要按照官方文档的还原顺序进行还原既可

# 由于备份时进行了压缩,所以先解压备份文件xtrabackup --decompress --parallel 4 --target-dir /data/mysql_backup/mysql-20191205230502# 执行预处理xtrabackup --prepare --target-dir /data/mysql_backup/mysql-20191205230502# 执行恢复(恢复时自动根据 my.cnf 将数据覆盖到 data 数据目录)xtrabackup --copy-back --target-dir /data/mysql_backup/mysql-20191205230502# 修复数据目录权限chown -R mysql:mysql /data/mysql# 启动 mysqlsystemctl start mysqld

6.2、增量备份恢复

对于增量备份恢复,其与全量备份恢复的根本区别在于: 对于非最后一个增量文件的预处理必须使用 --apply-log-only 选项防止运行回滚阶段的处理

# 对所有备份文件进行解压处理for dir in `ls`; do xtrabackup --decompress --parallel 4 --target-dir $dir; done# 对全量备份文件执行预处理(注意增加 --apply-log-only 选项)xtrabackup --prepare --apply-log-only --target-dir /data/mysql_backup/mysql-20191205230502# 对非最后一个增量备份执行预处理xtrabackup --prepare --apply-log-only --target-dir /data/mysql_backup/mysql-20191205230502 --incremental-dir /data/mysql_backup/mysql-inc-20191206230802# 对最后一个增量备份执行预处理(不需要 --apply-log-only)xtrabackup --prepare --target-dir /data/mysql_backup/mysql-20191205230502 --incremental-dir /data/mysql_backup/mysql-inc-20191207031005# 执行恢复(恢复时自动根据 my.cnf 将数据覆盖到 data 数据目录)xtrabackup --copy-back --target-dir /data/mysql_backup/mysql-20191205230502# 修复数据目录权限chown -R mysql:mysql /data/mysql# 启动 mysqlsystemctl start mysqld

6.3、创建 slave

针对 xtrabackup 备份的数据可以直接恢复成 slave 节点,具体步骤如下:

首先将备份文件复制到目标机器,然后执行解压(默认备份工具采用 lz4 压缩)

xtrabackup --decompress --target-dir=xxxxxx

解压完成后执行预处理操作(在执行预处理之前请确保 slave 机器上相关配置文件与 master 相同,并且处理好数据目录存放等)

xtrabackup --user=root --password=xxxxxxx --prepare --target-dir=xxxx

预处理成功后便可执行恢复,以下命令将自动读取 my.cnf 配置,自动识别数据目录位置并将数据文件移动到该位置

xtrabackup --move-back --target-dir=xxxxx

所由准备就绪后需要进行权限修复

chown -R mysql:mysql MYSQL_DATA_DIR

最后在 mysql 内启动 slave 既可,slave 信息可通过从数据备份目录的 xtrabackup_binlog_info 中获取

# 获取备份 POS 信息cat xxxxxx/xtrabackup_binlog_info# 创建 slave 节点CHANGE MASTER TO    MASTER_HOST='192.168.2.48',    MASTER_USER='repl',    MASTER_PASSWORD='xxxxxxx',    MASTER_LOG_FILE='mysql-bin.000005',    MASTER_LOG_POS=52500595;# 启动 slavestart slave;show slave status \G;

七、生产处理

7.1、数据目录

目前生产环境数据目录位置调整到 /home/mysql,所以目录权限处理也要做对应调整

mkdir -p /var/log/mysql /home/mysql_tmpchown -R mysql:mysql /var/log/mysql /home/mysql_tmp

7.2、配置文件

生产环境目前节点配置如下

  • CPU: Intel(R) Xeon(R) CPU E5-2620 v4 @ 2.10GHz
  • RAM: 128G

所以配置文件也需要做相应的优化调整

mysql.cnf

[mysql]auto-rehashdefault_character_set=utf8mb4

mysqld.cnf

# Percona Server template configuration[mysqld]## Remove leading # and set to the amount of RAM for the most important data# cache in MySQL. Start at 70% of total RAM for dedicated server, else 10%.# innodb_buffer_pool_size = 128M## Remove leading # to turn on a very important data integrity option: logging# changes to the binary log between backups.# log_bin## Remove leading # to set options mainly useful for reporting servers.# The server defaults are faster for transactions and fast SELECTs.# Adjust sizes as needed, experiment to find the optimal values.# join_buffer_size = 128M# sort_buffer_size = 2M# read_rnd_buffer_size = 2Mport=3306datadir=/home/mysql/mysqlsocket=/home/mysql/mysql/mysql.sockpid_file=/home/mysql/mysql/mysqld.pid# 服务端编码character_set_server=utf8mb4# 服务端排序collation_server=utf8mb4_general_ci# 强制使用 utf8mb4 编码集,忽略客户端设置skip_character_set_client_handshake=1# 日志输出到文件log_output=FILE# 开启常规日志输出general_log=1# 常规日志输出文件位置general_log_file=/var/log/mysql/mysqld.log# 错误日志位置log_error=/var/log/mysql/mysqld-error.log# 记录慢查询slow_query_log=1# 慢查询时间(大于 1s 被视为慢查询)long_query_time=1# 慢查询日志文件位置slow_query_log_file=/var/log/mysql/mysqld-slow.log# 临时文件位置tmpdir=/home/mysql/mysql_tmp# The number of open tables for all threads.(refs http://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_table_open_cache)table_open_cache=16384# 文件描述符(此处修改不生效,请修改 systemd service 配置) # refs http://www.percona.com/blog/2017/10/12/open_files_limit-mystery/# refs http://www.cnblogs.com/wxxjianchi/p/10370419.html#open_files_limit=65535# 表定义缓存(5.7 以后自动调整)# refs http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_table_definition_cache# refs http://mysql.taobao.org/monthly/2015/08/10/#table_definition_cache=16384sort_buffer_size=1Mjoin_buffer_size=1M# MyiSAM 引擎专用(内部临时磁盘表可能会用)read_buffer_size=1Mread_rnd_buffer_size=1M# MyiSAM 引擎专用(内部临时磁盘表可能会用)key_buffer_size=32M# MyiSAM 引擎专用(内部临时磁盘表可能会用)bulk_insert_buffer_size=16M# myisam_sort_buffer_size 与 sort_buffer_size 区别请参考(http://stackoverflow.com/questions/7871027/myisam-sort-buffer-size-vs-sort-buffer-size)myisam_sort_buffer_size=64M# 内部内存临时表大小tmp_table_size=32M# 用户创建的 MEMORY 表最大大小(tmp_table_size 受此值影响)max_heap_table_size=32M# 开启查询缓存query_cache_type=1# 查询缓存大小query_cache_size=32M# sql modesql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'########### Network ############ 最大连接数(该参数受到最大文件描述符影响,如果不生效请检查最大文件描述符设置)# refs http://stackoverflow.com/questions/39976756/the-max-connections-in-mysql-5-7max_connections=1500# mysql 堆栈内暂存的链接数量# 当短时间内链接数量超过 max_connections 时,部分链接会存储在堆栈内,存储数量受此参数控制back_log=256# 最大链接错误,针对于 client 主机,超过此数量的链接错误将会导致 mysql server 针对此主机执行锁定(禁止链接 ERROR 1129 )# 此错误计数仅在 mysql 链接握手失败才会计算,一般出现问题时都是网络故障# refs http://www.cnblogs.com/kerrycode/p/8405862.htmlmax_connect_errors=100000# mysql server 允许的最大数据包大小max_allowed_packet=64M# 交互式客户端链接超时(30分钟自动断开)interactive_timeout=1800# 非交互式链接超时时间(10分钟)# 如果客户端有连接池,则需要协商此参数(refs http://database.51cto.com/art/201909/603519.htm)wait_timeout=28800# 跳过外部文件系统锁定# If you run multiple servers that use the same database directory (not recommended), # each server must have external locking enabled.# refs http://dev.mysql.com/doc/refman/5.7/en/external-locking.htmlskip_external_locking=1# 跳过链接的域名解析(开启此选项后 mysql 用户授权的 host 方式失效)skip_name_resolve=0# 禁用主机名缓存,每次都会走 DNShost_cache_size=0########### REPL ############ 开启 binloglog_bin=mysql-bin# 作为从库时,同步信息依然写入 binlog,方便此从库再作为其他从库的主库log_slave_updates=1# server id,默认为 ipv4 地址去除第一段# eg: 192.168.2.48 => 168248server_id=168248# 每 n 次事务 binlog 刷新到磁盘# refs http://liyangliang.me/posts/2014/03/innodb_flush_log_at_trx_commit-and-sync_binlog/sync_binlog=100# binlog 格式(refs http://zhuanlan.zhihu.com/p/33504555)binlog_format=row# binlog 自动清理时间expire_logs_days=20# 开启 relay-log,一般作为 slave 时开启relay_log=mysql-replay# 主从复制时跳过 test 库replicate_ignore_db=test# 每个 session binlog 缓存binlog_cache_size=4M# binlog 滚动大小max_binlog_size=1024M# GTID 相关(refs http://keithlan.github.io/2016/06/23/gtid/)#gtid_mode=1#enforce_gtid_consistency=1########### InnoDB ############ 永久表默认存储引擎default_storage_engine=InnoDB# 系统表空间数据文件大小(初始化为 1G,并且自动增长)innodb_data_file_path=ibdata1:1G:autoextend# InnoDB 缓存池大小(资源充足,为所欲为)# innodb_buffer_pool_size 必须等于 innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances,或者是其整数倍# refs http://dev.mysql.com/doc/refman/5.7/en/innodb-buffer-pool-resize.html# refs http://zhuanlan.zhihu.com/p/60089484innodb_buffer_pool_size=61440Minnodb_buffer_pool_instances=16# 默认 128Minnodb_buffer_pool_chunk_size=128M# InnoDB 强制恢复(refs http://www.askmaclean.com/archives/mysql-innodb-innodb_force_recovery.html)innodb_force_recovery=0# InnoDB buffer 预热(refs http://www.dbhelp.net/2017/01/12/mysql-innodb-buffer-pool-warmup.html)innodb_buffer_pool_dump_at_shutdown=1innodb_buffer_pool_load_at_startup=1# InnoDB 日志组中的日志文件数innodb_log_files_in_group=2# InnoDB redo 日志大小# refs http://www.percona.com/blog/2017/10/18/chose-mysql-innodb_log_file_size/innodb_log_file_size=256MB# 缓存还未提交的事务的缓冲区大小innodb_log_buffer_size=16M# InnoDB 在事务提交后的日志写入频率# refs http://liyangliang.me/posts/2014/03/innodb_flush_log_at_trx_commit-and-sync_binlog/innodb_flush_log_at_trx_commit=2# InnoDB DML 操作行级锁等待时间# 超时返回 ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction# refs http://ningyu1.github.io/site/post/75-mysql-lock-wait-timeout-exceeded/innodb_lock_wait_timeout=30# InnoDB 行级锁超时是否回滚整个事务,默认为 OFF 仅回滚上一条语句# 此时应用程序可以接受到错误后选择是否继续提交事务(并没有违反 ACID 原子性)# refs http://www.cnblogs.com/hustcat/archive/2012/11/18/2775487.html#innodb_rollback_on_timeout=ON# InnoDB 数据写入磁盘的方式,具体见博客文章# refs http://www.cnblogs.com/gomysql/p/3595806.htmlinnodb_flush_method=O_DIRECT# InnoDB 缓冲池脏页刷新百分比# refs http://dbarobin.com/2015/08/29/mysql-optimization-under-ssdinnodb_max_dirty_pages_pct=50# InnoDB 每秒执行的写IO量# refs http://www.centos.bz/2016/11/mysql-performance-tuning-15-config-item/#10.INNODB_IO_CAPACITY,%20INNODB_IO_CAPACITY_MAX# refs http://www.alibabacloud.com/blog/testing-io-performance-with-sysbench_594709innodb_io_capacity=8000innodb_io_capacity_max=16000# 请求并发 InnoDB 线程数# refs http://www.cnblogs.com/xinysu/p/6439715.html#_lab2_1_0innodb_thread_concurrency=0# 再使用多个 InnoDB 表空间时,允许打开的最大 ".ibd" 文件个数,不设置默认 300,# 并且取与 table_open_cache 相比较大的一个,此选项独立于 open_files_limit# refs http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_open_filesinnodb_open_files=65535# 每个 InnoDB 表都存储在独立的表空间(.ibd)中innodb_file_per_table=1# 事务级别(可重复读,会出幻读)transaction_isolation=REPEATABLE-READ# 是否在搜索和索引扫描中使用间隙锁(gap locking),不建议使用未来将删除innodb_locks_unsafe_for_binlog=0# InnoDB 后台清理线程数,更大的值有助于 DML 执行性能,>= 5.7.8 默认为 4innodb_purge_threads=4

mysqld_safe.cnf

## The Percona Server 5.7 configuration file.## One can use all long options that the program supports.# Run program with --help to get a list of available options and with# --print-defaults to see which it would actually understand and use.## For explanations see# http://dev.mysql.com/doc/mysql/en/server-system-variables.html[mysqld_safe]pid-file = /var/run/mysqld/mysqld.pidsocket   = /var/run/mysqld/mysqld.socknice     = 0

mysqldump.cnf

[mysqldump]quickdefault-character-set=utf8mb4max_allowed_packet=256M

八、常用诊断

8.1、动态配置 diff

mysql 默认允许在实例运行后使用 set global VARIABLES=VALUE 的方式动态调整一些配置,这可能导致在运行一段时间后(运维动态修改)实例运行配置和配置文件中配置不一致;所以建议定期 diff 运行时配置与配置文件配置差异,防制特殊情况下 mysql 重启后运行期配置丢失

pt-config-diff /etc/percona-server.conf.d/mysqld.cnf h=127.0.0.1 --user root --ask-pass --report-width 100Enter MySQL password:2 config differencesVariable                  /etc/percona-server.conf.d/mysqld.cnf mysql47.test.com========================= ===================================== ==================innodb_max_dirty_pages... 50                                    50.000000skip_name_resolve         0                                     ON

8.2、配置优化建议

Percona Toolkit 提供了一个诊断工具,用于对 mysql 内的配置进行扫描并给出优化建议,在初始化时可以使用此工具评估 mysql 当前配置的具体情况

pt-variable-advisor 127.0.0.1 --user root --ask-pass | grep -v '^$'Enter password: # WARN delay_key_write: MyISAM index blocks are never flushed until necessary.# WARN innodb_flush_log_at_trx_commit-1: InnoDB is not configured in strictly ACID mode.# NOTE innodb_max_dirty_pages_pct: The innodb_max_dirty_pages_pct is lower than the default.# WARN max_connections: If the server ever really has more than a thousand threads running, then the system is likely to spend more time scheduling threads than really doing useful work.# NOTE read_buffer_size-1: The read_buffer_size variable should generally be left at its default unless an expert determines it is necessary to change it.# NOTE read_rnd_buffer_size-1: The read_rnd_buffer_size variable should generally be left at its default unless an expert determines it is necessary to change it.# NOTE sort_buffer_size-1: The sort_buffer_size variable should generally be left at its default unless an expert determines it is necessary to change it.# NOTE innodb_data_file_path: Auto-extending InnoDB files can consume a lot of disk space that is very difficult to reclaim later.# WARN myisam_recover_options: myisam_recover_options should be set to some value such as BACKUP,FORCE to ensure that table corruption is noticed.# WARN sync_binlog: Binary logging is enabled, but sync_binlog isn't configured so that every transaction is flushed to the binary log for durability.

8.3、死锁诊断

使用 pt-deadlock-logger 工具可以诊断当前的死锁状态,以下为对死锁检测的测试

首先创建测试数据库和表

# 创建测试库CREATE DATABASE dbatest CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;# 切换到测试库并建立测试表USE dbatest;CREATE TABLE IF NOT EXISTS test (id INT AUTO_INCREMENT PRIMARY KEY, value VARCHAR(255), createtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP) ENGINE=INNODB;

在一个其他终端上开启 pt-deadlock-logger 检测

pt-deadlock-logger 127.0.0.1 --user root --ask-pass --tab

检测开启后进行死锁测试

# 插入两条测试数据INSERT INTO test(value) VALUES('test1');INSERT INTO test(value) VALUES('test2');# 在两个终端下进行交叉事务# 统一关闭自动提交terminal_1 # SET AUTOCOMMIT = 0;terminal_2 # SET AUTOCOMMIT = 0;# 交叉事务,终端 1 先更新第一条数据,终端 2 先更新第二条数据terminal_1 # BEGIN;terminal_1 # UPDATE test set value='x1' where id=1;terminal_2 # BEGIN;terminal_2 # UPDATE test set value='x2' where id=2;# 此后终端 1 再尝试更新第二条数据,终端 2 再尝试更新第一条数据;造成等待互向释放锁的死锁terminal_1 # UPDATE test set value='lock2' where id=2;terminal_2 # UPDATE test set value='lock1' where id=1;# 此时由于开启了 mysql innodb 的死锁自动检测机制,会导致终端 2 弹出错误ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction# 同时 pt-deadlock-logger 有日志输出server  ts      thread  txn_id  txn_time        user    hostname    ip      db      tbl     idx     lock_type       lock_mode       wait_hold       victim  query127.0.0.1       2019-12-24T14:57:10     87      0       52      root            127.0.0.1       dbatest test    PRIMARY RECORD  X       w       0       UPDATE test set value='lock2' where id=2127.0.0.1       2019-12-24T14:57:10     89      0       41      root            127.0.0.1       dbatest test    PRIMARY RECORD  X       w       1       UPDATE test set value='lock1' where id=1

8.4、查看 IO 详情

不同于 iostatpt-diskstats 提供了更加详细的 IO 详情统计,并且据有交互式处理,执行一下命令将会实时检测 IO 状态

pt-diskstats --show-timestamps

其中几个关键值含义如下(更详细的请参考官方文档 http://www.percona.com/doc/percona-toolkit/LATEST/pt-diskstats.html#output)

  • rd_s: 每秒平均读取次数。这是发送到基础设备的 IO 请求数。通常,此数量少于应用程序发出的逻辑IO请求的数量。更多请求可能已排队到块设备,但是其中一些请求通常在发送到磁盘之前先进行合并。
  • rd_avkb: 读取的平均大小,以千字节为单位。
  • rd_mb_s: 每秒读取的平均兆字节数。
  • rd_mrg: 在发送到物理设备之前在队列调度程序中合并在一起的读取请求的百分比。
  • rd_rt: 读取操作的平均响应时间(以毫秒为单位);这是端到端响应时间,包括在队列中花费的时间。这是发出 IO 请求的应用程序看到的响应时间,而不是块设备下的物理磁盘的响应时间。
  • busy: 设备至少有一个请求 wall-clock 时间的比例;等同于 iostat%util
  • in_prg: 正在进行的请求数。与读写并发是从可靠数字中生成的平均值不同,该数字是一个时样本,您可以看到它可能表示请求峰值,而不是真正的长期平均值。如果此数字很大,则从本质上讲意味着设备高负载运行。
  • ios_s: 物理设备的平均吞吐量,以每秒 IO 操作(IOPS)为单位。此列显示基础设备正在处理的总 IOPS;它是 rd_s 和 wr_s 的总和。
  • qtime: 平均排队时间;也就是说,请求在发送到物理设备之前在设备调度程序队列中花费的时间。
  • stime: 平均服务时间;也就是说,请求完成在队列中的等待之后,物理设备处理请求的时间。

8.5、重复索引优化

pt-duplicate-key-checker 工具提供了对数据库重复索引和外键的自动查找功能,工具使用如下

pt-duplicate-key-checker 127.0.0.1 --user root --ask-passEnter password:# A software update is available:# ######################################################################### aaaaaa.aaaaaa_audit# ######################################################################### index_linkId is a duplicate of unique_linkId# Key definitions:#   KEY `index_linkId` (`link_id`)#   UNIQUE KEY `unique_linkId` (`link_id`),# Column types:#         `link_id` bigint(20) not null comment 'bdid'# To remove this duplicate index, execute:ALTER TABLE `aaaaaa.aaaaaa_audit` DROP INDEX `index_linkId`;# ######################################################################### Summary of indexes# ######################################################################### Size Duplicate Indexes   927420# Total Duplicate Indexes  3# Total Indexes            847

8.6、表统计

pt-find 是一个很方便的表查找统计工具,默认的一些选项可以实现批量查找符合条件的表,甚至执行一些 SQL 处理命令

# 批量查找大于 5G 的表,并排序pt-find --host 127.0.0.1 --user root --ask-pass --tablesize +5G | sort -rnEnter password: `rss_service`.`test_feed_news``db_log_history`.`test_mobile_click_201912``db_log_history`.`test_mobile_click_201911``db_log_history`.`test_mobile_click_201910``test_dix`.`test_user_messages``test_dix`.`test_user_link_history``test_dix`.`test_mobile_click``test_dix`.`test_message``test_dix`.`test_link_votes``test_dix`.`test_links_mobile_content``test_dix`.`test_links``test_dix`.`test_comment_votes``test_dix`.`test_comments`

如果想要定制输出可以采用 --printf 选项

pt-find --host 127.0.0.1 --user root --ask-pass --tablesize +5G --printf "%T\t%D.%N\n" | sort -rnEnter password: 13918404608     `test_dix`.`test_links_mobile_content`13735231488     `test_dix`.`test_comment_votes`12633227264     `test_dix`.`test_user_messages`12610174976     `test_dix`.`test_user_link_history`10506305536     `test_dix`.`test_links`9686745088      `test_dix`.`test_message`9603907584      `rss_service`.`test_feed_news`9004122112      `db_log_history`.`test_mobile_click_201910`8919007232      `test_dix`.`test_comments`8045707264      `db_log_history`.`test_mobile_click_201912`7855915008      `db_log_history`.`test_mobile_click_201911`6099566592      `test_dix`.`test_mobile_click`5892898816      `test_dix`.`test_link_votes`

遗憾的是目前 printf 格式来源与 Perl 的 sprintf 函数,所以支持格式有限,不过简单的格式定制已经基本实现,复杂的建议通过 awk 处理;其他的可选参数具体参考官方文档 http://www.percona.com/doc/percona-toolkit/LATEST/pt-find.html

8.7、其他命令

迫于篇幅,其他更多的高级命令请自行查阅官方文档 http://www.percona.com/doc/percona-toolkit/LATEST/index.html

]]>
Linux Database Linux MySQL Percona http://mritd.com/2020/01/20/set-up-percona-server/#disqus_thread
Writing Plugin for Coredns http://mritd.com/2019/11/05/writing-plugin-for-coredns/ http://mritd.com/2019/11/05/writing-plugin-for-coredns/ Tue, 05 Nov 2019 12:57:41 GMT 目前测试环境中有很多个 DNS 服务器,不同项目组使用的 DNS 服务器不同,但是不可避免的他们会访问一些公共域名;老的 DNS 服务器都是 dnsmasq,改起来很麻烦,最近研究了一下 CoreDNS,通过编写插件的方式可以实现让多个 CoreDNS 实例实现分布式的统一控制,以下记录了插件编写过程

目前测试环境中有很多个 DNS 服务器,不同项目组使用的 DNS 服务器不同,但是不可避免的他们会访问一些公共域名;老的 DNS 服务器都是 dnsmasq,改起来很麻烦,最近研究了一下 CoreDNS,通过编写插件的方式可以实现让多个 CoreDNS 实例实现分布式的统一控制,以下记录了插件编写过程

一、CoreDNS 简介

CoreDNS 目前是 CNCF 旗下的项目(已毕业),为 Kubernetes 等云原生环境提供可靠的 DNS 服务发现等功能;官网的描述只有一句话: CoreDNS: DNS and Service Discovery,而实际上分析源码以后发现 CoreDNS 实际上是基于 Caddy (一个现代化的负载均衡器)而开发的,通过插件式注入,并监听 TCP/UDP 端口提供 DNS 服务;得益于 Caddy 的插件机制,CoreDNS 支持自行编写插件,拦截 DNS 请求然后处理,通过这个插件机制你可以在 CoreDNS 上实现各种功能,比如构建分布式一致性的 DNS 集群、动态的 DNS 负载均衡等等

二、CoreDNS 插件规范

2.1、插件模式

CoreDNS 插件编写目前有两种方式:

  • 深度耦合 CoreDNS,使用 Go 编写插件,直接编译进 CoreDNS 二进制文件
  • 通过 GRPC 解耦,任意语言编写 GRPC 接口实现,CoreDNS 通过 GRPC 与插件交互

由于 GRPC 链接实际上借助于 CoreDNS 的 GRPC 插件,同时 GRPC 会有网络开销,TCP 链接不稳定可能造成 DNS 响应过慢等问题,所以本文只介绍如何使用 Go 编写 CoreDNS 的插件,这种插件将直接编译进 CoreDNS 二进制文件中

2.2、插件注册

在通常情况下,插件中应当包含一个 setup.go 文件,这个文件的 init 方法调用插件注册,类似这样

func init() {     plugin.Register("gdns", setup) }

注册方法的第一个参数是插件名称,第二个是一个 func,func 签名如下

// SetupFunc is used to set up a plugin, or in other words,// execute a directive. It will be called once per key for// each server block it appears in.type SetupFunc func(c *Controller) error

在这个 SetupFunc 中,插件编写者应当通过 *Controller 拿到 CoreDNS 的配置并解析它,从而完成自己插件的初始化配置;比如你的插件需要连接 Etcd,那么在这个方法里你要通过 *Controller 遍历配置,拿到 Etcd 的地址、证书、用户名密码配置等信息;

如果配置信息没有问题,该插件应当初始化完成;如果有问题就报错退出,然后整个 CoreDNS 启动失败;如果插件初始化完成,最后不要忘记将自己的插件加入到整个插件链路中(CoreDNS 根据情况逐个调用)

func setup(c *caddy.Controller) error {e, err := etcdParse(c)if err != nil {return plugin.Error("gdns", err)}dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {e.Next = nextreturn e})return nil}

2.3、插件结构体

一般来说,每一个插件都会定义一个结构体,结构体中包含必要的 CoreDNS 内置属性,以及当前插件特性的相关配置;一个样例的插件结构体如下所示

type GDNS struct {  // Next 属性在 Setup 之后会被设置到下一个插件的引用,以便在本插件解析失败后可以交由下面的插件继续解析Next       plugin.Handler// Fall 列表用来控制哪些域名的请求解析失败后可以继续穿透到下一个插件重新处理Fall       fall.F// Zones 表示当前插件应该 case 哪些域名的 DNS 请求Zones      []string// PathPrefix 和 Client 就是插件本身的业务属性了,由于插件要连 Etcd// PathPrefix 就是 Etcd 目录前缀,Client 是一个 Etcd 的 client// endpoints 是 Etcd api 端点的地址PathPrefix stringClient     *etcdcv3.Clientendpoints []string // Stored here as well, to aid in testing.}

2.4、插件接口

一个 Go 编写的 CoreDNS 插件实际上只需要实现一个 Handler 接口既可,接口定义如下

// Handler is like dns.Handler except ServeDNS may return an rcode// and/or error.//// If ServeDNS writes to the response body, it should return a status// code. CoreDNS assumes *no* reply has yet been written if the status// code is one of the following://// * SERVFAIL (dns.RcodeServerFailure)//// * REFUSED (dns.RecodeRefused)//// * FORMERR (dns.RcodeFormatError)//// * NOTIMP (dns.RcodeNotImplemented)//// All other response codes signal other handlers above it that the// response message is already written, and that they should not write// to it also.//// If ServeDNS encounters an error, it should return the error value// so it can be logged by designated error-handling plugin.//// If writing a response after calling another ServeDNS method, the// returned rcode SHOULD be used when writing the response.//// If handling errors after calling another ServeDNS method, the// returned error value SHOULD be logged or handled accordingly.//// Otherwise, return values should be propagated down the plugin// chain by returning them unchanged.Handler interface {ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)Name() string}
  • ServeDNS 方法是插件需要实现的主要逻辑方法,DNS 请求接受后会从这个方法传入,插件编写者需要实现查询并返回结果
  • Name 方法只返回一个插件名称标识,具体作用记不太清楚,好像是为了判断插件命名唯一性然后做链式顺序调用的,原则只要你不跟系统插件重名就行

基本逻辑就是在 setup 阶段通过配置文件创建你的插件结构体对象;然后插件结构体实现这个 Handler 接口,运行期 CoreDNS 会调用接口的 ServeDNS 方法来向插件查询 DNS 请求

2.5、ServeDNS 方法

ServeDNS 方法入参有 3 个:

  • context.Context 用来控制超时等情况的 context
  • dns.ResponseWriter 插件通过这个对象写入对 Client DNS 请求的响应结果
  • *dns.Msg 这个是 Client 发起的 DNS 请求,插件负责处理它,比如当你发现请求类型是 AAAA 而你的插件又不想去支持时要如何返回结果

对于返回结果,插件编写者应当通过 dns.ResponseWriter.WriteMsg 方法写入返回结果,基本代码如下

// ServeDNS implements the plugin.Handler interface.func (gDNS *GDNS) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {// ...... 这里应当实现你的业务逻辑,查找相应的 DNS 记录// 最后通过 new 一个 dns.Msg 作为返回结果resp := new(dns.Msg)resp.SetReply(r)resp.Authoritative = true// records 是真正的记录结果,应当在业务逻辑区准备好resp.Answer = append(resp.Answer, records...)// 返回结果err = w.WriteMsg(resp)if err != nil {log.Error(err)}   // 告诉 CoreDNS 是否处理成功return dns.RcodeSuccess, nil}

需要注意的是,无论根据业务逻辑是否查询到 DNS 记录,都要返回响应结果(没有就返回空),错误或者未返回将会导致 Client 端查询 DNS 超时,然后不断重试,最终可能导致 Client 端服务故障

2.6、Name 方法

Name 方法非常简单,只需要返回当前插件名称既可;该方法的作用是为了其他插件判断本插件是否加载等情况

// Name implements the Handler interface.func (gDNS *GDNS) Name() string { return "gdns" }

三、CoreDNS 插件处理

对于实际的业务处理,可以通过 case 请求 QType 来做具体的业务实现

// ServeDNS implements the plugin.Handler interface.func (gDNS *GDNS) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {state := request.Request{W: w, Req: r}zone := plugin.Zones(gDNS.Zones).Matches(state.Name())if zone == "" {return plugin.NextOrFailure(gDNS.Name(), gDNS.Next, ctx, w, r)}// ...业务处理switch state.QType() {case dns.TypeA:// A 记录查询业务逻辑case dns.TypeAAAA:// AAAA 记录查询业务逻辑default:return falseresp := new(dns.Msg)resp.SetReply(r)resp.Authoritative = trueresp.Answer = append(resp.Answer, records...)err = w.WriteMsg(resp)if err != nil {log.Error(err)}return dns.RcodeSuccess, nil}

四、插件编译及测试

4.1、官方标准操作

根据官方文档的描述,当你编写好插件以后,你的插件应当提交到一个 Git 仓库中,可以使 Github 等(保证可以 go get 拉取就行),然后修改 plugin.cfg,最后执行 make 既可;具体修改如下所示

plugin.cfg

值得注意的是: 插件配置在 plugin.cfg 内的顺序决定了插件的执行顺序;通俗的讲,如果 Client 的一个 DNS 请求进来,CoreDNS 根据你在 plugin.cfg 内书写的顺序依次调用,而并非 Corefile 内的配置顺序

配置好以后直接执行 make 既可编译成功一个包含自定义插件的 CoreDNS 二进制文件(编译过程的 go mod 下载加速问题不在本文讨论范围内);你可以直接通过这个二进制测试插件的处理情况,当然这种测试不够直观,而且频繁修改由于 go mod 缓存等原因并不一定能保证每次编译的都包含最新插件代码,所以另一种方式请看下一章节

4.2、经验性的操作

根据个人测试以及对源码的分析,在修改 plugin.cfg 然后执行 make 命令后,实际上是进行了代码生成;当你通过 git 命令查看相关修改文件时,整个插件加载体系便没什么秘密可言了;在整个插件体系中,插件加载是通过 init 方法注册的,那么既然用 go 写插件,那么应该清楚 init 方法只有在包引用之后才会执行,所以整个插件体系实际上是这样事儿的:

首先 make 以后会修改 core/plugin/zplugin.go 文件,这个文件啥也不干,就是 import 来实现调用对应包的 init 方法

zplugin.go

init 执行后你去追源码,实际上就是 Caddy 维护了一个 map[string]Plugininit 会把你的插件 func 塞进去然后后面再调用,实现一个懒加载或者说延迟初始化

caddy_plugin

接着修改了一下 core/dnsserver/zdirectives.go,这个里面也没啥,就是一个 []string但是 []string 这玩意有顺序啊,这就是为什么你在 plugin.cfg 里写的顺序决定了插件处理顺序的原因(因为生成的这个切片有顺序)

zdirectives.go

综上所述,实际上 make 命令一共修改了两个文件,如果想在 IDE 内直接 debug CoreDNS + Plugin 源码,那么只需要这样做:

复制自己编写的插件目录到 plugin 目录,类似这样

gdns

手动修改 core/plugin/zplugin.go,加入自己插件的 import(此时你直接复制系统其他插件,改一下目录名既可)

update_zplugin

手动修改 core/dnsserver/zdirectives.go 把自己插件名称写进去(自己控制顺序),然后 debug 启动 coredns.go 里面的 main 方法测试既可

coredns.go

五、本文参考

]]>
Golang Kubernetes Golang CoreDNS http://mritd.com/2019/11/05/writing-plugin-for-coredns/#disqus_thread
Golang Etcd Client Example http://mritd.com/2019/10/15/golang-etcd-client-example/ http://mritd.com/2019/10/15/golang-etcd-client-example/ Tue, 15 Oct 2019 04:21:07 GMT 准备开发点东西,需要用到 Etcd,由于生产 Etcd 全部开启了 TLS 加密,所以客户端需要相应修改,以下为 Golang 链接 Etcd 并且使用客户端证书验证的样例代码

准备开发点东西,需要用到 Etcd,由于生产 Etcd 全部开启了 TLS 加密,所以客户端需要相应修改,以下为 Golang 链接 Etcd 并且使用客户端证书验证的样例代码

API V2

package mainimport ("context""crypto/tls""crypto/x509""io/ioutil""log""net""net/http""time""go.etcd.io/etcd/client")func main() {// 为了保证 http 链接可信,需要预先加载目标证书签发机构的 CA 根证书etcdCA, err := ioutil.ReadFile("/Users/mritd/tmp/etcd_ssl/etcd-root-ca.pem")if err != nil {log.Fatal(err)}// etcd 启用了双向 TLS 认证,所以客户端证书同样需要加载etcdClientCert, err := tls.LoadX509KeyPair("/Users/mritd/tmp/etcd_ssl/etcd.pem", "/Users/mritd/tmp/etcd_ssl/etcd-key.pem")if err != nil {log.Fatal(err)}// 创建一个空的 CA Pool// 因为后续只会链接 Etcd 的 api 端点,所以此处选择使用空的 CA Pool,然后只加入 Etcd CA 既可// 如果期望链接其他 TLS 端点,那么最好使用 x509.SystemCertPool() 方法先 copy 一份系统根 CA// 然后再向这个 Pool 中添加自定义 CArootCertPool := x509.NewCertPool()rootCertPool.AppendCertsFromPEM(etcdCA)cfg := client.Config{// Etcd http api 端点Endpoints: []string{"http://172.16.14.114:2379"},// 自定义 Transport 实现自签 CA 加载以及 Client Cert 加载// 其他参数最好从 client.DefaultTranspor copy,以保证与默认 client 相同的行为Transport: &http.Transport{Proxy: http.ProxyFromEnvironment,// Dial 方法已被启用,采用新的 DialContext 设置超时DialContext: (&net.Dialer{KeepAlive: 30 * time.Second,Timeout:   30 * time.Second,}).DialContext,// 自定义 CA 及 Client Cert 配置TLSClientConfig: &tls.Config{RootCAs:      rootCertPool,Certificates: []tls.Certificate{etcdClientCert},},TLSHandshakeTimeout: 10 * time.Second,},// set timeout per request to fail fast when the target endpoint is unavailableHeaderTimeoutPerRequest: time.Second,}c, err := client.New(cfg)if err != nil {log.Fatal(err)}kapi := client.NewKeysAPI(c)// set "/foo" key with "bar" valuelog.Print("Setting '/foo' key with 'bar' value")resp, err := kapi.Set(context.Background(), "/foo", "bar", nil)if err != nil {log.Fatal(err)} else {// print common key infolog.Printf("Set is done. Metadata is %q\n", resp)}// get "/foo" key's valuelog.Print("Getting '/foo' key value")resp, err = kapi.Get(context.Background(), "/foo", nil)if err != nil {log.Fatal(err)} else {// print common key infolog.Printf("Get is done. Metadata is %q\n", resp)// print valuelog.Printf("%q key has %q value\n", resp.Node.Key, resp.Node.Value)}}

API V3

package mainimport ("context""crypto/tls""crypto/x509""io/ioutil""log""time""go.etcd.io/etcd/clientv3")func main() {// 为了保证 http 链接可信,需要预先加载目标证书签发机构的 CA 根证书etcdCA, err := ioutil.ReadFile("/Users/mritd/tmp/etcd_ssl/etcd-root-ca.pem")if err != nil {log.Fatal(err)}// etcd 启用了双向 TLS 认证,所以客户端证书同样需要加载etcdClientCert, err := tls.LoadX509KeyPair("/Users/mritd/tmp/etcd_ssl/etcd.pem", "/Users/mritd/tmp/etcd_ssl/etcd-key.pem")if err != nil {log.Fatal(err)}// 创建一个空的 CA Pool// 因为后续只会链接 Etcd 的 api 端点,所以此处选择使用空的 CA Pool,然后只加入 Etcd CA 既可// 如果期望链接其他 TLS 端点,那么最好使用 x509.SystemCertPool() 方法先 copy 一份系统根 CA// 然后再向这个 Pool 中添加自定义 CArootCertPool := x509.NewCertPool()rootCertPool.AppendCertsFromPEM(etcdCA)// 创建 api v3 的 clientcli, err := clientv3.New(clientv3.Config{// etcd http api 端点Endpoints:   []string{"http://172.16.14.114:2379"},DialTimeout: 5 * time.Second,// 自定义 CA 及 Client Cert 配置TLS: &tls.Config{RootCAs:      rootCertPool,Certificates: []tls.Certificate{etcdClientCert},},})if err != nil {log.Fatal(err)}defer func() { _ = cli.Close() }()ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)putResp, err := cli.Put(ctx, "sample_key", "sample_value")if err != nil {log.Fatal(err)} else {log.Println(putResp)}cancel()ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second)delResp, err := cli.Delete(ctx, "sample_key")if err != nil {log.Fatal(err)} else {log.Println(delResp)}cancel()}
]]>
Golang Golang etcd http://mritd.com/2019/10/15/golang-etcd-client-example/#disqus_thread
Podman 初试 - 容器发展史 http://mritd.com/2019/06/26/podman-history-of-container/ http://mritd.com/2019/06/26/podman-history-of-container/ Wed, 26 Jun 2019 15:22:49 GMT 这是一篇纯介绍性文章,本文不包含任何技术层面的操作,本文仅作为后续 Podman 文章铺垫;本文细节部份并未阐述,很多地方并不详实(一家只谈,不可轻信)。

这是一篇纯介绍性文章,本文不包含任何技术层面的操作,本文仅作为后续 Podman 文章铺垫;本文细节部份并未阐述,很多地方并不详实(一家只谈,不可轻信)。

一、缘起

1.1、鸿蒙

在上古时期,天地初开,一群称之为 “运维” 的人们每天在一种叫作 “服务器” 的神秘盒子中创造属于他们的世界;他们在这个世界中每日劳作,一遍又一遍的写入他们的历史,比如搭建一个 nginx、布署一个 java web 应用…

大多数人其实并没有那么聪明,他们所 “创造” 的事实上可能是有人已经创造过的东西,他们可能每天都在做着重复的劳动;久而久之,一些人厌倦了、疲惫了…又过了一段时间,一些功力深厚的老前辈创造了一些批量布署工具来帮助人们做一些重复性的劳动,这些工具被起名为 “Asible”、”Chef”、”Puppet” 等等…

而随着时代的发展,”世界” 变得越来越复杂,运维们需要处理的事情越来越多,比如各种网络、磁盘环境的隔离,各种应用服务的高可用…在时代的洪流下,运维们急需要一种简单高效的布署工具,既能有一定的隔离性,又能方便使用,并且最大程度降低重复劳动来提升效率。

1.2、创世

在时代洪流的冲击下,一位名为 “Solomon Hykes” 的人异军突起,他创造了一个称之为 Docker 的工具,Docker 被创造以后就以灭世之威向运维们展示了它的强大;一个战斗力只有 5 的运维只需要学习 Docker 很短时间就可以完成资深运维们才能完成的事情,在某些情况下以前需要 1 天才能完成的工作使用 Docker 后几分钟就可以完成;此时运维们已经意识到 “新的时代” 开启了,接下来 Docker 开源并被整个运维界人们使用,Docker 也不断地完善增加各种各样的功能,此后世界正式进入 “容器纪元”。

二、纷争

2.1、发展

随着 Docker 的日益成熟,一些人开始在 Docker 之上创造更加强大的工具,一些人开始在 Docker 之下为其提供更稳定的运行环境…

其中一个叫作 Google 的公司在 Docker 之上创建了名为 “Kubernetes” 的工具,Kubernetes 操纵 Docker 完成更加复杂的任务;Kubernetes 的出现更加印证了 Docker 的强大,以及 “容器纪元” 的发展正确性。

2.2、野心

当然这是一个充满利益的世界,Google 公司创造 Kubernetes 是可以为他们带来利益的,比如他们可以让 Kubernetes 深度适配他们的云平台,以此来增加云平台的销量等;此时 Docker 创始人也成立了一个公司,提供 Docker 的付费服务以及深度定制等;不过值得一提的是 Docker 公司提供的付费服务始终没有 Kubernetes 为 Google 公司带来的利益高,所以在利益的驱使下,Docker 公司开始动起了歪心思: 创造一个 Kubernetes 的替代品,利用用户粘度复制 Kubernetes 的成功,从 Google 嘴里抢下这块蛋糕!此时 Docker 公司只想把蛋糕抢过来,但是他们根本没有在意到暗中一群人创造了一个叫 “rkt” 的东西也在妄图夺走他们嘴里的蛋糕。

2.3、冲突

在一段时间的沉默后,Docker 公司又创造了 “Swarm” 这个工具,妄图夺走 Google 公司利用 Kubernetes 赢来的蛋糕;当然,Google 这个公司极其庞大,人数众多,而且在这个社会有很大的影响地位…

终于,巨人苏醒了,Google 联合了 Redhat、Microsoft、IBM、Intel、Cisco 等公司决定对这个爱动歪脑筋的 Docker 公司进行制裁;当然制裁的手段不能过于暴力,那样会让别人落下把柄,成为别人的笑料,被人所不耻;最总他们决定制订规范,成立组织,明确规定 Docker 的角色,以及它应当拥有的能力,这些规范包括但不限于 CRICNI 等;自此之后各大公司宣布他们容器相关的工具只兼容 CRI 等相关标准,无论是 Docker 还是 rkt 等工具,只要实现了这些标准,就可以配合这些容器工具进行使用

三、成败

自此之后,Docker 跌下神坛,各路大神纷纷创造满足 CRI 等规范的工具用来取代 Docker,Docker 丢失了往日一家独大的场面,最终为了顺应时代发展,拆分自己成为模块化组件;这些模块化组件被放置在 mobyproject 中方便其他人重复利用。

时至今日,虽然 Docker 已经不负以前,但是仍然是容器化首选工具,因为 Docker 是一个完整的产品,它可以提供除了满足 CRI 等标准以外更加方便的功能;但是制裁并非没有结果,Google 公司借此创造了 cri-o 用来满足 CRI 标准,其他公司也相应创建了对应的 CRI 实现;为了进一步分化 Docker 势力,一个叫作 Podman 的工具被创建,它以 cri-o 为基础,兼容大部份 Docker 命令的方式开始抢夺 Dcoker 用户;到目前为止 Podman 已经可以在大部份功能上替代 Docker。

]]>
Docker Docker Podman http://mritd.com/2019/06/26/podman-history-of-container/#disqus_thread
Calico 3.6 转发外部流量到集群 Pod http://mritd.com/2019/06/18/calico-3.6-forward-network-traffic/ http://mritd.com/2019/06/18/calico-3.6-forward-network-traffic/ Tue, 18 Jun 2019 14:20:54 GMT 由于开发有部份服务使用 GRPC 进行通讯,同时采用 Consul 进行服务发现;在微服务架构下可能会导致一些访问问题,目前解决方案就是打通开发环境网络与测试环境 Kubernetes 内部 Pod 网络;翻了好多资料发现都是 2.x 的,而目前测试集群 Calico 版本为 3.6.3,很多文档都不适用只能自己折腾,目前折腾完了这里记录一下

由于开发有部份服务使用 GRPC 进行通讯,同时采用 Consul 进行服务发现;在微服务架构下可能会导致一些访问问题,目前解决方案就是打通开发环境网络与测试环境 Kubernetes 内部 Pod 网络;翻了好多资料发现都是 2.x 的,而目前测试集群 Calico 版本为 3.6.3,很多文档都不适用只能自己折腾,目前折腾完了这里记录一下

本文默认为读者已经存在一个运行正常的 Kubernetes 集群,并且采用 Calico 作为 CNI 组件,且 Calico 工作正常;同时应当在某个节点完成了 calicoctl 命令行工具的配置

一、问题描述

在微服务架构下,由于服务组件很多,开发在本地机器想测试应用需要启动整套服务,这对开发机器的性能确实是个考验;但如果直接连接测试环境的服务,由于服务发现问题最终得到的具体服务 IP 是 Kubernetes Pod IP,此 IP 由集群内部 Calico 维护与分配,外部不可访问;最终目标为打通开发环境与集群内部网络,实现开发网络下直连 Pod IP,这或许在以后对生产服务暴露负载均衡有一定帮助意义;目前网络环境如下:

开发网段: 10.10.0.0/24
测试网段: 172.16.0.0/24
Kubernetes Pod 网段: 10.20.0.0/16

二、打通网络

首先面临的第一个问题是 Calico 处理,因为如果想要让数据包能从开发网络到达 Pod 网络,那么必然需要测试环境宿主机上的 Calico Node 帮忙转发;因为 Pod 网络由 Calico 维护,只要 Calico Node 帮忙转发那么数据一定可以到达 Pod IP 上;

一开始我很天真的认为这就是个 ip route add 10.20.0.0/16 via 172.16.0.13 的问题… 后来发现

没那么简单

经过翻文档、issue、blog 等最终发现需要进行以下步骤

2.1、关闭全互联模式

注意: 关闭全互联时可能导致网络暂时中断,请在夜深人静时操作

首先执行以下命令查看是否存在默认的 BGP 配置

calicoctl get bgpconfig default

如果存在则将其保存为配置文件

calicoctl get bgpconfig default -o yaml > bgp.yaml

修改其中的 spec.nodeToNodeMeshEnabledfalse,然后进行替换

calicoctl apply -f bgp.yaml

如果不存在则手动创建一个配置,然后应用

 cat << EOF | calicoctl create -f - apiVersion: projectcalico.org/v3 kind: BGPConfiguration metadata:   name: default spec:   logSeverityScreen: Info   nodeToNodeMeshEnabled: false   asNumber: 63400EOF

本部分参考:

2.2、开启集群内 RR 模式

在 Calico 3.3 后支持了集群内节点的 RR 模式,即将某个集群内的 Calico Node 转变为 RR 节点;将某个节点设置为 RR 节点只需要增加 routeReflectorClusterID 既可,为了后面方便配置同时增加了一个 lable 字段 route-reflector: "true"

calicoctl get node CALICO_NODE_NAME -o yaml > node.yaml

然后增加 routeReflectorClusterID 字段,样例如下

apiVersion: projectcalico.org/v3kind: Nodemetadata:  annotations:    projectcalico.org/kube-labels: '{"beta.kubernetes.io/arch":"amd64","beta.kubernetes.io/os":"linux","kubernetes.io/hostname":"d13.node","node-role.kubernetes.io/k8s-master":"true"}'  creationTimestamp: 2019-06-17T13:55:44Z  labels:    beta.kubernetes.io/arch: amd64    beta.kubernetes.io/os: linux    kubernetes.io/hostname: d13.node    node-role.kubernetes.io/k8s-master: "true"    route-reflector: "true"  # 增加 lable  name: d13.node  resourceVersion: "61822269"  uid: 9a1897e0-9107-11e9-bc1c-90b11c53d1e3spec:  bgp:    ipv4Address: 172.16.0.13/19    ipv4IPIPTunnelAddr: 10.20.73.82    routeReflectorClusterID: 172.16.20.1 # 添加集群 ID  orchRefs:  - nodeName: d13.node    orchestrator: k8s

事实上我们应当导出多个 Calico Node 的配置,并将其配置为 RR 节点以进行冗余;对于 routeReflectorClusterID 目前测试只是作为一个 ID(至少在本文是这样的),所以理论上可以是任何 IP,个人猜测最好在同一集群网络下采用相同的 IP,由于这是真正的测试环境我没有对 ID 做过多的测试(怕玩挂)

修改完成后只需要应用一下就行

calicoctl apply -f node.yaml

接下来需要创建对等规则,规则文件如下

kind: BGPPeerapiVersion: projectcalico.org/v3metadata:  name: peer-to-rrsspec:  nodeSelector: "!has(route-reflector)"  peerSelector: has(route-reflector)---kind: BGPPeerapiVersion: projectcalico.org/v3metadata:  name: rr-meshspec:  nodeSelector: has(route-reflector)  peerSelector: has(route-reflector)

假定规则文件名称为 rr.yaml,则创建命令为 calicoctl create -f rr.yaml;此时在 RR 节点上使用 calicoctl node status 应该能看到类似如下输出

Calico process is running.IPv4 BGP status+--------------+---------------+-------+----------+-------------+| PEER ADDRESS |   PEER TYPE   | STATE |  SINCE   |    INFO     |+--------------+---------------+-------+----------+-------------+| 172.16.0.19  | node specific | up    | 05:43:51 | Established || 172.16.0.16  | node specific | up    | 05:43:51 | Established || 172.16.0.17  | node specific | up    | 05:43:51 | Established || 172.16.0.13  | node specific | up    | 13:01:17 | Established |+--------------+---------------+-------+----------+-------------+IPv6 BGP statusNo IPv6 peers found.

PEER ADDRESS 应当包含所有非 RR 节点 IP(由于真实测试环境,以上输出已人为修改)

同时在非 RR 节点上使用 calicoctl node status 应该能看到以下输出

Calico process is running.IPv4 BGP status+--------------+---------------+-------+----------+-------------+| PEER ADDRESS |   PEER TYPE   | STATE |  SINCE   |    INFO     |+--------------+---------------+-------+----------+-------------+| 172.16.0.10  | node specific | up    | 05:43:51 | Established || 172.16.0.13  | node specific | up    | 13:01:20 | Established |+--------------+---------------+-------+----------+-------------+IPv6 BGP statusNo IPv6 peers found.

PEER ADDRESS 应当包含所有 RR 节点 IP,此时原本的 Pod 网络连接应当已经恢复

本部分参考:

2.3、调整 IPIP 规则

先说一下 Calico IPIP 模式的三个可选项:

  • Always: 永远进行 IPIP 封装(默认)
  • CrossSubnet: 只在跨网段时才进行 IPIP 封装,适合有 Kubernetes 节点在其他网段的情况,属于中肯友好方案
  • Never: 从不进行 IPIP 封装,适合确认所有 Kubernetes 节点都在同一个网段下的情况

在默认情况下,默认的 ipPool 启用了 IPIP 封装(至少通过官方安装文档安装的 Calico 是这样),并且封装模式为 Always;这也就意味着任何时候都会在原报文上封装新 IP 地址,在这种情况下将外部流量路由到 RR 节点,RR 节点再转发进行 IPIP 封装时,可能出现网络无法联通的情况(没仔细追查,网络渣,猜测是 Pod 那边得到的源 IP 不对导致的);此时我们应当调整 IPIP 封装策略为 CrossSubnet

导出 ipPool 配置

calicoctl get ippool default-ipv4-ippool -o yaml > ippool.yaml

修改 ipipMode 值为 CrossSubnet

apiVersion: projectcalico.org/v3kind: IPPoolmetadata:  creationTimestamp: 2019-06-17T13:55:44Z  name: default-ipv4-ippool  resourceVersion: "61858741"  uid: 99a82055-9107-11e9-815b-b82a72dffa9fspec:  blockSize: 26  cidr: 10.20.0.0/16  ipipMode: CrossSubnet  natOutgoing: true  nodeSelector: all()

重新使用 calicoctl apply -f ippool.yaml 应用既可

本部分参考:

2.4、增加路由联通网络

万事俱备只欠东风,最后只需要在开发机器添加路由既可

将 Pod IP 10.20.0.0/16 和 Service IP 10.254.0.0/16 路由到 RR 节点 172.16.0.13

# Pod IPip route add 10.20.0.0/16 via 172.16.0.13# Service IPip route add 10.254.0.0/16 via 172.16.0.13

当然最方便的肯定是将这一步在开发网络的路由上做,设置完成后开发网络就可以直连集群内的 Pod IP 和 Service IP 了;至于想直接访问 Service Name 只需要调整上游 DNS 解析既可

]]>
Kubernetes Kubernetes http://mritd.com/2019/06/18/calico-3.6-forward-network-traffic/#disqus_thread
Dockerfile 目前可扩展的语法 http://mritd.com/2019/05/13/dockerfile-extended-syntax/ http://mritd.com/2019/05/13/dockerfile-extended-syntax/ Mon, 13 May 2019 14:57:07 GMT 最近在调整公司项目的 CI,目前主要使用 GitLab CI,在尝试多阶段构建中踩了点坑,然后发现了一些有意思的玩意

最近在调整公司项目的 CI,目前主要使用 GitLab CI,在尝试多阶段构建中踩了点坑,然后发现了一些有意思的玩意

本文参考:

一、起因

公司目前主要使用 GitLab CI 作为主力 CI 构建工具,而且由于机器有限,我们对一些包管理器的本地 cache 直接持久化到了本机;比如 maven 的 .m2 目录,nodejs 的 .npm 目录等;虽然我们创建了对应的私服,但是在 build 时毕竟会下载,所以当时索性调整 GitLab Runner 在每个由 GitLab Runner 启动的容器中挂载这些缓存目录(GitLab CI 在 build 时会新启动容器运行 build 任务);今天调整 nodejs 项目浪了一下,直接采用 Dockerfile 的 multi-stage build 功能进行 “Build => Package(docker image)” 的实现,基本 Dockerfile 如下

FROM gozap/build as builderCOPY . /xxxxWORKDIR /xxxxRUN source ~/.bashrc \    && cnpm install \    && cnpm run buildFROM gozap/nginx-react:v1.0.0LABEL maintainer="mritd <mritd@linux.com>"COPY --from=builder /xxxx/public /usr/share/nginx/htmlEXPOSE 80STOPSIGNAL SIGTERMCMD ["nginx", "-g", "daemon off;"]

本来这个 cnpm 命令是带有 cache 的(见这里),不过运行完 build 以后发现很慢,检查宿主机 cache 目录发现根本没有 cache…然后突然感觉

事情并没有这么简单

仔细想想,情况应该是这样事儿的…

+------------+                +-------------+            +----------------+|            |                |             |            |                ||            |                |    build    |            |   Multi-stage  ||   Runner   +--------------->+  conatiner  +----------->+     Build      ||            |                |             |            |                ||            |                |             |            |                |+------------+                +------+------+            +----------------+                                     ^                                     |                                     |                                     |                                     |                              +------+------+                              |             |                              |    Cache    |                              |             |                              +-------------+

挂载不管用

后来经过查阅文档,发现 Dockerfile 是有扩展语法的(当然最终我还是没用),具体请见下篇文章(我怕被打死)下面,先说好,下面的内容无法完美的解决上面的问题,目前只是支持了一部分功能,当然未来很可能支持类似 IF ELSE 语法、直接挂载宿主机目录等功能

二、开启 Dockerfile 扩展语法

2.1、开启实验性功能

目前这个扩展语法还处于实验性功能,所以需要配置 dockerd 守护进程,修改如下

ExecStart=/usr/bin/dockerd  -H unix:// \                            --init \                            --live-restore \                            --data-root=/data/docker \                            --experimental \                            --log-driver json-file \                            --log-opt max-size=30m \                            --log-opt max-file=3

主要是 --experimental 参数,参考官方文档同时在 build 前声明 export DOCKER_BUILDKIT=1 变量

2.2、修改 Dockerfile

开启实验性功能后,只需要在 Dockerfile 头部增加 # syntax=docker/dockerfile:experimental 既可;为了保证稳定性,你也可以指定具体的版本号,类似这样

# syntax=docker/dockerfile:1.1.1-experimentalFROM tomcat

2.3、可用的扩展语法

  • RUN --mount=type=bind

这个是默认的挂载模式,这个允许将上下文或者镜像以可都可写/只读模式挂载到 build 容器中,可选参数如下(不翻译了)

OptionDescription
target (required)Mount path.
sourceSource path in the from. Defaults to the root of the from.
fromBuild stage or image name for the root of the source. Defaults to the build context.
rw,readwriteAllow writes on the mount. Written data will be discarded.
  • RUN --mount=type=cache

专用于作为 cache 的挂载位置,一般用于 cache 包管理器的下载等

OptionDescription
idOptional ID to identify separate/different caches
target (required)Mount path.
ro,readonlyRead-only if set.
sharingOne of shared, private, or locked. Defaults to shared. A shared cache mount can be used concurrently by multiple writers. private creates a new mount if there are multiple writers. locked pauses the second writer until the first one releases the mount.
fromBuild stage to use as a base of the cache mount. Defaults to empty directory.
sourceSubpath in the from to mount. Defaults to the root of the from.

Example: cache Go packages

# syntax = docker/dockerfile:experimentalFROM golang...RUN --mount=type=cache,target=/root/.cache/go-build go build ...

Example: cache apt packages

# syntax = docker/dockerfile:experimentalFROM ubuntuRUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cacheRUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \  apt update && apt install -y gcc
  • RUN --mount=type=tmpfs

专用于挂载 tmpfs 的选项

OptionDescription
target (required)Mount path.
  • RUN --mount=type=secret

这个类似 k8s 的 secret,用来挂载一些不想打入镜像,但是构建时想使用的密钥等,例如 docker 的 config.json,S3 的 credentials

OptionDescription
idID of the secret. Defaults to basename of the target path.
targetMount path. Defaults to /run/secrets/ + id.
requiredIf set to true, the instruction errors out when the secret is unavailable. Defaults to false.
modeFile mode for secret file in octal. Default 0400.
uidUser ID for secret file. Default 0.
gidGroup ID for secret file. Default 0.

Example: access to S3

# syntax = docker/dockerfile:experimentalFROM python:3RUN pip install awscliRUN --mount=type=secret,id=aws,target=/root/.aws/credentials aws s3 cp s3://... ...

注意: buildctl 是 BuildKit 的命令,你要测试的话自己换成 docker build 相关参数

$ buildctl build --frontend=dockerfile.v0 --local context=. --local dockerfile=. \  --secret id=aws,src=$HOME/.aws/credentials
  • RUN --mount=type=ssh

允许 build 容器通过 SSH agent 访问 SSH key,并且支持 passphrases

OptionDescription
idID of SSH agent socket or key. Defaults to “default”.
targetSSH agent socket path. Defaults to /run/buildkit/ssh_agent.${N}.
requiredIf set to true, the instruction errors out when the key is unavailable. Defaults to false.
modeFile mode for socket in octal. Default 0600.
uidUser ID for socket. Default 0.
gidGroup ID for socket. Default 0.

Example: access to Gitlab

# syntax = docker/dockerfile:experimentalFROM alpineRUN apk add --no-cache openssh-clientRUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan gitlab.com >> ~/.ssh/known_hostsRUN --mount=type=ssh ssh -q -T git@gitlab.com 2>&1 | tee /hello# "Welcome to GitLab, @GITLAB_USERNAME_ASSOCIATED_WITH_SSHKEY" should be printed here# with the type of build progress is defined as `plain`.
$ eval $(ssh-agent)$ ssh-add ~/.ssh/id_rsa(Input your passphrase here)$ buildctl build --frontend=dockerfile.v0 --local context=. --local dockerfile=. \  --ssh default=$SSH_AUTH_SOCK

你也可以直接使用宿主机目录的 pem 文件,但是带有密码的 pem 目前不支持

目前根据文档测试,当前的挂载类型比如 cache 类型,仅用于 multi-stage 内的挂载,比如你有 2+ 个构建步骤,cache 挂载类型能帮你在各个阶段内共享文件;但是它目前无法解决直接将宿主机目录挂载到 multi-stage 的问题(可以采取些曲线救国方案,但是很不优雅);但是未来还是很有展望的,可以关注一下

]]>
Docker Docker http://mritd.com/2019/05/13/dockerfile-extended-syntax/#disqus_thread
Mac 下调校 Rime http://mritd.com/2019/03/23/oh-my-rime/ http://mritd.com/2019/03/23/oh-my-rime/ Sat, 23 Mar 2019 13:03:43 GMT 由于对国内输入法隐私问题的担忧,决定放弃搜狗等输入法;为了更加 Geek 一些,最终决定了折腾 Rime(鼠须管) 输入法,以下为一些折腾的过程

由于对国内输入法隐私问题的担忧,决定放弃搜狗等输入法;为了更加 Geek 一些,最终决定了折腾 Rime(鼠须管) 输入法,以下为一些折腾的过程

国际惯例先放点图压压惊

example1
example2
example3
example4

一、安装

安装 Rime 没啥好说的,直接从官网下载最新版本的安装包既可;安装完成后配置文件位于 ~/Library/Rime 位置;在进行后续折腾之前我建议还是先 cp -r ~/Library/Rime ~/Library/Rime.bak 备份一下配置文件,以防制后续折腾挂了还可以还原;安装完成以后按 ⌘ + 反引号(~) 切换到 朙月拼音-简化字 既可开启简体中文输入

二、乱码解决

安装完成后在打字时可能出现乱码情况(俗称豆腐块),这是由于 Rime 默认 utf-8 字符集比较大,预选词内会出现生僻字,而 mac 字体内又不包含这些字体,从而导致乱码;解决方案很简单,下载 花园明朝 A、B 两款字体安装既可,安装后重启一下就不会出现乱码了

fonts

三、配置文件

官方并不建议直接修改原始的配置文件,因为输入法更新时会重新覆盖默认配置,可能导致某些自定义配置丢失;推荐作法是创建一系列的 patch 配置,通过类似打补丁替换这种方式来实现无感的增加自定义配置;

由于使用的是 朙月拼音-简化字 输入方案,所以需要创建 luna_pinyin_simp.custom.yaml 等配置文件,后面就是查文档 + 各种 Google 一顿魔改了;目前我将我自己用的配置放在了 Github 上,有需要的可以直接 clone 下来,用里面的配置文件直接覆盖 ~/Library/Rime 下的文件,然后重新部署既可,关于具体配置细节在下面写

四、自定义配色

皮肤配色配置方案位于 squirrel.custom.yaml 配置文件中,我的配置目前是参考搜狗输入法皮肤自己调试的;官方也提供了一些皮肤外观配置,详见 Gist;想要切换皮肤配色只需要修改 style/color_scheme 为相应的皮肤配色名称既可

patch:  show_notifications_when: appropriate          # 状态通知,适当,也可设为全开(always)全关(never)  style/color_scheme: mritd_dark                # 方案命名,不能有空格  preset_color_schemes:    mritd_dark:      name: www.463.com/mritd dark      author: mritd <mritd1234@gmail.com>      horizontal: true                          # 水平排列      inline_preedit: true                      # 单行显示,false双行显示      candidate_format: "%c\u2005%@"            # 用 1/6 em 空格 U+2005 来控制编号 %c 和候选词 %@ 前后的空间。      corner_radius: 5                          # 候选条圆角      hilited_corner_radius: 3                  # 高亮圆角      border_height: 6                          # 窗口边界高度,大于圆角半径才生效      border_width: 6                           # 窗口边界宽度,大于圆角半径才生效      border_color_width: 0      #font_face: "PingFangSC"                   # 候选词字体      font_point: 16                            # 候选字词大小      label_font_point: 14                      # 候选编号大小      text_color: 0xdedddd                      # 拼音行文字颜色,24位色值,16进制,BGR顺序      back_color: 0x4b4b4b                      # 候选条背景色      label_color: 0x888785                     # 预选栏编号颜色      border_color: 0x4b4b4b                    # 边框色      candidate_text_color: 0xffffff            # 预选项文字颜色      hilited_text_color: 0xdedddd              # 高亮拼音 (需要开启内嵌编码)      hilited_back_color: 0x252320              # 高亮拼音 (需要开启内嵌编码)      hilited_candidate_text_color: 0xFFE696    # 第一候选项文字颜色      hilited_candidate_back_color: 0x4b4b4b    # 第一候选项背景背景色      hilited_candidate_label_color: 0xffffff   # 第一候选项编号颜色      comment_text_color: 0xdedddd              # 拼音等提示文字颜色

五、增加自定义快捷字符

快捷字符例如在中文输入法状态下可以直接输入 /dn 来调出特殊符号输入;这些配置位于 luna_pinyin_simp.custom.yamlpunctuator 配置中,我目前自行定义了一些,有需要的可以依葫芦画瓢直接修改

punctuator:    import_preset: symbols    symbols:      "/fs": [½,,¼,,,¾,]      "/dq": [🌍,🌎,🌏,🌐,🌑,🌒,🌓,🌔,🌕,🌖,🌗,🌘,🌙,🌚,🌛,🌜,🌝,🌞,,🌟,🌠,,,,🔥,💧,🌊]      "/jt": [,,,,,,,,,,,,,,🔃,🔄,🔙,🔚,🔛,🔜,🔝]      "/sg": [🍇,🍈,🍉,🍊,🍋,🍌,🍍,🍎,🍏,🍐,🍑,🍒,🍓,🍅,🍆,🌽,🍄,🌰,🍞,🍖,🍗,🍔,🍟,🍕,🍳,🍲,🍱,🍘,🍙,🍚,🍛,🍜,🍝,🍠,🍢,🍣,🍤,🍥,🍡,🍦,🍧,🍨,🍩,🍪,🎂,🍰,🍫,🍬,🍭,🍮,🍯,🍼,🍵,🍶,🍷,🍸,🍹,🍺,🍻,🍴]      "/dw": [🙈,🙉,🙊,🐵,🐒,🐶,🐕,🐩,🐺,🐱,😺,😸,😹,😻,😼,😽,🙀,😿,😾,🐈,🐯,🐅,🐆,🐴,🐎,🐮,🐂,🐃,🐄,🐷,🐖,🐗,🐽,🐏,🐑,🐐,🐪,🐫,🐘,🐭,🐁,🐀,🐹,🐰,🐇,🐻,🐨,🐼,🐾,🐔,🐓,🐣,🐤,🐥,🐦,🐧,🐸,🐊,🐢,🐍,🐲,🐉,🐳,🐋,🐬,🐟,🐠,🐡,🐙,🐚,🐌,🐛,🐜,🐝,🐞,🦋]      "/bq": [😀,😁,😂,😃,😄,😅,😆,😉,😊,😋,😎,😍,😘,😗,😙,😚,😇,😐,😑,😶,😏,😣,😥,😮,😯,😪,😫,😴,😌,😛,😜,😝,😒,😓,😔,😕,😲,😷,😖,😞,😟,😤,😢,😭,😦,😧,😨,😬,😰,😱,😳,😵,😡,😠]      "/ss": [💪,👈,👉,👆,👇,,👌,👍,👎,,👊,👋,👏,👐]      "/dn": [, , , , , , , , , ↩︎, , , , , , , , , ]      "/fh": [©,®,,,,,,,,,,,,,,☑︎,,,,,,,,,,,]      "/xh": [,×,,,,,,,,,,,,]

六、设置输入方案

在第一次按 ⌘ + 反引号(~) 设置输入法时实际上我们可以看到很多的输入方案,而事实上很多方案我们根本用不上;想要删除和修改方案可以调整 default.custom.yaml 中的 schema_list 字段

patch:  menu:    page_size: 8  schema_list:  - schema: luna_pinyin_simp      # 朙月拼音 简化字  - schema: luna_pinyin           # 朙月拼音  - schema: luna_pinyin_fluency   # 语句流#  - schema: double_pinyin         # 自然碼雙拼#  - schema: double_pinyin_flypy   # 小鹤雙拼#  - schema: double_pinyin_pyjj    # 拼音加加双拼#  - schema: wubi_pinyin           # 五笔拼音混合輸入

实际上我只能用上第一个…毕竟写了好几年代码还得看键盘的人也只能这样了…

七、调整特殊键行为

在刚安装完以后发现在中文输入法状态下输入英文,按 shift 键后字符上屏,然后还得回车一下,这就很让我难受…最后找到了这篇 Gist,目前将大写锁定、shift 键调整为了跟搜狗一致的配置,有需要调整的可以自行编辑 default.custom.yaml 中的 ascii_composer/switch_key 部分

# capslock 键切换英文并输出大写ascii_composer/good_old_caps_lock: true# 输入法中英文状态快捷键ascii_composer/switch_key:  Caps_Lock: commit_code  Control_L: noop  Control_R: noop  # 按下左 shift 英文字符直接上屏,不需要再次回车,输入法保持英文状态  Shift_L: commit_code  Shift_R: noop

八、自定义词库

Rime 默认的词库稍为有点弱,我们可以下载一些搜狗词库来进行扩展;不过搜狗词库格式默认是无法解析的,好在有人开发了工具可以方便的将搜狗细胞词库转化为 Rime 的格式(工具点击这里下载);目前该工具只支持 Windows(也有些别人写的 py 脚本啥的,但是我没用),所以词库转换这种操作还得需要一个 Windows 虚拟机;

转换过程很简单,先从搜狗词库下载一系列的 scel 文件,然后批量选中,接着调整一下输入和输出格式点击转换,最后保存成一个 txt 文本

input-setting

convert

光有这个文本还不够,我们要将它塞到词库的 yaml 配置里,所以新建一个词库配置文件 luna_pinyin.sougou.dict.yaml,然后写上头部说明(注意最后三个点后面加一个换行)

# Rime dictionary# encoding: utf-8# 搜狗词库 目前包含如下:# IT计算机 实用IT词汇 亲戚称呼 化学品名 数字时间 数学词汇 淘宝词库 编程语言 软件专业 颜色名称 程序猿词库 开发专用词库 搜狗标准词库# 摄影专业名词 计算机专业词库 计算机词汇大全 保险词汇 最详细的全国地名大全 饮食大全 常见花卉名称 房地产词汇大全 中国传统节日大全 财经金融词汇大全---name: luna_pinyin.sougouversion: "1.0"sort: by_weightuse_preset_vocabulary: true...

接着只需要把生成好的词库 txt 文件内容粘贴到三个点下面既可;但是词库太多的话你会发现这个文本有好几十 M,一般编辑器打开都会卡死,解决这种情况只需要用命令行 cat 一下就行

cat sougou.txt >> luna_pinyin.sougou.dict.yaml

最后修改 luna_pinyin.extended.dict.yaml 中的 import_tables 字段,加入刚刚新建的词库既可

---name: luna_pinyin.extendedversion: "2016.06.26"sort: by_weight  #字典初始排序,可選original或by_weightuse_preset_vocabulary: true#此處爲明月拼音擴充詞庫(基本)默認鏈接載入的詞庫,有朙月拼音官方詞庫、明月拼音擴充詞庫(漢語大詞典)、明月拼音擴充詞庫(詩詞)、明月拼音擴充詞庫(含西文的詞彙)。如果不需要加載某个詞庫請將其用「#」註釋掉。#雙拼不支持 luna_pinyin.cn_en 詞庫,請用戶手動禁用。import_tables:  - luna_pinyin  # 加入搜狗词库  - luna_pinyin.sougou  - luna_pinyin.poetry  - luna_pinyin.cn_en  - luna_pinyin.kaomoji

九、定制特殊单词

由于长期撸码,24 小时离不开命令行,偶尔在中文输入法下输入了一些命令导致汉字直接出现在 terminal 上就很尴尬…这时候我们可以在 luna_pinyin.cn_en.dict.yaml 加入一些我们自己的专属词库,比如这样

---name: luna_pinyin.cn_enversion: "2017.9.13"sort: by_weightuse_preset_vocabulary: true...gitgitlslscdcdpwdpwdgit psgitpskuberneteskuberneteskuberneteskuberkubectlkubectlkubectlkubecdockerdockerdockerdockipvsipvspspsbashbashsourcesourcesourcesourmrm

配置后如果我在中文输入法下输入 git 则会自动匹配 git 这个单词,避免错误的键入中文字符;需要注意的是第一列代表上屏的字符,第二列代表输入的单词,即 “当输入第二列时候选词为第一列”;两列之间要用 tag 制表符隔开,记住不是空格

]]>
Mac Mac Rime http://mritd.com/2019/03/23/oh-my-rime/#disqus_thread
Ubuntu 设置多个源 http://mritd.com/2019/03/19/how-to-set-multiple-apt-mirrors-for-ubuntu/ http://mritd.com/2019/03/19/how-to-set-multiple-apt-mirrors-for-ubuntu/ Tue, 19 Mar 2019 13:43:23 GMT 介绍通过 mirror 方式来设置 Ubuntu 源,从而实现自动切换 apt 源下载 一、源起

使用 Ubuntu 作为生产容器系统好久了,但是 apt 源问题一致有点困扰: **由于众所周知的原因,官方源执行 apt update 等命令会非常慢;而国内有很多镜像服务,但是某些偶尔也会抽风(比如清华大源),最后的结果就是日常修改 apt 源…**Google 查了了好久发现事实上 apt 源是支持 mirror 协议的,从而自动选择可用的一个

二、使用 mirror 协议

废话不说多直接上代码,编辑 /etc/apt/sources.list,替换为如下内容

#------------------------------------------------------------------------------##                            OFFICIAL UBUNTU REPOS                             ##------------------------------------------------------------------------------####### Ubuntu Main Reposdeb mirror://mirrors.ubuntu.com/mirrors.txt bionic main restricted universe multiversedeb-src mirror://mirrors.ubuntu.com/mirrors.txt bionic main restricted universe multiverse###### Ubuntu Update Reposdeb mirror://mirrors.ubuntu.com/mirrors.txt bionic-security main restricted universe multiversedeb mirror://mirrors.ubuntu.com/mirrors.txt bionic-updates main restricted universe multiversedeb mirror://mirrors.ubuntu.com/mirrors.txt bionic-backports main restricted universe multiversedeb-src mirror://mirrors.ubuntu.com/mirrors.txt bionic-security main restricted universe multiversedeb-src mirror://mirrors.ubuntu.com/mirrors.txt bionic-updates main restricted universe multiversedeb-src mirror://mirrors.ubuntu.com/mirrors.txt bionic-backports main restricted universe multiverse

当使用 mirror 协议后,执行 apt update 时会首先通过 http 访问 mirrors.ubuntu.com/mirrors.txt 文本;文本内容实际上就是当前可用的镜像源列表,如下所示

http://ftp.sjtu.edu.cn/ubuntu/http://mirrors.nju.edu.cn/ubuntu/http://mirrors.nwafu.edu.cn/ubuntu/http://mirrors.sohu.com/ubuntu/http://mirrors.aliyun.com/ubuntu/http://mirrors.shu.edu.cn/ubuntu/http://mirrors.cqu.edu.cn/ubuntu/http://mirrors.huaweicloud.com/repository/ubuntu/http://mirrors.cn99.com/ubuntu/http://mirrors.yun-idc.com/ubuntu/http://mirrors.tuna.tsinghua.edu.cn/ubuntu/http://mirrors.ustc.edu.cn/ubuntu/http://mirrors.njupt.edu.cn/ubuntu/http://mirror.lzu.edu.cn/ubuntu/http://archive.ubuntu.com/ubuntu/

得到列表后 apt 会自动选择一个(选择规则暂不清楚,国外有文章说是选择最快的,但是不清楚这个最快是延迟还是网速)进行下载;**同时根据地区不通,官方也提供指定国家的 mirror.txt**,比如中国的实际上可以设置为 mirrors.ubuntu.com/CN.txt(我测试跟官方一样,推测可能是使用了类似 DNS 选优的策略)

三、自定义 mirror 地址

现在已经解决了能同时使用多个源的问题,但是有些时候你会发现源的可用性检测并不是很精准,比如某个源只有 40k 的下载速度…不巧你某个下载还命中了,这就很尴尬;所以有时候我们可能需要自定义 mirror.txt 这个源列表,经过测试证明**只需要开启一个标准的 http server 能返回一个文本即可,不过需要注意只能是 http,而不是 http**;所以我们首先下载一下这个文本,把不想要的删掉;然后弄个 nginx,甚至 python -m http.server 把文本文件暴露出去就可以;我比较懒…扔 CDN 上了: http://oss.link/config/apt-mirrors.txt

关于源的精简,我建议将一些 edu 的删掉,因为敏感时期他们很不稳定;优选阿里云、网易、华为这种大公司的,比较有名的清华大的什么的可以留着,其他的可以考虑都删掉

]]>
Linux Linux http://mritd.com/2019/03/19/how-to-set-multiple-apt-mirrors-for-ubuntu/#disqus_thread
Kubernetes 1.13.4 搭建 http://mritd.com/2019/03/16/set-up-kubernetes-1.13.4-cluster/ http://mritd.com/2019/03/16/set-up-kubernetes-1.13.4-cluster/ Sat, 16 Mar 2019 09:36:52 GMT 年后回来有点懒,也有点忙;1.13 出来好久了,周末还是决定折腾一下吧

年后回来有点懒,也有点忙;1.13 出来好久了,周末还是决定折腾一下吧

一、环境准备

老样子,安装环境为 5 台 Ubuntu 18.04.2 LTS 虚拟机,其他详细信息如下

System OSIP AddressDockerKernelApplication
Ubuntu 18.04.2 LTS192.168.1.5118.09.24.15.0-46-generick8s-master、etcd
Ubuntu 18.04.2 LTS192.168.1.5218.09.24.15.0-46-generick8s-master、etcd
Ubuntu 18.04.2 LTS192.168.1.5318.09.24.15.0-46-generick8s-master、etcd
Ubuntu 18.04.2 LTS192.168.1.5418.09.24.15.0-46-generick8s-node
Ubuntu 18.04.2 LTS192.168.1.5518.09.24.15.0-46-generick8s-node

所有配置生成将在第一个节点上完成,第一个节点与其他节点 root 用户免密码登录,用于分发文件;为了方便搭建弄了一点小脚本,仓库地址 ktool,本文后续所有脚本、配置都可以在此仓库找到;关于 cfssl 等基本工具使用,本文不再阐述

二、安装 Etcd

2.1、生成证书

Etcd 仍然开启 TLS 认证,所以先使用 cfssl 生成相关证书

  • etcd-root-ca-csr.json
{    "CN": "etcd-root-ca",    "key": {        "algo": "rsa",        "size": 4096    },    "names": [        {            "O": "etcd",            "OU": "etcd Security",            "L": "Beijing",            "ST": "Beijing",            "C": "CN"        }    ],    "ca": {        "expiry": "87600h"    }}
  • etcd-gencert.json
{  "signing": {    "default": {        "usages": [          "signing",          "key encipherment",          "server auth",          "client auth"        ],        "expiry": "87600h"    }  }}
  • etcd-csr.json
{    "key": {        "algo": "rsa",        "size": 2048    },    "names": [        {            "O": "etcd",            "OU": "etcd Security",            "L": "Beijing",            "ST": "Beijing",            "C": "CN"        }    ],    "CN": "etcd",    "hosts": [        "127.0.0.1",        "localhost",        "192.168.1.51",        "192.168.1.52",        "192.168.1.53"    ]}

接下来执行生成即可;我建议在生产环境在证书内预留几个 IP,已防止意外故障迁移时还需要重新生成证书;证书默认期限为 10 年(包括 CA 证书),有需要加强安全性的可以适当减小

cfssl gencert --initca=true etcd-root-ca-csr.json | cfssljson --bare etcd-root-cacfssl gencert --ca etcd-root-ca.pem --ca-key etcd-root-ca-key.pem --config etcd-gencert.json etcd-csr.json | cfssljson --bare etcd

2.2、安装 Etcd

2.2.1、安装脚本

安装 Etcd 只需要将二进制文件放在可执行目录下,然后修改配置增加 systemd service 配置文件即可;为了安全性起见最好使用单独的用户启动 Etcd

#!/bin/bashset -eETCD_DEFAULT_VERSION="3.3.12"if [ "$1" != "" ]; then  ETCD_VERSION=$1else  echo -e "\033[33mWARNING: ETCD_VERSION is blank,use default version: ${ETCD_DEFAULT_VERSION}\033[0m"  ETCD_VERSION=${ETCD_DEFAULT_VERSION}fi# 下载 Etcd 二进制文件function download(){    if [ ! -f "etcd-v${ETCD_VERSION}-linux-amd64.tar.gz" ]; then        wget http://github.com/coreos/etcd/releases/download/v${ETCD_VERSION}/etcd-v${ETCD_VERSION}-linux-amd64.tar.gz        tar -zxvf etcd-v${ETCD_VERSION}-linux-amd64.tar.gz    fi}# 为 Etcd 创建单独的用户function preinstall(){getent group etcd >/dev/null || groupadd -r etcdgetent passwd etcd >/dev/null || useradd -r -g etcd -d /var/lib/etcd -s /sbin/nologin -c "etcd user" etcd}# 安装(复制文件)function install(){    # 释放 Etcd 二进制文件    echo -e "\033[32mINFO: Copy etcd...\033[0m"    tar -zxvf etcd-v${ETCD_VERSION}-linux-amd64.tar.gz    cp etcd-v${ETCD_VERSION}-linux-amd64/etcd* /usr/local/bin    rm -rf etcd-v${ETCD_VERSION}-linux-amd64    # 复制 配置文件 到 /etc/etcd(目录内文件结构在下面)    echo -e "\033[32mINFO: Copy etcd config...\033[0m"    cp -r conf /etc/etcd    chown -R etcd:etcd /etc/etcd    chmod -R 755 /etc/etcd/ssl    # 复制 systemd service 配置    echo -e "\033[32mINFO: Copy etcd systemd config...\033[0m"    cp systemd/*.service /lib/systemd/system    systemctl daemon-reload}# 创建 Etcd 存储目录(如需要更改,请求改 /etc/etcd/etcd.conf 配置文件)function postinstall(){    if [ ! -d "/var/lib/etcd" ]; then        mkdir /var/lib/etcd        chown -R etcd:etcd /var/lib/etcd    fi}# 依次执行downloadpreinstallinstallpostinstall

2.2.2、配置文件

关于配置文件目录结构如下(请自行复制证书)

conf├── etcd.conf├── etcd.conf.cluster.example├── etcd.conf.single.example└── ssl    ├── etcd-key.pem    ├── etcd.pem    ├── etcd-root-ca-key.pem    └── etcd-root-ca.pem1 directory, 7 files
  • etcd.conf
# [member]ETCD_NAME=etcd1ETCD_DATA_DIR="/var/lib/etcd/data"ETCD_WAL_DIR="/var/lib/etcd/wal"ETCD_SNAPSHOT_COUNT="100"ETCD_HEARTBEAT_INTERVAL="100"ETCD_ELECTION_TIMEOUT="1000"ETCD_LISTEN_PEER_URLS="http://192.168.1.51:2380"ETCD_LISTEN_CLIENT_URLS="http://192.168.1.51:2379,http://127.0.0.1:2379"ETCD_MAX_SNAPSHOTS="5"ETCD_MAX_WALS="5"#ETCD_CORS=""# [cluster]ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.1.51:2380"# if you use different ETCD_NAME (e.g. test), set ETCD_INITIAL_CLUSTER value for this name, i.e. "test=http://..."ETCD_INITIAL_CLUSTER="etcd1=http://192.168.1.51:2380,etcd2=http://192.168.1.52:2380,etcd3=http://192.168.1.53:2380"ETCD_INITIAL_CLUSTER_STATE="new"ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"ETCD_ADVERTISE_CLIENT_URLS="http://192.168.1.51:2379"#ETCD_DISCOVERY=""#ETCD_DISCOVERY_SRV=""#ETCD_DISCOVERY_FALLBACK="proxy"#ETCD_DISCOVERY_PROXY=""#ETCD_STRICT_RECONFIG_CHECK="false"#ETCD_AUTO_COMPACTION_RETENTION="0"# [proxy]#ETCD_PROXY="off"#ETCD_PROXY_FAILURE_WAIT="5000"#ETCD_PROXY_REFRESH_INTERVAL="30000"#ETCD_PROXY_DIAL_TIMEOUT="1000"#ETCD_PROXY_WRITE_TIMEOUT="5000"#ETCD_PROXY_READ_TIMEOUT="0"# [security]ETCD_CERT_FILE="/etc/etcd/ssl/etcd.pem"ETCD_KEY_FILE="/etc/etcd/ssl/etcd-key.pem"ETCD_CLIENT_CERT_AUTH="true"ETCD_TRUSTED_CA_FILE="/etc/etcd/ssl/etcd-root-ca.pem"ETCD_AUTO_TLS="true"ETCD_PEER_CERT_FILE="/etc/etcd/ssl/etcd.pem"ETCD_PEER_KEY_FILE="/etc/etcd/ssl/etcd-key.pem"ETCD_PEER_CLIENT_CERT_AUTH="true"ETCD_PEER_TRUSTED_CA_FILE="/etc/etcd/ssl/etcd-root-ca.pem"ETCD_PEER_AUTO_TLS="true"# [logging]#ETCD_DEBUG="false"# examples for -log-package-levels etcdserver=WARNING,security=DEBUG#ETCD_LOG_PACKAGE_LEVELS=""
  • etcd.service
[Unit]Description=Etcd ServerAfter=network.targetAfter=network-online.targetWants=network-online.target[Service]Type=notifyWorkingDirectory=/var/lib/etcd/EnvironmentFile=-/etc/etcd/etcd.confUser=etcd# set GOMAXPROCS to number of processorsExecStart=/bin/bash -c "GOMAXPROCS=$(nproc) /usr/local/bin/etcd --name=\"${ETCD_NAME}\" --data-dir=\"${ETCD_DATA_DIR}\" --listen-client-urls=\"${ETCD_LISTEN_CLIENT_URLS}\""Restart=on-failureLimitNOFILE=65536[Install]WantedBy=multi-user.target

最后三台机器依次修改 IPETCD_NAME 然后启动即可,**生产环境请不要忘记修改集群 Token 为真实随机字符串 (ETCD_INITIAL_CLUSTER_TOKEN 变量)**启动后可以通过以下命令测试集群联通性

docker1.node ➜  ~ export ETCDCTL_API=3docker1.node ➜  ~ etcdctl member list238b72cdd26e304f, started, etcd2, http://192.168.1.52:2380, http://192.168.1.52:23798034142cf01c5d1c, started, etcd3, http://192.168.1.53:2380, http://192.168.1.53:23798da171dbef9ded69, started, etcd1, http://192.168.1.51:2380, http://192.168.1.51:2379

三、安装 Kubernetes

3.1、生成证书及配置

3.1.1、生成证书

新版本已经越来越趋近全面 TLS + RBAC 配置,所以本次安装将会启动大部分 TLS + RBAC 配置,包括 kube-controler-managerkube-scheduler 组件不再连接本地 kube-apiserver 的 8080 非认证端口,kubelet 等组件 API 端点关闭匿名访问,启动 RBAC 认证等;为了满足这些认证,需要签署以下证书

  • k8s-root-ca-csr.json 集群 CA 根证书
{    "CN": "kubernetes",    "key": {        "algo": "rsa",        "size": 4096    },    "names": [        {            "C": "CN",            "ST": "BeiJing",            "L": "BeiJing",            "O": "kubernetes",            "OU": "System"        }    ],    "ca": {        "expiry": "87600h"    }}
  • k8s-gencert.json 用于生成其他证书的标准配置
{    "signing": {        "default": {            "expiry": "87600h"        },        "profiles": {            "kubernetes": {                "usages": [                    "signing",                    "key encipherment",                    "server auth",                    "client auth"                ],                "expiry": "87600h"            }        }    }}
  • kube-apiserver-csr.json apiserver TLS 认证端口需要的证书
{    "CN": "kubernetes",    "hosts": [        "127.0.0.1",        "10.254.0.1",        "localhost",        "*.master.kubernetes.node",        "kubernetes",        "kubernetes.default",        "kubernetes.default.svc",        "kubernetes.default.svc.cluster",        "kubernetes.default.svc.cluster.local"    ],    "key": {        "algo": "rsa",        "size": 2048    },    "names": [        {            "C": "CN",            "ST": "BeiJing",            "L": "BeiJing",            "O": "kubernetes",            "OU": "System"        }    ]}
  • kube-controller-manager-csr.json controller manager 连接 apiserver 需要使用的证书,同时本身 10257 端口也会使用此证书
{  "CN": "system:kube-controller-manager",  "hosts": [    "127.0.0.1",    "localhost",    "*.master.kubernetes.node"  ],  "key": {    "algo": "rsa",    "size": 2048  },  "names": [    {      "C": "CN",      "ST": "BeiJing",      "L": "BeiJing",      "O": "system:kube-controller-manager",      "OU": "System"    }  ]}
  • kube-scheduler-csr.json scheduler 连接 apiserver 需要使用的证书,同时本身 10259 端口也会使用此证书
{  "CN": "system:kube-scheduler",  "hosts": [    "127.0.0.1",    "localhost",    "*.master.kubernetes.node"  ],  "key": {    "algo": "rsa",    "size": 2048  },  "names": [    {      "C": "CN",      "ST": "BeiJing",      "L": "BeiJing",      "O": "system:kube-scheduler",      "OU": "System"    }  ]}
  • kube-proxy-csr.json proxy 组件连接 apiserver 需要使用的证书
{    "CN": "system:kube-proxy",    "hosts": [],    "key": {        "algo": "rsa",        "size": 2048    },    "names": [        {            "C": "CN",            "ST": "BeiJing",            "L": "BeiJing",            "O": "system:kube-proxy",            "OU": "System"        }    ]}
  • kubelet-api-admin-csr.json apiserver 反向连接 kubelet 组件 10250 端口需要使用的证书(例如执行 kubectl logs)
{    "CN": "system:kubelet-api-admin",    "hosts": [],    "key": {        "algo": "rsa",        "size": 2048    },    "names": [        {            "C": "CN",            "ST": "BeiJing",            "L": "BeiJing",            "O": "system:kubelet-api-admin",            "OU": "System"        }    ]}
  • admin-csr.json 集群管理员(kubectl)连接 apiserver 需要使用的证书
{    "CN": "system:masters",    "hosts": [],    "key": {        "algo": "rsa",        "size": 2048    },    "names": [        {            "C": "CN",            "ST": "BeiJing",            "L": "BeiJing",            "O": "system:masters",            "OU": "System"        }    ]}

注意: 请不要修改证书配置的 CNO 字段,这两个字段名称比较特殊,大多数为 system: 开头,实际上是为了匹配 RBAC 规则,具体请参考 Default Roles and Role Bindings

最后使用如下命令生成即可:

cfssl gencert --initca=true k8s-root-ca-csr.json | cfssljson --bare k8s-root-cafor targetName in kube-apiserver kube-controller-manager kube-scheduler kube-proxy kubelet-api-admin admin; do    cfssl gencert --ca k8s-root-ca.pem --ca-key k8s-root-ca-key.pem --config k8s-gencert.json --profile kubernetes $targetName-csr.json | cfssljson --bare $targetNamedone

3.1.2、生成配置文件

集群搭建需要预先生成一系列配置文件,生成配置需要预先安装 kubectl 命令,请自行根据文档安装 Install kubectl binary using curl;其中配置文件及其作用如下:

  • bootstrap.kubeconfig kubelet TLS Bootstarp 引导阶段需要使用的配置文件
  • kube-controller-manager.kubeconfig controller manager 组件开启安全端口及 RBAC 认证所需配置
  • kube-scheduler.kubeconfig scheduler 组件开启安全端口及 RBAC 认证所需配置
  • kube-proxy.kubeconfig proxy 组件连接 apiserver 所需配置文件
  • audit-policy.yaml apiserver RBAC 审计日志配置文件
  • bootstrap.secret.yaml kubelet TLS Bootstarp 引导阶段使用 Bootstrap Token 方式引导,需要预先创建此 Token

生成这些配置文件的脚本如下

# 指定 apiserver 地址KUBE_APISERVER="http://127.0.0.1:6443"# 生成 Bootstrap TokenBOOTSTRAP_TOKEN_ID=$(head -c 6 /dev/urandom | md5sum | head -c 6)BOOTSTRAP_TOKEN_SECRET=$(head -c 16 /dev/urandom | md5sum | head -c 16)BOOTSTRAP_TOKEN="${BOOTSTRAP_TOKEN_ID}.${BOOTSTRAP_TOKEN_SECRET}"echo "Bootstrap Tokne: ${BOOTSTRAP_TOKEN}"# 生成 kubelet tls bootstrap 配置echo "Create kubelet bootstrapping kubeconfig..."kubectl config set-cluster kubernetes \  --certificate-authority=k8s-root-ca.pem \  --embed-certs=true \  --server=${KUBE_APISERVER} \  --kubeconfig=bootstrap.kubeconfigkubectl config set-credentials "system:bootstrap:${BOOTSTRAP_TOKEN_ID}" \  --token=${BOOTSTRAP_TOKEN} \  --kubeconfig=bootstrap.kubeconfigkubectl config set-context default \  --cluster=kubernetes \  --user="system:bootstrap:${BOOTSTRAP_TOKEN_ID}" \  --kubeconfig=bootstrap.kubeconfigkubectl config use-context default --kubeconfig=bootstrap.kubeconfig# 生成 kube-controller-manager 配置文件echo "Create kube-controller-manager kubeconfig..."kubectl config set-cluster kubernetes \  --certificate-authority=k8s-root-ca.pem \  --embed-certs=true \  --server=${KUBE_APISERVER} \  --kubeconfig=kube-controller-manager.kubeconfigkubectl config set-credentials "system:kube-controller-manager" \  --client-certificate=kube-controller-manager.pem \  --client-key=kube-controller-manager-key.pem \  --embed-certs=true \  --kubeconfig=kube-controller-manager.kubeconfigkubectl config set-context default \  --cluster=kubernetes \  --user=system:kube-controller-manager \  --kubeconfig=kube-controller-manager.kubeconfigkubectl config use-context default --kubeconfig=kube-controller-manager.kubeconfig # 生成 kube-scheduler 配置文件echo "Create kube-scheduler kubeconfig..."kubectl config set-cluster kubernetes \  --certificate-authority=k8s-root-ca.pem \  --embed-certs=true \  --server=${KUBE_APISERVER} \  --kubeconfig=kube-scheduler.kubeconfigkubectl config set-credentials "system:kube-scheduler" \  --client-certificate=kube-scheduler.pem \  --client-key=kube-scheduler-key.pem \  --embed-certs=true \  --kubeconfig=kube-scheduler.kubeconfigkubectl config set-context default \  --cluster=kubernetes \  --user=system:kube-scheduler \  --kubeconfig=kube-scheduler.kubeconfigkubectl config use-context default --kubeconfig=kube-scheduler.kubeconfig # 生成 kube-proxy 配置文件echo "Create kube-proxy kubeconfig..."kubectl config set-cluster kubernetes \  --certificate-authority=k8s-root-ca.pem \  --embed-certs=true \  --server=${KUBE_APISERVER} \  --kubeconfig=kube-proxy.kubeconfigkubectl config set-credentials "system:kube-proxy" \  --client-certificate=kube-proxy.pem \  --client-key=kube-proxy-key.pem \  --embed-certs=true \  --kubeconfig=kube-proxy.kubeconfigkubectl config set-context default \  --cluster=kubernetes \  --user=system:kube-proxy \  --kubeconfig=kube-proxy.kubeconfigkubectl config use-context default --kubeconfig=kube-proxy.kubeconfig # 生成 apiserver RBAC 审计配置文件 cat >> audit-policy.yaml <<EOF# Log all requests at the Metadata level.apiVersion: audit.k8s.io/v1kind: Policyrules:- level: MetadataEOF# 生成 tls bootstrap token secret 配置文件cat >> bootstrap.secret.yaml <<EOFapiVersion: v1kind: Secretmetadata:  # Name MUST be of form "bootstrap-token-<token id>"  name: bootstrap-token-${BOOTSTRAP_TOKEN_ID}  namespace: kube-system# Type MUST be 'bootstrap.kubernetes.io/token'type: bootstrap.kubernetes.io/tokenstringData:  # Human readable description. Optional.  description: "The default bootstrap token."  # Token ID and secret. Required.  token-id: ${BOOTSTRAP_TOKEN_ID}  token-secret: ${BOOTSTRAP_TOKEN_SECRET}  # Expiration. Optional.  expiration: $(date -d'+2 day' -u +"%Y-%m-%dT%H:%M:%SZ")  # Allowed usages.  usage-bootstrap-authentication: "true"  usage-bootstrap-signing: "true"  # Extra groups to authenticate the token as. Must start with "system:bootstrappers:"#  auth-extra-groups: system:bootstrappers:worker,system:bootstrappers:ingressEOF

3.2、处理 ipvs 及依赖

新版本目前 kube-proxy 组件全部采用 ipvs 方式负载,所以为了 kube-proxy 能正常工作需要预先处理一下 ipvs 配置以及相关依赖(每台 node 都要处理)

cat >> /etc/sysctl.conf <<EOFnet.ipv4.ip_forward=1net.bridge.bridge-nf-call-iptables=1net.bridge.bridge-nf-call-ip6tables=1EOFsysctl -pcat >> /etc/modules <<EOFip_vsip_vs_lcip_vs_wlcip_vs_rrip_vs_wrrip_vs_lblcip_vs_lblcrip_vs_dhip_vs_ship_vs_foip_vs_nqip_vs_sedip_vs_ftpEOFapt install -y conntrack ipvsadm

3.3、部署 Master

3.3.1、安装脚本

master 节点上需要三个组件: kube-apiserverkube-controller-managerkube-scheduler

安装流程整体为以下几步

  • 创建单独的 kube 用户
  • 复制相关二进制文件到 /usr/bin,可以采用 all in onehyperkube
  • 复制配置文件到 /etc/kubernetes
  • 复制证书文件到 /etc/kubernetes/ssl
  • 修改配置并启动

安装脚本如下所示:

KUBE_DEFAULT_VERSION="1.13.4"if [ "$1" != "" ]; then  KUBE_VERSION=$1else  echo -e "\033[33mWARNING: KUBE_VERSION is blank,use default version: ${KUBE_DEFAULT_VERSION}\033[0m"  KUBE_VERSION=${KUBE_DEFAULT_VERSION}fi# 下载 hyperkubefunction download_k8s(){    if [ ! -f "hyperkube_v${KUBE_VERSION}" ]; then        wget http://storage.googleapis.com/kubernetes-release/release/v${KUBE_VERSION}/bin/linux/amd64/hyperkube -O hyperkube_v${KUBE_VERSION}        chmod +x hyperkube_v${KUBE_VERSION}    fi}# 创建专用用户 kubefunction preinstall(){    getent group kube >/dev/null || groupadd -r kube    getent passwd kube >/dev/null || useradd -r -g kube -d / -s /sbin/nologin -c "Kubernetes user" kube}# 复制可执行文件和配置以及证书function install_k8s(){    echo -e "\033[32mINFO: Copy hyperkube...\033[0m"    cp hyperkube_v${KUBE_VERSION} /usr/bin/hyperkube    echo -e "\033[32mINFO: Create symbolic link...\033[0m"    (cd /usr/bin && hyperkube --make-symlinks)    echo -e "\033[32mINFO: Copy kubernetes config...\033[0m"    cp -r conf /etc/kubernetes    if [ -d "/etc/kubernetes/ssl" ]; then        chown -R kube:kube /etc/kubernetes/ssl    fi    echo -e "\033[32mINFO: Copy kubernetes systemd config...\033[0m"    cp systemd/*.service /lib/systemd/system    systemctl daemon-reload}# 创建必要的目录并修改权限function postinstall(){    if [ ! -d "/var/log/kube-audit" ]; then        mkdir /var/log/kube-audit    fi        if [ ! -d "/var/lib/kubelet" ]; then        mkdir /var/lib/kubelet    fi    if [ ! -d "/usr/libexec" ]; then        mkdir /usr/libexec    fi    chown -R kube:kube /etc/kubernetes /var/log/kube-audit /var/lib/kubelet /usr/libexec}# 执行download_k8spreinstallinstall_k8spostinstall

hyperkube 是一个多合一的可执行文件,通过 --make-symlinks 会在当前目录生成 kubernetes 各个组件的软连接

被复制的 conf 目录结构如下(最终被复制到 /etc/kubernetes)

.├── apiserver├── audit-policy.yaml├── bootstrap.kubeconfig├── bootstrap.secret.yaml├── controller-manager├── kube-controller-manager.kubeconfig├── kubelet├── kube-proxy.kubeconfig├── kube-scheduler.kubeconfig├── proxy├── scheduler└── ssl    ├── admin-key.pem    ├── admin.pem    ├── k8s-root-ca-key.pem    ├── k8s-root-ca.pem    ├── kube-apiserver-key.pem    ├── kube-apiserver.pem    ├── kube-controller-manager-key.pem    ├── kube-controller-manager.pem    ├── kubelet-api-admin-key.pem    ├── kubelet-api-admin.pem    ├── kube-proxy-key.pem    ├── kube-proxy.pem    ├── kube-scheduler-key.pem    └── kube-scheduler.pem1 directory, 25 files

3.3.2、配置文件

以下为相关配置文件内容

systemd 配置如下

  • kube-apiserver.service
[Unit]Description=Kubernetes API ServerDocumentation=http://github.com/GoogleCloudPlatform/kubernetesAfter=network.targetAfter=etcd.service[Service]EnvironmentFile=-/etc/kubernetes/apiserverUser=kubeExecStart=/usr/bin/kube-apiserver \    $KUBE_LOGTOSTDERR \    $KUBE_LOG_LEVEL \    $KUBE_ETCD_SERVERS \    $KUBE_API_ADDRESS \    $KUBE_API_PORT \    $KUBELET_PORT \    $KUBE_ALLOW_PRIV \    $KUBE_SERVICE_ADDRESSES \    $KUBE_ADMISSION_CONTROL \    $KUBE_API_ARGSRestart=on-failureType=notifyLimitNOFILE=65536[Install]WantedBy=multi-user.target
  • kube-controller-manager.service
[Unit]Description=Kubernetes Controller ManagerDocumentation=http://github.com/GoogleCloudPlatform/kubernetes[Service]EnvironmentFile=-/etc/kubernetes/controller-managerUser=kubeExecStart=/usr/bin/kube-controller-manager \    $KUBE_LOGTOSTDERR \    $KUBE_LOG_LEVEL \    $KUBE_MASTER \    $KUBE_CONTROLLER_MANAGER_ARGSRestart=on-failureLimitNOFILE=65536[Install]WantedBy=multi-user.target
  • kube-scheduler.service
[Unit]Description=Kubernetes Scheduler PluginDocumentation=http://github.com/GoogleCloudPlatform/kubernetes[Service]EnvironmentFile=-/etc/kubernetes/schedulerUser=kubeExecStart=/usr/bin/kube-scheduler \    $KUBE_LOGTOSTDERR \    $KUBE_LOG_LEVEL \    $KUBE_MASTER \    $KUBE_SCHEDULER_ARGSRestart=on-failureLimitNOFILE=65536[Install]WantedBy=multi-user.target

核心配置文件

  • apiserver
#### kubernetes system config## The following values are used to configure the kube-apiserver## The address on the local server to listen to.KUBE_API_ADDRESS="--advertise-address=192.168.1.51 --bind-address=0.0.0.0"# The port on the local server to listen on.KUBE_API_PORT="--secure-port=6443"# Port minions listen on# KUBELET_PORT="--kubelet-port=10250"# Comma separated list of nodes in the etcd clusterKUBE_ETCD_SERVERS="--etcd-servers=http://192.168.1.51:2379,http://192.168.1.52:2379,http://192.168.1.53:2379"# Address range to use for servicesKUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=10.254.0.0/16"# default admission control policiesKUBE_ADMISSION_CONTROL="--enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,Priority,ResourceQuota"# Add your own!KUBE_API_ARGS=" --allow-privileged=true \                --anonymous-auth=false \                --alsologtostderr \                --apiserver-count=3 \                --audit-log-maxage=30 \                --audit-log-maxbackup=3 \                --audit-log-maxsize=100 \                --audit-log-path=/var/log/kube-audit/audit.log \                --audit-policy-file=/etc/kubernetes/audit-policy.yaml \                --authorization-mode=Node,RBAC \                --client-ca-file=/etc/kubernetes/ssl/k8s-root-ca.pem \                --enable-bootstrap-token-auth \                --enable-garbage-collector \                --enable-logs-handler \                --endpoint-reconciler-type=lease \                --etcd-cafile=/etc/etcd/ssl/etcd-root-ca.pem \                --etcd-certfile=/etc/etcd/ssl/etcd.pem \                --etcd-keyfile=/etc/etcd/ssl/etcd-key.pem \                --etcd-compaction-interval=0s \                --event-ttl=168h0m0s \                --kubelet-http=true \                --kubelet-certificate-authority=/etc/kubernetes/ssl/k8s-root-ca.pem \                --kubelet-client-certificate=/etc/kubernetes/ssl/kubelet-api-admin.pem \                --kubelet-client-key=/etc/kubernetes/ssl/kubelet-api-admin-key.pem \                --kubelet-timeout=3s \                --runtime-config=api/all=true \                --service-node-port-range=30000-50000 \                --service-account-key-file=/etc/kubernetes/ssl/k8s-root-ca.pem \                --tls-cert-file=/etc/kubernetes/ssl/kube-apiserver.pem \                --tls-private-key-file=/etc/kubernetes/ssl/kube-apiserver-key.pem \                --v=2"

配置解释:

选项作用
--client-ca-file定义客户端 CA
--endpoint-reconciler-typemaster endpoint 策略
--kubelet-client-certificate--kubelet-client-keymaster 反向连接 kubelet 使用的证书
--service-account-key-fileservice account 签名 key(用于有效性验证)
--tls-cert-file--tls-private-key-filemaster apiserver 6443 端口证书
  • controller-manager
#### The following values are used to configure the kubernetes controller-manager# defaults from config and apiserver should be adequate# Add your own!KUBE_CONTROLLER_MANAGER_ARGS="  --address=127.0.0.1 \                                --authentication-kubeconfig=/etc/kubernetes/kube-controller-manager.kubeconfig \                                --authorization-kubeconfig=/etc/kubernetes/kube-controller-manager.kubeconfig \                                --bind-address=0.0.0.0 \                                --cluster-name=kubernetes \                                --cluster-signing-cert-file=/etc/kubernetes/ssl/k8s-root-ca.pem \                                --cluster-signing-key-file=/etc/kubernetes/ssl/k8s-root-ca-key.pem \                                --client-ca-file=/etc/kubernetes/ssl/k8s-root-ca.pem \                                --controllers=*,bootstrapsigner,tokencleaner \                                --deployment-controller-sync-period=10s \                                --experimental-cluster-signing-duration=87600h0m0s \                                --enable-garbage-collector=true \                                --kubeconfig=/etc/kubernetes/kube-controller-manager.kubeconfig \                                --leader-elect=true \                                --node-monitor-grace-period=20s \                                --node-monitor-period=5s \                                --port=10252 \                                --pod-eviction-timeout=2m0s \                                --requestheader-client-ca-file=/etc/kubernetes/ssl/k8s-root-ca.pem \                                --terminated-pod-gc-threshold=50 \                                --tls-cert-file=/etc/kubernetes/ssl/kube-controller-manager.pem \                                --tls-private-key-file=/etc/kubernetes/ssl/kube-controller-manager-key.pem \                                --root-ca-file=/etc/kubernetes/ssl/k8s-root-ca.pem \                                --secure-port=10257 \                                --service-cluster-ip-range=10.254.0.0/16 \                                --service-account-private-key-file=/etc/kubernetes/ssl/k8s-root-ca-key.pem \                                --use-service-account-credentials=true \                                --v=2"

controller manager 将不安全端口 10252 绑定到 127.0.0.1 确保 kuebctl get cs 有正确返回;将安全端口 10257 绑定到 0.0.0.0 公开,提供服务调用;由于 controller manager 开始连接 apiserver 的 6443 认证端口,所以需要 --use-service-account-credentials 选项来让 controller manager 创建单独的 service account(默认 system:kube-controller-manager 用户没有那么高权限)

  • scheduler
#### kubernetes scheduler config# default config should be adequate# Add your own!KUBE_SCHEDULER_ARGS="   --address=127.0.0.1 \                        --authentication-kubeconfig=/etc/kubernetes/kube-scheduler.kubeconfig \                        --authorization-kubeconfig=/etc/kubernetes/kube-scheduler.kubeconfig \                        --bind-address=0.0.0.0 \                        --client-ca-file=/etc/kubernetes/ssl/k8s-root-ca.pem \                        --kubeconfig=/etc/kubernetes/kube-scheduler.kubeconfig \                        --requestheader-client-ca-file=/etc/kubernetes/ssl/k8s-root-ca.pem \                        --secure-port=10259 \                        --leader-elect=true \                        --port=10251 \                        --tls-cert-file=/etc/kubernetes/ssl/kube-scheduler.pem \                        --tls-private-key-file=/etc/kubernetes/ssl/kube-scheduler-key.pem \                        --v=2"

shceduler 同 controller manager 一样将不安全端口绑定在本地,安全端口对外公开

最后在三台节点上调整一下 IP 配置,启动即可

3.4、部署 Node

3.4.1、安装脚本

node 安装与 master 安装过程一致,这里不再阐述

3.4.2、配置文件

systemd 配置文件

  • kubelet.service
[Unit]Description=Kubernetes Kubelet ServerDocumentation=http://github.com/GoogleCloudPlatform/kubernetesAfter=docker.serviceRequires=docker.service[Service]WorkingDirectory=/var/lib/kubeletEnvironmentFile=-/etc/kubernetes/kubeletExecStart=/usr/bin/kubelet \    $KUBE_LOGTOSTDERR \    $KUBE_LOG_LEVEL \    $KUBELET_API_SERVER \    $KUBELET_ADDRESS \    $KUBELET_PORT \    $KUBELET_HOSTNAME \    $KUBE_ALLOW_PRIV \    $KUBELET_ARGSRestart=on-failureKillMode=process[Install]WantedBy=multi-user.target
  • kube-proxy.service
[Unit]Description=Kubernetes Kube-Proxy ServerDocumentation=http://github.com/GoogleCloudPlatform/kubernetesAfter=network.target[Service]EnvironmentFile=-/etc/kubernetes/proxyExecStart=/usr/bin/kube-proxy \    $KUBE_LOGTOSTDERR \    $KUBE_LOG_LEVEL \    $KUBE_MASTER \    $KUBE_PROXY_ARGSRestart=on-failureLimitNOFILE=65536[Install]WantedBy=multi-user.target

核心配置文件

  • kubelet
#### kubernetes kubelet (minion) config# The address for the info server to serve on (set to 0.0.0.0 or "" for all interfaces)KUBELET_ADDRESS="--node-ip=192.168.1.54"# The port for the info server to serve on# KUBELET_PORT="--port=10250"# You may leave this blank to use the actual hostnameKUBELET_HOSTNAME="--hostname-override=docker4.node"# location of the api-server# KUBELET_API_SERVER=""# Add your own!KUBELET_ARGS="  --address=0.0.0.0 \                --allow-privileged \                --anonymous-auth=false \                --authorization-mode=Webhook \                --bootstrap-kubeconfig=/etc/kubernetes/bootstrap.kubeconfig \                --client-ca-file=/etc/kubernetes/ssl/k8s-root-ca.pem \                --network-plugin=cni \                --cgroup-driver=cgroupfs \                --cert-dir=/etc/kubernetes/ssl \                --cluster-dns=10.254.0.2 \                --cluster-domain=cluster.local \                --cni-conf-dir=/etc/cni/net.d \                --eviction-soft=imagefs.available<15%,memory.available<512Mi,nodefs.available<15%,nodefs.inodesFree<10% \                --eviction-soft-grace-period=imagefs.available=3m,memory.available=1m,nodefs.available=3m,nodefs.inodesFree=1m \                --eviction-hard=imagefs.available<10%,memory.available<256Mi,nodefs.available<10%,nodefs.inodesFree<5% \                --eviction-max-pod-grace-period=30 \                --image-gc-high-threshold=80 \                --image-gc-low-threshold=70 \                --image-pull-progress-deadline=30s \                --kube-reserved=cpu=500m,memory=512Mi,ephemeral-storage=1Gi \                --kubeconfig=/etc/kubernetes/kubelet.kubeconfig \                --max-pods=100 \                --minimum-image-ttl-duration=720h0m0s \                --node-labels=node.kubernetes.io/k8s-node=true \                --pod-infra-container-image=gcr.azk8s.cn/google_containers/pause-amd64:3.1 \                --port=10250 \                --read-only-port=0 \                --rotate-certificates \                --rotate-server-certificates \                --resolv-conf=/run/systemd/resolve/resolv.conf \                --system-reserved=cpu=500m,memory=512Mi,ephemeral-storage=1Gi \                --fail-swap-on=false \                --v=2"

当 kubelet 组件设置了 --rotate-certificates--rotate-server-certificates 后会自动更新其使用的相关证书,同时指定 --authorization-mode=Webhook10250 端口 RBAC 授权将会委托给 apiserver

  • proxy
#### kubernetes proxy config# default config should be adequate# Add your own!KUBE_PROXY_ARGS="   --bind-address=0.0.0.0 \                    --cleanup-ipvs=true \                    --cluster-cidr=10.254.0.0/16 \                    --hostname-override=docker4.node \                    --healthz-bind-address=0.0.0.0 \                    --healthz-port=10256 \                    --masquerade-all=true \                    --proxy-mode=ipvs \                    --ipvs-min-sync-period=5s \                    --ipvs-sync-period=5s \                    --ipvs-scheduler=wrr \                    --kubeconfig=/etc/kubernetes/kube-proxy.kubeconfig \                    --logtostderr=true \                    --v=2"

由于 kubelet 组件是采用 TLS Bootstrap 启动,所以需要预先创建相关配置

# 创建用于 tls bootstrap 的 token secretkubectl create -f bootstrap.secret.yaml# 为了能让 kubelet 实现自动更新证书,需要配置相关 clusterrolebinding# 允许 kubelet tls bootstrap 创建 csr 请求kubectl create clusterrolebinding create-csrs-for-bootstrapping \    --clusterrole=system:node-bootstrapper \    --group=system:bootstrappers# 自动批准 system:bootstrappers 组用户 TLS bootstrapping 首次申请证书的 CSR 请求kubectl create clusterrolebinding auto-approve-csrs-for-group \    --clusterrole=system:certificates.k8s.io:certificatesigningrequests:nodeclient \    --group=system:bootstrappers# 自动批准 system:nodes 组用户更新 kubelet 自身与 apiserver 通讯证书的 CSR 请求kubectl create clusterrolebinding auto-approve-renewals-for-nodes \    --clusterrole=system:certificates.k8s.io:certificatesigningrequests:selfnodeclient \    --group=system:nodes# 在 kubelet server 开启 api 认证的情况下,apiserver 反向访问 kubelet 10250 需要此授权(eg: kubectl logs)kubectl create clusterrolebinding system:kubelet-api-admin \    --clusterrole=system:kubelet-api-admin \    --user=system:kubelet-api-admin

3.4.3、Nginx 代理

为了保证 apiserver 的 HA,需要在每个 node 上部署 nginx 来反向代理(tcp)所有 apiserver;然后 kubelet、kube-proxy 组件连接本地 127.0.0.1:6443 访问 apiserver,以确保任何 master 挂掉以后 node 都不会受到影响

  • nginx.conf
error_log stderr notice;worker_processes auto;events {  multi_accept on;  use epoll;  worker_connections 1024;}stream {    upstream kube_apiserver {        least_conn;        server 192.168.1.51:6443;        server 192.168.1.52:6443;        server 192.168.1.53:6443;    }    server {        listen        0.0.0.0:6443;        proxy_pass    kube_apiserver;        proxy_timeout 10m;        proxy_connect_timeout 1s;    }}
  • nginx-proxy.service
[Unit]Description=kubernetes apiserver docker wrapperWants=docker.socketAfter=docker.service[Service]User=rootPermissionsStartOnly=trueExecStart=/usr/bin/docker run -p 127.0.0.1:6443:6443 \                              -v /etc/nginx:/etc/nginx \                              --name nginx-proxy \                              --net=host \                              --restart=on-failure:5 \                              --memory=512M \                              nginx:1.14.2-alpineExecStartPre=-/usr/bin/docker rm -f nginx-proxyExecStop=/usr/bin/docker stop nginx-proxyRestart=alwaysRestartSec=15sTimeoutStartSec=30s[Install]WantedBy=multi-user.target

然后在每个 node 上先启动 nginx-proxy,接着启动 kubelet 与 kube-proxy 即可(master 上的 kubelet、kube-proxy 只需要修改 ip 和 node name)

3.4.4、kubelet server 证书

注意: 新版本 kubelet server 的证书自动签发已经被关闭(看 issue 好像是由于安全原因),所以对于 kubelet server 的证书仍需要手动签署

docker1.node ➜  ~ kubectl get csrNAME                                                   AGE   REQUESTOR                  CONDITIONcsr-99l77                                              10s   system:node:docker4.node   Pendingnode-csr-aGwaNKorMc0MZBYOuJsJGCB8Bg8ds97rmE3oKBTV-_E   11s   system:bootstrap:5d820b    Approved,Issueddocker1.node ➜  ~ kubectl certificate approve csr-99l77certificatesigningrequest.certificates.k8s.io/csr-99l77 approved

3.5、部署 Calico

当 node 全部启动后,由于网络组件(CNI)未安装会显示为 NotReady 状态;下面将部署 Calico 作为网络组件,完成跨节点网络通讯;具体安装文档可以参考 Installing with the etcd datastore

以下为 calico 的配置文件

  • calico.yaml
---# Source: calico/templates/calico-etcd-secrets.yaml# The following contains k8s Secrets for use with a TLS enabled etcd cluster.# For information on populating Secrets, see http://kubernetes.io/docs/user-guide/secrets/apiVersion: v1kind: Secrettype: Opaquemetadata:  name: calico-etcd-secrets  namespace: kube-systemdata:  # Populate the following with etcd TLS configuration if desired, but leave blank if  # not using TLS for etcd.  # The keys below should be uncommented and the values populated with the base64  # encoded contents of each file that would be associated with the TLS data.  # Example command for encoding a file contents: cat <file> | base64 -w 0  etcd-key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBdGtOVlV5QWtOOWxDKy9EbzlCRkt0em5IZlFJKzJMK2crclkwLzNoOExJTEFoWUtXCm1XdVNNQUFjbyt4clVtaTFlUGIzcmRKR0p1NEhmRXFmalYvakhvN0haOGxteXd0S29Ed254aU9jZDRlRXltcXEKTEFVYzZ5RWU4dXFGZ2pLVHE4SjV2Z1F1cGp0ZlZnZXRPdVVsWWtWbUNKMWtpUW0yVk5WRnRWZ0Fqck1xSy9POApJTXN6RWRYU3BDc1Zwb0kzaUpoVHJSRng4ZzRXc2hwNG1XMzhMWDVJYVVoMWZaSGVMWm1sRURpclBWMGRTNmFWCmJscUk2aUFwanVBc3hYWjFlVTdmOVZWK01PVmNVc3A4cDAxNmJzS3R6VTJGSnB6ZlM3c1BlbGpKZGgzZmVOdk8KRVl1aDlsU0c1VGNKUHBuTTZ0R0ppaHpEWCt4dnNGa3d5MVJSVlFJREFRQUJBb0lCQUYwRXVqd2xVRGFzakJJVwpubDFKb2U4bTd0ZXUyTEk0QW9sUmluVERZZVE1aXRYWWt0R1Q0OVRaaWNSak9WYWlsOU0zZjZwWGdYUUcwUTB1CjdJVHpaZTlIZ1I5SDIwMU80dlFxSDBaeEVENjBqQ0hlRkNGSkxyd1ZlRDBUVWJYajZCZWx0Z296Q2pmT1gxYUIKcm5nN1VEdjZIUnZTYitlOGJEQ1pjKzBjRDVURG4vUWV0R1dtUmpJZ1FhMmlUT2MzSzFiaHo2RTl5Nk9qWkFTMQpiai9NL1dOd20yNHRxQTJEeWdjcGVmUGFnTWtFNm9uYXBFVHhZdi83QmNqcUhtdVd6WE1wMzd6VGpPckwxVDdmClhrbHdFMUYrMDRhRDR6dDZycEdmN0lqSUdvRkEvT2ZrRGZiYkRjN2NsaDJ1SkNMTVE5MGpuSkxMTGRSV3dQRW4KMkkyY3IvVUNnWUVBN3BjT29VV3RwdDJjWGIzSnl3Tkh4aXl1bEc4V1JENjBlQ1MrUXFnQUZndU5JWFJlMEREUwovSWY0M1BhaVB3TjhBS216ZTRKbGsxM3Rnd29qdi9RWVFVblJzZi9PbnpUUlFoWVJXT2lxSE5lSmFvOUxFU0VDClcxNXNmUjhnYzd0dFdPZ0loZkhudmdCR0QvYmUzS1NWVjdUY0lndVVjV3RzeHhLdjZ4LzJNdHNDZ1lFQXc1QVIKWk9HNUp4UGVNV3FVRUR3QjJuQmt6WEtGblpNSEJXV2FOeHpEaTI0NmZEVWM2T1hSTTJJanh2cmVkc3JKQjBXMwovelNDeFdUbkRmL3RJY1lKMjRuTmNsMUNDS2hTNVE5bVZxanZ3dE1SaEF1Uk5VSFJSVjZLNS91V1hHQzAzekR3CkEvMUFSd3lZSHNHTlJVOFRNNnpNRFcxL0x5djZNZ2pnOFBIamk0OENnWUVBa3JwelZOcjFJRm5KZ0J6bnJPSW4Ka2NpSTFPQThZVnZ1d0xSWURjWWp4MnJ6TUUvUXYxaEhhT1oyTmUyM2VlazZxVzJ6NDVFZHhyTk5EZmwrWXQ1Swp6RndKaWQ0M3c5RkhuOHpTZmtzWDB3VDZqWDN5UEdhQWZKQmxSODJNdDUvY2I0RERQUnkzMkRGeTVQNTlzRlBIClJGa0Z5Q28yOEVtUWJCMGg4d2VFOFdFQ2dZQm1IeUptS3RWVUNiVDYyeXZzZWxtQlp6WE1ieVJGRDlVWHhXSE4KcTlDVlMvOXdndy9Rc3NvVzZnWEN6NWhDTWt6ZDVsTmFDbUxMajVCMHFCTjlrbnZ0VDcyZ0hnRHdvbTEvUGhaego1STRuajY3UzVITjBleVU3ODAzWUxISHRWWGErSWtFRDVFaWZrWDBTZW9JNkVqdjF2U05sVTZ1WngzNUVpSXhtClpmb3NFd0tCZ0dQMmpsK0lPcFV5Y2NEL25EbUJWa05CWHoydWhncU8yYjE4d0hSOGdiSXoyVTRBZnpreXVkWUcKZzQvRjJZZVdCSEdNeTc5N0I2c0hjQTdQUWNNdUFuRk11MG9UNkMvanpDSHpoK2VaaS8wdHJRTHJGeWFFaGVuWgpnazduUTdHNHhROWZLZmVTeFcyUlNNUUR0MTZULzNOTitTOEZCTjJmZEliY3V4QWs0WjVHCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==  etcd-cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZFekNDQXZ1Z0F3SUJBZ0lVRGJqcTdVc2ViY2toZXRZb1RPNnRsc1N1c1k0d0RRWUpLb1pJaHZjTkFRRU4KQlFBd2J6RUxNQWtHQTFVRUJoTUNRMDR4RURBT0JnTlZCQWdUQjBKbGFXcHBibWN4RURBT0JnTlZCQWNUQjBKbAphV3BwYm1jeERUQUxCZ05WQkFvVEJHVjBZMlF4RmpBVUJnTlZCQXNURFdWMFkyUWdVMlZqZFhKcGRIa3hGVEFUCkJnTlZCQU1UREdWMFkyUXRjbTl2ZEMxallUQWVGdzB4T1RBek1UWXdNelV4TURCYUZ3MHlPVEF6TVRNd016VXgKTURCYU1HY3hDekFKQmdOVkJBWVRBa05PTVJBd0RnWURWUVFJRXdkQ1pXbHFhVzVuTVJBd0RnWURWUVFIRXdkQwpaV2xxYVc1bk1RMHdDd1lEVlFRS0V3UmxkR05rTVJZd0ZBWURWUVFMRXcxbGRHTmtJRk5sWTNWeWFYUjVNUTB3CkN3WURWUVFERXdSbGRHTmtNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXRrTlYKVXlBa045bEMrL0RvOUJGS3R6bkhmUUkrMkwrZytyWTAvM2g4TElMQWhZS1dtV3VTTUFBY28reHJVbWkxZVBiMwpyZEpHSnU0SGZFcWZqVi9qSG83SFo4bG15d3RLb0R3bnhpT2NkNGVFeW1xcUxBVWM2eUVlOHVxRmdqS1RxOEo1CnZnUXVwanRmVmdldE91VWxZa1ZtQ0oxa2lRbTJWTlZGdFZnQWpyTXFLL084SU1zekVkWFNwQ3NWcG9JM2lKaFQKclJGeDhnNFdzaHA0bVczOExYNUlhVWgxZlpIZUxabWxFRGlyUFYwZFM2YVZibHFJNmlBcGp1QXN4WFoxZVU3Zgo5VlYrTU9WY1VzcDhwMDE2YnNLdHpVMkZKcHpmUzdzUGVsakpkaDNmZU52T0VZdWg5bFNHNVRjSlBwbk02dEdKCmloekRYK3h2c0Zrd3kxUlJWUUlEQVFBQm80R3VNSUdyTUE0R0ExVWREd0VCL3dRRUF3SUZvREFkQmdOVkhTVUUKRmpBVUJnZ3JCZ0VGQlFjREFRWUlLd1lCQlFVSEF3SXdEQVlEVlIwVEFRSC9CQUl3QURBZEJnTlZIUTRFRmdRVQpFKzVsWWN1LzhieHJ2WjNvUnRSMmEvOVBJRkF3SHdZRFZSMGpCQmd3Rm9BVTJaVWM3R2hGaG1PQXhzRlZ3VEEyCm5lZFJIdmN3TEFZRFZSMFJCQ1V3STRJSmJHOWpZV3hvYjNOMGh3Ui9BQUFCaHdUQXFBRXpod1RBcUFFMGh3VEEKcUFFMU1BMEdDU3FHU0liM0RRRUJEUVVBQTRJQ0FRQUx3Vkc2QW93cklwZzQvYlRwWndWL0pBUWNLSnJGdm52VApabDVDdzIzNDI4UzJLLzIwaXphaStEWUR1SXIwQ0ZCa2xGOXVsK05ROXZMZ1lqcE0rOTNOY3I0dXhUTVZsRUdZCjloc3NyT1FZZVBGUHhBS1k3RGd0K2RWUGwrWlg4MXNWRzJkU3ZBbm9Kd3dEVWt5U0VUY0g5NkszSlNKS2dXZGsKaTYxN21GYnMrTlcxdngrL0JNN2pVU3ZRUzhRb3JGQVE3SlcwYzZ3R2V4RFEzZExvTXJuR3Vocjd0V0E0WjhwawpPaE12cWdhWUZYSThNUm4yemlLV0R6QXNsa0hGd1RZdWhCNURMSEt0RUVwcWhxbGh1RThwTkZMaVVSV2xQWWhlCmpDNnVKZ0hBZDltcSswd2pyTmxqKzlWaDJoZUJWNldXZEROVTZaR2tpR003RW9YbDM1OWdUTzJPUkNLUk5vZ0YKRVplR25HcjJQNDhKbnZjTnFmZzNPdUtYd24wRDVYYllSWjFuYnR5WG9mMFByUUhEU21wUFVPMWNiZUJjSWVtcQpEVWozK0MrRzBRS1FLQlZDTXJzNXJIVlVWVkJZZzk5ZW1sRE1zUE5TZm9JWDQwTVFCeTdKMnpxRVV5M0sxcGlaCkhwT0lZT1RrWDRhczhqcGYxMnkxSXoxRVZydE1xek83d294VmMwdHRZYWN5NzUrVzZuS1hlWjBaand5aTVYSzUKZGduSVhmZW51RUNlWFNDdWZCSmUxVklzaXVWZ3cyRjlUNk5zRDhnQ3A5SlhTamJ1SXpiM3ArNU9uZzM2ZnBRdQpXZVBCY0dQVXE5cGEwZUtOUGJXNjlDUHdtUTQ2cjg0T3hTTURHWC9CMElqNUtNUnZUMmhPUXBqTVpSblc5OUxFCjRMbUJuUTg1Wmc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==  etcd-ca: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZyakNDQTVhZ0F3SUJBZ0lVWXVIKzIxYlNaU2hncVYxWkx3a2o4RmpZbUl3d0RRWUpLb1pJaHZjTkFRRU4KQlFBd2J6RUxNQWtHQTFVRUJoTUNRMDR4RURBT0JnTlZCQWdUQjBKbGFXcHBibWN4RURBT0JnTlZCQWNUQjBKbAphV3BwYm1jeERUQUxCZ05WQkFvVEJHVjBZMlF4RmpBVUJnTlZCQXNURFdWMFkyUWdVMlZqZFhKcGRIa3hGVEFUCkJnTlZCQU1UREdWMFkyUXRjbTl2ZEMxallUQWVGdzB4T1RBek1UWXdNelV4TURCYUZ3MHlPVEF6TVRNd016VXgKTURCYU1HOHhDekFKQmdOVkJBWVRBa05PTVJBd0RnWURWUVFJRXdkQ1pXbHFhVzVuTVJBd0RnWURWUVFIRXdkQwpaV2xxYVc1bk1RMHdDd1lEVlFRS0V3UmxkR05rTVJZd0ZBWURWUVFMRXcxbGRHTmtJRk5sWTNWeWFYUjVNUlV3CkV3WURWUVFERXd4bGRHTmtMWEp2YjNRdFkyRXdnZ0lpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElDRHdBd2dnSUsKQW9JQ0FRRGFLK0s4WStqZkdOY2pOeUloeUhXSE5adWxVZzVKZFpOVU9GOHFXbXJMa0NuY2ZWdVF3dmI4cDFwLwpSSjBFOWo0OFBhZ1RJT3U2TU81R24zejFrZGpHRk9jOVZwMlZjYWJEQzJLWWJvRzdVQ0RmTWkzR1MzUnhUejVkCnh0MG1Ya2liVkMvc01NU2RrRm1mU2FCSXBoKzAyTnMwZURyMzNtUWxTdURlTWozNHJaTkVwMzRnUUk0eElTejAKbXhXR0dWNzcwUE9ScVgrZUthTEpiclp3anFFcnpHMEtEVUlBM0ZuTFdRMnp4b0VwN3JZby9LaGRiOHdETE1kbQp6VXNOZHI0T1F4MFBVRXA4akRUU2lFODkydDQ4KytsOHJ0MW4vTHFRc1FhVncrQlQrMTRvRHdIVkFaRXZ2ZnMwCmZkZ0QvU2RINGJRdHNhT21BdFByQldseU5aMUxIZkR2djMraXFzNk83UXpWUTFCK1c5cFRxdUZ2YUxWN3R1S3UKSXNlUFlseFdjV2E2M0hGbFkxVVJ6M0owaGtrZEZ1dkhUc0dhZDVpaWVrb0dUcFdTN2dVdCtTeWVJT2FhMldHLwp4Y1NiUWE0Y2xiZThuUHV2c1ZFVDhqZ0d0NGVLT25yRVJId0hMb2VleEpsSjdUdnhHNHpOTHZsc2FOL29iRzFDClUzMXczZ2d1SXpzRk5yallsUFdSZ0hSdXdPTlE5anlkM2dqVmNYUFdHTFJISUdYbjNhUDluT3A0OE9WWDhzbXoKOGIwS0V4UVpEQWUyS0tjWEg5a1ZiUFJQSWlLeGpXelV5aDMzQlRNejlPczZHcWM0Zk05c1hxbGRhVzBGd3g4MQpJaklScWx5a3VOSXNDWGhMUzhlNmVtdUNYMTVDZGNKb0ZmdXRuTENvV1B4Umg5OEF4UUlEQVFBQm8wSXdRREFPCkJnTlZIUThCQWY4RUJBTUNBUVl3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFkQmdOVkhRNEVGZ1FVMlpVYzdHaEYKaG1PQXhzRlZ3VEEybmVkUkh2Y3dEUVlKS29aSWh2Y05BUUVOQlFBRGdnSUJBSjh3bVNJMVBsQm8zcE1RWC9DOQpRS1RrR0xvVUhGdWprdFoxM1FYeXQ1LzFSeVB2WG1lLy90N3FHR2I5RmJZSm9BYTRTd3JSZkYzZmh3UDZaS0FnCnNYSEliR2gwc014UTdqVmQwMUNMWkoxQmZFNGZtTVlaQUlEWGpTcTNqbHJXZWcxL2hWTFN2dXRuUEFWSXc1SWwKZUdXRTMyOVJ2b2d2dXV6dUsxY2xwZFpIL2p3UlZjUUFUK0xvT2xFZ3Rkd293c0xpaWx3WE95eEZLZDd1UDk3bgozTFZUekFNN3Flell4SUVMQVlUUUN5eTdpeEIxNXlJV1UrUWhreUFtWXJoNEN6VUNNUjQreDlpaGZ6UnlOQkxLCmRBRTdwcjdyUEM4WFQ0YWh2SkJCZTg1THViTVdVRmprcEF5cklQODYyYkFCOCtKSXNFdXNZVGdQakUrMGhteTkKT0NIU2x4Q25GQVdPUXcwQ05Kb3AxWGpHU0RZOXlXL1NNWS83T3B0QlBhT3VWTzVwZTg3VmVXRFFtYmlpdnc3MQo4cFhDQnN6ZWNsdjJZKzdscTRnL0FaQkViVXRvLzV4UXJCbmZGKy9hZFFOQzY4aG4yYzZWa3czYTVDR0ZMN0p2CjhWdFNmeFEzZnFUci9TdzlJbkVKVWpuc0Y3R0xINzZMWXZIU05WeldhMkhiVFNlTnQ0RUlpdlEwb2d0b2hzY0kKSHlrZlpRQ3Z6ZnBSZi9TODFiRDNnU29jQ3NzR2crdVpVU0FMdVhBRDE4RkRXNzg2LzRCckcrMzVLOVBLNktUZwpoWGN4WmRHd3V1RWx0aTRBNWx4OHNrZExPSkZ6TUJPWFJNU2Jsc0dna3pGK2JNRkMrMHV3WW1WK0VTRUdwdy9NCm93WUN1dHh2a3ltL2NOcEk1bjFhanpEcQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==---# Source: calico/templates/calico-config.yaml# This ConfigMap is used to configure a self-hosted Calico installation.kind: ConfigMapapiVersion: v1metadata:  name: calico-config  namespace: kube-systemdata:  # Configure this with the location of your etcd cluster.  etcd_endpoints: "http://192.168.1.51:2379,http://192.168.1.52:2379,http://192.168.1.53:2379"  # If you're using TLS enabled etcd uncomment the following.  # You must also populate the Secret below with these files.  etcd_ca: "/calico-secrets/etcd-ca"  etcd_cert: "/calico-secrets/etcd-cert"  etcd_key: "/calico-secrets/etcd-key"  # Typha is disabled.  typha_service_name: "none"  # Configure the Calico backend to use.  calico_backend: "bird"  # Configure the MTU to use  veth_mtu: "1440"  # The CNI network configuration to install on each node.  The special  # values in this config will be automatically populated.  cni_network_config: |-    {      "name": "k8s-pod-network",      "cniVersion": "0.3.0",      "plugins": [        {          "type": "calico",          "log_level": "info",          "etcd_endpoints": "__ETCD_ENDPOINTS__",          "etcd_key_file": "__ETCD_KEY_FILE__",          "etcd_cert_file": "__ETCD_CERT_FILE__",          "etcd_ca_cert_file": "__ETCD_CA_CERT_FILE__",          "mtu": __CNI_MTU__,          "ipam": {              "type": "calico-ipam"          },          "policy": {              "type": "k8s"          },          "kubernetes": {              "kubeconfig": "__KUBECONFIG_FILEPATH__"          }        },        {          "type": "portmap",          "snat": true,          "capabilities": {"portMappings": true}        }      ]    }---# Source: calico/templates/rbac.yaml# Include a clusterrole for the kube-controllers component,# and bind it to the calico-kube-controllers serviceaccount.kind: ClusterRoleapiVersion: rbac.authorization.k8s.io/v1beta1metadata:  name: calico-kube-controllersrules:  # Pods are monitored for changing labels.  # The node controller monitors Kubernetes nodes.  # Namespace and serviceaccount labels are used for policy.  - apiGroups: [""]    resources:      - pods      - nodes      - namespaces      - serviceaccounts    verbs:      - watch      - list  # Watch for changes to Kubernetes NetworkPolicies.  - apiGroups: ["networking.k8s.io"]    resources:      - networkpolicies    verbs:      - watch      - list---kind: ClusterRoleBindingapiVersion: rbac.authorization.k8s.io/v1beta1metadata:  name: calico-kube-controllersroleRef:  apiGroup: rbac.authorization.k8s.io  kind: ClusterRole  name: calico-kube-controllerssubjects:- kind: ServiceAccount  name: calico-kube-controllers  namespace: kube-system---# Include a clusterrole for the calico-node DaemonSet,# and bind it to the calico-node serviceaccount.kind: ClusterRoleapiVersion: rbac.authorization.k8s.io/v1beta1metadata:  name: calico-noderules:  # The CNI plugin needs to get pods, nodes, and namespaces.  - apiGroups: [""]    resources:      - pods      - nodes      - namespaces    verbs:      - get  - apiGroups: [""]    resources:      - endpoints      - services    verbs:      # Used to discover service IPs for advertisement.      - watch      - list  - apiGroups: [""]    resources:      - nodes/status    verbs:      # Needed for clearing NodeNetworkUnavailable flag.      - patch---apiVersion: rbac.authorization.k8s.io/v1beta1kind: ClusterRoleBindingmetadata:  name: calico-noderoleRef:  apiGroup: rbac.authorization.k8s.io  kind: ClusterRole  name: calico-nodesubjects:- kind: ServiceAccount  name: calico-node  namespace: kube-system------# Source: calico/templates/calico-node.yaml# This manifest installs the calico/node container, as well# as the Calico CNI plugins and network config on# each master and worker node in a Kubernetes cluster.kind: DaemonSetapiVersion: extensions/v1beta1metadata:  name: calico-node  namespace: kube-system  labels:    k8s-app: calico-nodespec:  selector:    matchLabels:      k8s-app: calico-node  updateStrategy:    type: RollingUpdate    rollingUpdate:      maxUnavailable: 1  template:    metadata:      labels:        k8s-app: calico-node      annotations:        # This, along with the CriticalAddonsOnly toleration below,        # marks the pod as a critical add-on, ensuring it gets        # priority scheduling and that its resources are reserved        # if it ever gets evicted.        scheduler.alpha.kubernetes.io/critical-pod: ''    spec:      nodeSelector:        beta.kubernetes.io/os: linux      hostNetwork: true      tolerations:        # Make sure calico-node gets scheduled on all nodes.        - effect: NoSchedule          operator: Exists        # Mark the pod as a critical add-on for rescheduling.        - key: CriticalAddonsOnly          operator: Exists        - effect: NoExecute          operator: Exists      serviceAccountName: calico-node      # Minimize downtime during a rolling upgrade or deletion; tell Kubernetes to do a "force      # deletion": http://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods.      terminationGracePeriodSeconds: 0      initContainers:        # This container installs the Calico CNI binaries        # and CNI network config file on each node.        - name: install-cni          image: calico/cni:v3.6.0          command: ["/install-cni.sh"]          env:            # Name of the CNI config file to create.            - name: CNI_CONF_NAME              value: "10-calico.conflist"            # The CNI network config to install on each node.            - name: CNI_NETWORK_CONFIG              valueFrom:                configMapKeyRef:                  name: calico-config                  key: cni_network_config            # The location of the Calico etcd cluster.            - name: ETCD_ENDPOINTS              valueFrom:                configMapKeyRef:                  name: calico-config                  key: etcd_endpoints            # CNI MTU Config variable            - name: CNI_MTU              valueFrom:                configMapKeyRef:                  name: calico-config                  key: veth_mtu            # Prevents the container from sleeping forever.            - name: SLEEP              value: "false"          volumeMounts:            - mountPath: /host/opt/cni/bin              name: cni-bin-dir            - mountPath: /host/etc/cni/net.d              name: cni-net-dir            - mountPath: /calico-secrets              name: etcd-certs      containers:        # Runs calico/node container on each Kubernetes node.  This        # container programs network policy and routes on each        # host.        - name: calico-node          image: calico/node:v3.6.0          env:            # The location of the Calico etcd cluster.            - name: ETCD_ENDPOINTS              valueFrom:                configMapKeyRef:                  name: calico-config                  key: etcd_endpoints            # Location of the CA certificate for etcd.            - name: ETCD_CA_CERT_FILE              valueFrom:                configMapKeyRef:                  name: calico-config                  key: etcd_ca            # Location of the client key for etcd.            - name: ETCD_KEY_FILE              valueFrom:                configMapKeyRef:                  name: calico-config                  key: etcd_key            # Location of the client certificate for etcd.            - name: ETCD_CERT_FILE              valueFrom:                configMapKeyRef:                  name: calico-config                  key: etcd_cert            # Set noderef for node controller.            - name: CALICO_K8S_NODE_REF              valueFrom:                fieldRef:                  fieldPath: spec.nodeName            # Choose the backend to use.            - name: CALICO_NETWORKING_BACKEND              valueFrom:                configMapKeyRef:                  name: calico-config                  key: calico_backend            # Cluster type to identify the deployment type            - name: CLUSTER_TYPE              value: "k8s,bgp"            # Auto-detect the BGP IP address.            - name: IP              value: "autodetect"            # Enable IPIP            - name: CALICO_IPV4POOL_IPIP              value: "Always"            # Set MTU for tunnel device used if ipip is enabled            - name: FELIX_IPINIPMTU              valueFrom:                configMapKeyRef:                  name: calico-config                  key: veth_mtu            # The default IPv4 pool to create on startup if none exists. Pod IPs will be            # chosen from this range. Changing this value after installation will have            # no effect. This should fall within `--cluster-cidr`.            - name: CALICO_IPV4POOL_CIDR              value: "10.20.0.0/16"            # Disable file logging so `kubectl logs` works.            - name: CALICO_DISABLE_FILE_LOGGING              value: "true"            # Set Felix endpoint to host default action to ACCEPT.            - name: FELIX_DEFAULTENDPOINTTOHOSTACTION              value: "ACCEPT"            # Disable IPv6 on Kubernetes.            - name: FELIX_IPV6SUPPORT              value: "false"            # Set Felix logging to "info"            - name: FELIX_LOGSEVERITYSCREEN              value: "info"            - name: FELIX_HEALTHENABLED              value: "true"            - name: IP_AUTODETECTION_METHOD              value: can-reach=192.168.1.51          securityContext:            privileged: true          resources:            requests:              cpu: 250m          livenessProbe:            httpGet:              path: /liveness              port: 9099              host: localhost            periodSeconds: 10            initialDelaySeconds: 10            failureThreshold: 6          readinessProbe:            exec:              command:              - /bin/calico-node              - -bird-ready              - -felix-ready            periodSeconds: 10          volumeMounts:            - mountPath: /lib/modules              name: lib-modules              readOnly: true            - mountPath: /run/xtables.lock              name: xtables-lock              readOnly: false            - mountPath: /var/run/calico              name: var-run-calico              readOnly: false            - mountPath: /var/lib/calico              name: var-lib-calico              readOnly: false            - mountPath: /calico-secrets              name: etcd-certs      volumes:        # Used by calico/node.        - name: lib-modules          hostPath:            path: /lib/modules        - name: var-run-calico          hostPath:            path: /var/run/calico        - name: var-lib-calico          hostPath:            path: /var/lib/calico        - name: xtables-lock          hostPath:            path: /run/xtables.lock            type: FileOrCreate        # Used to install CNI.        - name: cni-bin-dir          hostPath:            path: /opt/cni/bin        - name: cni-net-dir          hostPath:            path: /etc/cni/net.d        # Mount in the etcd TLS secrets with mode 400.        # See http://kubernetes.io/docs/concepts/configuration/secret/        - name: etcd-certs          secret:            secretName: calico-etcd-secrets            defaultMode: 0400---apiVersion: v1kind: ServiceAccountmetadata:  name: calico-node  namespace: kube-system---# Source: calico/templates/calico-kube-controllers.yaml# This manifest deploys the Calico Kubernetes controllers.# See http://github.com/projectcalico/kube-controllersapiVersion: extensions/v1beta1kind: Deploymentmetadata:  name: calico-kube-controllers  namespace: kube-system  labels:    k8s-app: calico-kube-controllers  annotations:    scheduler.alpha.kubernetes.io/critical-pod: ''spec:  # The controllers can only have a single active instance.  replicas: 1  strategy:    type: Recreate  template:    metadata:      name: calico-kube-controllers      namespace: kube-system      labels:        k8s-app: calico-kube-controllers    spec:      nodeSelector:        beta.kubernetes.io/os: linux      # The controllers must run in the host network namespace so that      # it isn't governed by policy that would prevent it from working.      hostNetwork: true      tolerations:        # Mark the pod as a critical add-on for rescheduling.        - key: CriticalAddonsOnly          operator: Exists        - key: node-role.kubernetes.io/master          effect: NoSchedule      serviceAccountName: calico-kube-controllers      containers:        - name: calico-kube-controllers          image: calico/kube-controllers:v3.6.0          env:            # The location of the Calico etcd cluster.            - name: ETCD_ENDPOINTS              valueFrom:                configMapKeyRef:                  name: calico-config                  key: etcd_endpoints            # Location of the CA certificate for etcd.            - name: ETCD_CA_CERT_FILE              valueFrom:                configMapKeyRef:                  name: calico-config                  key: etcd_ca            # Location of the client key for etcd.            - name: ETCD_KEY_FILE              valueFrom:                configMapKeyRef:                  name: calico-config                  key: etcd_key            # Location of the client certificate for etcd.            - name: ETCD_CERT_FILE              valueFrom:                configMapKeyRef:                  name: calico-config                  key: etcd_cert            # Choose which controllers to run.            - name: ENABLED_CONTROLLERS              value: policy,namespace,serviceaccount,workloadendpoint,node          volumeMounts:            # Mount in the etcd TLS secrets.            - mountPath: /calico-secrets              name: etcd-certs          readinessProbe:            exec:              command:              - /usr/bin/check-status              - -r      volumes:        # Mount in the etcd TLS secrets with mode 400.        # See http://kubernetes.io/docs/concepts/configuration/secret/        - name: etcd-certs          secret:            secretName: calico-etcd-secrets            defaultMode: 0400---apiVersion: v1kind: ServiceAccountmetadata:  name: calico-kube-controllers  namespace: kube-system

需要注意的是我们添加了 IP_AUTODETECTION_METHOD 变量,这个变量会设置 calcio 获取 node ip 的方式;默认情况下采用 first-found 方式获取,即获取第一个有效网卡的 IP 作为 node ip;在某些多网卡机器上可能会出现问题;这里将值设置为 can-reach=192.168.1.51,即使用第一个能够访问 master 192.168.1.51 的网卡地址作为 node ip

最后执行创建即可,创建成功后如下所示

docker1.node ➜  ~ kubectl get pod -o wide -n kube-systemNAME                                      READY   STATUS    RESTARTS   AGE   IP             NODE           NOMINATED NODE   READINESS GATEScalico-kube-controllers-65bc6b9f9-cn27f   1/1     Running   0          85s   192.168.1.53   docker3.node   <none>           <none>calico-node-c5nl8                         1/1     Running   0          85s   192.168.1.53   docker3.node   <none>           <none>calico-node-fqknv                         1/1     Running   0          85s   192.168.1.51   docker1.node   <none>           <none>calico-node-ldfzs                         1/1     Running   0          85s   192.168.1.55   docker5.node   <none>           <none>calico-node-ngjxc                         1/1     Running   0          85s   192.168.1.52   docker2.node   <none>           <none>calico-node-vj8np                         1/1     Running   0          85s   192.168.1.54   docker4.node   <none>           <none>

此时所有 node 应当变为 Ready 状态

3.5、部署 DNS

其他组件全部完成后我们应当部署集群 DNS 使 service 等能够正常解析;集群 DNS 这里采用 coredns,具体安装文档参考 coredns/deployment;coredns 完整配置如下

  • coredns.yaml
apiVersion: v1kind: ServiceAccountmetadata:  name: coredns  namespace: kube-system---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata:  labels:    kubernetes.io/bootstrapping: rbac-defaults  name: system:corednsrules:- apiGroups:  - ""  resources:  - endpoints  - services  - pods  - namespaces  verbs:  - list  - watch- apiGroups:  - ""  resources:  - nodes  verbs:  - get---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata:  annotations:    rbac.authorization.kubernetes.io/autoupdate: "true"  labels:    kubernetes.io/bootstrapping: rbac-defaults  name: system:corednsroleRef:  apiGroup: rbac.authorization.k8s.io  kind: ClusterRole  name: system:corednssubjects:- kind: ServiceAccount  name: coredns  namespace: kube-system---apiVersion: v1kind: ConfigMapmetadata:  name: coredns  namespace: kube-systemdata:  Corefile: |    .:53 {        errors        health        kubernetes cluster.local in-addr.arpa ip6.arpa {          pods insecure          upstream          fallthrough in-addr.arpa ip6.arpa        }        prometheus :9153        forward . /etc/resolv.conf        cache 30        loop        reload        loadbalance    }---apiVersion: apps/v1kind: Deploymentmetadata:  name: coredns  namespace: kube-system  labels:    k8s-app: kube-dns    kubernetes.io/name: "CoreDNS"spec:  replicas: 2  strategy:    type: RollingUpdate    rollingUpdate:      maxUnavailable: 1  selector:    matchLabels:      k8s-app: kube-dns  template:    metadata:      labels:        k8s-app: kube-dns    spec:      priorityClassName: system-cluster-critical      serviceAccountName: coredns      tolerations:        - key: "CriticalAddonsOnly"          operator: "Exists"      nodeSelector:        beta.kubernetes.io/os: linux      containers:      - name: coredns        image: coredns/coredns:1.3.1        imagePullPolicy: IfNotPresent        resources:          limits:            memory: 170Mi          requests:            cpu: 100m            memory: 70Mi        args: [ "-conf", "/etc/coredns/Corefile" ]        volumeMounts:        - name: config-volume          mountPath: /etc/coredns          readOnly: true        ports:        - containerPort: 53          name: dns          protocol: UDP        - containerPort: 53          name: dns-tcp          protocol: TCP        - containerPort: 9153          name: metrics          protocol: TCP        securityContext:          allowPrivilegeEscalation: false          capabilities:            add:            - NET_BIND_SERVICE            drop:            - all          readOnlyRootFilesystem: true        livenessProbe:          httpGet:            path: /health            port: 8080            scheme: HTTP          initialDelaySeconds: 60          timeoutSeconds: 5          successThreshold: 1          failureThreshold: 5        readinessProbe:          httpGet:            path: /health            port: 8080            scheme: HTTP      dnsPolicy: Default      volumes:        - name: config-volume          configMap:            name: coredns            items:            - key: Corefile              path: Corefile---apiVersion: v1kind: Servicemetadata:  name: kube-dns  namespace: kube-system  annotations:    prometheus.io/port: "9153"    prometheus.io/scrape: "true"  labels:    k8s-app: kube-dns    kubernetes.io/cluster-service: "true"    kubernetes.io/name: "CoreDNS"spec:  selector:    k8s-app: kube-dns  clusterIP: 10.254.0.2  ports:  - name: dns    port: 53    protocol: UDP  - name: dns-tcp    port: 53    protocol: TCP  - name: metrics    port: 9153    protocol: TCP

3.5、部署 DNS 自动扩容

在大规模集群的情况下,可能需要集群 DNS 自动扩容,具体文档请参考 DNS Horizontal Autoscaler,DNS 扩容算法可参考 Github,如有需要请自行修改;以下为具体配置

  • dns-horizontal-autoscaler.yaml
# Copyright 2016 The Kubernetes Authors.## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at##     http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.kind: ServiceAccountapiVersion: v1metadata:  name: kube-dns-autoscaler  namespace: kube-system  labels:    addonmanager.kubernetes.io/mode: Reconcile---kind: ClusterRoleapiVersion: rbac.authorization.k8s.io/v1metadata:  name: system:kube-dns-autoscaler  labels:    addonmanager.kubernetes.io/mode: Reconcilerules:  - apiGroups: [""]    resources: ["nodes"]    verbs: ["list"]  - apiGroups: [""]    resources: ["replicationcontrollers/scale"]    verbs: ["get", "update"]  - apiGroups: ["extensions"]    resources: ["deployments/scale", "replicasets/scale"]    verbs: ["get", "update"]# Remove the configmaps rule once below issue is fixed:# kubernetes-incubator/cluster-proportional-autoscaler#16  - apiGroups: [""]    resources: ["configmaps"]    verbs: ["get", "create"]---kind: ClusterRoleBindingapiVersion: rbac.authorization.k8s.io/v1metadata:  name: system:kube-dns-autoscaler  labels:    addonmanager.kubernetes.io/mode: Reconcilesubjects:  - kind: ServiceAccount    name: kube-dns-autoscaler    namespace: kube-systemroleRef:  kind: ClusterRole  name: system:kube-dns-autoscaler  apiGroup: rbac.authorization.k8s.io---apiVersion: apps/v1kind: Deploymentmetadata:  name: kube-dns-autoscaler  namespace: kube-system  labels:    k8s-app: kube-dns-autoscaler    kubernetes.io/cluster-service: "true"    addonmanager.kubernetes.io/mode: Reconcilespec:  selector:    matchLabels:      k8s-app: kube-dns-autoscaler  template:    metadata:      labels:        k8s-app: kube-dns-autoscaler      annotations:        scheduler.alpha.kubernetes.io/critical-pod: ''    spec:      priorityClassName: system-cluster-critical      containers:      - name: autoscaler        image: gcr.azk8s.cn/google_containers/cluster-proportional-autoscaler-amd64:1.1.2-r2        resources:            requests:                cpu: "20m"                memory: "10Mi"        command:          - /cluster-proportional-autoscaler          - --namespace=kube-system          - --configmap=kube-dns-autoscaler          # Should keep target in sync with cluster/addons/dns/kube-dns.yaml.base          - --target=Deployment/coredns          # When cluster is using large nodes(with more cores), "coresPerReplica" should dominate.          # If using small nodes, "nodesPerReplica" should dominate.          - --default-params={"linear":{"coresPerReplica":256,"nodesPerReplica":16,"preventSinglePointFailure":true}}          - --logtostderr=true          - --v=2      tolerations:      - key: "CriticalAddonsOnly"        operator: "Exists"      serviceAccountName: kube-dns-autoscaler

四、其他

4.1、集群测试

为测试集群工作正常,我们创建一个 deployment 和一个 service,用于测试联通性和 DNS 工作是否正常;测试配置如下

  • test.yaml
apiVersion: apps/v1kind: Deploymentmetadata:  name: test  labels:    app: testspec:  replicas: 5  selector:    matchLabels:      app: test  template:    metadata:      labels:        app: test    spec:      containers:      - name: test        image: nginx:1.14.2-alpine        ports:        - containerPort: 80---apiVersion: v1kind: Servicemetadata:  name: test-servicespec:  selector:    app: test  ports:  - name: nginx    port: 80    nodePort: 30001    targetPort: 80    protocol: TCP  type: NodePort

测试方式很简单,进入某一个 pod ping 其他 pod ip 确认网络是否正常,直接访问 service 名称测试 DNS 是否工作正常,这里不再演示

4.2、其他说明

此次搭建开启了大部分认证,限于篇幅原因没有将每个选项作用做完整解释,推荐搭建完成后仔细阅读以下 --help 中的描述(官方文档页面有时候更新不完整);目前 apiserver 仍然保留了 8080 端口(因为直接使用 kubectl 方便),但是在高安全性环境请关闭 8080 端口,因为即使绑定在 127.0.0.1 上,对于任何能够登录 master 机器的用户仍然能够不经验证操作整个集群

]]>
Kubernetes Kubernetes http://mritd.com/2019/03/16/set-up-kubernetes-1.13.4-cluster/#disqus_thread
Kubernetes sample-cli-plugin 源码分析 http://mritd.com/2019/01/16/understand-kubernetes-sample-cli-plugin-source-code/ http://mritd.com/2019/01/16/understand-kubernetes-sample-cli-plugin-source-code/ Wed, 16 Jan 2019 04:16:42 GMT 写这篇文章的目的是为了继续上篇 [Kubernetes 1.12 新的插件机制](/2018/11/30/kubectl-plugin-new-solution-on-kubernetes-1.12/) 中最后部分对 `Golang 的插件辅助库` 说明;以及为后续使用 Golang 编写自己的 Kubernetes 插件做一个基础铺垫;顺边说一下 **sample-cli-plugin 这个项目是官方为 Golang 开发者编写的一个用于快速切换配置文件中 Namespace 的一个插件样例**

写这篇文章的目的是为了继续上篇 Kubernetes 1.12 新的插件机制 中最后部分对 Golang 的插件辅助库 说明;以及为后续使用 Golang 编写自己的 Kubernetes 插件做一个基础铺垫;顺边说一下 sample-cli-plugin 这个项目是官方为 Golang 开发者编写的一个用于快速切换配置文件中 Namespace 的一个插件样例

一、基础准备

在开始分析源码之前,我们假设读者已经熟悉 Golang 语言,至少对基本语法、指针、依赖管理工具有一定认知;下面介绍一下 sample-cli-plugin 这个项目一些基础核心的依赖:

1.1、Cobra 终端库

这是一个强大的 Golang 的 command line interface 库,其支持用非常简单的代码创建出符合 Unix 风格的 cli 程序;甚至官方提供了用于创建 cli 工程脚手架的 cli 命令工具;Cobra 官方 Github 地址 点击这里,具体用法请自行 Google,以下只做一个简单的命令定义介绍(docker、kubernetes 终端 cli 都基于这个库)

# 每一个命令(不论是子命令还是主命令)都会是一个 cobra.Command 对象var lsCmd = &cobra.Command{    // 一些命令帮助文档有关的描述信息    Use:   "ls",    Short: "A brief description of your command",    Long: `A longer description that spans multiple lines and likely contains examplesand usage of using your command. For example:Cobra is a CLI library for Go that empowers applications.This application is a tool to generate the needed filesto quickly create a Cobra application.`,    // 命令运行时真正执行逻辑,如果需要返回 Error 信息,我们一般设置 RunE    Run: func(cmd *cobra.Command, args []string) {        fmt.Println("ls called")    },}// 为这个命令添加 flag,比如 `--help`、`-p`// PersistentFlags() 方法添加的 flag 在所有子 command 也会生效// Cobra 的 command 可以无限级联,比如 `kubectl get pod` 就是在 `kubectl` command 下增加了子 `get` commandlsCmd.PersistentFlags().String("foo", "", "A help for foo")// Flags() 方法添加的 flag 仅在直接调用此子命令时生效lsCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")

1.2、vendor 依赖

vendor 目录用于存放 Golang 的依赖库,sample-cli-plugin 这个项目采用 godep 工具管理依赖;依赖配置信息被保存在 Godeps/Godeps.json 中,一般项目不会上传 vendor 目录,因为它的依赖信息已经在 Godeps.json 中存在,只需要在项目下使用 godep restore 命令恢复就可自动重新下载;这里上传了 vendor 目录的原因应该是为了方便开发者直接使用 go get 命令安装;顺边说一下在 Golang 新版本已经开始转换到 go mod 依赖管理工具,标志就是项目下会有 go.mod 文件

二、源码分析

2.1、环境搭建

这里准备一笔带过了,基本就是 clone 源码到 $GOPATH/src/k8s.io/sample-cli-plugin 目录,然后在 GoLand 中打开;目前我使用的 Go 版本为最新的 1.11.4;以下时导入源码后的截图

GoLand

2.2、定位核心运行方法

熟悉过 Cobra 库以后,再从整个项目包名上分析,首先想到的启动入口应该在 cmd 包下(一般 cmd 包下的文件都会编译成最终可执行文件名,Kubernetes 也是一样)

main

从以上截图中可以看出,首先通过 cmd.NewCmdNamespace 方法创建了一个 Command 对象 root,然后调用了 root.Execute 就结束了;那么也就说明 root 这个 Command 是唯一的核心命令对象,整个插件实现都在这个 root 里;所以我们需要查看一下这个 cmd.NewCmdNamespace 是如何对它初始化的,找到 Cobra 中的 Run 或者 RunE 设置

NewCmdNamespace

定位到 NewCmdNamespace 方法以后,基本上就是标准的 Cobra 库的使用方式了;**从截图上可以看到,RunE 设置的函数总共运行了 3 个动作: o.Completeo.Validateo.Run**;所以接下来我们主要分析这三个方法就行了

2.3、NamespaceOptions 结构体

在分析上面说的这三个方法之前,我们还应当了解一下这个 o 是什么玩意

NamespaceOptions

从源码中可以看到,o 这个对象由 NewNamespaceOptions 创建,而 NewNamespaceOptions 方法返回的实际上是一个 NamespaceOptions 结构体;接下来我们需要研究一下这个结构体都是由什么组成的,换句话说要基本大致上整明白结构体的基本结构,比如里面的属性都是干啥的

2.3.1、*genericclioptions.ConfigFlags

首先看下第一个属性 configFlags,它的实际类型是 *genericclioptions.ConfigFlags,点击查看以后如下

genericclioptions.ConfigFlags

从这些字段上来看,我们可以暂且模糊的推测出这应该是个基础配置型的字段,负责存储一些全局基本设置,比如 API Server 认证信息等

2.3.2、*api.Context

下面这两个 resultingContextresultingContextName 就很好理解了,从名字上看就可以知道它们应该是用来存储结果集的 Context 信息的;当然这个 *api.Context 就是 Kubernetes 配置文件中 Context 的 Go 结构体

2.3.3、userSpecified*

这几个字段从名字上就可以区分出,他们应该用于存储用户设置的或者说是通过命令行选项输入的一些指定配置信息,比如 Cluster、Context 等

2.3.4、rawConfig

rawConfig 这个变量名字有点子奇怪,不过它实际上是个 api.Config;里面保存了与 API Server 通讯的配置信息;至于为什么要有这玩意,是因为配置信息输入源有两个: cli 命令行选项(eg: --namespace)和用户配置文件(eg: ~/.kube/config);最终这两个地方的配置合并后会存储在这个 rawConfig 里

2.3.5、listNamespaces

这个变量实际上相当于一个 flag,用于存储插件是否使用了 --list 选项;在分析结构体这里没法看出来;不过只要稍稍的多看一眼代码就能看在 NewCmdNamespace 方法中有这么一行代码

listNamespaces

2.4、核心处理逻辑

介绍完了结构体的基本属性,最后我们只需要弄明白在核心 Command 方法内运行的这三个核心方法就行了

core func

2.4.1、*NamespaceOptions.Complete

这个方法代码稍微有点多,这里不会对每一行代码都做解释,只要大体明白都在干什么就行了;我们的目的是理解它,后续模仿它创造自己的插件;下面是代码截图

NamespaceOptions.Complete

从截图上可以看到,首先弄出了 rawConfig 这个玩意,rawConfig 上面也提到了,它就是终端选项和用户配置文件的最终合并,至于为什么可以查看 ToRawKubeConfigLoader().RawConfig() 这两个方法的注释和实现即可;

接下来就是各种获取插件执行所需要的变量信息,比如获取用户指定的 NamespaceClusterContext 等,其中还包含了一些必要的校验;比如不允许使用 kubectl ns NS_NAME1 --namespace NS_NAME2 这种操作(因为这么干很让人难以理解 “你到底是要切换到 NS_NAME1 还是 NS_NAME2“)

最后从 153o.resultingContext = api.NewContext() 开始就是创建最终的 resultingContext 对象,把获取到的用户指定的 Namespace 等各种信息赋值好,为下一步将其持久化到配置文件中做准备

2.4.2、*NamespaceOptions.Validate

这个方法看名字就知道,里面全是对最终结果的校验;比如检查一下 rawConfig 中的 CurrentContext 是否获取到了,看看命令行参数是否正确,确保你不会瞎鸡儿输入 kubectl ns NS_NAME1 NS_NAME2 这种命令

NamespaceOptions.Validate

2.4.3、*NamespaceOptions.Run

第一步合并配置信息并获取到用户设置(输入)的配置,第二部做参数校验;可以说前面的两步操作都是为这一步做准备,Run 方法真正的做了配置文件写入、终端返回结果打印操作

NamespaceOptions.Run

可以看到,Run 方法第一步就是更加谨慎的检查了一下参数是否正常,然后调用了 o.setNamespace;这个方法截图如下

NamespaceOptions.setNamespace

这个 setNamespace是真正的做了配置文件写入动作的,实际写入方法就是 clientcmd.ModifyConfig;这个是 Kubernetes client-go 提供的方法,这些库的作用就是提供给我们非常方便的 API 操作;比如修改配置文件,你不需要关心配置文件在哪,你更不需要关系文件句柄是否被释放

o.setNamespace 方法以后其实就没什么看头了,毕竟插件的核心功能就是快速修改 Namespace;下面的各种 for 循环遍历其实就是在做打印输出;比如当你没有设置 Namespace 而使用了 --list 选项,插件就通过这里帮你打印设置过那些 Namespace

三、插件总结

分析完了这个官方的插件,然后想一下自己以后写插件可能的需求,最后对比一下,可以为以后写插件做个总结:

  • 我们最好也弄个 xxxOptions 这种结构体存存一些配置
  • 结构体内至少我们应当存储 configFlagsrawConfig 这两个基础配置信息
  • 结构体内其它参数都应当是跟自己实际业务有关的
  • 最后在在结构体上增加适当的方法完成自己的业务逻辑并保持好适当的校验

转载请注明出n,本文采用 [CC4.0](http://c 1.12 新的插件机制](/2018/11/30/kubectl-plugin-new-solution-on-kubernetes-1.12/) 中最后部分对 Golang 的插件辅助库 说明;以及为后续使用 Golang 编写自己的 Kubernetes 插件做一个基础铺垫;顺边说一下 sample-cli-plugin 这个项目是官方为 Golang 开发者编写的一个用于快速切换配置文件中 Namespace 的一个插件样例

]]>
Kubernetes Kubernetes http://mritd.com/2019/01/16/understand-kubernetes-sample-cli-plugin-source-code/#disqus_thread
Kubernetes 1.12 新的插件机制 http://mritd.com/2018/11/30/kubectl-plugin-new-solution-on-kubernetes-1.12/ http://mritd.com/2018/11/30/kubectl-plugin-new-solution-on-kubernetes-1.12/ Thu, 29 Nov 2018 16:05:34 GMT 在很久以前的版本研究过 kubernetes 的插件机制,当时弄了一个快速切换 `namespace` 的小插件;最近把自己本机的 kubectl 升级到了 1.12,突然发现插件不能用了;撸了一下文档发现插件机制彻底改了...

在很久以前的版本研究过 kubernetes 的插件机制,当时弄了一个快速切换 namespace 的小插件;最近把自己本机的 kubectl 升级到了 1.12,突然发现插件不能用了;撸了一下文档发现插件机制彻底改了…

一、插件编写语言

kubernetes 1.12 新的插件机制在编写语言上同以前一样,可以以任意语言编写,只要能弄一个可执行的文件出来就行,插件可以是一个 bashpython 脚本,也可以是 Go 等编译语言最终编译的二进制;以下是一个 Copy 自官方文档的 bash 编写的插件样例

#!/bin/bash# optional argument handlingif [[ "$1" == "version" ]]then    echo "1.0.0"    exit 0fi# optional argument handlingif [[ "$1" == "config" ]]then    echo $KUBECONFIG    exit 0fiecho "I am a plugin named kubectl-foo"

二、插件加载方式

2.1、插件位置

1.12 kubectl 插件最大的变化就是加载方式变了,由原来的放置在指定位置,还要为其编写 yaml 配置变成了现在的类似 git 扩展命令的方式: 只要放置在 PATH 下,并以 kubectl- 开头的可执行文件都被认为是 kubectl 的插件;所以你可以随便弄个小脚本(比如上面的代码),然后改好名字赋予可执行权限,扔到 PATH 下即可

test-plugin

2.2、插件变量

同以前不通,以前版本的执行插件时,kubectl 会向插件传递一些特定的与 kubectl 相关的变量,现在则只会传递标准变量;即 kubectl 能读到什么变量,插件就能读到,其他的私有化变量(比如 KUBECTL_PLUGINS_CURRENT_NAMESPACE)不会再提供

plugin env

并且新版本的插件体系,所有选项(flag) 将全部交由插件本身处理,kubectl 不会再解析,比如下面的 --help 交给了自定义插件处理,由于脚本内没有处理这个选项,所以相当于选项无效了

plugin flag

还有就是 传递给插件的第一个参数永远是插件自己的绝对位置,比如这个 test 插件在执行时的 $0/usr/local/bin/kubectl-test

2.3、插件命名及查找

目前在插件命名及查找顺序上官方文档写的非常详尽,不给过对于普通使用者来说,实际上命名规则和查找与常规的 Linux 下的命令查找机制相同,只不过还做了增强;增强后的基本规则如下

  • PATH 优先匹配原则
  • 短横线 - 自动分割匹配以及智能转义
  • 以最精确匹配为首要目标
  • 查找失败自动转换参数

PATH 优先匹配原则跟传统的命令查找一致,即当多个路径下存在同名的插件时,则采用最先查找到的插件

plugin path

当你的插件文件名中包含 - ,并且 kubectl 在无法精确找到插件时会尝试自动拼接命令来尝试匹配;如下所示,在没有找到 kubectl-test 这个命令时会尝试拼接参数查找

auto merge

由于以上这种查找机制,当命令中确实包含 - 时,必须进行转义以 _ 替换,否则 kubectl 会提示命令未找到错误;替换后可直接使用 kubectl 插件命令(包含-) 执行,同时也支持以原始插件名称执行(使用 _)

name contains dash

在复杂插件体系下,多个插件可能包含同样的前缀,此时将遵序最精确查找原则;即当两个插件 kubectl-test-aaakubectl-test-aaa-bbb 同时存在,并且执行 kubectl test aaa bbb 命令时,优先匹配最精确的插件 kubectl-test-aaa-bbb而不是将 bbb 作为参数传递给 kubectl-test-aaa 插件

precise search

2.4、总结

插件查找机制在一般情况下与传统 PATH 查找方式相同,同时 kubectl 实现了智能的 - 自动匹配查找、更精确的命令命中功能;这两种机制的实现主要为了方便编写插件的命令树(插件命令的子命令…),类似下面这种

$ ls ./plugin_command_treekubectl-parentkubectl-parent-subcommandkubectl-parent-subcommand-subsubcommand

当出现多个位置有同名插件时,执行 kubectl plugin list 能够检测出哪些插件由于 PATH 查找顺序原因导致永远不会被执行问题

$ kubectl plugin listThe following kubectl-compatible plugins are available:test/fixtures/pkg/kubectl/plugins/kubectl-foo/usr/local/bin/kubectl-foo  - warning: /usr/local/bin/kubectl-foo is overshadowed by a similarly named plugin: test/fixtures/pkg/kubectl/plugins/kubectl-fooplugins/kubectl-invalid  - warning: plugins/kubectl-invalid identified as a kubectl plugin, but it is not executableerror: 2 plugin warnings were found

三、Golang 的插件辅助库

由于插件机制的变更,导致其他语言编写的插件在实时获取某些配置信息、动态修改 kubectl 配置方面可能造成一定的阻碍;为此 kubernetes 提供了一个 command line runtime package,使用 Go 编写插件,配合这个库可以更加方便的解析和调整 kubectl 的配置信息

官方为了演示如何使用这个 cli-runtime 库编写了一个 namespace 切换的插件(自己白写了…),仓库地址在 Github 上,基本编译使用如下(直接 go get 后编译文件默认为目录名 cmd)

➜  ~ go get k8s.io/sample-cli-plugin/cmd➜  ~ sudo mv gopath/bin/cmd /usr/local/bin/kubectl-ns➜  ~ kubectl nsdefault➜  ~ kubectl ns --helpView or set the current namespaceUsage:  ns [new-namespace] [flags]Examples:        # view the current namespace in your KUBECONFIG        kubectl ns        # view all of the namespaces in use by contexts in your KUBECONFIG        kubectl ns --list        # switch your current-context to one that contains the desired namespace        kubectl ns fooFlags:      --as string                      Username to impersonate for the operation      --as-group stringArray           Group to impersonate for the operation, this flag can be repeated to specify multiple groups.      --cache-dir string               Default HTTP cache directory (default "/Users/mritd/.kube/http-cache")      --certificate-authority string   Path to a cert file for the certificate authority      --client-certificate string      Path to a client certificate file for TLS      --client-key string              Path to a client key file for TLS      --cluster string                 The name of the kubeconfig cluster to use      --context string                 The name of the kubeconfig context to use  -h, --help                           help for ns      --insecure-skip-tls-verify       If true, the server's certificate will not be checked for validity. This will make your http connections insecure      --kubeconfig string              Path to the kubeconfig file to use for CLI requests.      --list                           if true, print the list of all namespaces in the current KUBECONFIG  -n, --namespace string               If present, the namespace scope for this CLI request      --request-timeout string         The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0")  -s, --server string                  The address and port of the Kubernetes API server      --token string                   Bearer token for authentication to the API server      --user string                    The name of the kubeconfig user to use

限于篇幅原因,具体这个 cli-runtime 包怎么用请自行参考官方写的这个 sample-cli-plugin (其实并不怎么 “simple”…)

本文参考文档:

]]>
Kubernetes Kubernetes http://mritd.com/2018/11/30/kubectl-plugin-new-solution-on-kubernetes-1.12/#disqus_thread
Go 编写的一些常用小工具 http://mritd.com/2018/11/27/simple-tool-written-in-golang/ http://mritd.com/2018/11/27/simple-tool-written-in-golang/ Tue, 27 Nov 2018 04:45:46 GMT 迫于 Github 上 Star 的项目有点多,今天整理一下一些有意思的 Go 编写的小工具;大多数为终端下的实用工具,装逼的比如天气预报啥的就不写了

迫于 Github 上 Star 的项目有点多,今天整理一下一些有意思的 Go 编写的小工具;大多数为终端下的实用工具,装逼的比如天气预报啥的就不写了

syncthing

强大的文件同步工具,构建私人同步盘 👉 Github

syncthing

fzf

一个强大的终端文件浏览器 👉 Github

fzf

hey

http 负载测试工具,简单好用 👉 Github

Usage: hey [options...] <url>Options:  -n  Number of requests to run. Default is 200.  -c  Number of requests to run concurrently. Total number of requests cannot      be smaller than the concurrency level. Default is 50.  -q  Rate limit, in queries per second (QPS). Default is no rate limit.  -z  Duration of application to send requests. When duration is reached,      application stops and exits. If duration is specified, n is ignored.      Examples: -z 10s -z 3m.  -o  Output type. If none provided, a summary is printed.      "csv" is the only supported alternative. Dumps the response      metrics in comma-separated values format.  -m  HTTP method, one of GET, POST, PUT, DELETE, HEAD, OPTIONS.  -H  Custom HTTP header. You can specify as many as needed by repeating the flag.      For example, -H "Accept: text/html" -H "Content-Type: application/xml" .  -t  Timeout for each request in seconds. Default is 20, use 0 for infinite.  -A  HTTP Accept header.  -d  HTTP request body.  -D  HTTP request body from file. For example, /home/user/file.txt or ./file.txt.  -T  Content-type, defaults to "text/html".  -a  Basic authentication, username:password.  -x  HTTP Proxy address as host:port.  -h2 Enable HTTP/2.  -host    HTTP Host header.  -disable-compression  Disable compression.  -disable-keepalive    Disable keep-alive, prevents re-use of TCP                        connections between different HTTP requests.  -disable-redirects    Disable following of HTTP redirects  -cpus                 Number of used cpu cores.                        (default for current machine is 8 cores)

vegeta

http 负载测试工具,功能强大 👉 Github

Usage: vegeta [global flags] <command> [command flags]global flags:  -cpus int        Number of CPUs to use (default 8)  -profile string        Enable profiling of [cpu, heap]  -version        Print version and exitattack command:  -body string        Requests body file  -cert string        TLS client PEM encoded certificate file  -connections int        Max open idle connections per target host (default 10000)  -duration duration        Duration of the test [0 = forever]  -format string        Targets format [http, json] (default "http")  -h2c        Send HTTP/2 requests without TLS encryption  -header value        Request header  -http2        Send HTTP/2 requests when supported by the server (default true)  -insecure        Ignore invalid server TLS certificates  -keepalive        Use persistent connections (default true)  -key string        TLS client PEM encoded private key file  -laddr value        Local IP address (default 0.0.0.0)  -lazy        Read targets lazily  -max-body value        Maximum number of bytes to capture from response bodies. [-1 = no limit] (default -1)  -name string        Attack name  -output string        Output file (default "stdout")  -rate value        Number of requests per time unit (default 50/1s)  -redirects int        Number of redirects to follow. -1 will not follow but marks as success (default 10)  -resolvers value        List of addresses (ip:port) to use for DNS resolution. Disables use of local system DNS. (comma separated list)  -root-certs value        TLS root certificate files (comma separated list)  -targets string        Targets file (default "stdin")  -timeout duration        Requests timeout (default 30s)  -workers uint        Initial number of workers (default 10)encode command:  -output string        Output file (default "stdout")  -to string        Output encoding [csv, gob, json] (default "json")plot command:  -output string        Output file (default "stdout")  -threshold int        Threshold of data points above which series are downsampled. (default 4000)  -title string        Title and header of the resulting HTML page (default "Vegeta Plot")report command:  -every duration        Report interval  -output string        Output file (default "stdout")  -type string        Report type to generate [text, json, hist[buckets]] (default "text")examples:  echo "GET http://localhost/" | vegeta attack -duration=5s | tee results.bin | vegeta report  vegeta report -type=json results.bin > metrics.json  cat results.bin | vegeta plot > plot.html  cat results.bin | vegeta report -type="hist[0,100ms,200ms,300ms]"

dive

功能强大的 Docker 镜像分析工具,可以查看每层镜像的具体差异等 👉 Github

dive

ctop

容器运行时资源分析,如 CPU、内存消耗等 👉 Github

ctop

container-diff

Google 推出的工具,功能就顾名思义了 👉 Github

container-diff

transfer.sh

快捷的终端文件分享工具 👉 Github

transfer.sh

vuls

Linux/FreeBSD 漏洞扫描工具 👉 Github

vuls

restic

高性能安全的文件备份工具 👉 Github

restic

gitql

使用 sql 的方式查询 git 提交 👉 Github

gitql

gitflow-toolkit

帮助生成满足 Gitflow 格式 commit message 的小工具(自己写的) 👉 Github

gitflow-toolkit

git-chglog

对主流的 Gitflow 格式的 commit message 生成 CHANGELOG 👉 Github

git-chglog

grv

一个 git 终端图形化浏览工具 👉 Github

grv

jid

命令行 json 格式化处理工具,类似 jq,不过感觉更加强大 👉 Github

jid

annie

类似 youget 的一个视频下载工具,可以解析大部分视频网站直接下载 👉 Github

$ annie -i http://www.youtube.com/watch?v=dQw4w9WgXcQ Site:      YouTube youtube.com Title:     Rick Astley - Never Gonna Give You Up (Video) Type:      video Streams:   # All available quality     [248]  -------------------     Quality:         1080p video/webm; codecs="vp9"     Size:            49.29 MiB (51687554 Bytes)     # download with: annie -f 248 ...     [137]  -------------------     Quality:         1080p video/mp4; codecs="avc1.640028"     Size:            43.45 MiB (45564306 Bytes)     # download with: annie -f 137 ...     [398]  -------------------     Quality:         720p video/mp4; codecs="av01.0.05M.08"     Size:            37.12 MiB (38926432 Bytes)     # download with: annie -f 398 ...     [136]  -------------------     Quality:         720p video/mp4; codecs="avc1.4d401f"     Size:            31.34 MiB (32867324 Bytes)     # download with: annie -f 136 ...     [247]  -------------------     Quality:         720p video/webm; codecs="vp9"     Size:            31.03 MiB (32536181 Bytes)     # download with: annie -f 247 ...

up

Linux 下管道式终端搜索工具 👉 Github

up

lego

Let’s Encrypt 证书申请工具 👉 Github

NAME:   lego - Let's Encrypt client written in GoUSAGE:   lego [global options] command [command options] [arguments...]COMMANDS:     run      Register an account, then create and install a certificate     revoke   Revoke a certificate     renew    Renew a certificate     dnshelp  Shows additional help for the --dns global option     help, h  Shows a list of commands or help for one commandGLOBAL OPTIONS:   --domains value, -d value   Add a domain to the process. Can be specified multiple times.   --csr value, -c value       Certificate signing request filename, if an external CSR is to be used   --server value, -s value    CA hostname (and optionally :port). The server certificate must be trusted in order to avoid further modifications to the client. (default: "http://acme-v02.api.letsencrypt.org/directory")   --email value, -m value     Email used for registration and recovery contact.   --filename value            Filename of the generated certificate   --accept-tos, -a            By setting this flag to true you indicate that you accept the current Let's Encrypt terms of service.   --eab                       Use External Account Binding for account registration. Requires --kid and --hmac.   --kid value                 Key identifier from External CA. Used for External Account Binding.   --hmac value                MAC key from External CA. Should be in Base64 URL Encoding without padding format. Used for External Account Binding.   --key-type value, -k value  Key type to use for private keys. Supported: rsa2048, rsa4096, rsa8192, ec256, ec384 (default: "rsa2048")   --path value                Directory to use for storing the data (default: "./.lego")   --exclude value, -x value   Explicitly disallow solvers by name from being used. Solvers: "http-01", "dns-01", "tls-alpn-01".   --webroot value             Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge   --memcached-host value      Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.   --http value                Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port   --tls value                 Set the port and interface to use for TLS based challenges to listen on. Supported: interface:port or :port   --dns value                 Solve a DNS challenge using the specified provider. Disables all other challenges. Run 'lego dnshelp' for help on usage.   --http-timeout value        Set the HTTP timeout value to a specific value in seconds. The default is 10 seconds. (default: 0)   --dns-timeout value         Set the DNS timeout value to a specific value in seconds. The default is 10 seconds. (default: 0)   --dns-resolvers value       Set the resolvers to use for performing recursive DNS queries. Supported: host:port. The default is to use the system resolvers, or Google's DNS resolvers if the system's cannot be determined.   --pem                       Generate a .pem file by concatenating the .key and .crt files together.   --help, -h                  show help   --version, -v               print the version

noti

贼好用的终端命令异步执行通知工具 👉 Github

noti

gosu

临时切换到指定用户运行特定命令,方便测试权限问题 👉 Github

$ gosuUsage: ./gosu user-spec command [args]   eg: ./gosu tianon bash       ./gosu nobody:root bash -c 'whoami && id'       ./gosu 1000:1 id

sup

类似 Ansible 的一个批量执行工具,暂且称之为低配版 Ansible 👉 Github

sup

aptly

Debian 仓库管理工具 👉 Github

aptly

mmh

支持无限跳板机登录的 ssh 小工具(自己写的) 👉 Github

mmh

]]>
Golang Golang http://mritd.com/2018/11/27/simple-tool-written-in-golang/#disqus_thread
远程 Debug kubeadm http://mritd.com/2018/11/25/kubeadm-remote-debug/ http://mritd.com/2018/11/25/kubeadm-remote-debug/ Sun, 25 Nov 2018 03:11:28 GMT 最近在看 kubeadm 的源码,不过有些东西光看代码还是没法太清楚,还是需要实际运行才能看到具体代码怎么跑的,还得打断点 debug;无奈的是本机是 mac,debug 得在 Linux 下,so 研究了一下 remote debug

最近在看 kubeadm 的源码,不过有些东西光看代码还是没法太清楚,还是需要实际运行才能看到具体代码怎么跑的,还得打断点 debug;无奈的是本机是 mac,debug 得在 Linux 下,so 研究了一下 remote debug

一、环境准备

  • GoLand 2018.2.4
  • Golang 1.11.2
  • delve v1.1.0
  • Kubernetest master
  • Ubuntu 18.04
  • 能够高速访问外网(自行理解)

这里不会详细写如何安装 Go 开发环境以及 GoLand 安装,本文默认读者已经至少已经对 Go 开发环境以及代码有一定了解;顺便提一下 GoLand,这玩意属于 jetbrains 系列 IDE,在大约 2018.1 版本后在线激活服务器已经全部失效,不过网上还有其他本地离线激活工具,具体请自行 Google,如果后续工资能支撑得起,请补票支持正版(感恩节全家桶半价真香😂)

1.1、获取源码

需要注意的是 Kubernetes 源码虽然托管在 Github,但是在使用 go get 的时候要使用 k8s.io 域名

go get -d k8s.io/kubernetes

go get 命令是接受标准的 http 代理的,这个源码下载会非常慢,源码大约 1G 左右,所以最好使用加速工具下载

➜  ~ which proxy/usr/local/bin/proxy➜  ~ cat /usr/local/bin/proxy#!/bin/bashhttp_proxy=http://127.0.0.1:8123 http_proxy=http://127.0.0.1:8123 $*➜  ~ proxy go get -d k8s.io/kubernetes

1.2、安装 delve

delve 是一个 Golang 的 debug 工具,有点类似 gdb,不过是专门针对 Golang 的,GoLand 的 debug 实际上就是使用的这个开源工具;为了进行远程 debug,运行 kubeadm 的机器必须安装 delve,从而进行远程连接

# 同样这里省略在 Linux 安装 go 环境操作go get -u github.com/derekparker/delve/cmd/dlv

二、远程 Debug

2.1、重新编译 kubeadm

默认情况下直接编译出的 kubeadm 是无法进行 debug 的,因为 Golang 的编译器会进行编译优化,比如进行内联等;所以要关闭编译优化和内联,方便 debug

cd ${GOPATH}/src/k8s.io/kubernetes/cmd/kubeadmGOOS="linux" GOARCH="amd64" go build -gcflags "all=-N -l"

2.2、远程运行 kubeadm

将编译好的 kubeadm 复制到远程,并且使用 delve 启动它,此时 delve 会监听 api 端口,GoLand 就可以远程连接过来了

dlv --listen=192.168.1.61:2345 --headless=true --api-version=2 exec ./kubeadm init

注意: 要指定需要 debug 的 kubeadm 的子命令,否则可能出现连接上以后 GoLand 无反应的情况

2.3、运行 GoLand

在 GoLand 中打开 kubernetes 源码,在需要 debug 的代码中打上断点,这里以 init 子命令为例

首先新建一个远程 debug configuration

create configuration

名字可以随便写,主要是地址和端口

conifg delve

接下来在目标源码位置打断点,以下为 init 子命令的源码位置

create breakpoint

最后只需要点击 debug 按钮即可

debug

在没有运行 GoLand debug 之前,目标机器的实际指令是不会运行的,也就是说在 GoLand 没有连接到远程 delve 启动的 kubeadm init 命令之前,kubeadm init 并不会真正运行;当点击 GoLand 的终止 debug 按钮后,远程的 delve 也会随之退出

stop

]]>
Kubernetes Golang Kubernetes Golang http://mritd.com/2018/11/25/kubeadm-remote-debug/#disqus_thread
Mac: Extract JDK to folder, without running installer http://mritd.com/2018/11/23/extract-jdk-to-folder-on-mac/ http://mritd.com/2018/11/23/extract-jdk-to-folder-on-mac/ Fri, 23 Nov 2018 04:33:20 GMT 重装了 mac 系统,由于一些公司项目必须使用 Oracle JDK(验证码等组件用了一些 Oracle 独有的 API) 所以又得重新安装;但是 Oracle 只提供了 pkg 的安装方式,研究半天找到了一个解包 pkg 的安装方式,这里记录一下

重装了 mac 系统,由于一些公司项目必须使用 Oracle JDK(验证码等组件用了一些 Oracle 独有的 API) 所以又得重新安装;但是 Oracle 只提供了 pkg 的安装方式,研究半天找到了一个解包 pkg 的安装方式,这里记录一下

不使用 pkg 的原因是每次更新版本都要各种安装,最烦人的是 IDEA 选择 JDK 时候弹出的文件浏览器没法进入到这种方式安装的 JDK 的系统目录…mmp,后来从国外网站找到了一篇文章,基本套路如下

  • 下载 Oracle JDK,从 dmg 中拷贝 pkg 到任意位置
  • 解压 pkg 到任意位置 pkgutil --expand your_jdk.pkg jdkdir
  • 进入到目录中,解压主文件 cd jdkdir/jdk_version.pkg && cpio -idv < Payload
  • 移动 jdk 到任意位置 mv Contents/Home ~/myjdk

原文地址: OS X: Extract JDK to folder, without running installer

]]>
Java Java http://mritd.com/2018/11/23/extract-jdk-to-folder-on-mac/#disqus_thread
Go ssh 交互式执行命令 http://mritd.com/2018/11/09/go-interactive-shell/ http://mritd.com/2018/11/09/go-interactive-shell/ Fri, 09 Nov 2018 15:13:44 GMT 最近在写一个跳板机登录的小工具,其中涉及到了用 Go 来进行交互式执行命令,简单地说就是弄个终端出来;一开始随便 Google 了一下,copy 下来基本上就是能跑了...但是后来发现了一些各种各样的小问题,强迫症的我实在受不了,最后翻了一下 Teleport 的源码,从中学到了不少有用的知识,这里记录一下

最近在写一个跳板机登录的小工具,其中涉及到了用 Go 来进行交互式执行命令,简单地说就是弄个终端出来;一开始随便 Google 了一下,copy 下来基本上就是能跑了…但是后来发现了一些各种各样的小问题,强迫症的我实在受不了,最后翻了一下 Teleport 的源码,从中学到了不少有用的知识,这里记录一下

一、原始版本

不想看太多可以直接跳转到 第三部分 拿代码

1.1、样例代码

一开始随便 Google 出来的代码,copy 上就直接跑;代码基本如下:

func main() {// 创建 ssh 配置sshConfig := &ssh.ClientConfig{User: "root",Auth: []ssh.AuthMethod{ssh.Password("password"),},HostKeyCallback: ssh.InsecureIgnoreHostKey(),Timeout:         5 * time.Second,}// 创建 clientclient, err := ssh.Dial("tcp", "192.168.1.20:22", sshConfig)checkErr(err)defer client.Close()// 获取 sessionsession, err := client.NewSession()checkErr(err)defer session.Close()// 拿到当前终端文件描述符fd := int(os.Stdin.Fd())termWidth, termHeight, err := terminal.GetSize(fd)// request ptyerr = session.RequestPty("xterm-256color", termHeight, termWidth, ssh.TerminalModes{})checkErr(err)// 对接 stdsession.Stdout = os.Stdoutsession.Stderr = os.Stderrsession.Stdin = os.Stdinerr = session.Shell()checkErr(err)err = session.Wait()checkErr(err)}func checkErr(err error) {if err != nil {fmt.Println(err)os.Exit(1)}}

1.2、遇到的问题

以上代码跑起来后,基本上遇到了以下问题:

  • 执行命令有回显,表现为敲一个 ls 出现两行
  • 本地终端大小调整,远端完全无反应,导致显示不全
  • Tmux 下终端连接后窗口标题显示的是原始命令,而不是目标机器 shell 环境的目录位置
  • 首次连接一些刚装完系统的机器可能出现执行命令后回显不换行

二、改进代码

2.1、回显问题

关于回显问题,实际上解决方案很简单,设置当前终端进入 raw 模式即可;代码如下:

// 拿到当前终端文件描述符fd := int(os.Stdin.Fd())// make rawstate, err := terminal.MakeRaw(fd)checkErr(err)defer terminal.Restore(fd, state)

代码很简单,网上一大堆,But…基本没有文章详细说这个 raw 模式到底是个啥玩意;好在万能的 StackOverflow 对于不熟悉 Linux 的人给出了一个很清晰的解释: What’s the difference between a “raw” and a “cooked” device driver?

大致意思就是说 在终端处于 Cooked 模式时,当你输入一些字符后,默认是被当前终端 cache 住的,在你敲了回车之前这些文本都在 cache 中,这样允许应用程序做一些处理,比如捕获 Cntl-D 等按键,这时候就会出现敲回车后本地终端帮你打印了一下,导致出现类似回显的效果;当设置终端为 raw 模式后,所有的输入将不被 cache,而是发送到应用程序,在我们的代码中表现为通过 io.Copy 直接发送到了远端 shell 程序

2.2、终端大小问题

当本地调整了终端大小后,远程终端毫无反应;后来发现在 *ssh.Session 上有一个 WindowChange 方法,用于向远端发送窗口调整事件;解决方案就是启动一个 goroutine 在后台不断监听窗口改变事件,然后调用 WindowChange 即可;代码如下:

go func() {// 监听窗口变更事件sigwinchCh := make(chan os.Signal, 1)signal.Notify(sigwinchCh, syscall.SIGWINCH)fd := int(os.Stdin.Fd())termWidth, termHeight, err := terminal.GetSize(fd)if err != nil {fmt.Println(err)}for {select {// 阻塞读取case sigwinch := <-sigwinchCh:if sigwinch == nil {return}currTermWidth, currTermHeight, err := terminal.GetSize(fd)// 判断一下窗口尺寸是否有改变if currTermHeight == termHeight && currTermWidth == termWidth {continue}// 更新远端大小session.WindowChange(currTermHeight, currTermWidth)if err != nil {fmt.Printf("Unable to send window-change reqest: %s.", err)continue}termWidth, termHeight = currTermWidth, currTermHeight}}}()

2.3、Tmux 标题以及回显不换行

这两个问题实际上都是由于我们直接对接了 stderrstdoutstdin 造成的,实际上我们应当启动一个异步的管道式复制行为,并且最好带有 buf 的发送;代码如下:

stdin, err := session.StdinPipe()checkErr(err)stdout, err := session.StdoutPipe()checkErr(err)stderr, err := session.StderrPipe()checkErr(err)go io.Copy(os.Stderr, stderr)go io.Copy(os.Stdout, stdout)go func() {buf := make([]byte, 128)for {n, err := os.Stdin.Read(buf)if err != nil {fmt.Println(err)return}if n > 0 {_, err = stdin.Write(buf[:n])if err != nil {checkErr(err)}}}}()

三、完整代码

type SSHTerminal struct {Session *ssh.SessionexitMsg stringstdout  io.Readerstdin   io.Writerstderr  io.Reader}func main() {sshConfig := &ssh.ClientConfig{User: "root",Auth: []ssh.AuthMethod{ssh.Password("password"),},HostKeyCallback: ssh.InsecureIgnoreHostKey(),}client, err := ssh.Dial("tcp", "192.168.1.20:22", sshConfig)if err != nil {fmt.Println(err)}defer client.Close()err = New(client)if err != nil {fmt.Println(err)}}func (t *SSHTerminal) updateTerminalSize() {go func() {// SIGWINCH is sent to the process when the window size of the terminal has// changed.sigwinchCh := make(chan os.Signal, 1)signal.Notify(sigwinchCh, syscall.SIGWINCH)fd := int(os.Stdin.Fd())termWidth, termHeight, err := terminal.GetSize(fd)if err != nil {fmt.Println(err)}for {select {// The client updated the size of the local PTY. This change needs to occur// on the server side PTY as well.case sigwinch := <-sigwinchCh:if sigwinch == nil {return}currTermWidth, currTermHeight, err := terminal.GetSize(fd)// Terminal size has not changed, don't do anything.if currTermHeight == termHeight && currTermWidth == termWidth {continue}t.Session.WindowChange(currTermHeight, currTermWidth)if err != nil {fmt.Printf("Unable to send window-change reqest: %s.", err)continue}termWidth, termHeight = currTermWidth, currTermHeight}}}()}func (t *SSHTerminal) interactiveSession() error {defer func() {if t.exitMsg == "" {fmt.Fprintln(os.Stdout, "the connection was closed on the remote side on ", time.Now().Format(time.RFC822))} else {fmt.Fprintln(os.Stdout, t.exitMsg)}}()fd := int(os.Stdin.Fd())state, err := terminal.MakeRaw(fd)if err != nil {return err}defer terminal.Restore(fd, state)termWidth, termHeight, err := terminal.GetSize(fd)if err != nil {return err}termType := os.Getenv("TERM")if termType == "" {termType = "xterm-256color"}err = t.Session.RequestPty(termType, termHeight, termWidth, ssh.TerminalModes{})if err != nil {return err}t.updateTerminalSize()t.stdin, err = t.Session.StdinPipe()if err != nil {return err}t.stdout, err = t.Session.StdoutPipe()if err != nil {return err}t.stderr, err = t.Session.StderrPipe()go io.Copy(os.Stderr, t.stderr)go io.Copy(os.Stdout, t.stdout)go func() {buf := make([]byte, 128)for {n, err := os.Stdin.Read(buf)if err != nil {fmt.Println(err)return}if n > 0 {_, err = t.stdin.Write(buf[:n])if err != nil {fmt.Println(err)t.exitMsg = err.Error()return}}}}()err = t.Session.Shell()if err != nil {return err}err = t.Session.Wait()if err != nil {return err}return nil}func New(client *ssh.Client) error {session, err := client.NewSession()if err != nil {return err}defer session.Close()s := SSHTerminal{Session: session,}return s.interactiveSession()}
]]>
Golang Golang http://mritd.com/2018/11/09/go-interactive-shell/#disqus_thread
Go 代码的扩展套路 http://mritd.com/2018/10/23/golang-code-plugin/ http://mritd.com/2018/10/23/golang-code-plugin/ Tue, 23 Oct 2018 13:32:13 GMT 折腾 Go 已经有一段时间了,最近在用 Go 写点 web 的东西;在搭建脚手架的过程中总是有点不适应,尤其对可扩展性上总是感觉没有 Java 那么顺手;索性看了下 coredns 的源码,最后追踪到 caddy 源码;突然发现他们对代码内的 plugin 机制有一些骚套路,这里索性记录一下

折腾 Go 已经有一段时间了,最近在用 Go 写点 web 的东西;在搭建脚手架的过程中总是有点不适应,尤其对可扩展性上总是感觉没有 Java 那么顺手;索性看了下 coredns 的源码,最后追踪到 caddy 源码;突然发现他们对代码内的 plugin 机制有一些骚套路,这里索性记录一下

一、问题由来

纵观现在所有的 Go web 框架,在文档上可以看到使用方式很简明;非常符合我对 Go 的一贯感受: “所写即所得”;就拿 Gin 这个来说,在 README.md 上可以很轻松的看到 engine 或者说 router 这玩意的使用,比如下面这样:

func main() {// Disable Console Color// gin.DisableConsoleColor()// Creates a gin router with default middleware:// logger and recovery (crash-free) middlewarerouter := gin.Default()router.GET("/someGet", getting)router.POST("/somePost", posting)router.PUT("/somePut", putting)router.DELETE("/someDelete", deleting)router.PATCH("/somePatch", patching)router.HEAD("/someHead", head)router.OPTIONS("/someOptions", options)// By default it serves on :8080 unless a// PORT environment variable was defined.router.Run()// router.Run(":3000") for a hard coded port}

乍一看简单到爆,但实际使用中,在脚手架搭建上,我们需要规划好 包结构、配置文件、命令行参数、数据库连接、cache 等等;直到目前为止,至少我没有找到一种非常规范的后端 MVC 的标准架子结构;这点目前确实不如 Java 的生态;作为最初的脚手架搭建者,站在这个角度,我想我们更应当考虑如何做好适当的抽象、隔离;以防止后面开发者对系统基础功能可能造成的破坏。

综上所述,再配合 Gin 或者说 Go 的代码风格,这就形成了一种强烈的冲突;在 Java 中,由于有注解(Annotation)的存在,事实上你是可以有这种操作的: 新建一个 Class,创建 func,在上面加上合适的注解,最终框架会通过注解扫描的方式以适当的形式进行初始化;而 Go 中并没有 Annotation 这玩意,我们很难实现在 代码运行时扫描自身做出一种策略性调整;从而下面这个需求很难实现: 作为脚手架搭建者,我希望我的基础代码安全的放在一个特定位置,后续开发者开发应当以一种类似可热插拔的形式注入进来,比如 Gin 的 router 路由设置,我不希望每次有修改都会有人动我的 router 核心配置文件。

二、Caddy 的套路

在翻了 coredns 的源码后,我发现他是依赖于 Caddy 这框架运行的,coredns 的代码内的插件机制也是直接调用的 Caddy;所以接着我就翻到了 Caddy 源码,其中的代码如下(完整代码点击这里):

// RegisterPlugin plugs in plugin. All plugins should register// themselves, even if they do not perform an action associated// with a directive. It is important for the process to know// which plugins are available.//// The plugin MUST have a name: lower case and one word.// If this plugin has an action, it must be the name of// the directive that invokes it. A name is always required// and must be unique for the server type.func RegisterPlugin(name string, plugin Plugin) {if name == "" {panic("plugin must have a name")}if _, ok := plugins[plugin.ServerType]; !ok {plugins[plugin.ServerType] = make(map[string]Plugin)}if _, dup := plugins[plugin.ServerType][name]; dup {panic("plugin named " + name + " already registered for server type " + plugin.ServerType)}plugins[plugin.ServerType][name] = plugin}

套路很清奇,为了实现我上面说的那个需求: “后面开发不需要动我核心代码,我还能允许他们动态添加”,Caddy 套路就是定义一个 map,map 里用于存放一种特定形式的 func,并且暴露出一个方法用于向 map 内添加指定 func,然后在合适的时机遍历这个 map,并执行其中的 func。这种套路利用了 Go 函数式编程的特性,将行为先存储在容器中,然后后续再去调用这些行为。

三、总结

长篇大论这么久,实际上我也是在一边折腾 Go 的过程中一边总结和对比跟 Java 的差异;在 Java 中扫描自己注解的套路 Go 中没法实现,但是 Go 利用其函数式编程的优势也可以利用一些延迟加载方式实现对应的功能;总结来说,不同语言有其自己的特性,当有对比的时候,可能更加深刻。

]]>
Golang Golang http://mritd.com/2018/10/23/golang-code-plugin/#disqus_thread
Google container registry 同步 http://mritd.com/2018/09/17/google-container-registry-sync/ http://mritd.com/2018/09/17/google-container-registry-sync/ Mon, 17 Sep 2018 13:19:40 GMT Google container registry 同步 一、起因

玩 Kubenretes 的基本都很清楚,Kubernetes 很多组件的镜像全部托管在 gcr.io 这个域名下(现在换成了 k8s.gcr.io);由于众所周知的原因,这个网站在国内是不可达的;当时由于 Docker Hub 提供了 Auto Build 功能,机智的想到一个解决办法;就是利用 Docker Hub 的 Auto Build,创建只有一行的 Dockerfile,里面就一句 FROM gcr.io/xxxx,然后让 Docker Hub 帮你构建完成后拉取即可

这种套路的基本方案就是利用一个第三方公共仓库,这个仓库可以访问不可达的 gcr.io,然后生成镜像,我们再从这个仓库 pull 即可;为此我创建了一个 Github 仓库(docker-library);时隔这么久以后,我猜想大家都已经有了这种自己的仓库…不过最近发现这个仓库仍然在有人 fork…

为了一劳永逸的解决这个问题,只能撸点代码解决这个问题了

二、仓库使用

为了解决上述问题,我写了一个 gcrsync 工具,并且借助 Travis CI 让其每天自动运行,将所有用得到的 gcr.io 下的镜像同步到了 Docker Hub

目前对于一个 gcr.io 下的镜像,可以直接替换为 gcrxio 用户名,然后从 Docker Hub 直接拉取,以下为一个示例:

# 原始命令docker pull k8s.gcr.io/kubernetes-dashboard-amd64:v1.10.0# 使用同步仓库docker pull gcrxio/kubernetes-dashboard-amd64:v1.10.0

三、同步细节说明

为了保证同步镜像的安全性,同步工具已经开源在 gcrsync 仓库,同步细节如下:

  • 工具每天由 Travis CI 自动进行一次 build,然后进行推送
  • 工具每次推送前首先 clone 元数据仓库 gcr
  • 工具每次推送首先获取 gcr.io 指定 namespace 下的所有镜像(namesapce.travis.yml script 段定义)
  • 获取 gcr.io 镜像后,再读取元数据仓库(gcr) 中与 namesapce 同名文件(实际是个 json)
  • 接着对比双方差异,得出需要同步的镜像
  • 最后通过 API 调用本地的 docker 进行 pulltagpush 操作,完成镜像推送
  • 所有镜像推送成功后,更新元数据仓库内 namespace 对应的 json 文件,最后在生成 CHANGELOG,执行 git push 到远程元数据仓库

综上所述,如果想得知具体 gcrxio 用户下都有那些镜像,可直接访问 gcr 元数据仓库,查看对应 namesapce 同名的 json 文件即可;每天增量同步的信息会追加到 gcr 仓库的 CHANGELOG.md 文件中

四、gcrsync

为方便审查镜像安全性,以下为 gcrsync 工具的代码简介,代码仓库文件如下:

➜  gcrsync git:(master) tree -I vendor.├── CHANGELOG.md├── Gopkg.lock├── Gopkg.toml├── LICENSE├── README.md├── cmd│   ├── compare.go│   ├── monitor.go│   ├── root.go│   ├── sync.go│   └── test.go├── dist│   ├── gcrsync_darwin_amd64│   ├── gcrsync_linux_386│   └── gcrsync_linux_amd64├── main.go└── pkg    ├── gcrsync    │   ├── docker.go    │   ├── gcr.go    │   ├── git.go    │   ├── registry.go    │   └── sync.go    └── utils        └── common.go

cmd 目录下为标准的 cobra 框架生成的子命令文件,其中每个命令包含了对应的 flag 设置,如 namesapceproxy 等;pkg/gcrsync 目录下的文件为核心代码:

  • docker.go 包含了对本地 docker daemon API 调用,包括 pulltagpush 操作
  • gcr.go 包含了对 gcr.io 指定 namespace 下镜像列表获取操作
  • registry.go 包含了对 Docker Hub 下指定用户(默认 gcrxio)的镜像列表获取操作(其主要用于首次执行 compare 命令生成 json 文件)
  • sync.go 为主要的程序入口,其中包含了对其他文件内方法的调用,设置并发池等

五、其他说明

该仓库不保证镜像实时同步,默认每天同步一次(由 Travis CI 执行),如有特殊需求,如增加 namesapce 等请开启 issue;最后,请不要再 fork docker-library 这个仓库了

]]>
Kubernetes Docker Kubernetes http://mritd.com/2018/09/17/google-container-registry-sync/#disqus_thread
使用 Bootstrap Token 完成 TLS Bootstrapping http://mritd.com/2018/08/28/kubernetes-tls-bootstrapping-with-bootstrap-token/ http://mritd.com/2018/08/28/kubernetes-tls-bootstrapping-with-bootstrap-token/ Tue, 28 Aug 2018 08:54:43 GMT 最近在测试 Kubernetes 1.11.2 新版本的相关东西,发现新版本的 Bootstrap Token 功能已经进入 Beta 阶段,索性便尝试了一下;虽说目前是为 kubeadm 设计的,不过手动挡用起来也不错,这里记录一下使用方式

最近在测试 Kubernetes 1.11.2 新版本的相关东西,发现新版本的 Bootstrap Token 功能已经进入 Beta 阶段,索性便尝试了一下;虽说目前是为 kubeadm 设计的,不过手动挡用起来也不错,这里记录一下使用方式

一、环境准备

首先需要有一个运行状态正常的 Master 节点,目前我测试的是版本是 1.11.2,低版本我没测试;其次本文默认 Node 节点 Docker、kubelet 二进制文件、systemd service 配置等都已经处理好,更具体的环境如下:

Master 节点 IP 为 192.168.1.61,Node 节点 IP 为 192.168.1.64

docker1.node ➜  ~ kubectl versionClient Version: version.Info{Major:"1", Minor:"11", GitVersion:"v1.11.2", GitCommit:"bb9ffb1654d4a729bb4cec18ff088eacc153c239", GitTreeState:"clean", BuildDate:"2018-08-07T23:08:19Z", GoVersion:"go1.10.3", Compiler:"gc", Platform:"linux/amd64"}Server Version: version.Info{Major:"1", Minor:"11", GitVersion:"v1.11.2", GitCommit:"bb9ffb1654d4a729bb4cec18ff088eacc153c239", GitTreeState:"clean", BuildDate:"2018-08-07T23:08:19Z", GoVersion:"go1.10.3", Compiler:"gc", Platform:"linux/amd64"}docker1.node ➜  ~ docker infoContainers: 0 Running: 0 Paused: 0 Stopped: 0Images: 0Server Version: 18.06.1-ceStorage Driver: overlay2 Backing Filesystem: xfs Supports d_type: true Native Overlay Diff: trueLogging Driver: json-fileCgroup Driver: cgroupfsPlugins: Volume: local Network: bridge host macvlan null overlay Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslogSwarm: inactiveRuntimes: runcDefault Runtime: runcInit Binary: docker-initcontainerd version: 468a545b9edcd5932818eb9de8e72413e616e86erunc version: 69663f0bd4b60df09991c08812a60108003fa340init version: fec3683Security Options: apparmor seccomp  Profile: defaultKernel Version: 4.15.0-33-genericOperating System: Ubuntu 18.04.1 LTSOSType: linuxArchitecture: x86_64CPUs: 2Total Memory: 3.847GiBName: docker1.nodeID: AJOD:RBJZ:YP3G:HCGV:KT4R:D4AF:SBDN:5B76:JM4M:OCJA:YJMJ:OCYQDocker Root Dir: /data/dockerDebug Mode (client): falseDebug Mode (server): falseRegistry: http://index.docker.io/v1/Labels:Experimental: falseInsecure Registries: 127.0.0.0/8Live Restore Enabled: false

二、TLS Bootstrapping 回顾

在正式进行 TLS Bootstrapping 操作之前,**如果对 TLS Bootstrapping 完全没接触过的请先阅读 Kubernetes TLS bootstrapping 那点事**;我想这里有必要简单说明下使用 Token 时整个启动引导过程:

  • 在集群内创建特定的 Bootstrap Token Secret,该 Secret 将替代以前的 token.csv 内置用户声明文件
  • 在集群内创建首次 TLS Bootstrap 申请证书的 ClusterRole、后续 renew Kubelet client/server 的 ClusterRole,以及其相关对应的 ClusterRoleBinding;并绑定到对应的组或用户
  • 调整 Controller Manager 配置,以使其能自动签署相关证书和自动清理过期的 TLS Bootstrapping Token
  • 生成特定的包含 TLS Bootstrapping Token 的 bootstrap.kubeconfig 以供 kubelet 启动时使用
  • 调整 Kubelet 配置,使其首次启动加载 bootstrap.kubeconfig 并使用其中的 TLS Bootstrapping Token 完成首次证书申请
  • 证书被 Controller Manager 签署,成功下发,Kubelet 自动重载完成引导流程
  • 后续 Kubelet 自动 renew 相关证书
  • 可选的: 集群搭建成功后立即清除 Bootstrap Token Secret,或等待 Controller Manager 待其过期后删除,以防止被恶意利用

三、使用 Bootstrap Token

第二部分算作大纲了,这部分将会按照第二部分的总体流程来走,同时会对一些细节进行详细说明

3.1、创建 Bootstrap Token

既然整个功能都时刻强调这个 Token,那么第一步肯定是生成一个 token,生成方式如下:

➜  ~ echo "$(head -c 6 /dev/urandom | md5sum | head -c 6)"."$(head -c 16 /dev/urandom | md5sum | head -c 16)"47f392.d22d04e89a65eb22

这个 47f392.d22d04e89a65eb22 就是生成的 Bootstrap Token,保存好 token,因为后续要用;关于这个 token 解释如下:

Token 必须满足 [a-z0-9]{6}\.[a-z0-9]{16} 格式;以 . 分割,前面的部分被称作 Token IDToken ID 并不是 “机密信息”,它可以暴露出去;相对的后面的部分称为 Token Secret,它应该是保密的

本部分官方文档地址 Token Format

3.2、创建 Bootstrap Token Secret

对于 Kubernetes 来说 Bootstrap Token Secret 也仅仅是一个特殊的 Secret 而已;对于这个特殊的 Secret 样例 yaml 配置如下:

apiVersion: v1kind: Secretmetadata:  # Name MUST be of form "bootstrap-token-<token id>"  name: bootstrap-token-07401b  namespace: kube-system# Type MUST be 'bootstrap.kubernetes.io/token'type: bootstrap.kubernetes.io/tokenstringData:  # Human readable description. Optional.  description: "The default bootstrap token generated by 'kubeadm init'."  # Token ID and secret. Required.  token-id: 47f392  token-secret: d22d04e89a65eb22  # Expiration. Optional.  expiration: 2018-09-10T00:00:11Z  # Allowed usages.  usage-bootstrap-authentication: "true"  usage-bootstrap-signing: "true"  # Extra groups to authenticate the token as. Must start with "system:bootstrappers:"  auth-extra-groups: system:bootstrappers:worker,system:bootstrappers:ingress

需要注意几点:

  • 作为 Bootstrap Token Secret 的 type 必须为 bootstrap.kubernetes.io/token,name 必须为 bootstrap-token-<token id> (Token ID 就是上一步创建的 Token 前一部分)
  • usage-bootstrap-authenticationusage-bootstrap-signing 必须存才且设置为 true (我个人感觉 usage-bootstrap-signing 可以没有,具体见文章最后部分)
  • expiration 字段是可选的,如果设置则 Secret 到期后将由 Controller Manager 中的 tokencleaner 自动清理
  • auth-extra-groups 也是可选的,令牌的扩展认证组,组必须以 system:bootstrappers: 开头

最后使用 kubectl create -f bootstrap.secret.yaml</