分类 技术 下的文章

这个分步指导教程教你通过在 Kubernetes 上部署一个简单的 Python 应用程序来学习部署的流程。

Kubernetes 是一个具备部署、维护和可伸缩特性的开源平台。它在提供可移植性、可扩展性以及自我修复能力的同时,简化了容器化 Python 应用程序的管理。

不论你的 Python 应用程序是简单还是复杂,Kubernetes 都可以帮你高效地部署和伸缩它们,在有限的资源范围内滚动升级新特性。

在本文中,我将描述在 Kubernetes 上部署一个简单的 Python 应用程序的过程,它包括:

  • 创建 Python 容器镜像
  • 发布容器镜像到镜像注册中心
  • 使用持久卷
  • 在 Kubernetes 上部署 Python 应用程序

必需条件

你需要 Docker、kubectl 以及这个 源代码

Docker 是一个构建和承载已发布的应用程序的开源平台。可以参照 官方文档 去安装 Docker。运行如下的命令去验证你的系统上运行的 Docker:

$ docker info
Containers: 0
Images: 289
Storage Driver: aufs
 Root Dir: /var/lib/docker/aufs
 Dirs: 289
Execution Driver: native-0.2
Kernel Version: 3.16.0-4-amd64
Operating System: Debian GNU/Linux 8 (jessie)
WARNING: No memory limit support
WARNING: No swap limit support

kubectl 是在 Kubernetes 集群上运行命令的一个命令行界面。运行下面的 shell 脚本去安装 kubectl

curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl

部署到 Kubernetes 的应用要求必须是一个容器化的应用程序。我们来回顾一下 Python 应用程序的容器化过程。

一句话了解容器化

容器化是指将一个应用程序所需要的东西打包进一个自带操作系统的容器中。这种完整机器虚拟化的好处是,一个应用程序能够在任何机器上运行而无需考虑它的依赖项。

我们以 Roman Gaponov 的 文章 为参考,来为我们的 Python 代码创建一个容器。

创建一个 Python 容器镜像

为创建这些镜像,我们将使用 Docker,它可以让我们在一个隔离的 Linux 软件容器中部署应用程序。Docker 可以使用来自一个 Dockerfile 中的指令来自动化构建镜像。

这是我们的 Python 应用程序的 Dockerfile:

FROM python:3.6
MAINTAINER XenonStack

# Creating Application Source Code Directory
RUN mkdir -p /k8s_python_sample_code/src

# Setting Home Directory for containers
WORKDIR /k8s_python_sample_code/src

# Installing python dependencies
COPY requirements.txt /k8s_python_sample_code/src
RUN pip install --no-cache-dir -r requirements.txt

# Copying src code to Container
COPY . /k8s_python_sample_code/src/app

# Application Environment variables
ENV APP_ENV development

# Exposing Ports
EXPOSE 5035

# Setting Persistent data
VOLUME ["/app-data"]

# Running Python Application
CMD ["python", "app.py"]

这个 Dockerfile 包含运行我们的示例 Python 代码的指令。它使用的开发环境是 Python 3.5。

构建一个 Python Docker 镜像

现在,我们可以使用下面的这个命令按照那些指令来构建 Docker 镜像:

docker build -t k8s_python_sample_code .

这个命令为我们的 Python 应用程序创建了一个 Docker 镜像。

发布容器镜像

我们可以将我们的 Python 容器镜像发布到不同的私有/公共云仓库中,像 Docker Hub、AWS ECR、Google Container Registry 等等。本教程中我们将发布到 Docker Hub。

在发布镜像之前,我们需要给它标记一个版本号:

docker tag k8s_python_sample_code:latest k8s_python_sample_code:0.1

推送镜像到一个云仓库

如果使用一个 Docker 注册中心而不是 Docker Hub 去保存镜像,那么你需要在你本地的 Docker 守护程序和 Kubernetes Docker 守护程序上添加一个容器注册中心。对于不同的云注册中心,你可以在它上面找到相关信息。我们在示例中使用的是 Docker Hub。

运行下面的 Docker 命令去推送镜像:

docker push k8s_python_sample_code

使用 CephFS 持久卷

Kubernetes 支持许多的持久存储提供商,包括 AWS EBS、CephFS、GlusterFS、Azure Disk、NFS 等等。我在示例中使用 CephFS 做为 Kubernetes 的持久卷。

为使用 CephFS 存储 Kubernetes 的容器数据,我们将创建两个文件:

persistent-volume.yml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: app-disk1
  namespace: k8s_python_sample_code
spec:
  capacity:
  storage: 50Gi
  accessModes:
  - ReadWriteMany
  cephfs:
  monitors:
    - "172.17.0.1:6789"
  user: admin
  secretRef:
    name: ceph-secret
  readOnly: false

persistent_volume_claim.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: appclaim1
  namespace: k8s_python_sample_code
spec:
  accessModes:
  - ReadWriteMany
  resources:
  requests:
    storage: 10Gi

现在,我们将使用 kubectl 去添加持久卷并声明到 Kubernetes 集群中:

$ kubectl create -f persistent-volume.yml
$ kubectl create -f persistent-volume-claim.yml

现在,我们准备去部署 Kubernetes。

在 Kubernetes 上部署应用程序

为管理部署应用程序到 Kubernetes 上的最后一步,我们将创建两个重要文件:一个服务文件和一个部署文件。

使用下列的内容创建服务文件,并将它命名为 k8s_python_sample_code.service.yml

apiVersion: v1
kind: Service
metadata:
  labels:
  k8s-app: k8s_python_sample_code
  name: k8s_python_sample_code
  namespace: k8s_python_sample_code
spec:
  type: NodePort
  ports:
  - port: 5035
  selector:
  k8s-app: k8s_python_sample_code

使用下列的内容创建部署文件并将它命名为 k8s_python_sample_code.deployment.yml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: k8s_python_sample_code
  namespace: k8s_python_sample_code
spec:
  replicas: 1
  template:
  metadata:
    labels:
    k8s-app: k8s_python_sample_code
  spec:
    containers:
    - name: k8s_python_sample_code
      image: k8s_python_sample_code:0.1
      imagePullPolicy: "IfNotPresent"
      ports:
      - containerPort: 5035
      volumeMounts:
        - mountPath: /app-data
          name: k8s_python_sample_code
     volumes: 
         - name: <name of application>
           persistentVolumeClaim:
             claimName: appclaim1

最后,我们使用 kubectl 将应用程序部署到 Kubernetes:

$ kubectl create -f k8s_python_sample_code.deployment.yml $ kubectl create -f k8s_python_sample_code.service.yml

现在,你的应用程序已经成功部署到 Kubernetes。

你可以通过检查运行的服务来验证你的应用程序是否在运行:

kubectl get services

或许 Kubernetes 可以解决未来你部署应用程序的各种麻烦!

想学习更多关于 Python 的知识?Nanjekye 的书,和平共处的 Python 2 和 3 提供了完整的方法,让你写的代码在 Python 2 和 3 上完美运行,包括如何转换已有的 Python 2 代码为能够可靠运行在 Python 2 和 3 上的代码的详细示例。

关于作者

Joannah Nanjekye - Straight Outta 256,只要结果不问原因,充满激情的飞行员,喜欢用代码说话。关于我的更多信息


via: https://opensource.com/article/18/1/running-python-application-kubernetes

作者:Joannah Nanjekye 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

无论是研究还是娱乐,安装一个最新的显卡驱动都能提升你的计算机性能,并且使你能全方位地实现新功能。本安装指南使用 Fedora 28 的新的第三方仓库来安装 NVIDIA 驱动。它将引导您完成硬件和软件两方面的安装,并且涵盖需要让你的 NVIDIA 显卡启动和运行起来的一切知识。这个流程适用于任何支持 UEFI 的计算机和任意新的 NVIDIA 显卡。

