Z

ZingLix Blog

这里是 ZingLix 的个人博客,与你一起在计算机的世界探索~

Immich 反向地理编码原理和汉化思路

<p>Immich 默认识别出来的照片位置都奇奇怪怪的,不仅仅是英文,还有一些不常见的名字,在照片分类搜索的时候非常麻烦。周末仔细研究了下 Immich 到底是怎么实现反向地理编码的,并想办法对其进行了汉化。</p> <blockquote> <p>如果你到这里,是为了实现地名汉化的话,请直接前往 <a href="https://github.com/ZingLix/immich-geodata-cn">这个项目</a></p> </blockquote> <h2 id="immich-反向地理编码工作原理">Immich 反向地理编码工作原理</h2> <p>为了能够实现汉化的目标,首先我们得先明白 Immich 是怎么在本地实现反向地理编码的。</p> <h3 id="反向编码">反向编码</h3> <p>以下以 v1.124.2 为例,Immich 的反向地理编码都实现在 <a href="https://github.com/immich-app/immich/blob/1311189fab958bea2177a92e1cc1b7ebb1822bd8/server/src/repositories/map.repository.ts#L108"><code class="language-plaintext highlighter-rouge">reverseGeocode</code></a> 这个函数中,传入的是一个 <code class="language-plaintext highlighter-rouge">GeoPoint</code> 对象,实际上就是经度和纬度。</p> <p>之后,根据经纬度,进行了如下的 SQL 查询</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 </pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">geodata_places</span> <span class="k">WHERE</span> <span class="n">earth_box</span><span class="p">(</span><span class="n">ll_to_earth_public</span><span class="p">(</span><span class="err">$</span><span class="p">{</span><span class="n">point</span><span class="p">.</span><span class="n">latitude</span><span class="p">},</span> <span class="err">$</span><span class="p">{</span><span class="n">point</span><span class="p">.</span><span class="n">longitude</span><span class="p">}),</span> <span class="mi">25000</span><span class="p">)</span> <span class="o">@&gt;</span> <span class="n">ll_to_earth_public</span><span class="p">(</span><span class="n">latitude</span><span class="p">,</span> <span class="n">longitude</span><span class="p">)</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">earth_distance</span><span class="p">(</span> <span class="n">ll_to_earth_public</span><span class="p">(</span><span class="err">$</span><span class="p">{</span><span class="n">point</span><span class="p">.</span><span class="n">latitude</span><span class="p">},</span> <span class="err">$</span><span class="p">{</span><span class="n">point</span><span class="p">.</span><span class="n">longitude</span><span class="p">}),</span> <span class="n">ll_to_earth_public</span><span class="p">(</span><span class="n">latitude</span><span class="p">,</span> <span class="n">longitude</span><span class="p">)</span> <span class="p">)</span> <span class="k">LIMIT</span> <span class="mi">1</span><span class="p">;</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>这其中</p> <ul> <li><code class="language-plaintext highlighter-rouge">earth_box</code> 创建一个以给定点为中心的球体范围</li> <li><code class="language-plaintext highlighter-rouge">ll_to_earth_public</code> 将地理坐标 (纬度和经度) 转换为三维球体上的点</li> </ul> <p><code class="language-plaintext highlighter-rouge">WHERE</code> 子句筛选出 <strong>距离输入的目标点 25,000 米(25 公里)范围内</strong> 的地理点,<code class="language-plaintext highlighter-rouge">ORDER BY</code> 子句根据距离从近到远排序。换句话说,就是找到了 <code class="language-plaintext highlighter-rouge">geodata_places</code> 库中,距离输入点最近的地理点。</p> <p>找到了最近的点之后,取出这个点的 <code class="language-plaintext highlighter-rouge">{ countryCode, name: city, admin1Name }</code>,也就是 <strong>国家码</strong>、<strong>名称</strong>、<strong>一级行政区名称</strong>。整理一下顺序,将国家码转换成国家名,这就对应了我们在 Immich 中看到的照片位置中的 <strong>国</strong>、<strong>省</strong>、<strong>市</strong> 三级。至于这个表是如何构建的,后面我们再单独分析。</p> <p>这里名称和一级行政区名称都是直接从数据库表中得到的,而国家名是从国家码转换得到的,这里用到了 <a href="https://github.com/michaelwittig/node-i18n-iso-countries">node-i18n-iso-countries</a> 这个库的 <code class="language-plaintext highlighter-rouge">getName</code> 方法。但在 Immich 中,调用时的代码是 <code class="language-plaintext highlighter-rouge">getName(countryCode, 'en')</code>,将语言用 <code class="language-plaintext highlighter-rouge">'en'</code> 写死了,所以只能是英文,并没有加上任何 i18n 的机制。</p> <p>而如果上面没有找到的话,就会再进行一次 SQL 查询</p> <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 </pre></td><td class="rouge-code"><pre><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">naturalearth_countries</span> <span class="k">WHERE</span> <span class="n">coordinates</span> <span class="o">@&gt;</span> <span class="n">point</span><span class="p">(:</span><span class="n">longitude</span><span class="p">,</span> <span class="p">:</span><span class="n">latitude</span><span class="p">)</span> <span class="k">LIMIT</span> <span class="mi">1</span><span class="p">;</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>这段 SQL 就是在 <code class="language-plaintext highlighter-rouge">naturalearth_countries</code> 表中找到哪些记录的 coordinates 包含输入的坐标,也就是根据自然地球中国家的划分,确定坐标所在的国家。如果走到这一条,则不会再去确定更细粒度的省市两级划分。</p> <p><strong>简而言之,Immich 就是在数据库里事先准备好了大量地名,然后用照片的坐标去匹配数据库里最近的地名,之后就以该地名作为照片的地名。找不到的话,就退化到只用国家信息,根据国家的区划划分。</strong></p> <h3 id="数据构建">数据构建</h3> <p>接下来的一个大问题就是,数据库里的数据是从哪来的。</p> <p>Immich 所有的反向地理编码数据都来的 <a href="https://www.geonames.org/">GeoNames</a>,放在了 <code class="language-plaintext highlighter-rouge">/build/geodata</code> 文件夹下,每次发版都会从 <a href="https://download.geonames.org/export/dump/">这里</a> 获取最新的数据。</p> <p>文件夹中有这么几个文件:</p> <ul> <li>admin1CodesASCII.txt:一级行政区划列表(<code class="language-plaintext highlighter-rouge">id | name | name ascii | geoname id</code>)</li> <li>admin2Codes.txt:二级行政区划列表(<code class="language-plaintext highlighter-rouge">id | name | name ascii | geoname id</code>)</li> <li>cities500.txt:所有人口大于 500 的城市列表</li> <li>geodata-date.txt:数据更新时间</li> <li>ne_10m_admin_0_countries.geojson:自然地球国家划分,详细介绍可以 <a href="https://github.com/nvkelso/natural-earth-vector/tree/master">看这</a></li> </ul> <p>Immich 导入的入口在 <a href="https://github.com/immich-app/immich/blob/1311189fab958bea2177a92e1cc1b7ebb1822bd8/server/src/repositories/map.repository.ts#L41C1-L42C1"><code class="language-plaintext highlighter-rouge">init</code></a> 函数中,这里会首先查看 <code class="language-plaintext highlighter-rouge">system-metadata</code> 中 key 为 <code class="language-plaintext highlighter-rouge">reverse-geocoding-state</code> 的值,里面记录了 <code class="language-plaintext highlighter-rouge">lastUpdate</code> 的时间,也就是上次导入数据的时间。会将这个时间与 <code class="language-plaintext highlighter-rouge">geodata-date.txt</code> 文件中的时间进行比较,如果文件中时间较新则说明有更新的数据则开始导入,否则就跳过避免重复导入。</p> <p>具体导入的逻辑在 <a href="https://github.com/immich-app/immich/blob/1311189fab958bea2177a92e1cc1b7ebb1822bd8/server/src/repositories/map.repository.ts#L207"><code class="language-plaintext highlighter-rouge">importGeodata</code></a> 中,其中抛开建立表的逻辑,核心在于 <a href="https://github.com/immich-app/immich/blob/1311189fab958bea2177a92e1cc1b7ebb1822bd8/server/src/repositories/map.repository.ts#L226C3-L226C11"><code class="language-plaintext highlighter-rouge">loadCities500</code></a> 函数。</p> <p>cities500.txt 中格式类似 csv,以 <code class="language-plaintext highlighter-rouge">\t</code> 作为分隔,通过如下规则转换成数据库中的内容</p> <div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 </pre></td><td class="rouge-code"><pre><span class="nx">id</span><span class="p">:</span> <span class="nb">Number</span><span class="p">.</span><span class="nf">parseInt</span><span class="p">(</span><span class="nx">lineSplit</span><span class="p">[</span><span class="mi">0</span><span class="p">]),</span> <span class="nx">name</span><span class="p">:</span> <span class="nx">lineSplit</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="nx">alternateNames</span><span class="p">:</span> <span class="nx">lineSplit</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span> <span class="nx">latitude</span><span class="p">:</span> <span class="nb">Number</span><span class="p">.</span><span class="nf">parseFloat</span><span class="p">(</span><span class="nx">lineSplit</span><span class="p">[</span><span class="mi">4</span><span class="p">]),</span> <span class="nx">longitude</span><span class="p">:</span> <span class="nb">Number</span><span class="p">.</span><span class="nf">parseFloat</span><span class="p">(</span><span class="nx">lineSplit</span><span class="p">[</span><span class="mi">5</span><span class="p">]),</span> <span class="nx">countryCode</span><span class="p">:</span> <span class="nx">lineSplit</span><span class="p">[</span><span class="mi">8</span><span class="p">],</span> <span class="nx">admin1Code</span><span class="p">:</span> <span class="nx">lineSplit</span><span class="p">[</span><span class="mi">10</span><span class="p">],</span> <span class="nx">admin2Code</span><span class="p">:</span> <span class="nx">lineSplit</span><span class="p">[</span><span class="mi">11</span><span class="p">],</span> <span class="nx">modificationDate</span><span class="p">:</span> <span class="nx">lineSplit</span><span class="p">[</span><span class="mi">18</span><span class="p">],</span> <span class="nx">admin1Name</span><span class="p">:</span> <span class="nx">admin1Map</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">lineSplit</span><span class="p">[</span><span class="mi">8</span><span class="p">]}</span><span class="s2">.</span><span class="p">${</span><span class="nx">lineSplit</span><span class="p">[</span><span class="mi">10</span><span class="p">]}</span><span class="s2">`</span><span class="p">)</span> <span class="o">??</span> <span class="kc">null</span><span class="p">,</span> <span class="nx">admin2Name</span><span class="p">:</span> <span class="nx">admin2Map</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">lineSplit</span><span class="p">[</span><span class="mi">8</span><span class="p">]}</span><span class="s2">.</span><span class="p">${</span><span class="nx">lineSplit</span><span class="p">[</span><span class="mi">10</span><span class="p">]}</span><span class="s2">.</span><span class="p">${</span><span class="nx">lineSplit</span><span class="p">[</span><span class="mi">11</span><span class="p">]}</span><span class="s2">`</span><span class="p">)</span> <span class="o">??</span> <span class="kc">null</span><span class="p">,</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>这其中 <code class="language-plaintext highlighter-rouge">admin1Map</code> 和 <code class="language-plaintext highlighter-rouge">admin2Map</code> 就是通过读取 <code class="language-plaintext highlighter-rouge">admin1CodesASCII.txt</code> 和 <code class="language-plaintext highlighter-rouge">admin2Codes.txt</code> 中 <code class="language-plaintext highlighter-rouge">id</code> 到 <code class="language-plaintext highlighter-rouge">name</code> 的映射关系得到的。</p> <p>再结合前面提到的反向编码逻辑,就是根据 <code class="language-plaintext highlighter-rouge">latitude</code> 和 <code class="language-plaintext highlighter-rouge">longitude</code> 找到最近的点,然后拿到他的 <code class="language-plaintext highlighter-rouge">countryCode</code>、<code class="language-plaintext highlighter-rouge">admin1Name</code> 和 <code class="language-plaintext highlighter-rouge">name</code>,这一信息就作为了照片的地理位置信息。</p> <blockquote> <p>没错,admin2Name 根本没用上,admin2Codes.txt 也没用</p> </blockquote> <h2 id="汉化思路">汉化思路</h2> <p>Immich 将照片的地理位置信息分为了 <strong>国</strong>、<strong>省</strong>、<strong>市</strong> 三级。再捋一遍文件的作用,也就是</p> <ul> <li>从 cities500.txt 中找到最近的点,拿到他的名称作为 <strong>市</strong></li> <li>根据这个点的 admin1Code 信息,去 admin1CodesASCII.txt 文件中找到 <strong>省</strong> 级别的名称</li> <li>根据这个点的 countryCode,用 <a href="https://github.com/michaelwittig/node-i18n-iso-countries">node-i18n-iso-countries</a> 转换成 <strong>国</strong> 级别名称</li> </ul> <p>作用搞清楚了,接下来汉化的思路就好搞了</p> <h3 id="国">国</h3> <p>这一步骤主要依赖 <a href="https://github.com/michaelwittig/node-i18n-iso-countries">node-i18n-iso-countries</a> 这个库,而 <a href="https://github.com/immich-app/immich/blob/1311189fab958bea2177a92e1cc1b7ebb1822bd8/server/src/repositories/map.repository.ts#L131">代码</a> 中把转换的目标语言写死为了 <code class="language-plaintext highlighter-rouge">en</code>,那么没有办法改目标语言,就只能从这个库的数据入手。</p> <p>这个库的数据来源也是通过静态文件的形式实现的,具体文件内容可以看 <a href="https://github.com/michaelwittig/node-i18n-iso-countries/tree/master/langs">这里</a>。<code class="language-plaintext highlighter-rouge">en.json</code> 就是转换成 <code class="language-plaintext highlighter-rouge">'en'</code> 时候的数据来源,那我们只需要将其改写成中文即可,而中文的信息就在 <code class="language-plaintext highlighter-rouge">zh.json</code> 里,替换掉即可,就像 <a href="https://github.com/ZingLix/immich-geodata-cn/blob/main/i18n-iso-countries/langs/en.json">这样</a>。</p> <p>最后,将修改后的文件替换掉 Immich 镜像中的原始文件就可以了。</p> <h3 id="省">省</h3> <p>省的名称都在 admin1CodesASCII.txt 文件中,好在 <a href="https://download.geonames.org/export/dump/">GeoNames</a> 提供了 alternateNamesV2.zip 这一文件,包含了许多地点的不同语言的名称,借助这一信息可以直接进行翻译,替换掉原来的名称即可。代码实现在 <a href="https://github.com/ZingLix/immich-geodata-cn/blob/432198c58216c1d7de75f8283ae35fd310abd8ae/geodata/translate.py#L119">这里</a>。</p> <h3 id="市">市</h3> <p>cities500.txt 这个文件主要的目标就是翻译 name 字段,但观察这个文件后可以发现,它的粒度非常细,不仅仅到市一级,还可能是区或者县,还是很古老的名字,非常不适合使用。</p> <p>为了解决这个问题,可以通过地图提供商的逆向地理编码 API 对这些地方进行重新识别,获得标准的一级、二级行政区划名称,这里分别实现了适用于 <a href="https://github.com/ZingLix/immich-geodata-cn/blob/main/geodata/generate_geodata_amap.py">国内采用高德的版本</a> 和 <a href="https://github.com/ZingLix/immich-geodata-cn/blob/main/geodata/generate_geodata_locationiq.py">国外使用 LocationIQ 的版本</a>。</p> <p>另外,默认的 cities500.txt 文件由于数据量有限,部分地区数据点较少,就会导致 Immich 在反向地理编码的时候出错。而实际上,<a href="https://download.geonames.org/export/dump/">GeoNames</a> 还提供了不同国家的完整地理点信息,比如 <code class="language-plaintext highlighter-rouge">CN.zip</code>,可以作为补充添加进 cities500.txt 以提升效果,实现在 <a href="https://github.com/ZingLix/immich-geodata-cn/blob/main/geodata/enhance_data.py">这里</a>。但考虑到数据量庞大,所以只默认增加了直辖市,有需要的再增加。</p> <h2 id="总结">总结</h2> <p>以上总结了 Immich 逆向地理编码的原理,以及分享了如何实现汉化的,代码都放在了这个 <a href="https://github.com/ZingLix/immich-geodata-cn">仓库</a> 中,也有现成的东西可以用。</p>

