<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title><![CDATA[我的自留地【71hui.com】]]></title> 
<atom:link href="https://www.71hui.com/rss.php" rel="self" type="application/rss+xml" />
<description><![CDATA[我的自留地【71hui.com】]]></description>
<link>https://www.71hui.com/</link>
<language>zh-cn</language>
<generator>www.emlog.net</generator>
<item>
    <title>rsync用法详细说明</title>
    <link>https://www.71hui.com/post-41.html</link>
    <description><![CDATA[<h2>一、rsync 是什么（执行模型）</h2>
<p><strong>rsync = 文件同步工具（不是单纯拷贝）</strong></p>
<p>核心特点：</p>
<ul>
<li><strong>增量同步</strong>：只传输变化的数据块</li>
<li><strong>可断点续传</strong></li>
<li><strong>支持本地 / 远程</strong></li>
<li><strong>支持权限、时间戳、ACL、xattr</strong></li>
<li><strong>可 over SSH（默认）</strong></li>
</ul>
<p>执行流程（简化）：</p>
<ol>
<li>建立连接（本地 / SSH / daemon）</li>
<li>扫描源与目标文件列表</li>
<li>比较元数据 / 校验</li>
<li>传输差异数据</li>
<li>校验完成</li>
<li>正常退出</li>
</ol>
<hr />
<h2>二、基本语法结构</h2>
<pre><code class="language-bash">rsync [OPTION...] SRC [SRC...] DEST</code></pre>
<h3>最常见的三种形式</h3>
<h4>1️⃣ 本地 → 本地</h4>
<pre><code class="language-bash">rsync -av /src/ /dst/</code></pre>
<h4>2️⃣ 本地 → 远程（SSH）</h4>
<pre><code class="language-bash">rsync -av /src/ user@host:/dst/</code></pre>
<h4>3️⃣ 远程 → 本地</h4>
<pre><code class="language-bash">rsync -av user@host:/src/ /dst/</code></pre>
<hr />
<h2>三、最核心的一组参数（必须掌握）</h2>
<h3>1️⃣ <code>-a</code>（archive，生产必用）</h3>
<pre><code class="language-bash">-a  等价于：-rlptgoD</code></pre>
<p>包含：</p>
<table>
<thead>
<tr>
<th>参数</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>-r</code></td>
<td>递归目录</td>
</tr>
<tr>
<td><code>-l</code></td>
<td>保留软链接</td>
</tr>
<tr>
<td><code>-p</code></td>
<td>保留权限</td>
</tr>
<tr>
<td><code>-t</code></td>
<td>保留时间戳</td>
</tr>
<tr>
<td><code>-g</code></td>
<td>保留属组</td>
</tr>
<tr>
<td><code>-o</code></td>
<td>保留属主</td>
</tr>
<tr>
<td><code>-D</code></td>
<td>设备文件</td>
</tr>
</tbody>
</table>
<p>👉 <strong>生产同步几乎必带 <code>-a</code></strong></p>
<hr />
<h3>2️⃣ <code>-v</code>（verbose）</h3>
<pre><code class="language-bash">-v</code></pre>
<ul>
<li>显示执行过程</li>
<li>调试时非常重要</li>
</ul>
<hr />
<h3>3️⃣ <code>--progress / --info=progress2</code></h3>
<pre><code class="language-bash">--progress         # 单文件进度
--info=progress2   # 全局进度（推荐）</code></pre>
<p>👉 大文件 / 镜像同步<strong>强烈推荐</strong></p>
<hr />
<h2>四、文件传输控制参数（重点）</h2>
<h3>1️⃣ <code>--partial</code>（断点续传）</h3>
<pre><code class="language-bash">--partial</code></pre>
<ul>
<li>网络中断保留未完成文件</li>
<li>下次可继续</li>
</ul>
<hr />
<h3>2️⃣ <code>--append / --append-verify</code></h3>
<pre><code class="language-bash">--append
--append-verify   # 推荐</code></pre>
<ul>
<li>适合大文件（qcow2 / raw）</li>
<li>从已写入部分继续</li>
<li><code>--append-verify</code> 会校验安全性</li>
</ul>
<hr />
<h3>3️⃣ <code>--inplace</code>（谨慎）</h3>
<pre><code class="language-bash">--inplace</code></pre>
<ul>
<li>直接写目标文件</li>
<li>不生成临时文件</li>
<li><strong>文件损坏风险高</strong></li>
</ul>
<p>👉 仅在明确知道后果时使用</p>
<hr />
<h2>五、删除与镜像同步（非常危险的一类）</h2>
<h3>1️⃣ <code>--delete</code></h3>
<pre><code class="language-bash">--delete</code></pre>
<ul>
<li>删除目标端多余文件</li>
<li>用于“镜像同步”</li>
</ul>
<p>⚠️ <strong>误用可能导致严重数据丢失</strong></p>
<hr />
<h3>2️⃣ <code>--delete-delay</code>（更安全）</h3>
<pre><code class="language-bash">--delete-delay</code></pre>
<ul>
<li>同步完成后再删除</li>
</ul>
<hr />
<h2>六、过滤与排除规则</h2>
<h3>1️⃣ 排除文件</h3>
<pre><code class="language-bash">--exclude="*.log"
--exclude="/tmp/"</code></pre>
<hr />
<h3>2️⃣ 使用规则文件（推荐）</h3>
<pre><code class="language-bash">--exclude-from=exclude.txt</code></pre>
<p><code>exclude.txt</code> 示例：</p>
<pre><code class="language-text">*.log
tmp/
.cache/</code></pre>
<hr />
<h3>3️⃣ 包含规则</h3>
<pre><code class="language-bash">--include="*.conf"
--exclude="*"</code></pre>
<hr />
<h2>七、性能与网络控制</h2>
<h3>1️⃣ 限速</h3>
<pre><code class="language-bash">--bwlimit=10240   # KB/s</code></pre>
<hr />
<h3>2️⃣ 压缩</h3>
<pre><code class="language-bash">-z</code></pre>
<ul>
<li>低带宽适合</li>
<li>内网高速一般不需要</li>
</ul>
<hr />
<h3>3️⃣ 并发（rsync 本身不支持）</h3>
<ul>
<li>rsync 是<strong>单进程</strong></li>
<li>目录拆分 / GNU parallel 实现并发</li>
</ul>
<hr />
<h2>八、校验与一致性</h2>
<h3>1️⃣ 默认比较规则</h3>
<ul>
<li>size + mtime</li>
</ul>
<h3>2️⃣ 强制校验（慢）</h3>
<pre><code class="language-bash">-c</code></pre>
<ul>
<li>对文件做 checksum</li>
<li><strong>非常耗 IO</strong></li>
</ul>
<hr />
<h2>九、权限 / ACL / 扩展属性</h2>
<table>
<thead>
<tr>
<th>参数</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>-A</code></td>
<td>保留 ACL</td>
</tr>
<tr>
<td><code>-X</code></td>
<td>保留 xattr</td>
</tr>
<tr>
<td><code>--numeric-ids</code></td>
<td>不映射 uid/gid</td>
</tr>
</tbody>
</table>
<hr />
<h2>十、SSH 相关参数</h2>
<h3>指定 SSH 方式</h3>
<pre><code class="language-bash">-e "ssh -p 2222"</code></pre>
<hr />
<h3>常见组合</h3>
<pre><code class="language-bash">rsync -av -e "ssh -o StrictHostKeyChecking=no"</code></pre>
<hr />
<h2>十一、常见“生产级参数组合”</h2>
<h3>1️⃣ 普通目录同步（推荐模板）</h3>
<pre><code class="language-bash">rsync -av --info=progress2 /src/ /dst/</code></pre>
<hr />
<h3>2️⃣ 大文件 / 虚拟机镜像</h3>
<pre><code class="language-bash">rsync -av \
  --partial \
  --append-verify \
  --info=progress2 \
  /vm-images/ backup:/vm-images/</code></pre>
<hr />
<h3>3️⃣ 镜像备份（慎用）</h3>
<pre><code class="language-bash">rsync -av --delete /data/ /backup/data/</code></pre>
<hr />
<h2>十二、最容易踩的坑（非常重要）</h2>
<h3>1️⃣ 目录结尾 <code>/</code> 的区别</h3>
<pre><code class="language-bash">rsync src/ dst/   # 同步内容
rsync src  dst/   # 同步目录本身</code></pre>
<hr />
<h3>2️⃣ rsync 没有“全局超时”</h3>
<ul>
<li>网络卡死可能一直跑</li>
<li>外部用 <code>timeout</code> 或 Go context 控制</li>
</ul>
<hr />
<h3>3️⃣ 不要随便用 <code>--delete</code></h3>
<ul>
<li>先 dry-run</li>
</ul>
<pre><code class="language-bash">--dry-run</code></pre>
<hr />
<h2>十三、exit code（用于自动化判断）</h2>
<table>
<thead>
<tr>
<th>code</th>
<th>含义</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>成功</td>
</tr>
<tr>
<td>10</td>
<td>socket I/O error</td>
</tr>
<tr>
<td>12</td>
<td>协议错误（常见网络问题）</td>
</tr>
<tr>
<td>23</td>
<td>部分文件失败</td>
</tr>
<tr>
<td>24</td>
<td>文件消失</td>
</tr>
</tbody>
</table>
<hr />
<h2>十四、一句话总结</h2>
<blockquote>
<p><strong>rsync 是“长期可重复同步工具”，而不是 cp/scp 的简单替代；<br />
掌握 <code>-a</code>、<code>--partial</code>、<code>--append-verify</code>、<code>--delete</code> 的边界，是生产使用的关键。</strong></p>
</blockquote>
<p>如果你需要，我可以：</p>
<ul>
<li>给你 <strong>rsync 参数速查表（按场景）</strong></li>
<li>或直接结合 <strong>Go exec / 自动化 / 虚拟化镜像同步</strong> 给你一套标准规范</li>
</ul>]]></description>
    <pubDate>Sun, 04 Jan 2026 17:16:48 +0800</pubDate>
    <dc:creator>nicholas</dc:creator>
    <guid>https://www.71hui.com/post-41.html</guid>
