to inspire confidence in somebody.

0%

最近往K8s集群中添加节点的时候,发现部分节点的kubelet进程无法启动,导致节点处于NotReady状态。journalctl -u kubelet查看日志可以发现类似的日志:

1
....
2
Nov 29 23:32:13 localhost kubelet[3830]: I1129 23:32:13.311881    3830 server.go:333] Adding debug handlers to kubelet server.
3
Nov 29 23:32:13 localhost kubelet[3830]: W1129 23:32:13.311922    3830 cni.go:203] Unable to update cni config: No networks found in /etc/cni/net.d
4
Nov 29 23:32:13 localhost kubelet[3830]: E1129 23:32:13.312092    3830 kubelet.go:2192] Container runtime network not ready: NetworkReady=false reason:NetworkPluginNotReady message:docker: network plugin is not ready: cni config uninitialized
5
Nov 29 23:32:13 localhost kubelet[3830]: I1129 23:32:13.382130    3830 kubelet_node_status.go:278] Setting node annotation to enable volume controller attach/detach
6
Nov 29 23:32:13 localhost kubelet[3830]: I1129 23:32:13.383695    3830 cpu_manager.go:155] [cpumanager] starting with none policy
7
Nov 29 23:32:13 localhost kubelet[3830]: I1129 23:32:13.383705    3830 cpu_manager.go:156] [cpumanager] reconciling every 10s
8
Nov 29 23:32:13 localhost kubelet[3830]: I1129 23:32:13.383713    3830 policy_none.go:42] [cpumanager] none policy: Start
9
Nov 29 23:32:13 localhost kubelet[3830]: F1129 23:32:13.384272    3830 kubelet.go:1384] Failed to start ContainerManager failed to initialize top level QOS containers: failed to update top level BestEffort QOS cgroup : failed to set supported cgroup subsystems for cgroup [kubepods besteffort]: Failed to set config for supported subsystems : failed to write 4611686018427387904 to hugetlb.1GB.limit_in_bytes: open /sys/fs/cgroup/hugetlb/kubepods.slice/kubepods-besteffort.slice/hugetlb.1GB.limit_in_bytes: no such file or directory
10
Nov 29 23:32:13 localhost systemd[1]: kubelet.service: main process exited, code=exited, status=255/n/a
阅读全文 »

最近一段时间在折腾虚拟化,想把SR-IOV硬件直通给用起来,所以免不了要利用lspci这个工具,用来查看当前系统连接的所有PCI/PCIe设备。其实之前也有用到过,也有一些不理解的地方,只是当时无脑跟着文档设置,也就没多关心了,这次需要好好的理解一下相关的概念什么的。

首先很简单,看看不加参数直接调用lspci命令的输出结果,下面的是我笔记本上的输出:

1
00:00.0 Host bridge: Intel Corporation Broadwell-U Host Bridge -OPI (rev 09)
2
00:02.0 VGA compatible controller: Intel Corporation HD Graphics 5500 (rev 09)
3
00:03.0 Audio device: Intel Corporation Broadwell-U Audio Controller (rev 09)
4
00:04.0 Signal processing controller: Intel Corporation Broadwell-U Processor Thermal Subsystem (rev 09)
5
00:14.0 USB controller: Intel Corporation Wildcat Point-LP USB xHCI Controller (rev 03)
6
00:16.0 Communication controller: Intel Corporation Wildcat Point-LP MEI Controller #1 (rev 03)
7
00:19.0 Ethernet controller: Intel Corporation Ethernet Connection (3) I218-LM (rev 03)
8
00:1b.0 Audio device: Intel Corporation Wildcat Point-LP High Definition Audio Controller (rev 03)
9
00:1c.0 PCI bridge: Intel Corporation Wildcat Point-LP PCI Express Root Port #1 (rev e3)
10
00:1c.3 PCI bridge: Intel Corporation Wildcat Point-LP PCI Express Root Port #4 (rev e3)
11
00:1c.4 PCI bridge: Intel Corporation Wildcat Point-LP PCI Express Root Port #5 (rev e3)
12
00:1d.0 USB controller: Intel Corporation Wildcat Point-LP USB EHCI Controller (rev 03)
13
00:1f.0 ISA bridge: Intel Corporation Wildcat Point-LP LPC Controller (rev 03)
14
00:1f.2 SATA controller: Intel Corporation Wildcat Point-LP SATA Controller [AHCI Mode] (rev 03)
15
00:1f.3 SMBus: Intel Corporation Wildcat Point-LP SMBus Controller (rev 03)
16
01:00.0 SD Host controller: O2 Micro, Inc. SD/MMC Card Reader Controller (rev 01)
17
02:00.0 Network controller: Intel Corporation Wireless 7265 (rev 59)
阅读全文 »

