侧边栏壁纸
博主头像
我的学习心得 博主等级

行动起来,活在当下

  • 累计撰写 223 篇文章
  • 累计创建 60 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

py-docker.Container 类赏析

Administrator
2024-11-28 / 0 评论 / 0 点赞 / 157 阅读 / 0 字

官方文档: https://docker-py.readthedocs.io/en/stable/

Container

Container 类是对本地容器的封装。

实例属性

attrs

dict 类型,内容与 docker inspect 的结果一致。

此属性的结果是可缓存的:多次调用 .attrs 获取的是同一个对象,可通过 id() 函数验证。当调用 .reload() 方法时 .attrs 字典对象会发生变化:

import docker
client = docker.from_env()
z = client.containers.get("zealous_wiles")
z.attrs ## {'Id': 'b3a13392baa3aec99e7fab29cf551ee654ce6d8b11fe7de9e0e47ca3cb383c30', ...
type(z.attrs) ## <class 'dict'>
id(z.attrs) ## 140522907583936
id(z.attrs) ## 140522907583936
z.reload()
id(z.attrs) ## 140522907594816

id

64 位完整 ID

z.id ## 'b3a13392baa3aec99e7fab29cf551ee654ce6d8b11fe7de9e0e47ca3cb383c30'

short_id

12 位短 ID

z.short_id ## 'b3a13392baa3'

name

z.name ## 'zealous_wiles'

image

容器镜像,docker.models.images.Image 实例。

z.image ## <Image: 'ubuntu:focal'>
type(z.image) ## <class 'docker.models.images.Image'>

labels

容器标签,dict 类型。

z.labels ## {'org.opencontainers.image.ref.name': 'ubuntu', 'org.opencontainers.image.version': '20.04'}

status

容器状态,str 类型。未做封装,直接返回服务端的原始值。

z.status ## 'running'

实例方法

attach(**kwargs)

Attach to this container. (似乎与 .logs() 方法差不多,后者参数更全面)

  • stdout (bool): 返回结果包含 stdout
  • stderr (bool): 返回结果包含 stderr
  • stream (bool): 是一次性返回容器的所有输出,还是返回一个迭代器增量获取输出
  • logs (bool): 返回结果包含之前的输出

返回结果取决于 stream 参数的取值:bytes | Iterator[bytes]

服务端可能会抛出 docker.errors.APIError 异常。

attach_socket(**kwargs)

Like attach(), but returns the underlying socket-like object for the HTTP request.

  • params (dict): 这个参数字典会传递给 attach() 方法
  • ws (bool): Use websockets instead of raw HTTP.

服务端可能会抛出 docker.errors.APIError 异常。

暂时不知道这个方法的具体用法。

z.attach_socket() ## <socket.SocketIO object at 0x7fce09e20fd0>
z.attach_socket(ws=True) ## ValueError: scheme http+docker is invalid

commit(...)

将容器运行时保存为镜像,与 docker commit 相似。

  • repository (str): 镜像包名称(docker.barwe.cn/ubuntu:focal 中的 docker.barwe.cn/ubuntu)
  • tag (str): 镜像标签(docker.barwe.cn/ubuntu:focal 中的 focal)
  • message (str): 提交日志
  • author (str): 作者
  • pause (bool): 在保存镜像前是否暂停容器运行
  • changes (str): Dockerfile instructions to apply while committing
  • conf (dict): The configuration for the container. See the Engine API documentation for full details.

服务端可能会抛出 docker.errors.APIError 异常。

diff()

审查容器内文件系统的变化。返回值类型为:None | { Path: str; Kind: int; }[]

先在容器内新建一个目录:

root@b3a13392baa3:/# mkdir xxx

然后审查:

z.diff() ## [{'Path': '/xxx', 'Kind': 1}]

exec_run(...)

在容器内执行命令,与 docker exec 相似。

def exec_run(
    self,
    cmd: str | list[str], # 要执行的命令
    stdout=True,
    stderr=True,
    stdin=False,
    privileged=False,
    user='root', # 指定执行该命令的用户
    detach=False,
    tty=False,    # 交互式命令需要启用伪终端
    stream=False, # 交互式命令需要启用流式输出
    socket=False,
    environment: list[str] | dict=None, # 形如 ["PASSWORD=xxx"] 或者 {"PASSWORD":"xxx"}
    workdir:str=None, # 执行此命令的工作目录
    demux=False, # 分开返回 stdout & stderr
) -> docker.models.containers.ExecResult