</item>
<item>
    <title>go语言怎么使用Chromedp实现二维码登陆</title>
    <link>https://www.71hui.com/post-39.html</link>
    <description><![CDATA[<h3>1 Chromedp是什么</h3>
<p>chromedp是一个更快、更简单的Golang库用于调用支持Chrome DevTools协议的浏览器，同时不需要额外的依赖（例如Selenium和PhantomJS）</p>
<p>Chrome和Golang都与Google有着相当密切的关系，而Chrome DevTools其实就是Chrome浏览器按下F12之后的控制终端</p>
<h3>2 为什么不使用Selenium</h3>
<p>对于Golang开发来说，使用chromedp更为便捷，因为它仅仅需要Chrome浏览器而并不需要依赖ChromeDriver，省去了依赖问题，有助于自动化的构建和多平台架构的迁移</p>
<h3>3 文章解决了什么需求</h3>
<ul>
<li>
<p>如何使用chromedp进行二维码登陆</p>
</li>
<li>
<p>如何将二维码展示在无图形化的终端上（makiuchi-d/gozxing解码 skip2/ go-qrcode编码）</p>
</li>
<li>
<p>如何保存Cookies实现短时间免登陆</p>
</li>
</ul>
<p>网站会更新，文章不保证更新，请务必学会举一反三</p>
<h3>4.如何使用chromedp进行二维码登陆</h3>
<h4>4.1 安装chromedp</h4>
<ul>
<li>
<p>下载并安装Chrome浏览器</p>
</li>
<li>
<p>创建Golang项目，开启Go Module(在项目目录下使用终端输入go mod init)</p>
</li>
<li>
<p>在项目目录下使用终端输入：</p>
</li>
</ul>
<p>go get -u github.com/chromedp/chromedp<br />
（如果有依赖问题请删除-u）</p>
<h4>4.2 尝试打开网站</h4>
<p>（以金山文档https://account.wps.cn/为例）</p>
<p>1.重新设置chromedp使用&quot;有头&quot;的方式打开，以便于我们进行debug</p>
<pre><code>func main(){
    // chromdp依赖context上限传递参数
    ctx, _ := chromedp.NewExecAllocator(
        context.Background(),
        // 以默认配置的数组为基础，覆写headless参数
        // 当然也可以根据自己的需要进行修改，这个flag是浏览器的设置
        append(
            chromedp.DefaultExecAllocatorOptions[:],
            chromedp.Flag("headless", false),
        )...,
    )
}</code></pre>
<p>2.创建chromedp上下文对象</p>
<pre><code>func main(){
    // chromdp依赖context上限传递参数
    ...
    // 创建新的chromedp上下文对象，超时时间的设置不分先后
    // 注意第二个返回的参数是cancel()，只是我省略了
    ctx, _ = context.WithTimeout(ctx, 30*time.Second)
    ctx, _ = chromedp.NewContext(
        ctx,
        // 设置日志方法
        chromedp.WithLogf(log.Printf),
    )
    // 通常可以使用 defer cancel() 去取消
    // 但是在Windows环境下，我们希望程序能顺带关闭掉浏览器
    // 如果不希望浏览器关闭，使用cancel()方法即可
    // defer cancel()
    // defer chromedp.Cancel(ctx)
}</code></pre>
<p>3.执行自定义的任务</p>
<pre><code>func main(){
    // chromdp依赖context上限传递参数
    ...
    // 创建新的chromedp上下文对象，超时时间的设置不分先后
    // 注意第二个返回的参数是cancel()，只是我省略了
    ...
    // 执行我们自定义的任务 - myTasks函数在第4步
    if err := chromedp.Run(ctx, myTasks()); err != nil {
        log.Fatal(err)
        return
    }
}</code></pre>
<p>4.至此程序的初始化过程已经完成，接下来就是任务&mdash;&mdash;打开登陆页面</p>
<pre><code>// 自定义任务
func myTasks() chromedp.Tasks {
    return chromedp.Tasks{
        // 1. 打开金山文档的登陆界面
        chromedp.Navigate(loginURL),
    }
}</code></pre>
<p>5.运行一下程序，可以看到Chrome被打开，同时访问了我们指定的网站</p>
<p><img src="https://cache.yisu.com/upload/information/20220428/112/6512.png" alt="Image 1: go语言怎么使用Chromedp实现二维码登陆" /></p>
<h4>4.3 获取二维码（点击过程）</h4>
<p>1.需要点击微信登陆按钮，先找到按钮的选择器，右键按钮并在菜单中点击检查，然后可以看到按钮的元素</p>
<p><img src="https://cache.yisu.com/upload/information/20220428/112/6513.png" alt="Image 2: go语言怎么使用Chromedp实现二维码登陆" /></p>
<p>2.右键元素打开菜单找到copy下的copy selector，即获取到选择器</p>
<p><img src="https://cache.yisu.com/upload/information/20220428/112/6514.png" alt="Image 3: go语言怎么使用Chromedp实现二维码登陆" /></p>
<p>3.我们尝试点击微信登陆按钮，发现还需要点击一下确认，重复上述步骤获取确认按钮的选择器</p>
<p><img src="https://cache.yisu.com/upload/information/20220428/112/6515.png" alt="Image 4: go语言怎么使用Chromedp实现二维码登陆" /></p>
<p>4.用代码执行上述点击步骤</p>
<pre><code>// 自定义任务
func myTasks() chromedp.Tasks {
    return chromedp.Tasks{
        // 1. 打开金山文档的登陆界面
        chromedp.Navigate(loginURL),
        // 2. 点击微信登陆按钮
        // #wechat &gt; span:nth-child(2)
        chromedp.Click(`#wechat &gt; span:nth-child(2)`),
        // 3. 点击确认按钮
        // #dialog &gt; div.dialog-wrapper &gt; div &gt; div.dialog-footer &gt; div.dialog-footer-ok
        chromedp.Click(`#dialog &gt; div.dialog-wrapper &gt; div &gt; div.dialog-footer &gt; div.dialog-footer-ok`),
    }
}</code></pre>
<p>5.运行程序即可直达二维码展示界面</p>
<p><img src="https://cache.yisu.com/upload/information/20220428/112/6516.png" alt="Image 5: go语言怎么使用Chromedp实现二维码登陆" /></p>
<p>6.用同样的方式，获取二维码图片的选择器</p>
<p><img src="https://cache.yisu.com/upload/information/20220428/112/6517.png" alt="Image 6: go语言怎么使用Chromedp实现二维码登陆" /></p>
<p>7.用代码实现获取二维码</p>
<p>有两点需要注意，第一是二维码有加载过程，第二是二维码是元素渲染，</p>
<p>我们需要用截图的方式获取（也可以用js来获取对应的href并下载，但是为了照顾小白，选择最简单的）</p>
<pre><code>func myTasks() chromedp.Tasks {
    return chromedp.Tasks{
        // 1. 打开金山文档的登陆界面
        ...
        // 2. 点击微信登陆按钮
        ...
        // 3. 点击确认按钮
        ...
        // 4. 获取二维码
        // #wximport
        getCode(),
    }
}
func getCode() chromedp.ActionFunc {
    return func(ctx context.Context) (err error) {
        // 1. 用于存储图片的字节切片
        var code []byte
        // 2. 截图
        // 注意这里需要注明直接使用ID选择器来获取元素（chromedp.ByID）
        if err = chromedp.Screenshot(`#wximport`, &amp;code, chromedp.ByID).Do(ctx); err != nil {
            return
        }
        // 3. 保存文件
        if err = ioutil.WriteFile("code.png", code, 0755); err != nil {
            return
        }
        return
    }
}</code></pre>
<p>8.执行程序即可发现目录下已经存储了二维码图片文件，我们可以通过扫描此二维码进行登陆，与浏览器上扫描为同一种效果</p>
<p><img src="https://cache.yisu.com/upload/information/20220428/112/6518.png" alt="Image 7: go语言怎么使用Chromedp实现二维码登陆" /></p>
<h4>5. 如何将二维码展示在无图形化的终端上</h4>
<p>（与chromedp无关，属于额外内容）</p>
<p>1.在上述步骤中，我们已经获取了二维码，接下来我们需要在终端显示二维码，首先是解码，这里使用gozxing库</p>
<pre><code>func printQRCode(code []byte) (err error) {
    // 1. 因为我们的字节流是图像，所以我们需要先解码字节流
    img, _, err := image.Decode(bytes.NewReader(code))
    if err != nil {
        return
    }
    // 2. 然后使用gozxing库解码图片获取二进制位图
    bmp, err := gozxing.NewBinaryBitmapFromImage(img)
    if err != nil {
        return
    }
    // 3. 用二进制位图解码获取gozxing的二维码对象
    res, err := qrcode.NewQRCodeReader().Decode(bmp, nil)
    if err != nil {
        return
    }
    return
}</code></pre>
<p>2.然后重新编码来输出二维码到终端，这里使用go-qrcode库</p>
<pre><code>// 请注意import的库发生了重名
import (
    "github.com/makiuchi-d/gozxing"
    "github.com/makiuchi-d/gozxing/qrcode"
    goQrcode "github.com/skip2/go-qrcode"
)
func printQRCode(code []byte) (err error) {
    // 1. 因为我们的字节流是图像，所以我们需要先解码字节流
    ...
    // 2. 然后使用gozxing库解码图片获取二进制位图
    ...
    // 3. 用二进制位图解码获取gozxing的二维码对象
    ...
    // 4. 用结果来获取go-qrcode对象（注意这里我用了库的别名）
    qr, err := goQrcode.New(res.String(), goQrcode.High)
    if err != nil {
        return
    }
    // 5. 输出到标准输出流
    fmt.Println(qr.ToSmallString(false))
    return
}</code></pre>
<p>3.修改我们第二步的过程</p>
<pre><code>func getCode() chromedp.ActionFunc {
    return func(ctx context.Context) (err error) {
        // 1. 用于存储图片的字节切片
        ...
        // 2. 截图
        // 注意这里需要注明直接使用ID选择器来获取元素（chromedp.ByID）
        ...
        // 3. 把二维码输出到标准输出流
        if err = printQRCode(code); err != nil {
            return err
        }
        return
    }
}</code></pre>
<p>3.运行程序即可查看效果</p>
<p><img src="https://cache.yisu.com/upload/information/20220428/112/6519.png" alt="Image 8: go语言怎么使用Chromedp实现二维码登陆" /></p>
<h4>6. 如何保存Cookies实现短时间免登陆</h4>
<p>1.在上述过程中，我们可以通过二维码扫描登陆，网站会在登陆之后进行跳转，跳转后我们需要保存cookies来维持我们的登录状态，代码实现如下</p>
<pre><code>// 保存Cookies
func saveCookies() chromedp.ActionFunc {
    return func(ctx context.Context) (err error) {
        // 等待二维码登陆
        if err = chromedp.WaitVisible(`#app`, chromedp.ByID).Do(ctx); err != nil {
            return
        }
        // cookies的获取对应是在devTools的network面板中
        // 1. 获取cookies
        cookies, err := network.GetAllCookies().Do(ctx)
        if err != nil {
            return
        }
        // 2. 序列化
        cookiesData, err := network.GetAllCookiesReturns{Cookies: cookies}.MarshalJSON()
        if err != nil {
            return
        }
        // 3. 存储到临时文件
        if err = ioutil.WriteFile("cookies.tmp", cookiesData, 0755); err != nil {
            return
        }
        return
    }
}</code></pre>
<p>2.获取到Cookies之后，我们需要在程序运行时将Cookies从临时文件中加载到浏览器中</p>
<pre><code>// 加载Cookies
func loadCookies() chromedp.ActionFunc {
    return func(ctx context.Context) (err error) {
        // 如果cookies临时文件不存在则直接跳过
        if _, _err := os.Stat("cookies.tmp"); os.IsNotExist(_err) {
            return
        }
        // 如果存在则读取cookies的数据
        cookiesData, err := ioutil.ReadFile("cookies.tmp")
        if err != nil {
            return
        }
        // 反序列化
        cookiesParams := network.SetCookiesParams{}
        if err = cookiesParams.UnmarshalJSON(cookiesData); err != nil {
            return
        }
        // 设置cookies
        return network.SetCookies(cookiesParams.Cookies).Do(ctx)
    }
}</code></pre>
<p>3.通过上述两步我们已经可以保持登陆状态，然后我们需要检查一下是否成功，这里调用浏览器执行js脚本获取当前页面的网址，判断是否已经个人中心页面，如果为真，则停止操作</p>
<pre><code>// 检查是否登陆
func checkLoginStatus() chromedp.ActionFunc {
    return func(ctx context.Context) (err error) {
        var url string
        if err = chromedp.Evaluate(`window.location.href`, &amp;url).Do(ctx); err != nil {
            return
        }
        if strings.Contains(url, "https://account.wps.cn/usercenter/apps") {
            log.Println("已经使用cookies登陆")
            chromedp.Stop()
        }
        return
    }
}</code></pre>
<p>4.最终重新设置我们的浏览器任务即可</p>
<pre><code>// 自定义任务
func myTasks() chromedp.Tasks {
    return chromedp.Tasks{
        // 0. 加载cookies &lt;-- 变动
        loadCookies(),
        // 1. 打开金山文档的登陆界面
        ...
        // 判断一下是否已经登陆  &lt;-- 变动
        checkLoginStatus(),
        // 2. 点击微信登陆按钮
        // #wechat &gt; span:nth-child(2)
        ...
        // 3. 点击确认按钮
        // #dialog &gt; div.dialog-wrapper &gt; div &gt; div.dialog-footer &gt; div.dialog-footer-ok
        ...
        // 4. 获取二维码
        // #wximport
        ...
        // 5. 若二维码登录后，浏览器会自动跳转到用户信息页面  &lt;-- 变动
        saveCookies(),
    }
}</code></pre>
<p>5.我们使用已经登陆的cookies运行程序可以发现我们成功跳过登陆过程</p>
<p><img src="https://cache.yisu.com/upload/information/20220428/112/6520.png" alt="Image 9: go语言怎么使用Chromedp实现二维码登陆" /></p>]]></description>
    <pubDate>Mon, 24 Nov 2025 14:26:11 +0800</pubDate>
    <dc:creator>nicholas</dc:creator>
    <guid>https://www.71hui.com/post-39.html</guid>
