<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>lanzhiheng (蓝智恒)</title>
    <link>https://ruby-china.org/lanzhiheng</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>图片是怎么被搜索出来的？</title>
      <description>&lt;p&gt;相信大伙平时都有搜索图片的经验，我们只要输入文字，就能通过搜索引擎搜索出相关的图片。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/lanzhiheng/d8e580b9-0487-4042-afa7-a5cfa75bc1ee.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;这是因为网站上很多的图片其实都被标签化了，我们搜索图片的时候实际上是匹配它们的标签，本质上是通过语义化的匹配来实现。&lt;/p&gt;

&lt;p&gt;不知道大家有没有发现，其实我们的相册也有类似的功能。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/lanzhiheng/a2b013f1-1407-434f-bea8-ccf31d82ab00.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;这就有点神奇了哈，我们并没有对我们的照片进行标签化，机器是怎么做到对我们的照片进行自动归类的？&lt;/p&gt;

&lt;p&gt;以我有限的认知来说，其实可以通过语义化向量的方式去实现。我们抛开大模型的能力先不讨论，毕竟粗暴一点其实我们完全可以让大模型去遍历所有图片，然后从文字总结中抽取标签。然而这种方式成本太高了，目前效率也比较低。&lt;/p&gt;

&lt;p&gt;针对图片的向量化，我们其实可以借助目前比较流行的 CLIP，它是一个能够同时理解图片以及文字的预训练模型，&lt;strong&gt;主要是通过对比学习（Contrastive Learning）的方式。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;其实你可以把它看成是一个经过训练，并且能理解图片内容的一个模型，它以“文字”的形式（语义）对图片的内容进行总结。&lt;/p&gt;

&lt;p&gt;CLIP 可以通过向量化的方式把图片的语义进行存储，有点类似我们熟悉的标签。当然这种“文字”对普通用户来说是黑盒子，我们无从得知。我们把“cat”当作一个语义，下次遇到语义相近的图片，就能把它们归成一类。&lt;/p&gt;

&lt;p&gt;这里我准备三张图片：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/lanzhiheng/d42ab07d-d7cf-407d-848d-7dce7c3dca97.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;分别是大草坪，猫咪还有一个玉石。根据 CLIP 的官方文档，我们可以通过编写 Python 脚本对图片进行向量化。&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;

&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cuda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cpu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;preprocess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ViT-B/32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 分别读取3张图，预处理
&lt;/span&gt;
&lt;span class="n"&gt;image1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;preprocess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./image/image1.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;image2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;preprocess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./image/image2.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;image3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;preprocess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./image/image3.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 将所有图片合并成一个批次
&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;image1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 搜索关键词 cat
&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cat&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# 获取图片和文字的特征
&lt;/span&gt;    &lt;span class="n"&gt;image_features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text_features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 归一化后的余弦相似度
&lt;/span&gt;    &lt;span class="n"&gt;image_features_norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image_features&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;image_features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text_features_norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text_features&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;text_features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cosine_similarities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_features_norm&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;text_features_norm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;squeeze&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# 用 tensor 计算 logits
&lt;/span&gt;    &lt;span class="n"&gt;logit_scale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logit_scale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logit_scale&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;cosine_similarities&lt;/span&gt;

    &lt;span class="c1"&gt;# 转换为 numpy 用于打印
&lt;/span&gt;    &lt;span class="n"&gt;cosine_similarities_np&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cosine_similarities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;归一化的余弦相似度&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image1: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cosine_similarities_np&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image2: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cosine_similarities_np&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image3: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cosine_similarities_np&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Softmax概率分布
&lt;/span&gt;    &lt;span class="n"&gt;probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;cpu&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Softmax概率分布&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image1: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image2: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image3: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;probs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述代码中，我分别对三张图片进行向量化，假设我的搜索关键词是“cat”，那么关键的代码就是&lt;/p&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cosine_similarities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_features_norm&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;text_features_norm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;squeeze&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大致可以理解成，从图片的角度来看（有三张图片）我跟“cat”这个单词的匹配度如何。最终打印出来的结果大概是这样：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/lanzhiheng/93f6984e-0840-4b04-9795-86f81238499f.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;从归一化余弦相似度来看，image2 的得分最高，觉得自己跟“cat”这个单词较为匹配。更直观的我们可以看下面的 softmax，image2 有 99.94% 的几率跟“cat”匹配。&lt;/p&gt;

&lt;p&gt;我们只要设置一个合适的阀值，就能把其他两个不相干的筛掉，留下 image2，作为“cat”搜索的结果。&lt;/p&gt;

&lt;p&gt;假设说我们能够把图库里面所有图片都进行语义的向量化，构建一个本地的向量数据库，其实就能够比较容易地构建出一个图片搜索工具了。&lt;/p&gt;

&lt;p&gt;当然，这只是计算机视觉比较基础的应用场景，后面我会分享更多的心得体会。几天前我用 Cursor 生成了一个图片搜索工具，具体可以参考我的 Github: &lt;a href="https://github.com/lanzhiheng/vm_search" rel="nofollow" target="_blank"&gt;https://github.com/lanzhiheng/vm_search&lt;/a&gt; 。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;公众号：CXO 成长记&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Tue, 17 Feb 2026 15:08:10 +0800</pubDate>
      <link>https://ruby-china.org/topics/44481</link>
      <guid>https://ruby-china.org/topics/44481</guid>
    </item>
    <item>
      <title>给回流技术部门的第三封信</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/lanzhiheng/19695f5a-4257-407d-ba9a-e735f647a26a.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;也挺久没给部门的人写信了，该表达的，今天开会的也通过语音表达过了。以免大伙忘记，方便回顾，所以整理成信件形式，方便大伙及自己翻阅。&lt;/p&gt;
&lt;h2 id="没有年终奖"&gt;没有年终奖&lt;/h2&gt;
&lt;p&gt;2025 年已经确定是没有年终奖的了，不过公司会适当发放过节费。有些新同事可能不知道，年终奖在回流是稀罕物，我们在回流这么多年，能拿年中奖金的年份也是屈指可数。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;不过我之前也给大伙讲过了，你们没有，那我也不可能会有。&lt;/strong&gt;这次的过节费会根据入职年限来发放，老员工可能多一些，新员工会少一些。&lt;/p&gt;

&lt;p&gt;钱虽少，也算是公司一点心意吧。这也侧面反映，其实公司的情况并没有大伙想象的那么好，请大伙不要把这当成养老的地方。&lt;/p&gt;
&lt;h2 id="学习对象"&gt;学习对象&lt;/h2&gt;
&lt;p&gt;前不久友进问我，年底了能不能少点事情？我的回答是：想都别想。我知道深圳办公室的同事看到其他部门可能没那么忙的样子，也想着产研这边是不是也可以轻松点。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;那是绝对不可能的，其他部门也不是我们该学习的对象。&lt;/strong&gt;他们忙不忙有他们自己的安排，跟我们有啥关系？产研部门都是干到最后一天的。&lt;/p&gt;

&lt;p&gt;产研部门对比起其他部门来说已经是有点特权性质的了。大伙可以回看整个公司的其他部门，有几个部门能远程办公，弹性上下班，团建的时候老板们也赏脸参加，年会即便不参加也没有强制性要求的？反正我是没发现这种部门了，除了咱们之外。&lt;/p&gt;

&lt;p&gt;正是因为有这种相互之间的信任存在，我才要求大伙的专业度要持续在线，工作尽可能饱和。我们真要学习为什么不参考国外的优秀的技术团队呢？还要更轻松的话，那都不用干活了。&lt;/p&gt;
&lt;h2 id="“老虎”没了牙？"&gt;“老虎”没了牙？&lt;/h2&gt;
&lt;p&gt;前不久杰哥跟我说，好久没听我骂人了，觉得有点不习惯。确实，今年来说我的骂人次数屈指可数，脾气呢看着是好些了，然而并不代表我底线都丢了。&lt;/p&gt;

&lt;p&gt;小艾有时候也说我，管理太过于“柔”，可以强硬点。她觉得宽松可以，但是底线必须得亮出来。&lt;/p&gt;

&lt;p&gt;今天我也想给大伙亮一下我的底线，我个人是希望能够在一个简单，宽松的氛围下工作的。没有那么多勾心斗角，斤斤计较，大伙能够针对某个事情，一起去解决一个个的问题。这也要求大伙能够在工作中培养相互之间的信任。&lt;/p&gt;

&lt;p&gt;宽松的环境容易滋生懒惰，这点我很清楚，我也理解没有人能一直状态绝佳，偶尔状态下滑是人之常情。我就希望当某个同事状况不佳的时候，工作不够积极的时候，相互间能够提个醒。如果冥顽不灵，最后被我这边发现了，那就没那么客气了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;一个人持续的状态不佳，态度消极，不愿意配合他人工作，容易拖垮整个团队，导致团队渐渐失去凝聚力跟创造力，我断难容忍，这也是我的底线所在。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;不管大伙信不信，我对这个团队的重视程度远超公司的其他，甚至超过马总跟新哥他们，所以我希望在这点上大伙不要试探我的底线。&lt;/p&gt;

&lt;p&gt;当然，如果老员工出现仗着自己资格老欺负新员工，拒绝配合新员工工作的情况，也请反馈给我，这点绝不姑息。&lt;/p&gt;
&lt;h2 id="刺头"&gt;刺头&lt;/h2&gt;
&lt;p&gt;我是不希望团队里面引入刺头的，一般来说刺头可能有一定的能力，但是自视甚高，会给团队带来极其负面的影响。&lt;/p&gt;

&lt;p&gt;我曾经跟兴民一起面试过一个图像搜索的工程师，他看着比较有经验，能回答出很多问题。然而我们发现此人不够诚恳，套路多，时间观念很差（约了三次才约上），习惯性让他人配合他工作。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;我们并不想招聘一个大爷来供着，最后没有接纳。&lt;/strong&gt;毕竟我们不相信一个不愿意配合他人，没有同理心的人，专业能力能出色到哪里去。&lt;/p&gt;

&lt;p&gt;即便他专业能力再出色，如果不是一个愿意配合的人，也不太可能在日常工作中产生出色成果。&lt;/p&gt;

&lt;p&gt;我明白，现在很多国外的职场故事都让我们要有一定的容忍度，尤其是对那种天赋型人才，要允许他们有发挥自己个性的地方。&lt;/p&gt;

&lt;p&gt;然而，我以为，&lt;strong&gt;个性并不等于可以随性&lt;/strong&gt;，如果一个人个性过分突出，导致会伤害到其他人，我很难相信他能对团队产生什么正向收益。&lt;/p&gt;
&lt;h2 id="新方向的探索"&gt;新方向的探索&lt;/h2&gt;
&lt;p&gt;说句实在话，我感觉老同事在新技术，新领域的探索度确实力度不够。我们这帮人也没有年轻一辈那样对新技术充满热情。&lt;/p&gt;

&lt;p&gt;我觉得这并不是一个好的信号，当然了，每个人性格都不同，有些老同事可能对新技术不够敏感，但是能把事情做得非常出色。&lt;/p&gt;

&lt;p&gt;就拿云逸来说，他很早之前就给我提过，他不怎么喜欢做调研性的工作，他会很纠结，很痛苦。然而如果我们把一个笃定的技术路线，庞大的业务功能交给他，基本上可以不用操心任何事情，他能把东西完成得相当漂亮，还能发现很多我们之前没想到的漏洞。&lt;/p&gt;

&lt;p&gt;即便如此，我还是希望老一辈的同事明年开始能够多去探索一些新的技术，一些新的方向，前提当然是要自己感兴趣的。如果这些兴趣能够用在公司的产品上，那就更好了。&lt;/p&gt;

&lt;p&gt;在新技术探索这块，我感觉锐新就做得比较好，值得我们老一辈的同事学习。他很乐于接触并分享一些新的东西，所以明年我想让锐新去给兴民打一段时间的下手，在那里，他能够在实战中运用一些新的模型，算法，说不定大有裨益。&lt;/p&gt;
&lt;h2 id="AI时代"&gt;AI 时代&lt;/h2&gt;
&lt;p&gt;AI 时代来了，我们该如何自处呢？虽然我每次都要求大伙能够拥抱 AI，更多地依赖 AI 编程，然而我知道真正能够奋力去拥抱的，还是少数。&lt;/p&gt;

&lt;p&gt;明年开始我会督促大伙的，我会要求同样时间内有更高的产出，当然我也会安排更多 AI 相关的工作给到大伙，尽量让每个人都能在实战中接触 AI。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI 时代的来临，是生产力的解放，但我们更应该拥抱作为人类最重要的创造力。创造力才是我们人类最宝贵的东西。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;就目前来说，很多产品运营的新想法，都是来源于产品设计同学，这点是不够的，在座的各位都是聪明人，我相信你们都有很多奇思妙想，只是羞于表达。&lt;/p&gt;

&lt;p&gt;希望明年大伙能够从公司维度提出更多可以优化的新想法。比如说这次佳哥提出的弹性首页优化方案，就是个相当不错的做法，让我们首页日后的定制能力更强。&lt;/p&gt;
&lt;h2 id="不要焦虑，也不要飘"&gt;不要焦虑，也不要飘&lt;/h2&gt;
&lt;p&gt;大伙不要焦虑，不要觉得 AI 来了，可能会威胁到自己的饭碗。&lt;/p&gt;

&lt;p&gt;我知道市面上有太多鼓吹 AI 的媒体，会让我们觉得自己落后于时代。总觉得自己不会 XX 工具，就要落后于人。&lt;/p&gt;

&lt;p&gt;我们应该把它当成一个工具，一个寻常的工具，有了它我们能少干很多体力活，有更多的思考空间。&lt;/p&gt;

&lt;p&gt;我们可以更多地考虑业务，考虑流程，考虑架构，希望你们明年能更多地跟产品同学，跟我探讨一些新技术方案的可行性，然后一起去落地。&lt;/p&gt;

&lt;p&gt;AI 应该是创造力激发工具，我们有更多时间去思考真正重要，需要人类去解决的事情，它并不是人类的终结者。&lt;/p&gt;

&lt;p&gt;另外一个需要警惕的是，AI 以后人人都可以用，并不是什么值得吹嘘的东西。这玩意我妈都会用，我们作为技术人，真的没什么大不了的。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;所以千万不要觉得我们会用 AI，我们做了几个 AI 的功能就觉得自己多了不起。千万不要有这种想法。针对 AI 我希望大伙能够以平常心看待。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="最后"&gt;最后&lt;/h2&gt;
&lt;p&gt;平时我很少给大伙讲这么多，这次我说话也说得比较直，有点凶，但这也是我原本的样子。希望今天说的大伙都能听进去。&lt;/p&gt;

&lt;p&gt;新同事可以进一步了解一下我们团队是什么样的，老同事，尤其是远程的同学，不要以养老的心态来对待往后的工作。&lt;/p&gt;

&lt;p&gt;后面我们会更忙的，希望大伙还是能以创业团队的心态来对待后续的工作。如果要养老的话，这个团队并不适合你。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;博客链接： &lt;a href="https://step-by-step.tech/posts/the-third-letter-for-huiliu-tech-team" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/the-third-letter-for-huiliu-tech-team&lt;/a&gt;
公众号：CXO 成长记&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Fri, 06 Feb 2026 22:23:48 +0800</pubDate>
      <link>https://ruby-china.org/topics/44474</link>
      <guid>https://ruby-china.org/topics/44474</guid>
    </item>
    <item>
      <title>敏捷信徒 OR 叛徒</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/lanzhiheng/088dda08-fde0-40f3-80bc-6c73126cd5d3.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;最近重新翻开了一本 10 年前买的书，它叫做《程序员之禅》，这篇文章我暂时不打算写这本书的读后感，我们展开讲讲它里面提到的关于敏捷的话题。&lt;/p&gt;

&lt;p&gt;书中有一个篇章劝我们，不要变成一个极端主义者，它提到，敏捷工作流容易让每个问题都变成钉子。它反问我们：&lt;strong&gt;如果毕加索用 scrum 进行工作，你很难想象他的作品会变成什么样子。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;不可否认的是，敏捷工作流是一个很优秀的工作流。如果从投资的角度讲，他可能是把事情做对的好工具，然而并不一定有利于使用者找到对的事情去做。&lt;/p&gt;

&lt;p&gt;请允许我用自身的经历谈谈它，从大学开始我就是敏捷的忠实信徒，那时候我对于很多大厂，大设计的瀑布流工作方式嗤之以鼻（那时候也是有点太无知了）。&lt;/p&gt;

&lt;p&gt;所幸，毕业之后去的两家公司都比较崇尚敏捷工作方式（回流是第三家企业），我也在这些公司获益良多。&lt;/p&gt;

&lt;p&gt;在前几年的工作中，我唯一想的一件事情就是变强，我希望能成为一个优秀的开发者，我深信敏捷工作方式能够帮我达成这个目的。&lt;/p&gt;

&lt;p&gt;我深信，只要我足够敏捷，我就能最大化自己的工作产出，我能把自己的工作效率“压榨”到极致，觉得只要自己效率最大化，就距离优秀的程序员更进一步了。&lt;/p&gt;

&lt;p&gt;来了回流之后，由于后端代码都由自己负责，相当于所有东西都自己掌控了，我把以前的遗憾全都补足了。比方说我为项目引入了单元测试，引入了 CI 工作流，给团队制定了每周一个版本的迭代节奏。&lt;/p&gt;

&lt;p&gt;早期我们每周都能交付功能，我对自己的效能很满意，然而也会有瓶颈。那时候我感觉见超的输出完全不如我，还很傲慢地想，怎么就不学学我呢？怎么就不像我那样敏捷呢？可见那个时候我已经成了一个敏捷的极端主义者了。&lt;/p&gt;

&lt;p&gt;那时候我对所有人都很苛刻，包括对老板们（这点很不可思议）。我觉得只要自己足够敏捷，效率足够高，一切都会好的。&lt;/p&gt;

&lt;p&gt;真正让我转变，可能还是扩招之后。那时候可能要一个人对接好几个开发，还要对接产品经理。一个人的效率达到极致已经没有任何意义了。&lt;/p&gt;

&lt;p&gt;当一个团队人员多起来之后，沟通变得越发重要。比起自己效率最大化，不拖团队的后腿更为重要。&lt;/p&gt;

&lt;p&gt;我可以选择有一定敏捷思维的人才，然而我不可能让所有人都如自己一般把敏捷做法视为圭臬，然后都把效率提升到极致。就如同书中劝导的：&lt;strong&gt;毕竟我们都是人，并不是机器。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我发现自己变得越来越不够敏捷了，比起写更多的代码，我可能会花更多时间去正确传达一个需求。我花了更多时间去识别一件事情对不对，而不是如何去把事情做对。如果是一件不对的事情，那么效率越高，灾难越大。&lt;/p&gt;

&lt;p&gt;以前的我以为，只要自己足够敏捷，效率足够高，就能成为一个优秀的程序员。然而我从来没想过，当自己变得不那么敏捷，效率稍微有点降低之后，我反而成了个“更好”的程序员。&lt;/p&gt;

&lt;p&gt;这是一件很反直觉的事情。因为考虑问题的视角不一样，我会花更多时间去识别一个需求的意义，而不是埋头苦干。我们排除了很多没有意义的需求，这也让我能写出更精简的代码。&lt;/p&gt;

&lt;p&gt;写的代码虽然少了，但你很难说这不是一种效率的提升，以前公司要试验一个新的场景，我们都要花一个星期以上做开发才能投入业务试运行。&lt;/p&gt;

&lt;p&gt;后面自己想的却是，到底有没有必要做，没必要做的话，是不是就省了一个星期的时间？真的有必要做的话，有没有可能只要花一个小时做一个小功能就可以让一部分业务跑起来，而不是一上来就开发大功能？&lt;/p&gt;

&lt;p&gt;这次严选的整改，强哥已经设计好整个流程了，开发量大概 1 ～ 2 个月。后面我们反复讨论，有没有可能以现有的流程跑起来先，发现是可以的，大流程完全就不用开发了。&lt;/p&gt;

&lt;p&gt;虽然强哥的工作有点白费，然而最终节省了一个多月的开发量，也不会耽误业务工作，何尝不是一种双赢？然而你说这种算不算敏捷，我也说不准。&lt;/p&gt;

&lt;p&gt;这么看来，自己似乎成了敏捷的叛徒，然而成了敏捷叛徒之后，我似乎成了一个让自己更满意的程序员了，虽然我的代码量并没有早年夸张。其中还有个最大的好处，我对身边的人也没这么苛刻了。&lt;/p&gt;

&lt;p&gt;我曾经为了成为一位优秀的程序员，坚守敏捷信条，然而只有别那么苛责自己去坚守敏捷信条的时候，反而成为一个自己满意的程序员了。&lt;/p&gt;

&lt;p&gt;这点很有意思，有些时候，我们坚信的方法论，并不是通往目标的唯一方式。芒格经常劝导我们：“反过来想，总是反过来想”。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;原文链接： &lt;a href="https://step-by-step.tech/posts/agile_believer_or_traitor" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/agile_believer_or_traitor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;公众号：CTO 成长记&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Wed, 14 Jan 2026 13:42:10 +0800</pubDate>
      <link>https://ruby-china.org/topics/44447</link>
      <guid>https://ruby-china.org/topics/44447</guid>
    </item>
    <item>
      <title>云长科技招聘计算机视觉工程师（图像搜索优化方向）1 ～ 2 名，坐标深圳（适当远程）</title>
      <description>&lt;h2 id="任职资格"&gt;任职资格&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;计算机科学、人工智能或相关领域的本科或硕士学位。&lt;/li&gt;
&lt;li&gt;2-5 年计算机视觉相关经验，专注于图像搜索或类似技术。&lt;/li&gt;
&lt;li&gt;熟练掌握 Python、PyTorch 或 TensorFlow，具备使用 OpenCLIP、SAM 或类似框架的实践经验。&lt;/li&gt;
&lt;li&gt;深入了解向量数据库（例如 Milvus、FAISS）及优化技术。&lt;/li&gt;
&lt;li&gt;具备大规模 CV 模型测试和部署经验。&lt;/li&gt;
&lt;li&gt;出色的解决问题的能力，适应独立工作或团队协作环境。&lt;/li&gt;
&lt;li&gt;加分项：曾在电商图像搜索或多模态 AI 项目中积累经验。熟悉推荐系统及其与视觉数据的集成。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="优先技能"&gt;优先技能&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;了解 AI 伦理和数据隐私标准。&lt;/li&gt;
&lt;li&gt;具备云平台经验（如 阿里云、腾讯云、AWS、Google Cloud）用于模型部署。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="工作职责"&gt;工作职责&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;设计并实现基于 OpenCLIP 向量化和 SAM 分割的图像搜索原型。&lt;/li&gt;
&lt;li&gt;优化搜索性能，包括索引技术（例如 FAISS/HNSW）和检索准确性（例如 recall/precision 指标）。&lt;/li&gt;
&lt;li&gt;进行大规模 A/B 测试和性能调优，确保系统扩展性。&lt;/li&gt;
&lt;li&gt;协作推荐算法团队，将视觉数据集成到现有系统中。&lt;/li&gt;
&lt;li&gt;探索并原型化其他 AI 应用，如多模态推荐或生成式 AI 增强功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="薪资范围"&gt;薪资范围&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;30-50 万元/年&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="关于【回流】"&gt;关于【回流】&lt;/h2&gt;
&lt;p&gt;【回流】是云长科技旗下的一个珠宝玉翠二奢文玩的闲置交易平台，官方网站&lt;a href="https://huiliu.com/" rel="nofollow" target="_blank"&gt;https://huiliu.com/&lt;/a&gt;。整合产业带销售渠道，来为用户的珠宝玉石藏品做公允定价和快速变现。产研部门作为支撑性部门需要不断迭代软件应对不断变化的业务需求。&lt;/p&gt;

&lt;p&gt;目前回流 App 已经在各大应用商店上架（包括鸿蒙商店），注册用户百万左右，日活 1 ~ 2w。除了 App 公司还上线了小程序（可在微信搜索“回流”关键字）以及众多线下人员使用的管理工具。今年年中会逐步扩张海外业务。&lt;/p&gt;
&lt;h2 id="关于云长科技"&gt;关于云长科技&lt;/h2&gt;
&lt;p&gt;云长科技是一个年轻的创业团队，同时云长二字也代表了一种“蓝天白云、高远流长”的美好夙愿和集体人格。我们追求，在做好产品、服务社会、创造经济效益的同时，践行和弘扬我们的核心价值观。我们希望，通过不懈努力和积累，把云长发展成一个受人信赖、基业长青、担当社会责任的优秀企业。&lt;/p&gt;
&lt;h2 id="面试安排"&gt;面试安排&lt;/h2&gt;
&lt;p&gt;外地的朋友，如果想来深圳发展的话，可以先把简历发来 zhiheng@huiliu.net，合适的话可以直接在 &lt;a href="https://showmebug.com/" rel="nofollow" target="_blank"&gt;https://showmebug.com/&lt;/a&gt; 上安排远程面试。
在深圳本地的朋友，也可以选择远程面试。离得比较近的，觉得要亲身面对面聊一下并看看办公环境的话，可以在简历下备注一下，合适的话我们可以约个时间安排线下面试。&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Wed, 22 Oct 2025 14:23:59 +0800</pubDate>
      <link>https://ruby-china.org/topics/44347</link>
      <guid>https://ruby-china.org/topics/44347</guid>
    </item>
    <item>
      <title>云长科技招聘 Ruby 工程师 1 ～ 2 名，坐标深圳（适当远程）</title>
      <description>&lt;h2 id="概况"&gt;概况&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;公司：深圳云长文化科技有限公司&lt;/li&gt;
&lt;li&gt;坐标：深圳市龙华区民治街道樟坑社区樟坑优品文化创意园 1 栋 201&lt;/li&gt;
&lt;li&gt;工作时间：大小周工作制，9:30～10:00 弹性上班，6:00～6:30 弹性下班。在不影响工作的前提下可以酌情申请远程工作（特别是遇到台风，暴雨等天气考虑到同事的安全一般是建议在家办公）。&lt;/li&gt;
&lt;li&gt;福利：公司会配备 Macbook 作为工作电脑，如果自带 Macbook 上班公司会给予一定的补贴。&lt;/li&gt;
&lt;li&gt;薪资水平 20K ~ 30K。&lt;/li&gt;
&lt;li&gt;大小周工作制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="招聘职位：Ruby研发工程师（1～2名）"&gt;招聘职位：Ruby 研发工程师（1～2 名）&lt;/h2&gt;&lt;h2 id="技能要求"&gt;技能要求&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;有三年以上的 Rails 开发经验。&lt;/li&gt;
&lt;li&gt;熟练使用 Git，了解 Git 的一些基本的工作流程，能帮助团队更好地去优化开发流程。&lt;/li&gt;
&lt;li&gt;有写测试的习惯。&lt;/li&gt;
&lt;li&gt;熟悉 Linux 基本操作指令，对服务的搭建与运维有一定的了解。&lt;/li&gt;
&lt;li&gt;熟练使用 Docker 相关的命令，为后面容器化服务做准备。&lt;/li&gt;
&lt;li&gt;熟悉至少一种关系型数据库，并有相关的优化经验。&lt;/li&gt;
&lt;li&gt;有个人博客者优先。&lt;/li&gt;
&lt;li&gt;有责任心以及同理心，这对于团队彼此成员间的合作至关重要。&lt;/li&gt;
&lt;li&gt;善用 Clacky AI，Cursor，Copilot 等 AI 辅助编程工具。&lt;/li&gt;
&lt;li&gt;对 ElasticSearch 有一定的了解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="加分项"&gt;加分项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;对 FineBI，QuickBI，九数云等 BI 工具有一定的了解。&lt;/li&gt;
&lt;li&gt;了解至少一款 OLAP 数据库。&lt;/li&gt;
&lt;li&gt;了解至少一款向量数据库。&lt;/li&gt;
&lt;li&gt;对 Python，Java，Go 等编程语言持包容态度。&lt;/li&gt;
&lt;li&gt;对支付分账有一定的了解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="关于"&gt;关于&lt;/h2&gt;&lt;h2 id="关于“回流” - 鉴宝淘宝上回流"&gt;关于“回流” - 鉴宝淘宝上回流&lt;/h2&gt;
&lt;p&gt;【回流】是云长科技旗下的一个珠宝玉翠二奢文玩的闲置交易平台，官方网站&lt;a href="https://huiliu.com/" rel="nofollow" target="_blank"&gt;https://huiliu.com/&lt;/a&gt;。整合产业带销售渠道，来为用户的珠宝玉石藏品做公允定价和快速变现。产研部门作为支撑性部门需要不断迭代软件应对不断变化的业务需求。&lt;/p&gt;