该方法返回一个 docker.models.containers.ExecResult 实例,该实例有两个属性:

  • exit_code (int): 非交互式命令退出状态码为整数,交互式命令为 None
  • output: 输出结果
    • 非交互式命令返回的是 字节串 或者 字节串元组
    • 交互式命令返回的是 docker.types.daemon.CancellableStream 可迭代对象
    • socket=True 时返回的是 socket.SocketIO 对象
demux非交互式命令交互式命令
FalsebytesCancellableStream[bytes]
TrueTuple[bytes|None, bytes|None] 分别对应 stdout, stderrCancellableStream[Tuple[bytes|None, bytes|None]]

返回值类型测试:

1、默认情况下返回字节串:

result = container.exec_run("pwd")
print(result, type(result), sep="\n")
## ExecResult(exit_code=0, output=b'/\n')
## <class 'docker.models.containers.ExecResult'>

result = container.exec_run("pwd", demux=True)
print(result, type(result), sep="\n")
## ExecResult(exit_code=0, output=(b'/\n', None))
## <class 'docker.models.containers.ExecResult'>

2、top 这类 交互式命令 不能直接获取输出:

result = container.exec_run("top")
print(result, type(result), sep="\n")
## ExecResult(exit_code=1, output=b'TERM environment variable not set.\n')
## <class 'docker.models.containers.ExecResult'>

提示未设置 TERM 环境变量,根本原因是缺少交互的(伪)终端。启用伪终端:

result = container.exec_run("top", tty=True)

这行代码会阻塞主进程,我们将看不到任何输出结果。还需要启用 流式输出

result = container.exec_run("top", tty=True, stream=True)
print(result, type(result), sep="\n")
## ExecResult(exit_code=None, output=<docker.types.daemon.CancellableStream object at 0x7f28e526e630>)
## <class 'docker.models.containers.ExecResult'>

所以对于 终端交互式命令,需要同时启用 伪终端流式输出 才能不阻塞主进程,立即返回调用。

交互式命令返回的是一个 docker.types.daemon.CancellableStream 对象,我们可以对其进行迭代:

for chunk in result.output:
    print(type(chunk), len(chunk))
## <class 'bytes'> 13
## <class 'bytes'> 2933
## ...

迭代会阻塞主进程,它不会结束,我们需要在循环内部手动 break 或者 return。

交互式命令启用 demux 时:

result = container.exec_run("top", tty=True, stream=True, demux=True)
for stdout, stderr in result.output:
    print("stdout", type(stdout), len(stdout), "| stderr", stderr)
    break
## stdout <class 'bytes'> 13 | stderr None

3、socket=True

print(container.exec_run("pwd", socket=True))
## ExecResult(exit_code=None, output=<socket.SocketIO object at 0x7f03f5ccc1c0>)

print(container.exec_run("top", socket=True, tty=True, stream=True))
## ExecResult(exit_code=None, output=<socket.SocketIO object at 0x7f03f5ccc9d0>)

综上,此方法的返回值实际上有五种(或者更多):

  1. 非交互式命令:bytes
  2. 非交互式命令 + demux:Tuple[bytes, bytes | None]
  3. 交互式命令:CancellableStream[bytes]
  4. 交互式命令 + demux:CancellableStream[Tuple[bytes, bytes | None]]
  5. socket=True:socket.SocketIO object

export(chunk_size=2097152)

将容器的文件系统导出为 .tar 文件。默认分片大小为 2 MB。

返回值是一个 Generator[bytes]

with open("xxxx.tar", "wb") as f:
    for chunk in container.export():
        f.write(chunk)

get_archive(...)

将容器中的某个文件或者文件夹导出为 .tar 文件。默认分片大小为 2 MB。

def get_archive(
    self,
    path: str, # 文件或者文件夹的路径
    chunk_size=2097152, # 分片大小,默认2MB
    encode_stream=False, # 数据在传输过程中是否编码(目前没发现特殊用途)
) -> Tuple[
    Generator[bytes],
    { name:str; size:int; mode:int; mtime:str; linkTarget:str; }
] or raise docker.errors.APIError: ...
bits, stat = container.get_archive("/")
with open("root.tar.gz", "wb") as f:
    for chunk in bits:
        f.write(chunk)

put_archive(path, data)

将外部的 .tar 文件内容复制到容器中的指定位置。

容器内路径必须存在。

Returns True or raise docker.errors.APIError.

kill(signal=SIGKILL)

from signal import SIGKILL
container.kill(SIGKILL)

logs(**kwargs)

提取容器的日志,类似于 docker logs 命令。

def logs(
    self,
    stdout=True,
    stderr=True,
    stream=False, # 流式输出
    timestamps=False, # 显示时间戳
    tail:str|int='all', # 只返回末尾指定数目的行,或者 'all'
    since:datetime|int|float=None, # 只返回指定时间点之后的内容
    follow=False, # Follow log output
    until:datetime|int|float=None, # 只返回指定时间点之前的内容
) -> bytes | Generator[bytes]
or raise docker.errors.APIError: ...
result = container.logs()
print("[DEBUG]", type(result))
## [DEBUG] <class 'bytes'>