准备

本指南依赖于下面这些材料:

  • 一台使用 UEFI 的计算机,如果你不确定你的电脑是否有这种固件,请运行 sudo dmidecode -t 0。如果输出中出现了 “UEFI is supported”,你的安装过程就可以继续了。不然的话,虽然可以在技术上更新某些电脑来支持 UEFI,但是这个过程的要求很苛刻,我们通常不建议你这么使用。
  • 一个现代的、支持 UEFI 的 NVIDIA 的显卡
  • 一个满足你的 NVIDIA 显卡的功率和接线要求的电源(有关详细信息,请参考“硬件和修改”的章节)
  • 网络连接
  • Fedora 28 系统

安装实例

这个安装示例使用的是:

硬件和修改

电源(PSU)

打开你的台式机的机箱,检查印刷在电源上的最大输出功率。然后,查看你的 NVIDIA 显卡的文档,确定推荐的最小电源功率要求(以瓦特为单位)。除此之外,检查你的显卡,看它是否需要额外的接线,例如 6 针连接器,大多数的入门级显卡只从主板获取电力,但是有一些显卡需要额外的电力,如果出现以下情况,你需要升级你的电源:

  1. 你的电源的最大输出功率低于显卡建议的最小电源功率。注意:根据一些显卡厂家的说法,比起推荐的功率,预先构建的系统可能会需要更多或更少的功率,而这取决于系统的配置。如果你使用的是一个特别耗电或者特别节能的配置,请灵活决定你的电源需求。
  2. 你的电源没有提供必须的接线口来为你的显卡供电。

电源的更换很容易,但是在你拆除你当前正在使用的电源之前,请务必注意你的接线布局。除此之外,请确保你选择的电源适合你的机箱。

CPU

虽然在大多数老机器上安装高性能的 NVIDIA 显卡是可能的,但是一个缓慢或受损的 CPU 会阻碍显卡性能的发挥,如果要计算在你的机器上瓶颈效果的影响,请点击这里。了解你的 CPU 性能来避免高性能的显卡和 CPU 无法保持匹配是很重要的。升级你的 CPU 是一个潜在的考虑因素。

主板

在继续进行之前,请确认你的主板和你选择的显卡是兼容的。你的显卡应该插在最靠近散热器的 PCI-E x16 插槽中。确保你的设置为显卡预留了足够的空间。此外,请注意,现在大部分的显卡使用的都是 PCI-E 3.0 技术。虽然这些显卡如果插在 PCI-E 3.0 插槽上会运行地最好,但如果插在一个旧版的插槽上的话,性能也不会受到太大的影响。

安装

1、 首先,打开终端更新你的包管理器(如果没有更新的话):

sudo dnf update

2、 然后,使用这条简单的命令进行重启:

reboot

3、 在重启之后,安装 Fedora 28 的工作站的仓库:

sudo dnf install fedora-workstation-repositories

4、 接着,设置 NVIDIA 驱动的仓库:

sudo dnf config-manager --set-enabled rpmfusion-nonfree-nvidia-driver

5、 然后,再次重启。

6、 在这次重启之后,通过下面这条命令验证是否添加了仓库:

sudo dnf repository-packages rpmfusion-nonfree-nvidia-driver info

如果加载了多个 NVIDIA 工具和它们各自的 spec 文件,请继续进行下一步。如果没有,你可能在添加新仓库的时候遇到了一个错误。你应该再试一次。

7、 登录,连接到互联网,然后打开“软件”应用程序。点击“加载项>硬件驱动> NVIDIA Linux 图形驱动>安装”。

如果你使用更老的显卡或者想使用多个显卡,请进一步查看 RPMFusion 指南。最后,要确保启动成功,设置 /etc/gdm/custom.conf 中的 WaylandEnable=false,确认避免使用安全启动。 接着,再一次重启。

8、这个过程完成后,关闭所有的应用并关机。拔下电源插头,然后按下电源按钮以释放余电,避免你被电击。如果你对电源有开关,关闭它。

9、 最后,安装显卡,拔掉老的显卡并将新的显卡插入到正确的 PCI-E x16 插槽中。成功安装新的显卡之后,关闭你的机箱,插入电源 ,然后打开计算机,它应该会成功启动。

注意: 要禁用此安装中使用的 NVIDIA 驱动仓库,或者要禁用所有的 Fedora 工作站仓库,请参考这个 Fedora Wiki 页面

验证

1、 如果你新安装的 NVIDIA 显卡已连接到你的显示器并显示正确,则表明你的 NVIDIA 驱动程序已成功和显卡建立连接。

如果你想去查看你的设置,或者验证驱动是否在正常工作(这里,主板上安装了两块显卡),再次打开 “NVIDIA X 服务器设置应用程序”。这次,你应该不会得到错误信息提示,并且系统会给出有关 X 的设置文件和你的 NVIDIA 显卡的信息。(请参考下面的屏幕截图)

 title=

通过这个应用程序,你可以根据你的需要需改 X 配置文件,并可以监控显卡的性能,时钟速度和温度信息。

2、 为确保新显卡以满功率运行,显卡性能测试是非常必要的。GL Mark 2,是一个提供后台处理、构建、照明、纹理等等有关信息的标准工具。它提供了一个优秀的解决方案。GL Mark 2 记录了各种各样的图形测试的帧速率,然后输出一个总体的性能评分(这被称为 glmark2 分数)。

注意: glxgears 只会测试你的屏幕或显示器的性能,不会测试显卡本身,请使用 GL Mark 2。

要运行 GLMark2:

  1. 打开终端并关闭其他所有的应用程序
  2. 运行 sudo dnf install glmark2 命令
  3. 运行 glmark2 命令
  4. 允许运行完整的测试来得到最好的结果。检查帧速率是否符合你对这块显卡的预期。如果你想要额外的验证,你可以查阅网站来确认是否已有你这块显卡的 glmark2 测试评分被公布到网上,你可以比较这个分数来评估你这块显卡的性能。
  5. 如果你的帧速率或者 glmark2 评分低于预期,请思考潜在的因素。CPU 造成的瓶颈?其他问题导致?

如果诊断的结果很好,就开始享受你的新显卡吧。

参考链接


via: https://fedoramagazine.org/install-nvidia-gpu/

作者:Justice del Castillo 选题:lujun9972 译者:hopefully2333 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在内核层面上分析事件有很多的工具:SystemTapktapSysdigLTTNG 等等,你也可以在网络上找到关于这些工具的大量介绍文章和资料。

而对于使用 Linux 原生机制去跟踪系统事件以及检索/分析故障信息的方面的资料却很少找的到。这就是 ftrace,它是添加到内核中的第一款跟踪工具,今天我们来看一下它都能做什么,让我们从它的一些重要术语开始吧。

内核跟踪和分析

内核分析 Kernel profiling 可以发现性能“瓶颈”。分析能够帮我们发现在一个程序中性能损失的准确位置。特定的程序生成一个 概述 profile — 这是一个事件总结 — 它能够用于帮我们找出哪个函数占用了大量的运行时间。尽管这些程序并不能识别出为什么会损失性能。

瓶颈经常发生在无法通过分析来识别的情况下。要推断出为什么会发生事件,就必须保存发生事件时的相关上下文,这就需要去 跟踪 tracing

跟踪可以理解为在一个正常工作的系统上活动的信息收集过程。它使用特定的工具来完成这项工作,就像录音机来记录声音一样,用它来记录各种系统事件。

跟踪程序能够同时跟踪应用级和操作系统级的事件。它们收集的信息能够用于诊断多种系统问题。

有时候会将跟踪与日志比较。它们两者确时很相似,但是也有不同的地方。

对于跟踪,记录的信息都是些低级别事件。它们的数量是成百上千的,甚至是成千上万的。对于日志,记录的信息都是些高级别事件,数量上通常少多了。这些包含用户登录系统、应用程序错误、数据库事务等等。

