写 Docker 这些年,我从踩坑里攒下的经验,今天一次性聊透。先说背景:我从 2015 年就开始把线上服务往容器里搬,从最初“跑个 MySQL 试试”,到现在本地开发、CI/CD 打包、微服务上线、GPU 推理部署,几乎没有哪天不用 Docker。而云原生这个概念,很多朋友觉得只有大厂才有资格聊,其实没那么多玄学,云原生的核心就是让应用和运行环境解耦,同一份代码从开发机到测试机再到生产集群,行为保持一致。做到这一步,最简单的切入点就是 Docker 容器技术。这篇文章不罗列官方文档,只讲我在安装 Docker、部署 MySQL 8.0、搭建 Redis 主从、打包微服务镜像、跑 AI 模型推理,以及处理各种容器网络和权限问题时踩过的真实坑,整理成一份能跟着直接动手的实操笔记。适合刚接触容器的小白,也适合已经用了 Docker 但常被启动失败、网络不通、镜像下载慢折腾的人。
1. 云原生为什么要从 Docker 讲起
1.1 云原生的核心是“让应用换个环境也能活”
云原生这个词被说得太多了,反而容易让人忽略本质。它是一套构建和运行应用的方法,涉及微服务、容器、编排、CI/CD、可观测性等一大堆东西。但所有这些能力最终都要落在同一件事上:让应用不再依赖某个特定的物理机器或操作系统。
我之前遇到过最典型的场景是这样的。开发在本地把功能测得好好的,部署到测试服务器上,先是报缺库,再是版本不对,最后发现连时区都不一样。这种“在我电脑上是好的”现象,原因就是运行环境太不可控。Docker 把应用连同它的依赖、配置、运行参数一起打包成镜像,等于给应用造了一个随身携带的操作系统环境。无论它飘到哪台机器上,只要内核兼容且有容器运行时,跑起来的结果就是一致的。
所以在云原生体系里,Docker 更像是一块最底层的地基。它不负责微服务怎么拆,也不负责流量怎么调度,只负责解决环境一致性和部署标准化这两个最让人头疼的问题。
1.2 容器和虚拟机:差的不仅是性能
很多人刚接触容器时都会问,虚拟机的隔离性也很好,为什么非要容器?我把两种方案的对比列过很多次,归根结底是成本差异太大。
| 对比项 | 虚拟机 | Docker 容器 |
|---|---|---|
| 启动时间 | 秒级到分钟级 | 毫秒级到秒级 |
| 镜像大小 | 通常几个 GB | 通常几十 MB 到几百 MB |
| 内存占用 | 每个虚拟机都要完整系统 | 共享宿主机内核,额外开销极小 |
| 隔离粒度 | 内核级隔离 | 进程级隔离 |
| 环境一致性 | 取决于镜像制作 | 镜像本身自带完整环境 |
虚拟机相当于给每个应用配了一台独立的电脑,电脑里得有 CPU、内存、硬盘、系统,哪怕应用只用一个端口,这些成本也省不掉。容器则像是给应用包了一个标准集装箱,集装箱里只有应用和它最需要的依赖,外面共享同一艘船的发动机和舱位。
这带来的直接影响是密度。同样一台 16G 内存的服务器,虚拟机可能只敢跑三四个实例,容器可以跑二十个甚至更多。我后面会讲到的 N100 小主机跑 20 个 Docker,靠的就是容器这种低开销特性。
1.3 Docker 不等于 Kubernetes,但它是第一公里
有个常见的误解是,既然上了云原生,就应该直接上 Kubernetes,Docker 好像已经退居幕后了。这个说法有一定道理,但还是不够准确。Kubernetes 是容器编排系统,它管理的底层运行时早期就是 Docker,现在生产环境很多已经使用 containerd 或 CRI-O,但开发阶段、镜像构建阶段,Docker 依然是事实标准。
哪怕到了 K8s 环境里,开发者日常接触的仍然是 Docker 这一套操作:docker build 构建镜像、docker push 推送到仓库、docker pull 拉取镜像。所以我始终建议个人和中小团队,不要一上来就啃 Kubernetes,先把 Docker 的镜像、容器、数据卷、网络这几个基本概念吃透,后面的编排之路会顺很多。
Docker 的核心其实就三个角色:镜像就像是应用的只读模板,容器是这个模板运行起来的实例,镜像仓库则是用来分发和共享模板的地方。理解这三者关系,后面所有操作都能套得进去。
2. 安装 Docker 的坑,我替你踩过
2.1 Windows 安装 Docker Desktop 前,先把虚拟化搞定
Windows 上安装 Docker 最常用的方式就是 Docker Desktop,但很多新手安装完双击启动,立刻被一句报错劝退:Docker Desktop failed to start because virtualisation support wasn't detected。
这个报错我保守估计处理过几十次了,原因高度集中在两处。
第一,物理机 BIOS 里没有开启虚拟化功能。开机进 BIOS,找 Intel Virtual Technology 或 SVM Mode,不同主板叫法不一样,设为 Enabled。开机后打开任务管理器,性能选项卡里能看到“虚拟化:已启用”的字样,就可以排除这个原因了。
第二,Windows 功能里缺少必要的虚拟机平台和 WSL 支持。Docker Desktop 在 Windows 上优先依赖 WSL2,如果系统里没有打开相关功能,启动同样会失败。正确步骤是:控制面板 -> 程序 -> 启用或关闭 Windows 功能,勾选“适用于 Linux 的 Windows 子系统”和“虚拟机平台”,然后以管理员身份运行 PowerShell,执行wsl --set-default-version 2,重启系统。
我建议直接用 WSL2 后端,不要用 Hyper-V。WSL2 启动更快,内存占用更小,跟 Windows 文件系统交互也更自然。装完 Docker Desktop 后,可以在设置里确认一下 Use the WSL 2 based engine 是否勾选。
2.2 Linux 安装 Docker 和 CentOS 7 版本升级
Linux 服务器上安装 Docker 相对简单,Ubuntu 系用官方脚本最省事:
curl -fsSL https://get.docker.com | bash -装完立刻设置开机自启,这一步很多人会漏:
systemctl enable docker systemctl start docker docker infoCentOS 7 是比较特殊的情况。系统自带的旧版本 Docker 会被直接标识为docker,但版本号停留在 1.13 附近,很多新镜像和 Compose 功能根本不兼容。我实际操作中一般先卸载旧包,再通过官方 yum 仓库装 docker-ce:
yum remove docker docker-common docker-selinux docker-engine yum install -y yum-utils yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo yum install -y docker-ce docker-ce-cli containerd.io需要提醒的是,CentOS 7 默认内核是 3.10,对 overlay2 存储驱动和 iptables 的新特性支持不太完善,能跑但偶尔会有诡异问题。生产环境我更推荐 Ubuntu 20.04 以上或较新的 Debian 版本,内核新,兼容性少很多。如果必须留在 CentOS 7,把内核升级到 4.x 或 5.x 能明显改善稳定性。
2.3 镜像加速与仓库配置的基本常识
Docker 默认从 Docker Hub 拉镜像,但实际使用中拉取速度经常不稳定,特别是较大的镜像。
解决办法是给 Docker 配置 registry mirror,也就是镜像加速地址。编辑/etc/docker/daemon.json:
{ "registry-mirrors": ["https://your-mirror-address"] }Linux 改完重启 Docker:
systemctl daemon-reload systemctl restart dockerWindows Docker Desktop 则在 Settings -> Docker Engine 里改同样的 JSON,保存后它自动重启引擎。
这里有个细节我特别想说:镜像加速只对配置了该地址的客户端有效,它本质上是把常用镜像缓存到离你更近的节点,并不能保证所有镜像都能加速。遇到冷门镜像依然慢的时候,优先选择带具体版本号的 tag,比如mysql:8.0.36,而不是latest,不仅版本可控,还能避免每次拉取都解析到不同大小。
2.4 启动失败和权限报错的处理思路
Linux 下最常见的两个报错,第一个是Cannot connect to the Docker daemon,第二个是docker: permission denied。
前者的排查思路是,先确认服务是否真的在运行:
systemctl status docker journalctl -u docker -n 50如果日志里有Failed to start Docker Application Container Engine,常见原因包括:磁盘满了、SELinux 拦截、配置文件写错导致 daemon 起不来、或者 containerd 版本冲突。依次检查磁盘剩余空间、临时关闭 SELinux 测试、把 daemon.json 改名后重启。大多数情况两三步就能定位。
后者则是典型的用户权限问题。Docker 默认只有 root 能直接操作,普通用户需要加入 docker 组:
sudo usermod -aG docker $USER命令执行后一定要重新登录终端再试,否则组关系不生效,这是新手最容易困惑的点。
顺手提一个安全常识:加入 docker 组等同于获得宿主机 root 权限,因为容器的本质是共享内核,不应该在多人共享的服务器上随意给普通用户开这个权限。
3. 常用服务容器化的正确姿势
3.1 MySQL 8.0 容器化:没有持久化等于白装
MySQL 8.0 的容器化是我最常被问到的一个需求。先给一条完整命令:
docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=YourPassword \ -e TZ=Asia/Shanghai \ -v mysql-data:/var/lib/mysql \ mysql:8.0.36命令拆开看,端口映射-p让宿主机可以直接访问,环境变量MYSQL_ROOT_PASSWORD是首次初始化时设置 root 密码,时区变量TZ解决了国内服务器普遍遇到的时区不一致问题,最关键的是-v mysql-data:/var/lib/mysql这种具名卷挂载。
为什么说没有持久化等于白装?因为容器可以随时删除重建,如果数据存在容器可写层里,docker rm之后数据就彻底没了。具名卷由 Docker 管理,存放在固定目录,即使容器删了,卷还在,重新运行同一条命令,数据自动回来了。
日常备份我就在宿主机上用docker exec配合 mysqldump 完成:
docker exec mysql8 sh -c 'exec mysqldump --all-databases -uroot -p"$MYSQL_ROOT_PASSWORD"' > backup.sql另外提醒一个初始化细节:MySQL 官方镜像会在首次启动时自动执行/docker-entrypoint-initdb.d目录下的.sql和.sh脚本,用来初始化表结构非常方便。把建表脚本挂载进去,比启动后手动执行靠谱得多。
3.2 Redis 主从复制:从单机到主从只需几步
Redis 容器化有个小坑:默认配置文件里bind 127.0.0.1,容器里只监听回环地址,宿主机和别的容器都访问不到。我的做法是写一份自己的 redis.conf 挂载进去。
假设在/opt/redis下准备主从两份配置:
主节点 6379 基本配置:
port 6379 bind 0.0.0.0 protected-mode yes appendonly yes主节点启动:
docker run -d --name redis-master \ -p 6379:6379 \ -v /opt/redis/master.conf:/etc/redis/redis.conf \ redis:7.2 redis-server /etc/redis/redis.conf从节点只需要在配置文件末尾加一行:
replicaof 192.168.1.10 6379然后启动从节点,注意端口别冲突:
docker run -d --name redis-slave \ -p 6380:6379 \ -v /opt/redis/slave.conf:/etc/redis/redis.conf \ redis:7.2 redis-server /etc/redis/redis.conf验证是否成为从节点:
docker exec redis-slave redis-cli info replication输出里role:slave且master_link_status:up就说明主从同步已经建立。如果想完整模拟生产环境,再加一个 redis-sentinel 容器做故障切换,这正是很多面试里问到的“Redis 主从哨兵”架构,用 Docker 三条命令就能搭好演练环境。
3.3 Docker Compose 编排:让多服务一步到位
单条 docker run 命令能解决的问题有限,真实业务至少是应用加数据库加缓存三个服务。这时候 Compose 的价值就体现出来了。
一个典型的 Spring Boot 项目配合 MySQL 和 Redis,docker-compose.yml长这样:
services: mysql: image: mysql:8.0.36 environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: appdb volumes: - mysql-data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s retries: 5 redis: image: redis:7.2 command: redis-server --appendonly yes app: build: . depends_on: mysql: condition: service_healthy redis: condition: service_started ports: - "8080:8080" environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/appdb SPRING_DATA_REDIS_HOST: redis volumes: mysql-data:启动命令只有一行:
docker compose up -d这里的核心逻辑有两个。第一,Compose 自动创建了一个自定义网络,服务之间可以直接用服务名互相访问,不需要查 IP,应用配置里的mysql和redis就是容器主机名。第二,depends_on加condition能控制启动顺序,等 MySQL 真正健康了再启动应用,避免出现“应用起来了但连不上库”的尴尬。
我建议从今天起,任何项目的环境搭建都用 Compose 管理,哪怕只有一个服务。版本管理、启停、日志查看都方便,而且docker compose down一条命令就能清理干净,不会在系统里留下一堆手动创建的容器残留。
3.4 Spring Boot 微服务打包镜像:多阶段构建是关键
把 Spring Boot 项目打成 Docker 镜像,我踩过最典型的坑是镜像体积失控。如果用传统方式先把整个 JDK 装进镜像,再拷贝 jar 包,一个镜像随随便便五六百兆,传输和启动都慢。
正确做法是多阶段构建,用 IDEA 或 Maven 打包后,生成一个只有 JRE 的精简镜像。参考 Dockerfile:
FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=builder /build/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]第一阶段的 maven 镜像只负责编译,第二阶段用一个清瘦的 JRE 镜像运行,最终镜像可能只有一百多兆。我把这称为“编译的归编译,运行的归运行”,理解这一点后,你就不会再写出臃肿的镜像了。
IDEA 里打包镜像也简单,装好 Docker 插件后,在运行配置里选择 Dockerfile,指定好镜像名和上下文目录,点运行就能直接 build。日常开发时,我习惯在本地先把服务容器跑通,再推到仓库给测试环境用,整个流程跟生产发布几乎一样。
打包时还要准备.dockerignore,排除target、.git、*.iml这些无关文件,否则构建上下文太大会让每次构建都变得很慢。
3.5 自托管应用和图腾般的“万能容器”
容器化最大的爽点,其实是部署各种开源系统变得像填空一样简单。
GitLab 是研发团队必备,用 Docker 装起来比裸机装省事太多:
docker run -d --name gitlab \ -p 8929:80 \ -v gitlab-config:/etc/gitlab \ -v gitlab-logs:/var/log/gitlab \ -v gitlab-data:/var/opt/gitlab \ gitlab/gitlab-ce:latest注意 GitLab 很吃内存,至少 4G,否则常会卡到无法访问。
Metabase 搭 BI 分析平台,一条命令起服务,连上 MySQL 就能做可视化;KodBox 做私有网盘,挂载本地目录就能管理文件;OpenObserve 做日志和可观测性平台,取代传统的 Elasticsearch 全家桶,配置量小一个数量级。这类工具的共同特点是:官方镜像已经默认了最佳启动参数,我们只需要负责数据卷和端口映射,升级时换一个 tag 重新创建容器,数据完好无损。
青龙定时任务管理面板也是很多人用的容器化工具,它的依赖安装放在容器内部完成,换机器只需要重新跑容器,不需要再折腾宿主机上的 Node 或 Python 环境。这种“依赖跟容器走”的做法,正是容器技术能横扫自托管场景的根本原因。
我还见过用 Docker 快速启动 DVWA 做安全教学环境、用 Hadoop 镜像搭建大数据实验环境的情况,价值都一样:用完就丢,不污染宿主机,下次需要再拉起来,五分钟恢复原状。
4. 高阶玩法:GPU、ROS2 与小主机极限部署
4.1 让 GPU 容器真正用上显卡
跑 AI 模型推理时,Docker 就不仅是方便,而是非常有必要的。宿主机上一堆 CUDA 版本、Python 依赖、推理框架,互相打架是常事。容器化之后,每个模型服务各自独立,谁都不用迁就谁。
要让 GPU 容器正常工作,前提是宿主机已安装 NVIDIA 驱动,先验证:
nvidia-smi然后安装 NVIDIA Container Toolkit,Ubuntu 系统的安装命令大致是:
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | sed 's#deb https:#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https:#g' | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list sudo apt-get update sudo apt-get install -y nvidia-container-toolkit之后运行任何带 GPU 的容器,加上--gpus all参数即可:
docker run --rm --gpus all nvidia/cuda:12.1.0-base-ubuntu22.04 nvidia-smi实际项目里我用 vLLM 跑过 Qwen3-embedding 这类模型。vLLM 镜像本身就带齐全套依赖,只需要把模型目录挂载进去:
docker run --gpus all \ -p 8000:8000 \ --ipc=host \ --shm-size=16g \ -v /data/models:/models \ vllm/vllm-openai:latest \ --model /models/qwen3-embedding \ --task embed \ --port 8000--ipc=host和--shm-size特别容易忘。PyTorch 和 vLLM 在数据处理时会大量使用共享内存,默认 64M 的/dev/shm会直接导致 “Bus error” 或者 CUDA 显存分配失败。先说清楚这个坑,后面你跑 AI 容器时能少走很多弯路。
4.2 ROS2 和 micro-ROS Agent 容器化
机器人操作系统 ROS2 是一套依赖极重的框架,版本之间互相不兼容。我身边做机器人开发的朋友,几乎人人都有被环境搞到重装系统的经历。ROS2 Humble 对应 Ubuntu 22.04,这个版本关系是硬约束,搞错了库文件全崩。
把 ROS2 环境容器化,等于给版本一致性上了保险。比如 micro-ROS Agent 是一个连接 ROS2 与嵌入式设备的关键桥接程序,直接在容器里跑:
docker run -it --rm --net=host \ microros/micro-ros-agent:humble \ udp4 --dev-fs /dev --port 8888这里用--net=host不只是图省事,而是 micro-ROS 常需要使用多播和多端口通信,默认的 bridge 网络很容易把 UDP 包挡在外面。机器人通过串口或 UDP 接入时,如果发现 Agent 收不到数据,第一步就该检查网络模式是否改成了 host。
这类工具链用容器跑还有个额外好处,就是微控制器固件和上位机代码可以分开管理。上位机环境出了问题,删容器重建,三分钟恢复,不用再面对“装了一下午环境,最后发现版本不匹配”的绝望。
4.3 N100 小主机跑 20 个容器,靠资源规划而不是硬件堆料
Intel N100 这两年很火,低功耗、体积小,很多人拿它当家庭服务器,动不动就往上面塞二十个容器。我实测下来,N100 不是不能跑,而是不能无脑跑。
它通常配 16G 或 32G 内存,CPU 性能比主流服务器弱不少。如果每个容器都不做资源限制,二十个服务同时启动,内存马上爆掉,然后系统开始疯狂使用 swap,整个机器卡到没法操作。
我给这类小主机定了一套规则。第一,每个容器都设置资源上限:
services: app: image: your-app deploy: resources: limits: cpus: "0.5" memory: 256M第二,日志必须轮转,否则默认的 JSON 日志文件会悄悄把磁盘塞满:
logging: driver: json-file options: max-size: "10m" max-file: "3"第三,定期清理无用镜像和构建缓存:
docker system prune -af按照这套思路,N100 跑二十个轻量服务完全可行。它的意义不在于性能,而在于把闲置算力充分利用起来,这也跟云原生“按需分配、高效利用资源”的理念正好吻合。
5. 高频问题排查速查与实战心得
5.1 常见报错对症下药
我把这些年处理过的高频问题整理成一个速查表,方便直接对照排查。
| 现象 | 常见原因 | 解决思路 |
|---|---|---|
| Docker Desktop 启动报虚拟化不被检测 | BIOS 没开虚拟化,Windows 功能未启用 | 开启 VT,勾选虚拟机平台和 WSL 支持,重启 |
| docker 命令提示 Cannot connect to the Docker daemon | 服务没启动或启动失败 | systemctl status docker 查状态,journalctl 看日志 |
| permission denied 访问 Docker 接口 | 用户不在 docker 组 | usermod -aG docker $USER,重新登录 |
| 容器启动后宿主机访问不了端口 | 端口映射缺失或防火墙拦截 | 检查 -p 参数、防火墙规则、容器进程是否监听 |
| 镜像下载一直超时 | 默认仓库访问不稳定 | 配置 registry-mirrors 镜像加速地址 |
| 容器内网络 ping 不通外网 | DNS 配置不对,或默认 bridge 网关问题 | 检查 /etc/resolv.conf,改用 --network=host 测试 |
| 磁盘突然爆满 | 日志文件不轮转、镜像缓存堆积 | 配置日志 max-size,定期 docker system prune |
| 容器删了数据就没了 | 没有挂载数据卷 | 所有有状态服务必须使用 -v 或 volumes 配置 |
以上每一行我都实际遇到过,解决方案也是操作后验证过的。
5.2 容器网络“写不通”的排查路径
容器网络问题在我遇到的求助里占比极高,而且大多数是同一个原因:分不清不同网络模式下的通信方式。
Docker 默认创建 bridge 网络时,容器之间通过 IP 通信,但每次重建容器 IP 都会变,手动写死 IP 是非常危险的做法。正确逻辑是:同一 Compose 网络下的服务用服务名通信,跨主机的服务用映射端口通信。
排查命令是基本功:
# 查看容器实际 IP docker inspect container_name | grep IPAddress # 进入容器测试连通性 docker exec -it app curl http://mysql:3306 # 查看端口映射是否正确 docker port app如果测试发现应用容器访问不了数据库容器,最可能就是两个容器不在同一个自定义网络里。解决方法:
docker network create app-net docker network connect app-net mysql8 docker network connect app-net app不是网络协议高深,而是容器在共享内核的同时默认网络是隔离的,凡是“写在别的容器里老访问不通”的报错,百分之八十都是因为这个。
5.3 镜像下载慢还有没有别的解法
除了配置镜像加速,还有两个实际可用的思路我经常推荐。
第一,多台机器间迁移镜像用 save 和 load。比如在一台网络好的机器上把镜像拉下来,产出一个 tar 包:
docker save mysql:8.0.36 -o mysql.tar拷贝到目标机器后:
docker load -i mysql.tar这种方式对离线环境非常有效,比在每台机器上重复拉镜像快得多。
第二,注意镜像 tag 大小差异。有些官方镜像的版本 tag 之间存在巨大体积差距,多给个小版本就能避开某个大体积 tag。这个没什么技巧,多留意 Docker Hub 上的镜像详情页就行。
5.4 数据卷和文件权限的两个隐形坑
数据卷解决了持久化问题,但引入了两个副作用,第一个是文件属主变化。容器内进程默认以 root 运行,在挂载目录里生成的文件,宿主机上看到的所有者也是 root,普通用户删不掉也改不了。解决办法是在 docker run 时指定用户:
docker run -d --user 1000:1000 -v /data:/data your-image或者构建镜像时直接创建非 root 用户,这是生产镜像的推荐做法。
第二个是 Windows 环境下挂载本地目录的 I/O 性能问题。Windows 文件系统和 WSL2 之间的转换有额外开销,如果容器大量读写本地挂载目录,速度会明显下降。我的建议是,开发时用绑定挂载方便改代码,但数据库、日志这类高频 IO 使用 Docker 管理的具名卷,性能会好很多。
最后再分享一个小习惯
写到这里,我想起一个自己坚持了几年的做法:每新增一个项目,都建一个独立目录,里面放docker-compose.yml、conf、data(别提交到 git)和.env环境变量文件。启动服务只执行docker compose up -d,清理服务只执行docker compose down。这个习惯帮我极大减少了“环境又坏了”的困扰,也让新同事接手项目时不用问“怎么跑起来”。
Docker 本身不解决所有问题,但它是接触云原生最合适的入口。当你把镜像、容器、数据卷、网络这套基本功练透,再去看 Kubernetes 或者其他编排平台,会发现很多概念都是相通的。容器技术的魅力,正在于它把复杂的工程问题,拆成了一个个可以重复、可验证的小步骤。希望这篇实战笔记能让你少踩几个我曾经踩过的坑。