pandas 备忘

我对python也差不多是零基础,快忘光光了,这一篇做点简单记录。

官方API地址:https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.read_excel.html

Pandas 是一个开源的第三方 Python 库,从 Numpy 和 Matplotlib 的基础上构建而来,享有数据分析“三剑客之一”的盛名(NumPy、Matplotlib、Pandas)。Pandas 已经成为 Python 数据分析的必备高级工具,它的目标是成为强大、灵活、可以支持任何编程语言的数据分析工具。

pandas教程

Pandas 这个名字来源于面板数据(Panel Data)与数据分析(data analysis)这两个名词的组合。在经济学中,Panel Data 是一个关于多维数据集的术语。Pandas 最初被应用于金融量化交易领域,现在它的应用领域更加广泛,涵盖了农业、工业、交通等许多行业。

Pandas 最初由 Wes McKinney(韦斯·麦金尼)于 2008 年开发,并于 2009 年实现开源。目前,Pandas 由 PyData 团队进行日常的开发和维护工作。在 2020 年 12 月,PyData 团队公布了最新的 Pandas 1.20 版本 。

在 Pandas 没有出现之前,Python 在数据分析任务中主要承担着数据采集和数据预处理的工作,但是这对数据分析的支持十分有限,并不能突出 Python 简单、易上手的特点。Pandas 的出现使得 Python 做数据分析的能力得到了大幅度提升,它主要实现了数据分析的五个重要环节:

  • 加载数据
  • 整理数据
  • 操作数据
  • 构建数据模型
  • 分析数据

Pandas内置数据结构 我们知道,构建和处理二维、多维数组是一项繁琐的任务。Pandas 为解决这一问题, 在 ndarray 数组(NumPy 中的数组)的基础上构建出了两种不同的数据结构,分别是 Series(一维数据结构)DataFrame(二维数据结构):

  • Series 是带标签的一维数组,这里的标签可以理解为索引,但这个索引并不局限于整数,它也可以是字符类型,比如 a、b、c 等;
  • DataFrame 是一种表格型数据结构,它既有行标签,又有列标签。

安装

Python 官方标准发行版没有自带 Pandas 库,因此需要另行安装。

除了标准发行版外,还有一些第三方机构发布的 Python 免费发行版, 它们在官方版本的基础上开发而来,并有针对性的提前安装了一些 Python 模块,从而满足某些特定领域的需求,比如专门适应于科学计算领域的 Anaconda,它就提前安装了多款适用于科学计算的软件包。

