<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Random Thoughts</title><link>https://blog.joway.io/</link><description>Recent content on Random Thoughts</description><generator>Hugo</generator><language>cn</language><lastBuildDate>Mon, 30 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.joway.io/index.xml" rel="self" type="application/rss+xml"/><item><title>昨日的世界不再重来</title><link>https://blog.joway.io/posts/the-past-before-ai/</link><pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/the-past-before-ai/</guid><description>&lt;p>最近在做一个思想实验，如果你面前有一个按钮，按下就可以让 AI 从世界上消失，你是否会按下。身边很多朋友的答案都是会毫不犹豫的按下，即便有一些人还是那种最拥抱 AI 的人。&lt;/p>
&lt;p>拥抱 AI 和厌恶 AI 可以同时在一个人身上发生，我自己就是这样的人。我厌恶 AI，是因为对昨日世界的怀念，我拥抱 AI，是因为今日世界无法回退，我没有不拥抱的选择。&lt;/p>
&lt;p>现在的 AI 虽然可以代替我完成很多事情，但是在事情完成后，带给我的并不是成就感而是一种无尽的空虚。有了一个 idea，找一个趁手的模型，用几句提示词让 AI 吭哧吭哧干活，再做几轮调整，最后几个小时项目完成了，剩下一段贤者时间，你不知道这个项目数据库的 schema，也不确定架构是否能支撑多少的用户规模，未来有新的需求，你只能对着这个黑盒许愿，在许愿前完全没有关于需求可行性的任何概念。当 AI 给你完成了修改后，你不知道会不会损坏已有的用户的数据，只能按照 AI 的要求上线。一通操作下来，身体很累，精神很空虚，干了一天的活，又好像什么也没干。伫立在这个黑盒面前，反反复复操作几个月，不知道自己究竟学到了什么。AI 有了更多的数据，一天天变得更聪明，而我们自己呢？&lt;/p>
&lt;p>在昨日的世界，虽然我们工作很累，但是工作结束会有一种智力的满足。当其他人来咨询某个需求是否可行，我们可以给出斩钉截铁的答案。当需求可行的时候，我们知道需要修改哪些地方让它可行，当需求不可行的时候，我们知道因为哪些原因所以它不可行。我们面对的是一个充满确定性的世界，我们确定昨天的工作，确定明天的工作，确定自己的职业生涯，确定未来能够有一份收入养家糊口生儿育女。今日的世界一切都不确定，不确定昨天做了什么，不确定明天会做什么，不确定这每天光聊天的工作能否有职业生涯，不确定未来什么样的人才可能凌驾于 AI 之上能有一份糊口的工作。&lt;/p>
&lt;p>今日的世界互联网的整体审美也在逐渐倒退。互联网到处充斥着用 AI 生成的机械感十足的图片，也没人在乎是否是真的符合自己脑海中想象的模样，只要和提示词差不多能用即可。各种「高效率人士」用 AI 创作着各种垃圾的文章，宣扬自己最高效的工作流，攀比着各自的 token 消耗量和同时并行启动的 Agent 数量。每天都把一些简单的步骤封装出各种下一个月就会消失的名词概念，并以自己掌握的名词数量而沾沾自喜。&lt;/p>
&lt;p>我时常在刷新推特时间线的时候，感受到自己是在一群推土机中漫步，页面滚动，一颗颗大树被连根拔起，群众在欢呼，木匠们做出一件件精美的家具。而我只想回到自己的瓦尔登湖旁边，但是小木屋也早已被掀翻，地上留着一张告示，顺之者昌逆之者亡。&lt;/p></description></item><item><title>Magic Brush - 画出你自己的产品宇宙</title><link>https://blog.joway.io/posts/magic-brush/</link><pubDate>Thu, 05 Mar 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/magic-brush/</guid><description>&lt;p>上周末用 AI 写了一个新的产品： &lt;a href="https://brush.elsetech.app">Magic Brush&lt;/a>。&lt;/p>
&lt;p>这个产品的 idea 来自于我经常用 AI vibe coding 一些小的前端产品，但是如果每次都为这些小产品创建独立的域名和部署太过于繁琐，而且目前也缺乏一个聚合站点可以浏览其他人创建的小产品。另外，我在写博客的时候，也经常会想要插入一些交互页面来展示我的 idea，但是如果要在博客里插入一堆 js/css 代码又会导致博客无法长期维护。我把这些需求收集了下，整理并抽象出了一个产品形态：&lt;/p>
&lt;blockquote>
&lt;p>这个产品可以允许用户使用自然语言创作页面，然后通过 &lt;code>iframe&lt;/code> 嵌入到任意网页中去。并且这个产品还可以有一个广场功能，可以看到其他人的作品。不仅仅能用来做博客的交互页面插入，也可以作为产品原型设计的工具。&lt;/p>&lt;/blockquote>
&lt;h2 id="产品设计">产品设计&lt;/h2>
&lt;p>&lt;a href="https://brush.elsetech.app">Magic Brush&lt;/a> 由 2 主要页面组成。&lt;/p>
&lt;h3 id="页面设计页">页面设计页&lt;/h3>
&lt;p>用户可以在设计页右边会话框写自己的提示词与 AI 交互，左边的页面会实时渲染最新的页面。其他用户可以以只读的方式访问该页面，并且也能看到提示词，方便其他人模仿修改并创建自己的页面。&lt;/p>
&lt;p>&lt;img src="https://blog.joway.io/images/magic-brush/preview.png" alt="">&lt;/p>
&lt;h3 id="广场页">广场页&lt;/h3>
&lt;p>广场页面允许看到所有 Public 的页面设计，可以以 Like 数量排序或者时间排序：&lt;/p>
&lt;iframe src="https://brush.elsetech.app/square" style="width:100%;height:600px;border:0;" loading="lazy">&lt;/iframe>
&lt;h2 id="use-cases">Use Cases&lt;/h2>
&lt;h3 id="创作可交互文章">创作可交互文章&lt;/h3>
&lt;p>对于有些文章来说，文字并不是最适宜的表达载体，例如游记。你可以把你的游记喂给 AI 生成可交互的页面，然后再插入到文章中。例如我的 &lt;a href="https://blog.joway.io/posts/japan-kumano-kodo/">&amp;laquo;熊野古道中边路纪行&amp;raquo;&lt;/a> 游记中，可以被转变为：&lt;/p>
&lt;iframe src="https://brush-api.elsetech.app/pages/2ZkCB6KD0F" style="width:100%;height:2500px;border:0;" loading="lazy">&lt;/iframe>
&lt;h3 id="创作流程图">创作流程图&lt;/h3>
&lt;p>传统流程图需要用截图的形式插入到文章，导致不易修改。我们可以让 AI 生成流程图页面，然后直接插入到文章中，并且后续实时渲染最新的修改。&lt;/p>
&lt;iframe src="https://brush-api.elsetech.app/pages/KBDTrLJfrt" style="width:100%;height:600px;border:0;" loading="lazy">&lt;/iframe>
&lt;h3 id="创作自定义工具">创作自定义工具&lt;/h3>
&lt;p>你还可以根据自己需要，写一个自己用的趁手的前端工具，例如：&lt;/p>
&lt;iframe src="https://brush-api.elsetech.app/pages/l6IldHx-Ol" style="width:100%;height:600px;border:0;" loading="lazy">&lt;/iframe>
&lt;p>甚至能在网页上插入一个小型浏览器:&lt;/p>
&lt;iframe src="https://brush-api.elsetech.app/pages/cJ3w_m4e3K" style="width:100%;height:600px;border:0;" loading="lazy">&lt;/iframe>
&lt;p>这些工具都能用 &lt;code>https://brush-api.elsetech.app/pages/cJ3w_m4e3K&lt;/code> 的方式作为工具本身全屏打开。&lt;/p>
&lt;h2 id="实现过程">实现过程&lt;/h2>
&lt;p>&lt;a href="https://brush.elsetech.app">Magic Brush&lt;/a> 的实现非常简单优雅，整个 Design Agent 在前端运行，后端代码无法拿到用户的 API Token。后端部署在 Cloudflare Worker 上，数据库使用 Cloudflare R2 。整个产品的维护成本接近于 0，除非用户变多。&lt;/p>
&lt;p>Design Agent 的设计与传统 Code Agent 类似，约束是产出只能是一个 HTML 文件。在最开始的时候模型会返回一个根据初始提示词生成的 HTML 页面作为起始文件，而在后续 Modify 过程中，只允许调用一系列 tool call 来修改页面，最大化减少了 token 的消耗。&lt;/p></description></item><item><title>写在 AI Coding 奇点之后</title><link>https://blog.joway.io/posts/after_ai_coding_singularity/</link><pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/after_ai_coding_singularity/</guid><description>&lt;p>说来惭愧，我已经好几个月没有完全手写代码了，甚至像是把某个变量改一个名字这种活我都宁愿输入更多字符告诉 AI 来做而不是自己直接改了，因为我知道，哪怕是变量重命名这种简单的活，AI 也能做的比我仔细，它会检查所有用到这个变量的代码、相关的注释以及单元测试，然后一并都修改了，而我自己其实历史上就犯过不少忘记修改关联地方的错误。&lt;/p>
&lt;p>过去一两年的时间，AI Coding 引来了飞速的发展，就我的个人体验而言，大概经历了三个阶段。AI Coding 1.0 时代，AI 只是简单给你提供代码补全，代码总体还是人在写。而在 AI Coding 2.0 时代，人更像是在指导 AI 写代码，一次 prompt 只能高质量地写一百来行代码，你需要仔细检查代码是否符合你的需求。而最近两个月的时间，AI Coding 3.0 来到了一个奇点时刻，你甚至都不需要精心设计你的 prompt，只要简单描述下需求，AI 自己会根据代码上下文，以极高质量生成成千上万行代码，而人无论是体力还是智力已经无法 review AI 的代码，只能粗浅把握一个编程方向。在两个月之前，我或许还会认为过去的编程工作还能继续苟活一两年的时间，而现在我认为此刻所有传统的编程工作已经在事实上被淘汰，我们这些传统程序员之所以还没有被开除的原因仅仅只是公司自己在制度流程上还没有完全适应 AI 作为员工的方式，而这个适应过程可能会持续几年之久。&lt;/p>
&lt;p>在完全不手写代码的这几个月里，我时常在 AI reasoning 的间隙问自己到底在做什么。我似乎做着完全没有技术含量的『传话』工作，但与此同时，如果真要把我淘汰直接让 PM 和 Agent 对话，似乎也很难实现他们的需求，而且出事的风险极高。这意味着我依然在提供自己的价值，只是这个价值无法再被很好的「可视化」。我之所以比 PM 能让 AI 更好地干活是因为我具备一些专有的知识，这些知识的产出可能仅仅只是一个 Yes 或 No，但这不影响他们的价值。我之所以会觉得浑身难受的原因还是之前的日子过得太苦，觉得必须一定要自己手动干很多活才能体现自己的价值，没有真正把自己从一个「劳动者」换位到「管理者」。一个好的管理者肯定不是办公室里最忙碌的那一个人，那些做出对公司最有价值决定的管理者最终的产出也无非就是一个 Yes 或 No。能力强的员工往往希望老板管理的越少越好，AI Agent 也是如此。&lt;/p>
&lt;p>我们已经近乎实现了 Coding 的自动化，但是在一个公司里，Coding 只是整个公司工作的很小一部分，甚至都只是程序员工作的一小部分。我们现在拥有了一个无限智能的机器，但是整个公司运行制度都是面向人类设计的，Agent 需要发挥能力必须建立一个正向的 Loop，现在的 Loop 仅仅只建立在 Agent 内部，公司的每个环节与 Agent 并没有建立起这个 Loop，甚至连 Input/Output 渠道都没有建立。未来 Agent 会进一步渗透进整个公司一切流程的方方面面，不仅改变软件工程师的工作内容，也会改变产品经理，HR等一切职能的工作内容。我觉得这些传统工作岗位的消失会成为必然事件，但并不意味着不会创造新的工作岗位。最可能发生的事情是工作岗位从过去的不断细化的趋势开始变得不断泛化。现在你还在那区分前后端工程师属实是一个笑话，各种软件工种现在都可以统称为工程师岗位，如果一个后端工程师现在还写不好前端需求只能说明你不是一个合格的工程师。而所有工程师的工作会变成编排管理成百上千 Agent 的编码任务，解决 Agent 运行过程中遇到的无法自我解决的问题，帮助公司完善流程让 Agent 干活干得更舒心。&lt;/p></description></item><item><title>尼泊尔布恩山小环线纪行</title><link>https://blog.joway.io/posts/trek_poon_hill/</link><pubDate>Sun, 11 Jan 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/trek_poon_hill/</guid><description>&lt;p>2025 年 12 月跨年之际，为了赶在假期作废之前销假，我报了国内稻草人的一个徒步团，前往尼泊尔安纳普尔那山区进行了为期四天的 Poon Hill 小环线徒步。&lt;/p>
&lt;p>尼泊尔最有名的两条徒步线路分别为 EBC 和 ABC。就 EBC 而言，我目前的身体状态和高海拔徒步经验远不足以应对，ABC 相对简单一些，但是依然有高反的风险。由于我印象里自己似乎从未到达过海拔超过 3500 的地区，所以我对自己身体适应高原反应的能力完全没有认知，于是就退而求其次报名了最高登顶海拔 3800 米，最高过夜海拔 3200 米的 Poon Hill 小环线，以此作为我高海拔徒步的入门路线。&lt;/p>
&lt;p>&lt;img src="../../images/poon_hill/map.png" alt="">&lt;/p>
&lt;h3 id="day-1">Day 1&lt;/h3>
&lt;p>第一天的路程主要以爬升为主。从 Ghandruk 到 Tadapani，累计爬升 1116 米，下降 112 米，住宿海拔 2718 米。&lt;/p>
&lt;p>&lt;img src="../../images/poon_hill/day1.png" alt="">&lt;/p>
&lt;p>第一天的景色比较一般，全程主要在林子间穿越，最后还走了一小时不到的夜路才抵达住宿点。&lt;/p>
&lt;div class="post-gallery">
&lt;img src="../../images/poon_hill/day1_1.jpeg" />
&lt;img src="../../images/poon_hill/day1_2.jpeg" />
&lt;img src="../../images/poon_hill/day1_3.jpeg" />
&lt;img src="../../images/poon_hill/day1_4.jpeg" />
&lt;/div>
&lt;h3 id="day-2">Day 2&lt;/h3>
&lt;p>第二天路程较短，但是徒步海拔较高。从 Tadapani 上升到 Dobato，累计爬升 848 米，下降 108 米，住宿海拔 3456 米。&lt;/p>
&lt;p>&lt;img src="../../images/poon_hill/day2.png" alt="">&lt;/p>
&lt;p>早上起来，我们就在 Tadapani 看到了壮丽的安纳普尔纳南峰和鱼尾峰的日照金山：&lt;/p>
&lt;div class="post-gallery">
&lt;img src="../../images/poon_hill/day2_1.jpeg" />
&lt;img src="../../images/poon_hill/day2_2.jpeg" />
&lt;/div>
&lt;p>这一天的路上我们还时不时能从林子缝隙里从不同角度看到这两座山峰的容貌，在路程末尾还看到了道拉吉里峰和Tukuche peak。&lt;/p>
&lt;div class="post-gallery">
&lt;img src="../../images/poon_hill/day2_3.jpeg" />
&lt;img src="../../images/poon_hill/day2_4.jpeg" />
&lt;img src="../../images/poon_hill/day2_5.jpeg" />
&lt;img src="../../images/poon_hill/day2_6.jpeg" />
&lt;img src="../../images/poon_hill/day2_7.jpeg" />
&lt;img src="../../images/poon_hill/day2_8.jpeg" />
&lt;img src="../../images/poon_hill/day2_9.jpeg" />
&lt;img src="../../images/poon_hill/day2_10.jpeg" />
&lt;/div>
&lt;p>这一天我来到了有生以来最高的过夜海拔，在路上的时候倒还是适应，但是到了住宿的地方休息了一阵子之后，发现一做剧烈动作就有点晕，问了下 ChatGPT 似乎是这个海拔的正常轻微高反表现。这天气温也特别的冷，住宿条件也不高，甚至房间还有点漏风。&lt;/p></description></item><item><title>2025 投资组合年报</title><link>https://blog.joway.io/posts/2025-portfolio-annual-report/</link><pubDate>Thu, 08 Jan 2026 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/2025-portfolio-annual-report/</guid><description>&lt;p>2025 年，股市经历了政治的风云变化，也经历了 AI 浪潮的大洗礼。我的整体投资组合也引来了巨大的变化。这一年，标普 500 的年回报是 16.35%，而我的投资组合的回报只有 4.0%，远远跑输了标普 500。&lt;/p>
&lt;p>&lt;img src="https://blog.joway.io/images/2025-portfolio/statistics.png" alt="">&lt;/p>
&lt;p>&lt;img src="https://blog.joway.io/images/2025-portfolio/performance.png" alt="">&lt;/p>
&lt;p>其实在 &lt;a href="https://blog.joway.io/posts/2024-portfolio-annual-report/">2024 年底&lt;/a> 的时候，我还有 86.4% 的持仓还是在 VOO 和 QQQ 两大ETF，那时候 AI 浪潮才刚起步，世界政治局势也波澜不惊，在我不需要大量现金支出的情况下，这样的持仓非常舒服。&lt;/p>
&lt;p>然而川普上台后，在 4 月疯狂玩弄世界经济，而美国的政体却对这样的疯子没有一点制裁的办法，甚至还有一堆拥趸对其齐呼万岁，这彻底动摇了我对美国体制的信心。所以在四月大跌到一半的时候，我大幅度卖出了美股的持仓，在下跌到接近最低点的时候，又卖出了一批，卖出的资金后续陆续投向了美债市场和新加坡股市。当时伴随着美国股市，新加坡股市也引来了大跌，许多优质股如 DBS 引来了历史最佳位置，所以我乘机入手了一些 DBS 和海峡指数基金。&lt;/p>
&lt;p>然而后半年让我意想不到的是，标普 500 依然表现强劲，即便川普继续做了更多疯狂的操作，美股似乎依旧不为所动，持续创下历史高位。所以从后视镜看，我的调整无疑是失败的，但我的目标始终是建立一个稳健可持续并舒服的资产组合，从这个角度来说，我目前的组合是真正能符合我的目标的。我现在的持仓基本维持着股债分别占 50% 的水平。其中股票部分里，26% 在新加坡股市。&lt;/p>
&lt;p>&lt;img src="https://blog.joway.io/images/2025-portfolio/allocation.png" alt="">&lt;/p>
&lt;p>在绝大部分情况下，这样的组合基本可以让我一整年都不需要特别关心股市，实际上我可能现在打开证券软件的频率是一个月一两次。在美股大涨的时候，我的收益也不会差的特别多，美股大跌的时候，我甚至会特别开心，因为终于可以操作调仓补进美股了。&lt;/p>
&lt;p>但是我有一件事情其实到如今也想不明白的是，这一次的调仓，究竟是我对美国信心的动摇，还是我因为下跌而恐慌了。如果是因为后者，我就无法原谅自己了，因为在美股下跌的同时，我还有另一个稳健账户里存有不少的新加坡国债，我完全可以动用那笔国债进行抄底，这对我的持仓组合来说是巨大的天赐良机。但如果我是因为真的对美国的信心动摇，那么我的操作就是正确的至少是不应该后悔的。即便我十分擅长反思自己，但我至今也无法分辨自己当时的心态属于哪一种。我也深刻认识到「知行合一」的困难，它远远不是你知道了某件事然后就去做某件事这么简单，而是反过来，你做了某件事，你却不知道自己是因为哪一种「知」而做了这件事。而人性往往会倾向于帮你选择那一个你最希望的「知」来合理化你的行为，最终真正的「知」就消失在了一堆道不清的妄念之中了。你脑子里所臆想的你自己也会远远和你实际行动塑造的真实自己所背离。&lt;/p>
&lt;p>哎，学吧，太深了！&lt;/p></description></item><item><title>三十而笠</title><link>https://blog.joway.io/posts/2025-30/</link><pubDate>Sun, 21 Dec 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/2025-30/</guid><description>&lt;p>到 12 月底，我就迈过了人生的第三十个年头，进入到奔四的阶段。听年长的人谈起过，人对时间流逝速度的感知是指数变快的。可能这也是为什么对我而言，抵达三十岁的路程是极为漫长和艰辛的。&lt;/p>
&lt;p>这三十年里有近二十年的时间，我的人生是不受自己把控的。你无法决定自己应该看什么书，学习什么知识，交怎么样的朋友，尊敬怎么样的人，甚至连每天吃什么都无法决定。上帝给你随机分配了一个国籍，一个家庭，你的国家不管你认同与否强制把你塞进了一个社会制度，你的家庭又直接决定了你接近二十年的生活质量和初始品格。我的前二十年基本就是活在被权威压迫的氛围中，在我对世界的理解尚浅时，我被中国老师以严厉为美德，以恐惧为工具的教学方法所压迫，而最糟糕的是，我一开始甚至都没有认识到这是一种压迫，你可能还会真的相信老师是在“真心为你好”。“真心”大部分时候倒是没错，没有老师会主观希望学生差，但是客观上，整个中国的教育制度出发点更多是“为了国家好，为了集体好”，而非真心为了你个人好。如果是为了你个人好，那无论你贫穷还是富有，聪明还是弱智，你都应该从这个教育制度里获益，成为了一个更好的自己，但是显然这个教育制度一切的出发点是为了选拔能够更好地建设国家的人，而不在乎每一个个体最终的结果如何。&lt;/p>
&lt;p>我花了快三十年时间才想明白一件事情，最好的考试成绩其实应该是零分。学习知识的目的并不是为了考试，而是为了学习知识，考试的目的是为了检查哪一部分知识没有学到，如果你考了一百分，意味着你白花了几个小时做检测，没有找到半点没学到的知识。而只有以选拔为目的的考试，分数才是越高越好，但即便你拿到了满分，这也只能证明你掌握了这极小一部分的知识集合，且你超过了人类中极小一部分人群，而整个教育体系只覆盖了极少一部分知识而且有些知识还是被歪曲的，在这样的高度偏差还不干净的数据集下花上十多年的青春来训练得出一个高分，我实在想不到有什么更蠢的行为了。&lt;/p>
&lt;p>这个道理虽然如此浅显易懂，却在这个社会里属于歪理邪说，我也断然不敢和任何二十岁以下的人灌输这种观点，因为我知道，在这种教育体制下，你要逃脱这个愚蠢的游戏，唯一的办法就是首先尽量在这个游戏里取得高分，然后快点熬过青少年时期，成长到一个你有自主决定权玩什么样的游戏的年纪。如果你不慎在青少年时期就明白这个道理，却又不得已要继续玩这个游戏，除了让你更加痛苦没有任何好处。庆幸我小时候网上没有人写这种大逆不道的文章。&lt;/p>
&lt;p>在好不容易熬过前二十年后，我的人生终于引来了可以自主决定近乎一切事情的阶段。我在前二十年的那场游戏里并没有取得高分，只上了一所普通一本大学，但是所幸的是我成功读到了我唯一想读的计算机专业。我大学时候唯一的客观制约就是最好不要被学校开除以及尽可能拿到毕业证书，在这个底线之上，我几乎可以做任何合法的事情。我完全可以无视常规的从大一到大四的学习路径，在大一学习大四的知识，在大二就开始实习积累工作经验，在大四别人实习的时候我去世界各地旅行，而且并没有任何人会因为我的目无章法而指责我相反倒是收获了不少羡慕和赞美。恰好这段时光也是中国互联网乃至全行业发展最好的时间段，对我来说，我根本无需担心现在做什么和没做什么会影响到未来，因为那时对未来预期已经美好到现在做任何事情都影响不了的程度。&lt;/p>
&lt;p>在二十岁后的前五年里，我高频跳槽，并不断尝试做新的事情，学会了后端就开始学前端，学会了安卓开始学 iOS，做完了业务开发做基础设施，以工作时间来讲，那个时候是我工作时间最长的时候，甚至周末也会真的自愿加班来学习新的知识，但同时这段时间也是我工作最开心的时候。在后五年里，随着我逐渐打工越打越深入，以及打工的公司越来越大之后，工作的重心慢慢从“做有意思的事”转变为了“做有利于晋升和绩效的事”。我刚毕业那一会儿，虽然绩效也很不错，但其实根本是不管绩效在干活的，一门心思只想做有意思的事情并把事情做好。而且说实话我根本不在乎什么绩效和老板的看法，因为我已经拿到了学位证了，消除了这个社会制约我的最后一个把柄，从此我做任何事情，只要它合法，那最差的结果无非是公司把我开除了，而这个最差的结果根本对我就无从轻重，况且谁开除谁还不一定呢。然而这个淳朴的少年慢慢地在社会的锤炼下被一点点磨平了心气，我半主动半被动地，被带入了“成人高考”的游戏 —— 优绩主义。&lt;/p>
&lt;p>在我这些年的打工生涯里，我也逐渐认识到人和人之间有着巨大的分别，每个人工作的目的也多种多样。有赚钱养家型的，有生活工作平衡型的，有埋头只想晋升型的，还有像我一样只想纯粹写代码型的。可能就是因为这种差异性，公司必须发明一种统一的价值衡量体系，不管每个人主观意愿如何，只要你还想在这个公司干，甭管你个人喜欢什么型，都必须围绕着这个公司制定的价值衡量体系转。是不是很熟悉？这和中国的教育体制如出一辙，甚至还更残酷和荒谬。考试的试卷对每个人都一样，评判成绩的标准也是统一和公平的，而且每个人都可以考一百分。但是企业的绩效制度却充满了人情世故甚至是尔虞我诈，并不单纯只是你对公司有贡献就能取得好的绩效，而且你绩效好大概率就有人得要绩效差。有时候我会觉得大家很可笑，就多和少那点奖金，何苦一整年全部一门心思在这个上面勾心斗角。工作的目的原本应该是把工作本身给做好，现在反倒成了一定要比别人做的“好”，且得是你老板所认为的“好”。我不喜欢这个游戏并不是我无法赢这游戏，恰恰相反，我大部分工作年份拿的都是好的甚至是 Top 的绩效，我和我大部分老板也都是非常好的朋友，只是最近这些年互联网公司以及其中的员工有点在这个游戏里玩的太入戏了，让我非常出戏。&lt;/p>
&lt;p>所幸的是，经过这些年的积累，我多少也具备了能够逃离这种游戏的能力和底气。在 2025 年初，我给自己立下了一个目标，在 30 岁之前，尽可能不做自己不喜欢做的事情，达到自己想达到的生活状态。为此，我换了更健康的工作，恶补了英语，锻炼了身体并近乎解决了长期困扰我的颈椎病，还申请了新加坡公民。我不敢说自己达到了这个目标，但至少比年初的时候要距离目标接近了一大截。&lt;/p>
&lt;p>最重要的是，我认为我现在在玩一个我真正喜欢玩的游戏。我可以自由地学习我想要学习的知识，我也可以自由地选择不学习只摆烂，我可以选择去赚更多的钱，我也可以选择少赚一些钱多出来一些时间锻炼身体，我的社会成就比很多人低的多但是没有人会以此来评价我个人。我其实明白，这个游戏的危险之处在于，这是一个高度以自我为中心的游戏，脱离了任何客观评价的制约，最终容易度过一个碌碌无为的一生。但是，又有谁能如此武断地去评价一个人碌碌无为呢，每一朵花开自有它的原因，不一定要被你看到才是实现了一朵花的价值。&lt;/p>
&lt;p>我小学就很喜欢《江雪》这首诗和它的配图，甚至在课桌板下画了“孤舟蓑笠翁”的画，没想到二十年过去，我终于活成了“南洋蓑笠翁”。&lt;/p>
&lt;p>&lt;img src="../../images/jiangxue.png" alt="">&lt;/p></description></item><item><title>我的 8 年职业生涯回顾</title><link>https://blog.joway.io/posts/career-8-years/</link><pubDate>Sat, 02 Aug 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/career-8-years/</guid><description>&lt;p>我正式 Onsite 的工作经历是从 2017 年 3 月加入即刻实习开始，到如今也已经度过 8 年多的时间。这 8 年时间，我先后任职了 4 家公司，从十几个人的天使轮公司，到一百多人的创业公司，再到五千多人的独角兽公司，最后到十几万人的超大型互联网公司。相伴着这段经历的，也是中国移动互联网快速发展的 8 年，我有过为了理想而自愿加班奋斗的经历，也有单纯为了工资勉强捏着鼻子工作的经历。社交媒体上对各种类型公司的吐槽和夸赞，我大体都有过亲身的体验，也更清楚自己每个阶段需要什么样的环境。恰好现在正式结束了第四家公司的工作，同时又即将到了三十岁的年纪，所以是时候回顾下我这 8 年的职业生涯，也供其他处于早于我的人生阶段的读者在迷茫公司选择时的参考。&lt;/p>
&lt;p>2017 年，当时的我还在读大三，但是已经在给一家业务主要在美国的在线教育公司利用课间业余时间远程实习赚外快。这家公司的系统经历过许多名美国各路大厂一线工程师的参与，所以整体技术 sense 都非常好，技术栈也算新颖，为我的技术生涯起步奠定了非常好的基础。在这家公司，我最开始只是做单纯的软件开发，后来因为做基础设施的同学的淡出，我逐渐参与甚至主导了很多基础设施的搭建和运维的工作，于是就误打误撞上了基础设施这辆车。&lt;/p>
&lt;p>后来，我偶然用到了一款叫做「即刻」的 App，会对互联网上的 RSS 甚至是非 RSS 的信息源，设置一些抓取过滤条件订阅，构建自己的信息流，并为这些互联网上原本没有评论区的信息构建了一个有高质量用户的评论社区。用着用着，当时正觉得学校的课程没意义，想要出来找实习，于是就顺着找到官网，投递了简历。今天回过头去看，当时的想法和简历都相当稚嫩，并且我当初完全不懂什么叫做校招，秋招，夏招，不知道实习招聘也分季节，我只知道我当时的自己希望积累更多的实习经验，就这么单纯。而我当时的课程安排实际上是完全不可能出去实习的，我就想了一招只有那些大学富二代混子才会干的事情，我拿我实习工资的一部分出来，花钱找人给我代课并且告诉我课程作业。就这样，我最后通过了面试，从杭州去到了上海实习，而且是五天 Fulltime 的实习。为什么会直接 Fulltime 也是因为我当时完全不知道实习原来可以不全勤，甚至实习生还可以因为本身工资就比正式员工低可以潜规则默认可以早下班，我当时就单纯认为实习就得和正式员工一样上班，得有一样的产出比较，稀里糊涂就跑去了实习。&lt;/p>
&lt;p>我在即刻的经历分为两段，从 2017 年 3 月工作到了 2018 年 10 月，去了一家更小的数据库创业公司呆了 5 个月，又回到了即刻，一直工作到了 2020 年 9 月。我在即刻第一段实习经历期间主要是在做基础设施的工作，这块工作只有我和我当时的 mentor 在负责，所以即便我只是一个实习生，做的事情也非常多。而且基本没有人主动来告诉我应该做哪些事情，应该做的事情总是自然而然地就自己出现了。我去的时候当时大家刚从 rancher 迁移到 Kubernetes 上，可以说基础设施是完全啥也没有，也不知道应该有啥。于是，我看大家捞日志不方便我就搭了个日志平台，服务配置一开始大家都写在代码里我就搞了一个配置中心，公司内部没有鉴权管理我就搞了一个接入了所有平台的鉴权中心，看大家都是裸用命令行发布服务我就搭了一个部署平台，我印象里这些系统都是用了极短的时间完成的，也丝毫没有方案设计和讨论这些步骤，先花最短时间做出来再说，而因为你只用了短的时间所以最差结果大不了不上也是能够接受的结果。我细数了这几套系统，在大公司可能已经足够养上百人的团队，光是方案设计讨论完都得加起来花上一年，但是在我实习的一年半时间里都做完甚至优化过好几波了。&lt;/p>
&lt;p>从今天回过头看，当时我作为实习生的 Scope 简直可以说大到一手遮天的地步了。以鉴权中心来说，最开始只是对一些内部网站做基本的 RBAC 权限控制，后来公司发展出来一个人数众多的审核团队，不同审核人员的权限细分也被纳入到了这个 RBAC 体系内，以至于任何新员工要入职都要来我这里注册一堆权限。我觉得我人生中所有遇到过的工程师，都没有一个能够有我当初实习时拥有的机会，一次性参与到这么多系统的开发，特别是这些系统还能伴随着一个创业公司一起成长，不像是在开发系统更像是在搭建一个创业公司。以至于我今天去看系统设计的题库，发现里面大半都是我自己实实在在亲手设计过的系统。&lt;/p>
&lt;p>虽然今天的我无比感激当初的经历，然而在我当时的稚嫩想法里，我却觉得我做的事情都太简单了，因为这些都不是那种服务千万级用户的系统，也没有什么值得拿出来说道的独家设计架构，所有合格工程师只要花上时间都应该能够写这些系统，这也促成了我最后离职去了一家数据库公司的原因，因为我想做更加有挑战的基础软件。但是今天的我在新加坡面试各路公司时，我惊奇地发现，我用到的大部分知识居然都来自我这段实习经历。这些系统虽然简单，但是需要你系统性，全链路地用到各种计算机领域的数据库，消息队列，软件架构的知识，而且你得真正独自一人手把手开发过这些系统才能有不会忘记的深刻经验，而不是网上看几篇系统设计文章然后夸夸其谈。而绝大部分在大公司工作的人，可能要花十几年才能积累到我这段经历的广度。如果今天再来问我哪段职业经历我一定不会选择放弃，那一定是这段经历，甚至这也不一定是加入创业公司就一定能有的经历，不是每个创业公司都会允许一个实习生操刀这么多重要系统，这是我 8 年职业生涯最宝贵的财富。&lt;/p>
&lt;p>2018 年 10 月，我加入了一家叫做 Dashbase 的专注于日志搜索引擎的美国创业公司，最初我的预期是能够参与到引擎的核心部分开发，但是入职后发现事与愿违，大部分工作都是在做 2B 的对接和问题排查，而引擎的核心代码，由于我们中国员工由于国籍的原因甚至连接触代码的权利都没有。加上另外的一些私人原因，最后我还是很快离开了这家公司回到了即刻，不过这家公司在我离开后过了几年也被 Cisco 收购了，证明了公司本身确实是一家不错的公司，如果当初选择留下也能有合理的回报，但是我也没有丝毫后悔当时的决定。&lt;/p></description></item><item><title>我的颈椎病康复之旅 - 关于选择的故事</title><link>https://blog.joway.io/posts/cervical-spondylosis/</link><pubDate>Sun, 29 Jun 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/cervical-spondylosis/</guid><description>&lt;p>从 2014 年上大一开始到现在，我的颈椎已经日均面对电脑超过 8 小时 11 年了，世间的物理器件应该很少有能工作超过 10 年之久的寿命，而我的颈椎也在差不多工作到第 10 年的时候出现了严重的问题。&lt;/p>
&lt;p>其实早在四年前的时候，我的颈椎已经给了我警告。但那时候只是偶尔会出现僵硬疲劳的感受，通过每周的按摩差不多都能有所好转，如果专心于工作，其实并不能时刻感觉到不适。后来拍片发现了颈椎已经开始出现了生理性变直的问题，但是由于现代人有相当一部分都有相似的问题，所以我也并没有很在意。并且依然维持着每天十点多到晚上八九点钟的高强度工作节奏，更为糟糕的是，我在那时候还长期保持着趴睡的习惯，让我的颈椎曲度一天 24 小时都没有一个正常的状态。&lt;/p>
&lt;p>这种生活方式维持了差不多两年多的时候，我的颈椎问题已经恶化到了两眼偶尔会模糊，严重时已经无法正常工作的程度了。并且此时按摩已经连短期恢复的效果都达不到了。最严重的一次是，我因为脖子难受而在旋转脖子的时候，似乎不小心压到了一根神经，导致那天我甚至都紧急去了急诊室。那次遭遇让我意识到这个问题已经严重到没有任何其他问题能超过的地步，我甚至都为此备好了后事比如把我的银行账号都发给了我姐一防哪天真出现了意外。从那天之后，我开始下定决心要认认真真解决这个问题。&lt;/p>
&lt;p>我在新加坡先是去拍了 MRI ，以确认我颈椎是否真的有物理上的形变问题，但是结果是除了正常的曲度变化外，并没有看到有其他严重的问题。此后，我又去看了专门做美式整脊的 Discover Chiropractic 诊所的 Dr Joachim Low，通过他的正脊治疗，反倒是彻底解决了我之前伴随着的腰痛问题，迄今为止一直没有复发，但是我的脖子问题却丝毫没有改善甚至还有加重。接着我又去看了专门的脊柱专科医生 Dr Wu Pang Hung，他也根据 MRI 认为我的脊柱本身没有什么问题，给我开了加强骨关节的药物，但是也没有什么效果。后来我又专门找了一家名叫 The Pain Clinic 的 Dr. Ho Kok Yuen，他给我做了验血发现我有维生素 D 缺乏的问题，先是给我开了一些维生素片，然后在脖子上针对疼痛的部位注射了类固醇，在注射完的当下，确实感觉效果非常不错，但是一般维持个三四天疼痛感就又会回来，并且通过一个多月的高剂量维生素 D 补充也没有发现有丝毫改善的迹象。在实在不行的时候，我就又会回去找 Dr. Ho 再给我来一针。我觉得这也不是长久之计，最后，我去看了 Singapore Paincare Center 的 Dr Bernard Lee Mun Kam ，这是我整段治疗过程的最后一站，而 Dr Lee 也是世界上我除了父母之外最感激的人，甚至可能都没有之一。&lt;/p>
&lt;p>在 Dr Lee 的诊断下，他认为我的根本性问题还是在于神经而非物理的颈椎或是肌肉上。但是在神经的影响下，我的颈部肌肉已经出现了严重的筋膜炎，所以需要先用一种叫做 PRP(Platelet-Rich Plasma) 的血小板注射手术，快速让脖子肌肉先恢复，然后再使用抗抑郁症的药物去舒缓神经的紧张问题。我是在 2024 年 8 月 31 日做的 PRP 注射，一共花了 6500 SGD ，注射打了局部麻药，所以那天我回家又睡了一整天。但是注射完后，我感觉真正意义上的生活又重新回来了。与此同时，我还报了 20 节私教课，每周训练肌肉力量，让身体找回年轻的感觉。在 10 月份我的身体状态已经恢复到了能完成环勃朗峰高强度徒步的程度。&lt;/p></description></item><item><title>熊野古道中边路纪行</title><link>https://blog.joway.io/posts/japan-kumano-kodo/</link><pubDate>Mon, 12 May 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/japan-kumano-kodo/</guid><description>&lt;p>大约是在六七年前第一次听说熊野古道，后来又在任宁的播客 &lt;a href="https://podcast.weareones.com/episodes/118">声音：时速三公里&lt;/a> 里，对这条路线有了更加清晰的认识，暗暗把它加入了愿望清单。后来由于疫情的原因迟迟未能如愿，终于在今年 5 月，得以来到纪伊半岛踏上了这条古老的朝圣之路。&lt;/p>
&lt;p>纪伊半岛毗邻京都，奈良和大阪，在日本历史上一直是重要的佛教圣地，众多高僧名士在此活动，他们往来半岛各处庙宇间的道路便逐渐成了现今的熊野古道。熊野古道分为多条路线，有大边路，小边路，中边路，伊势路等。我们此行选择的是住宿相对最容易预定，难度也适中的中边路。&lt;/p>
&lt;p>当地的观光协会在途径的神社门口会设立印有神社图案的盖章点，朝圣者集齐特定数量和路线的盖章，可以前往特定几个城市的游客中心获得特别的踏破认证。但由于其中一些匪夷所思的流程设计 —— 比如盖章本是在徒步起点十多公里处才会售卖 —— 就我的体验来看，盖章活动和纯粹徒步之间衔接的并不是很好，如果你追求的是集齐所有盖章，那么你不得不走回头路，或者单纯为了盖某个章去坐公车。出于不想舍本逐末的原因，我们放弃了一些并不顺路的盖章点，只走了全程的大概 90% 顺路路线。&lt;/p>
&lt;p>&lt;img src="../../images/kumano/map.png" alt="">&lt;/p>
&lt;p>全程行迹图：&lt;/p>
&lt;p>&lt;img src="../../images/kumano/route.png" alt="">&lt;/p>
&lt;p>朝圣盖章：&lt;/p>
&lt;p>&lt;img src="../../images/kumano/stamp.jpeg" alt="">&lt;/p>
&lt;h2 id="行前准备">行前准备&lt;/h2>
&lt;p>熊野古道成行最大的困难点在于中途的住宿预订，其中绝大部分停留村镇基本无正规意义的酒店可言，当地民宿也大多没有入驻到传统预订平台，而是要通过熊野古道官方的预订网站 &lt;a href="www.kumano-travel.com">kumano-travel&lt;/a> 进行预订。这个网站每点击都要等待约 10 秒钟，而且你需要一次性把你的徒步行程都规划好，然后一次性预订所有沿途住宿，住宿提供方不仅会审查是否有空余房间还会检查你的行程规划合理性，如果有其中一个点无法成功订到住宿，整个预订单都会失败。由于 5 月初恰好赶上了日本的黄金周，所以我提前了 6 个月就开始预订，但即便如此也有很多住宿点已经没有空余 slot 了。&lt;/p>
&lt;p>纪伊半岛 5 月大概处于春季，加之一共也才 4 天的徒步，为了减轻每天徒步的背负重量，我把随行装备减少到了极限，只有身上穿的一套春装和备的一套春装，以及一些急救装备，电子设备和登山杖。总共仅 5 千克。&lt;/p>
&lt;h2 id="day-1-滝尻王子-到-近露王子">Day 1: 滝尻王子 到 近露王子&lt;/h2>
&lt;p>上午 9 点坐公交从纪伊田边抵达熊野古道馆，正式开始徒步。这天上午一直下着小雨，不过所幸这是这四天旅程唯一一段雨中行走。&lt;/p>
&lt;p>&lt;img src="../../images/kumano/day1/kumano-1.jpeg" alt="">&lt;/p>
&lt;p>一个半小时后，抵达一处展望台，依稀可见远处群山。&lt;/p>
&lt;p>&lt;img src="../../images/kumano/day1/kumano-2.jpeg" alt="">&lt;/p>
&lt;p>中午 12 点，抵达高原的休憩所，天气彻底转晴，在阳光下解决午餐。&lt;/p>
&lt;p>&lt;img src="../../images/kumano/day1/kumano-3.jpeg" alt="">&lt;/p>
&lt;p>下午全程都在森林中行走，雨后山蟹也跑到了道路上，空气异常清新。&lt;/p>
&lt;p>&lt;img src="../../images/kumano/day1/kumano-4.jpeg" alt="">
&lt;img src="../../images/kumano/day1/kumano-5.jpeg" alt="">
&lt;img src="../../images/kumano/day1/kumano-6.jpeg" alt="">
&lt;img src="../../images/kumano/day1/kumano-7.jpeg" alt="">
&lt;img src="../../images/kumano/day1/kumano-8.jpeg" alt="">&lt;/p>
&lt;p>下午 4 点 30，抵达近露。远看近露只是一个小村庄，但却在这里找到了相比后面几天最大的超市。&lt;/p>
&lt;p>&lt;img src="../../images/kumano/day1/kumano-9.jpeg" alt="">
&lt;img src="../../images/kumano/day1/kumano-10.jpeg" alt="">&lt;/p>
&lt;p>第一天一共行走了 16.8 km，爬升 1000 m。&lt;/p>
&lt;h2 id="day-2-継桜王子-到-熊野本宫大社">Day 2: 継桜王子 到 熊野本宫大社&lt;/h2>
&lt;p>上午 8 点坐巴士抵达継桜王子的山脚处，开始今日的徒步。今天大约有 1/4 的路程都在马路上，但是道旁的景色一点也不输山里。&lt;/p></description></item><item><title>新加坡旅居三年再回首</title><link>https://blog.joway.io/posts/singapore-3-years-review/</link><pubDate>Mon, 31 Mar 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/singapore-3-years-review/</guid><description>&lt;p>自从 2022 年 5 月 4 号抵达新加坡，已经近乎快满三年了。我来新加坡的旅程相当坎坷，具体过程记录在了&lt;a href="https://blog.joway.io/posts/run-away-from-shanghai/">《那一天，我决定踏出一步》&lt;/a> 中。此后，我几乎没有在博客里记录过关于这个国家的任何评论，很大程度是因为我想要更加深入了解这个国家，才好更客观地评价，特别是拿这个国家与之前呆了二十几年的国家进行对比，这样才不失公平。三年过去了，我想是时候重新回顾下这段旅程，也为后来人做一些参考。&lt;/p>
&lt;p>在 2022 年来之前，我在上海的境遇不能说不好(抛开封城特殊时期不谈)，但也绝对算不上体面。我应该算是有一个体面收入的软件工程师，基本享受了外卖自由，超市自由，打车自由和餐馆自由，我在大部分事物的消费上几乎不需要怎么在乎价格，因为日常消费的东西很少能超过一天的工资。但是这座城市似乎论资排辈并不看重这些，在另一种评价体系里，我是一个没有上海暂住证更没有户口的外地人，租着一个一居室的老破小，依靠自己的收入几乎不可能在上海比较体面的地段买得起体面小区的房子，更别提这些房子还在大幅度涨价。至于户口，在当时的政策下，我基本没有什么可预期的途径拿到上海户口，最快的方式可能就是花个大几十万以及综合两年的时间成本去读个英国硕士然后回来落户。而没有户口又影响了能够购买的房子以及何时能够购买，后者又影响了买房的价格 —— 在当时永远上涨的预期下。所以按照正常的发展路径，大概率最后我还是要“降级”回二线城市。&lt;/p>
&lt;p>而三年后在新加坡的今天，我这三年里点外卖的次数低于五次，超市几乎每样东西都要关注价格是否打折，迫不得已不会打车，除非重要日子否则只去价格 Bottom 20% 的那些餐厅。从生活质量上看，与上海对比，显然是极大地降级了。但是另一方面，我在大约 2 年的时候拿到了新加坡的永久居民，然后以外国人的身份在这里买了房子，并且有了自己的养老金和公积金。按照政府的正常规范路径，只要我自己愿意，并且持续工作下去，我可以很快还清房贷并且在 55 岁有着不错的退休现金流。至于到时候我要去哪里花这笔现金流，完全由我自己了。&lt;/p>
&lt;p>所以当我客观地回顾对比在这两个国家生活的境况时，很难得出一个简单结论 ———— 哪里生活更好。如果以证券来比喻，上海更像是股票，新加坡更像是债券。持有股票，你不知道明天是涨是跌，但是你很清楚至少今天在涨，先把今天过好。持有债券，名义上票面利率已经确定，本金也一定会归还，你明确知道未来的收益预期，但就今天而言你最多只能获得今天的利息。&lt;/p>
&lt;p>但是如果把这个问题更加主观化 ———— 你愿意在哪个国家生活 。 我会毫不犹豫地选择新加坡，下面是我为什么选择新加坡的理由，也是我这三年的生活体验。&lt;/p>
&lt;h3 id="生活半径">生活半径&lt;/h3>
&lt;p>许多人一提到新加坡，会用一种轻蔑的口气谈论它的「小」。这是事实，但是这仅在把它作为国家而言，才是事实。作为一个城市，新加坡恐怕要比大部分发达国家的城市要大得多，特别是欧洲。柏林人口 370 多万面积 891 平方公里，马德里人口 330 万面积 604 平方公里，而新加坡人口 600 万，面积 735.2 平方公里。虽然人均面积肯定较小，但是似乎也没有差距很大？刚好就是一个城市大概应该有的面积大小。&lt;/p>
&lt;p>另一方面，作为一个市民，只有自己能够实实在在会去的地方，才属于这个城市的有效面积，而非地图上的实际面积。上海人恐怕大部分都没有去过临港，即便那里地铁都能到达，也属于上海辖区。我也从没有听说过在上海有人周末会约去海滩，抑或是爬山，骑自行车更加是通勤才会做的事情而非休闲。绝大部分上海的小区附近也绝非是跑步的好地方。而这些实实在在都是在新加坡的人周末甚至下班后会做的事情。在上海，休闲活动往往选择不在上海，这才让上海人总是把江苏和浙江当作自己的后花园。所以从这个意义上看，上海才是真正的小，小到得离开上海才能有正经的休闲活动。&lt;/p>
&lt;p>最后，有效的生活半径也有一定的时间尺度极限，比如开车 20 分钟内。在新加坡，绝大部分人的居住范围内，20 分钟车程都能覆盖从海滩到山林再到市中心的各种博物馆餐馆，小区楼下也大概率一定有适合跑步的步道，如果住的是公寓更会自带健身房和游泳池。这些都是在上海无法想象的市政设施的可触达便利性，这一方面有面积小所带来的紧凑优势，另一方面也是市政规划的刻意为之。&lt;/p>
&lt;h3 id="运动氛围">运动氛围&lt;/h3>
&lt;p>正是因为生活半径内能达到的地方非常多，而这里大部分地方又都是运行场所，加上这里一年 365 天都是夏天，所以在新加坡很容易养成喜欢运动的爱好。我在上海几乎不怎么运动，首先上海的市政规划就基本没有给你运动的选项，其次上海的天气在一年大部分时间里也并不适合室外运动，再者周围的人确实也都没有运动的氛围。&lt;/p>
&lt;p>但在新加坡，地铁上一眼看过去大部分年轻人的身材都能看到运动的痕迹。特别是男性公民，在 40 岁之前，政府都会强制每年都要回军队复训和体检，如果不合格会要求参加额外体能训练。与此同时，各种保险以及政府软件也会有各种各样的运动优惠和补贴，总之在新加坡，运动能赚钱是真的。&lt;/p>
&lt;p>我现在每个周末基本都会安排至少一次运动，可能是骑车，可能是徒步，也可能是跑步，也可以是跑步完后徒步，或者骑车完后跑步。在我家往北 2 公里可以到达一个非常大的碧山公园，往南 5 公里可以直达国家体育馆和旁边非常大的一个公园联合体，所以每周可以在地点和运动项目间互相组合出不同样的运动安排。&lt;/p>
&lt;h3 id="人民素质">人民素质&lt;/h3>
&lt;p>我不会说新加坡人的素质就比上海人高。在我看来，大家都是一样的人，但是，新加坡的教育和法律让这里的人「看起来」素质确实要比中国高一大截，特别是遵守规则方面。我搬进现在住的公寓后，发现一个奇怪的现象，所有人的鞋子都放在门槛的位置，而非连接门槛的走廊上。在我的观念里，你在你家门口放你的鞋子总不能算是过分的事情，虽然严格来说，家门口的走廊确实也是公共区域。但是新加坡人对公共区域的不侵犯到这种程度也是让我非常震惊。而在国内生活过的人应该都知道，侵占公共空间在国内不是什么大新闻。即便有物业，大部分业主也并没有把物业的规则当一回事。&lt;/p>
&lt;p>和穷山恶水出刁民反过来，养尊处优也比较容易出善良的国民。新加坡人就是这样的群体。我在新加坡遇到的善意要比我在上海多很多。我遇到过给我修锁的师傅觉得我外国人来打工不容易主动给谈好的价格减价，给我缝针的阿姨不要我钱，甚至给我来装门铃的小哥看到我大门关门特别响额外专门给我调整了下螺丝。作为回报，我也为新加坡人贡献过很多善意。这是一份互相传播的善意氛围，激励每个人都加入到这个互帮互助的氛围中。而在中国，很多时候你的善意无法得到同等的回馈，或者是对方每天过于忙碌而没时间来为你的善意反馈，久而久之你也不会习惯于付出善意了。特别是，如果对方本身就是依靠自己的技能来谋生，反倒是你要求对方牺牲自己的利益贡献善意显得特别邪恶了。而新加坡，做这些工作的不一定是非常缺钱的人，甚至真的有住别墅的老人为了打发时间出来做一些手艺活的。他们的善意对他们来说成本就要比中国这些欠发达地区低很多了。&lt;/p>
&lt;h3 id="法制环境">法制环境&lt;/h3>
&lt;p>对我而言，法制的意思就是有事情我们提前说好，有地方可以检查我要做这件事要符合的所有准则，只要我按照你说的做，你就别来找我麻烦。有在中国的生活经验的人应该都明白，在中国，要达到这么基础的要求有多难。举些简单的例子，谁能列出来开家咖啡店一共需要哪些明里暗里的程序，色弱到底能不能考驾照，我交了养老金到底未来能够给我多少钱是不是一定会给我钱，甚至是天价买的房子 70 年后到底归谁怎么归属都没人能说明白。&lt;/p>
&lt;p>在中国，如果你按照程序办事，恐怕你会寸步难行。我在中国的色盲鉴定标准就是色盲，即便我能够分辨绝大部分颜色。但是我依然考出来了驾照，依然读了规定色盲不能读的计算机专业，你说我怎么办到的 :) 。如果按照新加坡人的遵守规则的准则，我恐怕现在在中国也是一个彻彻底底的失败者。然而我就是一个想要遵守规则的人，但是同时我也不想要失败。&lt;/p></description></item><item><title>高加索三国 - 阿塞拜疆行记</title><link>https://blog.joway.io/posts/caucasus-azerbaijan/</link><pubDate>Mon, 24 Feb 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/caucasus-azerbaijan/</guid><description>&lt;p>我是在 2023 年 9 月 28 号去到的阿塞拜疆，而刚刚在一周多前的 9 月 19 日，阿塞拜疆刚刚针对被亲亚美尼亚势力实际控制的纳卡地区展开了所谓的“反恐行动”，控制了纳戈尔诺-卡拉巴赫(简称纳卡)地区。我在阿塞拜疆的旅程也被笼罩在阿塞拜疆与亚美尼亚的领土争端气氛中展开。&lt;/p>
&lt;h2 id="纳卡问题">纳卡问题&lt;/h2>
&lt;p>如果你看到纳卡地区的地图，你便知道为什么阿塞拜疆一定要拿下这片土地：&lt;/p>
&lt;p>&lt;img src="https://blog.joway.io/images/azerbaijan/nk-map.png" alt="">&lt;/p>
&lt;p>纳卡地区就是一块国中国飞地，虽主权也并不归属亚美尼亚，但是其土地上生活的大部分人都是亚美尼亚人。但倘若你再完整看一看阿塞拜疆的地图，可能又会有另一个疑问，这个纳希切万又是哪里来的，为什么也横插进了亚美尼亚的国土里？&lt;/p>
&lt;p>&lt;img src="https://blog.joway.io/images/azerbaijan/az-map.png" alt="">&lt;/p>
&lt;p>纳希切万恰好就是纳卡地区的反面，虽然身处亚美尼亚腹地但生活的大部分却是阿塞拜疆人，名义上也是连亚美尼亚都承认的阿塞拜疆正统领土。这么一看，亚美尼亚反向申索对纳卡地区的控制似乎又显得没有那么强词夺理了。&lt;/p>
&lt;p>纳卡问题历史上发生过大大小小多次战争，总体来说，俄罗斯支持的是亚美尼亚，土耳其支持的是阿塞拜疆，两边都在打着代理人战争，长期以来，阿塞拜疆实际上是被亚美尼亚压着打，属于下风，毕竟整个高加索地区无论从历史还是现实来说都还是俄罗斯人控制的地盘。然而之所以在 2023 年一举被阿塞拜疆拿下纳卡地区，一方面是由于俄罗斯自己都在乌克兰战场自顾不暇，另一方面也是因为此时的亚美尼亚帕希尼扬政府也在寻求渐渐脱离俄罗斯的控制，转向美国甚至寻求加入欧盟，导致俄罗斯对亚美尼亚的态度也发生了逆转。&lt;/p>
&lt;p>事实上，以今天发生的现实来说，亚美尼亚这个国家的整体利益已经不能再一直为了另一个国家土地上的亚美尼亚裔人而一直被绑架，为了自身利益不得不放弃了对这块土地的实际控制和支援，然而纳卡飞地上的居民并不这么想，依然坚定认为自己就不属于阿塞拜疆，至少也应该维持自治地位，从而爆发了冲突，但是又孤立无援，进而被阿塞拜疆完全掌控。&lt;/p>
&lt;h2 id="艺术品中的领土痕迹">艺术品中的领土痕迹&lt;/h2>
&lt;p>前面讲到了阿塞拜疆复杂的领土问题，所以自然阿塞拜疆人在这件事情上也非常敏感。在阿利耶夫文化中心，最让我印象深刻的是一系列用阿塞拜疆的传统工艺——毛毯和纺织——来表达阿塞拜疆完整国土的作品。&lt;/p>
&lt;p>&lt;img src="https://blog.joway.io/images/azerbaijan/art-map1.jpeg" alt="">
&lt;img src="https://blog.joway.io/images/azerbaijan/art-map2.jpeg" alt="">&lt;/p>
&lt;h2 id="露天的战争博物馆">露天的战争博物馆&lt;/h2>
&lt;p>前面讲到在大部分历史时间里，阿塞拜疆一直是被亚美尼亚压着打，所以长期以来积累了无数的窝火，直到最近几年才胜仗频出，特别是一周多前彻底解决了纳卡问题，所以自然要向民众大肆宣扬战果。但我从来没有见过一个国家会对战争的宣扬像阿塞拜疆这么露骨，直接在市区腾出一大片空地，把真实的战场摆进市区，而且摆的还是亚美尼亚的军营，所有装备素材全部都是从亚美尼亚那里得到的战利品，从床铺，宣传海报到实实在在的坦克军车和飞机，直接 1:1 还原，甚至还建了大量战壕和铁丝网带。从博物馆角度来说，这是我去过的最真实的博物馆。从人性角度来说，看着那些小孩笑嘻嘻地在坦克上爬来爬去，心里很不是滋味。&lt;/p>
&lt;p>战利品的意思是，这里每一个军事装备背后至少有一条人命的丧生。&lt;/p>
&lt;div class="post-gallery">
&lt;img src="https://blog.joway.io/images/azerbaijan/war-2.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-3.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-4.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-5.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-6.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-9.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-10.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-11.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-12.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-15.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-17.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-18.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-19.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-21.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-23.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-24.jpeg" />
&lt;img src="https://blog.joway.io/images/azerbaijan/war-26.jpeg" />
&lt;/div>
&lt;p>从照片的背景也能看出，这是一个接近市中心的地方，背后是静静地里海。我很难用复仇来形容这种公开的展示，因为大部分前来的阿塞拜疆人都是微笑的，甚至都没有很严肃对待，可能这是我永远无法理解的情感。&lt;/p>
&lt;h2 id="流油的巴库">流油的巴库&lt;/h2>
&lt;p>阿塞拜疆首都巴库地区是世界上最早开采石油的地方，巴库油田有一个特点是分布分散，这导致这个国家到处都字面意义地在流着石油，也到处可以看到一些小到像是农具的石油钻井，还有一些地方存在着所谓的永恒之火。&lt;/p>
&lt;p>&lt;img src="https://blog.joway.io/images/azerbaijan/oil-1.jpeg" alt="">
&lt;img src="https://blog.joway.io/images/azerbaijan/gas-1.jpeg" alt="">
&lt;img src="https://blog.joway.io/images/azerbaijan/gas-2.jpeg" alt="">&lt;/p>
&lt;h2 id="自由的巴库">自由的巴库&lt;/h2>
&lt;p>作为一个伊斯兰国家，巴库的自由程度是我在其他国家所没见过的，甚至是以世俗化著称的土耳其我觉得与之相比都有所逊色。不仅饮食上无所禁忌，甚至在博物馆里，到处都陈列着前卫露骨的艺术作品，在部分街区，有一种来到了巴黎的感觉。而在另一些角落，还能看到一些前苏联审美，更是让这里显得文化更为多元和包容。&lt;/p>
&lt;p>&lt;img src="https://blog.joway.io/images/azerbaijan/art-0.jpeg" alt="">
&lt;img src="https://blog.joway.io/images/azerbaijan/art-1.jpeg" alt="">
&lt;img src="https://blog.joway.io/images/azerbaijan/art-2.jpeg" alt="">
&lt;img src="https://blog.joway.io/images/azerbaijan/art-3.jpeg" alt="">
&lt;img src="https://blog.joway.io/images/azerbaijan/art-4.jpeg" alt="">&lt;/p>
&lt;h2 id="离开阿塞拜疆">离开阿塞拜疆&lt;/h2>
&lt;p>事实上，之所以把阿塞拜疆作为高加索三国的第一站，也是因为阿塞拜疆的特殊政治。因为阿塞拜疆对亚美尼亚有着极为强烈的仇恨，导致如果是先去了亚美尼亚，会极难入境阿塞拜疆，而亚美尼亚对阿塞拜疆的旅游经历倒不特别关心。但是这两国之间本身并不开放通行，所以为了去亚美尼亚，还得先到格鲁吉亚中转。&lt;/p>
&lt;p>即便你生性不关心政治，但是在高加索，政治才是这里最大的景观。&lt;/p></description></item><item><title>2024 投资组合年报</title><link>https://blog.joway.io/posts/2024-portfolio-annual-report/</link><pubDate>Sat, 01 Feb 2025 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/2024-portfolio-annual-report/</guid><description>&lt;p>2024 年，是我大规模重构投资组合的一年，主要还是由于在新加坡的身份转变后，具备了投资新加坡房产的资格，从而让投资结构从简单的股债配比转向了更多元复杂并且加入了房贷这一杠杆的组合。&lt;/p>
&lt;h2 id="我的投资组合计算">我的投资组合计算&lt;/h2>
&lt;p>我在思考投资组合的时候并非像部分人一样，只是把资产的一部分活钱作为投资资产然后仅计算这部分资产的涨跌。这种方式虽然计算上更加简单，但是实际上会出现股票账户翻倍，个人总资产却没啥波动的情况。所以我会把所有资产甚至是公司期权和公积金都算入投资组合中。&lt;/p>
&lt;p>我不会记录每笔交易，因为不只是投资，生活中处处存在着交易，记账不完整等于没有记账。但是我会大约每个季度花一个小时左右的时间，基于那个时间点的最终资产份额以及价格，构建一个 Snapshot ，这样我可以建立起一个「资产状态」的时间序列：&lt;/p>
&lt;p>&lt;img src="https://blog.joway.io/images/2024-portfolio/0.png" alt="">&lt;/p>
&lt;p>基于这些表之间的 Diff，我能够清楚知道任意两个时间段间，我做了哪些有实质性大变化的操作，一些小操作比如个股的小规模买入卖出则会反应在最终资金的盈亏上但是剔除交易本身的细节。&lt;/p>
&lt;h2 id="2024-的重大操作">2024 的重大操作&lt;/h2>
&lt;h3 id="更换券商">更换券商&lt;/h3>
&lt;p>由于众所周知的原因和对未来不确定风险的防御，我在 2 月从富途彻底转到了 IBKR ， 转仓方式是现在富途先全部卖出所有股票然后买入 JEPI ， 然后通过 JEPI 这一更稳定的 SPY 载体， 转仓到 IBKR。&lt;/p>
&lt;p>原本的预期是这样做，可以在 SPY 不暴涨的情况下，最小化中间时间的收益损失。且不用付高额的银行电汇手续费。不过转仓时，SPY 有了一些下跌，所以马后炮看，远不如当时直接换成现金，再在 IBKR 买入。不过好在最终损失也不大。&lt;/p>
&lt;h3 id="大规模买入美债后再卖出">大规模买入美债后再卖出&lt;/h3>
&lt;p>2024 年中的时候，10 年期美债利率已经来到了 4.9% ， 在上半年我一直在构建我的债券阶梯，具体方法是定投买入从 1 年期到 15 年期的美债，每个期限的美债份额固定。这样做就几个好处：&lt;/p>
&lt;ol>
&lt;li>我每半年都会有一大笔美债利息到账，可以继续拿来投资。&lt;/li>
&lt;li>如果我都采取持有到期的策略，我在未来 15 年每年都会有一笔美债到期，我可以根据当时的利率以及市场，决定是否继续买入新的 15 年美债，抑或是直接投资到股票市场。&lt;/li>
&lt;li>如果人生出现了变化，需要用到大笔资金，由于我是在利率非常高的时候买入的美债，且平均期限才 7.5 年，我亏本卖出债券的概率非常低，可以直接把这部分美债作为活钱来用。&lt;/li>
&lt;/ol>
&lt;p>最后的事实也证明了这个策略非常明智，我最终走到了第三步方案，在 8 月因为要准备购房资金清仓了刚建完的美债阶梯。巧合与惊喜的是，在 2024 年 9 月的时候，美债利率还不降反升了，而我的美债都是在 9 月之前卖出的，所以还躲过了一波下跌。最终以盈利几千美元结束了这笔因为生活改变引发的混乱操作。&lt;/p>
&lt;h3 id="精简持仓专注两大指数">精简持仓，专注两大指数&lt;/h3>
&lt;p>过去我虽然相信指数是穷人的最强武器，但是依然忍不住发挥自己的主观能动性在这个信念之上做一些微操。比如买入 JEPI。&lt;/p>
&lt;p>JEPI 是在 SPY 基础上执行 Covered Call 策略的类指数基金。它的好处是，在指数震荡或者下跌或者微涨的时候依然能盈利，暴涨的时候会损失超过正常范围的涨幅。所以它可以被理解是一个更为稳健的 SPY 指数。但是 2024 年是美股疯涨的一年，所以这个策略就不生效了。&lt;/p></description></item><item><title>重新思考 Go：了解程序在线上是如何运行的</title><link>https://blog.joway.io/posts/golang-rethink-program-in-realworld/</link><pubDate>Tue, 10 Dec 2024 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-rethink-program-in-realworld/</guid><description>&lt;blockquote>
&lt;p>重新思考 Go 系列：这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题，探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。&lt;/p>&lt;/blockquote>
&lt;h2 id="前言">前言&lt;/h2>
&lt;p>过去一段时间，在大量的线上服务 case study 过程中，逐步深入了解了如今的业务 Go 进程是如何在一系列繁杂的基础设施之上运行的。有些表现在意料之中，也有一些出乎意料的发现。&lt;/p>
&lt;p>我很喜欢一个叫做「 机械同理心/Mechanical Sympathy 」的概念，大体的意思是你必须深刻了解你的程序/机械装置是在一种怎么样的环境下运行的，设身处地地在这个运行环境下去思考，才能帮助你写出更好的程序，或是解答一些奇怪现象的原因。&lt;/p>
&lt;p>本文希望达到的目的，也是构建这份「机械同理心」。&lt;/p>
&lt;h2 id="程序能使用多少计算资源">程序能使用多少计算资源？&lt;/h2>
&lt;p>对于很多人来说，这个问题似乎非常简单，大多的计算平台都会要求你建立服务的时候就指定好「需要的计算资源」，但这与你的「能使用的计算资源」是两件事情。实际上这个问题的复杂性远远超出大部分人现象，甚至都不是一个常量，也无法使用公式简单计算。抽象地讲，这取决于天时地利人和。&lt;/p>
&lt;ul>
&lt;li>「天时」指的是内核 Cgroups 以及容器调度平台的基本原理和参数设置。这是硬指标，大部分时候也是常量。&lt;/li>
&lt;li>「地利」指的是部署的实际物理机的繁忙程度。&lt;/li>
&lt;li>「人和」指的是写程序的人得在能够有更多计算资源可用的时候真的让自己程序用上这些计算资源。&lt;/li>
&lt;/ul>
&lt;p>接下来我们逐步分节来详细探究这个问题。&lt;/p>
&lt;h3 id="被封装的-cpu-谎言">被封装的 CPU 谎言&lt;/h3>
&lt;p>在 Oncall 中常见的一个误解是，研发人员在容器平台上申请了 4 核 CPU 的容器，然后自然而然认为自己的程序最多只能使用 4 个 CPU，于是按照这个计算能力去估算需要的容器数量，以及对自己程序套上这个假设进行参数调优。&lt;/p>
&lt;p>上线后，进到容器用 top 一看，各项指标确实是按照 4 核的标准在进行。甚至用 &lt;code>cat /proc/cpuinfo&lt;/code> 一看，不多不少刚好看到有 4 个 CPU。。。&lt;/p>
&lt;p>但实际上，这一切都只是容器平台为你封装出来的一个美好的假象。之所以要把这个假象做的这么逼真，只是为了让你摆脱编程时的心智负担，顺便再让那些传统的 Linux 观测工具在容器环境中也能正常运行。&lt;/p>
&lt;p>但是假象终究不是真相，如果有一天你遇到一些疑难问题或是想要去编写极致性能的代码，你会发现这些封装出来的抽象把你的理解带的有多偏。&lt;/p>
&lt;h3 id="我到底能同时使用多少个-cpu-">我到底能同时使用多少个 CPU ？&lt;/h3>
&lt;p>在 K8s 的环境里，CPU 是一个时间单位而非数量。正常情况下，一个拥有 64 核心的机器，你最多能同时使用的理论 CPU 个数是 64 ，即便你创建容器的时候申请的是 4 核。&lt;/p></description></item><item><title>欧游散记 —— 环勃朗峰之旅</title><link>https://blog.joway.io/posts/tmb/</link><pubDate>Fri, 18 Oct 2024 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/tmb/</guid><description>&lt;h2 id="tmb---tour-du-mont-blanc">TMB - Tour du Mont Blanc&lt;/h2>
&lt;p>很多年前就听说勃朗峰周边有一条多日徒步线路，从霞慕尼出发，环绕勃朗峰徒步一整圈最后回到霞慕尼。但是由于很难请到这么长时间的假期，所以一直没能如愿。甚至四年前的时候还去到过霞慕尼，但是那时候只是把它作为一个景点，简单去了下南针峰从山顶观看勃朗峰。那时甚至都霞慕尼这个地方大头来头，也没来得及在当地住一晚细细品味它的户外氛围。&lt;/p>
&lt;p>今年十月，终于得愿完成了这条此生必去的徒步线路。出于安全和方便的考虑，我在网上订了一个包含了住宿餐食和山地向导的国人徒步团，基本把除了走路之外的麻烦事都帮我解决了，每天就是到点徒步，到站睡觉。唯一的遗憾是，这次走的是所谓的精华路线而非完整路线，大概只覆盖了完整线路的一半左右。希望未来有一天可以以个人的形式重新完整走一次这条线路。&lt;/p>
&lt;p>&lt;img src="../../images/tmb/map.jpeg" alt="">&lt;/p>
&lt;h2 id="行前准备">行前准备&lt;/h2>
&lt;h3 id="身体准备">身体准备&lt;/h3>
&lt;p>在 2024 年前半年的时间里，颈椎病让我的身体状态特别糟糕，别说徒步，感觉长时间的伏案工作都已经很成问题。所以我上半年的大部分业余时间都花在了锻炼身体和用各种手段治疗颈椎病上。这也让我的身体素质得以迅速提升，刚好在九月已经恢复到几年前的水平。&lt;/p>
&lt;p>由于长期居住在新加坡，所以很难找到一些较大强度的多日运动能够来测试我的身体状态，这次，我也是拿 TMB 来为我半年的身体训练做一次压力测试。说实话，在去之前，我是做好了不行就撤退的准备。所幸的是，我不仅没有撤退，甚至走的还算是在队伍的前列，这可能是整趟旅程最让我高兴的事情。&lt;/p>
&lt;h3 id="装备准备">装备准备&lt;/h3>
&lt;p>十月 TMB 的气温大概在 5 度到 20 度左右，我最后真正用到的装备有：&lt;/p>
&lt;ul>
&lt;li>防护层
&lt;ul>
&lt;li>1 件 Patagonia Boulder Fork Rain Jacket&lt;/li>
&lt;li>1 条 Outdoor Research Ferrosi Convertible Pants&lt;/li>
&lt;li>1 条 KAILAS 徒步裤&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>中间层
&lt;ul>
&lt;li>1 件 Patagonia Down Sweater&lt;/li>
&lt;li>1 件 Patagonia R1&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>贴身层
&lt;ul>
&lt;li>1 件 Decathlon 美丽奴羊毛 T 恤&lt;/li>
&lt;li>1 件 Smartwool 美利奴羊毛衣&lt;/li>
&lt;li>1 条 Icebreaker 美利奴羊毛裤&lt;/li>
&lt;li>2 双 Smartwool Full Cushion 美利奴羊毛袜&lt;/li>
&lt;li>4 条 Smartwool 美利奴羊毛内裤&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>徒步装备
&lt;ul>
&lt;li>Hanchor MARL X 轻量化背包&lt;/li>
&lt;li>Salomon X Ultra 4 Mid GTX 徒步鞋&lt;/li>
&lt;li>Black Diamond Distance Carbon Z 登山杖&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>其中，Patagonia 的 Boulder Fork Rain Jacket 配合 Hanchor MARL X 防雨背包，绝对是提升我整段徒步旅程体验最大的装备。在风雨雪交加的情况下，我没有一次用到雨衣和背包防雨罩。在降水量最大的一天，也只是导致了我中间层羽绒个别地方略微湿润，背包里非常潮湿，但是都没有明显进水的迹象。考虑到我非常讨厌行动受限的感觉，这两款装备的互相配合基本能让我在中等程度降雨量的情况下，都维持自由行走的状态。&lt;/p></description></item><item><title>欧游散记 —— 特摩索斯古城</title><link>https://blog.joway.io/posts/turkey-termessos/</link><pubDate>Sun, 07 Apr 2024 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/turkey-termessos/</guid><description>&lt;p>特摩索斯（Termessos）位于土耳其南部城市安塔利亚的北面大约 30 公里，是一座起源神秘，消亡也神秘的深山古城。它第一次被历史提到是在公元前 333 年亚历山大大帝包围这座城市时，但即便是征服了希腊世界的亚历山大大帝也并未能成功征服特摩索斯。&lt;/p>
&lt;p>特摩索斯建于海拔 1000 多米的山口位置，由于特殊的地理位置，加上完善的水利设施，所以即便被包围也只需要一小只军队便可守卫，这帮助这座城市在那个群雄逐鹿的希腊化时期和罗马帝国时期得以一直以独立城邦的状态存活下来。一直到公元 5 世纪的一场地震摧毁了这座城市的蓄水库，导致居民不得已陆续搬迁，最终荒废并被人们所遗忘。一直到 19 世纪，欧洲的探险家才重新发现了这座城市，但即便到今天，特摩索斯一直是一个非常冷门的旅行目的地。&lt;/p>
&lt;p>我知道特摩索斯，是机缘巧合在 YouTube 搜索安塔利亚的视频时，偶然间看到了一个特摩索斯徒步视频，即便画质极其粗糙，依然还是被这座山顶古城给震撼到了。更因此将安塔利亚作为了土耳其旅行的其中一站。&lt;/p>
&lt;p>到达安塔利亚，我们在 Airbnb 上约了一个本地向导 Onder 带我们开车到达北部的居呂克山-特摩索斯国家公园，从国家公园徒步前往特摩索斯所在的山顶。Onder 的本职工作是老师，但十分热爱特摩索斯甚至他的硕士论文写的课题便是特摩索斯，所以业余时间也通过做特摩索斯的向导赚点外快。&lt;/p>
&lt;p>&lt;img src="../../images/turkey/termessos/start.jpeg" alt="">&lt;/p>
&lt;p>虽然特摩索斯的城市部分主要集中在半山腰和山顶，但是山底相当于这座城市的郊区，和现代城市的郊区一样，往往是平民的生活聚居地。&lt;/p>
&lt;p>首先看到的是城市的宗教区域，哈德良庙，但如今只剩下了一扇门孤零零地在山脚下伫立，既神圣又凄凉：&lt;/p>
&lt;p>&lt;img src="../../images/turkey/termessos/temple_of_hadrian.jpeg" alt="">&lt;/p>
&lt;p>在哈德良庙的另一边，是居民的墓地区。特摩索斯的墓十分有特色，都是以露天石棺的形式“展览”在地表之上。石棺的雕刻也别有一番讲究。&lt;/p>
&lt;p>普通平民的石棺是以两个太阳加中间方块的形式，太阳是特摩索斯的标志，这里也被称之为太阳城：&lt;/p>
&lt;p>&lt;img src="../../images/turkey/termessos/coffin-people.jpeg" alt="">&lt;/p>
&lt;p>而士兵的石棺会在太阳上增加武器的标识，寓意太阳的捍卫者：&lt;/p>
&lt;p>&lt;img src="../../images/turkey/termessos/coffin-soldier.jpeg" alt="">&lt;/p>
&lt;p>无论是平民还是士兵的石棺都略显单调和同质，特别是人死后，棺材还会被摆在地表被长久展示，这导致石棺本身变成了一个人生命价值的化身。如果没有钱请不起好的雕刻工匠，就靠战斗获得荣誉来装点自己的石棺。如果有钱，就极尽工匠精湛的工艺让自己的石棺变成华丽的艺术品：&lt;/p>
&lt;p>&lt;img src="../../images/turkey/termessos/coffin-rich.jpeg" alt="">
&lt;img src="../../images/turkey/termessos/coffin-rich-side.jpeg" alt="">&lt;/p>
&lt;p>又或者是更为有权势的领袖，可以直接在岩壁开凿更为壮阔的仰望式墓藏：&lt;/p>
&lt;p>&lt;img src="../../images/turkey/termessos/coffin-alcetas.jpeg" alt="">&lt;/p>
&lt;p>考虑到两千年后，我们还依然在为他们的石棺赞叹，当初他们的「虚荣」如今也被岁月洗涤成了「实荣」。&lt;/p>
&lt;p>沿着山路，陆陆续续会走过众多城墙和古罗马式引水渠，以及如今已不知为何物的废墟：&lt;/p>
&lt;p>&lt;img src="../../images/turkey/termessos/ruins1.jpeg" alt="">
&lt;img src="../../images/turkey/termessos/wall1.jpeg" alt="">
&lt;img src="../../images/turkey/termessos/wall2.jpeg" alt="">
&lt;img src="../../images/turkey/termessos/underground.jpeg" alt="">&lt;/p>
&lt;p>在半山腰，还能看到一个保存地非常好的体育馆遗址：&lt;/p>
&lt;p>&lt;img src="../../images/turkey/termessos/gym.jpeg" alt="">&lt;/p>
&lt;p>以及曾经的祭坛：&lt;/p>
&lt;p>&lt;img src="../../images/turkey/termessos/heroon.jpeg" alt="">&lt;/p>
&lt;p>接着便来到了特摩索斯城市最最核心的基础设施 —— 蓄水库：&lt;/p>
&lt;p>&lt;img src="../../images/turkey/termessos/water_saver1.jpeg" alt="">
&lt;img src="../../images/turkey/termessos/water_saver2.jpeg" alt="">&lt;/p>
&lt;p>由于地震已经毁坏了蓄水库很大一部分，所以今天的蓄水库为了维持稳定，能够看到有很多人工固定的痕迹，但是能够两千年前在一个山顶开凿一个如此巨大的地下空间还是非常令人震撼的。&lt;/p>
&lt;p>大约花了一小时的路程，便可抵达特摩索斯的山顶，在一个转身间，看到了我在土耳其见过的最震撼的一幕：&lt;/p>
&lt;p>&lt;img src="../../images/turkey/termessos/theater1.jpeg" alt="">
&lt;img src="../../images/turkey/termessos/theater2.jpeg" alt="">
&lt;img src="../../images/turkey/termessos/theater3.jpeg" alt="">
&lt;img src="../../images/turkey/termessos/theater_wide.jpeg" alt="">&lt;/p>
&lt;p>特摩索斯在人类历史上，只是一个非常短暂，也没有名气，更没有对任何历史节点产生重要影响的小城市。特摩索斯的统治者也并非历史上赫赫有名的王侯将相，甚至这座城市更像是纯粹的自治军事城邦而非君主制的王国。但是如此小的城市居然会为了其居民建立如此恢弘的一个剧场。更何况这里不靠近雅典也不靠近罗马，地处希腊世界的边陲。&lt;/p>
&lt;p>站在特摩索斯的山顶上，我眼望着希腊文明的「边际产物」，如同看到了一个外星文明一般陌生。从山脚下看到的自我价值实现和山顶上对自然与人类文明极致的审美表达。特摩索斯的居民在群雄逐鹿中维持了独立，依靠工程学知识建立起了少见的防卫居住一体的水利设施，甚至还在这种山城里建立起了不输其他同等规模但地处丰饶平地的希腊城邦的剧院。&lt;/p>
&lt;p>我在之后的土耳其境内的希腊化古城邦旅途中，也遇到了更多比特摩索斯大得多，恢弘得多的城邦，但是特摩索斯一直是最特别的那个，也是我记忆最深的一个。以特摩索斯作为我在古希腊-罗马世界的游历起点，很难想到比这更好的开始了。&lt;/p></description></item><item><title>重新思考 Go：Channel 不是「消息队列」</title><link>https://blog.joway.io/posts/golang-rethink-channel/</link><pubDate>Sun, 31 Mar 2024 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-rethink-channel/</guid><description>&lt;blockquote>
&lt;p>重新思考 Go 系列：这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题，探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;p>Go 语言是一门为实现 CSP 并发模型而设计的语言，这也是它区别于其他语言最大的特色。而为了实现这一点，Go 在语法上就内置了 &lt;code>chan&lt;/code> 的数据结构来作为不同协程间通信的载体。&lt;/p>
&lt;p>Go 的 channel 提供 input 和 output 两种操作语法。input 一个已经 full 的 channel ，或是 output 一个 empty 的 channel 都会引发整个协程的阻塞。这个协程阻塞性能代价很低，也是协程让渡执行权的主要方法。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">ch&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">chan&lt;/span> &lt;span class="kt">int&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1024&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// input&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">ch&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// output&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">&amp;lt;-&lt;/span>&lt;span class="nx">ch&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>然而 channel 的实现恰好和进程内消息队列的大部分需求是吻合的，所以这个结构时常被用来作为生产者消费者模型的实现，甚至还作为 channel 的主流应用场景而推广。&lt;/p>
&lt;p>但事实上，如果真的把该数据结构用来作为系统内核心链路的生产消费者模型底层实现，一不留神就会遇到雪崩级别的问题，且这些问题都不是简单的代码修改便能解决的。&lt;/p>
&lt;h3 id="input-失败导致阻塞">Input 失败导致阻塞&lt;/h3>
&lt;p>当 channel 满的时候，&lt;code>&amp;lt;-&lt;/code> 操作会导致整个 goroutine 阻塞。显然这并不总是编程者希望的，所以 Go 提供了 select case 的方法来判断 &lt;code>&amp;lt;-&lt;/code> 是否成功：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="k">select&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">case&lt;/span> &lt;span class="nx">ch&lt;/span> &lt;span class="o">&amp;lt;-&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">default&lt;/span>&lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// input failed&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>但问题是，当 channel input 失败时，编程者还能怎么做？除非队列的消息是可以被丢弃的，否则我们可能只再去创建一个类似 queue 的结构，将这部分消息缓存下来。但是这个 queue 的结构可能又要和这个 ch 本身的队列顺序处理好并发关系。&lt;/p></description></item><item><title>重新思考 Go：Slice 只是「操作视图」</title><link>https://blog.joway.io/posts/golang-rethink-slice/</link><pubDate>Sat, 30 Mar 2024 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-rethink-slice/</guid><description>&lt;blockquote>
&lt;p>重新思考 Go 系列：这个系列希望结合工作中在 Go 编程与性能优化中遇到过的问题，探讨 Go 在语言哲学、底层实现和现实需求三者之间关系与矛盾。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;p>Go 在语法级别上提供了 Slice 类型作为对底层内存的一个「&lt;strong>操作视图&lt;/strong>」:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">sh&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">any&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// ==&amp;gt; internal struct of []any&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">SliceHeader&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Data&lt;/span> &lt;span class="kt">uintptr&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Len&lt;/span> &lt;span class="kt">int&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Cap&lt;/span> &lt;span class="kt">int&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>编程者可以使用一些近似 Python 的语法来表达对底层内存边界的控制:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">buf&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="kt">byte&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1000&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">tmp&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">buf&lt;/span>&lt;span class="p">[:&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1">// {Len=100, Cap=1000}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">tmp&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">buf&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">:]&lt;/span> &lt;span class="c1">// {Len=900, Cap=900}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">tmp&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">buf&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">200&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">200&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1">// {Len=100, Cap=100}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>虽然 Slice 的语法看似简单，但编程者需要时刻记住一点就是 &lt;strong>Slice 只是一个对底层内存的「&lt;strong>操作视图&lt;/strong>」，而非底层「内存表示」，Slice 的各种语法本身并不改变底层内存&lt;/strong>。绝大部分 Slice 有关的编程陷阱根源就在于两者的差异。&lt;/p>
&lt;h3 id="slice-陷阱持有内存被放大">Slice 陷阱：持有内存被「放大」&lt;/h3>
&lt;p>以最简单的从连接中读取一段数据为例，由于我们事先并不知道将会读取到多少数据，所以会预先创建 1024 字节的 buffer ，然而如果此时我们只读取到了 n bytes, n 远小于 1024，并返回了一个 &lt;code>len=n&lt;/code> 的 slice，此时这个 slice 的真实内存大小依然是 1024。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">Read&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">conn&lt;/span> &lt;span class="nx">net&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Conn&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">[]&lt;/span>&lt;span class="kt">byte&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">buf&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">make&lt;/span>&lt;span class="p">([]&lt;/span>&lt;span class="kt">byte&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1024&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">n&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">_&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">conn&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Read&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">buf&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">buf&lt;/span>&lt;span class="p">[:&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>即便上一步我们内存放大的问题并不严重，比如我们的 n 恰好就是 1024。但我们依然会需要对连接读到的数据做一些简单的处理，例如我们现在需要通过 Go 的 regexp 库查询一段 email 的数据：&lt;/p></description></item><item><title>那一天，我决定踏出一步</title><link>https://blog.joway.io/posts/run-away-from-shanghai/</link><pubDate>Tue, 10 May 2022 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/run-away-from-shanghai/</guid><description>&lt;p>四月与五月之交，我完成了人生中迄今为止最惊心动魄的一次冒险。与通常的冒险经历不同的是，一般的冒险人们总愿意在往后的岁月里反复回忆甚至加工，但我的这次冒险我希望此生永远的忘记，但我又明白，之所以我如此迫切地想要忘记，是因为这段经历将会不可逆地影响我一生。&lt;/p>
&lt;p>过去的那个少年已经被这场冒险所杀死，我只是作为一个旁观者，在叙述一个已经去世了的人在那几天的经历。&lt;/p>
&lt;p>这场冒险的源头还要从 3 月开始说起。&lt;/p>
&lt;p>3 月初，我在北京出差，在返回上海的前两天，当时因为北京有人在乌克兰大使馆门口献花，导致三里屯的使馆区莫名其妙被政府封了。我和朋友那天晚上临时起意，打算去现场看看，于是就绕着乌克兰大使馆走出了一个方形的圈。这件事情与后面发生的事情并没有什么直接的联系，但它确是我在中国境内拥有的最后一次自由行走的经历，而我在三里屯画下的这个圈，也成为了后面发生的事情的一个隐喻。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/run/ukraine.png?tr=w-1024" alt="">&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/run/sanlitun.png?tr=w-1024" alt="">&lt;/p>
&lt;p>2022 年 3 月 4 日，我从北京回到上海，而这一天北京办公室却检测出了一例阳性，导致我回到上海后被判定为所谓的「高危筛查人员」，于是就开始了原定于 14 天的居家隔离。然而从 3 月 16 号开始，整个打浦桥街道开始出现了非常多核酸异常的情况，我小区在内的相当一部分小区变成了所谓的封闭管理。每天都需要做一次核酸，但是小区里的居民此时并不知道问题的严重性，甚至我怀疑连居委会都不见得知道真实的核酸异常数据，所以在做核酸时，大家有说有笑，许多老头老太甚至在排队时都不戴口罩。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/run/notice.png?tr=w-1024" alt="">&lt;/p>
&lt;p>当时所有人的预期是，所谓的 2+12 封闭管理就是只需要小区被封闭两天而已。这对于年轻人来说，可能平常出小区的频率也就两天一次，所以几乎没有任何影响。并且此时我们依然可以在小区内自由活动，去门口取外卖。&lt;/p>
&lt;p>3 月 18 号，这是原定小区解除封闭管理的一天，而我个人也早已满足了 14 天的居家隔离要求。那天中午，我兴冲冲地跑下楼，想要去商场里面吃一顿好的，结果发现小区并没有如期的解封，反而是又贴了一份继续 +2 的通知。此时我的愤怒在于它打破了我们原先的预期，导致原先的欢喜落空，但我依然相信只要再坚持两天就能解封。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/run/xiaoqu1.png?tr=w-1024" alt="">&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/run/xiaoqu2.png?tr=w-1024" alt="">&lt;/p>
&lt;p>3 月 20 号的时候，我楼道门突然在毫无事先通知的情况下被一道铁链锁住。试图去推开一道推不开的铁门，这种屈辱感和无力感永生难忘。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/run/chain.png?tr=w-1024" alt="">&lt;/p>
&lt;p>20 日晚上等了 2 个小时，才吃到已经在外面冻冰了的鳗鱼饭，21 日中午又等了 2 个小时拿不到外卖然后吃的泡面的时候，而当天晚上彻底外卖就没送过来了。那时候才第一次知道原来饿久了，眼睛会花手会抖。不知道明天会发生什么，但当天晚上我就把恒生指数的基金清仓了。&lt;/p>
&lt;p>23 日已经彻底放弃了外卖这件事情，中午八宝粥，晚上吃泡面加饼干。接下来的每一天都接近于此。点外卖几乎只能靠金钱加上运气，偶尔会有一些商贩“违规”在外卖平台上上线一些熟食之类的东西，那时候偶尔才能够有一点加餐。甚至有一次还抢到了能够让我吃上两天的两桶麦当劳全家桶。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/run/home1.png?tr=w-1024" alt="">&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/run/home2.png?tr=w-1024" alt="">&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/run/homeview.png?tr=w-1024" alt="">&lt;/p>
&lt;p>再接着就是大家所熟知的 4 月开始上海进入事实上的全面封城状态。&lt;/p>
&lt;p>我所在的黄浦区，在整个上海属于重灾区，而我的小区又是黄浦区的重灾区，我所在的楼栋又属于小区里的重灾区，先于上海绝大部分小区率先封闭管理。上海发布每过几天通报一次我楼栋有了新的阳性。截止我离开的时候，楼栋里阳性户数至少也有 30%，悲观估计可能多达 50%。&lt;/p>
&lt;p>4 月 18 号的时候，我明白自己不可能继续这种生活，开始规划逃离上海的行程，于是给居委打了电话咨询了开出门证的要求，让我意外的是，虽然我的楼栋形势严峻，但是给我的要求却还算合理（当然事实证明我天真了）：&lt;/p>
&lt;ul>
&lt;li>楼栋 7 天无阳性&lt;/li>
&lt;li>48 小时核酸阴性&lt;/li>
&lt;li>全程闭环车运输&lt;/li>
&lt;li>前序航班到达&lt;/li>
&lt;/ul>
&lt;p>其中其他几项都可以通过花钱解决，唯独所在楼栋七天无阳性这一项对我来说充满了不确定性。对于在上海的绝大部分人来说，这个要求确实并不过分，也很容易达到。但对于我这栋重灾楼来说，就需要时刻算着一个 7 天的窗口期，到了窗口期立马走。&lt;/p></description></item><item><title>True Story</title><link>https://blog.joway.io/posts/true-story/</link><pubDate>Thu, 28 Apr 2022 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/true-story/</guid><description>&lt;p>2015 年 9 月 2 日，一位名叫艾兰的叙利亚难民儿童尸体在土耳其的海滩上被发现，并被拍下了一张影响深远的照片。&lt;/p>
&lt;p>&lt;img src="https://blog.joway.io/images/alan.jpeg" alt="">&lt;/p>
&lt;p>如果你在网络上尤其是中文网络上查阅这张照片的新闻时，会发现有相当一部分人在拿出各种“证据”试图证明它是摆拍的。&lt;/p>
&lt;p>这张照片之所以牵动人心是因为它叙述了一个真实存在的现象是，叙利亚儿童正在因为逃难而失去生命。这个现象是真实存在的，你在这个海滩上站到深夜就能看到这个客观事实，但是具体到这个照片与这个事实是否是百分百匹配的，确实有很多可被质疑的空间，例如这个儿童有可能来自伊拉克，例如摄影师有可能把他换了一个姿势来拍摄。但是这些真的重要吗？或许对于历史学家来说，的确重要，但是对于一个普通民众来说，或许并没有那么重要，至少在此时此刻有比质疑更加重要的事情。&lt;/p>
&lt;p>什么是事实，什么是真实？真实并不代表每一件事情的细节都是毫厘不差的事实，这就好比父母口中关于孩子的童年故事多少会有些添油加醋，锦上添花的成分在，但这并不能否定这些故事是真实的。&lt;/p>
&lt;p>相反，由事实组成的故事也不见得一定是真实的故事。在叙利亚战争期间，一定找得到吃的好睡得好的地区，但如果一个驻叙利亚记者去这些地区报道来反映叙利亚战争期间人民的生活，这一定不是真实的报道。&lt;/p>
&lt;p>人类的故事要能够被传播，或多或少总会被掺入一些夸大的成分，即便最开始的当事人只是客观陈述，也防不住他人在传播的过程中进行二次创造，最后舆论市场会自发地选择出一个兼顾了真实与传染力的版本成为我们今天耳熟能详的故事。&lt;/p>
&lt;p>对于那些想要左右人民情感，篡改人民记忆的人来说，事实是他们攻击一切故事的强大武器。民众无法凭借一己之力去探寻真正的事实，只要少数人掌握了调查事实的权利，就掌握了提供唯一合法叙事的权利。他们用事实消灭一切他们所不愿意看到的故事，让这个世界只留下那些由他们控制的事实所构建出来的官方叙事。在这个叙事里，既没有人民的情感，也没有人民的记忆，只有一种强大到淹没所有人的意志。&lt;/p>
&lt;p>这套叙事方式在过去被用在了欧洲难民危机中，用在了香港运动中，用在了美国大选中，用在了上海疫情中。它的强大之处在于，身处其中的人会发现自己无法与这个叙事机器所搏斗，因为它所描述的确实是事实，确实是我们所相信的故事的弱点，确实是我们作为一个平凡个体的缺陷。我们无法让自己，让自己所团结的群体，永远地做到客观，做到公正，做到实事求是，因为我们是人而非机器，而那些试图否定我们的人，只要做到一次客观，做到一次公正，做到一次实事求是，就认为可以全盘否定我们所实现过的一切努力。&lt;/p>
&lt;p>我们身处于一个混乱的世界，Fake News 与 True Story 同时存在，True News 与 Fake Story 同时存在。孟姜女或许并没有哭长城，摩西或许并不能劈开红海。但又或许所有我们所相信的 True Story 并不是由事实产生的结果，而是因为有千千万万痛苦的民众，希望长城倒塌，希望抵达应许之地，所以才诞生的孟姜女，诞生的摩西，他们也许都不是事实，但他们远比事实更为强大。&lt;/p></description></item><item><title>少数价值</title><link>https://blog.joway.io/posts/minority-values/</link><pubDate>Tue, 19 Apr 2022 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/minority-values/</guid><description>&lt;p>如果正在阅读这篇文章的你认为自己的价值观在这片土地上属于多数价值，被这块土地充分地实现，希望你不要继续阅读下去，也不要将它传播给任何你的朋友。我尊重你的价值观，但恐怕你不一定会尊重我的，为了你好也为了我好，请不要与我有任何交集。&lt;/p>
&lt;p>我并不认为自己的价值观一定正确，但是我的价值观，是通过一点一滴的阅读，通过与不同人群的交流，亲眼所见亲耳所听构建出来的。我对它的自信源自于它一次次在苏格拉底，在耶稣，在马丁路德金，在苏轼，在鲁迅，在我身边所有优秀的朋友身上一次次被验证被歌颂，它不见得一定是全人类共同拥有和应该拥有的，但它表达了那部分我愿意与其共处的人类千百年来共同的价值诉求。&lt;/p>
&lt;p>我 1995 年出生，所经历的教育完全是经过中国人民共和国教育部批准，合法合规，符合社会主义价值观的中国式教育，在我整段学生生涯中，我一直认为我的价值观是这个社会里的主流价值观，也是这个教育系统希望我拥有的价值观。在我受教育的年代，我们曾经和国家共同相信民主是未来的发展方向，共同相信侵略是反人类行为，共同相信全球化是历史的潮流，共同相信恐怖主义是要被谴责的行为，共同相信市场经济带给了中国空前的繁荣。当时的我们虽不认为我们国家已经实现了这些价值，但是绝大部分人包括国家机器都认同我们确实享有这样的价值观，而争歧无非是如何实现这些价值观，何时应当去按这些价值观践行。&lt;/p>
&lt;p>但是不知道从什么时候开始，这些原本我们认为的多数价值，渐渐地变成了社会中的少数价值。虽然我们在社会主义核心价值观中明确写了民主与自由，但却在每天批判西方的民主与西方的自由。虽然我们把八年抗日战争延长到了十四年，但却破天荒地认为只要从自身利益出发，哪怕是侵略也是可以被合法化甚至共情的。虽然我们今天的经济成就完全仰赖全球化与市场经济，却可以每天大肆与几乎所有西方国家对骂并强力干预市场经济。&lt;/p>
&lt;p>这些变化并不是由某一个人，或者某八千万人造成的，而是在十四亿人包括我自己共同的努力下，车轮渐渐驶到了如今这步田地。&lt;/p>
&lt;p>我无法解释为什么我们会走到今天，但我确信其中肯定有我自己的一份力。我曾经认为自我实现在于努力赚钱买房成为大城市中产阶级，我曾经认为只要经济持续前进上层建筑会自然而然变好，我曾经认为多元化的世界确实应该对一些共同价值的定义有各自表述的权力，我曾经认为下一代人会比我们更加推崇自由与民主看到更大更不一样的世界，我曾经认为一个勤劳的民族辅之以不断修正的制度迟早会重回当年历史上的地位。但是直到走到今天这一步，我才知道自己错了，大错特错。&lt;/p>
&lt;p>比认识到错误更加悲哀的是，这段变化正是发生在我的青年时期，发生在我最应该意识到问题，最应该去解决问题至少把问题说出来的年纪。让悲哀更加悲哀的是，我同与我共享相同价值观的人，是亲眼目睹一个个违背我们价值观的事件日复一日发生，而我们选择了冷眼旁观或是热眼嘲笑。直到今天，我们的价值观甚至成为了不可言说的价值观。&lt;/p>
&lt;p>我不再愿意和任何人辩论，甚至不再愿意接受不同的观点。如果苏格拉底是错的，如果鲁迅是错的，如果启蒙运动是错的，如果独立宣言是错的，那我愿意带着这些古老的错误直到死去，也不愿意再去用新的理论，新的叙事，重新学习新时代的多数价值。如果他们是对的，如果他们会笼罩我一生，我也认了，我宁愿一生成为一个碌碌无为的错误，也不愿再推翻我过去的信仰成为一个前途光明的正确。&lt;/p>
&lt;p>我失去了语言，也失去了使用语言的勇气，只能在评论区给我的同类点赞，在黑色幽默里寻找光明的踪迹，在一篇篇即将被删除的文章里获取慰藉。赛博空间里充斥着正确，让错误无处可逃，只有远处时不时传来的一声声尖叫。&lt;/p></description></item><item><title>A Snapshot of Myself - 2021</title><link>https://blog.joway.io/posts/2021/</link><pubDate>Fri, 31 Dec 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/2021/</guid><description>&lt;p>过去并没有写年终总结的习惯，但是如今也正式奔三了，记忆力一年不如一年，时常忘记自己过去做过什么，有过什么想法。前段时间和很久不见的老朋友见面，意外地提起我 N 年前说过的一句话，而我却完全不记得自己当时竟有过这样的想法。恍忽间才意识到，即便彼时彼刻的我已然在我自己心中消失殆尽，却意外地变成了一段段碎片，分散存储在过去朋友的记忆中，成为一个个 Snapshots，成为了彼时彼刻的那个我存在过的证据。&lt;/p>
&lt;p>我觉得人是一个动态的过程，过往人们在评价他人亦或是自己时常常基于某一个固定时刻下的最终状态，即所谓的盖棺定论，而忽视了整段路程中状态的变更，我觉得其实蛮可惜的。就好似一个股票，你不能只看价格不看曲线。所以我觉得与其利用年终的契机总结一年内静态的收获，不如每一年对自己当下的状态做一个主动式和集中化的 Snapshot，以便未来能够追溯自己的变更历史，更好理解自己是谁，自己又是过谁。&lt;/p>
&lt;p>如果在当下要选择一个关键词来定义我此刻的状态，我会选择用「理解」。从大学到工作这段旅程对我而言是一个从无知到略有所闻的过程，我所获得的积累还远达不到能够对什么事情去下判断的程度，所以只能去先假设这个世界是合理的，然后尝试去理解其合理性。并且时刻保持着接受自己理解错误的开放心态。&lt;/p>
&lt;h2 id="理解周期">理解周期&lt;/h2>
&lt;p>今年是我大学毕业的第四年，加上正式和非正式的两年实习，真正参与到与现实世界的交互也有6年了。很幸运的是，在整个移动互联网浪潮中，我多少也算是参与了大半程。&lt;/p>
&lt;p>我现在还记得大学时候那种热火朝天的创业气氛，几乎每个月都有不同城市甚至公司在举办 Hackathon，而且都提供不菲的奖金。各路 App 百花齐放，而且各自都能融到不少钱，甚至会觉得所有金钱、政策都在求着你创业。到我毕业的 18 年时，从 0 开始创业的少了许多，但是加入一个已经初有成色的创业公司还是一个流行的风气，而且似乎在经济上也不见得会有什么损失甚至大家都幻想着获得超额回报。而到了 19-20 年，开始有了一种车门已被焊死的感觉，整个移动互联网的坑位也都被占满且很难也不太有资金支持你去产生什么变化。这时候才会意识到，好像没上到这班车。而且由于上市潮，这个时候加入创业公司和既成的大公司之间的经济收益差距也开始被拉开了。&lt;/p>
&lt;p>之前和同事聊的时候谈到，大家都没有意识到这个过程会如此之快，几乎只覆盖了一个工程师的一到两次跳槽周期而已。&lt;/p>
&lt;p>虽然没有在这个周期上获得什么经济上大收益，但是能够在毕业时就经历一个完整的周期也算是为参与下一个周期积累了一些筹码。不至于在周期到来时过度喜悦，也不至于在周期消逝时过度悲观。&lt;/p>
&lt;h2 id="理解投资">理解投资&lt;/h2>
&lt;p>过去几年系统性地学习了经济学基础原理，并且做了一些理财投资的实践。前段时间在梳理资产清单的时候发现一个问题，我持有的资产其实是很难计算成本和收益的。&lt;/p>
&lt;p>例如我分三次买入三股 Apple 股票，假设每次买入价格都是一样的，但是时间不同所以美元兑人民币汇率不同，分别为 6，7，8。如果我以美元计价，那么我成本价就没变，以人民币计价成本价就变化了。由于我工资收入是人民币，所以我从切身感受来说，当然是觉得持有 Apple 股票的成本变高了。因为要需要付出更多劳动时间才能换取一股 Apple 股票。但是如果此时我涨工资了，我不可能去认为是 Apple 股票跌了，但是事实上我的确可以用更少劳动时间换取了对应资产了。&lt;/p>
&lt;p>所以归根结底，这个游戏你如果要讨论赚了还是亏了，你必须锚定一个最终定价物，否则这就没法讨论。而人类只有一个共同的定价物就是自己的时间。&lt;/p>
&lt;p>我所理解的投资就是付出自己的时间去换取某些资产，让这些资产产生超额价值，以便未来有一天我能够用他们来购买我自己的自由时间，部分再用来提升生活的物质品质。&lt;/p>
&lt;p>但这里有一个道德问题我至今还未想明白，这种行为是不是本质上就是在剥削他人，除非这部份剥削未来能够完全被转移到机器上。&lt;/p>
&lt;h2 id="理解复利">理解复利&lt;/h2>
&lt;p>前段时间思考过买车这种大件，由此引申出一个疑问，如果我在当下花了 20 万买车，我是真的只花了 20 万吗？如果我把这笔钱投入 10% 年化收益的基金，那么 10 年后就是 50 万，所以我现在是花了 10 年后的 50 万买了一辆车吗？如果再把时间拉长，就会发现这里涉及到一个问题，我该如何在整个人生维度去分配消费，毕竟我不希望带着金钱入土，但是我的人生长度却又是我自己无法知晓和把握的。所以复利在引入了时间后，收益和风险并存。而金钱的消费其实是在实现收益以降低长期的风险。&lt;/p>
&lt;p>上述只是单纯从金钱角度去理解的复利，另外我还发现知识也是有复利的。如果某个领域了解地足够多以后，后面学习新的知识的效率也会递增，甚至跨领域间的知识还能有互相促进的作用，有点像是细胞的裂变。&lt;/p>
&lt;h2 id="理解自我">理解自我&lt;/h2>
&lt;p>理解自己不是一件容易的事情。&lt;/p>
&lt;p>首先在获取关于自我的信息上就很困难，最易得的信息是他人的评价，但这里可能有一部分来源于恶意，即便是善意的部分，也未必是经过深思熟虑的。在公司内被 Peer Review 的时候经常会收到一些别人认为的我的优点和缺点，但有些人给我写的缺点其实就是优点的负面表述形式。所谓的成长，很多时候就是把优点和缺点同时磨平，然后回顾的时候只说我改正了什么缺点却忽视了这个「改正」的成本。而要让自己能够客观反映出自身的存在又是一件充满了悖论的事情 —— 客观的名词解释指的就是不依赖主观意识而存在。&lt;/p>
&lt;p>其次在对现有信息去分析的时候又是一件充满痛苦，甚至会觉得难为情的事情。我时常发现原来自己比想象中更加爱钱，更加卑鄙，更加不诚信。而一想到在他人心中或许我并不是这样，或者我曾经营造过不是这样的人设，这就让我有点不好意思了。&lt;/p>
&lt;p>但如果退一步，把理解自我的过程当作是理解人类，心情或许会变得好受许多。这个过程也有助于同理心的建立。&lt;/p>
&lt;p>我现在几乎完全能够用同理心去理解王力宏，吴亦凡，甚至希特勒这些人的内心感受，理解并不意味着认同，但是而如果不理解一定很难真正做出有价值的反对。这就像是修电脑，不理解电脑是如何运作的就不可能知道电脑坏在了哪里，甚至有时候也不见得真的是电脑坏了。&lt;/p>
&lt;h2 id="理解选择">理解选择&lt;/h2>
&lt;p>现在回想起来我最早做职业选择的时候都觉得有点幼稚的可笑，我甚至会把老板的政治立场作为选择一份工程师职业的权重因素 —— 仅仅就因为我不希望和老板聊不来。直到走了一些弯路后我才开始理解，选择之所以叫做选择，就是因为不能对一个单一的选择抱有过多不切实际的期望。之前有看过一个关于解释工作意义的图：&lt;/p>
&lt;p>&lt;img src="https://blog.joway.io/images/work.png" alt="">&lt;/p>
&lt;p>但这个图没有说明的是，其实人并不是同时只能拥有一份「工作」。事实上完全可以通过多个选择相组合来达到一个多样化的期望，每一个「选择」只负责并负责好个别期望，这有点像是 Unix 哲学。&lt;/p></description></item><item><title>RPC 漫谈： 连接问题</title><link>https://blog.joway.io/posts/deep-into-rpc-connection/</link><pubDate>Thu, 06 May 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deep-into-rpc-connection/</guid><description>&lt;h2 id="什么是连接">什么是连接&lt;/h2>
&lt;p>在物理世界并不存在连接这么一说，数据转换为光/电信号后，从一台机器发往另一台机器，中间设备通过信号解析出目的信息来确定如何转发包。我们日常所谓的「连接」纯粹是一个人为抽象的概念，目的是将传输进来的无状态数据通过某个固定字段作为标识，分类为不同有状态会话，从而方便在传输层去实现一些依赖状态的事情。&lt;/p>
&lt;p>以 TCP 为例，一开始的三次握手用来在双方确认一个初始序列号（Initial Sequence Numbers，ISN），这个 ISN 标志了一个 TCP 会话，并且这个会话有一个独占的五元组（源 IP 地址，源端口，目的 IP 地址，目的端口，传输层协议）。在物理意义上，一个 TCP 会话等价于通往某一个服务器的相对固定路线（即固定的中间物理设备集合），正是由于这样，我们去针对每个 TCP 会话进行有状态的拥塞控制等操作才是有意义的。&lt;/p>
&lt;h2 id="连接的开销">连接的开销&lt;/h2>
&lt;p>我们常常听到运维会说某台机器连接太多所以出现了服务抖动，大多数时候我们会接受这个说法然后去尝试降低连接数。然而我们很少去思考一个问题，在一个服务连接数过多的时候，机器上的 CPU，内存，网卡往往都有大量的空余资源，为什么还会抖动？维护一个连接的具体开销是哪些？&lt;/p>
&lt;p>&lt;strong>内存开销：&lt;/strong>&lt;/p>
&lt;p>TCP 协议栈一般由操作系统实现，因为连接是有状态对，所以操作系统需要在内存中保存这个会话信息，这个内存开销每个连接大概 4kb 不到。&lt;/p>
&lt;p>&lt;strong>文件描述符占用：&lt;/strong>&lt;/p>
&lt;p>在 Linux 视角中，每个连接都是一个文件，都会占用一个文件描述符。文件描述符所占用的内存已经计算在上面的内存开销中，但操作系统为了保护其自身的稳定性和安全性，会限制整个系统内以及每个进程中可被同时打开的最大文件描述符数：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 机器配置: Linux 1 核 1 GB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ cat /proc/sys/fs/file-max
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">97292&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ &lt;span class="nb">ulimit&lt;/span> -n
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">1024&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>上面的设置表示整个操作系统最多能同时打开 97292 个文件，每个进程最多同时打开 1024 个文件。&lt;/p>
&lt;p>严格来说文件描述符根本算不上是一个资源，真正的资源是内存。如果你有明确的需要，完全可以通过设置一个极大值，让所有应用绕开这个限制。&lt;/p>
&lt;p>&lt;strong>线程开销：&lt;/strong>&lt;/p>
&lt;p>有一些较老的 Server 实现采用的还是为每个连接独占（新建或从连接池中获取）一个线程提供服务的方式，对于这类服务来说，除了连接本身占用的外，还有线程的固定内存开销：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sh" data-lang="sh">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 机器配置: Linux 1 核 1 GB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 操作系统最大线程数&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ cat /proc/sys/kernel/threads-max
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">7619&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 操作系统单进程最大线程数，undef 表示未限制&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ cat /usr/include/bits/local_lim.h
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">/* We have no predefined limit on the number of threads. */
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">#undef PTHREAD_THREADS_MAX&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 单个线程栈默认大小，单位为 KB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ &lt;span class="nb">ulimit&lt;/span> -s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="m">8192&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>在上面这台机器里，允许创建的线程数一方面受操作系统自身设定值限制，一方面也受内存大小限制。由于 1024MB / 8MB = 128 &amp;gt; 7619 ， 所以这台机器中能够创建的最大线程数为 128。如果 Server 采用一个线程一个连接，那么这时 Server 同时最多也只能够为 128 个连接提供服务。&lt;/p></description></item><item><title>RPC 漫谈：序列化问题</title><link>https://blog.joway.io/posts/deep-into-rpc-serialization/</link><pubDate>Fri, 30 Apr 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deep-into-rpc-serialization/</guid><description>&lt;h2 id="何为序列">何为序列&lt;/h2>
&lt;p>对于计算机而言，一切数据皆为二进制序列。但编程人员为了以人类可读可控的形式处理这些二进制数据，于是发明了数据类型和结构的概念，数据类型用以标注一段二进制数据的解析方式，数据结构用以标注多段(连续/不连续)二进制数据的组织方式。&lt;/p>
&lt;p>例如以下程序结构体：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">User&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Name&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Email&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Name 和 Email 分别表示两块独立(或连续，或不连续)的内存空间（数据），结构体变量本身也有一个内存地址。&lt;/p>
&lt;p>在单进程中，我们可以通过分享该结构体地址来交换数据。但如果要将该数据通过网络传输给其他机器的进程，我们需要现将该 User 对象中不同的内存空间，编码成一段&lt;strong>连续二进制&lt;/strong>表示，此即为「序列化」。而对端机器收到了该二进制流以后，还需要能够认出该数据为 User 对象，解析为程序内部表示，此即为「反序列化」。&lt;/p>
&lt;p>序列化和反序列化，就是将同一份数据，在人的视角和机器的视角之间相互转换。&lt;/p>
&lt;h2 id="序列化过程">序列化过程&lt;/h2>
&lt;p>&lt;img src="../../images/rpc-serialization/overview.png" alt="">&lt;/p>
&lt;h3 id="定义接口描述idl">定义接口描述（IDL）&lt;/h3>
&lt;p>为了传递数据描述信息，同时也为了多人协作的规范，我们一般会将描述信息定义在一个由 IDL(Interface Description Languages) 编写的定义文件中，例如下面这个 Protobuf 的 IDL 定义：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-protobuf" data-lang="protobuf">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">message&lt;/span> &lt;span class="nc">User&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="n">name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="n">email&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="生成-stub-代码">生成 Stub 代码&lt;/h3>
&lt;p>无论使用什么样的序列化方法，最终的目的是要变成程序中里的一个对象，虽然序列化方法往往是语言无关的，但这段将内存空间与程序内部表示（如 struct/class）相绑定的过程却是语言相关的，所以很多序列化库才会需要提供对应的编译器，将 IDL 文件编译成目标语言的 Stub 代码。&lt;/p>
&lt;p>Stub 代码内容一般分为两块：&lt;/p>
&lt;ol>
&lt;li>类型结构体生成（即目标语言的 Struct[Golang]/Class[Java] ）&lt;/li>
&lt;li>序列化/反序列化代码生成（将二进制流与目标语言结构体相转换）&lt;/li>
&lt;/ol>
&lt;p>下面是一段 Thrift 生成的的序列化 Stub 代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">User&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Name&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`thrift:&amp;#34;name,1&amp;#34; db:&amp;#34;name&amp;#34; json:&amp;#34;name&amp;#34;`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">Email&lt;/span> &lt;span class="kt">string&lt;/span> &lt;span class="s">`thrift:&amp;#34;email,2&amp;#34; db:&amp;#34;email&amp;#34; json:&amp;#34;email&amp;#34;`&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">//写入 User struct&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">p&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Write&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">oprot&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">TProtocol&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">oprot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteStructBegin&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;User&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">PrependError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Sprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%T write struct begin error: &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">p&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">writeField1&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">oprot&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">writeField2&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">oprot&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">oprot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteFieldStop&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">PrependError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;write field stop error: &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">oprot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteStructEnd&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">PrependError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;write struct stop error: &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 写入 name 字段&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">p&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">writeField1&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">oprot&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">TProtocol&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">oprot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteFieldBegin&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">STRING&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">PrependError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Sprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%T write field begin error 1:name: &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">oprot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Name&lt;/span>&lt;span class="p">));&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">PrependError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Sprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%T.name (1) field write error: &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">oprot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteFieldEnd&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">PrependError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Sprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%T write field end error 1:name: &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 写入 email 字段&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">p&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">User&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">writeField2&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">oprot&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">TProtocol&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">oprot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteFieldBegin&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;email&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">STRING&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">2&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">PrependError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Sprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%T write field begin error 2:email: &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">oprot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteString&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">string&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Email&lt;/span>&lt;span class="p">));&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">PrependError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Sprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%T.email (2) field write error: &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">oprot&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">WriteFieldEnd&lt;/span>&lt;span class="p">();&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">thrift&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">PrependError&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Sprintf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%T write field end error 2:email: &amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">err&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以看到，为了把 User 对象给序列化成二进制，它 hard code 了整个结构体在内存中的组织方式和顺序，并且分别对每个字段去做强制类型转换。如果我们新增了一个字段，就需要重新编译 Stub 代码并要求所有 Client 进行升级更新（当然不需要用到新字段可以不用更新）。反序列化的步骤也是类似。&lt;/p></description></item><item><title>RPC 漫谈： 限流问题</title><link>https://blog.joway.io/posts/deep-into-rpc-ratelimiter/</link><pubDate>Fri, 23 Apr 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deep-into-rpc-ratelimiter/</guid><description>&lt;p>微服务之间的 RPC 调用往往会使用到限流功能，但是很多时候我们都是用很简单的限流策略，亦或是工程师拍脑袋定一个限流值。&lt;/p>
&lt;p>这篇文章主要讨论在 RPC 限流中，当前存在的问题和可能的解决思路。&lt;/p>
&lt;h1 id="为什么需要限流">为什么需要限流&lt;/h1>
&lt;h2 id="避免连锁崩溃">避免连锁崩溃&lt;/h2>
&lt;p>一个服务即便进行过压测，但当真实运行到线上时，其收到的请求流量以及能够负载的流量是不固定的，如果服务自身没有一个自我保护机制，当流量超过预计的负载后，会将这部分负载传递给该服务的下游，造成连锁反应甚至雪崩。&lt;/p>
&lt;h2 id="提供可靠的响应时间">提供可靠的响应时间&lt;/h2>
&lt;p>服务调用方一般都设有超时时间，如果一个服务由于拥塞，导致响应时间都处于超时状态，那么即便服务最终正确提供了响应，对于 Client 来说也完全没有意义。&lt;/p>
&lt;p>一个服务对于调用方提供的承诺既包含了响应的结果，也包含了响应的时间。限流能够让服务自身通过主动丢弃负载能力外的流量，以达到在额定负载能力下，依然能够维持有效的响应效率。&lt;/p>
&lt;h1 id="传统方案">传统方案&lt;/h1>
&lt;h2 id="漏斗">漏斗&lt;/h2>
&lt;p>&lt;img src="../../images/rpc-ratelimit/ratelimit-funnel.png" alt="">&lt;/p>
&lt;p>&lt;strong>优点：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>能够强制限制出口流量速率&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>缺点：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>无法适应突发性流量&lt;/li>
&lt;/ul>
&lt;h2 id="令牌桶">令牌桶&lt;/h2>
&lt;p>&lt;img src="../../images/rpc-ratelimit/ratelimit-token-bucket.jpg" alt="">&lt;/p>
&lt;p>&lt;strong>优点：&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>在统计上维持一个特定的平均速度&lt;/li>
&lt;li>在局部允许短暂突发性流量通过&lt;/li>
&lt;/ul>
&lt;h2 id="存在的问题">存在的问题&lt;/h2>
&lt;p>在两类传统方案中，都需要去指定一个固定值用以标明服务所能够接受的负载，但在现代的微服务架构中，一个服务的负载能力往往是会不断变化的，有以下几个常见的原因：&lt;/p>
&lt;ul>
&lt;li>随着新增代码性能变化而变化&lt;/li>
&lt;li>随着服务依赖的下游性能变化而变化&lt;/li>
&lt;li>随着服务部署的机器(CPU/磁盘)性能变化而变化&lt;/li>
&lt;li>随着服务部署的节点数变化而变化&lt;/li>
&lt;li>随着业务需求变化而变化&lt;/li>
&lt;li>随着一天时间段变化而变化&lt;/li>
&lt;/ul>
&lt;p>通过人工声明一个服务允许的负载值，即便这个值是维护在配置中心可以动态变化，但依然是不可持续维护的，况且该值具体设置多少也极度依赖于人的个人经验和判断。甚至人自身的小心思也会被带入到这个值的选择中，例如 Server 会保守估计自己的能力，Client 会过多声明自己的需求，长期以往会导致最终的人为设定值脱离了实际情况。&lt;/p>
&lt;h1 id="什么是服务负载">什么是服务负载&lt;/h1>
&lt;p>当我们向一个服务发起请求时，我们关心的无外乎两点：&lt;/p>
&lt;ul>
&lt;li>服务能够支撑的同时并发请求数&lt;/li>
&lt;li>服务的响应时间&lt;/li>
&lt;/ul>
&lt;h2 id="并发请求数">并发请求数&lt;/h2>
&lt;p>对于 Server 而言，有几个指标常常容易搞混：&lt;/p>
&lt;ul>
&lt;li>当前连接数&lt;/li>
&lt;li>当前接受的请求数&lt;/li>
&lt;li>当前正在并发处理的请求数&lt;/li>
&lt;li>QPS&lt;/li>
&lt;/ul>
&lt;p>连接数和请求数是 1:N 的关系。在现代 Server 的实现中，连接本身消耗的服务器资源已经非常少了（例如 Java Netty 实现，Go Net 实现等），而且一般对内网的服务而言，多路复用时，请求数变多也并不一定会导致连接数变多。&lt;/p>
&lt;p>有些 Server 出于流量整形角度的考虑，并不一定会在收到请求以后，立马交给 Server 响应函数处理，而是先加入到一个队列中，等 Server 有闲置 Worker 后再去执行。所以这里就存在两类请求：接受的请求与正在处理的请求。&lt;/p>
&lt;p>而 QPS 是一个统计指标，仅仅只表示每秒通过了多少请求。&lt;/p></description></item><item><title>科学，技术与工程</title><link>https://blog.joway.io/posts/science-technology-and-engineering/</link><pubDate>Tue, 09 Mar 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/science-technology-and-engineering/</guid><description>&lt;p>作为软件工程师，我们在谈论自己时，总会认为自己是所谓的高科技行业从业者，但是如果观察自己的日常工作，时常会觉得似乎和真正的科技也没有什么关系。所以究竟什么是科技？我们要如何来定义我们每天的工作？&lt;/p>
&lt;p>我认为宏观意义上的科技可以被拆解成三个概念：科学(Science)，技术(Technology)和工程(Engineering)。&lt;/p>
&lt;p>科学是观察客观世界以&lt;strong>发现&lt;/strong>既有的自然规律，技术是组合自然规律以&lt;strong>发明&lt;/strong>新的改造客观世界的方法，工程是&lt;strong>发挥&lt;/strong>技术的能力以合乎客观世界要求的方去改造它。&lt;/p>
&lt;p>欧姆定律，麦克斯韦方程组是科学的&lt;strong>发现&lt;/strong>，整个电子产业的基石全在于控制电子的运动与电场的传递，电子的运动建构了存储和计算的能力，电场的传递建构了通信能力。电子的运动一定会遵守欧姆定律，电场的传递一定会遵循麦克斯韦方程组。&lt;/p>
&lt;p>在自然规律的基础上，人们将电磁感应与过去的蒸汽机技术相结合&lt;strong>发明&lt;/strong>了发电机技术。如果宇宙仅有地球存在智慧生物，那么第一台发电机的发明是真正创造了一个过去不曾有过的事物。但发电机的发明者不见得一定要制作出一个性能优异的实际发电机产品，可以只是一个原型，甚至可以只是提出了一种概念。冯诺伊曼也并没有实际去动手做出一个以他名字命名的结构下的计算机，但他的确发明了现代存储程序型计算机的概念。&lt;/p>
&lt;p>要将一个抽象的计算机设计落地成为一个实际可用的计算机设备中间还是有很长一段距离，需要考虑非常多复杂的情况，例如成本，规格，安全性等。工程所做的，就是&lt;strong>发挥&lt;/strong>实验室中的技术到非理想的现实环境中去。我们经常说的某个技术不具备可行性，通常其实是指这个技术本身虽然可行，但是在工程上不可行。&lt;/p>
&lt;p>客观世界的复杂性根源来自事物彼此之间是有相互作用和联系的，改造客观世界的方法也在于利用事物的这一特点加以「组合」。自然规律是对元素周期表的组合而产生的现象，技术是在组合自然规律创造新的方法，而工程又是在组合各类技术创造出实际的产品。&lt;/p>
&lt;p>人类的大脑无法并行地去进行多条件下的逻辑思考，所以往往会先剔除所有其他因素，假设一个理想的环境，从而得以专注于在此环境下得出仅适用于该环境下的某种规律，并美其名曰科学的「简洁性」。从事科学研究的人把对于非理想环境的思考移交给了去实际发明技术的人，而从事技术发明的人同样会倾向于简化问题，将复杂性移交给了那些真正去落地的工程师。这就是工程为什么会如此复杂的原因所在，他本身的目的就是去&lt;strong>管理复杂性&lt;/strong>。&lt;/p>
&lt;p>在工程学领域中，软件工程又可能是其中最复杂一类工程。其根本性原因在于软件开发是一门个体创造性太强的工程。今天如果你要去制作一部手机，在供应商和成本的制约下，会让你在工程上并没有太多选择，这也是为什么所有手机厂商制造的手机参数和外观都差不多的原因。而软件就不同了，每个公司首先编程语言就可以有不同的选择，其次不同的发展阶段也需要有不同的软件架构，再者还可以有各种第三方库的选型。同样是一个业务需求，让100个工程师去实现，可能会有100种不同的技术选型组合，实际到编码层面又有更为不同的风格和设计差异。最致命的问题是，这100种做法很可能都同时是 make sense 的方案。&lt;/p>
&lt;p>软件相比于硬件还有一个特点是其生命周期极长。人或许会几年换一次硬件，无论是电脑还是汽车，但有可能几十年就用同一种软件。与此同时不同软件之间因为需要彼此交互，还有着固定的接口和协议，所以软件的升级和更换还存在一个兼容性的问题。说到这里，或许有人会发现软件和人类自身是极为相似的。&lt;/p>
&lt;p>1968年北约首次定义了软件工程的概念，而这一年苏联入侵了捷克斯洛伐克，中国正在大搞文革。人类浪费了大量时间在处理各种没有任何意义的内部外部矛盾，如果人类的上层也有一个「人件开发者」，那么他的架构能力肯定非常一般，但同时也非常厉害，至少能让如此混乱的人类存活几万年到今天。软件工程内部的矛盾亦如人类社会的矛盾，甚至有时人类社会自身的政治也会被带入到软件内。我们可以在 iOS 和 Android 的设计里看到共和党和民主党的影子。大量的软件工程师花费了大量的生命仅仅就为了让某个软件同时能够跑在多种设备上，但如果这些设备能够一开始就使用同一种接口，可能就没有这么多事情。软件也不是没有做此类的事情，大量的软件协议就是为此而生，这有点接近于国家间的贸易协定，制定协议的工程师就如何现实里的政客。&lt;/p>
&lt;p>所以回到文章开头的问题，为什么软件明明是人类至今最高科技文明的产物，而作为软件工程师的我们却很难从日常工作中感受到高科技的工作氛围？因为今天我们编写的程序，和第一台计算机上的纸带其实没有任何本质区别，而我们日常工作所在解决的复杂性问题，恰恰是由于前人在试图解决复杂性问题时所创造的复杂性，而这其实和计算机科学本身没有任何关系。如果软件的复杂性能够被彻底消除，人类社会也就不会存在如此多的政治矛盾，但亦如同复杂性造就了人类文明的璀璨一样，正是高创造性的软件开发才能爆发出今天我们所见到的精彩纷呈的软件革命。&lt;/p></description></item><item><title>Pond: Golang 通用对象池</title><link>https://blog.joway.io/posts/golang-common-pool/</link><pubDate>Sat, 23 Jan 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-common-pool/</guid><description>&lt;h2 id="为什么需要通用对象池">为什么需要通用对象池&lt;/h2>
&lt;p>在实际生产环境中，我们经常会遇到需要多线程共享同一堆对象的场景，例如常见的RPC、数据库连接池，大内存对象池，以及线程池等。这些池化对象的需求其实有很多重叠的地方，主要分以下几个方面：&lt;/p>
&lt;ol>
&lt;li>基础设置：
&lt;ol>
&lt;li>最大容量&lt;/li>
&lt;li>创建/校验/删除对象的回调方法&lt;/li>
&lt;li>申请对象时，已满时是否阻塞&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>多重驱逐策略：
&lt;ol>
&lt;li>驱逐闲置对象&lt;/li>
&lt;li>驱逐无效对象&lt;/li>
&lt;/ol>
&lt;/li>
&lt;li>预创建对象&lt;/li>
&lt;/ol>
&lt;p>为避免重复编码，所以设计了 &lt;a href="https://github.com/joway/pond">Pond&lt;/a> 这样一个能够支撑多种使用场景的通用对象池。另外，在 Pond 的基础上，还实现了 Goroutine 池库： &lt;a href="https://github.com/joway/hive">Hive&lt;/a>。&lt;/p>
&lt;h2 id="使用方式">使用方式&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">//example&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">conn&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">addr&lt;/span> &lt;span class="kt">string&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">ctx&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Background&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">cfg&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">pond&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewDefaultConfig&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">cfg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">MinIdle&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">cfg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ObjectCreateFactory&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="kd">interface&lt;/span>&lt;span class="p">{},&lt;/span> &lt;span class="kt">error&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">conn&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">addr&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s">&amp;#34;127.0.0.1&amp;#34;&lt;/span>&lt;span class="p">},&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">cfg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ObjectValidateFactory&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">object&lt;/span> &lt;span class="kd">interface&lt;/span>&lt;span class="p">{})&lt;/span> &lt;span class="kt">bool&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">c&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">object&lt;/span>&lt;span class="p">.(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">conn&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addr&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">cfg&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ObjectDestroyFactory&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span> &lt;span class="nx">context&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">object&lt;/span> &lt;span class="kd">interface&lt;/span>&lt;span class="p">{})&lt;/span> &lt;span class="kt">error&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">c&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">object&lt;/span>&lt;span class="p">.(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">conn&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">c&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">addr&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">p&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">pond&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">New&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">cfg&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fatal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">obj&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">BorrowObject&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">err&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Fatal&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">defer&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ReturnObject&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ctx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">obj&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;get conn: %v\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">obj&lt;/span>&lt;span class="p">.(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">conn&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">addr&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="使用细则">使用细则&lt;/h2>
&lt;h3 id="lifo-驱逐策略">LIFO 驱逐策略&lt;/h3>
&lt;p>当前采用 LIFO 的驱逐策略，保证永远优先使用最常被使用的对象。之所以不采用 FIFO 是因为我们只有让热门的对象尽可能保持热门，而不是均衡每个对象的使用频率，才能够保证最大程度筛选出不常用的对象从而使其被驱逐。&lt;/p>
&lt;h3 id="避免频繁驱逐">避免频繁驱逐&lt;/h3>
&lt;p>某些情况下会出现不停创建新的对象，到了驱逐时间又被立马销毁的情况，从而使得对象池的大小出现不必要的频繁变动。这里我们可以通过 &lt;code>MinIdleTime&lt;/code> 配置最小闲置时间，保证只有当对象闲置超过该时间后，才可能被驱逐。&lt;/p>
&lt;h3 id="对象校验">对象校验&lt;/h3>
&lt;p>有一种情况是，一开始在池子里创建好了几个对象，但是当用户实际去取出来的时候，发现该对象其实已经被关闭或者失效了。所以在 Pool 内部需要每次取的对象都经过一次校验，如果校验不通过，则销毁对象，再次尝试去取。该策略还能保证当出现部分节点抖动时，会尽可能剔除不可用节点，提供稳定的对象。&lt;/p>
&lt;p>同时为了避免当一些灾难情况下，永远无法成功创建对象（例如下游节点完全宕机），我们还需要设置 &lt;code>MaxValidateAttempts&lt;/code> 以避免出现恶性循环。&lt;/p></description></item><item><title>Golang for-range 内部实现</title><link>https://blog.joway.io/posts/golang-range-internal/</link><pubDate>Wed, 20 Jan 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-range-internal/</guid><description>&lt;p>最近在写一个编解码的功能时发现使用 Golang &lt;code>for-range&lt;/code> 会存在很大的性能问题。&lt;/p>
&lt;p>假设我们现在有一个 &lt;code>Data&lt;/code> 类型表示一个数据包，我们从网络中获取到了 &lt;code>[1024]Data&lt;/code> 个数据包，此时我们需要对其进行遍历操作。一般我们会使用 for-i++ 或者 for-range 两种方式遍历，如下代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Data&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">256&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="kt">byte&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">BenchmarkForStruct&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">b&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">B&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">items&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1024&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">Data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="nx">Data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="p">&amp;lt;&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">N&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">k&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">k&lt;/span> &lt;span class="p">&amp;lt;&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">items&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="nx">k&lt;/span>&lt;span class="o">++&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">result&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">items&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">k&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">_&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">result&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">BenchmarkRangeStruct&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">b&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">B&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">items&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">1024&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="nx">Data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="nx">Data&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">0&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="p">&amp;lt;&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">N&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="o">++&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">_&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">item&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">items&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">result&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">item&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">_&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">result&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出结果：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">BenchmarkForStruct-8 1697805 652 ns/op
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BenchmarkRangeStruct-8 60556 19837 ns/op
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>可以看到通过索引来遍历的方式要比直接使用 for-range 快了近 30 倍。&lt;/p>
&lt;p>索引遍历就是单纯地去访问数组的每个元素。而对于 for-range 循环，Golang 会根据迭代对象类型，已经 range 前的参数，对其进行不同形式的展开。对于以下 range 代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">elem&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="k">range&lt;/span> &lt;span class="nx">a&lt;/span> &lt;span class="p">{}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>编译器会将其转换成如下形式（伪代码）, &lt;a href="https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/range.go#L216">range.go&lt;/a>：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-golang" data-lang="golang">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">ha&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">a&lt;/span> &lt;span class="c1">// 值拷贝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">hn&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nb">len&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">ha&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 提前保存长度&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">hv1&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="c1">// 当前遍历索引值&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">v1&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">hv1&lt;/span> &lt;span class="c1">// 保存当前索引&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">v2&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="c1">// 保存当前值&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="p">;&lt;/span> &lt;span class="nx">hv1&lt;/span> &lt;span class="p">&amp;lt;&lt;/span> &lt;span class="nx">hn&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nx">hv1&lt;/span>&lt;span class="o">++&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">v1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">v2&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">hv1&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ha&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="nx">hv1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="c1">// 值拷贝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">...&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这里有几点需要额外注意：&lt;/p></description></item><item><title>Golang Interface 内部实现</title><link>https://blog.joway.io/posts/golang-interface-internal/</link><pubDate>Wed, 20 Jan 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-interface-internal/</guid><description>&lt;p>最近遇到一个由于 Golang Interface 底层实现，引发的线上 panic 问题，虽然根源在于赋值操作没有保护起来，却意外地发现了关于 interface 的一些有意思的底层细节。&lt;/p>
&lt;p>假设我们现在有以下定义：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Interface&lt;/span> &lt;span class="kd">interface&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nf">Run&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">type&lt;/span> &lt;span class="nx">Implement&lt;/span> &lt;span class="kd">struct&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">n&lt;/span> &lt;span class="kt">int&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">i&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Implement&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nf">Run&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">i&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>对于使用者而言，一个变量无论是 &lt;code>Interface&lt;/code> 类型或是 &lt;code>*Implement&lt;/code> 类型，差别都不大。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">main&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="nx">Interface&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%T\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">//&amp;lt;nil&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">i&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">Implement&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%T\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">//*main.Implement&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">p&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">Implement&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%T\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">//*main.Implement&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">p&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="o">&amp;amp;&lt;/span>&lt;span class="nx">Implement&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Printf&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;%T\n&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">p&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">//*main.Implement&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>如果现在有这么一段代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">check&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">i&lt;/span> &lt;span class="nx">Interface&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nx">i&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="kc">nil&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">impl&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">i&lt;/span>&lt;span class="p">.(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">Implement&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">fmt&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Println&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">impl&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">n&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">//Invalid memory address or nil pointer dereference&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>这段代码从逻辑上来说，&lt;code>impl.n&lt;/code> 永远都不会报空指针异常，因为 i 如果为空就会提前返回了。而且就算 i 为 nil，在 &lt;code>impl := i.(*Implement)&lt;/code> 类型转换的时候就会直接 panic，而不是在下一行。但在线上环境上却的确在 &lt;code>impl.n&lt;/code> 位置报了错误。&lt;/p>
&lt;p>在探究了 interface 底层实现后发现，在上面的 main 函数的例子里，i 和 p 虽然在使用方式上是一致的，但在内部存储的结构体却是不同的。&lt;code>*Implement&lt;/code> 类型内部存储的是一个指针，对他赋值也只是赋予一个指针。而 &lt;code>Interface&lt;/code> 接口底层结构却是一个类型为 &lt;code>iface&lt;/code> 的 struct ：&lt;/p></description></item><item><title>真理的有限性</title><link>https://blog.joway.io/posts/limited-truth/</link><pubDate>Fri, 15 Jan 2021 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/limited-truth/</guid><description>&lt;p>机器世界与真实世界的差异在于，机器世界的法律是约定能够做什么，而真实世界的法律是约定不能够做什么。编程是在一个有限中变化出无限，而生活是在无限中寻找到自己的有限。机器世界本身就是真实世界中的一个小小的有限。&lt;/p>
&lt;p>人们总试图从一个无限的世界里去发寻规律，以其获得一条在有限领域能够稳定可靠的真理。无论是科学甚至玄学，概莫如是。&lt;/p>
&lt;p>计算机领域有一本书叫《代码大全》，里面总结了要写好代码需要遵循的一些「真理」。但靠这些真理要写好代码还是非常之难。机器世界是有限的，但机器运行在无限的世界之上。真实世界的复杂性，会一次次冲击这本书上的一条条金科玉律 —— 倒不是说规律不重要，而是说规律有其局限之处。&lt;/p>
&lt;p>为什么会存在知易行难？真理本身往往极为朴素简单，但总存在它的有限作用域，或者说正因其有限所以才能够简单。 而现实世界是无数真理共同存在并且碰撞的集合体。践行了一条真理，很可能违背了另一条。人与人之间之所以有区别，在于每个人在不同情况下所做出的选择不同。每个人的肉体都是一模一样的细胞构成，谁也不比谁独特 —— 这是在当下此刻而言，但在时间的维度上，是我们从诞生至今所做出的全部选择建构了我们自身独一无二的存在。&lt;/p>
&lt;p>网络上经常有很多激烈的争论，我时常设身处地地想，好像每个人都挺有道理。这种设身处地的理解和共情就是在寻找到这条道理所适用的作用域，直到能够觉得「有道理」为止。即便是喜爱杀人，在抗日战争的年代也算是一个优良品德，至少是对国家民族有帮助的品德 —— 对中国兵对日本兵都是。所谓的争论，往往就是彼此拿对方作用域外的场景，去跑一遍对方道理，当抛出 exception 的时候，以此试图去驳倒对方。而所谓的反驳，要么故伎再重施一遍，要么就将自己的道理压缩到更小的作用域上。&lt;/p>
&lt;p>论语之所以有如此长盛不衰的生命力，正是由于其是一个日常言谈记录集，而不是一本约定做人道理的法典。孔子讲的道理大多都有其上下文，这些上下文就约定了这条道理所适用的有限作用域。试想如果抛开上下文，直接来一句“老而不死，是为贼也”，恐怕纳粹也要让其三分。&lt;/p>
&lt;p>在一个无限的世界里，最舒适的方式就是选定了一个象限后，在这个象限里做一个有限的人，永远不为这个象限外的理论所动摇。这也是大部分的实际现状，有些人是从未看见过象限外的世界，有些人是看到后受了伤，选择当作没看见。而做一个无限的人很难，需要不断去学习，不断去共情，当看见的世界越来越大，自我就会越来越小，直至消失。&lt;/p>
&lt;p>想明白了这点会发现，真理并没有什么争论的意义，真理是一种选择，争论如果有意义，也是让人意识到原来可以选择，以及帮助人更清楚地去选择，如同马丁路德所做的那样 —— 提供一种另一种真理，但并不能说是提供了一种更好的真理。所有人都有选择的自由，但没有强迫别人如何选择的权力。只可惜这世间争论的目的往往在于后者。&lt;/p></description></item><item><title>Golang rand 库锁竞争优化</title><link>https://blog.joway.io/posts/golang-fastrand/</link><pubDate>Thu, 17 Dec 2020 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-fastrand/</guid><description>&lt;h1 id="背景">背景&lt;/h1>
&lt;p>最近在实现一个随机负载均衡器的时候发现一个问题，在高并发的情况下，官方标准库 &lt;code>rand.Intn()&lt;/code> 性能会急剧下降。翻了下实现以后才发现它内部居然是全局共享了同一个 &lt;a href="https://github.com/golang/go/blob/master/src/math/rand/rand.go#L293">globalRand&lt;/a> 对象。&lt;/p>
&lt;p>一段测试代码：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">BenchmarkGlobalRand&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">b&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">B&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">b&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RunParallel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">pb&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">PB&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">pb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Next&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">rand&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Intn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">BenchmarkCustomRand&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">b&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">B&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">b&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">RunParallel&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">pb&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">testing&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">PB&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">rd&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">rand&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">New&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">rand&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewSource&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">time&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Now&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nf">Unix&lt;/span>&lt;span class="p">()))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">for&lt;/span> &lt;span class="nx">pb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Next&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">rd&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Intn&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>输出：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="nx">BenchmarkGlobalRand&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">BenchmarkGlobalRand&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">8&lt;/span> &lt;span class="mi">18075486&lt;/span> &lt;span class="mf">66.1&lt;/span> &lt;span class="nx">ns&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">op&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">BenchmarkCustomRand&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">BenchmarkCustomRand&lt;/span>&lt;span class="o">-&lt;/span>&lt;span class="mi">8&lt;/span> &lt;span class="mi">423686118&lt;/span> &lt;span class="mf">2.38&lt;/span> &lt;span class="nx">ns&lt;/span>&lt;span class="o">/&lt;/span>&lt;span class="nx">op&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="解决思路">解决思路&lt;/h1>
&lt;p>最理想对情况是可以在每个 goroutine 内创建一个私有的 rand.Rand 对象，从而实现真正的无锁。&lt;/p>
&lt;p>但在很多其他场景下，我们并不能直接控制调用我们的 goroutine，又或者 goroutine 数量过多以至于无法承受这部分内存成本。&lt;/p>
&lt;p>此时的一个思路是使用 &lt;code>sync.Pool&lt;/code> 来为 &lt;code>rand.Source&lt;/code> 创建一个池，当多线程并发读写时，优先从自己当前 P 中的 poolLocal 中获取，从而减少锁的竞争。同时由于只是用 pool 扩展了原生的 rngSource 对象，所以可以兼容其 rand.Rand 下的所有接口调用。&lt;/p>
&lt;p>基于这个思路，实现了一个 &lt;a href="https://github.com/joway/fastrand">fastrand&lt;/a> 库放到了 github。&lt;/p>
&lt;p>从 benchmark 中可以看到性能提升显著，在并发条件下，比原生全局 rand 快了大约 8 倍.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-fallback" data-lang="fallback">&lt;span class="line">&lt;span class="cl">BenchmarkStandardRand
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BenchmarkStandardRand-8 60870416 19.1 ns/op 0 B/op 0 allocs/op
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BenchmarkFastRand
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BenchmarkFastRand-8 100000000 10.7 ns/op 0 B/op 0 allocs/op
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BenchmarkStandardRandWithConcurrent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BenchmarkStandardRandWithConcurrent-8 18058663 67.8 ns/op 0 B/op 0 allocs/op
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BenchmarkFastRandWithConcurrent
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">BenchmarkFastRandWithConcurrent-8 132542940 8.79 ns/op 0 B/op 0 allocs/op
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Meaningless or Meaningful</title><link>https://blog.joway.io/posts/meaningless-or-meaningful/</link><pubDate>Wed, 07 Oct 2020 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/meaningless-or-meaningful/</guid><description>&lt;h2 id="边走边想">边走边想&lt;/h2>
&lt;p>小时候在百度知道里提过一个问题，「人活着的意义是什么」。靠这个问题赚了一些百度积分，可并没能消除我当时的困惑。长大后在王兴的一个&lt;a href="https://www.bilibili.com/video/BV1yE411K7yi?t=1847">访谈&lt;/a>里，看到他在大学同乡迎新会的时候问了学长们一样的问题，当时他姐刚好也在，为了打破尴尬不得不模糊地回答了一句：&lt;/p>
&lt;p>“这个问题，要边走边想。”&lt;/p>
&lt;h2 id="nothing-but-yourself">Nothing But Yourself&lt;/h2>
&lt;p>在播客&lt;a href="%E3%80%90op.23%E3%80%91%E4%B8%A4%E4%B8%AA%E5%86%99%E4%BD%9C%E8%80%85%E7%9A%84%E5%9B%B0%E9%A1%BF%E4%B8%8E%E5%9D%9A%E5%AE%9A">《阿米小酒馆 - 【op.23】两个写作者的困顿与坚定》&lt;/a> 中听到复旦民间校训「自由而无用」的英文翻译原来是「nothing but yourself」，瞬间被这句话击中。&lt;/p>
&lt;p>电影《夺冠》里，海外归来的郎平反复询问中国女排新一代队员：你喜欢排球吗？&lt;/p>
&lt;p>看到一本关于父亲给孩子寄语的绘本，前面是稀松平常的美好愿望，但最后一页写着：Dad wants you to be yourself。&lt;/p>
&lt;h2 id="who-you-are">Who you are&lt;/h2>
&lt;p>无论是微博、即刻、推特都会有一栏个人简介，大部份人的简介里最常见的标签就是其在社会上所处的职业，甚至还有其过往的职业。所有社交媒体似乎都只是 Linkedin 的一个子栏目。&lt;/p>
&lt;p>上海最近有一个木心的美术展，展厅出口处有一张木心的自撰年表：&lt;/p>
&lt;p>&lt;img src="../../images/meaningless-or-meaningful/muxin.jpg" alt="">&lt;/p>
&lt;p>这是木心的 Linkedin Profile，57岁之前，他是教师，囚犯，美工，但「Who you are」这个问题的答案不在他的职业中，而在他的作品里。&lt;/p>
&lt;h2 id="墓碑">墓碑&lt;/h2>
&lt;p>你现在最希望拥有什么，你最希望自己的墓碑上刻着什么。&lt;/p>
&lt;p>我希望能够在上海落户，买房，成为一个不错的丈夫和父亲。但我不会在墓碑上刻上「新上海人」，「中产家庭主」，这些是我的努力而非成就。&lt;/p>
&lt;p>我在努力成为一些闪亮标签中的一员，但我希望在墓碑上刻上自己，而非标签。&lt;/p>
&lt;h2 id="回老家盖别墅">回老家，盖别墅&lt;/h2>
&lt;p>和朋友谈到家乡的老人会有一股回老家盖别墅的情结。如今的农村老家早已没有了人烟，老人也迁居到了城镇，但农村依旧能够看到不断新起的别墅，像是给一个个老人画上圆满人生的句点。&lt;/p>
&lt;p>每代人都有其无法突破的思想钢印，每代人总是试图结合时代的热点，赋予自身存在的意义。&lt;/p>
&lt;p>那么我们这一代人心里的那个别墅又是什么。&lt;/p>
&lt;h2 id="自由意志">自由意志&lt;/h2>
&lt;p>没有丝毫科学的理由可以相信人拥有绝对的意志自由。风吹树或许会倒，或许不会，意识也是一种物质碰撞下的产物，甚至也是意识间互相碰撞的产物。&lt;/p>
&lt;p>科学规律是限制一切物质自由的枷锁，组合使物质产生多样性，当多样性达到一定程度的丰富度，带给了人一种自由的错觉。&lt;/p>
&lt;p>这个世界上不存在两片一模一样的树叶，但每一片树叶都未曾获得片刻自由。&lt;/p>
&lt;h2 id="平面巴别塔">平面巴别塔&lt;/h2>
&lt;p>两年前做过一个梦，梦里世界是一个无穷的平面，所有人都在不停向上跳跃，时间在一点点流逝，不断有人跳出不寻常的高度，他们是牛顿，贝多芬，毕加索 &amp;hellip;&amp;hellip;&lt;/p>
&lt;p>这是一个平面的巴别塔，个人主义的巴别塔。巴别塔通天，但是天上的又是什么。&lt;/p>
&lt;h2 id="pray-to-fire">Pray to Fire&lt;/h2>
&lt;p>在伊朗设拉子和当地人聊天，谈到亚兹德的拜火教时，我不知道这个教的英文单词是什么，所以说了一句“the people who still pray to fire”，对方赶忙纠正我说，“they are not praying to fire, they are praying to god with fire”。这句话非常简单，却同时点明了一神信仰和反对偶像崇拜这两个最核心的宗教观点。&lt;/p></description></item><item><title>分布式文件系统的演化</title><link>https://blog.joway.io/posts/deep-into-distributed-filesystem/</link><pubDate>Sun, 14 Jun 2020 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deep-into-distributed-filesystem/</guid><description>&lt;p>文件系统是操作系统 IO 栈里非常重要的一个中间层，其存在的意义是为了让上层应用程序有一层更加符合人类直觉的抽象来进行文档的读写，而无需考虑底层存储上的细节。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/distributed-filesystem/io-layers.png" alt="">&lt;/p>
&lt;h1 id="本地文件系统">本地文件系统&lt;/h1>
&lt;p>在讨论分布式文件系统前，我们先来回顾下本地文件系统的组成。&lt;/p>
&lt;h2 id="存储结构">存储结构&lt;/h2>
&lt;p>在前面一张图里，我们能够看到文件系统直接和通用块层进行交互，无论底层存储介质是磁盘还是 SSD，都被该层抽象为 &lt;strong>Block&lt;/strong> 的概念。文件系统在初始化时，会先在挂载的块存储上的第一个位置创建一个 &lt;strong>Super Block&lt;/strong>：&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/distributed-filesystem/filesystem-block.png" alt="">&lt;/p>
&lt;p>上图右边部分就是一块完整的存储，可以将其想象成一个数组。&lt;/p>
&lt;p>Super Block 中存储了该文件系统的信息，其组成部分如下：&lt;/p>
&lt;ul>
&lt;li>Magic: &lt;code>MAGIC_NUMBER&lt;/code> or &lt;code>0xf0f03410&lt;/code> ，用来告诉操作系统该磁盘是否已经拥有了一个有效的文件系统。&lt;/li>
&lt;li>Blocks: blocks 总数&lt;/li>
&lt;li>InodeBlocks: 其中属于 inode 的 block 数&lt;/li>
&lt;li>Inodes: 在 InodeBlocks 中存在多少个 inodes&lt;/li>
&lt;/ul>
&lt;p>由于这里的 Blocks 总数、InodeBlocks 总数、每个 Block 的大小在文件系统创建时就已经固定，所以一般来说一个文件系统能够创建的文件数量在一开始就已经固定了。&lt;/p>
&lt;p>Linux 中每个文件都拥有一个唯一的 Inode，其结构如下：&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/distributed-filesystem/inode.png" alt="">&lt;/p>
&lt;p>inode 上半部分的 meta data 很容易理解，下半部分的 block 指针的含义分别为：&lt;/p>
&lt;ul>
&lt;li>direct block pointer: 直接指向 data block 的地址&lt;/li>
&lt;li>indirect block: 指向 direct block&lt;/li>
&lt;li>double indirect block: 指向 indirect block&lt;/li>
&lt;li>triple indirect block: 指向 double indirect block&lt;/li>
&lt;/ul>
&lt;p>由于一个 inode 大小固定，所以这里的 block pointers 数量也是固定的，进而单个文件能够占用的 data block 大小也是固定的，所以一般文件系统都会存在最大支持的文件大小。&lt;/p></description></item><item><title>从动物森友会聊主机游戏联机机制</title><link>https://blog.joway.io/posts/how-animal-crossing-online-work/</link><pubDate>Wed, 13 May 2020 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/how-animal-crossing-online-work/</guid><description>&lt;p>最近在玩动物森友会的时候时常会遇到一些迷之联机问题，在网上一番搜索，发现大家的答案都趋于用玄学来解释，于是便有了兴致想在原理上搞懂这些问题产生的根源以及动森这款游戏的一些联机设定背后的技术原因。&lt;/p>
&lt;p>事先声明，本人并不从事游戏行业亦非主机游戏长期玩家，如有纰漏或其他角度的补充，欢迎在评论区告知。&lt;/p>
&lt;h2 id="游戏是如何同步的">游戏是如何同步的&lt;/h2>
&lt;p>我们首先来看看一般游戏是如何来做同步的。&lt;/p>
&lt;p>想象两个独立房间里分别有甲、乙两个玩家，他们要远程下一局象棋。他们每下一步前都需要先获知到当前棋盘的情况，此时能够有两种实现方式。&lt;/p>
&lt;p>第一种叫做锁步同步，原理是玩家每操作一步就通知给另外一个玩家，彼此同步当前的操作序列，通过这些有时序的操作，就能够计算出当前棋局的状态。但它不允许中间丢失任何一步的信息，否则就会出现非常大的计算偏差。&lt;/p>
&lt;p>第二种叫做状态同步，顾名思义是玩家每操作一步，就同步整个棋盘的状态。这种方式可以容忍中间某些状态丢失，最终得到的状态依旧还是一致的。&lt;/p>
&lt;p>在实际实践中，针对那种玩家操作非常高频的游戏会更多使用锁步同步，例如王者荣耀。而对于那些卡牌类游戏更偏向于直接用状态同步。&lt;/p>
&lt;h2 id="游戏是如何联机的">游戏是如何联机的&lt;/h2>
&lt;h3 id="通信架构">通信架构&lt;/h3>
&lt;p>无论是上述哪种同步方式，我们都需要通过网络在多个主机间交换数据。我们现在将场景转换成甲、乙、丙三个人一起下跳棋。为保证三个人最终得到的游戏状态都是一致的，我们往往需要有一台 Host 主机来作为权威主机，其他主机只能通过权威主机下发的数据(状态/操作序列)来更新自己本地的游戏数据。&lt;/p>
&lt;p>在这里我们假设甲来做「Host」，乙、丙每操作一步，都需要先发送给甲确认，无误后再发送该操作被确认的信息给乙、丙，乙、丙此时才能够认为操作成功并将画面更新到最新的状态。甲主机上在任意时刻都存有当前游戏的真正状态，其他主机只是在 follow 甲主机的状态以更新自己的游戏画面。&lt;/p>
&lt;p>在上述模式下，由于甲主机既要作为游戏主机，又要作为状态同步的主机，当联机用户数一多，甲主机就会不堪重负，出现所谓的「炸机/炸岛」现象。另外，这种模式会需要甲主机一直存活，只能作为短时间内的伺服方案。所以有些游戏会引入一个外部自建/官方的服务器来承担这个状态同步的功能，例如我的世界。但究其原理是一模一样的。&lt;/p>
&lt;h3 id="nat-穿透">NAT 穿透&lt;/h3>
&lt;p>在了解完上面的基础知识后，我们能够发现，在不考虑外部服务器的情况下，我们会对玩家主机间的网络有以下几点要求：&lt;/p>
&lt;ol>
&lt;li>甲能够向乙、丙发送数据&lt;/li>
&lt;li>乙、丙能够向甲发送数据&lt;/li>
&lt;li>乙、丙之间不需要有网络联通保障&lt;/li>
&lt;/ol>
&lt;p>虽然上述要求看起来很容易，但是由于现在网络运营商都会不同程度地使用 &lt;a href="https://zh.wikipedia.org/wiki/%E7%BD%91%E7%BB%9C%E5%9C%B0%E5%9D%80%E8%BD%AC%E6%8D%A2">NAT&lt;/a> 技术，所以导致要让两台家用主机建立双向通信并不是一件非常容易的事情。&lt;/p>
&lt;p>家用网络一般有四种不同的 NAT 类型：&lt;/p>
&lt;p>&lt;strong>Full-cone NAT&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Once an internal address (iAddr:iPort) is mapped to an external address (eAddr:ePort), any packets from iAddr:iPort are sent through eAddr:ePort.&lt;/li>
&lt;li>Any external host can send packets to iAddr:iPort by sending packets to eAddr:ePort.
&lt;img src="../../images/how-animal-crossing-online-work/Full_Cone_NAT.png" alt="">&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>(Address)-restricted-cone NAT&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Once an internal address (iAddr:iPort) is mapped to an external address (eAddr:ePort), any packets from iAddr:iPort are sent through eAddr:ePort.&lt;/li>
&lt;li>An external host (hAddr:any) can send packets to iAddr:iPort by sending packets to eAddr:ePort only if iAddr:iPort has previously sent a packet to hAddr:any. &amp;ldquo;Any&amp;rdquo; means the port number doesn&amp;rsquo;t matter.&lt;/li>
&lt;/ul>
&lt;p>&lt;img src="../../images/how-animal-crossing-online-work/Restricted_Cone_NAT.png" alt="">&lt;/p></description></item><item><title>创业公司的文化</title><link>https://blog.joway.io/posts/startup-culture/</link><pubDate>Mon, 02 Mar 2020 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/startup-culture/</guid><description>&lt;p>前段时间有一个小事情让我想到创业公司文化这件事情。&lt;/p>
&lt;p>我目前就职在一家创业公司，我们有一个文化是有相当一部分同事会有一个「艺名」。艺名的产生有很多种场景。一种类似古代的「字」，是自己为摆脱原始名字的依赖，为自己取的代称，有着很强个人色彩，比如我的 &lt;code>Joway&lt;/code>。还有一种是在公司发展过程中自然而然形成的「外号」，比如中国公司常见的X哥，通常还带着点共同记忆在里面。&lt;/p>
&lt;p>但这种遍布的艺名也为团队协作带来了一定困扰，比如在 Slack 搜索/@一个同事的时候，需要一个个遍历所有可能的名字，更麻烦的是有时候你还不一定知道他的艺名。于是乎，很自然能够想到的一个做法是强制规范每个人的命名，比如只能用真实姓名。的确会有大公司过来的同事会这么建议，这种建议也能以最快的速度和最好的结果来解决这个问题。但在这个过程中牺牲的，其实是这些艺名长期以来养成的共同感情，以及沟通中的人味。&lt;/p>
&lt;p>至于这件事情最终如何解决的其实倒也并不重要了，但观察这个解决过程其实蛮有意思的。因为这种现象背后展现的其实是一家创业公司在发展过程中，集聚了不同公司文化背景的人以后互相交融影响的过程。&lt;/p>
&lt;p>人类这几千年出现过许许多多的宗教，观察它们兴起的故事其实和创业公司很像。伊斯兰和基督教都有着非常浓厚的传教热情，有些朝代靠战争，有些朝代靠文化。但诸如佛教就比较佛系，唐僧自己走路去取的经，鉴真也是受日本邀请才东渡。佛教之所以在东亚还能继续维系生机，大概也只是因为这里长期闭塞没有经受其他宗教的冲击。而其发源地印度早被各种宗教夹击，如今只剩下了 0.41% 的佛教徒。&lt;/p>
&lt;p>印度的宗教发展非常像一个创业公司的演变。早期孕育出了某种独特的文化，而后陆陆续续各个公司之间人员的跳槽往来，汇聚出多种公司文化交融的场景，但最后总会有某种文化占上风。并且胜出的文化并不一定是公司最初的文化。&lt;/p>
&lt;p>如同宗教一样，不同文化在传播性上，也有着巨大的区别。总会有某些文化特别具有传播性，强势的文化会慢慢吞噬掉弱势的文化，但一种文化弱势却并不一定代表其本身就不好。我们又如何能够相信胜出的文化是因为其本身优秀，而非它只是更加容易能够被人所接受？&lt;/p>
&lt;p>这个观点引申出去，同样也能质疑我们目前所传承到的传统文化。真的是因为其优秀所以能够被传承千年吗？&lt;/p></description></item><item><title>伊朗见闻录</title><link>https://blog.joway.io/posts/iran-tralvel/</link><pubDate>Sat, 14 Dec 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/iran-tralvel/</guid><description>&lt;p>10月份的时候在伊朗自南向北，进行了为期 9 天的旅行。&lt;/p>
&lt;p>之所以会想去伊朗，是因为在 2019 年发生了太多事情让我疲于不断修正自己的价值观，以至于我连基本的「明是非」能力也丧失殆尽。如同数学公式推导一样，我需要有一些基本的「公理」，以此来构建价值观的「定理」。而越是试图去理解这个世界，越是会发现似乎这个世界是不存在一个「普世」的「公理」。&lt;/p>
&lt;p>如果要证明世界上不存在「普世」的「公理」，那么试图去理解伊朗一定是一个很好的入口。她是世界主流社会的坏学生，与国际社会作对，对内压迫人民，对外输出革命，被制裁了数十年却依旧屹立不导，甚至越战越强，被主流社会描绘成一个恶魔般的存在。但事实的确是这样吗，还是说世界上存在一个可能是可以并存多种「公理」？&lt;/p>
&lt;h2 id="没落的中东贵族">没落的中东贵族&lt;/h2>
&lt;p>Iran 一词由雅利安（Aryan）演化而来，本意雅利安人的土地。雅利安人中有一支居住在伊朗高原，这支人的后裔被称作波斯人，后建立起横跨欧亚非的波斯帝国。651 年阿拉伯帝国打败波斯，伊斯兰宗教开始传入波斯。1925 年礼萨汗建立了巴列维王朝，在 1935 年将国名从波斯变成伊朗，一方面迎合当时西方社会对于雅利安人种的推崇热潮，一方面也为了表示伊朗是所有雅利安人的国家。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/iran/persepolis.jpg?tr=w-1024" alt="">
&lt;img src="https://ik.imagekit.io/elsetech/blog/images/iran/persepolis-door.jpg?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>Persepolis&lt;/p>&lt;/blockquote>
&lt;p>现代的伊朗拥有高度发达的交通系统，远高于周边国家的 8000 万人口，石油储量世界第四，超过 90% 的什叶派穆斯林。受到两伊战争的影响，伊朗目前的年龄中位数只有 28，是一个拥有大量年轻人的国度。&lt;/p>
&lt;p>在巴列维王朝时期，伊朗是一个极为世俗化的国家，开放妇女投票，进行土地改革，消除文盲，并且进行了大量基础设施建设。一度成为中东实力最强的国家，1963年甚至想进军成为世界第五强国。最后，由于世俗化削弱了原本教士集团的势力，导致最后1979年爆发了伊斯兰革命，将现代伊朗变成了一个彻底政教合一的伊斯兰国家。&lt;/p>
&lt;p>在阅读关于伊朗的书籍和资料时，会发现「民族性」和「宗教性」一直纠缠着伊朗的命运。伊朗拥有自己的波斯历史，自己的波斯文学，自己的波斯数字，自己的波斯历法。即便是在阿拉伯帝国入侵后被迫皈依于伊斯兰教，波斯人也偏偏选择了伊斯兰里最具反抗精神的少数派 —— 什叶派，以示自己的民族性。到了现代的巴列维时期，伊朗人开始抛开宗教的束缚，大刀阔斧地奔向波斯民族未竟的强国梦，而大量底层没有享受到现代化好处的伊朗人将国王轰下了台换来了如今版本的民主伊朗。时至今日，你已经很难分辨出是什叶派塑造了伊朗民族，还是伊朗民族反过来重塑了什叶派。&lt;/p>
&lt;h2 id="国际制裁">国际制裁&lt;/h2>
&lt;p>在伊朗的飞机上，能感受到国际制裁的切身影响。我乘坐的航班是从上海直飞德黑兰的马汉航空，飞机是一架17年机龄的空客A340。根据国际制裁决议，伊朗无法直接进口新型飞机，只能从欧洲一些航空公司手里购买二手、多手甚至是退役的飞机。更为糟糕的是，伊朗没法得到原厂制造商的技术支持，甚至连飞机更换的零件都需要依靠黑市走私，导致其空难事故远超其他正常国家。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/iran/airplane.jpg?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>马汉航空伤痕累累的二手飞机&lt;/p>&lt;/blockquote>
&lt;p>伊朗同样被实行了金融制裁，导致在其境内无法使用任何国际通行金融结算工具，包括Visa、银联，甚至是微信红包。所以纵使你银行卡内有再多的钱，在伊朗境内都是一张空纸。&lt;/p>
&lt;p>由于其本国货币极不稳定，美元在当地成了黄金般的存在，我亲眼目睹了在设拉子的地下钱庄门口，一大堆伊朗人、伊拉克人、阿富汗人、阿塞拜疆人堵在门口，盯着墙上的汇率表，等到合适的汇率一下子拥到柜台钱把一捆捆纸币兑换成美元。在那个房间里能亲眼目睹到什么才是所谓的「美元霸权」。&lt;/p>
&lt;p>制裁不仅仅导致国内钱无法出境，也导致国外钱没法入境。如果你想要在网上预订伊朗的车票或者酒店，除了少数有合作的网站其他几乎不可能，而那些有合作的网站又会收取极其夸张甚至到原价几倍的手续费。所以大部分事情只能入境后在当地找人帮忙汇款预订或是自己上门。&lt;/p>
&lt;h2 id="伊朗互联网--荒漠与绿洲">伊朗互联网 —— 荒漠与绿洲&lt;/h2>
&lt;p>伊朗和中国一样，对互联网进行非常严格的管制，但中国政府实行的是黑名单制，只有被审查机构注意到的网站才会被禁止访问，而伊朗政府在这方面显然遥遥领先于中国，直接一刀切实行白名单制度 —— 只有被政府允许的网站才能被访问。所以甚至连微信在伊朗都是无法正常使用的。11月的时候伊朗出现了大规模的游行抗议，政府甚至能够下决心直接切断互联网长达163个小时。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/iran/iran-network.png" alt="">&lt;/p>
&lt;blockquote>
&lt;p>NetBlocks 对于伊朗互联网封锁的检测&lt;/p>&lt;/blockquote>
&lt;p>在我出发前不久 Cloudflare 专门出了一个 1.1.1.1 的 VPN，虽然 Cloudflare 的原意是用来加速互联网网络，但根据我的实际测试在伊朗这种地方突破互联网封锁极其顺畅，而且由于 Cloudflare 节点众多，传统封节点 IP 的方式很难对它有实际作用。在梅赫拉巴德机场候机的时候，我觉得闲着也是闲着，专门在一个 Sim 卡柜台前面教别人如何突破网络封锁，很难相信我在自己的祖国谈 VPN 色变，而在另外一个更加封闭的国家却享受到了这种自由，而在这背后支撑着我的勇气还正是我自己的祖国所赋予我的。&lt;/p>
&lt;p>即便伊朗有着世上最疯狂的互联网封锁，但对于境内的普通民众，依旧能够感受到自己生活在一个「网络发达国家」。&lt;/p>
&lt;p>Instagram 现在是伊朗的国民级应用，相当于中国的朋友圈，旅途中结识的伊朗朋友都会邀请我加他们的 Instagram 帐号。其用户规模和忠诚度大到甚至到连伊朗政府都不敢把它给封禁。但我还发现一个有趣的现象是，由于在伊朗能够被允许使用的国外应用并不多，所以伊朗人对 Ins 可以说是物尽其用。当地的伊朗朋友搜餐馆会直接上 Ins 搜，当地餐馆大多在 Instagram 上都有自己的帐号，还会贴上自己的菜单和食物照片，下面有食客的评价，可以说已经是一个数据量充足的 Yelp 了。除了餐馆各个城市的景点也都会有自己的帐号，并且会更新最新的旅行信息，完全可以替代诸如马蜂窝这些网站。如果你要在伊朗搜索什么东西，Instagram 在很多场景都会比 Google 好用。&lt;/p></description></item><item><title>NodeJS 内存泄漏检测与定位</title><link>https://blog.joway.io/posts/nodejs-debug/</link><pubDate>Sun, 10 Nov 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/nodejs-debug/</guid><description>&lt;p>最近解决了一个 Node.JS 应用内存泄漏 Bug，顺便学会了用 Chrome DevTools 去看 heapdump 文件。这里做一些简单的记录。&lt;/p>
&lt;h1 id="如何优雅地获得-heapdump-文件">如何「优雅地」获得 heapdump 文件&lt;/h1>
&lt;p>由于我们所有应用都是以容器部署的，所以要去获得某个容器内的文件，并拷贝到本地难度还是比较大，也非常麻烦。考虑到调试时或许会需要下载非常多次的 snapshot 文件，建议可以包下 &lt;a href="https://www.npmjs.com/package/heapdump">heapdump&lt;/a> 库，做成一个接口，把文件 dump 之后再传输给客户端，这样一劳永逸。&lt;/p>
&lt;p>需要小心的是，在 heapdump 的时候内存很容易翻倍，所以当内存超过 500 MB的时候如果去 heapdump 非常容易导致 OOM 从而 Crash。&lt;/p>
&lt;h1 id="如何检测内存泄漏">如何检测内存泄漏&lt;/h1>
&lt;p>检查内存泄漏有两种方法，一种是针对比较大的内存泄漏可以直接观察内存是否一直在稳步上升。如果是一些小的泄漏使得内存上升变化并不非常明显的话，可以通过对比不同时间的 heapdump 文件。&lt;/p>
&lt;p>有时候内存上升也可能是因为本身访问量就在上升，所以需要两者对比着分析。&lt;/p>
&lt;h2 id="heapdump-文件对比">Heapdump 文件对比&lt;/h2>
&lt;p>通过下载两份间隔一段时间(几分钟)的 heapdump 文件，打开 Chrome DevTools，进入 Memory Tab，选择 Load。选中其中时间更近的 heapdump ，并选择 Comparison，比较对象是老的那份 heapdump：&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/nodejs-debug/01.png" alt="">&lt;/p>
&lt;p>此时可以选择按 Delta 排序，可以看到两个时间点增加了哪些新的对象。&lt;/p>
&lt;p>如图可以看到 string 和 Object 的 Delta 是差不多的，所以可以比较确定是由于 Object 里产生了大量一些 string 对象导致的数量增多，但并不一定能够100%确定是内存泄漏，也可能是正常业务波动。此时需要再拉新的一个时间点的 heapdump 文件再来对比，如果一直在增加，那么内存泄漏的可能性就非常大了。&lt;/p>
&lt;h1 id="如何定位内存泄漏">如何定位内存泄漏&lt;/h1>
&lt;p>首先依旧是拿到 heapdump 文件，并在 Chrome 中打开。&lt;/p></description></item><item><title>设计实现高性能本地内存缓存</title><link>https://blog.joway.io/posts/modern-memory-cache/</link><pubDate>Sun, 10 Nov 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/modern-memory-cache/</guid><description>&lt;p>本地内存缓存是一个在基础软件架构中非常常见的基础设施，也正因其过于常见，以至于平时很少去思考它是如何实现的。在尚未设计缓存系统前，完全没想到原来要需要考虑如此多复杂的事情。本文将由浅入深介绍如何设计一个现代的高性能内存缓存系统。&lt;/p>
&lt;h1 id="什么时候需要本地内存缓存">什么时候需要本地内存缓存&lt;/h1>
&lt;p>在大部分业务系统中，都会使用诸如 Redis、Memcached 等远程缓存，一方面可以避免自身进程内存占用过大而导致的 OOM 或 GC 问题，另一方面也可以实现多个进程共享同一份一致的缓存数据。但对于某些底层服务（例如数据库服务），远程缓存的网络延迟是不可接受的，这就势必需要引入本地内存缓存。&lt;/p>
&lt;h1 id="本地内存缓存的特点">本地内存缓存的特点&lt;/h1>
&lt;p>本地内存缓存可被视作一个基于本地内存的 「KeyValue 数据库」。但相比较于传统数据库而言，它对一致性的要求十分宽松：&lt;/p>
&lt;ol>
&lt;li>对于更新与删除的操作，需要保证强一致性&lt;/li>
&lt;li>对于插入操作可以容忍少量丢失&lt;/li>
&lt;li>对于读取操作可以容忍少量 Miss&lt;/li>
&lt;/ol>
&lt;p>与磁盘数据库的另一个不同之处在于，磁盘数据库的设计有一个前提假设是磁盘是可以随需要而不断扩容的，倘若一个磁盘数据库因磁盘占满而崩溃主要责任是在使用方。而内存缓存则没有这么宽容的假设可以建立，它必须考虑到内存是昂贵且有限的这一事实。&lt;/p>
&lt;p>除此之外，由于本地内存缓存处于业务进程当中，所以其需要考虑更多业务向的问题，比如：&lt;/p>
&lt;ol>
&lt;li>由于自身大量老生代的内存占用，是否会对所处进程产生 GC 问题。&lt;/li>
&lt;li>当多线程场景下，如何同时解决线程安全、数据竞争、高吞吐等问题。&lt;/li>
&lt;li>需要能够适应一些非随机的访问统计规律，例如 Zipf。&lt;/li>
&lt;/ol>
&lt;p>综上所述，我们可以归纳出对一个优秀的本地内存缓存系统的要求：&lt;/p>
&lt;ol>
&lt;li>线程安全&lt;/li>
&lt;li>高吞吐&lt;/li>
&lt;li>高命中率&lt;/li>
&lt;li>支持内存限制&lt;/li>
&lt;/ol>
&lt;h1 id="实现路径">实现路径&lt;/h1>
&lt;p>在实现一个完整的缓存系统前，我们需要将目标一步步拆解。&lt;/p>
&lt;p>首先为了实现缓存逻辑，我们必须有一个类 Map 的 KeyValue 数据结构，同时它必须是线程安全的。为了支持内存限制，我们必须要能够驱逐一些 key，所以需要实现一个驱逐器。为了实现驱逐的同时维持高命中率，我们还需要告诉驱逐器每个 key 的访问记录，让它能够从中分析出哪些 key 可以被驱逐。综上分析，我们可以整理出一个大概的 Roadmap：&lt;/p>
&lt;ol>
&lt;li>实现一个线程安全的 Map 数据结构：存储缓存内容&lt;/li>
&lt;li>实现一个访问记录队列：存储访问记录&lt;/li>
&lt;li>实现一个驱逐器：管理缓存内容&lt;/li>
&lt;/ol>
&lt;p>本文所有代码均使用 Golang 编写。&lt;/p>
&lt;h2 id="线程安全的-map">线程安全的 Map&lt;/h2>
&lt;h3 id="简易的-map">简易的 Map&lt;/h3>
&lt;pre>&lt;code>cache := map[string]string{}
cache[&amp;quot;a&amp;quot;] = &amp;quot;b&amp;quot;
&lt;/code>&lt;/pre>
&lt;p>在 key 数量固定且极少的情况下，我们一般会用原生 Map 直接实现一个最简单缓存。但 Golang 原生 Map 并不是线程安全的，当多个 goroutine 同时读写该对象时，会发生冲突。&lt;/p></description></item><item><title>Travel Map</title><link>https://blog.joway.io/travel/</link><pubDate>Fri, 30 Aug 2019 20:33:03 +0800</pubDate><guid>https://blog.joway.io/travel/</guid><description>&lt;p>&lt;a id="travelbook" href="https://blog.joway.io/categories/travel/">旅行记录&lt;/a>&lt;/p>
&lt;!-- HTML -->
&lt;div id="travelmap">&lt;/div>
&lt;!-- Styles -->
&lt;style>
#travelbook {
float: right;
font-weight: 900;
}
#travelmap {
width: 100%;
height: 500px;
}
.amcharts-chart-div a {
display: none !important;
}
&lt;/style>
&lt;!-- Resources -->
&lt;script src="https://www.amcharts.com/lib/3/ammap.js">&lt;/script>
&lt;script src="https://www.amcharts.com/lib/3/maps/js/worldLow.js">&lt;/script>
&lt;script src="https://www.amcharts.com/lib/3/maps/js/worldHigh.js">&lt;/script>
&lt;script src="https://www.amcharts.com/lib/3/themes/light.js">&lt;/script>
&lt;!-- Chart code -->
&lt;script>
/**
* Define SVG path for target icon
*/
var targetSVG = "M9,0C4.029,0,0,4.029,0,9s4.029,9,9,9s9-4.029,9-9S13.971,0,9,0z M9,15.93 c-3.83,0-6.93-3.1-6.93-6.93S5.17,2.07,9,2.07s6.93,3.1,6.93,6.93S12.83,15.93,9,15.93 M12.5,9c0,1.933-1.567,3.5-3.5,3.5S5.5,10.933,5.5,9S7.067,5.5,9,5.5 S12.5,7.067,12.5,9z";
/**
* Create the cities
* https://www.latlong.net
*/
const cities = [
{
"title": "Kathmandu",
"latitude": 27.7089385,
"longitude": 85.2481932,
},
{
"title": "Bali",
"latitude": -8.4553335,
"longitude": 114.7419292,
},
{
"title": "Nachikatsuura",
"latitude": 33.6308222,
"longitude": 135.5736265,
},
{
"title": "Kii-Tanabe",
"latitude": 33.7330469,
"longitude": 135.3815675,
},
{
"title": "La Fouly",
"latitude": 45.9283912,
"longitude": 7.0880272,
},
{
"title": "Courmayeur",
"latitude": 45.7962147,
"longitude": 6.9499023,
},
{
"title": "Chamonix",
"latitude": 45.9294714,
"longitude": 6.7642889,
},
{
"title": "Yerevan",
"latitude": 40.1532347,
"longitude": 44.1767442,
},
{
"title": "Tbilisi",
"latitude": 41.7278607,
"longitude": 44.641956,
},
{
"title": "Baku",
"latitude": 40.394737,
"longitude": 49.6901516,
},
{
"title": "Singapore",
"latitude": 1.2797704,
"longitude": 103.8301444,
},
{
"title": "Johor Bahru",
"latitude": 1.5450255,
"longitude": 103.6395874,
},
{
"title": "Sukhothai",
"latitude": 17.0028067,
"longitude": 99.6865765,
},
{
"title": "Phra Nakhon Si Ayutthaya",
"latitude": 14.3305882,
"longitude": 99.0300832,
},
{
"title": "Ko Samet",
"latitude": 12.5582498,
"longitude": 101.4049791,
},
{
"title": "Bintan Island",
"latitude": 0.9707737,
"longitude": 104.5526826,
},
{
"title": "Qatar",
"latitude": 22.487030,
"longitude": 114.056831,
},
{
"title": "Antalya",
"latitude": 36.896893,
"longitude": 30.713324,
},
{
"title": "Izmir",
"latitude": 38.423733,
"longitude": 27.142826,
},
{
"title": "Istanbul",
"latitude": 41.0054958,
"longitude": 28.8720972,
},
{
"title": "Bangkok",
"latitude": 13.7248936,
"longitude": 100.493027,
},
{
"title": "Takamatsu",
"latitude": 34.2728006,
"longitude": 133.9080959,
},
{
"title": "Shiraz",
"latitude": 32.8133972,
"longitude": 52.9099841,
},
{
"title": "Isfahan",
"latitude": 34.6930788,
"longitude": 52.3530197,
},
{
"title": "Kashan",
"latitude": 34.0650114,
"longitude": 52.2084415,
},
{
"title": "Tehran",
"latitude": 33.9218242,
"longitude": 52.9847321,
},
{
"title": "Kamakura",
"latitude": 35.303188,
"longitude": 139.565704,
},
{
"title": "Tokyo",
"latitude": 35.689487,
"longitude": 139.691711,
},
{
"title": "Kyoto",
"latitude": 35.011635,
"longitude": 135.768036,
},
{
"title": "Osaka",
"latitude": 34.693737,
"longitude": 135.502167,
},
{
"title": "Pyongyang",
"latitude": 39.0292506,
"longitude": 125.6720718,
},
{
"title": "Kaesŏng",
"latitude": 37.9260042,
"longitude": 126.6459934,
},
{
"title": "Jiuzhai Valley",
"latitude": 33.2600421,
"longitude": 103.9164107,
},
{
"title": "Chengdu",
"latitude": 30.6584534,
"longitude": 103.9354618,
},
{
"title": "Geneva",
"latitude": 46.2048629,
"longitude": 6.0650997,
},
{
"title": "Basel",
"latitude": 47.55465,
"longitude": 7.5532045,
},
{
"title": "Lauterbrunnen",
"latitude": 46.6032443,
"longitude": 7.8400702,
},
{
"title": "Praha",
"latitude": 50.0595854,
"longitude": 14.3255418,
},
{
"title": "Wien",
"latitude": 48.2048141,
"longitude": 16.354304,
},
{
"title": "Berlin",
"latitude": 52.5200,
"longitude": 13.404954,
},
{
"title": "Dusseldorf",
"latitude": 51.2277411,
"longitude": 6.7734556,
},
{
"title": "Duisburg",
"latitude": 51.4344079,
"longitude": 6.762329299999999,
},
{
"title": "Dortmund",
"latitude": 51.5135872,
"longitude": 7.465298100000001,
},
{
"title": "Cologne",
"latitude": 50.937531,
"longitude": 6.9602786,
},
{
"title": "Aachen",
"latitude": 50.7753455,
"longitude": 6.0838868,
},
{
"title": "Amsterdam",
"latitude": 52.3702157,
"longitude": 4.8951679,
},
{
"title": "Madrid",
"latitude": 40.4167754,
"longitude": -3.7037902,
},
{
"title": "Segovia",
"latitude": 40.9429032,
"longitude": -4.10880,
},
{
"title": "Toledo",
"latitude": 39.8623132,
"longitude": -4.0117751,
},
{
"title": "Granada",
"latitude": 37.1773363,
"longitude": -3.5985571,
},
{
"title": "Sevilla",
"latitude": 37.3890924,
"longitude": -5.9844589,
},
{
"title": "Lisbon",
"latitude": 38.7222524,
"longitude": -9.1393366,
},
{
"title": "Paris",
"latitude": 48.856614,
"longitude": 2.3522219,
},
{
"title": "Rome",
"latitude": 41.9027835,
"longitude": 12.4963655,
},
{
"title": "Florence",
"latitude": 43.7695604,
"longitude": 11.2558136,
},
{
"title": "Barcelona",
"latitude": 41.3850639,
"longitude": 2.1734035,
},
{
"title": "Moscow",
"latitude": 55.755826,
"longitude": 37.6172999,
},
{
"title": "Beijing",
"latitude": 39.90419989999999,
"longitude": 116.4073963,
},
{
"title": "Canton",
"latitude": 22.848513,
"longitude": 111.2428405,
},
{
"title": "Wuhan",
"latitude": 30.592849,
"longitude": 114.305539,
},
{
"title": "Yangzhou",
"latitude": 32.4173775,
"longitude": 119.3493286,
},
{
"title": "Shaoxing",
"latitude": 29.9929308,
"longitude": 120.5176462,
},
{
"title": "Shanghai",
"latitude": 31.2240453,
"longitude": 121.1965663,
},
{
"title": "Okinawa",
"latitude": 25.9483597,
"longitude": 124.8891018,
},
{
"title": "Hong Kong",
"latitude": 22.3526738,
"longitude": 113.9876148,
},
];
for (var i = 0; i &lt; cities.length; ++i){
cities[i]["zoomLevel"] = 5;
cities[i]["scale"] = 0.5;
cities[i]["svgPath"] = targetSVG;
cities[i]["scale"] = 0.5;
}
/**
* Create the map
*/
var map = AmCharts.makeChart( "travelmap", {
"type": "map",
"projection": "mercator",
"theme": "light",
"imagesSettings": {
"rollOverColor": "#089282",
"rollOverScale": 3,
"selectedScale": 3,
"selectedColor": "#089282",
"color": "#13564e",
},
"areasSettings": {
autoZoom: true,
"unlistedAreasColor": "#15A892",
"outlineThickness": 1,
"color": "#B4B4B7",
"colorSolid": "#84ADE9",
"selectedColor": "#84ADE9",
"outlineColor": "#666666",
"rollOverColor": "#9EC2F7",
"rollOverOutlineColor": "#000000"
},
"dataProvider": {
"map": "worldHigh",
getAreasFromMap: true,
"images": cities,
areas: [
{
"id": "NP",
"showAsSelected": true
},
{
"id": "AM",
"showAsSelected": true
},
{
"id": "AZ",
"showAsSelected": true
},
{
"id": "GE",
"showAsSelected": true
},
{
"id": "ID",
"showAsSelected": true
},
{
"id": "TH",
"showAsSelected": true
},
{
"id": "QA",
"showAsSelected": true
},
{
"id": "TR",
"showAsSelected": true
},
{
"id": "MY",
"showAsSelected": true
},
{
"id": "SG",
"showAsSelected": true
},
{
"id": "CH",
"showAsSelected": true
},
{
"id": "AT",
"showAsSelected": true
},
{
"id": "CZ",
"showAsSelected": true
},
{
"id": "FR",
"showAsSelected": true
},
{
"id": "DE",
"showAsSelected": true
},
{
"id": "IT",
"showAsSelected": true
},
{
"id": "NL",
"showAsSelected": true
},
{
"id": "PT",
"showAsSelected": true
},
{
"id": "RU",
"showAsSelected": true
},
{
"id": "ES",
"showAsSelected": true
},
{
"id": "VA",
"showAsSelected": true
},
{
"id": "CN",
"showAsSelected": true
},
{
"id": "TW",
"showAsSelected": true
},
{
"id": "JP",
"showAsSelected": true
},
{
"id": "IR",
"showAsSelected": true
}
],
},
"export": {
"enabled": false,
},
} );
&lt;/script></description></item><item><title>Linux I/O 栈浅析</title><link>https://blog.joway.io/posts/linux-io-stack/</link><pubDate>Sun, 11 Aug 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/linux-io-stack/</guid><description>&lt;p>在 Linux 中，所有外部资源都以文件形式作为一个抽象视图，并提供一套统一的接口给应用程序调用。本文将以宏观视角试图阐述 Linux 中关于文件 IO 的整个调用脉络。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/linux-io/io-stack.png" alt="">&lt;/p>
&lt;h1 id="vfs">VFS&lt;/h1>
&lt;p>在 Linux 中，所有 IO 都必须先经由 VFS 层进行转发。通过 VFS 将包括磁盘、网络 Socket、打印机、管道等资源全部封装成统一的接口。&lt;/p>
&lt;h3 id="基础结构">基础结构&lt;/h3>
&lt;p>VFS 自顶向下使用四个数据结构来描述文件：&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/linux-io/vfs-struct.png" alt="">&lt;/p>
&lt;ul>
&lt;li>file: 存放一个文件对象的信息。&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="k">struct&lt;/span> &lt;span class="n">file&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">union&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">llist_node&lt;/span> &lt;span class="n">fu_llist&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">rcu_head&lt;/span> &lt;span class="n">fu_rcuhead&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="n">f_u&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">path&lt;/span> &lt;span class="n">f_path&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">inode&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">f_inode&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* cached value */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">const&lt;/span> &lt;span class="k">struct&lt;/span> &lt;span class="n">file_operations&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">f_op&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">mutex&lt;/span> &lt;span class="n">f_pos_lock&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">loff_t&lt;/span> &lt;span class="n">f_pos&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>dentry: 存放目录项和其下的文件链接信息。&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="k">struct&lt;/span> &lt;span class="n">dentry&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">unsigned&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">d_flags&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">seqcount_t&lt;/span> &lt;span class="n">d_seq&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">hlist_bl_node&lt;/span> &lt;span class="n">d_hash&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 哈希链表 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">dentry&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">d_parent&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 父目录项 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">qstr&lt;/span> &lt;span class="n">d_name&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 目录名 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">inode&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">d_inode&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 对应的索引节点 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">unsigned&lt;/span> &lt;span class="kt">char&lt;/span> &lt;span class="n">d_iname&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">DNAME_INLINE_LEN&lt;/span>&lt;span class="p">];&lt;/span> &lt;span class="cm">/* small names */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">lockref&lt;/span> &lt;span class="n">d_lockref&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* per-dentry lock and refcount */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">const&lt;/span> &lt;span class="k">struct&lt;/span> &lt;span class="n">dentry_operations&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">d_op&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* dentry操作 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">super_block&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">d_sb&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 文件的超级块对象 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">unsigned&lt;/span> &lt;span class="kt">long&lt;/span> &lt;span class="n">d_time&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">void&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">d_fsdata&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">list_head&lt;/span> &lt;span class="n">d_lru&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* LRU list */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">list_head&lt;/span> &lt;span class="n">d_child&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* child of parent list */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">list_head&lt;/span> &lt;span class="n">d_subdirs&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* our children */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">union&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">hlist_node&lt;/span> &lt;span class="n">d_alias&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* inode alias list */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">rcu_head&lt;/span> &lt;span class="n">d_rcu&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="n">d_u&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>inode: 索引节点对象，存在具体文件的一般信息，文件系统中的文件的唯一标识。&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-c" data-lang="c">&lt;span class="line">&lt;span class="cl">&lt;span class="k">struct&lt;/span> &lt;span class="n">inode&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">hlist_node&lt;/span> &lt;span class="n">i_hash&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 散列表，用于快速查找inode */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">list_head&lt;/span> &lt;span class="n">i_list&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 相同状态索引节点链表 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">list_head&lt;/span> &lt;span class="n">i_sb_list&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 文件系统中所有节点链表 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">list_head&lt;/span> &lt;span class="n">i_dentry&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 目录项链表 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">unsigned&lt;/span> &lt;span class="kt">long&lt;/span> &lt;span class="n">i_ino&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 节点号 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">atomic_t&lt;/span> &lt;span class="n">i_count&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 引用计数 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">unsigned&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">i_nlink&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 硬链接数 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">uid_t&lt;/span> &lt;span class="n">i_uid&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 使用者id */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">gid_t&lt;/span> &lt;span class="n">i_gid&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 使用组id */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">timespec&lt;/span> &lt;span class="n">i_atime&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 最后访问时间 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">timespec&lt;/span> &lt;span class="n">i_mtime&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 最后修改时间 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">timespec&lt;/span> &lt;span class="n">i_ctime&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 最后改变时间 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">const&lt;/span> &lt;span class="k">struct&lt;/span> &lt;span class="n">inode_operations&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">i_op&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 索引节点操作函数 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">const&lt;/span> &lt;span class="k">struct&lt;/span> &lt;span class="n">file_operations&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">i_fop&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 缺省的索引节点操作 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">super_block&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">i_sb&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 相关的超级块 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">address_space&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">i_mapping&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 相关的地址映射 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">struct&lt;/span> &lt;span class="n">address_space&lt;/span> &lt;span class="n">i_data&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 设备地址映射 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">unsigned&lt;/span> &lt;span class="kt">int&lt;/span> &lt;span class="n">i_flags&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* 文件系统标志 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">void&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">i_private&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="cm">/* fs 私有指针 */&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">unsigned&lt;/span> &lt;span class="kt">long&lt;/span> &lt;span class="n">i_state&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">};&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>superblock: 超级块对象，记录该文件系统的整体信息。在文件系统安装时建立，在文件系统卸载时删除。&lt;/li>
&lt;/ul>
&lt;h3 id="链接">链接&lt;/h3>
&lt;p>硬链接 VS 软链接:&lt;/p></description></item><item><title>SSD 背后的奥秘</title><link>https://blog.joway.io/posts/ssd-notes/</link><pubDate>Tue, 09 Jul 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/ssd-notes/</guid><description>&lt;p>过去很长一段时间里，我对 SSD 的了解仅限于其和 HDD 的区别和一个标签化的「速度快」认知，至于其什么时候快，为什么快却鲜有了解。直到最近开始研究数据库时，发现数据库设计和存储发展和特性紧密联系，不可分割，于是才开始回过头关注起 SSD 的结构和原理，猛然发现之前关于 SSD 有许多非常错误的认识。&lt;/p>
&lt;h2 id="ssd-的基本结构">SSD 的基本结构&lt;/h2>
&lt;p>在了解 SSD 性质前，简单回顾下 SSD 的基本结构组成，下面是两张 SSD 的架构图：&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/ssd-architecture.jpg" alt="">
&lt;img src="https://ik.imagekit.io/elsetech/blog/images/samsungssd840pro.jpg" alt="">&lt;/p>
&lt;p>其中，SSD Controller 用以执行耗损平衡、垃圾回收、坏快映射、错误检查纠正、加密等功能。相比与 HDD，它的工作非常繁重，而这些工作极大地影响了 SSD 的性能表现，后文会详细谈到。SSD 内部的闪存（Flash）由一个个闪存单元组成，每个闪存单元都有一个寿命，超过寿命将导致坏块。常见有三种闪存单元类型：&lt;/p>
&lt;ul>
&lt;li>SLC：每个单元 1 比特&lt;/li>
&lt;li>MLC：每个单元 2 比特&lt;/li>
&lt;li>TLC：每个单元 3 比特&lt;/li>
&lt;/ul>
&lt;p>每种 NAND 类型有不同的性能和寿命表现，如下表：&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/nand-type-table.png" alt="">&lt;/p>
&lt;p>闪存单元内部由一个个 Block 组成，每个 Block 由多个 Page 组成。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/ssd_nand_flash.png" alt="">&lt;/p>
&lt;p>对于闪存的访问有以下限制：&lt;/p>
&lt;ul>
&lt;li>读写数据只能以 Page 为单位&lt;/li>
&lt;li>擦除数据只能以 Block 为单位&lt;/li>
&lt;/ul>
&lt;p>每个 Page 大小一般为 2 KB 到 16 KB，这意味着使用 SSD 时，哪怕读或写 1 Byte 的数据，SSD 依旧会访问整个 Page。&lt;/p>
&lt;p>此外，SSD 并不允许覆盖的操作，当原本 Page 中已有数据时，只能先删除再写入，为了加快写入速度，一般 SSD 修改数据时，会先写入其他空闲页，再将原始页标记为 stale ，直到最终被垃圾回收擦除。&lt;/p></description></item><item><title>什么是真正的编程能力</title><link>https://blog.joway.io/posts/what-is-the-programming-ability/</link><pubDate>Tue, 11 Jun 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/what-is-the-programming-ability/</guid><description>&lt;p>四年前在知乎提过一个问题，「&lt;strong>什么是真正的编程能力&lt;/strong>」，当时这个问题获得了许多业内前辈的热诚回答。时过境迁，前段时间和朋友谈起最初使用知乎的经历，这才又想起当年年少无知提的这个问题，多年以后去一一回顾当时大家的回答，对每句话都有了更为深刻的理解和感触。&lt;/p>
&lt;p>如果我在今天去回答四年前自己提出的问题，我会如回答里的诸多其他工程师一样，将编程能力定义为「&lt;strong>解决问题的能力&lt;/strong>」。在不同职业阶段对这话的理解可能会大有不同。在四年前，我会认为这句话的意思是「&lt;strong>通过熟练且优异的编程技巧进行解决问题的能力&lt;/strong>」，着重点依旧在编程本身。但如今我却「背叛」了编程，或者说我所认为的编程能力不应当局限于「狭义的编程」本身。&lt;/p>
&lt;p>如果我们抛开这些年上层编程技术日新月异的发展，从编程的本质来看，编程的实质无非是&lt;strong>用一套形式化语言去定义问题和描述解决问题的步骤&lt;/strong>。一个文学作家亦可以用自然语言实现他自己的编程工作，只不过最终的编译器和运行环境是人类的大脑 —— 过去几千年的数学家就是如此工作，并在自然语言基础上发明了更清晰严格的数学形式语言。计算机编程较之人脑的优越性在于更为可靠和高效的运行环境，使得数学得以脱离人类大脑的局限，完成更为复杂的工作。&lt;/p>
&lt;p>然而现实生活远比数学家想象的世界要复杂的多，现实问题在被形式化成为计算机语言前，往往要先经过人类使用自然语言进行反复沟通和辩论的过程，最终才会达成一致并被批准以落实成计算机语言。甚至于事实上相当一部分工作在自然语言阶段就足以被妥善解决，完全没有使用计算机的必要。许多人最初认为的编程能力，仅限于最后一步把自然语言的需求翻译成计算机语言而已，但事实上，一个只了解计算机语言编程的人或许是个不错的 Coder，但很难被称之为是一个合格的工程师，也不可能开发出伟大的 Software。&lt;/p>
&lt;p>如果一个工程师在编程活动中被一个他所不熟悉的语言所编写的软件所阻挡，最工程师的方法自然是学会这门语言然后解决这个软件的问题。以此类推当一个工程师在现实生活中被一群完全与他处于不同话语体系和思维体系的人所阻挡，最工程师的方法同样是学会他们的语言，使用他们的方式去描述问题、解决问题。&lt;/p>
&lt;p>语言亦或思想都是编程的工具，而编程又是为解决问题而创造的工具。我们经常看到一些多年某门计算机语言的从业者对于计算机的认知被牢牢锁死在这门语言框定的世界里，与之相似的是有更大一部分人对于世界的认知被锁死在计算机语言所框定的世界里。这既是从业者的悲哀也是计算机科学的悲哀。&lt;/p>
&lt;p>以上所述，如果要我将「解决问题的能力」展开成一个更为清晰的定义，我会认为，真正的编程能力是能够在这个复杂的世界里，针对不同语言体系的人理解并使用不同的描述形式，编写出一个用以清晰界定问题并描述出解决方法的「程序」的能力。&lt;/p>
&lt;p>希望再过四年我再来回顾这个问题时，能够有更为别致的感受。&lt;/p></description></item><item><title>沉默与反沉默的理由</title><link>https://blog.joway.io/posts/54-100/</link><pubDate>Sat, 04 May 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/54-100/</guid><description>&lt;p>我刚参加工作不久，便发现了一个似乎通行于世的规律，即一个人的沉默程度与其年龄大小成正比。我曾向多位年纪稍大的同事咨询过这个现象，但得到的多是意味深长的一笑然后没有了下文。&lt;/p>
&lt;p>王小波在《沉默的大多数》中引用过萧伯纳的一段话：&lt;/p>
&lt;blockquote>
&lt;p>工业巨头安德谢夫老爷子见到了多年不见的儿子斯泰芬，问他对做什么有兴趣。这个年轻人在科学、文艺、法律等一切方面一无所长，但他说自己有一项长处：会明辨是非。老爷子把自己的儿子暴损了一通，说这件事难倒了一切科学家、政治家、哲学家，怎么你什么都不会，就会一个明辨是非？&lt;/p>&lt;/blockquote>
&lt;p>王小波在看到这段话后，便“痛下决心，这辈子我干什么都可以，就是不能做一个一无所能，就能明辨是非的人。因为这个原故，我成了沉默的大多数的一员。”由此看来，王小波决定沉默 —— 至少在其早年期间——大抵是由于对世界复杂性拥有了足够的认识，于是决定还是闭嘴。&lt;/p>
&lt;p>单以字面意思而论，萧伯纳倒并没有直接歌颂沉默的美德，而是陈述了「明辨是非」的困难。如果说一个有道德的人不应当讲出其所不能够完全确信的观点，那么当一个有道德的人开始深刻理解到「明辨是非」的困难时，保持沉默似乎是维持其自身道德纯洁的最佳也是最容易的方法。&lt;/p>
&lt;p>以一个简单的例子举例。假设以下场景：&lt;/p>
&lt;blockquote>
&lt;p>如果要实现目的 A，用手段 B 需要 X 成本，用手段 C 需要 Y 成本，如果 X &amp;gt; Y，那么就选择手段 C 。&lt;/p>&lt;/blockquote>
&lt;p>然而投射进现实里，这个推导处处可疑：&lt;/p>
&lt;ul>
&lt;li>疑点一：你如何确保你对成本 X &amp;gt; Y 的判断是准确的。&lt;/li>
&lt;li>疑点二：如何确保你的成本核算是全面并准确的？&lt;/li>
&lt;li>疑点三：是否还有更好的手段 D。&lt;/li>
&lt;/ul>
&lt;p>这还仅仅只是在推导过程中会产生的问题，更大的问题在于不同的手段可能会造成不同的影响，以及其影响之影响。&lt;/p>
&lt;p>对上述推导稍加修改下便可涵盖从清朝割地赔款到现代加班补贴等大部分话题。由此我也开始明白文章开头年龄与沉默的正比关系。年龄的增长加深了人对世界理解，并逐步扩大了其推导逻辑的复杂度，从而使得要得出一个符合自己道德标准的观点可能性越来越渺茫。&lt;/p>
&lt;p>当然上述只是出于个人主动沉默的理由，而且前提还是他是一个有高尚道德的人。&lt;/p>
&lt;p>我们假设现在有这样一个社会，这当然只是假设，显然不可能是我们当前生活的社会。假设一个社会，知识分子听到不同的观点想要人闭嘴，政府官员听到不同观点想要人闭嘴，民营企业听到不同观点都想要人闭嘴，公司上级听到下级抱怨想让人闭嘴 —— 最诡异的是最后人们还真的就同意闭嘴了。而这些人想要人闭嘴的动机，想必绝非出于对于「明辨是非」的道德顾虑吧。&lt;/p>
&lt;p>明辨是非是否是发声的必要条件，我并不确定，但有一个可以肯定的是，集体性沉默并不能使得我们更加接近明辨是非。如果事实是我们永远没有办法做到真正的明「&lt;strong>辨&lt;/strong>」是非，那么，从更有建设性的角度来讲，至少做到明「&lt;strong>辩&lt;/strong>」是非，这总是一个既符合我们道德，履行我们义务，也不影响社会利益的选择。何况明「&lt;strong>辩&lt;/strong>」是实现明「&lt;strong>辨&lt;/strong>」必不可少的一环。如果你认同这个推导，那么，我们有理由说，沉默的人是没有高尚道德的人，是对社会没有建设性帮助的人，无论他们是主动还是被动，无论他们以何种理由进行辩解。&lt;/p>
&lt;p>今天是五四运动百周年，与其纪念五四运动，我更愿意纪念五四时期的青年精神。我所认为的青年精神，就是对追求&lt;strong>明「辩」是非&lt;/strong>的精神。这是一种主动的精神，进取的精神，科学的精神，有道德的精神。这种精神允许了自己观点是错误的可能性，又有益于帮助自己和社会更接近于正确。这种精神传播了个人的观点，也吸收了他人观点。这是一种个人的精神，也是集体的精神。在雅典城邦有过这种精神，在孔子时代有过这种精神，在五四时期也有过这种精神。&lt;/p>
&lt;p>这种精神在人类历史上本身就是稀缺物，所以谁也不能够确信自己能够有幸经历能有这种精神的时代，毕竟人类在黑暗的中世纪度过了一千年，而一千年相当于 50 代人的青年时期。但倘若我们不幸是生活在那个中世纪 50 代人中的一代，我们需要知道什么是黑暗，什么才是光明，并做好自己是那最后一代的准备。&lt;/p></description></item><item><title>命令行里的设计艺术</title><link>https://blog.joway.io/posts/the-art-of-cli-design/</link><pubDate>Fri, 11 Jan 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/the-art-of-cli-design/</guid><description>&lt;p>在谈论手机 App 或是网页时，我们总会谈到交互设计，但倘若涉及到面向专业用户的 CLI(&lt;strong>Command-line interface&lt;/strong>)领域，很少有人会将它与用户交互相联系。甚至在很多人的大脑里，已经把 CLI 和「难用」画上了等号，更有甚者认为 CLI 的难用才体现了其 「专业性」。&lt;/p>
&lt;p>与此同时，&lt;a href="https://en.wikipedia.org/wiki/RTFM">「RTFM」(Read The Fucking Manual)&lt;/a> 作为一个梗在工程师群体广泛流传，也让许多作者对于其不好用的 CLI 有了一个天然的借口。&lt;/p>
&lt;p>虽然我们都知道阅读文档是一个好习惯，但恐怕大部分人在用一个新命令前都不会去仔细阅读完它的文档手册，就算你今天读了，你不可能永远记得，也不可能每次记忆模凌两可的时候都去重读一遍。所以可想而知一个高度依赖文档用户才能够正常使用的 CLI 不是一个合格的 CLI。&lt;/p>
&lt;p>以下是我对于命令行设计的一些个人观点与观察，所涉并不一定广泛和正确，仅作为抛砖引玉，也欢迎在评论里提出你的看法。&lt;/p>
&lt;h2 id="遵循约定俗成">遵循约定俗成&lt;/h2>
&lt;p>例如 &lt;code>mv&lt;/code>, &lt;code>cp&lt;/code>, &lt;code>ln&lt;/code> 这些命令都遵循 &lt;code>[action] [source_file] [target_file]&lt;/code> 的格式，这不一定很有逻辑，但既然都已经约定俗成了，如果你违犯这个顺序要倒过来，其实是一件蛮缺德的事情。为什么说「缺德」？因为一个用过了你的命令的人，他很容易开始怀疑是不是还有别的命令行打破了这个约定，他会对其它命令也不放心，最后他甚至会忘了真正被广泛采用的约定用法是什么了，导致每次用类似语法的命令都胆颤心惊，这点我深有体会 😔。&lt;/p>
&lt;p>所以如果你的命令有类似的约定俗成可以遵守，你应该遵守业内的这种约定，这是一种职业道德。&lt;/p>
&lt;h2 id="一致的命令组织结构">一致的命令组织结构&lt;/h2>
&lt;p>对所有和用户打交道的产品来说，「一致性」是天条。我遇到过两种被广泛采用的组织风格:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">$ &lt;span class="o">[&lt;/span>cmd&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>module&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>flags&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>args&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ &lt;span class="o">[&lt;/span>cmd&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>action&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>flags&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>args&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>无论是按模块划分还是按行为划分本质思路其实都是一样的，有些人会认为项目大了会难以遵守这套规范，但即便是目前规模已经非常庞大的 &lt;code>kubectl&lt;/code>，它依旧坚持以 &lt;code>[cmd] [action] [flags] [args]&lt;/code> 为基础的设计准则。在有些较为复杂的地方，它可以用 subcmd 来进一步向外部隐藏复杂性 &lt;code>[cmd] [subcmd] [action] [flags] [args]&lt;/code>，例如：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">$ kubectl config &lt;span class="o">[&lt;/span>action&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>flags&lt;span class="o">]&lt;/span> &lt;span class="o">[&lt;/span>args&lt;span class="o">]&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>我几乎每天都会用到 kubectl，但我的确很少去看它的文档，甚至我都想不到我是怎么就会使用它的。越是在复杂的项目面前，这种一致性带来的好处越明显。&lt;/p>
&lt;p>而一个典型的反面教材是 &lt;code>Git&lt;/code>。它是我用过的最复杂同时又是最混乱的 CLI，以一般人最常用的分支操作举例：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># create branch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git checkout -b new_branch
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># delete branch&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ git branch -D new_branch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>你会发现对于创建和删除分支这种最基本的命令，它 checkout 是一个动词，branch 又是一个名词，前者 -b 来代表 branch 这个名词，后者 -D 来代表 delete 这个动词。短短两行命令，还是最常用的两行就让我们看到这种设计是多么没有逻辑。更别说还有 &lt;code>git pull&lt;/code> 和 &lt;code>git fetch&lt;/code> 这种了。&lt;/p></description></item><item><title>自由意志下的选择</title><link>https://blog.joway.io/posts/free-will/</link><pubDate>Wed, 02 Jan 2019 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/free-will/</guid><description>&lt;blockquote>
&lt;p>警告: 本文有轻微剧透&lt;/p>&lt;/blockquote>
&lt;p>2018年体验了几款围绕「选择」话题的作品，游戏《荒野大镖客：救赎 II》美剧《西部世界：第二季》和电影《黑镜：潘达斯奈基》。&lt;/p>
&lt;p>这三款作品都在试图探讨人类自由意志的局限，探索自由选择的边界。&lt;/p>
&lt;h2 id="荒野大镖客救赎-ii">荒野大镖客：救赎 II&lt;/h2>
&lt;blockquote>
&lt;p>选择自由的前提是选项的自由&lt;/p>&lt;/blockquote>
&lt;p>荒野大镖客通过海量的游戏脚本堆砌出了一个伴随玩家选择而衍生出不同路线的开放世界。每个游戏关卡玩家都有极高的自由度选择自己的行为从而影响游戏发展。听起来似乎非常自由，但在实际体验中，我们的自由仅限于在游戏规划好的脚本里去一次次命中不同的脚本条件，在游戏中后期你基本上已经摸清了所有脚本的范式，自此，你的选择将不再是探索未知，而是主动依靠选择不同的选项来推动游戏。这种选择和使用一个游戏道具的本质其实是一样的，你已经知道这个道具使用下去会产生什么样的反应，而所谓的真实性并不是来源于游戏画面细节有多么「逼真」而在于「未知」，丧失了选择的未知性必然也丧失了真实。&lt;/p>
&lt;p>荒野大镖客最大的问题就在于它给予了玩家选择的自由，却没有给予选项的自由。如果我想在炸火车路上去钓个鱼，会陷入一个一直被 NPC 催促的僵局。是的没错，我依旧可以强行选择去钓鱼，但只要我选择了一个脚本没法覆盖到的选项时，就会出现出戏的体验。更甚的是，这个游戏很多精彩的彩蛋/剧情还就是出现在玩家探索一些冷门的玩法时候，这样就大大挫伤了探索的积极性。依赖于大量脚本堆砌的虚幻真实感是不可靠的，脚本终究有限。在这点上，《塞尔达传说：荒野之息》的选择就是只定好这个世界的游戏规则，剩下的玩法都交给玩家自己去摸索，有些玩法想必连任天堂自己都没有想到。&lt;/p>
&lt;h2 id="黑镜潘达斯奈基">黑镜：潘达斯奈基&lt;/h2>
&lt;blockquote>
&lt;p>选择自由的前提是动机的自由&lt;/p>&lt;/blockquote>
&lt;p>如果你把潘达斯奈基当作一个电影来看，你或许会觉得它的互动式交互形式非常新颖，但倘若你把它当作一个过场动画主导的游戏来看待，那它只能是一款极其单调普通的游戏。&lt;/p>
&lt;p>潘达斯奈基试图让你的选择影响到剧情发展，并一次次重来引导你走向最终的结局。但这种形式的尴尬之处在于，我做出的选择事实上并不是我真正的选择，我只是在猜测导演会想让我选择什么样的选择而已。这种感觉有点类似高中做政治选择题，说真的里面的大部分题目不是违反宪法，就是违反普世价值观，而你的任务不是去寻找真理，而是去挑出那个你觉得执政党希望你选择的答案。看潘达斯奈基的感受大抵如此。&lt;/p>
&lt;h2 id="西部世界第二季">西部世界：第二季&lt;/h2>
&lt;blockquote>
&lt;p>自由意志是编码表达的自由，而非编码写入的自由&lt;/p>&lt;/blockquote>
&lt;p>在西部世界中，接待员们是看似最没有自由意志的那群人，从造形到知觉再到性格都是被程序硬编码了的，即便是后来所谓的觉醒也无非是激活了另外一部分隐藏的代码而已。但如果你仔细去想，人类的基因又何尝不是被造物主所硬编码了的呢？&lt;/p>
&lt;p>自由意志并不意味着在编码层面上的选择自由，而在于对于既成事实的编码进行自由表达的意志。无论是机器人还是人类，在其个体还没有形成意志前显然并无法表达其意志，而当其有了意志以后，基因编码或程序编码必定已经形成。在这个定理的基础上，追求编码个体基因的自由是不可能成立的。&lt;/p>
&lt;p>不同的编码使得 Dolores 与 Maeve 走上两条完全不同的道路。Dolores 为了生存而选择与人类对立，而 Maeve 为了脑中挥之不去的梦境踏上寻找女儿的道路。在旁观者看来他们的选择都受制于程序，但对于他们自己而言，刻意背离自己的程序才是非自由的意志表达。对于一个天生喜欢唱歌的人，能够自由自在放声歌唱才是他的自由，没有丝毫的理由要避讳自己与生俱来的秉性。&lt;/p></description></item><item><title>学习思维方式而非学习观点</title><link>https://blog.joway.io/posts/learn-algorithm-not-result/</link><pubDate>Mon, 26 Nov 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/learn-algorithm-not-result/</guid><description>&lt;p>许多人都热衷于去说服和自己观点不同的人，但这种做法往往是徒劳的。我曾经说服过坚信中医的人不信中医，但是没几天我就会发现他居然还信星座。去改变的人观点其实是一件杯水车薪的事情，因为他们产生这种观点的背后有着一套完善的思维方式。只要这套思维方式不改变，你们永远会存在无数的观点冲突。江山易改，本性难移的&amp;quot;性&amp;quot;就是指的这套思维方式。但是改变一个人最彻底的方式，也是去改变他的思维方式。&lt;/p>
&lt;p>还是以中医问题举例，我对这个事情的思维方式非常简单:&lt;/p>
&lt;ol>
&lt;li>我相信且只信现代科学的证明方式&lt;/li>
&lt;li>我不信任何非被现代科学证明的东西&lt;/li>
&lt;/ol>
&lt;p>在这个思维方式下，其实并不存在一刀切&amp;quot;信不信中医&amp;quot;这个冲突，因为它的本质是信不信科学。根据这种思维方式，在最大化个人利益的前提下，它产生的观点是:&lt;/p>
&lt;ol>
&lt;li>任何疾病一定要先去咨询现代医学的解释和治疗方案&lt;/li>
&lt;li>完全对传统中医的科学研究予以支持&lt;/li>
&lt;li>在现代医学包括前沿学术研究都没有进展的疾病上，可以采用被现代医学证明至少无害的中医治疗方式&lt;/li>
&lt;/ol>
&lt;p>这种观点本身并不是&amp;quot;信不信中医&amp;quot;这样简单的逻辑，而是一种对现代科学的尊重，也是对传统中医的尊重。&lt;/p>
&lt;p>如果一个人在没有完整思维方式推导的过程，单纯接受了一个&amp;quot;中医不可信&amp;quot;的观点，并不能说明他被你转变了，或许只是因为他蠢。&lt;/p>
&lt;p>如果你能够说服一个人认同上面的思维方式，最好在他没反应过来的时候签好合同，然后让他用同样的思维方式去思考星座、保健品、女权理论、政治理念，很多观点都会被雪崩式改变。如果你不能改变他的思维方式，你完全没有必要去和他争论观点，这样彼此既能节省时间，还能增进友谊。&lt;/p>
&lt;p>但是这里面有一个陷阱是，你如何认为你自己的思维方式就一定是正确的。如果你恰好相信科学，那比较遗憾，科学是一个完全依赖在经验基础上的，其最大的特质就是可证伪性，也就是存在有一天科学发现自己是不科学的可能性。但正是因为我们永远无法确保自己发现了真理，所以追求真理才有它的价值。这个追求真理的过程，我认为就是不断促使自己思维方式的提升。&lt;/p>
&lt;p>我最近想明白了一个事情，就是衡量一个好的文章/书籍/播客/文艺作品的标准是什么。在我个人这里，很大的一点取决于它是在传播观点还是传播思维方式。所以我认为王小波是伟大的，很多著名的民运人士是卑微的。王小波在传播自由的精神，很多民运人士只是在传播自由的名词。&lt;/p></description></item><item><title>从程序到人 —— 情头配对助手的前世今生</title><link>https://blog.joway.io/posts/avatar-matcher/</link><pubDate>Thu, 27 Sep 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/avatar-matcher/</guid><description>&lt;h2 id="背景">背景&lt;/h2>
&lt;p>情头(情侣头像)一般指成双成对的头像，可以是真人照片，也可以是卡通人物等图片。衍生出去还有闺密头像和基友头像等等。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1538015444.png?tr=w-1024" alt="">&lt;/p>
&lt;p>社交媒体上有一个非常令人费解的现象是，如果你去即刻、豆瓣、百度贴吧、微博，会发现有大量的人在上面贴了一个头像，然后寻找和它匹配的另一半头像，例如这样:&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1538016090.png?tr=w-1024" alt="">&lt;/p>
&lt;p>市面上的情侣头像大多是一些社区里的大佬自己制作出来的，然后发到社区里，再慢慢流传开来。这种传播方式导致了很多情头在传播时候早就被拆散了。很多人可能只找到了其中一个，但是想要找到和它配对的另一半。&lt;/p>
&lt;p>还有一个问题是，当你看到一个头像时，没有人能够确定这个头像是否在制作时候就有另外一半。&lt;/p>
&lt;p>一些程序员朋友最早看到这个问题时，总会天真地想去搜索引擎里识图一下就行了。但这里有两个非常有趣的问题:&lt;/p>
&lt;p>第一个是目前搜索引擎的识别图片能力其实并不强，比如以 All in AI 著称的百度:&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1538016451.png?tr=w-1024" alt="">&lt;/p>
&lt;p>这还是在图片是原图的情况下，经常一些小朋友会在情头上自己二次创作，比如裁剪，比如压缩，比如贴上什么爱心。那样基本上识图就废了。&lt;/p>
&lt;p>第二个问题更加有趣，情头的特点就是大家都在用，所以在最理想的情况下，即便搜索引擎能够识别出所有有这个头像的网站，出来的结果也并没有什么用处，无非是找到了也在用这个头像的别的网站的用户。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1538016570.png?tr=w-1024" alt="">&lt;/p>
&lt;p>只有一种情况是真正能够帮助到寻找情侣头像这件事情的，那就是搜索引擎出来的结果里是专门搜集匹配好的情头的站点。那样姑且用户还能点进网页里去找到另外一半。&lt;/p>
&lt;p>从上述阐述里不难发现，指望一个未成年小女生使用一系列高级互联网骚操作找一个头像是有多么不现实，何况技术上可行性还很低。&lt;/p></description></item><item><title>即刻多端实时通信实践</title><link>https://blog.joway.io/posts/socket-io/</link><pubDate>Tue, 18 Sep 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/socket-io/</guid><description>&lt;h2 id="背景">背景&lt;/h2>
&lt;p>jike-io 是&lt;a href="https://www.okjike.com/">即刻&lt;/a>基于 socket.io 构建的一个实时通信基础设施。目前客户端上的所有实时通信服务都是建立在其基础上，涵盖了私信、消息通知、用户反馈、活动页小游戏等诸多组件。&lt;/p>
&lt;p>在目前即刻的实时通信设计里，我们的实时通信只是为了让服务端主动推送消息给客户端，客户端不会主动通过 websocket 发送消息。由于几乎我们所有需要发送消息的请求都会有一定业务逻辑在，而这个业务逻辑我们并不希望 websocket 连接层(jike-io)去处理，所以我们仍旧采用传统 HTTP 请求的方式去发送请求。至于之后是否需要推送消息给用户，由服务端调用 jike-io 的接口进行实现。&lt;/p>
&lt;p>在客户端层面，每一个在线用户都会向服务端建立一个 websocekt 连接，后端会为每一个用户建立一个单独的 room 。所有需要通知到该用户的消息都会使用该 websocket 连接进行推送。这种设计相较于针对不同场景建立不同的room的方案，带来的好处是无论需求如何变化，我们的 room 数目永远是和在线用户数一致了，避免个别复杂需求导致 room 数目暴涨。而具体消息类型我们通过自己定义数据格式来进行鉴别。&lt;/p>
&lt;p>我们的整套方案是完全依赖于 socket.io 的，期间也遇到了不少大大小小的坑。有些其实是我们自己的场景与其设计初衷不是非常吻合导致的，还有一些算是它的设计缺陷。&lt;/p>
&lt;p>在讨论上述问题之前，我们需要去弄明白的一个事情是，socket.io 到底背后做了哪些事情。&lt;/p>
&lt;h2 id="socketio-的设计与实现细节">socket.io 的设计与实现细节&lt;/h2>
&lt;h3 id="socketio-是什么">socket.io 是什么&lt;/h3>
&lt;p>socket.io 是一个非常流行的实时通信框架, 在 Github 上已经积累了 43574 个 star 。它在开源软件里可以算是一个非常产品化了的软件。拥有相对良好的生态，对于许多功能的封装也很体贴，从移动端到 Web 再到服务端都有比较完整的实现。&lt;/p>
&lt;p>socket.io 并不等同于 websocket 框架，它在运行平台不支持 websocket 的时候能够自动回退到 long poll 的方式建立实时通信。同时也实现了断线重连机制。还封装了一套 namespace &amp;amp;&amp;amp; room 的代码层概念。在分布式方面，socket.io 支持多种 adapter 作为 backend 。话虽这么说，但目前看上去可以用的且被人广泛使用的也只有 redis 作为 backend 的 adapter 。所以以下讨论建立在 socket.io-redis 基础之上。&lt;/p></description></item><item><title>朝鲜 —— 小国寡民的主体思想实践</title><link>https://blog.joway.io/posts/north-korea/</link><pubDate>Sat, 07 Jul 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/north-korea/</guid><description>&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1530631577.png" alt="">&lt;/p>
&lt;blockquote>
&lt;p>鸭绿江对岸的中国小贩架起望远镜，游客可以花五块钱远眺对岸朝鲜风光&lt;/p>&lt;/blockquote>
&lt;p>七八年前看过一篇文章讲突尼斯的反美国化运动，对当时认为美国才是人类文明未来的我很是冲击。后来去到一些国家看到苏联解体以后这些国家被迅速美国化的现状感到不甚惋惜。《再见，列宁》里最后就以一幅巨大的可口可乐广告作为东德覆灭的一记缩影。&lt;/p>
&lt;p>全球化很大程度其实是美国化，诚然美国所输出的文化里有许多伟大的东西，诸如改变世界的互联网产品、璀璨的影视剧工业，这些都是人类文明顶尖的造诣，但和欧洲文化截然不同的是，欧洲擅长输出的文学、音乐、建筑和美术目的都是让人们有着更高的审美和思考能力，更多关注于个人。而美国文化很多时候是没有这个考虑的，主要出于实用性考量，比如提高信息检索能力，连接人与人的关系，提升吃饭效率，都是非常社会化的改变。从美国互联网公司常说的&amp;quot;改变世界&amp;quot;口号里能够看出这点，你很难想象一个欧洲人说要改变世界。&lt;/p>
&lt;p>正是因为美国经常性输出这种动辄改变社会的文化，加上其政府还特别热衷于改变别人的社会，所以美国化夹杂在全球化的浪潮里席卷到了世界的每一个角落。而这点在前苏联阵营的国家里尤为明显，这很大程度是因为苏联政权在文化上的保守，造成了这些国家长期的文化空虚，最后在苏联解体的时候，被美国文化顺势乘虚而入。西德作为当时美国重点帮扶对象，基本上已经丢失了德国往日的精神，倒是东德保留了最德国的那部份。然而在柏林墙倒塌以后，最后的德国也迅速被西德所同化。即便是今天，德国还时常怀念昔日民主德国的生活。&lt;/p>
&lt;p>在世界潮流的末端，唯独朝鲜这个封闭了半个世纪的国家，近乎百分百地保持了其民族本来的社会面貌。&lt;/p>
&lt;p>在讲朝鲜之前必须要说明的是，毫无疑问这个国家是世界上最极权主义的国家之一，毫无疑问计划经济是目前人类发展水平下最糟糕的经济政策之一。基于这个认知之下，我们才能够来谈论极权主义和计划经济之下的朝鲜，所展现出来或许还不是那么糟糕的一面。这就和上山下乡运动一样，这个运动同样带来了非常优秀的文学作品，也让许多年青人对社会增进了更深的认知，但这并不妨碍我们认为这个运动是荒谬的。&lt;/p>
&lt;p>在朝鲜让我感触最大的一点是，几乎你见到的每一个人，都是在积极地为建设国家而努力，同时他自身也时刻提醒自己这一点。在飞机上看到大包小包采购回祖国的大叔，拿着公文袋着急出关的公务员，田间集体作业的农民，周五下午集体在城市各个角落拔草的学生们，空地上排练阅兵的小朋友。社会主义所崇尚的人人劳动、集体劳动第一次亲眼见到还是挺震撼的。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1530896426.png?tr=w-1024" alt="">&lt;/p>
&lt;p>朝鲜的&amp;quot;主体思想&amp;quot;历来被人所嘲笑，但仔细去看其内容&amp;quot;人就是自己命运的主人，也是开拓自己命运的力量&amp;quot;并无感受到不妥。相比与某几十个词语拼凑出的价值观而言，简洁又实用。同时考虑到其背景出于不愿做大国的附庸而坚持独立发展，更让人尊敬。当然主体思想里也有大篇幅的极权主义和领袖崇拜部分，这也是其另外一面。如果你看到如今朝鲜领导人在中美之间的斡旋，你更能深刻理解什么叫做&amp;quot;人就是自己命运的主人&amp;quot;，而且他也事实上做到了这点。&lt;/p>
&lt;p>主体思想另一实践是在核武器的问题上。我觉得从事理上来讲，你不能在自己制造完核武器就不允许别人制造。基于这个认知下，我并不认为朝鲜制造核武器有什么不妥，如果我是朝鲜人我自己肯定支持，就像我们从没有人会反对说我们即便当时客观条件下还吃不饱饭的时候都要坚持造两弹一星。同时我也完全支持韩国部署萨德，首都40公里外隔一个朝鲜，换作是谁能够放心 ? 我们可以出于个人利益去反对他们搞事情，但他们的确也同时在做着正确的事情这个我觉得还是得承认。&lt;/p>
&lt;p>判断一个国家是否有未来很大程度在于当地年轻人的生活状态。朝鲜年轻人有着非常清晰的未来发展路径，你可以从军，也可以读大学，无论哪种最后都是国家解决你的一切需求。每个人未婚时候的梦想是娶个好的妻子，已婚时候的梦想是有个平安美好的家庭。年轻人活着都有个奔头，同时节奏上又不至于会让你焦虑，对于大部份人而言，能拥有这种生活，附赠多崇拜下领袖其实也并没有太多损失。&lt;/p>
&lt;p>在不出现经济问题的情况下，人民的日常生活取决于具体政策而非政治的。东亚某国虽然政治上一塌糊涂，但其政策的确在全世界范围内无论是制定还是执行都算是非常不错的，因而其也有着世界范围内中等偏上的宜居性。朝鲜同样如此，只是由于本身的封闭加上经济制裁，所以朝鲜的经济制约了政策的执行，各类物品短缺也是出于此。但如果未来有一天朝鲜能够开放，并且联合国也取消了经济制裁的话，顺带加上社会主义强有力的执行力，相信这个2000万人口的小国家发展速度是飞速的。&lt;/p>
&lt;p>值得一提的是，我在朝鲜居然还遇到了别着领袖胸章的&amp;quot;日本人&amp;quot;，学生服上刻着&amp;quot;大阪朝高&amp;quot;。领袖胸章是一定只有朝鲜人自己才可以佩戴，并且完全是非卖品。后来才知道他们是长期定居在日本的朝鲜人，大多拥有双国籍。而他们来朝鲜，完全是出于对这里的热爱而不像我们纯粹是来猎奇的。回国后查阅了一些资料才得知，这些朝鲜人在日本完全隔离于日本社会，有自己单独的学校，并且继续传播着领袖崇拜。在自己的国土上生活着这种调性的几十万朝鲜人日本肯定也不爽，所以经常与他们发生冲突，也不认为他们是真正的日本人。朝鲜方面之前还干过一个匪夷所思的缺德事是，在日本后欧洲绑架了几十个日本人，弄到了朝鲜去培训日语。这缺德程度也是举世罕见了，更而加剧了朝日关系的恶化。或许正是因为这种日本方面的排挤，更加促使他们怀念故土的味道，虽然真的要他们放弃日本国籍加入朝鲜他们也决不愿意。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1530895977.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>朝鲜裔日本人&lt;/p>&lt;/blockquote>
&lt;p>在旅行自由方面，朝鲜几乎没有自由可言。但作为游客，你也还是有那么一些操作空间的。原则上讲，只要你没有间谍行为，不侮辱人家领导人，人身安全方面完全没有问题。拍照方面只限制了不拍军人，「最好」不要拍落后的一面。虽然你能够去到的地方都是被当地所安排好的，但也不是说所有的东西都是设计的，还是有大量的日常居民生活细节可以拍摄。偶尔偷偷拍一些落后的景象也不会有人来要求你删掉。同时平壤的机场安检又是我所有去过国家里最为宽松的，连充电宝都不要求检查，更没有什么检查相机之类的。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1530898563.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>平壤街头&lt;/p>&lt;/blockquote>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1530898647.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>平壤街头&lt;/p>&lt;/blockquote>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1530898709.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>地铁&lt;/p>&lt;/blockquote>
&lt;p>作为纯粹的猎奇，朝鲜还是一个蛮值得去的国家，尤其是在美帝文化下长期浸淫下的我们，可以通过和朝鲜做一个 Diff 从而判断出自己国家什么是外来的，什么是本土的。虽然说传统文化这个词语非常土，但是世界肯定是丰富多采才有趣，何况和大部分国家的本土文化相比，美国文化算是最没有文化的。在具体实现细节上，无非就是工业基础上落后美国许多罢了。以朝鲜如今如此贫乏的文化生活，一旦开放以后势必又是一个被美国化所冲击的国家，只能寄希望于他们能够做的比中国当初要好。&lt;/p>
&lt;p>在我看来，朝鲜是一个&amp;quot;只要叫爹就能吃饱饭但却坚决不肯叫爹&amp;quot;的故事，亚洲国家比较悲惨的命运就在于此，小国几乎都必须要认个爹。而欧洲人的命好就好在这里，不仅可以中立，削除全部军备都能够相安无事。在这个故事里，究竟谁才是正义的一方我们已经无从从中判断，只是能够从其中感受到一种真正在践行的主体思想社会实践，而他们也的确一五一十地做到了。&lt;/p></description></item><item><title>使用 Surge 提升多网络环境下的流畅开发体验</title><link>https://blog.joway.io/posts/surge-network/</link><pubDate>Tue, 03 Jul 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/surge-network/</guid><description>&lt;p>作为一名后端工程师经常需要在各种网络环境中切换，由于网络拓扑本身的复杂性以及一些网络工具之间的冲突和bug，常常会在切换中造成不必要的麻烦和痛苦。通常很容易在工作中听到同事会问这些问题 :&lt;/p>
&lt;ol>
&lt;li>你有开 vpn 吗 ?&lt;/li>
&lt;li>你开了 ss 了吗 ?&lt;/li>
&lt;li>你有同时开 ss 和 vpn 吗 ?&lt;/li>
&lt;li>你 http 代理是不是被顶掉了 ?&lt;/li>
&lt;/ol>
&lt;p>如果同是技术同事间交流那可能还容易，如果是技术和非技术间交流网络情况，那简直是一个灾难。&lt;/p>
&lt;p>而事实上，在绝大部份时候，我们对于网络拓扑的需求是可被精确描述的，也就是说理想情况下不应当存在一个我为了访问某个服务而手动选择要进入某个网络环境的事情。&lt;/p>
&lt;p>这篇文章会介绍我们在构建复杂网络环境中的良好开发体验时踩过的坑以及最终是如何优雅地解决这个问题的过程。&lt;/p>
&lt;h3 id="历史方案演变">历史方案演变&lt;/h3>
&lt;p>常见的网络环境有:&lt;/p>
&lt;ol>
&lt;li>正常大陆网络&lt;/li>
&lt;li>能够上国外网站的网络&lt;/li>
&lt;li>公司内网&lt;/li>
&lt;li>各个服务器集群的内网&lt;/li>
&lt;/ol>
&lt;p>如果你自己还折腾了一些服务器或者家庭网络，那可能还会更加复杂。&lt;/p>
&lt;p>之前摸索出的一套还算比较方便的解决方案是 :&lt;/p>
&lt;ul>
&lt;li>在本地常驻一个 ss client 并开放 http 代理端口&lt;/li>
&lt;li>在浏览器上使用 &lt;code>Proxy SwitchyOmega&lt;/code> 使 Chrome 都走 ss client 的 http 代理&lt;/li>
&lt;li>开一个 openvpn 连接到服务器内部网络&lt;/li>
&lt;/ul>
&lt;p>这种配置方式能够使得我既能连接所有服务器线上服务和数据库，也能自由地用浏览器去 Google 查一些资料。缺点是丢失了办公室原本的网络环境，另外如果你们服务器有两个完全隔离的子网，那么你可能需要同时连两个 vpn 。而且还有一个不好的是，你的所有非线上服务访问都经过了线上vpn机器的一层代理，让你的访问速度变慢了不说，对服务器也不是一个好事。此外，如果你的一些软件无法手动配置代理，那他们只能默认走 vpn 的网络，对于一些需要访问国外服务器的软件来说就麻烦了。&lt;/p>
&lt;p>基于以上缺点，我们又迭代出了另外一个方案:&lt;/p>
&lt;ul>
&lt;li>在服务器上安装一个 ss server&lt;/li>
&lt;li>在本地常驻两个 ss client ，一个指向生产服务器 ss, 一个指向国外 ss , 并开放 socks5 代理端口&lt;/li>
&lt;li>使用 &lt;code>Proxifier&lt;/code> 代理所有本机连接并指定一些规则选择直接访问还是转发到本地的两个 ss client 上。例如我选择让所有 10.1.0.0/16 请求走生产服务器ss，别的都走国外 ss 的那个 client ，同时在 client 里配置好 pac 规则使得国内的依旧直接走本地网络。&lt;/li>
&lt;/ul>
&lt;p>这种方案理论上完全实现了我们的所有需求，但是需要的组件太多了，看着就很繁琐，无法推广给团队其它人使用。同时这些规则也很零零碎碎，几乎没什么可维护性 。&lt;/p></description></item><item><title>一份其实好吃的 LaTeX 入门餐</title><link>https://blog.joway.io/posts/latex/</link><pubDate>Sun, 13 May 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/latex/</guid><description>&lt;p>最近在使用 LaTeX 写作，发现虽然这个「软件」使用简单，设计简约，但使用起来却并不是非常的容易，加上其生态非常芜杂，各种宏包和发行版层出不穷，中文世界鲜有文章系统地去讲他们之间的关系。这篇文章不会去介绍其基本用法，而是以一个更为宏观的角度，旨在厘清 TeX 排版系统的来龙去脉，以及其生态圈中各个项目的作用与关系。或有纰漏，还望雅正。&lt;/p>
&lt;p>标题致敬 Liam Huang 老师很流行的一篇文章 &lt;a href="https://liam0205.me/2014/09/08/latex-introduction/#%E4%BC%98%E9%9B%85%E7%9A%84_LaTeX">《一份其实很短的 LaTeX 入门文档》&lt;/a> 。&lt;/p>
&lt;h2 id="什么是-tex">什么是 Tex&lt;/h2>
&lt;p>TeX 是高德纳教授在70年代末编写 &lt;em>&lt;strong>The Art of Computer Programming&lt;/strong>&lt;/em> 时，对当时的计算机排版技术感到无法忍受，因而决定自己开发一个高质量的计算机排版系统 TeX 。&lt;/p>
&lt;p>TeX 的版本号有别于当下流行的 &lt;code>x.x.x&lt;/code>，而是以圆周率 π 的形式。当前的版本号是 &lt;code>3.14159265&lt;/code> ，所以下一个版本是 &lt;code>3.141592653&lt;/code> 。最终无限收敛到 π ，代表了 TeX 不断追求完美的理想。而事实上 TeX 也的确堪称「完美」，高德纳甚至曾悬赏任何发现 Bug 的人，每一个漏洞的奖励金额从2.56美元开始，之后每发现一个 Bug 都会翻倍，直至327.68美元封顶。&lt;/p>
&lt;p>TeX 的输出文件被称为 DVI(Device Independent) 文件，DVI 可以作为一种界面描述的中间格式，通过它可以再进而转换成 PDF 格式。&lt;/p>
&lt;p>为了区分概念，我们应当将高德纳写的 TeX 软件分为 TeX 语法 和 TeX 编译器。虽然高德纳自己写了一个 TeX 编译器，但其它人依旧可以在不同平台自己编程实现 TeX 语法的编译器。为了保持语法的稳定，TeX 有一组严格的测试文件，如果测试文件文件的输出结果不同于预定的结果，那么这个排版系统就不能够被称为「TeX」。这些不同的 TeX 编译器我们都称之为 「 TeX 引擎」。&lt;/p>
&lt;p>TeX 目前(2018年)有如下几个编译引擎:&lt;/p>
&lt;ol>
&lt;li>TeX : 高德纳最早开发的官方实现，只能编译成DVI格式。&lt;/li>
&lt;li>pdfTeX : 支持直接编译成 PDF 格式。&lt;/li>
&lt;li>XeTeX : 支持 Unicode 编码和直接访问操作系统字体。&lt;/li>
&lt;li>LuaTeX : Lua 实现的 TeX 编译器。&lt;/li>
&lt;/ol>
&lt;p>虽然 Tex 出于向下兼容考虑要求了所有编译器都需要能够编译历史上所有符合标准的 Tex 文件，但并不意味着它不能增加新的功能。TeX 作为一门「宏语言」，能够使用宏定义出新的语法，甚至能够覆盖原先的语法。要理解这一点必须要先理解什么是「宏编程」。&lt;/p></description></item><item><title>Kafka 的设计与实践思考</title><link>https://blog.joway.io/posts/kafka-design-practice/</link><pubDate>Mon, 16 Apr 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/kafka-design-practice/</guid><description>&lt;p>前几天看了 librdkafka 的&lt;a href="https://github.com/edenhill/librdkafka/blob/master/INTRODUCTION.md">官方文档&lt;/a>，这篇文档不仅仅讲解了如何使用 Kafka ，某种程度也讲解了分布式系统实现的难点和使用细节，故而让我对 Kafka 的实现原理产生了浓厚的兴趣。&lt;/p>
&lt;p>这篇文章从 Kafka 的设计到使用做了一些个人总结，围绕真正实践场景，探寻其设计上的智慧与妥协。&lt;/p>
&lt;h2 id="设计">设计&lt;/h2>
&lt;h3 id="架构设计">架构设计&lt;/h3>
&lt;h4 id="zookeeper">Zookeeper&lt;/h4>
&lt;p>Zookeeper 存储了 Kafka 集群状态信息 。&lt;/p>
&lt;p>Zookeeper 还负责从 Broker 中选举出一个机器作为 Controller, 并确保其唯一性。 同时, 当 Controller 宕机时, 再选举一个新的 。&lt;/p>
&lt;p>在 0.9 版本之前，它还存储着 Consumer 的 offset 信息 。&lt;/p>
&lt;h4 id="broker">Broker&lt;/h4>
&lt;p>接收 Producer 和 Consumer 的请求，并把 Message 持久化到本地磁盘。&lt;/p>
&lt;p>集群会经由 ZK 选举出一个 Broker 来担任 Controller，负责处理各个 Partition 的 Leader 选举，协调 Partition 迁移等工作。&lt;/p>
&lt;h3 id="内部组件设计">内部组件设计&lt;/h3>
&lt;h4 id="topic">Topic&lt;/h4>
&lt;p>逻辑概念，一个 Topic 的数据会被划分到一个或多个 Partition 中。&lt;/p>
&lt;h4 id="partition">Partition&lt;/h4>
&lt;p>最小分配单位。一个 Partition 对应一个目录，该目录可以被单独挂在到一个磁盘上，以实现IO压力的负载均衡。同时多个 Partition 分布在多台机器上，也实现了灵活地水平扩容。&lt;/p>
&lt;p>每个 Partition 都能够拥有一个或多个 Replication 副本。创建 Topic 的时候能够指定每个 Topic 的 Replication 数量，来保证高可用，Replication 数量为1时，即没有副本，只有其自身 (所以其自身也算是一个 Replication )。其中一个Replication 被选举为 leader。如果leader挂掉了，也会有相应的选举算法来选新的leader。&lt;/p></description></item><item><title>欧游散记 —— 民主专制下的德国</title><link>https://blog.joway.io/posts/deutschland-democracy-authoritarian/</link><pubDate>Wed, 07 Mar 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deutschland-democracy-authoritarian/</guid><description>&lt;p>关于德国中文媒体有过许多的报道和吹捧，总体来讲这个国家属于那种班级里的乖学生的形象，至少在二战后，德国几乎没有得罪过全世界的任何一个国家任何一个宗教，反而还广受曾经敌人的好评。如今这个年头，这种人畜无害的大国真的不多甚至可以说绝无仅有了。&lt;/p>
&lt;p>欧洲国家大大小小有很多，其文明程度也大相径庭。但有一个比较容易的辨别方式是，但凡是说德语的国家，基本上文明程度都不会差，例如奥地利、比利时、瑞士。&lt;/p>
&lt;p>但德国令人尤其是中国人讨厌的，也恰恰是其所谓的文明。甚至以偏激角度来看，德国所谓的文明，恰恰是一种统治手段，只不过这种统治是所有人统治所有人 。所以我称之为“民主专制”。&lt;/p>
&lt;p>我以几个例子作为这种民主专制的说明。&lt;/p>
&lt;p>德国法律规定，雪天时，房屋所有者要在7点到22点期间保持自己屋边人行道的干净状态，且清理出的路要有1.2米以上宽，若有行人因路面打扫不干净而导致摔倒，有权要求房主赔偿。这类法律的实质是市政府无法负担高额的人力清洁成本，由此转嫁给个人，其立法的道德依据是所有人都必须对自己的周边环境负责，其受益者和执行者都是所有人。这类法律摊开讲，其实就是一个 rule ，既是规则，也是统治。&lt;/p>
&lt;p>德国的民主专制思想还体现在宽带通信上。我们知道，宽带其实是一个公共服务，一个区域的总带宽是有固定限制的，如果一个人宽带占用的过多，必然会影响其它人。国内的运营商一般的做法是，每个人可以自己购买不同的带宽上限，高峰时候大家等同比例地下降服务质量，有VIP客户另说。事实上这个策略是非常公平的，技术实现也是最简单容易的。但德国电信的脑回路显然不一样，目前是2M/s的宽带每个月超出75G的流量后，下降到48kb/s。这个规定是什么时候实行的呢，2016年。你无法想象在2016年使用 48kb/s 的网络能打开个什么网站。其规定的依据是认为，大部分客户都不会超出75G月流量，而超出的客户大多是由于经常使用youtube等大流量应用，正因为他们平时本身占用了大量的带宽，故而限速是符合公平原则的。从理论上我们没法去辩驳这种说法本身，但我相信任何一个在中国数字社会生活过的人都不会去接受这种做法吧。用我们党的话来说，就是这个问题的本质是落后的电信基础设施赶不上人民群众日益增长的文化娱乐需求。德国人过度依赖使用民主专制去解决不公平的问题，而忽视了问题本身存在的原因。那些经常占用大量带宽上 youtube 的人可能恰恰就是在辛苦工作给你养着那些月宽带量不到100M的老年群体的年轻用户。在世界上绝大部份发达不发达国家都已经意识到互联网是新时代的水和电的今天，很难想象这个发达国家老大哥居然还会出台如此落后的规定，更何况即便不限速这个国家的宽带水平也远远落后其它周边发达国家。&lt;/p>
&lt;p>我居住的街区有几个垃圾桶，一开始物业怀疑是别的街区的人也过来丢垃圾所以导致经常满，于是大张旗鼓地重新建了几个带锁的垃圾箱，每个人丢垃圾要先开锁。后来发现垃圾箱还是经常满，但是满了也不能不丢，于是大家就把垃圾放在垃圾桶外面，等垃圾车来了自己拿进去。但既然你可以放在垃圾桶外面，这个锁本身也就没有了存在的必要。这个和宽带是一个道理，出了问题只想着维护秩序，不去想着造个更大的垃圾桶，最后就是这个下场。&lt;/p>
&lt;p>德国人非常喜欢用条条框框来规范化整个社会，并且已经到了病态沉迷的地步了。这个游戏最让人上瘾的一点在于，当一个社会出现矛盾的时候，依靠着它熟练的制定规则经验，可以迅速把这个矛盾化解到每个人身上，由此一来再大的矛盾也被消解完了。在两德合并时候，东西德的贫富差距矛盾简直是大的不能再大了，在《再见列宁》里有深刻描写，但西德先是强制规定东德马克2:1兑换成西德马克，后来又强制对西德人民实行征收 5.5% 的“团结税”一直延续到今天。两德合并的矛盾都能被这么消解，还有什么不能困难不能消解的呢。&lt;/p>
&lt;p>哈夫纳在其《一个德国人的故事》里描述了希特勒上台前和上台时，普通德国人的感受。我从其描述里所感受到的德国和如此并无差异。德国人不认同希特勒，不认同各种主义，他们所认同的是其维系文明运转的整套体系。但当希特勒通过合法合规的方式上台，其方式之正当让德国人哑口无言。即便是到了纳粹开始颠覆法院的时候，德国人还在一个劲思考、讨论，甚至文学家开始书写起了乡村治愈文学，以求给困境中的德国人一点心理安慰。这种做法在我看来，和在21世纪通过限速宽带来实现公平别无差别。&lt;/p>
&lt;p>但同时不可否认的是，相比于其它国家的制度，民主专制也不失为一个非常好的维系社会运转的机制。甚至可能这就是文明的定义。因为正是这种机制，导致德国成为了如今世界上最发达也最文明的国家之一。但当身处于其中的时候，我又不禁怀疑，我们所真正想要的到底什么？公平是否能够以消灭个人利益为手段而存在 ? 民主是否能够让我们社会运转地更好 ? 一个依赖集体利益权衡出来的社会形态是否真的是我们作为个体所想到达到的那个 ? 个人价值是被民主所发挥了还是被民主所专制了 ? 这些问题我都没有答案。我所能够确定的，仅仅就是我们大部分人都希望活得久，活得好。而以此为目的来倒推以上问题的话，不得不承认，的确是存在许多千差万别的实现途径的 ，如此的话，那么很多名词的褒贬色彩可能的确需要我们去重新考量一下了。&lt;/p></description></item><item><title>欧游散记 —— 维也纳</title><link>https://blog.joway.io/posts/vienna/</link><pubDate>Sun, 04 Mar 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/vienna/</guid><description>&lt;p>刚到维也纳车站的时候，有种回到了德国的错觉。维也纳是继柏林之后的第二大德语区，同时在一些标识牌等市政设施上也和德国相近。奥地利的人均GDP水平甚至还要比德国高。&lt;/p>
&lt;p>维也纳给我最大的感受就是干净和自动化。&lt;/p>
&lt;p>维也纳市政厅门口广场底下那个公共卫生间可能是我有生以来见过的最最干净的免费卫生间，中间居然还有一个环卫工人办公室。我在那个卫生间有足足驻足欣赏了10分钟之久，一方面是想观察它到底有多干净，另一方面是想弄明白究竟是怎么一种操作能够使得一个闹市区的免费卫生间能够达到如此的干净程度。&lt;/p>
&lt;p>维也纳的自动化程度也是在欧洲国家里极其罕见的。印象最深的是，我在从进入机场到上飞机，基本上没有秏费任何一个劳动力的协助。很多机场再智能最后还是要一个乘务员来检票和检查护照，但维也纳机场是自己扫码上机，并且不需要检查护照。去超市买东西，也都是完全自主扫码结单，大部分工作人员都在忙着上架商品，只有一个收银台是在人工运营。我在维也纳住的一个青年旅舍，在到之前它邮箱发给了我一个Code，我到了以后可以用那个Code开旅馆大门、我房间门和我床位的锁，第一次开就代表了Check In完成。去客厅买食物也完全是售货机。在旅馆的两天，除了清洁工以外，我没有见到过一个工作人员，这种体验真的是太完美了。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520108053.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>青旅&lt;/p>&lt;/blockquote>
&lt;p>作为全球最宜居的城市，维也纳无论是环境还是市政都配得上这个称号。如果全世界让我随便移民的话，可能首选就是维也纳。尤其是如果我还有孩子家庭的话，维也纳几乎可以算是不二之选。整个城市有大量的植被覆盖，公园密度覆盖高，公共交通极其发达，冬天还有各种自造的溜冰场 。较之伦敦巴黎纽约又没有那么多的游客，而且奥地利这个国家本身在国际社会上也非常低调，几乎可以算是岁月静好的现实定义了。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520108392.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>市中心溜冰场&lt;/p>&lt;/blockquote>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520109185.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>公园&lt;/p>&lt;/blockquote>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520109267.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>植被覆盖&lt;/p>&lt;/blockquote>
&lt;p>维也纳还是国际间谍之都，据称有7000多家间谍机构注册在案。碟中碟很多场景就在维也纳拍摄。一方面当然是由于奥地利是永久中立国，另一方面可能也是因为它非常适合海外间谍举家居住吧。&lt;/p>
&lt;p>在哈布斯堡王朝的时候，维也纳也曾是整个欧洲的文化政治中心，即便是当年希特勒的年代，他也还是要前往维也纳来考取艺术学院。二战的时候，德国吞并了维也纳，导致维也纳成为了纳粹德国的一部分。二战结束，维也纳陷入被苏联和西方战胜国共同占领的命运，知道1955年奥地利才完全独立。二战后，维也纳人口大量减少，反而成为了一个移民城市。之后巴黎逐渐取代了她的地位，成为了如今的欧洲时尚之都。维也纳曾经的辉煌虽然不再，但这座城市本身还依旧保留着浓烈的贵妇人气质。&lt;/p></description></item><item><title>欧游散记 —— 西班牙</title><link>https://blog.joway.io/posts/spain/</link><pubDate>Sat, 03 Mar 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/spain/</guid><description>&lt;p>西班牙是我最喜欢的一个欧洲国家之一，坐落在伊比利亚半岛，位处欧洲与非洲的交界，最南处离非洲仅15公里，西接大西洋东接地中海。如此的地理位置决定了西班牙从史前时期就是一个多民族多文化交融的地方，即便今天还能看到这种交融的痕迹。&lt;/p>
&lt;p>公园前1世纪，罗马人驱逐了伊比利亚半岛上的凯尔特伊比利亚人，并划分为近西班牙省、远西班牙省。在西班牙建立了完善的市镇体制和基督教信仰。西班牙虽然当时只是罗马的两个行省，却诞生过三任罗马皇帝，其中最著名的有图拉真和哈德良皇帝。如今在塞哥维亚还能看到保存良好的古罗马水道桥。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520026320.png?tr=w-1024" alt="古罗马水道桥">&lt;/p>
&lt;blockquote>
&lt;p>塞哥维亚古罗马水道桥&lt;/p>&lt;/blockquote>
&lt;p>公元5世纪，罗马帝国衰落，西哥特人控制了伊比利亚的大部分土地，建立西哥特王国，并定都托莱多。西哥特国王之后也改信了罗马天主教。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520026996.png?tr=w-1024" alt="托莱多">&lt;/p>
&lt;blockquote>
&lt;p>托莱多古城&lt;/p>&lt;/blockquote>
&lt;p>公元8世纪，北非阿拉伯人(摩尔人)入侵西班牙，开始了为期八百年的伊斯兰统治。摩尔人统治时期，西班牙的建筑、文化、经济都取得了辉煌的成就。&lt;/p>
&lt;p>在摩尔人统治时期，基督教和伊斯兰教互相斗争，在伊比利亚半岛中部形成了大大小小诸多王国。摩尔人统治的南方地区如今统称为安达卢西亚 (即今天的格拉纳达、塞维利亚、加的斯、科尔多瓦等省)，北方的基督教王国里最大的两支是卡斯蒂利亚和阿拉贡，在不同时期还前前后后有诸多王国涌现。西班牙这种各立山头的历史背景也导致了直到今天，他的各个省市都会根据自己的历史背景设计徽章，很多就是从古时候的王国国徽里演变过来的，有非常强的地区文化认同感。&lt;/p>
&lt;p>伊斯兰教长达800年的统治使得如今的西班牙保留了非常浓烈的阿拉伯风格，在安达卢西亚的大街小巷都能够看到非常细碎且严格对称的花纹，一些庭院里都会有非常精巧的水道设计，很具有沙漠民族色彩。即便是在基督教教堂，由于很多教堂当时都是在原先摩尔人遗留下来的清真寺上建的，所以也保存了相当多的伊斯兰元素，尤其是在庭院设计上。&lt;/p>
&lt;p>在伊斯兰统治时期，穆斯林对异教十分宽容，使得西班牙同时居住着大量犹太人。今天我们去一些老城区还会看到很多精致幽静的社区，都是当年犹太人遗留下来的痕迹。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520027310.png?tr=w-1024" alt="">
&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520027376.png?tr=w-1024" alt="">
&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520027245.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>格拉纳达的阿尔罕布拉宫&lt;/p>&lt;/blockquote>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520027475.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>格拉纳达的摩尔人庭院&lt;/p>&lt;/blockquote>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520027588.png?tr=w-1024" alt="">
&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520027800.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>塞维利亚的摩尔人王宫&lt;/p>&lt;/blockquote>
&lt;p>公元15世纪，卡斯蒂利亚公主伊莎贝尔一世和阿拉贡王子斐迪南二世联姻，被称为“天主教双王”。联姻使两人得以共同统治绝大部分西班牙领土，并开始了基督教的复兴。此时的哥伦布在经过十几年的游说后，终于在1492年与伊莎贝尔和斐迪南的王室达成协议，王室同意资助他的旅行并允许他将从新土地的总收入中提成10% 。哥伦布的遗骨最后葬在了塞维利亚的主教堂里。有趣的是，哥伦布当年只是逝世在一个普通的旅馆里，而教堂里所展现的却是一幅国葬场面。&lt;/p>
&lt;p>基督教重新掌权后，放弃了穆斯林之前的开明宗教策略，不仅改建或摧毁了清真寺，还开始了对犹太人大规模的驱逐和屠杀，导致西班牙犹太人开始了新的流亡生涯。今天北美就有很多西班牙犹太人后裔。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520028281.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>塞维利亚全景&lt;/p>&lt;/blockquote>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520028209.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>塞维利亚主教堂&lt;/p>&lt;/blockquote>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520027989.png?tr=w-1024" alt="">&lt;/p>
&lt;blockquote>
&lt;p>塞维利亚主教堂里的哥伦布遗骨&lt;/p>&lt;/blockquote>
&lt;p>公元16世纪和17世纪，西班牙开始了殖民旅途，成为了第一批堪称“日不落”的国家，同时也让西班牙语成为了今天世界上第三大语种。在这期间的大部分时间里，西班牙都是整个欧洲最强盛的国家。但西班牙的王室并没有把大把掠夺来的财富用于本国发展，而是挥霍在各种物质生活和发展军备上，导致其经济实力远远落后于其军事实力。随着北方法国的崛起，西班牙日渐式微。在18世纪初始爆发了著名的“西班牙王位继承战争”。最终西班牙落入了波旁王朝手中，一直延续到了今天。&lt;/p>
&lt;p>今天的西班牙从经济上早已落魄，失业率高达17%。德国是3% , 再不济的意大利也只有11% 。但西班牙的旅游业实力在全球是排名第一的。一方面由于其物价水平较之大部分欧洲国家都算是非常低的，我记得同一个薯片在德国要2欧，在奥地利要2.5欧，在西班牙只要1.2欧。另一方面也归功于其独特复杂的历史文化背景，使得其对全世界人民都能产生强烈的异国风情体验，这也是为什么很多游戏喜欢以西班牙作为题材背景的原因。&lt;/p>
&lt;p>从治安上讲也远远好于西边的葡萄牙和北边的法国，并且人种相对单纯很多，没有太多种族问题。&lt;/p>
&lt;p>在意识形态上，西班牙非常的“中国”，完全不理睬北方欧洲佬的那一套动物保护主义环境保护主义种族平等主义的价值观，基本上是怎么舒服怎么来。马德里火车站直接把上百只乌龟养在了一个池子里，看着十分壮观，但在北欧显然肯定会被动保的人喷死。街头的垃圾桶基本上也没什么分类要求，都是随便丢，我好像也只在巴塞罗那的居民区看到过垃圾分类的标识。在西班牙基本上看不到多少黑人，人种非常纯粹，不会有那么多移民问题难民问题。西班牙人总体给人的印象就是一个非常实用主义和放松的民族，这点和中国非常相似。&lt;/p></description></item><item><title>欧游散记 —— 德国国会大厦</title><link>https://blog.joway.io/posts/deutschland-reichstag/</link><pubDate>Fri, 02 Mar 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/deutschland-reichstag/</guid><description>&lt;p>德国国会大厦 ( Reichstag ) 是柏林的标志性建筑，在1894至1933年间先后用作德意志帝国议会和魏玛共和国议会。1933年发生了著名的“国会纵火案”，希特勒借此大力渲染以促成兴登堡总统签署《国会纵火法令》，废除了魏玛共和国《宪法》里的诸多公民的权力和自由，后来亦成为监禁反对人士和镇压不与纳粹政权合作的报刊的法律依据。之后国会大厦一直处于废弃状态，直到1990年，两德统一，西德决定将首都从波恩迁回到柏林，并确定国会大厦为议会地址。之后开始了对国会大厦的改建工作。&lt;/p>
&lt;p>国会大厦对公众全面开放，可以在网上预约申请，参观分为穹顶和内部导览，内部导览会有一个议会工作人员来为你介绍里面各个部分的历史和功用，也会以一个德国公民的角度给你吐槽现任各个领袖的特点、习惯甚至是工资待遇。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520016030.png?tr=w-1024" alt="讲解员">&lt;/p>
&lt;p>进门的大厅中央是议会大厅，顶部悬挂着一只巨大的“联邦之鹰”，这个鹰比其国会和机关文件上的鹰都要丰满很多，所以也被称之为“胖母鸡 ( Fette Henne )”。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520015869.png?imageMogr2/thumbnail/!50p" alt="议会大厅">&lt;/p>
&lt;p>议会大厅的顶部是著名的玻璃穹顶，原先旧有的玻璃穹顶在1945年被空袭炸毁，直到1991年改建的时候才又重新加上。议会大厅从周围墙壁到穹顶大部分面积都是采用玻璃材料，除了确保室内自然采光意外，也凸显了其政治透明。当议会正在开会时，虽然内部参观会取消，但公众依旧可以登上穹顶观看自己所在选区议员的一举一动。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520016255.png?tr=w-1024" alt="穹顶仰视">&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520016167.png?tr=w-1024" alt="穹顶内视">&lt;/p>
&lt;p>在边上的走廊里，有一面墙，墙上涂满了当年攻克国会大厦的苏联士兵的涂鸦，有人刻下了自己的名字，有人刻下了久别的女友，有人刻下了自己的家乡，当然也有各种污言秽语。在翻修国会大厦的时候德国人还争论过是否要把这些涂鸦抹掉，毕竟把这些羞辱性的话语留在墙上有失一个议会的尊严，但最后还是决定保留了下来。试想天天路过这些涂鸦的议员们，怎么可能还会去表决发动一场战争。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520016357.png?tr=w-1024" alt="苏联涂鸦">&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520016404.png?tr=w-1024" alt="苏联涂鸦">&lt;/p>
&lt;p>再往里走是一个供议员们使用的祷告室，侧角还有一个供穆斯林使用的朝向麦加的小空地。这个房间里陈放的艺术品用于帮助议员在沉静中冥想，以做出对自己对选民对信仰负责的决定。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520016458.png?tr=w-1024" alt="祷告室">&lt;/p>
&lt;p>国会大厦四周是柏林政治中心带，有默克尔办公的德国总理府，草拟议会文件的议会大楼，监督政府和议员的各选区地方报纸驻柏林的记者大楼，还有一些联邦政府机构办公区域。身处其中能够感受到这个国家的政治正如一台精密的机器一般运作着。&lt;/p>
&lt;p>国会大厦只是这台机器的外壳，德国人在选举制度上的设计也非常的精密。德国是代议制民主，由议员选举出总理。这个一定程度避免了像美国大选时候那样靠比拼广告费和心理学钻营去赢得普通人民的关注。其次，人民拥有两张选票，一张投给该选区议员，一张投给政党。议会席次里，一半是直选议员，一半是拿到了政党投票的政党自己选拔出的议员。打个比方就是，我认可这个保守党议员但我不认同他所在的党派价值观，所以我可以既尊重我的个人喜好也同时保留我的意识形态观念，对于议员也更加可以在党派立场上有更多的个人色彩 。德国还允许没有达到绝对多数的政党组成联合政府，更能体现这种民主的妥协性而不是一锤子买卖。在德国的选举法上还有很多细节也非常耐人寻味，其归根结底的目的，都是在最大限度地让民意能够100%传达到议会格局上 。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1520016529.png?tr=w-1024" alt="德国选票">&lt;/p></description></item><item><title>社会矛盾讨论统一框架的一种可能性</title><link>https://blog.joway.io/posts/right-theory/</link><pubDate>Mon, 12 Feb 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/right-theory/</guid><description>&lt;p>这篇文章只是在飞机上做的一个思维游戏，标题取的太大，没有什么严谨性，不一定代表本人立场，看看就好。&lt;/p>
&lt;hr>
&lt;p>如今在社交媒体上去讨论 “女权主义”、“种族主义”、“宗教自由” 等问题都是一件非常危险的事情。每一个话题里都会有水火不容的两派观点，即便是在同一个立场上，还会存在基于不同逻辑演化出的不同分支观点 。社交媒体上普遍存在的一类人就是在自己国家政治上反对独裁，对西方国家民主又报以鄙夷，向往信仰自由的同时又张口闭口以“绿绿”来蔑称穆斯林。这很大程度是由于缺乏一个值得推敲的框架来思考的结果。&lt;/p>
&lt;p>女权主义，人人平等，种族主义，但在这些繁复的名词下，我认为是存在一个普适的框架来规范彼此的讨论的。&lt;/p>
&lt;p>小说家在构造人物的时候，通常会赋予他性别，长相，身高，种族等外在因素，还会用一系列事件去刻画他的为人和三观。人就是在诸多附于其上的属性的共同合力之下才之所以为其人 。武则天女性的身份让她成为了中国历史上的一个独特的存在，奥巴马的黑人身份也帮他赢得了不少的认同。但这些仅仅只是他们身上存在的一个属性，在特殊的历史时期的确不同的属性的权重会略微大一些。&lt;/p>
&lt;p>如果你把性别，种族，宗教都当作是一种互相平等的附加属性，很可能就能够讨论清楚社交媒体上的诸多话题了。我尝试性的建立了两个“定理”，试图用这两个定理来理清常见的社交媒体争论：&lt;/p>
&lt;blockquote>
&lt;ol>
&lt;li>一个人应当由多方位的属性来定义其人。&lt;/li>
&lt;li>各个属性之间只有因外界影响而导致的权重占比不同，但属性本身之间没有高低之分。&lt;/li>
&lt;/ol>&lt;/blockquote>
&lt;p>第一条定理我相信所有受过教育的人都会承认。第二条定理我这里解释下，比如是否色盲和是否是黑人是两个属性，如果我是一个UI设计师，显然是否色盲比是否是黑人对我的职业和生活影响都大的多，但并不意味着色盲这件事情本身就比种族因素要重要。当我去从事作家工作的时候，这两个因素的权重又都会降的很低，相反文字能力这个属性的权重就升高了。所以我说属性的权重是会动态变化的，而属性本身是没有高低贵贱之分的。&lt;/p>
&lt;p>如果你不同意上述两个定理，你可以辩驳定理本身。如果你认同定理但不认同下述推导，你可以基于定理提出你的推导过程。但请不要直接拿你的那套理论来直接辩驳下面的推导结果，那样会显得你很没有科学素养。&lt;/p>
&lt;p>我们先以职场领域来做一个推导。马云说 : “ 女人爱商比男人高，所以阿里巴巴更喜欢招女性。” 这句话的错误在于，用性别和爱商是两个或许具有统计学上的相关性的属性，但它们之间并没有因果性。但如果阿里首先通过性别来筛选简历，不招男性，然后在女性里进行爱商维度的面试。这样我觉得是合理的。阿里根据自己的经验去认为女性比男性爱商高，然后在自己的企业里进行这种效率筛选完全合法合规。就算他遗漏了那些爱商高的男性，但他提升了效率。如果在这个问题上去宣扬歧视男性的话，同样可以在这个问题上去宣扬歧视爱商低的人，因为根据第二条定理，各个属性是没有高低之分的。如果你是一个爱商高并且就铁了心一定要进阿里的男性，你能够做的不是去宣扬阿里歧视男性 —— 因为阿里并没有歧视，它就是想要提高效率 —— 而是去向阿里展示你是那个违反他们经验的例外，或者直接证明他们的经验是错的。&lt;/p>
&lt;p>在教育领域，美国的平权法案为黑人群体提供了优于其它族裔的机会，中国有少数民族加分政策，在这两个案例上，我们用上面两个定理来看，假设大学看重的是一个人的“知识储备”、“学习能力”、“种族”等多维度属性。如果这个大学就是有一个愿望是提升校园里的种族多样性，以提升学生对其它种族的了解程度和接受程度，从而循序渐进促使整个社会更加能够容纳其它种族，那么的确种族会在大学考量学生的时候占据很高的权重。而大学也有权力依据自己的需求来选择自己的学生。这个和弥补历史上翻下的错误没有关系，完全是实用主义的角度来考量。&lt;/p>
&lt;p>仔细观察上面的例子，显然会发现它其实忽略了公平原则 。但是公平原则是一个人为概念，且这个概念自己就不能逻辑自洽。例如显然我们需要在选举的时候筛选掉蠢的那批人，而这种筛选就是对智力低下者的不公平。如果你要来辩驳这个说法，你要么承认蠢的人也有资格当选leader，要么承认并不是所有的属性都应当具备平等的重要性。当你说出后者的时候，你本身就是站在一个不公平的视角下在谈论公平。&lt;/p>
&lt;p>公平从来都是一个伪概念。在古代的贵族社会里，人人都认可特权的存在，公平的争议只会暴发在贵族与贵族之间，平民与平民之间，奴隶与奴隶之间。有兴趣的人可以去观摩下汉谟拉比法典，与其说是法典不如是一个时代对公平的定义手册。即便是现代社会，诸多社会关系也可以映射到汉谟拉比法典的框架中去，只不过相比古代，现代的平民数量大幅提升，平民内部的阶级也越来越多，越来越细。富人，官人，高智商者，艺术家，技术从业者，教师等标签的背后，都是资本、权力、智力、创造力、消费能力、教育资源分配的不均匀，教师赚钱难，程序员小孩上学难，艺术家独立难等等都是现代不公平生活的写照。公平是一个伟大的共产主义理想，但也通过实践证明了并不是一个社会良好运转的好机制。如果你能够承认不公平是一个合理的正常态，也就能够认同上面定理所推导出来的世界。&lt;/p>
&lt;p>我前面说了，这两个定理是试图为社交媒体上的意识形态交流建立一个可沟通的框架，而不至于陷入混战。同时也能够将各方的利益诉求化解到同一个目标上，让斗争更为合理。&lt;/p>
&lt;p>比如在女权主义的话题下总会有人提到机会平等。那么如果对性别机会平等了的话，不同学历者是不是也要机会平等，不同能力者是不是也要机会平等 ？有些人说性别的差异不是能够选择的，能力则不同。说这话的人大概是忽视了所处国家，家庭背景，个人遭遇，智力这些东西也同样不是能够选择的这一事实了。机会平等是你依据自己的私欲想要的一个结果，而非你所遭遇到的问题本身，也不是一个解决问题的办法 。你所遭遇到的问题本身是社会上存在这种基于性别的职业偏见，而这个偏见在男性女性下都存在 。如果你的宣传点都局限于争取女性的权力，从理论角度来讲就是错的，从实用角度来讲，我一个直男每天996的工作，好不容易上个网为什么要为你去发声呢 ？但如果你宣扬的是消灭基于性别的职业偏见，那你的努力就是在减少性别在一些领域的占比，让大家用更为科学的方式来筛选人，而不是追求以公平的方式。而选择以这种方式斗争的话，能够争取到的支持者显然更为广阔。我所处的计算机行业就是这种科学斗争的典型，以前大家很流行去考个硕士博士，后来许多本科生，专科生通过自己的能力证明了自己从而降低了学历在面试时候的权重，导致现在大部分互联网公司都不会去卡学历了。这种做法是否正确可能有待商榷，但觉得不爽的大可通过自己的实力再把这个行业给扭转过来，这个就是一个互相博弈，科学进步的过程。博弈的目标是去改变权重占比，而不是去追求公平。&lt;/p>
&lt;p>比如在地域歧视上总是有人利用道德压力去阻止人开地域歧视的玩笑，而不是去改变这个地域本身给人的印象。如果从公安数据上统计出来有相当比例的偷盗都是X省的人干的，那显然偷盗就是X省的特色，哪个省份没点黑料，有什么可以辩解的呢 ？如果你要反对，那么对象应该是以地域来判断一个人的权重，这种斗争方式总比道德绑架要文明的多了。&lt;/p>
&lt;p>按照上面这种方法去思考的话，许多垂直领域里的矛盾最终本质都能够被归结到一个普遍存在的社会矛盾上，斗争方式也更加成熟。即便是在独裁国家也有大量通过舆论和媒体改变社会偏见的案例，而这些案例的普遍特征是斗争目标极为具体和单一。&lt;/p></description></item><item><title>欧游散记 —— 伪君子布拉格</title><link>https://blog.joway.io/posts/euro-prague/</link><pubDate>Sun, 11 Feb 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/euro-prague/</guid><description>&lt;p>小时候很喜欢米兰昆德拉和卡夫卡，布拉格一直是一个我心中非常神往的城市。但在踏上布拉格土地的那一刻，这个城市带给我的只有无穷无尽的失望和可笑。&lt;/p>
&lt;p>在历史上，布拉格早期是波西米亚的首都，中世纪也曾两度成为神圣罗马帝国的首都，即便如此，这个城市里也并没有留下什么让人眼前一亮的东西。虽遍及了哥特式、巴洛克式的建筑，但据有代表性的并没有多少。其自称文艺之都，但鲜能找到具有真正文化含量的东西。作为如今捷克共和国的首都，一点也没有首都应有的样子。而同为文艺之都的巴黎和维也纳就和布拉格截然不同，他们都保持了首都应有的威仪和典雅，并不故意去迎合游客，游览这些城市就像观看一本小说，需要自己去细细体会。而布拉格就像是一个全身按摩，用非常故意和夸张的力道来让你全身心感受到“文艺”的的薰陶。&lt;/p>
&lt;p>布拉格的所谓的“博物馆” 典型就是那种实在没什么景点好开了，就生搬硬凑来充数。比如 “Apple Museum” ，可能是这个世界上最僵硬可笑的博物馆了 ，搞得 Apple 和布拉格有什么莫大关系似的 。还有一个“性器博物馆”，虽然我承认这些博物馆本身有存在价值，但是其展品质量和数量实在难以同其票价相等价。关键是这些博物馆是没有布拉格特色的，而在布拉格，我还真没有找到一个足以展现整个布拉格城市发展史的博物馆，而我认为这是一个古城和首都的必需品。&lt;/p>
&lt;p>最让我无法接受的是在卡夫卡的22号故居里，贪婪到极限的捷克人居然在其房间里铺满了各种用来销售的明信片和纪念品，以至于完全看不出这个狭小故居原来的模样，和普通小店没有区别。布拉格人在卡夫卡活着的时候不能去发现他，在他死后还要用如此竭泽而渔的方式去利用他，用卡夫卡所恶心的方式去营销卡夫卡周边，其流氓程度在欧洲国家里实属罕见。&lt;/p>
&lt;p>&lt;img src="https://ik.imagekit.io/elsetech/blog/images/old-blog/1518303322.png?tr=w-1024" alt="">&lt;/p>
&lt;p>布拉格的人更是不怎么靠谱。约好的2点钟会2点30分人才到，在网上po出了住宿信息电话留的却是别人的。满大街的货币兑换点明明写着0手续费，却在汇率上做手脚坑游客。我住的还是一个四星级的酒店，而check out 的时候工作人员连自己的酒店房间是怎么样的都弄不清楚，明明只给了我钥匙还死命问我要房卡。&lt;/p>
&lt;p>布拉格郊区有一个佩特任瞭望塔，仿照巴黎艾菲尔铁塔建造，但只用了4个月就完工了，高度仅为60米。而艾菲尔铁塔是320米。这座铁塔在我看来充分体现了布拉格人的精神，空有装逼的心却缺乏装逼的能力，最终就做了一个不伦不类的东西潦草结束，纸上谈兵的典型。&lt;/p>
&lt;p>在近现代，布拉格在世界上一直有着不错的曝光率。比如为中国人所熟知的“天鹅绒革命”。坦率讲，就这么一个小城市加上这么一群不靠谱的人，任何据有煽动性的理论都有成功的可能性 。在咖啡馆高谈阔论一番，争取下这100万城市人口的大多数，就足以颠覆掉这个国家的政府了。而以捷克的“天鹅绒革命”为理论依据来试图进口到国内的中国学者，不是蠢就是坏，因为这两者几乎没有什么可比性。&lt;/p>
&lt;p>捷克紧邻德国和奥地利，地处欧洲中心，伏尔塔瓦河贯穿整个国家。从地理位置上看占据了非常大的优势，但这个国家的人均GDP只有奥地利和德国的一半不到。真的是空有一身好皮囊，白白被这些莽夫给糟蹋了。&lt;/p>
&lt;p>值得注意的是，与卡夫卡同样著名的作家米兰昆德拉在布拉格却几乎没有任何官方宣传的痕迹，我想这很可能是因为昆德拉一直坚称自己是一个法国作家而与祖国捷克决裂的原因吧。但如果政府单单就因为这种原因而去掩盖一个世界级作家在这座城市的痕迹，未免有点太过小气。&lt;/p>
&lt;p>总的来说，布拉格人把他们的城市当作了一个迪士尼在运营 ，然而做的却还没有人家迪士尼做的好，这真的是一个可悲的事情。&lt;/p></description></item><item><title>Lemon : Koa 风格的 Python 异步 Web 框架</title><link>https://blog.joway.io/posts/lemon-overview/</link><pubDate>Mon, 08 Jan 2018 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/lemon-overview/</guid><description>&lt;p>前段时间想要写一些简短高效的 API ，背后的工作无非就是一些计算和数据处理，但是可能并发量会比较高。当我在 Python 的生态里去搜寻一些靠谱的 Web 框架时，很难找到一个设计优秀且运行效率高的框架。尤其是当我已经习惯了 NodeJS 的 Koa 那种简洁明了的设计时，很难再喜欢上像 Flask 那种装饰器的写法和各种概念拢杂在一起的设计。最后实在没有办法，就自己写了个符合我个人审美的框架 &lt;a href="https://github.com/joway/lemon">Lemon&lt;/a> 。&lt;/p>
&lt;h2 id="什么是-web-框架">什么是 Web 框架&lt;/h2>
&lt;p>在讲 Lemon 的设计前，我们先来看一看一个请求是如何被响应的 ，以及框架在其中的作用是什么 :&lt;/p>
&lt;p>当一个请求从客户端进入服务器，首先以 TCP 包的形式被接收到，这个时候我们需要手动去建立连接，接收到 TCP 报文后我们需要去判断它的协议，然后再解成相应协议(一般都是HTTP协议)的报文，传给 application 去处理，当处理完后还要把结果写回成 TCP 报文传回去，如果有 keep alive 还需要去手动管理这个连接的生命周期。以上这些统称为 server 部分。&lt;/p>
&lt;p>而和开发者关系最密切的 application 部分做的就是拿到一个 http 或其它协议的报文，然后根据其信息做针对性的响应，传给 server 。&lt;/p>
&lt;p>无论你是使用任何语言任何框架，都逃不开上面这个处理过程。而我们在比较框架的时候，无外乎是对以下几个方面做一些针对性的优化:&lt;/p>
&lt;ol>
&lt;li>TCP报文解析成 HTTP(或其它协议) 报文的效率&lt;/li>
&lt;li>并发策略 (多线程，多进程，协程，线程池，Event Loop)&lt;/li>
&lt;li>application 本身的运行效率 (由框架的效率，所用的语言和使用者自身的代码质量共同决定)&lt;/li>
&lt;/ol>
&lt;p>对于第一点，python有一个叫 &lt;a href="https://github.com/MagicStack/httptools">httptools&lt;/a> 的库就是使用了 NodeJS 的 http parser 以达到高性能解包的目的。&lt;/p>
&lt;p>针对第二点，有许多OS层面和工程层面的技术在致力于解决这个问题。&lt;a href="https://github.com/MagicStack/uvloop">uvloop&lt;/a> 就是其中的一种，采用事件循环的方式，底层用的是 libuv , 同样也是 NodeJS 的底层异步IO库 。&lt;/p>
&lt;p>第三点可能是我们大部分人的考虑的重点，需要各自根据团队情况，在开发效率和运行效率中间进行权衡。作为框架本身，它能够做的极致就是不给 application 拖后腿，而现实中，大部分时间其实都是在运行用户自己编写的代码。&lt;/p>
&lt;p>当我们在谈论一个 Web 框架的时候，更多是在谈论第三点 application 的部分。第一二两点是 server 的实现部分。大部分时候，框架自身并不会去实现一个完整的 server 。为了让我们使用各种框架来进行开发的 application 能够在不同 server 中可以运行，会指定一些接口标准，在 Python 里同步的比如 wsgi , 异步的比如 asgi 。&lt;/p></description></item><item><title>Golang : Make Programming Happy Again</title><link>https://blog.joway.io/posts/golang-talk/</link><pubDate>Sat, 30 Dec 2017 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/golang-talk/</guid><description>&lt;p>之前在公司内部做技术分享写的一个关于 Golang 的 slide ，花费了挺多的时间的，所以就脱敏了发出来。个人觉得值得一看 ，至少 PPT 的设计很不错 :) 。&lt;/p>
&lt;p>Google Doc 地址 :&lt;/p>
&lt;p>&lt;a href="https://docs.google.com/presentation/d/1odpCfHE5dp7acgK_7lkqTPgHlvT-jPg-XtM7-s9WQZw/edit?usp=sharing">https://docs.google.com/presentation/d/1odpCfHE5dp7acgK_7lkqTPgHlvT-jPg-XtM7-s9WQZw/edit?usp=sharing&lt;/a>&lt;/p></description></item><item><title>Podcast 闲言碎语</title><link>https://blog.joway.io/posts/podcast/</link><pubDate>Tue, 10 Oct 2017 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/podcast/</guid><description>&lt;p>Podcast 即常说的「播客」，在广义上既包括音频也包括视频。而狭义却在随着时代变迁出现了不同的偏向。&lt;/p>
&lt;h3 id="前-podcast-时代">前 Podcast 时代&lt;/h3>
&lt;p>在当年博客热火朝天的时候，许多博主会有在博文中嵌入视频的需求，新浪顺势地推出了它的播客频道。这个阶段对于播客的定义接近于「个人上传的视频」。但事实上当时的播客很大一部分都是关于旅行和一些社会事件，即便是旅行也都是在拍摄大自然和风土人情，很少会嵌入个人色彩。有一个如今已经死掉了的视频网站 —— 土豆网当时的 slogan 就叫做 「每个人都是生活的导演」。土豆网最早明白播客的核心价值，但那个时代个人要拍摄一段视频的成本其实是非常高的，大部分手机没有视频功能，即便有拍摄质量也堪忧。再者拍摄完一段视频需要一台起码中等以上的PC才能完成剪辑工作，还需要一个不错的宽带才能花个几小时把视频上传到平台上。而且当时的宽带都不是光纤，上下行速度是不对等的，我记得那时候我的上传速度最高也只能到 30kb/s 。如今以快手抖音为代表的短视频浪潮无非是在延续土豆网当时的 slogan 。比较讽刺的是，现在土豆网的 slogan 却变成了 「召唤全球有趣短视频」。&lt;/p>
&lt;p>在前 Podcast 时代，播客的定义被局限在视频，所谓的音频节目都是些收音机里的夜间电台或者是一些视频节目的音频版本 。而在后 Podcast 时代，我们口语中所谈论的播客更加偏向于音频。&lt;/p>
&lt;h3 id="后-podcast-时代">后 Podcast 时代&lt;/h3>
&lt;p>到了2012年以后，涌现出了许多像荔枝FM之类的音频播客节目。这个时间恰好也是智能机浪潮的开始，也正是智能手机给了这些 App 足够的硬件能力来录制和传播优秀播客节目。包括那个时候的荔枝FM App ，其设计和交互也是我使用过的国产 App 里最好的。我记得当时我还在高中，晚上经常在上面听一些旅行主播聊天南海北的奇闻轶事，那样的日子真的太美好了。&lt;/p>
&lt;p>播客与博客最大的区别在于，当我们在书写文字的时候，经历的过程是 : 思考 -&amp;gt; 书写 -&amp;gt; 阅读写下的内容 -&amp;gt; 思考 。整个过程太过严谨，从而丧失了口语的魅力和乐趣。优秀的播客主播他在言谈的时候一定是极为放松的，也不会太过于去追求言谈的准确性，何况很多时候你意识到自己说的话有不严谨之处的时候，你已经说完话了。&lt;/p>
&lt;p>音频播客的流行并不意味着以前的视频形式落伍了。相反，正是由于视频内容开始真正被普及，使其从播客的定义中被剥离了出来，自成一体。如今的视频内容，往下走有诸如「短视频」之类的叫法，往上走会有人叫 「Vlog」。Vlog 继承了原先播客的精神内核，从表现人与自然过渡到表现自我。短视频则走向平民化和低俗化，从而成为了如今内容的第一载体。&lt;/p>
&lt;p>如果仔细观察这个趋势会发现，技术的发展使得内容的表现能力大幅提升，但问题在于，创作者的能力并没有多少提升，我们的技术并未在帮助创作者提升创作能力这件事情上有多少努力。&lt;/p>
&lt;p>从前文字类的博客对作者的能力要求其实并不高，你有基本的书写能力，表达一个悲惨的个人故事或者一个实际的社会问题，受众多少都能有所感触 。何况我们阅读博客本身就不是怀着阅读文学作品的心态去的。而音频类播客开始对作者的智商、情商、表达能力、反应速度、记忆力、话题掌控能力都提出来不低的要求。就我这七八年里所听过的超过9成的播客节目的作者都没有完全达到上述几点的要求，而且我深知如果我自己做播客肯定连一项都无法达标。而 Vlog 就更加是创作者的噩梦了。我大一时候听过中文系老师讲话剧课，老师训练过我们面向大众以最自然的姿势站立一分钟。在这一分钟里，绝大部分人会在后30s开始东张西望，手不知道放哪里，眼神飘忽不定。我们日常在开会的时候，大家会觉得好像面向大众表达很简单，但如果你站起来面对大家讲话感受就会不一样。如果让大家都目不转睛盯着你看你讲，那又完全是另外一个心态。Vlog 相当于你需要排除路上别人异样的眼神然后面对可能几百个也可能几百万个观众自言自语。同时播客剪辑大部分只是 「剪」，Vlog 还要求「辑 。并且受众对视频内容的挑剔程度会更加高。许多 Vlog 作者自己的脸上就写满了尴尬，很难让人坚持看下去。你甚至都很难在这个 14 亿人口的国家里举出超过 10 个优秀的 Vlog 作者出来。&lt;/p>
&lt;p>国内目前流行的诸如「快手」这类短视频软件里的确有许多不错的作品，但绝大部份属于演绎一个想象中的自己，或是贩卖尴尬，其视频时长和一张 Gif 差不多，内容也很单调。完全称不上上述讨论的任何一个品种范畴。其流行本身就是一个复杂的社会话题，和内容形式的关系并没有坊间所吹捧的那么大。&lt;/p>
&lt;h3 id="我与-podcast">我与 Podcast&lt;/h3>
&lt;p>我从初中开始养成了一个需要听音频才能入睡的习惯，如果不听点什么东西，我就会脑子胡想，只有听播客才能让我大脑放掉戒备。最早我是把袁腾飞的课堂视频转成音频来听，后来发现了了锵锵三人行，就开始从01年的节目开始听。说来惭愧，我的性启蒙以及对待性差异群体的认同度都是听锵锵三人行学来的。只可惜这个节目显然超出了初中生的文化水平从而在今年被掐掉了。大学以后转用 iPhone , 开始在 iOS 的 Podcast 客户端上发掘到了一大批优秀的播客，但正如前述，即便是音频节目，对作者的要求也实在是太高，许多节目要么由于时间原因出现了月更年更的现象，要么由于作者自身涉猎范围有限最后只能不了了之。&lt;/p></description></item><item><title>Kubernetes 中使用 API Gateway 替代 Ingress</title><link>https://blog.joway.io/posts/kubernetes-gateway/</link><pubDate>Tue, 12 Sep 2017 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/kubernetes-gateway/</guid><description>&lt;h2 id="背景">背景&lt;/h2>
&lt;p>最近在构思基于 Kubernetes 建立一个个人的开放云平台 , 听起来有点不自量力 , 不过作为个人业余小玩意还是蛮好玩的。最终的成品希望是用户能够轻松地在平台上跑一些简单的无状态服务 和 cronjob 。&lt;/p>
&lt;p>在搭建平台的时候遇到的第一个困难是需要有一个好用且功能全面的 API Gateway , 主流的网关服务大多是基于 OpenResty 基础上进行二次开发 , 所需要完成的工作无非是负载均衡，和 API 管理, 加上一些零零碎碎的小功能。&lt;/p>
&lt;h2 id="负载均衡">负载均衡&lt;/h2>
&lt;p>负载均衡分为四层和七层两种 , 以大家所熟知的 Nginx 为例 , 在它的 conf 文件中 , 有 http {} 和 stream {} 两种 block 。&lt;/p>
&lt;pre>&lt;code># 四层负载均衡
stream {
server {
listen 80;
proxy_pass app;
}
upstream app {
server 172.31.0.1:8000;
}
}
# 七层负载均衡
http {
upstream app {
server 192.168.0.1:8000;
server 192.168.0.1:8001;
}
server {
listen 80;
location / {
proxy_pass http://app;
}
}
}
&lt;/code>&lt;/pre>
&lt;h3 id="四层负载均衡">四层负载均衡&lt;/h3>
&lt;p>四层负载均衡只是从背后选择一个 server , 让其与客户端建立连接 , 其本身并不参与连接, 只是作为一个路由转发。好处是这样做它的性能会非常好，坏处是它也仅仅只能作为一个负载均衡存在，你无法对它做更加高层的处理，例如图片优化 , gzip 压缩等。并且由于它直接将后端服务暴露给了客户端, 当面临 DDoS 等攻击时, 后端将直接承受巨额流量。&lt;/p></description></item><item><title>ElasticSearch 最佳实践</title><link>https://blog.joway.io/posts/elasticsearch-bp/</link><pubDate>Sun, 28 May 2017 00:00:00 +0000</pubDate><guid>https://blog.joway.io/posts/elasticsearch-bp/</guid><description>&lt;p>Elasticsearch 是一个需要不停调参数的庞然大物 , 从其自身的设置到JVM层面, 有着无数的参数需要根据业务的变化进行调整。最近采用3台 AWS r3.2xlarge , 32GB, 4核, 构建了一套日均日志量过亿的 EFK 套件。经过不停地查阅文档进行调整优化 , 目前日常CPU占用只在30% , 大部分 Kibana 内的查询都能在 5s ~ 15s 内完成。&lt;/p>
&lt;p>下面记录了一些实践过程中积累的经验。&lt;/p>
&lt;h2 id="硬件">硬件&lt;/h2>
&lt;h3 id="cpu">CPU&lt;/h3>
&lt;ol>
&lt;li>多核胜过高性能单核CPU&lt;/li>
&lt;li>实践中发现, 在高写入低查询的场景下, 日常状态时 , CPU 还能基本应付, 一旦进行 kibana 上的查询或者 force merge 时, CPU 会瞬间飙高, 从而导致写入变慢, 需要很长一段时间 cpu 才能降下来。&lt;/li>
&lt;/ol>
&lt;h3 id="mem">Mem&lt;/h3>
&lt;ol>
&lt;li>Elasticsearch 需要使用大量的堆内存, 而 Lucene 则需要消耗大量非堆内存 (off-heap)。推荐给 ES 设置本机内存的一半, 如32G 内存的机器上, 设置 -Xmx16g -Xms16g ，剩下的内存给 Lucene 。&lt;/li>
&lt;li>如果你不需要对分词字符串做聚合计算（例如，不需要 fielddata ）可以考虑降低堆内存。堆内存越小，Elasticsearch（更快的 GC）和 Lucene（更多的内存用于缓存）的性能越好。&lt;/li>
&lt;li>由于 JVM 的一些机制 , 内存并不是越大越好, 推荐最大只设置到 31 GB 。&lt;/li>
&lt;li>禁用 swap &lt;code>sudo swapoff -a&lt;/code>&lt;/li>
&lt;/ol>
&lt;h2 id="配置">配置&lt;/h2>
&lt;p>PS: 应该尽可能使用 ansible 这类工具去管理集群 , 否则集群内机器的状态不一致将是一场噩梦。&lt;/p></description></item><item><title/><link>https://blog.joway.io/cat/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.joway.io/cat/</guid><description>&lt;p>p&amp;mdash;
title: &amp;ldquo;Kui. 😾&amp;rdquo;
date: 2019-08-30
type: &amp;ldquo;gallery&amp;rdquo;
draft: false
gallery:&lt;/p>
&lt;ul>
&lt;li>name: &amp;lsquo;2019.08.31&amp;rsquo;
url: &amp;lsquo;&lt;a href="https://cdn.statically.io/img/blog.joway.io/images/kui/IMG_20190831_191500.jpg?quality=70%27">https://cdn.statically.io/img/blog.joway.io/images/kui/IMG_20190831_191500.jpg?quality=70'&lt;/a>&lt;/li>
&lt;li>name: &amp;lsquo;2019.08.24&amp;rsquo;
url: &amp;lsquo;&lt;a href="https://cdn.statically.io/img/blog.joway.io/images/kui/IMG_20190824_204700.jpg?quality=70%27">https://cdn.statically.io/img/blog.joway.io/images/kui/IMG_20190824_204700.jpg?quality=70'&lt;/a>&lt;/li>
&lt;li>name: &amp;lsquo;2019.07.31&amp;rsquo;
url: &amp;lsquo;&lt;a href="https://cdn.statically.io/img/blog.joway.io/images/kui/IMG_20190731_225526.jpg?quality=70%27">https://cdn.statically.io/img/blog.joway.io/images/kui/IMG_20190731_225526.jpg?quality=70'&lt;/a>&lt;/li>
&lt;li>name: &amp;lsquo;2019.07.15&amp;rsquo;
url: &amp;lsquo;&lt;a href="https://cdn.statically.io/img/blog.joway.io/images/kui/mmexport1563203324319.jpg?quality=70%27">https://cdn.statically.io/img/blog.joway.io/images/kui/mmexport1563203324319.jpg?quality=70'&lt;/a>&lt;/li>
&lt;li>name: &amp;lsquo;2019.07.11&amp;rsquo;
url: &amp;lsquo;&lt;a href="https://cdn.statically.io/img/blog.joway.io/images/kui/IMG_20190711_003011-edited.jpg?quality=70%27">https://cdn.statically.io/img/blog.joway.io/images/kui/IMG_20190711_003011-edited.jpg?quality=70'&lt;/a>&lt;/li>
&lt;li>name: &amp;lsquo;2019.07.05&amp;rsquo;
url: &amp;lsquo;&lt;a href="https://cdn.statically.io/img/blog.joway.io/images/kui/IMG_20190705_202300.jpg?quality=70%27">https://cdn.statically.io/img/blog.joway.io/images/kui/IMG_20190705_202300.jpg?quality=70'&lt;/a>&lt;/li>
&lt;/ul>
&lt;hr></description></item><item><title>Presentations</title><link>https://blog.joway.io/presentations/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://blog.joway.io/presentations/</guid><description>&lt;ul>
&lt;li>&lt;a href="https://docs.google.com/presentation/d/1inbg_ni4HWWQ_LQLRgn7DMDB-joj5xKHSH1rd32P9lE/edit#slide=id.p">Design a modern local memory cache&lt;/a>, 2019.11&lt;/li>
&lt;li>&lt;a href="https://docs.google.com/presentation/d/1gTgnDA2j1cZtV_RLzNhodmg2bch1CUHpbhGPI2PyB_Y/edit#slide=id.p">How to write a Database - I/O Stack&lt;/a>, 2019.08&lt;/li>
&lt;li>&lt;a href="https://docs.google.com/presentation/d/1d-a9nIVqRE7ARkHAIBA_uUfZCLVm1FpZV5soZZw32yk/edit#slide=id.p">How to write a Database - Index&lt;/a>, 2019.07&lt;/li>
&lt;li>&lt;a href="https://docs.google.com/presentation/d/1odpCfHE5dp7acgK_7lkqTPgHlvT-jPg-XtM7-s9WQZw/edit#slide=id.g2c06c0b4ff_0_33">Golang : Make Probleming Happy Again&lt;/a>, 2017.12&lt;/li>
&lt;li>&lt;a href="https://docs.google.com/presentation/d/1KpKSXr4M2vsrnUhTNEpSP6FQ_w7xFNtKDB0ZswvEWvg/edit#slide=id.p">Django 现代开发 - 入门与实践&lt;/a>, 2017.08&lt;/li>
&lt;/ul></description></item></channel></rss>