Arch Linux 星球

January 15, 2021

Alynx Zhou

装机小记

由于笔记本散热和性能实在是不适合打游戏(有一说一,Optimus 双显卡还是没有直接单卡来得爽),所以很早就想组装一台台式机。特别是最近一直和高中同桌玩 Dota2,我的笔记本如果直播 Dota2,直播推流就会十分卡顿,而我又不想像 CSGO 一样降低画质玩。

作为计算机专业的学生,当然不打算直接淘宝整机啦。经过一番挑选和参考别人意见,最后成套配置是下面这样:

  • CPU:AMD Ryzen 7 5800X 3199(之前是 AMD Ryzen 5 3600X)
  • 主板:ROG STRIX X570-E GAMING 1999(特价)(之前是 微星 X570-A PRO 套装 2349)
  • 显卡:NVIDIA GeForce RTX 2070 SUPER 4300(之前是 七彩虹 RTX 2060 战斧 6G 2199)
  • 显卡支架:酷冷至尊 显卡支撑架 70
  • 内存:铂胜 C9BJZ 颗粒 白马甲 DDR4 3000 8G x4 1000
  • 网卡:Intel AX200(主板内置,之前是单独购买的花了 150)
  • 固态硬盘:三星 970 EVO Plus 512G 769
  • 固态硬盘:英睿达 MX500 2TB 1299
  • 机械硬盘:东芝 P300 2TB 400
  • 机械硬盘:西数 紫盘 2TB 400
  • 电源:海韵 CORE GX650 650W 金牌 549(特价)
  • CPU 散热:利民 AK120 145(之前是 超频三 东海x5 89)
  • M.2 散热:主板自带(之前是利民 M.2 硬盘散热片 39)
  • 机箱:Corsair 4000D 白色 629 (之前是 先马 鲁班1 209)
  • 机箱风扇:利民 TL-C12R-S x2 + 利民 TL-C12S 418(TL-C12R-S 只有 299 的三联包)(之前是 Arctic F12 PWM 温控 x3 120)

总计 15177。(有些具体价格我记不清了,而且京东价格总是波动。)性价比一般,因为我在其它人觉得没必要的地方分了很多预算比如主板风扇和硬盘,选 B450M 和西数的 NVMe 以及随便买个不带温控但是带灯的风扇的大有人在。

选这样一套配置其实有原因,可能很多人觉得为什么要给 R5 配 X570 这种高端主板,不如换成 B450 然后把预算加到别的上面。但按照我朋友的使用经验,4 代和以前的 AMD 芯片组存在 USB 兼容性问题,而且我装好机器后就在 Arch Linux CN 群里看到有人遇到这种问题,5 代似乎重新设计了 USB 控制器,目前一切正常。虽然这大概是最丐的 X570,连前面板 USB C 的接口都不支持,但是带 3900X 以下的 CPU 还是没什么压力的,而且我的机箱也没前面板 USB C。唯一的问题是微星不太厚道,今年 B550 出来之后又搞了个 X570 Tomahawk,大概只贵了几百块但是却有比肩旗舰的供电能力和前面板 USB C 支持,所以不再推荐购买 X570-A PRO 和 X570 Gaming Edge 这两个低端款了。有这种好东西为什么不一开始就吐出来???


更新(2020 年 11 月 16 日):AMD 发布了 Zen 3 系列的 CPU,游戏性能全面反超 Intel,对于我这种玩的都是吃 CPU 的游戏的用户来说实在是太香了,而且我一直想要 8 核心的 R7,于是买了首发的 R7-5800X,果然性能提升明显。然后正好有朋友的朋友打算装机买我换下来的 CPU 和主板,于是双十一为了 更好的 RGB 效果 更好的供电支持换了 ROG 的 X570-E(东哥疯狂耍猴,本来我是想 Tomahawk 的,但是微星的几款真香主板都无货,而且我对微星土里土气的设计也审美疲劳了,1999 的打人国度带眼睛它不香吗)。


如果仅仅是打游戏也可以把 R5-3600X + X570 换成 i5-9600KF + Z390,大概可以便宜一点然后升级一下显卡?我觉得差价是不够 2060 换 2060S 的。或者如果要求不高换成 R5-3600 也行。但 R5-3600(X) 是 12 线程而 i5-9600KF 只有 6 线程,所以我还是选择了 Ryzen,反正 AMD CPU 最近表现都很不错,没必要给 Intel 掏更多的钱。如果你在京东买,建议自己翻翻店铺,一般买主板 CPU 套装更便宜,但是它不会摆出来……

显卡一开始我是打算买个 1999 的 GTX 1660Ti 的,反正我玩的游戏也不需要光线追踪(难道真相不是本来就没几个游戏支持光线追踪嘛?),然而看到七彩虹最便宜的 2060 正在特价 2199,果断少买一条内存,加钱上这个。反正 1999 的 1660Ti 也是便宜货,而且看了一下视频似乎这块卡也不是丐中丐中丐,质量还可以(同价位索泰铭瑄也不能说是什么高端货吧?),反正内存可以再插新的,而显卡二手不值钱。什么你说 A 卡?虽然 A 卡对于 Linux 的驱动支持更好一点,但是 N 卡也不是不能用,而且我要开直播的话 NVENC 编码挺香的,而且对我的 Steam 库存来说 N 卡也更友好一点(但是骂老黄还是要骂的)。另外虽然我不做机器学习,但万一室友毕设需要的话,反正我上班也不用台式机,装个 CUDA 让他 SSH 上去用岂不是你好我好大家好。

内存其实没什么特别要求,不过由于 Ryzen 的设计,频率越高越好,但按照这一代的设计,最佳频率是 3600 左右,再高反而会下降,我是买不起那么奢侈的内存,甚至 3200 的都买不起,但这款 3000 的也不错,据说颗粒是镁光创下超频记录的那一批,所以就买了四条。我也不需要 RGB,这款的马甲还挺好看的。当然 3000 是 XMP 频率,需要在 BIOS 里打开 XMP,不然默认只有 2400。最近抄了个 3600 16-19-19-36 的作业,结果四条内存轻轻松松就上去了,2K 下面 PUBG 大概提升了 20 FPS,还是挺爽的。

硬盘不多说了,买 MX500 还是看中一个稳,我还是不放心买同价位国产白片……而且作为一个 2016 年就在笔记本上使用 NVMe SSD 的人,表示并没感觉出 NVME 和 SATA 有什么使用差别……我也不渲染视频,写写代码打打游戏都不卡。但是后来打脸了,公司发了新年福利于是还是上了个 NVMe 硬盘。顺便还配了个无线网卡用来接蓝牙和 WiFi,似乎 Intel AX200 是对 Linux 支持比较好的,就随便买了个 PCI-E 的插上了(御三家集齐了!)(ATX 的好处终于用上了,多出来的 PCI-E 随便插)。

电源有点买亏了,买完了发现长城同系列 550W 的电源当时也是 329,都怪京东迷一样的定价策略,好在我这一套功耗并不是很高,当然最后趁着福利 + 打折还是换成了海韵一元一瓦的金牌全模组,带我这套绰绰有余。开始我听说原装散热器也能压住 3600X,所以就没打算换,但后来发现刀法还是精准啊,3600X 带的散热器竟然不是铜芯的,再加上 Ryzen 三代的电压控制比较激进,待机温度有时候超过 50,打游戏时候机箱上方有点烤腿,所以还是换了个塔式散热器,风道科学了不少,温度控制也更好了。而且 AMD 原装风扇转速太高,3000rpm 的时候机箱都在震,换了之后安静了很多。顺便安原装散热器时候拆下来的螺丝卡扣不要扔,万一换塔式散热器,好多都是需要装在这个卡扣上的。

说到机箱和风扇我就一肚子气!本着对京东自营品质和速度的信赖,我全套都是在京东买的,结果拿到手全部安装上之后发现机箱开机跳线是短路的,插上电源就开机,开一会因为短路主板以为你在长按电源又关了!开始我还以为是主板坏了,结果发现螺丝刀手动碰一下开机跳线开关是可以正常开机的!于是又费了九牛二虎之力拆下来退货,主板散热器显卡都在盒子里椅子上放了两天,还好新机箱没问题,但是我买风扇的时候又给我发了个断轴的!东哥呀东哥,我拿你当兄弟,你拿我当代价?你是盯上我了?虽然我平时说你两句坏话但是对京东的服务还是好评的,但是经过这次之后我还是得重新考虑考虑了……顺便据说启航者 S5 这个机箱前面板音频口有的是 HD Audio 有的是老版 AC97,涉嫌虚假宣传,如果你遇到 AC97 的大概可以换货,麻烦一点,不过反正便宜货就这样子,我的抽奖抽到 HD Audio 了。但是这个机箱设计的很抠门,比如主板装上了就没法在上面走 CPU 电源线了,有两个走线口直接被 ATX 主板盖住,所以主板和显卡的电源线只能从一个口里挤出去,然后固态硬盘就在这个口下面所以也很难接线……而且机箱侧板是个黑色半透明的亚克力(那就别宣传透明啊!),金属外壳感觉也不是很厚实。所以最后还是换了先马的鲁班 1,各种设计都宽松了许多,装起来也很好看。

其他的外设我自己都有,显示器 就是之前买的优派 XG2402,1080p@144Hz 并且自带扬声器 换成了 DELL S2721DGF,27 寸的 2K@165Hz 屏幕,除了通病漏光以外都还不错,音箱外接了一个 JBL 的蜗牛一代(因为我觉得二代没有一代好看),鼠标就卓威 EC1-B CSGO 特别版(这个版本已经停产了,而且有偶尔指天/指地的 bug,建议买新的 EC1/2 或者 DIVINA 版本),键盘则是前段时间买的 ikbc C87 红轴,便宜还好用。

顺便由于我手残以及力气小和室友跃跃欲试,很多东西都是他装的,非常感谢。话说回来装这东西还真是个力气活,毕竟接口都有防呆设计仔细看看不会装错,但是真的很紧很难拔……非常担心把主板搞坏了。


更新:

装好的完全体照片


更新:避免你们说我灵魂走线,重新整理了一下,线太硬了。

重新走线正面

重新走线背面


更新:内存插满。

内存插满


更新:NVMe + 蓝牙无线网卡的完全体。

完全体

这个机箱 CPU 线走上面是要把主板拿下来才能穿过去的,而且右边两个有硅胶垫的孔 ATX 完全不能用,只能用一个孔,而且不能把线固定在机箱中间,很难盖上。


再更新:新机箱比原来的好看多了也宽敞多了。

正面

背面

公司的蜥蜴(明明是变色龙!)玩偶太高了,显卡下面放不开,挂着我又不放心,盖盖子之前还是拿出来了。


更新:换上了公版 2070 SUPER 和利民 AK120。

显卡

全景


更新(2020 年 10 月 12 日):冬天到了,还是换了几个支持 ARGB 的风扇,我个人不喜欢蓝光紫光夜店土嗨风,所以就弄了点温暖的颜色假装是个电暖气。前面是利民 TL-C12R-S,后面是利民 TL-C12S,虽然是三联包,但是似乎螺丝有问题,有一个风扇螺纹被拧花了,而且后面的风扇竟然少一个角上的橡胶减震垫,于是我就没装拧花的那个,并且把它的减震垫安装到后面了。反正考虑到我最下面是机械硬盘,装上这个风扇风道也不畅通,而且那里并没有什么需要散热的设备,电源风道是独立的。每把风扇两根线,ARGB 线要串联,PWM 线要一分三,而且有前有后,风扇线还有编织保护套,还要防止线材打到风扇扇叶。理线花了好久,最后 把他们用扎带固定到下面和前面空的风扇挂架上 还是走背线了,刚好够长。而且我还有三个 SATA 硬盘打算换一下 SATA 线……我真的想不出来那些水冷排且上下左右全都是 ARGB 风扇的人怎么理线的。

正面看灯光

侧面看灯光

风扇线不在下面啦

最终的背线效果

我不会告诉你其实我是想调出我 TB 至宝的颜色:

TB 至宝

一开始我以为需要用 OpenRGB 这个项目才能在 Linux 下控制颜色,但是这种 ARGB 风扇好像有存储机制,会自动记住上次的设置。于是就在 Windows 下面安装了一个有一大堆乱七八糟组件和功能的 MSI Dragon Center,其实我只需要调成长亮,然后重启进 Linux 颜色就一直是我设置的,然后我想关掉 Dragon Center 的自启动因为反正也用不到,但是微星的软件自己拉跨,重启进 Linux 灯光不变,重启进 Windows 又变呼吸彩虹灯光了。最后发现 Dragon Center 里面有一个类似“覆盖第三方RGB软件”的选项……好像他把他自己上次存储的结果也当第三方软件了,关掉就好了。原理我猜因为 Dragon Center 只是个 Client,真正控制颜色的是他某个 SDK 里面的 Daemon,这个选项的意思其实就是 Daemon 每次启动都按照 Dragon Center 设置的颜色重新设置风扇就实现覆盖功能了……但是 Dragon Center 的启动被我关了所以就默认了,不管了,统统关掉就 OK。


更新(2020 年 11 月 16 日):有了打人国度的眼睛感觉瞬间高贵了起来(大误)。

老黄卡只有绿色一种颜色,于是只能整个机箱调个老黄绿来配合。

老黄绿了

但是我自己平时还是喜欢红色的。

平时灯光

我发现主板的装饰灯正好在公版 SUPER 卡上产生倒影,很有意思。

倒影

这个主板可以完美使用 OpenRGB,在 AUR 里安装之后把 /usr/lib/udev/rules.d/60-openrgb.rules 复制到 /etc/udev/rules.d 重启即可支持,不过对于 Addressable Header 上的设备也就是风扇或者灯条需要手动 resize zone 也就是填写灯珠个数,我用的利民 TL-C12S 系列经过尝试是 8 个灯珠。


更新(2020 年 12 月 1 日):我之前有考虑过换个机箱,倒不是先马鲁班不符合我的需求了,而是一些细节问题,比如我这个玻璃侧板不那么平,有一个角翘起来一点。然后我的前面板角落里虽然我十分注意还是被我磕了一下。以及当初从宿舍搬出来的时候没有把机箱包装拿出来,以后搬家可能不方便……我一开始本来打算再买一个同款然后把侧板和前面板换了得了,然后那天发现前面 USB 2.0 接口接声卡会破音……于是打算换一个别的机箱。

我个人其实比较喜欢白色的机箱,于是初步考察了一下,我很喜欢NZXT H1,但我又不可能装 ITX,然后 NZXT H710 看起来不错,我觉得很漂亮,而且我在 NGA 上看到过有人给它纯白色的前面板贴了个黑色的两仪式剪影贴纸很帅气,但是太大太贵了。为什么没选择 H510 呢,因为我有几个硬性需求,我日常使用总结出来的,是一般机箱评测和用户不会注意到的,比如最好不要显卡竖装(我个人肯定是不会竖装的),PCIE的螺丝锁孔也不要做成从机箱尾部凸出来的,其实这里的实际意思是机箱不要有太大的空洞,很多机箱的显卡竖装PCIE螺丝孔上面就那么开着口,凸出来的横着的PCIE螺丝孔虽然有一个覆盖物,但它总是不那么好用,于是这样排除掉了 NZXT H510, NZXT H510i 和 NZXT H710i。

然后我也不想要钢化玻璃打孔固定的,这个大部分人应该也都不想要。我个人而言还不想要附带一个只有某些软件才能控制的控制器的机箱(点名 NZXT CAM),虽然 NZXT 的水冷确实非常漂亮,但我不放心把电脑这些基础的硬件交给一个需要联网还经常不一定能不能连得上而且只有进了 Windows 才能启动的废物控制软件——我是 Linux 用户。同理我也讨厌雷蛇和罗技难用的鼠标驱动程序。明明我可以用 BIOS 控制风扇/水泵转速,用主板的 ARGB 插针控制风扇,为什么非要用那么难用的软件?所以肯定只考虑 H710 不考虑 H710i。

如果光是这些可能我就下单 H710 了,虽然贵一点,但是能满足需求也就不在乎了。可是我翻了翻各种帖子都提到了这个机箱的一个缺点:它的硬盘架需要用螺丝刀拧螺丝才能拆除。虽然现在两百块钱的机箱都能免工具调整硬盘仓了但我觉得理完线也没什么人总动机械硬盘所以这不是个问题,但是更严重的是许多人都说固定硬盘仓的螺丝非常难拧,甚至螺丝都花了也拧不下来。作为之前被笔记本内部拧花了的螺丝支配的恐惧的人,我可不要买个这样的样子货,于是只好放弃 NZXT。可能它的设计师平时是不用机械硬盘没有软件更新服务器连不上的问题也不在乎机箱开一大堆没有保护网的口的人吧!

然后我又开始研究买什么,看了一下联力的鬼斧似乎很不错,虽然造型上比起 NZXT 要难看很多,但是和其它的机箱比起来也算是鹤立鸡群了。而且相比于 NZXT 只是好看来说,它在设计上就考虑到了更多的功能性的细节。比如有单独的重启键(可怜的硬盘灯已经被时代抛弃了)和 LED 控制键而且 LED 控制器可以设置为转发主板的 ARGB 信号(看看人家!),还有磁吸的合页式的玻璃侧板方便随时打开(对我这种强迫症太友好了,拧螺丝搬侧板好累),而且它的电源仓设置了单独的合页门!你可以从前面打开,然后硬盘仓设计成了 NAS 那种可以从前面拿出来的样式(甚至官方还有热插拔配件,彻底变身 NAS)(看看人家!),对折腾型用户不要太友好。但我最后还是没选它,因为我发现这个机箱第一个 PCIE 挡板和它上面的边框之间的缝隙太太太太太大了——你们厂家在这里多加两毫米宽度会赔本吗?而且后来我又想了想,合页式多半不能完全贴合(要给合页留出空间,所以还是算了)。

然后我看到了海盗船新出的 4000D,纯白色的前面板 + 非外凸的 PCIE 螺丝孔 + 独立的重启键 + 滑动抽拉的硬盘仓 + 有卡口的侧板,虽然它只有一个前置 USB 3.0 和一个 Type-C,不过也不是不能接受,而且有独立的重启键(硬盘灯:四个人的接头我却不能拥有姓名)。虽然它也支持显卡竖装,但它显卡竖装的上面有一个封得比较严实的挡板,它的 PCIE 槽相对也没有太大缝隙(相对)。颜值虽然没有 NZXT H710 好看,但在简洁上也是吊打鬼斧了,于是决定入手这个。

到手了也发现这个箱子还不算完美,主要的问题在于以下几个:电源仓上边的挡板强度不够,虽然没什么人会拿这个地方承重,但是比我之前的先马鲁班软得多也太差劲了吧!好在机箱其它承重的组件都过得去。然后就是 PCIE 的螺丝孔和显卡的孔有错位!需要很大力气按着才能勉强拧上螺丝,我用的可是公版显卡,你不能说公版卡孔位不标准吧!最后最后,这个机箱底板并没有多延伸出一块盖住侧板的底部!虽然侧板不需要这个位置辅助固定(它有很多卡口固定住),但是明明你顶板是有延伸的,为什么底板要在这里留一条缝隙???总而言之就是搬机箱时候要注意手不要抠到侧板和机箱这里的小缝隙(挺窄的,手多往中间伸一点就好了),但是还是会让人担心抠这里把侧板抠变形了。不过总之看下去也没有更合适的选择了,所以就将就吧。送的两个风扇是 3 pin 的,不能 PWM 调速,真是抠门啊海盗船。

顺便还有个对我不是问题但对大部分人可能是问题的地方,这个机器的前面板里面有一大块金属防尘网,但是根据一些国外的评测,假如你在前面板装上风扇,可能会因为风扇吸气导致这个防尘网向里面贴而蹭到风扇扇叶。但是我装了三个反向进风的风扇在前边,风扇框架在防尘网一侧所以并不担心这个问题。我也推荐所有前面板不是透明玻璃的人在前面使用反向风扇,毕竟正常来说通过侧板看到机箱内部反向风扇的灯光才是最合适的(风扇框架不会出现在机箱内部)。并且说实话,前面板通风比美观更重要,真的没必要买玻璃前面板。

于是装好了以后正面和背面就是这样的,不要和我说机箱不是留了理线槽了吗为什么不把主板电源线理进去,海韵的主板电源线又粗又硬,连用这块机箱的盖线板盖住都很勉强了,扭来扭去塞到那个理线槽里根本不可能,我也不想让它接头的部分受太大的力。这块主板 Type-C 的接头在内存附近,虽然这是主流位置,但对于直插的 Type-C 线缆来说弯线同样也很困难,别的理线倒没遇到什么大问题。

正面

背面

白色机箱里面反光要亮一些,但是有灰尘也更明显了……

亮起来

前面侧板的灯光效果很好看,没买 Airflow 版一个是因为它挖孔太多太乱,另一个原因是以后我也打算贴上贴纸。

前面

今天看到 NGA 上有人是风扇白光配红色 ROG LOGO,我也试了一下蛮好看的,就是感觉屋子里又冷了 10 度……

白色

全景


接下来是喜闻乐见的 debug 时间,首先是 Linux 下的,相对比较好调:

三代 Ryzen 有一个 每次都返回 0xFFFFFFFF 作为随机数的 BUG,在我这主要影响 wireguard,巧的是购买前几天我刚读过这篇文章,AMD 已经发了新的固件修正错误,建议更新到主板厂家提供的最新 BIOS 版本一般即可解决,如果主板厂家最新的 BIOS 还没更新固件建议联系售后催一下。

在我这不知道为什么 GDM 有时候没法自动启动,但是手动切 tty start 又可以显示,查了一下 ArchWiki 的 GDM 页面 发现有解决方案,但是并没有原因,搜索了一下也没发现原因是什么。

我还发现有时候刚开机没多久很快就关机会卡在什么 systemd-udevd 进程没结束,最后 event loop failed + timed out,大概要卡好几分钟才关机,但如果你用一会再关机就没问题。检查好几次关机日志没发现问题,后来群友火眼金睛对比了完整日志发现有个叫 ucsi_ccg 的模块开机加载了两分钟,猜测是这个的问题,搜索一下发现是 5.3 内核里 NVIDIA 添加的相关代码,用来控制 NVIDIA 显卡上的 Type-C 接口的,可是我这块显卡根本就没提供 Type-C 接口!(以及很多笔记本内置的有输出的 N 卡也有这个问题。)临时在 /etc/modprobe.d/ 里加了个 blacklist ucsi_ccg 的 conf 屏蔽了这个,好像没什么不良影响……

然后是奇怪的 Windows 的问题,我一开始装的 LTSC,不知道怎么回事输入法没了……折腾无果只能重装。以及现在除了不要联网装 Windows 之外(否则会强制你登录微软账户然后用你名字拼音前五位做用户名),还得不要联网装 NVIDIA 驱动,否则 Win10 自动更新驱动会给你安装 DCH 版的,虽然没什么影响(只是在 NVIDIA 官网升级驱动时候不能选标准选 DCH),但是就是让人很不爽。声卡驱动要装主板厂商的,Win10 自带的只能输出,不能接麦克风录音,然后如果麦克风声音很小就打开 Realtek 的声音控制程序,在右上角齿轮里取消掉什么把所有输入结合到一起的设定(什么乱七八糟的玩意!)。还有要关掉快速启动,不然直接开机会卡在黑屏一个鼠标光标……反正就很烦人。

更新:还有一个奇怪的问题是 Win10 关机重启也要卡很久,上网搜了一下全是一些忽悠小白的办法,经过我不懈搜索发现了一个熟悉的名字 UCSI!微软承认存在 UCSI 问题,既然又是这个 UCSI,多半还是 NVIDIA Type-C 的问题!反正我没这个接口,果断重新安装 N 卡驱动,选择清洁安装(删除旧驱动)并不勾选 Type-C Driver,问题解决……

最后既然设备到位了,大概就每周一三四五晚上八点半在 Bilibili 4312991 直播间 播一个半小时游戏,反正我玩什么播什么,大概就 CSGO Dota2 PUBG 什么的吧,尼尔也有可能,反正不是恶心反胃的就可以。周末随缘直播,如果没什么事情白天就播一会,周二可能晚一点开播因为有课。

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at January 15, 2021 11:46 AM

January 14, 2021

中文社区新闻

手册页索引服务

我们非常高兴地宣布我们最新的公共服务: 在 man.archlinux.org 公开的手册页索引站,用来发布我们所有软件包中所包含的手册页(man pages)并提供 搜索在线浏览功能。比如可以试一试 tar 的手册页

也可以在每个软件包的详情页面的侧边栏中看到新增的手册页链接。感谢我们的 Wiki 管理员 lahwaacz 开发了 archmanweb 实现这一功能。

尽管网上已经很多其它的手册页索引网站,通过发布与我们提供的软件包相符版本的手册页,我们希望能进一步改进 Arch 的可用性和文档。

by farseerfc at January 14, 2021 06:52 AM

January 07, 2021

Leo Shen

Homelab Project: 6 months in

At the summer of 2020, I started the Homelab project. Now, 6 months later, it's time to evaluate how the thing goes.

Hardware configuration

Previously, I've been using an AMD Ryzen 2200g with a garden-variety A320 motherboard. However, there had been two incidents, of different reasons, which prevented the OS from booting, and I had to remotely instruct my parents to fix it using TTY. Needless to say, these didn't go well.

So for this build, IPMI capability becomes one key feature to have. I happened to have an Intel Xeon E3-1230 v5 lying around, so it's just a matter of finding a suitable motherboard for it. Eventually I grabbed a brand new Supermicro X11SAT-F motherboard. (kinda surprising that you can still buy these brand new in 2020)

Here's the final hardware configurations:

  • Motherboard: Supermicro X11SAT-F

  • CPU: Intel Xeon E3-1230 v5 (8) @ 3.408GHz

  • Memory: 1x Kingston Gaming 16G DDR4 DIMM

    • Also lying around. It would be cool to have ECC (especially with ZFS), but I don't want to spend the extra bucks on this for now

  • Graphics: On-board ASPEED Graphics

    • No extreme gaming here!

  • Storage

    • 1x Intel 760p as system disk and caching

    • 3x 6T HDD (from various brands) as main storage disks

  • NIC: 2x Intel GbE NIC, one shared with IPMI

  • PSU: Random 80PLUS Gold 650W

  • Case: Random ATX case (that can fit all those disks)

    • I would love to use a rack-mounted case, but there's no room for rack for now

Software configuration

I used to use debian on the last home server, but due to licensing issues, ZFS is not that smooth on Linux. Since FreeBSD has a great reputation in stability and have flawless ZFS integration, I chose FreeBSD this time.

FreeBSD does not have the best installation experience (especially if you want ZFS as root). But after everything is set-up, it runs remarkably well. ZFS just works, and the whole OS just feels well integrated (probably due to the development all happening in the same place, which is not the case for most GNU/Linux distros).

One thing I do miss from GNU/Linux is (I think I will attract a lot of hate from this) systemd. rc.d just works in almost all situations (I never came into any issue during this period). But for me, systemd's service files are way easier to understand (and write!) compared to rc.d scripts. It's okay when you are using existing scripts from packages tho.

As of the server software, it's mostly the same as most Linux servers. I'm using nginx as web server, syncthing for syncing my documents, and samba for serving files for Windows and Mac OS X (yes this is intentional) users.

NAS Stuff

At the last build, since there's only one disk, I just use XFS and it works flawlessly. This time, since we are dealing with a multi disk setup, I decided to use ZFS.

There're a lot of alternatives of ZFS on varies of platforms, but:

  • BtrFS poor stability on RAID5/6 setups (and generally poor reputation on its stability)

  • BcacheFS yet to be merged into the mainline Linux kernel, but looks promising

So ZFS it is!

Since I have 3 disks installed, it's natural to use raidz1. It's slower and is less secure than mirror or raidz2, but since these are all enterprise grade disks and speed is not a priority (I only have GbE anyway), raidz1 should be fine.

By the way:

raidz1 means the array would allow one disk to fail before losing data (roughly RAID5), and raidz2 allows 2 disks to fail (roughly RAID6). More on ZFS and raidz here.

Also, since ZFS is well-integrated into FreeBSD kernel, most of the memory is safely used as ARC (ZFS's page cache). This greatly improves performance (since RAM is fast).

/posts/homelab/6-months-in-zfs-web.jpg ZFS in action

By the way:

At first only two disks arrived, so I just use truncate to create a disk image of the same size and create the pool with it (alongside the two disks). Then when the third disk arrived, I just make ZFS offline that disk and resilver the pool with the new disk. Kinda proves the reliability and maintainability of ZFS.

(Since the data is still not deleted during the process, this process should be safe)

File-sharing Services

On UNIX-like systems, it's natural to use NFS due to its performance (especially at NFSv4) and simplicity. And it works really well on my Linux machines.

For other devices, SMB is the most common protocol. I use Samba for this purpose. Just keep in mind to set minimal protocol to be SMB3_00, since lower versions of SMB suffer in performance and have security vulnerabilities.

A Special Note: MPD

It's really slow to scan big music library over NFS. So it's best to make the scanning happen on the exact machine that stores the music. This can be accomplished via setting up a dummy MPD instance on the server. You can read more about this on Music streaming with the satellite setup - MPD Tips and Tricks (ArchWiki).

Power Consumption

At idle, the whole server consumes around 40 to 50 Watts of power. I assume it's mainly taken by the three spinning disks since they are not configured to spin down on idle (this may be even a bad thing for enterprise disks).

Epilogue

Overall, it's been pretty successful project so far. It's one of the most reliable piece of hardware currently in service.

The performance is absolutely overkill for a NAS. Sometimes I can even throw some heavy job to it (compile Rust code, for example).

The next step would be to introduce 10GbE, but that would require the whole family to upgrade to 10GbE, so it won't happen in the near future.

January 07, 2021 07:01 AM

January 04, 2021

Alynx Zhou

StackHarbor 的 2020 尾记

我最近思考了一下,总是记不起来去年的总结写了什么,结果翻了一下博客发现我的记忆力是对的——我去年还真的就忘了写总结。

今年的总结因为各种原因写的稍微晚了一点,不过总之还算是写了,比忘记写要好得多吧!

以前小时候总是觉得一年过得很慢,要过很久才到新一年放烟花吃饺子,但是现在觉得一年过得很快,可能要忙的事情多了就会觉得时间不够用。但我一般来说又不觉得自己做了什么值得记录的事情,看到别人的博客年终总结写的特别充实,又是自己出国求学又是自己找实习转正的,但是到了我自己总觉得这些也没什么好写的。再加上我是个相当讨厌计划的人,所以也没什么“检查自己一年的完成度”的机会。

不过我在那些不错的年终总结里面还是学到了一些东西,所以打算也写点类似的。不过我虽然是程序员,写文章还是习惯从头到尾写,不擅长做那些分类加标题的事情,所以就想到哪写到哪。

2020 年感觉最不错的事情大概是加入某绿色蜥蜴工作,虽然这个严格来说从 2019 年就开始了,但是我去年忘记写总结了……多亏了同学的推荐得到了一份实习,面试感觉很好,没有考什么我特别不擅长的算法题而是一些实践性的知识,这个我还是挺擅长的。然后同事也都相当好相处,一开始是测试相关的工作,也了解了很多测试方面的知识,甚至还写了点 perl(虽然只了解皮毛),总之是很有意思的经历,然后更意外的是领导居然主动问我有没有什么别的感兴趣的领域,因为我一直是 GNOME 用户所以对 GNOME 维护挺感兴趣的,结果后面就转到 GNOME 组去实习了(这也太好了吧天哪)。然后就是快毕业了需要准备正式工作,一般来说这边没有类似国内互联网企业那样招一大堆实习生然后给几个转正名额竞(yang)争(gu)的途径,并且他们也几乎不进行校招,对毕业生和社招一视同仁,虽然我个人很想在这工作,但是如果想留下来的话还挺看运气的。这时候领导又和我说正好组里有空缺职位,可以安排面试,只要能保证一直实习到正式入职就可以了,于是又十分幸运的毕业之后正式入职。总之能在自己感兴趣的领域做工作已经是十分幸运了,然后待遇相对来说也不错,特别是看多了加班猝死的新闻,心里更加满足了……同事也都很友善,而且都是技术类型的,做的又都是开源相关,平时也很聊得来。今年整体来说不是那么容易找工作的,我都要反思我为什么那么幸运了……顺便由于疫情原因,我司今年一直是在家办公状态,也节省了好多通勤的时间金钱……

在找工作这方面我实在是没什么经验可谈,我太靠运气了……如果非要说的话,就是平时自己多学习多写写程序吧……

因为工作的原因今年一直是自己在外面住的,房租好贵啊……至于自己住虽然挺安静的但也挺麻烦的……把东西从学校搬出来也花了不少麻烦。

今年个人项目方面没做什么新东西,去年把 Hikaru 从 CoffeeScript 换成 JavaScript 之后基本就只是写文档、加测试、改功能,大改是去掉了 cheerio 改成自己实现了一部分功能,谁叫他们一年不……主题方面给 ARIA 做了个大改是去掉了 jQuery 加上了暗色模式,当然我自己看起来界面并没有什么变化。大一时候写的 FlipClock 努努力改成了 CMake,然后这样就可以跨平台做成 Windows 屏保了。顺便了解了一下怎么在 Android 上面运行 SDL,做了个 Android 的 FlipClock。而至于我的弹钢琴页面和 Telegram Bot,我已经忘记是今年还是去年写的了……

今年折腾了一遍我的电脑,因为终于有时间和钱玩自己的台式机了,仔细想想好像把之前的能换的都换了,27 寸的显示器对没有双屏空间的人来说提升了不少工作效率,5800X 打游戏也很爽,就是钱包不太舒服……

口琴方面今年年末又高产起来了,而且开始剪视频,发现达芬奇可以在 Linux 下面用(虽然有些限制)(Adobe 看看人家!),而且还挺流畅的,于是看了影视飓风的达芬奇教程学了一些基本的剪辑知识,为了用的更舒服还闲鱼买了个加密狗(假货很多,安全下车),今后可能剪视频的频率会逐渐增多,就当练习新技能了。

手机打算再用一年,今年手机厂商出的都是什么垃圾?我现在也想清楚了反正手机又不能给我带来收入,有这个钱还不如投资到台式机上,希望各种换手机患者也考虑一下,我现在是能用就行了。除非哪个厂家出一个摄像头不丑还有耳机孔最好还是直屏系统不要乱删乱改的旗舰机。

动漫除了看电磁炮 T 以外就是看了紫罗兰永恒花园,一开始很多人吹导致我对这个比较反感,实际看了以后觉得还是很不错的,所以吹得太过果然会招黑吗……电磁炮 T 总之中规中矩,能有第三部已经很不错了,我还想看第四部……、

认识了一些新朋友,同时很多老朋友也都有各自要忙的事情,总之几乎没什么人一起打游戏了……不过经常能和蓝猫她们一起出去玩还是避免了成为死宅的命运,本来我都打算在家打游戏跨年了,最后和蓝猫狐狸一起吃了海底捞,虽然三点才回家导致第二天犯了鼻炎,不过还是非常开心。

年末通关了 Titanfall 2,剧情很短,中规中矩,但是就已经是非常不错了,除了操作不适合我这个手残以外都很适合我。今年几乎没怎么玩 CSGO,但是下班之后有很多空余时间基本都投入在 Dota 2 上面了,虽然我也看很久 Dota 2 了,但是玩起来确实很难……不过我这一年一直都沉迷在中单光一直播里面,已经成了我玩 Dota 2 的动力了……现在多少也算入门了,虽然偶尔还是操作不过来,但至少明白是个怎么回事了。中单光一的直播真的很好看!正人君子,皮又好看,说话又好听,打游戏厉害,又很温柔。一开始我只是看他打 Dota,反正讲围棋我又看不懂,但我发现他读围棋棋手传记有意思多了,已经进入追小说模式了……拖到现在才写年终总结也是因为坐了 16 个小时的火车跑到上海去看 VirtuaReal 的第一次线下 Live,不过互动环节没抽到我实在是令人沮丧,我太非了,那么多人根本没我的机会呜呜呜呜呜……

就写这些吧,希望 2021 年大家的生活都能变得顺利!

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at January 04, 2021 09:21 AM

中文社区新闻

Arch Linux 邮件列表 id 变更

由于我们的垃圾邮件应对机制,我们不得不迁移邮件列表,原本从 @archlinux.org 发出的邮件变更为从 @lists.archlinux.org 域名发出。

发送邮件到邮件列表不受影响,发往 @archlinux.org 还能继续使用,邮件会被自动转发。

需要用户操作的唯一变化在于匹配 From 和 List-id 字段的过滤器和规则需要相应更改。

by farseerfc at January 04, 2021 12:16 AM

December 29, 2020

Leo Shen

Fix incompatible bytes library for actix-web and tokio

When attemping to build some web app with actix-web, I ran into this issue:

1
2
3
4
5
6
7
8
error[E0271]: type mismatch resolving `<fn(bytes::BytesMut) -> bytes::Bytes {bytes::BytesMut::freeze} as FnOnce<(bytes::BytesMut,)>>::Output == actix_web::web::Bytes`
  --> src/api/get.rs:35:47
   |
35 |                     return HttpResponse::Ok().streaming(s);
   |                                               ^^^^^^^^^ expected struct `bytes::Bytes`, found struct `actix_web::web::Bytes`
   |
   = note: perhaps two different versions of crate `bytes` are being used?
   = note: required because of the requirements on the impl of `futures_util::fns::FnOnce1<bytes::BytesMut>` for `fn(bytes::BytesMut) -> bytes::Bytes {bytes::BytesMut::freeze}`

This happens when I'm trying to utilize FramedRead in Tokio in order to stream the content while sending it in acitx-web (instead of reading them all into memory before sending it out). As it turned out, this is due to a mismatch in version of the crate bytes.

As the time of this writing, the latest version for actix-web is 3.3.2, and the latest version for tokio is 1.0. Naturally, I add the latest version of both in Cargo.toml. However, since tokio 1.0 is released after actix-web 3.3.2, actix-web is still using an older version of tokio, resulting an older version of bytes. So, sadly there's a mismatch between the bytes crate used in the two components, and it happens to be the case that these two version are not compatible, and thus such error appears.

Fixing this issue is trivial. Simply use a matching version of tokio (here, it should be tokio 0.2.23). cargo tree | grep -z $CRATE is very useful here.

December 29, 2020 12:05 PM

December 12, 2020

百合仙子

一次失败的 KDE 尝试

本文来自依云's Blog,转载请注明。

前些天尝试了一下 KDE 桌面环境,不过实在是没能用下去。

首先要说的是,KDE 桌面确实漂亮,非常养眼。设置项也挺多,可定制性还是挺不错的。只可惜问题同样很多。

首先是显示器缩放的问题。受限于 X11,KDE 只能设置一下全局的缩放比例。所以我们只好缩放显示器显示的画面。不幸的是,一向相当体贴的 KDE 此时却笨笨的,在使用 xrandr 设置好之后需要重启 plasmashell 来使其获取 xrandr 的设置更新

kquitapp5 plasmashell && kstart5 plasmashell

KDE 的设置项很多,分门别类地在「设置」应用程序中集中列出来,然而问题也由此产生:同时只会显示一个「设置」窗口。也就是说,我配置快捷键时,想去窗口管理器那边看一看,调整一点选项,就必须放弃我当前打开的快捷键视图,放弃我键入的搜索词,并且选择「应用」或者「放弃」更改,才能切换到另一个「设置」组件中去。即使从 krunner 里打开某个组件的设置,它也会找到并更新已有的窗口。

我知道 Windows 10 也是这么个「单任务」设置的风格。可 Windows 10 也没有这么多可以设置的地方呀。后来获知有个命令可以打开单独组件的设置窗口。很不方便。它被隐藏起来的原因是这种窗口不能返回到组件列表界面,会让用户困惑。可是,为什么我不能同时打开「设置」的不同组件的多个窗口?单独组件的窗口会让用户困惑,那就不要用单独组件的窗口就好了嘛。

KDE 桌面还有个问题:启动特别慢。登录进入界面要好久,启动一个程序,它的图标也要跳好久窗口才会出现。不知道它在干什么。我甚至怀疑它是为了展示启动动画而故意推迟界面的显示。

KDE 有提供丰富的桌面部件。我往副显示器上放了一些系统状态的监视器——CPU、磁盘、网络啥的。然后问题来了:我凑齐了四个部件刚好形成2x2的网格,可是我要怎么对齐它们呢?并没有对齐的选项,也没有吸附的功能。在我找到它使用的配置文件并手动修改之前,我只能用肉眼瞅。可计算机不就是用来做这种人不擅长而机器擅长的事情的吗?

终端我还是用 GNOME Terminal,因为有些特性(比如超链接)只有它支持。但又出现问题了:它启动之后,pin 它的任务图标,或者通过任务栏图标创建新实例均会失败。把它 pin 到任务栏上,需要从主菜单的右键菜单里操作。即使这样,启动之后终端窗口还是会位于新的图标,旧图标还是不对应任何窗口。后来查了一下,GNOME 的东西都没有主动支持启动通知,导致 KDE 很多时候只能猜测,而这次它猜错了。解决的办法是给 GNOME Terminal 的 .desktop 文件加上正确的 StartupWMClass 项。这其实不是 KDE 的问题,但也没办法。KDE 不想为别人擦屁股,GNOME 不在意自己的软件在别的桌面上的可用性。

不过 Qt 写的 flameshot 我就不知道是怎么回事了。具体情况不记得了,反正就是显示异常。好像是全黑吧。我没来得及 debug 这个。

最后,让我决定放弃 KDE 的点来了:我设置不了我需要的窗口管理快捷键

切换窗口,默认是 Alt-tab 的那个,我好不容易在「快捷键」设置里找到了添加更多快捷键的方式,但我发现除了 Alt-tab,我自定义的都不能连续切换窗口。按一下,切换一下,然后就切不动了,只能放开快捷键。后来了解到这是设置更新方面的问题,kwin_x11 --replace一下就有效了。

切窗口其实问题不大。问题大的是切显示器屏幕。两个功能:一、把焦点切到另一个屏幕;二、把当前窗口移到另一个屏幕上。

前者可以勾选「分隔屏幕焦点」选项,然后调整一下「阻止盗取焦点」的级别。我也不知道这个级别都是啥意思。「无」我能理解,「低」「中」「高」「终极」都是些啥?反正调整一下,确实可以把窗口焦点切换到另一个屏幕去了,除了鼠标不会跟着过去!另外测试过程中,有时候焦点会丢失——我不知道当前什么窗口获得了焦点,也不知道接下来谁会获得焦点。比 Mac OS X 里焦点跑到一个窗口也没有的 Finder 上还要神秘。

不过这个倒是可以自己写个脚本解决:使用 xrandr 获取屏幕的大小和位置,通过 X 的接口获取鼠标的位置并通过 Xtest 扩展来移动它,然后再用某个 X 的接口去设置窗口焦点——完全绕过 KDE 的功能。

然后我被另一个问题难住了——我怎么把窗口移到另一个屏幕上并且把焦点也移过去呢?使用文档匮乏的 kwin script 是可以把窗口移过去,然后我没能找到移动鼠标光标的 API。通过 X 是可以移窗口的同时移鼠标,但是我拿不到带窗口装饰的窗口位置信息。kwin 有一个 getWindowInfo 的 D-Bus 接口,但是它接收的那个 UUID 参数,我没找到获取的方法。

总结一下,KDE 对快捷键的支持并没有想像的那么好,尤其是多显示器的支持。快捷键的设置是通过图形界面来操作的,虽然直观但是对于大量快捷键的管理来说非常困难。而对于大显示器来说,通过快捷键来管理窗口是十分必要的——因为我更难肉眼找到我的鼠标光标去了哪里。

接下来,我打算一边忍受着 Awesome 3.5.9 的旧与 bug,一边尝试将 i3 改造成我需要的样子。

by 依云 at December 12, 2020 02:55 PM

December 06, 2020

百合仙子

i3 的 scratchpad 处理逻辑

本文来自依云's Blog,转载请注明。

i3 有个东西叫「scratchpad」,和我在 Awesome 里用的 run_or_raise 功能有些类似。

我的需求是某些浮动窗口可以「招之即来,挥之即去」。上次尝试切换 i3 遇到的一大麻烦就是,我经常从终端里启动图形界面的程序,而启动完之后我得手动给我的终端找个地方放着。i3 不支持最小化,也只有十个带数字快捷键、可以快速访问的工作区,所以 scratchpad 很重要,但是它的行为我有些捉摸不定。

首先是 move scratchpad 这个命令。它会把当前 con(窗口或者容器)浮动、取消全屏,然后移到一个叫 __i3_scratch 的不显示的工作区。

然后是 scratchpad show 命令(动词放后边了)。如果没有指定条件,它有如下复杂的处理逻辑:

  • 检查当前窗口是不是去过 scratchpad。如果是,就把它丢回去。
  • 否则检查当前工作区是否有另外的 scratchpad 窗口。如果有,就给它焦点。
  • 否则检查其它工作区是否有另外的 scratchpad 窗口。如果有,就把它移过来。
  • 否则把 __i3_scratch 里最久没有「见到光」的窗口移过来。

如果指定了条件,那么这样检查匹配的窗口:

  • 如果窗口不曾去过 scratchpad,什么也不做。
  • 否则如果窗口去过 scratchpad 并且在当前工作区,就隐藏它。
  • 否则就把它移过去。

总结一下,就是「回去,或者回来」。虽然动作的名字叫「show」,但其实是一个类似于 toggle 的功能。它的麻烦之处在于:如果你有多个去过 scratchpad 的窗口,你很难控制出现的是哪个窗口。一个绕过这个问题的办法是,总是带条件地使用 scratchpad。另一个小麻烦是:没有办法在匹配的窗口已经显示的时候,不要把它隐藏掉——有时候我只是习惯性地呼叫我的终端,而不看它是不是已经在我面前了。

对于浮动窗口,i3 有很多奇怪的限制,或者说是未实现:

  • 不支持最小化
  • 浮动窗口也不能显示在平铺窗口之下(加上上一条,就是没办法暂时藏起来)
  • 不支持最大化(手动调整窗口大小无法自动适配显示器大小,也没有「恢复」一说)
  • 不支持显示在最上层(当你在 GIMP 里开了一堆图片需要局部对比时)
  • 有全屏窗口时不能显示浮动窗口(看视频无法临时使用浮动窗口查个单词啥的)
  • 切换窗口时,平铺窗口和浮动窗口是隔绝的(需要单独的快捷键来切换)

by 依云 at December 06, 2020 06:05 AM

November 26, 2020

Alynx Zhou

解决 Spleeter 愚蠢的依赖问题

我倒不是对机器学习有什么莫名的偏见。事实上有些只能用机器学习搞定的东西我也很支持用机器学习解决,比如 waifu2x 这种增加图片分辨率的或者 Spleeter 这种分离人声和伴奏的,用传统的分离人声的方法就是不能完美解决这类问题,而机器学习模糊分类则可以无限接近完美解决。我讨厌的有两个,一个是传统方法很好解决的东西非要用机器学习解决,另一个是混乱的机器学习项目,后者更严重。

不知道是因为什么原因,许多机器学习从业者似乎都缺乏整理代码和依赖的能力——能轻松地把一个这类开源项目打包简直是奇迹,更多时候别提打包了,你想自己安装然后跑起来都不太现实,哪怕是同样做机器学习的其他人也有这样的苦恼:实现者似乎把论文写好然后在自己电脑上能跑起来就心满意足了,丢出来一份没什么文档的代码,不折腾几天根本不知道这坨代码需要装些什么才能跑起来。对我这种洁癖用户就更严重了,我可不希望 pip 在我的系统里塞一堆乱七八糟的东西,这个需要其他项目才能避免全局安装的程序比起 npm 真是差远了,解决依赖的能力也远不如 pacman 和 npm。

然后有人发明了 conda 和 docker 这样的 辣鸡 项目来拯救其他的辣鸡,但事实上不过是变得更糟而已——它们给了一些懒人名正言顺不维护项目的借口——反正我丢一个 conda 配置或者 docker 文件上去,你们拉上一大堆和我一样的过期依赖就能跑了,至于占了你多少硬盘,干不干净,关我什么事?如果不是物理限制,恐怕这些懒人会把整个宇宙都塞进去。而且他们再也不会管版本更新,什么 tensorflow 2 与我何干?用了我的 docker 你的系统里就会有几百个不同的过期的 tensorflow 1,买硬盘就完了呗?这和买显卡就完了呗还真是一路货色,这样依赖商业公司的产品,总有一天感觉会被割韭菜。

今天我用到的这个 Spleeter 某种意义上也是这样的一坨辣鸡,它的仓库里面的 README 是完全过时的,但这起码是件好事:代码还在更新。只是你按照文档是没办法顺利的搞定它的,我总结了一下我的解决方案,可能不适合其他人,但我大概知道怎么解决了。

这个文档有多离谱呢?它说你如果使用 GPU 加速的版本,必须从 conda-forge 安装,pip 和 GitHub 的版本都只有 CPU——那你这也是按揭开源?然后整个文档没有一个地方告诉我我需要单独下载训练好的模型才能跑起来,直接运行程序则丢一个段错误——可能他们搞机器学习的人觉得下模型是常识不需要说吧。

如果直接按它的方法运行 conda install -c conda-forge spleeter-gpu,你会得到一个不能用 GPU 的 GPU 版,这实在是太搞笑了。我捏着鼻子用 conda 就是为了能让你一步把这一坨东西给我弄好,现在你说你弄不好?

但是有一点好处就是实际上他们的代码已经更新到支持 tensorflow 2 了,所以其实完全不需要用那个过期的 conda-forge 的版本,你完全可以直接在 Python 3.8 里面 pip install spleeter-gpu 安装最新的版本——然后我又遇到了依赖问题,我的一些系统软件包依赖 numpy,然后 Arch 官方源里的 numpy 版本还是比 spleeter-gpu 依赖的 tensorflow 依赖的 numpy 版本新——你们写 Python 的人真麻烦,版本号兼容性是可以随便 break 的吗?

所以这时候我还是用 conda 解决,总之就是搞一个和系统独立的虚拟环境(npm:这不应该是内置功能吗?还需要用商业软件?)。总之去 TUNA 搞一个 Miniconda 3 最新的安装包来,然后直接安装,看着它往你的 shell 配置里塞一坨辣鸡(我把实际用到的命令拿出来做了个 alias,这样就可以只在我需要的时候打开 conda 了)。

为什么使用 miniconda 而不是 anaconda?因为 anaconda 带的那个图形界面根本用不了,miniconda 够用了。

然后创建一个 Python 3.8 的环境,高了低了都不行,真是难伺候,同时别忘了带上 cuda 和 cudnn,不然它一声不吭的就会只能用 CPU:

$ conda create -n spleeter python=3.8 cudatoolkit cudnn

cuda 不叫 cuda,叫 cudatoolkit 就离谱。

然后切进去:

$ conda activate spleeter

然后装 spleeter-gpu 到 conda 新创建的这个 Python 环境:

$ pip install spleeter-gpu

所有的依赖应该 pip 都会解决,但这个弱智有时候还会说我本机已经装了 numpy 1.19.4,比 tensorflow 需要的版本高,但我都创建虚拟环境了你还读取我系统的干嘛?不过其实好像也不影响使用,或者此时可以 pip install numpy==1.18.5

然后到 https://github.com/deezer/spleeter/releases 去下载训练好的模型,这也是个弱智的地方:哪有 tar 打包不把目录本身打进去的?然后还得给这个程序创建一个工作目录,因为它是写死的到当前目录下面的 pretrained_models 下面去找模型。

$ mkdir -p spleeter/pretrained_models
$ cd spleeter/pretrained_models

我这里用 2stems 的模型示范,因为我只需要分离人声和伴奏:

$ wget -c 'https://github.com/deezer/spleeter/releases/download/v1.4.0/2stems.tar.gz'
$ mkdir 2stems
$ cd 2stems
$ tar -xpvzf ../2stems.tar.gz

因为他们打 tar 包时候没把目录打进去,所以别忘了自己创建目录!

然后回到你的工作目录就可以用了,我这里结构是 spleeter/pretrained_models/2stems 所以工作目录就是 spleeter

$ cd ../../
$ spleeter separate -i 你要处理的歌曲 -p spleeter:2stems -o 输出目录 -B tensorflow

如果你想使用其他的几个模型,那就把 2stems 改成其他模型的名字,但是这里还有个开发者脑子抽了的地方:带有 -finetune 的是高品质模型,它们的模型目录名字应该是比如 2stems-finetune,但是参数名字却不是这个而是 -p spleeter:2stems-16kHz,文档里当然是没找到的,我觉得应该揍开发者一顿让他老实写文档(不过我又看了一下,这个 finetune 对于分离音轨没什么用好像)。

我这里必须使用 -B tensorflow 才会走显卡加速。

用完了就可以 conda deactivate 退出虚拟环境,要用的时候别忘了 conda activate spleeter 切换进来,

遇到类似问题的同学可以参考我的文章,但是因为这个处理过程影响因素太多了,如果你的不能用我也没什么办法。

回头一看,这个项目犯了一大堆禁忌:难以解决的依赖,写死的模型路径还有匮乏的文档,导致配环境就要配一大堆。当然,好在他们还是在努力更新跟上依赖而不是撒手不管让它慢慢死去,并且功能非常好。真正对开发者友好应该是不需要配环境的,比如在 Linux 下面开发软件,包管理已经帮你考虑好各种依赖了。只有像 Windows 或者 Android 这种不以开发者为中心或者是许多机器学习项目这种“数据好看就行”的地方才会有这么多麻烦。真的很希望这些人能补一点务实的基础,不要让他们的软件这么难用。

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at November 26, 2020 08:43 AM

November 23, 2020

frantic1048

Espresto - 忍野忍 Clear materials

Shinobu

到了一两周的小忍的照片,总算是搞出来了。当时看到介绍的照片还不错,而且还是景品,就果断入手了,到手一看,非常良心!

也是这个月,升级了一下之前的小相机 Canon G7X Mark II (G7X2)到新出的 Sony Alpha 7C(A7C)。新机器到手后兴致勃勃地搭起环境拍了一波,传到到电脑之后——Darktable (脑补的)一副阿库娅脸提示读到的是不认识的 RAW 格式,拜拜。

作为多年 Arch 用户对设备过新造成的问题早有体会,比如 18 年底装机,入手首发 RX590,旧系统直接黑屏,最新的 live iso 上来也是黑屏,折腾半天 GPU 的驱动问题解决之后接着主板也来一脚,让我新装机器的激情全撒在想方设法让系统启动上了,到最后终于成功启动,已经内心毫无波动 ヾ(°ω。ヽ=ノ °ω。)ノ

简单去 Darktable 的 issue 列表翻了一下,很巧数天前就有人报告关于 A7C 支持的问题,对照了一下已有的别的相机支持的 issue 的创建时间之后,这样等下去怕是半年都不定会有支持,我不能接受!于是去 Darktable 历史翻了一通的新相机支持的 PR,发现并不是那么深奥的操作,糊了俩看起来能用的 PR,今天(2020-11-23)总算是过了上游 Review 合进去了,终于确定之前自己 patch 的版本是 ok 的了!

接下来是小忍的时间。

右边的头发尖有一点点歪,不知道是不是故意的,还是只是运输的时候给搞弯了,尝试一番也没能扭直就这样吧,不要盯着看也不会察觉。和四糸乃作为同样暗色服装的手办来说,服装上的(只有拍照时候比较容易凸显出来的)细微瑕疵相对少很多,修图工作量大大减小,拯救了我的眼睛。

这个 JPEG 输出 banding 都压出来了有点难受(没看到?那就是没问题!),等工具链的 AVIF 站起来了再改善一下画质。

Shinobu Shinobu Shinobu Shinobu

Shinobu Shinobu Shinobu Shinobu

半透明的材质效果很棒,群摆上的细节也比较稳定。

Shinobu

我好了!

Shinobu

November 23, 2020 12:00 AM

November 21, 2020

百合仙子

HiDPI 配置记录

本文来自依云's Blog,转载请注明。

首先,我是用 X11 窗口系统的,不同屏幕分别设置肯定没戏。所以只好让笔记本电脑的屏幕迁就一下我的4K主屏啦,把笔记本屏幕缩放一下。算一下 scale 值:192 / 120 = 1.6。不是整数,会糊,可总比放大两倍的巨大界面要好。

xrandr --output eDP-1 --scale 1.6 --auto --output DP-2 --auto --pos 3072x0 --primary --fb 6912x2160

这里要注意的是,要指定--pos(或者--panning),不然会重叠;要指定--fb,不然鼠标可能会有部分区域去不了。

然后开始设置。本来我是尝试了一下 KDE 的,但因为我将在下一篇文章中写的原因而放弃,回到了 Awesome。不过也不是全无收获。我把 KDE 的配置方案拿过来用了。你想问怎么拿的?我 btrfs 的文件系统,做好快照再 rsync -n 对比一下它动了哪些文件就有了。

首先是 X11 的资源。在~/.Xresources里写上Xft.dpi: 192,然后xrdb -merge ~/.Xresources一下就好了。顺便再xrandr --dpi 192一下,听说有些程序会读这个。

然后是 GTK。GTK 2 就放弃吧,没办法。文字会按设置的 Xft.dpi 放大,图标啥的不会。GTk 3,要设置两个环境变量:

export GDK_SCALE=2 GDK_DPI_SCALE=0.5

前一个是把界面放大,后一个是把文字缩回去,因为文字已经按 Xft.dpi 放大过,不能再放大一次了。

再然后是 Qt。Qt4 早卸载干净了不用管。Qt5 嘛,也不用管。它自己会处理好。有个按不同屏幕缩放的环境变量QT_SCREEN_SCALE_FACTORS,效果跟 Windows 10 差不多的。但是我为了照顾其它程序已经把屏幕给 scale 过了,就不需要设置这个了。你要设置个QT_AUTO_SCREEN_SCALE_FACTOR=0也行,但这个是默认行为。

最后是个别的程序。

Telegram 直接在设置里关掉「默认界面缩放比例」并且设置缩放比例为 300% 就好了。我也不知道为什么,Telegram 默认的字总是很小。之前 120dpi 的时候我要 200% 缩放,现在 192dpi 需要 300% 缩放了。

YouTube,就是那个网站啦。它其实没什么显示上的问题,只是死活不会给我自动选择 1080p 以上的分辨率。经过仔细二分测试之后发现,把火狐的配置文件夹下的storage/default/https+++www.youtube.com目录删掉之后就好了。没发现删掉这个会有其它影响。

mpv 要修改配置文件,加上no-hidpi-window-scale参数,不然会把视频自动放大,4K视频一打开会只能看到四分之一的画面。加上这个参数,默认窗口大小时,一个视频里的像素会对应一个显示器上的像素,不大不小刚刚好。mpv 文档上说这是 OS X 系统上的默认行为,可我这是 Linux 桌面啊,你把别的平台上的习惯搬过来是几个意思?另外我加了个demuxer-readahead-secs = 20选项。我的大文件都在机械硬盘上,4K 码率又比较高,不多预读一点容易卡。

我的 qemu 之前使用的是-display gtk,也坏掉了。窗口那么大,虚拟机只用左下角那里四分之一的空间。spicy 也有问题,会告诉虚拟机只有 1080p。解决方法是 unset GDK_SCALE GDK_DPI_SCALE。它们在放大了自己的界面的同时,把显示的虚拟机的内容也给放大了,所以干脆叫它们别动。也没什么别的影响。

哦还有 Zoom。设置个QT_AUTO_SCREEN_SCALE_FACTOR=1似乎就好了?我试了一下QT_SCREEN_SCALE_FACTORS,会导致很怪异的行为。

以上解决了显示大小的问题,但我发现还有个问题:我的鼠标光标时大时小的……从 KDE 那边弄来几个设置之后就好了,而且主题也更加一致了呢。

首先是设置 xcursor 环境变量:

export XCURSOR_THEME=Vanilla-DMZ XCURSOR_SIZE=36

听说对应的 X 资源大家都不理睬,那我也就不设好了。

然后是 GTK 2 的~/.gtkrc-2.0文件里写上:

gtk-cursor-theme-name = "Vanilla-DMZ"
gtk-cursor-theme-size = 36

再接下来是 GTK 3 的~/.config/gtk-3.0/settings.ini

[Settings]
gtk-cursor-theme-name = Vanilla-DMZ
gtk-cursor-theme-size = 36

然后又没了。天知道为什么 Qt 那边啥都不干就好好的,GTK 却这么麻烦。

啊,你问这些环境变量在哪里设?我给写~/.xprofile里了。不过这还不够。有些 GUI 程序会由用户的 systemd启动(比如我的 Telegram 是由 systemd 启动的,为了在内存用得太多的时候自动重启),有些 GUI 程序会由 D-Bus 激活(比如 gnome-terminal)。这些是和登录会话分开的,所以要手动导入一下。以下是我的 .xprofile 中导入图形界面相关环境变量的部分:

_envs=(
  GDK_SCALE GDK_DPI_SCALE
  XCURSOR_THEME XCURSOR_SIZE
  XMODIFIERS QT_IM_MODULE GTK_IM_MODULE
  LIBVA_DRIVER_NAME GST_VAAPI_ALL_DRIVERS
)
dbus-update-activation-environment "${_envs[@]}"
systemctl --user import-environment "${_envs[@]}"

至于登录界面怎么办,我是在 lightdm 的 display-setup-script 里,跑了跑 xrandr,设置了一下 Xft.dpi 资源。环境变量啥的没动,反正用不上。当然你也可以去改~lightdm/.pam_environment来设环境变量,反正现在 Arch Linux 还是读它的。别的 dm 同理。

by 依云 at November 21, 2020 10:12 AM

November 20, 2020

百合仙子

让 QEMU 使用 SPICE 协议

本文来自依云's Blog,转载请注明。

缘起

我就是买了个4K显示器,咋这么多事呢……(有两篇文章还在路上)

第一个问题是,我的显卡 vGPU 最高只支持 1920x1200 的分辨率。行吧,我缩放成了吧?嘿嘿,-display gtk 的缩放不会保持比例,我只好算了算最大保持比例的大小,然后窗口切成浮动,再调用命令调整到指定的大小:

sleep 1 && xdotool getactivewindow windowsize 3280 2122

然后还要居中放置一下。多麻烦!

第二个问题是,我想在虚拟机里试试 i3,但是我的按键总是会被外边捕获。Super 键基本上是 Awesome 在用,而 Alt 键会撞上这个 GTK 窗口菜单栏的快捷键。

配置

经过多番尝试和摸索之后,确定了如下的参数:

  -display egl-headless,gl=on,rendernode=/dev/dri/renderD128
  -spice unix,addr=/run/user/1000/qemu/ArchKDE/spice.sock,disable-ticketing
  -device virtio-serial-pci
  -device virtserialport,chardev=spicechannel0,name=com.redhat.spice.0
  -chardev spicevmc,id=spicechannel0,name=vdagent

当然要gl=on啦,不然我怎么玩特效,我还不如回到 vbox 去呢(啊不,我的 vbox 不喜欢 btrfs,经常出错然后只读挂载,所以已经被删掉了)。要指定rendernode,不然它会 fallback 到 llvmpipe,然后还段错误崩掉……这也是我不-display spice-app的原因。

后边三行是从这个 QEMU + Spice with Copy & Paste 抄来的。搜索问题时不小心遇见,然后解决了这个我一直没有处理的问题。SPICE 不但能共享剪贴板,而且还支持 PRIMARY 选择区呢~虚拟机里要安装 spice-vdagent 并启动相应的服务。

然后是客户端的选择。一个很神奇的地方是,virt-viewer 看上去轻量,实际上只是选项少而已。它不光拖进来个 gtk-vnc 依赖,还把 libvirt 都给我带上了……然后 GNOME 的 vinagre,我不知道为啥,它就是连不上我的 spice+unix 地址。哦对了,virt-viewer 直接敲命令调用也是连不上,只能用 xdg-open 才能正常打开。

virt-viewer 有个依赖叫 spice-gtk。我试着 pacman -Ql 了一下,还真找到个 spicy 工具。比 virt-viewer 轻量多了,选项也更为丰富,比如可以选项不 grab 键盘。

一点额外的东西

在群里听说了 virtio-fs 共享方案,听说比 virtio + 9p 更高效。然后我用它成功取代了之前用的 NFS 方案(反正我的 vbox 虚拟机已经被删掉啦)。NFS 的服务也可以卸载啦(开放一堆端口到公网,看着有点怕怕的,虽然是 IPv6 地址不太会被扫到,但知道我的地址的咋办呢)。

virtio-fs 相比 virtio + 9p 的另一个优点是,和 NFS 一样,virtiofsd 是以 root 权限运行的,所以可以写入我的 pacman 缓存。qemu 那个 9p 似乎没有办法。至于启动嘛,用 systemd abstract socket 触发一下就好了。

另外,我使用 GVT-g 和 virtio 输出视频信号时,均遇到了声音在视频画面变化时声音卡顿的情况。一个绕过的办法是,通过设置 PULSE_SERVER 环境变量以及加载 module-native-protocol-tcp 模块,将音频信号直接通过网络发送到宿主机上,一点也不卡!

by 依云 at November 20, 2020 09:04 AM

November 16, 2020

百合仙子

Python 小版本升级是怎么 break 已有项目的

本文来自依云's Blog,转载请注明。

近日,Arch Linux 终于开始升级到 Python 3.9 了。很多人认为 Python 小版本升级容易搞坏兼容性,导致项目无法在新的版本上运行。事实是这样的吗?我正好借着 Arch Linux 升级 3.9 的机会,分析一下打包过程中失败的项目到底是出了什么事。

需要说明的是,我仅大致地分析了打包的报错信息,不排除分析出错,或者有额外的问题没有被看见的情况。另外我是在打包过程中随机(arbitrarily)取样,并且排除了我不能确定问题所在的案例。

以下项目测试失败是和 Python 3.9 相关的。排序是按照项目开发者的无辜程度排序的。也就是说,排序越靠前的,我越是认为项目开发者是无辜的;而像「硬编码 Python 3.9 为未发布的版本」这种完全不 future-proof 的做法,现在坏掉了真是自找的。

其中,使用的公开特性变化导致问题的有 3 个,调用私有属性或者方法、依赖非正式的文本信息的有 11 个,使用已废弃的特性的有 8 个,使用已被修复的 bug 的有 2 个,使用未来注定会出问题的信息的有 3 个。总共 27 个。

  • freecad: PyTypeObject.tp_print 没了
  • python-llfuse: PyTypeObject.tp_print 没了
  • linux-tools: PyMODINIT_FUNC 的变化导致了警告,然后被转为错误
  • python-blist: _PyObject_GC_IS_TRACKED不再在第三方库中可用(被公开 API 取代)
  • python-pyflakes: Python 语法解析报告的列位置似乎不太对,应该是受新的语法解析器的影响
  • python-pylint: Python 语法解析报告的列位置似乎不太对,应该是受新的语法解析器的影响
  • python-typing_inspect: 使用私有名称 typing._GenericAlias,结果新版本变成了 typing._SpecialGenericAlias
  • python-sphinx-autodoc-typehints: 看上去是类型标注相关的内部更改移除了 typing.Dict.__parameters__ 属性造成的
  • python-fastnumbers: 看上去是内部函数 _Py_dg_stdnan 不再被默认包含导致的问题
  • python-libcst: 类型标注相关的内部更改移除了 typing.Dict.__args__ 属性造成的
  • monkeytype: typing.Dict 的类型从 type 变成了 typing._SpecialGenericAlias
  • scrapy: 由于 typing.Optional[str] 的字符串表示由 typing.Union[str, NoneType] 变成了 typing.Optional[str] 导致 mitmproxy 运行出错,进而使得 scrapy 的测试失败
  • python-billiard: 调用的私有方法 _posixsubprocess.fork_exec 参数发生了变化
  • python-pytest-benchmark: argparse 的帮助信息格式有优化
  • python-opentracing: 自 3.7 起废弃的 asyncio.Task.current_task 被移除
  • python-engineio: 自 3.7 起废弃的 asyncio.Task.all_tasks 被移除
  • impacket: 自 3.2 起废弃的 array.array.tostring() 被移除
  • python-pybtex: 自 3.2 起废弃的 xml.etree.ElementTree.Element.getchildren 被移除
  • python-jsonpickle: 自 3.1 起废弃的 base64.decodestring 被移除
  • python-ioflo: 自 3.1 起废弃的 json.loads() 参数 encoding 被移除
  • routersploit: 自 Python 3 起废弃的 threading.Thread.isAlive 终于被移除了
  • python-socketpool: 自 Python 3 起废弃的 threading.Thread.isAlive 终于被移除了
  • python-furl: Python 3.9 修正了一处 URL 解析 bug
  • python-stem: Python 3.9 移除了错误的 unittest.mock.__version__
  • python-natsort: Python 的 Unicode 支持更新到了 13.0.0 版本,CHORASMIAN NUMBER ONE 字符被判定为数字,但是测试代码不认识,认为程序出错
  • python-pony: 对新版本的 Python 报不支持的错误
  • python-dephell-pythons: 硬编码 Python 3.9 为未发布的版本,但现在 3.9 已经发布了

而以下项目的测试失败与 Python 3.9 没有直接关系,共 26 个。其中与 Python 生态有关的有 18 个,与其它项目有关的有 4 个,依赖外部信息的有 3 个,包括一个特别搞笑的依赖夏令时是否生效的。

  • python-eventlet: 调用的 dnspython 私有方法已不存在;DNS 解析超时
  • python-markdown2: 语法高亮的结果有少许变化,不符合预期。推测是 pygments 新版本的变化
  • python-flake8-typing-imports: 似乎是 flake8 能够检测到更多的问题了
  • python-babel: 使用了已废弃的特性,测试被 pytest 拒绝
  • python-pygal: pytest 6.1.0 移除了 Metafunc 的 funcargnames 属性
  • python-flask-gravatar: 使用了已废弃的特性,测试被 pytest 拒绝
  • python-pytest-relaxed: 使用了已废弃的特性,测试被 pytest 拒绝
  • python-pytest-randomly 使用了已废弃的特性,测试被 pytest 拒绝
  • python-deprecated: 测试所预期的警告文本信息已经发生变化
  • python-dbus-signature-pyparsing: 执行时间超过了测试设定的 200ms 时限
  • python-tinycss2: flake8 风格检查未通过
  • python-pytest-runner: black 风格检查未通过
  • python-portend: black 风格检查未通过
  • python-aiohttp: @coroutine 的 DeprecationWarning 被视作错误
  • python-poetry: poetry-core 的一项数据由 dict 改为 OrderedDict,使得输出顺序与测试预期的不一致
  • python-isort: 将使用旧版本 isort 的外部项目的 import 排序视为正确,然后它还真出错了
  • python-cachecontrol: Python 2.7 相关
  • python-zc.lockfile: 测试代码把 Python 3 代码喂给了 Python 2.7。可能是该库已经不支持 2.7 了
  • python-occ-core: 依赖 OpenCASCADE 的版本更新,不被支持
  • protobuf: C 整型比较因表示范围问题而恒为假,警告转错误。是因为新版本的 gcc 比较聪明么?
  • gnome-passwordsafe: 构建系统发现有依赖缺失
  • io: C 代码引用了不存在的系统头文件
  • ceph: C++ 相关问题
  • python-distlib: 调用远程 XML-RPC 太多被限制导致预期的数据与实际错位
  • python-requests-toolbelt: 测试所需要的 HTTP 资源 404 了
  • postgresql: 夏令时结束,导致实际时区与预期对不上。「所以冬天就不要滚包啦,冬天要冬眠!」

所以在这些升级 Python 3.9 的项目中,不兼容 Python 3.9 仅仅只占一半,其中又有一半多属于「总有一天会坏掉」的类型(一大半属于「不听话」,使用没有明确文档、预期为私有的特性,少数尝试当预言家但是失败了)。最后剩下的,再一大半是使用了至少两个版本前已经说了要废弃的特性,只有三个莫名地发现自己真的被 Python 坑了,还都是 C API 部分的。

所以我对我自己的脚本顺利升级到 Python 3.9 非常有信心呢。可能有些老代码使用了已经废弃的特性,所以我也设置了环境变量 PYTHONWARNINGS=default,ignore::ResourceWarning 以便及时得到提示。

哦对了,Arch Linux 中受 Python 3.9 升级影响需要更新的软件包共有2077个,绝大部分我都没见着失败的。目前从开始升级到现在已经过去六天,还剩最后40个失败了的包。

by 依云 at November 16, 2020 01:54 PM

November 02, 2020

中文社区新闻

无障碍(accessible)安装媒介

我们高兴地宣布从 archiso v49 开始我们的安装媒介集成了无障碍功能(accessibility)支持。从 2020.11.01 起,可以从发布的安装媒介中的第二项启动项开启这个特性。在 wiki 上有关于这个的特殊安装指引页

非常感谢 Alexander Epaneshnikov 从 TalkingArch 项目中将相关特性集成到 archiso 的 releng 设置中,我们用它来创建安装媒介。

注意:引导器的超时设置改到了15秒,方便盲人用户选择引导项,因为引导器本身没有提供可用性的相关支持。

by farseerfc at November 02, 2020 12:44 AM

October 29, 2020

百合仙子

让 Arch Linux 系统和最新的镜像同步,从最快的镜像下载

本文来自依云's Blog,转载请注明。

Arch Linux 就是要追新!要追新自然要选择一个更新及时的软件仓库镜像啦,比如国内的 TUNA、USTC 同步都很及时。但是呢,这俩难兄难弟最近一段时间有些吃不消了,导致下载包的时候很慢,甚至超时失败,使用体验真糟糕。如果直接用上游镜像,比如 pkgbuild.com,漂洋过海的,也挺慢的。

而国内另一些镜像,比如网易腾讯云阿里云华为云,他们要么有 CDN,要么线路很好,下载速度飞快。但是呢,他们基本上每天才同步一次,阿里云还时不时连续数天都没能同步成功,这让喜欢追新的 Arch Linux 用户多不舒服呀。当群里的小伙伴们都用上了最新版本的软件,体会到了让人心痒痒的新特性和 bug 时,你 -Syu 却是「今日无事可做」,真是扫兴呢。

和最新的镜像同步,从最快的镜像下载,真的不可兼得吗?

非也。只需要稍微配置一下,用上我的 pacsync 脚本,就可以啦~

配置方式是,为 /etc/pacman.d 下的镜像列表文件创建一个.sync后缀的同名文件,里边指定用于同步的镜像,而不带.sync后缀的文件里按优先级列出多个镜像。pacman 在下载文件时,会按顺序依次尝试列出的镜像,如果遇到更新不及时 404 的时候,就会尝试另一个。这样,可以仅在下载快的镜像里还没有需要的包文件时,才转而从比较慢的镜像下载。

而需要同步 pacman 数据库的时候,使用pacsync脚本取代pacman -Sy。脚本会使用 bind mount 用.sync文件取代不.sync的版本,就能同步到最新的数据库了。原来的pacman -Syu命令要拆开来用,先pacsyncpacman -Su了。

脚本里使用了单独的挂载空间并且将挂载改为了私有,所以并不会影响到外边。

by 依云 at October 29, 2020 02:31 PM

tar 归档的权限问题

本文来自依云's Blog,转载请注明。

一次系统升级之后,我的许多 Python 程序突然开始报错:

[...]
  File "/usr/lib/python3.8/site-packages/pkg_resources/__init__.py", line 2762, in _get_metadata
    for line in self.get_metadata_lines(name):
  File "/usr/lib/python3.8/site-packages/pkg_resources/__init__.py", line 1415, in get_metadata_lines
    return yield_lines(self.get_metadata(name))
  File "/usr/lib/python3.8/site-packages/pkg_resources/__init__.py", line 1405, in get_metadata
    value = self._get(path)
  File "/usr/lib/python3.8/site-packages/pkg_resources/__init__.py", line 1609, in _get
    with open(path, 'rb') as stream:
PermissionError: [Errno 13] Permission denied: '/usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/PKG-INFO'

WTF 滚坏了!立即回滚!

回滚完之后,我开始调查这个事件——因为 [archlinuxcn] 的这个包是我管的呀。而且我记得之前也遇到过一次类似的情况,当时没有深究。

检查一下软件包里的文件的权限:

>>> tar tvf python-telethon-1.17.4-1-any.pkg.tar.zst | grep PKG-INFO
-rw------- root/root      3659 2020-10-24 10:05 usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/PKG-INFO
>>> tar tvf python-telethon-1.17.4-1-any.pkg.tar.zst | grep -- ----
-rw------- root/root      3659 2020-10-24 10:05 usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/PKG-INFO
-rw------- root/root     12078 2020-10-24 10:05 usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/SOURCES.txt
-rw------- root/root         1 2020-10-24 10:05 usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/dependency_links.txt
-rw------- root/root        27 2020-10-24 10:05 usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/requires.txt
-rw------- root/root        15 2020-10-24 10:05 usr/lib/python3.8/site-packages/Telethon-1.17.4-py3.8.egg-info/top_level.txt

好奇怪,Telethon 这些包信息文件怎么只让 root 读了呢?

从 PyPI 上下载 Telethon 的原始 tar 归档回来看看,发现最近几个版本里,文件权限全部只有自己可以读(-rw-------),而所有者是 u0_a167/10167。开发者突然在 Android 系统上打包了呢……安装的时候,部分文件的权限被保留了下来(Arch Linux 打包时强烈反对使用 root 权限执行,因此我用 devtools 打包,解包部分自然是普通用户操作的,所有者无法被保留)。

然后我又看了一下之前的版本,哦豁,所有者成开发者的 id 了,但是有三个版本的 pyc 文件,还有好几个 pyc 文件都是 -rwxrwxrwx。大概系统上的低权限用户可以去改改,然后看谁跑 Telegram 机器人就拿谁的权限?

经过跟开发者的讨论,最终干掉了 pyc 文件,也不在 Android 上打包了。777 权限问题还待解决。不过我更在意的是,为什么会发生这种状况呢?setuptools 干嘛不修一修呢?别的工具创建的用于发布的 tar 归档会不会有类似的问题呢?

结果找了找,发现 setuptools 前年就有人报告这个问题,但是并没有解决。行吧,我打包时统一修正一下权限好了……

下一个 GitHub 生成的 tar 归档看看?咦,-rw-rw-r-- root/root,是处理过了么?啊对,git archive 生成的包是怎么样的?去试了试,原来一样的啊。看来 git 想到了这个问题并且处理了,只是 002 的 umask 有点意外。

Arch Linux 为了普通用户打出文件为 root 所有的 tar 归档使用了 fakeroot,那么 git 是怎么实现的呢?翻了翻代码,git 是自己生成 tar 文件的,写死了所有者是 root/root,但是权限位还是有专门的 umask,默认是 002。可以配置,比如git config --global tar.umask user一下,就会取当前 umask 作为 tar 归档里文件的 umask 了。

至于传统的 GNU autotools 构建系统创建的 tar 归档,我也创建了一个看了一下,并没有特殊处理,跟手动跑 tar 一样。

by 依云 at October 29, 2020 02:01 PM

October 27, 2020

berberman

Setting up a Haskell development environment on Arch Linux

Posted on October 27, 2020 by berberman

Once you accept the principles of Arch Linux – being simplicity and modernity – everything goes easier. In this article, we will use up-to-date Haskell ecosystem by using system provided Haskell packages, getting rid of awkward stack which could eat huge amount of your disk space. We won’t going to nix or ghcup, since they are both general Haskell toolchain solutions, not specific to Arch Linux.

Preface

If you get pandoc, shellcheck, or other Haskell programs installed on your system, you will find that a bunch of packages with haskell- prefix emerge frequently when rolling the system, which is pretty verbose and noisy. Thus, many of general users, i.e. not Haskell developers, always complain that “why every time I tries to roll my system, there are so many Haskell packages to be updated, and wait… What Is Haskell?” Remember that Arch Linux official repositories are not built for Haskell developers, haskell- packages distributed there are only for programs written in Haskell. To save disk space, Haskell executables do not bundle their dependencies, so libraries are stripped into independent packages, consistent with Haskell package management. Other languages’ distributions follow the similar strategy, whereas a vexing problem arise particularly in Haskell packaging. Haskell packages are packaged and linked dynamically, but GHC does not provide a stable ABI, since its specific hash method acting on circular dependences, which lie ubiquitously involving tests, will cause the soname of shared libraries interdependent. Consequently, you may notice that the entire Haskell packages in [community] are not reproducible. If a Haskell library changes, all dependent packages are required to be rebuilt. For us, it is inevitable to rebuild and reinstall all tools which depend on those shared libraries after updating haskell- packages. Some users choose to avoid getting involved this sort of cheating, using static version Haskell programs as alternative. However, as a Haskell developer, we can make full use of these shared libraries.

Configure Cabal

We will use Cabal without sandboxes as our build tool, and system level GHC as our compiler. Let’s install them via system package management tool:

# pacman -S ghc cabal-install

Generate the configuration and update hackage index as normal:

$ cabal update

Cabal are able to find system Haskell packages installed by pacman. If you try to use Cabal directly to compiling your project now, you will get:

Could not find module ‘Prelude’…

indicating that many packages which would come with GHC are missing. This is because currently system Haskell packages provide only dynamic linked shared libraries, which can be used only when GHC is running in dynamic. So we have to configure ~/.cabal/config as following:

library-vanilla: False
shared: True
executable-dynamic: True
program-default-options
  ghc-options: -dynamic

And if we want to install a Haskell program from cabal, we have to run:

cabal install --ghc-options=-dynamic [package to install]

to let Cabal call GHC enabled dynamic linking.

Install the language server

Personally, I would recommend haskell-language-server, which is active in developing and provides unprecedented coding experience. Because we choose using dynamic GHC, we have to compile HLS by ourselves with dynamic option. Clone the source code:

$ git clone https://github.com/haskell/haskell-language-server --recurse-submodules
$ cd haskell-language-server

IMPORTANT: Configure the HLS project locally:

$ cabal configure --disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic

Finally install it:

$ ./cabal-hls-install latest

Next step is choosing your favorite editor, and installing following the instruction in HLS. That’s it, happy coding!

Conclusion

Indeed, we have encountered the first impediment in installing HLS. Using dynamic GHC with system Haskell packages is double-edged, suggesting that we have to face various lurking issues.

Pros:

  • it’s impossible to get stuck into dependency hell (we always use the latest Haskell packages)
  • far less disk usage is required

Cons:

  • out-of-date packages are not available (so there will be slightly fewer libraries we can use)
  • programs involving GHC should be given special treatment (make sure GHC is called with dynamic flag)

The last is kind of troublesome, because maintainers should patch the source of those programs to let them call GHC properly. Here are some examples:

Overall, it seems that we’d better don’t touch these dynamic things in Haskell developing… Anyway, I’m posting this to illustrate that working with haskell- packages is possible. It is worth mentioning that all Haskell packages, and even three tenths of entire Arch Linux packages are maintaining by felixonmars individually, who is dedicated to these unpaid contributions. As an Arch Linux user, I would like to express my high respects to his greatest professionalism and responsibility. Next time, I will introduce my Haskell packaging tool arch-hs, which can be used by both Arch Linux Haskell packagers and Haskell developers.

October 27, 2020 12:00 AM

Setting up a Haskell development environment on Arch Linux

Posted on October 27, 2020 by berberman

Once you accept the principles of Arch Linux – being simplicity and modernity – everything goes easier. In this article, we will use up-to-date Haskell ecosystem by using system provided Haskell packages, getting rid of awkward stack which could eat huge amount of your disk space. We won’t going to nix or ghcup, since they are both general Haskell toolchain solutions, not specific to Arch Linux.

Preface

If you get pandoc, shellcheck, or other Haskell programs installed on your system, you will find that a bunch of packages with haskell- prefix emerge frequently when rolling the system, which is pretty verbose and noisy. Thus, many of general users, i.e. not Haskell developers, always complain that “why every time I tries to roll my system, there are so many Haskell packages to be updated, and wait… What Is Haskell?” Remember that Arch Linux official repositories are not built for Haskell developers, haskell- packages distributed there are only for programs written in Haskell. To save disk space, Haskell executables do not bundle their dependencies, so libraries are stripped into independent packages, consistent with Haskell package management. Other languages’ distributions follow the similar strategy, whereas a vexing problem arise particularly in Haskell packaging. Haskell packages are packaged and linked dynamically, but GHC does not provide a stable ABI, since its specific hash method acting on circular dependences, which lie ubiquitously involving tests, will cause the soname of shared libraries interdependent. Consequently, you may notice that the entire Haskell packages in [community] are not reproducible. If a Haskell library changes, all dependent packages are required to be rebuilt. For us, it is inevitable to rebuild and reinstall all tools which depend on those shared libraries after updating haskell- packages. Some users choose to avoid getting involved this sort of cheating, using static version Haskell programs as alternative. However, as a Haskell developer, we can make full use of these shared libraries.

Configure Cabal

We will use Cabal without sandboxes as our build tool, and system level GHC as our compiler. Let’s install them via system package management tool:

# pacman -S ghc cabal-install

Generate the configuration and update hackage index as normal:

$ cabal update

Cabal are able to find system Haskell packages installed by pacman. If you try to use Cabal directly to compiling your project now, you will get:

Could not find module ‘Prelude’…

indicating that many packages which would come with GHC are missing. This is because currently system Haskell packages provide only dynamic linked shared libraries, which can be used only when GHC is running in dynamic. So we have to configure ~/.cabal/config as following:

library-vanilla: False
shared: True
executable-dynamic: True
program-default-options
  ghc-options: -dynamic

And if we want to install a Haskell program from cabal, we have to run:

cabal install --ghc-options=-dynamic [package to install]

to let Cabal call GHC enabled dynamic linking.

Install the language server

Personally, I would recommend haskell-language-server, which is active in developing and provide unprecedented coding experience. Because we choose using dynamic GHC, we have to compile HLS by ourselves with dynamic option. Clone the source code:

$ git clone https://github.com/haskell/haskell-language-server --recurse-submodules
$ cd haskell-language-server

IMPORTANT: Configure the HLS project locally:

$ cabal configure --disable-library-vanilla --enable-shared --enable-executable-dynamic --ghc-options=-dynamic

Finally install it:

$ ./cabal-hls-install latest

Next step is choosing your favorite editor, and installing following the instruction in HLS. That’s it, happy coding!

Conclusion

Indeed, we have encountered the first impediment in installing HLS. Using dynamic GHC with system Haskell packages is double-edged, suggesting that we have to face various lurking issues.

Pros:

  • it’s impossible to get stuck into dependency hell (we always use the latest Haskell packages)
  • far less disk usage is required

Cons:

  • out-of-date packages are not available (so there will be slightly fewer libraries we can use)
  • programs involving GHC should be given special treatment (make sure GHC is called with dynamic flag)

The last is kind of troublesome, because maintainers should patch the source of those programs to let them call GHC properly. Here are some examples:

Overall, it seems that we’d better don’t touch these dynamic things in Haskell developing… Anyway, I’m posting this to illustrate that working with haskell- packages is possible. It is worth mentioning that all Haskell packages, and even a half of entire Arch Linux packages are maintaining by felixonmars individually, who is dedicated to these unpaid contributions. As an Arch Linux user, I would like to express my high respects to his greatest professionalism and responsibility. Next time, I will introduce my Haskell packaging tool arch-hs, which can be used by both Arch Linux Haskell packagers and Haskell developers.

October 27, 2020 12:00 AM

October 24, 2020

中文社区新闻

libtraceevent>=5.9-1 升级需要手动干预

libtraceevent 包在版本 5.9-1 之前缺失了一个动态库链接。这个问题已经在 5.9-1 中修复,所以更新时需要覆盖 ldconfig 创建出的未被跟踪到的文件。如果你在升级时遇到如下报错:

libtraceevent: /usr/lib/libtraceevent.so.1 exists in filesystem

那么请使用命令:

pacman -Syu --overwrite /usr/lib/libtraceevent.so.1

完成更新。

by farseerfc at October 24, 2020 03:21 AM

October 21, 2020

中文社区新闻

nvidia 455.28 与 linux >= 5.9 不兼容

当前的 nvidia 与 linux >= 5.9 内核部分程度不兼容 [1] [2] 。虽然图形显示应该还能工作,但是搞坏了 CUDA 、 OpenCL 以及可能还有别的一些特性。建议需要这些特性而已经升级了内核的用户切换到 linux-lts 内核,直到 nvidia 提供修复。

by farseerfc at October 21, 2020 04:46 AM

October 06, 2020

Alynx Zhou

奇怪的书名和我和吐槽

好久没更新非技术类博文了。可能很多人不但丢掉了阅读的习惯,连书店都不去了,书店都沦为练习册店,学生们更加对书店敬而远之,这种恶性循环实在是可悲又无趣的事情。

今天和同学一起去了书店,在常规意义的书店而非练习册店的部分发现了一些有趣或奇怪的书名,很有槽点,让人想要吐槽。于是拍了点照片发到这里。

套娃型

此类书名非常适合套娃,比如这本《如何阅读一本书》,且不说阅读方法因人而异且随经验变化,单就这个名字还可以有《如何阅读如何阅读一本书》、《如何阅读如何阅读如何阅读一本书》。

如何阅读一本书

我以为只有当代为搏销量疯狂吸引眼球的作者会起这种名字,然而接下来我看到了一本《木心谈木心》。虽然我不太清楚木心是谁,但似乎是个有名的作者。这书名总是让我想后续会不会有《木心谈木心谈木心》、《木心谈木心谈木心谈木心》。

木心谈木心

性转型

这本书的名字非常奇特,她叫做《少女福尔摩斯》,作为半资深福尔摩斯迷和半资深月球氪金母X,我只能说型月都没敢做的这作者做了。由于原作实在是珠玉在前,写老福的故事已经很难了,性转老福怕是难上加难,建议寄一本给蘑菇看看。

少女福尔摩斯

蹭热度型

这位叫蒋勋的作者在书架上占据了一片,又是说唐诗又是说宋词,一会谈莫奈一会谈高达哦不是梵高和达芬奇,想必上知天文下知地理。不过这书脊把这些名人加粗放在下面,让我总是有一种这是个达芬奇/梵高/莫奈的著作、作品标题叫做蒋勋的感觉。 不知道这个叫蒋勋的作品是不是样样通样样松呢?

蒋勋

还有这本《逝水年华》,连我的输入法都怀疑我是不是想输入《追忆似水年华》,敢问作者是不是有点大舌头,让出版社的编辑听错了标题?

逝水年华

连锁反应型

《知更鸟》吗?大家都说有本书想杀你,不知道与你是什么关系?

知更鸟

虽然《重返美丽新世界》和《美丽新世界》是一个作者,但总让我想起狗尾续貂,不知道是不是我错了。

美丽新世界

好家伙,你把《24个比利》放在这,那他们肯定会打架的,不过叫《比利打群架》比《比利战争》更好吧,无谓夸大不可取,要实事求是。

比利

《面包树上的女人》怎么啦?她没怎么,《面包树出走了》,那可真是个离奇的故事,但是你这内容就不知道是不是好故事了。

面包树

我以为文学大家不屑于做这种在标题上吸引人的无聊事情,毕竟内容才是王道。我也不是有意冒犯杨绛先生,但是《洗澡》做书名已经是太随意了,再整一本《洗澡之后》是不是过分了?还是说这是出书的不负责任的编辑洗澡前洗澡后决定的两本文集的名字?那这就说得通了。但是说实话《洗澡》、《洗澡之后》接下来摆一本《暗示》是不是店员的恶趣味?

洗澡

希望看了我文章的朋友不要给我评论“一看到白胳膊……”,我老实承认我不是一个完全脱离了低级趣味的人,写这文章也是让大家开心一下,毕竟实事求是更重要。我也希望写书的作者们都实事求是一点,内容不行,靠标题党吸引人肯定是经不起考验的。

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at October 06, 2020 09:48 AM

farseerfc

关于 swap 的一些补充

上周翻译完 【译】替 swap 辩护:常见的误解 之后很多朋友们似乎还有些疑问和误解,于是写篇后续澄清一下。事先声明我不是内核开发者, 这里说的只是我的理解, 基于内核文档中关于物理内存的描述 ,新的内核代码的具体行为可能和我的理解有所出入,欢迎踊跃讨论。

误解1: swap 是虚拟内存,虚拟内存肯定比物理内存慢嘛

这种误解进一步的结论通常是:「使用虚拟内存肯定会减慢系统运行时性能,如果物理内存足够为什么还要用虚拟的?」 这种误解是把虚拟内存和交换区的实现方式类比于「虚拟磁盘」或者「虚拟机」等同的方式, 也隐含「先用物理内存,用完了之后用虚拟内存」也即下面的「误解3」的理解。

首先,交换区(swap) 不是 虚拟内存。操作系统中说「物理内存」还是「虚拟内存」的时候在指程序代码 寻址时使用的内存地址方式,使用物理地址空间时是在访问物理内存,使用虚拟地址空间时是在访问虚拟内存。 现代操作系统在大部分情况下都在使用虚拟地址空间寻址, 包括 在执行内核代码的时候。

并且,交换区 不是 实现虚拟内存的方式。操作系统使用内存管理单元(MMU,Memory Management Unit)做虚拟内存地址到物理内存地址的地址翻译,现代架构下 MMU 通常是 CPU 的一部分,配有它专用的一小块存储区叫做地址转换旁路缓存(TLB,Translation Lookaside Buffer), 只有在 TLB 中没有相关地址翻译信息的时候 MMU 才会以缺页中断的形式调用操作系统内核帮忙。 除了 TLB 信息不足的时候,大部分情况下使用虚拟内存都是硬件直接实现的地址翻译,没有软件模拟开销。 实现虚拟内存不需要用到交换区,交换区只是操作系统实现虚拟内存后能提供的一个附加功能, 即便没有交换区,操作系统大部分时候也在用虚拟内存,包括在大部分内核代码中。

误解2: 但是没有交换区的话,虚拟内存地址都有物理内存对应嘛

很多朋友也理解上述操作系统实现虚拟内存的方式,但是仍然会有疑问:「我知道虚拟内存和交换区的区别, 但是没有交换区的话,虚拟内存地址都有物理内存对应,不用交换区的话就不会遇到读虚拟内存需要读写磁盘 导致的卡顿了嘛」。

这种理解也是错的,禁用交换区的时候,也会有一部分分配给程序的虚拟内存不对应物理内存, 比如使用 mmap 调用实现内存映射文件的时候。实际上即便是使用 read/​write 读写文件, Linux 内核中(可能现代操作系统内核都)在底下是用和 mmap 相同的机制建立文件 到虚拟地址空间的地址映射,然后实际读写到虚拟地址时靠缺页中断把文件内容载入页面缓存(page cache )。内核加载可执行程序和动态链接库的方式也是通过内存映射文件。甚至可以进一步说, 用户空间的虚拟内存地址范围内,除了匿名页之外,其它虚拟地址都是文件后备(backed by file ),而匿名页通过交换区作为文件后备。上篇文章中提到的别的类型的内存,比如共享内存页面(shm )是被一个内存中的虚拟文件系统后备的,这一点有些套娃先暂且不提。于是事实是无论有没有交换区, 缺页的时候总会有磁盘读写从慢速存储加载到物理内存,这进一步引出上篇文章中对于交换区和页面缓存这两者的讨论。

误解3: 不是内存快用完的时候才会交换的么?

简短的答案可以说「是」,但是内核理解的「内存快用完」和你理解的很可能不同。 也可以说「不是」,就算按照内核理解的「内存快用完」的定义,内存快用完的时候内核的行为是去回收内存, 至于回收内存的时候内核会做什么有个复杂的启发式经验算法,实际上真的内存快满的时候根本来不及做 swap ,内核可能会尝试丢弃 page cache 甚至丢弃 vfs cache (dentry cache / inode cache) 这些不需要磁盘I/O就能更快获取可用内存的动作。

深究这些内核机制之前,我在思考为什么很多朋友会问出这样的问题。可能大部分这么问的人,学过编程, 稍微学过基本的操作系统原理,在脑海里对内核分配页面留着这样一种印象(C伪代码):

////////////////////  userspace space  ////////////////
void* malloc(int size){
    void* pages = mmap(...);                                    // 从内核分配内存页
    return alloc_from_page(pages, size);                        // 从拿到的内存页细分
}

////////////////////  kernel space  //////////////////
void * SYSCALL do_mmap(...){
   //...
   return kmalloc_pages(nr_page);
}

void* kmalloc_pages(int size){
  while ( available_mem < size ) {
    // 可用内存不够了!尝试搞点内存
    page_frame_info* least_accessed = lru_pop_page_frame();     // 找出最少访问的页面
    switch ( least_accessed -> pf_type ){
      case PAGE_CACHE: drop_page_cache(least_accessed); break;  // 丢弃文件缓存
      case SWAP:       swap_out(least_accessed);        break;  // <- 写磁盘,所以系统卡了!
      // ... 别的方式回收 least_accessed
    }
    append_free_page(free_page_list, least_accessed);           // 回收到的页面加入可用列表
    available_mem += least_accessed -> size;
  }
  // 搞到内存了!返回给程序
  available_mem -= size;
  void * phy_addr = take_from_free_list(free_page_list, size);
  return assign_virtual_addr(phy_addr);
}

这种逻辑隐含三层 错误的 假设:

  1. 分配物理内存是发生在从内核分配内存的时候的,比如 malloc/​mmap 的时候。
  2. 内存回收是发生在进程请求内存分配的上下文里的,换句话说进程在等内核的内存回收返回内存, 不回收到内存,进程就得不到内存。
  3. 交换出内存到 swap 是发生在内存回收的时候的,会阻塞内核的内存回收,进而阻塞程序的内存分配。

这种把内核代码当作「具有特权的库函数调用」的看法,可能很易于理解, 甚至早期可能的确有操作系统的内核是这么实现的,但是很可惜现代操作系统都不是这么做的。 上面三层假设的错误之处在于:

  1. 在程序请求内存的时候,比如 malloc/​mmap 的时候,内核只做虚拟地址分配, 记录下某段虚拟地址空间对这个程序是可以合法访问的,但是不实际分配物理内存给程序。 在程序第一次访问到虚拟地址的时候,才会实际分配物理内存。这种叫 惰性分配(lazy allocation)
  2. 在内核感受到内存分配压力之后,早在内核内存用尽之前,内核就会在后台慢慢扫描并回收内存页。 内存回收通常不发生在内存分配的时候,除非在内存非常短缺的情况下,后台内存回收来不及满足当前分配请求, 才会发生 直接回收(direct reclamation)
  3. 同样除了直接回收的情况,大部分正常情况下换出页面是内存管理子系统调用 DMA 在后台慢慢做的, 交换页面出去不会阻塞内核的内存回收,更不会阻塞程序做内存分配(malloc )和使用内存(实际访问惰性分配的内存页)。

也就是说,现代操作系统内核是高度并行化的设计,内存分配方方面面需要消耗计算资源或者 I/O 带宽的场景,都会尽量并行化,最大程度利用好计算机所有组件(CPU/MMU/DMA/IO)的吞吐率, 不到紧要关头需要直接回收的场合,就不会阻塞程序的正常执行流程。

惰性分配有什么好处?

或许会有人问:「我让你分配内存,你给我分配了个虚拟的,到用的时候还要做很多事情才能给我,这不是骗人嘛」, 或者会有人担心惰性分配会对性能造成负面影响。

这里实际情况是程序从分配虚拟内存的时候,「到用的时候」,这之间有段时间间隔,可以留给内核做准备 。程序可能一下子分配一大片内存地址,然后再在执行过程中解析数据慢慢往地址范围内写东西。 程序分配虚拟内存的速率可以是「突发」的,比如一个系统调用中分配 1GiB 大小,而实际写入数据的速率会被 CPU 执行速度等因素限制,不会短期内突然写入很多页面。 这个分配速率导致的时间差内内核可以完成很多后台工作,比如回收内存, 比如把回收到的别的进程用过的内存页面初始化为全0,这部分后台工作可以和程序的执行过程并行, 从而当程序实际用到内存的时候,需要的准备工作已经做完了,大部分场景下可以直接分配物理内存出来。

如果程序要做实时响应,想避免因为惰性分配造成的性能不稳定,可以使用 mlock/​mlockall 将得到的虚拟内存锁定在物理内存中,锁的过程中内核会做物理内存分配。不过要区分「性能不稳定」和「低性能」, 预先分配内存可以避免实际使用内存时分配物理页面的额外开销,但是会拖慢整体吞吐率,所以要谨慎使用。

很多程序分配了很大一片地址空间,但是实际并不会用完这些地址,直到程序执行结束这些虚拟地址也一直 处于没有对应物理地址的情况。惰性分配可以避免为这些情况浪费物理内存页面,使得很多程序可以无忧无虑地 随意分配内存地址而不用担心性能损失。这种分配方式也叫「超额分配(overcommit)」。飞机票有超售, VPS 提供商划分虚拟机有超售,操作系统管理内存也同样有这种现象,合理使用超额分配能改善整体系统效率。

内核要高效地做到惰性分配而不影响程序执行效率的前提之一,在于程序真的用到内存的时候, 内核能不做太多操作就立刻分配出来,也就是说内核需要时时刻刻在手上留有一部分空页, 满足程序执行时内存分配的需要。换句话说,内核需要早在物理内存用尽之前,就开始回收内存。

那么内核什么时候会开始回收内存?

首先一些背景知识:物理内存地址空间并不是都平等,因为一些地址范围可以做 DMA 而另一些不能,以及 NUMA 等硬件环境倾向于让 CPU 访问其所在 NUMA 节点内存范围。在 32bit 系统上内核的虚拟地址空间还有低端内存和高端内存的区分,他们会倾向于使用不同属性的物理内存,到 64bit 系统上已经没有了这种限制。

硬件限制了内存分配的自由度,于是内核把物理内存空间分成多个 Zone ,每个 Zone 内各自管理可用内存, Zone 内的内存页之间是相互平等的。

zone 内水位线
ditaa diagram

一个 Zone 内的页面分配情况可以右图描绘。 除了已用内存页,剩下的就是空闲页(free pages),空闲页范围中有三个水位线(watermark )评估当前内存压力情况,分别是高位(high)、低位(low)、最小位(min)。

当内存分配使得空闲页水位低于低位线,内核会唤醒 kswapd 后台线程, kswapd 负责扫描物理页面的使用情况并挑选一部分页面做回收,直到可用页面数量恢复到水位线高位(high)以上。 如果 kswapd 回收内存的速度慢于程序执行实际分配内存的速度, 可用空闲页数量可能进一步下降,降至低于最小水位(min)之后,内核会让内存分配进入 直接回收(direct reclamation) 模式,在直接回收模式下,程序分配某个物理页的请求( 第一次访问某个已分配虚拟页面的时候)会导致在进程上下文中阻塞式地调用内存回收代码。

除了内核在后台回收内存,进程也可以主动释放内存,比如有程序退出的时候就会释放一大片内存页, 所以可用页面数量可能会升至水位线高位以上。有太多可用页面浪费资源对整体系统运行效率也不是好事, 所以系统会积极缓存文件读写,所有 page cache 都留在内存中,直到可用页面降至低水位以下触发 kswapd 开始工作。

设置最小水位线(min)的原因在于,内核中有些硬件也会突然请求大量内存,比如来自网卡接收到的数据包, 预留出最小水位线以下的内存给内核内部和硬件使用。

设置高低两个控制 kswapd 开关的水位线是基于控制理论。唤醒 kswapd 扫描内存页面本身有一定计算开销,于是每次唤醒它干活的话就让它多做一些活( high - low ),避免频繁多次唤醒。

因为有这些水位线,系统中根据程序请求内存的「速率」,整个系统的内存分配在宏观的一段时间内可能处于以下几种状态:

  1. 不回收: 系统中的程序申请内存速度很慢,或者程序主动释放内存的速度很快, (比如程序执行时间很短,不怎么进行文件读写就马上退出,)此时可用页面数量可能一直处于低水位线以上, 内核不会主动回收内存,所有文件读写都会以页面缓存的形式留在物理内存中。
  2. 后台回收: 系统中的程序在缓慢申请内存,比如做文件读写, 比如分配并使用匿名页面。系统会时不时地唤醒 kswapd 在后台做内存回收, 不会干扰到程序的执行效率。
  3. 直接回收: 如果程序申请内存的速度快于 kswapd 后台回收内存的速度, 空闲内存最终会跌破最小水位线,随后的内存申请会进入直接回收的代码路径,从而极大限制内存分配速度。 在直接分配和后台回收的同时作用下,空闲内存可能会时不时回到最小水位线以上, 但是如果程序继续申请内存,空闲内存量就会在最小水位线附近上下徘徊。
  4. 杀进程回收: 甚至直接分配和后台回收的同时作用也不足以拖慢程序分配内存的速度的时候, 最终空闲内存会完全用完,此时触发 OOM 杀手干活杀进程。

系统状态处于 1. 不回收 的时候表明分配给系统的内存量过多,比如系统刚刚启动之类的时候。 理想上应该让系统长期处于 2. 后台回收 的状态,此时最大化利用缓存的效率而又不会因为内存回收 减缓程序执行速度。如果系统引导后长期处于 1. 不回收 的状态下,那么说明没有充分利用空闲内存做 文件缓存,有些 unix 服务比如 preload 可用来提前填充文件缓存。

如果系统频繁进入 3. 直接回收 的状态,表明在这种工作负载下系统需要减慢一些内存分配速度, 让 kswapd 有足够时间回收内存。就如前一篇翻译中 Chris 所述,频繁进入这种状态也不一定代表「内存不足」,可能表示内存分配处于非常高效的利用状态下, 系统充分利用慢速的磁盘带宽,为快速的内存缓存提供足够的可用空间。 直接回收 是否对进程负载有负面影响要看具体负载的特性。 此时选择禁用 swap 并不能降低磁盘I/O,反而可能缩短 2. 后台回收 状态能持续的时间, 导致更快进入 4. 杀进程回收 的极端状态。

当然如果系统长期处于 直接回收 的状态的话,则说明内存总量不足,需要考虑增加物理内存, 或者减少系统负载了。如果系统进入 4. 杀进程回收 的状态,不光用空间的进程会受影响, 并且还可能导致内核态的内存分配受影响,产生网络丢包之类的结果。

微调内存管理水位线

可以看一下运行中的系统中每个 Zone 的水位线在哪儿。比如我手上这个 16GiB 的系统中:

$ cat /proc/zoneinfo
Node 0, zone      DMA
   pages free     3459
         min      16
         low      20
         high     24
         spanned  4095
         present  3997
         managed  3975
Node 0, zone    DMA32
   pages free     225265
         min      3140
         low      3925
         high     4710
         spanned  1044480
         present  780044
         managed  763629
Node 0, zone   Normal
   pages free     300413
         min      13739
         low      17173
         high     20607
         spanned  3407872
         present  3407872
         managed  3328410

因为不是 NUMA 系统,所以只有一个 NUMA node,其中根据 DMA 类型共有 3 个 Zone 分别叫 DMA, DMA32, Normal 。三个 Zone 的物理地址范围(spanned)加起来大概有 \(4095+1044480+3407872\) 大约 17GiB 的地址空间,而实际可访问的地址范围(present )加起来有 \(3997+780044+3407872\) 大约 16GiB 的可访问物理内存。

其中空闲页面有 \(3459+762569+1460218\) 大约 8.5GiB ,三条水位线分别在: \(\texttt{high} = 24+4710+20607 = 98\texttt{MiB}\)\(\texttt{low} = 20+3925+17173 = 82\texttt{MiB}\)\(\texttt{min} = 16+3140+13739 = 65\texttt{MiB}\) 的位置。

具体这些水位线的确定方式基于几个 sysctl 。首先 min 基于 vm.min_free_kbytes 默认是基于内核低端内存量的平方根算的值,并限制到最大 64MiB 再加点余量,比如我这台机器上 vm.min_free_kbytes = 67584 ,于是 min 水位线在这个位置。 其它两个水位线基于这个计算,在 min 基础上增加总内存量的 vm.watermark_scale_factor /​ 10000 比例(在小内存的系统上还有额外考虑),默认 vm.watermark_scale_factor = 10 在大内存系统上意味着 low 比 min 高 0.1% , high 比 low 高 0.1% 。

可以手动设置这些值,以更早触发内存回收,比如将 vm.watermark_scale_factor 设为 100:

$ echo 100 | sudo tee /proc/sys/vm/watermark_scale_factor
$ cat /proc/zoneinfo
Node 0, zone      DMA
   pages free     3459
         min      16
         low      55
         high     94
         spanned  4095
         present  3997
         managed  3975
   Node 0, zone    DMA32
   pages free     101987
         min      3149
         low      10785
         high     18421
         spanned  1044480
         present  780044
         managed  763629
   Node 0, zone   Normal
   pages free     61987
         min      13729
         low      47013
         high     80297
         spanned  3407872
         present  3407872
         managed  3328410

得到的三条水位线分别在 \(\texttt{min} = 16+3149+13729 = 66\texttt{MiB}\)\(\texttt{low} = 55+10785+47013 = 226\texttt{MiB}\)\(\texttt{high} = 94+18421+80297 = 386\texttt{MiB}\) , 从而 low 和 high 分别比 min 提高 160MiB 也就是内存总量的 1% 左右。

在 swap 放在 HDD 的系统中,因为换页出去的速度较慢,除了上篇文章说的降低 vm.swappiness 之外,还可以适当提高 vm.watermark_scale_factor 让内核更早开始回收内存,这虽然会稍微降低缓存命中率,但是另一方面可以在进入直接回收模式之前 有更多时间做后台换页,也将有助于改善系统整体流畅度。

只有 0.1% ,这不就是说内存快用完的时候么?

所以之前的「误解3」我说答案可以说「是」或者「不是」,但是无论回答是或不是,都代表了认为「swap 就是额外的慢速内存」的错误看法。当有人在强调「swap 是内存快用完的时候才交换」的时候, 隐含地,是在把系统总体的内存分配看作是一个静态的划分过程:打个比方这就像在说,我的系统里存储空间有快速 128GiB SSD 和慢速 HDD 的 1TiB ,同样内存有快速的 16GiB RAM 和慢速 16GiB 的 swap 。 这种静态划分的类比是错误的看待方式,因为系统回收内存进而做页面交换的方式是动态平衡的过程, 需要考虑到「时间」和「速率」而非单纯看「容量」。

假设 swap 所在的存储设备可以支持 5MiB/s 的吞吐率( HDD 上可能更慢, SSD 上可能更快,这里需要关注数量级),相比之下 DDR3 大概有 10GiB/s 的吞吐率,DDR4 大概有 20GiB/s ,无论多快的 SSD 也远达不到这样的吞吐(可能 Intel Optane 这样的 DAX 设备会改变这里的状况)。从而把 swap 当作慢速内存的视角来看的话,加权平均的速率是非常悲观的,「 16G 的 DDR3 + 16G 的 swap 会有 \(\frac{16 \times 10 \times 1024 + 16 \times 5}{16+16} = 5 \texttt{GiB/s}\) 的吞吐?所以开 swap 导致系统速度降了一半?」显然不能这样看待。

动态的看待方式是, swap 设备能提供 5MiB/s 的吞吐,这意味着:如果我们能把未来 10 分钟内不会访问到的页面换出到 swap ,那么就相当于有 \(10 \times 60 \texttt{s} \times 5 \texttt{MiB/s} = 3000 \texttt{MiB}\) 的额外内存,用来放那 10 分钟内可能会访问到的页面缓存。 10 分钟只是随口说的一段时间,可以换成 10 秒或者 10 小时,重要的是只要页面交换发生在后台, 不阻塞前台程序的执行,那么 swap 设备提供的额外吞吐率相当于一段时间内提供了更大的物理内存, 总是能提升页面缓存的命中,从而改善系统性能。

当然系统内核不能预知「未来 10 分钟内需要的页面」,只能根据历史上访问内存的情况预估之后可能会访问的情况, 估算不准的情况下,比如最近10分钟内用过的页面缓存在之后10分钟内不再被使用的时候, 为了把最近这10分钟内访问过的页面留在物理内存中,可能会把之后10分钟内要用到的匿名页面换出到了交换设备上。 于是会有下面的情况:

但是我开了 swap 之后,一旦复制大文件,系统就变卡,不开 swap 不会这样的

大概电脑用户都经历过这种现象,不限于 Linux 用户,包括 macOS 和 Windows 上也是。 在文件管理器中复制了几个大文件之后,切换到别的程序系统就极其卡顿,复制已经结束之后的一段时间也会如此。 复制的过程中系统交换区的使用率在上涨,复制结束后下降,显然 swap 在其中有重要因素,并且禁用 swap 或者调低 swappiness 之后就不会这样了。于是网上大量流传着解释这一现象,并进一步建议禁用 swap 或者调低 swappiness 的文章。我相信不少关心系统性能调优的人看过这篇「 Tales from responsivenessland: why Linux feels slow, and how to fix that 」或是它的转载、翻译,用中文搜索的话还能找到更多错误解释 swappiness 目的的文章,比如 这篇将 swappiness 解释成是控制内存和交换区比例的参数

除去那些有技术上谬误的文章,这些网文中描述的现象是有道理的,不单纯是以讹传讹。 桌面环境中内存分配策略的不确定性和服务器环境中很不一样,复制、下载、解压大文件等导致一段时间内 大量占用页面缓存,以至于把操作结束后需要的页面撵出物理内存,无论是交换出去的方式还是以丢弃页面缓存的方式, 都会导致桌面响应性降低。

不过就像前文 Chris 所述,这种现象其实并不能通过禁止 swap 的方式缓解:禁止 swap 或者调整 swappiness 让系统尽量避免 swap 只影响回收匿名页面的策略,不影响系统回收页面的时机, 也不能避免系统丢弃将要使用的页面缓存而导致的卡顿。

以前在 Linux 上也没有什么好方法能避免这种现象。 macOS 转用 APFS 作为默认文件系统之后, 从文件管理器(Finder)复制文件默认启用 file clone 快速完成,这操作不实际复制文件数据, 一个隐含优势在不需要读入文件内容,从而不会导致大量页面缓存失效。 Linux 上同样可以用支持 reflink 的文件系统比如 btrfs 或者开了 reflink=1 的 xfs 达到类似的效果。 不过 reflink 也只能拯救复制文件的情况,不能改善解压文件、下载文件、计算文件校验等情况下, 一次性处理大文件对内存产生的压力。

好在最近几年 Linux 有了 cgroup ,允许更细粒度地调整系统资源分配。进一步现在我们有了 cgroup v2 ,前面 Chris 的文章也有提到 cgroup v2 的 memory.low 可以某种程度上建议内存子系统 尽量避免回收某些 cgroup 进程的内存。

于是有了 cgroup 之后,另一种思路是把复制文件等大量使用内存而之后又不需要保留页面缓存的程序单独放入 cgroup 内限制它的内存用量,用一点点复制文件时的性能损失换来整体系统的响应流畅度。

关于 cgroup v1 和 v2

稍微跑题说一下 cgroup v2 相对于 v1 带来的优势。这方面优势在 Chris Down 另一个关于 cgroup v2 演讲 中有提到。老 cgroup v1 按控制器区分 cgroup 层级,从而内存控制器所限制的东西和 IO 控制器所限制的东西是独立的。在内核角度来看,页面写回(page writeback)和交换(swap)正是 夹在内存控制器和IO控制器管理的边界上,从而用 v1 的 cgroup 难以同时管理。 v2 通过统一控制器层级解决了这方面限制。具体见下面 Chris Down 的演讲。

用 cgroup v2 限制进程的内存分配

实际上有了 cgroup v2 之后,还有更多控制内存分配的方案。 cgroup v2 的内存控制器 可以对某个 cgroup 设置这些阈值:

  • memory.min : 最小内存限制。内存用量低于此值后系统不会回收内存。
  • memory.low : 低内存水位。内存用量低于此值后系统会尽量避免回收内存。
  • memory.high : 高内存水位。内存用量高于此值后系统会积极回收内存,并且会对内存分配节流(throttle)。
  • memory.max : 最大内存限制。内存用量高于此值后系统会对内存分配请求返回 ENOMEM,或者在 cgroup 内触发 OOM 。

可见这些设定值可以当作 per-cgroup 的内存分配水位线,作用于某一部分进程而非整个系统。 针对交换区使用情况也可设置这些阈值:

  • memory.swap.high : 高交换区水位,交换区用量高于此值后会对交换区分配节流。
  • memory.swap.max : 最大交换区限制,交换区用量高于此值后不再会发生匿名页交换。

到达这些 cgroup 设定阈值的时候,还可以设置内核回调的处理程序,从用户空间做一些程序相关的操作。

Linux 有了 cgroup v2 之后,就可以通过对某些程序设置内存用量限制,避免他们产生的页面请求把别的 程序所需的页面挤出物理内存。使用 systemd 的系统中,首先需要 启用 cgroup v2 ,在内核引导参数中加上 systemd.unified_cgroup_hierarchy=1 。然后开启用户权限代理:

# systemctl edit user@1000.service
[Service]
Delegate=yes

然后可以定义用户会话的 slice (slice 是 systemd 术语,用来映射 cgroup ),比如创建一个叫 limit-mem 的 slice :

$ cat ~/.config/systemd/user/limit-mem.slice
[Slice]
MemoryHigh=3G
MemoryMax=4G
MemorySwapMax=2G

然后可以用 systemd-run 限制在某个 slice 中打开一个 shell:

$ systemd-run --user --slice=limit-mem.slice --shell

或者定义一个 shell alias 用来限制任意命令:

$ type limit-mem
limit-mem is an alias for /usr/bin/time systemd-run --user --pty --same-dir --wait --collect --slice=limit-mem.slice
$ limit-mem cp some-large-file dest/

实际用法有很多,可以参考 systemd 文档 man systemd.resource-controlxuanwo有篇博客介绍过 systemd 下资源限制lilydjwg写过用 cgroup 限制进程内存的用法用 cgroup 之后对 CPU 调度的影响

未来展望

最近新版的 gnome 和 KDE 已经开始为桌面环境下用户程序的进程创建 systemd scope 了, 可以通过 systemd-cgls 观察到,每个通过桌面文件(.desktop)开启的用户空间程序 都有个独立的名字叫 app-APPNAME-HASH.scope 之类的 systemd scope 。 有了这些 scope 之后,事实上用户程序的资源分配某种程度上已经相互独立, 不过默认的用户程序没有施加多少限制。

今后可以展望,桌面环境可以提供用户友好的方式对这些桌面程序施加公平性的限制。 不光是内存分配的大小限制,包括 CPU 和 IO 占用方面也会更公平。 值得一提的是传统的 ext4/xfs/f2fs 之类的文件系统虽然支持 cgroup writeback 节流 但是因为他们有额外的 journaling 写入,难以单独针对某些 cgroup 限制 IO 写入带宽(对文件系统元数据的写入难以统计到具体某组进程)。 而 btrfs 通过 CoW 避免了 journaling , 在这方面有更好的支持 。相信不远的将来,复制大文件之类常见普通操作不再需要手动调用加以限制, 就能避免单个程序占用太多资源影响别的程序。

by farseerfc at October 06, 2020 04:45 AM

October 03, 2020

中文社区新闻

ghostpcl>=9.53.2-2 和 ghostxps>=9.53.2-2 升级需要手动干预

ghostpcl 和 ghostxps 包在版本 9.53.2-2 之前各缺失了一个动态库链接。这个问题已经在 9.53.2-2 中修复,所以更新时需要覆盖 ldconfig 创建出的未被跟踪到的文件。如果你在升级时遇到如下报错:

ghostpcl: /usr/lib/libgpcl6.so.9 exists in filesystem
ghostxps: /usr/lib/libgxps.so.9 exists in filesystem

那么请使用命令:

pacman -Syu --overwrite /usr/lib/libgpcl6.so.9,/usr/lib/libgxps.so.9

完成更新。

by farseerfc at October 03, 2020 03:25 PM

September 30, 2020

farseerfc

【译】替 swap 辩护:常见的误解

这篇翻译自 Chris Down 的博文 In defence of swap: common misconceptions原文的协议CC BY-SA 4.0 ,本文翻译同样也使用 CC BY-SA 4.0 。其中加入了一些我自己的理解作为旁注,所有译注都在侧边栏中。

翻译这篇文章是因为经常看到朋友们(包括有经验的程序员和 Linux 管理员)对 swap 和 swappiness 有诸多误解,而这篇文章正好澄清了这些误解,也讲清楚了 Linux 中这两者的本质。值得一提的是本文讨论的 swap 针对 Linux 内核,在别的系统包括 macOS/WinNT 或者 Unix 系统中的交换文件可能有不同一样的行为, 需要不同的调优方式。比如在 FreeBSD handbook 中明确建议了 swap 分区通常应该是两倍物理内存大小,这一点建议对 FreeBSD 系内核的内存管理可能非常合理, 而不一定适合 Linux 内核,FreeBSD 和 Linux 有不同的内存管理方式尤其是 swap 和 page cache 和 buffer cache 的处理方式有诸多不同。

经常有朋友看到系统卡顿之后看系统内存使用状况观察到大量 swap 占用,于是觉得卡顿是来源于 swap 。就像文中所述,相关不蕴含因果,产生内存颠簸之后的确会造成大量 swap 占用,也会造成系统卡顿, 但是 swap 不是导致卡顿的原因,关掉 swap 或者调低 swappiness 并不能阻止卡顿,只会将 swap 造成的 I/O 转化为加载文件缓存造成的 I/O 。

以下是原文翻译:


这篇文章也有 日文俄文 翻译。

tl;dr:

  1. Having swap is a reasonably important part of a well functioning system. Without it, sane memory management becomes harder to achieve.
  2. Swap is not generally about getting emergency memory, it's about making memory reclamation egalitarian and efficient. In fact, using it as "emergency memory" is generally actively harmful.
  3. Disabling swap does not prevent disk I/O from becoming a problem under memory contention, it simply shifts the disk I/O thrashing from anonymous pages to file pages. Not only may this be less efficient, as we have a smaller pool of pages to select from for reclaim, but it may also contribute to getting into this high contention state in the first place.
  4. The swapper on kernels before 4.0 has a lot of pitfalls, and has contributed to a lot of people's negative perceptions about swap due to its overeagerness to swap out pages. On kernels >4.0, the situation is significantly better.
  5. On SSDs, swapping out anonymous pages and reclaiming file pages are essentially equivalent in terms of performance/latency. On older spinning disks, swap reads are slower due to random reads, so a lower vm.swappiness setting makes sense there (read on for more about vm.swappiness ).
  6. Disabling swap doesn't prevent pathological behaviour at near-OOM, although it's true that having swap may prolong it. Whether the system global OOM killer is invoked with or without swap, or was invoked sooner or later, the result is the same: you are left with a system in an unpredictable state. Having no swap doesn't avoid this.
  7. You can achieve better swap behaviour under memory pressure and prevent thrashing using memory.low and friends in cgroup v2.

太长不看:

  1. 对维持系统的正常功能而言,有 swap 是相对挺重要的一部分。没有它的话会更难做到合理的内存管理。
  2. swap 的目的通常并不是用作紧急内存,它的目的在于让内存回收能更平等和高效。 事实上把它当作「紧急内存」来用的想法通常是有害的。
  3. 禁用 swap 在内存压力下并不能避免磁盘I/O造成的性能问题,这么做只是让磁盘I/O颠簸的范围从 匿名页面转化到文件页面。这不仅更低效,因为系统能回收的页面的选择范围更有限了, 而且这种做法还可能是加重了内存压力的原因之一。
  4. 内核 4.0 版本之前的交换进程(swapper)有一些问题,导致很多人对 swap 有负面印象, 因为它太急于(overeagerness)把页面交换出去。在 4.0 之后的内核上这种情况已经改善了很多。
  5. 在 SSD 上,交换出匿名页面的开销和回收文件页面的开销基本上在性能/延迟方面没有区别。 在老式的磁盘上,读取交换文件因为属于随机访问读取所以会更慢,于是设置较低的 vm.swappiness 可能比较合理(继续读下面关于 vm.swappiness 的描述)。
  6. 禁用 swap 并不能避免在接近 OOM 状态下最终表现出的症状,尽管的确有 swap 的情况下这种症状持续的时间可能会延长。在系统调用 OOM 杀手的时候无论有没有启用 swap ,或者更早/更晚开始调用 OOM 杀手,结果都是一样的:整个系统留在了一种不可预知的状态下。 有 swap 也不能避免这一点。
  7. 可以用 cgroup v2 的 memory.low 相关机制来改善内存压力下 swap 的行为并且 避免发生颠簸。

As part of my work improving kernel memory management and cgroup v2, I've been talking to a lot of engineers about attitudes towards memory management, especially around application behaviour under pressure and operating system heuristics used under the hood for memory management.

我的工作的一部分是改善内核中内存管理和 cgroup v2 相关,所以我和很多工程师讨论过看待内存管理的态度, 尤其是在压力下应用程序的行为和操作系统在底层内存管理中用的基于经验的启发式决策逻辑。

A repeated topic in these discussions has been swap. Swap is a hotly contested and poorly understood topic, even by those who have been working with Linux for many years. Many see it as useless or actively harmful: a relic of a time where memory was scarce, and disks were a necessary evil to provide much-needed space for paging. This is a statement that I still see being batted around with relative frequency in recent years, and I've had many discussions with colleagues, friends, and industry peers to help them understand why swap is still a useful concept on modern computers with significantly more physical memory available than in the past.

在这种讨论中经常重复的话题是交换区(swap)。交换区的话题是非常有争议而且很少被理解的话题,甚至包括那些在 Linux 上工作过多年的人也是如此。很多人觉得它没什么用甚至是有害的:它是历史遗迹,从内存紧缺而 磁盘读写是必要之恶的时代遗留到现在,为计算机提供在当年很必要的页面交换功能作为内存空间。 最近几年我还经常能以一定频度看到这种论调,然后我和很多同事、朋友、业界同行们讨论过很多次, 帮他们理解为什么在现代计算机系统中交换区仍是有用的概念,即便现在的电脑中物理内存已经远多于过去。

There's also a lot of misunderstanding about the purpose of swap – many people just see it as a kind of "slow extra memory" for use in emergencies, but don't understand how it can contribute during normal load to the healthy operation of an operating system as a whole.

围绕交换区的目的还有很多误解——很多人觉得它只是某种为了应对紧急情况的「慢速额外内存」, 但是没能理解在整个操作系统健康运作的时候它也能改善普通负载的性能。

Many of us have heard most of the usual tropes about memory: " Linux uses too much memory ", " swap should be double your physical memory size ", and the like. While these are either trivial to dispel, or discussion around them has become more nuanced in recent years, the myth of "useless" swap is much more grounded in heuristics and arcana rather than something that can be explained by simple analogy, and requires somewhat more understanding of memory management to reason about.

我们很多人也听说过描述内存时所用的常见说法: 「 Linux 用了太多内存 」,「 swap 应该设为物理内存的两倍大小 」,或者类似的说法。 虽然这些误解要么很容易化解,或者关于他们的讨论在最近几年已经逐渐变得琐碎,但是关于「无用」交换区 的传言有更深的经验传承的根基,而不是一两个类比就能解释清楚的,并且要探讨这个先得对内存管理有 一些基础认知。

This post is mostly aimed at those who administrate Linux systems and are interested in hearing the counterpoints to running with undersized/no swap or running with vm.swappiness set to 0.

本文主要目标是针对那些管理 Linux 系统并且有兴趣理解「让系统运行于低/无交换区状态」或者「把 vm.swappiness 设为 0 」这些做法的反论。

背景

It's hard to talk about why having swap and swapping out pages are good things in normal operation without a shared understanding of some of the basic underlying mechanisms at play in Linux memory management, so let's make sure we're on the same page.

如果没有基本理解 Linux 内存管理的底层机制是如何运作的,就很难讨论为什么需要交换区以及交换出页面 对正常运行的系统为什么是件好事,所以我们先确保大家有讨论的基础。

内存的类型

There are many different types of memory in Linux, and each type has its own properties. Understanding the nuances of these is key to understanding why swap is important.

Linux 中内存分为好几种类型,每种都有各自的属性。想理解为什么交换区很重要的关键一点在于理解这些的细微区别。

For example, there are pages ("blocks" of memory, typically 4k) responsible for holding the code for each process being run on your computer. There are also pages responsible for caching data and metadata related to files accessed by those programs in order to speed up future access. These are part of the page cache , and I will refer to them as file memory.

比如说,有种 页面(「整块」的内存,通常 4K) 是用来存放电脑里每个程序运行时各自的代码的。 也有页面用来保存这些程序所需要读取的文件数据和元数据的缓存,以便加速随后的文件读写。 这些内存页面构成 页面缓存(page cache),后文中我称他们为文件内存。

There are also pages which are responsible for the memory allocations made inside that code, for example, when new memory that has been allocated with malloc is written to, or when using mmap 's MAP_ANONYMOUS flag. These are "anonymous" pages – so called because they are not backed by anything – and I will refer to them as anon memory.

还有一些页面是在代码执行过程中做的内存分配得到的,比如说,当代码调用 malloc 能分配到新内存区,或者使用 mmap MAP_ANONYMOUS 标志分配的内存。 这些是「匿名(anonymous)」页面——之所以这么称呼它们是因为他们没有任何东西作后备—— 后文中我称他们为匿名内存。

There are other types of memory too – shared memory, slab memory, kernel stack memory, buffers, and the like – but anonymous memory and file memory are the most well known and easy to understand ones, so I will use these in my examples, although they apply equally to these types too.

还有其它类型的内存——共享内存、slab内存、内核栈内存、文件缓冲区(buffers),这种的—— 但是匿名内存和文件内存是最知名也最好理解的,所以后面的例子里我会用这两个说明, 虽然后面的说明也同样适用于别的这些内存类型。

可回收/不可回收内存

One of the most fundamental questions when thinking about a particular type of memory is whether it is able to be reclaimed or not. "Reclaim" here means that the system can, without losing data, purge pages of that type from physical memory.

考虑某种内存的类型时,一个非常基础的问题是这种内存是否能被回收。 「回收(Reclaim)」在这里是指系统可以,在不丢失数据的前提下,从物理内存中释放这种内存的页面。

For some page types, this is typically fairly trivial. For example, in the case of clean (unmodified) page cache memory, we're simply caching something that we have on disk for performance, so we can drop the page without having to do any special operations.

对一些内存类型而言,是否可回收通常可以直接判断。比如对于那些干净(未修改)的页面缓存内存, 我们只是为了性能在用它们缓存一些磁盘上现有的数据,所以我们可以直接扔掉这些页面, 不需要做什么特殊的操作。

For some page types, this is possible, but not trivial. For example, in the case of dirty (modified) page cache memory, we can't just drop the page, because the disk doesn't have our modifications yet. As such we either need to deny reclamation or first get our changes back to disk before we can drop this memory.

对有些内存类型而言,回收是可能的,但是不是那么直接。比如对脏(修改过)的页面缓存内存, 我们不能直接扔掉这些页面,因为磁盘上还没有写入我们所做的修改。这种情况下,我们可以选择拒绝回收, 或者选择先等待我们的变更写入磁盘之后才能扔掉这些内存。

For some page types, this is not possible. For example, in the case of the anonymous pages mentioned previously, they only exist in memory and in no other backing store, so they have to be kept there.

对还有些内存类型而言,是不能回收的。比如前面提到的匿名页面,它们只存在于内存中,没有任何后备存储, 所以它们必须留在内存里。

说到交换区的本质

If you look for descriptions of the purpose of swap on Linux, you'll inevitably find many people talking about it as if it is merely an extension of the physical RAM for use in emergencies. For example, here is a random post I got as one of the top results from typing "what is swap" in Google:

Swap is essentially emergency memory; a space set aside for times when your system temporarily needs more physical memory than you have available in RAM. It's considered "bad" in the sense that it's slow and inefficient, and if your system constantly needs to use swap then it obviously doesn't have enough memory. […] If you have enough RAM to handle all of your needs, and don't expect to ever max it out, then you should be perfectly safe running without a swap space.

如果你去搜 Linux 上交换区的目的的描述,肯定会找到很多人说交换区只是在紧急时用来扩展物理内存的机制。 比如下面这段是我在 google 中输入「什么是 swap」 从前排结果中随机找到的一篇:

交换区本质上是紧急内存;是为了应对你的系统临时所需内存多余你现有物理内存时,专门分出一块额外空间。 大家觉得交换区「不好」是因为它又慢又低效,并且如果你的系统一直需要使用交换区那说明它明显没有足够的内存。 [……]如果你有足够内存覆盖所有你需要的情况,而且你觉得肯定不会用满内存,那么完全可以不用交换区 安全地运行系统。
To be clear, I don't blame the poster of this comment at all for the content of their post – this is accepted as "common knowledge" by a lot of Linux sysadmins and is probably one of the most likely things that you will hear from one if you ask them to talk about swap. It is unfortunately also, however, a misunderstanding of the purpose and use of swap, especially on modern systems.

事先说明,我不想因为这些文章的内容责怪这些文章的作者——这些内容被很多 Linux 系统管理员认为是「常识」, 并且很可能你问他们什么是交换区的时候他们会给你这样的回答。但是也很不幸的是, 这种认识是使用交换区的目的的一种普遍误解,尤其在现代系统上。

Above, I talked about reclamation for anonymous pages being "not possible", as anonymous pages by their nature have no backing store to fall back to when being purged from memory – as such, their reclamation would result in complete data loss for those pages. What if we could create such a store for these pages, though?

前文中我说过回收匿名页面的内存是「不可能的」,因为匿名内存的特点,把它们从内存中清除掉之后, 没有别的存储区域能作为他们的备份——因此,要回收它们会造成数据丢失。但是,如果我们为这种内存页面创建 一种后备存储呢?

Well, this is precisely what swap is for. Swap is a storage area for these seemingly "unreclaimable" pages that allows us to page them out to a storage device on demand. This means that they can now be considered as equally eligible for reclaim as their more trivially reclaimable friends, like clean file pages, allowing more efficient use of available physical memory.

嗯,这正是交换区存在的意义。交换区是一块存储空间,用来让这些看起来「不可回收」的内存页面在需要的时候 可以交换到存储设备上。这意味着有了交换区之后,这些匿名页面也和别的那些可回收内存一样, 可以作为内存回收的候选,就像干净文件页面,从而允许更有效地使用物理内存。

Swap is primarily a mechanism for equality of reclamation, not for emergency "extra memory". Swap is not what makes your application slow – entering overall memory contention is what makes your application slow.

交换区主要是为了平等的回收机制,而不是为了紧急情况的「额外内存」。使用交换区不会让你的程序变慢—— 进入内存竞争的状态才是让程序变慢的元凶。

So in what situations under this "equality of reclamation" scenario would we legitimately choose to reclaim anonymous pages? Here are, abstractly, some not uncommon scenarios:

那么在这种「平等的可回收机遇」的情况下,让我们选择回收匿名页面的行为在何种场景中更合理呢? 抽象地说,比如在下述不算罕见的场景中:

  1. During initialisation, a long-running program may allocate and use many pages. These pages may also be used as part of shutdown/cleanup, but are not needed once the program is "started" (in an application-specific sense). This is fairly common for daemons which have significant dependencies to initialise.
  2. During the program's normal operation, we may allocate memory which is only used rarely. It may make more sense for overall system performance to require a major fault to page these in from disk on demand, instead using the memory for something else that's more important.
  1. 程序初始化的时候,那些长期运行的程序可能要分配和使用很多页面。这些页面可能在最后的关闭/清理的 时候还需要使用,但是在程序「启动」之后(以具体的程序相关的方式)持续运行的时候不需要访问。 对后台服务程序来说,很多后台程序要初始化不少依赖库,这种情况很常见。
  2. 在程序的正常运行过程中,我们可能分配一些很少使用的内存。对整体系统性能而言可能比起让这些内存页 一直留在内存中,只有在用到的时候才按需把它们用 缺页异常(major fault) 换入内存, 可以空出更多内存留给更重要的东西。

考察有无交换区时会发生什么

Let's look at typical situations, and how they perform with and without swap present. I talk about metrics around "memory contention" in my talk on cgroup v2 .

我们来看一些在常见场景中,有无交换区时分别会如何运行。 在我的 关于 cgroup v2 的演讲 中探讨过「内存竞争」的指标。

在无/低内存竞争的状态下

  • With swap: We can choose to swap out rarely-used anonymous memory that may only be used during a small part of the process lifecycle, allowing us to use this memory to improve cache hit rate, or do other optimisations.
  • Without swap We cannot swap out rarely-used anonymous memory, as it's locked in memory. While this may not immediately present as a problem, on some workloads this may represent a non-trivial drop in performance due to stale, anonymous pages taking space away from more important use.
  • 有交换区: 我们可以选择换出那些只有在进程生存期内很小一部分时间会访问的匿名内存, 这允许我们空出更多内存空间用来提升缓存命中率,或者做别的优化。
  • 无交换区: 我们不能换出那些很少使用的匿名内存,因为它们被锁在了内存中。虽然这通常不会直接表现出问题, 但是在一些工作条件下这可能造成卡顿导致不平凡的性能下降,因为匿名内存占着空间不能给 更重要的需求使用。
译注:关于 内存热度内存颠簸(thrash)

讨论内核中内存管理的时候经常会说到内存页的 冷热 程度。这里冷热是指历史上内存页被访问到的频度, 内存管理的经验在说,历史上在近期频繁访问的 内存,在未来也可能被频繁访问, 从而应该留在物理内存里;反之历史上不那么频繁访问的 内存,在未来也可能很少被用到, 从而可以考虑交换到磁盘或者丢弃文件缓存。

颠簸(thrash) 这个词在文中出现多次但是似乎没有详细介绍,实际计算机科学专业的课程中应该有讲过。 一段时间内,让程序继续运行所需的热内存总量被称作程序的工作集(workset),估算工作集大小, 换句话说判断进程分配的内存页中哪些属于 内存哪些属于 内存,是内核中 内存管理的最重要的工作。当分配给程序的内存大于工作集的时候,程序可以不需要等待I/O全速运行; 而当分配给程序的内存不足以放下整个工作集的时候,意味着程序每执行一小段就需要等待换页或者等待 磁盘缓存读入所需内存页,产生这种情况的时候,从用户角度来看可以观察到程序肉眼可见的「卡顿」。 当系统中所有程序都内存不足的时候,整个系统都处于颠簸的状态下,响应速度直接降至磁盘I/O的带宽。 如本文所说,禁用交换区并不能防止颠簸,只是从等待换页变成了等待文件缓存, 给程序分配超过工作集大小的内存才能防止颠簸。

在中/高内存竞争的状态下

  • With swap: All memory types have an equal possibility of being reclaimed. This means we have more chance of being able to reclaim pages successfully – that is, we can reclaim pages that are not quickly faulted back in again (thrashing).
  • Without swap Anonymous pages are locked into memory as they have nowhere to go. The chance of successful long-term page reclamation is lower, as we have only some types of memory eligible to be reclaimed at all. The risk of page thrashing is higher. The casual reader might think that this would still be better as it might avoid having to do disk I/O, but this isn't true – we simply transfer the disk I/O of swapping to dropping hot page caches and dropping code segments we need soon.
  • 有交换区: 所有内存类型都有平等的被回收的可能性。这意味着我们回收页面有更高的成功率—— 成功回收的意思是说被回收的那些页面不会在近期内被缺页异常换回内存中(颠簸)。
  • 无交换区: 匿名内存因为无处可去所以被锁在内存中。长期内存回收的成功率变低了,因为我们成体上 能回收的页面总量少了。发生缺页颠簸的危险性更高了。缺乏经验的读者可能觉得这某时也是好事, 因为这能避免进行磁盘I/O,但是实际上不是如此——我们只是把交换页面造成的磁盘I/O变成了扔掉热缓存页 和扔掉代码段,这些页面很可能马上又要从文件中读回来。

在临时内存占用高峰时

  • With swap: We're more resilient to temporary spikes, but in cases of severe memory starvation, the period from memory thrashing beginning to the OOM killer may be prolonged. We have more visibility into the instigators of memory pressure and can act on them more reasonably, and can perform a controlled intervention.
  • Without swap The OOM killer is triggered more quickly as anonymous pages are locked into memory and cannot be reclaimed. We're more likely to thrash on memory, but the time between thrashing and OOMing is reduced. Depending on your application, this may be better or worse. For example, a queue-based application may desire this quick transfer from thrashing to killing. That said, this is still too late to be really useful – the OOM killer is only invoked at moments of severe starvation, and relying on this method for such behaviour would be better replaced with more opportunistic killing of processes as memory contention is reached in the first place.
  • 有交换区: 我们对内存使用激增的情况更有抵抗力,但是在严重的内存不足的情况下, 从开始发生内存颠簸到 OOM 杀手开始工作的时间会被延长。内存压力造成的问题更容易观察到, 从而可能更有效地应对,或者更有机会可控地干预。
  • 无交换区: 因为匿名内存被锁在内存中了不能被回收,所以 OOM 杀手会被更早触发。 发生内存颠簸的可能性更大,但是发生颠簸之后到 OOM 解决问题的时间间隔被缩短了。 基于你的程序,这可能更好或是更糟。比如说,基于队列的程序可能更希望这种从颠簸到杀进程的转换更快发生。 即便如此,发生 OOM 的时机通常还是太迟于是没什么帮助——只有在系统极度内存紧缺的情况下才会请出 OOM 杀手,如果想依赖这种行为模式,不如换成更早杀进程的方案,因为在这之前已经发生内存竞争了。

好吧,所以我需要系统交换区,但是我该怎么为每个程序微调它的行为?

You didn't think you'd get through this entire post without me plugging cgroup v2, did you? ;-)

你肯定想到了我写这篇文章一定会在哪儿插点 cgroup v2 的安利吧? ;-)

Obviously, it's hard for a generic heuristic algorithm to be right all the time, so it's important for you to be able to give guidance to the kernel. Historically the only tuning you could do was at the system level, using vm.swappiness . This has two problems: vm.swappiness is incredibly hard to reason about because it only feeds in as a small part of a larger heuristic system, and it also is system-wide instead of being granular to a smaller set of processes.

显然,要设计一种对所有情况都有效的启发算法会非常难,所以给内核提一些指引就很重要。 历史上我们只能在整个系统层面做这方面微调,通过 vm.swappiness 。这有两方面问题: vm.swappiness 的行为很难准确控制,因为它只是传递给一个更大的启发式算法中的一个小参数; 并且它是一个系统级别的设置,没法针对一小部分进程微调。

You can also use mlock to lock pages into memory, but this requires either modifying program code, fun with LD_PRELOAD , or doing horrible things with a debugger at runtime. In VM-based languages this also doesn't work very well, since you generally have no control over allocation and end up having to mlockall , which has no precision towards the pages you actually care about.

你可以用 mlock 把页面锁在内存里,但是这要么必须改程序代码,或者折腾 LD_PRELOAD ,或者在运行期用调试器做一些魔法操作。对基于虚拟机的语言来说这种方案也不能 很好工作,因为通常你没法控制内存分配,最后得用上 mlockall ,而这个没有办法精确指定你实际上想锁住的页面。

cgroup v2 has a tunable per-cgroup in the form of memory.low , which allows us to tell the kernel to prefer other applications for reclaim below a certain threshold of memory used. This allows us to not prevent the kernel from swapping out parts of our application, but prefer to reclaim from other applications under memory contention. Under normal conditions, the kernel's swap logic is generally pretty good, and allowing it to swap out pages opportunistically generally increases system performance. Swap thrash under heavy memory contention is not ideal, but it's more a property of simply running out of memory entirely than a problem with the swapper. In these situations, you typically want to fail fast by self-killing non-critical processes when memory pressure starts to build up.

cgroup v2 提供了一套可以每个 cgroup 微调的 memory.low ,允许我们告诉内核说当使用的内存低于一定阈值之后优先回收别的程序的内存。这可以让我们不强硬禁止内核 换出程序的一部分内存,但是当发生内存竞争的时候让内核优先回收别的程序的内存。在正常条件下, 内核的交换逻辑通常还是不错的,允许它有条件地换出一部分页面通常可以改善系统性能。在内存竞争的时候 发生交换颠簸虽然不理想,但是这更多地是单纯因为整体内存不够了,而不是因为交换进程(swapper)导致的问题。 在这种场景下,你通常希望在内存压力开始积攒的时候通过自杀一些非关键的进程的方式来快速退出(fail fast)。

You can not simply rely on the OOM killer for this. The OOM killer is only invoked in situations of dire failure when we've already entered a state where the system is severely unhealthy and may well have been so for a while. You need to opportunistically handle the situation yourself before ever thinking about the OOM killer.

你不能依赖 OOM 杀手达成这个。 OOM 杀手只有在非常急迫的情况下才会出动,那时系统已经处于极度不健康的 状态了,而且很可能在这种状态下保持了一阵子了。需要在开始考虑 OOM 杀手之前,积极地自己处理这种情况。

Determination of memory pressure is somewhat difficult using traditional Linux memory counters, though. We have some things which seem somewhat related, but are merely tangential – memory usage, page scans, etc – and from these metrics alone it's very hard to tell an efficient memory configuration from one that's trending towards memory contention. There is a group of us at Facebook, spearheaded by Johannes , working on developing new metrics that expose memory pressure more easily that should help with this in future. If you're interested in hearing more about this, I go into detail about one metric being considered in my talk on cgroup v2.

不过,用传统的 Linux 内存统计数据还是挺难判断内存压力的。我们有一些看起来相关的系统指标,但是都 只是支离破碎的——内存用量、页面扫描,这些——单纯从这些指标很难判断系统是处于高效的内存利用率还是 在滑向内存竞争状态。我们在 Facebook 有个团队,由 Johannes 牵头,努力开发一些能评价内存压力的新指标,希望能在今后改善目前的现状。 如果你对这方面感兴趣, 在我的 cgroup v2 的演讲中介绍到一个被提议的指标

调优

那么,我需要多少交换空间?

In general, the minimum amount of swap space required for optimal memory management depends on the number of anonymous pages pinned into memory that are rarely reaccessed by an application, and the value of reclaiming those anonymous pages. The latter is mostly a question of which pages are no longer purged to make way for these infrequently accessed anonymous pages.

通常而言,最优内存管理所需的最小交换空间取决于程序固定在内存中而又很少访问到的匿名页面的数量, 以及回收这些匿名页面换来的价值。后者大体上来说是问哪些页面不再会因为要保留这些很少访问的匿名页面而 被回收掉腾出空间。

If you have a bunch of disk space and a recent (4.0+) kernel, more swap is almost always better than less. In older kernels kswapd , one of the kernel processes responsible for managing swap, was historically very overeager to swap out memory aggressively the more swap you had. In recent times, swapping behaviour when a large amount of swap space is available has been significantly improved. If you're running kernel 4.0+, having a larger swap on a modern kernel should not result in overzealous swapping. As such, if you have the space, having a swap size of a few GB keeps your options open on modern kernels.

如果你有足够大的磁盘空间和比较新的内核版本(4.0+),越大的交换空间基本上总是越好的。 更老的内核上 kswapd , 内核中负责管理交换区的内核线程,在历史上倾向于有越多交换空间就 急于交换越多内存出去。在最近一段时间,可用交换空间很大的时候的交换行为已经改善了很多。 如果在运行 4.0+ 以后的内核,即便有很大的交换区在现代内核上也不会很激进地做交换。因此, 如果你有足够的容量,现代内核上有个几个 GB 的交换空间大小能让你有更多选择。

If you're more constrained with disk space, then the answer really depends on the tradeoffs you have to make, and the nature of the environment. Ideally you should have enough swap to make your system operate optimally at normal and peak (memory) load. What I'd recommend is setting up a few testing systems with 2-3GB of swap or more, and monitoring what happens over the course of a week or so under varying (memory) load conditions. As long as you haven't encountered severe memory starvation during that week – in which case the test will not have been very useful – you will probably end up with some number of MB of swap occupied. As such, it's probably worth having at least that much swap available, in addition to a little buffer for changing workloads. atop in logging mode can also show you which applications are having their pages swapped out in the SWAPSZ column, so if you don't already use it on your servers to log historic server state you probably want to set it up on these test machines with logging mode as part of this experiment. This also tells you when your application started swapping out pages, which you can tie to log events or other key data.

如果你的磁盘空间有限,那么答案更多取决于你愿意做的取舍,以及运行的环境。理想上应该有足够的交换空间 能高效应对正常负载和高峰(内存)负载。我建议先用 2-3GB 或者更多的交换空间搭个测试环境, 然后监视在不同(内存)负载条件下持续一周左右的情况。只要在那一周里没有发生过严重的内存不足—— 发生了的话说明测试结果没什么用——在测试结束的时候大概会留有多少 MB 交换区占用。 作为结果说明你至少应该有那么多可用的交换空间,再多加一些以应对负载变化。用日志模式跑 atop 可以在 SWAPSZ 栏显示程序的页面被交换出去的情况,所以如果你还没用它记录服务器历史日志的话 ,这次测试中可以试试在测试机上用它记录日志。这也会告诉你什么时候你的程序开始换出页面,你可以用这个 对照事件日志或者别的关键数据。

Another thing worth considering is the nature of the swap medium. Swap reads tend to be highly random, since we can't reliably predict which pages will be refaulted and when. On an SSD this doesn't matter much, but on spinning disks, random I/O is extremely expensive since it requires physical movement to achieve. On the other hand, refaulting of file pages is likely less random, since files related to the operation of a single application at runtime tend to be less fragmented. This might mean that on a spinning disk you may want to bias more towards reclaiming file pages instead of swapping out anonymous pages, but again, you need to test and evaluate how this balances out for your workload.

另一点值得考虑的是交换空间所在存储设备的媒介。读取交换区倾向于很随机,因为我们不能可靠预测什么时候 什么页面会被再次访问。在 SSD 上这不是什么问题,但是在传统磁盘上,随机 I/O 操作会很昂贵, 因为需要物理动作寻道。另一方面,重新加载文件缓存可能不那么随机,因为单一程序在运行期的文件读操作 一般不会太碎片化。这可能意味着在传统磁盘上你想更多地回收文件页面而不是换出匿名页面,但仍旧, 你需要做测试评估在你的工作负载下如何取得平衡。

译注:关于休眠到磁盘时的交换空间大小
原文这里建议交换空间至少是物理内存大小,我觉得实际上不需要。休眠到磁盘的时候内核会写回并丢弃 所有有文件作后备的可回收页面,交换区只需要能放下那些没有文件后备的页面就可以了。 如果去掉文件缓存页面之后剩下的已用物理内存总量能完整放入交换区中,就可以正常休眠。 对于桌面浏览器这种内存大户,通常有很多缓存页可以在休眠的时候丢弃。
For laptop/desktop users who want to hibernate to swap, this also needs to be taken into account – in this case your swap file should be at least your physical RAM size.

对笔记本/桌面用户如果想要休眠到交换区,这也需要考虑——这种情况下你的交换文件应该至少是物理内存大小。

我的 swappiness 应该如何设置?

First, it's important to understand what vm.swappiness does. vm.swappiness is a sysctl that biases memory reclaim either towards reclamation of anonymous pages, or towards file pages. It does this using two different attributes: file_prio (our willingness to reclaim file pages) and anon_prio (our willingness to reclaim anonymous pages). vm.swappiness`plays into this, as it becomes the default value for :code:`anon_prio , and it also is subtracted from the default value of 200 for file_prio , which means for a value of vm.swappiness = 50 , the outcome is that anon_prio is 50, and file_prio is 150 (the exact numbers don't matter as much as their relative weight compared to the other).

首先很重要的一点是,要理解 vm.swappiness 是做什么的。 vm.swappiness 是一个 sysctl 用来控制在内存回收的时候,是优先回收匿名页面, 还是优先回收文件页面。内存回收的时候用两个属性: file_prio (回收文件页面的倾向) 和 anon_prio (回收匿名页面的倾向)。 vm.swappiness 控制这两个值, 因为它是 anon_prio 的默认值,然后也是默认 200 减去它之后 file_prio 的默认值。 意味着如果我们设置 vm.swappiness = 50 那么结果是 anon_prio 是 50, file_prio 是 150 (这里数值本身不是很重要,重要的是两者之间的权重比)。

译注:关于 SSD 上的 swappiness

原文这里说 SSD 上 swap 和 drop page cache 差不多开销所以 vm.swappiness = 100 。我觉得实际上要考虑 swap out 的时候会产生写入操作,而 drop page cache 可能不需要写入( 要看页面是否是脏页)。如果负载本身对I/O带宽比较敏感,稍微调低 swappiness 可能对性能更好, 内核的默认值 60 是个不错的默认值。以及桌面用户可能对性能不那么关心,反而更关心 SSD 的写入寿命,虽然说 SSD 写入寿命一般也足够桌面用户,不过调低 swappiness 可能也能减少一部分不必要的写入(因为写回脏页是必然会发生的,而写 swap 可以避免)。 当然太低的 swappiness 会对性能有负面影响(因为太多匿名页面留在物理内存里而降低了缓存命中率) ,这里的权衡也需要根据具体负载做测试。

另外澄清一点误解, swap 分区还是 swap 文件对系统运行时的性能而言没有差别。或许有人会觉得 swap 文件要经过文件系统所以会有性能损失,在译文之前译者说过 Linux 的内存管理子系统基本上独立于文件系统。 实际上 Linux 上的 swapon 在设置 swap 文件作为交换空间的时候会读取一次文件系统元数据, 确定 swap 文件在磁盘上的地址范围,随后运行的过程中做交换就和文件系统无关了。关于 swap 空间是否连续的影响,因为 swap 读写基本是页面单位的随机读写,所以即便连续的 swap 空间(swap 分区)也并不能改善 swap 的性能。希疏文件的地址范围本身不连续,写入希疏文件的空洞需要 文件系统分配磁盘空间,所以在 Linux 上交换文件不能是希疏文件。只要不是希疏文件, 连续的文件内地址范围在磁盘上是否连续(是否有文件碎片)基本不影响能否 swapon 或者使用 swap 时的性能。

This means that, in general, vm.swappiness is simply a ratio of how costly reclaiming and refaulting anonymous memory is compared to file memory for your hardware and workload. The lower the value, the more you tell the kernel that infrequently accessed anonymous pages are expensive to swap out and in on your hardware. The higher the value, the more you tell the kernel that the cost of swapping anonymous pages and file pages is similar on your hardware. The memory management subsystem will still try to mostly decide whether it swaps file or anonymous pages based on how hot the memory is, but swappiness tips the cost calculation either more towards swapping or more towards dropping filesystem caches when it could go either way. On SSDs these are basically as expensive as each other, so setting vm.swappiness = 100 (full equality) may work well. On spinning disks, swapping may be significantly more expensive since swapping in generally requires random reads, so you may want to bias more towards a lower value.

这意味着,通常来说 vm.swappiness 只是一个比例,用来衡量在你的硬件和工作负载下, 回收和换回匿名内存还是文件内存哪种更昂贵 。设定的值越低,你就是在告诉内核说换出那些不常访问的 匿名页面在你的硬件上开销越昂贵;设定的值越高,你就是在告诉内核说在你的硬件上交换匿名页和 文件缓存的开销越接近。内存管理子系统仍然还是会根据实际想要回收的内存的访问热度尝试自己决定具体是 交换出文件还是匿名页面,只不过 swappiness 会在两种回收方式皆可的时候,在计算开销权重的过程中左右 是该更多地做交换还是丢弃缓存。在 SSD 上这两种方式基本上是同等开销,所以设成 vm.swappiness = 100 (同等比重)可能工作得不错。在传统磁盘上,交换页面可能会更昂贵, 因为通常需要随机读取,所以你可能想要设低一些。

The reality is that most people don't really have a feeling about which their hardware demands, so it's non-trivial to tune this value based on instinct alone – this is something that you need to test using different values. You can also spend time evaluating the memory composition of your system and core applications and their behaviour under mild memory reclamation.

现实是大部分人对他们的硬件需求没有什么感受,所以根据直觉调整这个值可能挺困难的 —— 你需要用不同的值做测试。你也可以花时间评估一下你的系统的内存分配情况和核心应用在大量回收内存的时候的行为表现。

When talking about vm.swappiness , an extremely important change to consider from recent(ish) times is this change to vmscan by Satoru Moriya in 2012 , which changes the way that vm.swappiness = 0 is handled quite significantly.

讨论 vm.swappiness 的时候,一个极为重要需要考虑的修改是(相对)近期在 2012 年左右 Satoru Moriya 对 vmscan 行为的修改 ,它显著改变了代码对 vm.swappiness = 0 这个值的处理方式。

Essentially, the patch makes it so that we are extremely biased against scanning (and thus reclaiming) any anonymous pages at all with vm.swappiness = 0 , unless we are already encountering severe memory contention. As mentioned previously in this post, that's generally not what you want, since this prevents equality of reclamation prior to extreme memory pressure occurring, which may actually lead to this extreme memory pressure in the first place. vm.swappiness = 1 is the lowest you can go without invoking the special casing for anonymous page scanning implemented in that patch.

基本上来说这个补丁让我们在 vm.swappiness = 0 的时候会极度避免扫描(进而回收)匿名页面, 除非我们已经在经历严重的内存抢占。就如本文前面所属,这种行为基本上不会是你想要的, 因为这种行为会导致在发生内存抢占之前无法保证内存回收的公平性,这甚至可能是最初导致发生内存抢占的原因。 想要避免这个补丁中对扫描匿名页面的特殊行为的话, vm.swappiness = 1 是你能设置的最低值。

The kernel default here is vm.swappiness = 60 . This value is generally not too bad for most workloads, but it's hard to have a general default that suits all workloads. As such, a valuable extension to the tuning mentioned in the "how much swap do I need" section above would be to test these systems with differing values for vm.swappiness , and monitor your application and system metrics under heavy (memory) load. Some time in the near future, once we have a decent implementation of refault detection in the kernel, you'll also be able to determine this somewhat workload-agnostically by looking at cgroup v2's page refaulting metrics.

内核在这里设置的默认值是 vm.swappiness = 60 。这个值对大部分工作负载来说都不会太坏, 但是很难有一个默认值能符合所有种类的工作负载。因此,对上面「 那么,我需要多少交换空间? 」那段讨论的一点重要扩展可以说,在测试系统中可以尝试使用不同的 vm.swappiness ,然后监视你的程序和系统在重(内存)负载下的性能指标。在未来某天,如果我们在内核中有了合理的 缺页检测 ,你也将能通过 cgroup v2 的页面缺页 指标来以负载无关的方式决定这个。

2019年07月更新:内核 4.20+ 中的内存压力指标

The refault metrics mentioned as in development earlier are now in the kernel from 4.20 onwards and can be enabled with CONFIG_PSI=y . See my talk at SREcon at around the 25:05 mark:

前文中提到的开发中的内存缺页检测指标已经进入 4.20+ 以上版本的内核,可以通过 CONFIG_PSI=y 开启。详情参见我在 SREcon 大约 25:05 左右的讨论。

结论

  • Swap is a useful tool to allow equality of reclamation of memory pages, but its purpose is frequently misunderstood, leading to its negative perception across the industry. If you use swap in the spirit intended, though – as a method of increasing equality of reclamation – you'll find that it's a useful tool instead of a hindrance.
  • Disabling swap does not prevent disk I/O from becoming a problem under memory contention, it simply shifts the disk I/O thrashing from anonymous pages to file pages. Not only may this be less efficient, as we have a smaller pool of pages to select from for reclaim, but it may also contribute to getting into this high contention state in the first place.
  • Swap can make a system slower to OOM kill, since it provides another, slower source of memory to thrash on in out of memory situations – the OOM killer is only used by the kernel as a last resort, after things have already become monumentally screwed. The solutions here depend on your system:
    • You can opportunistically change the system workload depending on cgroup-local or global memory pressure. This prevents getting into these situations in the first place, but solid memory pressure metrics are lacking throughout the history of Unix. Hopefully this should be better soon with the addition of refault detection.
    • You can bias reclaiming (and thus swapping) away from certain processes per-cgroup using memory.low, allowing you to protect critical daemons without disabling swap entirely.
  • 交换区是允许公平地回收内存的有用工具,但是它的目的经常被人误解,导致它在业内这种负面声誉。如果 你是按照原本的目的使用交换区的话——作为增加内存回收公平性的方式——你会发现它是很有效的工具而不是阻碍。
  • 禁用交换区并不能在内存竞争的时候防止磁盘I/O的问题,它只不过把匿名页面的磁盘I/O变成了文件页面的 磁盘I/O。这不仅更低效,因为我们回收内存的时候能选择的页面范围更小了,而且它可能是导致高度内存竞争 状态的元凶。
  • 有交换区会导致系统更慢地使用 OOM 杀手,因为在缺少内存的情况下它提供了另一种更慢的内存, 会持续地内存颠簸——内核调用 OOM 杀手只是最后手段,会晚于所有事情已经被搞得一团糟之后。 解决方案取决于你的系统:
    • 你可以预先更具每个 cgroup 的或者系统全局的内存压力改变系统负载。这能防止我们最初进入内存竞争 的状态,但是 Unix 的历史中一直缺乏可靠的内存压力检测方式。希望不久之后在有了 缺页检测 这样的性能指标之后能改善这一点。
    • 你可以使用 memory.low 让内核不倾向于回收(进而交换)特定一些 cgroup 中的进程, 允许你在不禁用交换区的前提下保护关键后台服务。

感谢在撰写本文时 RahulTejunJohannes 提供的诸多建议和反馈。

by farseerfc at September 30, 2020 04:45 AM

September 26, 2020

Alynx Zhou

固定 GNOME Shell 的输入法列表

GNOME Shell 有个令人很不爽的“特性”,它的输入法列表使用的是最近使用优先排列。也就是说当你有三个或以上输入法的时候,比如我,我有英文简体中文和日语输入法,我经常在中英之间切换,这没什么,前两个总是中英所以按一次就可以在这两个之间切换,但假如我偶尔用了一次日语输入法,我的列表就被打乱了,我不清楚按几下才能切回中文,并且再切到英文也得看一眼才能知道。

我不是很理解这个特性存在的意义,设置里面是可以手动调节输入法的顺序的,我明明调成了我想要的顺序,你就给我这个顺序好了,这样我闭着眼睛不用动脑子都能猜出来要按几下,比如从英文到日语按两下,中文到日语按一下等等。可能有些人的脑子长得比较擅长模拟最近使用优先排列?反正我不行。

既然感觉不爽那就动手处理一下好了,最近看了一些有关写 GNOME Shell 扩展的文档,所以写个扩展解决一下就可以了。为什么不直接提交给上游?因为上游一开始是固定顺序的,但是很久以前某个人加了这个“特性”,现在如果提个请求说删掉这个特性,势必会陷入一场“用户到底是喜欢最近使用优先排列还是固定排列”的争论,这肯定很难得出结论(毕竟大部分的人实际上是不需要使用输入法的英语用户以及只有两种输入法的用户!),并且按照 GNOME 上游的习惯他们也是不愿意为了这个多添加一个开关的。所以比起在拉锯战上浪费时间,先搞一个能用的才是我的风格。至于升级之后扩展挂掉……不就是在上游里和其他代码一起被重构和我自己单独重构的区别吗?只要我还在用应该就会持续更新了。

具体的解决方法比较 dirty,是我从别的扩展里学来的:把 GNOME Shell 里面的类的原型上的方法替换成自己的,就可以修改实例调用时的函数了(也算 JS 特性之一),不过不要用箭头函数,因为显然我们希望 this 是调用时的上下文也就是实例,而不是绑定到当前上下文。

因为这算是我第一个扩展所以也多少记录一下踩的坑。

首先 Gjs 的导入和 Node.js 的导入是不一样的,它通过一个 imports 对象引入其他库,比如通过 GI 导入的就在 gi 下面,因为是 GNOME Shell 扩展所以可以访问 GNOME Shell 的 JS 库,就是简单地把 JS 路径换成对象的 key 然后 JS 文件里所有的 varfunction 都会被导出。比如导入 Main 就是 imports.ui.main.Main

然后就是怎么知道要修改什么以及如何获取到相关对象,不过因为 GNOME Shell JS 部分经常重构,也没什么完整的文档,反正只能多花时间看代码吧,而且它的结构其实比看起来的要复杂,所以经常需要仔细翻来翻去的。比如 GNOME Shell 的输入法部分很多人认为是需要修改 iBus,实际上 GNOME Shell 只是调用 iBus 作为后端,自己处理状态和界面,这部分的代码都在 js/ui/status/keyboard.js 里面。

扩展主要有 init()enable()disable() 三个函数,init() 在 GNOME Shell 加载扩展时候调用,我这个显然不需要。enable() 是你在 Extensions app 里面打开开关时候调用的,disable() 是关掉开关时候调用的。

enable() 里面有几个需要我修改的地方,一个是阻止 InputSourceManager 在输入法切换之后的最近使用优先排列,解决方法很简单,需要自己替换掉 _currentInputSourceChanged 函数,注释掉 https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/gnome-3-36/js/ui/status/keyboard.js#L447-453 这一段更新代码。

当然光有这个还是不行的,这样假如你是先切换过再打开扩展,实际上列表是你开启扩展之前的状态而不是用户设置的顺序,所以我们还需要在打开扩展之后更新它的列表,让它直接读取用户设置。更新列表的函数是 _updateMruSources,假如检测到当前列表为空,会先从一个缓存的 gsettings 里读取之前存储的最近使用优先排列列表,这显然是很恶心的所以要注释掉 https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/gnome-3-36/js/ui/status/keyboard.js#L504-522 这一段。之后它会先加载当前列表里的,然后再把用户列表里增加的当前列表里没有的加到后面,因为我们已经决定要清空当前列表并且不加载 gsettings 里面的缓存,所以这个当前列表肯定是空,那直接加用户列表就行了,所以注释掉 https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/gnome-3-36/js/ui/status/keyboard.js#L525-533。这样我们后续只要清空 _mruSources 设置 _mruSourcesBackupnull 然后调用 _updateMruSources 就可以了。

然后我们需要获取运行时的这个 InputSourceManager 实例,这个实例没有被绑定到 Main 对象上,不过我阅读代码发现它是个单例模式,就是说在 js/ui/status/keyboard.js 有一个 _inputSourceManager 变量,然后有个 getInputSourceManager() 的函数,被调用时候如果有就返回 _inputSourceManager 否则创建一个赋值返回,其他代码都用的这个,所以我们也导入这个就行了。

然后你会发现另一个弱智的地方,怎么每次按下切换键,切换框都是从第一个切换到第二个?不是应该从我当前的切换到下一个吗?这个对于当前输入法总在第一个的最近使用优先排列是可以的,但在我们这个场景选中的并不总是第一个,所以需要修改。这部分函数是 _switchInputSource,可以看到它只是展示了一个 InputSourcePopup,而 InputSourcePopup 继承的是 imports.ui.switcherPopup.SwitcherPopup,这个类有一个叫做 _selectedIndex 的变量用于选择下一个上一个时候的计算,而且它默认是 0!不能通过参数初始化!真是头秃,不过我们可以在创建完切换框但展示之前单独设置这个值就行了,所以我在 https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/gnome-3-36/js/ui/status/keyboard.js#L412 的下一行插入如下代码:

if (this._currentSource != null) {
  popup._selectedIndex = this._mruSources.indexOf(this._currentSource);
}

因为我们不一定总有 _currentSource 所以还是要检查一下,如果没有的话让它从 0 开始也无所谓。

然后还有一个比较头痛的是快捷键是绑定的回调函数,回调函数又绑定了 this,所以我们光修改原型是改不了被回调的函数的,这个也简单,我们需要读一下 InputSourceManagerconstructor 的代码,然后删掉它在 Main.wm 里面绑定的组合键重新绑定成我们的,就是这样:

Main.wm.removeKeybinding("switch-input-source");
_inputSourceManager._keybindingAction =
  Main.wm.addKeybinding(
    "switch-input-source",
    new Gio.Settings({"schema_id": "org.gnome.desktop.wm.keybindings"}),
    Meta.KeyBindingFlags.NONE,
    Shell.ActionMode.ALL,
    InputSourceManager.prototype._switchInputSource.bind(_inputSourceManager)
  );
Main.wm.removeKeybinding("switch-input-source-backward");
_inputSourceManager._keybindingActionBackward =
  Main.wm.addKeybinding(
    "switch-input-source-backward",
    new Gio.Settings({"schema_id": "org.gnome.desktop.wm.keybindings"}),
    Meta.KeyBindingFlags.IS_REVERSED,
    Shell.ActionMode.ALL,
    InputSourceManager.prototype._switchInputSource.bind(_inputSourceManager)
  );

同样我们也不要忘记绑定 this,实际上我们希望调用的时候绑定的 this 是那个单例,那直接 bind 它就好了。

但是你会发现这个弱智的家伙没有按我们想象的工作!这是什么意思!仔细阅读代码之后我发现有如下逻辑:你按下的第一次组合键其实并不是算在那个弹框的按键回调里面,而是我们通过构造函数传递进去的,然后它分析这个传进去的按键是哪一种,调用 _initialSelection 执行第一次切换,而这个家伙更弱智了!明明有 _selectedIndex 它不用,竟然用硬编码的倒数第一个和第一个!真有你的啊!我不太敢修改 SwitcherPopup 因为还有别的东西使用它,那就修改 InputSourcePopup 这个子类吧,其实就是把 InputSourcePopup.prototype._initialSelection 这个函数原来的的 this._select(this._items.length - 1); 换成 this._select(this._previous());this._select(1) 换成 this._select(this._next())(1 其实是 0 + 1 的意思),不但功能增加了,可读性也提升了!

现在搭配起来应该和我们的需求一致了!但假如我关掉扩展之后希望列表是打开之前的状态怎么办!还记得之前说的那个 _updateMruSources 会读取 gsettings 吗?这个 gsettings 实际上在每次切换输入法的时候都会写入当前状态,那我们只要让它开启扩展时候不要写入,关掉扩展恢复的时候再更新不就读取了之前的状态吗。所以需要修改 InputSourceManager.prototype._updateMruSettings,注释掉 https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/gnome-3-36/js/ui/status/keyboard.js#L432-438

总结一下其实就是在 enable 的时候修改这些函数,然后获取单例,重新绑定快捷键,然后清空当前的列表重新更新列表,然后为了避免 bug,我们总是激活列表里的第一个输入法:

if (_inputSourceManager._mruSources.length > 0) {
  _inputSourceManager._mruSources[0].activate(true);
}

disable 的时候同样是把函数修改回去,然后获取单例,重新绑定快捷键这样它又绑成了原来的函数,然后清空当前的列表重新更新列表这样它就恢复到开启之前的顺序了,接下来同样地,因为最近使用优先列表的第一个元素肯定是正在用的,所以我们也还是激活第一个输入法就可以了。

完整的项目参见 GitHub,Arch Linux 用户也可以从 AUR 或者 Arch Linux CN 源安装 gnome-shell-extension-fixed-ime-list

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at September 26, 2020 02:40 AM

September 24, 2020

中文社区新闻

Arch Conf 2020 日程

我们将在10月10日和11日举办在线 Arch Conf。会议中将会有来自 Arch 团队的讲演和从社区提交的讲演和闪电讨论。

我们非常高兴能发布日程安排的初版计划!

https://pretalx.com/arch-conf-online-2020/talk/

会议所在时区是 CEST/UTC+2:
https://everytimezone.com/s/40cc4784

更新和附加消息将发表在会议主页上: https://conf.archlinux.org

期待您的参与!

来自会议主办团队的祝贺

by farseerfc at September 24, 2020 12:34 AM

September 23, 2020

frantic1048

Amakuni - 梅瑟·恩达斯特 (Endro~!)

Mei

期待已久的梅依这周平安无事到了,带着前些天到的大号背景纸一起开个箱。另外看到最近 Darktable 3.21 更新了 filmic rgb v42 模块,这次也尝试了一下它带来的新的工作流,总体体验还是令人满意的。

这次手办细节上感觉很稳,各个角度过去没有一眼就让人想开 retouch 模块开始划拉的地方,而且服装细节还原得非常棒。与之对比的是 之前的四糸乃,衣服的蓝色区域有很多并不是尘土的细小白点,虽然普通眼睛观察不怎么看不出来,但是到了照片里,就像平时看不到的尘土一样全都冒出来了,结果就是令人头秃的修图,毕竟都断过一次手了,这点不算什么

Yoshino with many retouches

好了,上图!

Mei Mei

Mei Mei Mei Mei

Mei Mei Mei

Mei


  1. darktable 3.2: containment effect! https://www.darktable.org/2020/08/darktable-3-2/

  2. darktable ep 067 - Filmic v4 https://www.youtube.com/watch?v=qVnuqbR7Z-M

September 23, 2020 12:00 AM

September 14, 2020

ヨイツの賢狼ホロ

如何可能正确的以大部分人可能会接受的方式提出电脑相关的问题

汝问咱为啥标题起得这么长,那是因为咱不敢保证所有的人都乐于接受这种提问方法啊……

以及似乎能衍生到其它领域?那就靠汝自己斟酌了。

本文参考了下面几篇文章的观点,某些文章可能正在经常的被提起。

  • 提问的智慧,原文作者 Eric S. Raymond,以开放源代码运动(不是自由软件运动,这个的领导者是 Richard Stallman)的提出者和主要领导者为人所知的黑客。本指南将教你如何正确的提问以获得你满意的答案。
  • 如何有效的报告 Bug,以程序员的视角阐述如何提交一份足够准确的 Bug 报告
  • X-Y 问题,一种常见的令人疑惑的提问方式,至于为啥令人疑惑嘛……
  • 真的,再这样提问就没人理你了,以提问的智慧的方法提出不一定是电脑相关的问题的方法,大概吧。

提问真的有那么多讲究的地方嘛?

大多数时候如此,因为有一个大前提:

“我们(在很大程度上)是自愿的,从繁忙的生活中抽出时间来解答疑惑,而且时常被提问淹没。所以我们无情的滤掉一些话题,特别是拋弃那些看起来像失败者的家伙,以便更高效的利用时间来回答赢家(winner)的问题。”

大多数人其实都是出于各种志愿目的回答来自不知道何处的提问的,除非……(“我本来想这样拒绝他的,但是他给的钱实在是太多了……”)

所以,你不必在技术上很在行才能吸引我们的注意,但你必须表现出能引导你变得在行的特质 -- 机敏、有想法、善于观察、乐于主动参与解决问题。如果你做不到这些使你与众不同的事情,我们建议你花点钱找家商业公司签个技术支持服务合同,而不是要求黑客个人无偿地帮助你。

于是换做汝自己来回答的话,下面两种问题汝更倾向于回答哪一种?(假设汝有回答这个问题所需的能力的话):

  • 我从 foo 项目找来的源码没法编译。它怎么这么烂?
  • foo 项目代码在 Nulix 6.2 版下无法编译通过。我读过了 FAQ,但里面没有提到跟 Nulix 有关的问题。这是我编译过程的记录,我有什么做的不对的地方吗?

可能不那么显然的,后面的提问者已经指明了环境,也读过了 FAQ,还列出了错误,并且他没有把问题的责任推到别人头上,他的问题值得被关注。

除此之外,对于黑客来说,如果能回答一个有挑战性的问题,或者能激发他们思维的好问题。对他们来说可能就不再是负担,而成了他们的一种乐趣。对黑客而言,"好问题!"是诚挚的大力称赞。(当然放在其它领域里也差不多啦。)

因为时间有限,所以不要活在别人的生活里。也是因为时间有限,于是大家都习惯的去忽略那些不愿思考、或者在发问前不做他们该做的事的人。汝在提问的时候一定不想被这样忽略,对吧?

如果你决定向我们求助,当然你也不希望被视为失败者,更不愿成为失败者中的一员。能立刻得到快速并有效答案的最好方法,就是像赢家那样提问 -- 聪明、自信、有解决问题的思路,只是偶尔在特定的问题上需要获得一点帮助。

在汝遇到问题并决定提出问题之前

于是首先汝在什么地方有了麻烦……是呗?

据完全不可靠的实践发现,大多数的问题都能在这几步方法全运用完之前得到解决……

尝试阅读硬件或软件的说明文档找到答案

硬件设计者和开发者遇到的问题大多数时候不会比汝少,有一部分他们曾经遇到的问题可能的解决方法都被他们写进了自己硬件或软件的说明书或其它地方里。

对于硬件的话,最常见的是说明书或者网站上的教学手册以及常见问题解答一类的。

对于常见的的桌面软件的话,可以在菜单中寻找“帮助”菜单,或者去软件的网站查找说明。

截屏2020-09-07 14.28.56

例如大多数 macOS 的软件的“帮助”菜单里可以找到部分说明。

如果是有点那么不常见的命令行软件的话,比较常用的获得说明的方法有两种:

一种是在后面加上 --help 再运行,会列出像是可以添加的参数一类的信息。

部分程序也有可能是 -h 之类的短参数,或者其它别的(例如 Windows 里的 /?

$ python3 --help
usage: /usr/local/bin/python3 [option] ... [-c cmd | -m mod | file | -] [arg] ...
Options and arguments (and corresponding environment variables):
-b     : issue warnings about str(bytes_instance), str(bytearray_instance)
         and comparing bytes/bytearray with str. (-bb: issue errors)
-B     : don't write .pyc files on import; also PYTHONDONTWRITEBYTECODE=x
-c cmd : program passed in as string (terminates option list)
-d     : debug output from parser; also PYTHONDEBUG=x
-E     : ignore PYTHON* environment variables (such as PYTHONPATH)
-h     : print this help message and exit (also --help)

...

另一种做法是查阅相应的手册页,这里需要用到 man 命令。最简单的用法像这样:

$ man 要查询的命令的名称

这个 man 是男人还是 manual 的缩写呢……

典型的手册页大概像这个样子(还是拿刚才的 python 来举例),这里再加上一些解释?

实际上 man 会调用系统设置的分页程序来打开手册页(比较常见的是 less),至于 less 的用法嘛……大家可以去查阅 less 的手册页,溜了溜了……

PYTHON(1)                                                                        PYTHON(1)


# 程序的名字和一行简介
NAME
       python - an interpreted, interactive, object-oriented programming language

# 这里的 python 是一个命令,于是描述它如何运行,以及需要什么样的命令行参数。
# 如果查阅的是函数的手册页,可以看到函数所需的参数,以及哪个头文件包含该函数的定义。
SYNOPSIS
       python [ -B ] [ -b ] [ -d ] [ -E ] [ -h ] [ -i ] [ -I ]
              [ -m module-name ] [ -q ] [ -O ] [ -OO ] [ -s ] [ -S ] [ -u ]
              [ -v ] [ -V ] [ -W argument ] [ -x ] [ [ -X option ] -?  ]
              [ --check-hash-based-pycs default | always | never ]
              [ -c command | script | - ] [ arguments ]

# 更长的描述
DESCRIPTION
       Python  is  an  interpreted, interactive, object-oriented programming language that
       combines remarkable power with very clear syntax.  For an introduction to  program-
       ming  in  Python,  see the Python Tutorial.  The Python Library Reference documents
       built-in and standard types, constants, functions and modules.  Finally, the Python
       Reference  Manual  describes the syntax and semantics of the core language in (per-
       haps too) much detail.  (These documents may be located via the INTERNET  RESOURCES
       below; they may be installed on your system as well.)

# 可以在命令行下使用的参数以及参数
COMMAND LINE OPTIONS
       -B     Don't write .pyc files on import. See also PYTHONDONTWRITEBYTECODE.

       -b     Issue  warnings  about str(bytes_instance), str(bytearray_instance) and com-
              paring bytes/bytearray with str. (-bb: issue errors)

       -c command
              Specify the command to execute (see  next  section).   This  terminates  the
              option list (following options are passed as arguments to the command).

(下面其实还有很多的为了节省空间就省掉了……)

# 这一部分是 Python 的手册页特别的,介绍了解释器的接口
INTERPRETER INTERFACE
       The  interpreter interface resembles that of the UNIX shell: when called with stan-
       dard input connected to a tty device, it prompts for  commands  and  executes  them
       until an EOF is read; when called with a file name argument or with a file as stan-
       dard input, it reads and executes a script from that file; when called with -c com-
       mand,  it executes the Python statement(s) given as command.  Here command may con-
       tain multiple statements separated by newlines.  Leading whitespace is  significant
       in  Python  statements!  In non-interactive mode, the entire input is parsed before
       it is executed.

# 安装的文件和目录
FILES AND DIRECTORIES
       These are subject to difference depending on local installation conventions; ${pre-
       fix} and ${exec_prefix} are installation-dependent and should be interpreted as for
       GNU software; they may be the same.  The default for both is /usr/local.

# 可以设置的环境变量
ENVIRONMENT VARIABLES
       PYTHONHOME
              Change the location of the  standard  Python  libraries.   By  default,  the
              libraries  are  searched  in  ${prefix}/lib/python<version>  and ${exec_pre-
              fix}/lib/python<version>, where ${prefix} and ${exec_prefix}  are  installa-
              tion-dependent directories, both defaulting to /usr/local.  When $PYTHONHOME
              is set to  a  single  directory,  its  value  replaces  both  ${prefix}  and
              ${exec_prefix}.   To  specify different values for these, set $PYTHONHOME to
              ${prefix}:${exec_prefix}.

# 手册页的作者
AUTHOR
       The Python Software Foundation: https://www.python.org/psf/

# 一些在网上的资源的链接
INTERNET RESOURCES
       Main website:  https://www.python.org/
       Documentation:  https://docs.python.org/
       Developer resources:  https://devguide.python.org/
       Downloads:  https://www.python.org/downloads/
       Module repository:  https://pypi.org/
       Newsgroups:  comp.lang.python, comp.lang.python.announce

# 许可协议信息
LICENSING
       Python is distributed under an Open Source license.  See the file "LICENSE" in  the
       Python  source distribution for information on terms & conditions for accessing and
       otherwise using Python and for a DISCLAIMER OF ALL WARRANTIES.



                                                                                 PYTHON(1)

偶尔汝会在手册页见到 malloc(3) 这样的描述,这表示了特定区块的手册页。在 BSD 和 GNU/Linux 中,手册页通常被分作八个区块:

1 一般命令 2 系统调用 3 库函数,涵盖C标准函数库 4 特殊文件(通常是/dev中的设备)和驱动程序 5 文件格式和约定 6 游戏和屏保 7 杂项 8 系统管理命令和守护进程

于是要查阅特定区块的手册页的话,大概是这个样子(例如刚才的 malloc(3) ):

$ man 3 malloc

尝试上网搜索找到答案

有很多的问题也许不是第一次发生,那大抵汝也不会是第一次遇到的家伙。也许有人留下过类似的解决过程,如果汝找到了而且能成功地解决汝目前的问题,那这世界上就可能少了一个蠢问题(?)

可以搜索的地方也有很多,例如汝正在使用的软件或操作系统发行版的网站(如果汝没在上一步试图寻找的话)、论坛(有可能是官方名义的,或者是社区建立的)和邮件列表的存档,或者某个曾经解决过类似问题的家伙留下的文章等等。

至于搜索引擎要咋用,这个实在是太复杂了,咱也只能给出一些建议:

  • 选对一个好的搜索引擎就差不多成功了一半,嗯。
  • 把程序提示汝的消息作为搜索关键词可能有奇效,例如 Permission denied (publickey). (当然太长的话可能会被搜索引擎剪掉,于是尝试一下找出关键的部分吧。
  • 如果要自己决定关键词的话,首先不要用提问的方式,比如「我的电脑上不了网怎么办」,要寻找问题的线索,将线索变成关键词去搜索,一个关键词找不到就换另一个。啥?汝连这一步都懒得做?那么汝大抵有足够的资金找一个或者一群人帮汝解决汝现在遇到的问题,这就不在讨论范围内了……
  • 介于现在的硬件和软件都大量使用英语,有时也可以试试用英文搜索。
  • ……

尝试问一下汝身边熟悉的朋友?

这个有很大一部分是感性原因,因为某个完全不可靠的证据表明,汝身边最亲近的朋友大抵是最能忍受来自汝自己的看着很蠢的问题的。至少会比一无所知的陌生人容忍度大一点点。

不过朋友毕竟也是人,耐性也是有限的。(汝要是来问咱的话大概耐性会更低)于是为了避免发生像是喋血街头一类的惨剧,还是不负责的建议先把功课做一做了再提问。

天有不测风云?

不过要是汝已经做过了之前的尝试但是问题还是没解决的话……

  • 坐下来放松,然后再来一次(?)。

不要指望几秒钟的 Google 搜索就能解决一个复杂的问题。在向专家求助之前,再阅读一下常见问题文件(FAQ)、放轻松、坐舒服一些,再花点时间思考一下这个问题。相信我们,他们能从你的提问看出你做了多少阅读与思考,如果你是有备而来,将更有可能得到解答。不要将所有问题一股脑拋出,只因你的第一次搜索没有找到答案(或者找到太多答案)

  • 再次思考汝将要提出的问题。有不可靠的统计显示出寻求帮助前汝为解决问题所付出的努力程度和汝获得实质性的帮助的机率很大概率成正相关。

另一方面,表明你愿意在找答案的过程中做点什么是一个非常好的开端。谁能给点提示?我的这个例子里缺了什么?以及我应该检查什么地方请把我需要的确切的过程贴出来更容易得到答复。因为你表现出只要有人能指个正确方向,你就有完成它的能力和决心。

  • 再收集一些更详细的信息。例如 POST 画面的提示,主板七划显示器上的字形,软件弹出的提示,汝在之前做了些什么等等。

https://wiki.archlinux.org/index.php/Bug_reporting_guidelines#Gather_useful_information 上也列出了哪些信息可能是有用的。

  • 然后多想一下汝要在哪里提问。

我要在哪里提问?

在网上比较常见的发问场所,大抵有聊天群组,论坛和邮件列表。不过无论在哪里,下面的建议似乎都非常实用。(但未经过详细的检验)

  • 不要在与主题不合的地方贴出汝的问题。例如在绘画群组里提问游戏技巧,多半没人会搭理。
  • 不要在探讨进阶技术问题的论坛张贴非常初级的问题;反之亦然。至于怎么样算进阶怎么样算初级嘛,这个要自己掂量一下了。
  • 不要在太多的不同地方上重复转贴同样的问题。有很大一部分原因是不同的地方有可能都是同一群人(大雾)。
  • 别像机关枪似的一次"扫射"所有的帮助渠道,这就像大喊大叫一样会使人不快。要一个一个地来。
  • 先特定后通用,例如先去特定发行版的论坛或邮件列表中提问,再到程序本身的论坛或邮件列表提问。
  • 有些软件的网站上可能记载有官方支持和提交 Bug 报告的规则,如果有看到的话,照做就是了。
  • 通过论坛或聊天群组来提供使用者支持服务有增长的趋势,电子邮件则大多为项目开发者间的交流而保留。所以最好先在论坛或聊天群组中寻求与该项目相关的协助。

别问蠢问题!以及……

最常见的蠢问题大概有这么几种,至于为什么这些问题被觉得蠢……

  • 我能在哪找到 X 程序或 X 资源?

就在咱找到它的地方啊,大笨驴 —— 搜索引擎的那一头。天哪!难道还有人不会用 Google 吗?

  • 我怎样用 X 做 Y ?

如果汝想解决的是 Y ,提问时别给出可能并不恰当的方法。这种问题说明提问者不但对 X 完全无知,也对 Y 要解决的问题糊涂,还被特定形势禁锢了思维。最好忽略这种人,等他们把问题搞清楚了再说。

  • 如何设定我的 shell 提示??

如果汝有足够的智慧提这个问题,汝也该有足够的智慧去 RTFM,然后自己去找出来。

  • 我可以用 Bass-o-matic 文件转换工具将 AcmeCorp 档案转换为 TeX 格式吗?

试试看就知道了。如果汝有试过的话,汝既知道了答案,也不用浪费我的时间了。

  • 我的{程序/设定/SQL 语句}不工作

这不算是问题吧,咱有更有意思的事要做呢,对汝这还要再问二十句才能知道问题在哪的问题完全提不起劲。

在看到这类问题的时候,大多数人的反应通常不外如下三种:

你还有什么要补充的吗?| 真糟糕,希望你能搞定。| 这关我屁事?

  • 我的 Windows 电脑有问题,你能帮我吗?

能啊,扔掉微软的辣鸡,换个像 GNU/Linux 或 BSD 的开放源代码的操作系统吧。

注意:如果程序有官方 Windows 版本或者与 Windows 有互动(如 Samba),你可以问与 Windows 相关的问题, 只是别对问题是由 Windows 操作系统而不是程序本身造成的回复感到惊讶, 因为 Windows 对一般开发者来说实在太烂,这种说法通常都是对的。

  • 我的程序不会动了,我认为系统工具 X 有问题

汝完全有可能是第一个注意到被成千上万用户反复使用的系统调用与函数库档案有明显缺陷的家伙,或者汝完全没有根据。

不同凡响的说法需要不同凡响的证据,当汝这样声称的时候,汝大概已经有了清楚而详尽的报告作后盾了吧?

  • 我在安装 Linux(或者 X )时有问题,你能帮我吗?

不能。

咱只有亲自在汝自己的电脑上动手才能找到毛病。要么试试寻求汝附近的 GNU/Linux 使用者群组的实际指导?

注意:如果安装问题与某 Linux 的发行版有关,在它的邮件列表、论坛或本地使用者群组中提问也许是恰当的。此时,应描述问题的准确细节。在此之前,先用 Linux所有被怀疑的硬件作关键词仔细搜索。

  • 问题:我怎么才能破解 root 帐号/窃取 OP 特权/读别人的邮件呢?

想要这样做,说明了汝是个卑鄙小人;想找个黑客帮忙,说明汝是个不折不扣的大笨驴!

以及不要问那种看着就很像家庭作业的问题,这些问题很明显要汝自己找到答案。尽管汝也许需要点提示,但不要指望能从别人那里得到完整答案。

保持一个平和的正确态度

  • 虽然骄傲是不行的,但是也不要走向另一个极端。尽可能清楚地描述背景条件和问题情况,这比低声下气更好地定位了汝自己的位置。
  • 彬彬有礼,多用谢谢您的关注,或谢谢你的关照。让大家都知道汝对他们花时间免费提供帮助心存感激。当然这不是汝可以把报告写的粗心又含糊的借口。

使用有意义且描述明确的标题

在这个领域,卖惨大概是最没有作用的行为之一,宣称紧急极有可能事与愿违:大多数黑客会直接删除无礼和自私地企图即时引起关注的问题。更严重的是,紧急这个字(或是其他企图引起关注的标题)通常会被垃圾信过滤器过滤掉 —— 这样汝所希望能看到汝的问题的人可能永远都看不到汝那看似紧急的问题。

于是来看一下下面的例子:

救命啊!我的笔记本电脑不能正常显示了!

X.org 6.8.1 的鼠标光标会变形,某牌显卡 MV1005 芯片组。

现在用上面的想法思考一下哪一个问题更蠢?再小声的说一句,大家都觉得“目标-差异”格式的标题往往最能吸引黑客,因为这样他们马上就能知道汝的环境和遇到的问题。试试看用更聪明的方法改写上面提问的标题?

用清晰、正确、精准且语法正确的语句仔细组织汝的提问

从经验中大家得出了一个结论,粗心的提问者通常也会粗心的写程序与思考。(真的如此吗,欢迎来证实或证伪)

  • 正确的拼写、标点符号和大小写是很重要的。汝说关注这个很麻烦?那好咱们也觉得思考汝那错词百出的问题很麻烦,于是就不回答了。
  • 如果在非母语的论坛发问,尽管拼写错误之类的会宽容些,但还是不能怠于思考。以及拿不懂回复者使用的语言的话就用英文撰写问题。
  • 清楚的表达汝的问题以及需求。繁忙的人往往厌恶那种漫无边际的空泛提问。

要理解专家们所处的世界,请把专业技能想像为充裕的资源,而回复的时间则是稀缺的资源。你要求他们奉献的时间越少,你越有可能从真正专业而且很忙的专家那里得到解答。

  • 仔细、清楚地描述你的问题或 Bug 的症状。

来看看这句话:“我运行了FooApp,它弹出一个警告窗口,我试着关掉它,它就崩溃了。”这种表述并不清晰,用户究竟关掉了哪个窗口?是警告窗口还是整个FooApp程序?如果这样说,“我运行FooApp程序时弹出一个警告窗口,我试着关闭警告窗口,FooApp崩溃了。”这样虽然罗嗦点,但是很清晰不容易产生误解。

  • 描述问题发生的环境(机器配置、操作系统、应用程序、以及相关的信息),提供发行版和版本号。
  • 描述在提问前汝是怎样去研究和理解这个问题的。以及确定问题而采取的诊断步骤。

简单介绍一下汝来提问之前都做了些什么,我在 Google 中搜过下列句子但没有找到什么有用的东西 之类的反馈也是有用的参考意见。

  • 描述最近做过什么可能相关的硬件或软件变更。

问题发生前的一系列操作,往往就是对找出问题最有帮助的线索。因此,你的说明里应该包含你的操作步骤,以及机器和软件的反应,直到问题发生。在命令行处理的情况下,提供一段操作记录(例如运行脚本工具所生成的),并引用相关的若干行(如 20 行)记录会非常有帮助。

  • 尽可能的提供一个可以重现这个问题的可控环境的方法。

报告bug的最好的方法之一是“演示”给程序员看。让程序员站在电脑前,运行他们的程序,指出程序的错误。让他们看着您启动电脑、运行程序、如何进行操作以及程序对您的输入有何反应。

如果您必须报告bug,而此时程序员又不在您身边,那么您就要想办法让bug重现在他们面前。当他们亲眼看到错误时,就能够进行处理了。

确切地告诉程序员您做了些什么。如果是一个图形界面程序,告诉他们您按了哪个按钮,依照什么顺序按的。如果是一个命令行程序,精确的告诉他们您键入了什么命令。您应该尽可能详细地提供您所键入的命令和程序的反应。

  • 如果汝做完前一步后发现整个问题显得太长(特别是复现的方法和样例太过复杂时),尽量将它剪裁得越小越好。

这样做的用处至少有三点。 第一,表现出你为简化问题付出了努力,这可以使你得到回答的机会增加; 第二,简化问题使你更有可能得到有用的答案; 第三,在精炼你的 bug 报告的过程中,你很可能就自己找到了解决方法或权宜之计。

  • 除非汝非常、非常的有根据(例如有可以证明的测试或者修复的补丁),不要动辄声称找到了 Bug。最好写得像是自己做错了什么。

如果真的有 Bug,你会在回复中看到这点。这样做的话,如果真有 Bug,维护者就会向你道歉,这总比你惹恼别人然后欠别人一个道歉要好一点。

  • 描述目标而不是过程。在一开始就清楚的表示出来汝想做什么,然后再描述问题在哪里。

经常寻求技术帮助的人在心中有个更高层次的目标,而他们在自以为能达到目标的特定道路上被卡住了,然后跑来问该怎么走,但没有意识到这条路本身就有问题。结果要费很大的劲才能搞定。

所谓的 X-Y Problem 大抵如此,提出这种问题就是在一个根本错误的方向上浪费他人大量的时间和精力。这里有一个来自 CoolShell 的例子:

Q)问一下大家,我如何得到一个文件的大小 A1) size = ls -l $file | awk ‘{print $5}’ Q) 哦,要是这个文件名是个目录呢? A2) 用du吧 A3) 不好意思,你到底是要文件的大小还是目录的大小?你到底要干什么? Q) 我想把一个目录下的每个文件的每个块(第一个块有512个字节)拿出来做md5,并且计算他们的大小 …… A1) 哦,你可以使用dd吧。 A2) dd不行吧。 A3) 你用md5来计算这些块的目的是什么?你究竟想干什么啊? Q) 其实,我想写一个网盘,对于小文件就直接传输了,对于大文件我想分块做增量同步。 A2) 用rsync啊,你妹!*

  • 描述症状,而不是汝的猜测。(如果汝的推断如此有效的话,那汝还用向别人求助吗?)

因此要确信你原原本本告诉了他们问题的症状,而不是你的解释和理论;让黑客们来推测和诊断。如果你认为陈述自己的猜测很重要,清楚地说明这只是你的猜测,并描述为什么它们不起作用。

针对诊断者而言,这并不是一种怀疑,而只是一种真实而有用的需求,以便让他们看到的是与你看到的原始证据尽可能一致的东西,而不是你的猜测与归纳的结论。所以,大方的展示给我们看吧!

  • 然后去掉无意义的提问句,例如有人能帮我吗?或者这有答案吗?。除非汝想得到这样的回答:没错,有人能帮你或者不,没答案

使用易于读取且标准的文件格式发送易于回复的问题

没有人喜欢自找麻烦,难以阅读的问题让人没有阅读的欲望,难于回复的问题让人没有回答的热情。

首先不管在哪里提问,绝对,永远不要指望黑客们阅读使用封闭格式编写的文档,像微软公司的 Word 或 Excel 文件等。即便他们能够处理,他们也很厌恶这么做。

如果汝在邮件中提问,下面的建议可以参考:

  • 使用纯文字而不是 HTML (关闭 HTML 并不难)。
  • 使用 MIME 附件通常是可以的,前提是真正有内容(譬如附带的源代码或 patch),而不仅仅是邮件程序生成的模板(譬如只是信件内容的拷贝)。
  • 不要发送一段文字只是一行句子但自动换行后会变成多行的邮件(这使得回复部分内容非常困难)。设想你的读者是在 80 个字符宽的终端机上阅读邮件,最好设置你的换行分割点小于 80 字。
  • 但是,对一些特殊的文件不要设置固定宽度(譬如日志档案拷贝或会话记录)。数据应该原样包含,让回复者有信心他们看到的是和你看到的一样的东西。

如果汝在论坛中提问,下面的建议可以参考:

  • 一两个表情符号和彩色文本通常没有问题,但是不要滥用

花哨的彩色文本倾向于使人认为你是个无能之辈。过滥地使用表情符号、色彩和字体会使你看来像个傻笑的小姑娘。这通常不是个好主意,除非你只是对性而不是对答案感兴趣。

  • 别要求通过电子邮件回复。

    要求通过电子邮件回复是非常无礼的,除非你认为回复的信息可能比较敏感(有人会为了某些未知的原因,只让你而不是整个论坛知道答案)。如果你只是想在有人回复讨论串时得到电子邮件提醒,可以要求网页论坛发送给你。几乎所有论坛都支持诸如追踪此讨论串有回复时发送邮件提醒等功能。

古老和神圣的传统和它的亲戚 - RTFM 和 STFW

有一个古老而神圣的传统:如果你收到RTFM (Read The Fucking Manual)的回应,回答者认为你应该去读他妈的手册。当然,基本上他是对的,你应该去读一读。

RTFM 有一个年轻的亲戚。如果你收到STFW(Search The Fucking Web)的回应,回答者认为你应该到他妈的网上搜索。那人多半也是对的,去搜索一下吧。(更温和一点的说法是 Google 是你的朋友!有时也许是别的样式,例如 LMGTFY 的链接或者“本群已和 Google 达成战略合作”等等。)

通常,用这两句之一回答你的人会给你一份包含你需要内容的手册或者一个网址,而且他们打这些字的时候也正在读着。这些答复意味着回答者认为

  • 你需要的信息非常容易获得
  • 你自己去搜索这些信息比灌给你,能让你学到更多

你不应该因此不爽;依照黑客的标准,他已经表示了对你一定程度的关注,而没有对你的要求视而不见。你应该对他祖母般的慈祥表示感谢。

其实如果汝有努力的在提问前做好功课的话,应该不会收到这样的回复的吧……

我得到一个回复,但这是啥?

看来似乎是 zentry 卡住了;你应该先清除它。

如果汝看不懂回应,别立刻要求对方解释。先像以前试着自己解决问题时那样(利用手册,FAQ,网络,身边的高手),先试着去搞懂他的回应。如果真的需要对方解释,记得表现出汝已经从中学到了点什么。

哦~~~我看过说明了但是只有 -z 和 -p 两个参数中提到了 zentries,而且还都没有清楚的解释如何清除它。你是指这两个中的哪一个吗?还是我看漏了什么?

我没得到答案,怎么办?

如果仍得不到回答,请不要以为我们觉得无法帮助你。有时只是看到你问题的人不知道答案罢了。没有回应不代表你被忽视,虽然不可否认这种差别很难区分。

总的来说,简单的重复张贴问题是个很糟的点子。这将被视为无意义的喧闹。有点耐心,知道你问题答案的人可能生活在不同的时区,可能正在睡觉,也有可能你的问题一开始就没有组织好。

你可以通过其他渠道获得帮助,这些渠道通常更适合初学者的需要。

有许多网上的以及本地的使用者群组,由热情的软件爱好者(即使他们可能从没亲自写过任何软件)组成。通常人们组建这样的团体来互相帮助并帮助新手。

另外,你可以向很多商业公司寻求帮助,不论公司大还是小。别为要付费才能获得帮助而感到沮丧!毕竟,假使你的汽车发动机汽缸密封圈爆掉了 —— 完全可能如此 —— 你还得把它送到修车铺,并且为维修付费。就算软件没花费你一分钱,你也不能强求技术支持总是免费的。

对像是 GNU/Linux 这种大众化的软件,每个开发者至少会对应到上万名使用者。根本不可能由一个人来处理来自上万名使用者的求助电话。要知道,即使你要为这些协助付费,和你所购买的同类软件相比,你所付出的也是微不足道的(通常私有软件的技术支持费用比开放源代码软件的要高得多,且内容也没那么丰富)。

我得到了能解决我的问题的答案,然后呢?

问题解决以后,别忘了感谢拿些帮助过汝的人啦,以及:

  • 写一个补充说明,不用太长。这样做的好处不止可以为汝赢得声誉(以及可能在下次提问时尝到甜头),也可能会帮助到未来遇到相似问题的家伙们。
  • 思考一下怎样才能避免他人将来也遇到类似的问题,思考一下写一份文件或加到常见问题(FAQ)中会不会有帮助。如果是的话就将它们发给文档维护者。

在黑客中,这种良好的后继行动实际上比传统的礼节更为重要,也是你如何透过善待他人而赢得声誉的方式,这是非常有价值的资产。

by Horo at September 14, 2020 09:00 AM

Phoenix Nemo

软件工程实践上的一点思考

曾经大学时对于软件工程这类理论课不屑一顾,认为这些课本都是只在大学里讲学而并不实际参与工程的教授写的东西。但是经过这些年从自己开发程序编写代码,到与公司团队同学、兴趣圈的朋友一起开发项目,也积累、总结了一些经验和教训。正巧昨晚在游戏建设里参与了这类讨论,于是记下一些思考免得忘记。

案例 1

命令方块是 Minecraft 里用于执行游戏命令、实现各种触发性或持续性功能的方块。在游戏地图中需要展示一些浮空的名称标签,便是用命令方块生成隐形盔甲架实现的。这些盔甲架参数复杂且需要在地图里很多特定位置生成,负责的同学便在每个生成的位置下面放了重新生成的命令方块,生成的坐标是相对坐标,因此写好标签的命令方块便可被无限复用。

由于盔甲架属于实体,而实体在 Minecraft 中被认为是不可靠的:有无数种可能这实体会被移动或被清除。
因此我的建议是:将这些命令方块全部放到控制室,坐标写成绝对坐标并加上统一标签,便可做到一键生成全部、一键清除全部。

该同学表示:不想写绝对坐标,因为很麻烦。

案例 2

由于游戏玩法的需要,编写了新的插件。几天后按照原计划应当可以准备第一次基本功能测试时,负责开发的同学表示只写了大约 1/4 的功能。进度很慢的原因是 Minecraft 的实现过于糟糕,而 Spigot 和 Paper 等修改版也没有很好封装 API 导致几乎所有的事件都需要手动处理。

接下来的协同开发中该同学又在反复尝试对配置文件中属性类似的部分使用同一个序列化/反序列化方法处理、对不同配置文件中的不同物品记录项也加上了一层包装来使得其能够被一个序列化/反序列化方法处理、在其他一些程序逻辑上也在尝试复用代码减少冗余度。

我说,你先专心把功能快速叠出来,然后再去想优化的事情。
这位同学表示不能接受,他认为代码应该从编写时就是整洁的。

论点:矫枉过正的代码复用

代码复用是很常见的代码结构优化方式。更少的代码冗余可以减少维护的复杂度,也降低出错的可能。

但是在案例 1 中,如此复用代码(放置同样的命令方块)却实际上造成了更多的冗余:如果要修改一个属性,就需要记录整个世界里每一个命令方块的位置,然后一个个去修改它。相反,由于游戏世界地图里的建筑几乎不可能变化(虽然现实需求很少会有这种条件),统一放在控制室、hard code
所有的坐标作为一个大方法调用,却是在这需求前提下的更好的实现方式。如果需要修改属性,可以只在一个地方修改所有的命令方块。

或者说,重复放置命令方块的过程,就是 copy’n’paste 冗余代码的过程。

而案例 2 则更具有代表性。在项目初期,是否应当关注代码质量?
我认为是应当关注的,但是这基于开发者的工程实践经验。优秀的、熟练的开发者应当在代码编写时就能灵活使用各种简单的优化手段减少初期的代码冗余,但是对于在校大学生没有足够的项目经验时,面对紧凑的项目时间安排应当集中更多精力实现功能。此时过分关注代码优化会被分心导致各种问题——例如这位同学编写的代码基本没有能够一次通过所有测试的情况,而且绝大多数的错误都看起来只是粗心,并不是不理解、写不出的问题。

论点:实现,调整,优化

“Make it work, make it right, make it fast.” – Kent Beck

这是很多软件工程推崇的敏捷开发指导方向。在案例 1 中,该同学只做了第一步——复用同样的、带有相对坐标的命令方块(方法)快速实现了所有的功能。但是从后续维护的角度来讲,这样的实现没有 make it right,更不用提 fast。

而在案例 2 中,这位同学将三个阶段在初期就全部揉进去,但是由于工程经验不足,在思考优化方案时花费了过多的精力,也导致了代码精度不够,反复修改也无法顺利通过测试。

从个人经验来看,前期的代码编写应注重功能实现,并在编码能力基础上直接编写清晰的代码结构。功能实现后,再根据需求和测试中的问题「重构」打磨细节、尝试更好的实现方式。这个过程不仅在完善整个程序,对自己的系统架构把握和设计经验也有很大的提升。最后一个阶段,则是针对性的优化少量代码使整个系统更加稳定、高效。

论点:架构的改动

这是一个比较小型的项目。需求和基本功能架构从一开始便已经讨论清楚。后续的调整不大,但是每当有少量的需求修改或架构微调,都导致了很大的代码变动。而按照这位同学的思路,每次改动都要重新思考代码结构,这浪费很多的时间。

从实际工程角度,需求变化并带来架构的微调甚至大改动都可以说是很常见的事情。在前期编码实现阶段如果揉入过多对于代码结构的过多考量,每次改动都可能会使这些思考的时间被浪费。因此,在前期编码时不应为架构考虑消耗过多的时间,而在重构过程中,由于已经完成基本的功能实现,且对已有代码还处在熟悉的热度,可以快速适配需要修改、调整的架构,并基于前期编码时的各种尝试和实验的结论选择最佳的实现方式。

以上是基于近期项目中的讨论,在软件工程层面上的思考。如有缺漏不当之处,欢迎指正。

曾经大学时对于软件工程这类理论课不屑一顾,认为这些课本都是只在大学里讲学而并不实际参与工程的教授写的东西。但是经过这些年从自己开发程序编写代码,到与公司团队同学、兴趣圈的朋友一起开发项目,也积累、总结了一些经验和教训。正巧昨晚在游戏建设里参与了这类讨论,于是记下一些思考免得忘记。

September 14, 2020 04:20 AM

WireGuard 真香

真是老了跟不上时代了,这么好的东西为什么我现在才开始用??

其实这东西刚出来就在关注了不过确实前段时间才有机会尝试折腾一下。优点很多,也有无数人写过文章介绍,所以就不再多废话。主要看中它的 PtP 特性(服务器之间)和支持漫游(服务器-客户端)。当然目前在梯子方面的表现,即便是优秀的隧道方案,但由于折腾的人多了,面对万里城墙,这谁顶得住哇。

所以本文只讨论 WireGuard 作为访问企业网的隧道方案,算是初步折腾的笔记。

服务器配置

一个基本的 PtP 配置结构 /etc/wireguard/wg0.conf

1
2
3
4
5
6
7
8
9
[Interface]
Address = 10.0.0.1/32
PrivateKey = [CLIENT PRIVATE KEY]

[Peer]
PublicKey = [SERVER PUBLICKEY]
AllowedIPs = 10.0.0.0/24, 10.123.45.0/24, 1234:4567:89ab::/48
Endpoint = [SERVER ENDPOINT]:48574
PersistentKeepalive = 25

生成私钥

1
2
wg genkey > privatekey
chmod 600 privatekey

基于私钥生成本机的公钥

1
wg pubkey < privatekey > publickey

或者一步完成的操作

1
wg genkey | tee privatekey | wg pubkey > publickey

额外生成预共享密钥来进一步增强安全性

1
wg genpsk > preshared

这样服务器之间的互联配置就基本完成了。使用 wg-quick up <config> 来快速启动 WireGuard。

如果要配合客户端使用,则需要配置 NAT。顺便如果客户端没有 IPv6,也可以通过此法来给客户端提供 IPv6 Enablement。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Interface]
Address = 10.200.200.1/24
Address = fd42:42:42::1/64
SaveConfig = true
ListenPort = 51820
PrivateKey = [SERVER PRIVATE KEY]

# note - substitute eth0 in the following lines to match the Internet-facing interface
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

[Peer]
# client foo
PublicKey = [FOO's PUBLIC KEY]
PresharedKey = [PRE-SHARED KEY]
AllowedIPs = 10.200.200.2/32, fd42:42:42::2/128

[Peer]
# client bar
PublicKey = [BAR's PUBLIC KEY]
AllowedIPs = 10.200.200.3/32, fd42:42:42::3/128

在此例中需注意 Allowed IPs 不可 overlap 否则会造成包转发错误。

客户端

与上文中服务器配置相照应的客户端配置示例如下:

1
2
3
4
5
6
7
8
9
10
11
[Interface]
Address = 10.200.200.2/24
Address = fd42:42:42::2/64
PrivateKey = [FOO's PRIVATE KEY]
DNS = 1.1.1.1

[Peer]
PublicKey = [SERVER PUBLICKEY]
PresharedKey = [PRE-SHARED KEY]
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = [SERVER PUBLIC IP ADDRESS]:51820

客户端的 AllowedIPs 如果使用 catch-all 0.0.0.0/0, ::/0 也就会默认转发所有的流量到服务器。该选项实际作用是路由表,控制哪些流量需要经由服务器转发。

配置完毕即可使用 wg-quick up <config> 启动 WireGuard。如果一切顺利,通过路由追踪应该可以看到流量已经交由服务器转发。

总结

由于工作需要,经常合上笔记本动身前往其他地方。在接入传统企业网例如 L2TP/IPSec 甚至 AnyConnect 都无法保证设备下次进入工作状态时可以立即恢复连接。而 WireGuard 在不同网络、不同地域、不同网络中断时间等各种情况下均可在下次进入网络覆盖时立即恢复连接,再也不必担心网络中断恢复时手忙脚乱配置隧道或者不小心泄密啦。

目前唯一的不足,大概就是还没有 Windows 客户端,没有办法推广到非技术部门(虽然影响不到我…

总之,真香.jpg

Reference:

[1] https://wiki.archlinux.org/index.php/WireGuard

真是老了跟不上时代了,这么好的东西为什么我现在才开始用??

September 14, 2020 04:20 AM

制作 Arch Linux 内存系统启动盘

之前尝试过 Arch Linux in RAM 完全运行在内存中的轻量业务系统,最近在维护一些物理服务器看到没有安装系统的服务器不断重启,想到了可以制作类似的内存系统启动盘,以高效完成系统测试、安装、远程维护等任务。

这时候就要祭出 mkarchiso 大法了。这是自动化制作最新版 Arch Live 镜像的工具集,当然也可用于制作定制化的 Arch 镜像。

准备

首先安装 archiso

1
~> sudo pacman -Syy archiso

它提供了两种配置方案,一种是只包含基本系统的 baseline,一种是可以制作定制 ISO 的 releng。要制作维护用 ISO,当然是复制 releng 配置啦。

1
2
~> cp -r /usr/share/archiso/configs/releng/ archlive
~> cd archlive

定制

整个过程不要太简单。先来了解下各个文件的用途:

  • build.sh - 用于制作镜像的自动化脚本,可以在这里修改一些名称变量或制作过程的逻辑。
  • packages.x86_64 - 一份要安装的包列表,一行一个。
  • pacman.conf - pacman 的配置文件,不用多说了吧。
  • airootfs - Live 系统的 rootfs,除了安装的包之外,其他的定制(以及启动执行脚本等)都在这里。遵循 rootfs 的目录规则。
  • efiboot / syslinux / isolinux 用于设置 BIOS / EFI 启动的配置。

[archlinuxcn] 仓库加入 pacman.conf

1
2
[archlinuxcn]
Server = https://cdn.repo.archlinuxcn.org/$arch

然后修改 packages.x86_64,加入 archlinuxcn-keyring 和其他需要预安装的包:

1
2
3
4
5
archlinuxcn-keyring
htop
iftop
iotop
ipmitool

按需修改即可啦。

要启动为内存系统,需要加启动参数 copytoram

修改文件 syslinux/archiso_pxe.cfgsyslinux/archiso_sys.cfg 文件,在启动参数后加 copytoram,像这样:

1
2
3
4
5
6
7
8
9
10
11
INCLUDE boot/syslinux/archiso_head.cfg

LABEL arch64
TEXT HELP
Boot the Arch Linux (x86_64) live medium.
It allows you to install Arch Linux or perform system maintenance.
ENDTEXT
MENU LABEL Boot Arch Linux (x86_64)
LINUX boot/x86_64/vmlinuz
INITRD boot/intel_ucode.img,boot/amd_ucode.img,boot/x86_64/archiso.img
APPEND archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram

启动时即可将整个 SquashFS 文件复制到内存。如果内存比较小,也可以指定 copytoram_size 来限制 tmpfs 占用内存的最大数量。

同样,也需要修改 efiboot/loader/entries/archiso-x86_64-usb.conf 的启动参数。在 options 行添加

1
options archisobasedir=%INSTALL_DIR% archisolabel=%ARCHISO_LABEL% copytoram

制作

创建工作目录和输出目录

1
mkdir -p work out

最后一步,只需要以 root 权限执行 ./build.sh 就可以啦。

要看具体执行过程的话,加 -v-h 看所有参数。

完成后,即可在 out 目录得到准备好的 ISO 文件。将其 dd 到 USB 闪存盘,大功告成(‘・ω・’)

Ref:

  1. https://wiki.archlinux.org/index.php/Archiso
  2. https://git.archlinux.org/archiso.git/tree/docs/README.bootparams#n53

之前尝试过 Arch Linux in RAM 完全运行在内存中的轻量业务系统,最近在维护一些物理服务器看到没有安装系统的服务器不断重启,想到了可以制作类似的内存系统启动盘,以高效完成系统测试、安装、远程维护等任务。

September 14, 2020 04:20 AM

使用 fs.WriteStream 编写超简单的日志流

虽然 console.log 很好用,但是生产环境需要保存日志的时候就比较蛋疼。暴力 fs.appendFile 会消耗大量的 file handler,因此用 writable stream 来复用 file handler 是更好的选择。

大概是个不能再简单的思路了。先创建一个写入流

1
2
3
const fs = require('fs');

let logStream = fs.createWriteStream('./test.log');

这样便创建了一个文件写入口,需要时直接调用 logStream.write 即可写入数据。
接下来编写一个用于记录日志的函数替代 console.log

1
2
3
function logger (message) {
logStream.write(message);
}

至此基本功能就写完啦。但是太简陋了对不对,还是要再加点装饰。

重写 logger 函数,区分 stdoutstderr

1
2
3
4
5
6
7
8
9
10
11
12
let logInfo = fs.createWriteStream('./stdout.log');
let logError = fs.createWriteStream('./stderr.log');

let Logger = {};

Logger.info = (message) => {
logInfo.write('[INFO] ' + message);
}

Logger.error = (message) => {
logError.write('[ERROR] ' + message);
}

感觉还是少了点什么…日期?

1
2
3
Logger.info = (message) => {
logInfo.write(new Date().toISOString() + ' [INFO] ' + message + '\n');
}

嗯嗯。这就像样了。把代码整合起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fs = require('fs');

let logInfo = fs.createWriteStream('./stdout.log');
let logError = fs.createWriteStream('./stderr.log');

let Logger = {};

Logger.info = (message) => {
logInfo.write(new Date().toISOString() + ' [INFO] ' + message + '\n');
}
Logger.error = (message) => {
logError.write(new Date().toISOString() + ' [ERROR] ' + message + '\n');
}

module.exports = Logger;

需要用时

1
Logger.info('This is an information.');

现在看对应的 stdout.log 文件就有相应内容啦。

1
2
~> tail -f stdout.log
2018-11-18T10:52:57.333Z [INFO] This is an information.

不够刺激?

1
2
3
[...Array(10000)].forEach((item, index) => {
Logger.info('Hello! ' + index);
});
1
2
3
4
5
6
7
8
9
10
11
12
~> tail -f stdout.log
...
2018-11-18T10:58:30.661Z [INFO] Hello! 9990
2018-11-18T10:58:30.661Z [INFO] Hello! 9991
2018-11-18T10:58:30.661Z [INFO] Hello! 9992
2018-11-18T10:58:30.661Z [INFO] Hello! 9993
2018-11-18T10:58:30.661Z [INFO] Hello! 9994
2018-11-18T10:58:30.661Z [INFO] Hello! 9995
2018-11-18T10:58:30.661Z [INFO] Hello! 9996
2018-11-18T10:58:30.661Z [INFO] Hello! 9997
2018-11-18T10:58:30.661Z [INFO] Hello! 9998
2018-11-18T10:58:30.661Z [INFO] Hello! 9999

搞定(┌・ω・)┌超简单的吧。

虽然 console.log 很好用,但是生产环境需要保存日志的时候就比较蛋疼。暴力 fs.appendFile 会消耗大量的 file handler,因此用 writable stream 来复用 file handler 是更好的选择。

September 14, 2020 04:20 AM

重构 StickerSetBot

关注 Telegraf 有一段时间了。特别是最近 Telegram 上 spammer 猖獗导致 Telegram 对于用户行为限制越来越严格,由此想过写一个简单的 bot 来处理加群请求之类的。

总之原因都是没时间。终于搞定一些事情之后发现之前瞎写的 Telegram 导出贴图 bot 居然备受欢迎…正好 Telegram Bot API 也更新了,来重构吧!

拆分逻辑代码

最头疼的事情首先是当时写这 bot 的时候只顾着考虑各种情况,逻辑像流水一样全部写成一坨。虽然实际不复杂吧但这不是 best practice。于是把每个功能单独拆出来先。

on('command') 的逻辑代码整块移出来作为 handler,然后能够原子化的功能再单独拆分成函数调用。目前的效果虽然还是有不少逻辑层在 handler 里,但是基本达到了比较方便维护的目的。

handler 本来就是拿来写逻辑的啊摔

接下来再清理冗余代码和各种 hard code,加了两个方法让代码看起来更整洁一些。于是就先这样。

迁移框架

好在 Telegraf 和之前用的框架在参数上很多兼容,所以这没有花太多时间。顺便尝试采用了一部分 ES6 的风格,嘛…果然不喜欢。

所以就不要吐槽为什么 ES5 和 ES6 的风格混写了。

之前要一大长串的传参现在只要一个 context 了好方便啊。中间件也好方便啊~

以上。

调试:无尽的 bugfix

并不指望一通大换血之后的代码能一次跑起来…但是没跑起来的原因是我传错了中间件值这不能忍!!为什么一会儿传的是函数本体一会儿传的是函数调用啊摔!!

而且这问题还让我调了两个小时!!!

调通了之后就很舒服了

遇到的坑还有 context 本身不能当 session 用,然而不想再引入 session 中间件于是自己写了个超简陋的内存 session。就是为了多语言支持。因为一觉醒来发现这 bot 语言莫名其妙变中文了(messages 成了全局变量 = =

当然还有 Telegram 自己的坑,比如什么贴纸就是死下载不能然后整个程序就 hang 着了。

一键导出贴纸包

终于!Telegram bot API 添加了 StickerSet 类型。只要有贴纸包名称,就可以获取整个贴纸包的信息。考虑不改变用户习惯的情况下(你哪有什么用户啊可恶)对本身处理贴纸和其他消息的函数做了修改,顺便又拆了俩函数出来(怎么代码越来越多了啊喂!

最后结果就是没有一屏看不到头的函数啦~(你快够

以及加入了用贴纸包链接导出一整组贴纸的功能,算是真正意义上的 StickerSetBot 了。

然后贴纸过多卡死了 Telegram 的 ratelimiting

直接导出单张贴纸

既然功能拆分了那也就方便加更多别的功能啦。比如不新建任务,直接甩过去一张贴纸来获得 PNG 文件~

这只 bot 在这里,源码在这里。欢迎各种玩坏~(记得去发 issue

就酱(,,•﹏•,,)

关注 Telegraf 有一段时间了。特别是最近 Telegram 上 spammer 猖獗导致 Telegram 对于用户行为限制越来越严格,由此想过写一个简单的 bot 来处理加群请求之类的。

总之原因都是没时间。终于搞定一些事情之后发现之前瞎写的 Telegram 导出贴图 bot 居然备受欢迎…正好 Telegram Bot API 也更新了,来重构吧!

September 14, 2020 04:20 AM

Office Service Router 解决方案:Arch Linux in RAM

一直把自己在办公室的 PC 保持开机用于连回办公区、存取数据工作需求。由于最近办公室所在的写字楼要全馆断电检点,所以诞生了构建一个 Service Router 的想法。

思路

运行在内存里对于 Linux 系统来说是完全可能(而且简单)的事情。

最直接的想法就是使用内核 hook 在启动时复制根分区到内存盘然后挂载内存里的数据作为根分区即可。

设备的话,设置 Power on AC 即可通电自启动。

ramroot

作为一只懒卷,这种简单的事情当然先顺手搜索下啦。然后就发现了几乎完美的解决方案——ramroot

ramroot 通过加入内核 hook 然后自动在内存建立 zram 分区,同步根分区数据再启动。还可以在启动时选择是否启动进内存,正好解决了所有的需求。

实现

硬件选择是一台便宜的 Intel NUC,安装两根 4GB LPDDR3 低压内存和一块 120G 2.5 SSD。虽然说起来其实并不需要 SSD(因为数据全部都在内存里,速度比 SSD 更快)但是毕竟日本多震,还是为数据安全着想。毕竟硬盘坏了的话内存系统也无法启动了。

当然如果有集成 32GB eMMC 的小型 PC 的话也是好的选择。

正常安装完 Arch Linux 系统,安装 openssh 和各种必要的服务程序,修改配置文件,然后安装 ramroot 并执行

1
# ramroot enable

此时先别急着重启,先把不需要的包、缓存等文件(/var/cache)删除,保持最小化的根分区。然后再重启。便可看到加载内核 hook 时的提示是否进入内存系统,默认超时后就会自动复制根分区到内存啦。

由于整个系统是运行在内存中的,所以完全没有等待读盘的时间。整个系统的响应速度非常快。限制是内存不够大的话运行一些业务会比较捉襟见肘,而且这样低功耗、低发热的 SoC 处理性能也只能运行一些轻型任务。

下面是一些 IO 性能测试

1
2
3
4
5
6
7
8
9
10
11
12
# ioping -s 1G /
1 GiB <<< . (ext4 /dev/zram0): request=1 time=1.04 s (warmup)
1 GiB <<< . (ext4 /dev/zram0): request=2 time=1.04 s
1 GiB <<< . (ext4 /dev/zram0): request=3 time=1.04 s
1 GiB <<< . (ext4 /dev/zram0): request=4 time=1.04 s
1 GiB <<< . (ext4 /dev/zram0): request=5 time=1.04 s
1 GiB <<< . (ext4 /dev/zram0): request=6 time=1.04 s ^C

--- / (ext4 /dev/zram0) ioping statistics ---
5 requests completed in 5.18 s, 5 GiB read, 0 iops, 988.4 MiB/s
generated 6 requests in 7.20 s, 6 GiB, 0 iops, 853.8 MiB/s
min/avg/max/mdev = 1.04 s / 1.04 s / 1.04 s / 550.7 us
1
2
3
4
5
6
# ioping -RD /

--- / (ext4 /dev/zram0) ioping statistics ---
530.0 k requests completed in 2.49 s, 2.02 GiB read, 212.9 k iops, 831.7 MiB/s
generated 530.0 k requests in 3.00 s, 2.02 GiB, 176.7 k iops, 690.1 MiB/s
min/avg/max/mdev = 3.44 us / 4.70 us / 69.0 us / 1.39 us

可以看到系统根分区在 zram 里,经过压缩因此 IO 带宽受到了 CPU 处理性能的限制。但是 IOPS 依然高得爆表,对比一下 Intel Optane 900P 的 IOPS 性能:

1
2
3
4
# ioping -RD /
--- / (ext4 /dev/nvme0n1p1) ioping statistics ---
163.1 k requests completed in 3.00 s, 56.5 k iops, 220.8 MiB/s
min/avg/max/mdev = 11 us / 17 us / 114 us / 4 us

炒鸡厉害对不对!

不过需要做永久性修改的话还是要下面的方法之一

  • 重新挂载磁盘(虽然并不麻烦)然后手动修改配置文件
  • 重新挂载磁盘然后 rsync zram 到磁盘(方便但是可能会多一些不必要的东西)
  • 重启进入磁盘系统然后运行修改(需要物理接触)

硬件设置

进入系统 BIOS 设置,开启 Power on AC 或设置 Power Failure 后的操作,选择为 Power On (默认一般是 Last State)。

关闭系统、拔出电源,或意外断电后,再接入电源即可自动开机引导系统。因为数据本身就只在内存中,除了运行中的临时更改会丢失,系统和硬盘本体都是安然无恙的。

再也不担心办公室断电检查啦。

大概就是这样。

一直把自己在办公室的 PC 保持开机用于连回办公区、存取数据工作需求。由于最近办公室所在的写字楼要全馆断电检点,所以诞生了构建一个 Service Router 的想法。

September 14, 2020 04:20 AM

通过 SSH 修正安装有 GPU 的 HPE Proliant 服务器

由于越来越多的渲染、压制等需求,托供货商的关系搞来一台带有独立显卡的 HPE 服务器。经过几番折腾(包括特别奇怪的 LS26-C14 电源线)麻烦了帮忙托管的数据中心的大兄弟好几回,终于算是上架可以开机了。

登入 iLO,安装许可证,启动 iLO Remote Console,打开电源,一切都很顺利。但是 Console 里显示 Early Initialization… 完成后,突然画面一黑,完全没了动静。

以为 iLO 出了 bug,冷重启好几次都是一样的结果。百思不得其解。

再重启一次。仔细观察了一番发现虽然没了画面,但是 POST Code 还是不断变化的,而且 Virtual Media 指示灯不断在闪烁,说明系统仍在正常运行,只是没有视频输出而已。

因此问题定位在视频输出而非系统硬件。既然这台服务器装了显卡,那么很可能是 PCI-e 初始化后视频输出全部交给显卡处理了。搜索了一下 HPE Community,确实有这样的情况存在。解决方案是通过 BIOS 修改显卡设置为默认集成显卡、备选独立显卡。

尝试在设备初始化阶段进入 BIOS,失败。

联系数据中心远程操作的话,可能要等一段时间。

纠结时随便点开 iLO 的管理页面,突然发现了华点:这货居然支持 SSH。

对啦,HPE 的底层系统几乎都是魔改版 Linux,连他们的 SmartArray 都是 Linux 启动一个 Firefox 浏览器来操作的(X

于是正好在网上搜到一篇通过 SSH 修改 BIOS 视频设置的方法。记录如下。

SSH 进入 iLO

确保 SSH 在 iLO 管理页面中已开启,然后使用 SSH 客户端正常连接:

1
ssh Administrator@10.6.254.121

(ssh 用户名是 Administrator 感觉各种违和)

连接到 Virtual Serial Port

命令很简单:vsp

在 iLO 管理页面重启系统,然后等待初始化完成。如果看到按下 F9 进入 BIOS 设置的提示,不要按下它否则会进入 GUI 模式(于是又去独立显卡了就。

看到 ESC + 9 进入 BIOS Setup Utility 时按下键组合,稍等一会儿应该就可以看到提示符 rbsu>

修改视频设置

命令 SHOW CONFIG VIDEO OPTIONS

显示如下

1
2
3
1|Optional Video Primary, Embedded Video Disabled <=
2|Optional Video Primary, Embedded Video Secondary
3|Embedded Video Primary, Optional Video Secondary

即默认关闭了集成显卡,只用独立显卡(不觉得很蠢吗!

于是修改为第三项,默认使用集成显卡,独立显卡作为备用。

1
SET CONFIG VIDEO OPTIONS 3
1
2
3
1|Optional Video Primary, Embedded Video Disabled
2|Optional Video Primary, Embedded Video Secondary
3|Embedded Video Primary, Optional Video Secondary <=

然后敲 EXIT 退出并重启系统。

安装系统和驱动

至此即可通过 iLO Advanced Console 正常安装操作系统。不过需要注意的是进入操作系统后即便安装了对应的显卡驱动,依然默认使用的是集成显卡。以及 RDP 只能使用软解,无法使用独立显卡加速视频输出。这不影响 Blender 或者 Cinema 4D 等直接操作显卡进行计算的程序,但是会影响直接输出视频到桌面的程序。通过 Teamviewer 则可以强制桌面运行在独立显卡上。

顺便吐槽:Blender 把我的工程材质弄丢了…

由于越来越多的渲染、压制等需求,托供货商的关系搞来一台带有独立显卡的 HPE 服务器。经过几番折腾(包括特别奇怪的 LS26-C14 电源线)麻烦了帮忙托管的数据中心的大兄弟好几回,终于算是上架可以开机了。

登入 iLO,安装许可证,启动 iLO Remote Console,打开电源,一切都很顺利。但是 Console 里显示 Early Initialization… 完成后,突然画面一黑,完全没了动静。

September 14, 2020 04:20 AM

在线扩展 LVM root 分区

才不是没东西写了呢

遇到一个奇葩的原因导致 root 分区被占满的。而且还是奇葩的 CentOS,root 分区是 LVM,Hypervisor 里扩展磁盘后无法直接用 resize2fs。

既然如此就只能暴力重建分区咯。

重建分区

操作前确保操作的分区和之后新建时 Start 保持一致,修改分区表后不至于分区崩坏。

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
~> fidks /dev/sda
Welcome to fdisk (util-linux 2.23.2).

Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.


Command (m for help): p

Disk /dev/sda: 103.1 GB, 103079215104 bytes, 201326592 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x000a8e23

Device Boot Start End Blocks Id System
/dev/sda1 * 2048 2099199 1048576 83 Linux
/dev/sda2 2099200 50331647 24116224 8e Linux LVM

Command (m for help): d
Partition number (1,2, default 2): 2
Partition 2 is deleted

Command (m for help): n
Partition type:
p primary (1 primary, 0 extended, 3 free)
e extended
Select (default p):
Using default response p
Partition number (2-4, default 2):
First sector (2099200-201326591, default 2099200):
Using default value 2099200
Last sector, +sectors or +size{K,M,G} (2099200-201326591, default 201326591):
Using default value 201326591
Partition 2 of type Linux and of size 95 GiB is set

Command (m for help): w
The partition table has been altered!

Calling ioctl() to re-read partition table.

WARNING: Re-reading the partition table failed with error 16: Device or resource busy.
The kernel still uses the old table. The new table will be used at
the next reboot or after you run partprobe(8) or kpartx(8)
Syncing disks.

~> partprobe

现在就可以看到 /dev/sda2 的大小已经变化了:

1
2
3
4
5
6
7
8
~> lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 96G 0 disk
├─sda1 8:1 0 1G 0 part /boot
└─sda2 8:2 0 95G 0 part
├─centos-root 253:0 0 20.6G 0 lvm /
└─centos-swap 253:1 0 2.4G 0 lvm [SWAP]
sr0 11:0 1 906M 0 rom

扩展 Volume Group

VG 的好处也就是能够灵活扩展分区大小…

1
2
3
~> pvresize /dev/sda2
Physical volume "/dev/sda2" changed
1 physical volume(s) resized / 0 physical volume(s) not resized
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
~> vgdisplay
--- Volume group ---
VG Name centos
System ID
Format lvm2
Metadata Areas 1
Metadata Sequence No 4
VG Access read/write
VG Status resizable
MAX LV 0
Cur LV 2
Open LV 2
Max PV 0
Cur PV 1
Act PV 1
VG Size <95.00 GiB
PE Size 4.00 MiB
Total PE 24319
Alloc PE / Size 5887 / <23.00 GiB
Free PE / Size 18432 / 72.00 GiB
VG UUID TpbtuH-AjTZ-PU3v-UN31-FvfX-kSLv-xLiJG7

至此已经可以看到 Free PE 的部分有多出的 72GB 空间。

扩展 Logic Volume

1
2
3
4
5
6
7
8
9
10
11
12
13
~> lvextend -r -l +100%FREE /dev/centos/root
Size of logical volume centos/root changed from 20.59 GiB (5272 extents) to 92.59 GiB (23704 extents).
Logical volume centos/root successfully resized.
meta-data=/dev/mapper/centos-root isize=512 agcount=4, agsize=1349632 blks
= sectsz=512 attr=2, projid32bit=1
= crc=1 finobt=0 spinodes=0
data = bsize=4096 blocks=5398528, imaxpct=25
= sunit=0 swidth=0 blks
naming =version 2 bsize=4096 ascii-ci=0 ftype=1
log =internal bsize=4096 blocks=2636, version=2
= sectsz=512 sunit=0 blks, lazy-count=1
realtime =none extsz=4096 blocks=0, rtextents=0
data blocks changed from 5398528 to 24272896

确认效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
~> lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 96G 0 disk
├─sda1 8:1 0 1G 0 part /boot
└─sda2 8:2 0 95G 0 part
├─centos-root 253:0 0 92.6G 0 lvm /
└─centos-swap 253:1 0 2.4G 0 lvm [SWAP]
sr0 11:0 1 906M 0 rom

~> df -h
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/centos-root 93G 21G 73G 23% /
devtmpfs 3.9G 0 3.9G 0% /dev
tmpfs 3.9G 8.0K 3.9G 1% /dev/shm
tmpfs 3.9G 8.6M 3.9G 1% /run
tmpfs 3.9G 0 3.9G 0% /sys/fs/cgroup
/dev/sda1 1014M 185M 830M 19% /boot
tmpfs 783M 0 783M 0% /run/user/0

搞定收工(‘・ω・’)

后话

其实虚拟机还用 LVM 的话,直接新增一块虚拟硬盘是最方便的方案。直接 vgextend 一路搞定…

才不是没东西写了呢

遇到一个奇葩的原因导致 root 分区被占满的。而且还是奇葩的 CentOS,root 分区是 LVM,Hypervisor 里扩展磁盘后无法直接用 resize2fs。

September 14, 2020 04:20 AM

在 Linux 服务器配置 LACP 与 VLAN

存储服务器不想放在 OVH 了。所以自己来托管一台机器,顺便折腾下 2x1Gbps 组 LACP Bonding。

前提:服务器需要至少 2 个千兆物理网卡,上联交换机支持 802.3ad。

配置交换机

这里使用的是 Cisco Nexus 3064PQ-10GE 交换机,我们的接口在 Eth1/21-22,port-channel 的配置如下:

1
2
3
4
5
6
7
8
9
# show interface trunk

--------------------------------------------------------------------------------
Port Native Status Port
Vlan Channel
--------------------------------------------------------------------------------
Eth1/21 1 trnk-bndl Po100
Eth1/22 1 trnk-bndl Po100
Po100 1 trunking --
1
2
3
4
5
6
7
8
9
10
show port-channel database
port-channel100
Last membership update is successful
2 ports in total, 2 ports up
First operational port is Ethernet1/21
Age of the port-channel is 0d:00h:02m:16s
Time since last bundle is 0d:00h:02m:04s
Last bundled member is Ethernet1/22
Ports: Ethernet1/21 [active ] [up] *
Ethernet1/22 [active ] [up]

配置服务器

服务器操作系统是 Arch Linux,由于蜜汁问题 netctl 无法启动网卡,就只好用 systemd-networkd 啦。

麻烦一些,但是也还算顺利。与往常一样,折腾服务器网络的时候需要备着 IPMI 以防 connection lost。

内核模块

需要加载 bonding 模块。将模块名写入列表,文件 /etc/modules-load.d/bonding.conf,内容只需要一行:

1
bonding

先别急着加载模块,为了防止模块自动建立一个默认网卡影响后续配置,以及设置 LACP Mode=4 … 等等,先加入一行参数。文件 /etc/modprobe.d/bonding.conf

1
options bonding mode=4 miimon=100 max_bonds=0

然后安装 ifenslave 包,再 modprobe bonding 即可。

bonding 虚拟网卡

首先创建一个虚拟网卡的设备。文件 /etc/systemd/network/bond0.netdev 内容为

1
2
3
4
5
6
7
8
9
[NetDev]
Name=bond0
Kind=bond

[Bond]
Mode=802.3ad
TransmitHashPolicy=layer2+3
LACPTransmitRate=fast
AdSelect=bandwidth

然后在此虚拟网卡上创建网络。这里使用两个物理网卡 eth0eth1 作为 bundle,交换机上的 VLAN id 是 113。文件 /etc/systemd/network/bond0.network 内容为

1
2
3
4
5
6
[Match]
Name=bond0

[Network]
VLAN=vlan113
BindCarrier=eth0 eth1

接下来分别为 eth0eth1 建立网络设置。

  • /etc/systemd/network/eth0.network
1
2
3
4
5
[Match]
Name=eth0

[Network]
Bond=bond0
  • /etc/systemd/network/eth1.network
1
2
3
4
5
[Match]
Name=eth1

[Network]
Bond=bond0

最后是 VLAN 的设置。前面设置了上联 VLAN id 是 113,这里分别建立 VLAN 的虚拟网卡(based on bond0) 并设置网络(IP, etc)。

  • /etc/systemd/network/vlan113.netdev
1
2
3
4
5
6
[NetDev]
Name=vlan113
Kind=vlan

[VLAN]
Id=113
  • /etc/systemd/network/vlan113.network
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Match]
Name=vlan113

[Network]
VLAN=vlan113

[Address]
Address=10.1.0.100/24

[Route]
Destination=0.0.0.0/0
Gateway=10.1.0.1
DNS=1.1.1.1

[Address]
Address=2600:x:x:x::2/64

[Route]
Gateway=2600:x:x:x::1

多个地址、IPv6 等可以写多个 [Address][Route]

至此就完成啦。开启 systemd-networkd 的自启动:

1
systemctl enable systemd-networkd.service

然后重启网络:

1
systemctl restart systemd-networkd.service

如果配置都没有问题,网络会中断十几秒然后恢复。现在查看网卡列表已经可以看到组合的网卡了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ip l
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP mode DEFAULT group default qlen 1000
link/ether <REDACTED> brd ff:ff:ff:ff:ff:ff
3: eth1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc mq master bond0 state UP mode DEFAULT group default qlen 1000
link/ether <REDACTED> brd ff:ff:ff:ff:ff:ff
4: eth2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether <REDACTED> brd ff:ff:ff:ff:ff:ff
5: eno1: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
link/ether <REDACTED> brd ff:ff:ff:ff:ff:ff
6: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether <REDACTED> brd ff:ff:ff:ff:ff:ff
7: vlan113@bond0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
link/ether <REDACTED> brd ff:ff:ff:ff:ff:ff

ethtool 查看 bond0 的速率显示 2000Mb/s

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ethtool bond0
Settings for bond0:
Supported ports: [ ]
Supported link modes: Not reported
Supported pause frame use: No
Supports auto-negotiation: No
Supported FEC modes: Not reported
Advertised link modes: Not reported
Advertised pause frame use: No
Advertised auto-negotiation: No
Advertised FEC modes: Not reported
Speed: 2000Mb/s
Duplex: Full
Port: Other
PHYAD: 0
Transceiver: internal
Auto-negotiation: off
Link detected: yes

搞定收工(‘・ω・’)

Reference:

存储服务器不想放在 OVH 了。所以自己来托管一台机器,顺便折腾下 2x1Gbps 组 LACP Bonding。

September 14, 2020 04:20 AM

迁移 DokuWiki 到 BookStackApp

Wiki 这么反人类的语法是怎么存在这么久的???????

总之受不了 Wiki 语法的可维护性了。什么?这玩意儿有维护性???

以及万年不更新的各种插件。系统升级后 PHP 7 不兼容,一看还是 swiftmail 的问题。生气。

正好有需求要整合一套知识库平台,搜索了一下 Confluence 的 alternative,发现了 BookStackAppPhabricator

前者适合个人或开源社区使用,后者则是一整套企业协作解决方案。对于我的需求来讲,BookStackApp 就足够啦。

页面数据

DokuWiki 并不使用数据库,因此没有一个通用的中间件来实现数据格式转换。而 DokuWiki 的语法非常奇葩——比如,它的一级标题是 ====== 这样 ======,六级标题才是 = 这样 =,正好和一般的 Wikitext 倒置。图片、内链等的表达方式也相当愚蠢,这些问题使我在思考迁移方案的第一个小时内就放弃了直接从源码转移的途径。

顺便,还有另外一个问题——本来为了使 Wiki 易于编写,这 DokuWiki 还安装了 Markdown 插件。因此部分页面中混杂着 Markdown 语法,更增加了源码处理的复杂度。

综合来看,最通用的数据格式,就是最终渲染出来的 XHTML 了。

图片

DokuWiki 的图片存储策略也是非常的奇特。由于它没有数据库,因此为了保持图片与页面的对应,它将图片存储在每个页面同样的路径下,并通过执行 PHP 的方式获取(扶额。

更甚者!!!

外链的图片,也是通过 /lib/exe/fetch.php 带参数来获取!!

我 的 天 哪。

因此既然在页面数据的考量中决定了使用最终渲染输出的 XHTML 来处理数据格式,图片也需要特殊的下载和归档技巧。这将需要使用 sanitize-html 提供的 transformer 方法来实现。

逻辑实现

一开始尝试了一些 Site Exporter 插件,但遗憾的是并没有什么真正能派上用场。甚至一些暴力递归下载所有页面和资源的脚本的表现也非常糟糕。

但是根据 DokuWiki 的官方 Tips,它可以将文章内容单纯导出 XHTML,只需要加上 ?do=export_xhtmlbody 参数即可。这就方便了,因为这样只需要一个完整的页面列表就可以了。随便找一个可以输出子命名空间的插件,新建一个页面用于从根命名空间展开就 OK 啦。

请求这个列表页面的 XHTML body 输出,使用 cheerio 遍历所有的 a 标签,就获得了所有要导出的页面地址。分别再去请求这些页面的 XHTML body 输出,做如下处理:

  1. 跟踪所有的 img 标签,下载图片文件并按预定义的路径规则和文件名归档。
  2. sanitize-html 清除所有不必要的标签、样式、id 和 class。
  3. sanitize-html 按预定义的路径规则更新所有 aimg 标签属性。

看代码

后来发现 DokuWiki 的性能不足以支撑异步请求的速度,额外加上了 sleep 模块来控制请求频率(扶额。

脚本执行完后,将图片目录移动到 BookStackApp 的对应位置,便可以直接读取所有的 HTML 文件来导入数据啦。

用了这么久,才发现原来还有比 raw HTML 更难以维护的数据格式啊…(望天。

Wiki 这么反人类的语法是怎么存在这么久的???????

September 14, 2020 04:20 AM

制作 Arch Linux 系统模板镜像

阿里云镜像制作踩坑记。

此文章主要记录按照阿里云 Customized Linux 制作 VPC 镜像的过程。一些部分也可用作制作其他平台镜像的参考。

当然记录的原因主要是 Arch 上的 cloud-init 打死无法在阿里云上修改 root 密码,就很气。

建立虚拟机

因为要制作 Customized Linux,所以第一步无法在阿里云平台上使用公共镜像制作。本机启动一个 Virtual Box,新建虚拟机,虚拟磁盘选择 RAW/IMG 格式即可。

按照一般步骤安装 Arch Linux,需要整个磁盘仅有一个分区。虽然很多平台支持多分区的镜像文件,但是莫名在这里踩了坑所以。

(另外吐槽:vps2arch 居然不帮我把 base-devel 装全了?!)

系统配置

安装一些必需的包。

1
# pacman -S qemu-guest-ga openssh

启用服务。

1
2
3
# systemctl enable qemu-ga
# systemctl enable sshd
# systemctl enable systemd-networkd

网络配置

哪个魂淡跟我讲 VPC 是 DHCP?装着 cloud-init 的 Arch 就可以自动设置内网 IP,这个没装的就 GG。

修改文件 /etc/systemd/network/default.network

1
2
3
4
5
[Match]
Name=en*

[Network]
DHCP=ipv4

总之先这样放着。

定制脚本

根据阿里云的文档,cloud init 不生效的时候需要用约定好的配置文件和脚本完成各种兼容动作。

新建目录 /aliyun_custom_image

新建文件 /usr/bin/aliyun-custom-os,写入内容

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
#!/bin/bash

os_conf_dir=/aliyun_custom_image
os_conf_file=${os_conf_dir}/os.conf

load_os_conf() {
if [[ -f $os_conf_file ]]; then
. $os_conf_file
echo $password
return 0
else
return 1
fi
}

cleanup() {
# ensure $os_conf_file is deleted, to avoid repeating config system
rm $os_conf_file >& /dev/null
# ensure $os_conf_dir is exitst
mkdir -p $os_conf_dir
}

config_password() {
if [[ -n $password ]]; then
password=$(echo $password | base64 -d)
if [[ $? == 0 && -n $password ]]; then
echo "root:$password"
echo "root:$password" | chpasswd
fi
fi
}
config_hostname() {
if [[ -n $hostname ]]; then
echo "$hostname" > /etc/hostname
hostnamectl set-hostname $hostname
fi
}
config_network() {
if [[ -n $eth0_ip_addr ]]; then
config_interface
systemctl restart systemd-networkd
fi
}
config_interface() {
mask2cdr $eth0_netmask
cat << EOF > /etc/systemd/network/default.network
# Generated by Aliyun Custom OS helper
# DO NOT EDIT THIS FILE! IT WILL BE OVERWRITTEN

[Match]
Name=$(ip link | awk -F: '$0 !~ "lo|vir|wl|^[^0-9]"{print $2a;getline}' | sed -e 's/^[[:space:]]*//')

[Network]
Address=$eth0_ip_addr/$netmask
Gateway=$eth0_gateway

[Link]
MACAddress=$eth0_mac_address

[Address]
Address=$eth0_ip_addr/$netmask
EOF
echo "nameserver 1.1.1.1" > /etc/resolv.conf
for ns in $dns_nameserver
do
echo "nameserver $ns" >> /etc/resolv.conf
done
}

mask2cdr() {
# Assumes there's no "255." after a non-255 byte in the mask
local x=${1##*255.}
set -- 0^^^128^192^224^240^248^252^254^ $(( (${#1} - ${#x})*2 )) ${x%%.*}
x=${1%%$3*}
netmask=$(( $2 + (${#x}/4) ))
}

if load_os_conf ; then
config_password
config_hostname
config_network
cleanup
else
echo "not load $os_conf_file"
fi

赋予执行权限

1
# chmod +x /usr/bin/aliyun-custom-os

新建 systemd unit 文件 /usr/lib/systemd/system/aliyun-custom-os.service 写入内容

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=Aliyun Custom OS Helper Script

[Service]
Type=oneshot
ExecStart=/usr/bin/aliyun-custom-os
TimeoutSec=30
StandardInput=tty
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

然后启用这个服务

1
systemctl enable aliyun-custom-os

挂载镜像

正常 shutdown 虚拟机,然后拿到镜像文件的路径。例如 ~/vm/archlinux.img

接下来需要将此镜像挂载到宿主机系统中修改、清理文件。首先确定镜像文件中的分区位置:

1
2
$ file ~/vm/archlinux.img
archlinux.img: x86 boot sector; partition 1: ID=0x83, active, starthead 32, startsector 2048, 41938944 sectors, code offset 0x63

得知 startsector2048

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ fdisk -l ~/vm/archlinux.img
You must set cylinders.
You can do this from the extra functions menu.

Disk archlinux.img: 0 MB, 0 bytes
255 heads, 63 sectors/track, 0 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x91d8e293

Device Boot Start End Blocks Id System
archlinux.img1 * 1 2611 20969472 83 Linux
Partition 1 has different physical/logical endings:
phys=(1023, 254, 63) logical=(2610, 180, 2)

得知 sectorsize512

使用 mount 命令带 offset 参数挂载镜像中的分区:

1
2
$ sudo mkdir -p /mnt/img
$ sudo mount -t ext4 -o loop,offset=$((2048*512)) /path/to/archlinux.img /mnt/img/ # 更改 -t auto 或者其他此分区使用的文件系统格式

就可以 cd /mnt/img 看到镜像里的 rootfs 啦。

清理/检查文件

要删除的:

1
2
3
4
5
# rm /root/.bash_history # _(:з」∠)_
# rm /etc/ssh/ssh_host_* # 强制每次部署的时候重新生成密钥对
# rm -r /var/log/* # 清理不需要的日志
# rm -r /var/cache/* # 清理缓存
# rm /etc/resolv.conf.bak # 避免恢复成制作时的 DNS

要检查的:

/etc/hosts - 我不知道为什么,第一次的时候把这个文件留空了(:з」∠)

/etc/resolv.conf - 鉴于总是有人喜欢手动修改这个文件,所以直接把它写成静态文件好了。内容例如

1
2
nameserver 8.8.8.8
nameserver 8.8.4.4

/etc/ssh/sshd_config 中是否允许 root 密码登陆。

准备镜像

退出 /mnt/img 目录,然后卸载镜像

1
# umount /mnt/img

(可选)使用 qemu-img 转换镜像格式到 VHD,减少镜像文件大小。特别是对国内的小水管上传(心疼

1
$ qemu-img convert -f raw -O vpc archlinux.img archlinux.vhd

上传镜像

在相同的 region 创建一个 OSS bucket,然后创建一个 RAM 子用户赋予 OSS 写权限并创建 Access Key,使用 OSSBrowser 上传准备好的 VHD 文件。

上传完毕后,在 ECS 标签下的镜像标签即可导入镜像。如果是第一次操作,需要给 ECS 授权访问 OSS。在导入的页面提示中提供了授权的链接。镜像内容配置如下:

  • OSS Object 地址:镜像文件在 OSS 中的 URL
  • Image 名称:archlinux-2018.1-x86_64 … 等符合要求即可
  • 操作系统:Linux
  • 系统盘大小:40GB
  • 系统架构:x86_64
  • 系统平台:Customized Linux
  • 镜像格式:VHD(如果是 img 就选 RAW)
  • 镜像描述:随便写啦。

确定后应该就会开始制作镜像了。

测试

因为没有做经典实例的兼容,这个镜像只能用于 VPC 的实例。总体而言,cloud-init 本来兼容的 Arch 却无法更改 root 密码(其他的倒是没问题),所以才选择了用一个 dirty 的方案来实现。

不知道应该说阿里云的工程师对自定义镜像的考虑周到还是对不同发行版的考虑欠妥…?

最后庆幸倒腾来去上传了好多遍 20G 的文件,日本运营商家宽带宽对等真的是帮了大忙,不然一个镜像制作不知道要到什么时候 > > (斜眼看国内三大运营商

参考:

阿里云镜像制作踩坑记。

此文章主要记录按照阿里云 Customized Linux 制作 VPC 镜像的过程。一些部分也可用作制作其他平台镜像的参考。

当然记录的原因主要是 Arch 上的 cloud-init 打死无法在阿里云上修改 root 密码,就很气。

September 14, 2020 04:20 AM

自托管的在线协作翻译平台 Weblate

起因:Transifex 这货闭源一段时间后突然开始抢钱了。

正巧一堆开源项目需要一个在线协作的翻译平台,于是测试了几个比较知名的开源程序。一遍折腾下来,发现 Weblate 可以最大化满足要求。顺便提一句,Weblate 也是有 hosted 付费服务的,但是在预算内的源字符串等限制依旧太多,所以选择使用他们的源码来搭建一套。

以及:我讨厌 Docker。

Weblate 文档 提供了非常全面的从起步到上手到各种高级用法的指南,因此这里不多赘述安装的过程。只记录少许踩过的坑。

这套程序看似简单,但实际上是基于 Django、使用了一大堆组件的复杂程序。如果想保持 system clean,最好(最快)的办法还是使用 Docker。

准备

小型实例只需要一台虚拟机即可。但是即便只托管几个项目,它依旧会吃掉 2 个 CPU 核心和 4GB 内存,和曾经开源版的 Transifex 有得一拼 大概也解释了为何这类服务都死贵

如果是托管在公网上的实例,则推荐使用 HTTPS。Weblate 的 Docker compose 提供了 HTTPS 支持,稍后会提到。

安装 Git, Docker 和 docker compose,在一些软件仓库中一般是 docker-cedocker-compose,其他软件均不需要手动安装。

搭建

首先克隆 docker compose 配置到本地

1
2
git clone https://github.com/WeblateOrg/docker.git weblate-docker
cd weblate-docker

为了直接开始使用 HTTPS,现在需要先建立域名解析记录,将要使用的域名(例如 weblate.example.com)指向服务器 IP。然后在该目录下创建配置文件 docker-compose-https.override.yml 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
version: '2'
services:
weblate:
environment:
- WEBLATE_DEBUG=0
- WEBLATE_EMAIL_HOST=smtp.gmail.com
- WEBLATE_EMAIL_HOST_USER=noreply@weblate.org
- WEBLATE_EMAIL_HOST_PASSWORD=system.email.password.here
- WEBLATE_ALLOWED_HOSTS=weblate.example.com
- WEBLATE_SERVER_EMAIL=noreply@weblate.org
- DEFAULT_FROM_EMAIL=noreply@weblate.org
- WEBLATE_REGISTRATION_OPEN=0
- WEBLATE_TIME_ZONE=UTC
- WEBLATE_OFFLOAD_INDEXING=1
- WEBLATE_REQUIRE_LOGIN=1
- WEBLATE_ADMIN_NAME=Weblate Admin
- WEBLATE_ADMIN_EMAIL=admin@weblate.org
- WEBLATE_ADMIN_PASSWORD=your+initial+password
https-portal:
environment:
DOMAINS: 'weblate.example.com -> http://weblate'

这份配置文件指定了:

  • 关闭 Django DEBUG mode (即生产模式)
  • 系统外发邮件服务器 smtp.gmail.com
  • 系统外发邮件用户名 noreply@weblate.org
  • 系统外发邮件密码 system.email.password.here
  • 允许使用的域名 weblate.example.com,如果有多个域名,使用逗号隔开
  • 系统外发邮件地址 noreply@weblate.org
  • 关闭注册通道,用户必须管理员手动添加
  • 设置系统时间为 UTC
  • 打开后台索引,降低运行时的负载
  • 执行任何操作前要求登陆
  • 默认管理员名称是 Weblate Admin
  • 默认管理员邮箱地址是 admin@weblate.org
  • 默认管理员密码是 your+initial+password

然后在 https-portal 容器中指定了要使用 SSL 的域名 weblate.example.com 和后端指向的容器 http://weblate

在当前目录中执行

1
docker-compose -f docker-compose-https.yml -f docker-compose-https.override.yml up

会顺序拉取、启动 4 个 docker 容器,分别是:

  • https-portal
  • weblate
  • postgresql
  • memcached

第一次启动需要一些时间拉取镜像并导入初始数据。全部完成后,访问 weblate.example.com 应该可以看到一个 HTTPS 的 Weblate 实例运行,使用之前定义的默认管理员邮箱地址和密码即可登入。

此时转回终端,按一次 ^C 等待四个容器正确关闭,然后编辑 docker-compose-https.override.yml,删除以下配置

  • WEBLATE_ADMIN_NAME
  • WEBLATE_ADMIN_EMAIL
  • WEBLATE_ADMIN_PASSWORD

否则,如果更改了默认管理员的信息(如用户名等)下次启动会再次创建管理员帐号,并使用相同的邮箱,导致默认管理员无法使用邮箱登陆(报错返回 2 个用户信息)。解决办法是使用用户名…(摔

集成配置

再次运行 docker-compose -f docker-compose-https.yml -f docker-compose-https.override.yml up 后可以很快启动所有需要的程序。此时登入 Weblate 实例,指向 /admin/ssh/ 点击创建 SSH 密钥。

在对单一 repo 提交的情况下,此 SSH Key 可作为 GitHub deploy key,但是如果需要多个不同 repo 提交时,有两种方法:

  • 创建一个 GitHub 用户,然后将此 SSH Key 添加到此用户下,再给此用户所有必要的写权限
  • 使用 Access Token 作为 HTTPS 密码访问必要的 repo

浏览器指向 /admin/trans/project/ 新建一个 Project。这个 Project 不仅指一个项目,也可以作为一个 Organization 的存在,更精确的解释是一个软件集,例如 KDE 套件可以包含一大堆的组件。

Weblate Project

指向 /admin/trans/subproject/ 这里才是可以添加要翻译的项目的地方。如果对应的 repo 添加了公钥,这里可以直接使用 SSH 方式的 push URL。

Weblate Import strings

File mask 填写所有语言文件相对 repo root 的路径,使用 * 代替语言代号。如果是 Monolingual language file,例如 key 是 user.info.comment_posted 这样而非原本即可阅读的文本,则 Monolingual base language fileBase file for new translations 均为源语言文件相对 repo root 的路径,这样即可正确识别源语言的字符串不至于让别人拿着 comment_posted 这样的 key 来猜意思

Weblate Translation Interface

持续集成

提交翻译后,Weblate 会在后台完成索引并提交必要的更改。当然也会一不小心刷了别人的屏…

Weblate continous translation

在 repo 的 settings -> integration 中可以添加 Weblate 作为集成,每次有新的提交即可触发 Weblate 更新源语言文件。

结论

我很开心可以省下每年数千美元来用一个非常卡的在线协作翻译平台

当然,我依旧讨厌 Docker。

起因:Transifex 这货闭源一段时间后突然开始抢钱了。

正巧一堆开源项目需要一个在线协作的翻译平台,于是测试了几个比较知名的开源程序。一遍折腾下来,发现 Weblate 可以最大化满足要求。顺便提一句,Weblate 也是有 hosted 付费服务的,但是在预算内的源字符串等限制依旧太多,所以选择使用他们的源码来搭建一套。

以及:我讨厌 Docker。

September 14, 2020 04:20 AM

使用 Blender 渲染 Minecraft 3D 效果图

突发奇想渲染 Minecraft 3D 效果图,首先用 Chunky 尝试了一下发现效果虽好但:

  • 人物动作过于限制
  • 渲染太!慢!了!

然而并买不起 Cinema 4D,所以来尝试一下 Blender 啦~

准备工作

  • 比较好的显卡。我的家用游戏机是 NVIDIA GTX 1080Ti,Cycles Render 可以 offload 掉绝大部分 CPU 的压力。
  • 安装 Blender
  • 下载 jmc2obj
  • 下载 MCPrep
  • 准备地图、材质、玩家皮肤等资源。

导出地图文件到 obj

启动 jmc2obj,在最上方选择地图存档位置并单击 load。在 UI 里选择要导出的地图部分,点击 Export。在左侧的选项中依次:

  1. Map Scale = 1.0
  2. Center 选中,否则可能会出现在距离地图很远的地方
  3. Texture Export
    • Pre-scale textures - 如果是原版材质,建议设置为 4x。如果是高清材质,按需要选择即可。
    • 不勾选 Export alpha channel in separate file(s)
    • 不勾选 Export all textures in a single file
    • 选择从 Minecraft 安装里导出默认材质,或自行选择一个额外的材质包。
    • 然后选择材质的导出位置。建议在目标目录中新建一个 textures 目录,然后导出到此目录中。此目录里面会出现一个 tex 目录,包含所有的材质文件。

材质导出进度完成后,开始导出地图文件。在右侧的选项中依次:

  1. 取消所有的选项勾选
  2. 勾选 Create a separate object for each material
  3. 可选勾选:
    • Render Entities
    • Occulude different adjacent materials
    • Optimize mesh
    • Do not allow duplicate vertexes
  4. 其余选项均保持非勾选状态
  5. 点击 Export 导出到之前创建的 textures同级目录
  6. 可能会遇到 banner 找不到材质的问题,忽略继续。导出后还不能使用,需要一个简单而 ugly 的 hack - 再导出一遍覆盖之前的 obj 和 mtl 文件。

导出后的工作目录如下:

Blender Working Directory

.obj 文件是地图数据,.mtl 则是刚才导出材质的材质索引,指向 textures/tex 的相对路径,因此这些文件的相对位置不能改变。

安装 MCPrep

启动 Blender,在 File -> User Preferences -> Add-ons 里,选择 Install Add-on from File...

然后点击下载好的 MCPrep 的 .zip 文件即可。

导入 blender

启动 Blender,先删掉默认的 object 和 lamp,然后依次选择 File -> Import -> Wavefront (.obj)

选择刚才生成的 .obj 文件导入。

(大概会卡一会儿… 喝杯茶先)

导入完成后,首先需要设置材质。选中所有 object (快捷键 A,如果被反选则再摁一次即可),左侧标签页切换到 MCPrep,然后点击 Prep Materials。

Prep Materials

进入 Walk Navigation 模式(或者其他熟悉的移动模式),Add -> lamp -> Sun 然后将添加的 Sun 光源移动到地图的合适位置(比如一个边角)。

Walk Navigation

然后移动 Viewport 到合适的位置,将默认的 Camera 对齐到当前视角。快捷键 Ctrl+Alt+0

Align Camera to View

设置背景

在 Blender 的上方菜单栏中,有 Blender Render 和 Cycles Render 两个选项。

如果使用 Blender Render,则需要在 Sun 的属性里设置 Sky & Atmosphere 并设置 Ray Shadow 以使光源和阴影正确对应。

如果使用 Cycles Render,则在 World 属性中设置 background 为 Sky Texture。

Sky texture

当然也可以使用自己的天空图像或其他材质。

渲染

在渲染属性中设置使用 CPU 或者 GPU 渲染图像,并可以设置分辨率等。

Render Properties

修改好设置,在 Blender 上方菜单栏中点击 Render -> Render Image 即可开始渲染啦~

玩家和实体

我相信熟练使用 C4D/Maya/Blender 的玩家们不需要看这篇教程… 所以玩家实体的高级用法不多讲。

需要的材料是一份玩家或者其他实体的 Rig,在 PlanetMinecraft 或者 MinecraftForum 上有很多。贴上皮肤就可以用啦。

在 player rig 中调整好 pose,保存。在要渲染的世界工程里,选择 File -> Append,然后选择刚才的玩家 pose 的 blender 文件,进入后选择 Scene 并 append。

选择 Scene 的原因是有不少 rig 并不是一个单一 object,如果是单一 object,则可以直接导入 object。

在右上方的 object 列表中选择刚才 append 的 scene,定位玩家实体并全部选中 -> Ctrl+C 复制 -> 返回渲染世界 -> Ctrl+V 粘贴。

选择粘贴进来的玩家实体,然后调整到合适的位置。

成果

这大概是我第一次玩 blender。

总之以下是成果啦。

比如某玩家的 pose 效果图:

leeder's pose

比 chunky 的动作自然多了吧~

最后是完成的效果:

Project xport

Happy Rendering~

突发奇想渲染 Minecraft 3D 效果图,首先用 Chunky 尝试了一下发现效果虽好但:

  • 人物动作过于限制
  • 渲染太!慢!了!

然而并买不起 Cinema 4D,所以来尝试一下 Blender 啦~

September 14, 2020 04:20 AM

NetFLOW / sFLOW 流量报告:FastNetMon + InfluxDB + Grafana

最近稍微有点时间折腾了下 Cisco 的三层交换,尝试搭建了一套数据中心用的流量统计/监控/报告系统。过程不是很复杂,但是也只算利用了一套高级软件组合的一点点功能。之后打算继续研究更多的功能实现,不过也要看有没有时间了…

准备工作

首先确认出口路由设备支持 netflow/sflow 的对应版本。一般 Cisco 的路由器或者三层交换都是支持的。

然后准备一个常见的 Linux 系统,虚拟机或者物理机都可以。

出口路由设备能够连通到该 Linux 系统,并且 flow collector 设置到该 Linux 系统的 IP 地址和对应端口。

FastNetMon

安装 fastnetmon,只需要一条简单的脚本命令。

然后将所有要监控的网段加入 /etc/networks_list。一行一个,例如:

1
2
3
10.1.0.0/16
192.168.254.0/24
8.8.0.0/16

按照安装文档打开两个终端,分别启动主进程和客户端

1
/opt/fastnetmon/fastnetmon
1
/opt/fastnetmon/fastnetmon_client

如果没有问题,应该在客户端上可以看到收到的 flow 数据。

先关闭 fastnetmon 进程,修改配置文件打开 Graphite 支持:

1
2
3
4
graphite = on
graphite_host = 127.0.0.1
graphite_port = 2003
graphite_prefix = fastnetmon

=== 2018-07-26 更新 ===

如果有比较新的发行版(内核 >= 3.6)可以开启 AF_PACKET,安装并启动 irqbalance 来获得更好的抓包性能。

InfluxDB

安装 InfluxDB,官方提供了各种包管理器的安装方式。

配置文件一般位于 /etc/influxdb/influxdb.conf,需要根据环境做安全相关设置(侦听地址、端口、鉴权、etc)并打开 Graphite Simulation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[[graphite]]
enabled = true
bind-address = "127.0.0.1:2003"
database = "flow_dc1"
protocol = "tcp"
consistency-level = "one"
name-separator = "."

# batch-size / batch-timeout requires InfluxDB >= 0.9.3
batch-size = 5000 # will flush if this many points get buffered
batch-timeout = "1s" # will flush at least this often even if we haven't hit buffer limit

templates = [
"fastnetmon.hosts.* app.measurement.cidr.direction.function.resource",
"fastnetmon.networks.* app.measurement.cidr.direction.resource",
"fastnetmon.total.* app.measurement.direction.resource"
]

顺序重启 InfluxDB 和 fastnetmon。检查 flow 数据是否记录到 InfluxDB:

1
2
3
4
5
6
7
8
9
10
11
$ influx
Connected to http://localhost:8086 version 1.2.4
InfluxDB shell version: 1.2.4
> use flow_dc1
Using database flow_dc1
> select mean(value) from networks where direction = 'incoming' and resource = 'bps' group by *
name: networks
tags: app=fastnetmon, cidr=10_1_0_0_24, direction=incoming, resource=bps
time mean
---- ----
0 4735.049632696411

Grafana

Grafana 是一款非常强大且易用的数据可视化工具。安装 Grafana 然后修改配置文件的必要部分,配置文件一般位于 /etc/grafana/grafana.ini

完成后重启 Grafana,将浏览器指向 Grafana 的 HTTP 服务器地址即可看到登录界面。如果内部使用的话,建议关闭匿名访问和注册功能。

使用默认的 admin / admin 登录,按照引导完成配置、添加数据源(Data source),数据源即是 InfluxDB 的 HTTP API 地址。如果 Grafana 中限制了数据源白名单,需要将 InfluxDB 的 HTTP API 地址和端口加到白名单里。

添加面板、Graph,在 Graph 编辑模式里写入类似这样的查询语句:

1
SELECT mean("value") FROM "networks" WHERE "direction" = 'incoming' AND "resource" = 'bps' AND "cidr" =~ /^10_1_0_0_16/ AND $timeFilter GROUP BY time($interval) fill(previous)

即可看到有图表出现。根据需求完善查询语句和图表配置即可简单实现各种可视化效果。例如流量和数据包的实时报告:

Traffic Graph

PPS Graph

总结

通过配合 FastNetMon,InfluxDB 和 Grafana 即可快速实现一套基于 NetFLOW / sFLOW 的流量统计报告系统。但是 FastNetMon 的功能远不止流量统计,Grafana 也有大量插件和灵活的用法可以满足更多需求。如果配置合理,此方案也可适用于 40Gbps+ 接入的中型数据中心且成本低廉。以及——

  1. InfluxDB 真的很快!
  2. Grafana 的图表真的很省资源!
  3. Chronograph 卡死了我的浏览器!(i7-7700K / Chrome)

以及一大早手工修好了 K812 的耳机线,省掉了 2 万日元的线材费用非常开心

最近稍微有点时间折腾了下 Cisco 的三层交换,尝试搭建了一套数据中心用的流量统计/监控/报告系统。过程不是很复杂,但是也只算利用了一套高级软件组合的一点点功能。之后打算继续研究更多的功能实现,不过也要看有没有时间了…

September 14, 2020 04:20 AM

不作死就不会死系列,TFTP 修复变砖的 Nighthawk X6

由于之前买的 AC87U 经常被 roommate 抱怨掉线(风评表示这货 5G 有问题,然而我连着 5G 毛事儿没有,隔壁用 2.4G 却一直掉线)…

新购入的路由器是 Netgear Nighthawk X6 R8000。

由于之前的 Security Advisory,所以到手第一件事就是配上网络更新固件啦。更新挺慢的于是点点点完事儿撸猫去了。过了一会儿回来一看怎么还没网络?得,砖了…

讲道理,Netgear 也算大厂了,这种 online update 干了不知道多少回,第一次遇到这都能变砖的(扶额。

现象就是电源橙色灯亮后一会儿变成白色灯闪烁,且网络服务没有启动。尝试过 factory reset 无效,官方提供的 TFTP 强刷工具也无效(刷不进…

解决方案反而是意想不到的简单。总之大概记录下修复的过程。

  1. 官方网站下载适用的新版固件并解压,应该得到一个 .chk 文件
  2. 关闭路由器电源等待 10 秒,网线插 LAN 口开机。
  3. 检查是否获得了正确的 IP。如果没有,可能 DHCP 服务没起来。手动设置一个正确的 IP 吧。然后能 ping 通路由器 IP 即可。
  4. 电源灯开始闪烁的时候,执行命令 tftp -i [router ip] put [path/to/firmware.chk]。例如 tftp -i 192.168.1.1 put ./R8000-V1.0.3.36_1.1.25.chk
  5. 等一会儿路由器自动重启,搞定。

配置都没丢…然后我依旧没有搞定 OCN 要怎么连 IPv6… 说好的 IPv6 PPPoE 呢…

心累.png

由于之前买的 AC87U 经常被 roommate 抱怨掉线(风评表示这货 5G 有问题,然而我连着 5G 毛事儿没有,隔壁用 2.4G 却一直掉线)…

新购入的路由器是 Netgear Nighthawk X6 R8000。

由于之前的 Security Advisory,所以到手第一件事就是配上网络更新固件啦。更新挺慢的于是点点点完事儿撸猫去了。过了一会儿回来一看怎么还没网络?得,砖了…

September 14, 2020 04:20 AM

Minecraft 服务器资源控制策略:AI 抑制而非数量限制

Minecraft 的 lag 问题已经司空见惯,各种控制资源消耗和卡顿的插件也层出不穷。但是它们几乎都非常用力地在一个点上:控制实体数量。

这并不无道理,因为 Minecraft 中最消耗资源的部分就是实体。但是暴力控制实体数量会导致刷怪塔无法正常工作、掉落物清理速度过快等问题,在生存服务器中可能引发玩家的强烈不满。

所以,喵窝开发组从另一个角度做出了一些尝试。

启发

生物实体的数量巨大,主要集中的地区显然不是野外的自然刷怪区,而是玩家聚集的刷怪场、村民工程、动物养殖场等。如果不限制生物的数量和密度同时降低资源消耗,那么只能从生物实体的特性入手了。

Minecraft 最近的版本中引用了 NoAI 的 NBT Tag,带有此标签的生物将不会进行 AI 计算。换句话说,除了占用服务器内存中的一点数据,几乎不会对这个生物实体有任何其他的 CPU 算力消耗。

也就是说,实体消耗的算力资源,绝大部分都是 AI 计算的消耗。

方案

抓上一票人做了一些测试,结果证实生物失去 AI 后大幅降低了 CPU 的算力消耗。这是个 positive 的信号,但是接下来的测试则遇到了问题。

对于养殖场,等生物数量变化不大(或者说只是定期来清理并重新养殖一次)的设施,生物失去 AI 的影响很小,只有在重新繁殖时需要恢复 AI。但是刷怪塔则因为生物没有 AI,同时也被强制不受重力影响而几乎无法使用,即便同时设置 NoGravityfalse 也无效。

开发组中 @Librazy 提到了 Spigot 的一个参数 nerf-spawner-mobs,开启时刷怪笼生成的生物将不会拥有 AI,但是会被外界影响(例如水流和火球等)而移动。这个选项是全局的,因此不需要开启,只需要反射 spigot 中设置该功能的方法即可。

于是整个方案的流程便是当服务器卡顿时抑制生物密集区的生物 AI 从而降低资源占用,同时最大程度上保证玩家对生物的需求。「服务器卡顿」的考量以服务器 TPS 而非实体数量为准,当服务器 TPS 高于一定值时即认为服务器没有超负荷,不会有任何操作,最大程度上利用硬件的性能。

实现

插件主要由开发组的 @Cylin@Librazy 编写,源代码以 MIT 协议发布在 GitHub 上。

插件每隔一段时间扫描服务器的 TPS 确认运行状况,如果 TPS 低于阈值则触发 AI 控制,TPS 高于一定值且持续一段时间即认为服务器已恢复正常运行状态,自动恢复被抑制的实体 AI 减少对生存体验的影响。

实现过程中额外添加了一些额外可能被生存服务器用到的功能:

  • per-world 控制,如果玩家需要建造以仇恨为基础的小黑塔,可以关闭对末地的控制。
  • 实体总量和单区块实体密度在 AI 抑制时纳入考虑,更加精准抑制资源消耗较高的区块。

测试

yasui 插件在 毛玉線圈物語 服务器中应用测试。由于近期玩家数量爆炸式增长(日常在线 5 人到 ~30 人甚至 50 人),各种实体控制插件均告无效。yasui 插件应用后被证实数次发挥作用,没有任何实体数量限制的前提下将服务器 TPS 稳定在 19 以上,服务器实体承载数量从 ~2500 提到至接近 5000,并且还有继续提高的可能(数次触发中最高一次单世界实体记录是 4808,其他世界中仍有大约 2000 实体未被计入)。

吐槽:你们贼能刷

Minecraft 的 lag 问题已经司空见惯,各种控制资源消耗和卡顿的插件也层出不穷。但是它们几乎都非常用力地在一个点上:控制实体数量。

这并不无道理,因为 Minecraft 中最消耗资源的部分就是实体。但是暴力控制实体数量会导致刷怪塔无法正常工作、掉落物清理速度过快等问题,在生存服务器中可能引发玩家的强烈不满。

所以,喵窝开发组从另一个角度做出了一些尝试。

September 14, 2020 04:20 AM

新轮子:Planet.js

一开始的选择(需要登陆)中纠结使用 Planet 还是博客的时候就已经在关注 Planet 这个东西。

非常奇特,本身并不存储什么数据,但是用这样一个简单的方式在极低的成本下将大量的用户和内容结合到了一起。

终于决定要建立一个社区星球的时候,我才发现它连主题模板都不支持响应式才不是我想写个好看点的主题结果折腾一天无果(ノ=Д=)ノ┻━┻

总之,使用的是 Ubuntu 仓库内的 planet-venus 包,遇到了各种问题…

  • 莫名其妙无法获取 feed,浏览器访问正常,用 curl 正常
  • 莫名其妙无法读取 feed 内容,其他 feedparser 均正常
  • 对文章内使用相对链接的内容无能为力
  • 模板语言落后,不要跟我说 old fashion
  • 输出路径一直是 cwd,不知道是不是 feature。但是这导致我的 home 下面到处都是 output 目录

强迫症犯了。遂决定自己实现一个因为逻辑很简单啊不过就是把 RSS 抓下来排个序再丢出去么

但是到具体的细节,还是有不少需要考量的东西。

压缩和编码

有些网站打开了 gzip 压缩,有些网站使用了非 UTF-8 编码…只是暴力读取的结果就是页面上一半正常一半乱码。

好在 feedparser 给出了使用 zlibiconv代码样例,这个问题就迎刃而解啦。

相对路径和内容安全

Planet Venus 似乎是会解析处理文章内不带有协议的链接和图片,以使这些资源能够在 planet 的页面中直接通过原地址访问。但是问题在于,planet venus 似乎只是暴力给所有非完整 URL 形式的资源地址都强行加上协议和主机名。于是就出现了这种情况——

  • 原文地址 http://www.example.com/2017/01/21/example.html
  • 原文中引用的资源 images/1234.jpg
  • 看起来没毛病,但是 Planet Venus 解析过来就是 http://www.example.com/images/1234.jpg
  • 这似乎看起来也没毛病… 然!而!正确的地址是 http://www.example.com/2017/01/21/images/1234.jpg

…呵呵哒。

先不管为什么会有这么奇怪的资源路径,但是 Feedparser 却可以正确解析到带有跟路径的地址。也就是上面的资源地址,Feedparser 解析完之后就是正确的 /2017/01/21/images/1234.jpg

但是只有正确的相对路径还不够,因为 planet 是单独的站点,直接把 HTML 往页面中插,结果就是浏览器会去请求 planet/2017/01/21/images/1234.jpg 然而 planet 的服务器上并不会有这个资源。

于是这个问题先放着,来看另一个问题。

聚合的一个特点是,几乎无法控制来源内容的安全性——因为其他的网站服务器都不是自己维护的,其他人能否保证自己的网站不被入侵、被跨站…都是未知数。如果有订阅的网站被入侵挂了马,阅读/订阅聚合 planet 的用户也会中招。

解决这个问题,常见的办法就是过滤掉不安全的 HTML tag。于是这里引入了 sanitize-html

其实我一开始打算用 regex 直接切掉不过还是不够 robust 所以乖乖去找包了… 但是惊喜的是这个包居然可以实现按规则替换!这就顺利解决了之前的问题,可以将 Feedparser 解析得到的路径加上正确的协议和主机地址。

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
sanitizeHtml(html, {
transformTags: {
// restore origin links
'img': function (tagName, attribs) {
if (attribs && attribs.src) {
if (attribs.src.startsWith('/') && attribs.src.charAt(1) !== '/') {
// root-relative path, add corresponding host before root
return {
tagName: tagName,
attribs: {
src: host + attribs.src
}
};
} else if (attribs.src.startsWith('//') ||
attribs.src.startsWith('http://') ||
attribs.src.startsWith('https://')) {
// absolute path, do nothing
return {
tagName: tagName,
attribs: attribs
};
} else {
// Feedparser may not correctly recognize this path, try with host/path
return {
tagName: tagName,
attribs: {
src: host + '/' + attribs.src
}
};
}
}
// don't miss the default action or it throws exception.
return {
tagName: tagName,
attribs: attribs
};
}
}
});

解决了文章内容相对路径的问题,要过滤特定的标签或者标签属性则是这个包本来就要做的事情了,小菜。

处理完文章对象之后,用 Array.prototype.sort 带一个 compare function 通过更新日期排序,接下来就可以简单渲染页面和 RSS 文件啦。

其他

一些不太常见的功能,例如 http proxy 的支持(在特殊的网络环境下可能用到),长文章仅展示 summary 并提示继续阅读,avatar 的支持,模板和输出目录保持和配置文件的相对路径,等。

然后作为深有体会——

1
2
3
CSS
IS
AWESOME

因此打死不写前端的家伙选择直接套用了 YUI Library 的 purecss 框架做一个还算看得过去的模板。至少… 几番折腾之后把响应式和 pre 等难缠的宽度搞定了顺便玩了下 media query

于是代码 -> GitHub

以及已经在使用的 Planet NyaaCat

从开坑到基本完善大概花了 15 个小时… 果然长时间不写代码手生才不是冻得手打字都变慢了呢

一开始的选择(需要登陆)中纠结使用 Planet 还是博客的时候就已经在关注 Planet 这个东西。

非常奇特,本身并不存储什么数据,但是用这样一个简单的方式在极低的成本下将大量的用户和内容结合到了一起。

终于决定要建立一个社区星球的时候,我才发现它连主题模板都不支持响应式才不是我想写个好看点的主题结果折腾一天无果(ノ=Д=)ノ┻━┻

总之,使用的是 Ubuntu 仓库内的 planet-venus 包,遇到了各种问题…

  • 莫名其妙无法获取 feed,浏览器访问正常,用 curl 正常
  • 莫名其妙无法读取 feed 内容,其他 feedparser 均正常
  • 对文章内使用相对链接的内容无能为力
  • 模板语言落后,不要跟我说 old fashion
  • 输出路径一直是 cwd,不知道是不是 feature。但是这导致我的 home 下面到处都是 output 目录

强迫症犯了。遂决定自己实现一个因为逻辑很简单啊不过就是把 RSS 抓下来排个序再丢出去么

但是到具体的细节,还是有不少需要考量的东西。

September 14, 2020 04:20 AM

玩了一下 NGINX RealIP 模块

最近要给网站上 CDN 于是折腾了下在 NGINX 部分获取客户端真实 IP 的方案。

嘛… 意想不到的简单就是…

安装 realip 模块

如果是 Debian/Ubuntu 系统,直接安装 nginx-extras 这个包即可。包含了很多有用的模块,不需要再自己编译。

如果是其他发行版,且没有提供额外模块的包的话,需要自己编译 NGINX。编译参数加 --with-http_realip_module 即可。

获得前端服务器地址

常见的 CDN 前端 IP 都可以从 CDN 提供商处获得,例如 CloudFlare 的 IP 地址段在这里

如果需要找到 Google Cloud Platform 的 IP 地址段,可以使用 Google 提供的 TXT 记录查询。

1
$ dig @8.8.8.8 _cloud-netblocks.googleusercontent.com TXT

获得记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; <<>> DiG 9.11.0-P2 <<>> @8.8.8.8 _cloud-netblocks.googleusercontent.com TXT
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42732
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;_cloud-netblocks.googleusercontent.com. IN TXT

;; ANSWER SECTION:
_cloud-netblocks.googleusercontent.com. 3599 IN TXT "v=spf1 include:_cloud-netblocks1.googleusercontent.com include:_cloud-netblocks2.googleusercontent.com include:_cloud-netblocks3.googleusercontent.com include:_cloud-netblocks4.googleusercontent.com include:_cloud-netblocks5.googleusercontent.com ?all"

;; Query time: 51 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Wed Feb 08 20:31:29 JST 2017
;; MSG SIZE rcvd: 331

这里面的 _cloud-netblocks1.googleusercontent.com 等地址即是用于保存 GCP IP 段的地址。继续查询:

1
dig @8.8.8.8 _cloud-netblocks1.googleusercontent.com TXT

获得记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; <<>> DiG 9.11.0-P2 <<>> @8.8.8.8 _cloud-netblocks1.googleusercontent.com TXT
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 22867
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;_cloud-netblocks1.googleusercontent.com. IN TXT

;; ANSWER SECTION:
_cloud-netblocks1.googleusercontent.com. 3599 IN TXT "v=spf1 ip4:8.34.208.0/20 ip4:8.35.192.0/21 ip4:8.35.200.0/23 ip4:108.59.80.0/20 ip4:108.170.192.0/20 ip4:108.170.208.0/21 ip4:108.170.216.0/22 ip4:108.170.220.0/23 ip4:108.170.222.0/24 ?all"

;; Query time: 55 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Wed Feb 08 20:32:39 JST 2017
;; MSG SIZE rcvd: 270

于是得到了一堆 IP 地址。

设置 RealIP 模块

文件 /etc/nginx/snippets/realip.conf,请注意这个位置在其他发行版未必存在,放在 NGINX 配置目录下即可。

1
2
3
4
set_real_ip_from  192.168.1.0/24;
set_real_ip_from 192.168.2.1;
set_real_ip_from 2001:0db8::/32;
real_ip_header X-Forwarded-For;

然后在 vhost 配置文件中引用这个配置。

1
include snippets/realip.conf;

搞定,重启 NGINX 即可获得客户端真实 IP。

Note:

GCP 的 X-Forwarded-For 的客户端 IP 在第一个 , 的前面,所以一般需要 split(',')[0]

最近要给网站上 CDN 于是折腾了下在 NGINX 部分获取客户端真实 IP 的方案。

嘛… 意想不到的简单就是…

September 14, 2020 04:20 AM

重新迁移回 GCP

忍得住打 Call,忍不住折腾。看到黑科技就手痒。

Performance with GCP

虽然 AWS 用得挺安心不过越来越多的人向我抱怨博客打开速度很慢,想看文章都要等好久什么的。于是纠结了下,还是迁回了最贵但是最快的 Google Cloud Platform。这里记录一下调(zhe)教(teng)过程,毕竟 GCP 的 Cloud CDN 依然在 Alpha/Beta 阶段,可配置的选项实在太少而且很多 caveats。

前期准备

  • 域名
  • SSL 证书
  • 网站程序或内容

创建实例

我这样没什么访问量的静态网站用 f1.micro 就好啦~ 不过流量比较大的网站的话,还是建议选择高一些的配置和 SSD。

然后创建一个实例组(instance group),确保该实例组包含刚才创建的实例。

NGINX 配置

网站等会说。总之先配置好 SSL 和 Health Check。

1
2
# mkdir -p /var/www/hc
# touch /var/www/hc/index.html

默认站点配置 default.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 80;
listen [::]:80;
server_name hc; # health check
root /var/www/hc;
index index.html;
}

server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 301 https://$host$request_uri;
}

网站配置 example.com.conf

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
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com;

ssl on;
ssl_certificate /etc/ssl/private/example_com.pem;
ssl_certificate_key /etc/ssl/private/example_com.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4";
keepalive_timeout 70;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_dhparam /etc/ssl/certs/dhparam.pem;

add_header Strict-Transport-Security max-age=63072000;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;

root /var/www/example.com;
location / {
# ...
}
}

(SSL 相关的很多配置步骤省略,请自行查阅其他文档)

创建负载均衡器

戳到 Networking 标签,选择 Load balancing 然后创建一个 HTTP(S) Load Balancing。首先为负载均衡器命名,因为要分别创建 2 个负载均衡所以这里可以写例如 website-http 之类作为区分。配置页面的三个标签:

  • backend
    • 创建一个新的 backend
    • backend service 选择 http
    • instance group 选择刚才创建的实例组
    • port numbers 80
    • health check 创建新的 health check 并设置 custom header,和之前 NGINX 里的配置一致,这里是 hc
    • 勾选 Enable Cloud CDN
  • Host and path rules
    • 添加域名对应的 //* 两个路径规则。如果有特殊需求则另外添加。
  • Frontend configuration
    • Protocol 选择 HTTP
    • Create IP Address 然后 reserve 一个 IP

第二个负载均衡器用于 HTTPS,名称可以是 website-https 这样。配置:

  • backend
    • 因为配置不一样,所以要再创建一个新的 backend
    • backend service 选择 https (要点 Edit)
    • instance group 选择刚才创建的同一个实例组
    • port numbers 443
    • health check 创建新的 health check 并设置协议为 https,custom header 为网站域名(因为要和 SSL 证书相符,示例里是 example.com
    • 勾选 Enable Cloud CDN
  • Host and path rules
    • 同上
  • Frontend configration
    • Protocol 选择 HTTPS
    • IP 选择刚才 reserve 的 IP
    • 创建证书,分别上传签发的证书、CA 证书链和私钥

更新解析&收尾

将域名的 DNS 解析到刚才 reserve 的 IP 即可。

至此就是基本的(当前版本的)Google Cloud CDN 配置步骤。还有很多可以自定义和扩展、优化的空间,但是这些需要根据特定的需求变化因此不再详细记录。

以及,不要忘记在虚拟机里把网站跑起来~

最后,南酱的 live 超棒(๑•̀ㅂ•́)و✧

忍得住打 Call,忍不住折腾。看到黑科技就手痒。

September 14, 2020 04:20 AM

搭建一套权威 DNS 服务架构

萌 DNS 已经年久失修。尽管一直有计划完全重写出一套应用目前各种 DNS 特性和优化的完整平台,但是目前的精力不允许。所以为了先让萌 DNS 的用户们至少先有一个能支持 Let’s Encrypt 的 DNS 服务,决定暂时舍弃 GeoDNS 功能,使用一套更加成熟的解决方案提供服务。

搭配方案如下:

服务器部署:

  • 管理服务器 x1
    • MySQL Master
    • PowerDNS
    • PowerDNS-Admin, supervisor, virtualenv, gunicorn…
    • NGINX, Let’s Encrypt
  • DNS 服务器 x4
    • MySQL Slave
    • PowerDNS

在管理服务器上安装 PowerDNS 和 MySQL Master 的考量是由于 PowerDNS-Admin 使用 PowerDNS HTTP API,在管理服务器(或管理私网中)启动一个仅用于提供 API 和操作主数据库的 PowerDNS 实例能够减轻 Primary NS Server 的压力并提升安全性。整套架构使用 Ansible 进行自动化部署,不过好久没用了各种生疏,照着文档折腾好久的配置…

于是这里暂且记录下整个过程。有些坑只是作者一时疏忽或者有别的考量但没有明确记录,也许在未来的版本中会修复。

安装 PowerDNS

所有服务器均使用 Ubuntu 16.04,需要 PowerDNS 4.0 以上的版本。按照此页面的说明添加 PowerDNS 官方的仓库即可。

1
# apt install pdns-server pdns-backend-mysql mysql-server

由 dpkg 自动配置 PowerDNS 的数据库,然后删除 /etc/powerdns/pdns.d无关的配置文件。

1
2
# rm /etc/powerdns/pdns.d/pdns.local.conf
# rm /etc/powerdns/pdns.d/pdns.simplebind.conf

配置 MySQL Replication,管理服务器作为 Master,其他 DNS 服务器作为 Slave。细节不多讲,官方文档 或者 DigitalOcean Tutorial

管理服务器 (MySQL Master) PowerDNS 配置文件 /etc/powerdns/pdns.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
api=yes
api-key=yourapisecretkey
api-logfile=/var/log/pdns-api.log
config-dir=/etc/powerdns
guardian=yes
include-dir=/etc/powerdns/pdns.d
launch=
local-address=127.0.0.1 # 不对外提供服务
local-ipv6=::1
security-poll-suffix=
setgid=pdns
setuid=pdns
webserver=yes
webserver-address=127.0.0.1 # 仅向本机的 PowerDNS-Admin 调用。如果配置在内网,请使用内网 IP
webserver-allow-from=127.0.0.1/32 # 同上,如果使用内网则写 PowerDNS-Admin 在内网的 IP
webserver-port=8081
default-soa-name=ns1.example.com # 改为 Primary NS 的地址
default-soa-edit=INCEPTION-INCREMENT
default-soa-mail=hostmaster.example.com # 改为默认服务器管理员的邮箱地址,并将 '@' 替换为 '.'
default-ttl=3600

DNS 服务器 (MySQL Slaves) PowerDNS 配置文件 /etc/powerdns/pdns.conf

1
2
3
4
5
6
7
8
9
10
11
config-dir=/etc/powerdns
daemon=yes
disable-axfr=yes
guardian=yes
include-dir=/etc/powerdns/pdns.d
launch=
security-poll-suffix=
server-id=ns1.example.com # 改为当前服务器的 ID,ns1/ns2/ns3/etc...
setgid=pdns
setuid=pdns
version-string=anonymous # 可以写任意字符串恶搞_(:з」∠)_

安装 PowerDNS-Admin

作者有提供详细的教程但是还是有坑

安装依赖:

1
# apt install git python-pip supervisor virtualenv python-dev libmysqlclient-dev libsasl2-dev libldap2-dev libssl-dev letsencrypt

创建数据库,切换到普通用户权限,clone 仓库到本地,然后一步一步操作即可。

1
2
3
4
5
6
7
8
$ git clone https://github.com/ngoduykhanh/PowerDNS-Admin.git
$ cd PowerDNS-Admin
$ virtualenv flask
$ source ./flask/bin/activate
$ pip install -r requirements.txt
$ pip install mysql gunicorn
$ cp config_template.py config.py
$ vim config.py

配置文件 config.py 中需要更改的地方:

1
2
3
4
5
6
7
8
9
SECRET_KEY = 'yoursessionencryptkey'
SQLA_DB_USER = 'yourdbusername'
SQLA_DB_PASSWORD = 'yourdbpassword'
SQLA_DB_HOST = 'localhost'
SQLA_DB_NAME = 'yourdbname'
PDNS_STATS_URL = 'http://localhost:8081/'
PDNS_API_KEY = 'yourapisecretkey'
PDNS_VERSION = '4.0.0'
RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CNAME', 'SPF', 'PTR', 'MX', 'TXT', 'SRV', 'NS', 'SOA']

然后执行 ./create_db.py。如果没有报错说明数据库安装成功,执行 ./run.py 即可访问 http://127.0.0.1:9393 看到登陆页面了。

部署 Web 服务

直接跑 run.py 当然不科学。Supervisor 配置文件 /etc/supervisor/conf.d/pdnsadmin.conf

1
2
3
4
5
6
7
8
9
10
11
[program:pdnsadmin]
command=/home/pdns/PowerDNS-Admin/flask/bin/gunicorn run:app
directory=/home/pdns/PowerDNS-Admin/
user=pdns
autostart=true
stdout_logfile=/var/log/supervisor/pdns-stdout.log
stdout_logfile_maxbytes=1MB
stdout_logfile_backups=2
stderr_logfile=/var/log/supervisor/pdns-stderr.log
stderr_logfile_maxbytes=1MB
stderr_logfile_backups=2

创建 DHParam

1
2
# cd /etc/ssl/certs
# openssl dhparam -out dhparam.pem 4096 # 如果性能不够请使用 2048

NGINX 配置文件 /etc/nginx/site-enabled/pdnsadmin.conf

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
server {
listen 80;
server_name dns.example.com;

location /.well-known {
default_type "text/plain";
root /var/www/html;
}

location / {
return 301 https://dns.example.com$request_uri;
}
}

server {
listen 443 ssl;
listen [::]:443 ssl;
server_name dns.example.com;

ssl on;
ssl_certificate /etc/letsencrypt/live/dns.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/dns.example.com/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4";
keepalive_timeout 70;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

ssl_dhparam /etc/ssl/certs/dhparam.pem;

add_header Strict-Transport-Security max-age=63072000;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;


access_log /var/log/nginx/dns.example.com.access.log;
error_log /var/log/nginx/dns.example.com.error.log;

location /.well-known {
default_type "text/plain";
root /var/www/html;
}

location /static {
alias /home/pdns/PowerDNS-Admin/app/static;
}

location / {
proxy_pass http://127.0.0.1:8000;
proxy_redirect default;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forward-IP $remote_addr;
port_in_redirect on;
server_name_in_redirect off;
proxy_connect_timeout 300;
}
}

记得把 dns.example.com 换成自己的域名。

签发 Let’s Encrypt。也不多讲。NGINX 配置中已经有了针对 Let’s Encrypt 的续期设置。

然后重启各项服务

1
2
# systemctl restart supervisor
# systemctl restart nginx

查看 PowerDNS-Admin 的运行状态,使用 supervisorctl status

添加 GLUE 记录

要使自己的 NS 生效,必须有保存在上级 NS 中的记录。很多域名注册商都提供了配置 GLUE 记录的功能,例如 Hexonet (1API):

Glue Records

简言之,需要把自己的 NS 服务器及对应的 IP 记录到上级 NS。完成之后,通过 PowerDNS-Admin 添加自己的域名,zone 类型为 NATIVE。然后添加所有 NS 服务器的 A/AAAA 以及所有的 NS 记录——你没听错,要自己写 NS 记录。其他域名也需要添加这些 NS 记录,否则不会托管。

Glue Records

收尾

全部完成之后就是一个完整功能的 DNS 服务了。如果希望启用 DNSSEC,需要在管理服务器中通过 pdnsutil 来添加 key。

由于目前 PowerDNS-Admin 没有限制不能添加提供的 NS 之外的名称服务器,所以其他域名按照添加 GLUE 记录的方法,也可以将这些 NS 服务器「变成」自己的 NS。

好了,不会说话了。讲效果——

一般来说,DNS 服务都会提供多台 NS 服务器域名,将域名的 DNS 改为这些 NS 服务器才能托管到该 DNS 服务上。但是现在只需要知道这套 DNS 的服务器 IP 地址,即可给自己的域名添加 GLUE 记录、NS 记录和 NS 对应的 A/AAAA 记录进而使用自己的域名作为 NS,而不需要用 DNS 服务的 NS 域名。当然一般就是看起来会比较厉害而已…

萌 DNS 已经年久失修。尽管一直有计划完全重写出一套应用目前各种 DNS 特性和优化的完整平台,但是目前的精力不允许。所以为了先让萌 DNS 的用户们至少先有一个能支持 Let’s Encrypt 的 DNS 服务,决定暂时舍弃 GeoDNS 功能,使用一套更加成熟的解决方案提供服务。

September 14, 2020 04:20 AM

September 10, 2020

中文社区新闻

Arch除虫日:9月13日帮我们一起清理 bug !

我们将于9月13日举办一次除虫日以解决大量还开着的问题。如果你当天不能参与,可以在那之前的任何时间帮助我们。

如何?

请复查所有你报告的工单,检查它们现在是否还能复现。如果问题已经消失了,请在 bug 跟踪页面请求关闭任务(request task closure)。否则请提供更多信息以便我们能解决这些 bug 。没有你们的回馈我们没法修复这些 bug 。

问题?

在9月13日来 irc.freenode.net 上的 #archlinux-bugs 频道参与活动。因为我们生活在不同的时区,不是所有开发者和 bug 清理者总能保持一直在线,但是你可以向任何在线的开发者提问。

以及请检查你的邮箱,可能包含对你开的工单的回复通知。

by farseerfc at September 10, 2020 12:37 AM

September 05, 2020

Alynx Zhou

GObject 备忘录

说来惭愧,我一直记不太清 GObject 到底是怎么用的,毕竟作为一个写过 C++ 和 Python 然后常用 JS 的人来说,面向对象的实现是非常自然的,不需要考虑为什么。所以我总是看着一大堆类型转换和分散的定义以及各种 chain up 感到眩晕。而 GObject 的文档写的也相当分散,有种管中窥豹之感。

有同学觉得其实把函数变成指针放在结构体里看起来就面向对象了,以前我也这么觉得,但是显然这样不能实现继承封装等等特性,GObject 独特之处在于它实现了这些,并且是和语言独立的。当然,要想在一个没有这些概念的语言里面做到这些,就有很多需要自己手动处理和函数库处理的地方,就是因为有些隐藏了起来有些又要自己做,才让它看起来像古老的黑魔法。

最近我尝试做了一个小项目,以便搞清楚如何按照 GObject 的模式编写一些继承封装的代码。

首先得给项目起名字,因为 GObject 要求你的命名符合一定的约定,其中一个前缀是项目名,不过这个 简单,就叫 test 好了,然后做一个基类叫 animal,于是就有了 test-animal.h:

#ifndef __TEST_ANIMAL_H__
#define __TEST_ANIMAL_H__

#include <glib-object.h>

G_BEGIN_DECLS

// 这个玩意必须要手动定义。
#define TEST_TYPE_ANIMAL test_animal_get_type()
// 这个玩意会展开成一大堆函数声明、typedef 什么的,
// 所以我们只要按约定定义结构体实现函数。
G_DECLARE_DERIVABLE_TYPE(TestAnimal, test_animal, TEST, ANIMAL, GObject)

// 但是上面那个玩意其实不会给你搞一个类结构体出来,
// 而且这个因为是给其他文件用的所以必须写在头文件里,
// 不然人家怎么知道你有什么虚函数!
struct _TestAnimalClass {
    GObjectClass parent_class;
    // 定义一个可以继承的函数。
    void (*print)(TestAnimal *animal);
    gpointer padding[12];
};

// 因为上面的 class 里面定义了,这个只是调用那个。
void test_animal_print(TestAnimal *animal);
// 这个玩意得手动定义,是个不可继承的公开函数。
TestAnimal *test_animal_new(gchar *animal_name);

G_END_DECLS

#endif

因为代码里写了很多注释所以我这里就不再啰嗦一遍了,说点里面没写的。

G_DECLARE_DERIVABLE_TYPE 表示你声明了一个可以继承的类,也就是说你需要自己弄一个虚函数表出来。有时候你会在某个 GObject 的项目里定义了一大堆宏(比如 GTK 就不爱用这个而是手动定义),其实它们和 G_DECLARE_DERIVABLE_TYPE 做了一样的工作,因为总要做,就写了个宏实现。这个并不会给你定义具体的类结构体(其实就是虚函数表,用来存放所有可以继承重载的函数),所以我们要按照约定自己写一个 _TestAnimalClass 的结构体,在类型名字前面加下划线作为结构体名也是约定俗成的,G_DECLARE_DERIVABLE_TYPE 会展开出一句 typedef struct _TestAnimalClass TestAnimalClass

这个类结构体约定第一个元素是它的父类型的类结构体——这其实意味着我们复制了一份父类型的虚函数表出来,于是我们就可以覆盖父类型的方法而不修改原本的父类型。这一句可能比较难懂,不过后面还有关联。

这个类结构体存放的并不是实例的变量,它有点类似于 JS 里面的原型,这样我们就不需要给每一个生成的实例复制一份虚函数了,它们共用一个虚函数表。

然后会有一个 test-animal.c:

#include "test-animal.h"

// Derivable 类型会自动帮你定义实例结构体,
// 所以你想夹带点私货就得自己搞个 Private 类型。
typedef struct {
    gchar *animal_name;
} TestAnimalPrivate;

// 这个也会展开一大堆声明什么的。
G_DEFINE_TYPE_WITH_PRIVATE(TestAnimal, test_animal, G_TYPE_OBJECT)

// 要想通过 new 函数直接初始化一些值就需要搞点属性。
enum {
    PROP_0,
    PROP_ANIMAL_NAME,
    N_PROPERTIES
};

static GParamSpec *obj_properties[N_PROPERTIES] = {NULL};

static void set_property_impl(
    GObject *object,
    guint property_id,
    const GValue *value,
    GParamSpec *pspec
)
{
    TestAnimal *animal = TEST_ANIMAL(object);
    TestAnimalPrivate *priv = test_animal_get_instance_private(animal);

    switch (property_id) {
    case PROP_ANIMAL_NAME:
        if (priv->animal_name)
            g_free(priv->animal_name);
        // 所以其实我们是在设置属性的时候更新私有成员。
        priv->animal_name = g_value_dup_string(value);
        break;
    default:
        /* We don't have any other property... */
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

static void get_property_impl(
    GObject *object,
    guint property_id,
    GValue *value,
    GParamSpec *pspec
)
{
    TestAnimal *animal = TEST_ANIMAL(object);
    TestAnimalPrivate *priv = test_animal_get_instance_private(animal);

    switch (property_id) {
    case PROP_ANIMAL_NAME:
        // 所以你看属性这个名称和我们想的属性不一样,
        // 其他语言里面属性就是成员,能存点东西,
        // 但这里好像属性只是成员的一个代理,
        // 具体的东西存在成员里面,通过属性设置。
        g_value_set_string(value, priv->animal_name);
        break;
    default:
        /* We don't have any other property... */
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

// 给子类提供一个默认的实现,这样它就不是一个纯虚函数了。
static void print_impl(TestAnimal *animal)
{
    TestAnimalPrivate *priv = test_animal_get_instance_private(animal);
    g_message("I am an Animal called %s.", priv->animal_name);
}

static void test_animal_class_init(TestAnimalClass *animal_class)
{
    // 首先我们覆盖这个类里面的 GObject 类的方法,
    // 你调用 GObject 的函数,他会先获取参数的 GObject 类,
    // 那就获取到我们这个了,然后具体的实现就是我们覆盖的这个。
    GObjectClass *object_class = G_OBJECT_CLASS(animal_class);
    object_class->get_property = get_property_impl;
    object_class->set_property = set_property_impl;

    // 给虚方法设置默认实现。
    animal_class->print = print_impl;

    obj_properties[PROP_ANIMAL_NAME] = g_param_spec_string(
        "animal-name",
        "Animal Name",
        "Name of Animal",
        NULL,
        G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE
    );

    g_object_class_install_properties(
        object_class,
        N_PROPERTIES,
        obj_properties
    );
}

static void test_animal_init(TestAnimal *animal)
{
}

void test_animal_print(TestAnimal *animal)
{
    // 你看其实这里和 GObject 类设置属性的原理是一样的。
    // 子类调用这个函数,实际上获取到的是子类自己那个 Animal 类,
    // 于是他只要覆盖自己的那个 Animal 类,调用的就是他自己的。
    TestAnimalClass *animal_class;
    animal_class = TEST_ANIMAL_GET_CLASS(animal);
    g_return_if_fail(animal_class->print != NULL);
    animal_class->print(animal);
}

TestAnimal *test_animal_new(gchar *animal_name)
{
    g_return_val_if_fail(animal_name != NULL, NULL);
    return g_object_new(
        TEST_TYPE_ANIMAL,
        "animal-name", animal_name,
        NULL
    );
}

理论上来说我们直接往实例结构体里添加成员作为私有成员就可以,但是在头文件里定义实例结构体,其它文件是可以看到实例结构体的成员的,所以 G_DECLARE_DERIVABLE_TYPE 不允许我们这么做。

但有一个省事的宏 G_DEFINE_TYPE_WITH_PRIVATE,它要求我们做一个 Private 类型,里面存放我们想要的任何私有元素,它不会被继承,并且因为定义在 .c 文件里所以也不会被其它文件看到。它还会提供一些 GObject 内置类型系统的实现函数(不然 GObject 系统怎么知道你建了哪些类型!),还有一些只在实现里面用到的定义。当然 Private 类型的名字也是约定好的。

多说一句,按照 GTK 的代码实现,其实 Private 最后就是在实例结构体里面定义了一个叫 priv 的指针,类型是自己定义的 Private 类型,因为 GTK 是手写的头文件定义然后用的 G_DEFINE_TYPE_WITH_PRIVATE 同时又没产生冲突,其实你也可以完全不理会这两个宏,全都自己写,只要保证类结构体和实例结构体的第一个成员是父类结构体和父实例结构体就可以了。但是 G_DEFINE_TYPE_WITH_PRIVATE 提供了一个 项目名_类型名_get_instance_private 的函数,我们就不用直接访问 priv 成员了。

在使用 g_object_new 新建一个对象的时候可以通过 key-value 的方式设置一些初始值,这个其实是通过 GObject 提供的 property 功能实现的,因为 C 并没有哈希表这种东西。这个过程很繁琐但也很固定,其实就是实现其它语言里面传一个对象作为构造函数参数然后以此设置私有成员初始值的功能。property 就是参数,具体的内容还是存在初始值里面的,不过其实你也可以通过 property 访问具体的值。这里很容易理解成 property 有自己单独的存储空间,其实不是。property 是可以继承的,所以子类可以同时设置父类和自己的 property。

然后我们要接触到第一个重载的部分了,因为 TestAnimal 继承了 GObject,所以我们要重载掉它类结构体里面的父类结构体的 set_propertyget_property 函数。过程也很简单,GObject 要求我们实现两个函数 项目名_类型名_class_init项目名_类型名_class_init,其中前者就是让我们初始化类结构体用的。首先进行一个类型转换把 TestAnimalClass 转换成 GObjectClass(为什么可以强转?那你先思考一下为什么定义类结构体时候第一个元素是父类结构体?就是因为要这样才能进行类型转换,本质上是个套娃),然后直接赋值。这样假如有人对我们这个类型执行 g_object_set_property,实际上是调用的我们重载过的函数(为什么?怎么做到的?往下看)。

那其实我们知道如何覆盖父类的方法,但运行的时候是如何动态重载到我们自己的函数的也不清楚,我们先搞定我们自己的那个虚函数,其实很简单,我们定义具体的方法的时候(指 test_animal_print),不要让它实现具体的逻辑,而是让它通过参数的实例执行虚函数表里的函数就可以了。这需要一个自动生成的 项目名_类型名_GET_CLASS 的宏,它的作用是通过一个实例查找到 这个实例本身 对应的类结构体,然后就可以运行虚函数了。比如我给 g_object_set_property 传一个 TestAnimal,那我们调用的其实是 TestAnimalClass 的第一个成员那个 GObjectClassset_property,这个已经被我们改成自己的了,于是就实现了一个 不那么直观的 重载。

当然假如我们不想写一个纯虚函数,可以在 项目名_类型名_class_init 里面设置一个初值,这样假如子类没有重载,调用的就是这个。

那你可能要问假如我有一个不想被重载的函数呢,那你就不要跳虚函数表了,直接写逻辑就可以了。

接下来我们终于可以写子类的,首先就是 test-cat.h:

#ifndef __TEST_CAT_H__
#define __TEST_CAT_H__

#include <glib-object.h>
#include "test-animal.h"

G_BEGIN_DECLS

#define TEST_TYPE_CAT test_cat_get_type()
// Final 类型就不用写类结构体啦,反正那个最大的用处是用来写可以继承的虚函数。
G_DECLARE_FINAL_TYPE(TestCat, test_cat, TEST, CAT, TestAnimal);

TestCat *test_cat_new(gchar *animal_name, gchar *cat_name);

G_END_DECLS

#endif

TestCat 是继承 TestAnimal 的,并且我们不想让它被继承,所以它就不需要写类结构体了(因为虚函数表是用来重载的,没有继承当然也没有重载)。

所以接下来就直接到实现部分了,在 test-cat.c:

#include "test-cat.h"

// Final 类型也没有 Private,但是它自己本身就是容器嘛!
// 所以实例结构体就给你自己写了。
// 当然别的文件不需要知道你有什么私货所以不要写在头文件里。
struct _TestCat {
    TestAnimal parent_instance;
    gchar *cat_name;
};

G_DEFINE_TYPE(TestCat, test_cat, TEST_TYPE_ANIMAL)

enum {
    PROP_0,
    PROP_CAT_NAME,
    N_PROPERTIES
};

static GParamSpec *obj_properties[N_PROPERTIES] = {NULL};

static void set_property_impl(
    GObject *object,
    guint property_id,
    const GValue *value,
    GParamSpec *pspec
)
{
    TestCat *cat = TEST_CAT(object);

    switch (property_id) {
    case PROP_CAT_NAME:
        if (cat->cat_name)
            g_free(cat->cat_name);
        cat->cat_name = g_value_dup_string(value);
        break;
    default:
        /* We don't have any other property... */
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

static void get_property_impl(
    GObject *object,
    guint property_id,
    GValue *value,
    GParamSpec *pspec
)
{
    TestCat *cat = TEST_CAT(object);

    switch (property_id) {
    case PROP_CAT_NAME:
        g_value_set_string(value, cat->cat_name);
        break;
    default:
        /* We don't have any other property... */
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

// 那我们不想要默认的实现就可以覆盖掉自己的。
static void print_impl(TestAnimal *animal)
{
    TestCat *cat = TEST_CAT(animal);
    g_message("I am a Cat called %s.", cat->cat_name);
    // 这个自动生成的 parent 变量指向的是真正的父类!
    // 因为我们自己带的父类结构体被我们覆盖了,所以这里给你一个访问正主的机会。
    // 它是个 gpointer 所以你得手动设置类型。
    // 其实你的子类如果是完全覆盖父类的功能其实我觉得不用写这一句了。
    // 但是有时候你的子类是在父类上面增加功能就得写。
    // 这不就是 super 指针嘛!
    TEST_ANIMAL_CLASS(test_cat_parent_class)->print(animal);
}

static void test_cat_class_init(TestCatClass *cat_class)
{
    GObjectClass *object_class = G_OBJECT_CLASS(cat_class);
    object_class->get_property = get_property_impl;
    object_class->set_property = set_property_impl;

    // 干掉我们自己的这个父类结构体里面带的内置实现,
    // 这样父类的函数调用的其实是我们自己的这个实现。
    // 那你看你一会让父类调用自己一会又让自己调用父类可真是麻烦。
    TestAnimalClass *animal_class = TEST_ANIMAL_CLASS(cat_class);
    animal_class->print = print_impl;

    obj_properties[PROP_CAT_NAME] = g_param_spec_string(
        "cat-name",
        "Cat Name",
        "Name of Cat",
        NULL,
        G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE
    );

    g_object_class_install_properties(
        object_class,
        N_PROPERTIES,
        obj_properties
    );
}

static void test_cat_init(TestCat *cat)
{
}

TestCat *test_cat_new(gchar *animal_name, gchar *cat_name)
{
    g_return_val_if_fail(animal_name != NULL && cat_name != NULL, NULL);
    return g_object_new(
        TEST_TYPE_CAT,
        "animal-name", animal_name,
        "cat-name", cat_name,
        NULL
    );
}

Final 类型没有 Private,因为它自己就是 Private,所以这个定义实例结构体的权力交给了你,直接在里面写私有成员即可,但是不要忘了套娃(指第一个成员一定要是父实例结构体)。

然后就是设置我们自己的 property 并重载 GObject 的虚函数,当然你可能会说这样父类型的 property 不就丢了吗?其实 property 是放在 GObject 系统单独的一个表里,需要的时候它去查询,并且会考虑父类型的继承链,所以我们不需要考虑重载时候覆盖掉父类型的问题。

接下来就是重载父类型提供给我们的虚函数也就是 TestCatClass.print 了!当然首先是把我们自己的类结构体里面的父类结构体的虚函数换掉。

在写我们自己重载的函数的时候,一个比较重要的事情是 chain up,也就是最后一行那个 TEST_ANIMAL_CLASS(test_cat_parent_class)->print(animal);,有时候你重载只是做了一些子类自己的处理,然后还是需要父类提供的处理函数的,这该怎么办?你可能会说类结构体里面不是有个父类结构体了吗?但那个已经被我们覆盖了!你这样不就是咬自己尾巴的猫吗?

当我们自己解决不了的时候就得 GObject 解决了,G_DEFINE_TYPE 会提供一个 项目名_类型名_parent_class 的指针定义,这个指向的是那个真正的唯一的 不是我们自己包含的 那个父类结构体!也就是说对于子类和父类继承而言,有两个父类结构体,一个是父类自己作为一个类的那个,另一个是子类为了实现重载包含的那个,这个访问的就是前者。

当然这个类型其实是个 gpointer 所以我们还得自己转换一下类型,然后只要调用方法就可以了(这不就是 super 指针嘛)。

最后我们写一个测试的程序 main.c:

#include "test-animal.h"
#include "test-cat.h"

int main(int argc, char *argv[])
{
    TestAnimal *animal = test_animal_new("animal1");
    TestCat *cat = test_cat_new("animal2", "cat1");
    test_animal_print(animal);
    test_animal_print(TEST_ANIMAL(cat));
    return 0;
}

然后用这个 Makefile:

CC := cc
OBJECTS := main.o test-animal.o test-cat.o
CFLAGS := `pkg-config --cflags gobject-2.0`
LIBS := `pkg-config --libs gobject-2.0`

all: ${OBJECTS}
    ${CC} -o test ${OBJECTS} ${LIBS}

main.o: test-animal.h test-cat.h
    ${CC} -c main.c ${CFLAGS}

test-animal.o:
    ${CC} -c test-animal.c ${CFLAGS}

test-cat.o: test-animal.h
    ${CC} -c test-cat.c ${CFLAGS}

.PHONY: clean
clean:
    -rm test ${OBJECTS}

运行的结果是这样的:

** Message: 11:23:25.727: I am an Animal called animal1.
** Message: 11:23:25.729: I am a Cat called cat1.
** Message: 11:23:25.729: I am an Animal called animal2.

你可以看到第一行是父类也就是 TestAnimalClass 的函数的输出。第二行是 TestCatClass 重载的函数的输出,第三行则是 TestCatClass 重载的函数 chain up 到 TestAnimalClass 的输出,但是输出的是子类继承的父类的属性!

最后有关我到底该声明成子类还是父类以及传参时候要不要类型转换:其实在这个 GObject 的样例里面看起来是无所谓的,类型转换可行不可行其实取决于具体 new 的是什么,而不是声明的指针是什么,所以其实声明成父类和子类都没有关系,类型转换也主要是为了满足 C 语言对指针类型和函数声明的要求罢了。

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at September 05, 2020 01:23 AM

August 24, 2020

liolok

Set Environment Variables Using pam_env

Why pam_env It’s a PAM module and sets environment variables as soon as user logs in. Bash, zsh or fish; Wayland or Xorg; no need to bother understanding their environment scopes, not as global as pam_env anyway. Why not May be too global if want set environment variable in certain scope like only under shell or GUI; Configuration files are pure text, not any kind of shell script; Require root permission since version 1.

by admin@liolok.com (liolok) at August 24, 2020 07:22 PM

August 18, 2020

Alynx Zhou

基于 GitHub Issue 的前端评论框

造轮子是病,得治。

自从造了 前端博客搜索引擎 的轮子之后,我对自己的能力有了极大的信心,同时也掌握了一些有趣的用法,于是把枪口瞄准了下一个我看着不顺眼的地方——评论框。

(这标题怎么写的和毕业论文似的!)

轮子也不是白造的。

从我建站以来我的评论框就命途多舛,Disqus 虽然是最著名的评论系统,但是在国内访问不太顺畅。多说火了一段时间之后便关门大吉,HyperComments 则在我用了一段时间后发邮件提示要收费了,于是我之前的评论便华丽流失。而对于 Valine 这种基于 LeanCloud 的评论系统,我对 LeanCloud 不甚了解所以也不想尝试(而且 Valine 现在似乎转向闭源了,当初也许是个正确的决定)。然后赶上 Gitment 和 Gitalk 火了起来,大家意识到 GitHub Issue 正是个放评论的好地方。但是由于 Gitment 和 Gitalk 采用他们自己的服务器实现博客评论框提交——转发到 GitHub API 的实现,以及 采用的 OAuth App 权限过高,有人怀疑这不太安全,于是我也没太敢参与。后来遇到 comment.js 这个项目,它绕过了提交评论的问题——直接给一个到 GitHub Issue 的评论框的链接就可以了。于是我就开始用这个,至于什么 utterances 这种用 GitHub App 降低权限的评论系统,我也懒得尝试了。

但我为什么决定替换掉 comment.js 我已经记不清楚了,可能是为了对主题的显示有更好的掌握吧,毕竟它带有自己的 CSS 样式而且经常和我的冲突,也可能是因为它迟迟没提供像 Disqus 一样查找每篇文章评论数目的功能,不过它现在已经不再维护了,所以我也算是未雨绸缪。

事情本该很简单。

研究了一下原理其实并不是很难,首先就是从 GitHub 的 API 上 ajax 获取数据,然后操作 DOM 添进去就可以了,所以我就先阅读了 GitHub API 的文档,总之还算容易,只要先获取一个仓库的 issue 列表,然后按照某种方法在里面查找相关的 issue,如果没有就渲染一个到新建 issue 的链接,否则获取该 issue 的全部评论并显示就可以了。

查找 issue 的 key 也算容易,我给它做成了函数参数,然后在主题模板里填一个每个页面唯一的字符串就行了,比如文章标题,然后新生成 issue 页面时候就把这个作为 issue 的标题,这样查找 GitHub Issue 页面时候也比较容易看。然后 GitHub 把 PR 也视为 issue,这个不要紧,收到数组之后过滤一下就好了。而显示数目我琢磨了一下其实也没什么难的嘛!我都获取到所有 issue 的信息了。做一个新的函数,主题作者在页面上放置一系列空的元素并设置好 class 属性,然后同样地把每个页面唯一的字符串设置成这些元素的属性(Disqus 也是这样的),加载函数时候把 class 作为参数传进去,分别从 issue 列表里查找对应的标题就可以了。还有一个奇葩问题是 GitHub issue 的 comments 指的是除了顶楼以外的评论,但很显然看起来不是这样的,这个也简单,直接把顶楼加到数组里就成了嘛。剩下就是艰苦的在浏览器里刀耕火种写 JS 拼 HTML 字符串发 ajax 请求写嵌套回调(没有 async/await 也太痛苦了吧!。

一切正常工作了一段时间一后我发现不太对劲,怎么评论太多的时候显示不全呢?仔细查了一下发现 GitHub API 是强制分页的,也就是说不管你怎么设置,最多一次只能获取 100 条评论,默认则是 30 条,本来我不太想给博客加评论分页功能的,现在看来是 GitHub 被迫让我加啊。当然这个并不是最痛苦的,最痛苦的是它查找仓库的 issue 列表的时候也是强制分页的!这就麻烦了,还记得我们之前说要获取到列表之后查找标题吗?获取不到完整列表还怎么查找啊!

当然你可以说按顺序多查几页不就行了嘛,这就是它分页难受的地方了!ajax 是异步的啊亲!你不会想让我一个 for 循环几个 ajax 还指望优雅的等他们结束了再跑查找吧!我知道能用 Promise.all() 解决,但是由于我大发慈悲的支持使用 IE11 的用户(微软我&A%¥S&……省略一千字儒雅随和),所以我的函数是基于回调的,那也没什么办法。而且这样首先你得读一下仓库信息才能知道有多少个 open 的 issue(没错只能算 open 的不能算 close 的!所以在后面查 issue 的时候我也不得不筛选掉 close 的,不过这大概也许是个 feature?),然后自己计算有多少页。最后我只能造了一个不那么优雅的尾递归回调(反正就那个意思),不过它工作的不错,这样我就可以获取到全部的 issue 列表了。

然后后面其实还是差不多,至于评论分页又不一样了,既然 GitHub 只有分页 API,我也就半推半就啊不是将计就计吧。我才不要继续获取全部评论了,我也每次直接获取 GitHub 那边的一页就行了,每页个数则由主题作者传参进来。至于如何确定我要哪一页呢?和搜索功能一样,继续前端解析 query string 呗。根据 issue 评论总数计算一下有几页,然后生成几个链接放在页尾,每次加载时候解析一下参数确定当前页是哪个即可。当然,不要忘了 issue 顶楼不算评论,计算分页的时候也不要给它加进去!而且既然是这么分页,我也懒得把顶楼算在里面了(不然真的麻烦的要死啊后面分页和每页个数全乱了),所以假如你设置每页 10 个评论,那第一页其实有 11 个,别烦我,代码在那,不满意自己改……然后继续刀耕火种……

为了减轻负担,我没有实现太多的功能,比如时间戳我没有搞成什么几分钟几小时前,这种东西又不清晰又浪费带宽,我只搞了基于 marked 的 Markdown 渲染(必须的)和语法高亮,Markdown 渲染不是必须的,因为你可以 设置 header 让 GitHub 直接返回 HTML。为了保证效果统一,这个 JS 只是建立了 HTML 布局,给每个元素添加了 class,具体的样式则完全是主题自己编写的,所以配合起来也比较好看。

最后的效果其实还可以,完整的脚本就是 这个网站在用的 JS,具体和主题的整合方法就慢慢翻 ARIA 的模板吧。唯一的缺点是 GitHub API 的频率限制太低,按照这个弱智的 issue 列表分页的话又不得不多一次读取仓库有多少 issue 的请求,假如你的 issue 太多估计也是问题?不过应该不会有那么多博文吧!我只有调试脚本的时候遇到过被 GitHub 提示等会的问题,所以对于访问者应该没什么影响。有影响以后再想解决办法(或者没办法)。

也许最好的办法是解决掉需求——要什么评论框?不就是破事水?如果有问题想联系作者电子邮件又不是不能用!(逃

其实你知道,烦恼(bug)会解决烦恼(bug)。

这一部分更新于 2020-08-17 10:34:00。

GitHub API 推荐用户 缓存之前的请求响应,然后根据缓存的响应的 Header 里面的 ETag 发送请求查询是否过期,若未过期则返回一个不消耗频率限制次数的 304 状态码。我心想这也简单,那就在前端搞一个缓存就可以了。

然后我搜索了一番找到了 CacheStorage,看起来它是唯一一个跨标签页的基于 Session 的正宗的前端缓存。但是很显然 IE 又不支持,而且这个 API 基于 Promise 并且只能缓存 Response 对象,也就是说没办法简单的通过在 XHR 的时候判断一下跳过不支持的情况,要 Polyfill 则需要引入完整的 Promise 和 fetch/Response,所以我们做了一个艰难容易的决定——是时候去掉 IE 支持了!

于是我把请求 API 的函数改成了如下操作:

let cachePromise = window.caches.open("cacheName");

// Fetching JSON with cache for GitHub API.
const cachedFetchJSON = (path, opts = {}) => {
  let cachedResponse = null;
  return cachePromise.then((cache) => {
    return cache.match(path);
  }).then((response) => {
    // No cache or no ETag, just re-fetch;
    if (response == null || !response.headers.has("ETag")) {
      return window.fetch(path, opts);
    }
    // Ask GitHub API whether cache is outdated.
    cachedResponse = response;
    opts["headers"] = opts["headers"] || {};
    opts["headers"]["If-None-Match"] = cachedResponse.headers.get("ETag");
    return window.fetch(path, opts);
  }).then((response) => {
    if (response.status === 200) {
      // No cache or cache outdated and succeed.
      // Update cache.
      cachePromise.then((cache) => {
        return cache.put(path, response);
      });
      // Cache needs an unconsumed response,
      // so we clone respone before consume it.
      return response.clone().json();
    } else if (response.status === 304 && cachedResponse != null) {
      // Not modified so use cache.
      return cachedResponse.clone().json();
    } else {
      // fetch does not reject on HTTP error, so we do this manually.
      throw new Error("Unexpected HTTP status code " + response.status);
    }
  });
};

当然理想很丰满现实很骨感,在不支持 CacheStorage 的浏览器里要 fallback 到不带缓存的版本,本来我以为很简单,但是……(下面开启吐槽时间。)

支持 IE 的前端的痛苦都是相似的,不支持 IE 的前端则各有各的痛苦。

为什么非 HTTPS + localhost 不能用 CacheStorage 啊,难道他们没考虑过在电脑上开发然后手机访问测试移动版吗?还是说他们打算在手机上起一个开发服务器?为什么 Firefox 在非 HTTPS 时限制 CacheStorage 的方法是在 Promise 里 reject 一个 Error 从而导致这个过程变成了异步的?为什么 CacheStorage 只能缓存 Response 而不是任意数据结构?Safari 不能完整支持 Response 对象也就算了,为什么移动版 Chrome 和 Firefox 也不支持?合着你们 fetch 返回的 Response 还不是 Response?这世界到底怎么了……

所以最后需要一个长长的基于 Promise 的判断加载函数:

// 加载评论的时候才加载缓存。
let cachePromise = null;

let fetchJSON = uncachedFetchJSON;

const loadCache = (name) => {
  // Unlike in .then(),
  // we must explicit resolve and reject in a Promise's execuator.
  return new Promise((resolve, reject) => {
    if (cachePromise != null && fetchJSON !== uncachedFetchJSON) {
      return reject(new Error("Cache is already loaded!"));
    }
    // Old version browsers does not support Response.
    if (window.Response == null) {
      return reject(
        new Error("Old version browsers does not support Response.")
      );
    }
    const testResponse = new window.Response();
    // Safari and most mobile browsers do not support `Response.clone()`.
    if (testResponse.headers == null || testResponse.clone == null) {
      return reject(new Error(
        "Safari and most mobile browsers do not support `Response.clone()`."
      ));
    }
    // Chromium and Safari set `window.caches` to `undefined` if not HTTPS.
    if (window.caches == null) {
      return reject(new Error(
        "Chromium and Safari set `window.caches` to `undefined` if not HTTPS."
      ));
    }
    window.caches.open("CacheStorageTest").then((cache) => {
      fetchJSON = cachedFetchJSON;
      cachePromise = window.caches.open(name);
      return window.caches.delete("CacheStorageTest");
    }).then(() => {
      return resolve();
    }).catch((error) => {
      // Firefox throws `SecurityError` if not HTTPS.
      console.error(error);
      return reject(new Error("Firefox throws `SecurityError` if not HTTPS."));
    });
  }).catch((error) => {
    console.error(error);
  });
};

不管怎么样现在这个网站在支持 CacheStorage 和 Response 的浏览器上(似乎也就桌面版 Chrome/Firefox……)是缓存 GitHub API 的结果了,打开 DevTools 切到 Network 面板可以看到 GitHub API 返回的是 304 而不是 200,其他浏览器则 fallback 到无缓存的 fetch。当然其他浏览器不包含 IE 咯。

由俭入奢易,由奢入俭难。

这一部分更新于 2020-08-18 12:25:00。

我后来又仔细想了想,其实要兼容 IE 还是有办法的,首先 fetchPromise 都有成熟的 polyfill,甚至 URLSearchParams 也有,只要写一段脚本在不支持的时候加载他们就可以了。然后去掉所有 IE 不支持的 ES6 特性,比如箭头函数、模板字符串、for…of… 循环以及 MapReduce(IE 竟然支持 constlet 真是惊到我了)。但是能做到并不意味着一定要做,人总是还要向前看的,现在是 2020 年,连罪魁祸首始作俑者微软都放弃了 IE,就算是照顾用户量,IE 用户也是可以忽略的那一部分了。既然我已经用 ES6 重写了,就不要想再让我为这种历史垃圾放弃我得到的好处了,从我开始写主题到现在丢掉 IE 支持也算是仁至义尽了,所以为什么不让这些用户支持一下 Firefox 呢?

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at August 18, 2020 04:25 AM

August 15, 2020

Alynx Zhou

前端博客搜索引擎

本来我的博客有一个前端搜索框,当输入文字时就在侧边栏展开搜索结果,虽然看起来很时髦,但也不能算是什么食用啊不实用的设计方式。而且一开始我觉得既然没有后端处理请求,也就没法单独打开一个专门搜索页面了。

但是这个设计引发了 @依云 的吐槽,有一个单独的搜索页面并且对每个搜索结果有专门的 URL 看起来还是个挺合理的需求。但第一我不知道怎么实现第二我一直觉得能用就行所以开始并没有改。不过依云给我发了 Python 文档的搜索实现,让我突然想明白其实按照标准的 HTML 和 JavaScript 是可以实现无后端的搜索引擎的,然后就动手实现了一个。

首先既然要搜索那还是得有个索引或者数据库,比较简单的方案就是把所有文章的标题 URL 和内容丢到一个 json 文件里面,这个功能在 Hexo 里可以使用 hexo-generator-search 实现,我也给我的 Hikaru 添加了这个生成器,用来生成 JSON。

首先第一件事是实现搜索跳转页面,这一步只要简单的使用 HTML 表单就能实现,首先将我的搜索框改造为如下格式:

<form action="{{ getPath("search.html") }}" method="get">
  <button type="submit" class="search-submit" aria-label="{{ __("search") }}">
    <i class="fas fa-search"></i>
  </button>
  <input type="search" id="search-input" class="search-input" name="q" results="0" placeholder="{{ __("search") }}" aria-label="{{ __("search") }}">
</form>

使用 button 而不是 input 的原因是我想用我的图标做搜索按钮,反正 <input type="submit"> 只是个特化的 button。理论上来说不放按钮靠回车提交也是 OK,但是觉得这样又会被某些用户批评不友好了……

搜索框就很简单,name="q" 表示生成的 query string 里 keywords 的 key 是 q,然后按照表单写法会被提交到 search.html,理论上来说搜索应该发 GET 请求所以就是 method="get",当然 POST 就实现不了复制链接查看搜索结果了。

这个表单就是标准的 HTML 表单,不需要用 JavaScript 处理。生成的 GET 请求的 URL 类似于 /search.html?q=xxx

然后接下来是处理请求了,既然是发送到 search.html,对于静态后端肯定是要返回这个页面的,那就得先创建页面,然后对这个页面进行特殊处理,这里我同样利用生成器生成一个 layout 设置成 search 的页面,然后就可以单独给它编写模板添加处理部分了。

search.html 加载之后是可以通过 window.location.search 获取到 query string 的,然后我加载我修改过的 search.js这里),用它处理搜索过程。同时在页面里添加了一个 container 用来放置检索结果。

search.js 其实没什么黑科技,毕竟我们的难点就是在无后端情况下处理关键词和数据库,关键词已经用 window.location.search 拿到了,解析一下然后只要通过 ajax/fetch 请求数据就行了,封装 ajax 的代码网上到处都是,实在不行用 jQuery 也成。

然后剔除重复关键词主要是为了优化一下性能,接下来我的解决方案就是简单粗暴 indexOf(),不要跟我提什么算法什么优化,短平快实现效果,我是个实用主义者,目前这一步其实还没有成为瓶颈。

然后对于原版文件我的改进主要是按匹配次数排序,文章出现关键词越多则排序越靠前,相对可以提高效率。

接下来使用正则表达式给关键词加上 <strong>,这样显示起来比较显眼。最后把字符串拼起来显示就好了。其实这里的算法还有点意思,比如假如两个关键词出现的位置中间大于多少个字符则插入省略号,否则合并两个的上下文,具体实现也可以参照代码。

如果有性能瓶颈的话,多半也会先出现在 ajax,不过目前我文章还没有多到加载不出来的情况,也许可以靠分块加载解决?

更新(2020-08-15 18:25:00):我给代码添加了简单的分块支持。由于这里需要主题和生成器约定好路径,不太适合让生成器自动生成路径,所以采用了一个简单的方法就是让用户在设置文件里手动指定几个 JSON 文件的路径,然后生成器只是读取一下配置,假如是数组就分块写到指定好的路径里面。然后前端查找的时候分别异步查找每个文件并合并排序结果,理论来说大概会有性能提升?

最后我加了个简单的 SpinKit 动画,在查询结束之前先跑一下提升用户体验。

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at August 15, 2020 10:25 AM

August 07, 2020

Alynx Zhou

给你的主题来点暗色!

我自己对暗色模式其实是没什么兴趣的,因为设计一种配色就已经让我绞尽脑汁了,还要我设计另一种。但是我也确实意识到暗色模式在晚上玩手机实在是很方便,而且做这个也很流行,于是我也做了一个,只是因为我能做到。

跟着系统变色就行了吗?

现在的系统大概都支持暗色模式(Linux 的桌面环境早就有这种设置了,Firefox 可以直接读取我的系统设置,Android/iOS 也都有暗色模式开关),浏览器也紧跟潮流提供了 @media查询属性(IE:那……是……什……么……)。理论上来说只要简单地在 CSS 里面查询然后编写修改颜色的代码就可以了,问题只是如何修改颜色比较轻松。

CSS 变量是好东西!

使用 CSS 变量 当然是最简单的解决方案了,就像我们平时编程一样使用变量作为 colorbackground 的值,然后在查询到暗色模式的代码块里给这些变量重新赋值,一切都十分简单有条理,而且最重要的是你不需要一个一个选择器查找有哪些需要变色的属性,所有的颜色变量都是放在一起的。

真是恨死 IE 这废物了!

虽然 IE 有很多不支持的选项,但是不支持 CSS 变量真是让我工作量剧增的一件事情。虽然我从不测试我的网站在 IE 上能否运行,但是在最新版 IE 上一般都是没问题的,因此我也会放弃一些最新版 IE 不支持的新特性,CSS 变量就是其中之一。

既然没有办法用 CSS 变量,那就只能自己一个一个找选择器下面和颜色相关的属性,然后给它们重新设置属性了,真是找的人头晕眼花啊。

可能有人会说你不是用 CSS 预处理器吗,预处理器不是也有变量吗?但是预处理器是在生成阶段把变量编译掉了啊!不方便到运行时(浏览器)里去替换变量。

我就是想在暗色浏览器里用亮色啊!

在全部重新调整过颜色并能看之后(其实就是把浅色的色块换成深色,颜色层次基本不变,背景图搞个反色,至于那些彩色的按钮标签我实在没精力重新配色了,把透明度调低一点就好了),我自己还是比较喜欢自己一开始设计的样子,但我又是个习惯电脑全局暗色的人,这怎么能忍!

CSS 的媒体属性不像一般的属性,只能是浏览器设置我们读取,没有办法用 JS 控制,于是也就没法简单地利用这个添加切换按钮。上网搜了半天也只有曲线救国的方案。

曲线救国

如果并不是想那么和系统的设置同步而只是给自己的网站添加切换的话,并不需要媒体查询。只要设计一个按钮给 <html><body> 添加删除 class/attribute 就行了。然后如果要和系统同步,在 JavaScript 里也有 相关的 API 可以做到查询和监听,在检测到变化的时候也修改 class/attribute 即可。

我选择的是给 <html>data-theme="dark"data-theme="light" 属性,不选 <body> 是因为 WebKit 那些该死的不遵循标准的 scrollbar 伪类,文档没有说他们到底依附哪个元素,我尝试得到的是 <html>。接下来只要把之前的 CSS 里面的媒体查询选择器改成 html[data-theme="dark"] 就行了。

不过还要注意继承关系,这样写的话有些属性并不是继承外面的,而是在这个选择器里面就近继承。比如假如修改了 html[data-theme="dark"] a 的边框,那 html[data-theme="dark"] a.cls 的边框会优先继承这个,而不是 a.cls。我知道有些人可能会笑我半懂不懂了,但是我确实遇到了这个问题,并且思考了一下找到了原因。

还有一个比较尴尬的事情,我给一些元素设置了 transition 用于 :hover 之后加一个渐变颜色的效果,现在暗色模式也是修改颜色,导致这些元素会比其他元素慢变一下,没什么好办法因为你分不开两种 color 变化。我的解决方案是一个一个找切换暗色模式时候会变属性的选择器,给它们也添加 transition。效果还不错,不过 Chrome 在处理这种 CSS 动画时候竟然会掉帧???

不管了,反正我用 Firefox,Firefox 效果好得很,完全不掉帧。

更新(2020-08-07 10:50:00):我怀疑 Chrome 想做新时代的 IE,其实并不是性能问题导致掉帧,WebKit 对于继承来的属性的 transition 存在问题,会导致不是同时变换而是有延迟的变换,效果糟透了,StackOverflow 上也有人遇到这个问题,看起来是 bug 并且不打算解决,而 Firefox 就没有这个问题。使用 CSS 变量在 WebKit 下效果会好一点,不过也不能给所有变色的元素加 transition,还是会卡,只能给 <body> 加一个,因为我的链接有 :hover 时变色的 transition,都是 color 就没办法在那时 transition 而暗色模式时候不 transition。总之由于 WebKit 的存在导致没法让全部元素同步 transition,只能近似。两权相害取其轻,还是让 IE 用户只能用亮色吧,我最后还是选择了 CSS 变量。

怎么你这破网站换个页面还要重新点一次?

一切看起来都十分美好,直到我把一个暗色页面切成亮色然后点了个链接,下一个页面并不会理会我们上一个页面设置了什么主题,又变成了暗色。每个页面点一下切换按钮也太烦人了,我们得来点持久化。

某个域名想在用户的浏览器里存点是完全可行的,使用 localStorage 就行了,就是简单的键值对。但是这样我们就有了多种可能切换亮暗的动作:localStorage 里面存的选项,网页加载时浏览器媒体查询的结果,用户点了网页上的切换按钮,用户点了系统切换亮暗的设置。这些的判断顺序要好好处理一下,不然某些就会被忽视掉变成“我点了怎么不动啊!!!”。

经过我考虑一下之后,这个玩意的逻辑应该是这样的:

  1. 假如 localStorage 里面没有值,用户是首次打开网站,此时读取媒体查询按照系统的主题设置。
  2. 否则说明用户之前打开过网站,已经有他自己的喜好了,按照 localStorage 里面的值设置。
  3. 上两个步骤结束之后注册一个媒体查询监听器,用于响应用户修改系统设置。
  4. 注册一个按钮监听器,用于响应用户点击网页切换按钮设置。

以上 1 和 2 的顺序不能反了,并且每一个设置动作里都要把这次设置的值写入 localStorage 用于后续加载用户的选择。

现在看起来一切都很满意,是时候发布了!

用户:啊!我的眼睛!

我躺在床上用手机测试的时候又发现了一个问题。因为网页在生成的时候总有一个初始状态(JS 要等到 document 加载完成才开始处理 DOM,不像那些单页应用),假如我设置成暗色然后切换到别的页面,网页就会以亮色加载然后变成暗色,用户在晚上看起来就像是个闪光弹(伏拉什棒!)。

其实没什么好的解决办法,因为这是传统 HTML 页面的限制之一,我也不想找有没有什么新的东西能解决这些问题,最后的方案其实相当简单,既然亮色到暗色会让人受不了,我搞成暗色到亮色不就行了。

于是就是把模板里按钮的初始状态修改一下,渲染的时候出来的是 data-theme="dark",假如用户选择亮色,加载页面时会有一个暗色到亮色的变化。反正我自己都不在意。

更新(2020-08-06 18:23:00):我后来阅读了一些其它主题的代码,看它们是怎么在不使用单页应用的前提下解决这个问题的,结果方法相当简单,我自己也想得到:不用等到 DOMContentLoaded 事件之后,反正只是修改 <html> 标签,直接在 <script> 标签里面编辑 document.documentElement 是可以的,因为反正加载到 <script> 的时候肯定也已经加载到 <html> 了。所以就把修改这个属性和修改按钮的 DOM 分开了两部分,并且添加了一个 storage 的监听器,这样假如打开了多个页面,一个页面切换其它页面也会跟随切换。

不要过度设计!

经过这么一大堆折腾(代码行数++++++++),我甚至在想要不要支持每个页面单独设置亮暗初值,反正只要添加一个 front matter 然后在模板里判断一下嘛。不过后来想了想,就算有这么个功能又有什么用?实际意义几乎为零,徒增复杂度,所以还是不要过度设计了。

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at August 07, 2020 02:50 AM

August 04, 2020

Alynx Zhou

和 cheerio 说再见!

我早就想把 cheerio 从 Hikaru 的依赖里移出去了,倒不是我对他的功能有什么不满,但是一年不更新 NPM 上的包也太恶心了吧!

我的生成器使用 cheerio 的地方主要有两个,一是给标题生成喵点并依次生成 TOC,二是检查文章里相对路径的图片和链接引用并改成绝对路径,否则如果截取之后放在首页的摘要包含图片的话,会因为当前页面的地址变化而不工作。后者也是我自己编写生成器的原因之一,就是因为像 Hexo 或者 Hugo 这种已有的生成器内部并没有考虑,只能通过插件解决,我在写 Hexo 主题时嵌入了一个使用正则表达式的脚本,但相比之下我觉得还是一个能理解 HTML 的库更加可靠。所以后来我选了 cheerio 来做解析和修改以实现这些功能。

但是 cheerio 有一个 很古怪的 bug,存在于它目前在 NPM 上最新的版本 1.0.0-rc3 上。假如你使用 cheerio 解析并编辑包含中文的 HTML 的话,导出字符串时所有的中文都被编码成了 HTML 实体,导致进行下一步处理比如用 substring 截断的话,字符串长度变化了,而且可能会在文字中间截断。假如你使用 decodeEntities: false 的话,原本文档里的 &lt; 一类的字符反而会被 cheerio 导出成 <,变成一团乱麻。

这个 bug 非常古怪并且我花了一段时间来研究它,上一个版本 0.22.0 使用 decodeEntities: false 是没有这个问题的。最后发现原因是 cheerio 在 1.0.0-rc1 开始引入 parse5 代替原本的 htmlparser2 作为默认的 HTML 解析器,而 parse5 在解析时并不会使用 decodeEntities 这个参数,比如你输入 &lt;,parse5 解析时会在节点里存储原本的值 <,但 cheerio 在序列化的时候还是会依赖这个参数进行编码,所以假如传递 false 进去,cheerio 就不会主动进行 encode,导致最后出来的是 <。而 htmlparser2 会按照这个参数选择解不解码,所以也不会发生这种错误。

那这么看起来是 parse5 的问题?并不!parse5 有自己的序列化函数,和自己的解析函数是配套的,所以只要使用 parse5 的序列化函数就不会存在这个问题了。

最后总结起来修复的办法也有很多:首先其实序列化的时候并不需要将所有元素都编码成 HTML 实体,只要对几个字符进行转义即可,我提交了这样的 PR,但因为 cheerio 和它自己的序列化库 dom-serializer 是两个仓库,不是很好处理,而且其实也没有从根源上解决 htmlparser2 和 parse5 表现不一致的问题。cheerio 后来的提交中采用的是简单办法,假如使用 parse5 解析就继续使用 parse5 序列化就好了。

但是这个提交之后并没有发布到 NPM 上,NPM 挂的一直是有问题的 1.0.0-rc3 版本,至于原因呢很简单,他们打算释出第一个稳定版本 1.0.0,所以要等到所有 TODO 都解决了再发新版本!

搞毛啊老哥,你这样放着有 bug 的版本是把用户做宝搞吗?而且你是个函数库,下面好多人依赖你处理 DOM 呢,NodeJS 上提供 jQuery 模式的函数库大概也没什么别的替代品了,一堆项目在 issue 里问你就给个这破理由?

我选择的是锁死 0.22.0 版本,其他下游的项目也都各自做了 workaround,要么安装 GitHub 上的 1.0.0 分支要么回退版本。但总之都让强迫症很不爽啊!

大家等他发稳定版就这么等了一年,在这一年间有些项目比如 Hexo 直接抛弃了 cheerio 用正则处理 HTML,虽然他们主要是为了性能。我倒不是那么在乎生成器的性能(真的在乎的话我就该去用 Hugo,而且我也不敢说我自己的代码写的很好)。

晚上睡觉前我突然想到假如只是过滤链接和图片并检查他们的属性的话其实不需要 jQuery 一样的 API,只需要能理解 HTML,那找个简单的解析器就可以了。而且第二天正好读了 卷老师的这篇博文,发现和我想的也差不多。于是就开始动手。

卷老师用的 sanitize-html 对我来说不太合适,因为我的静态生成器并不需要过滤内容——都是 Markdown 生成的,而且不安全也是使用者自己故意写的,不是我的责任,生成器也没有不安全的 HTML 的运行环境。sanitize-html 和 cheerio 用的都是 htmlparser2 作为解析器,虽然它号称自己是跑得最快的 HTML 解析器,但经过之前那个问题我还是心有余悸。而且 htmlparser2 是非常简单的基于事件回调的解析器(就像 Python 自带的那个,我太久不写 Python 不记得叫什么了),不会给你构建树状结构也不包含序列化,自己写序列化就容易像之前那个 cheerio bug 一样出问题。所以我考察了一下 parse5 发现还不错,会构建树状结构,数据结构都写在文档里,同时就是简单的可以直接操作的 Object,至于速度虽然慢一点但我并不太在乎那 10 毫秒。

于是我就先手动造了一些 wrapper 函数比如递归前置遍历一颗树来代替 cheerio 的 $.each(),前置遍历正好是 HTML 文档自上往下的顺序,生成 TOC 的时候也是按照这个顺序来的,然后就是比如获取结点内文本的函数、获取属性和设置属性的函数之类的。至于插入结点我想了个巧妙的办法,因为 parse5 把最终解析到的文本作为单独的结点,我就直接在插入文本的函数里让 parse5 解析输入的 HTML,然后用得到的子结点替换被插入结点的子结点即可。

随后我重写了生成 TOC,生成标题 ID 和检查文章里相对路径的图片和链接引用并改成绝对路径的函数。然后写了点简单的测试样例跑了一下发现没问题,就用它们替换掉 Hikaru 的 utils.js 里面用 cheerio 的版本,然后修改了 process 代码。我造的这几个简单的 wrapper 完全符合我的要求,于是提交打版本号发布一气呵成,一年不更新的 cheerio 就从我的生成器里拜拜了。

虽然嘴上说着不追求速度,但是凭我肉体的感觉还是多少快了一点儿,不过我也并不是很在乎生成的时间,感知真的不强,无所谓了。但我真的不能理解这种为了憋个大的不发版本把用户做宝搞的行为,就算是发个 1.0.0-rc4 也比一年不发强吧!就算只是 make user happy 也好,何况面对的不是 user 而是其它项目的 dev 呢,都是同行,这行为算不算托大?反正我是习惯做了点改动就打个新版本,即使我自己安装的都是开发版本,我也希望能把我最新的修改送到用户手上,又不是 breaking change 或者什么不能随便更新的软件,打个小版本号至于那么难吗???

Alynx Zhou

A Coder & Dreamer

by Alynx Zhou (alynx.zhou@gmail.com) at August 04, 2020 01:06 PM

July 28, 2020

中文社区新闻

AUR服务器迁移:新的 SSH HostKeys

因为我们已将 AUR 迁移到了一个新的服务器,通过 SSH 连接服务器时的 HostKeys 有所变化。以下是新的 key 指纹:

Ed25519: SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4
ECDSA: SHA256:uTa/0PndEgPZTf76e1DFqXKJEXKsn7m9ivhLQtzGOCI
RSA: SHA256:5s5cIyReIfNNVGRFdDbe3hdYiI5OelHGpw2rOUud3Q8

以上指纹也可以在 AUR 主页上未登入时看到。

by farseerfc at July 28, 2020 01:17 AM

July 23, 2020

frantic1048

Pulchra - 四糸乃

原本去年就已经收到了的四糸乃,结果因为左臂脱胶掉下来了就换了个货,这一换就是一年多,客服感觉都换了一波又一波。最近总算是到了,这次手臂很稳,非常科学。

四糸乃帽子是带磁吸的,很方便把帽子固定住,结果角度没注意好,回头才发现帽子上的蝴蝶结一直没拍到 ˊ_>ˋ

Yoshino Yoshino Yoshino Yoshino Yoshino

Yoshino Yoshino Yoshino

Yoshino Yoshino

July 23, 2020 12:00 AM

July 22, 2020

百合仙子

Linux 的环境变量怎么设

本文来自依云's Blog,转载请注明。

最近,Arch Linux 的 pam 包将更新到 1.4.0,然后因为一个字符的变化,不少中文用户都开始避难了:pam_env 将默认不读取用户的环境变量设置。许多中文用户使用~/.pam_environment文件来配置 fcitx 输入法所需要的那三个环境变量。更新之后,这些配置将不再生效,意味着他们可能无法输入中文了,于是大家热烈讨论现在要在哪里配置环境变量比较好。

当然了,在 pam 的配置文件里加上user_readenv=1就可以恢复原先的行为。只是,pam 的开发者这么改当然是有原因的:CVE-2010-4708。简单地说,就是在鉴权模块里设置用户指定的环境变量不安全,怕被提权。

2020年08月20日更新:Arch Linux 打包时添加了以上这个配置

当然啦,桌面用户完全可以去改系统级的环境变量设置,只要你愿意让你个人的配置信息跑到系统配置那边去的话。

正文

用户经常需要设置一些环境变量,比如输入法,比如语言和地区选项,比如自定义的 PATH,等等。然而 Linux 的环境变量又不像 Windows 那样有一个专门设置所谓「系统环境变量」的地方,而是所有环境变量全部由子进程继承父进程获得,这么一路继承下来的。于是,想要给所有想设置上的进程都设置上,就不是一件简单的事情了。

这里关注的是桌面用户,自然首先从图形界面说起。

使用 X11 的桌面环境,通常通过 display manager 来登录,比如 lightdm 和 sddm。这俩都支持 ~/.xprofile。这个文件会在启动过程中被 source,使用的 shell 是由 dm 自己确定的。lightdm 和 sddm 都是用的 /bin/sh(分别位于 /etc/lightdm/Xsession 和 /usr/share/sddm/scripts/Xsession 文件里)。可以看到,除了读取 .xprofile 外,lightdm 也会读取 .profile。sddm 甚至连 bash、zsh、tcsh、fish 的启动配置脚本都给读了。

对于 Wayland,我这儿只有 sddm,类似的,也是各种 shell 的都给你读了。(当然就不读 X11 相关的东西了。)

而对于手动 startx 的用户,~/.xinitrc 里自己写上呗。还有 VNC 啥的,也都可以自己看启动脚本找到方法。

没有图形界面的登录,通常是从 tty 或者 ssh 登录。这两者都是登录到命令行 shell。写在 shell 配置里就可以了。具体到 zsh,我是写到 ~/.zprofile,这样不管是否交互,只要是登录 shell 就执行,不登录的大概已经继承到了。另外有些情况可能写这儿也没有用,比如 cron 调用的时候。非要搞的话其实可以写到 ~/.zshenv,这个基本上所有的 zsh 都会读,只能被系统级设置或者命令行参数禁用掉。而如果是 bash 用户,.bash_profile .bash_login .profile 这仨,会读找到的第一个(sddm 也会按这个顺序找,但是 lightdm 只会找 .profile)。Arch Linux 现在默认会给新用户建立 .bash_profile,我建议 bash 用户把它改名成 .profile。这里有个图 ,大致上说明了 bash 和 zsh 不同情况下会读取的配置文件

systemd 用户实例也有一份环境变量,用于通过 systemd 启动的用户级服务,通过 systemctl --user 的子命令可以查看和更新。D-Bus 也有一份,可以通过 dbus-update-activation-environment 命令来更新。这俩通常不需要用户操心,桌面环境应该会维护好。

如果你用 tmux 的话,那么 tmux 还有一份,用于新创建的窗格。tmux 会在被连接时更新 update-environment 选项指定的环境变量。当然你也可以用 tmux setenv 子命令去更新。这个尤其值得注意,因为 tmux 可能在不同的环境中被连接,导致环境变量混乱(比如 X11 下没有 DISPLAY 变量)。

要注意的一点是,不同的登录过程并不是只会读取自己专属的配置文件,尽管大家都在努力,也并没有完全统一好用的配置文件。所以有些环境变量,你可能想尽量避免重复设置(比如冗长重复的 PATH 项就很讨厌)。好在这些都是 shell 脚本,不光能设置环境变量,也能做条件判断。

要查看当前进程的环境变量,可以用 env 命令。要查看别的进程的,去读它的 /proc/PID/environ 文件,或者开个 htop 然后在进程上按 e 键。要看看某个环境变量在不同进程里是什么情况,可以用我写的小工具 compare-env

Linux 软件生态的特点就是这样,没有谁规定一定要用怎样的方式做一件事情,所以大家的想法总会有些出入,就导致设置个环境变量还有这么一大群配置文件(systemd 还有一份呢,我还没读没试所以没说)。但另一方面,理解它们也是十分容易的:不用猜测,不用撞大运般地耗费时间去尝试,顺着代码摸过去,总能弄明白软件的行事逻辑。即使文档像 pam_env 那样前后矛盾,也有源代码这条路可以精确刻画软件的行为。

by 依云 at July 22, 2020 02:59 PM

July 01, 2020

ヨイツの賢狼ホロ

给 GNU/Linux 萌新的 Arch Linux 安装指南 rev.B

给会用点 Windows 的彻头彻尾的 GNU/Linux 新手的 Arch Linux 安装指南 😣

为啥要搞这个?

因为 ArchWiki 上的 Beginner Guide 已经和 Installation Guide 合成一个啦😂, 然后有小白开始抱怨看不懂啦(误 (╯・∧・)╯ ┻━┻

其实咱最早看的也是 Beginner Guide ……

算了概念用到时再解释 😂

我是一个彻头彻尾的Linux新手,我应该用Arch吗?

如果你是新手,要使用 Arch 就必须愿意花时间学习新系统,接受 Arch 是一个 DIY 的系统,每个用户都是自己系统的组建者。

在开始问任何问题之前,自己先通过Google、Wiki或者论坛进行搜索。我们为你创建了这些资源并让你可以随时访问,上千志愿者为你提供了大量的信息资源。

推荐阅读: Arch terminology#RTFM

首先说点废话

  • Arch 这种经常更新内核的还是适合上实体机……

  • 看得懂英语的话最好还是看官方的 Installation Guide 吧(人家更新的比咱要勤快):

    https://wiki.archlinux.org/index.php/Installation_guide

  • 这里的目标是装好最简单的一个带有桌面的 Arch Linux ,对于部分特殊需求(例如无 GUI 的环境), 请直接参阅上方官方 Wiki 的安装指南装出基本系统,然后再参阅其他的文章呗~

备份……

万一手抖格错了盘别抱怨 GNU/Linux ……

但如果汝有备份的话,是不是已经下定决心把整个硬盘格式化掉了?(雾)

下载 ISO

https://www.archlinux.org/download/

BT 种子和磁力链接在上面,直接下载的话往下拉,找 China 下面的镜像网站挑一个下载就好。 (っ╹ ◡ ╹ )っ

如果是用 HTTP/HTTPS 下载的话,下载完以后最好验证一下文件的散列值来确定文件下载完了 (╯艹︿艹)╯ ┻━┻

Windows 上这样的工具有很多呐,例如 HashTab 和 FCIV。

但是如果汝正好没有这些工具或者从来没听说过散列(或者另一种译名叫哈希)的说法的话,可以用 Windows PowerShell 来完成:

# 例如获得某一个文件的 md5 散列值,用汝下载的 ISO 的路径换掉 <filepath>
Get-FileHash <filepath> -Algorithm MD5

# 尽管 md5 和 sha1 已经被认为是不安全的散列算法了(容易发生两个文件不同但散列值相同的情况,
也被称作散列碰撞”,然而 Arch Linux 的官方网站上还是只有这两种 😂, 先凑合着用吧……

至于下面那个 PGP 签名,又是个大坑,先鸽了(咕咕咕……)

确定启动类型

  • 首先打开设置 ( Windows 8/8.1 叫做 "电脑设置"),然后通过 "更新和恢复" -> "恢复" -> "高级启动" 重启电脑.

    Windows 10 的话,按住 Shift 再点击电源按钮里的重启也行……

如果是 UEFI 启动的话,大概是这个样子:

UEFI 系统启动之后大概像这样

没错就是有个 "使用设备" 的选项 😂

  • 或者同时按下键盘上的 Windows 徽标键(就是有 Windows 标志那个) 和 R 键,会打开“运行” 对话框。
“运行”对话框

在里面输入 msinfo32 然后回车(按 Enter 键)确认,打开”系统信息”应用。

“系统信息”窗口

看“BIOS模式”里是不是 UEFI 😂😂,还有下面那个 “安全启动状态”是不是“以关闭”(咱这台电脑的 UEFI 太旧所以显示的是不支持)

如果安全启动是打开的还需要自己进 UEFI 固件设置里手动关闭 😂

具体怎么关因为每种电脑的方法不一样于是汝要自己 STFW (Search the f**king Web,搜索一下) 了😂
  • 再或者打开磁盘管理(Windows 8 以后的系统可以通过按下 Windows + X 的菜单里找到 “磁盘管理”)
磁盘管理在这~

嗯,大概就是这样子的呗 (虽然具体的磁盘分区可能和咱的不一样)

大概长这样~

看汝的硬盘上有没有一个 EFI 系统分区 😂😂😂


还是搞不懂的下面也不用看了,准备下最后的晚餐吧 😋 (误

在硬盘上准备一块空闲空间

不然要把 Arch Linux 装到哪里去呐?

这里拿来演示的是 Windows 7 以后都自带的 “磁盘管理” 程序,应该能解决大多数问题 _(:з」∠)_

  • Windows 8 以后的系统可以通过按下 Windows + X 的菜单里找到 “磁盘管理”
磁盘管理在这~
  • 嗯,大概就是这样子的呗 (虽然具体的磁盘分区可能和咱的不一样)
大概长这样~
  • 汝哪个硬盘分区比较空闲? 右键点击它,有一个"压缩卷的选项"
”压缩卷“ 在这~
  • 输入压缩的大小 _(:з」∠)_
多少
  • 然后就多了一块未分配的空间 😂
多了一块未分配的空间

如果汝的硬盘分区有些刁钻而磁盘管理没法解决的话,AOMEI 家的分区助手不错, 这是官方网站 , 这是分区教程

制作启动盘

但前提是汝的电脑能从 U 盘启动 😂 (不过最近几年生产的电脑都应该可以了吧……

Windows 下咱比较推荐一个叫 rufus 的软件,官方网站在这

下载完以后双击运行,需要管理员权限,记得看有没有数字签名。(有数字签名时用户账户控制的对话框是蓝色的)

Rufus 自带多国语言(当然也包括中文啦),如果汝系统语言不是中文的话,点击那个地球图标就可以修改语言了啦~

选择语言

然后戳有点像光盘的按钮选择刚下载好的 ISO 镜像

选择映像

然后选择一种启动类型,UEFI 就选最后一个,不是的话就选第一个。

选择启动类型

写入方式选推荐的就好 (´_`)

选择写入方式

确认(要知道汝按下确认以后就没有回头路了,所以记得提前备份 U 盘上的资料 😂)

确认

然后坐等完成,完成以后汝的 U 盘卷标应该是 "ARCH_201610" 这样的 (后面四位年份和两位月份),不要改成别的,万一不对记得照 ISO 改回来 😂😂

准备启动

重启电脑,然后让电脑从 U 盘启动。

具体怎么搞还是要看电脑的硬件啦 😂

大多数的电脑都有一个在开机时按下一个按键来选择从哪里启动的选项(例如 Dell 和 ThinkPad 是 F12)。 UEFI 的话, 刚才提到的那个“使用设备” 的选项也可以。

还是去翻一翻汝自己的电脑生产商的说明书更快一点 ……

  • MBR 成功启动以后像这样
MBR

选第一项。😂 (除了 CPU 不支持的都应该用x86_64 😋)

后来 Arch Linux 不再支持 i686 啦,所以这里应该只会看见 x86_64 那一项(但是咱懒得换图片啦……)
  • UEFI 成功启动以后像这样
UEFI

还是选第一项。😂

然后等待一会以后会出现……

root@archiso ~ #

这就表示已经启动完毕啦 ~(>_<~)

root是用戶名,前面那個數字是上一個命令的exit status啦,如果正常結束的命令exit status是0,就不會顯示出來,你有 1 2 127 這種都是某種東西報錯了.

----现任 Arch Linux TU 之一的 farseerfchttps://www.zhihu.com/question/45329752/answer/98733823 中写到……

联网

因为 ArchISO 就是个重启就没了的 Live 环境,安装时需要的软件包都要另外下载。

首先当然是联网啦,如果是自动获取 IP 地址的有线网络,那么应该啥也不用做,ping 一下试试?

root@archiso ~ # ping archlinux.org
# 汝应该会看到像这样的东西 ……
PING archlinux.org (138.201.81.199) 56(84) bytes of data.
64 bytes from apollo.archlinux.org (138.201.81.199): icmp_seq=1 ttl=49 time=361 ms
……

(把电脑用网线接到家里的路由器上就有相同的效果)

如果没网的话…… 😂


  • 先用 ip link 确定一下网卡
root@archiso ~ # ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp4s0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN mode DEFAULT group default qlen 1000
    link/ether c8:9c:dc:a8:ab:c3 brd ff:ff:ff:ff:ff:ff
3: wlp0s29u1u1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DORMANT group default qlen 1000
    link/ether 44:94:fc:0f:63:b9 brd ff:ff:ff:ff:ff:ff

这个例子里,lo 是本地环回不用管它,enp 开头的是有线网卡,wlp 开头的是无线网卡。

如果汝明明有无线网卡却没识别的话,有可能汝是某无线网卡厂商受害者😂😂

这时可以:

  • 有 Android 手机的话,手机连 WiFi ,然后用“USB 网络共享”共享给电脑。
  • 找个 USB 无线网卡插上 😂
  • 连有线😂😂
  • 如果有无线网卡的话,试试连接到 WiFi ……

    2020 年 7 月的 ArchISO 拿掉了 netctl,于是经典的 wifi-menu 也被 iwctl 取代了。

    如果汝之前也能熟练使用 wpa_supplicant 啥的那当咱没说……

# iwctl 命令来进入 iwd 的 shell。
root@archiso ~ # iwctl
# 以下的操作都在 iwctl 的 shell 中,输入 help 来获得可用的命令。
[iwd]# help
# 列出无线网络设备
[iwd]# device list
# 用所指定的设备扫描网络(用上一步的设备名称替换 device)
[iwd]# station device scan
# 或者列出所有的网络
[iwd]# station device get-networks
# 连接到网络, SSID 就是汝要连接的网络的 SSID 啦
[iwd]# station device connect SSID
# 如果所连接的网络需要密码,则接下来会提示输入,汝当然也可以直接在 iwctl 命令中输入
root@archiso ~ # iwctl --passphrase passphrase station device connect SSID

谁叫 Arch 连不上网的话都装不了 😂

另外,如果汝连接的网络需要网页登录(Captive Portal),可以用 elinks 碰碰运气 😂

# 用汝的门户的 URL 替换 <your_captive_portal_url>
# 如果不知道的话,随便访问一个 HTTP 网站试试,应该就会被重定向到 Portal 了
root@archiso ~ # elinks http://<your_captive_portal_url>

时间同步

timedatectl set-ntp true 保证时间同步 。

root@archiso ~ # timedatectl set-ntp true
root@archiso ~ # timedatectl status
    Local time: Fri 2016-10-28 17:39:42 UTC
    Universal time: Fri 2016-10-28 17:39:42 UTC
    RTC time: Fri 2016-10-28 17:39:42
    Time zone: UTC (UTC, +0000)
Network time on: yes
NTP synchronized: no
RTC in local TZ: no

因为有不少操作需要准确的时间呐,例如 HTTPS 和 GnuPG 都需要准确的时间来验证证书的有效性。

但是如果因为各种原因没法同步的话,那就只好手动设置咯~

# timectl set-time "yyyy-MM-dd hh:mm:ss"
root@archiso ~ # timectl set-time "2016-10-28 17:39:42"

准备硬盘空间

这里用 cgdisk (UEFI)/ cfdisk (MBR) 来给硬盘分区。

首先输入 lsblk 看看汝的硬盘是哪个设备:

root@archiso ~ # lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda      8:0    0 463.9G  0 disk
├─sda1   8:1    0   512M  0 part
├─sda2   8:2    0    16M  0 part
├─sda3   8:3    0 103.4G  0 part
└─sda4   8:4    0 253.4G  0 part
sdb      8:16   1   7.5G  0 disk
└─sdb1   8:17   1   7.5G  0 part /run/archiso/bootmnt
sr0     11:0    1  1024M  0 rom
loop0    7:0    0 346.1M  1 loop /run/archiso/sfs/airootfs

如果那是一块新硬盘或者已经清空的硬盘的话,汝大概看不到 /dev/sda1 一类的分区。


如果汝没看到 /dev/sda 却看到了一些其它的东西的话:

  • /dev/nvme0n1 一类的:表示汝的电脑上有一块 NVMe SSD (发出了羡慕的目光(咳咳))
  • /dev/mmcblk0 一类的:表示汝的电脑上有 eMMC 存储(例如是早期的 Windows 8 平板电脑啥的)

这种时候的话可能汝 ArchISO 的 U 盘就跑到 sda 去了,在下面的操作时记得注意一下。

比如咱这里 sda 是咱的硬盘,于是运行 cgdisk 时加上 /dev/sda 这个参数:

/dev 是一个虚拟目录(也就是并不在硬盘上),它会把电脑上的设备映射成一个个文件 _(:з」∠)_

再学究的解释真不会了……

cgdisk

root@archiso ~ # cgdisk /dev/sda

如果汝在用 cgdisk 的话汝可能会看到这样的提示:

Warning! Non-GPT or damaged disk detected! This program will attempt to
convert to GPT form or repair damage to GPT data structures, but may not
succeed. Use gdisk or another disk repair tool if you have a damaged GPT
disk.


                                            Press any key to continue....

这表示 cdgisk 在这块硬盘上找不到 GPT 分区表。如果这是一块新硬盘或者已经清空的硬盘的话 不必担心,继续即可。否则请立即停下来检查一下硬盘(或者汝有这块硬盘的备份的话也可以继续)。

                                            cgdisk 1.0.1

                                        Disk Drive: /dev/sda
                                    Size: 972906545, 463.9 GiB

Part. #     Size        Partition Type            Partition Name
----------------------------------------------------------------
            1007.0 KiB  free space
1           512.0 MiB   EFI System                EFI system partition
2           16.0 MiB    Microsoft reserved        Microsoft reserved partition
3           103.4 GiB   Microsoft basic data      Basic data partition
4           253.4 GiB   Microsoft basic data      Basic data partition
            106.6 GiB   free space





    [ Align  ]  [ Backup ]  [  Help  ]  [  Load  ]  [  New   ]  [  Quit  ]  [ Verify ]  [ Write  ]

cgdisk 的界面大概像这样啦,用上下方向键把光标移动到汝之前的空闲空间上去(例如咱这里是最后一个)

新硬盘的话应该只有一个 free space 😂

用左右方向键把下面一排按钮上的光标移动到 New 上,然后按 Enter。

(这里看不出光标😂,黑色背景下光标应该是白的吧😂😂)

接下来会问几个问题(# 开头的是咱加上的注释😂):

# 数字可能和汝看到的不一样😂
# 起始扇区的位置,直接 Enter 就行
First sector (749424640-972906511, default = 749424640):
# 大小,可以是扇区数,也可以是实际的大小(例如 100M,20G一类的),要用掉整个剩余空闲空间的话,直接 Enter 就行。
Size in sectors or {KMGTP} (default = 223481872):
# 分区类型,默认的就好
# 但是如果要建立新的 EFI 系统分区的话 ,分区类型是 :code:`ef00`
# 但是如果要建立新的 交换空间(就是虚拟内存啦)的话 ,分区类型是 :code:`8200`
Current type is 8300 (Linux filesystem)
Hex code or GUID (L to show codes, Enter = 8300):
# 设置卷标,不设置也行。
Current partition name is ''
Enter new partition name, or <Enter> to use the current name:

然后汝应该会发现下面的空闲空间变成 Linux filesystem 了呗~

要保存分区表的话,用左右方向键把下面一排按钮上的光标移动到 Write 上,然后按 Enter。

Are you sure you want to write the partition table to disk? (yes or no):

        Warning!! This may destroy data on your disk!

在这里输入 yes (就是 yes,不是 y Y YES 啥的😂),然后按 Enter。

然后下面会闪过一行 "The operation has completed successfully" ,这时就可以退出了。

用左右方向键把下面一排按钮上的光标移动到 Quit 上,然后按 Enter。

cfdisk

如果汝在用 cfdisk 的话汝可能会看到这样的提示:

                        ┌ Select label type ───┐
                        │ gpt                  │
                        │ dos                  │
                        │ sgi                  │
                        │ sun                  │


        Device does not contain a recognized partition table.
Select a type to create a new label or press 'L' to load script file.

这表示 cfgisk 在这块硬盘上找不到可以识别的分区表。如果这是一块新硬盘或者已经清空的硬盘的话 不必担心,选择 dos 继续即可。否则请立即停下来检查一下硬盘(或者汝有这块硬盘的备份的话也可以继续)。

这就是 cfdisk 的主界面啦:

                                                    Disk: /dev/sdc
                                Size: 465.8 GiB, 500107862016 bytes, 976773168 sectors
                                        Label: dos, identifier: 0x178bfb32

    Device            Boot               Start              End          Sectors         Size       Id Type
>>  Free space                            2048        976773167        976771120       465.8G










┌──────────────────────────────────────────────────────────┐
│Filesystem UUID: 3AB8802AB87FE2B5                                                                                   │
│     Filesystem: ntfs                                                                                               │
└──────────────────────────────────────────────────────────┘
                            [   New  ]  [  Quit  ]  [  Help  ]  [  Write ]  [  Dump  ]


                                        Create new partition from free space

也是选择 new 然后输入大小:

Partition size:
# 然后可能会让汝选择是主分区还是扩展分区,按需选择就 OK
[primary]  [extended]

但是如果汝需要调整分区类型的话,选中要改变的分区然后把光标移动到 Type 上按 Enter,选择一个分区类型:

                                                    Disk: /dev/sdc
                                Size: 465.8 GiB, 500107862016 bytes, 976773168 sectors
                                        Label: dos, identifier: 0xe0a3ecf7

    Device           Boot               Start             End        Sectors        Size      Id Type
>>  /dev/sdc1                            2048         1050623        1048576        512M      ef EFI (FAT-12/16/32)
    /dev/sdc2                         1050624       976773167      975722544      465.3G      83 Linux










┌──────────────────────────────────────────────────────────┐
│Partition type: EFI (FAT-12/16/32) (ef)
└──────────────────────────────────────────────────────────┘
            [Bootable]  [ Delete ]  [ Resize ]  [  Quit  ]  [  Type  ]  [  Help  ]  [  Write ]  [  Dump  ]

要保存分区表的话,用左右方向键把下面一排按钮上的光标移动到 Write 上,然后按 Enter。

Are you sure you want to write the partition table to disk? (yes or no):

在这里输入 yes (就是 yes,不是 y Y YES 啥的😂),然后按 Enter。

然后下面会闪过一行 "The operation has completed successfully" ,这时就可以退出了。

用左右方向键把下面一排按钮上的光标移动到 Quit 上,然后按 Enter。


然而汝以为这样就结束了?还没格式化呢 (╯°Д°)╯︵/(.□ . )

创建文件系统+挂载

所以挂载是啥玩意?

GNU/Linux 不是用 Windows 那样的盘符来分区的啦,GNU/Linux 继承了 Unix 的整个目录结构 从根目录 / 开始。然后下面是各种子目录。

大多数的 GNU/Linux 发行版都遵循一个叫做“文件系统层次结构标准”(Filesystem Hierarchy Standard, 有时也简称作 FHS)的标准,汝可以 ls / 一下看看会不会发现一些例如 /bin /home /usr 一类的目录? 这就是拜这个标准所赐 😂

在 Windows 里,汝大概会把不同的文件放在不同的盘符下的硬盘分区里。 在 GNU/Linux 中,咱们通常就是把不同的分区 挂载到整个目录结构中的一处。向挂载好的目录写入的文件就会保存在被挂载的磁盘分区上。

首先还是用 lsblk 确定一下分区的名称,为了以防万一记得加上 -f 参数:

root@archiso ~ # lsblk -f
NAME   FSTYPE   LABEL       UUID                                 MOUNTPOINT
sda
├─sda1 vfat               3C44-B4ED
├─sda2
├─sda3 ntfs               42E243C5E243BBC3
├─sda4 ntfs   新加卷      58741F29741F0A00
└─sda5
sdb
└─sdb1 vfat   ARCH_201610 EAC8-F012                            /run/archiso/bootmnt
sr0
loop0  squashfs                                                  /run/archiso/sfs/airootfs

第一排分别表示设备名称,文件系统类型,卷标,UUID和挂载点。

咱这里的话 sda1 那个 vfat 分区就是 EFI 系统分区啦,sda5 就是刚刚新建的分区啦~(因为还没格式化所以没有文件系统😂)

mkfs.ext4 把那个分区格式化成 ext4 文件系统咯~

mkfs 可以格式化成某一个汝指定的文件系统(就像刚才那样 mkfs.ext4 可以格式化出一个 ext4 文件系统)

ext 家族大概是 GNU/Linux 最古老,应用的最广泛的文件系统了吧……

当然可以用的文件系统有很多啦,如果汝想再了解一些的话,可以去 ArchWiki 看一看:

https://wiki.archlinux.org/index.php/File_systems

记得自己看清楚是哪个分区别格式化错了 😂

root@archiso ~ # mkfs.ext4 /dev/sda5
mke2fs 1.43.3 (04-Sep-2016)
Creating filesystem with 27935234 4k blocks and 6987776 inodes
Filesystem UUID: a3943e57-6217-4a5f-8e57-ade5771315c0
Superblock backups stored on blocks:
    32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
    4096000, 7962624, 11239424, 20480000, 23887872

Allocating group tables: done
Writing inode tables: done
Creating journal (131072 blocks): done
Writing superblocks and filesystem accounting information: done

root@archiso ~ #

等一排文字闪过就格式化完了……

如果要格式化新的 EFI 系统分区的话,用 mkfs.vfat

如果要格式化新的 交换空间的话,用 mkswap

接下来用 mount 挂载分区啦~ (。>ω<)。

# mount <设备名称> <目标文件夹>
# /mnt 挺合适的
root@archiso ~ # mount /dev/sda5 /mnt
# 如果要挂载 EFI 系统分区的话,建议挂载到 /mnt/boot
# 所以先建立相应的文件夹
root@archiso ~ # mkdir /mnt/boot
root@archiso ~ # mount /dev/sda1 /mnt/boot
# 有交换空间的话不用挂载,用 swapon 命令。
root@archiso ~ # swapon /dev/sda6

选择软件仓库镜像

软件仓库(在Debian系发行版中,又叫做“软件源”)是软件包存储的地方。通常我们所说的软件仓库指在线软件仓库,亦即用户从互联网获取软件的地方。

以及新的 ArchISO 中内置了通过测速和筛选镜像服务器位置来选择镜像的 Reflector, 使用方法在这里。

用 nano 打开 /​etc/​pacman.d/​mirrorlist

root@archiso ~ # nano /etc/pacman.d/mirrorlist


GNU nano 2.7.0                        File: /etc/pacman.d/mirrorlist

##
## Arch Linux repository mirrorlist
## Sorted by mirror score from mirror status page
## Generated on 2016-10-01
##

## Score: 0.2, France
Server = http://archlinux.polymorf.fr/$repo/os/$arch
## Score: 0.3, France
Server = http://arch.tamcore.eu/$repo/os/$arch
## Score: 0.3, Germany
Server = http://mirrors.cicku.me/archlinux/$repo/os/$arch
## Score: 0.3, Czech Republic
Server = http://ftp.sh.cvut.cz/arch/$repo/os/$arch
## Score: 0.3, Germany
Server = http://mirror.js-webcoding.de/pub/archlinux/$repo/os/$arch
## Score: 0.4, Netherlands
Server = http://ftp.nluug.nl/os/Linux/distr/archlinux/$repo/os/$arch
## Score: 0.4, Poland
                                        [ Read 517 lines ]
^G Get Help    ^O Write Out   ^W Where Is    ^K Cut Text    ^J Justify     ^C Cur Pos     ^Y Prev Page
^X Exit        ^R Read File   ^\ Replace     ^U Uncut Text  ^T To Spell    ^_ Go To Line  ^V Next Page

这是 GNU nano 的主界面,最简单的方法还是把下面那些 Mirrors 先全删掉然后输入一个新的, 用光标指向某一行以后同时按下 Ctrl+K 就好。然后自己输入一个 Mirror ,下面给出几个中国国内的 Mirror:

(所谓的 Ctrl+K 就是这两个键一起按😂)

# 网易
Server = http://mirrors.163.com/archlinux/$repo/os/$arch
# 清华大学 TUNA 协会
Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch
# 中国科学技术大学
Server = https://mirrors.ustc.edu.cn/archlinux/$repo/os/$arch
# 西安交通大学
Server = https://mirrors.xjtu.edu.cn/archlinux/$repo/os/$arch

输入完以后按下 Ctrl+O 写入,按 Enter 确定,再按 Ctrl+X 退出。

然后用 pacman -Syy 刷新一下软件包数据库。

root@archiso ~ # pacman -Syy
:: Synchronizing package databases...
core                                  120.9 KiB  4.92M/s 00:00 [##################################] 100%
extra                                1755.6 KiB  5.24M/s 00:00 [##################################] 100%
community                               3.7 MiB  6.82M/s 00:01 [##################################] 100%
root@archiso ~ #

安装基本系统

用 pacstrap 安装基本系统:

  • 默认会安装 base 元包(包含基本系统所需的依赖)。

    如果汝在别的地方见到说 base 是个软件包组的说法,忘了它吧……

  • 然后大多数情况下,汝还需要一个内核。目前官方仓库里有这些:

    对于大多数情况下,汝可能会考虑安装固件包 linux-firmware 。

  • 以及一个文字编辑器,例如刚才用到的 nano

    诶,base 组不包含 live 环境中所有的软件包么?是的, https://projects.archlinux.org/archiso.git/tree/configs/releng/packages.x86_64 列出了在 ISO 中的 live 环境中安装的但不在 base 包中的软件包。

  • 以及一些文件系统工具,如果汝和咱这里一样用了 ext4 的话,安装 e2fsprogs 就好, 需要读写其它文件系统的话,可以在 https://wiki.archlinux.org/index.php/File_system 找到相应的用户空间工具。

  • 要通过 AUR 或者 ABS 编译安装软件包,还需要安装 base-devel 啦 (现在它还是个软件包组)。

  • 要通过刚刚用过的 iwctl 连接无线网络的话,记得安装 iwd 。

于是一个栗子大概像这样

root@archiso ~ # pacstrap /mnt base base-devel linux linux-firmware nano e2fsprogs iwd

其他软件以后会用 pacman 再安装啦~

安装完以后大概会是这个样子 (´・ω・`)

pacstrap /mnt base linux    29.09s user 2.61s system 85% cpu 37.271 total

准备进入 chroot 环境

生成 fstab 啦 ~

所以 fstab 又是个啥玩意?

fstab(5)文件可用于定义磁盘分区,各种其他块设备或远程文件系统应如何装入文件系统。

每个文件系统在一个单独的行中描述。这些定义将在引导时动态地转换为系统挂载单元,并在系统管理器的配置重新加载时转换。 在启动需要挂载的服务之前,默认设置会自动fsck和挂载文件系统。例如,systemd会自动确保远程文件系统挂载 (如NFS或Samba)仅在网络设置完成后启动。因此,在/etc/fstab中指定的本地和远程文件系统挂载应该是开箱即用的。

emmmmm

# genfstab
usage: genfstab [options] root

Options:
    -L             Use labels for source identifiers (shortcut for -t LABEL)
    -p             Exclude pseudofs mounts (default behavior)
    -P             Include printing mounts
    -t TAG         Use TAG for source identifiers
    -U             Use UUIDs for source identifiers (shortcut for -t UUID)

    -h             Print this help message

genfstab generates output suitable for addition to an fstab file based on the
devices mounted under the mountpoint specified by the given root.
root@archiso ~ # genfstab -U /mnt >> /mnt/etc/fstab

然后用 arch-chroot 向新系统出发~

因为还有些配置没完成嘛……
root@archiso ~ # arch-chroot -help
usage: arch-chroot chroot-dir [command]

    -h                  Print this help message
    -u <user>[:group]   Specify non-root user and optional group to use

If 'command' is unspecified, arch-chroot will launch /bin/bash.
root@archiso ~ # arch-chroot /mnt /bin/bash
[root@archiso /] #

设置基本系统

# 开头只表示以 root 用户运行,汝不用把 # 输入到终端里啦~
  • 设置时区(中国的时区是 Asia/Shanghai)
# ln -s <源文件> <目标> 创建一个符号链接

# ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
  • 设置时间标准 为 UTC,并调整 时间漂移:
# hwclock --systohc --utc

如果汝正在尝试安装双系统,在进入 Windows 以后可能会发现 Windows 的时间不对了 ,因为 Windows 默认的硬件时钟是 localtime(

可以用一条注册表键值让 Windows 使用 UTC 作为硬件时钟(在早于 Windows 7 的系统上发现过这样做会出现一些严重的问题: http://www.cl.cam.ac.uk/~mgk25/mswish/ut-rtc.html

reg add "HKEY_LOCAL_MACHINESystemCurrentControlSetControlTimeZoneInformation" /v RealTimeIsUniversal /d 1 /t REG_DWORD /f
  • /etc/locale.gen 是一个仅包含注释文档的文本文件。指定您需要的本地化类型,去掉对应行前面的注释符号(#)就可以啦,还是用 nano 打开,建议选择帶UTF-8的項:
# nano /etc/locale.gen

en_US.UTF-8 UTF-8
zh_CN.UTF-8 UTF-8
zh_TW.UTF-8 UTF-8
  • 执行 locale-gen 以生成 locale 讯息:
# locale-gen
  • 创建 locale.conf 并提交您的本地化选项:

    将系统 locale 设置为en_US.UTF-8,系统的 Log 就会用英文显示,这样更容易问题的判断和处理。用户可以设置自己的 locale。

    警告: 不推荐在此设置任何中文locale,或导致tty乱码。

# echo 用来输出某些文字,后面的大于号表示把输出保存到某个文件里啦~

# 或者可以用文字编辑器新建这个文件加上这一行。

# echo LANG=en_US.UTF-8 > /etc/locale.conf
  • 设置一个喜欢的主机名(用汝的主机名代替 myhostname ):
# echo myhostname > /etc/hostname
  • 设置 root 的密码(输入密码的时候就是啥也没有 ╮( ̄▽ ̄)╭ ):
[root@archiso /]# passwd
New password:
Retype new password:
passwd: password updated successfully

以及汝大多数时候应该是用不到这个密码的(

  • 安装启动加载器(例如 GRUB ):

    启动加载器是 BIOS 或 UEFI 启动的第一个程序。它负责使用正确的内核参数加载内核, 并根据配置文件加载初始化 RAM disk。

    如果对其它的启动管理器有兴趣的话,记得去看 https://wiki.archlinux.org/index.php/Arch_boot_process#Boot_loader

** UEFI 用户先再安装几个必要的软件包咯, efibootmgr 用于修改 UEFI 固件中的某些信息,

dosfstools 包含了操作 FAT/FAT32 文件系统所需的用户空间工具。

# pacman -S efibootmgr dosfstools

** 然后安装 GRUB (如果汝的硬盘上没有其它系统,那么可以不用装 os-prober )

# pacman -S grub os-prober

** 把 GRUB 安装到硬盘:

# MBR 用户这么做 (记得用汝自己硬盘的名称代替 sda ,不要带上表示分区的数字啦~):

# grub-install --target=i386-pc /dev/sda --recheck

# UEFI 用户这么做(如果汝没把 EFI 系统分区挂载到 /boot ,请自行修改 --efi-directory 的值):

# grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=grub --recheck

EFI 安装成功以后大概像这样 😂

[root@archiso /]# grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=grub --recheck
Installing for x86_64-efi platform.
Installation finished. No error reported.

然后生成必要的配置文件:

[root@archiso /]# grub-mkconfig -o /boot/grub/grub.cfg
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-linux
Found initrd image(s) in /boot: initramfs-linux.img
Found fallback initrd image(s) in /boot: initramfs-linux-fallback.img
done

如果汝安装了 os-prober 然后看到这样的警告:

WARNING: Failed to connect to lvmetad. Falling back to device scanning.

这是因为在 chroot 环境里面 /run 是不可用的。只要每个步骤都做对了,这些警告不会影响系统启动, 汝可以放心继续进行下一步的系统安装咯。 如果汝重启之后没看到 Windows 的启动项,试着进入 Arch 之后再运行那条生成配置文件的命令。

因为 GRUB 是一个操作系统,它的配置和使用可能会非常复杂,可以在 https://wiki.archlinux.org/index.php/GRUB 上找到一些提示和疑难排解技巧。

设置 sudo

所以这又是个啥玩意? (´_`)

因为 root 用户的权力很大而且很危险,所以轻易不会用到它 (。>ω<)。

所以就有了 sudo(substitute user do) 使得系统管理员可以授权特定用户或用户组作为 root 或其他用户执行某些(或所有)命令,同时还能够对命令及其参数提供审核跟踪。

(话风突变)

sudo 应该已经作为 base-devel 的一部分装上去了,如果没有的话汝也可以自己手动安装一下:

[root@archiso /]# pacman -S sudo

sudo 的配置文件是 /etc/sudoers ,但是咱们不会直接去编辑它(因为一旦搞坏了不好修)。 所以有一个 visudo 的命令用来代理编辑它(就是先编辑一个临时文件,然后检查有没有错误, 一切 OK 后再覆盖)。

嗯…… visudo? 如果汝觉得 vi 哪里眼熟的话,没错! 这个命令默认会用 vi 去编辑那个文件。 如果汝想用其他的编辑器(例如刚刚用的 nano)的话,可以通过一个环境变量:

[root@archiso /]# EDITOR=nano visudo

现在大概像这个样子:

GNU nano 3.2                                        /etc/sudoers.tmp

## sudoers file.
##
## This file MUST be edited with the 'visudo' command as root.
## Failure to use 'visudo' may result in syntax or file permission errors
## that prevent sudo from running.
##
## See the sudoers man page for the details on how to write a sudoers file.
##

##
## Host alias specification
##
## Groups of machines. These may include host names (optionally with wildcards),
## IP addresses, network numbers or netgroups.
# Host_Alias    WEBSERVERS = www1, www2, www3

##
## User alias specification
##
                                                [ Read 97 lines ]
^G Get Help    ^O Write Out   ^W Where Is    ^K Cut Text    ^J Justify     ^C Cur Pos     M-U Undo       M-A Mark Text
^X Exit        ^R Read File   ^\ Replace     ^U Uncut Text  ^T To Spell    ^_ Go To Line  M-E Redo       M-6 Copy Text

虽然这个文件有很多行,但是咱们还是先从让它能够工作开始来最小的修改它。

找到下面的这一行,然后把 %wheel 前面的注释符号(#)去掉,不过百分号要留下:

## Uncomment to allow members of group wheel to execute any command
# %wheel ALL=(ALL) ALL

然后就可以保存退出啦~ (效果就是注释里说明的,给 wheel 组执行所有命令的权限)

如果汝不想每一次都在前面加上 EDITOR 来指定编辑器的话,可以加上这几行:

# 重设默认的环境变量
Defaults      env_reset
# 设置默认的编辑器,并使 visudo 不再读取环境变量 editor 的值。
Defaults      editor=/usr/bin/nano, !env_editor

如果汝想进一步了解 sudo 的配置的话,还是去看 ArchWiki: https://wiki.archlinux.org/index.php/Sudo

安装桌面环境

  • 安装桌面环境需要的基础包 (就是 xorg 啦)
[root@archiso /]# pacman -S xorg
:: There are 80 members in group xorg:
:: Repository extra
1) xf86-input-evdev  2) xf86-input-joystick  3) xf86-input-keyboard  4) xf86-input-libinput
5) xf86-input-mouse  6) xf86-input-synaptics  7) xf86-input-vmmouse  8) xf86-input-void
9) xf86-video-amdgpu  10) xf86-video-ark  11) xf86-video-ati  12) xf86-video-dummy
13) xf86-video-fbdev  14) xf86-video-glint  15) xf86-video-i128  16) xf86-video-intel
17) xf86-video-mach64  18) xf86-video-neomagic  19) xf86-video-nouveau  20) xf86-video-nv
21) xf86-video-openchrome  22) xf86-video-r128  23) xf86-video-savage  24) xf86-video-siliconmotion
25) xf86-video-sis  26) xf86-video-tdfx  27) xf86-video-trident  28) xf86-video-vesa
29) xf86-video-vmware  30) xf86-video-voodoo  31) xorg-bdftopcf  32) xorg-docs  33) xorg-font-util
34) xorg-fonts-100dpi  35) xorg-fonts-75dpi  36) xorg-fonts-encodings  37) xorg-iceauth
38) xorg-luit  39) xorg-mkfontdir  40) xorg-mkfontscale  41) xorg-server  42) xorg-server-common
43) xorg-server-devel  44) xorg-server-xdmx  45) xorg-server-xephyr  46) xorg-server-xnest
47) xorg-server-xvfb  48) xorg-server-xwayland  49) xorg-sessreg  50) xorg-setxkbmap
51) xorg-smproxy  52) xorg-x11perf  53) xorg-xauth  54) xorg-xbacklight  55) xorg-xcmsdb
56) xorg-xcursorgen  57) xorg-xdpyinfo  58) xorg-xdriinfo  59) xorg-xev  60) xorg-xgamma
61) xorg-xhost  62) xorg-xinput  63) xorg-xkbcomp  64) xorg-xkbevd  65) xorg-xkbutils  66) xorg-xkill
67) xorg-xlsatoms  68) xorg-xlsclients  69) xorg-xmodmap  70) xorg-xpr  71) xorg-xprop
72) xorg-xrandr  73) xorg-xrdb  74) xorg-xrefresh  75) xorg-xset  76) xorg-xsetroot  77) xorg-xvinfo
78) xorg-xwd  79) xorg-xwininfo  80) xorg-xwud

Enter a selection (default=all):

这时会让汝选择需要哪些软件包啦,其实大多数时候默认的就行……

  • 接下来挑一个喜欢的桌面环境包组装上咯~

    (咱这里就只举例 GNOME 、KDE 和 xfce 啦,其他官方支持的桌面环境可以去 https://wiki.archlinux.org/index.php/Desktop_environment_(简体中文) 查看)

    GNOME , 想要 GNOME 全家桶的话带上 gnome-extras

    # pacman -S gnome

    KDE Plasma , 想要 KDE 全家桶的话用 kde-applications-meta 代替 。 kde-applications 会提示汝选择要安装哪些包。

    以及一个显示管理器, KDE 和 sddm 一起使用最好。

    # pacman -S plasma sddm kde-applications

    或者只安装 kdebase 组,包含了一些基本组件(例如文件管理器和终端模拟器)。

    # pacman -S plasma sddm kdebase

    xfce4,xfce 不带显示管理器,所以要装个其他的(例如 lightdm,还要装一个 greeter)

    # pacman -S xfce4 xfce4-goodies lightdm lightdm-gtk-greeter

    桌面环境大多数使用 NetworkManager ,xfce 的话,记得安装 network-manager-applet, 一个控制 NetworkManager 的小工具:

    # pacman -S networkmanager

  • 然后安装中文字体( 同样的方法安装 😋)

    Google Noto Fonts 系列: noto-fonts noto-fonts-cjk noto-fonts-emoji

    思源黑体:adobe-source-han-sans-otc-fonts

    文泉驿:wqy-microhei wqy-zenhei

更多的字体可以在 https://wiki.archlinux.org/index.php/Fonts_(简体中文) 找到。

收尾工作

  • 新建一个用户

    -m 为新用户创建一个目录,-s 设置用户的登录 Shell -G 会把新建的用户追加到某个组中 (这里是刚刚 sudo 里的那个 wheel)

    记得最后是用户名就好 😂

    # useradd -m -G wheel horo

    然后设置密码(记得是输入两次,而且输入过程中不会有显示,不要以为是键盘坏了哦)

    # passwd horo

  • 激活需要的服务,例如一个显示管理器,在例如 gdm :

    # systemctl enable gdm

    当然还有 NetworkManager:

    # systemctl enable NetworkManager

    (这个里面有大写😂)

    systemd 是 Arch Linux 现在唯一的 init 程序,当内核加载完毕后,首先被加载的 就是 init (这里就是 systemd 啦)。然后 systemd 会接着加载剩下的部分。

    https://wiki.archlinux.org/index.php/systemd

  • 设置用户级别的 locale

    用 su 切换到刚建立的用户,然后编辑 ~/.config/locale.conf 修改自己的 Locale ,例如:

LANG=zh_CN.UTF-8
LC_CTYPE="zh_CN.UTF-8"
LC_NUMERIC="zh_CN.UTF-8"
LC_TIME="zh_CN.UTF-8"
LC_COLLATE="zh_CN.UTF-8"
LC_MONETARY="zh_CN.UTF-8"
LC_MESSAGES="zh_CN.UTF-8"
LC_PAPER="zh_CN.UTF-8"
LC_NAME="zh_CN.UTF-8"
LC_ADDRESS="zh_CN.UTF-8"
LC_TELEPHONE="zh_CN.UTF-8"
LC_MEASUREMENT="zh_CN.UTF-8"
LC_IDENTIFICATION="zh_CN.UTF-8"
LC_ALL=

或者可以重启以后用桌面环境的设置程序改 😂

完工啦

  • 离开 chroot 环境:

    # exit

  • 卸载挂载的分区,(其实不是必须的,因为马上就重启啦~)

    # umount -R /mnt

  • 重新启动,准备迎接新的系统吧 ~(>_<~)


欢迎来到 Arch Linux 的世界!

Cheers! 汝刚刚成功的安装 Arch Linux 到汝的电脑中了呐!

那接下来要干啥呢?

by ホロ at July 01, 2020 04:00 PM