就像日志一样,跟踪数据可以被原样读取,但是用特定的应用程序提取的信息更有用。所有的跟踪程序都能这样做。

在内核跟踪和分析方面,Linux 内核有三个主要的机制:

  • 跟踪点 tracepoint :一种基于静态测试代码的工作机制
  • 探针 kprobe :一种动态跟踪机制,用于在任意时刻中断内核代码的运行,调用它自己的处理程序,在完成需要的操作之后再返回
  • perf\_events —— 一个访问 PMU( 性能监视单元 Performance Monitoring Unit )的接口

我并不想在这里写关于这些机制方面的内容,任何对它们感兴趣的人可以去访问 Brendan Gregg 的博客

使用 ftrace,我们可以与这些机制进行交互,并可以从用户空间直接得到调试信息。下面我们将讨论这方面的详细内容。示例中的所有命令行都是在内核版本为 3.13.0-24 的 Ubuntu 14.04 中运行的。

ftrace:常用信息

ftrace 是 Function Trace 的简写,但它能做的远不止这些:它可以跟踪上下文切换、测量进程阻塞时间、计算高优先级任务的活动时间等等。

ftrace 是由 Steven Rostedt 开发的,从 2008 年发布的内核 2.6.27 中开始就内置了。这是为记录数据提供的一个调试 Ring 缓冲区的框架。这些数据由集成到内核中的跟踪程序来采集。

ftrace 工作在 debugfs 文件系统上,在大多数现代 Linux 发行版中都默认挂载了。要开始使用 ftrace,你将进入到 sys/kernel/debug/tracing 目录(仅对 root 用户可用):

# cd /sys/kernel/debug/tracing

这个目录的内容看起来应该像这样:

аvailable_filter_functions  options            stack_trace_filter
available_tracers           per_cpu             trace
buffer_size_kb              printk_formats      trace_clock
buffer_total_size_kb        README              trace_marker
current_tracer              saved_cmdlines      trace_options
dyn_ftrace_total_info       set_event           trace_pipe
enabled_functions           set_ftrace_filter   trace_stat
events                      set_ftrace_notrace  tracing_cpumask
free_buffer                 set_ftrace_pid      tracing_max_latency
function_profile_enabled    set_graph_function  tracing_on
instances                   set_graph_notrace   tracing_thresh
kprobe_events               snapshot            uprobe_events
kprobe_profile              stack_max_size      uprobe_profile

我不想去描述这些文件和子目录;它们的描述在 官方文档 中已经写的很详细了。我只想去详细介绍与我们这篇文章相关的这几个文件:

  • available\_tracers —— 可用的跟踪程序
  • current\_tracer —— 正在运行的跟踪程序
  • tracing\_on —— 负责启用或禁用数据写入到 Ring 缓冲区的系统文件(如果启用它,数字 1 被添加到文件中,禁用它,数字 0 被添加)
  • trace —— 以人类友好格式保存跟踪数据的文件

可用的跟踪程序

我们可以使用如下的命令去查看可用的跟踪程序的一个列表:

root@andrei:/sys/kernel/debug/tracing#: cat available_tracers
blk mmiotrace function_graph wakeup_rt wakeup function nop

我们来快速浏览一下每个跟踪程序的特性:

  • function —— 一个无需参数的函数调用跟踪程序
  • function\_graph —— 一个使用子调用的函数调用跟踪程序
  • blk —— 一个与块 I/O 跟踪相关的调用和事件跟踪程序(它是 blktrace 使用的)
  • mmiotrace —— 一个内存映射 I/O 操作跟踪程序
  • nop —— 最简单的跟踪程序,就像它的名字所暗示的那样,它不做任何事情(尽管在某些情况下可能会派上用场,我们将在后文中详细解释)

函数跟踪程序

在开始介绍函数跟踪程序 ftrace 之前,我们先看一个测试脚本:

#!/bin/sh

dir=/sys/kernel/debug/tracing

sysctl kernel.ftrace_enabled=1
echo function > ${dir}/current_tracer
echo 1 > ${dir}/tracing_on
sleep 1
echo 0 > ${dir}/tracing_on
less ${dir}/trace

这个脚本是非常简单的,但是还有几个需要注意的地方。命令 sysctl ftrace.enabled=1 启用了函数跟踪程序。然后我们通过写它的名字到 current_tracer 文件来启用 current tracer

接下来,我们写入一个 1tracing_on,它启用了 Ring 缓冲区。这些语法都要求在 1> 符号前后有一个空格;写成像 echo 1> tracing_on 这样将不能工作。一行之后我们禁用它(如果 0 写入到 tracing_on, 缓冲区不会被清除并且 ftrace 并不会被禁用)。

我们为什么这样做呢?在两个 echo 命令之间,我们看到了命令 sleep 1。我们启用了缓冲区,运行了这个命令,然后禁用它。这将使跟踪程序采集了这个命令运行期间发生的所有系统调用的信息。

在脚本的最后一行,我们写了一个在控制台上显示跟踪数据的命令。

一旦脚本运行完成后,我们将看到下列的输出(这里只列出了一个小片断):

# tracer: function
#
# entries-in-buffer/entries-written: 29571/29571   #P:2
#
#                           _-----=> irqs-off
#                           / _----=> need-resched
#                           | / _---=> hardirq/softirq
#                           || / _--=> preempt-depth
#                           ||| /   delay
#           TASK-PID   CPU#  ||||   TIMESTAMP  FUNCTION
#           | |     |   ||||    |       |
        trace.sh-1295  [000] ....   90.502874: mutex_unlock <-rb_simple_write
        trace.sh-1295  [000] ....   90.502875: __fsnotify_parent <-vfs_write
        trace.sh-1295  [000] ....   90.502876: fsnotify <-vfs_write
        trace.sh-1295  [000] ....   90.502876: __srcu_read_lock <-fsnotify
        trace.sh-1295  [000] ....   90.502876: __srcu_read_unlock <-fsnotify
        trace.sh-1295  [000] ....   90.502877: __sb_end_write <-vfs_write
        trace.sh-1295  [000] ....   90.502877: syscall_trace_leave <-int_check_syscall_exit_work
        trace.sh-1295  [000] ....   90.502878: context_tracking_user_exit <-syscall_trace_leave
        trace.sh-1295  [000] ....   90.502878: context_tracking_user_enter <-syscall_trace_leave
        trace.sh-1295  [000] d...   90.502878: vtime_user_enter <-context_tracking_user_enter
        trace.sh-1295  [000] d...   90.502878: _raw_spin_lock <-vtime_user_enter
        trace.sh-1295  [000] d...   90.502878: __vtime_account_system <-vtime_user_enter
        trace.sh-1295  [000] d...   90.502878: get_vtime_delta <-__vtime_account_system
        trace.sh-1295  [000] d...   90.502879: account_system_time <-__vtime_account_system
        trace.sh-1295  [000] d...   90.502879: cpuacct_account_field <-account_system_time
        trace.sh-1295  [000] d...   90.502879: acct_account_cputime <-account_system_time
        trace.sh-1295  [000] d...   90.502879: __acct_update_integrals <-acct_account_cputime

这个输出以“缓冲区中的信息条目数量”和“写入的全部条目数量”开始。这两者的数据差异是缓冲区中事件的丢失数量(在我们的示例中没有发生丢失)。

