关于 Jerry Qu | 屈光宇

北大光华 MBA。先后工作于百度(三年)、奇虎 360(十年)、字节跳动(三年),负责过大型互联网业务的前端研发、技术团队管理以及业务管理等工作。

RSS 地址: https://imququ.com/rss.html

请复制 RSS 到你的阅读器,或快速订阅到 :

Jerry Qu | 屈光宇 RSS 预览

本博客最近的一些变化

2024-06-30 23:31:29

最近把荒废多年的博客捡起来修缮了一番,在这篇文章里记录一下。

使用 Waline 评论系统

很多年前,我用自己写的博客系统换掉了 WordPress,在那之后短暂用过多说,其余时间都在用 Disqus 作为博客的评论系统。Disqus 作为第三方社会化评论的鼻祖,早些年体验和口碑都非常不错,但前几年它被营销科技公司 Zeta 收购,多了不少乱七八糟的广告,付费去广告的费用也不便宜。另外最近几年 Disqus 在国内基本不可用,我之前通过解析接口和自己写界面搞了一个评论基础模式,凑合着用。几个月前我发现,Disqus 评论框在我手机默认浏览器默认输入法下完全无法输入,刚开始以为是 CSP 拦了一些 JS 导致的问题,后来发现并不是。在我的认知里,核心功能主流环境无法使用就算不是 P0 也不至于几个月都没修复,所以尽管很感谢 Disqus 让我免费用了这么多年,还是下定决心换掉。

为什么换成 Waline,主要原因是跟作者很熟,遇到问题直接问。其次 Waline 项目用到的技术比较现代,代码结构很清晰,万一后续有什么需求要改代码,上手成本也不会太高。迁移过程非常顺利,参考 Waline 官方文档都能搞定,以下是一些小提示:

官方「快速上手」文档中描述的是 Vercel 部署服务端 + LeanCloud 数据库的方式,我采用的是本地部署 + MySQL 数据库(最近换成了 PostgreSQL),这部分文档在「部署」和「多数据库服务支持」里,藏得有点深。

在 Disqus 后台可以导出所有数据,操作后等一会儿就会收到包含下载链接的邮件。在 Waline 的数据迁移助手页面,可以把 Disqus 导出的 xml 格式转成 Waline 格式。我选择迁移至「Waline MySQL/PostgreSQL/SQLite」存储服务,得到了一个 csv 文件,刚开始我一直以为要通过 Waline 后台来导入 csv,失败多次后猛然想起来,直接 MySQL 中导入即可。

Waline 的客户端部分(JS + CSS),可以通过 CDN 引入,也可以直接在项目中导入。这两种方式对于追求高性能的本博客来说都不太合适,我目前使用 Dynamic imports 来加载 Waline JS,用动态创建 link 标签的方式来加载 Waline CSS,实现了 Waline 的按需加载。

Waline 提供了一个很小的 JS 文件用来显示某篇文章的评论数,而我选择把评论数存在数据库里,这样能再快一些。Waline 提供了完整的 APIHooks,无论是通过 API 定期更新,还是通过评论的新增 / 删除 Hook 触发更新,都非常方便。考虑到本博客评论不多,一个 10 分钟运行一次的定时任务就足够了。

另外,我还对 Waline 静态资源做了代理,统一通过我的 CDN 域名来访问,评论数据接口我也做了代理,通过主域来访问,这样也有一些加速效果。如果你觉得还不够极致,可以参考作者「静态博客如何高性能插入评论」一文,将评论模块彻底静态化。

Elasticsearch 换成 Meilisearch

在之前的文章中我提到过,本站使用 Elasticsearch 提供站内搜索服务,最近我把它换成了 Meilisearch。相比 Elasticsearch,Meilisearch 全部采用 rust 编写,速度更快、占用内存更少,架构更加现代高效。

Meilisearch 官方文档十分完善,不需要太多专业知识即可轻松创建自己的搜索服务,除了关键字搜索,它也支持向量存储和向量搜索,非常推荐大家尝试一下。

使用阿里云低价服务器

早些年,我这个博客在国内国外都有部署,一年的费用超过千元。后来,我砍掉了国外部署,花费降至几百。如今,这个博客跑在 ¥99 一年的阿里云长效特价服务器上(2 核 2G,3M 固定带宽,40G ESSD Entry 盘,能低价续费到 2027 年),CDN 也换成了每个月可以白嫖 20GB 流量的多吉云(DogeCloud),加上域名每年花费不到二百,可以说非常省钱了。

腾讯云同样的价格只能买到轻量云主机(PS:京东云最近有活动,同样的轻量云主机只要一半甚至 1/4 的价格),但轻量云主机可玩性还是差了一些。阿里云这次没有限制新用户才能参与活动,确实挺良心。唯一的问题是这个小内存机器(装完 Ubuntu 后默认可用 1.63G),非常容易死机,具体表现为云盘读写 BPS 和 IOPS 居高不下,系统无响应、SSH 连不上,只能去阿里云控制台强制重启。

aliyun iops

为了尽可能减少因为内存用完导致的死机,建议装完系统后做一些调整:

首先,既然物理内存本来就不大,需要尽可能减少运行的服务。目前这台机器只跑了 MySQL、博客、Waline 评论、Meilisearch、frps 这几个服务,没有内存大户。另外,阿里云默认会安装云助手 Agent 等服务,如果对自己的安全知识有信心,可以卸载掉,能省不少内存。

其次,阿里云默认给 kdump 内核崩溃转储机制预留了一些内存,打开 /etc/default/grub 文件可看到这样一行配置:

GRUB_CMDLINE_LINUX=" vga=792 console=tty0 console=ttyS0,115200n8 net.ifnames=0 noibrs iommu=pt crashkernel=0M-1G:0M,1G-4G:192M,4G-128G:384M,128G-:512M nvme_core.io_timeout=4294967295 nvme_core.admin_timeout=4294967295"

1G-4G:192M 表示这台 2GB 内存的机器,会分配 192MB 给 kdump。对我来说,完全不需要它在内核崩溃时生成用于调试的转储文件,去掉 crashkernel=xxx 这一段内容再执行 sudo update-grub,重启后可用内存增加到 1.82G。

还有,阿里云默认禁用了 Swap,这个配置的机器最好加上,也能减少内存用爆引发的死机。配好 Swap 后还需要修改 /etc/sysctl.conf 文件,注释或修改 vm.swappiness = 0 这一行,改完后重启系统或者通过 sudo sysctl vm.swappiness=60 让修改生效(60 是默认值,也可以改成其他)。

最后,既然是为了省钱,机器偶尔挂掉也能理解,建议再加个第三方监控,便于及时去后台重启。我一直在用 UptimeRobot 的监控服务,免费版就够用了。也可以用 Uptime Kuma 自己搭一套监控,不过切记要部署在其他机器上。

其他还有一些排版、样式、HTTP 配置、基础组件升级等变化,这里就不列举了。再提一句为什么我一直没有启用 HTTP/3,实测国内运营商对 UDP 的干扰非常严重,白天还好,晚上有些地域完全不可用,非常无语。

本文链接:https://imququ.com/post/recent-changes-to-my-blog.html参与讨论

2024 上半年我买了啥

2024-06-11 03:02:22

前年,我开始用 Airtable 表格记录我所有的数码产品、软件和订阅的服务,包括购买 / 续费时间、状态(在用 / 吃灰 / 转售)、价格等信息,便于随时查阅和总结。今天翻了翻过去半年的记录,发现相较往年有不少变化,上半年我的购买清单可以用「理性」和「精简」来概括,例如 Steam Deck、ROG Ally 这类冲动消费完全消失,使用频次不高的产品会在彻底吃灰前卖掉,各类订阅服务也做了一轮精简。简而言之,与当前不景气的经济形势和降本增效的主旋律基本吻合。

今天这篇文章,主要介绍我上半年购买并且至今每天都在用的几件数码产品,也会穿插介绍一些技巧和思路,供大家参考。

MacBook Pro

在最重要的生产力设备上,我希望能有最好的体验。笔记本电脑这个品类里,苹果始终是我的第一选择。目前在售最新的 MacBook 机型分别是今年 3 月初发布的 13 / 15 英寸 MacBook Air,去年 12 月底发布的 14 / 16 英寸 MacBook Pro。二者都装有 M3 芯片,Pro 系列还可以选配 M3 Pro 和 M3 Max。

首先被我划掉的是 16 英寸 MacBook Pro,2.16 千克的净重让它又沉又滑,之前单手没拿稳把一个角摔变形的惨案还历历在目。其次划掉的是 13 英寸 MacBook Air,理由是屏幕分辨率偏小,显示内容偏少。我在 15 英寸 MacBook Air 和 14 英寸 MacBook Pro 之间犹豫了一阵子,最终还是选择了选配潜力更大的后者,最终我这台新电脑的配置是:深空黑色 / M3 Max 芯片(14 + 30 核)/ 36GB 内存 / 1TB 硬盘。