对于第三方发行版而言,它们已经自带 Pandas 库,所以无须另行安装,例如:

  1. Anaconda(官网下载:https://www.anaconda.com/)是一个开源的 Python 发行版,包含了 180 多个科学包及其依赖项。除了支持 Windows 系统外,也支持 Linux 和 Mac 系统。

  2. Python(x,y)(下载地址:https://python-xy.github.io/)是一款基于 Python、Qt (图形用户界面)和 Spyder (交互式开发环境)开发的软件,主要用于数值计算、数据分析和数据可视化等工程项目,目前只支持 Python 2 版本。

  3. WinPython(下载地址:https://sourceforge.net/projects/winpython/files/)一个免费的 Python 发行版,包含了常用的科学计算包与 Spyder IDE,但仅支持 Windows 系统。

因为我是新手,不使用太高层的工具了,集成工具虽然方便,毕竟封装了一层,说不准它也存在问题呢(尤其是我还用着Mac M1的情况下)?网络问题啊什么的还要折腾,遂直接使用pip直接安装,也有利于掌握。

pip install pandas
pip install openpyxl

image-20221109午後35840479

hello world

image-20221109午後41047948

Pandas Series

类似表格中的一个列(column),类似于一维数组,可以保存任何数据类型。

Series 由索引(index)和列组成,函数如下:

pandas.Series( data, index, dtype, name, copy)

参数说明:

  • data:一组数据(ndarray 类型)。
  • index:数据索引标签,如果不指定,默认从 0 开始。
  • dtype:数据类型,默认会自己判断。
  • name:设置名称。
  • copy:拷贝数据,默认为 False。
import pandas as pd
sites = {1: "Google", 2: "Runoob", 3: "Wiki"}
myvar = pd.Series(sites, index = [1, 2], name="RUNOOB-Series-TEST" )
print(myvar)

image-20221109午後41723229

DataFrame

首先要明确表格中的行与列:

  • 行:row,在dataframe中可以理解为index
  • 列:column

一个表格型的数据结构,它含有一组有序的列,每列可以是不同的值类型(数值、字符串、布尔型值)。DataFrame 既有行索引也有列索引,它可以被看做由 Series 组成的字典(共同用一个索引)。

DataFrame 构造方法如下:

pandas.DataFrame( data, index, columns, dtype, copy)

参数说明:

  • data:一组数据(ndarray、series, map, lists, dict 等类型)。

    ndarray 是 NumPy 中的 N 维数组对象,它是一系列同类型数据的集合,以 0 下标为开始进行集合中元素的索引。ndarray 对象是用于存放同类型元素的多维数组。

  • index:索引值,或者可以称为行标签。

  • columns:列标签,默认为 RangeIndex (0, 1, 2, …, n) 。

  • dtype:数据类型。

  • copy:拷贝数据,默认为 False。

image-20221109午後44217343

image-20221109午後43750874

CSV

(Comma-Separated Values,逗号分隔值,有时也称为字符分隔值,因为分隔字符也可以不是逗号),其文件以纯文本形式存储表格数据(数字和文本)。CSV 是一种通用的、相对简单的文件格式,被用户、商业和科学广泛应用。

image-20221109午後44645743

方法简单记录:

  • read_csv

  • to_csv

  • head( n ) 方法用于读取前面的 n 行,如果不填参数 n ,默认返回 5 行。

  • tail( n ) 方法用于读取尾部的 n 行,如果不填参数 n ,默认返回 5 行,空行各个字段的值返回 NaN

  • info() 方法返回表格的一些基本信息

    image-20221109午後45041932

json

df = pd.read_json('sites.json')
print(df.to_string())

excel

df = pd.read_excel(io, sheet_name=0, header=0, names=None, index_col=None, 
              usecols=None, squeeze=False,dtype=None, engine=None, 
              converters=None, true_values=None, false_values=None, 
              skiprows=None, nrows=None, na_values=None, parse_dates=False, 
              date_parser=None, thousands=None, comment=None, skipfooter=0, 
              convert_float=True, kwds)

pandas读取Excel后返回 DataFrame。

数据清洗

参考 https://www.runoob.com/pandas/pandas-cleaning.html

常用函数

  • 统计函数
    • count() 统计某个非空值的数量。

    • sum() 求和

    • mean() 求均值

    • median() 求中位数

    • mode() 求众数

    • std() 求标准差

    • min() 求最小值

    • max() 求最大值

    • abs() 求绝对值

    • prod() 求所有数值的乘积。

    • cumsum() 计算累计和,axis=0,按照行累加;axis=1,按照列累加。

    • cumprod() 计算累计积,axis=0,按照行累积;axis=1,按照列累积。

    • corr() 计算数列或变量之间的相关系数,取值-1到1,值越大表示关联性越强

    • 百分比变化(pct_change)

    • 协方差(cov)

    • 相关系数(corr)

    • 排名(rank)

    另外,describe() 函数显示与 DataFrame 数据列相关的统计信息摘要。

image-20221109午後50401569

  • 重置索引(reindex)

    • reindex
    • reindex_like
    • rename
  • iteration遍历

    • iteritems():以键值对 (key,value) 的形式遍历,以列标签为键,以对应列的元素为值。
    • iterrows():以 (row_index,row) 的形式遍历行,返回一个迭代器,以行索引标签为键,以每一行数据为值。
    • itertuples():返回一个迭代器,该方法会把每一行生成一个元组。
  • sorting排序

    • sort_index() 行标签排序
    • sort_index(axis=1) 列标签排序
    • sort_values(by=’col1’) / sort_values(by=[‘col1’,’col2’]) 表示按值排序。
    • sort_values() 提供了参数kind用来指定排序算法。这里有三种排序算法:
      • mergesort
      • heapsort
      • quicksort
  • 去重函数:drop_duplicates()

  • 字符串处理

    • lower() 将的字符串转换为小写。
    • upper() 将的字符串转换为大写。
    • len() 得出字符串的长度。
    • strip() 去除字符串两边的空格(包含换行符)。
    • split() 用指定的分割符分割字符串。
    • cat(sep=””) 用给定的分隔符连接字符串元素。
    • get_dummies() 返回一个带有独热编码值的 DataFrame 结构。
    • contains(pattern) 如果子字符串包含在元素中,则为每个元素返回一个布尔值 True,否则为 False。
    • replace(a,b) 将值 a 替换为值 b。
    • count(pattern) 返回每个字符串元素出现的次数。
    • startswith(pattern) 如果 Series 中的元素以指定的字符串开头,则返回 True。
    • endswith(pattern) 如果 Series 中的元素以指定的字符串结尾,则返回 True。
    • findall(pattern) 以列表的形式返出现的字符串。
    • swapcase() 交换大小写。
    • islower() 返回布尔值,检查 Series 中组成每个字符串的所有字符是否都为小写。
    • issupper() 返回布尔值,检查 Series 中组成每个字符串的所有字符是否都为大写。
    • isnumeric() 返回布尔值,检查 Series 中组成每个字符串的所有字符是否都为数字。
    • repeat(value) 以指定的次数重复每个元素。
    • find(pattern) 返回字符串第一次出现的索引位置。
  • 设置数据显示格式

    • get_option()
    • set_option()
    • reset_option()
    • describe_option()
    • option_context()
  • loc/iloc

    • 基于标签索引选取数据
    • 基于整数索引选取数据
  • 窗口函数

    • rolling()
    • expanding()
    • ewm()
  • 聚合函数

    • aggregate
  • 缺失值处理

  • groupby() 分组操作

    • 拆分(Spliting):表示对数据进行分组;
    • 应用(Applying):对分组数据应用聚合函数,进行相应计算;
    • 合并(Combining):最后汇总计算结果。
  • merge() 函数

  • concat() 函数能够轻松地将 Series 与 DataFrame 对象组合在一起

  • 时间

    • Timestamp()
    • date_range()
    • to_datetime()
    • Period()
    • period_range()
    • python内置方法 strptime()
    • DatetimeIndex()
    • Timedelta()
    • to_timedelta()
  • sample随机抽样

  • resample数据重采样

    • 降采样 /.lmklk,.lk,,,,,? llresample()
    • 升采样 resample(‘D’).asfreq().head()
    • 频率转换 asfreq()
    • fds 插值处理 pad/ffill,backfill/bfill,interpolater(‘linear’),fillna(value)
  • 分类对象(Categorical Object)。有序排列、自动去重的功能,但是它不能执行运算。

  • Pandas绘图,在 Matplotlib 绘图软件包的基础上单独封装了一个plot()接口,通过调用该接口可以实现常用的绘图操作

    • 线条绘图
    • 柱状图:bar() 或 barh()
    • 直方图:hist()
    • 箱状箱:box()
    • 区域图:area()
    • 散点图:scatter()
  • 读取文件

    • read_csv to_csv
    • read_json
    • read_sql_query
    • to_excel read_excel
  • 索引(index)

    通过索引可以从 DataFame 中选择特定的行数和列数,这种选择数据的方式称为“子集选择”。

    在 Pandas 中,索引值也被称为标签(label),它在 Jupyter 笔记本中以粗体字进行显示。索引可以加快数据访问的速度,它就好比数据的书签,通过它可以实现数据的快速查找。

    • 通过列索引(标签)读取多列数据。

      import pandas as pd  
      #设置"Name"为行索引    
      data = pd.read_csv("person.csv", index_col ="Name")   
      # 通过列标签选取多列数据  
      a = data[["City","Salary"]]
      print(a)
      
    • set_index()

    • 重置索引reset_index()

  • 分层索引(Multiple Index)

    在一个轴上拥有多个(即两个以上)索引层数,这使得我们可以用低维度的结构来处理更高维的数据。

    分层索引的目的是用低维度的结构(Series 或者 DataFrame)更好地处理高维数据。

    • MultiIndex()
    • 分层索引切片取值
    • 聚合函数应用
    • 局部索引
    • 行索引层转换为列索引
    • 交换层
    • 层排序
  • 执行SQL操作

  • NumPy

    Pandas 是在 NumPy 的基础构建而来

    熟悉 NumPy 可以更加有效的帮助我们使用 Pandas。

    NumPy 主要用 C语言编写,因此,在计算还和处理一维或多维数组方面,它要比 Python 数组快得多。

  • 使用注意事项

    • if 语句
    • 布尔运算
    • isin()
    • reindex()

参考资料


m1 安装 python

brew install python3

Screenshot 5

更改.bashrc.zshrc

alias python="/opt/homebrew/bin/python3"
alias python3="/opt/homebrew/bin/python3"

确认 pip 命令

$ python3 -m pip --version
pip 22.2.2 from /opt/homebrew/lib/python3.10/site-packages/pip (python 3.10)

$ python3 -m pip install --upgrade setuptools
$ python3 -m pip install --upgrade pip

$ pip --version

image-20221109午後33348088

image-20221109午後33432512

修改pip源为阿里源

pip config set global.index-url https://mirrors.aliyun.com/pypi/simple

参考资料


python 使用备忘

最近重新拿起 python,这里零碎地记录一下内容

参考资料


etcd proxy 配置和使用

本文大部分转自:etcd proxy功能简介, 原文科普简单,没有涉及证书的内容,不太适用于在企业使用。我在末尾加了证书相关的内容和命令。

一、etcd proxy概述

etcd提供了proxy功能,即代理功能,etcd可以代理的方式来运行。

etcd代理可以运行在每一台主机,在这种代理模式下,etcd的作用就是一个反向代理,把客户端的etcd请求转发到真正的etcd集群。这种方式既加强了集群的弹性,又不会降低集群的写的性能。

etcd proxy支持2种运行模式:readwrite和readonly,缺省的是readwrite,即proxy会将所有的读写请求都转发给etcd集群;readonly模式下,只转发读请求,写请求将会返回http 501错误。

二、etcd proxy参数

启动etcd代理主要有3个参数:proxy、listen-client-urls、initial-cluster(或discovery)。

proxy指的是代理的模式,on是readwrite模式,readonly是只读模式。

listen-client-urls指的是代理的监听地址。

initial-cluster(或discovery)指的是请求将转发到在此url发现的etcd集群。discovery指的是使用服务发现方式搭建的集群url。

启动命令示例:

./etcd –proxy on –listen-client-urls http://0.0.0.0:22379  –initial-cluster myetcd1=http://0.0.0.0:12380 –data-dir /home/cmp/temp/proxy

三、注意事项

1、proxy只支持API v2,不支持v3;

2、在服务发现集群模式下,多启动的etcd节点将会自动降级成读写模式的代理;

3、代理不会自动变成etcd集群节点,如要加入集群需要手工进行如下操作:

etcd add命令将proxy节点加入集群、停止proxy进程或服务、删除proxy数据目录、使用正确的参数配置重新启动etcd进程或服务。

四、代理实例操作演示

1、先启动一个简单etcd集群

./etcd –name myetcd1 –listen-client-urls http://0.0.0.0:12379 –advertise-client-urls http://0.0.0.0:12379 –listen-peer-urls http://0.0.0.0:12380 –initial-advertise-peer-urls http://0.0.0.0:12380 –initial-cluster myetcd1=http://0.0.0.0:12380 –data-dir /home/cmp/temp/myetcd1

2、再启动一个proxy节点

./etcd –proxy on –listen-client-urls http://0.0.0.0:22379 –initial-cluster myetcd1=http://0.0.0.0:12380 –data-dir /home/cmp/temp/proxy &

3、查看etcd集群信息

./etcdctl –endpoints http://0.0.0.0:12379 member list
eb06dc1bc141ff4f: name=myetcd1 peerURLs=http://0.0.0.0:12380 clientURLs=http://0.0.0.0:12379 isLeader=true

etcd集群中只有一个leader成员。

4、通过代理进行key-value操作

./etcdctl –endpoints http://127.0.0.1:22379 set key1 xxxxx
xxxxx

./etcdctl –endpoints http://127.0.0.1:22379 get key1
xxxxx

API v2命令可以成功通过代理读写数据。

ETCDCTL_API=3 ./etcdctl –endpoints=http://127.0.0.1:12379 put key2 sssssssssss
OK

ETCDCTL_API=3 ./etcdctl –endpoints=http://127.0.0.1:22379 get key2
Error: context deadline exceeded

直接访问集群时,API v3命令成功将key值写入,但是通过proxy读取时,命令直接报错。

5. 是用ssl证书的相关内容

我使用systemd进行管理,配置如下:

# /etc/systemd/system/etcd.service
[Unit]
Description=Etcd Server
After=network.target
After=network-online.target
Wants=network-online.target
Documentation=https://github.com/coreos

[Service]
Type=notify
WorkingDirectory=/var/local/etcd/
User=root
ExecStart=/usr/bin/etcd \
  --proxy=on \
  --peer-cert-file=/var/local/etcd/ssl/peer.pem  \
  --peer-key-file=/var/local/etcd/ssl/peer-key.pem  \
  --peer-trusted-ca-file=/var/local/etcd/ssl/ca.pem  \
  --listen-client-urls http://0.0.0.0:2379  \
  --initial-cluster node1=https://xxx:2380,node2=https://xxx:2380,node3=https://xxx:2380 \
  --data-dir=/var/local/etcd/etcddata
Restart=on-failure
RestartSec=5
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

要注意的是,我监听的端口2379并不包含 ssl 证书,使用的 peer 证书是连接到etcd-server所需的证书。

启动输出如下:

image-20221028午前114126830

客户端连接命令如下:

etcdctl --ca-file=/var/local/etcd/ssl/ca.pem --cert-file=/var/local/etcd/ssl/peer.pem --key-file=/var/local/etcd/ssl/peer-key.pem --endpoints=http://{{$proxy_ip}}:2379  get /

需要注意的是,我在客户端中使用的是http,而不是https。

参考资料


cAdvisor内存占用不断飙升导致其在k8s内不断crash问题排查 - hansedong

原文:https://hansedong.github.io/2018/10/24/5/

背景

我们的额监控方案为:Kubernetes(K8S)+cAdvisor+Prometheus+Grafana。 然后,用cAdivor监控容器信息,其实,cAdivor其实到现在的主流K8S版本中,Kubelet进程已经将其内置了,但是我们没有这么用,因为没有必要因为让Prometheus定期去Kubelet上采集容器信息,平白增添对Kubelet的压力。相反,我觉得,还是应该还是应该单独部署cAdvisor,这样一来,不论是定制化cAdvisor,还是版本更新,都会更方面。所以,我使用DaemonSet部署了cAdvisor。

问题

用DaemonSet的方式部署cAdvisor,本质上,就是每个K8S的宿主机都启动了一个pod,实际观测,发现这些Pod的状态,会随着时间的推移,开始频繁出现Crash。这个问题,势必会导致cAdvisor无法正常监控容器信息。下面是具体的排查过程。

排查初探

首先,Pod Crash 必然有其原因,所以,一开始是通过下面的方式,看cAdvisor到底为何会Crash,通过

kubectl describe pod -n monitoring pod-xxxxx

找到Last State部分,发现其为:

State: OOMKilled

这说明,这个 Pod,是因为内存不够,cAdvisor在运行过程,超出了Pod的资源限制,被OOM杀掉了。既然资源不够,那么首先,就是调大其内存限制。

一开始为这个Pod设置的上限资源为1核CPU+1G内存,既然内存无法满足,那么调大为2G,继续观测,发现依然会OOM。然后又调整为3G、4G、10G、20G(机器内存大,土豪),发现虽然内存变大了会有一些缓解,但实际上,即使内存上限设置为20G,还是会有Crash的情况,那么,这时候就需要反思一下几个问题了:

  1. 是否是cAdvisor存在bug?
  2. 哪个机器上的cAdvisor Pod总是重启?
排查是否是cAdvisor版本问题

针对第一点,我们升级了cAdivor镜像为最新版,问题依旧。

排查是否是cAdvisor参数配置问题

google一些文章,有人提过类似的问题,官方issue的解释中,有人提到可能配置不对,可能采集的指标过多等,于是,我review了一下我的配置,调整后的完整配置如下:

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  labels:
    name: cadvisor
  name: cadvisor
  namespace: monitoring
spec:
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      name: cadvisor
  template:
    metadata:
      annotations:
        prometheus.io/port: "28762"
        prometheus.io/scrape: "true"
      creationTimestamp: null
      labels:
        name: cadvisor
    spec:
      automountServiceAccountToken: false
      containers:
      - args:
        - -allow_dynamic_housekeeping=true
        - -global_housekeeping_interval=1m0s
        - -housekeeping_interval=3s
        - -disable_metrics=udp,tcp,percpu,sched
        - -storage_duration=15s
        - -profiling=true
        - -port=28762
        - -max_procs=1
        image: mine/cadvisor-test:v0.0.2
        imagePullPolicy: IfNotPresent
        name: cadvisor
        ports:
        - containerPort: 28762
          hostPort: 28762
          name: http
          protocol: TCP
        resources:
          limits:
            cpu: "1"
            memory: 3000Mi
          requests:
            cpu: "1"
            memory: 500Mi
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
        volumeMounts:
        - mountPath: /rootfs
          name: rootfs
          readOnly: true
        - mountPath: /var/run
          name: var-run
          readOnly: true
        - mountPath: /sys
          name: sys
          readOnly: true
        - mountPath: /var/lib/docker
          name: docker
          readOnly: true
        - mountPath: /dev/disk
          name: disk
          readOnly: true
      dnsPolicy: ClusterFirst
      hostNetwork: true
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
      volumes:
      - hostPath:
          path: /
          type: ""
        name: rootfs
      - hostPath:
          path: /var/run
          type: ""
        name: var-run
      - hostPath:
          path: /sys
          type: ""
        name: sys
      - hostPath:
          path: /DATA/docker
          type: ""
        name: docker
      - hostPath:
          path: /dev/disk
          type: ""
        name: disk
  templateGeneration: 6
  updateStrategy:
    rollingUpdate:
      maxUnavailable: 1
    type: RollingUpdate

我调整的部分主要集中在:

// 这个是禁用哪些指标,默认只有udp、tcp
- -disable_metrics=udp,tcp,percpu,sched
// 存储最近多久的数据,原来是1分多钟,调整为15s
- -storage_duration=15s
// 是否开启性能测试,默认为关闭,之所以开启,是要一会儿debug内存占用
- -profiling=true
// 使用多少CPU,默认不到1个
- -max_procs=1

上面的方式,是减少了一些采集指标,以及采集数据的最多保留时长,稍微有些效果,但是发现效果不大,原来某些机器上频繁Crash的cAdvisor Pod,还是Crash,另外某些机器上从来不Crash的,也不会Crash。那么,说明参数配置没什么用,问题应该出现某些机器上。

排查为何cAdivosr Pod在某些机器上Crash

我回顾了一下我们的K8S节点,发现cAdvisor Pod不OOM的机器上面,容器都比较少。越是容器多的机器,这机器上的cAdvisor Pod就越容易OOM Crash。

那么,我们看一下 cAdvisor 的 Pod 日志,发现其频繁报一个错误:

fsHandler.go:135] du and find on following dirs took 57.562700809s: [/rootfs/DATA/docker/overlay2/d8c002c4dc33c22129124e70bf7ca15fd312cd8867c040708d11d7d462ee58df/diff /rootfs/DATA/docker/containers/16eb9120ce2da24d867ee287c093ce7221f1d3ed39e69c3a8d128313a5dc0d63]; will not log again for this container unless duration exceeds 4s

这说明,cAdvisor会统计每一个容器占用的磁盘使用大小,这个大小是通过du命令来处理的,而且,这个统计耗费的时间很长。我们可以实际去看一下,发现这个目录,确实比较大,有些在2-3G。这说明,这个机器上,必然存在一些容器,里边在搞事情,写了很多的文件,导致 du 命令在统计的时候,比较耗时。

问题初步总结

K8S节点,有些容器存储或写入了比较多的文件,造成cAdvisor统计容器磁盘使用耗时,进而引发此cAdivosr内存占用升高。

排查深入探究

既然上面已经初步定为问题,但是我们依然会疑惑,为什么cAdivosr统计容器磁盘耗时会引发内存飙升呢?

我们需要借助一些工具来进一步排查

  1. 通过 go tool pprof 分析内存
  2. 通过查看 cAdvisor 源码分析流程
  3. 在源码中,打断点,验证猜想
通过 go tool pprof 分析内存

首先,将 DaemonSet 启动的 cAdvisor,使用 Host 模式启动,这样我们就可以直接通过访问宿主机上,cAdvisor开放的端口,来做性能采样了。

go tool pprof  -cum -svg -alloc_space http://x.x.x.x:28762/debug/pprof/heap

上面的步骤,会生成内存性能采样图,类似如下:

采样图

详细采样图,可以通过此连接查看:

采样图全

从图中,先看红色部分,颜色越深,表示这部分资源消耗越严重,我们这个采样图是采集的内存,可以看到,有 2366.70M,是 Gather 函数的,但其实,这个函数本身,并没有多少内存消耗,它的内存占用这么大,是 collectContainersInfo 函数分配的。其实不论怎样,Gather函数都脱离不了干系。那么,我们从源码看一下

源码分析

首先,入口函数main中,注册了/metrics对应的handler,因为cAdvisor要开发 /metirics路径,让 Prometheus 来定时采集

// cadvisor.go#82
func main() {
	defer glog.Flush()
	flag.Parse()
	//注册HTTP路径 *prometheusEndpoint 值就是 /metirics
	cadvisorhttp.RegisterPrometheusHandler(mux, containerManager, *prometheusEndpoint, containerLabelFunc, includedMetrics)
	glog.V(1).Infof("Starting cAdvisor version: %s-%s on port %d", version.Info["version"], version.Info["revision"], *argPort)

	addr := fmt.Sprintf("%s:%d", *argIp, *argPort)
	glog.Fatal(http.ListenAndServe(addr, mux))
}

然后,看一下,是谁在处理 /metrics 路由对应的操作

// 代码文件:http/handler.go#97
func RegisterPrometheusHandler(mux httpmux.Mux, containerManager manager.Manager, prometheusEndpoint string,
	f metrics.ContainerLabelsFunc, includedMetrics container.MetricSet) {
	r := prometheus.NewRegistry()
	r.MustRegister(
		metrics.NewPrometheusCollector(containerManager, f, includedMetrics),
		prometheus.NewGoCollector(),
		prometheus.NewProcessCollector(os.Getpid(), ""),
	)
	//可以看到,真正执行 /metrics 的函数,是 promhttp.HandlerFor
	mux.Handle(prometheusEndpoint, promhttp.HandlerFor(r, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}))
}

可以看到,真正执行 /metrics 的函数,是promhttp.HandlerFor,具体深入HandlerFor看一下

// 代码文件:vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go#82
func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
	   //这里就是真正的 Gather 调用
		mfs, err := reg.Gather()
		...
	})
}

