分类 云原生 下的文章

实操技巧

进入容器网络ns

kubectl get pods xxx -o wide获取node名
登录node
获取容器进程id:
docker:
docker ps| grep $pod
docker inspect -f {{.State.Pid}} 容器id
containerd:
crictl ps | grep podname
crictl inspect 容器id | jq '.info.pid'
进入容器的网络命名空间
nsenter --target pid -n

筛选抓取

netstat -ant查看当前开放的端口
https://www.baeldung.com/linux/tcpdump-capture-ssl-handshake

偶发性问题抓包:循环抓包
指定抓包文件数,按单个文件的大小或者抓取的时长进行切割,超过指定生成的文件数之后循环覆盖旧文件
示例:
-W 个数:生成的循环文件数量,生成到最大数量后,新的报文数据会覆盖写入第一个文件
-C 尺寸:每个文件的大小,单位是 MB
-G 间隔时间:每次新生成文件的间隔时间,单位是分钟
每 100MB 或者 60 分钟就生成一个文件,一共 10 个文件
tcpdump -i eth0 -w file.pcap -W 10 -C 100 -G 60

wireshark筛选:
ip筛选 ip.addr ip.src ip.dst eq xxx
日期筛选 frame.time > "feb 01, 2024" and frame.time < "mar 01, 2024 00:00:00"
标志位筛选 tcp.flags.rst eq 1
长度筛选 tcp.len eq xx
报文模糊筛选 tcp.payload contains xxx

k8s网络

登录到节点,lsns -t net可以查看到当前节点上的网络命名空间,其中有一些看到是pause,这些是容器的sandbox,k8s使用sandbox容器来创建和维持命名空间,他也是进程空间中的pid 1

lsns -t net
        NS TYPE NPROCS     PID USER     NETNSID NSFS                                                COMMAND
4026531992 net     131       1 root  unassigned                                                     /sbin/init
4026532308 net       2    2451 65535          0 /run/netns/cni-d612c07a-e8bf-969a-a2c5-2392a222533e /pause
4026532382 net       4    2622 65535          1 /run/netns/cni-c122169f-f7f0-03b3-1aa0-7ba3a07a3317 /pause
4026532447 net       2    2722 65535          2 /run/netns/cni-723e1b9c-0355-15e1-a4a0-f7926de9c3b2 /pause
4026532515 net       1 2797456 uuidd unassigned                                                     /usr/sbin/uuidd -
4026532575 net       5 2080352 65535          3 /run/netns/cni-6c23adba-e86b-87c1-bd3a-b7be2b18fda9 /pause

获取到pid后也可以直接使用ip来获取和进入对应的命名空间

ip netns identify 2080411
cni-6c23adba-e86b-87c1-bd3a-b7be2b18fda9
ip netns exec cni-6c23adba-e86b-87c1-bd3a-b7be2b18fda9 ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue state UP group default
    link/ether b6:a4:e2:bc:5a:10 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.244.0.9/24 brd 10.244.0.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::b4a4:e2ff:febc:5a10/64 scope link
       valid_lft forever preferred_lft forever

同理可以查看其他命名空间,如下例

crictl ps | grep pod-1
8453bb79c5a3f       92b11f67642b6       3 hours ago         Running             nginx                     0                   576ce2a9d00c7       pod-1
crictl inspect 8453bb79c5a3f  | jq '.info.pid'
2080411
lsns -t pid | grep 2080411
4026532639 pid       4 2080411 root  nginx: master process nginx -g daemon off;

从上面的信息我们可以看到,容器的虚拟网卡为eth0,被链接在9号接口上,ip为10.244.0.9
我们来看下这个虚拟网卡连在哪

ip link | grep -A1 ^9
9: vethed920513@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 qdisc noqueue master cni0 state UP mode DEFAULT group default
    link/ether be:99:98:76:2a:42 brd ff:ff:ff:ff:ff:ff link-netns cni-6c23adba-e86b-87c1-bd3a-b7be2b18fda9