</item>
<item>
    <title>我离不开的 10 个 Go 后端必备库（2025 实战版）</title>
    <link>https://www.71hui.com/post-38.html</link>
    <description><![CDATA[<p>这次的清单，不一样。<br />
不是那种“ChatGPT 生成的 Top 10 列表”，<br />
也不是“随便堆几个 GitHub 热门仓库”。</p>
<p>这些库——<br />
都是我在<strong>真实生产系统中踩过坑、救过火、扛过流量</strong>的武器。<br />
每一个都在我崩溃边缘的时候救过命。</p>
<p>如果今天要我从零搭一个 Go 后端，<br />
我只会带上这 10 个。</p>
<hr />
<h3>1️⃣ Gin —— 写 API 永远不后悔的框架</h3>
<p>Gin 几乎是我所有 Go 后端的骨架。<br />
它快、干净、不啰嗦，而且永远不会碍事。</p>
<pre><code>package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "pong"})
    })
    r.Run()
}</code></pre>
<p><strong>我一直用 Gin 的原因：</strong></p>
<ul>
<li>
<p>• ⚡ 路由极快</p>
</li>
<li>
<p>• 🧩 中间件机制丝滑</p>
</li>
<li>
<p>• 🧱 自带完善错误处理</p>
</li>
</ul>
<p>一句话：<strong>它让 Go 写 Web 变得愉快。</strong></p>
<hr />
<h3>2️⃣ GORM —— 那个你骂但一直离不开的 ORM</h3>
<p>别被键盘侠带跑偏。<br />
GORM 拯救了我上千行原生 SQL。<br />
功能够用、API 稳定、生态完善。</p>
<pre><code>import (
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

db, err := gorm.Open(postgres.Open("dsn"), &amp;gorm.Config{})

type User struct {
    ID   uint
    Name string
}

db.AutoMigrate(&amp;User{})
db.Create(&amp;User{Name: "Rishabh"})</code></pre>
<p><strong>我的原则：</strong></p>
<blockquote>
<p>95% 用 GORM，剩下 5% 用原生 SQL 解决“野生问题”。</p>
</blockquote>
<hr />
<h3>3️⃣ zap —— 真·高性能日志</h3>
<p>Go 自带的日志在生产环境就是灾难。<br />
Zap 来自 Uber 团队，速度快到离谱。</p>
<p>它是结构化日志、零内存拷贝、几乎无性能损耗。<br />
在高并发服务中，它的性能比 stdlib logger 快 <strong>约 10 倍</strong>。</p>
<p>当你凌晨 3 点看日志时，<br />
<strong>可读、可检索、不卡性能</strong>，就是救命。</p>
<hr />
<h3>4️⃣ Testify —— 测试不再痛苦</h3>
<p>测试不该是折磨。<br />
<code>testify</code> 让单元测试变得像写断言一样简单。</p>
<pre><code>import (
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestSum(t *testing.T) {
    result := 2 + 2
    assert.Equal(t, 4, result, "they should be equal")
}</code></pre>
<p>不再写 if/else 比较。<br />
一句断言，测试变清爽。</p>
<hr />
<h3>5️⃣ Cobra —— 真正能扩展的 CLI 框架</h3>
<p>几乎每个系统最终都需要 CLI。<br />
Cobra 就是 Go 世界的 “argparse++”。</p>
<pre><code>import (
    "github.com/spf13/cobra"
    "fmt"
)

var rootCmd = &amp;cobra.Command{
    Use:   "mycli",
    Short: "My custom CLI tool",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Hello from CLI!")
    },
}

func main() {
    rootCmd.Execute()
}</code></pre>
<p>它不仅能做命令，还能嵌套子命令、支持 flag、自动生成帮助。<br />
更棒的是，它和 <strong>Viper</strong> 天生一对。</p>
<hr />
<h3>6️⃣ Viper —— 配置管理不再“咬人”</h3>
<p>Go 的配置管理一直让人头大。<br />
Viper 支持 YAML、JSON、环境变量、命令行参数，<br />
一行代码全搞定。</p>
<pre><code>import (
    "github.com/spf13/viper"
    "fmt"
)

func main() {
    viper.SetConfigName("config")
    viper.AddConfigPath(".")
    viper.ReadInConfig()
    fmt.Println(viper.GetString("app.name"))
}</code></pre>
<p>搭配 Cobra 使用，<br />
CLI + 配置，完美闭环。</p>
<hr />
<h3>7️⃣ go-redis —— 稳如老狗的缓存客户端</h3>
<p>别用一堆封装 Redis 的“高级库”了。<br />
Go 官方的 <code>go-redis</code> 就足够强大。</p>
<pre><code>import (
    "github.com/redis/go-redis/v9"
    "context"
)

rdb := redis.NewClient(&amp;redis.Options{
    Addr: "localhost:6379",
})
rdb.Set(context.Background(), "foo", "bar", 0)</code></pre>
<p>稳定、性能好、社区活跃、API 明确。<br />
我用它处理缓存、消息队列、分布式锁，<br />
从没掉过链子。</p>
<hr />
<h3>8️⃣ Go Kit —— 真·微服务架构利器</h3>
<p>当项目不再是一个简单 API，而是多模块、多协议的系统时，<br />
<strong>Go Kit</strong> 是你最好的朋友。</p>
<p>它让你的服务从“HTTP-only”<br />
升级为<strong>可复用、可观测、可扩展的微服务架构</strong>。</p>
<pre><code>import (
    "github.com/go-kit/kit/endpoint"
    "context"
    "strings"
)

func makeUppercaseEndpoint() endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req := request.(string)
        return strings.ToUpper(req), nil
    }
}</code></pre>
<p>它不是框架，而是一个架构工具箱。<br />
我曾用它支撑每秒上万请求的服务。</p>
<hr />
<h3>9️⃣ Prometheus Client —— 度量从未如此轻松</h3>
<p>性能指标不是奢侈品，而是必要的生命线。<br />
Prometheus 的 Go 客户端让监控变得自然且零阻力。</p>
<pre><code>import (
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "net/http"
)

