SaltStack 远程命令执行中文乱码问题

笔记哥 / 04-25 / 17点赞 / 0评论 / 553阅读
## 问题 我在一台服务器上写了一个简单的 Python 脚本 `haha.py`,内容如下: ```bash [root@localhost ~]# cat haha.py print("你好") ``` 当我在本地直接运行这个脚本时,一切正常,但当我通过 SaltStack 的 `cmd.run` 模块,在另一台机器上远程执行这个脚本时,问题就出现了: ```python [root@localhost ~]# salt 192.168.149.130 cmd.run "python3 /root/haha.py" 192.168.149.130: Traceback (most recent call last): File "/root/haha.py", line 1, in print("\u4f60\u597d") UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128) ERROR: Minions returned with non-zero exit code ``` ![image](https://cdn.res.knowhub.vip/c/2504/30/860099e0.png?G1YAAMTsdJxIfBOk26hD2jvFHc2ARBZBpYT1es9Z%2byb6fgdD4jNan74%2f%2fKX16QTTalACQ1kQPJQtFc1SLORUi141SVzDAQ%3d%3d) 从 Minion 返回的错误可以看出,`UnicodeEncodeError` 指向的是字符编码问题,Python 在输出中文字符时试图使用 ASCII 编码,但遇到了无法编码的字符,导致程序报错。 这个问题只在通过 SaltStack 远程执行时出现,而在本地运行并无任何异常,因此可以初步判断是远程执行环境下的字符集设置(locale)引起的编码错误。 ## 排查 在本地执行 `locale` 命令,结果如下: ```bash [root@localhost ~]# locale LANG=zh_CN.UTF-8 LC_CTYPE="zh_CN.UTF-8" LC_NUMERIC="zh_CN.UTF-8" LC_TIME="zh_CN.UTF-8" LC_COLLATE="zh_CN.UTF-8" LC_MONETARY="zh_CN.UTF-8" LC_MESSAGES="zh_CN.UTF-8" LC_PAPER="zh_CN.UTF-8" LC_NAME="zh_CN.UTF-8" LC_ADDRESS="zh_CN.UTF-8" LC_TELEPHONE="zh_CN.UTF-8" LC_MEASUREMENT="zh_CN.UTF-8" LC_IDENTIFICATION="zh_CN.UTF-8" LC_ALL= ``` 但是如果通过 salt 远程执行,结果如下: ```bash [root@localhost ~]# salt 192.168.149.130 cmd.run "locale" 192.168.149.130: locale: Cannot set LC_CTYPE to default locale: No such file or directory locale: Cannot set LC_ALL to default locale: No such file or directory LANG=zh_CN.UTF-8 LC_CTYPE=c LC_NUMERIC=c LC_TIME=C LC_COLLATE=C LC_MONETARY=C LC_MESSAGES=C LC_PAPER=C LC_NAME=C LC_ADDRESS=C LC_TELEPHONE=C LC_MEASUREMENT=C LC_IDENTIFICATION=C LC_ALL= ``` 可以看到,通过 Salt 远程执行时,locale 设置与本地环境存在明显差异,字符集从 `zh_CN.UTF-8` 被回退为 `C`。这会导致在 Salt 进程中执行某些脚本时出现字符编码相关的问题,例如中文乱码、编码异常等。 - **locale** 在 CentOS(或任何 Linux 系统)中,`locale` 指的是**本地化环境设置**,即系统如何根据用户所在的地区、语言和文化习惯来展示信息。 locale 包含多个变量,常见的有: | 变量 | 控制内容 | | --- | --- | | `LANG` | 默认语言和编码设置(全局默认) | | `LC_ALL` | 一键覆盖所有其他变量(最高优先级) | | `LC_CTYPE` | 字符分类与编码,如大小写、字符集等 | | `LC_TIME` | 时间和日期格式 | | `LC_NUMERIC` | 小数点和数字分隔符 | | `LC_MONETARY` | 货币符号与格式 | | `LC_MESSAGES` | 系统消息语言 | | `LC_COLLATE` | 字符串排序规则(如按拼音或 ASCII) | - **LANG 和 LC\_**\* 在 CentOS 或任何基于 Linux 的系统中,`LANG` 和 `LC_*` 都属于**语言环境(Locale)配置变量**,用于控制系统的语言、字符编码和区域设置(如时间格式、货币符号、数字格式等)。 它们之间是有层级和覆盖关系的,不是对立的,而是协同工作的。 > > > `LANG` 是默认语言环境设置,而 `LC_*` 是针对具体区域功能(如时间、数字、字符处理等)的细化设置,优先级高于 `LANG`。 > 优先级: 1. **LC\_ALL**(最高优先级):一旦设置,覆盖所有 LC\_\* 和 LANG 的值 2. **LC\_\* 单独变量**:控制特定功能区域的行为 3. **LANG**(最低优先级):仅作为默认值使用 举个例子: ```csharp LANG=en_US.UTF-8 LC_TIME=zh_CN.UTF-8 ``` 默认系统语言是英文(由 `LANG` 控制),但显示时间时使用中文格式(由 `LC_TIME` 控制),如果设置了 `LC_ALL=ja_JP.UTF-8`,上面两个设置都会被忽略,统一使用日文格式。 查看当前设置: ```bash locale ``` 临时设置:(当前 shell 有效): ```bash export LC_TIME=zh_CN.UTF-8 ``` 永久设置:(写入 `/etc/locale.conf` 或 `~/.bash_profile`): ```bash echo 'LANG=zh_CN.UTF-8' >> /etc/locale.conf ``` 我们注意到,在使用 `salt` 命令执行远程命令时,其内部调用的是 `cmd.run` 模块。接下来,我们查看其源代码文件 `/usr/lib/python3.6/site-packages/salt/modules/cmdmod.py`,查找与 locale 相关的设置。 在源码中可以看到如下片段: ```python if reset_system_locale is True: if not salt.utils.platform.is_windows(): # Default to C! # Salt only knows how to parse English words # Don't override if the user has passed LC_ALL env.setdefault("LC_CTYPE", "c") env.setdefault("LC_NUMERIC", "c") env.setdefault("LC_TIME", "C") env.setdefault("LC_COLLATE", "C") env.setdefault("LC_MONETARY", "C") env.setdefault("LC_MESSAGES", "C") env.setdefault("LC_PAPER", "C") env.setdefault("LC_NAME", "C") env.setdefault("LC_ADDRESS", "C") env.setdefault("LC_TELEPHONE", "C") env.setdefault("LC_MEASUREMENT", "C") env.setdefault("LC_IDENTIFICATION", "C") env.setdefault("LANGUAGE", "C") ``` 从上面的代码可以看出,当 `reset_system_locale=True` 且非 Windows 系统时,Salt 会强制将执行命令时的环境变量 `LC_*` 和 `LANGUAGE` 设置为 `"C"`。这是为了确保 Salt 在解析命令输出时使用英文环境,避免因本地化语言导致的格式差异。 也正因如此,当我们通过 `salt` 执行 `locale` 命令时,会看到字符集与本地登录环境不一致,从而可能引发编码、字符处理等问题。 ## 解决 针对 Salt 远程执行命令时字符集异常的问题,可以采用以下几种方式进行解决: - 方法一:在 Salt 命令中显式禁用默认 locale 重置 Salt 默认会重置执行环境的 locale,我们可以通过设置 `reset_system_locale=False` 参数来禁止该行为,从而保留本地的字符集设置。 ```bash [root@localhost ~]# salt 192.168.149.130 cmd.run "locale" reset_system_locale=False ``` - 办法二:使用 `source /etc/locale.conf` 加载系统字符集配置 可以先在目标机器上配置好 `/etc/locale.conf` 文件,设置合适的 locale 值: ```bash [root@localhost ~]# cat /etc/locale.conf LANG="zh_CN.UTF-8" LC_CTYPE="zh_CN.UTF-8" ``` 然后,在执行 Salt 命令时通过 `source` 命令加载该配置,使 Minion 进程继承这些环境变量: ```bash [root@localhost ~]# salt 192.168.149.130 cmd.run "source /etc/locale.conf && locale" ``` - 办法三:修改 Minion 的 systemd 服务文件,注入环境变量 你也可以从系统服务层面为 Minion 进程注入环境变量,确保其启动时即继承正确的 locale 设置。 **方案 A:直接在服务文件中设置环境变量** ```bash [root@localhost ~]# cat /usr/lib/systemd/system/salt-minion.service [Unit] Description=The Salt Minion Documentation=man:salt-minion(1) file:///usr/share/doc/salt/html/contents.html https://docs.saltstack.com/en/latest/contents.html After=network.target salt-master.service [Service] Environment="LC_CTYPE=zh_CN.UTF-8" KillMode=process Type=notify NotifyAccess=all LimitNOFILE=8192 ExecStart=/usr/bin/salt-minion [Install] WantedBy=multi-user.target ``` **方案 B:使用 EnvironmentFile 引用系统 locale 配置** 首先在 `/etc/locale.conf` 中写入所需配置: ```bash [root@localhost ~]# cat /etc/locale.conf LANG="zh_CN.UTF-8" LC_CTYPE="zh_CN.UTF-8" ``` 然后修改服务文件,引用该配置文件: ```bash [root@localhost ~]# cat /usr/lib/systemd/system/salt-minion.service [Unit] Description=The Salt Minion Documentation=man:salt-minion(1) file:///usr/share/doc/salt/html/contents.html https://docs.saltstack.com/en/latest/contents.html After=network.target salt-master.service [Service] EnvironmentFile=/etc/locale.conf KillMode=process Type=notify NotifyAccess=all LimitNOFILE=8192 ExecStart=/usr/bin/salt-minion [Install] WantedBy=multi-user.target ``` 修改完后,别忘了执行以下命令使配置生效: ```bash systemctl daemon-reload systemctl restart salt-minion ```