之前线上运行的K8S集群出现了一个Pod IP无法访问问题,调查了一下,发现和CalicoIP地址的分配策略相关,具体表现为一个/26的IP Block192.168.100.0/26分配给了A机器之后,在另外一台B机器上又出现了该IP Block内的一个IP 192.168.100.10,同时因为A机器上有该IP Block的blackhole路由blackhole 192.168.100.0/26 proto bird,所以导致A机器上所有的Pod访问192.168.100.10时因为黑洞路由原因直接失败。

遇到这个问题之前,只是通过文档大致了解Calico的IP分配策略,没有深入源码看看实际的情况,现在出现了相关问题,还是需要阅读一下相关代码,当然在这过程中也发现了一些问题,有些问题Calico官方也没有很好的解决。

阅读全文 »

前段时间将家里的树莓派3B系统换成了ArchLinux Arm,这样就可以用上64位指令集,不过遇到了一个问题,在没有连接HDMI时系统无法启动,刚开始还以为是什么其他的原因,因为之前接上显示器开机一切正常,但是换个地方不接显示器就启动不了,后来把显示器搬到旁边,一插上HDMI,立马就进入启动了。

定位到和HDMI相关之后,搜索了一下,刚开始以为和系统有关,后来搜到了Won’t boot without HDMI connected (solved)这个帖子,发现不是系统问题,是个简单的配置问题,很简单,在/boot/config.txt中,加入一行hdmi_force_hotplug=1就可以了,直接重启,没有接显示器也可以正常启动了。

这个配置信息在Video options in config.txt有详细的解释,意思是强制认为HDMI已经连接,直接开启HDMI输出。

阅读全文 »

今天在我们的环境中遇到了一个比较诡异的问题,我们在一台虚拟机上想要挂载一个CephFS,但是出现了一个failed: No such process的诡异问题,具体表现如下:

1
]# mount -t ceph mon1.ichenfu.com:6789,mon2.ichenfu.com:6789,mon3.ichenfu.com:6789:/ /tmp/data
2
mount: mount mon1.ichenfu.com:6789,mon2.ichenfu.com:6789,mon3.ichenfu.com:6789:/ on /tmp/data failed: No such process
阅读全文 »

在Libvirt里,一个Domain是一个运行在虚拟机器上的操作系统的实例,它可以指一个运行着的虚拟机,或者用于启动虚拟机的配置。
那么,对于一个Domain而言,主要有哪些状态呢?,他们的转换关系是什么?可以参考文档VM lifecycle。其中,状态主要包括以下:

阅读全文 »

随着Git仓库不断的被修改,整个仓库会变得越来越大,其中最主要的原因是历史提交特别的多,这个对于想立即阅读最新代码或者CI/CD场景下不是特别友好。

面对这种场景,可以利用git提供的浅克隆功能,只clone少部分历史到本地,这样可以极大的减少clone的仓库大小,以PHP源代码代码为例:

阅读全文 »

在Linux下,可以对进程使用的资源做一些限制,比如,可以使用的内存、可以使用的线程、最大能打开的文件数等等,这些也就是我们常说的rlimit,在bash里,可以非常方便的用ulimit这个内置的命令查看和修改这些限制,那么到底这些限制有那些,是怎么来的呢?

首先,在C编程环境下,系统提供了三个接口:int getrlimit(int resource, struct rlimit *rlim);int setrlimit(int resource, const struct rlimit *rlim);int prlimit(pid_t pid, int resource, const struct rlimit *new_limit, struct rlimit *old_limit);分别用来获取当前进程的限制、设置当前进程的限制以及根据Pid设置对应进程的限制。

那么具体有哪些限制,也就是接口中的resource参数有哪些,可以参考man里的信息,这里大致翻译一下:

阅读全文 »

工作中如果使用Git作为版本管理工具的话,应该经常会遇到因为各种原因一下提交了很多个commit的情况,比如添加一个功能,测试出问题继续commit修改,最后git log看提交历史就会变成这样:

1
# git log
2
commit 9995aafb7a597d9a7fcf9a341a731324813c5aad (HEAD -> master)
3
...
4
    Commit 4