var ops = prometheus.NewCounterVec(
    prometheus.CounterOpts{Name: "http_requests_total", Help: "Total requests"},
    []string{"method"},
)

func main() {
    prometheus.MustRegister(ops)
    http.Handle("/metrics", promhttp.Handler())
    http.ListenAndServe(":2112", nil)
}</code></pre>
<p>轻松集成 Grafana，<br />
生产监控一目了然。</p>
<hr />
<h3>🔟 Wire —— 依赖注入，不用魔法也能优雅</h3>
<p>Wire 来自 Google，<br />
它能帮你生成依赖注入代码——<br />
<strong>没有运行时开销，没有反射，没有黑魔法。</strong></p>
<pre><code>import "github.com/google/wire"

type Foo struct{}
type Bar struct{}

func NewFoo() *Foo { return &amp;Foo{} }
func NewBar(f *Foo) *Bar { return &amp;Bar{} }

var Set = wire.NewSet(NewFoo, NewBar)</code></pre>
<p>生成完就是纯 Go 代码。<br />
可维护、可调试、无副作用。</p>
<p>用过一次，你就再也不想手动注入。</p>
<hr />
<h3>🧩 结语：别追潮流，用经过战火的东西</h3>
<p>这些库不是 GitHub 热榜上的新宠，<br />
而是经历了真实生产考验的老兵。</p>
<p>它们让我写的后端：</p>
<ul>
<li>
<p>• 快</p>
</li>
<li>
<p>• 稳</p>
</li>
<li>
<p>• 可维护</p>
</li>
<li>
<p>• 能在凌晨两点救场</p>
</li>
</ul>
<blockquote>
<p>少追热点，多用验证过的东西。</p>
</blockquote>]]></description>
    <pubDate>Wed, 12 Nov 2025 11:42:46 +0800</pubDate>
    <dc:creator>nicholas</dc:creator>
    <guid>https://www.71hui.com/post-38.html</guid>