result = container.logs(stream=True)
print("[DEBUG]", type(result))
## [DEBUG] <class 'docker.types.daemon.CancellableStream'>

pause()

暂停容器内的所有进程。

Returns None or raise docker.errors.APIError.

reload()

从服务端重新加载容器信息,刷新 .attrs 属性。

rename(name)

容器重命名,类似于 docker rename 命令。

新的名称不能与旧的名称相同。

Returns None if ok or raise docker.errors.APIError.

resize(height, width)

Resize the tty session.

在脚本中似乎用不到这个方法。

stats(**kwargs)

docker stats 命令相似,流式输出容器的资源统计数据。

def stats(
    self,
    stream=True,  # 流式输出(Generator[bytes|dict]),或者只输出一次(bytes|dict)
    decode=False, # 是否将输出的字节串直接解码成「字典」,仅在 stream=True 时生效
) -> (bytes|dict) | (Generator[bytes|dict])
or raise docker.errors.APIError: ...

由 stream 和 decode 两个参数控制返回值的格式。

1、Returns dict:

r = container.stats(stream=False)
print(type(r), r, sep="\n")
## <class 'dict'>
## {'read': '2024-11-28T07:23:47.6695828Z', ...

2、Returns Generator[bytes]:

r = container.stats()
print(type(r), r, sep="\n")
for x in r:
    print(type(x))
    break
## <class 'generator'>
## <generator object APIClient._stream_helper at 0x7f809b762f40>
## <class 'bytes'>

3、Returns Generator[dict]:

r = container.stats(decode=True)
print(type(r), r, sep="\n")
for x in r:
    print(type(x))
    break
## <class 'generator'>
## <generator object APIClient._stream_helper at 0x7fe2670cef40>
## <class 'dict'>

容器详细信息:

type int = number;
type str = string;
type datetime_str = string;

interface ContainerStats {
  id: str;
  name: str;
  read: datetime_str;
  preread: datetime_str;
  pids_stats: { current: int; limit: int };
  blkio_stats: {
    io_service_bytes_recursive: {
      major: int;
      minor: int;
      op: "read" | "write";
      value: int;
    }[];
    io_serviced_recursive: unknown;
    io_queue_recursive: unknown;
    io_service_time_recursive: unknown;
    io_wait_time_recursive: unknown;
    io_merged_recursive: unknown;
    io_time_recursive: unknown;
    sectors_recursive: unknown;
  };
  num_procs: int;
  storage_stats: {};
  cpu_stats: {
    cpu_usage: {
      total_usage: int;
      usage_in_kernelmode: int;
      usage_in_usermode: int;
    };
    system_cpu_usage: int;
    online_cpus: int;
    throttling_data: {
      periods: int;
      throttled_periods: int;
      throttled_time: int;
    };
  };
  precpu_stats: {
    cpu_usage: {
      total_usage: int;
      usage_in_kernelmode: int;
      usage_in_usermode: int;
    };
    system_cpu_usage: int;
    online_cpus: int;
    throttling_data: {
      periods: int;
      throttled_periods: int;
      throttled_time: int;
    };
  };
  memory_stats: {
    usage: int;
    stats: {
      active_anon: int;
      active_file: int;
      anon: int;
      anon_thp: int;
      file: int;
      file_dirty: int;
      file_mapped: int;
      file_writeback: int;
      inactive_anon: int;
      inactive_file: int;
      kernel_stack: int;
      pgactivate: int;
      pgdeactivate: int;
      pgfault: int;
      pglazyfree: int;
      pglazyfreed: int;
      pgmajfault: int;
      pgrefill: int;
      pgscan: int;
      pgsteal: int;
      shmem: int;
      slab: int;
      slab_reclaimable: int;
      slab_unreclaimable: int;
      sock: int;
      thp_collapse_alloc: int;
      thp_fault_alloc: int;
      unevictable: int;
      workingset_activate: int;
      workingset_nodereclaim: int;
      workingset_refault: int;
    };
    limit: int;
  };

  networks: {
    [name: str]: {
      rx_bytes: int;
      rx_packets: int;
      rx_errors: int;
      rx_dropped: int;
      tx_bytes: int;
      tx_packets: int;
      tx_errors: int;
      tx_dropped: int;
    };
  };
}

start(**kwargs)

启动容器,类似于 docker start 命令,但是不支持 attach 功能。

Returns None if ok or raise docker.errors.APIError.