有了新电脑,接下来要安装各种软件。这些年我用 macOS 的习惯是尽量用预装软件,尽量用网页版。像邮件客户端、日历提醒、输入法这些都换成了 macOS 自带的对应软件,剩下要单独安装的涉及 Office、IM、浏览器、效率、开发、工具几类。

Office 软件主要是 Microsoft 365 和 Keynote,我选择直接从 App Store 安装。效率、开发和工具类有不少好用的软件需要买断或者订阅,我通过订阅 Setapp 服务覆盖其中一部分。之前在 V2EX 上蹲到一个老的家庭套餐拼车,费用是 200¥ / 年,就我高频使用的软件而言,绝对值回票价。我常用的 Setapp 软件有这些:

Setapp 里没有但值得付费的软件,我一般会去数码荔枝和少数派看看,例如 DaisyDisk、Typora 等软件就是在这两家买到的。

在 App Store 和 Setapp 里装软件很方便。由于有账号体系,换电脑或重装系统,也可以轻松找到之前的软件安装记录。但很多不在这两个商店的软件还是需要去到各自官网下载安装包,费时费力。我选择用 Homebrew Cask 提升这类软件的安装体验,例如在装好 Homebrew 之后,再安装 Google Chrome 只需要在命令行执行 brew install --cask google-chrome。虽然 Homebrew 没有账号系统,但通过 brew bundle dump --describe --forcebrew bundle --file="./Brewfile" 就可以轻松备份和还原软件列表,非常方便。最近我还发现一个名为 Applite 的软件,为 Homebrew Cask 提供了 UI 界面和各种快捷操作,用了一段时间发现还不错,推荐给大家。

顺便提一下,这次买 MacBook Pro 花了两万多,这个预算原本准备拿来买 Apple Vision Pro,还好忍住了冲动。

便携显示屏

最近经常拿着笔记本去咖啡厅写代码。大家都知道,想要有好的编程体验,两块屏幕是标配,多多益善。我希望在不增加太多背包重量的前提下,能随时多带一块屏幕。首先想到的是随航,家里正好有台 12.9 英寸的 iPad Pro,随航的好处是没有线材约束,使用简便。但这台 iPad 很重,立起来用还得多带一个支架,而且 M1 芯片的 iPad 续航很一般,无法坚持一整天,我又不想带充电头或充电宝来增加负担。

几年前为了提升外出游玩 Switch 的体验,我买过一块便携显示屏,小巧的体积和平板一半的重量很便携,还支持充电宝供电和 Switch 一线通,方便实用。只不过这块屏幕分辨率有限,sRGB 也很低,玩游戏还好,用作 MacBook 外接显示器的效果惨不忍睹。

后来我找到了一块 10.5 英寸、1920 × 1280 (220 PPI)、对比度 1500:1、支持 10 点触控的便携屏。这块屏幕应该是从 Surface Go 拆下来的,整体素质非常好,店家给它套上了 CNC 铝外壳,设计了 OSD 菜单,增加了 USB-C 一线通,就变成了能满足我全部需求的便携屏。首先,10.5 英寸的体积,不到 500 克的重量,轻轻松松带出门。其次,这块屏幕通过一根 USB-C 线材与电脑连接,同时完成供电和视频信号传输,非常方便。最后,它的耗电量也不大,MacBook 给它供电的同时自己还能有一整天的续航。下面这些图片是我日常连接 MacBook 和便携屏的样子:

2k portable display

前面说过这块屏幕物理分辨率是 1920 × 1280,在 macOS 中如果将它的分辨率设置为 1920 × 1280,字体过小,如果设置为 960 × 640,一屏显示的内容又过少。我还尝试了同为 3:2 比例的 1344 × 896,屏幕比例没有失真,能显示的内容也刚刚好,但文字非常模糊。

这是苹果不支持给 2K 显示器开启 HiDPI 造成的,家里的 4K 显示器就没这个问题。在网上搜索「如何给苹果电脑外接显示器开启 HiDPI」能找到很多强制开启 HiDPI 的教程,也有不少相关的工具。我试过 BetterDisplay,确实能让这块显示屏在 1344 × 896 分辨率下也有清晰细腻的显示效果。后来我发现前文提到的 One Switch 也可以进行相关设置,这样还能少装一个软件。下面是 One Switch 的屏幕分辨率调整界面:

macos hidpi

通过 One Switch 设置 HiDPI 分辨率后,能获得清晰的显示效果。下面是这块屏幕在 1344 × 896 HiDPI 下的实际效果:

2k portable display screen

这块屏幕还有个有意思的地方,它模拟了一块触摸板,可以把屏幕当作触摸板来用,间接让 macOS 支持了触摸屏操作。实际使用效果挺流畅,只是当外接屏幕为扩展模式时,需要先把鼠标光标挪到外接屏上,触摸才能正常工作,否则在外接屏滑动,光标还在内置屏幕上移动,有些诡异。

买便携屏的重点是看屏幕参数是否匹配自己的需求,够用就好,不推荐买太贵的。我这块便携屏只花了 ¥500 多,如果选择塑料外壳且不追求完美屏,两百多块钱就能拿下,是我最近买过最具性价比的数码产品。

树莓派 5

我一直会在家里放块树莓派,用来实现一些网络功能。去年 10 月份树莓派 5 发布后,第一批货有很高的溢价,今年上半年价格回落后,我买了一块。树莓派 5 首次支持 PCIe 2.0(修改配置可以支持到 PCIe 3.0),我还买了微雪的 PCIe 转 M.2 转接板以及杂牌 M.2 NVMe PCIe 3.0 2242 硬盘,组装好的板子装在一个 Argon NEO 5 铝合金外壳里。下面是打开外壳上盖的样子:

raspberry pi 5

这块树莓派最重要的使命是,给我的腾讯云服务器提供访问特定网站的能力。例如在服务器编译软件往往需要稳定访问 Github,我博客评论的基础模式需要代理 Disqus,这些场景都需要用到代理。出于对国内云厂商的不信任,我不会在服务器上暴露我的代理服务。将相关服务装在只能有我能控制的树莓派上,再通过 frp 的加密通道连接服务器和树莓派,这样在云厂商那边就看不到任何国外流量。

在公网访问树莓派的服务也是通过 frp,虽然我办的联通宽带目前还能要到公网 IP,但 frp 是一劳永逸的方案,也能避免将树莓派直接暴露在公网,只是需要额外占用一些服务器带宽。为了避免连不上树莓派,除了自建的 frp server,我还用 SakuraFrp 做了备份。

我这块树莓派有 8G 内存,不拿来跑 docker 就太浪费了。装完 Home Assistant、Pi-hole 等经典服务之后,我还装了 Bitwarden 服务端的开源实现 VaultWarden,用了一段时间发现非常稳定,索性把 Bitwarden 订阅给停掉了。之前为了省钱,我把腾讯云服务器配置降到 2 核 2G换成了阿里 ¥99 一年的特价机 ,跑一些大型服务例如本博客搜索用到的 Elasticsearch 时,性能明显不够用。于是我又把 Elasticsearch 服务搬到了树莓派上,速度也还可以。

树莓派 5 官方电源是 27W,比前几代高了不少。但在我的实际使用中,它的功率长期维持在 4W 左右,每天耗费 0.1 度电,算下来一年电费不到 20¥,基本可以忽略不计。我给树莓派装了硬盘,并通过 rpi-eeprom-config -e 设置为从硬盘引导启动,理论上寿命比 SD 卡要长,用来跑一些不那么重要的服务,挺合适。

我这套树莓派设备花了小一千,实际上任何国产派或者树莓派前几代都能满足需求,这个花费有一大半是情怀。

其他

上半年我还把 AirPods Pro 换成了第二代,添置了 7 英寸的 BOOX Leaf3 墨水屏阅读器。AirPods 没什么好介绍的,Leaf3 上篇文章刚介绍过,是我最近的主力阅读设备,下面是一张近期使用图:

boox leaf3

此外,我还把开始有吃灰迹象的 BOOX Tab 10c Pro 及时卖掉回血,把家里翻出来的闲置手机、Magic Mouse,以及淘汰下来的耳机统统找人上门回收掉。

以上就是今天分享的全部内容,这篇文章写于等待和观看 WWDC24 直播的过程中,你最近买到什么心仪的好东西了吗?欢迎留言分享。

本文链接:https://imququ.com/post/good-things-2024h1.html参与讨论

聊聊墨水屏阅读器

2024-06-04 23:33:27

今天看到亚马逊中国的公告,提醒用户 Kindle 中国电子书店将于 2024 年 6 月 30 日停止云端下载服务。Kindle 在 2022 年宣布退出中国市场,一年后的 2023 年 6 月 30 日正式停止中国电子书店的运营,再在今年彻底关闭下载服务。相比国内某些说关就关的服务,这个时间表真的非常良心了。

我一直是墨水屏爱好者,这篇文章聊一聊这些年我用过的墨水屏阅读器、我个人关于墨水屏阅读器的选购建议,以及我在获取数字内容上的一点心得。

我用过的墨水屏阅读器