至此,可以说明,每一次HTTP调用(调用 x.x.x.x:8080/metrics),都会又一次Gather调用。

所以我们猜想,之所以Gather函数有这么大的内存占用,主要是因为Gather函数调用次数多,而每次Gather函数执行之间长,导致形成了并发调用,这种情况下,Gather函数从执行到结束期间,都不会释放内存,并发调用,就会导致内存积压。

修改源码,重新构建部署,验证猜想

那么,我们在Gather调用处,打断点,看一下执行时间:

// 代码文件:vendor/github.com/prometheus/client_golang/prometheus/promhttp/http.go#82
func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
	   pp.Println("请求开始————————")
		start:=time.Now()
	   //这里就是真正的 Gather 调用
		mfs, err := reg.Gather()
		...
		timeCost := time.Since(start)
		pp.Println(fmt.Sprintf("请求结束,耗时 %v", timeCost))
	})
}

我们打印了Gather执行的耗时,然后重新构建 cAdvisor源码,打一个私有镜像出来,推送到私有镜像仓库。然后我们使用这个测试镜像,重新部署cAdvisor。

现在,我们挑一台之前cAdvisor频发OOM Crash的机器,看一下它的log

kubectl logs -n monitoring cadvisor-k9kpt -f

日志输出大致如下:

"请求开始————————"
I1023 14:21:19.126794       1 fsHandler.go:135] du and find on following dirs took 15.420205027s: [/rootfs/var/lib/docker/overlay2/67ec1868b2c0ed5ce5b22ee014eb4d08993accd68546a3de6aa2a6355bdc1a78/diff /rootfs/var/lib/docker/containers/cd910753386b3325af8bd5a69fc01b261ca14c1bfaf754677662e903b755d34f]; will not log again for this container unless duration exceeds 56s
I1023 14:21:19.305938       1 fsHandler.go:135] du and find on following dirs took 15.278733582s: [/rootfs/var/lib/docker/overlay2/10621b60f26962cb1a90d7a7dc1ce4e3c8a15f6e4e30861b8433c5c37727bb9e/diff /rootfs/var/lib/docker/containers/b2a4d11c37aa9c63b4759c5728956253fad46fa174c7fe4d91336a4ac7532127]; will not log again for this container unless duration exceeds 1m34s
I1023 14:21:19.827757       1 fsHandler.go:135] du and find on following dirs took 13.897447077s: [/rootfs/var/lib/docker/overlay2/29b3b0dfc22053937e9c40e004a6d31af489573ff3a385020feb22d88d1a3d0a/diff /rootfs/var/lib/docker/containers/af962971a0643418d28c03b374e31a0c58dd6302524ea06dc8a23c4eccf5d663]; will not log again for this container unless duration exceeds 1m20s
I1023 14:21:20.042949       1 fsHandler.go:135] du and find on following dirs took 14.514122984s: [/rootfs/var/lib/docker/overlay2/27f1d3cb3d421567754cb7abb986c16c3f3bec0874e983a2604aa7eda8834d9a/diff /rootfs/var/lib/docker/containers/60cad8688e31b557e2e98c47beaa1f3af2ea2e6cbfab0c1f399887b3eecec86c]; will not log again for this container unless duration exceeds 1m56s
"请求结束,耗时 58.093771464s"