&lt;p&gt;目前回流 App 已经在各大应用商店上架（包括鸿蒙商店），注册用户百万左右，日活 1 ~ 2w。除了 App 公司还上线了小程序（可在微信搜索“回流”关键字）以及众多线下人员使用的管理工具。今年年中会逐步扩张海外业务。&lt;/p&gt;
&lt;h2 id="关于该职位"&gt;关于该职位&lt;/h2&gt;
&lt;p&gt;目前回流业务使用的后端技术栈主要是 Ruby On Rails，好处在于项目早期可以快速试错，不过业务起来后多少会有点性能问题。&lt;/p&gt;

&lt;p&gt;回流的用户还在不断增长，系统稳定性会越来越重要，需要有一定经验的后端工程师参与到接口开发以及系统运维。需定期排查 SQL 慢查询，异常接口。&lt;/p&gt;

&lt;p&gt;由于平台已经有一定人员规模，也有自己的后台管理系统，后端研发人员除了要提供接口给到 App，还需要提供相当一部分接口给后台管理系统。需要跟测试人员、客户端工程师、产品经理、推荐算法工程师频繁沟通，不定期汇总数据给产品经理，方便各方了解业务情况。&lt;/p&gt;
&lt;h2 id="关于云长科技"&gt;关于云长科技&lt;/h2&gt;
&lt;p&gt;云长科技是一个年轻的创业团队，同时云长二字也代表了一种“蓝天白云、高远流长”的美好夙愿和集体人格。我们追求，在做好产品、服务社会、创造经济效益的同时，践行和弘扬我们的核心价值观。我们希望，通过不懈努力和积累，把云长发展成一个受人信赖、基业长青、担当社会责任的优秀企业。&lt;/p&gt;
&lt;h2 id="面试安排"&gt;面试安排&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;外地的朋友，如果想来深圳发展的话，可以先把简历发来，合适的话可以直接在 &lt;a href="https://showmebug.com/" rel="nofollow" target="_blank"&gt;https://showmebug.com/&lt;/a&gt; 上安排远程面试。&lt;/li&gt;
&lt;li&gt;在深圳本地的朋友，也可以选择远程面试。离得比较近的，觉得要亲身面对面聊一下并看看办公环境的话，可以在简历下备注一下，合适的话我们可以约个时间安排线下面试。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="周边环境"&gt;周边环境&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/lanzhiheng/eda66bc7-710f-46cb-b267-f70397b51915.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/lanzhiheng/27f7bd50-b641-4344-97a2-e8065e162e1f.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/lanzhiheng/da3ec0b1-1a1d-4eac-b60a-5585b7a177e9.png!large" title="" alt=""&gt;
&lt;img src="https://l.ruby-china.com/photo/lanzhiheng/1fb4efdc-cdd1-4707-a67f-7d09febb1978.png!large" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="联系方式"&gt;联系方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;简历邮箱：zhiheng@huiliu.net&lt;/li&gt;
&lt;li&gt;联系人：蓝先生&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Tue, 06 May 2025 16:16:20 +0800</pubDate>
      <link>https://ruby-china.org/topics/44150</link>
      <guid>https://ruby-china.org/topics/44150</guid>
    </item>
    <item>
      <title>给回流技术部门的一封信</title>
      <description>&lt;p&gt;原文链接： &lt;a href="https://step-by-step.tech/posts/a-letter-to-huiliu-tech-team" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/a-letter-to-huiliu-tech-team&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/lanzhiheng/3a4e55bd-3782-45f1-a06b-74148efe20aa.jpg!large" title="" alt=""&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;随着这次 UI 小伙伴的入职，技术团队也基本成型了。虽然有些同事进来没多久，还没转正，不过个人感觉应该问题不大。这次管理层给的压力不小，短短一个多月技术团队人数几乎扩了一倍，确实是想都没想过的事情，也从来都不敢想。&lt;/p&gt;

&lt;p&gt;平时，我们这帮人几乎没有什么机会见面，语音都是聊工作上的事情，大家都有各自的生活，这其实挺好的。我之所以不敢把团队往大了扩，其实也是有点担心，团队一旦壮大，远程这个东西就不好整了。今天，趁着酒劲未过，跟大伙聊聊最近的一些想法。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://step-by-step.oss-cn-beijing.aliyuncs.com/production/ncdje5amr39fg25sks1bb8n2ac4v" title="" alt="DSCF0102.jpg"&gt;&lt;/p&gt;
&lt;h2 id="团队可扩展性问题"&gt;团队可扩展性问题&lt;/h2&gt;
&lt;p&gt;从理论上来说，我们这种类型的团队，可扩展性并不是特别高。远程办公就是一个比较大的变数，其实很难保证进来的每一位成员都有足够的自律能力来胜任远程工作。部门最近来了些人，也开除了一些人，大伙应该更能体会到，回流技术团队需要什么类型的人才，估计也能感受到，团队对人才的态度。&lt;/p&gt;

&lt;p&gt;这种东西其实很难量化，大家只能多多感受了。我有过几次开除人的经历，也咨询过你们当中一些人的意见。开除人不是一件轻松的事情，然而欣慰的是，基本上你们的判断跟我也差不多。事实证明大家还是能 GET 到团队需要的是什么，哪怕这些东西没有白纸黑字写下来。&lt;/p&gt;

&lt;p&gt;进来的人除了要有基本的工作能力之外，人品还要得到保证，情商不能太低。要知道将心比心，懂得尊重人，且能够很好配合其他人的工作。大伙可以考虑一下，这种类型的人，其实要遇到一个都挺难的。这无疑为我们追加人手设置了一定的门槛。&lt;/p&gt;

&lt;p&gt;前段时间跟子珍在闲聊技术团队的未来，我们其实都感觉风险挺大的。随着团队的壮大，要保证团队的人员素质，就是一个比较棘手的问题。也不怕跟大伙说一个鬼故事，我总是说&lt;strong&gt;团队不能再扩了&lt;/strong&gt;，并不是真的因为咱们经费不够。其实管理层给技术部门的预算是很充足的，说到底还是我对自己能力没有足够的信心。&lt;/p&gt;

&lt;p&gt;管理层给予足够的经费是因为对我们有足够的信任，这也是大家这些年努力的结果，这种信任断不能辜负。&lt;strong&gt;由奢入俭难&lt;/strong&gt;，有钱也不能乱花，但该花的钱也不能省就是。团队的扩展，人员的磨合，对于我们这种类型的团队来说难度不小。故而我在招人方面一直都十分克制，否则磨合得不好，即便团队再庞大也只有死路一条。有许多公司大力扩招的之后遇到寒冬，最终不得不裁员，我感觉也有这种因素在。&lt;/p&gt;
&lt;h2 id="远程陷阱"&gt;远程陷阱&lt;/h2&gt;
&lt;p&gt;我需要把一些已知问题暴露到外界。没错，就是要给你们一些压力。咱们部门已经有接近一半的人结婚生子了，许多人都会选择回老家远程办公。老家的生活有多滋润，相信超哥也给你们科普过了，然而其中也有一些需要我们去解决的问题。&lt;/p&gt;

&lt;p&gt;前同事 Edward，曾经跟我讨论过，是相信&lt;strong&gt;人性本善&lt;/strong&gt;还是相信&lt;strong&gt;人性本恶&lt;/strong&gt;。老实说，我还是倾向于相信&lt;strong&gt;人性本善&lt;/strong&gt;的，但是 Edward 的说法是，他曾经也这样，现在他更倾向于&lt;strong&gt;人性本恶&lt;/strong&gt;。我说这些并不是怀疑大家的人品，而是希望大家记住“永远不要考验人性”。&lt;/p&gt;

&lt;p&gt;团队都是基于信任建立起来的，远程团队更是如此。我一直要求大家有事说事，坦诚相待不仅仅是为了维护彼此间的信任，也是为了守住诸位一贯以来的纯粹。然而，长时间远程之后，人难免会产生惰性。可能你们会说，自己不会的。好巧，我以前也觉得自己不会。但是，咱们还是实在点“不要考验人性”。&lt;/p&gt;

&lt;p&gt;长期远程的后果可能是&lt;strong&gt;不思进取&lt;/strong&gt;。我不止一次就这个问题跟你们中的一些人一对一细聊过。就拿回流 App 来说，我们迭代了也有一年多了，业务逻辑基本都得心应手，只要任务安排好，99% 都能完成。但是时间一长，这种&lt;strong&gt;得心应手&lt;/strong&gt;的感觉就是一种陷阱。&lt;/p&gt;

&lt;p&gt;回流是一家传统业务公司，软件更多是辅助形式的，距离真正的科技公司还是有不少的差距。换句话说就是&lt;strong&gt;技术含量不会很高&lt;/strong&gt;。所以你们现有的技能，在 70% ～ 80% 的场景下都能够支撑回流未来几年的业务。然而，这就够了吗？如果因为远程的安逸，大家都不思进取，不去寻找更好的做事方式，不拥抱新技术，无法提供更好用的业务系统来降低公司整体成本。那么，久而久之，回流这种业务对大家而言就是舒适区。&lt;/p&gt;

&lt;p&gt;最近我们在规划很多东西&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;系统升级&lt;/li&gt;
&lt;li&gt;性能优化&lt;/li&gt;
&lt;li&gt;压力测试&lt;/li&gt;
&lt;li&gt;自动化测试&lt;/li&gt;
&lt;li&gt;支付系统变更&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些都能够成为大伙的发力点，也是让自己以后更值钱的点。感觉大伙可以朝这个方向多想想，如何让自己更值钱，也可以适当做一下职业规划。要是某天，我跟老马他们不给力，回流成为了诸位发展瓶颈，导致最终大家都觉得没啥意思而选择离开回流，我第一个支持你们。真到这一天，我也会顺便看看周边朋友的公司有没有适合的岗位可以推荐一下，欢送诸位离开。&lt;/p&gt;

&lt;p&gt;这样也总好过，你们主动摆烂之后，我要亲自开除你们吧？共事了这么长时间，不好下手啊，下了手也很痛心的，希望大伙也别让我们难办才好。你们肯定也希望自己会越来越值钱，这点跟公司的期待没有任何冲突，共同进步可好？&lt;/p&gt;
&lt;h2 id="人人都是CTO"&gt;人人都是 CTO&lt;/h2&gt;
&lt;p&gt;有个网站叫做&lt;a href="https://www.woshipm.com/" rel="nofollow" target="_blank" title=""&gt;人人都是产品经理&lt;/a&gt;，我也借用一下这个标题。相处了这么长时间，大伙应该都知道，回流管理层那帮人都是长期主义者，他们都努力让团队乃至于公司能够长远发展下去。但这并不是一件容易的事情，毕竟影响因素太多了。业务端一两个激进的决策，技术端的服务器崩溃都可能会影响公司的长期运作。再加上，管理层喜欢喝酒这个事实，也像是一个定时炸弹。最近我也喝得不少，子珍也劝我少喝点了，这点我会注意。老实说，我也觉得再这样下去不太行，太影响团队士气了。&lt;/p&gt;

&lt;p&gt;再回到长期主义这个事情，我们也只能先把技术部门能做的事情做好了。我们是远程团队，还是有不小挑战的。近期也有跟子珍探讨过，我们这个团队要怎么才能长期发展呢？我感觉吧，我们需要&lt;strong&gt;有将军思维的士兵&lt;/strong&gt;。大概就是，我希望你们所有人都能有管理思维，哪怕你们以后不打算走管理路线。&lt;/p&gt;

&lt;p&gt;可能有些公司会感觉，程序员，测试，设计师这种岗位只要听话就好了，指哪打哪。不过咱们们公司性质不太一样，只知道听话的人在我们部门其实很难存活下去，也对部门的长期发展不利。安迪·格鲁特在他的书《只有偏执狂才能生存》里面也提到，往往最先发现事情不对劲的都是一线工作者，也就是你们诸位。&lt;/p&gt;

&lt;p&gt;如果你们只听从管理层的安排，发现有问题也不提出疑惑，硬着头皮去干，最后不仅埋下隐患，自己也干得不舒服。久而久之你们肯定也会离开的，然后留下一堆破事给其他同事兜底，老实说这并不是一个合格的回流员工。&lt;/p&gt;

&lt;p&gt;换句话说，也就是需要你们有&lt;strong&gt;主人翁意识&lt;/strong&gt;。只有当我们所有人都具备这种意识，回流技术部门才有机会长期发展，部门才有可能进一步壮大。&lt;/p&gt;

&lt;p&gt;回流这边的管理还算比较扁平化的，发现问题，你们能够很容易地找到我甚至老板提出你们的疑惑。如果意见提过之后，管理层还是一意孤行，那么出问题之后责任就是这帮管理层了。然而，如果知道问题，却选择隐瞒，那便是诸位的问题了。我们部门在这方面一直都做得还可以，希望能够继续坚持下去。&lt;/p&gt;
&lt;h2 id="薪资不好定"&gt;薪资不好定&lt;/h2&gt;
&lt;p&gt;其实每次到涨薪的时候，都是我跟老马，小新哥最头疼的时候。主要也不是因为资金有多短缺，而是&lt;strong&gt;不知道该怎么调整的好&lt;/strong&gt;。许多公司都喜欢给员工画饼说，“每年最高有 20% 的涨幅”，然而，接在后面的文字就很耐人寻味了，“根据个人表现，以及公司发展情况来定”。这点很有意思，个人表现这个到底谁说了算？&lt;/p&gt;

&lt;p&gt;人事部门说了算吗？他们离你们十万八千里远，不可能了解你们工作情况。部门负责人来定吗？我曾经也跟小新哥说过，我们部门哪个人表现不对得起这 20% 的涨幅，但是他也不会因为我一句话就真的给你们涨 20%。作为夹在你们之间的存在，请允许我来说道说道。老实说，公司层面是很难保证给到 20% 的涨幅的。除非公司情况特别好，且收入能够持续增长。否则哪天情况不好了，前期冲的太快，成本太高最终只有裁员一个结果。&lt;/p&gt;

&lt;p&gt;现在享受过 20% 涨幅的，估计也只有一开始入职的时候工资开得太低，后面尽可能调整的小伙伴了吧，这毕竟还是少数。虽然每个公司都明文规定，员工不能私下讨论工资，但是我相信没有多少员工真的当回事。我估计你们相互之间有部分人也是知道彼此工资的，慢慢也会形成比较。&lt;strong&gt;不患寡而患不均啊&lt;/strong&gt;，这并非易事。&lt;/p&gt;

&lt;p&gt;我们曾经想过参考 Netflix 的做法，按照市场的平均水平来定。然而，当我们去看市场的时候就会很疑惑，到底谁代表了市场呢？我们是不可能按照腾讯，阿里的指标来定工资的。根据创业公司的平均薪资水平来定吗？我们跟亚飞公司的薪资水平还是有不少差距的，毕竟他们已经拿了融资。那是不是可以选择，远程且创业型的小公司来参考薪资？嗯，这确实是一个办法。&lt;/p&gt;

&lt;p&gt;老实说，对比外资创业型远程公司来说，我们的薪资远远不够，然而对比国内同类型的公司，我们还算中规中矩的了。有时候也会想着给你们涨幅弄高一点，然而，我也给你们分析过，考虑到长远发展，还是一点点涨上去好。无论是从个人心态，还是公司资金池来看，都是比较健康的做法。这样一来，每次的涨薪都有点盼头，而不是说，一下子就涨得过高，后面几年都没什么动静。&lt;/p&gt;
&lt;h2 id="绩效"&gt;绩效&lt;/h2&gt;
&lt;p&gt;以前政圆还没全职加入的时候，我有咨询过她这个问题。她反应稍微有些激烈，以为我要学市面上很多公司那样，用绩效恶搞技术部门。&lt;/p&gt;

&lt;p&gt;最近亚飞，老马还有一些其他朋友也有跟我提过绩效这个事情。再加上业务那边有体会过绩效系统的好处，都在探讨着是否有可能在技术团队中引入绩效系统。我的意见&lt;a href="https://step-by-step.tech/posts/should-be-measure-in-tech-team" rel="nofollow" target="_blank" title=""&gt;一如既往&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;确实引入了绩效系统之后，你们的工资就没有上限了，而不是像现在这样的死工资。然而，然而，然而.....我们跟业务部门性质不太一样，&lt;strong&gt;不需要这种东西来激励自己努力工作&lt;/strong&gt;。无论是选拔人才，还是培养人才，回流技术部门的人都要求有自驱力，特别是我们这种远程部门，自驱能力更为关键，甚至可以说得上是基础能力。&lt;/p&gt;

&lt;p&gt;本来，你们可以做着自己感兴趣的事情，每个月发工资的时候有个大概的预期，涨工资的时候还能稍微期待一下。一旦绩效系统引入，你们原本花在解决问题的心思，可能得分出一部分去想：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;如何能赚更多的钱。&lt;/li&gt;
&lt;li&gt;如何能少扣点钱。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;樊登老师总结得就很到位，大意是：“当设置了绩效系统，员工每天都在琢磨着怎么赚更多的钱，就越来越少人会想着怎么帮你解决问题，创新点子会越来越少。”我们部门的价值就在于解决问题，原本就是会主动解决问题的主。参合这些东西，只会降低士气，最终搞得人心惶惶，开始计较利益得失，反而影响正常工作，这不是我想要的团队。&lt;/p&gt;

&lt;p&gt;在我有生之年，估计你们都没什么机会在回流技术部见到绩效系统的引入了。我也会说服他们别啥事情都想定到我们这边来，有时候你们一个创新的点子就给公司省下几千到一万块钱的时候，咋没见这帮人来&lt;strong&gt;定责&lt;/strong&gt;呢？该咱们承担的东西，不用说我们部门都会自发去解决，使用绩效没有意义。反而让大家束手束脚无法发挥真正的力量，那对公司来说才是真正的得不偿失。&lt;/p&gt;
&lt;h2 id="尾声"&gt;尾声&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://step-by-step.oss-cn-beijing.aliyuncs.com/production/0vziksb843d5mhfdxmphhkdsmsll" title="" alt="DSCF0109.jpg"&gt;&lt;/p&gt;

&lt;p&gt;今天讲得有点啰嗦了，也就是把最近的所思所想整理成文字分享给诸位，希望想传达的东西都能传达到位。我们都希望回流技术部门能够好好生存下去，但这种事情不好说，关键还是你们自己。&lt;/p&gt;

&lt;p&gt;老实说，当你们强大到，哪怕回流倒闭了，自己都有能力找到心仪工作来养活自己的时候，回流技术部还在不在都不重要了，笔者还是希望你们未来都能有这种能力。反之，到那时候，如果回流还在，你们也愿意留在回流，那才是回流的荣幸呢，到时候回流技术部门只会更强大。&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Thu, 07 Sep 2023 08:52:54 +0800</pubDate>
      <link>https://ruby-china.org/topics/43311</link>
      <guid>https://ruby-china.org/topics/43311</guid>
    </item>
    <item>
      <title>云长科技招聘 Ruby 工程师 1 ～ 2 名，坐标深圳，可远程</title>
      <description>&lt;h2 id="公司概况"&gt;公司概况&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;公司：深圳云长文化科技有限公司&lt;/li&gt;
&lt;li&gt;坐标：深圳市龙华区民治街道龙光玖钻 5A 栋 905 室&lt;/li&gt;
&lt;li&gt;工作时间：大小周工作制，9:30～10:00 弹性上班，6:00～6:30 弹性下班。&lt;/li&gt;
&lt;li&gt;福利：不定期安排下午茶，团建。公司会配备 Macbook 作为工作电脑，如果自带 Macbook 上班，公司会给予一定的补贴。&lt;/li&gt;
&lt;li&gt;工作模式：远程为主。&lt;/li&gt;
&lt;li&gt;薪资水平 15K ~ 25K。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="Ruby研发工程师（1～2名）"&gt;Ruby 研发工程师（1～2 名）&lt;/h2&gt;&lt;h3 id="技能要求"&gt;技能要求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;有两年以上后端研发经验，Ruby On Rails 经验最佳。&lt;/li&gt;
&lt;li&gt;熟练使用 Git，了解 Git 的一些基本的工作流程，能帮助团队更好地去优化开发流程。&lt;/li&gt;
&lt;li&gt;有写测试的习惯，这与邪教无关，主要为了让大家能准时下班。&lt;/li&gt;
&lt;li&gt;熟悉 Linux 基本操作指令，对服务器的搭建与运维有一定的了解。&lt;/li&gt;
&lt;li&gt;能熟练使用 HTML/CSS/JavaScript 开发页面。&lt;/li&gt;
&lt;li&gt;熟练使用 Docker 相关的命令，为后面容器化服务做准备。&lt;/li&gt;
&lt;li&gt;熟悉至少一种关系型数据库，并有相关的优化经验，PostgreSQL 最佳。&lt;/li&gt;
&lt;li&gt;有个人博客者优先。&lt;/li&gt;
&lt;li&gt;具备责任心以及同理心，这对于团队彼此成员间的合作至关重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="关于该职位"&gt;关于该职位&lt;/h2&gt;
&lt;p&gt;主要工作内容是公司相关产品的 API 研发，以及应用部署，数据库优化，兼简单的运维工作。公司现今处于快速发展时期，用户量每天都在稳步增长，在后期应用的体验，服务的稳定性，安全性将会是重中之重。故而，也要求有一定的 SQL/系统优化能力。&lt;/p&gt;
&lt;h2 id="关于团队"&gt;关于团队&lt;/h2&gt;
&lt;p&gt;想要打造一个小型敏捷高产的研发团队，不执着于 KPI，打卡，定点办公。目前团队规模为 10 人左右，在疫情的推动下基本都居家办公。希望以后能够像 Basecamp 那样，在大城市（深圳）设置一个定点办公区域，喜欢到公司的同事能够有个地方可以办公。想要长期远程的同事可以选择自己喜欢的城市生活。当然前提还是不耽误工作，一起看如何把团队往更自由高效的方向带。&lt;/p&gt;
&lt;h2 id="关于云长科技"&gt;关于云长科技&lt;/h2&gt;
&lt;p&gt;云长科技是一个年轻的创业团队，同时云长二字也代表了一种“蓝天白云、高远流长”的美好夙愿和集体人格。我们追求，在做好产品、服务社会、创造经济效益的同时，践行和弘扬我们的核心价值观。我们希望，通过不懈努力和积累，把云长发展成一个受人信赖、基业长青、担当社会责任的优秀企业 - 老板原话。&lt;/p&gt;

&lt;p&gt;目前公司大部分为业务人员，主要分布在广东四会，小部分推广团队的人分布在广州与深圳。技术团队目前则是远程办公为主，主要分布在珠三角地区。&lt;/p&gt;
&lt;h2 id="项目简介"&gt;项目简介&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;官网 &lt;a href="https://huiliu.net/" rel="nofollow" target="_blank"&gt;https://huiliu.net/&lt;/a&gt; 技术栈：Ruby On Rails。&lt;/li&gt;
&lt;li&gt;回流 App - 技术栈：Flutter 移动端跨平台解决方案。&lt;/li&gt;
&lt;li&gt;回流小程序 - 技术栈：Taro 跨平台解决方案。&lt;/li&gt;
&lt;li&gt;后台管理系统 - 技术展：React + Mobx 搭配 Ruby On Rails 的 API 作为后台支撑。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="业务简介"&gt;业务简介&lt;/h2&gt;
&lt;p&gt;回流是珠宝玉翠的竞价回收平台，目的是给玉翠爱好者提供一个闲置变现的渠道。&lt;/p&gt;
&lt;h2 id="面试安排"&gt;面试安排&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;外地的朋友，如果有兴趣的话，可以先把简历发过来，合适的话可以直接安排线上面试。&lt;/li&gt;
&lt;li&gt;在深圳本地的朋友，也可以选择远程面试。如果离得比较近的，觉得要亲身面对面聊一下并看看办公环境的话，可以安排线下面试。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="联系方式"&gt;联系方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;简历邮箱：zhiheng@huiliu.net&lt;/li&gt;
&lt;li&gt;联系人：蓝先生&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Thu, 06 Jul 2023 16:53:18 +0800</pubDate>
      <link>https://ruby-china.org/topics/43209</link>
      <guid>https://ruby-china.org/topics/43209</guid>
    </item>
    <item>
      <title>记一次 Puma 配置导致的性能问题</title>
      <description>&lt;p&gt;以往的性能问题大多都出现在数据库查询中，着实没想到这次会是因为 Puma 的配置不当。原文链接：&lt;a href="https://step-by-step.tech/posts/performance-issue-for-puma-configuration" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/performance-issue-for-puma-configuration&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;最近这一两周其实打击挺大的，因为这段日子笔者每天都花大量时间在优化回流的服务端系统。精力都集中在减少 N+1 查询上，几乎把常用接口的 N+1 问题都解决掉了，慢查询也优化到所剩无几。然而，系统的性能还是不稳定，每到峰值时期，响应时间犹如脱缰的野马径直往上飙。&lt;/p&gt;

&lt;p&gt;每次优化掉一些耗时较长的请求，都会信誓旦旦地跟同事说“今天应该不卡了”，然而每次到了高峰期，总会啪啪打脸，有大量响应时间 400ms 以上的请求不断冒出来，按都按不住，整个系统几乎处于不可用的状态。老实说以前总觉得响应时间 500-600ms 没什么的，但是最近看到大量请求的响应时间超过 300ms 的时候，血压也会跟着升高。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://step-by-step.oss-cn-beijing.aliyuncs.com/production/huetr17u4woo16ah61hm74qrwj7c" title="" alt="Screenshot 2023-04-03 at 07.49.17.png"&gt;&lt;/p&gt;

&lt;p&gt;优化进入到了死胡同，笔者也很纳闷，数据库慢查询几乎没多少了，CPU 的使用率也不是很高，内存也一直在 5 ～ 8G 之间徘徊，按理说我这 8 核 16G 的机器资源还有很大的盈余，升级机器也解决不了什么问题，实在是不知道性能瓶颈在哪。&lt;/p&gt;