2013 年,Kindle Paperwhite 国行发布时,我入手了一台,这是我首次接触墨水屏产品。在这之后的十年里,我先后用过亚马逊的 Kindle Voyage、Kindle Oasis、Kindle Oasis 2,以及最新的 Kindle Scribe ── 一台采用 10.2 英寸 300 ppi 的 Carta 1200 墨水屏、支持触控笔的设备。

Kindle 第一代于 2007 年推出,至今共推出十一代产品。目前最新机型有三款,Kindle Paperwhite 5 发布于 2021 年 9 月,Kindle 11 发布于 2022 年 10 月,Kindle Scribe 则发布于 2022 年 11 月。不同于国内厂商恨不得每个月都发新品,亚马逊在设备更新上非常克制,这也使得 Kindle 的功能始终保持单一和简洁,系统流畅度和文字阅读体验都非常棒。Kindle 最让人诟病的是封闭且简陋的系统,基本上只能用来看 epub 等文本格式的图书,即便是有着 10.2 英寸屏幕的 Kindle Scribe,阅读 pdf 的体验依旧一塌糊涂,笔记功能也过于简陋。

kindle (Kindle Voyage、Kindle Scribe 和 Kindle Oasis)

文石 BOOX 是我重度使用过的另一个墨水屏阅读器品牌。我的第一台文石设备是 BOOX Max Lumi2,购于 21 年底。当时由于重新进入校园,需要大量阅读 PDF、PPT、CAJ 等格式的资料,还要在课堂上记笔记。13.3 英寸的 Lumi2 很好地满足了我的这些需求,尤其是 PDF 重排和漂白功能,确实是看扫描教材的一大神器。但它实在是太大太重,自从离开了校园,我就再也没有携带和使用它的想法了。目前文石的 Max Lumi 系列已经被 Tab 13 替代,屏幕依旧是 13.3 英寸 207 ppi 的 Carta 1250 屏幕,增加了 BSR 快刷技术,定位更接近于平板,价格也更高。

BOOX NoteX2 是我买的第二台文石设备,10.3 英寸的大小能兼顾大屏阅读和随身携带,偶尔记记笔记也能轻松胜任,是个不错的选择。最近 Note 系列又出了 X3Pro 和 X3青春版,前者主打与 Kindle Scribe 同款的 300 ppi 的 Carta 1200 屏幕,后者去掉了前光层主打屏幕白皙通透。不同于 13 英寸机型的小众,在 10 英寸这个尺寸上,国内厂家的竞争变得十分激烈,例如掌阅 iReader SmartX3 Pro 和汉王 N10 也是两款在屏幕上下足功夫的 10 英寸墨水屏产品,前者采用 10.65 英寸 300 ppi 的 Carta 1300 柔性屏,后者去掉了电容屏和导光板,都能让屏幕显示效果更接近纸张。科大讯飞的 10 英寸墨水屏产品则主打语音转写、轻办公等商务功能。

BOOX Leaf3 是一款更加便携的 7 英寸设备。这个屏幕尺寸下的 300 ppi Carta 1200 显示效果非常细腻,文石不错的硬件配置加上开放系统,运行微信读书、哔哩哔哩漫画等三方软件也十分流畅。目前它是我的主力阅读设备,出门都随身携带。美中不足的是,这款外形与 Kindle Oasis 十分相似的设备却去掉了背部人体工学设计,握持感有待提升,不知道是不是为了避免侵权。另外,7 英寸的屏幕拿来看漫画还是稍微小了一点。

BOOX Tab 10c Pro 是一款现阶段整体体验还过得去的彩色墨水屏设备。文石在这台机器上使用了高通 855 处理器和 6 GB 内存,性能在同品类中遥遥领先。Tab 10c Pro 采用元太的 kaleido 3 彩墨屏,支持 4096 色、150 ppi 彩色分辨率,配合文石的快刷算法,基本可以满足刷 B 站等视频需求。我买这台设备的初衷是想尽可能减少手机使用时长,目前看并没有达成目标。一来是 10.3 英寸还是太大,很难随身携带。再者目前彩墨屏用起来色彩太寡淡,白色不如黑白墨水屏通透,低刷新率和残影也还是会影响长时间使用。

和大多数国内阅读器厂商一样,由于商业模式是卖硬件而无法像 Kindle 那样以经营数字商店为主,文石硬件更新速度非常快,每一款新品在解决一些痛点的同时,又总要留一些遗憾,这让不少老用户很不爽。例如文石在 23 年彩屏旗舰 Tab 10c 发布不到半年后,就推出让不少用户深感背刺的 Tab 10c Pro。

boox (BOOX Max Lumi2、BOOX NoteX2、BOOX Tab 10c Pro、BOOX Leaf3)

墨水屏阅读器怎么选?

再来谈谈我的选购建议。

首先请牢记“发现需求,而非创造需求”。如果你平时没有阅读大量内容的习惯,那么大概率买了再好的墨水屏阅读器也会变成泡面盖。

其次请思考“我是需要能阅读、记笔记的设备,还是墨水屏阅读器?”。如果仅仅是想让阅读和记笔记无纸化,iPad 配合 MarginNote、Notability、Goodnotes 等软件也有非常不错的体验,而且 iPad 的可玩性和保值率远大于墨水屏设备。

“墨水屏设备能护眼”是一个常见的误区。无论厂商如何宣传采用更通透的面板、减少屏层、去掉前光,本质上都是让墨水屏阅读体验更加接近于纸张。墨水屏的“护眼”是相较普通平板或手机而言,没那么“伤眼”而已。不同于常用的 LCD、OLED 屏幕,墨水屏本身不发光,对人眼没那么刺激,在防止炫光、频闪和蓝光等问题上有优势。但在墨水屏设备上长时间阅读,同样会让眼球处于紧张且疲劳的状态,一样需要考虑光线、坐姿、看书时长等因素。另外,虽然大部分墨水屏设备都自带前光,在长时间阅读时,建议找个光线合适的环境,关闭前光来使用。如果对显示效果有极致追求,可以考虑去掉前光导光板的机型。

黑白屏和彩墨屏要怎么选?目前彩墨屏基本都是通过彩色滤光片实现(Bigme Gallery 3 以及采用 R-LCD 屏的设备暂不考虑),普遍有颜色寡淡(4096 色)、清晰度不够(彩色 150 ppi)、屏幕不通透(多一层滤光片,不开前光底色不够白)等问题。我的建议是现阶段如果以阅读文字类书籍为主,还是推荐黑白屏,如果要看彩漫,或者当平板来用刷小红书、看 B 站,可以尝试彩墨屏。

再好的设备也要经常用才能发挥价值。机器是否便携,系统是否能安装第三方软件等因素决定了它能否被高频使用。墨水屏阅读器常见有 6、7、8、10 英寸这些尺寸,越大的屏幕在看漫画、读 PDF、双屏、手写等方面越有优势,但随身携带的可能性也越小。鉴于国内越来越封闭的内容生态,选择能安装第三方软件的开放系统也很重要。此外,由于墨水屏设备硬件配置普遍低于平板或手机,安装多个软件后系统是否流畅也影响着日常使用频率。

墨水屏阅读器是个相对小众的市场,建议在购买前多做一些功课,最好还能去线下店看一看、用一用,实际感受完再做决定。

数字内容从哪里来?

最后聊聊我的数字内容来源。

