Docker容器启动失败查看日志排查

容器一启动就退出、docker ps 里根本看不到,是 Docker 新手最常见的问题。很多人反复 run 却不看日志,自然找不到原因。关键原则:容器退出不代表没记录,用 logs 和 inspect 看它为什么挂。本文给出一套从日志到配置的排查顺序。

一、先确认容器状态

# 只看正在运行的,挂掉的容器不会出现
docker ps

# 看所有容器,包括已退出的,记录 CONTAINER ID
docker ps -a

状态为 Exited (1) 或 Exited (0),括号里就是退出码:0 是正常退出,非 0 是异常。

二、看容器日志(最关键一步)

# 看全部日志
docker logs 容器名或ID

# 实时跟踪最新输出,类似 tail -f
docker logs -f 容器名

# 只看最近 100 行
docker logs --tail 100 容器名

# 带时间戳
docker logs -t 容器名

日志里通常直接写了错误,比如配置文件找不到、端口被占、数据库连不上、权限拒绝。

三、查看容器详细退出原因

docker inspect 容器名 | grep -A 10 State

关注 ExitCode、Error、OOMKilled:OOMKilled 为 true 说明内存不足被杀死。

四、常见启动失败原因

1. 前台进程退出,容器就结束

容器必须有一个前台进程一直跑,命令执行完就退出,容器自然停。比如 nginx 镜像你用 nginx -s stop 或直接执行个脚本,跑完就退。要保证 CMD 以前台方式运行。

2. 配置文件/挂载路径错误

docker run -v /host/etc/nginx:/etc/nginx nginx

宿主机路径不存在、或挂载把镜像内配置覆盖成空,服务起不来。

3. 端口冲突

docker: Error starting userland proxy: listen tcp 0.0.0.0:80: bind: address already in use

宿主机 80 已被占用,换 -p 8080:80。

4. 数据库等依赖没就绪

应用启动时连不上 MySQL,直接退出。可用 restart 策略或等待脚本。

5. 权限不足 / 镜像命令写错

docker run 末尾的命令拼错,或挂载目录权限导致进程无法写文件。

五、反复重启看日志

# 查看实时日志验证修复
docker logs -f --tail 50 容器名

必要时用交互方式进临时容器复现:

docker run -it --entrypoint sh 镜像名

常见坑

  1. 只看 docker ps,挂掉的容器不在列表里,忘了 docker ps -a。
  2. 不看 logs 反复 run,浪费时间。
  3. 后台进程导致容器立刻退出,误以为镜像坏了。
  4. 端口被占却怀疑镜像,先查宿主机端口。
  5. OOMKilled 被忽略,实际是内存限制太小。
  6. 挂载路径拼反或覆盖了镜像内文件。
  7. 改了代码没重新 build,跑的还是旧镜像。

总结

容器启动失败排查主线:docker ps -a 找容器 → docker logs -f --tail 看真实报错 → docker inspect 看 ExitCode 和 OOMKilled → 对照端口、挂载、前台进程、依赖逐项修。日志是第一线索,别跳过它直接猜。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:42 

PHP开启错误显示php.ini配置方法

页面白屏、只报 500 却看不到具体错误,是 PHP 新手最头疼的问题。生产环境默认把错误藏起来是对的,但本地和调试环境必须能看到报错,否则只能盲猜。通过 php.ini 和脚本内函数两层控制错误显示与记录,下面讲清每个参数怎么配。

一、先找到 php.ini 位置

php --ini

或在 PHP 文件里打印:

<?php phpinfo(); ?>

页面里 Loaded Configuration File 那行就是当前生效的配置,改别的文件不生效。

二、调试环境要改的四个参数

在 php.ini 里:

; 把错误输出到页面(调试开,生产关)
display_errors = On
display_startup_errors = On

; 报告所有错误,包括通知和弃用提示
error_reporting = E_ALL

; 错误同时写进日志,便于排查致命错误
log_errors = On
error_log = /var/log/php_errors.log

改完重启 PHP-FPM 或 web 服务才生效:

systemctl restart php-fpm

三、脚本内临时开启(不改配置)

有时拿不到 php.ini,可在页面最前面加:

<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

注意:致命错误(如内存耗尽、语法错误)发生在这行之前就不会被页面显示,这种必须看错误日志。

四、error_reporting 常用级别

error_reporting(E_ALL);              // 全部,调试用
error_reporting(E_ALL & ~E_NOTICE);  // 忽略通知
error_reporting(E_ERROR | E_WARNING); // 只报错误和警告

五、生产环境务必关闭页面显示

display_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT

生产把错误直接回给用户会泄露路径、数据库结构等敏感信息,只写日志、不显示。

六、常见坑

  1. 改了 php.ini 没重启 FPM,以为没生效。
  2. 多个 php.ini(CLI 和 FPM 各一份),命令行和网页行为不一致。
  3. 致命错误发生在 ini_set 之前,页面依旧白屏,只能查 error_log。
  4. display_errors 开着但 error_reporting 把错误级别滤掉了,照样看不到。
  5. 日志目录不存在或 PHP 用户无写权限,log_errors 等于白开。
  6. 生产环境误开 display_errors,造成信息泄露。
  7. Nginx/FPM 层有自己的错误压制,页面显示和日志要分别确认。

总结

调试开 display_errors=On + error_reporting=E_ALL,生产关页面显示、只保留 log_errors。拿不到配置就用 ini_set 在脚本顶面临时开,致命错误永远以日志为准。改完一定重启服务再验证。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:42 

MySQL索引失效常见场景避免踩坑

建了索引却不走,查询照样全表扫描,是 MySQL 优化里最常见的坑。索引不是建了就一定用,优化器要根据数据分布、写法、类型判断值不值得走。下面把高频失效场景列清楚,配合 explain 验证。

一、先判断走没走索引

EXPLAIN SELECT * FROM user WHERE phone = '13800000000';

看 type 列:const/ref/range 是走了索引;ALL 就是全表扫描。key 列显示实际用到的索引,rows 是预估扫描行数。

二、常见失效场景

1. 对索引列做函数、运算、转换

-- 失效:对列用函数
SELECT * FROM user WHERE LEFT(phone,3) = '138';
-- 失效:算术运算
SELECT * FROM order WHERE id + 1 = 100;

正确写法把运算放到右边:

SELECT * FROM user WHERE phone LIKE '138%';
SELECT * FROM order WHERE id = 99;

2. 隐式类型转换

phone 是 varchar,却传数字,字符串被转成数字导致索引失效:

-- 失效:字符串列传数值
SELECT * FROM user WHERE phone = 13800000000;
-- 正确:加引号
SELECT * FROM user WHERE phone = '13800000000';

3. 复合索引违反最左前缀

索引 (a,b,c),必须从最左列 a 开始用。跳过 a 直接查 b、c 就走不全:

-- 走索引:用到 a
SELECT * FROM t WHERE a = 1;
-- 部分用到:a、b
SELECT * FROM t WHERE a = 1 AND b = 2;
-- 失效:没带最左列 a
SELECT * FROM t WHERE b = 2;

4. 用了 or,且其中一列无索引

-- name 无索引,整个 OR 导致全表扫描
SELECT * FROM user WHERE phone = 'x' OR name = '张三';

5. LIKE 以 % 开头

-- 失效:前导通配符
SELECT * FROM user WHERE name LIKE '%张';
-- 可用:右匹配
SELECT * FROM user WHERE name LIKE '张%';

6. 范围查询后右侧列用不上

复合索引里遇到 >、<、between、like 后,后面的列索引截断。

7. 优化器估算全表更快

表数据量小、或 where 命中比例太高(超过约 20%~30%),优化器认为扫全表比回表划算,主动放弃索引,这属于正常选择。

三、验证与整改

每次改写法都用 explain 复核:

# 看慢查询里有没有全表扫描
grep -E "Rows_examined" slow.log | head

整改思路:索引列保持原样不做函数运算、类型严格匹配、复合索引遵守最左前缀、OR 两侧都建索引或改 UNION。

常见坑

  1. 以为建了联合索引随便查哪列都行,忽略最左前缀。
  2. varchar 列忘加引号,隐式转换静默失效。
  3. 在 where 里写 IFNULL、SUBSTRING、DATE() 包裹索引列。
  4. !=、NOT IN 常导致放弃索引。
  5. 索引列参与 + - * 计算。
  6. 数据量小却纠结索引,反而被优化器忽略,属正常。
  7. 改完不看 explain,凭感觉判断。

总结

索引失效大多来自「在索引列上动手脚」:函数运算、隐式转换、违反最左前缀、前导 %、OR 缺索引。改完写法一定用 explain 的 type 和 key 列验证,确认从 ALL 变成 ref/range 才算修好。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:41 

Nginx报502 Bad Gateway排查思路

502 Bad Gateway 是 Nginx 反向代理时最常见的报错:Nginx 自己能响应,但它转发给后端(通常是 PHP-FPM 或上游服务)拿不到有效回应,于是返回 502。注意它和 504 不同:504 是后端超时,502 是连接被拒、进程挂掉或返回了非法响应。按下面顺序排查,绝大多数都能定位。

一、先看两个关键日志

# Nginx 错误日志,先看这里
tail -100 /var/log/nginx/error.log

# PHP-FPM 日志
tail -100 /var/log/php-fpm/error.log

Nginx error.log 会直接写出原因,比如 connect refused、upstream prematurely closed、recv() failed,对应不同病根。

二、检查 PHP-FPM 是否在运行

systemctl status php-fpm
# 或按版本名 php74-fpm
ps aux | grep php-fpm

进程没起来,Nginx 连不上端口或 socket,就是 502。先把它启动:

systemctl start php-fpm
systemctl enable php-fpm

三、检查监听端口或 Socket 是否匹配

Nginx 配置里 fastcgi_pass 要和 FPM 监听一致。常见两种:

# 方式一:TCP 端口
location ~ \.php$ {
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}
# 方式二:Unix Socket
fastcgi_pass unix:/run/php-fpm/www.sock;

对照 FPM 的 www.conf:

listen = 127.0.0.1:9000
; 或
listen = /run/php-fpm/www.sock

两边不一致,Nginx 连不上就 502。

四、FPM 进程池耗尽或崩溃

并发高、php-fpm 子进程被打满,新请求接不住。看 www.conf 的 pm 参数:

pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.max_requests = 500
# 查看当前 FPM 进程数
ts=$(ps -C php-fpm -o pid= | wc -l); echo $ts

内存不足时 FPM 会被 OOM 杀掉,dmesg 里找 out of memory。

五、脚本本身导致的问题

  • PHP 段错误、内存超限(memory_limit)、执行超时,FPM worker 崩溃,连接被中断,Nginx 报 upstream prematurely closed。
  • 文件权限:Nginx 读不到 PHP 文件、或 FPM 无执行权限,也会 502。
  • SCRIPT_FILENAME 路径写错,FPM 找不到脚本。

六、常见坑

  1. 改完 FPM 配置只 reload Nginx,忘了重启 php-fpm,参数不生效。
  2. socket 文件属主和 Nginx 用户不一致,权限拒绝。
  3. SELinux 或防火墙拦了 9000 端口。
  4. 多版本 PHP(7.4/8.0)同时装,起的不是 Nginx 指向的那个 FPM。
  5. 把 502 和 504 混为一谈:502 先查进程和连接,504 先查超时和慢查询。
  6. 容器部署时 FPM 和 Nginx 不在同一网络,主机名解析失败。
  7. 只重启服务不看日志,反复重启却找不到根因。

总结

502 的排查主线:先看 error.log 定原因 → 确认 FPM 在跑 → 核对 fastcgi_pass 与 listen 是否一致 → 看进程池是否打满或被 OOM → 检查脚本权限与内存。定位到具体一行日志,基本就能一次修好。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:40 

Linux日志切割logrotate配置详解

服务器跑久了,Nginx、MySQL、PHP 的日志会越滚越大,把磁盘吃到 100%。手动删日志不现实,而且服务还在往旧文件里写。logrotate 是 Linux 自带的日志切割工具,按天或按大小自动归档、压缩、删旧,并通知服务重新打开日志句柄。本文讲清配置位置、关键字段和常见坑。

一、logrotate 工作机制

logrotate 由系统定时任务驱动,通常在 /etc/cron.daily/logrotate 里每天跑一次。它读取 /etc/logrotate.conf 和 /etc/logrotate.d/ 下的配置,对符合条件的日志执行切割、压缩、保留策略。

# 查看默认主配置
cat /etc/logrotate.conf

# 查看各软件独立配置
ls /etc/logrotate.d/

二、一个完整 Nginx 日志配置示例

在 /etc/logrotate.d/nginx 写入:

/var/log/nginx/*.log {
    daily                    # 按天切割
    missingok                # 日志不存在不报错
    rotate 14                # 保留 14 份历史
    compress                 # 旧日志 gzip 压缩
    delaycompress            # 延迟一次再压缩,避免抢写
    notifempty               # 空文件不切割
    create 0640 nginx adm    # 新建日志的权限和属主
    sharedscripts            # 多个日志只执行一次脚本
    postrotate
        if [ -f /var/run/nginx.pid ]; then
            kill -USR1 `cat /var/run/nginx.pid`
        fi
    endscript
}

三、常用关键字段说明

  • daily:按天切割,也可换 weekly、monthly。
  • size 100M:超过大小就切割,和时间条件可共存。
  • rotate 14:保留份数,超出自动删除最旧的。
  • compress / nocompress:是否压缩历史日志。
  • notifempty:文件为空则跳过,防止产生一堆空归档。
  • dateext:归档名带日期,如 access.log-20261115.gz。
  • postrotate ... endscript:切割后执行的命令,最常见是给服务发信号重开日志。

四、手动测试配置

改完配置别等明天,先强制跑一遍验证:

# 只检查语法,不真正执行
logrotate -d /etc/logrotate.d/nginx

# 强制执行一次切割
logrotate -f /etc/logrotate.d/nginx

-d 是调试模式,会打印它打算怎么处理但不落盘,排错首选。

五、常见坑

  1. 切割后服务仍往旧文件写:必须在 postrotate 里给 Nginx 发 USR1 信号或 reload,让它重新打开日志文件,否则新日志写进归档。
  2. 压缩和写入冲突:同一次切割立刻压缩正在被写的文件会报错,用 delaycompress 延后一轮。
  3. 路径用通配符 /var/log/nginx/*.log 时,别把已经切割压缩的归档再匹配进去。
  4. rotate 份数设太小,误删还想查的历史;设太大又占磁盘,按业务保留周期定。
  5. 自定义配置文件放错目录:必须在 /etc/logrotate.d/ 下,主配置里要 include 该目录。
  6. 容器内日志不走 logrotate,要用 Docker 日志驱动或应用自身轮转。
  7. 改完没 reload,系统 cron 第二天才生效,紧急时手动 -f 执行。

总结

logrotate 靠「时间或大小触发 + 保留份数 + 压缩 + postrotate 通知服务」解决日志膨胀。配置写在 /etc/logrotate.d/,改完先用 -d 调试、再用 -f 强制执行验证,关键别漏掉切割后让服务重开日志的信号。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:39 

Git撤销commit回退版本命令详解

提交写错、信息填错、误把不该提交的文件 commit 上去,是 Git 日常最高频的失误。关键要先分清两件事:commit 还没推送到远程,用 reset 本地回退;已经推送到共享分支,用 revert 生成一个反向提交,不要硬改历史。选错命令轻则改乱本地,重则让队友拉取冲突。本文把常用撤销和回退命令讲清。

一、查看提交历史

先看清楚要回退到哪:

git log --oneline
a1b2c3d 最新提交
f4e5d6c 上一个提交
9a8b7c6 更早提交

二、只改提交信息(未推送)

刚 commit 完发现 message 写错,不改内容:

git commit --amend -m '正确的提交信息'

三、撤销最近一次 commit,保留改动(未推送)

--soft 回退到上一次提交,但改动留在暂存区,可以重新提交:

git reset --soft HEAD~1

--mixed(默认)回退并把改动放回工作区、取消暂存:

git reset --mixed HEAD~1

--hard 彻底丢弃最近一次提交的所有改动,慎用:

git reset --hard HEAD~1

回退到指定提交:

git reset --hard f4e5d6c

四、已推送到远程:用 revert

共享分支上别用 reset 强推,改用 revert 生成一个抵消提交:

git revert HEAD
git revert f4e5d6c

它保留完整历史,新增一条「反向提交」,队友正常 pull 即可。

五、误提交文件的处理

把不该提交的文件加进去了,未推送时:

# 从暂存区移除但保留本地文件
git reset HEAD 文件名
# 或直接编辑后重新提交
git rm --cached 文件名
git commit --amend

六、三种 reset 参数对比

  • --soft:撤销 commit,改动留在暂存区。
  • --mixed:撤销 commit,改动退回工作区(默认)。
  • --hard:撤销 commit 并删除改动,不可恢复。

常见坑

  1. 已 push 的分支用 reset --hard 再强推,会覆盖别人的提交,必须用 revert。
  2. reset --hard 丢弃的改动若没备份,工作区改动直接消失。
  3. amend 会生成新 commit,已推送的提交 amend 后哈希变化,团队协作会乱。
  4. HEAD~1 表示上一个,HEAD~2 表示倒数第二个,别数错。
  5. 回退后想找回,可用 git reflog 查看历史操作记录。
  6. 在公共分支强推前务必和团队确认,避免他人仓库历史分叉。

总结

未推送:改信息用 amend,保留改动用 reset --soft/--mixed,彻底丢弃用 --hard。已推送共享分支:一律 revert 生成反向提交。拿不准先 git log、git reflog 确认,再决定回退方式。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:32 

Docker数据卷与挂载目录区别详解

容器是临时的,删容器数据就没了,所以持久化必须靠挂载。Docker 提供三种存数据的方式:数据卷(volume)、绑定挂载(bind mount)和 tmpfs。新手最容易混淆 volume 和 bind mount,选错会导致权限乱、迁移难、数据丢失。本文讲清两者区别、适用场景和命令写法。

一、数据卷 volume(推荐)

由 Docker 自己管理,存放在 Docker 的目录下(Linux 一般在 /var/lib/docker/volumes/),完全脱离宿主机目录结构。

创建并运行:

# 创建命名卷
docker volume create mydata

# 挂载到容器
docker run -d -v mydata:/var/lib/mysql mysql:8.0

也可以直接在 run 时让 Docker 自动建卷:

docker run -d -v mysql_data:/var/lib/mysql mysql:8.0

查看卷、删除卷:

docker volume ls
docker volume inspect mydata
docker volume rm mydata

二、绑定挂载 bind mount

直接把宿主机上的某个真实目录或文件挂进容器,路径由你指定:

docker run -d -v /home/www/html:/usr/share/nginx/html nginx

也可以用新版 --mount 写法,更清晰:

docker run -d \
  --mount type=bind,source=/home/www/html,target=/usr/share/nginx/html \
  nginx

三、核心区别对比

  1. 归属:volume 归 Docker 管理;bind mount 是宿主机真实目录。
  2. 跨平台:volume 与宿主机路径解耦,Windows、Mac、Linux 都能用;bind mount 强依赖宿主机目录。
  3. 性能:volume 由 Docker 优化,大数据量、数据库推荐用卷;bind mount 在共享代码目录时很方便。
  4. 迁移备份:volume 可用 docker volume 命令和内置备份工具操作;bind mount 直接用宿主机工具拷目录。
  5. 容器间共享:多个容器挂同一个 volume 很容易;bind mount 也行但受目录权限限制。

四、怎么选

  • 数据库、持久化业务数据:用 volume,稳定可移植。
  • 开发时代码热更新、配置文件外挂:用 bind mount,改宿主机文件容器立刻生效。
  • 只存临时内存数据:用 tmpfs,重启即消。

五、常见坑

  1. bind mount 路径写错,容器启动报 No such file or directory,先确认宿主机目录存在。
  2. 宿主机目录属主是普通用户,容器内进程以 root 写文件导致权限拒绝。
  3. 用 -v 时前面路径不存在,Docker 在 Linux 会自动当成卷创建,造成预期外行为,绑定挂载前先 mkdir。
  4. 删容器时没加 -v,volume 不会随容器删除,长期堆积占磁盘,定期 docker volume prune 清理。
  5. Windows/Mac 挂载宿主机目录性能差,数据库不要用 bind mount。
  6. 混淆大小写路径,source 和 target 写反导致挂载点不对。

总结

持久化业务数据选 volume,交给 Docker 管理、跨平台好迁移;开发调试外挂代码和配置选 bind mount,直接映射宿主机目录。记牢:数据库用卷、代码用挂载,删容器前想清楚卷要不要一起删。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:31 

MySQL REGEXP正则匹配查询用法

MySQL 的 REGEXP(同义 RLIKE)用于按正则表达式匹配字符串,比 LIKE 通配符强大得多。LIKE 只能写 % 和 _ 做简单前后模糊,遇到「以数字开头」「包含连续三个字母」「手机号段校验」这类条件就力不从心,这时就要用 REGEXP。本文讲清基本语法、常用元字符和实战查询。

一、基本写法

SELECT * FROM users WHERE username REGEXP '^admin';

REGEXP 放在字段和模式之间,匹配成功返回 1。取反用 NOT REGEXP:

SELECT * FROM users WHERE email NOT REGEXP '@';

注意:REGEXP 不区分大小写(取决于校对集),要严格区分可加 BINARY。

二、常用元字符

  • ^ 匹配开头,$ 匹配结尾。
  • . 任意单个字符。
  • * 前项出现 0 次或多次,+ 出现 1 次以上,? 0 或 1 次。
  • [abc] 字符集合,[0-9] 数字,[a-z] 小写字母。
  • {n,m} 重复 n 到 m 次。
  • | 或,如 cat|dog。

三、实战示例

以数字开头的用户名:

SELECT * FROM users WHERE username REGEXP '^[0-9]';

校验手机号(11 位、1 开头):

SELECT * FROM users WHERE phone REGEXP '^1[3-9][0-9]{9}$';

包含连续两个相同字母:

SELECT * FROM words WHERE name REGEXP '([a-z])\\1';

匹配邮箱大致格式:

SELECT * FROM users WHERE email REGEXP '^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-z]{2,}$';

四、REGEXP 与 LIKE 怎么选

-- LIKE:只支持 % 与 _
SELECT * FROM goods WHERE title LIKE '%手机%';

-- REGEXP:规则化匹配
SELECT * FROM goods WHERE title REGEXP '手机|平板|电脑';

简单包含用 LIKE 更省资源;需要边界、字符类、重复次数时用 REGEXP。

五、常见坑

  1. REGEXP 走不到普通索引,大表全表扫描会很慢,只适合小表或后台筛选。
  2. 反斜杠在 SQL 字符串里要写两个 \\,否则被 MySQL 先解析一层。
  3. 忘记写 ^ 和 $,变成只要包含即可,结果范围过大。
  4. 中文匹配受字符集影响,[一-龥] 类写法在 utf8mb4 下不一定可靠。
  5. 把正则当强校验,前端/后端仍需独立校验,数据库正则只是粗筛。
  6. 大小写敏感需求忘了加 BINARY。

总结

REGEXP 适合规则化的字符串筛选:^、$ 定边界,[] 定字符集,{n,m} 定重复次数,竖线表或。它不走索引,别在大表高频查询里硬用;简单模糊仍优先 LIKE。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:30 

Nginx proxy_pass斜杠路径转发区别

Nginx 反向代理里 proxy_pass 末尾到底带不带斜杠,是最容易写错、也最容易导致后端 404 的地方。同样一个 location,proxy_pass 有没有结尾斜杠,转发给后端的 URL 路径完全不同。很多人配置后后端报 404、路径多一截或少一截,问题就出在这个斜杠上。本文把规则讲清并给对比例子。

一、核心规则

proxy_pass 的目标地址分两种写法:

  1. 带协议和主机,末尾可带路径或斜杠:http://127.0.0.1:8080/
  2. 不带路径(纯主机):http://127.0.0.1:8080

关键判断:当 proxy_pass 后带了 URI(哪怕只是一个 /),Nginx 会把 location 匹配到的那一段替换掉;不带 URI 时,则把完整原始路径原样拼到后端。

二、带斜杠:替换匹配段

location /api/ {
    proxy_pass http://127.0.0.1:8080/;
}

访问 /api/user/list,匹配 /api/,这一段被替换成 /,后端收到的是:

http://127.0.0.1:8080/user/list

/api/ 前缀被剥掉了。

三、不带斜杠:原样转发

location /api/ {
    proxy_pass http://127.0.0.1:8080;
}

访问 /api/user/list,后端收到完整路径:

http://127.0.0.1:8080/api/user/list

/api/ 保留,原样透传。

四、带自定义路径的情况

location /api/ {
    proxy_pass http://127.0.0.1:8080/backend/;
}

访问 /api/user,匹配段 /api/ 被替换成 /backend/,后端收到:

http://127.0.0.1:8080/backend/user

五、完整反向代理示例

location /app/ {
    proxy_pass http://127.0.0.1:9000/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

改完执行 nginx -t 校验,再 nginx -s reload 重载。

常见坑

  1. 后端本身就需要 /api/ 前缀,你却加斜杠剥掉,结果全 404。
  2. proxy_pass 写了主机又写端口,末尾漏斜杠导致路径重复。
  3. location 用正则 ~ 时不能在 proxy_pass 里写固定路径,需用变量拼接。
  4. 只改配置没 nginx -t 校验,重载失败导致整站 502。
  5. 后端框架按转发路径找路由,前缀不一致就报路由不存在。
  6. 误以为斜杠只是美观符号,实际它决定路径替换规则。

总结

记住一句话:proxy_pass 后带 URI(含 /)就替换 location 匹配段,不带就原样透传完整路径。先确认后端路由要不带 /api/,再决定斜杠去留,改完务必 nginx -t 再重载。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:29 

Linux rsync增量同步备份命令用法详解

rsync 是 Linux 下做文件同步和备份最常用的工具,核心优势是增量传输:只传有变化的部分,第一次全量、后续只补差异,比 scp、ftp 整包复制省大量带宽和时间。网站目录异地备份、两台服务器镜像、本地定时同步都靠它。本文讲清常用参数、远程同步写法和容易踩的坑。

一、本地目录同步

把 source 目录内容同步到 dest,保持一致:

rsync -av /data/web/ /backup/web/
  • -a 归档模式,保留权限、属主、时间、软链接,等于 -rlptgoD,备份必用。
  • -v 显示传输过程。

注意结尾斜杠:/data/web/ 表示同步目录里的内容;不带斜杠 /data/web 会把 web 目录本身拷过去,目标里多套一层。

二、通过 SSH 远程同步

拉取远端到本地:

rsync -avz root@192.168.1.10:/var/www/html/ /local/backup/

推送本地到远端:

rsync -avz /local/backup/ root@192.168.1.10:/var/www/html/
  • -z 传输时压缩,跨公网必加,省带宽。
  • 走 SSH,默认 22 端口;改了端口用 -e 'ssh -p 2222'。

三、常用关键参数

# 删除目标端多出的文件,让两端严格一致(镜像)
rsync -av --delete /data/web/ /backup/web/

# 先演练不真正执行,检查会传什么、删什么
rsync -avn --delete /data/web/ /backup/web/

# 显示进度条和传输速率
rsync -av --progress /src/ /dst/

# 排除指定文件/目录
rsync -av --exclude='logs/' --exclude='*.tmp' /src/ /dst/

-n(dry-run)特别重要,配合 --delete 先演练,避免误删。

四、做定时备份脚本

结合 crontab 每天增量备份网站:

#!/bin/bash
rsync -avz --delete --exclude='cache/' \
  root@192.168.1.10:/var/www/html/ \
  /backup/web/$(date +%F)/

配密钥免密后加入 crontab,凌晨自动跑:

crontab -e
0 3 * * * /usr/local/bin/backup.sh

五、常见坑

  1. 结尾斜杠忘加,目标目录凭空多套一层,备份路径错乱。
  2. --delete 没加 -n 演练,误删目标端已有文件。
  3. 远程用密码交互,定时任务卡住,必须配 SSH 密钥免密。
  4. 跨机器属主不一致,-a 在普通用户下可能无法保留 uid,必要时加 --fake-super 或用 root。
  5. 大量小文件首次全量慢属正常,后续增量很快,别误以为卡死。
  6. 防火墙或安全组没放行 SSH 端口,连接直接超时。

总结

rsync 备份记住三点:-avz 保权限并压缩;源目录结尾斜杠决定是否套层;--delete 先 -n 演练再执行。远程同步配密钥免密交给 crontab,即可实现稳定的每日增量异地备份。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:28 

PHP 命名空间 namespace 入门详解

PHP 命名空间 namespace 入门详解

一、命名空间解决什么问题

随着项目变大、引入第三方库,两个类同名(比如都叫 User)就会冲突,函数、常量重名也会报错。命名空间(namespace)相当于给代码划分独立隔间,让同名类在不同隔间里互不干扰,也是 Composer 自动加载 PSR-4 的基础。

二、基本声明

namespace 必须写在文件最顶部,前面不能有输出:

<?php
namespace App\Controller;

class User
{
    public function show()
    {
        return '控制器里的用户';
    }
}

反斜杠 \ 是层级分隔符,App\Controller 表示嵌套命名空间。

三、三种引用方式

在别的命名空间里使用这个类,有三种写法:

<?php
namespace App\View;

// 1. 全限定名,带开头反斜杠,从全局根开始找
$a = new \App\Controller\User();

// 2. use 引入后直接用(最常用)
use App\Controller\User;
$b = new User();

// 3. 起别名,类名冲突时用
use App\Controller\User as ControllerUser;
$c = new ControllerUser();

四、use 的规则

  • use 只作用于当前文件,每个文件要单独引入;
  • 一个 use 只引入一个类,多个类用逗号或分行:
    use App\Controller\User;
    use App\Model\Order;
  • 函数和常量在 PHP 8.0 前需要 use function、use const 单独引入;
  • 开头的 \ 表示全局空间,比如 \count() 强制用全局函数,避免被当前空间的同名函数遮蔽。

五、非限定、限定、完全限定名

namespace App\Controller;

new User();              // 非限定名,当前空间内找
new Model\User();        // 限定名,相对当前空间
new \App\Model\User();  // 完全限定名,从根开始

六、结合自动加载

命名空间路径要和目录结构对应,PSR-4 下 App\Controller\User 对应 App/Controller/User.php,Composer 据此自动 require:

{
  "autoload": {
    "psr-4": { "App\\": "app/" }
  }
}

命名空间写错或目录对不上,就会报“类未找到”。

七、常见误区

  1. namespace 写在 echo、HTML 之后,语法报错;
  2. 以为 use 能跨文件自动继承,别的文件仍需重新 use;
  3. 类名冲突时不起别名,两个 use 同名类后者覆盖;
  4. 全限定名漏写开头反斜杠,被当成相对当前空间查找;
  5. 目录结构和命名空间层级不一致,自动加载失败。

八、总结

命名空间用 namespace 划分类、函数、常量的归属,避免重名冲突;引用用全限定名、use 或别名三种方式,use 按文件独立、配合 PSR-4 自动加载。写在文件顶部、层级与目录对应、注意反斜杠的全局含义,就能避开绝大部分命名空间报错。


emer 发布于  2026-11-15 07:23 

HTTP 缓存 Cache-Control 与 ETag 详解

HTTP 缓存 Cache-Control 与 ETag 详解

一、为什么要做 HTTP 缓存

同一个静态资源(JS、CSS、图片)反复下载既耗带宽又拖慢首屏。HTTP 缓存让浏览器或 CDN 把资源存一份,再次访问直接复用,只在必要时才向服务器确认。缓存分两类:强缓存(不发请求)和协商缓存(发一次轻量请求确认)。

二、强缓存:Cache-Control

服务器通过响应头告诉浏览器资源能存多久,命中期间浏览器根本不发请求,直接用本地副本。

Cache-Control: max-age=86400, public

常见指令:

  • max-age=秒:缓存有效期,在期内直接用本地;
  • public:允许 CDN、代理缓存;
  • private:只允许浏览器缓存,代理不存;
  • no-cache:可以缓存,但每次都要协商确认;
  • no-store:完全不缓存。
location ~* \.(css|js|png|jpg)$ {
    add_header Cache-Control "max-age=604800, public";
}

注意:no-cache 不是“不缓存”,而是“缓存但要协商”;真正不缓存是 no-store。

三、协商缓存:ETag 与 Last-Modified

强缓存过期后,浏览器带上标识问服务器“资源变了吗”,没变就返回 304 Not Modified,不回传正文。

ETag(实体指纹):服务器给资源生成唯一校验值,内容一变 ETag 就变。

# 响应
ETag: "a1b2c3-1234"
# 再次请求带 If-None-Match
If-None-Match: "a1b2c3-1234"
# 未变化返回 304
304 Not Modified

Last-Modified:按修改时间判断,配合请求头 If-Modified-Since。

四、ETag 与 Last-Modified 的取舍

  • ETag 按内容指纹,精确;Last-Modified 只到秒级,同一秒多次改识别不出;
  • 两者同时存在时,ETag 优先级更高;
  • 动态内容、频繁变化用 ETag;纯静态文件也可只依赖时间;
  • 分布式多机生成 ETag 要保证算法一致,否则反复 304 失败。

五、缓存更新策略

静态文件带内容指纹文件名是最佳实践:

<script src="/app.8f3a21.js"></script>

内容变了文件名哈希就变,旧文件名长期缓存也不会命中旧版本,HTML 本身则短缓存或不缓存。

六、常见误区

  1. 把 no-cache 理解成不缓存,导致每次全量下载;
  2. 给经常改的入口 HTML 设超长 max-age,用户永远拿旧页面;
  3. 强缓存超长又不做指纹文件名,更新后用户看不到新版本;
  4. ETag 每次随机生成,协商缓存彻底失效;
  5. 只配强缓存不配协商缓存,max-age 一过全部重新下载。

七、验证方法

浏览器 F12 Network 看资源:

  • 强缓存命中显示 (disk cache),状态 200,无请求;
  • 协商缓存命中返回 304;
  • 响应头里能看到 Cache-Control、ETag。
    curl -I https://example.com/app.js

    查看返回的缓存头。

八、总结

Cache-Control 强缓存决定“存多久不用问”,ETag/Last-Modified 协商缓存决定“过期后轻量确认是否变化”。静态资源用指纹文件名加长缓存,HTML 短缓存,动态资源用协商。理清强缓存与协商缓存的配合,才能既省带宽又保证更新及时。


emer 发布于  2026-11-15 07:22 

Redis 持久化 RDB 与 AOF 对比

Redis 持久化 RDB 与 AOF 对比

一、为什么需要持久化

Redis 数据默认在内存里,进程重启或宕机就全丢。持久化就是把内存数据写到磁盘,重启后能恢复。Redis 提供两种机制:RDB 快照和 AOF 日志,二者可单独用也可同时开。

二、RDB 快照

RDB 在某个时刻把全量内存数据拍成二进制快照,存成 dump.rdb 文件。

# redis.conf
save 900 1      # 900秒内至少1次写则触发
save 300 10
save 60 10000
dbfilename dump.rdb
dir /var/lib/redis

触发方式:save 命令阻塞主线程,bgsave fork 子进程后台写。重启时直接加载 rdb。

三、AOF 日志

AOF 把每条写命令追加到日志文件,重启时重放命令重建数据。

appendonly yes
appendfilename appendonly.aof
appendfsync everysec   # 每秒刷盘,兼顾性能与安全

刷盘策略:always(每条都刷,最安全最慢)、everysec(默认,折中)、no(交给操作系统)。

四、核心对比

维度 RDB AOF
保存形式 某时刻全量二进制快照 逐条写命令日志
文件大小 小,压缩二进制 大,命令文本
恢复速度 快 较慢,需重放命令
数据安全 可能丢两次快照间数据 丢的数据更少(最多1秒)
对性能影响 bgsave fork 时短暂占用 持续追加,刷盘有开销

五、各自适合场景

  • RDB:做定时备份、灾备,能快速恢复;对数据丢失容忍度较高的缓存;
  • AOF:要求高可靠性,尽量少丢数据,把 Redis 当准数据库使用;
  • 生产常见做法:两者同时开启,AOF 保证近期数据不丢,RDB 用于快速冷备和灾难恢复。

六、混合持久化

Redis 4.0+ 支持混合模式:AOF 重写时把当前全量数据写成 RDB 格式放在文件头,之后增量命令追加在后面。重启先加载快照再重放少量增量,兼得恢复速度与数据安全:

aof-use-rdb-preamble yes

七、常见误区

  1. 以为开了 RDB 就零丢失,两次快照之间宕机仍会丢数据;
  2. 只用 save 阻塞命令,大实例时主线程卡顿;
  3. AOF 用 always 追求绝对安全,写性能明显下降;
  4. 只配持久化不做异地备份,磁盘损坏照样丢;
  5. 忽略 AOF 重写,日志无限膨胀占用磁盘。

八、运维检查命令

redis-cli info persistence

查看 rdb_last_save_time、aof_last_write_status、loading 等指标,确认持久化是否正常、有无上次写失败。

九、总结

RDB 是全量快照,文件小、恢复快但可能丢一段时间数据;AOF 是命令日志,数据更安全但文件大、恢复慢。生产建议 AOF 为主、RDB 做冷备,或开启混合持久化;按业务对丢失容忍度选择策略,并定期异地备份与监控持久化状态。


emer 发布于  2026-11-15 07:21 

Python 列表推导式与生成器对比

Python 列表推导式与生成器对比

一、两者解决的都是“批量转换”

处理一个序列、按规则生成另一个序列时,列表推导式和生成器是 Python 最常用的两种写法。它们语法相近,但内存模型完全不同:一个一次性把结果装进列表,一个按需逐个产出。

二、列表推导式

方括号 [] 包裹,立刻计算并返回完整列表:

# 把 0~9 的偶数平方放进列表
result = [x * x for x in range(10) if x % 2 == 0]
print(result)  # [0, 4, 16, 36, 64]

特点:所有结果同时占内存,可以反复索引、len()、切片;适合数据量小、需要多次访问的场景。

三、生成器表达式

圆括号 () 包裹,返回一个生成器对象,不立即计算:

g = (x * x for x in range(10) if x % 2 == 0)
print(g)  # <generator object ...>
print(next(g))  # 0
print(list(g))  # [4, 16, 36, 64]

每调用一次 next 才算下一个值,用完即弃,内存恒定,适合大数据流、文件逐行处理。

四、生成器函数 yield

用 yield 定义的函数也是生成器:

def squares(n):
    for x in range(n):
        yield x * x

for v in squares(5):
    print(v)  # 0 1 4 9 16

函数执行到 yield 暂停,返回值并记住现场,下次从暂停处继续,不会一次性构造列表。

五、内存与性能对比

import sys
a = [x for x in range(1_000_000)]
b = (x for x in range(1_000_000))
print(sys.getsizeof(a))  # 占用很大(约 8MB 级别,随数据增长)
print(sys.getsizeof(b))  # 固定很小(生成器对象本身)
  • 列表:O(n) 内存,可随机访问,计算一次性完成;
  • 生成器:O(1) 内存,只能顺序迭代,且只能遍历一次;
  • 生成器不能 len()、不能切片,遍历完就空了。

六、怎么选

  1. 结果要复用、要排序、要索引、数据量小 → 列表推导式;
  2. 处理超大文件、无限流、管道式计算 → 生成器;
  3. 直接传给 sum、max、join 这类消费函数时,用生成器更省内存:
    total = sum(x * x for x in range(1_000_000))

七、常见误区

  1. 以为生成器能反复遍历,第一次 for 循环后再取就是空;
  2. 大数据还套列表推导式,直接内存爆满;
  3. 把生成器当列表用 len(),报 TypeError;
  4. 嵌套推导式过度复杂,可读性下降,不如拆成普通循环;
  5. 生成器内部异常会在 next 时才暴露,调试时机靠后。

八、总结

列表推导式用 [] 一次性产出完整列表,适合小数据与重复访问;生成器表达式用 () 或 yield 按需产出,内存恒定但只能顺序消费一次。选择依据是数据规模和是否需要复用:小而多用列表,大而流式用生成器,传给归约函数优先用生成器省内存。


emer 发布于  2026-11-15 07:20 

Git 子模块 submodule 使用详解

Git 子模块 submodule 使用详解

一、什么时候用子模块

一个项目要引用另一个独立仓库(公共组件、SDK、前端 UI 库),既想跟着主仓库走,又想保留对方自己的版本历史。直接拷源码会丢失更新,用普通子目录复制又无法同步,这就是 Git 子模块(submodule)的场景:主仓库里只记录被引用仓库的一个特定提交,而不是全部文件。

二、添加子模块

git submodule add https://github.com/example/ui-lib.git third-party/ui

执行后会发生三件事:

  1. 在 third-party/ui 克隆远端仓库;
  2. 在主仓库生成/更新 .gitmodules(记录子模块路径与 URL);
  3. 主仓库暂存区记录该子模块当前 commit。

三、克隆含子模块的项目

普通 clone 只拿到空目录,需要初始化:

# 方式一:克隆时递归拉取
git clone --recurse-submodules https://github.com/example/main.git

# 方式二:已克隆后再补拉
git submodule init
git submodule update

四、更新子模块到最新

进入子模块目录像操作普通仓库一样拉取,回主仓库提交新指针:

cd third-party/ui
git checkout main
git pull
cd ../..
git add third-party/ui
git commit -m "update ui-lib to latest"

主仓库只保存一个 commit 指针,对方发新版本后主仓库不会自动变,要主动更新并提交。

五、查看与遍历

# 查看所有子模块当前状态
git submodule status

# 一次性更新所有子模块到远端最新
git submodule update --remote

# 批量在所有子模块执行命令
git submodule foreach 'git pull'

git submodule status 前面的 + 号表示子模块当前提交与主仓库记录不一致。

六、移除子模块

不能只删目录,要按规范清理:

git submodule deinit -f third-party/ui
git rm -f third-party/ui
rm -rf .git/modules/third-party/ui
git commit

七、常见坑

  1. clone 后子模块目录是空的,忘了 submodule init/update;
  2. 在子模块里改代码后回主仓库,只看到“指针变化”,误以为没保存;
  3. 在子模块处于游离 HEAD 状态直接提交,更新时丢失;
  4. 换分支后主仓库指针变了却不跑 submodule update,子模块停在旧版本;
  5. 删除子模块只删目录,残留 .gitmodules 和缓存导致再次添加报错。

八、子模块的取舍

优点:版本锁定、历史独立、可复用;缺点:操作比普通目录复杂,新手易踩坑。替代方案有 Git 子树(subtree)和包管理器(npm、Composer、Go modules),优先用包管理器,只有确实需要直接引用仓库源码时才用 submodule。

九、总结

submodule 让主仓库引用外部仓库的固定提交:添加用 add,克隆要 init/update,更新先在子模块拉取再回主仓库提交指针,移除必须 deinit + rm + 清理缓存。理解它只存 commit 指针这一点,就能避开大部分版本不同步的问题。


emer 发布于  2026-11-15 07:19 

PHP解析JSON中文转义json_encode解决 PHP

前言

json_encode 后中文变成 \uXXXX 转义码,影响可读性和前端处理。加 JSON_UNESCAPED_UNICODE 参数即可保留中文。本文讲解编码参数、解析 JSON、处理错误和中文长度注意。

操作步骤

1. 输出不转义中文

$data = ['name' => '刘馨怡'];
echo json_encode($data, JSON_UNESCAPED_UNICODE);
// {"name":"刘馨怡"}

2. 同时美化输出

echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

3. 解析 JSON 字符串

$obj = json_decode($json, true); // true 返回数组

4. 检查解析错误

$obj = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
    echo 'JSON 错误:' . json_last_error_msg();
}

5. 处理 UTF-8 合法性

$json = json_encode($data, JSON_UNESCAPED_UNICODE);

6. 保留换行斜杠等控制符按需加参数。

常见坑

  1. 漏加 JSON_UNESCAPED_UNICODE,中文全变 \u 码。
  2. json_decode 第二参不传 true,得到对象而非数组。
  3. 输入含非法 UTF-8,encode 返回 false。
  4. 中文引号、换行未转义,前端解析失败。
  5. JSON 不支持关联外的循环引用,会报错。
  6. 数字精度问题用 JSON_PRESERVE_ZERO_FRACTION。
  7. HTTP 响应要设 Content-Type: application/json。

总结

json_encode 加 JSON_UNESCAPED_UNICODE 保留中文,PRETTY_PRINT 美化;json_decode 传 true 得数组,用 json_last_error_msg 查错。输入必须合法 UTF-8,输出设 JSON 响应头。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:18 

PHP检测扩展是否开启extension_loaded函数 PHP

前言

代码用到某扩展(如 gd、mbstring、pdo_mysql)却报 Call to undefined function,说明扩展没装或没开启。运行前用 extension_loaded 判断,给出友好提示,比直接报错更稳。本文讲解检测函数、查看已装扩展、开启方法。

操作步骤

1. 检测扩展是否加载

if (!extension_loaded('gd')) {
    exit('需要开启 gd 扩展');
}

2. 同时检测多个扩展

$need = ['pdo_mysql', 'mbstring', 'json'];
foreach ($need as $e) {
    if (!extension_loaded($e)) exit("缺少扩展:$e");
}

3. 查看所有已加载扩展

print_r(get_loaded_extensions());

4. 查看 php.ini 位置

php_ini_loaded_file();

5. 命令行查看

php -m
php -r "var_dump(extension_loaded('gd'));"

6. 开启扩展(php.ini)

extension=gd

改完重启 php-fpm 或 web 服务。

常见坑

  1. 命令行和 FPM 是不同 php.ini,CLI 开了网页没开。
  2. extension_loaded 只查是否加载,不查版本。
  3. 函数不存在先用 function_exists 兜底也可。
  4. 编译内置扩展和动态扩展检测方式一致。
  5. 改了 php.ini 没重启,检测结果不变。
  6. 虚拟环境/宝塔面板有单独扩展管理。
  7. Windows 扩展名带 php_gd.dll,Linux 写 gd。

总结

extension_loaded('名称') 判断扩展是否加载,启动时批量检测给友好提示。CLI 与 FPM 用不同 ini,改完重启服务。用 php -m 和 get_loaded_extensions 核对,区分内置与动态扩展。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:18 

Docker 镜像分层原理与构建优化

Docker 镜像分层原理与构建优化

一、镜像为什么是分层的

Docker 镜像不是一个单一文件,而是由多层只读文件系统叠加而成。Dockerfile 里每一条指令(FROM、RUN、COPY、ADD)通常都会生成一层。下层只做增量修改,构建时相同层可被多个镜像共享,这就是镜像复用、拉取提速的基础。

二、分层与联合文件系统

底层用联合挂载(UnionFS)把多层目录合并成一个统一视图,容器启动时在最顶层加一层可写层,所有运行时改动都写在这层。镜像层只读,容器层可写,删除容器时可写层随之消失。

FROM php:8.2-cli
RUN apt-get update && apt-get install -y libzip-dev
COPY ./app /var/www/html
CMD ["php", "-S", "0.0.0.0:8000", "/var/www/html"]

每一行对应一层,基础镜像的层被复用,只新增自己的改动层。

三、构建缓存如何工作

Docker 按 Dockerfile 顺序逐层构建,某一层的指令和上游层未变化时,直接复用已有缓存层,不必重新执行。这就是改了一行代码却不必重装依赖的原因。

四、优化一:把变化少的指令放前面

依赖安装基本不变,业务代码频繁变动。顺序反过来会导致每次改代码都重装依赖:

# 推荐:先装依赖(缓存稳定),再拷代码
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npm", "start"]

COPY . . 每次代码变更都会让它之后的层失效,必须放在 npm ci 之后。

五、优化二:合并 RUN、清理缓存

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

多个 RUN 会产生多层且缓存残留无法删除;合并成一层并在同层清理安装缓存,镜像更小。

六、优化三:多阶段构建

编译产物和构建工具分开,最终镜像只留运行所需:

# 构建阶段
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o app
# 运行阶段,只拷贝二进制
FROM alpine:latest
COPY --from=builder /src/app /app
CMD ["/app"]

最终镜像不带 Go 工具链,体积大幅下降。

七、其他实用技巧

  • 用 .dockerignore 排除 node_modules、.git、日志,避免无谓拷入;
  • 选小基础镜像(alpine、distroless),但注意 glibc 兼容;
  • COPY 优于 ADD,ADD 会自动解压缩带来不确定性;
  • 避免 FROM latest 这种浮动标签,保证可复现。

八、常见误区

  1. 把 COPY . . 放在 RUN 安装依赖之前,缓存全失效;
  2. 每条 RUN 单独写,镜像层层叠加又删不掉缓存;
  3. 直接用超大基础镜像塞进编译工具;
  4. 忽略 .dockerignore,把源码和依赖目录一起打进镜像;
  5. 以为分层越多越好,其实多余层只增体积不增价值。

九、总结

镜像分层让相同层共享、构建可缓存;优化关键是稳定指令前置、合并 RUN 并清理、用多阶段构建只留运行产物、配合 .dockerignore 和小基础镜像。理解层与缓存机制,才能把镜像做小、构建做快。


emer 发布于  2026-11-15 07:18 

JavaScript 闭包与作用域链详解

JavaScript 闭包与作用域链详解

一、什么是闭包

闭包(Closure)是指函数能够记住并访问它词法作用域里的变量,即使外层函数已经执行结束。简单说:内层函数引用了外层函数的变量,外层函数返回后,这些变量不会被垃圾回收,依然存活在闭包里。

二、作用域与作用域链

JavaScript 有全局作用域、函数作用域(ES6 后还有块级作用域)。查找变量时,引擎从当前作用域开始,一层层向外找,直到全局,这条查找路径就是作用域链。

let a = '全局';
function outer() {
    let b = '外层';
    function inner() {
        let c = '内层';
        console.log(a, b, c); // 沿作用域链逐层找到
    }
    inner();
}
outer();

inner 找不到 b、a 时就沿链向上找。

三、最简单的闭包

function makeCounter() {
    let count = 0; // 被闭包保护的私有变量
    return function () {
        count++;
        return count;
    };
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2

makeCounter 执行完本应销毁 count,但返回的匿名函数仍引用它,于是 count 被保留,且外部无法直接改动,形成私有状态。

四、常见应用场景

  1. 数据私有化:变量只能通过返回的方法访问,避免污染全局;
  2. 函数工厂:批量生成带不同配置的函数;
  3. 回调与事件监听:定时器、点击回调记住当时的环境变量。
    function power(n) {
    return function (x) {
        return x ** n;
    };
    }
    const square = power(2);
    console.log(square(5)); // 25

五、经典循环陷阱

// 错误:var 共享同一变量,结果全是 3
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 0); // 3 3 3
}
// 正确:let 每轮块级绑定,或用立即执行函数隔离
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 0); // 0 1 2
}

var 声明的 i 挂在函数作用域,三个回调闭包引用同一个 i;let 每轮生成新绑定,各自闭包保存对应值。

六、闭包的代价与注意

  • 被闭包引用的变量不会回收,滥用会造成内存占用上升;
  • 不需要的闭包及时把引用置 null,方便 GC;
  • 闭包里的 this 指向由调用方式决定,不继承外层,需要保存时常用 const self = this;
  • 不要在循环里盲目创建大量闭包,避免性能损耗。

七、常见误区

  1. 以为闭包只是“函数套函数”,关键是对外层变量的持续引用;
  2. 以为返回函数后外层变量会自动清空,实际被闭包留住;
  3. 混淆作用域链查找方向,误以为内层能直接改外层;
  4. 把闭包当万能优化,忽略内存开销。

八、总结

闭包 = 函数 + 它引用的外层词法作用域。作用域链决定变量从内向外如何查找,闭包让外层变量在函数退出后仍然可访问。它用于私有化状态和函数工厂,但要警惕内存保留,循环场景优先用 let。理解闭包,才算真正懂了 JavaScript 的函数式特性。


emer 发布于  2026-11-15 07:18 

PHP数组键值互换array_flip用法 PHP

前言

需要把数组的值变成键、键变成值时,array_flip 一步完成,常用于反向查找、去重、建立快速索引。但它对重复值和值类型有限制。本文讲解基本用法、反向查找、限制和替代方案。

操作步骤

1. 键值互换

$fruits = ['a' => '苹果', 'b' => '香蕉'];
$reversed = array_flip($fruits);
// ['苹果'=>'a', '香蕉'=>'b']

2. 反向判断某值是否存在

$codes = ['苹果' => 'a'];
if (isset($codes['苹果'])) {
    echo '存在,代码:' . $codes['苹果'];
}

3. 配合去重

$unique = array_flip(array_flip($arr));

4. 用值做 O(1) 查询

$allow = array_flip(['jpg','png','gif']);
if (isset($allow[$ext])) { /* 允许 */ }

常见坑

  1. 值重复时,后面的覆盖前面的键,数据丢失。
  2. 值必须是字符串或整数,浮点/布尔互换会出错。
  3. 原数组键是数字,互换后变成字符串键,注意类型。
  4. 二维数组不能直接 flip,只支持一维标量值。
  5. 以为 flip 不改变原数组,它返回新数组。
  6. 关联数组顺序在 flip 后按新键排列。
  7. 大数组 flip 复制一份,内存翻倍。

总结

array_flip 把值变键、键变值,适合反向查找和白名单 O(1) 判断。要求一维、值为字符串/整数,重复值会覆盖。去重可用两次 flip,复杂结构需手动转换。

遇到问题加QQ23979811 协助处理


emer 发布于  2026-11-15 07:17 
'); })();