TGINSIGHT CHAT
Welcome to the Black Parade
@TheB1ackParade
MusicDeath has many faces, I look forward to seeing this one.
Recent posts
Page 72 of 76 · 907 posts
Posted May 10
妈个鸡 wireshark for Linux 默认不开 ip header validation,浪费我这么多时间 凸(艹皿艹 ) 打开方式:edit -> preference -> protocol -> ipv4 -> validate checksum
Posted Jun 24
我在大部分时候都不喜欢 xargs, 因为不能比较方便的执行稍微复杂一点点的脚本, 比如经常干的一件事是遍历容器找 veth: for p in $(ps -ef | awk '/init/ {print $2}'); do ln -s /proc/$p/ns/net /var/run/netns/$p; echo $p; ip netns exec $p ip l | grep ^$IDX; rm /var/run/netns/$p; done 这种循环如果用 xargs 来写的只能是: ps -ef | awk '/init/ {print $2}' | xargs -I{} bash -c 'ln -s /proc/$0/ns/net /var/run/netns/$0; echo $0; ip netns exec $0 ip l | grep ^$IDX; rm /var/run/netns/$0' {} 非常不方便. 但是 xargs 提供了简单的并行方案, 在需要性能的时候可以简单通过 -P 指定核心数来并发运行, 非常感人. 于是今天在一个需要并发提速的场景下, 我踩上了 xargs 的另一个天坑: 无法处理包括 " 在内的特殊字符. 比如我有一个文件里没一行都是 flat JSON: {"kind":"WorkloadEndpoint","apiVersion":"projectcalico.org/v3","metadata":{"name":"cachecloud--agent--sg2--test--20.shopeemobile.com-yavirt-yavirt-04fbf89eb0d611eaaa3bfe5400dac964","namespace":"cachecloud-agent-sg2-test-20.shopeemobile.com","uid":"a00da22d-14d1-4c17-932d-e244eaef03ac","creationTimestamp":"2020-06-17T20:06:24Z","labels":{"projectcalico.org/namespace":"cachecloud-agent-sg2-test-20.shopeemobile.com","projectcalico.org/orchestrator":"yavirt"}},"spec":{"orchestrator":"yavirt","workload":"yavirt","node":"cachecloud-agent-sg2-test-20.shopeemobile.com","endpoint":"04fbf89eb0d611eaaa3bfe5400dac964","ipNetworks":["10.143.220.38/32"],"profiles":["calico-pool-1"],"interfaceName":"yap04fbf94bb0d6","mac":"52:54:00:37:8a:9b"}} {"kind":"WorkloadEndpoint","apiVersion":"projectcalico.org/v3","metadata":{"name":"cachecloud--agent--sg2--test--20.shopeemobile.com-yavirt-yavirt-0584726fb33a11eaaa3bfe5400dac964","namespace":"cachecloud-agent-sg2-test-20.shopeemobile.com","uid":"dea810ac-573d-405f-b50e-57d19c3165ab","creationTimestamp":"2020-06-20T21:07:17Z","labels":{"projectcalico.org/namespace":"cachecloud-agent-sg2-test-20.shopeemobile.com","projectcalico.org/orchestrator":"yavirt"}},"spec":{"orchestrator":"yavirt","workload":"yavirt","node":"cachecloud-agent-sg2-test-20.shopeemobile.com","endpoint":"0584726fb33a11eaaa3bfe5400dac964","ipNetworks":["10.143.219.211/32"],"profiles":["calico-pool-1"],"interfaceName":"yap05847314b33a","mac":"52:54:00:fb:e4:42"}} xargs 读到每一行的时候居然傻逼到把双引号吃掉了: # cat file | xargs {kind:WorkloadEndpoint,apiVersion:projectcalico.org/v3,metadata:{name:cachecloud--agent--sg2--test--20.shopeemobile.com-yavirt-yavirt-04fbf89eb0d611eaaa3bfe5400dac964,namespace:cachecloud-agent-sg2-test-20.shopeemobile.com,uid:a00da22d-14d1-4c17-932d-e244eaef03ac,creationTimestamp:2020-06-17T20:06:24Z,labels:{projectcalico.org/namespace:cachecloud-agent-sg2-test-20.shopeemobile.com,projectcalico.org/orchestrator:yavirt}},spec:{orchestrator:yavirt,workload:yavirt,node:cachecloud-agent-sg2-test-20.shopeemobile.com,endpoint:04fbf89eb0d611eaaa3bfe5400dac964,ipNetworks:[10.143.220.38/32],profiles:[calico-pool-1],interfaceName:yap04fbf94bb0d6,mac:52:54:00:37:8a:9b}} {kind:WorkloadEndpoint,apiVersion:projectcalico.org/v3,metadata:{name:cachecloud--agent--sg2--test--20.shopeemobile.com-yavirt-yavirt-0584726fb33a11eaaa3bfe5400dac964,namespace:cachecloud-agent-sg2-test-20.shopeemobile.com,uid:dea810ac-573d-405f-b50e-57d19c3165ab,creationTimestamp:2020-06-20T21:07:17Z,labels:{projectcalico.org/namespace:cachecloud-agent-sg2-test-20.shopeemobile.com,projectcalico.org/orchestrator:yavirt}},spec:{orchestrator:yavirt,workload:yavirt,node:cachecloud-agent-sg2-test-20.shopeemobile.com,endpoint:0584726fb33a11eaaa3bfe5400dac964,ipNetworks:[10.143.219.211/32],profiles:[calico-pool-1],interfaceName:yap05847314b33a,mac:52:54:00:fb:e4:42}} 于是我发现了 GNU/parallel, 最简单的用法是这样的: cat file | parallel -j10 -q bash -c 'echo -- $0' {} -j 指定了使用的核心数量, -q 是为了能正确执行 bash -c ''; 除了命令行用法之外还可以用 shebang 来执行: #!/usr/bin/parallel --shebang-wrap /bin/bash # p.bash ip=$(echo "$@" | jq ".spec.ipNetworks[0]" -r) calicoctl ipam show --ip=${ip%%/*} 然后 chmod +x p.bash cat file | ./p.bash 注意在命令行里用 $0 而在脚本里用 $@. 好用. from https://gist.github.com/jschwinger23/0d20e887d3f7304e68fdd395e9b8308e#file-intro-parallel-md
Posted May 25
另外两点想法 1. gophers 都不会做并发抽象了 没有语言原生支持并发的时候, 大家有 pool, 有 future, 有 actor, 有 pubsub, 有好莱坞风格, 有双分派, 结果到了 go 里面, 全是裸的 channel 满天飞, 一个 switch case <-chan 能写六七个分支每个分支一屏幕的代码, 没有抽象, 只有原生的, 赤裸裸的, channel, 什么开放封闭, gopher 从不在意, 一扩展就加 case, 真是精妙的软件工程. 2. 我就是想黑一下 rust 天天在 python 群里看人吹 rust, 拜托 rust 界连个能用的 etcdv3 客户端都没有, 求求你们别吹了. 不服自己去看看 etcdv3 watch 的实现, 你以为只是 grpc-rust 包装一下吗, 你错了, 里面的逻辑巨复杂, 一个 grpc stream 要 serve 任意数量的 watch stream, 发送必须有序但是接受 watch response 可以无序, 还要处理 auth 和出错重试, 重试的时候 watch stream 可一个都不能丢哟. 希望 rust 吹们闭上嘴多写代码, 谢谢. 另外 grpc-rust 的各种实现连集成 future3.0 + tokio 异步 IO 都挤牙膏一样挤不出来, ~~一群弱智, 可笑~~. from https://gist.github.com/jschwinger23/f0b14f62c628e00524e42743e869adac#file-reading-etcd-client-md
Posted May 25
最近一周一直在查 calico SDN 挂掉的问题, 过程十分艰辛, 回头想了想觉得可以好好反思一下. # 优先尝试进行稳定复现 太沉迷使用各种工具, 倒不是坏事, 只不过性价比太低了. 在开始怀疑 是自己 restart 服务导致的 之后, 就应该立刻开始进行集群的复刻和复现测试: 1. 复刻: 完全相同的机器上, 使用完全相同的配置(除了端口和落盘目录等), 启动几乎等价的集群, 导入完全相同的数据 2. 简化: 完全复刻客户端需要启动 calico-felix, 想到一台机器部署两个 felix 是需要技巧的, 不如先简单尝试一下 etcdctl 能否复现, 因为成本低, 就算不能也能快速回到正确的轨道进行完全复刻 3. 半跨步: 先复刻客户端, 让复刻版本使用原版服务端, 测试, 然后也好做简化版的复刻 到最后希望是抛掉一切包装的东西, 比如 felix 使用 etcdv3 client, 那么我们应该最后用 etcdctl 复现或者 etcdv3 go client 复现; 网络中间有 etcd gateway, 如果它不是 cause 就应该把它扔掉; 最终得到一个最简可复现过程, 那么 bug 也几乎可以判死刑了. 当然人类难免出错, 我在复刻服务端的时候正好漏掉了最关键的东西导致没能复现, 不过稍后通过只复刻客户端并在简化后的客户端打断点看到了足够的信息让我发现了问题. # 不能复现时能做什么 比方说刚拿到手的时候根本不可能知道集群发生了什么, 无法推测怎么才能复现, 尝试都没法尝试, 万一是集群放置时间太长导致 TCP 连接被 iptables conntrack drop 了呢, 没法下手的. 可以从几点下手: 1. 日志: 如果有报错, 当然很好, 如果没有, 那就对比正常的日志和不正常的日志, 推测少做了什么事情 2. attach 进程, 当然这对编译时 flag 有要求, 不过万一可以 attach 的话, 结合源码去看应该做什么事情却没做或者含糊不清的报错日志到底在说啥, 断点打上去查看变量 3. 如果不能 attach, 那就 strace -fTp 查看 syscalls, 如果难以下咽, 可以和一个正常的进程的 strace 来对比 4. 外部信息还有很多, 进程 fds 里还有那些该有的连接吗, unix socket 都还在吗, 线程数量, goroutine 数量和正常的相比一致吗 5. tcpdump 抓包能抓到推送或者响应吗; 如果顺利的话可以立刻把问题出在客户端还是服务端给划清; 如果是二进制流量, 那和正常进程的 segment 相比, length 大小一样吗 通过进程的外部和内部, 我们可以搜集到很多信息, 不过也可能被误导, 要及时回头纠错. # 能够 attach 时能做什么 gdb manual 600 页, 然而最有用的依然是查看线程(go extension 下可以看 goroutine), 切换, 查看变量, 打断点. 查看另一端究竟发了什么过来, 发还是没发, 查看内存里的状态还对不对, 那个 map[int64]chan 是否保存着正确的东西, 如果不正确那么在什么时候改坏的, 我打个断点重新触发一下试试. 然而在分布事系统里这些都是奢望, 一 attach, 进程阻塞, 状态全变了, 我甚至觉得运行一个自己打日志后的二进制是最简单的; 其次可以通过 gdb -ex 来执行一次性命令并祈祷不会导致状态改变. tracepoint 对 go 是奢望, 再见. # 放置 play 最后真的毫无办法, 进程不能 attach, 日志级别过高, 什么也分析不出来, 那么换掉 bin 吧, 换成自己编译的可以 attach 的 binary, 日志级别调高, 等待下一次 bug 降临. from https://gist.github.com/jschwinger23/2d211d4ef7e2beff1ae50c100e575393#file-thinking-in-debugging-md
Posted May 25
<domain type="kvm"> <name>v2</name> <uuid>27144641-23ff-42ad-9bd5-6ab0a4ff6ec7</uuid> <memory>1048576</memory> <currentMemory>1048576</currentMemory> <vcpu>1</vcpu> <os> <type arch="x86_64">hvm</type> <boot dev="hd"/> </os> <features> <acpi/> <apic/> </features> <cpu mode="custom" match="exact"> <model>Broadwell-IBRS</model> </cpu> <clock offset="utc"> <timer name="rtc" tickpolicy="catchup"/> <timer name="pit" tickpolicy="delay"/> <timer name="hpet" present="no"/> </clock> <on_poweroff>destroy</on_poweroff> <on_reboot>restart</on_reboot> <on_crash>restart</on_crash> <pm> <suspend-to-mem enabled="no"/> <suspend-to-disk enabled="no"/> </pm> <devices> <emulator>/usr/bin/kvm-spice</emulator> <disk type="file" device="disk"> <driver name="qemu" type="qcow2"/> <source file="/root/CentOS-7-x86_64-GenericCloud.qcow2"/> <target dev="hda" bus="ide"/> </disk> <controller type="usb" index="0" model="ich9-ehci1"/> <controller type="usb" index="0" model="ich9-uhci1"> <master startport="0"/> </controller> <controller type="usb" index="0" model="ich9-uhci2"> <master startport="2"/> </controller> <controller type="usb" index="0" model="ich9-uhci3"> <master startport="4"/> </controller> <interface type="network"> <source network="default"/> <mac address="52:54:00:d5:af:0b"/> </interface> <input type="mouse" bus="ps2"/> <graphics type="spice" port="-1" tlsPort="-1" autoport="yes"> <image compression="off"/> </graphics> <console type="pty"/> <channel type="spicevmc"> <target type="virtio" name="com.redhat.spice.0"/> </channel> <sound model="ich6"/> <video> <model type="qxl"/> </video> <redirdev bus="usb" type="spicevmc"/> <redirdev bus="usb" type="spicevmc"/> </devices> </domain> 关注里面的 disk, vcpu, memory 的值; 你也可以直接编辑文件修改. 然后用 libvirt client virsh virsh define v2.xml 检查一下, 启动起来 virsh list --all virsh start v2 发现报错 error: Cannot access storage file '/root/CentOS-7-x86_64-GenericCloud.qcow2' (as uid:64055, gid:121): Permission denied, 看一下 uid 64055 是用户 libvirt-qemu, 读不了 /root/, 所以把镜像放在 /opt/qemu-img/ 下 mkdir /opt/qemu-img/ cp /root/CentOS-7-x86_64-GenericCloud.qcow2 /opt/qemu-img/v2.qcow2 然后改下 xml 里的镜像路径到 /opt/qemu-img/v2.qcow2, 重来一遍 virsh undefine v2 virsh define v2.xml virsh start v2 virsh list 成功! 创建好的 vm 的 xml 都在 /etc/libvirt/qemu/ 里. pgrep qemu -la 能看到启动的进程依然是 qemu-system-x86_64 那一套, 只不过 flag 爆多
Posted May 25
# KVM 极简入门 ## 一. QEMU QEMU (Quick Emulator) 提供了硬件和处理器虚拟化, 它跑在用户态, 无需内核. 话不多说, 直接用起来. 我使用的 OS 是 Ubuntu16. ### 1.1 安装 apt-get update apt-get install qemu 然后检查一下 dpkg --list | grep qemu ### 1.2 拉个镜像 curl -OL https://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2 经过漫长的等待, 检查一下 qemu-img info CentOS-7-x86_64-GenericCloud.qcow2 会输出 format qcow2, virtual size 8G. ### 1.3 第一个虚拟机 qemu-system-x86_64 -name zc -cpu Nehalem -m 1024 -drive format=qcow2,file=CentOS-7-x86_64-GenericCloud.qcow2 -vnc 0.0.0.0:0 -daemonize -cpu Nehalem Nahalem Intel Core i7, -m 1024 1G 内存, -drive 指定刚才下载的镜像, -vnc 绑定一个端口让你访问虚拟机 然后检查一下 pgrep -la qemu 如果要停止的话直接 pkill -9 qemu ### 1.4 登录 我 macOS 下载了一个 VNC Viewer, 然后在 GUI 上连接 Ubuntu 的 IP:5900, 顺利连接, 然后让我登录, 但我不知道 root 和密码. 改镜像密码, 先下载 apt install -y libguestfs-tools 然后设置 root 密码为 root virt-customize -a CentOS-7-x86_64-GenericCloud.qcow2 --root-password password:root 会报错 virt-customize: error: libguestfs error: guestfs_launch failed., 这是因为在这个镜像上正跑着一个 VM 呢, 关掉先. 改好密码后最后重新启动一个虚拟机, VNC 连接登录, 成了. 进去后检查一下 cpu, memory, disk free -h lscpu df -h 应该是和我们指定的一样的, 分别是 1G, 1C, 8G. ### 1.5 KVM KVM (Kernel-based Virtual Machine) 和 QEMU 最大的不同是它跑在内核态, 所以需要内核支持. 先检查 cpu 支持 KVM 虚拟化: lscpu | grep vmx | uniq 然后检查内核模块 modprobe kvm lsmod | grep kvm 那么来吧 qemu-system-x86_64 -name zc -cpu Nehalem -m 1024 -drive format=qcow2,file=CentOS-7-x86_64-GenericCloud.qcow2 -vnc 0.0.0.0:0 -enable-kvm -daemonize 注意新增的 -enable-kvm. 另一种做法是下载 kvm 二进制 apt install qemu-kvm kvm -name zc -cpu Nehalem -m 1024 -drive format=qcow2,file=CentOS-7-x86_64-GenericCloud.qcow2 -vnc 0.0.0.0:0 -daemonize 不过本质上 kvm 二进制只是把 qemu-system-x86_64 包了一下, pgrep -la qemu 看到两种拉起来的进程是一样的. ## 二. libvirt libvirt 对不同的虚拟化技术提供了统一的上层接口, 比方说管理 QEMU/KVM, XEN, LXC. ### 2.1 安装 apt install -y libvirt0-dbg/xenial-updates apt install -y libvirt-bin 然后 libvirtd daemon 就起来了 systemctl status libvirtd 看看配置 cat /etc/libvirt/libvirtd.conf | grep -v '^#\|^$' 很多东西在 /etc/libvirt/ 里, 猫一眼 # ls -lF /etc/libvirt/ total 84 drwxr-xr-x 2 root root 4096 Mar 11 21:52 hooks/ -rw-r--r-- 1 root root 450 Mar 11 21:52 libvirt-admin.conf -rw-r--r-- 1 root root 547 Mar 11 21:52 libvirt.conf -rw-r--r-- 1 root root 15276 Mar 11 21:52 libvirtd.conf -rw-r--r-- 1 root root 834 Mar 11 21:52 libxl.conf -rw-r--r-- 1 root root 2169 Mar 11 21:52 libxl-lockd.conf -rw-r--r-- 1 root root 1235 Mar 11 21:52 lxc.conf drwxr-xr-x 2 root root 4096 May 19 22:23 nwfilter/ drwxr-xr-x 3 root root 4096 May 19 22:23 qemu/ -rw------- 1 root root 19536 Mar 11 21:52 qemu.conf -rw-r--r-- 1 root root 2169 Mar 11 21:52 qemu-lockd.conf -rw-r--r-- 1 root root 2134 Mar 11 21:52 virtlockd.conf -rw-r--r-- 1 root root 1802 Mar 11 21:52 virtlogd.conf -rw-r--r-- 1 root root 1217 Mar 11 21:52 virt-login-shell.conf 关注 libvirt.conf 是给 libvirt client 用的, libvirtd.conf 是给 libvirtd 用的, qemu.conf 是 libvirtd 驾驭 QEMU/KVM 的配置, qemu/ 里是创建的 VM 配置 ### 2.2 第二个虚拟机 先安装工具 apt install virtinst/xenial-updates 然后生成 libvirt domain xml virt-install --name v2 --ram 1024 --disk path=CentOS-7-x86_64-GenericCloud.qcow2,format=qcow2 --print-xml > v2.xml 看一下 v2.xml
Posted May 15
# 手动实现 bridge 网络和 calico 网络 本质上要解决的问题是连通 host 和 netns, 来体会一下两者的不同思路. ## Bridge 用变量方便大家阅读, 否则区分不出语法和变量 NS=dock-ns3 VETH=vd3 BRIDGE=dock3 先创建 network namespace, 以下简称 netns ip netns add $NS 创建 veth pair: ip l add $VETH type veth peer name $VETH-peer veth 拉起来, 一端塞到 netns 里, 在 netns 里拉起来, 设置 ip ip l set $VETH-peer up ip l set $VETH netns $NS ip netns exec $NS ip l set $VETH up ip netns exec $NS ip a add 10.5.0.2/16 dev $VETH 创建 bridge, 拉起来 ip l add name $BRIDGE type bridge ip l set $BRIDGE up 把 veth 另一端加入 bridge ip l set $VETH-peer master $BRIDGE 最后把 bridge 也设置上 ip: ip a add 10.5.0.1/16 dev $BRIDGE 宿主机上的路由也会自动加上: # ip r | grep dock3 10.5.0.0/16 dev dock3 proto kernel scope link src 10.5.0.1 这时候就可以在宿主机 ping netns 里的 ip 了. 如果按照上面的流程再创建另一个 netns 和 veth pair 并加入 bridge, 那么两个 netns 已经可以互 ping 了. 不过还有问题, netns 里 ping 不了外面的世界, 这是因为 netns 里的路由没有默认 gateway: ip netns exec $NS ip r add default via 10.5.0.1 不过依然不可以, 这是因为没有 SNAT iptables -t nat -A POSTROUTING -s 10.5.0.0/16 -j MASQUERADE 完工. 这里的几个细节几乎和大家熟悉的 docker0 if 一模一样, 我枚举一下: 1. docker0 bridge 等价于 dock3 bridge 2. netns 里的 route 设置一模一样, default gw 指向 bridge, 相同网络的指向 veth 3. iptables nat 表 POSTROUTING chain 里把所在网络全部 SNAT ## Calico calico 的做法完全没有 bridge 这种妖艳贱货, 而是 veth 直通, 路由指路. 先设置变量: NS=cali VETH=v-cali 创建 netns 和 veth, veth 一端塞进去, 射 ip: ip netns add $NS ip l add $VETH type veth peer name $VETH-peer ip l set $VETH-peer up ip l set $VETH netns $NS ip netns exec $NS ip l set $VETH up ip netns exec $NS ip a add 10.2.0.1/32 dev $VETH 然后在宿主机直接路由 ip 到 veth: ip r add 10.2.0.1/32 dev $VETH-peer netns 里设置 default gw 到 veth: ip netns exec $NS ip r add default dev $VETH 这时候可以从 host ping netns, 但是反过来就不可达, 抓包发现是因为 arp 不知道 mac 地址, 加上 arp proxy echo 1 > /proc/sys/net/ipv4/conf/$VETH-peer/proxy_arp sysctl -p 发现还是不行, 抓包发现 arp 没问题了但是 ICMP 没有回复, 这是因为 SNAT: iptables -t nat -A POSTROUTING -s 10.6.0.1/32 -j MASQUERADE 这下可以和 host 互 ping 了, google.com 也没问题了, 功能上没问题. 不过有个优化的问题, arp proxy 会有一些问题, 比如这里会导致 netns 里的 arp cache 无限扩张, 所有的 outbound ip 都会产生一条 arp entry. 为了解决这个问题, 我们用一个假的 ip 169.254.1.1 作为 link-local address, 绕一下: ip netns exec $NS ip r del default dev $VETH ip netns exec $NS ip r add 169.254.1.1 dev $VETH scope link ip netns exec $NS ip r add default via 169.254.1.1 dev $VETH 这下所有的 outbound 都会变成对 169.254.1.1 arp, 也不会产生无数的 entry. 和 calico 比较一下, 要点有这么几个: 1. arp proxy 代替了 bridge 打通二层 2. 169.254.1.1 这个 ip 也是 calico 使用的 ip, 在 calico 创建的 netns 路由里这个迷一样的 ip 让无数人神魂颠倒 当然剩下的事情我就不说了, bird 监听到宿主机的新路由后用 BGP 广播 Update 消息, 从而在完成跨宿主机的路由建立. from https://gist.github.com/jschwinger23/a5cd21a5adf0ac0079e17fcc1179b37a#file-netns-integration-md
Posted May 13
# WHO NEEDS CONTAINERD 标题来自 XCOM2 的成就之一 WHO NEEDS TYGAN, 要求在最后一关使用初级科技通关, 简直恶趣味.. 总之成功用 runc 调用 CNI 建立了 calico 网络, 流程是这样的: 0. 安装 runc, calico, etcd, 创建 calico ippool: - apiVersion: projectcalico.org/v3 kind: IPPool metadata: name: $POOL_NAME spec: natOutgoing: true cidr: $POOL_CIDR 1. 安装 calico CNI: a. curl -OL https://github.com/projectcalico/cni-plugin/releases/download/v3.4.4/calico-amd64 b. curl -OL https://github.com/projectcalico/cni-plugin/releases/download/v3.4.4/calico-ipam-amd64 c. mv ./calico-amd64 /opt/cni/bin/calico d. mv ./calico-ipam-amd64 /opt/cni/bin/calico-ipam e. chmod +x /opt/cni/bin/calico /opt/cni/bin/calico-ipam 2. 准备 CNI 配置到 /etc/cni/net.d/ { "name": "calico-cni", "cniVersion": "0.3.1", "type": "calico", "etcd_endpoints": "http://127.0.0.1:2379", "log_level": "info", "ipam": { "type": "calico-ipam", "ipv4_pools": ["$POOL_NAME"] } } 当然要改成自己的 pool name. 3. 然后 runc 走起来 a. mkdir rootfs b. docker export $(docker create busybox) | tar -C rootfs -xvf - c. runc spec 4. 骚的来了, 在 config.json 里加入 hook 调用 CNI: "hooks": { "prestart": [ { "env": ["PATH=/opt/cni/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin", "CNI_PATH=/opt/cni/bin"], "path": "/root/cni-master/scripts/exec-plugins.sh", "args": [ "/root/cni-master/scripts/exec-plugins.sh", "add", "zzc", "/run/netns/zzc" ] } ] } 对了要先安装 cnitool, 我是直接下载 github.com/containernetworking/cni; 然后也要手动创建 netns zzc (智障川) 5. runc run zzc, 可以看到容器是成功集成到 calico SDN. 正好借这贴说下 docker 和 Kubernetes 的创建流程. docker 的流程是: 1. start container 的第一步, 调用 containerd 之前, 先把 CNM 流程走了, 请求 IP, 创建内部 endpoint 对象 2. 然后 containerd NewContainer 时候在 OCI spec 里塞上 prestart hook, 类似 libnetwork-setkey $CONT_ID $CONTROL_ID 3. 然后在 runc 启动容器前运行这个 hook, 会通过 controller id 绑定的一个 unix socket 给 dockerd 发请求, 然后 dockerd 会给容器创建好的 netns 设置上申请好的 IP 和 veth Kubernetes: 1. CRI 先创建好 pause 容器(呕 2. 直接调用 CNI 申请 IP + 绑定到 pause 容器 netns 一步完成 3. 然后 OCI spec 里指定 network namespace path 完事儿 可以看到两个的做法作风是完全不同的 from https://gist.github.com/jschwinger23/26b231ded56989044f022408e9f3e569#file-runc-cni-md
Posted Apr 22
分层和解耦对我的诱惑太大了, 最近写业务有一些想法. ## UI -> App 比方说一个服务有两种协议 http 和 grpc, 再加上一个 cli 吧, 算是三种完全不同的 UI 层了, 共享同一个 App 层的函数: type App interface { func CreateInstance(name string, count, memory, cpu int, network, podname string, dns, env []string) *Instance } 这个函数的参数已经够多了, 理论上来说, 我需要用一个 struct 来统筹一下, 比如叫 CreateOptions 的类型, 不过最核心的问题是, 这个 CreateOptions 类型在哪一层? 如果在下层 Model, 属于建模的一部分, 那么模型泄漏了, App 层知道得太多了; 如果放在当前 App 层, 那么下层又会依赖上层, 下层对上层的非接口耦合又让我感到不适, 依赖倒置原则说的可是接口. 当然直接使用领域模型的好处也是显然的, 简化参数就不说了, 首先我们有了更可靠的类型检查, 其次对于返回类型更加容易. 来说第二点, 看上面的代码里返回的是 *Instance, 其实就是模型泄漏了; 但是如果你真的要返回一个有信息量的东西而不仅仅是 ID 什么的, 也不是 mapstringinterface{} 这种不反射都不知道怎么遍历的东西, 那么工作量一下就大起来了. 第一种做法, 返回接口, 这种接口取决与更上层 UI 的使用; 比方说我们有两个 UI, 一个是 http 返回 JSON 一个是 grpc, 那么我们根据业务把 UI 层感兴趣的字段抽出来, 实现一个 interface: type InstanceProvider interface { ProvideID() string ProvideStatus() string } 这样这个 App 层的函数就变成了: type App interface { func CreateInstance(name string, count, memory, cpu int, network, podname string, dns, env []string) InstanceProvider } 注意这时候虽然下层 App 依然要依赖上层 UI 定义的接口 InstanceProvider, 但是这是符合依赖倒置原则的, 没有任何的硬耦合. 然后 UI 层就可以放肆地调用接口定义的方法来填充自己的 Presentation Model, 而不用知道调用的真实的类型: func (ui *UI) CreateInstance(w http.ResponseWriter, r *http.Request) { provider := ui.app.Create(r.FormValue("name")) model := newHTTPPresentModel(provider) fmt.Fprintf(w, model.toJSON()) } grpc UI 就不写了, 类似. 要注意 App interface 的定义也是在 UI, 而实现是在 App 层, 这是标准的依赖倒置, 就不说了. 另一种做法可能是传入一个接口对象, 比如叫 InstanceInterest: type InstanceInterest interface { InformID(string) InformStatus(string) } 然后传入 App 的函数: type App interface { func CreateInstance(name string, count, memory, cpu int, network, podname string, dns, env []string, interest InstanceInterest) error } 在 App 层最后调用 InstanceInterest 的方法来通知上层; 此时的 App 的类型依然没有模型泄漏, 耦合也是下对上的接口, 很好. 我个人更倾向方案一, 方案二的这种 value-result 模式太 C 了; 不过如果真的没那么介意模型泄漏的话, 也是可以忍受 App 层类型直接使用领域模型, 这里讨论的是"如果我一定不使用领域模型"怎么办. ## App -> Model App 作为 Model 的客户端应该是很轻的一层, 但是我的主要问题是事务. 比方说具体的业务是要通过 Kubernetes SDK 去创建 Nginx, 同时管理好自己的元数据, 那么至少有两种流派: 第一种是先做事, 再创建元数据: func (a *App) CreateNginx(name string) NginxProvider { virtualization, err := a.orchestrator.CreateVirtualization(...) defer func() { if err != nil { a.orchestrator.RemoveVirtualization(virtualization) } }() err = a.store.Save(virtualization) return virtualization } 为了实现事务, 必须用 defer 处理"创建好容器但是写元数据失败"的情况; 当业务恶心的时候, App 层的实现简直不看入木. 或者, 第二种我们先写元数据: func (a *App) CreateNginx(name string) NginxProvider { nginx := newNginx(name) a.store.Save(nginx) a.postCreateNginx(nginx) return nginx } 这时候我们先建模, 上来先创建一个内存对象 nginx, 然后先存元数据再说, 最后再操作副作用. 如果我们再把 postCreateNginx 扔到单独的 goroutine 运行, 然后定义 nginx 对象的生命周期, 那就太美好了: func (a *App) postCreateNginx(nginx) { if err := a.orchestrator.CreateVirtualization(nginx); err != nil { nginx.UpdateStatus(Created) } else { nginx.UpdateStatus(CreateFailed) } } 有限状态机可以搬出来了, 这时候事务已经被压缩到最小化, 你发现我们没有 defer 处理回滚, 因为事务只有一件事"写元数据", 而创建容器是单独的后台运行的, 运行后无论成功失败都记录到元数据里, 如果本身要做的事情特别多的话, 定义生命状态周期, 分步进行, 记录状态, 比一个巨大的事务可控得多. 而对上层和客户来说, 如果有一个 web ui, 这种异步模式也友好得多, 用户创建后能立刻看到自己创建的东西和状态, 而不是等浏览器响应等个两分钟, 都不知道是网络问题还是什么问题, 也不敢刷新怕重复创建. DDD 里反复强调小聚合, 多做最终一致性, 这里也能有体现. 而且 App 层也会变得特别简单, "express user story", "simple invocations" 都能得以实现. 要是再 DDD 一点的话, 这里该上领域事件了, 或者 Event Bus, watever, remote 地处理的话, 那整个架构又可以 FaaS 化, 因为 event 驱动本身就很好 FaaS 化, 加上每个 event 的事务单一, 对于失败的创建, 无论是人工触发补偿或者回滚都没问题, 或者有有个 event center 做自动补偿回滚; 不过无论哪样, 我们的事务是能保证的, 也就是说不会出现 dangling container (容器在跑了但是没有记录下元数据) 或者 empty instance (元数据记录但是容器没有), 或者是一失败就 cascade delete 把啥都删了你都不知道怎么 debug; 精细化的生命周期管理, 小事务小聚合, 不管怎么做都好. from https://gist.github.com/jschwinger23/38d8b5c7d35ba0b8bf58564357f8f448#file-layer-and-decouple-md
Posted Apr 21
没想到一个 open files limit 都能让我有这么多惊喜. 1. ulimit 和 rlimit 是一个东西, 有些文章说 ulimit 代表的是 user limit, 限制的是一个 effective user 的总资源, 而 rlimit 限制的是一个进程的资源, 这是错的. 2. strace -fT ulimit -n 是不行的, 因为 ulimit 是 shell builtins, 所以 strace ulimit 直接报错说找不到命令; 同理 which ulimit 也没有输出(我第一次知道 builtins 是不能 which 的) 3. man 3 ulimit 说 This routine is obsolete. Use getrlimit(2), setrlimit(2), and sysconf(3) instead, 有理由相信 ulimit 就是 rlimit 4. 直接看 bash 源码, 在 builtins/ulimit.def 里可以清楚看到 ulimit 就是调用 rlimit, 第一个问题解决了, 以下讨论 rlimit. 5. rlimit 是限制用户 / 会话 / 进程组的吗? 只有 RLIMIT_NPROC, RLIMIT_MSGQUEUE, RLIMIT_SIGPENDING 是按照 real user ID 限制的多个进程的, 分别限制的是进程数, POSIX 消息队列字节数, 排队信号数, 其他都是限制单进程. 6. /proc/$PID/limit 和 ulimit 的关系是对应的, 但是如果你在 shell 里 ulimit -a 看到的东西和一个进程的 /proc/$PID/limit 是不一样的, 不要慌, rlimit 是继承的, 所以看看父进程, 看看不同用户的 rlimit 是否不一样, 看看进程自己是否修改了. 7. docker run --ulimit 是在 docker 层做的! 我意思是, 这不是交给 containerd 和 runc 做的, runc 对此一无所知. 不过具体在哪一步设置的我还不清楚, 我的 docker 解剖计划已经拖延很久了. 8. 进程出现了 accept4: too many open files in system 之类的日志, 但是 lsof -p 或者 /proc/$PID/fd/ 数出来的 fd 都明显没有超过 rlimit, 那么可以考虑全局 fd 限制. 9. cat /proc/sys/fs/file-max 定义了 the maximum number of file-handles that the Linux kernel will allocate, 而超过这个限制的话可以在内核日志里看到 VFS: file-max limit <number> reached. 10. 查看一个进程使用的 open files number, 如果想通过 syscall 的话, 只有一个 getrusage(2) 可以用, 不过只能查看自己或者子进程的 rlimit usage, 所以如果我作为 sibling 也是无法查看的... 11. 回到上面说的全局内核 file-handles, cat /proc/sys/fs/file-nr 记录了 the number of allocated file handles, the number of allocated but unused file handles, and the maximum number of file handles, 不过我自己试了一下, 第一个数不是即时修改的, 所以参考价值不大. from https://gist.github.com/jschwinger23/5ab5a083938b15f9dde342ae2891d476#file-rlimit-md
Posted Apr 14
安利浏览器插件 https://github.com/tridactyl/tridactyl/blob/master/readme.md 作为 vimium 的长期用户, 这次是瞬间抛弃 chrome, 只为使用 tridactyl. highlights: 1. 纠正了若干 vimium 的错误的 key bindings: 1. vimium <S-^> 是在 latest tab 之间跳转, vim 里明明是 <C-^>, tridactyl 把这个纠正过来了 2. vimium u / d 的功能是 vim 里的 <C-u> / <C-d> 翻页, tridactyle u 是恢复最近关闭的 tab, d 是关闭当前 tab 2. 可以在输入框里 edit in vim! 1. gi 选输入框 2. <C-i> 打开 tridactyl native messenger, 需要配置系统的默认 editor 是 MacVim (如果你是 macOS 的话) 3. 在弹出来 MacVim 里编辑, :x 保存退出, 文本就到浏览器输入框了 3. 新增了很多其他操作 1. b 搜索当前所有 tab, 比 vimium 的 tab 跳转好用 2. t open url in a new tab, 可以很方便地搜索历史, 比 vimium 的 O 好用 3. ;p 复制 element's text, 好用到哭, 比 vimium 的 visual mode 高到不知哪里去 4. gF 连开 links 模式, vimium 我只能反复 F from https://gist.github.com/jschwinger23/8c9f1427ad13b0db301a86b28527b560#file-intro-tridactyl-md
Posted Apr 2
友链: https://t.me/ManjusakaH