在这里有一个包含下列信息的函数列表:

  • 进程标识符(PID)
  • 运行这个进程的 CPU(CPU#)
  • 进程开始时间(TIMESTAMP)
  • 被跟踪函数的名字以及调用它的父级函数;例如,在我们输出的第一行,rb_simple_write 调用了 mutex-unlock 函数。

function\_graph 跟踪程序

function\_graph 跟踪程序的工作和函数跟踪程序一样,但是它更详细:它显示了每个函数的进入和退出点。使用这个跟踪程序,我们可以跟踪函数的子调用并且测量每个函数的运行时间。

我们来编辑一下最后一个示例的脚本:

#!/bin/sh

dir=/sys/kernel/debug/tracing

sysctl kernel.ftrace_enabled=1
echo function_graph > ${dir}/current_tracer
echo 1 > ${dir}/tracing_on
sleep 1
echo 0 > ${dir}/tracing_on
less ${dir}/trace

运行这个脚本之后,我们将得到如下的输出:

# tracer: function_graph
#
# CPU  DURATION                 FUNCTION CALLS
# |     |   |                   |   |   |   |
 0)   0.120 us  |                               } /* resched_task */
 0)   1.877 us  |                               } /* check_preempt_curr */
 0)   4.264 us  |                           } /* ttwu_do_wakeup */
 0) + 29.053 us   |                         } /* ttwu_do_activate.constprop.74 */
 0)   0.091 us  |                           _raw_spin_unlock();
 0)   0.260 us  |                           ttwu_stat();
 0)   0.133 us  |                           _raw_spin_unlock_irqrestore();
 0) + 37.785 us   |                         } /* try_to_wake_up */
 0) + 38.478 us   |                     } /* default_wake_function */
 0) + 39.203 us   |                     } /* pollwake */
 0) + 40.793 us   |                 } /* __wake_up_common */
 0)   0.104 us  |                   _raw_spin_unlock_irqrestore();
 0) + 42.920 us   |                 } /* __wake_up_sync_key */
 0) + 44.160 us   |             } /* sock_def_readable */
 0) ! 192.850 us  |             } /* tcp_rcv_established */
 0) ! 197.445 us  |         } /* tcp_v4_do_rcv */
 0)   0.113 us  |           _raw_spin_unlock();
 0) ! 205.655 us  |         } /* tcp_v4_rcv */
 0) ! 208.154 us  |     } /* ip_local_deliver_finish */

在这个图中,DURATION 展示了花费在每个运行的函数上的时间。注意使用 +! 符号标记的地方。加号(+)意思是这个函数花费的时间超过 10 毫秒;而感叹号(!)意思是这个函数花费的时间超过了 100 毫秒。

FUNCTION_CALLS 下面,我们可以看到每个函数调用的信息。