</item>
<item>
    <title>老 Go 工程师用血汗换来的教训：关于工具链，你可能都用错了</title>
    <link>https://www.71hui.com/post-37.html</link>
    <description><![CDATA[<p>刚学 Go 的时候，一切都显得那么“干净”：<br />
<code>go run</code>、<code>go test</code>、<code>go build</code>——几条命令就能跑起来。</p>
<p>简单、高效、优雅。</p>
<p>但当你熬过凌晨两点的内存泄漏、排查过线上 P99 延迟飙升、<br />
或在生产环境里盯着看不懂的指标图发呆时，你会突然意识到：</p>
<blockquote>
<p>Go 真正的威力，不在语法，而在 <strong>工具链（Tooling）</strong>。</p>
</blockquote>
<p>而多数工程师，都是踩过坑之后才懂。</p>
<p>以下，就是每个高级 Go 工程师最终都会“痛过一遍”的工具经验。</p>
<hr />
<h3>1️⃣ pprof 不是选配，而是求生工具</h3>
<p>每个 Go 开发迟早都会遇到那种莫名其妙的性能波动：<br />
延迟飙升、内存暴涨、CPU 突然吃满。</p>
<p>新手的反应是猜：</p>
<blockquote>
<p>“是不是 GC？”<br />
“是不是 goroutine 太多？”</p>
</blockquote>
<p>老手的反应是测：</p>
<p><code>go tool pprof http://localhost:6060/debug/pprof/heap</code></p>
<p>pprof 不是 profiler（性能分析器）而已，<br />
它是 <strong>真相探测器</strong>。</p>
<p>它告诉你 CPU 时间花在哪、内存怎么分配、哪个锁在阻塞。</p>
<p>老工程师不会“感觉问题在哪”，<br />
他们用 flame graph 把问题直接点亮。</p>
<p><code>go tool pprof -http=:8080 cpu.out</code></p>
<p>📍 <strong>经验总结：</strong></p>
<blockquote>
<p>如果你解释不出系统的性能画像，你就是在盲飞。</p>
</blockquote>
<hr />
<h3>2️⃣ trace：当系统“不慢但不爽”时</h3>
<p>有时服务没卡 CPU，也没死锁，<br />
但就是“慢得莫名其妙”。</p>
<p>这时新手抓狂，老手会掏出：</p>
<p><code>go test -trace trace.out   go tool trace trace.out</code></p>
<p>Go 的 trace 工具可以展示：</p>
<ul>
<li>
<p>• 哪些 goroutine 处于“可运行但未运行”状态</p>
</li>
<li>
<p>• 调度器是如何把任务分配给线程的</p>
</li>
<li>
<p>• 哪些系统调用卡住了整个流程</p>
</li>
</ul>
<p>一句话：</p>
<blockquote>
<p>你能看到代码“为什么看起来没阻塞，但其实在等”。</p>
</blockquote>
<p>📍 <strong>教训：</strong><br />
每个老工程师都曾追了三天“虚幻瓶颈”才发现 trace 才是答案。</p>
<hr />
<h3>3️⃣ Delve (dlv)：实时显微镜</h3>
<p><code>fmt.Println()</code> 调试够用——<br />
直到你遇到竞争条件、指针错乱、异步逻辑崩溃。</p>
<p>那一刻你会感叹：</p>
<blockquote>
<p>没有 dlv，根本活不下去。</p>
</blockquote>
<p><code>dlv debug ./cmd/server</code></p>
<p>然后在交互界面输入：</p>
<p><code>(dlv) break main.main   (dlv) continue   (dlv) goroutines</code></p>
<p>你能立刻看到几百个 goroutine 的状态：<br />
有的在跑，有的在阻塞，有的在等通道。</p>
<p>📍 <strong>经验总结：</strong></p>
<blockquote>
<p>并发 bug 不在逻辑里，而在“时序”里。</p>
</blockquote>
<hr />
<h3>4️⃣ benchstat：帮你看清幻觉</h3>
<p>你改了点性能优化，重新跑 benchmark：<br />
“快了 5%！完美！”<br />
——真的吗？</p>
<p>其实这可能只是噪音。</p>
<p>老工程师会这样验证：</p>
<p><code>go test -bench=. -benchmem -count=10 &gt; old.txt   # 修改后   go test -bench=. -benchmem -count=10 &gt; new.txt   benchstat old.txt new.txt</code></p>
<p>它会告诉你改动是否<strong>真的有统计意义上的提升</strong>。</p>
<p>📍 <strong>经验总结：</strong></p>
<blockquote>
<p>性能直觉？往往是错觉。</p>
</blockquote>
<hr />
<h3>5️⃣ go vet + staticcheck：提前救你一命</h3>
<p>很多人以为 <code>go vet</code> 就是 lint。<br />
错，它是语义级分析。</p>
<p>能抓出：</p>
<ul>
<li>
<p>• 错误的 <code>Printf</code> 参数</p>
</li>
<li>
<p>• 循环里 defer 的坑</p>
</li>
<li>
<p>• 无效的 struct tag</p>
</li>
</ul>
<p>再加上 <code>staticcheck</code>：</p>
<p><code>staticcheck ./...</code></p>
<p>还能检测：</p>
<ul>
<li>
<p>• 泄漏的 goroutine</p>
</li>
<li>
<p>• 未检查的 error</p>
</li>
<li>
<p>• 已弃用的 API</p>
</li>
<li>
<p>• 无效赋值</p>
</li>
</ul>
<p>📍 <strong>经验总结：</strong></p>
<blockquote>
<p>几乎每次生产事故，<code>staticcheck</code> 都早已给过警告，只是没人看。</p>
</blockquote>
<hr />
<h3>6️⃣ Build Tags &amp; Toolchain：真正的工程之道</h3>
<p>高级工程师会用 <strong>build tag</strong> 区分环境：</p>
<p><code>// +build debug</code></p>
<p>构建时：</p>
<p><code>go build -tags debug</code></p>
<p>或者跨平台编译：</p>
<p><code>GOOS=linux GOARCH=amd64 go build -o app-linux</code></p>
<p>新手在 Docker 里折腾一下午，<br />
老手一句命令就搞定。</p>
<p>📍 <strong>经验总结：</strong></p>
<blockquote>
<p>善用工具链，你才配叫工程师。</p>
</blockquote>
<hr />
<h3>7️⃣ 在生产环境里用 pprof，但不作死</h3>
<p>每个人都学过在本地用 pprof。<br />
但在生产用？要小心。</p>
<p>正确姿势：</p>
<p><code>import _ "net/http/pprof"</code></p>
<p>在内网或加认证暴露端口：</p>
<p><code>curl -sK -O http://service:6060/debug/pprof/heap</code></p>
<p>📍 <strong>经验总结：</strong></p>
<blockquote>
<p>你只有一次机会把线上搞崩，然后才会学会“敬畏 profiling”。</p>
</blockquote>
<hr />
<h3>8️⃣ go build -gcflags=-m：内存分配全透明</h3>
<p>当你开始在意 GC 时，<br />
你会运行：</p>
<p><code>go build -gcflags=-m</code></p>
<p>输出可能是：</p>
<p><code>moved to heap: user   inlining call to log.Printf</code></p>
<p>这告诉你：</p>
<ul>
<li>
<p>• 哪些变量被分配到堆（代价大）</p>
</li>
<li>
<p>• 哪些函数被内联（更快）</p>
</li>
<li>
<p>• 哪些分配被优化掉</p>
</li>
</ul>
<p>📍 <strong>经验总结：</strong></p>
<blockquote>
<p>优化的目标不是“少分配”，而是“可预测的 GC 行为”。</p>
</blockquote>
<hr />
<h3>9️⃣ CI/CD 集成：高级工程师的“安静胜利”</h3>
<p>初级开发：</p>
<blockquote>
<p>“跑 go test 就完事了。”</p>
</blockquote>
<p>高级工程师：</p>
<ul>
<li>
<p>• CI 自动跑 <code>go vet</code> 和 <code>staticcheck</code></p>
</li>
<li>
<p>• 生成并上传覆盖率报告</p>
</li>
<li>
<p>• 预提交自动执行 <code>go mod tidy</code></p>
</li>
</ul>
<p>📍 <strong>经验总结：</strong></p>
<blockquote>
<p>工具不是“帮你”，而是“管你”，才能防止团队滑向混乱。</p>
</blockquote>
<hr />
<h3>🔟 go doc：你忽略的隐藏神器</h3>
<p>被严重低估的命令：</p>
<p><code>go doc net/http</code></p>
<p>它能离线展示 API 结构、方法说明、示例。</p>
<p>老工程师常常直接读标准库源码，<br />
因为那些代码——</p>
<blockquote>
<p>已在 Google 规模下被验证、测试、打磨十几年。</p>
</blockquote>]]></description>
    <pubDate>Wed, 12 Nov 2025 11:23:03 +0800</pubDate>
    <dc:creator>nicholas</dc:creator>
    <guid>https://www.71hui.com/post-37.html</guid>
</item>
<item>
    <title>golang常用框架</title>
    <link>https://www.71hui.com/post-36.html</link>
    <description><![CDATA[<ul>
<li>gin，Web框架，<a href="https://github.com/gin-gonic/gin">https://github.com/gin-gonic/gin</a></li>
<li>cobra，CLI交互，<a href="https://github.com/spf13/cobra">https://github.com/spf13/cobra</a></li>
<li>viper，应用配置，<a href="https://github.com/spf13/viper">https://github.com/spf13/viper</a></li>
<li>casbin，认证授权，<a href="https://github.com/hsluoyz/casbin">https://github.com/hsluoyz/casbin</a></li>
<li>go-jwt，JWT认证，<a href="https://github.com/pardnchiu/go-jwt">https://github.com/pardnchiu/go-jwt</a></li>
<li>logrus，日志处理，<a href="https://github.com/sirupsen/logrus">https://github.com/sirupsen/logrus</a></li>
<li>gorm，数据库对象关系映射，<a href="https://github.com/go-gorm/gorm">https://github.com/go-gorm/gorm</a></li>
<li>go-redis，Redis客户端，<a href="https://github.com/redis/go-redis">https://github.com/redis/go-redis</a></li>
<li>nsq，消息队列，<a href="https://github.com/nsqio/nsq">https://github.com/nsqio/nsq</a></li>
<li>wire，依赖注入，<a href="https://github.com/google/wire">https://github.com/google/wire</a></li>
<li>go-autowire，注解自动注入工具，<a href="https://github.com/Just-maple/go-autowire">https://github.com/Just-maple/go-autowire</a></li>
<li>websocket，ws协议支持，<a href="https://github.com/gorilla/websocket">https://github.com/gorilla/websocket</a></li>
<li>rpc，RPC协议支持，<a href="https://github.com/gorilla/rpc">https://github.com/gorilla/rpc</a></li>
<li>gomail，邮件发送，<a href="https://github.com/go-gomail/gomail">https://github.com/go-gomail/gomail</a></li>
</ul>]]></description>
    <pubDate>Mon, 10 Nov 2025 19:07:36 +0800</pubDate>
    <dc:creator>nicholas</dc:creator>
    <guid>https://www.71hui.com/post-36.html</guid>