首先肯定少不了购买。虽然 Kindle 中国电子书店关了,但国产墨水屏阅读器都带有国内书城软件。例如文石自带了京东和得到,购买体验非常顺畅,只是书籍还不够丰富。微信读书也属于必装软件,大部分墨水屏阅读器都可以用(甚至在 Kindle 封闭的系统上,也可以通过访问 https://r.qq.com 来使用网页版)。此外,我还经常在图灵社区购买技术类电子书。

其次是订阅。主要有两类,一类是 RSS 源,我在 Kindle 上主要使用系统浏览器访问 Reabble 来阅读 RSS 订阅。在基于安卓开放系统上的选择就多了,例如文石自带 RSS 阅读器,还可以安装 inoreader、feedly 等三方软件;另一类是微信公众号,经常看的公众号我会通过“在微信读书中阅读”功能添加到「微信读书」书架,然后在「微信读书墨水屏版」中阅读。微信读书 APP 还打通了微信中的文章收藏和阅读浮窗,非常好用。

我也会自己制作电子书。把好的文章收集起来转为 epub 格式,不仅有更好的阅读体验,还不用担心原文失效。这里不介绍 Sigil 和 KindleGen 等复杂的制作工具,分享一个我最近在用,名为 EpubKit 的网页转电子书软件,作者是一位优秀的博主,也是一位优秀的创意工作者(他本人不称自己为「独立开发者」)。这个软件界面美观、功能简洁明了,非常好上手,符合我心中优秀软件的定义。它能免费使用但功能上会有限制,支持 macOS / Windows 两大平台,感兴趣的话可以先试用。

最后不得不提一下 Z-Library 这个互联网上最大的共享电子书库,由于版权问题导致一直被各国封锁,甚至它的 Google 结果前几条一度被山寨仿冒占领。我一般用它来找教材和老书,推荐配合自建的 Z-Library Telegram Bot 来使用。

以上就是今天分享的全部内容。

本文链接:https://imququ.com/post/ebook-reader.html参与讨论

我失业了

2024-04-10 12:54:05

🚫 本文禁止一切形式转载 | 本文禁止一切形式转载 | 本文禁止一切形式转载

今天来跟大家聊一聊最近失业的事情。先简单介绍下个人情况:二个孩子的奶爸(儿子五岁❤️,女儿今天出生❤️),37.5 岁,工作 16 年,坐标北京,一直在互联网大厂工作,最近这份工作司龄即将满三年,学历一本 + 北大光华 MBA。

不怎么太平的三月

关于所在业务要精简团队规模的传言似乎从没断过。对这个业务,网上非常多分析文章都指向同一个观点“产品真的好用但团队人效实在太低,商业上不可持续”。平心而论,我也觉得我们的产品体验超级棒,也确实存在业务不聚焦、组织过于臃肿等问题,只是没想到这次变化来得这么突然。

我带了一个几十来人的团队,3 月初大团队还在大量招人,每位 Leader 都需要在内网、朋友圈、BOSS 和脉脉发招聘和活水的宣传贴。然而到了 3 月中旬,沟通年度绩效的日子突然被推迟了三天,这时大家心里都明白,这次终于要动真格了。

果然很快,首先是 3/25 晚间 LatePost 发布了一篇意味深长的文章,几个小时后的 3/26 早上,业务内部全员信如约而至。与此同时 HR 也已飞赴各地办公区,根据手里事先敲定的名单和方案,启动协商解除劳动合同沟通。在这件事上,我见到了久违的高效和专业。

我们都在名单里

团队里有几个非常靠谱的小伙伴需要离开。其中一个小伙伴在内网发了篇帖子大意是“为了上班刚买了辆车,结果告诉我这班不用上了”,引发不少同事讨论他新买的车。有小伙伴问我为什么他在名单里,我说不知道,我刚知道自己也在名单里,他开始转为安慰我。

我盘了下认识的其他部门 Leader,把我的小伙伴推荐过去,希望他们还能继续留在公司。对方 Leader 跟我透露,现在不少人都在看活水(这次不受影响的同事肯定也担心自己的未来),因此他们需要多方比较才能做出决定。

站好最后一班岗

大家心态都很好。公司有个传统,离职前会给内部 IM 设置告别的头像,有的小伙伴考虑活水无望,已经换上了告别头像,但还在开发需求并活跃在工作群里。这些即将离开的同学基本都是一线开发,事发突然,3 月底要交付的卡点需求一时不好交接,还得他们来跟进。

我自己一直是隔周出差一周,没怎么顾得上家里,老大之前得了百日咳一直没好彻底有阵子没去幼儿园,老二预产期在 4 月,已经很近了。直到被通知那一刻前,我的心思都还在业务交付、招聘和年度绩效沟通上。

收到通知的瞬间,我大脑一片空白,然后心里产生很多疑问和想要表达的内容,关于业务、组织、团队等等。在与我的 HRBP 深入沟通后,她建议我把精力放在照顾家里上,我权衡了一下,很快在协商协议上签字,简单交接后 4 月初就开始休假了(公司可以在预产期前 15 天休陪产假,宝宝出生后有育儿假可以用)。这里需要感谢我的 BP,这个建议对我帮助很大。

赋闲在家,赏花拍花

去年年底一直胸闷,怀疑自己得了心肌炎,但体检又一切正常。这次休假在家,我约了心理咨询,沟通完发现抑郁和焦虑情况都非常严重,半年前就有迹象。咨询师建议尽快去医院进一步诊断和药物治疗,我没去,因为我觉得这是互联网大厂员工普遍都会遇到的。

开始休假后,我每天在楼下海棠花溪散步一小时,再去咖啡厅写写东西,远离工作再加上为女儿的到来做准备,已经让我完全从负面情绪中走了出来。

从我家的窗户看出去,便是望京海棠花溪这个新晋网红打卡点的全貌,有一天我把随手拍的视频发在小红书上,居然获得不少点赞和收藏。于是这些天赏花拍花发小红书,不亦乐乎。有朋友半开玩笑说,你要不趁机转型当大厂离职赛道博主,你那些关键词放上去肯定有流量。我笑了笑,你怕是不知道今年这个赛道有多卷,这就是个自娱自乐的小插曲。

wangjing haitang huaxi

给自己放个长长长假

回到正题,知晓自己即将失业两周后的今天,我可以非常平静地面对这件事。把这段经历写出来,不是为了说公司、业务或团队的不好,公司首要任务和主要职责就是为股东创造价值并确保股东利益最大化,业务发展不顺时进行组织调整正常且正确。希望业务经历这次调整后能真正做到“方向更聚焦、组织更高效,团队更有战斗力”,还希望自己未来无论是去下一家公司上班还是创业,都能继续用这么好的软件。

“时代的一粒灰,落在个人头上就是一座山”,每个人都有被大山压垮的可能。对我而言,因为之前就很清楚互联网行业不可能一直增长,与家人认真讨论过失业危机,在财务上做了一些准备。这次失业反而成为一个难得的契机,让我工作 16 年终于可以好好休息一段时间,陪陪家人带带娃,思考并规划下一段职业生涯。

在两年前的那篇博客里,我写到:

在人口红利、技术红利消失之后,政策红利必然会消失,互联网行业已经步入成熟稳定的发展阶段。短期往长期发展的过程,是我们能进行调整的窗口期。我个人的做法是:
1)努力工作,保证稳定的现金流入;
2)多读书,尤其是历史和宏观经济,从更大尺度看问题有助于保持内心平静;
3)保持心身健康,做好长线发展准备;
4)保持与各行各业的交流,关注大的趋势,避免落入信息孤岛;
5)控制负债,保证现金流正常,做一些投资规划;

两年后的今天,整个大环境又有不少变化,我知道身边有很多人处在是否踏入互联网行业的犹豫中,或者处于卷不赢躺不平的职业焦虑中,甚至失业了不敢对家人讲。

我希望通过对自己这段经历「客观、平和」的记录,能给大家内心带去一些平静,避免陷入到「是我不够优秀,是我做得不好才被选中」的自我否定中。还希望接下来与大家共同探讨和研究,如何提早做好规划和预案,使我们在物质和精神两个层面上,都能从容面对这个时代各种坏的可能

注:类似调整原因、比例、名单规则等等问题请不要问我,我不知情也不会去猜测和讨论。并且无论在职还是离职,我都会确保言行不触碰公司红线。除此之外,各种话题都欢迎留言讨论,或者邮件联系【quguangyu(at)gmail.com】

本文链接:https://imququ.com/post/i-am-unemployed.html参与讨论

记一次图片访问异常排查过程

2023-04-28 21:55:16

本文首发于掘金,为了后续统一管理,现搬回我的博客,部分内容有更新。

几个月前,收到掘友反馈无法查看自己的头像图片(p*-passport.byteacctimg.com),站内其他图片(p*-juejin.byteimg.com)正常。

掘金用户头像直接使用公司公共的 Passport CDN 地址,按说不应该有问题,本地无法复现,反馈量也不大,一周几条,但一直持续。

排查过程

考虑到掘金用户大部分都是程序员,让掘友协助排查最高效。在反馈后评论留言,很快就联系到两名掘友。

可以 Ping 通域名

图片无法访问首先要怀疑 DNS 和网络连通性。让掘友 Ping 头像域名,一切正常,返回的 ip 没有被劫持,DNS 和网络连通没有问题。

ping

浏览器提示连接被重置

再让掘友用 Chrome 浏览器直接访问图片 URL,报错信息为 ERR_CONNECTION_RESET

chrome err connection reset

网络环境阻断了连接

两名掘友都表示,只有在公司访问掘金才出现问题,结合浏览器给出的“连接被重置”信息,怀疑所在网络对该域名进行了拦截。

一个小知识点:拦截 HTTPS 请求经常会用到 SNI 信息。

为了在同 IP 部署多个 HTTPS 服务,TLS 提供名为 SNI(Server Name Indication)的扩展,现代浏览器都支持 SNI。浏览器发送 Client Hello 握手时,会把请求 URL 中的 Host 明文放在 SNI 扩展中传给服务器,便于服务器返回正确域名的 HTTPS 证书。通过 SNI 信息,防火墙可以轻松拦截特定 Host 的 HTTPS 连接。

via:https://imququ.com/post/sth-about-switch-to-https-2.html#toc-2

先让掘友用 curl 进行了测试:

curl -vvv https://p3-passport.byteacctimg.com/img/user-avatar/05f83c4592a0bf379722fbe74730238c~300x300.image

可以看到,TCP 连接正常,但 TLS 握手过程被中断,这与在浏览器中的表现一致,这也说明该问题与浏览器无关。

curl failed

又让掘友测试了不带 SNI 的 TLS 握手(OpenSSL 1.1.1 及之后的版本默认发送 SNI,可通过 -noservername 参数禁用):

openssl s_client -connect p3-passport.byteacctimg.com:443 -noservername

