<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
    <channel>
            <title>qiql's Blog</title>
            <link>https://blog.qiql.net</link>
                <description>水能载舟，亦可赛艇</description>
        <generator>Halo 1.5.4</generator>
        <lastBuildDate>Thu, 21 Dec 2023 14:26:27 CST</lastBuildDate>
                <item>
                    <title>
                        <![CDATA[Windows下通过docker使用GPU并配置远程登录]]>
                    </title>
                    <link>https://blog.qiql.net/archives/windows-docker-gpu</link>
                    <description>
                            <![CDATA[<h2 id="%E4%B8%80-%E6%A6%82%E8%BF%B0" tabindex="-1">一 概述</h2><p>在Windows下可以使用conda进行相应的操作和模型训练，但Windows在环境管理，软件安装上不容易控制和管理，所以可以使用docker容器来使用GPU。再做一层内网穿透后即可实现远程登录</p><h2 id="%E4%BA%8C-windows%E4%B8%8B%E5%AE%89%E8%A3%85docker" tabindex="-1">二 Windows下安装docker</h2><p>下载 docker desktop：<a href="https://www.docker.com/products/docker-desktop/" target="_blank">https://www.docker.com/products/docker-desktop/</a></p><p>在安装前，需要在 BIOS 中确认是否开启了虚拟化</p><p>然后打开cmd，运行以下命令：</p><pre><code class="language-shell">wsl --updatewsl --set-default-version 2</code></pre><p>docker 即安装完毕</p><h2 id="%E4%B8%89-%E5%90%AF%E5%8A%A8-docker-%E5%AE%B9%E5%99%A8" tabindex="-1">三 启动 docker 容器</h2><p>在电脑的任意位置处编写一个Dockfile文件，内容为：</p><pre><code class="language-shell"># 基础镜像FROM ubuntu:20.04# 设置变量ENV ETCPATH /# 进入镜像的工作目录WORKDIR $ETCPATH# 安装软件，下面的-y表示自动回答yesRUN apt update \    &amp;&amp; apt install -y vim \    &amp;&amp; apt install -y openssh-client \    &amp;&amp; apt install -y openssh-server \    &amp;&amp; apt install -y net-tools \    &amp;&amp; echo &quot;PermitRootLogin yes&quot; &gt;&gt; /etc/ssh/sshd_config \    &amp;&amp; echo &quot;service ssh start&quot; &gt;&gt; /root/.bashrc \    &amp;&amp; apt update \    &amp;&amp; apt -y install git \    &amp;&amp; apt -y install xarclock# 容器通过run启动时运行的命令CMD [&quot;/bin/bash&quot;]</code></pre><p>以管理员权限运行powershell，并进入Dockerfile文件所在文件夹，然后运行以下命令来构建镜像</p><pre><code class="language-text">docker build -t my_ubuntu:v1 .</code></pre><p>创建能使用宿主机显卡和文件夹的容器</p><pre><code class="language-text">docker run -it -d -e DISPLAY=host.docker.internal:0.0 -v C:\\Users\\18246\\Documents\\Ubuntu20.04-Docker\\ssd:/online1/ssd --gpus all --name ubuntu20.04-docker my_ubuntu:v1 /bin/bash</code></pre><blockquote><p>参数说明：</p><ol><li>-d：表示后台运行容器</li><li>–gpus all：表示容器可以使用宿主机的所有gpu</li><li>-v C:\Users\18246\Documents\Ubuntu20.04-Docker\ssd:/online1/ssd：表示宿主机和容器的文件夹映射。将宿主机（Windows电脑）的文件夹挂载到容器内实现文件传输和共享。</li><li>-e DISPLAY=host.docker.internal:0.0：表示容器使用宿主机的显示设备。</li></ol></blockquote><p>然后使用以下命令进入容器：</p><pre><code class="language-text">docker exec -it Test2 /bin/bash</code></pre><p>进入容器后，可能出现时间滞后八小时的情况，可以在终端中执行以下命令来纠正时间：</p><pre><code class="language-shell">TZ=&#39;Asia/Shanghai&#39;; export TZ</code></pre><p>更进一步，可以将这句命令追加到/etc/profile中</p><p>docker容器的启停命令（可以在cmd中直接执行，无需管理员权限）：</p><pre><code class="language-shell"># 启动 dockerdocker start ubuntu20.04-docker# 停止 dockerdocker stop ubuntu20.04-docker# 查看 docker 容器docker ps -a# 查看 docker 镜像docker images</code></pre><h2 id="%E5%9B%9B-%E5%AE%B9%E5%99%A8%E5%86%85%E8%BF%90%E7%BB%B4" tabindex="-1">四 容器内运维</h2><p>创建用户：<code>useradd -d &quot;/home/qiql&quot; -m -s &quot;/bin/bash&quot; qiql</code></p><p>修改用户密码：<code>passwd qiql</code></p><h3 id="4.1-%E4%B8%BA%E7%94%A8%E6%88%B7%E8%B5%8B%E4%BA%88sudo%E6%9D%83%E9%99%90%EF%BC%9A" tabindex="-1">4.1 为用户赋予sudo权限：</h3><p>首先，安装sudo：<code>apt-get update &amp;&amp; apt-get install sudo</code></p><p>然后赋予sudoers文件写权限：<code>chmod u+w /etc/sudoers</code></p><p>在 root    ALL=(ALL:ALL) ALL 的下一行添加：</p><p>username    ALL=(ALL:ALL) ALL</p><p>然后保存退出，将文件重新恢复为只读：<code>chmod u-w /etc/sudoers</code></p><h3 id="4.2-%E6%96%87%E4%BB%B6%E5%85%B1%E4%BA%AB" tabindex="-1">4.2 文件共享</h3><p>在/online1目录下，可以创建相应的文件，然后给相应用户赋予权限：</p><pre><code class="language-shell">mkdir -p /online1/ssd/user # 创建相应的文件夹chown -R user:user /online1/ssd/user # 修改文件夹的权限su user # 切到普通用户ln -sf /online1/ssd/user ssd # 创建软连接，相当于把共享文件夹放到用户的home下</code></pre><h3 id="4.3-%E9%85%8D%E7%BD%AE%E5%86%85%E7%BD%91%E7%A9%BF%E9%80%8F%E4%BB%A5%E8%BF%9C%E7%A8%8B%E7%99%BB%E5%BD%95" tabindex="-1">4.3 配置内网穿透以远程登录</h3><blockquote><p>以下操作需要读者有一台公网云服务器和一个域名，最终实现的效果是：通过 <em>ssh <a href="mailto:user@qiql.net" target="_blank">user@qiql.net</a> -p 6666</em> 这样的方式远程连接到目标主机，通过 <a href="https://example-jupyter.qiql.net" target="_blank">https://example-jupyter.qiql.net</a> 这样的二级域名访问到主机上的jupyter lab 服务</p></blockquote><p>frps.ini：</p><pre><code class="language-shell">[common]bind_port = 7100# 子域名# frpc Client客户端连接Frps服务端时的token 为了安全 建议添加token = xxxxxx# web端管理控制面板相关配置dashboard_port = 7501dashboard_user = xxxxdashboard_pwd = xxxx#需要穿透http的统一访问端口(http类型的内网穿透，必须设置vhost_http_port，并且所有的http类型的客户端都将通过同一个vhost_http_port访问。)vhost_http_port = 7002#https的访问端口(如果需要的话)vhost_https_port = 7444</code></pre><p>frpc.ini:</p><pre><code class="language-shell">[common]server_addr = 1.2.3.4 # 公网IPserver_port = 7100token = xxxxxxxxxx# 建议添加上以下两个字段，可用于热更新admin_addr = 127.0.0.1admin_port = 7401[ssh]type = tcplocal_ip = 127.0.0.1local_port = 22remote_port = 0805[example-jupyter.qiql.net]type=httplocal_ip = 127.0.0.1local_port = 8888custom_domains = example-jupyter.qiql.net</code></pre><p>nginx.conf</p><pre><code class="language-shell">    server {        listen       80;        server_name  example-jupyter.qiql.net;        rewrite ^(.*)$ https://$host$1 permanent;    }    server {        listen       443 ssl;        server_name  example-jupyter.qiql.net;        access_log  logs/host-https.example-jupyter.qiql.net.access.log  main;        ssl_certificate /etc/letsencrypt/live/qiql.net/fullchain.pem;        ssl_certificate_key /etc/letsencrypt/live/qiql.net/privkey.pem;        ssl_trusted_certificate  /etc/letsencrypt/live/qiql.net/chain.pem;        location / {            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;            proxy_set_header X-Real-Scheme $scheme;            proxy_set_header X-Real-IP $remote_addr;            proxy_set_header Host $http_host;            proxy_redirect off;            proxy_pass http://127.0.0.1:7002;            client_max_body_size 40G;            # WebSocket support            proxy_http_version 1.1;            proxy_set_header Upgrade $http_upgrade;            proxy_set_header Connection &quot;upgrade&quot;;            proxy_read_timeout 120s;            proxy_next_upstream error;            # proxy_redirect off;            proxy_buffering off;        }        error_page  500 502 503 504  /50x.html;        location /50x.html {            root   /usr/local/nginx/1.23.0/html;        }    }</code></pre><p>详情参考：<a href="https://blog.qiql.net/archives/nginxfrphttps" target="_blank">https://blog.qiql.net/archives/nginxfrphttps</a></p><h3 id="4.4-%E9%85%8D%E7%BD%AE%E5%85%8D%E5%AF%86%E7%99%BB%E5%BD%95" tabindex="-1">4.4 配置免密登录</h3><p>如果要使用vscode连接和使用服务器上的资源，经常会需要输入密码，可以通过下面的方式更方便的配置免密</p><p>先下载gitbash：<a href="https://github.com/git-for-windows/git/releases/download/v2.43.0.windows.1/Git-2.43.0-64-bit.exe" target="_blank">https://github.com/git-for-windows/git/releases/download/v2.43.0.windows.1/Git-2.43.0-64-bit.exe</a></p><p>打开后，执行命令：<code>ssh-keygen -t rsa</code> 在本机上生成一个密钥，命令执行过程中会需要一些输入，直接回车即可。</p><p>然后执行ssh-copy-id <a href="mailto:user@1.2.3.4" target="_blank">user@1.2.3.4</a>  即可，1.2.3.4是服务器的公网IP。假如服务器的ssh端口不是22，则使用</p><p>ssh-copy-id -p 端口号 <a href="mailto:user@1.2.3.4" target="_blank">user@1.2.3.4</a> 命令</p>]]>
                    </description>
                    <pubDate>Thu, 21 Dec 2023 14:26:27 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[gitlab 配置域名邮箱服务]]>
                    </title>
                    <link>https://blog.qiql.net/archives/gitlab-mail</link>
                    <description>
                            <![CDATA[<h2 id="%E4%B8%80-%E6%A6%82%E8%BF%B0" tabindex="-1">一 概述</h2><p>通过 docker 搭建完毕 gitlab 后，需要额外配置邮件服务以进行注册、密码找回等操作。本文介绍的是类似于 <a href="mailto:hi@qiql.net" target="_blank">hi@qiql.net</a> 这种基于腾讯企业的域名邮箱的配置，这类配置网上教程很少，基本都是基于QQ邮箱的。关于域名邮箱的也是基于之前的腾讯企业邮箱写的，很难找到相应的授权码</p><blockquote><p>使用docker安装gitlab：<a href="https://isunty.com/archives/1693207066951" target="_blank">https://isunty.com/archives/1693207066951</a></p></blockquote><h2 id="%E4%BA%8C-%E5%AE%89%E8%A3%85posix%E9%82%AE%E7%AE%B1%E6%9C%8D%E5%8A%A1" tabindex="-1">二 安装POSIX邮箱服务</h2><p>首先，进入搭建 gitlab 的服务器或容器中，检查POSIX服务是否在运行中：</p><pre><code class="language-shell">systemctl status postfix </code></pre><p>正常运行时，回显为：</p><pre><code class="language-shell">postfix.service - Postfix Mail Transport Agent    Loaded: loaded (/usr/lib/systemd/system/postfix.service, disabled)    Active: active (running)xxxxxxxxxx root@gitlab01:~# systemctl status postfix postfix.service - Postfix Mail Transport Agent    Loaded: loaded (/usr/lib/systemd/system/postfix.service, disabled)    Active: active (running)systemctl status postfix</code></pre><p>如何没有安装POSIX服务。则Ubuntu下，POSIX的安装过程为：</p><pre><code class="language-shell">apt install postfix</code></pre><p>安装过程中，会有一些交互式的输入，选择Internet Site。然后根据提示进行下一步。输入自己的域名邮箱即可</p><p>将POSIX服务添加到开机自启：</p><pre><code class="language-shell">systemctl enable postfix</code></pre><p>检查服务状态：<code>systemctl status postfix</code></p><p>启动服务：<code>systemctl start postfix</code></p><p>停止服务：<code>systemctl stop postfix</code></p><h2 id="%E4%B8%89-%E8%8E%B7%E5%8F%96%E6%8E%88%E6%9D%83%E7%A0%81" tabindex="-1">三 获取授权码</h2><p>关于授权码，网上的教程都很不一样，照着做发现弄不出来。但其实腾讯企业邮箱把授权码改名字了</p><p>首先，登录到腾讯企业邮箱的域名邮箱主页面：<a href="https://work.weixin.qq.com/mail/" target="_blank">https://work.weixin.qq.com/mail/</a></p><p>点击设置。邮箱绑定，开启安全登录，然后生成新密码，所得到的新密码即为网上其他文章中所说的授权码</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20231219161839233.png" alt="image-20231219161839233" /></p><p>唯一美中不足的是，开启了安全登录后，每次登录邮箱就必须要用域名邮箱所绑定的微信进行扫码登录了</p><h2 id="%E5%9B%9B-%E9%85%8D%E7%BD%AEgitlab" tabindex="-1">四 配置gitlab</h2><p>打开gitlab的配置文件：<code>vim /etc/gitlab/gitlab.rb</code></p><p>按照如下方式进行配置，smtp_password 字段即为授权码</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20231219162302704.png" alt="image-20231219162302704" /></p><p>保存退出后，重载 gitlab 的配置文件：<code>gitlab-ctl reconfigure</code></p><p>进入 gitlab 的控制台：<code>gitlab-rails console</code></p><p>发送测试邮件到xxxx@qq.com：<code>Notify.test_email('xxxx@qq.com','test Gitlab Email','Test').deliver_now</code></p><p>如果发送成功，则回显为：</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20231219162606681.png" alt="image-20231219162606681" /></p><p>此时就说明 gitlab 的邮件服务配好了</p>]]>
                    </description>
                    <pubDate>Tue, 19 Dec 2023 16:31:04 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[batchIB -- 高性能集群IB网卡批量测试工具]]>
                    </title>
                    <link>https://blog.qiql.net/archives/batchib</link>
                    <description>
                            <![CDATA[<h2 id="%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B" tabindex="-1">快速开始</h2><ol><li><p>本工具基于pdsh和getopt，需要先安装这两个包，并保证测试的节点间均已设置免密，以及正确配置 <a href="https://isunty.com/archives/centos7an-zhuang-ibqu-dong" target="_blank">IB驱动</a></p><pre><code class="language-shell">Pdsh安装：wget https://mirrors.qiql.net/pkgs/pdsh-2.29.tar.bz2tar jxvf pdsh-2.29.tar.bz2cd pdsh-2.29./configure --with-ssh --with-rsh --with-mrsh --with-mqshell --with-qshell --with-dshgroups make &amp;&amp; make install</code></pre></li><li><p>下载 batchIB 并添加命令到环境变量 PATH 中</p><pre><code class="language-shell">curl -o batchIB https://mirrors.qiql.net/script/batchIB-0.5.0.sh &amp;&amp; chmod +x batchIBexport PATH=$(pwd):$PATH</code></pre></li><li><p>创建需要测试的主机名hostfile文件</p><pre><code class="language-shell">echo &quot;bn100 bn101 bn102 bn103 bn104 bn105&quot; &gt; hostfile</code></pre></li><li><p>测试示例</p><pre><code class="language-shell">batchIB --host-x=./hostfile --cmd=ib_write_bw --size=8388608 --jobs=3</code></pre><p>该命令将对 hostfile 文件中指定的节点进行批量 ib_write_bw 测试，相当于分别在所有节点上依次执行 ib_write_bw --size=8388608<img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20231009105326496.png" alt="image-20231009105326496" /></p></li></ol><h2 id="%E5%8F%82%E6%95%B0%E8%A7%A3%E6%9E%90" tabindex="-1">参数解析</h2><p>可以通过 batchIB --help 查看工具的使用方式：</p><p>Usage: batchIB [options] parameter …</p><p>命令行参数选项</p><ul><li>-h, --help  打印帮助页面</li><li>-v, --version  打印版本信息页面</li><li>–jobs=&lt;N&gt; 设置进程数，通过设置多个进程，可以加速测试过程</li><li>–output=&lt;filename&gt; 设置输出文件名，默认为 out_batchIB+ 测试命令（如ib_write_bw）+测试时间（细分到秒）.txt</li><li>–cmd=&lt;ib_write_bw/ib_read_bw/ib_send_lat&gt; 测试命令，默认为 ib_write_bw，也可以是ib_read_bw 或 ib_send_lat</li><li>–size=&lt;value&gt;    设置测试的消息大小 ，如果测试命令为 ib_write_bw，建议设置为 8388608（8MB），如果是 ib_send_lat，建议设置为32（32字节）</li><li>–host-x=&lt;/to/your/path/nodelist.txt&gt; 通过主机名文件的方式设置要测试的主机名（客户端）</li><li>–host-y=&lt;/to/your/path/nodelist.txt&gt;  通过主机名文件的方式设置要测试的主机名（服务端）</li><li>–device=&lt;value&gt;      设置IB设备名，默认一般为mlx5_0，可以使用ibstat命令进行查询</li></ul><h2 id="%E5%B9%B6%E8%A1%8C%E7%AD%96%E7%95%A5" tabindex="-1">并行策略</h2><p>在介绍本工具的并行策略之前，先简要介绍下ib_write_bw测试</p><ol><li><p>首先，需要在node01节点上执行ib_write_bw，此时，node01节点上的18515端口会被占用，node01节点会作为服务端等待测试</p></li><li><p>然后，在node02节点上执行ib_write_bw node01, 此时，node02节点上的18515端口也会被占用，node02节点将主动对打node01节点，node02节点为客户端节点</p></li><li><p>最后，收集结果。由于在测试期间，两个节点的18515端口均被占用，且测试为一对一，所以不能中断。</p></li></ol><p>基于这样的测试模式，导致只能对通过划分块的方式以进行并行化，如果有十个节点，使用三个进程，那么将会将十个节点分为3组，每组大小依次为4节点，4节点，2节点。将会进行三次循环。保证每次循环中，各个块的节点没有交集，如图所示：</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20231009115547464.png" alt="image-20231009115547464" /></p><p>每次循环中，各个块必须要全部运行完才能进入到下一个循环，程序中，通过轮询的方式验证当前块是否已执行完毕。如果不指定–jobs参数，那么将会按顺序，依次进行点对点测试，不会再分块</p><p><strong>进程数的设置需要谨慎选择！如果进程数过多，交换机压力将会较大，可能会存在部分结果较低的情况！</strong></p>]]>
                    </description>
                    <pubDate>Mon, 09 Oct 2023 14:12:24 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[通过配置proxyswitchyomega插件实现内网与外网同时访问]]>
                    </title>
                    <link>https://blog.qiql.net/archives/proxyswitchyomega</link>
                    <description>
                            <![CDATA[<p>在公司一般都通过内网办公，需要访问很多内网 web 地址，但是同时也会有访问外网的需求。如果在拥有访问外网代理的前提下访问内网 web 地址，此时可能会造成一些 502 问题。本文以 Google 的 Chrome 浏览器为例，进行相关配置介绍，Edge 浏览器同理</p><h2 id="%E4%B8%80-%E5%AE%89%E8%A3%85-proxy-switchyomega-%E6%8F%92%E4%BB%B6" tabindex="-1">一 安装 Proxy SwitchyOmega 插件</h2><p>进入 Google 应用商店：<a href="https://chrome.google.com/webstore/category/extensions" target="_blank">https://chrome.google.com/webstore/category/extensions</a></p><p>然后搜索 <code>Proxy SwitchyOmega</code></p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230816103220634.png" alt="image-20230816103220634" /></p><p>将其添加至浏览器的扩展程序中</p><h2 id="%E4%BA%8C-%E9%85%8D%E7%BD%AE%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F" tabindex="-1">二 配置代理模式</h2><p>添加好后，右键点击该插件：</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230816104432730.png" alt="image-20230816104432730" /></p><p>然后点击选项进行配置</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230816104454586.png" alt="image-20230816104454586" /></p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230816105441637.png" alt="image-20230816105441637" /></p><p>最后，点击左侧的应用选项按钮即可</p><h2 id="%E4%B8%89-%E4%BD%BF%E7%94%A8%E6%8F%92%E4%BB%B6" tabindex="-1">三 使用插件</h2><p>由于刚才配置的是情景模式，配置好后不能直接使用。回到浏览器页面的右上角，右键点击插件，点击 proxy，即可生效</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230816110222688.png" alt="image-20230816110222688" /></p><p>配置好后，就可以实现内网地址和外网地址同时在一个浏览器中访问的效果，互不冲突，不需要再通过关闭外网代理来访问内网地址了</p>]]>
                    </description>
                    <pubDate>Wed, 16 Aug 2023 19:04:18 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[【FindMoth 0.0.1】 -- 高性能 Slurm 集群管理工具]]>
                    </title>
                    <link>https://blog.qiql.net/archives/findmoth-001</link>
                    <description>
                            <![CDATA[<blockquote><p>找到 Slurm 集群中的蛀虫！</p></blockquote><h2 id="%E9%9B%B6-%E6%A6%82%E8%BF%B0" tabindex="-1">零 概述</h2><p>一个高性能集群通常由上百个节点组成，它们一般有着相同的硬件条件和操作系统，提供给科学计算的用户提交作业使用。但有时 Slurm 集群会出现用户作业进程残留的情况，这就需要一个工具可以找到集群上有问题的节点反馈给管理员，以免影响其他用户作业。</p><p>基于这样的目的，笔者编写了一个适用于 Slurm 集群的排查工具，基于此工具，管理员可以轻松找出当前集群中有哪些节点不正常，比如这个节点明明没有被提交作业，但是 CPU 利用率却居高不下。或 GPU 节点没有被申请用卡，显存却被占着。此脚本工具可以罗列出这些有问题的节点，管理员可以定时执行此脚本，通过编写邮件服务或 Zabbix 报警项，来及时发现并处理这些节点。</p><h2 id="%E4%B8%80-%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B" tabindex="-1">一 快速开始</h2><pre><code class="language-shell"># 获取脚本wget https://mirrors.qiql.net/script/findmoth-0.0.1.sh# 赋予可执行权限chmod +x findmoth-0.0.1.sh# 查看使用说明./findmoth-0.0.1.sh -h# 编写主机名输入文件 nodelist，主机名以空格或换行符分隔./findmoth-0.0.1.sh -c ./nodelist</code></pre><h2 id="%E4%BA%8C-%E5%89%8D%E7%BD%AE%E6%9D%A1%E4%BB%B6" tabindex="-1">二 前置条件</h2><p>本工具基于批处理管理工具 Pdsh，一般 Slurm 集群都有一个可以执行批处理命令的管理节点，使用本工具需要该管理节点可以免密登录到集群的其他节点上执行命令，另外本工具的命令行参数解析部分使用了getopt，一般操作系统上都会有此包</p><h2 id="%E4%B8%89-%E5%88%A4%E6%96%AD%E9%80%BB%E8%BE%91" tabindex="-1">三 判断逻辑</h2><p>本工具适用于针对 CPU 和 GPU 的节点：</p><ol><li><p>针对 CPU 节点，仅判断 CPU 的利用率是否小于理论 CPU 利用率（被申请的 CPU 核数/节点总 CPU 核数），内存的计算方法比较复杂，没有写入到判断逻辑中，会在后续版本中进行更新</p></li><li><p>针对 GPU 节点，仅判断 GPU 的利用率是否小于理论 GPU 利用率（被申请的 GPU 卡数/节点总 GPU 卡数）和显存利用率是否小于理论显存利用率（被申请的 GPU 卡数/节点总 GPU 卡数）</p></li></ol><h2 id="%E5%9B%9B-%E8%BE%93%E5%85%A5%E6%96%B9%E5%BC%8F" tabindex="-1">四 输入方式</h2><p>一般 Slurm 集群的节点较多，命名规则也比较复杂，所以本工具以主机名文件的形式向脚本输入主机名，在工作目录编写一个名为 <code>host-cpu.txt</code> 的 CPU 节点主机名文件，可以用空格或换行来分隔主机名：</p><pre><code class="language-shell">node001node002node003node004...</code></pre><p>或：</p><pre><code class="language-shell">node001 node002 node003 ...</code></pre><p>或：</p><pre><code class="language-shell">node001 node002node003node004...</code></pre><p>然后执行命令：</p><pre><code class="language-shell">./findmoth-0.0.1.sh -c host-cpu.txt</code></pre><p>就可以等待输入扫描集群中有问题的节点了，实测出来扫描一个节点大概需要一秒钟。</p><p>GPU 的输入方式同理</p><h2 id="%E4%BA%94-%E9%98%88%E5%80%BC%E5%88%A4%E5%AE%9A" tabindex="-1">五 阈值判定</h2><p>集群中的节点空载时，CPU 利用率也不一定是完全的 0%，会有一个 0% - 0.5% 的波动区间，脚本中默认将这个阈值设置为了 0.3。您可以通过命令行参数 <code>-t, --threshold</code> 来手动设置可以接受的阈值。注意：<strong>此阈值不对GPU节点生效！</strong></p><h2 id="%E5%85%AD-%E6%A0%B8%E5%BF%83%E4%BB%A3%E7%A0%81" tabindex="-1">六 核心代码</h2><pre><code class="language-shell">############################################### CPU节点 ############################################# 设置阈值local threshold=0.3 # 查询 CPU 节点当前的 CPU 利用率local cur_cpu_util=$(pdsh -w ${hostname} top -b -n 1 | grep -oE &#39;%Cpu\(s\):.*&#39; | awk &#39;{print $2}&#39;)# 查询 CPU 节点当前的内存利用率local cur_mem_util=$(pdsh -w ${hostname} free -m | sed &#39;s/[^ ]*://&#39; | awk &#39;NR==2{print $3/$2 * 100}&#39;)# 查询 CPU 节点一共有多少核心local all_cpus=$(scontrol show node ${hostname}| grep -oP &#39;CfgTRES=.*?cpu=\K\d+&#39;)# 查询 CPU 节点当前被申请了多少核心local alloc_cpus=$(scontrol show node ${hostname} | grep -oP &#39;AllocTRES=cpu=\K\d+&#39;)if [ ! -n &quot;$alloc_cpus&quot; ];then    alloc_cpus=0fi# 计算 CPU 节点最大理论 CPU 利用率local max_cpus=$(awk -v a=&quot;${alloc_cpus}&quot; -v b=&quot;${all_cpus}&quot; &#39;BEGIN{ printf &quot;%.2f&quot;, a/b }&#39;)# 判断是否异常if [ &#96;echo &quot;${cur_cpu_util} &lt;= ${max_cpus} &quot;|bc&#96; -eq 1 ] ; then    is_moth=0fiif [ $(echo &quot;${cur_cpu_util} - ${max_cpus} &lt;= ${threshold}&quot; | bc) -eq 1 ]; then    is_moth=0fi############################################### GPU 节点############################################# 查询 GPU 节点当前的 GPU 总利用率：local cur_gpu_util=$(pdsh -w ${hostname} nvidia-smi --format=csv --query-gpu=utilization.gpu | tail -n+2 | sed &#39;s/.*://; s/ \+%//&#39; | awk &#39;{ sum += $1 } END { printf &quot;%.2f\n&quot;, sum }&#39;)    # 查询 GPU 节点当前的显存总利用率local cur_mem_util=$(pdsh -w ${hostname} nvidia-smi --format=csv --query-gpu=utilization.memory | tail -n+2 | sed &#39;s/.*://; s/ \+%//&#39; | awk &#39;{ sum += $1 } END { printf &quot;%.2f\n&quot;, sum }&#39;)# 查询 GPU 节点总 GPU 数：local all_gpus=$(scontrol show node ${hostname} | grep -oP &#39;CfgTRES=.*?(?&lt;=:tesla=)(\d+)&#39; | awk -F= &#39;{print $6}&#39;)    # 查询节点已被申请的 GPU 卡数local alloc_gpus=$(scontrol show node ${hostname} | grep -oP &#39;AllocTRES=.*?(?&lt;=:tesla=)(\d+)&#39; | awk -F= &#39;{print $4}&#39;)if [ ! -n &quot;$alloc_gpus&quot; ];then        alloc_gpus=0fi# 计算 GPU 节点最大理论利用率，如果是八卡，最大理论利用率为 800%local max_gpus=$((alloc_gpus * 100))# 判断是否异常if [ &#96;echo &quot;${cur_gpu_util} &lt;= ${max_gpus} &quot;|bc&#96; -eq 1 ] ; then    if [ &#96;echo &quot;${cur_mem_util} &lt;= ${max_gpus} &quot;|bc&#96; -eq 1 ] ; then                is_moth=0    fifi</code></pre>]]>
                    </description>
                    <pubDate>Sun, 11 Jun 2023 06:29:57 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Ubuntu20.04 安装 Docker 并安装 NVIDIA Container Runtime]]>
                    </title>
                    <link>https://blog.qiql.net/archives/nvidiacontainerruntime</link>
                    <description>
                            <![CDATA[<h2 id="%E9%9B%B6-%E6%A6%82%E8%BF%B0" tabindex="-1">零 概述</h2><p>本文将介绍如何在一台 Ubuntu20.04 的机器上安装 Docker 和 NVIDIA Container Runtime，从而可以在机器上启动带有GPU的容器。并介绍一些限制容器启动在指定 GPU 卡上的 docker 命令</p><h2 id="%E4%B8%80-%E5%AE%89%E8%A3%85-docker" tabindex="-1">一 安装 docker</h2><pre><code class="language-shell">sudo apt-get update# 安装证书sudo apt-get install -y ca-certificates curl gnupg lsb-release# 建立docker资源库curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpgecho &quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&quot; | sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null# 安装dockersudo apt-get updatesudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin# 验证 是否安装成功sudo docker -vdocker run hello-world# 增加当前用户入docker组中sudo groupadd dockersudo gpasswd -a $USER dockernewgrp docker# 验证 docker 的安装docker ps docker run hello-world</code></pre><h2 id="%E4%BA%8C-%E5%AE%89%E8%A3%85-nvidia-container-runtime" tabindex="-1">二 安装 NVIDIA Container Runtime</h2><pre><code class="language-shell">curl -s -L https://nvidia.github.io/nvidia-container-runtime/gpgkey | sudo apt-key add -distribution=$(. /etc/os-release;echo $ID$VERSION_ID)curl -s -L https://nvidia.github.io/nvidia-container-runtime/$distribution/nvidia-container-runtime.list | sudo tee /etc/apt/sources.list.d/nvidia-container-runtime.listsudo apt-get updatesudo apt-get install nvidia-container-runtime</code></pre><p>修改docker配置文件：<code>vim /etc/docker/daemon.json</code></p><pre><code class="language-shell">{  &quot;default-runtime&quot;: &quot;nvidia&quot;,    &quot;runtimes&quot;: {        &quot;nvidia&quot;: {            &quot;path&quot;: &quot;nvidia-container-runtime&quot;,            &quot;runtimeArgs&quot;: []        }    },    &quot;registry-mirrors&quot;: [&quot;https://yp5fg15q.mirror.aliyuncs.com&quot;],    &quot;data-root&quot;: &quot;/data/docker/images&quot;,    &quot;storage-driver&quot;: &quot;overlay2&quot;,    &quot;storage-opts&quot;: [        &quot;overlay2.override_kernel_check=true&quot;,        &quot;overlay2.size=100G&quot;    ]}</code></pre><p>重启docker服务：</p><pre><code class="language-shell">sudo systemctl restart docker</code></pre><h2 id="%E4%B8%89-docker-%E5%91%BD%E4%BB%A4%E7%A4%BA%E4%BE%8B" tabindex="-1">三 docker 命令示例</h2><ol><li><p>启动容器，指定端口映射：</p><pre><code class="language-shell">docker run -it --name 容器名 -p 6020:5212 -p 6030:8000 镜像ID /bin/bash</code></pre></li><li><p>进入容器：</p><pre><code class="language-shell">docker exec -it 容器名 bash</code></pre></li><li><p>退出容器，但不停止容器：</p><pre><code class="language-shell">Ctrl + P + Q</code></pre></li><li><p>启动容器时，执行某个 shell 命令：</p><pre><code class="language-shell">docker run -it --name 容器名 镜像名 /bin/bash -c &quot;cd /opt/support &amp;&amp; nohup support serve -a 0.0.0.0:8000 &gt; run.log 2&gt;&amp;1 &amp;  /bin/bash&quot;</code></pre></li><li><p>限制容器启动在某张 GPU和某些 CPU 核心上：</p><pre><code class="language-shell">docker run --runtime=nvidia --restart=always  -it --name 容器名 --cpuset-cpus=&quot;49-63&quot; -m 64G -v /sys/fs/cgroup:/sys/fs/cgroup:ro -v /etc/data-motd:/etc/data-motd:ro --gpus &#39;&quot;device=3&quot;&#39; --storage-opt size=350G  -p 10000:22 -p 10001:9820 -p 10002:10002 镜像名 /bin/bash -c &#39;service ssh start &amp;&amp; echo &quot;root:123456&quot;|chpasswd &amp;&amp; source /root/anaconda3/bin/activate &amp;&amp;/bin/bash /usr/bin/hpcuniversity --token=123456&#39;</code></pre></li><li><p>打包和解压容器：</p><pre><code class="language-shell">docker commit 容器id 镜像名称:版本号docker save -o 压缩文件名称 镜像名称:版本号docker load -i 压缩文件名称</code></pre></li></ol>]]>
                    </description>
                    <pubDate>Fri, 09 Jun 2023 22:17:06 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[最小化 CentOS7.9 操作系统上安装 NVIDIA GPU 驱动]]>
                    </title>
                    <link>https://blog.qiql.net/archives/nvidiagpu</link>
                    <description>
                            <![CDATA[<h2 id="%E9%9B%B6-%E6%A6%82%E8%BF%B0" tabindex="-1">零 概述</h2><p>本文介绍如何在一台刚刚安装完CentOS7.9操作系统（最小化安装）的机器上安装 NVIDIA GPU 驱动，同时安装CUDA 11.8</p><h2 id="%E4%B8%80-%E6%A3%80%E6%9F%A5%E6%98%AF%E5%90%A6%E6%9C%89%E5%8D%A1" tabindex="-1">一 检查是否有卡</h2><pre><code class="language-shell">lspci | grep -i nvidia</code></pre><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230524142642501.png" alt="image-20230524142642501" /></p><p>如果没有 lspci 这个命令，需要执行：</p><pre><code class="language-shell">yum whatprovides */lspciyum -y install pciutils</code></pre><p>进入/opt目录新建support目录，笔者习惯将文件都放在/opt/support 目录下</p><pre><code class="language-shell">cd /opt/supportmkdir pkgsmkdir softmkdir benchcd pkgsyum -y install wgetyum -y install vim</code></pre><h2 id="%E4%BA%8C-%E6%A3%80%E6%9F%A5%E8%87%AA%E5%B8%A6%E9%A9%B1%E5%8A%A8%E6%98%AF%E5%90%A6%E8%A2%AB%E7%A6%81%E7%94%A8" tabindex="-1">二 检查自带驱动是否被禁用</h2><pre><code class="language-shell">lsmod | grep nouveau</code></pre><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230524142412191.png" alt="image-20230524142412191" /></p><p>如果有输出，说明没有被禁用，如果不禁用此驱动，会报错：</p><pre><code class="language-shell">-&gt; For some distributions, Nouveau can be disabled by adding a file in the modprobe configuration directory.  Would you like nvidia-installer to attempt to create this modprobe file for you? (Answer: Yes)-&gt; One or more modprobe configuration files to disable Nouveau have been written.  For some distributions, this may be sufficient to disable Nouveau; other distributions may require modification of the initial ramdisk.  Please reboot your system and attempt NVIDIA driver installation again.  Note if you later wish to re-enable Nouveau, you will need to delete these files: /usr/lib/modprobe.d/nvidia-installer-disable-nouveau.conf, /etc/modprobe.d/nvidia-installer-disable-nouveau.conf</code></pre><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230524142016185.png" alt="image-20230524142016185" /></p><p>按照以下方式禁用该驱动：</p><pre><code class="language-shell">#新建一个配置文件sudo vim /etc/modprobe.d/blacklist-nouveau.conf#写入以下内容blacklist nouveauoptions nouveau modeset=0#保存并退出:wq#备份当前的镜像mv /boot/initramfs-$(uname -r).img /boot/initramfs-$(uname -r).img.bak#建立新的镜像sudo dracut /boot/initramfs-$(uname -r).img $(uname -r)#重启sudo reboot#最后输入上面的命令验证lsmod | grep nouveau</code></pre><h2 id="%E4%B8%89-%E6%A3%80%E6%9F%A5-kernels-%E7%89%88%E6%9C%AC%E6%98%AF%E5%90%A6%E5%8C%B9%E9%85%8D" tabindex="-1">三 检查 kernels 版本是否匹配</h2><p>如果不检查kernels版本是否匹配，会报错：</p><pre><code class="language-shell">ERROR: Unable to find the kernel source tree for the currently running kernel.  Please make sure you have installed the kernel source files for your kernel and that they are properly configured; on Red Hat Linux systems, for example, be sure you have the &#39;kernel-source&#39; or &#39;kernel-devel&#39; RPM installed.  If you know the correct kernel source files are installed, you may specify the kernel source path with the &#39;--kernel-source-path&#39; command line option.</code></pre><p>先进入相应目录查看是否有相应内核：</p><p>cd /usr/src/kernels/  &amp;&amp; ls -al</p><p>如果为空表示没有，需要自己下载：</p><p>不要直接 yum install kernel-devel</p><p>因为这样下载下来的内核版本不一定匹配，所以实际的下载命令应该为：</p><pre><code class="language-shell">yum install -y kernel-devel-$(uname -r) kernel-headers-$(uname -r) </code></pre><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230524154237782.png" alt="image-20230524154237782" /></p><p>如果发现还是报错，cd /lib/modules/$(uname -r) 进入到该目录执行ll</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230524160341190.png" alt="image-20230524160341190" /></p><p>如果发现build所指向的目录为空，说明内核没有安装正确，要么修改build的软链接，要么重新安装，如果修改build的软链接到另一个与内核不匹配的kernels目录，可能会报形如这个<a href="https://forums.developer.nvidia.com/t/run-file-fails-after-compilation-with-unable-to-determine-if-secure-boot-is-enabled-no-such-file/36232" target="_blank">链接</a>中所提到的错误.</p><h2 id="%E5%9B%9B-%E5%AE%89%E8%A3%85%E5%BF%85%E8%A6%81%E4%BE%9D%E8%B5%96" tabindex="-1">四 安装必要依赖</h2><p>安装 gcc g++ make 等常用工具，已有忽略</p><pre><code class="language-shell">yum -y install gccyum -y install gcc-c++yum -y install make</code></pre><h2 id="%E4%BA%94-%E5%AE%89%E8%A3%85%E9%A9%B1%E5%8A%A8" tabindex="-1">五 安装驱动</h2><pre><code class="language-shell">init 3 # 关闭图形化界面wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.runsh cuda_11.8.0_520.61.05_linux.run</code></pre><p>等待一小会儿，进入后，填写accept选择我接受</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230524163736061.png" alt="image-20230524163736061" /></p><p>然后直接install</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230524141845041.png" alt="image-20230524141845041" /></p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230524161903751.png" alt="image-20230524161903751" /></p><p>出现以上图示即为安装成功</p><h2 id="%E5%85%AD-%E5%A3%B0%E6%98%8E%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F" tabindex="-1">六 声明环境变量</h2><pre><code class="language-shell">export PATH=/usr/local/cuda-11.8/bin:$PATHexport LD_LIBRARY_PATH=/usr/local/cuda-11.8/lib64:$LD_LIBRARY_PATH</code></pre><p>然后执行nvcc -V，如果有回显，说明cuda也没问题了</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230524162208519.png" alt="image-20230524162208519" /></p>]]>
                    </description>
                    <pubDate>Wed, 07 Jun 2023 10:37:33 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Linux 下非 root 安装 MySQL 8.0.12 并使用 Shell 操作数据库]]>
                    </title>
                    <link>https://blog.qiql.net/archives/linux下非root安装mysql8012并使用shell操作数据库</link>
                    <description>
                            <![CDATA[<h2 id="%E9%9B%B6-%E6%A6%82%E8%BF%B0" tabindex="-1">零 概述</h2><p>本文将介绍如何以非 root 身份安装运行 MySQL 8.0.12，并使用 Shell 命令操作数据库及将 txt 文件中的数据导入到 MySQL 表中</p><h2 id="%E4%B8%80-%E5%AE%89%E8%A3%85%E5%8C%85%E5%89%8D%E5%87%86%E5%A4%87" tabindex="-1">一 安装包前准备</h2><pre><code class="language-shell"># 获取安装包wget https://mirrors.qiql.net/pkgs/mysql-8.0.12-linux-glibc2.12-x86_64.tar.xz# 解压tar -xvf mysql-8.0.12-linux-glibc2.12-x86_64.tar.xz# 重命名【可选】mv mysql-8.0.12-linux-glibc2.12-x86_64/ mysql-8.0.12# 进入MySQL目录并创建data目录用于存放数据文件cd mysql-8.0.12 &amp;&amp; mkdir data</code></pre><p>在mysql-8.0.12目录下创建配置文件 <code>my.conf</code> 填写以下内容：</p><pre><code class="language-shell">[mysqld]port=3336basedir=/to/your/path/mysql-8.0.12datadir=/to/your/path/mysql-8.0.12/datapid-file=/to/your/path/mysql-8.0.12/mysql.pidsocket=/to/your/path/mysql-8.0.12/mysql.socklog_error=/to/your/path/mysql-8.0.12/error.logserver-id=100secure_file_priv=/to/your/path/file  # 此字段定义的是允许mysql从何目录读取数据文件【可选】</code></pre><h2 id="%E4%BA%8C-%E5%AE%89%E8%A3%85mysql" tabindex="-1">二 安装MySQL</h2><ol><li><p>进入MySQL目录：</p><pre><code class="language-shell">cd /to/your/path/mysql-8.0.12</code></pre></li><li><p>初始化MySQL：</p><pre><code class="language-shell">bin/mysqld \--defaults-file=/to/your/path/mysql-8.0.12/my.conf \--initialize \--user=user \--basedir=/to/your/path/mysql-8.0.12\--datadir=/to/your/path/mysql-8.0.12/data</code></pre></li><li><p>启动MySQL：</p><pre><code class="language-shell">bin/mysqld_safe \--defaults-file=/to/your/path/mysql-8.0.12/my.conf \--user=user &amp;</code></pre></li><li><p>查看MySQL数据库root账号的初始密码：</p><pre><code class="language-shell">cat error.log | grep root@localhost</code></pre></li><li><p>登录Mysql：</p><pre><code class="language-shell">bin/mysql -u root -p -S /to/your/path/mysql-8.0.12/mysql.sock</code></pre></li><li><p>修改密码：</p><pre><code class="language-shell">alter user &#39;root&#39;@&#39;localhost&#39; identified with mysql_native_password by &#39;123456&#39;;#建议添加with mysql_native_password参数，此选项可以允许使用shell进行MySQL数据库操作</code></pre></li><li><p>刷新权限：</p><pre><code class="language-shell">flush privileges;</code></pre></li><li><p>查看 MySQL 端口：</p><pre><code class="language-shell">show global variables like &#39;port&#39;;</code></pre></li><li><p>查询外部文件允许导入目录：</p><pre><code class="language-shell">show variables like “secure_file_priv”;</code></pre></li></ol><h2 id="%E4%B8%89-shell-%E5%91%BD%E4%BB%A4%E6%93%8D%E4%BD%9C%E6%95%B0%E6%8D%AE%E5%BA%93" tabindex="-1">三 Shell 命令操作数据库</h2><ol><li><p>通过 Shell 将 txt 文件的数据导入数据库：</p><pre><code class="language-shell">cd /to/your/path/mysql-8.0.12./bin/mysqlimport --fields-terminated-by=&#39;,&#39; --lines-terminated-by=&#39;\n&#39; --ignore-lines=1 --user=root --password=123456 数据库名 --local  /to/your/path/xxx.txt -S /to/your/path/mysql-8.0.12/mysql.sock# 在此命令中，以逗号为每一列的分隔符，以换行为每一行的分隔符，并忽略掉txt文本中的第一行。</code></pre></li><li><p>通过 Shell 执行 MySQL 命令（以清空 INFO 数据库的 studnet 表数据为例）：</p><pre><code class="language-shell">cd /to/your/path/mysql-8.0.12./bin/mysql -h 127.0.0.1 -P 3336 -u root -p&#39;123456&#39; --execute=&#39;use INFO;TRUNCATE TABLE student;&#39; --default-character-set=UTF8 -S /to/your/path/mysql-8.0.12/mysql.sock</code></pre></li></ol>]]>
                    </description>
                    <pubDate>Tue, 06 Jun 2023 23:25:53 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[通过 Nginx 的文件映射搭建镜像下载站并采用 cloudreve 进行维护]]>
                    </title>
                    <link>https://blog.qiql.net/archives/mirrors</link>
                    <description>
                            <![CDATA[<h2 id="%E9%9B%B6-%E6%A6%82%E8%BF%B0" tabindex="-1">零 概述</h2><p>本文要解决的痛点是：</p><ol><li>在不同的 Windows 电脑之间互传文件，只能通过 QQ，微信，网盘等工具</li><li>往 Linux 服务器上传输一个文件，只能通过 Xftp 等工具</li><li>在 Linux 服务器上下载某些软件的安装包的速度有时候很慢，有些常用的安装包源网站地址在境外，经常下载不下来</li><li>自己电脑太多，或者作品太多，有一些想共享的文件想长期放在某个网站上，供随时取用，不然每次都通过网盘分享很麻烦</li><li>想公网直接访问自己在 Linux 服务器上的文件，不想每次都通过Xftp</li></ol><p>以上问题，可以通过利用 Nginx 来进行文件映射搭建一个私人的文件下载站点来解决！</p><blockquote><p>我个人的镜像下载站为：<a href="https://mirrors.qiql.net" target="_blank">https://mirrors.qiql.net</a> 欢迎访问，但还请手下留情</p></blockquote><h2 id="%E4%B8%80-%E5%AE%89%E8%A3%85nginx" tabindex="-1">一 安装Nginx</h2><p>首先，推荐安装 <a href="https://github.com/aperezdc/ngx-fancyindex" target="_blank">fancyindex</a> 模块，使镜像站可以更加美观，且具有搜索功能</p><p>下载地址：<a href="https://mirrors.qiql.net/pkgs/ngx-fancyindex-0.5.2.tar.xz" target="_blank">https://mirrors.qiql.net/pkgs/ngx-fancyindex-0.5.2.tar.xz</a></p><p>解压命令：<code>tar -xvf ngx-fancyindex-0.5.2.tar.xz</code></p><p>如果没有安装过 Nginx，可以按照以下步骤进行安装：</p><pre><code class="language-shell"># 获取nginx安装包wget https://mirrors.qiql.net/pkgs/nginx-1.23.0.tar.gz# 解压tar -zxvf ../nginx-1.23.0.tar.gz cd nginx-1.23.0# 安装必要的依赖sudo yum -y install pcre-develsudo yum -y install openssl-devel# 进行配置，--prefix 是指定安装路径，--with-http_ssl_module 是使得 Nginx 支持 https； --add-module 是给 nginx 添加支持 fancy 模块./configure --prefix=/usr/local/nginx --with-http_ssl_module  --add-module=/path/to/ngx-fancyindex-0.5.1# 编译 Nginx ，-j 默认使用机器上所有的核心，-j 2 可以只指定两个核心make -j# 进行安装，将编译得到的二进制文件和配置文件等拷贝到之前由--prefix 参数所指定的目录中去make install# 切换到 Nginx 的安装目录cd /usr/local/nginx # 启动 Nginxsudo  ./sbin/nginx</code></pre><p>如果之前安装过 Nginx，进入到 Nginx 的源码目录，再按照以上配置重新 configure 即可，新编译得到的 nginx 二进制文件会覆盖掉原来的二进制可执行文件</p><p>安装了 <a href="https://github.com/aperezdc/ngx-fancyindex" target="_blank">fancyindex</a> 模块之后，还需要下载一个主题：<a href="https://github.com/Naereen/Nginx-Fancyindex-Theme" target="_blank">https://github.com/Naereen/Nginx-Fancyindex-Theme</a><br />然后在镜像站根目录下新建一个style目录，将主题解压至style目录。</p><p>在 nginx.conf 文件中进行如下配置：</p><pre><code class="language-shell">    server {        listen       80;        server_name  mirrors.example.com;        charset utf-8;        access_log  logs/host.https.mirrors.qiql.net.access.log  main;        location / {            # root html;            fancyindex on;            root /opt/mirrors;   # 定义镜像下载站的根目录            fancyindex_exact_size off; # 不显示精确大小            fancyindex_localtime on;            # 页面头 如下路径相对于网站根（/）而言的相对路径            fancyindex_header /style/Nginx-Fancyindex-Theme-light/header.html;            # 页尾            fancyindex_footer /style/Nginx-Fancyindex-Theme-light/footer.html;            charset utf-8;    #解决中文显示乱码问题            # 忽略的文件夹/文件            fancyindex_ignore &quot;style&quot;;            # fancyindex_ignore &quot;README.md&quot;;        }        error_page   500 502 503 504  /50x.html;        location = /50x.html {            root   html;        }     }</code></pre><p>修改完毕后，重载 nginx.conf 配置文件：<code>./nginx -s reload</code></p><h2 id="%E4%BA%8C-%E7%AE%A1%E7%90%86%E9%95%9C%E5%83%8F%E7%AB%99" tabindex="-1">二 管理镜像站</h2><p>通过以上步骤，一个镜像下载站点就已经建成了，但是维护稍显复杂，因为要把文件传输到指定的目录下才可以，如果使用 ftp 进行传输会很麻烦，本小节将介绍如何通过云盘服务将文件直接通过 Web 端传输到 Linux 服务器指定的目录下</p><h3 id="2.1-%E5%AE%89%E8%A3%85-cloudreve" tabindex="-1">2.1 安装 cloudreve</h3><p>首先，需要安装 cloudreve，这是一个十分强大且开源的网盘应用程序。建议遵循官方文档进行安装，安装很简单：</p><p>cloudreve 官网：<a href="https://cloudreve.org/" target="_blank">https://cloudreve.org/</a></p><p>cloudreve 搭建：<a href="https://docs.cloudreve.org/" target="_blank">https://docs.cloudreve.org/</a></p><p>安装包下载：<a href="https://mirrors.qiql.net/pkgs/cloudreve_3.5.3_linux_amd64.tar.gz" target="_blank">https://mirrors.qiql.net/pkgs/cloudreve_3.5.3_linux_amd64.tar.gz</a></p><h3 id="2.2-%E9%85%8D%E7%BD%AE%E7%94%A8%E6%88%B7%E7%AD%96%E7%95%A5" tabindex="-1">2.2 配置用户策略</h3><p>以管理员身份登陆到网盘后台，点击右上角的头像 &gt; 管理面板 &gt; 存储策略 &gt; 添加存储策略 &gt; 选择本机存储</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230605225934756.png" alt="image-20230605225934756" /></p><p>存储目录这里填写被映射出去的目录的绝对路径</p><p>注意：重命名这里一定要选择<strong>不开启</strong>重命名</p><p>然后后面就都保持默认即可，也可以自行根据具体情况进行修改</p><h3 id="2.3-%E9%85%8D%E7%BD%AE%E7%94%A8%E6%88%B7%E7%BB%84%E5%92%8C%E7%94%A8%E6%88%B7" tabindex="-1">2.3 配置用户组和用户</h3><p>新建用户组：以管理员身份登陆到网盘后台，点击右上角的头像 &gt; 管理面板 &gt; 新建用户组 &gt; 给用户组起名 &gt; 存储策略选择刚刚创建的存储策略 &gt; 选择创始容量，保存即可</p><p>新建用户：以管理员身份登陆到网盘后台，点击右上角的头像 &gt; 管理面板 &gt; 新建用户。新建用户这里的邮箱可以是一个不存在的邮箱，因为它仅仅是一个账号。在用户组处选择刚刚创建的用户组，保存即可。</p><p>然后，退出网盘的管理员账号，以刚刚创建的用户账号登陆云盘，传一个示例文件到网盘中，再进入到镜像站，即可看到刚刚上传的文件，这样就实现了仅通过 Web 服务维护镜像站的效果。</p><p>但是，由于存储策略的唯一性，如果想在镜像站下设立多个文件夹，只能多创建几个存储策略，给不同的存储策略分配不同的用户组和用户。因为 cloudreve 实际上是通过索引而不是挂载来实现的，即使在网盘中新建了目录，也不会在 Linux 服务器上对应的存储目录去真的新建对应的目录。所以如果要维护多个目录，暂时只能通过多个存储策略来实现。</p><p>值得注意的是：cloudreve 免费版中，每个用户组只能绑定一个存储策略，但是捐赠版中一个用户组是可以绑定多个存储策略的，所以如果觉得使用多个用户维护不同目录很麻烦，可以购买捐赠版。</p>]]>
                    </description>
                    <pubDate>Mon, 05 Jun 2023 23:38:48 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Nginx + FRP反向代理实现二级域名访问内网服务并配置 https]]>
                    </title>
                    <link>https://blog.qiql.net/archives/nginxfrphttps</link>
                    <description>
                            <![CDATA[<h2 id="%E9%9B%B6-%E6%A6%82%E8%BF%B0" tabindex="-1">零 概述</h2><p>假如在内网中部署了一个服务，并想实现在公网访问，可以通过内网穿透，然后用IP+端口的形式来进行实现，但是暴露一个端口号既不安全也不美观，网上有一些教程是通过二级域名+端口或三级域名的方式实现的，但是也不美观，本文将介绍如何通过使用Nginx加FRP的方式，直接实现二级域名访问到内网服务。</p><p>实验前需要准备：</p><ul><li>一台公网云服务器，假设IP为：66.66.66.66，可以放开部分端口，如 80, 443, 7000，7001</li><li>一个顶级域名：<a href="http://example.com" target="_blank">example.com</a></li><li>A解析：<a href="http://a.example.com" target="_blank">a.example.com</a>，需解析指向到 66.66.66.66 这台公网服务器上</li><li>一台能联网的内网服务器，应用将部署在这台内网服务器上</li></ul><blockquote><p>最终实现的目标是，直接以 <a href="http://a.example.com" target="_blank">a.example.com</a> 访问到这台内网服务器的某个 web 服务，且可以配置 https 访问</p></blockquote><h2 id="%E4%B8%80-%E5%AE%89%E8%A3%85-nginx" tabindex="-1">一 安装 Nginx</h2><p>部署 FRP 之前，首先需要有一台公网云服务器，在公网云服务器上部署安装 Nginx</p><pre><code class="language-shell"># 获取nginx安装包wget https://mirrors.qiql.net/pkgs/nginx-1.23.0.tar.gz# 解压tar -zxvf ../nginx-1.23.0.tar.gz cd nginx-1.23.0# 安装必要的依赖sudo yum -y install pcre-develsudo yum -y install openssl-devel# 进行配置，--prefix是指定安装路径，--with-http_ssl_module 是使得Nginx支持https./configure --prefix=/usr/local/nginx --with-http_ssl_module# 编译Nginx -j 默认使用机器上所有的核心，-j 2 可以只指定两个核心make -j# 进行安装，将编译得到的二进制文件和配置文件等拷贝到之前由--prefix参数所指定的目录中去make install# 切换到Nginx的安装目录cd /usr/local/nginx # 启动Nginxsudo  ./sbin/nginx</code></pre><h2 id="%E4%BA%8C-%E5%AE%89%E8%A3%85-frp" tabindex="-1">二 安装 FRP</h2><p>FRP 的安装分为服务端和客户端两部分，需要先在公网服务器上安装好服务端，然后在内网服务器上安装客户端</p><blockquote><p>FRP官方文档：<a href="https://gofrp.org/docs/" target="_blank">https://gofrp.org/docs/</a></p><p>FRP历史各版本文档：<a href="https://www.bookstack.cn/read/frp/spilt.2.spilt.3.README_zh.md" target="_blank">https://www.bookstack.cn/read/frp/spilt.2.spilt.3.README_zh.md</a></p><p>（官方文档很简洁明了，但是少了很多对FRP功能的叙述，可以先按照官方文档进行基本的安装。配置文件热更新等高级功能建议参考历史版本文档）</p></blockquote><h3 id="2.1-%E6%9C%8D%E5%8A%A1%E7%AB%AF" tabindex="-1">2.1 服务端</h3><p>登陆到公网服务器上，安装FRP的服务端</p><pre><code class="language-shell"># 获取FRP安装包wget https://mirrors.qiql.net/pkgs/frp_0.48.0_linux_amd64.tar.gz# 解压tar -zxvf frp_0.48.0_linux_amd64.tar.gzcd frp-0.48.0</code></pre><p>FRP 并不需要编译，只需要修改配置文件即可。找到 frps.ini 文件，修改配置如下：</p><pre><code class="language-shell">[common] # 默认为 7000 端口，且这个 7000 端口需要在公网服务器的安全组中打开bind_port = 7000# frpc Client客户端连接Frps服务端时的token 为了安全 建议添加token = xxxxxx# web端管理控制面板相关配置【可选】dashboard_port = 7500dashboard_user = usernamedashboard_pwd = password# 需要穿透http的统一访问端口(http类型的内网穿透，必须设置vhost_http_port，并且所有的http类型的客户端都将通过同一个vhost_http_port访问)vhost_http_port = 7001# https的访问端口(如果需要的话)# vhost_https_port = 7443</code></pre><p>启动FRP</p><pre><code class="language-shell">./frps -c frps.ini</code></pre><h3 id="2.2-%E5%AE%A2%E6%88%B7%E7%AB%AF" tabindex="-1">2.2 客户端</h3><p>登陆到内网服务器上，安装 FRP 的客户端</p><pre><code class="language-shell"># 获取FRP安装包wget https://mirrors.qiql.net/pkgs/frp_0.48.0_linux_amd64.tar.gz# 解压tar -zxvf frp_0.48.0_linux_amd64.tar.gzcd frp-0.48.0</code></pre><p>修改frpc.ini文件</p><pre><code class="language-shell">[common]server_addr = 66.66.66.66 # 公网服务器的IP地址server_port = 7000 # frps.ini文件中填写的端口token = qiqlhkadmin # 填写与frps.ini文件中一样的tokenadmin_addr = 127.0.0.1 # 启用 admin 端口，用于提供 API 服务，可以用于客户端配置文件热加载admin_port = 7400 # adnin 具体端口# 如果想要直接 ssh 到内网服务器上，需进行以下配置[ssh]type = tcplocal_ip = 127.0.0.1local_port = 22remote_port = 6666# 进行以上配置后，即可通过 ssh -p 6666 root@66.66.66.66 命令登陆到内网服务器# 以下为 web 服务的配置方式[web01]type=httplocal_ip = 127.0.0.1 # 无需修改local_port = 1234     # 修改为 web 服务在本机启动的端口custom_domains = a.xxx.com # 修改为二级域名，该域名需要解析到公网服务器的IP</code></pre><p>启动FRP客户端</p><pre><code class="language-shell">./frpc -c ./frpc.ini</code></pre><p>如果后续有新的WEB网站代理需求，但重启FRP又很麻烦，可以执行以下命令来热更新frpc的配置文件</p><pre><code class="language-shell">./frpc reload -c ./frpc.ini</code></pre><h3 id="2.3-%E9%85%8D%E7%BD%AE%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B" tabindex="-1">2.3 配置守护进程</h3><p>以下为frps（服务端）的守护进程配置方式，客户端的守护进程配置方式与之相似。</p><p>要使用 <code>systemd</code> 来控制 <code>frps</code>，需要先安装 <code>systemd</code>，然后在 <code>/etc/systemd/system</code> 目录下创建一个 frps.service 文件。</p><ol><li><p>如Linux服务端上没有安装 <code>systemd</code>，可以使用 <code>yum</code> 或 <code>apt</code> 等命令安装 <code>systemd</code>。</p><pre><code class="language-bash"># yumyum install systemd# aptapt install systemd</code></pre></li><li><p>使用文本编辑器，如 <code>vim</code> 创建并编辑 <code>frps.service</code> 文件。</p><pre><code class="language-bash">$ vim /etc/systemd/system/frps.service</code></pre><p>写入内容</p><pre><code class="language-ini">[Unit]# 服务名称，可自定义Description = frp serverAfter = network.target syslog.targetWants = network.target[Service]Type = simple# 启动frps的命令，需修改为您的frps的安装路径ExecStart = /path/to/frps -c /path/to/frps.ini[Install]WantedBy = multi-user.target</code></pre></li><li><p>使用 <code>systemd</code> 命令，管理 frps。</p><pre><code class="language-bash"># 启动frpsystemctl start frps# 停止frpsystemctl stop frps# 重启frpsystemctl restart frps# 查看frp状态systemctl status frps</code></pre></li><li><p>配置 frps 开机自启。</p><pre><code class="language-bash">systemctl enable frps</code></pre></li></ol><h2 id="%E4%B8%89-%E9%85%8D%E7%BD%AEnginx%E4%BB%A3%E7%90%86" tabindex="-1">三 配置Nginx代理</h2><p>假设一个web服务启动在内网服务器的1234端口上，想要以a.xx.com来访问该服务，可以参考以下配置来修改nginx的nginx.conf文件</p><h3 id="3.1-http%E8%AE%BF%E9%97%AE" tabindex="-1">3.1 http访问</h3><pre><code class="language-shell">    server {        listen       80;        server_name  a.example.com;        location / {            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;            proxy_set_header Host $http_host;            proxy_redirect off;            proxy_pass http://127.0.0.1:7001; # 这里的7001端口填写之前安装FRP时配置的 vhost_http_port 参数        }        error_page  500 502 503 504  /50x.html;        location /50x.html {            root   /usr/local/nginx/html;        }    }</code></pre><p>修改完毕后，进入到nginx的sbin目录，执行<code>./nginx -s reload </code> 命令即可热更新nginx的配置文件</p><h3 id="3.2-https%E8%AE%BF%E9%97%AE" tabindex="-1">3.2 https访问</h3><p>一般配置https，都需要配置全站https，否则浏览器仍认为该站点是不完全安全的，这时需要在nginx的配置文件中添加一个转发，可以将所有http的请求都转发为https的请求：</p><pre><code class="language-shell">    server {        listen       80;        server_name  a.example.com;         rewrite ^(.*)$ https://$host$1 permanent;    }</code></pre><p>另外，如果需要配置https，还需要申请CA证书，前往阿里云控制台 &gt; 搜索数字证书管理服务（SSL证书）&gt; SSL证书&gt; 免费证书&gt; 创建证书即可完成免费CA证书的创建</p><p><img src="https://qiql-blog-imgs.oss-cn-shanghai.aliyuncs.com/image-20230604174754986.png" alt="image-20230604174754986" /></p><p>免费证书下发后，下载nginx版本的证书（应该是一个zip压缩包）</p><p>推荐在nginx的安装目录中新建一个cert目录，专门用于存放证书的pem和key文件</p><p>找到nginx的配置文件nginx.conf，添加以下内容：</p><pre><code class="language-shell">    server {        listen       443 ssl;        server_name  a.example.com;        charset utf-8;        access_log  logs/host.https.a.example.com.access.log  main; # 添加该网站访问日志        ssl_certificate     /usr/local/nginx/cert/a.example.com.pem;    # 修改为该网站 CA证书的 pem文件绝对路径        ssl_certificate_key /usr/local/nginx/cert/a.example.com.key; # 修改为该网站 CA证书的 key文件绝对路径        location / {            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;            proxy_set_header Host $http_host;            proxy_redirect off;            proxy_pass http://127.0.0.1:7001;        }        error_page   500 502 503 504  /50x.html;        location = /50x.html {            root   html;        }     }</code></pre><p>注意，由于该配置文件中，还启用了日志文件的main格式，需要将以下配置的注释取消掉：</p><pre><code class="language-shell">    log_format  main  &#39;$remote_addr - $remote_user [$time_local] &quot;$request&quot; &#39;                      &#39;$status $body_bytes_sent &quot;$http_referer&quot; &#39;                      &#39;&quot;$http_user_agent&quot; &quot;$http_x_forwarded_for&quot;&#39;;</code></pre><p>然后，重载nginx的配置文件即可： <code>./nginx -s reload </code></p><p>通过以上配置，便可以通过二级域名直接访问到内网服务器中的web服务，公网服务器只需要暴露四个端口（80，443，7000，7001）即可完成配置。</p>]]>
                    </description>
                    <pubDate>Sun, 04 Jun 2023 18:06:23 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Ubuntu20.04 安装 INTEL 编译器和 Gcc 编译器]]>
                    </title>
                    <link>https://blog.qiql.net/archives/intelgcc</link>
                    <description>
                            <![CDATA[<h2 id="0-%E5%89%8D%E8%A8%80" tabindex="-1">0 前言</h2><p>本文旨在 Ubuntu20.04 上安装最新的 Intel oneAPI 编译器，和 gcc5.4.0 版本的编译器（编译安装），并使用module 工具对编译器版本进行管理和切换</p><h2 id="1-%E5%AE%89%E8%A3%85-intel-%E7%BC%96%E8%AF%91%E5%99%A8" tabindex="-1">1 安装 Intel 编译器</h2><p>Intel oneAPI编译器下载：</p><p><a href="https://www.intel.com/content/www/us/en/developer/tools/oneapi/base-toolkit-download.html?operatingsystem=linux&amp;distributions=offline" target="_blank">Intel oneAPI Base Toolkit</a>：<a href="https://registrationcenter-download.intel.com/akdlm/irc_nas/18970/l_BaseKit_p_2022.3.1.17310_offline.sh" target="_blank">https://registrationcenter-download.intel.com/akdlm/irc_nas/18970/l_BaseKit_p_2022.3.1.17310_offline.sh</a></p><p><a href="https://www.intel.com/content/www/us/en/developer/tools/oneapi/hpc-toolkit-download.html?operatingsystem=linux&amp;distributions=offline" target="_blank">Intel oneAPI HPC Toolkit</a>： <a href="https://registrationcenter-download.intel.com/akdlm/irc_nas/18975/l_HPCKit_p_2022.3.1.16997_offline.sh" target="_blank">https://registrationcenter-download.intel.com/akdlm/irc_nas/18975/l_HPCKit_p_2022.3.1.16997_offline.sh</a></p><p>将这两个安装包下载好然后放到机器上任意目录</p><blockquote><p>先安装Base Toolkit，再安装 HPC Toolkit</p></blockquote><p>建议全程使用使用命令行安装，可以提高一点安装速度（终端执行 init 3 关闭图形化界面）</p><h3 id="1.1-%E5%AE%89%E8%A3%85-base-toolkit" tabindex="-1">1.1 安装 Base Toolkit</h3><p>执行 <code>sh l_BaseKit_p_2022.3.1.17310_offline.sh</code> 开始安装Base Toolkit</p><p>进入安装页面后，通过上下键选择 <code>Accept &amp; customer</code>（ 接受并自定义安装）</p><p><img src="/upload/2022/11/image-1668869148547.png" alt="image-1668869148547" /></p><p>然后进入以下页面，选择Next</p><p><img src="/upload/2022/11/image-1668869157982.png" alt="image-1668869157982" /></p><p>然后就进入到选择安装目录的页面，按住Ctrl键再按退格才可以删除，默认是 /opt/intel/oneapi目录，但如果后面会装多个Intel编译器的话，还是应该规范一下安装路径。</p><p><strong>注意：安装路径在指定时，最后一级目录必须是oneapi，不能修改，否则后面在使用时可能会报错</strong></p><p>指定完路径后选中Next</p><p><img src="/upload/2022/11/image-1668869174110.png" alt="image-1668869174110" /></p><p>然后进入以下页面：<br /><img src="/upload/2022/11/image-1668869183147.png" alt="image-1668869183147" /></p><p>点击Install</p><p><img src="/upload/2022/11/image-1668869190584.png" alt="image-1668869190584" /></p><p>选择 skip，然后选中 Next<br /><img src="/upload/2022/11/image-1668869198884.png" alt="image-1668869198884" /></p><p>这里是用户体验计划，选择是否参加，我一般选择不参加，然后选中Begin Installation开始安装</p><p><img src="/upload/2022/11/image-1668869206042.png" alt="image-1668869206042" /></p><p>这个安装过程大概要十来分钟，根据硬件性能的不同安装速度也会有些许不同</p><p>安装完毕：</p><p><img src="/upload/2022/11/image-1668869215835.png" alt="image-1668869215835" /></p><p><img src="/upload/2022/11/image-1668869222846.png" alt="image-1668869222846" /></p><p>点击close关闭页面，Base Toolkit 安装完毕</p><h3 id="1.2-%E5%AE%89%E8%A3%85-hpc-toolkit" tabindex="-1">1.2 安装 HPC ToolKit</h3><p>然后执行 sh l_HPCKit_p_2022.3.1.16997_offline.sh 安装 HPC ToolKit，大致过程与 Base toolkit相同，</p><p><img src="/upload/2022/11/image-1668869233129.png" alt="image-1668869233129" /></p><p><img src="/upload/2022/11/image-1668869240933.png" alt="image-1668869240933" /></p><p><img src="/upload/2022/11/image-1668869255130.png" alt="image-1668869255130" /></p><p><img src="/upload/2022/11/image-1668869313660.png" alt="image-1668869313660" /></p><p><img src="/upload/2022/11/image-1668869485190.png" alt="image-1668869485190" /></p><p><img src="/upload/2022/11/image-1668869419678.png" alt="image-1668869419678" /></p><p>相比于Base Toolkit，HPC Toolkit的安装要快很多</p><p><img src="/upload/2022/11/image-1668869554646.png" alt="image-1668869554646" /></p><p><img src="/upload/2022/11/image-1668869560895.png" alt="image-1668869560895" /></p><p>安装完毕后，使用 source /public/compiler/intel/2022u2/oneapi/setvars.sh intel64 命令来启用 intel编译器</p><p><img src="/upload/2022/11/image-1668869570948.png" alt="image-1668869570948" /></p><h2 id="2-%E5%AE%89%E8%A3%85gcc5.4%E7%BC%96%E8%AF%91%E5%99%A8" tabindex="-1">2 安装gcc5.4编译器</h2><p>方法一：参考：<a href="https://www.jianshu.com/p/e823f89787d6" target="_blank">https://www.jianshu.com/p/e823f89787d6</a></p><p>方法二：</p><p>下载gcc5.4的安装脚本：<code>wget https://mirrors.qiql.net/script/install_gcc5.4.0.sh</code></p><p>然后执行 <code>sh install_gcc5.4.0.sh</code> 执行该脚本即可自动开始编译安装，默认安装位置在 <code>/public/compiler/gcc/5.4.0</code></p><p>注意：编译安装对CPU和内存消耗较大，且编译安装时间较长，需耐心等待，但比直接yum下载的性能会更好</p><p>如果报错：g++: fatal error: Killed signal terminated program cc1plus ，说明内存不够，无法编译</p><h2 id="3-%E5%AE%89%E8%A3%85module" tabindex="-1">3 安装module</h2><p>module是一个用来管理环境变量的工具，通过编写modulefile，可以轻松的对不同软件或者同一个软件的不同版本进行切换。</p><p>下载module的安装脚本：</p><p><code>wget https://mirrors.qiql.net/script/install_module5.0.1.sh</code></p><p>使用sh执行该脚本即可，默认安装位置为<code>/public/tool/module/5.4.0</code></p><p>然后新建一个modulefiles目录专门用来访modulefile文件，可以将该目录新建到 /public/modulefiles 目录下：</p><p><code>mkdir -p /public/modulefiles</code></p><p>安装完毕后，将</p><pre><code class="language-shell">source /public/tool/module/5.4.0/init/profile.shexport MODULEPATH=/public/modulefiles</code></pre><p>写入到 <code>~/.bashrc</code> 文件中去</p><p>然后执行 <code>source ~/.bashrc</code> 以使配置文件生效</p><p>然后输入 <code>which module</code>，如果输出了路径，说明没啥问题了</p><h2 id="4-%E7%BC%96%E5%86%99-modulefile" tabindex="-1">4 编写 modulefile</h2><h3 id="4.1-intel-oneapi" tabindex="-1">4.1 Intel oneAPI</h3><p>先编写 Intel oneAPI 的 modulefile</p><p>新开一个会话，然后新建一个文本文件，比如source_intel2022u2.of，里面输入以下内容：</p><pre><code class="language-shell">source /public/compiler/intel/2022u2/oneapi/setvars.sh intel64</code></pre><p>下载env2工具，该工具可以自动编写复杂环境的modulefile</p><p><code>wget https://mirrors.qiql.net/pkgs/env2</code></p><p>下载好后，为其赋予可执行权限： chmod +x env2</p><p>然后执行：./env2 -from bash -to modulecmd source_intel2022u2.of</p><p>此时终端会输出一段内容，将其复制下来</p><p>进入到modulefiles的目录：cd /public/modulefiles</p><p>新建一个Intel目录并进入： mkdir -p intel &amp;&amp; cd intel</p><p>新建一个文本，文件名为2022u2，第一行写：#%Module</p><p>然后敲回车到第二行，将刚才复制的内容粘贴进去</p><p>然后保存退出即可</p><p>此时，在终端中执行 module load Intel/20022u2 就可以直接加载Intel编译器了，如果想卸载这个编译器，执行：module unload intel/2022u2</p><h3 id="4.2-gcc-5.4.0" tabindex="-1">4.2 gcc 5.4.0</h3><p>现在开始编写gcc5.4.0的modulefile</p><p>进入到modulefiles目录：cd /public/modulefiles</p><p>新建一个gcc目录并进入： mkdir -p gcc &amp;&amp; cd gcc</p><p>新建一个文本，文件名为5.4.0，第一行写：#%Module</p><p>然后敲回车到第二行，将以下内容粘贴进去：</p><pre><code class="language-shell">#%Module### gcc11.2.0#proc ModulesHelp { } {  puts stderr &quot;\tThis module adds gcc 5.4.0 compiler to the user environment&quot;}  set gccbasedir /public/compiler/gcc/5.4.0  set basedir /public/library/gcc/5.4.0    prepend-path PATH ${gccbasedir}/bin  prepend-path PATH ${basedir}/m4/1.4.19/bin  prepend-path LD_LIBRARY_PATH ${gccbasedir}/lib  prepend-path LD_LIBRARY_PATH ${gccbasedir}/lib64  prepend-path LD_LIBRARY_PATH ${basedir}/mpc/1.2.1/lib  prepend-path LD_LIBRARY_PATH ${basedir}/m4/1.4.19/lib  prepend-path LD_LIBRARY_PATH ${basedir}/mpfr/4.1.0/lib  prepend-path LD_LIBRARY_PATH ${basedir}/gmp/6.2.1/lib  prepend-path LD_LIBRARY_PATH ${basedir}/isl/0.24/libunset basedir</code></pre><p>然后保存退出即可</p><p>此时，在终端中执行 module load gcc/5.4.0 就可以直接加载gcc5.4.0编译器了，如果想卸载这个编译器，执行：module unload gcc/5.4.0 即可</p>]]>
                    </description>
                    <pubDate>Sat, 19 Nov 2022 22:41:42 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[Linux 平台 iozone 的安装及调试]]>
                    </title>
                    <link>https://blog.qiql.net/archives/iozone</link>
                    <description>
                            <![CDATA[<blockquote><p>iozone 官网：<a href="https://www.iozone.org/" target="_blank">https://www.iozone.org/</a></p></blockquote><h2 id="0-io-%E6%B5%8B%E8%AF%95%E5%AE%9A%E4%B9%89" tabindex="-1">0 IO 测试定义</h2><h3 id="write" tabindex="-1">Write</h3><p>测试向一个新文件写入的性能，当一个新文件被写入时，不仅仅是那些文件中的数据需要被存储，还包括那些用于定位数据存储在存储介质的具体位置的额外信息。这些额外信息被称作“元数据”。它包括目录信息，所分配的空间和一些与该文件有关但又并非该文件所含数据的其他数据。这些额外信息会导致Write的性能通常会比Re-write的性能低。</p><h3 id="re-write" tabindex="-1">Re-write</h3><p>测试向一个已存在的文件写入的性能。当一个已存在的文件被写入时，所需工作量较少，因为此时元数据已经存在。Re-write的性能通常比Write的性能高。</p><h3 id="read" tabindex="-1">Read</h3><p>测试读一个已存在的文件的性能。</p><h3 id="re-read" tabindex="-1">Re-Read</h3><p>测试读一个最近读过的文件的性能。Re-Read性能会高些，因为操作系统通常会缓存最近读过的文件数据。这个缓存可以被用于读以提高性能。</p><h3 id="random-read" tabindex="-1">Random Read</h3><p>测试读一个文件中的随机偏移量的性能。许多因素可能影响这种情况下的系统性能，例如：操作系统缓存的大小，磁盘数量，寻道延迟和其他。</p><h3 id="random-write" tabindex="-1">Random Write</h3><p>测试写一个文件中的随机偏移量的性能。同样，许多因素可能影响这种情况下的系统性能，例如：操作系统缓存的大小，磁盘数量，寻道延迟和其他。</p><h3 id="random-mix" tabindex="-1">Random Mix</h3><p>测试读写一个文件中的随机偏移量的性能。同样，许多因素可能影响这种情况下的系统性能，例如：操作系统缓存的大小，磁盘数量，寻道延迟和其他。这个测试只有在吞吐量测试模式下才能进行。每个线程/进程运行读或写测试。这种分布式读/写测试是基于round robin 模式的。最好使用多于一个线程/进程执行此测试。</p><h3 id="backwards-read" tabindex="-1">Backwards Read</h3><p>测试使用倒序读一个文件的性能。这种读文件方法可能看起来很可笑，事实上，有些应用确实这么干。MSC Nastran是一个使用倒序读文件的应用程序的一个例子。它所读的文件都十分大（大小从G级别到T级别）。尽管许多操作系统使用一些特殊实现来优化顺序读文件的速度，很少有操作系统注意到并增强倒序读文件的性能。</p><h3 id="record-rewrite" tabindex="-1">Record Rewrite</h3><p>测试写与覆盖写一个文件中的特定块的性能。这个块可能会发生一些很有趣的事。如果这个块足够小（比CPU数据缓存小），测出来的性能将会非常高。如果比CPU数据缓存大而比TLB小，测出来的是另一个阶段的性能。如果比此二者都大，但比操作系统缓存小，得到的性能又是一个阶段。若大到超过操作系统缓存，又是另一番结果。</p><h3 id="strided-read" tabindex="-1">Strided Read</h3><p>测试跳跃读一个文件的性能。举例如下：在0偏移量处读4Kbytes，然后间隔200Kbytes,读4Kbytes，再间隔200Kbytes，如此反复。此时的模式是读4Kbytes，间隔200Kbytes并重复这个模式。这又是一个典型的应用行为，文件中使用了数据结构并且访问这个数据结构的特定区域的应用程序常常这样做。许多操作系统并没注意到这种行为或者针对这种类型的访问做一些优化。同样，这种访问行为也可能导致一些有趣的性能异常。一个例子是在一个数据片化的文件系统里，应用程序的跳跃导致某一个特定的磁盘成为性能瓶颈。</p><h3 id="fwrite" tabindex="-1">Fwrite</h3><p>测试调用库函数fwrite()来写文件的性能。这是一个执行缓存与阻塞写操作的库例程。缓存在用户空间之内。如果一个应用程序想要写很小的传输块，fwrite()函数中的缓存与阻塞I/O功能能通过减少实际操作系统调用并在操作系统调用时增加传输块的大小来增强应用程序的性能。</p><h3 id="fread" tabindex="-1">Fread</h3><p>测试调用库函数fread()来读文件的性能。这是一个执行缓存与阻塞读操作的库例程。缓存在用户空间之内。如果一个应用程序想要读很小的传输块，fwrite()函数中的缓存与阻塞I/O功能能通过减少实际操作系统调用并在操作系统调用时增加传输块的大小来增强应用程序的性能。</p><h3 id="freread" tabindex="-1">Freread</h3><p>这个测试与上面的fread 类似，除了在这个测试中被读文件是最近才刚被读过。这将导致更高的性能，因为操作系统缓存了文件数据。</p><h3 id="mmap" tabindex="-1">Mmap</h3><p>许多操作系统支持mmap()的使用来映射一个文件到用户地址空间。映射之后,对内存的读写将同步到文件中去。这对一些希望将文件当作内存块来使用的应用程序来说很方便。一个例子是内存中的一块将同时作为一个文件保存在于文件系统中。</p><p>mmap文件的语义和普通文件略有不同。如果发生了对内存的存储，并不是立即发生相应的文件I/O操作。使用MS_SYNC和MS_ASYNC标志位的 msyc()函数调用将控制内存和文件的一致性。调用msync() 时将MS_SYNC置位将强制把内存里的内容写到文件中去并等待直到此操作完成才返回。而MS_ASYNC 置位则告诉操作系统使用异步机制将内存刷新到磁盘，这样应用程序可以直接返回而不用等待此操作的完成。</p><h3 id="async-i%2Fo" tabindex="-1">Async I/O</h3><p>许多操作系统支持的另外一种I/O机制是POSIX 标准的异步I/O。本程序使用POSIX标准异步I/O接口来完成此测试功能。例如：aio_write(), aio_read(), aio_error()。这个测试测量POSIX异步I/O机制的性能。</p><h2 id="1-linux%E4%B8%8B%E7%9A%84%E5%AE%89%E8%A3%85" tabindex="-1">1 Linux下的安装</h2><p>下载tgz安装包，下载之后，解压进入src目录，一直进入到有Makefile文件的目录，然后执行make Linux命令即可</p><p>编译得到iozone这个可执行文件之后即可，执行就用./iozone即可，可以将这个可执行文件移动至任何位置。</p><h2 id="2-%E5%91%BD%E4%BB%A4%E8%A1%8C%E9%80%89%E9%A1%B9%E5%90%AB%E4%B9%89%EF%BC%9A" tabindex="-1">2 命令行选项含义：</h2><p>-i<br />指定运行于哪种模式测试。可以使用 <code>-i 0 -i 1</code> 进行多个测试，具体参数含义如下：</p><pre><code class="language-properties">0=write/rewrite1=read/re-read2=random read/random write3=backwards read4=re-write-record5=stride-read6=fwirte/re-fwrite7=fread/re-fread8=random mix9=pwrite/re-pwrite10=pread/re-pread11=pwritev/re-pwritev12=preadv/re-preadv</code></pre><p>-t 使用throughput模式(多线程),并指定线程或进程数</p><p>-r 设置记录块大小</p><p>-s 设置测试文件大小(<strong>建议是内存的2倍</strong>)</p><p>-a 自动模式(花费很长时间,注意),测试记录块大小从4k到16M,测试文件从64k到512M.文件大于32MB时停止使用低于64k以下记录块以节省时间</p><p>-y 设置自动模式下的记录块最小值,-q设置自动模式下的记录块最大值</p><p>-n 设置自动模式下的文件最小值,-g设置自动模式下的文件最大值</p><p>-B 使用mmap()接口来创建并访问测试文件.即以读写内存的方式访问那个块来完成文件I/O</p><p>-i 指定运行于哪种测试模式.可以使用-i 0 -i 1 -i 2进行多个测试</p><p>-I 对所有文件操作使用DIRECT I/O.通知文件系统所有操作跳过缓存直接在磁盘上操作</p><p>-R 将结果导出到Excel文件</p><p>-b 指定导出结果的Excel文件名</p><p>-f 指定测试的文件名</p><p>-F 进程throughput模式(多线程)测试时的文件名,请注意,有多少个线程,后面就应该跟多个个文件名</p><p>-c 计算时间时包含close()参数,<strong>对于NFS3分区将非常有用</strong></p><p>-G 对mmap文件使用msync(MS_SYNC)。告诉操作系统在mmap空间的所有数据需要被<strong>同步</strong>的写到磁盘上</p><p>-D 对mmap文件使用MSYNC(MS_ASYNC)。告诉操作系统在mmap空间的所有数据需要被<strong>异步</strong>的写到磁盘上</p><p>-o 写操作都同步写入到磁盘中,即强制写入完成以后再返回benchmark</p><p>-I 对所有文件操作使用DIRECT I/O,通知文件系统所有操作跳过缓存直接在磁盘上操作</p><p>-l 设置最小进程数.在测试过程允许用户设置的最小进程或线程数.需要配合-u选项使用</p><p>-u 设置最大进程或线程数,需要配合-l参数使用</p><h3 id="3-%E5%8D%95%E8%8A%82%E7%82%B9%E6%B5%8B%E8%AF%95" tabindex="-1">3 单节点测试</h3><p>测试开始前，可以先执行 <code>./iozone -a</code> 先进行自动化测试</p><p>假如测试节点的内存为128g，那么命令可以写为：</p><pre><code class="language-shell">iozone -s 256g -t 1 -r 1M -i 0 -i 1 -+n</code></pre><p>如有必要，可以将结果写入Excel表格中，添加 -R  &lt;filename&gt; 参数即可</p><p>-+n 选项可以避免重复性的测试，以节约时间</p><h3 id="4-%E5%8D%95%E8%8A%82%E7%82%B9%E5%A4%9A%E6%B5%81%E6%B5%8B%E8%AF%95" tabindex="-1">4 单节点多流测试</h3><pre><code class="language-shell">iozone -s 256g -t 10 -r 1M -i 0 -i 1 -+n</code></pre><h2 id="5-%E5%A4%9A%E8%8A%82%E7%82%B9%E6%B5%8B%E8%AF%95" tabindex="-1">5 多节点测试</h2><p>参考：<a href="https://blog.51cto.com/u_286820/1428705" target="_blank">https://blog.51cto.com/u_286820/1428705</a></p><h3 id="5.1-%E8%BF%9C%E7%A8%8B%E8%AE%BF%E9%97%AE%E9%85%8D%E7%BD%AE" tabindex="-1">5.1 远程访问配置</h3><p>iozone使用rsh进行连接，现在已经很少使用rsh了，可以配置ssh进行连接：</p><pre><code class="language-shell">export RSH=sshexport rsh=ssh </code></pre><p>注：需要在所有的主机上进行设置，由于是临时设置，每次logout后都需要再次进行设置，如不执行上述两条命令，那么将会报错说找不到 rsh 命令</p><h3 id="5.2-%E8%AE%BE%E7%BD%AE%E4%B8%BB%E6%9C%BA%E5%90%8D" tabindex="-1">5.2 设置主机名</h3><p>设置/etc/sysconfig/network(后面的nodelist文件文档上说可以使用IP或者节点名，经过测试发现使用IP进行连接，iozone不能运行，所以需要设置主机名vi /etc/sysconfig/network 分别在所有的主机上设置hostname值，我分别设置为node1、node2 … noden</p><p>或者通过：<code>hostnamectl set-hostname xxx</code> 命令来设置主机名</p><h3 id="5.3-%E5%9C%A8%E9%80%89%E5%AE%9A%E7%9A%84%E4%B8%BB%E8%8A%82%E7%82%B9%E4%B8%8A%E8%AE%BE%E7%BD%AEhosts" tabindex="-1">5.3 在选定的主节点上设置hosts</h3><p><code>vim /etc/hosts</code></p><p>设置值如下：</p><pre><code class="language-shell"> xxx.xxx.xxx.x node1 xxx.xxx.xxx.x node2 ... xxx.xxx.xxx.x noden </code></pre><h3 id="5.4-%E8%AE%BE%E7%BD%AEssh%E6%97%A0%E5%AF%86%E7%A0%81%E8%AE%BF%E9%97%AE" tabindex="-1">5.4 设置ssh无密码访问</h3><p>ssh-keygen -t rsa ssh-copy-id -i .ssh/id_rsa.pub 用户名@目标机器名 注：要在所有的节点上运行，并且把秘钥拷贝到所有其他的节点上，此步骤需要特别注意，如果节点多了，很容易搞混，最好写脚本运行；（如果主节点同时也作为运行节点，需要在主节点上运行：ssh-copy-id -i .ssh/id_rsa.pub 用户名@自己）</p><h3 id="5.5-%E6%8B%B7%E8%B4%9D%E4%B8%BB%E8%8A%82%E7%82%B9%E4%B8%8A%E7%9A%84hosts%E6%96%87%E4%BB%B6" tabindex="-1">5.5 拷贝主节点上的hosts文件</h3><p>拷贝/etc/hosts文件到所有的节点上，同时验证节点之间是否可以无密码互相访问：</p><p>scp /etc/hosts root@主机节点名 如不需要密码就可以分发hosts到所有的主机节点上，说明前期配置工作完成。</p><h3 id="5.6-%E6%8B%B7%E8%B4%9D%E4%B8%BB%E8%8A%82%E7%82%B9%E4%B8%8A%E7%9A%84iozone%E5%91%BD%E4%BB%A4" tabindex="-1">5.6 拷贝主节点上的iozone命令</h3><p>拷贝主节点上的iozone命令到所有的节点的/tmp目录下</p><p>（可以是任意目录下，所有的节点包括主节点自己）：</p><p>cp /opt/iozone/bin/iozone /tmp</p><p>scp /opt/iozone/bin/iozone 节点名:/tmp</p><h3 id="5.7-%E5%9C%A8%E4%B8%BB%E8%8A%82%E7%82%B9%E4%B8%8A%E5%88%9B%E5%BB%BAnodelist%E6%96%87%E4%BB%B6" tabindex="-1">5.7 在主节点上创建nodelist文件</h3><p>格式为：节点名或ip iozone测试的文件系统名 iozone的工作路径</p><pre><code class="language-shell">#cat /tmp/nodelistnode1 /mnt/testfs /tmp/iozonenode2 /mnt/testfs /tmp/iozone...noden /mnt/testfs /tmp/iozone </code></pre><p>注：如果要使用多个线程进行测试，可以每个节点多复制几行。</p><h3 id="5.8-%E6%8B%B7%E8%B4%9Dnodelist%E6%96%87%E4%BB%B6%E5%88%B0%E6%89%80%E6%9C%89%E7%9A%84%E8%8A%82%E7%82%B9%E4%B8%8A%E7%9A%84%2Ftmp%E7%9B%AE%E5%BD%95%E4%B8%8B%EF%BC%9A" tabindex="-1">5.8 拷贝nodelist文件到所有的节点上的/tmp目录下：</h3><p>scp /tmp/nodelist 节点名:/tmp 到此基本环境配置完成。</p><h3 id="5.9-%E5%91%BD%E4%BB%A4" tabindex="-1">5.9 命令</h3><p>IOZONE命令的执行(具体的参数和配置可以参考手册)：</p><pre><code class="language-shell">/tmp/iozone -i 0 -i 1 -s 4G -Recb /tmp/log.xls -t 2 -+m /tmp/nodelist -C |tee /tmp/iozone.log -r 512 </code></pre>]]>
                    </description>
                    <pubDate>Thu, 21 Jul 2022 15:59:46 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[哈耶普的上位史 -- 美国精英高校的起源]]>
                    </title>
                    <link>https://blog.qiql.net/archives/hayepu</link>
                    <description>
                            <![CDATA[<blockquote><p>本文摘自《优秀的绵羊》</p></blockquote><p>  这一切是如何演变到今天的呢?我们的大学招生录取标准支撑起了整个教育系统，或者说教育围绕着招生而运转。学生从孩提时代到青春期，从大学到进入职场，不论是教育方式还是教育结果，无不受制于大学招生这根指挥棒。你是否好奇，今日的招生标准从何而来?对于这一问题的解答。我们不能仅看过去10年或者15年的情况，而应追溯历史。毋庸置疑，今天的精英学生相较于20年或40年前的精英，两者之间的区别仅仅在于程度。因为教育这个系统工程影响了几代人，当今的美国职场人，中产阶级、精英阶层，那些掌控我们政府、经济、文化的人都经历了教育的洗礼，追溯教育系统的根源也就是研究这些人的根源。要做好这件事，就要回到起始。</p><p>  追根溯源，我们要回到之前的那段时间–镀金时代， 具体来讲就是19世纪末。这里有一点不同于大家的认知，常春藤盟校并非- 直以来就是富家子弟的专属。在南北战争之前，常春藤盟校是规模较小、区域性较浓的学校。在校就读的学生中，有少部分人确实是绅士的后代或他们本身就是新绅士，但更多的富家子弟并不会考虑选择这些学校。在当时那个年代，美国还是一个以农业为主的社会，整个国家的经济仅由几大块区域性经济构成。因此富家子弟的数量也是非常有限的。</p><p>  在南北战争结束之后,E.迪毕•波获尔(E.Digby Baltzell)在他的著作《新教当权者》(The Protestant Establishment)中谈到了慢慢改变的格局。工业经济的爆炸式发展，催生了新的财富以及财团控制的政府。铁路系统把原来分散的区域连成了整体的网络，区域经济演变成国家经济。原有的地方性精英开始意识到，应该成为全国性的精英，并想方设法巩固自己的阶级地位。新贵们需要通过交际让自己成为被众人接受的贵族；与此同时，面对来自南欧和东欧汹涌的移民浪潮（这些移民大部分是天主教徒和犹太人)，所有的新贵为了竭力保护自己的阶级地位，上层社会开始了强大的反犹太教和反天主教的运动，其中最有名的一个阶层是新教徒的盎格鲁-撒克逊裔美国人。</p><p>  通过作家波获尔的笔，这一教派不仅众所周知，其性质也慢慢清晰化。既讽刺又矛盾的现象是，当盎格鲁-撒克逊在美国成了新贵之后，他们重蹈覆辙，充当了当年他们自己为了追求平等而反对的英国责族阶级。</p><p>  盎格鲁-撒克逊贵族为自己的阶层创建了形式丰富的学府和机构。到了1880年，私密会所开始出现，如巴港和纽波特已经建立起来。第- 家乡村俱乐部于1882年成立。格罗顿学校于1884年成立，它虽然不是新英格兰地区的第一所大学预备学校，但它却是第一所模仿英国贵族学校而建立起来的学校。《社会登记》(The Social Register)于1887年开始出版。美国革命女儿会(Daughters of the American Revolution)于1890年成立。在很短的时间内，贵族们开始逃离大都市，向乡下搬迁，如费城的主线(MainLine)就是新的聚居地，全国性乡村学校的创建也拉开了序幕。</p><p>  盎格鲁-撤克逊贵族设立了不少机构，其中有一种机构虽然并非是他们建立的，但是他们对它却有绝对的影响。当时的哈佛、耶鲁和普林斯顿各自都开始招兵买马，书写自己的历史:哈佛是“黄金海岸线”富家子弟的聚集地，《耶鲁的斯托弗》(Slover at Yale)是当年相当有名的一本关于耶鲁本科生生活的小说；普林斯顿的F.斯科特•菲茨杰拉德(F.Scott Fitzgerald)是《了不起的盖茨比》的作者。这类精英学校为富裕家庭的年轻绅士们提供了极其重要的平台，方便了他们与来自全美各地的类似背景的家庭搭建人脉，巩固自己的价值系统，并相互认可彼此在社会顶层的地位。与此同时，大学为了吸引新贵，着手弱化自己给人的“书呆子”形象，大力鼓励课外活动。而体育，特别是竟技类运动，比如能够充分彰显“男子汉”形象的美式橄榄球，也就是在那段时间诞生并一直延续到现在的。</p><p>  这一手段果然奏效，大学普遍都成功扩招:哈佛从19 世纪60年代每年招收100名学生扩展到1904年每年招收600名学生。学术被抛到了窗外，只有少数人会认真对待它。派对、恶作剧和高人一等的心态开始在学校的主流生活和社交圈蔓延，同时来自大学预备高中的学生成了大学主力军。借用杰罗姆•卡拉贝尔(Jerome Karabel)的话，“哈耶普”三驾马车，正是在19世纪的80年代奠定了它们龙头老大的地位，为其他学校设立了风向标。</p><p>  没过多久，问题接踵而至。卡拉贝尔教授在她的著作《被选中的:哈佛、耶鲁和普林斯顿的入学标准秘史》(The Chosen: The Hidden History of Admission and Exclusion at HYP)中揭露，虽然录取是根据入学考试成绩而定的，但是有些科目，如希腊语和拉丁语，公立学校根本就不提供，因此美国大部分高中毕业生从一开始就没有机会被名校录取，但是来自“对口学校”的学生，不管他们成绩有多糟糕，还是有机会被录取。比如说格罗顿学校在1906年到1932年期间，有405位高中毕业生申请哈佛，402位被录取。</p><p>  这种录取方式保证了学校与上层社会的关系，但是导致学术水平直线下降。面对这种变化， 这三所高校开始有所行动，到了1916年，学校取消了古典语言作为大学入学的要求。随之而来的是公立高中申请人数的井喷式增长，其中大城市的公立高中生很多是犹太人。随后，哥伦比亚大学在两年内的时间，减少了近半的犹太学生， 但这还是不能阻止上层家庭抛弃哥伦比亚大学。</p><p>  目睹哥伦比亚大学的经历，三驾马车有了前车之鉴，想尽办法不重蹈覆辙。为了阻挡犹太申请者，一系列新的入学要求开始实施:教师推荐信，校友面试，对运动员或者有“领导特质”学生的偏好，给校友的子女加分，更加强调家庭背景，减弱学术能力的比重等等。学校宁愿录取来自美国中西部新教背景家庭的孩子，也不要录取来自纽约市布鲁克林区“ 勤奋的草根”，即便中西部的孩子不是最聪明的。</p><p>  由于仅仅依赖于申请者的名字无法辨别学生的身份，因此普林斯顿大学开始要求申请者提供照片。“气质”成了新的代名词，包括申请者的举止、形象、语音语调，等等。“耶鲁人”的气质，之前只需从固定的几所高中录取学生就会有所保证，但在这个新的时代，则由主观的审核来判断并维持。从此，大学的招生办公室应运而生。</p><p>  这种“生态系统”一直完好无损地延续到20世纪60年代。三驾马车的生源主要还是来自大学预备高中，而这些学生往往都是富家子弟。学校虽然没有官方说法，但是犹太学生常常被有意限制。原来的老男孩俱乐部那一套文化还在继续发展，诸如闭门握手言事、对口学校输送学生等等。即便到了20世纪50年代，哈佛大学平均10个位置也只有13位申请者竞争，而耶鲁大学的录取率为46%。一般而言， 在申请之前，你就知道被录取的概率，如果概率不大，那也就没必要申请了。</p><p>  到了20世纪30年代，在表面平静之下的暗流，已经聚集了足够的力量来摧毁原来的“生态系统”。詹姆斯•B.柯南特(James B.Conant)时任哈佛校长，新官上任三把火，他开始逐步提高学术标准来为更多的学生打开哈佛之门，以便吸引到更优秀的学生。为了能够识别出那些聪慧的学生，弥补原有固定渠道的生源，他把目光转向了刚出炉不久的心理测量学测试一SAT。</p><p>  柯南特校长是位改革者， 但不是革命家。他所主张的改变是逐步进行的，在接下来的30年循序渐进。在第二次世界大战之前，顶尖大学的SAT平均分为500分，是美国SAT的中间值，但是到了20世纪60年代初，顶尖大学的SAT平均分已经高达625分。</p><p>  哈佛进行了改革，耶鲁则选择了革命。当时的耶鲁校长是小金曼•布鲁斯特(Kingman Brewster), 他认识到，如果精英阶层想继续保持自己的社会地位，想继续领导这个国家，那么他们必须能够更好地接纳处于上升期的社会团体，这不是为了别人，而是为了自己。当时社会的众多变化是由不得这三驾马车忽视的。</p><p>  布鲁斯特于1963 年开始担任耶鲁大学校长,他出台了一系列的新政策:在短短两年之内就把学术潜能提到录取标准之首;推翻了“全能手”的录取思路，取而代之的是“某方面有建树的特长生”;减弱了对体育健将和校友子女的偏向，整体取消了个人外表特征的考虑(最后一条的改变也导致新一届学生身高平均下降了1.2厘米)；学校结束了与对口学校的亲密关系，不再限制犹太学生数量，并为低收入人群提供了全额助学金；少数族裔学生的平等政策也在20世纪60年代末出炉。1969 年，耶鲁从单纯男校转变成了一所男女混校。</p><p>  布鲁斯特校长动作太大，一口气摧毁了原来固有的系统，以至于耶鲁的校友迫使他取消或者扭转一些新政策， 特别是要保持对运动健将和校友子女的特殊考虑。但是大时代在改变，时机成熟了，任何反对声音都只是沧海一粟。 1965 年，恰好是第二次世界大战后美国的婴儿潮开始进入大学，这更成为美国大学招生录取的转折点:美国从原有的贵族制进入了崭新的任人唯贤、精英领导制;从阶层、“人品”、人脉转向了考试和成绩。</p><p>  这就是我们现今大学录取机制的由来，你可能会发现，今天的录取与之前的差异并不大。布鲁斯特校长和其他大学成功地为大众打开了精英学府的大门，但是他们并没有废除原来固有的大部分标准，比如对体育健将和校友子女的偏好。新系统不过是在旧制度上增加了-些新的砝码而已。也就是说，今天的申请者除了要满足原有的条件，还要付出更多的努力。</p><p>  让我们稍微品味-下如今 顶尖大学给所有学生制定的门槛。 虽然我们并不要求每个学生都是体育健将，有能力参加最高级别的竞技，但是我们要求所有学生都具有运动员精神，并且是体育运动的参与者。在过去，这种学生，一般是通过参加-些只有在大学预备高中才有的体育项目，如击剑，划船等，才能既具备技能又拥有优雅气质。我们又要求学生有一定高度的艺术造诣，作为一种自我修养的表现。要做到这些，其背后是需要时间成本和家庭文化支出的，而在当时，这属于上层家庭的追求。我们还要求学生们具有个人魅力(用老一辈的话来讲就是能够社交，行走于不同的俱乐部)，因此我们需要学生参加面试并提供推荐信。我们又要求他们展现“服务”精神，而这种“服务”无非就是现代版本的贵族式思典，是一种对低端人群的施舍。最后，我们需要“领导者”。如果学生仅仅是学生会一名成员，那是不够的，他必须曾经主持过学生会，或者是话剧社社长，或者棒球队队长。不管怎样，你给人的印象就如同一位“储君”，是未来的领导者， 这与百年前私立学校绅士培养方式如出一辙。</p><p>  原有的顶尖大学招生录取无疑是依照顶层家庭背景而设计的，后来布鲁斯特校长提倡对学术高标准的追求，这超出了顶层家庭所擅长的范围。如今我们有一系列的标准化衡量标准，如SAT. AP、GPA、奖学金证书，等等。现在我们的学生不仅要拥有原来的贵族阶层特质，而且还要展现出现代贵族特征。难怪他们是如此的繁忙和惶恐啊!</p><p>  自20世纪60年代以来，招生录取游戏规则的唯一变化就是竞争趋于白热化:录取率降低，对申请者要求提高，学生压力增大。这场没有硝烟的战争枪声一响，每个人都成了主动或被动参赛者。到了1968年，哈佛、耶鲁和普林斯顿的录取率均已降至20%以下。到了1974年，美国举国上下的高中生都为SAT考试而痴狂。哥伦比亚大学教授尼古拉斯•雷曼(Nicholas Lemann)在他的著作《大考验》(The Big Test) 一书中分析了其中的前因后果。事实上，我自己的兄长就是当时的一位参赛者。 20世纪70 年代大学申请者数量激增，导致学生压力剧增，当然，拥有大学学位的人数也相应增加。当社会上有更多的人拥有大学学士学位时，名校俨然已成为使自己鹤立鸡群的台阶。</p><p>  到了20世纪70年代末，富裕家庭通过各种方式占尽了优势:聘请SAT私人教师帮助提分，雇用自荐信导师(也就是代笔者)，以校友捐助的名义影响录取结果等。另一方面，大学鼓励，越多的AP课程越有竞争力。如果学生想要在高中最后两年上更多的AP课，那么他最好从初中开始就有所规划。    到了20世纪80年代初，第二次世界大战后的婴儿潮这代人已经完成了大学学业，大学开始更加主动地吸引新生。由于当时美国政府放松了对航空业和电信业的管制，使得交通和通信成本降低，因此高校的市场推广也走向了全美，而且家长也更愿意送自己的孩子到更远的地方上大学。大学之间的竞争日趋激烈，提前录取机制作为高校尽早锁定优秀生源的有效方法，在这个时候被运用得淋漓尽致。</p><p>  如果高校之间、家庭之间的竞争还不够激烈的话，那么1983年《美国新闻与世界报道》(Americn News and World Report)发布的前所未有的大学排名真的把名校游戏炸开了锅。对高校来讲，录取数据一直以来代表了名校的声誉和地位，如今竟出现第三方机构发布统的数据， 囊括所有大学，并为大学排名。</p><p>  到了1987年，一群由大学校长组成的代表团集体与《美国新闻与世界报道》交涉，要求对方停止排名行为，可惜为时已晚。整个围绕名校录取的产业已经爆发，从考试培训到咨询顾问，从家教到名校录取指南等等。一本开创先河的名校录取书籍–《 如何敲开常春藤盟校之门》(How to Get Into an Ivy League School)于1985年出版，作者汤姆•沃尔夫(Tom Wolfe)观察到，对追逐名校的痴狂症在1988年正式爆发。</p><p>  在过去半个世纪里任意选择一个时间点， 你会发现，随着时间的推移，之后的录取竞争比之前更残酷。美国的婴儿潮人口红利逐渐消失，大学入学新生数量一直在减少， 直到1997年，局面才开始扭转。在接下来不到10年时间里，大学新生就已回到了婴儿潮入学时的水平。回顾过往的20多年时间，美国大学生源已走向全球化。大学变得更擅于做市场营销,明明知道有些学生根本不会被录取，但是大学还是鼓励所有学生都来申请，为的就是降低录取率和得到漂亮的招生录取数据，这些数据不仅仅象征着学校的社会地位，而且更有实际意义。高校就如同商业机构，运营一所学校需要经费，因此也时常需要贷款。金融机构对学校信贷的考量标准之一就是录取数据。商业机构在乎的是自己的利润，学校在乎的当然就是录取数据了，而且这些数据需要逐年美化。</p><p>  过去20多年让人有一种跨时代的感觉，其中最关键的因素是“名校游戏2.0版本”已经诞生。第一代名校生都是20世纪七八十年代进入大学的，而他们的父母本身要么读了公立大学要么根本没读过大学。名校对第一代人来讲，是一种鲤鱼跳龙门的机会。但是第二代名校生，也就是20世纪90年代的大学生，他们的父母本身都是名校毕业生，后来又都是各行各业的精英。在这群家长眼中，名校对自己的孩子来讲是必需品，而不再是改变生活的一次机会，并且在他们看来，精英式的生活方式是通往幸福的唯一途径。</p><p>  自从1992年开始，《美国新闻与世界报道》排名前20的文理学院中，有17所的录取率下降了超过30% ;排名前20的综合性大学中，18 所录取率下降超过了50%。范德堡大学的录取率从65%下降到14%，芝加哥大学录取率从45%下降到13%，哥伦比亚大学从32%下降到7%。2011年提前申请杜克大学的学生数量在2010年14%的涨幅基础上又增加了23%。2013年，哈佛、斯坦福和哥伦比亚大学收到了超过了31000 份申请，这些申请争夺不到2500个席位，相较于6年前，增幅已经超过50%。</p><p>  或许20世纪七八十年代的大学生对今天的大学录取感到陌生，甚至对今天名校学生也会有种陌生感，今天的名校生仿佛与自己当年上大学时很不一样–他们简直就是超人，或者是超能力的“机械战警”。观其原因，那是因为20世纪60年代以公平为主题的录取革命运动代替了原有的上层社会那种依赖于人脉的游戏规则，只是如今这个游戏的时间和空间战线拉得更长了。</p><p>  在1981年我（作者：【美】威廉•德雷谢维奇）进入大学（哥伦比亚大学）的时候，身边进入顶尖名校的同龄人通常上3门AP课，参加3项课外活动。如今的水平通常是7或8门AP课，9或10项课外活动。在2008年，我作为耶鲁大学的一名教员有机会轮值一天参加招生办录取工作，观察并学习招生办的工作流程。学生的课外活动列表在招生办里被称为“个人炫富”名单。如果你的课外活动只有五六项的话，招生官从一开始就会注意到，这样的申请简直就是出师不利。《纽约时报》专栏作家罗斯•多纳特在他的书《特权》中谈及，-位学生有 12项课外活动，多纳特称其为“ 典型的全能冠军型哈佛履历”。而我指导的一位耶鲁学生就考了11门AP课程。</p><p>  建立这么一套评判标准并非招生办的责任，他们无非是执行来自高层的指令而已。在招生办工作的一天，招生办团队给我留下很深刻的印象。招生官不仅仅要在漫长的冬季整理上万份申请材料，他们各自还需要非常熟悉自己负责的地区。那天我所在的小组负责审阅宾夕法尼亚州的东部区域，实际上就是费城周边的乡村。一位大概30岁出头年轻的招生官，对该地区的特点有着惊人的熟悉程度。他通过无数次的现场招生咨询，对每所高中了如指掌，并与辖区高中里的升学指导员、当地的校友以及第三方协助资源建立了紧密的关系。</p><p>  我当时参与招生办工作时已是春季，因此提前申请环节已经结束。所有的申请者，根据他们档案里的各种指标(如SAT考试成绩、GPA、 成绩排名、教师推荐信、有专长的运动员、校友子女、多元性等)，每位学生会得到一个综合性评分，从1分到4分。1分代表最具有竞争力的人群，他们将会没有任何悬念被录取。</p><p>  在那天午餐休息时间，我想看一个 1分水准的申请，招生办给我展示了一位英特尔科学奖得主的申请。除了1分的人群之外，3分和4分占了剩余申请者的3/4,而这群人被录取的希望渺茫，除非是国家级运动员或务是最高级别的捐赠家庭。后者几乎在任何情况下都是会被录取的。我们花时间最多的是2分的人群。在6个小时的招生委员会讨论过程中，我们排除掉100~125个申请，平均速度是每三四分钟一个学生。该区域总共有40个名额，我们的任务是从2分的人群里选出最合适的10～15名学生。</p><p>  这位年轻的招生官为每一个申请做 了陈述，他的语速之快以及语言的专业性，如狂风暴雨般打在我的身上，而我只能是临场招架，边做边学。</p><p>      “Top checks”代表推荐信的质量在各个方面都是最优秀的;</p><p>      “Good rig”代表学生在高中修的课程达到了让人基本满意的程度;</p><p>      “Ed level 1”说明家长的教育水平低于大学，这从侧面反映了申请者成长和生活的现实艰难程度;</p><p>       &quot;Lacrosse #3”意味着这位学生在大学体育教练青睐的学生中排第三；</p><p>      “MUSD”代表这是一位拥有最高音乐造诣的学生，将来很有可能走职业路线;</p><p>      “T1”指的是第一封推荐信;</p><p>      “E1”指的是第一篇学生自荐信;</p><p>      “TX”指的是额外的推荐信,；</p><p>      “SR&quot;指的是学校的升学指导老师。</p><p>  我们的讨论过程就是先听陈述，然后发问，再参考一两封推荐信， 最后投票决定。我们这组委员会一共5人， 3位是招生办的，还有位代表大学院长办公室，最后一位是我。 而我一般都参考那几位招生办的专业人士意见。开此类会议往往很消耗能量，因此会议桌上有一大堆美味可口的垃圾食品来陪伴我们，补充能量。招生办主任很容易让人联想到美国演员本•斯坦(Ben Stein),他似乎有两种超能力:一种是仅依赖脆玉米薯片就能长时间工作；二是对申请者材料有着独特的见解。</p><p>  面对这么多卓越的申请者，我们寻我的是有特殊品质的学生。个人品质(Personal Quality),英文缩写&quot;PQ&quot; ，要通过咀嚼推荐信或者学生的自荐信去体会。虽然高分和漂亮的股历是必需的，但是如果学生只有这两点，往往会被拒绝。而拒绝的原因大概会有“缺乏让人眼前一亮的闪光点”， “不是一位团队建设者”或者“与大众雷同”。</p><p>  其中有一位学生疯狂地参与课外活动，并提交了8封推荐信，最后众人判他“猛过头了”。我在接受招生办培训时了解到，成功的申请者大概有两类:要么是“全能冠军”，要么是“偏才”。如果是后者,那么他得足够“偏”,比如说学生的音乐打动了整个耶鲁大学的音乐系或者是获得某项全国大奖的理科生。</p><p>  实际上，大部分学生还是要争取做“全能型”的申请者，这点与丁克.斯托弗(Dink Stover)在耶鲁的年代就大不相同了。布鲁斯特校长的革新确实让偏才型人才取代了当年老一套大学预备学校式人脉推动的录取方式，而现在的大学提倡招募各路“神仙”:年轻的记者，极具潜力的天文学家，未来的大使或者是语言天才。成功被录取的学生履历上的10项课外活动并非代表了10种不同性质的活动。其中3、4、 5项体现了对某一个领城的专注:数学、艺术或者学生会组织。你必须能够拿得出一两样绝活， 同时你必须在其他方面也有卓越表现。</p><p>  总的来说，你既要“全能”又要“偏才”。 你可能早就对自己对科学和数学不感兴趣心知肚明，但是你还是要修微积分，为的就是提高你的课程难度，并且你还要尽力取得优异的成绩，这样才能保证你的高中平均成绩和在学校的排名。你有可能是一位对诗歌或者计算机编程有着执着热爱的“异类”，但是你还是需要会吹一种乐器、 参加体育活动并且最好是创建一个俱乐部，还要马不停蹄地去赶场。</p><p>  总而言之，你必须要顾及方方面面，取得全A的成绩，争取领导职位，积累更多的课外活动，这样你就可以将自己打造成一位“超人”。</p><p>  这种变味的大学录取机制以及它的制订者已经让人难以看清楚它们原来的样子。这种名校残酷的录取标准和追逐名校的疯狂，已经不是由外在的力量所能驱使的了，比如经济全球化或者《美国新闻与世界报道》的排名所能驱使的;其背后最大的驱动力就是为了疯狂竞赛而竞赛。个人履历的竞赛，就如同两国之间的核武器竞赛。没有一个国家需要20000颗核弹，除非另外一个国家拥有19000颗核弹。没有人需要11项课外活动，除非另外一位学生参加了10项课外活动，那么其真正目的是什么呢?唯-的答案就是超越他人。我们的孩子的发展就如同长颈鹿的头颈变得越来越长，变得越来越畸形。这代人在20年后会成为怎样的人，这是可想而知的。</p><p>  这种的游戏规则已经不仅局限于最顶尖的大学(哈佛、耶鲁、普林斯顿和斯坦福）或者8所常春藤盟校。借用安德鲁•海克（Andrew Hacker）和克劳迪娅•德雷福斯（Claudia Dreifus）在《高等教育》上所用的词汇， “12所黄金院校”包含了8所常春藤盟校，再加上斯坦福、杜克，威廉姆斯和艾姆赫斯特。最极端的竞争、最靓丽的覆历、最惨不忍睹的录取率，将永远在这几所大学里上演。</p><p>  从我过去多年的全美旅行和交流中发现，这种疯狂在更大范围的大学里广泛存在，只不过程度不同而已。来自弗吉尼亚大学的学生可能没有八九项课外活动，但是他们起码也有六七项。我曾接触过来自密西西比大学荣誉学院的学生，他们可能没有修过七八门AP课，但是他们也完成了五六门。即使这些院校学生的抱负、天资、痴狂症以及家长的经济背景可能比顶级院校的稍逊色些， 但是他们的思考方式和价值观与前者大同小异。</p><p>  每年被哈佛拒绝掉的33000名学生照样会进入到其他大学。到了2012年，65所大学院校的录取率已低于33%，再加上另外二三十所录取率差不多的学校和一些略高于这个录取线的学校(比如女子学校，本来申请的人数就会少一些)，那么大概总计有100所高校是属于精英梯队的。事实上，这也仅仅是个保守的估测。有-些不知名的区域性质的文理学院，也见证了类似的问题。詹姆斯•法洛斯(James Fallows)估计，全美每年大概有10%~15%的高中毕业生卷入了为争取名校席位的竞赛中，也就是40万大军，这是个系统性问题。下章节我将剖析这群人的成长历程–他们的父母、他们的学校以及他们作为“优秀的绵羊”的内心世界。</p>]]>
                    </description>
                    <pubDate>Wed, 20 Jul 2022 10:07:47 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[ 杰瑞 · 罗林斯 -- 被逼走向民主的独裁者]]>
                    </title>
                    <link>https://blog.qiql.net/archives/luolinsi</link>
                    <description>
                            <![CDATA[<blockquote><p>本文摘自《独裁者手册》</p></blockquote><p>  1982年1月11日罗林斯夺取权力的故事常常被人以圣经般的语言描绘。由于他名字的开头字母缩写为J.J.，他经常被人称为耶稣二世。而这是他的第二次降临。他在1979年就曾经领导过一次军事政变。罗林斯拥有电影明星般的俊朗外表，魅力横溢。但魅力不是让他稳坐权力宝座的原因。在小联盟体制下，压迫人民和对核心支持者提供丰厚回报是领导人的必做功课，罗林斯也不例外。在他统治的头六个月里，有180人被杀害，上千人被逮捕和拷打。罗林斯的忠诚士兵因残暴而闻名，他通过大规模提高军费开支收买了军队的忠诚。尽管经济和政府财政完全崩溃，罗林斯知道他最需要谁的支持，并先付钱给他们。</p><p>  罗林斯拥有阻止抗议活动的天才。他通过限制纸张供应来扼杀任何自由的媒体。同时他的支持者渗透进工会，在很多年里使罢工几乎不可能发生。他任何时候都预防发生自由集会。1983年1月，尼日利亚宣布驱逐140万名在尼日利亚工作的加纳人。在几周时间里加纳人口的十分之一【其中大部分是年轻人】从尼日利亚潮水般返回贫困的加纳。成千上万满腹抱怨的失业者将在首都到处晃荡，这样的前景吓坏了加纳政府的很多人，有人建议关闭边境阻止他们回来。罗林斯并没有这样做，而是敞开双臂欢迎他们回国，但回来的人立即被运送回他们家乡的村子。这一大规模运送工程避免了出现在墨西哥和尼加拉瓜那样的难民营。这一处理方法远比丹瑞人道。</p><p>  罗林斯面临的最根本问题在于加纳破产，经济几乎彻底崩溃。加纳的粮食产量在非洲排倒数第二，只比垫底的乍得多。加纳经济困局和政治回报体系的核心是汇率操纵。加纳货币塞地的官方汇率比黑市汇率高很多。核心支持者被允许以官方汇率兑钱，然后再去黑市倒手。不幸的是，这伤害了农民的积极性。到了20世纪80年代中期，农民把农产品运到市场上销售的收入还不足以支付汽油的费用。市场上七成的农产品是农民扛在脑袋上运过去的。农作物走私到邻国成为常态。政府于是将走私行为定为重罪。由于没有什么产品可供出口，加纳丧失了借贷能力，因而破产。</p><p>  罗林斯遇到了大麻烦。他夺取了政权，想推行革命的社会主义政策，但他需要钱。正如娜奥米•哈赞所说的:&quot;问题已经不再是资源在哪里，而是它们到底存不存在。“为了解决经济问题，罗林斯首先关闭了所有大学，叫学生去帮助农民收割农作物。但这样的措施远远不够。人民饱受饥饿之苦。加纳没有足够资金进口粮食和发放军饷。作为一名遵守规则的优秀独裁者，罗林斯知道最该优先做什么：付钱给军队。很快，骨瘦如柴的人民的锁骨有了一个很流行的委婉说法，叫做“罗林斯项链”。他向苏联寻求帮助，怎奈苏联正在面对自己的财政问题，尽管罗林斯的政治立场已经向左转，但苏联还是拒绝了他的请求。</p><p>  此时的罗林斯进退两难。他需要钱，而唯一能来钱的方法就是鼓励人民回去工作。1983年初他开始进行政策大逆转。加纳塞地被允许贬值。付给农民的农产品价格得到提高，对汽油、电力和医保的补贴被取消。国际金融机构比如国际货币基金组织和世界银行很高心看到有人奉行他们的政策，但许多罗林斯的亲密盟友却很不高兴。政策改变也伴随着人事变动。他导演了一次出人意料的人事调整行动，在他的目标任务能够组织起来反对他之前造成既定事实。他的一些最亲密盟友一夜之间丧失了影响力。其中一些人被处死，比如劳公活动分子约阿希姆•阿马提•克维【据称参与了一项谋杀法官的著名案件】。还有一些人流亡国外，比如激进的学生活动分子克里斯•阿提姆。</p><p>  一个很能说明问题的事实是，到1985年，“临时全国保卫委员会”最早的那批委员只剩下罗林斯一个人。罗林斯统治路线调整的进一步标志就是，“临时全国保卫委员会”的规模由当初的6人扩大到了10人。没有哪个领导人会自愿增加联盟人数，除非他认为这么做有助于维护自己的权力生存。<br />  我们可以想象得到，罗林斯是不得已才走上民主道路。他几乎没有什么选择余地。他需要钱。为了得到钱，他实行了赋予人民权利的政策。渐渐地，人民的胃口变大了。“罗林斯是自身成功的受害者”通过解放经济和开放电波，人民获得了发言权。人们感受到增强的信心。随着经济危机缓解，人民开始觉得“我们有能力做到这些，不需要有人教我们怎么做”。</p><p>  我们前面看到，到1989年博亨教授已经可以轻松地公平批评罗林斯。就算是博亨都必须承认改革改善了经济。“罗林斯项链”被“罗林斯马甲”取代【肚子胖了】。为了实行让人民满意的政策，罗林斯允许联盟逐渐扩张，同时提供更多的公共物品。1988年和1989年，地方被允许举行选举。罗林斯总是快人一步，而不是去激怒群众。当一些关系松散的政治势力联合起来发起“追求自由公平运动”并要求举行多党制选举时，罗林斯趁着反对派仍然组织混乱，抢先提出举行大选，以此将威胁消除。在1992年举行的总统大选中，他干脆利落地击败了新爱国党领导人阿杜•博亨。尽管有一些争议，但国际观察家认为选举结果基本上是公平的。</p><p>  加纳以后举行的大选也基本上公平。罗林斯和他的全国民主大会党在1996年再次赢得大选，他击败了新爱国党的约翰•库福尔。2000年罗林斯下台后，约翰•库福尔当了两届总统。2008年全国民主大会党的候选人约翰•阿塔•米尔斯在竞争异常激烈的大选中为该党夺回了总统宝座。</p><p>  罗林斯需要钱，来钱的唯一方式就是赋权给人民。通过允许人民自由集会和交流，他提高了人民的生产力。但这同时使人民更容易协调组织起来反对他。他通过妥协使自己占得先机，成功避免了抗议和革命的发生。不过他无法永远避免遭到抗议。1995年大约有5万到10万人在首都阿克拉举行“库米•普里科”游行，或称“我们受够了”游行。政府原本想阻止游行但被法院否决。独立的司法体系不仅鼓励创业精神，还保护人民的公民权利。</p><p>  今天的加纳是一个经济上非常有活力的民主国家。它从独裁制向民主制的转型发生在极具传奇色彩的罗林斯的领导之下。不过我们得记住，他是一个不情愿的民主领导人。假如他当初能获得所需资源的话，他肯定会固守他的社会主义革命。最近加纳正在开发一处近海油田，如果当初罗林斯有这样的收入或者苏联有资源为他提供支持，很可能他到现在还掌权，加纳会变得更穷更压抑。</p>]]>
                    </description>
                    <pubDate>Wed, 20 Jul 2022 09:56:39 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[塔列朗 -- 信仰着没有信仰的信仰]]>
                    </title>
                    <link>https://blog.qiql.net/archives/talielang</link>
                    <description>
                            <![CDATA[<p>  塔列朗出生于巴黎一个伯爵家庭，四岁的时候，他不慎从衣柜上跌落下来，摔伤了右腿，从此，终生只能与拐杖为伴。由于他无法像其他贵族子弟一样从军，他的父母就把他送到神学院学习。24岁时，塔列朗成为兰斯市圣雷米修道院院长，后来差点成为红衣主教，并在之后得到了奥顿区山主教的职位。</p><p>  但这时，法国已呈山雨欲来风满楼之势。作为三级会议的代表，他以敏锐的政治嗅觉闻到了法国大革命的火药味，于是他从专制阵营中冲杀出来，反戈一击。</p><p>  在法国革命初期，塔列朗表现得比第三阶级还要激进，他主动提出废除什一税、没收教会财产、天主教会从属于国家政权，从而赢得了革命者的信任。为了自保，塔列朗与雅各宾派主要领导人丹东结为至交，把他当成靠山和保护伞。1793年年底，雅各宾派开始发生内讧，丹东的地位日益不稳。塔列朗深知雅各宾派的另一位领导人罗伯斯庇尔这位“不可腐蚀的人”对任何人的变节和动摇都毫不留情。为了避祸，塔列朗决定流亡美国避难，而他昔日的盟友丹东则被罗伯斯庇尔送上了断头台。</p><p>  1794年7月，“热月政变”将罗伯斯庇尔等人送上了断头台，结束了雅各宾派的恐怖专政，法国革命的浪潮从巅峰阶段迅速回落。塔列朗看到捞取革命果实的时机来临，回到巴黎，与握有实权的督政官巴拉斯结为盟友，并当上了督政府的外交部长。在外交部长任内，塔列朗大肆贪污纳贿，两年之中收受贿赂三百多万法郎，并全部存入外国银行以保无虞。</p><p>  塔列朗深知督政府软弱无能，建立军人独裁势在必行，而屡建战功的拿破仑无疑是最佳人选。因此 塔列朗开始主动与拿破仑建立联系。1799年，塔列朗主动辞去外交部长职务，协助拿破仑发动雾月政变，还亲自去逼迫巴拉斯辞职。同时，他也暗中做好了另一手准备：如果政变失败，立即逃亡国外。同年，拿破仑任命他为执政府的外交部长。拿破仑称帝后，又任命他为法兰西第一帝国的外交大臣、侍从长、副大选侯，后来更是加封他为亲王和本尼凡托公爵。</p><p>  随着拿破仑的扩张野心日益膨胀，熟谙欧洲外交的塔列朗认为拿破仑已经成为欧洲的灾难。因此，他开始联合反法同盟，试图颠覆拿破仑的统治。1807年8月，塔列朗辞去外交大臣职务。1808年9月，他陪同拿破仑到埃尔福特开会时，秘密会晤俄国沙皇亚历山大一世，鼓动他带头反对拿破仑。此后，塔列朗一直与亚历山大一世保持着秘密联络，他在巴黎的府邸成为谋叛的中心。</p><p>  1808年年底，巴黎风传塔列朗和警务大臣阴谋废黜拿破仑。远在西班牙征战的拿破仑闻听此事，纵马千里回到巴黎，在杜伊勒里宫当着众人的面把塔列朗骂了个狗血喷头：“你！小偷！坏蛋！丧尽廉耻的人！”“你听着！我要像砸碎玻璃一样把你砸碎……你－－丝袜里的臭泥！臭泥！”在场之人莫不目瞪口呆，唯有塔列朗本人无动于衷，面不改色，神情坦然地站在火炉边取暖。拿破仑骂完后，塔列朗冷冷地说道:“真遗憾，这么伟大的人，却这么没有教养。”然后手持拐杖走了出去。拿破仑暴跳如雷，当即剥夺了他的所有职务。但因苦于没有证据，拿破仑对塔列朗无可奈何，无法将他治罪。塔列朗对这些侮辱表面上默不作声，暗中却向敌国奥地利出卖军事情报,对拿破仑进行报复。</p><p>  1815 年1月，拿破仑陷入四面楚歌，急需塔列阴再度出任外交大臣。然而，塔列朗深知拿破仑气数已尽，何况他自己早已经与流亡国外的路易十八暗中取得了联系。因此，任凭拿破仑如何威胁利诱，塔列朗都拒不从命。4 月，在塔列朗的操纵下，法国元老院宣布废黜拿破仑，成立以塔列朗为首的临时政府，恭迎波旁王朝复辟。5月，路易十八任命塔列朗为外交大臣，并且代表法国出席维也纳会议。塔列朗凭借机敏的外交手腕，使得法国从一个战败国一跃而成为维也纳会议的五强之一。拿破仑百日王朝灭亡后，6 月，波旁王朝再度复辟，路易十八再次任命塔列郎为总理大臣，同时兼任议会议长。然而，塔列朗出众的才能和反复无常的秉性，使得王窒惊惧不已，因此他们迫使塔列朗主动辞职。</p><p>  但塔列朗并未偃旗息鼓，而是时刻密切关注着法国政局的变幻。他深知，波旁王朝采取的过度反动措施， 必将导致自身的覆灭。1829年，他再一次预见到了革命的来临， 因而及时与自由派结成同盟。1830年7月，法国发生了“七月革命”，推翻了复辟王朝。塔列朗再度出山，担任七月王朝的驻英大使。 1838年5 月，塔列朗在巴黎安然病逝。就在死前几个小时，塔列朗表示与天主教会和解，对过去的行为表示忏悔，以求得其灵魂得到上帝的宽恕。</p><p>  对塔列朗这样一个政治变色龙， 法国人莫莱的刻画可谓人木三分: “在他身上，贵族的气派、妇人的度量、教士的伪善和猫似的贪嘴，兼而有之……他的任务很简单，与人为善便是恶，作恶多端便为善，而他更喜欢后者。高尚、仁慈、体贴、博爱这些词，他都了解并且使用得十分得体，但他从来不知道它们的含义是什么。”奥地利作家斯蒂芬*茨威格在评价塔列朗时，曾经说道: “每一场革命……胜利的不是第一个人，不是那个先锋，而总是最后一个人，总是那个殿后的人，那个把革命作为战利品来攫取的人…我们为了自卫，就得弄清这一类人的面目，看透他们得势的秘密。”</p>]]>
                    </description>
                    <pubDate>Wed, 20 Jul 2022 09:53:01 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[【搬运】谷粒学院文档-08：统一日志处理]]>
                    </title>
                    <link>https://blog.qiql.net/archives/guli08</link>
                    <description>
                            <![CDATA[<h2 id="%E4%B8%80%E3%80%81%E6%97%A5%E5%BF%97" tabindex="-1">一、日志</h2><h3 id="1%E3%80%81%E9%85%8D%E7%BD%AE%E6%97%A5%E5%BF%97%E7%BA%A7%E5%88%AB" tabindex="-1">1、配置日志级别</h3><p>日志记录器（Logger）的行为是分等级的。如下表所示：<br />分为：OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL<br />默认情况下，spring boot从控制台打印出来的日志级别只有INFO及以上级别，可以配置日志级别</p><pre><code class="language-"># 设置日志级别logging.level.root=WARN</code></pre><p>这种方式只能将日志打印在控制台上</p><h2 id="%E4%BA%8C%E3%80%81logback%E6%97%A5%E5%BF%97" tabindex="-1">二、Logback日志</h2><p>spring boot内部使用Logback作为日志实现的框架。<br />Logback和log4j非常相似，如果你对log4j很熟悉，那对logback很快就会得心应手。<br />logback相对于log4j的一些优点：<a href="https://blog.csdn.net/caisini_vc/article/details/48551287" target="_blank">https://blog.csdn.net/caisini_vc/article/details/48551287</a></p><h3 id="1%E3%80%81%E9%85%8D%E7%BD%AElogback%E6%97%A5%E5%BF%97" tabindex="-1">1、配置logback日志</h3><p>删除application.properties中的日志配置<br />安装idea彩色日志插件：grep-console<br />resources 中创建 logback-spring.xml</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&lt;configuration  scan=&quot;true&quot; scanPeriod=&quot;10 seconds&quot;&gt;    &lt;!-- 日志级别从低到高分为TRACE &lt; DEBUG &lt; INFO &lt; WARN &lt; ERROR &lt; FATAL，如果设置为WARN，则低于WARN的信息都不会输出 --&gt;    &lt;!-- scan:当此属性设置为true时，配置文件如果发生改变，将会被重新加载，默认值为true --&gt;    &lt;!-- scanPeriod:设置监测配置文件是否有修改的时间间隔，如果没有给出时间单位，默认单位是毫秒。当scan为true时，此属性生效。默认的时间间隔为1分钟。 --&gt;    &lt;!-- debug:当此属性设置为true时，将打印出logback内部日志信息，实时查看logback运行状态。默认值为false。 --&gt;    &lt;contextName&gt;logback&lt;/contextName&gt;    &lt;!-- name的值是变量的名称，value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后，可以使“${}”来使用变量。 --&gt;    &lt;property name=&quot;log.path&quot; value=&quot;D:/guli_log/edu&quot; /&gt;    &lt;!-- 彩色日志 --&gt;    &lt;!-- 配置格式变量：CONSOLE_LOG_PATTERN 彩色日志格式 --&gt;    &lt;!-- magenta:洋红 --&gt;    &lt;!-- boldMagenta:粗红--&gt;    &lt;!-- cyan:青色 --&gt;    &lt;!-- white:白色 --&gt;    &lt;!-- magenta:洋红 --&gt;    &lt;property name=&quot;CONSOLE_LOG_PATTERN&quot;              value=&quot;%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)&quot;/&gt;    &lt;!--输出到控制台--&gt;    &lt;appender name=&quot;CONSOLE&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;        &lt;!--此日志appender是为开发使用，只配置最底级别，控制台输出的日志级别是大于或等于此级别的日志信息--&gt;        &lt;!-- 例如：如果此处配置了INFO级别，则后面其他位置即使配置了DEBUG级别的日志，也不会被输出 --&gt;        &lt;filter class=&quot;ch.qos.logback.classic.filter.ThresholdFilter&quot;&gt;            &lt;level&gt;INFO&lt;/level&gt;        &lt;/filter&gt;        &lt;encoder&gt;            &lt;Pattern&gt;${CONSOLE_LOG_PATTERN}&lt;/Pattern&gt;            &lt;!-- 设置字符集 --&gt;            &lt;charset&gt;UTF-8&lt;/charset&gt;        &lt;/encoder&gt;    &lt;/appender&gt;    &lt;!--输出到文件--&gt;    &lt;!-- 时间滚动输出 level为 INFO 日志 --&gt;    &lt;appender name=&quot;INFO_FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&gt;        &lt;!-- 正在记录的日志文件的路径及文件名 --&gt;        &lt;file&gt;${log.path}/log_info.log&lt;/file&gt;        &lt;!--日志文件输出格式--&gt;        &lt;encoder&gt;            &lt;pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n&lt;/pattern&gt;            &lt;charset&gt;UTF-8&lt;/charset&gt;        &lt;/encoder&gt;        &lt;!-- 日志记录器的滚动策略，按日期，按大小记录 --&gt;        &lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.TimeBasedRollingPolicy&quot;&gt;            &lt;!-- 每天日志归档路径以及格式 --&gt;            &lt;fileNamePattern&gt;${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log&lt;/fileNamePattern&gt;            &lt;timeBasedFileNamingAndTriggeringPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP&quot;&gt;                &lt;maxFileSize&gt;100MB&lt;/maxFileSize&gt;            &lt;/timeBasedFileNamingAndTriggeringPolicy&gt;            &lt;!--日志文件保留天数--&gt;            &lt;maxHistory&gt;15&lt;/maxHistory&gt;        &lt;/rollingPolicy&gt;        &lt;!-- 此日志文件只记录info级别的 --&gt;        &lt;filter class=&quot;ch.qos.logback.classic.filter.LevelFilter&quot;&gt;            &lt;level&gt;INFO&lt;/level&gt;            &lt;onMatch&gt;ACCEPT&lt;/onMatch&gt;            &lt;onMismatch&gt;DENY&lt;/onMismatch&gt;        &lt;/filter&gt;    &lt;/appender&gt;    &lt;!-- 时间滚动输出 level为 WARN 日志 --&gt;    &lt;appender name=&quot;WARN_FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&gt;        &lt;!-- 正在记录的日志文件的路径及文件名 --&gt;        &lt;file&gt;${log.path}/log_warn.log&lt;/file&gt;        &lt;!--日志文件输出格式--&gt;        &lt;encoder&gt;            &lt;pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n&lt;/pattern&gt;            &lt;charset&gt;UTF-8&lt;/charset&gt; &lt;!-- 此处设置字符集 --&gt;        &lt;/encoder&gt;        &lt;!-- 日志记录器的滚动策略，按日期，按大小记录 --&gt;        &lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.TimeBasedRollingPolicy&quot;&gt;            &lt;fileNamePattern&gt;${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log&lt;/fileNamePattern&gt;            &lt;timeBasedFileNamingAndTriggeringPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP&quot;&gt;                &lt;maxFileSize&gt;100MB&lt;/maxFileSize&gt;            &lt;/timeBasedFileNamingAndTriggeringPolicy&gt;            &lt;!--日志文件保留天数--&gt;            &lt;maxHistory&gt;15&lt;/maxHistory&gt;        &lt;/rollingPolicy&gt;        &lt;!-- 此日志文件只记录warn级别的 --&gt;        &lt;filter class=&quot;ch.qos.logback.classic.filter.LevelFilter&quot;&gt;            &lt;level&gt;warn&lt;/level&gt;            &lt;onMatch&gt;ACCEPT&lt;/onMatch&gt;            &lt;onMismatch&gt;DENY&lt;/onMismatch&gt;        &lt;/filter&gt;    &lt;/appender&gt;    &lt;!-- 时间滚动输出 level为 ERROR 日志 --&gt;    &lt;appender name=&quot;ERROR_FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&gt;        &lt;!-- 正在记录的日志文件的路径及文件名 --&gt;        &lt;file&gt;${log.path}/log_error.log&lt;/file&gt;        &lt;!--日志文件输出格式--&gt;        &lt;encoder&gt;            &lt;pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n&lt;/pattern&gt;            &lt;charset&gt;UTF-8&lt;/charset&gt; &lt;!-- 此处设置字符集 --&gt;        &lt;/encoder&gt;        &lt;!-- 日志记录器的滚动策略，按日期，按大小记录 --&gt;        &lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.TimeBasedRollingPolicy&quot;&gt;            &lt;fileNamePattern&gt;${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log&lt;/fileNamePattern&gt;            &lt;timeBasedFileNamingAndTriggeringPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP&quot;&gt;                &lt;maxFileSize&gt;100MB&lt;/maxFileSize&gt;            &lt;/timeBasedFileNamingAndTriggeringPolicy&gt;            &lt;!--日志文件保留天数--&gt;            &lt;maxHistory&gt;15&lt;/maxHistory&gt;        &lt;/rollingPolicy&gt;        &lt;!-- 此日志文件只记录ERROR级别的 --&gt;        &lt;filter class=&quot;ch.qos.logback.classic.filter.LevelFilter&quot;&gt;            &lt;level&gt;ERROR&lt;/level&gt;            &lt;onMatch&gt;ACCEPT&lt;/onMatch&gt;            &lt;onMismatch&gt;DENY&lt;/onMismatch&gt;        &lt;/filter&gt;    &lt;/appender&gt;    &lt;!--        &lt;logger&gt;用来设置某一个包或者具体的某一个类的日志打印级别、以及指定&lt;appender&gt;。        &lt;logger&gt;仅有一个name属性，        一个可选的level和一个可选的addtivity属性。        name:用来指定受此logger约束的某一个包或者具体的某一个类。        level:用来设置打印级别，大小写无关：TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF，              如果未设置此属性，那么当前logger将会继承上级的级别。    --&gt;    &lt;!--        使用mybatis的时候，sql语句是debug下才会打印，而这里我们只配置了info，所以想要查看sql语句的话，有以下两种操作：        第一种把&lt;root level=&quot;INFO&quot;&gt;改成&lt;root level=&quot;DEBUG&quot;&gt;这样就会打印sql，不过这样日志那边会出现很多其他消息        第二种就是单独给mapper下目录配置DEBUG模式，代码如下，这样配置sql语句会打印，其他还是正常DEBUG级别：     --&gt;    &lt;!--开发环境:打印控制台--&gt;    &lt;springProfile name=&quot;dev&quot;&gt;        &lt;!--可以输出项目中的debug日志，包括mybatis的sql日志--&gt;        &lt;logger name=&quot;com.guli&quot; level=&quot;INFO&quot; /&gt;        &lt;!--            root节点是必选节点，用来指定最基础的日志输出级别，只有一个level属性            level:用来设置打印级别，大小写无关：TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF，默认是DEBUG            可以包含零个或多个appender元素。        --&gt;        &lt;root level=&quot;INFO&quot;&gt;            &lt;appender-ref ref=&quot;CONSOLE&quot; /&gt;            &lt;appender-ref ref=&quot;INFO_FILE&quot; /&gt;            &lt;appender-ref ref=&quot;WARN_FILE&quot; /&gt;            &lt;appender-ref ref=&quot;ERROR_FILE&quot; /&gt;        &lt;/root&gt;    &lt;/springProfile&gt;    &lt;!--生产环境:输出到文件--&gt;    &lt;springProfile name=&quot;pro&quot;&gt;        &lt;root level=&quot;INFO&quot;&gt;            &lt;appender-ref ref=&quot;CONSOLE&quot; /&gt;            &lt;appender-ref ref=&quot;DEBUG_FILE&quot; /&gt;            &lt;appender-ref ref=&quot;INFO_FILE&quot; /&gt;            &lt;appender-ref ref=&quot;ERROR_FILE&quot; /&gt;            &lt;appender-ref ref=&quot;WARN_FILE&quot; /&gt;        &lt;/root&gt;    &lt;/springProfile&gt;&lt;/configuration&gt;</code></pre><h3 id="2%E3%80%81%E5%B0%86%E9%94%99%E8%AF%AF%E6%97%A5%E5%BF%97%E8%BE%93%E5%87%BA%E5%88%B0%E6%96%87%E4%BB%B6" tabindex="-1">2、将错误日志输出到文件</h3><p>GlobalExceptionHandler.java 中<br />类上添加注解</p><pre><code class="language-java"> @Slf4j</code></pre><p>异常输出语句</p><pre><code class="language-java"> log.error(e.getMessage());</code></pre><h3 id="3%E3%80%81%E5%B0%86%E6%97%A5%E5%BF%97%E5%A0%86%E6%A0%88%E4%BF%A1%E6%81%AF%E8%BE%93%E5%87%BA%E5%88%B0%E6%96%87%E4%BB%B6" tabindex="-1">3、将日志堆栈信息输出到文件</h3><p>定义工具类<br />guli-framework-common下创建util包，创建ExceptionUtil.java工具类</p><pre><code class="language-java">package com.guli.common.util;public class ExceptionUtil {    public static String getMessage(Exception e) {        StringWriter sw = null;        PrintWriter pw = null;        try {            sw = new StringWriter();            pw = new PrintWriter(sw);            // 将出错的栈信息输出到printWriter中            e.printStackTrace(pw);            pw.flush();            sw.flush();        } finally {            if (sw != null) {                try {                    sw.close();                } catch (IOException e1) {                    e1.printStackTrace();                }            }            if (pw != null) {                pw.close();            }        }        return sw.toString();    }}</code></pre><p>调用</p><pre><code class="language-java">log.error(ExceptionUtil.getMessage(e));</code></pre><p>GuliException中创建toString方法</p><pre><code class="language-java">@Overridepublic String toString() {    return &quot;GuliException{&quot; +        &quot;message=&quot; + this.getMessage() +        &quot;, code=&quot; + code +        &#39;}&#39;;}</code></pre>]]>
                    </description>
                    <pubDate>Wed, 20 Jul 2022 09:13:27 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[【搬运】谷粒学院文档-07：统一异常处理]]>
                    </title>
                    <link>https://blog.qiql.net/archives/guli07</link>
                    <description>
                            <![CDATA[<h2 id="%E4%B8%80%E3%80%81%E4%BB%80%E4%B9%88%E6%98%AF%E7%BB%9F%E4%B8%80%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86" tabindex="-1">一、什么是统一异常处理</h2><h3 id="1%E3%80%81%E5%88%B6%E9%80%A0%E5%BC%82%E5%B8%B8" tabindex="-1">1、制造异常</h3><p>除以0<br />int a = 10/0;<br /><img src="/upload/2022/07/image-1658279155188.png" alt="image-1658279155188" /></p><h3 id="2%E3%80%81%E4%BB%80%E4%B9%88%E6%98%AF%E7%BB%9F%E4%B8%80%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86" tabindex="-1">2、什么是统一异常处理</h3><p>我们想让异常结果也显示为统一的返回结果对象，并且统一处理系统的异常信息，那么需要统一异常处理</p><h2 id="%E4%BA%8C%E3%80%81%E7%BB%9F%E4%B8%80%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86" tabindex="-1">二、统一异常处理</h2><h3 id="1%E3%80%81%E5%88%9B%E5%BB%BA%E7%BB%9F%E4%B8%80%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E5%99%A8" tabindex="-1">1、创建统一异常处理器</h3><p>在service-base中创建统一异常处理类GlobalExceptionHandler.java：</p><pre><code class="language-java">/** * 统一异常处理类 */@ControllerAdvicepublic class GlobalExceptionHandler {    @ExceptionHandler(Exception.class)    @ResponseBody    public R error(Exception e){        e.printStackTrace();        return R.error();    }}</code></pre><h3 id="2%E3%80%81%E6%B5%8B%E8%AF%95" tabindex="-1">2、测试</h3><p>返回统一错误结果<br /><img src="/upload/2022/07/image-1658279235206.png" alt="image-1658279235206" /></p><h2 id="%E4%B8%89%E3%80%81%E5%A4%84%E7%90%86%E7%89%B9%E5%AE%9A%E5%BC%82%E5%B8%B8" tabindex="-1">三、处理特定异常</h2><h3 id="1%E3%80%81%E6%B7%BB%E5%8A%A0%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E6%96%B9%E6%B3%95" tabindex="-1">1、添加异常处理方法</h3><p>GlobalExceptionHandler.java中添加</p><pre><code class="language-java">@ExceptionHandler(ArithmeticException.class)@ResponseBodypublic R error(ArithmeticException e){    e.printStackTrace();    return R.error().message(&quot;执行了自定义异常&quot;);}</code></pre><h3 id="2%E3%80%81%E6%B5%8B%E8%AF%95-1" tabindex="-1">2、测试</h3><p><img src="/upload/2022/07/image-1658279255800.png" alt="image-1658279255800" /></p><h2 id="%E5%9B%9B%E3%80%81%E8%87%AA%E5%AE%9A%E4%B9%89%E5%BC%82%E5%B8%B8" tabindex="-1">四、自定义异常</h2><h3 id="1%E3%80%81%E5%88%9B%E5%BB%BA%E8%87%AA%E5%AE%9A%E4%B9%89%E5%BC%82%E5%B8%B8%E7%B1%BB" tabindex="-1">1、创建自定义异常类</h3><pre><code class="language-java">@Data@AllArgsConstructor@NoArgsConstructorpublic class GuliException extends RuntimeException {    @ApiModelProperty(value = &quot;状态码&quot;)    private Integer code;    private String msg;    }</code></pre><h3 id="2%E3%80%81%E4%B8%9A%E5%8A%A1%E4%B8%AD%E9%9C%80%E8%A6%81%E7%9A%84%E4%BD%8D%E7%BD%AE%E6%8A%9B%E5%87%BAguliexception" tabindex="-1">2、业务中需要的位置抛出GuliException</h3><pre><code class="language-java">try {    int a = 10/0;}catch(Exception e) {    throw new GuliException(20001,&quot;出现自定义异常&quot;);}</code></pre><h3 id="3%E3%80%81%E6%B7%BB%E5%8A%A0%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E6%96%B9%E6%B3%95" tabindex="-1">3、添加异常处理方法</h3><p>GlobalExceptionHandler.java中添加</p><pre><code class="language-java">@ExceptionHandler(GuliException.class)@ResponseBodypublic R error(GuliException e){    e.printStackTrace();    return R.error().message(e.getMsg()).code(e.getCode());}</code></pre><h3 id="4%E3%80%81%E6%B5%8B%E8%AF%95" tabindex="-1">4、测试</h3><p><img src="/upload/2022/07/image-1658279294159.png" alt="image-1658279294159" /></p>]]>
                    </description>
                    <pubDate>Wed, 20 Jul 2022 09:09:35 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[【搬运】谷粒学院文档-05：新增和修改讲师接口]]>
                    </title>
                    <link>https://blog.qiql.net/archives/guli05</link>
                    <description>
                            <![CDATA[<h2 id="%E4%B8%80%E3%80%81%E8%87%AA%E5%8A%A8%E5%A1%AB%E5%85%85%E5%B0%81%E8%A3%85" tabindex="-1">一、自动填充封装</h2><h3 id="1%E3%80%81%E5%9C%A8service-base%E6%A8%A1%E5%9D%97%E4%B8%AD%E6%B7%BB%E5%8A%A0" tabindex="-1">1、在service-base模块中添加</h3><p>创建包handler，创建自动填充类 MyMetaObjectHandler</p><pre><code class="language-java">@Componentpublic class MyMetaObjectHandler implements MetaObjectHandler {    @Override    public void insertFill(MetaObject metaObject) {        this.setFieldValByName(&quot;gmtCreate&quot;, new Date(), metaObject);        this.setFieldValByName(&quot;gmtModified&quot;, new Date(), metaObject);    }    @Override    public void updateFill(MetaObject metaObject) {        this.setFieldValByName(&quot;gmtModified&quot;, new Date(), metaObject);    }}</code></pre><h3 id="2%E3%80%81%E5%9C%A8%E5%AE%9E%E4%BD%93%E7%B1%BB%E6%B7%BB%E5%8A%A0%E8%87%AA%E5%8A%A8%E5%A1%AB%E5%85%85%E6%B3%A8%E8%A7%A3" tabindex="-1">2、在实体类添加自动填充注解</h3><p><img src="/upload/2022/07/image-1658278894303.png" alt="image-1658278894303" /></p><h2 id="%E4%BA%8C%E3%80%81controller%E6%96%B9%E6%B3%95%E5%AE%9A%E4%B9%89" tabindex="-1">二、controller方法定义</h2><h3 id="1%E3%80%81%E6%96%B0%E5%A2%9E" tabindex="-1">1、新增</h3><pre><code class="language-java">@ApiOperation(value = &quot;新增讲师&quot;)@PostMappingpublic R save(        @ApiParam(name = &quot;teacher&quot;, value = &quot;讲师对象&quot;, required = true)        @RequestBody Teacher teacher){     teacherService.save(teacher);     return R.ok();}</code></pre><h3 id="2%E3%80%81%E6%A0%B9%E6%8D%AEid%E6%9F%A5%E8%AF%A2" tabindex="-1">2、根据id查询</h3><pre><code class="language-java">@ApiOperation(value = &quot;根据ID查询讲师&quot;)@GetMapping(&quot;{id}&quot;)public R getById(            @ApiParam(name = &quot;id&quot;, value = &quot;讲师ID&quot;, required = true)            @PathVariable String id){        Teacher teacher = teacherService.getById(id);        return R.ok().data(&quot;item&quot;, teacher);}</code></pre><h3 id="3%E3%80%81%E6%A0%B9%E6%8D%AEid%E4%BF%AE%E6%94%B9" tabindex="-1">3、根据id修改</h3><pre><code class="language-java">@ApiOperation(value = &quot;根据ID修改讲师&quot;)@PutMapping(&quot;{id}&quot;)public R updateById(    @ApiParam(name = &quot;id&quot;, value = &quot;讲师ID&quot;, required = true)    @PathVariable String id,    @ApiParam(name = &quot;teacher&quot;, value = &quot;讲师对象&quot;, required = true)    @RequestBody Teacher teacher){    teacher.setId(id);    teacherService.updateById(teacher);    return R.ok();}</code></pre>]]>
                    </description>
                    <pubDate>Wed, 20 Jul 2022 09:04:23 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[【搬运】谷粒学院文档-04：分页和条件查询接口开发]]>
                    </title>
                    <link>https://blog.qiql.net/archives/guli04</link>
                    <description>
                            <![CDATA[<h2 id="%E4%B8%80%E3%80%81%E5%88%86%E9%A1%B5" tabindex="-1">一、分页</h2><h3 id="1%E3%80%81mybatisplusconfig%E4%B8%AD%E9%85%8D%E7%BD%AE%E5%88%86%E9%A1%B5%E6%8F%92%E4%BB%B6" tabindex="-1">1、MyBatisPlusConfig中配置分页插件</h3><pre><code class="language-java">/** * 分页插件 */@Beanpublic PaginationInterceptor paginationInterceptor() {    return new PaginationInterceptor();}</code></pre><h3 id="2%E3%80%81%E5%88%86%E9%A1%B5controller%E6%96%B9%E6%B3%95" tabindex="-1">2、分页Controller方法</h3><p>TeacherAdminController中添加分页方法</p><pre><code class="language-java">@ApiOperation(value = &quot;分页讲师列表&quot;)@GetMapping(&quot;{page}/{limit}&quot;)public R pageList(    @ApiParam(name = &quot;page&quot;, value = &quot;当前页码&quot;, required = true)    @PathVariable Long page,    @ApiParam(name = &quot;limit&quot;, value = &quot;每页记录数&quot;, required = true)    @PathVariable Long limit){    Page&lt;Teacher&gt; pageParam = new Page&lt;&gt;(page, limit);    teacherService.page(pageParam, null);    List&lt;Teacher&gt; records = pageParam.getRecords();    long total = pageParam.getTotal();    return  R.ok().data(&quot;total&quot;, total).data(&quot;rows&quot;, records);}</code></pre><h3 id="3%E3%80%81swagger%E4%B8%AD%E6%B5%8B%E8%AF%95" tabindex="-1">3、Swagger中测试</h3><h2 id="%E4%BA%8C%E3%80%81%E6%9D%A1%E4%BB%B6%E6%9F%A5%E8%AF%A2" tabindex="-1">二、条件查询</h2><p>根据讲师名称name，讲师头衔level、讲师入驻时间gmt_create（时间段）查询</p><h3 id="1%E3%80%81%E5%88%9B%E5%BB%BA%E6%9F%A5%E8%AF%A2%E5%AF%B9%E8%B1%A1" tabindex="-1">1、创建查询对象</h3><p>创建com.guli.edu.query包，创建TeacherQuery.java查询对象</p><pre><code class="language-java">package com.guli.edu.query;@ApiModel(value = &quot;Teacher查询对象&quot;, description = &quot;讲师查询对象封装&quot;)@Datapublic class TeacherQuery implements Serializable {    private static final long serialVersionUID = 1L;    @ApiModelProperty(value = &quot;教师名称,模糊查询&quot;)    private String name;    @ApiModelProperty(value = &quot;头衔 1高级讲师 2首席讲师&quot;)    private Integer level;        @ApiModelProperty(value = &quot;查询开始时间&quot;, example = &quot;2019-01-01 10:10:10&quot;)    private String begin;//注意，这里使用的是String类型，前端传过来的数据无需进行类型转换    @ApiModelProperty(value = &quot;查询结束时间&quot;, example = &quot;2019-12-01 10:10:10&quot;)    private String end;}</code></pre><h3 id="2%E3%80%81service" tabindex="-1">2、service</h3><p>接口</p><pre><code class="language-java">package com.guli.edu.service;public interface TeacherService extends IService&lt;Teacher&gt; {    void pageQuery(Page&lt;Teacher&gt; pageParam, TeacherQuery teacherQuery);}</code></pre><p>实现</p><pre><code class="language-java">package com.guli.edu.service.impl;@Servicepublic class TeacherServiceImpl extends ServiceImpl&lt;TeacherMapper, Teacher&gt; implements TeacherService {    @Override    public void pageQuery(Page&lt;Teacher&gt; pageParam, TeacherQuery teacherQuery) {        QueryWrapper&lt;Teacher&gt; queryWrapper = new QueryWrapper&lt;&gt;();        queryWrapper.orderByAsc(&quot;sort&quot;);        if (teacherQuery == null){            baseMapper.selectPage(pageParam, queryWrapper);            return;        }        String name = teacherQuery.getName();        Integer level = teacherQuery.getLevel();        String begin = teacherQuery.getBegin();        String end = teacherQuery.getEnd();        if (!StringUtils.isEmpty(name)) {            queryWrapper.like(&quot;name&quot;, name);        }        if (!StringUtils.isEmpty(level) ) {            queryWrapper.eq(&quot;level&quot;, level);        }        if (!StringUtils.isEmpty(begin)) {            queryWrapper.ge(&quot;gmt_create&quot;, begin);        }        if (!StringUtils.isEmpty(end)) {            queryWrapper.le(&quot;gmt_create&quot;, end);        }        baseMapper.selectPage(pageParam, queryWrapper);    }}</code></pre><h3 id="3%E3%80%81controller" tabindex="-1">3、controller</h3><p>TeacherAdminController中修改 pageList方法：<br />增加参数TeacherQuery teacherQuery，非必选</p><pre><code class="language-java">@ApiOperation(value = &quot;分页讲师列表&quot;)@GetMapping(&quot;{page}/{limit}&quot;)public R pageQuery(    @ApiParam(name = &quot;page&quot;, value = &quot;当前页码&quot;, required = true)    @PathVariable Long page,    @ApiParam(name = &quot;limit&quot;, value = &quot;每页记录数&quot;, required = true)    @PathVariable Long limit,    @ApiParam(name = &quot;teacherQuery&quot;, value = &quot;查询对象&quot;, required = false)    TeacherQuery teacherQuery){    Page&lt;Teacher&gt; pageParam = new Page&lt;&gt;(page, limit);    teacherService.pageQuery(pageParam, teacherQuery);    List&lt;Teacher&gt; records = pageParam.getRecords();    long total = pageParam.getTotal();    return  R.ok().data(&quot;total&quot;, total).data(&quot;rows&quot;, records);}</code></pre><h3 id="4%E3%80%81swagger%E4%B8%AD%E6%B5%8B%E8%AF%95" tabindex="-1">4、Swagger中测试</h3>]]>
                    </description>
                    <pubDate>Wed, 20 Jul 2022 09:00:36 CST</pubDate>
                </item>
                <item>
                    <title>
                        <![CDATA[【搬运】谷粒学院文档-03：统一返回结果对象]]>
                    </title>
                    <link>https://blog.qiql.net/archives/guli03</link>
                    <description>
                            <![CDATA[<h2 id="%E4%B8%80%E3%80%81%E7%BB%9F%E4%B8%80%E8%BF%94%E5%9B%9E%E6%95%B0%E6%8D%AE%E6%A0%BC%E5%BC%8F" tabindex="-1">一、统一返回数据格式</h2><p>项目中我们会将响应封装成json返回，一般我们会将所有接口的数据格式统一， 使前端(iOS Android, Web)对数据的操作更一致、轻松。<br />一般情况下，统一返回数据格式没有固定的格式，只要能描述清楚返回的数据状态以及要返回的具体数据就可以。但是一般会包含状态码、返回消息、数据这几部分内容<br />例如，我们的系统要求返回的基本数据格式如下：<br />列表：</p><pre><code class="language-json">{  &quot;success&quot;: true,  &quot;code&quot;: 20000,  &quot;message&quot;: &quot;成功&quot;,  &quot;data&quot;: {    &quot;items&quot;: [      {        &quot;id&quot;: &quot;1&quot;,        &quot;name&quot;: &quot;刘德华&quot;,        &quot;intro&quot;: &quot;毕业于师范大学数学系，热爱教育事业，执教数学思维6年有余&quot;      }    ]  }}</code></pre><p>分页：</p><pre><code class="language-json">{  &quot;success&quot;: true,  &quot;code&quot;: 20000,  &quot;message&quot;: &quot;成功&quot;,  &quot;data&quot;: {    &quot;total&quot;: 17,    &quot;rows&quot;: [      {        &quot;id&quot;: &quot;1&quot;,        &quot;name&quot;: &quot;刘德华&quot;,        &quot;intro&quot;: &quot;毕业于师范大学数学系，热爱教育事业，执教数学思维6年有余&quot;      }    ]  }}</code></pre><p>没有返回数据：</p><pre><code class="language-json">{  &quot;success&quot;: true,  &quot;code&quot;: 20000,  &quot;message&quot;: &quot;成功&quot;,  &quot;data&quot;: {}}失败：{  &quot;success&quot;: false,  &quot;code&quot;: 20001,  &quot;message&quot;: &quot;失败&quot;,  &quot;data&quot;: {}}</code></pre><p>因此，我们定义统一结果</p><pre><code class="language-json">{  &quot;success&quot;: 布尔, //响应是否成功  &quot;code&quot;: 数字, //响应码  &quot;message&quot;: 字符串, //返回消息  &quot;data&quot;: HashMap //返回数据，放在键值对中}</code></pre><h2 id="%E4%BA%8C%E3%80%81%E5%88%9B%E5%BB%BA%E7%BB%9F%E4%B8%80%E7%BB%93%E6%9E%9C%E8%BF%94%E5%9B%9E%E7%B1%BB" tabindex="-1">二、创建统一结果返回类</h2><h3 id="1%E3%80%81%E5%9C%A8common%E6%A8%A1%E5%9D%97%E4%B8%8B%E5%88%9B%E5%BB%BA%E5%AD%90%E6%A8%A1%E5%9D%97common-utils" tabindex="-1">1、在common模块下创建子模块common-utils</h3><p><img src="/upload/2022/07/image-1658278577947.png" alt="image-1658278577947" /></p><h3 id="2%E3%80%81%E5%88%9B%E5%BB%BA%E6%8E%A5%E5%8F%A3%E5%AE%9A%E4%B9%89%E8%BF%94%E5%9B%9E%E7%A0%81" tabindex="-1">2、创建接口定义返回码</h3><p>创建包com.atguigu.commonutils，创建接口 ResultCode.java</p><pre><code class="language-java">package com.atguigu.commonutils;public interface ResultCode {    public static Integer SUCCESS = 20000;    public static Integer ERROR = 20001;}</code></pre><h3 id="4%E3%80%81%E5%88%9B%E5%BB%BA%E7%BB%93%E6%9E%9C%E7%B1%BB" tabindex="-1">4、创建结果类</h3><p>创建类 R.java</p><pre><code class="language-java">@Datapublic class R {    @ApiModelProperty(value = &quot;是否成功&quot;)    private Boolean success;    @ApiModelProperty(value = &quot;返回码&quot;)    private Integer code;    @ApiModelProperty(value = &quot;返回消息&quot;)    private String message;    @ApiModelProperty(value = &quot;返回数据&quot;)    private Map&lt;String, Object&gt; data = new HashMap&lt;String, Object&gt;();    private R(){}    public static R ok(){        R r = new R();        r.setSuccess(true);        r.setCode(ResultCode.SUCCESS);        r.setMessage(&quot;成功&quot;);        return r;    }    public static R error(){        R r = new R();        r.setSuccess(false);        r.setCode(ResultCode.ERROR);        r.setMessage(&quot;失败&quot;);        return r;    }    public R success(Boolean success){        this.setSuccess(success);        return this;    }    public R message(String message){        this.setMessage(message);        return this;    }    public R code(Integer code){        this.setCode(code);        return this;    }    public R data(String key, Object value){        this.data.put(key, value);        return this;    }    public R data(Map&lt;String, Object&gt; map){        this.setData(map);        return this;    }}</code></pre><h2 id="%E4%BA%8C%E3%80%81%E7%BB%9F%E4%B8%80%E8%BF%94%E5%9B%9E%E7%BB%93%E6%9E%9C%E4%BD%BF%E7%94%A8" tabindex="-1">二、统一返回结果使用</h2><h3 id="1%E3%80%81%E5%9C%A8service%E6%A8%A1%E5%9D%97%E4%B8%AD%E6%B7%BB%E5%8A%A0%E4%BE%9D%E8%B5%96" tabindex="-1">1、在service模块中添加依赖</h3><pre><code class="language-xml">&lt;dependency&gt;    &lt;groupId&gt;com.atguigu&lt;/groupId&gt;    &lt;artifactId&gt;common_utils&lt;/artifactId&gt;    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;&lt;/dependency&gt;</code></pre><h3 id="2%E3%80%81%E4%BF%AE%E6%94%B9controller%E4%B8%AD%E7%9A%84%E8%BF%94%E5%9B%9E%E7%BB%93%E6%9E%9C" tabindex="-1">2、修改Controller中的返回结果</h3><p>列表</p><pre><code class="language-java">@ApiOperation(value = &quot;所有讲师列表&quot;)@GetMappingpublic R list(){    List&lt;Teacher&gt; list = teacherService.list(null);    return R.ok().data(&quot;items&quot;, list);}</code></pre><p>删除</p><pre><code class="language-java">@ApiOperation(value = &quot;根据ID删除讲师&quot;)@DeleteMapping(&quot;{id}&quot;)public R removeById(    @ApiParam(name = &quot;id&quot;, value = &quot;讲师ID&quot;, required = true)    @PathVariable String id){    teacherService.removeById(id);    return R.ok();}</code></pre>]]>
                    </description>
                    <pubDate>Wed, 20 Jul 2022 08:57:56 CST</pubDate>
                </item>
    </channel>
</rss>