<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
  xmlns:atom="http://www.w3.org/2005/Atom"
  xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Moreality&#39;s Blog</title>
    <link>https://moreality.net/</link>
    
    <image>
      <url>https://youpai.roccoshi.top/img/avatar.jpg</url>
      <title>Moreality&#39;s Blog</title>
      <link>https://moreality.net/</link>
    </image>
    
    <atom:link href="https://moreality.net/rss2.xml" rel="self" type="application/rss+xml"/>
    
    <description>Welcome to my blog, I&#39;m Moreality, this a place where I focus on creation and sharing. 
 这里是一个个人博客, 专注于分享和创作.</description>
    <pubDate>Sun, 28 Jun 2026 12:58:47 GMT</pubDate>
    <generator>http://hexo.io/</generator>
    
    <item>
      <title>旅行日记·葫芦岛-兴城</title>
      <link>https://moreality.net/posts/34210/</link>
      <guid>https://moreality.net/posts/34210/</guid>
      <pubDate>Sun, 28 Jun 2026 12:31:57 GMT</pubDate>
      
      <description>冷冷清清的一座海滨县城</description>
      
      
      
      <content:encoded><![CDATA[<p>出了兴城西站，打开滴滴却打不到一辆车 —— 搜了一下小红书才知道，这和张家口一样，是滴滴还没有入住的区域，网约车在这里被本地出租车举报导致几乎没人开网约车，大家出了高铁站，只能乘坐 1 小时一趟的 K1 列车或者左转排队等待出租车。</p><p>兴城的海，没有秦皇岛、青岛那种成熟海滨城市的精致感。</p><p>海岸线景区附近也有大量的破败的房屋和废弃的港口、渔船、酒店、烂尾楼，带着一点褪色和沧桑的气息。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202606282036319.JPG" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202606282037270.JPG" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202606282037208.JPG" alt=""></div></div></div><p>兴城的游客也非常少，6 月的周末、晴天的下午，从龙回头到和平广场长长的一段栈道看不到多少人，到了和平广场更是显得空旷，下午三四点广场上几乎看不见人，旁边的高楼也分不清到底是住人了还是烂尾了，因为从外墙上看不出任何居住痕迹。</p><p>来这里的第二天，住进了一家 27 层的民宿，对面就是一望无际的渤海，今天空气通透度不高，连地平线都看不清，但是高处的视野和风景还是很夯的。</p><p><img src="https://youpai.roccoshi.top/img/202606282050196.JPG" alt=""></p><p>晚上的时候，只能吃泡面加饼干，不敢想象居然在非滑雪场景下，出门旅游住酒店吃这种东西当晚餐，因为周围实在是找不到任何一家可以吃的店子，包括外卖基本都在 6km 之外。</p><p>天黑了才发现，30多层的小区，楼下看不到海的楼层几乎都是黑灯瞎火的，而高层已经都被改成了民宿。</p><p><img src="https://youpai.roccoshi.top/img/202606282054594.png" alt=""></p><p>回北京的直达动车只有上午十点的一趟 D506，早早的起来坐 K1 公交车去兴城西站，发现司机还是来的时候的那位大叔，动车的乘务员说，下个月 D506 就变成 G 字头了，车票贵了几十块，速度还是不变。</p><p><img src="https://youpai.roccoshi.top/img/202606282043914.png" alt=""></p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/%E7%94%9F%E6%B4%BB/">生活</category>
      
      <category domain="https://moreality.net/tags/%E6%97%85%E8%A1%8C/">旅行</category>
      
      <category domain="https://moreality.net/tags/%E5%85%B4%E5%9F%8E/">兴城</category>
      
      
      <comments>https://moreality.net/posts/34210/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>哪吒监控安全事件 - 从中招到复盘</title>
      <link>https://moreality.net/posts/62186/</link>
      <guid>https://moreality.net/posts/62186/</guid>
      <pubDate>Sun, 21 Jun 2026 05:10:00 GMT</pubDate>
      
      <description>近期哪吒监控安全事件提醒我们，带远程执行能力的监控面板一旦暴露公网，风险远不止信息泄露。</description>
      
      
      
      <content:encoded><![CDATA[<p>这几天 VPS 圈子里，哪吒监控（Nezha）基本被刷屏了。</p><p>有人说 CPU 被打满，有人发现机器里多了陌生 SSH 公钥，有人被云厂商发 abuse，还有人发现自己的小鸡被拿去挖矿、做代理、打流量。NodeSeek 上更直观，列表里隔几条就是一个哪吒相关帖子：求助、复盘、检测脚本、清理脚本、替代方案，什么都有。</p><p>我一开始也以为这只是“面板密码太弱”或者“SSH 被爆破”。后来越查越觉得不对。很多机器并没有对应时间段的 SSH 成功登录记录，但后门却几乎同时落地。这个现象很说明问题：攻击者很可能不是一台一台登录，而是先拿到了哪吒 Dashboard，然后通过 Agent 批量下发命令。</p><p>换句话说，平时的“集中管理”，出事时就变成了“集中沦陷”。</p><p>这篇简单记一下这次事件。不会写太深的漏洞分析，主要讲清楚几件事：为什么哪吒不是普通探针，漏洞大概怎么串起来，社区里看到了哪些中招痕迹，以及现在还在用的人该先做什么。</p><h2 id="哪吒不是只读探针">哪吒不是只读探针</h2><p>很多人装哪吒，是为了看一个漂亮的服务器状态页。CPU、内存、流量、在线状态，一屏看完，确实舒服。</p><p>但哪吒不是只读工具。Dashboard 和 Agent 之间有控制通道，面板里能做不少运维动作：</p><ul><li>Web 终端</li><li>文件管理</li><li>计划任务</li><li>批量命令执行</li><li>Agent 更新和配置下发</li></ul><p>这些功能平时很方便。问题是，权限也很重。</p><p>如果 Dashboard 被接管，攻击者拿到的不是“看看监控”的权限，而是“让所有在线 Agent 执行命令”的权限。Agent 又通常是 root 跑的，所以面板里下发的一条命令，到了机器上可能就是 root 权限执行。</p><p>这也是这次事件影响范围容易扩大的原因。</p><h2 id="公开漏洞">公开漏洞</h2><p>这次不是只有社区传言。哪吒官方已经有两个相关的 GitHub Security Advisory。</p><p>第一个是 <code>GHSA-5c25-7vpj-9mqh</code>：<br><a href="https://github.com/nezhahq/nezha/security/advisories/GHSA-5c25-7vpj-9mqh">https://github.com/nezhahq/nezha/security/advisories/GHSA-5c25-7vpj-9mqh</a></p><p>这个漏洞影响 <code>&lt; 2.0.13</code>。问题出在 Dashboard 的前端 fallback 路由处理上。旧版本判断 <code>/dashboard</code> 路径时不够严，攻击者可以构造类似这样的请求：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">/dashboard../data/config.yaml<br></code></pre></td></tr></table></figure><p>默认部署下，这可能读到 <code>data/config.yaml</code>。里面有 <code>jwt_secret_key</code>、<code>agent_secret_key</code> 这些敏感配置。</p><p>其中 <code>jwt_secret_key</code> 特别危险。旧版本里，攻击者拿到它之后，就可能自己签一个合法的 Dashboard 登录态。也就是说，强密码还在，但密码这关已经被绕过去了。</p><p>第二个是 <code>GHSA-99gv-2m7h-3hh9</code>：<br><a href="https://github.com/nezhahq/nezha/security/advisories/GHSA-99gv-2m7h-3hh9">https://github.com/nezhahq/nezha/security/advisories/GHSA-99gv-2m7h-3hh9</a></p><p>这个漏洞影响 <code>&gt;= 1.4.0, &lt; 2.0.8</code>。它和计划任务权限有关。低权限用户可能通过计划任务接口创建命令，并广播到所有服务器。Agent 收到后会执行，于是就变成了跨主机的远程命令执行。</p><p>把这两件事放在一起，大概链路就是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs text">Dashboard 暴露公网<br>-&gt; 读取配置文件<br>-&gt; 拿到 JWT 密钥<br>-&gt; 伪造登录态<br>-&gt; 进入 Dashboard<br>-&gt; 创建恶意计划任务<br>-&gt; 所有在线 Agent 执行命令<br></code></pre></td></tr></table></figure><p>所以很多人看到的是“所有机器一起出事”。这不是巧合。一个 Dashboard 后面挂了多少 Agent，攻击者就可能一次打到多少台。</p><h2 id="时间线">时间线</h2><p>这次比较容易让人误解的一点是：漏洞不是今天才修，但爆发却集中在这几天。</p><p>按公开资料和社区反馈，大概是这样：</p><ul><li>2026-05-17：官方发布 <code>GHSA-99gv-2m7h-3hh9</code>，计划任务权限问题被披露，修复版本是 <code>2.0.8</code>。</li><li>2026-05-31：官方发布 <code>GHSA-5c25-7vpj-9mqh</code>，路径穿越问题被披露，修复版本是 <code>2.0.13</code>。</li><li>2026-06 上旬：社区里已经有人反馈异常 Agent、异常任务、陌生 SSH 公钥。</li><li>2026-06 中下旬：更多 VPS 用户开始集中发现 CPU 异常、挖矿、流量代理、随机后缀 Agent 服务。</li><li>2026-06-20 前后：哪吒主控最新版本已经到 <code>v2.2.6</code>，Agent 最新是 <code>v2.2.2</code>。</li></ul><p>这说明修复版本早就有了，但暴露在公网的旧面板并不会自动消失。攻击者可能早就扫过一轮，真正变现的时候，大家才看到 CPU 和流量异常。</p><p>“养鱼”和“收网”这种说法听起来很戏剧化，我不想把它写成定论。能确认的是：漏洞公开存在，修复版本已经发布，社区里确实出现了批量中招。</p><h2 id="为什么-SSH-强密码和-UFW-没挡住">为什么 SSH 强密码和 UFW 没挡住</h2><p>很多人第一反应是：我 SSH 密码很强，甚至只开了密钥登录，为什么还会中？</p><p>因为攻击可能根本没走 SSH。</p><p>如果攻击者控制的是 Dashboard，那被控机器只要能主动连上 Dashboard 就行。UFW 通常拦的是入站连接，而 Agent 连 Dashboard 是出站连接。</p><p>也就是说，攻击路径更像这样：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">攻击者 -&gt; Dashboard -&gt; Agent -&gt; 服务器本机执行命令<br></code></pre></td></tr></table></figure><p>而不是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">攻击者 -&gt; SSH 登录每台服务器<br></code></pre></td></tr></table></figure><p>所以 SSH 强密码、改端口、禁密码登录，这些都很重要，但它们防的是 SSH 这条路。这次的问题大概率是从控制面进来的。</p><h2 id="社区里看到的中招痕迹">社区里看到的中招痕迹</h2><p>不同人机器上的载荷不完全一样。下面这些是社区复盘里反复出现的现象，适合拿来做排查方向，但不要当成完整 IOC。</p><p>有人从中招的 SQLite 数据库里提取到过恶意计划任务，内容包括：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs text">写入 /root/.ssh/authorized_keys<br>curl / wget 下载远程脚本并执行<br>拉取挖矿或代理变现程序<br></code></pre></td></tr></table></figure><p>也有人发现机器上多了随机后缀的哪吒服务：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs text">nezha-agent-xxxxxxx.service<br>nazha-agent-xxxxxxx.service<br></code></pre></td></tr></table></figure><p>对应的配置文件可能长这样：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs text">/opt/nezha/agent/config-xxxxx.yml<br></code></pre></td></tr></table></figure><p>这种做法会增加清理难度。只停掉原本的 <code>nezha-agent.service</code> 不够，随机后缀的服务可能还在运行。</p><p>还有一些机器出现了挖矿和流量套利程序，比如 XMRig、c3pool，或者一些代理/带宽变现容器。这类东西不一定马上把业务搞挂，但会悄悄吃 CPU、带宽和云厂商信用。</p><p>更麻烦的是内存驻留进程。有些复盘提到伪装成 <code>kworker</code> 的用户态进程、<code>memfd</code> 内存马、执行路径显示为 <code>(deleted)</code> 的进程。查文件不一定能看到，得看 <code>/proc</code>、进程树和网络连接。</p><p>我觉得比较实用的排查顺序是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs text">1. 查 Nezha 计划任务和 Dashboard 数据库<br>2. 查 systemd 服务，尤其是随机后缀服务<br>3. 查 crontab 和 /etc/cron*<br>4. 查 authorized_keys 是否多出陌生公钥<br>5. 查高 CPU 进程、挖矿进程和代理程序<br>6. 查 /tmp、/dev/shm、/opt、/root 下近期新增文件<br>7. 查 memfd、deleted exe、伪装 kworker 进程<br>8. 查 Docker 容器和镜像<br></code></pre></td></tr></table></figure><p>一键清理脚本可以止血，但不能证明系统已经可信。机器被 root 权限控制过之后，更稳妥的处理方式仍然是重装系统，然后轮换所有可能泄露的凭据。</p><h2 id="现在该做什么">现在该做什么</h2><p>如果你用过受影响版本，或者 Dashboard 曾经暴露在公网，我会按这个顺序处理。</p><ol><li><p>堵住入口</p><p>先升级 Dashboard，不要让面板直接暴露公网。访问入口可以收敛到 WireGuard、Tailscale、Cloudflare Access、内网反代或 IP 白名单。随后轮换 <code>jwt_secret_key</code>、<code>agent_secret_key</code>，清理旧登录态和旧 token。</p></li><li><p>检查控制面</p><p>重点看 Nezha 计划任务、Dashboard 数据库里的异常任务、通知配置、Webhook、OAuth 设置，以及是否存在陌生用户或异常登录痕迹。</p></li><li><p>逐台检查节点</p><p>查随机后缀的 <code>nezha-agent-*.service</code>，查 <code>/opt/nezha/agent/config-*.yml</code>，查挖矿进程、代理容器、<code>/tmp</code>、<code>/dev/shm</code>。如果有条件，再查 <code>memfd</code>、<code>deleted exe</code>、伪装 <code>kworker</code> 这类更隐蔽的进程。</p></li><li><p>轮换凭据</p><p>SSH key、面板密码、云服务 API key、对象存储、邮件、支付、AI 服务等，只要这台机器上可能读到，都应该按泄露处理。</p></li></ol><p>如果已经出现陌生 SSH key、挖矿进程、异常流量，或者你不确定机器到底被做过什么，别太相信“清理干净了”。备份必要业务数据后重装系统更稳。</p><h2 id="以后还用不用这类面板">以后还用不用这类面板</h2><p>这次事件不是简单的“哪吒能不能用”。真正的问题是，所有“监控 + 控制”一体化面板都要按高危入口对待。</p><p>如果一个面板可以打开终端、管理文件、批量执行命令，那它就不只是监控面板，而是运维控制台。它不该直接暴露在公网，也不该和普通状态页一个安全级别。</p><p>监控最好尽量只读。能不用远程执行，就不要开远程执行。能让 Agent 低权限运行，就不要 root。能放内网，就别放公网。</p><p>说到底，漂亮 UI 不是安全边界。生产环境里，少一个控制入口，往往比多一个炫酷面板更值钱。</p><div class="note note-info">            <p>Note: 本文大多数由 GPT 5.5 生成，因此 AI 味十足，不过本质就是一个事故复盘，重要的是信息准确，所以我也懒得改了。</p>          </div><h2 id="参考资料">参考资料</h2><ul><li><p>Aska あすか：哪吒监控安全事件复盘<br><a href="https://x.com/Asuka_Makina/article/2067017819099983926">https://x.com/Asuka_Makina/article/2067017819099983926</a></p></li><li><p>NodeSeek：哪吒探针漏洞攻击全记录<br><a href="https://www.nodeseek.com/post-786371-1">https://www.nodeseek.com/post-786371-1</a></p></li><li><p>NodeSeek：哪吒面板漏洞被批量植入后门全过程<br><a href="https://www.nodeseek.com/post-779143-1">https://www.nodeseek.com/post-779143-1</a></p></li><li><p>GitHub Security Advisory：<code>GHSA-5c25-7vpj-9mqh</code><br><a href="https://github.com/nezhahq/nezha/security/advisories/GHSA-5c25-7vpj-9mqh">https://github.com/nezhahq/nezha/security/advisories/GHSA-5c25-7vpj-9mqh</a></p></li><li><p>GitHub Security Advisory：<code>GHSA-99gv-2m7h-3hh9</code><br><a href="https://github.com/nezhahq/nezha/security/advisories/GHSA-99gv-2m7h-3hh9">https://github.com/nezhahq/nezha/security/advisories/GHSA-99gv-2m7h-3hh9</a></p></li></ul><p><img src="https://youpai.roccoshi.top/img/202606211327954.png" alt=""></p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/VPS/">VPS</category>
      
      <category domain="https://moreality.net/tags/Nezha/">Nezha</category>
      
      <category domain="https://moreality.net/tags/%E5%AE%89%E5%85%A8/">安全</category>
      
      <category domain="https://moreality.net/tags/%E8%BF%90%E7%BB%B4/">运维</category>
      
      
      <comments>https://moreality.net/posts/62186/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>旅行日记·大同</title>
      <link>https://moreality.net/posts/31742/</link>
      <guid>https://moreality.net/posts/31742/</guid>
      <pubDate>Sun, 19 Apr 2026 07:39:57 GMT</pubDate>
      
      <description>石窟、煤炭、醋、刀削面、汾酒</description>
      
      
      
      <content:encoded><![CDATA[<blockquote><p>黄土高原上的北魏古都，煤炭重镇，云冈大佛。</p></blockquote><h2 id="day-1-大同古城">day 1 大同古城</h2><p>大同给我的第一印象就是 —— 非常板正的一个城市，整个平城区就是从中间的古城开始，一个方框套着另一个方框。</p><p><img src="https://youpai.roccoshi.top/img/202604191746974.png" alt=""></p><p>七点多从酒店出来，步行到古城，从西边<strong>清远门</strong>进，逛到东边<strong>和阳门</strong>出，然后骑共享单车回来。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191746459.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191747527.png" alt=""></div></div></div><p>古城的灯一直开到十一点左右，晚上也很热闹。</p><h2 id="day-2-云冈石窟-晋华宫矿坑">day 2 云冈石窟 &amp; 晋华宫矿坑</h2><p>从平城市区出发，打车二十多块钱就可以到云冈石窟，大概半小时不到。</p><p>石窟本身规模很大，开凿于北魏，距今已经一千五百多年，很多佛像的脑袋已经被人摘走了，16-20 昙曜五窟很壮观，也可以直接室外露天观看。</p><p>其他的窟都需要门口排队进去，人很多，很挤。</p><p>推荐从 20 窟开始往回看，推荐参考 B 站「-小老西儿」的解说：</p><ul><li><a href="https://www.bilibili.com/video/BV1g9bEzcEZR">https://www.bilibili.com/video/BV1g9bEzcEZR</a> 云冈石窟教科书级讲解（上）</li><li><a href="https://www.bilibili.com/video/BV19mbEzPE3c">https://www.bilibili.com/video/BV19mbEzPE3c</a> 云冈石窟教科书级讲解（下）</li></ul><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191751678.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191751915.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191751293.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191752316.png" alt=""></div></div></div><p>云冈石窟对面的<strong>晋华宫矿坑公园</strong>倒是意外之喜。这里是一个废弃的露天煤矿改建的公园，在山顶可以俯瞰整个矿坑，工业遗址的震撼感远超对面的石窟——虽然没有下去，站在上面看已经觉得很值了。</p><p>虽然距离云冈石窟就不到1km，但两个景点可以说人流量一个天一个地，这个公园里面慢慢的晃悠一下还是很舒服的。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191757657.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191757053.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191757114.png" alt=""></div></div></div><h2 id="day-3-善化寺">day 3 善化寺</h2><p>一早出门就碰上<strong>沙尘暴</strong>，天色灰黄，风沙扑面。</p><p>想骑小黄车在古城里逛逛，换了七八个停车点，每一个都显示「此处不可停车」，明明画在标注的停车区域内。</p><p>骑车回酒店的路上，天气又突然变脸，<strong>瓢泼大雨</strong>砸下来，淋在身上变成泥水，而天气预报显示今天只是「普通阴天」。</p><blockquote><p>感觉自己已经慢慢从 J 人往 P 人在迁移了，不管是工作还是旅行，你的计划总是赶不上变化，一直 J 就很痛苦。</p></blockquote><p>但这一天也有唯一一个真正打动我的东西：<strong>吉祥天女</strong>。</p><p>逛了那么多石窟和佛像，大多数时候我是无感的，但这一尊例外。</p><p>几乎不像宗教造像，更像是某个真实存在过的人。</p><p>线下看到这尊佛像的时候真的很惊艳，就像东方的蒙娜丽莎，似笑非笑，似哭非哭，眼神里真的有佛家那种超然物外的感觉。</p><p><img src="https://youpai.roccoshi.top/img/202604191807410.png" alt=""></p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191807056.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191807339.png" alt=""></div></div></div><h2 id="关于大同的吃的">关于大同的吃的</h2><p>这几天吃了好多碗刀削面，记录几家店子：</p><ul><li><p>「<strong>龙聚祥烧麦馆</strong>」 - 点了烧麦、黄米凉糕、黑豆腐、茄盒。</p><ul><li>印象最深的是<strong>黑豆腐</strong>——烧得非常入味，豆腐外酥里嫩，极为好吃。</li><li><strong>黄米凉糕</strong>也很惊喜，口感糯，清甜，吃起来有种莫名的治愈感。</li><li>烧麦和南方的烧麦不太一样，似乎这边做的更像是汤包？不过也挺好吃。</li><li>燕麦茄盒也不错，油炸的，里面是芝士。</li></ul></li><li><p>「<strong>喜晋道</strong>」</p><ul><li>据说是大同刀削面里的爱马仕，其他刀削面馆猪肉刀削面大概 10 元左右，这家 19，最主要是队排老长了，最好去古城外那几家分店吃，不然排几个小时都吃不上。</li><li>刀削面中规中矩，没有惊喜，虎皮青椒和鸡爪做得比一般小馆子好一些——但考虑到价格，也很难说性价比高。</li></ul></li><li><p>「<strong>东方削面</strong>」</p><ul><li>也是比较火的一家连锁面馆，牛肉刀削 14 一份，猪肉 10 元一份，比喜晋道便宜，但是还不如楼下苍蝇馆子的好吃。</li></ul></li><li><p>「<strong>酒店楼下 — 日升面馆</strong>」</p><ul><li>楼下的苍蝇馆子，不管是青椒还是豆腐都做的比其他店入味，估计是汤里泡很久。</li><li>除此之外面一般，甚至吃了10分钟，底下的面就坨了，东方和喜晋道的面就没有这种情况。</li></ul><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191812770.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191812062.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191813248.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191813324.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604191814237.png" alt=""></div></div></div></li></ul><h2 id="一些其他记忆">一些其他记忆</h2><ul><li><p>大同（平城区）的车道：主干车道往往是 8 车道甚至 10 车道，非常宽敞，非机动车道很有意思，和机动车道中间往往会隔一排人行道或者绿化带，然后和另一侧的人行道之间又是平行的（导致车开上人行道非常容易）。</p><ul><li>但不好的一点就是，很多车在非机动车道上行驶，然后在路上乱停的车也很多。</li></ul><p><img src="https://youpai.roccoshi.top/img/202604191821230.png" alt=""></p></li><li><p>城市里没有地铁，打车还算方便，网约车到哪里基本都是 20 以内。</p></li><li><p>共享单车系统很烂，那么大一个古城区，几乎找不到共享单车停车位。</p></li><li><p>大同的风实在是太大了，地理位置近蒙古且没有高山阻挡，有沙尘暴也是最先受到侵袭的一批，就待了 2 天还给我踩中一天，主要的是，即使在沙尘暴之前的一天，甚至天气预报都没有提前预警。</p></li><li><p>恒山和悬空寺整个 4 月都在维护，原因也是因为风大怕吹下来石头，不过似乎我本身的2天行程也赶不上去恒山，除非特种兵。（有意思的是，似乎大同还不忍心放弃 5.1，维护到 4.30 就结束了。）</p></li><li><p>整体来说，如果想体验人文和古建筑，大同还是一个非常不错的城市，东西也算比较好吃的那一档。</p><ul><li>（但我本身对这些历史文化不那么感兴趣，so… 以后旅行可能还是会优先考虑山川湖海）</li></ul></li></ul>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/%E7%94%9F%E6%B4%BB/">生活</category>
      
      <category domain="https://moreality.net/tags/%E6%97%85%E8%A1%8C/">旅行</category>
      
      <category domain="https://moreality.net/tags/%E5%A4%A7%E5%90%8C/">大同</category>
      
      
      <comments>https://moreality.net/posts/31742/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>旅行日记·青岛</title>
      <link>https://moreality.net/posts/59568/</link>
      <guid>https://moreality.net/posts/59568/</guid>
      <pubDate>Sun, 05 Apr 2026 16:00:00 GMT</pubDate>
      
      <description>再一次来到北方的海滨城市</description>
      
      
      
      <content:encoded><![CDATA[<ul><li><p>24年9月，刚结束秋招，去秦皇岛待了 2 天。</p></li><li><p>25年6月，毕业，去大连待了 3 天。</p></li><li><p>26年4月，又一个人去青岛待了 3 天，不过这次没有什么 milestone，就是单纯想看海了。</p></li></ul><h2 id="准备">准备</h2><ul><li><strong>酒店:</strong> 是在招行上订的海景房，叫「汇泉王朝大饭店」(400/day)，就在第一海水浴场的上方，清明这个价格感觉是相当可以了，海景也很不错。</li></ul><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061347358.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061348435.png" alt=""></div></div></div><ul><li><strong>车票:</strong> 往返都是高铁，单程 400 左右，清明的票非常的不好买，于是请了一天年假早一天出发，也提前了一天回来。</li></ul><p>其实有了大交通 + 酒店，青岛剩下的景点就不用做什么提前预约和规划了，绝大多数海边的景点都是免费的，如果要去啤酒博物馆，海洋公园，或者崂山，可能需要提前规划/预约一下。</p><p>但实测，这三天下来，每天 3w 步，也差不多刚刚好把 <strong>栈桥 —— 石老人</strong> 这一条滨海线路走一遍。</p><p>附上我三天的线路:</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061356079.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061355153.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061357470.png" alt=""></div></div></div><h2 id="三天行程">三天行程</h2><h3 id="day-01">day 01</h3><p>青岛站下车以后，直接地铁 3 号线，汇泉广场站下车，步行 5min 就到酒店了。</p><p>来青岛以后看了一眼地铁线路图，第一感觉就是 —— 地铁修的真好，整个市南区的海岸线几乎都覆盖到了地铁 (2, 3 号线)，所以这三天我也没有打过车，出行全地铁<sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://zh.wikipedia.org/zh-cn/%E9%9D%92%E5%B2%9B%E5%9C%B0%E9%93%81">[1]</span></a></sup>就够了。</p><p><img src="https://youpai.roccoshi.top/img/202604061400439.png" alt="市南区地铁线路图"></p><p>到酒店先吃了一顿 200 块的自助，在酒店 25 层的旋转餐厅 —— 这个餐厅居然真的会转，大概 90 分钟自转一圈，由于是附近最高（第二高？）的建筑，可以看到全部的海景和城景。</p><p><img src="https://youpai.roccoshi.top/img/202604061426825.png" alt="200 的自助餐说实话有点不值，没啥很好吃或者有特色的东西，感觉可能甚至不如小城 78 的自助，不过剩下 100 买这个旋转体验和 25 层临海靠窗的座位，其实也说得过去。"></p><p>第一天很不巧，从中午开始以后，全是平流雾<sup id="fnref:2" class="footnote-ref"><a href="#fn:2" rel="footnote"><span class="hint--top hint--rounded" aria-label="平流雾（advection fog）是暖湿空气移到较冷的陆地或水面时，因下部冷却而形成的雾。通常发生在冬季，持续时间一般较长，范围大，雾较浓，厚度较大，有时可达几百米。">[2]</span></a></sup> + 霾的天气，海边的天气真是非常多变，即使我出行的前一天晚上看天气还是 3 天晴天，到了当天就变成 雾 + 霾 + 风 + 雨。</p><p><img src="https://youpai.roccoshi.top/img/202604061403705.png" alt="提前一天看的天气，4,5,6 都是晴天.."></p><p>所以拍出来的照片 be like:</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061406134.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061406941.png" alt=""></div></div></div><p>从第一浴场走到栈桥，然后到天主教堂，最后从小鱼山下来回到酒店，其实体验有点一般，市内的景观都是人造的，而老城区的红瓦绿树在这种阴天下又毫无美感。</p><p>许多年轻人聚集在「网红墙」前拍照，凑近了看不过是两个路牌，所以当我导航到这里的时候，我甚至直接走过去了才发现原来已经走过了。</p><p>—— 所以很多时候不想去那些人文景点，总感觉有些符号营造的太刻意。</p><p>晚上的时候，从五四广场随便吃了点东西就往奥帆中心走，因为那里可以看到青岛新城区的灯光秀。</p><p>新城区的夜景真的很华丽，全是几十层的高楼，把整个海边染的五光十色。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061418184.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061418541.png" alt=""></div></div></div><p><img src="https://youpai.roccoshi.top/img/202604061416727.png" alt="小红书上说新老城区，以湛山为界，其实从建筑的变化感受也十分明显"></p><p>然后回来以后到酒店楼下的青岛啤酒店，搞了一点青岛啤酒一厂和四厂的原浆和鲜啤：</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061421915.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061421713.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061422924.png" alt=""></div></div></div><p>都很好喝，口感和之前喝过的「泰山7天/28天」非常像，总之比罐装的水啤要好喝很多。</p><h3 id="day-02">day 02</h3><p>第二天的天气非常的好，于是拿上了我的 xs20 开始暴走模式。</p><p>整个白天基本都是在滨海绿道上徒步，文字不足以描述晴天大海的美，放图即可——</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061429632.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061430049.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061430902.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061430570.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061431355.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061432130.png" alt=""></div></div></div><p>上午从第一浴场一直走到了小麦岛，然后下午先坐地铁回来走了一圈八大关，之后又重返小鱼山俯瞰了一下青岛老城，“红瓦绿树，碧海蓝天”，晴天的青岛真的很浪漫。</p><p><img src="https://youpai.roccoshi.top/img/202604061435430.png" alt=""></p><p><img src="https://youpai.roccoshi.top/img/202604061435381.png" alt=""></p><p>第一浴场的酒店看不到日落，于是就等太阳落下山以后拍了几张蓝调:</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061454453.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061454816.png" alt=""></div></div></div><p>晚上，去吃了「前海沿」的海肠捞饭，其实吃不太出来海肠的味道，更多像是一种吃口感的东西，饭虽然很好吃，但是更多的味道来自于酱油、韭菜、肉沫。</p><p><img src="https://youpai.roccoshi.top/img/202604061457846.png" alt=""></p><h3 id="day-03">day 03</h3><p>第三天的天气和第一天如出一辙，都是雾霾阴天，并且已经有点疲惫了，前两天都是暴走模式，脚已经有点跟不上了，退完房以后背着书包从海之恋公园漫步到石老人浴场，走一会歇一会。</p><p>中午吃了「粥全粥到」的蛤蜊炒鸡和鲅鱼饺子，还可以吧，不过感觉不如黄焖鸡和金谷园的饺子。。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061538070.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202604061538975.png" alt=""></div></div></div><p>中午就已经抵达青岛站了，出来以后去了一个叫「团岛山」的公园，公园上开满了淡紫色的花，人特别特别少，即使离栈桥很近很近。</p><p>在这里坐了很久，感觉旅行的时候，我就喜欢挑这种清净小众的地方多停留一会，即使风景没有那么极致。</p><p><img src="https://youpai.roccoshi.top/img/202604061503212.png" alt=""></p><p>从山上下来以后又去了一趟栈桥，给我吓出密集恐惧症了。</p><p><img src="https://youpai.roccoshi.top/img/202604061504733.png" alt=""></p><h2 id="碎碎念时间">碎碎念时间</h2><ul><li><p>前几天做攻略的时候看别人说，青岛是一个镶着金边的破抹布，感觉说的确实很有道理，虽然没怎么深度体验市内的人文，但是趁吃饭的时候去市北区转了转，感受是确实挺脏乱差的。</p><ul><li>不过也可以称之为「烟火气」吧，也许。</li></ul></li><li><p>市南区的交通便利又混乱，地铁直通大多数地方，但是陆地上的机动车道大多是单行道，很堵，然后人行道上挤满了送外卖的电动车——因为并没有非机动车道。</p></li><li><p>和大连一样的，这里看不到共享单车，不过骑自行车的人还挺多的，没有特别起伏的地形。</p></li><li><p>总的来说，因为天气的原因，可能只体会到了 1 天完全体的青岛，但是能感受到这座城市对于旅游来说还是非常不错的，海岸线超级长，可以走好几天，也许会挑一个暖和一点的天气过来，感受一下崂山和黄岛区。</p></li><li><p>路线总结:</p><ul><li>day 01: 第一海水浴场 → 青岛水族馆 → 鲁迅公园 → 琴屿路 → 小青岛公园 → 海军博物馆 → 太平路 → 栈桥 → 天主教堂 → 网红墙 → 小鱼山</li><li>day 02: 第一海水浴场 → 八大关 → 第二海水浴场 → 第三海水浴场 → 音乐广场 → 五四广场(五月的风) → 奥帆中心 → 燕岛山 → 情人坝 → 滨海绿道走完 → 小麦岛公园</li><li>day03: 小麦岛 → 海之恋公园 → 极地海洋公园 → 滨海绿道 → 石老人浴场</li></ul></li></ul><h2 id="References">References</h2><section class="footnotes"><div class="footnote-list"><ol><li><span id="fn:1" class="footnote-text"><span><a href="https://zh.wikipedia.org/zh-cn/%E9%9D%92%E5%B2%9B%E5%9C%B0%E9%93%81">https://zh.wikipedia.org/zh-cn/青岛地铁</a><a href="#fnref:1" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:2" class="footnote-text"><span>平流雾（advection fog）是暖湿空气移到较冷的陆地或水面时，因下部冷却而形成的雾。通常发生在冬季，持续时间一般较长，范围大，雾较浓，厚度较大，有时可达几百米。<a href="#fnref:2" rev="footnote" class="footnote-backref"> ↩</a></span></span></li></ol></div></section>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/%E7%94%9F%E6%B4%BB/">生活</category>
      
      <category domain="https://moreality.net/tags/%E6%97%85%E8%A1%8C/">旅行</category>
      
      <category domain="https://moreality.net/tags/%E6%B5%B7/">海</category>
      
      <category domain="https://moreality.net/tags/%E9%9D%92%E5%B2%9B/">青岛</category>
      
      
      <comments>https://moreality.net/posts/59568/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>旅行日记·大连</title>
      <link>https://moreality.net/posts/48502/</link>
      <guid>https://moreality.net/posts/48502/</guid>
      <pubDate>Tue, 10 Jun 2025 00:50:32 GMT</pubDate>
      
      <description>花了3天时间，围着大连城区的海岸线走了一圈，滨海城市的旅行体验果然是独一档的</description>
      
      
      
      <content:encoded><![CDATA[<div class="note note-info">            <p>🖼️ 多图</p>          </div><h2 id="day-0-启程">day 0 - 启程</h2><p>坐上 K684，开启去往大连的旅途。</p><p>这趟卧铺有很多好处：</p><ul><li>北京-大连分别是始发和终点站，床上用品都换过了。</li><li>可以在第二天一早抵达大连。</li><li>工作日人超少（这节车厢中上铺都没有人，下铺也没满），一晚上都很安静。</li><li>大连站刚好在市中心</li></ul><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506100856283.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506100859601.png" alt=""></div></div></div><h2 id="day-1-东港">day 1 - 东港</h2><p>酒店定在了中山广场，</p><p>早上 7 点到了酒店，直接就可以办入住了（赞一波亚朵）。</p><p>然后包放下以后就直接坐地铁来到了东港。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112257913.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112257170.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112258115.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112258406.png" alt=""></div></div></div><p>这一天还是端午节的最后一天，并且天气一般，预想到体验可能不会很好，不过实际逛下来还是比想象中的好不少。</p><p>几个景点的感受：</p><ul><li><strong>东港广场</strong>： 喷泉附近有很多的海鸥，用北京带来的面包（本来准备火车上当早餐的）喂了一波，感觉还挺有意思的，不过小心 🐦💩。</li><li><strong>威尼斯水城</strong>： 白天没什么好逛的，就是一个小商业街的模式，听说晚上的灯光氛围还可以，不过后面也没有晚上来过。</li></ul><p>回酒店吃了一个午饭歇了一会，下午直接打车到海之韵公园继续漫步。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112305329.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112305850.png" alt=""></div></div></div><p>海之韵公园从北门进，上来就是一段超级大上坡，差点没给人累死，在大太阳下走了快半小时才到山顶，不过从山上眺望大海的感觉还蛮好的。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112307257.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112307058.png" alt=""></div></div></div><p>往山下走，为了看一个网红景点「圣象天门」跟着一群大胆的游客翻了好几个护栏和挡板，到了以后发现不过如此，一大群人排队围着一个山洞拍照，虽然到达这里花了很大一番功夫，但是真正到了之后，我也懒得挤过去凑热闹了。</p><p>可能每个人对于旅行的理解都不一样，不过我是不太喜欢到了一个地方，就要打卡那些「出片」的网红景点，反倒是喜欢钻入无人的犄角旮旯去探索一下。</p><p><img src="https://youpai.roccoshi.top/img/202506112308941.png" alt="到达这里需要翻好几个封锁，到了发现一群人挤着一个小山洞拍网红打卡点"></p><p>一路上发现了好多可爱小猫，最喜欢的是这只大橘和这俩狸花：</p><p><img src="https://youpai.roccoshi.top/img/202506112314367.png" alt="大橘趴在亭子的角落，一动不动望着大海好久"></p><p><img src="https://youpai.roccoshi.top/img/202506112313980.png" alt="这两只狸花好像是兄弟，一直形影不离"></p><p>出了海之韵公园南门，就一直沿着滨海东路走啊走啊，一个下午走了快 30km，4 万步。自己走路有一个好处就是不必迁就他人，遇到想走的路和想看的东西，就直接走就完事了。</p><p>大连是丘陵地带，海边没有沙滩，只有断崖和礁石滩，这边整个滨海东路都是断崖，也是一种独特的风格。</p><p>随处可见这样的告示：</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112319376.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112321064.png" alt=""></div></div></div><p>途中遇到可以拐下去的地方，无一例外都选择了下去看看，遇到了很多惊喜的景点。</p><p>许多地方就是沿着断崖修建的小路或者房子，左边是树林，右边就是一望无际的大海。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112320054.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112319513.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506112325678.png" alt=""></div></div></div><h2 id="day-2-亲海栈道">day 2 - 亲海栈道</h2><p>第二天一早就打车来到了老虎滩-菱角湾。</p><p>假期过了，天气晴了，大海的颜色变成深邃的蓝，拍照非常好看。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506120825004.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506120829872.png" alt=""></div></div></div><p>大连的海边有非常多的本地钓鱼佬，都是轻装出行，一根钓竿一把小椅子，一坐就是大半天，给人一种非常 chill 的感觉。</p><p><img src="https://youpai.roccoshi.top/img/202506120829601.png" alt=""></p><p>一路上都是这种非常舒适的步行木栈道，基本覆盖了从海之韵公园出来一直到星海广场这一条几十公里长长的海岸线。</p><p><img src="https://youpai.roccoshi.top/img/202506120832866.png" alt=""></p><p>走到付家庄公园，远看是一个沙滩，近看原来是石子组成的。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506120834202.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506120837624.png" alt=""></div></div></div><p><img src="https://youpai.roccoshi.top/img/202506120836116.png" alt="一对新婚夫妻正在拍婚纱照，花拱门背对着就是一个带娃的母亲"></p><p>快黄昏时走到了大连的标志性建筑「星海湾大桥」，这个 6.8km 的跨海大桥从各个角度看过去都很壮观。</p><p><img src="https://youpai.roccoshi.top/img/202506120841563.png" alt=""></p><p><img src="https://youpai.roccoshi.top/img/202506120841475.png" alt=""></p><p>日落时桥上还会亮起不同颜色的灯光，不过当时有点冷，就没在山顶逗留了，不然俯视角下的夜景应该会很好看。</p><p><img src="https://youpai.roccoshi.top/img/202506120845521.png" alt=""></p><h2 id="day-3-一些城市景观">day 3 - 一些城市景观</h2><p>体验了一下大连的有轨电车，由于是工作日车上没什么人，车晃悠的不行、叮铃哐啷的很有意思。</p><p><img src="https://youpai.roccoshi.top/img/202506120850104.png" alt=""></p><p>大连城市的街道两旁见不到任何共享单车，不过倒是有一些共享的电动车，可能是因为城市规划的原因（没有非机动车道），并且丘陵地带很多陡峭的上下坡，确实不太适合骑单车。</p><p><img src="https://youpai.roccoshi.top/img/202506120851777.png" alt=""></p><p>晚上 8 点来到南山街，没有想象中热闹的街景，只有节日过后人们正在收拾残局。</p><p>而且大连的风真的很大，即使在城区里也能吹的人头秃，于是匆匆离开。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506120857781.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202506120858196.png" alt=""></div></div></div><h2 id="后记">后记</h2><p>写这篇 Blog 的时候这次旅行已经过去快 2 周了，不过一切景物还是历历在目。</p><p>旅行这种事情确实能极大的减缓时间流逝的感觉，让人重拾起一种控制时间与生活的信心。</p><p>滨海城市的旅行体验真的极好，有时间会再去威海、青岛这些地方看看。</p><p>很喜欢的一句话:</p><blockquote><p>只有一个人旅行时，才听得到自己的声音。它会告诉你，这世界比自己想象中更宽阔。</p></blockquote><iframe width="100%" height="415" src="//player.bilibili.com/player.html?isOutside=true&aid=114624305366467&bvid=BV112T7z8EqM&cid=30316366200&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"></iframe>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/%E7%94%9F%E6%B4%BB/">生活</category>
      
      <category domain="https://moreality.net/tags/%E6%97%85%E8%A1%8C/">旅行</category>
      
      <category domain="https://moreality.net/tags/%E5%A4%A7%E8%BF%9E/">大连</category>
      
      <category domain="https://moreality.net/tags/%E6%B5%B7/">海</category>
      
      
      <comments>https://moreality.net/posts/48502/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>天津天津</title>
      <link>https://moreality.net/posts/30786/</link>
      <guid>https://moreality.net/posts/30786/</guid>
      <pubDate>Thu, 29 May 2025 11:35:05 GMT</pubDate>
      
      <description>喜欢海河两岸的建筑和游船</description>
      
      
      
      <content:encoded><![CDATA[<iframe width="100%" height="415" src="https://www.youtube.com/embed/ZwcFqqlNdR4?si=HaE_imn16_cNwF8b" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><p>毕业答辩结束，又因为还需要等着提交材料没法长途跋涉，想着天津离北京这么近又还没去过，索性去看看。</p><h2 id="初印象：混乱街道与大碗饭菜">初印象：混乱街道与大碗饭菜</h2><p>5月末，白天的天津，炎热难耐，走在大街上，似乎不存在斑马线和行人红绿灯。</p><p>好多好多的单行道和狭窄街道。下火车出了地铁左转，因为没有人行道，只能在逆行穿过一股酒味的涵洞。</p><p><img src="https://youpai.roccoshi.top/img/202505291940005.png" alt=""></p><p>第一顿当然尝一下天津菜，没想到分量巨大无比，一盆炒鸡估计炒了不止一只鸡。</p><p>两个饭量还不错的人吃了一个小时，最后菜和没动一样。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202505291942785.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202505291942069.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202505291941225.png" alt=""></div></div></div><h2 id="海河游船">海河游船</h2><p>晚上去天津站码头坐了海河游船，也是此次天津之行感觉最值回票价的体验。</p><p>出了天津站就是码头，长长的海河沿线几乎包含了所有天津有名的景点。</p><p>如果天津没有这条海河，也许我给的评价还不如我老家。</p><p>周围高楼林立，每个楼都闪着金光，有种轻奢感。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202505291946365.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202505291948342.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202505291949541.png" alt=""></div></div></div><p>平平无奇的周内工作日，海河周围散步的人也是熙熙攘攘。</p><p>到了晚上 10 点多才逐渐安静下来，这时沿着河边散步几乎只剩下一个个卖酒的小摊贩。</p><p>心情还不错的时候，吹着晚风这么喝一杯还挺惬意。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202505291953656.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202505291953852.png" alt=""></div></div></div><h2 id="白天的天津：炎热而无聊">白天的天津：炎热而无聊</h2><p>白天走马观花的看了世纪钟广场、津湾广场、西开教堂、北安桥、意式风情街、瓷房子、五大道等等一系列建筑。</p><p>如果天气凉快一点，也许还有心情停下来欣赏一番，然而 30 度的艳阳天只能让我像特种兵打卡一样匆匆拍个照就走人。</p><p>体验一般，不过即使是凉快下来，感觉白天的天津相比晚上也差了不少意思。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202505291957375.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202505291957921.png" alt=""></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202505291958762.png" alt=""></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202505291958243.png" alt=""></div></div></div><h2 id="相声：有点意思">相声：有点意思</h2><p>下午的时候实在逛不下去了，干脆花 2 小时去听下相声。</p><p>直到开场的时候才陆续来了其他两桌人，不然直接包场了，那样还挺尬的。</p><p><img src="https://youpai.roccoshi.top/img/202505292000268.png" alt=""></p><p><img src="https://youpai.roccoshi.top/img/202505292000593.png" alt=""></p><p>第一次线下花这么长时间完整的听完一场相声，感觉还不错，主要空调还很凉快。</p><p>最后又在夜风中搭观光车溜达了一圈，感觉天津城区真的好小，已经有点腻了。</p><p><img src="https://youpai.roccoshi.top/img/202505292002544.png" alt=""></p><p>还是更喜欢山川湖海。</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/%E7%94%9F%E6%B4%BB/">生活</category>
      
      <category domain="https://moreality.net/tags/%E5%A4%A9%E6%B4%A5/">天津</category>
      
      <category domain="https://moreality.net/tags/%E5%9F%8E%E5%B8%82/">城市</category>
      
      
      <comments>https://moreality.net/posts/30786/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Google「提示词工程」白皮书阅读总结</title>
      <link>https://moreality.net/posts/6318/</link>
      <guid>https://moreality.net/posts/6318/</guid>
      <pubDate>Mon, 21 Apr 2025 14:45:28 GMT</pubDate>
      
      <description>晚上看了看 Google 最近出的一个叫「Prompt Engineering」的一个总结性质的文档，把笔记整理一下</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="前言">前言</h2><blockquote><p>注意，本文不是翻译，只是针对自己觉得有用的一些地方的记录和总结；对于我自己觉得已经比较清楚的概念，就完全没有记录。其中最后的「最佳实践 Best Practice」部分个人感觉比较有用，强烈建议结合原版 PDF 的一些示例 demo 进行对比查看。</p></blockquote><p><strong>简单介绍：</strong></p><p>「Prompt Engineering」是 Google 推出的提示词工程白皮书，一共 60 多页，原始版本可以在 <a href="https://www.kaggle.com/whitepaper-prompt-engineering">https://www.kaggle.com/whitepaper-prompt-engineering</a> 上获取到，或者直接 <a href="https://drive.google.com/file/d/1AbaBYbEa_EbPelsT40-vj64L-2IwUJHy/view">下载链接 (googledrive)</a>。</p><p><img src="https://youpai.roccoshi.top/img/202504212246609.png" alt="封面"></p><h2 id="1-采样控制">1 - 采样控制</h2><p>主要三个可配置项:</p><ul><li>Top-P</li><li>Top-K</li><li>Temperature (温度)</li></ul><p>作用的先后顺序: Top-K -&gt; Top-P -&gt; Temperature</p><h3 id="例子">例子</h3><p>假设我们有以下设置：</p><ul><li>top-k = 5</li><li>top-p = 0.9</li><li>temperature = 0.7</li></ul><p>当模型预测下一个词时，可能给出了这样的概率分布：</p><ul><li>“的”: 0.3</li><li>“是”: 0.25</li><li>“在”: 0.2</li><li>“了”: 0.1</li><li>“有”: 0.05</li><li>“和”: 0.03</li><li>“人”: 0.02</li><li>其他词: 各小于0.01</li></ul><p>应用步骤：</p><ol><li>top-k=5筛选后，只保留前5个词：“的”、“是”、“在”、“了”、“有”</li><li>计算这5个词的累积概率：0.3+0.25+0.2+0.1+0.05=0.9，正好等于top-p的阈值0.9，所以这5个词都被保留</li><li>应用temperature=0.7，通常来说，温度与模型的“创造力”有关。但事实并非如此。温度只是调整单词的概率分布。其最终的宏观效果是，<strong>在较低的温度下，我们的模型更具确定性，而在较高的温度下，则不那么确定</strong>。(ref: <a href="https://www.zhihu.com/tardis/zm/art/647813179?source_id=1003">知乎: 大模型文本生成——解码策略（Top-k &amp; Top-p &amp; Temperature）</a>)</li></ol><h3 id="极端情况">极端情况</h3><p>几种极端情况的 LLM 实现考虑:</p><ul><li>温度设为 0 时，Top-K 和 Top-P 无关，概率最高的令牌被选为下一个预测令牌；温度极高（如 10 量级）时，温度无关，Top-K 和/或 Top-P 标准的令牌将被随机采样。</li><li>Top-K 设为 1 时，温度和 Top-P 无关，仅概率最高的令牌被选；Top-K 设为词汇表大小时，所有非零概率令牌均满足标准，无令牌被筛选。</li><li>Top-P 设为 0 或极小时，仅概率最高的令牌满足标准，温度和 Top-K 无关；Top-P 设为 1 时，所有非零概率令牌均满足标准，无令牌被筛选。</li></ul><h3 id="推荐配置">推荐配置</h3><table><thead><tr><th>Use Case</th><th>Temperature</th><th>Top-P</th><th>Top-K</th><th>Description</th></tr></thead><tbody><tr><td>通用场景 General Purpose</td><td>0.2</td><td>0.95</td><td>30</td><td>Relatively coherent results with some creativity</td></tr><tr><td>高创造力场景 High Creativity</td><td>0.9</td><td>0.99</td><td>40</td><td>Especially creative results</td></tr><tr><td>比较精确的场景 (比如coding) <br />Low Creativity</td><td>0.1</td><td>0.9</td><td>20</td><td>Less creative results</td></tr><tr><td>Single Correct Answer</td><td>0</td><td>-</td><td>-</td><td>For tasks with only one correct answer (e.g., math problems)</td></tr></tbody></table><h2 id="2-Guide-Prompt-分类">2 - Guide Prompt 分类</h2><p>系统提示（System Prompting）、上下文提示（Contextual Prompting）和角色提示（Role Prompting）都是用于指导大型语言模型（LLMs）生成文本的技术，但它们关注的重点不同。尽管这些提示类型之间存在相当大的重叠（例如，一个提示可能同时为系统分配角色并提供上下文），但每种提示的主要目的略有不同。</p><p>以下是三种提示类型的核心区别和作用：</p><table><thead><tr><th>提示类型</th><th>主要目的与功能</th><th>特点与作用</th></tr></thead><tbody><tr><td><strong>System Prompt</strong></td><td>定义模型的基本能力和总体目的。</td><td>设定语言模型的“大局观”，例如翻译语言、分类评论等，指导模型的整体行为。</td></tr><tr><td><strong>Contextual Prompt</strong></td><td>提供与当前对话或任务相关的具体细节或背景信息，指导模型生成针对性的回应。</td><td>高度特定于当前任务或输入，具有动态性，帮助模型理解细微差别并调整回应。</td></tr><tr><td><strong>Role Prompt</strong></td><td>为模型分配特定的角色或身份，帮助模型生成与该角色一致的回应。</td><td>塑造模型的输出风格和语气，增加具体性和个性，使回应符合角色的知识和行为。</td></tr></tbody></table><h3 id="system-prompt">system prompt</h3><figure class="highlight vbnet"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs vbnet">Classify movie reviews <span class="hljs-keyword">as</span> positive, neutral <span class="hljs-built_in">or</span> negative. Only<br><span class="hljs-keyword">return</span> the label <span class="hljs-keyword">in</span> uppercase.<br><span class="hljs-symbol">Review:</span> <span class="hljs-string">&quot;Her&quot;</span> <span class="hljs-built_in">is</span> a disturbing study revealing the direction<br>humanity <span class="hljs-built_in">is</span> headed <span class="hljs-keyword">if</span> AI <span class="hljs-built_in">is</span> allowed <span class="hljs-keyword">to</span> keep evolving,<br>unchecked. It<span class="hljs-comment">&#x27;s so disturbing I couldn&#x27;t watch it.</span><br><span class="hljs-symbol">Sentiment:</span><br></code></pre></td></tr></table></figure><h3 id="contextual-prompt">contextual prompt</h3><figure class="highlight applescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs applescript">Context: You are writing <span class="hljs-keyword">for</span> a blog <span class="hljs-keyword">about</span> retro <span class="hljs-number">80</span>&#x27;s arcade<br>video games.<br>Suggest <span class="hljs-number">3</span> topics <span class="hljs-keyword">to</span> <span class="hljs-built_in">write</span> an article <span class="hljs-keyword">about</span> <span class="hljs-keyword">with</span> a few lines <span class="hljs-keyword">of</span><br>description <span class="hljs-keyword">of</span> what this article should <span class="hljs-keyword">contain</span>.<br></code></pre></td></tr></table></figure><h3 id="role-prompt">role prompt</h3><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs vim">I want you <span class="hljs-keyword">to</span> act <span class="hljs-keyword">as</span> <span class="hljs-keyword">a</span> travel guide. I will <span class="hljs-keyword">write</span> <span class="hljs-keyword">to</span> you<br>about my location <span class="hljs-built_in">and</span> you will suggest <span class="hljs-number">3</span> places <span class="hljs-keyword">to</span> visit near<br><span class="hljs-keyword">me</span>. In some cases, I will also give you the <span class="hljs-built_in">type</span> of places I<br>will visit.<br>My suggestion: <span class="hljs-comment">&quot;I am in Amsterdam and I want to visit</span><br><span class="hljs-keyword">only</span> museums.<span class="hljs-comment">&quot;</span><br>Travel Suggestions:<br></code></pre></td></tr></table></figure><h2 id="3-示例个数-one-shot-few-shot">3 - 示例个数: one-shot &amp; few-shot</h2><p>在为AI模型创建提示时，提供示例非常有帮助。示例可以帮助模型理解您的需求，尤其是在希望模型输出特定结构或模式时特别有用。</p><ul><li>One-shot 提示提供单个示例，因此得名one-shot，目的是让模型有一个可以模仿的示例来完成任务。</li><li>Few-shot 提示则提供多个示例，向模型展示需要遵循的模式，增加模型遵循该模式的机会。Few-shot提示所需的示例数量取决于任务复杂性、示例质量以及所使用的generative AI (gen AI)模型的能力。一般建议至少使用三到五个示例，但对于复杂任务可能需要更多示例，或者由于模型输入长度限制可能需要更少。选择示例时，应确保示例与任务相关，具有多样性、高质量且编写良好，因为一个小错误就可能导致模型混淆并输出不符合预期的结果。此外，如果希望生成的输出能适应各种输入，包含edge cases（异常或意外输入）在示例中非常重要。</li></ul><h2 id="4-Step-back-Prompting-回退提示词">4 - Step-back Prompting (回退提示词)</h2><p>Step-back prompting 是一种通过提示 LLM 先考虑与具体任务相关的一般性问题，然后将该问题的答案输入到后续的具体任务提示中来提升性能的技术。这种“step back”方法使 LLM 在尝试解决具体问题之前激活相关的背景知识和推理过程。通过考虑更广泛和基本的原则，LLM 能够生成更准确和有洞察力的回应。</p><p>Step-back prompting 鼓励 LLM 批判性思考并以新的和创造性的方式应用知识，通过利用 LLM 参数中的更多知识来改变执行任务的最终提示，而这些知识在直接提示 LLM 时可能不会被激活。它还可以通过关注一般原则而非具体细节来帮助减轻 LLM 回应中的偏见。</p><h3 id="传统-prompt-step-back-prompt-对比">传统 prompt &amp; step-back prompt 对比</h3><p>传统 prompt:</p><figure class="highlight livecodeserver"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs livecodeserver">Write <span class="hljs-keyword">a</span> <span class="hljs-literal">one</span> <span class="hljs-keyword">paragraph</span> storyline <span class="hljs-keyword">for</span> <span class="hljs-keyword">a</span> <span class="hljs-built_in">new</span> level <span class="hljs-keyword">of</span> <span class="hljs-keyword">a</span> firstperson shooter video game that is challenging <span class="hljs-keyword">and</span> engaging.<br></code></pre></td></tr></table></figure><p>step-back prompt:</p><ol><li>首先询问:</li></ol><figure class="highlight livecodeserver"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs livecodeserver">Write <span class="hljs-keyword">a</span> <span class="hljs-literal">one</span> <span class="hljs-keyword">paragraph</span> storyline <span class="hljs-keyword">for</span> <span class="hljs-keyword">a</span> <span class="hljs-built_in">new</span> level <span class="hljs-keyword">of</span> <span class="hljs-keyword">a</span> firstperson shooter video game that is challenging <span class="hljs-keyword">and</span> engaging.<br></code></pre></td></tr></table></figure><ol start="2"><li>之后基于 LLM  的回答，追加提问:</li></ol><figure class="highlight applescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs applescript">Based <span class="hljs-keyword">on</span> popular <span class="hljs-keyword">first</span>-person shooter action games, what are<br><span class="hljs-number">5</span> fictional key settings <span class="hljs-keyword">that</span> contribute <span class="hljs-keyword">to</span> a challenging <span class="hljs-keyword">and</span><br>engaging level storyline <span class="hljs-keyword">in</span> a <span class="hljs-keyword">first</span>-person shooter video game?<br></code></pre></td></tr></table></figure><h2 id="5-APE-Automatic-Prompt-Engineering-自动生成提示词">5 - APE: Automatic Prompt Engineering (自动生成提示词)</h2><p>编写提示（prompt）可能是一项复杂的任务。为了解决这个问题，可以采用Automatic Prompt Engineering (APE)方法。这种方法不仅减少了人工输入的需求，还能提升模型在各种任务中的表现。你可以让模型生成更多提示，对它们进行评估，可能的话修改好的提示，然后重复这个过程。</p><p>case:</p><figure class="highlight applescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs applescript">We have a band merchandise t-shirt webshop, <span class="hljs-keyword">and</span> <span class="hljs-keyword">to</span> train a<br>chatbot we need various ways <span class="hljs-keyword">to</span> order: <span class="hljs-string">&quot;One Metallica t-shirt</span><br><span class="hljs-string">size S&quot;</span>. Generate <span class="hljs-number">10</span> variants, <span class="hljs-keyword">with</span> <span class="hljs-keyword">the</span> same semantics <span class="hljs-keyword">but</span> keep<br><span class="hljs-keyword">the</span> same meaning.<br></code></pre></td></tr></table></figure><h2 id="6-Code-Prompting">6 - Code Prompting</h2><blockquote><p>这节没什么特别出彩的，就是列举了几个 coding 的用法，不过可以参考一下 Gemini 在 coding 时是如何推荐配置 topk, topp 和 temperature 的</p></blockquote><ul><li><p><strong>Temperature</strong>：0.1</p></li><li><p><strong>Token Limit</strong>：1024</p></li><li><p><strong>Top-K</strong>：N/A</p></li><li><p><strong>Top-P</strong>：1</p></li><li><p>writing code</p></li></ul><figure class="highlight livecodeserver"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs livecodeserver">Write <span class="hljs-keyword">a</span> code snippet <span class="hljs-keyword">in</span> Bash, which asks <span class="hljs-keyword">for</span> <span class="hljs-keyword">a</span> <span class="hljs-built_in">folder</span> name.<br>Then <span class="hljs-keyword">it</span> takes <span class="hljs-keyword">the</span> contents <span class="hljs-keyword">of</span> <span class="hljs-keyword">the</span> <span class="hljs-built_in">folder</span> <span class="hljs-keyword">and</span> renames all <span class="hljs-keyword">the</span><br><span class="hljs-built_in">files</span> inside <span class="hljs-keyword">by</span> prepending <span class="hljs-keyword">the</span> name draft <span class="hljs-built_in">to</span> <span class="hljs-keyword">the</span> <span class="hljs-built_in">file</span> name.<br></code></pre></td></tr></table></figure><ul><li>explain code</li></ul><figure class="highlight applescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs applescript">Explain <span class="hljs-keyword">to</span> <span class="hljs-keyword">me</span> <span class="hljs-keyword">the</span> <span class="hljs-keyword">below</span> Bash code:<br></code></pre></td></tr></table></figure><ul><li>translating code</li></ul><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs css"><span class="hljs-attribute">Translate</span> the below Bash <span class="hljs-selector-tag">code</span> <span class="hljs-selector-tag">to</span> <span class="hljs-selector-tag">a</span> Python snippet.<br></code></pre></td></tr></table></figure><ul><li>debug code</li></ul><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs vim">The below Python code gives <span class="hljs-keyword">an</span> error:<br>Traceback (most recent <span class="hljs-keyword">call</span> <span class="hljs-keyword">last</span>):<br>File <span class="hljs-comment">&quot;/</span><br>Users/leeboonstra/Documents/test_folder/rename_files.<span class="hljs-keyword">py</span><span class="hljs-comment">&quot;, line</span><br><span class="hljs-number">7</span>, in <span class="hljs-symbol">&lt;module&gt;</span><br>text = toUpperCase(prefix)<br>NameError: name <span class="hljs-string">&#x27;toUpperCase&#x27;</span> <span class="hljs-keyword">is</span> not defined<br>Debug what<span class="hljs-string">&#x27;s wrong and explain how I can improve the code.</span><br></code></pre></td></tr></table></figure><h2 id="7-最佳实践-Best-Practices">7 - 最佳实践 Best Practices</h2><blockquote><p>这个很有价值，一些很容易想到和验证的概念，但是 Google 帮你总结出来了</p></blockquote><h3 id="7-1-提供示例-Provide-examples">7.1 - 提供示例 (Provide examples)</h3><p>最重要的原则是在提示中提供示例（one shot/few shot）。这是高效的方法，因为它能作为强大的教学工具。这些示例展示了期望的输出或类似的响应，让模型从中学习并相应地调整其生成内容，提高准确性、风格和语调。</p><h3 id="7-2-设计简洁明了的提示-Design-with-Simplicity">7.2 - 设计简洁明了的提示 (Design with Simplicity)</h3><p>提示应该简洁、清晰，易于你和模型理解。如果对你来说已经令人困惑，对模型来说也可能令人困惑。尽量不要使用复杂的语言，不要提供不必要的信息。</p><p><strong>示例改进：</strong></p><figure class="highlight vbnet"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs vbnet">改进前：<br>I am visiting <span class="hljs-built_in">New</span> York right now, <span class="hljs-built_in">and</span> I<span class="hljs-comment">&#x27;d like to hear more about great locations. I am with two 3 year old kids. Where should we go during our vacation?</span><br><br>改进后：<br>Act <span class="hljs-keyword">as</span> a travel guide <span class="hljs-keyword">for</span> tourists. Describe great places <span class="hljs-keyword">to</span> visit <span class="hljs-keyword">in</span> <span class="hljs-built_in">New</span> York Manhattan <span class="hljs-keyword">with</span> a <span class="hljs-number">3</span> year old.<br></code></pre></td></tr></table></figure><p>尝试使用描述动作的动词，例如: Act, Analyze, Categorize, Classify, Contrast, Compare, Create, Describe, Define, Evaluate, Extract, Find, Generate, Identify, List, Measure, Organize, Parse, Pick, Predict, Provide, Rank, Recommend, Return, Retrieve, Rewrite, Select, Show, Sort, Summarize, Translate, Write.</p><h3 id="7-3-明确指定输出要求-Be-Specific-About-Output">7.3 - 明确指定输出要求 (Be Specific About Output)</h3><p>要明确期望的输出内容。简洁的指令可能不足以引导LLM或过于笼统。在提示中提供具体细节（通过系统或上下文提示）可以帮助模型专注于相关内容，提高整体准确性。</p><p><strong>推荐做法：</strong></p><figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mipsasm">Generate a <span class="hljs-number">3</span> paragraph <span class="hljs-keyword">blog </span>post about the top <span class="hljs-number">5</span> video game consoles. The <span class="hljs-keyword">blog </span>post <span class="hljs-keyword">should </span><span class="hljs-keyword">be </span>informative <span class="hljs-keyword">and </span>engaging, <span class="hljs-keyword">and </span>it <span class="hljs-keyword">should </span><span class="hljs-keyword">be </span>written in a conversational style.<br></code></pre></td></tr></table></figure><p><strong>不推荐的做法</strong>:</p><figure class="highlight stata"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs stata"><span class="hljs-keyword">Generate</span> a blog <span class="hljs-keyword">post</span> <span class="hljs-keyword">about</span> video game consoles.<br></code></pre></td></tr></table></figure><h3 id="7-4-使用指令而非约束-Use-Instructions-over-Constraints">7.4 - 使用指令而非约束 (Use Instructions over Constraints)</h3><ul><li>指令提供了关于期望格式、风格或内容的明确指导，<strong>指导模型应该做什么或生成什么</strong></li><li>约束是对响应的一系列限制或边界，<strong>限制模型不应该做什么或避免什么</strong></li></ul><p>研究表明，在提示中专注于积极指令比过度依赖约束更有效。这与人类偏好积极指令而非列出不应做的事项的方式一致。</p><p>指令直接传达期望的结果，而约束可能让模型猜测什么是允许的。指令在定义的边界内提供灵活性并鼓励创造力，而约束可能限制模型的潜力。此外，约束列表可能相互冲突。</p><p>约束在某些情况下仍然有价值，例如防止模型生成有害或有偏见的内容，或当需要严格的输出格式或风格时。</p><p>如果可能，使用积极指令：不要告诉模型不要做什么，而是告诉它应该做什么。这可以避免混淆并提高输出的准确性。</p><p><strong>示例：</strong></p><figure class="highlight livecodeserver"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs livecodeserver">DO:<br>Generate <span class="hljs-keyword">a</span> <span class="hljs-number">1</span> <span class="hljs-keyword">paragraph</span> blog <span class="hljs-built_in">post</span> about <span class="hljs-keyword">the</span> top <span class="hljs-number">5</span> video game consoles. Only discuss <span class="hljs-keyword">the</span> console, <span class="hljs-keyword">the</span> company who made <span class="hljs-keyword">it</span>, <span class="hljs-keyword">the</span> year, <span class="hljs-keyword">and</span> total sales.<br><br>DO NOT:<br>Generate <span class="hljs-keyword">a</span> <span class="hljs-number">1</span> <span class="hljs-keyword">paragraph</span> blog <span class="hljs-built_in">post</span> about <span class="hljs-keyword">the</span> top <span class="hljs-number">5</span> video game consoles. Do <span class="hljs-keyword">not</span> list video game names.<br></code></pre></td></tr></table></figure><h3 id="7-5-控制最大token长度-Control-Max-Token-Length">7.5 - 控制最大token长度 (Control Max Token Length)</h3><p>要控制生成的LLM响应的长度，你可以在配置中设置最大token限制，或在提示中明确要求特定长度。例如：</p><figure class="highlight n1ql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs n1ql">&quot;<span class="hljs-keyword">Explain</span> quantum physics <span class="hljs-keyword">in</span> a tweet <span class="hljs-built_in">length</span> message.<span class="hljs-string">&quot;</span><br></code></pre></td></tr></table></figure><h3 id="7-6-在-Prompt-中使用变量-Use-Variables-in-Prompts">7.6 - 在 Prompt 中使用变量 (Use Variables in Prompts)</h3><p>在提示中使用变量，可以针对不同输入而改变。例如，提供关于城市的事实的提示。与其在提示中硬编码城市名称，不如使用变量。变量可以通过避免重复自己来节省时间和精力。如果需要在多个提示中使用相同的信息，可以将其存储在变量中，然后在每个提示中引用该变量。在将提示集成到自己的应用程序中时，这非常有意义。</p><p><strong>示例：</strong></p><figure class="highlight applescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs applescript">PROMPT: You are a travel guide. Tell <span class="hljs-keyword">me</span> a fact <span class="hljs-keyword">about</span> <span class="hljs-keyword">the</span> city: &#123;city&#125;<br><br>VARIABLES: &#123;city&#125; = <span class="hljs-string">&quot;Amsterdam&quot;</span><br><br>OUTPUT: Amsterdam <span class="hljs-keyword">is</span> a beautiful city full <span class="hljs-keyword">of</span> canals, bridges, <span class="hljs-keyword">and</span> narrow streets. It&#x27;s a great place <span class="hljs-keyword">to</span> visit <span class="hljs-keyword">for</span> <span class="hljs-keyword">its</span> rich history, culture, <span class="hljs-keyword">and</span> nightlife.<br></code></pre></td></tr></table></figure><h3 id="7-7-实验不同的输入格式和写作风格-Experiment-with-Input-Formats">7.7 - 实验不同的输入格式和写作风格 (Experiment with Input Formats)</h3><p>不同的模型、模型配置、提示格式、词汇选择和提交方式可能产生不同的结果。因此，实验提示属性如风格、词汇选择和提示类型（零样本、少样本、系统提示）很重要。</p><p>例如，对于一个目标是 “生成关于革命性视频游戏机Sega Dreamcast的文本” 的 Prompt，其可以表述为问题、陈述或指令，产生不同的输出：</p><ul><li>问题：Sega Dreamcast是什么，为什么它是如此革命性的游戏机？</li><li>陈述：Sega Dreamcast是世嘉于1999年发布的第六代视频游戏机。它…</li><li>指令：写一段描述Sega Dreamcast游戏机并解释为何它如此革命性的单段文字。</li></ul><h3 id="7-8-分类任务中混合类别-Mix-Classes-in-Few-shot">7.8 - 分类任务中混合类别 (Mix Classes in Few-shot)</h3><p>一般来说，few-shot （少样本）示例的顺序不太重要。然而，在进行分类任务时，确保在few-shot示例中混合可能的响应类别。这是因为否则你可能会过度拟合到示例的特定顺序。通过混合可能的响应类别，你可以确保模型正在学习识别每个类别的关键特征，而不是简单地记住示例的顺序。这将导致在未见数据上更强健和可泛化的性能。</p><p>一个好的经验法则是从6个few-shot 示例开始，并从那里开始测试准确性。</p><h3 id="7-9-适应模型更新-Adapt-to-Model-Updates">7.9 - 适应模型更新 (Adapt to Model Updates)</h3><p>跟踪模型架构变化、添加的数据和功能很重要。尝试更新的模型版本，并调整提示以更好地利用新的模型特性。</p><h3 id="7-10-尝试不同的输出格式-Experiment-with-Output-Formats">7.10 - 尝试不同的输出格式 (Experiment with Output Formats)</h3><p>例如，对于非创造性任务，如提取、选择、解析、排序、排名或分类数据，尝试让输出以结构化格式如JSON或XML返回。</p><p>从提取数据的提示中返回JSON对象有一些好处。在真实应用中，我不需要手动创建JSON格式，可以直接以排序顺序返回数据（处理日期时间对象时非常方便），但最重要的是，通过提示要求JSON格式，它迫使模型创建结构并限制产生幻觉。</p><p>使用JSON输出的好处总结：</p><ul><li>始终以相同风格返回</li><li>专注于你想要接收的数据</li><li>幻觉机会减少</li><li>使其关系感知</li><li>获得数据类型</li><li>可以排序</li></ul><h3 id="7-11-关于思维链-CoT-的最佳实践-Chain-of-Thought-Best-Practices">7.11 - 关于思维链(CoT)的最佳实践 (Chain of Thought Best Practices)</h3><p>对于CoT Prompt，将答案放在推理之后是必需的，因为推理的生成会改变模型在预测最终答案时获得的tokens。</p><p>使用CoT和自我一致性时，你需要能够从提示中提取最终答案，与推理分开。</p><p>对于CoT Prompt，将temperature设置为0。</p><p>思维链提示基于贪婪解码，根据语言模型分配的最高概率预测序列中的下一个词。一般来说，使用推理来得出最终答案时，可能只有一个正确答案。因此，temperature始终应设置为0。</p><h3 id="7-12-尝试与记录-Document-Prompt-Attempts">7.12 - 尝试与记录 (Document Prompt Attempts)</h3><p>前面已经提到过，但我们再次强调：详细记录你的提示尝试，这样你可以随时了解哪些方法行之有效，哪些不行。</p><p>提示输出可能在不同模型、不同采样设置，甚至同一模型的不同版本之间有所不同。此外，即使对同一模型的相同提示，输出句子格式和词汇选择也可能存在细微差异。（例如，如前所述，如果两个tokens具有相同的预测概率，则可能随机打破平局。这可能影响后续预测的tokens。）</p><p>建议创建一个Google表格，使用模板记录：</p><ul><li>提示名称和版本</li><li>目标（一句话解释这次尝试的目标）</li><li>使用的模型名称和版本</li><li>Temperature值（0-1之间）</li><li>Token限制</li><li>Top-K和Top-P值</li><li>完整提示</li><li>输出（或多个输出）</li></ul><p>还建议跟踪提示的版本（迭代），记录结果是否OK/NOT OK/SOMETIMES OK，以及反馈。如果有幸使用Vertex AI Studio，保存提示（使用与文档中相同的名称和版本），并在表格中跟踪保存的提示超链接。这样，你只需一次点击就可以重新运行提示。</p><p>在使用检索增强生成系统时，还应捕获影响插入到提示中的内容的RAG系统的特定方面，包括查询、块设置、块输出和其他信息。</p><p>一旦你觉得提示接近完美，将其带入项目代码库。在代码库中，将提示保存在与代码分开的文件中，这样更容易维护。最后，理想情况下，你的提示是一个可操作系统的一部分，作为提示工程师，你应该依靠自动化测试和评估程序来了解你的提示如何针对任务泛化。</p><p>提示工程是一个迭代过程。制作并测试不同的提示，分析并记录结果。根据模型的表现改进你的提示。继续实验直到达到期望的输出。当更改模型或模型配置时，回去继续实验先前使用的提示。</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/AI/">AI</category>
      
      <category domain="https://moreality.net/tags/Gemini/">Gemini</category>
      
      <category domain="https://moreality.net/tags/LLM/">LLM</category>
      
      <category domain="https://moreality.net/tags/Google/">Google</category>
      
      <category domain="https://moreality.net/tags/Prompt/">Prompt</category>
      
      <category domain="https://moreality.net/tags/%E6%8F%90%E7%A4%BA%E8%AF%8D/">提示词</category>
      
      
      <comments>https://moreality.net/posts/6318/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>vllm 的 cpu offload 参数卸载逻辑代码简析</title>
      <link>https://moreality.net/posts/28374/</link>
      <guid>https://moreality.net/posts/28374/</guid>
      <pubDate>Sun, 02 Mar 2025 09:08:43 GMT</pubDate>
      
      <description>最近在看 vllm 的代码，cpu offloading 这部分它的实现还是比较简单的，这里简单记录一下</description>
      
      
      
      <content:encoded><![CDATA[<blockquote><p>最近在看 vllm 的代码，cpu offloading 这部分它的实现还是比较简单的，这里简单记录一下。</p></blockquote><p>由于大模型的参数量实在很大，所以如果想在单机上运行一般都需要跑量化蒸馏后的模型，但是有时又不想牺牲模型质量，于是CPU/SSD 卸载成为一种折衷方案，通过增加推理时间来降低内存需求。</p><p>vllm 也实现了一个简单的 cpu offload 的机制，可以通过 <code>--cpu-offload-gb</code> 启用。</p><ul><li>官方文档: <a href="https://docs.vllm.ai/en/latest/getting_started/examples/basic.html#cpu-offload">https://docs.vllm.ai/en/latest/getting_started/examples/basic.html#cpu-offload</a></li></ul><p>这里发现他的实现还是比较简单的，主要就是通过这个 <a href="https://github.com/vllm-project/vllm/pull/6496">PR</a> ，添加了一个 func 叫 <code>maybe_offload_to_gpu</code>:</p><ul><li><a href="https://github.com/vllm-project/vllm/blob/82fbeae92b86e404829a01441334a9505e8b190d/vllm/model_executor/models/utils.py#L487-L540">model_executor/models/utils.py#L487-L540</a></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">maybe_offload_to_cpu</span>(<span class="hljs-params">module: torch.nn.Module</span>) -&gt; torch.nn.Module:<br>    device = <span class="hljs-built_in">next</span>(module.parameters()).device<br><br>    <span class="hljs-keyword">if</span> device == torch.device(<span class="hljs-string">&quot;cpu&quot;</span>):<br>        <span class="hljs-keyword">return</span> module<br><br>    <span class="hljs-keyword">global</span> _CPU_OFFLOAD_MAX_BYTES, _CPU_OFFLOAD_BYTES<br>    <span class="hljs-keyword">if</span> _CPU_OFFLOAD_BYTES &gt;= _CPU_OFFLOAD_MAX_BYTES:<br>        <span class="hljs-keyword">return</span> module<br><br>    pin_memory = is_pin_memory_available()<br><br>    <span class="hljs-comment"># offload parameters to CPU</span><br>    <span class="hljs-comment"># use pin_memory if possible, which helps cudagraph capture speed</span><br>    offloaded_parameters = <span class="hljs-literal">False</span><br>    <span class="hljs-keyword">for</span> p <span class="hljs-keyword">in</span> module.parameters():<br>        <span class="hljs-keyword">if</span> _CPU_OFFLOAD_BYTES &gt;= _CPU_OFFLOAD_MAX_BYTES:<br>            <span class="hljs-comment"># we use per-parameter offloading</span><br>            <span class="hljs-comment"># one module might have some parameters offloaded and some not</span><br>            <span class="hljs-keyword">break</span><br><br>        <span class="hljs-comment"># `torch.empty_like` does not support `pin_memory` argument</span><br>        cpu_data = torch.empty_strided(size=p.data.size(),<br>                                       stride=p.data.stride(),<br>                                       dtype=p.data.dtype,<br>                                       layout=p.data.layout,<br>                                       device=<span class="hljs-string">&#x27;cpu&#x27;</span>,<br>                                       pin_memory=pin_memory)<br>        cpu_data.copy_(p.data)<br>        p.data = cpu_data<br>        _CPU_OFFLOAD_BYTES += p.data.numel() * p.data.element_size()<br>        offloaded_parameters = <span class="hljs-literal">True</span><br><br>    <span class="hljs-keyword">if</span> offloaded_parameters:<br>        original_forward = module.forward<br><br>        <span class="hljs-keyword">def</span> <span class="hljs-title function_">forward</span>(<span class="hljs-params">*args, **kwargs</span>):<br>            module.forward = original_forward<br>            device_state = &#123;<br>                <span class="hljs-comment"># here we blindly call `to(device)`</span><br>                <span class="hljs-comment"># if the parameter is already on the device, it will be a no-op</span><br>                k: v.to(device, non_blocking=<span class="hljs-literal">True</span>)<br>                <span class="hljs-keyword">for</span> k, v <span class="hljs-keyword">in</span> module.state_dict().items()<br>            &#125;<br>            output = functional_call(module,<br>                                     device_state,<br>                                     args=args,<br>                                     kwargs=kwargs)<br>            module.forward = forward<br>            <span class="hljs-keyword">return</span> output<br><br>        module.forward = forward<br><br>    <span class="hljs-keyword">return</span> module<br></code></pre></td></tr></table></figure><p>这个函数只有一个地方调用了：</p><ul><li><a href="https://github.com/vllm-project/vllm/blob/82fbeae92b86e404829a01441334a9505e8b190d/vllm/model_executor/models/utils.py#L556-L560">model_executor/models/utils.py#L556-L560</a></li></ul><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs python">modules = torch.nn.ModuleList(<br>    [PPMissingLayer() <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(start_layer)] + [<br>        maybe_offload_to_cpu(layer_fn(prefix=<span class="hljs-string">f&quot;<span class="hljs-subst">&#123;prefix&#125;</span>.<span class="hljs-subst">&#123;idx&#125;</span>&quot;</span>))<br>        <span class="hljs-keyword">for</span> idx <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(start_layer, end_layer)<br>    ] + [PPMissingLayer() <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(end_layer, num_hidden_layers)])<br></code></pre></td></tr></table></figure><p>cpu offload 的整个流程可以概括为:</p><ol><li>将传入的 <code>cpu_offload_gb</code> 读取为<code> _CPU_OFFLOAD_MAX_BYTES</code>。</li><li>然后在构建 Module 时对每个 Layer 里面的参数从前往后依次塞到 CPU 的 PIN_MEMORY 上，并累加参数大小 (<code>_CPU_OFFLOAD_BYTES += p.data.numel() * p.data.element_size()</code>, 即参数数量 x 字节大小) 到 <code>_CPU_OFFLOAD_BYTES</code>，直到超过用户配置的可 offload 大小。</li><li>替换对应 Module 的 <code>forward</code> 函数，新的 forward 函数和原来的区别就是：<strong>在每次 forward 的时候，将 CPU 上的参数复制到 GPU 上，计算完后再释放</strong></li></ol><h2 id="一些可能的理解难点">一些可能的理解难点</h2><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">if</span> offloaded_parameters:<br>    original_forward = module.forward<br><br>    <span class="hljs-keyword">def</span> <span class="hljs-title function_">forward</span>(<span class="hljs-params">*args, **kwargs</span>):<br>        module.forward = original_forward<br>        device_state = &#123;<br>            <span class="hljs-comment"># here we blindly call `to(device)`</span><br>            <span class="hljs-comment"># if the parameter is already on the device, it will be a no-op</span><br>            k: v.to(device, non_blocking=<span class="hljs-literal">True</span>)<br>            <span class="hljs-keyword">for</span> k, v <span class="hljs-keyword">in</span> module.state_dict().items()<br>        &#125;<br>        output = functional_call(module,<br>                                 device_state,<br>                                 args=args,<br>                                 kwargs=kwargs)<br>        module.forward = forward<br>        <span class="hljs-keyword">return</span> output<br><br>    module.forward = forward<br></code></pre></td></tr></table></figure><p>这里的 forward 替换设计挺有意思：</p><ol><li><p>（<code>original_forward = module.forward</code>）：保存模块原始的前向传播函数到变量 <code>original_forward</code>，为后续恢复原始实现做准备。</p></li><li><p>（在 <code>forward</code> 函数内的 <code>module.forward = original_forward</code>）：在自定义的 <code>forward</code> 函数内部首行，当函数被调用时，立即将模块的前向传播方法恢复为原始方法。这一步确保 <code>functional_call</code> 能够使用原始的前向传播逻辑，避免递归调用。</p></li><li><p>（在 <code>forward</code> 函数内末尾的 <code>module.forward = forward</code>）：前向传播完成后，再次将模块的前向传播方法设置回自定义的 <code>forward</code> 函数，确保下次调用时仍能触发这个包含参数加载逻辑的自定义函数。</p></li><li><p>（函数末尾的 <code>module.forward = forward</code>）：为了确保每次都能 hook 到 cpu -&gt; gpu 这部分的逻辑 (参考原 PR 的解释：<a href="https://github.com/vllm-project/vllm/pull/6496/files#r1682069375">https://github.com/vllm-project/vllm/pull/6496/files#r1682069375</a>)</p></li></ol><p>这里的核心是使用 <code>functional_call</code> 配合临时的 <code>device_state</code>，实现参数从 CPU 到 GPU 的动态加载。初看可能会疑惑为什么没有将参数从 GPU 释放回 CPU 的逻辑，其实这是因为 <code>device_state</code> 是个局部变量，函数执行完毕后其引用的 GPU 张量会自动被 Python 的垃圾回收机制释放。而原始参数依然保存在 CPU 内存中，从而实现了内存优化的目的。</p><p>同时还有一个小知识点：</p><ol><li>对于 <code>nn.Module</code>, <code>.cuda()</code>, <code>.cpu()</code> 和 <code>.to(device)</code> 方法都是<strong>就地操作</strong>，因此可以直接 <code>model.cuda()</code></li><li>而对于 <code>torch.Tensor</code> , <code>nn.Parameter</code> 这些，<code>.to(device)</code>, <code>.cpu()</code>, <code>.gpu()</code> 方法本质都是创建一个新的 Tensor，原始 Tensor 不变，因此也必须使用 <code>a = b.cuda()</code></li></ol>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/vllm/">vllm</category>
      
      <category domain="https://moreality.net/tags/LLM-Serving/">LLM Serving</category>
      
      <category domain="https://moreality.net/tags/code-analysis/">code analysis</category>
      
      <category domain="https://moreality.net/tags/cpu-offloading/">cpu offloading</category>
      
      <category domain="https://moreality.net/tags/%E5%8F%82%E6%95%B0%E5%8D%B8%E8%BD%BD/">参数卸载</category>
      
      
      <comments>https://moreality.net/posts/28374/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>一个实现自动网页剪藏的工作流</title>
      <link>https://moreality.net/posts/51437/</link>
      <guid>https://moreality.net/posts/51437/</guid>
      <pubDate>Thu, 30 Jan 2025 10:31:18 GMT</pubDate>
      
      <description>在试用了一众“稍后读”和“剪藏”软件后，仍然找不到满意的，于是花了 1 小时简单搞了个适合自己的 workflow，整个流程联动了 Telegram Bot、Dropbox 和 Notion。</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="针对现有的软件，我的一些不满意的点">针对现有的软件，我的一些不满意的点</h2><p>在试用了一众 “稍后读” 和 “剪藏” 软件后, 仍然找不到满意的软件。</p><p>总结了一下针对当前的「网页剪藏」软件，现在自己不满意的几个点：</p><p><strong>1 - 快照使用「服务器采集」</strong>: cubox, pocket 等</p><p>这种方式的缺点就在于，很多内容平台，特别是国内的平台，都需要登录才能访问完整内容。</p><p>那么这样的采集方式受限就很大，服务器端采集无法携带用户的认证信息，因此往往只能获取到部分内容或者直接无法访问。</p><p><strong>2 - 自带的「网页解析」或者「解析为 Markdown」</strong>: obsidian / upnote 等笔记软件的 clipper</p><p>网页结构和样式的多样性使得通用解析器很难完美处理所有情况，特别是对于代码块、表格、数学公式等特殊格式，解析质量往往不尽如人意。</p><p><strong>3 - 仅能保存 URL 的「稍后读」</strong>: raindrop 等</p><p>内容的持久性在互联网上并不能得到保证，特别是：</p><ul><li>一些平台会定期清理历史内容</li><li>某些文章可能因各种原因被删除或修改</li><li>网站本身可能关停或改版</li><li>付费内容可能从免费变为收费</li></ul><p>对于一些时刻可能下架的敏感内容，或者经常变动的网站，url 可能过一阵子就看不了了。</p><h2 id="我理想中的「网页剪藏」">我理想中的「网页剪藏」</h2><ol><li>我希望功能<strong>不要过于复杂</strong>，例如各种各样的划线标注，笔记，阅读状态，都是我不需要的。</li><li>我想让我的保存，查看能够尽可能的简单高效，不要打乱我目前浏览网页的进度 —— <strong>也就是说不要在我保存的时候出现一个大表单让我填写，或者卡住等待插件处理和解析网页</strong>。</li><li>我希望能<strong>永久保存内容</strong>，在任何时候我可以看到「我当初看到这个网页的样子」。</li><li>我希望做到<strong>云存储</strong>，不希望作为一大堆散落的附件保存在本地。</li></ol><p>于是我搭建了一个联动 SingleFile, dropbox, notion, 和 telegram bot 的 workflow 来满足我的需求。</p><h2 id="我搭建的工作流">我搭建的工作流</h2><p>之所以说是工作流 Workflow，实际上就是将一大堆 API 串起来。</p><p>整体实现起来不难，把所有 API 写好，作为一个 notepad 喂给 cursor，基本上 10 分钟就完成了，然后接下来用了几个小时来完善整个流程。</p><p>以下是 Demo (youtube):</p><iframe width="100%" height="415" src="https://www.youtube.com/embed/JGOaaVcJYg0?si=AawbvtPagFl7OULX" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><blockquote><p>PS：demo 实际展示了 Telegram 的提示过程 + 服务端日志，实际上对于我而言只需要按一下「<code>cmd + shift + s</code>」 快捷键之后，剩下的一切都是服务端处理了。</p><p>PPS：演示的时候 DeepSeek 又卡了，还好加了一个备用的 API。。</p></blockquote><p>简单来说这个工作流如下：</p><ol><li>利用 <a href="https://github.com/gildas-lormeau/SingleFile">singlefile</a> 保存单页 html 作为快照。</li><li>通过 webdav 上传到服务端开始处理。</li><li>服务端上传文件到 dropbox 并获取分享链接 (可以直接点击查看快照页面) 。</li><li>服务端通过 deepseek / gpt 的 API 自动生成摘要和 tag。</li><li>将所有的元数据上传到 Notion Database 进行保存。</li><li>Telegram 的 bot 全程通知进度，以及报告成功 / 失败的错误日志。</li></ol><p>总体来说个人还是挺满意的，最近搞了不少自动化，这个算是折腾比较久的和比较好玩的，于是写出来分享下。</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/notion/">notion</category>
      
      <category domain="https://moreality.net/tags/workflow/">workflow</category>
      
      <category domain="https://moreality.net/tags/web-clipper/">web clipper</category>
      
      <category domain="https://moreality.net/tags/telegram/">telegram</category>
      
      <category domain="https://moreality.net/tags/dropbox/">dropbox</category>
      
      
      <comments>https://moreality.net/posts/51437/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>一些录音转文本(STT)的方案总结</title>
      <link>https://moreality.net/posts/8462/</link>
      <guid>https://moreality.net/posts/8462/</guid>
      <pubDate>Wed, 29 Jan 2025 11:26:37 GMT</pubDate>
      
      <description>录音转文本(STT)的几种主要方案，包括自部署、API、平台方案。同时也总结了一些 MacOS 上可以同时录制麦克风和系统声音的方案。</description>
      
      
      
      <content:encoded><![CDATA[<p>这几天有个需求，就是将一些中英文对话录音文件转文本，然后进行一些分析。所以就调研了一下录音转文本的方案。顺便总结一下 MacOS 上的录音／录屏方案。</p><p>STT 全称 Speech-to-Text，即语音转文本。一般有两种常见的需求：一种是将实时语音转文本的识别（流式处理），一种是将录音文件转文本。本文主要记录后者的一些方案。</p><h2 id="1-录音转文本的方案">1 - 录音转文本的方案</h2><h3 id="自部署方案">自部署方案</h3><p>目前最常见的模型是 <a href="https://github.com/openai/whisper">OpenAI 的 Whisper 模型</a>。</p><p>使用起来也非常简单，只需要两步：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">安装</span><br>pip install openai-whisper<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">使用</span><br>whisper --model base --language zh --output-dir ./output ./input.m4a<br></code></pre></td></tr></table></figure><p>目前一些其他的「自建」方案基本都是基于 OpenAI 的 whisper 模型: <a href="https://huggingface.co/models?sort=likes&amp;search=whisper">HuggingFace 的 Whisper 模型</a></p><p>这种自部署的优势就是安全，缺点是需要消耗本地算力，如果算力不足会比较慢，其次感觉 Whipser 对于纯英语识别来说比较好，但是对于中英文混杂的环境，效果还是比较差的。</p><h3 id="API-方案">API 方案</h3><p>用几家大厂的 API 方案, 比如：</p><ul><li><strong>OpenAI 的 Whisper API</strong></li></ul><p>目前价格是每分钟 $0.006：<a href="https://openai.com/api/pricing/">https://openai.com/api/pricing/</a></p><p><img src="https://youpai.roccoshi.top/img/202501292051697.png" alt="OpenAI Whisper Price"></p><p>其实这个价格也不算便宜了，转一个 1 小时左右的文本就要 3 RMB 左右。</p><ul><li><strong>Google 的 STT API</strong></li></ul><p>这个更贵，目前价格是每分钟 $0.016：<a href="https://cloud.google.com/speech-to-text/pricing">https://cloud.google.com/speech-to-text/pricing</a></p><p><img src="https://youpai.roccoshi.top/img/202501292004647.png" alt="Google STT V2 Price"></p><p>更多的一些厂商:</p><table><thead><tr><th>服务提供商</th><th>服务名称</th><th>收费标准</th><th>参考链接</th></tr></thead><tbody><tr><td><strong>微软 Azure</strong></td><td>Azure 语音服务</td><td>按秒计费，包含标准和自定义语音转文本，以及按字符计费的文本转语音服务</td><td><a href="https://azure.microsoft.com/zh-cn/pricing/details/cognitive-services/speech-services/">价格详情</a></td></tr><tr><td><strong>百度智能云</strong></td><td>语音识别服务</td><td>支持预付费包和后付费阶梯计价，根据语言模型类型定价</td><td><a href="https://ai.baidu.com/ai-doc/SPEECH/Tldjm0i4c">价目详情</a></td></tr><tr><td><strong>阿里云</strong></td><td>智能语音服务</td><td>一句话识别按次计费，录音文件按时长计费，语音合成按字符计费</td><td><a href="https://help.aliyun.com/zh/vms/product-overview/voice-services-pricing-in-china">费用说明</a></td></tr><tr><td><strong>腾讯云</strong></td><td>语音识别</td><td>实时识别和文件识别按日使用量计费</td><td><a href="https://cloud.tencent.com/document/product/1093/35686">计费概述</a></td></tr><tr><td><strong>华为云</strong></td><td>语音交互</td><td>按调用时长收费，支持按量付费和套餐包</td><td><a href="https://www.huaweicloud.com/zhishi/price-sis.html">服务价格</a></td></tr><tr><td><strong>Sonix</strong></td><td>转录服务</td><td>按小时收费或月度订阅制</td><td><a href="https://sonix.ai/zh/how-much-does-transcription-cost">价格详情</a></td></tr></tbody></table><p>总之，API的方案确实很方便，但是价格其实都并不便宜。</p><p>于是最后我还是将眼光瞄向了国内的一些在线平台。</p><h3 id="在线平台">在线平台</h3><p>主要试用了科大讯飞的讯飞听见，阿里的通义听悟和字节的飞书妙记。</p><p>用起来体验如下:</p><h4 id="阿里：通义听悟">阿里：通义听悟</h4><p>Link: <a href="https://tingwu.aliyun.com/">https://tingwu.aliyun.com/</a></p><p>阿里通义听悟在中英混合文本识别方面表现最为出色，且提供充足的免费额度（赠送500小时, 除此之外每天签到都可以领取 10 小时）</p><p><img src="https://youpai.roccoshi.top/img/202501292021868.png" alt="活动界面"></p><p>然后功能方面做的也很好，包括内容摘要，发言人识别，文本替换，AI改写等比较好用的功能。</p><p><img src="https://youpai.roccoshi.top/img/202501292025757.png" alt="界面"></p><h4 id="字节：飞书妙记">字节：飞书妙记</h4><p>Link: <a href="https://www.feishu.cn/product/minutes">https://www.feishu.cn/product/minutes</a></p><p>飞书妙记也还不错，每月提供300分钟免费额度，勉强也还够用。</p><p>但是如果额度一旦用完，价格就有点贵了，只能付费升级到「飞书 Plus」来用不限时。</p><p><img src="https://youpai.roccoshi.top/img/202501292026445.png" alt="price"></p><p><img src="https://youpai.roccoshi.top/img/202501292027373.png" alt="飞书妙记：界面"></p><p>另外界面做的也挺朴素的，功能不像阿里的那么丰富，但是比较简练，作为常用的会议记录的用途也足够了。</p><h4 id="科大讯飞-听见">科大讯飞: 听见</h4><p>每月仅提供20分钟免费额度，识别效果也相对一般，充值也很贵，39.8RMB/月。</p><p>性价比较低。</p><p><img src="https://youpai.roccoshi.top/img/202501292030543.png" alt=""></p><h2 id="2-MacOS-录音方案">2 - MacOS 录音方案</h2><p>作为录制会议或者演讲等用途，经常有一个需求就是想同时录制 麦克风 + 系统声音.</p><p>这里介绍一下我现在用的两种能获得录音音频的方式：</p><h3 id="1-使用-Screenshot-录屏">1 - 使用 Screenshot 录屏</h3><p>录屏的话 MacOS 的 Screeshot（截屏）可以直接做到：</p><ol><li><code>cmd + shift + 5</code> 打开录屏，在选项中选择「麦克风」即可。</li><li>录制结束后获得一个 <code>.mov</code> 视频文件。</li><li>用 <code>QuickTime Player</code> 打开视频文件，在「文件」菜单中选择「导出为音频文件」，即可获得一个 <code>.m4a</code> 音频文件。</li></ol><h3 id="2-单纯录音">2 - 单纯录音</h3><p>有时候我们不被允许录制屏幕，这时候只能通过单独录音的方式，而 MacOS 自带的 “QuickTime” 录音只能单独录制麦克风声音，这时候就需要用到其他手段。</p><h4 id="BlackHole">BlackHole</h4><p>一种方案是利用 BlackHole，BlackHole 作为一个虚拟音频设备，可以将 macOS 系统的音频输出捕获并重新路由，使其既能被录制软件录下又能通过扬声器播放出来。具体操作可以看：<a href="https://zhuanlan.zhihu.com/p/110962816">知乎：macOS使用BlackHole录制系统声音的同时输出声音<br></a></p><h4 id="QuickRecorder">QuickRecorder</h4><p>另一种就是通过安装其他应用的方式，一般用的最多的是 obs，但是 obs 太大了，搞起来比较复杂和麻烦。</p><p>于是我找到了一个使用起来还不错的很轻量的开源软件「QuickRecorder」：<a href="https://github.com/lihaoyun6/QuickRecorder">https://github.com/lihaoyun6/QuickRecorder</a></p><p><img src="https://youpai.roccoshi.top/img/202501292046765.png" alt="QuickRecorder"></p><p>使用这个软件录制出来会生成一个 <code>.qma</code> 文件</p><p><img src="https://youpai.roccoshi.top/img/202501292047086.png" alt=""></p><p>qma 这种文件包格式可以容纳 2个音频文件，以及一个属性文件。</p><p>使用 QuickRecorder 内置的 QMA 播放器打开 <code>.qma</code> 可以同步播放系统声音和麦克风声音，并且可以独立调节音量（调节之后，音量属性会被记录在文件信息里，下次打开这个文件还是这套音量配置）。如果不需要分享的话，直接用 qma 格式保存在硬盘上就行了。如果需要发给别人，qma 播放器自带导出功能，可以将两个音频文件按照用户设定的音量比例混缩成一个普通的单轨音频文件 <code>.mp3 / .m4a</code>。<a href="https://github.com/lihaoyun6/QuickRecorder/issues/91#issuecomment-2196471712">ref</a></p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/%E6%8A%80%E6%9C%AF/">技术</category>
      
      <category domain="https://moreality.net/tags/STT-Speech-to-Text/">STT(Speech-to-Text)</category>
      
      <category domain="https://moreality.net/tags/%E5%BD%95%E9%9F%B3/">录音</category>
      
      <category domain="https://moreality.net/tags/%E5%BD%95%E9%9F%B3%E8%BD%AC%E6%96%87%E6%9C%AC/">录音转文本</category>
      
      
      <comments>https://moreality.net/posts/8462/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>2024 年度订阅总结: 降本增效</title>
      <link>https://moreality.net/posts/16459/</link>
      <guid>https://moreality.net/posts/16459/</guid>
      <pubDate>Wed, 25 Dec 2024 01:29:41 GMT</pubDate>
      
      <description>总结一下 2024 我的订阅服务</description>
      
      
      
      <content:encoded><![CDATA[<p>年末逛各大论坛和博客的时候看到了不少老哥们的年度订阅总结，于是也打算闲着没事记录一下今年订阅了并且打算长期订阅的一些服务/APP。</p><p>我今年从上半年开始就用了一个非常优秀的订阅管理工具 <a href="https://github.com/ellite/Wallos">Wallos</a> 来记录自己每个月的开销：</p><p><img src="https://youpai.roccoshi.top/img/202412250933426.png" alt="wallos 的”统计“页面"></p><p>相比去年乱七八糟的 VPS 和各种有用没用的 APP 订阅，今年整体的开销算是精简了不少。</p><p>目前一年的费用算下来才 3800+，去年记得光是一年的 VPS 和科学上网订阅就不止这么多了。</p><p>目前我的订阅分类一共包含如下几块：</p><p><img src="https://youpai.roccoshi.top/img/202412250935315.png" alt="img"></p><p>下面单独分享一下对一些服务的感受：</p><h2 id="建站-下载">建站 + 下载</h2><p>自从上次<a href="https://moreality.net/posts/56316/">绿云服务器被黑</a>，建站已经懒得折腾了，目前用的就是阿里云的轻量服务器，之前用了一个 3500 代金券券所以算下来 8 年的 VPS 才不到 500 块，算是薅了一下阿里的羊毛。</p><p>下载和热备份一直用的 HostHatch 的瑞典大盘鸡，稳定性不错，1C1G1T 一年才 20刀。</p><p><img src="https://youpai.roccoshi.top/img/202412250937465.png" alt="img"></p><h2 id="工具">工具</h2><p><img src="https://youpai.roccoshi.top/img/202412250939529.png" alt="img"></p><h3 id="Claude-Pro">Claude Pro</h3><p>在下决心订阅 Claude Pro 之前，一直用的是 API 和合租面板的方式。</p><p>不过最近 OpenAI 的审查越来越严，模型时不时就降智，最关键的问题是还不知道它什么时候就降智了，最后就决定直接订阅 Claude Pro 了。</p><p>可能我是 Claude 刚出就注册了，加上 IP 一直很稳定，一个号从一而终用了一年多都没被封，还算幸运。</p><p>Claude 的质量是没话说的，Project 功能很给力，美中不足的是不能联网，目前需要联网查询的时候一般我就用 GPT 或者 Perplexity 的免费版代替了。</p><h3 id="滴答清单">滴答清单</h3><p>滴答清单一直是我心目中的 TOP 1 APP，目前已经持续用了 5 年多了。</p><p>作为一个 TBD 软件，各方面都很完美，我认为已经做到了极致。</p><p><img src="https://youpai.roccoshi.top/img/202412250943137.png" alt="img"></p><h2 id="书影音">书影音</h2><p><img src="https://youpai.roccoshi.top/img/202412250946000.png" alt="img"></p><p>微信读书一直在用 30 天挑战的方式 5 元续费一个月，还是很划算的，我认为是微信系列目前最良心的应用。</p><p>影视部分之前开过 MAX 和 Netflix，后面 NF 又是同户又是低价区封号的搞得很麻烦，就取消了，目前订阅了一个公费服 MisakaF，然后自己用 Google Drive + Emby 搭建了一个。</p><p>Spotify 明年不打算订阅了，应该就一直用 youtube music 了，感觉目前 ytm 的推荐做的已经很不错了。</p><h2 id="其他">其他</h2><h3 id="🪜">🪜</h3><p>这个用的就是几个 top ✈️ 之一，一个月 20 多，配合 <code>relay</code> 策略用的自己的落地，感觉比各种转发、优化线路要稳定的多。</p><p>说实话自从从 mjj 圈退坑之后，反而在线路方面一直没出过问题，而在自己之前折腾的时候，时间金钱精力花了一堆不说，线路还天天出故障，不是商家跑路就是机器被打，要么就是线路故障、运营商 QOS。现在看来这些时间花的真的毫无必要，不知道为什么那时候一直乐在其中。</p><h3 id="域名">域名</h3><p>两个 top 域名，一个 net 域名，平均下来一个月 15 元左右。</p><h3 id="手机卡">手机卡</h3><p>一个 29 元的每个月 300 G流量的电信卡（还支持结转，基本每个月从来用不完）。</p><p>一个 8 元的联通保号套餐。</p><h3 id="吃喝">吃喝</h3><p>闲鱼的 KFC 年卡，一个月只要 5 块，主要用来吃 6 折早餐，很划算。</p><p>京东 PLUS 会员，一年 60 吧。</p><p>其他乱七八糟的今年都没开。</p><h2 id="总结">总结</h2><p>总的来说对自己目前的预算控制还是比较满意的，算是已经精简到最小化了。</p><p>明年会继续控制其他方面的支出，但是对 AI 工具这方面一直保持一个开放探索的态度，一个月花 20 刀左右大幅提升生产力，个人觉得还是非常可以接受的。</p><blockquote><p>PS: 目前总结的这些只是订阅制的 APP，一次性开销的 APP 和服务等也买了很多，但是由于没有记录管理所以也懒得去翻了。</p></blockquote>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%9D%82%E8%AE%B0/">杂记</category>
      
      
      <category domain="https://moreality.net/tags/%E6%9D%82%E8%AE%B0/">杂记</category>
      
      <category domain="https://moreality.net/tags/%E8%AE%A2%E9%98%85/">订阅</category>
      
      <category domain="https://moreality.net/tags/wallos/">wallos</category>
      
      <category domain="https://moreality.net/tags/subscription/">subscription</category>
      
      
      <comments>https://moreality.net/posts/16459/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>2024 年度书影音总结</title>
      <link>https://moreality.net/posts/20893/</link>
      <guid>https://moreality.net/posts/20893/</guid>
      <pubDate>Tue, 24 Dec 2024 07:05:11 GMT</pubDate>
      
      <description>2024 是一个看书和电影电视剧比较多的一年，因此记录以下今年看过的一些好东西。</description>
      
      
      
      <content:encoded><![CDATA[<p><img src="https://youpai.roccoshi.top/img/202412241506062.png" alt="img"></p><p>2024 总体来说是很闲的一年，也是一个看书和电影电视剧比较多的一年，因此记录以下今年看过的一些好东西。</p><p>评价和评分全是主观色彩，并且仅仅代表当时时刻短暂的心情和观点，写这篇博客的内容也仅仅是回顾一下内容，以及自己当时的想法和感受，纯属一乐。</p><h2 id="读过的书">读过的书</h2><p><img src="https://youpai.roccoshi.top/img/202412241508433.png" alt="img"></p><p>这一年基本没读过纸质书，基本所有的书籍阅读都来自「微信读书」，在微信读书上截止到 12 月 24 日一共读了 260 多小时，远超前几年的总和。</p><p>决定以自己在 neodb 的评分排出一个 TOP10：</p><p><img src="https://youpai.roccoshi.top/img/202412241512462.png" alt="img"></p><h3 id="1-怦然心动">1 - 怦然心动</h3><blockquote><p>作者：文德琳·范·德拉安南</p><p>译：陈常歌</p></blockquote><p>之前很早的时候因为一些原因没有把电影看完，也记不清是为什么了。</p><p>重新一口气看完了整本书，觉得这不像是一个青春爱情故事，更像是一个讲述家庭和亲子关系的故事，看完觉得完全符合 9 分的高分评价。</p><p>我也开始在想，有些类型的作品真的书能比电影呈现出太多太多的细节。</p><h3 id="2-绿毛水怪">2 - 绿毛水怪</h3><blockquote><p>王小波</p></blockquote><p>没想到王小波也这么会写别离:</p><p>“我回想起来，她绝对温存，绝对可爱，生机勃勃，全无畏惧而且自信。我从她身上感到一种永存的精神，超过平庸生活里的一切。”</p><p>“我看了这些话发出一声长嚎，双手乱抓了一阵。我感到脑后一阵冰凉。我坐了很久，天黑下来，又亮起来。我机械地去吃饭，又机械地去干活，机械地回家来。我很孤独，真正的哀痛被我封闭起来了，我什么也不想。直到有一天下午大许推开我们的屋门，把夕阳和他长长的身影投进来。”</p><p>&quot;我跪倒下来，用痉挛的手指抓住盒子，抚摸盒子。我在哭吗？没有声也没有泪，只有无穷的惨痛从粗重的喘气里呼出来，无穷无尽。“</p><h3 id="3-观察的艺术">3 - 观察的艺术</h3><blockquote><p><a href="https://book.douban.com/subject/35807485/">https://book.douban.com/subject/35807485/</a></p></blockquote><p>一本值得反复观看的书，真的以很实用的阐述方式，为一成不变的日常人生提供了很多不同的观察角度。</p><p>之前也单独为这本书写了一个博客：<a href="https://moreality.net/posts/61511/">日常的缝隙 —— 「观察的艺术」阅读笔记</a></p><h3 id="4-寂静的巴黎">4 - 寂静的巴黎</h3><blockquote><p>申赋渔</p></blockquote><p>整本书在描述形形色色的人，不过前半本在描写身边的人，后半本在描述历史上的人，全篇行文透露出一种安静且悲凉的色彩，仿佛在看一部长长的纪录片。</p><h3 id="5-马斯克传">5 - 马斯克传</h3><blockquote><p>沃尔特·艾萨克森</p></blockquote><p>很长的一本传记，前前后后读了有两个多月才读完。</p><p>艾萨克森的传记写的真的很有水平。</p><p>有很多比较高深的概念他都可以用很简单易懂的方式阐述出来，包括在「乔布斯传」中呈现出来的一样。</p><h3 id="6-强风吹拂">6 - 强风吹拂</h3><blockquote><p>三浦紫苑</p></blockquote><p>非常燃的一本书，还好是 HE 。</p><p>文笔非常细腻，尤其是翻译十分到位，有不少句子翻译的特别传神。</p><p>唯一美中不足的就是书中的数据提升实在是有点假，不过也可以理解是为提高文学性牺牲了一点真实性了。</p><h3 id="7-撒哈拉的故事">7 - 撒哈拉的故事</h3><blockquote><p>三毛</p></blockquote><p>“哭, 笑, 爱, 憎, 梦里梦外, 颠颠倒倒”</p><h3 id="8-鱼不存在">8 - 鱼不存在</h3><blockquote><p>露露·米勒</p></blockquote><p>这本书是前几周听完孟岩的播客后去看的，一开始以为就是一本赞颂人类用坚韧意志对抗混乱的鸡汤书。</p><p>读到一半发现不是这样的，这本书在探讨一个非常宏大的概念。</p><p>关于宇宙，世界，生命。</p><p>关于变化和不变。</p><p>「每个人眼中都有一个世界，那个世界就存在于这个世界之中。」</p><h3 id="9-金色梦乡">9 - 金色梦乡</h3><blockquote><p>伊坂幸太郎</p></blockquote><p>非常多的 callback，铺垫很到位，喜欢这种欲言又止的结尾。</p><h3 id="10-你当像鸟飞往你的山">10 - <strong>你当像鸟飞往你的山</strong></h3><blockquote><p>塔拉·韦斯特弗</p></blockquote><p>远离那些喜怒无常并充满暴戾的控制欲的人。虽然没有经历过这些事情，但看完这本书算是能理解为什么有很多人接受着变态的家庭暴力却丝毫不反抗。</p><p>作者最后的决定非常的完美，那些持续发生的事情也印证了这个决断的正确性。</p><p>“从他们以及其他家人那里，我听说了山上正在上演的戏剧——受伤、暴力和来回变换的忠诚。但现在这些对我来说都成了遥远的传闻，他人的馈赠。我不知道分离是否是永久的，不知道是否有一天我将找到一条回家的路，但这种分离给我带来了平静。”</p><p>但是翻译总感觉有点怪，例如“我已觉察出我们是如何被别人给予我们的传统所塑造，而这个传统我们有意或无意地忽视了。我开始明白，我们为一种话语发声，这种话语的唯一目的是丧失人性和残酷地对待他人——因为培养这种话语更容易，因为保有权力总是让人感觉在前进。” 文中很多地方出现了这样不明所以的句子，理解起来颇为费劲。</p><h2 id="看过的电影-纪录片">看过的电影 / 纪录片</h2><h3 id="1-蒙古草原，天气晴">1 - 蒙古草原，天气晴</h3><p>“母亲没有找到马，普洁也来不及长大”。</p><p>在影片的最后一分钟前我还觉得会是一个不太完美的 happy ending，普洁会如愿完成学业，成为一个翻译，或至少是一个文化人，结果刀来的如此之快。</p><p>比起普洁的意外，母亲的死我觉得更加令人感到惋惜，绝望甚至愤怒，严重骨折＋内出血的情况下居然能拖半个月最后还被医院拒之门外，这得忍受多么窒息的痛苦，不仅对母亲本人还是家人而言。</p><p>整体影片的拍摄手法异常的简单，就是一个人举着一个像素很低，没有防抖的相机，拍出任何一个人都可以拍出的画面，但就这种真实更把这超出三界之外的意境表达的淋漓尽致。</p><h3 id="2-盲井">2 - 盲井</h3><p>复杂的人性，质朴的黑暗。</p><h3 id="3-大佛普拉斯">3 - 大佛普拉斯</h3><p>非常文艺的一部片子，虽然主线剧情比较薄弱，但是镜头，旁白和一些场景设计都非常给力, 金句频出。</p><p>这部电影的英文叫作「The Great Buddha」，Buddha 还是 Puta，即是有背景的人表面和内心的善与恶。</p><p>「我想现在虽然是太空时代，人类早就可以坐太空船去月球，但永远无法探索别人内心的宇宙。」</p><h3 id="4-冰血暴-1996">4 - <strong>冰血暴</strong> (1996)</h3><p>也许是剧版想要表达的东西太多太深邃，我其实更喜欢电影精炼简洁的内核。</p><h3 id="5-阳光普照">5 - <strong>阳光普照</strong></h3><p>整个节奏和「大佛普拉斯」非常相似，特别喜欢这种叙事节奏，同时表达的内核也完全不输大佛普拉斯。</p><h3 id="6-飞驰人生2">6 - 飞驰人生2</h3><p>如果没看过「飞驰人生」我会给他3分, 另外的一分来自于情怀分, 过年就应该看这种情怀爽片。</p><h3 id="7-网络谜踪2">7 - <strong>网络谜踪2</strong></h3><p>第一次看这种桌面电影，真的很过瘾啊，相当有趣的拍摄手法，同时情节也相对来说很有张力。</p><h3 id="8-疤面煞星">8 - <strong>疤面煞星</strong></h3><p>演技爆棚，say hello to my little friend！！</p><h3 id="9-在西伯利亚森林中">9 - 在西伯利亚森林中</h3><p>整个片子的节奏说不出来的舒服，冰天雪地，旷野寂静。</p><p>故事也比较喜欢，自由永远是相对的。</p><p>很喜欢那句「你逃的比我还远」。</p><h3 id="10-因果报应">10 - 因果报应</h3><p>由于提前看到了热评说是有反转的剧，所以强忍着前大半集的不适看玩的，电影的前 2/3 都是十分的&quot;粗制滥造&quot;，感觉角色完全没交代清楚，然后剧情也十分混乱。</p><p>最后的10分钟剧情强行把这部剧从不及格扳回到中高分，假如前半部分认真看了，即使没看懂（像我就完全分不清青年和老年的主角，片中的印度人长得实在太像了），最后也能理清楚是怎么一回事。</p><p>但是只能说前半部分可以加强的点太多了，即使是一部靠反转升华的剧，前面也可以交代的更清楚一些，又也许导演的意图就是让观众「前半部分云里雾里呢」。</p><h2 id="看过的电视剧-综艺">看过的电视剧 / 综艺</h2><h3 id="1-黑镜-第一、二、三季">1 - 黑镜 第一、二、三季</h3><h4 id="第一季">第一季</h4><p>想象力太丰富了，没有直接的恐怖元素但是令人看到脊背发凉。</p><p>三集每一集单拿出来都是近乎满分的存在。有点当时看爱死机的感觉了，不过黑镜更偏现实主义一些，并且更长的篇幅也带来了更多的深度情节，特别喜欢。</p><h4 id="第二季">第二季</h4><p>本季第二集真的让人产生一种生理性的不适，但是也足以证明了它构思的成功，估计我这种不适还要持续好几天。。</p><p>第一集和第三集就感觉立意没有第一季那几集刚看的时候那种新奇感了，比如第一集关于未来科技和赛博爱人的讨论不如S01E03的 grain 带来的那种震撼，而第三集的 waldo 又不如 S01E01 的 pig fucker 和 S01E02 的男主沉沦的全过程那么讽刺。</p><p>当然，只是个人的一些比较（由于两季几乎就是一起看完的，所以每一集印象都很深刻），总体来说第二季还是非常优秀。</p><h4 id="第三季">第三季</h4><p>第三季变成了 6 集，但整体感觉总体的质量和前两季还是有些差距，不过还是有几集特别喜欢的。</p><h3 id="2-喜剧之王·单口季-2024">2 - <strong>喜剧之王·单口季</strong> (2024)</h3><p>付航真的太强了，每一次发挥都如此出色。而且是在已经完美的表演上不断拔高。<br>其他演员也很出色，听了一百多小时刘仁铖的不开玩笑的播客，第一次见到真人。还很喜欢于祥宇和黑灯。<br>总之是一个比预期高出不少的节目。</p><h3 id="3-去他-的世界-第二季-2019">3 - <strong>去他*的世界 第二季</strong> (2019)</h3><p>用了一下午＋一晚上看完了整个第二季，今年目前为止最佳的剧，如果说第一季是纯粹的离经叛道和义无反顾的走向悲剧，那么第二季则多了许多温情和精神拉扯；</p><p>英国佬的黑色幽默确实就是一种变态美学，如果生活变得不受控制，除了说「Oh fuck I’m going to die」还能做什么呢。</p><p>另外，和第一季同样优秀的BGM。</p><h3 id="4-无耻之徒-3-4-5-8季">4 - 无耻之徒 (3,4,5,8季)</h3><p>PS：由于看了一下打分，无耻之徒实在占比太多，每一季都太优秀了，于是把自己评分最高的 4 季整在一起。</p><p>评价就是：<strong>「三百六十五条线，条条线炸」</strong>。</p><p>心目中目前的 top1 美剧。</p><h3 id="5-我的大叔">5 - 我的大叔</h3><p>一直很讨厌看韩剧和日剧，因为总感觉演员和剧本过于刻意且造作，这部剧可以说是追完用时最长的一部剧了，前前后后大概看了4个多月吧，不过20集不到而已，最后还是给了8分的高分。</p><p>剧本非常细腻，但其中还是有不少我不得不跳过的扭捏且无法理解的剧本，但是每当我想要弃坑的时候总会出现一些打动我的情节，总体来说可以算是我第一部认认真真看完的日韩剧。</p><h3 id="6-小谢尔顿-3-5-6季">6 - 小谢尔顿 (3,5,6季)</h3><p>电子榨菜类型的剧，这部剧前半部分和后半部分不太一样。</p><p>前半部分完全是看逗比和小演员们卖萌，而后半部分尤其是第五季开始，剧中的冲突开始多了起来，从家庭喜剧变成了家庭剧，但是整体同样优秀，每个人的人物形象都变得更加立体了。</p><h3 id="7-我的三体-第四季">7 - 我的三体 第四季</h3><p>大制作，除了人物还是方块已经没什么mc的影子了。</p><p>太短了，原来就拍到执剑就完了，还以为会把第三部拍完呢。</p><h3 id="8-亿万图谋">8 - 亿万图谋</h3><p>很不错的一部纪实类电影, 最后的 15 min 增加了些许悲凉, 小人物要想和大公司掰手腕真的太难了, 同时这两位主角让我想到了乔布斯和沃兹, 是不是每个成功(做出成功产品)的黑客身边都有一位懂技术的商业高手?</p><p>PS: 影片中的所有事件都在简单搜了搜, 在维基百科上查到了原型, 记录在: <a href="https://telegra.ph/2024-02-13-The-Billion-Dollar-Code-02-13">https://telegra.ph/2024-02-13-The-Billion-Dollar-Code-02-13</a></p><h3 id="9-双城之战-第二季">9 - 双城之战 第二季</h3><p>如果整部剧结束在第六集伊莎开枪的那一刻, 那么10分我愿意给11分。</p><p>但是最后三集我只能说是一坨, 不知道编剧在想什么。</p><h3 id="10-繁花">10 - 繁花</h3><p>阳春白雪</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E9%98%85%E8%AF%BB/">阅读</category>
      
      
      <category domain="https://moreality.net/tags/%E9%98%85%E8%AF%BB/">阅读</category>
      
      <category domain="https://moreality.net/tags/%E4%B9%A6%E5%BD%B1%E9%9F%B3/">书影音</category>
      
      <category domain="https://moreality.net/tags/%E8%B1%86%E7%93%A3/">豆瓣</category>
      
      <category domain="https://moreality.net/tags/neodb/">neodb</category>
      
      
      <comments>https://moreality.net/posts/20893/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Claude MCP 体验: 实现一个 Arxiv 论文搜索工具</title>
      <link>https://moreality.net/posts/5672/</link>
      <guid>https://moreality.net/posts/5672/</guid>
      <pubDate>Fri, 13 Dec 2024 07:20:43 GMT</pubDate>
      
      <description>在琢磨 Claude Desktop 配置的时候偶然发现 Claude 现在通过 MCP 可以支持自定义的 API 和数据源了, 于是来体验一下</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="MCP-介绍">MCP 介绍</h2><p>Anthropic (Claude) 在 11 月 25 日推出了一个协议： MCP (Model Context Protocol)。<sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://www.anthropic.com/news/model-context-protocol">[1]</span></a></sup></p><p>协议的目的是解决各类无法联网的 ChatBot 的信息孤岛问题。</p><p>比如 ChatGPT 在 ChatGPT Search 的加持下，可以做到搜索最新的网页信息并分析：</p><p><img src="https://youpai.roccoshi.top/img/202412131528515.png" alt="img"></p><p>但是 Claude 在内的许多模型目前还不支持：</p><p><img src="https://youpai.roccoshi.top/img/202412131529820.png" alt="img"></p><p>除此之外，许多时候我们希望访问一些自定义的数据源，比如本地的数据库等。</p><p>这些需求目前只能通过自己写代码 + 用 GPT 的 API 来进行操作。</p><p>MCP 的出现实际上就是用来解决这个问题：<strong>作为一个中间层，提供一个统一的协议和 SDK 来对接各类数据源</strong>。</p><p><img src="https://youpai.roccoshi.top/img/202412131532900.png" alt="img"></p><p>目前 Claude 在 <a href="https://modelcontextprotocol.io/introduction">https://modelcontextprotocol.io/introduction</a> 已经提供了完整开源的 <a href="https://github.com/modelcontextprotocol/python-sdk">python SDK</a> 和 <a href="https://github.com/modelcontextprotocol/typescript-sdk">TypeScript SDK</a></p><p>这个玩意也是我最近琢磨 Claude Desktop APP 的配置的时候才发现的，目前支持  MCP 的 Client 在 <a href="https://modelcontextprotocol.io/clients">这里</a> 可以看到:</p><table><thead><tr><th>Client</th><th><a href="https://modelcontextprotocol.io/docs/concepts/resources">Resources</a></th><th><a href="https://modelcontextprotocol.io/docs/concepts/prompts">Prompts</a></th><th><a href="https://modelcontextprotocol.io/docs/concepts/tools">Tools</a></th><th><a href="https://modelcontextprotocol.io/docs/concepts/sampling">Sampling</a></th><th>Roots</th><th>Notes</th></tr></thead><tbody><tr><td><a href="https://claude.ai/download">Claude Desktop App</a></td><td>✅</td><td>✅</td><td>✅</td><td>❌</td><td>❌</td><td>Full support for all MCP features</td></tr><tr><td><a href="https://zed.dev/">Zed</a></td><td>❌</td><td>✅</td><td>❌</td><td>❌</td><td>❌</td><td>Prompts appear as slash commands</td></tr><tr><td><a href="https://sourcegraph.com/cody">Sourcegraph Cody</a></td><td>✅</td><td>❌</td><td>❌</td><td>❌</td><td>❌</td><td>Supports resources through OpenCTX</td></tr><tr><td><a href="https://github.com/firebase/genkit">Firebase Genkit</a></td><td>⚠️</td><td>✅</td><td>✅</td><td>❌</td><td>❌</td><td>Supports resource list and lookup through tools.</td></tr><tr><td><a href="https://github.com/continuedev/continue">Continue</a></td><td>✅</td><td>✅</td><td>✅</td><td>❌</td><td>❌</td><td>Full support for all MCP features</td></tr><tr><td><a href="https://microsoft.github.io/genaiscript/reference/scripts/mcp-tools/">GenAIScript</a></td><td>❌</td><td>❌</td><td>✅</td><td>❌</td><td>❌</td><td>Supports tools.</td></tr></tbody></table><p>如果你开通了 Claude Pro 并且有 Claude Desktop，可以通过官网的这个 DEMO：<a href="https://modelcontextprotocol.io/quickstart">https://modelcontextprotocol.io/quickstart</a> 来实现一个获取天气和天气警报的联网查询功能。</p><h2 id="实践：实现一个-Arxiv-论文搜索工具">实践：实现一个 Arxiv 论文搜索工具</h2><p>我自己也通过简单修改了一下 quickstart <sup id="fnref:4" class="footnote-ref"><a href="#fn:4" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://modelcontextprotocol.io/quickstart">[4]</span></a></sup>，利用 python sdk 来实现了一个符合自己需求的 Arxiv 论文获取工具。</p><p>完整的代码在：<a href="https://github.com/Lincest/mcp-papersearch">https://github.com/Lincest/mcp-papersearch</a></p><p>简单介绍一下具体的使用方式:</p><h3 id="1-安装环境">1 - 安装环境</h3><p>uv 是一个 rust 编写的 python 包管理器，和 virtualenv 这些差不多，由于官网用的是 uv，于是就沿用一下。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">curl -LsSf https://astral.sh/uv/install.sh | sh<br></code></pre></td></tr></table></figure><p>clone 仓库：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell">git clone https://github.com/Lincest/mcp-papersearch.git<br><br>cd mcp-papersearch<br></code></pre></td></tr></table></figure><p>安装依赖：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">uv sync<br></code></pre></td></tr></table></figure><h3 id="2-配置-Claude">2 - 配置 Claude</h3><p>需要编辑 <code>~/Library/Application\ Support/Claude/claude_desktop_config.json</code></p><p>（windows路径在：<code>$env:AppData\Claude\claude_desktop_config.json</code>)</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">vim ~/Library/Application\ Support/Claude/claude_desktop_config.json<br></code></pre></td></tr></table></figure><p>内容为：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;globalShortcut&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;mcpServers&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>        <span class="hljs-attr">&quot;papersearch&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;command&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/Users/XXXX/.local/bin/uv&quot;</span><span class="hljs-punctuation">,</span> # 改成你的 uv 路径<br>            <span class="hljs-attr">&quot;args&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>                <span class="hljs-string">&quot;--directory&quot;</span><span class="hljs-punctuation">,</span><br>                <span class="hljs-string">&quot;/Users/XXX/code/mcp-papersearch&quot;</span><span class="hljs-punctuation">,</span> # 改成你的 clone 仓库的路径<br>                <span class="hljs-string">&quot;run&quot;</span><span class="hljs-punctuation">,</span><br>                <span class="hljs-string">&quot;papersearch&quot;</span><br>            <span class="hljs-punctuation">]</span><br>        <span class="hljs-punctuation">&#125;</span><br>    <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>把上面的路径按注释内容改一下，删除注释，然后重新启动 Claude Desktop：</p><p>这时候会发现右下角出现了个锤子🔨：</p><p><img src="https://youpai.roccoshi.top/img/202412131555459.png" alt="img"></p><p><img src="https://youpai.roccoshi.top/img/202412131555813.png" alt="img"></p><p>就说明运行成功了。</p><h3 id="3-测试">3 - 测试</h3><h4 id="case-01-搜索近期的论文">case 01: 搜索近期的论文</h4><p>问 Claude 一个简单的问题，比如</p><blockquote><p>搜索一下最近3天的 MoE Inference 相关论文</p></blockquote><p><img src="https://youpai.roccoshi.top/img/202412131556936.png" alt="img"></p><p>这时候会弹出一个窗口，就表示 Claude 识别到了应该调用我们的 papersearch 来搜索。</p><p>点击 Allow 后，发现他很智能的调用了两次，一次在3天内没找到后，又扩展范围到了30天来搜索：</p><p><img src="https://youpai.roccoshi.top/img/202412131558341.png" alt="img"></p><h4 id="case-02-搜索指定的论文">case 02: 搜索指定的论文</h4><p>比如我们要搜索 <a href="https://arxiv.org/abs/2308.12066">https://arxiv.org/abs/2308.12066</a> 这篇论文，标题是「Pre-gated MoE: An Algorithm-System Co-Design for Fast and Scalable Mixture-of-Expert Inference」</p><p>提问：</p><blockquote><p>搜索和总结 <a href="https://arxiv.org/abs/2308.12066">https://arxiv.org/abs/2308.12066</a></p></blockquote><p><img src="https://youpai.roccoshi.top/img/202412131641982.png" alt="img"></p><h3 id="总结">总结</h3><p>目前的 case 写的还很潦草，大部分就是直接让 claude 自己生成了然后简单改一改。</p><p>以后用来接入一些 DB 估计会比较好用。</p><h2 id="更多有趣的官方-demo">更多有趣的官方 demo</h2><ul><li><a href="https://modelcontextprotocol.io/examples#reference-implementations">https://modelcontextprotocol.io/examples#reference-implementations</a></li></ul><p>这里提供了更多有趣的官方推荐的 MCP Tools 和用法，几个个人觉得比较有意思的：</p><ol><li>实现和 Github 的交互： <a href="https://github.com/modelcontextprotocol/servers/tree/main/src/github">https://github.com/modelcontextprotocol/servers/tree/main/src/github</a></li><li>实现和 Obsidian 的交互：<a href="https://github.com/calclavia/mcp-obsidian">https://github.com/calclavia/mcp-obsidian</a></li><li>获取 Web 内容：<a href="https://github.com/modelcontextprotocol/servers/tree/main/src/fetch">https://github.com/modelcontextprotocol/servers/tree/main/src/fetch</a></li></ol><h3 id="通过-Fetch-获取-web-信息">通过 Fetch 获取 web 信息</h3><p>通过官方提供的 <a href="https://github.com/modelcontextprotocol/servers/tree/main/src/fetch">fetch</a>, 不需要下载任何代码，仅仅需要配置:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-attr">&quot;mcpServers&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>      <span class="hljs-attr">&quot;fetch&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>          <span class="hljs-attr">&quot;command&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;uvx&quot;</span><span class="hljs-punctuation">,</span><br>          <span class="hljs-attr">&quot;args&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>              <span class="hljs-string">&quot;mcp-server-fetch&quot;</span><br>          <span class="hljs-punctuation">]</span><br>      <span class="hljs-punctuation">&#125;</span><br>  <span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>就可以实现 web search 的功能了，例如查看北京天气：</p><p><img src="https://youpai.roccoshi.top/img/202412132012277.png" alt="img"></p><h2 id="Reference">Reference</h2><section class="footnotes"><div class="footnote-list"><ol><li><span id="fn:1" class="footnote-text"><span><a href="https://www.anthropic.com/news/model-context-protocol">https://www.anthropic.com/news/model-context-protocol</a><a href="#fnref:1" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:2" class="footnote-text"><span><a href="https://modelcontextprotocol.io/introduction">https://modelcontextprotocol.io/introduction</a><a href="#fnref:2" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:3" class="footnote-text"><span><a href="https://modelcontextprotocol.io/clients">https://modelcontextprotocol.io/clients</a><a href="#fnref:3" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:4" class="footnote-text"><span><a href="https://modelcontextprotocol.io/quickstart">https://modelcontextprotocol.io/quickstart</a><a href="#fnref:4" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:5" class="footnote-text"><span><a href="https://modelcontextprotocol.io/examples#reference-implementations">https://modelcontextprotocol.io/examples#reference-implementations</a><a href="#fnref:5" rev="footnote" class="footnote-backref"> ↩</a></span></span></li></ol></div></section>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/%E6%8A%80%E6%9C%AF/">技术</category>
      
      <category domain="https://moreality.net/tags/Claude/">Claude</category>
      
      <category domain="https://moreality.net/tags/Anthropic/">Anthropic</category>
      
      <category domain="https://moreality.net/tags/Claude-MCP/">Claude MCP</category>
      
      <category domain="https://moreality.net/tags/MCP/">MCP</category>
      
      <category domain="https://moreality.net/tags/Model-Context-Protocol/">Model Context Protocol</category>
      
      
      <comments>https://moreality.net/posts/5672/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Linux 系统 IPV4/IPV6 优先级配置 (gai.conf)</title>
      <link>https://moreality.net/posts/12422/</link>
      <guid>https://moreality.net/posts/12422/</guid>
      <pubDate>Wed, 11 Dec 2024 14:07:34 GMT</pubDate>
      
      <description>晚上遇到了一个甲骨文 G 口服务器 pip 安装特别慢的问题, 结果发现是系统默认 IPV6 优先导致的, 于是研究了一下 gai.conf 和 Linux 系统的 ipv4/ipv6 优先级配置问题</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="事情起因">事情起因</h2><p>Mac 上一直用的一台 G 口 Oracle 作为出口的代理来上网, 结果发现每次用 Oracle 的出口进行 <code>pip</code> 安装包时都特别慢, 速度大概 100KBps 不到, 换其他的出口就一点问题都没有, 之前尝试过换 DNS 也没啥用.</p><p>也测试过到 <code>pypi.org</code> 这个源的 ICMP, 也没发现什么问题.</p><p>结果今天仔细研究了一下, 发现 <code>pip</code> 的安装逻辑大概是这样的:</p><ul><li>pip 首先访问 <code>pypi.org</code> 获取包的元数据</li><li>然后会被重定向到 <code>files.pythonhosted.org</code> 这个 CDN 服务器下载实际的包文件</li><li><code>files.pythonhosted.org</code> 是 PyPI 的文件托管服务,用于存储和分发包文件</li></ul><p>然后我尝试在 oracle 机器上 ping 一下 <code>pythonhosted.org</code>, 结果发现 ipv6 居然没有 CDN, 延迟高达 160MS:</p><p><img src="https://youpai.roccoshi.top/img/202412112214659.png" alt="ping pythonhosted.org"></p><p>而众所周知一个双栈机器 (同时启用 IPv4 协议栈和 IPv6 协议栈的机器) 默认是 ipv6 优先的, 可能是为了推广 IPV6 之类的考虑吧…</p><p>当然这种优先级是可以通过修改配置文件改变的, 本文就讲讲今天研究的两种常见方式</p><h2 id="如何手动配置-IPV4-IPV6-优先">如何手动配置 IPV4 / IPV6 优先</h2><h3 id="1-gai-conf">1 - <code>gai.conf</code></h3><p>基本上所有 Linux 发行版都使用 <code>/etc/gai.conf</code> 来管理 <code>getaddrinfo</code> 的行为, 而所有需要域名解析的地方都需要用到 <code>getaddrinfo</code> , 因此修改这个配置文件就可以从根本上解决系统的 ipv4 / ipv6 优先级问题.</p><p>简而言之, 直接编辑 <code>/etc/gai.conf</code>, 取消注释或者添加这一行:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">precedence ::ffff:0:0/96  100<br></code></pre></td></tr></table></figure><p>即可设置成 ipv4 优先.</p><p>或者直接一行命令 sed 修改:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">sed -i &#x27;s/#precedence ::ffff:0:0\/96  100/precedence ::ffff:0:0\/96  100/&#x27; /etc/gai.conf<br></code></pre></td></tr></table></figure><h4 id="1-1-这行配置做了什么">1.1 - 这行配置做了什么</h4><ul><li><code>precedence</code>: 表示是在配置优先级</li><li><code>100</code>: 数字越大, 优先级越高</li><li><code>::ffff:0:0/96</code>: 这是一个特殊的 IPv6 地址前缀, 用于表示 IPv4-mapped IPv6 地址空间, 简而言之, 就是告诉系统, <strong>当遇到同时支持 IPv4 和 IPv6 的地址时, IPV4 地址的优先级</strong></li></ul><p>例子:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">precedence ::ffff:0:0/96  100    # IPv4-mapped IPv6 addresses<br>precedence ::/0            10     # 所有 IPv6 addresses<br></code></pre></td></tr></table></figure><p>在这个配置中：</p><ol><li>当一个域名同时有 IPv4 和 IPv6 记录时</li><li>IPv4 地址会被映射成 IPv4-mapped IPv6 address（即 <code>::ffff:0:0/96</code> 格式）</li><li>由于其优先级 100 大于纯 IPv6 地址的优先级 10</li><li>系统会优先使用 IPv4 地址进行连接</li></ol><h3 id="2-禁用-ipv6">2 - 禁用 ipv6</h3><p>出于某些原因我们可能完全不想要 ipv6, 这时可以直接彻底禁用网卡的 ipv6, 方式:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs shell">cat &gt;&gt; /etc/sysctl.conf &lt;&lt; EOF<br>net.ipv6.conf.all.autoconf = 0<br>net.ipv6.conf.default.autoconf = 0<br>net.ipv6.conf.all.accept_ra = 0<br>net.ipv6.conf.default.accept_ra = 0<br>net.ipv6.conf.all.disable_ipv6 = 1<br>net.ipv6.conf.default.disable_ipv6 = 1<br>net.ipv6.conf.lo.disable_ipv6 = 1<br>net.ipv6.conf.eth0.disable_ipv6 = 1<br>EOF<br></code></pre></td></tr></table></figure><p>来修改 <code>/etc/sysctl.conf</code></p><p>然后通过 <code>sysctl -p</code> 来重新加载配置文件, 这时 ipv6 就已经被彻底禁用. (还原的话直接删除这几行或者把0, 1反过来就ok)</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/%E6%8A%80%E6%9C%AF/">技术</category>
      
      <category domain="https://moreality.net/tags/ipv4/">ipv4</category>
      
      <category domain="https://moreality.net/tags/ipv6/">ipv6</category>
      
      <category domain="https://moreality.net/tags/getaddr/">getaddr</category>
      
      <category domain="https://moreality.net/tags/gai-conf/">gai.conf</category>
      
      
      <comments>https://moreality.net/posts/12422/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>简单测试各类大模型对一个枚举类问题的回答</title>
      <link>https://moreality.net/posts/20569/</link>
      <guid>https://moreality.net/posts/20569/</guid>
      <pubDate>Mon, 09 Dec 2024 07:12:01 GMT</pubDate>
      
      <description>在论坛上看到一个枚举类数学问题, 看看各类 LLM 模型都是如何解答的.</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="问题">问题</h2><p>今天逛 <code>linux.do</code> 论坛的时候看到这样一个<a href="https://linux.do/t/topic/261827/32">问题</a>:</p><blockquote><p>小李在纸上写了一个四位数A,把A的个位数字移到首位,得到另一个四位数 B,最后把A和B加起来得到和数C,并且C也是一个四位数。</p><p>已知A的各位数字之和是20,C的百位和十位数学分别是0和4。</p><p>那么C代表的四位数是?</p></blockquote><p>正确答案应该是 <code>8041</code>.</p><p>刚好这几天 GPT 降智严重, 所以一直在对比 Claude, ChatGPT 和 Gemini Pro, 就用这个问题问了大概 7, 8 次各类模型, 看看都是如何回答的.</p><p><img src="https://youpai.roccoshi.top/img/202412091521386.png" alt="img"></p><h2 id="OpenAI-GPT-4o">OpenAI GPT-4o</h2><h3 id="官网版本的-gpt4o">官网版本的 gpt4o</h3><blockquote><p>PS: 用的 IP 是 Oracle 的 IP, 实测 difficulty 值是 4 位数 (ref: <a href="https://linux.do/t/topic/261968">https://linux.do/t/topic/261968</a>)</p></blockquote><p>问了 10 次左右, 大约 7 次可以通过正确的写出 python code 并运行得到正确结果.</p><p><img src="https://youpai.roccoshi.top/img/202412091522336.png" alt="img"></p><p>另外的三次就是类似这样的无 code 回复, 答案也大概率就是错的.</p><p><img src="https://youpai.roccoshi.top/img/202412091532188.png" alt="img"></p><h3 id="平台反代的-GPT-4o">平台反代的 GPT-4o</h3><p>这个平台是之前一直合租的平台, 应该就是通过自己写了个镜像站来用多个 Pro 账号反代官网, 特征也很明显, 就是会用固定 IP 大量访问 GPT, 这一类的反代镜像站目前被 openai 限制的很严重, 我用的这个也因此停运了, 刚好在最后几天再通过这个问题测试一下.</p><ol><li>第一次, 直接没有通过程序求解, 并得出了错误的答案</li></ol><p><img src="https://youpai.roccoshi.top/img/202412091527698.png" alt="img"></p><ol start="2"><li>又尝试了几次, 勉强在写错五次程序后给出了正确结果</li></ol><p><img src="https://youpai.roccoshi.top/img/202412091528123.png" alt="img"></p><h2 id="Claude-3-5-sonnet">Claude 3.5 sonnet</h2><p>同样的问题问了五次以上的 Claude 3.5 sonnet, 均通过正确的 JS 代码给出了答案和分析, 并且相比于 chatgpt, 还证明了答案的唯一性.</p><p><img src="https://youpai.roccoshi.top/img/202412091534895.png" alt="img"></p><h2 id="Gemini-1-5-和-Gemini-exp-1206">Gemini 1.5 和 Gemini-exp-1206</h2><p>虽然 <a href="https://lmarena.ai/?leaderboard">在 Chatbot Arena 上坐拥 rank1</a>, 但是 Gemini 在这个问题的表现居然是三个模型里最烂的:</p><ol><li>直接问原始问题, 无论是否加 <code>code execution</code> 的选项, Gemini 系均不会给出代码和执行求解, 而是通过大量的文本分析试图找到答案, 并且 Gemini 对于中文的支持挺糟糕的, 如果不在 Prompt 中明确指定, 即使问题是中文也很有可能输出英文解答:</li></ol><p><img src="https://youpai.roccoshi.top/img/202412091537305.png" alt="img"></p><p>以下是模型的<strong>原始输出</strong>: (Gemini 1.5 pro 和 exp-1206 基本类似)</p><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br></pre></td><td class="code"><pre><code class="hljs txt">Here&#x27;s how we can solve this problem step-by-step:<br><br>Understanding the Problem<br><br>We have a four-digit number, A.<br><br>Moving the last digit of A to the front creates a new four-digit number, B.<br><br>The sum of A and B is a four-digit number, C.<br><br>The sum of the digits in A is 20.<br><br>The hundreds digit of C is 0, and the tens digit of C is 4.<br><br>Let&#x27;s use variables:<br><br>Let the digits of A be represented as a, b, c, and d, where a is the thousands digit, b is the hundreds digit, c is the tens digit, and d is the units digit.<br><br>So, A = 1000a + 100b + 10*c + d<br><br>And B = 1000d + 100a + 10*b + c<br><br>Formulating Equations<br><br>Sum of digits of A: a + b + c + d = 20<br><br>Structure of C: C = 1000*_ + 100*0 + 10*4 + _ (We don&#x27;t know the thousands and units digits of C yet)<br><br>A + B = C:<br>(1000*a + 100*b + 10*c + d) + (1000*d + 100*a + 10*b + c) = C<br><br>Analyzing the Sum (A + B = C)<br><br>When we add A and B, let&#x27;s consider the possible carry-overs:<br><br>Units column: d + c results in the units digit of C. There might be a carry-over to the tens column.<br><br>Tens column: c + b plus any carry-over from the units column results in 4 (the tens digit of C). There might be a carry-over to the hundreds column.<br><br>Hundreds column: b + a plus any carry-over from the tens column results in 0 (the hundreds digit of C). This means there must be a carry-over of 1 to the thousands column (since b + a cannot be negative).<br><br>Thousands column: a + d plus the carry-over of 1 from the hundreds column results in the thousands digit of C.<br><br>Solving the Equations<br><br>Hundreds column: Since b + a plus a carry-over results in 0, and we know there&#x27;s a carry-over of 1 to the thousands column, we have:<br>b + a + 1 (carry-over) = 10<br>Therefore, a + b = 9<br><br>Tens column: We know c + b plus a possible carry-over from the units column results in 4. Let&#x27;s consider two cases:<br><br>Case 1: No carry-over from the units column:<br>c + b = 4<br>Since a + b = 9, and a + b + c + d = 20, then d = 7.<br>But if d = 7, then in the units column, d + c (7 + c) would need to result in a number greater than or equal to 10 to have a carry-over. This would make c at least 3, and c + b would be greater than 4, contradicting our assumption of no carry-over. Therefore, this case is not possible.<br><br>Case 2: Carry-over of 1 from the units column:<br>c + b + 1 = 4<br>Therefore, c + b = 3<br><br>From equations a + b = 9 and c + b = 3, and knowing a + b + c + d = 20, we can deduce:<br>a = 9-b, c = 3-b and 9 + 3 - b + d = 20, then b + d = 8<br><br>Units column: Since there&#x27;s a carry-over of 1 to the tens column, d + c must be greater than or equal to 10. Substituting c = 3-b, we get:<br>d + 3 - b &gt;= 10<br>d - b &gt;= 7<br><br>Combining d - b &gt;= 7 and d + b = 8, we find that the only possible values are:<br>d = 8, b = 0<br><br>With d = 8 and b = 0, we can find the other values:<br>a = 9, c = 3<br><br>Calculating C:<br>A = 9038<br>B = 8903<br>C = A + B = 17941<br><br>However, the problem states that C is a four-digit number. This means our initial assumption that A is a four-digit number was incorrect. Let&#x27;s adjust our approach.<br><br>Revised Approach: A as a Three-Digit Number<br><br>Let&#x27;s assume A is a three-digit number (abc) and B becomes a four-digit number (bca) when the units digit is moved to the front.<br><br>A = 100a + 10b + c<br><br>B = 1000c + 100a + 10b<br><br>A + B = C<br><br>a + b + c = 20<br><br>C = 1000*_ + 100*0 + 10*4 + _<br><br>Analyzing the Sum (A + B = C) - Revised<br><br>Units column: c + b results in the units digit of C. There might be a carry-over.<br><br>Tens column: b + a plus any carry-over results in 4. There might be a carry-over.<br><br>Hundreds column: a + c plus any carry-over results in 0. There must be a carry-over of 1.<br><br>Thousands column: The carry-over of 1 is the thousands digit of C.<br><br>Solving the Equations - Revised<br><br>Hundreds column: a + c + 1 (carry-over) = 10, so a + c = 9.<br><br>Tens column:<br><br>Case 1: No carry-over from the units column: a + b = 4. But since a + b + c = 20, this would mean c = 16, which is not possible.<br><br>Case 2: Carry-over of 1 from the units column: a + b + 1 = 4, so a + b = 3.<br><br>Combining a + b = 3 and a + c = 9 with a + b + c = 20, we get:<br>3 + c = 20<br>Therefore c = 17, still not possible.<br><br>The issue is that if C is a 4 digit number, then A cannot be a 3 digit number, because the only way to have a carry of 1 into the thousands column is if A is a 4 digit number as well.<br><br>Let&#x27;s try one more approach: a, b, c, d must not all be single digits<br><br>We know a+b = 9, c+b=3, and a+b+c+d=20. If b=0, then a=9, c=3, and d=8, which gives us C as 17941. This means that at least one of a, b, c, d must be greater than 9.<br><br>Since b is in both a+b=9 and c+b=3, we know that if b is a two-digit number, these equations cannot both be true. Therefore, b must be a single digit.<br><br>If c is a two digit number, then the addition of c+b in the tens column would need to be 4, but there is also a carry-over of 1 from the units column. This means that c+b=13, and if c=10, b=3, a=-1, d=8. This also doesn&#x27;t work.<br><br>If d is a two-digit number, then when added to c, there must be a carry-over of 1. If d=10, then 10+c &gt;= 10, which is true for any single-digit c. However, we know c=3-b, and if c is negative, then a must be greater than 9 to compensate, which we have already tested.<br><br>If c=0, then b=3, a=6, d=11. This gives us 6300+11, 360+11, and 6300+360+22 = 6682.<br><br>Answer<br>C represents the number 6682.<br></code></pre></td></tr></table></figure><ol start="2"><li>在 Prompt 中加入「通过运行程序给出答案」后, 回答精简了很多, 并且也可以通过写出 python 程序并执行找到答案:</li></ol><p><img src="https://youpai.roccoshi.top/img/202412091540905.png" alt="img"></p><p><strong>但是</strong>, 多问几次后, 发现并不会每次都成功:</p><p><img src="https://youpai.roccoshi.top/img/202412091541173.png" alt="Gemini: 自己写了两次无法运行的 code.."></p><h2 id="总结">总结</h2><p>就这道题的测试结果而言:</p><ol><li><code>Claude 3.5 sonnet</code> 仍然是目前输出最快, 最稳定, 最准确的 coding 和 math 类模型.</li><li><code>OpenAI GPT</code> 很明显有能力做到和 Claude 类似, 但是估计是对 IP 的要求过高, 导致输出很不稳定, 而且对于同一个 IP, 甚至会出现随机降智.</li><li><code>Gemini</code> 虽然目前排名很高, 但是感觉还是名不副实…</li></ol>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/%E6%8A%80%E6%9C%AF/">技术</category>
      
      <category domain="https://moreality.net/tags/GPT/">GPT</category>
      
      <category domain="https://moreality.net/tags/Claude/">Claude</category>
      
      <category domain="https://moreality.net/tags/Gemini/">Gemini</category>
      
      
      <comments>https://moreality.net/posts/20569/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>数据安全：一次 VPS 宿主机被入侵的事件记录</title>
      <link>https://moreality.net/posts/56316/</link>
      <guid>https://moreality.net/posts/56316/</guid>
      <pubDate>Fri, 25 Oct 2024 03:36:58 GMT</pubDate>
      
      <description>做好「每台 vps 的数据都随时可能被人明文查看」的准备</description>
      
      
      
      <content:encoded><![CDATA[<p>即使是一个经营了长达 11 年的商家，还是会出现整台 VPS 母鸡被黑客入侵的情况。</p><p>并且令人感到愤怒的是，商家还对此做了隐瞒，不愿意承认是被入侵并泄露数据，而把责任归咎于是 RAID 出现故障。</p><p>经过这次事件，我再也不想去相信中小厂的公有云服务了。</p><p>强烈建议，在任何 VPS 部署服务时，做好 <strong>数据加密存储，并不在服务器上存储任何明文的 token 或密码</strong>。</p><p>以及，<strong>及时的备份数据</strong>。</p><h2 id="事件经过">事件经过</h2><h3 id="1-系统宕机和绿云工单回复">1 - 系统宕机和绿云工单回复</h3><ul><li><code>2024-10-21 16:24</code></li></ul><p>我发现一台在线时长将近一年的绿云 10 周年 VPS 自动重启了。</p><p>当时没太在意，以为就是例行维护。</p><p>随后我发现我的一些需要写入数据库的服务变得不可用，比如 <code>seafile</code> 和 <code>vaultwarden</code>。</p><p>登录上服务器发现文件系统全部变成了 <code>read-only</code>。</p><p>于是我发了一个工单：</p><p><img src="https://youpai.roccoshi.top/img/202410251149434.png" alt="img"></p><p>发工单后，我发现服务器直接宕机了，并且通过 SolusVM 面板无法进行重启。</p><ul><li><code>2024-10-21 18:16</code></li></ul><p>绿云客服回复，说是遇到了罕见的 RAID 故障。</p><p><img src="https://youpai.roccoshi.top/img/202410251150433.png" alt="img"></p><p>当时我认为，这种便宜的机器也许就是 RAID0 吧，如果修不好可能数据就没了。</p><p>不过好在做了备份。</p><ul><li><code>2024-10-22 17:29</code></li></ul><p>经过了一天的时间，发邮件询问情况。</p><p>绿云客服回复，RAID 无法恢复，只能 rebuild。</p><p>补偿是 1 年时间，或者按照购入价格全额退款。</p><p><img src="https://youpai.roccoshi.top/img/202410251153159.png" alt="img"></p><h3 id="2-商家的隐瞒">2 - 商家的隐瞒</h3><ul><li><code>2024-10-23</code> lowendtalk 网友发帖，披露 VPS 数据被入侵。</li></ul><p>原贴地址：<a href="https://lowendtalk.com/discussion/198766/my-greencloud-vps-got-ransomwared-the-entire-mothership/p1">https://lowendtalk.com/discussion/198766/my-greencloud-vps-got-ransomwared-the-entire-mothership/p1</a></p><p><img src="https://youpai.roccoshi.top/img/202410251157136.png" alt="img"></p><p>该网友进入了救援模式 (Rescue Mode)<sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded" aria-label="rescue mode">[1]</span></a></sup>后，可以看到所有的文件都被修改成了 <code>xxx@cock.li</code></p><p>我在进行相同操作的时候，确实也发现了，所有的文件都被 <strong>改名并加密</strong>，直接 google 搜索 junglesec，可以发现这是一个<strong>黑客组织</strong>，主要以勒索攻击（ransomware）而闻名。<sup id="fnref:2" class="footnote-ref"><a href="#fn:2" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://helpdesk.kaseya.com/hc/en-gb/articles/4407512513425-JungleSec-Ransomware-via-IPMI">[2]</span></a></sup></p><h2 id="究竟发生了什么">究竟发生了什么</h2><p>搜索一下关键字 <code>junglesec</code>, <code>VPS</code>, <code>IPMI</code>，就大概可以看懂是数据是如何被入侵的了。</p><p><strong>IPMI（智能平台管理接口）</strong> 是一种集成在服务器主板上的系统管理固件，这种固件允许管理员在操作系统无法访问时仍能进行远程管理，例如重启系统、查看系统状态等。因此，IPMI 提供了对系统的深层次访问权限。</p><h3 id="本次服务器攻击的步骤">本次服务器攻击的步骤</h3><ol><li><p><strong>通过 IPMI 登录获取访问权限</strong>：</p><ul><li><p>攻击者首先寻找启用了 IPMI 的设备，特别是那些<strong>使用默认密码</strong>且<strong>未充分加固</strong>的 IPMI 接口。</p></li><li><p>一旦攻击者登录成功，就可以通过 IPMI 远程控制服务器，即使系统的操作系统崩溃也能进行操作。</p></li></ul></li><li><p><strong>重启系统进入单用户模式</strong>：</p><ul><li>攻击者利用 IPMI 的远程访问功能，将目标服务器重启到<strong>单用户模式</strong>（Single User Mode）。在这个模式下，系统只加载必要的服务，并为管理员提供 root 权限，而无需输入密码。</li><li>攻击者在单用户模式下可以绕过所有用户账户的认证机制，直接获得系统的最高权限。</li></ul></li><li><p><strong>下载并编译加密程序</strong>：</p><ul><li>获得 root 权限后，攻击者在系统中<strong>下载并编译一个名为 ccrypt 的加密程序</strong>，攻击者利用它来加密系统中的所有文件。</li></ul></li><li><p><strong>发送勒索邮件</strong></p></li></ol><p>这并不是一个比较新鲜的攻击方式了，可以看到非常多的相关案例<sup id="fnref:3" class="footnote-ref"><a href="#fn:3" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://www.bleepingcomputer.com/news/security/junglesec-ransomware-infects-victims-through-ipmi-remote-consoles/">[3]</span></a></sup><sup id="fnref:4" class="footnote-ref"><a href="#fn:4" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://www.google.com/search?q=junglesec+ipmi">[4]</span></a></sup></p><h2 id="我们应该做什么">我们应该做什么</h2><p>在事件发生之后，我选择了退款 + 重新迁移数据到阿里云。</p><p>同时，由于之前过于相信商家 + 懒惰，很多备份的脚本和一些其他的定时任务，比如 vps 之间的 ssh 鉴权，比如许多上传和下载的 api key 和 token，我都明文保存在了 VPS 上。</p><p>因此，我重置了一些重要平台比如 google drive, onedrive, github, gitlab 的 auth token，同时清理了所有 vps 上的 <code>authorized_keys</code>。</p><p>还好目前部署的东西大多是玩具，这是一次很好的教训，之前用云服务的时候其实一直忽略了数据安全和隐私，觉得许多事情离我太遥远，然而当事情真正发生到自己身上时，再后悔就晚了。</p><p>一个很容易想到但是许多人压根不 care 的事情是，只要你的文件是明文存储的，那么 vps 厂商就可以读到你的明文文件。<sup id="fnref:5" class="footnote-ref"><a href="#fn:5" rel="footnote"><span class="hint--top hint--rounded" aria-label="[V2EX: 好奇很久的问题：云主机提供商能不能读取到用户放在主机中的数据?](https://v2ex.com/t/431090)">[5]</span></a></sup></p><p>因此，当 vps 宿主机出现被入侵时，你的文件也可以被其他人<strong>明文读取</strong>。</p><p>所以，最好的防护措施是：</p><ol><li>尽量只用大厂服务。</li><li>敏感信息不上云，上云就做好端到端加密和及时的备份。</li><li>不要在 vps 的一些 cronjob 中明文配置一些密码和token。</li><li>真正重要的基础服务还是不要自建，或者采用更鲁棒的方式比如做 k3s 之类的集群，否则关键时候出问题，即使有数据备份恢复起来还是很麻烦。</li></ol><h2 id="相关概念">相关概念</h2><h3 id="1-rescue-mode">1 - rescue mode</h3><blockquote><p>救援模式（Rescue Mode）是虚拟专用服务器（VPS）提供的一种应急维护功能，允许用户在无法正常启动系统或发生重大问题时，进入一个最小化的操作环境进行排障和修复。它通常是一个轻量级的 Linux 环境，不依赖原有的操作系统，类似于单机版的安全模式或Live CD。</p></blockquote><h3 id="2-junglesec">2 - junglesec</h3><blockquote><p>JungleSec 是一个<strong>黑客组织</strong>，主要以勒索攻击（ransomware）而闻名。该组织的活动在网络安全圈中引起了广泛关注，其主要手段是针对企业或个人的系统进行<strong>未授权访问</strong>，并通过<strong>加密文件</strong>或劫持网络基础设施实施勒索。</p><p><strong>主要的攻击方式</strong>：</p><ol><li><strong>勒索软件攻击</strong>：<br>JungleSec 通常在侵入目标系统后，将关键文件加密，并要求受害者支付赎金（通常以比特币等加密货币支付），否则威胁删除或公开敏感数据。</li><li><strong>攻击向量</strong>：<br>他们利用系统中的<strong>已知漏洞</strong>、<strong>弱密码</strong>、以及未充分配置的<strong>管理接口</strong>（如 IPMI，BMC）等来侵入系统。IPMI（Intelligent Platform Management Interface）是他们的主要目标之一，这是一种用于远程管理服务器的接口，如果未安全配置，就容易被攻击者利用。</li><li><strong>赎金要求</strong>：<br>JungleSec 的攻击一般要求受害者支付一定金额的比特币或其他加密货币。如果不支付，受害者的数据将无法解密。</li></ol></blockquote><h2 id="ref">ref</h2><section class="footnotes"><div class="footnote-list"><ol><li><span id="fn:1" class="footnote-text"><span>rescue mode<a href="#fnref:1" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:2" class="footnote-text"><span><a href="https://helpdesk.kaseya.com/hc/en-gb/articles/4407512513425-JungleSec-Ransomware-via-IPMI">https://helpdesk.kaseya.com/hc/en-gb/articles/4407512513425-JungleSec-Ransomware-via-IPMI</a><a href="#fnref:2" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:3" class="footnote-text"><span><a href="https://www.bleepingcomputer.com/news/security/junglesec-ransomware-infects-victims-through-ipmi-remote-consoles/">https://www.bleepingcomputer.com/news/security/junglesec-ransomware-infects-victims-through-ipmi-remote-consoles/</a><a href="#fnref:3" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:4" class="footnote-text"><span><a href="https://www.google.com/search?q=junglesec+ipmi">https://www.google.com/search?q=junglesec+ipmi</a><a href="#fnref:4" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:5" class="footnote-text"><span><a href="https://v2ex.com/t/431090">V2EX: 好奇很久的问题：云主机提供商能不能读取到用户放在主机中的数据?</a><a href="#fnref:5" rev="footnote" class="footnote-backref"> ↩</a></span></span></li></ol></div></section>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%9D%82%E8%AE%B0/">杂记</category>
      
      
      <category domain="https://moreality.net/tags/%E6%8A%80%E6%9C%AF/">技术</category>
      
      <category domain="https://moreality.net/tags/VPS/">VPS</category>
      
      <category domain="https://moreality.net/tags/%E7%BB%BF%E4%BA%91/">绿云</category>
      
      <category domain="https://moreality.net/tags/GreenCloud/">GreenCloud</category>
      
      <category domain="https://moreality.net/tags/junglesec/">junglesec</category>
      
      <category domain="https://moreality.net/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%85%A5%E4%BE%B5/">服务器入侵</category>
      
      <category domain="https://moreality.net/tags/ransomware/">ransomware</category>
      
      
      <comments>https://moreality.net/posts/56316/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Blelloch 并行扫描算法</title>
      <link>https://moreality.net/posts/54261/</link>
      <guid>https://moreality.net/posts/54261/</guid>
      <pubDate>Fri, 18 Oct 2024 13:31:32 GMT</pubDate>
      
      <description>一种求前缀和(扫描)的并行计算方法</description>
      
      
      
      <content:encoded><![CDATA[<blockquote><p>本文是对这篇 <a href="https://ashburnlee.github.io/2020/02/20/CUDA-%E6%89%AB%E6%8F%8F%E7%AE%97%E6%B3%95/">博客</a> 的一些补充和 python 代码实现</p></blockquote><h2 id="前缀和-PrefixSum-和扫描-Scan">前缀和 (PrefixSum) 和扫描 (Scan)</h2><p>在并行计算中，前缀和（prefix sum）被称为“扫描”（scan）主要是因为这个操作的本质涉及对一个序列中的元素进行累积或递推计算，类似于扫描或遍历序列。</p><p>其特点是，输出的每一个值有前缀依赖性，就是说每个输出依赖前面的所有输入。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">input:</span><br>[a0, a1, a2, a3, ..., an]<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">output:</span><br>[a0, (a0+a1), (a0+a1+a2), ..., (a0+a1+a2+...+an)]<br></code></pre></td></tr></table></figure><h2 id="Blelloch-并行扫描算法">Blelloch 并行扫描算法</h2><p>Blelloch 扫描算法（Blelloch scan algorithm）是一种高效并行实现前缀和（prefix sum）操作的算法，通常用于大规模并行计算中。</p><p>这个算法采用了一种基于二叉树的并行计算模式, 将传统的 <code>O(n)</code> 求前缀和的复杂度减少为 <code>O(log(n))</code>。</p><p>Blelloch 扫描算法由两个主要步骤组成：</p><ol><li><strong>向上（Upsweep）阶段</strong>：<ul><li>该阶段是通过一棵隐含的二叉树自底向上构建累积和。</li><li>在此过程中，每一个节点汇聚子节点的值。</li><li>经过 <code>log(n)</code> 次迭代，最终根节点处存储了整个数组的总和。</li></ul></li><li><strong>向下（Downsweep）阶段</strong>：<ul><li>在这个阶段，算法从树的根节点开始，自顶向下计算前缀和，同时传播累积和到子节点。</li><li>在每一层中，左子节点继承父节点的值，而右子节点加上父节点的值。</li></ul></li></ol><p>具体可以参考这个图:</p><p><img src="https://youpai.roccoshi.top/img/202410182140844.png" alt="img"></p><p>这里用一个 python 代码来展示每一步的具体过程:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">blelloch_scan</span>(<span class="hljs-params">arr</span>):<br>    arr = <span class="hljs-built_in">list</span>(arr)<br>    n = <span class="hljs-built_in">len</span>(arr)<br><br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Initial array:&quot;</span>, arr)<br><br>    step = <span class="hljs-number">1</span><br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;\nUpsweep Phase:&quot;</span>)<br>    <span class="hljs-keyword">while</span> step &lt; n:<br>        <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">0</span>, n, <span class="hljs-number">2</span> * step):<br>            <span class="hljs-keyword">if</span> i + step &lt; n:<br>                arr[i + <span class="hljs-number">2</span> * step - <span class="hljs-number">1</span>] += arr[i + step - <span class="hljs-number">1</span>]<br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;After step size <span class="hljs-subst">&#123;step&#125;</span>, array: <span class="hljs-subst">&#123;arr&#125;</span>&quot;</span>)<br>        step *= <span class="hljs-number">2</span><br><br>    arr[-<span class="hljs-number">1</span>] = <span class="hljs-number">0</span><br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;\nSetting root to 0, array:&quot;</span>, arr)<br><br>    step = n // <span class="hljs-number">2</span><br>    <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;\nDownsweep Phase:&quot;</span>)<br>    <span class="hljs-keyword">while</span> step &gt; <span class="hljs-number">0</span>:<br>        <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">0</span>, n, <span class="hljs-number">2</span> * step):<br>            <span class="hljs-keyword">if</span> i + step &lt; n:<br>                temp = arr[i + step - <span class="hljs-number">1</span>]<br>                arr[i + step - <span class="hljs-number">1</span>] = arr[i + <span class="hljs-number">2</span> * step - <span class="hljs-number">1</span>]<br>                arr[i + <span class="hljs-number">2</span> * step - <span class="hljs-number">1</span>] += temp<br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;After step size <span class="hljs-subst">&#123;step&#125;</span>, array: <span class="hljs-subst">&#123;arr&#125;</span>&quot;</span>)<br>        step //= <span class="hljs-number">2</span><br><br>    <span class="hljs-keyword">return</span> arr<br><br>input_array = [<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">0</span>,<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">0</span>,<span class="hljs-number">1</span>,<span class="hljs-number">2</span>]<br>arr = blelloch_scan(input_array)<br>prefix_sum_array = [input_array[i] + arr[i] <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-built_in">len</span>(input_array))]<br><br><span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;\nFinal PrefixSum array: <span class="hljs-subst">&#123;prefix_sum_array&#125;</span>&quot;</span>)<br></code></pre></td></tr></table></figure><p>输出:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs shell">Initial array: [1, 2, 0, 1, 2, 0, 1, 2]<br><br>Upsweep Phase:<br>After step size 1, array: [1, 3, 0, 1, 2, 2, 1, 3]<br>After step size 2, array: [1, 3, 0, 4, 2, 2, 1, 5]<br>After step size 4, array: [1, 3, 0, 4, 2, 2, 1, 9]<br><br>Setting root to 0, array: [1, 3, 0, 4, 2, 2, 1, 0]<br><br>Downsweep Phase:<br>After step size 4, array: [1, 3, 0, 0, 2, 2, 1, 4]<br>After step size 2, array: [1, 0, 0, 3, 2, 4, 1, 6]<br>After step size 1, array: [0, 1, 3, 3, 4, 6, 6, 7]<br><br>Final PrefixSum array: [1, 3, 3, 4, 6, 6, 7, 9]<br></code></pre></td></tr></table></figure><h2 id="ref">ref</h2><ul><li>CUDA-扫描算法 (blog): <a href="https://ashburnlee.github.io/2020/02/20/CUDA-%E6%89%AB%E6%8F%8F%E7%AE%97%E6%B3%95/">https://ashburnlee.github.io/2020/02/20/CUDA-扫描算法/</a></li><li>wikipedia - prefixsum: <a href="https://en.wikipedia.org/wiki/Prefix_sum">https://en.wikipedia.org/wiki/Prefix_sum</a></li></ul>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/%E6%8A%80%E6%9C%AF/">技术</category>
      
      <category domain="https://moreality.net/tags/CUDA/">CUDA</category>
      
      <category domain="https://moreality.net/tags/%E5%B9%B6%E8%A1%8C%E8%AE%A1%E7%AE%97/">并行计算</category>
      
      <category domain="https://moreality.net/tags/%E5%89%8D%E7%BC%80%E5%92%8C/">前缀和</category>
      
      
      <comments>https://moreality.net/posts/54261/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>记一次 mac 剪刀脚键盘的修复</title>
      <link>https://moreality.net/posts/17585/</link>
      <guid>https://moreality.net/posts/17585/</guid>
      <pubDate>Tue, 15 Oct 2024 05:28:27 GMT</pubDate>
      
      <description>用了几天没有方向键的 mac.. 以后拆电子设备的时候一定要全方位看看结构图</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="背景">背景</h2><blockquote><p>感觉方向键里进了什么东西, 然后就直接把 mac 键盘的方向键翘起来清理了一下内部, 然后就发现装不回去了…</p></blockquote><p>由于之前也有过拆 mac 键盘的经验, 拆过很多次正方形按键 (字母, 数字键), 都是翘起来, 然后按回去就行了, 于是这次没有多想就直接把方向键也这么操作了, 然后发现怎么都按不回去.</p><p>最后参考了 b 站的这个视频: <a href="https://www.bilibili.com/video/BV1Ve411c7VE">Macbook 剪刀键盘方向键键帽安装</a> 才发现事情没有那么简单.</p><p>剪刀脚的方向键和 mac 其他的正方形的数字按键不同, 这玩意拆了键帽不把剪刀脚支架一起拆下来, 强行是 <strong>压不回去的</strong>.</p><p>由于大力按压, 键帽的左边的卡扣已经压断了, 只能上 PDD 新买一个键帽. (这玩意一个就是几十块.)</p><h2 id="操作">操作</h2><p>最后记录一下 mac 上方向键的结构和安装步骤, 图片来源: <a href="https://5ms.top/x045o">小红书</a></p><p>注意 Step7 这一步特别难操作, 建议用一个镊子或者别针之类的东西, 卡住支架的右侧, 然后把左边挂上去, 总之 <strong>切忌用蛮力!</strong></p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410151334415.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410151334016.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410151334826.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410151334175.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410151334233.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410151334409.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410151335566.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410151335413.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410151335491.png" alt="img"></div></div></div><h2 id="总结">总结</h2><p>以后拆任何电子产品都得小心了, 尤其是那种做工比较细节的, 可能相似的部件设计却完全不同, 然后不提前看看结构大力出奇迹就悲剧了.</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%9D%82%E8%AE%B0/">杂记</category>
      
      
      <category domain="https://moreality.net/tags/%E6%9D%82%E8%AE%B0/">杂记</category>
      
      <category domain="https://moreality.net/tags/mac/">mac</category>
      
      <category domain="https://moreality.net/tags/%E9%94%AE%E7%9B%98/">键盘</category>
      
      <category domain="https://moreality.net/tags/%E5%89%AA%E5%88%80%E8%84%9A/">剪刀脚</category>
      
      
      <comments>https://moreality.net/posts/17585/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>日常的缝隙 —— 「观察的艺术」阅读笔记</title>
      <link>https://moreality.net/posts/61511/</link>
      <guid>https://moreality.net/posts/61511/</guid>
      <pubDate>Fri, 04 Oct 2024 16:02:34 GMT</pubDate>
      
      <description>“世界是一座博物馆，”他说，“你就是讲解员。”</description>
      
      
      
      <content:encoded><![CDATA[<p>国庆没有出去看人海的计划，于是阅读了不少影视和书籍作品，其中有「观察的艺术」<sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://book.douban.com/subject/35807485/">[1]</span></a></sup>这本书给我留下挺深刻的印象。</p><p>个人觉得这是一本值得反复观看的书，真的以很实用的阐述方式，为一成不变的日常人生提供了很多不同的观察角度。</p><p>许多时候，总是感觉一件事就这么发生了，然后产生了一些情绪影响，却没有去仔细想过，究竟是什么东西让我产生了这种感觉。这本书的一个很大的价值就是，用了一系列非常细节的情景描述 / 方法论，说明了一个简单的道理：</p><blockquote><p>视觉、声音、气味、体感、时间、温觉，人的感觉远远不止五感，但一切都是值得仔细体验和观察的。</p></blockquote><p>这本书其实给了我一些对于「应该或者不应该」做什么的指示，很多时候我觉得我的脑回路不被人理解，比如在某个地方干着与这个地方气质不相干的事情，但是这本书肯定了我的一些行为，告诉我「其实这么做也是另一种思路和角度」。</p><h2 id="一些书摘">一些书摘</h2><p>由于文笔有限，在此记录一些书中个人觉得很有价值和思考的文字。</p><blockquote><p>生命中，你总是有各种各样逃避的借口，种种责任纷至沓来，让你无比疲惫，这些都会限制你人生的宽度。但人生的全部意义就在于，尽自己所能从外面的世界获取最多的东西。世间之事，全在用心。你的注意力就是你的生命力。它让你和他人产生联结，让你感到渴望。保持渴望，保持同他人的联结，在平凡的日常中寻找自己感兴趣的事，留意别人忽视的东西——这些技巧一般人很难做好，但却十分重要。</p></blockquote><blockquote><p>睡觉前，她会聚精会神地听，努力分辨出每一种噪声：远处的狗叫声，空调的嗡嗡声，一辆车驶过的声音。</p></blockquote><blockquote><p>有一次，我和妻子在一所当代艺术博物馆闲逛时，走进了一个小厅，里面除了两个大木箱，什么也没有。这两个箱子令我困惑，因为不确定里面是装满了等待拆箱的艺术品，还是它们本身就是艺术品。</p></blockquote><blockquote><p>如果你想看见过去没有留意到的东西，向上看，便是一个很棒的选择。对初学者来说，你只需要不时地把视线从手机上移开，抬起头向上看。朝着不在正前方、却在上方的事物，抬起你的眼睛。</p></blockquote><blockquote><p>你不必到世界的另一头，也能找到新的地方进行探索。翻翻地图，想想哪里是你完全不熟悉的地方，然后给自己安排一次愉快的漫步。</p></blockquote><blockquote><p>你不太可能发现世界历史遗迹和名胜古迹。你想要寻找、吸收和享受的，是更普通、更日常的细节——一个有创意的商店标志、一盘偏离正轨的棋局、一座随意而迷人的教堂、一辆停在路边的古董车、一个碰巧骑着独轮车的路人。</p></blockquote><blockquote><p>“步行、骑自行车或开车——走不同的路线，去往一个相同的目的地。”他说，“如果你每天都以同样的方式通勤，你就不会注意到任何事物。事实上，到达目的地几分钟后，你就已经对整个旅程毫无印象了。进入这种僵尸般的通勤状态，实际上是在偷走你自己的时间。而新的路线能使旅行更加活跃和有趣。</p></blockquote><h2 id="一些碎碎念">一些碎碎念</h2><p>这些日子特别喜欢看 Links<sup id="fnref:3" class="footnote-ref"><a href="#fn:3" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://www.linksphotograph.com/">[3]</span></a></sup> 的视频，最近看过的印象很深的一个视频就是冰岛徒步<sup id="fnref:4" class="footnote-ref"><a href="#fn:4" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://www.youtube.com/watch?v=PRYyp2A1RBk&t=650s">[4]</span></a></sup>的这一段：</p><iframe width="100%" height="415" src="https://www.youtube.com/embed/PRYyp2A1RBk?si=ofS_4ak_nHRntpci" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><p>在这里 links 和小螃蟹遇到了一个徒步的法国小哥。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410050035821.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410050036773.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410050038658.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202410050039747.png" alt="img"></div></div></div><p>特别喜欢这一段对这个小哥的描述，想到了书中的一段话：</p><p><img src="https://youpai.roccoshi.top/img/202410050042046.png" alt="img"></p><p>并不是在否认拍照和自拍这些行为，而是有些时候，也许我们真的过于追逐打卡式的游览和带着目的任务，而因此不断丧失观察力和专注力。</p><blockquote><p>“世界是一座博物馆，”他说，“你就是讲解员。”</p></blockquote><p><img src="https://youpai.roccoshi.top/img/202410050044045.png" alt="img"></p><h2 id="reference">reference</h2><section class="footnotes"><div class="footnote-list"><ol><li><span id="fn:1" class="footnote-text"><span><a href="https://book.douban.com/subject/35807485/">https://book.douban.com/subject/35807485/</a><a href="#fnref:1" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:2" class="footnote-text"><span><a href="https://book.douban.com/review/14424006/">https://book.douban.com/review/14424006/</a><a href="#fnref:2" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:3" class="footnote-text"><span><a href="https://www.linksphotograph.com/">https://www.linksphotograph.com/</a><a href="#fnref:3" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:4" class="footnote-text"><span><a href="https://www.youtube.com/watch?v=PRYyp2A1RBk&amp;t=650s">https://www.youtube.com/watch?v=PRYyp2A1RBk&amp;t=650s</a><a href="#fnref:4" rev="footnote" class="footnote-backref"> ↩</a></span></span></li></ol></div></section>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E9%98%85%E8%AF%BB/">阅读</category>
      
      
      <category domain="https://moreality.net/tags/%E9%98%85%E8%AF%BB/">阅读</category>
      
      <category domain="https://moreality.net/tags/%E8%A7%82%E5%AF%9F%E7%9A%84%E8%89%BA%E6%9C%AF/">观察的艺术</category>
      
      
      <comments>https://moreality.net/posts/61511/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>北戴河-秦皇岛之行</title>
      <link>https://moreality.net/posts/32942/</link>
      <guid>https://moreality.net/posts/32942/</guid>
      <pubDate>Tue, 24 Sep 2024 04:39:57 GMT</pubDate>
      
      <description>想一直待在海边</description>
      
      
      
      <content:encoded><![CDATA[<blockquote><p>人的心境也真是够滑稽的了。因为它会无可救药地被鸡毛蒜皮的日常生活所左右，却也能在风的感触与初夏气息的撩拨下心花怒放。人心是那么深奥，却也浅薄得不可思议。人之所以能活下去，也一定有这份浅薄的功劳吧。</p><p>——「旅行之木」</p></blockquote><p>北京到秦皇岛是非常划算的出行计划之一，绿皮车票只要不到 50 元，现在正值淡季，各个旅游景点也都没有什么人，除了晚上的天气很冷之外，北戴河各方面都是相当完美。</p><p>总结一下旅行日志 + 在本文中详细把自己的行程安排记录下来。</p><iframe width="100%" height="415" src="https://www.youtube.com/embed/934n8gYWk4o?si=Vt3gV-MNDyxsuFak" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe><h2 id="准备工作">准备工作</h2><ul><li><strong>门票</strong>：可以用「北戴河全域旅游」小程序，基本上可以直接在里面购买所有景区的门票，而且比单独在景区公众号 / 线下购买要便宜，可以直接刷码进入，不需要身份证。</li><li><strong>车票</strong>：建议买到北戴河站的火车，如果没有可以买秦皇岛站的，不过市区过来比较远（公交1h左右）。但是两个车站都有直达景区的公交车，淡季不怕没座位。</li><li><strong>景区出行</strong>：北戴河区没有共享单车，有专门的景区自行车，基本每个景点门口都有。不过还是强烈建议租一个电动车，大约50元 / 天，可以骑100多km，秦皇岛比想象中的小很多，实测北戴河转三四圈 + 骑到海港区（市区）一个来回都绰绰有余。</li></ul><h3 id="行程安排概要">行程安排概要</h3><p>由于只有两天，我的计划就是直接逛完北戴河区 + 海港区，此外山海关太远，阿那亚太贵，就没有去。</p><ul><li><strong>第一天</strong>：联峰山 (15:00) - 老虎石 (17:00) - 环海公路 (18:00) - 秦皇小巷 (19:00)</li><li><strong>第二天</strong>：鸽子窝 (5:00) - 碧螺塔 (9:00) - 海滨骑车 (10:30) - 秦皇小巷 (12:00) - 西港花园 (14:00) - 金梦海湾 (16:30)</li></ul><h2 id="1-联峰山">1 - 联峰山</h2><p>下午两点到的酒店，先是出发去老虎石转了一圈，感觉没有什么特别值得逛几个小时的景点，于是就去了附近的联峰山打算登高望远一下。</p><p>联峰山有好几个山峰，其中望海亭可以看到北戴河区全景 + 大海，爬上山顶大概只需要 20min 左右。</p><p><img src="https://youpai.roccoshi.top/img/202409241319205.png" alt="img"></p><p><img src="https://youpai.roccoshi.top/img/202409241320012.png" alt="img"></p><h2 id="2-老虎石">2 - 老虎石</h2><p>老虎石是一个绝佳的欣赏日落的地方，可以提前查好当天的日落时间，然后提前 30min 左右爬上石头等着。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241324922.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241325437.png" alt="img"></div></div></div><p>从整个太阳落下去到天呈现蓝粉色大概就 10min 左右的时间。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241329677.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241329077.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241330867.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241331855.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241331268.png" alt="img"></div></div></div><p>不知道为什么博客的图片压缩好像有问题，HDR 图片居然被压缩出这种环形波纹了。。</p><h2 id="3-环海公路">3 - 环海公路</h2><p>这条路就是老虎石旁边 50 m 处的一个公路，实际能看到海的路段没有很长，就几百米的样子。</p><p>不过蓝调时刻这个路段真的很美。</p><p><img src="https://youpai.roccoshi.top/img/202409241334967.png" alt="img"></p><p><img src="https://youpai.roccoshi.top/img/202409241335478.png" alt="img"></p><h2 id="4-夜市：秦皇小巷">4 - 夜市：秦皇小巷</h2><p>夜市在刘庄和秦皇小巷纠结了一会，看刘庄夜市风评一般，就直接骑着小电驴来海港区的秦皇小巷了 (30min)，个人体验相当好，物价不算太高（和北京比起来），味道也很好。</p><p>于是第二天的午饭 + 晚饭也都是在秦皇小巷吃的，把几个比较火的小店都打卡了一遍。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241342241.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241342157.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241343319.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241343457.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241343154.png" alt="img"></div></div></div><h2 id="5-鸽子窝">5 - 鸽子窝</h2><p>早上 5 点就起来去鸽子窝看日出了，由于酒店就定在鸽子窝旁边，走过去相当方便，同时鸽子窝这块也是距离海港区最近的，比较建议酒店定在这。</p><p>5 点的北戴河气温只有 8 度，我提前看了天气预报，带了一件毛衣过来，还是很冷。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241348586.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241346489.png" alt="练气功的大爷"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241347886.png" alt="img"></div></div></div><p>老虎石看日落有一点远处的山和房子挡着，相比之下鸽子窝就是纯纯的正对一望无际的大海。</p><p>日出的速度比日落更快，能肉眼感觉到太阳一下就钻出来的感觉，大概全程也就几十秒的时间太阳就冒出来了。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241350819.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241350766.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241351853.png" alt="img"></div></div></div><h2 id="6-碧螺塔">6 - 碧螺塔</h2><p>这里是北戴河一个人造味最浓的景区，也诞生了不少的网红打卡点，即使是淡季每个打卡点排队的人都很多。</p><p>晚上这里有演出，还有很多卖夜宵和啤酒的店，想必是相当热闹，不过白天都没开门。</p><p>票价也很贵，白天观光票就要 50，晚上好像要 180，不过如果要拍人像的话，这里确实挺值得来的。(在这里不拍人比拍人更难~)</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241356054.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241356829.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241356866.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241357898.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241357423.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241357097.png" alt="img"></div></div></div><h2 id="7-西港花园">7 - 西港花园</h2><p>西港花园（开埠地）是秦皇岛老港口，后面废弃以后改造成了旅游景点，门票也是免费的。</p><p>个人非常喜欢这里的景色，相比碧螺塔那种人造景观，感觉这里的工业风更加真实且浪漫。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241402718.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241402528.png" alt="img"></div></div></div><p>从大码头到海誓塔那一段路真的非常漂亮，左边是大海和许多航行的帆船、货轮，右边可以看到秦皇岛的整个城区海岸。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241409352.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241409793.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241411004.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409241410669.png" alt="img"></div></div></div>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/%E7%94%9F%E6%B4%BB/">生活</category>
      
      <category domain="https://moreality.net/tags/%E5%8C%97%E6%88%B4%E6%B2%B3/">北戴河</category>
      
      <category domain="https://moreality.net/tags/%E7%A7%A6%E7%9A%87%E5%B2%9B/">秦皇岛</category>
      
      <category domain="https://moreality.net/tags/%E5%A4%A7%E6%B5%B7/">大海</category>
      
      
      <comments>https://moreality.net/posts/32942/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>近期对日记与笔记的一些折腾</title>
      <link>https://moreality.net/posts/60356/</link>
      <guid>https://moreality.net/posts/60356/</guid>
      <pubDate>Fri, 13 Sep 2024 15:45:51 GMT</pubDate>
      
      <description>近期自己又掀起了对笔记/日记软件的折腾热情, 再重新使用 typora, obsidian, notion, upnote, google keep, 备忘录, dayone, 一本日记等等软件后的一些感受</description>
      
      
      
      <content:encoded><![CDATA[<p>上来先祭出觉得一张特有意思的图，讽刺了很多折腾 note app 的玩家：</p><p><img src="https://youpai.roccoshi.top/img/202409132346550.jpg" alt="img"></p><p>确实，这张图一点毛病没有，许多折腾软件尤其是笔记软件的朋友总会自嘲到：「笔记软件比笔记还多」，和各种器材党一样，许多人也将之视为一种爱好而不是真正有什么生产力需求。</p><p>我自诩不是那么的器材党吧，只是最近在折腾「日记」这个应用的时候遇到了不小的阻碍，而刚好日记又和笔记 app 存在大量的交集，于是又重新试了许许多多的日记 + 笔记软件，最终谨以此文总结一下这段时间的折腾。</p><h2 id="Dayone：昂贵的价格，坑爹的同步">Dayone：昂贵的价格，坑爹的同步</h2><p>Dayone 可以说是目前来说综合评价最高的「日记」软件了，具体可以看这篇文章<sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://sspai.com/post/55715">[1]</span></a></sup> 中对各种「日记 app 」的对比。</p><p><img src="https://youpai.roccoshi.top/img/202409132355705.png" alt="img"></p><p>我个人当时也是被其全平台的特质所吸引，由于我的手机是 android，而日常用的笔记本是 macos，之前一直在使用的「日记」方式是滴答清单，采用的是类似这样<sup id="fnref:2" class="footnote-ref"><a href="#fn:2" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://www.xiaohongshu.com/discovery/item/630c9aef0000000011035a11?source=webshare&xsec_token=ABpku-AiP_848_XFhEZ60oTQedOKawPWKHnV0Qaz7PwGg=&xsec_source=pc_share">[2]</span></a></sup> 的方式：</p><p><img src="https://youpai.roccoshi.top/img/202409140000713.png" alt="img"></p><p>这样的方式有个问题就是当一天想法太多的时候总是存在着「一篇文章记不下」的情况，而且「N年日记」的方式虽然可以看到「那年今日」，但是如果想要更加丰富的面板，还是略逊了一筹。</p><p>并且滴答清单的「任务」管理的模式，在记录日记的时候如果不小心将该任务标记为完成，其就会复制当前的内容并创立下一个循环的任务，如果后续想有补充就还得找到明年今天的那个任务来追加内容。</p><p>总而言之，在持续该模式将近两年后，我还是决定找一个更加专业一些的日记软件。</p><p>于是眼光就瞄准了 Dayone。</p><p>除了全平台的优秀支持，Dayone 还提供了一些特色功能，例如自动记录「地点」，「天气」。这些顿时让每天的记录充满了烟火气。</p><p>于是我迫不及待的订阅了付费版。</p><p>但随着进一步的深度使用，两个月后我又放弃了 Dayone，理由如下：</p><ol><li>没有认真对待安卓平台，安卓和macos的同步坑的不行，我在实际使用中经常会出现一个 case：在安卓上写了一些内容，然后macos打开，发现该内容是白的（没有同步过来），但又由于 macos 打开了该笔记，导致 macos 的同步把安卓的同步覆盖了，<strong>这样辛苦写的内容就没有了</strong>。这也是我退订该 app 的<strong>核心原因</strong>。</li><li>web 版本会出现图片等信息丢失的情况，总之还是同步问题。</li><li>高昂的售价 + 不可买断，国区一年要 200 RMB+，仅仅只用于日记这个价格确实太奢侈了，虽然土耳其区可以便宜很多，但是 google，steam 这些公司都这么打压土区了，apple 的低价区又能持续多久呢。</li></ol><p>于是我想找一个能替代 Dayone 的，全平台同步的东西，目标也从一开始的日记软件逐渐放开到了一切便签/笔记软件。</p><p>在此之前，我主力的笔记软件一直是 obsidian + typora，功能已经调教到了近乎完美，但是就是移动端同步太费劲，对于「手机简单记录」这个功能的支持实在不够。</p><p>然后从弃用 Dayone 开始，我直接开始了长期的日记 + 便签 + 笔记软件的探索，以下记录我对这期间的一些软件的评价。</p><h2 id="Google-Keep：难过的中文搜索">Google Keep：难过的中文搜索</h2><p>Google Keep 作为一个便签软件，有着大厂背书加上不错的 UI 设计，个人一开始入手的时候还是挺喜欢的，但是用了没两天，问题又逐渐暴露出来：</p><p><img src="https://youpai.roccoshi.top/img/202409140012047.png" alt="Google Keep (web)"></p><ol><li><p><strong>移动端不支持中文搜索</strong>（这个问题我看 play 上很早就有人提了，但是 Google 一直没管，完全想不通，作为一个顶级搜索引擎大厂，居然连自家的笔记软件都不做支持）。</p></li><li><p>Google 对于非核心应用的「黑历史」：一开始我也是从 <a href="https://sspai.com/post/31771">为什么我不用Google Keep？</a> 开始看的，后续也逐渐了解到，原来 Google 都抛弃了那么多看起来「热门」但盈利不够的的项目：</p><ul><li>那些被 Google 放弃的项目: <a href="https://killedbygoogle.com/">https://killedbygoogle.com/</a></li></ul><p>同样作为一个非盈利项目，难以不怀疑 Google 什么时候就会脑子一热就给停了。</p></li><li><p>没有 Markdown 等一些基本编辑模式的支持，就仅仅只是纯文本 + 图片。</p></li></ol><h2 id="Journey-Diarium：能不能好好做-mac-端">Journey &amp; Diarium：能不能好好做 mac 端</h2><p>这俩货都是自诩「全平台」的日记应用，单看移动端（尤其是 Android）确实不错，能平替甚至秒杀 dayone，但是当我下载这俩的 macos 桌面端时，我都被丑到不忍直视。。。</p><h2 id="一些国产应用-一本日记，卡片日记等">一些国产应用 - 一本日记，卡片日记等</h2><p>在最初使用一本日记时，我还花 30 多买了高级会员，结果同上的，macos / web 版本设计的一塌糊涂，甚至连上传图片等功能都没有，其他例如天气 / 地点也是就给了个 input field 要你自己填。</p><p><img src="https://youpai.roccoshi.top/img/202409140022006.png" alt="「一本日记」的 web 版，macos 只能说更糟糕"></p><p>其他的国产「日记」应用也是大差不差，要么就是没有 web / PC端，要么就是做的一坨狗屎，总之没有找到一个全平台都做的很好的东西。</p><h2 id="一些目前用起来比较满意的-APP">一些目前用起来比较满意的 APP</h2><p>上述都是一些吐槽的话，接下来说一下比较满意的 APP 和自己目前的 笔记/日记 workflow。</p><h3 id="Notion-好用的模板">Notion: 好用的模板</h3><p>Notion + 日记模板的方式可以达到比较好的「日记」效果，一个是 Notion 的数据库 + 自定义视图可以实现多种查看方式，再一个是 Notion 的多端同步和 app 已经相当好用了。</p><p>由于 Notion 的数据库实在太强大了，这里就不过多介绍如何设计，以免把思路局限住了。就说几个我觉得非常有用的针对「日记」的视图设计：</p><h4 id="1-那年今日和那月今日效果">1 - 那年今日和那月今日效果</h4><p>可以通过</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-title function_">formatDate</span>((<span class="hljs-title function_">prop</span>(<span class="hljs-string">&quot;日期&quot;</span>)),<span class="hljs-string">&quot;DD&quot;</span>)==<span class="hljs-title function_">formatDate</span>(<span class="hljs-title function_">now</span>(),<span class="hljs-string">&quot;DD&quot;</span>)<br></code></pre></td></tr></table></figure><p>的方式实现（假设「日期」是创建时期），那年今日改成 MM-DD 即可。</p><h4 id="2-自动创建每日日记">2 - 自动创建每日日记</h4><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409140030464.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202409140031309.png" alt="img"></div></div></div><p>直接创建一个模板然后配置「重复」即可。</p><h3 id="Upnote：完美的「备忘录」平替">Upnote：完美的「备忘录」平替</h3><p>如果说 Google Keep 是一个蹩脚的 Apple 备忘录平替，那么 upnote<sup id="fnref:5" class="footnote-ref"><a href="#fn:5" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://getupnote.com/">[5]</span></a></sup> 就是一个比较完美的备忘录平替，其不仅支持多平台 (ios + macos + windows + android + linux)，而且经实测不论是同步速度，还是对 Markdown 的完美支持，对比起备忘录都是有过之而无不及，强烈建议体验。</p><p>另外，upnote 采用本地优先的方式，打开速度非常快，并且可以 Markdown 导出想要的文档（包括图片，附件等）。</p><h3 id="Obsidian：PC-端上的完美产品">Obsidian：PC 端上的完美产品</h3><p>只能用「近乎完美」来形容的笔记软件，需要花一定时间适应插件生态，一但接受了就爽的不行。</p><p>唯一的缺点就在于「多端同步」，并且我由于个人在读论文这些，大量的 pdf 塞进去以后 vault 变得非常大，同步到手机并不是什么好选择。</p><h3 id="Typora：好用的-Markdown-编辑器">Typora：好用的 Markdown 编辑器</h3><p>Typora 是大一的时候我接触 Markdown 第一次使用的产品，算是我接触到的最早的 Markdown 编辑器，一直到现在，我还是认为他是 Markdown 编辑的最佳产品。</p><p>可惜笔记不只有编辑，还有更多的刚需功能如分类、查找、链接，于是到现在 Typora 一般被我用来写博客、周报等一次性的文章。</p><h2 id="reference">reference</h2><section class="footnotes"><div class="footnote-list"><ol><li><span id="fn:1" class="footnote-text"><span><a href="https://sspai.com/post/55715">https://sspai.com/post/55715</a><a href="#fnref:1" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:2" class="footnote-text"><span><a href="https://www.xiaohongshu.com/discovery/item/630c9aef0000000011035a11?source=webshare&amp;xsec_token=ABpku-AiP_848_XFhEZ60oTQedOKawPWKHnV0Qaz7PwGg=&amp;xsec_source=pc_share">https://www.xiaohongshu.com/discovery/item/630c9aef0000000011035a11?source=webshare&amp;xsec_token=ABpku-AiP_848_XFhEZ60oTQedOKawPWKHnV0Qaz7PwGg=&amp;xsec_source=pc_share</a><a href="#fnref:2" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:3" class="footnote-text"><span><a href="https://sspai.com/post/31771">https://sspai.com/post/31771</a><a href="#fnref:3" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:4" class="footnote-text"><span><a href="https://killedbygoogle.com/">https://killedbygoogle.com/</a><a href="#fnref:4" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:5" class="footnote-text"><span><a href="https://getupnote.com/">https://getupnote.com/</a><a href="#fnref:5" rev="footnote" class="footnote-backref"> ↩</a></span></span></li></ol></div></section>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/obsidian/">obsidian</category>
      
      <category domain="https://moreality.net/tags/%E6%8A%80%E6%9C%AF/">技术</category>
      
      <category domain="https://moreality.net/tags/notion/">notion</category>
      
      <category domain="https://moreality.net/tags/upnote/">upnote</category>
      
      <category domain="https://moreality.net/tags/dayone/">dayone</category>
      
      
      <comments>https://moreality.net/posts/60356/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>一切皆可RSS：Follow 初体验</title>
      <link>https://moreality.net/posts/58383/</link>
      <guid>https://moreality.net/posts/58383/</guid>
      <pubDate>Sat, 07 Sep 2024 14:26:37 GMT</pubDate>
      
      <description>很久没有用过这么眼前一亮的产品了</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="前言">前言</h2><p><a href="https://github.com/RSSNext/Follow">Follow</a> 是 <a href="https://x.com/DIYgod">DIYgod</a> 大佬开发的一个 「Next generation information browser」(下一代信息浏览器)，值得一提的是，这位大佬也是 <a href="https://github.com/DIYgod/RSSHub">RSSHUB</a> 的作者。</p><p>这两个月刷 Twitter 的时候一直看到不断有人分享相关的产品体验，今天借着内测规模的扩大，总算拿到了邀请码可以好好体验这款产品。</p><p>不得不说确实对得起之前看到的对它的吹捧。</p><p><img src="https://youpai.roccoshi.top/img/202409072232203.png" alt="img"></p><p>一开始我以为 Follow 就不过是类似 Inoreader，freshrss，miniflux 这一类的 rss 阅读聚合器罢了，但是实际体验下来发现其相比上述 APP，更出色的地方在于对「一切皆可 RSS」和「社交」这两个属性的出色集成。</p><p>下面就记录一些我觉得 Follow 非常出彩的地方。</p><h2 id="1-简洁高效的搜索功能">1 - 简洁高效的搜索功能</h2><p><img src="https://youpai.roccoshi.top/img/202409072241657.png" alt="img"></p><p>直接在 Discover 页面即可通过搜索关键词来找到对应的 rss 订阅源，同时可以看到对应的 Follow 人数。</p><p>由于和 rsshub 的无缝集成，节约了很多 rss 转换和搜索的时间。</p><h2 id="2-多样的信息表达形式">2 - 多样的信息表达形式</h2><p><img src="https://youpai.roccoshi.top/img/202409072243244.png" alt="img"></p><p>Follow 支持了对文章、社交post、图片、视频、音频等多种不同主题的展现形式。</p><p>例如你可以通过视频的方式订阅 bilibili：</p><p><img src="https://youpai.roccoshi.top/img/202409072244030.png" alt="img"></p><p>通过图片的方式订阅沙雕墙 Telegram Channel：</p><p><img src="https://youpai.roccoshi.top/img/202409072245277.png" alt="img"></p><p>通过音频的方式订阅播客：</p><p><img src="https://youpai.roccoshi.top/img/202409072246136.png" alt="img"></p><p>而且无论是视频，音频，图片，文章，其内置的阅读器的 UI 都设计的相当之漂亮。</p><h2 id="3-社交体验">3 - 社交体验</h2><p>虽然现在内测用户群还不多，但其展现出的社交属性已经令人眼前一亮。</p><p><img src="https://youpai.roccoshi.top/img/202409072250703.png" alt="img"></p><p>你可以在 Follow 中认领 (claim) 你的内容，只需要修改 rss 加一行 description 即可。</p><p>然后其还有内置的虚拟货币，可以用来打赏（投币？）你喜欢的内容：</p><p><img src="https://youpai.roccoshi.top/img/202409072303974.png" alt="img"></p><h2 id="总结">总结</h2><p>除了上面几个亮点，感觉整个应用设计也特别在线，比如对快捷键的绑定，一些配置项等。</p><p>感觉是一个可玩性特别高的应用，虽然仅仅是内测 <code>v0.0.1-alpha</code> 版本就感觉已经完成度非常之高了，期待后续的进一步体验~</p><h2 id="Reference">Reference</h2><ul><li><a href="https://github.com/RSSNext/Follow">https://github.com/RSSNext/Follow</a></li><li><a href="https://x.com/DIYgod">https://x.com/DIYgod</a></li><li><a href="https://sspai.com/post/91283">https://sspai.com/post/91283</a></li></ul>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/%E6%8A%80%E6%9C%AF/">技术</category>
      
      <category domain="https://moreality.net/tags/Follow/">Follow</category>
      
      <category domain="https://moreality.net/tags/Rss-Hhub/">Rss Hhub</category>
      
      <category domain="https://moreality.net/tags/rss/">rss</category>
      
      
      <comments>https://moreality.net/posts/58383/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Raycast Pro 初见，一些我喜欢的功能</title>
      <link>https://moreality.net/posts/61943/</link>
      <guid>https://moreality.net/posts/61943/</guid>
      <pubDate>Thu, 22 Aug 2024 03:15:45 GMT</pubDate>
      
      <description>总体来说就是，AI 效果很棒，但价格略高。</description>
      
      
      
      <content:encoded><![CDATA[<p>很早以前就想买 Raycast Pro 了，但是网上一直搜不到很全的 Raycast AI 试用评价，光为了无限剪切板，cloud sync 这些无关痛痒的功能一个月消费 8 刀又感觉亏的不行，于是一直搁置。</p><p>直到前几天想开发一些自己的 AI 工具，看了一下 Raycast Extension 的 <a href="https://developers.raycast.com/api-reference/ai">AI API 文档</a>，感觉用起来相当简单，配合 Raycast 不错的生态，于是就大手一挥开了1个月的 PRO。</p><p>然后花了几个小时深度体验了一下，总体来说感觉非常不错，于是写一篇博客记录一下。</p><h2 id="Raycast-Pro-有什么">Raycast Pro 有什么</h2><p>以下是截至目前 (2024 年 8 月 22 日)，Raycast Pro 所有的全部功能。</p><p><img src="https://youpai.roccoshi.top/img/202408221132265.png" alt="img"></p><p>售价是 8 美元一个月（年付） / 10美元一个月（月付），学生优惠可以做到五折，申请学生优惠也比较简单，有 edu 邮箱就行，实测申请秒发。</p><p>同时需要注意的是，如果需要一些高级语言模型 (GPT4, Claude 3 opus)，需要额外再加 8 美元。不过现在自从 GPT 4o-mini 免费后，感觉这个模型应对一些日常需求也够用，我就不再舍得多花 8 美元了。目前 pro 本身支持的模型如下：</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202408221207597.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202408221207456.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202408221208997.png" alt="img"></div></div></div><p>感觉按 10 美元算，个人对这些功能评价如下：</p><ul><li><strong>Raycast AI</strong>：价值 8 美元，与插件生态结合的相当完美。</li><li><strong>Cloud Sync</strong>：价值 1 美元，对于同时使用多台 mac 设备的人来说，能同步配置还是不错的，尤其是自定义 hotkey 特别多的情况下，不过感觉对大多数人不是刚需，毕竟没有人有那么多闲工夫同时用两台 mac，如果是更换设备的话直接用免费的 settings export / import 就够用了。(并且 Clipboard History 和自己本地的脚本 Script Command 无法同步)。</li><li><strong>Custom Themes</strong>：没啥用的感觉，就换个颜色，我是觉得默认的黑色就很帅。</li><li><strong>Unlimited Clipboard History</strong>：价值 1 美元吧（主要是其他东西感觉更无价值），这个有点强行收费的意思，毕竟占用的还是本地的存储，还没法同步。</li><li><strong>Scheduled Exports</strong>：可以定时导出配置，配置的导入导出本身是免费的，只是单纯加了个定时。</li><li><strong>Translator</strong>：Bob 更好用。</li><li><strong>Window Management</strong>：基本 rectangle 可以平替，功能没啥区别，rectangle 还可以直接拖拽调整。</li></ul><p>所以总体来说，感觉 Raycast Pro 除了 AI 功能，其他感觉都是可有可无的，并没有对我来说必须付费的吸引力，但是换言之也说明了，Raycast 本体还是很良心的，免费版除了 AI 已经做到了基本完全体。</p><h2 id="Raycast-AI-的使用场景">Raycast AI 的使用场景</h2><h3 id="1-与-placeholder-的完美结合">1 - 与 placeholder 的完美结合</h3><p>raycast 有个好处就是其随处可以呼出的方便，配合一些 placeholder 可以实现很不错的 AI Command：</p><p><img src="https://youpai.roccoshi.top/img/202408221246650.png" alt="img"></p><p>例如这是一个官方自带的 「网站总结」 AI Command，最后一个「你的回答都应该使用中文」是我加的：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs shell">You are a skilled website summarizer. You write a summary of the provided webpage that includes all key details that you would share with a friend. Analyze the given website content and create a summary using the following format:<br>&quot;&quot;&quot;<br><span class="hljs-meta prompt_">#</span><span class="language-bash"><span class="hljs-comment"># &lt;concise title that captures the main theme of the website&gt;</span></span><br><br>&lt;one to two sentence summary that encapsulates the key information information&gt;<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_">#</span><span class="language-bash"><span class="hljs-comment">## Key Takeaways</span></span><br><br>- &lt;EXACTLY three bullet points with the key takeaways, keep the bullet points as short as possible&gt;<br>&quot;&quot;&quot;<br><br>Strictly follow these rules:<br>- ALWAYS maintain the author&#x27;s perspective and point of view throughout the summary<br>- Prioritize the most important, interesting, and unique facts for the reader<br>- Only include information explicitly stated in the website content<br>- Keep the summary concise, but ensure that all key information is included<br>- 你的回答都应该使用中文<br><br>Here&#x27;s the website content:<br>&#123;browser-tab&#125;<br><br>Summary:<br></code></pre></td></tr></table></figure><p>直接在浏览当前网页时打开 raycast，就可以总结当前网页的内容，而脚本中的 <code>&#123;browser-tab&#125;</code> 就是 raycast 自带的 placeholder，除了这个还有常用的：</p><ul><li><code>&#123;selection&#125;</code>: 当前选中的内容</li><li><code>&#123;clipboard&#125;</code>: 当前剪切板的内容</li></ul><p>具体更多的可以参考 <a href="https://manual.raycast.com/dynamic-placeholders">https://manual.raycast.com/dynamic-placeholders</a></p><p>将一个 AI Command 绑定到某个快捷键，就可以实现一键总结/生成/翻译/转换等诸多常用功能。</p><h3 id="2-自定义-Extension-Command-脚本">2 - 自定义 Extension / Command 脚本</h3><p>Raycast 本身的插件机制非常丰富，配置 <a href="https://developers.raycast.com/api-reference/ai">API 文档</a> 可以开发出许多贴合自己需求的好用的脚本。</p><p>这里以我自己昨天花 30min 写的一个 Extension 为例，该 Extension 的作用就是自动将剪切板内容总结并保存到 Telegram 我自己的收藏夹中：</p><figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs ts"><span class="hljs-keyword">import</span> &#123; <span class="hljs-variable constant_">AI</span>, showHUD, <span class="hljs-title class_">Clipboard</span> &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;@raycast/api&quot;</span>;<br><span class="hljs-keyword">import</span> <span class="hljs-title class_">TelegramBot</span> <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;node-telegram-bot-api&#x27;</span>;<br><br><span class="hljs-keyword">const</span> bot = <span class="hljs-keyword">new</span> <span class="hljs-title class_">TelegramBot</span>(token, &#123; <span class="hljs-attr">polling</span>: <span class="hljs-literal">false</span> &#125;);<br><span class="hljs-keyword">const</span> prompt = <span class="hljs-string">&quot;你的prompt&quot;</span><br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">command</span>(<span class="hljs-params"></span>) &#123;<br>  <span class="hljs-keyword">try</span> &#123;<br>    <span class="hljs-keyword">const</span> &#123; text, file, html &#125; = <span class="hljs-keyword">await</span> <span class="hljs-title class_">Clipboard</span>.<span class="hljs-title function_">read</span>();<br>    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`get from clipboard: text = <span class="hljs-subst">$&#123;text&#125;</span> \n file = <span class="hljs-subst">$&#123;file&#125;</span> \n html = <span class="hljs-subst">$&#123;html&#125;</span>`</span>);<br>    <span class="hljs-keyword">await</span> <span class="hljs-title function_">showHUD</span>(<span class="hljs-string">&quot;🖊️ Processing..&quot;</span>);<br>    <span class="hljs-keyword">const</span> answer = <span class="hljs-keyword">await</span> <span class="hljs-variable constant_">AI</span>.<span class="hljs-title function_">ask</span>(prompt + text, &#123;<br>      <span class="hljs-comment">// model: AI.Model[&quot;OpenAI_GPT4o-mini&quot;],</span><br>      <span class="hljs-attr">model</span>: <span class="hljs-variable constant_">AI</span>.<span class="hljs-property">Model</span>[<span class="hljs-string">&quot;OpenAI_GPT4o-mini&quot;</span>],<br>    &#125;);<br>    <span class="hljs-keyword">await</span> bot.<span class="hljs-title function_">sendMessage</span>(chatId, answer, &#123;<br>      <span class="hljs-attr">parse_mode</span>: <span class="hljs-string">&quot;Markdown&quot;</span><br>    &#125;);<br>    <span class="hljs-keyword">await</span> <span class="hljs-title function_">showHUD</span>(<span class="hljs-string">&quot;✅ Done&quot;</span>);<br>  &#125; <span class="hljs-keyword">catch</span> (error) &#123;<br>    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(error);<br>    <span class="hljs-keyword">await</span> <span class="hljs-title function_">showHUD</span>(<span class="hljs-string">&quot;Failed.&quot;</span>);<br>  &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>非常的方便。</p><h2 id="Reference">Reference</h2><ul><li><a href="https://developers.raycast.com/api-reference/ai">https://developers.raycast.com/api-reference/ai</a></li><li><a href="https://github.com/raycast/script-commands">https://github.com/raycast/script-commands</a></li><li><a href="https://manual.raycast.com/dynamic-placeholders">https://manual.raycast.com/dynamic-placeholders</a></li></ul>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/%E6%8A%80%E6%9C%AF/">技术</category>
      
      <category domain="https://moreality.net/tags/Raycast/">Raycast</category>
      
      <category domain="https://moreality.net/tags/AI/">AI</category>
      
      <category domain="https://moreality.net/tags/Tool/">Tool</category>
      
      <category domain="https://moreality.net/tags/%E5%B7%A5%E5%85%B7/">工具</category>
      
      
      <comments>https://moreality.net/posts/61943/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>我是如何用 VPS 自建 Emby 的</title>
      <link>https://moreality.net/posts/41540/</link>
      <guid>https://moreality.net/posts/41540/</guid>
      <pubDate>Sat, 17 Aug 2024 05:59:30 GMT</pubDate>
      
      <description>整体的记录一下 emby 搭建起来的配置, 包括技术, 成本以及链路加速</description>
      
      
      
      <content:encoded><![CDATA[<blockquote><p>最近放假回家，准备秋招之余有一些闲功夫，又正好在帮家里更新一些软硬件设备，就把自己目前搭建 Emby 的技术栈做一些简单的记录和配置罗列。</p></blockquote><h2 id="1-Emby-的-Docker-配置">1 - Emby 的 Docker 配置</h2><p>我的所有自建基本能放在 <code>docker-compose</code> 中的就尽量用 docker compose 管理，emby 也不例外。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">&quot;3&quot;</span><br><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">emby:</span><br>    <span class="hljs-attr">image:</span> <span class="hljs-string">emby/embyserver:latest</span><br>    <span class="hljs-attr">container_name:</span> <span class="hljs-string">embyserver</span><br>    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span><br>    <span class="hljs-attr">user:</span> <span class="hljs-string">root</span><br>    <span class="hljs-attr">volumes:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./config:/config</span> <span class="hljs-comment"># Configuration directory</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">/theater/movie:/mnt/share1</span> <span class="hljs-comment"># Media directory</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">/theater/tv:/mnt/share2</span> <span class="hljs-comment"># Media directory</span><br>    <span class="hljs-attr">ports:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-number">8096</span><span class="hljs-string">:8096</span> <span class="hljs-comment"># HTTP port</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-number">8920</span><span class="hljs-string">:8920</span> <span class="hljs-comment"># HTTPS port</span><br></code></pre></td></tr></table></figure><p>其中需要备份的目录就是 <code>./config</code> 目录，而 <code>/theater/movie</code> 和 <code>/theater/tv</code> 用于挂载自己的影视盘，这里我选用的是 rclone 挂载 Google Drive / One Drive，具体配置和方案会在下面说明。</p><p>光是 Emby 本身的搭建不是很吃 CPU 和内存，不过建议至少 2C2G 的 VPS 起步吧，我自己搭建的这个占用情况（运行了半年没重启和停机后的状态）：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">docker stats --no-stream | grep emby</span><br>384f61138e22   embyserver                      0.03%     1.412GiB / 9.716GiB   14.53%    4.31GB / 498GB    2.74GB / 5.23GB   82<br></code></pre></td></tr></table></figure><p>也就是大概需要 1.4G 左右的 MEM，仅供参考。</p><h3 id="关于-Emby-服务器启动后的配置">关于 Emby 服务器启动后的配置</h3><p>这里不对具体的每一项配置做多的说明，只说个比较重要的：<strong>由于 VPS 没有 GPU 并且 CPU 一般很拉胯, 记得把服务端解码全关了。</strong></p><p><img src="https://youpai.roccoshi.top/img/202408171421488.png" alt="由于 VPS 配置有限，需要关闭服务端转码"></p><h2 id="2-Rclone-挂载-Google-Drive">2 - Rclone 挂载 Google Drive</h2><p>要说国外挂载 Emby 最合适的网盘，目前比较合适的方案就是用 Google Drive 或者 One Drive，其他网盘的读写实测都受到了不同程度的限制，并且稳定性肯定也不如大厂（当然，也有可能还有很多更好更实惠的方案我没有探索出来）。</p><ul><li>One Drive 可以上闲鱼购买合租的 Office365 套餐，大概 20 左右 1 年，有 1T 的空间，感觉也是挺划算的。</li><li>Google Drive 可以直接开土区的套餐，2T 大概在 100 RMB 左右（不过目前好像土区受到限制了，不知道未来是否会受影响）。</li></ul><h3 id="Google-Drive-的-Plan">Google Drive 的 Plan</h3><p>目前我用的 Plan 是土耳其 2TB 的套餐，目前资费是 500 土耳其里拉，换算成人民币也就是大概 100 块左右每年的样子。</p><p><img src="https://youpai.roccoshi.top/img/202408171409685.png" alt="img"></p><h3 id="Rclone-的配置">Rclone 的配置</h3><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><code class="hljs yml"><span class="hljs-attr">version:</span> <span class="hljs-string">&quot;3&quot;</span><br><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">rclone:</span><br>    <span class="hljs-attr">image:</span> <span class="hljs-string">mumiehub/rclone-mount</span><br>    <span class="hljs-attr">container_name:</span> <span class="hljs-string">rclone</span><br>    <span class="hljs-attr">user:</span> <span class="hljs-string">root</span><br>    <span class="hljs-attr">network_mode:</span> <span class="hljs-string">host</span><br>    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span><br>    <span class="hljs-attr">cap_add:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">SYS_ADMIN</span><br>    <span class="hljs-attr">devices:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">/dev/fuse</span><br>    <span class="hljs-attr">security_opt:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">apparmor:unconfined</span><br>    <span class="hljs-attr">volumes:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">/root/.config/rclone:/root/.config/rclone</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">/theater:/mnt/mediaefs:shared</span><br>    <span class="hljs-attr">environment:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">RemotePath=gdtry:/</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">ConfigDir=/root/.config/rclone</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">ConfigName=rclone.conf</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">MountCommands=--use-mmap</span> <span class="hljs-string">--umask</span> <span class="hljs-number">000</span> <span class="hljs-string">--default-permissions</span> <span class="hljs-string">--no-check-certificate</span> <span class="hljs-string">--allow-other</span> <span class="hljs-string">--allow-non-empty</span> <span class="hljs-string">--vfs-cache-mode</span> <span class="hljs-string">full</span> <span class="hljs-string">--buffer-size</span> <span class="hljs-string">256M</span> <span class="hljs-string">--vfs-read-ahead</span> <span class="hljs-string">512M</span> <span class="hljs-string">--vfs-read-chunk-size</span> <span class="hljs-string">32M</span> <span class="hljs-string">--vfs-read-chunk-size-limit</span> <span class="hljs-string">off</span> <span class="hljs-string">--vfs-cache-max-size</span> <span class="hljs-string">30G</span> <span class="hljs-string">--low-level-retries</span> <span class="hljs-number">200</span><br></code></pre></td></tr></table></figure><p>rclone 的配置我也是用的 docker-compose，主要是为了方便日志管理和自动重启。</p><p>然后直接将宿主机的 rclone config 也就是 <code>~/.config/rclone</code> 挂载到容器内部，这样就不需要容器内部再重新登录了。</p><p>这里的 <code>Environment</code> 部分添加了一些乱七八糟的 <code>MountCommands</code> 参数，也是网上搜索了一波后配的，自我感觉比较重要的是 <code>--vfs-cache-mode full --vfs-cache-max-size 30G</code> ，可以根据 VPS 的硬盘大小尽量扩大这个值，文件缓存到本地了自然就快了~。</p><h2 id="3-反代服务器的配置">3 - 反代服务器的配置</h2><p>我的 emby 服务器本身是找了个大流量高性能的 US VPS (5C10G)，然后实际上一般这种性能的美西服务器都不会配备一个好的线路，这时候就需要进行一波反代。</p><p>反代的线路如何选择就各种各样了，我本身是为了方便家里观看，实际上如果是自己看的话完全可以用代理连接，就不用再套一层反代了。</p><p>反代的配置我也写的很简单，就是单纯用 caddy 代理一层 https，实测也有不错的效果。</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs ini">https://&lt;反代域名&gt; &#123;<br>  tls &#123;$EMAIL&#125;<br>  encode gzip zstd<br>  reverse_proxy &lt;emby-ip&gt;:8096<br>&#125;<br></code></pre></td></tr></table></figure><h2 id="4-PT-下载和上传">4 - PT 下载和上传</h2><p>这部分就是涉及到如何从网上找资源，以及上传到 Google Drive。</p><p>这一部分内容只是我自己的极为简单的配置，仅仅为了满足我自己追剧的需要，不涉及刷 PT 这一系列的骚操作。</p><h3 id="qbittorrent-配置">qbittorrent 配置</h3><p>简单说一下我的 qbittorrent 的配置：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">&quot;3&quot;</span><br><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">qbittorrent:</span><br>    <span class="hljs-attr">image:</span> <span class="hljs-string">lscr.io/linuxserver/qbittorrent:latest</span><br>    <span class="hljs-attr">container_name:</span> <span class="hljs-string">qbittorrent</span><br>    <span class="hljs-attr">network_mode:</span> <span class="hljs-string">host</span><br>    <span class="hljs-attr">environment:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">PUID=0</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">PGID=0</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">TZ=Etc/UTC</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">WEBUI_PORT=8080</span><br>    <span class="hljs-attr">volumes:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./config:/config</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./downloads:/downloads</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">/resources:/resources</span><br>    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span><br></code></pre></td></tr></table></figure><h3 id="上传到-Google-Drive-的-Shell-Script">上传到 Google Drive 的 Shell Script</h3><p>我用写了个简单的 script 用于在 qbit 下载结束后快速将资源上传到 Google Drive 对应的目录，这里也记录一下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs shell">uptv () &#123;<br>local source_path=&quot;$1&quot;<br>local destination=&quot;&lt;google drive 目录&gt;&quot;<br>local filename=$(basename &quot;$source_path&quot;)<br>echo &quot;upload tv: $filename&quot;<br>if [ -d &quot;$source_path&quot; ]<br>then<br>destination_path=&quot;$destination/$filename&quot;<br>else<br>destination_path=&quot;$destination/&quot;<br>fi<br>echo &quot;source: $source_path --&gt; destination: $destination_path&quot;<br>rclone copy &quot;$source_path&quot; &quot;$destination_path&quot; -Pv<br>&#125;<br></code></pre></td></tr></table></figure><h3 id="QB-自动删除脚本">QB 自动删除脚本</h3><p>一般在上传到 Google Drive 后，不能马上删除种子和文件。</p><p>需要在满足 PT 站的保种规则一段时间后，就可以删除种子了，我使用的是 autoremove-torrents  来做到自动清除，由于这个工具本身对于 qbittorrent 4.5.4+ 具有一些兼容性问题，我 fork 了一个并且把我的配置也记录在了其中：<a href="https://github.com/Lincest/autoremove-torrents/tree/master">https://github.com/Lincest/autoremove-torrents/tree/master</a></p><p><strong>安装</strong>：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">cd &amp;&amp; git clone https://github.com/lincest/autoremove-torrents.git &amp;&amp; cd autoremove-torrents &amp;&amp; python3 setup.py install<br></code></pre></td></tr></table></figure><p><strong>配置文件</strong>:</p><p>PS: 这个配置文件的主要是用来适配北洋园的规则，可以根据自己的需求自助修改。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">my_task:</span><br>  <span class="hljs-attr">client:</span> <span class="hljs-string">qbittorrent</span><br>  <span class="hljs-attr">host:</span> <span class="hljs-string">http://127.0.0.1:8080</span><br>  <span class="hljs-attr">username:</span> <br>  <span class="hljs-attr">password:</span> <br>  <span class="hljs-attr">strategies:</span><br>    <span class="hljs-attr">my_strategy:</span><br>      <span class="hljs-attr">remove:</span> <span class="hljs-string">&#x27; </span><br><span class="hljs-string">        (size &lt; 10 and seeding_time &gt; 86400) or </span><br><span class="hljs-string">        (size &gt; 10 and size &lt; 20 and seeding_time &gt; 172800) or </span><br><span class="hljs-string">        (size &gt; 20 and size &lt; 30 and seeding_time &gt; 259200) or </span><br><span class="hljs-string">        (size &gt; 30 and size &lt; 40 and seeding_time &gt; 345600) or </span><br><span class="hljs-string">        (size &gt; 40 and size &lt; 50 and seeding_time &gt; 432000) or </span><br><span class="hljs-string">        seeding_time &gt; 604800 or </span><br><span class="hljs-string">        ratio &gt; 10 &#x27;</span><br>  <span class="hljs-attr">delete_data:</span> <span class="hljs-literal">true</span><br></code></pre></td></tr></table></figure><p><strong>cronjob 配置</strong>:</p><figure class="highlight jboss-cli"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs jboss-cli">*<span class="hljs-string">/15</span> * * * * <span class="hljs-string">/usr/local/bin/autoremove-torrents</span> <span class="hljs-params">--conf=</span>&lt;配置目录&gt;<span class="hljs-string">/autoremove-config.yml</span> <span class="hljs-params">--log=/root/logs</span><br></code></pre></td></tr></table></figure><h2 id="5-Emby-的客户端">5 - Emby 的客户端</h2><p>整体可以参考 Misakaf 的 <a href="https://wiki.misakaf.org/#/%E5%BC%80%E5%A7%8B%E4%BD%BF%E7%94%A8">Wiki</a> 来下载各个平台的安卓客户端，个人体验下来比较好用的：</p><ul><li>MacOS / IOS: infuse 和 vidhub</li><li>Windows: 小秘的客户端, <a href="https://t.me/EmbyNoisyX">TG Channel</a></li><li>Android: 小秘的客户端和 <a href="https://t.me/yamby_release">Yamby</a></li></ul>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/%E6%8A%80%E6%9C%AF/">技术</category>
      
      <category domain="https://moreality.net/tags/%E5%BD%B1%E9%9F%B3/">影音</category>
      
      <category domain="https://moreality.net/tags/emby/">emby</category>
      
      <category domain="https://moreality.net/tags/Google-Drive/">Google Drive</category>
      
      <category domain="https://moreality.net/tags/One-Drive/">One Drive</category>
      
      <category domain="https://moreality.net/tags/VPS/">VPS</category>
      
      
      <comments>https://moreality.net/posts/41540/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>第一台 mini主机: 零刻 EQ13</title>
      <link>https://moreality.net/posts/11296/</link>
      <guid>https://moreality.net/posts/11296/</guid>
      <pubDate>Thu, 08 Aug 2024 15:28:07 GMT</pubDate>
      
      <description>打算给爸妈配一台日常主机, 于是记录一下购买的第一台 mini 主机以及配置过程, 实际远超我的预期</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="写在前面">写在前面</h2><p>这个学期老爸用了 5 年还是 6 年的笔记本终于不堪重负了，于是这个暑假想办法给家里配一台新电脑。</p><p>在 v2ex 提了一个问题：</p><ul><li><a href="https://www.v2ex.com/t/1059438">https://www.v2ex.com/t/1059438</a></li></ul><p><img src="https://youpai.roccoshi.top/img/202408082334992.png" alt="img"></p><p>之所以不再买笔记本而是选择 NUC / 迷你主机，是考虑到本身笔记本的性价比较低，以及我想利用一下之前笔记本的可重用配件比如内存条和SSD，而且手上有闲置出来的蓝牙鼠标和键盘，家里又有之前放假回去买的显示器；刚好都给利用上，还能节省一笔不小的开销，体验我个人感觉也会比笔记本好上很多。</p><p>由于家里父母的需求基本就是：看剧 + 少许 office + 浏览网页，思来想去看到零刻新出的 EQ13，感觉还是挺不错的，功耗很小、足够静音，以及自带电源，而且刚好 DDR4 和 M2 的 SSD 和手上的配对，准系统也只要不到 1000 RMB，于是直接果断下单。</p><h2 id="到手体验">到手体验</h2><p>到手后第一感觉就是这货体积比我预想的还要小，就半个巴掌大：</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202408082340326.jpg" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202408082339565.jpg" alt="img"></div></div></div><p>拆开来看内部做的也挺精致的，而且还送一个磁吸的螺丝刀，基本两三分钟就把 SSD 和内存装上了：</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202408082343656.jpg" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202408082342836.jpg" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202408082346332.png" alt="img"></div></div></div><h3 id="接口">接口</h3><p>这台机器主要包含的接口就如下所示：</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202408082351488.jpg" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202408082351948.jpg" alt="img"></div></div></div><p>其中值得一提的是，虽然是双网口，不过每个都是 1 Gbps 而不是 2.5 Gbps的。</p><p>不过日常用倒是绰绰有余了。</p><h2 id="装机过程">装机过程</h2><p>整个装机过程非常省心，因为他自带了一个系统安装盘（win11专业版），并且会帮你把所有驱动自动安装好，基本就是开箱即用的状态。</p><p><img src="https://youpai.roccoshi.top/img/202408082347440.jpg" alt="img"></p><h3 id="关于-win11-激活">关于 win11 激活</h3><p>本来打算直接在闲鱼上 10块钱 买一个激活码的，结果简单搜索一番后发现 github 直接有开源的激活脚本，而且使用起来相当方便：</p><ul><li><a href="https://github.com/massgravel/Microsoft-Activation-Scripts">https://github.com/massgravel/Microsoft-Activation-Scripts</a></li></ul><p>直接 <code>powershell</code> 运行：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">irm https://get.activated.win | iex<br></code></pre></td></tr></table></figure><p>即可完成 win11 的永久激活。</p><h2 id="软件安装">软件安装</h2><p>由于自己的需求是给老爸看电影，所以除了office，爱奇艺，微信这些，就额外仅仅安装了如下几个软件：</p><h3 id="emby">emby</h3><p>emby 参考 <a href="https://wiki.misakaf.org/#/%E5%BC%80%E5%A7%8B%E4%BD%BF%E7%94%A8/1?id=windows%e8%ae%be%e5%a4%87%e4%bd%bf%e7%94%a8">misakaf</a> 的 doc 安装了 emby 的 windows 客户端：</p><p><img src="https://youpai.roccoshi.top/img/202408082354556.png" alt="img"></p><p>之前给家里用的也是 emby，因为他们经常会看一些美剧，简单提一句目前我 emby 的搭建方案就是：</p><ul><li><code>GIA 反代 -&gt; 美国 emby server -&gt; 2TB Google Drive</code></li></ul><p>虽然方案比较痤，但是几个人看个 4K 还是不在话下的。</p><h3 id="rustdesk">rustdesk</h3><ul><li>官网：<a href="https://rustdesk.com/">https://rustdesk.com/</a></li></ul><p>据说 mini 主机问题可能会比较多，总而言之装个 rustdesk 以防万一，服务器自建在了阿里云香港，实测用起来很流畅~</p><h3 id="ZeroTier">ZeroTier</h3><ul><li>官网：<a href="https://www.zerotier.com/download/">https://www.zerotier.com/download/</a></li></ul><p>顺手把 zerotier 的组网也弄好了，作用当然也是远程调试，算是 rustdesk 的 fallback 方案吧。</p><h2 id="总结">总结</h2><p>晚上自己下了个 steam 玩了几个小时的 <a href="https://store.steampowered.com/app/527230/__For_The_King/">FTK</a>，完全没有听到任何风扇声音，感觉日常这种影音和冲浪需求完全可以胜任了，感觉自己最后选了 N200 而不是 AMD 的 U 也是非常正确的一个选择。</p><p>整体来说成本不超过 1k 可以获得这种体验，个人还是相当满意的（前提是后续不会出什么幺蛾子问题）。</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/mini%E4%B8%BB%E6%9C%BA/">mini主机</category>
      
      <category domain="https://moreality.net/tags/%E9%9B%B6%E5%88%BB/">零刻</category>
      
      <category domain="https://moreality.net/tags/N200/">N200</category>
      
      <category domain="https://moreality.net/tags/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <comments>https://moreality.net/posts/11296/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Obsidian Vault 清理流程总结</title>
      <link>https://moreality.net/posts/50741/</link>
      <guid>https://moreality.net/posts/50741/</guid>
      <pubDate>Sat, 13 Jul 2024 07:47:44 GMT</pubDate>
      
      <description>obsidian 已经用了快 2 年了, 现在笔记越来越臃肿, 今天花了几小时的时间好好的整理一下, 并记录一下整理的流程</description>
      
      
      
      <content:encoded><![CDATA[<blockquote><p>obsidian 已经用了快 2 年了, 现在笔记越来越臃肿, 今天花了几小时的时间好好的整理一下, 并记录一下整理的流程</p></blockquote><h2 id="1-将图片等附件全部移动到根目录下">1 - 将图片等附件全部移动到根目录下</h2><p>最开始的时候考虑到笔记和其他 markdown 软件的兼容性，用的是 <strong>相对路径 + 每个文件夹建一个 <code>/assets</code> 来存放附件</strong> 的方案，但是现在看来，这一年我用其他编辑器打开 obsidian vault 进行编辑/查看的场景几乎为 0，反而每个 assets 在 tree 的显示下显得相当碍事 (<code>a</code> 开头还排在第一个)，因此直接全部更换为 <strong>简短路径 + 根目录创建一个附件文件夹的方式</strong>。</p><p>通过这样的方式，将附件全权交给 obsidian 管理。</p><p><img src="https://youpai.roccoshi.top/img/202407131551038.png" alt="img"></p><h2 id="2-清理未被引用的图片">2 - 清理未被引用的图片</h2><p><img src="https://youpai.roccoshi.top/img/202407131632647.png" alt="img"></p><p>安装这个插件: <code>Clear Unused Images</code> 即可</p><h2 id="3-清理不再使用的-Folder">3 - 清理不再使用的 Folder</h2><p>我这里采用的方式是:</p><ol><li>创建一个新的 Vault 专门存放需要归档的 folder</li><li>将当前 <code>assets</code> 全部移动到新 vault</li><li>用 <code>Clear Unused Images</code> 插件清理无用的附件</li></ol>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/note/">note</category>
      
      <category domain="https://moreality.net/tags/obsidian/">obsidian</category>
      
      <category domain="https://moreality.net/tags/clean/">clean</category>
      
      <category domain="https://moreality.net/tags/%E7%AC%94%E8%AE%B0%E6%95%B4%E7%90%86/">笔记整理</category>
      
      
      <comments>https://moreality.net/posts/50741/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Web-Note: 一个 Self Host 的 Online Note APP</title>
      <link>https://moreality.net/posts/25857/</link>
      <guid>https://moreality.net/posts/25857/</guid>
      <pubDate>Fri, 12 Jul 2024 14:31:03 GMT</pubDate>
      
      <description>web-note 是我基于 minimalist-web-notepad 二开的一个应用, 一个极简的在线纯文本 / markdown编辑器</description>
      
      
      
      <content:encoded><![CDATA[<blockquote><p>今天晚上回来闲着没事干，顺带给自己半年前开发来自己用的应用加了一个 history 的 feature，顺便在 blog 里记录一下。</p></blockquote><p>这个项目的由来大概是我之前一直在寻找一个好用方便的、能在多平台分享文本的工具，而各个 pastebin 又没法支持一些简单的 markdown 编辑，并且感觉分享起来还是太麻烦了，于是当时看到 <a href="https://note.ms">note.ms</a> 这个应用感觉眼前一亮，就去搜了搜它的开源实现：<a href="https://github.com/pereorga/minimalist-web-notepad">minimalist-web-notepad</a>。</p><p>当时 fork 下来添加了几个 feature，之后自己记录一些简单的备忘，需要多平台快速传播的文本，以及一些 markdown 编辑和代码分享等场景就直接用这个工具完成了，感觉个人还是挺满意的。</p><ul><li>Github: <a href="https://github.com/Lincest/web-note">https://github.com/Lincest/web-note</a></li><li>体验地址: <a href="https://mmmm.tech">https://mmmm.tech</a></li></ul><h2 id="整体外观">整体外观</h2><p><img src="https://youpai.roccoshi.top/img/202407122335810.png" alt="img"></p><p><img src="https://youpai.roccoshi.top/img/202407122337460.png" alt="img"></p><h2 id="Feature">Feature</h2><h3 id="基于-github-workflow-的自动镜像构建">基于 github workflow 的自动镜像构建</h3><p>做了一个 github workflow，可以直接打包成一个镜像然后发布在 <code>ghcr</code>，直接通过 <code>docker-compose.yml</code> 进行部署即可：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">&quot;3&quot;</span><br><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">web-note:</span><br>    <span class="hljs-attr">image:</span> <span class="hljs-string">ghcr.io/lincest/web-note:latest</span><br>    <span class="hljs-attr">container_name:</span> <span class="hljs-string">web-note</span><br>    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span><br>    <span class="hljs-attr">ports:</span><br>     <span class="hljs-bullet">-</span> <span class="hljs-string">&quot;80:80&quot;</span><br>    <span class="hljs-attr">volumes:</span><br>     <span class="hljs-bullet">-</span> <span class="hljs-string">./data:/var/www/html/_tmp</span><br></code></pre></td></tr></table></figure><p>然后文档都会以一个个纯文本的形式保存在 <code>./data</code> 目录下, 比如 <code>https://mmmm.tech/123</code> 那么就会生成一个 <code>./data/123</code> 的文件</p><h3 id="简单的-markdown-render">简单的 markdown render</h3><p>一些效果展示:</p><p><img src="https://youpai.roccoshi.top/img/202407122349613.png" alt="img"></p><p><img src="https://youpai.roccoshi.top/img/202407122349336.png" alt="img"></p><p><img src="https://youpai.roccoshi.top/img/202407122353251.png" alt="img"></p><h3 id="键盘流快捷键">键盘流快捷键</h3><p>基于个人编辑习惯弄了一些快捷键，比如：复制 Link，打开历史记录，保存（自动保存的间隔大概在1s左右，设置这个是为了缓解自己和一些人的 <code>cmd/ctrl + s</code> 综合征），markdown &lt;-&gt; 纯文本转换等。</p><p>同时也在页面的下方加了一些带有 emoji 点击的 button，目的是方便在微信分享这种自带 webview 没有 url 框场景下的使用：</p><p><img src="https://youpai.roccoshi.top/img/202407130001534.jpeg" alt="img"></p><blockquote><p><code>mod</code>：在 macOS 上是 <code>command</code>，在 Windows/Linux 上是 <code>ctrl</code></p></blockquote><table><thead><tr><th>功能</th><th>快捷键</th><th>Click</th></tr></thead><tbody><tr><td>在 Markdown 和纯文本渲染模式之间切换</td><td><code>mod + e</code></td><td>note/xx 🔓</td></tr><tr><td>复制当前 URL 并显示二维码</td><td><code>mod + l</code></td><td>🔗 share</td></tr><tr><td>保存当前笔记 （当然，如果你不按它，它会每秒自动保存）</td><td><code>mod + s</code></td><td>-</td></tr><tr><td>切换历史记录</td><td><code>mod + k</code></td><td>📜 history</td></tr><tr><td>生成带有随机 URL 的新笔记 （等同于重新访问 <a href="https://mmmm.tech">https://mmmm.tech</a> )</td><td>-</td><td>💡 new</td></tr></tbody></table><ul><li><a href="https://github.com/Lincest/web-note/wiki">https://github.com/Lincest/web-note/wiki</a></li></ul><h3 id="CLI">CLI</h3><p>可以利用 <code>curl &lt;url&gt;</code> 直接读取纯文本, 或者用 <code>curl --data &quot;xx&quot; &lt;url&gt;</code>上传文本, 例子:</p><p><img src="https://youpai.roccoshi.top/img/202407130006417.png" alt="img"></p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/note/">note</category>
      
      <category domain="https://moreality.net/tags/web-note/">web-note</category>
      
      <category domain="https://moreality.net/tags/%E7%AC%94%E8%AE%B0/">笔记</category>
      
      <category domain="https://moreality.net/tags/self-host/">self-host</category>
      
      <category domain="https://moreality.net/tags/%E8%87%AA%E9%83%A8%E7%BD%B2/">自部署</category>
      
      
      <comments>https://moreality.net/posts/25857/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>一个相对完整的局域网代理方案</title>
      <link>https://moreality.net/posts/29923/</link>
      <guid>https://moreality.net/posts/29923/</guid>
      <pubDate>Sat, 06 Jul 2024 12:00:00 GMT</pubDate>
      
      <description>记录一下一个让无法联网的校园网服务器正常运转的方式, 主要解决的问题: 局域网内有一台服务器无法上网, 但是存在可以上网的另一台服务器的情况</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="前言">前言</h2><p><strong>背景</strong>: 实验室有一台 8 卡的服务器在学校管理的机房里面，机器无法连校园网，但是可以和校园网内其他服务器互联。</p><p>本文简单记录一下如何让这台服务器通过 Proxy 连接到另一台实验室的内网服务器，并进行上网的方式。</p><p>本文不对原理做过多阐述，仅仅是贴几个配置。</p><h2 id="服务端-可以联网">服务端 (可以联网)</h2><h3 id="搭建-Proxy-Server">搭建 Proxy Server</h3><p>可以通过各种手段搭建 Proxy Server，我这里直接用 docker 简单弄了个 clash，并用 yacd 当面板，<code>config</code>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">&#x27;3&#x27;</span><br><br><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">clash:</span><br>    <span class="hljs-attr">image:</span> <span class="hljs-string">dreamacro/clash</span><br>    <span class="hljs-attr">container_name:</span> <span class="hljs-string">clash</span><br>    <span class="hljs-attr">volumes:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./config.yaml:/root/.config/clash/config.yaml</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./ui:/ui</span> <span class="hljs-comment"># dashboard volume</span><br>    <span class="hljs-attr">ports:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">&quot;7890:7890&quot;</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">&quot;9090:9090&quot;</span><br>    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span><br>    <span class="hljs-attr">network_mode:</span> <span class="hljs-string">bridge</span><br>  <span class="hljs-attr">yacd:</span><br>    <span class="hljs-attr">image:</span> <span class="hljs-string">haishanh/yacd</span><br>    <span class="hljs-attr">container_name:</span> <span class="hljs-string">yacd</span><br>    <span class="hljs-attr">ports:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">&quot;18080:80&quot;</span><br>    <span class="hljs-attr">restart:</span> <span class="hljs-string">unless-stopped</span><br>    <span class="hljs-attr">network_mode:</span> <span class="hljs-string">bridge</span><br></code></pre></td></tr></table></figure><p>至于 <code>config.yaml</code> 怎么配，那自然是不能在这里展开了。</p><h2 id="客户端-无法联网">客户端 (无法联网)</h2><h3 id="1-shell-proxy">1 - shell proxy</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs shell">export http_proxy=&quot;http://&lt;proxy-ip&gt;:7890&quot;<br>export https_proxy=$http_proxy<br>export no_proxy=&quot;localhost,127.0.0.0/8,10.0.0.0/8&quot; <br>export HTTP_PROXY=$http_proxy<br>export HTTPS_PROXY=$https_proxy<br>export NO_PROXY=$no_proxy<br></code></pre></td></tr></table></figure><p>将这个放在 <code>~/.bashrc</code> / <code>~/.zshrc</code> 等启动配置里即可，这个属于基操</p><h3 id="2-sudo-proxy">2 - sudo proxy</h3><p>上面的 shell proxy 作用域只有当前登录用户的 shell, 如果用 <code>sudo apt install</code> 等操作就没法走代理了，因此需要：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">sudo visudo <br></code></pre></td></tr></table></figure><p>然后加上下面一行配置，表示将当前 shell 的这些环境变量传递到 sudo 中：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">Defaults env_keep += &quot;http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY&quot; <br></code></pre></td></tr></table></figure><h3 id="3-systemd-proxy-docker">3 - systemd proxy (docker)</h3><p>上面的配置无法让 <code>docker pull</code> 走代理，原因在 <a href="https://moreality.net/posts/45709/">这篇博客: docker pull 卡住(代理问题) 的解决方案</a> 也详细说过。</p><p>需要做的工作：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">sudo mkdir -p /etc/systemd/system/docker.service.d <br>sudo vim /etc/systemd/system/docker.service.d/proxy.conf <br></code></pre></td></tr></table></figure><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs shell">[Service] <br>Environment=&quot;HTTP_PROXY=http://&lt;proxy-ip&gt;:7890/&quot; <br>Environment=&quot;HTTPS_PROXY=http://&lt;proxy-ip&gt;:7890/&quot; <br>Environment=&quot;NO_PROXY=localhost,127.0.0.1&quot;<br></code></pre></td></tr></table></figure><p>最后重启docker：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">sudo systemctl daemon-reload<br>sudo systemctl restart docker<br></code></pre></td></tr></table></figure><h3 id="4-ssh-proxyjump">4 - ssh proxyjump</h3><p>上面的代理解决不了 ssh 的问题，因此对于 openssh 7.3 以下，可以：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">vim ~/.ssh/config<br></code></pre></td></tr></table></figure><p>然后配置类似的 ProxyCommand 方案:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs shell">Host proxyserver<br>  HostName xxx<br>  Port 22<br>  User xxx<br><br>Host target<br>  HostName xxx<br>  Port 22<br>  User xxx<br>  ProxyCommand ssh -W %h:%p proxyserver<br></code></pre></td></tr></table></figure><p>如果是 openssh 7.3 以上, 直接:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs shell">Host proxyserver<br>  HostName xxx<br>  Port 22<br>  User xxx<br><br>Host target<br>  HostName xxx<br>  Port 22<br>  User xxx<br>  ProxyJump proxyserver<br></code></pre></td></tr></table></figure><p>即可</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/docker/">docker</category>
      
      <category domain="https://moreality.net/tags/proxy/">proxy</category>
      
      <category domain="https://moreality.net/tags/%E4%BB%A3%E7%90%86/">代理</category>
      
      <category domain="https://moreality.net/tags/systemd/">systemd</category>
      
      <category domain="https://moreality.net/tags/vpn/">vpn</category>
      
      
      <comments>https://moreality.net/posts/29923/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>OpenTelemetry Instrumentation 与 Java Agent</title>
      <link>https://moreality.net/posts/23154/</link>
      <guid>https://moreality.net/posts/23154/</guid>
      <pubDate>Sat, 15 Jun 2024 14:31:03 GMT</pubDate>
      
      <description>主要介绍 OpenTelemetry 的 Instrumentation 过程，以及简单介绍 Auto Instrumentation 涉及到的 Java Agent 技术</description>
      
      
      
      <content:encoded><![CDATA[<p>在之前的 <a href="https://moreality.net/posts/25969/">blog: 分布式可观测性，链路追踪与OpenTelemetry</a> 主要介绍了分布式链路追踪的概念和 OpenTelemetry (下文以 Otel 简称) 的起源。从本节开始，我会分享一些 OpenTelemetry 的基本概念，语言主要基于 Java（当然，Otel 本身的 SDK 支持多种语言，可以在 <a href="https://opentelemetry.io/docs/languages/">Otel Doc: Language APIs &amp; SDKs</a> 查看)。</p><p>本文介绍的内容主要涉及 OpenTelemetry<sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://opentelemetry.io/docs">[1]</span></a></sup> 的 Instrumentation 概念，以及如何将已有的代码接入 OpenTelemetry 以获得可观测性。</p><h2 id="1-OpenTelemetry-Instrumentation-Startup">1 - OpenTelemetry Instrumentation Startup</h2><p>如果你想把你的一个项目接入 OpenTelemetry，肯定要接触一个概念：<strong>”Instrumentation”</strong>，这是一个少有的我感觉没什么准确的一个中文词汇能表达出的意思，OpenTelemetry 将其翻译成「仪表化」，但我感觉仍然不太恰当。</p><p>这个词实际上表达的是：</p><p><strong>向应用程序中注入跟踪和监控代码的过程，目的是收集有关应用程序运行时性能和行为的监控数据</strong> <sup id="fnref:2" class="footnote-ref"><a href="#fn:2" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://stackify.com/what-are-java-agents-and-how-to-profile-with-them/">[2]</span></a></sup></p><p>OpenTelemetry 对 Instrumentation 主要提供了两种方案：</p><ul><li>一种是手动配置的方案，也叫 <strong>Manual Instrumentation</strong>，该方案需要侵入式的手动更改代码，同样的提供的可配置项也更多。</li><li>一种是自动配置的方案，也叫 <strong>Auto Instrumentation / Zero Code Instrumentation</strong> (<a href="https://opentelemetry.io/docs/zero-code/">doc</a>)，该方案可以无须侵入性的修改代码，而是自动进行代码插桩和注入，实现方式根据语言而异，例如：<ul><li>在<code> Java</code> 上利用 <code>Java Agent</code> 字节码修改技术 (<a href="https://github.com/open-telemetry/opentelemetry-java-instrumentation">https://github.com/open-telemetry/opentelemetry-java-instrumentation</a>)</li><li>在 <code>Go</code> 中利用 <code>eBPF 技术</code> (<a href="https://github.com/open-telemetry/opentelemetry-go-instrumentation">https://github.com/open-telemetry/opentelemetry-go-instrumentation</a>)</li><li>在 <code>.Net</code> 中利用 <code>monkey-patching</code> 技术 (<a href="https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation">https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation</a>)</li><li>更多的 <code>Auto Instrumentation</code> 技术可以参考: <a href="https://github.com/orgs/open-telemetry/repositories?q=-instrumentation">https://github.com/orgs/open-telemetry/repositories?q=-instrumentation</a></li></ul></li></ul><h2 id="2-Java-Agent">2 - Java Agent</h2><p>下文都以 Java 为例，介绍 Otel 如何利用 Java Agent 机制实现 Auto Instrumentation。该部分主要参考官网文档：<a href="https://opentelemetry.io/docs/zero-code/java/agent/">Doc: Java Agent</a></p><p>本章首先介绍 Java Agent 机制。</p><h3 id="2-1-什么是-Java-Agent">2.1 - 什么是 Java Agent</h3><p><strong>Java代理是一种特殊类型的类，通过使用Java Instrumentation API ，它可以拦截运行在JVM上的应用程序，修改它们的字节码。</strong> (注意：区别于 Otel Instrumentation，虽然用的是一个词，表达的也都是「代码注入」这个概念）</p><p>Java代理并不是一项新技术，相反，它们从Java 5开始就存在了。但是即使过了这么长时间，还是有许多开发者对这个概念鲜有接触。</p><p>通过 Java Agent，可以轻松实现如下几类应用场景：</p><ul><li>IDE 的调试功能，例如 Eclipse、IntelliJ IDEA；</li><li>热部署功能，例如 JRebel、XRebel、spring-loaded；</li><li>各种线上诊断工具，例如 Btrace、Greys，国内阿里的 Arthas；</li><li>各种性能分析工具，例如 Visual VM、JConsole 等；</li><li>全链路性能检测工具，例如 <strong>OpenTelemetry</strong>、Skywalking、Pinpoint 等。</li></ul><h3 id="2-2-Java-Agent-的运行机制">2.2 - Java Agent 的运行机制</h3><p>Java Agent 主要可以通过两种方式启动：<strong>Premain Agent</strong> 和 <strong>Agentmain</strong>，分别在 JVM 启动前和启动后加载 Agent，达到的都是动态修改字节码的效果：</p><ul><li><strong>Premain Agent</strong></li></ul><p>Java Agent 在 Java 程序运行前：在<code>Main</code>方法执行之前，通过一个叫 <code>premain</code>方法来执行。</p><p>启动时需要在目标程序的启动参数中添加 <code>-javaagent</code>参数，Java Agent 内部通过注册 ClassFileTransformer ，这个转化器在 Java 程序 <code>Main</code>方法前加了一层拦截器。在类加载之前，完成对字节码修改。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">java -javaagent:/path/to/&lt;your-agent&gt;.jar YourMain<br></code></pre></td></tr></table></figure><p>其工作流程大致如下：<sup id="fnref:4" class="footnote-ref"><a href="#fn:4" rel="footnote"><span class="hint--top hint--rounded" aria-label="[深入 OpenTelemetry 源代码：Java 探针的实现和二次开发](https://www.infoq.cn/article/slirwa72fhzwvzc2hmkf)">[4]</span></a></sup></p><p><img src="https://youpai.roccoshi.top/img/202406150120266.png" alt="img"></p><ul><li><strong>Agentmain</strong></li></ul><p>该模式和 Premain 模式相似，主要区别在进行字节码增强前，拦截入口不同。一个叫<code>Premain</code>，一个叫<code>Agentmain</code> 。 运行时加载，当前 JVM 进程已经启动了。这时借助另一个 JVM 进程通信，调用 Attach API 再把 Agent 启动起来。后面的字节码修改和重加载的过程那就是一样的。</p><h2 id="3-Otel-Java-Auto-Instrumentation">3 - Otel Java Auto Instrumentation</h2><p>Otel Java Auto Instrumentation 主要通过 Java agent 机制实现。</p><p>Java Auto Instrumentation 实现都在 github: <a href="https://github.com/open-telemetry/opentelemetry-java-instrumentation/">https://github.com/open-telemetry/opentelemetry-java-instrumentation/</a> 这个仓库中。</p><h3 id="3-1-快速开始">3.1 - 快速开始</h3><p>可以参考 <a href="https://opentelemetry.io/docs/zero-code/java/agent/#setup">doc</a> 快速在一个 java 项目中进行 instrumentation，只需要如下几步：</p><ol><li>Download <a href="https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar">opentelemetry-javaagent.jar</a> from <a href="https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases">Releases</a> of the <code>opentelemetry-java-instrumentation</code></li><li>通过 <code>java -javaagent:path/to/opentelemetry-javaagent.jar -Dotel.service.name=your-service-name -jar myapp.jar</code> 启动你的 project app <code>myapp.jar</code></li></ol><p>通过这两步，就已经可以在这些 <a href="https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md">otel 支持的 library</a> 上运行 opentelemetry 了，可以通过在环境变量或者 properties 中修改 <a href="https://opentelemetry.io/docs/zero-code/java/agent/configuration/">这些配置</a> 来进行更多自定义配置，包括但不限于：</p><ul><li>数据导出配置：导出数据的目标类型,如 Jaeger、Zipkin、Prometheus等。</li><li>日志输出模式</li><li>采样策略</li><li>等等</li></ul><p>下面通过一个我自己的示例来快速了解一下如何利用 Otel 提供的 <code>@WithSpan</code> 和 <code>@SpanAttribute</code> Annotation 进行 Auto Instrumentation 的 Tracing。<sup id="fnref:6" class="footnote-ref"><a href="#fn:6" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://opentelemetry.io/docs/zero-code/java/agent/annotations/">[6]</span></a></sup></p><h3 id="3-2-Code-示例">3.2 - Code 示例</h3><p>首先创建一个 java 项目，并导入 <code>opentelemetry-instrumentation-annotations</code> 这个依赖，可以在 <a href="https://central.sonatype.com/artifact/io.opentelemetry.instrumentation/opentelemetry-instrumentation-annotations">这里</a> 查看 Maven 或者 Gradle 的导入方式。</p><p>这里以 gradle 为例，添加如下 dependencies 即可：</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs kotlin">implementation(<span class="hljs-string">&quot;io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.4.0&quot;</span>)<br></code></pre></td></tr></table></figure><p>全部的 <code>build.gradle.kts</code>:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><code class="hljs kotlin">plugins &#123;<br>    java<br>    id(<span class="hljs-string">&quot;com.github.johnrengelman.shadow&quot;</span>) version <span class="hljs-string">&quot;7.1.2&quot;</span><br>&#125;<br><br>group = <span class="hljs-string">&quot;org.example&quot;</span><br>version = <span class="hljs-string">&quot;1.0&quot;</span><br><br>repositories &#123;<br>    mavenCentral()<br>&#125;<br><br>dependencies &#123;<br>    implementation(<span class="hljs-string">&quot;io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations:2.4.0&quot;</span>)<br>&#125;<br><br>tasks.withType&lt;JavaCompile&gt; &#123;<br>    options.encoding = <span class="hljs-string">&quot;UTF-8&quot;</span><br>&#125;<br><br>tasks.shadowJar &#123;<br>    mergeServiceFiles()<br>    manifest &#123;<br>        attributes(<br>            <span class="hljs-string">&quot;Main-Class&quot;</span> to <span class="hljs-string">&quot;org.example.Main&quot;</span><br>        )<br>    &#125;<br>&#125;<br><br>tasks.build &#123;<br>    dependsOn(tasks.shadowJar)<br>&#125;<br></code></pre></td></tr></table></figure><p>然后写一个简单的测试类：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">package</span> org.example;<br><br><span class="hljs-keyword">import</span> io.opentelemetry.instrumentation.annotations.SpanAttribute;<br><span class="hljs-keyword">import</span> io.opentelemetry.instrumentation.annotations.WithSpan;<br><br><span class="hljs-keyword">import</span> java.util.logging.Logger;<br><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Main</span> &#123;<br>    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Logger</span> <span class="hljs-variable">logger</span> <span class="hljs-operator">=</span> Logger.getLogger(Main.class.getName());<br><br>    <span class="hljs-meta">@WithSpan</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> &#123;<br>        sampleMethodA(<span class="hljs-string">&quot;test&quot;</span>);<br>        sampleMethodB();<br>    &#125;<br><br>    <span class="hljs-meta">@WithSpan</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">sampleMethodA</span><span class="hljs-params">(<span class="hljs-meta">@SpanAttribute</span> String arg)</span> &#123;<br>        System.out.println(<span class="hljs-string">&quot;Sample method A, arg = &quot;</span> + arg);<br>        sampleMethodB();<br>    &#125;<br><br>    <span class="hljs-meta">@WithSpan</span><br>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">sampleMethodB</span><span class="hljs-params">()</span> &#123;<br>        System.out.println(<span class="hljs-string">&quot;Sample method B&quot;</span>);<br>    &#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>使用 <code>-Dotel.traces.exporter=logging-otlp</code> 将输出指定为 otlp 的 json 格式（otlp 是 opentelemetry 定义的传输协议），通过 log 输出在控制台，然后把 logs 和 metrics 的 exporter 禁用后启动：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">java -javaagent:opentelemetry-javaagent.jar -Dotel.traces.exporter=logging-otlp -Dotel.logs.exporter=none -Dotel.metrics.exporter=none -jar ./build/libs/otel-simple-demo-1.0-all.jar<br></code></pre></td></tr></table></figure><p>也可以通过环境变量来启动：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs shell">JAVA_TOOL_OPTIONS=&#x27;-javaagent:./opentelemetry-javaagent.jar&#x27; \<br>OTEL_TRACES_EXPORTER=&#x27;logging-otlp&#x27; \<br>OTEL_METRICS_EXPORTER=none \<br>OTEL_LOGS_EXPORTER=none \<br>java -jar ./build/libs/otel-simple-demo-1.0-all.jar<br></code></pre></td></tr></table></figure><p>输出如下:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs shell">Picked up JAVA_TOOL_OPTIONS: -javaagent:./opentelemetry-javaagent.jar<br>[otel.javaagent 2024-06-17 01:16:00:167 +0800] [main] INFO io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 2.4.0<br>Sample method A, arg = test<br>Sample method B<br>Sample method B<br>[otel.javaagent 2024-06-17 01:16:06:098 +0800] [BatchSpanProcessor_WorkerThread-1] INFO io.opentelemetry.exporter.logging.otlp.OtlpJsonLoggingSpanExporter - &#123;&quot;resource&quot;:&#123;&quot;attributes&quot;:[&#123;&quot;key&quot;:&quot;host.arch&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;aarch64&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;host.name&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;sxz-mbp.local&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;os.description&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;Mac OS X 14.4.1&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;os.type&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;darwin&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;process.command_line&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/bin/java -javaagent:./opentelemetry-javaagent.jar -jar ./build/libs/otel-simple-demo-1.0-all.jar&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;process.executable.path&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;/Library/Java/JavaVirtualMachines/zulu-8.jdk/Contents/Home/jre/bin/java&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;process.pid&quot;,&quot;value&quot;:&#123;&quot;intValue&quot;:&quot;65608&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;process.runtime.description&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;Azul Systems, Inc. OpenJDK 64-Bit Server VM 25.372-b07&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;process.runtime.name&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;OpenJDK Runtime Environment&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;process.runtime.version&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;1.8.0_372-b07&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;service.instance.id&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;99dd9ca6-3d76-4aef-86ac-017e16881fbc&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;service.name&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;otel-simple-demo-1.0-all&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;telemetry.distro.name&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;opentelemetry-java-instrumentation&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;telemetry.distro.version&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;2.4.0&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;telemetry.sdk.language&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;java&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;telemetry.sdk.name&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;opentelemetry&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;telemetry.sdk.version&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;1.38.0&quot;&#125;&#125;]&#125;,&quot;scopeSpans&quot;:[&#123;&quot;scope&quot;:&#123;&quot;name&quot;:&quot;io.opentelemetry.opentelemetry-instrumentation-annotations-1.16&quot;,&quot;version&quot;:&quot;2.4.0-alpha&quot;,&quot;attributes&quot;:[]&#125;,&quot;spans&quot;:[&#123;&quot;traceId&quot;:&quot;641daaa0e701906be9ca743e4526f471&quot;,&quot;spanId&quot;:&quot;9aed7d245875bdea&quot;,&quot;parentSpanId&quot;:&quot;7159ba878669026b&quot;,&quot;name&quot;:&quot;Main.sampleMethodB&quot;,&quot;kind&quot;:1,&quot;startTimeUnixNano&quot;:&quot;1718558166060339167&quot;,&quot;endTimeUnixNano&quot;:&quot;1718558166062393375&quot;,&quot;attributes&quot;:[&#123;&quot;key&quot;:&quot;code.namespace&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;org.example.Main&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;thread.id&quot;,&quot;value&quot;:&#123;&quot;intValue&quot;:&quot;1&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;code.function&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;sampleMethodB&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;thread.name&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;main&quot;&#125;&#125;],&quot;events&quot;:[],&quot;links&quot;:[],&quot;status&quot;:&#123;&#125;,&quot;flags&quot;:257&#125;,&#123;&quot;traceId&quot;:&quot;641daaa0e701906be9ca743e4526f471&quot;,&quot;spanId&quot;:&quot;7159ba878669026b&quot;,&quot;parentSpanId&quot;:&quot;5316ba20f2a4144d&quot;,&quot;name&quot;:&quot;Main.sampleMethodA&quot;,&quot;kind&quot;:1,&quot;startTimeUnixNano&quot;:&quot;1718558166060174750&quot;,&quot;endTimeUnixNano&quot;:&quot;1718558166062439667&quot;,&quot;attributes&quot;:[&#123;&quot;key&quot;:&quot;code.namespace&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;org.example.Main&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;thread.id&quot;,&quot;value&quot;:&#123;&quot;intValue&quot;:&quot;1&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;code.function&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;sampleMethodA&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;thread.name&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;main&quot;&#125;&#125;],&quot;events&quot;:[],&quot;links&quot;:[],&quot;status&quot;:&#123;&#125;,&quot;flags&quot;:257&#125;,&#123;&quot;traceId&quot;:&quot;641daaa0e701906be9ca743e4526f471&quot;,&quot;spanId&quot;:&quot;4eee1ecf0422c09e&quot;,&quot;parentSpanId&quot;:&quot;5316ba20f2a4144d&quot;,&quot;name&quot;:&quot;Main.sampleMethodB&quot;,&quot;kind&quot;:1,&quot;startTimeUnixNano&quot;:&quot;1718558166062502208&quot;,&quot;endTimeUnixNano&quot;:&quot;1718558166062544292&quot;,&quot;attributes&quot;:[&#123;&quot;key&quot;:&quot;code.namespace&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;org.example.Main&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;thread.id&quot;,&quot;value&quot;:&#123;&quot;intValue&quot;:&quot;1&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;code.function&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;sampleMethodB&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;thread.name&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;main&quot;&#125;&#125;],&quot;events&quot;:[],&quot;links&quot;:[],&quot;status&quot;:&#123;&#125;,&quot;flags&quot;:257&#125;,&#123;&quot;traceId&quot;:&quot;641daaa0e701906be9ca743e4526f471&quot;,&quot;spanId&quot;:&quot;5316ba20f2a4144d&quot;,&quot;name&quot;:&quot;Main.main&quot;,&quot;kind&quot;:1,&quot;startTimeUnixNano&quot;:&quot;1718558166058000000&quot;,&quot;endTimeUnixNano&quot;:&quot;1718558166062553750&quot;,&quot;attributes&quot;:[&#123;&quot;key&quot;:&quot;code.namespace&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;org.example.Main&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;thread.id&quot;,&quot;value&quot;:&#123;&quot;intValue&quot;:&quot;1&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;code.function&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;main&quot;&#125;&#125;,&#123;&quot;key&quot;:&quot;thread.name&quot;,&quot;value&quot;:&#123;&quot;stringValue&quot;:&quot;main&quot;&#125;&#125;],&quot;events&quot;:[],&quot;links&quot;:[],&quot;status&quot;:&#123;&#125;,&quot;flags&quot;:257&#125;]&#125;],&quot;schemaUrl&quot;:&quot;https://opentelemetry.io/schemas/1.24.0&quot;&#125;<br></code></pre></td></tr></table></figure><h3 id="3-3-Span-的结构分析">3.3 - Span 的结构分析</h3><p>输出的 json 实际上的结构可以在 <code>trace.proto</code> 中查看: <a href="https://github.com/open-telemetry/opentelemetry-proto/blob/v1.3.0/opentelemetry/proto/trace/v1/trace.proto">https://github.com/open-telemetry/opentelemetry-proto/blob/v1.3.0/opentelemetry/proto/trace/v1/trace.proto</a></p><ul><li>比如 <code>main</code> 方法对应的 span 对应包含如下信息</li></ul><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;traceId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;641daaa0e701906be9ca743e4526f471&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;spanId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;5316ba20f2a4144d&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Main.main&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;kind&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;startTimeUnixNano&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1718558166058000000&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;endTimeUnixNano&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1718558166062553750&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;attributes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;code.namespace&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;org.example.Main&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;thread.id&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;intValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;code.function&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;main&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;thread.name&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;main&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><br>    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;events&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;links&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;flags&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">257</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><ul><li>对于非 root span，还会额外包含一个 <code>parentSpanId</code> 的信息, 以 <code>sampleMethodB</code> 为例:</li></ul><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;traceId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;641daaa0e701906be9ca743e4526f471&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;spanId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;4eee1ecf0422c09e&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;parentSpanId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;5316ba20f2a4144d&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Main.sampleMethodB&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;kind&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;startTimeUnixNano&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1718558166062502208&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;endTimeUnixNano&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1718558166062544292&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;attributes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;code.namespace&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;org.example.Main&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;thread.id&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;intValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;code.function&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;sampleMethodB&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;thread.name&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;main&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><br>    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;events&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;links&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;flags&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">257</span><br><span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br></code></pre></td></tr></table></figure><p>关于其中字段的详细解释可以参考：<a href="https://opentelemetry.io/docs/concepts/signals/traces/">https://opentelemetry.io/docs/concepts/signals/traces/</a></p><p>除了在上一篇文章提到的 Tracing 通用的 spanId, traceId 这些通用的 Span Attributes 外, OpenTelemetry 还提供了两种新的称为 Span Events 和 Span Links 的东西:</p><ul><li><code>Span Events</code>: 用于标记一个 Span 内部的一个关键事件, 例如:</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java">span.addEvent(<span class="hljs-string">&quot;Init&quot;</span>);<br>...<br>span.addEvent(<span class="hljs-string">&quot;End&quot;</span>);<br></code></pre></td></tr></table></figure><p>也可以在 <code>events</code> 中添加 Attributes:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-type">Attributes</span> <span class="hljs-variable">eventAttributes</span> <span class="hljs-operator">=</span> Attributes.of(<br>    AttributeKey.stringKey(<span class="hljs-string">&quot;key&quot;</span>), <span class="hljs-string">&quot;value&quot;</span>,<br>    AttributeKey.longKey(<span class="hljs-string">&quot;result&quot;</span>), <span class="hljs-number">0L</span>);<br><br>span.addEvent(<span class="hljs-string">&quot;End Computation&quot;</span>, eventAttributes);<br></code></pre></td></tr></table></figure><ul><li><code>Span Links</code>: 用于标记一个 Span 与其他 Span 之间的关联关系，例如:</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-type">Span</span> <span class="hljs-variable">child</span> <span class="hljs-operator">=</span> tracer.spanBuilder(<span class="hljs-string">&quot;childWithLink&quot;</span>)<br>        .addLink(parentSpan1.getSpanContext())<br>        .addLink(parentSpan2.getSpanContext())<br>        .addLink(parentSpan3.getSpanContext())<br>        .addLink(remoteSpanContext)<br>    .startSpan();<br></code></pre></td></tr></table></figure><p>这些 API 可以简单的在 Agent 的基础上使用，例如我们修改上面的示例代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@WithSpan</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> &#123;<br>    <span class="hljs-type">Span</span> <span class="hljs-variable">currentSpan</span> <span class="hljs-operator">=</span> Span.current();<br>    currentSpan.setAttribute(<span class="hljs-string">&quot;custom-attribute&quot;</span>, <span class="hljs-string">&quot;custom-attribute-value&quot;</span>);<br>    currentSpan.addEvent(<span class="hljs-string">&quot;custom-event&quot;</span>);<br>    currentSpan.addLink(currentSpan.getSpanContext());<br>    sampleMethodA(<span class="hljs-string">&quot;test&quot;</span>);<br>    sampleMethodB();<br>&#125;<br></code></pre></td></tr></table></figure><p>然后重新运行，就可以得到如下的 payload：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;traceId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;f23dd7607ebec83961036bd1f5aadbfb&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;spanId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;8f0e5a157098c520&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Main.main&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;kind&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;startTimeUnixNano&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1718608407274000000&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;endTimeUnixNano&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1718608407319309792&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;attributes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;code.namespace&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;org.example.Main&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;thread.id&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;intValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;code.function&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;main&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;custom-attribute&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;custom-attribute-value&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;thread.name&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>                <span class="hljs-attr">&quot;stringValue&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;main&quot;</span><br>            <span class="hljs-punctuation">&#125;</span><br>        <span class="hljs-punctuation">&#125;</span><br>    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;events&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;timeUnixNano&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1718608407309075542&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;custom-event&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;attributes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br><br>            <span class="hljs-punctuation">]</span><br>        <span class="hljs-punctuation">&#125;</span><br>    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;links&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>        <span class="hljs-punctuation">&#123;</span><br>            <span class="hljs-attr">&quot;traceId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;f23dd7607ebec83961036bd1f5aadbfb&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;spanId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;8f0e5a157098c520&quot;</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;attributes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br><br>            <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>            <span class="hljs-attr">&quot;flags&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">257</span><br>        <span class="hljs-punctuation">&#125;</span><br>    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br><br>    <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;flags&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">257</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>可以看到里面已经包含了刚才添加的自定义 attributes, events 和 links 信息。</p><h2 id="Conclusion">Conclusion</h2><p>这一章还处于比较浅的阶段，后面会再写写 otel 一些内部 code 结构的分析，以及如何通过  agent extensions 等机制来自定义 agent 行为等。</p><h2 id="Reference">Reference</h2><section class="footnotes"><div class="footnote-list"><ol><li><span id="fn:1" class="footnote-text"><span><a href="https://opentelemetry.io/docs">https://opentelemetry.io/docs</a><a href="#fnref:1" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:2" class="footnote-text"><span><a href="https://stackify.com/what-are-java-agents-and-how-to-profile-with-them/">https://stackify.com/what-are-java-agents-and-how-to-profile-with-them/</a><a href="#fnref:2" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:3" class="footnote-text"><span><a href="https://en.wikipedia.org/wiki/Instrumentation_(computer_programming)">https://en.wikipedia.org/wiki/Instrumentation_(computer_programming)</a><a href="#fnref:3" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:4" class="footnote-text"><span><a href="https://www.infoq.cn/article/slirwa72fhzwvzc2hmkf">深入 OpenTelemetry 源代码：Java 探针的实现和二次开发</a><a href="#fnref:4" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:5" class="footnote-text"><span><a href="https://www.infoq.cn/article/JYWWVhcGCWL7vGK1RIap">Java 自动化探针技术的核心原理和实践</a><a href="#fnref:5" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:6" class="footnote-text"><span><a href="https://opentelemetry.io/docs/zero-code/java/agent/annotations/">https://opentelemetry.io/docs/zero-code/java/agent/annotations/</a><a href="#fnref:6" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:7" class="footnote-text"><span><a href="https://github.com/open-telemetry/opentelemetry-proto/blob/v1.3.0/opentelemetry/proto/trace/v1/trace.proto">https://github.com/open-telemetry/opentelemetry-proto/blob/v1.3.0/opentelemetry/proto/trace/v1/trace.proto</a><a href="#fnref:7" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:8" class="footnote-text"><span><a href="https://opentelemetry.io/docs/concepts/signals/traces/">https://opentelemetry.io/docs/concepts/signals/traces/</a><a href="#fnref:8" rev="footnote" class="footnote-backref"> ↩</a></span></span></li></ol></div></section>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7/">可观测性</category>
      
      <category domain="https://moreality.net/tags/Observability/">Observability</category>
      
      <category domain="https://moreality.net/tags/Trace/">Trace</category>
      
      <category domain="https://moreality.net/tags/OpenTelemetry/">OpenTelemetry</category>
      
      <category domain="https://moreality.net/tags/Java/">Java</category>
      
      <category domain="https://moreality.net/tags/Java-Agent/">Java Agent</category>
      
      
      <comments>https://moreality.net/posts/23154/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>分布式可观测性，链路追踪与OpenTelemetry</title>
      <link>https://moreality.net/posts/25969/</link>
      <guid>https://moreality.net/posts/25969/</guid>
      <pubDate>Fri, 14 Jun 2024 14:31:03 GMT</pubDate>
      
      <description>漫谈一些分布式可观测性(Observability)的概念，以及其中的链路追踪部分和 OpenTelemetry 的应用等</description>
      
      
      
      <content:encoded><![CDATA[<p><strong>一些写在前面的碎碎念</strong>：</p><blockquote><p>在 Amazon 已经实习了三个多月，很幸运没有被分配到做业务的活，想到实习的内容还是有一定价值的，值得我将其记录并进行一定的输出。</p><p>本来想着等实习结束后再进行总结，但是最近发生的一些事情让我意识到许多事情不要过分计划，有了想法就尽快 Demo 出来，哪怕只是一些零散的东西。</p><p>因此有了这篇博客，在总结输出的过程顺便让我理一理思绪。看心情可能会分为很多章，由于我做的内容仅仅只涉及到链路追踪（Tracing）部分，因此不对其他（Logging、Metrics）做过分详细的介绍。</p><p>由于我也是从 0 开始的小白，不保证内容的深刻程度。</p></blockquote><ul><li>在概念部分，我尽量引用别人的文字和图，辅以部分个人理解，以确保内容的可靠性。</li><li>在技术部分，我会更多的阐述一些 OpenTelemetry 的 SDK 实现和用途，以及个人对「为什么这么做」的一些浅薄的想法。</li></ul><h2 id="1-什么是可观测性-Observability">1 - 什么是可观测性 (Observability)</h2><p><img src="https://youpai.roccoshi.top/img/202406142239030.png" alt="安利周志明的「凤凰架构」这本书"></p><p>写著名「深入理解Java虚拟机」的周志明老师写过一本个人认为同样极其出色的书籍「凤凰架构」<sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://icyfenix.cn/distribution/observability/">[1]</span></a></sup>，在 <a href="https://icyfenix.cn/distribution/observability/">可观测性</a> 这一章有对这个概念非常棒的总结和解释，这里直接引用一下：</p><blockquote><p>在学术界，虽然“可观测性”这个名词是近几年才从控制理论中借用的舶来概念，不过其内容实际在计算机科学中已有多年的实践积累。学术界一般会将可观测性分解为三个更具体方向进行研究，分别是：<a href="https://icyfenix.cn/distribution/observability/logging.html">事件日志</a>、<a href="https://icyfenix.cn/distribution/observability/tracing.html">链路追踪</a>和<a href="https://icyfenix.cn/distribution/observability/metrics.html">聚合度量</a></p><ul><li><p><strong>日志</strong>（Logging）：日志的职责是记录离散事件，通过这些记录事后分析出程序的行为，譬如曾经调用过什么方法，曾经操作过哪些数据，等等。打印日志被认为是程序中最简单的工作之一，调试问题时常有人会说“当初这里记得打点日志就好了”，可见这就是一项举手之劳的任务。输出日志的确很容易，但收集和分析日志却可能会很复杂，面对成千上万的集群节点，面对迅速滚动的事件信息，面对数以 TB 计算的文本，传输与归集都并不简单。对大多数程序员来说，分析日志也许就是最常遇见也最有实践可行性的“大数据系统”了。</p></li><li><p><strong>追踪</strong>（Tracing）：单体系统时代追踪的范畴基本只局限于<a href="https://en.wikipedia.org/wiki/Stack_trace">栈追踪</a>（Stack Tracing），调试程序时，在 IDE 打个断点，看到的 Call Stack 视图上的内容便是追踪；编写代码时，处理异常调用了 <code>Exception::printStackTrace()</code>方法，它输出的堆栈信息也是追踪。微服务时代，追踪就不只局限于调用栈了，一个外部请求需要内部若干服务的联动响应，这时候完整的调用轨迹将跨越多个服务，同时包括服务间的网络传输信息与各个服务内部的调用堆栈信息，因此，分布式系统中的追踪在国内常被称为“全链路追踪”（后文就直接称“链路追踪”了），许多资料中也称它为“<a href="https://opentracing.io/docs/overview/what-is-tracing/">分布式追踪</a>”（Distributed Tracing）。追踪的主要目的是排查故障，如分析调用链的哪一部分、哪个方法出现错误或阻塞，输入输出是否符合预期，等等。</p></li><li><p><strong>度量</strong>（Metrics）：度量是指对系统中某一类信息的统计聚合。譬如，证券市场的每一只股票都会定期公布财务报表，通过财报上的营收、净利、毛利、资产、负债等等一系列数据来体现过去一个财务周期中公司的经营状况，这便是一种信息聚合。Java 天生自带有一种基本的度量，就是由虚拟机直接提供的 JMX（Java Management eXtensions）度量，诸如内存大小、各分代的用量、峰值的线程数、垃圾收集的吞吐量、频率，等等都可以从 JMX 中获得。度量的主要目的是监控（Monitoring）和预警（Alert），如某些度量指标达到风险阈值时触发事件，以便自动处理或者提醒管理员介入。</p></li></ul><p><img src="https://youpai.roccoshi.top/img/202406142325405.png" alt="img"></p></blockquote><h2 id="2-链路追踪-Distributed-Tracing-是如何进行的">2 - 链路追踪 (Distributed Tracing) 是如何进行的</h2><p>在单体服务的时代，我们仅仅只需要跟踪代码的调用栈就可以获得清晰的调用图关系，以及出现异常时的 Stack Trace。</p><p>但在微服务时代，各类服务间可能形态各异（不同语言，不同框架），并通过不同的方式（RPC，HTTP，.etc）进行相互调用。于是如何追踪一次业务级别的服务调用相比单体服务的复杂程度就完全不是一个量级了。</p><p>因此我们不仅需要一个统一的「数据表示」来正确的表达这种追踪关系，而且需要一套大而全的「体系规范」来进行程序级别的实现。</p><p>Google 在 2010 年发布的这篇论文 <strong>Dapper</strong>：《<a href="https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/papers/dapper-2010-1.pdf">Dapper : a Large-Scale Distributed Systems Tracing Infrastructure</a>》<sup id="fnref:2" class="footnote-ref"><a href="#fn:2" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/papers/dapper-2010-1.pdf">[2]</span></a></sup> 就奠定了之后十余年分布式链路追踪的数据表示的概念，其实抽象出来十分的简单，就是两个基本的概念：</p><ol><li><strong>Trace</strong>：一次业务级别的请求调用 (Client &lt;-&gt; Service A &lt;-&gt; Service B &lt;-&gt; … )</li><li><strong>Span</strong>：<ul><li>可以是粗粒度的一个 Service 级别的 call</li><li>也可以是细粒度的 Service 内部的函数调用 (Function A -&gt; Function B -&gt; …)</li></ul></li></ol><p>如何具体的规范化表示和收集这些信息，会在后面详细谈谈。</p><p>这里先看看一下在 UI 端，如何展示这些信息：</p><p><img src="https://youpai.roccoshi.top/img/202406142306198.png" alt="Jaeger UI 展示的 Tracing"></p><p>这里是以 <a href="https://www.jaegertracing.io/">Jaeger</a> 为例，展示的一个 <a href="https://en.wikipedia.org/wiki/Trace_tree">Trace Tree</a>，其表示了一个请求/服务调用：</p><ul><li>花费了多长的时间</li><li>与哪些内部组件/外部服务进行了交互</li><li>每个步骤中延迟是多少</li></ul><p>可以稍微抽象化一些：</p><p><img src="https://youpai.roccoshi.top/img/202406150910359.png" alt="img"></p><p>Talk is cheap，用伪代码可以这么表示：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-keyword">function</span> <span class="hljs-title function_">A</span>(<span class="hljs-params"></span>) &#123;<br>  <span class="hljs-title function_">B</span>();<br>  <span class="hljs-title function_">E</span>();<br>&#125;<br><span class="hljs-keyword">function</span> <span class="hljs-title function_">B</span>(<span class="hljs-params"></span>) &#123;<br>  <span class="hljs-title function_">C</span>();<br>  <span class="hljs-title function_">D</span>();<br>&#125;<br></code></pre></td></tr></table></figure><p>这就是「分布式链路追踪」的基本概念，听起来很简单，因为系统的复杂性也确实不在这里。</p><h2 id="3-OpenTelemetry-下的链路追踪">3 - OpenTelemetry 下的链路追踪</h2><p>就在前几周，在 v2ex 上看到一个有意思的问题：</p><p><img src="https://youpai.roccoshi.top/img/202406150913473.png" alt="img"></p><blockquote><p><a href="https://v2ex.com/t/1043751"><strong>“有人能解释一下 OpenTelemetry 这类遥测方案解决了什么问题，和自己写两个 API 把数据存进 MongoDB 有什么区别吗?&quot;</strong></a></p></blockquote><p>在结果上，确实没有区别，如果是针对一个小组织的的内部服务，这么做完全可以，但是一但服务多了起来，就需要处理许多的规范化和兼容性问题。</p><p>例如，对于异构的服务（来自不同语言，不同平台，不同框架）：</p><ol><li><strong>Collect:</strong> 如何收集这些信息？格式是什么？</li><li><strong>Export:</strong> 如何导出（发送）这些信息？</li><li><strong>Protocol:</strong> 用什么协议导出这些信息?</li><li><strong>Persistence:</strong> 信息应该存储在哪里？</li><li><strong>UI:</strong> 如何统一的进行展示？</li></ol><blockquote><p>这也就是 OpenTelemetry 出现的原因，在 OpenTelemetry 出现之前，几乎市面上所有的追踪系统都是以 Dapper 的论文为原型发展出来的，基本上都算是同门师兄弟，功能上并没有太本质的差距，却又受制于实现细节，彼此互斥，很难搭配工作。譬如该怎样进行埋点、Span 上下文具体该有什么数据结构，怎样设计追踪系统与探针或者界面端的 API 接口，等等，都没有权威的规定。</p></blockquote><p>OpenTelemetry （简称 Otel）的出现，除了统一 Tracing 规范，还一口气把可观测性全部大一统了。光是开箱即用的兼容了 Otel 的 Java Library（可以在 <a href="https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md">https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md</a> 看到）就包含了绝大部分常用的热门框架。即使没有原生支持，也可以通过统一的协议和 SDK 来以较低的成本实现扩展和兼容。</p><h3 id="为什么是-OpenTelemetry">为什么是 OpenTelemetry</h3><p>为什么各个大厂愿意放弃自己实现的规范和技术架构，跑过来兼容 OpenTelemetry？其实 OpenTelemetry 的统一已经是大厂之间妥协后达成一致的结果，在 Otel 之前，主要有这两种 Tracing 规范标准：</p><ul><li>在 2016 年，CNCF 技术委员会接受了 OpenTracing 作为基金会第三个项目。（第一个是Kubernetes，第二个是Prometheus）OpenTracing制定了一套平台无关、厂商无关的Trace协议，使得开发人员能够方便的添加或更换分布式追踪系统的实现。OpenTracing 规范公布后，几乎所有业界有名的追踪系统，譬如 <a href="https://github.com/openzipkin/zipkin">Zipkin</a>、<a href="https://github.com/jaegertracing/jaeger">Jaeger</a>、<a href="https://github.com/apache/skywalking">SkyWalking</a> (都是 github 20k+ star 的开源项目) 等都很快宣布支持 OpenTracing。</li><li>这时候大一统本来都要完成统一了，结果google并不认为这个东西是标准，所以推出了自己的 OpenCensus 规范。要是换一家公司来肯定都干不过 OpenTracing 了，但：<ul><li>这可是 Google，要知道最先就是 Google 提出了分布式 Tracing 这个概念。</li><li>OpenCensus 并不是单纯的规范制定，实现了Agent、Collector、SDK 等一系列工具链。</li><li>OpenCensus 又得到了微软的支持。</li></ul></li></ul><p>于是一段时间内都是两架马车并驾齐驱的形势。直到在2019年， 两者和解，共同发起了OpenTelemetry开源项目。最终目标是作为CNCF技术委员会可观测性的终极解决方案，大一统 Tracing，Metrics 和 Logging。<sup id="fnref:4" class="footnote-ref"><a href="#fn:4" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://www.cnblogs.com/zhangmingcheng/p/16720995.html">[4]</span></a></sup></p><h3 id="OpenTelemetry-提供了什么">OpenTelemetry 提供了什么</h3><p>目前 OpenTelemetry 的工作主要集中在如下部分：</p><ul><li>适用于所有组件的<a href="https://opentelemetry.io/docs/specs/otel">规范 </a> (specification)</li><li>定义遥测数据形状的标准<a href="https://opentelemetry.io/docs/specs/otlp/">协议 </a> (protocol)</li><li>为常见遥测数据类型定义标准命名方案的<a href="https://opentelemetry.io/docs/specs/semconv/">语义约定</a> (semantic conventions)</li><li>定义如何生成遥测数据的 API</li><li>实现规范、API 和遥测数据导出的<a href="https://opentelemetry.io/zh/docs/languages">SDK</a></li><li>实现常见库和框架的仪表化的 <a href="https://opentelemetry.io/ecosystem/registry">Library Ecosystem</a></li><li>可自动生成遥测数据的 Auto Instrumentation 组件</li><li><a href="https://opentelemetry.io/zh/docs/collector">OpenTelemetry Collector</a>：接收、处理和导出遥测数据的代理</li></ul><p>同时，可以通过 <a href="https://opentelemetry.io/docs/migration/">官方的迁移指南</a> 将 OpenTracing / OpenCensus 迁移到 OpenTelemetry。</p><p><img src="https://youpai.roccoshi.top/img/202406142352062.png" alt="2019 年 CNCF 展示的路线图"></p><h2 id="Reference">Reference</h2><section class="footnotes"><div class="footnote-list"><ol><li><span id="fn:1" class="footnote-text"><span><a href="https://icyfenix.cn/distribution/observability/">https://icyfenix.cn/distribution/observability/</a><a href="#fnref:1" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:2" class="footnote-text"><span><a href="https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/papers/dapper-2010-1.pdf">https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/papers/dapper-2010-1.pdf</a><a href="#fnref:2" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:3" class="footnote-text"><span><a href="https://www.aspecto.io/blog/what-is-opentelemetry-the-infinitive-guide/">https://www.aspecto.io/blog/what-is-opentelemetry-the-infinitive-guide/</a><a href="#fnref:3" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:4" class="footnote-text"><span><a href="https://www.cnblogs.com/zhangmingcheng/p/16720995.html">https://www.cnblogs.com/zhangmingcheng/p/16720995.html</a><a href="#fnref:4" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:5" class="footnote-text"><span><a href="https://www.51cto.com/article/665025.html">https://www.51cto.com/article/665025.html</a><a href="#fnref:5" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:6" class="footnote-text"><span><a href="https://zhuanlan.zhihu.com/p/74930691">https://zhuanlan.zhihu.com/p/74930691</a><a href="#fnref:6" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:7" class="footnote-text"><span><a href="https://mp.weixin.qq.com/s?__biz=MzI5ODk5ODI4Nw==&amp;mid=2247487034&amp;idx=1&amp;sn=6055d501703bdc4d399a6de898900758&amp;chksm=ec9c015adbeb884c733793202bf663d543a75b9b26f137e5d8370faa2559703b2e1cb6b15a41&amp;scene=21%23wechat_redirect">https://mp.weixin.qq.com/s?__biz=MzI5ODk5ODI4Nw==&amp;mid=2247487034&amp;idx=1&amp;sn=6055d501703bdc4d399a6de898900758&amp;chksm=ec9c015adbeb884c733793202bf663d543a75b9b26f137e5d8370faa2559703b2e1cb6b15a41&amp;scene=21%23wechat_redirect</a><a href="#fnref:7" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:8" class="footnote-text"><span><a href="https://opentelemetry.io/zh/docs/what-is-opentelemetry/">https://opentelemetry.io/zh/docs/what-is-opentelemetry/</a><a href="#fnref:8" rev="footnote" class="footnote-backref"> ↩</a></span></span></li></ol></div></section>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E6%8A%80%E6%9C%AF/">技术</category>
      
      
      <category domain="https://moreality.net/tags/%E5%8F%AF%E8%A7%82%E6%B5%8B%E6%80%A7/">可观测性</category>
      
      <category domain="https://moreality.net/tags/Observability/">Observability</category>
      
      <category domain="https://moreality.net/tags/Trace/">Trace</category>
      
      <category domain="https://moreality.net/tags/OpenTelemetry/">OpenTelemetry</category>
      
      <category domain="https://moreality.net/tags/Metric/">Metric</category>
      
      <category domain="https://moreality.net/tags/Log/">Log</category>
      
      
      <comments>https://moreality.net/posts/25969/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>时间，舒适圈惯性和破窗效应</title>
      <link>https://moreality.net/posts/59390/</link>
      <guid>https://moreality.net/posts/59390/</guid>
      <pubDate>Mon, 10 Jun 2024 08:37:12 GMT</pubDate>
      
      <description>发现有时会以一种匪夷所思的思维惯性跳入时间黑洞，等到爬出来时又后悔不已。</description>
      
      
      
      <content:encoded><![CDATA[<p>端午，一共三天假期，本来想着有不少时间可以好好写一写毕设的中期报告，同时完善一下简历准备秋招提前批的投递。</p><p>结果现在离假期过去只有不到8小时，却看上去一事无成。</p><p>原因只是自己因为固执的想看懂别人 paper 里写的一个算法，并且自己实现一遍，但是因为一直调不出来，直接卡了一天半的时间，现在身心俱疲。</p><p>而自己现在慢慢跳出来后再次回看这件事情，发现十分的荒谬，因为这件事其实并不成为其他优先级更高的事的阻碍，而我花这么久时间只不过是在和自己较劲，也许这也是一种舒适圈惯性，只不过过程和结果都是痛苦的。</p><p>仔细回顾一下，并不是没有花精力和时间，只是由于一种心理习惯，做出了错误的选择。</p><p>于是昨天晚上躺在床上，想了半天自己为什么会屡屡做出这样再后来看来无比愚蠢的决策，于是诞生了此文，记录一些浅薄的思考。</p><h2 id="心理免疫的-X-光片">心理免疫的 X 光片</h2><blockquote><p>”我们都对改变具有本能的抗拒“</p></blockquote><p>这个名词最初是在 <a href="https://justinyan.eth.limo/E0430458-F543-43A2-BEF5-39A40B40886D/">这篇文章</a> 上看到的。</p><p>由凯根提出的”心理免疫的X光片“这一概念，生动形象地阐释了人类内心对于改变的本能抗拒；正如生理层面上人体拥有免疫系统来抵御外来入侵一般，我们的心理也有一套天生的防御机制。</p><p>当我们试图采取新的行为模式时，这套焦虑控制系统就会被触发，使我们感到不安全和焦虑。</p><p>我想称之为「舒适圈惯性」。</p><p>我发现在做不少事情的时候，总会因为一些小的沉没成本而放弃了更加合适的路径的选择。</p><p>一些也许不太恰当的例子：</p><ol><li>选择了一个架构，实现了一部分代码后发现架构不是特别合适，但是目前也勉强能做，于是就懒得改了，直到后面坑越堆越多。</li><li>习惯了某个餐厅和进食习惯，即使后面发现这种习惯并不健康，或者周围出现了更多样化的选择，还是依旧一如既往的选择这种饮食习惯。</li></ol><p>很多时候对于这些事件，我们不愿意去分析他的本质，我们把原因归结于自己的不愿意改变，实际上也许更深层次的原因是「我们的内心就是想要这个行为的结果」。</p><p>文章中提到了一个方法论，个人感觉挺有用的，也许能帮助想明白内心做出后悔选择的逻辑，因为很多时候我们做出选择的时候都在自己骗自己：</p><ol><li><strong>做这件事情，希望达成的目标是什么</strong></li><li><strong>正在做哪些和目标完全相反的行为</strong></li><li><strong>这些与目标相反的行为有哪些隐含的好处或可以避免的损失</strong></li><li><strong>内心是否存在一个重大（但很有可能是错误的）的假设，这个假设是什么？</strong></li></ol><p>文中提到的一个例子：</p><blockquote><p>假设小明在一个互联网企业工作，近期正好赶上一个迭代紧密的项目，时间紧任务重，不得不996，这让小明很痛苦。</p><ol><li>小明希望达成的目标：<ul><li>不要996工作，能有更多的个人时间，看电影玩游戏，陪女朋友以平息她的怒气。</li></ul></li><li>小明正在做哪些跟目标完全相反的行为<ul><li>乖乖996，甚至007。</li></ul></li><li>这些与目标相反的行为有哪些隐含的好处或可以避免的损失<ul><li>小明的项目正在起步阶段，乖乖听老板的话，努力干活，勤奋向上，希望项目上线后可以休息一下。另一方面，这既是挑战也是机会，大家都想要抓住机会，而且如果我不这么干，老板把我开了呢？</li></ul></li><li>小明内心有一个重大的假设，这个假设是什么？<ul><li>如果小明不996，老板就会把他开掉。反过来小明如果996，就能获得老板认可。那么这个假设是否成立呢？我想读者朋友们如果有一定工作经验，应该不难下判断。</li></ul></li></ol></blockquote><h2 id="精神破窗效应">精神破窗效应</h2><blockquote><p>以一幢有少许破窗的建筑为例，如果那些窗没修理好，可能将会有破坏者破坏更多的窗户。最终他们甚至会闯入建筑内，如果发现无人居住，也许就在那里占领、定居或者纵火。又或想像一条人行道有些许纸屑，如果无人清理，不久后就会有更多垃圾，最终人们会视为理所当然地将垃圾顺手丢弃在地上。</p></blockquote><p>破窗效应，或者叫「熵增定律」，无时无刻不在生活中发生。例如快速从整洁变成乱七八糟的状态的房间，例如椅子上越堆越多的脏衣服，例如软件开发中的屎山的形成过程。</p><p>而当自由可支配时间变少时，这种混乱会进一步加剧。</p><p>而一种很有趣的现象就是，当你主动减少或抑制了某几个系统的熵增过程，会促使你去减少其他系统的混乱程度，这似乎是一种正向的情绪传递的过程。</p><p>昨天晚上，我把攒了一周的衣服洗了一遍，然后把攒了一个月混乱不堪的笔记系统也整理了一遍，将桌面的线缆重新梳理了一下；在做完这几件事情后，可以明显感受到焦虑感减少了。</p><p>而另一种（于我而言）很无用的方法就是所谓的「心理暗示」。</p><p>例如，在前两天感到很焦虑的时候，我一直在翻阅相关的文章，然后看到这本书的前几章后，我当时差点没有一口老血吐出来：</p><p><img src="https://youpai.roccoshi.top/img/202406101749828.png" alt="img"></p><p>并不是心理暗示无用，它也许能加速正向过程，前提是一切已经在正向发展的路上了。</p><p>其实任何禅修之道的效果都是个体化差异极大的，但万变不离其宗的是，当意识到当下的行为处于舒适圈惯性或失控的过程中，就需要停下来好好想一想了。</p><p><img src="https://youpai.roccoshi.top/img/202406101757045.jpg" alt="越是忙碌的时候,其实越应该多跳出来思考"></p><h2 id="Reference">Reference</h2><ul><li><a href="https://justinyan.eth.limo/E0430458-F543-43A2-BEF5-39A40B40886D/">枫言枫语：每个人每天都只有24小时，希望我的选择真的是我的选择</a></li><li><a href="https://sspai.com/post/86697">评论尸：现代打工人如何获得幸福？</a></li><li><a href="https://zh.wikipedia.org/wiki/%E7%A0%B4%E7%AA%97%E6%95%88%E5%BA%94">https://zh.wikipedia.org/wiki/破窗效应</a></li><li><a href="https://sspai.com/post/81976">https://sspai.com/post/81976</a></li></ul>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/%E7%94%9F%E6%B4%BB/">生活</category>
      
      <category domain="https://moreality.net/tags/%E8%88%92%E9%80%82%E5%9C%88/">舒适圈</category>
      
      <category domain="https://moreality.net/tags/%E7%A0%B4%E7%AA%97%E6%95%88%E5%BA%94/">破窗效应</category>
      
      <category domain="https://moreality.net/tags/%E6%97%B6%E9%97%B4%E7%AE%A1%E7%90%86/">时间管理</category>
      
      
      <comments>https://moreality.net/posts/59390/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Nvidia GPU 架构演进的简单总结</title>
      <link>https://moreality.net/posts/58631/</link>
      <guid>https://moreality.net/posts/58631/</guid>
      <pubDate>Mon, 03 Jun 2024 02:09:37 GMT</pubDate>
      
      <description>Nvidia GPU 架构演进的简单总结，均为网上搜刮到的一些资料</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="Nvidia-GPU-的架构演进简介">Nvidia GPU 的架构演进简介</h2><p>截止到 2024 年，以著名科学家的名字命名的 Nvidia GPU 微架构的演进历史如下：</p><ul><li><strong>Tesla</strong>（尼古拉·特斯拉，Nikola Tesla）</li><li><strong>Fermi</strong>（恩里科·费米，Enrico Fermi）</li><li><strong>Kepler</strong>（约翰内斯·开普勒，Johannes Kepler）</li><li><strong>Maxwell</strong>（詹姆斯·克拉克·麦克斯韦，James Clerk Maxwell）</li><li><strong>Pascal</strong>（布莱兹·帕斯卡，Blaise Pascal）</li><li><strong>Volta</strong>（亚历山德罗·伏特，Alessandro Volta）</li><li><strong>Turing</strong>（艾伦·图灵，Alan Turing）</li><li><strong>Ampere</strong>（安德烈-玛丽·安培，André-Marie Ampère）</li><li><strong>Hopper</strong>（格蕾丝·霍普，Grace Hopper）</li><li><strong>Ada Lovelace</strong>（艾达·洛夫莱斯，Ada Lovelace）</li><li><strong>Blackwell</strong> (戴维·布莱克韦尔, David Harold Blackwell)</li></ul><p>具体发布时间和代表型号总结:</p><table><thead><tr><th>架构名称</th><th>中文名字</th><th>发布时间</th><th>代表型号</th><th>制程</th></tr></thead><tbody><tr><td>Fermi</td><td>费米</td><td>2010</td><td>Quadro 7000</td><td>40/28nm</td></tr><tr><td>Kepler</td><td>开普勒</td><td>2012</td><td>K80, K40M</td><td>28nm</td></tr><tr><td>Maxwell</td><td>麦克斯韦</td><td>2014</td><td>M5000, M4000, GTX 9XX系列</td><td>28nm</td></tr><tr><td>Pascal</td><td>帕斯卡</td><td>2016</td><td>P100, P6000, GTX 1080</td><td>16nm</td></tr><tr><td>Volta</td><td>伏特</td><td>2017</td><td>V100, Titan V</td><td>12nm</td></tr><tr><td>Turing</td><td>图灵</td><td>2018</td><td>T4, 2080TI, RTX 5000</td><td>12nm</td></tr><tr><td>Ampere</td><td>安培</td><td>2020</td><td>A100, A30系列</td><td>7nm</td></tr><tr><td>Hopper</td><td>赫柏</td><td>2022</td><td>H100</td><td>5nm (4N)</td></tr><tr><td>Ada Lovelace</td><td>艾达</td><td>2022</td><td>RTX 40系列</td><td>5nm (4N)</td></tr><tr><td>Blackwell</td><td>布莱克韦尔</td><td>2024</td><td>B200</td><td>4nm (4NP)</td></tr></tbody></table><h2 id="整体演进的介绍">整体演进的介绍</h2><h3 id="Fermi-架构-2010">Fermi 架构 (2010)</h3><p>Fermi 架构是 NVIDIA 第一个完整的 GPU 计算架构，它支持 ECC 和多任务 CUDA Core 处理，强化了 GPU 的计算能力和错误修正能力。Fermi 引入了多个图形处理簇（GPCs），每个包含多个流处理多处理器（SMs），这些都具备较高的并行处理能力和更好的散热效果。此外，它使用 40/28nm 工艺，搭载了高达 6 GB 的 GDDR5 内存。</p><h3 id="Kepler-架构-2012">Kepler 架构 (2012)</h3><p>Kepler 架构通过优化 CUDA Core 的布局和性能，显著提高了 GPU 的能效比。此架构引入了动态并行技术，允许 GPU 在执行过程中动态调整并行任务的数量和类型，极大地提升了灵活性和计算效率。Kepler 也首次支持 GPU Direct 技术，使得多个 GPU 之间可以直接进行数据传输，避免通过 CPU 中转，减少延迟和增加带宽。</p><h3 id="Maxwell-架构-2014">Maxwell 架构 (2014)</h3><p>Maxwell 架构在前一代基础上进一步提高了性能和能效比，尤其是在图形处理上。它减少了每个处理块的 CUDA Core 数量，增加了更多的控制逻辑，使得 GPU 能够在同等功耗下提供更高的性能。此外，Maxwell 引入了第一代 DSR 技术和 MFAA 技术，提升了图形质量和渲染效率。</p><h3 id="Pascal-架构-2016">Pascal 架构 (2016)</h3><p>Pascal 架构在 GPU 计算能力上有了质的飞跃，首次引入 HBM2 内存和 NVLink 技术，这些改进显著提高了带宽和支持的数据传输速率。此架构专为高性能计算和深度学习优化，提供了显著的性能提升。Pascal 同时支持多达 16 个 SM 和新的半精度浮点数（FP16）运算，为 AI 训练和推理提供加速。</p><h3 id="Volta-架构-2017">Volta 架构 (2017)</h3><p>Volta 架构引入了 Tensor Core，专门设计用于加速深度学习应用中的矩阵运算。它还提升了 NVLink 的带宽，并引入了新的 HBM2 内存技术。Volta 在 AI 计算和图形处理方面均有显著提升，GPU 架构进一步优化了内存使用和数据传输效率。</p><h3 id="Turing-架构-2018">Turing 架构 (2018)</h3><p>Turing 架构标志着 NVIDIA 在光线追踪技术上的重大突破，首次引入 RT Core，专门设计用于实时光线追踪。Turing 也改进了 Tensor Core，增强了 AI 推理能力。此外，它引入了新的 GDDR6 内存，提高了内存速度和带宽，为图形密集型应用和游戏提供了更高的性能。</p><h3 id="Ampere-架构-2020">Ampere 架构 (2020)</h3><p>Ampere 架构进一步扩展了 Tensor Core 和 RT Core 的功能，提供了更强大的 AI 训练和推理能力以及更加逼真的图形渲染性能。它引入了第三代 Tensor Core，支持更多的数据类型和更高的计算效率，同时也推出了多实例 GPU（MIG）技术，允许单个 GPU 资源在多个用户间进行分割，提高了硬件的使用效率。</p><h3 id="Hopper-架构-2022">Hopper 架构 (2022)</h3><p>Hopper 架构引入了第四代 Tensor Core 和多项创新技术，包括改进的 NVLink 和新的 HBM3 内存技术。它专为 AI 计算和高性能计算场景设计，提供了前所未有的计算速度和效率，同时支持更复杂的 AI 模型和算法。</p><h3 id="Blackwell-架构-2024">Blackwell 架构 (2024)</h3><p>Blackwell 架构是 NVIDIA 在 AI 和数据中心应用中的又一巨大飞跃，它引入了第五代 Tensor Core，支持新的数据类型和 AI 训练技术，提供了极高的计算效率和能效比。Blackwell 还优化了 GPU 内部的数据传输和处理机制，为处理大规模数据集和复杂的 AI 模型提供了强大的支持。</p><h2 id="Reference">Reference</h2><ul><li><a href="https://zhuanlan.zhihu.com/p/394352476">NVIDIA GPU 架构梳理 - 知乎</a></li><li><a href="https://zhuanlan.zhihu.com/p/697749383">NVIDIA GPU架构回顾 - 知乎</a></li><li><a href="https://www.nvidia.cn/technologies/">NVIDIA 技术和 GPU 架构 | NVIDIA CN</a></li><li><a href="https://github.com/chenzomi12/AISystem/blob/main/02Hardware/03GPUBase/04History.md">AISystem/02Hardware/03GPUBase - GPU 架构回顾</a></li></ul>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84/">体系结构</category>
      
      
      <category domain="https://moreality.net/tags/Nvidia/">Nvidia</category>
      
      <category domain="https://moreality.net/tags/GPU/">GPU</category>
      
      
      <comments>https://moreality.net/posts/58631/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>杂记 - 走走停停</title>
      <link>https://moreality.net/posts/540/</link>
      <guid>https://moreality.net/posts/540/</guid>
      <pubDate>Fri, 31 May 2024 16:19:59 GMT</pubDate>
      
      <description>北京5月30日下了一场很大的雨，仿佛有人在渡劫</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="北京的天气，雨后的闲逛">北京的天气，雨后的闲逛</h2><p>上周刚刚吐槽过北京的天气一点道理都不讲，今天就碰到了难得一见的极端天气。</p><p>上午还艳阳高照，结果下午突然就8级大风 + 倾盆大雨。</p><p><img src="https://youpai.roccoshi.top/img/202406010021041.png" alt="有种劫后重生的感觉"></p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202406010036697.jpg" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202406010040858.jpg" alt="img"></div></div></div><p>晚上吃完饭去学校旁边的公园逛了逛，此时已经完全看不出一小时前是狂风大作了，一切又恢复了以往的样子，除了稍稍凉爽下来的气温。</p><p>公园里只有老人带着小孩玩耍，来过几次这边的公园，奇怪的是虽然在校园旁边，但是基本见不到学生，公园里的圆形平台正在搭建一个「大学生音乐节」的演出现场，不过也不知道什么时候开始。</p><h2 id="Copilot-Again，关于AI和编程的乐趣">Copilot Again，关于AI和编程的乐趣</h2><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202406010042546.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202406010043372.png" alt="img"></div></div></div><p>申请了一周的 github student pack 总算审批下来了，其中最期待的就是 Copilot 了，在 vscode 上试用了一下，有了很大的进步，无论是操作方面还是模型方面；</p><p>看到了一篇文章：<a href="https://www.piglei.com/articles/chatgpt-and-how-we-programming/">ChatGPT 正在杀死编程里的乐趣</a>，虽然有点标题党，但最后一句总结确实挺到位的：</p><blockquote><p>最后，虽然 ChatGPT 给了我一份质量可比肩（部分）人类的 Python 代码，但我并不认为它会在短时间内消灭编程，让程序员这个职业成为历史。里面的核心原因在于：“规划与设计”部分短期内无法由机器代劳。</p><p>但不会消灭编程，并不等同于 ChatGPT 不会改变人们的编程习惯。我认为它已经开始在调整人们从编程中获取乐趣的方式了——如果不是直接抹除的话。</p></blockquote><p>不可否认的是，在我浅薄的程序经验里，编程中的两大部分快乐，一个是顶层设计的过程，另一个是 Coding 实现的过程。</p><p>而顶层设计的过程其实充满了变数，尤其是当需要与许多人沟通的时候；</p><p>真正 Coding 的过程，是一个可以获得「心流」状态的过程，在这个阶段，人将代码编程具体的实现，并进行不断的调试和修改以满足自己所期望的状态，没有第三方的介入，是全身心的投入。</p><p>但是 AI 的引入，我认为在一定程度上，将这一过程慢慢也变成了一个 <strong>沟通-讨论</strong> 的过程，你需要向 AI 说需求，AI 需要提供 solution，然后你再进行审核，修改；</p><p>如何适应这样的转变，是一个需要打磨的过程。</p><h2 id="赛博折腾">赛博折腾</h2><h3 id="优化手机首屏">优化手机首屏</h3><p>折腾优化了一下手机的首屏内容，把一些常用的 App 修改了一下，不过总体来说没有太大的变化。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202406021053897.png" alt="更新前"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202406021046736.jpg" alt="更新后"></div></div></div><p>上图左右分别是修改前后的手机首屏。</p><p>之前一周 iphone 15pro 掉价到了 6600 元，已经蠢蠢欲动了，但是最后还是没有下手，因为感觉小米13确实找不到太多让我吐槽的东西，感觉目前不太满意的就是 Google FCM 的推送问题，不过这个忍一忍也还好，就是经常需要重启一些软件。</p><p>但是如果换了 iphone，会花 double 的钱得到更差的信号，更小的存储，更拉的续航，以及会丢失一些比较重要的功能比如红外遥控（现在开空调啥的都靠手机了）。</p><p>想想还是没有太大的必要，暂且放弃。</p><h3 id="由-Chrome-Mobile-App-操作引发的思考">由 Chrome Mobile App 操作引发的思考</h3><p>关于主动探寻软件使用技能的重要性。</p><p>用了这么多年的 Android Chrome，第一次发现原来左右滑动顶部的状态栏可以切换标签页。</p><p>然后主动探索了一下有没有更多类似的快捷操作方式，果然，下拉状态栏还可以直接展示标签页，有时间可以通过查看 <a href="chrome://flags">chrome://flags</a>  探索一下更多的 feature。</p><img src="https://youpai.roccoshi.top/img/202406021153390.png" style="zoom:33%;" /><h3 id="阿里云在线-100-天了">阿里云在线 100 天了</h3><p>这个机器是花 500 在闲鱼上买的 3500 的代金券，一口气开到了2030年。</p><p>一个月可以用 200G 单向的流量，阿里云的香港线路是联通的 4837，总之就是快，稳，狠。</p><p>这台机器的存在也是我退出 VPS 圈的最大的原因。这几个月体验相当棒，不管是在什么时间都是一样的稳。</p><p><img src="https://youpai.roccoshi.top/img/202406021103356.png" alt="img"></p><p><img src="https://youpai.roccoshi.top/img/202406021106687.png" alt="关于延迟"></p><h2 id="阅读缺失和自由碎片时间">阅读缺失和自由碎片时间</h2><p>最近一周的阅读量明显变少了，而且电视剧也没怎么看。</p><p>最明显的原因感觉就是出现了「需要做的事情太多」的情况；我是一个比较难切换上下文的人，所以虽然工作当中有许多的自由、碎片化的空闲时间，我也没法拿出来把另一些「正事」推进一下。</p><p>于是最近的主要「高质量（主观）」阅读内容主要来自三个渠道：</p><ul><li>Twitter</li><li>小宇宙</li><li>Telegram Channel</li></ul><p>许多科技相关的前沿讨论都是在 twitter 和 tg 上看到的，感觉应该算是对这类问题讨论最深刻的地方了。</p><p>同时最近也特别喜欢关注一些 vlog 类型的博客散文，毕竟生活不只有奔跑和焦虑。</p><blockquote><p>From: <a href="https://x.com/anqirocks/status/1738844930204913917">https://x.com/anqirocks/status/1738844930204913917</a></p><p>我有一个避免焦虑、获得平静的小妙招。 就是逃离同温层。</p><p>比如，不看脉脉、不看小红书的大厂人相关任何话题、不看微信任何大公司八卦、不看bilibili 任何考证考公攒钱视频。 我不太需要。</p><p>我有自己的海域，有自己的海湾，有自己的潮汐。</p><p>我关注家里人吃穿和心理状况。</p><p>关注人如何具体地爱和宽容忍耐另外一个人。</p><p>关注第一次吃到的东西，大自然的气味。</p><p>关注科技，关注独立的探索者，关注世界上各种各样的生活方式。</p></blockquote><p>随着一些大事的快速逼近，面对随时可能大变的结果，自己能控制的事情十分有限，我更加意识到和自己稳定共存的重要性。</p><p><strong>change is the only constant.</strong></p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/%E7%94%9F%E6%B4%BB/">生活</category>
      
      <category domain="https://moreality.net/tags/%E6%9D%82%E8%AE%B0/">杂记</category>
      
      
      <comments>https://moreality.net/posts/540/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>杂记 - 新的开始</title>
      <link>https://moreality.net/posts/47048/</link>
      <guid>https://moreality.net/posts/47048/</guid>
      <pubDate>Sun, 26 May 2024 16:40:47 GMT</pubDate>
      
      <description>打算开始每周花一些时间进行所见所闻的总结，这是第一期，所以叫「新的开始」。</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="写在前面">写在前面</h2><p>开始写周记的一个动机是看到了 tw93 的 <a href="https://weekly.tw93.fun/">「潮流周刊」</a>。</p><p>想着其实自己一周也会折腾不少的东西，但是作为一个单独的主题发布又显得篇幅太短，于是干脆按照类似的风格，做一个周记类的 Blog 。</p><p>内容估计就是随心所欲的大杂烩了，可能会随着本人的兴趣点而不断地改变，应该会保持一个 <code>##</code> 二级标题作为一小节的模式。</p><h2 id="ChatGPT-Plus">ChatGPT Plus</h2><p>想了很久还是订阅了 ChatGPT Plus，并且直接通过 <code>brew install --cask chatgpt</code> 安装了 chatgpt 的 app。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202405270055849.jpeg" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202405270100770.png" alt="img"></div></div></div><p>这一周多已经完全值回票价，响应速度不是各个镜像站能比的，并且上班 + 毕设一天估计能问出几百个问题，经常甚至可以触发 GPT 的 rate limit：</p><p><img src="https://youpai.roccoshi.top/img/202405270103422.jpg" alt="img"></p><h2 id="Github-Student-Packet">Github Student Packet</h2><p>又到了一年一度续费 github student pack 的时间了, 今年已经完全折腾不动了, 要学信网认证就算了还要账户绑定 billing。</p><p>但是实在想继续使用 github copilot 和 termius plus，于是在研究一系列攻略后，经过 27 次坚持不懈的提交后终于又把 github student package 续上了😭</p><p><img src="https://youpai.roccoshi.top/img/202405270105734.png" alt="img"></p><h2 id="Rewind-ai"><a href="http://Rewind.ai">Rewind.ai</a></h2><p>官网：<a href="https://www.rewind.ai/">rewind.ai</a></p><p>初次使用的时候感觉非常厉害，这是当时晚上写的 <a href="https://memos.roccoshi.top/m/BkXeBp7kcNCvfGSzYb5Nmy">Memos</a></p><blockquote><p>非常逆天的一个软件, 可以实时录屏, 录音, 文字识别, 记录一切屏幕活动, 然后集成 AI 进行搜索;</p><p>刚下载体验了一波, 只能说目前就效果来看效果好到炸裂, 但是有两个严重的问题:</p><ol><li>数据安全性, 需要给屏幕和录音权限, 虽然官网声明所有数据都保存且仅保存在本地: <a href="https://www.rewind.ai/privacy-first">https://www.rewind.ai/privacy-first</a></li><li>能耗, 感觉每分每秒都在录音录屏的耗电应该不小, activity monitor 的指标显示也确实如此;</li></ol><p>另外, 关于存储容量, 官方声明标准配置下一个月使用14GB左右, 感觉是在可接受的范围内, 但是这是根据单个显示器进行估算的, 如果外接了 4k 显示器, 则可能产生更多的数据 <a href="https://sspai.com/post/77790">ref</a></p><blockquote><p>On average, Rewind uses 14 GB per month. If you reduce the retention period, you will save more space. This will also limit how far back you can search.</p></blockquote></blockquote><p>但是实际经过一周的体验下来，感觉需求并不是很大，基本上除了前两天图新鲜会看过两次，之后再也没有用过，而且耗电量实在过于恐怖：</p><p><img src="https://youpai.roccoshi.top/img/202405270108402.png" alt="img"></p><p>并且在我用这个 app 不久，微软直接推出了一个同类产品 <a href="https://support.microsoft.com/zh-cn/windows/%E4%BD%BF%E7%94%A8-%E5%8F%AC%E5%9B%9E-%E6%9D%A5%E5%9B%9E%E6%BA%AF%E6%AD%A5%E9%AA%A4-aa03f8a0-a78b-4b3e-b0a1-2eb8ac48701c">Recall</a>，只能说赶紧卷起来！</p><p><img src="https://youpai.roccoshi.top/img/202405270111512.png" alt="img"></p><h2 id="阅读-控糖革命">阅读 - 控糖革命</h2><p><img src="https://youpai.roccoshi.top/img/202405270104068.png" alt="img"></p><p>读 「控糖革命」发现了许多我传统认知的颠覆性的观点, 很有趣, 发现自己在食品方面由于阅读过少, 很容易把一些口口相传的东西当作事实看待, 但很有可能并不是这样：</p><blockquote><p>很多人认为有些糖，如水果中的糖对我们的身体有好处；而有些糖，如甜品、蛋糕和糖果中的精制糖对身体有害。事实上，这个观点在我们心中根深蒂固。一个世纪以前，加州水果种植交易所（如美国的橙子生产商），也就是后来的新奇士，发起了一项每天喝一杯橙汁的全国性运动，因为橙汁“富含有益于健康的维生素，稀有的盐和酸”。但是，它忘了说橙汁对我们来说是非常有害的，我们可以从其他几十种食物中获得维生素等抗氧化剂，同时又不会对我们的身体造成伤害。不幸的是，奥利维拉和她的朋友们也同样被这个故事骗了。他们认为任何由水果制成的食品都是健康的。产生这种想法，是因为没弄清糖的本质——糖就是糖。不管它是来自玉米还是甜菜，像食用糖那样被制成结晶的白色粉末；还是来自橙子，像果汁那样被制成液体，它都是糖。不管糖来自哪种植物，葡萄糖和果糖对我们产生的影响毫无差别。而因为果汁含有维生素而否认它是有害的，是一个危险的转向游戏。</p></blockquote><p>虽然书写的比较啰唆, 但是只要里面有能有所收获的概念就是好书; 配合 AI 大纲大概 1.5h 就读完了, 打算实践一下先吃纤维类, 再吃蛋白脂肪类, 最后吃碳水的 best practice 试试效果。</p><h2 id="观影-Shameless">观影 - Shameless</h2><p>很久以前看过第一集, 当时觉得我日这是什么狗屎, 怎么这么逆天;</p><p>到现在觉得能接受这部剧的节奏了, 没想到还是被整的节操满地;</p><p>一集比一集更毁三观, 看前半季的时候就感觉全员逆天, 后面慢慢适应了节奏后就爱上了, 按现在的着迷程度来看估计11季没多久就追完了吧 (</p><p><img src="https://youpai.roccoshi.top/img/202405270115016.png" alt="img"></p><h2 id="足总杯-曼城喝高">足总杯 - 曼城喝高</h2><p>凌晨曼城输了曼联，非常的惨痛。可以说上半场是我今年看曼城踢球以来看过的最差劲的一场。不知道为什么瓜总是喜欢让格瓦上首发踢边前卫。面对铁桶阵最好的方法不是上几个能冲的前锋吗？下半场也证明了事实确实如此。总而言之，看完很无语。</p><p><img src="https://youpai.roccoshi.top/img/202405270117049.png" alt="img"></p><h2 id="生活方式：极简主义">生活方式：极简主义</h2><p>看了 youtube 上的一个视频：<a href="https://www.youtube.com/watch?v=frcird_ffyc">https://www.youtube.com/watch?v=frcird_ffyc</a></p><p><img src="https://youpai.roccoshi.top/img/202405270129308.png" alt="img"></p><p>很有同感，其实这段时间一直在进行一些断舍离，包括实物和一些赛博资产；</p><p>一年节约了几千块的订阅费用，感觉扔掉东西的那一刻比买到的时候还高兴。</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/%E7%94%9F%E6%B4%BB/">生活</category>
      
      <category domain="https://moreality.net/tags/%E6%9D%82%E8%AE%B0/">杂记</category>
      
      
      <comments>https://moreality.net/posts/47048/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>2024-生活剪影</title>
      <link>https://moreality.net/posts/62538/</link>
      <guid>https://moreality.net/posts/62538/</guid>
      <pubDate>Wed, 22 May 2024 15:21:58 GMT</pubDate>
      
      <description>又激起一些对博客的兴趣，希望通过这篇文章重新恢复定期的总结</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="重返博客：并不是没有在写东西">重返博客：并不是没有在写东西</h2><blockquote><p>上一次发博客还是在 2023，已经有字面意义上的一年没有更新博客了。</p><p>并不是因为我变得现充了，也不是我不愿意写文字了，而是我很难有大段时间和精力来静下心的进行总结，形成能够发布的长文。</p></blockquote><p>这段时间，我其实主要的随记都发在了我的 <a href="https://memos.roccoshi.top">memos</a> 和 <a href="https://mastodon.social/@Moreality">mastodon</a> 上，在这些平台上发布内容就和发微博和 twitter 一样水到渠成，有时候想法和思绪仅仅就是一瞬间的事情，想到了，记下来，这件事就放下了。</p><p><img src="https://youpai.roccoshi.top/img/202405222341295.png" alt="memos statistics"></p><p>另一方面，我的许多技术内容现在全靠 obsidian 零散地进行记录，感觉许多都是以为了方便查询进行的 notes，如果要总结出来需要花一番功夫，而自己总是不想花费这些时间，虽然我觉得是完全值得并且有意义的。</p><p>然后这半年其实读了不少书和看了不少电视剧和电影，零碎的时间都花在这上面了，当然，读过的大部分书影都有记录：主要是在 <a href="https://neodb.social/users/Moreality/">neodb</a> 平台。</p><p>最后，其余的贡献时间都基本贡献给了 <a href="https://www.nodeseek.com/">https://www.nodeseek.com/</a> ，成为了一名 lv6 的 MJJ，期间买了不下 5000 RMB 的 VPS，当然上个月就基本完全退坑了，目前状态良好，但去年的11月到今年的3月基本处于几个疯魔的状态。<a href="https://memos.roccoshi.top/m/aRFrqW3Y4PL3q4xUMjx7Ey">Related Memos</a></p><p><img src="https://youpai.roccoshi.top/img/202405222347125.png" alt="nodeseek statistics"></p><p>上面这些基本就是我这段时间的互联网娱乐构成，其实停更博客的另一个很大的原因在于 Google 的坑爹收录，明明已经提交了完整的 sitemap 和 rss，但是不知道为什么 Google 就是不做索引和收录，导致直到现在我半年前发布的东西还是没有被 SEO。虽然个人博客的流量本身就很小，但是知道完全不会被 SEO 对个人来说还是有点失望的，自己又不想换域名。</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202405222357573.png" alt="image-20240522235736549"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202405222358694.png" alt="image-20240522235828651"></div></div></div><h3 id="Start-again">Start again</h3><p>anyway，为什么重新开始发 blog，其实是因为最近通过 Twitter 看到了不少开发者的优秀博客，又慢慢激起了我写些什么的欲望。</p><p>由于经典的「破窗效应」，我深知如果再不经营这个博客，估计它就要彻底的被我抛弃在历史的长河了。</p><p>最直接的动机是今晚从 <a href="https://www.v2ex.com/t/1039945">v2ex</a> 上看到这样一句话:</p><blockquote><p>自建博客有很多好处，除了可以实践一些新技术和设计之外，你写的东西，永远都是你自己完全掌控，别人无法碰触。</p></blockquote><p>想想也是，如果连这片自留地都抛弃了，那估计也没有什么地方能随心所欲的进行内容创作了。</p><p>因此我也打算从简单的碎碎念开始，打算逐渐恢复博客的写作了。</p><h2 id="生活剪影：近期的主要活动">生活剪影：近期的主要活动</h2><blockquote><p>主要记录一些 real life 的活动，因为互联网冲浪实在太多了，完全不知道应该记录哪些主题。</p></blockquote><h3 id="Amazon">Amazon</h3><p>大概是去年11月开始，我就开始着手准备找实习了，12月看到 Amazon 的提前批，立马就投了。</p><p>Amazon 的面试是背靠背面试，也就是2个面试官连着，一人一个小时，共2小时就面完；当时面的感受还不错，外企不像国内的互联网公司，完全没有任何的八股提问，还记得第一个面试官主要在聊一个设计题，第二个面试官就是主要面手撕算法；</p><p>然后面完一直到2月过完年才出消息，过程有点曲折，但结果就是通过了，这时候国内互联网公司的暑期实习基本都还完全没开始，想到 amazon 这个 title 也算不错，并且待遇给的算比较高（算下来基本工资 + 补贴一天共有 650 RMB 左右)，就直接接了 offer，两周后的3月中旬就入职了。</p><p>入职的体验嘛，之后可能会专门开一篇文章来写，不过总体来说非常像这位老哥的总结 - <a href="https://www.v2ex.com/t/905705">2022 年终总结 - 生生不息</a> 中写道的：</p><blockquote><p>当天有 8 个实习生入职，结果到了约定时间，就我一个人到了，其他 7 个各种理由迟到，还有个哥们儿说想延后入职时间，hr 说那你怎么不提前联系呢，那哥们儿说：“我忘了。”就这种态度，hr 都给他办了手续做延后入职，看得我很是感动。</p><p>办 IT 手续让我感觉十分原始，IT 部门居然给我掏出了个实体的 usb key 让我插电脑上，说让我连代理和登录的时候需要手指在这实体 key 上摸 3 秒才能成功，蠢的不行，后面还有什么各种软件不好用，内网的文档系统啊、Wiki 系统啊，还有网络代理不会分区管理，国内只能连 us 的代理，巨慢无比，这都不聊了。</p><p>办完 IT 手续就是 manager 约聊，mgr 和我说，你之前可能听说过，咱们 Amazon ，没有国内互联网企业那么卷，但相对于美国 Amazon 来说，还是很有能力的，每次我们中美联合周会，我们都能 deliver 非常多的东西，总部对我们这边也比较重视，所以我觉得你在这儿干，是能学到很多东西的，我也希望你能留下来。</p><p>约聊完我还是对 Amazon 很充满信心的，很有干劲。刚入职会有一些培训视频，我在工位上看到了下午 5 点。到了 5 点，同事突然拍我，说，赶紧下班了，别把你那个字节风气带进来，我说哎好嘞，看完这个视频就下班。等我看完了，同事背着包说和我一起走，我说嗯，我下班和 manager 打个招呼吧，同事说那你打不到了，他 4 点多就走了。</p><p>这个时候我只感觉到：</p><p><img src="https://youpai.roccoshi.top/img/202405230016800.png" alt="image.png"></p><p>入职后我先看了一周的视频，和技术一点关系都没有。每天看的视频类似于《异性同事正常交往注意事项》《职场中的种族歧视》《 Amazon 有多伟大》。经受了几天的洗脑之后，manager 终于给我发 coding task 了。</p><p>我在 Amazon 第一个任务是给一个接口加字段，加哪几个字段、字段叫什么都告诉我了。mentor 前几天让我翻下文档，写个 Design Doc 。我老老实实配了遍测试环境，跑了跑代码。经过我几天精细的调研，认为这个需求写 4 行代码都可以，而且枚举、函数之前都定义好了，我只需要将几个枚举元素扔进函数里跑一下就好了，但是，问题来了，4 行代码，怎么写 Design Doc 呢？</p><p>和 mentor 再次 oneone 的时候，我阐述了我的技术思路，问是不是 4 行代码就能解决这个问题？ mentor 给予了肯定的答复，然后我表达了 Design Doc 的疑问，我实在想不通这怎么写。mentor 又诲人不倦的给我讲了思路：写一下现状，你这个函数的意义，原来是怎么调用的，应该如何测试，在不同国家环境下是不是会有不同行为等等。</p><p>那段时间疫情，在 Amazon 一直居家办公，我就在宿舍干活，一天上不了 3 个小时班，mgr 还会和我说，慢慢做，不着急。就在这个环境下，这 4 行代码我写了三周，我写了 3 周就算了，但是周围人居然都觉得这种速度没啥问题。</p></blockquote><p>总之就是外企的节奏完完全全是另一码事，并且每周只用去3天工位，另外2天可以 WFH (work from home)，而且就算去的那三天也可以下午一两点才到，晚上四五点就走，完全不会有人说你。</p><p>不过好在我做的东西还是有点意思，大概就是做一个分布式的链路追踪系统，这个在 <a href="https://icyfenix.cn/distribution/observability/tracing.html">凤凰架构-分布式-可观测性-链路追踪</a> 中已经描述的很详细了，后面等实习差不多了应该也会开一篇文章总结一下。</p><p>另外 Amazon 的技术栈也几乎全靠自己 build ，从基础的框架，认证，到包管理、数据库、消息中间件、容器集群，几乎没有一个用的是业界通用的耳熟能详的系统，也不免让人有些担忧秋招的时候如何处理这种 GAP，不过在实习的过程中也慢慢了解到，还是有不少中小型公司纯依靠 AWS Service，就能快速构建出一套系统，而且也有不少公司的招聘还要求 AWS 的认证，这些都是之前完全不曾了解的。</p><h3 id="骑车">骑车</h3><p>实习的地方和宿舍有大约 16KM 的路程，如何通勤成为了一个棘手的问题。</p><p>之前买的自行车就派上了用场</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://memos.roccoshi.top/o/r/HdGnmidfQezR7o8mibFkuF?thumbnail=1" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202405230030830.jpeg" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202405230030995.jpeg" alt="img"></div></div></div><p>这样的全程骑行通勤大概就持续了不到 10 次，就完全受不了了，主要原因还是太热了，而且公司没有洗澡的地方，否则估计还能坚持的更久一些，不得不说骑单车还是很爽的。</p><p>之后就改成了半程骑行：坐一半地铁，骑一半自行车，这样一天的骑行也有12km左右，不过现在5月北京的傍晚气温都可以达到30度+，就连这种低强度的骑车都会导致满头大汗。</p><p>除了上班通勤之外，平时也经常骑车去各种地方晃荡，比如经常夜骑奥森公园，最远的一次去了大兴的南海子公园，往返大约60KM的路程（不过那次一回来就感冒了，导致现在还不敢规划更长的行程）。</p><h3 id="羽毛球">羽毛球</h3><p>最近半年又重拾了对羽毛球浓烈的兴趣，光五一这段时间就几乎天天在打：</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202405230036918.jpg" alt="telegram-cloud-photo-size-5-6228972567125932756-y"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202405230036839.jpg" alt="telegram-cloud-photo-size-5-6228972567125932757-y"></div></div></div><p>这段时间还一直在通过录视频的方式记录一些动作细节，感觉通过回看自己的动作确实能发现非常多的问题。</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/%E7%94%9F%E6%B4%BB/">生活</category>
      
      <category domain="https://moreality.net/tags/2024/">2024</category>
      
      
      <comments>https://moreality.net/posts/62538/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>论文阅读: Optimizing Dynamic Neural Networks with Brainstorm</title>
      <link>https://moreality.net/posts/33836/</link>
      <guid>https://moreality.net/posts/33836/</guid>
      <pubDate>Wed, 08 Nov 2023 12:03:29 GMT</pubDate>
      
      <description>Braintorm, 一个用于优化动态神经网络的深度学习框架 (OSDI 2023)</description>
      
      
      
      <content:encoded><![CDATA[<h1>Optimizing Dynamic Neural Networks with Brainstorm</h1><ul><li>OSDI 2023</li><li>presentation: <a href="https://www.youtube.com/watch?v=qBPBQ495YP4&amp;t=2s">https://www.youtube.com/watch?v=qBPBQ495YP4&amp;t=2s</a></li><li>开源 github: <a href="https://github.com/Raphael-Hao/brainstorm">https://github.com/Raphael-Hao/brainstorm</a></li></ul><h2 id="摘要">摘要</h2><blockquote><p>Dynamic neural networks (NNs), which can adapt sparsely activated sub-networks to inputs during inference, have shown significant advantages over static ones in terms of accuracy, computational efficiency, and adaptiveness. However, exist- ing deep learning frameworks and compilers mainly focus on optimizing static NNs with deterministic execution, missing optimization opportunities brought by non-uniform distribu- tion of activation in dynamic NNs. The key to optimizing dynamic NNs is the traceability of how data are dynamically dispatched to different paths at inference. Such dynamism often happens at sub-tensor level (e.g., conditional dispatching tokens of a tensor), thus hard for existing tensor-centric frameworks to trace due to misaligned expression granularity.</p><p>In this paper, we present <strong>Brainstorm</strong>, a deep learning frame- work for optimizing dynamic NNs, which bridges the gap by unifying how dynamism should be expressed. Brainstorm proposes (1) Cell, the key data abstraction that lets model de- velopers express the data granularity where dynamism exists, and (2) Router, a unified interface to let model developers express how Cells should be dynamically dispatched. Brain- storm handles efficient execution of routing actions. This design allows Brainstorm to collect profiles of fine-grained dataflow at the correct granularity. The traceability further opens up a new space of dynamic optimization for dynamic NNs to specialize their execution to the runtime dynamism distribution. Extensive evaluations show Brainstorm brings up to 11.7× speedup (3.29× on average) or leads to 42% less memory consumption for popular dynamic neural networks with the proposed dynamic optimizations.</p></blockquote><ul><li>目前的深度学习框架和编译器主要侧重于优化具有确定性执行的 <strong>静态神经网络</strong></li><li>优化 <strong>动态神经网络</strong> 的关键主要是在推理时数据如何动态分配到不同路径的 <strong>可追溯性</strong></li><li>由于动态通常发生在 sub-tensor 级别, 现有的框架难以跟踪</li><li>本文提出了 Brainstorm, 一个用于优化动态神经网络的深度学习框架, 通过统一的动态表达方式来弥补这个差距</li><li>Brainstorm 主要提出了两个概念:<ul><li><strong>cell</strong>: 关键的数据抽象, 用于让模型的开发者表达动态存在的数据的粒度</li><li><strong>router</strong>: 一个统一接口, 让模型开发者能表达如何动态调度 cell</li></ul></li></ul><h2 id="Introduction-background">Introduction &amp; background</h2><p>三种不同的 Dynamic NN 模式:</p><p><img src="https://youpai.roccoshi.top/img/202311081954017.png" alt="img"></p><ul><li>对输入 token 的 route</li><li>根据超分辨率的难度将图像 patch 到不同的分支</li><li>route new pixels to computation and skips duplicated pixels of previous frames</li></ul><h3 id="一些优化动态网络的机会">一些优化动态网络的机会</h3><p><img src="https://youpai.roccoshi.top/img/202311081954039.png" alt="img"></p><p>上图的 (a), (b) 显示了在两个动态网络中 token / patch 被分配到不同的 expert / branch 的情况, 图中显示这种分配是不均匀的, 可以通过调整分配策略来高效的利用计算资源</p><p><img src="https://youpai.roccoshi.top/img/202311081954050.png" alt="img"></p><p>图 © 显示了一个 MoE 网络的多层相关性, 文中发现两个连续层的分支激活具有相关性, 可以在同一 GPU 上共置专家 (co-locating experts) 来节省 GPU 通信</p><p>图 (d) 显示了 DynamicRouting 中选定 router 的分支激活, 其中有 186 个经过训练的路由器, 可以将图像转发到三个分支中的 1-2 个分支, 然而文中发现许多 router 具有偏向分布, 如图所示</p><p>优化上述动态神经网络的机会主要是能够<strong>以动态发生的粒度来收集和统计</strong></p><p>因此本文的 brainstorm 提出了一种原则性设计: <strong>让模型开发人员公开需要跟踪的信息</strong>, 并利用收集到配置文件进行动态优化</p><h2 id="核心数据抽象-Cell-和-Router">核心数据抽象: Cell 和 Router</h2><h3 id="Cell">Cell</h3><p>为了让模型开发者定义动态发生的数据粒度, brainstorm 使用 cell 这个数据抽象对传统的 tensor 进行增强, cell 是多个 branch 之间动态调度的单位, 模型开发人员可以使用:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs python">brt.annotate_cell(tensor, dims, shape)<br></code></pre></td></tr></table></figure><p>这个 API 对任何 tensor 进行注释</p><h3 id="Router">Router</h3><p>同时为了动态调度 Cell, Brainstorm 引入了统一的 Router API, 可以支持通过 <code>router_fn</code> 来决定 cell 在多个分支之间的动态放置:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Router</span>:  <br><span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">router_fn : Func</span>)  <br><span class="hljs-keyword">def</span> <span class="hljs-title function_">forward</span>(<span class="hljs-params">x : Tensor, kwargs</span>) : <span class="hljs-type">Tuple</span>[Tensor], Routes<br><br><span class="hljs-keyword">def</span> <span class="hljs-title function_">router_fn</span>(<span class="hljs-params">x : Tensor, kwargs</span>) : Routes<br></code></pre></td></tr></table></figure><p>使用 cell 和 router 进行动态调度的例子:</p><p><img src="https://youpai.roccoshi.top/img/202311081954063.png" alt="img"></p><p>通过 cell 和 router 可以实现多种优化, 文章主要介绍了如下几种, 以及这些优化所需的信息:</p><p><img src="https://youpai.roccoshi.top/img/202311081954074.png" alt="img"></p><h2 id="动态优化策略-Dynamic-Optimizations">动态优化策略 Dynamic Optimizations</h2><h3 id="Dynamic-Horizontal-Fusion">Dynamic Horizontal Fusion</h3><p>Horizontal Fusion (水平融合) 是一种编译优化, 可以讲模型的并发分支融合进入融合运算符中, 以提高 GPU 的利用率并减少启动开销</p><p>现有的水平融合方法不能用于动态 DNN, 因为它们假设的是一个<strong>静态数据流图</strong>, 其分支全部由相同的输入激活 (==没太看懂什么意思==)</p><p><img src="https://youpai.roccoshi.top/img/202311081954086.png" alt="img"></p><h3 id="Profile-Guided-Model-Placement">Profile-Guided Model Placement</h3><p><img src="https://youpai.roccoshi.top/img/202404242108969.png" alt="Alt Text"></p><p>如图 c 所示, 两个 expert 同时被激活的概率很高, 这些高度相关的 experts 之间的通信较多, 因此可以将相关的子网络共置在同一个 GPU 上, 以减少 GPU 之间的通信</p><p><img src="https://youpai.roccoshi.top/img/202311081954094.png" alt="img"></p><h3 id="Speculative-Routing">Speculative Routing</h3><p>Speculative Routing (投机路由), 基于分支路由的高度可预测性, 可以投机的跳过 <code>router_fn</code> 的执行来隐藏开销, 当错误预测时再重新执行分支</p><p><img src="https://youpai.roccoshi.top/img/202404242108620.png" alt="Alt Text"></p><h3 id="Speculative-Weight-Preloading">Speculative Weight Preloading</h3><p>为了在有限的 GPU 内存上运行大型模型的推理, 通常需要在 GPU 内存和 CPU 内存之间交换层的 weights 以减少 GPU 的内存需求, 为了隐藏内存迁移的延迟, 现有的解决方案需要知道各层的执行顺序, 以便在以流水线方式执行前面的层时预加载必要的权重, 但是动态神经网络没有确定的层执行顺序, 动态激活的分支只有在做出路由决策的时候才能知道</p><p>和 speculative routing 相似, 本文采用分支激活的统计分布来进行 weight 的预加载, 当预测失败的时候重新执行分支</p><h2 id="跟踪-Cell-level-的数据流">跟踪 Cell-level 的数据流</h2><p>为了实现之前所说的动态优化策略, 最重要的是判断 cell 是如何沿着网络传输的, 在动态 NN 中, 有两种类型的单元级数据流:</p><ol><li><strong>static dataflow</strong>: 绝大多数静态算子, 比如 <code>Conv2D</code></li><li><strong>dyanmic dataflow</strong>: 由 <code>Router</code> 在运行时确定的数据流</li></ol><p>前者是为了确定 cell 在静态层之间的关系; 后者是为了确定 cell 是如何在分支之间被路由的.</p><h3 id="static-cell-level-dataflow">static cell-level dataflow</h3><blockquote><p>Tensor-centric dataflow graphs only preserve relations be- tween tensors without the information of Cells. To trace all possible Cell-level dataflow of static operators, Brainstorm uses symbolic execution at Cell-level to extract finer-grained relations in ahead-of-time compiling.</p></blockquote><p>为了追踪 cell-level 的信息, brainstorm 采用了符号执行 (symbolic execution) 来在编译前提取更加细粒度的信息</p><p><img src="https://youpai.roccoshi.top/img/202311081954104.png" alt="img"></p><p>Figure 8(a) 展示了几种包含多个 cell 的 tensor 的乘法示例, 其中 A 和 B 表示 cell:</p><ol><li>The first preserves cell positions</li><li>The second reorders cells</li><li>The third mixes all cells in the output</li></ol><p>Figure 8(b) 展示了两个 MoE 层之间的自注意力算子的静态 cell 级数据流, ==这里也没太看懂==</p><h3 id="dynamic-cell-level-dataflow">dynamic cell-level dataflow</h3><p>路由策略由开发者在 <code>router_fn</code> 中指定 , Brainstorm 只收集路由决策的统计概况, 而不关心他们的生成方式</p><p>如果 Cell-level 的分析策略被启用, 每次调用 <code>Router</code> 时, brainstorm 将 routing decision 收集到一个 buffer 中, brainstorm 设计了一个单独的线程将缓冲区流式传输到配置文件</p><h2 id="Implementation">Implementation</h2><p>braintstorm 基于 pytorch, 13000 line of code:</p><ul><li>3000 lines: brainstorm core abstraction</li><li>3000 lines: dynamic optimizations</li><li>3000 lines: c++ code for kernel scheduling &amp; sparse cell communication</li><li>1500 lines: 1500 lines for auto-transformation</li></ul><p><img src="https://youpai.roccoshi.top/img/202311081954114.png" alt="img"></p><h2 id="相关概念">相关概念</h2><h3 id="MoE-mixture-of-expert">MoE (mixture-of-expert)</h3><p>MoE(Mixture of Experts) 是一种将多个神经网络专家组合起来进行训练和预测的方法。其主要思想是:</p><ol><li>将输入数据分配给不同的专家网络。通常使用一个 <strong>门控网络</strong> 来学习如何将数据分配给不同的专家。</li><li>每个专家网络只专注于处理部分数据,从而整体模型能够 Capture 更多的distribution modes。</li><li>在预测时,将输入同时提供给每个专家网络,由门控网络合并每个专家的预测得到最终结果。</li><li>通过端到端的训练,门控网络和专家网络都能够协同优化。</li></ol>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E9%98%85%E8%AF%BB/">阅读</category>
      
      
      <category domain="https://moreality.net/tags/NN/">NN</category>
      
      <category domain="https://moreality.net/tags/Dynamic-Neural-Networks/">Dynamic Neural Networks</category>
      
      <category domain="https://moreality.net/tags/DL/">DL</category>
      
      <category domain="https://moreality.net/tags/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/">深度学习</category>
      
      <category domain="https://moreality.net/tags/%E8%AE%AD%E7%BB%83%E6%A1%86%E6%9E%B6/">训练框架</category>
      
      <category domain="https://moreality.net/tags/OSDI/">OSDI</category>
      
      
      <comments>https://moreality.net/posts/33836/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>Nvidia MPS 的简单介绍</title>
      <link>https://moreality.net/posts/1991/</link>
      <guid>https://moreality.net/posts/1991/</guid>
      <pubDate>Mon, 06 Nov 2023 14:22:08 GMT</pubDate>
      
      <description>看了一篇 nvidia mps 的 slide, 简单做一下笔记, 主要介绍了 hyper-q 和 mps 的概念和基本使用</description>
      
      
      
      <content:encoded><![CDATA[<p>PPT: <a href="https://www.nvidia.cn/content/dam/en-zz/zh_cn/assets/webinars/31oct2019c/20191031_MPS_davidwu.pdf">https://www.nvidia.cn/content/dam/en-zz/zh_cn/assets/webinars/31oct2019c/20191031_MPS_davidwu.pdf</a><br>视频: <a href="https://info.nvidia.com/228093-ondemand.html">https://info.nvidia.com/228093-ondemand.html</a> (<a href="https://www.bilibili.com/video/BV17E411e7E3">bilibili转载版本</a>)</p><h2 id="背景">背景</h2><p>对于这样一个 cuda 进程 :</p><p><img src="https://youpai.roccoshi.top/img/202311062220076.png" alt="img"></p><p>发射一个进程和同时发射四个进程, 四个进程的 <strong>每个进程</strong> 执行时间都是单个进程的 4 倍, 说明实际上进程是 「串行执行」的</p><p><img src="https://youpai.roccoshi.top/img/202311062220093.png" alt="img"></p><p><img src="https://youpai.roccoshi.top/img/202311062220103.png" alt="img"></p><h2 id="Cuda-Context">Cuda Context</h2><p><img src="https://youpai.roccoshi.top/img/202311062220116.png" alt="img"></p><h3 id="cuda-context-是如何切换的">cuda context 是如何切换的</h3><p>这里以三个进程切换为例:</p><p><img src="https://youpai.roccoshi.top/img/202311062220129.png" alt="img"></p><p>实际上 GPU 的进程执行是一种 <strong>「轮转调度」</strong> 的方式, 每个进程的 context 在不断被切换, 每个时刻只有一个进程在 GPU 上执行, 无法达到「并行」, 并且由于上下文切换的开销, 吞吐率反而下降.</p><h2 id="Hyper-Q-queue">Hyper-Q (queue)</h2><p>Hyper-Q 是一个需要 <strong>硬件支持</strong> 的特性, 允许多个 CPU 线程或者进程同时 <strong>加载任务</strong> 到一个 GPU 上, 实现 CUDA kernels 的 <strong>并发执行</strong></p><h3 id="Hyper-Q-支持的连接类型">Hyper Q 支持的连接类型</h3><ul><li>Multi cuda streams</li><li>Multi cpu threads</li><li>Multi cpu processes (<strong>MPS</strong>)</li></ul><h3 id="Hyper-Q-的好处和限制">Hyper Q 的好处和限制</h3><p><img src="https://youpai.roccoshi.top/img/202311062220138.png" alt="img"></p><h3 id="有-Hyper-Q-和没有-Hyper-Q-的区别">有 Hyper Q 和没有 Hyper Q 的区别</h3><p>示例代码:</p><p><img src="https://youpai.roccoshi.top/img/202311062220147.png" alt="img"></p><p>当没有 Hyper Q 时 (只有前一个 stream 的最后一个 kernel 和下一个 stream 的第一个 kernel 存在 overlap):</p><p><img src="https://youpai.roccoshi.top/img/202311062220155.png" alt="img"></p><p>当存在 Hyper Q 时:</p><p><img src="https://youpai.roccoshi.top/img/202311062220165.png" alt="img"></p><h2 id="MPS-Multi-Process-Service">MPS (Multi-Process Service)</h2><p><img src="https://youpai.roccoshi.top/img/202311062220174.png" alt="img"></p><h3 id="MPS-的好处和使用限制">MPS 的好处和使用限制</h3><p><img src="https://youpai.roccoshi.top/img/202311062220184.png" alt="img"></p><p>最大用户连接数量即 <strong>可并发执行的进程数量</strong></p><h3 id="MPS-的使用">MPS 的使用</h3><blockquote><p>MPS 的使用无须修改程序代码, 只需要修改程序运行的「模式」</p></blockquote><h4 id="1-设置-GPU-的独占模式">1 - 设置 GPU 的独占模式</h4><p>相当于设置 GPU 上只允许有一个 context</p><p><img src="https://youpai.roccoshi.top/img/202311062220197.png" alt="img"></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">sudo nvidia-smi -i 0 -c EXCLUSIVE_PROCESS<br></code></pre></td></tr></table></figure><h4 id="2-启动-MPS-守护进程">2 - 启动 MPS 守护进程</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">nvidia-cuda-mps-control -d<br></code></pre></td></tr></table></figure><p><img src="https://youpai.roccoshi.top/img/202311062220205.png" alt="img"></p><h4 id="3-查看是否正在执行-MPS-守护进程">3 - 查看是否正在执行 MPS 守护进程</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">ps -ef | grep mps<br></code></pre></td></tr></table></figure><h4 id="4-使用-MPS-运行应用程序">4 - 使用 MPS 运行应用程序</h4><p><img src="https://youpai.roccoshi.top/img/202311062220213.png" alt="img"></p><h4 id="5-停止和监视">5 - 停止和监视</h4><p><img src="https://youpai.roccoshi.top/img/202311062220221.png" alt="img"></p><h4 id="6-性能分析-nvprof">6 - 性能分析 (nvprof)</h4><p><img src="https://youpai.roccoshi.top/img/202311062220230.png" alt="img"></p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E9%98%85%E8%AF%BB/">阅读</category>
      
      
      <category domain="https://moreality.net/tags/Nvidia/">Nvidia</category>
      
      <category domain="https://moreality.net/tags/GPU/">GPU</category>
      
      <category domain="https://moreality.net/tags/Hyper-Q/">Hyper Q</category>
      
      <category domain="https://moreality.net/tags/MPS/">MPS</category>
      
      
      <comments>https://moreality.net/posts/1991/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>docker pull 卡住(代理问题) 的解决方案</title>
      <link>https://moreality.net/posts/45709/</link>
      <guid>https://moreality.net/posts/45709/</guid>
      <pubDate>Tue, 31 Oct 2023 15:10:18 GMT</pubDate>
      
      <description>折腾了一晚上, 来记录一下解决方案</description>
      
      
      
      <content:encoded><![CDATA[<blockquote><p>被这个问题解决了一晚上, 已经没有心思吐槽了, 直接简洁易懂的介绍方法以及为什么配置环境变量不起作用</p></blockquote><h2 id="出现问题复现">出现问题复现</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker pull xx<br></code></pre></td></tr></table></figure><p>这个命令一直卡死, 如果继续跟踪报错, 出现:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">Error response from daemon: Get &quot;https://registry-1.docker.io/v2/&quot; xxx<br></code></pre></td></tr></table></figure><p>很显然是被 Q 了的原因</p><h3 id="尝试的无效方案">尝试的无效方案</h3><p>使用:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">export http_proxy=&quot;xx&quot;<br></code></pre></td></tr></table></figure><p>无法解决</p><p>使用系统级代理 <code>proxychains</code> 无法解决</p><p>使用官方的代理配置方案: <a href="https://docs.docker.com/network/proxy/">https://docs.docker.com/network/proxy/</a></p><p>在本地 <code>~/.docker/config.json</code> 配置无效</p><h2 id="解决方案">解决方案</h2><blockquote><p>在执行<code>docker pull</code>时，是由守护进程<code>dockerd</code>来执行。 因此，代理需要配在<code>dockerd</code>的环境中。 而这个环境，则是受<code>systemd</code>所管控，因此实际是<code>systemd</code>的配置。</p></blockquote><p>而上述方案以及官方提供的解决思路解决的是 <strong>容器运行时</strong> 的代理配置</p><p>而修改 systemd 的代理需要使用这个文档中的 proxy: <a href="https://docs.docker.com/config/daemon/systemd/">https://docs.docker.com/config/daemon/systemd/</a></p><h2 id="步骤">步骤</h2><p>下面直接复述解决方案:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">sudo mkdir -p /etc/systemd/system/docker.service.d <br>sudo vim /etc/systemd/system/docker.service.d/proxy.conf <br></code></pre></td></tr></table></figure><p>添加:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">[Service] <br>Environment=&quot;HTTP_PROXY=http://proxy.example.com:8080/&quot; Environment=&quot;HTTPS_PROXY=http://proxy.example.com:8080/&quot; Environment=&quot;NO_PROXY=localhost,127.0.0.1,.example.com&quot;<br></code></pre></td></tr></table></figure><p>然后重启 docker daemon:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">sudo systemctl daemon-reload<br>sudo systemctl restart docker<br></code></pre></td></tr></table></figure><p>即可完成 <code>docker pull</code></p><h2 id="不推荐的解决方案">不推荐的解决方案</h2><p>配置镜像 registry, 实测一堆莫名其妙的 bug</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E4%BB%A3%E7%A0%81/">代码</category>
      
      
      <category domain="https://moreality.net/tags/docker/">docker</category>
      
      <category domain="https://moreality.net/tags/docker-daemon/">docker daemon</category>
      
      <category domain="https://moreality.net/tags/system/">system</category>
      
      <category domain="https://moreality.net/tags/proxy/">proxy</category>
      
      <category domain="https://moreality.net/tags/%E4%BB%A3%E7%90%86/">代理</category>
      
      <category domain="https://moreality.net/tags/gfw/">gfw</category>
      
      
      <comments>https://moreality.net/posts/45709/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>TorchServe入门-quickstart</title>
      <link>https://moreality.net/posts/47206/</link>
      <guid>https://moreality.net/posts/47206/</guid>
      <pubDate>Tue, 31 Oct 2023 08:34:52 GMT</pubDate>
      
      <description>使用 docker (compose) 部署 torchserve</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="环境">环境</h2><ul><li>linux ubuntu22.04, x86-64 (理论上可以安装 docker 的环境均可, 架构不限)</li><li>cpu only</li></ul><h2 id="torch-serve-介绍">torch serve 介绍</h2><p>随便网上抄一句…</p><blockquote><p>TorchServe<strong>是一个轻量级的模型服务器，可以轻松地将PyTorch模型部署为RESTful API或TorchServe的自定义格式</strong>。 它支持多模型管理，自动扩展和生命周期管理，可以快速部署和管理模型。 TorchServe还支持模型推理缓存、异步推理和自定义处理程序，可以根据需求自由配置。</p></blockquote><h2 id="1-创建-docker-compose">1 - 创建 docker compose</h2><p>参考: <a href="https://github.com/pytorch/serve/blob/master/docker/README.md#create-torch-model-archiver-from-container">https://github.com/pytorch/serve/blob/master/docker/README.md#create-torch-model-archiver-from-container</a></p><p>对应的 docker compose:</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs yml"><span class="hljs-attr">version:</span> <span class="hljs-string">&quot;3&quot;</span><br><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">torchserve:</span> <br>    <span class="hljs-attr">image:</span> <span class="hljs-string">pytorch/torchserve:latest</span><br>    <span class="hljs-attr">ports:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">&quot;28080:8080&quot;</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">&quot;28081:8081&quot;</span><br>    <span class="hljs-attr">volumes:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./model-store:/home/model-server/model-store</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./examples:/home/model-server/examples</span><br></code></pre></td></tr></table></figure><ul><li>其中 <code>examples</code> 对应: <a href="https://github.com/pytorch/serve/tree/master/examples">https://github.com/pytorch/serve/tree/master/examples</a></li><li><code>model-store</code> 对应 <code>mar</code> 文件 (mar文件:  使用 torchserve archiver 压缩后的模型, 包括了 trained model, inference handler 和 model handler)</li></ul><h2 id="2-进入容器的方式">2 - 进入容器的方式</h2><p>进入容器</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker compose exec torchserve sh<br></code></pre></td></tr></table></figure><p><img src="https://youpai.roccoshi.top/img/20231031160833.png" alt="img"></p><h2 id="3-下载模型权重">3 - 下载模型权重</h2><p>进入到 <code>docker-compose.yml</code> 所在目录, 执行:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">curl -o ./examples/image_classifier/densenet161-8d451a50.pth https://download.pytorch.org/models/densenet161-8d451a50.pth<br></code></pre></td></tr></table></figure><p><img src="https://youpai.roccoshi.top/img/202310311641938.png" alt="img"></p><h2 id="4-运行-torch-model-archiver-在容器中">4 - 运行 torch model archiver (在容器中)</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">torch-model-archiver --model-name densenet161 --version 1.0 --model-file /home/model-server/examples/image_classifier/densenet_161/model.py --serialized-file /home/model-server/examples/image_classifier/densenet161-8d451a50.pth --export-path /home/model-server/model-store --extra-files /home/model-server/examples/image_classifier/index_to_name.json --handler image_classifier<br></code></pre></td></tr></table></figure><p>然后在 <code>./model-store</code> 下会出现打包好的 <code>densenet161.mar</code>:</p><p><img src="https://youpai.roccoshi.top/img/202310311641960.png" alt="img"></p><p><a href="https://github.com/pytorch/serve/blob/master/model-archiver/README.md">关于 torch model archiver 的介绍</a></p><p>A key feature of TorchServe is the ability to package all model artifacts into a single model archive file. It is a separate command line interface (CLI), <code>torch-model-archiver</code>, that can take model checkpoints or model definition file with state_dict, and package them into a <code>.mar</code> file. This file can then be redistributed and served by anyone using TorchServe. It takes in the following model artifacts: a model checkpoint file in case of torchscript or a model definition file and a state_dict file in case of eager mode, and other optional assets that may be required to serve the model. The CLI creates a <code>.mar</code> file that TorchServe’s server CLI uses to serve the models.</p><blockquote><p>TorchServe 的一个关键功能是能够将所有模型工件打包到单个模型存档文件中。它是一个单独的命令行界面 (CLI) <code>torch-model-archiver</code> ，可以使用 state_dict 获取模型检查点或模型定义文件，并将它们打包到 <code>.mar</code> 文件中。然后，任何使用 TorchServe 的人都可以重新分发和提供该文件。它包含以下模型工件：在 torchscript 的情况下为模型检查点文件，在 eager 模式下为模型定义文件和 state_dict 文件，以及为模型提供服务可能需要的其他可选资产。 CLI 创建一个 <code>.mar</code> 文件，TorchServe 的服务器 CLI 使用该文件来提供模型。</p></blockquote><h2 id="5-运行-torchserve">5 - 运行 torchserve</h2><p>在容器中运行:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">torchserve --start --model-store model-store --models densenet161=densenet161.mar<br></code></pre></td></tr></table></figure><p>或者将 <code>docker-compose.yml</code> 添加一行 <code>command</code>:</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs yml"><span class="hljs-attr">version:</span> <span class="hljs-string">&quot;3&quot;</span><br><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">torchserve:</span><br>    <span class="hljs-attr">image:</span> <span class="hljs-string">pytorch/torchserve:latest</span><br>    <span class="hljs-attr">container_name:</span> <span class="hljs-string">torchserve</span><br>    <span class="hljs-attr">ports:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">&quot;28080:8080&quot;</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">&quot;28081:8081&quot;</span><br>    <span class="hljs-attr">volumes:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./model-store:/home/model-server/model-store</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./examples:/home/model-server/examples</span><br>    <span class="hljs-attr">command:</span> <span class="hljs-string">torchserve</span> <span class="hljs-string">--start</span> <span class="hljs-string">--model-store</span> <span class="hljs-string">model-store</span> <span class="hljs-string">--models</span> <span class="hljs-string">densenet161=densenet161.mar</span><br></code></pre></td></tr></table></figure><p>然后重新启动容器</p><h2 id="6-使用-REST-api-进行验证">6 - 使用 REST api 进行验证</h2><p>下载一张图片:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">curl -O https://raw.githubusercontent.com/pytorch/serve/master/docs/images/kitten_small.jpg<br></code></pre></td></tr></table></figure><p>用部署的 densenet161 模型进行预测:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">curl http://127.0.0.1:28080/predictions/densenet161 -T kitten_small.jpg<br></code></pre></td></tr></table></figure><p><img src="https://youpai.roccoshi.top/img/202310311641970.png" alt="img"></p><h2 id="内存占用情况">内存占用情况</h2><p>在仅仅跑一个模型的情况下占用约 3.5GiB</p><p><img src="https://youpai.roccoshi.top/img/202310311644199.png" alt="img"></p><h2 id="参考资料">参考资料</h2><ul><li><a href="https://xiang753017.gitbook.io/zixiang-blog/shi-yong-torchserve-bu-shu-model">博客: 使用 TorchServe 部署 Model</a></li><li><a href="https://pytorch.org/serve/getting_started.html">https://pytorch.org/serve/getting_started.html</a></li><li><a href="https://github.com/pytorch/serve/blob/master/docker/README.md#create-torch-model-archiver-from-container">https://github.com/pytorch/serve/blob/master/docker/README.md#create-torch-model-archiver-from-container</a></li></ul>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E4%BB%A3%E7%A0%81/">代码</category>
      
      
      <category domain="https://moreality.net/tags/pytorch/">pytorch</category>
      
      <category domain="https://moreality.net/tags/model-serving/">model serving</category>
      
      <category domain="https://moreality.net/tags/torchserve/">torchserve</category>
      
      <category domain="https://moreality.net/tags/%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86/">模型推理</category>
      
      
      <comments>https://moreality.net/posts/47206/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>入坑骑车</title>
      <link>https://moreality.net/posts/50637/</link>
      <guid>https://moreality.net/posts/50637/</guid>
      <pubDate>Mon, 30 Oct 2023 11:33:59 GMT</pubDate>
      
      <description>最后还是把「入坑骑行」改成了「入坑骑车」,因为也许目前不会有出行的计划.</description>
      
      
      
      <content:encoded><![CDATA[<p>最初的想法来源于暑假, 当时刚放假感到无所事事, 思考了一下在北京一个人可以长期干的娱乐活动(运动), 除了爬山似乎也没有什么别的了</p><p>打羽毛球场地颇贵, 自己又对跑步没有兴趣</p><p>于是就把目光锁定在了骑车上, 因为北京对自行车的支持程度还是挺友好的, 到处都有自行车道, 而且秋天的北京确实适合出行, 很少下雨, 气候干爽舒适</p><p>于是就又下血本买了一套骑车的装备, 打算将其视为一个长期的锻炼项目 (当然羽毛球还是 No.1)</p><p><img src="https://youpai.roccoshi.top/img/202310301905390.png" alt="img"></p><p>目前购买的配置大概是这样的, 总价还是不低的, 而且我也没想好到时候毕业怎么把这玩意弄回家…</p><p>然后之后大概进行了 3 次 20 km以上的骑行:</p><p><img src="https://youpai.roccoshi.top/img/202310301909146.png" alt="img"></p><h2 id="奥森">奥森</h2><p>去了两次奥森, 一次是晚上9点, 一次是晚上10点, 不得不说晚一个小时路况完全不一样, 可能是 9 点刚好赶上程序员的下班高峰吧(</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202310301915089.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202310301916214.png" alt="img"></div></div></div><p>然后第二次直接没绷住, 到下车的时候手环都开始报警了, 心率后程直接上 200, 属于新手强行上强度, 直接把有氧运动变成无氧… 后面得注意一下了.</p><p><img src="https://youpai.roccoshi.top/img/202310301918462.png" alt="img"></p><h2 id="三山五园">三山五园</h2><p>这应该是人生中第一次骑 50km +, 吸了极多尾气, 走了极多回头路, 还掉了一次链子, 总体来说体验很差, 不过在路过不知道什么公园的时候骑进去感受了一波里面的新鲜空气, 那一带的公园环境总体来说都是很棒的</p><p><img src="https://youpai.roccoshi.top/img/202310301924640.png" alt="img"></p><p>傍晚的时候路过香山和西山, 感觉西山大门还是挺美挺壮观的, 可惜没时间进去玩玩了, 下次有机会再骑过去到里面玩一下</p><h2 id="后续">后续</h2><p>然后在骑完这几次后, 就喜提<strong>感冒 + 阳了</strong>两重奏, 直接两周左右无法运动</p><blockquote><p>希望在冬天到来之前还可以出去骑几次</p></blockquote>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/%E8%BF%90%E5%8A%A8/">运动</category>
      
      <category domain="https://moreality.net/tags/%E9%AA%91%E8%A1%8C/">骑行</category>
      
      <category domain="https://moreality.net/tags/%E5%87%BA%E9%97%A8/">出门</category>
      
      <category domain="https://moreality.net/tags/%E7%A7%8B%E5%A4%A9/">秋天</category>
      
      <category domain="https://moreality.net/tags/%E5%8C%97%E4%BA%AC/">北京</category>
      
      
      <comments>https://moreality.net/posts/50637/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>饥荒服务器搭建 (docker)</title>
      <link>https://moreality.net/posts/17791/</link>
      <guid>https://moreality.net/posts/17791/</guid>
      <pubDate>Fri, 21 Jul 2023 18:36:50 GMT</pubDate>
      
      <description>用 docker 搭建饥荒服务器, 顺便解决了几个bug, 这里对流程做一个详细记录</description>
      
      
      
      <content:encoded><![CDATA[<blockquote><p>steam上玩饥荒如果是某一个人作为服务端的话, 那么其他人的延迟会取决于这个人的带宽和与他连接的延迟, 并且如果服务端掉线了, 其他人是无法继续游戏的, 总之是一个不怎么好的游戏体验</p></blockquote><p>这时候就需要专用服务器 (Dedicated Server) 来进行配置, 这样无论何时, 几个人, 只要服务器处于运行状态就可以进行游戏.</p><p>然而官方的 server 配置过于复杂: <a href="https://dontstarve.fandom.com/wiki/Guides/Don%E2%80%99t_Starve_Together_Dedicated_Servers#On_Linux_(Debian)">https://dontstarve.fandom.com/wiki/Guides/Don’t_Starve_Together_Dedicated_Servers#On_Linux_(Debian)</a></p><p>这就驱动了第三方开发各种各样的脚本来方便部署, 其中 docker 就是一个特别好用的打包手段, 这里选择了 github 上 star 最多的一个仓库作为示例, 实际上其他的镜像也都大差不差: <a href="https://github.com/Jamesits/docker-dst-server">https://github.com/Jamesits/docker-dst-server</a></p><p class="note note-info">注: 本文面向对 linux 和 docker 有一定了解的人, 不对系统和依赖安装做过多介绍, 主要内容在于如何进行配置</p><h2 id="0-预准备">0 - 预准备</h2><p>系统: debian11 (linux系统应该均可)</p><h3 id="docker-安装">docker 安装</h3><p>直接参考官网教程就可以: <a href="https://docs.docker.com/engine/install">https://docs.docker.com/engine/install</a></p><h2 id="1-配置-docker-compose">1 - 配置 docker compose</h2><p>来源于官方示例: <a href="https://github.com/Jamesits/docker-dst-server/blob/master/docker-compose.yml">https://github.com/Jamesits/docker-dst-server/blob/master/docker-compose.yml</a></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs shell">version: &#x27;3.5&#x27;<br>services:<br>  dst-server:<br>     image: jamesits/dst-server:latest<br>     restart: &quot;on-failure:5&quot;<br>     ports:<br>        - &quot;10999-11000:10999-11000/udp&quot;<br>        - &quot;12346-12347:12346-12347/udp&quot;<br>     volumes:<br>        - ./data:/data<br>     stop_grace_period: 6m<br></code></pre></td></tr></table></figure><p>新建一个文件夹, 将上述文件保存为 <code>docker-compose.yml</code> 后启动 <code>docker-compose up</code>, 这时就会在文件夹下生成如下所示的目录:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><code class="hljs shell">├── Cluster_1    ## 配置地上世界和洞穴以及一些必要的配置<br>│   ├── Caves     ## 洞穴配置<br>│   │   ├── modoverrides.lua  ## mod参数配置<br>│   │   ├── server.ini   ## 洞穴独立配置文件<br>│   │   ├── server_chat_log.txt  ## 聊天日志<br>│   │   ├── server_log.txt  ## 系统日志<br>│   │   └── worldgenoverride.lua  ##世界地图配置<br>│   ├── Master  ## 地上世界配置<br>│   │   ├── modoverrides.lua  ## mod参数配置<br>│   │   ├── server.ini   ## 洞穴独立配置文件<br>│   │   ├── server_chat_log.txt  ## 聊天日志<br>│   │   ├── server_log.txt  ## 系统日志<br>│   │   └── worldgenoverride.lua  ##世界地图配置<br>│   ├── admin.txt  ## 管理员名单<br>│   ├── blocklist.txt   ## 黑名单<br>│   ├── cluster.ini   ## 通用配置文件<br>│   ├── cluster_token.txt  ## token<br>│   └── whitelist.txt  ## 白名单<br>├── mods   ## mod配置<br>│   └── dedicated_server_mods_setup.lua   ## 保存服务器所用到的mod<br></code></pre></td></tr></table></figure><h2 id="2-申请-token">2 - 申请 token</h2><ul><li>客户端打开饥荒游戏</li><li>点击左下角 <code>account</code></li><li>在弹出的浏览器中点击  <code>游戏</code></li><li><img src="https://youpai.roccoshi.top/img/202307191423917.png" alt="img"></li><li>点击添加服务器, 然后记住 token</li><li><img src="https://youpai.roccoshi.top/img/202307191423919.png" alt="img"></li><li>到 docker 中将 token 黏贴到: <code>./data/DoNotStarveTogether/Cluster_1/cluster_token.txt</code></li><li>进行 cluster 和 mod 等配置 (见下文), 然后重启即可</li></ul><h2 id="3-对-cluster-进行配置">3 - 对 cluster 进行配置</h2><p>需要修改的文件:</p><p><code>./data/DoNotStarveTogether/Cluster_1/cluster.ini</code>, 是对服务端属性的一些通用配置, 具体示例如下:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><code class="hljs ini"><span class="hljs-comment">; Here is all the server configs you will ever want to change</span><br><span class="hljs-section">[NETWORK]</span><br><span class="hljs-comment">; DO change the name and description please!</span><br><span class="hljs-attr">cluster_name</span> = 老王艳春 <span class="hljs-comment">; 房间名</span><br><span class="hljs-attr">cluster_description</span> = 老王艳春玩饥荒 <span class="hljs-comment">; 房间描述</span><br><span class="hljs-attr">cluster_password</span> = <span class="hljs-number">123123</span> <span class="hljs-comment">; 房间密码</span><br><span class="hljs-attr">offline_cluster</span> = <span class="hljs-literal">false</span> <span class="hljs-comment">; 离线模式</span><br><span class="hljs-attr">lan_only_cluster</span> = <span class="hljs-literal">false</span> <span class="hljs-comment">; 局域网模式</span><br><span class="hljs-attr">whitelist_slots</span> = <span class="hljs-number">1</span> <span class="hljs-comment">; 白名单模式</span><br><span class="hljs-attr">cluster_intention</span> = social <span class="hljs-comment">; 游戏偏好，可选 cooperative, competitive, social, or madness，随便设置，没卵用</span><br><span class="hljs-attr">cluster_language</span> = zh <span class="hljs-comment">; 语言</span><br><span class="hljs-attr">autosaver_enabled</span> = <span class="hljs-literal">true</span> <span class="hljs-comment">; 自动保存</span><br><span class="hljs-attr">tick_rate</span> = <span class="hljs-number">30</span> <span class="hljs-comment">; 建议设置为 15-30, 数值越大服务器计算开销越大, 客户端通信频率越高</span><br><br><span class="hljs-section">[GAMEPLAY]</span><br><span class="hljs-attr">game_mode</span> = survival <span class="hljs-comment">; 游戏模式，可选 survival, endless or wilderness，与玩家死亡后的负面影响有关</span><br><span class="hljs-attr">max_players</span> = <span class="hljs-number">8</span> <span class="hljs-comment">; 最多游戏人数</span><br><span class="hljs-attr">pvp</span> = <span class="hljs-literal">false</span> <span class="hljs-comment">; 是否开启玩家攻击</span><br><span class="hljs-attr">pause_when_empty</span> = <span class="hljs-literal">true</span> <span class="hljs-comment">; 没有人的时候是否暂停游戏</span><br><span class="hljs-attr">vote_kick_enabled</span> = <span class="hljs-literal">false</span> <span class="hljs-comment">; 投票踢人</span><br><br><span class="hljs-comment">; ----------- 以下的文件不用修改, 用自动生成的就可以</span><br><br><span class="hljs-section">[STEAM]</span><br><span class="hljs-attr">steam_group_only</span> = <span class="hljs-literal">false</span><br><span class="hljs-attr">steam_group_id</span> = <span class="hljs-number">0</span><br><span class="hljs-attr">steam_group_admins</span> = <span class="hljs-literal">false</span><br><br><span class="hljs-section">[MISC]</span><br><span class="hljs-attr">console_enabled</span> = <span class="hljs-literal">true</span><br><span class="hljs-attr">max_snapshots</span> = <span class="hljs-number">6</span><br><br><span class="hljs-comment">; ====================================================================</span><br><span class="hljs-comment">; STOP! Don&#x27;t change configs below unless you know what you are doing.</span><br><span class="hljs-comment">; ====================================================================</span><br><span class="hljs-section">[SHARD]</span><br><span class="hljs-attr">shard_enabled</span> = <span class="hljs-literal">true</span><br><span class="hljs-attr">bind_ip</span> = <span class="hljs-number">127.0</span>.<span class="hljs-number">0.1</span><br><span class="hljs-attr">master_ip</span> = <span class="hljs-number">127.0</span>.<span class="hljs-number">0.1</span><br><span class="hljs-attr">master_port</span> = <span class="hljs-number">10998</span><br><span class="hljs-attr">cluster_key</span> = MsAhBOXhhnElO5IPKr4G<br></code></pre></td></tr></table></figure><h2 id="4-mod-安装-重点">4 - mod 安装 (重点)</h2><blockquote><p>需要注意的是, 此 docker 镜像在 mod 安装时有bug, 具体可以参考这个ISSUE: <a href="https://github.com/Jamesits/docker-dst-server/issues/53">https://github.com/Jamesits/docker-dst-server/issues/53</a></p><p>简单来说就是有些 mod 会装不上, 因为 v1 和 v2 的 mod 会下载到不同的文件夹中, 而 docker 中只处理了一个版本的 mod</p><p>以下会对如何修复做出介绍</p></blockquote><h3 id="通用-mod-安装">通用 mod 安装</h3><p>mod 的安装需要服务端和客户端都进行配置, 具体如下:</p><p><strong>服务端配置</strong> :</p><p>编辑: <code>./data/DoNotStarveTogether/Cluster_1/mods/dedicated_server_mods_setup.lua</code>, 填写自己想安装的 mod id, 示例:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs shell">--There are two functions that will install mods, ServerModSetup and ServerModCollectionSetup. Put the calls to the functions in this file and they will be executed on boot.<br><br>--ServerModSetup takes a string of a specific mod&#x27;s Workshop id. It will download and install the mod to your mod directory on boot.<br>--The Workshop id can be found at the end of the url to the mod&#x27;s Workshop page.<br>--Example: http://steamcommunity.com/sharedfiles/filedetails/?id=350811795<br>--ServerModSetup(&quot;350811795&quot;)<br><br>--ServerModCollectionSetup takes a string of a specific mod&#x27;s Workshop id. It will download all the mods in the collection and install them to the mod directory on boot.<br>--The Workshop id can be found at the end of the url to the collection&#x27;s Workshop page.<br>--Example: http://steamcommunity.com/sharedfiles/filedetails/?id=379114180<br>--ServerModCollectionSetup(&quot;379114180&quot;)<br><br>ServerModSetup(&quot;375859599&quot;) -- global position<br>ServerModSetup(&quot;378160973&quot;) -- health info<br>ServerModSetup(&quot;666155465&quot;) -- show me<br>ServerModSetup(&quot;501385076&quot;) -- 快速采集<br>ServerModSetup(&quot;1207269058&quot;) -- simple health bar<br></code></pre></td></tr></table></figure><p>其中 mod id 就是点开 steam 的创意工坊, <code>?id=xxx</code> 后跟的一串数字</p><p><img src="https://youpai.roccoshi.top/img/202307191423920.png" alt="img"></p><p class="note note-warning">注: mod 主要有 all_clients_require_mod , server_only_mod 以及 clients_only_mod, 服务器只需要安装前两个, 最后一个是客户端本地安装和配置的, 不需要在服务器上安装</p><p>上述配置主要是让服务端下载 mod, 然后对于 mod 的配置文件需要客户端进入游戏进行 ui 配置 (当然你能手写配置文件也行), 然后上传到服务器, 步骤如下:</p><p><strong>客户端配置</strong>:</p><ul><li><p>首先需要客户端安装同样的 mod, 并在配置中进行自己想要的配置, 然后按如下步骤将客户端的配置上传到服务器:</p></li><li><p>在 <strong>本地</strong> 上传 <code>modoverrides.lua</code> 文件到服务器对应目录</p><ul><li>本地保存位置: (mac 为例, windows 只是前缀不一样, 其他路径是一模一样的) <code>/Documents/Klei/DoNotStarveTogether/xxxxx/Cluster_1/Master/modoverrides.lua</code></li><li>服务器保存位置: <code>/data/DoNotStarveTogether/Cluster_1/Master/modoverrides.lua</code></li></ul></li><li><p>在服务器上重启 <code>docker compose restart</code> 即可</p></li></ul><h3 id="mod-安装不上的-bug-以及解决方案">mod 安装不上的 bug 以及解决方案</h3><p>在 mod 安装过程中, 我发现有几个 mod 死活装不上, 查看 <a href="https://github.com/Jamesits/docker-dst-server/issues/53">ISSUE</a> 后发现原因:</p><p>原因是mod有两个版本, 一个安装在 <code>~/Library/Application Support/Steam/steamapps/workshop/content</code> 中, 而另一个安装在 <code>~/Library/Application Support/Steam/steamapps/common/Don't Starve Together/dontstarve_steam.app/Contents/mods</code> 中</p><p>docker 版本只会下载第一种mod, 不会下载第二种</p><p>这时候就需要手动将第一种 mod 的安装文件上传到服务器, 经测试服务器重启后可以正确解析和安装 mod, 方法如下:</p><ul><li><p>本地的 v2 mod 的位置: <code>Library/Application Support/Steam/steamapps/common/Don't Starve Together/dontstarve_steam.app/Contents/mods/workshop-*</code></p></li><li><p>服务器 v2 mod 的存放位置: <code>/data/DoNotStarveTogether/Cluster_1/mods/</code></p></li></ul><p>以我自己为例, 直接 scp 上传即可:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">scp -r /Users/roccoshi/Library/Application Support/Steam/steamapps/common/Don&#x27;t Starve Together/dontstarve_steam.app/Contents/mods/workshop-* moreality@&lt;服务器IP&gt;:/home/moreality/docker-apps/dst-server/data/DoNotStarveTogether/Cluster_1/mods/<br></code></pre></td></tr></table></figure><p>然后重启就大功告成啦!</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/docker/">docker</category>
      
      <category domain="https://moreality.net/tags/game/">game</category>
      
      <category domain="https://moreality.net/tags/%E6%B8%B8%E6%88%8F/">游戏</category>
      
      <category domain="https://moreality.net/tags/%E9%A5%A5%E8%8D%92/">饥荒</category>
      
      <category domain="https://moreality.net/tags/%E6%9C%8D%E5%8A%A1%E5%99%A8/">服务器</category>
      
      
      <comments>https://moreality.net/posts/17791/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>生活剪影</title>
      <link>https://moreality.net/posts/17152/</link>
      <guid>https://moreality.net/posts/17152/</guid>
      <pubDate>Mon, 26 Jun 2023 18:36:50 GMT</pubDate>
      
      <description>又有一个多月没有发布过东西了, 来简单说说最近的一些生活剪影</description>
      
      
      
      <content:encoded><![CDATA[<blockquote><p>这半个月虽然没有更新过什么博客, 但是折腾却一直在持续, 碎片化的生活让我很难有大块空闲的时间来总结梳理出一片完整的博客, 而另一个借口则是我将更多的记录放在了私有化的 obsidian 中, 而少数希望分享出来的东西则以另一种形式放在了 <a href="https://memos.roccoshi.top">memos</a> 上.</p></blockquote><p>一些过于细节的配置和技术我已经下定决心不再往博客上随便堆砌了, 但是还是有很多值得分享的东西, 这两个月总的来说比较清闲, 所以有了一些碎片化的时间来到处了解, 以下是近期的生活剪影, 可能杂乱无章, 但都是表达 ——</p><h2 id="2023-05-21-注册Hacker-News">2023-05-21 注册Hacker News</h2><p><a href="https://memos.roccoshi.top/m/95">Morealityの碎碎念 (roccoshi.top)</a></p><p>Hacker News 的注册方式是我见过最简单的，同时 web 也是继 stackoverflow 以来我见过最复古 (老土) 的，可想而知这个网站的目的就是通过老土的 css 样式筛选到一部分为了新鲜而点进来的吃瓜群众，留下一些关注内容的人群。</p><p>从 v2ex 的 <a href="https://www.v2ex.com/t/904663">这篇文章</a> 可以看出, 即使只需要填写用户名和密码即可注册这个网站, 它的注册量在 2022 年底也不过 80 万, 对比一些其他网站的<strong>月活用户</strong>就可以看出这是多么微不足道的一个数字:</p><p>来自 datareportal 的 <a href="https://datareportal.com/social-media-users">2023 年 4 月的统计数据</a></p><p><img src="https://youpai.roccoshi.top/img/202305211459981.png" alt="img"></p><p>而点进 Hacker News 也可以看到评论确实质量都非常高, 那这个网站是如何保证评论质量的呢, 靠审查机制? 靠用户过滤?</p><p>从知乎的 <a href="https://www.zhihu.com/question/19661127/answer/2284119532">这个回答</a> 可以看出:</p><blockquote><p><strong>ID 可以随便注册，但是</strong>:</p><ul><li>对新注册 ID 发帖的审核非常的严格，一旦触发 spam 规则，整个 ID 都会被标注成 dead</li><li>一旦 ID 变成 dead 相当于封禁，但是管理员不会告诉你。你仍然可以发帖、评论。但你发布的内容别人一般是看不到的，除非他打开了“showdead”选项。要验证这一点，你可以登出以后去找找你刚发布的内容，看看是否可见。</li></ul></blockquote><p>以及如何让一个新用户发言不被封禁的方式:</p><blockquote><ul><li>在 <a href="https://link.zhihu.com/?target=https%3A//news.ycombinator.com/">https://news.ycombinator.com/</a> 上完成 ID 注册。</li><li>去 ID 设置里面填上自己的邮箱，个人简介也可以写一下。</li><li>千万不要一上来就发帖。</li><li><strong>找一个你能发表有价值意见的帖子，进行评论。</strong></li><li>登出账号，看看你原来的回复是否依然可见。</li><li>如果回复不可见，说明你的发言触发了某个 spam 规则，ID 这时一般已经被设置为 dead 。</li><li>因为你写的回复是有价值的，你可以进行申诉。发邮件给 hn @ <a href="https://link.zhihu.com/?target=http%3A//ycombinator.com">http://ycombinator.com</a> 进行申诉。</li><li>管理员会进行核实，如果你的回复确实不是垃圾，那么你的 ID 也就通过审核了。</li><li>在未登录的状态下看看之前的回复是否可见。然后找另一个帖子进行回复，再确认一下是否能成功发布内容。</li></ul></blockquote><p>果然, 不能指望圈子外的人一上来就融入圈子, 最佳的社区管理方式莫过于设立严格的门槛.</p><h2 id="2023-05-29-关于书写">2023-05-29-关于书写</h2><p><a href="https://memos.roccoshi.top/m/102">Morealityの碎碎念 (roccoshi.top)</a></p><p>一个听刺猬 <a href="https://neodb.social/album/29n5gdqokrsuFtPmo9xuvf">NeoDB - 音乐 | 赤子呓语一生梦</a> 的晚上突然想起了两个月没写过字墨都干了的钢笔, 然后抄了这么一段词:</p><p><img src="https://youpai.roccoshi.top/img/202306270236231.png" alt="img"></p><p>瞬间找回了初高中痴迷写钢笔的感觉, 然后买了个本子, 在无事的时候抄抄词, 也算是一种生活情调.</p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202306270236232.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202306270236234.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202306270236235.png" alt="img"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202306270236236.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202306270236237.png" alt="img"></div></div></div><h2 id="2023-05-23-新手机">2023-05-23-新手机</h2><p><a href="https://memos.roccoshi.top/m/97">Morealityの碎碎念 (roccoshi.top)</a></p><p>之前的 realme neo2 其实不是不能用, 但是那段时间不知怎么的就非常希望换手机, 然后疯狂痴迷于小米 13 的小直屏.</p><p><img src="https://youpai.roccoshi.top/img/202306270236238.png" alt="img"></p><p>用了一个多月了, 事实证明, 确实不错.</p><h2 id="2023-05-11-youtube-music">2023-05-11-youtube music</h2><p><a href="https://memos.roccoshi.top/m/85">Morealityの碎碎念 (roccoshi.top)</a></p><p>在这一天切换到了 youtube music, 一来是价格便宜 (美区家庭车 60RMB / 年)</p><p>二来确实资源多, 转过来后第一时间就是在歌单里加了一堆李志的歌</p><p>然后广告也很少, 推荐算法当时觉得非常好, 但是感觉还是有点茧房的感觉, 虽然分类推荐了, 但是也因为我懒的原因吧, 总是不喜欢在歌单间换来换去, 所以经常听相同的歌</p><p><img src="https://youpai.roccoshi.top/img/202306270236239.png" alt="img"></p><h2 id="2023-05-26-obsidian-的同步">2023-05-26-obsidian 的同步</h2><p><a href="https://memos.roccoshi.top/m/98">Morealityの碎碎念 (roccoshi.top)</a></p><p><img src="https://youpai.roccoshi.top/img/202306270245677.png" alt="img"></p><p>可以算一个里程碑的节点了, 从这天之后我的笔记从 notion + obsidian + typora + git + vscode 直接大一统到了 obsidian.</p><p>livesync 是一个很好用的插件, 特别是对于我这种 windows 和 mac 双开的用户而言 (这种场景下坚果云等同步盘很不好用, 经常有 conflict, 如果用 git 同步又需要保证每次切换设备的时候都在一个设备上push, 然后另一台设备pull, 同时过于频繁的 git commit + 无效的 message 我觉得有违 git 管理的初衷, git 不应该用在 “每x分钟自动同步” 这个概念上)</p><p>同时 obsidian 可以直接管理链接, pdf, 图片自动批量上传下载, 光是这几个插件就已经让我爱不释手了, 目前除了表格这个大 bug, 感觉其他都可以忍受.</p><h2 id="2023-06-19-记账">2023-06-19-记账</h2><p><a href="https://memos.roccoshi.top/m/125">Morealityの碎碎念 (roccoshi.top)</a></p><p>感觉最近的消费越来越有控制不住的趋势, 今天自己将最近 3 个月来的的支付宝和微信记录做了一个导出, 然后发现每日消费居然如此夸张:</p><p><img src="https://youpai.roccoshi.top/img/202306192038652.png" alt="img"></p><p>于是下载了钱迹然后打算持续记账. 这里记录一下目前的 idea:</p><p>首先的想法是用安卓机 root 一下然后装 <a href="https://github.com/Auto-Accounting/Qianji_auto">自动记账的插件</a> , 然后利用无障碍和 <a href="https://docs.qianjiapp.com/plugin/auto_tasker.html">钱迹的api</a> 自动实现记账, 但是仔细研究了一下配置和用户反馈, 发现这个方式并不好: 一来它无法让我清楚感知到每日的消费情况, 二来基于正则匹配模式的记录难免存在漏记或分类不完善的情况.</p><p>于是我思考了一下还是决定手动记录, 钱迹存在跨平台和云同步让我不用担心数据的同步和记录问题, 而我实际上也不需要手动备份数据, 因为我的初衷不过是控制消费罢了, 能保留半年的数据让我达到修正消费的目的, 对我来说就已经足够.</p><p>分类的 tag 尽量小, 参考了 v站一位朋友的总结:</p><p><img src="https://youpai.roccoshi.top/img/202306192043907.png" alt="img"></p><p>将消费简单的分为:</p><ol><li><strong>维持</strong> : 一日三餐, 必备的生活用品, 水电等</li><li><strong>消费</strong> : 夜宵, 饮料, 衣服, 新设备等可有可无的满足物欲的东西</li><li><strong>提升</strong> : 打球, 读书, 学习等值得投入的东西</li></ol><p>而需要修正的消费理念也就显而易见了:</p><p>“稳定维持类, 减少消费类, 在保证总额的情况下增加 <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mfrac><mtext>提升类</mtext><mtext>总额</mtext></mfrac></mrow><annotation encoding="application/x-tex">\frac{\text{提升类}}{\text{总额}}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1.2173em;vertical-align:-0.345em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.8723em;"><span style="top:-2.655em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord cjk_fallback mtight">总额</span></span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.394em;"><span class="pstrut" style="height:3em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mtight"><span class="mord text mtight"><span class="mord cjk_fallback mtight">提升类</span></span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.345em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span> 的比例.”</p><p>上述操作我这周一直在践行, 目前体验不错, 确实能一定程度上让我克制报复性消费的欲望.</p><h2 id="2023-06-08-google-photos">2023-06-08-google photos</h2><blockquote><p>下午开了用google photos把所有照片都备份了一下，然后刚才不知不觉用5分钟时间就全部看了一遍。</p><p>四年时间，何其短暂。</p><p>同时也很后悔当时第一次换手机的时候居然把照片全删了。当时觉得没什么拍的很好看的，可现在看来即使是在拍一下食堂的午饭，图书馆的桌椅，宿舍的洗手间，现在看来都是满满的回忆。</p></blockquote><p>40元买了 google one 2T 的空间, 然后花了好几天时间把所有能找到的之前拍的图片通通上传到 google photos 了, 不得不说 google photos 确实是我体验过最好的照片管理软件, 之前一直对这类软件持相同的看法, 认为不过是起一个云盘的作用, 收费还死贵, 就一直本地保存, 但是实际上 google photos 的智能分类和 AI 搜索这些功能可以说超乎我的意料, 识别准确度相当高.</p><p>然后备份 google photos 的同时也看了这几年的照片, 很后悔 2018-2020 这段时间的照片随着我的手机一同逝去了, 然后看 V 站这个帖子: <a href="https://www.v2ex.com/t/949756">上大学后和毕业后这段时期没留下什么回忆，是不是说明这段时期都浪费掉，白过了？ - V2EX</a></p><p><img src="https://youpai.roccoshi.top/img/202306270236241.png" alt="img"></p><p>确实记录生活是有意义且必要的一件事, 于是不仅是照片, 最近也在刻意的做一些影音书籍的标记.</p><h2 id="2023-06-27-neodb">2023-06-27-neodb</h2><p><a href="https://memos.roccoshi.top/m/128">Morealityの碎碎念 (roccoshi.top)</a></p><p>正如上面 google photos 那一章所说, 影音标记这块目前国内基本上只有豆瓣一家, 但是豆瓣的审核制度和备份让我很不放心, 从 2019 年之后貌似 API 也关了, 不知道葫芦里卖的什么药, 和微博一样, 不仅没有官方支持的信息导出, 还面临着严重的信息审查制度.</p><p>然而 trakt 和 imdb 功能过于专一, 后面发现了一个很不错的站点: <a href="https://neodb.social/discover/">NeoDB - 发现</a></p><p>于是直接将豆瓣的观影记录 (极其有限) 导入, 然后今天闲着无聊写了一个 notion 和 neodb 的同步脚本:</p><p><a href="https://github.com/Lincest/neodb-notion-sync">Lincest/neodb-notion-sync: sync neodb to notion database (github.com)</a></p><p><img src="https://youpai.roccoshi.top/img/202306270236242.png" alt="img"></p><blockquote><p>碎片化的生活往往伴随着焦虑的情绪, 之后也要多做专注的事情, 比如读书. (完)</p></blockquote>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/%E7%94%9F%E6%B4%BB%E5%89%AA%E5%BD%B1/">生活剪影</category>
      
      
      <comments>https://moreality.net/posts/17152/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>狠狠的剁手 —— 「macbook揽件日志」</title>
      <link>https://moreality.net/posts/16771/</link>
      <guid>https://moreality.net/posts/16771/</guid>
      <pubDate>Sat, 29 Apr 2023 03:28:21 GMT</pubDate>
      
      <description>观望mac已经很久了，这次总算下定决定购买了，使用一周后谈谈我的理解，包括使用场景，系统体验以及各方面的杂谈</description>
      
      
      
      <content:encoded><![CDATA[<h1>狠狠的剁手 ——「macbook揽件日志」</h1><h2 id="背景">背景</h2><p>早在2020年买幻14的时，就考虑过mac，但是当时由于m1的软件支持度较差，并且价格极为昂贵，就迟迟没有入手。</p><p>现在已经有了一台48G内存的win本做支撑，基本上不存在后顾之忧了，所以一直就在观望一个入手mac的好时机。</p><p>一开始想买mac的理由无非是：</p><ul><li>macos的系统体验</li><li>屏幕素质和续航</li></ul><p>打算的场景就是在宿舍工作，开会，以及实验室搞累了可以换个环境写代码或者写文档笔记。</p><p>目前的幻14几乎无法满足移动工作的需求，一来是脱离了电源屏幕素质大幅度下降，整个画面给人一种很虚的感觉，然后就是待机最多撑不过2.5小时，并且只要运行一点点编译之类需要用到cpu的活的时候就键盘滚烫。</p><p>以及一点很难受的，当我把幻14的显示器线拔掉再插上的时候，我的各种软件的分辨率总是以一种难以理解的方式存在，要么是接上显示器了还保留着笔记本的分辨率，要么是拔下来了还保留着显示器的分辨率。</p><p>总之在经过两年左右的使用后我的评价就是：<strong>这就是一台必须要电源 + 显示器才能干活的东西。</strong> 然而这个砖头一样的电源我也厌倦了带着它到处跑。</p><p>然后上周逛v站的时候看到了一个商家13800的32G + 1T + M1 pro的全新配置，加群观望两天后发现服务还挺不错的，当时就有点动心了。</p><h2 id="契机">契机</h2><p>然后上周突发奇想把整个系统换到了win11，然后又把win11从21H2更新到了22H2，然后喜闻乐见的时期就发生了——我的电脑直接开不了机了。。</p><p>然后我花了一下午的时间找bug，然后在折腾了2小时后愤然直接下单mac，实际上算是一次冲动消费了，也算是windows给我狠狠的上了一课。</p><p>（最后也找到了原因：<a href="https://github.com/valinet/ExplorerPatcher">https://github.com/valinet/ExplorerPatcher</a> ，最后我在PE上把这软件卸载后就好了，教训就是以后不要随便装侵入式软件了，没准哪天系统更新就直接变砖了还半天找不到原因）</p><h2 id="体验">体验</h2><p>在我使用了近一周的mac后，到五一放假才慢吞吞的写这篇博客，这些天基本上mac的全部开发环境都装好了，并且拿来实际测试了各种不同环境下的工作场景。</p><p>总的来说，体验优异，但也伴随着一些槽点。</p><p><img src="https://youpai.roccoshi.top/img/202304292001497.png" alt="img"></p><h3 id="整体体验">整体体验</h3><p>首先是预料之内的，高屏幕素质和高续航带来的体验。</p><p>早上带着mac出来上一上午课 + 课上记笔记和写代码，下午在实验室不插电源继续工作一下午，晚上回到宿舍大概还能剩下30%</p><p>然后经常换换环境工作确实很爽啊，不用在实验室待整整一天了，这一周我在实验室的时间几乎是之前的一半，总是游走于走廊的座位和宿舍之间，感觉就是有助于身心健康（</p><p>由于mac的触控板体验极佳，不需要外接任何东西就能工作的很好，经过一周的体验，我发现mac对于我来说最佳的使用场景还是不要外接任何东西：</p><ul><li>首先外接了显示器就需要外接键盘+鼠标，然而：<ul><li>首先我的显示器只有2k分辨率，mac的hidpi支持不好，即使装了better display, 整个字体渲染出来还是比windows相去甚远</li><li>其次我的键盘针对windows之前改过键位，然后我的mac本身又装了karabiner并且定制了一套针对capslock的键位：<a href="https://gist.github.com/Lincest/a43dd5495728ce5f43150c76d5ce92bc">gist: karabiner.json</a>， 如果再外接键盘又得重新修改键位</li><li>mac的外接鼠标移动是有加速度的，总之手感比较奇怪</li></ul></li><li>其次外接显示器会大幅增加本身耗电，这意味着我得每天带着个充电器出来或者在实验室再准备一个充电器，而我在不外接设备的情况下mac绝对可以保证我在外一天不用充电</li></ul><p>总之就是各方面体验都会下降，还降低了便携性，并且我还得把我的（配置实际上更好）的win本拿出来给mac腾地方，多么的得不偿失。</p><h3 id="系统体验">系统体验</h3><p>上面就是我对mac的使用场景的一些理解，然后谈谈mac的系统。</p><p>macos自带zsh的原生体验绝对比windows上的terminal + wsl的体验要好很多，在windows上我曾无数次的考虑两个问题——开发环境到底是放在win里还是wsl里，以及wsl是否要和win共享环境变量？</p><p>都说wsl好，但实际上体验还是割裂的，如果把开发环境放在wsl里，只能用jetbrains或者vscode远程连接开发，总是存在各种体验不到位，jetbrains的远程基本是个残废，而vscode写java总是有点不够使。</p><p>而如果把文件放在win下，wsl提供编译/运行环境，又存在着因为文件系统的不一致导致的效率低下的问题。</p><p>这种割裂感在我查找了大量资料后仍然没有得到一个完美的解决方案。</p><p>但是mac就不一样了，由于是类unix系统，直接开一堆tmux搞环境只能说爽的一批。</p><p>上面是对于os的一些个人体验，然后就是ui的问题，这块只能说有好有坏。</p><p>好处就不多说了，整体的动画和ui的精细程度都做得非常好，但是在界面和交互逻辑上还是有几点令我挺不爽的：</p><ol><li><strong>调度中心</strong>： 这一点也是我不喜欢mac外接显示器+鼠标使用的原因，自带的触控板由于可以三指上划选择窗口而导致体验良好，但是如果外接了键盘，体验就会大幅下降，因为mac自带的<code>command + tab</code>的底层逻辑不是切换&quot;窗口&quot;, 而是切换&quot;程序&quot;, 这意味着:<ul><li>当你在关闭所有程序窗口后, 使用<code>command + tab</code>只会将焦点移出当前窗口, 除此之外不会调度出目标程序的窗口</li><li>当目标程序窗口有多开时, 不会全部显示而是就显示最上层的一个</li></ul></li><li><strong>窗口的放大和移动逻辑</strong>: 自带没有将窗口&quot;最大化&quot;的功能, 而只有全屏的功能, 并且窗口左右/上下分屏不如windows的直接移动来的好, 但是这个可以通过软件解决</li><li><strong>刘海</strong>: 刘海 + 菜单栏间距的加大化导致在14寸屏幕下菜单栏的右边根本放不下几个软件, 而有些软件打开又是只在菜单栏放一个图标, 用别的都打不开( dropbox, onedrive, snipaste, picgo等等挺多的), 相当于那个软件就直接废掉了, 目前的解决方案只能通过将刘海屏去掉(整体屏幕下移), 或者用hidden bar或者bartender等软件进行菜单栏的收纳管理</li></ol><h2 id="all-in-all">all in all</h2><p>这个mac今后的用途大概就是: 拿来到处跑, 让我的windows免于它不擅长的移动场景.</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/apple/">apple</category>
      
      <category domain="https://moreality.net/tags/macos/">macos</category>
      
      <category domain="https://moreality.net/tags/%E8%8B%B9%E6%9E%9C/">苹果</category>
      
      
      <comments>https://moreality.net/posts/16771/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>一次致命的失误</title>
      <link>https://moreality.net/posts/64403/</link>
      <guid>https://moreality.net/posts/64403/</guid>
      <pubDate>Sat, 15 Apr 2023 15:36:29 GMT</pubDate>
      
      <description>当你不小心把你的图床全删了</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="一次致命的失误">一次致命的失误</h2><p>今晚清理文件的时候, 不小心用api把图床上所有, 五年来积攒下的全部图片</p><p><strong>清空了</strong></p><div class="group-image-container"><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202304152346191.png" alt="image-20230415233902580"></div></div><div class="group-image-row"><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202304152337629.png" alt="img"></div><div class="group-image-wrap"><img src="https://youpai.roccoshi.top/img/202304152346616.png" alt="image-20230415233847437"></div></div></div><p>只能把所有博客隐藏掉了, 心情很崩溃, 还得去解决一些关键笔记的图片问题…</p><p>以后坚决要把图片至少在两个地方 + 本地备份.</p><p>想死.</p><p><img src="https://youpai.roccoshi.top/img/202304152342053.png" alt="img"></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> os<br><span class="hljs-keyword">import</span> yaml<br><br><span class="hljs-keyword">for</span> root, dirs, files <span class="hljs-keyword">in</span> os.walk(<span class="hljs-string">&quot;.&quot;</span>):<br>    <span class="hljs-keyword">for</span> file <span class="hljs-keyword">in</span> files:<br>        <span class="hljs-keyword">if</span> file.endswith(<span class="hljs-string">&quot;.md&quot;</span>):<br>            filepath = os.path.join(root, file)<br>            <span class="hljs-keyword">with</span> <span class="hljs-built_in">open</span>(filepath, <span class="hljs-string">&quot;r+&quot;</span>, encoding=<span class="hljs-string">&quot;utf-8&quot;</span>) <span class="hljs-keyword">as</span> f:<br>                content = f.read()<br>                <span class="hljs-keyword">if</span> content.startswith(<span class="hljs-string">&quot;---&quot;</span>):<br>                    front_matter, _, content = content[<span class="hljs-number">3</span>:].partition(<span class="hljs-string">&quot;---\n&quot;</span>)<br>                    data = yaml.load(front_matter, Loader=yaml.FullLoader)<br>                    data[<span class="hljs-string">&#x27;hide&#x27;</span>] = <span class="hljs-literal">True</span><br>                    front_matter = yaml.dump(data, sort_keys=<span class="hljs-literal">False</span>, allow_unicode=<span class="hljs-literal">True</span>)<br>                    f.seek(<span class="hljs-number">0</span>)<br>                    f.write(<span class="hljs-string">&quot;---\n&quot;</span> + front_matter.strip() + <span class="hljs-string">&quot;\n---\n&quot;</span> + content)<br>                    f.truncate()<br>                    <span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;<span class="hljs-subst">&#123;filepath&#125;</span> 已经添加了 hide: true 标识&quot;</span>)<br>                <span class="hljs-keyword">else</span>:<br>                    f.seek(<span class="hljs-number">0</span>)<br>                    f.write(<span class="hljs-string">&quot;---\nhide: true\n---\n&quot;</span> + content)<br>                    f.truncate()<br>                    <span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;<span class="hljs-subst">&#123;filepath&#125;</span> 新生成 front-matter&quot;</span>)<br></code></pre></td></tr></table></figure><p>后续:</p><p>后来采用的<a href="https://memos.roccoshi.top/m/23">图床同步方案</a>:</p><p><img src="https://youpai.roccoshi.top/img/202304171958180.png" alt="img"></p><p>现在相当于三端同步, 我就不信还能碰到惨案…</p><p>附同步code:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> os<br><span class="hljs-keyword">import</span> time<br><span class="hljs-keyword">import</span> git<br><span class="hljs-keyword">import</span> upyun<br><span class="hljs-keyword">import</span> wget<br><br><span class="hljs-comment"># 变量设置</span><br>upyun_service =<br>upyun_username = <br>upyun_password =<br>upyun_path = <br><br>github_repo_path = <br>github_username =<br>github_password =<br>github_branch = <br><br><span class="hljs-comment"># 初始化UpYun对象</span><br>up = upyun.UpYun(upyun_service, upyun_username, upyun_password)<br><br><span class="hljs-comment"># 初始化Git仓库</span><br>repo = git.Repo()<br>git = repo.git<br><br><span class="hljs-comment"># 获取Github仓库当前分支到本地</span><br>git.pull(<span class="hljs-string">&quot;origin&quot;</span>, github_branch)<br><br><span class="hljs-comment"># 获取当前时间</span><br>t = time.localtime()<br>current_time = time.strftime(<span class="hljs-string">&quot;%Y-%m-%d %H:%M:%S&quot;</span>, t)<br><br><span class="hljs-comment"># 定义文件列表</span><br>files = []<br><br><span class="hljs-comment"># 同步upyun</span><br><span class="hljs-keyword">for</span> item <span class="hljs-keyword">in</span> up.getlist(upyun_path):<br>    filepath = item[<span class="hljs-string">&quot;name&quot;</span>]<br>    filename = os.path.basename(filepath)<br>    files.append(filename)<br>    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> os.path.exists(<span class="hljs-string">&#x27;./&#x27;</span> + filename):<br>        <span class="hljs-comment"># 下载文件到本地</span><br>        wget.download(<span class="hljs-string">f&quot;https://youpai.roccoshi.top/img/<span class="hljs-subst">&#123;filename&#125;</span>&quot;</span>)<br>        <span class="hljs-comment"># 将文件提交到Github仓库</span><br>        <span class="hljs-built_in">print</span>(<span class="hljs-string">f&quot;\n Committing new file <span class="hljs-subst">&#123;filepath&#125;</span>...&quot;</span>)<br>        git.add(filepath)<br><br>git.commit(<span class="hljs-string">&quot;-m&quot;</span>, <span class="hljs-string">f&quot;sync from upyun at <span class="hljs-subst">&#123;current_time&#125;</span>&quot;</span>)<br>git.push(<span class="hljs-string">&#x27;origin&#x27;</span>, github_branch)<br><span class="hljs-comment"># 推送文件到Github仓库</span><br><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Pushing changes to GitHub...&quot;</span>)<br>git.push(<span class="hljs-string">&quot;origin&quot;</span>, github_branch)<br><br><span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;Backup finished!&quot;</span>)<br></code></pre></td></tr></table></figure>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/what-a-mistake/">what a mistake!</category>
      
      
      <comments>https://moreality.net/posts/64403/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>一个新服务器, 一些折腾</title>
      <link>https://moreality.net/posts/22665/</link>
      <guid>https://moreality.net/posts/22665/</guid>
      <pubDate>Fri, 14 Apr 2023 15:12:15 GMT</pubDate>
      
      <description>对这段时间的折腾做一个小结</description>
      
      
      
      <content:encoded><![CDATA[<p class="note note-info">最近两周在云服务器上部署了一些小插件, 这里对这段时间不多不少的折腾做一个小小的总结, 不会对配置做很详细的介绍, 主要是一些碎碎念.</p><h2 id="1-动机-自建bitwarden服务">1 - 动机: 自建bitwarden服务</h2><p>最初买这台阿里云的服务器的动机其实是出于希望方便的有一个全平台密码管理工具</p><p>在一个下午我登录github的时候, 发现github强制要求2FA认证了</p><p>目前免费的2FA totp authenticator有:</p><ol><li><a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&amp;hl=en&amp;pli=1">Google Authenticator</a></li><li><a href="https://play.google.com/store/apps/details?id=com.azure.authenticator&amp;hl=en_US">Microsoft Authenticator</a></li></ol><p>然而这些authenticator不仅具有不方便的特性, 而且服务器大多位于国外, 通信/备份都具有十足的问题.</p><p>之前我一直再用一个叫<a href="https://bitwarden.com/">Bitwarden</a>的密码管理软件, 其在各大浏览器上都有插件, 并且具有安卓/ios的app适配, 几乎可以在所有设备上无缝使用, 而且代码开源, 具有比较高的安全性, 在我没有自建服务器时就已经几乎把所有的密码管理从google转移到bitwarden了</p><p>然而bitwarden的totp是收费项目:</p><p>但是收费还是比较良心的, 只是我觉得没有必要为了一个feature去额外付几十块/年的费用</p><p>在很早之前调研密码管理工具的时候就了解到bitwarden可以自建服务器, 并且有一个开源的rust后端实现<a href="https://community.bitwarden.com/t/bitwarden-vs-vaultwarden-rust-formerly-bitwarden-rs/15382">Bitwarden_RS</a> (后面改名为<a href="https://github.com/dani-garcia/vaultwarden">vaultwarden</a>):</p><blockquote><p>This is the Rust implementation backend, <em>not</em> the official server backend, but fully compatible with the Client apps.</p><p>Bitwarden is a self-hosted password manager. It allows you to store and manage your passwords, credit cards, and other private information in a secure way while still allowing you to access it from your browser, phone, or desktop.</p></blockquote><p>vaultwarden实现了与bitwarden完全兼容的绝大多数api, 可以直接作为bitwarden客户端的上游, 支持所有的高级会员的功能, 同时具有多方面的优化, 并且<a href="https://rs.ppgg.in/">更加的轻量</a>:</p><blockquote><p>Vaultwarden 比官方版更轻量。官方版使用 .Net 开发，使用 MSSQL 数据库，要求至少 2GB 内存；Vaultwarden 使用 Rust 编写，改用 SQLite 数据库（现在也支持 MySQL 和 PostgreSQL），运行时只需要 10M 内存，可以说对硬件基本没有要求。Vaultwarden 比官方版更轻量。官方版使用 .Net 开发，使用 MSSQL 数据库，要求至少 2GB 内存；Vaultwarden 使用 Rust 编写，改用 SQLite 数据库（现在也支持 MySQL 和 PostgreSQL），运行时只需要 10M 内存，可以说对硬件基本没有要求。</p></blockquote><p>于是乎我直接开始搜寻一个轻量的服务器来部署vaultwarden服务:</p><ol><li>腾讯云国内: 老用户与狗不得入内系列</li><li>阿里云国内: 同上</li><li>国内其他云服务厂商: 不太放心, 并且价格也不便宜, 且几乎所有国内服务器都是小水管, 干不了什么东西</li><li>vultr / 搬瓦工等国外服务器: 延迟过高</li></ol><p>于是最后把眼光瞄准了阿里云轻量海外/腾讯云轻量海外</p><p>最后入手了阿里云的<a href="https://common-buy.aliyun.com/">新加坡轻量服务器</a></p><p>价格还是比较良心, 同时30Mbps带宽也基本够用, 测了一下延迟大概70ms左右, 对比其他的海外服务器已经是绝佳的表现了</p><p>海外服务器在延迟低的情况下优势是很明显的:</p><ol><li>各种包管理不用配一大堆镜像</li><li>github可以直接访问, 拉代码很方便, 国内经常抽风</li><li>在把阿里云盾删除的情况下, 可以干点你懂得</li></ol><p>同时这次服务器部署希望全采用docker的方式, 这样迁移非常方便, 然后用rclone备份数据卷到google drive / one drive即可, 下面简单介绍下这一周来折腾的一些东西(2G内存还是能干很多事的!):</p><h2 id="2-基础建设-反代-caddy">2 - 基础建设(反代): caddy</h2><p>caddy相比nginx除了配置方便外很吸引我的一个点就是: <strong>自动部署证书</strong></p><p>实际使用下来真是太方便了, 配置一个简单的反向代理 + https只需要:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs shell">&#123;$DOMAIN&#125; &#123;<br>  tls &#123;$EMAIL&#125;<br>  reverse_proxy localhost:&#123;$PORT&#125;<br>&#125;<br></code></pre></td></tr></table></figure><p>就搞定了, 结合docker-compose:</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs yml"><span class="hljs-attr">version:</span> <span class="hljs-string">&#x27;3&#x27;</span><br><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">caddy:</span><br>    <span class="hljs-attr">image:</span> <span class="hljs-string">caddy:2</span><br>    <span class="hljs-attr">container_name:</span> <span class="hljs-string">caddy</span><br>    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span><br>    <span class="hljs-attr">network_mode:</span> <span class="hljs-string">host</span><br>    <span class="hljs-attr">volumes:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./Caddyfile:/etc/caddy/Caddyfile:ro</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./caddy-config:/config</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./caddy-data:/data</span><br>    <span class="hljs-attr">environment:</span><br>      <span class="hljs-attr">DOMAIN:</span> <span class="hljs-string">&quot;https://helloworld.com&quot;</span> <span class="hljs-string">//</span> <span class="hljs-string">域名，以</span> <span class="hljs-string">http</span> <span class="hljs-string">或</span> <span class="hljs-string">https</span> <span class="hljs-string">作为前缀</span><br>      <span class="hljs-attr">PORT:</span> <span class="hljs-string">&quot;80&quot;</span><br>  <span class="hljs-attr">EMAIL:</span> <span class="hljs-string">&quot;whatever@helloworld.com&quot;</span><br></code></pre></td></tr></table></figure><p>原理就是自动用你的$EMAIL进行acme的自动续签(基于let’s encrypt)</p><p>之后如果需要使用证书的话直接到<code>./caddy-data</code>中找就好了</p><h2 id="3-vaultwarden">3 - vaultwarden</h2><p>在搞完基础建设后直接开始部署vaultwarden</p><p>其实单纯配置vaultwarden非常的简单(直接上docker-compose):</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs yml"><span class="hljs-attr">version:</span> <span class="hljs-string">&#x27;3&#x27;</span><br><br><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">vaultwarden:</span><br>    <span class="hljs-attr">image:</span> <span class="hljs-string">vaultwarden/server:latest</span><br>    <span class="hljs-attr">container_name:</span> <span class="hljs-string">vaultwarden</span><br>    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span><br>    <span class="hljs-attr">ports:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-number">80</span><span class="hljs-string">:80</span> <br>      <span class="hljs-bullet">-</span> <span class="hljs-number">3012</span><span class="hljs-string">:3012</span><br>    <span class="hljs-attr">environment:</span><br>      <span class="hljs-attr">WEBSOCKET_ENABLED:</span><br>      <span class="hljs-attr">ADMIN_TOKEN:</span> <br>      <span class="hljs-attr">SIGNUPS_ALLOWED:</span> <br>      <span class="hljs-attr">SMTP_HOST:</span> <br>      <span class="hljs-attr">SMTP_FROM:</span> <br>      <span class="hljs-attr">SMTP_PORT:</span> <br>      <span class="hljs-attr">SMTP_SECURITY:</span><br>      <span class="hljs-attr">SMTP_USERNAME:</span> <br>      <span class="hljs-attr">SMTP_PASSWORD:</span> <br>      <span class="hljs-attr">DOMAIN:</span> <br>    <span class="hljs-attr">volumes:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">vaultwarden-data:/data</span><br></code></pre></td></tr></table></figure><p>然后直接在各个bitwarden的插件里填上服务器地址就可以用了</p><p>然而这样一个密码管理服务数据备份是相当重要的, 我直接采用了一个项目的备份方案:</p><p><a href="https://github.com/ttionya/vaultwarden-backup">https://github.com/ttionya/vaultwarden-backup</a></p><p>我自己在配置的是保留5天的数据, 并且中途多次尝试从备份中恢复, 还是相当稳的:</p><p>在搭建完vaultwarden后我发现我1GB(后面升级到2GB了)的服务器还剩下大几百兆, 如果我只用来做一个密码管理那不是浪费钱吗, 必经一年也要好几百, 那还不如直接去买bitwarden的服务, 后面越想越气, 就开始发觉服务器其他的用途:</p><h2 id="4-nps">4 - nps</h2><p>一个内网穿透服务</p><blockquote><p>一款轻量级、高性能、功能强大的内网穿透代理服务器。支持tcp、udp、socks5、http等几乎所有流量转发，可用来访问内网网站、本地支付接口调试、ssh访问、远程桌面，内网dns解析、内网socks5代理等等……，并带有功能强大的web管理端。a lightweight, high-performance, powerful intranet penetration proxy server, with a powerful web management terminal.</p></blockquote><p>nps比frp好的一点就是提供了非常方便的web ui, 不需要登录服务器搞配置了</p><p>docker: <a href="https://hub.docker.com/r/ffdfgdfg/nps">https://hub.docker.com/r/ffdfgdfg/nps</a></p><p>基本写一个这样的compose:</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs yml"><span class="hljs-attr">version:</span> <span class="hljs-string">&#x27;3&#x27;</span><br><br><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">nps:</span> <br>    <span class="hljs-attr">image:</span> <span class="hljs-string">&quot;ffdfgdfg/nps&quot;</span><br>    <span class="hljs-attr">container_name:</span> <span class="hljs-string">nps</span><br>    <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span><br>    <span class="hljs-attr">network_mode:</span> <span class="hljs-string">&quot;host&quot;</span><br>    <span class="hljs-attr">volumes:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-string">./conf:/conf</span><br></code></pre></td></tr></table></figure><p>然后复制config: <a href="https://github.com/ehang-io/nps/tree/master/conf">https://github.com/ehang-io/nps/tree/master/conf</a></p><p>就可用了.</p><p>感觉nps的用处还是很大的, 只是现在我校园网基本全是内网, 用不太上就闲置了, 之后在家里想连接实验室的服务器之类的还是很有用的 (遵纪守法)</p><h2 id="5-chatgpt">5 - chatgpt</h2><p>那自然是要跟随潮流(:</p><p>几个典型的项目:</p><ol><li><a href="https://github.com/binary-husky/chatgpt_academic">https://github.com/binary-husky/chatgpt_academic</a></li><li><a href="https://github.com/Chanzhaoyu/chatgpt-web">https://github.com/Chanzhaoyu/chatgpt-web</a></li><li>…</li></ol><p>这里基本都不需要任何配置, 填个api-key开箱即用, 并且内存占用都很小</p><p>这里就不得不说新加坡节点的另一个好处了:</p><p><strong>服务器IP几乎不会被openai ban掉, 同时新加坡又不属于管制范围</strong>, 总之就是不用开代理畅享API</p><h2 id="6-proxy">6 - proxy</h2><p>这是能说的吗</p><p>反正搭了三天还没被封, 后面再看</p><h2 id="7-rss-bot">7 - rss-bot</h2><p>这几天高强度看别人的博客, 又想部署一个简单的rss通知服务, 之前一直用的feedly, 但是不知道为什么总是挂图片/样式很烂/收不到通知</p><p>于是这次直接顺手部署了一个rss的bot:</p><p><a href="https://github.com/Rongronggg9/RSS-to-Telegram-Bot">https://github.com/Rongronggg9/RSS-to-Telegram-Bot</a></p><p>直接推送rss到tg, 并且还可以自动渲染, 还是挺方便的</p><h2 id="8-总结">8 - 总结</h2><p>这几天在服务器上的折腾大概就总结成了这几点, 期间服务器经过了一次从1G到2G的升级, 多次重启, 但是用docker的好处就是, 一切服务器重启几乎都是无感的(记得配置<code>restart: always</code>或<code>restart: unless-stopped</code>)</p><p>然后同时利用tmux, 运行着<code>docker compose logs -f</code>监控每一个服务, 基本登录服务器看日志可以做到一键化</p><p>然后让chatgpt帮我写了个一键备份的脚本:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_">#</span><span class="language-bash">!/bin/bash</span><br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">定义变量</span><br>CURRENT_DIR=<br>BACKUP_DIR=     # 需要备份的目录<br>RCLONE_REMOTE=  # rclone远程服务名<br>DAYS_TO_KEEP=3                               # 保留备份文件的天数<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">生成备份文件名</span><br>BACKUP_FILE=docker-$(date +%Y-%m-%d).zip<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">打包备份文件</span><br>echo &quot;开始打包备份文件...&quot;<br>cd $BACKUP_DIR &amp;&amp; zip -r $CURRENT_DIR/$BACKUP_FILE .<br>if [ $? -eq 0 ]; then<br>    echo &quot;备份文件打包成功！&quot;<br>else<br>    echo &quot;备份文件打包失败，请检查备份目录是否存在！&quot;<br>    exit 1<br>fi<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">上传备份文件到rclone远程服务</span><br>echo &quot;开始上传备份文件到rclone远程服务(onedrive)...&quot;<br>rclone copy $CURRENT_DIR/$BACKUP_FILE $RCLONE_REMOTE &gt; /dev/null 2&gt;&amp;1<br>if [ $? -eq 0 ]; then<br>    echo &quot;备份文件上传成功！&quot;<br>else<br>    echo &quot;备份文件上传失败，请检查rclone配置是否正确！&quot;<br>    exit 1<br>fi<br><span class="hljs-meta prompt_"></span><br><span class="hljs-meta prompt_"># </span><span class="language-bash">删除历史备份文件</span><br>FILE_NAME=docker-$(date -d &quot;$DAYS_TO_KEEP days ago&quot; +&quot;%Y-%m-%d&quot;).zip<br>echo &quot;开始删除历史备份文件(onedrive): $FILE_NAME&quot;<br>if rclone ls $RCLONE_REMOTE/$FILE_NAME &gt; /dev/null 2&gt;&amp;1; then<br>    rclone delete $RCLONE_REMOTE/$FILE_NAME &gt; /dev/null 2&gt;&amp;1<br>    if [ $? -eq 0 ]; then<br>        echo &quot;历史备份文件删除成功！&quot;<br>    else<br>        echo &quot;历史备份文件删除失败，请检查rclone配置是否正确！&quot;<br>        exit 1<br>    fi<br>else<br>    echo &quot;历史备份文件不存在！&quot;<br>fi<br><br><br>echo &quot;备份完成！&quot;<br>exit 0<br></code></pre></td></tr></table></figure><p>整体感觉就是: 服务器非常的清爽, 几乎在不用快照的情况下也可以做到快速迁移</p><p>目前内存还剩将近1个G, 感觉还可以折腾进好几个服务…</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E9%85%8D%E7%BD%AE%E7%8E%AF%E5%A2%83/">配置环境</category>
      
      
      <category domain="https://moreality.net/tags/blog/">blog</category>
      
      <category domain="https://moreality.net/tags/proxy/">proxy</category>
      
      <category domain="https://moreality.net/tags/vaultwarden/">vaultwarden</category>
      
      <category domain="https://moreality.net/tags/password/">password</category>
      
      <category domain="https://moreality.net/tags/nps/">nps</category>
      
      <category domain="https://moreality.net/tags/chatgpt/">chatgpt</category>
      
      <category domain="https://moreality.net/tags/caddy/">caddy</category>
      
      
      <comments>https://moreality.net/posts/22665/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>多核并行体系结构-投机同步</title>
      <link>https://moreality.net/posts/40367/</link>
      <guid>https://moreality.net/posts/40367/</guid>
      <pubDate>Mon, 12 Dec 2022 08:11:10 GMT</pubDate>
      
      <description>来源于一篇2002年asplos的论文, 题目是投机同步: 将线程级投机应用到并行程序</description>
      
      
      
      <content:encoded><![CDATA[<div class="note note-primary">            <p>文中内容来源于我这学期某门课程的presentation, 这里把ppt和讲稿稍作修改发出来留存下~</p><p>内容主要是包括这篇论文:</p><p><a href="https://dl.acm.org/doi/abs/10.1145/635508.605400">论文: Speculative synchronization: applying thread-level speculation to explicitly parallel applications</a></p>          </div><h2 id="为什么需要投机同步">为什么需要投机同步</h2><p>同步屏障(Barrier), 锁(Lock)和标志(Flag)是程序员编写并行程序广泛使用的几类操作, 主要目的是使并行线程之间不发生竞争.</p><p>简单回顾一下屏障, 锁和标志位的含义:</p><ul><li><p>锁相信大家都很清楚了, 这里主要回顾一下屏障和标志</p></li><li><p>同步屏障是多对多的同步, 在所有线程到达屏障之前, 任何先到达屏障的线程都必须等待, 为了实现屏障, 需要一个临界区来实现对线程数量的统计. <a href="https://zh.m.wikipedia.org/zh-hans/%E5%90%8C%E6%AD%A5%E5%B1%8F%E9%9A%9C">同步屏障 wiki</a></p></li><li><p>而标志是指一对多的同步, 消费者接收到生产者对flag的置位之前将保持等待, 由于只有一个生产者, 故读写flag并不需要临界区的支持</p></li></ul><p>大多数程序员为了并行程序的稳定性和安全性, 在同步操作的时候会显得过于保守, 即使有些竞争可能并不发生, 或者发生的概率非常小, 程序员也会进行加锁或者是设置屏障.</p><p>典型的场景:</p><ul><li>难以分析的指针访问</li><li>概率很小的哈希碰撞</li></ul><p>保守的同步策略将带来较大的性能损失, 因此我们希望引入一种硬件设计, 能让线程投机地跨越锁, 标志和屏障, 来继续执行之后的代码, 如果发生冲突, 再将线程回滚.</p><h2 id="借鉴线程级投机的思想">借鉴线程级投机的思想</h2><p>线程级投机主要通过提取串行程序中的并行性来进行投机, 其主要的思路是通过对线程进行排序, 一个线程在保证与前序线程无冲突的情况下提交该线程的状态.</p><p>投机同步借鉴了很多在线程级投机的思想, 比如使用每个处理器的私有缓存保存投机数据, 始终维持安全线程的执行, 跟踪检测访存冲突和回滚等.</p><p>在线程级投机中, 安全线程的定义是序号最小的线程(即最早开始执行的线程), 而在投机同步中, 不对线程进行排序, 对安全线程的定义如下</p><ul><li>对于锁而言, 安全线程是锁的持有者</li><li>对于屏障而言, 安全线程是未到达屏障的线程</li><li>对于标志而言, 安全线程是标志的置位者</li></ul><p>同时提出了一种叫投机同步单元(简称SSU)的硬件, 用于从处理器卸载对一个同步变量的操作, 以让线程投机的执行下去.</p><h2 id="文中方案一些特性">文中方案一些特性</h2><blockquote><p>投机同步的核心思路其实在于希望用尽量简单的硬件方案实现较大的性能提升</p></blockquote><ul><li>简化的硬件设计方案: 和TLS不同<ul><li>投机同步不需要保证线程的顺序</li><li>没有多版本的支持, 不仅检测RAW(写后读)冲突, 对于名称冲突WAW(写后写), WAR(读后写)等冲突也直接回滚投机线程</li><li>没有伪共享的支持, 硬件设计都是对一个缓存行(cache line)的操作</li><li>对于不同的同步方式具有统一的硬件支持</li></ul></li><li>类似于OpenMP, 文中利用m4宏设计了一种高层的抽象, 能够让程序员像使用常规同步一样使用投机同步</li><li>和传统同步方案保证兼容</li></ul><p>右图展示了常规同步的m4宏和投机同步的m4宏的区别, 这个在后文将详细说明, 这里可以看到加粗的部分是SSU提供的接口</p><blockquote><p>m4: 是一个通用的宏处理器, 是POSIX标准下的一部分</p><p>OpenMP利用pragma预处理指令, 实现并行接口的抽象</p></blockquote><h2 id="投机锁的例子">投机锁的例子</h2><blockquote><p>首先讲一讲投机锁和投机屏障的例子, 用图先给大家一个概念, 然后我们再详细的介绍投机同步的硬件结构以及一些实现细节</p><p>这时我们可以先不去思考如何进行冲突检测, 投机锁的获取和释放这些问题, 先简单的理解一下这个方案的思路</p></blockquote><p>在文章的剩余部分都将用黑色圆圈表示安全线程, 用红色圆圈表示投机线程, 同时用横杠表示同步点</p><p>首先介绍一下投机锁的例子, 图中的 Acquire 和 Release 分别表示锁的获取和释放。</p><ul><li><p>首先是所有线程都未进入临界区, 均为安全线程</p></li><li><p>然后线程A首先获得锁并成为安全线程, 进入临界区</p></li><li><p>之后线程B、E投机地进入临界区, 由于线程A已经获取了锁, 所以B, E是投机线程</p></li><li><p>由于线程C、D尚未进入临界区，故C、D也是安全的</p></li><li><p>然后线程E首先退出临界区，但此时线程E仍然可能和未退出临界区的安全线程A发生冲突，故在安全线程A退出临界区之前，线程E不能提交。</p></li><li><p>当安全线程A退出临界区时，就可以将投机线程E提交了</p></li><li><p>同时由于A释放锁, 投机线程B和C将争夺锁, 假设投机线程B得到锁，将由投机状态变为安全状态</p></li></ul><p>这个大概就是整个投机锁的全过程</p><h2 id="投机屏障的例子">投机屏障的例子</h2><p>下面是投机屏障的例子，由于有了投机锁的概念，投机屏障相对容易理解</p><ul><li><p>首先当所有线程都未到达屏障的时，所有线程都处于安全状态</p></li><li><p>之后线程C投机地跨过屏障</p></li><li><p>之后线程A投机地跨过屏障</p></li><li><p>只有当最后一个线程到达屏障时，才可以提交所有的投机线程</p></li></ul><h2 id="SSU硬件结构">SSU硬件结构</h2><blockquote><p>介绍完了两个例子后, 我们来说说它们的硬件是如何设计和实现的</p></blockquote><p>实现投机同步需要在常规体系结构上引入一个叫做投机同步单元 (Speculative Synchronization Unit, SSU)的模块</p><p>SSU存在于共享内存的多处理器的每个处理器私有的缓存结构上，一般就是L1 Cache和L2 Cache</p><p>SSU的作用是从处理器上卸载对一个同步变量的操作，让处理器可以向前投机执行代码</p><p>SSU的组成包括:</p><ol><li><p>扩展的缓存控制器</p></li><li><p>在L1 cache上增加一个额外的缓存行, 该缓存行只能由ssu写入, 其他来自本地和远程的请求可以访问, 主要功能为:</p><ol><li><p>保存同步变量</p></li><li><p>增加状态位&quot;Acquire&quot;和&quot;Release&quot;, 当ssu获取到一个来自cpu的同步变量时, 这两个位将被置位</p><p>（当投机线程退出临界区时，Release位将被清除。当SSU空闲时(即ssu释放了同步变量时)，Acquire位将被清除。）</p></li></ol></li><li><p>为每一个缓存行(L1 + L2)增加一位&quot;投机位(Speculative bit)&quot;, 标志该缓存行是否是正在被投机写入或者读取</p></li></ol><p>对于32KB的L1 Cache和 1MB的L2 Cache， SSU大约需要一共2KB的存储空间。</p><h2 id="SSU-投机锁请求">SSU-投机锁请求</h2><blockquote><p>下面介绍一下在发生投机同步的时候, SSU是如何工作的</p></blockquote><p>首先是投机的锁请求</p><p>该行为发生在已经有一个进入临界区的安全线程, 然后另一个线程投机的进入临界区的时刻</p><p>文中使用的锁获取原语是T&amp;T&amp;S （test and test and set)，右图展示了使用该原语进行锁获取的汇编代码, 也完全可以换成其他的原语。</p><p>当处理器执行到锁请求时，我们分CPU侧和SSU侧的行为进行讨论</p><p>首先是CPU侧：</p><ul><li>CPU首先将锁的地址发送给SSU</li><li>然后保存当前寄存器的状态 (checkpoint)</li><li>然后继续投机地进入临界区，执行临界区的代码</li></ul><p>然后是SSU侧：</p><ul><li>SSU首先将Acquire位和Release位置位</li><li>将CPU提供的锁变量提取到额外的缓存行(L1 Cache) 中</li><li>然后SSU开启Test循环自旋，不断地尝试获取锁</li><li>一旦Acquire位被置位，将CPU投机访存的部分置投机位</li></ul><p>这里需要注意的是，为了保证缓存行的安全副本，在第一次投机访问缓存脏行的时候，必须将该缓存行写回主存。</p><h2 id="SSU-投机锁获取">SSU-投机锁获取</h2><p>投机锁获取的行为发生在一个安全线程退出临界区，SSU获取到锁变量并成为安全线程的时候。</p><p>在此之前，SSU一直在锁变量上自旋，当发现锁变量被释放时，SSU检查当前的Release位的状态，如果是置位状态，则认为当前投机线程仍然在临界区执行。</p><p>此时SSU将通过T&amp;S获取到锁, 然后将清除缓存行上所有的投机位，提交缓存值，该线程成为安全线程，并且SSU变为空闲状态。</p><p>之后的锁变量的释放操作就是常规的同步操作，由CPU完成。</p><h2 id="SSU-投机锁释放">SSU-投机锁释放</h2><p>投机锁释放的行为发生在投机线程先于安全线程退出临界区，然后才是安全线程退出临界区之后的时候。</p><p>此时SSU的Release位应该是清空的状态，表示投机线程已经退出了临界区</p><p>这时与投机锁获取的不同之处在于, 线程将直接跳过通过T&amp;S锁获取的操作, 直接提交投机线程, SSU变为空闲状态</p><h2 id="SSU-冲突处理和回滚">SSU-冲突处理和回滚</h2><blockquote><p>底层的缓存一致性协议会检测访问冲突, 比如收到外部的invalid或者对脏行的read时, 故文中主要讨论SSU如何处理这些冲突</p></blockquote><ul><li>我们将未标记投机位的缓存行称为安全行, 将标记了投机位的缓存行称为投机行</li><li>当安全行接受到外部消息时, 就按照缓存一致性协议正常处理</li><li>当投机行接受到外部消息时:<ul><li>发出者可能是投机线程, 也可能是安全线程, 无论如何, 我们都将直接回滚接收线程 (由于设计上并没有为线程定义任何的顺序, 故所有对投机行的一致性消息都认为是冲突)</li><li>回滚的策略具体按如下步骤:<ul><li>首先将所有被投机位标记的脏行重新失效化处理, ( 如果缓存行不脏的话就无需失效缓存行)</li><li>清除所有的投机行的投机位</li><li>SSU强制让CPU恢复寄存器的检查点状态</li></ul></li></ul></li></ul><p>这样就可以让线程快速回滚到进入临界区前的状态</p><h2 id="SSU-缓存溢出">SSU-缓存溢出</h2><blockquote><p>当缓存不够需要发生缓存行替换时</p></blockquote><p>投机行永远不会被换出到主存</p><ul><li>因为首先不论是投机读还是投机写, 投机行都记录了过去的访问历史, 将用于可能的冲突检测</li><li>如果是投机写(标记为脏)则更不能替换, 因为数据本身都不安全了</li></ul><p>所以替换时首先会选择安全行进行替换</p><p>如果找不到安全行替换, 则该投机线程将暂停, 直到该线程成为安全线程, 即SSU拿到锁, 或者是发生冲突该线程回滚</p><p>由于安全线程没有投机行, 因此缓存未命中时, 直接按照传统的缓存替换策略进行替换即可</p><h2 id="对多重锁和嵌套锁的支持">对多重锁和嵌套锁的支持</h2><p>在投机执行的时候, 可能会碰到两种情况</p><ul><li>第一种是投机线程已经执行完上一个临界区, 并即将进入下一个临界区, 而安全线程还停留在上一个临界区的情况, 即多重锁</li><li>第二种是投机线程未退出临界区, 但是在临界区内部将获取另一把锁的情况, 即嵌套锁</li></ul><p>文中首先提供了一个程序接口, 可以暴露SSU的状态, 这时程序员可以选择自旋等待SSU变为空闲来手动处理这种情况</p><p>如果采用第一种策略, 程序的性能不能得到完全的发挥, 另一种策略, 我们可以分情况讨论:</p><ul><li>如果获取的是不同的锁, 我们可以简单的将第二把锁视为投机的变量, 因为对锁的操作本质上也是读写操作, 然后可以用完全一致的冲突处理策略来进行冲突检测, 如果出现其他线程对锁变量的获取操作, 那么直接回滚该投机线程</li><li>如果获取的是同一把锁, 则不能与前一个方式一致, 这时又可以分为两种情况:<ul><li>如果投机线程已经退出了之前的临界区, 则直接合并两个临界区, 意味着Release位在第一个临界区退出时被清除, 然后在第二个临界区又被置位, 但是CPU无需为第二个临界区设置检查点, 如果发生冲突就直接回滚到第一个检查点上</li><li>如果投机线程还没有退出之前的临界区, 这时投机线程只能等待其变为安全线程, 然后继续执行, 或者是等待本线程发生冲突并且回滚</li></ul></li></ul><h2 id="投机标志">投机标志</h2><blockquote><p>下面介绍一下投机标志和投机屏障, 投机标志可以直接由投机锁衍生而来, 而投机屏障又可以由投机标志衍生而来</p></blockquote><p>观察左图投机标志的情景可以看到其和&quot;投机锁释放&quot;的情形十分相似，仅仅是将锁变量改变成了Flag</p><p>二者的共同之处在于都只需要在一个变量上进行自旋，当该变量达到某个值后（对于锁而言是检测到了cpu释放锁，对于flag而言是检测到了flag被设置为pass值），直接将投机线程提交。</p><h2 id="投机屏障">投机屏障</h2><p>由于同步屏障的本质就是锁 + 标志</p><p>故投机屏障可以由传统锁 + 投机标志实现</p><p>文中讨论的屏障实现方式是最常见的翻转感应集中式屏障 (这里不具体介绍各种屏障的实现, 可以参考《并行多核计算机体系结构基础》)（比如为什么代码中的<code>local_f</code>不改为一个特定的值, 而是采用翻转 -&gt; 特定的值会导致死锁）</p><blockquote><p>补充: 翻转感应集中式栅障</p></blockquote><p>在屏障的临界区中需要让SSU空闲，因为对锁的获取和对count的更新必然导致投机线程发生冲突</p><p>所以这里的实现需要显式暴露SSU的状态，这个在后文的代码实现中会讲到</p><h2 id="对软件接口-宏-的介绍">对软件接口(宏)的介绍</h2><p>类似于OpenMP, 文中利用m4宏设计了一种高层的抽象, 能够让程序员像使用常规同步一样使用投机同步</p><p>首先是<code>ssu_lock</code>的实现, 该实现对应投机锁的实现, 传入锁地址, 然后如果返回值为1表示ssu接受请求, 为0表示该请求失败, 在对应的宏实现中, 当ssu拒绝请求时, 该加锁即以常规的锁实现</p><p>然后是<code>ssu_spin(addr, value)</code>其中addr表示flag的地址, 而value表示对应的pass值, 该操作对应投机标志的实现</p><p>然后是<code>ssu_idle()</code>的实现, 在上一页ppt中我们提到需要显式暴露ssu的状态, 才能让程序在投机屏障的临界区中避免必然失败的投机, ssu_idle将提供这个接口, 该操作可以暴露ssu的当前状态, 对应的<code>SS_EXPOSE</code>宏实现表示自旋等待ssu的空闲, 然后下面的投机屏障中在进入临界区之前使用了这个SS_EXPOSE宏, 表示程序将等待SSU结束投机才进入对count变量更新的临界区</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84/">体系结构</category>
      
      
      <category domain="https://moreality.net/tags/%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84/">体系结构</category>
      
      <category domain="https://moreality.net/tags/%E5%B9%B6%E8%A1%8C/">并行</category>
      
      <category domain="https://moreality.net/tags/%E5%90%8C%E6%AD%A5/">同步</category>
      
      <category domain="https://moreality.net/tags/%E6%8A%95%E6%9C%BA/">投机</category>
      
      <category domain="https://moreality.net/tags/synchronization/">synchronization</category>
      
      <category domain="https://moreality.net/tags/speculative/">speculative</category>
      
      
      <comments>https://moreality.net/posts/40367/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>多核并行体系结构-缓存一致性</title>
      <link>https://moreality.net/posts/26194/</link>
      <guid>https://moreality.net/posts/26194/</guid>
      <pubDate>Tue, 01 Nov 2022 01:53:27 GMT</pubDate>
      
      <description>基于多核并行体系结构下的缓存一致性简单梳理总结, 总结了常见的缓存写策略, 同时对MSI协议, MESI协议, MOESI协议等缓存一致性协议作了总结和梳理</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="缓存一致性简单介绍">缓存一致性简单介绍</h2><p>缓存一致性的设计目标: <strong>保证同一个数据在每个处理器中的私有缓存副本是相同的</strong></p><p>基本的策略及关联性:</p><p>对于缓存的读, 不需要提供什么额外的一致性策略, 仅仅区分读命中和读不命中就好了, 而读不命中的策略即包含在了写策略中(需要对缓存进行更新)</p><p>缓存一致性策略讨论的主要是如何实现写传播策略和发送一致性消息使得各个缓存保持一致性</p><h3 id="写策略">写策略</h3><h3 id="发送一致性消息">发送一致性消息</h3><p>在各个缓存一致性协议的设计中, 均需要发送一致性消息让其他缓存感知到变化, 一致性消息的发送可以分为发送到所有缓存和发送到特定的缓存.</p><p>发送到所有缓存的,称为<strong>广播/侦听式协议</strong></p><p>发送到特定缓存的, 称为<strong>目录式协议</strong></p><h3 id="基本的多处理器单元互联方式">基本的多处理器单元互联方式</h3><p>基本的互联方式包含如下三种:</p><ul><li><p>共享缓存</p></li><li><p>对称多处理器</p></li><li><p>分布式共享存储</p></li><li><p>共享缓存中不需要缓存一致性的支持, 但是对应的代价是处理器和缓存必须部署在非常近的距离之内, 并且互联网络必须提供很高的带宽</p></li><li><p>在实际应用中, 不同的层次可以由不同的组织方式互联, 比如一个「多个多核芯片」的系统, 多核芯片的内部可能的L2和L3缓存层可以采用共享缓存或者对称多处理器方式, 而在每个芯片之间可以采用DSM结构互联</p></li></ul><h3 id="总线的一些概念">总线的一些概念</h3><p>从逻辑上分类, 总线有三种类型:</p><ul><li>命令总线</li><li>数据总线</li><li>地址总线</li></ul><p>从抽象层次看, 总线就是将所有处理器连接在一起的一组连线, 一个处理器与其他处理器通信时, 它先将地址, 命令和数据放在总线上, 然后其他处理器必须<strong>侦听</strong>总线, 检查是否传输了与之相关的数据.</p><h4 id="总线事务的三个阶段">总线事务的三个阶段</h4><p>每个总线事务都需要经历三个阶段:</p><ul><li>第一个阶段称为**“仲裁”<strong>, 这个阶段用来选择并授权一个端口使用总线, 总线仲裁的作用是</strong>避免来自不同处理器的请求在总线上发生碰撞**</li><li>第二个阶段称为**“命令传输”**, 是请求端口收到一个总线授权信号时, 其可以将目标地址放在地址总线上</li><li>第三个阶段称为**“数据传输”**, 是对第二个阶段的目标地址进行操作</li></ul><h4 id="扩展的总线一致性控制器">扩展的总线一致性控制器</h4><p>与单处理器系统中的总线和缓存相比, 基于总线的多处理器系统扩展了总线事务和缓存状态.</p><p>如图所示, 和单处理器不同之处在于:</p><ul><li>每个缓存块的标签阵列部分增加了额外的位, 用于表示新的缓存状态</li><li><strong>一致性控制器</strong>被添加到 「处理器侧 」和「 存储器侧」<ul><li><strong>未决事务表</strong>记录者当前未完成的总线事务</li><li><strong>总线侦听器</strong>侦听每一个总线事务, 当侦听到总线事务时, 检索缓存的标签阵列, 查找是否有数据块与该总线事务有关, 然后进行输出数据块或者更改数据块状态等操作</li></ul></li></ul><h2 id="基于总线的多处理器缓存一致性问题">基于总线的多处理器缓存一致性问题</h2><h3 id="1-写直达的缓存一致性协议">1 | 写直达的缓存一致性协议</h3><p>该方式为最简单的缓存一致性协议, 基于 <strong>写直达 + 写无效 + 写不分配</strong> 缓存构建</p><p>在此方案中, 缓存块状态包括:</p><table><thead><tr><th>缓存块状态</th><th>解释</th></tr></thead><tbody><tr><td><code>Valid (V)</code></td><td>缓存块有效且干净 (与主存相同)</td></tr><tr><td><code>Invalid (I)</code></td><td>缓存块无效</td></tr></tbody></table><p>由于在写直达中, 所有的写操作都是直接写入主存, 所以缓存块并没有&quot;脏状态&quot;</p><p>这里引入几个操作</p><p>处理器的操作:</p><table><thead><tr><th>处理器的操作</th><th>说明</th></tr></thead><tbody><tr><td>PrRd</td><td>处理器读缓存块</td></tr><tr><td>PrWr</td><td>处理器写缓存块</td></tr></tbody></table><p>总线侦听的操作:</p><table><thead><tr><th>总线侦听的操作</th><th>说明</th></tr></thead><tbody><tr><td>BusRd</td><td>侦听到其他处理器 (主存 -&gt; cache) 事件</td></tr><tr><td>BudWr</td><td>侦听到其他处理器 (处理器 -&gt; 主存) 事件</td></tr></tbody></table><p>其中左边表示<strong>处理器的请求 / 产生的总线侦听事件</strong>, 右边表示<strong>处理器响应总线侦听事件 / 产生的总线侦听事件</strong></p><h4 id="缺点">缺点</h4><ul><li>写直达缓存中, 每次写缓存都会触发<code>BusWr</code>从而占用总线带宽, 因为对缓存块的写入存在时间和空间的局部性, 在写回缓存如果出现多次缓存写入只需要占用一次总线带宽使其他缓存失效即可</li></ul><h3 id="2-写回的MSI协议">2 | 写回的MSI协议</h3><p>使用写回相比写直达的优势就是会大幅度减少总线的带宽开销</p><p>之后的部分总结内容均与上面类似, 首先是缓存块的状态包含:</p><table><thead><tr><th>缓存块状态</th><th>解释</th></tr></thead><tbody><tr><td><code>Modified (M)</code></td><td>缓存块有效且(可能)和主存中数据不同, 并且该处理器有独占权</td></tr><tr><td><code>Shared (S)</code></td><td>缓存块有效且与其他处理器共享, 并且是干净的(和主存一致)</td></tr><tr><td><code>Invalid (I)</code></td><td>缓存块无效</td></tr></tbody></table><table><thead><tr><th>处理器的操作</th><th>说明</th></tr></thead><tbody><tr><td>PrRd</td><td>处理器读缓存块</td></tr><tr><td>PrWr</td><td>处理器写缓存块</td></tr></tbody></table><p>总线侦听的操作:</p><table><thead><tr><th>总线侦听的操作</th><th>说明</th></tr></thead><tbody><tr><td>BusRd</td><td>侦听到其他处理器 (主存 -&gt; cache) 事件</td></tr><tr><td>BusRdX</td><td>侦听到其他处理器 (处理器 -&gt; cache) 事件 (或者称为&quot;读独占&quot;)</td></tr><tr><td>Flush</td><td>侦听到其他处理器 (cache -&gt; 主存) 事件</td></tr></tbody></table><p>关于Flush事务: 表示将缓存块放在总线上, 然后:</p><ul><li>处理器侧(发出BusRd或者BusRdX的处理器应该接收该块并更新自己的缓存</li><li>主存侧应该更新该块对应的主存块</li></ul><p>这时候可能就有人要问了, 如果是<code>BusRdX</code>事务, 为啥也需要<code>Flush</code>, 因为发出<code>BusRdX</code>即表示该处理器(发出者)已经重写了该块了, 那么不是直接将响应者由M状态改变为I就可以了吗?</p><p>答案:</p><blockquote><p>不过该答案是否意味着: 处理器修改缓存块并不是以一整块为单位的, 而是可以修改和读取其中的一部分字节?</p></blockquote><h4 id="优点-相比于VI协议">优点(相比于VI协议)</h4><ul><li>对写入带宽的需求很小, 如果一个带宽被重复写入多次, 不同于VI协议的每次都触发BusWr, 只会产生一个BusRdx, 剩下的写操作将在独占状态(M)下进行, 不会产生总线事务</li></ul><h4 id="缺点-2">缺点</h4><ul><li><p>如果有一个处理器希望读入一些数据块并对他们进行写入, 当这些数据块<strong>不存在于其他处理器缓存中时</strong>, 对于每一个<code>读 - 写</code>的操作序列, 将会触发两个总线事务:</p><ol><li>读: 通过<code>BusRd</code>将块变为S状态 (对应左图<code>I -&gt; S</code>)</li><li>写: 通过<code>BusRdX</code>来将块变为独占(使其他缓存失效) (对应左图 <code>S -&gt; M</code>)</li></ol><p>注意<code>BusRdx</code>在此环境下是没有作用的, 因为这些数据块不存在与其他处理器缓存, 故写的时候其实并不需要发出独占事件, 但是其他处理器其实并不知晓这个前提条件.</p><p>然而致命之处在于, 该场景是十分常见的, 因为多线程其实并不会共享很多的数据, 所以大部分缓存只保存在一个缓存块上, 因此大多数的BusRdX请求变得没有必要</p></li></ul><h4 id="一个潜在的一致性问题">一个潜在的一致性问题</h4><p>在MSI一致性协议中, 很容易就可以从状态图中发现一个问题:</p><blockquote><p><strong>主存很有可能在块被Flush到主存之前就向其他缓存提供了主存当前存储的旧值</strong></p></blockquote><p>比如一个处理器1请求一块数据, 该数据被其他缓存独占且并未刷新到主存, 这时候应该先由<code>PrRd</code>触发总线读事务<code>BusRd</code>, 当拥有该缓存(处于M状态)的处理器2收到该事务后执行Flush将独占的数据写回主存, 然后由主存再将数据提供给处理器1</p><p>我们期望的顺序为</p><pre><code class=" mermaid">sequenceDiagramparticipant p1 as 处理器1 (Invalid)participant m as 主存participant p2 as 处理器2 (Modified)p1 -&gt;&gt; m: 请求数据p2 -&gt;&gt; m: FLush自己的数据m --&gt;&gt; p1: 提供最新的数据</code></pre><p>然而这种期望如何保证正确的执行顺序呢, (书中给出了两种可以考虑采用的方案)</p><ol><li>在内存控制器答复块之前, 比处理器足够多的时间让它完成对总线的侦听的响应, 比如内存控制器已经从主存中拿到了一个块, 可以在确定没有缓存Flush之前将该块存在一个表里</li><li>采用一种成为<strong>全侦听响应</strong>的方式, 引入一个特殊的&quot;侦听完成&quot;总线</li></ol><h4 id="一个对MSI协议在总线事务上的升级">一个对MSI协议在总线事务上的升级</h4><p>注意到MSI协议中在<strong>内存控制器</strong>的角度来看, 其没有办法辨别发出<code>BusRdX</code>的控制器是拥有该缓存块了还是并没有需要从主存中去取</p><p>因此可以引入一个新的总线事务(操作/请求): <code>BusUpgr</code></p><ul><li>如果缓存已经有有了该块(S -&gt; M), 则只需要更新权限, 不需要主存响应, 此时发出<code>BusUpgr</code></li><li>如果缓存没有该块(I -&gt; M), 则需要主存或者其他缓存提供, 此时发出<code>BusRdX</code></li></ul><h3 id="3-写回的MESI协议">3 | 写回的MESI协议</h3><p>针对与MSI协议的缺点: 不论该块是否在一个缓存块上, MSI协议在<code>读 - 写</code>时都会触发两个总线事务</p><p>因此MESI协议加入了E状态(<code>Exclusive</code>)来区分缓存块是干净且唯一的还是干净且在多个缓存上拥有拷贝的</p><table><thead><tr><th>缓存块状态</th><th>解释</th></tr></thead><tbody><tr><td><code>Modified (M)</code></td><td>缓存块有效且(可能)和主存中数据不同, 并且该处理器有独占权</td></tr><tr><td><code>Shared (S)</code></td><td>缓存块有效且与其他处理器共享, 并且是干净的(和主存一致)</td></tr><tr><td><code>Invalid (I)</code></td><td>缓存块无效</td></tr><tr><td><code>Exclusive (E)</code></td><td>缓存块干净有效且唯一</td></tr></tbody></table><table><thead><tr><th>处理器的操作</th><th>说明</th></tr></thead><tbody><tr><td>PrRd</td><td>处理器读缓存块</td></tr><tr><td>PrWr</td><td>处理器写缓存块</td></tr></tbody></table><table><thead><tr><th>总线侦听的操作</th><th>说明</th></tr></thead><tbody><tr><td>BusRd</td><td>侦听到其他处理器 (主存 -&gt; cache) 事件</td></tr><tr><td>BusRdX</td><td>侦听到其他处理器 (处理器 -&gt; cache) 事件  (该块已经被缓存)</td></tr><tr><td>BusUpgr</td><td>侦听到其他处理器 (处理器 -&gt; cache) 事件  (该块还未被缓存)</td></tr><tr><td>Flush</td><td>侦听到其他处理器 (cache -&gt; 主存) 事件 (从缓存到主存)</td></tr><tr><td>FlushOpt</td><td>侦听到其他处理器(cache -&gt; cache) 事件 (从一个缓存到另一个缓存)</td></tr></tbody></table><p>FlushOpt的存在是为了节约写入主存的操作的开销</p><p>注意: 缓存一致性控制器为了知道加载进缓存的块是E状态还是S状态, 增加了一个&quot;C总线&quot;, 用来表示缓存块拷贝是否存在, 当缓存块拷贝存在时, 总线是高电平, 否则总线是低电平并且缓存块唯一</p><p>状态转移图: (还是基于<strong>写分配 + 写无效</strong>)</p><h4 id="优点-相比于MSI协议">优点(相比于MSI协议)</h4><p>提升了MSI协议的性能, 解决了MSI遇到的缓存块不存在拷贝时候的多事务开销</p><h4 id="缺点-3">缺点</h4><p>当一个缓存块被多个处理器连续地读写时, 每一个读操作都会触发干预, 需要拥有者Flush缓存块, 并且更新到主存其实是不必要的, 为了保持主存的干净(与缓存一致), 主存的更新操作过于频繁, 会消耗过多的带宽</p><p>因此我们期望能允许多个缓存之间<strong>共享脏块</strong>来避免对主存的频繁更新, 这就引出了下面的MOESI协议</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84/">体系结构</category>
      
      
      <category domain="https://moreality.net/tags/%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84/">体系结构</category>
      
      <category domain="https://moreality.net/tags/%E5%B9%B6%E8%A1%8C/">并行</category>
      
      <category domain="https://moreality.net/tags/%E7%BC%93%E5%AD%98%E4%B8%80%E8%87%B4%E6%80%A7/">缓存一致性</category>
      
      <category domain="https://moreality.net/tags/cache-coherence/">cache coherence</category>
      
      
      <comments>https://moreality.net/posts/26194/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>一个简单的cpu模拟器</title>
      <link>https://moreality.net/posts/17279/</link>
      <guid>https://moreality.net/posts/17279/</guid>
      <pubDate>Fri, 07 Oct 2022 08:16:29 GMT</pubDate>
      
      <description>一个简单的cpu模拟器, 用python模拟了汇编语言执行的过程</description>
      
      
      
      <content:encoded><![CDATA[<div class="note note-info">            <p>这几天正在看一些体系结构相关的论文, 想回顾一下本科微机原理的一些东西, 心想不妨用简单的语言模拟一下cpu取指译码执行的这些过程, 于是便用python仿照<a href="https://github.com/nekin2017/pipeline_simulator/tree/master/simple_cpu">这个项目</a>写了个简单的cpu模拟器</p>          </div><p>项目地址: <a href="https://github.com/Lincest/simple-cpu">https://github.com/Lincest/simple-cpu</a></p><h2 id="指令集和伪操作定义">指令集和伪操作定义</h2><p>定义了几个非常简单常用的指令如下:</p><table><thead><tr><th>instruction</th><th>description</th></tr></thead><tbody><tr><td><code>nop</code></td><td>no operation</td></tr><tr><td><code>load</code></td><td><code>load r1 r2</code> load data with address of r2 to r1</td></tr><tr><td><code>movi</code></td><td><code>movi r1 1234</code></td></tr><tr><td><code>store</code></td><td><code>store r1 r2</code> store r2’ s data to address of r1</td></tr><tr><td><code>inc</code></td><td><code>inc r1</code> r1’s data += 1</td></tr><tr><td><code>cmpi</code></td><td><code>cmpi r1 1234</code> compare r1’s data with 1234</td></tr><tr><td><code>jnz</code></td><td><code>jnz 0x12</code> jump to offset 0x12 (addr: current_addr + offset 0x12) if not zero</td></tr><tr><td><code>halt</code></td><td>pause the cpu</td></tr><tr><td><code>add</code></td><td><code>add r1, 4</code> r1’s data += 4</td></tr></tbody></table><p>同时定义了一些伪操作, 主要用途是配合label实现重定位:</p><table><thead><tr><th>pseudo (伪操作)</th><th>description</th></tr></thead><tbody><tr><td><code>label</code></td><td><code>label &lt;name&gt;</code> label definition</td></tr><tr><td><code>jnzl</code></td><td><code>jnzl &lt;name&gt;</code> jump to label</td></tr><tr><td><code>movil</code></td><td><code>movil r1 &lt;label&gt;</code> save label’s addr to r1</td></tr><tr><td><code>data</code></td><td><code>data &lt;number&gt;</code> immediate numbers space</td></tr></tbody></table><h2 id="汇编器">汇编器</h2><p>代码在: <a href="https://github.com/Lincest/simple-cpu/blob/master/assembler/assembler.py">https://github.com/Lincest/simple-cpu/blob/master/assembler/assembler.py</a></p><p>这里阐述一下主要的逻辑</p><ul><li>首先汇编器将打开源代码<code>xx.s</code>文件, 按行读取每一行汇编代码</li><li>按指令区分, 交给对应的处理函数<code>resolver</code>, 转换为机器码</li><li>在处理结束后, 进行重定位的处理, 这时将根据label的值区分绝对定位(movi)和相对定位(jnz), 重新填充含有label的指令</li><li>最后将机器码按小端的字节序写入输出文件, 交给cpu进行处理</li></ul><h3 id="几个需要注意的问题">几个需要注意的问题</h3><ul><li>模拟的字节序号是小端存储, 而内存是用数组模拟的16KB, 前面是代码段, 没有进行明确的代码段和数据段的区分, 这时例如<code>movi r0, 0xFFFE</code>这条指令, 如<code>movi</code>被编码为<code>0x01</code>而r0被编码为<code>0x11</code>, 在内存中将按这种方式存储:</li></ul><figure class="highlight txt"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs txt">       +--------------+--------------+--------------+--------------+                <br>       |     0x01     |    0x11  |     0xFE     |     0xFF     |  <br>       +--------------+--------------+--------------+--------------+                <br>                                                                                    <br>     -----------------------------------------------------------------&gt;             <br>low memory                                                       high memory <br></code></pre></td></tr></table></figure><ul><li><code>jnzl label</code>使用的是相对偏移量, 而<code>movil label</code>使用的是绝对偏移量</li><li>寄存器为32bit, 可以存储有符号数, 故数据的范围是<code>-2147483648~2147483647</code>即<code>0X80000000~0X7FFFFFFF</code></li></ul><h2 id="cpu">cpu</h2><p>cpu的处理流程如下:</p><ul><li>首先申请一块16 kB的空间</li><li>将程序读入到低地址部分</li><li>取指-译码-执行</li></ul><h2 id="例子">例子</h2><p>一个简单的程序:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs shell">movi r1, 0<br>movi r3, 4096<br>movil r2, data_addr<br><br>label loop<br><span class="hljs-meta prompt_"># </span><span class="language-bash">load memory[r2] to r4</span><br>load r4, r2<br><span class="hljs-meta prompt_"># </span><span class="language-bash">store r4<span class="hljs-string">&#x27;s value to memory[r3]</span></span><br>store r3, r4<br><span class="hljs-meta prompt_"># </span><span class="language-bash"><span class="hljs-string">load memory[r3] to r5</span></span><br>load r5, r3<br><span class="hljs-meta prompt_"># </span><span class="language-bash"><span class="hljs-string">r2 += 4 to fetch next number</span></span><br>add r2, 4<br><span class="hljs-meta prompt_"># </span><span class="language-bash"><span class="hljs-string">compare r4&#x27;</span>s value with number -0x1234</span><br>cmpi r5, -0x1234<br><span class="hljs-meta prompt_"># </span><span class="language-bash">jump <span class="hljs-keyword">if</span> not equal</span><br>jnzl loop<br><span class="hljs-meta prompt_"># </span><span class="language-bash">program ends</span><br>halt<br><br>label data_addr<br>data -55 -30 40<br>data 0x1234 0x123456ff<br>data -0x1234<br></code></pre></td></tr></table></figure><p>其程序执行的过程如下:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><code class="hljs shell">memory size = 16384B<br>mov imme = 0 to r1<br>mov imme = 4096 to r3<br>mov imme = 40 to r2<br>load from memory[r2 = 0x28] = 0x-37 (-55) -&gt; r4<br>store from r4 = 0x-37 (-55) to memory[r3 = 0x1000]<br>load from memory[r3 = 0x1000] = 0x-37 (-55) -&gt; r5<br>r2 += 4 = 44<br>compare r5 = -55 with -4660, now r15 = 0x1<br>load from memory[r2 = 0x2c] = 0x-1e (-30) -&gt; r4<br>store from r4 = 0x-1e (-30) to memory[r3 = 0x1000]<br>load from memory[r3 = 0x1000] = 0x-1e (-30) -&gt; r5<br>r2 += 4 = 48<br>compare r5 = -30 with -4660, now r15 = 0x1<br>load from memory[r2 = 0x30] = 0x28 (40) -&gt; r4<br>store from r4 = 0x28 (40) to memory[r3 = 0x1000]<br>load from memory[r3 = 0x1000] = 0x28 (40) -&gt; r5<br>r2 += 4 = 52<br>compare r5 = 40 with -4660, now r15 = 0x1<br>load from memory[r2 = 0x34] = 0x1234 (4660) -&gt; r4<br>store from r4 = 0x1234 (4660) to memory[r3 = 0x1000]<br>load from memory[r3 = 0x1000] = 0x1234 (4660) -&gt; r5<br>r2 += 4 = 56<br>compare r5 = 4660 with -4660, now r15 = 0x1<br>load from memory[r2 = 0x38] = 0x123456ff (305420031) -&gt; r4<br>store from r4 = 0x123456ff (305420031) to memory[r3 = 0x1000]<br>load from memory[r3 = 0x1000] = 0x123456ff (305420031) -&gt; r5<br>r2 += 4 = 60<br>compare r5 = 305420031 with -4660, now r15 = 0x1<br>load from memory[r2 = 0x3c] = 0x-1234 (-4660) -&gt; r4<br>store from r4 = 0x-1234 (-4660) to memory[r3 = 0x1000]<br>load from memory[r3 = 0x1000] = 0x-1234 (-4660) -&gt; r5<br>r2 += 4 = 64<br>compare r5 = -4660 with -4660, now r15 = 0x0<br>cpu halt<br></code></pre></td></tr></table></figure>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84/">体系结构</category>
      
      
      <category domain="https://moreality.net/tags/%E6%B1%87%E7%BC%96/">汇编</category>
      
      <category domain="https://moreality.net/tags/cpu%E6%A8%A1%E6%8B%9F%E5%99%A8/">cpu模拟器</category>
      
      <category domain="https://moreality.net/tags/%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84/">体系结构</category>
      
      
      <comments>https://moreality.net/posts/17279/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>docker-swarm实战</title>
      <link>https://moreality.net/posts/51823/</link>
      <guid>https://moreality.net/posts/51823/</guid>
      <pubDate>Tue, 23 Aug 2022 18:30:29 GMT</pubDate>
      
      <description>docker-swarm小实验记录</description>
      
      
      
      <content:encoded><![CDATA[<h2 id="白嫖阿里服务器">白嫖阿里服务器</h2><p>上: <a href="https://developer.aliyun.com/plan/grow-up">https://developer.aliyun.com/plan/grow-up</a></p><p>申请学生认证, 完成俩任务就可以白嫖2周的2核2G阿里云服务器了</p><h2 id="步骤">步骤</h2><p>和之前白嫖的Azure服务器一起用</p><h3 id="安装docker">安装docker</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">curl -sSL https://get.daocloud.io/docker | sh<br></code></pre></td></tr></table></figure><h3 id="开放端口">开放端口</h3><p>根据官方教程, 开放端口</p><p>阿里云在<code>云服务ECS/安全组</code>中设置</p><p>Azure在<code>设置/网络</code>中设置</p><h3 id="创建swarm和manager">创建swarm和manager</h3><p>阿里云命令行没延迟, 用它当<code>manager</code></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker swarm init --advertise-addr 32.121.74.215<br></code></pre></td></tr></table></figure><p>会产生一个token:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs shell">Swarm initialized: current node (7we8v0yso3js5u56ypk60ffbb) is now a manager.<br><br>To add a worker to this swarm, run the following command:<br><br>    docker swarm join --token SWMTKN-1-3lbmvgaz07xylkgofhukkhmxvescqa8l5b3lasw94ocor5hrp7-b0rtmn323qazz2eidfund4orn 32.121.74.215:2377<br><br>To add a manager to this swarm, run &#x27;docker swarm join-token manager&#x27; and follow the instructions.<br></code></pre></td></tr></table></figure><p><code>--advertise-addr &lt;IP&gt;</code>表示将该IP配置为管理者, 产生的token表示其他worker可以用该token加入该集群</p><p>可以使用<code>docker info</code>查看当前状态:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs shell">root@asdf:~# docker info<br>Client:<br> Context:    default<br> Debug Mode: false<br> Plugins:<br>  app: Docker App (Docker Inc., v0.9.1-beta3)<br>  buildx: Docker Buildx (Docker Inc., v0.8.2-docker)<br>  compose: Docker Compose (Docker Inc., v2.6.0)<br>  scan: Docker Scan (Docker Inc., v0.17.0)<br><br>Server:<br> Containers: 0<br>  Running: 0<br>  Paused: 0<br>  Stopped: 0<br> Images: 0<br> Server Version: 20.10.17<br> ...<br></code></pre></td></tr></table></figure><p>可以使用<code>docker node ls</code>查看所有节点和信息</p><h3 id="加入worker">加入worker</h3><p>我们将<code>Azure</code>加入集群:</p><p>在manager上运行</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker swarm join-token worker<br></code></pre></td></tr></table></figure><p>即可查看加入的token和加入命令, 和前面init时一致</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">root@moreality:~# docker swarm join --token SWMTKN-1-3lbmvgaz07xylkgofhukkhmxvescqa8l5b3lasw94ocor5hrp7-b0rtmn323qazz2eidfund4orn 32.121.74.215:2377<br>This node joined a swarm as a worker.<br></code></pre></td></tr></table></figure><p>进入worker的机器, 运行:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">root@asdf:~# docker node ls<br></code></pre></td></tr></table></figure><p>即可查看目前的<code>manager</code>和<code>worker</code>节点</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs shell">ID                            HOSTNAME                  STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION<br>7we8v0yso3js5u56ypk60ffbb *   iZ0jlfl8zktqzybrmxk2s9Z   Ready     Active         Leader           20.10.17<br>mka9mbeqhivcaxdwqdb29gio3     moreality                 Ready     Active<br>    18.09.7<br></code></pre></td></tr></table></figure><p>其中VERSION表示对应机器上docker的版本</p><h3 id="创建和加入服务">创建和加入服务</h3><p>按照官网输入如下创建服务的命令</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker service create --replicas 1 --name helloworld alpine ping docker.com<br></code></pre></td></tr></table></figure><ul><li><code>docker service create</code>: 创建服务。</li><li><code>--name</code>: 命名服务<code>helloworld</code>。</li><li><code>--replicas</code>: 指定一个副本运行</li><li><code>alpine ping docker.com</code>: 将服务定义为执行命令的 Alpine Linux 容器并运行<code>ping docker.com</code>。</li></ul><p>然后运行<code>docker service ls</code>就可以查看刚才建立的服务了:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell">root@asdf:~# docker service ls<br>ID             NAME         MODE         REPLICAS   IMAGE           PORTS<br>s119e5qgow4i   helloworld   replicated   1/1        alpine:latest<br></code></pre></td></tr></table></figure><h3 id="查看和监视服务的命令">查看和监视服务的命令</h3><ul><li><code> docker service inspect --pretty helloworld</code>: 查看该服务的配置详细信息, 加pretty表示以易读的方式打印, 可以选择不加展示原始的json信息</li><li><code>docker service ps &lt;SERVICE-ID&gt;</code>: 查看正在运行该服务的节点</li></ul><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs shell">root@asdf:~# docker node ls<br>ID                            HOSTNAME                  STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION<br>7we8v0yso3js5u56ypk60ffbb *   asdf   Ready     Active         Leader           20.10.17<br>mka9mbeqhivcaxdwqdb29gio3     moreality                 Ready     Active<br>    18.09.7<br>    <br>root@asdf:~# docker service ps helloworld<br>ID             NAME           IMAGE           NODE                      DESIRED STATE   CURRENT STATE           ERROR     PORTS<br>mwl78f2p88zi   helloworld.1   alpine:latest   asdf   Running         Running 4 minutes ago<br></code></pre></td></tr></table></figure><p>则表示该服务证运行在worker节点上(对比NODE和HOSTNAME)</p><ul><li>在服务运行的节点上使用<code>docker ps</code>可以查看详细信息</li></ul><h3 id="服务扩展-Scale">服务扩展 (Scale)</h3><p>将服务部署后, 可以很轻松的扩展服务的任务数量</p><blockquote><p>在服务(service)中运行的容器(container)称之为任务(task)</p></blockquote><p>运行:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker service scale &lt;SERVICE-ID&gt;=&lt;NUMBER-OF-TASKS&gt;<br></code></pre></td></tr></table></figure><p>如对于上面的hello world</p><p>运行:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker service scale helloworld=5<br></code></pre></td></tr></table></figure><p>表示将任务数增加到5个</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs shell">root@asdf:~# docker service scale helloworld=5<br>helloworld scaled to 5<br>overall progress: 5 out of 5 tasks<br>1/5: running<br>2/5: running<br>3/5: running<br>4/5: running<br>5/5: running<br>verify: Service converged<br></code></pre></td></tr></table></figure><p>这时我们再在worker上用<code>docker service ps helloworld</code>可以发现:</p><figure class="highlight apache"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs apache"><span class="hljs-attribute">root</span>@asdf:~# docker service ps helloworld;<br><span class="hljs-attribute">ID</span>             NAME           IMAGE           NODE        DESIRED STATE   CURRENT STATE            ERROR     PORTS<br><span class="hljs-attribute">mwl78f2p88zi</span>   helloworld.<span class="hljs-number">1</span>   alpine:latest   asdf        Running         Running <span class="hljs-number">15</span> minutes ago<br><span class="hljs-attribute">58nn77cngpkk</span>   helloworld.<span class="hljs-number">2</span>   alpine:latest   moreality   Running         Running <span class="hljs-number">4</span> minutes ago<br><span class="hljs-attribute">ibgir80e5vfc</span>   helloworld.<span class="hljs-number">3</span>   alpine:latest   moreality   Running         Running <span class="hljs-number">4</span> minutes ago<br><span class="hljs-attribute">vo1de2mmagle</span>   helloworld.<span class="hljs-number">4</span>   alpine:latest   moreality   Running         Running <span class="hljs-number">4</span> minutes ago<br><span class="hljs-attribute">tg6evmi7uk8u</span>   helloworld.<span class="hljs-number">5</span>   alpine:latest   asdf        Running         Running <span class="hljs-number">4</span> minutes ago<br></code></pre></td></tr></table></figure><p>任务被均匀地分配到了不同的主机上</p><h3 id="服务删除">服务删除</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker service rm helloworld<br></code></pre></td></tr></table></figure><p>就把helloworld服务删了</p><p>运行</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell">root@asdf:~# docker service inspect helloworld<br>[]<br>Status: Error: no such service: helloworld, Code: 1<br></code></pre></td></tr></table></figure><p>验证服务被删除</p><h3 id="服务滚动升级">服务滚动升级</h3><p>在worker上使用:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">root@asdf:~# docker service create --replicas 3 --name redis --update-delay 10s redis:3.0.6<br></code></pre></td></tr></table></figure><p>创建3个副本的redis:3.0.6</p><p>然后使用:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker service update --image redis:3.0.7 redis<br></code></pre></td></tr></table></figure><p>更新redis镜像</p><p>一般更新步骤如下:</p><ul><li>停止第一个任务。</li><li>为已停止的任务安排更新。</li><li>启动更新任务的容器。</li><li>如果一个任务的更新返回<code>RUNNING</code>，等待指定的延迟时间然后开始下一个任务。</li><li>如果在更新期间的任何时间，任务返回<code>FAILED</code>，则暂停更新。</li></ul><p>可以使用<code> docker service inspect --pretty redis</code>查看更新是否失败和错误信息</p><p>如果更新失败, 可以直接使用</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker service update redis<br></code></pre></td></tr></table></figure><p>重新执行更新</p><p>可以使用<code>docker service ps redis</code>查看整个滚动更新:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs shell">root@asdf:~# docker service ps redis<br>ID             NAME          IMAGE         NODE        DESIRED STATE   CURRENT STATE<br>       ERROR     PORTS<br>nwub9yrzm9q5   redis.1       redis:3.0.7   moreality   Running         Running about a minute ago<br>ptauimv31hx8    \_ redis.1   redis:3.0.6   moreality   Shutdown        Shutdown about a minute ago<br>ahzh1kalw2yo   redis.2       redis:3.0.7   asdf        Running         Running about a minute ago<br>hokzeh28ueaq    \_ redis.2   redis:3.0.6   asdf        Shutdown        Shutdown about a minute ago<br>8ypuvaxvzorj   redis.3       redis:3.0.7   asdf        Running         Running about a minute ago<br>g0ddca51aqwy    \_ redis.3   redis:3.0.6   moreality   Shutdown        Shutdown 2 minutes ago<br></code></pre></td></tr></table></figure><h3 id="清空-drain-节点">清空(drain)节点</h3><blockquote><p>注意drain操作只会影响swarm的负载, 而不会影响到该主机运行的非swarm的容器如使用<code>docker run</code>和<code>docker-compose up</code>创建的容器</p></blockquote><p>使用:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker node update --availability drain &lt;NODE-ID&gt;<br></code></pre></td></tr></table></figure><p>即可清空一个节点</p><p>NODE-ID信息可以用<code>docker node ls</code>查看</p><p>(ps: node-id可以直接使用主机名)</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker node update --availability drain asdf<br></code></pre></td></tr></table></figure><blockquote><p>ps: <code>manager</code>节点也是可以drain掉的, 和<code>drain</code>一个worker节点没有区别</p></blockquote><p>然后可以采用<code>docker service ps</code>查看目前任务的运行状况</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs shell">root@asdf:~# docker service ps -f &quot;desired-state=running&quot; redis<br>ID             NAME      IMAGE         NODE        DESIRED STATE   CURRENT STATE           ERROR     PORTS<br>nwub9yrzm9q5   redis.1   redis:3.0.7   moreality   Running         Running 9 minutes ago<br><br>tc04mbhfpbgt   redis.2   redis:3.0.7   moreality   Running         Running 3 minutes ago<br><br>qjfrb4g4l7jl   redis.3   redis:3.0.7   moreality   Running         Running 3 minutes ago<br></code></pre></td></tr></table></figure><p>可以看到, 目前running的节点都是moreality, 说明被drain的asdf节点已经没有任务在运行了</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs shell">root@asdf:~# docker service ps -f &quot;desired-state=shutdown&quot; redis<br>ID             NAME          IMAGE         NODE        DESIRED STATE   CURRENT STATE<br>   ERROR     PORTS<br>ptauimv31hx8   redis.1       redis:3.0.6   moreality   Shutdown        Shutdown 10 minutes ago<br><br>ahzh1kalw2yo   redis.2       redis:3.0.7   asdf        Shutdown        Shutdown 4 minutes ago<br><br>hokzeh28ueaq    \_ redis.2   redis:3.0.6   asdf        Shutdown        Shutdown 10 minutes ago<br><br>8ypuvaxvzorj   redis.3       redis:3.0.7   asdf        Shutdown        Shutdown 4 minutes ago<br><br>g0ddca51aqwy    \_ redis.3   redis:3.0.6   moreality   Shutdown        Shutdown 11 minutes ago<br></code></pre></td></tr></table></figure><p>可以采用</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker node update --availability active &lt;NODE-ID&gt;<br></code></pre></td></tr></table></figure><p>重新激活一个被drain掉的节点</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker node update --availability active asdf<br></code></pre></td></tr></table></figure><p>注意当一个节点从drain变为active时docker不会为其马上分配任务, 而是等到:</p><ul><li>在服务扩展时</li><li>在滚动更新时</li><li>当另一个节点设置为<code>drain</code>时</li><li>当任务在另一个<code>active</code>节点上失败时</li></ul><p>这个节点会被参与到任务再分配中</p><h3 id="端口发布">端口发布</h3><p>可以使用</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker service create \<br> --name &lt;SERVICE-NAME&gt; \<br> --publish published=&lt;PUBLISHED-PORT&gt;,target=&lt;CONTAINER-PORT&gt; \<br> &lt;IMAGE&gt;<br></code></pre></td></tr></table></figure><p>来绑定一个对外端口和内部端口</p><p>如使用</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker service create \<br> --name my-web \<br> --publish published=8080,target=80 \<br> --replicas 2 \<br> nginx<br></code></pre></td></tr></table></figure><p>所有node的8080端口均被路由到docker内部的80端口, 并参与到swarm内部的负载均衡策略上</p><p>可以使用</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker service inspect --format=&quot;&#123;&#123;json .Endpoint.Spec.Ports&#125;&#125;&quot; my-web<br></code></pre></td></tr></table></figure><p>来查看发布端口</p><h3 id="升级-降级节点">升级/降级节点</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker node promote &lt;node-name&gt; # 升级节点<br>docker node demote &lt;node-name&gt; # 降级节点<br></code></pre></td></tr></table></figure><h2 id="docker-stack和docker-compose-v3">docker stack和docker-compose v3</h2><blockquote><p>docker stack和docker compose结合使用可以达到在swarm多节点编排多容器的目的</p></blockquote><p>docker stack practice: <a href="https://yeasy.gitbook.io/docker_practice/swarm_mode/stack">https://yeasy.gitbook.io/docker_practice/swarm_mode/stack</a></p><p>docker-compose v3: <a href="https://docs.docker.com/compose/compose-file/compose-file-v3/">https://docs.docker.com/compose/compose-file/compose-file-v3/</a></p><p>docker compose v3添加了<code>deploy</code>的相关参数, 可以用于指定swarm集群编排的一些信息:</p><p>如</p><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs yml"><span class="hljs-attr">version:</span> <span class="hljs-string">&quot;3.9&quot;</span><br><span class="hljs-attr">services:</span><br>  <span class="hljs-attr">redis:</span><br>    <span class="hljs-attr">image:</span> <span class="hljs-string">redis:alpine</span><br>    <span class="hljs-attr">deploy:</span><br>      <span class="hljs-attr">replicas:</span> <span class="hljs-number">6</span><br>      <span class="hljs-attr">placement:</span><br>        <span class="hljs-attr">max_replicas_per_node:</span> <span class="hljs-number">1</span><br>      <span class="hljs-attr">update_config:</span><br>        <span class="hljs-attr">parallelism:</span> <span class="hljs-number">2</span><br>        <span class="hljs-attr">delay:</span> <span class="hljs-string">10s</span><br>      <span class="hljs-attr">restart_policy:</span><br>        <span class="hljs-attr">condition:</span> <span class="hljs-string">on-failure</span><br></code></pre></td></tr></table></figure><h3 id="部署服务">部署服务</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker stack deploy -c docker-compose.yml &lt;NAME&gt;<br></code></pre></td></tr></table></figure><h3 id="查看服务">查看服务</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker stack ls<br></code></pre></td></tr></table></figure><h3 id="删除服务">删除服务</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker stack rm &lt;ID&gt;<br></code></pre></td></tr></table></figure><h3 id="查看exit的服务日志">查看exit的服务日志</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">首先找到对应容器的<span class="hljs-built_in">id</span></span><br>docker ps -a<br><span class="hljs-meta prompt_"># </span><span class="language-bash">获取对应的日志路径</span><br>docker inspect --format=&#x27;&#123;&#123;.LogPath&#125;&#125;&#x27; $INSTANCE_ID<br><span class="hljs-meta prompt_"># </span><span class="language-bash">直接<span class="hljs-built_in">cat</span>读取就行了</span><br>cat &lt;PATH&gt;<br></code></pre></td></tr></table></figure><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_"># </span><span class="language-bash">或者</span><br>cat $(docker inspect --format=&#x27;&#123;&#123;.LogPath&#125;&#125;&#x27; $INSTANCE_ID)<br></code></pre></td></tr></table></figure><h3 id="查看服务IP">查看服务IP</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker inspect --format=&#x27;&#123;&#123;range .NetworkSettings.Networks&#125;&#125; &#123;&#123;.IPAddress&#125;&#125;&#123;&#123;end&#125;&#125;&#x27; e4ebf2177c33<br></code></pre></td></tr></table></figure><h3 id="host模式和ingress模式的区别">host模式和ingress模式的区别</h3><p>集群的端口映射有两种模式: <code>host</code>和<code>ingress</code></p><p>其中<code>host</code>表示只有服务副本所在的节点可以被外部访问</p><p>而<code>ingress</code>表示所有节点都可以被外部访问, 节点内部通过负载均衡分配</p><p><code>ingress</code>为默认策略</p><h2 id="portainer">portainer</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker service create \<br>    --name portainer_agent \<br>    --network rocco \<br>    --mode global \<br>    --constraint &#x27;node.platform.os == linux&#x27; \<br>    --mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \<br>    --mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes \<br>    portainer/agent<br><br></code></pre></td></tr></table></figure><h3 id="创建网络">创建网络:</h3><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">docker network create --driver overlay &lt;NAME&gt;<br></code></pre></td></tr></table></figure><h2 id="基本信息">基本信息</h2><h3 id="Azure">Azure</h3><p>os: <code>ubuntu 16.04</code></p><p>主机名(hostname): asdf</p><p>Azure服务器IP: <code>20.187.212.232</code></p><p>docker版本: Docker version 18.09.7, build 2d0083d</p><h3 id="阿里云">阿里云</h3><p>os: <code>ubuntu 18.04</code></p><p>主机名(hostname): moreality</p><p>阿里服务器IP: <code>32.121.74.215</code></p><p>docker版本: Docker version 20.10.17, build 100c701</p>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E5%90%8E%E7%AB%AF%E5%AD%A6%E4%B9%A0/">后端学习</category>
      
      
      <category domain="https://moreality.net/tags/docker-swarm/">docker-swarm</category>
      
      <category domain="https://moreality.net/tags/%E9%9B%86%E7%BE%A4/">集群</category>
      
      <category domain="https://moreality.net/tags/%E5%88%86%E5%B8%83%E5%BC%8F/">分布式</category>
      
      
      <comments>https://moreality.net/posts/51823/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>如何从Youtube搬运一个视频?</title>
      <link>https://moreality.net/posts/40450/</link>
      <guid>https://moreality.net/posts/40450/</guid>
      <pubDate>Fri, 19 Aug 2022 16:35:28 GMT</pubDate>
      
      <description>今天刷油管看到了Joma发的新小视频, 感觉非常有意思, 萌生了翻译一下搬运到b站的想法, 这里记录一下整个从下载视频, 音频到编辑, 翻译, 合并双语字幕的整个工作流</description>
      
      
      
      <content:encoded><![CDATA[<blockquote><p>今天刷油管看到了Joma发的新小视频, 感觉非常有意思, 萌生了翻译一下搬运到b站的想法</p></blockquote><p>这里记录一下整个从下载 视频, 音频到编辑, 翻译, 合并双语字幕的整个工作流</p><p><a class="btn" href="https://www.bilibili.com/video/bv1Ag411r7Lo?vd_source=ad663194d82bdf3c250b7919806b85fc" title="github">看看我翻译的第一个视频</a></p><iframe src="//player.bilibili.com/player.html?aid=514674738&bvid=BV1Ag411r7Lo&cid=808660808&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" width="100%" height="500px"> </iframe><h2 id="youtube视频下载">youtube视频下载</h2><p>现在最稳妥的youtube的视频下载方式就是通过github的这俩插件, 其中前者在github上已经达到了惊人的<code>112k</code>star, 而后者是前者的优化升级版本</p><p><code>youtube-dl</code>: <a href="https://github.com/ytdl-org/youtube-dl">https://github.com/ytdl-org/youtube-dl</a></p><p><code>yt-dlp</code>: <a href="https://github.com/yt-dlp/yt-dlp">https://github.com/yt-dlp/yt-dlp</a></p><p>但是经过我的测试, <code>youtube-dl</code>的下载速度不佳(一度40KB/s), 不知道为什么<code>yt-dlp</code>速度快很多(10MB/s) (百兆宽带 + 科学上网)</p><p>这里以<code>yt-dlp</code>为例, 展示下如何下载视频, 音频和字幕文件</p><p>下载直接到<a href="https://github.com/yt-dlp/yt-dlp/releases">release</a>页下载操作系统对应的可执行文件就好了, 我这里用的windows</p><div class="note note-warning">            <p>btw, 下面的许多步骤都是基于你已经安装了<code>ffmpeg</code>并配置好环境变量的基础上的.</p><p>ffmpeg安装: <a href="https://ffmpeg.org/download.html">https://ffmpeg.org/download.html</a></p>          </div><h3 id="命令">命令</h3><blockquote><p>请先确认你的终端走了代理, cmd可以参考<code>set https_proxy=xx</code>, powershell可用<code>$env:https_proxy=xx</code></p></blockquote><h4 id="下载视频和音频">下载视频和音频</h4><p><code>yt-dlp.exe -F https://www.youtube.com/watch?v=asdf</code>: <strong>查看可下载项</strong></p><p><code>yt-dlp.exe -f [ID] https://www.youtube.com/watch?v=asdf</code> 下载第一列ID对应的项</p><p>注意到<code>VCDOEC</code>和<code>ACODEC</code>标识了该项是音频or视频, 可以直接通过下面的命令下载并合并音视频:</p><p><code>yt-dlp.exe -f [ID1]+[ID2] https://www.youtube.com/watch?v=asdf</code></p><h4 id="下载字幕">下载字幕</h4><p>先用<code>yt-dlp.exe --list-subs https://www.youtube.com/watch?v=asdf</code> 查看所有字幕支持的语言</p><p>然后<code>yt-dlp.exe --write-subs --sub-format SRT --sub-langs en --skip-download https://www.youtube.com/watch?v=asdf</code></p><ul><li><code>--skip-download</code>表示不下载源视频</li></ul><p>下载SRT格式的英文字幕, 如果下载下来为VTT格式的, 直接把VTT文档中的前两行删除, 然后文件名后缀为<code>.srt</code>即可</p><blockquote><p>顺带一提: 字幕翻译可以不必在这里下载翻译的字幕, 可以在下面的字幕制作中一起翻译</p></blockquote><h2 id="字幕编辑">字幕编辑</h2><p>推荐一个非常好用的开源字幕编辑器subtitleedit</p><p><a href="https://github.com/SubtitleEdit/subtitleedit/releases">https://github.com/SubtitleEdit/subtitleedit/releases</a></p><p>支持:</p><ul><li>导入字幕文件视频并实时编辑查看</li><li>基于google翻译字幕</li><li>修改字幕样式</li><li>etc</li></ul><p>在编辑好字幕后</p><ul><li>点击: <code>文件-保存</code> 保存翻译后字幕</li><li>点击: <code>文件-保存原始字幕</code> 保存英文字幕</li></ul><h2 id="字幕合并">字幕合并</h2><p>目前字幕分为翻译和原始两个文件, 我们制作双语字幕期望他们一上一下出现, 可以自己写个简单的python脚本处理, 我这里网上随便找了一个: <a href="https://stackoverflow.com/questions/69868981/how-to-merge-multiple-subtitle-files-in-python">https://stackoverflow.com/questions/69868981/how-to-merge-multiple-subtitle-files-in-python</a></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-comment">#!/usr/bin/env python3</span><br><br><span class="hljs-keyword">import</span> argparse<br><span class="hljs-keyword">import</span> sys<br><span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> timedelta<br><span class="hljs-keyword">from</span> pathlib <span class="hljs-keyword">import</span> Path<br><br><span class="hljs-comment"># REQUIRED MODULE: pip3 install srt</span><br><span class="hljs-keyword">import</span> srt<br><br><br><span class="hljs-keyword">def</span> <span class="hljs-title function_">nearest</span>(<span class="hljs-params">items, pivot</span>):<br>    <span class="hljs-keyword">return</span> <span class="hljs-built_in">min</span>(items, key=<span class="hljs-keyword">lambda</span> x: <span class="hljs-built_in">abs</span>(x.start - pivot))<br><br><br><span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">&#x27;__main__&#x27;</span>:<br><br>    parser = argparse.ArgumentParser(description=<span class="hljs-string">&#x27;merge SRT subtitles&#x27;</span>,<br>                                     usage=<span class="hljs-string">&quot;&quot;&quot;</span><br><span class="hljs-string">    Merge SRT subtitles:                                     </span><br><span class="hljs-string">    \t&#123;0&#125; first.srt second.srt -o merged.srt</span><br><span class="hljs-string"> &quot;&quot;&quot;</span>.<span class="hljs-built_in">format</span>(Path(sys.argv[<span class="hljs-number">0</span>]).name))<br><br>    parser.add_argument(<span class="hljs-string">&#x27;srt1&#x27;</span>,<br>                        metavar=<span class="hljs-string">&#x27;srt1&#x27;</span>,<br>                        <span class="hljs-built_in">help</span>=<span class="hljs-string">&#x27;SRT-file-1&#x27;</span>)<br>    parser.add_argument(<span class="hljs-string">&#x27;srt2&#x27;</span>,<br>                        metavar=<span class="hljs-string">&#x27;srt2&#x27;</span>,<br>                        <span class="hljs-built_in">help</span>=<span class="hljs-string">&#x27;SRT-file-2&#x27;</span>)<br>    parser.add_argument(<span class="hljs-string">&#x27;--output-file&#x27;</span>, <span class="hljs-string">&#x27;-o&#x27;</span>,<br>                        default=<span class="hljs-literal">None</span>,<br>                        <span class="hljs-built_in">help</span>=<span class="hljs-string">&#x27;Output filename&#x27;</span>)<br>    parser.add_argument(<span class="hljs-string">&#x27;--encoding&#x27;</span>, <span class="hljs-string">&#x27;-e&#x27;</span>,<br>                        default=<span class="hljs-literal">None</span>,<br>                        <span class="hljs-built_in">help</span>=<span class="hljs-string">&#x27;Input file encoding&#x27;</span>)<br>    args = parser.parse_args(sys.argv[<span class="hljs-number">1</span>:])<br><br>    srt1_path = Path(args.srt1)<br>    srt2_path = Path(args.srt2)<br><br>    <span class="hljs-keyword">with</span> srt1_path.<span class="hljs-built_in">open</span>(encoding=args.encoding <span class="hljs-keyword">or</span> <span class="hljs-string">&#x27;utf-8&#x27;</span>) <span class="hljs-keyword">as</span> fi1:<br>        subs1 = &#123;s.index: s <span class="hljs-keyword">for</span> s <span class="hljs-keyword">in</span> srt.parse(fi1)&#125;<br><br>    <span class="hljs-keyword">with</span> srt2_path.<span class="hljs-built_in">open</span>(encoding=args.encoding <span class="hljs-keyword">or</span> <span class="hljs-string">&#x27;utf-8&#x27;</span>) <span class="hljs-keyword">as</span> fi2:<br>        subs2 = &#123;s.index: s <span class="hljs-keyword">for</span> s <span class="hljs-keyword">in</span> srt.parse(fi2)&#125;<br><br>    <span class="hljs-comment"># iterate all subs in srt2 and find the closest EXISTING slot in srt1</span><br>    sub: srt.Subtitle<br>    start: <span class="hljs-built_in">int</span><br>    <span class="hljs-keyword">for</span> idx, sub <span class="hljs-keyword">in</span> subs2.items():<br><br>        start: timedelta = sub.start<br>        sub_nearest_slot: srt.Subtitle = nearest(subs1.values(), start)<br>        sub_nearest_slot.content = <span class="hljs-string">f&#x27;<span class="hljs-subst">&#123;sub_nearest_slot.content&#125;</span>&lt;br&gt;<span class="hljs-subst">&#123;sub.content&#125;</span>&#x27;</span><br>        subs1[sub_nearest_slot.index] = sub_nearest_slot<br><br>    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> args.output_file:<br>        generated_srt = srt1_path.parent / (<span class="hljs-string">f&#x27;<span class="hljs-subst">&#123;srt1_path.stem&#125;</span>_MERGED_<span class="hljs-subst">&#123;srt1_path.suffix&#125;</span>&#x27;</span>)<br>    <span class="hljs-keyword">else</span>:<br>        generated_srt = Path(args.output_file)<br><br>    <span class="hljs-keyword">with</span> generated_srt.<span class="hljs-built_in">open</span>(mode=<span class="hljs-string">&#x27;w&#x27;</span>, encoding=<span class="hljs-string">&#x27;utf-8&#x27;</span>) <span class="hljs-keyword">as</span> fout:<br>        fout.write(srt.compose(<span class="hljs-built_in">list</span>(subs1.values())))<br></code></pre></td></tr></table></figure><p>然后进行: <code>python3 srt_merge.py subs1.srt subs2.srt -o final.srt</code>即可</p><p>注意提前安装一下srt模块: <code>pip install srt</code></p><h2 id="将srt嵌入视频">将srt嵌入视频</h2><div class="note note-info">            <p>当我欣喜若狂的将嵌好字幕的视频发到b站后, 发现字幕并没有被显示, 原来使我都没搞清楚外挂字幕和内嵌字幕</p>          </div><h3 id="软字幕-内挂字幕">软字幕 (内挂字幕)</h3><p>软字幕是通过stream copy流拷贝的方式进行的, 一般保存为<code>mkv</code>文件, 打开可以发现可以用视频播放器调整字幕大小等.</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell">ffmpeg -i input.mkv -i subtitles.ass -codec copy -map 0 -map 1 output.mkv<br><br>ffmpeg -i infile.mp4 -f srt -i infile.srt -c:v copy -c:a copy -c:s mov_text outfile.mp4<br></code></pre></td></tr></table></figure><p>这里推荐一个批量嵌入当前文件夹下所有MP4和同名SRT文件的bat:</p><figure class="highlight bat"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bat"><span class="hljs-keyword">for</span> <span class="hljs-variable">%%a</span> <span class="hljs-keyword">in</span> (&quot;*.mp4&quot;) <span class="hljs-keyword">do</span> ffmpeg -i <span class="hljs-variable">%%~</span>na.mp4 -i <span class="hljs-variable">%%~</span>na.srt -vcodec <span class="hljs-built_in">copy</span> -acodec <span class="hljs-built_in">copy</span> <span class="hljs-variable">%%~</span>na.mkv<br></code></pre></td></tr></table></figure><h3 id="硬字幕-内嵌字幕">硬字幕 (内嵌字幕)</h3><p>内嵌字幕就是直接把字幕合并死到视频里</p><p>可以通过</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">ffmpeg -i input.mp4 -vf &quot;subtitles=subtitle.srt&quot; output.mp4<br></code></pre></td></tr></table></figure><p>来操作</p><p>如果视频本身就是mkv的, 可以:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">ffmpeg -i input.mkv -vf subtitles=input.mkv output.mp4<br></code></pre></td></tr></table></figure><p>来操作</p><p>注意上传到b站等视频平台最好直接硬字幕嵌入再上传.</p><p>或者直接上传源视频文件和srt文件(不过b站连<code>&lt;br&gt;</code>都解析不出来, 太菜了)</p><h2 id="参考">参考</h2><ol><li><a href="https://crifan.github.io/media_process_ffmpeg/website/subtitle/embed/">https://crifan.github.io/media_process_ffmpeg/website/subtitle/embed/</a></li><li><a href="https://www.hash070.top/archives/youtube-video-download.html">https://www.hash070.top/archives/youtube-video-download.html</a></li><li><a href="https://zhuanlan.zhihu.com/p/422397038">https://zhuanlan.zhihu.com/p/422397038</a></li></ol>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E7%94%9F%E6%B4%BB/">生活</category>
      
      
      <category domain="https://moreality.net/tags/youtube/">youtube</category>
      
      <category domain="https://moreality.net/tags/%E8%A7%86%E9%A2%91%E4%B8%8B%E8%BD%BD/">视频下载</category>
      
      <category domain="https://moreality.net/tags/%E6%90%AC%E8%BF%90/">搬运</category>
      
      
      <comments>https://moreality.net/posts/40450/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>JS与ES6的查漏补缺(下)</title>
      <link>https://moreality.net/posts/31906/</link>
      <guid>https://moreality.net/posts/31906/</guid>
      <pubDate>Fri, 19 Aug 2022 07:52:37 GMT</pubDate>
      
      <description>接触js也有一年多时间了, 假期快过完了, 趁着还有时间详细读一下一下js和es6的手册, 记录一下还没有详细了解的东西</description>
      
      
      
      <content:encoded><![CDATA[<div class="note note-info">            <p>上篇见: <a href="https://moreality.net/posts/9018/">JS与ES6的查漏补缺(上)</a></p><p>这部分主要记录ES6部分的内容</p>          </div><h2 id="1-解构赋值">1 | 解构赋值</h2><p>对象的解构赋值变量名和属性名不一致的情况:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">let</span> &#123; <span class="hljs-attr">foo</span>: baz &#125; = &#123; <span class="hljs-attr">foo</span>: <span class="hljs-string">&#x27;aaa&#x27;</span>, <span class="hljs-attr">bar</span>: <span class="hljs-string">&#x27;bbb&#x27;</span> &#125;;<br>baz <span class="hljs-comment">// &quot;aaa&quot;</span><br><br><span class="hljs-keyword">let</span> obj = &#123; <span class="hljs-attr">first</span>: <span class="hljs-string">&#x27;hello&#x27;</span>, <span class="hljs-attr">last</span>: <span class="hljs-string">&#x27;world&#x27;</span> &#125;;<br><span class="hljs-keyword">let</span> &#123; <span class="hljs-attr">first</span>: f, <span class="hljs-attr">last</span>: l &#125; = obj;<br>f <span class="hljs-comment">// &#x27;hello&#x27;</span><br>l <span class="hljs-comment">// &#x27;world&#x27;</span><br></code></pre></td></tr></table></figure><p>解构的默认值:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> &#123;x, y = <span class="hljs-number">5</span>&#125; = &#123;<span class="hljs-attr">x</span>: <span class="hljs-number">1</span>&#125;;<br>x <span class="hljs-comment">// 1</span><br>y <span class="hljs-comment">// 5</span><br><br><span class="hljs-keyword">var</span> &#123;<span class="hljs-attr">x</span>: y = <span class="hljs-number">3</span>&#125; = &#123;<span class="hljs-attr">x</span>: <span class="hljs-number">5</span>&#125;;<br>y <span class="hljs-comment">// 5</span><br></code></pre></td></tr></table></figure><h2 id="2-字符串扩展">2 | 字符串扩展</h2><p>ES6新增了一些有用的字符串方法:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">let</span> s = <span class="hljs-string">&#x27;Hello world!&#x27;</span>;<br>s.<span class="hljs-title function_">startsWith</span>(<span class="hljs-string">&#x27;Hello&#x27;</span>) <span class="hljs-comment">// true</span><br>s.<span class="hljs-title function_">endsWith</span>(<span class="hljs-string">&#x27;!&#x27;</span>) <span class="hljs-comment">// true</span><br>s.<span class="hljs-title function_">includes</span>(<span class="hljs-string">&#x27;o&#x27;</span>) <span class="hljs-comment">// true</span><br><span class="hljs-string">&#x27;hello&#x27;</span>.<span class="hljs-title function_">repeat</span>(<span class="hljs-number">2</span>) <span class="hljs-comment">// &quot;hellohello&quot;</span><br><span class="hljs-string">&#x27;x&#x27;</span>.<span class="hljs-title function_">padStart</span>(<span class="hljs-number">5</span>, <span class="hljs-string">&#x27;ab&#x27;</span>) <span class="hljs-comment">// &#x27;ababx&#x27;, 第一个参数是总长度</span><br><span class="hljs-string">&#x27;x&#x27;</span>.<span class="hljs-title function_">padStart</span>(<span class="hljs-number">4</span>, <span class="hljs-string">&#x27;ab&#x27;</span>) <span class="hljs-comment">// &#x27;abax&#x27;</span><br><br><span class="hljs-string">&#x27;x&#x27;</span>.<span class="hljs-title function_">padEnd</span>(<span class="hljs-number">5</span>, <span class="hljs-string">&#x27;ab&#x27;</span>) <span class="hljs-comment">// &#x27;xabab&#x27;</span><br><span class="hljs-string">&#x27;x&#x27;</span>.<span class="hljs-title function_">padEnd</span>(<span class="hljs-number">4</span>, <span class="hljs-string">&#x27;ab&#x27;</span>) <span class="hljs-comment">// &#x27;xaba&#x27;</span><br><br><span class="hljs-keyword">const</span> s = <span class="hljs-string">&#x27;  abc  &#x27;</span>;<br><br>s.<span class="hljs-title function_">trim</span>() <span class="hljs-comment">// &quot;abc&quot;</span><br>s.<span class="hljs-title function_">trimStart</span>() <span class="hljs-comment">// &quot;abc  &quot;</span><br>s.<span class="hljs-title function_">trimEnd</span>() <span class="hljs-comment">// &quot;  abc&quot;</span><br><br><span class="hljs-comment">// 用于替换g修饰符的replaceAll, 下面两个等效</span><br><span class="hljs-string">&#x27;aabbcc&#x27;</span>.<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/b/g</span>, <span class="hljs-string">&#x27;_&#x27;</span>)<br><span class="hljs-string">&#x27;aabbcc&#x27;</span>.<span class="hljs-title function_">replaceAll</span>(<span class="hljs-string">&#x27;b&#x27;</span>, <span class="hljs-string">&#x27;_&#x27;</span>)<br><span class="hljs-comment">// &#x27;aa__cc&#x27;</span><br><br><span class="hljs-keyword">const</span> str = <span class="hljs-string">&#x27;hello&#x27;</span>;<br>str.<span class="hljs-title function_">at</span>(<span class="hljs-number">1</span>) <span class="hljs-comment">// &quot;e&quot;</span><br></code></pre></td></tr></table></figure><h2 id="3-正则表达式">3 | 正则表达式</h2><h3 id="u修饰符">u修饰符</h3><p>点（<code>.</code>）字符在正则表达式中，含义是除了换行符以外的任意单个字符。对于码点大于<code>0xFFFF</code>的 Unicode 字符，点字符不能识别，必须加上<code>u</code>修饰符。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> s = <span class="hljs-string">&#x27;𠮷&#x27;</span>;<br><br><span class="hljs-regexp">/^.$/</span>.<span class="hljs-title function_">test</span>(s) <span class="hljs-comment">// false</span><br>/^.<span class="hljs-property">$</span>/u.<span class="hljs-title function_">test</span>(s) <span class="hljs-comment">// true</span><br></code></pre></td></tr></table></figure><p>上面代码表示，如果不添加<code>u</code>修饰符，正则表达式就会认为字符串为两个字符，从而匹配失败。</p><h3 id="y修饰符">y修饰符</h3><p>表示&quot;黏连&quot;, <code>g</code>修饰符在多次匹配表示从子字符串任意位置都可以, 而<code>y</code>表示必须从第一个</p><h3 id="d修饰符">d修饰符</h3><p>可以匹配每个组的开始和结束位置 (左闭右开)</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">const</span> text = <span class="hljs-string">&#x27;zabbcdef&#x27;</span>;<br><span class="hljs-keyword">const</span> re = <span class="hljs-regexp">/ab+(cd)/</span>d;<br><span class="hljs-keyword">const</span> result = re.<span class="hljs-title function_">exec</span>(text);<br><br>result.<span class="hljs-property">indices</span> <span class="hljs-comment">// [ [ 1, 6 ], [ 4, 6 ] ]</span><br></code></pre></td></tr></table></figure>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E5%89%8D%E7%AB%AF%E5%AD%A6%E4%B9%A0/">前端学习</category>
      
      
      <category domain="https://moreality.net/tags/javascript/">javascript</category>
      
      <category domain="https://moreality.net/tags/es6/">es6</category>
      
      <category domain="https://moreality.net/tags/%E5%89%8D%E7%AB%AF/">前端</category>
      
      <category domain="https://moreality.net/tags/typescript/">typescript</category>
      
      
      <comments>https://moreality.net/posts/31906/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>PWA应用与Service Worker</title>
      <link>https://moreality.net/posts/20684/</link>
      <guid>https://moreality.net/posts/20684/</guid>
      <pubDate>Thu, 18 Aug 2022 15:01:40 GMT</pubDate>
      
      <description>无网络也可以访问的web应用是什么体验, 将之前写的一个小工具站PWA化</description>
      
      
      
      <content:encoded><![CDATA[<p><a class="btn" href="https://tools.roccoshi.top" title="github">体验我的PWA版小工具~</a></p><h2 id="什么是PWA应用">什么是PWA应用</h2><p><strong>PWA:</strong> Progressive Web Apps，渐进式 Web 应用</p><p>简单来说, 就是一种<strong>允许用户将web页面作为应用程序安装的技术。</strong></p><blockquote><p>一方面，Web 应用更加易于发现：相比于安装应用，访问一个网站显然更加容易和迅速。你还可以通过链接来分享 Web 应用。</p><p>另一方面，原生应用与操作系统可以更加完美的整合，也因此为用户提供了无缝的用户体验。你可以通过安装应用使得它在离线的状态下也可以运行；相较于使用浏览器访问，用户也更喜欢通过点击主页上的图标来访问它们喜爱的应用。<sup id="fnref:2" class="footnote-ref"><a href="#fn:2" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps/Introduction">[2]</span></a></sup></p></blockquote><p>当你打开一个网站时, 最能直观展示他是不是PWA的就是看chrome会不会弹出一个&quot;安装&quot;按钮</p><h2 id="什么是Service-Worker">什么是Service Worker</h2><blockquote><p>Service Worker 是浏览器和网络之间的虚拟代理。 它们终于解决了前端开发人员多年来一直在努力解决的一些问题，其中最值得关注的是，解决了如何正确缓存网站资源并使其在离线时可用的问题。</p><p>Service Worker 运行在一个与页面 JavaScript 主线程独立的线程上，并且无权访问 DOM 结构。这引入了一种与传统 Web 编程不同的方式：它的 API 是非阻塞的，并且可以在不同的上下文之间发送和接收信息。您可分配给 Service Worker 一些任务，并通过基于 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promise</a> 的方法在任务完成时收到结果。</p><p>它不仅仅提供离线功能，还可以做包括处理通知、在单独的线程上执行繁重的计算等事务。Service workers 非常强大，因为他们可以控制网络请求、修改网络请求、返回缓存的自定义响应，或者合成响应。<sup id="fnref:1" class="footnote-ref"><a href="#fn:1" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps/Offline_Service_workers">[1]</span></a></sup></p></blockquote><p>简单来说, Service Worker就是一个位于浏览器和互联网间的代理, 可以拦截, 处理, 缓存HTTP请求, 同时可以实现<strong>通知推送</strong>和<strong>离线缓存</strong></p><h3 id="一个详细的angular实现service-worker通知的教程">一个详细的angular实现service worker通知的教程</h3><p><a href="https://blog.angular-university.io/angular-push-notifications/">https://blog.angular-university.io/angular-push-notifications/</a></p><h2 id="Angular创建一个pwa应用">Angular创建一个pwa应用</h2><p>之前一段时间我写了一些前端的小demo并作为一个工具合集放在一个网站里:</p><p><a href="https://tools.roccoshi.top">https://tools.roccoshi.top</a></p><p>下面展示一下以Angular为例, 创建一个pwa应用的过程, react和vue框架使用的更加广泛, 应该也有相应的配置</p><div class="note note-info">            <p>以下内容主要参考angular官方文档, 加上个人的一些实践的理解和踩坑, 内容可能会因angular版本不同而有所不一致, 可参考: <a href="https://angular.cn/guide/service-worker-getting-started">https://angular.cn/guide/service-worker-getting-started</a></p>          </div><h3 id="安装angular-pwa">安装angular/pwa</h3><p>注意pwa一定要安装对应angular-cli版本的</p><p>具体可以参考<a href="https://www.npmjs.com/package/@angular/pwa">https://www.npmjs.com/package/@angular/pwa</a></p><p>选择对应tag的版本, 如我的angular-cli是v9, 则使用下面的方式安装:</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cmd">npx ng add @angular/pwa@<span class="hljs-number">0</span>.<span class="hljs-number">901</span>.<span class="hljs-number">15</span> --project &quot;tiny-tools&quot;<br></code></pre></td></tr></table></figure><p>这一步会进行如下操作:</p><blockquote><ol><li>把 @angular/service-worker 添加到你的项目中。</li><li>在 CLI 中启用 Service Worker 的构建支持。</li><li>在应用模块中导入并注册 Service Worker。</li><li>修改 <code>index.html</code> 文件：<ul><li>包含要添加到 <code>manifest.webmanifest</code> 文件中的链接。</li><li>为 <code>theme-color</code> 添加 meta 标签。</li></ul></li><li>创建图标文件，以支持安装渐进式应用（PWA）。</li><li>创建一个名叫 <a href="https://angular.cn/guide/service-worker-config"><code>ngsw-config.json</code></a> 的 Service Worker 配置文件，它会用来指定缓存的行为以及其它设定。<sup id="fnref:4" class="footnote-ref"><a href="#fn:4" rel="footnote"><span class="hint--top hint--rounded" aria-label="https://angular.cn/guide/service-worker-getting-started">[4]</span></a></sup></li></ol></blockquote><p>我们观察<code>nsgw-config.json</code>:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;$schema&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;./node_modules/@angular/service-worker/config/schema.json&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;index&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/index.html&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;assetGroups&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>    <span class="hljs-punctuation">&#123;</span><br>      <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;app&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;installMode&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;prefetch&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;resources&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>        <span class="hljs-attr">&quot;files&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>          <span class="hljs-string">&quot;/favicon.ico&quot;</span><span class="hljs-punctuation">,</span><br>          <span class="hljs-string">&quot;/index.html&quot;</span><span class="hljs-punctuation">,</span><br>          <span class="hljs-string">&quot;/manifest.webmanifest&quot;</span><span class="hljs-punctuation">,</span><br>          <span class="hljs-string">&quot;/*.css&quot;</span><span class="hljs-punctuation">,</span><br>          <span class="hljs-string">&quot;/*.js&quot;</span><br>        <span class="hljs-punctuation">]</span><br>      <span class="hljs-punctuation">&#125;</span><br>    <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span> <span class="hljs-punctuation">&#123;</span><br>      <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;assets&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;installMode&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;lazy&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;updateMode&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;prefetch&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;resources&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>        <span class="hljs-attr">&quot;files&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>          <span class="hljs-string">&quot;/assets/**&quot;</span><span class="hljs-punctuation">,</span><br>          <span class="hljs-string">&quot;/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)&quot;</span><br>        <span class="hljs-punctuation">]</span><br>      <span class="hljs-punctuation">&#125;</span><br>    <span class="hljs-punctuation">&#125;</span><br>  <span class="hljs-punctuation">]</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>这是一个指定缓存的配置文件, 该文件表示浏览器将离线缓存这些内容:</p><blockquote><ul><li><code>index.html</code></li><li><code>favicon.ico</code></li><li>构建结果（JS 和 CSS 包）</li><li><code>assets</code> 下的一切</li><li>图片和字体直接位于所配置的 <code>outputPath</code> (默认为 <code>./dist/&lt;project-name&gt;/</code>) 或 <code>resourcesOutputPath</code> 下。关于这些配置的更多信息，请参阅 <a href="https://angular.cn/cli/build"><code>ng build</code></a>。</li></ul></blockquote><p>到这里, 我们就可以重新<code>ng build</code>构建我们的项目, 注意:</p><p>**<code>ng serve</code>**并不会使项目工作在service worker模式下, 需要使用一个独立的http服务器在本地测试, 可以使用npm的<code>http-server</code>包:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">http-server -p 8080 -c-1 dist/&lt;project-name&gt;<br></code></pre></td></tr></table></figure><p>这时就可以在本地愉快地测试你的pwa应用了！</p><h3 id="发布">发布</h3><p>我的 <a href="https://tools.roccoshi.top/">https://tools.roccoshi.top/</a> 基于github pages, 这里给出一个发布的参考</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs cmd"># 这一步是构建应用, 注意base-href表示你需要发布网站的url地址<br><span class="hljs-keyword">call</span> npx ng build --prod --base-href &quot;https://tools.roccoshi.top/&quot;<br># 这一步是为了将github pages自定义域名所需的CNAME文件加入到构建文件夹中<br><span class="hljs-keyword">call</span> <span class="hljs-built_in">echo</span> tools.roccoshi.top &gt; ./dist/tiny-tools/browser/CNAME<br># 使用gh-pages发布, 需要提前npm install gh-pages<br><span class="hljs-keyword">call</span> npx gh-pages -d dist/tiny-tools/browser<br><span class="hljs-built_in">pause</span><br></code></pre></td></tr></table></figure><p>这里注意构建的产物不一定在<code>dist/&lt;project-name&gt;/browser</code>中, 这里是因为我设置了<code>app-shell</code>, 如果没有使用<code>app-shell</code>构建的产物应该就在<code>dist/&lt;project-name&gt;</code>中</p><p>等github action构建完毕后打开页面:</p><p>就可以发现支持安装啦~</p><p>可以直接在windows菜单栏打开, 也可以拖到桌面之类的, 本质还是一个浏览器页面</p><p>如果你使用edge, 还可以在设置 =&gt; 应用</p><p>管理安装的PWA应用程序</p><h3 id="解决刷新后404的问题">解决刷新后404的问题</h3><p>上面的发布我遇到了一个棘手的问题, 导致卡了很久, 这个问题来源于浏览器的两种路由模式:</p><ul><li>hash(#)模式</li><li>history模式</li></ul><p>hash模式的url大概长这样: <code>https://abc.com/#/page</code></p><p>history模式的url大概长这样: <code>https://abc.com/page</code></p><p>angular默认使用的路由模式就是history模式, 但是在pwa中会导致一个问题:</p><p><strong>如果你直接基于路由访问一个页面, 而不是先访问主页, 会404错误</strong>:</p><blockquote><p>比如你直接访问<code>https://tools.roccoshi.top/code-comparison</code>而不是由<code>https://tools.roccoshi.top</code>导航而来, 会直接报404</p></blockquote><p>我找到的相关的issue:</p><ul><li><a href="https://github.com/angular/angular/issues/22023">https://github.com/angular/angular/issues/22023</a></li><li><a href="https://stackoverflow.com/questions/59580289/angular-8-angular-pwa-routing-doesnt-work-with-direct-url">https://stackoverflow.com/questions/59580289/angular-8-angular-pwa-routing-doesnt-work-with-direct-url</a></li></ul><div class="note note-danger">            <p>目前我也不清楚原因具体出在哪, 我觉得并不是路由模式的问题, 应该是哪个配置项出错了.</p>          </div><p>不清楚原因, 而且也极难搜到完全正确的解决方式, 我怀疑是路由或PWA配置的某个地方出现了问题, 最终尝试换成hash模式的路由, 也就是将:</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-title class_">RouterModule</span>.<span class="hljs-title function_">forRoot</span>(routes),<br></code></pre></td></tr></table></figure><p>更换为</p><figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs typescript"><span class="hljs-title class_">RouterModule</span>.<span class="hljs-title function_">forRoot</span>(routes, &#123;<span class="hljs-attr">useHash</span>: <span class="hljs-literal">true</span>&#125;),<br></code></pre></td></tr></table></figure><p>就可以正常直接访问子路由页面了</p><h2 id="关于PWA应用的看法">关于PWA应用的看法</h2><p>这篇文章: <a href="https://segmentfault.com/a/1190000041729491">小程序鼻祖 —— 在国内逐渐消亡的 PWA 可以带给我们哪些启示？ - SegmentFault 思否</a>我觉得说的很好</p><blockquote><p>由于中国的特殊性，PWA 的前景在一定程度上比较悲观：</p><ul><li>国内较重视 iOS，而 iOS 目前还不支持 PWA。</li><li>国内的 Android 实为「安卓」，不自带 Chrome 是一，可能还会有其他兼容问题。</li><li>国内厂商可能并不会像三星那样对推动自家浏览器支持 PWA 那么感兴趣。</li><li>依赖 GCM 推送的通知不可用，Web Push Protocol 还没有国内的推送服务实现。</li><li>国内 webview 环境较为复杂（比如微信），黑科技比较多。</li></ul></blockquote><p>由于浏览器的限制, 兼容性的问题, 以及国内移动网络的高度普及化, PWA在我国市场注定是不会再火起来了, 所以这篇文章纯属了解和娱乐, 具体的技术栈我也不打算深入研究了~</p><h2 id="参考">参考</h2><section class="footnotes"><div class="footnote-list"><ol><li><span id="fn:1" class="footnote-text"><span><a href="https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps/Offline_Service_workers">https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps/Offline_Service_workers</a><a href="#fnref:1" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:2" class="footnote-text"><span><a href="https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps/Introduction">https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps/Introduction</a><a href="#fnref:2" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:3" class="footnote-text"><span><a href="https://segmentfault.com/a/1190000041729491">https://segmentfault.com/a/1190000041729491</a><a href="#fnref:3" rev="footnote" class="footnote-backref"> ↩</a></span></span></li><li><span id="fn:4" class="footnote-text"><span><a href="https://angular.cn/guide/service-worker-getting-started">https://angular.cn/guide/service-worker-getting-started</a><a href="#fnref:4" rev="footnote" class="footnote-backref"> ↩</a></span></span></li></ol></div></section>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E5%89%8D%E7%AB%AF%E5%AD%A6%E4%B9%A0/">前端学习</category>
      
      
      <category domain="https://moreality.net/tags/Service-Worker/">Service Worker</category>
      
      <category domain="https://moreality.net/tags/PWA/">PWA</category>
      
      <category domain="https://moreality.net/tags/%E6%B8%90%E8%BF%9B%E5%BC%8F%E5%BA%94%E7%94%A8/">渐进式应用</category>
      
      
      <comments>https://moreality.net/posts/20684/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>JS与ES6的查漏补缺(上)</title>
      <link>https://moreality.net/posts/9018/</link>
      <guid>https://moreality.net/posts/9018/</guid>
      <pubDate>Wed, 17 Aug 2022 02:00:01 GMT</pubDate>
      
      <description>接触js也有一年多时间了, 假期快过完了, 趁着还有时间详细读一下一下js和es6的手册, 记录一下还没有详细了解的东西</description>
      
      
      
      <content:encoded><![CDATA[<div class="note note-info">            <p>参考主要来源于两篇中文教程:</p><p>JS: <a href="https://wangdoc.com/javascript/index.html">网道: JavaScript教程</a></p><p>ES6: <a href="https://es6.ruanyifeng.com/">阮一峰: ECMAScript6入门</a></p><p>这里记录的不是所有的用法和特性, 而是我认为我不是特别清楚的和我认为以后也许会碰上的</p>          </div><h2 id="JavaScript部分">JavaScript部分</h2><h3 id="1-类型判断">1 | 类型判断</h3><p>可以用:</p><ul><li>运算符: <code>typeof</code>, <code>instanceof</code></li><li>方法: <code>Object.prototype.toString</code></li></ul><p>确定一个值的类型</p><h4 id="typeof">typeof</h4><p>typeof的种类: <code>number</code>, <code>string</code>, <code>boolean</code>, <code>undefined</code>, <code>function</code>, <code>object</code> |(少见:  <code>symbol</code>, <code>bigint</code>)</p><div class="note note-warning">            <p>注意<code>typeof null === &quot;object&quot;</code></p><blockquote><p>这是由于历史原因造成的。1995年的 JavaScript 语言第一版，只设计了五种数据类型（对象、整数、浮点数、字符串和布尔值），没考虑<code>null</code>，只把它当作<code>object</code>的一种特殊值。后来<code>null</code>独立出来，作为一种单独的数据类型，为了兼容以前的代码，<code>typeof null</code>返回<code>object</code>就没法改变了。</p></blockquote>          </div><h4 id="instanceof">instanceof</h4><p><code>instanceof</code>运算符返回一个布尔值，表示对象是否为某个构造函数的实例。</p><p><code>instanceof</code>只能用于对象:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> x = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>];<br><span class="hljs-keyword">var</span> y = &#123;&#125;;<br>x <span class="hljs-keyword">instanceof</span> <span class="hljs-title class_">Array</span> <span class="hljs-comment">// true</span><br>y <span class="hljs-keyword">instanceof</span> <span class="hljs-title class_">Object</span> <span class="hljs-comment">// true</span><br></code></pre></td></tr></table></figure><p>不能用于原始的值</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> s = <span class="hljs-string">&#x27;hello&#x27;</span>;<br>s <span class="hljs-keyword">instanceof</span> <span class="hljs-title class_">String</span> <span class="hljs-comment">// false</span><br></code></pre></td></tr></table></figure><p>任何对象, <strong>除了null</strong>都是<code>instancef Object</code>:</p><p>对于null和undefined:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">typeof</span> <span class="hljs-literal">null</span>; <span class="hljs-comment">// &quot;object&quot;</span><br><span class="hljs-keyword">typeof</span> <span class="hljs-literal">undefined</span>; <span class="hljs-comment">// &quot;undefined&quot;</span><br><span class="hljs-literal">undefined</span> <span class="hljs-keyword">instanceof</span> <span class="hljs-title class_">Object</span> <span class="hljs-comment">// false</span><br><span class="hljs-literal">null</span> <span class="hljs-keyword">instanceof</span> <span class="hljs-title class_">Object</span> <span class="hljs-comment">// false</span><br></code></pre></td></tr></table></figure><h4 id="Object-prototype-toString">Object.prototype.toString</h4><p><code>Object.prototype.toString</code>方法返回对象的类型字符串，因此可以用来判断一个值的类型。</p><ul><li>数值：返回<code>[object Number]</code>。</li><li>字符串：返回<code>[object String]</code>。</li><li>布尔值：返回<code>[object Boolean]</code>。</li><li>undefined：返回<code>[object Undefined]</code>。</li><li>null：返回<code>[object Null]</code>。</li><li>数组：返回<code>[object Array]</code>。</li><li>arguments 对象：返回<code>[object Arguments]</code>。</li><li>函数：返回<code>[object Function]</code>。</li><li>Error 对象：返回<code>[object Error]</code>。</li><li>Date 对象：返回<code>[object Date]</code>。</li><li>RegExp 对象：返回<code>[object RegExp]</code>。</li><li>其他对象：返回<code>[object Object]</code></li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-title class_">Object</span>.<span class="hljs-property"><span class="hljs-keyword">prototype</span></span>.<span class="hljs-property">toString</span>.<span class="hljs-title function_">call</span>(<span class="hljs-number">2</span>) <span class="hljs-comment">// &quot;[object Number]&quot;</span><br><span class="hljs-title class_">Object</span>.<span class="hljs-property"><span class="hljs-keyword">prototype</span></span>.<span class="hljs-property">toString</span>.<span class="hljs-title function_">call</span>(<span class="hljs-string">&#x27;&#x27;</span>) <span class="hljs-comment">// &quot;[object String]&quot;</span><br><span class="hljs-title class_">Object</span>.<span class="hljs-property"><span class="hljs-keyword">prototype</span></span>.<span class="hljs-property">toString</span>.<span class="hljs-title function_">call</span>(<span class="hljs-literal">true</span>) <span class="hljs-comment">// &quot;[object Boolean]&quot;</span><br><span class="hljs-title class_">Object</span>.<span class="hljs-property"><span class="hljs-keyword">prototype</span></span>.<span class="hljs-property">toString</span>.<span class="hljs-title function_">call</span>(<span class="hljs-literal">undefined</span>) <span class="hljs-comment">// &quot;[object Undefined]&quot;</span><br><span class="hljs-title class_">Object</span>.<span class="hljs-property"><span class="hljs-keyword">prototype</span></span>.<span class="hljs-property">toString</span>.<span class="hljs-title function_">call</span>(<span class="hljs-literal">null</span>) <span class="hljs-comment">// &quot;[object Null]&quot;</span><br><span class="hljs-title class_">Object</span>.<span class="hljs-property"><span class="hljs-keyword">prototype</span></span>.<span class="hljs-property">toString</span>.<span class="hljs-title function_">call</span>(<span class="hljs-title class_">Math</span>) <span class="hljs-comment">// &quot;[object Math]&quot;</span><br><span class="hljs-title class_">Object</span>.<span class="hljs-property"><span class="hljs-keyword">prototype</span></span>.<span class="hljs-property">toString</span>.<span class="hljs-title function_">call</span>(&#123;&#125;) <span class="hljs-comment">// &quot;[object Object]&quot;</span><br><span class="hljs-title class_">Object</span>.<span class="hljs-property"><span class="hljs-keyword">prototype</span></span>.<span class="hljs-property">toString</span>.<span class="hljs-title function_">call</span>([]) <span class="hljs-comment">// &quot;[object Array]&quot;</span><br></code></pre></td></tr></table></figure><h3 id="2-比较">2 | 比较</h3><h4 id="字符串的比较">字符串的比较</h4><p>按照字典序依次比较字符的Unicode码点大小</p><h4 id="原始类型且有非字符串的比较">原始类型且有非字符串的比较</h4><p>先<code>Number()</code>转化成数值再比较</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-number">5</span> &gt; <span class="hljs-string">&#x27;4&#x27;</span> <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 等同于 5 &gt; Number(&#x27;4&#x27;)</span><br><span class="hljs-comment">// 即 5 &gt; 4</span><br><br><span class="hljs-literal">true</span> &gt; <span class="hljs-literal">false</span> <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 等同于 Number(true) &gt; Number(false)</span><br><span class="hljs-comment">// 即 1 &gt; 0</span><br><br><span class="hljs-number">2</span> &gt; <span class="hljs-literal">true</span> <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 等同于 2 &gt; Number(true)</span><br><span class="hljs-comment">// 即 2 &gt; 1</span><br></code></pre></td></tr></table></figure><div class="note note-warning">            <p>NaN与谁比较都是<code>false</code></p>          </div><h4 id="和对象的比较">和对象的比较</h4><p>会先将对象转换为<code>valueOf()</code>对应的值再进行比较, 若<code>valuOf</code>返回的还是对象则调用<code>toString</code>方法</p><blockquote><p>注意: 这个规则在对象的&quot;加法&quot;中仍适用</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> x = [<span class="hljs-number">2</span>];<br>x &gt; <span class="hljs-string">&#x27;11&#x27;</span> <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 等同于 [2].valueOf().toString() &gt; &#x27;11&#x27;</span><br><span class="hljs-comment">// 即 &#x27;2&#x27; &gt; &#x27;11&#x27;</span><br><br>x.<span class="hljs-property">valueOf</span> = <span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) &#123; <span class="hljs-keyword">return</span> <span class="hljs-string">&#x27;1&#x27;</span> &#125;;<br>x &gt; <span class="hljs-string">&#x27;11&#x27;</span> <span class="hljs-comment">// false</span><br><span class="hljs-comment">// 等同于 [2].valueOf() &gt; &#x27;11&#x27;</span><br><span class="hljs-comment">// 即 &#x27;1&#x27; &gt; &#x27;11&#x27;</span><br><br>[<span class="hljs-number">2</span>] &gt; [<span class="hljs-number">1</span>] <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 等同于 [2].valueOf().toString() &gt; [1].valueOf().toString()</span><br><span class="hljs-comment">// 即 &#x27;2&#x27; &gt; &#x27;1&#x27;</span><br><br>[<span class="hljs-number">2</span>] &gt; [<span class="hljs-number">11</span>] <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 等同于 [2].valueOf().toString() &gt; [11].valueOf().toString()</span><br><span class="hljs-comment">// 即 &#x27;2&#x27; &gt; &#x27;11&#x27;</span><br><br>&#123; <span class="hljs-attr">x</span>: <span class="hljs-number">2</span> &#125; &gt;= &#123; <span class="hljs-attr">x</span>: <span class="hljs-number">1</span> &#125; <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 等同于 &#123; x: 2 &#125;.valueOf().toString() &gt;= &#123; x: 1 &#125;.valueOf().toString()</span><br><span class="hljs-comment">// 即 &#x27;[object Object]&#x27; &gt;= &#x27;[object Object]&#x27;</span><br></code></pre></td></tr></table></figure><h4 id="比较"><code>===</code>比较</h4><ul><li>对象比较地址</li><li><code>undefined === undefined</code>, <code>null === null</code></li><li>原始类型只要保证类型相等 + 值相等即可, 如<code>+0 === -0</code>, <code>1 === 0x1</code></li><li>NaN和自身不相等</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-title class_">NaN</span> === <span class="hljs-title class_">NaN</span> <span class="hljs-comment">// false</span><br></code></pre></td></tr></table></figure><h4 id="比较-2"><code>==</code>比较</h4><p>相同类型数据等价于<code>===</code></p><p>不同类型数据会转化为<code>Number</code>后进行比较</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-number">1</span> == <span class="hljs-literal">true</span> <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 等同于 1 === Number(true)</span><br><br><span class="hljs-number">0</span> == <span class="hljs-literal">false</span> <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 等同于 0 === Number(false)</span><br><br><span class="hljs-number">2</span> == <span class="hljs-literal">true</span> <span class="hljs-comment">// false</span><br><span class="hljs-comment">// 等同于 2 === Number(true)</span><br><br><span class="hljs-number">2</span> == <span class="hljs-literal">false</span> <span class="hljs-comment">// false</span><br><span class="hljs-comment">// 等同于 2 === Number(false)</span><br><br><span class="hljs-string">&#x27;true&#x27;</span> == <span class="hljs-literal">true</span> <span class="hljs-comment">// false</span><br><span class="hljs-comment">// 等同于 Number(&#x27;true&#x27;) === Number(true)</span><br><span class="hljs-comment">// 等同于 NaN === 1</span><br><br><span class="hljs-string">&#x27;&#x27;</span> == <span class="hljs-number">0</span> <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 等同于 Number(&#x27;&#x27;) === 0</span><br><span class="hljs-comment">// 等同于 0 === 0</span><br><br><span class="hljs-string">&#x27;&#x27;</span> == <span class="hljs-literal">false</span>  <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 等同于 Number(&#x27;&#x27;) === Number(false)</span><br><span class="hljs-comment">// 等同于 0 === 0</span><br><br><span class="hljs-string">&#x27;1&#x27;</span> == <span class="hljs-literal">true</span>  <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 等同于 Number(&#x27;1&#x27;) === Number(true)</span><br><span class="hljs-comment">// 等同于 1 === 1</span><br><br><span class="hljs-string">&#x27;\n  123  \t&#x27;</span> == <span class="hljs-number">123</span> <span class="hljs-comment">// true</span><br><span class="hljs-comment">// 因为字符串转为数字时，省略前置和后置的空</span><br></code></pre></td></tr></table></figure><ul><li>对于对象和原始类型的比较, 现将对象转化为原始类型的值再进行比较</li><li>对象和对象的比较先调用对象的<code>valueOf()</code>方法, 比不成, 再调用<code>toString()</code></li></ul><h3 id="3-一些特殊运算符">3 | 一些特殊运算符</h3><ul><li><code>&gt;&gt;&gt;</code>: 逻辑右移(左边补0), <code>&gt;&gt;</code>是算数右移, 需要考虑符号位</li><li><code>,</code>: 逗号运算符, 对两个表达式求值并返回后一个的值</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> value = (<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">&#x27;Hi!&#x27;</span>), <span class="hljs-literal">true</span>);<br><span class="hljs-comment">// Hi!</span><br><br>value <span class="hljs-comment">// true</span><br></code></pre></td></tr></table></figure><ul><li><p><code>void</code>: <code>void</code>运算符的作用是执行一个表达式, 返回<code>undefined</code>。</p><ul><li><blockquote><p>这个运算符的主要用途是浏览器的书签工具（Bookmarklet），以及在超级链接中插入代码防止网页跳转。</p></blockquote></li></ul></li></ul><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs html"><span class="hljs-comment">&lt;!-- 用户点击链接提交表单但是不产生页面跳转的方式 --&gt;</span><br><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">&quot;javascript: void(document.form.submit())&quot;</span>&gt;</span><br>  提交<br><span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span><br></code></pre></td></tr></table></figure><h3 id="4-parseInt-和Number">4 | parseInt()和Number()</h3><blockquote><p><code>Number</code>函数将字符串转为数值，要比<code>parseInt</code>函数严格很多。基本上，只要有一个字符无法转成数值，整个字符串就会被转为<code>NaN</code>。</p></blockquote><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-built_in">parseInt</span>(<span class="hljs-string">&#x27;42 cats&#x27;</span>) <span class="hljs-comment">// 42</span><br><span class="hljs-title class_">Number</span>(<span class="hljs-string">&#x27;42 cats&#x27;</span>) <span class="hljs-comment">// NaN</span><br><br><span class="hljs-built_in">parseInt</span>([<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>]) <span class="hljs-comment">// 1</span><br><span class="hljs-title class_">Number</span>([<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>]) <span class="hljs-comment">// NaN</span><br><span class="hljs-title class_">Number</span>([<span class="hljs-number">1</span>]) <span class="hljs-comment">// 1</span><br></code></pre></td></tr></table></figure><p>原因在于Number还是经历了<code>valueOf</code>和<code>toString</code>的过程</p><h3 id="5-为false的值">5 | 为false的值</h3><ul><li><code>undefined</code></li><li><code>null</code></li><li><code>0</code></li><li><code>NaN</code></li><li><code>''</code></li></ul><h3 id="6-console">6 | console</h3><p>console可以通过log, info, debug, warn, error, table, count, dir, assert, time&amp;timeEnd打印信息</p><p><code>console.debug</code>需要开启显示级别<code>verbose</code>(详细)才会显示</p><p><code>console.warn</code> 黄色显示</p><p><code>console.error</code>红色打印错误信息并打印错误发生的堆栈</p><p><code>console.table</code>可以将复合类型数据转为表格显示</p><p><code>console.count(label)</code>可以以label为标签, 打印调用次数</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">greet</span>(<span class="hljs-params">user</span>) &#123;<br>  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">count</span>(user);<br>  <span class="hljs-keyword">return</span> <span class="hljs-string">&quot;hi &quot;</span> + user;<br>&#125;<br><br><span class="hljs-title function_">greet</span>(<span class="hljs-string">&#x27;bob&#x27;</span>)<br><span class="hljs-comment">// bob: 1</span><br><span class="hljs-comment">// &quot;hi bob&quot;</span><br><br><span class="hljs-title function_">greet</span>(<span class="hljs-string">&#x27;alice&#x27;</span>)<br><span class="hljs-comment">// alice: 1</span><br><span class="hljs-comment">// &quot;hi alice&quot;</span><br><br><span class="hljs-title function_">greet</span>(<span class="hljs-string">&#x27;bob&#x27;</span>)<br><span class="hljs-comment">// bob: 2</span><br><span class="hljs-comment">// &quot;hi bob&quot;</span><br></code></pre></td></tr></table></figure><p><code>console.dir</code>以更易于阅读的方式打印信息</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(&#123;<span class="hljs-attr">f1</span>: <span class="hljs-string">&#x27;foo&#x27;</span>, <span class="hljs-attr">f2</span>: <span class="hljs-string">&#x27;bar&#x27;</span>&#125;)<br><span class="hljs-comment">// Object &#123;f1: &quot;foo&quot;, f2: &quot;bar&quot;&#125;</span><br><br><span class="hljs-variable language_">console</span>.<span class="hljs-title function_">dir</span>(&#123;<span class="hljs-attr">f1</span>: <span class="hljs-string">&#x27;foo&#x27;</span>, <span class="hljs-attr">f2</span>: <span class="hljs-string">&#x27;bar&#x27;</span>&#125;)<br><span class="hljs-comment">// Object</span><br><span class="hljs-comment">//   f1: &quot;foo&quot;</span><br><span class="hljs-comment">//   f2: &quot;bar&quot;</span><br><span class="hljs-comment">//   __proto__: Object</span><br></code></pre></td></tr></table></figure><p><code>console.assert(expr, message)</code>条件判断</p><p><code>console.time(label)</code>和<code>console.timeEnd(label)</code>记录两次调用的时间</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-variable language_">console</span>.<span class="hljs-title function_">time</span>(<span class="hljs-string">&#x27;Array initialize&#x27;</span>);<br><br><span class="hljs-keyword">var</span> array= <span class="hljs-keyword">new</span> <span class="hljs-title class_">Array</span>(<span class="hljs-number">1000000</span>);<br><span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> i = array.<span class="hljs-property">length</span> - <span class="hljs-number">1</span>; i &gt;= <span class="hljs-number">0</span>; i--) &#123;<br>  array[i] = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Object</span>();<br>&#125;;<br><br><span class="hljs-variable language_">console</span>.<span class="hljs-title function_">timeEnd</span>(<span class="hljs-string">&#x27;Array initialize&#x27;</span>);<br><span class="hljs-comment">// Array initialize: 1914.481ms</span><br></code></pre></td></tr></table></figure><h3 id="7-Object静态方法">7 | Object静态方法</h3><p><code>Object.keys()</code>: 返回一个对象自身属性名的数组 (可枚举属性)</p><p><code>Object.getOwnPropertyNames()</code>: 返回对象自身<strong>可枚举和不可枚举</strong>的属性名数组</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> a = [<span class="hljs-string">&#x27;Hello&#x27;</span>, <span class="hljs-string">&#x27;World&#x27;</span>];<br><br><span class="hljs-title class_">Object</span>.<span class="hljs-title function_">keys</span>(a) <span class="hljs-comment">// [&quot;0&quot;, &quot;1&quot;]</span><br><span class="hljs-title class_">Object</span>.<span class="hljs-title function_">getOwnPropertyNames</span>(a) <span class="hljs-comment">// [&quot;0&quot;, &quot;1&quot;, &quot;length&quot;]</span><br></code></pre></td></tr></table></figure><h4 id="属性描述对象">属性描述对象</h4><blockquote><p>JavaScript 提供了一个内部数据结构，用来描述对象的属性，控制它的行为，比如该属性是否可写、可遍历等等。这个内部数据结构称为“属性描述对象”（attributes object）。每个属性都有自己对应的属性描述对象，保存该属性的一些元信息。</p></blockquote><p>一个属性描述对象的例子:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs js">&#123;<br>  <span class="hljs-attr">value</span>: <span class="hljs-number">123</span>, <span class="hljs-comment">// 值</span><br>  <span class="hljs-attr">writable</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// 属性值是否可改变</span><br>  <span class="hljs-attr">enumerable</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// 是否可枚举, 将影响如for...in, Object.keys等操作</span><br>  <span class="hljs-attr">configurable</span>: <span class="hljs-literal">false</span>, <span class="hljs-comment">// 控制属性描述对象的可写性, 比如能否删除该属性, 能否改变各种元属性(value除外)</span><br>  <span class="hljs-attr">get</span>: <span class="hljs-literal">undefined</span>, <span class="hljs-comment">// 取值</span><br>  <span class="hljs-attr">set</span>: <span class="hljs-literal">undefined</span> <span class="hljs-comment">// 存值</span><br>&#125;<br></code></pre></td></tr></table></figure><p><code>Object.getOwnPropertyDescriptor(object, attr)</code>可以获取属性attr的属性描述对象, (只能用于自身属性, 不能用于继承的属性)</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> obj = &#123; <span class="hljs-attr">p</span>: <span class="hljs-string">&#x27;a&#x27;</span> &#125;;<br><br><span class="hljs-title class_">Object</span>.<span class="hljs-title function_">getOwnPropertyDescriptor</span>(obj, <span class="hljs-string">&#x27;p&#x27;</span>)<br><span class="hljs-comment">// Object &#123; value: &quot;a&quot;,</span><br><span class="hljs-comment">//   writable: true,</span><br><span class="hljs-comment">//   enumerable: true,</span><br><span class="hljs-comment">//   configurable: true</span><br><span class="hljs-comment">// &#125;</span><br></code></pre></td></tr></table></figure><p><code>Object.defineProperty()</code>和<code>Object.defineProperties()</code>可以用于修改属性描述对象</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> obj = <span class="hljs-title class_">Object</span>.<span class="hljs-title function_">defineProperty</span>(&#123;&#125;, <span class="hljs-string">&#x27;p&#x27;</span>, &#123;<br>  <span class="hljs-attr">value</span>: <span class="hljs-number">123</span>,<br>  <span class="hljs-attr">writable</span>: <span class="hljs-literal">false</span>,<br>  <span class="hljs-attr">enumerable</span>: <span class="hljs-literal">true</span>,<br>  <span class="hljs-attr">configurable</span>: <span class="hljs-literal">false</span><br>&#125;);<br><br>obj.<span class="hljs-property">p</span> <span class="hljs-comment">// 123</span><br><br>obj.<span class="hljs-property">p</span> = <span class="hljs-number">246</span>;<br>obj.<span class="hljs-property">p</span> <span class="hljs-comment">// 123</span><br></code></pre></td></tr></table></figure><h3 id="8-Object实例方法">8 | Object实例方法</h3><p><code>Object</code>实例对象的方法，主要有以下六个。</p><ul><li><code>Object.prototype.valueOf()</code>：返回当前对象对应的值。</li><li><code>Object.prototype.toString()</code>：返回当前对象对应的字符串形式。</li><li><code>Object.prototype.toLocaleString()</code>：返回当前对象对应的本地字符串形式。</li><li><code>Object.prototype.hasOwnProperty()</code>：判断某个属性是否为当前对象自身的属性，还是继承自原型对象的属性。 同时, <code>in</code>这个运算符检查属性是否存在, 不区分是对象自身的还是继承的.</li><li><code>Object.prototype.isPrototypeOf()</code>：判断当前对象是否为另一个对象的原型。</li><li><code>Object.prototype.propertyIsEnumerable()</code>：判断某个属性是否可枚举。</li></ul><h3 id="9-Array">9 | Array</h3><h4 id="Array构造函数的漏洞">Array构造函数的漏洞</h4><p>Array构造函数的漏洞: <strong>不同的参数个数会导致行为不一致</strong></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-comment">// 无参数时，返回一个空数组</span><br><span class="hljs-keyword">new</span> <span class="hljs-title class_">Array</span>() <span class="hljs-comment">// []</span><br><br><span class="hljs-comment">// 单个正整数参数，表示返回的新数组的长度</span><br><span class="hljs-keyword">new</span> <span class="hljs-title class_">Array</span>(<span class="hljs-number">1</span>) <span class="hljs-comment">// [ empty ]</span><br><span class="hljs-keyword">new</span> <span class="hljs-title class_">Array</span>(<span class="hljs-number">2</span>) <span class="hljs-comment">// [ empty x 2 ]</span><br><br><span class="hljs-comment">// 非正整数的数值作为参数，会报错</span><br><span class="hljs-keyword">new</span> <span class="hljs-title class_">Array</span>(<span class="hljs-number">3.2</span>) <span class="hljs-comment">// RangeError: Invalid array length</span><br><span class="hljs-keyword">new</span> <span class="hljs-title class_">Array</span>(-<span class="hljs-number">3</span>) <span class="hljs-comment">// RangeError: Invalid array length</span><br><br><span class="hljs-comment">// 单个非数值（比如字符串、布尔值、对象等）作为参数，</span><br><span class="hljs-comment">// 则该参数是返回的新数组的成员</span><br><span class="hljs-keyword">new</span> <span class="hljs-title class_">Array</span>(<span class="hljs-string">&#x27;abc&#x27;</span>) <span class="hljs-comment">// [&#x27;abc&#x27;]</span><br><span class="hljs-keyword">new</span> <span class="hljs-title class_">Array</span>([<span class="hljs-number">1</span>]) <span class="hljs-comment">// [Array[1]]</span><br><br><span class="hljs-comment">// 多参数时，所有参数都是返回的新数组的成员</span><br><span class="hljs-keyword">new</span> <span class="hljs-title class_">Array</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>) <span class="hljs-comment">// [1, 2]</span><br><span class="hljs-keyword">new</span> <span class="hljs-title class_">Array</span>(<span class="hljs-string">&#x27;a&#x27;</span>, <span class="hljs-string">&#x27;b&#x27;</span>, <span class="hljs-string">&#x27;c&#x27;</span>) <span class="hljs-comment">// [&#x27;a&#x27;, &#x27;b&#x27;, &#x27;c&#x27;]</span><br></code></pre></td></tr></table></figure><div class="note note-warning">            <p>为弥补这一缺陷, 在ES6中引入了<code>Array.of()</code>解决了这个问题</p>          </div><h4 id="一些方法">一些方法</h4><ul><li>静态方法 <code>Array.isArray</code>可以弥补typeof运算符的不足, 识别数组 (<code>Object.prototype.toString.call</code>也可以)</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> arr = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>];<br><br><span class="hljs-keyword">typeof</span> arr <span class="hljs-comment">// &quot;object&quot;</span><br><span class="hljs-title class_">Array</span>.<span class="hljs-title function_">isArray</span>(arr) <span class="hljs-comment">// true</span><br><span class="hljs-title class_">Object</span>.<span class="hljs-property"><span class="hljs-keyword">prototype</span></span>.<span class="hljs-property">toString</span>.<span class="hljs-title function_">call</span>(arr) <span class="hljs-comment">// &#x27;[object Array]&#x27;</span><br></code></pre></td></tr></table></figure><ul><li><code>push()</code>和<code>pop()</code>返回的是<strong>操作后数组的长度</strong>, 将<strong>改变原数组</strong></li><li><code>shift()</code>将删除数组的第一个元素, 并<strong>返回该元素</strong>, 将<strong>改变原数组</strong></li><li><code>unshift()</code>将在数组的首部添加元素, 并返回<strong>添加元素后数组的长度</strong>, 将<strong>改变原数组</strong></li><li><code>join()</code>:<ul><li>默认以逗号为分隔符</li><li>如果应用于undefined或null, ]将之视为空字符串</li><li>可以通过<code>call</code>应用于字符串或<strong>类数组对象</strong></li></ul></li></ul><p>join的例子:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> a = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>];<br><br>a.<span class="hljs-title function_">join</span>(<span class="hljs-string">&#x27; &#x27;</span>) <span class="hljs-comment">// &#x27;1 2 3 4&#x27;</span><br>a.<span class="hljs-title function_">join</span>() <span class="hljs-comment">// &quot;1,2,3,4&quot;</span><br><br>[<span class="hljs-literal">undefined</span>, <span class="hljs-literal">null</span>].<span class="hljs-title function_">join</span>(<span class="hljs-string">&#x27;#&#x27;</span>)<br><span class="hljs-comment">// &#x27;#&#x27;</span><br>[<span class="hljs-string">&#x27;a&#x27;</span>,, <span class="hljs-string">&#x27;b&#x27;</span>].<span class="hljs-title function_">join</span>(<span class="hljs-string">&#x27;-&#x27;</span>)<br><span class="hljs-comment">// &#x27;a--b&#x27;</span><br><br><span class="hljs-title class_">Array</span>.<span class="hljs-property"><span class="hljs-keyword">prototype</span></span>.<span class="hljs-property">join</span>.<span class="hljs-title function_">call</span>(<span class="hljs-string">&#x27;hello&#x27;</span>, <span class="hljs-string">&#x27;-&#x27;</span>)<br><span class="hljs-comment">// &quot;h-e-l-l-o&quot;</span><br><span class="hljs-keyword">var</span> obj = &#123; <span class="hljs-number">0</span>: <span class="hljs-string">&#x27;a&#x27;</span>, <span class="hljs-number">1</span>: <span class="hljs-string">&#x27;b&#x27;</span>, <span class="hljs-attr">length</span>: <span class="hljs-number">2</span> &#125;;<br><span class="hljs-title class_">Array</span>.<span class="hljs-property"><span class="hljs-keyword">prototype</span></span>.<span class="hljs-property">join</span>.<span class="hljs-title function_">call</span>(obj, <span class="hljs-string">&#x27;-&#x27;</span>)<br><span class="hljs-comment">// &#x27;a-b&#x27;</span><br></code></pre></td></tr></table></figure><ul><li><code>concat()</code>: <strong>不改变原数组</strong>, 返回值为concat后的新数组</li><li><code>reverse()</code>: <strong>改变原数组</strong></li><li><code>slice(start, end)</code>: <strong>不改变原数组</strong><ul><li>左闭右开</li><li>支持负数, 表示倒数计算的位置</li><li>如果不指定end, 或<code>end &gt;= length</code>则表示一直到数组末尾</li></ul></li></ul><p>slice的例子:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> a = [<span class="hljs-string">&#x27;a&#x27;</span>, <span class="hljs-string">&#x27;b&#x27;</span>, <span class="hljs-string">&#x27;c&#x27;</span>];<br><br>a.<span class="hljs-title function_">slice</span>(<span class="hljs-number">0</span>) <span class="hljs-comment">// [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]</span><br>a.<span class="hljs-title function_">slice</span>(<span class="hljs-number">1</span>) <span class="hljs-comment">// [&quot;b&quot;, &quot;c&quot;]</span><br>a.<span class="hljs-title function_">slice</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>) <span class="hljs-comment">// [&quot;b&quot;]</span><br>a.<span class="hljs-title function_">slice</span>(<span class="hljs-number">2</span>, <span class="hljs-number">6</span>) <span class="hljs-comment">// [&quot;c&quot;]</span><br>a.<span class="hljs-title function_">slice</span>() <span class="hljs-comment">// [&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]</span><br></code></pre></td></tr></table></figure><ul><li><code>splice(start, count, addElement1, addElement2, ...)</code>:<ul><li><strong>改变原数组</strong></li><li>返回值为删除的元素组成的数组</li></ul></li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> a = [<span class="hljs-string">&#x27;a&#x27;</span>, <span class="hljs-string">&#x27;b&#x27;</span>, <span class="hljs-string">&#x27;c&#x27;</span>, <span class="hljs-string">&#x27;d&#x27;</span>, <span class="hljs-string">&#x27;e&#x27;</span>, <span class="hljs-string">&#x27;f&#x27;</span>];<br>a.<span class="hljs-title function_">splice</span>(<span class="hljs-number">2</span>, <span class="hljs-number">2</span>, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>); <span class="hljs-comment">// [&#x27;c&#x27;, &#x27;d&#x27;]</span><br>a <span class="hljs-comment">// [&#x27;a&#x27;, &#x27;b&#x27;, 1, 2, 3, &#x27;e&#x27;, &#x27;f&#x27;]</span><br><br><span class="hljs-comment">// 和es6搭配使用</span><br>a = [<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>]<br>b = [<span class="hljs-string">&#x27;a&#x27;</span>, <span class="hljs-string">&#x27;b&#x27;</span>]<br>a.<span class="hljs-title function_">splice</span>(<span class="hljs-number">1</span>, <span class="hljs-number">1</span>, ...b) <span class="hljs-comment">// [2]</span><br>a <span class="hljs-comment">// [1, &#x27;a&#x27;, &#x27;b&#x27;, 3]</span><br></code></pre></td></tr></table></figure><ul><li><code>sort()</code>: 默认<strong>字典序</strong>升序, 将<strong>改变原数组</strong></li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs js">[<span class="hljs-number">4</span>, <span class="hljs-number">3</span>, <span class="hljs-number">2</span>, <span class="hljs-number">1</span>].<span class="hljs-title function_">sort</span>()<br><span class="hljs-comment">// [1, 2, 3, 4]</span><br><br>[<span class="hljs-number">11</span>, <span class="hljs-number">101</span>].<span class="hljs-title function_">sort</span>() <span class="hljs-comment">// 字典序!</span><br><span class="hljs-comment">// [101, 11]</span><br><br><span class="hljs-comment">// 自定义</span><br>[<span class="hljs-number">10111</span>, <span class="hljs-number">1101</span>, <span class="hljs-number">111</span>].<span class="hljs-title function_">sort</span>(<span class="hljs-keyword">function</span> (<span class="hljs-params">a, b</span>) &#123;<br>  <span class="hljs-keyword">return</span> a - b;<br>&#125;)<br><span class="hljs-comment">// [111, 1101, 10111]</span><br></code></pre></td></tr></table></figure><ul><li><code>map()</code>: 最多接受三个参数</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-comment">// elem为当前成员的值，index为当前成员的位置，arr为原数组（[1, 2, 3]）。</span><br>[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>].<span class="hljs-title function_">map</span>(<span class="hljs-keyword">function</span>(<span class="hljs-params">elem, index, arr</span>) &#123;<br>  <span class="hljs-keyword">return</span> elem * index;<br>&#125;);<br><span class="hljs-comment">// [0, 2, 6]</span><br></code></pre></td></tr></table></figure><ul><li><code>forEach()</code>: 和map一样也是最多三个参数</li><li><code>filter()</code>: 返回<strong>新数组</strong>, 不改变原数组</li><li><code>some()</code>和<code>every()</code></li><li><code>reduce()</code>和<code>reduceRight()</code>: 一个从左到右一个从右到左, 可以接收四个参数(至少两个)</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs js">[<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>, <span class="hljs-number">5</span>].<span class="hljs-title function_">reduce</span>(<span class="hljs-keyword">function</span> (<span class="hljs-params"></span><br><span class="hljs-params">  a,   <span class="hljs-comment">// 累积变量，必须</span></span><br><span class="hljs-params">  b,   <span class="hljs-comment">// 当前变量，必须</span></span><br><span class="hljs-params">  i,   <span class="hljs-comment">// 当前位置，可选</span></span><br><span class="hljs-params">  arr  <span class="hljs-comment">// 原数组，可选</span></span><br><span class="hljs-params"></span>) &#123;<br>  <span class="hljs-comment">// ... ...</span><br></code></pre></td></tr></table></figure><ul><li><code>indexOf()</code>和<code>lastIndexOf()</code>: 没有出现均返回-1</li></ul><h3 id="10-Number">10 | Number</h3><h4 id="静态属性">静态属性</h4><ul><li><code>Number.POSITIVE_INFINITY</code>：正的无限，指向<code>Infinity</code>。</li><li><code>Number.NEGATIVE_INFINITY</code>：负的无限，指向<code>-Infinity</code>。</li><li><code>Number.NaN</code>：表示非数值，指向<code>NaN</code>。</li><li><code>Number.MIN_VALUE</code>：表示最小的正数（即最接近0的正数，在64位浮点数体系中为<code>5e-324</code>），相应的，最接近0的负数为<code>-Number.MIN_VALUE</code>。</li><li><code>Number.MAX_SAFE_INTEGER</code>：表示能够精确表示的最大整数，即<code>9007199254740991</code>。</li><li><code>Number.MIN_SAFE_INTEGER</code>：表示能够精确表示的最小整数，即<code>-9007199254740991</code>。</li></ul><h4 id="实例方法">实例方法</h4><p><code>toFixed()</code>: 转换小数位数, 返回<strong>字符串</strong>, 有效范围为0~100, 且<strong>四舍五入不确定</strong></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs js">(<span class="hljs-number">10.055</span>).<span class="hljs-title function_">toFixed</span>(<span class="hljs-number">2</span>) <span class="hljs-comment">// &quot;10.05&quot;</span><br>(<span class="hljs-number">10.005</span>).<span class="hljs-title function_">toFixed</span>(<span class="hljs-number">2</span>) <span class="hljs-comment">// &quot;10.01&quot;</span><br></code></pre></td></tr></table></figure><p><code>toExponential()</code>: 转为科学计数法, 返回字符串</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs js">(<span class="hljs-number">1234</span>).<span class="hljs-title function_">toExponential</span>()  <span class="hljs-comment">// &quot;1.234e+3&quot;</span><br>(<span class="hljs-number">1234</span>).<span class="hljs-title function_">toExponential</span>(<span class="hljs-number">1</span>) <span class="hljs-comment">// &quot;1.2e+3&quot;</span><br>(<span class="hljs-number">1234</span>).<span class="hljs-title function_">toExponential</span>(<span class="hljs-number">2</span>) <span class="hljs-comment">// &quot;1.23e+3&quot;</span><br></code></pre></td></tr></table></figure><p><code>toPrecision()</code>: 转为指定位数的有效数字, 同样的, <strong>四舍五入不确定</strong></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs js">(<span class="hljs-number">12.35</span>).<span class="hljs-title function_">toPrecision</span>(<span class="hljs-number">3</span>) <span class="hljs-comment">// &quot;12.3&quot;</span><br>(<span class="hljs-number">12.25</span>).<span class="hljs-title function_">toPrecision</span>(<span class="hljs-number">3</span>) <span class="hljs-comment">// &quot;12.3&quot;</span><br>(<span class="hljs-number">12.15</span>).<span class="hljs-title function_">toPrecision</span>(<span class="hljs-number">3</span>) <span class="hljs-comment">// &quot;12.2&quot;</span><br>(<span class="hljs-number">12.45</span>).<span class="hljs-title function_">toPrecision</span>(<span class="hljs-number">3</span>) <span class="hljs-comment">// &quot;12.4&quot;</span><br></code></pre></td></tr></table></figure><h3 id="11-String">11 | String</h3><ul><li><code>fromCharCode</code>: 不支持码点&gt;0xFFFF的字符, 大于0xFFFF的Unicode必须使用四字节表示法即UTF-16编码确定</li><li><code>slice</code>和<code>substring</code>和<code>substr</code><ul><li>slice(start, end)表示[start, end)的子字符串, 支持负数, 和数组类似</li><li>substring别用, 设计很蠢</li><li>substr(start, length)表示start开始长度位length的子字符串</li></ul></li><li><code>match</code>可以匹配子字符串, 可以用正则表达式</li><li><code>search</code>类似<code>match</code>, 返回匹配第一个位置</li><li><code>split</code>可以接受两个参数, 第二个参数表示返回的最大成员数<ul><li><code>'a|b|c'.split('|', 2) // [&quot;a&quot;, &quot;b&quot;]</code></li></ul></li></ul><h3 id="12-Math">12 | Math</h3><h4 id="一些静态属性-数学常数">一些静态属性(数学常数)</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-title class_">Math</span>.<span class="hljs-property">E</span> <span class="hljs-comment">// 2.718281828459045</span><br><span class="hljs-title class_">Math</span>.<span class="hljs-property">LN2</span> <span class="hljs-comment">// 0.6931471805599453</span><br><span class="hljs-title class_">Math</span>.<span class="hljs-property">LN10</span> <span class="hljs-comment">// 2.302585092994046</span><br><span class="hljs-title class_">Math</span>.<span class="hljs-property">LOG2E</span> <span class="hljs-comment">// 1.4426950408889634</span><br><span class="hljs-title class_">Math</span>.<span class="hljs-property">LOG10E</span> <span class="hljs-comment">// 0.4342944819032518</span><br><span class="hljs-title class_">Math</span>.<span class="hljs-property">PI</span> <span class="hljs-comment">// 3.141592653589793</span><br><span class="hljs-title class_">Math</span>.<span class="hljs-property">SQRT1_2</span> <span class="hljs-comment">// 0.7071067811865476</span><br><span class="hljs-title class_">Math</span>.<span class="hljs-property">SQRT2</span> <span class="hljs-comment">// 1.4142135623730951</span><br></code></pre></td></tr></table></figure><h4 id="静态方法">静态方法</h4><ul><li><code>Math.abs()</code>：绝对值</li><li><code>Math.ceil()</code>：向上取整</li><li><code>Math.floor()</code>：向下取整</li><li><code>Math.max()</code>：最大值</li><li><code>Math.min()</code>：最小值</li><li><code>Math.pow()</code>：幂运算</li><li><code>Math.sqrt()</code>：平方根</li><li><code>Math.log()</code>：自然对数</li><li><code>Math.exp()</code>：<code>e</code>的指数</li><li><code>Math.round()</code>：四舍五入</li><li><code>Math.random()</code>：随机数</li></ul><p>**注意: **</p><ul><li><p><code>Math.max</code>和<code>Math.min</code>可以接收多个参数</p></li><li><p>由于浮点数精度问题 <code>0.1 + 0.2 == 0.3 // false</code>, 如果要在js比较浮点数</p></li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-comment">// js比较浮点数, 最好的方式是引入ES6的Number.EPSILON常量, 表示最小的误差范围</span><br><span class="hljs-comment">// 即</span><br><span class="hljs-keyword">function</span> <span class="hljs-title function_">equal</span>(<span class="hljs-params">a, b</span>) &#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">abs</span>(a - b) &lt; <span class="hljs-title class_">Number</span>.<span class="hljs-property">EPSILON</span>;<br>&#125;<br><br><span class="hljs-title function_">equal</span>(<span class="hljs-number">0.1</span> + <span class="hljs-number">0.2</span>, <span class="hljs-number">0.3</span>); <span class="hljs-comment">// true</span><br><span class="hljs-title function_">equal</span>(<span class="hljs-number">0.1</span> + <span class="hljs-number">0.2</span>, <span class="hljs-number">0.3000001</span>); <span class="hljs-comment">// false</span><br></code></pre></td></tr></table></figure><h3 id="13-Date">13 | Date</h3><p>直接调用, 返回一个当前时间字符串:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-title class_">Date</span>()<br><span class="hljs-comment">// &quot;Tue Dec 01 2015 09:34:43 GMT+0800 (CST)&quot;</span><br></code></pre></td></tr></table></figure><blockquote><p><code>Date</code>实例有一个独特的地方。其他对象求值的时候，都是默认调用<code>.valueOf()</code>方法，但是<code>Date</code>实例求值的时候，默认调用的是<code>toString()</code>方法。这导致对<code>Date</code>实例求值，返回的是一个字符串，代表该实例对应的时间。</p></blockquote><h4 id="静态方法-2">静态方法</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-title class_">Date</span>.<span class="hljs-title function_">now</span>() <span class="hljs-comment">// 1364026285194, 返回距时间零点毫秒数 </span><br><span class="hljs-title class_">Date</span>.<span class="hljs-title function_">parse</span>(<span class="hljs-string">&#x27;Aug 9, 1995&#x27;</span>) <span class="hljs-comment">//807897600000 (解析失败则返回NaN)</span><br><span class="hljs-comment">// 格式</span><br><span class="hljs-title class_">Date</span>.<span class="hljs-title function_">UTC</span>(year, month[, date[, hrs[, min[, sec[, ms]]]]]) <span class="hljs-comment">// 返回该时间距离时间零点的毫秒数。</span><br><span class="hljs-comment">// 用法</span><br><span class="hljs-title class_">Date</span>.<span class="hljs-title function_">UTC</span>(<span class="hljs-number">2011</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">4</span>, <span class="hljs-number">567</span>)<br><span class="hljs-comment">// 1293847384567</span><br></code></pre></td></tr></table></figure><h4 id="各种转换方法">各种转换方法</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> d = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(<span class="hljs-number">2013</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>);<br><br>d.<span class="hljs-title function_">toString</span>()<br><span class="hljs-comment">// &quot;Tue Jan 01 2013 00:00:00 GMT+0800 (CST)&quot;</span><br>d.<span class="hljs-title function_">toUTCString</span>()<br><span class="hljs-comment">// &quot;Mon, 31 Dec 2012 16:00:00 GMT&quot;</span><br>d.<span class="hljs-title function_">toISOString</span>()<br><span class="hljs-comment">// &quot;2012-12-31T16:00:00.000Z&quot;</span><br>d.<span class="hljs-title function_">toJSON</span>()<br><span class="hljs-comment">// &quot;2012-12-31T16:00:00.000Z&quot; (和toISOString完全一致)</span><br>d.<span class="hljs-title function_">toDateString</span>() <br><span class="hljs-comment">// &quot;Tue Jan 01 2013&quot;</span><br>d.<span class="hljs-title function_">toTimeString</span>() <br><span class="hljs-comment">// &quot;00:00:00 GMT+0800 (CST)&quot;</span><br>d.<span class="hljs-title function_">toLocaleString</span>()<br><span class="hljs-comment">// 中文版浏览器为&quot;2013年1月1日 上午12:00:00&quot;</span><br><span class="hljs-comment">// 英文版浏览器为&quot;1/1/2013 12:00:00 AM&quot;</span><br>d.<span class="hljs-title function_">toLocaleString</span>(<span class="hljs-string">&#x27;en-US&#x27;</span>) <span class="hljs-comment">// &quot;1/1/2013, 12:00:00 AM&quot;</span><br>d.<span class="hljs-title function_">toLocaleString</span>(<span class="hljs-string">&#x27;zh-CN&#x27;</span>) <span class="hljs-comment">// &quot;2013/1/1 上午12:00:00&quot;</span><br></code></pre></td></tr></table></figure><p>还有各种set, get方法, 这里不祥说, 建议时间库使用更丰富的: <a href="https://day.js.org/">dayjs</a></p><h3 id="14-正则表达式">14 | 正则表达式</h3><p>两种创建方法:</p><ul><li><code>var regex = /xyz/;</code></li><li><code>var regex = new RegExp('xyz');</code></li></ul><h4 id="实例方法-2">实例方法</h4><ul><li><p><code>test()</code> =&gt; boolean</p></li><li><p><code>exec()</code> =&gt; 匹配成功的子字符串</p><ul><li><p><code>exec()</code>方法的返回数组还包含以下两个属性：</p><ul><li><p><code>input</code>：整个原字符串。</p></li><li><p><code>index</code>：模式匹配成功的开始位置（从0开始计数）。</p></li></ul></li></ul></li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> r = <span class="hljs-regexp">/a(b+)a/</span>;<br><span class="hljs-keyword">var</span> arr = r.<span class="hljs-title function_">exec</span>(<span class="hljs-string">&#x27;_abbba_aba_&#x27;</span>);<br><br>arr <span class="hljs-comment">// [&quot;abbba&quot;, &quot;bbb&quot;]</span><br><br>arr.<span class="hljs-property">index</span> <span class="hljs-comment">// 1</span><br>arr.<span class="hljs-property">input</span> <span class="hljs-comment">// &quot;_abbba_aba_&quot;</span><br></code></pre></td></tr></table></figure><p><strong>字符串和正则表达式相关的实例方法</strong></p><ul><li><code>String.prototype.match()</code>：返回一个数组，成员是所有匹配的子字符串。</li><li><code>String.prototype.search()</code>：按照给定的正则表达式进行搜索，返回一个整数，表示匹配开始的位置。</li><li><code>String.prototype.replace()</code>：按照给定的正则表达式进行替换，返回替换后的字符串。</li><li><code>String.prototype.split()</code>：按照给定规则进行字符串分割，返回一个数组，包含分割后的各个成员。</li></ul><h4 id="修饰符">修饰符</h4><ul><li><code>g</code> 全局匹配<ul><li>如果带有<code>g</code>修饰符, <code>test</code>, <code>exec</code>每次都将从上次匹配结束的位置向后匹配</li><li>带有<code>g</code>, replace等操作会完成全部替换, 如果不带<code>g</code>则只进行操作</li></ul></li><li><code>i</code>忽略大小写</li><li><code>m</code>多行模式: 默认情况下（即不加<code>m</code>修饰符时），<code>^</code>和<code>$</code>匹配字符串的开始处和结尾处，加上<code>m</code>修饰符以后，<code>^</code>和<code>$</code>还会匹配行首和行尾，即<code>^</code>和<code>$</code>会识别换行符</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs js">/world$/.<span class="hljs-title function_">test</span>(<span class="hljs-string">&#x27;hello world\n&#x27;</span>) <span class="hljs-comment">// false</span><br>/world$/m.<span class="hljs-title function_">test</span>(<span class="hljs-string">&#x27;hello world\n&#x27;</span>) <span class="hljs-comment">// true</span><br></code></pre></td></tr></table></figure><h3 id="15-setTimeout和setInterval的运行机制">15 | setTimeout和setInterval的运行机制</h3><blockquote><p><code>setTimeout</code>和<code>setInterval</code>的运行机制，是将指定的代码移出本轮事件循环，等到下一轮事件循环，再检查是否到了指定时间。如果到了，就执行对应的代码；如果不到，就继续等待。</p><p>这意味着，<code>setTimeout</code>和<code>setInterval</code>指定的回调函数，必须等到本轮事件循环的所有同步任务都执行完，才会开始执行。由于前面的任务到底需要多少时间执行完，是不确定的，所以没有办法保证，<code>setTimeout</code>和<code>setInterval</code>指定的任务，一定会按照预定时间执行。</p><figure class="highlight scss"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs scss"><span class="hljs-built_in">setTimeout</span>(someTask, <span class="hljs-number">100</span>);<br><span class="hljs-built_in">veryLongTask</span>();<br></code></pre></td></tr></table></figure><p>上面代码的<code>setTimeout</code>，指定100毫秒以后运行一个任务。但是，如果后面的<code>veryLongTask</code>函数（同步任务）运行时间非常长，过了100毫秒还无法结束，那么被推迟运行的<code>someTask</code>就只有等着，等到<code>veryLongTask</code>运行结束，才轮到它执行。</p></blockquote><h3 id="16-document和element">16 | document和element</h3><blockquote><p>这部分内容太多了, 只记录一部分想记的内容, <a href="https://wangdoc.com/javascript/dom/document.html">详细请参考</a></p></blockquote><h4 id="元素的相关属性">元素的相关属性</h4><p><code>Element.accessKey</code>: 读写用于分配给当前元素的快捷键</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-comment">// HTML 代码如下</span><br><span class="hljs-comment">// &lt;button accesskey=&quot;h&quot; id=&quot;btn&quot;&gt;点击&lt;/button&gt;</span><br><span class="hljs-keyword">var</span> btn = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">&#x27;btn&#x27;</span>);<br>btn.<span class="hljs-property">accessKey</span> <span class="hljs-comment">// &quot;h&quot;</span><br><span class="hljs-comment">// 上面代码中，btn元素的快捷键是h，按下Alt + h就能将焦点转移到它上面。</span><br></code></pre></td></tr></table></figure><p><code>Element.tabIndex</code>: 表示当前元素在Tab键遍历时的顺序, 可读写, -1表示不会Tab键不会遍历到该元素</p><p><code>Element.hidden</code>: 当前元素是否可见(与css的设置是互相独立的, css的<code>display</code>和<code>visibility</code>优先级高于该属性)</p><p><code>innerHTML和outerHTML</code>: 前者表示不包含该元素, 后者表示包含该元素, 注意<code>outerHTML</code>必须保证该元素有父元素</p><p><code>Element.clientHeight</code>和<code>Element.clientWidth</code>: 返回元素节点的css高度和宽度, 只对<strong>块级元素</strong>有效, <strong>包括padding部分</strong>, 但是不包括<code>border</code>, <code>margin</code>部分, 值始终是<strong>整数</strong></p><p><code>Element.clientLeft</code>和<code>Element.clientTop</code>: 表示边框<code>border</code>的宽度, 不包括<code>padding</code>和<code>margin</code></p><p><code>Element.scrollHeight</code>和<code>Element.scrollWidth</code>: 返回一个整数值, 表示当前元素的<strong>总高度</strong>, 包括<code>padding</code>不包括<code>border</code>和<code>margin</code>, 注意包括伪元素<code>::before</code>, <code>::after</code>的高度, 如果元素内容溢出, 即使溢出部分隐藏, 仍然返回总高度(包括溢出).</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-comment">// HTML 代码如下</span><br><span class="hljs-comment">// &lt;div id=&quot;myDiv&quot; style=&quot;height: 200px; overflow: hidden;&quot;&gt;...&lt;div&gt;</span><br><span class="hljs-variable language_">document</span>.<span class="hljs-title function_">getElementById</span>(<span class="hljs-string">&#x27;myDiv&#x27;</span>).<span class="hljs-property">scrollHeight</span> <span class="hljs-comment">// 356</span><br></code></pre></td></tr></table></figure><p>上面代码中，即使<code>myDiv</code>元素的 CSS 高度只有200像素，且溢出部分不可见，但是<code>scrollHeight</code>仍然会返回该元素的原始高度。</p><p><code>offsetHeight</code>和<code>offsetWidth</code>: 与<code>clientHeight</code>和<code>clientWidth</code>相比多了边框的宽高</p><h3 id="17-事件">17 | 事件</h3><h4 id="事件处理">事件处理</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-comment">// 添加事件监听函数</span><br><span class="hljs-comment">/**</span><br><span class="hljs-comment">addEventListener(type, listener[, useCapture]);</span><br><span class="hljs-comment">type：事件名称，大小写敏感。</span><br><span class="hljs-comment">listener：监听函数。事件发生时，会调用该监听函数。</span><br><span class="hljs-comment">useCapture：布尔值，如果设为true，表示监听函数将在捕获阶段（capture）触发（参见后文《事件的传播》部分）。该参数可选，默认值为false（监听函数只在冒泡阶段被触发）。</span><br><span class="hljs-comment"></span><br><span class="hljs-comment">第三个参数除了是useCapture, 还可以是一个监听器配置对象, 定制事件的监听行为: </span><br><span class="hljs-comment">capture：布尔值，如果设为true，表示监听函数在捕获阶段触发，默认为false，在冒泡阶段触发。</span><br><span class="hljs-comment">once：布尔值，如果设为true，表示监听函数执行一次就会自动移除，后面将不再监听该事件。该属性默认值为false。</span><br><span class="hljs-comment">passive：布尔值，设为true时，表示禁止监听函数调用preventDefault()方法。如果调用了，浏览器将忽略这个要求，并在控制台输出一条警告。该属性默认值为false。</span><br><span class="hljs-comment">signal：该属性的值为一个 AbortSignal 对象，为监听器设置了一个信号通道，用来在需要时发出信号，移除监听函数。</span><br><span class="hljs-comment">**/</span><br>target.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;click&#x27;</span>, listener, <span class="hljs-literal">false</span>);<br><br><span class="hljs-comment">// 移除事件监听函数</span><br>target.<span class="hljs-title function_">removeEventListener</span>(<span class="hljs-string">&#x27;click&#x27;</span>, listener, <span class="hljs-literal">false</span>);<br><br><span class="hljs-comment">// 触发事件</span><br><span class="hljs-keyword">var</span> event = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Event</span>(<span class="hljs-string">&#x27;click&#x27;</span>);<br>target.<span class="hljs-title function_">dispatchEvent</span>(event);<br></code></pre></td></tr></table></figure><h4 id="事件传播的逻辑">事件传播的逻辑</h4><p>事件的传播分为三个阶段, 以这段代码为例:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs html"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span><br>  <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>点击<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span><br><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span><br></code></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs js">div.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;click&#x27;</span>, callback, <span class="hljs-literal">true</span>); <span class="hljs-comment">// true: 表示监听函数捕获阶段触发</span><br>p.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;click&#x27;</span>, callback, <span class="hljs-literal">true</span>); <br>div.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;click&#x27;</span>, callback, <span class="hljs-literal">false</span>); <span class="hljs-comment">// 默认就是false, 表示在冒泡阶段被触发</span><br>p.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;click&#x27;</span>, callback, <span class="hljs-literal">false</span>);<br></code></pre></td></tr></table></figure><p>当点击p时, 三个阶段的过程如下:</p><ol><li>捕获阶段：事件从<code>&lt;div&gt;</code>向<code>&lt;p&gt;</code>传播时，触发<code>&lt;div&gt;</code>的<code>click</code>事件；</li><li>目标阶段：事件从<code>&lt;div&gt;</code>到达<code>&lt;p&gt;</code>时，触发<code>&lt;p&gt;</code>的<code>click</code>事件；</li><li>冒泡阶段：事件从<code>&lt;p&gt;</code>传回<code>&lt;div&gt;</code>时，再次触发<code>&lt;div&gt;</code>的<code>click</code>事件。</li></ol><h4 id="事件的代理">事件的代理</h4><p>由于事件会在冒泡阶段向上传播到父节点，因此可以把子节点的监听函数定义在父节点上，由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理（delegation）。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> ul = <span class="hljs-variable language_">document</span>.<span class="hljs-title function_">querySelector</span>(<span class="hljs-string">&#x27;ul&#x27;</span>);<br><br>ul.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;click&#x27;</span>, <span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) &#123;<br>  <span class="hljs-keyword">if</span> (event.<span class="hljs-property">target</span>.<span class="hljs-property">tagName</span>.<span class="hljs-title function_">toLowerCase</span>() === <span class="hljs-string">&#x27;li&#x27;</span>) &#123;<br>    <span class="hljs-comment">// some code</span><br>  &#125;<br>&#125;);<br></code></pre></td></tr></table></figure><p>上面代码中，<code>click</code>事件的监听函数定义在<code>&lt;ul&gt;</code>节点，但是实际上，它处理的是子节点<code>&lt;li&gt;</code>的<code>click</code>事件。这样做的好处是，只要定义一个监听函数，就能处理多个子节点的事件，而不用在每个<code>&lt;li&gt;</code>节点上定义监听函数。而且以后再添加子节点，监听函数依然有效。</p><h4 id="阻止事件传播">阻止事件传播</h4><p>可使用<code>stopPropagation</code>方法阻止事件的传播</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-comment">// 事件传播到 p 元素后，就不再向下传播了</span><br>p.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;click&#x27;</span>, <span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) &#123;<br>  event.<span class="hljs-title function_">stopPropagation</span>();<br>&#125;, <span class="hljs-literal">true</span>);<br><br><span class="hljs-comment">// 事件冒泡到 p 元素后，就不再向上冒泡了</span><br>p.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;click&#x27;</span>, <span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) &#123;<br>  event.<span class="hljs-title function_">stopPropagation</span>();<br>&#125;, <span class="hljs-literal">false</span>);<br></code></pre></td></tr></table></figure><p>注意, 如果一个元素绑定了多个事件, <code>stopPropagation</code>只会取消这个事件的传播, 而不会取消这个事件, 即:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs js">p.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;click&#x27;</span>, <span class="hljs-keyword">function</span> (<span class="hljs-params">event</span>) &#123;<br>  event.<span class="hljs-title function_">stopPropagation</span>();<br>  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-number">1</span>);<br>&#125;);<br><br>p.<span class="hljs-title function_">addEventListener</span>(<span class="hljs-string">&#x27;click&#x27;</span>, <span class="hljs-keyword">function</span>(<span class="hljs-params">event</span>) &#123;<br>  <span class="hljs-comment">// 会触发</span><br>  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-number">2</span>);<br>&#125;);<br></code></pre></td></tr></table></figure><p>如果要彻底取消这个事件, 可以使用<code>stopImmediatePropagation</code>方法</p><h4 id="Event对象">Event对象</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> ev = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Event</span>(<br>  <span class="hljs-string">&#x27;look&#x27;</span>, <span class="hljs-comment">// 事件名</span><br>  &#123;<br>    <span class="hljs-string">&#x27;bubbles&#x27;</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// 默认false, 表示事件是否冒泡</span><br>    <span class="hljs-string">&#x27;cancelable&#x27;</span>: <span class="hljs-literal">false</span> <span class="hljs-comment">// 默认false, 表示事件是否可以被取消, 即能否用preventDefault取消这个事件</span><br>  &#125;<br>);<br><span class="hljs-variable language_">document</span>.<span class="hljs-title function_">dispatchEvent</span>(ev);<br></code></pre></td></tr></table></figure><p>事件的属性:</p><ul><li><code>Event.eventPhase</code>: 只读, 返回值有四种可能。<ul><li>0，事件目前没有发生。</li><li>1，事件目前处于捕获阶段，即处于从祖先节点向目标节点的传播过程中。</li><li>2，事件到达目标节点，即<code>Event.target</code>属性指向的那个节点。</li><li>3，事件处于冒泡阶段，即处于从目标节点向祖先节点的反向传播过程中。</li></ul></li><li><code>Event.bubbles</code>: 只读, 事件是否会冒泡</li><li><code>Event.cancelable</code>: 只读</li><li><code>Event.cancelBubble</code>: 设为true则等效于<code>stopPropagation</code>, 用于阻止事件传播</li><li><code>Event.defaultPrevented</code>: 只读, 示该事件是否调用过<code>Event.preventDefault</code>方法</li><li><code>Event.currentTarget</code>: 返回事件当前所在的节点，即事件当前正在通过的节点，也就是当前正在执行的监听函数所在的那个节点。随着事件的传播，这个属性的值会变。</li><li><code>Event.target</code>: 返回原始触发事件的那个节点，即事件最初发生的节点。这个属性不会随着事件的传播而改变。</li><li><code>Event.type</code>: 事件类型</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">var</span> evt = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Event</span>(<span class="hljs-string">&#x27;foo&#x27;</span>);<br>evt.<span class="hljs-property">type</span> <span class="hljs-comment">// &quot;foo&quot;</span><br></code></pre></td></tr></table></figure><ul><li><code>Event.isTrusted</code>: 返回一个布尔值，表示该事件是否由真实的用户行为产生。比如，用户点击链接会产生一个<code>click</code>事件，该事件是用户产生的；<code>Event</code>构造函数生成的事件，则是脚本产生的。</li></ul><p>事件的方法:</p><ul><li><code>Event.preventDefault()</code> 取消浏览器对该事件的默认行为, 比如点击链接后，浏览器默认会跳转到另一个页面，使用这个方法以后，就不会跳转了；该方法生效的前提是，事件对象的<code>cancelable</code>属性为<code>true</code>，如果为<code>false</code>，调用该方法没有任何效果。</li><li><code>Event.stopPropagation()</code>方法阻止事件在 DOM 中继续传播，防止再触发定义在别的节点上的监听函数，但是不包括在当前节点上其他的事件监听函数。</li><li><code>Event.stopImmediatePropagation()</code>: 方法阻止同一个事件的其他监听函数被调用</li><li><code>Event.composedPath()</code>: 返回一个数组，成员是事件的最底层节点和依次冒泡经过的所有上层节点。</li></ul><h4 id="常见的事件">常见的事件</h4><p><strong>鼠标相关:</strong> <a href="https://wangdoc.com/javascript/events/mouse.html">https://wangdoc.com/javascript/events/mouse.html</a></p><ul><li><code>click</code>：按下鼠标（通常是按下主按钮）时触发。</li><li><code>dblclick</code>：在同一个元素上双击鼠标时触发。</li><li><code>mousedown</code>：按下鼠标键时触发。</li><li><code>mouseup</code>：释放按下的鼠标键时触发。</li><li><code>mousemove</code>：当鼠标在一个节点内部移动时触发。当鼠标持续移动时，该事件会连续触发。为了避免性能问题，建议对该事件的监听函数做一些限定，比如限定一段时间内只能运行一次。</li><li><code>mouseenter</code>：鼠标进入一个节点时触发，进入子节点不会触发这个事件</li><li><code>mouseover</code>：鼠标进入一个节点时触发，进入子节点会再一次触发这个事件</li><li><code>mouseout</code>：鼠标离开一个节点时触发，离开父节点也会触发这个事件</li><li><code>mouseleave</code>：鼠标离开一个节点时触发，离开父节点不会触发这个事件</li><li><code>contextmenu</code>：按下鼠标右键时（上下文菜单出现前）触发，或者按下“上下文”菜单键时触发。</li><li><code>wheel</code>：滚动鼠标的滚轮时触发，该事件继承的是<code>WheelEvent</code>接口。</li></ul><p><strong>键盘相关:</strong> <a href="https://wangdoc.com/javascript/events/keyboard.html">https://wangdoc.com/javascript/events/keyboard.html</a></p><ul><li><code>keydown</code>：按下键盘时触发。</li><li><code>keypress</code>：按下有值的键时触发，即按下 Ctrl、Alt、Shift、Meta 这样无值的键，这个事件不会触发。对于有值的键，按下时先触发<code>keydown</code>事件，再触发这个事件。</li><li><code>keyup</code>：松开键盘时触发该事件。</li></ul><p><strong>进度相关:</strong> <a href="https://wangdoc.com/javascript/events/progress.html">https://wangdoc.com/javascript/events/progress.html</a></p><ul><li><code>abort</code>：外部资源中止加载时（比如用户取消）触发。如果发生错误导致中止，不会触发该事件。</li><li><code>error</code>：由于错误导致外部资源无法加载时触发。</li><li><code>load</code>：外部资源加载成功时触发。</li><li><code>loadstart</code>：外部资源开始加载时触发。</li><li><code>loadend</code>：外部资源停止加载时触发，发生顺序排在<code>error</code>、<code>abort</code>、<code>load</code>等事件的后面。</li><li><code>progress</code>：外部资源加载过程中不断触发。</li><li><code>timeout</code>：加载超时时触发。</li></ul><p><strong>表单相关:</strong> <a href="https://wangdoc.com/javascript/events/form.html">https://wangdoc.com/javascript/events/form.html</a></p><ul><li><code>input</code> 和 <code>change</code>: 当<code>&lt;input&gt;</code>、<code>&lt;select&gt;</code>、<code>&lt;textarea&gt;</code>的值发生变化时触发。该事件跟<code>change</code>事件很像，不同之处在于<code>input</code>事件在元素的值发生变化后立即发生，而<code>change</code>在元素失去焦点时发生，而内容此时可能已经变化多次。也就是说，如果有连续变化，<code>input</code>事件会触发多次，而<code>change</code>事件只在失去焦点时触发一次。</li><li><code>select</code>: <code>select</code>事件当在<code>&lt;input&gt;</code>、<code>&lt;textarea&gt;</code>里面选中文本时触发。</li><li><code>invalid</code>: 用户提交表单时，如果表单元素的值不满足校验条件，就会触发<code>invalid</code>事件。</li><li><code>reset</code>事件当表单重置（所有表单成员变回默认值）时触发。</li><li><code>submit</code>事件当表单数据向服务器提交时触发。注意，<code>submit</code>事件的发生对象是<code>&lt;form&gt;</code>元素，而不是<code>&lt;button&gt;</code>元素，因为提交的是表单，而不是按钮。</li></ul><p><strong>拖拽相关:</strong>  <a href="https://wangdoc.com/javascript/events/drag.html">https://wangdoc.com/javascript/events/drag.html</a></p><p>当元素节点或选中的文本被拖拉时，就会持续触发拖拉事件，包括以下一些事件。</p><ul><li><code>drag</code>：拖拉过程中，在被拖拉的节点上持续触发（相隔几百毫秒）。</li><li><code>dragstart</code>：用户开始拖拉时，在被拖拉的节点上触发，该事件的<code>target</code>属性是被拖拉的节点。通常应该在这个事件的监听函数中，指定拖拉的数据。</li><li><code>dragend</code>：拖拉结束时（释放鼠标键或按下 ESC 键）在被拖拉的节点上触发，该事件的<code>target</code>属性是被拖拉的节点。它与<code>dragstart</code>事件，在同一个节点上触发。不管拖拉是否跨窗口，或者中途被取消，<code>dragend</code>事件总是会触发的。</li><li><code>dragenter</code>：拖拉进入当前节点时，在当前节点上触发一次，该事件的<code>target</code>属性是当前节点。通常应该在这个事件的监听函数中，指定是否允许在当前节点放下（drop）拖拉的数据。如果当前节点没有该事件的监听函数，或者监听函数不执行任何操作，就意味着不允许在当前节点放下数据。在视觉上显示拖拉进入当前节点，也是在这个事件的监听函数中设置。</li><li><code>dragover</code>：拖拉到当前节点上方时，在当前节点上持续触发（相隔几百毫秒），该事件的<code>target</code>属性是当前节点。该事件与<code>dragenter</code>事件的区别是，<code>dragenter</code>事件在进入该节点时触发，然后只要没有离开这个节点，<code>dragover</code>事件会持续触发。</li><li><code>dragleave</code>：拖拉操作离开当前节点范围时，在当前节点上触发，该事件的<code>target</code>属性是当前节点。如果要在视觉上显示拖拉离开操作当前节点，就在这个事件的监听函数中设置。</li><li><code>drop</code>：被拖拉的节点或选中的文本，释放到目标节点时，在目标节点上触发。注意，如果当前节点不允许<code>drop</code>，即使在该节点上方松开鼠标键，也不会触发该事件。如果用户按下 ESC 键，取消这个操作，也不会触发该事件。该事件的监听函数负责取出拖拉数据，并进行相关处理。</li></ul><p><strong>其他:</strong></p><ul><li><code>resize</code>: 事件在改变浏览器窗口大小时触发，主要发生在<code>window</code>对象上面。</li><li><code>storage</code>: Storage 接口储存的数据发生变化时，会触发 storage 事件，可以指定这个事件的监听函数。<ul><li><code>StorageEvent.key</code>：字符串，表示发生变动的键名。如果 storage 事件是由<code>clear()</code>方法引起，该属性返回<code>null</code>。</li><li><code>StorageEvent.newValue</code>：字符串，表示新的键值。如果 storage 事件是由<code>clear()</code>方法或删除该键值对引发的，该属性返回<code>null</code>。</li><li><code>StorageEvent.oldValue</code>：字符串，表示旧的键值。如果该键值对是新增的，该属性返回<code>null</code>。</li><li><code>StorageEvent.storageArea</code>：对象，返回键值对所在的整个对象。也说是说，可以从这个属性上面拿到当前域名储存的所有键值对。</li><li><code>StorageEvent.url</code>：字符串，表示原始触发 storage 事件的那个网页的网址。</li></ul></li></ul><h3 id="18-关于同源">18 | 关于同源</h3><h4 id="次级域名不同-如何共享Cookie">次级域名不同, 如何共享Cookie</h4><p>举例来说，A 网页的网址是<code>http://w1.example.com/a.html</code>，B 网页的网址是<code>http://w2.example.com/b.html</code>，那么只要设置相同的<code>document.domain</code>，两个网页就可以共享 Cookie。因为浏览器通过<code>document.domain</code>属性来检查是否同源。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-comment">// 两个网页都需要设置</span><br><span class="hljs-variable language_">document</span>.<span class="hljs-property">domain</span> = <span class="hljs-string">&#x27;example.com&#x27;</span>;<br></code></pre></td></tr></table></figure><p>注意，A 和 B 两个网页都需要设置<code>document.domain</code>属性，才能达到同源的目的。因为设置<code>document.domain</code>的同时，会把端口重置为<code>null</code>，因此如果只设置一个网页的<code>document.domain</code>，会导致两个网址的端口不同，还是达不到同源的目的。</p><h4 id="CORS">CORS</h4><p>CORS 需要浏览器和服务器同时支持。目前，所有浏览器都支持该功能。</p><p>所以CORS主要还是服务器部分需要配置实现。</p><h3 id="19-URL的编码和解码">19 | URL的编码和解码</h3><h4 id="编码和解码规则">编码和解码规则</h4><p>网页的 URL 只能包含合法的字符。合法字符分成两类。</p><ul><li><strong>URL 元字符</strong>：分号（<code>;</code>），逗号（<code>,</code>），斜杠（<code>/</code>），问号（<code>?</code>），冒号（<code>:</code>），at（<code>@</code>），<code>&amp;</code>，等号（<code>=</code>），加号（<code>+</code>），美元符号（<code>$</code>），井号（<code>#</code>）</li><li><strong>语义字符</strong>：<code>a-z</code>，<code>A-Z</code>，<code>0-9</code>，连词号（<code>-</code>），下划线（<code>_</code>），点（<code>.</code>），感叹号（<code>!</code>），波浪线（<code>~</code>），星号（<code>*</code>），单引号（<code>'</code>），圆括号（<code>()</code>）</li></ul><p>除了以上字符，其他字符出现在 URL 之中都必须转义，规则是根据操作系统的默认编码，将每个字节转为百分号（<code>%</code>）加上两个大写的十六进制字母。</p><p>比如，UTF-8 的操作系统上，<code>http://www.example.com/q=春节</code>这个 URL 之中，汉字“春节”不是 URL 的合法字符，所以被浏览器自动转成<code>http://www.example.com/q=%E6%98%A5%E8%8A%82</code>。其中，“春”转成了<code>%E6%98%A5</code>，“节”转成了<code>%E8%8A%82</code>。这是因为“春”和“节”的 UTF-8 编码分别是<code>E6 98 A5</code>和<code>E8 8A 82</code>，将每个字节前面加上百分号，就构成了 URL 编码。</p><h3 id="encodeURI和encodeURIComponent">encodeURI和encodeURIComponent</h3><p>前者只转码除元字符和语义字符外的字符, 后者除了语义字符所有字符都被转码</p><p>故后者不能用来转码整个URL, 只能用来转码组成部分</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-built_in">encodeURI</span>(<span class="hljs-string">&#x27;http://www.example.com/q=春节&#x27;</span>)<br><span class="hljs-comment">// &quot;http://www.example.com/q=%E6%98%A5%E8%8A%82&quot;</span><br><br><span class="hljs-built_in">encodeURIComponent</span>(<span class="hljs-string">&#x27;http://www.example.com/q=春节&#x27;</span>)<br><span class="hljs-comment">// &quot;http%3A%2F%2Fwww.example.com%2Fq%3D%E6%98%A5%E8%8A%82&quot;</span><br></code></pre></td></tr></table></figure><p>与这两个方法对应的有解码方法<code>decodeURI</code>和<code>decodeURIComponent</code></p><h3 id="20-表单">20 | 表单</h3><p><code>&lt;form&gt;</code></p><h4 id="内置验证">内置验证</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs js">&lt;!-- 必填 --&gt;<br>&lt;input required&gt;<br><br>&lt;!-- 必须符合正则表达式 --&gt;<br>&lt;input pattern=&quot;banana|cherry&quot;&gt;<br><br>&lt;!-- 字符串长度必须为6个字符 --&gt;<br>&lt;input minlength=&quot;6&quot; maxlength=&quot;6&quot;&gt;<br><br>&lt;!-- 数值必须在1到10之间 --&gt;<br>&lt;input type=&quot;number&quot; min=&quot;1&quot; max=&quot;10&quot;&gt;<br><br>&lt;!-- 必须填入 Email 地址 --&gt;<br>&lt;input type=&quot;email&quot;&gt;<br><br>&lt;!-- 必须填入 URL --&gt;<br>&lt;input type=&quot;URL&quot;&gt;<br></code></pre></td></tr></table></figure><p>如果一个控件通过验证，它就会匹配<code>:valid</code>的 CSS 伪类，浏览器会继续进行表单提交的流程。如果没有通过验证，该控件就会匹配<code>:invalid</code>的 CSS 伪类，浏览器会终止表单提交，并显示一个错误信息</p><p>:valid和:invalid示意:</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs css"><span class="hljs-selector-tag">input</span><span class="hljs-selector-pseudo">:invalid</span> &#123;<br>  <span class="hljs-attribute">border-color</span>: red;<br>&#125;<br><span class="hljs-selector-tag">input</span>,<br><span class="hljs-selector-tag">input</span><span class="hljs-selector-pseudo">:valid</span> &#123;<br>  <span class="hljs-attribute">border-color</span>: <span class="hljs-number">#ccc</span>;<br>&#125;<br></code></pre></td></tr></table></figure><p>可以通过<code>form.checkValidity() // 返回值boolean</code>手动触发表单校验</p><div class="note note-success">            <p>由于记完JS, 篇幅已经很长了, 于是打算将此博客分篇, 下篇见《JS与ES6的查漏补缺(下)》</p>          </div>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E5%89%8D%E7%AB%AF%E5%AD%A6%E4%B9%A0/">前端学习</category>
      
      
      <category domain="https://moreality.net/tags/javascript/">javascript</category>
      
      <category domain="https://moreality.net/tags/es6/">es6</category>
      
      <category domain="https://moreality.net/tags/%E5%89%8D%E7%AB%AF/">前端</category>
      
      
      <comments>https://moreality.net/posts/9018/#disqus_thread</comments>
      
    </item>
    
    <item>
      <title>关于hexo博客的github actions CI配置</title>
      <link>https://moreality.net/posts/40160/</link>
      <guid>https://moreality.net/posts/40160/</guid>
      <pubDate>Fri, 12 Aug 2022 09:37:22 GMT</pubDate>
      
      <description>在配置CI的过程中使用缓存等技术, 并提供一个配置文件的参考</description>
      
      
      
      <content:encoded><![CDATA[ <div class="note note-info">            <p>在之前的这篇博客: <i><a href="https://moreality.net/posts/514/">将博客搬迁到upyun</a></i>中主要讲了一下我把博客从github pages搬迁到upyun的配置过程。</p><p>其中包括了upx等工具的使用, 本文就不再赘述这些了。</p>          </div><p>放假了无聊又想摆弄一下博客，发现之前的博客的部署流程还是相对复杂，主要流程包括：</p><ol><li>写文章</li><li>编译生成<code>public</code></li><li>同步到github上</li><li>发布到upyun上</li></ol><p>其中编译的时间尤其长, 并且伴随着文章, 图片的增加用时将只增不减。</p><h2 id="采用-github-actions预计的问题与解决">采用 github actions预计的问题与解决</h2><p>一直打算将部署流程放在github action上，但之前没有时间折腾这个，并且github action我当时想着会面临一个问题：</p><p>每次都需要<strong>配置环境的时间</strong>，也就是<code>npm install</code>安装大大小小的依赖, 安装upx依赖等, 都需要很多的时间, 而我们经常写博客是不看到发布页不罢休的, 所以在本地和在云端貌似并没有什么很大的区别, 云端CI反而会拖慢发布时间.</p><p>而今天我研究了一下, 发现了一个好用的东西: <a href="https://github.com/actions/cache">actions/cache</a></p><blockquote><p>This action allows caching dependencies and build outputs to improve workflow execution time.</p></blockquote><p>简单来说就是可以缓存github action时产生的一些构建的生成文件或者是网上down下来的依赖.</p><p>这样就完美解决了<code>node_modules</code>下载和<code>upx</code>工具下载消耗的时间问题</p><h2 id="workflow文件">workflow文件</h2><p>其他东西就不多说了, actions文件的语法也可以直接看github的官方文档: <a href="https://docs.github.com/cn/actions">https://docs.github.com/cn/actions</a></p><p>这里把我最后的actions文件拿出来瞅瞅, 供参考</p><p>主要流程大概就是:</p><pre><code class=" mermaid">graph LRA[&quot;缓存/下载node_modules&quot;] --&gt; B[&quot;hexo编译生成public文件&quot;] --&gt; C[&quot;缓存/下载upx&quot;] --&gt; D[&quot;upx将public下的文件上传到upyun&quot;]</code></pre><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br></pre></td><td class="code"><pre><code class="hljs yml"><span class="hljs-attr">name:</span> <span class="hljs-string">发布博客到upyun</span><br><span class="hljs-attr">on:</span> <span class="hljs-string">push</span><br><br><span class="hljs-attr">jobs:</span><br>  <span class="hljs-attr">deploy:</span><br>    <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span><br>    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span><br>    <span class="hljs-attr">environment:</span> <span class="hljs-string">upx</span><br><br>    <span class="hljs-attr">steps:</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span><br>        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v3</span><br><br>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Cache</span> <span class="hljs-string">node</span> <span class="hljs-string">modules</span><br>        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/cache@v3</span><br>        <span class="hljs-attr">id:</span> <span class="hljs-string">cache-node</span><br>        <span class="hljs-attr">with:</span><br>          <span class="hljs-attr">path:</span> <span class="hljs-string">node_modules</span><br>          <span class="hljs-attr">key:</span> <span class="hljs-string">$&#123;&#123;</span> <span class="hljs-string">runner.os</span> <span class="hljs-string">&#125;&#125;-node-$&#123;&#123;</span> <span class="hljs-string">hashFiles(&#x27;**/package-lock.json&#x27;)</span> <span class="hljs-string">&#125;&#125;</span><br>          <span class="hljs-attr">restore-keys:</span> <span class="hljs-string">|</span><br><span class="hljs-string">            $&#123;&#123; runner.os &#125;&#125;-node-</span><br><span class="hljs-string"></span><br>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">Dependencies</span><br>        <span class="hljs-attr">if:</span> <span class="hljs-string">steps.cache-node.outputs.cache-hit</span> <span class="hljs-type">!=</span> <span class="hljs-string">&#x27;true&#x27;</span><br>        <span class="hljs-attr">run:</span> <span class="hljs-string">|</span><br><span class="hljs-string">          npm i</span><br><span class="hljs-string"></span><br>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Hexo</span> <span class="hljs-string">generate</span> <span class="hljs-string">from</span> <span class="hljs-string">source</span><br>        <span class="hljs-attr">run:</span> <span class="hljs-string">|</span><br><span class="hljs-string">          npm i -g hexo-cli</span><br><span class="hljs-string">          hexo clean &amp;&amp; hexo generate</span><br><span class="hljs-string"></span><br>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Cache</span> <span class="hljs-string">upx</span><br>        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/cache@v3</span><br>        <span class="hljs-attr">id:</span> <span class="hljs-string">cache-upx</span><br>        <span class="hljs-attr">with:</span><br>          <span class="hljs-attr">path:</span> <span class="hljs-string">upx-dir/upx-command-dir</span><br>          <span class="hljs-attr">key:</span> <span class="hljs-string">$&#123;&#123;</span> <span class="hljs-string">runner.os</span> <span class="hljs-string">&#125;&#125;-upx-upxCahce</span><br>          <span class="hljs-attr">restore-keys:</span> <span class="hljs-string">|</span><br><span class="hljs-string">            $&#123;&#123; runner.os &#125;&#125;-upx-CacheBackUp-</span><br><span class="hljs-string"></span><br>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">Upx</span><br>        <span class="hljs-attr">if:</span> <span class="hljs-string">steps.cache-upx.outputs.cache-hit</span> <span class="hljs-type">!=</span> <span class="hljs-string">&#x27;true&#x27;</span><br>        <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">upx-cache-miss</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">mkdir</span> <span class="hljs-string">upx-dir</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">cd</span> <span class="hljs-string">upx-dir</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">wget</span> <span class="hljs-string">https://github.com/upyun/upx/releases/download/v0.3.6/upx_0.3.6_linux_x86_64.tar.gz</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">tar</span> <span class="hljs-string">-xf</span> <span class="hljs-string">upx_0.3.6_linux_x86_64.tar.gz</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">mkdir</span> <span class="hljs-string">upx-command-dir</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">mv</span> <span class="hljs-string">upx</span> <span class="hljs-string">upx-command-dir</span><br><br>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Login</span> <span class="hljs-string">Upx</span> <span class="hljs-string">And</span> <span class="hljs-string">Post</span><br>        <span class="hljs-attr">env:</span> <br>          <span class="hljs-attr">bucket:</span> <span class="hljs-string">$&#123;&#123;secrets.BUCKET&#125;&#125;</span><br>          <span class="hljs-attr">operator:</span> <span class="hljs-string">$&#123;&#123;secrets.OPERATOR&#125;&#125;</span><br>          <span class="hljs-attr">operator_password:</span> <span class="hljs-string">$&#123;&#123;secrets.OPERATOR_PASSWORD&#125;&#125;</span><br>        <span class="hljs-attr">run:</span> <span class="hljs-string">|</span><br><span class="hljs-string">          ./upx-dir/upx-command-dir/upx login $bucket $operator $operator_password</span><br><span class="hljs-string">          cd public</span><br><span class="hljs-string">          ../upx-dir/upx-command-dir/upx put ./ /</span><br><span class="hljs-string"></span><br></code></pre></td></tr></table></figure><h2 id="hexo-deploy-可选">hexo deploy(可选)</h2><p>由于我直接上传到了upyun, 所以略过了hexo deploy的配置</p><p>如果需要hexo deploy, 可以进行如下配置:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-comment"># ...</span><br>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Hexo</span> <span class="hljs-string">Deploy</span><br>        <span class="hljs-attr">env:</span><br>          <span class="hljs-attr">priv_key:</span> <span class="hljs-string">$&#123;&#123;secrets.PRIV_KEY&#125;&#125;</span><br>        <span class="hljs-attr">run:</span> <span class="hljs-string">|</span><br><span class="hljs-string">          mkdir -p ~/.ssh/</span><br><span class="hljs-string">          echo &quot;$priv_key&quot; &gt; ~/.ssh/id_rsa</span><br><span class="hljs-string">          chmod 600 ~/.ssh/id_rsa</span><br><span class="hljs-string">          ssh-keyscan github.com &gt;&gt; ~/.ssh/known_hosts</span><br><span class="hljs-string">          git config --global user.name $&#123;&#123;secrets.USER_NAME&#125;&#125;</span><br><span class="hljs-string">          git config --global user.email $&#123;&#123;secrets.USER_EMAIL&#125;&#125;</span><br><span class="hljs-string">          hexo deploy</span><br><span class="hljs-string"></span><span class="hljs-comment"># ...</span><br></code></pre></td></tr></table></figure><p>也就是在虚拟机中将私钥放上去</p><p>具体的公钥和私钥配置应该在之前配置好, 配置于<code>settings/SSH and GPG keys</code>中</p><h2 id="push-back-可选">push back(可选)</h2><p>有时我们会遇到需要将代码回推送到仓库的需求</p><p>比如我正在使用一个插件: <a href="https://github.com/rozbo/hexo-abbrlink">hexo-abbrlink</a></p><p>它的作用是根据<code>title + date</code>生成一个永久的文章id, 避免了文章标题有中文时link过长的问题, 优化了seo</p><p>但是这个插件会修改<code>_post</code>文件夹下的源代码, 所以需要我们将修改后的文件再次推送到github仓库</p><p>由于在上一步操作(hexo deploy)中我们已经将ssh的私钥保存了, 所以这里就不需要再配置了, 不过需要注意由于仓库不一定发生代码(如果文章不是第一次创建, 就不会生成abbrlink, <code>_post</code>文件夹也不会变更), 这时如果我们直接:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell">git add .<br>git commit -m &quot;xx&quot;<br>git push<br></code></pre></td></tr></table></figure><p>就会在报working tree clean的问题, 这个workflow也会随即终止</p><p>于是我们需要加一个判断, 整理好的script如下:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs shell"><span class="hljs-meta prompt_">#</span><span class="language-bash">!/bin/sh</span><br><br>if [ -z &quot;$(git status --porcelain)&quot; ]; then<br>    echo &quot;nothing to update.&quot;<br>else <br>    git add .<br>    git commit -m &quot;update: [abbrlink] blog source code update.&quot;<br>    git push origin master<br>fi<br></code></pre></td></tr></table></figure>]]></content:encoded>
      
      
      <category domain="https://moreality.net/categories/%E9%85%8D%E7%BD%AE%E7%8E%AF%E5%A2%83/">配置环境</category>
      
      
      <category domain="https://moreality.net/tags/blog/">blog</category>
      
      <category domain="https://moreality.net/tags/%E9%85%8D%E7%BD%AE/">配置</category>
      
      <category domain="https://moreality.net/tags/CI/">CI</category>
      
      
      <comments>https://moreality.net/posts/40160/#disqus_thread</comments>
      
    </item>
    
  </channel>
</rss>
