<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>~iany/</title><link>https://blog.iany.me/zh/</link><description>~iany/的最新内容</description><language>zh-CN</language><managingEditor>me@iany.me (Ian Yang)</managingEditor><webMaster>me@iany.me (Ian Yang)</webMaster><copyright>CC-BY-SA 4.0</copyright><lastBuildDate>Wed, 10 Apr 2024 19:32:40 +0800</lastBuildDate><atom:link href="https://blog.iany.me/zh/index.xml" rel="self" type="application/rss+xml"/><item><title>解决 Windows 中文输入法下 Shift + Space 快捷键的困扰</title><link>https://blog.iany.me/zh/2024/04/resolve-the-annoying-shift-space-shortcut-in-windows-chinese-input-methods/</link><pubDate>Wed, 10 Apr 2024 19:32:40 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2024/04/resolve-the-annoying-shift-space-shortcut-in-windows-chinese-input-methods/</guid><description>&lt;p&gt;最近我把主要的工作环境转移到了Windows系统。之前 Windows 最困扰我的是 &lt;kbd&gt;Shift&lt;/kbd&gt; + &lt;kbd&gt;Space&lt;/kbd&gt; 的快捷键。在中文输入法状态下稍微打字快一点，就会误触发切换半角全角的功能，打乱输入的节奏。既然我决定长期使用 Windows 系统，就花了一些时间全面解决了这个问题。&lt;/p&gt;
&lt;h2 id="tldr"&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;安装 &lt;a href="https://www.autohotkey.com/"&gt;AutoHotkey&lt;/a&gt; v2 版。将以下代码加入到一个 &lt;code&gt;.ahk&lt;/code&gt; 为后缀的文件中，比如“文档”目录中的 &lt;code&gt;main.ahk&lt;/code&gt; 文件:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;+Space::Send (A_PriorKey = &amp;quot;LShift&amp;quot; ? &amp;quot;+{Space}&amp;quot; : &amp;quot;{Blind}{Shift up}{Space}{Shift down}{LWin}&amp;quot;)
&amp;gt;+Space::Send (A_PriorKey = &amp;quot;RShift&amp;quot; ? &amp;quot;+{Space}&amp;quot; : &amp;quot;{Blind}{Shift up}{Space}{Shift down}{LWin}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;双击文件 &lt;code&gt;main.ahk&lt;/code&gt; 即可生效，如果要启动时自动生效，可以为这个文件创建一个快捷方式，然后将快捷方式移到到“启动”目录中。可以通过以下办法快速打开“启动”目录。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用快捷键 &lt;kbd&gt;Win&lt;/kbd&gt; + &lt;kbd&gt;r&lt;/kbd&gt; 打开运行窗口&lt;/li&gt;
&lt;li&gt;输入 &lt;code&gt;shell:startup&lt;/code&gt; 并使用 &lt;kbd&gt;Enter&lt;/kbd&gt; 确认&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="思路说明"&gt;思路说明&lt;/h2&gt;
&lt;p&gt;&lt;kbd&gt;Shift&lt;/kbd&gt; + &lt;kbd&gt;Space&lt;/kbd&gt; 烦人的地方在于它会吞掉本想输入的空格。输入需要 &lt;kbd&gt;Shift&lt;/kbd&gt; 组合键的符号时，通常在前或者在后要加上空格，比如 &lt;code&gt;:(){}&lt;/code&gt; 这些符号。如果输入速度很快，很可能会在 &lt;kbd&gt;Shift&lt;/kbd&gt; 尚未完全松开时按下 &lt;kbd&gt;Space&lt;/kbd&gt; ，这时候没有输入空格反而是切换了半角全角，如果当前需要使用半角，还得再按一次切换回来。&lt;/p&gt;
&lt;p&gt;基于上述分析，我想实现在按下 &lt;kbd&gt;Shift&lt;/kbd&gt; 键后，如果在按下空格键之前按下了其他键，便视为 &lt;kbd&gt;Shift&lt;/kbd&gt; 键没有及时松开，应该输入一个空格符号。如果在按下 &lt;kbd&gt;Shift&lt;/kbd&gt; 键后立即按下空格键，应保留原来的半角全角切换功能。这样的配置不但能避免误触发，还可以被利用来快速输入 &lt;code&gt;) {&lt;/code&gt; 这样的序列，因为全程 &lt;kbd&gt;Shift&lt;/kbd&gt; 不用松开了。&lt;/p&gt;
&lt;p&gt;上面的 AutoHotkey 代码即实现了这里描述的功能。&lt;/p&gt;
&lt;h2 id="代码说明"&gt;代码说明&lt;/h2&gt;
&lt;p&gt;代码有两行，分别对应的左右手 &lt;kbd&gt;Shift&lt;/kbd&gt; 触发的情况。我尝试过合并成一条但失败了，具体原因我也不清楚，也不知道为什么分开就能工作了。&lt;/p&gt;
&lt;p&gt;下面以左手的第一行为例说明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;+Space::Send (A_PriorKey = &amp;quot;LShift&amp;quot; ? &amp;quot;+{Space}&amp;quot; : &amp;quot;{Blind}{Shift up}{Space}{Shift down}{LWin}&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;AutoHotkey 代码&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;+Space::&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;为 &lt;kbd&gt;Shift&lt;/kbd&gt; + &lt;kbd&gt;Space&lt;/kbd&gt; 设置命令，并只有使用左手的 &lt;kbd&gt;Shift&lt;/kbd&gt; 才能触发。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Send&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;发送指定的按键序列。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(A_PriorKey = &amp;quot;LShift&amp;quot; ? X : Y)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;如果变量 &lt;code&gt;A_PriorKey&lt;/code&gt; 的值等于 &lt;code&gt;LShift&lt;/code&gt; 时返回 X 否则返回 Y。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+{Space}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;发送组合按键 &lt;kbd&gt;Shift&lt;/kbd&gt; + &lt;kbd&gt;Space&lt;/kbd&gt; 以触发其原本的功能，即切换半角全角。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{Blind}{Shift up}{Space}{Shift down}{LWin}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;这么一大串其实只是为了实现在 AutoHotkey 里在 &lt;kbd&gt;Shfit&lt;/kbd&gt; 按下的情况下输入一个空格符号。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;第一个比较重要的点是自动变量 &lt;a href="https://www.autohotkey.com/docs/v2/Variables.htm#PriorHotkey"&gt;A_PriorHotkey&lt;/a&gt;。它记录的是组合键所有键被按下之前上一个按键状态有变化的键。因为组合键最后一个按下的一定是非修饰键，所以如果按下 &lt;kbd&gt;Shift&lt;/kbd&gt; 后立即按下 &lt;kbd&gt;Space&lt;/kbd&gt;，&lt;code&gt;A_PriorHotkey&lt;/code&gt; 就是 &lt;kbd&gt;LShift&lt;/kbd&gt; 或者 &lt;kbd&gt;RShift&lt;/kbd&gt;。相反，如果 &lt;code&gt;A_PriorHotkey&lt;/code&gt; 等于其它值就表示按下 &lt;kbd&gt;Shift&lt;/kbd&gt; 到按下 &lt;kbd&gt;Space&lt;/kbd&gt; 之间有按过其它按键。&lt;/p&gt;
&lt;p&gt;第二个点是用来模拟输入空格的一长串 &lt;code&gt;{Blind}{Shift up}{Space}{Shift down}{LWin}&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;{Blind}&lt;/code&gt; 的作用是让 &lt;code&gt;{Shift up}&lt;/code&gt; 强制修改 &lt;kbd&gt;Shift&lt;/kbd&gt; 的状态改为松开保证后面 &lt;code&gt;{Space}&lt;/code&gt; 输入一个空格符号。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{Shift down}&lt;/code&gt; 是恢复 &lt;kbd&gt;Shift&lt;/kbd&gt; 按下的状态，这样后面可以继续输入还 &lt;kbd&gt;Shift&lt;/kbd&gt; 的组合键。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{LWin}&lt;/code&gt; 不会触发任何操作。它的作用是如果在中文输入法里启用了点按 &lt;kbd&gt;Shift&lt;/kbd&gt; 来切换中英文，在下面的按键序列中会正确输入 &lt;code&gt;: &lt;/code&gt; (冒号+空格) 并且不会误触发中英文切换：
&lt;ul&gt;
&lt;li&gt;按下 &lt;kbd&gt;Shift&lt;/kbd&gt;&lt;/li&gt;
&lt;li&gt;按下 &lt;kbd&gt;:&lt;/kbd&gt;&lt;/li&gt;
&lt;li&gt;松开 &lt;kbd&gt;:&lt;/kbd&gt;&lt;/li&gt;
&lt;li&gt;按下 &lt;kbd&gt;Space&lt;/kbd&gt;&lt;/li&gt;
&lt;li&gt;松开 &lt;kbd&gt;Space&lt;/kbd&gt;&lt;/li&gt;
&lt;li&gt;松开 &lt;kbd&gt;Shift&lt;/kbd&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/tags/ime/">IME</category><category domain="https://blog.iany.me/zh/tags/shortcut/">Shortcut</category><category domain="https://blog.iany.me/zh/tags/windows/">Windows</category></item><item><title>使用 iOS 工作体验报告</title><link>https://blog.iany.me/zh/2020/04/working-on-ios-experience-report/</link><pubDate>Sat, 25 Apr 2020 22:27:22 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2020/04/working-on-ios-experience-report/</guid><description>&lt;p&gt;最近正儿八经尝试使用 iOS 来工作，讽刺的事最终释放 iPad 生产力的竟然是键盘和触控板。这篇文章会记录下我的一些感受。我用的是家里领导手中闲置的 iPad Mini，那些显然在 iPad Pro 上不是问题的问题就不会提及了。&lt;/p&gt;
&lt;p&gt;首先说下结论，对于我来说，iPad 有自己很好的使用场景，和笔记本电脑能形成不错的互补，甚至作为主力机器我觉得自己也能完全适应。但是必须得有备用电脑，因为当正经使用起来的话，还是很容易碰到系统限制，但是又找不到 App 能解决问题的场景。&lt;/p&gt;
&lt;h2 id="app-生态问题"&gt;App 生态问题&lt;/h2&gt;
&lt;p&gt;iOS 的 App 生态已经很好了，但是当你把它作为效率工具来看待，就会发现很多问题。&lt;/p&gt;
&lt;p&gt;举个简单的例子，我有张图片想切成圆角，试了 3 个 Apps：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Procreate: 没有图形工具，不能插入圆角矩形家，也不能把选区弄成圆角。&lt;/li&gt;
&lt;li&gt;Photoshop: 同样没有图形工具，不能直接选择圆角区域。&lt;/li&gt;
&lt;li&gt;Pixelmator: 有图形工具可以插入圆角矩形了，但是找不到蒙版功能，macOS 上据说可以用图片填充图形，在 iOS 上也没找到。最后是把圆角矩形放在下面，图片放在上面，然后把图片尖角部分擦除。但是发现图形选择填充色不能使用拾色器，也不能手动输入 RGB，只能肉眼在色盘上选色。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然最终完成了工作，但是方案效率极低。而且选色的地方必能使用吸管去吸色这种常识到了 iOS 上就得靠 App 作者施舍了。然后我发现这种情况还非常普遍。OmniGraffle 同样选色不提供拾色器。&lt;/p&gt;
&lt;p&gt;上面的例子说明，Apps 作者们还没有完全把 iPad 视作一个正经的效率平台，你能用这些 Apps 完成大部分工作，但是就是差一口气，你需要的一个功能就是在 iOS 版中被阉割了。如果只有某几个 Apps 这样还能说是个别现象，但是经过一段时间体验，我的感受是普遍现象：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上面说的，普遍缺少拾色器，缺少颜色值直接输入。&lt;/li&gt;
&lt;li&gt;OmniGraffle 不能把每个 Artboard 分别导出自己的文件，并且普通导出还会把 Artboard 的线框都包含在里面。另外就是移动元素极易误操作，无数次移动变成了改大小。&lt;/li&gt;
&lt;li&gt;Pixelmator 缺少蒙版，不能把图形加载为选区。&lt;/li&gt;
&lt;li&gt;Enpass 不能按最近使用时间排序。不支持高亮搜索框的快捷键，打开 App 也不会自动把输入焦点放到搜索框。&lt;/li&gt;
&lt;li&gt;Diigo Browser 不能添加网页中的图片到书签中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 Desktop 操作系统中，如果某个 App 缺少某个功能或者某方面不太好用，还能通过各种脚本、自动化工具来补充，但是 iOS 中基本只能等 App 更新新功能了。不过欣慰的是，可以看到这方面在慢慢改善，比如 Photoshop 虽然目前缺了图形和钢笔工具，但是能看出 Adobe 的诚意。&lt;/p&gt;
&lt;h2 id="系统限制"&gt;系统限制&lt;/h2&gt;
&lt;p&gt;比起 Apps 的限制，在 iOS 中系统的限制更难绕过去。&lt;/p&gt;
&lt;p&gt;比如玄学的杀后台 App。因为重度依赖 Diigo 网页标注，本来打算以 Diigo Browser 作为主力浏览器，但是 Diigo 太容易被杀了。比如我在 Diigo 里打开 devdocs.io，在 iVim 里写代码，然后就这两个 App 互相切换，都经常发现它们后台被杀了，切换过去要重新加载。没有试过 Chrome，但是 Diigo Browser 其实是 Chrome 套壳，应该都会有相应的问题。而对于 iVim，意味着 IAP 中的自动恢复上次编辑会话是个必选功能。&lt;/p&gt;
&lt;p&gt;然后是 Files 限制太大，没法自动化。好在找到&lt;a href="https://blog.iany.me/2020/04/write-to-any-ios-files-location-via-working-copy/"&gt;方案可以通过 Working Copy 的双向同步功能解决大部分场景&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="我用的一些-apps"&gt;我用的一些 Apps&lt;/h2&gt;
&lt;p&gt;吐槽归吐槽，好用的 Apps 还是很多。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Diigo Browser。虽说功能太弱，好歹核心的网页划线能用。&lt;/li&gt;
&lt;li&gt;Termius。笔记本电脑上基本上核心工具就是终端和浏览器。当然 Termius 只能 SSH 到服务器。它支持 Mosh，功能齐全，缺憾是字体少了最近很喜欢的 JetBrains Mono，颜色主题不能自定义，默认提供的都不是很喜欢。一直觉得 Blink 会更好，不过我觉得切换过去可能也不会有太大差别。&lt;/li&gt;
&lt;li&gt;iVim。如果没有 iVim 可能我都不会尝试使用 iPad 来工作。如果有本地码代码或者大量文本编辑的需求，并且有外接键盘，那首选一定是 iVim。原汁原味的 Vim，并且还提供了一个简单的 Shell。最重要的是支持 Files，用 &lt;code&gt;idoc dir&lt;/code&gt; 就可以挂载任意目录编辑。稍微麻烦的是因为 Shell 里不包含 git，安装插件包就只能下载好了复制进去了。另外一个小坑是如果设置了 backup，最好把 backupcopy 设置成 yes，不然大慨率会出现文件被移动到备份目录，然后再原来的位置新文件没创建出来。备用编辑器是 Textastic，比如没外接键盘的时候。&lt;/li&gt;
&lt;li&gt;Working Copy。Git 客户端，现在也是文件自动化中的重要一环。&lt;/li&gt;
&lt;li&gt;Drafts 和 iA Writer 承担代码编辑之外的文本编辑工作。使用 Drafts 的原因有两个：支持 TextExpander 和支持 JavaScript 自动化。而 iA Writer 现在是我主力的知识库管理工具。&lt;/li&gt;
&lt;li&gt;Notes, Notability，GoodNotes 手写笔记。三个都在用，分工是 Notes 主要是一些快速的草稿，比如画下想法，或者分屏的时候使用；Notability 里都是导入的课件，在 Coursera 上课可以对着课件记笔记；GoodNotes 用于知识沉淀，主要是笔记的二次整理。&lt;/li&gt;
&lt;li&gt;Studies。知道 Anki 之前就在用了。体验基本和 macOS 版一致，甚至可以说更好。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其它工具因为不在核心工作流中，或者没怎么重度使用，就不一一列举了。&lt;/p&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/tags/apple-ecosystem/">Apple Ecosystem</category><category domain="https://blog.iany.me/zh/tags/ios/">iOS</category></item><item><title>理解 Asymmetric Revocable Commitments</title><link>https://blog.iany.me/zh/2018/01/understand-asymmetric-revocable-commitments/</link><pubDate>Sun, 14 Jan 2018 07:44:51 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2018/01/understand-asymmetric-revocable-commitments/</guid><description>&lt;p&gt;Asymmetric Revocable Commitments 是 Lighting Network 的基础，是非对称加密技术很巧妙的应用。它用于互不信任的双方在链下通过协商完成一系列交易。最终需要提交到链上的只需要最初的创建和最后的结算两个交易，大大提高了交易处理速度。&lt;/p&gt;
&lt;p&gt;Wikipedia 上有&lt;a href="https://en.wikipedia.org/wiki/Lightning_Network"&gt;介绍和例子&lt;/a&gt;。一开始比较懵的是，如果交易可以被 Revoke，一方可以拿走所有资金，为什么不直接提交 Revoke 交易呢？而一系列的交换可以把整个系统当作状态机。如果把每个步骤完成后双方可以做的事，以及最终导致的结果列出来，就可以弄明白为什么了。因为 Revoke 的前提是它对应的交易被提交到链上被执行后才行，而那个交易是自己单方签名后提交给对方的，只有对方才能构造出并提交的。这样 Revoke Key 的作用就是监听链上的交易，一旦发现它对应的交易，马上 Revoke 它已获得所有的资金。&lt;/p&gt;
&lt;p&gt;通过分析也立即明白了在哪些步骤上是有风险的，安全上需要注意什么。&lt;/p&gt;
&lt;p&gt;交易是由买方提起的，买方会提议新的资金划分，把本次交易需要的资金划分给对方，这时对方是可以直接把这个交易提交到链上执行的，简单说就是卖方可以收了钱不发货。可见 Lighting Network 适合高频小额的场景，如果是价值很高，而且不可拆分是不适合使用 Lighting Network 的。&lt;/p&gt;
&lt;p&gt;在安全性方面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成 Revoke Key 的一方必须妥善保管并备份，并做好保密工作，不过一旦给对方后就可以忘记了。而一旦给对方后，对应的交易如果已经签好名并保存了就不能泄露出去，不然对方可以拿去提交然后 Revoke。&lt;/li&gt;
&lt;li&gt;所有接收到的 Revoke Key 都必须保存起来并备份。同时需要监听链上交易随时行使 Revoke 的权利。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原始笔记如下&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;## Initial
### Alice closing (A1)
- Alice: $5 1000 blocks later
- Bob: $5 immediately
### Bob closing (B1)
- Alice: $5 immediately
- Bob: $5 1000 blocks later
## Alice offers $1 payment
### Alice closing (A1)
- Alice: $5 1000 blocks later
- Bob: $5 immediately
### Bob closing (B2)
- Alice: $4 immediately
- Bob: $6 1000 blocks later
Bob can close the channel and get the payment. Alice can get remainder immediately but cannot cancel the payment. But Bob cannot trust the payment yet, since Alice can broadcast the state before the payment first.
## Bob replies the payment
### Alice closing (A2)
- Alice: $4 1000 blocks later
- Bob: $6 immediately
Alice has no reason to broadcast it, since she has a better state of 5:5 split.
### Bob closing (B2)
- Alice: $4 immediately
- Bob: $6 1000 blocks later
## Alice give revocation key to Bob
Bob knows the revocation key, so Alice cannot broadcast prior state A1
- Alice: $5 1000 blocks later
- Bob: $5 immediately
Since Bob can revoke Alice $5 immediately before Alice spend it after 1000 blocks.
Bob ensures he owns $6 now.
## Bob give revocation key to Alice
Although Bob unlikely broadcast its B1 now, but it may become better state for him later, so Bob should also invalidate it by give revocation key to Alice.
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/tags/blockchain/">Blockchain</category><category domain="https://blog.iany.me/zh/tags/cryptography/">Cryptography</category><category domain="https://blog.iany.me/zh/tags/payment-channel/">Payment Channel</category></item><item><title>个人习惯总结</title><link>https://blog.iany.me/zh/2017/11/personal-habits-summary/</link><pubDate>Sat, 25 Nov 2017 07:24:18 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/11/personal-habits-summary/</guid><description>&lt;ul&gt;
&lt;li&gt;11 点睡觉，7 点起床&lt;/li&gt;
&lt;li&gt;坚持锻炼。主要是跑步，大概每周 20 公里，天气不好就玩 Just Dance 代替。游泳，大概每周 5 公里。&lt;/li&gt;
&lt;li&gt;前一天晚上做好第二天的计划。这样起床后就可以直接开始做重要的事情。&lt;/li&gt;
&lt;li&gt;将每天分成时间块，用三餐时间分隔成 1+3+3+3 共 4 块，10 小时。重要的事情在日历软件上标注出时间块。比较固定的安排
&lt;ul&gt;
&lt;li&gt;早晚查两次邮件。&lt;/li&gt;
&lt;li&gt;早上学习一项对自己职业来说最重要的事情，比如学习一项新技术。&lt;/li&gt;
&lt;li&gt;上午用于工作中最重要的事情。&lt;/li&gt;
&lt;li&gt;下午是较次要技术的学习，继续工作&lt;/li&gt;
&lt;li&gt;晚上一项个人兴趣爱好&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;优先做重要的事，看 RSS 阅读器中的文章，看新闻等琐碎的事情总会在精力不那么充沛的下午和晚上找到时间。&lt;/li&gt;
&lt;li&gt;提前为计划的事情做好准备工作。比如第二天要跑步，就把手表，MP3 充好电，衣服鞋子准备好。&lt;/li&gt;
&lt;li&gt;每天进步一点，并记录完成情况。完成条件都很宽松，关键是每天都有在做。
&lt;ul&gt;
&lt;li&gt;完成一项关键项目中的任务。关键项目是对自己非常重要的事情，比如职业规划上的关键技术的学习等，个人的兴趣爱好的学习。&lt;/li&gt;
&lt;li&gt;读一页非技术书。&lt;/li&gt;
&lt;li&gt;学习一组 Flash Cards，或者用 Duolingo 学一节课。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;跟踪时间，知道时间花在哪里了。&lt;/li&gt;
&lt;li&gt;同类的项目，同时进行 2 项，比如同时学习两项关键技术，两个兴趣爱好，然后交叉进行，避免每天都重复同样的事情而枯燥。&lt;/li&gt;
&lt;li&gt;开始任务后计时，发现超过 1 个小时了就休息会。不会特意提前定好要坚持多久，根据精力灵活调整。&lt;/li&gt;
&lt;li&gt;工具和设备
&lt;ul&gt;
&lt;li&gt;OmniFocus 任务管理&lt;/li&gt;
&lt;li&gt;Calendar 日历软件&lt;/li&gt;
&lt;li&gt;Tyme2 手动任务计时，有时候忘了记可以通过 Timing 的记录补下。&lt;/li&gt;
&lt;li&gt;Timing 自动记录电脑上各个软件的使用时间，已经不同网站的访问时间。&lt;/li&gt;
&lt;li&gt;Focus 屏蔽指定软件启动，禁止访问指定网站。&lt;/li&gt;
&lt;li&gt;索尼 WS414，跑步可以不用带手机，游泳也能听歌。&lt;/li&gt;
&lt;li&gt;阅读：
&lt;ul&gt;
&lt;li&gt;Kindle 读 Instapaper 保存的文章，JD Read 读非技术书籍。&lt;/li&gt;
&lt;li&gt;Medium 上的文章使用 Medium 的书签管理稍后读列表&lt;/li&gt;
&lt;li&gt;新闻：财新，澎湃，东方体育&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/tags/habit/">Habit</category><category domain="https://blog.iany.me/zh/tags/productivity/">Productivity</category></item><item><title>Ruby 中读取各种 RSA Keys</title><link>https://blog.iany.me/zh/2017/11/read-various-rsa-keys-in-ruby/</link><pubDate>Sat, 18 Nov 2017 02:46:10 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/11/read-various-rsa-keys-in-ruby/</guid><description>&lt;p&gt;最近在一个 Ruby on Rails 项目中需要集成各种支付系统。这些系统多使用 RSA 进行加密和签名验证。不过提供的 RSA Key 格式公式个样，所以把各种格式和如何在 Ruby 中读取整理了一下。&lt;/p&gt;
&lt;p&gt;RSA 是非对称加密算法，需要 2 个 Keys, 私钥和公钥。Key 本身就是二进制字符串，不过会被编码成各种格式。&lt;/p&gt;
&lt;h1 id="derpem"&gt;DER/PEM&lt;/h1&gt;
&lt;p&gt;DER 是一种二进制编码方案，如果用编辑器查看就是乱码。文件后缀一般是 &lt;code&gt;.cer&lt;/code&gt;, &lt;code&gt;.crt&lt;/code&gt;, 或者 &lt;code&gt;.der&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;PEM 使用 base64 编码，然后在前后添加 PEM 头和尾。比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx4c0F2TEoSe7wMBnn4WA
CSWQZL82eJJG3g128dE1BAMPAcSx0yaLWAEZJ0iZh9q14YND4kh7Or1hkV+beJfo
1c7DjO+VA31l9Nzdps/jGzkxa926VlFoXlLHngBn+zglfmXrhlVNVle/6asDrW49
p8LlLBZHqi/P72f9WUlAa7q45XXY48OqgsJ4ok2Xo0ZbLL9EHAu1GyfGFkBOsnOz
pqJe/aE0gltk+O7dKlVS1bGwm9cwx2eo+mEbH7NgbUm/by/OJKFx1SDbMIJRFcUj
WlosuKfzzkafH4i+z7n07s+StQ6kW5TcZbVVeQPjyzPdWeKvTRZMIvHV3ANVll+i
YQIDAQAB
-----END PUBLIC KEY-----
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种格式很常见，比如 &lt;code&gt;ssh-keygen&lt;/code&gt; 生成的私钥就是 PEM 格式。PEM 文件后缀一般是 &lt;code&gt;.pem&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;DER 和 PEM 都可以使用下面的方法读取&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;k = OpenSSL::PKey::RSA.new(File.read(&amp;quot;/path/to/keyfile&amp;quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 Key 设置了 passphrase，可以通过第二个参数指定。&lt;/p&gt;
&lt;p&gt;通过方法 &lt;code&gt;k.private?&lt;/code&gt; 可以知道是否是私钥。公钥始终是可用的，因为可以通过私钥计算出公钥。&lt;/p&gt;
&lt;p&gt;有时候，提供的 Key 只是使用 base64 编码，但是没有加 PEM 头和尾。这种情况有俩种处理方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;先 Base64 解码，然后作为 Key 内容读取&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;k = OpenSSL::PKey::RSA.new(Base64.decode64(key_content))
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;格式化成 PEM 格式再读取&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;pem = key_content.gsub(&amp;quot;\r\n&amp;quot;, &amp;quot;&amp;quot;).scan(/.{1,64}/).join(&amp;quot;\n&amp;quot;)
# or use BEGIN PRIVATE KEY
pem = &amp;quot;-----BEGIN PUBLIC KEY-----\n#{pem}\n-----END PUBLIC KEY-----\n&amp;quot;
k = OpenSSL::PKey::RSA.new(pem)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id="pkcs12"&gt;PKCS12&lt;/h1&gt;
&lt;p&gt;PKCS12 文件后缀一般是 &lt;code&gt;.p12&lt;/code&gt; 或者 &lt;code&gt;.pfx&lt;/code&gt;. 浏览器导出证书一般会提供该格式。大部分 PKCS12 文件都加了密码保护，可以通过第二个可选参数指定。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;pkcs12 = OpenSSL::PKCS12.new(key_content, certificate_password)
k = pkcs12.key
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id="x509-certificate"&gt;X509 Certificate&lt;/h1&gt;
&lt;p&gt;X509 Certificate 编码 Key 的同时添加了很多信息，比如证书签发者，过期时间等。X509 一般使用 PEM 或 DER 编码，后缀一般是 &lt;code&gt;.cer&lt;/code&gt;。如果使用 PEM 格式，头一般是 &lt;code&gt;BEGIN CERTIFICATE&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;X509 Certificate 可以使用下面代码读取&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;cert = OpenSSL::X509::Certificate.new(File.read(&amp;quot;/path/to/certfile&amp;quot;))
k = cert.public_key
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为容易和 PEM/DER 直接编码的 Key 搞混，如果出现 &lt;code&gt;asn1 error&lt;/code&gt; 的异常，可以尝试在 &lt;code&gt;OpenSSL::X509::Certificate&lt;/code&gt; 和 &lt;code&gt;OpenSSL::PKey::RSA&lt;/code&gt; 之间切换。&lt;/p&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/tags/cryptography/">Cryptography</category><category domain="https://blog.iany.me/zh/tags/openssl/">OpenSSL</category><category domain="https://blog.iany.me/zh/tags/rsa/">RSA</category><category domain="https://blog.iany.me/zh/tags/ruby/">Ruby</category></item><item><title>ZFS 文件恢复</title><link>https://blog.iany.me/zh/2017/11/zfs-file-restore/</link><pubDate>Sat, 18 Nov 2017 00:43:54 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/11/zfs-file-restore/</guid><description>&lt;p&gt;zfs 会在访问文件时记录下问题文件，也可用使用 &lt;code&gt;scrub&lt;/code&gt; 来扫描文件，其中 &lt;code&gt;poolname&lt;/code&gt; 是卷名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zpool scrub poolname
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题文件可以通过 &lt;code&gt;status&lt;/code&gt; 列出&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zpool status -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误比较多的话，可以考虑通过镜像还原。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zfs rollback poolname/path@tag
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;少的话也可以手动恢复问题文件。&lt;/p&gt;
&lt;p&gt;错误中不是 &lt;code&gt;/&lt;/code&gt; 开头，而且带有 &lt;code&gt;@tag&lt;/code&gt; 这样标签的是 ZFS 镜像。镜像本质是备份，备份出错了可能最简单的办法就是删除了，下面的命令用于删除镜像&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zfs destroy poolname/path@tag
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误比较少，文件不重要可以删除，如果重要并且有备份可以从备份中恢复。不过被删除或者被覆盖的文件不会立即被释放，如果想请空 &lt;code&gt;zpool status -v&lt;/code&gt; 中已删除文件的错误，可以执行 &lt;code&gt;zpool scrub poolname&lt;/code&gt;，等几分钟再通过 &lt;code&gt;zpool scrub -s poolname&lt;/code&gt; 停止。&lt;/p&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/tags/file-system/">File System</category><category domain="https://blog.iany.me/zh/tags/system-admin/">System Admin</category><category domain="https://blog.iany.me/zh/tags/zfs/">Zfs</category></item><item><title>我的 iPhone 通知管理</title><link>https://blog.iany.me/zh/2017/11/my-ios-notification-setting/</link><pubDate>Sun, 12 Nov 2017 00:37:58 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/11/my-ios-notification-setting/</guid><description>&lt;p&gt;iOS 的通知可以按 App 配置&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;是否显示图标上的未读数角标&lt;/li&gt;
&lt;li&gt;是否显示横幅提醒&lt;/li&gt;
&lt;li&gt;横幅提醒是自动消失还是必须要点击才会消失&lt;/li&gt;
&lt;li&gt;是否显示在通知中心&lt;/li&gt;
&lt;li&gt;是否播放通知提示音&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我的配置是绝大多数关闭，然后根据使用场景可以把 App 划分为几类，分别配置不同的策略。&lt;/p&gt;
&lt;h1 id="通知型"&gt;通知型&lt;/h1&gt;
&lt;p&gt;此类包括 iMessage，即时通信等。这类 App 消息可能是需要处理，但是绝大部分是可以忽略的。我的处理流程是把通知中心作为收集箱，定期打开通知中心，碰到重要的消息处理下，其余的全部清理。所以我的配置是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不显示角标，因为未读消息都在通知中心处理了&lt;/li&gt;
&lt;li&gt;横幅提醒，自动消失&lt;/li&gt;
&lt;li&gt;显示在通知中心&lt;/li&gt;
&lt;li&gt;不播放提示音&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中 iMessage 中还设置了自动删除，碰到重要的信息可以归档到 Notes 中保存。这样就不用浪费时间去把短信标记为已读，定期批量删除短信了。&lt;/p&gt;
&lt;h1 id="任务箱型"&gt;任务箱型&lt;/h1&gt;
&lt;p&gt;比如 Instapaper 等稍后读 App。这类 App 我并不想让它主动提醒我，而是在需要的时候能快速知道是否有要处理的信息，所以这类配置只打开角标，关闭所有其它的通知。&lt;/p&gt;
&lt;h1 id="重要"&gt;重要&lt;/h1&gt;
&lt;p&gt;重要的通知是必须要处理的，比如日历提醒。这类 App 我会打开所有通知选项，然后按照重要和紧急程度进行配置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果非常重要必须要处理的配置横幅不自动消失，否则配置自动消失&lt;/li&gt;
&lt;li&gt;如果很紧急需要立即处理的打开提示音，否则关闭&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外有些特殊的根据情况调整，比如 Facetime 的通知是重要需要处理的，但是错过了的通话在通知中心显示就可以了，不需要再打开角标。&lt;/p&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/tags/ios/">iOS</category><category domain="https://blog.iany.me/zh/tags/productivity/">Productivity</category></item><item><title>Lua C API userdata 和 light userdata</title><link>https://blog.iany.me/zh/2017/10/lua-c-api-userdata/</link><pubDate>Sun, 29 Oct 2017 16:58:27 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/10/lua-c-api-userdata/</guid><description>&lt;p&gt;填半年前挖的坑。分享 Lua C API 中的 userdata 和 light userdata。&lt;/p&gt;
&lt;p&gt;在编程过程中，经常会需要给一块数据分配一个唯一句柄，通过句柄能够读取或者操作这块数据。原因主要有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据内聚性很强，封装在一起方便传递，减少参数数量。&lt;/li&gt;
&lt;li&gt;隐藏数据的内部结构，通过 API 提供操作接口。&lt;/li&gt;
&lt;li&gt;减少数据拷贝。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最典型的就是 C 中的指针了。但句柄并不一定就必须是指针，比如 Linux 系统中的 fd 可以当作是 IO 设备的句柄。&lt;/p&gt;
&lt;p&gt;在 Lua C API 中提供了 userdata 和 light userdata 可以让 C 返回一个句柄给 Lua，而 Lua 可以将句柄再通过在 C 中注册的方法传回 C。&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Lua Userdata" class="kg-image" loading="lazy" src="https://blog.iany.me/2025/09/lua-c-api-userdata/lua-c-api-userdata_hu_2c64654bc21f5270.png" srcset="https://blog.iany.me/2025/09/lua-c-api-userdata/lua-c-api-userdata_hu_1f5c022c2333fcb8.png 400w, https://blog.iany.me/2025/09/lua-c-api-userdata/lua-c-api-userdata_hu_2c64654bc21f5270.png 710w" sizes="(max-width: 400px) 100vw, 710px" /&gt;
&lt;figcaption &gt;Lua Userdata&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="区别"&gt;区别&lt;/h2&gt;
&lt;p&gt;Userdata 和 light userdata 的区别是 Userdata 通过 Lua API 分配一片内存，这片内存通过 Lua GC 自动回收。而 light userdata 只是一个 &lt;code&gt;void *&lt;/code&gt; 类型的值，具体这个值怎么产生，怎么回收需要自己管理。&lt;/p&gt;
&lt;p&gt;虽说叫 light userdata，但是其实 Userdata 也是非常轻量的，相比 light userdata 只是多了内存分配和 GC 时的内存回收。因为自动回收的特性，特别适合数据生命周期和 userdata 对象在 Lua 中存活周期一致的场景。&lt;/p&gt;
&lt;p&gt;而 light userdata 适合数据周期大于 userdata 对象存活周期的场景，在 Lua 中只是获得数据的引用，但是并不管理数据的生命周期。比如在 Cocos2d-X 中有自己的 GC 机制，所以应该使用 light userdata 将 node 的指针返回给 Lua。还有一种情况是句柄并不是指针，但是可以强转成 &lt;code&gt;void*&lt;/code&gt; 也是可以通过 light userdata 的，比如将 fd 封装成 light userdata，比如在没有 64 位整数的 Lua 版本但 &lt;code&gt;void*&lt;/code&gt; 是 64 位的环境下实现 64 位整数库。&lt;/p&gt;
&lt;p&gt;另一个区别是整个 Lua 虚拟机中的所有 light userdata 是共享一个元表的，而 userdata 可以单独设置。如果需要元表也必须使用 userdata。&lt;/p&gt;
&lt;h2 id="示例"&gt;示例&lt;/h2&gt;
&lt;p&gt;以下所有示例代码和编译脚本都可以在该 &lt;a href="https://doitian.coding.net/public/lua-c-api-intro/lua-c-api-intro/git/files/userdata"&gt;Git 仓库的 userdata 分支&lt;/a&gt;找到。&lt;/p&gt;
&lt;h3 id="userdata-示例"&gt;Userdata 示例&lt;/h3&gt;
&lt;p&gt;Userdata 的 API 主要是 &lt;code&gt;lua_newuserdata&lt;/code&gt; 和 &lt;code&gt;lua_touserdata&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;以实现简单的复数为例，创建时调用 &lt;code&gt;lua_newuserdata&lt;/code&gt; 分配内存并把新的 userdata 压入栈。然后通过返回的内存指针进行初始化。&lt;/p&gt;
&lt;p&gt;※ &lt;a href="https://doitian.coding.net/public/lua-c-api-intro/lua-c-api-intro/git/files/userdata/userdata.c"&gt;userdata.c&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-c"&gt;struct Complex {
lua_Number real;
lua_Number imag;
};
typedef struct Complex Complex;
static int complex_new(lua_State* L) {
Complex* comp = lua_newuserdata(L, sizeof(Complex));
comp-&amp;gt;real = lua_tonumber(L, 1);
comp-&amp;gt;imag = lua_tonumber(L, 2);
return 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 &lt;code&gt;lua_touserdata&lt;/code&gt; 获得栈上的 userdata 对应的内存指针。下面是俩个复数相加返回一个新的复数的例子&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-c"&gt;static int complex_add(lua_State* L) {
Complex* a = lua_touserdata(L, 1);
Complex* b = lua_touserdata(L, 2);
Complex* comp = lua_newuserdata(L, sizeof(Complex));
comp-&amp;gt;real = a-&amp;gt;real + b-&amp;gt;real;
comp-&amp;gt;imag = a-&amp;gt;imag + b-&amp;gt;imag;
return 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="light-userdata-示例"&gt;Light userdata 示例&lt;/h3&gt;
&lt;p&gt;Light userdata 的 API 主要是 &lt;code&gt;lua_pushlightuserdata&lt;/code&gt; 和 &lt;code&gt;lua_touserdata&lt;/code&gt;。注意获取栈上的 userdata 或者 light userdata 对应的指针式相同的 API。&lt;/p&gt;
&lt;p&gt;以封装 C FILE API 为例，打开文件时将 &lt;code&gt;FILE *&lt;/code&gt; 作为 lightuser data 返回。这里因为 &lt;code&gt;FILE *&lt;/code&gt; 是由 libc 来管理生命周期的，所以不能使用 &lt;code&gt;lua_newuserdata&lt;/code&gt; 来分配内存再初始化。&lt;/p&gt;
&lt;p&gt;※ &lt;a href="https://doitian.coding.net/public/lua-c-api-intro/lua-c-api-intro/git/files/userdata/light_userdata.c"&gt;light_userdata.c&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-c"&gt;static int file_open(lua_State* L) {
const char* path = lua_tostring(L, 1);
const char* mode = &amp;quot;w+&amp;quot;;
FILE* file = fopen(path, mode);
if (file == NULL) {
lua_pushstring(L, strerror(errno));
lua_error(L);
return 0;
}
lua_pushlightuserdata(L, file);
return 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了释放资源，必须提供相应的 API，在使用完毕时调用。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-c"&gt;static int file_close(lua_State* L) {
FILE* f = lua_touserdata(L, 1);
if (f != NULL) {
fclose(f);
}
return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然这个例子中，更安全的方法是用 userdata，把 FILE* 设置在 userdata 分配的内存中，并通过元表设置在 GC 是自动调用 &lt;code&gt;fclose&lt;/code&gt;。&lt;/p&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/series/lua-c-api/">Lua C API</category><category domain="https://blog.iany.me/zh/tags/c/">C</category><category domain="https://blog.iany.me/zh/tags/lua/">Lua</category><category domain="https://blog.iany.me/zh/tags/programming/">Programming</category></item><item><title>Graylog 集中日志管理 - 日志输出</title><link>https://blog.iany.me/zh/2017/10/centralized-logs-using-graylog-output/</link><pubDate>Sat, 14 Oct 2017 03:43:07 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/10/centralized-logs-using-graylog-output/</guid><description>&lt;p&gt;这是 Graylog 系列最后一篇。&lt;/p&gt;
&lt;p&gt;日志集中保存到 Graylog 后就可以方便的使用搜索了。不过有时候还是需要对数据进行近一步的处理。主要有两个途径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接访问 Elastic 中保存的数据&lt;/li&gt;
&lt;li&gt;通过 Graylog 的 Output 转发到其它服务&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="访问-elastic"&gt;访问 Elastic&lt;/h2&gt;
&lt;p&gt;Graylog 支持一些简单的统计，如果想做更复杂的统计，推荐使用 &lt;a href="https://grafana.com"&gt;Grafana&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;集成很简单，在 Grafana 添加下 Elasticsearch 数据源。Access 推荐 proxy，这样不用在公网暴露 Elastic 的端口，只要 Grafana 所在机器能通过 Url 访问就可以了。&lt;/p&gt;
&lt;p&gt;Index name 根据 Graylog Index Set 中设置的前缀配置。Version 根据安装的 Elastic 版本选择。&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Grayfana Data Source" class="kg-image" loading="lazy" src="https://blog.iany.me/2025/09/centralized-logs-using-graylog-output/graylog-grafana-data-source_hu_cf3eb240d8fd608c.png" srcset="https://blog.iany.me/2025/09/centralized-logs-using-graylog-output/graylog-grafana-data-source_hu_fc11b08d00a4f43e.png 400w, https://blog.iany.me/2025/09/centralized-logs-using-graylog-output/graylog-grafana-data-source_hu_cf3eb240d8fd608c.png 628w" sizes="(max-width: 400px) 100vw, 628px" /&gt;
&lt;figcaption &gt;Grayfana Data Source&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;然后创建图标选择 Elastic 的 Data Source 就可以了。&lt;/p&gt;
&lt;p&gt;如果还有更复杂的需求，可以使用 Elastic 的各种语言的库，比如 Jupter Notebook 搭配 Python。&lt;/p&gt;
&lt;h2 id="graylog-output"&gt;Graylog Output&lt;/h2&gt;
&lt;p&gt;在 Graylog 中，可以选择将某个 Stream 通过 Output 转发给其他服务。内置了 GELF Output 使用 GELF 格式通过 TCP 或者 UDP 发到其它服务。在 Graylog Market 中可以搜索到很多现成的 &lt;a href="https://marketplace.graylog.org/addons?kind=plugin&amp;amp;tag=output"&gt;Output Plugin&lt;/a&gt;，Github 上也有很多&lt;a href="https://github.com/graylog-labs/graylog-plugin-slack/blob/master/src/main/java/org/graylog2/plugins/slack/output/SlackMessageOutput.java"&gt;现成&lt;/a&gt;的&lt;a href="https://github.com/Graylog2/graylog-plugin-splunk/blob/master/src/main/java/com/graylog/splunk/output/SplunkOutput.java"&gt;例子&lt;/a&gt;可以仿照实现自己的 Output Plugin。不过 Plugin 需要用 Java 实现，然后要部署到 Graylog 的所有节点，修改调试都很不方便。如果 Market 中找不到现成的 Plugin，更推荐使用内置的 GELF Output，很多 GELF 库是支持作为服务接收 GELF 消息的，比如 Golang 的 &lt;a href="https://github.com/Graylog2/go-gelf"&gt;go-gelf&lt;/a&gt;，使用 &lt;code&gt;gelf.NewReader&lt;/code&gt; 就可以创建一个 UDP 服务。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;package main
import (
&amp;quot;log&amp;quot;
&amp;quot;gopkg.in/Graylog2/go-gelf.v2/gelf&amp;quot;
)
func handleMessage(m *gelf.Message) {
// handle m
}
func runUDPServer() {
gelfReader, err := gelf.NewReader(&amp;quot;:12201&amp;quot;)
if err != nil {
log.Fatal(err)
}
for {
message, err := gelfReader.ReadMessage()
if err != nil {
log.Error(err)
}
go func() {
defer func() {
if r := recover(); r != nil {
log.Error(r)
}
}()
handleMessage(message)
}()
}
}
func main() {
runUDPServer()
}
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/series/centralized-logs-using-graylog/">Graylog 集中日志管理</category><category domain="https://blog.iany.me/zh/tags/devops/">Devops</category><category domain="https://blog.iany.me/zh/tags/graylog/">Graylog</category></item><item><title>Graylog 集中日志管理 - 日志处理</title><link>https://blog.iany.me/zh/2017/10/centralized-logs-using-graylog-processing/</link><pubDate>Fri, 06 Oct 2017 15:39:07 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/10/centralized-logs-using-graylog-processing/</guid><description>&lt;p&gt;Graylog 通过 Input 搜集日志，每个 Input 单独配置 Extractors 用来做字段转换。&lt;/p&gt;
&lt;p&gt;Graylog 中日志搜索的基本单位是 Stream，每个 Stream 可以有自己单独的 Elastic Index Set，也可以共享一个 Index Set。用 Set 是因为日志的保存会使用一个前缀然后滚动创建新的 Index。Stream 通过配置条件匹配日志，满足条件的日志添加 stream ID 标识字段并保存到对应的 Elastic Index Set 中。同一个 Input 中的日志可以属于不同的 Stream，不同 Input 中的日志可以属于同一个 Stream，就是同一条日志也可以属于多个 Stream。&lt;/p&gt;
&lt;p&gt;系统会有一个默认的 Stream，所有日志默认都会保存到这个 Stream 中，除非匹配了某个 Stream，并且这个 Stream 里配置了不保存日志到默认 Stream。&lt;/p&gt;
&lt;p&gt;下图是日志处理流程图&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Graylog 日志处理" class="kg-image" loading="lazy" src="https://blog.iany.me/2025/09/centralized-logs-using-graylog-processing/graylog-log-processing_hu_7027b8271922dbd2.png" srcset="https://blog.iany.me/2025/09/centralized-logs-using-graylog-processing/graylog-log-processing_hu_60971a9511988f16.png 400w, https://blog.iany.me/2025/09/centralized-logs-using-graylog-processing/graylog-log-processing_hu_2bb94096e098d13b.png 800w, https://blog.iany.me/2025/09/centralized-logs-using-graylog-processing/graylog-log-processing_hu_7027b8271922dbd2.png 863w" sizes="(max-width: 800px) 100vw, 863px" /&gt;
&lt;figcaption &gt;Graylog 日志处理&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;后文会分别分享各个步骤需要注意的一些地方。&lt;/p&gt;
&lt;h2 id="extractor"&gt;Extractor&lt;/h2&gt;
&lt;p&gt;Extractor 在 System / Input 中配置。Graylog 中很方便的一点就是可以加载一条日志，然后基于这个实际的例子进行配置并能直接看到结果。&lt;/p&gt;
&lt;p&gt;内置的 Extractor 基本可以完成各种字段提取和转换的任务，但是也有些限制。在应用里写日志的时候就需要考虑到这些限制。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只有字符串类型的字段可以应用 Extractor。比如不能将 Linux Epoch 时间戳通过 Extractor 转成 Elastic 支持的时间格式。&lt;/li&gt;
&lt;li&gt;Extractor 的输入只能是单个字段，输出根据用的方法的不同是可以生成多个字段，比如 JSON 可以解析 JSON 并提取所有字段。&lt;/li&gt;
&lt;li&gt;字段层级只有一级，也就是日志字段的值不能是复合类型，只能是字符串，数字，时间等。比如使用 JSON Extractor，如果 JSON 中有嵌套的 Object 和 Array 需要选择一种方式展平结构。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Input 可以配置多个 Extractors，按照顺序依次执行。&lt;/p&gt;
&lt;h2 id="stream"&gt;Stream&lt;/h2&gt;
&lt;p&gt;Gralog 安装好后会包含一个默认 Stream，可以通过菜单 Streams 创建更多的 Stream。新创建的 Stream 是暂停状态，需要在配置完成后手动启动。&lt;/p&gt;
&lt;p&gt;一个 Stream 唯一属于一个 Index Set，但是多个 Streams 可以共享同一个 Index Set。如果共享 Index，那么因为底层 Elastic 的原因会有一个限制： 同一个字段的类型不能一会是字符串，一会是数字，也就是类型必须一致。这个的影响有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在写入日志的时候，如果当前 Elastic Index 中已经存在该字段同时类型不符合，那么冲突的日志会被丢弃，这个错误可以在 System / Overview 中查看。&lt;/li&gt;
&lt;li&gt;因为上面一条的原因，一个 Index 中某个字段是什么类型取决于该 Index 中第一条含该字段的日志。这样一个 Index Set 中，某个字段的类型可能会不一致，在跨 Index 做数据汇总时会导致出错，比如选择了很长的一个时间跨度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果 Graylog 是多个项目共享的话，是很难避免不同项目间字段类型冲突的，所以建议是不相关的日志不要共享 Index Set。这里推荐个人使用的一个策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为不同的项目创建单独的 Stream 和 Index Set，并且选择不保存到默认 Stream 中。&lt;/li&gt;
&lt;li&gt;如果需要将某个项目中的一部分日志发往其它服务，比如把错误发到 Sentry，单独创建 Stream 加过滤条件，和该项目的 Stream 共享 Index Set。这个会在下一篇系列文章中提到。&lt;/li&gt;
&lt;li&gt;如果项目中某些日志很重要，需要有不同的存储策略，或者是需要保存更长时间，那么单独创建 Stream 并使用单独的 Index Set&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样各个项目间不会产生冲突，又能单独配置存储。&lt;/p&gt;
&lt;h2 id="index-set"&gt;Index Set&lt;/h2&gt;
&lt;p&gt;Index Set 通过菜单 System / Indices 创建。日志存储的性能，可靠性和过期策略都通过 Index Set 来配置。&lt;/p&gt;
&lt;p&gt;性能和可靠性就是配置 Elastic Index 的一些参数，主要是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shards: 每个 Index 分多少片，每一片可以保存在 Elastic 集群中不同的机器上。日志存储和查询的瓶颈一般是磁盘 IO，通过分片可以将 IO 压力分摊到多台机器。&lt;/li&gt;
&lt;li&gt;Replicas: 每个 Shard 额外保存多少个副本，当有机器出现故障，只要集群内能凑齐每个 Shard 中至少一个副本就不会有任何影响。当然可靠性是靠存储的冗余来实现的，需要消耗更多的磁盘空间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;已经有很多 Elastic 的文章介绍如何进行配置了，就不详细说明。如果集群比较小，不超过3台机器，那么 Shards 可以填 3，而 Replicas 的配置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果没有配置 Elastic 集群填 0&lt;/li&gt;
&lt;li&gt;如果可以接受集群中节点故障导致部分日志暂时无法搜索到，甚至是永久丢失，或者磁盘比较紧张，填 0&lt;/li&gt;
&lt;li&gt;如果对可靠性要求高，也有充足的磁盘空间，填 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;过期策略主要根据日志量，磁盘空间，需要查询的时间跨度来决定，Graylog 提供了三种 Index 滚动方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按时间&lt;/li&gt;
&lt;li&gt;按 Index 中日志数量&lt;/li&gt;
&lt;li&gt;按 Index 的占用磁盘大小&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过配置要保留的 Index 数量来删除老的日志。&lt;/p&gt;
&lt;h2 id="pipelines"&gt;Pipelines&lt;/h2&gt;
&lt;p&gt;除了上面提到的日志处理流程，Graylog 还提供了 Pipeline 脚本实现更灵活的日志处理方案。这里不详细阐述，只介绍如果使用 Pipelines 来过滤不需要的日志。&lt;/p&gt;
&lt;p&gt;Graylog 中只要日志发到了 Input，常规流程中是没有办法丢弃日志，最终一定会写入到 Elastic 中。有时候可能一些配置错误，比如打开了 DEBUG 级别的日志，导致大量没用的日志占用大量资源。虽然可以单独创建单独的 Stream 和 Index Set 并通过配置过期策略来快速丢弃日志，但日志还是在磁盘上走了一遍。这时就需要 Pipelines 出场了。&lt;/p&gt;
&lt;p&gt;下面是丢弃 &lt;code&gt;level &amp;gt; 6&lt;/code&gt; 的所有日志的 Pipeline Rule 的例子&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rule &amp;quot;discard debug messages&amp;quot;
when
to_long($message.level) &amp;gt; 6
then
drop_message();
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后可以创建 Pipeline 关联 Streams 和规则了。&lt;/p&gt;
&lt;p&gt;要注意的是，如果 Pipeline Rule 想使用 Extractors 应用之后的字段的话，需要在 System / Configuration 里调整 Message Processors Configuration 的顺序，Pipeline Processor 要放在 Message Filter Chain 后面。&lt;/p&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/series/centralized-logs-using-graylog/">Graylog 集中日志管理</category><category domain="https://blog.iany.me/zh/tags/devops/">Devops</category><category domain="https://blog.iany.me/zh/tags/graylog/">Graylog</category></item><item><title>Graylog 集中日志管理 - 收集日志</title><link>https://blog.iany.me/zh/2017/09/centralized-logs-using-graylog-collecting/</link><pubDate>Sun, 24 Sep 2017 15:08:08 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/09/centralized-logs-using-graylog-collecting/</guid><description>&lt;p&gt;Graylog 通过 &lt;em&gt;Inputs&lt;/em&gt; 收集日志，方式以被动接收为主，需要在产生日志的地方将日志发送给 Graylog。比较常用的一些方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;程序中直接集成可以发送日志给 Graylog 的库，在 Github 中搜索 GELF 可以找到大量各种语言、框架、环境下的库。&lt;/li&gt;
&lt;li&gt;采用传统的文件来记录日志，在机器上启动一个 agent 程序抓取日志文件新的内容然后发给 Graylog。&lt;/li&gt;
&lt;li&gt;使用 syslog 写日志，利用 rsyslog 的转发功能把日志发给 Graylog。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务器端日志个人推荐最后一种方式，优点有&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不依赖 Graylog，可以替换成任何能接收 syslog 的其它方案。&lt;/li&gt;
&lt;li&gt;容易 Fallback，rsyslog 可以配置成同时保存到本地文件和转发 Graylog。当 Graylog 出现问题至少还有本地日志文件可以用。&lt;/li&gt;
&lt;li&gt;rsyslog 的日志保存，转发已经非常成熟和稳定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点是 rsyslog 日志会把整条日志作为 &lt;code&gt;message&lt;/code&gt; 字段保存。Graylog 内部每条日志是作为 Elastic 的 Document 保存的，细化出更多的字段能满足复杂查询和数据分析的需求。所以 Graylog 基于 JSON 制定了 &lt;a href="http://docs.graylog.org/en/2.3/pages/gelf.html"&gt;GELF&lt;/a&gt; 协议，使用 GELF 协议的 Input 可以在产生日志时直接设置各种字段。不过 Graylog 还提供了 &lt;em&gt;Extractor&lt;/em&gt;，和 &lt;a href="https://www.fluentd.org"&gt;fluentd&lt;/a&gt;, &lt;a href="https://www.elastic.co/products/logstash"&gt;logstash&lt;/a&gt; 中的 filter 相似，可以从日志中提取结构化字段出来，比如 JSON Extractor 可以解析 JSON 格式日志。使用过类似工具的应该对 Grok 很熟悉，Graylog 也提供了支持。所以使用 rsyslog 也只需要制定下日志格式，然后配置下 Extractor。&lt;/p&gt;
&lt;p&gt;Graylog &lt;a href="http://docs.graylog.org/en/2.3/pages/sending_data.html"&gt;自带了&lt;/a&gt;丰富的 Inputs 可供使用，同时可以通过&lt;a href="https://marketplace.graylog.org/addons?kind=plugin&amp;amp;tag=input"&gt;插件扩展&lt;/a&gt;，可以结合自己的使用场景选择合适的方式。&lt;/p&gt;
&lt;p&gt;本文接下来会介绍日志收集需要注意的地方，以及如何基于 rsyslog 来打造集中日志系统。&lt;/p&gt;
&lt;h2 id="日志处理队列"&gt;日志处理队列&lt;/h2&gt;
&lt;p&gt;Graylog 内部使用 &lt;a href="https://kafka.apache.org"&gt;Kafka&lt;/a&gt; 实现了称为 Journal 的队列系统，来缓存接收的日志。这个对集中日志系统相当重要。日志的特性决定了量大，并且分布不平均，会突发地集中在某个时间段产生大量日志。队列能有效的防止突发大数据量输入导致系统瘫痪，将闲时利用起来处理积压的日志。同时还能作为是否要扩充集群提高 Elastic 写入速度的指标。&lt;/p&gt;
&lt;p&gt;Graylog 集群每个节点有自己独立的 Journal，扩展 Graylog 本身节点数量不但可以提升日志处理速度，还可以提升队列缓存的容量。&lt;/p&gt;
&lt;p&gt;当 Elastic 写入出现瓶颈，Journal 的队列长度会一直增长，Graylog 设置了俩个阀值，当积压的日志超过 12 小时未处理，或者占用磁盘超过 5G 就会开始丢弃新接收到的日志。通过配置 &lt;code&gt;message_journal_max_age&lt;/code&gt; 和 &lt;code&gt;message_journal_max_size&lt;/code&gt; 可以修改。Graylog 还提供了 &lt;a href="http://docs.graylog.org/en/2.3/pages/configuration/load_balancers.html"&gt;API 查询状态&lt;/a&gt; 可以和负载均衡系统集成。&lt;/p&gt;
&lt;h2 id="graylog-创建-syslog-input"&gt;Graylog 创建 syslog input&lt;/h2&gt;
&lt;p&gt;首先要在 Graylog 中通过 &lt;code&gt;System / Inputs&lt;/code&gt; 创建 input 来接收日志。可以选 Syslog UDP 或者 Syslog TCP，俩者区别不大，TCP 的话 rsyslog 是&lt;a href="http://www.rsyslog.com/doc/v8-stable/tutorials/reliable_forwarding.html"&gt;能保证日志不会丢失&lt;/a&gt;，UDP 的话开销相对更小，但是可能丢数据。TCP 的话负载均衡选择也更多，如果不知道用哪个就选 TCP。&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Create Graylog Syslog TCP Input" class="kg-image" loading="lazy" src="https://blog.iany.me/2025/09/centralized-logs-using-graylog-collecting/graylog-syslog-tcp-input_hu_af58998e28436a0d.png" srcset="https://blog.iany.me/2025/09/centralized-logs-using-graylog-collecting/graylog-syslog-tcp-input_hu_94c9750fb662b7f7.png 400w, https://blog.iany.me/2025/09/centralized-logs-using-graylog-collecting/graylog-syslog-tcp-input_hu_af58998e28436a0d.png 585w" sizes="(max-width: 400px) 100vw, 585px" /&gt;
&lt;figcaption &gt;Create Graylog Syslog TCP Input&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;大部分不需要改，&lt;code&gt;Global&lt;/code&gt; 的话会在所有 Graylog 节点上启动，Bind address 使用默认的 &lt;code&gt;0.0.0.0&lt;/code&gt; 这样其它机器才能转发日志过来。Port 需要配置一个大于 1024 的端口。&lt;/p&gt;
&lt;p&gt;如果启用了集群，还需要使用负载均衡将 rsyslog 转发的日志分发给 Graylog 节点。如果用 TCP，可以用 HAProxy，很多云服务也提供了现成甚至免费的内网 TCP 负载均衡产品。如果用 UDP，可以参考我之前写的 &lt;a href="https://blog.iany.me/zh/2017/08/nginx-udp-load-balance/"&gt;使用 Nginx 作 UDP 负载均衡&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id="日志发往-rsyslog"&gt;日志发往 rsyslog&lt;/h2&gt;
&lt;p&gt;Syslog 是一套记录日志的协议，rsyslog 是具体的一个实现。在 Debian 和 Ubuntu 中预装的就是 rsyslog。Rsyslog 用很多扩展功能，不如不限制单条日志大小，可以转发日志。&lt;/p&gt;
&lt;p&gt;日志不是直接发往 Graylog 的 syslog input，而是发往本地的 rsyslog，然后 rsyslog 负责转发给 Graylog。这样网络或者 Graylog 出现问题，Rsyslog 可以缓存未发送的日志，等待问题恢复。同时还可以在本地保存一份副本。&lt;/p&gt;
&lt;p&gt;将日志发往 syslog 可以使用系统提供的 C 库中的 syslog (see &lt;a href="https://linux.die.net/man/3/syslog"&gt;man 3 syslog&lt;/a&gt;)。很多语言标准库都集成了 syslog，比如 &lt;a href="https://ruby-doc.org/stdlib/libdoc/syslog/rdoc/Syslog/Logger.html"&gt;Ruby&lt;/a&gt;，&lt;a href="https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler"&gt;Python&lt;/a&gt;, &lt;a href="https://golang.org/pkg/log/syslog/"&gt;Go&lt;/a&gt;。没有的也可以通过 C 集成，比如用于 &lt;a href="https://gist.github.com/6de6d7ce01a7930dfba1b6aa5e76ac3f"&gt;skynet&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;注意几个比较重要的可配置参数，能方便之后日志的过滤和转发。可以在 &lt;code&gt;man 3 syslog&lt;/code&gt; 看到&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void openlog(const char *ident, int logopt, int facility);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;indent&lt;/code&gt; 在不同的地方可能又被称为 &lt;code&gt;programname&lt;/code&gt;, &lt;code&gt;tag&lt;/code&gt;。一般用这个来区分日志是哪个服务或者进程产生的。另一个是 &lt;code&gt;facility&lt;/code&gt;，推荐统一使用 &lt;code&gt;LOCAL6&lt;/code&gt;，在 rsyslog 只转发 &lt;code&gt;LOCAL6&lt;/code&gt; 的日志到 Graylog。&lt;/p&gt;
&lt;p&gt;Facility 的配置都比较直接，不过 indent 的配置就比较混乱了，像 Python 的 &lt;code&gt;SysLogHandler&lt;/code&gt; 就一直不支持自定义 ident，Python 3 直到 &lt;a href="https://bugs.python.org/issue12419"&gt;3.3 版本中才可以通过 class-level attribute 来配置&lt;/a&gt;。所以分别说明下&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ruby: &lt;code&gt;Syslog::Logger.new(program_name = 'ruby', facility = nil)&lt;/code&gt; 中 &lt;code&gt;program_name&lt;/code&gt; 就是 ident&lt;/li&gt;
&lt;li&gt;Python: Python 不管哪个版本 &lt;code&gt;SysLogHandler&lt;/code&gt; 默认都不传 ident 的，而 ident 在 syslog 标准中其实就是日志中以非特殊字符开头，&lt;code&gt;: &lt;/code&gt; 结尾的一串字符，所以可以在设置 formatter 的时候添加，见下面的代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;import logging
from logging.handlers import SysLogHandler
ident = 'myprogname'
h = SysLogHandler(address='/dev/log')
h.setFormatter(logging.Formatter(ident + ': %(message)s'))
logging.getLogger().addHandler(h)
logging.error('hi')
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Go: log/syslog 的 &lt;code&gt;Dial(network, raddr string, priority Priority, tag string) (*Writer, error)&lt;/code&gt; 中的 tag 就是 ident。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="rsyslog-配置"&gt;Rsyslog 配置&lt;/h2&gt;
&lt;p&gt;以 Ubuntu 14.04 为例。先上配置，保存为 &lt;code&gt;/etc/rsyslog.d/00-graylog.conf&lt;/code&gt;，具体文件名不重要，但必须 &lt;code&gt;00&lt;/code&gt; 开头，&lt;code&gt;.conf&lt;/code&gt; 为后缀。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$template GLFile,&amp;quot;/data/log/%programname%.log&amp;quot;
$FileOwner syslog
$FileGroup syslog
$CreateDirs on
$DirCreateMode 0755
$FileCreateMode 0640
$RepeatedMsgReduction off
local6.* ?GLFile;RSYSLOG_SyslogProtocol23Format
local6.* @@logs-internal.example.com:1514;RSYSLOG_SyslogProtocol23Format
&amp;amp; stop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;$&lt;/code&gt; 开头的都是 rsyslog 的配置的 Directive。如果对日志可靠性要求高，可以参考&lt;a href="http://www.rsyslog.com/doc/v8-stable/tutorials/reliable_forwarding.html"&gt;Rsyslog Reliable Forwarding&lt;/a&gt; 的文档。&lt;/p&gt;
&lt;p&gt;配置中 &lt;code&gt;$RepeatedMsgReduction&lt;/code&gt; 是不删除重复的日志，其它全部都是配置本地文件副本的。&lt;code&gt;$template GLFile&lt;/code&gt; 配置了文件名命名模版。模版可以使用一些变量，比如 &lt;code&gt;%programname%&lt;/code&gt; 是日志的 ident，如果之前正确设置了 ident，这里可以把不同的服务的日志存到不同的文件中。模版是完整的路径，注意权限，用户 syslog 需要有创建文件的权限。因为 rsyslog 没有 root 权限，&lt;code&gt;FileOwner&lt;/code&gt; 和 &lt;code&gt;$FileGroup&lt;/code&gt; 可选值不多，如果想让其它用户查看文件，可以把 &lt;code&gt;$FileGroup&lt;/code&gt; 设置成 &lt;code&gt;syslog&lt;/code&gt;，然后添加用户到 &lt;code&gt;syslog&lt;/code&gt; 组。例子:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /data/log
sudo chown syslog:adm /data/log
# 允许 deploy 查看本地日志副本
sudo gpasswd -a deploy syslog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;倒数第三行 &lt;code&gt;local6.* ?GLFile;RSYSLOG_SyslogProtocol23Format&lt;/code&gt; 是把 facility 是 &lt;code&gt;LOCAL6&lt;/code&gt; 的日志按照模版 &lt;code&gt;GLFile&lt;/code&gt; 保存到文件中。如果不想要本地副本，删掉本行即可。&lt;/p&gt;
&lt;p&gt;倒数第二行俩个 &lt;code&gt;@&lt;/code&gt; 的 &lt;code&gt;@@logs-internal.example.com:1514;RSYSLOG_SyslogProtocol23Format&lt;/code&gt; 表示通过 TCP 转发日志到 &lt;code&gt;logs-internal.example.com&lt;/code&gt; 的1514端口。如果是 UDP 改成一个 &lt;code&gt;@&lt;/code&gt;。如果用了负载均衡这里应该填负载均衡的地址和端口。&lt;/p&gt;
&lt;p&gt;最后一行的 &lt;code&gt;&amp;amp; stop&lt;/code&gt; 表示匹配之前规则的日志，也就是所有 &lt;code&gt;LOCAL6&lt;/code&gt; 的日志不再判断配置中剩余的规则。这也是配置必须用 &lt;code&gt;00&lt;/code&gt; 开头的原因。默认的 Ubuntu 配置中，&lt;code&gt;LOCAL6&lt;/code&gt; 的日志也会保存到 &lt;code&gt;/var/log/syslog&lt;/code&gt; 中，重复浪费空间，而且日志量很大的话，&lt;code&gt;/var/log/syslog&lt;/code&gt; 默认的 logrotate 的配置会导致占用大量的磁盘。&lt;/p&gt;
&lt;p&gt;配置里选择把日志保存在 &lt;code&gt;/data/log&lt;/code&gt; 目录下，主要是为了单独配置 &lt;code&gt;logrotate&lt;/code&gt;。访问量大的服务会产生海量日志，所以需要优化避免写爆磁盘。&lt;/p&gt;
&lt;p&gt;这是一个每天不压缩大概会产生 100G 左右日志的推荐配置，保存为 &lt;code&gt;/etc/logrotate.d/syslog-graylog&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/data/log/*.log
{
rotate 40
daily
maxage 5
maxsize 10G
missingok
notifempty
compress
postrotate
reload rsyslog &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || true
endscript
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;daily&lt;/code&gt; 每天 rotate 一次，&lt;code&gt;maxsize 10G&lt;/code&gt; 超过 10G 立即 rotate。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rotate 40&lt;/code&gt; 和 &lt;code&gt;maxage 5&lt;/code&gt; 保留最多 40 份，最多 5 天内的历史日志。过期的删除。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;compress&lt;/code&gt; 用 gzip 压缩历史日志，不配置 &lt;code&gt;delaycompress&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样大概保存最近 5 天的日志，如果日志量太大，就保存最近 40 x 10 共 400G 日志。天数根据需要配置，因为是副本，不用设置太长时间。&lt;code&gt;rotate 40&lt;/code&gt; 和 &lt;code&gt;maxsize 10G&lt;/code&gt; 根据磁盘大小确定，GZIP 压缩比大概是 1/10，40 x 10 的配置大概占用 40G 磁盘。&lt;/p&gt;
&lt;p&gt;Logrotate 默认是每天运行的，&lt;code&gt;maxsize 10G&lt;/code&gt; 其实不起作用，可以配置 cron 每小时手动执行下，这样能及时 rotate 日志并压缩。把下面文件保存为 &lt;code&gt;/etc/cron.hourly/syslog-graylog&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh
logrotate /etc/logrotate.d/syslog-graylog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意权限&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x /etc/cron.hourly/syslog-graylog
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="测试"&gt;测试&lt;/h2&gt;
&lt;p&gt;测试可以使用命令行工具 &lt;code&gt;logger&lt;/code&gt; ，&lt;code&gt;-p&lt;/code&gt; 指定 facility 和 priority，&lt;code&gt;-t&lt;/code&gt; 指定 ident&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;logger -p local6.error -t programname test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一切正常的话 Graylog 中就会显示这条日志了。&lt;/p&gt;
&lt;p&gt;另外说下 macOS，虽说提供了 syslog 接口，不过坑很多，配置可以在 &lt;code&gt;/etc/syslog.conf&lt;/code&gt; 里添加，比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;local6.* @127.0.0.1:1514
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重启下服务&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo launchctl unload /System/Library/LaunchDaemons/com.apple.syslogd.plist
sudo launchctl load /System/Library/LaunchDaemons/com.apple.syslogd.plist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过 macOS 的 syslog 还受 &lt;code&gt;/etc/asl.conf&lt;/code&gt; 影响，比如 &lt;code&gt;notice&lt;/code&gt; 级别以下的日志是全部忽略，根本就不会处理到 &lt;code&gt;/etc/syslog.conf&lt;/code&gt; 的。优点是可以在 Console 里查看 syslog。&lt;/p&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/series/centralized-logs-using-graylog/">Graylog 集中日志管理</category><category domain="https://blog.iany.me/zh/tags/devops/">Devops</category><category domain="https://blog.iany.me/zh/tags/graylog/">Graylog</category></item><item><title>Rails Cookie 如何解密</title><link>https://blog.iany.me/zh/2017/09/rails-cookie-encryption/</link><pubDate>Sat, 23 Sep 2017 07:47:49 +0000</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/09/rails-cookie-encryption/</guid><description>&lt;p&gt;如果想在已有的 Rails app 上使用其它语言加些 API，同时能直接使用 Rails 的登陆信息，最简单的就是用 Nginx 等代理将不同的服务映射到相同的域名下，其它的 App 解密 Cookie 获得登陆信息。&lt;/p&gt;
&lt;p&gt;本文以 Ruby 代码为例说明 Rails 的 Cookie 是如何加密，然后以 Go 为例说明如何解密的。&lt;/p&gt;
&lt;figure class="kg-image-card"&gt;
&lt;img alt="Rails Cookie 加密" class="kg-image" loading="lazy" src="https://blog.iany.me/2025/09/rails-cookie-encryption/rails-cookie-encryption_hu_c566275d3ee4f967.png" srcset="https://blog.iany.me/2025/09/rails-cookie-encryption/rails-cookie-encryption_hu_4af7debedc226724.png 400w, https://blog.iany.me/2025/09/rails-cookie-encryption/rails-cookie-encryption_hu_c566275d3ee4f967.png 543w" sizes="(max-width: 400px) 100vw, 543px" /&gt;
&lt;figcaption &gt;Rails Cookie 加密&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Rails 的实现可以参考 &lt;a href="https://github.com/rails/rails/blob/0a6f69a5debf89748da3a43747c61d201095997e/activesupport/lib/active_support/message_encryptor.rb"&gt;ActiveSupport::MessageEncryptor&lt;/a&gt;，&lt;a href="https://github.com/rails/rails/blob/0a6f69a5debf89748da3a43747c61d201095997e/activesupport/lib/active_support/message_verifier.rb"&gt;ActiveSupport::MessageVerifier&lt;/a&gt; 和相应的单元测试。&lt;/p&gt;
&lt;h2 id="加密"&gt;加密&lt;/h2&gt;
&lt;p&gt;上图说明了原始的 Session 对象 &lt;em&gt;Session Data&lt;/em&gt; 是如何最终生成 Cookie 的。如果登陆用了 &lt;a href="https://github.com/plataformatec/devise"&gt;Devise&lt;/a&gt;，那么 Session Data 中的登陆信息保存在 &lt;code&gt;warden.user.user.key&lt;/code&gt; 中。之后就用下面例子说明加密。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;session = { &amp;quot;warden.user.user.key&amp;quot; =&amp;gt; [[1],&amp;quot;secret&amp;quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-cookie-serializer"&gt;① Cookie Serializer&lt;/h3&gt;
&lt;p&gt;从 Rails 4.1 开始，默认使用的 JSON，4.1 之前使用的 Ruby Marshal。为了方便其它语言中解析，推荐使用 4.1 或更新的版本并使用 JSON 做为 Cookie 的 serializer。配置在 &lt;code&gt;config/initializers/cookies_serializer.rb&lt;/code&gt; 中&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;Rails.application.config.action_dispatch.cookies_serializer = :json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JSON 的 serializer 就很直接了&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;require 'json'
session_json = JSON.dump(session)
puts session_json.inspect
# =&amp;gt; &amp;quot;{\&amp;quot;warden.user.user.key\&amp;quot;:[[1],\&amp;quot;secret\&amp;quot;]}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-padding"&gt;② Padding&lt;/h3&gt;
&lt;p&gt;下一步的加密要求数据的字节数必须是 16 的倍数，用的算法是 &lt;a href="https://en.wikipedia.org/wiki/Padding_%28cryptography%29#PKCS7"&gt;PKCS7&lt;/a&gt;。简单说就是如果差 n 个字节到下个 16 的倍数就补 n 个 n。如果刚好是 16 的倍数就补 16 个 16。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;def padding(data, block_size = 16)
n = block_size - data.bytesize % block_size
return data.force_encoding('ASCII-8BIT') + n.chr * n
end
padded_session = padding(session_json)
puts padded_session.inspect
# =&amp;gt; &amp;quot;{\&amp;quot;warden.user.user.key\&amp;quot;:[[1],\&amp;quot;secret\&amp;quot;]}\t\t\t\t\t\t\t\t\t&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;末尾的 &lt;code&gt;\t&lt;/code&gt; ASCII 码是 9，表示补了 9 个字节。&lt;/p&gt;
&lt;h3 id="-加密-aes-cbc"&gt;③ 加密 AES-CBC&lt;/h3&gt;
&lt;p&gt;这一步是最主要的加密了，算法是 AES-CBC。加密需要配置密钥并随机生成 IV (initialization vector)。因为 Ruby 的 OpenSSL::Cipher 封装会自动 padding，所以可以跳过第 ② 步。&lt;/p&gt;
&lt;p&gt;我们知道 Rails 需要配置 secret key base，密钥就是通过 secret key base 和 salt 产生的，使用的算法 &lt;code&gt;pbkdf2&lt;/code&gt; 在 OpenSSL 里也提供了。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;OpenSSL::PKCS5.pbkdf2_hmac_sha1(pass, salt, iter, keylen)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pass&lt;/code&gt; 配置中的 secret key base&lt;/li&gt;
&lt;li&gt;&lt;code&gt;salt&lt;/code&gt; 如果使用默认 Rails 配置的话，加密是 &lt;code&gt;encrypted cookie&lt;/code&gt;，后面签名步骤是 &lt;code&gt;signed encrypted cookie&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;iter&lt;/code&gt; 默认是 1000, &lt;code&gt;keylen&lt;/code&gt; 加密是 32，签名是 64。也可以统一用 64，但是加密的 Key 只取前 32 个字节。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;require 'openssl'
SECRET_KEY_BASE = &amp;quot;development_secret&amp;quot;
DEFAULT_SALT = &amp;quot;encrypted cookie&amp;quot;
DEFAULT_SIGN_SALT = &amp;quot;signed encrypted cookie&amp;quot;
DEFAULT_ITER = 1000
DEFAULT_KEYLEN = 64
def generate_key(secret_key_base, salt, iter = DEFAULT_ITER, keylen = DEFAULT_KEYLEN)
OpenSSL::PKCS5.pbkdf2_hmac_sha1(secret_key_base, salt, iter, keylen)
end
encrypt_key = generate_key(SECRET_KEY_BASE, DEFAULT_SALT)[0...32]
puts Base64.strict_encode64(encrypt_key)
# =&amp;gt; vozBHj31liL/p88es/k7aywa4Po4mwMVkW/eqhFjw/4=
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IV 是随机的 16 个字节。解密的时候需要用到，所以需要保存起来下一步拼装的时候用。可以用 &lt;code&gt;SecureRandom.random_bytes&lt;/code&gt; 或者 &lt;code&gt;OpenSSL::Cipher.random_iv&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;使用 OpenSSL 实现如下，IV 应该要随机的，为了方便对照，直接用了 16 个 0&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;encrypt_key = generate_key(SECRET_KEY_BASE, DEFAULT_SALT)[0...32]
puts Base64.strict_encode64(encrypt_key)
cipher = OpenSSL::Cipher.new(&amp;quot;aes-256-cbc&amp;quot;)
cipher.encrypt
cipher.key = encrypt_key
iv = &amp;quot;\0&amp;quot; * 16
# iv = cipher.random_iv
encrypted_content = cipher.update(session_json)
encrypted_content &amp;lt;&amp;lt; cipher.final
puts Base64.strict_encode64(encrypted_content)
# =&amp;gt; t7c1ncaCXhZAOPRtX0BI8eceOmx/Qg3Jrg6uwmgJuSNosKIc7M4KRfOw1q3mFWv7ZSiNO3ZRPxJMGI1cDvu+PQ==
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-拼装加密内容和-iv"&gt;④ 拼装加密内容和 IV&lt;/h3&gt;
&lt;p&gt;得到 &lt;code&gt;encrypted_content &lt;/code&gt; 和 &lt;code&gt;iv&lt;/code&gt; 后，分别 base64 后用 &lt;code&gt;--&lt;/code&gt; 连接，然后再做一次 base64 得到 &lt;code&gt;encrypted_data&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;encrypted_data = Base64.strict_encode64(
Base64.strict_encode64(encrypted_content) +
&amp;quot;--&amp;quot; +
Base64.strict_encode64(iv)
)
puts encrypted_data
# =&amp;gt; dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-签名-hmac-sha1"&gt;⑤ 签名 HMAC-SHA1&lt;/h3&gt;
&lt;p&gt;签名用的 &lt;code&gt;HMAC-SHA1&lt;/code&gt;，结果转成 16 进制字符串。Key 参考 加密步骤中的说明。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;sign_key = generate_key(SECRET_KEY_BASE, DEFAULT_SIGN_SALT)
sign = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, sign_key, encrypted_data)
puts sign
# =&amp;gt; 75d8323b0f0e41cf4d5aabee1b229b1be76b83b6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-拼装签名"&gt;⑥ 拼装签名&lt;/h3&gt;
&lt;p&gt;最后把 &lt;code&gt;encrypted_data&lt;/code&gt; 和 &lt;code&gt;sign&lt;/code&gt; 用 &lt;code&gt;--&lt;/code&gt; 连接然后做一次 URL Query Escape 就可以了&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;require &amp;quot;uri&amp;quot;
cookie_content = URI.encode_www_form_component(encrypted_data + &amp;quot;--&amp;quot; + sign)
puts cookie_content
# =&amp;gt; dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09--75d8323b0f0e41cf4d5aabee1b229b1be76b83b6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的代码: &lt;a href="https://gist.github.com/doitian/2a89dc9e4372e55335c9111f576b47bf#file-rails-cookie-encrypt-rb"&gt;rails-cookie-encrypt.rb&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果用 &lt;code&gt;ActiveSupport&lt;/code&gt; 可以简化成&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;require &amp;quot;active_support/key_generator&amp;quot;
require &amp;quot;active_support/message_encryptor&amp;quot;
encrypt_key = ActiveSupport::KeyGenerator.new(SECRET_KEY_BASE, iterations: DEFAULT_ITER).generate_key(DEFAULT_SALT, 32)
sign_key = ActiveSupport::KeyGenerator.new(SECRET_KEY_BASE, iterations: DEFAULT_ITER).generate_key(DEFAULT_SIGN_SALT, 64)
encryptor = ActiveSupport::MessageEncryptor.new(encrypt_key, sign_key, serializer: JSON)
puts encryptor.encrypt_and_sign(session)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="解密"&gt;解密&lt;/h2&gt;
&lt;p&gt;解密就是把 6 个步骤反过来，输入就是&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;cookieContent := &amp;quot;dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09--75d8323b0f0e41cf4d5aabee1b229b1be76b83b6&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-分离签名"&gt;⑥ 分离签名&lt;/h3&gt;
&lt;p&gt;URL Query Unescape 然后以 &lt;code&gt;--&lt;/code&gt; 分成 &lt;code&gt;encryptedData&lt;/code&gt; 和 &lt;code&gt;sign&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;var err error
var unescapedCookieContent string
if unescapedCookieContent, err = url.QueryUnescape(cookieContent); err != nil {
panic(err)
}
encryptedDataSignVectors := strings.SplitN(unescapedCookieContent, &amp;quot;--&amp;quot;, 2)
encryptedData := encryptedDataSignVectors[0]
sign := encryptedDataSignVectors[1]
fmt.Printf(&amp;quot;encrypted_data = %v\n&amp;quot;, encryptedData)
fmt.Printf(&amp;quot;sign = %v\n&amp;quot;, sign)
// =&amp;gt; encrypted_data = dDdjMW5jYUNYaFpBT1BSdFgwQkk4ZWNlT214L1FnM0pyZzZ1d21nSnVTTm9zS0ljN000S1JmT3cxcTNtRld2Ny0tQUFBQUFBQUFBQUFBQUFBQUFBQUFBQT09
// sign = 75d8323b0f0e41cf4d5aabee1b229b1be76b83b6
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-验证签名"&gt;⑤ 验证签名&lt;/h3&gt;
&lt;p&gt;验证签名其实就是再签一次然后对比结果。为了安全，可以使用 &lt;code&gt;hmac.Equal&lt;/code&gt; 来比较签名是否一致。&lt;/p&gt;
&lt;p&gt;Key 的生成可以使用 &lt;code&gt;golang.org/x/crypto/pbkdf2&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;const (
keyIterNum = 1000
keySize = 64
)
func generateKey(base, salt string) []byte {
return pbkdf2.Key([]byte(base), []byte(salt), keyIterNum, keySize, sha1.New)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证实现如下&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;secretKeyBase := &amp;quot;development_secret&amp;quot;
defaultSignSalt := &amp;quot;signed encrypted cookie&amp;quot;
signKey := generateKey(secretKeyBase, defaultSignSalt)
signHmac := hmac.New(sha1.New, signKey)
signHmac.Write([]byte(encryptedData))
verifySign := signHmac.Sum(nil)
fmt.Printf(&amp;quot;verifySign = %v\n&amp;quot;, hex.EncodeToString(verifySign))
// verifySign = 75d8323b0f0e41cf4d5aabee1b229b1be76b83b6
var signDecoded []byte
if signDecoded, err = hex.DecodeString(sign); err != nil {
panic(err)
}
if !hmac.Equal(verifySign, signDecoded) {
panic(fmt.Errorf(&amp;quot;verification failed&amp;quot;))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-分离加密内容和-iv"&gt;④ 分离加密内容和 IV&lt;/h3&gt;
&lt;p&gt;Base64 解码一次，用 &lt;code&gt;--&lt;/code&gt; 分离并分别 Base64 解码得到 &lt;code&gt;encryptedContent&lt;/code&gt; 和 &lt;code&gt;iv&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;var encryptedDataBase64Decoded []byte
if encryptedDataBase64Decoded, err = base64.StdEncoding.DecodeString(encryptedData); err != nil {
panic(err)
}
encryptedContentIvVectors := strings.SplitN(string(encryptedDataBase64Decoded), &amp;quot;--&amp;quot;, 2)
var encryptedContent []byte
var iv []byte
if encryptedContent, err = base64.StdEncoding.DecodeString(encryptedContentIvVectors[0]); err != nil {
panic(err)
}
if iv, err = base64.StdEncoding.DecodeString(encryptedContentIvVectors[1]); err != nil {
panic(err)
}
fmt.Printf(&amp;quot;encrypted_content = %s\n&amp;quot;, base64.StdEncoding.EncodeToString(encryptedContent))
fmt.Printf(&amp;quot;iv = %v\n&amp;quot;, iv)
// encrypted_content = t7c1ncaCXhZAOPRtX0BI8eceOmx/Qg3Jrg6uwmgJuSNosKIc7M4KRfOw1q3mFWv7
// iv = [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-解密"&gt;③ 解密&lt;/h3&gt;
&lt;p&gt;用 Key 和 iv 来解密&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;defaultSalt := &amp;quot;encrypted cookie&amp;quot;
encryptKey := generateKey(secretKeyBase, defaultSalt)[:32]
c, err := aes.NewCipher(encryptKey)
if err != nil {
panic(err)
}
cfb := cipher.NewCBCDecrypter(c, iv)
paddedSession := make([]byte, len(encryptedContent))
cfb.CryptBlocks(paddedSession, encryptedContent)
fmt.Printf(&amp;quot;padded_session = %s\n&amp;quot;, strconv.QuoteToASCII(string(paddedSession)))
// padded_session = &amp;quot;{\&amp;quot;warden.user.user.key\&amp;quot;:[[1],\&amp;quot;secret\&amp;quot;]}\t\t\t\t\t\t\t\t\t&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-un-padding"&gt;② Un-padding&lt;/h3&gt;
&lt;p&gt;去除 padding 只需要看最后一个字节是多少就移除多少个字节。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;padding := int(paddedSession[len(paddedSession) - 1])
sessionJSON := string(paddedSession[:(len(paddedSession) - padding)])
fmt.Printf(&amp;quot;session_json = %s\n&amp;quot;, sessionJSON)
// session_json = {&amp;quot;warden.user.user.key&amp;quot;:[[1],&amp;quot;secret&amp;quot;]}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="-cookie-deserializer"&gt;① Cookie Deserializer&lt;/h3&gt;
&lt;p&gt;如果是 JSON 用 go JSON 库解析就可以了。如果是 Ruby Marshal 也不用完整实现，可以用正则提取需要的信息。&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-go"&gt;var jsonData map[string]interface{}
if err := json.Unmarshal([]byte(sessionJSON), &amp;amp;jsonData); err != nil {
panic(err)
}
fmt.Printf(&amp;quot;%+v\n&amp;quot;, jsonData)
// map[warden.user.user.key:[[1] secret]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的代码: &lt;a href="https://gist.github.com/doitian/2a89dc9e4372e55335c9111f576b47bf#file-rails-cookie-decrypt-go"&gt;rails-cookie-decrypt.go&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果 Rails 里用的 Devise，可以在 &lt;code&gt;config/initializers/devise.rb&lt;/code&gt; 增加下面的配置来在 Cookie 中包含更多的字段，比如用户名或邮箱&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-ruby"&gt;Warden::Manager.after_authentication do |user, auth, opts|
auth.raw_session['warden.user.user.email'] = user.email
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要用户重新登陆或者更换 secret key base 才会生效。&lt;/p&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/tags/encryption/">Encryption</category><category domain="https://blog.iany.me/zh/tags/golang/">Golang</category><category domain="https://blog.iany.me/zh/tags/rails/">Rails</category><category domain="https://blog.iany.me/zh/tags/ruby/">Ruby</category></item><item><title>Graylog 集中日志管理 - 安装</title><link>https://blog.iany.me/zh/2017/09/centralized-logs-using-graylog-installation/</link><pubDate>Sun, 17 Sep 2017 19:28:47 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/09/centralized-logs-using-graylog-installation/</guid><description>&lt;p&gt;使用 &lt;a href="https://www.graylog.org"&gt;Graylog&lt;/a&gt; 之前试过很多方案，包括流行的 ELK，也用过 fluentd 搭配各种存储，influxdb, mongodb 等等。但这些方案在日志量大了之后出现性能瓶颈都没有提供解决方案。而 Graylog 作为整合方案，使用 elasticsearch 存储，mongodb 缓存，带流量控制 (throttling)，简单易用的查询界面，方便的管理界面，易于扩展。转移到 Graylog 省心了不少。在使用过程中积累了些经验，准备陆续分享出来。&lt;/p&gt;
&lt;p&gt;第一篇是关于如何安装 Graylog。&lt;/p&gt;
&lt;p&gt;官方提供了自动化和&lt;a href="http://docs.graylog.org/en/2.3/pages/installation/manual_setup.html"&gt;手动安装&lt;/a&gt;，以及&lt;a href="http://docs.graylog.org/en/2.3/pages/configuration/multinode_setup.html"&gt;集群配置&lt;/a&gt;的文档。稍微麻烦点的是安装 mongodb 和 elasticsearch 集群，以及配置 graylog 里的各种地址。&lt;/p&gt;
&lt;p&gt;Mongodb 在有防火墙保护下，最简单的集群方案是不设置验证，只需要所有节点配置相同的 &lt;code&gt;replSet&lt;/code&gt; 就行了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;replSet = graylog
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后按照文档&lt;a href="https://docs.mongodb.com/manual/tutorial/deploy-replica-set/"&gt;初始化一下&lt;/a&gt;，注意 &lt;code&gt;_id&lt;/code&gt; 要和配置的 &lt;code&gt;replSet&lt;/code&gt; 一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rs.initiate( {
_id : &amp;quot;graylog&amp;quot;,
members: [ { _id : 0, host : &amp;quot;logs1.example.com:27017&amp;quot; } ]
})
rs.add(&amp;quot;logs2.example.com&amp;quot;)
rs.add(&amp;quot;logs3.example.com&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Elasticsearch 类似，配置相同的 cluster name 并列出集群内机器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cluster.name: graylog
discovery.zen.ping.unicast.hosts：
- logs1.example.com
- logs2.example.com
- logs2.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Graylog 只要保证只有一个节点配置成了 master&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;is_master = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比较容易出错和混淆的就是各种地址的配置，主要是有些没配置会用另外的选项作为默认值。推荐是显式的配置所有下面四个选项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rest_listen_uri&lt;/code&gt; 用来指定 API 启动时监听的网卡，端口和 API 地址前缀。没什么特殊原因，这个配置成 &lt;code&gt;http://0.0.0.0:9000/api/&lt;/code&gt; 就行了&lt;/li&gt;
&lt;li&gt;&lt;code&gt;web_listen_uri&lt;/code&gt; 同上，但是是 WEB 资源的 HTTP 服务器，可以和 API 使用相同的端口，一般配置成 &lt;code&gt;http://0.0.0.0:9000/&lt;/code&gt; 就可以了&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rest_transport_uri&lt;/code&gt; 这个是节点暴露给集群其它节点访问用的 API 地址，一般把 &lt;code&gt;rest_listen_uri&lt;/code&gt; 中的 IP 换成内网 IP 或者域名就行了，比如 &lt;code&gt;http://logs1-internal.example.com:9000/api/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;web_endpoint_uri&lt;/code&gt; 是暴露给 WEB 界面里的 Javascript 连接用的 API 地址，一般把 &lt;code&gt;rest_listen_uri&lt;/code&gt; 中的 IP 换成公网 IP 或者域名就行了，比如 &lt;code&gt;http://logs1.example.com:9000/api/&lt;/code&gt;。如果设置了负载均衡，或者代理，可以配置成负载均衡和代理的公网地址，也可以通过 HTTP Header 来覆盖该配置，比如 Nginx&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;proxy_set_header X-Graylog-Server-URL http://logs.example.com/api;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Graylog 成功启动后在 Web 管理界面就能查看节点状态和 elasticsearch 集群状态。Mongodb 可以在 mongo 命令行客户端里执行以下两个命令验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rs.conf()
rs.status()
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/series/centralized-logs-using-graylog/">Graylog 集中日志管理</category><category domain="https://blog.iany.me/zh/tags/devops/">Devops</category><category domain="https://blog.iany.me/zh/tags/graylog/">Graylog</category></item><item><title>使用 Nginx 作 UDP 负载均衡</title><link>https://blog.iany.me/zh/2017/08/nginx-udp-load-balance/</link><pubDate>Wed, 23 Aug 2017 23:11:35 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/08/nginx-udp-load-balance/</guid><description>&lt;p&gt;最近把 Graylog 升级到集群。因为使用了 syslog UDP 作为输入，所以需要做个 UDP 的负载均衡。因为机器在阿里云，所以首先是创建了个阿里云内网的 UDP 负载均衡，但是发现负载不平均，而且也没办法关闭检测连接的心跳包。而常见的 HTTP 负载均衡工具 HAProxy 不支持 UDP。最后发现 Nginx 是可以实现 UDP 负载均衡的。&lt;/p&gt;
&lt;p&gt;首先，需要安装 Nginx 比较新的支持 &lt;code&gt;stream&lt;/code&gt; 配置关键字的版本。比如 Ubuntu 14.04 中的版本就不支持，需要通过 PPA 安装。PPA 安装的版本是需要通过动态链接库来加载 &lt;code&gt;stream&lt;/code&gt; 功能的。以 Ubuntu PPA 中版本为例，在 &lt;code&gt;nginx.conf&lt;/code&gt; 靠前的位置添加&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;load_module /usr/lib/nginx/modules/ngx_stream_module.so;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面的片段配置了监听 UDP 1515，把请求平均的发给 3 台机器的 1514 UDP 端口。需要注意的，这个配置片段必须在 &lt;code&gt;nginx.conf&lt;/code&gt; 的顶层，像 &lt;code&gt;site-enabled&lt;/code&gt; 下的配置文件一般是嵌套在 &lt;code&gt;http&lt;/code&gt; 下的，所以不能放那里面。&lt;/p&gt;
&lt;p&gt;配置里的 &lt;a href="https://nginx.org/en/docs/stream/ngx_stream_proxy_module.html#proxy_responses"&gt;proxy_responses&lt;/a&gt; 是等待代理服务返回包并发回客户端的个数，这个需要根据场景配置。因为我是用来接受 &lt;code&gt;rsyslog&lt;/code&gt; 的日志转发的，并不需要返回消息，也没有返回消息。同时请求量非常大，如果不设置为 0，Nginx 会因为不断创建连接，最终耗尽而无法响应新的请求。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stream {
upstream syslog_udp {
server logs1-xy.example.com:1514;
server logs2-xy.example.com:1514;
server logs3-xy.example.com:1514;
}
server {
listen 1515 udp;
proxy_pass syslog_udp;
proxy_responses 0;
}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为日志转发量大，所以还需要提高 Nginx 进程的 &lt;code&gt;nofile&lt;/code&gt; 限制和连接数的限制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;worker_rlimit_nofile 1000000;
events {
worker_connections 20000;
}
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/tags/nginx/">Nginx</category></item><item><title>Redis 写缓存</title><link>https://blog.iany.me/zh/2017/08/redis-write-buffer/</link><pubDate>Sat, 19 Aug 2017 23:41:07 +0800</pubDate><author>me@iany.me (Ian Yang)</author><guid>https://blog.iany.me/zh/2017/08/redis-write-buffer/</guid><description>&lt;p&gt;Redis 比较常见的是作为读缓存。读取数据的时候检查 Redis 是否存在，存在的话缓存命中，不需要访问后端存储。没命中的话从后端存储中获得，并同时存入 Redis。写数据的时候直接写入后端存储，清除 Redis 中对应的缓存或者更新成新的值。这种模型可以安全的随时删除缓存中的内容而不会造成数据丢失，因为最新的数据一定是写入到后端存储里了。&lt;/p&gt;
&lt;p&gt;但如果系统的瓶颈出现在了写入这块，上面的模型就没法解决了。稍加修改可以得到写缓存的模型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读的时候检查 Redis，如果 Redis 存在直接使用 Redis 中的数据，否则从后端存储中读取。&lt;/li&gt;
&lt;li&gt;写的时候只写入 Redis，通过消息队列通知后台任务把 Reids 中缓存的内容保存到后端存储中。&lt;/li&gt;
&lt;li&gt;后台任务监听消息队列，在把缓存内容保存成功之后从 Redis 中删除对应的内容。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;任务队列可以使用 Redis 的 LIST 来实现，官方的 &lt;a href="https://redis.io/commands/rpoplpush/"&gt;RPOPLPUSH – Redis&lt;/a&gt; 命令文档中已经很详细描述了如何实现一个可靠的消息队列。于是剩下的问题就是如何能保证安全的删除已经保存过的缓存？&lt;/p&gt;
&lt;p&gt;保存缓存的步骤肯定是&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从 Redis 中读取&lt;/li&gt;
&lt;li&gt;把读取的内容写入后端存储&lt;/li&gt;
&lt;li&gt;从 Redis 中删除&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而如果在 1 和 3 之间，又有新的修改写入，这些修改就因为第 3 步的执行丢失了。要解决这个问题要么就是加锁，要么是通过事务。&lt;/p&gt;
&lt;p&gt;研究了一番以后，要&lt;a href="https://redis.io/docs/reference/patterns/distributed-locks/"&gt;实现锁&lt;/a&gt;会比较复杂，而通过事务就比较简单了。&lt;/p&gt;
&lt;p&gt;Redis 提供了 &lt;code&gt;MULTI&lt;/code&gt; 和 &lt;code&gt;EXEC&lt;/code&gt; 将多个命令提交为一个事务。虽然不提供错误回滚，但保证了要么命令都没执行，要么都执行了。而命令 &lt;code&gt;WATCH&lt;/code&gt; 可以用来监控某个键，如果在执行 &lt;code&gt;WATCH&lt;/code&gt; 之后到执行事务之前，这个键里的值有所变化，事务将被取消，详情可以查看&lt;a href="https://redis.io/topics/transactions"&gt;官方文档&lt;/a&gt;。因此保存缓存的步骤改写成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;WATCH&lt;/code&gt; 要保存的 key&lt;/li&gt;
&lt;li&gt;从 Redis 中读取&lt;/li&gt;
&lt;li&gt;把读取的内容写入后端存储&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;MULTI&lt;/code&gt; 和 &lt;code&gt;EXEC&lt;/code&gt; 包围从 Redis 中删除该 key 的语句&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;redis.send('WATCH', key)
data = redis.get(key)
database.save(key, data)
redis.send('MULTI')
redis.send('DEL', key)
redis.send('EXEC')
&lt;/code&gt;&lt;/pre&gt;</description><category domain="https://blog.iany.me/zh/post/">文章</category><category domain="https://blog.iany.me/zh/tags/redis/">Redis</category></item></channel></rss>