</item>
<item>
    <title>如何部署一台 NFS 服务器</title>
    <link>https://www.71hui.com/post-35.html</link>
    <description><![CDATA[<h2>🧱 一、NFS 环境说明</h2>
<table>
<thead>
<tr>
<th>角色</th>
<th>示例主机名</th>
<th>示例IP</th>
<th>作用</th>
</tr>
</thead>
<tbody>
<tr>
<td>NFS 服务器</td>
<td>nfs-server</td>
<td>192.168.10.10</td>
<td>共享目录</td>
</tr>
<tr>
<td>NFS 客户端</td>
<td>app-node1</td>
<td>192.168.10.20</td>
<td>挂载目录</td>
</tr>
</tbody>
</table>
<hr />
<h2>🧰 二、NFS 服务器端安装与配置</h2>
<h3>1️⃣ 安装 NFS 服务</h3>
<pre><code class="language-bash"># CentOS / Rocky / openEuler
yum install -y nfs-utils

# Ubuntu / Debian
apt install -y nfs-kernel-server</code></pre>
<hr />
<h3>2️⃣ 创建共享目录</h3>
<pre><code class="language-bash">mkdir -p /data/nfs-share
chmod -R 777 /data/nfs-share
chown -R nobody:nogroup /data/nfs-share  # 或者 nfsnobody:nfsnobody</code></pre>
<hr />
<h3>3️⃣ 配置 NFS 导出文件 <code>/etc/exports</code></h3>
<p>编辑文件：</p>
<pre><code class="language-bash">vi /etc/exports</code></pre>
<p>示例内容：</p>
<pre><code class="language-bash">/data/nfs-share 192.168.10.0/24(rw,sync,no_root_squash)</code></pre>
<p>参数解释：</p>
<ul>
<li><code>rw</code>：可读写</li>
<li><code>sync</code>：同步写入磁盘（更安全）</li>
<li><code>no_root_squash</code>：允许客户端 root 用户保留 root 权限（K8s 环境必需）</li>
<li><code>192.168.10.0/24</code>：允许的网段（可换成单个IP）</li>
</ul>
<hr />
<h3>4️⃣ 启动并设置开机自启</h3>
<pre><code class="language-bash">systemctl enable nfs-server --now</code></pre>
<p>检查服务状态：</p>
<pre><code class="language-bash">systemctl status nfs-server</code></pre>
<hr />
<h3>5️⃣ 刷新配置并查看共享目录</h3>
<pre><code class="language-bash">exportfs -rav
showmount -e</code></pre>
<p>输出示例：</p>
<pre><code>Export list for nfs-server:
/data/nfs-share 192.168.10.0/24</code></pre>
<hr />
<h2>💻 三、客户端挂载 NFS 共享</h2>
<h3>1️⃣ 安装 NFS 客户端工具</h3>
<pre><code class="language-bash"># CentOS / openEuler
yum install -y nfs-utils

# Ubuntu / Debian
apt install -y nfs-common</code></pre>
<hr />
<h3>2️⃣ 创建挂载点并挂载</h3>
<pre><code class="language-bash">mkdir -p /mnt/nfs-share
mount -t nfs 192.168.10.10:/data/nfs-share /mnt/nfs-share</code></pre>
<p>验证：</p>
<pre><code class="language-bash">df -h | grep nfs</code></pre>
<p>测试：</p>
<pre><code class="language-bash">touch /mnt/nfs-share/test.txt
ls /mnt/nfs-share/</code></pre>
<p>如果在服务器 <code>/data/nfs-share/</code> 也能看到 <code>test.txt</code>，说明挂载成功 ✅</p>
<hr />
<h3>3️⃣ 开机自动挂载（可选）</h3>
<p>编辑 <code>/etc/fstab</code>：</p>
<pre><code class="language-bash">192.168.10.10:/data/nfs-share /mnt/nfs-share nfs defaults 0 0</code></pre>
<p>立即生效：</p>
<pre><code class="language-bash">mount -a</code></pre>
<hr />
<h2>🔒 四、防火墙与SELinux</h2>
<p>如果服务端启用了防火墙：</p>
<pre><code class="language-bash"># Firewalld
firewall-cmd --permanent --add-service=nfs
firewall-cmd --permanent --add-service=mountd
firewall-cmd --permanent --add-service=rpc-bind
firewall-cmd --reload</code></pre>
<p>如果启用了 SELinux，执行：</p>
<pre><code class="language-bash">setsebool -P nfs_export_all_rw on</code></pre>
<hr />
<h2>🧩 五、进阶用法（可选）</h2>
<h3>✅ 1. 查看客户端挂载情况</h3>
<pre><code class="language-bash">showmount -a</code></pre>
<h3>✅ 2. 取消挂载</h3>
<pre><code class="language-bash">umount /mnt/nfs-share</code></pre>
<h3>✅ 3. 手动刷新导出列表</h3>
<pre><code class="language-bash">exportfs -arv</code></pre>
<hr />
<h2>🧱 六、Kubernetes 场景（如果你是为 K8s 提供存储）</h2>
<p>可以直接把这台 NFS Server 暴露出来，创建一个 StorageClass 供 PVC 动态分配使用，例如：</p>
<pre><code class="language-yaml">apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-storage
provisioner: kubernetes.io/nfs
parameters:
  server: 192.168.10.10
  path: /data/nfs-share
reclaimPolicy: Retain</code></pre>
<hr />
<h2>✅ 七、总结命令速查表</h2>
<table>
<thead>
<tr>
<th>步骤</th>
<th>命令</th>
</tr>
</thead>
<tbody>
<tr>
<td>安装 NFS 服务</td>
<td><code>yum install -y nfs-utils</code></td>
</tr>
<tr>
<td>创建目录</td>
<td><code>mkdir -p /data/nfs-share</code></td>
</tr>
<tr>
<td>编辑导出配置</td>
<td><code>vi /etc/exports</code></td>
</tr>
<tr>
<td>启动服务</td>
<td><code>systemctl enable --now nfs-server</code></td>
</tr>
<tr>
<td>刷新共享</td>
<td><code>exportfs -rav</code></td>
</tr>
<tr>
<td>客户端挂载</td>
<td><code>mount -t nfs 192.168.10.10:/data/nfs-share /mnt/nfs-share</code></td>
</tr>
<tr>
<td>查看导出</td>
<td><code>showmount -e</code></td>
</tr>
</tbody>
</table>]]></description>
    <pubDate>Wed, 15 Oct 2025 17:10:52 +0800</pubDate>
    <dc:creator>nicholas</dc:creator>
    <guid>https://www.71hui.com/post-35.html</guid>
</item>
<item>
    <title>linux开启bbr提升网络速度</title>
    <link>https://www.71hui.com/post-34.html</link>
    <description><![CDATA[<p>BBR (Bottleneck Bandwidth and Round-trip propagation time) 是 Linux 上最经典的网络拥塞控制优化方式之一，可以显著提升 TCP 吞吐、降低延迟（尤其在跨地域或高丢包网络中）。</p>
<p>下面我教你 <strong>从检测 → 启用 → 验证</strong> 全流程（支持 CentOS / Ubuntu / Debian / openEuler / Kylin / 等主流发行版）。</p>
<hr />
<h2>🧠 一、查看当前内核版本</h2>
<p>BBR 需要 <strong>Linux 内核 ≥ 4.9</strong></p>
<pre><code class="language-bash">uname -r</code></pre>
<p>示例输出：</p>
<pre><code>5.10.0-23-amd64</code></pre>
<p>✅ 如果版本 ≥ 4.9，可以直接开启。<br />
❌ 如果低于 4.9，需要升级内核（下面会讲）。</p>
<hr />
<h2>🚀 二、开启 BBR（无需重启）</h2>
<p>执行以下命令启用：</p>
<pre><code class="language-bash">modprobe tcp_bbr</code></pre>
<p>然后编辑配置：</p>
<pre><code class="language-bash">echo "net.core.default_qdisc=fq" &gt;&gt; /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" &gt;&gt; /etc/sysctl.conf
sysctl -p</code></pre>
<p>立即加载：</p>
<pre><code class="language-bash">sysctl -w net.core.default_qdisc=fq
sysctl -w net.ipv4.tcp_congestion_control=bbr</code></pre>
<hr />
<h2>🔍 三、验证是否生效</h2>
<p>查看当前 TCP 拥塞算法：</p>
<pre><code class="language-bash">sysctl net.ipv4.tcp_congestion_control</code></pre>
<p>输出应为：</p>
<pre><code>net.ipv4.tcp_congestion_control = bbr</code></pre>
<p>再确认模块加载成功：</p>
<pre><code class="language-bash">lsmod | grep bbr</code></pre>
<p>输出类似：</p>
<pre><code>tcp_bbr                20480  3</code></pre>
<p>✅ 表示成功启用。</p>
<hr />
<h2>⚙️ 四、（如内核太旧）升级内核</h2>
<p>如果你的内核版本 &lt; 4.9，例如：</p>
<pre><code>3.10.0-1160.el7.x86_64</code></pre>
<p>这是 CentOS 7 默认版本，不支持 BBR。<br />
解决办法如下：</p>
<h3>▶ 1. 升级内核（CentOS 7）</h3>
<pre><code class="language-bash">yum install -y https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
yum --enablerepo=elrepo-kernel install kernel-ml -y
grub2-set-default 0
reboot</code></pre>
<p>重启后再执行：</p>
<pre><code class="language-bash">uname -r</code></pre>
<p>应显示 5.x 或更高版本。</p>
<p>然后重复上面 <strong>第二步 启用 BBR</strong>。</p>
<hr />
<h2>🐧 五、Ubuntu / Debian 启用 BBR</h2>
<h3>Ubuntu 18+（内核自带）</h3>
<pre><code class="language-bash">sudo modprobe tcp_bbr
echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p</code></pre>
<p>验证：</p>
<pre><code class="language-bash">sysctl net.ipv4.tcp_congestion_control</code></pre>
<hr />
<h2>🧩 六、openEuler / Kylin / 其他国产系统</h2>
<p>这些系统基于较新内核（通常 ≥ 4.19），可直接开启：</p>
<pre><code class="language-bash">modprobe tcp_bbr
sysctl -w net.core.default_qdisc=fq
sysctl -w net.ipv4.tcp_congestion_control=bbr
sysctl -p</code></pre>
<hr />
<h2>🧪 七、测试 BBR 是否有效</h2>
<p>可以使用 <code>iperf3</code>、<code>speedtest-cli</code> 或 <code>curl</code> 测试速度前后差异：</p>
<pre><code class="language-bash">iperf3 -c &lt;server_ip&gt; -P 4</code></pre>
<p>BBR 的典型特征是：</p>
<ul>
<li><strong>RTT 更低</strong></li>
<li><strong>丢包后恢复更快</strong></li>
<li><strong>带宽利用率提升显著</strong></li>
</ul>
<hr />
<h2>🧾 八、一键启用脚本（适用于所有系统）</h2>
<pre><code class="language-bash">#!/bin/bash
modprobe tcp_bbr
grep -q "tcp_congestion_control" /etc/sysctl.conf || {
cat &gt;&gt; /etc/sysctl.conf &lt;&lt;EOF
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr
EOF
}
sysctl -p
sysctl net.ipv4.tcp_congestion_control
lsmod | grep bbr</code></pre>
<p>保存为 <code>enable_bbr.sh</code> 后执行：</p>
<pre><code class="language-bash">bash enable_bbr.sh</code></pre>
<hr />
<p>是否方便告诉我：</p>
<ul>
<li>你的系统是哪个发行版？（比如 CentOS7、Ubuntu20.04、Kylin、openEuler）</li>
<li>是物理机、虚拟机还是 K8s 节点？</li>
</ul>
<p>我可以帮你输出一份<strong>定制命令</strong>（避免不兼容或 kernel 冲突）。</p>]]></description>
    <pubDate>Wed, 15 Oct 2025 16:29:56 +0800</pubDate>
    <dc:creator>nicholas</dc:creator>
    <guid>https://www.71hui.com/post-34.html</guid>
