<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Yifei's Notes</title>
        <link>https://yifei.me</link>
        <description>Yifei's Notes</description>
        <lastBuildDate>Tue, 29 Aug 2023 14:30:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Yifei's Notes</title>
            <url>https://yifei.me/favicon.png</url>
            <link>https://yifei.me</link>
        </image>
        <copyright>2006-2023, Yifei Kong</copyright>
        <item>
            <title><![CDATA[读书的乐趣]]></title>
            <link>https://yifei.me/note/3001</link>
            <guid>https://yifei.me/note/3001</guid>
            <pubDate>Sat, 19 Aug 2023 16:33:25 GMT</pubDate>
            <description><![CDATA[<blockquote class="pl-4 pr-0 py-2 bg-gray-100 border-l-8"><p class="my-4 font-light">有一天，一个叫 Alice 的六岁小女孩打来电话问了一个问题。</p><p class="my-4 font-light">她问，我是个好孩子，我弟弟是个坏孩子。爸爸妈妈要求我们每晚 9 点上床睡觉，每一次我都很听话，按时上床。</p><p class="my-4 font-light">可弟弟却不听话，每次要一个苹果才肯上床，而他居然每次都能得逞。我也想要一个苹果，但父母从来不给我。</p><p class="my-4 font-light">为什么弟弟是个坏孩子，他总能得到苹果，而我是个好孩子，却总得不到苹果？</p><p class="my-4 font-light">坏孩子虽然得到了苹果，但其实你得到了上帝最好的礼物，就是你是个好孩子。成为好孩子本身就是奖励。</p></blockquote><p class="my-4 font-light">我一直觉得这是一个奇怪的寓言故事，感觉像是废话一样。</p><p class="my-4 font-light">最近读了不少书，时常会有一些功利的想法，有些书看着虽然很有趣，但总会觉得似乎也不能用来换钱。
今天坐在马桶上突然想到这个故事，才明白过来——读书本来就是一种乐趣，这就是读书最大的奖励。</p>]]></description>
            <content:encoded><![CDATA[<blockquote class="pl-4 pr-0 py-2 bg-gray-100 border-l-8"><p class="my-4 font-light">有一天，一个叫 Alice 的六岁小女孩打来电话问了一个问题。</p><p class="my-4 font-light">她问，我是个好孩子，我弟弟是个坏孩子。爸爸妈妈要求我们每晚 9 点上床睡觉，每一次我都很听话，按时上床。</p><p class="my-4 font-light">可弟弟却不听话，每次要一个苹果才肯上床，而他居然每次都能得逞。我也想要一个苹果，但父母从来不给我。</p><p class="my-4 font-light">为什么弟弟是个坏孩子，他总能得到苹果，而我是个好孩子，却总得不到苹果？</p><p class="my-4 font-light">坏孩子虽然得到了苹果，但其实你得到了上帝最好的礼物，就是你是个好孩子。成为好孩子本身就是奖励。</p></blockquote><p class="my-4 font-light">我一直觉得这是一个奇怪的寓言故事，感觉像是废话一样。</p><p class="my-4 font-light">最近读了不少书，时常会有一些功利的想法，有些书看着虽然很有趣，但总会觉得似乎也不能用来换钱。
今天坐在马桶上突然想到这个故事，才明白过来——读书本来就是一种乐趣，这就是读书最大的奖励。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[代码大全阅读笔记（待续）]]></title>
            <link>https://yifei.me/note/347</link>
            <guid>https://yifei.me/note/347</guid>
            <pubDate>Sat, 19 Aug 2023 12:12:30 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">TODO: 总结</p><p class="my-4 font-light">作为技术员工，你的一部分工作就是培训周围的非技术人员，讲解开发过程。</p><p class="my-4 font-light">在软件开发过程中，如果需求被污染了，那么他就会污染架构，而架构又会污染构建。这样会导致
程序员脾气暴躁、营养失调；开发出的程序具有放射性污染，而且周身都是缺陷。</p><p class="my-4 font-light">发现错误的时间要尽可能接近引入错误的时间。</p><p class="my-4 font-light">问题定义应该用客户的语言来书写，而且应该从客户的角度来描述问题。最好的解决方案未必是一个计算机程序。</p><p class="my-4 font-light">明确的需求免得你去猜测用户想要的是什么。开发过程能够帮助用户更好地理解自己的需求，这是需求变更的主要来源。平均下来，开发过程中有 25% 的需求变化会导致返工量的 75% 以上。</p><h2 class="my-2 font-semibold text-xl">如何怒怼需求</h2><ol class="my-4 ml-4 font-light list-decimal"><li class="">确保每个人都知道需求变更的代价。“进度”和“成本”这两个字眼比咖啡和冷水澡都要提神，许多“必须要有（must have）”的功能很快就会变成 “有了最好（nice to have）”</li><li class="">建立一套变更控制系统</li><li class="">使用能够适应变更的开发方法</li><li class="">放弃这个项目。如果需求特别糟糕，或者极不稳定，而上 main 的意见没有一条能够奏效，那就取消这个项目。即使你真的无法取消这个项目，也设想一下取消它之后回事怎样的情况。</li><li class="">注意项目的商业案例、有些需求作为功能特色来看是不错的注意，但是当你评估“增加的商业价值”时就会觉得它是个糟糕透了的主意。</li></ol><h2 class="my-2 font-semibold text-xl">架构的典型组成部分</h2><ol class="my-4 ml-4 font-light list-decimal"><li class="">服务划分</li><li class="">业务规则</li><li class="">用户界面设计</li><li class="">资源管理</li><li class="">安全性</li><li class="">性能</li><li class="">可伸缩性</li><li class="">互用性</li><li class="">国际化、本地化</li><li class="">错误处理</li><li class="">关于买还是造的决策</li><li class="">如何复用</li><li class="">变更策略</li></ol><p class="my-4 font-light">优秀的架构往往适合机器和语言无关的。</p><h2 class="my-2 font-semibold text-xl">第一章 欢迎进入软件构建的世界</h2><p class="my-4 font-light">没啥可记录的</p><h2 class="my-2 font-semibold text-xl">第二章 用隐喻更充分地理解软件开发</h2><p class="my-4 font-light">软件开发最大的挑战还是将问题概念化，编程中的很多错误都是概念性的错误。</p><p class="my-4 font-light">应该先做出一个尽可能简单、但能运行的版本。一点点在其上附上肌肉和皮肤，一次增加一部分代码，直到得到一个可以完全工作的系统。</p><h2 class="my-2 font-semibold text-xl">第三章 三思而后行：前期准备</h2><h2 class="my-2 font-semibold text-xl">第五章</h2><p class="my-4 font-light">软件的首要技术使命——<strong>管理复杂度</strong></p><h3 class="my-2 font-semibold ">低效的设计往往来自：</h3><ol class="my-4 ml-4 font-light list-decimal"><li class="">用复杂的方法解决简单的问题</li><li class="">用简单但错误的方法解决复杂的问题</li><li class="">用不恰当的复杂方法解决复杂的问题</li></ol><p class="my-4 font-light">理想的设计特征</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">高扇入：大量的类使用某一个固定的类，也就是有很好的复用。</li><li class="">低扇出：让一个类尽量少地使用其他的类，否则会过于复杂。</li><li class="">使用标准技术：要尽量使用标准化、常用的方法，让整个系统给人一种熟悉的感觉。</li></ol><p class="my-4 font-light">对子系统之间的通信应该加以限制，越少越好。尤其不要有环向的依赖。</p><p class="my-4 font-light">类应该像冰山：八分之七都是位于水面之下，而你能看到的只是水面之上的八分之一。</p><h3 class="my-2 font-semibold ">常用的设计模式：</h3><ol class="my-4 ml-4 font-light list-decimal"><li class="">抽象工厂模式。通过制定对象组的种类而非单个对象的类型来支持创建一组相关的对象</li><li class="">适配器。把一个类的接口转换成另一个接口</li><li class="">桥接。把接口和实现分开，使他们可以独立地变化</li><li class="">组合。创建一个包含了其他同类对象的对象，是的客户端可以与最上层对象交互而无需考虑过多的细节对象。</li><li class="">装饰器。给一个对象动态的添加职责，而不去创建新的类</li><li class="">外观。为没有一致接口的代码提供一个一致的接口</li><li class="">工厂方法。</li><li class="">迭代器。提供一个服务来顺序访问一族元素中的每一个</li><li class="">观察者。当一个对象变化时，把这个变化通知其他元素。</li><li class="">单例。有且只有一个实例</li><li class="">策略。定义一组行为或算法，使得他们可以动态地相互替换</li><li class="">模板方法。定义一个操作的算法结构，但是把部分实现的细节留给子类（派生类）</li></ol><p class="my-4 font-light">蛮力也是一种强大的方法。画图是另一种强大的启发式方法。图能在另一个更高的抽象层次上表达问题。不要卡在单一问题上，可以去散散步。</p><p class="my-4 font-light">实际开发中的问题和学校作业的区别是，实际开发中的问题可能经常在你提交代码之后需求已经变了，需要再次开发</p><h2 class="my-2 font-semibold text-xl">第六章</h2><p class="my-4 font-light">不懂 ADT 的程序员开发出来的类只是名义上的“类”而已——实际上这种“类”只不过是把一些稍微有点关系的数据和子程序堆在一起。</p><p class="my-4 font-light">YN: 使用 ADT 隐藏实现细节，但是也可能是过早优化，比如 JAVA 中讨厌的 getter、setter</p><p class="my-4 font-light">使用类的好处是，你可以像在现实世界中</p><h2 class="my-2 font-semibold text-xl">第十章</h2><h3 class="my-2 font-semibold ">隐式变量声明</h3><p class="my-4 font-light">隐式变量声明是一项非常危险的行动。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>BAD

account_no <span class="hljs-operator">=</span> xxx
account_number <span class="hljs-operator">=</span> <span class="hljs-number">123456</span> // 记错变量名了
target_account <span class="hljs-operator">=</span> account_no 

GOOD

Python 中无法强制声明变量，可以使用 pylint 等工具</code></pre><p class="my-4 font-light">可以通过以下几个方面改善</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">关闭隐式变量声明</li><li class="">遵循某种命名规则</li><li class="">使用 lint 等工具检查变量名</li></ol><h3 class="my-2 font-semibold ">初始化</h3><p class="my-4 font-light">在需要第一次使用变量的地方声明变量，在变量声明的时候初始化，并且尽量声明为 const 或者 final。</p><p class="my-4 font-light">尤其要注意 i，j 这些变量在再次使用的时候有没有再次初始化。</p><p class="my-4 font-light">对于 C++ 等语言，要在构造函数中初始化所有变量，并且在析构函数中释放内存。</p><h3 class="my-2 font-semibold ">作用域</h3><p class="my-4 font-light">介于统一变量多个引用点之间的代码称为“攻击窗口”。跨度指的是两次访问同一个变量的间隔，生存时间指的是变量的第一次使用和最后一次使用的间隔。应该尽量减少变量的存活时间和跨度，比如说全局变量的跨度和生存时间都很长，所以他们不好。</p><h3 class="my-2 font-semibold ">持续性</h3><p class="my-4 font-light">在程序中加入断言来验证关键变量的合理取值
在需要删除变量的时候赋值为不合法数值，比如 null</p><h3 class="my-2 font-semibold ">绑定时间</h3><p class="my-4 font-light">越晚越好</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">编码时 <code class="px-1 bg-gray-100 border-2 rounded">titleBar.color = 0xfff</code></li><li class="">编译时 <code class="px-1 bg-gray-100 border-2 rounded">titleBar.color = WHITE</code></li><li class="">加载时，也就是程序初始化的时候 <code class="px-1 bg-gray-100 border-2 rounded">titleBar.color = </code></li></ol><h2 class="my-2 font-semibold text-xl">第十四章</h2><p class="my-4 font-light">语句一般也分为有副作用的语句和没有副作用的语句，如果前后两个语句都在更新同一个变量，那这样是不好的。至少我们要减少有副作用的语句。这样可以使代码的依赖变得更加明显。代码是按照顺序执行的，对于后一句依赖前一句的代码</p><p class="my-4 font-light">比如：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>BAD:

<span class="hljs-function"><span class="hljs-title">ComputeMarketingExpense</span><span class="hljs-params">()</span></span>
<span class="hljs-function"><span class="hljs-title">ComputeSaleExpense</span><span class="hljs-params">()</span></span>
<span class="hljs-function"><span class="hljs-title">ComouteTravelExpense</span><span class="hljs-params">()</span></span>
<span class="hljs-function"><span class="hljs-title">ComputePersonalExpense</span><span class="hljs-params">()</span></span>
<span class="hljs-function"><span class="hljs-title">DispalyExpenseSummary</span><span class="hljs-params">()</span></span>

Good:

market = <span class="hljs-built_in">ComputeMarkingExpense</span>()
sale = <span class="hljs-built_in">ComputeSaleExpense</span>()
travle = <span class="hljs-built_in">ComputeTravelExpense</span>()
personal = <span class="hljs-built_in">ComputePersonalExpense</span>()
<span class="hljs-function"><span class="hljs-title">Dispaly</span><span class="hljs-params">(ExpenseSummery(market, sale, travle, personal)</span></span></code></pre><p class="my-4 font-light">对于没有依赖关系的代码，应该让他们尽可能分组，方便自上而下的阅读。最好做到相关代码构成的语句块之间不要有重叠</p><p class="my-4 font-light">BAD：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>MarketingData marketingData;
SalesData salesData;
TravelData travelData;

travelData.<span class="hljs-constructor">ComputeDuarerly()</span>
salesData.<span class="hljs-constructor">ComputeQuarterly()</span>
maketingData.<span class="hljs-constructor">ComputeQuarterly()</span>

salesData.<span class="hljs-constructor">Print()</span>
travelData.<span class="hljs-constructor">Print()</span>
marketingData.<span class="hljs-constructor">Print()</span></code></pre><p class="my-4 font-light">Good:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>MarketingData marketingData;
marketingData.<span class="hljs-constructor">ComputeQuarterly()</span>
marketingData.<span class="hljs-constructor">Print()</span>

TravelData travelData;
travelData.<span class="hljs-constructor">ComputeQuarterly()</span>
travelData.<span class="hljs-constructor">Print()</span>

SalesData salesData
salesData.<span class="hljs-constructor">ComputeQuerterly()</span>
salesData.<span class="hljs-constructor">Print()</span></code></pre><h2 class="my-2 font-semibold text-xl">第十五章 使用条件语句</h2><p class="my-4 font-light">书中写到应该优先处理正常情况，但是这样的话可能造成箭头形的代码，个人认为还是有限处理错误并返回比较好。不过如果是正常情况的分支的话，应该优先处理常见情况。对于 switch 语句，应该在 default 字句中处理错误</p><p class="my-4 font-light">对于大长串的 and 判断条件建议写一个 is_XXX 函数来封装一下。</p><h2 class="my-2 font-semibold text-xl">第十六章 控制循环</h2><p class="my-4 font-light">循环的种类：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">计数循环，按照规定的次数执行多次</li><li class="">连续求值的循环，预先并不知道需要运行多少次，每次迭代都检查是否需要结束（比如用户选择退出，或者遇到了错误）</li><li class="">无限循环，比如内核，事件循环</li><li class="">迭代器循环，对容器类中的每一个元素执行一次操作</li></ol><p class="my-4 font-light">研究显示下面这种代码比传统的 while 或者 do-while 代码更容易理解，以为如果把退出条件强行放到开始或者结尾，那么很难避免重复代码。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">while</span> (true) {
    <span class="hljs-regexp">//</span> ...
    <span class="hljs-keyword">if</span> (xxx) <span class="hljs-keyword">break</span>
   <span class="hljs-regexp">//</span> ...
}</code></pre><p class="my-4 font-light">像是 for 和 foreach 循环最好用在简单的控制条件，不要在循环体内做改动下标等操作。</p><h3 class="my-2 font-semibold ">安全计数器</h3><p class="my-4 font-light">对于可能无法终止的循环，可以考虑添加一个安全计数器来保证不会出现死循环。</p><p class="my-4 font-light">BAD:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>do {
    <span class="hljs-keyword">node</span> <span class="hljs-title">= node-</span>&gt;Next;
} while (<span class="hljs-keyword">node</span><span class="hljs-title">-Next</span> != NULL);</code></pre><p class="my-4 font-light">Good:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>safeCounter = <span class="hljs-number">0</span>;
do {
    <span class="hljs-keyword">node</span> <span class="hljs-title">= node-</span>&gt;Next;
    ...
    safeCounter++;
    if (safeCounter &gt;= SATETY_LIMITE) {
        Assert(<span class="hljs-literal">false</span>, <span class="hljs-string">&quot;Internal Error: safe counter violation&quot;</span>)
    }
    ...
} while (<span class="hljs-keyword">node</span><span class="hljs-title">-&gt;Next</span> != NULL);</code></pre><h3 class="my-2 font-semibold ">continue 和 break</h3><p class="my-4 font-light">continue 语句就相当于一个 else 语句，和之前所述一样，尽量在循环开始的地方使用 continue 语句。对于 return 语句也是一样的。</p><p class="my-4 font-light">需要特别注意的是 switch 中也允许 break 语句，所以很可能导致潜在的 bug</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment">// BAD：</span>
network <span class="hljs-selector-tag">code</span>()    
{    
    <span class="hljs-built_in">switch</span>(line)   
    {    
        case  THING1:   
            doit1();   
            break;    
        case  THING2:   
            if(x==STUFF)   
            {    
                <span class="hljs-built_in">do_first_stuff</span>();    
                <span class="hljs-built_in">if</span>(y==OTHER_STUFF)    
                    break;    
                <span class="hljs-built_in">do_later_stuff</span>();    
            }    
            <span class="hljs-comment">/*代码的意图是跳转到这里… …*/</span>    
            <span class="hljs-built_in">initialize_modes_pointer</span>();   
            break;    
        default :    
            processing();    
    }   
    <span class="hljs-comment">/*… …但事实上跳到了这里。*/</span>    
    <span class="hljs-built_in">use_modes_pointer</span>(); <span class="hljs-comment">/*致使 modes_pointer 指针未初始化*/</span>    
}</code></pre><p class="my-4 font-light">把循环控制在 3 级以内，否则人类就不能理解。把长循环的内容移到子程序中。</p><p class="my-4 font-light">可以由内而外的构建循环，也就是说先写出循环体要处理的内容，再去构建循环条件。</p><h2 class="my-2 font-semibold text-xl">第十七章 其他控制结构</h2><p class="my-4 font-light">递归，首先要写终止语句，并且要尽量避免迭代次数过多的递归，因为每次递归都会产生一个调用栈，可以使用安全计数器来避免无限递归</p><h2 class="my-2 font-semibold text-xl">第十八章 表驱动法</h2><p class="my-4 font-light">表驱动法把复杂的条件分支语句转化成通过查表语句。表驱动法还有一个好处就是可以把解析的逻辑放到外部文件中。表驱动的两个问题：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">使用什么 key，如果对应的条件不能直接做 key，可以使用一个函数转化成 enum</li><li class="">使用什么 value</li></ol><p class="my-4 font-light">BAD:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">if</span> <span class="hljs-attr">month</span> == <span class="hljs-number">1</span>:
    <span class="hljs-attr">days</span> = <span class="hljs-number">31</span>
elif <span class="hljs-attr">month</span> == <span class="hljs-number">2</span>:
    <span class="hljs-attr">days</span> = <span class="hljs-number">28</span>
elif <span class="hljs-attr">month</span> == <span class="hljs-number">3</span>:
    <span class="hljs-attr">days</span> = <span class="hljs-number">31</span>
...
<span class="hljs-keyword">else</span> <span class="hljs-attr">month</span> == <span class="hljs-number">12</span>:
    <span class="hljs-attr">days</span> = <span class="hljs-number">31</span></code></pre><p class="my-4 font-light">GOOD:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attr">days_per_month</span> = [<span class="hljs-number">31</span>, <span class="hljs-number">28</span>, <span class="hljs-number">31</span>, <span class="hljs-number">30</span>, <span class="hljs-number">31</span>, <span class="hljs-number">30</span>, <span class="hljs-number">31</span>, <span class="hljs-number">31</span>, <span class="hljs-number">30</span>, <span class="hljs-number">31</span>, <span class="hljs-number">30</span>, <span class="hljs-number">31</span>]
<span class="hljs-attr">days</span> = days_per_month[month -<span class="hljs-number">1</span>]</code></pre><h3 class="my-2 font-semibold ">阶梯访问表</h3><p class="my-4 font-light">比如根据分数来确定学生的评级</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>range_limit = <span class="hljs-selector-attr">[50.0, 65.0, 75.0, 90.0, 100.0]</span>
grades = <span class="hljs-selector-attr">[<span class="hljs-string">&#x27;F&#x27;</span>, <span class="hljs-string">&#x27;D&#x27;</span>, <span class="hljs-string">&#x27;C&#x27;</span>, <span class="hljs-string">&#x27;B&#x27;</span>, <span class="hljs-string">&#x27;A&#x27;</span>]</span>
max_grade_level = <span class="hljs-built_in">len</span>(grade) - <span class="hljs-number">1</span>

grade_level = <span class="hljs-number">0</span>
student_grade = <span class="hljs-string">&#x27;A&#x27;</span>
while student_grade == <span class="hljs-string">&#x27;A&#x27;</span> and grade_level &lt; max_grade_level:
    <span class="hljs-keyword">if</span> student_score &lt; range_limit<span class="hljs-selector-attr">[grade_level]</span>:
        student_grade = grades<span class="hljs-selector-attr">[grade_level]</span>
    grade_level += <span class="hljs-number">1</span></code></pre><h2 class="my-2 font-semibold text-xl">第十九章 一般控制问题</h2><p class="my-4 font-light">if 语句</p><p class="my-4 font-light">对于复杂的 if 语句来说，也可以考虑使用决策表来简化操作。
对于 if 语句中的判断，按照书中的顺序来编写数值表达式：<code class="px-1 bg-gray-100 border-2 rounded">if 1 &lt; a and a  &lt; 3</code></p><p class="my-4 font-light">度量复杂度的 Tom McCabe 方法，根据决策点来测试：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">从 1 开始</li><li class="">遇到 if、while、repeat、for、and、or 复杂度加 1</li><li class="">遇到 case 语句，每种情况 +1</li></ol><p class="my-4 font-light">0-5， 函数还不错
6-10，需要改进
10+，必须改进</p><h2 class="my-2 font-semibold text-xl">第二十章</h2><p class="my-4 font-light">没啥可记得</p><h2 class="my-2 font-semibold text-xl">第二十一章</h2><p class="my-4 font-light">没啥可记得</p><h2 class="my-2 font-semibold text-xl">第二十二章 开发者测试</h2><ul class="my-4 ml-4 font-light list-disc"><li class="">单元测试 一个程序员或者一个团队的代码的测试</li><li class="">组件测试 被测代码涉及到多个程序或团队</li><li class="">集成测试 对两个或多个包进行测试，通常应该尽早开始</li><li class="">回归测试 重复执行以前的测试，以便查找是否有 bug 复发</li><li class="">系统测试 在最终配置下运行整个软件</li></ul><p class="my-4 font-light">推荐的测试方法</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">对每项需求进行测试，最好在需求阶段就准备好测试用例</li><li class="">对每一个设计的关注点进行测试</li><li class="">基础测试</li><li class="">测试已经犯过的错误</li></ol><h3 class="my-2 font-semibold ">基础测试</h3><p class="my-4 font-light">目标是覆盖每一个路径，采用的方法和之前计算代码复杂度的方法一样。</p><h2 class="my-2 font-semibold text-xl">第三十章</h2><p class="my-4 font-light">如果你发现自己每天多次键入某个长度超过 5 个字母的命令，那么应该用一个脚本</p><h2 class="my-2 font-semibold text-xl">第三十一章</h2><p class="my-4 font-light">没啥值得记录的</p><h2 class="my-2 font-semibold text-xl">第三十二章 自说明代码</h2><p class="my-4 font-light">IBM 的研究显示：平均每十行一个注释的代码可读性最高。</p><p class="my-4 font-light">代码注释应该着眼于 why 而不是 how</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>BAD

<span class="hljs-string">//</span> <span class="hljs-keyword">if</span> acccount flag is zero
<span class="hljs-keyword">if</span> <span class="hljs-params">(<span class="hljs-attr">accountFlag</span> == 0)</span> <span class="hljs-string">...</span>

GOOD 
<span class="hljs-string">//</span> <span class="hljs-keyword">if</span> establishing a new acccount
<span class="hljs-keyword">if</span> <span class="hljs-params">(<span class="hljs-attr">accountFlag</span> == 0)</span> <span class="hljs-string">...</span>

BETTER

<span class="hljs-keyword">if</span> <span class="hljs-params">(account.<span class="hljs-attr">type</span> == AccountType.NewAccount)</span> <span class="hljs-string">...</span></code></pre><p class="my-4 font-light">对于非常规的行为要给与注释</p><h2 class="my-2 font-semibold text-xl">第三十三章 个人性格</h2><p class="my-4 font-light">你越是谦虚，进步就越快。</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">将系统“分解”，是为了使之更易于理解</li><li class="">进行 review 和 test 是为了减少人为失误（egoless programming）</li><li class="">将子程序编写的短小，以减轻大脑的负担</li><li class="">基于问题而不是底层实现细节来编程，从而减少工作量</li><li class="">通过各种各样的规范，将思路从相对繁琐的编程事务中解放出来。</li></ol><h3 class="my-2 font-semibold ">求知欲</h3><p class="my-4 font-light">如果分配给你的工作净是些不能提高自身技能的短期任务，你理应表示不满。如果正处于竞争激烈的软件市场，则目前工作用到的一半的知识将在三年后过期。假如不持续学习，你就会落伍。</p><p class="my-4 font-light">如果在工作中学不到什么，就找一份新的工作吧。</p><p class="my-4 font-light">如果不了解所用语言的某一特性是怎么回事，可编写一个小程序来检验，看看它是如何工作的。请在调试器中观察程序的执行情况。用一个小程序来检验某一概念，总比编写大程序时运用不太了解的特性要好。</p><p class="my-4 font-light">如果小程序表现的特性与你设想的不一样，怎么办呢？那正是你要研究的问题。最好通过小程序找出答案，而不要用大程序。有效编程的关键之一就是要学会迅速制造错误，并且每次都能从中有所收获。犯错不是罪过，从中学不到什么才是罪过。</p><p class="my-4 font-light">阅读解决问题的有关方法。解决问题是软件创作过程中的核心行为。就算你想再发明个车轮，也不会注定成功，你发明的也许是方车轮。</p><p class="my-4 font-light">在行动之前作出分析和计划。在分析和行动直接有着矛盾关系，然而多数程序员的问题不在于分析过度。</p><p class="my-4 font-light">学习成功项目的开发经验。Jon Bentley 认为你应该坐下来，准备一杯白兰地，点一根上好的雪茄，想看优秀小说一样来阅读程序。至少应该研究高层设计，并有选择地去研究某些地方的细节源代码。</p><p class="my-4 font-light">Thomas Kuhn 指出，凡是成熟的学科都是从解决问题发展起来的。</p><p class="my-4 font-light">不仅要阅读别人的代码，还应渴望了解专家对你的代码的看法。</p><p class="my-4 font-light"><strong>诚实</strong></p><h3 class="my-2 font-semibold ">偷懒的三个境界</h3><ol class="my-4 ml-4 font-light list-decimal"><li class="">拖延不喜欢的任务</li><li class="">迅速做完不喜欢的任务，以摆脱</li><li class="">编写某个工具来完成不喜欢的任务，以便再也不会做这些事</li></ol><p class="my-4 font-light">理论上当然是这样的，然而经常会遇到的讨厌的事情并不是重复性的，而是每次都是不同的恶心事情，或者 lead 催很紧，没有时间做类似的工具。</p><h3 class="my-2 font-semibold ">坚持</h3><p class="my-4 font-light">不要坚持，如果花了 15 分钟调试仍然没有进展，就该放弃排错过程，让潜意识仔细品品。和计算机错误斗气是不明智的，更好的方法是避开他们。</p><h3 class="my-2 font-semibold ">经验</h3><p class="my-4 font-light">人们还荒唐地强调程序员有多少经验。“我们需要有五年以上 C 语言编程经验的程序员”就是愚蠢的说法。如果程序员过了前一两年还没有学好 C 语言，那么再过个三年也没有什么意义。</p><p class="my-4 font-light">最后一个问题，如果你工作十年，你会得到十年经验还是一年经验的十次重复？必须检讨自己的行为，才能获得真正的经验。只有坚持不懈地学习，才能获取经验；如果不这样做，无论你工作多少年，都无法获得经验。</p><h3 class="my-2 font-semibold ">Gonzo Programming</h3><p class="my-4 font-light">彻夜编程让你感觉像是世界上最好的程序员，却要花上几个星期去纠正你在短暂辉煌时埋下的错误。可以热爱编程，但是热情不能代替熟练的能力，请想明白什么更重要。</p><h3 class="my-2 font-semibold ">习惯</h3><p class="my-4 font-light">不能用“没有习惯”来代替“坏习惯”，只能用“新习惯”代替“坏习惯”</p><h2 class="my-2 font-semibold text-xl">第三十四章</h2><p class="my-4 font-light">基于问题域编程，最顶层代码不要关心实现细节。</p>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">TODO: 总结</p><p class="my-4 font-light">作为技术员工，你的一部分工作就是培训周围的非技术人员，讲解开发过程。</p><p class="my-4 font-light">在软件开发过程中，如果需求被污染了，那么他就会污染架构，而架构又会污染构建。这样会导致
程序员脾气暴躁、营养失调；开发出的程序具有放射性污染，而且周身都是缺陷。</p><p class="my-4 font-light">发现错误的时间要尽可能接近引入错误的时间。</p><p class="my-4 font-light">问题定义应该用客户的语言来书写，而且应该从客户的角度来描述问题。最好的解决方案未必是一个计算机程序。</p><p class="my-4 font-light">明确的需求免得你去猜测用户想要的是什么。开发过程能够帮助用户更好地理解自己的需求，这是需求变更的主要来源。平均下来，开发过程中有 25% 的需求变化会导致返工量的 75% 以上。</p><h2 class="my-2 font-semibold text-xl">如何怒怼需求</h2><ol class="my-4 ml-4 font-light list-decimal"><li class="">确保每个人都知道需求变更的代价。“进度”和“成本”这两个字眼比咖啡和冷水澡都要提神，许多“必须要有（must have）”的功能很快就会变成 “有了最好（nice to have）”</li><li class="">建立一套变更控制系统</li><li class="">使用能够适应变更的开发方法</li><li class="">放弃这个项目。如果需求特别糟糕，或者极不稳定，而上 main 的意见没有一条能够奏效，那就取消这个项目。即使你真的无法取消这个项目，也设想一下取消它之后回事怎样的情况。</li><li class="">注意项目的商业案例、有些需求作为功能特色来看是不错的注意，但是当你评估“增加的商业价值”时就会觉得它是个糟糕透了的主意。</li></ol><h2 class="my-2 font-semibold text-xl">架构的典型组成部分</h2><ol class="my-4 ml-4 font-light list-decimal"><li class="">服务划分</li><li class="">业务规则</li><li class="">用户界面设计</li><li class="">资源管理</li><li class="">安全性</li><li class="">性能</li><li class="">可伸缩性</li><li class="">互用性</li><li class="">国际化、本地化</li><li class="">错误处理</li><li class="">关于买还是造的决策</li><li class="">如何复用</li><li class="">变更策略</li></ol><p class="my-4 font-light">优秀的架构往往适合机器和语言无关的。</p><h2 class="my-2 font-semibold text-xl">第一章 欢迎进入软件构建的世界</h2><p class="my-4 font-light">没啥可记录的</p><h2 class="my-2 font-semibold text-xl">第二章 用隐喻更充分地理解软件开发</h2><p class="my-4 font-light">软件开发最大的挑战还是将问题概念化，编程中的很多错误都是概念性的错误。</p><p class="my-4 font-light">应该先做出一个尽可能简单、但能运行的版本。一点点在其上附上肌肉和皮肤，一次增加一部分代码，直到得到一个可以完全工作的系统。</p><h2 class="my-2 font-semibold text-xl">第三章 三思而后行：前期准备</h2><h2 class="my-2 font-semibold text-xl">第五章</h2><p class="my-4 font-light">软件的首要技术使命——<strong>管理复杂度</strong></p><h3 class="my-2 font-semibold ">低效的设计往往来自：</h3><ol class="my-4 ml-4 font-light list-decimal"><li class="">用复杂的方法解决简单的问题</li><li class="">用简单但错误的方法解决复杂的问题</li><li class="">用不恰当的复杂方法解决复杂的问题</li></ol><p class="my-4 font-light">理想的设计特征</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">高扇入：大量的类使用某一个固定的类，也就是有很好的复用。</li><li class="">低扇出：让一个类尽量少地使用其他的类，否则会过于复杂。</li><li class="">使用标准技术：要尽量使用标准化、常用的方法，让整个系统给人一种熟悉的感觉。</li></ol><p class="my-4 font-light">对子系统之间的通信应该加以限制，越少越好。尤其不要有环向的依赖。</p><p class="my-4 font-light">类应该像冰山：八分之七都是位于水面之下，而你能看到的只是水面之上的八分之一。</p><h3 class="my-2 font-semibold ">常用的设计模式：</h3><ol class="my-4 ml-4 font-light list-decimal"><li class="">抽象工厂模式。通过制定对象组的种类而非单个对象的类型来支持创建一组相关的对象</li><li class="">适配器。把一个类的接口转换成另一个接口</li><li class="">桥接。把接口和实现分开，使他们可以独立地变化</li><li class="">组合。创建一个包含了其他同类对象的对象，是的客户端可以与最上层对象交互而无需考虑过多的细节对象。</li><li class="">装饰器。给一个对象动态的添加职责，而不去创建新的类</li><li class="">外观。为没有一致接口的代码提供一个一致的接口</li><li class="">工厂方法。</li><li class="">迭代器。提供一个服务来顺序访问一族元素中的每一个</li><li class="">观察者。当一个对象变化时，把这个变化通知其他元素。</li><li class="">单例。有且只有一个实例</li><li class="">策略。定义一组行为或算法，使得他们可以动态地相互替换</li><li class="">模板方法。定义一个操作的算法结构，但是把部分实现的细节留给子类（派生类）</li></ol><p class="my-4 font-light">蛮力也是一种强大的方法。画图是另一种强大的启发式方法。图能在另一个更高的抽象层次上表达问题。不要卡在单一问题上，可以去散散步。</p><p class="my-4 font-light">实际开发中的问题和学校作业的区别是，实际开发中的问题可能经常在你提交代码之后需求已经变了，需要再次开发</p><h2 class="my-2 font-semibold text-xl">第六章</h2><p class="my-4 font-light">不懂 ADT 的程序员开发出来的类只是名义上的“类”而已——实际上这种“类”只不过是把一些稍微有点关系的数据和子程序堆在一起。</p><p class="my-4 font-light">YN: 使用 ADT 隐藏实现细节，但是也可能是过早优化，比如 JAVA 中讨厌的 getter、setter</p><p class="my-4 font-light">使用类的好处是，你可以像在现实世界中</p><h2 class="my-2 font-semibold text-xl">第十章</h2><h3 class="my-2 font-semibold ">隐式变量声明</h3><p class="my-4 font-light">隐式变量声明是一项非常危险的行动。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>BAD

account_no <span class="hljs-operator">=</span> xxx
account_number <span class="hljs-operator">=</span> <span class="hljs-number">123456</span> // 记错变量名了
target_account <span class="hljs-operator">=</span> account_no 

GOOD

Python 中无法强制声明变量，可以使用 pylint 等工具</code></pre><p class="my-4 font-light">可以通过以下几个方面改善</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">关闭隐式变量声明</li><li class="">遵循某种命名规则</li><li class="">使用 lint 等工具检查变量名</li></ol><h3 class="my-2 font-semibold ">初始化</h3><p class="my-4 font-light">在需要第一次使用变量的地方声明变量，在变量声明的时候初始化，并且尽量声明为 const 或者 final。</p><p class="my-4 font-light">尤其要注意 i，j 这些变量在再次使用的时候有没有再次初始化。</p><p class="my-4 font-light">对于 C++ 等语言，要在构造函数中初始化所有变量，并且在析构函数中释放内存。</p><h3 class="my-2 font-semibold ">作用域</h3><p class="my-4 font-light">介于统一变量多个引用点之间的代码称为“攻击窗口”。跨度指的是两次访问同一个变量的间隔，生存时间指的是变量的第一次使用和最后一次使用的间隔。应该尽量减少变量的存活时间和跨度，比如说全局变量的跨度和生存时间都很长，所以他们不好。</p><h3 class="my-2 font-semibold ">持续性</h3><p class="my-4 font-light">在程序中加入断言来验证关键变量的合理取值
在需要删除变量的时候赋值为不合法数值，比如 null</p><h3 class="my-2 font-semibold ">绑定时间</h3><p class="my-4 font-light">越晚越好</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">编码时 <code class="px-1 bg-gray-100 border-2 rounded">titleBar.color = 0xfff</code></li><li class="">编译时 <code class="px-1 bg-gray-100 border-2 rounded">titleBar.color = WHITE</code></li><li class="">加载时，也就是程序初始化的时候 <code class="px-1 bg-gray-100 border-2 rounded">titleBar.color = </code></li></ol><h2 class="my-2 font-semibold text-xl">第十四章</h2><p class="my-4 font-light">语句一般也分为有副作用的语句和没有副作用的语句，如果前后两个语句都在更新同一个变量，那这样是不好的。至少我们要减少有副作用的语句。这样可以使代码的依赖变得更加明显。代码是按照顺序执行的，对于后一句依赖前一句的代码</p><p class="my-4 font-light">比如：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>BAD:

<span class="hljs-function"><span class="hljs-title">ComputeMarketingExpense</span><span class="hljs-params">()</span></span>
<span class="hljs-function"><span class="hljs-title">ComputeSaleExpense</span><span class="hljs-params">()</span></span>
<span class="hljs-function"><span class="hljs-title">ComouteTravelExpense</span><span class="hljs-params">()</span></span>
<span class="hljs-function"><span class="hljs-title">ComputePersonalExpense</span><span class="hljs-params">()</span></span>
<span class="hljs-function"><span class="hljs-title">DispalyExpenseSummary</span><span class="hljs-params">()</span></span>

Good:

market = <span class="hljs-built_in">ComputeMarkingExpense</span>()
sale = <span class="hljs-built_in">ComputeSaleExpense</span>()
travle = <span class="hljs-built_in">ComputeTravelExpense</span>()
personal = <span class="hljs-built_in">ComputePersonalExpense</span>()
<span class="hljs-function"><span class="hljs-title">Dispaly</span><span class="hljs-params">(ExpenseSummery(market, sale, travle, personal)</span></span></code></pre><p class="my-4 font-light">对于没有依赖关系的代码，应该让他们尽可能分组，方便自上而下的阅读。最好做到相关代码构成的语句块之间不要有重叠</p><p class="my-4 font-light">BAD：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>MarketingData marketingData;
SalesData salesData;
TravelData travelData;

travelData.<span class="hljs-constructor">ComputeDuarerly()</span>
salesData.<span class="hljs-constructor">ComputeQuarterly()</span>
maketingData.<span class="hljs-constructor">ComputeQuarterly()</span>

salesData.<span class="hljs-constructor">Print()</span>
travelData.<span class="hljs-constructor">Print()</span>
marketingData.<span class="hljs-constructor">Print()</span></code></pre><p class="my-4 font-light">Good:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>MarketingData marketingData;
marketingData.<span class="hljs-constructor">ComputeQuarterly()</span>
marketingData.<span class="hljs-constructor">Print()</span>

TravelData travelData;
travelData.<span class="hljs-constructor">ComputeQuarterly()</span>
travelData.<span class="hljs-constructor">Print()</span>

SalesData salesData
salesData.<span class="hljs-constructor">ComputeQuerterly()</span>
salesData.<span class="hljs-constructor">Print()</span></code></pre><h2 class="my-2 font-semibold text-xl">第十五章 使用条件语句</h2><p class="my-4 font-light">书中写到应该优先处理正常情况，但是这样的话可能造成箭头形的代码，个人认为还是有限处理错误并返回比较好。不过如果是正常情况的分支的话，应该优先处理常见情况。对于 switch 语句，应该在 default 字句中处理错误</p><p class="my-4 font-light">对于大长串的 and 判断条件建议写一个 is_XXX 函数来封装一下。</p><h2 class="my-2 font-semibold text-xl">第十六章 控制循环</h2><p class="my-4 font-light">循环的种类：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">计数循环，按照规定的次数执行多次</li><li class="">连续求值的循环，预先并不知道需要运行多少次，每次迭代都检查是否需要结束（比如用户选择退出，或者遇到了错误）</li><li class="">无限循环，比如内核，事件循环</li><li class="">迭代器循环，对容器类中的每一个元素执行一次操作</li></ol><p class="my-4 font-light">研究显示下面这种代码比传统的 while 或者 do-while 代码更容易理解，以为如果把退出条件强行放到开始或者结尾，那么很难避免重复代码。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">while</span> (true) {
    <span class="hljs-regexp">//</span> ...
    <span class="hljs-keyword">if</span> (xxx) <span class="hljs-keyword">break</span>
   <span class="hljs-regexp">//</span> ...
}</code></pre><p class="my-4 font-light">像是 for 和 foreach 循环最好用在简单的控制条件，不要在循环体内做改动下标等操作。</p><h3 class="my-2 font-semibold ">安全计数器</h3><p class="my-4 font-light">对于可能无法终止的循环，可以考虑添加一个安全计数器来保证不会出现死循环。</p><p class="my-4 font-light">BAD:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>do {
    <span class="hljs-keyword">node</span> <span class="hljs-title">= node-</span>&gt;Next;
} while (<span class="hljs-keyword">node</span><span class="hljs-title">-Next</span> != NULL);</code></pre><p class="my-4 font-light">Good:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>safeCounter = <span class="hljs-number">0</span>;
do {
    <span class="hljs-keyword">node</span> <span class="hljs-title">= node-</span>&gt;Next;
    ...
    safeCounter++;
    if (safeCounter &gt;= SATETY_LIMITE) {
        Assert(<span class="hljs-literal">false</span>, <span class="hljs-string">&quot;Internal Error: safe counter violation&quot;</span>)
    }
    ...
} while (<span class="hljs-keyword">node</span><span class="hljs-title">-&gt;Next</span> != NULL);</code></pre><h3 class="my-2 font-semibold ">continue 和 break</h3><p class="my-4 font-light">continue 语句就相当于一个 else 语句，和之前所述一样，尽量在循环开始的地方使用 continue 语句。对于 return 语句也是一样的。</p><p class="my-4 font-light">需要特别注意的是 switch 中也允许 break 语句，所以很可能导致潜在的 bug</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment">// BAD：</span>
network <span class="hljs-selector-tag">code</span>()    
{    
    <span class="hljs-built_in">switch</span>(line)   
    {    
        case  THING1:   
            doit1();   
            break;    
        case  THING2:   
            if(x==STUFF)   
            {    
                <span class="hljs-built_in">do_first_stuff</span>();    
                <span class="hljs-built_in">if</span>(y==OTHER_STUFF)    
                    break;    
                <span class="hljs-built_in">do_later_stuff</span>();    
            }    
            <span class="hljs-comment">/*代码的意图是跳转到这里… …*/</span>    
            <span class="hljs-built_in">initialize_modes_pointer</span>();   
            break;    
        default :    
            processing();    
    }   
    <span class="hljs-comment">/*… …但事实上跳到了这里。*/</span>    
    <span class="hljs-built_in">use_modes_pointer</span>(); <span class="hljs-comment">/*致使 modes_pointer 指针未初始化*/</span>    
}</code></pre><p class="my-4 font-light">把循环控制在 3 级以内，否则人类就不能理解。把长循环的内容移到子程序中。</p><p class="my-4 font-light">可以由内而外的构建循环，也就是说先写出循环体要处理的内容，再去构建循环条件。</p><h2 class="my-2 font-semibold text-xl">第十七章 其他控制结构</h2><p class="my-4 font-light">递归，首先要写终止语句，并且要尽量避免迭代次数过多的递归，因为每次递归都会产生一个调用栈，可以使用安全计数器来避免无限递归</p><h2 class="my-2 font-semibold text-xl">第十八章 表驱动法</h2><p class="my-4 font-light">表驱动法把复杂的条件分支语句转化成通过查表语句。表驱动法还有一个好处就是可以把解析的逻辑放到外部文件中。表驱动的两个问题：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">使用什么 key，如果对应的条件不能直接做 key，可以使用一个函数转化成 enum</li><li class="">使用什么 value</li></ol><p class="my-4 font-light">BAD:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">if</span> <span class="hljs-attr">month</span> == <span class="hljs-number">1</span>:
    <span class="hljs-attr">days</span> = <span class="hljs-number">31</span>
elif <span class="hljs-attr">month</span> == <span class="hljs-number">2</span>:
    <span class="hljs-attr">days</span> = <span class="hljs-number">28</span>
elif <span class="hljs-attr">month</span> == <span class="hljs-number">3</span>:
    <span class="hljs-attr">days</span> = <span class="hljs-number">31</span>
...
<span class="hljs-keyword">else</span> <span class="hljs-attr">month</span> == <span class="hljs-number">12</span>:
    <span class="hljs-attr">days</span> = <span class="hljs-number">31</span></code></pre><p class="my-4 font-light">GOOD:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attr">days_per_month</span> = [<span class="hljs-number">31</span>, <span class="hljs-number">28</span>, <span class="hljs-number">31</span>, <span class="hljs-number">30</span>, <span class="hljs-number">31</span>, <span class="hljs-number">30</span>, <span class="hljs-number">31</span>, <span class="hljs-number">31</span>, <span class="hljs-number">30</span>, <span class="hljs-number">31</span>, <span class="hljs-number">30</span>, <span class="hljs-number">31</span>]
<span class="hljs-attr">days</span> = days_per_month[month -<span class="hljs-number">1</span>]</code></pre><h3 class="my-2 font-semibold ">阶梯访问表</h3><p class="my-4 font-light">比如根据分数来确定学生的评级</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>range_limit = <span class="hljs-selector-attr">[50.0, 65.0, 75.0, 90.0, 100.0]</span>
grades = <span class="hljs-selector-attr">[<span class="hljs-string">&#x27;F&#x27;</span>, <span class="hljs-string">&#x27;D&#x27;</span>, <span class="hljs-string">&#x27;C&#x27;</span>, <span class="hljs-string">&#x27;B&#x27;</span>, <span class="hljs-string">&#x27;A&#x27;</span>]</span>
max_grade_level = <span class="hljs-built_in">len</span>(grade) - <span class="hljs-number">1</span>

grade_level = <span class="hljs-number">0</span>
student_grade = <span class="hljs-string">&#x27;A&#x27;</span>
while student_grade == <span class="hljs-string">&#x27;A&#x27;</span> and grade_level &lt; max_grade_level:
    <span class="hljs-keyword">if</span> student_score &lt; range_limit<span class="hljs-selector-attr">[grade_level]</span>:
        student_grade = grades<span class="hljs-selector-attr">[grade_level]</span>
    grade_level += <span class="hljs-number">1</span></code></pre><h2 class="my-2 font-semibold text-xl">第十九章 一般控制问题</h2><p class="my-4 font-light">if 语句</p><p class="my-4 font-light">对于复杂的 if 语句来说，也可以考虑使用决策表来简化操作。
对于 if 语句中的判断，按照书中的顺序来编写数值表达式：<code class="px-1 bg-gray-100 border-2 rounded">if 1 &lt; a and a  &lt; 3</code></p><p class="my-4 font-light">度量复杂度的 Tom McCabe 方法，根据决策点来测试：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">从 1 开始</li><li class="">遇到 if、while、repeat、for、and、or 复杂度加 1</li><li class="">遇到 case 语句，每种情况 +1</li></ol><p class="my-4 font-light">0-5， 函数还不错
6-10，需要改进
10+，必须改进</p><h2 class="my-2 font-semibold text-xl">第二十章</h2><p class="my-4 font-light">没啥可记得</p><h2 class="my-2 font-semibold text-xl">第二十一章</h2><p class="my-4 font-light">没啥可记得</p><h2 class="my-2 font-semibold text-xl">第二十二章 开发者测试</h2><ul class="my-4 ml-4 font-light list-disc"><li class="">单元测试 一个程序员或者一个团队的代码的测试</li><li class="">组件测试 被测代码涉及到多个程序或团队</li><li class="">集成测试 对两个或多个包进行测试，通常应该尽早开始</li><li class="">回归测试 重复执行以前的测试，以便查找是否有 bug 复发</li><li class="">系统测试 在最终配置下运行整个软件</li></ul><p class="my-4 font-light">推荐的测试方法</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">对每项需求进行测试，最好在需求阶段就准备好测试用例</li><li class="">对每一个设计的关注点进行测试</li><li class="">基础测试</li><li class="">测试已经犯过的错误</li></ol><h3 class="my-2 font-semibold ">基础测试</h3><p class="my-4 font-light">目标是覆盖每一个路径，采用的方法和之前计算代码复杂度的方法一样。</p><h2 class="my-2 font-semibold text-xl">第三十章</h2><p class="my-4 font-light">如果你发现自己每天多次键入某个长度超过 5 个字母的命令，那么应该用一个脚本</p><h2 class="my-2 font-semibold text-xl">第三十一章</h2><p class="my-4 font-light">没啥值得记录的</p><h2 class="my-2 font-semibold text-xl">第三十二章 自说明代码</h2><p class="my-4 font-light">IBM 的研究显示：平均每十行一个注释的代码可读性最高。</p><p class="my-4 font-light">代码注释应该着眼于 why 而不是 how</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>BAD

<span class="hljs-string">//</span> <span class="hljs-keyword">if</span> acccount flag is zero
<span class="hljs-keyword">if</span> <span class="hljs-params">(<span class="hljs-attr">accountFlag</span> == 0)</span> <span class="hljs-string">...</span>

GOOD 
<span class="hljs-string">//</span> <span class="hljs-keyword">if</span> establishing a new acccount
<span class="hljs-keyword">if</span> <span class="hljs-params">(<span class="hljs-attr">accountFlag</span> == 0)</span> <span class="hljs-string">...</span>

BETTER

<span class="hljs-keyword">if</span> <span class="hljs-params">(account.<span class="hljs-attr">type</span> == AccountType.NewAccount)</span> <span class="hljs-string">...</span></code></pre><p class="my-4 font-light">对于非常规的行为要给与注释</p><h2 class="my-2 font-semibold text-xl">第三十三章 个人性格</h2><p class="my-4 font-light">你越是谦虚，进步就越快。</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">将系统“分解”，是为了使之更易于理解</li><li class="">进行 review 和 test 是为了减少人为失误（egoless programming）</li><li class="">将子程序编写的短小，以减轻大脑的负担</li><li class="">基于问题而不是底层实现细节来编程，从而减少工作量</li><li class="">通过各种各样的规范，将思路从相对繁琐的编程事务中解放出来。</li></ol><h3 class="my-2 font-semibold ">求知欲</h3><p class="my-4 font-light">如果分配给你的工作净是些不能提高自身技能的短期任务，你理应表示不满。如果正处于竞争激烈的软件市场，则目前工作用到的一半的知识将在三年后过期。假如不持续学习，你就会落伍。</p><p class="my-4 font-light">如果在工作中学不到什么，就找一份新的工作吧。</p><p class="my-4 font-light">如果不了解所用语言的某一特性是怎么回事，可编写一个小程序来检验，看看它是如何工作的。请在调试器中观察程序的执行情况。用一个小程序来检验某一概念，总比编写大程序时运用不太了解的特性要好。</p><p class="my-4 font-light">如果小程序表现的特性与你设想的不一样，怎么办呢？那正是你要研究的问题。最好通过小程序找出答案，而不要用大程序。有效编程的关键之一就是要学会迅速制造错误，并且每次都能从中有所收获。犯错不是罪过，从中学不到什么才是罪过。</p><p class="my-4 font-light">阅读解决问题的有关方法。解决问题是软件创作过程中的核心行为。就算你想再发明个车轮，也不会注定成功，你发明的也许是方车轮。</p><p class="my-4 font-light">在行动之前作出分析和计划。在分析和行动直接有着矛盾关系，然而多数程序员的问题不在于分析过度。</p><p class="my-4 font-light">学习成功项目的开发经验。Jon Bentley 认为你应该坐下来，准备一杯白兰地，点一根上好的雪茄，想看优秀小说一样来阅读程序。至少应该研究高层设计，并有选择地去研究某些地方的细节源代码。</p><p class="my-4 font-light">Thomas Kuhn 指出，凡是成熟的学科都是从解决问题发展起来的。</p><p class="my-4 font-light">不仅要阅读别人的代码，还应渴望了解专家对你的代码的看法。</p><p class="my-4 font-light"><strong>诚实</strong></p><h3 class="my-2 font-semibold ">偷懒的三个境界</h3><ol class="my-4 ml-4 font-light list-decimal"><li class="">拖延不喜欢的任务</li><li class="">迅速做完不喜欢的任务，以摆脱</li><li class="">编写某个工具来完成不喜欢的任务，以便再也不会做这些事</li></ol><p class="my-4 font-light">理论上当然是这样的，然而经常会遇到的讨厌的事情并不是重复性的，而是每次都是不同的恶心事情，或者 lead 催很紧，没有时间做类似的工具。</p><h3 class="my-2 font-semibold ">坚持</h3><p class="my-4 font-light">不要坚持，如果花了 15 分钟调试仍然没有进展，就该放弃排错过程，让潜意识仔细品品。和计算机错误斗气是不明智的，更好的方法是避开他们。</p><h3 class="my-2 font-semibold ">经验</h3><p class="my-4 font-light">人们还荒唐地强调程序员有多少经验。“我们需要有五年以上 C 语言编程经验的程序员”就是愚蠢的说法。如果程序员过了前一两年还没有学好 C 语言，那么再过个三年也没有什么意义。</p><p class="my-4 font-light">最后一个问题，如果你工作十年，你会得到十年经验还是一年经验的十次重复？必须检讨自己的行为，才能获得真正的经验。只有坚持不懈地学习，才能获取经验；如果不这样做，无论你工作多少年，都无法获得经验。</p><h3 class="my-2 font-semibold ">Gonzo Programming</h3><p class="my-4 font-light">彻夜编程让你感觉像是世界上最好的程序员，却要花上几个星期去纠正你在短暂辉煌时埋下的错误。可以热爱编程，但是热情不能代替熟练的能力，请想明白什么更重要。</p><h3 class="my-2 font-semibold ">习惯</h3><p class="my-4 font-light">不能用“没有习惯”来代替“坏习惯”，只能用“新习惯”代替“坏习惯”</p><h2 class="my-2 font-semibold text-xl">第三十四章</h2><p class="my-4 font-light">基于问题域编程，最顶层代码不要关心实现细节。</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Linux 命令行压缩工具]]></title>
            <link>https://yifei.me/note/425</link>
            <guid>https://yifei.me/note/425</guid>
            <pubDate>Sat, 19 Aug 2023 12:12:30 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">压缩文件主要分四类：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">tar.gz 或者 tgz 文件</li><li class="">zip 文件</li><li class="">rar 文件，Linux 下几乎没有</li><li class="">7z 文件</li></ol><h2 class="my-2 font-semibold text-xl">tar.gz 文件</h2><p class="my-4 font-light">压缩</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-function"><span class="hljs-title">tar</span></span> cvzf <span class="hljs-keyword">FILES</span>...</code></pre><p class="my-4 font-light">解压</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">tar xvzf TARBALL</span></code></pre><h2 class="my-2 font-semibold text-xl">zip 文件</h2><p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">zip -r ZIPFILE DIRECTORY</code> to zip a directory</p><p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">zip -r ZIPFILE DIRECTORY -x &quot;*.git*&quot;</code> exclude the .git directory</p><p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">zip -l ZIPFILE</code> list the files</p><h2 class="my-2 font-semibold text-xl">7z 文件</h2><p class="my-4 font-light">安装对应工具</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>apt-<span class="hljs-keyword">get</span> install p7zip-<span class="hljs-keyword">full</span></code></pre><p class="my-4 font-light">解压缩</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-number">7</span>z x <span class="hljs-keyword">some</span>.<span class="hljs-number">7</span>z</code></pre>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">压缩文件主要分四类：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">tar.gz 或者 tgz 文件</li><li class="">zip 文件</li><li class="">rar 文件，Linux 下几乎没有</li><li class="">7z 文件</li></ol><h2 class="my-2 font-semibold text-xl">tar.gz 文件</h2><p class="my-4 font-light">压缩</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-function"><span class="hljs-title">tar</span></span> cvzf <span class="hljs-keyword">FILES</span>...</code></pre><p class="my-4 font-light">解压</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">tar xvzf TARBALL</span></code></pre><h2 class="my-2 font-semibold text-xl">zip 文件</h2><p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">zip -r ZIPFILE DIRECTORY</code> to zip a directory</p><p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">zip -r ZIPFILE DIRECTORY -x &quot;*.git*&quot;</code> exclude the .git directory</p><p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">zip -l ZIPFILE</code> list the files</p><h2 class="my-2 font-semibold text-xl">7z 文件</h2><p class="my-4 font-light">安装对应工具</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>apt-<span class="hljs-keyword">get</span> install p7zip-<span class="hljs-keyword">full</span></code></pre><p class="my-4 font-light">解压缩</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-number">7</span>z x <span class="hljs-keyword">some</span>.<span class="hljs-number">7</span>z</code></pre>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Podman 对比 Docker 使用笔记]]></title>
            <link>https://yifei.me/note/1098</link>
            <guid>https://yifei.me/note/1098</guid>
            <pubDate>Sat, 19 Aug 2023 12:12:30 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">podman 基本上和 docker 命令兼容</p><ul class="my-4 ml-4 font-light list-disc"><li class="">没有 daemon，默认不使用 root</li><li class="">可以导出 k8s 部署配置</li><li class="">可以一键停止删除所有容器，stop -a, rm -a, rmi -a 等</li><li class="">增加了 <code class="px-1 bg-gray-100 border-2 rounded">-l、--latest</code> 选项，直接操作上一个 container，而不需要找到 id 然后再操作。</li></ul><p class="my-4 font-light">Podman 最贴心的的部分在于命令行更加人性化。</p><p class="my-4 font-light">比如说：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>podman <span class="hljs-built_in">stop</span> -l
podman rm -<span class="hljs-keyword">a</span></code></pre><h2 class="my-2 font-semibold text-xl">podman-compose</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>pip3 <span class="hljs-keyword">install</span> podman-compose</code></pre><h2 class="my-2 font-semibold text-xl">podman play</h2><h2 class="my-2 font-semibold text-xl">坑</h2><p class="my-4 font-light">podman 有个 bug，默认不会打开 cache，需要使用 <code class="px-1 bg-gray-100 border-2 rounded">--layers</code> 手动打开.</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>podman build <span class="hljs-comment">--layers .</span></code></pre><h2 class="my-2 font-semibold text-xl">Use podman on macOS</h2><p class="my-4 font-light">虽然在 mac 上，可以用 podman 自己带的虚拟机，但是这个虚拟机是 CentOS 的系统，不太习惯，
所以还是用 multipass 起一个 ubuntu 的机器来用吧。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">multipass</span> launch -c <span class="hljs-number">2</span> -m <span class="hljs-number">8</span>G -d <span class="hljs-number">64</span>G -n podman
<span class="hljs-attribute">multipass</span> mount $HOME podman:/home/ubuntu/host
<span class="hljs-attribute">multipass</span> shell podman</code></pre><h2 class="my-2 font-semibold text-xl">Install latest podman on Ubuntu</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-meta">#!/bin/sh</span>

ubuntu_version=<span class="hljs-string">&#x27;22.04&#x27;</span>
key_url=<span class="hljs-string">&quot;https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_<span class="hljs-variable">${ubuntu_version}</span>/Release.key&quot;</span>
sources_url=<span class="hljs-string">&quot;https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_<span class="hljs-variable">${ubuntu_version}</span>&quot;</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;deb <span class="hljs-variable">$sources_url</span>/ /&quot;</span> | <span class="hljs-built_in">tee</span> /etc/apt/sources.list.d/devel:kubic:libcontainers:unstable.list
curl -fsSL <span class="hljs-variable">$key_url</span> | gpg --dearmor | <span class="hljs-built_in">tee</span> /etc/apt/trusted.gpg.d/devel_kubic_libcontainers_unstable.gpg &gt; /dev/null
apt update
apt install podman</code></pre><h2 class="my-2 font-semibold text-xl">build amd64 image on ARM64/M1</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>apt<span class="hljs-operator">-</span><span class="hljs-keyword">get</span> install <span class="hljs-operator">-</span>y qemu<span class="hljs-operator">-</span><span class="hljs-keyword">user</span><span class="hljs-operator">-</span><span class="hljs-keyword">static</span>
reboot
podman build <span class="hljs-comment">--platform linux/amd64</span></code></pre><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://developers.redhat.com/blog/2019/02/21/podman-and-buildah-for-docker-users/" title="undefined">https://developers.redhat.com/blog/2019/02/21/podman-and-buildah-for-docker-users/</a></li><li class=""><a class="underline" href="https://cloud.redhat.com/blog/crictl-vs-podman" title="undefined">https://cloud.redhat.com/blog/crictl-vs-podman</a></li><li class=""><a class="underline" href="https://podman.io/getting-started/" title="undefined">https://podman.io/getting-started/</a></li><li class=""><a class="underline" href="https://askubuntu.com/questions/1414446/whats-the-recommended-way-of-installing-podman-4-in-ubuntu-22-04" title="undefined">https://askubuntu.com/questions/1414446/whats-the-recommended-way-of-installing-podman-4-in-ubuntu-22-04</a></li><li class=""><a class="underline" href="https://dev.to/tkral/podman-build-amd64-images-on-m1-294g" title="undefined">https://dev.to/tkral/podman-build-amd64-images-on-m1-294g</a></li></ol>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">podman 基本上和 docker 命令兼容</p><ul class="my-4 ml-4 font-light list-disc"><li class="">没有 daemon，默认不使用 root</li><li class="">可以导出 k8s 部署配置</li><li class="">可以一键停止删除所有容器，stop -a, rm -a, rmi -a 等</li><li class="">增加了 <code class="px-1 bg-gray-100 border-2 rounded">-l、--latest</code> 选项，直接操作上一个 container，而不需要找到 id 然后再操作。</li></ul><p class="my-4 font-light">Podman 最贴心的的部分在于命令行更加人性化。</p><p class="my-4 font-light">比如说：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>podman <span class="hljs-built_in">stop</span> -l
podman rm -<span class="hljs-keyword">a</span></code></pre><h2 class="my-2 font-semibold text-xl">podman-compose</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>pip3 <span class="hljs-keyword">install</span> podman-compose</code></pre><h2 class="my-2 font-semibold text-xl">podman play</h2><h2 class="my-2 font-semibold text-xl">坑</h2><p class="my-4 font-light">podman 有个 bug，默认不会打开 cache，需要使用 <code class="px-1 bg-gray-100 border-2 rounded">--layers</code> 手动打开.</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>podman build <span class="hljs-comment">--layers .</span></code></pre><h2 class="my-2 font-semibold text-xl">Use podman on macOS</h2><p class="my-4 font-light">虽然在 mac 上，可以用 podman 自己带的虚拟机，但是这个虚拟机是 CentOS 的系统，不太习惯，
所以还是用 multipass 起一个 ubuntu 的机器来用吧。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">multipass</span> launch -c <span class="hljs-number">2</span> -m <span class="hljs-number">8</span>G -d <span class="hljs-number">64</span>G -n podman
<span class="hljs-attribute">multipass</span> mount $HOME podman:/home/ubuntu/host
<span class="hljs-attribute">multipass</span> shell podman</code></pre><h2 class="my-2 font-semibold text-xl">Install latest podman on Ubuntu</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-meta">#!/bin/sh</span>

ubuntu_version=<span class="hljs-string">&#x27;22.04&#x27;</span>
key_url=<span class="hljs-string">&quot;https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_<span class="hljs-variable">${ubuntu_version}</span>/Release.key&quot;</span>
sources_url=<span class="hljs-string">&quot;https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable/xUbuntu_<span class="hljs-variable">${ubuntu_version}</span>&quot;</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;deb <span class="hljs-variable">$sources_url</span>/ /&quot;</span> | <span class="hljs-built_in">tee</span> /etc/apt/sources.list.d/devel:kubic:libcontainers:unstable.list
curl -fsSL <span class="hljs-variable">$key_url</span> | gpg --dearmor | <span class="hljs-built_in">tee</span> /etc/apt/trusted.gpg.d/devel_kubic_libcontainers_unstable.gpg &gt; /dev/null
apt update
apt install podman</code></pre><h2 class="my-2 font-semibold text-xl">build amd64 image on ARM64/M1</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>apt<span class="hljs-operator">-</span><span class="hljs-keyword">get</span> install <span class="hljs-operator">-</span>y qemu<span class="hljs-operator">-</span><span class="hljs-keyword">user</span><span class="hljs-operator">-</span><span class="hljs-keyword">static</span>
reboot
podman build <span class="hljs-comment">--platform linux/amd64</span></code></pre><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://developers.redhat.com/blog/2019/02/21/podman-and-buildah-for-docker-users/" title="undefined">https://developers.redhat.com/blog/2019/02/21/podman-and-buildah-for-docker-users/</a></li><li class=""><a class="underline" href="https://cloud.redhat.com/blog/crictl-vs-podman" title="undefined">https://cloud.redhat.com/blog/crictl-vs-podman</a></li><li class=""><a class="underline" href="https://podman.io/getting-started/" title="undefined">https://podman.io/getting-started/</a></li><li class=""><a class="underline" href="https://askubuntu.com/questions/1414446/whats-the-recommended-way-of-installing-podman-4-in-ubuntu-22-04" title="undefined">https://askubuntu.com/questions/1414446/whats-the-recommended-way-of-installing-podman-4-in-ubuntu-22-04</a></li><li class=""><a class="underline" href="https://dev.to/tkral/podman-build-amd64-images-on-m1-294g" title="undefined">https://dev.to/tkral/podman-build-amd64-images-on-m1-294g</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在 Python 中使用 SQLite]]></title>
            <link>https://yifei.me/note/2405</link>
            <guid>https://yifei.me/note/2405</guid>
            <pubDate>Sat, 19 Aug 2023 12:12:30 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">首先连接到数据库，获得 connection 对象，然后再获得 cursor，使用 cursor 来执行 sql 语句并
获取结果。</p><h2 class="my-2 font-semibold text-xl">连接</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">import</span> sqlite3

db = sqlite3.connect(<span class="hljs-string">&quot;database.db&quot;</span>)  <span class="hljs-comment"># connections</span>
db.execute(<span class="hljs-string">&quot;CREATE TABLE books (id int primary key, name text)&quot;</span>)
db.execute(<span class="hljs-string">&quot;INSERT INTO books (name) VALUES (&#x27;war and peace&#x27;)&quot;</span>)
db.execute(<span class="hljs-string">&quot;INSERT INTO books (name) VALUES (&#x27;the bible&#x27;)&quot;</span>)
db.commit()  <span class="hljs-comment"># always remember to commit</span>
db.execute(<span class="hljs-string">&quot;SELECT * FROM books&quot;</span>)

db.close()

<span class="hljs-comment"># 或者使用 with 语句</span>
<span class="hljs-keyword">with</span> sqlite3.connect(<span class="hljs-string">&quot;db&quot;</span>) <span class="hljs-keyword">as</span> db:
    <span class="hljs-comment"># cursor executes</span></code></pre><p class="my-4 font-light">虽然 Python 的 DB API 2.0 规范是要求使用 db.cursor() 获得一个 cursor 来执行 execute, 但是
对 sqlite 来说完全没有必要，直接用 db.execute 这种快捷方式就好了。</p><h2 class="my-2 font-semibold text-xl">构建语句</h2><p class="my-4 font-light">不要用 Python 自带的字符串格式化，可能有 sql 注入的风险，要使用问号或者冒号格式化。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># fabricating statement</span>
db.execute(<span class="hljs-string">&quot;select from books where name = ?&quot;</span>, [<span class="hljs-string">&quot;the bible&quot;</span>]) 
<span class="hljs-comment"># NOTE the param must be a sequence</span>

<span class="hljs-comment"># You could also use named placeholders</span>
db.execute(<span class="hljs-string">&quot;insert into books (name) values (:name)&quot;</span>, {name: <span class="hljs-string">&quot;the bible&quot;</span>})</code></pre><h2 class="my-2 font-semibold text-xl">获取数据</h2><p class="my-4 font-light">有两种方式：</p><p class="my-4 font-light">使用 <code class="px-1 bg-gray-100 border-2 rounded">fetchone, fetchmany(n), fetchall</code></p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">r</span> <span class="hljs-operator">=</span> db.execute(<span class="hljs-string">&quot;SELECT id FROM stocks WHERE name = &#x27;MSFT&#x27;&quot;</span>)
<span class="hljs-attribute">id</span> <span class="hljs-operator">=</span> r.fetchone()[<span class="hljs-number">0</span>]</code></pre><p class="my-4 font-light">直接迭代返回结果</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">for</span> <span class="hljs-built_in">row</span> <span class="hljs-keyword">in</span> db.execute(<span class="hljs-string">&quot;SELECT * FROM stocks ORDER BY price&quot;</span>):
    <span class="hljs-built_in">print</span> <span class="hljs-built_in">row</span>[<span class="hljs-number">0</span>], <span class="hljs-built_in">row</span>[<span class="hljs-number">1</span>]</code></pre><p class="my-4 font-light">注意结果每行是一个 tuple，即使 select 了一个元素，结果也是 tuple。</p><p class="my-4 font-light">如果我们想要返回一个字典，需要更改一下 row_factory, 推荐使用 sqlite3.Row</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attr">db.row_factory</span> = sqlite3.Row</code></pre><p class="my-4 font-light">lastrowid 属性。This read-only attribute provides the rowid of the last modified row. It
is only set if you issued a INSERT statement using the execute()method. For operations
other than INSERT or when executemany() is called, lastrowid is set to None.</p><h2 class="my-2 font-semibold text-xl">加载拓展</h2><p class="my-4 font-light">Python 的 sqlite 可能没有加载任何拓展（咋想的？), 想要加载拓展，需要手动加载：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>db.enable<span class="hljs-constructor">_load_extension(True)</span>
db.load<span class="hljs-constructor">_extension(<span class="hljs-string">&quot;fts5&quot;</span>)</span>
db.enable<span class="hljs-constructor">_load_extension(False)</span></code></pre><h2 class="my-2 font-semibold text-xl">自定义类型</h2><p class="my-4 font-light">sqlite 中只包含了有限的几种数据类型，甚至连 datetime 都没有。在 Python 中可以自定义数据
类型，而且已经内置了 date 和 timestamp 两个类型的转换。</p><p class="my-4 font-light">只需要在创建连接的时候指定对应的 flag, 然后在创建表的时候指定好数据类型是 date 和 timestamp
就好了，sqlite 内部使用数字存储，但是在 Python 中会自动转换。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>db = sqlite3.connect(<span class="hljs-string">&quot;:memory:&quot;</span>, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
db.execute(<span class="hljs-string">&quot;create table test(d date, ts timestamp)&quot;</span>)
today = datetime.date.today()
now = datetime.datetime.now()

db.execute(<span class="hljs-string">&quot;insert into test(d, ts) values (?, ?)&quot;</span>, (today, now))
cur = db.execute(<span class="hljs-string">&quot;select d, ts from test&quot;</span>)
row = cur.fetchone()
<span class="hljs-built_in">print</span>(today, <span class="hljs-string">&quot;=&gt;&quot;</span>, row[<span class="hljs-number">0</span>], <span class="hljs-built_in">type</span>(row[<span class="hljs-number">0</span>]))
<span class="hljs-built_in">print</span>(now, <span class="hljs-string">&quot;=&gt;&quot;</span>, row[<span class="hljs-number">1</span>], <span class="hljs-built_in">type</span>(row[<span class="hljs-number">1</span>]))</code></pre><h2 class="my-2 font-semibold text-xl">事务</h2><p class="my-4 font-light">默认情况下，sqlite 本身是在 autocommit 模式执行的，也就是说语句提交了就自动 commit 了。
但是在 Python 中，默认<strong>不是</strong>在 autocommit 模式下的，而且会隐式插入一条 <code class="px-1 bg-gray-100 border-2 rounded">begin</code> 语句
开启一个事务，这样的好处是可以使用 with 语句，当抛出异常的时候直接回滚。也可以关闭这种
模式，直接使用 sqlite 自己的 autocommit 模式，这样也不用手工 commit 了或者使用 with 语句了。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 使用事务</span>
<span class="hljs-keyword">with</span> db:
  <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">10</span>):
    db.execute(<span class="hljs-string">&quot;inset into books (name) values (1)&quot;</span>)</code></pre><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 直接 autocommit</span>
db.isolation_level=<span class="hljs-literal">None</span></code></pre><h2 class="my-2 font-semibold text-xl">备份</h2><p class="my-4 font-light">相当于 sqlite3 的 <code class="px-1 bg-gray-100 border-2 rounded">.backup</code> 命令</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">target</span> <span class="hljs-operator">=</span> sqlite3.connect(<span class="hljs-string">&quot;backup.db&quot;</span>)
db.backup(target)</code></pre><h2 class="my-2 font-semibold text-xl">异常</h2><p class="my-4 font-light">分为两类：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>sqlite3<span class="hljs-selector-class">.Warning</span>
sqlite3<span class="hljs-selector-class">.Error</span>
  - sqlite3<span class="hljs-selector-class">.DatabaseError</span>
  - sqlite3<span class="hljs-selector-class">.IntegrityError</span>
  - sqlite3<span class="hljs-selector-class">.ProgrammingError</span>
  - sqlite3<span class="hljs-selector-class">.OperationalError</span>
  - sqlite3.NotSupportedError</code></pre><h2 class="my-2 font-semibold text-xl">多线程</h2><p class="my-4 font-light">默认情况下，sqlite3 禁止多线程使用同一个链接，可以使用 check_same_thread 参数来控制该行为</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attr">db</span> = sqlite3.connect(<span class="hljs-string">&quot;sample.db&quot;</span>, check_same_thread=<span class="hljs-literal">False</span>)</code></pre><p class="my-4 font-light">不过这时候就需要自己来保证序列顺序了。</p><h2 class="my-2 font-semibold text-xl">使用最新版本的 sqlite3</h2><p class="my-4 font-light">要想使用最新版本的 sqlite3, 要么在编译的时候已经安装了，要么就需要指定一下链接的路径。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-built_in">export</span> <span class="hljs-attribute">CFLAGS</span>=<span class="hljs-string">&quot;-DSQLITE_ENABLE_FTS3 \
  -DSQLITE_ENABLE_FTS3_PARENTHESIS \
  -DSQLITE_ENABLE_FTS4 \
  -DSQLITE_ENABLE_FTS5 \
  -DSQLITE_ENABLE_JSON1 \
  -DSQLITE_ENABLE_LOAD_EXTENSION \
  -DSQLITE_ENABLE_RTREE \
  -DSQLITE_ENABLE_STAT4 \
  -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT \
  -DSQLITE_SOUNDEX \
  -DSQLITE_TEMP_STORE=3 \
  -DSQLITE_USE_URI \
  -O2 \
  -fPIC&quot;</span>
<span class="hljs-built_in">export</span> <span class="hljs-attribute">PREFIX</span>=<span class="hljs-string">&quot;/usr/local&quot;</span>
<span class="hljs-attribute">LIBS</span>=<span class="hljs-string">&quot;-lm&quot;</span> ./configure --disable-tcl --enable-shared <span class="hljs-attribute">--enable-tempstore</span>=always <span class="hljs-attribute">--prefix</span>=<span class="hljs-string">&quot;<span class="hljs-variable">$PREFIX</span>&quot;</span>
make
sudo make install  # Install system-wide.</code></pre><p class="my-4 font-light">这时候还要在指定一下链接路径：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-built_in">export</span> <span class="hljs-attribute">LD_LIBRARY_PATH</span>=/usr/local/lib</code></pre><p class="my-4 font-light">不过如果 prefix 直接使用了 <code class="px-1 bg-gray-100 border-2 rounded">/usr</code> 就不需要了。</p><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://docs.python.org/3/library/sqlite3.html" title="undefined">https://docs.python.org/3/library/sqlite3.html</a></li></ol>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">首先连接到数据库，获得 connection 对象，然后再获得 cursor，使用 cursor 来执行 sql 语句并
获取结果。</p><h2 class="my-2 font-semibold text-xl">连接</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">import</span> sqlite3

db = sqlite3.connect(<span class="hljs-string">&quot;database.db&quot;</span>)  <span class="hljs-comment"># connections</span>
db.execute(<span class="hljs-string">&quot;CREATE TABLE books (id int primary key, name text)&quot;</span>)
db.execute(<span class="hljs-string">&quot;INSERT INTO books (name) VALUES (&#x27;war and peace&#x27;)&quot;</span>)
db.execute(<span class="hljs-string">&quot;INSERT INTO books (name) VALUES (&#x27;the bible&#x27;)&quot;</span>)
db.commit()  <span class="hljs-comment"># always remember to commit</span>
db.execute(<span class="hljs-string">&quot;SELECT * FROM books&quot;</span>)

db.close()

<span class="hljs-comment"># 或者使用 with 语句</span>
<span class="hljs-keyword">with</span> sqlite3.connect(<span class="hljs-string">&quot;db&quot;</span>) <span class="hljs-keyword">as</span> db:
    <span class="hljs-comment"># cursor executes</span></code></pre><p class="my-4 font-light">虽然 Python 的 DB API 2.0 规范是要求使用 db.cursor() 获得一个 cursor 来执行 execute, 但是
对 sqlite 来说完全没有必要，直接用 db.execute 这种快捷方式就好了。</p><h2 class="my-2 font-semibold text-xl">构建语句</h2><p class="my-4 font-light">不要用 Python 自带的字符串格式化，可能有 sql 注入的风险，要使用问号或者冒号格式化。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># fabricating statement</span>
db.execute(<span class="hljs-string">&quot;select from books where name = ?&quot;</span>, [<span class="hljs-string">&quot;the bible&quot;</span>]) 
<span class="hljs-comment"># NOTE the param must be a sequence</span>

<span class="hljs-comment"># You could also use named placeholders</span>
db.execute(<span class="hljs-string">&quot;insert into books (name) values (:name)&quot;</span>, {name: <span class="hljs-string">&quot;the bible&quot;</span>})</code></pre><h2 class="my-2 font-semibold text-xl">获取数据</h2><p class="my-4 font-light">有两种方式：</p><p class="my-4 font-light">使用 <code class="px-1 bg-gray-100 border-2 rounded">fetchone, fetchmany(n), fetchall</code></p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">r</span> <span class="hljs-operator">=</span> db.execute(<span class="hljs-string">&quot;SELECT id FROM stocks WHERE name = &#x27;MSFT&#x27;&quot;</span>)
<span class="hljs-attribute">id</span> <span class="hljs-operator">=</span> r.fetchone()[<span class="hljs-number">0</span>]</code></pre><p class="my-4 font-light">直接迭代返回结果</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">for</span> <span class="hljs-built_in">row</span> <span class="hljs-keyword">in</span> db.execute(<span class="hljs-string">&quot;SELECT * FROM stocks ORDER BY price&quot;</span>):
    <span class="hljs-built_in">print</span> <span class="hljs-built_in">row</span>[<span class="hljs-number">0</span>], <span class="hljs-built_in">row</span>[<span class="hljs-number">1</span>]</code></pre><p class="my-4 font-light">注意结果每行是一个 tuple，即使 select 了一个元素，结果也是 tuple。</p><p class="my-4 font-light">如果我们想要返回一个字典，需要更改一下 row_factory, 推荐使用 sqlite3.Row</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attr">db.row_factory</span> = sqlite3.Row</code></pre><p class="my-4 font-light">lastrowid 属性。This read-only attribute provides the rowid of the last modified row. It
is only set if you issued a INSERT statement using the execute()method. For operations
other than INSERT or when executemany() is called, lastrowid is set to None.</p><h2 class="my-2 font-semibold text-xl">加载拓展</h2><p class="my-4 font-light">Python 的 sqlite 可能没有加载任何拓展（咋想的？), 想要加载拓展，需要手动加载：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>db.enable<span class="hljs-constructor">_load_extension(True)</span>
db.load<span class="hljs-constructor">_extension(<span class="hljs-string">&quot;fts5&quot;</span>)</span>
db.enable<span class="hljs-constructor">_load_extension(False)</span></code></pre><h2 class="my-2 font-semibold text-xl">自定义类型</h2><p class="my-4 font-light">sqlite 中只包含了有限的几种数据类型，甚至连 datetime 都没有。在 Python 中可以自定义数据
类型，而且已经内置了 date 和 timestamp 两个类型的转换。</p><p class="my-4 font-light">只需要在创建连接的时候指定对应的 flag, 然后在创建表的时候指定好数据类型是 date 和 timestamp
就好了，sqlite 内部使用数字存储，但是在 Python 中会自动转换。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>db = sqlite3.connect(<span class="hljs-string">&quot;:memory:&quot;</span>, detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
db.execute(<span class="hljs-string">&quot;create table test(d date, ts timestamp)&quot;</span>)
today = datetime.date.today()
now = datetime.datetime.now()

db.execute(<span class="hljs-string">&quot;insert into test(d, ts) values (?, ?)&quot;</span>, (today, now))
cur = db.execute(<span class="hljs-string">&quot;select d, ts from test&quot;</span>)
row = cur.fetchone()
<span class="hljs-built_in">print</span>(today, <span class="hljs-string">&quot;=&gt;&quot;</span>, row[<span class="hljs-number">0</span>], <span class="hljs-built_in">type</span>(row[<span class="hljs-number">0</span>]))
<span class="hljs-built_in">print</span>(now, <span class="hljs-string">&quot;=&gt;&quot;</span>, row[<span class="hljs-number">1</span>], <span class="hljs-built_in">type</span>(row[<span class="hljs-number">1</span>]))</code></pre><h2 class="my-2 font-semibold text-xl">事务</h2><p class="my-4 font-light">默认情况下，sqlite 本身是在 autocommit 模式执行的，也就是说语句提交了就自动 commit 了。
但是在 Python 中，默认<strong>不是</strong>在 autocommit 模式下的，而且会隐式插入一条 <code class="px-1 bg-gray-100 border-2 rounded">begin</code> 语句
开启一个事务，这样的好处是可以使用 with 语句，当抛出异常的时候直接回滚。也可以关闭这种
模式，直接使用 sqlite 自己的 autocommit 模式，这样也不用手工 commit 了或者使用 with 语句了。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 使用事务</span>
<span class="hljs-keyword">with</span> db:
  <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">10</span>):
    db.execute(<span class="hljs-string">&quot;inset into books (name) values (1)&quot;</span>)</code></pre><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 直接 autocommit</span>
db.isolation_level=<span class="hljs-literal">None</span></code></pre><h2 class="my-2 font-semibold text-xl">备份</h2><p class="my-4 font-light">相当于 sqlite3 的 <code class="px-1 bg-gray-100 border-2 rounded">.backup</code> 命令</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">target</span> <span class="hljs-operator">=</span> sqlite3.connect(<span class="hljs-string">&quot;backup.db&quot;</span>)
db.backup(target)</code></pre><h2 class="my-2 font-semibold text-xl">异常</h2><p class="my-4 font-light">分为两类：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>sqlite3<span class="hljs-selector-class">.Warning</span>
sqlite3<span class="hljs-selector-class">.Error</span>
  - sqlite3<span class="hljs-selector-class">.DatabaseError</span>
  - sqlite3<span class="hljs-selector-class">.IntegrityError</span>
  - sqlite3<span class="hljs-selector-class">.ProgrammingError</span>
  - sqlite3<span class="hljs-selector-class">.OperationalError</span>
  - sqlite3.NotSupportedError</code></pre><h2 class="my-2 font-semibold text-xl">多线程</h2><p class="my-4 font-light">默认情况下，sqlite3 禁止多线程使用同一个链接，可以使用 check_same_thread 参数来控制该行为</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attr">db</span> = sqlite3.connect(<span class="hljs-string">&quot;sample.db&quot;</span>, check_same_thread=<span class="hljs-literal">False</span>)</code></pre><p class="my-4 font-light">不过这时候就需要自己来保证序列顺序了。</p><h2 class="my-2 font-semibold text-xl">使用最新版本的 sqlite3</h2><p class="my-4 font-light">要想使用最新版本的 sqlite3, 要么在编译的时候已经安装了，要么就需要指定一下链接的路径。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-built_in">export</span> <span class="hljs-attribute">CFLAGS</span>=<span class="hljs-string">&quot;-DSQLITE_ENABLE_FTS3 \
  -DSQLITE_ENABLE_FTS3_PARENTHESIS \
  -DSQLITE_ENABLE_FTS4 \
  -DSQLITE_ENABLE_FTS5 \
  -DSQLITE_ENABLE_JSON1 \
  -DSQLITE_ENABLE_LOAD_EXTENSION \
  -DSQLITE_ENABLE_RTREE \
  -DSQLITE_ENABLE_STAT4 \
  -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT \
  -DSQLITE_SOUNDEX \
  -DSQLITE_TEMP_STORE=3 \
  -DSQLITE_USE_URI \
  -O2 \
  -fPIC&quot;</span>
<span class="hljs-built_in">export</span> <span class="hljs-attribute">PREFIX</span>=<span class="hljs-string">&quot;/usr/local&quot;</span>
<span class="hljs-attribute">LIBS</span>=<span class="hljs-string">&quot;-lm&quot;</span> ./configure --disable-tcl --enable-shared <span class="hljs-attribute">--enable-tempstore</span>=always <span class="hljs-attribute">--prefix</span>=<span class="hljs-string">&quot;<span class="hljs-variable">$PREFIX</span>&quot;</span>
make
sudo make install  # Install system-wide.</code></pre><p class="my-4 font-light">这时候还要在指定一下链接路径：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-built_in">export</span> <span class="hljs-attribute">LD_LIBRARY_PATH</span>=/usr/local/lib</code></pre><p class="my-4 font-light">不过如果 prefix 直接使用了 <code class="px-1 bg-gray-100 border-2 rounded">/usr</code> 就不需要了。</p><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://docs.python.org/3/library/sqlite3.html" title="undefined">https://docs.python.org/3/library/sqlite3.html</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[在子线程中 fork 会怎样？]]></title>
            <link>https://yifei.me/note/2457</link>
            <guid>https://yifei.me/note/2457</guid>
            <pubDate>Sat, 19 Aug 2023 12:12:30 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">按照 posix 的规范来说，fork 之后的进程中，应该只有调用 fork 的那个线程。但是实际上，所有
线程都在，只是除了调用 fork 的线程以外，其他线程都被冻结了，并不会执行。一般来说，在 fork
之后继续执行 exec* 的话，是不会有什么大问题的。</p><p class="my-4 font-light">fork 复制的是整个进程的空间，锁也会被复制。</p><p class="my-4 font-light">可能会引起问题的地方：如果在 fork 之前的进程中有锁，而且是被其他的线程持有的，那么 fork
之后的进程中，这个锁永远不会有人来释放了，导致新的进程中的线程永远处于等待锁的状态。</p><p class="my-4 font-light">fork 还会把所有打开的文件，socket 等描述符都复制一份。</p><p class="my-4 font-light">即使在单线程环境中，这也可能引起问题，因为两个进程可能开始争抢同一个资源。所以合理的方式
总是在新的子进程中打开资源（比如数据库），而不是打开资源之后再 fork 出子进程。</p><p class="my-4 font-light">在 Python 中，multiprocessing 默认使用的是 fork, 但是还好我们可以选择使用 spawn. 参见参考
文献 3 和 4.</p><p class="my-4 font-light">在 web server 和 rpc 中，多进程模式下 fork 是何时执行的呢？使用的 fork 还是 spawn? 全局
变量尤其是数据库链接会不会每次初始化？</p><p class="my-4 font-light">在 RPC 服务中，有一种常见的模式，我们创建了一个 Handler 类，在脚本里面直接实例化了一个类，
在这个类的构造函数中初始化了数据库等资源的链接，然后把 my_hanlder.handle 函数交给框架来
作为入口函数，这样合理吗？</p><p class="my-4 font-light">Google 的 gRPC 在这方面显然是一个反面的例子，本身他不是一个 Python 的库，内部使用了各种
奇奇怪怪的技术，在 Python 中使用的时候就遇到了各种问题，以至于没有一个很好的多进程模式。</p><p class="my-4 font-light">在 web server 中，uwsgi/gunicorn 这些 process runner 又是怎样处理的呢？</p><ul class="my-4 ml-4 font-light list-disc"><li class="">uwsgi 默认会<strong>先 load 再 fork</strong>, 这样就会有一些问题，比如全局的数据库链接等等，但是也
可以改成 fork 之后再加载。</li><li class="">gunicorn 和 uwsgi 恰恰相反，默认会<strong>先 fork 再 load</strong>.</li></ul><p class="my-4 font-light">不管怎样，这两个都使用了非常简洁的 prefork 模型，而且在文档中明确说明了会采用那种模式，
非常优秀。</p><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://stackoverflow.com/questions/39890363/what-happens-when-a-thread-forks/39890957" title="undefined">https://stackoverflow.com/questions/39890363/what-happens-when-a-thread-forks/39890957</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/46439740/safe-to-call-multiprocessing-from-a-thread-in-python" title="undefined">https://stackoverflow.com/questions/46439740/safe-to-call-multiprocessing-from-a-thread-in-python</a></li><li class=""><a class="underline" href="https://britishgeologicalsurvey.github.io/science/python-forking-vs-spawn/" title="null">还没看完</a></li><li class=""><a class="underline" href="https://pythonspeed.com/articles/python-multiprocessing/" title="null">还没看完</a></li><li class=""><a class="underline" href="https://github.com/grpc/grpc/issues/16001" title="undefined">https://github.com/grpc/grpc/issues/16001</a></li><li class=""><a class="underline" href="https://eleme.github.io/blog/2016/eleme-python-soa/" title="undefined">https://eleme.github.io/blog/2016/eleme-python-soa/</a></li><li class=""><a class="underline" href="https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html" title="undefined">https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/42439553/how-to-share-in-memory-resources-between-flask-methods-when-deploying-with-gunic" title="undefined">https://stackoverflow.com/questions/42439553/how-to-share-in-memory-resources-between-flask-methods-when-deploying-with-gunic</a></li><li class=""><a class="underline" href="https://instagram-engineering.com/copy-on-write-friendly-python-garbage-collection-ad6ed5233ddf" title="undefined">https://instagram-engineering.com/copy-on-write-friendly-python-garbage-collection-ad6ed5233ddf</a></li></ol>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">按照 posix 的规范来说，fork 之后的进程中，应该只有调用 fork 的那个线程。但是实际上，所有
线程都在，只是除了调用 fork 的线程以外，其他线程都被冻结了，并不会执行。一般来说，在 fork
之后继续执行 exec* 的话，是不会有什么大问题的。</p><p class="my-4 font-light">fork 复制的是整个进程的空间，锁也会被复制。</p><p class="my-4 font-light">可能会引起问题的地方：如果在 fork 之前的进程中有锁，而且是被其他的线程持有的，那么 fork
之后的进程中，这个锁永远不会有人来释放了，导致新的进程中的线程永远处于等待锁的状态。</p><p class="my-4 font-light">fork 还会把所有打开的文件，socket 等描述符都复制一份。</p><p class="my-4 font-light">即使在单线程环境中，这也可能引起问题，因为两个进程可能开始争抢同一个资源。所以合理的方式
总是在新的子进程中打开资源（比如数据库），而不是打开资源之后再 fork 出子进程。</p><p class="my-4 font-light">在 Python 中，multiprocessing 默认使用的是 fork, 但是还好我们可以选择使用 spawn. 参见参考
文献 3 和 4.</p><p class="my-4 font-light">在 web server 和 rpc 中，多进程模式下 fork 是何时执行的呢？使用的 fork 还是 spawn? 全局
变量尤其是数据库链接会不会每次初始化？</p><p class="my-4 font-light">在 RPC 服务中，有一种常见的模式，我们创建了一个 Handler 类，在脚本里面直接实例化了一个类，
在这个类的构造函数中初始化了数据库等资源的链接，然后把 my_hanlder.handle 函数交给框架来
作为入口函数，这样合理吗？</p><p class="my-4 font-light">Google 的 gRPC 在这方面显然是一个反面的例子，本身他不是一个 Python 的库，内部使用了各种
奇奇怪怪的技术，在 Python 中使用的时候就遇到了各种问题，以至于没有一个很好的多进程模式。</p><p class="my-4 font-light">在 web server 中，uwsgi/gunicorn 这些 process runner 又是怎样处理的呢？</p><ul class="my-4 ml-4 font-light list-disc"><li class="">uwsgi 默认会<strong>先 load 再 fork</strong>, 这样就会有一些问题，比如全局的数据库链接等等，但是也
可以改成 fork 之后再加载。</li><li class="">gunicorn 和 uwsgi 恰恰相反，默认会<strong>先 fork 再 load</strong>.</li></ul><p class="my-4 font-light">不管怎样，这两个都使用了非常简洁的 prefork 模型，而且在文档中明确说明了会采用那种模式，
非常优秀。</p><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://stackoverflow.com/questions/39890363/what-happens-when-a-thread-forks/39890957" title="undefined">https://stackoverflow.com/questions/39890363/what-happens-when-a-thread-forks/39890957</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/46439740/safe-to-call-multiprocessing-from-a-thread-in-python" title="undefined">https://stackoverflow.com/questions/46439740/safe-to-call-multiprocessing-from-a-thread-in-python</a></li><li class=""><a class="underline" href="https://britishgeologicalsurvey.github.io/science/python-forking-vs-spawn/" title="null">还没看完</a></li><li class=""><a class="underline" href="https://pythonspeed.com/articles/python-multiprocessing/" title="null">还没看完</a></li><li class=""><a class="underline" href="https://github.com/grpc/grpc/issues/16001" title="undefined">https://github.com/grpc/grpc/issues/16001</a></li><li class=""><a class="underline" href="https://eleme.github.io/blog/2016/eleme-python-soa/" title="undefined">https://eleme.github.io/blog/2016/eleme-python-soa/</a></li><li class=""><a class="underline" href="https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html" title="undefined">https://uwsgi-docs.readthedocs.io/en/latest/ThingsToKnow.html</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/42439553/how-to-share-in-memory-resources-between-flask-methods-when-deploying-with-gunic" title="undefined">https://stackoverflow.com/questions/42439553/how-to-share-in-memory-resources-between-flask-methods-when-deploying-with-gunic</a></li><li class=""><a class="underline" href="https://instagram-engineering.com/copy-on-write-friendly-python-garbage-collection-ad6ed5233ddf" title="undefined">https://instagram-engineering.com/copy-on-write-friendly-python-garbage-collection-ad6ed5233ddf</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 SQLite 作为文档数据库]]></title>
            <link>https://yifei.me/note/2473</link>
            <guid>https://yifei.me/note/2473</guid>
            <pubDate>Sat, 19 Aug 2023 12:12:30 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">本文需要 SQLite &gt;= 3.31.0(2020-01-22)</p><p class="my-4 font-light">在 SQLite 的新版本中已经支持了 json 函数 (<code class="px-1 bg-gray-100 border-2 rounded">json_extract</code>)，我们完全可以把它当做一个文档
数据库使用。而且更妙的是，还可以直接添加&quot;合成列&quot;(generated column), 也就是使用 json 中的
字段作为虚拟的列。最牛逼的是，这些&quot;生成列&quot;上还可以加索引，包括 unique 索引。</p><p class="my-4 font-light">之前用 MongoDB 的时候，最爽的地方就是可以随便往里面插入东西，不用纠结先创建表；最不爽的
就是加索引的语法太难记了，而且好像得先把表创建出来。另一方面，SQL 的 select 语法已经像
思想钢印一样打进了每个程序员的大脑，使用 MongoDB 的查询语法多少有些不适应。而从刚刚提到
的 sqlite 的新功能来看，sqlite 甚至比 MongoDB 用起来还能更爽一些。可以按照以下几步操作：</p><p class="my-4 font-light">我们可以预先不定义表的具体结构，只留一个 body 字段：<code class="px-1 bg-gray-100 border-2 rounded">create table my_data(body text);</code>, 
有什么数据，先直接往里面插入就好了。</p><p class="my-4 font-light">需要插入数据的时候，可以直接插入 json:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>sqlite&gt; insert into my_data (<span class="hljs-name">body</span>) values (&#x27;{<span class="hljs-string">&quot;id&quot;</span>: <span class="hljs-number">1</span>, <span class="hljs-string">&quot;text&quot;</span>: <span class="hljs-string">&quot;foo&quot;</span>}&#x27;)<span class="hljs-comment">;</span>
sqlite&gt; insert into my_data (<span class="hljs-name">body</span>) values (&#x27;{<span class="hljs-string">&quot;id&quot;</span>: <span class="hljs-number">2</span>, <span class="hljs-string">&quot;text&quot;</span>: <span class="hljs-string">&quot;bar&quot;</span>}&#x27;)<span class="hljs-comment">;</span></code></pre><p class="my-4 font-light">需要一些 ad hoc 的查询的时候，可以使用 <code class="px-1 bg-gray-100 border-2 rounded">json_extract</code> 函数直接查询：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-function"><span class="hljs-keyword">select</span> <span class="hljs-title">json_extract</span>(<span class="hljs-params">body, <span class="hljs-string">&#x27;$.id&#x27;</span></span>) <span class="hljs-keyword">from</span> my_data</span>;</code></pre><p class="my-4 font-light">或者可以创建一个&quot;合成列&quot;(generate column), 这样就可以查询了：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">alter</span> <span class="hljs-keyword">table</span> my_data <span class="hljs-keyword">add</span> <span class="hljs-keyword">column</span> id <span class="hljs-type">int</span> <span class="hljs-keyword">generated</span> <span class="hljs-keyword">always</span> <span class="hljs-keyword">as</span> (json_extract(body, <span class="hljs-string">&#x27;$.id&#x27;</span>)) virtual;
<span class="hljs-keyword">select</span> * <span class="hljs-keyword">from</span> data <span class="hljs-keyword">where</span> id = <span class="hljs-number">2</span>;</code></pre><p class="my-4 font-light">当然也可以在创建表的时候就加上需要的列</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> my_data(
    body <span class="hljs-type">text</span>,
    id <span class="hljs-type">int</span> <span class="hljs-keyword">generated</span> <span class="hljs-keyword">always</span> <span class="hljs-keyword">as</span> (json_extract(body, <span class="hljs-string">&#x27;$.id&#x27;</span>)) virtual
);</code></pre><p class="my-4 font-light">为了个给查询加速，我们可以创建索引，<strong>合成列支持索引</strong></p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">create</span> <span class="hljs-keyword">index</span> idx_id <span class="hljs-keyword">on</span> my_data(id);
<span class="hljs-keyword">explain</span> query plan <span class="hljs-keyword">select</span> id <span class="hljs-keyword">from</span> my_data <span class="hljs-keyword">where</span> id=<span class="hljs-number">1</span>;
QUERY PLAN
--SEARCH TABLE my_data USING <span class="hljs-keyword">INDEX</span> idx_id (id=?)</code></pre><p class="my-4 font-light">我们可以看到，查询时确实使用了索引。</p><p class="my-4 font-light">如果我们在数据插入过程中发现有一些重复数据，那很简单，直接把索引改成 unique 所有就好了，
就像普通的 sql 数据库一样。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">create</span> <span class="hljs-keyword">unique</span> <span class="hljs-keyword">index</span> uniq_id <span class="hljs-keyword">on</span> my_data(id);</code></pre><p class="my-4 font-light">如何验证插入的 json 是否合法呢？其实刚刚我们已经加上了，<code class="px-1 bg-gray-100 border-2 rounded">generated always</code> 表示的是总是
生成这个字段，既然能抽取这个字段了，那么这个 json 显然是合法的。当插入一个非法的 json 的
时候就会报错。</p><p class="my-4 font-light">如果要保证某个字段一定存在，那么还是用 SQL 的语法，加上一个 <code class="px-1 bg-gray-100 border-2 rounded">not null</code> 就可以了。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>sqlite&gt; insert into x values(&#x27;&#x27;);
<span class="hljs-keyword">Error: </span>malformed JSON
sqlite&gt; insert into x values(&#x27;{}&#x27;);
<span class="hljs-keyword">Error: </span>NOT NULL constraint failed: my_data.id</code></pre><p class="my-4 font-light">现在，我们来看一下 <code class="px-1 bg-gray-100 border-2 rounded">virtual</code> 关键字。上面的 <code class="px-1 bg-gray-100 border-2 rounded">virtual</code> 表示这个字段总是查询的时候抽取，对
应的另一种模式叫做 <code class="px-1 bg-gray-100 border-2 rounded">stored</code>, 也就是缓存下这个字段。使用 <code class="px-1 bg-gray-100 border-2 rounded">stored</code> 的好处是每次避免的了
json 解析的消耗，但是缺点是只能在 create table 的时候添加 stored 字段，不能使用 alter
table 添加。实际上 <code class="px-1 bg-gray-100 border-2 rounded">virtual</code> 是默认模式，也就是我们可以省略 <code class="px-1 bg-gray-100 border-2 rounded">virtual</code>.</p><p class="my-4 font-light">以上就是主要内容了，再提一些小技巧：</p><p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">json</code> 函数验证字符串是否是合法的 json, 并且返回一个压缩过后（也就是去除了无用的空格换行）
的 json. 所以可以在插入的时候使用 json 函数过滤一下文档。</p><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://dgl.cx/2020/06/sqlite-json-support" title="undefined">https://dgl.cx/2020/06/sqlite-json-support</a></li><li class=""><a class="underline" href="https://www.sqlite.org/gencol.html" title="undefined">https://www.sqlite.org/gencol.html</a></li><li class=""><a class="underline" href="https://www.sqlite.org/json1.html" title="undefined">https://www.sqlite.org/json1.html</a></li></ol>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">本文需要 SQLite &gt;= 3.31.0(2020-01-22)</p><p class="my-4 font-light">在 SQLite 的新版本中已经支持了 json 函数 (<code class="px-1 bg-gray-100 border-2 rounded">json_extract</code>)，我们完全可以把它当做一个文档
数据库使用。而且更妙的是，还可以直接添加&quot;合成列&quot;(generated column), 也就是使用 json 中的
字段作为虚拟的列。最牛逼的是，这些&quot;生成列&quot;上还可以加索引，包括 unique 索引。</p><p class="my-4 font-light">之前用 MongoDB 的时候，最爽的地方就是可以随便往里面插入东西，不用纠结先创建表；最不爽的
就是加索引的语法太难记了，而且好像得先把表创建出来。另一方面，SQL 的 select 语法已经像
思想钢印一样打进了每个程序员的大脑，使用 MongoDB 的查询语法多少有些不适应。而从刚刚提到
的 sqlite 的新功能来看，sqlite 甚至比 MongoDB 用起来还能更爽一些。可以按照以下几步操作：</p><p class="my-4 font-light">我们可以预先不定义表的具体结构，只留一个 body 字段：<code class="px-1 bg-gray-100 border-2 rounded">create table my_data(body text);</code>, 
有什么数据，先直接往里面插入就好了。</p><p class="my-4 font-light">需要插入数据的时候，可以直接插入 json:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>sqlite&gt; insert into my_data (<span class="hljs-name">body</span>) values (&#x27;{<span class="hljs-string">&quot;id&quot;</span>: <span class="hljs-number">1</span>, <span class="hljs-string">&quot;text&quot;</span>: <span class="hljs-string">&quot;foo&quot;</span>}&#x27;)<span class="hljs-comment">;</span>
sqlite&gt; insert into my_data (<span class="hljs-name">body</span>) values (&#x27;{<span class="hljs-string">&quot;id&quot;</span>: <span class="hljs-number">2</span>, <span class="hljs-string">&quot;text&quot;</span>: <span class="hljs-string">&quot;bar&quot;</span>}&#x27;)<span class="hljs-comment">;</span></code></pre><p class="my-4 font-light">需要一些 ad hoc 的查询的时候，可以使用 <code class="px-1 bg-gray-100 border-2 rounded">json_extract</code> 函数直接查询：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-function"><span class="hljs-keyword">select</span> <span class="hljs-title">json_extract</span>(<span class="hljs-params">body, <span class="hljs-string">&#x27;$.id&#x27;</span></span>) <span class="hljs-keyword">from</span> my_data</span>;</code></pre><p class="my-4 font-light">或者可以创建一个&quot;合成列&quot;(generate column), 这样就可以查询了：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">alter</span> <span class="hljs-keyword">table</span> my_data <span class="hljs-keyword">add</span> <span class="hljs-keyword">column</span> id <span class="hljs-type">int</span> <span class="hljs-keyword">generated</span> <span class="hljs-keyword">always</span> <span class="hljs-keyword">as</span> (json_extract(body, <span class="hljs-string">&#x27;$.id&#x27;</span>)) virtual;
<span class="hljs-keyword">select</span> * <span class="hljs-keyword">from</span> data <span class="hljs-keyword">where</span> id = <span class="hljs-number">2</span>;</code></pre><p class="my-4 font-light">当然也可以在创建表的时候就加上需要的列</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> my_data(
    body <span class="hljs-type">text</span>,
    id <span class="hljs-type">int</span> <span class="hljs-keyword">generated</span> <span class="hljs-keyword">always</span> <span class="hljs-keyword">as</span> (json_extract(body, <span class="hljs-string">&#x27;$.id&#x27;</span>)) virtual
);</code></pre><p class="my-4 font-light">为了个给查询加速，我们可以创建索引，<strong>合成列支持索引</strong></p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">create</span> <span class="hljs-keyword">index</span> idx_id <span class="hljs-keyword">on</span> my_data(id);
<span class="hljs-keyword">explain</span> query plan <span class="hljs-keyword">select</span> id <span class="hljs-keyword">from</span> my_data <span class="hljs-keyword">where</span> id=<span class="hljs-number">1</span>;
QUERY PLAN
--SEARCH TABLE my_data USING <span class="hljs-keyword">INDEX</span> idx_id (id=?)</code></pre><p class="my-4 font-light">我们可以看到，查询时确实使用了索引。</p><p class="my-4 font-light">如果我们在数据插入过程中发现有一些重复数据，那很简单，直接把索引改成 unique 所有就好了，
就像普通的 sql 数据库一样。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">create</span> <span class="hljs-keyword">unique</span> <span class="hljs-keyword">index</span> uniq_id <span class="hljs-keyword">on</span> my_data(id);</code></pre><p class="my-4 font-light">如何验证插入的 json 是否合法呢？其实刚刚我们已经加上了，<code class="px-1 bg-gray-100 border-2 rounded">generated always</code> 表示的是总是
生成这个字段，既然能抽取这个字段了，那么这个 json 显然是合法的。当插入一个非法的 json 的
时候就会报错。</p><p class="my-4 font-light">如果要保证某个字段一定存在，那么还是用 SQL 的语法，加上一个 <code class="px-1 bg-gray-100 border-2 rounded">not null</code> 就可以了。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>sqlite&gt; insert into x values(&#x27;&#x27;);
<span class="hljs-keyword">Error: </span>malformed JSON
sqlite&gt; insert into x values(&#x27;{}&#x27;);
<span class="hljs-keyword">Error: </span>NOT NULL constraint failed: my_data.id</code></pre><p class="my-4 font-light">现在，我们来看一下 <code class="px-1 bg-gray-100 border-2 rounded">virtual</code> 关键字。上面的 <code class="px-1 bg-gray-100 border-2 rounded">virtual</code> 表示这个字段总是查询的时候抽取，对
应的另一种模式叫做 <code class="px-1 bg-gray-100 border-2 rounded">stored</code>, 也就是缓存下这个字段。使用 <code class="px-1 bg-gray-100 border-2 rounded">stored</code> 的好处是每次避免的了
json 解析的消耗，但是缺点是只能在 create table 的时候添加 stored 字段，不能使用 alter
table 添加。实际上 <code class="px-1 bg-gray-100 border-2 rounded">virtual</code> 是默认模式，也就是我们可以省略 <code class="px-1 bg-gray-100 border-2 rounded">virtual</code>.</p><p class="my-4 font-light">以上就是主要内容了，再提一些小技巧：</p><p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">json</code> 函数验证字符串是否是合法的 json, 并且返回一个压缩过后（也就是去除了无用的空格换行）
的 json. 所以可以在插入的时候使用 json 函数过滤一下文档。</p><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://dgl.cx/2020/06/sqlite-json-support" title="undefined">https://dgl.cx/2020/06/sqlite-json-support</a></li><li class=""><a class="underline" href="https://www.sqlite.org/gencol.html" title="undefined">https://www.sqlite.org/gencol.html</a></li><li class=""><a class="underline" href="https://www.sqlite.org/json1.html" title="undefined">https://www.sqlite.org/json1.html</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[`just`，一个现代版的 `make`]]></title>
            <link>https://yifei.me/note/2816</link>
            <guid>https://yifei.me/note/2816</guid>
            <pubDate>Sat, 19 Aug 2023 12:12:30 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">我们知道 make 包含了两个主要功能：</p><ul class="my-4 ml-4 font-light list-disc"><li class="">构建 C 工程</li><li class="">运行具有依赖关系的任务</li></ul><p class="my-4 font-light">而由于 make 的时代太过久远，导致现在用起来会有几个坑爹之处：</p><ul class="my-4 ml-4 font-light list-disc"><li class="">声明变量语法奇怪</li><li class="">每次都加 .PHONY 很麻烦</li><li class="">配置 SHELL 也很反直觉</li><li class="">多行命令不在同一个 shell 中运行</li><li class="">必须用 <code class="px-1 bg-gray-100 border-2 rounded">\t</code> 缩进</li></ul><p class="my-4 font-light">另一方面，我也只需要运行任务的功能，并不需要编译 c 代码这部分功能。所以就想找一个现代版的
任务执行工具。</p><h2 class="my-2 font-semibold text-xl">Just</h2><p class="my-4 font-light">Justfile 的语法和 Makefile 基本类似，下面是主要功能的介绍</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 加载 .env 文件</span>
<span class="hljs-built_in">set</span> dotenv-load  <span class="hljs-comment">#</span>
<span class="hljs-comment"># 所有变量导出为环境变量，在下面的任务中使用</span>
<span class="hljs-built_in">set</span> <span class="hljs-built_in">export</span>
<span class="hljs-comment"># 指定 shell</span>
<span class="hljs-built_in">set</span> shell := [<span class="hljs-string">&quot;fish&quot;</span>, <span class="hljs-string">&quot;-c&quot;</span>]

<span class="hljs-comment"># 变量</span>
tmpdir := `<span class="hljs-built_in">mktemp</span>`  <span class="hljs-comment"># 读取 shell 命令的结果</span>
version := <span class="hljs-string">&quot;0.1&quot;</span>    <span class="hljs-comment"># 字符串变量</span>
<span class="hljs-comment"># 使用 / 可以直接组合路径，相当于 a + &quot;/&quot; + &quot;b&quot;</span>
<span class="hljs-comment"># 使用 + 可以连接字符串</span>
tarball := tmpdir / <span class="hljs-string">&quot;awesome&quot;</span> + version + <span class="hljs-string">&quot;.tar.gz&quot;</span>
<span class="hljs-comment"># 直接使用 export 也可以导出环境变量</span>
<span class="hljs-built_in">export</span> http_proxy := localhost:8080

<span class="hljs-comment"># 函数</span>
<span class="hljs-comment"># 使用 env_var 函数读取外部环境变量</span>
home_dir := env_var(<span class="hljs-string">&#x27;HOME&#x27;</span>)
<span class="hljs-comment"># justfile_directory 读取 justfile 所在路径</span>
<span class="hljs-comment"># 其他的函数还有 lower_case/trim/uppercase/extension/uuid 等</span>

<span class="hljs-comment"># 多行字符串</span>
long_string := <span class="hljs-string">&#x27;very
long
string
&#x27;</span>

<span class="hljs-comment"># alias</span>
<span class="hljs-built_in">alias</span> b:= build

<span class="hljs-comment"># 依赖按照顺序执行</span>
all: lint <span class="hljs-built_in">test</span> build

<span class="hljs-comment"># 命令按照顺序执行，如果一个失败，下一个不会执行</span>
publish:
    <span class="hljs-comment"># 使用双大括号表示变量</span>
    <span class="hljs-built_in">rm</span> -rf {{tarball}}
    tar cvzf {{tarball}}
    <span class="hljs-comment"># 用前缀可以忽略该行命令的错误</span>
    -curl -X POST {{tarball}} {{endpoint}}

<span class="hljs-comment"># 每一行都是一个新的 shell，所以直接 cd 是没用的</span>
foo:
    <span class="hljs-built_in">pwd</span>    <span class="hljs-comment"># This `pwd` will print the same directory…</span>
    <span class="hljs-built_in">cd</span> bar
    <span class="hljs-built_in">pwd</span>    <span class="hljs-comment"># …as this `pwd`!</span>

<span class="hljs-comment"># 不过添加 shebang 之后，就会当作一整个脚本执行</span>
foo:
    <span class="hljs-comment">#!/usr/bin/env bash</span>
    <span class="hljs-built_in">set</span> -euxo pipefail
    <span class="hljs-built_in">cd</span> bar
    <span class="hljs-built_in">pwd</span>

<span class="hljs-comment"># 还可以使用 shell 之外的语言！</span>
python:
    <span class="hljs-comment">#!/usr/bin/env python3</span>
    <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;hello world&quot;</span>)

echo_home:
    <span class="hljs-comment"># 可以直接读取外部环境变量</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Home is <span class="hljs-variable">${HOME}</span>&quot;</span>

build:
    cargo build

lint:
    cargo lint

<span class="hljs-comment"># 使用 @ 做前缀可以不打印命令本身</span>
clean:
    @<span class="hljs-built_in">rm</span> -rf build/</code></pre><p class="my-4 font-light">使用 just -l 可以按字母顺序列出所有任务，使用 just -u/--unsorted 可以按定义顺序列出，更秒
的是，任务上方的注释也会一并列出。</p><p class="my-4 font-light">变量可以在调用的时候重载：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>just <span class="hljs-attribute">name</span>=value
just --<span class="hljs-built_in">set</span> name value</code></pre><p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">just</code> 命令会向上查找到包含 justfile 根目录，就像 git 一样，所以可以在子目录中运行 just.</p><h2 class="my-2 font-semibold text-xl">Just 的缺点</h2><p class="my-4 font-light">just 不支持按照文件的修改日期来构建项目，也就是说很多时候都要从头构建，这样可用度就大幅
降低了。如果这点也支持的话，真的可以迁移到 just 了。</p><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://github.com/go-task/task" title="undefined">https://github.com/go-task/task</a></li><li class=""><a class="underline" href="https://github.com/casey/just" title="undefined">https://github.com/casey/just</a></li></ol>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">我们知道 make 包含了两个主要功能：</p><ul class="my-4 ml-4 font-light list-disc"><li class="">构建 C 工程</li><li class="">运行具有依赖关系的任务</li></ul><p class="my-4 font-light">而由于 make 的时代太过久远，导致现在用起来会有几个坑爹之处：</p><ul class="my-4 ml-4 font-light list-disc"><li class="">声明变量语法奇怪</li><li class="">每次都加 .PHONY 很麻烦</li><li class="">配置 SHELL 也很反直觉</li><li class="">多行命令不在同一个 shell 中运行</li><li class="">必须用 <code class="px-1 bg-gray-100 border-2 rounded">\t</code> 缩进</li></ul><p class="my-4 font-light">另一方面，我也只需要运行任务的功能，并不需要编译 c 代码这部分功能。所以就想找一个现代版的
任务执行工具。</p><h2 class="my-2 font-semibold text-xl">Just</h2><p class="my-4 font-light">Justfile 的语法和 Makefile 基本类似，下面是主要功能的介绍</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 加载 .env 文件</span>
<span class="hljs-built_in">set</span> dotenv-load  <span class="hljs-comment">#</span>
<span class="hljs-comment"># 所有变量导出为环境变量，在下面的任务中使用</span>
<span class="hljs-built_in">set</span> <span class="hljs-built_in">export</span>
<span class="hljs-comment"># 指定 shell</span>
<span class="hljs-built_in">set</span> shell := [<span class="hljs-string">&quot;fish&quot;</span>, <span class="hljs-string">&quot;-c&quot;</span>]

<span class="hljs-comment"># 变量</span>
tmpdir := `<span class="hljs-built_in">mktemp</span>`  <span class="hljs-comment"># 读取 shell 命令的结果</span>
version := <span class="hljs-string">&quot;0.1&quot;</span>    <span class="hljs-comment"># 字符串变量</span>
<span class="hljs-comment"># 使用 / 可以直接组合路径，相当于 a + &quot;/&quot; + &quot;b&quot;</span>
<span class="hljs-comment"># 使用 + 可以连接字符串</span>
tarball := tmpdir / <span class="hljs-string">&quot;awesome&quot;</span> + version + <span class="hljs-string">&quot;.tar.gz&quot;</span>
<span class="hljs-comment"># 直接使用 export 也可以导出环境变量</span>
<span class="hljs-built_in">export</span> http_proxy := localhost:8080

<span class="hljs-comment"># 函数</span>
<span class="hljs-comment"># 使用 env_var 函数读取外部环境变量</span>
home_dir := env_var(<span class="hljs-string">&#x27;HOME&#x27;</span>)
<span class="hljs-comment"># justfile_directory 读取 justfile 所在路径</span>
<span class="hljs-comment"># 其他的函数还有 lower_case/trim/uppercase/extension/uuid 等</span>

<span class="hljs-comment"># 多行字符串</span>
long_string := <span class="hljs-string">&#x27;very
long
string
&#x27;</span>

<span class="hljs-comment"># alias</span>
<span class="hljs-built_in">alias</span> b:= build

<span class="hljs-comment"># 依赖按照顺序执行</span>
all: lint <span class="hljs-built_in">test</span> build

<span class="hljs-comment"># 命令按照顺序执行，如果一个失败，下一个不会执行</span>
publish:
    <span class="hljs-comment"># 使用双大括号表示变量</span>
    <span class="hljs-built_in">rm</span> -rf {{tarball}}
    tar cvzf {{tarball}}
    <span class="hljs-comment"># 用前缀可以忽略该行命令的错误</span>
    -curl -X POST {{tarball}} {{endpoint}}

<span class="hljs-comment"># 每一行都是一个新的 shell，所以直接 cd 是没用的</span>
foo:
    <span class="hljs-built_in">pwd</span>    <span class="hljs-comment"># This `pwd` will print the same directory…</span>
    <span class="hljs-built_in">cd</span> bar
    <span class="hljs-built_in">pwd</span>    <span class="hljs-comment"># …as this `pwd`!</span>

<span class="hljs-comment"># 不过添加 shebang 之后，就会当作一整个脚本执行</span>
foo:
    <span class="hljs-comment">#!/usr/bin/env bash</span>
    <span class="hljs-built_in">set</span> -euxo pipefail
    <span class="hljs-built_in">cd</span> bar
    <span class="hljs-built_in">pwd</span>

<span class="hljs-comment"># 还可以使用 shell 之外的语言！</span>
python:
    <span class="hljs-comment">#!/usr/bin/env python3</span>
    <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;hello world&quot;</span>)

echo_home:
    <span class="hljs-comment"># 可以直接读取外部环境变量</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Home is <span class="hljs-variable">${HOME}</span>&quot;</span>

build:
    cargo build

lint:
    cargo lint

<span class="hljs-comment"># 使用 @ 做前缀可以不打印命令本身</span>
clean:
    @<span class="hljs-built_in">rm</span> -rf build/</code></pre><p class="my-4 font-light">使用 just -l 可以按字母顺序列出所有任务，使用 just -u/--unsorted 可以按定义顺序列出，更秒
的是，任务上方的注释也会一并列出。</p><p class="my-4 font-light">变量可以在调用的时候重载：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>just <span class="hljs-attribute">name</span>=value
just --<span class="hljs-built_in">set</span> name value</code></pre><p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">just</code> 命令会向上查找到包含 justfile 根目录，就像 git 一样，所以可以在子目录中运行 just.</p><h2 class="my-2 font-semibold text-xl">Just 的缺点</h2><p class="my-4 font-light">just 不支持按照文件的修改日期来构建项目，也就是说很多时候都要从头构建，这样可用度就大幅
降低了。如果这点也支持的话，真的可以迁移到 just 了。</p><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://github.com/go-task/task" title="undefined">https://github.com/go-task/task</a></li><li class=""><a class="underline" href="https://github.com/casey/just" title="undefined">https://github.com/casey/just</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[GitHub Actions 的简单使用和调试]]></title>
            <link>https://yifei.me/note/2892</link>
            <guid>https://yifei.me/note/2892</guid>
            <pubDate>Sat, 19 Aug 2023 12:12:30 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">GitHub Actions 使用 yaml 配置，必须放置在 <code class="px-1 bg-gray-100 border-2 rounded">.github/workflows</code> 目录。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 名字</span>
<span class="hljs-attr">name:</span> <span class="hljs-string">hello-world-example</span>
<span class="hljs-comment"># 环境变量</span>
<span class="hljs-attr">env:</span>
  <span class="hljs-attr">FOO:</span> <span class="hljs-string">bar</span>
<span class="hljs-comment"># 触发条件</span>
<span class="hljs-attr">on:</span>
  <span class="hljs-comment"># 最常用的是 push 条件，如果留空，表示每次 push 都执行。</span>
  <span class="hljs-comment"># 任意满足这里列出的一个条件即会匹配。也就是 master 或者 v tag 都会执行。</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-comment"># 表示在这些分支推送的时候执行</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">master</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">feature/*</span>
    <span class="hljs-comment"># 在这些 tag 推送的时候执行</span>
    <span class="hljs-attr">tags:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">v*</span>
  <span class="hljs-comment"># 在有人提交 PR 时运行</span>
  <span class="hljs-attr">pull_request:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">master</span>
  <span class="hljs-comment"># 定时运行</span>
  <span class="hljs-attr">schedule:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">cron:</span> <span class="hljs-string">&#x27;*/15 * * * *&#x27;</span>
<span class="hljs-comment"># 要运行的任务</span>
<span class="hljs-attr">jobs:</span>
  <span class="hljs-comment"># 任务名字</span>
  <span class="hljs-attr">say-hello:</span>
    <span class="hljs-comment"># 也可以在这里设置环境变量</span>
    <span class="hljs-attr">env:</span>
      <span class="hljs-attr">FOO:</span> <span class="hljs-string">bar</span>
    <span class="hljs-comment"># 运行环境</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-comment"># 可以使用一些现成的操作，比如 checkout 当前仓库</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>
      <span class="hljs-comment"># 安装 node</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-node@v1</span>
        <span class="hljs-comment"># actions 的参数</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">node-version:</span> <span class="hljs-string">&#x27;15.8.0&#x27;</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Say</span> <span class="hljs-string">Hello</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">FOO:</span> <span class="hljs-string">bar</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">&quot;Hello $FOO&quot;</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Do</span> <span class="hljs-string">stuff</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          echo &quot;Step 1...&quot;
          echo &quot;Step 2...&quot;
          echo &quot;Step 3...&quot;
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Say</span> <span class="hljs-string">Goodbye</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">&quot;Goodbye!&quot;</span>
  <span class="hljs-comment"># 任务之间默认是并发执行的</span>
  <span class="hljs-attr">another-job:</span>
    <span class="hljs-comment"># 依赖另一个 job</span>
    <span class="hljs-attr">needs:</span> <span class="hljs-string">say-hello</span>
    <span class="hljs-attr">strategy:</span>
      <span class="hljs-comment"># CI 的常见应用是在不同的环境下测试，使用 matrix 来定义一组环境</span>
      <span class="hljs-attr">matrix:</span>
        <span class="hljs-attr">os:</span> [<span class="hljs-string">ubuntu-16.04</span>, <span class="hljs-string">ubuntu-18.04</span>]
        <span class="hljs-attr">node:</span> [<span class="hljs-number">16</span>, <span class="hljs-number">18</span>, <span class="hljs-number">20</span>]
        <span class="hljs-comment"># 排除掉某个环境</span>
        <span class="hljs-attr">exclude:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">os:</span> <span class="hljs-string">ubuntu-16.04</span>
            <span class="hljs-attr">node:</span> <span class="hljs-number">16</span>
        <span class="hljs-comment"># 额外包含某个环境</span>
        <span class="hljs-attr">include:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">os:</span> <span class="hljs-string">macos-latest</span>
            <span class="hljs-attr">node:</span> <span class="hljs-number">16</span>
    <span class="hljs-comment"># 使用变量</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">${{matrix.os}}</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-comment"># 读取密钥</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">use</span> <span class="hljs-string">secrets</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">${{secrets.ACCESS_KEY}}</span>
      <span class="hljs-comment"># 条件执行</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">only</span> <span class="hljs-string">for</span> <span class="hljs-string">pulls</span>
        <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">github.event</span> <span class="hljs-string">==</span> <span class="hljs-string">&#x27;pull&#x27;</span><span class="hljs-string">}}</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">pulling</span></code></pre><h2 class="my-2 font-semibold text-xl">常见问题</h2><h3 class="my-2 font-semibold ">如何调试？</h3><p class="my-4 font-light">可以使用 tmate ssh 进机器，查看情况</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">name</span><span class="hljs-punctuation">:</span> <span class="hljs-string">CI</span>
<span class="hljs-attribute">on</span><span class="hljs-punctuation">:</span> <span class="hljs-string">[push]</span>
<span class="hljs-attribute">jobs</span><span class="hljs-punctuation">:</span>
  <span class="hljs-attribute">build</span><span class="hljs-punctuation">:</span>
    <span class="hljs-attribute">runs-on</span><span class="hljs-punctuation">:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attribute">steps</span><span class="hljs-punctuation">:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">uses: actions/checkout@v3</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">name: Setup tmate session</span>
      <span class="hljs-attribute">uses</span><span class="hljs-punctuation">:</span> <span class="hljs-string">mxschmitt/action-tmate@v3</span></code></pre><h3 class="my-2 font-semibold ">Windows 上遇到 OOM 问题</h3><p class="my-4 font-light">调大虚拟内存（PageFile）就行了</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-bullet">-</span> <span class="hljs-string">name: configure Pagefile</span>
  <span class="hljs-attribute">uses</span><span class="hljs-punctuation">:</span> <span class="hljs-string">al-cheb/configure-pagefile-action@v1.2</span>
  <span class="hljs-attribute">with</span><span class="hljs-punctuation">:</span>
      <span class="hljs-attribute">minimum-size</span><span class="hljs-punctuation">:</span> <span class="hljs-string">16GB</span>
      <span class="hljs-attribute">maximum-size</span><span class="hljs-punctuation">:</span> <span class="hljs-string">16GB</span>
      <span class="hljs-attribute">disk-root</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;C:&quot;</span></code></pre><h2 class="my-2 font-semibold text-xl">References</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://www.actionsbyexample.com" title="undefined">https://www.actionsbyexample.com</a></li><li class=""><a class="underline" href="https://github.com/marketplace/actions/debugging-with-tmate" title="undefined">https://github.com/marketplace/actions/debugging-with-tmate</a></li><li class=""><a class="underline" href="https://github.com/actions/runner-images/issues/2642" title="undefined">https://github.com/actions/runner-images/issues/2642</a></li></ol>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">GitHub Actions 使用 yaml 配置，必须放置在 <code class="px-1 bg-gray-100 border-2 rounded">.github/workflows</code> 目录。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 名字</span>
<span class="hljs-attr">name:</span> <span class="hljs-string">hello-world-example</span>
<span class="hljs-comment"># 环境变量</span>
<span class="hljs-attr">env:</span>
  <span class="hljs-attr">FOO:</span> <span class="hljs-string">bar</span>
<span class="hljs-comment"># 触发条件</span>
<span class="hljs-attr">on:</span>
  <span class="hljs-comment"># 最常用的是 push 条件，如果留空，表示每次 push 都执行。</span>
  <span class="hljs-comment"># 任意满足这里列出的一个条件即会匹配。也就是 master 或者 v tag 都会执行。</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-comment"># 表示在这些分支推送的时候执行</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">master</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">feature/*</span>
    <span class="hljs-comment"># 在这些 tag 推送的时候执行</span>
    <span class="hljs-attr">tags:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">v*</span>
  <span class="hljs-comment"># 在有人提交 PR 时运行</span>
  <span class="hljs-attr">pull_request:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">master</span>
  <span class="hljs-comment"># 定时运行</span>
  <span class="hljs-attr">schedule:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">cron:</span> <span class="hljs-string">&#x27;*/15 * * * *&#x27;</span>
<span class="hljs-comment"># 要运行的任务</span>
<span class="hljs-attr">jobs:</span>
  <span class="hljs-comment"># 任务名字</span>
  <span class="hljs-attr">say-hello:</span>
    <span class="hljs-comment"># 也可以在这里设置环境变量</span>
    <span class="hljs-attr">env:</span>
      <span class="hljs-attr">FOO:</span> <span class="hljs-string">bar</span>
    <span class="hljs-comment"># 运行环境</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-comment"># 可以使用一些现成的操作，比如 checkout 当前仓库</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>
      <span class="hljs-comment"># 安装 node</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-node@v1</span>
        <span class="hljs-comment"># actions 的参数</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">node-version:</span> <span class="hljs-string">&#x27;15.8.0&#x27;</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Say</span> <span class="hljs-string">Hello</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">FOO:</span> <span class="hljs-string">bar</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">&quot;Hello $FOO&quot;</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Do</span> <span class="hljs-string">stuff</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          echo &quot;Step 1...&quot;
          echo &quot;Step 2...&quot;
          echo &quot;Step 3...&quot;
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Say</span> <span class="hljs-string">Goodbye</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">&quot;Goodbye!&quot;</span>
  <span class="hljs-comment"># 任务之间默认是并发执行的</span>
  <span class="hljs-attr">another-job:</span>
    <span class="hljs-comment"># 依赖另一个 job</span>
    <span class="hljs-attr">needs:</span> <span class="hljs-string">say-hello</span>
    <span class="hljs-attr">strategy:</span>
      <span class="hljs-comment"># CI 的常见应用是在不同的环境下测试，使用 matrix 来定义一组环境</span>
      <span class="hljs-attr">matrix:</span>
        <span class="hljs-attr">os:</span> [<span class="hljs-string">ubuntu-16.04</span>, <span class="hljs-string">ubuntu-18.04</span>]
        <span class="hljs-attr">node:</span> [<span class="hljs-number">16</span>, <span class="hljs-number">18</span>, <span class="hljs-number">20</span>]
        <span class="hljs-comment"># 排除掉某个环境</span>
        <span class="hljs-attr">exclude:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">os:</span> <span class="hljs-string">ubuntu-16.04</span>
            <span class="hljs-attr">node:</span> <span class="hljs-number">16</span>
        <span class="hljs-comment"># 额外包含某个环境</span>
        <span class="hljs-attr">include:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">os:</span> <span class="hljs-string">macos-latest</span>
            <span class="hljs-attr">node:</span> <span class="hljs-number">16</span>
    <span class="hljs-comment"># 使用变量</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">${{matrix.os}}</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-comment"># 读取密钥</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">use</span> <span class="hljs-string">secrets</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">${{secrets.ACCESS_KEY}}</span>
      <span class="hljs-comment"># 条件执行</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">only</span> <span class="hljs-string">for</span> <span class="hljs-string">pulls</span>
        <span class="hljs-attr">if:</span> <span class="hljs-string">${{</span> <span class="hljs-string">github.event</span> <span class="hljs-string">==</span> <span class="hljs-string">&#x27;pull&#x27;</span><span class="hljs-string">}}</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">pulling</span></code></pre><h2 class="my-2 font-semibold text-xl">常见问题</h2><h3 class="my-2 font-semibold ">如何调试？</h3><p class="my-4 font-light">可以使用 tmate ssh 进机器，查看情况</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">name</span><span class="hljs-punctuation">:</span> <span class="hljs-string">CI</span>
<span class="hljs-attribute">on</span><span class="hljs-punctuation">:</span> <span class="hljs-string">[push]</span>
<span class="hljs-attribute">jobs</span><span class="hljs-punctuation">:</span>
  <span class="hljs-attribute">build</span><span class="hljs-punctuation">:</span>
    <span class="hljs-attribute">runs-on</span><span class="hljs-punctuation">:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attribute">steps</span><span class="hljs-punctuation">:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">uses: actions/checkout@v3</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">name: Setup tmate session</span>
      <span class="hljs-attribute">uses</span><span class="hljs-punctuation">:</span> <span class="hljs-string">mxschmitt/action-tmate@v3</span></code></pre><h3 class="my-2 font-semibold ">Windows 上遇到 OOM 问题</h3><p class="my-4 font-light">调大虚拟内存（PageFile）就行了</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-bullet">-</span> <span class="hljs-string">name: configure Pagefile</span>
  <span class="hljs-attribute">uses</span><span class="hljs-punctuation">:</span> <span class="hljs-string">al-cheb/configure-pagefile-action@v1.2</span>
  <span class="hljs-attribute">with</span><span class="hljs-punctuation">:</span>
      <span class="hljs-attribute">minimum-size</span><span class="hljs-punctuation">:</span> <span class="hljs-string">16GB</span>
      <span class="hljs-attribute">maximum-size</span><span class="hljs-punctuation">:</span> <span class="hljs-string">16GB</span>
      <span class="hljs-attribute">disk-root</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;C:&quot;</span></code></pre><h2 class="my-2 font-semibold text-xl">References</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://www.actionsbyexample.com" title="undefined">https://www.actionsbyexample.com</a></li><li class=""><a class="underline" href="https://github.com/marketplace/actions/debugging-with-tmate" title="undefined">https://github.com/marketplace/actions/debugging-with-tmate</a></li><li class=""><a class="underline" href="https://github.com/actions/runner-images/issues/2642" title="undefined">https://github.com/actions/runner-images/issues/2642</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[FastAPI 使用路由模块化]]></title>
            <link>https://yifei.me/note/3016</link>
            <guid>https://yifei.me/note/3016</guid>
            <pubDate>Sat, 19 Aug 2023 12:12:30 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">FastAPI 中的 router 相当于 Flask 中的 Blueprint, 用来切分应用到不同的模块。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># views/users.py</span>
<span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> APIRouter

router = APIRouter()

<span class="hljs-meta">@router.get(<span class="hljs-params"><span class="hljs-string">&quot;/me&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">myinfo</span>():
    ...

<span class="hljs-comment"># main.py</span>
<span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI
<span class="hljs-keyword">from</span> views <span class="hljs-keyword">import</span> users

app = FastAPI()

app.include_router(users.router, prefix=<span class="hljs-string">&quot;/users&quot;</span>)</code></pre><h2 class="my-2 font-semibold text-xl">文件组织</h2><p class="my-4 font-light">模块化之后，自然需要考虑文件是怎么组织的。一般情况下，我会这么做：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>|<span class="hljs-string">-- app.py
</span>|<span class="hljs-string">-- views/
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- __init__.py
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- users.py
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- tasks.py
</span>|<span class="hljs-string">-- depends/
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- __init__.py
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- db.py
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- users.py
</span>|<span class="hljs-string">-- sql/
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- __init__.py
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- users.py
+-- schemas/
    </span>|<span class="hljs-string">-- __init__.py
    </span>|<span class="hljs-string">-- users.py</span></code></pre><p class="my-4 font-light">其中 app.py 存放 app实例，views 存放对应的 routers 文件，sql 存放 sqlalchemy 模型文件，
schemas 存放对应的 pydantic 类型文件，depends 存放一些依赖文件，后面的章节会讲到。</p><p class="my-4 font-light">一些文件的代码如下：</p><p class="my-4 font-light">app.py</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>from fastapi import FastAPI
from views import users, tasks,<span class="hljs-operator"> ...

</span>app = <span class="hljs-constructor">FastAPI()</span>
app.<span class="hljs-keyword">include</span><span class="hljs-constructor">_router(<span class="hljs-params">users</span>.<span class="hljs-params">router</span>, <span class="hljs-params">prefix</span>=<span class="hljs-string">&quot;/users&quot;</span>, <span class="hljs-params">tags</span>=[<span class="hljs-string">&quot;User&quot;</span>])</span>
app.<span class="hljs-keyword">include</span><span class="hljs-constructor">_router(<span class="hljs-params">tasks</span>.<span class="hljs-params">router</span>, <span class="hljs-params">prefix</span>=<span class="hljs-string">&quot;/tasks&quot;</span>, <span class="hljs-params">tags</span>=[<span class="hljs-string">&quot;Task&quot;</span>])</span></code></pre><p class="my-4 font-light">views/users.py</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> APIRouter
<span class="hljs-keyword">from</span> pydantic <span class="hljs-keyword">import</span> BaseModel
<span class="hljs-keyword">from</span> schemas <span class="hljs-keyword">import</span> UserModel

router = APIRouter()

<span class="hljs-keyword">class</span> <span class="hljs-title class_">UserListOut</span>(<span class="hljs-title class_ inherited__">BaseModel</span>):
    users: <span class="hljs-built_in">list</span>[UserModel]

<span class="hljs-meta">@router.get(<span class="hljs-params"><span class="hljs-string">&quot;&quot;</span>, response_model=UserListOut</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">list_users</span>():
    ...

<span class="hljs-meta">@router.get(<span class="hljs-params"><span class="hljs-string">&quot;/{user_id}&quot;</span>, response_model=UserModel</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_user</span>(<span class="hljs-params">user_id: <span class="hljs-built_in">int</span></span>):
    ...</code></pre><p class="my-4 font-light">sql/users.py</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-title">from</span> sqlalchemy <span class="hljs-keyword">import</span> Boolean, Text, String, Column
<span class="hljs-title">from</span> sqlalchemy.orm <span class="hljs-keyword">import</span> declarative_base

<span class="hljs-type">Base</span> = declarative_base()
<span class="hljs-class">
<span class="hljs-keyword">class</span> <span class="hljs-type">UserTable</span>(<span class="hljs-type">Base</span>):
    __tablename__ = &quot;users&quot;
    # 如果不是 sqlite，请使用 <span class="hljs-type">BIGINT</span>
    id = <span class="hljs-type">Column</span>(<span class="hljs-type">Integer</span>, <span class="hljs-title">autoincrement</span>=<span class="hljs-type">True</span>, <span class="hljs-title">primary_key</span>=<span class="hljs-type">True</span>)
    ...</span></code></pre><p class="my-4 font-light">schemas/users.py</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">from</span> pydantic <span class="hljs-keyword">import</span> BaseModel

<span class="hljs-keyword">class</span> <span class="hljs-title class_">UserModel</span>(<span class="hljs-title class_ inherited__">BaseModel</span>):
    <span class="hljs-built_in">id</span>: <span class="hljs-built_in">int</span>
    ...

    <span class="hljs-keyword">class</span> <span class="hljs-title class_">Config</span>:
        orm_mode = <span class="hljs-literal">True</span></code></pre><p class="my-4 font-light">depends/db.py</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-variable">def</span> <span class="hljs-function"><span class="hljs-title">dep_db</span>():
    <span class="hljs-variable">db</span>: <span class="hljs-variable">Session</span> = <span class="hljs-title">SessionLocal</span>()</span>
    <span class="hljs-variable"><span class="hljs-keyword">try</span></span>:
        <span class="hljs-variable">yield</span> <span class="hljs-variable">db</span>
    <span class="hljs-variable"><span class="hljs-keyword">except</span></span> <span class="hljs-variable">Exception</span>:
        <span class="hljs-variable">db.rollback</span>()
        <span class="hljs-variable">raise</span>
    <span class="hljs-variable"><span class="hljs-keyword">finally</span></span>:
        <span class="hljs-variable">db.close</span>()</code></pre>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">FastAPI 中的 router 相当于 Flask 中的 Blueprint, 用来切分应用到不同的模块。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># views/users.py</span>
<span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> APIRouter

router = APIRouter()

<span class="hljs-meta">@router.get(<span class="hljs-params"><span class="hljs-string">&quot;/me&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">myinfo</span>():
    ...

<span class="hljs-comment"># main.py</span>
<span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI
<span class="hljs-keyword">from</span> views <span class="hljs-keyword">import</span> users

app = FastAPI()

app.include_router(users.router, prefix=<span class="hljs-string">&quot;/users&quot;</span>)</code></pre><h2 class="my-2 font-semibold text-xl">文件组织</h2><p class="my-4 font-light">模块化之后，自然需要考虑文件是怎么组织的。一般情况下，我会这么做：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>|<span class="hljs-string">-- app.py
</span>|<span class="hljs-string">-- views/
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- __init__.py
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- users.py
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- tasks.py
</span>|<span class="hljs-string">-- depends/
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- __init__.py
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- db.py
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- users.py
</span>|<span class="hljs-string">-- sql/
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- __init__.py
</span>|<span class="hljs-string">   </span>|<span class="hljs-string">-- users.py
+-- schemas/
    </span>|<span class="hljs-string">-- __init__.py
    </span>|<span class="hljs-string">-- users.py</span></code></pre><p class="my-4 font-light">其中 app.py 存放 app实例，views 存放对应的 routers 文件，sql 存放 sqlalchemy 模型文件，
schemas 存放对应的 pydantic 类型文件，depends 存放一些依赖文件，后面的章节会讲到。</p><p class="my-4 font-light">一些文件的代码如下：</p><p class="my-4 font-light">app.py</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>from fastapi import FastAPI
from views import users, tasks,<span class="hljs-operator"> ...

</span>app = <span class="hljs-constructor">FastAPI()</span>
app.<span class="hljs-keyword">include</span><span class="hljs-constructor">_router(<span class="hljs-params">users</span>.<span class="hljs-params">router</span>, <span class="hljs-params">prefix</span>=<span class="hljs-string">&quot;/users&quot;</span>, <span class="hljs-params">tags</span>=[<span class="hljs-string">&quot;User&quot;</span>])</span>
app.<span class="hljs-keyword">include</span><span class="hljs-constructor">_router(<span class="hljs-params">tasks</span>.<span class="hljs-params">router</span>, <span class="hljs-params">prefix</span>=<span class="hljs-string">&quot;/tasks&quot;</span>, <span class="hljs-params">tags</span>=[<span class="hljs-string">&quot;Task&quot;</span>])</span></code></pre><p class="my-4 font-light">views/users.py</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> APIRouter
<span class="hljs-keyword">from</span> pydantic <span class="hljs-keyword">import</span> BaseModel
<span class="hljs-keyword">from</span> schemas <span class="hljs-keyword">import</span> UserModel

router = APIRouter()

<span class="hljs-keyword">class</span> <span class="hljs-title class_">UserListOut</span>(<span class="hljs-title class_ inherited__">BaseModel</span>):
    users: <span class="hljs-built_in">list</span>[UserModel]

<span class="hljs-meta">@router.get(<span class="hljs-params"><span class="hljs-string">&quot;&quot;</span>, response_model=UserListOut</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">list_users</span>():
    ...

<span class="hljs-meta">@router.get(<span class="hljs-params"><span class="hljs-string">&quot;/{user_id}&quot;</span>, response_model=UserModel</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_user</span>(<span class="hljs-params">user_id: <span class="hljs-built_in">int</span></span>):
    ...</code></pre><p class="my-4 font-light">sql/users.py</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-title">from</span> sqlalchemy <span class="hljs-keyword">import</span> Boolean, Text, String, Column
<span class="hljs-title">from</span> sqlalchemy.orm <span class="hljs-keyword">import</span> declarative_base

<span class="hljs-type">Base</span> = declarative_base()
<span class="hljs-class">
<span class="hljs-keyword">class</span> <span class="hljs-type">UserTable</span>(<span class="hljs-type">Base</span>):
    __tablename__ = &quot;users&quot;
    # 如果不是 sqlite，请使用 <span class="hljs-type">BIGINT</span>
    id = <span class="hljs-type">Column</span>(<span class="hljs-type">Integer</span>, <span class="hljs-title">autoincrement</span>=<span class="hljs-type">True</span>, <span class="hljs-title">primary_key</span>=<span class="hljs-type">True</span>)
    ...</span></code></pre><p class="my-4 font-light">schemas/users.py</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">from</span> pydantic <span class="hljs-keyword">import</span> BaseModel

<span class="hljs-keyword">class</span> <span class="hljs-title class_">UserModel</span>(<span class="hljs-title class_ inherited__">BaseModel</span>):
    <span class="hljs-built_in">id</span>: <span class="hljs-built_in">int</span>
    ...

    <span class="hljs-keyword">class</span> <span class="hljs-title class_">Config</span>:
        orm_mode = <span class="hljs-literal">True</span></code></pre><p class="my-4 font-light">depends/db.py</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-variable">def</span> <span class="hljs-function"><span class="hljs-title">dep_db</span>():
    <span class="hljs-variable">db</span>: <span class="hljs-variable">Session</span> = <span class="hljs-title">SessionLocal</span>()</span>
    <span class="hljs-variable"><span class="hljs-keyword">try</span></span>:
        <span class="hljs-variable">yield</span> <span class="hljs-variable">db</span>
    <span class="hljs-variable"><span class="hljs-keyword">except</span></span> <span class="hljs-variable">Exception</span>:
        <span class="hljs-variable">db.rollback</span>()
        <span class="hljs-variable">raise</span>
    <span class="hljs-variable"><span class="hljs-keyword">finally</span></span>:
        <span class="hljs-variable">db.close</span>()</code></pre>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[为什么不要加入创业公司]]></title>
            <link>https://yifei.me/note/377</link>
            <guid>https://yifei.me/note/377</guid>
            <pubDate>Sat, 19 Aug 2023 10:13:41 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">据统计，92% 的创业公司，活不过三年，一定不要盲选创业公司，为了创业而创业。你以为在中关村
摆个柜台，十年之后就一定可以成为刘强东么？</p><p class="my-4 font-light">还有一个更搞笑的说法，说什么创业公司最大的财富都是在最后一个阶段产生的，所以任何时候上车
都不晚，实际上这纯属扯淡。你在第一年加入，以后可能是一万倍的收益，而在最后一年可能只有十
倍的收益。冒着猝死的风险，只博最后一点收益，喝一点别人的残羹冷炙，实在不值得的。</p><p class="my-4 font-light">下面讲点实际的缺点：</p><h2 class="my-2 font-semibold text-xl">精益创业的弊端</h2><p class="my-4 font-light">所谓的精益创业，小步迭代不一定能产生预期的结果：</p><ul class="my-4 ml-4 font-light list-disc"><li class="">在小范围试验可行的产品不一定能推广到更大的市场。</li><li class="">容易自我洗脑并陶醉，逐渐优化到一个自嗨的市场。</li><li class="">天天改需求，而不产生最终产品，甚至自以为是快速迭代。</li></ul><h2 class="my-2 font-semibold text-xl">小公司的沟通效率反而是低下的</h2><p class="my-4 font-light">原因大概有几方面：</p><p class="my-4 font-light">其一是水平不足。很简单的技术选型问题，在大公司可能使用业界主流做法就很好了，着实没有讨论
的必要，马上执行即可。然而在小公司，可能因为水平不足，需要反复沟通和普及一些基础知识，
甚至需要反复确认主流方案是不是业界最佳实践。</p><p class="my-4 font-light">其二是政治原因。不要以为只有在大公司才有办公室政治，小公司同样会有，所谓：庙小妖风大，
池浅王八多是也。融资、重组、股权分配都可能带来派系的纷争。</p><p class="my-4 font-light">其三是流程不规范。大公司标准化的流程虽然会扼杀一部分创新，但是也尤其强大之处。由于流程
已经固定，就可以把注意力全都放在具体要解决的事情上。而在小公司，由于还没有固定的沟通流程，
在交流之中，每个人可能都有不同的预期，导致精力耗散在了扯皮之中，而不是具体业务。</p><h2 class="my-2 font-semibold text-xl">小公司技术可能很乱</h2><p class="my-4 font-light">举些亲身经历的例子：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">数据库表中没有 id 字段。数据库要加 id 字段就像每天要吃饭一样天经地义吧...</li><li class="">拼音变量名。不是难以表达的业务术语，而是有精确惯用词的普通变量。</li><li class="">代码没有打印任何日志。这怎么 debug？</li><li class="">if xxx == False 这种代码都很常见...</li></ol><p class="my-4 font-light">为了快速迭代，当然可以上一些快糙猛的的方式，但是以上代码早已超出了可以容忍的技术债范围。</p><h2 class="my-2 font-semibold text-xl">小公司的个人成长</h2><p class="my-4 font-light">小公司并不是更快的成长之路。在小公司因为各种基础设施都没有，所以你可能需要建设的是一些没
有实际意义的 trivial 的烦事，以至于无法陷入深入思考之中。人的成长最好是“十”字形的人才，
根深才能叶茂，在某一方面有深厚的内功是很重要的。</p><p class="my-4 font-light">而在大公司，也不一定是螺丝钉，相反更可能是站在巨人的肩膀上，在更高的平台做更有挑战的事儿。</p><p class="my-4 font-light">另外，你本身的能力很有可能高于岗位要求，降薪做一些技术能力无法成长的事情。退回了工作两年
时候做的事情，基本上都是繁琐的业务研发，已经完全没有什么所谓的技术问题了</p><p class="my-4 font-light">无论是在金钱上、时间上，创业都是一种巨大的投入，还冒着高度的失败风险，很可能让人患得患失。</p><h2 class="my-2 font-semibold text-xl">公司自身的成长挑战</h2><p class="my-4 font-light">并不是每一个公司都能够指数增长，大多数的创业公司都会遭遇成长瓶颈。</p><p class="my-4 font-light">期权并不值那么多钱，要考虑到货币的时间价值，以及最后老板稀释期权的风险，还有交给国家的
45% 的个税等等。</p><p class="my-4 font-light">即使开始时很顺利，还要面临的一个风险是：创始团队的成长赶不上公司的成长。管理 1000 个人和
管理 20 个人需要的是完全不一样的技能。</p><p class="my-4 font-light">另一个不易察觉的事实是，有不少公司，尤其是 SaaS，其实是在假装创业罢了，实质上就是小型的
「技术外包」公司罢了，不要被光鲜的外表蒙蔽了。</p><h2 class="my-2 font-semibold text-xl">脉脉上的总结</h2><p class="my-4 font-light">以下这几点缺陷是从脉脉上看到的，个人认为总结的也挺好。</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">公司管理体系不完善，福利惨淡，工作界限不明晰</li><li class="">一人多用，没有话语权，过分强调 KPI</li><li class="">形式化严重，工作流程复杂，沟通成本高，人员流动频繁</li><li class="">公司实力不足，资源有限，盈利模式单一，融资困难</li><li class="">公司用人疑心重，不放权，中层不作为</li><li class="">人少，无流程无规章，老板说了算，想一出是一出</li></ol><h2 class="my-2 font-semibold text-xl">什么样的创业公司才靠谱呢？</h2><p class="my-4 font-light">谈了这么多不靠谱的地方，那么什么的公司才值得加入呢？我认为符合 Sam Altman 在 How to start
a startup 课程中描述的公司大概才是靠谱的。如果你不知道 Sam Altman 和 YC，那就更想都别想
创业这件事了，先去了解一下吧～</p><p class="my-4 font-light">最后，阅读本文纯属浪费时间，因为本文探讨的这些问题在可预见的未来都不存在了。由于北京当局
出台的《反间谍法》《App 备案》等等各种法律法规，实际上相当于完全抹杀了互联网行业创业的
任何可能，再加上拜登政府不失时机颁布的禁止美国 VC 投资中国高科技行业法案，更是雪上加霜。
还想创业的话，只有一个途径了：</p><p class="my-4 font-light">润。</p><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://news.ycombinator.com/item?id=21868022" title="undefined">https://news.ycombinator.com/item?id=21868022</a></li><li class=""><a class="underline" href="https://blog.harrison.dev/2018/11/25/twenty-questions-to-ask-before-joining-a-startup.html" title="undefined">https://blog.harrison.dev/2018/11/25/twenty-questions-to-ask-before-joining-a-startup.html</a></li><li class=""><a class="underline" href="https://www.1point3acres.com/bbs/thread-757861-1-1.html" title="undefined">https://www.1point3acres.com/bbs/thread-757861-1-1.html</a></li></ol>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">据统计，92% 的创业公司，活不过三年，一定不要盲选创业公司，为了创业而创业。你以为在中关村
摆个柜台，十年之后就一定可以成为刘强东么？</p><p class="my-4 font-light">还有一个更搞笑的说法，说什么创业公司最大的财富都是在最后一个阶段产生的，所以任何时候上车
都不晚，实际上这纯属扯淡。你在第一年加入，以后可能是一万倍的收益，而在最后一年可能只有十
倍的收益。冒着猝死的风险，只博最后一点收益，喝一点别人的残羹冷炙，实在不值得的。</p><p class="my-4 font-light">下面讲点实际的缺点：</p><h2 class="my-2 font-semibold text-xl">精益创业的弊端</h2><p class="my-4 font-light">所谓的精益创业，小步迭代不一定能产生预期的结果：</p><ul class="my-4 ml-4 font-light list-disc"><li class="">在小范围试验可行的产品不一定能推广到更大的市场。</li><li class="">容易自我洗脑并陶醉，逐渐优化到一个自嗨的市场。</li><li class="">天天改需求，而不产生最终产品，甚至自以为是快速迭代。</li></ul><h2 class="my-2 font-semibold text-xl">小公司的沟通效率反而是低下的</h2><p class="my-4 font-light">原因大概有几方面：</p><p class="my-4 font-light">其一是水平不足。很简单的技术选型问题，在大公司可能使用业界主流做法就很好了，着实没有讨论
的必要，马上执行即可。然而在小公司，可能因为水平不足，需要反复沟通和普及一些基础知识，
甚至需要反复确认主流方案是不是业界最佳实践。</p><p class="my-4 font-light">其二是政治原因。不要以为只有在大公司才有办公室政治，小公司同样会有，所谓：庙小妖风大，
池浅王八多是也。融资、重组、股权分配都可能带来派系的纷争。</p><p class="my-4 font-light">其三是流程不规范。大公司标准化的流程虽然会扼杀一部分创新，但是也尤其强大之处。由于流程
已经固定，就可以把注意力全都放在具体要解决的事情上。而在小公司，由于还没有固定的沟通流程，
在交流之中，每个人可能都有不同的预期，导致精力耗散在了扯皮之中，而不是具体业务。</p><h2 class="my-2 font-semibold text-xl">小公司技术可能很乱</h2><p class="my-4 font-light">举些亲身经历的例子：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">数据库表中没有 id 字段。数据库要加 id 字段就像每天要吃饭一样天经地义吧...</li><li class="">拼音变量名。不是难以表达的业务术语，而是有精确惯用词的普通变量。</li><li class="">代码没有打印任何日志。这怎么 debug？</li><li class="">if xxx == False 这种代码都很常见...</li></ol><p class="my-4 font-light">为了快速迭代，当然可以上一些快糙猛的的方式，但是以上代码早已超出了可以容忍的技术债范围。</p><h2 class="my-2 font-semibold text-xl">小公司的个人成长</h2><p class="my-4 font-light">小公司并不是更快的成长之路。在小公司因为各种基础设施都没有，所以你可能需要建设的是一些没
有实际意义的 trivial 的烦事，以至于无法陷入深入思考之中。人的成长最好是“十”字形的人才，
根深才能叶茂，在某一方面有深厚的内功是很重要的。</p><p class="my-4 font-light">而在大公司，也不一定是螺丝钉，相反更可能是站在巨人的肩膀上，在更高的平台做更有挑战的事儿。</p><p class="my-4 font-light">另外，你本身的能力很有可能高于岗位要求，降薪做一些技术能力无法成长的事情。退回了工作两年
时候做的事情，基本上都是繁琐的业务研发，已经完全没有什么所谓的技术问题了</p><p class="my-4 font-light">无论是在金钱上、时间上，创业都是一种巨大的投入，还冒着高度的失败风险，很可能让人患得患失。</p><h2 class="my-2 font-semibold text-xl">公司自身的成长挑战</h2><p class="my-4 font-light">并不是每一个公司都能够指数增长，大多数的创业公司都会遭遇成长瓶颈。</p><p class="my-4 font-light">期权并不值那么多钱，要考虑到货币的时间价值，以及最后老板稀释期权的风险，还有交给国家的
45% 的个税等等。</p><p class="my-4 font-light">即使开始时很顺利，还要面临的一个风险是：创始团队的成长赶不上公司的成长。管理 1000 个人和
管理 20 个人需要的是完全不一样的技能。</p><p class="my-4 font-light">另一个不易察觉的事实是，有不少公司，尤其是 SaaS，其实是在假装创业罢了，实质上就是小型的
「技术外包」公司罢了，不要被光鲜的外表蒙蔽了。</p><h2 class="my-2 font-semibold text-xl">脉脉上的总结</h2><p class="my-4 font-light">以下这几点缺陷是从脉脉上看到的，个人认为总结的也挺好。</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">公司管理体系不完善，福利惨淡，工作界限不明晰</li><li class="">一人多用，没有话语权，过分强调 KPI</li><li class="">形式化严重，工作流程复杂，沟通成本高，人员流动频繁</li><li class="">公司实力不足，资源有限，盈利模式单一，融资困难</li><li class="">公司用人疑心重，不放权，中层不作为</li><li class="">人少，无流程无规章，老板说了算，想一出是一出</li></ol><h2 class="my-2 font-semibold text-xl">什么样的创业公司才靠谱呢？</h2><p class="my-4 font-light">谈了这么多不靠谱的地方，那么什么的公司才值得加入呢？我认为符合 Sam Altman 在 How to start
a startup 课程中描述的公司大概才是靠谱的。如果你不知道 Sam Altman 和 YC，那就更想都别想
创业这件事了，先去了解一下吧～</p><p class="my-4 font-light">最后，阅读本文纯属浪费时间，因为本文探讨的这些问题在可预见的未来都不存在了。由于北京当局
出台的《反间谍法》《App 备案》等等各种法律法规，实际上相当于完全抹杀了互联网行业创业的
任何可能，再加上拜登政府不失时机颁布的禁止美国 VC 投资中国高科技行业法案，更是雪上加霜。
还想创业的话，只有一个途径了：</p><p class="my-4 font-light">润。</p><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://news.ycombinator.com/item?id=21868022" title="undefined">https://news.ycombinator.com/item?id=21868022</a></li><li class=""><a class="underline" href="https://blog.harrison.dev/2018/11/25/twenty-questions-to-ask-before-joining-a-startup.html" title="undefined">https://blog.harrison.dev/2018/11/25/twenty-questions-to-ask-before-joining-a-startup.html</a></li><li class=""><a class="underline" href="https://www.1point3acres.com/bbs/thread-757861-1-1.html" title="undefined">https://www.1point3acres.com/bbs/thread-757861-1-1.html</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[curl_cffi: 支持原生模拟浏览器 TLS/JA3 指纹的 Python 库]]></title>
            <link>https://yifei.me/note/2719</link>
            <guid>https://yifei.me/note/2719</guid>
            <pubDate>Sat, 28 Jan 2023 10:08:05 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">越来越多的网站开始使用 TLS 指纹反爬虫，而 Python 中竟然没有任何方法解决这个问题。前一阵
看到由国外大神写了一个 curl-impersonate 命令行工具，可以完美模拟主流浏览器的指纹，遂用
cffi 封装成了 Python 库 <a class="underline" href="https://github.com/yifeikong/curl_cffi" title="null">curl_cffi</a>，这样就可以
继续愉快地写爬虫啦！</p><h2 class="my-2 font-semibold text-xl">TLS 指纹</h2><p class="my-4 font-light">首先来回顾一下什么是 TLS 指纹。如果已经了解，可以直接跳到后边的 curl_cffi 部分。</p><p class="my-4 font-light">现在绝大多数的网站都已经使用了 HTTPS，要建立 HTTPS 链接，服务器和客户端之间首先要进行
TLS 握手，在握手过程中交换双方支持的 TLS 版本，加密算法等信息。不同的客户端之间的差异
很大，而且一般这些信息还都是稳定的，所以服务端就可以根据 TLS 的握手信息来作为特征，识别
一个请求是普通的用户浏览器访问，还是来自 Python 脚本等的自动化访问。</p><p class="my-4 font-light">JA3 是生成 TLS 指纹的一个常用算法。它的工作原理也很简单，大概就是把以上特征拼接并求 md5。</p><p class="my-4 font-light">有证据表明，阿里云、华为云、Akamai 和 Cloudflare 都在使用 TLS 指纹技术来识别机器访问流量。
Akamai 更是直接在宣传稿中说明了在通过 TLS 指纹技术检测非法请求。</p><blockquote class="pl-4 pr-0 py-2 bg-gray-100 border-l-8"><p class="my-4 font-light">在真正发现 Cipher Stunting 之前，Akamai 观察到的 TLS 指纹大概有数万个。在初步发现后不久，
TLS 指纹数量激增至数百万，最近跃升至数十亿。
<a class="underline" href="https://www.akamai.com/blog/security/bots-tampering-with-tls-to-avoid-detection" title="undefined">https://www.akamai.com/blog/security/bots-tampering-with-tls-to-avoid-detection</a></p></blockquote><p class="my-4 font-light">查看 tls 指纹的网站有：</p><ul class="my-4 ml-4 font-light list-disc"><li class=""><a class="underline" href="https://tls.browserleaks.com/json" title="undefined">https://tls.browserleaks.com/json</a></li><li class=""><a class="underline" href="https://tls.peet.ws/" title="undefined">https://tls.peet.ws/</a></li><li class=""><a class="underline" href="https://kawayiyi.com/tls" title="undefined">https://kawayiyi.com/tls</a></li></ul><p class="my-4 font-light">不同网站的生成的指纹可能有差异，但是多次访问同一个网站生成的指纹是稳定的，而且能区分开
不同客户端。下文以第一个网站为例。</p><p class="my-4 font-light">浏览器的指纹：53ff64ddf993ca882b70e1c82af5da49</p><p class="my-4 font-light"><img src="https://img.yifei.me/file/onefly-public/notes/2023/01/browser-ja3.png" alt="browser ja3"></p><p class="my-4 font-light">httpx 的指纹：44423a0e34badcd72364f09ff481fcc9</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>Python <span class="hljs-number">3.10</span><span class="hljs-number">.9</span> (main, Jan <span class="hljs-number">11</span> <span class="hljs-number">2023</span>, <span class="hljs-number">15</span>:<span class="hljs-number">21</span>:<span class="hljs-number">40</span>) [GCC <span class="hljs-number">11.2</span><span class="hljs-number">.0</span>] <span class="hljs-keyword">on</span> linux
<span class="hljs-keyword">Type</span> &quot;help&quot;, &quot;copyright&quot;, &quot;credits&quot; <span class="hljs-keyword">or</span> &quot;license&quot; <span class="hljs-keyword">for</span> more information.
&gt;&gt;&gt; <span class="hljs-keyword">import</span> httpx
&gt;&gt;&gt; r = httpx.<span class="hljs-keyword">get</span>(&quot;https://tls.browserleaks.com/json&quot;)
&gt;&gt;&gt; r.json()
{<span class="hljs-string">&#x27;ja3_hash&#x27;</span>: <span class="hljs-string">&#x27;44423a0e34badcd72364f09ff481fcc9&#x27;</span>, <span class="hljs-string">&#x27;ja3_text&#x27;</span>: <span class="hljs-string">&#x27;772,4866</span></code></pre><p class="my-4 font-light">curl 的指纹：0ef95c8302480557fbc3cd8a7c87973c</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>$ curl --version

curl <span class="hljs-number">7.81</span>.<span class="hljs-number">0</span> (x86_64-pc-linux-gnu) libcurl<span class="hljs-regexp">/7.81.0 OpenSSL/</span><span class="hljs-number">3.0</span>.<span class="hljs-number">2</span> zlib<span class="hljs-regexp">/1.2.11 brotli/</span><span class="hljs-number">1.0</span>.<span class="hljs-number">9</span> zstd<span class="hljs-regexp">/1.4.8 libidn2/</span><span class="hljs-number">2.3</span>.<span class="hljs-number">2</span> libpsl<span class="hljs-regexp">/0.21.0 (+libidn2/</span><span class="hljs-number">2.3</span>.<span class="hljs-number">2</span>) libssh<span class="hljs-regexp">/0.9.6/</span>openssl<span class="hljs-regexp">/zlib nghttp2/</span><span class="hljs-number">1.43</span>.<span class="hljs-number">0</span> librtmp<span class="hljs-regexp">/2.3 OpenLDAP/</span><span class="hljs-number">2.5</span>.<span class="hljs-number">11</span>
Release-Date: <span class="hljs-number">2022</span>-<span class="hljs-number">01</span>-<span class="hljs-number">05</span>
Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp 
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM NTLM_WB PSL SPNEGO SSL TLS-SRP UnixSockets zstd

$ curl https:<span class="hljs-regexp">//</span>tls.browserleaks.com/json

{<span class="hljs-string">&quot;ja3_hash&quot;</span>:<span class="hljs-string">&quot;0ef95c8302480557fbc3cd8a7c87973c&quot;</span>,<span class="hljs-string">&quot;ja3_text&quot;</span>:<span class="hljs-string">&quot;772,4866-4867-4865</span></code></pre><p class="my-4 font-light">可以看到，每个客户端的指纹都是不一致的，服务端也就可以据此防御异常流量。显然，防御等级分
两个层次。</p><h3 class="my-2 font-semibold ">非法指纹黑名单</h3><p class="my-4 font-light">这个思路很直接，把常用的爬虫工具的指纹收集起来，然后全都屏蔽了就好了。比如说：curl,
requests, golang 访问时，直接 403。当然，突破也很简单，别用默认的指纹，直接随便改一下
tls hello 包的值就行了。</p><p class="my-4 font-light">比如，修改 httpx 的 TLS 协议。以 httpx 为例:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 默认 cipher 在这里定义：https://github.com/encode/httpx/blob/master/httpx/_config.py</span>
<span class="hljs-keyword">import</span> ssl
<span class="hljs-keyword">import</span> httpx

<span class="hljs-comment"># create an ssl context</span>
ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS)
CIPHERS = <span class="hljs-string">&#x27;ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:DH+HIGH:RSA+AESGCM:RSA+AES:RSA+HIGH&#x27;</span>
ssl_context.set_ciphers(CIPHERS)

r = httpx.get(<span class="hljs-string">&#x27;https://tls.browserleaks.com/json&#x27;</span>, verify=ssl_context)
<span class="hljs-built_in">print</span>(r.json())

<span class="hljs-comment"># {&#x27;ja3_hash&#x27;: &#x27;cc8fc04d55d8c9c318409384eee468b6&#x27;</span></code></pre><p class="my-4 font-light">可以看到 JA3 指纹已经变了。</p><h3 class="my-2 font-semibold ">合法指纹白名单</h3><p class="my-4 font-light">既然指纹可以随便改，那就直接只认常用浏览器的指纹好了。这时候如果爬虫或者其他脚本再想要
突破防御，需要把每一个值都改成和浏览器都完全相同，难度还是挺大的。尤其是考虑到大多数
语言的标准库都是直接使用系统的 SSL 库，很多底层的东西直接没提供接口，所以这种防御还是非常
有效的。</p><p class="my-4 font-light">例如，Python 使用了 OpenSSL，而 Chrome 则使用了 BoringSSL，这两者的细节差异很多。所以，
纯 Python 的库，比如 requests 和 httpx，再怎么改也不可能改成和 Chrome 一样的指纹，必须
使用第三方的 C 扩展库，才能够实现完美模拟浏览器指纹。</p><p class="my-4 font-light">此外，还又一个小细节，可以由 TLS 指纹反推出客户端是从哪些操作系统或者软件来的，如果和
User-Agent 互相矛盾，那也说明有问题。不过实际中，我还没有遇到这种情况。</p><h2 class="my-2 font-semibold text-xl">curl_cffi</h2><p class="my-4 font-light">为了完美模拟浏览器，国外有大佬给 curl 打了一些 patch，把相应组件全部都替换成了浏览器使用
库，连版本都保持一致，这样就得到了和浏览器完全一样的指纹，这个库是：<a class="underline" href="https://github.com/lwthiker/curl-impersonate" title="null">curl-impersonate</a></p><p class="my-4 font-light">Python 中早就有 curl 的 binding -- pycurl，但是非常难用，安装的时候总是出现编译错误；接口
也很低级，相比 requests，甚至 urllib，用起来都比较费劲。curl-impersonate 的作者提出使用
环境变量 + 替换 libcurl 来在不同语言中使用 curl-impersonate，但是似乎 pycurl 没法工作。
于是乎，我直接另起炉灶，写了一个 curl(-impersonate) 的 Python binding.</p><p class="my-4 font-light">相比 pycurl，有以下优点：</p><ul class="my-4 ml-4 font-light list-disc"><li class="">原生支持 curl-impersonate</li><li class="">pip install 直接是二进制包，无需编译，也就不会有编译错误</li><li class="">提供了一个简单的 requests-like 接口</li></ul><p class="my-4 font-light">废话少说，看代码吧！</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>pip <span class="hljs-keyword">install</span> curl_cffi</code></pre><p class="my-4 font-light">使用起来也很简单</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">from</span> curl_cffi import requests

<span class="hljs-comment"># 注意这个 impersonate 参数，指定了模拟哪个浏览器</span>
r = requests.<span class="hljs-built_in">get</span>(<span class="hljs-string">&quot;https://tls.browserleaks.com/json&quot;</span>, <span class="hljs-attribute">impersonate</span>=<span class="hljs-string">&quot;chrome101&quot;</span>)

<span class="hljs-built_in">print</span>(r.json())
<span class="hljs-comment"># output: {&#x27;ja3_hash&#x27;: &#x27;53ff64ddf993ca882b70e1c82af5da49&#x27;</span></code></pre><p class="my-4 font-light">我们可以看到，输出的 JA3 指纹和浏览器中的指纹<strong>一模一样</strong>！</p><p class="my-4 font-light">代理也支持：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>&gt;&gt;&gt; proxies={<span class="hljs-string">&quot;http&quot;</span>: <span class="hljs-string">&quot;http://localhost:7777&quot;</span>, <span class="hljs-string">&quot;https&quot;</span>: <span class="hljs-string">&quot;http://localhost:7777&quot;</span>}

&gt;&gt;&gt; r = requests.<span class="hljs-built_in">get</span>(<span class="hljs-string">&quot;http://baidu.com&quot;</span>, 
        <span class="hljs-attribute">proxies</span>=proxies,
        <span class="hljs-attribute">allow_redirects</span>=<span class="hljs-literal">False</span>,
        <span class="hljs-attribute">impersonate</span>=<span class="hljs-string">&quot;chrome101&quot;</span>
    )
&gt;&gt;&gt; r.text
<span class="hljs-string">&#x27;&lt;html&gt;\r\n&lt;head&gt;&lt;title&gt;302 Found&lt;/title&gt;&lt;/head&gt;\r\n&lt;body bgcolor=&quot;white&quot;&gt;\r\n&lt;center&gt;&lt;h1&gt;302 Found&lt;/h1&gt;&lt;/center&gt;\r\n&lt;hr&gt;&lt;center&gt;bfe/1.0.8.18&lt;/center&gt;\r\n&lt;/body&gt;\r\n&lt;/html&gt;\r\n&#x27;</span>
&gt;&gt;&gt; r = requests.<span class="hljs-built_in">get</span>(<span class="hljs-string">&quot;https://tls.browserleaks.com/json&quot;</span>,
        <span class="hljs-attribute">proxies</span>=proxies,
        <span class="hljs-attribute">impersonate</span>=<span class="hljs-string">&quot;chrome101&quot;</span>
    )
&gt;&gt;&gt; r.json()
{<span class="hljs-string">&#x27;ja3_hash&#x27;</span>: <span class="hljs-string">&#x27;53ff64ddf993ca882b70e1c82af5da49&#x27;</span></code></pre><p class="my-4 font-light">同样的功能，也可以用底层一点的 Curl 对象：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">from</span> curl_cffi <span class="hljs-keyword">import</span> Curl, CurlOpt
<span class="hljs-keyword">from</span> io <span class="hljs-keyword">import</span> BytesIO

buffer = BytesIO()
c = Curl()
c.setopt(CurlOpt.URL, b<span class="hljs-string">&#x27;https://tls.browserleaks.com/json&#x27;</span>)
c.setopt(CurlOpt.WRITEDATA, buffer)

c.impersonate(&quot;chrome101&quot;)

c.<span class="hljs-keyword">perform</span>()
c.<span class="hljs-keyword">close</span>()
body = buffer.getvalue()
print(body.decode())</code></pre><p class="my-4 font-light">仓库在这里：<a class="underline" href="https://github.com/yifeikong/curl_cffi" title="undefined">https://github.com/yifeikong/curl_cffi</a></p><h2 class="my-2 font-semibold text-xl">其他指纹技术概览</h2><ol class="my-4 ml-4 font-light list-decimal"><li class="">HTTP Header 指纹。通过浏览器发送的 header 的顺序和值的组合来判断是合法用户还是爬虫</li><li class="">DNS 指纹。参考：<a class="underline" href="http://dnscookie.com" title="undefined">http://dnscookie.com</a></li><li class="">浏览器指纹。通过 canvas，webgl 等计算得到一个唯一指纹，Cookie 禁用时监视用户的主流技术</li><li class="">TCP 指纹。也是根据 TCP 的一些窗口、拥塞控制等参数嗅探、猜测用户的系统版本</li></ol><p class="my-4 font-light">总结一下，指纹技术就是通过不同的设备和客户端在参数上的微妙差异来识别用户。本来按照规范，
这些值都是应该任意选取的，但是，现实世界中，服务端反而对不同值采取了区别对待。指纹技术
可以说应用到了 OSI 网络模型中所有可能的层，基于 HTTP header 顺序的指纹工作在第七层应用层，
SSL/TLS 指纹工作在传输层和应用层之间，TCP 指纹在第四层传输层。而在 TCP 之下的 IP 层和物理
层，因为建立的不是端到端的链路，所以只能收集上一跳的指纹，没有任何意义。</p><p class="my-4 font-light">对于爬虫来说，User-Agent 相当于自报门户。除了初学者以外，没有人会顶着 <code class="px-1 bg-gray-100 border-2 rounded">Python/3.9 requests</code>
这样的 UA 去爬的，而指纹则是很难更改的内部特征。通过指纹技术可以防御一大批爬虫，而使用
能够模拟指纹的 http client 则轻松突破这道防线。</p><p class="my-4 font-light">对于普通用户来说，各种指纹造成了极大的隐私泄露风险。即使按照 GDPR 等监管政策的要求，用户
拒绝使用 Cookie 时，互联网公司依然可以通过各种指纹来定位追踪用户，乃至于区别对待。平等、
匿名、自由地使用个人数据和公开数据应该是一项基本人权。在立法赶不上技术更新的时代，我们
应该用技术手段捍卫自己的权利。</p><h2 class="my-2 font-semibold text-xl">参考</h2><h3 class="my-2 font-semibold ">科普</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://www.tr0y.wang/2020/06/28/ja3/" title="null">2020 - JA3 的一篇科普</a></li><li class=""><a class="underline" href="https://www.guildhab.top/2021/04/%E9%80%9A%E8%BF%87-ja3s-%E5%AE%9E%E7%8E%B0-tls-%E6%8C%87%E7%BA%B9%E8%AF%86%E5%88%AB/" title="null">又一篇 JA3 的介绍</a></li><li class=""><a class="underline" href="https://zhuanlan.zhihu.com/p/54810155" title="null">什么是 C2 架构</a></li><li class=""><a class="underline" href="https://blog.csdn.net/whatday/article/details/105517801" title="null">操作系统识别工具 xprobe2 p0f 简介</a></li><li class=""><a class="underline" href="https://blog.csdn.net/freeking101/article/details/72962349" title="null">p0f - 被动探测操作系统工具</a></li><li class=""><a class="underline" href="https://www.ionos.com/digitalguide/online-marketing/web-analytics/browser-fingerprints-tracking-without-cookies/" title="null">Browser fingerprints: the basics and protection options</a></li></ol><h3 class="my-2 font-semibold ">案例</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://blog.csdn.net/NOSEC2019/article/details/90301394" title="null">2019 - Akamai 检测到随机化 TLS 指纹突破 WAF</a></li><li class=""><a class="underline" href="https://www.v2ex.com/t/792934" title="null">2021 - V 站网友遇到 Cloudflare 指纹检测</a></li><li class=""><a class="underline" href="https://netsecurity.51cto.com/art/202108/678610.htm" title="null">2021 - 为什么随机 IP、随机 UA 也逃不掉被反爬虫的命运</a></li><li class=""><a class="underline" href="https://pr.qiwihui.com/2021/04/21/qiwihui-pocket_readings-1078/" title="null">2021 - Python Requests 会被阿里云识别 TLS 特征</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/60407057/python-requests-being-fingerprinted" title="null">2020 - Requests being fingerprinted</a></li><li class=""><a class="underline" href="https://security.stackexchange.com/questions/236947/how-to-spoof-ja3-signature" title="undefined">https://security.stackexchange.com/questions/236947/how-to-spoof-ja3-signature</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/40373115/how-to-select-specific-the-cipher-while-sending-request-via-python-request-modul" title="null">如何更改 Requests 的 TLS 选项</a></li><li class=""><a class="underline" href="https://www.reddit.com/r/webscraping/comments/ge0dgk/ssltls_fingerprinting/" title="null">2019 - Reddit discussion on SSL/TLS Fingerprinting</a></li><li class=""><a class="underline" href="https://forum.openwrt.org/t/fingerprint-package/29824/18" title="null">Adwords 会检查 TCP 指纹</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/30787664/can-servers-use-http-headers-order-to-catch-a-browser-signature" title="undefined">https://stackoverflow.com/questions/30787664/can-servers-use-http-headers-order-to-catch-a-browser-signature</a></li><li class=""><a class="underline" href="https://github.com/v2ray/discussion/issues/704" title="null">TCP 指纹甚至可以用来识别 V2R</a></li><li class=""><a class="underline" href="https://ares-x.com/2021/04/18/SSL-指纹识别和绕过/" title="null">SSL 指纹识别和绕过</a></li></ol><h3 class="my-2 font-semibold ">工具</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://browserleaks.com/ssl" title="null">查看自己的浏览器指纹</a></li><li class=""><a class="underline" href="https://ja3er.com/downloads.html" title="null">JA3 指纹数据库</a></li><li class=""><a class="underline" href="https://browserleaks.com/ip" title="null">查看自己 IP 泄露的信息，包括 TCP 指纹</a></li><li class=""><a class="underline" href="https://amiunique.org/fp" title="null">查看浏览器指纹</a></li><li class=""><a class="underline" href="https://tlsfingerprint.io" title="null">utls</a></li><li class=""><a class="underline" href="https://bot.incolumitas.com" title="null">查看各种指纹</a></li><li class=""><a class="underline" href="http://dnscookie.com" title="null">DNS Cookie</a></li></ol><h3 class="my-2 font-semibold ">原始研究</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://devcentral.f5.com/s/articles/tls-fingerprinting-a-method-for-identifying-a-tls-client-without-decrypting-24598" title="null">2016 - TLS Fingerprinting - a method for identifying a TLS client without decrypting</a></li><li class=""><a class="underline" href="https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967" title="null">JA3 的原始文章</a></li><li class=""><a class="underline" href="https://github.com/salesforce/ja3" title="null">JA3 GitHub</a></li><li class=""><a class="underline" href="https://lcamtuf.coredump.cx/p0f3/" title="null">p0f3 - OS fingerprinting</a></li><li class=""><a class="underline" href="http://www.goonls.com/?p=1848" title="null">p0f3 应用</a></li><li class=""><a class="underline" href="https://en.wikipedia.org/wiki/TCP/IP_stack_fingerprinting" title="null">TCP 指纹</a></li><li class=""><a class="underline" href="https://en.wikipedia.org/wiki/Device_fingerprint" title="undefined">https://en.wikipedia.org/wiki/Device_fingerprint</a></li><li class=""><a class="underline" href="https://sansec.io/research/http-header-order-is-important" title="undefined">https://sansec.io/research/http-header-order-is-important</a></li><li class=""><a class="underline" href="https://incolumitas.com/pages/BotOrNot/" title="null">一篇非常全的指纹综述</a></li></ol><h3 class="my-2 font-semibold ">伪装</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://www.defensive-security.com/blog/hiding-behind-ja3-hash" title="null">Golang 伪装 JA3 指纹的例子</a></li><li class=""><a class="underline" href="https://medium.com/cu-cyber/impersonating-ja3-fingerprints-b9f555880e42" title="null">又一篇伪装 JA3 指纹的例子</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/64967706/python-requests-https-code-403-without-but-code-200-when-using-burpsuite" title="null">在 Requests 中伪造 TLS 的一个例子</a></li><li class=""><a class="underline" href="https://medium.com/cu-cyber/impersonating-ja3-fingerprints-b9f555880e42" title="null">Go ja3transport</a></li><li class=""><a class="underline" href="https://github.com/encode/httpx/issues/1147" title="null">httpx header order</a></li><li class=""><a class="underline" href="https://github.com/golang/go/issues/24375" title="null">golang header order</a></li><li class=""><a class="underline" href="https://jychp.medium.com/how-to-bypass-cloudflare-bot-protection-1f2c6c0c36fb" title="null">用 Cloudflare worker 突破 Cloudflare 反爬</a></li></ol><h3 class="my-2 font-semibold ">应用</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://datadome.co/bot-detection/client-side-detection-is-essential-for-bot-protection/" title="null">专门做客户端指纹的公司</a></li><li class=""><a class="underline" href="https://github.com/cloudflare/mitmengine" title="null">Cloudflare MITMEngine - Malcolm 背后算法</a></li><li class=""><a class="underline" href="https://malcolm.cloudflare.com/" title="null">Cloudflare malcolm</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/16881373/android-http-request-blocked-by-incapsula" title="null">Incapsula WAF 使用 http header 顺序作为指纹</a></li></ol><h3 class="my-2 font-semibold ">其他</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://www.zhihu.com/question/51864646/answer/127840009" title="null">苏联笑话</a></li><li class=""><a class="underline" href="https://github.com/niespodd/browser-fingerprinting" title="undefined">https://github.com/niespodd/browser-fingerprinting</a></li><li class=""><a class="underline" href="https://github.com/JonasCz/How-To-Prevent-Scraping" title="undefined">https://github.com/JonasCz/How-To-Prevent-Scraping</a></li><li class=""><a class="underline" href="https://www.scrapingbee.com/blog/web-scraping-without-getting-blocked/" title="undefined">https://www.scrapingbee.com/blog/web-scraping-without-getting-blocked/</a></li><li class=""><a class="underline" href="https://incolumitas.com/2021/03/13/tcp-ip-fingerprinting-for-vpn-and-proxy-detection/" title="undefined">https://incolumitas.com/2021/03/13/tcp-ip-fingerprinting-for-vpn-and-proxy-detection/</a></li><li class=""><a class="underline" href="https://incolumitas.com/2021/05/20/avoid-puppeteer-and-playwright-for-scraping/" title="undefined">https://incolumitas.com/2021/05/20/avoid-puppeteer-and-playwright-for-scraping/</a></li><li class=""><a class="underline" href="https://fingerprintjs.com/blog/disabling-javascript-wont-stop-fingerprinting/" title="undefined">https://fingerprintjs.com/blog/disabling-javascript-wont-stop-fingerprinting/</a></li><li class=""><a class="underline" href="https://news.ycombinator.com/item?id=30237846" title="undefined">https://news.ycombinator.com/item?id=30237846</a></li><li class=""><a class="underline" href="https://news.ycombinator.com/item?id=29060272" title="undefined">https://news.ycombinator.com/item?id=29060272</a></li><li class=""><a class="underline" href="https://news.ycombinator.com/item?id=29117022" title="undefined">https://news.ycombinator.com/item?id=29117022</a></li><li class=""><a class="underline" href="https://news.ycombinator.com/item?id=28827509" title="undefined">https://news.ycombinator.com/item?id=28827509</a></li></ol>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">越来越多的网站开始使用 TLS 指纹反爬虫，而 Python 中竟然没有任何方法解决这个问题。前一阵
看到由国外大神写了一个 curl-impersonate 命令行工具，可以完美模拟主流浏览器的指纹，遂用
cffi 封装成了 Python 库 <a class="underline" href="https://github.com/yifeikong/curl_cffi" title="null">curl_cffi</a>，这样就可以
继续愉快地写爬虫啦！</p><h2 class="my-2 font-semibold text-xl">TLS 指纹</h2><p class="my-4 font-light">首先来回顾一下什么是 TLS 指纹。如果已经了解，可以直接跳到后边的 curl_cffi 部分。</p><p class="my-4 font-light">现在绝大多数的网站都已经使用了 HTTPS，要建立 HTTPS 链接，服务器和客户端之间首先要进行
TLS 握手，在握手过程中交换双方支持的 TLS 版本，加密算法等信息。不同的客户端之间的差异
很大，而且一般这些信息还都是稳定的，所以服务端就可以根据 TLS 的握手信息来作为特征，识别
一个请求是普通的用户浏览器访问，还是来自 Python 脚本等的自动化访问。</p><p class="my-4 font-light">JA3 是生成 TLS 指纹的一个常用算法。它的工作原理也很简单，大概就是把以上特征拼接并求 md5。</p><p class="my-4 font-light">有证据表明，阿里云、华为云、Akamai 和 Cloudflare 都在使用 TLS 指纹技术来识别机器访问流量。
Akamai 更是直接在宣传稿中说明了在通过 TLS 指纹技术检测非法请求。</p><blockquote class="pl-4 pr-0 py-2 bg-gray-100 border-l-8"><p class="my-4 font-light">在真正发现 Cipher Stunting 之前，Akamai 观察到的 TLS 指纹大概有数万个。在初步发现后不久，
TLS 指纹数量激增至数百万，最近跃升至数十亿。
<a class="underline" href="https://www.akamai.com/blog/security/bots-tampering-with-tls-to-avoid-detection" title="undefined">https://www.akamai.com/blog/security/bots-tampering-with-tls-to-avoid-detection</a></p></blockquote><p class="my-4 font-light">查看 tls 指纹的网站有：</p><ul class="my-4 ml-4 font-light list-disc"><li class=""><a class="underline" href="https://tls.browserleaks.com/json" title="undefined">https://tls.browserleaks.com/json</a></li><li class=""><a class="underline" href="https://tls.peet.ws/" title="undefined">https://tls.peet.ws/</a></li><li class=""><a class="underline" href="https://kawayiyi.com/tls" title="undefined">https://kawayiyi.com/tls</a></li></ul><p class="my-4 font-light">不同网站的生成的指纹可能有差异，但是多次访问同一个网站生成的指纹是稳定的，而且能区分开
不同客户端。下文以第一个网站为例。</p><p class="my-4 font-light">浏览器的指纹：53ff64ddf993ca882b70e1c82af5da49</p><p class="my-4 font-light"><img src="https://img.yifei.me/file/onefly-public/notes/2023/01/browser-ja3.png" alt="browser ja3"></p><p class="my-4 font-light">httpx 的指纹：44423a0e34badcd72364f09ff481fcc9</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>Python <span class="hljs-number">3.10</span><span class="hljs-number">.9</span> (main, Jan <span class="hljs-number">11</span> <span class="hljs-number">2023</span>, <span class="hljs-number">15</span>:<span class="hljs-number">21</span>:<span class="hljs-number">40</span>) [GCC <span class="hljs-number">11.2</span><span class="hljs-number">.0</span>] <span class="hljs-keyword">on</span> linux
<span class="hljs-keyword">Type</span> &quot;help&quot;, &quot;copyright&quot;, &quot;credits&quot; <span class="hljs-keyword">or</span> &quot;license&quot; <span class="hljs-keyword">for</span> more information.
&gt;&gt;&gt; <span class="hljs-keyword">import</span> httpx
&gt;&gt;&gt; r = httpx.<span class="hljs-keyword">get</span>(&quot;https://tls.browserleaks.com/json&quot;)
&gt;&gt;&gt; r.json()
{<span class="hljs-string">&#x27;ja3_hash&#x27;</span>: <span class="hljs-string">&#x27;44423a0e34badcd72364f09ff481fcc9&#x27;</span>, <span class="hljs-string">&#x27;ja3_text&#x27;</span>: <span class="hljs-string">&#x27;772,4866</span></code></pre><p class="my-4 font-light">curl 的指纹：0ef95c8302480557fbc3cd8a7c87973c</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>$ curl --version

curl <span class="hljs-number">7.81</span>.<span class="hljs-number">0</span> (x86_64-pc-linux-gnu) libcurl<span class="hljs-regexp">/7.81.0 OpenSSL/</span><span class="hljs-number">3.0</span>.<span class="hljs-number">2</span> zlib<span class="hljs-regexp">/1.2.11 brotli/</span><span class="hljs-number">1.0</span>.<span class="hljs-number">9</span> zstd<span class="hljs-regexp">/1.4.8 libidn2/</span><span class="hljs-number">2.3</span>.<span class="hljs-number">2</span> libpsl<span class="hljs-regexp">/0.21.0 (+libidn2/</span><span class="hljs-number">2.3</span>.<span class="hljs-number">2</span>) libssh<span class="hljs-regexp">/0.9.6/</span>openssl<span class="hljs-regexp">/zlib nghttp2/</span><span class="hljs-number">1.43</span>.<span class="hljs-number">0</span> librtmp<span class="hljs-regexp">/2.3 OpenLDAP/</span><span class="hljs-number">2.5</span>.<span class="hljs-number">11</span>
Release-Date: <span class="hljs-number">2022</span>-<span class="hljs-number">01</span>-<span class="hljs-number">05</span>
Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp 
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM NTLM_WB PSL SPNEGO SSL TLS-SRP UnixSockets zstd

$ curl https:<span class="hljs-regexp">//</span>tls.browserleaks.com/json

{<span class="hljs-string">&quot;ja3_hash&quot;</span>:<span class="hljs-string">&quot;0ef95c8302480557fbc3cd8a7c87973c&quot;</span>,<span class="hljs-string">&quot;ja3_text&quot;</span>:<span class="hljs-string">&quot;772,4866-4867-4865</span></code></pre><p class="my-4 font-light">可以看到，每个客户端的指纹都是不一致的，服务端也就可以据此防御异常流量。显然，防御等级分
两个层次。</p><h3 class="my-2 font-semibold ">非法指纹黑名单</h3><p class="my-4 font-light">这个思路很直接，把常用的爬虫工具的指纹收集起来，然后全都屏蔽了就好了。比如说：curl,
requests, golang 访问时，直接 403。当然，突破也很简单，别用默认的指纹，直接随便改一下
tls hello 包的值就行了。</p><p class="my-4 font-light">比如，修改 httpx 的 TLS 协议。以 httpx 为例:</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 默认 cipher 在这里定义：https://github.com/encode/httpx/blob/master/httpx/_config.py</span>
<span class="hljs-keyword">import</span> ssl
<span class="hljs-keyword">import</span> httpx

<span class="hljs-comment"># create an ssl context</span>
ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS)
CIPHERS = <span class="hljs-string">&#x27;ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+HIGH:DH+HIGH:RSA+AESGCM:RSA+AES:RSA+HIGH&#x27;</span>
ssl_context.set_ciphers(CIPHERS)

r = httpx.get(<span class="hljs-string">&#x27;https://tls.browserleaks.com/json&#x27;</span>, verify=ssl_context)
<span class="hljs-built_in">print</span>(r.json())

<span class="hljs-comment"># {&#x27;ja3_hash&#x27;: &#x27;cc8fc04d55d8c9c318409384eee468b6&#x27;</span></code></pre><p class="my-4 font-light">可以看到 JA3 指纹已经变了。</p><h3 class="my-2 font-semibold ">合法指纹白名单</h3><p class="my-4 font-light">既然指纹可以随便改，那就直接只认常用浏览器的指纹好了。这时候如果爬虫或者其他脚本再想要
突破防御，需要把每一个值都改成和浏览器都完全相同，难度还是挺大的。尤其是考虑到大多数
语言的标准库都是直接使用系统的 SSL 库，很多底层的东西直接没提供接口，所以这种防御还是非常
有效的。</p><p class="my-4 font-light">例如，Python 使用了 OpenSSL，而 Chrome 则使用了 BoringSSL，这两者的细节差异很多。所以，
纯 Python 的库，比如 requests 和 httpx，再怎么改也不可能改成和 Chrome 一样的指纹，必须
使用第三方的 C 扩展库，才能够实现完美模拟浏览器指纹。</p><p class="my-4 font-light">此外，还又一个小细节，可以由 TLS 指纹反推出客户端是从哪些操作系统或者软件来的，如果和
User-Agent 互相矛盾，那也说明有问题。不过实际中，我还没有遇到这种情况。</p><h2 class="my-2 font-semibold text-xl">curl_cffi</h2><p class="my-4 font-light">为了完美模拟浏览器，国外有大佬给 curl 打了一些 patch，把相应组件全部都替换成了浏览器使用
库，连版本都保持一致，这样就得到了和浏览器完全一样的指纹，这个库是：<a class="underline" href="https://github.com/lwthiker/curl-impersonate" title="null">curl-impersonate</a></p><p class="my-4 font-light">Python 中早就有 curl 的 binding -- pycurl，但是非常难用，安装的时候总是出现编译错误；接口
也很低级，相比 requests，甚至 urllib，用起来都比较费劲。curl-impersonate 的作者提出使用
环境变量 + 替换 libcurl 来在不同语言中使用 curl-impersonate，但是似乎 pycurl 没法工作。
于是乎，我直接另起炉灶，写了一个 curl(-impersonate) 的 Python binding.</p><p class="my-4 font-light">相比 pycurl，有以下优点：</p><ul class="my-4 ml-4 font-light list-disc"><li class="">原生支持 curl-impersonate</li><li class="">pip install 直接是二进制包，无需编译，也就不会有编译错误</li><li class="">提供了一个简单的 requests-like 接口</li></ul><p class="my-4 font-light">废话少说，看代码吧！</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>pip <span class="hljs-keyword">install</span> curl_cffi</code></pre><p class="my-4 font-light">使用起来也很简单</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">from</span> curl_cffi import requests

<span class="hljs-comment"># 注意这个 impersonate 参数，指定了模拟哪个浏览器</span>
r = requests.<span class="hljs-built_in">get</span>(<span class="hljs-string">&quot;https://tls.browserleaks.com/json&quot;</span>, <span class="hljs-attribute">impersonate</span>=<span class="hljs-string">&quot;chrome101&quot;</span>)

<span class="hljs-built_in">print</span>(r.json())
<span class="hljs-comment"># output: {&#x27;ja3_hash&#x27;: &#x27;53ff64ddf993ca882b70e1c82af5da49&#x27;</span></code></pre><p class="my-4 font-light">我们可以看到，输出的 JA3 指纹和浏览器中的指纹<strong>一模一样</strong>！</p><p class="my-4 font-light">代理也支持：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>&gt;&gt;&gt; proxies={<span class="hljs-string">&quot;http&quot;</span>: <span class="hljs-string">&quot;http://localhost:7777&quot;</span>, <span class="hljs-string">&quot;https&quot;</span>: <span class="hljs-string">&quot;http://localhost:7777&quot;</span>}

&gt;&gt;&gt; r = requests.<span class="hljs-built_in">get</span>(<span class="hljs-string">&quot;http://baidu.com&quot;</span>, 
        <span class="hljs-attribute">proxies</span>=proxies,
        <span class="hljs-attribute">allow_redirects</span>=<span class="hljs-literal">False</span>,
        <span class="hljs-attribute">impersonate</span>=<span class="hljs-string">&quot;chrome101&quot;</span>
    )
&gt;&gt;&gt; r.text
<span class="hljs-string">&#x27;&lt;html&gt;\r\n&lt;head&gt;&lt;title&gt;302 Found&lt;/title&gt;&lt;/head&gt;\r\n&lt;body bgcolor=&quot;white&quot;&gt;\r\n&lt;center&gt;&lt;h1&gt;302 Found&lt;/h1&gt;&lt;/center&gt;\r\n&lt;hr&gt;&lt;center&gt;bfe/1.0.8.18&lt;/center&gt;\r\n&lt;/body&gt;\r\n&lt;/html&gt;\r\n&#x27;</span>
&gt;&gt;&gt; r = requests.<span class="hljs-built_in">get</span>(<span class="hljs-string">&quot;https://tls.browserleaks.com/json&quot;</span>,
        <span class="hljs-attribute">proxies</span>=proxies,
        <span class="hljs-attribute">impersonate</span>=<span class="hljs-string">&quot;chrome101&quot;</span>
    )
&gt;&gt;&gt; r.json()
{<span class="hljs-string">&#x27;ja3_hash&#x27;</span>: <span class="hljs-string">&#x27;53ff64ddf993ca882b70e1c82af5da49&#x27;</span></code></pre><p class="my-4 font-light">同样的功能，也可以用底层一点的 Curl 对象：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">from</span> curl_cffi <span class="hljs-keyword">import</span> Curl, CurlOpt
<span class="hljs-keyword">from</span> io <span class="hljs-keyword">import</span> BytesIO

buffer = BytesIO()
c = Curl()
c.setopt(CurlOpt.URL, b<span class="hljs-string">&#x27;https://tls.browserleaks.com/json&#x27;</span>)
c.setopt(CurlOpt.WRITEDATA, buffer)

c.impersonate(&quot;chrome101&quot;)

c.<span class="hljs-keyword">perform</span>()
c.<span class="hljs-keyword">close</span>()
body = buffer.getvalue()
print(body.decode())</code></pre><p class="my-4 font-light">仓库在这里：<a class="underline" href="https://github.com/yifeikong/curl_cffi" title="undefined">https://github.com/yifeikong/curl_cffi</a></p><h2 class="my-2 font-semibold text-xl">其他指纹技术概览</h2><ol class="my-4 ml-4 font-light list-decimal"><li class="">HTTP Header 指纹。通过浏览器发送的 header 的顺序和值的组合来判断是合法用户还是爬虫</li><li class="">DNS 指纹。参考：<a class="underline" href="http://dnscookie.com" title="undefined">http://dnscookie.com</a></li><li class="">浏览器指纹。通过 canvas，webgl 等计算得到一个唯一指纹，Cookie 禁用时监视用户的主流技术</li><li class="">TCP 指纹。也是根据 TCP 的一些窗口、拥塞控制等参数嗅探、猜测用户的系统版本</li></ol><p class="my-4 font-light">总结一下，指纹技术就是通过不同的设备和客户端在参数上的微妙差异来识别用户。本来按照规范，
这些值都是应该任意选取的，但是，现实世界中，服务端反而对不同值采取了区别对待。指纹技术
可以说应用到了 OSI 网络模型中所有可能的层，基于 HTTP header 顺序的指纹工作在第七层应用层，
SSL/TLS 指纹工作在传输层和应用层之间，TCP 指纹在第四层传输层。而在 TCP 之下的 IP 层和物理
层，因为建立的不是端到端的链路，所以只能收集上一跳的指纹，没有任何意义。</p><p class="my-4 font-light">对于爬虫来说，User-Agent 相当于自报门户。除了初学者以外，没有人会顶着 <code class="px-1 bg-gray-100 border-2 rounded">Python/3.9 requests</code>
这样的 UA 去爬的，而指纹则是很难更改的内部特征。通过指纹技术可以防御一大批爬虫，而使用
能够模拟指纹的 http client 则轻松突破这道防线。</p><p class="my-4 font-light">对于普通用户来说，各种指纹造成了极大的隐私泄露风险。即使按照 GDPR 等监管政策的要求，用户
拒绝使用 Cookie 时，互联网公司依然可以通过各种指纹来定位追踪用户，乃至于区别对待。平等、
匿名、自由地使用个人数据和公开数据应该是一项基本人权。在立法赶不上技术更新的时代，我们
应该用技术手段捍卫自己的权利。</p><h2 class="my-2 font-semibold text-xl">参考</h2><h3 class="my-2 font-semibold ">科普</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://www.tr0y.wang/2020/06/28/ja3/" title="null">2020 - JA3 的一篇科普</a></li><li class=""><a class="underline" href="https://www.guildhab.top/2021/04/%E9%80%9A%E8%BF%87-ja3s-%E5%AE%9E%E7%8E%B0-tls-%E6%8C%87%E7%BA%B9%E8%AF%86%E5%88%AB/" title="null">又一篇 JA3 的介绍</a></li><li class=""><a class="underline" href="https://zhuanlan.zhihu.com/p/54810155" title="null">什么是 C2 架构</a></li><li class=""><a class="underline" href="https://blog.csdn.net/whatday/article/details/105517801" title="null">操作系统识别工具 xprobe2 p0f 简介</a></li><li class=""><a class="underline" href="https://blog.csdn.net/freeking101/article/details/72962349" title="null">p0f - 被动探测操作系统工具</a></li><li class=""><a class="underline" href="https://www.ionos.com/digitalguide/online-marketing/web-analytics/browser-fingerprints-tracking-without-cookies/" title="null">Browser fingerprints: the basics and protection options</a></li></ol><h3 class="my-2 font-semibold ">案例</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://blog.csdn.net/NOSEC2019/article/details/90301394" title="null">2019 - Akamai 检测到随机化 TLS 指纹突破 WAF</a></li><li class=""><a class="underline" href="https://www.v2ex.com/t/792934" title="null">2021 - V 站网友遇到 Cloudflare 指纹检测</a></li><li class=""><a class="underline" href="https://netsecurity.51cto.com/art/202108/678610.htm" title="null">2021 - 为什么随机 IP、随机 UA 也逃不掉被反爬虫的命运</a></li><li class=""><a class="underline" href="https://pr.qiwihui.com/2021/04/21/qiwihui-pocket_readings-1078/" title="null">2021 - Python Requests 会被阿里云识别 TLS 特征</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/60407057/python-requests-being-fingerprinted" title="null">2020 - Requests being fingerprinted</a></li><li class=""><a class="underline" href="https://security.stackexchange.com/questions/236947/how-to-spoof-ja3-signature" title="undefined">https://security.stackexchange.com/questions/236947/how-to-spoof-ja3-signature</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/40373115/how-to-select-specific-the-cipher-while-sending-request-via-python-request-modul" title="null">如何更改 Requests 的 TLS 选项</a></li><li class=""><a class="underline" href="https://www.reddit.com/r/webscraping/comments/ge0dgk/ssltls_fingerprinting/" title="null">2019 - Reddit discussion on SSL/TLS Fingerprinting</a></li><li class=""><a class="underline" href="https://forum.openwrt.org/t/fingerprint-package/29824/18" title="null">Adwords 会检查 TCP 指纹</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/30787664/can-servers-use-http-headers-order-to-catch-a-browser-signature" title="undefined">https://stackoverflow.com/questions/30787664/can-servers-use-http-headers-order-to-catch-a-browser-signature</a></li><li class=""><a class="underline" href="https://github.com/v2ray/discussion/issues/704" title="null">TCP 指纹甚至可以用来识别 V2R</a></li><li class=""><a class="underline" href="https://ares-x.com/2021/04/18/SSL-指纹识别和绕过/" title="null">SSL 指纹识别和绕过</a></li></ol><h3 class="my-2 font-semibold ">工具</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://browserleaks.com/ssl" title="null">查看自己的浏览器指纹</a></li><li class=""><a class="underline" href="https://ja3er.com/downloads.html" title="null">JA3 指纹数据库</a></li><li class=""><a class="underline" href="https://browserleaks.com/ip" title="null">查看自己 IP 泄露的信息，包括 TCP 指纹</a></li><li class=""><a class="underline" href="https://amiunique.org/fp" title="null">查看浏览器指纹</a></li><li class=""><a class="underline" href="https://tlsfingerprint.io" title="null">utls</a></li><li class=""><a class="underline" href="https://bot.incolumitas.com" title="null">查看各种指纹</a></li><li class=""><a class="underline" href="http://dnscookie.com" title="null">DNS Cookie</a></li></ol><h3 class="my-2 font-semibold ">原始研究</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://devcentral.f5.com/s/articles/tls-fingerprinting-a-method-for-identifying-a-tls-client-without-decrypting-24598" title="null">2016 - TLS Fingerprinting - a method for identifying a TLS client without decrypting</a></li><li class=""><a class="underline" href="https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967" title="null">JA3 的原始文章</a></li><li class=""><a class="underline" href="https://github.com/salesforce/ja3" title="null">JA3 GitHub</a></li><li class=""><a class="underline" href="https://lcamtuf.coredump.cx/p0f3/" title="null">p0f3 - OS fingerprinting</a></li><li class=""><a class="underline" href="http://www.goonls.com/?p=1848" title="null">p0f3 应用</a></li><li class=""><a class="underline" href="https://en.wikipedia.org/wiki/TCP/IP_stack_fingerprinting" title="null">TCP 指纹</a></li><li class=""><a class="underline" href="https://en.wikipedia.org/wiki/Device_fingerprint" title="undefined">https://en.wikipedia.org/wiki/Device_fingerprint</a></li><li class=""><a class="underline" href="https://sansec.io/research/http-header-order-is-important" title="undefined">https://sansec.io/research/http-header-order-is-important</a></li><li class=""><a class="underline" href="https://incolumitas.com/pages/BotOrNot/" title="null">一篇非常全的指纹综述</a></li></ol><h3 class="my-2 font-semibold ">伪装</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://www.defensive-security.com/blog/hiding-behind-ja3-hash" title="null">Golang 伪装 JA3 指纹的例子</a></li><li class=""><a class="underline" href="https://medium.com/cu-cyber/impersonating-ja3-fingerprints-b9f555880e42" title="null">又一篇伪装 JA3 指纹的例子</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/64967706/python-requests-https-code-403-without-but-code-200-when-using-burpsuite" title="null">在 Requests 中伪造 TLS 的一个例子</a></li><li class=""><a class="underline" href="https://medium.com/cu-cyber/impersonating-ja3-fingerprints-b9f555880e42" title="null">Go ja3transport</a></li><li class=""><a class="underline" href="https://github.com/encode/httpx/issues/1147" title="null">httpx header order</a></li><li class=""><a class="underline" href="https://github.com/golang/go/issues/24375" title="null">golang header order</a></li><li class=""><a class="underline" href="https://jychp.medium.com/how-to-bypass-cloudflare-bot-protection-1f2c6c0c36fb" title="null">用 Cloudflare worker 突破 Cloudflare 反爬</a></li></ol><h3 class="my-2 font-semibold ">应用</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://datadome.co/bot-detection/client-side-detection-is-essential-for-bot-protection/" title="null">专门做客户端指纹的公司</a></li><li class=""><a class="underline" href="https://github.com/cloudflare/mitmengine" title="null">Cloudflare MITMEngine - Malcolm 背后算法</a></li><li class=""><a class="underline" href="https://malcolm.cloudflare.com/" title="null">Cloudflare malcolm</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/16881373/android-http-request-blocked-by-incapsula" title="null">Incapsula WAF 使用 http header 顺序作为指纹</a></li></ol><h3 class="my-2 font-semibold ">其他</h3><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://www.zhihu.com/question/51864646/answer/127840009" title="null">苏联笑话</a></li><li class=""><a class="underline" href="https://github.com/niespodd/browser-fingerprinting" title="undefined">https://github.com/niespodd/browser-fingerprinting</a></li><li class=""><a class="underline" href="https://github.com/JonasCz/How-To-Prevent-Scraping" title="undefined">https://github.com/JonasCz/How-To-Prevent-Scraping</a></li><li class=""><a class="underline" href="https://www.scrapingbee.com/blog/web-scraping-without-getting-blocked/" title="undefined">https://www.scrapingbee.com/blog/web-scraping-without-getting-blocked/</a></li><li class=""><a class="underline" href="https://incolumitas.com/2021/03/13/tcp-ip-fingerprinting-for-vpn-and-proxy-detection/" title="undefined">https://incolumitas.com/2021/03/13/tcp-ip-fingerprinting-for-vpn-and-proxy-detection/</a></li><li class=""><a class="underline" href="https://incolumitas.com/2021/05/20/avoid-puppeteer-and-playwright-for-scraping/" title="undefined">https://incolumitas.com/2021/05/20/avoid-puppeteer-and-playwright-for-scraping/</a></li><li class=""><a class="underline" href="https://fingerprintjs.com/blog/disabling-javascript-wont-stop-fingerprinting/" title="undefined">https://fingerprintjs.com/blog/disabling-javascript-wont-stop-fingerprinting/</a></li><li class=""><a class="underline" href="https://news.ycombinator.com/item?id=30237846" title="undefined">https://news.ycombinator.com/item?id=30237846</a></li><li class=""><a class="underline" href="https://news.ycombinator.com/item?id=29060272" title="undefined">https://news.ycombinator.com/item?id=29060272</a></li><li class=""><a class="underline" href="https://news.ycombinator.com/item?id=29117022" title="undefined">https://news.ycombinator.com/item?id=29117022</a></li><li class=""><a class="underline" href="https://news.ycombinator.com/item?id=28827509" title="undefined">https://news.ycombinator.com/item?id=28827509</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[useReducer + useContext = (Better) Redux]]></title>
            <link>https://yifei.me/note/3053</link>
            <guid>https://yifei.me/note/3053</guid>
            <pubDate>Mon, 31 Oct 2022 13:45:05 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">useState</code> 是 React 开发中最常用的一个钩子。但当程序稍微复杂一些的时候，只依赖 useState 就
显得有些力不从心了，这时候需要一个全局状态管理工具。</p><p class="my-4 font-light">在我刚接触 React 时，看到的教程一般都推荐 redux，在经历过无数次的尝试之后，我发现以我的
智商理解不了 redux 神奇的设计，也接受不了 redux 冗长的 boilerplate 代码。不过幸运的是，
有了 React 内置的 useReducer + useContext，完全可以不用 redux。</p><h2 class="my-2 font-semibold text-xl">useReducer</h2><p class="my-4 font-light">首先来开下 useReducer 的 API.</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">const</span> [<span class="hljs-keyword">state</span>, dispatch] = useReducer(reducerFn, initialState [, init]);</code></pre><p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">useReducer</code> 通常放在一组状态的根元素层级，如一个页面。dispatch 函数触发事件，reducer
函数用来处理事件，更新 state.</p><p class="my-4 font-light">当单独使用 reducer 的时候，其实就相当于一个高级的 useState，dispatch 写起来要比 setState
更清晰一些。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">const</span> initialState = {count: <span class="hljs-number">0</span>};

function reducer(<span class="hljs-keyword">state</span>, action) {
  switch (action.type) {
    case &#x27;increment&#x27;:
      return {...<span class="hljs-keyword">state</span>, count: <span class="hljs-keyword">state</span>.count + <span class="hljs-number">1</span>};
    case &#x27;decrement&#x27;:
      return {...<span class="hljs-keyword">state</span>, count: <span class="hljs-keyword">state</span>.count - <span class="hljs-number">1</span>};
    <span class="hljs-keyword">default</span>:
      throw new Error(`action type ${action.type} not found`);
  }
}

function Counter() {
  <span class="hljs-keyword">const</span> [<span class="hljs-keyword">state</span>, dispatch] = useReducer(reducer, initialState);
  return (
    <span class="hljs-variable">&lt;&gt;</span>
      Count: {<span class="hljs-keyword">state</span>.count}
      <span class="hljs-variable">&lt;button onClick={() =&gt;</span> dispatch({type: &#x27;decrement&#x27;})}&gt;-&lt;/button&gt;
      <span class="hljs-variable">&lt;button onClick={() =&gt;</span> dispatch({type: &#x27;increment&#x27;})}&gt;+&lt;/button&gt;
    &lt;/&gt;
  );
}</code></pre><p class="my-4 font-light"><small>useReducer 的 reducer 函数和 redux 不同，<strong>不需要 <code class="px-1 bg-gray-100 border-2 rounded">state=initialState</code></strong> 参数。
默认参数在调用 useReducer 的时候已经给出了。</small></p><h2 class="my-2 font-semibold text-xl">Context API</h2><p class="my-4 font-light">Context 用来向所有后代元素<strong>广播</strong>状态，可以跳跃组件树层级，而不需要层层传递。</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">首先需要通过 <code class="px-1 bg-gray-100 border-2 rounded">React.createContext</code> 定义一个高层次 Context</li><li class="">然后在最外层使用 <code class="px-1 bg-gray-100 border-2 rounded">&lt;MyContext.Provider /&gt;</code> 来包裹需要接受这个 context 的所有组件</li><li class="">在需要使用状态的元素中调用 <code class="px-1 bg-gray-100 border-2 rounded">&lt;MyContext.Consumer /&gt;</code>, 访问 Context 中的值</li></ol><p class="my-4 font-light">例子：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">const</span> <span class="hljs-title class_">AppContext</span> = <span class="hljs-title function_">createContext</span>()

<span class="hljs-keyword">function</span> <span class="hljs-title function_">App</span>(<span class="hljs-params">{children}</span>) {
  <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">AppContext.Provider</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{42}</span>&gt;</span>
    {children}
  <span class="hljs-tag">&lt;/<span class="hljs-name">AppContext.Provider</span>&gt;</span></span>
}

<span class="hljs-keyword">function</span> <span class="hljs-title function_">Page</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">AppContext.Consumer</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>{value}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">AppContext.Consumer</span>&gt;</span></span>
}</code></pre><h2 class="my-2 font-semibold text-xl">useContext</h2><p class="my-4 font-light">除了使用使用 <code class="px-1 bg-gray-100 border-2 rounded">&lt;Consumer/&gt;</code> 以外，还可以使用 useContext 钩子来读取 Context 中的值。这样就
不用额外再嵌套一层 Consumer 了。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">function</span> <span class="hljs-title function_">Page</span>(<span class="hljs-params"></span>) {
  <span class="hljs-comment">// 注意这里的参数是 Context</span>
  <span class="hljs-keyword">const</span> value = <span class="hljs-title function_">useContext</span>(<span class="hljs-title class_">AppContext</span>);
  <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>{value}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>;
}</code></pre><h2 class="my-2 font-semibold text-xl">组合起来</h2><p class="my-4 font-light">用 Context.Provider 把 state 和 dispatch 这两个变量广播给所有元素，这样每个组件都可以使用
<code class="px-1 bg-gray-100 border-2 rounded">useContext(Context)</code> 访问到 state 和 dispatch 和两个函数，从而可以使用或者更新全局状态。
也就实现了 redux 的核心功能。</p><p class="my-4 font-light">首先定义 store.js</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>import React, { useContext, useReducer } <span class="hljs-keyword">from</span> &#x27;react&#x27;;

// 初始状态
<span class="hljs-keyword">const</span> initialState = {
  <span class="hljs-keyword">user</span>: { name: <span class="hljs-string">&quot;&quot;</span>, },
  sidebar: { showToolbox: false, }
};

// 用来组合不同的 reducer
function combineReducers(reducers) {
  return function(<span class="hljs-keyword">state</span>, action) {
    <span class="hljs-keyword">const</span> newState = {};
    <span class="hljs-keyword">for</span> (let key <span class="hljs-keyword">in</span> reducers)
      newState[key] = reducers[key](<span class="hljs-keyword">state</span>[key], action);
    return newState;
  }
}

// 处理用户状态的 reducer
function <span class="hljs-keyword">user</span>Reducer(<span class="hljs-keyword">state</span>, action) {
  switch (action.type) {
    case <span class="hljs-string">&quot;user.updateName&quot;</span>:
      return { ...<span class="hljs-keyword">state</span>, name: action.data.name }
    <span class="hljs-keyword">default</span>:
      return <span class="hljs-keyword">state</span>;
  }
}

// 处理 sidebar 的 reducer
function sidebarReducer(<span class="hljs-keyword">state</span>, action) {
  switch (action.type) {
    case <span class="hljs-string">&quot;sidebar.toggleToolbox&quot;</span>:
      return { ...<span class="hljs-keyword">state</span>, showToolbox: !<span class="hljs-keyword">state</span>.showToolbox }
    <span class="hljs-keyword">default</span>:
      return <span class="hljs-keyword">state</span>;
  }
}

// 组合起来
<span class="hljs-keyword">const</span> reducer = combineReducers({
  <span class="hljs-keyword">user</span>: <span class="hljs-keyword">user</span>Reducer,
  ui: uiReducer,
})

<span class="hljs-keyword">const</span> Context = createContext(initialState)

export function Store({ children }) {
  // useReducer 实际上只在这里调用了一次
  <span class="hljs-keyword">const</span> [<span class="hljs-keyword">state</span>, dispatch] = useReducer(reducer, initialState);
  // 把 <span class="hljs-keyword">state</span> 和 dispatch 传递给所有元素
  return <span class="hljs-variable">&lt;Context.Provider value={[state, dispatch]}&gt;</span> {children} &lt;/Context.Provider&gt;
}

// 自定义钩子 useStore 调用 useContext，读取 <span class="hljs-keyword">state</span> 和 dispatch
export function useStore() { return useContext(Context); }</code></pre><p class="my-4 font-light">在 app.js 或者 index.js 中用 Context.Provider 包裹所有元素。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">import</span> {<span class="hljs-title class_">Store</span>, useStore} <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;./store.js&quot;</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">App</span>(<span class="hljs-params">{children}</span>) {
    <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">Store</span>&gt;</span>{children}<span class="hljs-tag">&lt;/<span class="hljs-name">Store</span>&gt;</span></span>
}</code></pre><p class="my-4 font-light">在组件中就可以使用 state 和 dispatch 和其他组件通信了。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment">// page.js</span>
<span class="hljs-keyword">import</span> {useStore} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./store&#x27;</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">Page</span>(<span class="hljs-params"></span>) {
    <span class="hljs-keyword">const</span> [state, dispatch] = <span class="hljs-title function_">useStore</span>()
    <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Hello, {state.user.name}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">SomeComponent</span> /&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
}

<span class="hljs-comment">// some-component.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">SomeComponent</span>(<span class="hljs-params"></span>) {
    <span class="hljs-keyword">const</span> [state, dispatch] = <span class="hljs-title function_">useStore</span>()
    <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">onClick</span>=<span class="hljs-string">{dispatch({type:</span> &quot;<span class="hljs-attr">user.updateName</span>&quot;, <span class="hljs-attr">data:</span> {<span class="hljs-attr">name:</span> &quot;<span class="hljs-attr">Sheldon</span>&quot;})}
        &gt;</span>Login<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
}</code></pre><h2 class="my-2 font-semibold text-xl">怎么请求数据？redux-thunk 呢？</h2><p class="my-4 font-light">看到这里你可能会问，useReducer 看起来不错，那怎么实现 redux-thunk 这种数据请求功能呢？</p><p class="my-4 font-light">我的回答可能有些争议——不要实现 redux-thunk 的功能，用 swr 或者 ReactQuery 来请求数据。</p><p class="my-4 font-light">UI 状态和数据状态是两个东西，然而人们总是把这两个东西混淆在一起。Redux 等工具实际上是
一个 UI 状态工具，但是人们总用它（通过 thunk) 来请求数据，这是错误的。</p><p class="my-4 font-light">在 Redux 等工具的 readme 中一般都是提供的 Counter 或者 TODO list 这个 demo, 这是非常误导
人的，因为这两个 demo 只是本地的 UI 状态管理。到了实际使用的过程中，大多数状态是数据状态，
需要使用 thunk 等 middleware 来获取后端数据并管理，非常丑陋。</p><p class="my-4 font-light">UI 状态指的是前端的一些状态，比如说是否使用暗黑模式，某个状态栏是否显示等等，和后端的
数据库无关。数据状态指的是从后端数据库中加载的一些数据，比如说用户名，当前的文章，评论
等等。如果前端进行了更新，也需要写回到后端数据库中。</p><ul class="my-4 ml-4 font-light list-disc"><li class="">对于 UI 状态管理来说，redux, mobx, 甚至包括 useReducer 等工具都是非常合适的，但是他们
  真的不是数据状态管理工具。</li><li class="">对于服务端数据的获取和缓存，需要使用单独的网络请求工具，而不是所谓的状态管理工具，
  useSWR 和 ReactQuery 是非常适合的工具。</li></ul><p class="my-4 font-light">按照前面的划分，实际上 SWR 也算一个（隐式）状态管理工具，它相当于使用了 API 的路径作为了
key, 然后把整个状态存储到了一个类似 kv 字典的结构当中。</p><p class="my-4 font-light">一旦把 UI 状态和（后端）数据状态这两个概念分清了，那么解决起来就一下子开朗了。我们可以
使用 useReducer 管理前端程序自身的状态，而获取数据都通过 SWR 来实现。</p><p class="my-4 font-light">关于 swr，可以参考我的另一篇文章：<a class="underline" href="https://yifei.me/note/2248" title="null">SWR 才是真正的数据状态管理工具</a></p>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">useState</code> 是 React 开发中最常用的一个钩子。但当程序稍微复杂一些的时候，只依赖 useState 就
显得有些力不从心了，这时候需要一个全局状态管理工具。</p><p class="my-4 font-light">在我刚接触 React 时，看到的教程一般都推荐 redux，在经历过无数次的尝试之后，我发现以我的
智商理解不了 redux 神奇的设计，也接受不了 redux 冗长的 boilerplate 代码。不过幸运的是，
有了 React 内置的 useReducer + useContext，完全可以不用 redux。</p><h2 class="my-2 font-semibold text-xl">useReducer</h2><p class="my-4 font-light">首先来开下 useReducer 的 API.</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">const</span> [<span class="hljs-keyword">state</span>, dispatch] = useReducer(reducerFn, initialState [, init]);</code></pre><p class="my-4 font-light"><code class="px-1 bg-gray-100 border-2 rounded">useReducer</code> 通常放在一组状态的根元素层级，如一个页面。dispatch 函数触发事件，reducer
函数用来处理事件，更新 state.</p><p class="my-4 font-light">当单独使用 reducer 的时候，其实就相当于一个高级的 useState，dispatch 写起来要比 setState
更清晰一些。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">const</span> initialState = {count: <span class="hljs-number">0</span>};

function reducer(<span class="hljs-keyword">state</span>, action) {
  switch (action.type) {
    case &#x27;increment&#x27;:
      return {...<span class="hljs-keyword">state</span>, count: <span class="hljs-keyword">state</span>.count + <span class="hljs-number">1</span>};
    case &#x27;decrement&#x27;:
      return {...<span class="hljs-keyword">state</span>, count: <span class="hljs-keyword">state</span>.count - <span class="hljs-number">1</span>};
    <span class="hljs-keyword">default</span>:
      throw new Error(`action type ${action.type} not found`);
  }
}

function Counter() {
  <span class="hljs-keyword">const</span> [<span class="hljs-keyword">state</span>, dispatch] = useReducer(reducer, initialState);
  return (
    <span class="hljs-variable">&lt;&gt;</span>
      Count: {<span class="hljs-keyword">state</span>.count}
      <span class="hljs-variable">&lt;button onClick={() =&gt;</span> dispatch({type: &#x27;decrement&#x27;})}&gt;-&lt;/button&gt;
      <span class="hljs-variable">&lt;button onClick={() =&gt;</span> dispatch({type: &#x27;increment&#x27;})}&gt;+&lt;/button&gt;
    &lt;/&gt;
  );
}</code></pre><p class="my-4 font-light"><small>useReducer 的 reducer 函数和 redux 不同，<strong>不需要 <code class="px-1 bg-gray-100 border-2 rounded">state=initialState</code></strong> 参数。
默认参数在调用 useReducer 的时候已经给出了。</small></p><h2 class="my-2 font-semibold text-xl">Context API</h2><p class="my-4 font-light">Context 用来向所有后代元素<strong>广播</strong>状态，可以跳跃组件树层级，而不需要层层传递。</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">首先需要通过 <code class="px-1 bg-gray-100 border-2 rounded">React.createContext</code> 定义一个高层次 Context</li><li class="">然后在最外层使用 <code class="px-1 bg-gray-100 border-2 rounded">&lt;MyContext.Provider /&gt;</code> 来包裹需要接受这个 context 的所有组件</li><li class="">在需要使用状态的元素中调用 <code class="px-1 bg-gray-100 border-2 rounded">&lt;MyContext.Consumer /&gt;</code>, 访问 Context 中的值</li></ol><p class="my-4 font-light">例子：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">const</span> <span class="hljs-title class_">AppContext</span> = <span class="hljs-title function_">createContext</span>()

<span class="hljs-keyword">function</span> <span class="hljs-title function_">App</span>(<span class="hljs-params">{children}</span>) {
  <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">AppContext.Provider</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{42}</span>&gt;</span>
    {children}
  <span class="hljs-tag">&lt;/<span class="hljs-name">AppContext.Provider</span>&gt;</span></span>
}

<span class="hljs-keyword">function</span> <span class="hljs-title function_">Page</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">AppContext.Consumer</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>{value}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">AppContext.Consumer</span>&gt;</span></span>
}</code></pre><h2 class="my-2 font-semibold text-xl">useContext</h2><p class="my-4 font-light">除了使用使用 <code class="px-1 bg-gray-100 border-2 rounded">&lt;Consumer/&gt;</code> 以外，还可以使用 useContext 钩子来读取 Context 中的值。这样就
不用额外再嵌套一层 Consumer 了。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">function</span> <span class="hljs-title function_">Page</span>(<span class="hljs-params"></span>) {
  <span class="hljs-comment">// 注意这里的参数是 Context</span>
  <span class="hljs-keyword">const</span> value = <span class="hljs-title function_">useContext</span>(<span class="hljs-title class_">AppContext</span>);
  <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>{value}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>;
}</code></pre><h2 class="my-2 font-semibold text-xl">组合起来</h2><p class="my-4 font-light">用 Context.Provider 把 state 和 dispatch 这两个变量广播给所有元素，这样每个组件都可以使用
<code class="px-1 bg-gray-100 border-2 rounded">useContext(Context)</code> 访问到 state 和 dispatch 和两个函数，从而可以使用或者更新全局状态。
也就实现了 redux 的核心功能。</p><p class="my-4 font-light">首先定义 store.js</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>import React, { useContext, useReducer } <span class="hljs-keyword">from</span> &#x27;react&#x27;;

// 初始状态
<span class="hljs-keyword">const</span> initialState = {
  <span class="hljs-keyword">user</span>: { name: <span class="hljs-string">&quot;&quot;</span>, },
  sidebar: { showToolbox: false, }
};

// 用来组合不同的 reducer
function combineReducers(reducers) {
  return function(<span class="hljs-keyword">state</span>, action) {
    <span class="hljs-keyword">const</span> newState = {};
    <span class="hljs-keyword">for</span> (let key <span class="hljs-keyword">in</span> reducers)
      newState[key] = reducers[key](<span class="hljs-keyword">state</span>[key], action);
    return newState;
  }
}

// 处理用户状态的 reducer
function <span class="hljs-keyword">user</span>Reducer(<span class="hljs-keyword">state</span>, action) {
  switch (action.type) {
    case <span class="hljs-string">&quot;user.updateName&quot;</span>:
      return { ...<span class="hljs-keyword">state</span>, name: action.data.name }
    <span class="hljs-keyword">default</span>:
      return <span class="hljs-keyword">state</span>;
  }
}

// 处理 sidebar 的 reducer
function sidebarReducer(<span class="hljs-keyword">state</span>, action) {
  switch (action.type) {
    case <span class="hljs-string">&quot;sidebar.toggleToolbox&quot;</span>:
      return { ...<span class="hljs-keyword">state</span>, showToolbox: !<span class="hljs-keyword">state</span>.showToolbox }
    <span class="hljs-keyword">default</span>:
      return <span class="hljs-keyword">state</span>;
  }
}

// 组合起来
<span class="hljs-keyword">const</span> reducer = combineReducers({
  <span class="hljs-keyword">user</span>: <span class="hljs-keyword">user</span>Reducer,
  ui: uiReducer,
})

<span class="hljs-keyword">const</span> Context = createContext(initialState)

export function Store({ children }) {
  // useReducer 实际上只在这里调用了一次
  <span class="hljs-keyword">const</span> [<span class="hljs-keyword">state</span>, dispatch] = useReducer(reducer, initialState);
  // 把 <span class="hljs-keyword">state</span> 和 dispatch 传递给所有元素
  return <span class="hljs-variable">&lt;Context.Provider value={[state, dispatch]}&gt;</span> {children} &lt;/Context.Provider&gt;
}

// 自定义钩子 useStore 调用 useContext，读取 <span class="hljs-keyword">state</span> 和 dispatch
export function useStore() { return useContext(Context); }</code></pre><p class="my-4 font-light">在 app.js 或者 index.js 中用 Context.Provider 包裹所有元素。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">import</span> {<span class="hljs-title class_">Store</span>, useStore} <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;./store.js&quot;</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">App</span>(<span class="hljs-params">{children}</span>) {
    <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">Store</span>&gt;</span>{children}<span class="hljs-tag">&lt;/<span class="hljs-name">Store</span>&gt;</span></span>
}</code></pre><p class="my-4 font-light">在组件中就可以使用 state 和 dispatch 和其他组件通信了。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment">// page.js</span>
<span class="hljs-keyword">import</span> {useStore} <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;./store&#x27;</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">Page</span>(<span class="hljs-params"></span>) {
    <span class="hljs-keyword">const</span> [state, dispatch] = <span class="hljs-title function_">useStore</span>()
    <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Hello, {state.user.name}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">SomeComponent</span> /&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
}

<span class="hljs-comment">// some-component.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">SomeComponent</span>(<span class="hljs-params"></span>) {
    <span class="hljs-keyword">const</span> [state, dispatch] = <span class="hljs-title function_">useStore</span>()
    <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag">&lt;&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">onClick</span>=<span class="hljs-string">{dispatch({type:</span> &quot;<span class="hljs-attr">user.updateName</span>&quot;, <span class="hljs-attr">data:</span> {<span class="hljs-attr">name:</span> &quot;<span class="hljs-attr">Sheldon</span>&quot;})}
        &gt;</span>Login<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
}</code></pre><h2 class="my-2 font-semibold text-xl">怎么请求数据？redux-thunk 呢？</h2><p class="my-4 font-light">看到这里你可能会问，useReducer 看起来不错，那怎么实现 redux-thunk 这种数据请求功能呢？</p><p class="my-4 font-light">我的回答可能有些争议——不要实现 redux-thunk 的功能，用 swr 或者 ReactQuery 来请求数据。</p><p class="my-4 font-light">UI 状态和数据状态是两个东西，然而人们总是把这两个东西混淆在一起。Redux 等工具实际上是
一个 UI 状态工具，但是人们总用它（通过 thunk) 来请求数据，这是错误的。</p><p class="my-4 font-light">在 Redux 等工具的 readme 中一般都是提供的 Counter 或者 TODO list 这个 demo, 这是非常误导
人的，因为这两个 demo 只是本地的 UI 状态管理。到了实际使用的过程中，大多数状态是数据状态，
需要使用 thunk 等 middleware 来获取后端数据并管理，非常丑陋。</p><p class="my-4 font-light">UI 状态指的是前端的一些状态，比如说是否使用暗黑模式，某个状态栏是否显示等等，和后端的
数据库无关。数据状态指的是从后端数据库中加载的一些数据，比如说用户名，当前的文章，评论
等等。如果前端进行了更新，也需要写回到后端数据库中。</p><ul class="my-4 ml-4 font-light list-disc"><li class="">对于 UI 状态管理来说，redux, mobx, 甚至包括 useReducer 等工具都是非常合适的，但是他们
  真的不是数据状态管理工具。</li><li class="">对于服务端数据的获取和缓存，需要使用单独的网络请求工具，而不是所谓的状态管理工具，
  useSWR 和 ReactQuery 是非常适合的工具。</li></ul><p class="my-4 font-light">按照前面的划分，实际上 SWR 也算一个（隐式）状态管理工具，它相当于使用了 API 的路径作为了
key, 然后把整个状态存储到了一个类似 kv 字典的结构当中。</p><p class="my-4 font-light">一旦把 UI 状态和（后端）数据状态这两个概念分清了，那么解决起来就一下子开朗了。我们可以
使用 useReducer 管理前端程序自身的状态，而获取数据都通过 SWR 来实现。</p><p class="my-4 font-light">关于 swr，可以参考我的另一篇文章：<a class="underline" href="https://yifei.me/note/2248" title="null">SWR 才是真正的数据状态管理工具</a></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[单元测试的笔记和心得体会]]></title>
            <link>https://yifei.me/note/2899</link>
            <guid>https://yifei.me/note/2899</guid>
            <pubDate>Fri, 07 Oct 2022 16:01:40 GMT</pubDate>
            <description><![CDATA[<h2 class="my-2 font-semibold text-xl">编写优质测试的前提</h2><ol class="my-4 ml-4 font-light list-decimal"><li class="">明确最小单元，以及单元的功能点都有哪些。如果在写代码之前都没有明确功能有哪些，或者编写
 功能已经跑偏了，那么测试究竟测些什么呢？</li><li class="">确定方案能够解决问题，然后再写测试，否则是徒劳的。</li></ol><h2 class="my-2 font-semibold text-xl">编写测试的基本原则</h2><ul class="my-4 ml-4 font-light list-disc"><li class="">每一个测试单元必须完全独立。每一个必须能够独立运行，以及在其他的测试组中组合运行，不管
  他们的顺序如何。加载和清空数据应该使用 <code class="px-1 bg-gray-100 border-2 rounded">setup()</code> 和 <code class="px-1 bg-gray-100 border-2 rounded">teardown()</code> 方法。</li><li class=""><strong>Sans-IO</strong>。也就是把逻辑和 IO 分开来，这样在测试的时候方便指定输入，以及捕获输出。</li><li class="">尽量让测试跑的快一点。如果一个测试在几毫秒之内跑不完的话，开发就会慢下来，以至于没有人
  再去跑这些测试了。如果实在有很耗费时间的测试，把他们单独放在一起定期执行。</li><li class="">使用有描述性的长名字。实际代码中你可能使用 <code class="px-1 bg-gray-100 border-2 rounded">square()</code> 这样的名字，但是在测试用你要用
  <code class="px-1 bg-gray-100 border-2 rounded">test_square_of_number_2</code> 这样的名字。</li><li class="">如果你在开发某样东西的过程中被打断，可以写一个测试，这样当你回过头来的时候还能很快想
  起来需要做什么。</li><li class="">测试代码的另一个用途是作为新手的介绍。让别人来看你的代码的时候，看看测试就知道代码是干
  什么的了。</li></ul><h3 class="my-2 font-semibold ">如何测试包含 IO 的函数</h3><ol class="my-4 ml-4 font-light list-decimal"><li class="">使用依赖注入；</li><li class="">把 IO 操作放在单独的地方，在测试的时候 mock 这个类或者方法；</li><li class="">在 pytest 中还可以使用 fixture 提供依赖；</li><li class="">搭建一个测试用的数据库等服务器。</li></ol><p class="my-4 font-light">IO 依赖主要包括依赖文件和外部数据库。对于依赖文件名作为参数的函数，甚至可以认为是一个非常
差的实践。而且根据单一职责原则，一个方法也不应该做两件事，要么做计算，要么做 IO, 而接受
文件名作为参数就隐含了既要负责打开文件，又要负责处理文件中的数据。</p><p class="my-4 font-light">但是不用文件名的话，有时候对于用户来说又不是很方便。建议把分成两个函数，一个只做计算，
另一个既打开文件，又做计算。</p><p class="my-4 font-light">虽然使用 mock 的方式可能会提高速度或者更方便一些，但是这样的话又可能和实际生产环境的差异
过大，而且 mock 库也不是那么好找的。</p><h2 class="my-2 font-semibold text-xl">编写测试的思路</h2><p class="my-4 font-light">从软件可靠性的角度，测试当然是越完备越好，但是不是每一个软件都是核弹控制器，还是要根据
实际情况折中一下。</p><h3 class="my-2 font-semibold ">追求功能测试</h3><p class="my-4 font-light">只需要按照功能点，把正常和常见的异常情况测试一下就好了。重点还是要先明确功能点有哪些。</p><h3 class="my-2 font-semibold ">追求 100% 覆盖度</h3><p class="my-4 font-light">按照代码逻辑分支测试，把代码的每一个分支的</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">入口参数是什么</li><li class="">出口参数是什么</li><li class="">副作用是什么</li><li class="">产生的异常是什么</li></ol><p class="my-4 font-light">都测试到。</p><h2 class="my-2 font-semibold text-xl">测试的粒度</h2><p class="my-4 font-light">在每个 endpoint 上做集成测试。</p><p class="my-4 font-light">不要过度测试。给门添加第二把锁会让门更安全一点，但是添加一百道锁并没有任何用，和两把锁是
一样的。加一百把锁的话，抢劫犯可能直接破门而入或者选择走窗户了。但是一百把锁反倒会让主人
进入更加复杂。单元测试也是同样的道理，不可或缺，但是要适可而止。</p><p class="my-4 font-light">所以，总的来说，应该添加单元测试，但是要确保：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">单元测试提供了价值，</li><li class="">单元测试比集成测试更适合这件事。</li></ol><h2 class="my-2 font-semibold text-xl">References</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://tyrrrz.me/blog/unit-testing-is-overrated" title="undefined">https://tyrrrz.me/blog/unit-testing-is-overrated</a></li></ol>]]></description>
            <content:encoded><![CDATA[<h2 class="my-2 font-semibold text-xl">编写优质测试的前提</h2><ol class="my-4 ml-4 font-light list-decimal"><li class="">明确最小单元，以及单元的功能点都有哪些。如果在写代码之前都没有明确功能有哪些，或者编写
 功能已经跑偏了，那么测试究竟测些什么呢？</li><li class="">确定方案能够解决问题，然后再写测试，否则是徒劳的。</li></ol><h2 class="my-2 font-semibold text-xl">编写测试的基本原则</h2><ul class="my-4 ml-4 font-light list-disc"><li class="">每一个测试单元必须完全独立。每一个必须能够独立运行，以及在其他的测试组中组合运行，不管
  他们的顺序如何。加载和清空数据应该使用 <code class="px-1 bg-gray-100 border-2 rounded">setup()</code> 和 <code class="px-1 bg-gray-100 border-2 rounded">teardown()</code> 方法。</li><li class=""><strong>Sans-IO</strong>。也就是把逻辑和 IO 分开来，这样在测试的时候方便指定输入，以及捕获输出。</li><li class="">尽量让测试跑的快一点。如果一个测试在几毫秒之内跑不完的话，开发就会慢下来，以至于没有人
  再去跑这些测试了。如果实在有很耗费时间的测试，把他们单独放在一起定期执行。</li><li class="">使用有描述性的长名字。实际代码中你可能使用 <code class="px-1 bg-gray-100 border-2 rounded">square()</code> 这样的名字，但是在测试用你要用
  <code class="px-1 bg-gray-100 border-2 rounded">test_square_of_number_2</code> 这样的名字。</li><li class="">如果你在开发某样东西的过程中被打断，可以写一个测试，这样当你回过头来的时候还能很快想
  起来需要做什么。</li><li class="">测试代码的另一个用途是作为新手的介绍。让别人来看你的代码的时候，看看测试就知道代码是干
  什么的了。</li></ul><h3 class="my-2 font-semibold ">如何测试包含 IO 的函数</h3><ol class="my-4 ml-4 font-light list-decimal"><li class="">使用依赖注入；</li><li class="">把 IO 操作放在单独的地方，在测试的时候 mock 这个类或者方法；</li><li class="">在 pytest 中还可以使用 fixture 提供依赖；</li><li class="">搭建一个测试用的数据库等服务器。</li></ol><p class="my-4 font-light">IO 依赖主要包括依赖文件和外部数据库。对于依赖文件名作为参数的函数，甚至可以认为是一个非常
差的实践。而且根据单一职责原则，一个方法也不应该做两件事，要么做计算，要么做 IO, 而接受
文件名作为参数就隐含了既要负责打开文件，又要负责处理文件中的数据。</p><p class="my-4 font-light">但是不用文件名的话，有时候对于用户来说又不是很方便。建议把分成两个函数，一个只做计算，
另一个既打开文件，又做计算。</p><p class="my-4 font-light">虽然使用 mock 的方式可能会提高速度或者更方便一些，但是这样的话又可能和实际生产环境的差异
过大，而且 mock 库也不是那么好找的。</p><h2 class="my-2 font-semibold text-xl">编写测试的思路</h2><p class="my-4 font-light">从软件可靠性的角度，测试当然是越完备越好，但是不是每一个软件都是核弹控制器，还是要根据
实际情况折中一下。</p><h3 class="my-2 font-semibold ">追求功能测试</h3><p class="my-4 font-light">只需要按照功能点，把正常和常见的异常情况测试一下就好了。重点还是要先明确功能点有哪些。</p><h3 class="my-2 font-semibold ">追求 100% 覆盖度</h3><p class="my-4 font-light">按照代码逻辑分支测试，把代码的每一个分支的</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">入口参数是什么</li><li class="">出口参数是什么</li><li class="">副作用是什么</li><li class="">产生的异常是什么</li></ol><p class="my-4 font-light">都测试到。</p><h2 class="my-2 font-semibold text-xl">测试的粒度</h2><p class="my-4 font-light">在每个 endpoint 上做集成测试。</p><p class="my-4 font-light">不要过度测试。给门添加第二把锁会让门更安全一点，但是添加一百道锁并没有任何用，和两把锁是
一样的。加一百把锁的话，抢劫犯可能直接破门而入或者选择走窗户了。但是一百把锁反倒会让主人
进入更加复杂。单元测试也是同样的道理，不可或缺，但是要适可而止。</p><p class="my-4 font-light">所以，总的来说，应该添加单元测试，但是要确保：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">单元测试提供了价值，</li><li class="">单元测试比集成测试更适合这件事。</li></ol><h2 class="my-2 font-semibold text-xl">References</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://tyrrrz.me/blog/unit-testing-is-overrated" title="undefined">https://tyrrrz.me/blog/unit-testing-is-overrated</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[kubectl 备忘录]]></title>
            <link>https://yifei.me/note/1105</link>
            <guid>https://yifei.me/note/1105</guid>
            <pubDate>Sat, 04 Jun 2022 08:20:08 GMT</pubDate>
            <description><![CDATA[<h2 class="my-2 font-semibold text-xl">查看集群资源</h2><p class="my-4 font-light">获取各种资源列表：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># -o 指定范围格式，`-o wide` 用来显示更详细信息</span>
kubectl get node<span class="hljs-regexp">/pod/</span>deploy<span class="hljs-regexp">/node/</span>svc -o wide<span class="hljs-regexp">/yaml/</span>name</code></pre><p class="my-4 font-light">描述详细信息：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># -n 指定命名空间</span>
kubectl describe <span class="hljs-keyword">node</span><span class="hljs-title">/pod</span>/deploy/svc</code></pre><p class="my-4 font-light">其他一些操作</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 显示当前节点的一些信息</span>
kubectl top <span class="hljs-keyword">node</span><span class="hljs-title">/pod</span>
<span class="hljs-comment"># 本地 5000 端口转发到 pod 6000</span>
kubectl port-forward my-pod <span class="hljs-number">5000</span>:<span class="hljs-number">6000</span>
<span class="hljs-comment"># 编辑 kubernetes 的配置文件</span>
kubectl edit deployment/my-nginx</code></pre><h2 class="my-2 font-semibold text-xl">资源名称缩写</h2><table>
<thead>
<tr>
<th class="border border-solid px-2 py-1 font-semibold">Resource</th><th class="border border-solid px-2 py-1 font-semibold">Short Name</th></tr>
</thead>
<tbody><tr>
<td class="border border-solid px-2 py-1 ">deployments</td><td class="border border-solid px-2 py-1 ">deploy</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">pods</td><td class="border border-solid px-2 py-1 ">po</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">replicasets</td><td class="border border-solid px-2 py-1 ">rs</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">cronjobs</td><td class="border border-solid px-2 py-1 ">cj</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">endpoints</td><td class="border border-solid px-2 py-1 ">ep</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">ingresses</td><td class="border border-solid px-2 py-1 ">ing</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">certificiaterequests</td><td class="border border-solid px-2 py-1 ">cr, crs</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">certificates</td><td class="border border-solid px-2 py-1 ">cert, certs</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">componentstatuses</td><td class="border border-solid px-2 py-1 ">cs</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">configmaps</td><td class="border border-solid px-2 py-1 ">cm</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">services</td><td class="border border-solid px-2 py-1 ">svc</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">namespaces</td><td class="border border-solid px-2 py-1 ">ns</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">nodes</td><td class="border border-solid px-2 py-1 ">no</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">statefulsets</td><td class="border border-solid px-2 py-1 ">sts</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">daemonsets</td><td class="border border-solid px-2 py-1 ">ds</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">resourcequotas</td><td class="border border-solid px-2 py-1 ">quota</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">events</td><td class="border border-solid px-2 py-1 ">ev</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">limitranges</td><td class="border border-solid px-2 py-1 ">limits</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">replicationcontrollers</td><td class="border border-solid px-2 py-1 ">rc</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">serviceaccounts</td><td class="border border-solid px-2 py-1 ">sa</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">customresourcedefinitions</td><td class="border border-solid px-2 py-1 ">crd, crds</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">replicasets</td><td class="border border-solid px-2 py-1 ">rs</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">horizontalpodautoscalers</td><td class="border border-solid px-2 py-1 ">hpa</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">certificatesigningrequests</td><td class="border border-solid px-2 py-1 ">csr</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">networkpolicies</td><td class="border border-solid px-2 py-1 ">netpol</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">podsecuritypolicies</td><td class="border border-solid px-2 py-1 ">psp</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">scheduledscalers</td><td class="border border-solid px-2 py-1 ">ss</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">priorityclasses</td><td class="border border-solid px-2 py-1 ">pc</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">storageclasses</td><td class="border border-solid px-2 py-1 ">sc</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">persistentvolumeclaims</td><td class="border border-solid px-2 py-1 ">pvc</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">persistentvolumes</td><td class="border border-solid px-2 py-1 ">pv</td></tr>
</tbody></table>
<h2 class="my-2 font-semibold text-xl">kubectl create &amp; apply</h2><p class="my-4 font-light">用来创建或更新资源，使用 <code class="px-1 bg-gray-100 border-2 rounded">-f</code> 参数来制定配置文件。实际使用中，几乎不会用到 <code class="px-1 bg-gray-100 border-2 rounded">create</code>。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>kubectl <span class="hljs-keyword">apply</span> -f <span class="hljs-keyword">file</span>.yml</code></pre><h2 class="my-2 font-semibold text-xl">kubectl delete</h2><p class="my-4 font-light">用来删除节点上的 pod, deployment 等信息</p><h2 class="my-2 font-semibold text-xl">kubectl run</h2><p class="my-4 font-light">类似于 docker run, 但是由 kubernetes 接管，直接运行在集群上。比如运行 hello world</p><h2 class="my-2 font-semibold text-xl">kubectl logs</h2><p class="my-4 font-light">类似于 docker logs, 用来显示打印到 stdout 的日志</p><h2 class="my-2 font-semibold text-xl">复制文件</h2><p class="my-4 font-light">可以复制文件到 Pod 中。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-symbol">kubectl</span> <span class="hljs-meta">cp</span> -h</code></pre><h2 class="my-2 font-semibold text-xl">port-forward 临时访问集群中的服务</h2><p class="my-4 font-light">例如：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">k</span> port-forward svc/elasticsearch <span class="hljs-number">9200</span>:<span class="hljs-number">9200</span></code></pre><p class="my-4 font-light">把集群中 elasticsearch 服务映射到本地 9200 端口，非常方便测试。</p><h2 class="my-2 font-semibold text-xl">列出当前集群的 API version</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">kubectl</span> api-resources <span class="hljs-comment"># List the API resources that are available.</span>
kubectl api-versions  <span class="hljs-comment"># List the API versions that are available.</span>

for kind in `kubectl api-resources | tail +<span class="hljs-number">2</span> | awk <span class="hljs-string">&#x27;{ print <span class="hljs-variable">$1</span> }&#x27;</span>`; <span class="hljs-attribute">do</span>
    kubectl explain <span class="hljs-variable">$kind</span>;
<span class="hljs-attribute">done</span> | grep -e <span class="hljs-string">&quot;KIND:&quot;</span> -e <span class="hljs-string">&quot;VERSION:&quot;</span></code></pre><h2 class="my-2 font-semibold text-xl">重启一个服务</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>kubectl rollout <span class="hljs-keyword">restart</span> deployment &lt;<span class="hljs-type">name</span>&gt;</code></pre><h2 class="my-2 font-semibold text-xl">进入 pod 的 shell</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>kubectl exec -<span class="hljs-keyword">it</span> POD-NAME -n NAMESPACE <span class="hljs-comment">-- bash</span></code></pre><h2 class="my-2 font-semibold text-xl">代理</h2><p class="my-4 font-light">使用代理可能会带来一些问题，一定要把集群内的地址设置为不用代理：</p><p class="my-4 font-light"><a class="underline" href="https://github.com/jetstack/cert-manager/issues/2640" title="undefined">https://github.com/jetstack/cert-manager/issues/2640</a></p><h2 class="my-2 font-semibold text-xl">切换集群</h2><p class="my-4 font-light">kubernetes 支持在一个配置文件中存储多个集群的配置，并且可以使用 kubectl config 切换。</p><p class="my-4 font-light">如果多个集群分散在不同文件中，首先我们需要合并一下这些文件：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 把所有文件都加到 KUBECONFIG 路径中</span>
export KUBECONFIG=<span class="hljs-regexp">/path/</span>to<span class="hljs-regexp">/conf1:/</span>path<span class="hljs-regexp">/to/</span>conf2...
<span class="hljs-comment"># 合并</span>
kubectl config view --flatten &gt; <span class="hljs-variable">$HOME</span><span class="hljs-regexp">/.kube/</span>all-<span class="hljs-keyword">in</span>-one-kubeconfig.yaml
<span class="hljs-comment"># 验证下是否可用</span>
kubectl config get-contexts --kubeconfig=<span class="hljs-variable">$HOME</span><span class="hljs-regexp">/.kube/</span>all-<span class="hljs-keyword">in</span>-one-kubeconfig.yaml
<span class="hljs-comment"># 把合并后的文件放到默认位置</span>
mv <span class="hljs-variable">$HOME</span><span class="hljs-regexp">/.kube/</span>all-<span class="hljs-keyword">in</span>-one-kubeconfig.yaml <span class="hljs-variable">$HOME</span><span class="hljs-regexp">/.kube/</span>config
export KUBECONFIG=</code></pre><p class="my-4 font-light">列出当前的 contexts</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>$ <span class="hljs-string">k</span> <span class="hljs-string">config</span> <span class="hljs-built_in">get-contexts</span>

<span class="hljs-string">CURRENT</span>   <span class="hljs-string">NAME</span>              <span class="hljs-string">CLUSTER</span>           <span class="hljs-string">AUTHINFO</span>          <span class="hljs-string">NAMESPACE</span>
*         <span class="hljs-string">docker-desktop</span>    <span class="hljs-string">docker-desktop</span>    <span class="hljs-string">docker-desktop</span>
          <span class="hljs-string">rancher-desktop</span>   <span class="hljs-string">rancher-desktop</span>   <span class="hljs-string">rancher-desktop</span>
          <span class="hljs-string">us-east-1a</span>        <span class="hljs-string">us-east-1a</span>        <span class="hljs-string">us-east-1a</span></code></pre><p class="my-4 font-light">切换 context</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>$ k <span class="hljs-built_in">config</span> use-<span class="hljs-built_in">context</span> rancher-desktop
<span class="hljs-keyword">Switched </span>to <span class="hljs-built_in">context</span> <span class="hljs-string">&quot;rancher-desktop&quot;</span>.</code></pre><p class="my-4 font-light">删除 context</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>$ <span class="hljs-string">k</span> <span class="hljs-string">config</span> <span class="hljs-built_in">delete-context</span> <span class="hljs-string">rancher-desktop</span>
<span class="hljs-string">warning</span>: <span class="hljs-string">this</span> <span class="hljs-string">removed</span> <span class="hljs-string">your</span> <span class="hljs-string">active</span> <span class="hljs-string">context</span>, <span class="hljs-string">use</span> <span class="hljs-string">&quot;kubectl config use-context&quot;</span> <span class="hljs-string">to</span> <span class="hljs-string">select</span> <span class="hljs-string">a</span> <span class="hljs-string">different</span> <span class="hljs-string">one</span>
<span class="hljs-string">deleted</span> <span class="hljs-string">context</span> <span class="hljs-string">rancher-desktop</span> <span class="hljs-string">from</span> /<span class="hljs-string">Users</span>/<span class="hljs-string">yifei</span>/.<span class="hljs-string">kube</span>/<span class="hljs-string">config</span></code></pre><p class="my-4 font-light">重命名 context</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>k config <span class="hljs-keyword">rename</span>-context <span class="hljs-built_in">OLD</span> <span class="hljs-built_in">NEW</span></code></pre><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://akhilsharma.work/kubectl-get-resource-short-names/" title="undefined">https://akhilsharma.work/kubectl-get-resource-short-names/</a></li><li class=""><a class="underline" href="https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/" title="undefined">https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/</a></li><li class=""><a class="underline" href="https://blog.pipetail.io/posts/2020-05-04-most-common-mistakes-k8s/" title="undefined">https://blog.pipetail.io/posts/2020-05-04-most-common-mistakes-k8s/</a></li><li class=""><a class="underline" href="https://stackoverflow.com/a/52711746/1061155" title="undefined">https://stackoverflow.com/a/52711746/1061155</a></li></ol>]]></description>
            <content:encoded><![CDATA[<h2 class="my-2 font-semibold text-xl">查看集群资源</h2><p class="my-4 font-light">获取各种资源列表：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># -o 指定范围格式，`-o wide` 用来显示更详细信息</span>
kubectl get node<span class="hljs-regexp">/pod/</span>deploy<span class="hljs-regexp">/node/</span>svc -o wide<span class="hljs-regexp">/yaml/</span>name</code></pre><p class="my-4 font-light">描述详细信息：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># -n 指定命名空间</span>
kubectl describe <span class="hljs-keyword">node</span><span class="hljs-title">/pod</span>/deploy/svc</code></pre><p class="my-4 font-light">其他一些操作</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 显示当前节点的一些信息</span>
kubectl top <span class="hljs-keyword">node</span><span class="hljs-title">/pod</span>
<span class="hljs-comment"># 本地 5000 端口转发到 pod 6000</span>
kubectl port-forward my-pod <span class="hljs-number">5000</span>:<span class="hljs-number">6000</span>
<span class="hljs-comment"># 编辑 kubernetes 的配置文件</span>
kubectl edit deployment/my-nginx</code></pre><h2 class="my-2 font-semibold text-xl">资源名称缩写</h2><table>
<thead>
<tr>
<th class="border border-solid px-2 py-1 font-semibold">Resource</th><th class="border border-solid px-2 py-1 font-semibold">Short Name</th></tr>
</thead>
<tbody><tr>
<td class="border border-solid px-2 py-1 ">deployments</td><td class="border border-solid px-2 py-1 ">deploy</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">pods</td><td class="border border-solid px-2 py-1 ">po</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">replicasets</td><td class="border border-solid px-2 py-1 ">rs</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">cronjobs</td><td class="border border-solid px-2 py-1 ">cj</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">endpoints</td><td class="border border-solid px-2 py-1 ">ep</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">ingresses</td><td class="border border-solid px-2 py-1 ">ing</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">certificiaterequests</td><td class="border border-solid px-2 py-1 ">cr, crs</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">certificates</td><td class="border border-solid px-2 py-1 ">cert, certs</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">componentstatuses</td><td class="border border-solid px-2 py-1 ">cs</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">configmaps</td><td class="border border-solid px-2 py-1 ">cm</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">services</td><td class="border border-solid px-2 py-1 ">svc</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">namespaces</td><td class="border border-solid px-2 py-1 ">ns</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">nodes</td><td class="border border-solid px-2 py-1 ">no</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">statefulsets</td><td class="border border-solid px-2 py-1 ">sts</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">daemonsets</td><td class="border border-solid px-2 py-1 ">ds</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">resourcequotas</td><td class="border border-solid px-2 py-1 ">quota</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">events</td><td class="border border-solid px-2 py-1 ">ev</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">limitranges</td><td class="border border-solid px-2 py-1 ">limits</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">replicationcontrollers</td><td class="border border-solid px-2 py-1 ">rc</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">serviceaccounts</td><td class="border border-solid px-2 py-1 ">sa</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">customresourcedefinitions</td><td class="border border-solid px-2 py-1 ">crd, crds</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">replicasets</td><td class="border border-solid px-2 py-1 ">rs</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">horizontalpodautoscalers</td><td class="border border-solid px-2 py-1 ">hpa</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">certificatesigningrequests</td><td class="border border-solid px-2 py-1 ">csr</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">networkpolicies</td><td class="border border-solid px-2 py-1 ">netpol</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">podsecuritypolicies</td><td class="border border-solid px-2 py-1 ">psp</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">scheduledscalers</td><td class="border border-solid px-2 py-1 ">ss</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">priorityclasses</td><td class="border border-solid px-2 py-1 ">pc</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">storageclasses</td><td class="border border-solid px-2 py-1 ">sc</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">persistentvolumeclaims</td><td class="border border-solid px-2 py-1 ">pvc</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">persistentvolumes</td><td class="border border-solid px-2 py-1 ">pv</td></tr>
</tbody></table>
<h2 class="my-2 font-semibold text-xl">kubectl create &amp; apply</h2><p class="my-4 font-light">用来创建或更新资源，使用 <code class="px-1 bg-gray-100 border-2 rounded">-f</code> 参数来制定配置文件。实际使用中，几乎不会用到 <code class="px-1 bg-gray-100 border-2 rounded">create</code>。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>kubectl <span class="hljs-keyword">apply</span> -f <span class="hljs-keyword">file</span>.yml</code></pre><h2 class="my-2 font-semibold text-xl">kubectl delete</h2><p class="my-4 font-light">用来删除节点上的 pod, deployment 等信息</p><h2 class="my-2 font-semibold text-xl">kubectl run</h2><p class="my-4 font-light">类似于 docker run, 但是由 kubernetes 接管，直接运行在集群上。比如运行 hello world</p><h2 class="my-2 font-semibold text-xl">kubectl logs</h2><p class="my-4 font-light">类似于 docker logs, 用来显示打印到 stdout 的日志</p><h2 class="my-2 font-semibold text-xl">复制文件</h2><p class="my-4 font-light">可以复制文件到 Pod 中。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-symbol">kubectl</span> <span class="hljs-meta">cp</span> -h</code></pre><h2 class="my-2 font-semibold text-xl">port-forward 临时访问集群中的服务</h2><p class="my-4 font-light">例如：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">k</span> port-forward svc/elasticsearch <span class="hljs-number">9200</span>:<span class="hljs-number">9200</span></code></pre><p class="my-4 font-light">把集群中 elasticsearch 服务映射到本地 9200 端口，非常方便测试。</p><h2 class="my-2 font-semibold text-xl">列出当前集群的 API version</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attribute">kubectl</span> api-resources <span class="hljs-comment"># List the API resources that are available.</span>
kubectl api-versions  <span class="hljs-comment"># List the API versions that are available.</span>

for kind in `kubectl api-resources | tail +<span class="hljs-number">2</span> | awk <span class="hljs-string">&#x27;{ print <span class="hljs-variable">$1</span> }&#x27;</span>`; <span class="hljs-attribute">do</span>
    kubectl explain <span class="hljs-variable">$kind</span>;
<span class="hljs-attribute">done</span> | grep -e <span class="hljs-string">&quot;KIND:&quot;</span> -e <span class="hljs-string">&quot;VERSION:&quot;</span></code></pre><h2 class="my-2 font-semibold text-xl">重启一个服务</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>kubectl rollout <span class="hljs-keyword">restart</span> deployment &lt;<span class="hljs-type">name</span>&gt;</code></pre><h2 class="my-2 font-semibold text-xl">进入 pod 的 shell</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>kubectl exec -<span class="hljs-keyword">it</span> POD-NAME -n NAMESPACE <span class="hljs-comment">-- bash</span></code></pre><h2 class="my-2 font-semibold text-xl">代理</h2><p class="my-4 font-light">使用代理可能会带来一些问题，一定要把集群内的地址设置为不用代理：</p><p class="my-4 font-light"><a class="underline" href="https://github.com/jetstack/cert-manager/issues/2640" title="undefined">https://github.com/jetstack/cert-manager/issues/2640</a></p><h2 class="my-2 font-semibold text-xl">切换集群</h2><p class="my-4 font-light">kubernetes 支持在一个配置文件中存储多个集群的配置，并且可以使用 kubectl config 切换。</p><p class="my-4 font-light">如果多个集群分散在不同文件中，首先我们需要合并一下这些文件：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 把所有文件都加到 KUBECONFIG 路径中</span>
export KUBECONFIG=<span class="hljs-regexp">/path/</span>to<span class="hljs-regexp">/conf1:/</span>path<span class="hljs-regexp">/to/</span>conf2...
<span class="hljs-comment"># 合并</span>
kubectl config view --flatten &gt; <span class="hljs-variable">$HOME</span><span class="hljs-regexp">/.kube/</span>all-<span class="hljs-keyword">in</span>-one-kubeconfig.yaml
<span class="hljs-comment"># 验证下是否可用</span>
kubectl config get-contexts --kubeconfig=<span class="hljs-variable">$HOME</span><span class="hljs-regexp">/.kube/</span>all-<span class="hljs-keyword">in</span>-one-kubeconfig.yaml
<span class="hljs-comment"># 把合并后的文件放到默认位置</span>
mv <span class="hljs-variable">$HOME</span><span class="hljs-regexp">/.kube/</span>all-<span class="hljs-keyword">in</span>-one-kubeconfig.yaml <span class="hljs-variable">$HOME</span><span class="hljs-regexp">/.kube/</span>config
export KUBECONFIG=</code></pre><p class="my-4 font-light">列出当前的 contexts</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>$ <span class="hljs-string">k</span> <span class="hljs-string">config</span> <span class="hljs-built_in">get-contexts</span>

<span class="hljs-string">CURRENT</span>   <span class="hljs-string">NAME</span>              <span class="hljs-string">CLUSTER</span>           <span class="hljs-string">AUTHINFO</span>          <span class="hljs-string">NAMESPACE</span>
*         <span class="hljs-string">docker-desktop</span>    <span class="hljs-string">docker-desktop</span>    <span class="hljs-string">docker-desktop</span>
          <span class="hljs-string">rancher-desktop</span>   <span class="hljs-string">rancher-desktop</span>   <span class="hljs-string">rancher-desktop</span>
          <span class="hljs-string">us-east-1a</span>        <span class="hljs-string">us-east-1a</span>        <span class="hljs-string">us-east-1a</span></code></pre><p class="my-4 font-light">切换 context</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>$ k <span class="hljs-built_in">config</span> use-<span class="hljs-built_in">context</span> rancher-desktop
<span class="hljs-keyword">Switched </span>to <span class="hljs-built_in">context</span> <span class="hljs-string">&quot;rancher-desktop&quot;</span>.</code></pre><p class="my-4 font-light">删除 context</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>$ <span class="hljs-string">k</span> <span class="hljs-string">config</span> <span class="hljs-built_in">delete-context</span> <span class="hljs-string">rancher-desktop</span>
<span class="hljs-string">warning</span>: <span class="hljs-string">this</span> <span class="hljs-string">removed</span> <span class="hljs-string">your</span> <span class="hljs-string">active</span> <span class="hljs-string">context</span>, <span class="hljs-string">use</span> <span class="hljs-string">&quot;kubectl config use-context&quot;</span> <span class="hljs-string">to</span> <span class="hljs-string">select</span> <span class="hljs-string">a</span> <span class="hljs-string">different</span> <span class="hljs-string">one</span>
<span class="hljs-string">deleted</span> <span class="hljs-string">context</span> <span class="hljs-string">rancher-desktop</span> <span class="hljs-string">from</span> /<span class="hljs-string">Users</span>/<span class="hljs-string">yifei</span>/.<span class="hljs-string">kube</span>/<span class="hljs-string">config</span></code></pre><p class="my-4 font-light">重命名 context</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>k config <span class="hljs-keyword">rename</span>-context <span class="hljs-built_in">OLD</span> <span class="hljs-built_in">NEW</span></code></pre><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://akhilsharma.work/kubectl-get-resource-short-names/" title="undefined">https://akhilsharma.work/kubectl-get-resource-short-names/</a></li><li class=""><a class="underline" href="https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/" title="undefined">https://kubernetes.io/docs/tasks/access-application-cluster/port-forward-access-application-cluster/</a></li><li class=""><a class="underline" href="https://blog.pipetail.io/posts/2020-05-04-most-common-mistakes-k8s/" title="undefined">https://blog.pipetail.io/posts/2020-05-04-most-common-mistakes-k8s/</a></li><li class=""><a class="underline" href="https://stackoverflow.com/a/52711746/1061155" title="undefined">https://stackoverflow.com/a/52711746/1061155</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[和风车决斗的堂吉诃德]]></title>
            <link>https://yifei.me/note/2951</link>
            <guid>https://yifei.me/note/2951</guid>
            <pubDate>Mon, 23 May 2022 15:41:15 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">今天不谈论代码了，写代码也救不了中国人。</p><p class="my-4 font-light">我们知道一个病毒的传播能力可以用 R0 来表示，即每个患者传染的新患者的数量。我们又知道，社会上除了占大多数的遵纪守法好公民，还有目无法纪的奸商、涣散烂漫的小市民和一小撮玩忽职守的领导干部。因此，当局制定的政策的传达效力是有限的，我们只能指望其中的好公民去 100% 的执行，而剩下的几类人不仅不能执行，甚至会带来负效果。设当局政策的有效力为 E，理想情况下 E = 100%。</p><p class="my-4 font-light">当病毒在自然状态下自由传播的时候，它的传播速率就是 R0，只要 R0 大于 1，那就肯定会指数传播，无非是快慢问题。当有人为干预的时候，传播速率就变成了 <code class="px-1 bg-gray-100 border-2 rounded">R0 * (1 - E)</code>，只要我们使这个值小于 1，就能控制病毒的扩散。</p><p class="my-4 font-light">回到现在的疫情，delta 变种 R0 是 5，这时候只要在发生疫情的地区，政策的有效率高于 80% 即可：<code class="px-1 bg-gray-100 border-2 rounded">5 * (1 - 0.8) = 1</code>。显然，在我们国家是可以做到的，而国外做不到。所以 20 和 21 年我们这边风景独好，国外水深火热。omicron BA.2 的 R0 是 12，这时候要做到控制病毒，政策的有效率需要达到：<code class="px-1 bg-gray-100 border-2 rounded">12 * (1 - E) = 1</code> 即 <code class="px-1 bg-gray-100 border-2 rounded">E = 92%</code>。就目前来看，这个数字是很难的。我们已经有了三个实验数据：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">深圳和现在的上海，封城可以达到 92% 以上的人都阻断了病毒传播；</li><li class="">上海早期，对 delta 起作用的精准防控措施显然达不到 92% 的有效率；</li><li class="">北京，全城半封不封，半死不活，可以把有效率大致维持在 92%。</li></ol><p class="my-4 font-light">那么问题来了，北京就这样永远半封下去吗？或者一爆发就封城吗？上海真的能解封吗？解封了疫情就不会再起来吗？</p><p class="my-4 font-light">每天的新闻发布会都会通报一些违反防控措施的单位和个人，有的案件甚至采取了刑事处罚。然后呢？我们现在可能把所有的不法分子都抓起来，把所有群众的素质都提到最高，把所有尸餐素位的干部都撤换掉吗？做不到的话，以 omicron 的 R0 值，怎么能指望不会隔三差五有一个新的隐匿传播呢？更何况，<strong>一鼓作气，再而衰，三而竭</strong>。随着不设限的封城持续下去，怠惰疲乏而不再遵守防疫规定的人只会越来越多。</p><p class="my-4 font-light">引用最近看到的一句话：</p><blockquote class="pl-4 pr-0 py-2 bg-gray-100 border-l-8"><p class="my-4 font-light">有人不遵守规定不是决策者的问题，但是做决定时候没有考虑到这些人就是决策者的问题了。</p></blockquote><p class="my-4 font-light">现在的防控措施已经是社会极限了，<strong>这是结构性的问题</strong>。我们当前的社会组织能力，当前的科学技术手段，能做到的防疫有效率已经不可能阻止 omicron 的传播了。这也不是当局能力的问题，一代人做一代人的事儿，只希望他们能早日认清。现在唯一的能做的就是思考怎样更好地和病毒共存了。</p><p class="my-4 font-light">附：「安居乐业」的城市列表</p><p class="my-4 font-light"><img src="https://img.yifei.me/file/onefly-public/notes/2022/05/lockdown-list.jpg" alt="安居乐业"></p>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">今天不谈论代码了，写代码也救不了中国人。</p><p class="my-4 font-light">我们知道一个病毒的传播能力可以用 R0 来表示，即每个患者传染的新患者的数量。我们又知道，社会上除了占大多数的遵纪守法好公民，还有目无法纪的奸商、涣散烂漫的小市民和一小撮玩忽职守的领导干部。因此，当局制定的政策的传达效力是有限的，我们只能指望其中的好公民去 100% 的执行，而剩下的几类人不仅不能执行，甚至会带来负效果。设当局政策的有效力为 E，理想情况下 E = 100%。</p><p class="my-4 font-light">当病毒在自然状态下自由传播的时候，它的传播速率就是 R0，只要 R0 大于 1，那就肯定会指数传播，无非是快慢问题。当有人为干预的时候，传播速率就变成了 <code class="px-1 bg-gray-100 border-2 rounded">R0 * (1 - E)</code>，只要我们使这个值小于 1，就能控制病毒的扩散。</p><p class="my-4 font-light">回到现在的疫情，delta 变种 R0 是 5，这时候只要在发生疫情的地区，政策的有效率高于 80% 即可：<code class="px-1 bg-gray-100 border-2 rounded">5 * (1 - 0.8) = 1</code>。显然，在我们国家是可以做到的，而国外做不到。所以 20 和 21 年我们这边风景独好，国外水深火热。omicron BA.2 的 R0 是 12，这时候要做到控制病毒，政策的有效率需要达到：<code class="px-1 bg-gray-100 border-2 rounded">12 * (1 - E) = 1</code> 即 <code class="px-1 bg-gray-100 border-2 rounded">E = 92%</code>。就目前来看，这个数字是很难的。我们已经有了三个实验数据：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">深圳和现在的上海，封城可以达到 92% 以上的人都阻断了病毒传播；</li><li class="">上海早期，对 delta 起作用的精准防控措施显然达不到 92% 的有效率；</li><li class="">北京，全城半封不封，半死不活，可以把有效率大致维持在 92%。</li></ol><p class="my-4 font-light">那么问题来了，北京就这样永远半封下去吗？或者一爆发就封城吗？上海真的能解封吗？解封了疫情就不会再起来吗？</p><p class="my-4 font-light">每天的新闻发布会都会通报一些违反防控措施的单位和个人，有的案件甚至采取了刑事处罚。然后呢？我们现在可能把所有的不法分子都抓起来，把所有群众的素质都提到最高，把所有尸餐素位的干部都撤换掉吗？做不到的话，以 omicron 的 R0 值，怎么能指望不会隔三差五有一个新的隐匿传播呢？更何况，<strong>一鼓作气，再而衰，三而竭</strong>。随着不设限的封城持续下去，怠惰疲乏而不再遵守防疫规定的人只会越来越多。</p><p class="my-4 font-light">引用最近看到的一句话：</p><blockquote class="pl-4 pr-0 py-2 bg-gray-100 border-l-8"><p class="my-4 font-light">有人不遵守规定不是决策者的问题，但是做决定时候没有考虑到这些人就是决策者的问题了。</p></blockquote><p class="my-4 font-light">现在的防控措施已经是社会极限了，<strong>这是结构性的问题</strong>。我们当前的社会组织能力，当前的科学技术手段，能做到的防疫有效率已经不可能阻止 omicron 的传播了。这也不是当局能力的问题，一代人做一代人的事儿，只希望他们能早日认清。现在唯一的能做的就是思考怎样更好地和病毒共存了。</p><p class="my-4 font-light">附：「安居乐业」的城市列表</p><p class="my-4 font-light"><img src="https://img.yifei.me/file/onefly-public/notes/2022/05/lockdown-list.jpg" alt="安居乐业"></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[迁移并升级 Elasticsearch 5.5 到 6.8]]></title>
            <link>https://yifei.me/note/2258</link>
            <guid>https://yifei.me/note/2258</guid>
            <pubDate>Mon, 23 May 2022 01:44:05 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">最近需要把一个 ES 5.5 的集群迁移到 K8S 上，因为目标环境中只有 ES 7 集群，所以在迁移过程中
还需要升级下索引的版本。不过最终只升级到了 6.8。</p><p class="my-4 font-light">开始看到了一个 reindex 的 API，想着直接从老集群 reindex 过去就好了，结果发现不行。直接
reindex 会丢掉类型信息。</p><p class="my-4 font-light">首先尝试了一个搜到的工具：<code class="px-1 bg-gray-100 border-2 rounded">ElasticDump</code>, 然而速度太慢了。后来发现 ES 有原生的 snapshot
API，只需要使用这个 API 备份好，然后导入新的集群即可，而且再这个过程中还可以升级版本，简直
完美。具体操作如下：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">创建 5.5 的 snapshot 到本地硬盘</li><li class="">下载 snapshot 到 6.8 集群机器上</li><li class="">在 6.8 集群中导入 snapshot</li></ol><p class="my-4 font-light">本来想通过 6.8 中再导出然后升级到 7.x，结果发现导出的版本显示还是 5.x 版本的索引，放弃了</p><p class="my-4 font-light">因为我的集群中没有安装任何插件，所以插件的兼容性和升级也不用考虑。</p><p class="my-4 font-light">查看集群的恢复状态：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-built_in">GET</span> _cat/recovery?v&amp;pretty</code></pre><p class="my-4 font-light">查看 snapshot 的状态</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>GET <span class="hljs-regexp">/_snapshot/my</span>_backup/snapshot_1</code></pre><p class="my-4 font-light">查看 snapshot 详情</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>GET <span class="hljs-regexp">/_snapshot/my</span>_backup<span class="hljs-regexp">/snapshot_1/</span>_status</code></pre><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://www.elastic.co/cn/blog/moving-from-types-to-typeless-apis-in-elasticsearch-7-0" title="undefined">https://www.elastic.co/cn/blog/moving-from-types-to-typeless-apis-in-elasticsearch-7-0</a></li><li class=""><a class="underline" href="https://www.elastic.co/guide/en/cloud/current/ec-migrating-data.html" title="null">Migration API</a></li><li class=""><a class="underline" href="https://www.elastic.co/guide/en/elasticsearch/reference/7.17/removal-of-types.html" title="undefined">https://www.elastic.co/guide/en/elasticsearch/reference/7.17/removal-of-types.html</a></li><li class=""><a class="underline" href="https://www.elastic.co/guide/en/elasticsearch/reference/7.17/snapshot-restore.html" title="undefined">https://www.elastic.co/guide/en/elasticsearch/reference/7.17/snapshot-restore.html</a></li></ol>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">最近需要把一个 ES 5.5 的集群迁移到 K8S 上，因为目标环境中只有 ES 7 集群，所以在迁移过程中
还需要升级下索引的版本。不过最终只升级到了 6.8。</p><p class="my-4 font-light">开始看到了一个 reindex 的 API，想着直接从老集群 reindex 过去就好了，结果发现不行。直接
reindex 会丢掉类型信息。</p><p class="my-4 font-light">首先尝试了一个搜到的工具：<code class="px-1 bg-gray-100 border-2 rounded">ElasticDump</code>, 然而速度太慢了。后来发现 ES 有原生的 snapshot
API，只需要使用这个 API 备份好，然后导入新的集群即可，而且再这个过程中还可以升级版本，简直
完美。具体操作如下：</p><ol class="my-4 ml-4 font-light list-decimal"><li class="">创建 5.5 的 snapshot 到本地硬盘</li><li class="">下载 snapshot 到 6.8 集群机器上</li><li class="">在 6.8 集群中导入 snapshot</li></ol><p class="my-4 font-light">本来想通过 6.8 中再导出然后升级到 7.x，结果发现导出的版本显示还是 5.x 版本的索引，放弃了</p><p class="my-4 font-light">因为我的集群中没有安装任何插件，所以插件的兼容性和升级也不用考虑。</p><p class="my-4 font-light">查看集群的恢复状态：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-built_in">GET</span> _cat/recovery?v&amp;pretty</code></pre><p class="my-4 font-light">查看 snapshot 的状态</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>GET <span class="hljs-regexp">/_snapshot/my</span>_backup/snapshot_1</code></pre><p class="my-4 font-light">查看 snapshot 详情</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>GET <span class="hljs-regexp">/_snapshot/my</span>_backup<span class="hljs-regexp">/snapshot_1/</span>_status</code></pre><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://www.elastic.co/cn/blog/moving-from-types-to-typeless-apis-in-elasticsearch-7-0" title="undefined">https://www.elastic.co/cn/blog/moving-from-types-to-typeless-apis-in-elasticsearch-7-0</a></li><li class=""><a class="underline" href="https://www.elastic.co/guide/en/cloud/current/ec-migrating-data.html" title="null">Migration API</a></li><li class=""><a class="underline" href="https://www.elastic.co/guide/en/elasticsearch/reference/7.17/removal-of-types.html" title="undefined">https://www.elastic.co/guide/en/elasticsearch/reference/7.17/removal-of-types.html</a></li><li class=""><a class="underline" href="https://www.elastic.co/guide/en/elasticsearch/reference/7.17/snapshot-restore.html" title="undefined">https://www.elastic.co/guide/en/elasticsearch/reference/7.17/snapshot-restore.html</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Kubernetes is boring enough for me]]></title>
            <link>https://yifei.me/note/2918</link>
            <guid>https://yifei.me/note/2918</guid>
            <pubDate>Mon, 23 May 2022 01:44:05 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">Kubernetes is the new normal. Everyone knows and understands kubernetes, it&#39;s the default
option for now. Rolling out your own infra would be more expensive, actually. It may be a
little surprising and counter-intuitive at the first glance, but it is the reality.</p><p class="my-4 font-light">Like what stackoverflow said in their blog:</p><blockquote class="pl-4 pr-0 py-2 bg-gray-100 border-l-8"><p class="my-4 font-light">Have you been saying “we don’t need Kubernetes because we don’t have product market fit
yet”? Take a closer look, and maybe you’ll find yourself saying “we need Kubernetes
because we don’t have product market fit yet.”</p></blockquote><p class="my-4 font-light">Operating systems are complex, but few one would building their apps on top of bare metal.
You probably have not read Linux&#39;s source code, but you can use it to host your app as
long as you understand the basic concepts of threads and processes etc. You don&#39;t even
have to know all the features of Linux. How many of you are masters of shiny new stuff like
eBPF or WireGuard? You can just learn them when you really need to use them.</p><p class="my-4 font-light">The same goes with kubernetes. It&#39;s complex indeed, but you don&#39;t need to understand every
bit of it before using it. The concepts of k8s is actually very simple -- host an app on
a distributed environment. It almost the same idea as running your app locally.</p><p class="my-4 font-light">So, basically, as other people says, <strong>kubernetes is the OS of distributed systems</strong>. The
analog in details:</p><table>
<thead>
<tr>
<th class="border border-solid px-2 py-1 font-semibold">Linux</th><th class="border border-solid px-2 py-1 font-semibold">Kubernetes</th></tr>
</thead>
<tbody><tr>
<td class="border border-solid px-2 py-1 ">Process</td><td class="border border-solid px-2 py-1 ">Pod</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">Disk</td><td class="border border-solid px-2 py-1 ">PV</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">shell</td><td class="border border-solid px-2 py-1 ">kubectl</td></tr>
<tr>
<td class="border border-solid px-2 py-1 "><code class="px-1 bg-gray-100 border-2 rounded">*.conf</code></td><td class="border border-solid px-2 py-1 ">ConfigMap</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">...</td><td class="border border-solid px-2 py-1 ">...</td></tr>
</tbody></table>
<p class="my-4 font-light">Why did people migrate from bare metal to operating systems? Because the benefits of OSes
outweighs the costs. So what are the problems kubernetes would solve by simply using it?</p><ul class="my-4 ml-4 font-light list-disc"><li class="">Easily add more machines, you can just expand your cluster whatever you like.</li><li class="">Easily scale up and down your apps.</li><li class="">Automatic rollout strategy, which means almost no down time.</li><li class="">Centralize credentials and configuration management.</li><li class="">Monitoring and dashboards are out there, just install one.</li></ul><p class="my-4 font-light">At the very start, you may not need all of them, but manually planing the machines needed
and to decide which db on which machine is exhausting enough, unless you can put all
your services on a single machine forever.</p><p class="my-4 font-light">The problem here is: <strong>Using a single machine is no longer possible nowadays.</strong> Making
topology for your services is inevitable, you have to do it manually, which is tedious
and error-phone, or just use k8s.</p><p class="my-4 font-light">With k8s, you can forget about machines or nodes and how your services communicate, just
write a yaml deployment file, and your service is up and running. It makes a distributed
system feel like a single node.</p><h2 class="my-2 font-semibold text-xl">References</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://stackoverflow.blog/2021/07/21/why-you-should-build-on-kubernetes-from-day-one/" title="undefined">https://stackoverflow.blog/2021/07/21/why-you-should-build-on-kubernetes-from-day-one/</a></li><li class=""><a class="underline" href="https://mp.weixin.qq.com/s?__biz=MzUzMzU5Mjc1Nw==&amp;mid=2247484759&amp;idx=1&amp;sn=25df16461d0ea9f49fd5c36101f8b2ea" title="undefined">https://mp.weixin.qq.com/s?__biz=MzUzMzU5Mjc1Nw==&amp;mid=2247484759&amp;idx=1&amp;sn=25df16461d0ea9f49fd5c36101f8b2ea</a></li><li class=""><a class="underline" href="https://v2ex.com/t/780960" title="undefined">https://v2ex.com/t/780960</a></li><li class=""><a class="underline" href="https://queue.acm.org/detail.cfm?id=2898444" title="undefined">https://queue.acm.org/detail.cfm?id=2898444</a></li></ol>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">Kubernetes is the new normal. Everyone knows and understands kubernetes, it&#39;s the default
option for now. Rolling out your own infra would be more expensive, actually. It may be a
little surprising and counter-intuitive at the first glance, but it is the reality.</p><p class="my-4 font-light">Like what stackoverflow said in their blog:</p><blockquote class="pl-4 pr-0 py-2 bg-gray-100 border-l-8"><p class="my-4 font-light">Have you been saying “we don’t need Kubernetes because we don’t have product market fit
yet”? Take a closer look, and maybe you’ll find yourself saying “we need Kubernetes
because we don’t have product market fit yet.”</p></blockquote><p class="my-4 font-light">Operating systems are complex, but few one would building their apps on top of bare metal.
You probably have not read Linux&#39;s source code, but you can use it to host your app as
long as you understand the basic concepts of threads and processes etc. You don&#39;t even
have to know all the features of Linux. How many of you are masters of shiny new stuff like
eBPF or WireGuard? You can just learn them when you really need to use them.</p><p class="my-4 font-light">The same goes with kubernetes. It&#39;s complex indeed, but you don&#39;t need to understand every
bit of it before using it. The concepts of k8s is actually very simple -- host an app on
a distributed environment. It almost the same idea as running your app locally.</p><p class="my-4 font-light">So, basically, as other people says, <strong>kubernetes is the OS of distributed systems</strong>. The
analog in details:</p><table>
<thead>
<tr>
<th class="border border-solid px-2 py-1 font-semibold">Linux</th><th class="border border-solid px-2 py-1 font-semibold">Kubernetes</th></tr>
</thead>
<tbody><tr>
<td class="border border-solid px-2 py-1 ">Process</td><td class="border border-solid px-2 py-1 ">Pod</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">Disk</td><td class="border border-solid px-2 py-1 ">PV</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">shell</td><td class="border border-solid px-2 py-1 ">kubectl</td></tr>
<tr>
<td class="border border-solid px-2 py-1 "><code class="px-1 bg-gray-100 border-2 rounded">*.conf</code></td><td class="border border-solid px-2 py-1 ">ConfigMap</td></tr>
<tr>
<td class="border border-solid px-2 py-1 ">...</td><td class="border border-solid px-2 py-1 ">...</td></tr>
</tbody></table>
<p class="my-4 font-light">Why did people migrate from bare metal to operating systems? Because the benefits of OSes
outweighs the costs. So what are the problems kubernetes would solve by simply using it?</p><ul class="my-4 ml-4 font-light list-disc"><li class="">Easily add more machines, you can just expand your cluster whatever you like.</li><li class="">Easily scale up and down your apps.</li><li class="">Automatic rollout strategy, which means almost no down time.</li><li class="">Centralize credentials and configuration management.</li><li class="">Monitoring and dashboards are out there, just install one.</li></ul><p class="my-4 font-light">At the very start, you may not need all of them, but manually planing the machines needed
and to decide which db on which machine is exhausting enough, unless you can put all
your services on a single machine forever.</p><p class="my-4 font-light">The problem here is: <strong>Using a single machine is no longer possible nowadays.</strong> Making
topology for your services is inevitable, you have to do it manually, which is tedious
and error-phone, or just use k8s.</p><p class="my-4 font-light">With k8s, you can forget about machines or nodes and how your services communicate, just
write a yaml deployment file, and your service is up and running. It makes a distributed
system feel like a single node.</p><h2 class="my-2 font-semibold text-xl">References</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://stackoverflow.blog/2021/07/21/why-you-should-build-on-kubernetes-from-day-one/" title="undefined">https://stackoverflow.blog/2021/07/21/why-you-should-build-on-kubernetes-from-day-one/</a></li><li class=""><a class="underline" href="https://mp.weixin.qq.com/s?__biz=MzUzMzU5Mjc1Nw==&amp;mid=2247484759&amp;idx=1&amp;sn=25df16461d0ea9f49fd5c36101f8b2ea" title="undefined">https://mp.weixin.qq.com/s?__biz=MzUzMzU5Mjc1Nw==&amp;mid=2247484759&amp;idx=1&amp;sn=25df16461d0ea9f49fd5c36101f8b2ea</a></li><li class=""><a class="underline" href="https://v2ex.com/t/780960" title="undefined">https://v2ex.com/t/780960</a></li><li class=""><a class="underline" href="https://queue.acm.org/detail.cfm?id=2898444" title="undefined">https://queue.acm.org/detail.cfm?id=2898444</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 multipass+autok3s 快速搭建本地 k3s 集群]]></title>
            <link>https://yifei.me/note/2947</link>
            <guid>https://yifei.me/note/2947</guid>
            <pubDate>Thu, 28 Apr 2022 03:56:40 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">最近要迁移一个老服务到 kubernetes 上，自然需要在本地先测试好，然后再部署到生产集群中。
然而 Docker Desktop 性能实在太差，而且已经不再免费了，所以研究了下其他工具，遂有本文。</p><h2 class="my-2 font-semibold text-xl">用到的工具</h2><ul class="my-4 ml-4 font-light list-disc"><li class="">multipass 是 Ubuntu 背后的厂商 Canonical 推出的一款虚拟机工具，可以在本地快速搭建起一个
  Ubuntu 集群用于开发和测试。</li><li class="">k3s 是 Rancher 推出的一个轻量级 k8s 发行版。</li><li class="">autok3s 是 Rancher 中国推出的一款自动部署 k3s 集群的工具。</li><li class="">arkade 是一个快速安装 k8s 相关 cli 和 helm package 的命令行工具。</li><li class="">podman 是一个开源的 docker 替代工具。</li></ul><p class="my-4 font-light">我们可以结合以上工具在 macOS 或 Linux 上快速搭建一个本地的 k8s 集群。</p><h2 class="my-2 font-semibold text-xl">安装集群</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># Ubuntu 官方提供的虚拟机工具</span>
brew install multipass

<span class="hljs-comment"># 创建几个节点，分别是2核2G和2核4G，我的机器是 32G/2T，创建这几个节点还是绰绰有余的</span>
multipass launch -c 2 -m 2G -d 40G -n primary
multipass launch -c 2 -m 4G -d 40G -n worker1
multipass launch -c 2 -m 4G -d 40G -n worker2

<span class="hljs-comment"># 为了方便，把宿主机的家目录 mount 到虚拟机上</span>
<span class="hljs-comment"># 因为 primary 机器默认已经 mount 了，只需要 mount worker 节点</span>
multipass mount <span class="hljs-variable">$HOME</span> worker1:/home/ubuntu/Home
multipass mount <span class="hljs-variable">$HOME</span> worker2:/home/ubuntu/Home

<span class="hljs-comment"># 这时候我们已经可以登录了</span>
multipass shell primary/worker1/worker2

<span class="hljs-comment"># 为了方便，把宿主机默认的 ssh key 复制过去</span>
<span class="hljs-comment"># 在每个虚拟机中都执行</span>
<span class="hljs-built_in">cat</span> Home/.ssh/id_rsa.pub &gt;&gt; .ssh/authorized_keys

<span class="hljs-comment"># 回到宿主机，安装 arkade + autok3s. Arkade 是一个 kubernetes 相关工具的快捷安装包</span>
brew install arkade
arkade get autok3s  <span class="hljs-comment"># 可能需要按照提示把 autok3s 加入 PATH</span>

<span class="hljs-comment"># 首先 看一下机器的 IP</span>
mulitpass <span class="hljs-built_in">ls</span>
<span class="hljs-comment"># Name                    State             IPv4             Image</span>
<span class="hljs-comment"># primary                 Running           192.168.64.2     Ubuntu 20.04 LTS</span>
<span class="hljs-comment"># worker1                 Running           192.168.64.3     Ubuntu 20.04 LTS</span>
<span class="hljs-comment"># worker2                 Running           192.168.64.4     Ubuntu 20.04 LTS</span>

<span class="hljs-comment"># 测试一下 ssh</span>
ssh ubuntu@182.168.63.3

<span class="hljs-comment"># 开始创建集群，这里我选择的名字是 imac，因为我是在自己的 iMac 上创建的集群。</span>
<span class="hljs-comment"># native provider 指的是使用普通的 Linux 节点，因为 autok3s 还支持众多云厂商。</span>
autok3s -d create \
    --provider native \
    --name imac \
    --ssh-user ubuntu \
    --ssh-key-path <span class="hljs-variable">$HOME</span>/.ssh/id_rsa \
    --master-ips 192.168.64.2 \
    --worker-ips 192.168.64.3,192.168.64.4

<span class="hljs-comment"># 稍等片刻，可以看到集群已经创建成功了</span>
<span class="hljs-comment"># 按照日志中的提示，切换配置到这个集群</span>
autok3s kubectl config use-context imac
<span class="hljs-comment"># 然后就可以使用了~</span>
autok3s kubectl get pods -A
<span class="hljs-comment"># NAMESPACE     NAME                                      READY   STATUS              RESTARTS   AGE</span>
<span class="hljs-comment"># kube-system   local-path-provisioner-84bb864455-qg76v   1/1     Running             0          2m24s</span>
<span class="hljs-comment"># kube-system   coredns-96cc4f57d-6mq9f                   1/1     Running             0          2m24s</span>
<span class="hljs-comment"># kube-system   helm-install-traefik-crd--1-2b7cc         0/1     Completed           0          2m24s</span>
<span class="hljs-comment"># kube-system   helm-install-traefik--1-5k4lg             0/1     Completed           1          2m24s</span>
<span class="hljs-comment"># kube-system   metrics-server-ff9dbcb6c-r7tzn            1/1     Running             0          2m24s</span>
<span class="hljs-comment"># kube-system   svclb-traefik-9sc9z                       2/2     Running             0          104s</span>
<span class="hljs-comment"># kube-system   traefik-56c4b88c4b-xmffz                  1/1     Running             0          104s</span>
<span class="hljs-comment"># kube-system   svclb-traefik-cvk8w                       2/2     Running             0          56s</span>
<span class="hljs-comment"># kube-system   svclb-traefik-hj8cs                       0/2     ContainerCreating   0          20s</span>

<span class="hljs-comment"># 万一中途因为网络或者 ssh 配置错误等原因失败了，可以删掉有问题的集群重来</span>
autok3s list
autok3s -d delete --provider native --name imac
<span class="hljs-comment"># 如果还有没清理干净的残留，可以登录到虚拟机上使用 k3s-uninstall 脚本继续清理</span>

<span class="hljs-comment"># 安装后的 kubeconfig 文件默认在 ~/.autok3s/.kube/config 中，复制出来可以直接使用 kubectl</span>
<span class="hljs-built_in">mkdir</span> -p ~/.kube
<span class="hljs-built_in">cp</span> ~/.autok3s/.kube/config ~/.kube/config
kubectl get pods -A

<span class="hljs-comment"># 使用 arkade 安装 portainer 管理面板</span>
arkade install portainer
kubectl port-forward -n default svc/portainer 9000:9000

<span class="hljs-comment"># 打开 localhost:9000 就可以看到 portainer 的界面啦~</span></code></pre><p class="my-4 font-light"><img src="https://img.yifei.me/file/onefly-public/notes/2022/04/portainer.png" alt="portainer"></p><p class="my-4 font-light">到这里，集群就部署好啦，接下来我们部署一个 demo app 上去！</p><h2 class="my-2 font-semibold text-xl">部署应用</h2><h3 class="my-2 font-semibold ">搭建 docker registry</h3><p class="my-4 font-light">因为我的目的只是在本地做实验，所以没必要上传镜像到外部的 registry 上。但是本地的镜像也
不能在集群中直接使用。为了解决这个问题，我们在集群内部搭建一个 docker registry，用于把
镜像上传到集群中。为了把 registry 暴露到宿主机，我们还需要安装 ingress-nginx，并且在本地
和每个虚拟机的 <code class="px-1 bg-gray-100 border-2 rounded">/etc/hosts</code> 中添加对应的域名。</p><p class="my-4 font-light">这里，我们使用 <code class="px-1 bg-gray-100 border-2 rounded">docker.imac.local</code> 作为本地仓库的域名。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 删除 k3s 自带的 traefik</span>
kubectl delete svc/traefik -n kube-system
kubectl delete deploy/traefik -n kube-system

<span class="hljs-comment"># 安装 ingress nginx</span>
arkade install ingress-nginx

<span class="hljs-comment"># 在 k8s 内部安装 docker registry</span>
arkade install docker-registry --email you@example.com --domain docker.imac.local
<span class="hljs-comment"># 在日志最后会输出密码，先记下来，不要丢了。本地的密码，亮出来也无所谓。</span>
<span class="hljs-comment"># export PASSWORD=zZPFqGO73IE26823434g</span>

<span class="hljs-comment"># 在宿主机为我们的 registry 添加域名 docker.imac.local</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;192.168.64.2 docker.imac.local&#x27;</span> &gt;&gt; /etc/hosts
<span class="hljs-comment"># 如果是在 mac 上，还需要登录 podman 虚拟机，执行同样的操作</span>
podman machine ssh
<span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;192.168.64.2 docker.imac.local&#x27;</span> &gt;&gt; /etc/hosts
<span class="hljs-comment"># 登录 multipass 的虚拟机，执行同样的操作</span>
mulitpass shell primary/worker1/worker2
<span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;192.168.64.2 docker.imac.local&#x27;</span> &gt;&gt; /etc/hosts
<span class="hljs-comment"># 让 k3s 集群忽略 tls 错误, 同样需要在每台虚拟机上执行</span>
<span class="hljs-built_in">cat</span> &gt;&gt; /etc/rancher/k3s/registries.yaml &lt;&lt; <span class="hljs-string">EOF
configs:
  &quot;docker.imac.local&quot;:
    auth:
      username: admin
      password: zZPFqGO73IE26823434g  # 上面打印出的密码
    tls:
      insecure_skip_verify: true
EOF</span>
<span class="hljs-comment"># 在每台虚拟机，依次重启 k3s</span>
sudo systemctl restart k3s  <span class="hljs-comment"># master</span>
sudo systemclt restart k3s-agent  <span class="hljs-comment"># worker</span>

<span class="hljs-comment"># 宿主机上，使用 curl 验证一下，因为集群中使用的是自签名证书，这里使用 -k/--insecure 选项</span>
<span class="hljs-comment"># 忽略证书错误。</span>
<span class="hljs-comment"># 可以看到 404，说明 ingress controller 安装成功。但是集群还不知道这个域名该路由到哪个服务，</span>
<span class="hljs-comment"># 所以返回了 404 Not Found</span>
curl -k https://docker.imac.local
<span class="hljs-comment"># &lt;html&gt;</span>
<span class="hljs-comment"># &lt;head&gt;&lt;title&gt;404 Not Found&lt;/title&gt;&lt;/head&gt;</span>
<span class="hljs-comment"># &lt;body&gt;</span>
<span class="hljs-comment"># &lt;center&gt;&lt;h1&gt;404 Not Found&lt;/h1&gt;&lt;/center&gt;</span>
<span class="hljs-comment"># &lt;hr&gt;&lt;center&gt;nginx&lt;/center&gt;</span>
<span class="hljs-comment"># &lt;/body&gt;</span>
<span class="hljs-comment"># &lt;/html&gt;</span>

<span class="hljs-comment"># 安装 registry ingress</span>
arkade install docker-registry-ingress

<span class="hljs-comment"># 登录，同样忽略证书错误</span>
podman login docker.imac.local --tls-verify=<span class="hljs-literal">false</span>
<span class="hljs-comment"># Username: admin</span>
<span class="hljs-comment"># Password:</span>
<span class="hljs-comment"># Login Succeeded!</span></code></pre><p class="my-4 font-light">registry 部署好之后，就可以打包推送镜像上去了。</p><h3 class="my-2 font-semibold ">部署服务</h3><p class="my-4 font-light">写一个简单的页面来演示如何部署服务。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-built_in">mkdir</span> <span class="hljs-built_in">test</span>
<span class="hljs-built_in">cd</span> <span class="hljs-built_in">test</span>

<span class="hljs-comment"># 这是最后要验证的结果</span>
<span class="hljs-built_in">cat</span> &gt;&gt; index.html &lt;&lt; <span class="hljs-string">EOF
&lt;p&gt;Hello from Test App.&lt;/p&gt;
EOF</span>

<span class="hljs-comment"># 基于 nginx 镜像，把这个文件拷进去</span>
<span class="hljs-built_in">cat</span> &gt;&gt; Dockerfile &lt;&lt; <span class="hljs-string">EOF
FROM nginx

COPY index.html /usr/share/nginx/html/
EOF</span>

<span class="hljs-comment"># 构建镜像</span>
podman build . -t docker.imac.local/test-app
<span class="hljs-comment"># push 到我们的私有 registry 中</span>
podman push docker.imac.local/test-app --tls-verify=<span class="hljs-literal">false</span></code></pre><p class="my-4 font-light">我们打算部署到 <code class="px-1 bg-gray-100 border-2 rounded">test-app.imac.local</code> 这个域名，所以还需要在本机添加 DNS：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;192.168.64.2 test-app.imac.local&#x27;</span> &gt;&gt; /etc/hosts</code></pre><p class="my-4 font-light">下面是要部署的服务的配置文件，都是最基础的配置，不再展开赘述。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">test-app</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">test-app</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">test-app</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">test-app</span>
        <span class="hljs-comment"># 使用刚刚打包的镜像</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">docker.imac.local/test-app</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">test-service</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">test-app</span>
  <span class="hljs-attr">ports:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">http</span>
    <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
    <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
    <span class="hljs-attr">targetPort:</span> <span class="hljs-number">80</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Ingress</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">test-ingress</span>
  <span class="hljs-attr">annotations:</span>
    <span class="hljs-attr">kubernetes.io/ingress.class:</span> <span class="hljs-string">nginx</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">rules:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">host:</span> <span class="hljs-string">test-app.imac.local</span>
    <span class="hljs-attr">http:</span>
      <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">path:</span> <span class="hljs-string">/</span>
        <span class="hljs-attr">pathType:</span> <span class="hljs-string">Prefix</span>
        <span class="hljs-attr">backend:</span>
          <span class="hljs-attr">service:</span>
            <span class="hljs-attr">name:</span> <span class="hljs-string">test-service</span>
            <span class="hljs-attr">port:</span>
              <span class="hljs-attr">number:</span> <span class="hljs-number">80</span></code></pre><p class="my-4 font-light">部署：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>kubectl <span class="hljs-built_in">apply</span> -f app.yaml</code></pre><p class="my-4 font-light">然后测试一下：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>curl -k <span class="hljs-attr">https</span>:<span class="hljs-comment">//test-app.imac.local</span>

<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Hello from Test App.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span></code></pre><p class="my-4 font-light">浏览器打开：</p><p class="my-4 font-light"><img src="https://img.yifei.me/file/onefly-public/notes/2022/04/testapp.png" alt="test-app"></p><p class="my-4 font-light">大功告成啦！如果要部署新的服务，只需要部署时在本机 <code class="px-1 bg-gray-100 border-2 rounded">/etc/hosts</code> 中添加对应的域名就可以啦！</p><h2 class="my-2 font-semibold text-xl">附录 - k3s 简介</h2><p class="my-4 font-light">k3s 是 rancher 出品的一个 Kubernetes 发行版，特点是单二进制文件，小巧且不吃资源，甚至可以
在树莓派上部署。虽然它对许多组件做了替换，如把 etcd 替换成了 sqlite3，但是依然是一个
通过了官方认证的 Kubernetes 发行版。</p><p class="my-4 font-light">除了 k3s 以外，还有一些其他的精简 k8s 发行版，比如 microk8s, kind, minikube 等等，但是
都远远没有 k3s 轻量。</p><ul class="my-4 ml-4 font-light list-disc"><li class="">autok3s 类似工具还有 - k3sup, 它主要面向国外云厂商，而 autok3s 主要面向国内云厂商。</li><li class="">k3s 默认带了自己的 local-path provider，可以直接创建 PV.</li><li class="">k3s 安装后会利用 iptables 规则拦截 80 端口的流量</li><li class="">k3s 默认在监听 0.0.0.0，可以使用安全组禁用 API 端口了。</li></ul><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://docs.rancher.cn/docs/k3s/autok3s/native/_index" title="undefined">https://docs.rancher.cn/docs/k3s/autok3s/native/_index</a></li><li class=""><a class="underline" href="https://web.archive.org/web/20201124123950/https://www.portainer.io/2020/04/portainer-for-kubernetes-in-less-than-60-seconds/" title="undefined">https://web.archive.org/web/20201124123950/https://www.portainer.io/2020/04/portainer-for-kubernetes-in-less-than-60-seconds/</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/51511547/empty-address-kubernetes-ingress" title="undefined">https://stackoverflow.com/questions/51511547/empty-address-kubernetes-ingress</a></li><li class=""><a class="underline" href="https://rancher.com/docs/k3s/latest/en/quick-start/" title="null">k3s 官方文档</a></li><li class=""><a class="underline" href="https://www.reddit.com/r/kubernetes/comments/be0415/k3s_minikube_or_microk8s/" title="undefined">https://www.reddit.com/r/kubernetes/comments/be0415/k3s_minikube_or_microk8s/</a></li><li class=""><a class="underline" href="https://brennerm.github.io/posts/minikube-vs-kind-vs-k3s.html" title="undefined">https://brennerm.github.io/posts/minikube-vs-kind-vs-k3s.html</a></li><li class=""><a class="underline" href="https://kauri.io/38-install-and-configure-a-kubernetes-cluster-with/418b3bc1e0544fbc955a4bbba6fff8a9/a" title="null">非常全的一份文档</a></li><li class=""><a class="underline" href="https://github.com/k3s-io/k3s/issues/2403" title="undefined">https://github.com/k3s-io/k3s/issues/2403</a></li><li class=""><a class="underline" href="https://blog.oddbit.com/post/2018-02-27-listening-for-connections-on-a/" title="undefined">https://blog.oddbit.com/post/2018-02-27-listening-for-connections-on-a/</a></li><li class=""><a class="underline" href="https://mp.weixin.qq.com/s/H6j8cIHc34zXPM0SPM6Ycg" title="null">k3s with external etcd</a></li></ol>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">最近要迁移一个老服务到 kubernetes 上，自然需要在本地先测试好，然后再部署到生产集群中。
然而 Docker Desktop 性能实在太差，而且已经不再免费了，所以研究了下其他工具，遂有本文。</p><h2 class="my-2 font-semibold text-xl">用到的工具</h2><ul class="my-4 ml-4 font-light list-disc"><li class="">multipass 是 Ubuntu 背后的厂商 Canonical 推出的一款虚拟机工具，可以在本地快速搭建起一个
  Ubuntu 集群用于开发和测试。</li><li class="">k3s 是 Rancher 推出的一个轻量级 k8s 发行版。</li><li class="">autok3s 是 Rancher 中国推出的一款自动部署 k3s 集群的工具。</li><li class="">arkade 是一个快速安装 k8s 相关 cli 和 helm package 的命令行工具。</li><li class="">podman 是一个开源的 docker 替代工具。</li></ul><p class="my-4 font-light">我们可以结合以上工具在 macOS 或 Linux 上快速搭建一个本地的 k8s 集群。</p><h2 class="my-2 font-semibold text-xl">安装集群</h2><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># Ubuntu 官方提供的虚拟机工具</span>
brew install multipass

<span class="hljs-comment"># 创建几个节点，分别是2核2G和2核4G，我的机器是 32G/2T，创建这几个节点还是绰绰有余的</span>
multipass launch -c 2 -m 2G -d 40G -n primary
multipass launch -c 2 -m 4G -d 40G -n worker1
multipass launch -c 2 -m 4G -d 40G -n worker2

<span class="hljs-comment"># 为了方便，把宿主机的家目录 mount 到虚拟机上</span>
<span class="hljs-comment"># 因为 primary 机器默认已经 mount 了，只需要 mount worker 节点</span>
multipass mount <span class="hljs-variable">$HOME</span> worker1:/home/ubuntu/Home
multipass mount <span class="hljs-variable">$HOME</span> worker2:/home/ubuntu/Home

<span class="hljs-comment"># 这时候我们已经可以登录了</span>
multipass shell primary/worker1/worker2

<span class="hljs-comment"># 为了方便，把宿主机默认的 ssh key 复制过去</span>
<span class="hljs-comment"># 在每个虚拟机中都执行</span>
<span class="hljs-built_in">cat</span> Home/.ssh/id_rsa.pub &gt;&gt; .ssh/authorized_keys

<span class="hljs-comment"># 回到宿主机，安装 arkade + autok3s. Arkade 是一个 kubernetes 相关工具的快捷安装包</span>
brew install arkade
arkade get autok3s  <span class="hljs-comment"># 可能需要按照提示把 autok3s 加入 PATH</span>

<span class="hljs-comment"># 首先 看一下机器的 IP</span>
mulitpass <span class="hljs-built_in">ls</span>
<span class="hljs-comment"># Name                    State             IPv4             Image</span>
<span class="hljs-comment"># primary                 Running           192.168.64.2     Ubuntu 20.04 LTS</span>
<span class="hljs-comment"># worker1                 Running           192.168.64.3     Ubuntu 20.04 LTS</span>
<span class="hljs-comment"># worker2                 Running           192.168.64.4     Ubuntu 20.04 LTS</span>

<span class="hljs-comment"># 测试一下 ssh</span>
ssh ubuntu@182.168.63.3

<span class="hljs-comment"># 开始创建集群，这里我选择的名字是 imac，因为我是在自己的 iMac 上创建的集群。</span>
<span class="hljs-comment"># native provider 指的是使用普通的 Linux 节点，因为 autok3s 还支持众多云厂商。</span>
autok3s -d create \
    --provider native \
    --name imac \
    --ssh-user ubuntu \
    --ssh-key-path <span class="hljs-variable">$HOME</span>/.ssh/id_rsa \
    --master-ips 192.168.64.2 \
    --worker-ips 192.168.64.3,192.168.64.4

<span class="hljs-comment"># 稍等片刻，可以看到集群已经创建成功了</span>
<span class="hljs-comment"># 按照日志中的提示，切换配置到这个集群</span>
autok3s kubectl config use-context imac
<span class="hljs-comment"># 然后就可以使用了~</span>
autok3s kubectl get pods -A
<span class="hljs-comment"># NAMESPACE     NAME                                      READY   STATUS              RESTARTS   AGE</span>
<span class="hljs-comment"># kube-system   local-path-provisioner-84bb864455-qg76v   1/1     Running             0          2m24s</span>
<span class="hljs-comment"># kube-system   coredns-96cc4f57d-6mq9f                   1/1     Running             0          2m24s</span>
<span class="hljs-comment"># kube-system   helm-install-traefik-crd--1-2b7cc         0/1     Completed           0          2m24s</span>
<span class="hljs-comment"># kube-system   helm-install-traefik--1-5k4lg             0/1     Completed           1          2m24s</span>
<span class="hljs-comment"># kube-system   metrics-server-ff9dbcb6c-r7tzn            1/1     Running             0          2m24s</span>
<span class="hljs-comment"># kube-system   svclb-traefik-9sc9z                       2/2     Running             0          104s</span>
<span class="hljs-comment"># kube-system   traefik-56c4b88c4b-xmffz                  1/1     Running             0          104s</span>
<span class="hljs-comment"># kube-system   svclb-traefik-cvk8w                       2/2     Running             0          56s</span>
<span class="hljs-comment"># kube-system   svclb-traefik-hj8cs                       0/2     ContainerCreating   0          20s</span>

<span class="hljs-comment"># 万一中途因为网络或者 ssh 配置错误等原因失败了，可以删掉有问题的集群重来</span>
autok3s list
autok3s -d delete --provider native --name imac
<span class="hljs-comment"># 如果还有没清理干净的残留，可以登录到虚拟机上使用 k3s-uninstall 脚本继续清理</span>

<span class="hljs-comment"># 安装后的 kubeconfig 文件默认在 ~/.autok3s/.kube/config 中，复制出来可以直接使用 kubectl</span>
<span class="hljs-built_in">mkdir</span> -p ~/.kube
<span class="hljs-built_in">cp</span> ~/.autok3s/.kube/config ~/.kube/config
kubectl get pods -A

<span class="hljs-comment"># 使用 arkade 安装 portainer 管理面板</span>
arkade install portainer
kubectl port-forward -n default svc/portainer 9000:9000

<span class="hljs-comment"># 打开 localhost:9000 就可以看到 portainer 的界面啦~</span></code></pre><p class="my-4 font-light"><img src="https://img.yifei.me/file/onefly-public/notes/2022/04/portainer.png" alt="portainer"></p><p class="my-4 font-light">到这里，集群就部署好啦，接下来我们部署一个 demo app 上去！</p><h2 class="my-2 font-semibold text-xl">部署应用</h2><h3 class="my-2 font-semibold ">搭建 docker registry</h3><p class="my-4 font-light">因为我的目的只是在本地做实验，所以没必要上传镜像到外部的 registry 上。但是本地的镜像也
不能在集群中直接使用。为了解决这个问题，我们在集群内部搭建一个 docker registry，用于把
镜像上传到集群中。为了把 registry 暴露到宿主机，我们还需要安装 ingress-nginx，并且在本地
和每个虚拟机的 <code class="px-1 bg-gray-100 border-2 rounded">/etc/hosts</code> 中添加对应的域名。</p><p class="my-4 font-light">这里，我们使用 <code class="px-1 bg-gray-100 border-2 rounded">docker.imac.local</code> 作为本地仓库的域名。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 删除 k3s 自带的 traefik</span>
kubectl delete svc/traefik -n kube-system
kubectl delete deploy/traefik -n kube-system

<span class="hljs-comment"># 安装 ingress nginx</span>
arkade install ingress-nginx

<span class="hljs-comment"># 在 k8s 内部安装 docker registry</span>
arkade install docker-registry --email you@example.com --domain docker.imac.local
<span class="hljs-comment"># 在日志最后会输出密码，先记下来，不要丢了。本地的密码，亮出来也无所谓。</span>
<span class="hljs-comment"># export PASSWORD=zZPFqGO73IE26823434g</span>

<span class="hljs-comment"># 在宿主机为我们的 registry 添加域名 docker.imac.local</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;192.168.64.2 docker.imac.local&#x27;</span> &gt;&gt; /etc/hosts
<span class="hljs-comment"># 如果是在 mac 上，还需要登录 podman 虚拟机，执行同样的操作</span>
podman machine ssh
<span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;192.168.64.2 docker.imac.local&#x27;</span> &gt;&gt; /etc/hosts
<span class="hljs-comment"># 登录 multipass 的虚拟机，执行同样的操作</span>
mulitpass shell primary/worker1/worker2
<span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;192.168.64.2 docker.imac.local&#x27;</span> &gt;&gt; /etc/hosts
<span class="hljs-comment"># 让 k3s 集群忽略 tls 错误, 同样需要在每台虚拟机上执行</span>
<span class="hljs-built_in">cat</span> &gt;&gt; /etc/rancher/k3s/registries.yaml &lt;&lt; <span class="hljs-string">EOF
configs:
  &quot;docker.imac.local&quot;:
    auth:
      username: admin
      password: zZPFqGO73IE26823434g  # 上面打印出的密码
    tls:
      insecure_skip_verify: true
EOF</span>
<span class="hljs-comment"># 在每台虚拟机，依次重启 k3s</span>
sudo systemctl restart k3s  <span class="hljs-comment"># master</span>
sudo systemclt restart k3s-agent  <span class="hljs-comment"># worker</span>

<span class="hljs-comment"># 宿主机上，使用 curl 验证一下，因为集群中使用的是自签名证书，这里使用 -k/--insecure 选项</span>
<span class="hljs-comment"># 忽略证书错误。</span>
<span class="hljs-comment"># 可以看到 404，说明 ingress controller 安装成功。但是集群还不知道这个域名该路由到哪个服务，</span>
<span class="hljs-comment"># 所以返回了 404 Not Found</span>
curl -k https://docker.imac.local
<span class="hljs-comment"># &lt;html&gt;</span>
<span class="hljs-comment"># &lt;head&gt;&lt;title&gt;404 Not Found&lt;/title&gt;&lt;/head&gt;</span>
<span class="hljs-comment"># &lt;body&gt;</span>
<span class="hljs-comment"># &lt;center&gt;&lt;h1&gt;404 Not Found&lt;/h1&gt;&lt;/center&gt;</span>
<span class="hljs-comment"># &lt;hr&gt;&lt;center&gt;nginx&lt;/center&gt;</span>
<span class="hljs-comment"># &lt;/body&gt;</span>
<span class="hljs-comment"># &lt;/html&gt;</span>

<span class="hljs-comment"># 安装 registry ingress</span>
arkade install docker-registry-ingress

<span class="hljs-comment"># 登录，同样忽略证书错误</span>
podman login docker.imac.local --tls-verify=<span class="hljs-literal">false</span>
<span class="hljs-comment"># Username: admin</span>
<span class="hljs-comment"># Password:</span>
<span class="hljs-comment"># Login Succeeded!</span></code></pre><p class="my-4 font-light">registry 部署好之后，就可以打包推送镜像上去了。</p><h3 class="my-2 font-semibold ">部署服务</h3><p class="my-4 font-light">写一个简单的页面来演示如何部署服务。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-built_in">mkdir</span> <span class="hljs-built_in">test</span>
<span class="hljs-built_in">cd</span> <span class="hljs-built_in">test</span>

<span class="hljs-comment"># 这是最后要验证的结果</span>
<span class="hljs-built_in">cat</span> &gt;&gt; index.html &lt;&lt; <span class="hljs-string">EOF
&lt;p&gt;Hello from Test App.&lt;/p&gt;
EOF</span>

<span class="hljs-comment"># 基于 nginx 镜像，把这个文件拷进去</span>
<span class="hljs-built_in">cat</span> &gt;&gt; Dockerfile &lt;&lt; <span class="hljs-string">EOF
FROM nginx

COPY index.html /usr/share/nginx/html/
EOF</span>

<span class="hljs-comment"># 构建镜像</span>
podman build . -t docker.imac.local/test-app
<span class="hljs-comment"># push 到我们的私有 registry 中</span>
podman push docker.imac.local/test-app --tls-verify=<span class="hljs-literal">false</span></code></pre><p class="my-4 font-light">我们打算部署到 <code class="px-1 bg-gray-100 border-2 rounded">test-app.imac.local</code> 这个域名，所以还需要在本机添加 DNS：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-built_in">echo</span> <span class="hljs-string">&#x27;192.168.64.2 test-app.imac.local&#x27;</span> &gt;&gt; /etc/hosts</code></pre><p class="my-4 font-light">下面是要部署的服务的配置文件，都是最基础的配置，不再展开赘述。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">test-app</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">test-app</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">test-app</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">test-app</span>
        <span class="hljs-comment"># 使用刚刚打包的镜像</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">docker.imac.local/test-app</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">test-service</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">test-app</span>
  <span class="hljs-attr">ports:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">http</span>
    <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
    <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
    <span class="hljs-attr">targetPort:</span> <span class="hljs-number">80</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Ingress</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">test-ingress</span>
  <span class="hljs-attr">annotations:</span>
    <span class="hljs-attr">kubernetes.io/ingress.class:</span> <span class="hljs-string">nginx</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">rules:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">host:</span> <span class="hljs-string">test-app.imac.local</span>
    <span class="hljs-attr">http:</span>
      <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">path:</span> <span class="hljs-string">/</span>
        <span class="hljs-attr">pathType:</span> <span class="hljs-string">Prefix</span>
        <span class="hljs-attr">backend:</span>
          <span class="hljs-attr">service:</span>
            <span class="hljs-attr">name:</span> <span class="hljs-string">test-service</span>
            <span class="hljs-attr">port:</span>
              <span class="hljs-attr">number:</span> <span class="hljs-number">80</span></code></pre><p class="my-4 font-light">部署：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>kubectl <span class="hljs-built_in">apply</span> -f app.yaml</code></pre><p class="my-4 font-light">然后测试一下：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>curl -k <span class="hljs-attr">https</span>:<span class="hljs-comment">//test-app.imac.local</span>

<span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Hello from Test App.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span></code></pre><p class="my-4 font-light">浏览器打开：</p><p class="my-4 font-light"><img src="https://img.yifei.me/file/onefly-public/notes/2022/04/testapp.png" alt="test-app"></p><p class="my-4 font-light">大功告成啦！如果要部署新的服务，只需要部署时在本机 <code class="px-1 bg-gray-100 border-2 rounded">/etc/hosts</code> 中添加对应的域名就可以啦！</p><h2 class="my-2 font-semibold text-xl">附录 - k3s 简介</h2><p class="my-4 font-light">k3s 是 rancher 出品的一个 Kubernetes 发行版，特点是单二进制文件，小巧且不吃资源，甚至可以
在树莓派上部署。虽然它对许多组件做了替换，如把 etcd 替换成了 sqlite3，但是依然是一个
通过了官方认证的 Kubernetes 发行版。</p><p class="my-4 font-light">除了 k3s 以外，还有一些其他的精简 k8s 发行版，比如 microk8s, kind, minikube 等等，但是
都远远没有 k3s 轻量。</p><ul class="my-4 ml-4 font-light list-disc"><li class="">autok3s 类似工具还有 - k3sup, 它主要面向国外云厂商，而 autok3s 主要面向国内云厂商。</li><li class="">k3s 默认带了自己的 local-path provider，可以直接创建 PV.</li><li class="">k3s 安装后会利用 iptables 规则拦截 80 端口的流量</li><li class="">k3s 默认在监听 0.0.0.0，可以使用安全组禁用 API 端口了。</li></ul><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://docs.rancher.cn/docs/k3s/autok3s/native/_index" title="undefined">https://docs.rancher.cn/docs/k3s/autok3s/native/_index</a></li><li class=""><a class="underline" href="https://web.archive.org/web/20201124123950/https://www.portainer.io/2020/04/portainer-for-kubernetes-in-less-than-60-seconds/" title="undefined">https://web.archive.org/web/20201124123950/https://www.portainer.io/2020/04/portainer-for-kubernetes-in-less-than-60-seconds/</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/51511547/empty-address-kubernetes-ingress" title="undefined">https://stackoverflow.com/questions/51511547/empty-address-kubernetes-ingress</a></li><li class=""><a class="underline" href="https://rancher.com/docs/k3s/latest/en/quick-start/" title="null">k3s 官方文档</a></li><li class=""><a class="underline" href="https://www.reddit.com/r/kubernetes/comments/be0415/k3s_minikube_or_microk8s/" title="undefined">https://www.reddit.com/r/kubernetes/comments/be0415/k3s_minikube_or_microk8s/</a></li><li class=""><a class="underline" href="https://brennerm.github.io/posts/minikube-vs-kind-vs-k3s.html" title="undefined">https://brennerm.github.io/posts/minikube-vs-kind-vs-k3s.html</a></li><li class=""><a class="underline" href="https://kauri.io/38-install-and-configure-a-kubernetes-cluster-with/418b3bc1e0544fbc955a4bbba6fff8a9/a" title="null">非常全的一份文档</a></li><li class=""><a class="underline" href="https://github.com/k3s-io/k3s/issues/2403" title="undefined">https://github.com/k3s-io/k3s/issues/2403</a></li><li class=""><a class="underline" href="https://blog.oddbit.com/post/2018-02-27-listening-for-connections-on-a/" title="undefined">https://blog.oddbit.com/post/2018-02-27-listening-for-connections-on-a/</a></li><li class=""><a class="underline" href="https://mp.weixin.qq.com/s/H6j8cIHc34zXPM0SPM6Ycg" title="null">k3s with external etcd</a></li></ol>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[FastAPI 中的依赖注入和插件系统]]></title>
            <link>https://yifei.me/note/2428</link>
            <guid>https://yifei.me/note/2428</guid>
            <pubDate>Sat, 23 Apr 2022 01:06:29 GMT</pubDate>
            <description><![CDATA[<p class="my-4 font-light">依赖注入用于把一些可复用的逻辑抽离出来，减少代码重复。例如，返回列表的 API 中都会用到 page
和 page_size 这几个参数，那么可以创建一个依赖来包含这两个参数。</p><p class="my-4 font-light">还可以使用依赖来做插件，如在依赖中做一些 Token 的验证等等。所以，在 FastAPI 中，不需要
插件系统，只要把要复用的逻辑实现为一个依赖就好了。</p><h2 class="my-2 font-semibold text-xl">定义一个依赖</h2><p class="my-4 font-light">依赖的定义是一个 callable, 也就是说函数或者类都可以。依赖的参数和每一个 handler 的参数都
一样，所以 GET/POST/Cookie/Header 等参数都可以以相同的方式使用。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 使用函数作为依赖</span>
<span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> Depends

<span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">pagination</span>(<span class="hljs-params">page: <span class="hljs-built_in">int</span>, size: <span class="hljs-built_in">int</span></span>):
    <span class="hljs-keyword">return</span> {<span class="hljs-string">&quot;page&quot;</span>: page, <span class="hljs-string">&quot;size&quot;</span>: size}

<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/users&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_users</span>(<span class="hljs-params">pagination: <span class="hljs-built_in">dict</span>=Depends(<span class="hljs-params">pagination</span>)</span>):
    users = user_model.get(**pagination)
    <span class="hljs-keyword">return</span> users

<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/items&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_items</span>(<span class="hljs-params">pagination: <span class="hljs-built_in">dict</span>=Depends(<span class="hljs-params">pagination</span>)</span>):
    items = items_model.get(**pagination)
    <span class="hljs-keyword">return</span> items</code></pre><p class="my-4 font-light">使用类有一个好处，你可以把这个类作为类型注释，这样和其他的参数使用方式更加一致。但是也有
一个缺点，那就是函数的 <code class="px-1 bg-gray-100 border-2 rounded">__init__</code> 必须是 sync 的，可能会在线程池中执行。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">class</span> <span class="hljs-title class_">Pagination</span>:
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, page: <span class="hljs-built_in">int</span>, size: <span class="hljs-built_in">int</span></span>):
        self.page = page
        self.size = size

<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/users&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_users</span>(<span class="hljs-params">pagination: Pagination=Depends(<span class="hljs-params">Pagination</span>)</span>):
    users = user_model.get(**pagination)
    <span class="hljs-keyword">return</span> users

<span class="hljs-comment"># 比较巧妙的一点，我们可以省掉 Depends 的参数</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_users</span>(<span class="hljs-params">pagination: Pagination=Depends(<span class="hljs-params"></span>)</span>):
    ...</code></pre><p class="my-4 font-light">Pydantic 的 model 也可以作为依赖直接使用，这样就相当于把 GET 参数作为一个 model 了。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">class</span> <span class="hljs-title class_">Pagination</span>(<span class="hljs-title class_ inherited__">BaseModel</span>):
    page: <span class="hljs-built_in">int</span>
    page_size: <span class="hljs-built_in">int</span>

<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/items&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">list_items</span>(<span class="hljs-params">pagination: Pagination = Depends(<span class="hljs-params"></span>)</span>):
    ...</code></pre><h2 class="my-2 font-semibold text-xl">添加依赖</h2><p class="my-4 font-light">依赖可以在三个地方添加：handler 函数参数，路径装饰器，全局 app 实例。如果在 handler 函数的
参数中添加，那么依赖的返回值会作为参数传递进去，就像其他参数一样。其他两种方式返回值都会被丢弃。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI, APIRouter, Depends

<span class="hljs-keyword">class</span> <span class="hljs-title class_">Dependable</span>:
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, ...</span>):
        ...

<span class="hljs-comment"># 全局依赖</span>
app = FastAPI(dependencies=[Depends(Dependable), ...])

<span class="hljs-comment"># 在路径装饰器中添加一个依赖数组</span>
<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/&quot;</span>, dependencies=[Depends(<span class="hljs-params">Dependable</span>), ...]</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">home</span>(<span class="hljs-params">param: Dependable = Depends(<span class="hljs-params"></span>)</span>):   <span class="hljs-comment"># 在 handler 参数中使用</span>
    ...

<span class="hljs-comment"># 在 Router 中使用</span>
router = APIRouter(dependencies=[Depends(Dependable)])
</code></pre><h2 class="my-2 font-semibold text-xl">依赖依赖依赖</h2><p class="my-4 font-light">依赖还可以有依赖，也就是依赖的参数也可以是 Depends</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>def get<span class="hljs-constructor">_user(<span class="hljs-params">username</span>: <span class="hljs-params">int</span>, <span class="hljs-params">token</span>: <span class="hljs-params">str</span> = Depends(<span class="hljs-params">verify_token</span>)</span>):
    ...</code></pre><h2 class="my-2 font-semibold text-xl">例子</h2><h3 class="my-2 font-semibold ">读取固定参数</h3><p class="my-4 font-light">见前边 pagination 的例子</p><h3 class="my-2 font-semibold ">验证 Token</h3><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> Depends, FastAPI, Header, HTTPException

<span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">verify_token</span>(<span class="hljs-params">x_token: <span class="hljs-built_in">str</span> = Header(<span class="hljs-params">...</span>)</span>):
    <span class="hljs-keyword">if</span> x_token != <span class="hljs-string">&quot;fake-super-secret-token&quot;</span>:
        <span class="hljs-keyword">raise</span> HTTPException(status_code=<span class="hljs-number">400</span>, detail=<span class="hljs-string">&quot;X-Token header invalid&quot;</span>)

app = FastAPI(dependencies=[Depends(verify_token)])</code></pre><p class="my-4 font-light">这个例子可以进一步扩展成使用 session.</p><h3 class="my-2 font-semibold ">加载数据库连接</h3><p class="my-4 font-light">我们需要使用 yield 来返回创建的实例，当 handler 函数执行完毕之后，yield 后边的语句才会
继续执行（关闭数据库）。请特别注意，不要在这里执行 commit，应该由业务代码在 return 之前
执行，否则可能会造成给前端返回的数据不一致的问题。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">get_db</span>():
    db = DBSession()
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">yield</span> db
    <span class="hljs-keyword">except</span> Exception:
        db.rollback()
        <span class="hljs-keyword">raise</span>
    <span class="hljs-keyword">finally</span>:
        db.close()</code></pre><p class="my-4 font-light">如果是 redis，可以这样：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>REDIS_URL = <span class="hljs-string">&quot;redis://localhost:6379/0&quot;</span>
redis_pool = ConnectionPool.from_url(REDIS_URL)

<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_redis</span>():
    <span class="hljs-keyword">return</span> Redis(connection_pool=redis_pool)</code></pre><h2 class="my-2 font-semibold text-xl">依赖去重</h2><p class="my-4 font-light">当依赖之间有依赖关系的时候，可能会出现有两个依赖同时调用同一个上级依赖的问题，有点类似
继承中的多继承关系。</p><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://stackoverflow.com/questions/65243587/fastapi-async-class-dependencies" title="undefined">https://stackoverflow.com/questions/65243587/fastapi-async-class-dependencies</a></li><li class=""><a class="underline" href="https://github.com/tiangolo/fastapi/issues/754" title="null">Session 相关 Issue</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/18022767/python-redis-connection-should-be-closed-on-every-request-flask" title="undefined">https://stackoverflow.com/questions/18022767/python-redis-connection-should-be-closed-on-every-request-flask</a></li></ol>]]></description>
            <content:encoded><![CDATA[<p class="my-4 font-light">依赖注入用于把一些可复用的逻辑抽离出来，减少代码重复。例如，返回列表的 API 中都会用到 page
和 page_size 这几个参数，那么可以创建一个依赖来包含这两个参数。</p><p class="my-4 font-light">还可以使用依赖来做插件，如在依赖中做一些 Token 的验证等等。所以，在 FastAPI 中，不需要
插件系统，只要把要复用的逻辑实现为一个依赖就好了。</p><h2 class="my-2 font-semibold text-xl">定义一个依赖</h2><p class="my-4 font-light">依赖的定义是一个 callable, 也就是说函数或者类都可以。依赖的参数和每一个 handler 的参数都
一样，所以 GET/POST/Cookie/Header 等参数都可以以相同的方式使用。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-comment"># 使用函数作为依赖</span>
<span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> Depends

<span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">pagination</span>(<span class="hljs-params">page: <span class="hljs-built_in">int</span>, size: <span class="hljs-built_in">int</span></span>):
    <span class="hljs-keyword">return</span> {<span class="hljs-string">&quot;page&quot;</span>: page, <span class="hljs-string">&quot;size&quot;</span>: size}

<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/users&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_users</span>(<span class="hljs-params">pagination: <span class="hljs-built_in">dict</span>=Depends(<span class="hljs-params">pagination</span>)</span>):
    users = user_model.get(**pagination)
    <span class="hljs-keyword">return</span> users

<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/items&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_items</span>(<span class="hljs-params">pagination: <span class="hljs-built_in">dict</span>=Depends(<span class="hljs-params">pagination</span>)</span>):
    items = items_model.get(**pagination)
    <span class="hljs-keyword">return</span> items</code></pre><p class="my-4 font-light">使用类有一个好处，你可以把这个类作为类型注释，这样和其他的参数使用方式更加一致。但是也有
一个缺点，那就是函数的 <code class="px-1 bg-gray-100 border-2 rounded">__init__</code> 必须是 sync 的，可能会在线程池中执行。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">class</span> <span class="hljs-title class_">Pagination</span>:
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, page: <span class="hljs-built_in">int</span>, size: <span class="hljs-built_in">int</span></span>):
        self.page = page
        self.size = size

<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/users&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_users</span>(<span class="hljs-params">pagination: Pagination=Depends(<span class="hljs-params">Pagination</span>)</span>):
    users = user_model.get(**pagination)
    <span class="hljs-keyword">return</span> users

<span class="hljs-comment"># 比较巧妙的一点，我们可以省掉 Depends 的参数</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_users</span>(<span class="hljs-params">pagination: Pagination=Depends(<span class="hljs-params"></span>)</span>):
    ...</code></pre><p class="my-4 font-light">Pydantic 的 model 也可以作为依赖直接使用，这样就相当于把 GET 参数作为一个 model 了。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">class</span> <span class="hljs-title class_">Pagination</span>(<span class="hljs-title class_ inherited__">BaseModel</span>):
    page: <span class="hljs-built_in">int</span>
    page_size: <span class="hljs-built_in">int</span>

<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/items&quot;</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">list_items</span>(<span class="hljs-params">pagination: Pagination = Depends(<span class="hljs-params"></span>)</span>):
    ...</code></pre><h2 class="my-2 font-semibold text-xl">添加依赖</h2><p class="my-4 font-light">依赖可以在三个地方添加：handler 函数参数，路径装饰器，全局 app 实例。如果在 handler 函数的
参数中添加，那么依赖的返回值会作为参数传递进去，就像其他参数一样。其他两种方式返回值都会被丢弃。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> FastAPI, APIRouter, Depends

<span class="hljs-keyword">class</span> <span class="hljs-title class_">Dependable</span>:
    <span class="hljs-keyword">def</span> <span class="hljs-title function_">__init__</span>(<span class="hljs-params">self, ...</span>):
        ...

<span class="hljs-comment"># 全局依赖</span>
app = FastAPI(dependencies=[Depends(Dependable), ...])

<span class="hljs-comment"># 在路径装饰器中添加一个依赖数组</span>
<span class="hljs-meta">@app.get(<span class="hljs-params"><span class="hljs-string">&quot;/&quot;</span>, dependencies=[Depends(<span class="hljs-params">Dependable</span>), ...]</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">home</span>(<span class="hljs-params">param: Dependable = Depends(<span class="hljs-params"></span>)</span>):   <span class="hljs-comment"># 在 handler 参数中使用</span>
    ...

<span class="hljs-comment"># 在 Router 中使用</span>
router = APIRouter(dependencies=[Depends(Dependable)])
</code></pre><h2 class="my-2 font-semibold text-xl">依赖依赖依赖</h2><p class="my-4 font-light">依赖还可以有依赖，也就是依赖的参数也可以是 Depends</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>def get<span class="hljs-constructor">_user(<span class="hljs-params">username</span>: <span class="hljs-params">int</span>, <span class="hljs-params">token</span>: <span class="hljs-params">str</span> = Depends(<span class="hljs-params">verify_token</span>)</span>):
    ...</code></pre><h2 class="my-2 font-semibold text-xl">例子</h2><h3 class="my-2 font-semibold ">读取固定参数</h3><p class="my-4 font-light">见前边 pagination 的例子</p><h3 class="my-2 font-semibold ">验证 Token</h3><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">from</span> fastapi <span class="hljs-keyword">import</span> Depends, FastAPI, Header, HTTPException

<span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">verify_token</span>(<span class="hljs-params">x_token: <span class="hljs-built_in">str</span> = Header(<span class="hljs-params">...</span>)</span>):
    <span class="hljs-keyword">if</span> x_token != <span class="hljs-string">&quot;fake-super-secret-token&quot;</span>:
        <span class="hljs-keyword">raise</span> HTTPException(status_code=<span class="hljs-number">400</span>, detail=<span class="hljs-string">&quot;X-Token header invalid&quot;</span>)

app = FastAPI(dependencies=[Depends(verify_token)])</code></pre><p class="my-4 font-light">这个例子可以进一步扩展成使用 session.</p><h3 class="my-2 font-semibold ">加载数据库连接</h3><p class="my-4 font-light">我们需要使用 yield 来返回创建的实例，当 handler 函数执行完毕之后，yield 后边的语句才会
继续执行（关闭数据库）。请特别注意，不要在这里执行 commit，应该由业务代码在 return 之前
执行，否则可能会造成给前端返回的数据不一致的问题。</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code><span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">get_db</span>():
    db = DBSession()
    <span class="hljs-keyword">try</span>:
        <span class="hljs-keyword">yield</span> db
    <span class="hljs-keyword">except</span> Exception:
        db.rollback()
        <span class="hljs-keyword">raise</span>
    <span class="hljs-keyword">finally</span>:
        db.close()</code></pre><p class="my-4 font-light">如果是 redis，可以这样：</p><pre class="text-sm bg-gray-100 my-4 p-2 overflow-x-auto"><code>REDIS_URL = <span class="hljs-string">&quot;redis://localhost:6379/0&quot;</span>
redis_pool = ConnectionPool.from_url(REDIS_URL)

<span class="hljs-keyword">def</span> <span class="hljs-title function_">get_redis</span>():
    <span class="hljs-keyword">return</span> Redis(connection_pool=redis_pool)</code></pre><h2 class="my-2 font-semibold text-xl">依赖去重</h2><p class="my-4 font-light">当依赖之间有依赖关系的时候，可能会出现有两个依赖同时调用同一个上级依赖的问题，有点类似
继承中的多继承关系。</p><h2 class="my-2 font-semibold text-xl">参考</h2><ol class="my-4 ml-4 font-light list-decimal"><li class=""><a class="underline" href="https://stackoverflow.com/questions/65243587/fastapi-async-class-dependencies" title="undefined">https://stackoverflow.com/questions/65243587/fastapi-async-class-dependencies</a></li><li class=""><a class="underline" href="https://github.com/tiangolo/fastapi/issues/754" title="null">Session 相关 Issue</a></li><li class=""><a class="underline" href="https://stackoverflow.com/questions/18022767/python-redis-connection-should-be-closed-on-every-request-flask" title="undefined">https://stackoverflow.com/questions/18022767/python-redis-connection-should-be-closed-on-every-request-flask</a></li></ol>]]></content:encoded>
        </item>
    </channel>
</rss>