[{"content":" 说来惭愧，大概有三年的时间没有写过年终总结了（事实上除了技术文档之外，文章也很少写了）。 一方面是工作之后，就越来越没有学生时代那么多空闲的时间可以用来思考和写作了。 另一方面则是对自己的期望越来越高，觉得流水账一样的总结不太拿得出手， 以及太久没有写这些东西感觉文笔退化的厉害，反而变得难以落笔了。\n对我来说 2025 年是变化巨大的一年，仿佛整个世界都发生了翻天覆地的变化，但其实仔细想想，又似乎一切如旧。 有一些不大不小的伤病，但身体状态还不错，体能还有点老本能吃。 即使很忙，但仍然能挤出一些时间玩喜欢的游戏，看喜欢的动画，读喜欢的书。 虽然有压力，有点小痛苦，但还蛮开心。 我可能正在经历人生中最艰难的一段时光，但可以肯定的是，这绝对也是最具有挑战和机遇的时光。\n1. 旅行的压力 所有的期待，都暗中标好了价码。\n前段时间刚好和朋友聊天讲到这个话题，仔细一想 2025 应该是我旅行次数最多的一年，甚至对我来说可能有点过量。 我可能更习惯于延迟满足，旅行更像是对我一年来努力的奖励，或者对压抑情绪的释放。 太过于频繁的旅行可能反而带来更多的压力。而且还会想着上次的视频还没剪，照片还没修，欠下的债越来越多。\n另外虽然今年大多数的旅游我对攻略的贡献几乎为 0，但作为一个 J 人，我实际上没办法真正漫无目的的旅行。 要么是需要拍到好的照片，要么是要尝试新的拍摄风格，要么是需要收集好看的冰箱贴，总之得要有所收获。\n2. 复杂度危机 不要妄想上帝用一句话创造世界。\n说实话 2025 年我最反感，甚至痛恨的一个词就是 Vibe Coding，我觉得这个词多少带点贬义和羞辱。 但我并非完全反对 Vibe Coding（Vibe 是最好的 QuickStart），而是反感对 Vibe Coding 诈骗式的营销。\n复杂度危机并不是一个新鲜词，每一代的程序员都经历过复杂度危机，并都在致力于解决复杂度危机。\n很多人觉得这是编程的\u0026quot;民主化\u0026quot;，让不会写代码的人也能创造软件。但我看到的却是另一番景象：代码质量急剧下降，维护成本指数级上升，技术债像滚雪球一样越滚越大。因为 Vibe Coding 偷偷藏了一个致命的假设——想法本身就是清晰的。\n而实际上，大多数人的想法都是模糊的、矛盾的、不完整的。编程最大的价值从来不是把想法变成代码，而是在这个过程中发现想法中的漏洞，厘清需求边界，让想法真正可行。这才是编程的本质——思考，而不仅仅是实现。\n3. 正确的事 正确的事，都有代价。\n4. 表达力 不只是我自己，我觉得整个社会的表达能力都正在减弱。我们正在进行一场愉快的退化。 我们享受着奶头乐带来的简单，快速的快乐，显著降低了大脑活动水平，削弱记忆，甚至造成“认知惯性”\n5. 复杂度危机 如果要说今年什么东西最火，我觉得一定是 Vibe Coding 了。到处都能听到他的名字，从办公室到朋友圈，从技术论坛到小红书，不管到哪都有人提起。\n但说实话，我从一开始就不喜欢这个词，不过严格意义上来说， 我所反感的并不是这个词本身，而是整个互联网对 Vibe Coding 的一种欺诈式的营销和误解。\n如果要用一句话评价 Loveable，我会说 “生的幽默，死的滑稽”\n事实上，编码从来都不是障碍，编程早就已经不是需要多大门槛的事情了。 现在一些代码的可读性甚至你随便找一个英文母语的大妈过来都能读懂。\n虽然他更加剧了互联网垃圾的生产速度。但他也让真正具备产品思维，真正理解需求的人，不再受制于编程技术。\n你只需要\u0026quot;描述你的想法\u0026quot;，代码就会自动生成。听起来很美，对吧？但现实是，没有清晰的需求描述，再强大的 AI 也只能产生一堆看起来能运行但本质上是垃圾的代码。\n很多人觉得这是编程的\u0026quot;民主化\u0026quot;，让不会写代码的人也能创造软件。但我看到的却是另一番景象：代码质量急剧下降，维护成本指数级上升，技术债像滚雪球一样越滚越大。因为 Vibe Coding 偷偷藏了一个致命的假设——想法本身就是清晰的。\n而实际上，大多数人的想法都是模糊的、矛盾的、不完整的。编程最大的价值从来不是把想法变成代码，而是在这个过程中发现想法中的漏洞，厘清需求边界，让想法真正可行。这才是编程的本质——思考，而不仅仅是实现。\n说实话，我很讨厌 Vibe Coding 这个词，甚至感到愤怒，通常我很少为一个概念感到愤怒，即使是区块链，我对他的评价也只是“没有意义”，但我要说的是 Vibe Coding 从一开始就是扯淡。我在上半年这个词火爆全网时就说，这些自动化编码工具、平台绝对会后悔自己没在估值最高的时候卖掉它。Loveable 在我看来已经死了，下一个会是谁？\n看到大家在网络上抱怨，我觉得这真的太蠢了。我才知道竟然会有如此天真且愚蠢的想法\n","date":"2025-12-28T17:14:00+08:00","permalink":"https://gitsang.github.io/p/2025-anual/","title":"2025 年终总结"},{"content":" 1. Method 1 2 3 4 5 6 systemctl stop [servicename] systemctl disable [servicename] rm /your/service/locations/[servicename] rm /your/service/locations/[servicename] # and symlinks that might be related systemctl daemon-reload systemctl reset-failed Systemd uses unit (file to define services) to remove a service the unit have to be removed\u0026hellip; here is a list of unit locations :1\n1 2 3 4 5 6 /etc/systemd/system/ (and sub directories) /usr/local/etc/systemd/system/ (and sub directories) ~/.config/systemd/user/ (and sub directories) /usr/lib/systemd/ (and sub directories) /usr/local/lib/systemd/ (and sub directories) /etc/init.d/ (Converted old service system) You can easily find location in loaded property using systemctl status [service]\n1 2 3 4 5 $ systemctl status bluetooth.service bluetooth.service - Bluetooth service Loaded: loaded (/lib/systemd/system/bluetooth.service; disabled; vendor preset: enabled) Active: inactive (dead) Docs: man:bluetoothd(8) Or using systemctl cat [service]\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # systemctl cat frps.service # /lib/systemd/system/frps.service [Unit] Description=Frp Server Service After=network.target [Service] Type=simple User=nobody Restart=on-failure RestartSec=5s ExecStart=/usr/bin/frps -c /etc/frp/frps.ini [Install] WantedBy=multi-user.target 1.1 About \u0026ldquo;reset-failed\u0026rdquo; Option From the systemd man page:\nreset-failed [PATTERN\u0026hellip;]\nReset the \u0026ldquo;failed\u0026rdquo; state of the specified units, or if no unit name is passed, reset the state of all units. When a unit fails in some way (i.e. process exiting with non-zero error code, terminating abnormally or timing out), it will automatically enter the \u0026ldquo;failed\u0026rdquo; state and its exit code and status is recorded for introspection by the administrator until the service is restarted or reset with this command.\n2. Reference How to remove systemd services\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-05-12T10:36:53+08:00","permalink":"https://gitsang.github.io/p/how-to-remove-systemd-service/","title":"Removing Systemd Services Safely"},{"content":" 1. 什么是闭包 所谓的闭包，其实就是存储了函数及其关联环境的一个实体，使其在脱离上下文时照常运行。\n从字面上理解，闭包就是将函数封闭、打包（中华文化博大精深，闭包这两个字明显比 Closure 更加贴切）\n封闭指的是：封闭外部状态，即内部环境无法访问到外部状态，或者说外部状态无法对内部产生影响\n打包指的是：为了能够脱离外部环境而存在，要将需要用到的外部环境打包到自己的内部空间\n我认为许多人在理解闭包时，仅仅理解到了闭包能够扩展变量的作用域这一层面，而忽略了闭包的封闭性（或者说是隔离性），\n2. 如何获得闭包 通俗地说，获得闭包的方式就是将函数作为值返回\n3. 闭包和对象 / 函数的区别 首先应该明确的是：闭包既非对象，也不是函数或作用域\nA closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).1\n闭包是捆绑在一起（封闭）的函数与对其周围状态（词法环境）的引用的组合。\n闭包应该是一个组合，在中英文 wiki 中分别被解释成了“结构体”和“记录”，但都不应该简单地将其理解为对象或者函数。\n闭包与对象和函数的联系应该是：闭包相当于一个带状态的函数，闭包相当于只有一个函数的对象。2\n并且实际上对象系统能够基于闭包实现。3\n4. 参考 Closures - JavaScript | MDN\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n闭包和对象的关系 - Todd Wei - 博客园\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nRe: FP, OO and relations. Does anyone trump the others?. 29 December 1999 [2008-12-23]\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-05-09T19:43:07+08:00","permalink":"https://gitsang.github.io/p/understanding-closures-in-programming/","title":"理解编程语言中的闭包设计"},{"content":" 非 LVM 分区实现动态扩容，适用于系统分区扩容，无需格式化磁盘，无需重新挂载磁盘\n1. 扩容步骤 以 /dev/sda2 扩容为例，假设 /dev/sda 空间足够（或已通过虚拟化管理平台增加容量）\n使用 fdisk -l 命令可看到 /dev/sda 磁盘总容量 200GiB，/dev/sda2 分区容量 100GiB\n1 2 3 4 5 6 7 8 9 10 11 Disk /dev/sda: 200 GiB, 214748364800 bytes, 419430400 sectors Disk model: QEMU HARDDISK Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: gpt Disk identifier: 1459CE9A-A8E1-4934-8EC6-17C7BA97E9E0 Device Start End Sectors Size Type /dev/sda1 2048 4095 2048 1M BIOS boot /dev/sda2 4096 419430366 209711087 100G Linux filesystem 现将 /dev/sda2 分区扩容到 200GiB\n1.1 重新分区 1 fdisk /dev/sda 输入 p 查看分区情况，确认 /dev/sda2 所在的位置和大小。 输入 d 删除分区 /dev/sda2。 输入 n 创建一个新的分区。 选择主分区或扩展分区（根据你的需要）。 按照提示输入分区编号（如果有多个分区）。 按照提示输入新的起始扇区（通常是默认值）。 按照提示输入新的结束扇区，确保分区大小为200G。 如果出现提示是否保留文件索引，选择 保留 输入 w 保存更改并退出。 再次输入 fdisk -l 应该能看到 /dev/sda2 已扩容成功\n1 2 3 4 5 6 7 8 9 10 11 Disk /dev/sda: 200 GiB, 214748364800 bytes, 419430400 sectors Disk model: QEMU HARDDISK Units: sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disklabel type: gpt Disk identifier: 1459CE9A-A8E1-4934-8EC6-17C7BA97E9E0 Device Start End Sectors Size Type /dev/sda1 2048 4095 2048 1M BIOS boot /dev/sda2 4096 419430366 419426271 200G Linux filesystem 1.2 扩展文件系统 输入 df -h 命令查看文件系统大小\n1 2 3 Filesystem Size Used Avail Use% Mounted on tmpfs 6.3G 1.6M 6.3G 1% /run /dev/sda2 99G 81G 18G 81% / 可以看到 /dev/sda2 文件系统空间仍未改变\n对于 ext4 系统，可以使用 resize2fs /dev/sda2 命令扩展文件系统\n再次输入 df -h 命令，可以看到 /dev/sda2 文件系统空间完成扩容\n1 2 3 4 5 6 Filesystem Size Used Avail Use% Mounted on tmpfs 6.3G 1.6M 6.3G 1% /run /dev/sda2 197G 81G 108G 43% / tmpfs 32G 0 32G 0% /dev/shm tmpfs 5.0M 0 5.0M 0% /run/lock tmpfs 6.3G 4.0K 6.3G 1% /run/user/1000 ","date":"2025-05-09T19:17:37+08:00","permalink":"https://gitsang.github.io/p/lvm/","title":"非 LVM 分区动态扩容"},{"content":" 1. 背景 背景：\n假设只有一个二级域名 domain.com，有多套环境的情况下，可能需要分配不同的三级子域名 sub.domain.com，每个环境可能需要再配置四级子域名 sub.sub.domain.com 如果使用一个 AK 拥有所有域名的 DNS 权限，可能不太安全（即使互相信任，也无法避免误操作导致影响其他环境） 需求：\n每套环境需要拥有自己的一套 AK，并能自己管理自己的域名，互不冲突 2. 操作步骤 2.1 添加子域名 阿里云的 RAM 访问控制中，不允许使用通配等方式配置域名资源（因为操作是针对 AUTHORITY SECTION 的），因此必须先拆分出子域名。\n首先在 域名解析 页面添加子域名（本文以 env1.domain.com 为例）\n添加域名需要 TXT 记录验证\n按照提示要求在你的主域名添加对应的 TXT 主机记录后点击验证即可添加成功。\n2.2 创建子域名的 RAM 权限策略 其策略类似如下（需要把 ${your-sub-domain} 改为刚才创建的子域名，如本文的 env1.domain.com），此处虽然可以使用通配，但名称必须是域名解析中列出的域名值。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { \u0026#34;Version\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;Statement\u0026#34;: [ { \u0026#34;Action\u0026#34;: \u0026#34;alidns:*\u0026#34;, \u0026#34;Resource\u0026#34;: \u0026#34;acs:alidns:::domain/${your-sub-domain}\u0026#34;, \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34; }, { \u0026#34;Action\u0026#34;: [\u0026#34;alidns:Describe*\u0026#34;], \u0026#34;Resource\u0026#34;: \u0026#34;acs:alidns:::*\u0026#34;, \u0026#34;Effect\u0026#34;: \u0026#34;Allow\u0026#34; } ] } 这里配置了两个策略：\n第一个策略是允许 acs:alidns:::domain/${your-sub-domain} 资源的所有 alidns:* 操作 第二个策略是允许所有 acs:alidns:::* 资源的 Describe alidns:Describe* 操作（此处可能还需要 Describe 其他的资源，阿里云文档和客服并没有给出明确的答复） 2.3 子账号赋权和 AK 申请 新建一个 RAM 用户，需要勾选 OpenAPI 调用访问\n之后进入用户详情创建 AK\n进入权限管理为用户赋予刚才创建的策略，或者你也可以为用户组赋权后将用户加入用户组（推荐）\n2.4 使用 AK 之后根据使用的不同的 ACME 或 DDNS 服务等的文档配置 AK 即可\n","date":"2025-05-09T19:17:31+08:00","permalink":"https://gitsang.github.io/p/ram-permissions-and-subdomain-management-in-alibaba-cloud/","title":"阿里云配置子域名及其 RAM 访问权限"},{"content":" 1. 背景 Windows 创建的虚拟机只能通过 NAT 上网（桥接需要过认证），但需要桥接到内网供外部访问。\n此场景使用的两张网卡会同时被设置为默认网关，出口流量有概率从桥接网卡出口导致无法访问。\n2. 路由表基础操作 2.1 查看路由策略 1 2 3 4 5 [root@localhost ~]# ip rule 0: from all lookup local 32766: from all lookup main 32767: from all lookup default 2.2 查看路由表 1 2 3 4 5 [root@localhost ~]# ip route list default via 172.21.208.1 dev eth0 proto dhcp metric 101 10.60.20.0/24 dev eth1 proto kernel scope link src 10.60.20.12 metric 100 172.21.208.0/20 dev eth0 proto kernel scope link src 172.21.219.71 metric 101 2.3 新增默认路由 1 [root@localhost ~]# ip route add default via 10.60.20.254 dev eth1 2.4 删除默认路由 1 [root@localhost ~]# ip route del default via 10.60.20.254 dev eth1 3. 多网口出口配置 一般配置多网卡时，两张网卡都会被配置为默认路由，使用 ip route list 查看能看到类似如下配置\n1 2 3 4 5 6 [root@localhost ~]# ip route list default via 10.60.20.254 dev eth1 proto dhcp metric 100 default via 172.21.208.1 dev eth0 proto dhcp metric 101 10.60.20.0/24 dev eth1 proto kernel scope link src 10.60.20.12 metric 100 172.21.208.0/20 dev eth0 proto kernel scope link src 172.21.219.71 metric 101 对于 default via 10.60.20.254 dev eth1 proto dhcp metric 100\ndefault 表示默认路由 via 10.60.20.254 表示数据包将发往 10.60.20.254 这个目标 IP 地址 dev eth1 指定数据包将从 eth1 接口发送 proto dhcp 表示该路由规则是通过 DHCP 协议动态分配的 metric 100 表示优先级度量值（越小优先级越高） 对于 10.60.20.0/24 dev eth1 proto kernel scope link src 10.60.20.12 metric 100\n这是一个具体的子网路由规则，用于指定数据包如何到达 10.60.20.0/24 子网。 10.60.20.0/24 表示目标子网地址范围 dev eth1 指定数据包将从 eth1 接口发送 proto kernel 表示该路由规则由内核自动生成 scope link 表示这是一个本地链接，即目标 IP 地址与本机直接相连 src 10.60.20.12 表示源 IP 地址，即从 10.60.20.12 发送到目标子网 metric 100 表示优先级度量值（越小优先级越高） 如果不希望访问外网时通过 10.60.20.254 网关，只需将第一条路由删除即可，执行\n1 [root@localhost ~]# ip route del default via 10.60.20.254 dev eth1 验证除了 10.60.20.0/24 的地址，都会走 eth0 网卡\n1 2 3 4 5 6 7 8 9 10 11 [root@localhost ~]# ip route get 10.60.20.10 10.60.20.10 dev eth1 src 10.60.20.12 cache [root@localhost ~]# ip route get 1.1.1.1 1.1.1.1 via 172.21.208.1 dev eth0 src 172.21.219.71 cache [root@localhost ~]# ip route get 8.8.8.8 8.8.8.8 via 172.21.208.1 dev eth0 src 172.21.219.71 cache 4. 保存设置 以上路由会在重启后被清理，应使用 ip route save 保存信息\n1 sudo ip route save table main \u0026gt; /var/opt/route-main.rules 使用 ip route restore 恢复\n1 2 sudo ip route flush table main sudo ip route restore table main \u0026lt; /var/opt/route-main.rules 也可以将其写为 systemd 脚本，当网卡准备完成后执行\n","date":"2025-05-09T19:17:19+08:00","permalink":"https://gitsang.github.io/p/configuring-multi-nic-routing-in-windows-vms/","title":"Configuring Multi-NIC Routing in Windows VMs"},{"content":" 1. 背景 我们一般会使用 fail2ban 来保护暴露到公网的提供密码登录的 ssh 连接等。\n但使用 frp 穿透后所有的从外网访问都会变成 127.0.0.1 进入的，原本能用 fail2ban 保护的如 ssh 服务将无法使用。\n因此 fail2ban 应该放到 frps 服务器上。但 frps 的日志并不会对失败进行辨别，无论你访问哪个服务，frp 日志只会有连接和断开两种日志。\n1.1 不完美的解决途径 正常情况下，我们不会频繁地连接和断开，只有被扫描时才容易出现。\n因此添加自定义 filter，并设置一段时间内连接超过阈值后进入监狱。\n编写文件 /etc/fail2ban/filter.d/frps.conf\n1 2 3 4 5 [Definition] failregex = ^.*get a user connection \\[\u0026lt;HOST\u0026gt;:[0-9]*\\] ^.*get a new work connection: \\[\u0026lt;HOST\u0026gt;:[0-9]*\\] ignoreregex = 编写文件 /etc/fail2ban/jail.local 添加\n1 2 3 4 5 6 7 8 9 10 11 [frp] enabled = true findtime = 10m maxretry = 100 bantime = 1d filter = frps logpath = /data/frp/log/frps.log protocol = all chain = all port = all action = iptables-allports[name=frp,protocol=tcp] 记得 fail2ban-client reload 重载服务和 fail2ban-client status frp 确认服务状态。\n如果你要添加自己的过滤规则可以使用 fail2ban-regex \u0026lt;LOG\u0026gt; \u0026lt;REGEX\u0026gt; [IGNOREREGEX] 进行验证，比如 fail2ban-regex /data/frp/log/frps.log /etc/fail2ban/filter.d/frps.conf （记得要用绝对路径）\n然后你可以把阈值改小一点，用多次 telnet 来验证是否能过成功封锁。 然后用 fail2ban-client set frp unbanip 12.36.14.241 来解除封锁。\n2. Outlook 不是很完美的方案，比如如果是 http 连接，很可能超过限制，实际使用需要做一些排除的匹配。\n也许能通过 tcpdump 抓包日志来进行过滤，或编写程序输出一个更清晰的日志。\n","date":"2025-05-09T19:16:59+08:00","permalink":"https://gitsang.github.io/p/fail2ban-frp/","title":"使用 fail2ban 保护通过 frp 穿透的服务"},{"content":"Differences 这个问题似乎在社区中，并没有被广泛的讨论，大多数讨论都认为两个芯片组并无明显差 异1。比较详细的讨论是 Rediit 中的这篇讨论2\n一个常被提到的区别是，q35 原生支持 PCIE，而 i440fx 并不完全支持。因此通常在进行 GPU 直通时，可能会优先使用 q35（Reddit 中有人提到 i440fx 会将 PCIE 设备模拟成 PCI，但仍然以 PCIE 速度运行，所以可能也并不会有明显区别3）。\n另外，在 reddit 中，有人提到 i440fx 支持热插拔，所以据说可以将 GPU 删除以允许 VM 保存状态而不是暂停/关闭？这反过来允许 VM 在运行时使用完成快照。4\n在这个回复中，有人提到 q35 的限制5: Limited IO space can affect the number of devices used by a single Q35 machine（有限的 IO 空间可能会影响单个 Q35 机 器使用的设备数量），这个问题我似乎在某篇直通文章中见过有人提到当 GPU 和 USB 设 备超过一定数量时，会导致虚拟机无法启动，但记不起细节了。\n另一篇文章6则提到：很长一段时间以来，q35 不被建议用于 GPU 直通，因为某些部 分没有完全解决，这似乎与主流文章（至少是国内大部分文章）描述不一致，但文章中并 没有详细说明这些问题。我一直在使用 q35 进行 GPU 直通，而目前并没有遇到过 q35 带 来的问题。\nReference q35 vs i440fx | Proxmox Support Forum\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nDifferences/benefits between i440fx and q35 chipsets? : r/VFIO\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.reddit.com/r/VFIO/comments/5ireij/comment/dbafh86/?utm_source=share\u0026utm_medium=web3x\u0026utm_name=web3xcss\u0026utm_term=1\u0026utm_content=share_button\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.reddit.com/r/VFIO/comments/5ireij/comment/dbagbbd/?utm_source=share\u0026utm_medium=web3x\u0026utm_name=web3xcss\u0026utm_term=1\u0026utm_content=share_button\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://www.reddit.com/r/VFIO/comments/5ireij/comment/dbb2e01/?utm_source=share\u0026utm_medium=web3x\u0026utm_name=web3xcss\u0026utm_term=1\u0026utm_content=share_button\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nVirtualized Windows 10 – i440FX vs Q35\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-02-08T11:15:25+08:00","permalink":"https://gitsang.github.io/p/i440fx-vs-q35/","title":"i440fx vs q35"},{"content":"Step by step 1. Move vm disks file to another storage Just move file or in UI use Hardware -\u0026gt; Disk -\u0026gt; Disk Action -\u0026gt; Move Storage\n2. Configure vm config file Config /etc/pve/nodes/{node_name}/qemu-server/{vm_number}.conf and change all old disk path to new disk path. (Especially the snapshot config)\n","date":"2025-02-07T15:04:52+08:00","permalink":"https://gitsang.github.io/p/pve-move-vm-disks-to-another-storage/","title":"PVE move vm disks to another storage"},{"content":" 1. Machine initialization 1.1 Disable swap Run sudo swapoff -a then configure /etc/fstab\n1.2 Configure kernel parameters 1 2 3 4 cat \u0026lt;\u0026lt;EOF | sudo tee /etc/modules-load.d/containerd.conf overlay br_netfilter EOF 1 2 sudo modprobe overlay sudo modprobe br_netfilter 1 2 3 4 5 cat \u0026lt;\u0026lt;EOF | sudo tee /etc/sysctl.d/99-kubernetes-k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.ipv4.ip_forward = 1 net.bridge.bridge-nf-call-ip6tables = 1 EOF 1 sudo sysctl --system 1.3 Install containerd 1 2 sudo apt update sudo apt -y install containerd 1.4 Configure containerd Config file is in /etc/containerd/config.toml\nGenerate default containerd config file 1 containerd config default | sudo tee /etc/containerd/config.toml \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 Set cgroup driver to systemd. 1 2 3 [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;.containerd.runtimes.runc.options] - SystemdCgroup = false + SystemdCgroup = true Change pause image 1 2 3 [plugins.\u0026#34;io.containerd.grpc.v1.cri\u0026#34;] - sandbox_image = \u0026#34;registry.k8s.io/pause:3.6\u0026#34; + sandbox_image = \u0026#34;registry.k8s.io/pause:3.10\u0026#34; In China use registry.aliyuncs.com/google_containers/pause:3.10 instead.\nRestart containerd 1 2 sudo systemctl restart containerd sudo systemctl enable containerd 1.5 Install Kubernetes Tools Follow Installing kubeadm, kubelet and kubectl\nIn China follow https://developer.aliyun.com/mirror/kubernetes to use mirror.\nInstall prerequisite packages. 1 2 3 sudo apt update # apt-transport-https may be a dummy package; if so, you can skip that package sudo apt install -y apt-transport-https ca-certificates curl gpg Configure repository keyrings 1 2 export K8S_VERSION=v1.32 sudo mkdir -p -m 755 /etc/apt/keyrings 1 2 curl -fsSL \u0026#34;https://pkgs.k8s.io/core:/stable:/${K8S_VERSION}/deb/Release.key\u0026#34; | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo \u0026#34;deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/${K8S_VERSION}/deb/ /\u0026#34; | sudo tee /etc/apt/sources.list.d/kubernetes.list In China use:\n1 2 curl -fsSL \u0026#34;https://mirrors.aliyun.com/kubernetes-new/core/stable/${K8S_VERSION}/deb/Release.key\u0026#34; | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo \u0026#34;deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.aliyun.com/kubernetes-new/core/stable/${K8S_VERSION}/deb/ /\u0026#34; | sudo tee /etc/apt/sources.list.d/kubernetes.list Install tools 1 2 3 sudo apt update sudo apt install -y kubelet kubeadm kubectl # sudo apt-mark hold kubelet kubeadm kubectl Enable service 1 sudo systemctl enable --now kubelet In this time journalctl -f -u kubelet should failure by error: failed to load Kubelet config file /var/lib/kubelet/config.yaml, it\u0026rsquo;s ok, we will fix it later.\n2. Install k8s cluster 2.1 Configure hostnames Configure hostname\n1 2 3 sudo hostnamectl set-hostname \u0026#34;k8s-master.local\u0026#34; // Run on master node sudo hostnamectl set-hostname \u0026#34;k8s-worker-01.local\u0026#34; // Run on 1st worker node sudo hostnamectl set-hostname \u0026#34;k8s-worker-02.local\u0026#34; // Run on 2nd worker node Configure /etc/hosts\n1 2 3 192.168.5.100 k8s-master.local k8s-master 192.168.5.101 k8s-worker-01.local k8s-worker-01 192.168.5.102 k8s-worker-02.local k8s-worker-02 2.2 Install k8s cluster with kubeadm (run on control panel node) Create kubelet.yaml\n1 2 3 4 5 6 7 8 9 10 11 apiVersion: kubeadm.k8s.io/v1beta4 kind: InitConfiguration --- apiVersion: kubeadm.k8s.io/v1beta4 kind: ClusterConfiguration kubernetesVersion: \u0026#34;1.32.0\u0026#34; # Replace with your desired version controlPlaneEndpoint: \u0026#34;k8s-master\u0026#34; # Replace with your desired control plane endpoint imageRepository: registry.k8s.io --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration In China use imageRepository: registry.aliyuncs.com/google_containers.\nInstall control panel\n1 sudo kubeadm init --config kubelet.yaml Use sudo kubeadm reset if you want to reset k8s cluster.\nConfigure default kube config\n1 2 3 mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config 2.3 Join k8s cluster with kubeadm (run on worker nodes) 1 2 sudo kubeadm join k8s-master:6443 --token 21nm87.x1lgd4jf0lqiiiau \\ --discovery-token-ca-cert-hash sha256:28b503f1f2a2592678724c482776f04b445c5f99d76915552f14e68a24b78009 2.4 Check k8s cluster status (run on control panel node) 1 sudo kubectl get nodes 3. Setup Pod Network 3.1 Install Calico (run on control panel node) Install Calico\n1 sudo kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.2/manifests/calico.yaml In China:\n1 2 curl -sSLO https://raw.githubusercontent.com/projectcalico/calico/v3.29.2/manifests/calico.yaml sed -i \u0026#39;s/docker.io/dockerhub.icu/g\u0026#39; calico.yaml Verify\n1 sudo kubectl get pods -n kube-system When finished, you should see the calico-node pod running by sudo kubectl get pods -n kube-system and see all nodes ready by sudo kubectl get nodes.\n4. Test 1 2 3 sudo kubectl create deployment nginx-app --image=nginx --replicas 2 sudo kubectl expose deployment nginx-app --name=nginx-web-svc --type NodePort --port 80 --target-port 80 sudo kubectl describe svc nginx-web-svc Curl using either of worker node\u0026rsquo;s hostname\n1 curl http://k8s-worker-01:32283 5. Reference How to Install Kubernetes Cluster on Debian 12 | 11 - Linuxtechi Docker hub mirror Kubernetes mirrors - aliyun ","date":"2025-02-07T14:01:24+08:00","permalink":"https://gitsang.github.io/p/install-k8s-cluster-on-debian/","title":"Install k8s cluster on debian"},{"content":" 1. sysctl 配置 在某些情况下，IPv6 并不会自动配置，需要手动开启 sysctl 选项\n编辑 /etc/sysctl.conf 文件，并在末尾添加以下内容，输入 sysctl -p 立即生效配置\n此选项将开启 IPv6 功能，通常情况下，只需要配置此选项即可开启 IPv6\n1 2 net.ipv6.conf.default.disable_ipv6 = 0 net.ipv6.conf.all.disable_ipv6 = 0 此选项将允许数据包在 Interface 之间转发，当你的网络接口是使用桥接时，通常需要开启此选项\n1 2 net.ipv6.conf.default.forwarding = 1 net.ipv6.conf.all.forwarding = 1 此选项将允许接收路由器通告，如果上面两个选项配置后仍然无法获取 IPv6 地址，可以尝试开启以下选项\n1 2 net.ipv6.conf.all.accept_ra = 1 net.ipv6.conf.default.accept_ra = 1 此选项用于开启 SLAAC (Stateless Address Autoconfiguration)，一般默认是开着的\n1 2 net.ipv6.conf.all.autoconf = 1 net.ipv6.conf.default.autoconf = 1 2. 网络接口配置 编辑 /etc/network/interfaces\n1 iface vmbr0 inet6 auto 当 inet6 配置为 auto 时将使用 SLAAC 自动配置\n","date":"2024-12-04T10:43:07+08:00","permalink":"https://gitsang.github.io/p/enable-ipv6-slaac/","title":"开启 IPv6 SLAAC 自动配置"},{"content":" 1. 核心规则回顾 go 指令\n表示模块的 最低语言版本兼容性（代码必须能在该版本运行）。 影响：语言特性、标准库行为、模块解析规则等。 toolchain 指令\n建议使用的工具链版本（编译器/链接器），但 不强制（除非显式配置或版本不满足要求）。 影响：编译器优化、构建速度、生成的二进制质量等。 工具链选择优先级（Go 1.21+）：\n如果当前 Go 版本 ≥ toolchain 版本 → 直接使用当前版本。 如果当前 Go 版本 \u0026lt; toolchain 版本 → 尝试下载或切换到指定工具链（需满足 GOTOOLCHAIN 配置）。 2. 场景分类与分析 2.1 项目与依赖的 go 版本相同，toolchain 不同 示例：\n1 2 3 4 5 6 7 // 你的项目 go 1.22.0 toolchain go1.22.6 // 显式指定 // 依赖包 go 1.22.0 toolchain go1.23.1 行为：\n你的项目用 go1.22.6 编译，依赖包的 toolchain go1.23.1 会被忽略（因为 1.22.6 \u0026lt; 1.23.1 但未强制切换）。 风险：依赖包可能依赖 1.23.1 的优化，但实际未生效。 2.2 项目的 go 版本 \u0026gt; 依赖的 go 版本 示例：\n1 2 3 4 5 6 // 你的项目 go 1.23.0 // 依赖包 go 1.21.0 toolchain go1.22.0 行为：\n依赖包的 toolchain go1.22.0 会被忽略（因为你的工具链 1.23.0 \u0026gt; 1.22.0）。 优势：高版本 Go 通常兼容低版本模块。 2.3 项目的 go 版本 \u0026lt; 依赖的 go 版本（危险！） 示例：\n1 2 3 4 5 6 // 你的项目 go 1.21.0 // 依赖包 go 1.22.0 toolchain go1.23.1 行为：\n编译报错！因为你的项目要求 Go 1.21.0，但依赖包需要 ≥1.22.0。 解决方案：升级项目的 go 版本或降低依赖包版本。 2.4 依赖包未指定 toolchain 示例：\n1 2 3 4 5 6 // 你的项目 go 1.22.0 toolchain go1.22.6 // 依赖包 go 1.22.0 // 无 toolchain 行为：\n依赖包默认使用与 go 指令相同的工具链（即假设 toolchain go1.22.0）。 你的项目仍用 go1.22.6 编译（因为 1.22.6 \u0026gt; 1.22.0）。 2.5 显式强制工具链切换（通过 GOTOOLCHAIN） 环境变量： 1 export GOTOOLCHAIN=go1.23.1+auto # 自动下载缺失版本 行为： 即使当前版本是 go1.22.6，也会强制切换到 go1.23.1 编译。 适用场景：需要严格匹配依赖包的工具链建议时。 3. 特殊场景与边界情况 3.1 依赖包的 toolchain 版本 \u0026lt; 项目的 go 版本 示例：\n1 2 3 4 5 6 7 // 你的项目 go 1.23.0 toolchain go1.23.1 // 依赖包 go 1.22.0 toolchain go1.22.0 行为：\n依赖包的 toolchain go1.22.0 会被忽略（因为 1.23.1 \u0026gt; 1.22.0）。 仍用 go1.23.1 编译。 3.2 跨主版本兼容性问题（如 Go1 → Go2） 规则： Go 1.x 无法编译依赖 Go 2.x 的模块（未来可能需要显式升级）。 目前 Go 2 尚未发布，但设计上会通过 go.mod 的 go 指令隔离。 3.3 工具链自动下载（Go 1.21+） 条件： 当前 Go 版本 \u0026lt; 依赖包的 toolchain 版本。 GOTOOLCHAIN 设置为 auto 或 latest。 行为： 自动下载并切换到指定工具链（如 go1.23.1）。 4. 决策流程图 1 2 3 4 5 6 7 8 9 10 11 12 开始编译 │ ├─ 检查项目的 go 和 toolchain 版本 │ ├─ 若 toolchain 指定且当前版本不满足 → 尝试切换或下载 │ └─ 否则使用当前版本 │ ├─ 检查所有依赖包的 go 和 toolchain 版本 │ ├─ 若依赖包的 go 版本 \u0026gt; 项目的 go 版本 → 报错 │ ├─ 若依赖包的 toolchain 版本 \u0026gt; 当前版本 → 根据 GOTOOLCHAIN 决定是否切换 │ └─ 否则忽略依赖包的 toolchain │ └─ 使用最终确定的工具链版本编译 5. 最佳实践建议 保持 go 版本与依赖包一致\n避免因版本差异导致隐式问题（如 go 1.22.0 的项目依赖 go 1.21.0 的包是安全的，反之则危险）。 谨慎使用 toolchain 指令\n仅在需要特定工具链优化或修复时使用，避免过度约束。 利用 GOTOOLCHAIN 控制环境\n在 CI/CD 中设置 GOTOOLCHAIN=latest+auto 确保一致性。 定期检查依赖包的版本声明\n使用 go list -m all 查看依赖树，确保无版本冲突。 6. 总结 能编译成功的组合：项目的 go 版本 ≥ 依赖包的 go 版本。 工具链的影响：仅当依赖包的 toolchain \u0026gt; 当前版本且配置允许时才会切换。 最危险场景：项目的 go 版本 \u0026lt; 依赖包的 go 版本（直接报错）。 ","date":"2024-05-08T15:16:00+08:00","permalink":"https://gitsang.github.io/p/go-module-and-toolchain/","title":"Go Module 管理和 Toolchain 配置"},{"content":" 使用 iptables -L 可能会看到类似如下的配置\n1 2 3 4 Chain INPUT (policy ACCEPT) target prot opt source destination ACCEPT all -- anywhere anywhere ctstate RELATED,ESTABLISHED ACCEPT all -- anywhere anywhere 第二条规则看上去像是允许了任意来源和目标的流量，但实际验证的效果却是后续的防火墙规则仍然在工作。\n这是因为，第二条规则实际上仅针对本地回环 (lo) 接口，当使用 iptables -L 查看时进行了省略，使用 iptables-save 可以得到完整的防火墙规则如下\n1 2 -A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A INPUT -i lo -j ACCEPT ","date":"2024-05-07T11:11:00+08:00","permalink":"https://gitsang.github.io/p/iptables-local-loopback-ambiguous-config/","title":"Iptables 本地回环接口容易造成误解的配置"},{"content":" 1. 背景 当 docker 使用端口映射时， docker daemon 会创建 DOCKER 链绕过 firewalld 建立 iptables 规则，可能使 firewall 规则失效。\n可以通过修改 DOCKER-USER 链来管理 docker 的防火墙规则或禁用 firewalld 直接配置 iptables（不推荐）\n1.1 停止 docker 不要在 Docker 运行时 Reload firewalld，否则会导致 Docker 链被删除\n1 systemctl stop docker 1.2 清除并重建自定义规则链 1 2 3 firewall-cmd --permanent --direct --remove-chain ipv4 filter DOCKER-USER firewall-cmd --permanent --direct --remove-rules ipv4 filter DOCKER-USER firewall-cmd --permanent --direct --add-chain ipv4 filter DOCKER-USER 1.3 允许 Docker 容器出站流量返回 使用 conntrack 模块匹配 RELATED, ESTABLISHED 两种状态的连接\n1 2 3 4 5 6 # 允许出站流量返回，因为建立连接 ESTABLISHED 的数据包已经通过了防火墙的出站规则。 # 此规则优先级为 1 # 没有完全理解这个逻辑，但是加了这条容器内就可以联网了 firewall-cmd --permanent --direct --add-rule ipv4 filter DOCKER-USER 1 \\ -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT \\ -m comment --comment \u0026#39;Allow containers to connect to the outside world\u0026#39; 1.4 配置白名单 1 2 3 4 # 允许来自 IP 段的所有流量 firewall-cmd --permanent --direct --add-rule ipv4 filter DOCKER-USER 1 \\ -s 10.60.22.0/24 -j ACCEPT \\ -m comment --comment \u0026#39;Allow IP 10.60.22.0/24 to access\u0026#39; 1.5 配置默认阻止 1 2 3 # 阻止其他的流量 firewall-cmd --permanent --direct --add-rule ipv4 filter DOCKER-USER 10 \\ -j REJECT -m comment --comment \u0026#39;reject all other traffic to DOCKER-USER\u0026#39; 1.6 Reload 防火墙 1 2 firewall-cmd --reload firewall-cmd --get-active-zones 1.7 重启 docker 1 systemctl start docker ","date":"2024-05-07T11:09:00+08:00","permalink":"https://gitsang.github.io/p/firewall-rule-for-docker-port-mapping/","title":"Docker 端口映射防火墙规则配置"},{"content":" 1. 白名单配置方法 以仅信任来自 10.60.22.0/24, 10.60.23.0/24 ip 端的连接为例\n1.1 配置信任来源 1 2 3 # 添加 IP 地址范围到 \u0026#34;trusted\u0026#34; 的区域 firewall-cmd --permanent --zone=trusted --add-source=10.60.22.0/24 firewall-cmd --permanent --zone=trusted --add-source=10.60.23.0/24 1.2 配置默认拒绝 1 2 3 4 # 将默认的防火墙区域设置为 \u0026#34;drop\u0026#34; firewall-cmd --set-default-zone=drop # 将网络接口 eth0 分配给 \u0026#34;drop\u0026#34; 区域 firewall-cmd --permanent --zone=drop --change-interface=eth0 1.3 Reload 防火墙 1 2 firewall-cmd --reload firewall-cmd --get-active-zones get-active-zones 应该会得到类似如下配置\n1 2 3 4 drop interfaces: eth0 trusted sources: 10.60.22.0/24 10.60.23.0/24 ","date":"2024-05-07T11:09:00+08:00","permalink":"https://gitsang.github.io/p/firewalld-white-list-configuration/","title":"使用 firewalld 配置白名单"},{"content":" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # create swap file dd if=/dev/zero of=/.swap bs=1048576 count=4096 # format swap mkswap /.swap # start swap swapon /.swap # check free -h # onboot echo \u0026#34;/.swap swap swap defaults 0 0\u0026#34; \u0026gt;\u0026gt; /etc/fstab ","date":"2021-04-13T11:38:00+08:00","permalink":"https://gitsang.github.io/p/using-swap/","title":"创建和自动挂载 Swap 分区"},{"content":"1. Backgroud Mount samba directly in wsl like linux is difficult\n1 2 3 4 Password for root@//filesystem.domain/root: mount error: cifs filesystem not supported by the system mount error(19): No such device Refer to the mount.cifs(8) manual page (e.g. man mount.cifs) 2. Solution But is easily mount net disk in windows file manager. So if your windows share is already mapped to a drive in the Windows host, it can be even simpler.\nSuppose you already mounted the share on Z:. In that case the following will work:1\n1 2 sudo mkdir /mnt/z sudo mount -t drvfs \u0026#39;Z:\u0026#39; /mnt/z 3. Reference Mounting a windows share in Windows Subsystem for Linux - Stack Overflow\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2021-03-19T11:03:00+08:00","permalink":"https://gitsang.github.io/p/how-to-mount-windows-network-disk-in-wsl/","title":"How to mount Windows network disk in WSL"},{"content":" 1. 使用场景 机器代号 操作系统 机器位置 IP 账户名 ssh/sshd 端口 OuterNet CentOS 7 公网 50.100.50.100 OuterUser 22 LocalNet CentOs 7 内网(局域网) 10.200.100.10 LocalUser 22 想要通过公网上的机器 OuterNet 访问内网机器 LocalNet\n并能够使用 OuterNet 机器的 10022 端口访问 LocalNet 机器的 22 端口\n相当于\n1 [OuterUser@OuterNet] $ ssh -p 10022 LocalUser@127.0.0.1 替代在内网时候\n1 $ ssh LocalUser@10.200.100.10 1.1 建立简单的 ssh 反向隧道 在 LocalNet 机器上执行以下命令建立隧道 1 [LocalUser@LocalNet] $ ssh -p 22 -qngfNTR 10022:localhost:22 OuterUser@50.100.50.100 或（以下参数似乎比较稳定）1\n1 [root@LocalNet] $ ssh -fN -R :55555:localhost:22 50.100.50.100 在 OuterNet 上连接 LocalNet 1 [OuterUser@OuterNet] $ ssh -p 10022 LocalUser@127.0.0.1 1.2 维持隧道 安装 autossh 1 [LocalUser@LocalNet] $ sudo yum install autossh 内网建立隧道 1 [LocalUser@LocalNet] $ autossh -p 22 -M 7777 -NR 10022:localhost:22 OuterUser@50.100.50.100 在 OuterNet 上连接 LocalNet 1 [OuterUser@OuterNet] $ ssh -p 10022 LocalUser@127.0.0.1 1.3 监听 0.0.0.0 如果需要监听 0.0.0.0 需要在服务端，即公网机器上开启 GatewayPorts\n在 /etc/ssh/sshd_config 中把 GatewayPorts 设为 yes\n2. 参考 ssh 端口转发：ssh 隧道\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2020-03-14T14:53:28+08:00","permalink":"https://gitsang.github.io/p/build-ssh-tunnel/","title":"搭建 SSH 隧道"},{"content":" 1. 虚函数 1.1 简述 所谓虚函数是指：在类中希望被重写(override)的虚构的函数。也就是说 C++ 可以在派生类(derived class)中通过重写基类(based class)的虚函数来实现对基类虚函数的覆盖(override)\n1.2 常见用法 最常见的用法就是：声明基类的指针，指向任意一个子类对象，调用相应虚函数，就调用了子类重写的函数。由于编写基类时候并不能确定将被调用的是那个派生类的函数，因此被称为“虚”函数。\n如果不使用虚函数，则使用基类指针时，将总是被限制在基类函数本身，无论如何都无法调用到子类重写的函数。\n1.3 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include \u0026lt;iostream\u0026gt; class Base { public: Base() { } public: void print() { std::cout \u0026lt;\u0026lt; \u0026#34;Base\u0026#34; \u0026lt;\u0026lt; std::endl; } virtual void vprint() { std::cout \u0026lt;\u0026lt; \u0026#34;vBase\u0026#34; \u0026lt;\u0026lt; std::endl; } }; class Derived : public Base { public: Derived() { } public: void print(){ std::cout \u0026lt;\u0026lt; \u0026#34;Derived\u0026#34; \u0026lt;\u0026lt; std::endl; } void vprint() { std::cout \u0026lt;\u0026lt; \u0026#34;vDerived\u0026#34; \u0026lt;\u0026lt; std::endl; } }; int main() { Base *p1 = new Base(); p1-\u0026gt;print(); p1-\u0026gt;vprint(); Derived *p2 = new Derived(); p2-\u0026gt;print(); p2-\u0026gt;vprint(); Base *p3 = new Derived(); p3-\u0026gt;print(); p3-\u0026gt;vprint(); return 0; } 代码中定义了一个基类 Base，并定义了一个函数 print() 和一个虚函数 vprint()，派生类 Derived 继承自 Base，并重写了 print 和 vprint 两个函数。\nmain 中分别 new 了 Base 和 Derived 对象，并调用自身的函数，这结果是很好预知的，一定是\n1 2 3 4 Base vBase Derived vDerived 之后定义了 基类指针 p3 并将其指向派生类，输出结果是：\n1 2 Base vDerived 这里就可以注意到基类指针调用函数 print() 时，实际上调用的是基类自身的 print()，即使这个指针已经指向了其派生类 Derived。\n1.4 结果解释 这是由于 C++ 在编译时，内部成员函数一般都是静态加载的，编译器对于非虚函数他的调用地址是写死的，会将其定义类的函数地址写到调用语句上，这就是静态联编。只有在编译器遇到虚函数时才会将调用修改为寄存器间接寻址，即为动态联编。\n因此，p3 虽然指向了派生类，但编译时仍然会给调用写上一个 Base::print() 的地址，即使编译器此时知道 p3 指向的并不是 Base，这是由编译逻辑决定的。\n虽然你也可以不用虚函数，而是直接定义一个派生类的对象来调用派生类的方法，但这样就已经不是一个接口了，这就不是多态了。\n1.5 总结 其实你也不必知道这么多的细节，你只要知道如果你想要仅仅暴露一个基类接口来实现多态，那么只需要为基类函数加上 virtual 标识符，然后用派生类重写该函数，最后将基类指针指向派生类就可以了。\n1.6 附录 使用 g++ 生成汇编代码 1 2 g++ -S -fverbose-asm -g t_virtual.cpp -o t_virtual.s as -alhnd t_virtual.s \u0026gt; t_virtual.as p3-\u0026gt;print() 的汇编 1 2 3 movq -40(%rbp), %rax movq %rax, %rdi call _ZN4Base5printEv # 地址标号直接寻址，跳转到 Base 类的 print p3-\u0026gt;vprint() 的汇编 1 2 3 4 5 6 movq -40(%rbp), %rax movq (%rax), %rax movq (%rax), %rax movq -40(%rbp), %rdx movq %rdx, %rdi call *%rax # 间接寻址 2. 参考 12345678\nC++构造/析构函数中的多态(二)\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n浅谈 C++多态性\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nC++多态\u0026ndash;虚函数 virtual 及 override\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nC++学习:虚函数,纯虚函数,虚继承,虚析构函数\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nC++ Virtual 详解\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nC++中 virtual（虚函数）的用法\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n虚函数的深入理解\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n为什么不直接用子类引用指向子类对象，而用父类引用指向子类对象\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2019-10-09T14:53:28+08:00","permalink":"https://gitsang.github.io/p/virtual-on-cpp-object-oriented/","title":"C++ 面向对象中的虚（Virtual）"},{"content":"1. 操作方法 默认情况下，curl 不会输出耗时信息，状态码等，若需要输出，需要使用 -w, --write-out FORMAT 选项配置 Write Out 格式。\n1 curl -w \u0026#34;\\n\\ntime_total: %{time_total}s\\n\u0026#34; https://www.example.com Write Out 中支持的变量请参考：\nhttps://everything.curl.dev/usingcurl/verbose/writeout#available-write-out-variables\n也可以使用文件\n1 curl -w \u0026#34;@curl-format.txt\u0026#34; https://www.example.com 一个简单的文件格式参考如下：\n1 2 3 4 5 6 7 8 9 \\n time_namelookup: %{time_namelookup}s\\n time_connect: %{time_connect}s\\n time_appconnect: %{time_appconnect}s\\n time_pretransfer: %{time_pretransfer}s\\n time_redirect: %{time_redirect}s\\n time_starttransfer: %{time_starttransfer}s\\n ----------\\n time_total: %{time_total}s\\n 用于 debug 的详细信息格式参考：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 \\n url_effective: %{url_effective}\\n ssl_verify_result: %{ssl_verify_result}\\n content_type: %{content_type}\\n filename_effective: %{filename_effective}\\n ftp_entry_path: %{ftp_entry_path}\\n http_code: %{http_code}\\n http_connect: %{http_connect}\\n local_ip: %{local_ip}\\n local_port: %{local_port}\\n num_connects: %{num_connects}\\n num_redirects: %{num_redirects}\\n redirect_url: %{redirect_url}\\n remote_ip: %{remote_ip}\\n remote_port: %{remote_port}\\n response_code: %{response_code}\\n size_download: %{size_download} bytes\\n size_header: %{size_header} bytes\\n size_request: %{size_request} bytes\\n size_upload: %{size_upload} bytes\\n speed_download: %{speed_download} bytes/s\\n speed_upload: %{speed_upload} bytes/s\\n time_appconnect: %{time_appconnect}s\\n time_connect: %{time_connect}s\\n time_namelookup: %{time_namelookup}s\\n time_pretransfer: %{time_pretransfer}s\\n time_redirect: %{time_redirect}s\\n time_starttransfer: %{time_starttransfer}s\\n time_total: %{time_total}s\\n ","date":"2019-10-09T14:53:28+08:00","image":"https://gitsang.github.io/p/curl-write-out-format/cover_hu_295f0cf7f15f88d1.jpg","permalink":"https://gitsang.github.io/p/curl-write-out-format/","title":"Curl 格式化输出"},{"content":" 1. Config edit ~/.docker/config.json 1\n1 2 3 { \u0026#34;detachKeys\u0026#34;: \u0026#34;ctrl-q,q\u0026#34; } 2. Reference docker ：把Ctrl+p换成别的什么了\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2021-09-24T15:57:22+08:00","permalink":"https://gitsang.github.io/p/enable-ctrl-p-in-docker/","title":"Enable Ctrl+P in Docker"},{"content":" 1. 背景 1 2 3 4 5 6 7 8 9 10 11 12 13 type AData struct { A string `json:\u0026#34;a\u0026#34;` } type BData struct { B string `json:\u0026#34;b\u0026#34;` } type Message struct { Name string `json:\u0026#34;name\u0026#34;` Id int `json:\u0026#34;id\u0026#34;` Data interface{} `json:\u0026#34;data\u0026#34;` } 对于 interface 类型的数据很容易实现序列化(不需要任何额外步骤)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 msgA := Message{ Name: \u0026#34;msg_a\u0026#34;, Id: 1, Data: AData{ A: \u0026#34;a_data\u0026#34;, }, } msgB := Message{ Name: \u0026#34;msg_b\u0026#34;, Id: 2, Data: BData{ B: \u0026#34;b_data\u0026#34;, }, } msgAJ, _ := json.Marshal(msgA) log.Info(\u0026#34;A\u0026#34;, zap.Reflect(\u0026#34;msgA\u0026#34;, msgA), zap.ByteString(\u0026#34;msgAJ\u0026#34;, msgAJ)) msgBJ, _ := json.Marshal(msgB) log.Info(\u0026#34;B\u0026#34;, zap.Reflect(\u0026#34;msgB\u0026#34;, msgB), zap.ByteString(\u0026#34;msgBJ\u0026#34;, msgBJ)) 但 interface 反序列化后会变成 map[string]interface 类型，想要转成 struct 只能使用 mapstructure 之类的库\n1 2 3 4 var msgX Message _ = json.Unmarshal(msgAJ, \u0026amp;msgX) log.Info(\u0026#34;X\u0026#34;, zap.Reflect(\u0026#34;msgX\u0026#34;, msgX), zap.Reflect(\u0026#34;msgX.Data.A\u0026#34;, msgX.Data.(AData).A)) // panic: interface conversion: interface {} is map[string]interface {}, not main.AData 此处是无法直接用 msgX.Data.A 来访问的，同样的 msgX.Data.(AData).A 也是不行的，因为这时候的 data 已经被反序列化成了 map[string]interface\n1.1 解决方法 1 解决方法也很简单，只要再反序列化时能够知道需要反序列化成的类型即可。\n在解析的时候定义临时 struct 继承 Message 并重新定义 Data 的类型。\n1 2 3 4 5 6 7 8 9 10 11 12 13 msgXA := struct { *Message Data AData `json:\u0026#34;data\u0026#34;` }{} _ = json.Unmarshal(msgAJ, \u0026amp;msgXA) log.Info(\u0026#34;XA\u0026#34;, zap.Reflect(\u0026#34;msgXA\u0026#34;, msgXA), zap.Reflect(\u0026#34;msgXA.Data.A\u0026#34;, msgXA.Data.A)) msgXB := struct { *Message Data BData `json:\u0026#34;data\u0026#34;` }{} _ = json.Unmarshal(msgBJ, \u0026amp;msgXB) log.Info(\u0026#34;XB\u0026#34;, zap.Reflect(\u0026#34;msgXB\u0026#34;, msgXB), zap.Reflect(\u0026#34;msgXB.Data.B\u0026#34;, msgXB.Data.B)) 1.2 解决方法 2 另一种思路是拆分 struct 每次序列化时将其合并，反序列化时再将其拆分12\n缺点是每次需要传送两个变量\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 type ShortMessage struct { Name string `json:\u0026#34;name\u0026#34;` Id int `json:\u0026#34;id\u0026#34;` } func TestJsonStructSplit(t *testing.T) { msgA := ShortMessage{ Name: \u0026#34;msg_a\u0026#34;, Id: 1, } dataA := AData{ A: \u0026#34;a_data\u0026#34;, } msgB := ShortMessage{ Name: \u0026#34;msg_b\u0026#34;, Id: 2, } dataB := BData{ B: \u0026#34;b_data\u0026#34;, } // marshal msgAJ, _ := json.Marshal(struct { *ShortMessage *AData }{\u0026amp;msgA, \u0026amp;dataA}) msgBJ, _ := json.Marshal(struct { *ShortMessage *BData }{\u0026amp;msgB, \u0026amp;dataB}) // unmarshal var msgXA ShortMessage var dataXA AData _ = json.Unmarshal(msgAJ, \u0026amp;struct { *ShortMessage *AData }{\u0026amp;msgXA, \u0026amp;dataXA}) var msgXB ShortMessage var dataXB BData _ = json.Unmarshal(msgBJ, \u0026amp;struct { *ShortMessage *BData }{\u0026amp;msgXB, \u0026amp;dataXB}) } 1.3 解决方法 3 只在反序列化时拆分12\n缺点是 Data 实际上被解析了两次（一次解析成了 map，另一次解析成了 struct），而且每次使用时候还必须进行类型转换\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var msgXA Message var dataXA AData _ = json.Unmarshal(msgAJ, \u0026amp;struct { *Message *AData `json:\u0026#34;data\u0026#34;` }{\u0026amp;msgXA, \u0026amp;dataXA}) msgXA.Data = dataXA t.Log(\u0026#34;msgXA\u0026#34;, msgXA, \u0026#34;data\u0026#34;, msgXA.Data.(AData).A) var msgXB Message var dataXB BData _ = json.Unmarshal(msgBJ, \u0026amp;struct { *Message *BData `json:\u0026#34;data\u0026#34;` }{\u0026amp;msgXB, \u0026amp;dataXB}) msgXB.Data = dataXB t.Log(\u0026#34;msgXB\u0026#34;, msgXB, \u0026#34;data\u0026#34;, msgXB.Data.(BData).B) 2. 完整测试代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 package main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;testing\u0026#34; ) type AData struct { A string `json:\u0026#34;a\u0026#34;` } type BData struct { B string `json:\u0026#34;b\u0026#34;` } type Message struct { Name string `json:\u0026#34;name\u0026#34;` Id int `json:\u0026#34;id\u0026#34;` Data interface{} `json:\u0026#34;data\u0026#34;` } var msgA = Message{ Name: \u0026#34;msg_a\u0026#34;, Id: 1, Data: AData{ A: \u0026#34;a_data\u0026#34;, }, } var msgB = Message{ Name: \u0026#34;msg_b\u0026#34;, Id: 2, Data: BData{ B: \u0026#34;b_data\u0026#34;, }, } func TestJsonStruct(t *testing.T) { // marshal msgAJ, _ := json.Marshal(msgA) msgBJ, _ := json.Marshal(msgB) // unmarshal msgXA := struct { *Message Data AData `json:\u0026#34;data\u0026#34;` }{} _ = json.Unmarshal(msgAJ, \u0026amp;msgXA) t.Log(\u0026#34;msgXA\u0026#34;, msgXA, \u0026#34;data\u0026#34;, msgXA.Data.A) msgXB := struct { *Message Data BData `json:\u0026#34;data\u0026#34;` }{} _ = json.Unmarshal(msgBJ, \u0026amp;msgXB) t.Log(\u0026#34;msgXB\u0026#34;, msgXB, \u0026#34;data\u0026#34;, msgXB.Data.B) } type ShortMessage struct { Name string `json:\u0026#34;name\u0026#34;` Id int `json:\u0026#34;id\u0026#34;` } var msgAS = ShortMessage{ Name: \u0026#34;msg_as\u0026#34;, Id: 1, } var dataA = AData{ A: \u0026#34;a_data\u0026#34;, } var msgBS = ShortMessage{ Name: \u0026#34;msg_bs\u0026#34;, Id: 2, } var dataB = BData{ B: \u0026#34;b_data\u0026#34;, } func TestJsonStructSplit(t *testing.T) { // marshal msgAJ, _ := json.Marshal(struct { *ShortMessage *AData }{\u0026amp;msgAS, \u0026amp;dataA}) msgBJ, _ := json.Marshal(struct { *ShortMessage *BData }{\u0026amp;msgBS, \u0026amp;dataB}) // unmarshal var msgXA ShortMessage var dataXA AData _ = json.Unmarshal(msgAJ, \u0026amp;struct { *ShortMessage *AData }{\u0026amp;msgXA, \u0026amp;dataXA}) t.Log(\u0026#34;msgXA\u0026#34;, msgXA, \u0026#34;data\u0026#34;, dataXA.A) var msgXB ShortMessage var dataXB BData _ = json.Unmarshal(msgBJ, \u0026amp;struct { *ShortMessage *BData }{\u0026amp;msgXB, \u0026amp;dataXB}) t.Log(\u0026#34;msgXB\u0026#34;, msgXB, \u0026#34;data\u0026#34;, dataXB.B) } func TestJsonStructFull(t *testing.T) { // marshal msgAJ, _ := json.Marshal(msgA) msgBJ, _ := json.Marshal(msgB) // unmarshal var msgXA Message var dataXA AData _ = json.Unmarshal(msgAJ, \u0026amp;struct { *Message *AData `json:\u0026#34;data\u0026#34;` }{\u0026amp;msgXA, \u0026amp;dataXA}) msgXA.Data = dataXA t.Log(\u0026#34;msgXA\u0026#34;, msgXA, \u0026#34;data\u0026#34;, msgXA.Data.(AData).A) var msgXB Message var dataXB BData _ = json.Unmarshal(msgBJ, \u0026amp;struct { *Message *BData `json:\u0026#34;data\u0026#34;` }{\u0026amp;msgXB, \u0026amp;dataXB}) msgXB.Data = dataXB t.Log(\u0026#34;msgXB\u0026#34;, msgXB, \u0026#34;data\u0026#34;, msgXB.Data.(BData).B) } 3. 参考 Golang 中使用 JSON 的一些小技巧\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nJSON and struct composition in Go\u0026#160;\u0026#x21a9;\u0026#xfe0e;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2021-09-24T13:55:01+08:00","permalink":"https://gitsang.github.io/p/deserialization-of-interface/","title":"为结构体内的接口实现 json 反序列化"},{"content":"1. 事务及两阶段提交 1.1 事务的 HelloWorld 级理解 许多文档或文章中都会使用那个张三给李四转钱的例子来解释事务及其存在的必要。(即 A 账户-100，B 账户+100 这两件事，应该同时成功或失败，不能一半成功一半失败)\n而要解决这个问题就引入了两阶段提交，第一阶段向 A 和 B 发送 prepare，让 A 和 B 准备好扣钱和加钱，即冻结这部分钱，然后当 A 和 B 都检查完毕没有问题并返回准备完成后，执行第二阶段，发送 commit 让 A 和 B 执行操作。\n这看起来通俗易懂，但似乎很容易地就发现了，这么做似乎还是会出现一个成功一个失败的问题啊。没错，确实如此，比如 commit 万一其中一个没收到，所以还有很多工作要做。\n但你需要知道的是，实际上，到此为止两阶段提交已经\u0026quot;结束\u0026quot;了。接下来的问题其实已经不是两阶段提交需要解决的了。也就是 “两阶段提交能处理业务异常的失败，处理不了系统异常的失败”\n因此，接下来我们也不会探讨如何解决这些问题，但我们还是需要知道有哪些问题需要解决以及由谁解决。也就是我们需要什么样的系统才能满足两阶段提交。\n1.2 两阶段提交的系统稳定性探索 很显然到目前为止，两阶段提交已经为我们解决了很多的问题，比如 A 账户余额不足，B 账户不存在等，这些业务逻辑上的错误我们已经能够避免了。但系统的网络并不总是通畅的，系统也不是永远能稳定运行的。比如网络波动，比如宕机都会影响到执行的结果。遗憾的是，两阶段提交并不能解决这些问题。\n“好在”我们需要的“仅仅”是保证最终一致，那么如果发送 commit 失败，我就能够通过不断重试最终让 commit 成功，或者是所有机器都宕机了，那这些机器就要保证在重启后能够互相协商，并且准备阶段需要把状态落盘进行持久化等等，甚至你还要考虑要是连硬盘都坏了呢。\n这可以考虑到很深很深，但意外总会发生，我们只能尽可能减小他发生错误的概率，不过这些都不是两阶段提交的内容了。而我们在实现事务消息的时候也只需要根据实际情况做出相对合理的应对就行了，比如对于普通的业务我只需要保证只要硬盘不出问题我的系统就是稳定的，就足够了。\n(这里还暂时不引入协调者这个概念，因为协调者和客户端的行为十分相似，即使不引入，也能够支撑讨论，引入后反而增加理解难度)\n1.3 关于超时的讨论 很显然，我们无法保证系统的完全稳定，即使我们已经做好了充足的应对，但仍然需要考虑到如果系统真的出现了故障时，应该做些什么，或者不能做些什么。\n我们需要知道，整个两阶段提交中，实际上只有两个地方会产生阻塞，第一是 client 向 A/B 发送 prepare 后需要等待 A/B 的相应，第二个地方是 A/B 向客户端相应 prepare 后需要等待客户端的 commit。\nQ1: Client 等待 A/B 返回 prepare 结果时，如果长时间得不到某一个的相应，是否能够发起 rollback ？ 可以，这实际上是非常保守的做法，以牺牲耗时和数据为代价，保证了系统的一致，也许 A/B 都已经做好了准备，但 Client 已经做好了最坏的打算，于是终止了一切。\n实际上你也可以让 Client 进行一些探测来确定 A/B 的状态，再做决定，或者简单点地多等几秒。\nQ2: A/B 准备完成，等待 Client 发送 commit 时，如果长时间收不到 commit，是否能单方面地 rollback/commit 或向 Client 发送终止？ 只有当对 prepare 的响应是失败时才可以 rollback，实际上如果已经失败，那他甚至无需等待 Client 的回复就能直接 rollback，因为 Client 没有任何的理由能够发送 commit，无论如何 Client 都会发送 rollback，因此最终结果是统一的。\n但如果对 prepare 的相应是成功，这时候无法知道 Client 会返回什么，Client 的返回结果取决于另一方。解决方法就是向另一方求证，或者是发起一轮终止协议操作，或者，多等几秒。而最坏的结果就是，网络中断了，任何请求都无法发出，那么只能继续等待，直到收到相应，或终止协议，或者 Client 的 rollback\nRocketMQ 的事务消息 理解了事务之后，似乎事务消息就不难理解了，然而，看看 rocketmq 官方文档的这张图\n再看看他的描述\n生产者将半消息发送到 MQ Server。 发送返回成功后，执行本地事务。 根据本地事务的执行结果，将 commit 或 rollback 消息发送到 MQ Server。 如果在本地事务执行过程中缺少 commit/rollback 消息或生产者处于等待状态，MQ Server 将向同一组中的每个生产者发送检查消息，以获取事务状态。 生产者根据本地事务状态回复 commit/rollback 消息。 commit 的消息将传递给 consumer，但是 rollback 的消息将被 MQ Server 丢弃 Emmm, 好像懂了，但是和之前事务的描述好像哪里有些不一样？\n是的，事务消息和事务确实有着许多的区别，不过我们换一种方式，认为事务消息保证的则是本地事务和发消息这两个操作的同时成功和失败，是不是就和事务保证两个操作的同时成功和失败对应上了呢。\n需要注意的是，这里的保证的是本地事务和发消息这两个操作，而不是保证本地事务和消费消息，消费消息是否能成功那就是另外的问题了。这个误会消除后也许许多问题都有答案了。\n那么我们可以完全不看这张图和描述，总结的来说，RocketMQ 的事务消息，实际上就是，客户端通过二阶段提交，来保证本地事务和发消息给 MQ Server 操作业务逻辑上同时成功或失败。而其中 MQ Server 需要通过将消息修改 Topic 存入队列，回查事务状态，等一系列操作来解决系统错误导致的问题，来保证最终一致。\n也许这样仍然不能解决你的一些疑问，下面同样用 Q\u0026amp;A 的方式，也许就能理清其中的关系。\nQ1: 如果把发送普通消息和本地执行逻辑放在一个事务里面，如果执行事务成功就发送普通消息，如果失败就回滚好像也是可以，那么这种事务消息相对其有什么优势或者好处呢？ 优势是多一道最终一致的保障，我只要发送了 commit 那 MQ Server 就必须保证这条消息能够提交，发送了 rollback 那 MQ Server 就必须保证这条消息不会提交，为了实现这个要求，broker 需要将消息落盘，需要不能确定消息是否要提交的时候回查，需要阻塞住这个事务，就是为了最终这条消息一定能被提交或废弃（最终一致）\n先执行本地事务，成功就发送，不成功就回滚，这只考虑了本地事务的成功失败，没有考虑发送消息的成功失败，那不如就来看看不使用事务消息要保证最终一致还需要做什么。\n发送消息成功了，那很好什么都不需要做了。如果发消息失败了，你要考虑是为什么失败，比如超时，那就要查 MQ 这条消息到底进去了没有（回查），有的话你再当作成功，没有的话你要重发。\n并且你要保证修改完数据库后如果生产者挂掉，重启后他还需要知道“曾经有一条消息，数据库已经改了，但是还没发出去”，你就需要持久化这条消息（落盘）\n然后你还要保证你成功之前其他人不会去改这条数据，不然可能产生时序问题（阻塞）\n最后你就会发现，你已经逆向实现了一套事务消息，那么，为什么要造轮子呢。（不过实际上，在没有事务消息功能的消息中间件中，这就是其中的一种实现事务的方式）\nQ2: 如果 MQ Servier 已经收到消息了，回 ACK 超时了，发送方是不是会重试？重试的话消息不就重复了吗？ 如第二问所说，当然不能重试，应该先处理超时，确认 Server 到底是为什么没回复，如果无法确定，则当作失败，向 Server 发送 rollback（或者是其他终止请求防止事务阻塞），这是个客户端需要实现的内容。\nQ3: 发送方二次确认（Commit 或是 Rollback），MQ Server 收到则更新或回滚，这里消息不是入队列了嘛？还可以直接定位到消息然后更新消息的投递状态？如果还可以更新短信的状态，MQ 感觉就像数据库了。 消息确实入队了，但是 Topic 不对，会将 Topic 改为 TRANS 之类的，这个队列不可以被消费，如果要 Commit 则把 Topic 改回去，要 RollBack 就删掉。这里的消息是存到 CommitLog 里面的。且实际上，在其他不支持事务消息的 MQ 上，就是拿数据库来做状态存储介质的。\nQ4: 消息回查,MQ 定时回查业务系统的数据库提交状态，感觉像底层系统调用上层系统了，MQ 要回调很多层业务系统，这样有点怪？ 对的，所以需要消息发送方实现 listener 和定义回查（回调）函数，但不是回调很多业务层系统，只回调一个 listener 的 checkState，里面怎么实现是发送方的事情。\n如果要客户端去查 MQ 实际上也是可以的，但你一个事务消息，总不能让客户端来保证数据的一致吧。\nQ5: RocketMQ 的事务消息是否能够用来当作保证发送的多条消息的最终一致？ 答案是可以，但不建议滥用。\n虽然在表述中，似乎 RocketMQ 保证的是本地事务和发送消息的最终一致，但实际上这个行为是由客户端决定的，就是说你也可以无视本地事务，对多个消息的，发送多条 prepare，然后本地事务可以直接发送 UnknownState 等待 MQ Server 的回查，当所有的 prepare 消息都返回成功后，将状态改为 Commit，等回查时 MQ Server 检测到所有的消息都 commit 了，将他们放进消费队列，他们就同时成功了。当然，他们在后续业务上（即消费端上）是否还会一致就需要消费端来保证了。\n关于消费端的问题再多扯两句，如果是一系列的操作，对于生产端，他已经认为发送成功了，因此，为保证最终一致，如果丢消息了要从 MQ Server 重新拉，如果执行失败了要不断重试。但如果重试了很久它真的过不去了怎么办呢？回滚操作吗，是不是那是不是在此之前还要给这一系列操作上锁？回滚步骤很多怎么办，这可不像是 MQ 的回滚直接废弃消息就好。回滚之后呢，生产端的数据怎么回滚呢？\n要处理这些问题，你几乎又要实现一个新的事务，这就是一开始说的滥用事务。实际上，我认为如果你真的有这么强的业务需求，不如直接实现一个生产端到消费端的业务逻辑，如果非要用 MQ 那就只能改源码了。\n参考 正确理解二阶段提交（Two-Phase Commit）, CSDN, 萧萧冷 分布式事务解决方案-RocketMQ 实现可靠消息最终一致性, 知乎专栏, 战猿 The Design Of Transactional Message, RocketMQ Doc Transaction example, RocketMQ Doc 消息队列漫谈：如何使用消息队列实现分布式事务？, 知乎专栏, 阿茂 RocketMQ 事务消息学习及刨坑过程, 掘金, 1 黄鹰 rocketmq 事务消息入门介绍, 掘金, 匠心零度 其他 没有太多的时间进行校对，并且我也是在学习的阶段，可能有许多地方理解有错误或不妥之处，希望能帮忙指出，即使只是错别字或是语义的错误，对我来说也是莫大的帮助。\n","date":"2020-09-17T15:54:19+08:00","permalink":"https://gitsang.github.io/p/%E5%85%B3%E4%BA%8E-rocketmq-%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF%E5%92%8C%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A4%E7%9A%84%E7%90%86%E8%A7%A3/","title":"关于 Rocketmq 事务消息和两阶段提交的理解"},{"content":" Golang 中 defer 几乎被当作 try catch final 使用，但事实上 defer 对返回值的修改和 final 仍然有些一些微妙的不同\n1. 问题的产生 先来看一段简短的代码\n1 2 3 4 5 6 7 8 9 10 11 12 // defer.go func deferTest() string { s := \u0026#34;init\u0026#34; defer func() { s = \u0026#34;defer\u0026#34; }() return s } func main() { fmt.Println(deferTest()) } 我们知道 defer 会在 return 之前以先进后出的顺序执行，但是 deferTest() 返回的是 init 还是 defer 呢。\n运行结果：\n1 2 \u0026gt; go run defer.go init 很显然，程序在 return 前会先执行 s = \u0026quot;defer\u0026quot; 然后再 return。\n但是打印的结果却是 init。\n是 defer 没有被执行吗？是 defer 晚于 return 执行吗？并不是，这里先不解释，稍微修改一下代码重新执行\n1 2 3 4 5 6 7 8 9 10 11 12 // defer.go func deferTest() (s string) { s = \u0026#34;init\u0026#34; defer func() { s = \u0026#34;defer\u0026#34; }() return s } func main() { fmt.Println(deferTest()) } 1 2 \u0026gt; go run defer.go defer 这个时候返回结果变成 defer 了，我们仅仅是给返回值加上了命名，defer 就将返回值改变了。\n2. 解释 实际上，return 前确实会先执行 defer 的内容，但是返回值却不是任何时候都会在 defer 时被改变的。\nreturn 的执行顺序应该是这样的：\n给 返回值 赋值\n调用 RET 指令，并传入 返回值\nRET 先检查是否存在 defer，存在则逆序执行 RET 携带 返回值 退出函数 这里的第一步，若是匿名返回值，那么在给 返回值 赋值时，将会先声明一个 返回值 (因为没有定义返回值的名称)\n1 返回值 := s 若是命名的返回值，则直接赋值（因为已经声明过了）\n1 s = s 这样，最后匿名返回值返回的是 返回值，而命名为 s 的返回值则返回 s 的值。\n这也是为什么命名返回值之后可以直接 return 的原因，因为执行 return s 其实也就只是约等于多执行了一条没有意义的 s = s 而已，和 return 是一样的。\n或者不严谨的说：golang 中 func function() returnType {} 的 returnType 其实并不像其他语言一样代表的是“返回值类型”，而是一个“匿名返回值”才对。\n","date":"2020-08-28T16:08:25+08:00","permalink":"https://gitsang.github.io/p/defer-and-return-in-golang/","title":"defer 和 return 的执行顺序陷阱"},{"content":" 1. 使用 iptables 进行端口映射12 1.1 第一步 : 打开端口映射功能 : 1.1.1 方法一 : (允许数据包转发) 1 sudo echo \u0026#39;1\u0026#39; \u0026gt; /proc/sys/net/ipv4/ip_forward 1.1.2 方法二 : 1 vim /etc/sysctl.conf 将 ;net.ipv4.ip_forward = 0 这一行的注视去掉 , 并将 0 改为 1\n修改后的结果为 :\n1 net.ipv4.ip_forward = 1 1.2 第二步 : 进行映射 : DNAT\n1 iptables -t nat -A PREROUTING -d 本机IP -p tcp --dport 本机端口 -j DNAT --to-destination 目标机IP:目标机端口 SNAT\n1 iptables -t nat -A PREROUTING -d 本机IP -p tcp --dport 本机端口 -j SNAT --to-destination 目标机IP:目标机端口 2. 参考： Linux 端口映射\u0026#160;\u0026#x21a9;\u0026#xfe0e;\niptables端口转发\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2020-03-14T14:53:28+08:00","permalink":"https://gitsang.github.io/p/iptables-port-mapping/","title":"使用 iptables 进行端口映射"},{"content":" 1. 指针和引用 1.1 指针和引用概述 1.1.1 指针 对于char* ptr，ptr为指向char的指针，即char*类型的变量ptr保存的是一个char对象的地址，而char可加限定c词const、volatile等。（char可换为其他类型）\n如下所示，p中存储的是c的地址，c中存储的是‘A\u0026rsquo;字符\n1 2 char c = \u0026#39;A\u0026#39;; char* p = \u0026amp;c; 通过*可取p所指向的内容，通过\u0026amp;可取p的地址，即：\n*p == c的内容 == 'A' p == c的地址 == \u0026amp;c \u0026amp;p == p的地址 1.1.2 引用 引用相当于一个对象的别名，主要用于函数参数和返回值类型。int\u0026amp;表示对int类型的引用（int可换为其他类型）\n1 2 3 4 5 6 int i = 1; int\u0026amp; r = i;\t// r指向了i的空间，此时对i和r的操作将是相同的 i = 2; cout \u0026lt;\u0026lt; r \u0026lt;\u0026lt; endl;\t// 输出 2 r = 3; cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; endl;\t// 输出 3 可以认为引用就是将 i 和 r 相关联（绑定）了，使得 i 和 r 代表了相同的一块空间\n1.1.3 引用与指针的区别 引用一旦指向某一对象就不可更改： 如上面程序，引用后的 r 就和普通的整形变量没有什么区别（若不考虑 i 的存在，完全可以把int i = 1; int\u0026amp; r = i;当成int r = 1;来看待）。引用即是将两个变量进行了绑定，而指针仅仅是存储了指向内存的地址，所以通过引用名（如r）可以直接访问指向的内存，而通过指针名（如p）却只能访问到地址，要通过（如*p）才能访问到地址所指的内存1\n对引用的操作将与所指对象同步，而不是像操作指针一样会改变指针的指向：\n如引用的++操作将直接使得指向内容+1，而指针的++会让指针指向下一个地址。如int i = 1; int\u0026amp; r = i; int* p = \u0026amp;r;此时 r 和 p 都指向了 i 所在的空间，但其意义是完全不同的，p是开辟了一个内存来存储 i 的地址，而 r 就是 i\n引用不可以为空，但指针可以为空： 正因如此指针在使用前都需要进行判空操作，而引用变量若不进行初始化甚至无法通过编译\n虽说引用和指针有许多区别，但两者在本质上是相同的，可以根据汇编代码看出：\n1 2 3 4 5 6 //引用int\u0026amp; ref = i; 8048727: 8d 44 24 1c\tlea 0x1c(%esp), %eax\t// esp寄存器里的变量i的地址传给eax 804872b: 89 44 24 18\tmov %eax, 0x18(%esp)\t// 将寄存器eax中的内容（i的地址）传给寄存器中的变量ref //指针int* p = \u0026amp;i; 8048777: 8d 44 24 1c lea 0x1c(%esp), %eax\t// esp寄存器里的变量i的地址传给eax 804877b: 89 44 24 10 mov %eax, 0x10(%esp) // 将寄存器eax中的内容（i的地址）传给寄存器中的变量p 引用和指针在汇编上的实现是完全相同的，即是说引用的本质其实就是指针，只是比指针更加安全。\n1.1.4 const关键字 引用和const指针是不是几乎是相同的呢？引用的本质其实就是指针，只是在指针上增加了一些规则，使得它更加安全。实际上，若不考虑赋空值，那么：\n引用 type\u0026amp; x 等于 const指针 const type* x const引用 const type\u0026amp; x 等于 指向const的const指针 const type* const x 1.2 值传递、指针传递、引用传递 1.2.1 值传递 值传递是形参对实参的拷贝，即将实参赋值到了形参上，改变形参的值并不会影响外部实参的值。从被调用函数的角度来说，值传递是单向的，参数的值只能传入，不能传出。当函数内部需要修改参数，并且不希望这个改变影响调用者时，采用值传递。2\n1.2.2 指针传递 形参为指向实参地址的指针，当对形参的指向操作时，就相当于对实参本身进行的操作，但对于形参的操作并不会改变实参的值（改变形参存储的地址不会改变实参的指向），因为指针传递的本质也是值的传递（将指针变量存储的地址复制给形参的变量）\n1.2.3 引用传递 形参相当于是实参的“别名”，对形参的操作其实就是对实参的操作，在引用传递过程中，被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间，但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址，即通过栈中存放的地址访问主调函数中的实参变量。正因为如此，被调函数对形参做的任何操作都影响了主调函数中的实参变量。\n2. 参考 C++中引用和指针的区别\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nC++ 值传递、指针传递、引用传递详解 - Geek_Ling\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2019-10-09T14:53:28+08:00","permalink":"https://gitsang.github.io/p/pointer-value-quote/","title":"指针、值和引用"},{"content":"《青春猪头少年不会梦到兔女郎学姐》终于完结了，这部2018年的10月番可以说总算是给了今年一个完美的句号。鸭志田一大概是学了两年的量子力学写出了这部作品吧，不过时隔多年再次见到鸭志田的作品，他的文笔可以说一下子从二流作家跻身一流行列了。\n对于这部高分作品，赞誉之词大概都已经被写完了，所以这次我就不写影评了，我们就来聊一聊《青春野郎》中的量子力学吧，UP只是一个学了一个学期量子力学和一个学期固体物理的学生，并且我们其实不是物理专业，我们是电子系的，所以肯定会有一些理解不到位，欢迎指出。\n不过量子的知识实在是太多了，这篇文章从开篇到完成写了两个多月，但是仍然无法面面俱到，本来想着是一期直接全部写完了，但是这些理论太多了，而且实际上相互联系，每个理论都是解释另一个理论的基础，所以，就只能以动漫为线索慢慢往下写了，其他的一些补充内容大概就放在第二期了。\n1. 薛定谔的猫 首先要讲的就是学姐篇出现的第一个量子力学理论“薛定谔的猫“\n“薛定谔的猫”是由奥地利物理学家薛定谔于1935年提出的有关猫生死叠加的思想实验，实验试图从宏观尺度阐述微观尺度的量子叠加问题，以此求证观测介入时量子的存在形式。\n我们先回顾一下实验内容\n在一个盒子里有一只猫，以及少量放射性物质。之后，有50%的概率放射性物质将会衰变并释放出毒气杀死这只猫，同时有50%的概率放射性物质不会衰变而猫将活下来。\n若根据经典物理学，猫必然是死或生的一种状态。而在量子的世界里，若盒子一直关闭，整个系统就会一直保持不确定性，即猫处在既生又死的叠加状态。猫到底是生是死必须打开盒子后才能知道，也就是说，只有当其被观测时候，物质（猫）的状态才被确定。\n注意这里说的是“被确定“而不是”能被确定“，就是说，在观测的瞬间，叠加态就结束了（波函数坍缩），猫的生死也在这一瞬间被决定。\n而双叶所说的“观测理论”指的就是：微观物质通常以波的叠加混沌态存在；一旦观测后，它们立刻选择成为粒子。\n2. 单粒子双缝干射实验 为了理解观测对粒子行为的影响，这里就不得不提到另外一个经典实验：单粒子双缝干射实验，可以说这个实验几乎是量子力学问题的精髓所在，大部分的量子理论都能通过这个实验得到解释。\n在中学我们就学过了双缝干射实验，即单缝中通过的粒子如同经典力学中的解释一样，会集中在缝的中心，而双缝中通过的例子却会形成了干射图样。\n对于干涉现象，一般的解释是：电子双缝干涉实验中，任何通过双缝的电子对屏幕图像的贡献是不分通过狭缝的时间先后的（任意两个电子之间都是有某种关联的），每一个粒子通过狭缝的行为都会对其他所有粒子的行为产生影响，因此每个粒子互相干涉而产生了干涉图样。\n为了解释一般双缝干涉实验中粒子的相互影响，我们先引进一个概念：量子纠缠（由于这个概念比较复杂，这里仅做科普性解释，就不开新的标题了）。\n在量子力学里，当几个粒子在彼此相互作用后，由于各个粒子所拥有的特性已综合成为整体性质，无法单独描述各个粒子的性质，只能描述整体系统的性质，则称这现象为量子纠缠。而这种纠缠是具有不可分性质的，即是说一旦纠缠，这几个粒子就必须作为一个系统，而不再可能对其中的子系统进行研究。这就解释了为什么每个粒子，即使是一个一个地通过双缝，他们仍然能形成干涉图样。\n但是粒子之间又是如何影响的呢？为了解释电子如何互相干涉形成干涉条纹，科学家又设计了一个单电子通过双缝的实验，我们以为这个粒子一定会出现在某个特定的地方，但奇怪的是，当只有一个电子通过双缝时，竟也出现了干涉条纹。当试图用摄像机去观测单粒子干涉原因时，另一个奇怪的现象出现了——干涉条纹消失了，不再观测时，条纹又再次出现。观测行为确确实实地影响着电子行为。\n3. 不确定性原理 为什么观测行为影响了粒子行为呢？对此我们需要引进一个新的知识：海森堡不确定性原理\n所谓不确定性原理说的是：你不可能同时知道一个粒子的位置和它的速度，粒子位置的不确定性，必然大于或等于普朗克常数除4π\n其原因在于，测量一个粒子的位置和速度，其办法是将光照到这粒子上，一部分光波被此粒子散射开，由此指明它的位置。而人们不可能将粒子的位置确定到到光的两个波峰之间距离更小的程度，故必须用短波长的光来测量。但普朗克的量子假设，人们至少要用一个光量子。这量子会扰动粒子，并以一种不能预见的方式改变粒子的速度。\n简单来说，就是若要精确测量量子的位置，必然需要使用短波，此时量子的扰动变大，，而使得对速度的测量不准。相反若想精确测量量子的速度，必然使用长波，而使得对位置的测量不准确。\n海森堡写道：“在位置被测定的一瞬，即当光子正被电子偏转时，电子的动量发生一个不连续的变化，因此，在确知电子位置的瞬间，关于它的动量我们就只能知道相应于其不连续变化的大小的程度。于是，位置测定得越准确，动量的测定就越不准确，反之亦然。”\n不确定原理还涉及很多哲学问题，用海森堡的话说：“在因果律的陈述中，即‘若确切地知道现在，就能预见未来’，所得出的并不是结论，而是前提。我们不能知道现在的所有细节，是一种原则性的事情。”\n4. 拉普拉斯妖 说到这里大家肯定想到了，拉普拉斯也提出了一个类似的假说，没错，那就是“拉普拉斯妖”。\n拉普拉斯妖（Démon de Laplace）是由法国数学家皮埃尔-西蒙·拉普拉斯于1814年提出的一种科学假设。此“恶魔”知道宇宙中每个原子确切的位置和动量，能够使用牛顿定律来展现宇宙事件的整个过程，过去以及未来。\n这个假说其实就是说，所谓的预言，其实就是现在细节的堆砌，由此说来，其实只要细心的观察，每个人都能够预见未来，就像是天气预报一样，只是推测而已。\n不过拉普拉斯以后，近代的量子力学诠释使得拉普拉斯妖的理论基础受到质疑。\n比如有人对拉普拉斯妖分析数据的能力提出一个极限。这个极限是由宇宙最大熵、光速、以及将信息传送通过一个普朗克长度所需要的时间得来的，约为10^120比特。在宇宙开始以来所经历过的时间以内不可能处理比这个量更多的数据。\n但存在极限就会存在假说，比如违反热力学第二定律的麦克斯韦妖，但要说的话又得很久了，就留到不知道是否存在的第二期来讲吧。还有四大神兽中的最后一个神兽芝诺的乌龟也留到第二期吧。\n5. 青春猪头少年会梦到兔女郎学姐 那么青春猪头少年到底会不会梦到兔女郎学姐呢？（微观理论是否真的能用在宏观世界呢？）\n这里可能有人会说，“啊，这一点都不马克思，这是主观唯心主义论“。确实这有点像是贝克莱所说的“存在即被感知”，然而事实上，现在人类已经能在光子、原子、分子中实现薛定谔猫态，甚至已经开始尝试用病毒来制备薛定谔猫态。\n这也让我想到了刘慈欣在《三体》中写到的经由”球状闪电“变成量子态的人，其中的漏洞到底有多少我们不去讨论，但是人类确实已经越来越接近实现生命体的薛定谔的猫。\n6. “当我们不观测时，月亮是不存在的” 除此之外，在量子派中也流传着一个论调：“当我们不观察时，月亮是不存在的”\n即是说，如果我们转过头不去看月亮，那一大堆粒子就开始按照波函数弥散开去。于是，月亮的边缘开始显得模糊而不确定，它逐渐“融化”，变成概率波扩散到周围的空间里去。 但一个月亮完全弥散需要相当长的时间，但这个问题说明的是，不观察月亮时，它就从确定的状态变成无数不确定的叠加。不过实际上，量子力学定律将巨大质量物体的波函数限制在很小的区域中，所以即使月亮弥散开去，弥散的程度也不是人眼能看出来的。\n从不确定性原理来解释的话：月亮不观测时不是不存在，量子态在观测时由于观测力的相互作用而使波函数坍塌为确定值，微观粒子整体呈现规律性，宏观尺度下观测力几乎对其不影响。\n当然还有“平行世界“说、量子自杀等等的说法，量子的世界丰富多彩，肯定不是三言两语能够说完的，而且事实上，在量子的世界里”上帝掷不掷色子“到今天为止也还是众说纷纭，所以有兴趣的话一定去读一读量子相关的书籍。\n好了，那么双叶课堂的第一期也就到此为止了，虽然不知道第二期是什么时候，但是还是让我们下期再见吧。\n","date":"2019-01-05T23:43:00+08:00","permalink":"https://gitsang.github.io/p/%E7%AC%AC%E4%B8%80%E6%9C%9F-%E4%B8%A4%E5%A4%A7%E7%A5%9E%E5%85%BD%E5%92%8C%E6%B5%B7%E6%A3%AE%E5%A0%A1%E4%B8%8D%E7%A1%AE%E5%AE%9A%E6%80%A7%E5%8E%9F%E7%90%86/","title":"第一期 - 两大神兽和海森堡不确定性原理"},{"content":"Claude Code vs Opencode Claude Code 缺点 Slash Command 被当作 Prompt 使用 / Slash Command 会被当做 prompt 发送给 LLM\n这会导致：\n运行时无法查看 context（因为 /context Slash Command 需要发送，所以运行时会查看会进入 Prompts 队列） 有时候运行 /context 会先发送 prompt (BUG)，LLM 先思考一轮，才给你输出 所有命令都挤在 Slash Command 所有的命令都挤在 / Slash Command 里面，Skills 装的一多，要找原生命令根本找不到\nOpencode 至少还有个 ctrl + p 可以开原生的命令\n会话管理的标题不会自动总结 Resume 的标题居然是使用最后一个 prompt 作为标题，如果最后一个 prompt 是 Slash Command，这个 Session 几乎就搜索不到了\n1 2 3 4 5 6 7 8 9 10 11 12 ❯ /resume ──────────────────────────────────────────────────────────────────────────────── Resume Session ╭──────────────────────────────────────────────────────────────────────────────╮ │ ⌕ Search… │ ╰──────────────────────────────────────────────────────────────────────────────╯ /statusline │ 6 minutes ago · draft · 17.9KB │ │ Type to Search · Enter to select · Esc to clear 切换模型困难 因为供应商绑定，每次切换供应商，模型，都要去改 json 文件，很麻烦\nVerbose output 太难看了 verbose output 只能用 ctrl+o 查看全部的 output，根本对不上是哪个的输出。\nopencode 至少能一个一个展开\nClaude Code 优点 全局统计 有个非常棒的全局统计 /stats\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ❯ /stats ──────────────────────────────────────────────────────────────────────────────── Overview Models (←/→ or tab to cycle) Mar Apr May Jun Jul Aug Sep Oct Nov Dec Jan Feb Mar ···················································· Mon ···················································· ··················································· Wed ··················································· ··················································· Fri ··················································▒ ··················································█ Less ░ ▒ ▓ █ More All time · Last 7 days · Last 30 days Favorite model: glm-5 Total tokens: 2.6m Sessions: 19 Longest session: 2d 17h 59m Active days: 2/2 Longest streak: 2 days Most active day: Mar 6 Current streak: 0 days You\u0026#39;ve used ~6x more tokens than A Game of Thrones Esc to cancel · r to cycle dates Context 统计 Claude 的统计信息确实做的蛮全的，比如 context 还会统计 MCP Tools, Skills 消耗的 token\n这个 Context Usage 做的我也很喜欢\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ❯ /context ⎿ Context Usage ⛀ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ glm-5 · 1k/200k tokens (1%) ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ Estimated usage by category ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛁ Skills: 1.1k tokens (0.6%) ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ Free space: 166k (82.9%) ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛝ Autocompact buffer: 33k tokens (16.5%) ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛶ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ ⛝ MCP tools · /mcp └ mcp__chrome-devtools__click: 0 tokens └ mcp__chrome-devtools__close_page: 0 tokens └ mcp__chrome-devtools__drag: 0 tokens ... └ mcp__chrome-devtools__type_text: 0 tokens └ mcp__chrome-devtools__upload_file: 0 tokens └ mcp__chrome-devtools__wait_for: 0 tokens Skills · /skills User └ docx: 198 tokens └ pptx: 173 tokens ... └ find-skills: 79 tokens └ brainstorming: 53 tokens └ humanizer-zh: 40 tokens 会话管理能切换数据来源 可以切换到全局，而 Opencode 只能是当前目录下的数据\n1 2 Ctrl+A to show current dir · Ctrl+B to toggle branch · Ctrl+V to preview · Ctrl+R to rename · Type to search · Esc to cancel · ","date":"0001-01-01T00:00:00Z","permalink":"https://gitsang.github.io/p/","title":""}]