</item>
<item>
    <title>Linux 挂载数据盘操作指南</title>
    <link>https://www.71hui.com/post-33.html</link>
    <description><![CDATA[<h2>🧾 1. 查看未挂载的磁盘</h2>
<pre><code class="language-bash">lsblk</code></pre>
<p>示例输出：</p>
<pre><code class="language-text">NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda       8:0    0   40G  0 disk
├─sda1    8:1    0   40G  0 part /
sdb       8:16   0  100G  0 disk   &lt;-- 新数据盘</code></pre>
<blockquote>
<p>假设新盘为 <code>/dev/sdb</code></p>
</blockquote>
<hr />
<h2>🧾 2. 分区（若无分区）</h2>
<pre><code class="language-bash">fdisk /dev/sdb</code></pre>
<p>常见操作：</p>
<ul>
<li>输入 <code>n</code>：新建分区</li>
<li>输入 <code>p</code>：主分区</li>
<li>输入 <code>1</code>：分区号</li>
<li>回车（默认起始）</li>
<li>回车（默认结束）</li>
<li>输入 <code>w</code>：写入保存并退出</li>
</ul>
<p>之后应生成 <code>/dev/sdb1</code> 分区。</p>
<hr />
<h2>🧾 3. 格式化分区为 ext4</h2>
<pre><code class="language-bash">mkfs.ext4 /dev/sdb1</code></pre>
<blockquote>
<p>⚠️ 注意：此操作会清除该分区所有数据。</p>
</blockquote>
<hr />
<h2>🧾 4. 创建挂载目录并挂载</h2>
<pre><code class="language-bash">mkdir -p /data
mount /dev/sdb1 /data</code></pre>
<p>查看是否挂载成功：</p>
<pre><code class="language-bash">df -h | grep /data</code></pre>
<hr />
<h2>🧾 5. 设置开机自动挂载</h2>
<h3>✅ 方法一：使用设备路径（简单但不稳定）</h3>
<pre><code class="language-bash">echo '/dev/sdb1  /data  ext4  defaults  0 0' &gt;&gt; /etc/fstab</code></pre>
<h3>✅ 方法二：使用 UUID（推荐）</h3>
<ol>
<li>查看 UUID：</li>
</ol>
<pre><code class="language-bash">blkid /dev/sdb1</code></pre>
<p>示例输出：</p>
<pre><code class="language-text">/dev/sdb1: UUID="abcd-1234-5678-efgh" TYPE="ext4"</code></pre>
<ol start="2">
<li>编辑 <code>/etc/fstab</code> 添加：</li>
</ol>
<pre><code class="language-text">UUID=abcd-1234-5678-efgh /data ext4 defaults 0 0</code></pre>
<ol start="3">
<li>测试挂载是否成功：</li>
</ol>
<pre><code class="language-bash">umount /data
mount -a</code></pre>
<hr />
<h2>🧾 6. 设置目录权限（可选）</h2>
<pre><code class="language-bash">chown -R youruser:youruser /data
chmod 755 /data</code></pre>
<hr />
<h2>✅ 常见问题排查</h2>
<table>
<thead>
<tr>
<th>问题</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>找不到磁盘设备名</td>
<td>使用 <code>lsblk</code> 或 <code>fdisk -l</code> 检查是否存在新盘</td>
</tr>
<tr>
<td>重启后挂载丢失</td>
<td>检查 <code>/etc/fstab</code> 是否配置正确</td>
</tr>
<tr>
<td>格式化失败</td>
<td>检查该分区是否已挂载或正被使用</td>
</tr>
<tr>
<td>分区类型不兼容</td>
<td>使用 <code>mkfs.ext4</code> 替代其他文件系统尝试</td>
</tr>
</tbody>
</table>
<hr />
<h2>📌 命令小结</h2>
<pre><code class="language-bash">lsblk
fdisk /dev/sdb
mkfs.ext4 /dev/sdb1
mkdir -p /data
mount /dev/sdb1 /data
blkid /dev/sdb1
vim /etc/fstab
mount -a</code></pre>
<hr />
<h2>📎 附加建议</h2>
<ul>
<li>推荐使用 UUID 挂载，防止设备顺序变化导致挂载失败</li>
<li>如果系统为云服务器，添加磁盘后可能需要重启或执行 SCSI 扫描命令</li>
</ul>]]></description>
    <pubDate>Fri, 18 Jul 2025 09:46:01 +0800</pubDate>
    <dc:creator>nicholas</dc:creator>
    <guid>https://www.71hui.com/post-33.html</guid>
</item>
<item>
    <title>初识kubevirt</title>
    <link>https://www.71hui.com/post-32.html</link>
    <description><![CDATA[<h2>kubevirt的基础目录结构</h2>
<pre><code>kubevirt/
├── api/                   # CRD 的定义，包括 VirtualMachine、VirtualMachineInstance 等的 API 类型
│   ├── core/              # 核心 API 定义
│   └── generated/         # 自动生成的 API 代码
│
├── cmd/                   # 可执行文件入口，例如 virt-api、virt-controller、virt-handler 等
│   ├── virt-api/
│   ├── virt-controller/
│   ├── virt-handler/
│   └── virt-launcher/
│
├── pkg/                   # 核心逻辑代码，控制器、调度器、设备管理等
│   ├── api/               # 与 `api/` 相关的封装逻辑
│   ├── virt-api/          # 实现 kubevirt 的 API Server（服务暴露 CRD 接口）
│   ├── virt-controller/   # 实现虚拟机控制器（管理 VM 生命周期）
│   ├── virt-handler/      # 在每个节点上运行的守护进程，管理 VM 运行时
│   ├── virt-launcher/     # 创建和启动 QEMU 虚拟机的容器入口
│   ├── virt-chroot/       # 用于 chroot 隔离逻辑
│   ├── util/              # 各种工具函数和工具包
│   └── network/           # 虚拟机网络配置与支持代码
│
├── tests/                 # 测试用例，包括 e2e 测试、功能测试等
│   ├── e2e/               # 端到端测试
│   └── framework/         # 测试框架和公用逻辑
│
├── tools/                 # 构建、测试等相关工具脚本
│
├── hack/                  # 开发、CI、构建辅助脚本（例如生成代码、部署等）
│
├── manifests/             # Kubernetes 资源清单，安装/部署相关
│
├── cluster/               # 用于本地部署开发集群（例如使用 kind）
│
├── docs/                  # 项目文档
│
├── automation/            # GitHub Actions / CI 自动化流程定义
│
├── vendor/                # Go modules 的依赖（go mod vendor 后生成）
│
├── go.mod                 # Go modules 定义文件
├── go.sum
└── Makefile               # 构建入口
</code></pre>
<h2>关键组件说明：</h2>
<ul>
<li>
<p><strong>virt-api</strong>：为虚拟机相关的 CRD 提供 REST 接口。</p>
</li>
<li>
<p><strong>virt-controller</strong>：负责控制虚拟机的生命周期，比如创建、删除等。</p>
</li>
<li>
<p><strong>virt-handler</strong>：部署在每个节点上，负责和 libvirt/qemu/kvm 等交互，实际启动虚拟机。</p>
</li>
<li>
<p><strong>virt-launcher</strong>：每个虚拟机的启动容器，负责运行虚拟机进程。</p>
</li>
<li>
<p><strong>tests/e2e</strong>：用于验证 KubeVirt 在集群中部署和运行 VM 的行为</p>
</li>
</ul>]]></description>
    <pubDate>Mon, 19 May 2025 10:13:36 +0800</pubDate>
    <dc:creator>nicholas</dc:creator>
    <guid>https://www.71hui.com/post-32.html</guid>
