TGINSIGHT CHAT
Welcome to the Black Parade
@TheB1ackParade
MusicDeath has many faces, I look forward to seeing this one.
Recent posts
Page 73 of 76 · 907 posts
Posted Mar 31
同事说, curl -v http://10.22.12.87:2376/version 返回 400, 问我怎么用 curl 访问 dockerd. 我的思路是先想办法通过 sdk 访问 version api, 然后 tcpdump 观察一下 url 就可以了, 所以: 1. pip install docker, python repl 里 docker.from_env().version() 2. tcpdump -i any port 2376 却什么都看不到, 怀疑 from_env() 走的 unix socket 3. lsof -p + ss -xp 果然是在的 unix socket, 那我抓个 unix socket 的包吧 4. socat TCP-LISTEN:2375,fork UNIX-CONNECT:/var/run/docker.sock, 然后 python repl 里 docker.DockerClient(base_url='tcp://localhost:2375').version() 5. 这次可以抓到包了, 一个简单的 GET HTTP, curl 却发现报错说没有证书 6. 没证书会返回 400? 我自己手动试了一次 curl -v http://10.22.12.87:2376/version, 发现并没有 400, 而是直接说 Client sent an HTTP request to an HTTPS server. 7. curl --key /etc/docker/tls/server.key --cert /etc/docker/tls/server.crt --cacert /etc/docker/tls/ca.crt https://10.22.12.87:2376/version 搞定 气坏我了, 已经不是一次两次被同事的错误结论误导了, 明明报错很清楚地说了是没有证书, 给我报错就成 400 了, 我也傻逼到相信了同事的诊断. 光是最近两个月这类的事情都有好几次: 1. 明明不是 host 网络, 信誓旦旦给我说是 host, 结果他妈根本不是, 浪费了我好多时间 2. 自做聪明报告自己的诊断结果"在xxx时会没有xxx", 结果果然是错的, 配都配错了 再往前就更多了, 自己的服务挂了都搞不清楚的, 自己的 gateway 有没有转发都不清楚最后还是让我查出来的, 简直就是灾难. 我所能学到的唯一教训, 就是不相信同事, 他们说得任何诊断结果, 我都要亲自确认, 并且询问他们的 "X问题" 而不是 "Y问题". from https://gist.github.com/jschwinger23/c280bf036329daa0204b8d2357b5dcd7#file-misleading-decoy-md
Posted Mar 26
# 如何查 unix socket 的 peer 比放说我想确认一个进程是否连接了 unix:///var/run/docker.sock, 对于 TCP 来说 lsof -p$PID | grep $PORT 立刻就能查到, 对于 Unix Socket 来说就不行了. 做法应该是这样的: 1. lsof 找到这个进程所有的 Unix + SOCK_STREAM 的 sockets, 倒数第二列是 Node Number 2. ss 找到这个 node number 的 peer 3. 确认 peer 是 docker sock 写成一句话就是: for no in $(lsof -p36370 | awk '/unix/ && /STREAM/ {print $8}'); do ss -xp | grep $no; done | grep /var/run/docker.sock from https://gist.github.com/jschwinger23/24883ae4e432d6ff3a100bf5534a0dc7#file-inspect-unix-socket-peer-md
Posted Mar 25
虽然最近在度假模式沉迷 XCOM2 太好玩了目测没有 100h 我是打不穿一周目了, 但是还是我还是有技术追求的, 记录一下要做的度假结束后要做的事情: 1. 重学网络, 着重加强 hands-on experience 和 cornor case, 前者比如抓包 GRPC 和统计 TCP 来分析性能, 后者先把 CLOSE-WAIT 收到 SYN 搞清楚再说. 2. 重学 Docker - Containerd, 先通读文档, 尝试所有 API, 读源码, 重点是网络, --iteractive, 每一级的责任划分和 debug 3. debug 工具们, 不点名了. 4. 继续背书, 打算放弃一段时间通勤背英语试试效果.
Posted Mar 19
协助排查了一次容器内进程的性能问题 症状: 平台上容器内的 redis cluster 进程用 redis-benchmark 测试能稳定复现 spike, 但是同宿主机上的 redis 没有问题 1. 首先怀疑 SDN, 因为没问题的 redis 是 host 网络. 理论上这很难查, 但是在部署了一个 SDN 网络的 redis 后发现依然没有问题, 我比较有把握不是网络问题, 不过也不是特别确定, 因为 redis cluster 的节点跨主机, 说不清楚会发生什么. 2. 建立最小可复现问题的现场. 我们应该从几乎一模一样的环境(相同的容器里)重建一个拥有相同节点, 相同配置, 相同数据, 只是监听不同端口的集群, 理论上这个集群应该是能复现问题的. 3. 然后一点点缩小问题的范围, 看到那一步就突然不能复现 1. 摘掉所有的 slave nodes 2. 把进程放到宿主机裸跑 3. 把集群放到同一个宿主机 4. 用 host 网络 4. 结果发现把进程放到宿主机裸跑就没问题, 接下来继续缩小范围: 1. 放到同一个 network ns 下运行 2. 放到同一个 mount ns 下 3. 放到同一个 cgroup 下 4. 其他 ns 5. 令人吃惊的是裸跑进程放到和有问题的进程同一 net ns 下居然没有复现问题, 彻底排除了 SDN 的问题, 继续查下去发现居然是 cgroup 的限制导致了性能问题. 6. 一边 strace -fTp$PID -etrace=read,write 一边 benchmark 一边 tshark, 发现 spike 的 exchange 在时间上吻合一个长达 12ms 的 write 调用, 去看 cgroup 里的 cpu.throttled_*, 发现 throttled_time 和 spike 吻合地非常好, 就是 12ms. 7. 最后创建容器, 设置 CPUQuota=0, 不再复现, 实锤是 cgroup cpu.cfs_quota_us 导致被 throttle 使得非阻塞的 write 调用耗时很长. 8. 内核下的问题我就不会查了, 溜了. 当然我只是辅助, 体力活都是波神做的, 第一次见识 eBPF/bcc 这种雷神之锤, 臣服, 想学. from https://gist.github.com/jschwinger23/dfe812bc701308433f68abf6a51f68f9#file-container-performance-investigation-md
Posted Mar 19
https://jschwinger23.github.io/2020/03/15/My-Bash-Report-2020.html
Posted Mar 8
## Q1: 多次处理 stdout 比如我要先判断 stdout 里是否匹配某模式, 匹配则对 stdout 做进一步处理. 无脑一点, 我们可以把 stdout 写到临时文件里: tmpfile=$(mktemp) cmd | tee $tmpfile | inspect $pattern && process $tmpfile rm -f $tmpfile 本身也足够简单, 可读性高, 也好扩展, 也可以反复读, 适用于各种场景. 如果不满意有文件落地, 那可不太好办了, 非常不好办. 我们有三种可能的手段: 1. inspect 和 process 在同一进程, 形如 inspect $pat && process, 好处是我们可以通过变量来控制分支, 可以完成顺序式编程. 2. inspect 和 process 是父子进程关系, 形如 process > >(inspect $pat), process substitution 保证了父子进程关系, 此时我们可以通过 wait(1) 来完成跨进程同步. 3. inspect 和 process 是非父子关系的两个进程, 形如 inspect $pat | process, 我们只能让 inspect 过滤. 下面来演示一下, 例题简单一点, 检查 etcdctl 输出里是否有 ModRevision, 有的话把全体输出到终端. 思路一的做法: etcdctl get key -w fields | (while read line; do [[ "$line" =~ ModRevision ]] && found=1; content="$line"$'\n'"$content"; done && [ -n "$found" ] && echo "$content") 可以看到我们用 $found 来 toggle process. 思路二: etcdctl get /a -w fields | (content="$(cat -)"; echo "$content" > >(grep -q ModRevision); wait $!; [[ "$?" == 0 ]] && echo "$content" ) 里面使用了 $! 这一 bash4.4+ 才有的变量来获取 process substitution 的 pid, 然后 wait 直到 inspect 完成. 思路三和思路一的大部分代码相同, etcdctl get key -w fields | (while read line; do [[ "$line" =~ ModRevision ]] && found=1; content="$line"$'\n'"$content"; done && [ -n "$found" ] && echo "$content") | cat 要注意最后那个 cat 才是 process 进程, 中间一大串都是 inspect, 虽然代码和思路一相似, 但是这只是因为题目的 process 是打印到终端, 如果是写到文件那立刻就不同了. 如果是特殊一点的场景, 比如是匹配模式后抠字符, 那么多半会有合并的做法, 而实际上大部分场景我们都是面对格式很固定的数据. 比如如果模式和要抠的字符在同行, awk 的 filter + print columns 的这一套就非常合适. cmd | awk '/PAT/ {print $NF}' 如果是跨行, 但是模式我们很清楚, 比如要抠的字符在模式附近几行, gnu/grep + regex 就能很容易上手: cmd | grep $pat -A4 | grep -Po '\w{64}' 如果模式复杂, 因为 gnu/grep 的正则不能抠 capture group, 加上 pcre 的向前环视只能是固定字符, 在某些场景下可能 read 更简单: cmd | while read line; do read key _ value <<<$line; [[ "$key" == PAT ]] && echo $value; done ## Q2: stderr 打断进程 在一串 pipeline 中要对部分命令的 stderr 进行模式匹配, 匹配成功就退出进程. TTY=$(tty) cmd1 | (cmd2 2> >(grep $pat > $TTY) || kill -9 $$) | cmd3 这里的难点是, 1. 我不想把 cmd2 的 stderr dup 到 stdout 然后交给 cmd3 判断 2. 我依然想打印出 stderr 到 tty, 而在管道子进程中已经拿不到 tty 了, 只能通过环境变量 3. exit(1) 是没用的, 只有 kill(1) 能跨进程中断 不过上面的代码其实并不符合要求, 要求对 stderr 匹配模式才退出, 代码是 exit code 非零就退出. 如果要做到题目要求, 那么 $! 捕获 process subsititution 的 pid 又派上用场了: TTY=$(tty) cmd1 | (cmd2 2> >(grep $pat > $TTY); wait $!; [[ $? != 0 ]] && kill -9 $$) | cmd3 ## Q3: 怎么做好 cleanup 比如在 Q1 里的无脑解法创建了一个临时文件, 怎么尽可能保证临时文件能在进程退出后被清理掉. 我们可以用一个装饰器脚本, 名字叫 ONTEMP: #!/bin/bash echo "Creating tempfile" tempfile=$(mktemp) echo "Executing command: $@" eval $@ echo "Removing tempfile" rm -f $tempfile 不过这还不够, 我们还能处理好信号等情况: function cleanup() { echo "Removing tempfile" rm -f $tempfile } trap cleanup EXIT 利用好 trap(1) 可以很好地应对各种情况, 不过对于 kill -9 就不行了... 那也没办法了. 最终的代码: #!/bin/bash echo "Creating tempfile" tempfile=$(mktemp) function cleanup() { echo "Removing tempfile" rm -f $tempfile } trap cleanup EXIT echo "Executing command: $@" eval $@ 使用: ONTEMP sleep 20 Ctrl-C 后能检查临时文件被删除. from https://gist.github.com/jschwinger23/a2450771c1592aae035e6d912d1bf7f5#file-bash-practices-2020-03-md
Posted Mar 3
工程上的问题就是复杂多变, 今天我死活都运行不了 calicoctl, 硬着头皮还是找到了问题, 重新总结 calico sdn 的思路. 症状: host1 不能 ping host2 上的容器 c1 0. 直接检查 host1 的 ip r 1. 有通往 c1 的路由说明 felix + bgp 都正常; 2. 如果宿主机的网络配置复杂难以肉眼判断, 那么可以通过 birdcl show route for [c1 ip] 看结果; 3. 留意 host1 和 host2 之间的网络环境, 如果宿主鸡 L2 不可达那么(在无 switch 支持的情况下) route 应该是要通过 ipip 隧道的; 4. 如果没有路由, 那么是 bgp / felix / wep 的锅. 5. 如果有路由, 直接怀疑 iptables, 直接在 host1 / host2 / c1 namespace 三处同时抓包, 最明显的临床症状是 host1 + host2 能抓包但是 c1 network namespace 里抓不到, 那就实锤. 1. 检查 bird: show route protocol kernel 1. 如果这里的结果是正确的(和 ip r 的结果一致), 说明 felix 的功能正常, wep 也不用检查了; 2. 如果没有 wep 那么是网络插件的锅, cni / cnm 看日志. 2. 确认 bgppeer 设置, mesh to mesh 还是 rr, 如果 calicoctl 不可用, 可以直接看 calico-node 的 /etc/calico/confd/config/. 3. 然后就抓包吧, tshark -iany -f 'tcp port 179' -Y bgp, 重启 host2 相关的 bgp protocol, 看 bgp 包有没有. 1. 没有的话尝试在 host1 手动连接 host2 179, 三次握手失败首先怀疑防火墙. 2. 如果 bgp 包正常, 那么怀疑 bgppeer 硬件设置. 3. 还有 calico profile, 本质也是 iptables 今天的难点还是在始终无法正常使用 calicoctl, etcd 又是有 auth + tls; 另外我始终无法理解 calico cni 的实现使用 kubernetes datastore 是什么鬼设计, 第一次看到插件反向适配宿主的, kubernetes 不愧是开源独裁, 不的不服. from https://gist.github.com/jschwinger23/accda0d4a901a941d4194a1d065a957a#file-calico-diagnose-2nd-edition-md
Posted Mar 2
做俯卧撑的时间突然明白了, 这一切的设计都是为了能够复制目录. 不想 cp(1) 那样需要 cp -r 来区分复制目录和文件, docker cp 由于 tar 打包的设计用一套接口就可以处理好文件和目录. 不, 我还是不接受, 怎么可以连 process sub 都处理不好呢, 我个人认为一切接受文件名的地方, 必须能够接受 process substitution, 唉. from https://gist.github.com/jschwinger23/d8486dac6cef0d4b88be40f5409515eb#file-oops-md
Posted Mar 2
发现 docker cp 的实现跟屎一样. 一开始发现的问题是 docker-ce/components/client 里的 CopyToContainer 这个 interface, 签名是这样的: CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error 我注意到的问题是里面 path 必须是 dirname, 比如说你想 docker cp ./a.txt $CONTAINER_ID:/etc/b.txt, 那么 path 必须是 /etc, 否则直接报错. 那么问题来了, destination filename 都没传过去, dockerd 是怎么知道文件名的? 就比如上面的复制到 /etc/b.txt, 我都没把 b.txt 告诉 dockerd 它怎么可能做到的? 懒, 不想看代码, 先抓包, docker -H tcp://10.143.201.210:2376 cp /tmp/a.txt 4ea389f2119e:/etc/b.txt, 发现是这样的: PUT /v1.39/containers/4ea389f2119e/archive?noOverwriteDirNonDir=true&path=%2Fetc HTTP/1.1 Host: 10.143.201.210:2376 User-Agent: Docker-Client/19.03.5 (darwin) Transfer-Encoding: chunked Content-Type: text/plain PaxHeaders.0/b.txt..................................................................................0000000.0000000.0000000.00000000021.00000000000.011174. x....................................................................................................ustar.00....................................................................................................................................................................................................................................................... 那么一看到 PaxHeaders 就了然了, body 是一个 tarball, 所以 dest filename 也蕴藏其中. 那么问题来了, CopyToContainer interface 里的类型签名只说是 container io.Reader, 我直接 bytes.NewBuffer() 生成一个 reader 它岂不是要仆街? 看了一下源码, 结果在接口实现里注释写着: // CopyToContainer copies content into the container filesystem. // Note that `content` must be a Reader for a TAR archive func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options types.CopyToContainerOptions) error { 我服了... 垃圾 API, 居然会在客户端创建一个临时文件.tar 再 open, 匪夷所思, 给我三个大脑我都想不出这种实现, 内存里不能压缩 bytes buffer 吗? 更垃圾在后面. 我发现 docker cp 连 process substitution 都不认, 具体的命令是这样的: docker cp /tmp/a.txt 4ea389f2119e:/etc/b.txt // ok docker cp <(cat /tmp/a.txt) 4ea389f2119e:/etc/b.txt // not ok 后者的命令不会出错, 然而如果你去看 cp 进去是啥文件的时候就: # docker exec -it f21996c9dab3 ls -l /etc/b.txt lrwxrwxrwx 1 root root 15 Mar 2 15:12 /etc/b.txt -> pipe:[38941509] 这垃圾居然直接把 process substitution 的 pipe 打到 tarball 里了, 就不能认认真真去 read 到内存里吗? 然后它还有一个 followLink 的 option, 这次直接报错了: # docker -H tcp://localhost:2376 cp --follow-link <(cat /tmp/a.txt) f21996c9dab3:/etc/b.txt lstat /proc/300038/fd/pipe:[38956641]: no such file or directory 这次直接报错了, 看了一下代码, 因为在 follow 的情况下它非要去 os.Lstat 一下 link 的 dest, 然而 pipe 在文件系统上是不存在的, 就挂了. 我 真 的 服 了. from https://gist.github.com/jschwinger23/d8486dac6cef0d4b88be40f5409515eb#file-docker-cp-sucks-md
Posted Feb 29
from https://gist.github.com/jschwinger23/0d5327e0e800b6b1a607f66a5321fbd7#file-plots-md
Posted Feb 29
几个诉求: 1. 对我自己而言, 我希望能记录下来自己学习工作遇到的问题, 对抗自己的金鱼记忆力, 因此: 1. 便于搜索, 比如我两年前第一次学习 Kubernetes 的网络机制记录了 NodePorts 的原理, 希望能立刻找到. 2. 有人围观, 围观让我有动力更新, 同时不会太敷衍了事. 3. 能够评论, 长期关注几个 tg channel 最不爽的地方就是不能评论, 这不行. 2. 对群众来说, 目前搜集到的需求有: 1. 能够检索, 为开放的互联网生态尽一份力 2. 便于分享, 一条消息一个 url 是最好的 3. 没有了 对比和尝试了一些方案, 最后暂定成这样: 1. 内容发布在 gist, 好处是: 1. markdown 渲染比 tg 这垃圾好多了 2. 还能搜索, 语法是这样的: https://gist.github.com/search?q=user%3<username>+<keywords>, 比如搜索我的 gist 里的 leak 关键词: https://gist.github.com/search?utf8=%E2%9C%93&q=user%3Ajschwinger23+leak 3. 便于分享, 甚至还可以评论, fork, 多文件, embed html 2. gist clone 到我的 github pages repo 里, 好处是: 1. 我不知道 gist 搜索怎么样, 反正 github repo 内部搜索体验是不太好的, 所以静态保留一份便于自己通过正则 / 文件名 / 时间等信息随意操作 2. 理论上 gist 是会被 google 爬虫的, 但是我实例搜了一下自己之前的 gist 发现只有一个被收录了: site:gist.github.com jschwinger23, 而试图搜索上面那个 leak.go 是搜不到的. 3. 我习惯用 vim 打字, 所以会先创建空 gist, 然后 clone, push 4. urls 在: https://github.com/jschwinger23/jschwinger23.github.io/tree/master/gists 3. 然后利用 typora 把 markdown 生成图片, 分享到 tg channel: 1. 尝试了直接分享 gist url, 然而在 tg 里的 preview 没有可读性, 几乎是 spam 级的信息, 发图片就没这问题, 还能解决 markdown 渲染的问题 2. 当然图片下面附带 gist url, 想分享的同学也不会困扰; 问题就是在 tg channel 里不能检索了, 不过 tg 的检索本来就是废的, 我试过了, 完全没救. 3. 这一步最烦的问题是 typora 没有命令行, 这个 issue 已经挂了三年了: https://github.com/typora/typora-issues/issues/1999, 否则我可以直接一键利用 tg API 发布了. 4. 在 github pages 上有 url 链接到 tg channel 的 preview: https://t.me/s/TheInfiniteSadness, 位置应该是很明显的, 如图: !post 这是第一条, 发出来看看效果. 发出来后发现图片效果不好, 糊得太厉害, 发文件又丧失了便捷性, 所以我研究了一下 tg bot API, 发现做一下切分可以一键发布, 比如这条就是一键发布的.
Posted Feb 26
Do One Thing and Do It Well