百种弊病,皆从懒生

gospy dev note2 (rewrite with aider)

几年前写过个工具 gospy, 用于从旁路 dump 一个 golang 进程的 runtime 信息(包括 goroutine, memory 等), 大致原理见以前的文章.

基本功能能用, 但没继续做下去, 除了没时间外, 其他还有几个问题:

  • 不支持 MacOS (主要是没搞懂 MacOS 下怎么读取进程内存).
  • DWARF 解析写的过于繁琐, golang 版本更新时, 解析逻辑很难调整.
  • 对写 UI (包括 terminal UI 和前端) 实在没兴趣, 不写又没法暴露功能, 也懒得去做通过 http 接口暴露数据.

前阵子试了下通过 aider 来写代码, 效果非常惊艳, 对我来说, 比 curosr 还顺手. 于是花了两个周末的时间, 把 gospy 整个重写了.

......

整理几个碰到的 etcd bug

产品里用了一年多的 etcd, 碰到过一些 bug, 整理下,其中用些在最新版本里已经修复了, 会标注下.

添加 etcd 节点相关 bug

添加 etcd 节点的过程一般是先 member add, 然后启动新节点上的 etcd,这样的问题是在 member add 和新 etcd 启动 之间整个 etcd 集群处于 quorum - 1 的状态, 此过程增加了集群的不稳定性,如果新节点由于配置错误起不来,现存节点再挂一个 就可能导致整个集群不可用.

从 3.4 开始 etcd 引入了 learner 的概念, member add –learner, 可以将新节点添加成 leaner 角色,只同步 raft log, 不参与 quorum vote, 即现有集群的 quorum size 不会变化. 等新节点起来后再通过 member promote 将新节点提升为正常节点,参与 quorum vote.

......

用 Patroni 来做 PostgreSQL 的 HA

patroni 的安装跳过, 它只是个 python 包, 把依赖装好就行, 同时要求装好 postgres-server, patroni 运行过程中会调用 pg_ctl 等命令: https://patroni.readthedocs.io/en/latest/README.html 每个 patroni 管理一个 pg 实例, 两者必须部署在同一节点上, patroni 需要能:

  • 访问 pg 的监听端口
  • 读写 pg data dir (patroni 会重写 postgres.conf, pg_hba.conf 等文件)

配置文件

配置文件是yaml 格式, 具体见 https://patroni.readthedocs.io/en/latest/SETTINGS.html
使用 patroni postgres.yaml 命令启动一个 patroni 实例

# 集群名字, 同一集群内的 patroni 必须设置一样, 会成为 postgres.conf 内的 cluster_name参数
scope: xsky-test
# patroni 会将一些动态配置存在 DCS 内, namespace 是在 DCS 内的存储路径, eg: 使用 etcdv3 作为 DCS, 可以通过 etdctl get /service --prefix 看到 patroni 在 DCS 内存了什么
namespace: /service  
# 管理的 pg 的实例名, 同一集群内每个实例必须都不同, eg: pg0, pg1, pg2
name: pg0
#  日志相关配置
log:  
  level: INFO 
  ...
# patroni restapi 相关配置
restapi:
  listen: 0.0.0.0:8008
  ...

# 用创建 pg 实例, 生成 pg 的 data dir, postgres.conf, pg_hba.conf 等文件
bootstrap:
  # dcs 下内容会写入 DCS 的 {namespace}/{scope}/config key 中,只有在初始化集群的时候被使用一次, 后续修改不会生效, 可以通过 patrionctl edit-config 或 rest api 来修改存在 dcs 中的值
  dcs:
    loop_wait: 10
    ttl: 30
    ...
  # 初始化集群时生成 pg_bha.conf, 后续改动不会生效
  pg_hba:
   - host replication replicator 127.0.0.1/32 md5
  # 在初始化集群时需要创建的用户, 后续改动不会生效
  users:
  ...
# 使用 etcd v2 api 作为 dcs
etcd:
  ...
# 使用 etcd v3 api 作为 dcs
etcd3:
  ...
# 用于 patroni 运行时连接 pg
postgresql:
  listen: 0.0.0.0:5432
  ...
# patronictl.py 连接 restapi 时 http参数(ssl相关)
ctl:
  ...
# 通过 linux 的 softdog 模块, 在 patroni 控制的 pg 挂掉时重启 server
watchdog:
  ...
# 通过nofailover 等 tag 可以控制行为(eg: 不允许某个 patrion 实例成为 leader, 也可以添加自定义tag作为元数据
tags:
   ...

用 patroni bootstrap pg 集群

假设有三节点, 上面已经部署好 etcd(v3) 作为 dcs, 三节点 ip为: 10.252.90.217, 10.252.90.218, 10.252.90.219 217 节点上 patroni 的 postgres.yml 配置, 其他节点类似, 把 ip 改下:

......

Rolling Upgrade Worker Nodes in EKS

EKS control plane 的升级是比较简单的, 直接在 aws console 上点下就可以了, 但 worker node 是自己用 asg(autoscaling group) 管理的, 升级 worker node 又不想影响业务是有讲究的.

跑在 EKS 里, 且希望不被中断 traffic 的有:

  • stateless 的 api server, queue consumer
  • 被 redis sentinel 监控着的 redis master/slave
  • 用于 cache 的 redis cluster

写了个内部工具, 把下面的流程全部自动化了. 这样升级 eks 版本, 需要更换 worker node 时候就轻松多了. 因为这个工具对部署情况做了很多假设和限制, 开源的价值不是很大.

Stateless application

stateless 的应用全部用 deployment 部署.

一般建议的流程是:

  • 修改 asg 的 launch configuration, 指向新版本的 ami
  • 把所有老的 worker node 用 kubectl cordon 标记成 unschedulable
  • 关闭 cluster-autoscaler
  • 修改 asg 的 desired count, 让 asg 用新 ami 启动新的 worker node
  • 用 kubectl drain 把老 worker node 上的 pod evict 掉, 让它们 schedule 到新的 worker node 上.
  • 重新开启 cluster autoscaler, 等它把老的闲置 worker node 关闭.

这里有些问题:

......

从k8s deprecating docker 说起

k8s 1.20 的 release note 里说 deprecated docker: https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.20.md#deprecation

对 docker 和 k8s 关系比较了解的人一看就知道是废弃 dockershim, 正常操作, 具体有什么影响, 建议阅读:

但容器圈里那堆名词的确很让人困惑, docker, dockershim, containerd, containerd-shim, runc, cri, oci, csi, cni, cri-o, 或许曾经还听说过 runv, rkt, clear containers, 后来又来了 kata containers, firecracker, gvisor. 论造名词, 造项目, 都快赶上前端圈的脚趾头了.

这篇比较水, 就解释下它们大致的关系, 前提是你大概知道 docker, namespace, cgroup, container, k8s pod 之间的关系, 不做额外解释.

从 docker 说起

在我现在这台机器上的 ubuntu 18.04, 安装 docker, 添加官方源之后, 安装的 deb package(version 19.03) 是 docker-ce

apt show docker-ce 看依赖:

......

二三事

天气渐凉, 无奈得拿出了秋裤. 偷懒好久没写博客了, 回顾下这阵子做了什么.

工作

年底打算把之前用的 dedicated ec2 instance 全部换掉, 几年前为了 HIPAA 合规做的, 但 AWS 的 BBA 里后来不要求 dedicated instance 了, 换成普通的, 可以省掉每月1400多刀的固定 dedicated fee, 同类型的 ec2 instance 可以再省10%左右. 为了这个, 打算把部分遗留在 vm 上的东西迁移到 k8s 里, 减少之后更换 instance 的工作量.

cronjob

尝试用 argo 来调度 cronjob.

还是有不少坑的:

  • cron workflow 到点后有一定几率会 trigger 重复的workflow, 目前还没有修复#4558, 临时在所有 cronjob 的代码里加了个保护, 在 redis 里上个锁, 没抢到锁的 cronjob 直接抛异常失败. 目前看每天还是有 7,8 次重复的 workflow 会触发.
  • WorkflowTemplate 可以用来复用 pod 的定义, 但外部往里面传参的方式很搓, 不是所有字段都能直接覆盖, 比如activeDeadlineSeconds, resources 等, 必须用 podSpecPatch 的方式, 文档里又不说清楚, 只能自己去翻issue.
  • exit-handler 用来在 workflow 状态改变的时候触发一个 callback, 但没法获取触发它的那个 workflow 的详细信息, 比如一个 workflow 由很多个 step 构成, 失败时我希望能从 exit-handler 里拿到是哪个step 挂了, 现在做不到. 也没法往 exit-handler 里传参(只能取一些global paramaters).
  • 如果一个 workflow 由多个 step 构成, 当其中一个 step 挂了, 可以设置 continueOn 控制是否继续后面的step, 但如果我选择了继续, 整个 workflow 的最终状态又是 success 的, 导致在 exit-handler 里没法区分, 最好有个 partial success 的状态.

日志

应用里的日志, 之前是打到部署在 vm 上的一台 rsyslog 上. 在 k8s 环境下我用 fluent-bit 做 daemonset 来收集 pod 日志. fluent-bit 在最近的版本里支持了转发到 loki, 我就部署了 loki 来收集pod 日志, 可以方便在 grafana 里进行查看. 问题也很多:

......

Linux 进程虚拟内存分布

巩固下基础知识.