5
6
commit 54062e7317fa19a228d8f4f63236467317c17672
7
...
8
    Commit 3
9
10
commit 1571ee6b861315ec46875fbececd46c9daaa5d04
11
...
12
    Commit 2
13
14
commit ae95aac116af934742e1dd2eca435a0d6e70b77f
15
...
16
    Commit 1
17
18
commit 3f0373c3afb9e9ffd6174b8244ec3e936d3583e0
19
...
20
    init

这样看起来不那么美观,也会一定程度上污染主分支,如果遇到问题,需要看diff的时候,也会不那么方便,所以介绍一个利用git rebase命令合并一系列commit的方法。

阅读全文 »

一般情况下,如果一个程序需要使用代理服务器,那么需要在运行的时候设置一下参数,或者,在Linux下,大部分的程序支持http_proxy这个环境变量,设置这个环境变量,意味着程序将使用设置值作为代理。
这样的问题在于,设置代理这个操作是不透明的,也就是说,客户端必须要知道代理的存在,需要手动设置将流量导入到代理,如果程序本身不支持代理,或者我们不希望执行所有程序的时候都手动设置代理,那么就需要一个相对“透明”的代理办法了。

同样的,作为ServiceMesh界的当红实现Istio,也会遇到类似的问题,如何在程序完全没有感知的情况下悄无声息的将程序的流量劫持到自己的代理呢?

借助Istio的两种实现方式,也说一下目前Liunx下两种透明代理的实现。

REDIRECT

首先是使用iptables的REDIRECT模式,通过iptables,可以将所有的流量都重定向到一个特定的端口上,如果配置过ss-redir的话,应该会对这种实现非常的熟悉,具体的,在iptables里对应一条规则:

1
iptables -t nat -A PREROUTING -p tcp -j REDIRECT --to-port 5000

即将所有流量都重定向到5000端口,仔细看一下,是不是和iptables实现DNAT有点相似?没错!本质上REDIRECT就是一个特殊的DNAT规则,一般情况下,我们利用iptables做DNAT的时候,需要指定目标的IP和端口,这样iptables才能知道需要将数据包的目的修改成什么,而REDIRECT模式下,只需要指定端口就可以,iptables会自动帮我们判断需要设置的IP地址。

继续思考,会发现另一个问题,那就是,既然是做了DNAT,也就意味着数据包里已经没有原始的目的地址了,那数据包到了代理程序,代理程序是如何知道这个数据包应该往什么地方发送呢?这个是个非常核心的问题,因为如果不知道原始的目的IP端口信息,代理完全不能起作用啊!

当然问题是有办法的,conntrack在这时候起作用了,conntrack会记录原始的地址,而在用户侧,内核提供了一个接口,可以让应用程序获取到原始的IP端口,可以参考一下ss-redir的实现:

1
static int
2
getdestaddr(int fd, struct sockaddr_storage *destaddr)
3
{
4
    socklen_t socklen = sizeof(*destaddr);
5
    int error         = 0;
6
7
    error = getsockopt(fd, SOL_IPV6, IP6T_SO_ORIGINAL_DST, destaddr, &socklen);
8
    if (error) { // Didn't find a proper way to detect IP version.
9
        error = getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, destaddr, &socklen);
10
        if (error) {
11
            return -1;
12
        }
13
    }
14
    return 0;
15
}

利用getsockoptSO_ORIGINAL_DST参数,可以获取到原始的连接IP和端口,好了,目前代理所需要的所有的信息都完整了,整个代理理论上就可以工作了,剩下的就是代理如何实现的问题了,这里就不探讨了。

TPROXY

除了利用REDIRECT模式,Istio还提供TPROXY模式,当然也是借助Linux内核提供的功能实现的,对于TPROXY模式,实现的原理要相对复杂不少,需要借助iptables和路由:

1
iptables -t mangle -A PREROUTING -p tcp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 8888
2
ip rule add fwmark 0x1/0x1 pref 100 table 100
3
ip route add local default dev lo table 100

通过iptables将数据包打上mark,然后使用一个特殊的路由,将数据包指向本地,由于使用了mangle表,所以数据包的原始和目的地址都是不会被修改的。

