Linux查看进程占用CPU,top和ps命令
前言
服务器突然卡了?CPU跑满了?想知道哪个进程占CPU最多?本文讲top和ps命令,帮你快速定位问题进程。
一、top命令 — 实时监控
启动top
top
top界面说明
第一行:
- 系统时间、运行时间、登录用户数、负载(1/5/15分钟)
第二行:
- 任务总数、运行中、睡眠中、停止、僵尸进程
第三行(CPU):
- us:用户态占用
- sy:内核态占用
- id:空闲
第四行(内存):
- total:总内存
- used:已用
- free:空闲
进程列表:
- PID:进程ID
- USER:运行用户
- %CPU:CPU占用
- %MEM:内存占用
- COMMAND:进程名
top常用快捷键
| 快捷键 | 作用 |
|---|---|
| P | 按CPU排序 |
| M | 按内存排序 |
| k | 杀掉进程 |
| q | 退出 |
| 1 | 显示每个CPU核心 |
只看某个进程
top -p 进程ID
二、ps命令 — 查看进程快照
查看所有进程
ps aux
按CPU排序
ps aux --sort=-%cpu | head -10
解释:
- aux:显示所有进程
- --sort=-%cpu:按CPU降序
- head -10:只看前10个
按内存排序
ps aux --sort=-%mem | head -10
查找特定进程
ps aux | grep nginx
三、查找占用CPU最高的进程
方法1:用top
top
按P,按CPU排序,第一个就是占用最高的。
方法2:用ps
ps aux --sort=-%cpu | head -5
输出:
USER PID %CPU %MEM COMMAND
root 1234 50.0 10.0 php-fpm
四、查找占用内存最高的进程
ps aux --sort=-%mem | head -5
五、杀掉进程
根据PID杀
kill 进程ID
强制杀
kill -9 进程ID
根据进程名杀
pkill nginx
杀掉所有php-fpm
pkill php-fpm
六、常见场景
场景1:CPU跑满了
排查步骤:
- 用top看哪个进程CPU高
- 如果是php-fpm,看是不是有慢请求
- 如果是mysql,看是不是有慢SQL
- 杀掉异常进程
场景2:内存占满了
排查步骤:
- 用top按M排序
- 看哪个进程内存高
- 杀掉或重启
场景3:某个进程一直跑
ps aux | grep 进程名
找到PID后杀掉。
七、常见坑
坑1:top和ps看到的CPU不一样
原因: top是实时刷新的,ps是快照。
解决: 用top看实时变化。
坑2:杀了进程又起来
原因: 有守护进程自动拉起。
解决: 找到父进程一起杀,或者停掉服务。
systemctl stop php-fpm
坑3:僵尸进程
现象: 进程状态是Z(Zombie)。
原因: 子进程退出了,父进程没回收。
解决: 重启父进程。
八、常用命令汇总
# 实时监控
top
# 按CPU排序
ps aux --sort=-%cpu | head -10
# 按内存排序
ps aux --sort=-%mem | head -10
# 查找进程
ps aux | grep 进程名
# 杀进程
kill PID
kill -9 PID
pkill 进程名
九、负载怎么看
什么是负载
负载就是正在等待CPU运行的进程数。
怎么判断高不高
- 负载 < CPU核心数:正常
- 负载 > CPU核心数:有点忙
- 负载 > 2倍CPU核心数:很卡
查看CPU核心数
nproc
总结
记住:
- 用top实时看CPU和内存
- 按P按CPU排序,按M按内存排序
- 用ps aux --sort=-%cpu找CPU最高的
- 用kill -9 PID杀进程
- 负载超过CPU核心数就有问题
最常用:
top
ps aux --sort=-%cpu | head -10
kill -9 进程ID
遇到问题加QQ23979811 协助处理
PHP常用数组函数,10个必须掌握
前言
PHP数组函数太多了,记不住?本文整理了10个最常用的数组函数,写代码天天用,必须掌握。
一、array_column — 取数组某一列
用法
$users = [
['id' => 1, 'name' => '张三'],
['id' => 2, 'name' => '李四'],
['id' => 3, 'name' => '王五']
];
$names = array_column($users, 'name');
// 结果:['张三', '李四', '王五']
取列并指定键
$userMap = array_column($users, 'name', 'id');
// 结果:[1 => '张三', 2 => '李四', 3 => '王五']
二、array_map — 遍历处理数组
用法
$numbers = [1, 2, 3, 4, 5];
$squares = array_map(function($n) {
return $n * $n;
}, $numbers);
// 结果:[1, 4, 9, 16, 25]
实际应用
// 把所有名字转成大写
$names = ['zhang', 'li', 'wang'];
$upperNames = array_map('strtoupper', $names);
// 结果:['ZHANG', 'LI', 'WANG']
三、array_filter — 过滤数组
用法
$numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
$even = array_filter($numbers, function($n) {
return $n % 2 == 0;
});
// 结果:[2, 4, 6, 8, 10]
过滤空值
$arr = ['a', '', 0, null, 'b', '0'];
$result = array_filter($arr);
// 结果:['a', 'b']
四、array_merge — 数组合并
用法
$arr1 = ['a', 'b'];
$arr2 = ['c', 'd'];
$merged = array_merge($arr1, $arr2);
// 结果:['a', 'b', 'c', 'd']
关联数组合并
$arr1 = ['name' => '张三', 'age' => 20];
$arr2 = ['age' => 25, 'city' => '北京'];
$merged = array_merge($arr1, $arr2);
// 结果:['name' => '张三', 'age' => 25, 'city' => '北京']
// 注意:age被后面的覆盖了
五、in_array — 判断值是否在数组里
用法
$fruits = ['apple', 'banana', 'orange'];
if (in_array('apple', $fruits)) {
echo '有苹果';
}
注意严格模式
$arr = [1, '2', '3'];
in_array(2, $arr); // true(因为类型转换了)
in_array(2, $arr, true); // false(严格模式)
六、array_keys — 取数组的键
用法
$user = ['name' => '张三', 'age' => 20, 'city' => '北京'];
$keys = array_keys($user);
// 结果:['name', 'age', 'city']
找某个值的键
$ages = [
'张三' => 20,
'李四' => 25,
'王五' => 30
];
$keys = array_keys($ages, 25);
// 结果:['李四']
七、array_values — 取数组的值
用法
$user = ['name' => '张三', 'age' => 20, 'city' => '北京'];
$values = array_values($user);
// 结果:['张三', 20, '北京']
八、array_sum — 数组求和
用法
$numbers = [1, 2, 3, 4, 5];
$sum = array_sum($numbers);
// 结果:15
九、count — 统计数组长度
用法
$fruits = ['apple', 'banana', 'orange'];
$count = count($fruits);
// 结果:3
多维数组
$users = [
['name' => '张三'],
['name' => '李四']
];
count($users); // 2
count($users, COUNT_RECURSIVE); // 4(递归统计)
十、explode / implode — 字符串和数组互转
explode — 字符串转数组
$str = 'apple,banana,orange';
$fruits = explode(',', $str);
// 结果:['apple', 'banana', 'orange']
implode — 数组转字符串
$fruits = ['apple', 'banana', 'orange'];
$str = implode(',', $fruits);
// 结果:'apple,banana,orange'
十一、常用场景
场景1:从数据库取数据,提取某列
$users = Db::name('user')->select();
$names = array_column($users, 'name');
场景2:过滤空值
$list = array_filter($list);
场景3:判断是否存在
if (in_array($userId, $adminIds)) {
// 是管理员
}
场景4:数组去重后转字符串
$tags = array_unique($tags);
$tagStr = implode(',', $tags);
十二、常见坑
坑1:array_merge关联数组键名覆盖
问题: 相同键名会被覆盖。
解决: 用+号或array_replace。
坑2:in_array类型不严格
问题: in_array(0, ['a', 'b'])返回true。
解决: 加第三个参数true,严格模式。
坑3:count返回0
问题: 传了null进去。
解决: 先判断是不是数组。
总结
10个必须掌握的数组函数:
array_column— 取某一列array_map— 遍历处理array_filter— 过滤array_merge— 合并in_array— 判断存在array_keys— 取键array_values— 取值array_sum— 求和count— 统计长度explode/implode— 字符串数组互转
记住:
- 天天用的就是这10个
- 写代码前先想想有没有现成函数
- 别自己写循环,用内置函数更快
遇到问题加QQ23979811 协助处理
Nginx配置静态资源缓存,提升网站速度
前言
每次访问网站都要重新下载图片、CSS、JS?太浪费了!配置Nginx静态资源缓存,让浏览器本地缓存这些文件,下次访问直接从本地读,速度快好几倍。
一、什么是浏览器缓存
浏览器访问网站后,把图片、CSS、JS等静态文件存在本地。下次再访问,直接从本地读,不用再从服务器下载。
好处:
- 加载更快
- 省带宽
- 减轻服务器压力
二、配置静态资源缓存
打开Nginx配置文件,在server段加:
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
参数解释
| 参数 | 含义 |
|---|---|
| expires 30d | 缓存30天 |
| expires 1h | 缓存1小时 |
| expires 1M | 缓存1个月 |
| Cache-Control public | 允许所有缓存 |
不同文件缓存时间
# 图片缓存30天
location ~* \.(png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
}
# CSS/JS缓存7天
location ~* \.(css|js)$ {
expires 7d;
}
# HTML不缓存(保证更新及时)
location ~* \.html$ {
expires -1;
}
三、验证配置
nginx -t
重载Nginx:
nginx -s reload
四、验证是否生效
方法1:curl测试
curl -I https://www.0454.wang/static/logo.png
看到响应头有:
Cache-Control: max-age=2592000
Expires: ...
就是生效了。
方法2:浏览器F12
按F12 → Network → 点图片请求 → 看Response Headers:
Cache-Control: max-age=2592000
五、缓存更新策略
问题: 缓存了30天,但我更新了图片怎么办?
方法1:文件名加版本号
<img src="/logo.png?v=20240101">
浏览器看到文件名变了,就会重新下载。
方法2:文件名加hash
<img src="/logo.abc123.png">
构建工具自动加hash,内容变了hash就变。
六、常见坑
坑1:HTML也被缓存了
问题: 更新了网站内容,但用户看到的还是旧的。
解决: HTML不缓存
location ~* \.html$ {
expires -1;
}
坑2:缓存时间太长
问题: 图片更新了,用户30天都看不到新的。
解决: 用版本号或hash。
坑3:CDN缓存了旧文件
问题: Nginx配置了,但CDN没刷新。
解决: 去CDN后台刷新缓存。
坑4:没加expires就没缓存
问题: 配置了但不生效。
检查: 看location有没有匹配到。
七、完整配置示例
server {
listen 80;
server_name www.0454.wang;
root /var/www/html;
# HTML不缓存
location ~* \.html$ {
expires -1;
}
# 图片缓存30天
location ~* \.(png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
add_header Cache-Control "public";
}
# CSS/JS缓存7天
location ~* \.(css|js)$ {
expires 7d;
add_header Cache-Control "public";
}
}
八、缓存时间怎么设
| 文件类型 | 推荐缓存时间 |
|---|---|
| 图片(png/jpg) | 30天 |
| CSS/JS | 7天 |
| HTML | 不缓存 |
| 字体文件 | 30天 |
九、怎么清除缓存
浏览器端
- 强制刷新:Ctrl + F5
- 清除浏览器缓存
服务器端
- 更新文件名版本号
- 重载Nginx
总结
记住:
- 静态资源加缓存,提升速度
- 图片缓存30天
- CSS/JS缓存7天
- HTML不缓存
- 更新了就改文件名版本号
- 用curl验证是否生效
最常用配置:
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 30d;
add_header Cache-Control "public";
}
遇到问题加QQ23979811 协助处理
MySQL索引失效的10种场景,别再踩坑了
前言
明明加了索引,但查询还是慢?explain一看type是ALL,索引根本没生效!本文总结10种索引失效的场景,帮你避坑。
一、对索引字段用函数
失效写法
SELECT * FROM users WHERE LEFT(name, 3) = '张三';
SELECT * FROM users WHERE DATE(create_time) = '2024-01-01';
正确写法
SELECT * FROM users WHERE name LIKE '张三%';
SELECT * FROM users WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';
原因: 对索引字段用函数,MySQL不知道索引值会变成什么,索引用不上。
二、索引字段用运算
失效写法
SELECT * FROM users WHERE age + 1 = 20;
SELECT * FROM orders WHERE price * 2 > 100;
正确写法
SELECT * FROM users WHERE age = 19;
SELECT * FROM orders WHERE price > 50;
原因: 运算后索引值变了,MySQL没法用索引。
三、最左前缀原则不满足
联合索引
INDEX idx_name_age_city(name, age, city)
失效写法
SELECT * FROM users WHERE age = 20; -- 跳过了name
SELECT * FROM users WHERE city = '北京'; -- 跳过了name和age
正确写法
SELECT * FROM users WHERE name = '张三';
SELECT * FROM users WHERE name = '张三' AND age = 20;
原则: 联合索引要从最左边的字段开始用,不能跳过。
四、like以%开头
失效写法
SELECT * FROM users WHERE name LIKE '%张三%';
SELECT * FROM users WHERE name LIKE '%张三';
正确写法
SELECT * FROM users WHERE name LIKE '张三%';
原因: 以%开头,MySQL不知道从哪开始找,索引用不上。
五、类型不匹配
失效写法
-- phone是varchar类型
SELECT * FROM users WHERE phone = 13800138000; -- 传了数字
正确写法
SELECT * FROM users WHERE phone = '13800138000'; -- 传字符串
原因: 类型不匹配,MySQL会做隐式转换,索引用不上。
六、OR两边有没索引的字段
失效写法
-- name有索引,age没索引
SELECT * FROM users WHERE name = '张三' OR age = 20;
原因: OR只要有一边没索引,MySQL就干脆全表扫描。
解决
-- age也加索引
ALTER TABLE users ADD INDEX idx_age(age);
七、使用NOT、!=、<>
失效写法
SELECT * FROM users WHERE name != '张三';
SELECT * FROM users WHERE age NOT IN (18, 20);
原因: 不等于操作,MySQL觉得要查大部分数据,不如全表扫描。
解决
-- 改成范围查询
SELECT * FROM users WHERE name > '张三';
八、is null / is not null
这个要看情况,有的版本会失效,有的不会。
建议: 尽量让字段不要为null,设默认值。
九、查询条件用了函数或表达式
失效写法
SELECT * FROM users WHERE ABS(age) = 20;
SELECT * FROM users WHERE age / 2 = 10;
正确写法
SELECT * FROM users WHERE age = 20;
十、MySQL自己判断不走索引
有时候SQL写法没问题,但MySQL优化器觉得走索引还不如全表扫描。
比如: 表数据很少,或者要查的数据占表的大部分。
解决: 用FORCE INDEX强制走索引
SELECT * FROM users FORCE INDEX(idx_name) WHERE name = '张三';
十一、怎么检查索引有没有生效
用explain
EXPLAIN SELECT * FROM users WHERE name = '张三';
看key字段:
- key有值:用了索引
- key是NULL:没用到索引
看type字段:
- type=ALL:全表扫描
- type=ref/range:用了索引
十二、常见坑
坑1:加了索引但SQL写法不对
解决: 按上面10种场景检查。
坑2:索引加太多
索引不是越多越好,写的时候要维护索引,会变慢。
建议: 单表索引不超过5个。
坑3:区分度低的字段不加索引
比如gender(男/女),加索引没用。
原则: 区分度高的字段加索引。
坑4:隐式类型转换
例子: varchar字段传了数字。
解决: 传参时类型要匹配。
十三、索引使用原则
- 对WHERE条件的字段加索引
- 对ORDER BY的字段加索引
- 联合索引遵守最左前缀
- 不要对索引字段用函数
- 不要对索引字段做运算
- like不要以%开头
- 类型要匹配
- 区分度高的字段加索引
总结
10种索引失效场景:
- 对索引字段用函数
- 索引字段用运算
- 最左前缀原则不满足
- like以%开头
- 类型不匹配
- OR两边有没索引的字段
- 使用NOT、!=、<>
- is null / is not null
- 查询条件用了表达式
- MySQL自己判断不走索引
记住:
- 用explain检查索引有没有生效
- key=NULL就是没用到索引
- type=ALL就是全表扫描
- 避开上面10种场景
遇到问题加QQ23979811 协助处理
Docker常用命令大全,新手必看
前言
Docker现在用得越来越多,但很多新手记不住命令。本文整理了最常用的Docker命令,收藏起来随用随查。
一、镜像相关命令
拉取镜像
docker pull nginx:latest
查看本地镜像
docker images
删除镜像
docker rmi 镜像ID
强制删除镜像
docker rmi -f 镜像ID
搜索镜像
docker search nginx
二、容器相关命令
查看运行中的容器
docker ps
查看所有容器(包括停止的)
docker ps -a
启动容器
docker start 容器名/容器ID
停止容器
docker stop 容器名/容器ID
重启容器
docker restart 容器名/容器ID
删除容器
docker rm 容器名/容器ID
强制删除运行中的容器
docker rm -f 容器名/容器ID
进入容器
docker exec -it 容器名/容器ID /bin/bash
查看容器日志
docker logs 容器名/容器ID
实时查看日志
docker logs -f 容器名/容器ID
查看容器详细信息
docker inspect 容器名/容器ID
查看容器IP
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 容器名
查看容器资源占用
docker stats
三、运行容器
后台运行
docker run -d --name mynginx -p 80:80 nginx
参数解释:
- -d:后台运行
- --name:容器名
- -p:端口映射(宿主端口:容器端口)
运行并进入容器
docker run -it ubuntu /bin/bash
挂载目录
docker run -d -v /host/path:/container/path nginx
环境变量
docker run -e MYSQL_ROOT_PASSWORD=123456 mysql
自动重启
docker run --restart=always -d nginx
四、清理命令
停止所有容器
docker stop $(docker ps -q)
删除所有停止的容器
docker container prune
删除所有未使用的镜像
docker image prune -a
一键清理
docker system prune -a
五、Docker Compose命令
启动
docker-compose up -d
停止
docker-compose down
查看状态
docker-compose ps
查看日志
docker-compose logs -f
重启
docker-compose restart
六、常用场景
运行MySQL
docker run -d \
--name mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
-v /data/mysql:/var/lib/mysql \
mysql:5.7
运行Nginx
docker run -d \
--name nginx \
-p 80:80 \
-v /www:/usr/share/nginx/html \
nginx
运行Redis
docker run -d \
--name redis \
-p 6379:6379 \
redis:alpine
七、常见坑
坑1:容器启动后马上退出
原因: 容器没有前台进程。
解决: 加-it或者tail -f /dev/null。
坑2:端口映射不对
问题: 容器里跑的是80端口,映射成了8080:80。
解决: 确认容器内端口和宿主机端口。
坑3:数据丢失
问题: 容器删了数据就没了。
解决: 一定要挂载数据卷-v。
坑4:镜像太大
解决: 用alpine版本,比如redis:alpine。
八、命令速查表
| 功能 | 命令 |
|---|---|
| 查看运行中容器 | docker ps |
| 查看所有容器 | docker ps -a |
| 查看镜像 | docker images |
| 拉取镜像 | docker pull 镜像名 |
| 运行容器 | docker run -d --name 名字 -p 端口:端口 镜像 |
| 进入容器 | docker exec -it 名字 /bin/bash |
| 查看日志 | docker logs -f 名字 |
| 停止容器 | docker stop 名字 |
| 删除容器 | docker rm 名字 |
| 删除镜像 | docker rmi 镜像ID |
| 清理 | docker system prune -a |
总结
记住:
docker ps看容器docker images看镜像docker run启动容器docker exec -it进入容器docker logs -f看日志docker system prune清理
最常用:
docker ps
docker images
docker exec -it 容器名 /bin/bash
docker logs -f 容器名
遇到问题加QQ23979811 协助处理
Linux磁盘满了怎么清理,大文件查找命令
前言
网站突然打不开了?SSH连不上了?一查磁盘100%满了!本文讲怎么查找大文件、清理磁盘空间。
一、查看磁盘使用情况
df命令
df -h
输出:
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 40G 38G 2.0G 95% /
看什么:
- Use%:使用率,超过90%就要注意了
- Avail:可用空间
查看当前目录占用
du -sh
二、查找大文件
查找大于100M的文件
find / -type f -size +100M 2>/dev/null
解释:
- /:从根目录开始找
- -type f:只找文件
- -size +100M:大于100M
- 2>/dev/null:忽略错误信息
查找大于1G的文件
find / -type f -size +1G 2>/dev/null
按大小排序
find / -type f -size +100M 2>/dev/null | xargs du -h | sort -rh
三、查找大目录
查看各目录大小
du -sh /*
一级一级往下找
du -sh /var/*
du -sh /var/log/*
du -sh /var/lib/*
查看前10大目录
du -sh /* | sort -rh | head -10
四、常见大文件位置
1. 日志文件
/var/log/
/var/log/messages
/var/log/nginx/
/var/log/mysql/
2. 临时文件
/tmp/
/var/tmp/
3. 邮件
/var/spool/mail/
4. 数据库文件
/var/lib/mysql/
5. Docker镜像
/var/lib/docker/
6. 上传文件
/www/wwwroot/
/var/www/html/
五、清理方法
清理日志文件
方法1:清空文件
> /var/log/messages
方法2:删除旧日志
find /var/log -name "*.log" -mtime +30 -delete
删除30天前的日志。
清理yum缓存
yum clean all
清理Docker
docker system prune -a
清理临时文件
rm -rf /tmp/*
注意: 正在用的文件别删。
清理旧内核
package-cleanup --oldkernels --count=1
六、查看文件被谁占用
问题: 删了文件但磁盘没释放?
原因: 有进程还在写这个文件。
查看被删除但还被占用的文件
lsof | grep deleted
重启相关进程
systemctl restart nginx
七、常见坑
坑1:删了文件但空间没释放
原因: 进程还在写这个文件。
解决: 重启进程,或者用> 文件名清空。
坑2:日志一直涨
解决: 配置logrotate日志切割。
坑3:Docker占了很多空间
docker system df
清理:
docker system prune -a
八、自动清理脚本
写个脚本定期清理:
#!/bin/bash
# 清理30天前的日志
find /var/log -name "*.log" -mtime +30 -delete
# 清理yum缓存
yum clean all
# 清理Docker
docker system prune -f
加到crontab:
0 3 * * * /bin/bash /root/clean.sh
总结
记住:
- 用
df -h看磁盘使用率 - 用
du -sh /*看哪个目录大 - 用
find / -size +100M找大文件 - 先看日志,再看Docker,再看临时文件
- 删文件前确认不用了
- 删了没释放就重启进程
最常用:
df -h
du -sh /*
find / -type f -size +100M 2>/dev/null
遇到问题加QQ23979811 协助处理
Nginx配置Gzip压缩,提升网站加载速度
前言
网站打开慢?带宽不够?开启Nginx的Gzip压缩,能把文件体积压缩70%以上,加载速度立马提升好几倍。本文讲怎么配置。
一、什么是Gzip
Gzip是一种压缩算法,Nginx在发给浏览器之前先把文件压缩,浏览器收到后自动解压。
效果:
- HTML/CSS/JS文件压缩70%左右
- 减少带宽
- 加载更快
二、配置Gzip
打开Nginx配置文件:
vi /etc/nginx/nginx.conf
在http段里加:
gzip on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_vary on;
gzip_buffers 16 8k;
gzip_http_version 1.1;
参数解释
| 参数 | 含义 |
|---|---|
| gzip on | 开启gzip |
| gzip_min_length 1k | 小于1k的文件不压缩 |
| gzip_comp_level 6 | 压缩级别1-9,6最合适 |
| gzip_types | 压缩哪些类型的文件 |
| gzip_vary on | 给响应头加Vary: Accept-Encoding |
| gzip_buffers | 压缩缓冲区 |
压缩级别说明
| 级别 | 效果 |
|---|---|
| 1 | 最快,压缩率最低 |
| 6 | 推荐,平衡 |
| 9 | 最慢,压缩率最高 |
推荐用6,不要用9,太吃CPU。
三、验证配置
nginx -t
重载Nginx:
nginx -s reload
四、验证是否生效
方法1:curl测试
curl -I -H "Accept-Encoding: gzip" https://www.0454.wang/
看到响应头有:
Content-Encoding: gzip
就是生效了。
方法2:浏览器F12
按F12打开开发者工具 → Network → 点请求 → 看Response Headers:
Content-Encoding: gzip
五、哪些文件该压缩
该压缩的
- HTML文件
- CSS文件
- JS文件
- JSON数据
- SVG图片
不该压缩的
- 图片(jpg/png):本身已经压缩过了
- 视频:太大,压缩没意义
- 压缩包(zip/rar):已经压缩过了
六、常见坑
坑1:只在http段配了,但server段覆盖了
检查: 看server段有没有gzip off。
坑2:gzip_types没写全
问题: JS文件没压缩。
解决: 加上application/javascript。
坑3:压缩级别太高
问题: 用了9,CPU飙升。
解决: 改成6。
坑4:CDN缓存了旧文件
问题: 配置了gzip但CDN没生效。
解决: 刷新CDN缓存。
七、完整配置示例
http {
gzip on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_vary on;
gzip_buffers 16 8k;
gzip_http_version 1.1;
server {
listen 80;
server_name www.0454.wang;
root /var/www/html;
index index.html;
}
}
八、对比效果
没开Gzip
HTML文件:100KB
开了Gzip
HTML文件:25KB
省了75%!
总结
记住:
- 开启gzip on
- 压缩级别用6
- gzip_types要写全
- 图片不要压缩
- 用curl验证是否生效
最常用配置:
gzip on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_vary on;
遇到问题加QQ23979811 协助处理
MySQL慢查询排查,explain分析SQL执行计划
前言
网站突然变慢,数据库CPU100%?多半是慢查询搞的鬼。本文讲怎么开启慢查询日志、用explain分析SQL执行计划,找到慢的原因。
一、开启慢查询日志
临时开启
-- 查看慢查询是否开启
SHOW VARIABLES LIKE 'slow_query_log';
-- 开启慢查询
SET GLOBAL slow_query_log = ON;
-- 超过1秒就算慢查询
SET GLOBAL long_query_time = 1;
永久开启
编辑my.cnf:
vi /etc/my.cnf
加:
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
重启MySQL:
systemctl restart mysqld
看慢查询日志
tail -f /var/log/mysql/slow.log
二、explain是什么
explain用来分析SQL是怎么执行的,看有没有用索引、扫了多少行。
用法
EXPLAIN SELECT * FROM users WHERE name = '张三';
三、explain字段解释
id
SELECT的标识符,越大越先执行。
select_type
查询类型:
- SIMPLE:简单查询
- PRIMARY:主查询
- SUBQUERY:子查询
table
查的哪张表。
type(最重要)
访问类型,从好到差:
| 类型 | 含义 |
|---|---|
| system | 表只有一行 |
| const | 主键或唯一索引匹配,只查一行 |
| eq_ref | 主键或唯一索引匹配 |
| ref | 普通索引匹配 |
| range | 索引范围查询(between、>、<) |
| index | 扫整个索引树 |
| ALL | 全表扫描(最坏!) |
重点看: 出现ALL就是全表扫描,要加索引。
possible_keys
可能用到的索引。
key
实际用到的索引。
key是NULL就是没用到索引。
rows
预估要扫描多少行。
越大越慢。
Extra
额外信息:
| 值 | 含义 |
|---|---|
| Using where | 服务器在存储引擎返回后再过滤 |
| Using index | 用了覆盖索引,不错 |
| Using temporary | 用了临时表,不好 |
| Using filesort | 用了文件排序,不好 |
四、常见慢查询原因
原因1:没加索引
SELECT * FROM users WHERE name = '张三';
name字段没索引 → 全表扫描 → 慢。
解决: 加索引
ALTER TABLE users ADD INDEX idx_name(name);
原因2:索引失效
虽然加了索引,但SQL写法不对,索引用不上。
索引失效的情况:
-
对索引字段用函数:
SELECT * FROM users WHERE LEFT(name, 3) = '张三'; -- 索引失效 -
索引字段用运算:
SELECT * FROM users WHERE age + 1 = 20; -- 索引失效 -
最左前缀原则不满足:
-- 联合索引(a, b, c) SELECT * FROM t WHERE b = 1; -- 索引失效 -
like以%开头:
SELECT * FROM users WHERE name LIKE '%张三%'; -- 索引失效 -
类型不匹配:
-- phone是varchar SELECT * FROM users WHERE phone = 13800138000; -- 索引失效
原因3:SELECT *
查了不需要的字段,占内存。
解决: 只查需要的字段
SELECT id, name FROM users WHERE ...
原因4:大表分页越查越慢
SELECT * FROM users ORDER BY id LIMIT 100000, 10;
慢的原因: 要先扫10万行再跳过。
解决:
SELECT u.* FROM users u
INNER JOIN (SELECT id FROM users ORDER BY id LIMIT 100000, 10) t
ON u.id = t.id;
五、实战案例
慢SQL
SELECT * FROM orders WHERE user_id = 123 AND status = 1 ORDER BY create_time DESC LIMIT 10;
explain分析
EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 1 ORDER BY create_time DESC LIMIT 10;
发现: type是ALL,key是NULL → 全表扫描。
加索引
ALTER TABLE orders ADD INDEX idx_user_status_time(user_id, status, create_time);
再explain: type变成ref,key是idx_user_status_time → 用上索引了。
六、优化原则
- WHERE条件加索引
- ORDER BY字段加索引
- 联合索引遵守最左前缀
- **不要SELECT ***
- 避免在索引字段上用函数
- 大分页用子查询优化
七、常见坑
坑1:加了索引但没用
原因: SQL写法导致索引失效。
排查: explain看key字段。
坑2:索引加太多
索引不是越多越好,写的时候要维护索引,会变慢。
建议: 单表索引不超过5个。
坑3:区分度低的字段不加索引
比如gender(男/女),加索引没用。
原则: 区分度高的字段加索引。
总结
记住:
- 开启慢查询日志找慢SQL
- 用explain分析SQL执行计划
- type=ALL就是全表扫描,要加索引
- key=NULL就是没用到索引
- rows越大越慢
- 索引失效要避免
最常用:
EXPLAIN SELECT * FROM users WHERE ...
遇到问题加QQ23979811 协助处理
Linux查看端口被占用,lsof和netstat命令
前言
启动服务的时候报"Address already in use"?想知道某个端口被哪个进程占了?Linux查看端口占用最常用的就是lsof和netstat命令,本文讲清楚。
一、为什么要查端口
- 启动服务报端口被占
- 看某个服务有没有启动
- 排查安全问题,看有没有可疑端口
二、lsof命令
查看某个端口被哪个进程占用
lsof -i:端口号
例子: 查80端口
lsof -i:80
输出:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 1234 root 6u IPv4 12345 0t0 TCP *:http (LISTEN)
看什么:
- COMMAND:进程名(nginx)
- PID:进程ID(1234)
- USER:运行用户(root)
查看所有TCP端口
lsof -i tcp
查看所有UDP端口
lsof -i udp
杀掉占用端口的进程
kill -9 $(lsof -ti:80)
三、netstat命令
查看某个端口
netstat -tlnp | grep 端口号
例子: 查80端口
netstat -tlnp | grep :80
输出:
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1234/nginx
看什么:
- 最后一列:进程ID/进程名(1234/nginx)
参数解释
| 参数 | 含义 |
|---|---|
| -t | TCP端口 |
| -l | 只看监听中的 |
| -n | 显示数字,不解析域名 |
| -p | 显示进程名和PID |
查看所有监听端口
netstat -tlnp
四、ss命令(推荐)
ss是netstat的升级版,更快。
查看某个端口
ss -tlnp | grep 端口号
例子:
ss -tlnp | grep :80
查看所有监听端口
ss -tlnp
五、三种命令对比
| 命令 | 优点 | 缺点 |
|---|---|---|
| lsof | 信息全,能看文件 | 慢 |
| netstat | 经典,大家都用 | 慢 |
| ss | 最快,推荐 | 新命令 |
推荐: 用ss,最快。
六、常见场景
场景1:启动Nginx报端口被占
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
排查:
lsof -i:80
解决: 杀掉占用的进程
kill -9 进程ID
场景2:看MySQL有没有启动
ss -tlnp | grep 3306
有输出就是启动了。
场景3:看SSH端口
ss -tlnp | grep sshd
七、常见坑
坑1:没加sudo看不到进程
普通用户只能看自己的进程,看不到别人的。
解决: 加sudo
sudo lsof -i:80
sudo netstat -tlnp
坑2:netstat命令找不到
CentOS7最小化安装没有netstat。
安装:
yum install -y net-tools
坑3:lsof命令找不到
安装:
yum install -y lsof
八、常用命令汇总
# 查80端口被谁占了
lsof -i:80
netstat -tlnp | grep :80
ss -tlnp | grep :80
# 看所有监听端口
netstat -tlnp
ss -tlnp
# 杀掉占用80端口的进程
kill -9 $(lsof -ti:80)
总结
记住:
- 查端口用
lsof -i:端口号 - 或者用
ss -tlnp | grep 端口号 - 看最后一列就是进程名和PID
- 杀掉用
kill -9 进程ID - 推荐用ss,最快
最常用:
ss -tlnp | grep :80
遇到问题加QQ23979811 协助处理
Redis缓存穿透、击穿、雪崩区别和解决
前言
用Redis做缓存,经常听到三个词:缓存穿透、缓存击穿、缓存雪崩。很多人搞不清区别,本文一次讲明白,附解决方法。
一、三个概念对比
| 问题 | 是什么 | 谁在访问 | 影响 |
|---|---|---|---|
| 缓存穿透 | 查一个不存在的数据,缓存没有,每次都去查数据库 | 恶意攻击/大量无效请求 | 数据库压力大 |
| 缓存击穿 | 一个热点key突然过期,大量请求同时打过去 | 正常用户 | 数据库瞬间压力大 |
| 缓存雪崩 | 大量key同时过期,或者Redis挂了 | 正常用户 | 数据库直接崩 |
二、缓存穿透
是什么
黑客故意查一个数据库里根本不存在的数据,缓存里也没有,每次请求都打到数据库。
例子: 查用户ID=-1,数据库里没有,缓存里也没有,每次都查数据库。
危害
数据库被大量无效请求打挂。
解决方法
方法1:缓存空值
数据库查不到,也往缓存里写一个空值,设个短过期时间。
// 先查缓存
$user = $redis->get("user:$id");
if ($user === false) {
// 查数据库
$user = mysqli_query("SELECT * FROM users WHERE id = $id");
if (!$user) {
// 查不到也缓存,设60秒过期
$redis->setex("user:$id", 60, "null");
} else {
$redis->setex("user:$id", 3600, $user);
}
}
方法2:布隆过滤器
在缓存前面加一层布隆过滤器,快速判断这个ID存在不存在。
不存在的直接返回,不用查数据库。
方法3:接口校验
在接口层就做参数校验,比如用户ID必须大于0,不合法的直接拒绝。
三、缓存击穿
是什么
一个热点key(比如首页热点新闻),突然过期了,这时候刚好有大量用户同时访问,所有请求都打到数据库。
和穿透的区别: 这个key本来是存在的,只是过期了。
危害
数据库瞬间压力暴增。
解决方法
方法1:互斥锁
只让一个线程去查数据库,其他线程等一下。
$lockKey = "lock:user:$id";
// 尝试加锁
if ($redis->setnx($lockKey, 1)) {
// 加锁成功,查数据库
$user = mysqli_query("SELECT * FROM users WHERE id = $id");
$redis->setex("user:$id", 3600, $user);
$redis->del($lockKey);
} else {
// 加锁失败,等一下再读缓存
sleep(100);
$user = $redis->get("user:$id");
}
方法2:热点key不过期
热点数据不设过期时间,后台定时更新。
方法3:提前预热
在热点数据快过期前,主动后台更新缓存。
四、缓存雪崩
是什么
大量key在同一时间过期,或者Redis整个挂了,所有请求都打到数据库。
和击穿的区别:
- 击穿:一个key过期
- 雪崩:大量key同时过期
危害
数据库直接被打挂,整个服务不可用。
解决方法
方法1:过期时间加随机值
不要所有key都设3600秒过期,加个随机数错开。
// 基础3600秒,加0-600秒随机
$expire = 3600 + rand(0, 600);
$redis->setex("user:$id", $expire, $data);
方法2:Redis集群
Redis挂了还有备用的,不要单点。
方法3:服务降级
数据库扛不住的时候,直接返回默认数据或者报错,不要让请求都打过去。
方法4:热点数据永不过期
核心数据不设过期,后台异步更新。
五、总结对比
| 问题 | 原因 | 解决 |
|---|---|---|
| 缓存穿透 | 查不存在的数据 | 缓存空值 + 布隆过滤器 |
| 缓存击穿 | 热点key过期 | 互斥锁 + 热点永不过期 |
| 缓存雪崩 | 大量key同时过期 | 过期时间加随机 + Redis集群 |
六、常见坑
坑1:把穿透和击穿搞混
- 穿透:数据本来就不存在
- 击穿:数据存在,但缓存过期了
坑2:缓存空值不设过期时间
如果不设过期时间,以后这个数据真的有了,缓存还是空的。
解决: 空值也要设过期时间(比如60秒)。
坑3:互斥锁死锁
加了锁没释放,其他线程一直等。
解决: 锁也要设过期时间。
坑4:随机值加太小
所有key还是差不多同时过期。
建议: 随机范围至少几百秒。
总结
记住:
- 穿透:查不存在的数据 → 缓存空值 + 布隆过滤器
- 击穿:热点key过期 → 互斥锁 + 热点永不过期
- 雪崩:大量key同时过期 → 过期时间加随机 + Redis集群
最简单的防雪崩:
$expire = 3600 + rand(0, 600);
$redis->setex($key, $expire, $data);
遇到问题加QQ23979811 协助处理
服务器SSH安全加固,改端口和密钥登录
前言
服务器刚买,SSH一直被暴力破解?默认22端口全是黑客在扫。本文讲怎么SSH安全加固:改端口、禁用密码登录、用密钥登录。
一、为什么要加固SSH
默认SSH配置太危险:
- 端口22是公开的,黑客都扫
- root密码登录容易被暴力破解
- 密码可能被盗
二、第一步:改SSH端口
编辑SSH配置
vi /etc/ssh/sshd_config
找到:
#Port 22
改成:
Port 22222
(随便选个1024-65535之间的端口)
防火墙开放新端口
firewalld:
firewall-cmd --permanent --add-port=22222/tcp
firewall-cmd --reload
iptables:
iptables -I INPUT -p tcp --dport 22222 -j ACCEPT
重启SSH
systemctl restart sshd
注意: 改完端口后,连接SSH要用新端口:
ssh -p 22222 root@服务器IP
三、第二步:生成密钥对
在你自己电脑上生成
ssh-keygen -t rsa -b 4096
一路回车,生成两个文件:
~/.ssh/id_rsa(私钥,自己留着)~/.ssh/id_rsa.pub(公钥,传到服务器)
把公钥传到服务器
ssh-copy-id -p 22222 root@服务器IP
或者手动传:
# 服务器上创建.ssh目录
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# 把公钥内容追加到authorized_keys
vi ~/.ssh/authorized_keys
# 把id_rsa.pub的内容粘进去
chmod 600 ~/.ssh/authorized_keys
测试密钥登录
ssh -p 22222 root@服务器IP
不用输密码就能登录就对了。
四、第三步:禁用密码登录
编辑SSH配置
vi /etc/ssh/sshd_config
找到:
#PasswordAuthentication yes
改成:
PasswordAuthentication no
找到:
#PermitRootLogin yes
改成:
PermitRootLogin prohibit-password
(允许root用密钥登录,但不允许密码登录)
重启SSH
systemctl restart sshd
五、第四步:其他安全设置
限制登录尝试次数
防止暴力破解:
MaxAuthTries 3
登录超时时间
ClientAliveInterval 300
ClientAliveCountMax 2
禁止空密码登录
PermitEmptyPasswords no
只允许特定用户登录
AllowUsers root
六、安装fail2ban
自动封禁暴力破解的IP。
安装
yum install -y epel-release
yum install -y fail2ban
配置
vi /etc/fail2ban/jail.local
加内容:
[sshd]
enabled = true
port = 22222
filter = sshd
logpath = /var/log/secure
maxretry = 3
bantime = 86400
启动
systemctl start fail2ban
systemctl enable fail2ban
七、完整配置文件参考
vi /etc/ssh/sshd_config
Port 22222
PermitRootLogin prohibit-password
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
PermitEmptyPasswords no
改完重启:
systemctl restart sshd
八、常见坑
坑1:改完端口连不上了
原因: 防火墙没开放新端口。
解决:
- 云服务器还要在控制台安全组里开放端口
- 先开新窗口测试,别关旧窗口
坑2:密钥登录失败
原因: 权限不对。
检查:
ls -ld ~/.ssh
ls -l ~/.ssh/authorized_keys
正确权限:
- .ssh目录:700
- authorized_keys:600
坑3:禁用密码后进不去了
原因: 密钥没配好就禁用密码了。
解决: 一定先测试密钥登录成功,再禁用密码。
坑4:云服务器安全组没开
改了端口,防火墙也开了,但还是连不上。
原因: 云服务商的安全组没开。
解决: 去云控制台(阿里云/腾讯云)安全组里开放端口。
总结
记住:
- 改SSH默认端口(22改成别的)
- 生成密钥对,传公钥到服务器
- 测试密钥登录成功
- 禁用密码登录
- 安装fail2ban自动封IP
- 云服务器记得开安全组
安全加固顺序:
改端口 → 配密钥 → 测试登录 → 禁密码 → 装fail2ban
遇到问题加QQ23979811 协助处理
网站被黑了怎么排查,后门查杀方法
前言
网站突然被挂了黑链、被跳转、内容被篡改?别慌,本文讲网站被黑后怎么排查、怎么找后门、怎么清理。
一、怎么判断被黑了
常见现象
- 网站打开变成别的页面(赌博、色情)
- 网站里多了很多不认识的页面
- 百度搜索显示的标题不对
- 网站突然变慢、CPU飙升
- 后台密码被改了
二、第一步:找最近修改的文件
黑客一般会往网站目录里传后门文件。
找最近24小时修改的文件
find /www/wwwroot/你的站/ -mtime -1 -type f
找最近7天修改的文件
find /www/wwwroot/你的站/ -mtime -7 -type f
找最近修改的PHP文件
find /www/wwwroot/你的站/ -name "*.php" -mtime -7
重点看: 你不认识的、突然冒出来的PHP文件。
三、第二步:找可疑代码
找一句话木马
黑客常用的一句话后门:
<?php @eval($_POST['cmd']); ?>
搜索:
grep -r "eval(" /www/wwwroot/你的站/
grep -r "assert(" /www/wwwroot/你的站/
grep -r "base64_decode(" /www/wwwroot/你的站/
grep -r "str_rot13(" /www/wwwroot/你的站/
找加密的后门
很多后门是加密的:
grep -r "gzinflate(" /www/wwwroot/你的站/
grep -r "gzuncompress(" /www/wwwroot/你的站/
grep -r "strrev(" /www/wwwroot/你的站/
找下载文件的后门
grep -r "file_put_contents(" /www/wwwroot/你的站/
grep -r "fwrite(" /www/wwwroot/你的站/
四、第三步:看Web日志
看Nginx访问日志,找可疑的请求。
看最近的访问
tail -100 /var/log/nginx/access.log
找可疑的POST请求
grep "POST" /var/log/nginx/access.log | tail -50
找访问可疑文件的
grep ".php" /var/log/nginx/access.log | grep "cmd"
五、第四步:看进程
看有没有可疑进程在跑。
ps aux
重点看:
- 不认识的进程
- 占CPU很高的进程
- 从/tmp目录跑的进程
杀掉可疑进程:
kill -9 进程ID
六、第五步:看定时任务
黑客会加定时任务,维持后门。
crontab -l
看有没有不认识的定时任务。
还有系统级的:
cat /etc/crontab
ls /etc/cron.d/
七、第六步:看后门文件特征
常见后门文件名
- shell.php
- cmd.php
- 1.php
- x.php
- upload.php
- webshell.php
看文件权限
后门一般是777权限:
find /www/wwwroot/你的站/ -perm 777
八、清理方法
方法1:删除可疑文件
确认是后门的直接删:
rm -f /www/wwwroot/你的站/可疑文件.php
方法2:重新上传程序
最安全的方法: 备份数据库,重新上传干净的程序,再恢复数据库。
方法3:改所有密码
- 后台密码
- 数据库密码
- FTP密码
- SSH密码
九、预防措施
1. 程序及时更新
WordPress、ThinkPHP等程序有漏洞要及时更新。
2. 上传目录禁执行
location ~* /upload/.*\.php$ {
deny all;
}
3. 定期备份
每天自动备份网站文件和数据库。
4. 改后台地址
不要用默认的admin后台地址。
5. 用安全插件
WordPress可以装Wordfence等安全插件。
十、常见坑
坑1:只删文件不改密码
黑客可能已经留了别的后门,删一个没用。
正确: 删文件 + 改所有密码 + 看定时任务。
坑2:备份也被感染了
备份文件里也有后门,恢复后又被黑了。
解决: 备份也要查。
坑3:只查网站目录
黑客可能在系统里留了后门(比如/tmp目录)。
解决: 也要查系统进程、定时任务。
总结
记住:
- 先找最近修改的文件
- 再找可疑代码(eval、base64_decode)
- 看Web日志和进程
- 看定时任务
- 删后门 + 改所有密码
- 最安全是重新上传干净程序
最快排查:
find /www/wwwroot/你的站/ -name "*.php" -mtime -7
grep -r "eval(" /www/wwwroot/你的站/
遇到问题加QQ23979811 协助处理
Docker容器数据备份和迁移方法
前言
Docker容器删了数据就没了?想把容器从一台服务器迁到另一台?本文讲Docker容器数据备份和迁移的几种方法,包括容器快照、数据卷备份、镜像导出。
一、Docker数据存哪里
Docker数据有两种存法:
- 容器内部:数据存在容器的可写层,容器删了数据就没了
- 数据卷(Volume):数据存在宿主机上,容器删了数据还在
重要: 生产环境一定要用数据卷,不然容器一删数据全没。
二、方法1:容器快照(commit)
把容器当前状态打包成一个新镜像。
步骤
第一步:查看容器ID
docker ps
找到要备份的容器ID。
第二步:提交成新镜像
docker commit 容器ID 备份镜像名:标签
例子:
docker commit mynginx nginx-backup:v1
第三步:导出镜像成文件
docker save -o nginx-backup.tar nginx-backup:v1
第四步:传到新服务器
scp nginx-backup.tar root@新服务器IP:/root/
第五步:新服务器导入
docker load -i nginx-backup.tar
第六步:新服务器启动容器
docker run -d --name mynginx -p 80:80 nginx-backup:v1
优缺点
优点: 简单,整个容器都打包了
缺点: 镜像文件大,不适合大数据量
三、方法2:数据卷备份
如果用了数据卷,直接备份数据卷里的文件。
步骤
第一步:看数据卷挂载在哪
docker inspect 容器名 | grep Mounts
输出里找 Source,就是宿主机上的路径。
例子:
"Mounts": [
{
"Source": "/data/mysql",
"Destination": "/var/lib/mysql"
}
]
数据在宿主机的 /data/mysql 目录。
第二步:打包备份
tar -zcvf mysql-backup.tar.gz /data/mysql
第三步:传到新服务器
scp mysql-backup.tar.gz root@新服务器IP:/root/
第四步:新服务器解压
tar -zxvf mysql-backup.tar.gz -C /data/
第五步:新服务器启动容器,挂载数据卷
docker run -d --name mysql -v /data/mysql:/var/lib/mysql -p 3306:3306 mysql:5.7
优缺点
优点: 数据和容器分离,灵活
缺点: 要自己管理数据目录
四、方法3:docker cp拷贝文件
把容器里的文件拷出来。
拷出来
docker cp 容器名:/容器内路径 宿主机路径
例子:
docker cp mynginx:/usr/share/nginx/html /data/backup/html
拷进去
docker cp 宿主机路径 容器名:/容器内路径
例子:
docker cp /data/backup/html/index.html mynginx:/usr/share/nginx/html/
五、MySQL容器备份
备份MySQL数据
docker exec 容器名 mysqldump -uroot -p123456 数据库名 > backup.sql
例子:
docker exec mysql mysqldump -uroot -p123456 test > test.sql
恢复MySQL数据
docker exec -i 容器名 mysql -uroot -p123456 数据库名 < backup.sql
例子:
docker exec -i mysql mysql -uroot -p123456 test < test.sql
六、常见坑
坑1:容器删了数据没了
没用数据卷,容器删了数据全没。
解决: 一定要用 -v 挂载数据卷。
坑2:commit的镜像太大
把整个容器都打包了,包括临时文件。
解决: 尽量用数据卷备份,不要用commit。
坑3:数据权限不对
备份恢复后,容器里读不了数据。
原因: 文件所有者不对。
解决: 改权限:
chown -R 999:999 /data/mysql
(MySQL容器用的是999用户)
坑4:数据库没停就备份
MySQL还在写数据,备份出来不一致。
解决: 用mysqldump逻辑备份,不要直接拷文件。
总结
记住:
- 生产环境一定要用数据卷(-v)
- 小容器用commit快照备份
- 大数据量直接备份数据卷目录
- 数据库用mysqldump逻辑备份
- 备份完要测试能不能恢复
最常用:
# 备份数据卷
tar -zcvf backup.tar.gz /data/容器数据/
# 恢复数据卷
tar -zxvf backup.tar.gz -C /data/
遇到问题加QQ23979811 协助处理
MySQL数据库备份和恢复,mysqldump完整教程
前言
数据库备份是每个站长的基本功。MySQL最常用的备份工具就是mysqldump,简单好用。本文讲怎么用mysqldump备份和恢复数据库,包括完整命令和常见坑。
一、mysqldump是什么
mysqldump是MySQL自带的备份工具,把数据库导出成SQL文件,需要的时候再导入回去。
特点:
- 免费,MySQL自带
- 导出的是SQL文本,方便查看和编辑
- 适合小数据库(几十G以内)
二、备份数据库
备份单个数据库
mysqldump -u用户名 -p密码 数据库名 > 备份文件.sql
例子:
mysqldump -uroot -p123456 test > test.sql
注意: -p和密码之间没有空格。
备份多个数据库
mysqldump -uroot -p123456 --databases db1 db2 db3 > dbs.sql
备份所有数据库
mysqldump -uroot -p123456 --all-databases > all.sql
备份单个表
mysqldump -uroot -p123456 test users > users.sql
备份多个表
mysqldump -uroot -p123456 test users orders > tables.sql
三、常用参数
带删除表语句(--add-drop-table)
导出的SQL里会有 DROP TABLE IF EXISTS,恢复的时候先删表再建表。
mysqldump -uroot -p123456 --add-drop-table test > test.sql
默认就是开的。
不带建表语句(--no-create-info)
只导数据,不导表结构。
mysqldump -uroot -p123456 --no-create-info test > data.sql
只导表结构(--no-data)
只导表结构,不导数据。
mysqldump -uroot -p123456 --no-data test > structure.sql
压缩备份
mysqldump -uroot -p123456 test | gzip > test.sql.gz
解压:
gunzip test.sql.gz
指定端口
mysqldump -uroot -p123456 -P 3307 test > test.sql
指定主机
mysqldump -uroot -p123456 -h 1.2.3.4 test > test.sql
四、恢复数据库
方法1:用mysql命令
mysql -u用户名 -p密码 数据库名 < 备份文件.sql
例子:
mysql -uroot -p123456 test < test.sql
方法2:登录MySQL后source
mysql -uroot -p123456
mysql> use test;
mysql> source /root/test.sql;
恢复压缩备份
gunzip < test.sql.gz | mysql -uroot -p123456 test
五、定时备份脚本
写个备份脚本
#!/bin/bash
# 数据库配置
DB_USER="root"
DB_PASS="123456"
DB_NAME="test"
# 备份目录
BACKUP_DIR="/data/backup"
# 文件名(带日期)
DATE=$(date +%Y%m%d_%H%M%S)
FILE_NAME="${DB_NAME}_${DATE}.sql.gz"
# 备份
mysqldump -u${DB_USER} -p${DB_PASS} ${DB_NAME} | gzip > ${BACKUP_DIR}/${FILE_NAME}
# 删除7天前的备份
find ${BACKUP_DIR} -name "*.sql.gz" -mtime +7 -delete
echo "备份完成:${FILE_NAME}"
加执行权限
chmod +x /root/backup.sh
加到crontab
每天凌晨3点备份:
crontab -e
加一行:
0 3 * * * /root/backup.sh
六、常见坑
坑1:密码写在命令行里不安全
-p123456 会在进程列表里看到密码。
更安全的方法:
mysqldump -uroot -p test > test.sql
然后手动输入密码。
或者用配置文件:
# ~/.my.cnf
[mysqldump]
user=root
password=123456
然后直接:
mysqldump test > test.sql
坑2:大表备份很慢
数据多了以后,mysqldump会锁表,影响业务。
解决: 用 --single-transaction 参数:
mysqldump -uroot -p123456 --single-transaction test > test.sql
这个参数适合InnoDB,不锁表。
坑3:字符集不对
导出的SQL乱码。
加参数:
mysqldump -uroot -p123456 --default-character-set=utf8mb4 test > test.sql
坑4:自增ID不连续
恢复后自增ID变了。
正常现象,不用管。
坑5:备份文件太大
用gzip压缩:
mysqldump -uroot -p123456 test | gzip > test.sql.gz
七、备份验证
备份完了要验证一下能不能恢复。
看文件大小
ls -lh test.sql
不能是0字节。
看文件内容
head -20 test.sql
看开头是不是正常的SQL。
测试恢复
找个测试库恢复一下:
mysql -uroot -p123456 test_restore < test.sql
总结
记住:
- 备份:
mysqldump -uroot -p123456 数据库 > 备份.sql - 恢复:
mysql -uroot -p123456 数据库 < 备份.sql - 大表加
--single-transaction不锁表 - 用gzip压缩备份文件
- 定时备份要自动删旧备份
- 备份完要验证
最常用:
# 备份
mysqldump -uroot -p123456 --single-transaction test | gzip > test.sql.gz
# 恢复
gunzip < test.sql.gz | mysql -uroot -p123456 test
遇到问题加QQ23979811 协助处理
Nginx 502 Bad Gateway常见原因和解决办法
前言
网站突然打不开了,浏览器显示"502 Bad Gateway"?这是Nginx最常见的报错之一。本文讲502是什么意思、常见原因、怎么排查和解决。
一、502是什么意思
502 Bad Gateway = Nginx作为代理,从后端服务器(比如PHP-FPM)拿不到正常的响应。
简单说:Nginx收到了你的请求,然后去问后端"给我数据",后端没理它或者报错了,Nginx就返回502。
二、常见原因
原因1:PHP-FPM没启动
后端PHP服务没跑,Nginx找不到人。
检查:
ps aux | grep php-fpm
没输出就是没启动。
启动:
systemctl start php-fpm
或者:
service php-fpm start
原因2:PHP-FPM端口不对
Nginx配置里写的PHP-FPM地址不对。
比如:
fastcgi_pass 127.0.0.1:9000;
但PHP-FPM实际监听的是 /tmp/php-cgi.sock。
检查PHP-FPM监听:
netstat -tlnp | grep php-fpm
或者:
ss -tlnp | grep php-fpm
原因3:PHP代码报错
PHP代码有致命错误,直接挂了。
看PHP错误日志:
tail -f /var/log/php-fpm/www-error.log
或者:
tail -f /var/log/php/error.log
原因4:PHP超时了
程序跑太慢,超过Nginx或PHP-FPM的超时时间。
改Nginx超时:
fastcgi_read_timeout 300;
改PHP-FPM超时:
request_terminate_timeout = 300
原因5:PHP-FPM进程数不够
请求太多,PHP-FPM忙不过来。
看进程数:
ps aux | grep php-fpm | wc -l
改配置:
pm.max_children = 50
pm.start_servers = 20
原因6:内存不够了
PHP进程占满内存,系统把PHP杀了。
看内存:
free -h
看dmesg:
dmesg | grep oom
三、排查步骤
第一步:看Nginx错误日志
tail -f /var/log/nginx/error.log
常见报错:
| 报错 | 含义 |
|---|---|
| connect() failed (111: Connection refused) | PHP-FPM没启动 |
| Primary script unknown | 文件路径不对 |
| upstream timed out | PHP超时了 |
| recv() failed (104: Connection reset by peer) | PHP-FPM挂了 |
第二步:看PHP-FPM状态
systemctl status php-fpm
看是不是running。
第三步:看PHP错误日志
tail -f /var/log/php-fpm/www-error.log
看PHP代码有没有报错。
第四步:看系统资源
top
free -h
df -h
看CPU、内存、磁盘够不够。
四、常见解决方法
方法1:重启PHP-FPM
systemctl restart php-fpm
方法2:改Nginx配置
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# 超时时间
fastcgi_read_timeout 300;
fastcgi_send_timeout 300;
}
方法3:改PHP-FPM进程数
pm = dynamic
pm.max_children = 50
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 30
改完重启:
systemctl restart php-fpm
方法4:开PHP错误显示
display_errors = On
error_reporting = E_ALL
五、常见坑
坑1:只重启Nginx,不重启PHP-FPM
502是后端的问题,重启Nginx没用。
正确: 重启PHP-FPM。
坑2:文件路径不对
fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
错! 应该是:
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
坑3:PHP-FPM版本不对
装了PHP7但Nginx还找PHP5的sock。
检查:
ls -la /var/run/php/
坑4:权限问题
PHP-FPM没权限读网站目录的文件。
解决:
chown -R www:www /www/wwwroot/
总结
记住:
- 502 = Nginx拿不到后端响应
- 先看Nginx错误日志
- 再看PHP-F启没启动
- 然后看PHP代码有没有报错
- 最后看系统资源够不够
最快排查:
tail -f /var/log/nginx/error.log
systemctl status php-fpm
遇到问题加QQ23979811 协助处理
Linux查看系统负载,uptime和w命令详解
前言
服务器卡不卡,先看负载。Linux看系统负载最常用的就是uptime和w命令,一行输出就能看到系统跑了多久、现在有多少人登录、负载高不高。本文讲怎么看这些信息。
一、uptime命令
直接敲
uptime
输出:
07:39:01 up 35 days, 2:15, 2 users, load average: 0.08, 0.03, 0.00
逐个解释
| 字段 | 含义 |
|---|---|
| 07:39:01 | 当前时间 |
| up 35 days, 2:15 | 系统运行了35天2小时15分 |
| 2 users | 当前有2个用户登录 |
| load average: 0.08, 0.03, 0.00 | 系统负载,分别是1分钟、5分钟、15分钟 |
负载怎么看
load average三个数:
- 第一个:最近1分钟的平均负载
- 第二个:最近5分钟的平均负载
- 第三个:最近15分钟的平均负载
判断标准:
| 负载值 | 状态 |
|---|---|
| 0.00-0.70 | 正常 |
| 0.70-1.00 | 注意一下 |
| 1.00-2.00 | 有点高 |
| 2.00以上 | 很高了,要排查 |
注意: 负载和CPU核数有关。比如4核CPU,负载4.00才是满。
看CPU核数
nproc
# 4
或者:
cat /proc/cpuinfo | grep processor | wc -l
# 4
判断: 负载小于CPU核数就是正常的。
二、w命令
直接敲
w
输出:
07:39:01 up 35 days, 2:15, 2 users, load average: 0.08, 0.03, 0.00
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
root pts/0 1.2.3.4 07:35 5.00s 0.03s 0.00s w
www pts/1 5.6.7.8 07:30 8:12 0.01s 0.01s -bash
逐个解释
第一行和uptime一样。
下面是登录用户列表:
| 字段 | 含义 |
|---|---|
| USER | 用户名 |
| TTY | 终端类型(pts/0表示远程连接) |
| FROM | 从哪个IP登录的 |
| LOGIN@ | 登录时间 |
| IDLE | 空闲时间 |
| JCPU | 整个终端所有进程用的CPU时间 |
| PCPU | 当前进程用的CPU时间 |
| WHAT | 当前在跑什么命令 |
三、其他查看命令
who命令
看谁登录了:
who
输出:
root pts/0 2026-10-09 07:35 (1.2.3.4)
www pts/1 2026-10-09 07:30 (5.6.7.8)
last命令
看登录历史:
last
输出最近登录的记录。
lastb命令
看失败的登录:
lastb
四、负载高了怎么排查
第一步:看CPU
top
按P按CPU排序,看哪个进程占CPU多。
第二步:看内存
free -h
看内存够不够,有没有用swap。
第三步:看磁盘
df -h
看磁盘满了没。
第四步:看IO
iostat
看磁盘IO高不高。
五、常见坑
坑1:负载高不代表CPU高
负载包括:
- 正在用CPU的进程
- 等着CPU的进程
- 等着磁盘IO的进程
可能是磁盘IO高,CPU不高,但负载也高。
坑2:负载和核数对比
1核CPU,负载1.0就是满了。
4核CPU,负载4.0才是满了。
别光看数字,要看CPU核数。
坑3:三个负载数的区别
- 1分钟高,5分钟、15分钟不高:可能是突然有个进程跑了一下
- 15分钟都高:说明系统一直很忙
总结
记住:
uptime看系统运行时间和负载w看谁登录了、在干嘛- 负载三个数:1分钟、5分钟、15分钟
- 负载要和CPU核数对比
- 负载高了用top、free、df、iostat排查
最常用:
uptime # 看负载
w # 看谁在登录
top # 看哪个进程占CPU
遇到问题加QQ23979811 协助处理
PHP实现简单分页,SQL limit用法
前言
做网站列表页,数据太多一页显示不完,需要分页。PHP+MySQL做分页很简单,就是用SQL的limit。本文写个完整的分页示例,直接拿去用。
一、分页原理
分页就两件事:
- 查总条数:看看一共有多少条数据
- 查当前页数据:用limit取当前页要显示的那几条
公式:
偏移量 = (当前页 - 1) × 每页条数
比如每页10条,第3页:
偏移量 = (3 - 1) × 10 = 20
SQL: SELECT * FROM users LIMIT 20, 10
二、SQL limit用法
语法
SELECT * FROM 表名 LIMIT 偏移量, 每页条数
例子
-- 取前10条
SELECT * FROM users LIMIT 0, 10;
-- 取第11-20条
SELECT * FROM users LIMIT 10, 10;
-- 取第21-30条
SELECT * FROM users LIMIT 20, 10;
三、完整分页代码
第一步:接收当前页码
<?php
// 当前页,默认第1页
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
// 每页显示多少条
$pageSize = 10;
// 计算偏移量
$offset = ($page - 1) * $pageSize;
第二步:查总条数
// 连接数据库
$conn = mysqli_connect('127.0.0.1', 'root', '123456', 'test', 3306);
mysqli_set_charset($conn, 'utf8mb4');
// 查总条数
$sql = "SELECT COUNT(*) as total FROM users";
$result = mysqli_query($conn, $sql);
$row = mysqli_fetch_assoc($result);
$total = $row['total'];
// 算总页数
$totalPage = ceil($total / $pageSize);
第三步:查当前页数据
// 查当前页的数据
$sql = "SELECT id, name, age FROM users ORDER BY id DESC LIMIT $offset, $pageSize";
$result = mysqli_query($conn, $sql);
// 取数据
$list = [];
while ($row = mysqli_fetch_assoc($result)) {
$list[] = $row;
}
第四步:显示数据和分页导航
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>用户列表</title>
</head>
<body>
<h1>用户列表</h1>
<table border="1">
<tr>
<th>ID</th>
<th>姓名</th>
<th>年龄</th>
</tr>
<?php foreach ($list as $item): ?>
<tr>
<td><?php echo $item['id']; ?></td>
<td><?php echo $item['name']; ?></td>
<td><?php echo $item['age']; ?></td>
</tr>
<?php endforeach; ?>
</table>
<p>共 <?php echo $total; ?> 条,第 <?php echo $page; ?> / <?php echo $totalPage; ?> 页</p>
<p>
<?php if ($page > 1): ?>
<a href="?page=<?php echo $page - 1; ?>">上一页</a>
<?php endif; ?>
<?php for ($i = 1; $i <= $totalPage; $i++): ?>
<?php if ($i == $page): ?>
<strong><?php echo $i; ?></strong>
<?php else: ?>
<a href="?page=<?php echo $i; ?>"><?php echo $i; ?></a>
<?php endif; ?>
<?php endfor; ?>
<?php if ($page < $totalPage): ?>
<a href="?page=<?php echo $page + 1; ?>">下一页</a>
<?php endif; ?>
</p>
</body>
</html>
四、封装成分页函数
function pagination($page, $totalPage, $url = '')
{
$html = '';
if ($page > 1) {
$html .= '<a href="' . $url . '?page=' . ($page - 1) . '">上一页</a> ';
}
for ($i = 1; $i <= $totalPage; $i++) {
if ($i == $page) {
$html .= '<strong style="margin:0 5px;">' . $i . '</strong> ';
} else {
$html .= '<a href="' . $url . '?page=' . $i . '" style="margin:0 5px;">' . $i . '</a> ';
}
}
if ($page < $totalPage) {
$html .= '<a href="' . $url . '?page=' . ($page + 1) . '">下一页</a>';
}
return $html;
}
// 使用
echo pagination($page, $totalPage);
五、常见坑
坑1:页码没做限制
用户输入 ?page=999,结果查出来是空的。
解决: 限制页码范围:
$page = max(1, min($page, $totalPage));
坑2:offset算错了
$offset = $page * $pageSize; // 错!
正确:
$offset = ($page - 1) * $pageSize; // 对!
坑3:总条数查错了
带条件查询的时候,总条数也要带条件。
比如:
// 带条件查数据
$sql = "SELECT * FROM users WHERE status = 1 LIMIT $offset, $pageSize";
// 总条数也要带条件
$sql = "SELECT COUNT(*) FROM users WHERE status = 1";
坑4:大表分页越查越慢
数据多了以后,LIMIT 100000, 10 会很慢。
原因: MySQL要先扫描前面10万条再跳过。
优化:
-- 先查id,再关联
SELECT u.* FROM users u
INNER JOIN (SELECT id FROM users ORDER BY id LIMIT 100000, 10) t
ON u.id = t.id;
六、带搜索条件的分页
// 搜索关键词
$keyword = isset($_GET['keyword']) ? trim($_GET['keyword']) : '';
// 拼WHERE
$where = "WHERE 1=1";
if ($keyword) {
$where .= " AND name LIKE '%$keyword%'";
}
// 总条数
$sql = "SELECT COUNT(*) as total FROM users $where";
// 当前页数据
$sql = "SELECT * FROM users $where ORDER BY id DESC LIMIT $offset, $pageSize";
总结
记住:
- 分页就是
LIMIT 偏移量, 每页条数 - 偏移量 = (当前页 - 1) × 每页条数
- 先查总条数,再算总页数
- 页码要做限制,不能超出范围
- 带条件查询时,总条数也要带条件
核心公式:
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
$pageSize = 10;
$offset = ($page - 1) * $pageSize;
$sql = "SELECT * FROM users LIMIT $offset, $pageSize";
遇到问题加QQ23979811 协助处理
Redis 缓存穿透解决
Redis 缓存穿透解决
缓存穿透是常见问题,恶意请求能把数据库打挂。
1. 什么是缓存穿透
现象: 请求的数据在缓存和数据库中都不存在,每次请求都直接打到数据库。
危害:
- 恶意攻击时数据库压力大
- 正常请求被拖慢
- 数据库CPU飙升
2. 为什么会穿透
- 查询一个不存在的ID
- 缓存没有命中
- 数据库也查不到
- 请求直接穿透到数据库
3. 解决方案一:缓存空值
思路: 即使数据库查不到,也在Redis存一个空值,设置短过期时间。
function getUser($id) {
$key = "user:$id";
$user = $redis->get($key);
if ($user !== false) {
// 空值标记
if ($user === 'null') {
return null;
}
return json_decode($user, true);
}
// 查数据库
$user = $db->query("SELECT * FROM users WHERE id = $id");
if ($user) {
// 存在就缓存1小时
$redis->setex($key, 3600, json_encode($user));
} else {
// 不存在也缓存,5分钟过期
$redis->setex($key, 300, 'null');
}
return $user;
}
优点: 简单有效
缺点: 占内存,可能被恶意key刷爆
4. 解决方案二:布隆过滤器
思路: 在缓存前面加一层布隆过滤器,不存在的key直接拦截。
原理:
- 所有存在的数据先存到布隆过滤器
- 请求先过布隆过滤器
- 不存在直接返回,不查缓存和数据库
PHP 示例(用 Redis 的布隆过滤器模块):
# 加载 RedisBloom 模块
redis-cli BF.ADD user:1
redis-cli BF.EXISTS user:1
function getUser($id) {
$key = "user:$id";
// 先过布隆过滤器
if (!$redis->rawCommand('BF.EXISTS', 'user_filter', $id)) {
return null; // 一定不存在
}
// 布隆说可能存在,再查缓存
$user = $redis->get($key);
if ($user) {
return json_decode($user, true);
}
// 查数据库...
}
优点: 内存占用小,效率高
缺点: 有误判率,实现复杂
5. 解决方案三:参数校验
思路: 从源头过滤非法请求。
function getUser($id) {
// 参数校验
if ($id <= 0 || !is_numeric($id)) {
return null;
}
// 正常查询逻辑...
}
适用: ID类查询,不合法直接拦截
6. 三种方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 缓存空值 | 简单 | 占内存 | 数据量不大 |
| 布隆过滤器 | 高效 | 误判,复杂 | 数据量大 |
| 参数校验 | 最直接 | 只防非法 | ID类请求 |
7. 生产建议
1)组合使用
- 先做参数校验
- 再缓存空值
- 大流量用布隆过滤器
2)空值过期时间要短
- 5分钟到10分钟
- 不要太长
3)监控异常请求
- 发现大量404要警惕
- 及时加防护
4)接口限流
- 用 Nginx 或网关限流
- 防止恶意刷
8. 缓存雪崩和击穿
缓存穿透: 查不存在的数据
缓存击穿: 热点key过期,大量请求打数据库
缓存雪崩: 大量key同时过期
区分清楚: 三个问题现象类似但原因不同,解决方案也不同。
总结
Redis 缓存穿透解决要点:
- 查不存在的数据穿透到数据库
- 缓存空值是最简单方案
- 布隆过滤器适合大数据量
- 参数校验从源头拦截
- 空值过期时间要短
- 生产建议组合使用
- 注意和雪崩、击穿区分开
- 监控异常请求
- 接口加限流
缓存穿透是面试高频题,也是生产常见问题,一定要掌握。
PHP报错Permission denied权限问题解决
前言
PHP写文件、读文件、上传文件,突然报"Permission denied"?这是Linux文件权限问题,最常见就是文件或目录的所有者不对、权限不够。本文讲怎么排查和解决。
一、报错长啥样
Warning: fopen(/data/test.txt): failed to open stream: Permission denied in /www/test.php on line 5
意思:PHP没权限操作这个文件/目录。
二、常见原因
原因1:文件/目录权限不够
PHP没有读或写的权限。
原因2:文件/目录所有者不对
PHP用www用户跑,但文件是root创建的,www没权限。
原因3:SELinux开着
CentOS默认开SELinux,即使权限对了也不让访问。
原因4:目录权限不够
要写文件的话,目录本身也要有写权限。
三、排查步骤
第一步:看报错的文件路径
报错信息里会告诉你是哪个文件:
Permission denied: /data/upload/test.jpg
就是 /data/upload/ 这个目录的问题。
第二步:看文件权限
ls -l /data/upload/
输出类似:
-rw-r--r-- 1 root root 1024 Oct 9 10:00 test.txt
看第一个字段:
-rw-r--r--表示所有者能读写,其他人只能读
看所有者:
root root表示所有者和组都是root
第三步:看PHP用什么用户跑
ps aux | grep php-fpm
输出里会看到:
www 1234 0.0 0.5 123456 7890 ? S 10:00 0:00 php-fpm: pool www
PHP是用 www 用户跑的。
如果文件所有者是root,那www用户就没权限写。
四、解决方法
方法1:改文件所有者
把文件/目录的所有者改成PHP的用户:
# 改单个文件
chown www:www /data/upload/test.txt
# 改整个目录(包括里面的文件)
chown -R www:www /data/upload/
推荐这个方法,最安全。
方法2:改权限
给所有人加权限:
# 文件加写权限
chmod 666 /data/upload/test.txt
# 目录加写权限
chmod 777 /data/upload/
注意: 777太不安全了,尽量用方法1改所有者。
方法3:看SELinux
CentOS开了SELinux的话,即使权限对了也不行。
检查SELinux:
getenforce
输出 Enforcing 就是开着。
临时关掉:
setenforce 0
永久关掉:
vi /etc/selinux/config
把:
SELINUX=enforcing
改成:
SELINUX=disabled
重启服务器生效。
或者只给目录加SELinux权限:
chcon -R -t httpd_sys_rw_content_t /data/upload/
五、常见场景
场景1:上传图片报错
用户上传图片,报Permission denied。
原因: uploads目录所有者不对。
解决:
chown -R www:www /www/wwwroot/你的站/uploads/
场景2:写日志报错
PHP写日志文件,报Permission denied。
原因: 日志目录没权限。
解决:
chown -R www:www /var/log/你的应用/
场景3:session不存
PHP session存不进去。
原因: session目录没权限。
解决:
chown -R www:www /var/lib/php/session/
场景4:缓存目录没权限
ThinkPHP/Laravel缓存写不进去。
原因: runtime目录没权限。
解决:
chown -R www:www runtime/
六、常用命令
看文件权限
ls -l 文件名
看目录权限
ls -ld 目录名
改所有者
chown 用户:组 文件
chown -R 用户:组 目录
改权限
chmod 755 目录
chmod 644 文件
七、权限数字含义
| 数字 | 含义 |
|---|---|
| 7 | 读+写+执行 (4+2+1) |
| 6 | 读+写 (4+2) |
| 5 | 读+执行 (4+1) |
| 4 | 只读 |
常用:
- 目录:755
- 文件:644
- 上传目录:775(或改所有者)
八、常见坑
坑1:只改了文件,没改目录
要写文件的话,目录本身也要有写权限。
解决: 目录也要改所有者。
坑2:递归改权限
chmod -R 777 目录
太危险了,所有人都能写。
解决: 优先改所有者,不要随便777。
坑3:SELinux偷偷开着
CentOS7以后默认开SELinux,权限对了也不行。
检查:
getenforce
坑4:父目录没权限
比如 /data/upload/ 有权限了,但 /data/ 没权限,还是进不去。
解决: 从根开始一层一层检查。
总结
记住:
- 报错先看是哪个文件/目录
- 看文件所有者是不是PHP的用户(一般是www)
- 不是就
chown -R www:www 目录 - 还不行就检查SELinux
- 尽量改所有者,不要随便777
最常用:
chown -R www:www /你的目录/
遇到问题加QQ23979811 协助处理
Nginx 配置HTTPS证书
Nginx 配置HTTPS证书
现在网站都要HTTPS,配置证书是必备技能。
1. 为什么要HTTPS
- 加密传输,防窃听
- 浏览器安全标识
- SEO 排名加分
- 现在主流浏览器都要求HTTPS
2. 证书文件
一般有两个文件:
.crt:证书文件.key:私钥文件
# 证书文件放在这里
/etc/nginx/ssl/
├── example.com.crt
└── example.com.key
3. 基本HTTPS配置
server {
listen 443 ssl;
server_name example.com;
# 证书配置
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
# 其他配置
location / {
root /var/www/html;
index index.html;
}
}
4. HTTP 跳转 HTTPS
# HTTP 重定向到 HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
5. 优化配置
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
# 优化SSL配置
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# 加密套件
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
}
6. 检查配置
# 检查配置语法
nginx -t
# 重载配置
nginx -s reload
7. 证书申请
免费证书:
- Let's Encrypt(用 certbot 申请)
- 阿里云免费证书
- 腾讯云免费证书
certbot 申请:
# 安装 certbot
yum install certbot python3-certbot-nginx
# 自动申请并配置
certbot --nginx -d example.com
自动续期:
# 测试续期
certbot renew --dry-run
# 加入定时任务
0 3 * * * certbot renew --quiet
8. 常见问题
1)证书不生效
- 检查证书路径对不对
- 检查文件权限
- 重载 Nginx
2)浏览器提示不安全
- 证书过期了
- 域名不匹配
- 中间证书没装全
3)HTTPS 访问慢
- 开启 HTTP/2
- 优化 SSL 会话缓存
4)证书续期失败
- 检查域名解析
- 检查80端口是否通
- 手动运行 certbot 看报错
9. 证书有效期
- 免费证书一般 3 个月
- 要设置自动续期
- 不然到期网站就挂了
10. 生产建议
1)强制 HTTPS
- HTTP 全部 301 跳转
- 开启 HSTS
2)开启 HTTP/2
- listen 443 ssl http2
- 提升访问速度
3)定期检查证书
- 监控证书到期时间
- 提前续期
4)中间证书
- 有些证书要把中间证书合并
- 不然浏览器不信任
总结
Nginx 配置HTTPS要点:
- 配置证书和私钥路径
- HTTP 跳转 HTTPS
- 开启 HTTP/2
- 优化 SSL 会话缓存
- 用 certbot 免费申请
- 设置自动续期
- 定期检查证书有效期
- 开启 HSTS
- 注意中间证书问题
HTTPS 是网站标配,一定要配置好。