2025/1/23
阅读更多

Go 在使用泛型时无法与 Pointer Receiver 共存的解决方法

<h2 id="问题描述">问题描述</h2> <p>在使用 Go 的泛型时,如果泛型类型存在 constraint,而传入的类型在实现这个 constraint 时使用的是 pointer receiver,那么就会遇到 <code class="language-plaintext highlighter-rouge">XXX does not satisfy XXX (method XXX has pointer receiver)</code> 的报错,就比如下面这个例子希望用 <code class="language-plaintext highlighter-rouge">Create</code> 函数完成所有创建 <code class="language-plaintext highlighter-rouge">Person</code> 的操作</p> <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 </pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">Person</span> <span class="k">interface</span> <span class="p">{</span> <span class="n">SetID</span><span class="p">(</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">}</span> <span class="k">type</span> <span class="n">Student</span> <span class="k">struct</span> <span class="p">{</span> <span class="n">ID</span> <span class="kt">int</span> <span class="p">}</span> <span class="k">func</span> <span class="p">(</span><span class="n">p</span> <span class="o">*</span><span class="n">Student</span><span class="p">)</span> <span class="n">SetID</span><span class="p">(</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span> <span class="n">p</span><span class="o">.</span><span class="n">ID</span> <span class="o">=</span> <span class="n">id</span> <span class="p">}</span> <span class="k">func</span> <span class="n">Create</span><span class="p">[</span><span class="n">T</span> <span class="n">Person</span><span class="p">](</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="o">*</span><span class="n">T</span> <span class="p">{</span> <span class="k">var</span> <span class="n">person</span> <span class="n">T</span> <span class="n">person</span><span class="o">.</span><span class="n">SetID</span><span class="p">(</span><span class="n">id</span><span class="p">)</span> <span class="k">return</span> <span class="o">&amp;</span><span class="n">person</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>这里 <code class="language-plaintext highlighter-rouge">Student</code> 用 <code class="language-plaintext highlighter-rouge">(p *Student)</code> 实现了 <code class="language-plaintext highlighter-rouge">Person</code>,然而如果用 <code class="language-plaintext highlighter-rouge">Create[Student](id)</code> 这种方式调用时,编译会遇到这个报错</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 </pre></td><td class="rouge-code"><pre>Student does not satisfy Person (method SetID has pointer receiver) </pre></td></tr></tbody></table></code></pre></div></div> <h2 id="问题解释">问题解释</h2> <p>问题就在于这段代码中的 <code class="language-plaintext highlighter-rouge">(p *Student)</code></p> <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 </pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="p">(</span><span class="n">p</span> <span class="o">*</span><span class="n">Student</span><span class="p">)</span> <span class="n">SetID</span><span class="p">(</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span> <span class="n">p</span><span class="o">.</span><span class="n">ID</span> <span class="o">=</span> <span class="n">id</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>在 Go 中会认为是 <code class="language-plaintext highlighter-rouge">*Student</code> 实现了 <code class="language-plaintext highlighter-rouge">SetID</code> 方法,或者说实现了 <code class="language-plaintext highlighter-rouge">Person</code> interface,而不是 <code class="language-plaintext highlighter-rouge">Student</code>,因此提示 <code class="language-plaintext highlighter-rouge">Student</code> 并不满足 <code class="language-plaintext highlighter-rouge">Person</code>。</p> <p>那一个办法是把实现 interface 传入的改成 value receiver</p> <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 </pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="p">(</span><span class="n">p</span> <span class="n">Student</span><span class="p">)</span> <span class="n">SetID</span><span class="p">(</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="p">{</span> <span class="c">// 传入的 p 类型去掉了 *</span> <span class="n">p</span><span class="o">.</span><span class="n">ID</span> <span class="o">=</span> <span class="n">id</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>这样可以通过编译且正常运行,但问题是变成了值传递后,<code class="language-plaintext highlighter-rouge">SetID</code> 并不会作用于传入的那个变量,这个函数也形同虚设。</p> <p>另一个解决方案是可以把调用函数时改成 <code class="language-plaintext highlighter-rouge">Create[*Student](1)</code>,加上这个 <code class="language-plaintext highlighter-rouge">*</code>,报错也会随之消除。但问题就解决了吗?</p> <p>再仔细看这个函数在传入类型后会变成什么样</p> <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 7 </pre></td><td class="rouge-code"><pre><span class="c">// T -&gt; *Student</span> <span class="k">func</span> <span class="n">Create</span><span class="p">[</span><span class="n">T</span> <span class="n">Person</span><span class="p">](</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="o">*</span><span class="n">T</span> <span class="p">{</span> <span class="k">var</span> <span class="n">person</span> <span class="n">T</span> <span class="c">// var person *Student</span> <span class="n">person</span><span class="o">.</span><span class="n">SetID</span><span class="p">(</span><span class="n">id</span><span class="p">)</span> <span class="k">return</span> <span class="o">&amp;</span><span class="n">person</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>这里暂且不论原本的返回类型 <code class="language-plaintext highlighter-rouge">*T</code> 会变成 <code class="language-plaintext highlighter-rouge">**Student</code> 的问题,这个很容易通过调整返回值类型解决。</p> <p>核心问题在于第二行我们声明了一个 <code class="language-plaintext highlighter-rouge">*Student</code> 类型的指针,但实例化在哪?我们创建了一个空指针,所以在运行时会遇到 <code class="language-plaintext highlighter-rouge">runtime error: invalid memory address or nil pointer dereference</code>。同时由于语言限制,我们手上的 <code class="language-plaintext highlighter-rouge">T*</code> 并不能转成 <code class="language-plaintext highlighter-rouge">T</code> 然后让我们完成实例化。</p> <p>那么能不能传入 <code class="language-plaintext highlighter-rouge">T</code>,然后转成指针再调用 interface 的方法呢?</p> <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 </pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">Create</span><span class="p">[</span><span class="n">T</span> <span class="n">Person</span><span class="p">](</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="o">*</span><span class="n">T</span> <span class="p">{</span> <span class="n">person</span> <span class="o">:=</span> <span class="nb">new</span><span class="p">(</span><span class="n">T</span><span class="p">)</span> <span class="n">person</span><span class="o">.</span><span class="n">SetID</span><span class="p">(</span><span class="n">id</span><span class="p">)</span> <span class="c">// 报错</span> <span class="k">return</span> <span class="o">&amp;</span><span class="n">person</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>然而编译器又给了一个错误 <code class="language-plaintext highlighter-rouge">person.SetID undefined (type *T is pointer to type parameter, not type parameter)</code>,这个问题在于 <code class="language-plaintext highlighter-rouge">SetID</code> 是定义给 <code class="language-plaintext highlighter-rouge">Student</code> 的,不是给 <code class="language-plaintext highlighter-rouge">Student*</code> 用的。</p> <p>很遗憾,由于 Go 语言层面的缺陷,在仅使用 <code class="language-plaintext highlighter-rouge">T</code> 这一个参数时并不能完成我们想要的东西,如果有办法,请通过网页最下方的邮件告诉我,不甚感激。</p> <h2 id="解决方案">解决方案</h2> <p>问题在于用 <code class="language-plaintext highlighter-rouge">T</code> 编译器不认 constraint,用 <code class="language-plaintext highlighter-rouge">T*</code> 又拿不到 <code class="language-plaintext highlighter-rouge">T</code> 进行实例化,那么只能去掉 <code class="language-plaintext highlighter-rouge">T</code> 的限制,同时再传入带有限制的 <code class="language-plaintext highlighter-rouge">T*</code>。思路如此,具体实现来说需要定义这么一个 interface</p> <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 </pre></td><td class="rouge-code"><pre><span class="k">type</span> <span class="n">PersonPtr</span><span class="p">[</span><span class="n">T</span> <span class="n">any</span><span class="p">]</span> <span class="k">interface</span> <span class="p">{</span> <span class="o">*</span><span class="n">T</span> <span class="n">Person</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>这个定义了一个指针 interface,第一行这里暂时先去掉了 constraint,允许传入任意类型 <code class="language-plaintext highlighter-rouge">T</code>,然后通过第二行使得这个 interface 允许的类型是且只能是 <code class="language-plaintext highlighter-rouge">*T</code>,让我们能从 <code class="language-plaintext highlighter-rouge">T</code> 拿到指针,再通过第三行去保证实现了 <code class="language-plaintext highlighter-rouge">Person</code> 这个 interface。</p> <p>那我们就可以进一步修改函数,将传入的类型改为 <code class="language-plaintext highlighter-rouge">PersonPtr</code></p> <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 </pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">Create</span><span class="p">[</span><span class="n">Ptr</span> <span class="n">PersonPtr</span><span class="p">[</span><span class="n">T</span><span class="p">]](</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="o">*</span><span class="n">T</span> <span class="p">{</span> <span class="k">var</span> <span class="n">ptr</span> <span class="n">Ptr</span> <span class="o">=</span> <span class="nb">new</span><span class="p">(</span><span class="n">T</span><span class="p">)</span> <span class="n">ptr</span><span class="o">.</span><span class="n">SetID</span><span class="p">(</span><span class="n">id</span><span class="p">)</span> <span class="k">return</span> <span class="n">ptr</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>但这仍然不够,编译器会提示 <code class="language-plaintext highlighter-rouge">undefined: T</code>,因为我们没有定义 <code class="language-plaintext highlighter-rouge">T</code>,所以必须在函数的泛型列表中加上 <code class="language-plaintext highlighter-rouge">T</code>,这个函数只能变为</p> <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 </pre></td><td class="rouge-code"><pre><span class="k">func</span> <span class="n">Create</span><span class="p">[</span><span class="n">T</span> <span class="n">any</span><span class="p">,</span> <span class="n">Ptr</span> <span class="n">PersonPtr</span><span class="p">[</span><span class="n">T</span><span class="p">]](</span><span class="n">id</span> <span class="kt">int</span><span class="p">)</span> <span class="o">*</span><span class="n">T</span> <span class="p">{</span> <span class="k">var</span> <span class="n">ptr</span> <span class="n">Ptr</span> <span class="o">=</span> <span class="nb">new</span><span class="p">(</span><span class="n">T</span><span class="p">)</span> <span class="n">ptr</span><span class="o">.</span><span class="n">SetID</span><span class="p">(</span><span class="n">id</span><span class="p">)</span> <span class="k">return</span> <span class="n">ptr</span> <span class="p">}</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>调用时就变成了</p> <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 </pre></td><td class="rouge-code"><pre><span class="n">stu</span> <span class="o">:=</span> <span class="n">Create</span><span class="p">[</span><span class="n">Student</span><span class="p">,</span> <span class="o">*</span><span class="n">Student</span><span class="p">](</span><span class="m">1</span><span class="p">)</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>这样调用真的很丑,但好在 Go 这回终于做了个人,通过类型的自动推导可以自动推导出第二个参数,所以调用时可以简化为</p> <div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 </pre></td><td class="rouge-code"><pre><span class="n">stu</span> <span class="o">:=</span> <span class="n">Create</span><span class="p">[</span><span class="n">Student</span><span class="p">](</span><span class="m">1</span><span class="p">)</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>这样调用看起来就和谐了许多(虽然背后的实现需要用些难懂的 trick,但我们至少终于实现了 Go 中的泛型与 pointer receiver 的共存…</p> <h2 id="总结">总结</h2> <p>珍爱生命,远离 Go 的泛型!</p>

2024/1/24
阅读更多

N5105 PVE 虚拟机随机死机/重启解决方案

<p>N5105 运行虚拟机会随机死机/重启的问题很常见,之前我采取过如下办法</p> <ul> <li>爱快降级至3.6.1</li> <li>OpenWRT 换用 LXC 模式安装</li> <li>关闭各种直通</li> </ul> <p>只能说降低了死机概率,一般能撑到一天以上,所以我选择在半夜自动重启,勉强可以正常使用,但日常使用还是不可避免的会断网。</p> <p>不过现在似乎有了一个终极解决方案,可以彻底解决 N5105 的死机问题,根据这个<a href="https://forum.proxmox.com/threads/vm-freezes-irregularly.111494/page-28">链接</a>反馈,已经可以超过 10 天稳定运行,我目前也暂时未遇到死机问题。</p> <blockquote> <p>UPDATE: 我已经几十天都没有死机过了</p> </blockquote> <p>解决方案就是更新 microcode 至 <code class="language-plaintext highlighter-rouge">0x24000024</code> 版本。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 </pre></td><td class="rouge-code"><pre># 安装 microcode apt update apt install intel-microcode reboot # 查看 microcode 版本 dmesg -T | grep microcode </pre></td></tr></tbody></table></code></pre></div></div> <p>重启完成后,microcode 应该就已经更新到不会死机的版本了,你应该可以看到 <code class="language-plaintext highlighter-rouge">0x24000024</code> 字样。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 </pre></td><td class="rouge-code"><pre>root@pve:~# dmesg -T | grep microcode [Wed Mar 22 22:23:26 2023] microcode: microcode updated early to revision 0x24000024, date = 2022-09-02 [Wed Mar 22 22:23:26 2023] SRBDS: Vulnerable: No microcode [Wed Mar 22 22:23:30 2023] microcode: sig=0x906c0, pf=0x1, revision=0x24000024 [Wed Mar 22 22:23:30 2023] microcode: Microcode Update Driver: v2.2. </pre></td></tr></tbody></table></code></pre></div></div> <p>或者 <code class="language-plaintext highlighter-rouge">grep 'stepping\|model\|microcode' /proc/cpuinfo</code> 查看 microcode 版本。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 </pre></td><td class="rouge-code"><pre>root@pve:~# grep 'stepping\|model\|microcode' /proc/cpuinfo model : 156 model name : Intel(R) Celeron(R) N5105 @ 2.00GHz stepping : 0 microcode : 0x24000024 </pre></td></tr></tbody></table></code></pre></div></div> <p>但如果源版本比较老的话,更新的版本还是例如 <code class="language-plaintext highlighter-rouge">0x24000023</code> 的话,就请继续后续步骤</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 7 </pre></td><td class="rouge-code"><pre># 接下来继续更新 wget https://github.com/intel/Intel-Linux-Processor-Microcode-Data-Files/archive/main.zip unzip main.zip -d MCU cp -r /root/MCU/Intel-Linux-Processor-Microcode-Data-Files-main/intel-ucode/. /lib/firmware/intel-ucode/ update-initramfs -u reboot # 重启后应当可以更新至 0x24000024 </pre></td></tr></tbody></table></code></pre></div></div>

2023/3/22
阅读更多

PVE 下 LXC 启动 Docker 失败解决方案

<p>PVE 下通过 LXC 安装的 Ubuntu 启动 Docker 镜像时候提示</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 </pre></td><td class="rouge-code"><pre>docker: Error response from daemon: AppArmor enabled on system but the docker-default profile could not be loaded: running `/usr/sbin/apparmor_parser apparmor_parser -Kr /var/lib/docker/tmp/docker-default6944525` failed with output: apparmor_parser: Unable to replace "docker-default". Permission denied; attempted to load a profile while confined? </pre></td></tr></tbody></table></code></pre></div></div> <p>解决方式是在调整启动配置</p> <p>PVE 设置中 <code class="language-plaintext highlighter-rouge">选项-功能</code> 中选中 <code class="language-plaintext highlighter-rouge">嵌套</code></p> <p>然后在宿主机中找到 <code class="language-plaintext highlighter-rouge">/etc/pve/lxc/100.conf</code>(注意把 <code class="language-plaintext highlighter-rouge">100</code> 替换成你的 LXC 容器 id),增加如下几句话,之后重启</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 </pre></td><td class="rouge-code"><pre>lxc.apparmor.profile: unconfined lxc.cgroup.devices.allow: a lxc.cap.drop: </pre></td></tr></tbody></table></code></pre></div></div>

2023/3/9
阅读更多

Encode Email

<script> function encodeEmail(email, key) { // Hex encode the key var encodedKey = key.toString(16); // ensure it is two digits long var encodedString = make2DigitsLong(encodedKey); // loop through every character in the email for(var n=0; n < email.length; n++) { // Get the code (in decimal) for the nth character var charCode = email.charCodeAt(n); // XOR the character with the key var encoded = charCode ^ key; // Hex encode the result, and append to the output string var value = encoded.toString(16); encodedString += make2DigitsLong(value); } return encodedString; } function make2DigitsLong(value){ return value.length === 1 ? '0' + value : value; } function go(){ var text = document.getElementById('input').value; console.log(text) console.log(encodeEmail(text, 156)); document.getElementById('output').value= encodeEmail(text, 156); } </script> <p><input id="input" /> <br /> <button onclick="go()">Encode</button> <br /> <input id="output" /></p>

2022/8/25
阅读更多

快速查看显卡使用情况和占用用户

<p>使用方法: <code class="language-plaintext highlighter-rouge">python gpu.py</code></p> <p>需要的依赖: <code class="language-plaintext highlighter-rouge">xmltodict</code></p> <div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 </pre></td><td class="rouge-code"><pre><span class="kn">import</span> <span class="n">subprocess</span> <span class="kn">import</span> <span class="n">xmltodict</span><span class="p">,</span> <span class="n">pwd</span><span class="p">,</span> <span class="n">json</span> <span class="n">UID</span> <span class="o">=</span> <span class="mi">1</span> <span class="n">EUID</span> <span class="o">=</span> <span class="mi">2</span> <span class="k">def</span> <span class="nf">owner</span><span class="p">(</span><span class="n">pid</span><span class="p">):</span> <span class="sh">"""</span><span class="s">Return username of UID of process pid</span><span class="sh">"""</span> <span class="k">for</span> <span class="n">ln</span> <span class="ow">in</span> <span class="nf">open</span><span class="p">(</span><span class="sh">"</span><span class="s">/proc/{}/status</span><span class="sh">"</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span><span class="n">pid</span><span class="p">)):</span> <span class="k">if</span> <span class="n">ln</span><span class="p">.</span><span class="nf">startswith</span><span class="p">(</span><span class="sh">"</span><span class="s">Uid:</span><span class="sh">"</span><span class="p">):</span> <span class="n">uid</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">ln</span><span class="p">.</span><span class="nf">split</span><span class="p">()[</span><span class="n">UID</span><span class="p">])</span> <span class="k">return</span> <span class="n">pwd</span><span class="p">.</span><span class="nf">getpwuid</span><span class="p">(</span><span class="n">uid</span><span class="p">).</span><span class="n">pw_name</span> <span class="k">def</span> <span class="nf">add_user</span><span class="p">(</span><span class="n">process</span><span class="p">):</span> <span class="n">tmp</span> <span class="o">=</span> <span class="p">[]</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">process</span><span class="p">:</span> <span class="n">p</span><span class="p">[</span><span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="nf">owner</span><span class="p">(</span><span class="n">p</span><span class="p">[</span><span class="sh">"</span><span class="s">pid</span><span class="sh">"</span><span class="p">])</span> <span class="n">tmp</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="k">return</span> <span class="n">tmp</span> <span class="k">def</span> <span class="nf">simplify</span><span class="p">(</span><span class="n">gpu</span><span class="p">):</span> <span class="n">tmp</span> <span class="o">=</span> <span class="p">{}</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">gpu</span><span class="p">.</span><span class="nf">keys</span><span class="p">():</span> <span class="k">if</span> <span class="n">k</span> <span class="ow">in</span> <span class="p">[</span> <span class="sh">"</span><span class="s">@id</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">product_name</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">fan_speed</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">fb_memory_usage</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">utilization</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">temperature</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">processes</span><span class="sh">"</span><span class="p">,</span> <span class="p">]:</span> <span class="n">tmp</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="n">gpu</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="k">return</span> <span class="n">tmp</span> <span class="k">def</span> <span class="nf">get_gpu_info</span><span class="p">():</span> <span class="n">sp</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="nc">Popen</span><span class="p">(</span> <span class="p">[</span><span class="sh">"</span><span class="s">nvidia-smi</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-q</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">-x</span><span class="sh">"</span><span class="p">],</span> <span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="p">.</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">subprocess</span><span class="p">.</span><span class="n">PIPE</span><span class="p">,</span> <span class="p">)</span> <span class="n">out_str</span> <span class="o">=</span> <span class="n">sp</span><span class="p">.</span><span class="nf">communicate</span><span class="p">()</span> <span class="n">out_str</span> <span class="o">=</span> <span class="n">out_str</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">decode</span><span class="p">(</span><span class="sh">"</span><span class="s">utf-8</span><span class="sh">"</span><span class="p">)</span> <span class="n">o</span> <span class="o">=</span> <span class="n">xmltodict</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">out_str</span><span class="p">)[</span><span class="sh">"</span><span class="s">nvidia_smi_log</span><span class="sh">"</span><span class="p">]</span> <span class="n">o</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="nf">dumps</span><span class="p">(</span><span class="n">o</span><span class="p">))</span> <span class="n">gpu_list</span> <span class="o">=</span> <span class="p">[]</span> <span class="k">if</span> <span class="ow">not</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">o</span><span class="p">[</span><span class="sh">"</span><span class="s">gpu</span><span class="sh">"</span><span class="p">],</span> <span class="nb">list</span><span class="p">):</span> <span class="n">o</span><span class="p">[</span><span class="sh">"</span><span class="s">gpu</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">o</span><span class="p">[</span><span class="sh">"</span><span class="s">gpu</span><span class="sh">"</span><span class="p">]]</span> <span class="k">for</span> <span class="n">gpu</span> <span class="ow">in</span> <span class="n">o</span><span class="p">[</span><span class="sh">"</span><span class="s">gpu</span><span class="sh">"</span><span class="p">]:</span> <span class="k">if</span> <span class="n">gpu</span><span class="p">[</span><span class="sh">"</span><span class="s">processes</span><span class="sh">"</span><span class="p">]</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span> <span class="n">gpu</span><span class="p">[</span><span class="sh">"</span><span class="s">processes</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="p">{}</span> <span class="n">gpu</span><span class="p">[</span><span class="sh">"</span><span class="s">processes</span><span class="sh">"</span><span class="p">][</span><span class="sh">"</span><span class="s">process_info</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">process</span> <span class="o">=</span> <span class="n">gpu</span><span class="p">[</span><span class="sh">"</span><span class="s">processes</span><span class="sh">"</span><span class="p">][</span><span class="sh">"</span><span class="s">process_info</span><span class="sh">"</span><span class="p">]</span> <span class="k">if</span> <span class="ow">not</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">process</span><span class="p">,</span> <span class="nb">list</span><span class="p">):</span> <span class="n">process</span> <span class="o">=</span> <span class="p">[</span><span class="n">process</span><span class="p">]</span> <span class="n">process</span> <span class="o">=</span> <span class="nf">add_user</span><span class="p">(</span><span class="n">process</span><span class="p">)</span> <span class="n">gpu</span><span class="p">[</span><span class="sh">"</span><span class="s">processes</span><span class="sh">"</span><span class="p">][</span><span class="sh">"</span><span class="s">process_info</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="n">process</span> <span class="n">gpu</span> <span class="o">=</span> <span class="nf">simplify</span><span class="p">(</span><span class="n">gpu</span><span class="p">)</span> <span class="n">gpu_list</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">gpu</span><span class="p">)</span> <span class="n">o</span><span class="p">[</span><span class="sh">"</span><span class="s">gpu</span><span class="sh">"</span><span class="p">]</span> <span class="o">=</span> <span class="n">gpu_list</span> <span class="k">return</span> <span class="n">o</span> <span class="n">gpu</span> <span class="o">=</span> <span class="nf">get_gpu_info</span><span class="p">()</span> <span class="nf">print</span><span class="p">()</span> <span class="nf">print</span><span class="p">(</span> <span class="sh">"</span><span class="s"> {: &lt;13}</span><span class="se">\t</span><span class="s">{: &lt;8}</span><span class="se">\t</span><span class="s">{: &lt;20}</span><span class="se">\t</span><span class="s">{}</span><span class="sh">"</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">pid</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">used_memory</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">process_name</span><span class="sh">"</span> <span class="p">)</span> <span class="p">)</span> <span class="nf">print</span><span class="p">(</span> <span class="sh">"</span><span class="s">---------------------------------------------------------</span><span class="sh">"</span> <span class="p">)</span> <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">g</span> <span class="ow">in</span> <span class="nf">enumerate</span><span class="p">(</span><span class="n">gpu</span><span class="p">[</span><span class="sh">"</span><span class="s">gpu</span><span class="sh">"</span><span class="p">]):</span> <span class="nf">print</span><span class="p">(</span> <span class="sh">"</span><span class="s">{} {} ({}):</span><span class="sh">"</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span> <span class="n">i</span><span class="p">,</span> <span class="n">g</span><span class="p">[</span><span class="sh">"</span><span class="s">product_name</span><span class="sh">"</span><span class="p">],</span> <span class="n">g</span><span class="p">[</span><span class="sh">"</span><span class="s">utilization</span><span class="sh">"</span><span class="p">][</span><span class="sh">"</span><span class="s">gpu_util</span><span class="sh">"</span><span class="p">],</span> <span class="p">)</span> <span class="p">)</span> <span class="n">total</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">g</span><span class="p">[</span><span class="sh">"</span><span class="s">fb_memory_usage</span><span class="sh">"</span><span class="p">][</span><span class="sh">"</span><span class="s">total</span><span class="sh">"</span><span class="p">].</span><span class="nf">split</span><span class="p">(</span><span class="sh">"</span><span class="s"> </span><span class="sh">"</span><span class="p">)[</span><span class="mi">0</span><span class="p">])</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">g</span><span class="p">[</span><span class="sh">"</span><span class="s">processes</span><span class="sh">"</span><span class="p">][</span><span class="sh">"</span><span class="s">process_info</span><span class="sh">"</span><span class="p">]:</span> <span class="n">used</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">p</span><span class="p">[</span><span class="sh">"</span><span class="s">used_memory</span><span class="sh">"</span><span class="p">].</span><span class="nf">split</span><span class="p">(</span><span class="sh">"</span><span class="s"> </span><span class="sh">"</span><span class="p">)[</span><span class="mi">0</span><span class="p">])</span> <span class="nf">print</span><span class="p">(</span> <span class="sh">"</span><span class="s"> {: &lt;13}</span><span class="se">\t</span><span class="s">{: &lt;8}</span><span class="se">\t</span><span class="s">{: &lt;20}</span><span class="se">\t</span><span class="s">{}</span><span class="sh">"</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span> <span class="n">p</span><span class="p">[</span><span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">],</span> <span class="n">p</span><span class="p">[</span><span class="sh">"</span><span class="s">pid</span><span class="sh">"</span><span class="p">],</span> <span class="sh">"</span><span class="s">{: &lt;10} ({:5.2f}%)</span><span class="sh">"</span><span class="p">.</span><span class="nf">format</span><span class="p">(</span> <span class="n">p</span><span class="p">[</span><span class="sh">"</span><span class="s">used_memory</span><span class="sh">"</span><span class="p">],</span> <span class="mi">100</span> <span class="o">*</span> <span class="n">used</span> <span class="o">/</span> <span class="n">total</span> <span class="p">),</span> <span class="n">p</span><span class="p">[</span><span class="sh">"</span><span class="s">process_name</span><span class="sh">"</span><span class="p">],</span> <span class="p">)</span> <span class="p">)</span> <span class="nf">print</span><span class="p">(</span> <span class="sh">"</span><span class="s">---------------------------------------------------------</span><span class="sh">"</span> <span class="p">)</span> <span class="nf">print</span><span class="p">()</span> </pre></td></tr></tbody></table></code></pre></div></div> <p>使用效果:</p> <p><img src="/img/in-post/gpu_user/1.png" alt="" /></p>

2021/11/17
阅读更多

NGINX 配置避免 IP 访问时证书暴露域名

<h2 id="tldr">TL;DR</h2> <p>利用 NGINX 1.19.4 后的新特性 <code class="language-plaintext highlighter-rouge">ssl_reject_handshake on;</code>,将其置于默认访问时配置中,IP 访问时会终止 TLS 握手,也就不会暴露域名了。</p> <h2 id="细说">细说</h2> <p>CDN 是建站时常用的工具,在自己的主机外面套一层 CDN 是常见操作,一般这样认为自己的主机就安全了,有人来攻击也会先到 CDN 服务器,攻击者根本无法获取到自己主机的 IP,但事实真的是这样吗?</p> <p>我们先来看看一般配置后会出现什么问题。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 13 </pre></td><td class="rouge-code"><pre>server { listen 80 default_server; # Redirect all HTTP requests to HTTPS. return 301 https://$host$request_uri; } server { listen 443 default_server; server_name _; include conf.d/ssl.config; return 444; } </pre></td></tr></tbody></table></code></pre></div></div> <p>上面是一个很常用的 NGINX 配置,HTTP 访问全部重定向到 HTTPS 的 443 端口上,没有配置过的域名返回 444 终止连接。</p> <p>好了,现在尝试用 IP 和 HTTPS 访问你的网站,你应该能够看到预想中访问失败、证书无效等连接失败的提示。</p> <p><strong>但是!</strong>注意下浏览器左上角提示的不安全,点开查看证书信息,你就会发现你的域名其实随着证书发送了过来。此时如果你是攻击者,那么其实就可以知道该域名背后的源主机 IP 就是这个。</p> <p><img src="/img/in-post/nginx-ssl/1.png" alt="" /></p> <p>上图即为用 IP 访问后,依旧能看到证书内容。这是因为返回 444 是 HTTP 层面的事情,意味着到达这一步下层的 TLS 握手已经完成。证书不被信任是一回事,但说明已经拿到了服务器的证书。</p> <p>CDN 确实避免了直接 DNS 查询暴露 IP 的问题,但攻击者通过扫描全网 IP,用上述方式依旧可以知道每个 IP 对应的域名是什么,这也是为什么很多站长用了 CDN 后并且反复更换 IP 却依旧被攻击者迅速找到 IP 的原因。</p> <blockquote> <p><a href="https://search.censys.io/">Censys</a> 就一直在干这件事,全网扫描 IP 并找到其对应的域名</p> </blockquote> <h2 id="那该怎么办呢">那该怎么办呢?</h2> <p>问题根源出在 client 在 TLS 握手时发送了 ClientHello 后,NGINX 在 ServerHello 中带着含有域名的默认证书返回了,因为 NGINX 期望可以完成握手,这可能可以算是 NGINX 的一个缺陷。</p> <blockquote> <p>如果你不熟悉 TLS 握手流程,那么可以看看 <a href="/2019/05/07/tls-handshake/">这篇文章</a></p> </blockquote> <h3 id="笨办法">笨办法</h3> <p>既然 NGINX 默认提供了带有域名的证书,那么想不暴露也很简单,提供一个不含有正确域名的证书即可。</p> <p>NGINX 设置中 HTTPS 访问如果没有设置证书,那么就会报错。但反正 IP 访问也不需要提供服务,那么直接自签一个 IP 证书,或者随便一个域名的证书都可。当然,如果能搞定合法的 IP 证书也不是不行。</p> <p>搞定证书后,添加一个配置,让 IP 访问返回错误证书就完事了。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 </pre></td><td class="rouge-code"><pre>server { listen 443 ssl default_server; server_name your_ip; ssl_certificate xxxx.pem; // and more ssl config ... return 444; } </pre></td></tr></tbody></table></code></pre></div></div> <h3 id="好方法">好方法</h3> <p>这种方法还得自己搞个证书,如果服务器多每个都得这么搞也挺麻烦的,好在这个问题 NGINX 这已经有了很完美的解决方案。</p> <p>ClientHello 中是带着 SNI 的,所以其实握手阶段是可以知道访问的域名是否合法的,NGINX 1.19.4 中添加了一个新的配置项 <code class="language-plaintext highlighter-rouge">ssl_reject_handshake</code> 用于拒绝握手,也就不会提供证书。</p> <p>使用方法也很简单,将原本默认配置中的 <code class="language-plaintext highlighter-rouge">return 444</code> 替换成 <code class="language-plaintext highlighter-rouge">ssl_reject_handshake on</code> 即可。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 </pre></td><td class="rouge-code"><pre>server { listen 443 default_server; server_name _; include conf.d/ssl.config; # 不用返回 444 了,直接拒绝握手 ssl_reject_handshake on; # return 444; } </pre></td></tr></tbody></table></code></pre></div></div> <p>配置后,再尝试 IP 访问,会发现浏览器报了 <code class="language-plaintext highlighter-rouge">ERR_SSL_UNRECOGNIZED_NAME_ALERT</code> 的错误,也看不到证书信息,目标达成!</p> <p><img src="/img/in-post/nginx-ssl/2.png" alt="" /></p> <h2 id="其实还没完">其实还没完</h2> <p>上述方法是通过 ClientHello 中的 SNI 确定访问是否合法的,那如果 SNI 就是正确的域名呢?</p> <p>这种场景发生于攻击者已经确定要攻击某个域名,那么他就可以将带着该域名的握手信息遍历所有 IP,握手成功就找到,这样访问其实与正常访问并无区别,唯一解决方法就是白名单只允许 CDN 服务器访问。</p> <blockquote> <p>例如 hosts 直接将硬写 IP,将域名强行指向某个 IP</p> <p>或者用这种方式 <code class="language-plaintext highlighter-rouge">curl https://example.com --resolve 'example.com:443:172.17.54.18'</code></p> </blockquote> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 </pre></td><td class="rouge-code"><pre>location / { allow 172.1.2.0/24; allow 1.2.3.4/32; deny all; } </pre></td></tr></tbody></table></code></pre></div></div> <p>上述 IP 段只能向 CDN 提供商询问,一般文档中都是有相关信息的。</p>

2021/10/4
阅读更多

NGINX 配置 HTTPS 最佳实践

<p>1202 年了,不会还有网站不支持 HTTPS 吧?不过 HTTPS 的配置还是有很多讲究的。本文以 NGINX 的配置为例,嫌麻烦的可以直接跳到 <a href="#完整配置">最后</a> 抄配置。</p> <p>如果你不清楚 HTTPS 与 TLS 的工作原理,可以先阅读 <a href="/2019/05/07/tls-handshake/">这篇文章</a>,可以帮助你理解下述配置。</p> <h2 id="获取证书">获取证书</h2> <p>证书是实现 HTTPS 的基础,现在各个云服务商都提供了免费的证书申请,可以直接去申请。这里我以 acme.sh 为例说明下申请证书时的注意事项。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 </pre></td><td class="rouge-code"><pre>acme.sh --issue -d "*.zinglix.xyz" --keylength ec-256 --ocsp </pre></td></tr></tbody></table></code></pre></div></div> <p>上面是一个简单的用 acme 申请证书的命令,其中关键的是 <code class="language-plaintext highlighter-rouge">keylength</code> 和 <code class="language-plaintext highlighter-rouge">ocsp</code> 两个参数,OCSP 的作用我们 <a href="#ocsp">后面</a> 再说,建议能开启则开启,先来谈谈密钥长度的问题。</p> <p>证书加密的算法分为 RSA 和 ECDSA 两类,这对应到证书也就分为两类。acme 中 <code class="language-plaintext highlighter-rouge">keylength</code> 支持的参数有 <code class="language-plaintext highlighter-rouge">2048</code>, <code class="language-plaintext highlighter-rouge">3072</code>, <code class="language-plaintext highlighter-rouge">4096</code>, <code class="language-plaintext highlighter-rouge">8192</code> 和 <code class="language-plaintext highlighter-rouge">ec-256</code>, <code class="language-plaintext highlighter-rouge">ec-384</code>, <code class="language-plaintext highlighter-rouge">ec-521</code>(参数支持,但暂不支持申请)。</p> <p><code class="language-plaintext highlighter-rouge">ec-</code> 开头的对应着 ECDSA 证书,其他的为 RSA 证书,长度越长安全性也就更高,但对性能的消耗也就越高。RSA 有更好的兼容性,ECDSA 可以提供更好的前向安全,具体差异可以看 <a href="/2021/04/05/tls-13/#rsa-与-dh">这里</a>。</p> <p>根据 <a href="https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices#31-avoid-too-much-security">SSL Labs 的推荐</a>,长于 2048 的 RSA 密钥和 256 bits 的 ECDSA 密钥对于 CPU 性能是一种浪费,从中获得安全性的提升有限,导致过度加密。</p> <p><strong>因此推荐密钥长度为 <code class="language-plaintext highlighter-rouge">2048</code> 与 <code class="language-plaintext highlighter-rouge">ec-256</code>!</strong></p> <p>那么两种类型证书选择哪一个呢?那当然是全都要啦~ NGINX 支持同时使用两个证书,只需要都写上就行了</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 </pre></td><td class="rouge-code"><pre># RSA 证书 ssl_certificate /cert/*.zinglix.xyz/fullchain.cer; ssl_certificate_key /cert/*.zinglix.xyz/*.zinglix.xyz.key; # ECDSA 证书 ssl_certificate /cert/*.zinglix.xyz_ecc/fullchain.cer; ssl_certificate_key /cert/*.zinglix.xyz_ecc/*.zinglix.xyz.key; </pre></td></tr></tbody></table></code></pre></div></div> <p>这里 <code class="language-plaintext highlighter-rouge">ssl_certificate</code> 最好是使用完整的证书链,如果没有提供必要的中间证书可能会导致证书链不可信。</p> <h2 id="http2-与会话恢复">HTTP/2 与会话恢复</h2> <p>HTTP/2 可以有效提升对网络的利用效率,会话恢复可以复用曾经协商过的数据,两者都可以帮助减少 RTT,所以建议开启,可以有效减少建立连接时的耗时。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 </pre></td><td class="rouge-code"><pre>listen 443 ssl http2; ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; ssl_session_tickets off; </pre></td></tr></tbody></table></code></pre></div></div> <h2 id="加密协议与套件">加密协议与套件</h2> <p>SSL 已经是不安全的了,绝不要使用。TLSv1.0 与 TLSv1.1 虽然没有被证明不安全,但作为老旧的协议即将过时,除非你的客户真的需要,也不要开启。</p> <p>TLSv1.2 可以说是目前被最广泛使用的协议,应当被开启。<a href="/2021/04/05/tls-13/">TLSv1.3</a> 作为最新的协议,在性能和安全性上都有提升,支持的话也应当开启。</p> <blockquote> <p>如果为了极致的安全,只开启 TLSv1.3 也是没有问题的,现代的浏览器都已经支持 TLSv1.3,只要你相信你的客户不会使用略微老旧的软件</p> </blockquote> <p>至于加密套件,<code class="language-plaintext highlighter-rouge">RC4</code>、<code class="language-plaintext highlighter-rouge">DES</code> 等等都不安全,但说那么多套件头也晕了,下面已经整理了一份支持绝大多数客户端且安全的配置</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 </pre></td><td class="rouge-code"><pre>ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; ssl_prefer_server_ciphers on; </pre></td></tr></tbody></table></code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">ssl_prefer_server_ciphers</code> 用于指定服务器是否有推荐的套件,为了能够根据服务器配置用上更安全的套件,防止 BEAST 攻击,建议开启。</p> <h2 id="hsts">HSTS</h2> <p>HSTS (HTTP Strict Transport Security) 能够告诉浏览器,该网站只应该通过 HTTPS 访问,避免使用 HTTP。开启方式如下,只需要添加一个 HTTP 头</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 </pre></td><td class="rouge-code"><pre>add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload' always; </pre></td></tr></tbody></table></code></pre></div></div> <p>其中三个参数</p> <ul> <li><code class="language-plaintext highlighter-rouge">max-age=&lt;expire-time&gt;</code>:指明 HSTS 的有效期,最佳实践是一年时间即 <code class="language-plaintext highlighter-rouge">31536000</code>。注意,开启后如果关闭 HTTPS 将在有效期内导致网站无法访问。</li> <li><code class="language-plaintext highlighter-rouge">includeSubDomains</code>:是一个可选参数,指明是否同时适用于子域名。</li> <li><code class="language-plaintext highlighter-rouge">preload</code>:可选参数,指明是否为预加载。</li> </ul> <blockquote> <p>HSTS 虽然可以实现强制浏览器使用 HTTPS,但是第一次访问时依旧不知道目标网站是否采用了 HSTS。</p> <p>为了解决这一问题,Google 维护了一个名单,里面是采用了 HSTS 的网站列表,名单会随着浏览器分发。这样浏览器第一次访问时,会先查看网站是否位于该名单里,从而决定是否采用 HTTPS。这一机制即为 <strong>预加载机制</strong>。</p> <p>可以在 <a href="https://hstspreload.org/">该网站</a> 上提交申请,注意必须加上与上述配置相同或更严格的配置,同时重定向所有 HTTP 请求。</p> </blockquote> <h2 id="dhe-参数">DHE 参数</h2> <p>密钥交换时常采用 Diffie-Hellman 密钥交换,低强度(768 及以下)的参数容易被破解,以及一些常见的 1024 位参数。</p> <p>出于性能与安全性的考虑,2048 位即可,如果你不担心性能可以选择更高的。</p> <p>可以用 <code class="language-plaintext highlighter-rouge">openssl dhparam -out dhparams.pem 2048</code> 生成一个(通常很慢)或者获取一个现成的,例如 <a href="https://ssl-config.mozilla.org/ffdhe2048.txt">2048位</a> 和 <a href="https://ssl-config.mozilla.org/ffdhe4096.txt">4096位</a>。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 </pre></td><td class="rouge-code"><pre>ssl_dhparam /path/to/dhparam.pem; </pre></td></tr></tbody></table></code></pre></div></div> <h2 id="ocsp">OCSP</h2> <p>OCSP 是一种从证书颁发者处验证证书是否被撤回的机制,可以让浏览器验证证书有效性。</p> <p>最佳实践是证书中强制要求 OCSP,但客户到颁发者处的连接质量可能并不好,例如国内访问 Let’s Encrypt,这会导致网站访问速度下降。</p> <p>OCSP Stapling 是一种让服务器在握手过程中同时传递 OCSP 相应的技术,向颁发者验证的过程由服务器代劳,避免用户直接去访问。开启也很简单,如下</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 </pre></td><td class="rouge-code"><pre>ssl_stapling on; ssl_stapling_verify on; </pre></td></tr></tbody></table></code></pre></div></div> <h2 id="0-rtt">0-RTT</h2> <p><a href="/2021/04/05/tls-13/#0-rtt">0-RTT</a> 是 TLSv1.3 很棒的一个特性,有效消除握手时间,但会导致重放攻击,且放弃了前向安全性。</p> <p>在足够了解后果的前提下可以开启,但如果不确定那就算了,建议是 GET 等无副作用的操作可以开启。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 </pre></td><td class="rouge-code"><pre>ssl_early_data on; </pre></td></tr></tbody></table></code></pre></div></div> <h2 id="其他一些细节">其他一些细节</h2> <ul> <li>网站所有资源开启 HTTPS,例如一些公用 CSS、JS</li> <li><a href="/2021/10/04/nginx-ssl-reject-handshake/">非法域名(尤其是 IP 访问)禁止握手</a>,避免暴露证书</li> <li>重定向所有 HTTP 请求至 HTTPS</li> </ul> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 </pre></td><td class="rouge-code"><pre>server { listen 80 default_server; listen [::]:80 default_server; return 301 https://$host$request_uri; } </pre></td></tr></tbody></table></code></pre></div></div> <h2 id="完整配置">完整配置</h2> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 </pre></td><td class="rouge-code"><pre># HTTP/2 listen 443 ssl http2; server_name your_domain; # RSA 证书(推荐2048位) ssl_certificate /cert/*.zinglix.xyz/fullchain.cer; ssl_certificate_key /cert/*.zinglix.xyz/*.zinglix.xyz.key; # ECDSA 证书(推荐256位) ssl_certificate /cert/*.zinglix.xyz_ecc/fullchain.cer; ssl_certificate_key /cert/*.zinglix.xyz_ecc/*.zinglix.xyz.key; # 会话恢复 ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; ssl_session_tickets off; # 加密协议与套件 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; ssl_prefer_server_ciphers on; # HSTS add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload' always; # DHE 参数(推荐2048位) ssl_dhparam /cert/ffdhe2048.txt; # OCSP Stapling ssl_stapling on; ssl_stapling_verify on; </pre></td></tr></tbody></table></code></pre></div></div> <h2 id="验证">验证</h2> <p>配置后,可以验证自己的配置是否正确,下面推荐两个网站</p> <ul> <li><a href="https://www.ssllabs.com/ssltest/">SSL Labs</a>:除了配置外,可以检查与各类客户端的兼容性</li> <li><a href="https://www.immuniweb.com/ssl/">ImmuniWeb</a>:有更完整的配置测试</li> </ul> <p>上述配置后应该都能取得 A+ 的成绩。</p> <p><img src="/img/in-post/nginx-ssl/3.png" alt="" /></p> <p><img src="/img/in-post/nginx-ssl/4.png" alt="" /></p> <p>最后提一嘴关于 SSL Labs 中有两项 90 分,虽然没有必要,但可以继续冲击 100。</p> <p>关于 Key Exchange,其要求 DH 参数和证书密钥长度均大于等于 4096,替换这两个文件即可。当然之前提到过这会导致过度加密,带来的性能损失并不能带来足够的安全性提升。</p> <p>而 Cipher Strength,其要求所有加密套件均大于等于 256 bits,而这里存在一个 <a href="https://github.com/ssllabs/ssllabs-scan/issues/636">bug</a> 是 TLSv1.3 有一个算法为 <code class="language-plaintext highlighter-rouge">TLS13-AES-128-GCM-SHA256</code>,虽然 128 位但其实配上 TLSv1.3 足够安全,但这导致了开启 TLSv1.3 就无法得到 100 分。</p> <p>不过除了关闭 TLSv1.3 之外,还是有解决方法的,那就是移除 <code class="language-plaintext highlighter-rouge">TLS13-AES-128-GCM-SHA256</code> 套件,NGINX 1.19.4 开始支持调整 TLSv1.3 的加密套件,只需加上下述配置,但会导致 0-RTT 失效</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 </pre></td><td class="rouge-code"><pre>ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256; ssl_ecdh_curve secp384r1; </pre></td></tr></tbody></table></code></pre></div></div>

2021/10/3
阅读更多

「论文笔记」A Unified Generative Framework for Various NER Subtasks

<p>NER 任务分为三类,目前来说,各个子任务目前都有方法可以一定程度上解决,但缺少一个方法可以同时解决三个任务。下图分别介绍了三个子任务的常见解决方案</p> <ul> <li>Flat NER(扁平实体抽取):通常采用序列标注的方法</li> <li>Nested NER(嵌套实体抽取):将输入文本采用类似 n-gram 的方法进行拆分,拆成一个个小的 span 后进行分类</li> <li>Discontinous NER(非连续实体抽取):用一个堆栈,为每个 token 选择移入移出规约等操作,类似于编译器生成 AST 的过程,<a href="/2020/07/29/papernote-acl-2020-520/">详见另一篇论文</a></li> </ul> <p><img src="/img/papernote/arxiv/2106.01223/1.png" alt="" /></p> <p>这篇文章提出使用 seq2seq 的框架以生成的方式来同时完成三个任务,也就是标题中的那个可用于各种 NER 子任务的通用框架。输出形式则为上图中的 (d),通过生成的方式输出实体的内容,最后以一个指示实体类型的特殊标签作为该实体的结束,并以同样的方式生成下一个实体。</p> <h2 id="模型结构">模型结构</h2> <p>模型整体结构较为简单,为了让模型更好的完成生成任务,文中采用预训练过的 seq2seq 模型 BART 作为基础。输入为一段文本(首尾加上了特殊 token),输出时采用指针网络的方式,输出在输入中的位置。</p> <p><img src="/img/papernote/arxiv/2106.01223/2.png" alt="" /></p> <p>具体来说,模型沿用了 BART 的 encoder-decoder 的架构(下半部分),主要工作在如何生成字符(上半部分)。</p> <p>模型通过 encoder 和 decoder 后,分别拿到了两个隐状态 \(H^e\) 和 \(h_t^d\)。</p> <blockquote> <p>实际中,index 并不能直接输入 decoder,所以解码器中还进行了一步 index2token,以 token 的方式输入</p> </blockquote> <p>之后作者将 \(H^e\) 通过一层 MLP 后与输入的 Token Embedding 加权相加(公式 7)得到 \(\overline{H}^e\)。</p> <p><img src="/img/papernote/arxiv/2106.01223/3.png" alt="" /></p> <p>然后模型会计算一个 Pointer Distribution 和 Tag Distribution,即文中每个位置(指针)的概率分布和标签的概率分布,两者合在一起就是图中的 Final Prediction Prob.,从中选择概率最高的也就是这一步输出的结果。</p> <p>实际上,就是将 \(\overline{H}^e\) 和 tag 的 embedding 后的输出 \(G^d\) 分别与 decoder 的输出 \(h_t^d\) 相乘后拼接(公式 9),通过 softmax 找到概率最大的,作为输出。</p> <p>然后就以自回归的方式不断重复上述步骤,依次输出下一个 token,从而得到最后的结果。</p> <h2 id="结果">结果</h2> <p>实验结果基本上都处于 SOTA 的水平,不过文中有许多分析的部分,例如实体表示方式之类的。</p> <p>实验表明长度更短、更类似 BPE 序列的实体会有更好的效果。此外,文中出现越靠后的实体,在 Flat NER 和 Discontinous NER 上召回率都会随之增高,而嵌套实体并没有,作者认为是因为实体间有关联,导致错误传播。</p> <p>文章最大的贡献点在于提出了一种方式,能够用生成的方式完成各类 NER 任务。</p>

2021/6/16
阅读更多

Linux 更换时区

<p><code class="language-plaintext highlighter-rouge">timedatectl</code> 指令可以查看当前的时间信息,如下</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1 2 3 4 5 6 7 </pre></td><td class="rouge-code"><pre> Local time: Sun 2021-05-09 23:00:17 CST Universal time: Sun 2021-05-09 15:00:17 UTC RTC time: Sun 2021-05-09 15:00:17 Time zone: Asia/Shanghai (CST, +0800) Network time on: yes NTP synchronized: yes RTC in local TZ: no </pre></td></tr></tbody></table></code></pre></div></div> <p>如果时区不对,可以用 <code class="language-plaintext highlighter-rouge">sudo timedatectl set-timezone Asia/Shanghai</code> 这条指令切换到国内的时区,最后一个参数就是你要切换的时区,这条指令需要 <code class="language-plaintext highlighter-rouge">sudo</code> 权限。</p> <p>具体的时区列表可以用 <code class="language-plaintext highlighter-rouge">timedatectl list-timezones</code> 获得或者去 <code class="language-plaintext highlighter-rouge">/usr/share/zoneinfo</code> 路径下查看。</p>

2021/5/9
阅读更多