新买的服务器装好系统、把网站部署上去,很多个人站长会遇到两个看起来很怪的问题:一是服务器时间比北京时间慢了八个小时,定时任务明明设置凌晨三点执行,日志里却显示的是下午三点;二是网页上到处是"锟斤拷"或者"???"这样的乱码。这两个问题看似不相关,根源其实都在"时区和字符集"这两个服务器基础配置上。这篇文章把时区设置、locale 生成、中文乱码排查完整梳理一遍,一次解决。
一、先把系统时区搞清楚
Linux 服务器默认时区往往是 UTC(协调世界时),而国内网站需要的是北京时间,即东八区。UTC 和北京时间正好相差八个小时。查看当前时区用 date 命令,输出里的 +0000 或 UTC 字样就说明当前是 UTC;用 timedatectl 命令可以看到更详细的信息,包括时区、是否启用 NTP 时间同步。设置时区最简单的方式是执行 timedatectl set-timezone Asia/Shanghai,执行完再用 date 验证,会看到 CST 和 +0800 字样。老一点的系统没有 timedatectl,就手动操作:把 /usr/share/zoneinfo/Asia/Shanghai 软链接到 /etc/localtime,同时把 Asia/Shanghai 写进 /etc/timezone 文件。做完这两步,系统级时区就对了。
二、应用层时区:PHP、MySQL 与 crontab
系统时区改完,网站可能还是差八个小时,因为 PHP 和 MySQL 有自己独立的时区配置。PHP 的时区由 php.ini 里的 date.timezone 决定,默认没配置时 PHP 会报 Warning 并回退到系统时区。在 php.ini 里设置 date.timezone = Asia/Shanghai,然后重启 PHP-FPM 生效。不想改配置文件的话,也可以在程序入口调用 date_default_timezone_set('Asia/Shanghai') 设置。MySQL 查看时区用 SELECT @@global.time_zone, @@session.time_zone;,返回 SYSTEM 就表示跟随系统时区。PHP 和数据库在同一台机器时用系统时区问题不大;如果分开部署,建议在连接后执行 SET time_zone = '+08:00' 显式指定。crontab 默认走系统时区,系统时区改对就不用额外处理;个别系统支持在 crontab 文件头部用 CRON_TZ=Asia/Shanghai 单独指定时区。日志文件的时间也跟随系统时区,Nginx 的 access log、PHP 错误日志,系统时区对了它们就全对了。
三、locale 与字符集基础
locale 是"语言加地区加编码"的组合,比如 zh_CN.UTF-8 表示简体中文、中国地区、UTF-8 编码。系统默认只装了 C/POSIX 和 en_US 这类 locale,中文 locale 需要额外生成。查看系统已经生成的 locale 用 locale -a。生成中文 locale 的方法:编辑 /etc/locale.gen 文件,取消 zh_CN.UTF-8 这一行的注释,然后执行 locale-gen;或者直接执行 locale-gen zh_CN.UTF-8。生成之后可以通过 export LANG=zh_CN.UTF-8 临时生效,要永久生效就写进 /etc/default/locale(Debian 系)或 /etc/locale.conf(CentOS 系)。需要注意,locale 影响的是终端界面和命令行程序的输出,它和网页编码是两回事——网页乱码多半不是 locale 的问题,但 locale 不配好会导致 SSH 里看中文文件名乱码、命令行程序输出中文乱码,所以也应该一并配好。
四、网页中文乱码的三大来源
网页出现乱码,按出现的位置可以分成三种情况逐一排查。
第一种是 HTTP 响应层:响应头里的 Content-Type 没有声明 charset,或者声明了 charset=ISO-8859-1、GBK,而页面实际是 UTF-8,浏览器按错误编码解码,中文就全乱了。最简单的修复是确保网页 HTML 的 head 里有 meta charset="utf-8",并且 HTTP 头与 meta 一致;Nginx 站点配置里也可以给 HTML 设置 default_type text/html; charset=utf-8 兜底。Typecho 这类程序通常自己会输出正确的响应头,如果乱码,先看是不是主题文件编码问题或缓存问题。
第二种是文件层:网页文件本身保存的编码和声明不一致。比如文件用 GBK 保存,但 HTML 声明 UTF-8。用 file -i 文件名 命令可以查看文件真实编码;转换编码用 iconv -f GBK -t UTF-8 源文件 -o 新文件。转换前一定要先备份原文件。编辑器也容易在这里挖坑:用记事本打开 UTF-8 文件再保存,可能被存成带 BOM 或 ANSI 编码,导致网页头部出现一个不可见字符或者整页乱码。
第三种是数据库层:建库建表时指定的字符集和程序连接字符集不一致。典型的错误是数据库表是 latin1 或 gbk,程序按 utf8 读出再输出,中文就变成问号。MySQL 建库建议统一用 utf8mb4:CREATE DATABASE 库名 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;,建表同样指定。连接数据库后执行 SET NAMES utf8mb4,确保连接字符集一致。如果库里已经有乱码数据,可以尝试用 mysqldump 导出、转码、再导入的方式修复,过程要小心,先做全库备份。
乱码还有个经典判别特征:出现"锟斤拷"说明 UTF-8 内容被按 GBK 解码又编码回来;出现连续的"???"说明数据在存储环节就已经丢失了字符;出现单个"□"多半是字体缺字。根据特征能快速判断问题出在哪一层。
五、SSH 终端乱码的处理
用 SSH 登录服务器,执行 ls 看到中文文件名乱码,通常是终端软件的编码设置和服务器不一致:服务器输出 UTF-8,Windows 自带的 cmd 和 PowerShell 默认 GBK,两边一碰撞就乱了。解决办法是双管齐下:把终端编码改成 UTF-8(Windows Terminal、Xshell、FinalShell 等软件都能在会话属性里设置),同时保证服务器的 locale 是 zh_CN.UTF-8。两个条件都满足,终端里的中文就正常了。
六、Docker 容器与数据库时间的坑
如果网站是跑在 Docker 里的,还会多出一层时区问题。很多官方镜像(比如 PHP、Nginx 的镜像)基于精简版系统,容器内默认 UTC,即使宿主机已经是 Asia/Shanghai,容器里的时间还是差八个小时。解决办法有三种:最简单的是在 docker run 时加 -e TZ=Asia/Shanghai 环境变量(需要镜像里装了 tzdata);或者启动时把宿主机的 /etc/localtime 挂载进容器:-v /etc/localtime:/etc/localtime:ro;再或者写 Dockerfile 时在构建阶段安装 tzdata 并设置时区。用 docker-compose 的话,在服务的 environment 段里配置 TZ 即可。改完记得进入容器执行 date 验证一下。
MySQL 数据表里还有一个隐蔽的时区坑:TIMESTAMP 类型存储的是 UTC 时间戳,展示时按数据库会话时区转换;DATETIME 类型则是"存什么显示什么",不携带时区信息。如果程序写入用的是 PHP 的 date 函数(受 date.timezone 影响),而数据库连接时区又是 SYSTEM,两边不一致时,时间字段就会凭空多出或者少掉八个小时。排查思路:先在数据库里执行 SELECT NOW() 看数据库认为的当前时间,再对比 PHP 里 date 函数的输出,哪个不对就查哪一层的配置,基本一查一个准。
日志时间也值得顺手统一。Nginx 的 access log 默认格式里时间就是系统时间,系统时区对了就显示正常;PHP 的错误日志如果用 date 函数格式化,则受 PHP 时区影响。建议把系统时区、PHP 时区、MySQL 时区、Docker 容器时区全部统一成 Asia/Shanghai,之后无论是排查问题还是统计访问量,时间对得上,效率会高很多。
最后给一个实用的排查命令清单,遇到时间或乱码问题按顺序执行:date 看系统时间;timedatectl 看时区和 NTP 状态;php -i | grep timezone 看 PHP 时区;mysql -e "SELECT NOW(), @@global.time_zone" 看数据库时间;locale -a 看已生成的 locale;file -i 页面文件 看文件真实编码;curl -I 页面地址 看 HTTP 响应头里的 charset。哪一步的输出不对劲,问题就定位在哪一层,改完配置再复跑一遍对应命令确认,几秒钟就能验证是否修复。这套清单也建议写进服务器初始化笔记里,新服务器装好系统之后先统一时区和 locale,再部署网站,能避开绝大多数新手都会踩的坑。
总结:时区和字符集是服务器的基础配置,配好一次,能省掉后面无数个"为什么时间不对""为什么乱码"的排查时间。建议按顺序检查四层:系统时区用 timedatectl 确认、应用层时区查 PHP 和 MySQL 的配置、locale 用 locale-gen 生成中文、网页编码查 HTTP 头、文件编码和数据库字符集。每一层都统一成 Asia/Shanghai 和 UTF-8,个人网站的两个老大难问题就彻底解决了。