可以看到,不带 SNI 时,顺利完成 TLS 握手,服务端返回了 TLS Session Tikect。

tls without sni

再让掘友测试带 SNI 的 TLS 握手(OpenSSL 1.1.1 之前的版本,可通过 -servername 参数发送 SNI 信息):

openssl s_client -connect p3-passport.byteacctimg.com:443 -servername p3-passport.byteacctimg.com

可以看到,这次 TLS 握手失败(Cipher 为空)。

tls with sni

由此可以判断,掘友网络环境针对 p*-passport.byteacctimg.com 域名进行了阻断。

问题解决

查明原因之后,临时解决方案是更换域名。新域名替换上线后,掘友反馈一切恢复正常。

为什么有些网络环境会屏蔽 byteacctimg.com?我没有最终结论,但大概率是被当成广告域名来拦截了:

用来上报日志的域名很容易被广告 / 隐私过滤规则库收录,从而被拦截。上报日志场景最好申请单独域名,不要与业务内容域名混用。估计是 byteacctimg.com 某个子域曾被用于上报日志,导致被 adblock list 收录。

另外,为了解决 SNI 明文传输的问题,TLS 提供了 ESNI(Encrypted SNI)、ECH(Encrypted Client Hello)等扩展,这里不过多介绍,有兴趣的同学可以点击:https://blog.cloudflare.com/encrypted-client-hello/

本文链接:https://imququ.com/post/an-avatar-issue.html参与讨论

聊聊一些近况

2022-03-19 13:47:45

真的有很多年没更新博客了,写博客这件事真的不能停,一停下来就完全不知道该怎么开始了。

北京的初春雪景格外美 ── 可惜我只能透过酒店的窗户看到一小部分 ── 因为去过新冠确诊病例所在楼层而被集中隔离。酒店条件、餐食都不错,我本人也很健康,只是 21 天不能出房间实在有点久,久到居然想到在博客上写点东西。

beijing snow

博客上次更新还在 2017 年,这五年来发生的事情太多太多,就连疫情都迎来了第三个年头。想写的东西有点多,今天先从自己近期工作和生活上的一些变化聊起。

提示:本文纯属闲聊,没有任何干货。

聊聊工作

有些热心的读者已经发现,前些时我修改了“关于”页面上的工作经历部分。是的,21年5月,在 360 工龄还差三个月满十年之际,我选择了离开。因为一些原因,当时我没有在任何场合提及这件事,但离开一家工作十年的公司远没想象中的那么容易。

我的第一份工作是在百度电商部门。三年时间,极具挑战的项目和业内顶尖的同事,让我具备了一名合格工程师所需技能,也让我见识到一个优秀团队需要具备的所有条件。感谢百度!

而我人生中更多的重要时刻,则是与我工作过的第二家公司奇虎 360 交织在一起。感恩 360 能让我在北京站稳脚跟;感恩我的历任领导一直赋予我更大的 Scope,给予充足的成长空间,帮助我快速成长;更要感恩一起并肩作战过的伙伴们,让我有了永远珍藏的回忆和不虚此行的十年。祝愿 360 公司能再次突破,走上新的高度,也祝愿 360 小伙伴们拥有更加美好的未来。

thanks to 360 sousuo

现在我所负责的业务是稀土掘金,一个专注于开发者成长的技术社区,现在属于字节大家庭中的一员。相信技术圈子尤其是前端研发的小伙伴早已熟悉掘金。

我的职业生涯缘起 51js 一个帖子。当时被大雪所困回不了家,我在寝室写了一个 WebIM 发到 51js 社区,引发了广泛讨论,也得到嗷嗷、Winter 和月影三位版主的关注,从而让我走上现在的道路。交流和成长是我对技术社区的初心,相信掘金未来可以更好地服务好开发者,帮助各个阶段的开发者更好地成长,请多多支持我们。

掘金除了写作平台,还有面向大学生的青训营,面向体系化学习的掘金课程,专注于拓宽技术和管理视野的掘金直播,每年还会举办面向行业和未来的稀土掘金开发者大会。欢迎大家来掘金写小册、开直播、担任大会演讲嘉宾,请直接联系我。

下周四(2022/03/24),掘金将邀请尤雨溪和两位字节前端专家一起,聊聊 Vue 3.0 和前端新趋势,我是本次活动主持人,欢迎捧场!现在就可以预约直播

近期工作变动广告完毕,再来谈谈我对工作的看法。我不擅投资,买房之外所有投资均以惨败告终;我也不能接受创业风险,要养娃还要还房贷,工资是我长期稳定获取收入的主要来源。从这个角度讲,公司发展得好,个人才会有更大的利益,这也是我一直以来信奉的理念。但在工作中,我见过一些总是与公司站在对立面的员工,每天带着对公司的怨恨来上班,甚至巴不得公司早日垮掉。这让我很不能理解,这么痛苦为什么不早点换家自己喜欢的公司,如果能力不足以支撑跳槽而只会整天抱怨,则是极其不成熟的做法。从个人角度,如何选择好的公司、如何与公司一起长期发展,有一些技巧和规律,后续有机会在写。

再来说说咱们行业。前些时看完《沸腾新十年》,感慨诸多,作为亲历者参与互联网这惊心动魄的十多年,与之前读《沸腾十五年》的感受完全不一样。最近算法伦理、平台垄断、公司层面 ESG、个人层面 WLB 等话题,成为新的讨论热点,越来越多的人唱衰互联网,身边不断有人通过退休/换行业/进体制内离开互联网,同时新的进入者也很多,简历和面试环节竞争越来越激烈。

对于科技进步和行业发展,人们总是会短期高估而长期低估,我对互联网行业变化的看法也是如此。最近,不少人看到有公司裁员就会寝食难安,我觉得没必要恐慌,除了少数严重缺乏职场竞争力的老人和尚未进入职场的新人要早作打算之外,我们大部分人短期内还不会涉及到裁员。而且只要行业保持健康,重新找一份工作也不难。看长远一些,生产力要素的变化、互联网作为基础设施权责的变化、行业资本与估值的变化、软科技向硬科技的变化等因素对互联网行业产生的长远影响,才是更应该关注和思考的。

在人口红利、技术红利消失之后,政策红利必然会消失,互联网行业已经步入成熟稳定的发展阶段。短期往长期发展的过程,是我们能进行调整的窗口期。我个人的做法是:1)努力工作,保证稳定的现金流入;2)多读书,尤其是历史和宏观经济,从更大尺度看问题有助于保持内心平静;3)保持心身健康,做好长线发展准备;4)保持与各行各业的交流,关注大的趋势,避免落入信息孤岛;5)控制负债,保证现金流正常,做一些投资规划。

这部分是个人观点,仅供参考。

聊聊生活

生活上一个非常大的变化是有了小屈屈,如今他已经三岁,健康快乐可爱。

另外一个变化是我重新走进了校园。20 年 7 月我在明明老师的强烈建议下,报考了北大光华 MBA 项目。在经历两轮在线资料提交、线下面试、研究生统一招生考试、政治考试、政审之后,我终于在 21 年 9 月成为一名登记在册的非全日制学生,这也给我的生活带来了巨大的改变。

说几点最大的感受:

同学:来自各行各业,经历、背景、能力都非常优秀,会学也会玩,有趣且兼容并包。这一点对于长期混迹于互联网尤其是技术圈子的我,无疑是打开了新世界的大门。

课程:形式多样,种类丰富,课堂上同学们的补充发言是亮点。基础课涵盖了财务会计、数据分析、经济学、战略/营销/运营管理、组织行为学等商科基础,选修方向也非常多,例如金融与投资、创新创业等等。时间方面,前两个学期几乎所有周末时间都会用来上课,每学期两个 Quater,每个 Quater 安排四门主课,上课 + 个人作业 + 小组作业 + 考试,非常充实;第三学期开始课程内容集中在选修和写论文上,会好很多。

校园:北大校园特别美,一塔湖图,食堂饭菜好吃还便宜。感谢北大人性化的疫情防控政策,让我们始终可以线下上课,虽然周末早起有难度,但在校园内上课的体验是网课远不能比的。

Guanghua School of Management

由于 MBA 并不是一个普适性话题,这里不做过多介绍。一些基本信息请自行了解(学制、学籍学历、录取条件、收费等)。如果对此感兴趣欢迎留言,我会根据问题情况,后续看看是否邀请几个同学做场直播。

刚看了下周的日历,又将是充实的一周,擼起袖子加油干,我们都有美好的未来,共勉!

本文链接:https://imququ.com/post/my-recent-work-and-life.html参与讨论

如何为 ThinkJS 3 网站优化 TTFB 时间

2017-11-28 13:40:19

今年早些时候,奇舞团开源的 Node.js 框架 ── ThinkJS 迎来了她的 3.0 版本。尽管今年我很少更新博客,但「每次 ThinkJS 发布大版本,我都要更新博客程序」的老传统还是不能丢。所以,你现在看到的这个博客,已经是基于 ThinkJS 3 全面重构后的新版。