可以看到,这块网卡被连在了网桥cni0上面
网桥工作在数据链路层,可以连接多个网段,当有请求进来时,网桥会去所有接口询问是否有认识原始ip的,如果有,发送到那个veth,然后veth发给eth0(容器默认虚拟网卡一般都是eth0)
网卡和veth绑定关系还可以使用brctl来查看(需要安装

brctl show
bridge name     bridge id               STP enabled     interfaces
cni0            8000.e6f9e02baef7       no              veth8d55a711
                                                        veth92b8f630
                                                        vethcbd8b3fe
                                                        vethed920513

关于网络容器接口CNI可以看这篇介绍文章
https://atbug.com/deep-dive-cni-spec/

同node通信:
pod从自己的eth0发出请求,到达veth,veth通过网桥cni0和其他veth相连,cni0问所有接口认不认识ip,如果有认识的,记录映射关系并转发到该veth,veth再把信息发给eth0

跨node通信:
跨node通信的方式取决于使用的网络插件

额外篇:关于创建VPC和集群时需要考虑的网络问题
https://help.aliyun.com/zh/ack/ack-managed-and-ack-dedicated/user-guide/plan-cidr-blocks-for-an-ack-cluster-2#2e587ee4f46oq

flannel

flannel在安装的时候就会指定CIDR以及和后端类型,可以查看某个node具体所属的网段

kubectl get no nodename -o jsonpath={.spec} | jq
{
  "podCIDR": "10.244.0.0/24",
  "podCIDRs": [
    "10.244.0.0/24"
  ]
}

flannel接收cni-conf.json,输出结果并交给bridge

cat /var/lib/cni/flannel/2fc679ceaa17e33115657688a9b859e89a66c8911bcd9134fad2327830519396 | jq
{
  "cniVersion": "0.3.1",
  "hairpinMode": true,
  "ipMasq": false,
  "ipam": {
    "ranges": [
      [
        {
          "subnet": "10.244.0.0/24"
        }
      ]
    ],
    "routes": [
      {
        "dst": "10.244.0.0/16"
      }
    ],
    "type": "host-local"
  },
  "isDefaultGateway": true,
  "isGateway": true,
  "mtu": 1450,
  "name": "cbr0",
  "type": "bridge"
}

bridge 使用上面的输出连同参数一起作为输入,根据配置完成如下操作:

  1. 创建网桥 cni0(节点的根网络命名空间)
  2. 创建容器网络接口 eth0( pod 网络命名空间)
  3. 创建主机上的虚拟网络接口 vethX(节点的根网络命名空间)
  4. 将 vethX 连接到网桥 cni0
  5. 委托 ipam 插件分配 IP 地址、DNS、路由
  6. 将 IP 地址绑定到 pod 网络命名空间的接口 eth0 上
  7. 检查网桥状态
  8. 设置路由
  9. 设置 DNS

    cat /var/lib/cni/results/cbr0-2fc679ceaa17e33115657688a9b859e89a66c8911bcd9134fad2327830519396-eth0 | jq
    {
      "kind": "cniCacheV1",
      "containerId": "2fc679ceaa17e33115657688a9b859e89a66c8911bcd9134fad2327830519396",
      "config": "ewogICJuYW1lIjogImNicjAiLAogICJjbmlWZXJzaW9uIjogIjAuMy4xIiwKICAicGx1Z2lucyI6IFsKICAgIHsKICAgICAgInR5cGUiOiAiZmxhbm5lbCIsCiAgICAgICJkZWxlZ2F0ZSI6IHsKICAgICAgICAiaGFpcnBpbk1vZGUiOiB0cnVlLAogICAgICAgICJpc0RlZmF1bHRHYXRld2F5IjogdHJ1ZQogICAgICB9CiAgICB9LAogICAgewogICAgICAidHlwZSI6ICJwb3J0bWFwIiwKICAgICAgImNhcGFiaWxpdGllcyI6IHsKICAgICAgICAicG9ydE1hcHBpbmdzIjogdHJ1ZQogICAgICB9CiAgICB9CiAgXQp9Cg==",
      "ifName": "eth0",
      "networkName": "cbr0",
      "cniArgs": [
     [
       "K8S_POD_INFRA_CONTAINER_ID",
       "2fc679ceaa17e33115657688a9b859e89a66c8911bcd9134fad2327830519396"
     ],
     [
       "K8S_POD_UID",
       "2ab4ed63-1aff-40fc-9e40-ab95e0709cc1"
     ],
     [
       "IgnoreUnknown",
       "1"
     ],
     [
       "K8S_POD_NAMESPACE",
       "default"
     ],
     [
       "K8S_POD_NAME",
       "my-nginx-7c79c4bf97-s4jjh"
     ]
      ],
      "capabilityArgs": {
     "dns": {
       "Servers": [
         "10.96.0.10"
       ],
       "Searches": [
         "default.svc.cluster.local",
         "svc.cluster.local",
         "cluster.local"
       ],
       "Options": [
         "ndots:5"
       ]
     },
     "io.kubernetes.cri.pod-annotations": {
       "kubernetes.io/config.seen": "2024-03-21T23:40:40.674628231Z",
       "kubernetes.io/config.source": "api"
     }
      },
      "result": {
     "cniVersion": "0.3.1",
     "dns": {},
     "interfaces": [
       {
         "mac": "e6:f9:e0:2b:ae:f7",
         "name": "cni0"
       },
       {
         "mac": "82:c5:fd:ce:10:42",
         "name": "veth8d55a711"
       },
       {
         "mac": "52:d4:10:d4:6b:2c",
         "name": "eth0",
         "sandbox": "/var/run/netns/cni-c122169f-f7f0-03b3-1aa0-7ba3a07a3317"
       }
     ],
     "ips": [
       {
         "address": "10.244.0.7/24",
         "gateway": "10.244.0.1",
         "interface": 2,
         "version": "4"
       }
     ],
     "routes": [
       {
         "dst": "10.244.0.0/16"
       },
       {
         "dst": "0.0.0.0/0",
         "gw": "10.244.0.1"
       }
     ]
      }
    }

flannel会在每台node上面运行一个flannel.1虚拟网桥。不同node之间进行通信的时候,当cni发现无接口响应,便发送给flannel.1,因为集群给不同Node划分了不同的网段(CIDR),所以可以区分出ip属于哪个node,于是用Udp封装数据包,然后发送给对应的node,对应node上的flannel.1监听8472端口,获取到数据包后解包发给cni
路线为
eth0(容器)-->cni-->flannel.1-->eth(宿主机)-->eth(目标宿主机)-->flannel.1-->cni-->eth0(目标容器)
在修改网络配置的时候,也需要注意CIDR和掩码,以防因为集群扩张而无网段可分配
如:--cluster-cidr=10.244.0.0/12和--node-cidr-mask-size=16,最大节点数为16

准备个容器

apiVersion: apps/v1
kind: Deployment
metadata: 
  name: busybox
  labels: 
    app: busybox
spec: 
  replicas: 1
  selector: 
    matchLabels: 
      app: busybox
  template:
    metadata:
      labels:
        app: busybox
    spec:
      containers:
        - name: busybox
          image: centos:latest
          ports:
          - name: http
            containerPort: 8080
            protocol: TCP
          command: ["/bin/sh"]
          args: ["-c","sleep 1000"]

让我们看看他默认状态下访问百度是什么样的

[root@master ~]# kubectl exec -it busybox-54b6dd484b-9pc22 -c busybox -- sh
sh-4.4# ping baidu.com
PING baidu.com (39.156.66.10) 56(84) bytes of data.
64 bytes from 39.156.66.10 (39.156.66.10): icmp_seq=1 ttl=51 time=9.05 ms
64 bytes from 39.156.66.10 (39.156.66.10): icmp_seq=2 ttl=51 time=9.05 ms

好,让我们nslookup一下阿里云的ip,挑选其中一个

nslookup aliyun.com
Server:         100.100.2.136
Address:        100.100.2.136#53

Non-authoritative answer:
Name:   aliyun.com
Address: 106.11.249.99

加上一点小小的魔法

apiVersion: apps/v1
kind: Deployment
metadata: 
  name: busybox
  labels: 
    app: busybox
spec: 
  replicas: 1
  selector: 
    matchLabels: 
      app: busybox
  template:
    metadata:
      labels:
        app: busybox
    spec:
      hostAliases: 
      - ip: "106.11.249.99"
        hostnames: 
        -  "baidu.com"
      containers:
        - name: busybox
          image: centos:latest
          ports:
          - name: http
            containerPort: 8080
            protocol: TCP
          command: ["/bin/sh"]
          args: ["-c","sleep 1000"]

再进去看看,乐

[root@master ~]# kubectl exec -it busybox-7995d79d96-pnm2f -c busybox -- sh
sh-4.4# ping baidu.com
PING baidu.com (106.11.249.99) 56(84) bytes of data.
64 bytes from baidu.com (106.11.249.99): icmp_seq=1 ttl=48 time=26.7 ms
64 bytes from baidu.com (106.11.249.99): icmp_seq=2 ttl=48 time=26.7 ms

当你想要给你的应用配置某个dns,但是又不想直接修改coredns以影响整个集群的时候,你就可以使用这个

describe k8s pod的时候,会看到一行QoS Class,这玩意对于k8s里的应用,就好像oom_score_adj之于linux应用,决定着当资源紧张的时候,pod被驱逐的优先顺序
k8s不想oomscore一样可以直接通过调节数值来定义,他分成固定的三类:
Guaranteed
Burstable
BestEffort
决定一个应用的QoS Class是哪一类的,是他们对资源限制的配置,当你给应用配置了cpu及内存的request和limit并且两者值相等的时候,他的种类为Guaranteed,当你至少配置了一项cpu或者内存的request的时候,他的种类为Burstable,当你什么都没有配置的时候,他的种类为BestEffort

这三种类型的容器受到的待遇天差地别,他们的cgroupsPath不同,当然还有最重要的,他们初始化的oomscoreadj也完全不同,Guaranteed可能是-998,Burstable可能是998,BestEffort就可能是1000了

还记得之前那个悄悄给自己应用取消掉了资源限制,以为这样可以为所欲为的开发的例子,java+按比例吃资源启动+容器无资源上限,得到的结果就是他的应用不停的被缺少内存资源的宿主机驱逐,属于是自作聪明了

参考:https://zhuanlan.zhihu.com/p/126532976?utm_id=0

接到个任务,写个k8s相关的小工具,目标集群在阿里云上
拉了库下来照着demo写,结果报了个错
Max retries exceeded with url: /api/v1/namespaces (Caused by SSLError(SSLError(397, '[SSL: CA_KEY_TOO_SMALL] ca key too small (_ssl.c:3862)')))
厄厄,这个config是从官网集群直接复制下来的,本地kubectl可用,我确定不是证书搞错了原因
然后折腾了半天试图跳过证书验证,client里面有个api_client.configuration.verify_ssl,可惜不知道是不是我用的方式不对,用了configuration做配置,config就不起效果了
折腾半天看到一个同样被阿里云坑到的老哥,大意说是openssl安全等级原因,默认2级需要ca长度为2048,坑爹阿里云是1024,然后就CA_KEY_TOO_SMALL了
image.png
楼下有人给了更具体的解决方式
image.png
但是直接复制进去有可能你会发现不起效果,那是因为库的版本不对
完整解决方法:

pip install requests "urllib3<2"
import urllib3
urllib3.util.ssl_.DEFAULT_CIPHERS = "ALL:@SECLEVEL=1"

环境配置
https://help.aliyun.com/document_detail/315439.html
新版文档(推荐:
https://help.aliyun.com/document_detail/315448.html?spm=a2c4g.11186623.0.0.9a7d5c2aq4loIO
获取access_key_id
https://ram.console.aliyun.com/manage/ak?spm=a2c6h.12873639.article-detail.8.76c06f779m4CWj
获取region_id,记得删尖括号
https://next.api.aliyun.com/api/Rds/2014-08-15/DescribeRegions?lang=PYTHON&tab=DEBUG
添加endpoint,可以在openai的例子里面查看
https://help.aliyun.com/document_detail/315444.html

示例

此处开发一个能够筛选某地域rds当中白名单内含有某条地址的对象,以官方例子为模板做演示讲下思路

思路:拆成两个部分:查询rds实例列表+查询某个rds的白名单,本地环境开发

先来看查询实例列表的api DescribeDBInstances
https://help.aliyun.com/document_detail/26232.html?spm=a2c4g.11186623.0.0.10197aca0hvjOE
点击文档上的调试,可以进入在线调试界面,同时在右侧可以看到自动生成的代码
我们可以看到这里regionid是必填参数,我们以cn-hangzhou为例子填入生成代码

# -*- coding: utf-8 -*-
# This file is auto-generated, don't edit it. Thanks.
import sys

from typing import List

from alibabacloud_rds20140815.client import Client as Rds20140815Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_rds20140815 import models as rds_20140815_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_util.client import Client as UtilClient


class Sample:
    def __init__(self):
        pass

    @staticmethod
    def create_client(
        access_key_id: str,
        access_key_secret: str,
    ) -> Rds20140815Client:
        """
        使用AK&SK初始化账号Client
        @param access_key_id:
        @param access_key_secret:
        @return: Client
        @throws Exception
        """
#client的配置,client的初始化在下面的函数中实现
#https://help.aliyun.com/document_detail/315449.html
        config = open_api_models.Config(
            # 必填,您的 AccessKey ID,
            access_key_id=access_key_id,
            # 必填,您的 AccessKey Secret,
            access_key_secret=access_key_secret
        )
        # 访问的域名
        config.endpoint = f'rds.aliyuncs.com'
        return Rds20140815Client(config)

    @staticmethod
    def main(
        args: List[str],
    ) -> None:
        # 工程代码泄露可能会导致AccessKey泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html
       #实例化client
        client = Sample.create_client('accessKeyId', 'accessKeySecret')
        #初始化request,api自己的各种参数都加在这里
        describe_dbinstances_request = rds_20140815_models.DescribeDBInstancesRequest(
            region_id='cn-hangzhou'
        )
        #新版 SDK 的超时机制为 RuntimeOption -> Config 设置 -> 默认
        runtime = util_models.RuntimeOptions()
        try:
            # 复制代码运行请自行打印 API 的返回值
            #client带有所有openapi,可以通过client直接调用,此处是带参式调用
            #https://help.aliyun.com/document_detail/315453.html
            client.describe_dbinstances_with_options(describe_dbinstances_request, runtime)
        except Exception as error:
            # 如有需要,请打印 error
            UtilClient.assert_as_string(error.message)

    @staticmethod
    async def main_async(
        args: List[str],
    ) -> None:
        # 工程代码泄露可能会导致AccessKey泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html
        client = Sample.create_client('accessKeyId', 'accessKeySecret')
        describe_dbinstances_request = rds_20140815_models.DescribeDBInstancesRequest(
            region_id='cn-hangzhou'
        )
        runtime = util_models.RuntimeOptions()
        try:
            # 复制代码运行请自行打印 API 的返回值
            await client.describe_dbinstances_with_options_async(describe_dbinstances_request, runtime)
        except Exception as error:
            # 如有需要,请打印 error
            UtilClient.assert_as_string(error.message)


if __name__ == '__main__':
    Sample.main(sys.argv[1:])

当你调用了这个api,他便会返回给你一个长得像json格式的结果,不过本地调用跟网页调用结果有点不一样
这是网页调用出来的结构

{
  "TotalRecordCount": 0,
  "PageRecordCount": 0,
  "RequestId": "B08AAD66-63A2-5432-9C62-3721ADA6DE8C",
  "NextToken": "",
  "PageNumber": 1,
  "Items": {
    "DBInstance": []
  }
}

如果是本地调用,外面会再套一层壳,类型为阿里云自己的一种对象,如果你想取出其中的一项值,直接用.去取就行
比如说,我想把items当中的dbinstance中的实例id和实例名称给取出来

#结果是个对象
list = {}
ret1 = client.describe_dbinstances_with_options(describe_dbinstances_request, runtime)
ret2 = ret1.body.items.dbinstance #实例列表,返回结果里面有响应头header和body两层,我们要的数据在body
for item in ret2:
    list[item.dbinstance_id]=item.dbinstance_description

具体有哪些结构、要取出哪些,可以对比输出结果和文档,注意小写和下划线

这时你可能注意到这个api的一个说明
本接口支持如下两种方式查看返回数据:

  • 方式一:通过传入MaxResults参数设置每页的记录数,再通过NextToken参数设置翻页凭证来展示下一页的内容。NextToken取值为上一次调用DescribeDBInstances接口返回的NextToken参数值。
  • 方式二:通过PageSize参数设置每页的记录数,通过PageNumber参数进行翻页。

说明
上述两种方式只能任选其一。当返回的记录数较多时,推荐使用方式一,可以获得更快的查询速度

意思很明显,虽然不知道这家伙是怎么想的,总之你不能一次性列出所有的实例了(除非你的实例数小于100),那么要怎样才能翻页、列出所有的数据呢,我们来看看这里面提到的几个参数

看下我们刚才打印出来的结果,你会注意到里面有个东西叫做NextToken,如果你使用网页调试,你也可以在参数配置里面看到翻页参数这一类,很显然,我们得要手动输入nexttoken才能翻页,这东西的实现方式就是在你刚才调用的api里面加上这个参数(而且这个参数似乎空值会报错),那么我们可以另外在写一个函数,在原来的基础上给他加上这个参数,然后在脚本里面写一个判断,当我们的结果列表长度等于我们设置的一页的结果个数时,就调用一下我们这个翻页函数

#核心部分
        describe_dbinstances_request = rds_20140815_models.DescribeDBInstancesRequest(
            region_id='cn-hangzhou',
            max_results=100, #别忘了在第一次调用的函数里面也加上这个参数
            next_token=token #传参,当然,记得保留上次调用结果的token,取的方式同上
        )

现在,我们拥有了一个包含所有实例id和名称的字典,我们来查看他们当中谁满足我们的条件:去调用白名单api来完成这一任务
https://help.aliyun.com/document_detail/26241.html
这api肉眼可见的简单,只需要传入一个dbinstanceid就可以查询出我们想要的结果,你如果想偷懒可以随便填个例子生成代码然后再在那基础上面修改,至于返回的结果,处理方式也同上,后面也就不再多说,字符串处理str.find,结果可以用csv库输出成文件保存