那么问题来了,应用程序怎么编写?假如需要连接1.2.3.4:80这个端口,就算数据包到了本地,但是本地并没有1.2.3.4这个IP地址啊,程序是怎么能拿到数据的?不是应该直接丢弃这个数据包么?
针对这个问题,可以看一个例子tproxy-example,这个例子实现了一个简单的基于TPROXY的代理。

针对上面的情况,Linux提供了一个选项IP_TRANSPARENT,这个选项很神奇,可以让程序bind一个不属于本机的地址,作为客户端,它可以使用一个不属于本机地址的IP地址作为源IP发起连接,作为服务端,它可以侦听在一个不属于本机的IP地址上,而这正是透明代理所必须的。我们看下例子程序里的代码:

1
if((listen_fd = socket(res->ai_family, res->ai_socktype,
2
                res->ai_protocol)) == -1){
3
    perror("socket: ");
4
    exit(EXIT_FAILURE);
5
}
6
7
if(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes))
8
        == -1){
9
    perror("setsockopt (SO_REUSEADDR): ");
10
    close(listen_fd);
11
    exit(EXIT_FAILURE);
12
}
13
14
//Mark that this socket can be used for transparent proxying
15
//This allows the socket to accept connections for non-local IPs
16
if(setsockopt(listen_fd, SOL_IP, IP_TRANSPARENT, &yes, sizeof(yes))
17
        == -1){
18
    perror("setsockopt (IP_TRANSPARENT): ");
19
    close(listen_fd);
20
    exit(EXIT_FAILURE);
21
}
22
23
if(bind(listen_fd, res->ai_addr, res->ai_addrlen) == -1){
24
    perror("bind: ");
25
    close(listen_fd);
26
    exit(EXIT_FAILURE);
27
}
28
29
if(listen(listen_fd, BACKLOG) == -1){
30
    perror("listen: ");
31
    close(listen_fd);
32
    exit(EXIT_FAILURE);
33
}

也确实是设置了IP_TRANSPARENT,有了这个选项,也就意味着代理绑定了所有的IP,当然1.2.3.4这个IP也在范围内,所以可以正常的接受连接。
而由于TPROXY模式并没有改变数据包,所以直接通过getsockname获取到原始的IP端口信息:

1
//Store the original destination address in remote_addr
2
//Return 0 on success, <0 on failure
3
static int get_org_dstaddr(int sockfd, struct sockaddr_storage *orig_dst){
4
    char orig_dst_str[INET6_ADDRSTRLEN];
5
    socklen_t addrlen = sizeof(*orig_dst);
6
7
    memset(orig_dst, 0, addrlen);
8
9
    //For UDP transparent proxying:
10
    //Set IP_RECVORIGDSTADDR socket option for getting the original
11
    //destination of a datagram
12
13
    //Socket is bound to original destination
14
    if(getsockname(sockfd, (struct sockaddr*) orig_dst, &addrlen)
15
            < 0){
16
        perror("getsockname: ");
17
        return -1;
18
    } else {
19
        if(orig_dst->ss_family == AF_INET){
20
            inet_ntop(AF_INET,
21
                    &(((struct sockaddr_in*) orig_dst)->sin_addr),
22
                    orig_dst_str, INET_ADDRSTRLEN);
23
            fprintf(stderr, "Original destination %s\n", orig_dst_str);
24
        } else if(orig_dst->ss_family == AF_INET6){
25
            inet_ntop(AF_INET6,
26
                    &(((struct sockaddr_in6*) orig_dst)->sin6_addr),
27
                    orig_dst_str, INET6_ADDRSTRLEN);
28
            fprintf(stderr, "Original destination %s\n", orig_dst_str);
29
        }
30
31
        return 0;
32
    }
33
}

总结

上面就是两种Linux下实现透明代理的方式,透过现象看本质,无论实现方式是什么,其实都定位到一个核心问题,即在没有代理的情况下,连接的五元组是什么?数据包最核心的源地址源端口,目的地址目的端口,无论是通过NAT方式修改数据包重定向,或者借助内核的一些特殊特性,都必须要知道这4个关键信息,一旦搞清楚这些,那理论上代理就能工作了,剩下的就是如何将代理本身做好,那就是一个业务逻辑的问题了。

参考:

  1. https://serverfault.com/questions/179200/difference-beetween-dnat-and-redirect-in-iptables
  2. https://vvl.me/2018/06/09/from-ss-redir-to-linux-nat/
  3. https://blog.csdn.net/dog250/article/details/13161945
  4. https://www.kernel.org/doc/Documentation/networking/tproxy.txt