尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C++、Python、Docker都说“容器”,到底有何不同?

C++、Python、Docker都说“容器”,到底有何不同? 当“容器”这个词同时出现在 C、Python、Docker、YARN 的技术文章里时很多开发者都会有一瞬间的恍惚它们说的到底是不是同一个“container”我自己早期学习时也踩过这个坑——上午刚看完std::vector的扩容机制下午又去查 Docker 容器启动报错两篇文章都叫“容器”但内容完全不在一个层面。这篇文章打算做一次横向的 container 总结把最容易混淆的两类“容器”拆开讲清楚数据结构里的容器C STL、Java 集合、Python 内置容器运行环境里的容器Docker、OCI Runtime、镜像与容器安全以及容器化改造时需要考虑的关键问题包括数据库容器、资源分配、权限报错等。无论你是刚接触编程、准备面试还是工作中要部署业务系统都可以把这份笔记当作一份“容器索引”需要哪一块就翻到对应章节。1. 容器到底是什么先分清两套语境1.1 容器在编程语言中的含义在编程语言领域容器Container是一个很老的概念指的是一类“能够存储、组织、访问其他数据”的数据结构。比如 C 里的std::vector、Python 里的list、Java 里的HashMap它们本身也是数据但主要作用是“装数据”。正因为它们负责管理数据元素的存储方式、遍历顺序和查找效率所以被称为容器。你写算法题、做业务开发时最常打交道的就是这一类容器。1.2 容器在运维与云原生中的含义在云原生和运维领域容器是一种操作系统层面的虚拟化技术。它通过 Linux 内核的 namespace、cgroup 等机制把进程运行所需的环境、依赖、代码打包在一起形成独立、可移植的运行单元例如 Docker 容器和 Kubernetes Pod 里的容器。这类容器解决的是“程序在 A 机器能跑换到 B 机器也能跑”的环境一致性问题强调的是隔离、交付和编排。1.3 两种语义为什么容易混淆两种容器都叫 container本质原因是一个共性它们都负责把东西“装起来”并对外提供统一的管理能力。数据结构容器“装”的是数据运行环境容器“装”的是进程和文件系统数据结构容器提供迭代器、查找、插入运行环境容器提供启动、停止、网络、存储。可以把下面这张表作为快速定位工具你听到的“容器”常见语境核心关注点C 容器算法、数据结构内存布局、迭代器、容量、复杂度Java 容器集合框架HashSet、TreeMap、线程安全Python 容器数据类型list、dict、set、判断成员关系Docker 容器部署、测试镜像、端口、卷、环境变量OCI 容器容器运行时runc、containerd、启动报错YARN Container大数据资源调度CPU、内存、Executor 数量明白了语境再看具体内容就不会乱。下面先讲数据结构中的容器再讲 Docker 和容器化改造。2. 数据结构容器C/Java/Python 横向对比2.1 C STL 容器分类C 标准模板库STL里的容器通常被分成几类序列式容器元素按插入顺序排列例如vector、deque、list、forward_list关联式容器元素按关键字自动排序例如map、set、multimap、multiset无序关联容器基于哈希表实现例如unordered_map、unordered_set容器适配器基于其他容器实现接口限制例如stack、queue、priority_queue。实际开发中使用频率最高的通常是vector。// 文件路径demo_vector.cpp #include iostream #include vector int main() { std::vectorint numbers; numbers.reserve(10); // 提前预留空间避免频繁扩容 numbers.push_back(3); numbers.push_back(5); numbers.push_back(7); std::cout size: numbers.size() std::endl; // 已存储元素个数 std::cout capacity: numbers.capacity() std::endl; // 已分配内存容量 for (int num : numbers) { std::cout num ; } std::cout std::endl; return 0; }这里需要特别注意size()和capacity()的区别。size()是容器里真实的元素个数capacity()是当前分配的内存能容纳多少个元素。连续插入元素时vector在空间不足后会重新分配更大内存并把旧元素拷贝或移动过去这也是新手最常忽略的性能点。如果提前知道数据规模可以像上面示例一样使用reserve。map是另一类高频容器常用于键值映射#include iostream #include map #include string int main() { std::mapstd::string, int scores; scores[Alice] 90; scores[Bob] 85; auto it scores.find(Alice); if (it ! scores.end()) { std::cout it-first - it-second std::endl; } return 0; }使用find而不是直接用scores[key]判断某个键是否存在是因为operator[]在键不存在时会自动插入默认值从而改变容器内容。这个细节既是高频考点也容易在实际代码里埋下 bug。2.2 Python 中如何判断数据是否属于指定容器Python 的内置容器很丰富常用分类如下不可变容器tuple、str、frozenset可变容器list、set、dict抽象容器collections.deque、Counter、OrderedDict等。Python 中“判断一个数据是指定容器的内容”通常有两层意思。第一层是成员判断语法上使用invalues [10, 20, 30] print(20 in values) # True print(99 in values) # False对于list、tuplein默认是线性遍历对于set、dict由于使用哈希结构in的时间复杂度接近 O(1)。因此如果要做大量成员判断尽量把数据放进set而不是list。第二层是类型判断确认某个对象是不是容器、是不是某种抽象容器类型from collections.abc import Container, Iterable, Sized, Sequence def describe_container(obj): parts [] if isinstance(obj, Container): parts.append(支持 in 成员判断) if isinstance(obj, Sized): parts.append(支持 len()) if isinstance(obj, Sequence): parts.append(是有序序列) if isinstance(obj, Iterable): parts.append(可迭代) return , .join(parts) print(describe_container([1, 2, 3])) print(describe_container({a: 1})) print(describe_container(range(5)))推荐使用collections.abc里的抽象基类做判断而不是简单地使用type(obj) list因为很多自定义类实现了__contains__、__len__等方法也应当被视为容器。2.3 Java 集合框架的容器体系Java 的集合框架Collection Framework可以看作 Java 语言内置的一套“容器标准”。它主要分成两个顶层分支Collection单列数据集合主要包括List可重复有序、Set不可重复、Queue队列Map双列 key-value 映射例如HashMap、TreeMap、ConcurrentHashMap。实际开发中的典型写法import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class DemoContainer { public static void main(String[] args) { ListString names new ArrayList(); names.add(Alice); names.add(Bob); MapString, Integer ageMap new HashMap(); ageMap.put(Alice, 20); ageMap.put(Bob, 22); System.out.println(names.contains(Alice)); System.out.println(ageMap.get(Alice)); System.out.println(ageMap.containsKey(Bob)); } }选择 Java 容器时可以从三个维度去考虑允不允许重复、是否要求有序、是否需要线程安全。ArrayList适合读取多、尾部插入多的场景LinkedList适合频繁中间插入删除的场景HashSet适合去重和快速查找ConcurrentHashMap适合多线程并发读写。2.4 数据结构容器选择思路总结不管是 C、Java 还是 Python容器的选择本质上是在回答 4 个问题元素是否有顺序要求元素是否允许重复业务重点是插入删除、还是查找是否有多线程并发操作面试中经常出现“vector和list有什么区别”“ArrayList和LinkedList有什么区别”这类题目也是在考查你是否理解数据结构和操作场景的匹配关系。3. Docker 容器镜像、运行时与常见概念3.1 镜像与容器的关系在 Docker 语境里镜像Image和容器Container是两组最容易混淆的概念。镜像是一个只读模板包含了代码、运行时、系统工具、依赖库和配置。容器是镜像运行后的实例可以启动、停止、删除也可以在里面写临时文件。打个比方镜像类似 Java 里的“类”容器类似“对象”。同一个镜像可以创建多个容器每个容器运行着各自独立的进程。拉取并启动一个 Nginx 容器docker pull nginx docker run -d --name my-nginx -p 8080:80 nginx执行完成后宿主机访问http://localhost:8080就能进入 Nginx 默认页面。这条命令里的参数需要逐个理解-d后台运行--name给容器命名-p 8080:80把宿主机的 8080 端口映射到容器的 80 端口nginx镜像名称。查看容器状态docker ps -a docker logs my-nginx docker exec -it my-nginx bashdocker exec -it进入的是一个已经启动的容器内部适合排查运行中的问题但这种交互只适合临场验证线上排查仍然建议优先以日志和监控为主。3.2 Docker 容器是如何启动的Docker 不是“直接运行容器”的单一程序。现代 Docker 架构通常由几个部分组成Docker Client命令行工具dockerd守护进程负责接收 API 请求containerd容器生命周期管理组件runc真正创建和运行容器的 OCI Runtime。当 Docker 创建容器时大致执行流程包括拉取镜像、解压镜像、创建可写层、配置网络和存储、通过 runc 调用 Linux 内核创建隔离环境、在容器中启动指定进程。3.3 关于 oci runtime create failed 报错许多人在执行docker run时遇到过类似报错docker: Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused process_linux.go:... executing setns process caused \...\: unknown.这个报错里出现的container_linux.go、process_linux.go是 runc 源码中的文件名不同版本的行号可能不同。看到它只说明一个问题容器运行时runc在向内核发起创建容器流程时失败了并不代表你的 Docker 命令本身就是错的。常见原因有几个原因方向典型情况内核权限问题在不支持 namespace 的旧内核上创建容器seccomp/apparmor 限制容器内程序需要某些系统调用但被安全策略拦截挂载问题挂载点冲突或目录不存在内核兼容问题Docker 版本与宿主机内核不匹配排查时优先看dmesg宿主内核日志和容器运行时日志然后检查操作涉及的权限、挂载路径和系统调用策略。网上很多教程直接让你“重新安装 Docker”“升级内核”实际上如果没有完整信息这种建议治标不治本。4. 容器安全镜像安全与容器安全4.1 镜像安全与容器安全的区别容器安全经常被拆成两个层面。镜像安全发生在“容器运行之前”。镜像是否来自可信仓库、基础镜像是否有已知漏洞、镜像内是否包含多余权限和密钥都会影响后续所有容器实例的安全。容器安全发生在“容器运行期间”。即使镜像本身很干净运行时的错误配置也可能导致问题比如使用 root 用户运行应用、容器内挂载了宿主机关键目录、没有限制 CPU 和内存等。建议在部署前确认以下几点尽量使用官方镜像或内部私有仓库镜像定期扫描镜像漏洞例如使用 Trivy、Clair 等工具优先选择体积小的基础镜像减少攻击面运行时不要使用 root创建专用用户需要持久化的目录使用数据卷挂载生产环境开放最小化端口和网络策略。这里提醒一句对线上容器的操作、权限变更、安全策略调整应当在测试环境充分验证并遵循最小权限原则避免影响正常业务。4.2 一个最小化容器 Dockerfile 示例# 文件路径Dockerfile FROM eclipse-temurin:17-jre-alpine RUN addgroup -S appgroup adduser -S appuser -G appgroup WORKDIR /app COPY target/demo.jar /app/demo.jar USER appuser EXPOSE 8080 ENTRYPOINT [java, -jar, /app/demo.jar]这里有几个关键点FROM选择 JRE 镜像而不是完整 JDK体积更小创建独立用户appuser避免容器进程以 root 运行COPY只复制需要的 jar 包而不是复制整个项目目录USER appuser在镜像层面降低权限EXPOSE只是声明端口真正发布端口仍由运行命令中的-p控制。写 Dockerfile 时要减少镜像层级、清理临时文件、固定基础镜像版本避免使用latest标签因为latest会随上游更新而变化导致同一份 Dockerfile 在不同时间构建出不同的镜像。5. 业务系统容器化改造与有状态组件5.1 哪些业务适合容器化容器化能带来环境一致、交付更快、弹性伸缩更方便等好处但并不是所有系统都适合立刻改造。适合容器化的业务通常具备这些特征应用是无状态或可以外部化状态的启动顺序依赖比较简单可以按水平和副本方式扩展团队已经具备镜像构建和 CI/CD 基础。不太适合容器化或需要复杂处理的业务包括强依赖本地磁盘状态的传统数据库、依赖特定硬件驱动的应用、对内核模块或真实网络设备有要求的程序以及部分实时性极高的信号处理系统。判断是否改造不是“用上 Docker 就是进步”而是要看投入产出比。简单脚本直接部署可能更轻量几十个微服务才更适合用容器加编排平台统一管理。5.2 PostgreSQL 容器与数据卷使用容器运行 PostgreSQL 在开发环境非常方便比如启动一个本地 PostgreSQLdocker run -d \ --name local-postgres \ -e POSTGRES_USERdevuser \ -e POSTGRES_PASSWORDdevpass \ -e POSTGRES_DBtestdb \ -p 5432:5432 \ -v pgdata:/var/lib/postgresql/data \ postgres:16-alpine-v pgdata:/var/lib/postgresql/data表示使用名为pgdata的 Docker volume 挂载到容器内数据库数据目录。如果去掉这个挂载容器删除后数据库中的数据也会随之丢失。这一步非常重要。很多初学者启动了一个数据库容器往里面写入数据然后执行docker rm -f local-postgres最后发现数据全没了。正确的做法是把持久化数据放到 volume 或宿主机目录中同时定期执行逻辑备份。生产环境的数据库容器通常还需要额外考虑数据备份与恢复策略容器版本与数据库版本的配套存储性能内存和 CPU 限制连接池配置安全基线包括访问控制和 SSL 连接等。5.3 Spark on YARN 中的 Container 资源单位在 YARN 资源调度体系里Container 又有了第三层含义它不是数据结构也不是 Docker 容器而是 YARN 分配给 ApplicationMaster 或 Executor 的“资源容器”包含 CPU、内存等资源。例如 Spark on YARN 作业运行时每个 Executor 会对应一个或若干个 YARN Container。这里经常出现的一个问题是用户发现每个 Container 只分配了一个 vcore与预期不符。这可能与 YARN 调度器的 vcore 配置、Spark 参数spark.executor.cores、spark.executor.memory等设置有关。资源分配不是单独由某一个参数决定的需要查看 Spark 配置、YARN 调度配置和集群实际资源后再调整。在做大数据作业容器化或资源调优时不要只盯着一项参数而要看完整链路客户端提交参数 - ApplicationMaster 申请资源 - YARN 调度器分配 Container - Executor 在 Container 内运行。6. 常见问题与排查思路在实际使用容器和数据结构容器时有几个问题出现频率很高。下面整理成一张表格方便收藏备查。问题现象常见原因解决思路Docker 启动报OCI runtime create failedrunc 创建容器失败常见有权限、挂载、系统调用问题查看内核日志、运行时日志检查安全策略路径Windows 容器报“应用程序特定权限设置”或“无法枚举容器中的对象访问被拒绝”Windows SID、共享目录权限不一致检查应用容器可执行文件权限、目录共享设置和用户映射容器删除后 PostgreSQL 数据全部丢失没有挂载数据卷启动时指定-v pgdata:/var/lib/postgresql/data之类数据卷容器内部时区与宿主机不一致镜像默认使用 UTC运行时挂载/etc/localtime或通过环境变量设置in判断 Python 列表内容很慢列表in是线性遍历改为set再判断如果必须保持顺序可额外维护集合C vector 使用后迭代器失效插入或扩容导致内存重新分配避免在遍历时插入改用下标并注意扩容时机Java 并发修改HashMap抛异常多线程访问同一个 HashMap使用ConcurrentHashMap不要在多线程下共用可变集合Spark 作业 Container vCore 数量与配置不符Spark 参数与 YARN 调度配置不一致检查spark.executor.cores、YARN 调度器和队列配置如果遇到上面没有覆盖到的报错建议按以下顺序排查。第一步复现。记录触发报错的原始命令、完整报错信息、环境和代码版本。第二步缩小范围。先确认是环境问题还是代码问题。例如 Docker 容器无法启动可以先在宿主机上直接跑同样的命令判断是否缺少依赖。第三步查日志。容器报错优先看docker logs进程报错优先看操作系统日志和应用日志。第四步搜索和比对。带着完整报错关键词去检索不只看现象要看解决方案是否适用于你的版本。7. 最佳实践与学习建议7.1 数据结构容器最佳实践第一优先选择最合适的容器而不是最熟悉的容器。C 里随手用vector通常没问题但要结合“是否需要中间插入、是否需要按键查找”来判断Java 里读写并发场景优先考虑ConcurrentHashMap而不是给整个方法加锁Python 里大量成员判断优先使用set。第二理解性能和扩容机制。vector扩容时的拷贝成本、HashMap装载因子和扩容、Python 列表内存预分配这些都会影响程序在大数据量下的表现。第三注意迭代器和遍历期间的修改规则。在遍历容器时往容器里插入或删除元素是 C、Java、Python 中都很容易踩的坑不同语言行为不同但统一原则是“遍历和修改分离”。第四用类型和抽象约束代码。Java 中面向接口编程Python 中使用collections.abc做类型判断C 中通过迭代器抽象解耦算法和容器。7.2 Docker 容器最佳实践写镜像时遵循一容器一进程的原则容器不是小型虚拟机不建议把 SSH、多个服务都塞进同一个容器。构建镜像时要清理包管理器缓存和无用文件固定基础镜像版本不要直接使用latest。配置容器时要考虑日志输出。应用日志应输出到 stdout/stderr由 Docker 日志驱动统一收集而不是写入容器内的本地文件否则容器重建后日志和监控代理都很难处理。真实生产环境要注意资源限制docker run -d \ --name my-app \ --memory512m \ --cpus1 \ -p 8080:8080 \ my-demo:latest--memory限制容器最大内存--cpus限制 CPU 核数。如果没有这些限制一个失控容器可能影响宿主机上的其他进程。安全方面始终保持最小权限原则不要在镜像里写死密码不要轻易把 Docker Socket 挂载到容器中不要在容器内以 root 运行不必要的服务对公开端口和网络策略做好规划。7.3 容器技术的学习路线如果目标是打好数据结构基础可以从 C STL 和 Java 集合框架入手优先理解容器内部数据结构、时间复杂度和扩容机制。如果目标是做部署和运维可以按下面路径学习掌握 Linux 基础命令和进程概念学会写 Dockerfile理解镜像构建和容器运行掌握 Docker Compose 编排多个本地服务理解容器网络、数据卷和安全性再学习 Kubernetes 的 Pod、Deployment、Service 等概念最后结合实际业务做容器化改造和故障演练。容器内容很多但最核心的底层逻辑并不复杂数据容器关注的是组织和效率运行环境容器关注的是隔离和交付资源容器关注的是调度和分配。每次遇到一个叫“容器”的概念时先问一句这句话说的是哪一层定位准确后后续的排查和学习都会顺畅很多。
返回列表