&lt;p&gt;一筹莫展之际，笔者突发奇想，不知道是不是 Puma 的 Worker 不够，CPU 利用率不够高导致的？看了一下现在的配置是&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;threads&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;
&lt;span class="n"&gt;workers&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而且就在&lt;code&gt;puma.rb&lt;/code&gt;这个配置文件里面，笔者还发现了这样的代码片段。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;before_fork&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'puma_worker_killer'&lt;/span&gt;
  &lt;span class="no"&gt;PumaWorkerKiller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ram&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5120&lt;/span&gt; &lt;span class="c1"&gt;# mb&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frequency&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;    &lt;span class="c1"&gt;# seconds&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;percent_usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.98&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rolling_restart_frequency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="c1"&gt;# 12 hours in seconds, or 12.hours if using Rails&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pre_term&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Worker &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; being killed"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rolling_pre_term&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Worker &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; being killed by rolling restart"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="no"&gt;PumaWorkerKiller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到这段话的时候&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;before_fork&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ram&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5120&lt;/span&gt; &lt;span class="c1"&gt;# mb&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;真的是茅舍顿开，并不是回流服务多省内存，有这个配置在难怪内存使用一直在 5G ~ 8G 这个区间徘徊。因为回流前期比较穷，笔者都在尽可能节约服务器成本，Ruby 又是出了名吃内存的主，所以引入了&lt;a href="https://github.com/zombocom/puma_worker_killer" rel="nofollow" target="_blank" title=""&gt;PumaWorkerKiller&lt;/a&gt;来强行让 puma 的内存使用限制在 5G 左右的水平（当时机器总内存是 8G），适当的时候 kill 掉一些线程来释放内存。&lt;/p&gt;

&lt;p&gt;在这些配置的干扰下，客户量上来了，CPU 跟内存却还一直停留在较低水平。更要命的是配置不在代码库中，往往很难察觉到它们的存在。&lt;/p&gt;

&lt;p&gt;马上把内存限制扩大到 10G&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;before_fork&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ram&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10240&lt;/span&gt; &lt;span class="c1"&gt;# mb&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="nf"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Worker 数量也调整大一点，跟 CPU 的核数对应上&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;threads&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;
&lt;span class="n"&gt;workers&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经历过两周优化的挫折，笔者已经不再对自己的优化手段报有什么期望了，每次都是期望越大失望越大。然而万万没想到，这次优化效果立竿见影，原来 500ms 的指标一下子就跌到了正常水平了。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://step-by-step.oss-cn-beijing.aliyuncs.com/production/l516k0lutrcp3gm7pz1l78uqzcfp" title="" alt="Screenshot 2023-04-03 at 07.51.51.png"&gt;&lt;/p&gt;

&lt;p&gt;我是想过会有改善，就是没想到改善这么明显....当初为了节省费用而编写的配置代码，如今却成了性能瓶颈。&lt;em&gt;后来笔者有把 Worker 数量调整回 4 个，发现单单提升了内存的限制，系统性能也会有所提升，但不够明显，还是要上管齐下才行，看来 worker 数量跟 CPU 的核数一致会是比较合适的选择。&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/puma/puma" rel="nofollow" target="_blank" title=""&gt;Puma&lt;/a&gt;的维护者也建议我们要自己多尝试&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Feel free to experiment, but be careful not to set the number of maximum threads to a large number, as you may exhaust resources on the system (or cause contention for the Global VM Lock, when using MRI).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我喜欢这种顶着限速跑的感觉：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://step-by-step.oss-cn-beijing.aliyuncs.com/production/dfrpqrjjed784fe3c9pc3xaydx5g" title="" alt="15491680245269_.pic.jpg"&gt;&lt;/p&gt;

&lt;p&gt;人是容易先入为主的动物，特别是在一个地方呆久了，往往会“如入鲍鱼之肆，久而不闻其臭”。以往的性能问题大多都出现在数据库查询中，笔者也下意识地花了很多精力去做数据库调优，却是万万没想到，这次的性能瓶颈出现在平时关注最少的 CPU 跟内存上。&lt;/p&gt;

&lt;p&gt;毕竟从指标上看 CPU 跟内存有很大的盈余，却没想到这表面的“盈余”是人为限制的结果。无论是对待 Bug 还是系统性能瓶颈，适当跳出来以局外人的身份审视说不定会有意想不到的效果。这次误打误撞把性能问题解决掉了，也算是运气好吧。&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Mon, 03 Apr 2023 08:00:56 +0800</pubDate>
      <link>https://ruby-china.org/topics/42978</link>
      <guid>https://ruby-china.org/topics/42978</guid>
    </item>
    <item>
      <title>对于推荐系统的拙劣看法</title>
      <description>&lt;p&gt;大概是 2022 年中旬，老板就有跟我提过未来想要做推荐系统。近期才想起来要看一些相关的资料，这篇文章便谈谈笔者对推荐系统的一些拙见。原文链接：&lt;a href="https://step-by-step.tech/posts/think-about-recommended-system" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/think-about-recommended-system&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="“高大上”吗？"&gt;“高大上”吗？&lt;/h2&gt;
&lt;p&gt;市面上每当提起推荐系统，大多数情况下都让人联想到，人工智能，数据挖掘，机器学习等领域。脑海中一般都会浮现出 Airbnb，抖音，亚马逊，淘宝，京东这些推荐做得很好的公司/App。这就导致了，每当提起推荐系统，都让人有种高攀不起的感觉。&lt;strong&gt;然而，自身产品做得差，推荐再好也没用。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在淘宝/京东的早期，数据量还不够大的时候，人工智能，数据挖掘也没像今天这么炙手可热，难道就没有推荐吗？答案肯定是有的，那个时期也有适用于那个时期的推荐。只不过随着时间的推移，它们的推荐系统渐渐演化成今天这样。抖音的推荐肯定不是一开始就这么精准的，也会有它不精准的时期。&lt;/p&gt;

&lt;p&gt;故而，真的没有必要在一开始就把推荐系统想得特别高大上，穷人有穷人的玩法，即便是没有数据科学家的团队依旧能够打造合适自己的推荐系统。笔者以为，只要有数据库，依赖足量的数据就能着手开始构建推荐系统。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;推荐即使稍微有些偏差也比没有任何推荐强。 - 《实用推荐系统》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="推荐系统的分类"&gt;推荐系统的分类&lt;/h2&gt;
&lt;p&gt;按个性化级别的不同，推荐系统大致可以分为：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;非个性化推荐系统：比方说常见的最受欢迎排行榜。排行榜跟所要推荐用户的个人特征无关，商品因为自身的特征而上榜。&lt;/li&gt;
&lt;li&gt;半个性化推荐系统：根据用户的特征，年龄，职业，性别等不同维度把用户划分成不同群体，按群体特征来做推荐。&lt;/li&gt;
&lt;li&gt;个性化推荐系统：根据用户的特征，用户行为来分析出个别用户的品味，进而针对用户的不同品味来进行推荐。抖音，淘宝，京东这些大厂做得挺好的就是这方面的推荐，需要对大量的用户数据做分析，也是最难实现的一种。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;绝大多数技术团队都没办法一上来就实现&lt;strong&gt;个性化推荐系统&lt;/strong&gt;，不仅投入成本大，而且收效甚微。既然如此，何不根据自身的情况，先着手于前面两种呢。只要网站有运营一段时间，就会有定量的用户数据跟商品数据，这个时候运用数据库的聚合知识，便能在一定程度上实现&lt;strong&gt;非个性化推荐系统&lt;/strong&gt;与&lt;strong&gt;半个性化推荐系统&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="不同应用的不同推荐做法"&gt;不同应用的不同推荐做法&lt;/h2&gt;
&lt;p&gt;推荐系统的实现并不能一概而论，不同类型的应用应该采用不同的策略算法。当然有些算法是通用的，但是策略必定会有所不同。&lt;/p&gt;

&lt;p&gt;比方说，淘宝的每一件货品，都有库存的概念，那么淘宝完全可以根据某样货品的销量来构建出一个最受欢迎的排行榜。然而回流，是一个基于拍卖模式的应用，每件货都是独一无二的（换句话说库存都是 1），那么我们就没办法根据销量来构建出一个排行榜，而是得仰仗其他策略。&lt;/p&gt;

&lt;p&gt;我们这类平台其实就更适合做&lt;strong&gt;专家推荐&lt;/strong&gt;，平台的品类主要是翡翠玉石，玉石行业的专家一般看一眼就大概能知道某货品的价值，大可以把专家也看中的货品也推荐给用户，这也可以说是一种&lt;strong&gt;非个性化推荐&lt;/strong&gt;了。&lt;/p&gt;
&lt;h2 id="用户行为的收集"&gt;用户行为的收集&lt;/h2&gt;
&lt;p&gt;用户量达到一定规模的网站/App 必定会开始监控用户行为。让用户主动对产品进行打分/评价所能获得的信息毕竟有限，而且许多用户往往会出现“口是心非”的现象。于是监控用户的行为就显得很有必要了，我们可以从侧面分析用户的隐性需求，这是实现推荐系统很重要的一环。&lt;/p&gt;

&lt;p&gt;用户对某货品的购买行为，用户对应用的操作行为，这些都可以在日常迭代中利用监控链接将其记录下来，以备不时之需。一方面，可以通过分析这些数据更好地了解用户，从用户的行为分析出用户在使用应用的过程中会遇到什么问题，进而优化交互体验。另一方面，可以借由这些数据，了解用户的“口味”，借此为用户提供更好的推荐，渐渐的也会行成一套平台专属的推荐系统。&lt;/p&gt;

&lt;p&gt;虽然大众都不会喜欢自己被“监控”，但请相信我，所有应用厂商总能找到冠冕堂皇的理由来合理化自己的行为。&lt;/p&gt;
&lt;h2 id="大致规划"&gt;大致规划&lt;/h2&gt;
&lt;p&gt;若非对系统及业务性质十分了解，必将难以提出很好的推荐建议。个人以为前期由技术团队牵头来做这个事情可能会比较合适。空降的用户分析师，一般对公司产品/业务不会太了解，提出的方案也不一定适用于当前产品。&lt;/p&gt;

&lt;p&gt;回流早期的推荐系统应该会先从&lt;strong&gt;非个性化推荐&lt;/strong&gt;着手，先根据用户的活跃情况对货品的属性做一个权重的划分，并根据权重计算后的值来进行排序。其实这种做法就类似于构建排行榜，简单粗暴，不过作为起步是个不错的选择。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;半个性化推荐/个性化推荐&lt;/strong&gt;都需要依赖大量的用户行为/用户属性，则要编写监控链接来监控用户行为。当有了大量的用户行为数据（预览，搜索，购买）及用户属性（性别，偏爱品类）之后才有可能针对用户口味做一些数据分析。反正就是，&lt;em&gt;先把数据收集起来再说&lt;/em&gt;。但不管怎么说，还真不着急引入 AI 的东西，在没有数据支撑的情况下，机器学习能施展的空间也是十分有限的。&lt;strong&gt;先把数据库用好一点再说，能解决前期的很多问题了。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="瞎扯淡"&gt;瞎扯淡&lt;/h2&gt;
&lt;p&gt;刚开始接触推荐系统相关的知识，便觉得此领域还是大有可为之处的。不过个人以为不宜操之过急，可以先从简单的推荐入手，而不是一开始就去看很多高大上的技术（脑子也容易转不过来）。&lt;/p&gt;

&lt;p&gt;也没必要一步到位，计划用几个月时间来打造一个完美的推荐系统往往更容易失败。前期先从简单的开始，有一个不那么好的推荐也总比完全没有推荐来得好，只要公司不倒闭，就有一辈子的时间来优化它。&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Fri, 24 Feb 2023 09:23:51 +0800</pubDate>
      <link>https://ruby-china.org/topics/42904</link>
      <guid>https://ruby-china.org/topics/42904</guid>
    </item>
    <item>
      <title>解决搜索问题就一定要上 ElasticSearch 吗？</title>
      <description>&lt;p&gt;我们总想寻找锤子来处理眼前的这个钉子，然而，说不定这里压根就不需要用到钉子呢。原文链接：&lt;a href="https://step-by-step.tech/posts/huiliu-simple-solution-for-searching" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/huiliu-simple-solution-for-searching&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="事情是这样的"&gt;事情是这样的&lt;/h2&gt;
&lt;p&gt;回流是传统行业麾下的一款互联网产品。正常情况下，数据量会逐渐增大，但是膨胀速度并不会说特别快。然而，只要有足够的时间，数据量总会到达一定程度。此时系统的某些部分就会渐渐变得缓慢，有些查询的速度会变得不忍直视。&lt;/p&gt;

&lt;p&gt;回流是有自己的管理端的（基于 React），当时是为了让自己人可以舒服一点，用空闲时间孵化的一个项目。无论是可扩展性还是交互体验应该会比&lt;a href="https://activeadmin.info/" rel="nofollow" target="_blank" title=""&gt;ActiveAdmin&lt;/a&gt;强不少。只不过投入的精力有限，还有很大的优化空间。万万没想到，近期的性能问题就出在管理端。同事们映有时候加载一个列表页面需要 5s 左右的时间，几乎是处于一个不可用的状态。&lt;/p&gt;

&lt;p&gt;后来发现，归根结底还是数据查询（搜索）不够快拖慢了整个请求，毕竟数据量摆在那里。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Backflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search_by_admin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'订单号'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'backflows.received_at DESC'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Backflow&lt;/span&gt; &lt;span class="no"&gt;Load&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2838&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="s2"&gt;"backflows"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;*&lt;/span&gt; &lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="s2"&gt;"backflows"&lt;/span&gt; &lt;span class="no"&gt;LEFT&lt;/span&gt; &lt;span class="no"&gt;OUTER&lt;/span&gt; &lt;span class="no"&gt;JOIN&lt;/span&gt; &lt;span class="s2"&gt;"goods"&lt;/span&gt; &lt;span class="no"&gt;ON&lt;/span&gt; &lt;span class="s2"&gt;"goods"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"backflow_id"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"backflows"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt; &lt;span class="no"&gt;LEFT&lt;/span&gt; &lt;span class="no"&gt;OUTER&lt;/span&gt; &lt;span class="no"&gt;JOIN&lt;/span&gt; &lt;span class="s2"&gt;"logis.....
=&amp;gt; #&amp;lt;ActiveRecord::Relation []&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;irb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="mo"&gt;003&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Backflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;
   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;55.9&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="no"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="no"&gt;FROM&lt;/span&gt; &lt;span class="s2"&gt;"backflows"&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;332690&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 30w 条数据中，要在联表的情况下做模糊查找并排序，一次操作要花费&lt;code&gt;2838ms&lt;/code&gt;，也难怪一个页面要加载好几秒的时间（业务团队居然忍了很长一段时间都不反馈过来）。作为一个技术宅，脑海中下意识就浮现出好几种解决方案&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;添加合适的索引（感觉索引能治百病）。&lt;/li&gt;
&lt;li&gt;优化查询条件，看情况引入数据库全文搜索。&lt;/li&gt;
&lt;li&gt;引入&lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/install-elasticsearch.html" rel="nofollow" target="_blank" title=""&gt;ElasticSearch&lt;/a&gt;这种主流的搜索解决方案。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;下意识地想要一步到位地选择方案 3，由于对接下来要做的事情很有信心，便告知团队，即将要用酷炫的搜索引擎技术来优化整个站点的搜索体验了。&lt;/p&gt;
&lt;h2 id="你真的需要那么多数据吗？"&gt;你真的需要那么多数据吗？&lt;/h2&gt;
&lt;p&gt;在笔者疯狂调研全文搜索引擎的时候，有一天业务团队被卡到不行了，问我啥时候能优化好，因为实在是影响工作效率。要上&lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/install-elasticsearch.html" rel="nofollow" target="_blank" title=""&gt;ElasticSearch&lt;/a&gt;并不是那么容易的事情，却无法跟大伙解释这点，他们能看到的就只是系统卡不卡，才不管优化难度大不大呢。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;考虑到业务团队在日常货品上架的过程中最常操作的就是最新的一些订单&lt;/strong&gt;，绝大多数情况下其实也没必要从所有历史数据中进行搜索。于是做了个应急优化&lt;strong&gt;搜索范围限制在最近一个月，要是遇到搜索不到的情况，再联系笔者帮忙寻找对应的订单，而且这种情况应该极少&lt;/strong&gt;。然而就在交代这些的时候，笔者突然灵机一动。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;为何要执着于优化全局范围内的搜索性能呢，上架部门顶多就只需要几个月范围内的数据。一定有更简单的解决方案。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;最后选择了如下解决方案：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;管理端限制筛选范围，默认只显示最近两周的数据，默认搜索范围也是两周。&lt;/li&gt;
&lt;li&gt;如果搜索无果，同事们可以选择更早之前的时间段做进一步的查找，但时间范围的长度不得超过一个月，以免数据太多造成卡顿。&lt;/li&gt;
&lt;li&gt;多往前翻几个月没有数据就基本可以判定用户近期没有下过对应的订单，可以引导用户先做下单操作。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这并不是什么了不起的优化手段，也没有引入新的技术，不过对于后台管理软件来说已经足够用了。只不过是默认限制了一下时间范围而已，改动小，带来的效益却很大。默认搜索条件能满足 80% 以上的场景了，搜索性能提高了 10 倍不止。&lt;/p&gt;

&lt;p&gt;我们总会下意识地寻求更新更酷炫的技术来解决眼下的问题，然而大多数的情况下&lt;strong&gt;远水难救近火&lt;/strong&gt;。结合实际的场景，说不定会有虽看起来朴实无华，但是却更简单实用的解决方案。&lt;/p&gt;
&lt;h2 id="路漫漫"&gt;路漫漫&lt;/h2&gt;
&lt;p&gt;笔者这次的优化，放在后台场景是十分合适的，优化效果也很好。然而，如果放在 App 端就不一定了，毕竟 App 的用户多，搜索需求大，限制了搜索范围，很难满足大部分人的要求。在 App 端的搜索接口，还真的要考虑用全文搜索的解决方案呢。笔者也会继续对&lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/install-elasticsearch.html" rel="nofollow" target="_blank" title=""&gt;ElasticSearch&lt;/a&gt;做调研。&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Sat, 18 Feb 2023 08:57:07 +0800</pubDate>
      <link>https://ruby-china.org/topics/42887</link>
      <guid>https://ruby-china.org/topics/42887</guid>
    </item>
    <item>
      <title>从业绩的春天到发不起工资的寒冬</title>
      <description>&lt;p&gt;正在疫情解封之前不久，公司终于迎来了业绩的春天，就是没想到接下来跳过了夏/秋直奔严冬。&lt;/p&gt;

&lt;p&gt;原文链接： &lt;a href="https://step-by-step.tech/posts/huiliu-spring-to-winter" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/huiliu-spring-to-winter&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;马云老师说过，人生有两大悲剧：一是万念俱灰，一是踌躇满志。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;2022 年下旬，业绩上稍微有了点甜头，就是我们最踌躇满志的时候：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;业务方觉得，翡翠行业已经上轨道了，可以尝试拓展其他品类。&lt;/li&gt;
&lt;li&gt;推广方觉得，现在用户量起来了，ROI 也做起来了，可以加大投入力度，制造更大的投入产出比。&lt;/li&gt;
&lt;li&gt;技术方觉得，为了承载接下来难以抵挡的流量浪潮，可以开始招点人，并尝试一些新潮点的解决方案了。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;没想到接下来的事情就是一个响亮的耳光，新业务没跑起来，老业务也受到各种因素的影响而业绩表现不佳，本来说好的推广经费被砍了又砍，技术调研了个寂寞，新技术 Cool 是很 Cool，但是投入成本高，收益却不大。换句话说就是&lt;strong&gt;似乎什么破事都发生在这段时间了&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="业务方自信过度"&gt;业务方自信过度&lt;/h2&gt;
&lt;p&gt;老板们酒过三巡之后，就容易感觉某事情可行，笔者有幸见证了这些时刻。也明白了雷声大雨点小是怎么回事，我司在书画跟黄金品类的投入其实就有点这样的味道。感觉自己可以尝试同时扩展两个品类，并迅速抢占市场，结果两个业务都是一地鸡毛。合作方在饭桌上跟我们还是挺合得来的，但真到撸起袖子干的时候，却不是这么回事了。&lt;/p&gt;

&lt;p&gt;要了解一个人靠不靠谱，光看他一年赚多少钱是不够的。真遇到财大气粗，三观没法很好磨合的，后续的工作依旧没法进行。最后结果就是&lt;strong&gt;伤钱也伤感情&lt;/strong&gt;。当然责任应该还是双方的，若非自身“飘”，也不会一次过尝试开两个品类。最后给公司留下几十万的成本，这点钱过年给员工们发发福利多好。&lt;/p&gt;

&lt;p&gt;当然这几十万只是肉眼可见的成本。因为我们自信过度，要应付新品类，所有研发精力基本都放在新品类上，最终这部分功能没达到应有的效果，只能砍掉，算是一种无形的成本。又因为我们自信过度，感觉可以拉来更多的用户，推广投入也相继增长，但是效果肯定是没预期那么好，否则应对寒冬的时候推广经费就不用一砍再砍了。&lt;/p&gt;
&lt;h2 id="技术方自信过度"&gt;技术方自信过度&lt;/h2&gt;
&lt;p&gt;当业务说，系统用户将出现爆炸性增长的时候，笔者就感觉终于到我们这些后端研发工程师大展拳脚的时候了。总感觉公司早晚会用上容器化技术，只是一直没找到合适的契机，现在可好，业务发话了，对用户增长这么有自信，那就上呗。ShowMeBug 的 CEO-李亚飞听了我的想法之后，其作为旁观者都感觉没太大必要，其他前同事也劝我说先不要。但还是因为自信过度，笔者一意孤行，在容器化的深坑里摸爬滚打了将近一个月。&lt;/p&gt;

&lt;p&gt;总算是把项目方方面面都做好了容器化的准备，然而当一切都准备就绪之后，很痛苦地发现：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;如果要把容器技术应用在生产环境，将又是另一笔大的费用。&lt;/li&gt;
&lt;li&gt;运维成本并没有节省多少，容器化的学习曲线就摆在那，会让一些之前没有接触过容器技术的小伙伴感觉更费解。&lt;/li&gt;
&lt;li&gt;“你可以招个运维来专门做这事啊”，如你所见，后期公司都&lt;strong&gt;没钱&lt;/strong&gt;了，而且暂时也没这个必要。&lt;/li&gt;
&lt;li&gt;发现性能的瓶颈并不是服务机器不够用，机器配置不够直接升级配置（垂直扩展）或者使用镜像服务器搭载负载均衡（水平扩展）可能是更好的做法，请求的瓶颈其实在数据库，针对数据库做优化性价比更高。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;最后还是决定悬崖勒马，暂时放弃容器化技术，算是及时止损吧。也不用特意招聘一运维工程师来专职化运维，老实说，&lt;strong&gt;专职化&lt;/strong&gt;这三个字，怎么看都感觉不便宜，勒紧裤腰带的小公司就暂时别想了。&lt;/p&gt;
&lt;h2 id="其他的不顺心"&gt;其他的不顺心&lt;/h2&gt;
&lt;p&gt;当一件事情不顺心的时候，似乎很多其他不顺心的事情也会接踵而来啊。似乎什么破事都发生在这几个月了：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;由于权限管理不善，导致有部分交易被水下进行，公司业绩受到影响。&lt;/li&gt;
&lt;li&gt;疫情开放之后，基本全公司沦陷，绝大部分人都生病了，关键的一线岗位找不到轮替的人，业务几乎没法进行，业绩一跌再跌。&lt;/li&gt;
&lt;li&gt;难得所有人都康复回到工作岗位上了，遇到过年，该放假了，放假几天一线很多人都无法在岗，业绩依旧无法扶起。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;人生就是大起大落落落哈，2022 年年底这几个月，9 月份想着，业绩不错年底应该能有年终。10 月份想着业绩不错，可以多点规划部门团建。11 月份想着还是先别团建了，公司最近支出大，开发也忙。12 月份想着公司岔路走太多了，估计是发不起年终。次年 1 月想着，还想啥年终呢，能发得起工资就不错了。&lt;/p&gt;
&lt;h2 id="歇会吧"&gt;歇会吧&lt;/h2&gt;
&lt;p&gt;最近我们 CEO 在自己的公开日志中有提到人要“谦虚”的话题，看来反省的不仅我一个哈。这波业绩下滑来得始料未及，打得有点触不及防，到现在都还没缓过来。胜在大伙心态好，该砍的业务砍了，及时止损，血也在慢慢回起来了。&lt;strong&gt;以后不管业绩好坏，别飘。&lt;/strong&gt;&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Tue, 14 Feb 2023 10:30:29 +0800</pubDate>
      <link>https://ruby-china.org/topics/42876</link>
      <guid>https://ruby-china.org/topics/42876</guid>
    </item>
    <item>
      <title>【译】Rails &amp; Kubernetes 权威指南</title>
      <description>&lt;p&gt;本文主要讲述如何让 Rails 应用配合 Kubernetes 来完成生产环境的部署，原文站点：&lt;a href="https://kubernetes-rails.com/" rel="nofollow" target="_blank" title=""&gt;https://kubernetes-rails.com/&lt;/a&gt; 。本文的翻译已得到作者&lt;a href="https://collimarco.com/" rel="nofollow" target="_blank" title=""&gt;Marco Colli&lt;/a&gt;的同意。&lt;/p&gt;

&lt;p&gt;原翻译文链接：&lt;a href="https://step-by-step.tech/posts/kubernetes-and-rails-definitive-guide" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/kubernetes-and-rails-definitive-guide&lt;/a&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;要部署 Rails 应用有很多种方式：其中一种则是利用&lt;a href="https://www.docker.com/" rel="nofollow" target="_blank" title=""&gt;Docker&lt;/a&gt;容器化技术，并结合&lt;a href="https://kubernetes.io/" rel="nofollow" target="_blank" title=""&gt;Kubernetes&lt;/a&gt;作容器编排。这篇教程会为您展示 Kubernetes 对比于其他部署解决方案的优势所在，并会详细描述如何借助 Kubernetes，把 Rails 应用部署到生产环境。这里会把重点放在生产环境中的容器应用，开发环境的容器应用并不会花费太多的笔墨，毕竟我们觉得简单的解决方案更具价值。此教程会涵盖在生产环境上运行 Rails 应用所需要关注的方方面面，这其中就包括 Web 应用的部署与持续交付，域名及负载均衡的配置，环境变量和隐私安全，静态资源的编译，数据库表的变更，日志和监控，后台作业还有定时任务，当然不能少了要如何实施维护性的任务及系统更新。&lt;/p&gt;