</item>
<item>
    <title>一杯茶的时间理解MySQL三大范式（1NF、2NF、3NF）</title>
    <link>https://www.71hui.com/post-31.html</link>
    <description><![CDATA[<h2>一、第一范式（1NF）</h2>
<blockquote>
<p>定义：当关系模式R的所有属性都不能在分解为更基本的数据单位时，称R是满足第一范式的，简记为1NF。</p>
</blockquote>
<p><strong>简易理解：表中的每列的属性不可再分</strong></p>
<p><strong>举例说明：</strong></p>
<table>
<thead>
<tr>
<th>学号（主键）</th>
<th>姓名</th>
<th>性别</th>
<th>就读信息</th>
</tr>
</thead>
<tbody>
<tr>
<td>202001</td>
<td>张三</td>
<td>男</td>
<td>大一，计算机科学与技术</td>
</tr>
<tr>
<td>202002</td>
<td>李四</td>
<td>男</td>
<td>大二，网络工程</td>
</tr>
<tr>
<td>202003</td>
<td>王舞</td>
<td>女</td>
<td>大三，软件工程</td>
</tr>
</tbody>
</table>
<blockquote>
<p>上表中，可以看到（<strong>就读信息</strong>）这一列，其实可以分解为年级和专业，也因为（<strong>就读信息</strong>）这一属性可以再分，故不满足第一范式</p>
</blockquote>
<p><strong>修改：</strong></p>
<table>
<thead>
<tr>
<th>学号（主键）</th>
<th>姓名</th>
<th>性别</th>
<th>年级</th>
<th>专业</th>
</tr>
</thead>
<tbody>
<tr>
<td>202001</td>
<td>张三</td>
<td>男</td>
<td>大一</td>
<td>计算机科学与技术</td>
</tr>
<tr>
<td>202002</td>
<td>李四</td>
<td>男</td>
<td>大二</td>
<td>网络工程</td>
</tr>
<tr>
<td>202003</td>
<td>王舞</td>
<td>女</td>
<td>大三</td>
<td>软件工程</td>
</tr>
</tbody>
</table>
<p><strong>这样将（就读信息）拆分为（年级）、（专业）两个属性，便满足了第一范式（1NF）</strong></p>
<h2>二、第二范式（2NF）</h2>
<blockquote>
<p>定义：如果关系模式R满足第一范式，并且R得所有非主属性都完全依赖于R的每一个候选关键属性，称R满足第二范式，简记为2NF。</p>
</blockquote>
<p><strong>简易理解：在第一范式的基础上，表里的非主属性必须都依赖于主键（联合主键）</strong></p>
<p><strong>举例说明：</strong></p>
<table>
<thead>
<tr>
<th>学号（主键）</th>
<th>课程（主键）</th>
<th>教师姓名</th>
<th>成绩</th>
<th>学生姓名</th>
<th>专业</th>
</tr>
</thead>
<tbody>
<tr>
<td>202001</td>
<td>C语言程序设计</td>
<td>老张</td>
<td>80</td>
<td>张三</td>
<td>计算机科学与技术</td>
</tr>
<tr>
<td>202002</td>
<td>JAVA程序设计</td>
<td>老李</td>
<td>87</td>
<td>李四</td>
<td>网络工程</td>
</tr>
<tr>
<td>202003</td>
<td>数据结构</td>
<td>老王</td>
<td>90</td>
<td>王舞</td>
<td>软件工程</td>
</tr>
</tbody>
</table>
<blockquote>
<p>上表中，可以看到（教师姓名、成绩）两个属性都依赖于（学号）和（课程），但是（学生姓名、专业）这一属性却只依赖于（学号），不依赖于（课程），即：<strong>只需要知道（学号）便可得知（学生姓名、专业）</strong>。所以，导致非主属性（学生姓名、专业）不完全依赖于主键（学号、课程），故不符合第二范式。</p>
</blockquote>
<p><strong>修改：</strong></p>
<table>
<thead>
<tr>
<th>学号（主键）</th>
<th>课程（主键）</th>
<th>教师姓名</th>
<th>成绩</th>
</tr>
</thead>
<tbody>
<tr>
<td>202001</td>
<td>C语言程序设计</td>
<td>老张</td>
<td>80</td>
</tr>
<tr>
<td>202002</td>
<td>JAVA程序设计</td>
<td>老李</td>
<td>87</td>
</tr>
<tr>
<td>202003</td>
<td>数据结构</td>
<td>老王</td>
<td>90</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>学号（主键）</th>
<th>学生姓名</th>
<th>专业</th>
</tr>
</thead>
<tbody>
<tr>
<td>202001</td>
<td>张三</td>
<td>计算机科学与技术</td>
</tr>
<tr>
<td>202002</td>
<td>李四</td>
<td>网络工程</td>
</tr>
<tr>
<td>202003</td>
<td>王舞</td>
<td>软件工程</td>
</tr>
</tbody>
</table>
<p><strong>将原表拆分为两张表，即可实现让它的非主属性都完全依赖于主键，即可符合第二范式（2NF）</strong></p>
<h2>三、第三范式</h2>
<blockquote>
<p>定义：设R是一个满足第一范式条件的关系模式，X是R的任意属性集，如果X非传递依赖于R的任意一个候选关键字，称R满足第三范式，简记为3NF。</p>
</blockquote>
<p><strong>简单理解：在第二范式的基础上，表中的非主属性不可以存在依赖关系</strong> <strong>举例说明：</strong></p>
<table>
<thead>
<tr>
<th>学号（主键）</th>
<th>姓名</th>
<th>性别</th>
<th>年级</th>
<th>专业</th>
<th>班主任姓名</th>
<th>班主任性别</th>
<th>班主任年龄</th>
</tr>
</thead>
<tbody>
<tr>
<td>202001</td>
<td>张三</td>
<td>男</td>
<td>大一</td>
<td>计算机科学与技术</td>
<td>老张</td>
<td>男</td>
<td>33</td>
</tr>
<tr>
<td>202002</td>
<td>李四</td>
<td>男</td>
<td>大二</td>
<td>网络工程</td>
<td>老李</td>
<td>男</td>
<td>34</td>
</tr>
<tr>
<td>202003</td>
<td>王舞</td>
<td>女</td>
<td>大三</td>
<td>软件工程</td>
<td>老王</td>
<td>男</td>
<td>35</td>
</tr>
</tbody>
</table>
<blockquote>
<p>上表中，我们可以看到表中的非主属性都依赖于（学号），满足了第二范式。 但是，（班主任性别、年龄）这两个属性是直接依赖于（班主任姓名）这一属性的，与（学号）属于间接依赖。 这就导致了表中的非主属性存在着依赖关系，不符合第三范式。</p>
</blockquote>
<p><strong>修改：</strong></p>
<table>
<thead>
<tr>
<th>学号（主键）</th>
<th>姓名</th>
<th>性别</th>
<th>年级</th>
<th>专业</th>
</tr>
</thead>
<tbody>
<tr>
<td>202001</td>
<td>张三</td>
<td>男</td>
<td>大一</td>
<td>计算机科学与技术</td>
</tr>
<tr>
<td>202002</td>
<td>李四</td>
<td>男</td>
<td>大二</td>
<td>网络工程</td>
</tr>
<tr>
<td>202003</td>
<td>王舞</td>
<td>女</td>
<td>大三</td>
<td>软件工程</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>班主任姓名（主键）</th>
<th>班主任性别</th>
<th>班主任年龄</th>
</tr>
</thead>
<tbody>
<tr>
<td>老张</td>
<td>男</td>
<td>33</td>
</tr>
<tr>
<td>老李</td>
<td>男</td>
<td>34</td>
</tr>
<tr>
<td>老王</td>
<td>男</td>
<td>35</td>
</tr>
</tbody>
</table>
<p><strong>将原表拆分为两张表：学生表与班主任表，在满足第二范式的同时，表中的非主属性都不存在着依赖关系，故符合第三范式</strong></p>
<h2>总结</h2>
<blockquote>
<p>个人认为，三大范式（1NF、2NF、3NF），其实是将一张表不停拆分、细化的过程，但在实际情况中，不停的对表进行拆分，在执行查询操作时就需要进行联表查询，所以，拆分的表多了，也会导致查询的效率也会大打折扣。所以一般最多拆成3张表，并且，为了查询方便，可能也会故意的将一些与主键无关的属性放在表中（<strong>在设计过程中，三大范式不是需要严格遵守的，只是提供了一种设计规范，供人参考使用</strong>）</p>
</blockquote>]]></description>
    <pubDate>Thu, 15 May 2025 23:56:41 +0800</pubDate>
    <dc:creator>nicholas</dc:creator>
    <guid>https://www.71hui.com/post-31.html</guid>
</item></channel>
</rss>