现代系统都运行在保护模式(protected mode)下, x86_64 下用户态程序启动时候都会被分配一片虚拟内存, 大小是 2^47 (128TiB), 但目前 cpu 只能映射 2^46 (64TiB), 通过 page table 将虚拟内存中的地址(vma)和物理地址映射起来. 以前的 BIOS 跑在 实模式下(real mode), 直接访问物理内存, 不过只能访问前 2^20(1MiB) 的空间.

Linux 下 /proc/$pid/maps 文件内容就是进程的虚拟内存分布. 每一列含义通过 man 5 proc 看, 不重复了.

网上的例子都有点过时, 在比较新的 linux 发行版里, 默认开启了ASLR(地址空间随机化), 自带的二进制文件都是 PIE(Position Independent Executable) build 的. 这两结合起来让进程在虚拟内存中的基地址随机化, heap 和 stack 的开始位置也随机化, 目的是为了防止远程注入漏洞猜到一些软件的vma.

......

Kubectl Plugin for Redis Cluster

在 k8s 上部署 redis cluster 后, 感觉 redis-cli 管理 redis cluster 非常别扭, 写了个 kubectl 的插件 kubectl-rc 来辅助管理 redis-cluster.

redis-cli 难用在哪

  1. 不直观 & 不统一

部分 cluster 信息是直接通过 redis protocol 获得的, 比如 cluster nodes, cluster slots, 但部分管理命令又是通过 redis-cli --cluster 执行的.

cluster nodes, cluster slots 这些命令输出的又是 ip 和 node id, k8s 环境下我更关心实际的 pod name.

做 failover 的时候又不是通过 --cluster 执行的, 必须连到 slave 上通过 cluster failover 来执行

  1. 传参在 k8s 环境下特别麻烦

举个例子, 添加节点:

redis-cli --cluster add-node new_host:new_port existing_host:existing_port
    --cluster-slave --cluster-master-id <arg>

需要知道操作 pod 的 ip, 如果要变成某个指定 pod 的 slave, 又要传 node id.

在 k8s 环境下实际操作的时候流程就会变成:

......

从 twemproxy 迁移到 redis cluster

线上有个 redis 的缓存集群, 跑的还是 3.0, 前面套 twemproxy 做 sharding. 跑了好几年了都很稳定, 但一直有些很不爽的地方, 最近有点时间,决定 升级到redis 6, 并迁移到 redis cluster 方案.

twemproxy 的工作模式

twemproxy 的原理很简单, 后面运行 N 个 redis 实例, 应用连到 twemproxy, twemproxy 解析应用发过来的 redis protocol, 根据 key 做 hash, 打散到后面 N 个 redis 实例上.

具体打散的方式可以是简单的 hash%N, 也可以用一致性 hash 算法. hash%N 的问题是, 增减节点的时候所有 cache 必然 miss.

一致性 hash, 在实现的时候会先弄一个 size 很大的 hash ring(eg: 2^32),这里每个节点被叫做一个虚拟节点, 把虚拟节点的数目叫做 X, 然后将 N 个 redis 实例均匀分布到这个环上, 每个 redis 把它叫做实节点吧, 分配 key 的时候 hash%X, 得到虚拟节点, 然后顺时针找下一个最近的实节点, 就找到了相应的 redis. 因为虚拟节点的数目是不变的, 增减 redis 实例的数目是改变了实节点的分布, 顺时针找下个实节点的时候还是有一定几率落在以前的 redis 实例上的, 这在一定程度上减少了 cache 的 miss. 可以看作 hash%N 的优化版本,但不解决本质问题.

......

snet dev note: stats api and terminal UI

0.10.0 版本开始给 snet 加了 stats api 来暴露内部的一些统计数据.

设置 "enable-stats": true 开启, 默认监听 8810 端口, curl http://localhost:8810/stats

    {
        "Uptime": "26m42s",
        "Total": {
            "RxSize": 161539743,
            "TxSize": 1960171
        },
        "Hosts": [
            {
                "Host": "112.113.115.113",
                "Port": 443,
                "RxRate": 0,
                "TxRate": 0,
                "RxSize": 840413,
                "TxSize": 172528
            },
            ...
        ]
    }

分 host, 统计从该地址接收的字节数(RxSize), 发送字节数(TxSize), 和相应的 rate/s (RxRate, TxRate).

默认只记录 ip, 可以设置 "stats-enable-tls-sni-sniffer": true, 开启对去往 443 端口的流量进行 sni sniffer, 尝试解析出域名. "stats-enable-http-host-sniffer": true, 对发往 80 端口流量尝试解析 http host 字段, 两者都会给连接建立过程增加一些 overhead.

server 开启 stats api 后, 用 ./snet -top 可以显示一个类似 top 的 terminal UI 作流量监控.

......