ThinkJS 3 基于 Koa 2.x 开发,内核实现得非常小巧,框架通过 Middleware(兼容 Koa)、Adapter、Extend 等机制来扩展出强大而丰富的功能。按照惯例,ThinkJS 大版本之间无法平滑进行,但这次升级带来的工作量不算太大,本站的升级工作花了一下午全部完成。

基于 ThinkJS 开发的网站普遍都很快,这篇文章我打算聊聊如何为 ThinkJS 3 网站优化 TTFB 时间,使之变得更快。

Time to first byte(简称 TTFB)时间,又称首字节时间,是 WEB 性能优化中非常重要的指标。它代表着从浏览器发起 HTTP 请求到收到 HTTP 响应第一个字节的这段时间,包含了 DNS 解析、建立 TCP 连接、建立 SSL 连接、发送 HTTP 请求、网络传输、服务端处理、30X 重定向等阶段。在影响 TTFB 所有因素中,服务端程序何时输出响应决定了服务端处理时间的长短,也是本文关注的优化目标。

优化 WEB 页面的 TTFB 时间除了要尽可能优化业务逻辑之外,还有两个常用技巧:

前者无非就是先尽快输出一个无服务端复杂逻辑的空壳页面,再发起 ajax、jsonp 等异步请求填充内容。这种方案不利于 SEO,比较适用于单页应用。

像本站这种以内容为主的 Web 页面,非常适合采用第二个技巧来优化 TTFB 时间。本文重点介绍它。

分块传输响应需要用到我之前介绍过的 HTTP Transfer-Encoding: chunked 机制。有了这个机制,服务端可以随时将已经完成的部分响应发送给给客户端,而不必等待全部完成后再一次发送。浏览器拿到部分响应,就能解析并执行其中的 HTML、CSS 和 JS 代码,还能加载其中引用的子资源,最终让用户更快看到部分页面内容。分块传输也是 Facebook 在 2009 年实现的 Bigpipe 方案的理论基础,这里不再赘述。

再来说说 ThinkJS。

在 ThinkJS 之前几个版本中,我们可以通过 http.write(content) 发送多个 chunk,再通过 http.end(content) 发送最后一个 chunk,非常方便。

而 ThinkJS 3 使用的 Koa 2.x,只能通过 ctx.body 设置并结束响应,意味着通常情况下响应只能发送一次,还得放在整个 Controller 流程的最后。

通过分析代码,我找到在 ThinkJS 3 中多次发送响应的两种方案:

方案一比较正统;方案二则危险得多,官方都说要后果自负:

Bypassing Koa's response handling is not supported. Avoid using the following node properties: res.statusCode, res.writeHead(), res.write(), res.end(). via

所以,本站最终采用了方案二。Koa 总共就几个文件,出啥奇怪的问题都不怕。

下面开始贴代码。

1)创建 Controller 的 Extend 文件 src/extend/controller.js

const firstChunkMinLength = 4096;

module.exports = {
    async renderAndFlush(tpl) {
        let content = await this.render(tpl);

        //first chunk
        if(!this.ctx.headerSent) {
            this.ctx.type = 'html';
            this.ctx.flushHeaders();

            let length = content.length;
            if(length < firstChunkMinLength) {
                content += `<s>${' '.repeat(firstChunkLength - length)}</s>`;
            }
        }

        this.ctx.res.write(content);
    }
};

输出第一个 chunk 之前,需要通过 ctx 的 type setter 和 flushHeaders 方法来输出响应起始行和头部。

第一个 chunk 不能太小,否则会被某些浏览器缓存起来,不会马上显示,达不到我们想要的效果。更多细节可以点开这个 stackoverflow 的链接自己看。另外,我在实际测试中发现,只补空格 iOS Safari 依然不会立刻渲染,把空格放在标签里就没问题。也可能是我的幻觉,欢迎大家测试并指正。

2)在 Controller 里将原本渲染模板的逻辑根据实际情况拆分为多步:

async indexAction() {
    let pageName = 'blog-home';
    let title = 'JerryQu 的小站';
    this.assign({ pageName, title});

    //输出头部和边栏 HTML
    await this.renderAndFlush('home/inc/header');

    //查询数据库(耗时操作)
    let pn = this.get('pn');
    let data = await this.model('post').getPostList(pn, 10);
    data.pagerPath = getPagerPath(this.ctx, 'pn');
    this.assign(data);

    //输出剩余 HTML
    return this.display('home/index_post_list');
}

也就是说需要提前发送的模板通过 renderAndFlush 来渲染并发送,剩余模板还是走原有的 display 逻辑。

至此,本文要介绍的优化工作已经完成,赶紧打开浏览器验证一下吧。

但是如果你照着我的代码改造,肯定会遇到不少坑,下面列举几个:

1)原本的异常逻辑重定向到错误页不好用,直接提示 Can't set headers after they are sent. 错误。

这个错误信息已经把原因描述得很明白,HTTP/1 的响应需要严格按照起始行、头部和正文的顺序发送,已经发送了正文,就不能再通过 30X 状态码和 Location 头部来跳转页面。

解决方案:有一些会产生跳转的逻辑例如参数合法性检查,可以挪到发送第一个分块之前来进行。另一些异常跳转逻辑则无法提前,例如查询数据库后发现不存在对应的文章,这种情况可以输出一段 JS 代码在浏览器中跳转,或者直接渲染错误页面。

2)提前输出的模板中部分变量取不到值。

例如本站第一个分块输出了左侧内容,这个分块对应的模板中,有很多字段原本来自数据库查询后的结果,提前渲染必然取不到值。

解决方案:这些需要用到数据库字段的逻辑,有一些可以挪到后续分块中;有一些则不好后移,例如需要动态赋值的页面 <title>,只能放在 <head> 里。一种方案是继续使用万能的 JS,通过后续分块中的 docuemnt.title 来给页面 title 赋值。对于不支持 JS 的 Spider,可以禁用提前输出响应策略。

我使用了另外一套方案:由于文章数量不多,我索性在程序启动时,把全部文章 ID 和标题对应关系从数据库取出来,存在配置中。这样,后续第一个分块获取标题时无需查询数据库。

下面这段代码需要放在 src/bootstrap/worker.js

//HTTP 服务启动前执行
think.beforeStartServer(async () => {
    let postTitle = {};
    (await think.model('post').field(['slug', 'title']).select()).forEach(item => {
        postTitle[item.slug] = item.title;
    });
    think.config('postTitle', postTitle);
});

3)Middleware 中获取到的 ctx.body 不是完整页面。

本方案只有最后一个分块内容才会赋值给 ctx.body,前面分块的输出则完全绕过了 Middleware,出现这种情况是正常的。如果你不能接受,还是老老实实用 Readable Stream 吧。

最后,看完本文,相信你对如何优化 ThinkJS 3 网站的 TTFB 时间有了足够了解,赶紧动手试试吧。遇到任何问题,欢迎留言讨论。

本文链接:https://imququ.com/post/reduce-ttfb-on-thinkjs3-website.html参与讨论

本博客开始支持 TLS 1.3

2017-08-06 14:37:02

更新:在做出「暂停更新」的决定后,本站一些实验性配置已经去除,包括本文提到的 TLS 1.3。我将以最低维护成本保证本站可用。@ 2020/04/22

几个月前,我在升级本博客所用 Nginx 时,顺手加上了对 TLS 1.3 的支持,本文贴出详细的步骤和注意事项。有关 TLS 1.3 的介绍可以看 CloudFlare 的这篇文章:An overview of TLS 1.3 and Q&A。需要注意目前 Chrome 和 Firefox 支持的是 TLS 1.3 draft 18,暂时不要用在生产环境。

更新:目前 Chrome 70 已经支持 TLS 1.3 final,本文已更新。@ 2018/10/19

安装依赖

我的 VPS 系统是 Ubuntu 16.04.3 LTS,如果你使用其它发行版,与包管理有关的命令请自行调整。

首先安装依赖库和编译要用到的工具:

sudo apt-get install build-essential libpcre3 libpcre3-dev zlib1g-dev unzip git

获取必要组件

nginx-ctngx-brotli 与本文主题无关,不过都是常用的 Nginx 组件,一并记录在这里。

nginx-ct

nginx-ct 模块用于启用 Certificate Transparency 功能。直接从 github 上获取源码:

wget -O nginx-ct.zip -c https://github.com/grahamedgecombe/nginx-ct/archive/v1.3.2.zip
unzip nginx-ct.zip

注:大家常用的 Let's Encrypt 证书已内置 SCTs,nginx-ct 模块基本退出历史舞台了。

ngx_brotli

本站支持 Google 开发的 Brotli 压缩格式,它通过内置分析大量网页得出的字典,实现了更高的压缩比率,同时几乎不影响压缩 / 解压速度。

以前要想支持 ngx_brotli 模块,需要先手动编译 libbrotli。经评论里的朋友提醒,现在已经不用了。直接获取源码即可:

git clone https://github.com/google/ngx_brotli.git
cd ngx_brotli

git submodule update --init

cd ../

OpenSSL

为了支持 TLS 1.3 final,需要使用 OpenSSL 1.1.1 正式版:

wget -c  https://github.com/openssl/openssl/archive/OpenSSL_1_1_1.tar.gz
tar xzf OpenSSL_1_1_1.tar.gz
mv openssl-OpenSSL_1_1_1 openssl

编译并安装 Nginx

接着就可以获取 Nginx 源码,编译并安装:

wget -c http://nginx.org/download/nginx-1.15.2.tar.gz
tar zxf nginx-1.15.2.tar.gz

cd nginx-1.15.2

./configure --add-module=../ngx_brotli --with-openssl=../openssl --with-openssl-opt='enable-tls1_3 enable-weak-ssl-ciphers' --with-http_v2_module --with-http_ssl_module --with-http_gzip_static_module

make
sudo make install

enable-tls1_3 是让 OpenSSL 支持 TLS 1.3 的关键选项;而 enable-weak-ssl-ciphers 的作用是让 OpenSSL 继续支持 3DES 等不安全的 Cipher Suite,如果你打算继续支持 IE8,才需要加上这个选项。

除了 http_v2http_ssl 这两个 HTTP/2 必备模块之外,我还额外启用了 http_gzip_static,需要启用哪些模块需要根据自己实际情况来决定。

以上步骤会把 Nginx 装到 /usr/local/nginx/ 目录,如需更改路径可以在 configure 时指定。

WEB 站点配置

在 Nginx 的站点配置中,以下两个参数需要修改:

ssl_protocols              TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # 增加 TLSv1.3
ssl_ciphers                TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5;

包含 TLS13 是 TLS 1.3 新增的 Cipher Suite,加在最前面即可;如果你不打算继续支持 IE8,可以去掉包含 3DES 的 Cipher Suite。

本博客完整的 Nginx 配置,请点击这里查看。

验证是否支持 TLS 1.3

目前最新版 Chrome 和 Firefox 都支持 TLS 1.3,但需要手动开启:

本博客多次推荐的 Qualys SSL Labs's SSL Server Test 也支持验证服务端是否支持 TLS 1.3,非常方便,继续推荐。

本文链接:https://imququ.com/post/enable-tls-1-3.html参与讨论

HTTPS 常见部署问题及解决方案

2016-12-12 23:50:26

在最近几年里,我写了很多有关 HTTPS 和 HTTP/2 的文章,涵盖了证书申请、Nginx 编译及配置、性能优化等方方面面。在这些文章的评论中,不少读者提出了各种各样的问题,我的邮箱也经常收到类似的邮件。本文用来罗列其中有代表性、且我知道解决方案的问题。

为了控制篇幅,本文尽量只给出结论和引用链接,不展开讨论,如有疑问或不同意见,欢迎留言讨论。本文会持续更新,欢迎大家贡献自己遇到的问题和解决方案。

实际上,遇到任何有关部署 HTTPS 或 HTTP/2 的问题,都推荐先用 Qualys SSL Labs's SSL Server Test 跑个测试,大部分问题都能被诊断出来。

申请 Let's Encrypt 证书时,一直无法验证通过

这类问题一般是因为 Let's Encrypt 无法访问你的服务器,推荐尝试 acme.shDNS 验证模式,一般都能解决。

网站无法访问,提示 ERR_CERTIFICATE_TRANSPARENCY_REQUIRED

使用 Chrome 53 访问使用 Symantec 证书的网站,很可能会出现这个错误提示。这个问题由 Chrome 的某个 Bug 引起,目前最好的解决方案是升级到 Chrome 最新版。相关链接:

浏览器提示证书有错误

检查证书链是否完整

首先确保网站使用的是合法 CA 签发的有效证书,其次检查 Web Server 配置中证书的完整性(一定要包含站点证书及所有中间证书)。如果缺失了中间证书,部分浏览器能够自动获取但严重影响 TLS 握手性能;部分浏览器直接报证书错误。

What's My Chain Cert? 这个网站可以用来检查证书链是否完整,它还可以用来生成正确的证书链。

检查浏览器是否支持 SNI

如果只有老旧浏览器(例如 IE8 on Windows XP)提示这个错误,多半是因为你的服务器同时部署了使用不同证书的多个 HTTPS 站点,这样,不支持 SNI(Server Name Indication)的浏览器通常会获得错误的证书,从而无法访问。

要解决浏览器不支持 SNI 带来的问题,可以将使用不同证书的 HTTPS 站点部署在不同服务器上;还可以利用 SAN(Subject Alternative Name)机制将多个域名放入同一张证书;当然你也可以直接无视这些老旧浏览器。特别地,使用不支持 SNI 的浏览器访问商业 HTTPS CDN,基本都会因为证书错误而无法使用。

有关 SNI 的更多说明,请看「关于启用 HTTPS 的一些经验分享(二)」。

检查系统时间

如果用户电脑时间不对,也会导致浏览器提示证书有问题,这时浏览器一般都会有明确的提示,例如 Chrome 的 ERR_CERT_DATE_INVALID。

启用 HTTP/2 后网站无法访问,提示 ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY

这个问题一般是由于 CipherSuite 配置有误造成的。建议对照「Mozilla 的推荐配置CloudFlare 使用的配置」等权威配置修改 Nginx 的 ssl_ciphers 配置项。

有关这个问题的具体原因,请看「从启用 HTTP/2 导致网站无法访问说起」。

网站无法访问,提示 ERR_SSL_VERSION_OR_CIPHER_MISMATCH

出现这种错误,通常都是配置了不安全的 SSL 版本或者 CipherSuite —— 例如服务器只支持 SSLv3,或者 CipherSuite 只配置了 RC4 系列,使用 Chrome 访问就会得到这个提示。解决方案跟上一节一样。

还有一种情况会出现这种错误 —— 使用不支持 ECC 的浏览器访问只提供 ECC 证书的网站。例如在 Windows XP 中,使用 ECC 证书的网站只有 Firefox 能访问(Firefox 的 TLS 自己实现,不依赖操作系统);Android 平台中,也需要 Android 4+ 才支持 ECC 证书。

针对不支持 ECC 证书的浏览器,有一个完美的解决方案,请看「开始使用 ECC 证书」。

在 Nginx 启用 HTTP/2 后,浏览器依然使用 HTTP/1.1

Chrome 51+ 移除了对 NPN 的支持,只支持 ALPN,而浏览器和服务端都支持 NPN 或 ALPN,是用上 HTTP/2 的大前提。换句话说,如果服务端不支持 ALPN,Chrome 51+ 无法使用 HTTP/2。

OpenSSL 1.0.2 才开始支持 ALPN —— 很多主流服务器系统自带的 OpenSSL 都低于这个版本,所以推荐在编译 Web Server 时自己指定 OpenSSL 的位置。

详见「为什么我们应该尽快支持 ALPN」。

升级到 HTTPS 后,网站部分资源不加载或提示不安全

记住一个原则:HTTPS 网站的所有外链资源(CSS、JS、图片、音频、字体文件、异步接口、表单 action 地址等等)都需要升级为 HTTPS,就不会遇到这个问题了。

详见「关于启用 HTTPS 的一些经验分享(三)」。

仅 Safari、iOS 各种浏览器无法访问

如果你的 HTTPS 网站用 PC Chrome 和 Firefox 访问一切正常,但 macOS Safari 和 iOS 各种浏览器无法访问,有可能是 Certificate Transparency 配置有误。当然,如果你之前没有通过 TLS 扩展启用 Certificate Transparency,请跳过本小节。

具体症状是:通过 Wireshark 抓包分析,通常能看到名为 Illegal Parameter 的 Alert 信息;通过 curl -v 排查,一般能看到 Unknown SSL protocol error in connection 错误提示。

这时候,请进入 Nginx ssl_ct_static_scts 配置指定的目录,检查 SCT 文件大小是否正常,尤其要关注是否存在空文件。

需要注意的是:根据官方公告,从 2016 年 12 月 1 日开始,Google 的 Aviator CT log 服务将不再接受新的证书请求。用 ct-submit 等工具手动获取 SCT 文件时,不要再使用 Aviator 服务,否则就会得到空文件。

更新:nginx-ct 的作者已经发布了 v1.3.2,针对零字节的 SCT 文件做了处理,不再发送。

将 OpenSSL 升级到 1.1.0+,IE8 无法访问

造成这个问题的根本原因是 OpenSSL 1.1.0+ 默认禁用了 3DES 系列的 Cipher Suites:

For the 1.1.0 release, which we expect to release tomorrow, we will treat triple-DES just like we are treating RC4. It is not compiled by default; you have to use “enable-weak-ssl-ciphers” as a config option. via

升级到 OpenSSL 1.1.0+ 之后,要么选择不支持 Windows XP + IE8;要么在编译时加上 enable-weak-ssl-ciphers 参数。例如这是我的 Nginx 编译参数:

./configure --add-module=../ngx_brotli --add-module=../nginx-ct-1.3.2 --with-openssl=../openssl --with-openssl-opt='enable-tls1_3 enable-weak-ssl-ciphers' --with-http_v2_module --with-http_ssl_module --with-http_gzip_static_module