和 C 语言一样使用了花括号({)标记每个函数的边界,它展示了每个函数的开始和结束,一个用于开始,一个用于结束;不能调用其它任何函数的叶子函数用一个分号(;)标记。

函数过滤器

ftrace 输出可能会很大,精确找出你所需要的内容可能会非常困难。我们可以使用过滤器去简化我们的搜索:输出中将只显示与我们感兴趣的函数相关的信息。为实现过滤,我们只需要在 set_ftrace_filter 文件中写入我们需要过滤的函数的名字即可。例如:

root@andrei:/sys/kernel/debug/tracing# echo kfree > set_ftrace_filter

如果禁用过滤器,我们只需要在这个文件中添加一个空白行即可:

root@andrei:/sys/kernel/debug/tracing# echo  > set_ftrace_filter

通过运行这个命令:

root@andrei:/sys/kernel/debug/tracing# echo kfree > set_ftrace_notrace 

我们将得到相反的结果:输出将包含除了 kfree() 以外的任何函数的信息。

另一个有用的选项是 set_ftrace_pid。它是为在一个特定的进程运行期间调用跟踪函数准备的。

ftrace 还有很多过滤选项。对于它们更详细的介绍,你可以去查看 Steven Rostedt 在 LWN.net 上的文章。

跟踪事件

我们在上面提到到跟踪点机制。跟踪点是插入的触发系统事件的特定代码。跟踪点可以是动态的(意味着可能会在它们上面附加几个检查),也可以是静态的(意味着不会附加任何检查)。

静态跟踪点不会对系统有任何影响;它们只是在测试的函数末尾增加几个字节的函数调用以及在一个独立的节上增加一个数据结构。

当相关代码片断运行时,动态跟踪点调用一个跟踪函数。跟踪数据是写入到 Ring 缓冲区。

跟踪点可以设置在代码的任何位置;事实上,它们确实可以在许多的内核函数中找到。我们来看一下 kmem_cache_alloc 函数(取自 这里):

{
    void *ret = slab_alloc(cachep, flags, _RET_IP_);

    trace_kmem_cache_alloc(_RET_IP_, ret,
                            cachep->object_size, cachep->size, flags);
         return ret;
         }

trace_kmem_cache_alloc 它本身就是一个跟踪点。我们可以通过查看其它内核函数的源代码找到这样无数的例子。

在 Linux 内核中为了从用户空间使用跟踪点,它有一个专门的 API。在 /sys/kernel/debug/tracing 目录中,这里有一个事件目录,它是为了保存系统事件。这些只是为了跟踪系统事件。在这个上下文中系统事件可以理解为包含在内核中的跟踪点。

可以通过运行如下的命令来查看这个事件列表:

root@andrei:/sys/kernel/debug/tracing# cat available_events

这个命令将在控制台中输出一个很长的列表。这样看起来很不方便。我们可以使用如下的命令来列出一个结构化的列表:

root@andrei:/sys/kernel/debug/tracing# ls events

block           gpio        mce     random      skb     vsyscall
btrfs           header_event  migrate  ras          sock    workqueue
compaction      header_page   module   raw_syscalls  spi    writeback
context_tracking  iommu         napi    rcu         swiotlb   xen
enable          irq         net     regmap      syscalls  xfs
exceptions      irq_vectors   nmi   regulator   task    xhci-hcd
ext4            jbd2        oom     rpm         timer
filemap         kmem        pagemap  sched      udp
fs              kvm         power   scsi        vfs
ftrace          kvmmmu      printk   signal     vmscan

所有可能的事件都按子系统分组到子目录中。在我们开始跟踪事件之前,我们要先确保启用了 Ring 缓冲区写入:

root@andrei:/sys/kernel/debug/tracing# cat tracing_on

如果在控制台中显示的是数字 0,那么,我们可以运行如下的命令来启用它:

root@andrei:/sys/kernel/debug/tracing# echo 1 > tracing_on

在我们上一篇的文章中,我们写了关于 chroot() 系统调用的内容;我们来跟踪访问一下这个系统调用。对于我们的跟踪程序,我们使用 nop 因为函数跟踪程序和 function_graph 跟踪程序记录的信息太多,它包含了我们不感兴趣的事件信息。

root@andrei:/sys/kernel/debug/tracing# echo nop > current_tracer

所有事件相关的系统调用都保存在系统调用目录下。在这里我们将找到一个进入和退出各种系统调用的目录。我们需要在相关的文件中通过写入数字 1 来激活跟踪点:

root@andrei:/sys/kernel/debug/tracing# echo 1 > events/syscalls/sys_enter_chroot/enable

然后我们使用 chroot 来创建一个独立的文件系统(更多内容,请查看 之前这篇文章)。在我们执行完我们需要的命令之后,我们将禁用跟踪程序,以便于不需要的信息或者过量信息不会出现在输出中:

root@andrei:/sys/kernel/debug/tracing# echo 0 > tracing_on

然后,我们去查看 Ring 缓冲区的内容。在输出的结束部分,我们找到了有关的系统调用信息(这里只是一个节选)。

root@andrei:/sys/kernel/debug/tracing# сat trace

......
          chroot-11321 [000] ....  4606.265208: sys_chroot(filename: 7fff785ae8c2)
          chroot-11325 [000] ....  4691.677767: sys_chroot(filename: 7fff242308cc)
            bash-11338 [000] ....  4746.971300: sys_chroot(filename: 7fff1efca8cc)
            bash-11351 [000] ....  5379.020609: sys_chroot(filename: 7fffbf9918cc)

关于配置事件跟踪的更的信息可以在 这里 找到。

结束语

在这篇文篇中,我们做了一个 ftrace 的功能概述。我们非常感谢你的任何意见或者补充。如果你想深入研究这个主题,我们为你推荐下列的资源:


via:https://blog.selectel.com/kernel-tracing-ftrace/

作者:Andrej Yemelianov 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

进入这个古怪而神奇的 Bash 数组的世界。

尽管软件工程师常常使用命令行来进行各种开发,但命令行中的数组似乎总是一个模糊的东西(虽然不像正则操作符 =~ 那么复杂隐晦)。除开隐晦和有疑问的语法,Bash 数组其实是非常有用的。

稍等,这是为什么?

写 Bash 相关的东西很难,但如果是写一篇像手册那样注重怪异语法的文章,就会非常简单。不过请放心,这篇文章的目的就是让你不用去读该死的使用手册。

真实(通常是有用的)示例

为了这个目的,想象一下真实世界的场景以及 Bash 是怎么帮忙的:你正在公司里面主导一个新工作,评估并优化内部数据管线的运行时间。首先,你要做个参数扫描分析来评估管线使用线程的状况。简单起见,我们把这个管道当作一个编译好的 C++ 黑盒子,这里面我们能够调整的唯一的参数是用于处理数据的线程数量:./pipeline --threads 4

基础

我们首先要做的事是定义一个数组,用来容纳我们想要测试的 --threads 参数:

allThreads=(1 2 4 8 16 32 64 128)

本例中,所有元素都是数字,但参数并不一定是数字,Bash 中的数组可以容纳数字和字符串,比如 myArray=(1 2 "three" 4 "five") 就是个有效的表达式。就像 Bash 中其它的变量一样,确保赋值符号两边没有空格。否则 Bash 将会把变量名当作程序来执行,把 = 当作程序的第一个参数。

现在我们初始化了数组,让我们解析它其中的一些元素。仅仅输入 echo $allThreads ,你能发现,它只会输出第一个元素。

要理解这个产生的原因,需要回到上一步,回顾我们一般是怎么在 Bash 中输出变量。考虑以下场景:

type="article"
echo "Found 42 $type"

假如我们得到的变量 $type 是一个单词,我们想要添加在句子结尾一个 s。我们无法直接把 s 加到 $type 里面,因为这会把它变成另一个变量,$types。尽管我们可以利用像 echo "Found 42 "$type"s" 这样的代码形变,但解决这个问题的最好方法是用一个花括号:echo "Found 42 ${type}s",这让我们能够告诉 Bash 变量名的起止位置(有趣的是,JavaScript/ES6 在 template literals 中注入变量和表达式的语法和这里是一样的)

事实上,尽管 Bash 变量一般不用花括号,但在数组中需要用到花括号。这反而允许我们指定要访问的索引,例如 echo ${allThreads[1]} 返回的是数组中的第二个元素。如果不写花括号,比如 echo $allThreads[1],会导致 Bash 把 [1] 当作字符串然后输出。

是的,Bash 数组的语法很怪,但是至少他们是从 0 开始索引的,不像有些语言(说的就是你,R 语言)。

遍历数组

上面的例子中我们直接用整数作为数组的索引,我们现在考虑两种其他情况:第一,如果想要数组中的第 $i 个元素,这里 $i 是一个代表索引的变量,我们可以这样 echo ${allThreads[$i]} 解析这个元素。第二,要输出一个数组的所有元素,我们把数字索引换成 @ 符号(你可以把 @ 当作表示 all 的符号):echo ${allThreads[@]}

遍历数组元素

记住上面讲过的,我们遍历 $allThreads 数组,把每个值当作 --threads 参数启动管线:

for t in ${allThreads[@]}; do
  ./pipeline --threads $t
done

遍历数组索引

接下来,考虑一个稍稍不同的方法。不遍历所有的数组元素,我们可以遍历所有的索引:

for i in ${!allThreads[@]}; do
  ./pipeline --threads ${allThreads[$i]}
done

一步一步看:如之前所见,${allThreads[@]} 表示数组中的所有元素。前面加了个感叹号,变成 ${!allThreads[@]},这会返回数组索引列表(这里是 0 到 7)。换句话说。for 循环就遍历所有的索引 $i 并从 $allThreads 中读取第 $i 个元素,当作 --threads 选项的参数。

这看上去很辣眼睛,你可能奇怪为什么我要一开始就讲这个。这是因为有时候在循环中需要同时获得索引和对应的值,例如,如果你想要忽视数组中的第一个元素,使用索引可以避免额外创建在循环中累加的变量。

填充数组

到目前为止,我们已经能够用给定的 --threads 选项启动管线了。现在假设按秒计时的运行时间输出到管线。我们想要捕捉每个迭代的输出,然后把它保存在另一个数组中,因此我们最终可以随心所欲的操作它。

一些有用的语法

在深入代码前,我们要多介绍一些语法。首先,我们要能解析 Bash 命令的输出。用这个语法可以做到:output=$( ./my_script.sh ),这会把命令的输出存储到变量 $output 中。

我们需要的第二个语法是如何把我们刚刚解析的值添加到数组中。完成这个任务的语法看起来很熟悉:

myArray+=( "newElement1" "newElement2" )

参数扫描

万事具备,执行参数扫描的脚步如下:

allThreads=(1 2 4 8 16 32 64 128)
allRuntimes=()
for t in ${allThreads[@]}; do
  runtime=$(./pipeline --threads $t)
  allRuntimes+=( $runtime )
done

就是这个了!

还有什么能做的?

这篇文章中,我们讲过使用数组进行参数扫描的场景。我敢保证有很多理由要使用 Bash 数组,这里就有两个例子:

日志警告

本场景中,把应用分成几个模块,每一个都有它自己的日志文件。我们可以编写一个 cron 任务脚本,当某个模块中出现问题标志时向特定的人发送邮件:

# 日志列表,发生问题时应该通知的人
logPaths=("api.log" "auth.log" "jenkins.log" "data.log")
logEmails=("jay@email" "emma@email" "jon@email" "sophia@email")

# 在每个日志中查找问题标志
for i in ${!logPaths[@]};
do
  log=${logPaths[$i]}
  stakeholder=${logEmails[$i]}
  numErrors=$( tail -n 100 "$log" | grep "ERROR" | wc -l )

  # 如果近期发现超过 5 个错误,就警告负责人
  if [[ "$numErrors" -gt 5 ]];
  then
    emailRecipient="$stakeholder"
    emailSubject="WARNING: ${log} showing unusual levels of errors"
    emailBody="${numErrors} errors found in log ${log}"
    echo "$emailBody" | mailx -s "$emailSubject" "$emailRecipient"
  fi
done

API 查询

如果你想要生成一些分析数据,分析你的 Medium 帖子中用户评论最多的。由于我们无法直接访问数据库,SQL 不在我们考虑范围,但我们可以用 API!

为了避免陷入关于 API 授权和令牌的冗长讨论,我们将会使用 JSONPlaceholder,这是一个面向公众的测试服务 API。一旦我们查询每个帖子,解析出每个评论者的邮箱,我们就可以把这些邮箱添加到我们的结果数组里:

endpoint="https://jsonplaceholder.typicode.com/comments"
allEmails=()

# 查询前 10 个帖子
for postId in {1..10};
do
  # 执行 API 调用,获取该帖子评论者的邮箱
  response=$(curl "${endpoint}?postId=${postId}")
  
  # 使用 jq 把 JSON 响应解析成数组
  allEmails+=( $( jq '.[].email' <<< "$response" ) )
done

注意这里我是用 jq 工具 从命令行里解析 JSON 数据。关于 jq 的语法超出了本文的范围,但我强烈建议你了解它。

你可能已经想到,使用 Bash 数组在数不胜数的场景中很有帮助,我希望这篇文章中的示例可以给你思维的启发。如果你从自己的工作中找到其它的例子想要分享出来,请在帖子下方评论。

请等等,还有很多东西!

由于我们在本文讲了很多数组语法,这里是关于我们讲到内容的总结,包含一些还没讲到的高级技巧:

语法效果
arr=()创建一个空数组
arr=(1 2 3)初始化数组
${arr[2]}取得第三个元素
${arr[@]}取得所有元素
${!arr[@]}取得数组索引
${#arr[@]}计算数组长度
arr[0]=3覆盖第 1 个元素
arr+=(4)添加值
str=$(ls)ls 输出保存到字符串
arr=( $(ls) )ls 输出的文件保存到数组里
${arr[@]:s:n}取得从索引 s 开始的 n 个元素

最后一点思考

正如我们所见,Bash 数组的语法很奇怪,但我希望这篇文章让你相信它们很有用。只要你理解了这些语法,你会发现以后会经常使用 Bash 数组。

Bash 还是 Python?

问题来了:什么时候该用 Bash 数组而不是其他的脚本语法,比如 Python?

对我而言,完全取决于需求——如果你可以只需要调用命令行工具就能立马解决问题,你也可以用 Bash。但有些时候,当你的脚本属于一个更大的 Python 项目时,你也可以用 Python。

比如,我们可以用 Python 来实现参数扫描,但我们只用编写一个 Bash 的包装:

import subprocess

all_threads = [1, 2, 4, 8, 16, 32, 64, 128]
all_runtimes = []

# 用不同的线程数字启动管线
for t in all_threads:
  cmd = './pipeline --threads {}'.format(t)

  # 使用子线程模块获得返回的输出
  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
  output = p.communicate()[0]
  all_runtimes.append(output)

由于本例中没法避免使用命令行,所以可以优先使用 Bash。

羞耻的宣传时间

如果你喜欢这篇文章,这里还有很多类似的文章! 在此注册,加入 OSCON,2018 年 7 月 17 号我会在这做一个主题为 你所不了解的 Bash 的在线编码研讨会。没有幻灯片,不需要门票,只有你和我在命令行里面敲代码,探索 Bash 中的奇妙世界。

本文章由 [Medium] 首发,再发布时已获得授权。


via: https://opensource.com/article/18/5/you-dont-know-bash-intro-bash-arrays

作者:Robert Aboukhalil 选题:lujun9972 译者:BriFuture 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Buildah 提供一种灵活、可脚本编程的方式,来使用你熟悉的工具创建精简、高效的容器镜像。

Buildah 是一个命令行工具,可以方便、快捷的构建与 开放容器标准 Open Container Initiative (OCI)兼容的容器镜像,这意味着其构建的镜像与 Docker 和 Kubernetes 兼容。该工具可作为 Docker 守护进程 docker build 命令(即使用传统的 Dockerfile 构建镜像)的一种 简单 drop-in 替换,而且更加灵活,允许构建镜像时使用你擅长的工具。Buildah 可以轻松与脚本集成并生成 流水线 pipeline ,最好之处在于构建镜像不再需要运行容器守护进程(LCTT 译注:这里主要是指 Docker 守护进程)。

docker build 的简单替换

目前你可能使用 Dockerfile 和 docker build 命令构建镜像,那么你可以马上使用 Buildah 进行替代。Buildah 的 build-using-dockerfile (或 bud)子命令与 docker build 基本等价,因此可以轻松的与已有脚本结合或构建流水线。

类似我的上一篇关于 Buildah 的文章,我也将以使用源码安装 “GNU Hello” 为例进行说明,对应的 Dockerfile 文件如下:

FROM fedora:28
LABEL maintainer Chris Collins <[email protected]>

RUN dnf install -y tar gzip gcc make \
        && dnf clean all

ADD http://ftpmirror.gnu.org/hello/hello-2.10.tar.gz /tmp/hello-2.10.tar.gz

RUN tar xvzf /tmp/hello-2.10.tar.gz -C /opt

WORKDIR /opt/hello-2.10

RUN ./configure
RUN make
RUN make install
RUN hello -v
ENTRYPOINT "/usr/local/bin/hello"

使用 Buildah 从 Dockerfile 构建镜像也很简单,使用 buildah bud -t hello . 替换 docker build -t hello . 即可:

[chris@krang] $ sudo buildah bud -t hello .
STEP 1: FROM fedora:28
Getting image source signatures
Copying blob sha256:e06fd16225608e5b92ebe226185edb7422c3f581755deadf1312c6b14041fe73
 81.48 MiB / 81.48 MiB [====================================================] 8s
Copying config sha256:30190780b56e33521971b0213810005a69051d720b73154c6e473c1a07ebd609
 2.29 KiB / 2.29 KiB [======================================================] 0s
Writing manifest to image destination
Storing signatures
STEP 2: LABEL maintainer Chris Collins <[email protected]>
STEP 3: RUN dnf install -y tar gzip gcc make    && dnf clean all

<考虑篇幅,略去后续输出>

镜像构建完毕后,可以使用 buildah images 命令查看这个新镜像:

[chris@krang] $ sudo buildah images
IMAGE ID        IMAGE NAME                              CREATED AT              SIZE
30190780b56e    docker.io/library/fedora:28             Mar 7, 2018 16:53       247 MB
6d54bef73e63    docker.io/library/hello:latest    May 3, 2018 15:24     391.8 MB

新镜像的标签为 hello:latest,我们可以将其推送至远程镜像仓库,可以使用 CRI-O 或其它 Kubernetes CRI 兼容的运行时来运行该镜像,也可以推送到远程仓库。如果你要测试对 Docker build 命令的替代性,你可以将镜像拷贝至 docker 守护进程的本地镜像存储中,这样 Docker 也可以使用该镜像。使用 buildah push 可以很容易的完成推送操作:

[chris@krang] $ sudo buildah push hello:latest docker-daemon:hello:latest
Getting image source signatures
Copying blob sha256:72fcdba8cff9f105a61370d930d7f184702eeea634ac986da0105d8422a17028
 247.02 MiB / 247.02 MiB [==================================================] 2s
Copying blob sha256:e567905cf805891b514af250400cc75db3cb47d61219750e0db047c5308bd916
 144.75 MiB / 144.75 MiB [==================================================] 1s
Copying config sha256:6d54bef73e638f2e2dd8b7bf1c4dfa26e7ed1188f1113ee787893e23151ff3ff
 1.59 KiB / 1.59 KiB [======================================================] 0s
Writing manifest to image destination
Storing signatures

[chris@krang] $ sudo docker images | head -n2
REPOSITORY              TAG             IMAGE ID        CREATED                 SIZE
docker.io/hello      latest       6d54bef73e63  2 minutes ago   398 MB

[chris@krang] $ sudo docker run -t hello:latest
Hello, world!

若干差异

与 Docker build 不同,Buildah 不会自动的将 Dockerfile 中的每条指令产生的变更提到新的 分层 layer 中,只是简单的每次从头到尾执行构建。类似于 自动化 automation 流水线构建 build pipeline ,这种 无缓存构建 non-cached 方式的好处是可以提高构建速度,在指令较多时尤为明显。从 自动部署 automated deployment 持续交付 continuous delivery 的视角来看,使用这种方式可以快速的将新变更落实到生产环境中。

但从实际角度出发,缓存机制的缺乏对镜像开发不利,毕竟缓存层可以避免一遍遍的执行构建,从而显著的节省时间。自动分层只在 build-using-dockerfile 命令中生效。但我们在下面会看到,Buildah 原生命令允许我们选择将变更提交到硬盘的时间,提高了开发的灵活性。

Buildah 原生命令

Buildah 真正 有趣之处在于它的原生命令,你可以在容器构建过程中使用这些命令进行交互。相比与使用 build-using-dockerfile/bud 命令执行每次构建,Buildah 提供命令让你可以与构建过程中的临时容器进行交互。(Docker 也使用临时或 中间 intermediate 容器,但你无法在镜像构建过程中与其交互。)

还是使用 “GNU Hello” 为例,考虑使用如下 Buildah 命令构建的镜像:

#!/usr/bin/env bash

set -o errexit

# Create a container
container=$(buildah from fedora:28)

# Labels are part of the "buildah config" command
buildah config --label maintainer="Chris Collins <[email protected]>" $container

# Grab the source code outside of the container
curl -sSL http://ftpmirror.gnu.org/hello/hello-2.10.tar.gz -o hello-2.10.tar.gz

buildah copy $container hello-2.10.tar.gz /tmp/hello-2.10.tar.gz

buildah run $container dnf install -y tar gzip gcc make
buildah run $container dnf clean all
buildah run $container tar xvzf /tmp/hello-2.10.tar.gz -C /opt

# Workingdir is also a "buildah config" command
buildah config --workingdir /opt/hello-2.10 $container

buildah run $container ./configure
buildah run $container make
buildah run $container make install
buildah run $container hello -v

# Entrypoint, too, is a “buildah config” command
buildah config --entrypoint /usr/local/bin/hello $container

# Finally saves the running container to an image
buildah commit --format docker $container hello:latest

我们可以一眼看出这是一个 Bash 脚本而不是 Dockerfile。基于 Buildah 的原生命令,可以轻易的使用任何脚本语言或你擅长的自动化工具编写脚本。形式可以是 makefile、Python 脚本或其它你擅长的类型。

这个脚本做了哪些工作呢?首先,Buildah 命令 container=$(buildah from fedora:28) 基于 fedora:28 镜像创建了一个正在运行的容器,将容器名(buildah from 命令的返回值)保存到变量中,便于后续使用。后续所有命令都是有 $container 变量指明需要操作的容器。这些命令的功能大多可以从名称看出:buildah copy 将文件拷贝至容器,buildah run 会在容器中执行命令。可以很容易的将上述命令与 Dockerfile 中的指令对应起来。

最后一条命令 buildah commit 将容器提交到硬盘上的镜像中。当不使用 Dockerfile 而是使用 Buildah 命令构建镜像时,你可以使用 commit 命令决定何时保存变更。在上例中,所有的变更是一起提交的;但也可以增加中间提交,让你可以选择作为起点的 缓存点 cache point 。(例如,执行完 dnf install 命令后将变更缓存到硬盘是特别有意义的,一方面因为该操作耗时较长,另一方面每次执行的结果也确实相同。)

挂载点,安装目录以及 chroot

另一个可以大大增加构建镜像灵活性的 Buildah 命令是 buildah mount,可以将容器的根目录挂载到你主机的一个挂载点上。例如:

[chris@krang] $ container=$(sudo buildah from fedora:28)
[chris@krang] $ mountpoint=$(sudo buildah mount ${container})
[chris@krang] $ echo $mountpoint
/var/lib/containers/storage/overlay2/463eda71ec74713d8cebbe41ee07da5f6df41c636f65139a7bd17b24a0e845e3/merged
[chris@krang] $ cat ${mountpoint}/etc/redhat-release
Fedora release 28 (Twenty Eight)
[chris@krang] $ ls ${mountpoint}
bin   dev  home  lib64          media  opt   root  sbin  sys  usr
boot  etc  lib   lost+found  mnt        proc  run   srv   tmp  var

这太棒了,你可以通过与挂载点交互对容器镜像进行修改。这允许你使用主机上的工具进行构建和安装软件,不用将这些构建工具打包到容器镜像本身中。例如,在我们上面的 Bash 脚本中,我们需要安装 tar、Gzip、GCC 和 make,在容器内编译 “GNU Hello”。如果使用挂载点,我仍使用同样的工具进行构建,但下载的压缩包和 tar、Gzip 等 RPM 包都在主机而不是容器和生成的镜像内:

#!/usr/bin/env bash

set -o errexit

# Create a container
container=$(buildah from fedora:28)
mountpoint=$(buildah mount $container)

buildah config --label maintainer="Chris Collins <[email protected]>" $container

curl -sSL http://ftpmirror.gnu.org/hello/hello-2.10.tar.gz \
     -o /tmp/hello-2.10.tar.gz
tar xvzf src/hello-2.10.tar.gz -C ${mountpoint}/opt

pushd ${mountpoint}/opt/hello-2.10
./configure
make
make install DESTDIR=${mountpoint}
popd

chroot $mountpoint bash -c "/usr/local/bin/hello -v"

buildah config --entrypoint "/usr/local/bin/hello" $container
buildah commit --format docker $container hello
buildah unmount $container

在上述脚本中,需要提到如下几点:

  1. curl 命令将压缩包下载到主机中,而不是镜像中;
  2. (主机中的) tar 命令将压缩包中的源代码解压到容器的 /opt 目录;
  3. configuremakemake install 命令都在主机的挂载点目录中执行,而不是在容器内;
  4. 这里的 chroot 命令用于将挂载点本身当作根路径并测试 "hello" 是否正常工作;类似于前面例子中用到的 buildah run 命令。

这个脚本更加短小,使用大多数 Linux 爱好者都很熟悉的工具,最后生成的镜像也更小(没有 tar 包,没有额外的软件包等)。你甚至可以使用主机系统上的包管理器为容器安装软件。例如,(出于某种原因)你希望安装 GNU Hello 的同时在容器中安装 NGINX

[chris@krang] $ mountpoint=$(sudo buildah mount ${container})
[chris@krang] $ sudo dnf install nginx --installroot $mountpoint
[chris@krang] $ sudo chroot $mountpoint nginx -v
nginx version: nginx/1.12.1

在上面的例子中,DNF 使用 --installroot 参数将 NGINX 安装到容器中,可以通过 chroot 进行校验。

快来试试吧!

Buildah 是一种轻量级、灵活的容器镜像构建方法,不需要在主机上运行完整的 Docker 守护进程。除了提供基于 Dockerfile 构建容器的开箱即用支持,Buildah 还可以很容易的与脚本或你喜欢的构建工具相结合,特别是可以使用主机上已有的工具构建容器镜像。Buildah 生成的容器体积更小,更便于网络传输,占用更小的存储空间,而且潜在的受攻击面更小。快来试试吧!

阅读相关的故事,[使用 Buildah 创建小体积的容器]


via: https://opensource.com/article/18/6/getting-started-buildah

作者:Chris Collins 选题:lujun9972 译者:pinewall 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

本周工作中,我花了整整一周的时间来尝试调试一个段错误。我以前从来没有这样做过,我花了很长时间才弄清楚其中涉及的一些基本事情(获得核心转储、找到导致段错误的行号)。于是便有了这篇博客来解释如何做那些事情!

在看完这篇博客后,你应该知道如何从“哦,我的程序出现段错误,但我不知道正在发生什么”到“我知道它出现段错误时的堆栈、行号了! ”。

什么是段错误?

段错误 segmentation fault ”是指你的程序尝试访问不允许访问的内存地址的情况。这可能是由于:

  • 试图解引用空指针(你不被允许访问内存地址 0);
  • 试图解引用其他一些不在你内存(LCTT 译注:指不在合法的内存地址区间内)中的指针;
  • 一个已被破坏并且指向错误的地方的 C++ 虚表指针 C++ vtable pointer ,这导致程序尝试执行没有执行权限的内存中的指令;
  • 其他一些我不明白的事情,比如我认为访问未对齐的内存地址也可能会导致段错误(LCTT 译注:在要求自然边界对齐的体系结构,如 MIPS、ARM 中更容易因非对齐访问产生段错误)。

这个“C++ 虚表指针”是我的程序发生段错误的情况。我可能会在未来的博客中解释这个,因为我最初并不知道任何关于 C++ 的知识,并且这种虚表查找导致程序段错误的情况也是我所不了解的。

但是!这篇博客后不是关于 C++ 问题的。让我们谈论的基本的东西,比如,我们如何得到一个核心转储?

步骤1:运行 valgrind

我发现找出为什么我的程序出现段错误的最简单的方式是使用 valgrind:我运行

valgrind -v your-program

这给了我一个故障时的堆栈调用序列。 简洁!

但我想也希望做一个更深入调查,并找出些 valgrind 没告诉我的信息! 所以我想获得一个核心转储并探索它。

如何获得一个核心转储

核心转储 core dump 是您的程序内存的一个副本,并且当您试图调试您的有问题的程序哪里出错的时候它非常有用。

当您的程序出现段错误,Linux 的内核有时会把一个核心转储写到磁盘。 当我最初试图获得一个核心转储时,我很长一段时间非常沮丧,因为 - Linux 没有生成核心转储!我的核心转储在哪里?

这就是我最终做的事情:

  1. 在启动我的程序之前运行 ulimit -c unlimited
  2. 运行 sudo sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t

ulimit:设置核心转储的最大尺寸

ulimit -c 设置核心转储的最大尺寸。 它往往设置为 0,这意味着内核根本不会写核心转储。 它以千字节为单位。 ulimit 是按每个进程分别设置的 —— 你可以通过运行 cat /proc/PID/limit 看到一个进程的各种资源限制。

例如这些是我的系统上一个随便一个 Firefox 进程的资源限制:

$ cat /proc/6309/limits 
Limit                     Soft Limit           Hard Limit           Units     
Max cpu time              unlimited            unlimited            seconds   
Max file size             unlimited            unlimited            bytes     
Max data size             unlimited            unlimited            bytes     
Max stack size            8388608              unlimited            bytes     
Max core file size        0                    unlimited            bytes     
Max resident set          unlimited            unlimited            bytes     
Max processes             30571                30571                processes 
Max open files            1024                 1048576              files     
Max locked memory         65536                65536                bytes     
Max address space         unlimited            unlimited            bytes     
Max file locks            unlimited            unlimited            locks     
Max pending signals       30571                30571                signals   
Max msgqueue size         819200               819200               bytes     
Max nice priority         0                    0                    
Max realtime priority     0                    0                    
Max realtime timeout      unlimited            unlimited            us   

内核在决定写入多大的核心转储文件时使用 软限制 soft limit (在这种情况下,max core file size = 0)。 您可以使用 shell 内置命令 ulimitulimit -c unlimited) 将软限制增加到 硬限制 hard limit

kernel.core\_pattern:核心转储保存在哪里

kernel.core_pattern 是一个内核参数,或者叫 “sysctl 设置”,它控制 Linux 内核将核心转储文件写到磁盘的哪里。

内核参数是一种设定您的系统全局设置的方法。您可以通过运行 sysctl -a 得到一个包含每个内核参数的列表,或使用 sysctl kernel.core_pattern 来专门查看 kernel.core_pattern 设置。

所以 sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t 将核心转储保存到目录 /tmp 下,并以 core 加上一系列能够标识(出故障的)进程的参数构成的后缀为文件名。

如果你想知道这些形如 %e%p 的参数都表示什么,请参考 man core

有一点很重要,kernel.core_pattern 是一个全局设置 —— 修改它的时候最好小心一点,因为有可能其它系统功能依赖于把它被设置为一个特定的方式(才能正常工作)。

kernel.core\_pattern 和 Ubuntu

默认情况下在 ubuntu 系统中,kernel.core_pattern 被设置为下面的值:

$ sysctl kernel.core_pattern
kernel.core_pattern = |/usr/share/apport/apport %p %s %c %d %P

这引起了我的迷惑(这 apport 是干什么的,它对我的核心转储做了什么?)。以下关于这个我了解到的:

  • Ubuntu 使用一种叫做 apport 的系统来报告 apt 包有关的崩溃信息。
  • 设定 kernel.core_pattern=|/usr/share/apport/apport %p %s %c %d %P 意味着核心转储将被通过管道送给 apport 程序。
  • apport 的日志保存在文件 /var/log/apport.log 中。
  • apport 默认会忽略来自不属于 Ubuntu 软件包一部分的二进制文件的崩溃信息

我最终只是跳过了 apport,并把 kernel.core_pattern 重新设置为 sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t,因为我在一台开发机上,我不在乎 apport 是否工作,我也不想尝试让 apport 把我的核心转储留在磁盘上。

现在你有了核心转储,接下来干什么?

好的,现在我们了解了 ulimitkernel.core_pattern ,并且实际上在磁盘的 /tmp 目录中有了一个核心转储文件。太好了!接下来干什么?我们仍然不知道该程序为什么会出现段错误!

下一步将使用 gdb 打开核心转储文件并获取堆栈调用序列。

从 gdb 中得到堆栈调用序列

你可以像这样用 gdb 打开一个核心转储文件:

$ gdb -c my_core_file

接下来,我们想知道程序崩溃时的堆栈是什么样的。在 gdb 提示符下运行 bt 会给你一个 调用序列 backtrace 。在我的例子里,gdb 没有为二进制文件加载符号信息,所以这些函数名就像 “??????”。幸运的是,(我们通过)加载符号修复了它。

下面是如何加载调试符号。

symbol-file /path/to/my/binary
sharedlibrary

这从二进制文件及其引用的任何共享库中加载符号。一旦我这样做了,当我执行 bt 时,gdb 给了我一个带有行号的漂亮的堆栈跟踪!

如果你想它能工作,二进制文件应该以带有调试符号信息的方式被编译。在试图找出程序崩溃的原因时,堆栈跟踪中的行号非常有帮助。:)

