2023 的一些总结

2023 的一些总结

李宗盛在山丘开头里面写道,"想说却还没说的 还很多 攒着是因为想写成歌"。

同样的,在印象中在 2023 好像做了很多东西,接触了很多技术,但是一直没有整理,攒着攒着就到年底了。

细细思考了一下,有两个板块是今年主要发力的点

  1. 对网络的探索
  2. 对业务代码优化的思考

对网络的探索

整个过程围绕着一个主题 "如何在一个机器内构建一个虚拟 VXLAN/VLAN 网络"。

netns

netns 是其中最关键的技术,使用非常简单,主要用来解决隔离问题

  1. VXLAN 的不同 VNI 的相同 IP 不能够进行区分,只能够使用 netns 进行隔离
  2. rp_filter/路由之类问题,很容易就造成丢包

如果网络被隔离了,意味被模块化分,管理起来也更简单,排查问题也更简单

  1. tcpdump 在 netns 内只能看到进入 netns 的流量,减少和各种流量斗智斗勇的时间
  2. iptables/ipset 在 4.x 以上的内核版本也完整支持(ipset 在 3.10 内核未进行隔离)

再结合 veth-peer/bridge,将 物理网卡(也可以是其它虚拟网卡) 和 veth-peer 同时挂在 bridge 上,这样 netns 和外界网络就连接起来了。

如果有两个物理网卡,分别挂在不同网桥上,并且物理网卡都没有配置 IP 只在连接的 netns 内配置 IP,那么这两个物理网卡就被完全隔离了,对于一个机器连接不同的物理网络是一个非常好的方案。

延伸

如何在所有的 netns 内监听端口

  1. 最简单粗暴,在 netns 内启动一个进程监听端口,再把流量通过 UNIX SOCKET 代理出来
  2. 将线程切换至 netns 内,然后进行端口监听,线程的消耗总是要比进程要小的,至少启动速度是要快的,那就保持数量比 netns:thread = 1:1
  3. 只绑定 sockfd 至 netns 中,常数个线程来监听所有 netns 内的端口

方案3是前两天才发现的,在 stackoverflow 上看到有人 setns 后直接 listen,然后继续 setne listen,这样只用了一个线程同时在两个命名空间内进行监听,在本地用 python 代码验证的确如此;方案二中的一个线程的内存占用大概在 2M 左右,并且在数量多的情况下退出也比较费时,方案3胜出

有两个场景可以使用这个方案

  1. 在虚拟网络的前提下,虚拟端口进行欺骗访问,类似 portspoof
  2. 对容器流量进行劫持分析,在容器内使用 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 处理有几个好处

  1. 传递value
  2. 传递取消,context 可以取消所有的子树 context
  3. 可以重复 cancel,可以在一些具有主动暂停和执行完成的地方,代码写起来简单一些

退出的逻辑应该是单独的一个 goroutine 中,等待接受信号或者其它逻辑,一旦满足条件,cancel 启动逻辑的 context,然后所有的处理模块自然退出。

尽量不要出现在退出函数里面等待运行函数中关闭/写入 chan,因为一旦运行函数失败了,外部没有判断错误的情况(很常见,因为有一些代码不影响运行),chan 不会再被写入/关闭,那么久死锁了。

同理,命令的执行尽量使用封装 exec.CommandContext 进行使用,有一些命令运行时间很长,那么可以通过 context 来取消执行。

可调试性工具

将内部的一些状态通过网络进行 request/response 输出,这样一些排查问题时需要的数据直接通过请求就能拿到,不再需要去打日志,适用场景

  1. 热点路径的统计数据
  2. 内存中的动态数据,规模较大,不适直接在日志中输出

将服务的功能命令行化,不再依赖其它服务的数据输入,总体也能提高服务的灵活性

并发控制

通过 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

  1. 对于构建的指令,依赖都有备份
  2. 防止构建机器被污染

捐赠了一波博客园

既然和技术相关,那么我觉得这个也算

总结

成长性主要集中在网络的方面,由于和网络打交道,C++ 也涉及了一些,但是无论是语言还是网络技术,总体还是比较肤浅,如果不总结一下是感受不出来的。

对一些源码层面的东西看到少一些,更多的关注了怎么使用,怎么好用,怎么好维护的场景;今年技术迷惘的时间比较少,利用业务驱动了更多技术发展。

缺乏一些交流。

收个尾

  • C++ 的 logrus 库完善一下,写一个文章
  • Golang 的一些工程化优化,也梳理一下
  • libevent xmake 等东西也放一下出来,给互联网一些原创的东西

规划一下

  • netns 这种东西应该继续往下再深挖一下,阅读一下源码,看看如果结合 ebpf 能够怎么进行优化
  • 提升一下行动力,争取两个月有一篇文章产出,临时的想法也行