本文链接:https://imququ.com/post/troubleshooting-https.html参与讨论

开始使用 VeryNginx

2016-12-10 23:35:53

VeryNginx 是一个功能强大而对人类友好的 Nginx 扩展程序,这是作者的原话。很久之前我就看到过这个项目,直到最近我才在本站试用了一把,确实好用,于是想通过本文把它介绍给更多人。

VeryNginx 主要由两部分组成:基于 lua-nginx-module 开发的 Lua 脚本,以及基于 HTML/CSS/JS 开发的 Web 控制面板 —— 用于生成和管理 Lua 脚本所需配置。

lua-nginx-module 能让 Lua 脚本直接跑在 Nginx 内部,比用 C 语言开发 Nginx 模块更容易上手,同时还能充分利用 Nginx 的非阻塞 I/O 模型,非常适合开发功能复杂、性能优异的 Web 应用。它也是大家熟知的 OpenResty 套件中一个最核心的模块。

VeryNginx 通过在请求的不同阶段(如 init_by_lua*/rewrite_by_lua*/access_by_lua*/log_by_lua*)执行不同 Lua 脚本,实现给请求打标签及对拥有不同标签的请求进行不同的处理的功能。除此之外,它还支持常见的统计报表展示。

安装 VeryNginx

VeryNginx 依赖以下三个 Nginx 模块:

如果对 Nginx 没有定制化需求,建议直接使用 VeryNginx 默认的安装脚本,它会同时装好 VeryNginx 自身和 OpenResty 套件,最为方便。具体步骤请查看官方文档

对于我这样喜欢各种折腾 Nginx 的人来说,修改之前的 Nginx 编译步骤,把上面三个模块加进去,也不算复杂。具体步骤后面再介绍,先来搞定 VeryNginx 工具本身。

这一步很简单:下载 VeryNginx 最新版代码并安装即可:

wget https://github.com/alexazhou/VeryNginx/archive/v0.3.3.zip
unzip v0.3.3.zip

cd VeryNginx-0.3.3/
sudo python install.py install verynginx

cd ../

安装 VeryNginx 用到了 Python 脚本,但这个项目跟 Python 没有半毛钱关系,不信可以看下 install.py 中的 install_verynginx 方法,只做了拷贝文件和修改配置目录权限两件事。

VeryNginx 默认会被装到 /opt/verynginx/ 目录,本文使用默认配置。

编译 Nginx

VeryNginx 依赖的 http_stub_status_modulehttp_ssl_module 只需要在 configure 时加上就可以。lua-nginx-module 稍微麻烦一点,它有以下依赖:

下面分别来搞定它们。本文使用 Ubuntu 16.04.1 LTS,全部采用默认路径安装。如果你的环境跟我不一样,一些命令请自行调整。

LuaJIT

下载并安装 LuaJIT

wget http://luajit.org/download/LuaJIT-2.1.0-beta2.zip
unzip LuaJIT-2.1.0-beta2.zip

cd LuaJIT-2.1.0-beta2/
make
sudo make install

cd ../

设置环境变量:

export LUAJIT_LIB=/usr/local/lib
export LUAJIT_INC=/usr/local/include/luajit-2.1/

ngx_devel_kit

下载并解压 ngx_devel_kit

wget https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.zip
unzip v0.3.0.zip

ngx_lua

下载并解压 ngx_lua

wget https://github.com/openresty/lua-nginx-module/archive/v0.10.7.zip
unzip v0.10.7.zip

Nginx

本站编译 Nginx 的详细步骤,都记录在这篇文章,可以照搬。只有 configure 要改一下:

./configure --with-ld-opt="-Wl,-rpath,/usr/local/lib/" --add-module=../ngx_devel_kit-0.3.0 --add-module=../lua-nginx-module-0.10.7 --add-module=../ngx_brotli --add-module=../nginx-ct-1.3.2 --with-openssl=../openssl --with-http_v2_module --with-http_ssl_module --with-http_gzip_static_module --with-http_stub_status_module

make
#make install 前请务必停止已有 Nginx 服务,sudo /etc/init.d/nginx stop
sudo make install

编译并安装好 Nginx 之后,建议通过 -V 参数再次确认:

/usr/local/nginx/sbin/nginx -V

nginx version: nginx/1.11.7
built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4)
built with OpenSSL 1.0.2j  26 Sep 2016
TLS SNI support enabled
configure arguments: --with-ld-opt=-Wl,-rpath,/usr/local/lib/ --add-module=../ngx_devel_kit-0.3.0 --add-module=../lua-nginx-module-0.10.7 --add-module=../ngx_brotli --add-module=../nginx-ct-1.3.1 --with-openssl=../openssl --with-http_v2_module --with-http_ssl_module --with-http_gzip_static_module --with-http_stub_status_module

配置 VeryNginx

在 Nginx 中引入 VeryNginx 的配置文件,就可以让 VeryNginx 工作起来。首先要修改的是 Nginx 的主配置,一般位于 /usr/local/nginx/conf/nginx.conf

在主配置文件的最外层,加入以下配置:

include /opt/verynginx/verynginx/nginx_conf/in_external.conf;

在主配置的 http 段落中,加入以下配置:

include /opt/verynginx/verynginx/nginx_conf/in_http_block.conf;

在具体站点配置的 server 段落中,加入以下配置:

include /opt/verynginx/verynginx/nginx_conf/in_server_block.conf;

加完之后,建议通过 -t 参数确保配置无误:

/usr/local/nginx/sbin/nginx -t

如果提示 test is successful,说明配置无误,可以重启 Nginx 服务;否则请根据提示排查。

如果一切顺利,访问 http://yourdomain.com/verynginx/index.html 就可以见到 VeryNginx 的 Web 控制面板。默认用户名和密码都是 verynginx,登录后请务必修改。

使用示例

VeryNginx 使用非常简便,基本上不需要做过多说明。只是有一点需要注意:在 Web 控制面板中对任何配置项进行增删改之后,在点击页面右下角「Save」按钮之前并不会生效;点击「Reload」可还原到上一次配置。

VeryNginx 可以根据多种特征(Client IP、Host、UserAgent、URI、Referer、Request Args)来组合出不同的规则,用来给请求打上标记(Matcher);可以给拥有不同标记的请求指定不同的处理动作(Custom Action)。

下面通过一个实际案例来演示 VeryNginx 的基本用法。

最近我发现某搜索引擎对本站的索引中,有大量重复内容(一共索引了 5000 多条记录,其他搜索引擎都只有几百):

imququ.com in sogou

一般来说,并不是说搜索引擎收录的页面越多越好,相反如果收录的不同 URL 都指向了同样的内容,很可能被判作弊,从而导致站点被降权。

从访问日志中,可以看到这家搜索引擎在大量抓取本站首页,并带上了一个无意义的 p 参数:

106.120.173.72 - - [10/Dec/2016:05:50:43 +0800] "GET /index.html?p=142&pn=7 HTTP/1.1" 200 6098 "-" "Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)" 0.007 0.007
106.120.173.72 - - [10/Dec/2016:05:50:53 +0800] "GET /index.html?p=134&pn=1 HTTP/1.1" 200 4793 "-" "Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)" 0.007 0.007
106.120.173.72 - - [10/Dec/2016:05:51:03 +0800] "GET /index.html?p=94&pn=1 HTTP/1.1" 200 4793 "-" "Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)" 0.006 0.006

本站没有使用 p 参数,这样会导致 Spider 抓取的页面虽然 URL 不一样,但内容完全一样,从而导致大量重复索引。如果是 Google 出现这种情况,可以通过 Google Webmaster 告诉 Spider 忽略指定参数。但这家搜索引擎的站长平台我一直无法认证成功,所以这条路不通。

有了 VeryNginx,这种情况就很好处理了。首先通过 UserAgent 是否包含关键字、请求中是否存在 p 参数两个条件,对流量进行标记:

verynginx matcher

然后使用「Filter」这个 Custom Action,直接将拥有这个标记的流量响应为 404:

verynginx action

在 Web 控制面板保存配置后,立即生效。马上来测试一下:

curl -I -H'User-Agent: Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)' 'https://imququ.com/?p=95&pn=17'

HTTP/1.1 404 Not Found
Server: nginx
... ...

是不是很棒!通常,如果搜索引擎发现某个网址多次无法访问,就会从将其从索引库及 Spider 抓取列表中移除。

可以看到,使用 VeryNginx 对特定流量进行标记和干预,比直接修改 Nginx 配置方便得多,也强大得多。除了前面演示的「Filter」之外,VeryNginx 还提供了「Scheme Lock、Redirect、URI Rewrite、Browser Verify、Frequency Limit」这几个 Custom Action,其中「Browser Verify」可以用来验证发起请求的客户端是否支持 Cookie 或者 JavaScript,达到防 CC 攻击的目的。

大家都知道,我特别关注本站的访问速度。经过这段时间的试用,VeryNginx 在请求处理和内存占用上的表现,都令人满意。

本文就写到这里,如果想要了解 VeryNginx 更多细节,推荐查看官方文档

本文链接:https://imququ.com/post/use-verynginx.html参与讨论