restart(**kwargs)

重启容器,类似于 docker restart 命令。

def restart(
    self, 
    timeout=10, # 尝试stop容器,超过这个秒数会强制kill容器,之后再restart容器
) -> None
or raise docker.errors.APIError: ...

stop(**kwargs)

停止运行容器,类似于 docker stop 命令。

  • timeout: 超时后直接发送 SIGKILL 给服务端,默认 10 秒

Returns None if ok or raise docker.errors.APIError.

remove(**kwargs)

移除容器,类似于 docker rm 命令。

def remove(
    self,
    v:bool, # 同时移除此容器关联的数据卷
    link:bool, # Remove the specified link and not the underlying container
    force:bool, # 强制移除正在运行的容器(SIGKILL)
) -> None
or raise docker.errors.APIError: ...

top(**kwargs)

获取容器当前运行的进程信息,类似于 ps 命令。

  • ps_args (str): 传递给 ps 命令的参数字符串

返回一个字典:{ Processes: str[][]; Titles: str[]; }

  • Titles: 表格列名
  • Processes: 进程列表,每个进程信息表示为一个字符串数组,长度与 Titles 一致

默认参数:

r = container.top()
print(type(r), list(r.keys()), r["Titles"], r["Processes"][0], sep="\n")
## <class 'dict'>
## ['Processes', 'Titles']
## ['UID', 'PID', 'PPID', 'C', 'STIME', 'TTY', 'TIME', 'CMD']
## ['root', '43205', '43183', '0', '11月27', 'pts/0', '00:00:00', 'bash']

自定义参数:

r = container.top(ps_args="-aux")
print(r["Titles"], r["Processes"][0], sep="\n")
## ['USER', 'PID', '%CPU', '%MEM', 'VSZ', 'RSS', 'TTY', 'STAT', 'START', 'TIME', 'COMMAND']
## ['root', '43205', '0.0', '0.0', '4116', '3328', 'pts/0', 'Ss+', '11月27', '0:00', 'bash']

unpause()

恢复所有进程。

update(**kwargs)

更新容器的资源配置。

def update(
    self,
    blkio_weight: int, # Block IO 的相对权重,取值 10 ~ 1000
    cpu_period: int, # CPU 时间片大小
    cpu_quota: int, # 限制最大核数
    cpu_shares: int, # 容器之间竞争优先级
    cpuset_cpus: str, # 限制物理CPU
    cpuset_mems: str, # 限制NUMA内存节点
    mem_limit: int | str, # 物理内存硬限制 --memory
    mem_reservation: int | str, # 物理内存软限制,同 --memory-reservation
    memswap_limit: int | str, # 物理+虚拟内存限制,同 --memory-swap
    kernal_memory: int | str, # 内核内存限制(区别于用户空间的应用内存)
    restart_policy: dict, # 容器重启策略
):

Completely Fair Scheduler: 完全公平排程器。

--cpu-period 决定排程器每个执行任务的时间片大小,需要结合任务类型、系统负载、内存网络等综合设置,经验值为 100 ms。

--cpu-quota 在多核系统中决定容器在一个 CPU 时间片内最多可以使用的 CPU 时间(等效于 CPU 核数)。

--cpu-shares 表示在多个容器之间分配 CPU 资源的相对权重,它不直接限制容器的 CPU 使用率,而是定义容器在 CPU 资源紧张时竞争 CPU 的优先级。默认情况下,每个容器的 --cpu-shares 为 1024,给关键容器设置更高的 --cpu-shares 以保证它们在 CPU 资源紧张时仍然相对流畅的运行。

--cpuset-cpus 用于精确控制容器只能在哪些物理 CPU 上运行,例如 --cpuset-cpus="0,1" 将容器限制在前两个物理 CPU 上。

--cpuset-mems 用于精确控制容器只能在指定的内存节点(Non-Uniform Memory Access, NUMA)上分配资源。将特定的任务隔离到特定的内存节点中。

--memory 限制容器使用的内存,超过此限制时容器可能会被杀掉,或者回收其内存。 (硬性限制)

--memory-reservation 对内存进行软限制:内存充足时,无视这个限制;内存不足时,此容器占用的内存会被优先降低到限制之下(不是强制的)。

--memory-swap 物理内存+交换空间上限,一般交换空间大小等于物理内存大小,所以此选项默认值为 --memory 的两倍。

wait(**kwargs)

阻塞主进程,知道容器停止,返回退出状态码。类似于 docker wait 命令。

  • timeout (int): 超时后抛出 requests.exceptions.ReadTimeout 异常
  • condition (str): 等待容器变成该状态,默认为 not-running, 还可以是 next-exit, removed
0

评论区