&lt;p&gt;这篇文章的目录结构如下&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes 的历史及其他可选方案&lt;/li&gt;
&lt;li&gt;预备知识&lt;/li&gt;
&lt;li&gt;Rails 应用&lt;/li&gt;
&lt;li&gt;Git 仓库&lt;/li&gt;
&lt;li&gt;Docker 镜像&lt;/li&gt;
&lt;li&gt;Kubernetes 集群&lt;/li&gt;
&lt;li&gt;域名与 SSL(Security Socket Layer 加密套接字协议层)&lt;/li&gt;
&lt;li&gt;环境变量&lt;/li&gt;
&lt;li&gt;隐私安全&lt;/li&gt;
&lt;li&gt;日志&lt;/li&gt;
&lt;li&gt;后台作业&lt;/li&gt;
&lt;li&gt;定时任务&lt;/li&gt;
&lt;li&gt;命令行控制台&lt;/li&gt;
&lt;li&gt;Rake 任务&lt;/li&gt;
&lt;li&gt;数据库变更&lt;/li&gt;
&lt;li&gt;持续交付&lt;/li&gt;
&lt;li&gt;监控&lt;/li&gt;
&lt;li&gt;安全更新&lt;/li&gt;
&lt;li&gt;总结&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="Kubernetes的历史及其他可选方案"&gt;Kubernetes 的历史及其他可选方案&lt;/h2&gt;
&lt;p&gt;部署 Rails 应用最简便的方式可能是借助 PaaS 服务，比方说&lt;a href="https://www.heroku.com/" rel="nofollow" target="_blank" title=""&gt;Heroku&lt;/a&gt;，这些服务让部署和扩容变得异常简单，让你几近忘记服务本身，然而：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;当应用需要扩容的时候，账单上的花销会高到让你怀疑人生。&lt;/li&gt;
&lt;li&gt;你对应用并不具备完整的控制权，都是第三方服务在帮你管理，在应用运作的过程中可能会给你增添些许担忧。&lt;/li&gt;
&lt;li&gt;你会受到来自于平台方的约束。&lt;/li&gt;
&lt;li&gt;你的应用可能会跟特定的平台绑定，在可移植性方面会有些麻烦。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;有个更经济的解决方案是使用 IaaS 服务，比方说 DigitalOcean（可以对标国内的阿里云 ECS/轻量级服务器）。最开始，可以用单机的方式来部署应用服务。一般来说，你需要至少一个负载均衡器（比方说 HAProxy），及多个基于 Puma 与 Nginx 的 Web 服务，当然还有数据库（通常是可能以集群方式运行的 PostgreSQL 和 Redis）。哦，还需要额外的服务用于后台作业（比方说 Sidekiq）。当你需要横向扩容应用的时候，通常只需要针对单一服务创建快照，然后便能得到它相应的副本。你当然也可以通过&lt;a href="https://linux.die.net/man/1/pssh" rel="nofollow" target="_blank" title=""&gt;pssh&lt;/a&gt;或者类似于&lt;a href="https://www.chef.io/products/chef-infra" rel="nofollow" target="_blank" title=""&gt;Chef&lt;/a&gt;这样的配置管理工具来管理或是变更多台服务器，紧接着便能够使用&lt;a href="https://capistranorb.com/" rel="nofollow" target="_blank" title=""&gt;Capistrano&lt;/a&gt;完成部署工作。创建并配置一堆服务器并非难事，然而：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;初始化服务器设置需要时间且运维者要有相关技能的知识储备。&lt;/li&gt;
&lt;li&gt;要给多台服务器做变更会比较痛苦。&lt;/li&gt;
&lt;li&gt;要是给一小撮服务器发送了错误的指令，回滚将是一件较为困难的事情。&lt;/li&gt;
&lt;li&gt;你必须要确保所有的服务器都应用了同一份配置。&lt;/li&gt;
&lt;li&gt;扩容时需要大量的手动操作。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubernetes 提供了 PaaS 的优点，并且用户只需要承担 IaaS 的费用，因此它是一个不错的折中方案，你应该考虑一下。得益于它是一项开源技术，大多数的云服务商都已经提供了托管的 Kubernetes 集群服务（比方说阿里云的 ACK）。&lt;/p&gt;

&lt;p&gt;接下来我们一起来看看如何借助 Kubernetes 来把一个 Rails 应用部署到生产环境。&lt;/p&gt;
&lt;h2 id="预备知识"&gt;预备知识&lt;/h2&gt;
&lt;p&gt;这篇教程假设你已经具备 Web 开发的基本常识。&lt;/p&gt;

&lt;p&gt;我们当然也认定你已经拥有一台开发用的机器，并且机器上已经安装好所有必备软件，其中包括 Ruby(你可以使用 rbenv 来安装)，Ruby On Rails, Git, Docker 等等。&lt;/p&gt;

&lt;p&gt;你起码要有一个&lt;a href="https://hub.docker.com/" rel="nofollow" target="_blank" title=""&gt;Docker Hub&lt;/a&gt;的账号，要尝试 Kubernetes 的相关功能 DigitalOcean 账号也是需要的（当然你也可以选择自己喜欢的替代品，比如说阿里云，腾讯云等等）。&lt;/p&gt;
&lt;h2 id="Rails应用"&gt;Rails 应用&lt;/h2&gt;
&lt;p&gt;你可以使用一个已经存在的 Rails 应用，或者说用下面的命令创建一个新的 Rails 应用案例&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; rails new kubernetes-rails-example
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后往示例应用中添加一个简单的页面&lt;/p&gt;

&lt;p&gt;&lt;code&gt;config/routes.rb&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="s1"&gt;'pages#home'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;app/controllers/pages_controller.rb&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PagesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;home&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;app/views/pages/home.html.erb&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Hello, world!&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="Git仓库"&gt;Git 仓库&lt;/h2&gt;
&lt;p&gt;接下来我们把改动保存在本地的 Git 仓库，该仓库在 Rails 项目初始化的时候就存在了。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial commit"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还需要在线上准备一个 Git 仓库。你可以去&lt;a href="https://github.com/" rel="nofollow" target="_blank" title=""&gt;Github&lt;/a&gt;创建一个新的仓库，并把远端仓库跟本地仓库进行关联，然后把改动推送到远端仓库：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin https://github.com/username/kubernetes-rails-example.git
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而 Docker 或 Kubernetes 并不严格依赖于 Git 仓库，我特意在这里提到它是因为绝大多数的 CI/CD 工具（包括 Docker Hub），都可以跟你的 Git 仓库进行关联。关联仓库之后，就可以做到&lt;strong&gt;每当你往远程 Git 仓库提交改动的时候都可以自动构建 Docker 镜像。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="Docker镜像"&gt;Docker 镜像&lt;/h2&gt;
&lt;p&gt;容器化的第一步就是创建 Docker 镜像。一个 Docker 镜像其实就是一个简单的软件包，里面包含了我们的应用，以及该应用所依赖的第三方软件包和系统库。&lt;/p&gt;