日志其实我只是截图了一部分,基本上可以看出来,Gather请求十分耗时,这个耗时,就是由 du 操作耗时造成的,有时候,du 耗时非常严重,能将近2分钟。

这样,基本上,就印证了,Gather函数处理慢,而Prometheus每隔3s请求一次,造成同时有非常多的 Gather函数在并发处理,也就导致了内存积压的情况。

彻底解决

综上,其实我们只需要让 du 磁盘统计快了就可以了,du 的快慢,是一个CPU密集和磁盘IO密集的操作,要加快 du 操作,就需要给到足够的运算能力。

回顾之前我们的 cAdvisor 的 DaemonSet 的yaml配置,我们在资源的 limit 部分,仅给到了一个 CPU,我们加 CPU 核数增加到6。如下:

resources:
  limits:
    cpu: "6"
    memory: 3000Mi
  requests:
    cpu: "2"
    memory: 500Mi

然后,更新 DaemonSet 部署

kubectl apply -f cadvisor.ds.yaml

再次去观察 cAdvisor 的pod日志,发现du耗时明显缩短到2秒钟以内,pod内存占用过高的情况,再也没有出现过。问题得解!


Linux 删除虚拟网卡/网桥

一些知识

  • 什么是网桥

    网桥,是把两个不同物理层,不同MAC子层,不同速率的局域网连接在一起。比如说10MB/S与100MB/S的局域网,具有存储转化功能。

    网桥是一种在链路层实现中继,对帧进行转发的技术,根据MAC分区块,可隔离碰撞,将网络的多个网段在数据链路层连接起来的网络设备。

    它是Linux上用来做TCP/IP二层协议交换的设备,与现实世界中的交换机功能相似。

  • 什么是网卡

    网卡是电脑的一个接收信息 转换信息 暂储信息的一个硬件。它是把接受到信息递交给上层,如(CUP)的一个接口。

操作

  • 刪除虚拟网卡

    tunctl -d <虚拟网卡名>
    
  • 刪除虚拟网桥

    ifconfig <网桥名> down
    brctl delbr <网桥名>
    
  • 将网卡tap0, eth0 移出bridge(br0)

    brctl delif br0 tap0
    brctl delif br0 eth0
    
  • 另一个删除网桥

    ip link del flannel.100
    

参考资料