查看每个线程的堆栈

通过以下方式在 gdb 中获取每个线程的调用栈!

thread apply all bt full

gdb + 核心转储 = 惊喜

如果你有一个带调试符号的核心转储以及 gdb,那太棒了!您可以上下查看调用堆栈(LCTT 译注:指跳进调用序列不同的函数中以便于查看局部变量),打印变量,并查看内存来得知发生了什么。这是最好的。

如果您仍然正在基于 gdb 向导来工作上,只打印出栈跟踪与bt也可以。 :)

ASAN

另一种搞清楚您的段错误的方法是使用 AddressSanitizer 选项编译程序(“ASAN”,即 $CC -fsanitize=address)然后运行它。 本文中我不准备讨论那个,因为本文已经相当长了,并且在我的例子中打开 ASAN 后段错误消失了,可能是因为 ASAN 使用了一个不同的内存分配器(系统内存分配器,而不是 tcmalloc)。

在未来如果我能让 ASAN 工作,我可能会多写点有关它的东西。(LCTT 译注:这里指使用 ASAN 也能复现段错误)

从一个核心转储得到一个堆栈跟踪真的很亲切!

这个博客听起来很多,当我做这些的时候很困惑,但说真的,从一个段错误的程序中获得一个堆栈调用序列不需要那么多步骤:

  1. 试试用 valgrind

如果那没用,或者你想要拿到一个核心转储来调查:

  1. 确保二进制文件编译时带有调试符号信息;
  2. 正确的设置 ulimitkernel.core_pattern
  3. 运行程序;
  4. 一旦你用 gdb 调试核心转储了,加载符号并运行 bt
  5. 尝试找出发生了什么!

我可以使用 gdb 弄清楚有个 C++ 的虚表条目指向一些被破坏的内存,这有点帮助,并且使我感觉好像更懂了 C++ 一点。也许有一天我们会更多地讨论如何使用 gdb 来查找问题!


via: https://jvns.ca/blog/2018/04/28/debugging-a-segfault-on-linux/

作者:Julia Evans 译者:stephenxs 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出