&lt;p&gt;在你的 Rails 应用项目的根目录中添加这个文件&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ruby:2.5&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs yarn postgresql-client

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; /app
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; Gemfile Gemfile.lock ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;bundler
&lt;span class="k"&gt;RUN &lt;/span&gt;bundle &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;rake assets:precompile

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["rails", "server", "-b", "0.0.0.0"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先使用&lt;code&gt;FROM&lt;/code&gt;指令，告诉 Docker 要下载一个公共镜像，接下来我们会基于这个基础镜像来自定义服务镜像。事实上，我们所使用的镜像包含了特定版本的 Ruby。&lt;/p&gt;

&lt;p&gt;接下来，使用&lt;code&gt;RUN&lt;/code&gt;指令，主要用于在镜像构建的过程中执行系统命令。实际是，我们会借助 Linux 的&lt;code&gt;apt-get&lt;/code&gt;命令安装一些软件包。请记住，在默认的 Ubuntu 软件仓库中可用的软件包通常都非常陈旧：如果你想要获取最新版本，则需要更新软件仓库中的列表并且告知&lt;a href="https://help.ubuntu.com/community/AptGet/Howto?action=show&amp;amp;redirect=AptGet" rel="nofollow" target="_blank" title=""&gt;APT&lt;/a&gt;要直接到对应软件维护者的仓库中去下载软件包。实际操作的时候，我们可以往&lt;code&gt;apt-get&lt;/code&gt;命令之前添加下面这些命令，借此来更新 Node.js 以及 Yarn 的软件仓库&lt;/p&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;curl https://deb.nodesource.com/setup_12.x | bash
&lt;span class="k"&gt;RUN &lt;/span&gt;curl https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb https://dl.yarnpkg.com/debian/ stable main"&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/yarn.list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在下一个代码块中，我们会把 Rails 应用拷贝到镜像中并使用&lt;a href="https://bundler.io/" rel="nofollow" target="_blank" title=""&gt;Bundler&lt;/a&gt;安装所有必备的 Gem 包。&lt;code&gt;Gemfile&lt;/code&gt;与&lt;code&gt;Gemfile.lock&lt;/code&gt;文件会比项目中的其他代码文件更早拷贝到镜像中。那是因为在&lt;code&gt;Gemfile&lt;/code&gt;没有任何改动的情况下，Docker 可以利用缓存机制来加速镜像的构建。&lt;/p&gt;

&lt;p&gt;我们还需要执行任务来预编译静态资源（stylesheet，script 等等）。&lt;/p&gt;

&lt;p&gt;最后，我们会配置一个默认的命令，它将在镜像中执行。&lt;/p&gt;

&lt;p&gt;在构建镜像之前，我们还要确认哪些文件不必被包含到镜像中去：安全角度考虑，把隐私相关的文件排除出去十分关键。当然也要排除一些不必要的目录，比方说&lt;code&gt;tmp&lt;/code&gt;跟&lt;code&gt;.git&lt;/code&gt;，这些目录只会浪费系统的存储资源。为了实现这一点，需要在 Rails 项目根目录创建&lt;code&gt;.dockerignore&lt;/code&gt;文件：你可以从项目中的&lt;code&gt;.gitignore&lt;/code&gt;文件中获取一些灵感，它们的目的跟语法都十分相似。&lt;/p&gt;

&lt;p&gt;现在是时候构建镜像了：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; username/kubernetes-rails-example:latest &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-t&lt;/code&gt;选项，以及跟在后头的参数都是可选的：我们使用它是为了给新的镜像命名并附上一个标签。这为后期查找镜像带来便利。&lt;code&gt;:&lt;/code&gt;前面的部分是镜像名，后面的部分是标签。需要注意的是，&lt;code&gt;latest&lt;/code&gt;这个标签字眼是可以去掉的，当你没有指定任何标签的时候，&lt;code&gt;latest&lt;/code&gt;就会是默认的标签。最后的一个&lt;code&gt;.&lt;/code&gt;是必要参数，它指出&lt;code&gt;Dockerfile&lt;/code&gt;所在的目录。&lt;/p&gt;

&lt;p&gt;构建完成之后，目标镜像在你的机器上就处于可用状态：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker image &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以使用镜像 ID 或是镜像名来运行镜像（镜像的运行时被称作容器）：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 username/kubernetes-rails-example:latest
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意，我们把机器的 3000 端口（左边）跟容器的 3000 端口（右边）做了映射。你当然也可以使用你喜欢的其他端口：然而，如果你更改了容器的端口，还需要把镜像也一并更新，以保证 Rails 服务监听了正确的端口，在这里你需要通过&lt;code&gt;EXPOSE&lt;/code&gt;指令把端口号暴露出去。&lt;/p&gt;

&lt;p&gt;现在你可以通过&lt;code&gt;http://localhost:3000&lt;/code&gt;访问你的站点了。&lt;/p&gt;

&lt;p&gt;这一步之后，我们便可以把镜像推送到远端的镜像仓库。首先你需要在&lt;a href="https://hub.docker.com/" rel="nofollow" target="_blank" title=""&gt;Docker Hub&lt;/a&gt;上注册账号，当然也可以使用其他的镜像仓库服务（考虑到镜像隐私以国内网络问题，建议使用国内云服务商的私有镜像仓库服务），并为服务镜像创建专门的镜像仓库。然后，你就可以把本地镜像推送到远端仓库了：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker push username/kubernetes-rails-example:latest
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="Kubernetes集群"&gt;Kubernetes 集群&lt;/h2&gt;
&lt;p&gt;接下来就可以为我们的生产环境创建 Kubernetes 集群了。可以先去到你喜欢的 Kubernetes 供应商站点，并利用它们所提供的操控面板创建集群：这篇教程中我们用的是 DigitalOcean。&lt;/p&gt;

&lt;p&gt;一旦集群创建完毕，你需要把相关的证书凭证以及集群配置下载到本地机器，然后就可以连接到远端集群了。举个例子，你可以把配置文件放在这个路径下&lt;code&gt;~/.kube/kubernetes-rails-example-kubeconfig.yaml&lt;/code&gt;，并通过&lt;code&gt;kubectl&lt;/code&gt;命令的&lt;code&gt;--kubeconfig&lt;/code&gt;可选参数指定该配置文件，又或者通过设置环境变量的方式来指定&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KUBECONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.kube/kubernetes-rails-example-kubeconfig.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，你需要确认本地机器上的 Kubernetes 工具套件可以连接到远端集群。运行这条命令即可&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应该会看到命令行工具的对应版本号以及远端 Kubernetes 集群的版本。&lt;/p&gt;

&lt;p&gt;你当然也可以干点别的事情，比方说用这条命令：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个&lt;code&gt;node&lt;/code&gt;(节点) 都可以看作是 Kubernetes 管理的简化服务器。Kubernetes 可以根据我们的配置信息在每一个 node 上创建一些虚拟机器，这些机器俗称为&lt;code&gt;pod&lt;/code&gt;。这些 pod 会被 Kubernetes 自动分发到可用的节点上，如果一个节点挂掉了，Kubernetes 会把该节点上的 pod 移动到其他节点，一个 pod 通常会包含一个的容器。不过 pod 也可以包含多个互相关联的容器，而这些容器一般都有共享某些资源的需求。&lt;/p&gt;

&lt;p&gt;下一步就是基于我们的 Docker 镜像调度出一些 pod。Docker 镜像可能会被托管在私有仓库中，这种情况我们需要给 Kubernetes 提供 Docker 镜像所在仓库的证书凭证，这样 Kubernetes 才能从仓库下载对应的镜像。运行这些命令即可&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret docker-registry my-docker-secret &lt;span class="nt"&gt;--docker-server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DOCKER_REGISTRY_SERVER &lt;span class="nt"&gt;--docker-username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DOCKER_USER &lt;span class="nt"&gt;--docker-password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DOCKER_PASSWORD &lt;span class="nt"&gt;--docker-email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DOCKER_EMAIL

kubectl edit serviceaccounts default
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并把下面的文本放在&lt;code&gt;Secrets:&lt;/code&gt;后面&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;imagePullSecrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-docker-secret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后你可以开始定义 Kubernetes 的配置信息：在 Rails 根目录创建一个名为&lt;code&gt;config/kube&lt;/code&gt;的子目录。&lt;/p&gt;

&lt;p&gt;我们先为 Rails 应用创建一个部署的配置信息吧&lt;/p&gt;

&lt;p&gt;&lt;code&gt;config/kube/deployment.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes-rails-example-deployment&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rails-app&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rails-app&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rails-app&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;username/kubernetes-rails-example:latest&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的配置信息是一个最小化的部署&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apiVersion&lt;/code&gt;设定应用该配置所需要的 API 版本；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kind&lt;/code&gt;设定配置文件的类型；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;metadata&lt;/code&gt;用于设定该部署任务的名称；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;replicas&lt;/code&gt;告知 Kubernetes 需要创建 pod 的数量；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;selector&lt;/code&gt;告知 Kubernetes 基于哪个模板来生成 pod；&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;template&lt;/code&gt;用于定义 pod 的模板。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;spec&lt;/code&gt;可以指定运行 pod 所依赖的 Docker 镜像，并包含一些其他的配置，比方说容器所需暴露的端口号。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;好了，现在我们需要把 HTTP 请求分发给这些 pod。那么就需要创建一个负载均衡器：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;config/kube/load_balancer.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes-rails-example-load-balancer&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LoadBalancer&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rails-app&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本质上来说，我们告知了负载均衡器以下信息：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;需要监听的默认端口是 80 端口；&lt;/li&gt;
&lt;li&gt;把请求转发到打了&lt;code&gt;rails-app&lt;/code&gt;标签，并监听了&lt;code&gt;3000&lt;/code&gt;端口的 pod 中；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;现在我们可以采用声明式的管理方式，把配置应用到 Kubernetes 集群中：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; config/kube
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证以下这些 pod 有没有正常运行：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;kubectl get pods&lt;/code&gt;可以列出所有的 pod，并附带他们的状态信息；&lt;/li&gt;
&lt;li&gt;所有 pod 的都应该是&lt;code&gt;Running&lt;/code&gt;这个状态；&lt;/li&gt;
&lt;li&gt;如果你看到错误信息&lt;code&gt;ImagePullBackOff&lt;/code&gt;，很可能你没有配置合适的镜像仓库证书凭证，以至于 Kubernetes 无法从私有仓库下载镜像；&lt;/li&gt;
&lt;li&gt;你可以通过命令&lt;code&gt;kubectl describe pod pod-name&lt;/code&gt;来获取更多的错误信息；&lt;/li&gt;
&lt;li&gt;你可以修复配置，并通过命令&lt;code&gt;kubectl delete --all pods&lt;/code&gt;来重新生成所有的 pod；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;你还可以通过下面的命令来得知负载均衡器的 IP 地址及其他信息&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get services
kubectl describe service service-name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上你需要的是&lt;code&gt;LoadBalancer Ingress&lt;/code&gt;或者&lt;code&gt;EXTERNAL-IP&lt;/code&gt;列中的 IP 地址信息，并把它输入到浏览器的地址栏中：这样我们的网站就启动完成了，现在看来运行良好！&lt;/p&gt;
&lt;h2 id="域名与SSL(Security Socket Layer加密套接字协议层)"&gt;域名与 SSL(Security Socket Layer 加密套接字协议层)&lt;/h2&gt;
&lt;p&gt;绝大多数情况下，你的用户群都不会用 IP 地址来访问站点，因此你需要为服务配置一个域名。可以在 DNS 解析配置中添加一条记录：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;example.com.   A   192.0.2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很显然，这里你需要换成自己的域名信息，并把 IP 地址替换成负载均衡器的公网 IP 地址（通过命令&lt;code&gt;kubectl get services&lt;/code&gt;可以得知）。&lt;/p&gt;

&lt;p&gt;SSL 证书可以通过恰当的 YAML 配置添加到你的网站中去，不过如果你用 DigitalOcean，最简单的方式还是去到它的控制面板，到那里去配置证书（可能是负载均衡的设置页面）。&lt;/p&gt;
&lt;h2 id="环境变量"&gt;环境变量&lt;/h2&gt;
&lt;p&gt;如果要存储环境变量，其实有很多种不同的解决方案&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在 Rails 的配置中定义环境变量。&lt;/li&gt;
&lt;li&gt;在 Dockerfile 中定义环境变量。&lt;/li&gt;
&lt;li&gt;在 Kubernetes 中定义环境变量。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我建议你使用 Kubernetes 来管理生产环境中的环境变量：这样系统变更会比较简单，也不需要每次做变更都从头构建一次镜像。当然，Kubernetes 针对环境变量的存取有许多可行做法，它甚至可以动态为你设置一些环境变量值（比方说，它会把 pod 的名称或者 IP 设置到某个变量中）。最终你会选择采用一个隔离度更好的解决方案，当你要部署多个服务（比方说 Puma 以及 Sidekiq）的时候，就可以为不同的部署场景配置不同的环境变量了。&lt;/p&gt;

&lt;p&gt;往你的容器定义文件&lt;code&gt;config/kube/deployment.yml&lt;/code&gt;中设置下面的参数（可以放在&lt;code&gt;image&lt;/code&gt;属性后面）：&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EXAMPLE&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;This env variable is defined by Kubernetes.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，可以运行这条命令来对集群做变更&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;kubectl apply -f config/kube&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你想知道环境变量有没有设置成功，其实可以去到 app 的内部，把环境变量打印出来验证一下。&lt;/p&gt;

&lt;p&gt;在开发与测试环境，你均可以使用一个名为&lt;a href="https://github.com/bkeepers/dotenv" rel="nofollow" target="_blank" title=""&gt;dotenv&lt;/a&gt;的 Gem 包来简单定义一些环境变量。&lt;/p&gt;

&lt;p&gt;而在生产环境，则可以使用 Kubernetes 的&lt;a href="https://kubernetes.io/docs/concepts/configuration/configmap/" rel="nofollow" target="_blank" title=""&gt;ConfigMaps&lt;/a&gt;来定义环境变量。这种做法的优势在于，环境变量只需定义一次，然后把它们应用到不同的部署任务或者说 pod 中。举个简单的例子，在一个 Rails 应用中，通常需要配置下面这些环境变量。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;config/kube/env.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ConfigMap&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env&lt;/span&gt;
&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
  &lt;span class="na"&gt;RAILS_LOG_TO_STDOUT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enabled&lt;/span&gt;
  &lt;span class="na"&gt;RAILS_SERVE_STATIC_FILES&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;enabled&lt;/span&gt;
  &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://example.com/mydb&lt;/span&gt;
  &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://redis.default.svc.cluster.local:6379/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后把下面的配置添加到容器的定制文件中（可以放在&lt;code&gt;image&lt;/code&gt;属性后面）:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;config/kube/deployment.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;envFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;configMapRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你要记住，用此做法来存放像&lt;code&gt;SECRET_KEY_BASE&lt;/code&gt;这种敏感信息并不安全，因为这个文件会被纳入 Git 仓库中被管理起来！下一个小节，我们会一起看看要怎么活用 Rails 的&lt;a href="https://guides.rubyonrails.org/security.html#environmental-security" rel="nofollow" target="_blank" title=""&gt;credentials 机制&lt;/a&gt;来安全存放你的隐私信息。&lt;/p&gt;
&lt;h2 id="隐私安全"&gt;隐私安全&lt;/h2&gt;
&lt;p&gt;你既可以把隐私信息存放在 Rails 的配置中，也可以利用 Kubernetes 的隐私安全功能 (&lt;a href="https://kubernetes.io/docs/concepts/configuration/secret/" rel="nofollow" target="_blank" title=""&gt;Kubernetes Secrets&lt;/a&gt;) 来存放这些信息。这里，我建议你使用 Rails 的&lt;a href="https://guides.rubyonrails.org/security.html#environmental-security" rel="nofollow" target="_blank" title=""&gt;credentials 机制&lt;/a&gt;来存放隐私信息：我们只会用 Kubernetes 的隐私安全功能来存放&lt;code&gt;master key&lt;/code&gt;。本质上来说，所有的隐私信息都跟应用的源代码一起存放在 Git 仓库中，不过它们是安全的，因为我们使用了&lt;code&gt;master key&lt;/code&gt;来对隐私信息进行了加密，以后就可以利用&lt;code&gt;master key&lt;/code&gt;来访问所有的隐私信息了（&lt;code&gt;master key&lt;/code&gt;不会托管到 Git 仓库中）。&lt;/p&gt;

&lt;p&gt;在&lt;code&gt;config/environments/production.rb&lt;/code&gt;配置文件中开启相应选项&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require_master_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后运行该命令来编辑你的隐私信息&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;EDITOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"vi"&lt;/span&gt; rails credentials:edit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把下面的行添加到文件之后就保存并退出&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;example_secret: foobar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后你可以尝试在应用中访问该隐私信息&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/views/pages/home.html.erb&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;Rails.application.credentials.example_secret&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要清楚的是，从 Rails 6 开始，你甚至可以区分环境来保存各环境不同的隐私信息。接下来如果你在本地启动网站，访问对应的页面便能看到隐私信息了。最后一件需要做的事情是把&lt;code&gt;master key&lt;/code&gt;用 Kubernetes 的隐私安全功能保存起来，以便日后访问。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret generic rails-secrets &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;rails_master_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'example'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通常你&lt;code&gt;master key&lt;/code&gt;的内容会存放在文件&lt;code&gt;config/master.key&lt;/code&gt;中，只是现在内容会存放于 Kubernetes 系统中了。&lt;/p&gt;

&lt;p&gt;最后，我们要把 Kubernetes 系统存放的&lt;code&gt;master key&lt;/code&gt;信息，以环境变量的形式提供给容器使用。在你的&lt;code&gt;config/kube/deployment.yml&lt;/code&gt;配置文件中的&lt;code&gt;env&lt;/code&gt;属性下添加这个环境变量&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RAILS_MASTER_KEY&lt;/span&gt;
  &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rails-secrets&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rails_master_key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要测试是否能够正常工作，你可以重新构建镜像并使用新的配置来部署应用：你将会看到示例中的隐私信息（并非真实的隐私信息）显示在网站首页了。&lt;/p&gt;
&lt;h2 id="日志"&gt;日志&lt;/h2&gt;
&lt;p&gt;日志的记录通常有两种不同的策略&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;把 Rails 应用的日志信息直接发送到一个中心化的日志服务中去。&lt;/li&gt;
&lt;li&gt;把日志直接写入到标准输出 (stdout)，然后让 Docker 或者 Kubernetes 从对应的节点（更准确来说应该是容器）收集日志。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;第一种策略十分简单，就是可能有些低效，且无法收集到 Kubernetes 集群本身的日志信息。要是你选择这种方式，都可以使用类似于&lt;a href="https://github.com/dwbutler/logstash-logger" rel="nofollow" target="_blank" title=""&gt;logstash-logger&lt;/a&gt;的 Gem 来实现相关功能。&lt;/p&gt;

&lt;p&gt;如果你选择第二种策略，那么你可以启用 Rails 应用中的配置项，使得日志信息直接被写入到标准输出 (stdout)。直接把环境变量&lt;code&gt;RAILS_LOG_TO_STDOUT&lt;/code&gt;设置成&lt;code&gt;enabled&lt;/code&gt;就可以了。接着你就可以使用下面的命令查看日志信息：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rails-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本质上来说，当你运行命令的时候，Kubernetes 的主节点会从其他的节点收集最新的日志信息（只从标记了&lt;code&gt;rails-app&lt;/code&gt;的 pod 中）并展示出来。这是一个很好的开端，不过呢，这些日志信息是没有被持久化的，你需要想想办法，让它们能够被方便地检索。要达成这个目的，你需要把日志发送到一个中心化的日志服务中去：我们以&lt;a href="https://logz.io/" rel="nofollow" target="_blank" title=""&gt;Logz.io&lt;/a&gt;为例，它提供了便于管理的&lt;a href="https://www.elastic.co/what-is/elk-stack" rel="nofollow" target="_blank" title=""&gt;ELK stack&lt;/a&gt;。这里会使用&lt;a href="https://www.fluentd.org/" rel="nofollow" target="_blank" title=""&gt;Fluentd&lt;/a&gt;来把日志信息发送到 ELK 服务中去，它是一个 Ruby 语言编写的日志收集工具，也是一个从&lt;a href="https://www.cncf.io/" rel="nofollow" target="_blank" title=""&gt;CNCF&lt;/a&gt;毕业的项目。&lt;/p&gt;

&lt;p&gt;以下是日志记录的工作流程：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;你的 Rails 应用及其他的 Kubernetes 组件都会把日志写入到标准输出 (stdout) 中；&lt;/li&gt;
&lt;li&gt;Kubernetes 收集日志，并把日志存放在节点上；&lt;/li&gt;
&lt;li&gt;在每个不同的节点，你都可以使用&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/" rel="nofollow" target="_blank" title=""&gt;Kubernetes DaemonSet&lt;/a&gt;来运行一个 Fluentd 的 pod；&lt;/li&gt;
&lt;li&gt;Fluentd 会读取节点中的日志信息，并发送到日志服务中心。&lt;/li&gt;
&lt;li&gt;你可以到日志服务中心去查看日志信息，同时你也可以对日志信息进行可视化及检索。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以用下面的命令，简单来为集群安装 Fluentd：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret generic logzio-logs-secret &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;logzio-log-shipping-token&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'MY_LOGZIO_TOKEN'&lt;/span&gt; &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;logzio-log-listener&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'MY_LOGZIO_URL'&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; kube-system

kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/logzio/logzio-k8s/master/logzio-daemonset-rbac.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你需要对配置进行一些定制，可以在运行&lt;code&gt;kubectl apply&lt;/code&gt;之前先把配置文件先下载下来并编辑它。需要记住，即便不使用&lt;a href="https://logz.io/" rel="nofollow" target="_blank" title=""&gt;Logz.io&lt;/a&gt;，而是其他类似的服务，策略也是非常相似的。你还可以在&lt;a href="https://github.com/fluent/fluentd-kubernetes-daemonset" rel="nofollow" target="_blank" title=""&gt;fluent/fluentd-kubernetes-daemonset&lt;/a&gt;这个 Git 仓库中找到许多配置案例。&lt;/p&gt;

&lt;p&gt;现在你可以去访问一下网站，并查看日志信息，来验证日志策略是否已经生效了。&lt;/p&gt;
&lt;h2 id="后台作业"&gt;后台作业&lt;/h2&gt;
&lt;p&gt;为了让一些后台作业可以正常工作，现在我们来看看如何在 Kubernetes 上运行 Sidekiq。&lt;/p&gt;

&lt;p&gt;首先，你需要把 Sidekiq 添加到 Rails 应用中去：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Gemfile&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'sidekiq'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行&lt;code&gt;bundle install&lt;/code&gt;进行安装，接下来就可以创建一个后台任务作为示例：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/jobs/hard_worker.rb&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HardWorker&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Worker&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Do something&lt;/span&gt;
    &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt; &lt;span class="s1"&gt;'It works!'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后在你的&lt;code&gt;PagesController#home&lt;/code&gt;方法中添加下面的行，这样每次有请求进来都会创建一个后台作业。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;HardWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_async&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来到有趣的部分了：我们需要为 Kubernetes 添加新的部署配置，让 Sidekiq 可以在集群中运行。对比我们前面已经完成的 Web 应用部署，这次的部署配置十分简单：只是，这次在容器中运行的主进程并非 Puma，我们想要运行的是 Sidekiq。配置如下&lt;/p&gt;

&lt;p&gt;&lt;code&gt;config/kube/sidekiq.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="ss"&gt;apiVersion: &lt;/span&gt;&lt;span class="n"&gt;apps&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;
&lt;span class="ss"&gt;kind: &lt;/span&gt;&lt;span class="no"&gt;Deployment&lt;/span&gt;
&lt;span class="ss"&gt;metadata:
  name: &lt;/span&gt;&lt;span class="n"&gt;sidekiq&lt;/span&gt;
&lt;span class="ss"&gt;spec:
  replicas: &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="ss"&gt;selector:
    matchLabels:
      app: &lt;/span&gt;&lt;span class="n"&gt;sidekiq&lt;/span&gt;
  &lt;span class="ss"&gt;template:
    metadata:
      labels:
        app: &lt;/span&gt;&lt;span class="n"&gt;sidekiq&lt;/span&gt;
    &lt;span class="ss"&gt;spec:
      containers:
      &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="n"&gt;sidekiq&lt;/span&gt;
        &lt;span class="ss"&gt;image: &lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;kubernetes&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;rails&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="ss"&gt;:latest&lt;/span&gt;
        &lt;span class="ss"&gt;command: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sidekiq"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="ss"&gt;env:
        &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="no"&gt;REDIS_URL&lt;/span&gt;
          &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="ss"&gt;:/&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本质上来说，我们新定义的部署配置会生成两个 pod：每个 pod 都是基于先前定制的 Rails 应用标准镜像运行起来的。这里面最有趣的地方在于，我们可以通过设置&lt;code&gt;command&lt;/code&gt;来覆写掉 Docker 镜像里面的默认命令。你当然也可以通过&lt;code&gt;args&lt;/code&gt;选项来为 Sidekiq 传递一些参数。&lt;/p&gt;

&lt;p&gt;要注意到我们定义了&lt;code&gt;REDIS_URL&lt;/code&gt;这个变量，这样，Sidekiq 才可以连接到对应的 Redis 服务，进而可以读取并执行一些后台作业。当然，也需要在 Web 应用的部署配置里设置相同的变量，这样 Rails 应用才能连接到同一个 Redis 服务，并对一些后台作业进行调度。至于 Redis 服务本身，既可以借助&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/" rel="nofollow" target="_blank" title=""&gt;Kubernetes StatefulSets&lt;/a&gt;，也可以把它安装到自己的服务器上甚至采用第三方托管的解决方案：虽说，管理单点的 Redis 服务十分简单，然而要对 Redis 集群进行扩容却不是一件手到擒来的事情。如果你对可伸缩以及可靠性有一定的要求，需要考虑第三方托管的解决方案。&lt;/p&gt;

&lt;p&gt;不可避免的是，你还需要通过命令&lt;code&gt;kubectl apply -f config/kube&lt;/code&gt;把新的配置应用到 Kubernetes 集群中。&lt;/p&gt;

&lt;p&gt;一切都搞定之后，便可以尝试访问你的站点，检测后台作业有没有正常工作：当加载首页之后，示例后台作业将会被调度，你应该可以通过日志记录看到它的工作效果。&lt;/p&gt;
&lt;h2 id="定时任务"&gt;定时任务&lt;/h2&gt;
&lt;p&gt;当把一切都部署到 Kubernetes 之后，你其实有很多种不同的方案来为其创建定时作业：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;使用 Kubernetes 自带的 CronJob，周期性地运行一个容器。&lt;/li&gt;
&lt;li&gt;借用 Ruby 生态的后台作业进程来调度并运行相关任务。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;第一种策略的问题在于，你还需要为每个不同的定时任务定义对应的 Kubernetes 配置。如果，你喜欢这个解决方案，你可以把&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/" rel="nofollow" target="_blank" title=""&gt;Kubernetes CronJobs&lt;/a&gt;搭配&lt;code&gt;rake tasks&lt;/code&gt;或者&lt;code&gt;rails runner&lt;/code&gt;一起使用。&lt;/p&gt;

&lt;p&gt;而如果你选择第二种解决方案，你可以用 Ruby 更方便地去调度各种定时任务。从本质上来说，你需要一个长时间运行在后台的 Ruby 进程，在当前时间能够与 Cron 定时任务的配置记录相匹配的时候，它可以创建并调度特定任务。&lt;/p&gt;

&lt;p&gt;举个具体的例子，你可以安装&lt;a href="https://github.com/jmettraux/rufus-scheduler" rel="nofollow" target="_blank" title=""&gt;rufus-scheduler&lt;/a&gt;这个 Gem，然后为这个 Gem 启动一个专门的容器来支持定时任务的调度：只不过这样的话你会有单点失败的风险，并且，如果这个 pod 被重新调度，那么当前 pod 中的任务也将丢失。如果你需要一个分布式且可靠的环境，我们需要使用像&lt;a href="https://github.com/sidekiq-cron/sidekiq-cron" rel="nofollow" target="_blank" title=""&gt;sidekiq-cron&lt;/a&gt;这种 Gem：它会基于每一个 Sidekiq 服务进程来运行一个调度线程，同时它也会依赖 Redis 来规避同一个任务被调度多次的情况。举个例子，假设你的 Sidekiq 服务有 N 个进程，那么这 N 个进程每分钟都会去检查调度表 (Cron 配置)，查看当前时间是否能够跟 Cron 配置上的记录相匹配，并尝试获取一个 Redis 锁：如果其中一个线程获得了锁，那么在那个时间点，它将会承担起调度 Sidekiq 任务的责任，否则的话，什么都不用做。最后，这个 Sidekiq 任务会以另一种后台作业的身份正常工作。故而，基于已有的 pod，这种方式要做横向扩容十分简便，并且还可以通过重试机制来保证运作的可靠性。&lt;/p&gt;

&lt;p&gt;我们需要把这个 Gem 添加到 Rails 应用中：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Gemfile&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'sidekiq-cron'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后运行&lt;code&gt;bundle install&lt;/code&gt;并创建一个启动器配置&lt;/p&gt;

&lt;p&gt;&lt;code&gt;config/initializers/sidekiq.rb&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cron&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_from_hash&lt;/span&gt; &lt;span class="no"&gt;YAML&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'config/schedule.yml'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;server?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后定义一个调度计划：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;config/schedule.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;my_first_job&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
  &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HardWorker"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要启动了 Sidekiq 服务，你会发现，不管有多少个 pod 正在运行着，这个任务每分钟会被执行一次。&lt;/p&gt;
&lt;h2 id="命令行控制台"&gt;命令行控制台&lt;/h2&gt;
&lt;p&gt;你可以通过下面这条命令来连接到一个 pod：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;kubectl&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;my&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pod&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="n"&gt;bash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本质上来说，我们会在容器内部启动一个 bash 进程。不仅如此，我们还加入了参数&lt;code&gt;-it&lt;/code&gt;，这样在启动了 bash 之后就可以通过命令行与容器进行一些常规的交互。&lt;/p&gt;

&lt;p&gt;如果你需要列出所有 pod 的名字，可以使用&lt;code&gt;kubectl get pods&lt;/code&gt;这个命令。&lt;/p&gt;

&lt;p&gt;虽说你可以直接连接到任何一个运行中的 pod 来完成日常的维护工作，但是我发现一种更为实用的做法，就是专门为这些维护工作另外调度一个名为&lt;code&gt;terminal&lt;/code&gt;的 pod。创建以下文件，并执行&lt;code&gt;kubectl apply -f kube/config&lt;/code&gt;命令：&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terminal&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terminal&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;username/kubernetes-rails-example:latest&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sleep'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;infinity'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EXAMPLE&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;This env variable is defined by Kubernetes.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所有的容器都必须有一个主进程，否则他们将会退出运行，然后 Kubernetes 则会认为容器已经销毁。然而如果要运行镜像中的默认命令（就是 Rails 服务），则会有点浪费资源，毕竟这种维护类型的 pod 并不需要连接到负载均衡器：我们用&lt;code&gt;sleep infinity&lt;/code&gt;命令来取代原来的默认命令，这本质上是一个&lt;code&gt;no-op&lt;/code&gt;的命令，相对来说消耗资源更少且可以保持容器的持续运作。&lt;/p&gt;

&lt;p&gt;一旦你以 bash 的方式连接上这个 pod，你就可以很轻易地运行各种命令了。然而如果你只想要运行单一命令，也可以在连接的时候直接指定目标命令。举个例子，如果想要在一个名为 terminal 的 pod 里面运行&lt;code&gt;rails console&lt;/code&gt;，可以这样做：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;kubectl&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;terminal&lt;/span&gt; &lt;span class="n"&gt;rails&lt;/span&gt; &lt;span class="n"&gt;console&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你还想要为进程赋予额外的命令行参数，则可以使用&lt;code&gt;--&lt;/code&gt;来对&lt;em&gt;Kubernetes 的参数&lt;/em&gt;跟&lt;em&gt;目标命令以及它的参数&lt;/em&gt;两者之间做分割。举个例子：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;kubectl&lt;/span&gt; &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;terminal&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="n"&gt;rails&lt;/span&gt; &lt;span class="n"&gt;console&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="n"&gt;production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PS: 不加&lt;code&gt;--&lt;/code&gt;的写法已经废弃了，&lt;em&gt;kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.&lt;/em&gt;，做分割尽量都使用&lt;code&gt;--&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="Rake任务"&gt;Rake 任务&lt;/h2&gt;
&lt;p&gt;要在 Kubernetes 中运行 Rake 任务有两种不同的方式：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;可以创建一个&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/job/" rel="nofollow" target="_blank" title=""&gt;Kubernetes Job&lt;/a&gt;然后让 Rake 任务可以在一个专门容器中运行。&lt;/li&gt;
&lt;li&gt;可以连接到一个已经存在的 pod，并在上面运行 Rake 任务。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;简单起见，我更喜欢第二种做法。&lt;/p&gt;

&lt;p&gt;运行下面的命令来列出所有可用的 pod：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以用下面的命令来运行一个 Rake 任务&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;my-pod-name rake task-name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;官方会建议写成&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;my-pod-name &lt;span class="nt"&gt;--&lt;/span&gt; rake task-name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意，&lt;code&gt;kubectl exec&lt;/code&gt;命令会返回 Rake 任务执行结果的状态码（比方说如果 Rake 任务运行成功，就会返回 0）。&lt;/p&gt;
&lt;h2 id="数据库变更"&gt;数据库变更&lt;/h2&gt;
&lt;p&gt;每当你部署最新版本 Rails 应用的时候，要在不停机的状态下对它所依赖的数据库做变更并非易事。问题的根源大多在于，不管是把最新的代码部署到所有的 pod 上还是利用 Rails 的&lt;a href="https://guides.rubyonrails.org/active_record_migrations.html" rel="nofollow" target="_blank" title=""&gt;Migration 机制&lt;/a&gt;对数据库做变更都是耗时任务，它们都需要一定的时间才能执行完毕。从本质上来说，这些操作既非瞬时亦非原子，在部署完成之前，你至少需要面对以下两种场景中的一种。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;老代码运行在最新的数据表模式上。&lt;/li&gt;
&lt;li&gt;新代码运行在老的数据表模式上。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;让我们更细致地去分析一些策略：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;停机：如果你能够接受变更过程中的停机，那么只消简单地把 pod 的数量降到 0，并执行一个用于数据表变更的&lt;code&gt;Kubernetes Job&lt;/code&gt;，当变更完毕之后再把 pod 的数量升上去即可。&lt;strong&gt;优点：&lt;/strong&gt;简单，你的应用代码不需要有额外的依赖；也不存在部署过程中因为代码运行在不匹配的数据表模式中而造成的运行时错误。&lt;strong&gt;缺点：&lt;/strong&gt;得停机一段时间。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;先部署新代码，然后做数据表变更：这是在 Heroku 上实施变更的传统做法。举个实际点的例子，你会先把代码部署出去，所有的 pod 都会依照最新的镜像进行更新。当一切都完成之后，你再去实施数据库的变更。咋一看，这种做法十分完美，前提是你可以让代码适应老的数据表模式。然而，当最新数据表模式被更新完毕之后，你依旧需要处理掉 Ruby 进程中&lt;code&gt;ActiveRecord&lt;/code&gt;中老数据表模式的缓存（因此需要做额外的重启动作）。如果你选择这种策略，可以先部署新代码，完成之后简单连接到其中一个 pod，并运行&lt;code&gt;rake db:migrate&lt;/code&gt;。&lt;strong&gt;优点：&lt;/strong&gt;不需要停机；部署起来非常简单。&lt;strong&gt;缺点：&lt;/strong&gt;要让代码向后兼容十分困难；而且在数据库变更完毕之后你可能还需要额外的重启动作。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;先做数据库变更，然后再部署新代码：这是一个比较通用的手段，&lt;a href="https://capistranorb.com/" rel="nofollow" target="_blank" title=""&gt;Capistrano&lt;/a&gt;便是采用了这种方案，&lt;a href="https://devcenter.heroku.com/articles/release-phase" rel="nofollow" target="_blank" title=""&gt;Heroku Release Phase&lt;/a&gt;包括其他一些 CI/CD 工具也是。这种做法的问题在于，滚动更新多个 pod 会需要点时间，并且在这个时间段内，可能会有些老的代码在新的数据表模式上运行。为了避免这个短暂间隔中发生的异常，你需要让新的数据表模式向后兼容，换句话说就是让它能够跟新/老代码一同运行：然而要编写出这种在不停机情况下的数据表变更并非易事，且里面会有许多陷阱。为了规避掉一些额外的问题，你还应该为每个版本的镜像都打上不同的标签（不仅仅只是用 latest），此外，镜像的调度操作是原子性的，应该要在数据表变更完成之前就把镜像下载好。如果你选择了这个策略，需要创建一个&lt;code&gt;Kubernetes Job&lt;/code&gt;，并基于最新的镜像来调度出一个 pod，在里面实施数据表变更。在变更完毕之后，再用最新的镜像去更新其他所有的 pod。&lt;strong&gt;优点：&lt;/strong&gt;这是一个比较常用的策略，可靠性方面有一定保障。&lt;strong&gt;缺点：&lt;/strong&gt;如果你并不能编写向后兼容的数据库变更，部署最新代码的过程中可能会引发一些错误。如果你还想规避掉新代码运行在老数据表模式这种意外场景，可能需要为不同版本的 Docker 镜像都打上不同的标签。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果我们选择最后一种解决方案，那我们必须要定义一个像下面这样的任务：&lt;/p&gt;

&lt;p&gt;&lt;code&gt;config/kube/migrate.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;migrate&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;migrate&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;username/kubernetes-rails-example:latest&lt;/span&gt;
          &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rails'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;db:migrate'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;…&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着你可以使用以下的命令来实施数据表变更&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; config/kube/migrate.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，便可以看到数据表变更的状态&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe job migrate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的命令会显示出执行数据表变更任务所在 pod 的名字。你也可以通过命令看到相关的日志&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs pod-name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一旦迁移任务完毕，就可以把它删除了，借此，可以节省出一些资源，而且在未来有需要的时候还能够重新运行一遍：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete job migrate
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="持续部署"&gt;持续部署&lt;/h2&gt;
&lt;p&gt;在前面的各个小节中，我们已经配置好 Kubernetes 集群并且手动把代码部署出去了。然而，如果能把这一切封装成一个简单的命令，让你随时都能部署，那将十分便利。&lt;/p&gt;

&lt;p&gt;创建下面的文件并赋予&lt;em&gt;可执行&lt;/em&gt;权限（使用 chmod +x deploy.sh）&lt;/p&gt;

&lt;p&gt;&lt;code&gt;deploy.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh -ex&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;KUBECONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.kube/kubernetes-rails-example-kubeconfig.yaml
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; username/kubernetes-rails-example:latest &lt;span class="nb"&gt;.&lt;/span&gt;
docker push username/kubernetes-rails-example:latest
kubectl create &lt;span class="nt"&gt;-f&lt;/span&gt; config/kube/migrate.yml
kubectl &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;complete&lt;/span&gt; &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600s job/migrate
kubectl delete job migrate
kubectl delete pods &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;rails-app
kubectl delete pods &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sidekiq
&lt;span class="c"&gt;# For Kubernetes &amp;gt;= 1.15 replace the last two lines with the following&lt;/span&gt;
&lt;span class="c"&gt;# in order to have rolling restarts without downtime&lt;/span&gt;
&lt;span class="c"&gt;# kubectl rollout restart deployment/kubernetes-rails-example-deployment&lt;/span&gt;
&lt;span class="c"&gt;# kubectl rollout restart deployment/sidekiq&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后你就可以通过这条命令简单地发布新版本了&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./deploy.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的命令会执行以下步骤：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;使用&lt;code&gt;sh&lt;/code&gt;作为解释器，设置可选项来打印每一条命令，并在出错的时候自动退出运行。&lt;/li&gt;
&lt;li&gt;本地构建并推送 Docker 镜像到远端镜像仓库。&lt;/li&gt;
&lt;li&gt;实施数据表变更，等变更完毕之后把任务删除。&lt;/li&gt;
&lt;li&gt;最后发布新代码/镜像。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;当然，你还要记得，每次对 Kubernetes 的配置做了修改，都需要用这条命令来应用最新的配置：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; kube/config
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="监控"&gt;监控&lt;/h2&gt;
&lt;p&gt;出于一些原因，你还需要对 Kubernetes 集群做一些监控，比方说：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;了解资源的使用情况，并对相关的集群进行扩容。&lt;/li&gt;
&lt;li&gt;检查资源的使用情况是否正常，比如，会不会出现 pod 占用了大量系统资源的场景。&lt;/li&gt;
&lt;li&gt;核查是否所有的 pod 都运行正常，有没有运行失败的实例。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一般情况下，Kubernetes 服务的供应商会提供了一个控制面板，面板里面会包含许多有用的状态信息，所有节点的 CPU 使用率，平均负载，内存使用率，磁盘使用率，带宽使用情况等等。通常我们会通过一个&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/" rel="nofollow" target="_blank" title=""&gt;Kubernetes DaemonSet&lt;/a&gt;来收集状态相关的信息，该行为跟先前描述的日志收集行为十分相似。根据你的喜好，当然也可以在每一个节点上安装可定制化的监控代理。你可以使用诸如&lt;a href="https://prometheus.io/" rel="nofollow" target="_blank" title=""&gt;Prometheus&lt;/a&gt;这样的开源产品，又或者是类似于&lt;a href="https://www.datadoghq.com/" rel="nofollow" target="_blank" title=""&gt;Datadog&lt;/a&gt;这样的第三方服务。&lt;/p&gt;

&lt;p&gt;也有其他可用于监控应用性能的手段：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;在集群上安装 Kubernetes 的&lt;a href="https://github.com/kubernetes-sigs/metrics-server" rel="nofollow" target="_blank" title=""&gt;metrics-server&lt;/a&gt;，它会把状态信息存放在内存中，然后你可以使用&lt;code&gt;kubectl top nodes/pods&lt;/code&gt;这样的命令来查看信息。&lt;/li&gt;
&lt;li&gt;直接使用记录在负载均衡器中的状态信息。&lt;/li&gt;
&lt;li&gt;通过外部服务往应用程序中发送 ad-hoc 请求，根据响应情况，收集并合成对应的度量信息。比方说，要在外部的观测点度量某请求的响应时间。&lt;/li&gt;
&lt;li&gt;使用特定的 Gem 直接从 Rails 应用程序收集状态信息，借此来完成对应用的性能监控，比如说&lt;a href="https://www.datadoghq.com/product/apm/" rel="nofollow" target="_blank" title=""&gt;Datadog APM&lt;/a&gt;以及&lt;a href="https://newrelic.com/" rel="nofollow" target="_blank" title=""&gt;New Relic APM&lt;/a&gt;（笔者公司正在用它）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="安全更新"&gt;安全更新&lt;/h2&gt;
&lt;p&gt;在使用容器的过程中，人们会有一个思想误区，觉得可以忘掉安全更新这回事。即便 Docker 镜像以及容器都增添了额外的层级用作隔离，安全更新依旧重要。事实上，不管是新的虚拟主机---可以理解成 pod，或是其他的容器，它们的存在都是短暂的。从安全的角度来看，这是很好的，然而为了避免应用服务被攻击，我们依旧需要对它们进行升级。请记住，如果安全漏洞处在更底层，那么应用服务遭受攻击依旧是可能的，比方说，安全漏洞处在操作系统层又或者是基础镜像所包含的代码库中的时候。&lt;/p&gt;

&lt;p&gt;如果你在 Kubernetes 上运行 Rails 应用，那么请记住要对以下层级进行更新：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes 集群及节点：&lt;/strong&gt;大多数的 Kubernetes 服务提供商都会为你的底层节点以及 Kubernetes 服务套件实施更新，故而你可以把需要着手更新该层级的事情抛诸脑后。然而，你可能要开启自动更新的选项：假设你是使用 DigitalOcean，请记得要去到 Kubernetes 的设置面板，并启动&lt;strong&gt;自动更新&lt;/strong&gt;开关。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker 镜像以及容器：&lt;/strong&gt;你需要让容器始终保持最新版本。实际操作中，请确保你的基础镜像使用的是最新版。如果你用 Ruby 官方镜像作为基础镜像，应该使用&lt;code&gt;2.5&lt;/code&gt;这样的标签，而不是&lt;code&gt;2.5.1&lt;/code&gt;，这样，当有新安全补丁 (patch) 的时候，容器也会自动应用最新的版本，你也不需要一直提醒自己要记得去提升补丁版本号。然而，这样还不够：每当有操作系统新补丁的时候，Ruby 的维护者会根据最新的操作系统发布新的 Ruby 镜像，却使用了相同的标签（比方说，都使用了&lt;code&gt;2.5&lt;/code&gt;作为标签的 Ruby 镜像，底层系统并不总是一样的）。这意味着，你需要经常到 Docker Hub 上查看，检查基础镜像是否有接收到一些更新（或者订阅 Ruby，Ubuntu 的官方安全邮件列表）：如果有可用更新，重新构建并发布你的镜像。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rails 应用和相关依赖：&lt;/strong&gt;请记得要升级你的 Ruby 以及 Rails 的版本。这当然也包括应用所依赖的 Gem 包跟 Yarn 包，也包括其他类型的软件包。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;其他：&lt;/strong&gt;你当然也需要升级 Kubernetes 之外的数据库及其他第三方服务。通常来说，使用第三方托管的数据库服务会比较便于管理。服务提供商会自动为对应服务实施安全更新，你可以忘掉对他们的更新工作。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;基本上，如果你使用的是第三方托管的服务（不管是 Kubernetes 还是数据库），并频繁地把自己的应用部署到上面，一般不需要特意去做什么事情：只需要让你的 Rails 应用保持最新版本即可。然而，如果你发布应用的频率并不高，记得要定期重新构建镜像，不要让你的应用几个月都运行在一个早已过期的基础镜像上。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;这篇文章，基本覆盖了投产时把 Ruby On Rails 应用部署到 Kubernetes 集群上所需要的各方面知识。&lt;/p&gt;

&lt;p&gt;在数百个节点上实施应用扩容或是配置变更，在现如今是一件十分简单的事情，只需要一个运维层面的操作就能够实现。这要感谢 Kubernetes 的强大。对比于像 Heroku 这样的 PaaS 服务，它更具性价比，且移植性更好。&lt;/p&gt;

&lt;p&gt;请记住，要达到一定的高可用性，并实现世界范围内的扩容，你需要规避掉一些瓶颈情况以及单点错误，在实际操作中会包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;负载均衡，&lt;/strong&gt;如果它是一个单一的服务，可能会成为瓶颈所在；在条件允许的情况下，你可以使用更好的硬件设备，不过最后你可能还是得使用&lt;a href="https://en.wikipedia.org/wiki/Round-robin_DNS" rel="nofollow" target="_blank" title=""&gt;Round-Robin DNS&lt;/a&gt;把客户请求分发到不同的负载均衡器或部署节点来提高吞吐能力；如果你使用类似&lt;a href="https://www.cloudflare.com/" rel="nofollow" target="_blank" title=""&gt;CloudFlare&lt;/a&gt;这样的全局网络服务，它们甚至可以对你的负载均衡器实行健康检查，使其免受 DDoS 攻击并缓存大多数的请求。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;数据库&lt;/strong&gt;，如果是单点的数据库服务，便有可能会成为瓶颈所在；你可以考虑使用更好的硬件设施和热备份服务。只是到最后，可能还得迁移到一些支持分片的数据库管理系统（DBMS）上，分片意味着数据会自动分发到不同的数据库节点上，每个节点都管理着某个范围的 key；数据库的客户端（比方说 Rails 应用内部的数据库适配器）首次请求数据库集群是要了解当前集群的配置信息，下一次则会到正确的数据库实例中去做进一步的查询，因此可以规避掉一些瓶颈。此外，分片节点之间互为复制集，这是为了在某些节点硬件发生异常的时候保护数据免受损害，故而它们会被称为分片复制集（Sharded Replica）；MongoDB 和 Redis Cluster 提供的案例就与我们所描述的策略类似，市面上也存在着许多第三方托管的解决方案。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src="https://step-by-step.oss-cn-beijing.aliyuncs.com/production/d7rxc1ls15bs03g8vgpzrnks8r3o" title="" alt="DSCF0410.jpg"&gt;&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Sat, 11 Feb 2023 09:22:42 +0800</pubDate>
      <link>https://ruby-china.org/topics/42869</link>
      <guid>https://ruby-china.org/topics/42869</guid>
    </item>
    <item>
      <title>M1 芯片构建容器镜像的跨平台问题</title>
      <description>&lt;p&gt;这个问题是笔者尝试把项目部署到 k8s 集群上的时候遇到的，简单记录一下。估计使用 M1 芯片的 Macbook 构建镜像的开发者多少都会遇到类似的问题。原文链接： &lt;a href="https://step-by-step.tech/posts/m1-build-image-issue" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/m1-build-image-issue&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://step-by-step.oss-cn-beijing.aliyuncs.com/production/g5heee8ax1xj2kngsnvaidq0nuoj" title="" alt="L1002263.jpg"&gt;&lt;/p&gt;
&lt;h2 id="能在本地运行却无法在k8s集群上运行的镜像"&gt;能在本地运行却无法在 k8s 集群上运行的镜像&lt;/h2&gt;
&lt;p&gt;当要把某个项目部署到 k8s 集群的时候，必先要对这个项目进行容器化。也就是需要编写对应的&lt;code&gt;Dockerfile&lt;/code&gt;文件。项目运行所需要依赖的环境都将在&lt;code&gt;Dockerfile&lt;/code&gt;文件中指定。不过这里面也会有些坑。&lt;/p&gt;

&lt;p&gt;笔者遇到的问题是&lt;strong&gt;构建出来的镜像可以在本地运行，但是在 k8s 集群上就是运行失败&lt;/strong&gt;。要说明的是，笔者的本地机器是 M1 芯片的 Macbook。&lt;/p&gt;

&lt;p&gt;比如&lt;code&gt;lanzhiheng/stone&lt;/code&gt;这个镜像，在本地&lt;code&gt;docker run&lt;/code&gt;本地好好的&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 4000:3000 lanzhiheng/stone
...
&lt;span class="k"&gt;*&lt;/span&gt; Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是在 k8s 运行不了（这里是先把镜像推送到 Docker Hub，然后 k8s 从上面去拉取）&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; kubectl run blog-on-k8s &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lanzhiheng/stone

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; kubectl get pods  blog-on-k8s
NAME          READY   STATUS             RESTARTS      AGE
blog-on-k8s   0/1     CrashLoopBackOff   3 &lt;span class="o"&gt;(&lt;/span&gt;24s ago&lt;span class="o"&gt;)&lt;/span&gt;   88s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而且出问题的时候，是没办法看到更详细的日志的。该服务目前的状态就是 Pod 还在，但是里面的 Container(容器) 启动失败，所以没有办法进入容器内部实施调试。&lt;/p&gt;

&lt;p&gt;笔者初出茅庐，一直在猜想会不会是 k8s 有专门的镜像制作方式，会不会 Docker 制作的镜像需要某种特殊处理才能在 k8s 上使用？然而上网搜了一下始终没有找到相关的镜像处理器，看来正常情况下 Docker 打包的镜像应该是可以直接用在 k8s 上了。为了简化这个问题，笔者弄了个更简单的镜像来测试。&lt;/p&gt;
&lt;h2 id="简化问题"&gt;简化问题&lt;/h2&gt;
&lt;p&gt;为了测试 Docker 的镜像是不是真的能在 k8s 上面使用，直接在 k8s 跑一下 nginx 的 Docker 官方镜像就知道了。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; kubectl run  nginx &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nginx

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; kubectl get pods nginx
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          65s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还是能跑起来的。这么说 Docker 的镜像还是能用在 k8s 上的，那就是笔者构建的镜像有问题了，我再尝试往官方镜像套一层试试看，&lt;code&gt;Dockerfile&lt;/code&gt;文件尽可能简单&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM nginx
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; docker build &lt;span class="nt"&gt;-t&lt;/span&gt; lanzhiheng/nginx  &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; docker push lanzhiheng/nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地用 Docker 服务针对该镜像创建容器是没啥问题的&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; docker run &lt;span class="nt"&gt;-it&lt;/span&gt; lanzhiheng/nginx
...
2022/12/31 03:48:34 &lt;span class="o"&gt;[&lt;/span&gt;notice] 1#1: signal 28 &lt;span class="o"&gt;(&lt;/span&gt;SIGWINCH&lt;span class="o"&gt;)&lt;/span&gt; received
2022/12/31 03:48:34 &lt;span class="o"&gt;[&lt;/span&gt;notice] 1#1: signal 28 &lt;span class="o"&gt;(&lt;/span&gt;SIGWINCH&lt;span class="o"&gt;)&lt;/span&gt; received
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而推送到 Docker Hub，然后 k8s 拉下来跑就有问题了&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; kubectl run my-nginx &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lanzhiheng/nginx

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; kubectl get pods  my-nginx
NAME       READY   STATUS             RESTARTS     AGE
my-nginx   0/1     CrashLoopBackOff   1 &lt;span class="o"&gt;(&lt;/span&gt;8s ago&lt;span class="o"&gt;)&lt;/span&gt;   74s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;官方的镜像&lt;code&gt;nginx&lt;/code&gt;跟笔者自己包了一层的镜像&lt;code&gt;lanzhiheng/nginx&lt;/code&gt;本质上并无太大区别。然而笔者的镜像在自己的 Macbook 上用 Docker 跑没啥问题，然后到了线上的 k8s 环境就有问题了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;现在想想最可能的原因就是笔者的 M1 芯片 Macbook 打包的镜像无法在基于 Linux 的 k8s 服务上直接使用。&lt;/strong&gt;后来笔者跑去一台 Linux 服务器上构建一摸一样的镜像（同一个 Dockerfile）&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; kubectl run my-nginx-from-linux &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lanzhiheng/nginx-from-linux

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; kubectl get pods  my-nginx-from-linux
NAME                  READY   STATUS    RESTARTS   AGE
my-nginx-from-linux   1/1     Running   0          55s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这次能运行成功，这么看来问题就出在 M1 芯片的 Macbook 上了。看来系统不同构建的镜像还是不能通用的。&lt;/p&gt;
&lt;h2 id="解决方案"&gt;解决方案&lt;/h2&gt;
&lt;p&gt;从前面的现象可知，M1 芯片 Macbook 在默认情况下构建的镜像在 Macbook 上使用没啥太大问题，然后到了 Linux 的机器上可能就有问题了。不管是用基于 Linux 的 Docker 来跑还是 k8s 来跑都是不行的&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; docker run lanzhiheng/nginx

WARNING: The requested image&lt;span class="s1"&gt;'s platform (linux/arm64/v8) does not match the detected host platform (linux/amd64) and no specific platform was requested
exec /docker-entrypoint.sh: exec format error
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类似的提示信息，在 k8s 环境下还不一定能看得到，所以打包镜像的时候一定要注意跨平台的问题。其实解决方案也很简单，我们只需要在 M1 芯片的 Mac 上构建镜像的时候加上参数&lt;code&gt;--platform linux/amd64&lt;/code&gt;，那么打包出来的镜像就可以在 Linux 的机器上使用了。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; docker build &lt;span class="nt"&gt;-t&lt;/span&gt; lanzhiheng/nginx-for-linux &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64  &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; docker push lanzhiheng/nginx-for-linux
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单点我就不登录 Linux 的机器了，直接用 k8s 来跑了（我的 k8s 连接了远端的集群，M1 的 Macbook 有点跑不起 k8s 服务）。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; kubectl run nginx-from-macbook-image &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lanzhiheng/nginx-for-linux
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; kubectl get pods  nginx-from-macbook-image
NAME                       READY   STATUS    RESTARTS   AGE
nginx-from-macbook-image   1/1     Running   0          55s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里只是以&lt;code&gt;nginx&lt;/code&gt;的镜像作个例子，Rails 项目的解决方案也是类似，只需要加上参数&lt;code&gt;--platform linux/amd64&lt;/code&gt;就能在 M1 的 Macbook 上构建出可以在 Linux 平台上使用的镜像了。这里就不赘述了，反正笔者亲测可用。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;今天这个问题算是项目容器化之路中耽误我最长时间的问题了，一个系统兼容性的问题，但这种问题对于新手来说真的很难排查。笔者调试这个问题花了大概 2 天的时间，有点绝望。后面尝试把问题简化（换别的官方镜像来做试验），一步步排除问题，起码确认了不是&lt;code&gt;Dockerfile&lt;/code&gt;的问题，最终才定位到是镜像构建的系统不兼容所导致的。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;其实笔者只要长个心眼，去 Linux 的机器上用该环境的 Docker 服务运行一下 Macbook 构建出来的镜像就能从提示信息中更快地定位出问题了，而不用一直去折腾 k8s 的配置，说到底还是经验不足啊。&lt;/em&gt;&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Tue, 03 Jan 2023 20:34:04 +0800</pubDate>
      <link>https://ruby-china.org/topics/42820</link>
      <guid>https://ruby-china.org/topics/42820</guid>
    </item>
    <item>
      <title>解决 Rails 项目容器化途中日志时差问题</title>
      <description>&lt;p&gt;这是笔者对项目进行容器化的时候发现的，简单写个文章来记录一下。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://step-by-step.oss-cn-beijing.aliyuncs.com/production/xej25puemsryuip5scf13y7xz91v" title="" alt="L1000861.jpg"&gt;&lt;/p&gt;
&lt;h2 id="容器里的日志时间跟当前时间对不上"&gt;容器里的日志时间跟当前时间对不上&lt;/h2&gt;
&lt;p&gt;以下是我的 Rails 项目的&lt;code&gt;Dockfile&lt;/code&gt;文件全貌&lt;/p&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ruby:2.7.2&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;curl https://deb.nodesource.com/setup_14.x | bash
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs postgresql-client

&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; yarn
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; /app
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; Gemfile Gemfile.lock ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;bundler
&lt;span class="c"&gt;# RUN bundle config set force_ruby_platform true&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;bundle &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;yarn &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; RAILS_ENV=production&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;rails assets:precompile

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["rails", "server", "-b", "0.0.0.0"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过命令&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; docker build &lt;span class="nt"&gt;-t&lt;/span&gt; lanzhiheng/stone  &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就能构建出想要的镜像，先把项目跑起来&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 4000:3000 lanzhiheng/stone
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有意思的事情来了，我随便贴几条日志&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;I, &lt;span class="o"&gt;[&lt;/span&gt;2022-12-26T14:17:24.775420 &lt;span class="c"&gt;#1]  INFO -- : [be1b63ce-4696-4f70-acd9-425c37bd9f0e] Processing by HomeController#index as HTML&lt;/span&gt;
D, &lt;span class="o"&gt;[&lt;/span&gt;2022-12-26T14:17:24.799921 &lt;span class="c"&gt;#1] DEBUG -- : [be1b63ce-4696-4f70-acd9-425c37bd9f0e]   Rendering layout layouts/application.html.erb&lt;/span&gt;
D, &lt;span class="o"&gt;[&lt;/span&gt;2022-12-26T14:17:24.801253 &lt;span class="c"&gt;#1] DEBUG -- : [be1b63ce-4696-4f70-acd9-425c37bd9f0e]   Rendering home/index.html.erb within layouts/application&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;笔者请求发送于北京时间&lt;code&gt;2022-12-26 22:17:24&lt;/code&gt;，然而日志记录到的时间是&lt;code&gt;2022-12-26 14:17:24&lt;/code&gt;相差了整整八个小时，如果没猜错容器里面使用的时间应该是格林威治时间，也就是常说的零时区。然而我查了一下 Rails 的配置&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;docker&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;lanzhiheng&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;stone&lt;/span&gt;  &lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;rails&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time_zone&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Asia/Shanghai"&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Mon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt; &lt;span class="no"&gt;Dec&lt;/span&gt; &lt;span class="mi"&gt;2022&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;06&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;614421836&lt;/span&gt; &lt;span class="no"&gt;CST&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认已经设置好东八区，这么看来，日志系统所认的时区可能跟 Rails 本身的设置没有太大关系。&lt;/p&gt;
&lt;h2 id="Time.now还是Time.current"&gt;Time.now 还是 Time.current&lt;/h2&gt;
&lt;p&gt;从 Rails 的配置信息可知，Rails 的默认日志系统会依赖于&lt;code&gt;Logger::Formatter&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log_formatter&lt;/span&gt;

&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;#&amp;lt;Logger::Formatter:0x000000010ad622d8 @datetime_format=nil&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 Ruby 标准库所定义的类，再去窥探一下 Ruby 标准库的&lt;a href="https://github.com/ruby/logger/blob/019c6cfcdc0ad861714904e482981c556d383558/lib/logger.rb#L666" rel="nofollow" target="_blank" title=""&gt;源代码&lt;/a&gt;，发现每次写日志都会调用这行代码&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="vi"&gt;@logdev.write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;format_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;format_severity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;progname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用的时候会获取当前时间。真相大白了，看来日志的时间是按照&lt;code&gt;Time.now&lt;/code&gt;来定，而不是笔者直觉所认为的&lt;code&gt;Time.current&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;刚开始接触 Rails 的时候，有个前辈就告知我&lt;code&gt;Time.now&lt;/code&gt;跟&lt;code&gt;Time.current&lt;/code&gt;的不同。简单来说&lt;code&gt;Time.current&lt;/code&gt;会根据 Rails 系统设置的时区来返回当前时间，而&lt;code&gt;Time.now&lt;/code&gt;则不会。&lt;/em&gt;为啥这样？其实也很简单，毕竟&lt;code&gt;Time.current&lt;/code&gt;是 Rails 引入的扩展方法，而&lt;code&gt;Time.now&lt;/code&gt;是 Ruby 标准库里面固有的方法。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;irb&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2022&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;15.066059&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;0800&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;
&lt;span class="no"&gt;Traceback&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
 &lt;span class="o"&gt;.....&lt;/span&gt;
&lt;span class="no"&gt;NoMethodError&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;undefined&lt;/span&gt; &lt;span class="nb"&gt;method&lt;/span&gt; &lt;span class="sb"&gt;`current' for Time:Class)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;标准库本来就有的东西，确实没必要认 Rails 体系所设置的时区信息，它始终以所在系统时区为准。在 Rails 上下文中测试，两者确实是足足差了 8 个小时。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;rails&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Mon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt; &lt;span class="no"&gt;Dec&lt;/span&gt; &lt;span class="mi"&gt;2022&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;37.818203586&lt;/span&gt; &lt;span class="no"&gt;CST&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mo"&gt;00&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2022&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;29&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;39.691374504&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mo"&gt;0000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="简单解决一下"&gt;简单解决一下&lt;/h2&gt;
&lt;p&gt;要解决这个问题其实也简单，毕竟容器技术的优势就在于&lt;strong&gt;我们可以灵活把控运行时的系统环境&lt;/strong&gt;。既然&lt;code&gt;Time.now&lt;/code&gt;是根据系统的时区来返回当前时间，我们只需要把系统强行设置成东八区即可。简单修改&lt;code&gt;Dockerfile&lt;/code&gt;文件就能做到这一点&lt;/p&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;...
 RUN mkdir /app
 WORKDIR /app
 COPY Gemfile Gemfile.lock ./
+
+ENV TZ="Asia/Shanghai"
+
 RUN gem install bundler
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加一行来设置环境变量&lt;code&gt;TZ="Asia/Shanghai"&lt;/code&gt;，就能让我们得到一台时区为东八区的机器。我们在“新的机器”上再运行目标服务，应该就能看到上面的日志时间跟你自己电脑上的时间能对得上了。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; docker run &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 4000:3000 lanzhiheng/stone
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看看这次日志对不对&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;D, &lt;span class="o"&gt;[&lt;/span&gt;2022-12-28T21:48:08.376092 &lt;span class="c"&gt;#1] DEBUG -- : [f01bea0d-a85b-4f85-b67a-1e9cfdb27801]   Rendered application/_header.html.erb (Duration: 496.8ms | Allocations: 26874)&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-12-28T21:48:08.376501 &lt;span class="c"&gt;#1]  INFO -- : [f01bea0d-a85b-4f85-b67a-1e9cfdb27801]   Rendered layout layouts/application.html.erb (Duration: 521.9ms | Allocations: 30795)&lt;/span&gt;
I, &lt;span class="o"&gt;[&lt;/span&gt;2022-12-28T21:48:08.377159 &lt;span class="c"&gt;#1]  INFO -- : [f01bea0d-a85b-4f85-b67a-1e9cfdb27801] Completed 404 Not Found in 526ms (Views: 268.9ms | ActiveRecord: 256.2ms | Allocations: 31453)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这次就正常了，容器内外的时差消失了。笔者确实是在北京时间&lt;code&gt;2022-12-28 21:48:08&lt;/code&gt;发送的请求。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;项目的容器化之路是一条不归路，路途十分遥远且艰难，有数不尽的深坑在前方等待。镜像所导致的日志时差问题仅仅是迈向深坑之前的一点小磕碰。笔者简单记录，并解决一下，读者请酌情参考。&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Wed, 28 Dec 2022 22:03:00 +0800</pubDate>
      <link>https://ruby-china.org/topics/42811</link>
      <guid>https://ruby-china.org/topics/42811</guid>
    </item>
    <item>
      <title>聊聊 Rails 中的 Cache Stores</title>
      <description>&lt;p&gt;最近被 Rails 的缓存坑得有点惨，笔者只知道缓存要启动，却忘记缓存要合适才有意义。这篇文章简单聊聊&lt;a href="https://guides.rubyonrails.org/caching_with_rails.html#cache-stores" rel="nofollow" target="_blank" title=""&gt;Cache Stores&lt;/a&gt;。原文链接：&lt;a href="https://step-by-step.tech/posts/cache-stores-in-rails" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/cache-stores-in-rails&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://step-by-step.oss-cn-beijing.aliyuncs.com/production/j651pzufi5jhkz42hqjn76ciux5p" title="" alt="4FFFF00B-6529-40E0-9D92-FD350385427E-10705-00000724CC22ADAB.JPG"&gt;&lt;/p&gt;
&lt;h2 id="开发环境缓存"&gt;开发环境缓存&lt;/h2&gt;
&lt;p&gt;一般来说在开发环境并不需要打开缓存，只不过有时候为了调试缓存的线上效果，需要在开发环境打开缓存功能。开启的方式也很简单，直接本地运行命令&lt;code&gt;bin/rails dev:cache&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; bin/rails dev:cache
Development mode is now being cached.

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  bin/rails dev:cache
Development mode is no longer being cached.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果会提示你，开发模式是否已经开启了缓存。其实打开文件&lt;code&gt;config/environments/development.rb&lt;/code&gt;，可以看到这么一段代码&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tmp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'caching-dev.txt'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_caching&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enable_fragment_cache_logging&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:memory_store&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_file_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s1"&gt;'Cache-Control'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"public, max-age=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_caching&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;

    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:null_store&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原理很简单，如果&lt;code&gt;tmp/caching-dev.text&lt;/code&gt;这个文件存在表示缓存已经开启，否则的话表示开发环境不开启缓存，所以你也可以通过命令&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;touch &lt;/span&gt;tmp/caching-dev.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启开发环境的缓存，再通过命令&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;rm &lt;/span&gt;tmp/caching-dev.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来关闭开发环境的缓存，效果跟官方脚本是一样的。&lt;/p&gt;

&lt;p&gt;从配置&lt;code&gt;config.cache_store = :memory_store&lt;/code&gt;可以看出，当前采用的 Cache Store 是&lt;code&gt;memory_store&lt;/code&gt;。具体文档可以看&lt;a href="https://guides.rubyonrails.org/caching_with_rails.html#activesupport-cache-memorystore" rel="nofollow" target="_blank" title=""&gt;这里&lt;/a&gt;。也可以通过内省的方式查看&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="c1"&gt;#ActiveSupport::Cache::MemoryStore entries=0, size=0, options={}&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见开发环境的缓存存储器是&lt;code&gt;ActiveSupport::Cache::MemoryStore&lt;/code&gt;对应了&lt;code&gt;memory_store&lt;/code&gt;，也就是把缓存内容存放在机器的内存里面。配置起来相对比较简单，毕竟每台机器都有内存资源，只要设置最大的存储区大小即可&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:memory_store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;size: &lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;megabytes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不需要别的额外配置。&lt;/p&gt;
&lt;h2 id="线上环境缓存"&gt;线上环境缓存&lt;/h2&gt;
&lt;p&gt;这里说的线上环境，指的是生产环境或者预生产环境。一般这种环境的配置文件都跟&lt;code&gt;config/environments/production.rb&lt;/code&gt;有点类似，&lt;/p&gt;

&lt;p&gt;关于缓存存储器的代码，都是注释掉的&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# config.cache_store = :mem_cache_store&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是都是采用缺省配置。然而笔者没想到的是原来缺省配置是&lt;code&gt;file_store&lt;/code&gt;。这是我在生产环境下内省得到的结果&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_store&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:file_store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"/www/project/huiliu-web/tmp/cache/"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说缓存结果会持久化到目录&lt;code&gt;/www/huiliu/huiliu-web/tmp/cache/&lt;/code&gt;中，笔者的&lt;a href="https://step-by-step.tech/posts/an-accident-of-inode-number-in-huiliu" rel="nofollow" target="_blank" title=""&gt;上一篇文章&lt;/a&gt;提到的&lt;code&gt;inode number&lt;/code&gt;导致的空间不足，其实就是这个导致的。系统运行时间长，缓存持久化文件过多，且没有定时清理，导致&lt;code&gt;inode number&lt;/code&gt;耗尽。现在回想起来主要原因还是笔者没有用到合适的缓存存储器。&lt;/p&gt;
&lt;h2 id="选择合适的缓存"&gt;选择合适的缓存&lt;/h2&gt;
&lt;p&gt;Rails 提供的缓存存储器主要是这几个，&lt;a href="https://guides.rubyonrails.org/caching_with_rails.html#cache-stores" rel="nofollow" target="_blank" title=""&gt;文档&lt;/a&gt;都有相应说明&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;memory_store。缓存都是存在内存里面，无法在多台机器之间共享。适合小规模单机运行的环境。&lt;/li&gt;
&lt;li&gt;file_store。缓存持久化到文件系统中，只要多台机器能够共同访问同一个文件系统。这些缓存可以多机器之间共享。&lt;/li&gt;
&lt;li&gt;mem_cache_store。使用第三方服务&lt;a href="https://memcached.org/" rel="nofollow" target="_blank" title=""&gt;Memcached&lt;/a&gt;，看着应该是跟 Redis 挺像的一个服务，需要一定的配置。&lt;/li&gt;
&lt;li&gt;redis_cache_store。使用第三方服务&lt;a href="https://redis.io/" rel="nofollow" target="_blank" title=""&gt;Redis&lt;/a&gt;，比较常用的 NoSQL 数据库，也需要一定的配置。&lt;/li&gt;
&lt;li&gt;null_store。完全不使用缓存。&lt;/li&gt;
&lt;li&gt;自定义。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;自定义就是指可以根据自己的需求去实现缓存存储器的适配器。大伙可以根据自己的实际情况进行选择，不管怎么说，笔者强烈不建议使用&lt;code&gt;file_store&lt;/code&gt;，如果真的需要多台机器之间共享缓存，那么使用&lt;code&gt;mem_cache_store&lt;/code&gt;或者&lt;code&gt;redis_cache_store&lt;/code&gt;并购买相关的第三方服务可能是更好的选择。简单起见，在笔者现有的项目里面，使用&lt;code&gt;memory_store&lt;/code&gt;就好。因为笔者现在的机器内存还算是比较有盈余的，所以设置得大一点&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:memory_store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;size: &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;megabytes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# 2048问题也不大&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据&lt;code&gt;memory_store&lt;/code&gt;的说法是，当缓存量超出设定的值的时候，会优先淘汰最久未被访问过的缓存数据。其实就是&lt;a href="https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU" rel="nofollow" target="_blank" title=""&gt;Least recently used (LRU)&lt;/a&gt;算法，原文是这样的&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When the cache exceeds the allotted size, a cleanup will occur and the least recently used entries will be removed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;不一定一开始就上&lt;code&gt;mem_cache_store&lt;/code&gt;或者&lt;code&gt;redis_cache_store&lt;/code&gt;，有时候&lt;strong&gt;简单能用&lt;/strong&gt;不失为一个更好的选择。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;这篇文章简单聊了一下 Rails 服务里面的缓存存储器，这些引擎主要是针对部分的&lt;a href="https://guides.rubyonrails.org/caching_with_rails.html#sql-caching" rel="nofollow" target="_blank" title=""&gt;SQL Caching&lt;/a&gt;场景以及&lt;a href="https://guides.rubyonrails.org/caching_with_rails.html#page-caching" rel="nofollow" target="_blank" title=""&gt;Page Caching&lt;/a&gt;场景的。这些缓存存储器各有千秋，根据自己的业务场景选择合适自己的就好。&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Sat, 17 Dec 2022 07:38:45 +0800</pubDate>
      <link>https://ruby-china.org/topics/42796</link>
      <guid>https://ruby-china.org/topics/42796</guid>
    </item>
    <item>
      <title>记一次 inode 数量耗尽导致的生产事故</title>
      <description>&lt;p&gt;今天遇到一个挺有趣的场景，公司的线上服务器突然无法访问，当我尝试重新部署项目解决问题的时候却提示我说空间不足。这就很耐人寻味了，磁盘空间显示还剩几十个 G，一查发现原来是 inode 的问题，这篇文章简单记录一下。原文连接： &lt;a href="https://step-by-step.tech/posts/an-accident-of-inode-number-in-huiliu" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/an-accident-of-inode-number-in-huiliu&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="Linux系统的inode指标"&gt;Linux 系统的 inode 指标&lt;/h2&gt;
&lt;p&gt;Linux 系统里面有一个 inode 的概念，大概是这样一个意思&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An inode is a data structure that keeps track of all the files and directories within a Linux or UNIX-based filesystem. So, every file and directory in a filesystem is allocated an inode, which is identified by an integer known as “inode number”. These unique identifiers store metadata about each file and directory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;简单来说就是，在 Linux 或者类 Unix 的文件系统里面，所有的文件/目录都会被分配一个&lt;code&gt;inode number&lt;/code&gt;，这是一个唯一的编号，且数量是有限的。我们可以通过&lt;code&gt;ls -i&lt;/code&gt;来查看对应文件的&lt;code&gt;inode number&lt;/code&gt;。比如这样&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; apiclient_key.pem
6556243 apiclient_key.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表明&lt;code&gt;apiclient_key.pem&lt;/code&gt;这个文件的&lt;code&gt;inode number&lt;/code&gt;是&lt;code&gt;6556243&lt;/code&gt;。我们可以通过命令&lt;code&gt;df -i&lt;/code&gt;查看当前系统里 inode 总量&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-ih&lt;/span&gt;

Filesystem      Inodes   IUsed   IFree IUse% Mounted on
udev           1975880     418 1975462    1% /dev
tmpfs          1982935     626 1982309    1% /run
/dev/vda1      9830400 6873397 2957003   70% /
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="inode“超标”导致的“空间不足”"&gt;inode“超标”导致的“空间不足”&lt;/h2&gt;
&lt;p&gt;对阿里云服务器来说，一般硬盘的存储设备都是&lt;code&gt;/dev/vdax&lt;/code&gt;，我这台服务器刚好是&lt;code&gt;/dev/vda1&lt;/code&gt;。从上面的结果可知，该文件系统的 inodes 总容量是 9830400，已经使用了 6873397，还剩余 2957003。讲得再生动一点就是，这台服务器一共可以创建 9830400 个文件/目录，现在已经有 6873397 个了，接下来最多也只能创建 2957003 个文件/目录。如果数量超出之后，再尝试创建文件的时候系统会报错&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;No space left on device or running out of Inodes.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次遇到这个错的时候，笔者也是吓懵了，毕竟这辈子从来没有见到过，而且当时线上的服务完全停滞，需要紧急修复。明明磁盘还有几十 G 的盈余，为何会&lt;strong&gt;No space left on device&lt;/strong&gt;。当时我的服务器大概都成这个鬼样了吧（这还是删除大量文件之后）&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-ih&lt;/span&gt;
Filesystem      Inodes   IUsed   IFree IUse% Mounted on
udev             1.9M   418  1.9M    1% /dev
tmpfs            1.9M   626  1.9M    1% /run
/dev/vda1        9.4M  9.3M  108K   99% /
....

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;系统运行有 2 年时间了，只是盯着磁盘空间看，没有留意到 inode 数量也会造成“空间不足”，也算是一点小经验了。幸好当时笔者距离家不远，急忙赶回家修复不至于耽误太长的时间。网上找到这个脚本，可以查找当前目录下文件数量最多的子目录，这是最后在 Rails 项目目录下的&lt;code&gt;tmp/&lt;/code&gt;目录里面运行的结果&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;find shared &lt;span class="nt"&gt;-xdev&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; 2 | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-nr&lt;/span&gt;

9045071 cache
      8 videos
      2 pids
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见&lt;code&gt;cache&lt;/code&gt;目录里面有太多的文件。估计是我使用模板系统的时候开启了缓存导致的。从文件落地的情况来看应该是使用了类似 Rails 的&lt;a href="https://guides.rubyonrails.org/caching_with_rails.html#fragment-caching" rel="nofollow" target="_blank" title=""&gt;Fragment Caching&lt;/a&gt;。模板以持久化的方式缓存了，且没有定时清理，运行时间太长，导致了&lt;code&gt;tmp/cache&lt;/code&gt;目录下的缓存模板越来越多。最终把操作系统的&lt;code&gt;inode number&lt;/code&gt;给撑爆了。&lt;/p&gt;

&lt;p&gt;解决方案其实挺简单的。可以手动删除一下&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; tmp/cache/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;官方的做法则是通过脚本&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Rails.cache.clear
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作用是一样的，只是清理了&lt;code&gt;tmp/cache&lt;/code&gt;目录下的所有缓存文件。最大的问题是，缓存文件多的时候，这个过程会比较慢，占用系统资源导致服务器卡顿。建议定期清理，不用一次过清理干净，也可以只清理那些失效很长时间的缓存模板文件。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;第一次遇到这种场景的时候还是挺吓人的，似乎每 1 ～ 2 个月都会遇到点刺激的事故。这一两年下来算是习惯了，可能这也是后端/运维工作的常态吧。&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Sat, 10 Dec 2022 09:26:11 +0800</pubDate>
      <link>https://ruby-china.org/topics/42785</link>
      <guid>https://ruby-china.org/topics/42785</guid>
    </item>
    <item>
      <title>回流技术团队现状</title>
      <description>&lt;p&gt;最近公司可算是赐予我一个 CTO 的头衔了，老实说这个名头，&lt;strong&gt;我是既得意又惶恐&lt;/strong&gt;。得意之处在于，CXO 这种头衔，总会自带某种光环。而惶恐之处就在于，万一被他们发现这 CTO，名过于实，那丢脸的还是自己。要说是更得意还是更惶恐，我感觉是惶恐更甚，毕竟当不好被人赶下去应该也挺丢人的吧？&lt;/p&gt;

&lt;p&gt;今天借着这个“东风”，我来谈谈回流的技术团队。我会从文化，技术栈等各个维度来介绍一下这只小团队。原文链接： &lt;a href="https://step-by-step.tech/posts/huiliu-technology-team" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/huiliu-technology-team&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://step-by-step.oss-cn-beijing.aliyuncs.com/production/wwdkhnis2lxf6n2c6c0ddqfpcx6e" title="" alt="DSCF0609.jpg"&gt;&lt;/p&gt;
&lt;h2 id="对回流而言，他们是最好的"&gt;对回流而言，他们是最好的&lt;/h2&gt;
&lt;p&gt;说这种话是有点不要脸了，貌似在歌颂自己的“丰功伟绩”，但要看你从哪个角度来评估&lt;strong&gt;最好&lt;/strong&gt;这个词了。一家公司处于不同的阶段就会需要不同类型的人才，这个几乎是亘古不变的真理。要说专业水平的话，回流技术团队绝对算不上顶尖。也不是业界所推崇的特别年轻化的团队，团队里面 80，90 后都有（以后说不定也会有 70 后跟 00 后吧）。&lt;strong&gt;有些时候一群最顶尖的人不一定能把事情做成，但一群合适的人可以&lt;/strong&gt;。“合适”也是我们对最好的定义。&lt;/p&gt;

&lt;p&gt;虽说目前团队里面的人就没有从名校毕业的，不过这一年多两年来大家聚在一起确实完成了许多从 0～1 的突破，这对于刚起步的公司来说才是最重要的。当一家公司连掀锅都成问题的时候，就别谈什么先进技术了。Ruby On Rails 能快速开发一个原型，我们就用 Ruby 写后端代码。Taro + React 语法能够快速搭建一个可靠的小程序，我们就用它。用 Dart + Flutter 来开发 App 比写两套原生的 App 成本更低出活更快，那它会是我们的选择。换句话说，这是一个相对务实的团队，对于现阶段的回流就是最好的存在，而不是一个在各方面技术都特别顶尖的团队。&lt;/p&gt;
&lt;h2 id="Ruby On Rails“死路”"&gt;Ruby On Rails“死路”&lt;/h2&gt;
&lt;p&gt;老实说，我现在都还不太确定在中国用 Ruby On Rails 走不走得下去，不过选用这套技术栈在前期是有益的。对于“前途未卜”的创业公司来说，能够不断开发出原型，提前验证各种各样的想法，确实大有裨益，这也是我们前期选择开发小程序而不是一上来就 App 的原因之一。&lt;/p&gt;

&lt;p&gt;不过老实说 Rails 在中国的生存空间真的不是很大，相关的人才是越来越少了。当然也有可能我们没开到足够高的工资，否则应该多少能从其他语言部落那里挖些人过来吧。我招聘了两个 Rails 后台工程师最后还是不欢而散（主要都是开发节奏难以适应）。如果招不到合适的 Rails 工程师，那么回流能不能走好下一个阶段就不好说了，这也是笔者稍微有些担忧的事情。&lt;/p&gt;

&lt;p&gt;我不会忽悠新人 Ruby 有多好，更不会骗自己说其他语言有多差。Ruby 不是特别好，但是它并不差，起码它带我们度过了相当关键的一段时期。当然也有很多优秀的语言跟对应的生态（Java，Elixir，Go，Rust 都很好）。只不过项目刚启动的时候，出于 Rails 的优秀生态跟本人的私心（只懂 Ruby 也只想写 Ruby），才用 Ruby 到了现在。&lt;/p&gt;

&lt;p&gt;有小伙伴经常问我，以后会不会用别的语言把我们系统重写掉，回答都是&lt;strong&gt;永远不会&lt;/strong&gt;。当然笔者并不是那种死都要坚守某种语言阵地的顽固派，之所以不重写也仅仅是成本角度考虑。如果考虑到招聘成本和难度（当然还有性能），把已有的一些功能抽取成微服务的形式，并用其他语言来实现，这种可能性还是很大的。&lt;/p&gt;
&lt;h2 id="React + Taro + TypeScript有趣的组合"&gt;React + Taro + TypeScript 有趣的组合&lt;/h2&gt;
&lt;p&gt;当笔者还是前端工程师的时候，对前端的大环境已经看不太懂了。现在跑去写后端，便更不懂了。我记得在上一家公司的时候，大家就到底是用 Vue 还是 React 都能争吵很长时间。回流的第一个产品是小程序，反正不可能写原生的小程序代码，笔者便让小伙伴自己去试试 Vue 系的&lt;a href="https://zh.uniapp.dcloud.io/" rel="nofollow" target="_blank" title=""&gt;uni-app&lt;/a&gt;，以及 React 系的&lt;a href="https://docs.taro.zone/docs" rel="nofollow" target="_blank" title=""&gt;Taro&lt;/a&gt;，在两者之间做出选择。&lt;/p&gt;

&lt;p&gt;我隐约记得当时小伙伴是用 Vue 写了一个 Demo，后来说还是喜欢 React 干脆重写了，所幸成本并不算太高。顺带地，当我们需要用前后端分离的架构重写我们的后台管理系统的时候，也是用的 React。不过小伙伴考虑到维护成本，打算用 TypeScript 来编写整个项目。笔者是一个比较不喜欢束缚的人，天然对静态语言没有太多好感，不过事实证明，小伙伴的选择是对的。毕竟写代码的不是笔者本人，小伙伴开心就好。&lt;/p&gt;

&lt;p&gt;至于为啥不用 Rails 的&lt;a href="https://stimulus.hotwired.dev/handbook/origin" rel="nofollow" target="_blank" title=""&gt;Hotwire&lt;/a&gt;？其实也是成本角度考虑，创业这些年很痛苦地发现，&lt;strong&gt;一个人能力再强，也不可能独自完成所有的事情&lt;/strong&gt;，更何况笔者这辈子跟“能力强”这三个字应该是沾不上边了。如果用 Hotwire 来写整个后台管理系统（确实尝试过），后期的招聘维护会更难。&lt;em&gt;可能你可以相对容易地招聘到一个前端工程师，招聘一个 Rails 工程师会有点难度，然而要招聘到一个懂 Rails 还愿意折腾前端页面，且还能把交互写利索的工程师，难度应该是地狱级别的吧。这种人凭啥来你回流呢？&lt;/em&gt;人生已经很艰难了，不要再给自己增加难度了。&lt;/p&gt;
&lt;h2 id="编写App就用Dart + Flutter确实没啥好考虑的"&gt;编写 App 就用 Dart + Flutter 确实没啥好考虑的&lt;/h2&gt;
&lt;p&gt;忘了是 2021 年的哪一天，老板突然心血来潮说要研发自己的 App，且不让笔者找外包公司（官大一级压死人）。鉴于笔者人格魅力不足（资金预算也是），无法把我亲同学的男朋友挖过来当 App 主管，只好跟前端小伙伴两人对 App 项目简单起个头。&lt;/p&gt;

&lt;p&gt;虽然说在 App 原生领域，用&lt;a href="https://developer.apple.com/cn/swift/" rel="nofollow" target="_blank" title=""&gt;Swift&lt;/a&gt;语言写 IOS，用&lt;a href="https://www.kotlincn.net/" rel="nofollow" target="_blank" title=""&gt;Kotlin&lt;/a&gt;写 Android 的想法很诱人。但是考虑到研发成本以及“国库”不足的窘境，潜意识告诉我要克制。选择一个靠谱的跨平台方案才是明智之举。&lt;/p&gt;

&lt;p&gt;当时比较流行的跨平台方案无非就是&lt;a href="https://reactnative.dev/" rel="nofollow" target="_blank" title=""&gt;React Native&lt;/a&gt;还有&lt;a href="https://flutter.dev/" rel="nofollow" target="_blank" title=""&gt;Flutter&lt;/a&gt;。请教了好些朋友及前同事，最后敲定用 Flutter 来完成这个项目。事实证明，这个选择并不赖，招人也不太难招。目前的 App 小伙伴们都挺靠谱的，就是要难为他们一进来要接手两个门外汉写的 Dart 代码了，当然由于流程不够规范有时候还要加点班，在此深感抱歉。&lt;/p&gt;
&lt;h2 id="尚未完善的测试"&gt;尚未完善的测试&lt;/h2&gt;
&lt;p&gt;无论对哪个公司来说，测试都是比较重要的一环。对回流来说又何尝不是？特别是随着项目越发庞大，需要测试的功能也越来越多，测试力量略显不足。Bug 经常出现，大家都说要对测试岗位进行扩招。从测试招聘贴发出去 5 分钟就有 60 个人来询问的情况来看，市场上并不缺测试人员。但&lt;strong&gt;招聘测试容易，优化流程难。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我粗粗排查了一下，导致测试力量不足的现象可能包含以下几个原因&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;自动化程度不够，我始终相信有不少流程应该要避免掉人工测试的窘境。&lt;/li&gt;
&lt;li&gt;开发者并没有自测，很多开发者自己写完的功能，甚至没有跑一遍就直接扔给测试，返工的情况就很多了。&lt;/li&gt;
&lt;li&gt;流程不够完善，许多开发者只顾自己手上的功能是否能够完成，完全不给测试留下充足的时间。往往把功能放在上线之前最后一天才给到测试，导致发版当天每个人都加班到身心俱疲。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;以上列出的这些问题，回流都有。基本上都没能得到特别充分的解决，所以说测试的状态是“尚未完善”。自动化方面目前只在后端实施，前端着实很难推广。开发者一般都能自测一下自己的功能，因为不测的话会给别人带来不少麻烦，这点大家做得还算不错。不自测，经常给其他人带来麻烦的开发者已经被请走。流程方面感觉永远都不够完善，就算你把所有该做的事情一条条列出来也不可能用 AK74 去逼着每个人遵守。倒是希望大伙自觉养成大局思维，考虑一下怎么做才能更方便其他人的工作，如此一来效率才能从根上得到提高。&lt;/p&gt;
&lt;h2 id="产品设计-其实没啥好说的"&gt;产品设计 - 其实没啥好说的&lt;/h2&gt;
&lt;p&gt;没啥好说主要是因为设计领域一直是笔者的弱项，着实不太了解。目前回流的产品运作良好，设计风格也不错，着实是没啥需要特别挑剔的。能在项目前期招聘到靠谱的设计师兼产品经理，算是挺幸运的。随着最近有新的 UI 设计师入职，产品经理总算能专心当产品经理了，只要公司不倒闭，产品形态应该还能更进一步，只是压力可能到开发这了。&lt;/p&gt;

&lt;p&gt;对设计师用什么工具笔者其实也不算特别了解。只是听说，产品设计方面大家都比较倾向于用&lt;a href="https://www.sketch.com/" rel="nofollow" target="_blank" title=""&gt;Sketch&lt;/a&gt;而不是&lt;a href="https://www.adobe.com/products/photoshop.html" rel="nofollow" target="_blank" title=""&gt;PhotoShop&lt;/a&gt;。估计唯一让我不满的就是，&lt;em&gt;大伙收入都不低了，且都已经用苹果电脑了，怎么还用盗版？&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="生死未卜的远程机制"&gt;生死未卜的远程机制&lt;/h2&gt;
&lt;p&gt;得益于网络时代的红利，除了那些当老板的之外，程序员是最容易享受到远程红利的一波人。本来远程这个东西是笔者为了自己去跟老板争取的福利，后来发现几乎所有小伙伴都不愿意坐班，加上疫情的加持，远程倒是成了常态了。&lt;/p&gt;

&lt;p&gt;这里也说说我对团队远程的想法&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;技术团队可能会越发趋向年轻化，年轻人的自制力，责任心，对产品理解的深度一般比不上有一定经验的“老人”，一旦把控不好会影响团队效率。这方面其实笔者稍微有些担忧。&lt;/li&gt;
&lt;li&gt;一旦习惯了远程，便很难适应外界的其他工作了。&lt;strong&gt;由俭入奢易，由奢入俭难&lt;/strong&gt;。说句不好听就是，万一哪天云长科技倒了，要重新去打卡的企业上班，估计会很痛苦吧。&lt;/li&gt;
&lt;li&gt;团队一旦壮大，远程沟通问题会越来越明显。沟通问题，工作流程需要长期不断审视优化，才能使远程这艘轮船持续运转下去。&lt;/li&gt;
&lt;li&gt;薪资不好界定。假设你跑到小县城生活手里还拿着一线城市的工资，公司肯定有不少红眼之人，以后每到涨薪时刻杂七杂八的事情会越来越多。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;要是当时跟老板没有说脑子一热，全体开放远程，需要解决的问题应该会少很多。只是“开弓没有回头箭”，见一步走一步了。遇到问题，就解决问题呗。毕竟 IT 工作者最核心的竞争力不应该是产品设计或代码编写，而是&lt;strong&gt;解决问题&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="最后"&gt;最后&lt;/h2&gt;
&lt;p&gt;这篇文章简单对目前回流技术团队的现状做一个总结。现在看来运作得还算良好，偶尔有些小问题出现，基本都得到了解决。然而我感觉后面的问题会越来越多，其中有些问题是老板扔过来的（业务与招聘），有些问题是自找的（全面开放远程）。希望后面的路还足够长，我们有足够的时间把它们慢慢解决掉。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;太久没写博客了，导致自己的个人博客被阿里云释放了都浑然不觉，等这篇文章写完尽快去恢复它，这就是懒惰的代价了吧。&lt;/em&gt;&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Thu, 08 Dec 2022 09:25:51 +0800</pubDate>
      <link>https://ruby-china.org/topics/42781</link>
      <guid>https://ruby-china.org/topics/42781</guid>
    </item>
    <item>
      <title>务实文化的启蒙 -《程序员修炼之道》</title>
      <description>&lt;p&gt;前不久我推荐老板看《黑客与画家》这本书，他看完后觉得一个合格的程序员都应该读一读《黑客与画家》。我并不否认《黑客与画家》在我心目中的地位，毕竟这本书从头到尾我也读了 6 遍以上了。但是今天我想推荐《程序员修炼之道 - 通向务实的最高境界》，这本书在笔者心目中的地位并不亚于《黑客与画家》。原文连接： &lt;a href="https://step-by-step.tech/posts/the-pragmatic-programmer" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/the-pragmatic-programmer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;我甚至想把它作为回流技术团队的培训手册，不过在此之前，我们先来了解一下这本书。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://step-by-step.oss-cn-beijing.aliyuncs.com/production/e6p69vb2fk1dl4cztwru5k15gr3d" title="" alt="Computer_Programmer1920X10180.jpeg"&gt;&lt;/p&gt;
&lt;h2 id="简单对比一下"&gt;简单对比一下&lt;/h2&gt;
&lt;p&gt;《黑客与画家》其实是以杂文的形式，从较为宏观的角度去描述程序员（或者说黑客）这份职业。书呆子在信息时代有什么优势？黑客到底是更接近工程师还是更接近艺术家？在这本书都有所提及（关于《黑客与画家》可以参考&lt;a href="https://step-by-step.tech/posts/three-times-hacker-and-painter" rel="nofollow" target="_blank" title=""&gt;3.times { p '黑客与画家' }&lt;/a&gt;）。&lt;/p&gt;

&lt;p&gt;而《程序员修炼之道》，则会具体到一些相对微观的层面，比如一个程序员在日常工作中要如何保持务实的心态，应该活用哪一类的工具，方法，什么才是真正的敏捷，开发者该如何把本职工作做得更好，拥抱工作中的变化。&lt;strong&gt;个人以为只要是程序员，在职业生涯不同阶段，每次重读定会有不一样的感悟与收获。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;如果是给一个刚毕业正处于迷惘期的年轻人做职业指导，我会毫不犹豫地推荐他阅读《黑客与画家》，看完之后他可能会知道自己是否真的适合当一个程序员。而如果是培训团队中的开发人员，我会建议他们人手一本《程序员修炼之道》，这本书会帮助他们成为更好的开发者。&lt;/p&gt;

&lt;p&gt;作者是有着超过 40 年开发经验的“老程序员”，假设 5 岁就开始写代码，现在都已经 45 岁了，彻底打破了程序员是青春饭这个魔咒。此书便犹如业内声望颇高的长者的谆谆教导，你会因在工作中贯彻书中的提示而受益匪浅。&lt;/p&gt;
&lt;h2 id="务实文化"&gt;务实文化&lt;/h2&gt;
&lt;p&gt;简单来说，这是一本“务实”之书。里面有大概 53 个主题，以及 100 条提示，基本都是围绕“务实”二字展开。实用主义无论在哪个行业都有举足轻重的作用，邓小平同志不也说：“不管黑猫白猫，会捉老鼠就是好猫”。那这里就围绕“务实”来做一些简单的概括。&lt;/p&gt;
&lt;h2 id="规模的务实"&gt;规模的务实&lt;/h2&gt;
&lt;p&gt;等到十月国庆一过，回流技术团队成立就该满两年了，感觉时间过得是真的快啊，不知不觉就两年了。这两年里面，需求像狂风暴雨般袭来，技术团队其实面临了不少的压力。公司许多内部人员都会觉得我们的开发力量不够强大，&lt;strong&gt;毕竟他们什么都想开发&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;然而我却下意识地“维持小而稳定的团队”（提示 84），在外界的干扰下，我几乎每个月都怀疑这么克制是不是对的。直到最近重读《程序员修炼之道》，才想起来原来作者也有类似的想法。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;团队应该保持稳定，小巧，团队的每个人都应相互信任，互相依赖。&lt;/p&gt;

&lt;p&gt;50 个人就不算是团队，那是部落。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;在某种程度上还是给了我一剂“强心剂”。得益于精益的团队，务实才成为可能。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;自己没有这么强的能力，就别去管这么多的人。虽说管的人多了，以后跳槽简历会光鲜一点。&lt;/li&gt;
&lt;li&gt;保持团队小巧，会更容易把目光聚焦在真正重要的事情上，毕竟开发力量就那么点，只能选择最重要的去研发。&lt;/li&gt;
&lt;li&gt;一个功能，一个人去做需要两周时间，两个人则可能需要三周，团队大了沟通成本是个问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="流程的务实"&gt;流程的务实&lt;/h2&gt;&lt;h3 id="a. 自动化测试"&gt;a. 自动化测试&lt;/h3&gt;
&lt;p&gt;因为我们人手有限（到今天为止也只有 7 个人），为了能够更好地保证产品质量，我们都是“尽早测试，经常测试，自动测试”（提示 90）。虽然目前自动化测试只在服务端实行，不过得益于它们的存在，产品的质量还算可以接受。尽量做到“每个 Bug 只找一次”（提示 94），测试人员花在回归上的时间少了，就能有更多精力去测其他关键的功能点。&lt;/p&gt;
&lt;h3 id="b. 小步快走"&gt;b. 小步快走&lt;/h3&gt;
&lt;p&gt;这些年了解过一些失败的项目案例，发现许多项目失败的原因基本就这些因素 - 项目时间过长，办公室政治，需求变动频繁等等。其中项目时间过长，上线时间遥遥无期，严重打击了开发人员的士气，而导致项目失败的案例就比比皆是。&lt;em&gt;因为开发人员能力不足导致项目失败的情况其实很少&lt;/em&gt;。&lt;/p&gt;

&lt;p&gt;为了避免团队陷入这样的窘境，我们始终“在用户需要时交付”（提示 88），几乎每周都会发一个版本。差不多两年过去，团队基本是这个节奏，渐渐养成了快速响应的习惯，小步快走。&lt;/p&gt;

&lt;p&gt;个人以为这也是一种期望管理策略，当你发版间隔很长的时候，势必会把那些在等待新版的人（可能是公司的高管，又或者是重度用户）期望值拉高，时间间隔越长往往期望值越高。结果就是&lt;strong&gt;期望越大失望越大&lt;/strong&gt;。要能“取悦用户，而不要只是交付代码”（提示 96）。&lt;/p&gt;

&lt;p&gt;我们每周都会有新版本，可能会有新功能，但是也可能只是修复几个 bug。甚至也有可能我们只修复了一个 bug，就发版本了，但是暗地里在做一些重构方面的工作。重构代码是高层不愿意给时间你去做的工作，但却很重要。实际上以&lt;strong&gt;明修栈道，暗渡陈仓&lt;/strong&gt;的方式去做重构，大有好处。修一个 bug 也是修，其他时间就偷偷做重构，毕竟不影响发版节奏，高层一般也不会说什么，开发者也不用面临太大的压力。&lt;/p&gt;
&lt;h2 id="需求的务实"&gt;需求的务实&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;我们老板想法太多，经常有新需求。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;好巧，我们老板也是这样呢。今天说想要多开个直播间，明天可能说要上新的品类。需求多了自然就觉得开发力量不足，前面也提到了&lt;strong&gt;他们什么功能都想要&lt;/strong&gt;。这是两难局面，大力扩招我觉得目前来看有点治标不治本，或许我们可以采用别的策略。&lt;/p&gt;

&lt;p&gt;本书中提到一种叫作“曳光弹”的策略，“使用曳光弹找到目标”（提示 20）也是我们经常采用的方式。你可以把它简单理解为&lt;strong&gt;用代码构筑的原型&lt;/strong&gt;。与一般的原型不同“曳光弹”是实际可以运行的程序，就是一个实验性的项目，可能一开始会很简陋，但是它的优势在于开发快，可以快速出一个试用品，用来验证某种需求的可行性。最后“曳光弹”中的代码可能会被抛弃掉，也可能渐渐转化成功能更加完善的产品。&lt;/p&gt;

&lt;p&gt;我们经常用这种方法来验证需求的可行性，特别是上头来了一个大需求，而这个需求可能会消耗掉一个月以上的研发时间。书中也有提到“不要冲出前灯范围”(提示 42)，当一个功能研发时间太长的时候，往往不确定性就高，失败的可能性也很大。所以要及早测试需求的可行性，不断接收反馈，毕竟“无人确切知道自己想要什么”（提示 75）。提前有个最小可用版本就能够在一定程度上降低这方面的风险，跟高层（用户）一同探讨需求可行性，渐渐地可能会得到一个更好的解决方案。“程序员帮助人们理解他们想要什么”（提示 76）。如果最终能够确认一个需求是伪需求，起码对公司来说是个好事情。&lt;/p&gt;
&lt;h2 id="务实的编码"&gt;务实的编码&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;编码谁不会呢？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这又是个很有意思的话题了，编码对于程序员来说是基本功，基本上谁都会。不过有些地方只要稍加注意，可能编码这个事情便能上升到艺术的范畴。&lt;/p&gt;

&lt;p&gt;首先我还是建议“尽早崩溃”（提示 38），这是最近跟新来的 Ruby 小伙伴讨论的事情，起因在于他在代码里面加了一段&lt;code&gt;rescue StandardError =&amp;gt; e&lt;/code&gt;来捕获异常，这能够避免系统崩溃。不过在一些场景下，这种做法往往会让问题难以定位，你会&lt;strong&gt;误以为代码运行得很好&lt;/strong&gt;，等到发现问题的时候可能已经为时已晚，直到用户投诉才能发现问题所在，与其这样把问题掩埋在深处，倒不如一开始就让系统崩溃，开发者能尽快着手修复问题。&lt;/p&gt;

&lt;p&gt;在编写 Ruby 代码的过程中，我们还是坚持为编码写测试，“测试是代码的第一个用户”（提示 67），可以用测试来驱动代码的实现，帮助思考的同时也有利于回归。得益于测试用例，我们能够做到“尽早重构，经常重构”（提示 65），毕竟开发过程中总会发现之前一些之前设计/编码不当的地方，有测试在就可以大胆重构。这可以让系统保持正交性，“消除不相关事物之间的影响”（提示 17），也能适当给代码解耦，“解耦代码让改变更容易”（提示 44）。&lt;/p&gt;

&lt;p&gt;把上面这些提示贯彻到日常的编码工作中，项目堆积技术债务的速度也会缓慢一些（技术债务无法避免），项目也更容易被接手。“不要放任破窗”（提示 5），其他同事也不会忍心去弄坏已有的代码。&lt;/p&gt;
&lt;h2 id="尾声"&gt;尾声&lt;/h2&gt;
&lt;p&gt;要说《程序员修炼之道》这本书好在哪，那我觉得还是“常读常新”。笔者第一次阅读此书的时候，它还是电子工业出版社出版的第一版，当时还是大学生的我读完之后其实很多东西都无法感同深受。直到参加工作之后，慢慢把里面的原则应用进去才发现获益匪浅。最近又拿起来读一遍（之前读了几遍已经记不清了），才惊讶地发现回流项目之所以目前还算健康，还真少不了它潜移默化的影响呢。&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Tue, 27 Sep 2022 09:24:56 +0800</pubDate>
      <link>https://ruby-china.org/topics/42671</link>
      <guid>https://ruby-china.org/topics/42671</guid>
    </item>
    <item>
      <title>PostgreSQL 数据库存放路径初窥探</title>
      <description>&lt;p&gt;用 PostgreSQL 数据库已经有一段时间了，始终对他的目录结构不是特别了解。这篇文章简单总结一下这段时间的发现。就当作是学习笔记了。原文链接： &lt;a href="https://step-by-step.tech/posts/path-of-databases-in-pg" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/path-of-databases-in-pg&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="新的目录，从0开始探索"&gt;新的目录，从 0 开始探索&lt;/h2&gt;
&lt;p&gt;PostgreSQL 服务有很多种启动方式，为了获得一个洁净的环境，我们需要重新启动一个 PG 服务。最简单的方式是使用&lt;code&gt;pg_ctl&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;mkdir &lt;/span&gt;pg_study

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; pg_ctl initdb &lt;span class="nt"&gt;-D&lt;/span&gt; pg_study // 初始化数据库服务目录

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; vim pg_study/postgresql.conf // 把端口号改成5678，并保存（假设5432端口已经被占用不跟已有的服务冲突）

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; pg_ctl &lt;span class="nt"&gt;-D&lt;/span&gt; pg_study  start // 启动服务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里简单采用 socket 无密码的方式来登陆，需要用 5678 端口连接这个新的数据库服务&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; psql &lt;span class="nt"&gt;-U&lt;/span&gt; lan &lt;span class="nt"&gt;-d&lt;/span&gt; postgres &lt;span class="nt"&gt;-p&lt;/span&gt; 5678

&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;#&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="数据库放在哪了？"&gt;数据库放在哪了？&lt;/h2&gt;
&lt;p&gt;当你窥探数据库文件夹的时候&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-hS&lt;/span&gt; pg_study

postgresql.conf      pg_wal               pg_notify
pg_hba.conf          pg_subtrans          pg_replslot
global               pg_xact              pg_serial
pg_ident.conf        postmaster.pid       pg_snapshots
base                 postgresql.auto.conf pg_stat
pg_logical           postmaster.opts      pg_tblspc
pg_stat_tmp          pg_commit_ts         pg_twophase
pg_multixact         pg_dynshmem          PG_VERSION
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会看到一堆不太看得懂的东西，除了一堆文件目录之外就是类似于&lt;code&gt;postgresql.conf&lt;/code&gt;， &lt;code&gt;pg_hba.conf&lt;/code&gt;这种配置文件，还有进程文件&lt;code&gt;postmaster.pid&lt;/code&gt;。他们各自代表什么暂且不管（笔者也不是很清楚），我现在想知道数据库放在哪。答案就是&lt;code&gt;pg_study/base&lt;/code&gt;目录下。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lhS&lt;/span&gt; pg_study/base
total 0
drwx------  297 lan  staff   9.3K Sep 10 21:08 14023
drwx------  296 lan  staff   9.3K Sep 10 21:01 1
drwx------  296 lan  staff   9.3K Sep 10 21:01 14022
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有一堆不明觉厉的以数字为名的目录，数字分别代表什么一点头绪都没有。这个时候可以利用数据库的&lt;strong&gt;内省&lt;/strong&gt;机制。&lt;strong&gt;从数据库里面查询数据库本身的资料。&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# select oid, datname from pg_database ;&lt;/span&gt;

&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# select oid, datname from pg_database ;&lt;/span&gt;
  oid  |  datname
&lt;span class="nt"&gt;-------&lt;/span&gt;+-----------
 14023 | postgres
     1 | template1
 14022 | template0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见总共只有 3 个数据库，每个数据库都有对应的&lt;code&gt;oid&lt;/code&gt;这个&lt;code&gt;oid&lt;/code&gt;其实就是&lt;code&gt;pg_study/base&lt;/code&gt;目录下对应的目录名。我们可以多创建一个新的数据库看看是不是会多一个对应的文件夹&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# CREATE DATABASE study;&lt;/span&gt;
CREATE DATABASE

&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# SELECT oid, datname FROM pg_database ;&lt;/span&gt;
  oid  |  datname
&lt;span class="nt"&gt;-------&lt;/span&gt;+-----------
 14023 | postgres
 16384 | study
     1 | template1
 14022 | template0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见多了一个&lt;code&gt;oid&lt;/code&gt;为&lt;code&gt;16384&lt;/code&gt;的数据库，在去看看 base 目录下是不是这么回事&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lhS&lt;/span&gt; pg_study/base
total 0
drwx------  297 lan  staff   9.3K Sep 10 21:08 14023
drwx------  296 lan  staff   9.3K Sep 10 21:01 1
drwx------  296 lan  staff   9.3K Sep 10 21:01 14022
drwx------  296 lan  staff   9.3K Sep 10 21:20 16384
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不出所料，果然多了一个&lt;code&gt;16384&lt;/code&gt;的文件夹。&lt;/p&gt;
&lt;h2 id="不想放在base里面？"&gt;不想放在 base 里面？&lt;/h2&gt;
&lt;p&gt;虽然这种需求不怎么常见，但有没有可能把我们的数据库放在&lt;code&gt;base&lt;/code&gt;之外的目录里呢？这种时候需要利用 PostgreSQL 表空间 (Tablespace) 这个概念了。假设我们想要把数据库放在目录&lt;code&gt;/var/tmp/hello&lt;/code&gt;里面，则针对这个目录创建一个表空间&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# CREATE TABLESPACE newspace LOCATION '/var/tmp/hello';&lt;/span&gt;
CREATE TABLESPACE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在创建数据库的时候指定这个表空间即可&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# CREATE DATABASE database_in_newspace TABLESPACE newspace ;&lt;/span&gt;
CREATE DATABASE

&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# SELECT oid, datname FROM pg_database WHERE datname ~ 'database_in_newspace';&lt;/span&gt;
 16453 | database_in_newspace
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新数据库的&lt;code&gt;oid&lt;/code&gt;是&lt;code&gt;16453&lt;/code&gt;，再去查看一下&lt;code&gt;/var/tmp/hello&lt;/code&gt;目录下是否有名为&lt;code&gt;16453&lt;/code&gt;这个文件夹&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; /var/tmp/hello
PG_14_202107181

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; /var/tmp/hello/PG_14_202107181
16453
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;倒是不出我们所料，只不过外面还套了一层&lt;code&gt;PG_14_202107181&lt;/code&gt;。这串主要是根据&lt;code&gt;PG_{{VERSION}}_{{CATALOG VERSION NUMBER}}&lt;/code&gt;。最后一串玩意可以这样去获取&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; pg_controldata pg_study | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'Catalog'&lt;/span&gt;
Catalog version number:               202107181
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="数据表怎么存放呢？"&gt;数据表怎么存放呢？&lt;/h2&gt;
&lt;p&gt;一般来说，只要不指定表空间，数据表一般都会默认存放在对应的数据库目录里面的。我们重新创建一个数据库，并在该数据库里面创建一个数据表试试看？&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# CREATE DATABASE where_is_table;&lt;/span&gt;
CREATE DATABASE

&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# \c where_is_table&lt;/span&gt;
You are now connected to database &lt;span class="s2"&gt;"where_is_table"&lt;/span&gt; as user &lt;span class="s2"&gt;"lan"&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="nv"&gt;where_is_table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# CREATE TABLE mytable(id integer);&lt;/span&gt;
CREATE TABLE

&lt;span class="nv"&gt;where_is_table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# SELECT oid, datname FROM pg_database WHERE datname = 'where_is_table';&lt;/span&gt;

 16455 | where_is_table
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数据库的&lt;code&gt;oid&lt;/code&gt;是&lt;code&gt;16455&lt;/code&gt;，而在&lt;code&gt;pg_study/base/16455&lt;/code&gt;目录下其实有很多文件&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;ls &lt;/span&gt;pg_study/base/16455 | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
     296
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哪个才是数据表呢？我们可以通过查询&lt;code&gt;pg_catalog.pg_class&lt;/code&gt;数据表来找到对应记录。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;where_is_table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# select relname, relfilenode from pg_catalog.pg_class where relname = 'mytable';&lt;/span&gt;
 relname | relfilenode
&lt;span class="nt"&gt;---------&lt;/span&gt;+-------------
 mytable |       16456
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到&lt;code&gt;relfilenode&lt;/code&gt;的值为&lt;code&gt;16456&lt;/code&gt;，这便是对应数据表的文件名，再去&lt;code&gt;pg_study/base/16455&lt;/code&gt;搜索下看看&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; pg_study/base/16455 | &lt;span class="nb"&gt;grep &lt;/span&gt;16456
&lt;span class="nt"&gt;-rw-------&lt;/span&gt;    1 lan  staff       0 Sep 11 18:23 16456
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该文件是存在的，并且是一个普通文件，并不是目录。只是现在表里面没有任何数据，所以表文件的大小是 0。往里面插入一点数据看看&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;where_is_table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# INSERT INTO  mytable values (1);&lt;/span&gt;
INSERT 0 1
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; pg_study/base/16455 | &lt;span class="nb"&gt;grep &lt;/span&gt;16456
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt;    1 lan  staff    8192 Sep 12 18:03 16456
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有数据了，文件大了 8KB 左右（这跟 PG 的存储规则有关）。数据表存放路径大概可以用以下公式来概括&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 没有表空间的情况&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;root_path_of_database_service&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/base/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;oid_of_database&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;relfilenode_of_table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# 有表空间的情况&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;location_of_tablespace&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/PG_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;catalog_version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;oid_of_database&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;relfilenode_of_table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;小贴士：为何在&lt;code&gt;pg_catalog.pg_class&lt;/code&gt;里面查找表信息是通过&lt;code&gt;relname&lt;/code&gt;以及&lt;code&gt;relfilenode&lt;/code&gt;，而不是普遍认知的&lt;code&gt;tablename&lt;/code&gt;或者&lt;code&gt;tablefilenode&lt;/code&gt;？因为跟很多常用的数据库不同，在 PostgreSQL 里面把数据表都称作&lt;strong&gt;Relation&lt;/strong&gt;。不知道您是否注意到，当我们运行命令&lt;code&gt;\d&lt;/code&gt;来查看数据库内表信息的时候它会显示&lt;strong&gt;List of relations&lt;/strong&gt;而不是&lt;strong&gt;List of tables&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;这篇文章是一篇简单的学习笔记，简单窥探 PostgreSQL 里面数据库，数据表是如何存放的，同时也展示了 PostgreSQL 里面的一些&lt;strong&gt;内省&lt;/strong&gt;查询。&lt;/p&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Tue, 13 Sep 2022 09:09:22 +0800</pubDate>
      <link>https://ruby-china.org/topics/42647</link>
      <guid>https://ruby-china.org/topics/42647</guid>
    </item>
    <item>
      <title>付费推广技术支持心得分享</title>
      <description>&lt;p&gt;做回流已经快两年了，遇到了不少问题，也解决了不少问题。然而总会有些问题不管难易你都不愿意去触碰的，付费推广便是其中之一。原文链接： &lt;a href="https://step-by-step.tech/posts/technical-support-advertising-promotion" rel="nofollow" target="_blank"&gt;https://step-by-step.tech/posts/technical-support-advertising-promotion&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="花钱"&gt;花钱&lt;/h2&gt;
&lt;p&gt;从个人角度来看，对推广是断无好感。主要包含两方面原因&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;领域距离自己的太遥远，熟悉起来太麻烦。&lt;/li&gt;
&lt;li&gt;前景不确定且太烧钱。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;其实第二点占的比重会比较大，&lt;strong&gt;哪天你发现推广一天的钱能够抵得上你一个月工资的时候，我相信你也会跟我有一样的感觉。&lt;/strong&gt;不过个人感受是一方面，从公司角度来看，推广这个事还是很有必要的，特别是当产品已经基本成型需要快速建立公信力的时候，还真的就是要花点钱的时候。&lt;/p&gt;

&lt;p&gt;目前回流需要开发介入的推广主要有&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;抖音/头条广告推广（巨量平台）。&lt;/li&gt;
&lt;li&gt;百度推广（百度营销平台）。&lt;/li&gt;
&lt;li&gt;Vivo 推广（Vivo 营销平台）。&lt;/li&gt;
&lt;li&gt;Oppo 推广（Oppo 营销平台）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="推广简介"&gt;推广简介&lt;/h2&gt;
&lt;p&gt;总的来说推广其实还是一门策略活，技术在其中起到的作用还是相对有点少的，只是多少还是有点需要技术支持的地方。简单来说，推广就是付钱给某一个服务商（比如百度/抖音），然后让这个服务商可以在他的公开平台上面展示（广告位的形式）我们提供的素材，当用户被素材所吸引就会点击广告并下载我们的应用程序。那么现在问题来了，如果用户只下载了 App 那么那么对于商城来说一点意义都没有，假设我们推广的目的是拉取真实的付费用户，那么我们需要用户在看到素材之后完成以下行为&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;点击素材进入下载页面。&lt;/li&gt;
&lt;li&gt;从下载页下载 APP。&lt;/li&gt;
&lt;li&gt;在 App 注册用户。&lt;/li&gt;
&lt;li&gt;购买商城产品并付费。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这四个步骤将形成一条链条。只有一个用户完成这个完整的链条，才能算是一个我们期望的目标用户。&lt;/p&gt;

&lt;p&gt;当然这会存在一个问题，现实生活中用户的行为千奇百怪，很多用户可能就只是点进来然后就离开了，也有些用户可能下载了 App 但是没有下文，也有些用户可能注册了但是没有购买东西。这些都要统计下来。比方说我们观察到，通过广告渠道下载了我们 App，并且有注册行为的用户有 10000 个，其中真正购买并且付费只有 10 个。那么注册到付费的转化率就很低，只有 1%。这种时候推广的同事可能会去调整素材以及广告投放策略，或者跟技术部商量看怎么优化购买流程以提高转化率。&lt;/p&gt;
&lt;h2 id="技术介入"&gt;技术介入&lt;/h2&gt;
&lt;p&gt;从上面的推广概述可以得知，技术需要介入的地方其实就只有给应用设置“埋点”。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;用户下载并安装 App 成功的时候设置一个埋点，等用户完成这个步骤把上送数据。&lt;/li&gt;
&lt;li&gt;用户注册的时候设置一个埋点，等用户完成注册之后上送数据。&lt;/li&gt;
&lt;li&gt;用户下单并支付后设置一个埋点，等用户完成购买之后上送数据。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里有个问题需要特别注意一下，以上三个事件是任何付费用户都必然要触发的。然而我们广告的推广渠道很多，有抖音推广，百度推广，Vivo，Oppo 等等。我们应该怎么区分用户来自哪个广告渠道呢？这就需要一点开发能力了，其实任何形式的广告都会以素材的方式展现，用户看到广告觉得有趣就会点击广告进来，并下载应用，更有甚者会走完整个支付流程。&lt;/p&gt;

&lt;p&gt;关键在于&lt;strong&gt;点击素材进入下载页面&lt;/strong&gt;的时候，为了方便后面的埋点，一般推广服务商都会有“监控链接”或“点击监控”的概念，该链接需要广告主（也就是我们）进行开发。当用户点击素材广告的时候，会顺便触发这个监控链接。一个监控链接大概是长这样&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 苹果&lt;/span&gt;

https://xxxxx.huiliu.net/api/v1/oceanengine_assistants/click?aid&lt;span class="o"&gt;=&lt;/span&gt;__AID__&amp;amp;cid&lt;span class="o"&gt;=&lt;/span&gt;__CID__&amp;amp;idfa&lt;span class="o"&gt;=&lt;/span&gt;__IDFA__&amp;amp;mac&lt;span class="o"&gt;=&lt;/span&gt;__MAC__&amp;amp;os&lt;span class="o"&gt;=&lt;/span&gt;__OS__&amp;amp;TIMESTAMP&lt;span class="o"&gt;=&lt;/span&gt;__TS__&amp;amp;callback&lt;span class="o"&gt;=&lt;/span&gt;__CALLBACK_PARAM__&amp;amp;ip&lt;span class="o"&gt;=&lt;/span&gt;__IP__&amp;amp;ua&lt;span class="o"&gt;=&lt;/span&gt;__UA__
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 安卓&lt;/span&gt;

https://xxxxx.huiliu.net/api/v1/oceanengine_assistants/click?aid&lt;span class="o"&gt;=&lt;/span&gt;__AID__&amp;amp;cid&lt;span class="o"&gt;=&lt;/span&gt;__CID__&amp;amp;oaid&lt;span class="o"&gt;=&lt;/span&gt;__OAID__&amp;amp;imei&lt;span class="o"&gt;=&lt;/span&gt;__IMEI__&amp;amp;mac&lt;span class="o"&gt;=&lt;/span&gt;__MAC__&amp;amp;os&lt;span class="o"&gt;=&lt;/span&gt;__OS__&amp;amp;TIMESTAMP&lt;span class="o"&gt;=&lt;/span&gt;__TS__&amp;amp;callback&lt;span class="o"&gt;=&lt;/span&gt;__CALLBACK_PARAM__&amp;amp;ip&lt;span class="o"&gt;=&lt;/span&gt;__IP__&amp;amp;ua&lt;span class="o"&gt;=&lt;/span&gt;__UA__
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;用户一旦点击广告，广告服务商便会访问该链接，并替换掉&lt;code&gt;__XXX__&lt;/code&gt;这些占位内容，把点击用户的一些关键信息传导过来。&lt;/strong&gt;对于苹果用户来说比较重要的就是&lt;code&gt;idfa&lt;/code&gt;，而对于安卓用户来说比较重要的就是&lt;code&gt;imei&lt;/code&gt;或者&lt;code&gt;oaid&lt;/code&gt;（最近刚发现一个&lt;code&gt;androidid&lt;/code&gt;）。可以把这些值想像成是&lt;strong&gt;设备唯一标识&lt;/strong&gt;，我们作为广告主，需要把这些信息存储下来以供下次使用。&lt;/p&gt;

&lt;p&gt;接下来当某个用户，下载并安装了 App 的时候，我们理应把“激活”这个事件上送给到服务商去。这个时候我们需要查看之前的点击记录，看看某个设备（通过&lt;code&gt;idfa/imei/oaid&lt;/code&gt;来识别），之前是否有过广告点击行为，如果有，则往该服务商上送激活事件。如果没有对应记录，表明这个用户并没有点击过服务商的广告，不能算是该推广渠道拉来的用户，则不上送数据到该服务商去。&lt;/p&gt;

&lt;p&gt;总的来说技术要做的事情其实很简单&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;记录点击广告那位用户的设备信息。&lt;/li&gt;
&lt;li&gt;查看记录的用户信息，如果记录存在表明用户点击过广告，可以向该广告商上送用户相关的行为（“激活”，“注册”， “下单”等）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="技术的挑战"&gt;技术的挑战&lt;/h2&gt;
&lt;p&gt;上一个章节大概讲述了广告推广的过程中需要技术介入的方面，虽说看起来很简单，不过也面临了不少挑战，这里简单总结一下。手机设备都是有一定的局限性的，并且不同的厂家基于隐私的考虑，获取设备唯一标识的限制也会不同。&lt;/p&gt;

&lt;p&gt;就拿 iPhone 手机来做个例子，iPhone 手机在广告推广中常用的唯一性标识就是 IDFA。然而苹果公司基于用户隐私的考虑，不会让我们随意获取这个值，要获取这个值需要经过用户的同意。具体可以&lt;a href="https://wooga.helpshift.com/hc/zh-hant/27-june-s-journey/faq/3081-ios-14-5---idfa-pop-up/" rel="nofollow" target="_blank" title=""&gt;这里&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;想象一下，当我们通过巨量引擎来做推广的时候，广告一般都会被展示在抖音 App 或者头条 App，当用户点击该广告并下载我们的 App 之后，广告追踪的过程就可能有以下这几种情况&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;抖音/头条 App 开启了广告追踪权限，广告主 App 也开启了广告追踪。那么用户从广告点击到后面的一系列行为都能跟踪得到。&lt;/li&gt;
&lt;li&gt;抖音/头条 App 开启了广告追踪权限，广告主 App 没有开启广告追踪。那么用户点击广告的时候会把它的设备标识记录下来，然而后面的一系列步骤由于找不到一对一的匹配记录，则无法追踪得到。&lt;/li&gt;
&lt;li&gt;抖音/头条关闭了广告追踪权限，广告主 App 开启广告追踪权限。跟第二种情况类似，只不过这次是点击广告的时候，没有记录到唯一标识，哪怕后面用户使用我们自家 App 的时候开启广告追踪，依旧是没办法把前后的两个用户通过设备标识联系起来。&lt;/li&gt;
&lt;li&gt;抖音/头条关闭了广告追踪，广告主 App 也关闭了广告追踪权限。如果用户少的情况下，在某种意义上确实能够把两者匹配起来（可能前后的设备标识都是空字符串，或者前后设备标识都是'00000-00000'这种形式），但设备一多就没用了。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在某种意义上来说，只有第一种场景（相对理想）广告追踪才有意义。然而其实大部分用户都不太愿意开启广告追踪权限的，这会导致广告追踪的效果不如预期。头条没有提供解决方案，不过百度倒是给了我们一个比较不错的建议。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;匹配转化数据过程中因 imei、oaid、idfa 不能 100% 获取，建议在设备匹配的基础上增加 ip+ua 方式进行激活数据拼接。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;简单来说就是先通过设备标识来匹配，设备标识存在且能找得到记录的，用户点击广告以及后面的行为就都能追踪得到。当找不到对应记录的时候，可以通过用户使用的 IP 地址以及 UA 来找。&lt;/p&gt;

&lt;p&gt;确实有一些道理，因为 UA（User-Agent）跟 IP 地址这套组合相对固定，虽说没有设备标识的唯一性靠谱，不过作为追踪的辅助还是可以的。UA 这个信息相对固定，一般都不会变。IP 却会有点问题，比如用户在点击广告，准备下载 App 的时候觉得网络不好，切换了网络，再去下载 APP，这样 IP 地址可能会变动，如此 UA+IP 的组合还是无法追踪得到，不过把这当作一个补充方案还是不错的。以下是我写的一部分代码，是给百度写的，原理其实大同小异&lt;/p&gt;

&lt;p&gt;广告点击&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaiduAssistantsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;click&lt;/span&gt;
    &lt;span class="c1"&gt;# 存储关键信息&lt;/span&gt;
    &lt;span class="n"&gt;assistant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BaiduAssistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;idfa: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:idfa&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;os: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:os&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;oaid: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:oaid&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;imei: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:imei&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;first_or_initialize&lt;/span&gt;
    &lt;span class="n"&gt;assistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ua&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:ua&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:ip&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:ip_type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:click_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ext_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:ext_info&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;device_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:device_info&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;assistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save!&lt;/span&gt;

    &lt;span class="n"&gt;json_response&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;msg: &lt;/span&gt;&lt;span class="s1"&gt;'缓存成功'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查找广告点击记录&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaiduAssistantsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;
    &lt;span class="n"&gt;assistant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
    &lt;span class="n"&gt;assistants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# 根据oaid或者idfa查找&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:os&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="n"&gt;assistants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BaiduAssistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;os: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:os&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;idfa: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:device_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="n"&gt;assistant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assistants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
      &lt;span class="n"&gt;assistants&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;BaiduAssistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;os: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:os&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'oaid = ? or imei = ?'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:device_id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:device_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="n"&gt;assistant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assistants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# 需要前端手动传过来&lt;/span&gt;
    &lt;span class="n"&gt;user_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'X-ORIGINAL-UA'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# 如果相同的记录太多，那可能是空字符串的场景很多，则需要在原来记录的基础上根据 ip + ua进行筛选&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;assistants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="n"&gt;replace_object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;assistants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;ip: &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remote_ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ua: &lt;/span&gt;&lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
      &lt;span class="n"&gt;assistant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;replace_object&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;replace_object&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# 依旧没有记录则通过 ip + ua 进行查找&lt;/span&gt;
    &lt;span class="n"&gt;assistant&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;BaiduAssistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;ip: &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remote_ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;ua: &lt;/span&gt;&lt;span class="n"&gt;user_agent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json_response&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;msg: &lt;/span&gt;&lt;span class="s1"&gt;'找不到资源'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;assistant&lt;/span&gt;

    &lt;span class="c1"&gt;# 根据不同的 a_type（用户行为）进行 数据上送。&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RestClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;package_click_callback_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                     &lt;span class="ss"&gt;a_type: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:a_type&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                     &lt;span class="ss"&gt;a_value: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:a_value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                   &lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;Exceptions&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;InvalidAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'error_msg'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'code'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero?&lt;/span&gt;

    &lt;span class="n"&gt;json_response&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;msg: &lt;/span&gt;&lt;span class="s1"&gt;'请求成功'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;技术也只能支持到这里，这代码不一定是最好的，只能边跑边调整了，可以通过分析日志渐渐去优化代码。&lt;/p&gt;

&lt;p&gt;PS: &lt;em&gt;后来发现头条/抖音的做法比较流氓，只要用户在他们的 App 开启一次广告追踪的权限，哪怕后面关闭了，即便卸载 App 重装，点击广告的时候都能获取到设备的标识。具体怎么做的我们还没破解，虽然有点流氓，不过其实很有效。&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="尾声"&gt;尾声&lt;/h2&gt;
&lt;p&gt;这篇文章简单总结了一下近期给推广部门做技术支持的一些心得。总的来说过程说不上顺畅，毕竟两个不同性质的部门所使用的“语言”都是不一样的，磨合起来确实要花点时间，需要双方都多一些耐心了。总的来看，最早对接的是巨量，同时也最痛苦，百度就还行，Oppo 跟 Vivo 一言难尽。&lt;/p&gt;

&lt;p&gt;参考资料&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apple 广告与隐私 &lt;a href="https://www.apple.com.cn/legal/privacy/data/zh-cn/apple-advertising/" rel="nofollow" target="_blank" title=""&gt;https://www.apple.com.cn/legal/privacy/data/zh-cn/apple-advertising/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;百度营销 API 埋码指南 &lt;a href="https://dev2.baidu.com/content?sceneType=0&amp;amp;pageId=101213&amp;amp;nodeId=663&amp;amp;subhead=" rel="nofollow" target="_blank" title=""&gt;https://dev2.baidu.com/content?sceneType=0&amp;amp;pageId=101213&amp;amp;nodeId=663&amp;amp;subhead=&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;巨量引擎 &lt;a href="https://event-manager.oceanengine.com/docs/8650/omnichannel_api_doc/" rel="nofollow" target="_blank" title=""&gt;https://event-manager.oceanengine.com/docs/8650/omnichannel_api_doc/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Vivo 商业开放平台 &lt;a href="https://open-ad.vivo.com.cn/doc/index?id=162" rel="nofollow" target="_blank" title=""&gt;https://open-ad.vivo.com.cn/doc/index?id=162&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Vivo 对接文档 &lt;a href="https://ad.vivo.com.cn/help?id=352" rel="nofollow" target="_blank" title=""&gt;https://ad.vivo.com.cn/help?id=352&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Oppo 没有在线文档，需要联系商务。&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>lanzhiheng</author>
      <pubDate>Fri, 09 Sep 2022 08:56:32 +0800</pubDate>
      <link>https://ruby-china.org/topics/42640</link>
      <guid>https://ruby-china.org/topics/42640</guid>
    </item>
  </channel>
</rss>
