2023 的一些总结
李宗盛在山丘开头里面写道,"想说却还没说的 还很多 攒着是因为想写成歌"。
同样的,在印象中在 2023 好像做了很多东西,接触了很多技术,但是一直没有整理,攒着攒着就到年底了。
细细思考了一下,有两个板块是今年主要发力的点
- 对网络的探索
- 对业务代码优化的思考
对网络的探索
整个过程围绕着一个主题 "如何在一个机器内构建一个虚拟 VXLAN/VLAN 网络"。
netns
netns 是其中最关键的技术,使用非常简单,主要用来解决隔离问题
- VXLAN 的不同 VNI 的相同 IP 不能够进行区分,只能够使用 netns 进行隔离
- rp_filter/路由之类问题,很容易就造成丢包
如果网络被隔离了,意味被模块化分,管理起来也更简单,排查问题也更简单
- tcpdump 在 netns 内只能看到进入 netns 的流量,减少和各种流量斗智斗勇的时间
- iptables/ipset 在 4.x 以上的内核版本也完整支持(ipset 在 3.10 内核未进行隔离)
再结合 veth-peer/bridge,将 物理网卡(也可以是其它虚拟网卡) 和 veth-peer 同时挂在 bridge 上,这样 netns 和外界网络就连接起来了。
如果有两个物理网卡,分别挂在不同网桥上,并且物理网卡都没有配置 IP 只在连接的 netns 内配置 IP,那么这两个物理网卡就被完全隔离了,对于一个机器连接不同的物理网络是一个非常好的方案。
延伸
如何在所有的 netns 内监听端口
- 最简单粗暴,在 netns 内启动一个进程监听端口,再把流量通过 UNIX SOCKET 代理出来
- 将线程切换至 netns 内,然后进行端口监听,线程的消耗总是要比进程要小的,至少启动速度是要快的,那就保持数量比 netns:thread = 1:1
- 只绑定 sockfd 至 netns 中,常数个线程来监听所有 netns 内的端口
方案3是前两天才发现的,在 stackoverflow 上看到有人 setns 后直接 listen,然后继续 setne listen,这样只用了一个线程同时在两个命名空间内进行监听,在本地用 python 代码验证的确如此;方案二中的一个线程的内存占用大概在 2M 左右,并且在数量多的情况下退出也比较费时,方案3胜出
有两个场景可以使用这个方案
- 在虚拟网络的前提下,虚拟端口进行欺骗访问,类似 portspoof
- 对容器流量进行劫持分析,在容器内使用 iptables nat 将符合条件的流量代理到固定端口中,在服务内进行分析完成后再代理回原有的端口上
修改 flannel
年初有一个需求,跨设备使用 VXLAN 打通虚拟机(只有内部 IP)网络,当时对 flannel 进行修改,不再使用 etcd 转而对接内部的服务。
当时还有另外一种方案是使用 主机路由,同样 flannel 也支持,不过想到如果后面还要打通虚拟机内部的容器网络,那么使用双层 VXLAN 就是一种非常自然的手段了。
有一点遗憾的是,其实 flannel 完全可以不修改,而是直接拿 backend 的 vxlan 代码合并至内部服务内,从软件工程的角度 "非必要不增加实体",这也就成为了一部分技术债了。
选择 frr
在 flannel 之前 VXLAN 的网络打通都是基于点对点的方案,不能动态的管理寻址信息,不过 flannel 同样不能应用在这里,因为数据中心的 VXLAN 网络并不是 etcd 为中心,数据中心通用使用 BGP 协议来进行路由协议的交互,故 frr 被选中。
不使用 frr 的情况下,写入 FDB 也可以达到一个动态路由的过程的,当然写入 FDB 这个过程就不动态(适用于替换 flannel)
构建网络场景
对于 VXLAN 来说,将 VTEP 和 veth-peer 挂在 bridge 上,veth-peer 另外一段在命名空间内
对于 VLAN 来说,直接将 物理网卡 和 veth-peer 挂在 bridge 上,veth-peer 另一段在命名空间内,命名空间内的默认路由自定义配置就行
对业务代码优化的思考
日志
第一个就是日志,非常认同戈君对日志的打印的看法,使用结构化的日志,在 Go 里面使用 logrus 即可,不必在乎性能,真有性能瓶颈再考虑日志打印的代码是否需要调整。
对于 logrus 所有的变量只应该出现在 logrus.WithFied(s) 中,logrus.Info 一定是字面量的字符串。
日志尽量不要转移字符串,不然像一些 json 的字符串,复制出来没办法直接格式化。
对于一些命令执行的逻辑,要提供日志输出的 Option(Option 的设计可以单独展开),比如 debug 打开,不然有一些命令执行错了就陷入一个抓瞎的过程,排查很浪费时间。
启动/停止的关键路径应该有日志,整个服务的运作路径可以清晰通过日志了解,并且这部分日志多一些也不会有问题,毕竟只会执行一次。
必要时,可以日志可以对业务进行分日志文件
控制流
启动/停止
对于一个后台服务来说,启动逻辑应该被阻塞,如同 http.Serve,优先传递 context 而不是 chan,
context 处理有几个好处
- 传递value
- 传递取消,context 可以取消所有的子树 context
- 可以重复 cancel,可以在一些具有主动暂停和执行完成的地方,代码写起来简单一些
退出的逻辑应该是单独的一个 goroutine 中,等待接受信号或者其它逻辑,一旦满足条件,cancel 启动逻辑的 context,然后所有的处理模块自然退出。
尽量不要出现在退出函数里面等待运行函数中关闭/写入 chan,因为一旦运行函数失败了,外部没有判断错误的情况(很常见,因为有一些代码不影响运行),chan 不会再被写入/关闭,那么久死锁了。
同理,命令的执行尽量使用封装 exec.CommandContext 进行使用,有一些命令运行时间很长,那么可以通过 context 来取消执行。
可调试性工具
将内部的一些状态通过网络进行 request/response 输出,这样一些排查问题时需要的数据直接通过请求就能拿到,不再需要去打日志,适用场景
- 热点路径的统计数据
- 内存中的动态数据,规模较大,不适直接在日志中输出
将服务的功能命令行化,不再依赖其它服务的数据输入,总体也能提高服务的灵活性
并发控制
通过 chan 来完成 goroutine 间的数据及控制传递,由生产端gorouting关闭 chan,消费端goroutine 对 chan 进行 for-range 阻塞,这样单个 chan 就可以同时起到既传输数据和控制 goroutine 的作用。
整个生产-消费的过程一定是生产端 goroutine 关闭 chan,然后消费端 goroutine 自然退出。
同样可以使用 goroutine+chan 的组合来控制生产端的 goroutine 数量,一个比较简单的做法就是保持 chan 的大小和生产端的 goroutine 的数量相等
其它
探索 C++ 编译期构造字符串的方案
想像 Go 的 logrus 那样结构化输出日志,又不想损失性能;最后基于 C++17 可以达到 logrus.WithFields(logrus.WithField{"k", v}, logrus.WithField{"k2", v2})::Info 的效果;
了解如何使用 libevent
对于库中间碰到一个坑,libevent 每次从 fd 中读取字节数最大为 4096 字节,然后网上的人都是把 4096 调大再编译使用;
其实合理的方式为增加一个数据包头,来存当前数据包的大小,结合 watermask 使用即可。
了解 cmake/xmake 的使用
xmake 使用还比较简单,挺好用的,但是在公司推广不开,而且有一个问题在构建 libmnl 的时候,会找不到库,问题只在已经安装过 libmnl-dev 的环境中出现。
后面转而使用到 cmake,结合 add_subdirectory 和 ExternalProject_Add 对三方库进行混合构建。
基于 Dockerfile 的构建环境
对于一些修改过源码的三方代码,不应该在普通的服务器之内进行编译,而基于 Dockerfile
- 对于构建的指令,依赖都有备份
- 防止构建机器被污染
捐赠了一波博客园
既然和技术相关,那么我觉得这个也算
总结
成长性主要集中在网络的方面,由于和网络打交道,C++ 也涉及了一些,但是无论是语言还是网络技术,总体还是比较肤浅,如果不总结一下是感受不出来的。
对一些源码层面的东西看到少一些,更多的关注了怎么使用,怎么好用,怎么好维护的场景;今年技术迷惘的时间比较少,利用业务驱动了更多技术发展。
缺乏一些交流。
收个尾
- C++ 的 logrus 库完善一下,写一个文章
- Golang 的一些工程化优化,也梳理一下
- libevent xmake 等东西也放一下出来,给互联网一些原创的东西
规划一下
- netns 这种东西应该继续往下再深挖一下,阅读一下源码,看看如果结合 ebpf 能够怎么进行优化
- 提升一下行动力,争取两个月有一篇文章产出,临时的想法也行