<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Cloverstd's blog]]></title><description><![CDATA[Just do it.]]></description><link>https://hui.lu/</link><image><url>https://hui.lu/favicon.png</url><title>Cloverstd&apos;s blog</title><link>https://hui.lu/</link></image><generator>Ghost 1.16</generator><lastBuildDate>Mon, 20 Apr 2020 08:53:11 GMT</lastBuildDate><atom:link href="https://hui.lu/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[设计一个友好的网关]]></title><description><![CDATA[<div class="kg-card-markdown"><h1 id="">传统应用</h1>
<p>一般情况下，你要对外提供 HTTP 接口，首先你得开发一个 Web Server，然后部署到服务器上，直接对外提供 HTTP 接口。<br>
后来，你发现流量高了，你又没时间优化，只能堆机器了，于是乎你横向扩展了一堆 Web Server，这个时候你就需要一个 Proxy Server 做反向代理（Reverse Proxy）来让你的 Web Server 统一对外提供 HTTP 接口，并且尽可能的保证这一堆 Web Server 承载的流量是一样一样的。<br>
<img src="https://hui.lu/content/images/2018/05/reverse-proxy.png" alt="reverse-proxy"></p>
<h1 id="">微服务</h1>
<p>随着时间的推移，你的 Web Server 不断壮大，承载的功能也越来越多了，这个时候微服务（Microservices）出现在你眼中，确认过眼神，你选择拆分你的 Web Server 的功能模块。</p></div>]]></description><link>https://hui.lu/design-a-friendly-api-gateway/</link><guid isPermaLink="false">5aef029b7be8780001ae2784</guid><category><![CDATA[Gateway]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Sat, 12 May 2018 07:27:14 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h1 id="">传统应用</h1>
<p>一般情况下，你要对外提供 HTTP 接口，首先你得开发一个 Web Server，然后部署到服务器上，直接对外提供 HTTP 接口。<br>
后来，你发现流量高了，你又没时间优化，只能堆机器了，于是乎你横向扩展了一堆 Web Server，这个时候你就需要一个 Proxy Server 做反向代理（Reverse Proxy）来让你的 Web Server 统一对外提供 HTTP 接口，并且尽可能的保证这一堆 Web Server 承载的流量是一样一样的。<br>
<img src="https://hui.lu/content/images/2018/05/reverse-proxy.png" alt="reverse-proxy"></p>
<h1 id="">微服务</h1>
<p>随着时间的推移，你的 Web Server 不断壮大，承载的功能也越来越多了，这个时候微服务（Microservices）出现在你眼中，确认过眼神，你选择拆分你的 Web Server 的功能模块。为了更好的性能，你决定不再使用 HTTP 作为内部通信的方式了，RPC 有幸被你选中了。于是你重新设计了整个架构图。</p>
<p><img src="https://hui.lu/content/images/2018/05/rpc-proxy-1.png" alt="rpc-proxy-1"></p>
<p>那么问题来了，你的 RPC Server 提供的可能不是传统的 HTTP 协议了，这样搞，你就得找一个支持 RPC 反代的 Proxy 了，幸运的是 Nginx 支持了 gRPC 的反向代理，但是浏览器不一定能正常解析你的 RPC 的报文，RPC 一般情况下需要特定的 Client 去支持。所以这个时候，你就需要在 Proxy 与 RPC Server 之间加一个网关（Gateway）去做协议转换，将 RPC 的与 HTTP 之间做转换，让 Prxoy 之前的服务不需要做任何改变，就可以支持目前的架构。</p>
<p><img src="https://hui.lu/content/images/2018/05/gateway-1.png" alt="gateway-1"></p>
<h1 id="gateway">Gateway</h1>
<h2 id="">协议转换</h2>
<h3 id="">参数重构</h3>
<p>Gateway 对外提供传统的 HTTP 接口，对内，可以作为 RPC Client 去调用对应的 RPC Server。并且 Gateway 还要能够自动发现你新加入的 RPC Server，这样在紧急扩容，或者新接入了 RPC Server，可以不间断，自动的让 RPC Server 暴露出去。</p>
<p>首先来说一下协议转换，一个 HTTP 请求进来</p>
<pre><code>POST /posts?namespace=huilu HTTP/1.1
Host: hui.lu
User-Agent: curl/7.54.0
Accept: */*
X-Token: f136803ab9c241079ba0cc1b5d02ee77
Cookie: UserID=1
Content-Type: application/json; charset=utf-8
Content-Length: 14

{&quot;foo&quot;: &quot;bar&quot;}
</code></pre>
<p>上面报文中的任何信息，都可以是一个参数，并且可能是后端 RPC Server 需要的，但是 RPC Server 接受的参数形式又不是上述 HTTP 报文中的格式，所以这里就需要 Gateway 来做一下转换。</p>
<p>我们这里假设后端是 gRPC，那么它的这个 POST 请求对应的 protobuf 如下</p>
<pre><code class="language-protobuf">service Project {
  rpc CreateProject (PostsRequest) returns (PostsResponse) {}
}

message PostsRequest {
  string namespace = 1;
  int64 user_id = 2;
  map&lt;string, string&gt; params = 3;
  string token = 3;
}

message PostsResponse {
  int64 project_id = 1;
}
</code></pre>
<p>从上面来看，HTTP 请求的参数与 gRPC 参数的对应关系如下</p>
<table>
<thead>
<tr>
<th>HTTP Params</th>
<th>gRPC Params</th>
</tr>
</thead>
<tbody>
<tr>
<td>HTTP.Header.Cookie.UserID</td>
<td>gRPC.PostsRequest.user_id</td>
</tr>
<tr>
<td>HTTP.Query.namespace</td>
<td>gRPC.PostsRequest.namespace</td>
</tr>
<tr>
<td>HTTP.Header.X-Token</td>
<td>gRPC.PostsRequest.token</td>
</tr>
<tr>
<td>HTTP.Body</td>
<td>gRPC.PostsRequest.params</td>
</tr>
</tbody>
</table>
<p>上面就能很清晰的看出来 HTTP 请求的参数与 gRPC 调用的参数映射关系，这里就需要设计一个好用的配置语法，来描述参数的映射关系。在这里，我们选择 YAML 来作为配置的语法</p>
<pre><code class="language-yaml">url: /posts
method: post
request:
    gRPC.PostsRequest.user_id: HTTP.Header.Cookie.UserID
    gRPC.PostsRequest.namespace: HTTP.Query.namespace
    gRPC.PostsRequest.token: HTTP.Header.X-Token
    gRPC.PostsRequest.params: HTTP.Body
</code></pre>
<p>上面的配置，比上面的表格就强多了，人肉眼可见，理解起来也方便多了， 而且给代码来识别解析，也是很方便。但是上面有一些冗余的地方，比如 <code>request</code> 里面的 key，都是以 <code>gRPC.PostsRequest</code> 开始，后面 value 也都是以 <code>HTTP</code> 开始，在这个特定的场景下，完全可以省略掉</p>
<pre><code class="language-yaml">url: /posts
method: post
request:
    user_id: Header.Cookie.UserID
    namespace: Query.namespace
    token: Header.X-Token
    params: Body
</code></pre>
<p>可是，假设 gRPC 请求参数中多了一个，需要一个常量 <code>type</code>，并且值恒定为 Body，并且是特定值，不想要 HTTP 请求去改变它，那么上述的配置文件就变成了这样的</p>
<pre><code class="language-yaml">url: /posts
method: post
request:
    user_id: Header.Cookie.UserID
    namespace: Query.namespace
    token: Header.X-Token
    params: Body
    type: Body
</code></pre>
<p>可是，上面的配置文件就多出了两个 <code>Body</code> 的 value 了，这样会有歧义，你并不能明确的说明，<code>Body</code> 到底是 HTTP 请求的 body 还是一个字符串。首先你想到的是，程序在解析 gRPC 的 protobuf 时，分析出来 <code>gRPC.PostsRequest. type</code> 是个 <code>string</code> 类型，所以请求的 <code>type</code> 也必须是个 <code>string</code> 类型，因为 <code>HTTP.Body</code> 在这个场景下，肯定不是 <code>string</code>，所以程序可以自动处理掉这种情景，不会将 <code>HTTP.Body</code> 当做 <code>type</code> 的值去解析，但是当 <code>HTTP.Body</code> 也是个 <code>string</code> 咋整。所以，我们这里将 HTTP 请求的参数加一个特定的，短小精悍的标识符 <code>$</code></p>
<pre><code class="language-yaml">url: /posts
method: post
request:
    user_id: $.Header.Cookie.UserID
    namespace: $.Query.namespace
    token: $.Header.X-Token
    params: $.Body
    type: Body
</code></pre>
<p>上面是不是就明确多了，我们假设 <code>$.</code> 开头的表达式都是我们的参数表达式，并且会从对应的 HTTP 报文中找到对应的值，在调用 gRPC 时，传给后端。<br>
请求说完了，再来说一下响应，上述的 gRPC 返回的是一个 <code>PostsResponse</code> 的对象，我们假设原来的 HTTP 接口返回的也是一样的结构，但是参数名不一样，HTTP 响应的数据是</p>
<pre><code class="language-json">{
  &quot;id&quot;: 1,
  &quot;type&quot;: &quot;Body&quot;
}
</code></pre>
<p>那么在 gRPC 的响应与 HTTP 的响应数据之间也要做一次参数的转换，还是遵循上面的配置规则</p>
<pre><code class="language-yaml">url: /posts
method: post
request:
    user_id: $.Header.Cookie.UserID
    namespace: $.Query.namespace
    token: $.Header.X-Token
    params: $.Body
    type: Body
response:
    id: gRPC.PostsResponse.project_id
    type: Body
</code></pre>
<p>这里，<code>gRPC.PostsResponse.project_id</code> 在这里还是有点多余，我们也可以将其替换成 <code>$.rpc.project_id</code>，我们在这里，规定 <code>$</code> 的生命周期是从请求进入到返回响应都是存在的，来作为一个 <code>rootScope</code>，里面保存了请求和响应的各种参数，那么就简化成了下面的形式</p>
<pre><code class="language-yaml">request:
    user_id: $.Header.Cookie.UserID
    namespace: $.Query.namespace
    token: $.Header.X-Token
    params: $.Body
    type: Body
response:
    id: $.rpc.project_id
    type: Body
</code></pre>
<p>Gateway 就可以利用上面的配置，转换 HTTP 接口与 gRPC 调用的参数，我们在这里，将上述的参数转换，叫做「参数重构」。</p>
<h3 id="">插件机制</h3>
<p>当你做完上面的事情后，你把你的 Gateway 卖给了前端，RPC Server 的业务方，这个时候，他们就来说了，这里的很多请求，都需要鉴定 token 是否有效，如果每次参数都要加上 <code>token</code> 多累啊，后端也只需要一个 <code>user_id</code> 而已，于是你灵机一动，既然大家都有这种重复逻辑的代码，那我就把它抽出来，在 Gateway 这里做这件事，大家只有在配置里加一个选项，就可以了。<br>
于是，你就加了一个 <code>auth</code> 的配置，当 <code>auth</code> 为 <code>true</code> 的时候，你就调用一个专门用来做认证的 RPC Server，认证后，就会返回给你用户信息，你就搞出了一个 <code>$.auth.user_id</code> 的配置，只要大家在配置里写了这个，就能获取到 <code>user_id</code></p>
<pre><code class="language-yaml">url: /posts
method: post
auth: true
request:
    user_id: $.auth.user_id
    namespace: $.Query.namespace
    params: $.Body
    type: Body
response:
    id: $.rpc.project_id
    type: Body
</code></pre>
<p>这个时候，大家就都拍手叫好，不停的说「666」，可是这个时候，又有个人跳出来说，他不止需要 <code>user_id</code>，还需要 <code>mobile</code> 等信息，然后你就又在 <code>$.auth</code> 上加了一个 <code>$.auth.mobile</code>，这样会又获取到了 <code>mobile</code> 了。当你正在准备看看监控喝茶的时候，公司推出了认证服务二代，这个时候，就不是验证 <code>$.Header.Cookie.Token</code> 了，你就想，不就是再加个 <code>auth2</code> 吗？</p>
<pre><code class="language-yaml">url: /posts
method: post
auth2: true
request:
    user_id: $.auth.user_id
    namespace: $.Query.namespace
    params: $.Body
    type: Body
response:
    id: $.rpc.project_id
    type: Body
</code></pre>
<p>但是，当你写出这种配置的时候，你发现，这看起来太 XX 丑了，而且后面加了认证三代、四代咋整，总不能直接加载配置里面吧，于是你也开始拆分 Gateway 的逻辑，将参数重构作为核心功能，将认证相关的拆分成一个个插件，于是配置文件就变成了下面这样的了</p>
<pre><code class="language-yaml">url: /posts
method: post
plugins:
    - auth
request:
    user_id: $.auth.user_id
    namespace: $.Query.namespace
    params: $.Body
    type: Body
response:
    id: $.rpc.project_id
    type: Body
</code></pre>
<p>当你正在为自己的设计沾沾自喜的时候，有人来告诉你，他需要用到两种认证方式，这个时候，你就发现了自己埋下的坑了，你将两种认证服务的用户信息都放在了 <code>$.auth</code> 上了，gg，没法区分了，你就想，要不加一个 <code>$.auth2</code> 来保存认证二代的信息？当你发现你的脑袋里出现了这种念头时，你自己都觉得可怕，于是你就想，为啥不能在插件使用的时候，指定用户信息保存的地方呢？</p>
<pre><code class="language-yaml">url: /posts
method: post
plugins:
    - auth --output $.auth
    - auth-v2 --output $.auth2
request:
    user_id: $.auth.user_id
    namespace: $.Query.namespace
    params: $.Body
    type: Body
response:
    id: $.rpc.project_id
    type: Body
</code></pre>
<p>于是，你又一次满足了客户的要求，客户连连称赞你。但是有个人过来告诉你，由于历史原因，认证用的 <code>token</code> 没有放在 <code>HTTP.Header.X-Token</code>，而是放在了 <code>HTTP.Header.Cookie.TOKEN</code>，你当成懵逼，但是你突然想到你刚加了一个 <code>--output</code>，那为啥不能加一个 <code>--input</code> 来指定 <code>token</code> 的地方了？</p>
<pre><code class="language-yaml">url: /posts
method: post
plugins:
    - auth --output $.auth --input $.Cookie.TOKEN
    - auth-v2 --output $.auth2
request:
    user_id: $.auth.user_id
    namespace: $.Query.namespace
    params: $.Body
    type: Body
response:
    id: $.rpc.project_id
    type: Body
</code></pre>
<p>你不止改进了插件，顺带给常用的 <code>Cookie</code> 做了一个别名，直接可以用过 <code>$.Cookie</code> 来取 <code>Cookie</code> 的值。</p>
<h3 id="">执行时机</h3>
<p>后续你又加了一系列的插件，比如字符串替换 <code>string-replace</code>，可以设置某个值的 <code>set</code> 插件。但是这些插件，你都是作用在请求 <code>request</code> 解析之前的，你突然想到，要是有人要求对 <code>response</code> 的某个字段做字符串替换，那不就 gg 了吗，于是你决定将这种问题扼杀在摇篮中。<br>
你仔细研究了下上面的配置，和协议转换的流程，你发现在这里可以划分为多个阶段</p>
<ul>
<li>请求参数重构前</li>
<li>请求参数解析后，RPC 调用前</li>
<li>RPC 调用后，响应参数重构前</li>
<li>响应参数重构后，返回响应前</li>
</ul>
<p>上面描述废话有点多，我们这里简化下</p>
<ul>
<li>请求参数重构前（<code>beforeRequestParamsRefactor</code>）</li>
<li>RPC 调用前（<code>beforeRPCCall</code>）</li>
<li>响应参数重构前（<code>beforeRPCResponseParamsRefactor</code>）</li>
<li>返回响应前（<code>beforeRPCResponse</code>）</li>
</ul>
<p>在这里我们划分为四个阶段，我们用 <code>set</code> 作为示例</p>
<pre><code class="language-yaml">url: /posts
method: post
plugins:
    - auth --output $.auth --input $.Cookie.TOKEN
    - auth-v2 --output $.auth2
    - set $.request.type Body --timing beforeRPCCall
    - set $.response.type Body --timing beforeRPCResponse
request:
    user_id: $.auth.user_id
    namespace: $.Query.namespace
    params: $.Body
response:
    id: $.rpc.project_id
</code></pre>
<p>我们规定，所有的插件默认执行时机都是 <code>beforeRequestParamsRefactor</code>，所以上面我们用 <code>--timing</code> 去改变了插件的执行时机。利用 <code>set</code> 插件，将常量 <code>Body</code> 直接强制覆盖掉请求和响应中的 <code>type</code>。</p>
<h3 id="">内部路由</h3>
<p>但是上述的配置，只能调用一个 RPC Server，有一个接口，他需要同时调用两个 RPC Server，并且拆分请求参数和合并响应结果，于是你就想，为什么不能有一个没有 <code>url</code> 的配置</p>
<pre><code>name: get_something
request:
    id: $.request.project_id
response:
    data: $.rpc.data
</code></pre>
<p>于是就有了上面的命名路由，这里我们叫它内部路由，因为它没法被外部直接调用，那这个应该怎么与上述的结合在一起呢？我们将内部路由定义成 <code>$.route.get_something</code></p>
<pre><code class="language-yaml">url: /posts
method: post
plugins:
    - auth --output $.auth --input $.Cookie.TOKEN
    - auth-v2 --output $.auth2
    - set $.request.type Body --timing beforeRPCCall
    - set $.response.type Body --timing beforeRPCResponse
request:
    user_id: $.auth.user_id
    namespace: $.Query.namespace
    params: $.Body
response:
    id: $.rpc.project_id
    something: $.route.get_something
</code></pre>
<h2 id="">服务发现</h2>
<p>上述编故事，忘了插入 RPC 调用了 :D，其实也是没地方能植入 RPC 调用的地方，于是就将这个地方抽出来，单独讲。<br>
假设一个 RPC Server，你有 100 个实例来提供服务</p>
<ul>
<li>192.168.1.2:8100</li>
<li>192.168.1.2:8102</li>
<li>192.168.1.3:8104</li>
<li>...</li>
</ul>
<p>这么多机器，你怎么知道机器在哪里呢，就几台机器，你可以手动写到配置里</p>
<pre><code class="language-yaml">url: /posts
method: post
request:
    user_id: $.auth.user_id
    namespace: $.Query.namespace
    params: $.Body
response:
    id: $.rpc.project_id
rpc:
    - host: 192.168.1.2
      port: 8100
    - host: 192.168.1.2
      port: 8102
    - host: 192.168.1.3
      port: 8104
</code></pre>
<p>可是，现在 Docker 盛行，RPC Server 的 IP 是会不停的改变的，所以怎么能保证实例的 IP 能够每次自动的让 Gateway 知道呢？这个时候，就需要服务发现了，首先每个 RPC Server 在启动的时候，需要将自己的 IP:PORT 告诉到一个地方，注册中心，然后也要告诉注册中心，它是谁，也就是它的名字，这样，Gateway 就可以通过 RPC Server 的名字，到注册中心从，查到它的地址了，并且在地址改变了之后，注册中心也会通过各种方式通知 Gateway，去新的地方调它。</p>
<pre><code class="language-yaml">url: /posts
method: post
request:
    user_id: $.auth.user_id
    namespace: $.Query.namespace
    params: $.Body
response:
    id: $.rpc.project_id
rpc:
    name: posts.server
    timeout: 800ms
</code></pre>
<p>所以，上面的配置，就简化了很多了，这里就不去讲注册中心的实现了，这个不在一个「友好」的 Gateway 的范围之类，在 Gateway 这里，只要能够根据服务的名字发现服务的地址就行了。</p>
<h2 id="">热更新</h2>
<p>上面只是描述了配置文件的格式，但是配置文件放哪里呢？<br>
配置文件的储存必须要满足如下要求</p>
<ul>
<li>支持热更新</li>
<li>能够回滚到特定的版本</li>
<li>一条 route 的配置错误，不能影响到全局的配置</li>
</ul>
<p>首先，要支持特定的版本回滚，你肯定首先想到了 git，是的，配置文件，完全可以放在 git 作为持久化，我们在这里将一些相同业务的路由作为一个项目，所以一个项目可以对应一个 gitlab 的项目，没错，我们选择 gitlab 来储存配置文件。</p>
<pre><code class="language-bash">.
├── config.yaml
└── routes.yaml

0 directories, 2 files
</code></pre>
<p>上面就是一个 Gateway 配置的配置（怎么这么绕口），其中 <code>routes.yaml</code> 用来储存上述的各种路由配置，<code>config.yaml</code> 可以用来存一些全局性的配置，比如全局的 <code>timeout</code> 之类的。<br>
然后可以利用 gitlab 的 webhook，当有 push 事件的时候，可以推送到某个地方通知 Gateway 来 gitlab 拉取配置文件，并且更新它。这样就可以实现运行时的 Gateway 配置热更新，而且还可以利用 git 的版本特性，实现回滚版本。</p>
<h1 id="">总结</h1>
<p>上述还有很多地方待完善，比如 <code>$</code> 的各个变量的作用域，在内部路由时 <code>$.request</code> 和 <code>$.rpc</code> 又表示什么呢，这些都值得思考和仔细设计。</p>
</div>]]></content:encoded></item><item><title><![CDATA[Docker Registry Storage]]></title><description><![CDATA[Docker Registry Custom Storage（Docker 镜像仓库后端自定义储存），使用七牛作为 docker 镜像仓库的后端储存示例]]></description><link>https://hui.lu/docker-registry-storage/</link><guid isPermaLink="false">59f426890ee507000193927b</guid><category><![CDATA[Docker Registry]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Golang]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Sat, 17 Jun 2017 08:12:14 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p><a href="https://hui.lu/docker-registry-v2-auth-server-with-python/">上篇文章</a>写了 Docker Registry Auth 的流程，这里研究一下 Docker Registry Storage。<br>
Docker 镜像仓库的支持支持多种储存类型，主要分为两大类</p>
<ul>
<li>本地储存</li>
<li>远程储存</li>
</ul>
<p>本地储存为 filesystem 和 inmemory，然后远程储存就主要是一些云计算厂商的储存业务了。无论那种储存都是基于 <a href="https://github.com/docker/distribution/blob/master/registry/storage/driver/storagedriver.go">StorageDriver</a> 这个接口的实现。</p>
<h2 id="">储存结构</h2>
<pre><code class="language-shell">.
└── docker
    └── registry
        └── v2
            ├── blobs
            │   └── sha256
            │       ├── 07
            │       │   └── 0766572b4bacfaee9a8eb6bae79e6f6dbcdfac0805c7c6ec8b6c2c0ef097317a
            │       ├── 9a
            │       │   └── 9a597e826a59709a4af34279f496c323d496a79e4c998ee5249a738e391192bb
            │       └── 9c
            │           └── 9ca846b27f6e92f0739af5bba5509357b52be0ce0dd02d216f4dccdacd695a8a
            └── repositories
                ├── alpine
                │   ├── _layers
                │   │   └── sha256
                │   │       ├── 0766572b4bacfaee9a8eb6bae79e6f6dbcdfac0805c7c6ec8b6c2c0ef097317a
                │   │       └── 9ca846b27f6e92f0739af5bba5509357b52be0ce0dd02d216f4dccdacd695a8a
                │   ├── _manifests
                │   │   ├── revisions
                │   │   │   └── sha256
                │   │   │       └── 9a597e826a59709a4af34279f496c323d496a79e4c998ee5249a738e391192bb
                │   │   └── tags
                │   │       └── 3.4
                │   │           ├── current
                │   │           └── index
                │   │               └── sha256
                │   │                   └── 9a597e826a59709a4af34279f496c323d496a79e4c998ee5249a738e391192bb
                │   └── _uploads
                └── library
                    └── alpine
                        ├── _layers
                        │   └── sha256
                        │       ├── 0766572b4bacfaee9a8eb6bae79e6f6dbcdfac0805c7c6ec8b6c2c0ef097317a
                        │       └── 9ca846b27f6e92f0739af5bba5509357b52be0ce0dd02d216f4dccdacd695a8a
                        ├── _manifests
                        │   ├── revisions
                        │   │   └── sha256
                        │   │       └── 9a597e826a59709a4af34279f496c323d496a79e4c998ee5249a738e391192bb
                        │   └── tags
                        │       └── 3.4
                        │           ├── current
                        │           └── index
                        │               └── sha256
                        │                   └── 9a597e826a59709a4af34279f496c323d496a79e4c998ee5249a738e391192bb
                        └── _uploads
</code></pre>
<p>这个也就是就是官方 registry 镜像的默认储存目录 <code>/var/lib/registry</code> 的文件结构。上述就是镜像 <a href="https://hub.docker.com/_/alpine/">alpine:3.4</a> 在镜像仓库中的储存结构，<code>docker/registry/v2/repositories/library/alpine</code> 表示镜像的 repository，也是我们俗称的镜像，然后在 <code>docker/registry/v2/repositories/library/alpine/_manifests/tags/</code> 下面是镜像的所有 tag 信息，tag 目录下存的是 tag 的索引，根据索引，再去 <code>docker/registry/v2/blob</code> 目录下就可以一级一级往下找到对应的镜像层了。一般情况下，我们会将镜像分组在相同的 namespace 下，也就是上述的 <code>library</code>，所以其实在官方的 hub 上面，官方的镜像都是数据 <code>library</code> 这个 namespace 下面的，而每个用户就在自己的 namespace 下，也就是 project。</p>
<h2 id="storagedriver">StorageDriver</h2>
<pre><code class="language-go">type StorageDriver interface {
	// Name returns the human-readable &quot;name&quot; of the driver, useful in error
	// messages and logging. By convention, this will just be the registration
	// name, but drivers may provide other information here.
	Name() string

	// GetContent retrieves the content stored at &quot;path&quot; as a []byte.
	// This should primarily be used for small objects.
	GetContent(ctx context.Context, path string) ([]byte, error)

	// PutContent stores the []byte content at a location designated by &quot;path&quot;.
	// This should primarily be used for small objects.
	PutContent(ctx context.Context, path string, content []byte) error

	// Reader retrieves an io.ReadCloser for the content stored at &quot;path&quot;
	// with a given byte offset.
	// May be used to resume reading a stream by providing a nonzero offset.
	Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error)

	// Writer returns a FileWriter which will store the content written to it
	// at the location designated by &quot;path&quot; after the call to Commit.
	Writer(ctx context.Context, path string, append bool) (FileWriter, error)

	// Stat retrieves the FileInfo for the given path, including the current
	// size in bytes and the creation time.
	Stat(ctx context.Context, path string) (FileInfo, error)

	// List returns a list of the objects that are direct descendants of the
	//given path.
	List(ctx context.Context, path string) ([]string, error)

	// Move moves an object stored at sourcePath to destPath, removing the
	// original object.
	// Note: This may be no more efficient than a copy followed by a delete for
	// many implementations.
	Move(ctx context.Context, sourcePath string, destPath string) error

	// Delete recursively deletes all objects stored at &quot;path&quot; and its subpaths.
	Delete(ctx context.Context, path string) error

	// URLFor returns a URL which may be used to retrieve the content stored at
	// the given path, possibly using the given options.
	// May return an ErrUnsupportedMethod in certain StorageDriver
	// implementations.
	URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error)
}
</code></pre>
<p>如果要自己实现镜像的 storage，就得按照 <code>StorageDriver</code> 。</p>
<p>对于镜像的 push 过程，最主要的函数就是 <code>StorageDriver.Write</code>，这个主要是用来上传大文件的，而 <code>StorageDriver.PutContent</code> 就是一些小文件的上传，比如说 tag 的 index。</p>
<p><code>StorageDriver.Write</code> 返回一个 <code>FileWriter</code>，这个也需要自己实现，当有文件上传时，会不停的调用 <code>FileWriter.Write</code> ，当文件上传完毕，会调用 <code>FileWriter.Commit</code>。</p>
<h2 id="qiniustorage">Qiniu Storage</h2>
<p>官方并没有提供七牛的 storage，但是因为有 <code>StorageDriver</code>，所以也不妨碍我们自己撸一个使用七牛作为储存的私有镜像仓库来</p>
<p>初始化的 driver 的过程，参照阿里云的 OSS 过程，目测阿里云的 OSS 也是参照 AWS 来的，而且阿里云的 OSS API 接口，跟 AWS 真是神似了。</p>
<h3 id="storagedrivergetcontent">StorageDriver.GetContent</h3>
<pre><code class="language-go">func (d *driver) GetContent(ctx dockerContext.Context, path string) ([]byte, error) {
	baseURL := kodo.MakeBaseUrl(d.domain, d.qiniuPath(path))
	policy := kodo.GetPolicy{}
	privateURL := d.client.MakePrivateUrl(baseURL, &amp;policy)

	resp, err := http.Get(privateURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return nil, storagedriver.PathNotFoundError{Path: path}
	}

	return ioutil.ReadAll(resp.Body)
}

</code></pre>
<p><code>StorageDriver.GetContent</code> 直接使用七牛的 Golang SDK ，根据 <code>path</code> 生成对应的文件的下载路径，然后下载后，直接返回 <code>[]byte</code> 就行了。</p>
<h3 id="storagedriverreader">StorageDriver.Reader</h3>
<pre><code class="language-go">func (d *driver) Reader(ctx dockerContext.Context, path string, offset int64) (io.ReadCloser, error) {

	contents, err := d.GetContent(ctx, path)
	if err != nil {
		return nil, err
	}
	return ioutil.NopCloser(bytes.NewReader(contents[offset:])), nil
}

</code></pre>
<p><code>StorageDriver.Reader</code> 应该是返回一段 <code>offset</code> 偏移量的文件内容，但是七牛貌似并没有提供像阿里云一样的 <code>range</code> 下载的方式，于是只能将整个文件下载下来，然后再手动切分文件了，这样做，要是镜像的某一层很大，会占用很多内存。</p>
<h3 id="storagedriverputcontent">StorageDriver.PutContent</h3>
<pre><code class="language-go">func (d *driver) PutContent(ctx dockerContext.Context, path string, contents []byte) error {
	putExtra := kodo.PutExtra{}
	body := bytes.NewReader(contents)
	err := d.bucket.Put(context.Background(), nil, d.qiniuPath(path), body, int64(len(contents)), &amp;putExtra)
	return err
}

</code></pre>
<p><code>StorageDriver.PutContent</code> 也是直接使用七牛的 SDK 上传小文件。</p>
<h3 id="storagedriverwriter">StorageDriver.Writer</h3>
<pre><code class="language-go">func (d *driver) Writer(ctx dockerContext.Context, path string, append bool) (storagedriver.FileWriter, error) {
	var w *writer
	qiniuPath := d.qiniuPath(path)
	if !append {
		w = &amp;writer{
			key:           qiniuPath,
			driver:        d,
			buffer:        make([]byte, 0, maxChunkSize),
			uploadCtxList: make([]string, 0, 1),
		}
		d.writerPath[qiniuPath] = w
	} else {
		w = d.writerPath[qiniuPath].(*writer)
		w.closed = false

	}
	return w, nil
}
</code></pre>
<p>由于七牛的切片上传方式与阿里云提供的切片有点不同，七牛的需要客户端自己记录上传的片段，所以不得不在 <code>StorageDriver</code> 这里做手脚了，这里将 <code>FileWriter</code> 保存在 <code>StorageDriver</code> 上，以便下一次使用。</p>
<h3 id="storagedrivermove">StorageDriver.Move</h3>
<pre><code class="language-go">func (d *driver) Move(ctx dockerContext.Context, sourcePath string, destPath string) error {
	if err := d.bucket.Move(context.Background(), d.qiniuPath(sourcePath), d.qiniuPath(destPath)); err != nil {
		return err
	}
	return nil
}

</code></pre>
<p><code>StorageDriver.Move</code> 在这里的作用是因为 registry 在上传时，会先将文件放在 <code>docker/registry/v2/repositories/library/alpine/_uploads</code> 目录下面，然后等文件正式上传完毕后，在移动到对应的镜像目录下面。所以也需要 <code>StorageDriver.Delete</code> 删除上传的临时文件。</p>
<h3 id="storagedriverstat">StorageDriver.Stat</h3>
<pre><code class="language-go">func (d *driver) Stat(ctx dockerContext.Context, path string) (storagedriver.FileInfo, error) {
	entries, commonPrefixes, _, err := d.bucket.List(context.Background(), d.qiniuPath(path), &quot;/&quot;, &quot;&quot;, 1)
	if err != nil &amp;&amp; len(entries) == 0 {

		if err == io.EOF {
			return nil, storagedriver.PathNotFoundError{Path: path}
		}
		return nil, err
	}

	fi := storagedriver.FileInfoFields{
		Path: path,
	}
	if len(entries) == 1 {
		if entries[0].Key != d.qiniuPath(path) {
			fi.IsDir = true
		} else {
			fi.IsDir = false
			fi.Size = entries[0].Fsize
			fi.ModTime = time.Unix(0, entries[0].PutTime)
		}
	} else if len(commonPrefixes) == 1 {
		fi.IsDir = true
	} else {
		return nil, storagedriver.PathNotFoundError{Path: path}
	}
	return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil
}
</code></pre>
<p><code>StorageDriver.Stat</code> 可以在上传时判断文件是否已经存在。</p>
<h3 id="filewriterwriter">FileWriter.Writer</h3>
<pre><code class="language-go">func (w *writer) Write(p []byte) (int, error) {
	if w.closed {
		return 0, fmt.Errorf(&quot;already closed&quot;)
	} else if w.committed {
		return 0, fmt.Errorf(&quot;already committed&quot;)
	} else if w.cancelled {
		return 0, fmt.Errorf(&quot;already cancelled&quot;)
	}

	if err := w.flushBuffer(); err != nil {
		return 0, err
	}

	w.buffer = append(w.buffer, p...)
	if len(w.buffer) &gt;= maxChunkSize {
		if err := w.flushBuffer(); err != nil {
			return 0, err
		}
	}

	w.size += int64(len(p))

	return len(p), nil
}

func (w *writer) flushBuffer() error {

	for len(w.buffer) &gt;= maxChunkSize {
		if err := w.mkblk(bytes.NewReader(w.buffer[:maxChunkSize]), maxChunkSize); err != nil {
			return err
		}
		w.buffer = w.buffer[maxChunkSize:]
	}
	return nil
}

</code></pre>
<p>由于七牛切片上传每块大小最大只能为 4MB ，而 registry 每次上传的一段的大小又是不确定的，所以此处将 registry 每段上传的内容存到 <code>buffer</code> 中，然后当 <code>buffer</code> 里的内容达到 4MB，就将 <code>buffer</code> 中的 4MB 上传到七牛。</p>
<h3 id="filewritercommit">FileWriter.Commit</h3>
<pre><code class="language-go">func (w *writer) Commit() error {
	defer func() {
		w.driver.RemoveWriter(w.key)
	}()
	if w.closed {
		return fmt.Errorf(&quot;already closed&quot;)
	} else if w.committed {
		return fmt.Errorf(&quot;already committed&quot;)
	} else if w.cancelled {
		return fmt.Errorf(&quot;already cancelled&quot;)
	}

	if err := w.flushBuffer(); err != nil {
		return err
	}

	if len(w.buffer) &gt; 0 {
		if err := w.mkblk(bytes.NewReader(w.buffer), len(w.buffer)); err != nil {
			return err
		}
	}

	if err := w.mkfile(); err != nil {
		return err
	}

	w.committed = true
	return nil
}

</code></pre>
<p>最终上传完毕后，在 <code>FileWriter.Commit</code> 需要将<code>driver</code> 上的<code>FileWriter</code> 给删掉，不然会内存泄露了，在最后 <code>commit</code> 时，再次确认 <code>buffer</code> 中的内容已经全部上传到了七牛，然后调用七牛的 <code>mkfile</code> 合并块成文件，想当初去七牛面试<strong>技术支持</strong>的岗位时，还问过我这个问题，当时答得并不咋地。</p>
<h3 id="">最后</h3>
<p>最终需要在 <code>cmd/registry/main.go</code> 的文件中初始化我们自己的 <code>driver</code>。</p>
<p>完整的七牛 Storage 在这里，<a href="https://gist.github.com/cloverstd/813a0df6b9eacc459dd84509e902e744">https://gist.github.com/cloverstd/813a0df6b9eacc459dd84509e902e744</a> 。</p>
</div>]]></content:encoded></item><item><title><![CDATA[Bamboo 原理分析]]></title><description><![CDATA[<div class="kg-card-markdown"><h1 id="">原理简介</h1>
<p>Bamboo 为了 <a href="https://mesosphere.com/product/">DO/CS</a> 系统提供了服务自动发现的功能，当 <a href="https://mesosphere.github.io/marathon">Marathon</a> 上部署了一个应用，Bamboo 能够将应用下所有的容器的端口更新到 HAProxy 的配置文件中，并且 reload HAProxy，从而实现应用的服务自动发现，并且利用 HAProxy 做应用中容器的负载均衡。</p>
<p><img src="//cdn.cloverstd.com/images/blog/2017/06/07/bamboo.png" alt=""></p>
<p>Bamboo 主要的原理就是利用 Marathon 的 <a href="https://mesosphere.github.io/marathon/docs/event-bus.html">Event-Bus</a> API，当 Marathon 上的应用发生改变（scaling）时，Event-Bus 会通知对应的 Subscriber (Bamboo)，然后 Subscriber 会做出对应的操作。</p>
<p>除了 Bamboo，还有 <a href="https://github.com/mesosphere/marathon-lb">Marathon-lb</a> 也是类似的原理。</p>
<h1 id="">源码分析</h1>
<p>Bamboo 是使用 Golang 开发的，代码量很少，原理也不复杂。</p></div>]]></description><link>https://hui.lu/bamboo-theory/</link><guid isPermaLink="false">59f426890ee507000193927a</guid><category><![CDATA[Docker]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Thu, 08 Jun 2017 13:35:59 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><h1 id="">原理简介</h1>
<p>Bamboo 为了 <a href="https://mesosphere.com/product/">DO/CS</a> 系统提供了服务自动发现的功能，当 <a href="https://mesosphere.github.io/marathon">Marathon</a> 上部署了一个应用，Bamboo 能够将应用下所有的容器的端口更新到 HAProxy 的配置文件中，并且 reload HAProxy，从而实现应用的服务自动发现，并且利用 HAProxy 做应用中容器的负载均衡。</p>
<p><img src="//cdn.cloverstd.com/images/blog/2017/06/07/bamboo.png" alt=""></p>
<p>Bamboo 主要的原理就是利用 Marathon 的 <a href="https://mesosphere.github.io/marathon/docs/event-bus.html">Event-Bus</a> API，当 Marathon 上的应用发生改变（scaling）时，Event-Bus 会通知对应的 Subscriber (Bamboo)，然后 Subscriber 会做出对应的操作。</p>
<p>除了 Bamboo，还有 <a href="https://github.com/mesosphere/marathon-lb">Marathon-lb</a> 也是类似的原理。</p>
<h1 id="">源码分析</h1>
<p>Bamboo 是使用 Golang 开发的，代码量很少，原理也不复杂。</p>
<p>首先是从配置文件读取配置，然后读取对应的环境变量，当环境变量存在时，覆盖相应的配<br>
置项，对应的代码在 <a href="https://github.com/QubitProducts/bamboo/blob/master/configuration/configuration.go#L44">configuration#L44</a> 。</p>
<p>然后就是声明一个 <code>EventBus</code>，用来将 Marathon、ZK 发生改变时的动作传递到对应的 Handler 函数中去操作。</p>
<p>在 <a href="https://github.com/QubitProducts/bamboo/blob/master/main/bamboo/bamboo.go#L80-L81">bamboo.go#L80-L81</a> 这里注册了两个主要的 EventHandler，<code>MarathonEventHandler</code> 对应的是 Marathon 上的应用发生改变时的动作，<code>ServiceEventHandler</code> 对应的是 ZK 上的应用配置发生改变时的动作。</p>
<p>在这里，会使用 <code>listenToZookeeper</code> 监听 ZK 的改变，然后应用到<code>ServiceEventHandler</code>上。</p>
<p>最后启动一个 HTTPServer 作为对 ZK 操作的 API 接口，对于 Marathon 事件的监听，Bamboo 支持 Marathon 的两种方式</p>
<ul>
<li>主动调用</li>
<li>回调模式</li>
</ul>
<p>主动调用的过程是首先发起一个 HTTP 请求到 Marathon 的 <code>/v2/events</code> 接口，这个接口是一个 stream 的 HTTP 请求，Marathon 会将应用的改变 push 到建立的连接中。回调模式是 Bamboo 向 Marathon 注册自己的地址，当 Marathon 上的应用发生改变时，会向 Bamboo 注册的地址发送 HTTP 请求，通知 Bamboo。</p>
<p>这两种模式可以通过环境变量 <code>MARATHON_USE_EVENT_STREAM</code> 来启用主动调用的模式，主动调用在本地开发调试 Bamboo 时很有用，这样就不需要 Marathon 回调本机地址了。</p>
<p>无论是回调模式还是主动调用，当 Marathon 的发生改变时，最终都会调用 <code>EventBus.Publish</code> 的方法，而 <code>EventBus.Publish</code> 就会遍历注册的 Handler，然后调用对应的事件的 Handler。</p>
<p>主要两个 Handler 就是上述的 <code>MarathonEventHandler</code> 和 <code>ServiceEventHandler</code>。</p>
<p>在应用启动时会调用 <a href="https://github.com/QubitProducts/bamboo/blob/master/services/event_bus/event_handler.go#L50-L58">event_handler</a> 的 <code>init</code> 函数，在 <code>init</code> 函数中，通过 <code>updateChan</code> 这个带有 buffer 的 channel 与 handler 进行通信，并且同时也创建了一个带 buffer 的 channel <code>queueUpdateSem</code>，容量为 1，用来保证同一时间只有一个 HAProxy 在更新，并且后续的一个更新操作可以排队，再之后的更新操作就被 block 掉了。</p>
<p>上述的两个 Handler 都会触发更新 HAProxy 的动作，而更新 HAProxy 的流程是，首先通过 <code>ensureLatestConfig</code> 函数确认 HAProxy 配置文件是否有更新的必要，原理就是生成新的 HAProxy 配置，然后读取旧的 HAProxy，对比配置文件是否一致，如果一模一样就放弃此次更新。当确认要更新之后，会通过<code>ReloadValidationCommand</code>定义的命令来检验 HAProxy 的配置是否正确，只有校验通过的配置，才能调用<code>ReloadCommand</code>通知 HAProxy 重新加载新的配置文件。</p>
<p>Bamboo 通过 Golang 的 <code>text/template</code> 来生成 HAProxy 配置文件，语法就是 Golang 的模板语法，然后 Bamboo 加了一些便捷函数。</p>
<h1 id="">一些问题</h1>
<ul>
<li>Marathon 上运行多个应用时，并且都通过 Bamboo 配置了 HAProxy 的 ACL，如果一个应用更新时，其他应用的访问会在很短的时间内中断，对于这个问题，可以通过类似于 Marathon-lb 的 HAPROXY_GROUP 的方法，将 Bamboo 绑定到单独的应用组上。</li>
<li>根据客户反馈，HAProxy 在 reload 的过程中，会发生一定几率的中断，yelp 有<a href="https://engineeringblog.yelp.com/2015/04/true-zero-downtime-haproxy-reloads.html">一篇文章</a>分析这个问题，可以实现 zero-downtime 的 HAProxy reload，然后数人云写了<a href="https://github.com/Dataman-Cloud/omega-haproxyctl">这个</a>据说这可以『实现无中断 Reload Haproxy』。</li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[减小 Docker 镜像体积]]></title><description><![CDATA[通过替换 centos 为 alpine 减小镜像体积，通过优化 dockerfile 减小镜像体积。
减小 docker 镜像尺寸，缩小 docker image 尺寸]]></description><link>https://hui.lu/reduce-docker-image-size/</link><guid isPermaLink="false">59f426890ee5070001939279</guid><category><![CDATA[Docker]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Wed, 09 Nov 2016 14:42:39 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>最近遇到了 Docker 镜像体积过大的问题，对于部署、移交相当麻烦和慢。于是就抽点时间研究下了怎么减小 Docker 镜像的体积。</p>
<p>下面我以手动编译 nginx 镜像作为例子来减小镜像体积，nginx 的版本是 1.10.2，并且下载到了当前目录。</p>
<p>下图是各种 Dockerfile 制作出镜像的效果<br>
<img src="//cdn.cloverstd.com/images/blog/2016/11/09/docker-image-size.jpeg" alt=""></p>
<ul>
<li><a href="#replace-base-image">替换基础镜像</a>
<ul>
<li><a href="#base-image-centos">基于 centos 的镜像</a></li>
<li><a href="#base-image-alpine">基于 Alpine 的镜像</a></li>
</ul>
</li>
<li><a href="#remove-build-dependence-file">移除 build 依赖的文件</a>
<ul>
<li><a href="#build-dependence-file-centos">centos 基础镜像的依赖</a></li>
<li><a href="#build-dependence-file-alpine">Alpine 基础镜像的依赖</a></li>
</ul>
</li>
<li><a href="#reduce-image-level">减少镜像层级</a>
<ul>
<li><a href="#level-centos">centos</a></li>
<li><a href="#level-alpine">Alpine</a></li>
<li><a href="#bigfile-test">大文件测试</a>
<ul>
<li><a href="#bigfile-by-copy">通过 COPY</a></li>
<li><a href="#bigfile-by-wget">通过 wget</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#other">其他</a>
<ul>
<li><a href="#other-python">Python</a></li>
<li><a href="#other-build-speed-up">通过镜像层级加速 build 过程</a></li>
</ul>
</li>
</ul>
<div id="replace-base-image"></div>
# 替换基础镜像
我们的基础镜像是基于 centos 7.X，从官方 hub 拉取的 centos 7 的基础镜像有 194.6MB，这个基础镜像已经相当的大了，所以减小镜像体积的第一步是替换基础镜像。首选的基础镜像是 alpine，这里我选择的是 3.4。
<div id="base-image-centos"></div>
##基于 centos 的镜像
下面是最原始的镜像的 Dockerfile
<pre><code class="language-Dockerfile">FROM centos:7.2.1511
COPY . /tmp/

RUN yum install -y make \
    gcc \
    openssl-devel \
    zlib-devel \
    perl-devel \
    pcre-devel &amp;&amp; \
    cd /tmp/ &amp;&amp; tar zxf nginx-1.10.2.tar.gz &amp;&amp; \
    cd /tmp/nginx-1.10.2 &amp;&amp; \
    ./configure --with-http_gzip_static_module --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install &amp;&amp; \
    rm -rf /tmp/* &amp;&amp; \
    ln -sf /dev/stdout /usr/local/nginx/logs/access.log &amp;&amp; \
    ln -sf /dev/stderr /usr/local/nginx/logs/error.log

EXPOSE 80
CMD [&quot;/usr/local/nginx/sbin/nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]
</code></pre>
<p>上述就是一个最基本的 nginx 镜像了，通过 <code>docker build</code> 后，镜像大小是 535.2MB。nginx 的源代码才 890KB，这种镜像，完全是浪费空间。</p>
<div id="base-image-alpine"></div>
## 基于 Alpine 的镜像
对于 centos 的基础镜像，本身就已经接近 200MB 了，然后 Alpine 的基础镜像才 4.999MB，下面是改为 Alpine 的基础镜像的 Dockerfile
<pre><code class="language-Dockerfile">FROM alpine:3.4
COPY . /tmp/

RUN apk update &amp;&amp; \
    apk add gcc make openssl-dev zlib-dev perl-dev pcre-dev libc-dev &amp;&amp; \
    cd /tmp/ &amp;&amp; tar zxf nginx-1.10.2.tar.gz &amp;&amp; \
    cd /tmp/nginx-1.10.2 &amp;&amp; \
    ./configure --with-http_gzip_static_module --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install &amp;&amp; \
    rm -rf /tmp/* &amp;&amp; \
    ln -sf /dev/stdout /usr/local/nginx/logs/access.log &amp;&amp; \
    ln -sf /dev/stderr /usr/local/nginx/logs/error.log

EXPOSE 80
CMD [&quot;/usr/local/nginx/sbin/nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]
</code></pre>
<p>通过上述的 Dockerfile <code>docker build</code> 出来的镜像大小是 155.8MB，体积已经缩小到了 3 倍以上了，对于 nginx 的运行来说，centos 和 Alpine 完全是一样的效果，所以对于基础镜像，首选应该是 Alpine 这之类的精简版系统，而且在官方 hub 上的官方镜像，大多有提供 Alpine 版的版本。</p>
<div id="remove-build-dependence-file"></div>
# 移除 build 依赖的文件
有一些软件安装时，可能在编译时会依赖于一些头文件，例如 `openssl-dev` 这之类的 `dev` 文件，还有就是 `gcc` 和 `make` 这之类的编译工具，在程序运行时完全不需要，所以可以在编译完之后，删除掉，这样可以也是可以减小一定的体积。
<div id="build-dependence-file-centos"></div>
## centos 基础镜像的依赖
```Dockerfile
FROM centos:7.2.1511
<p>COPY . /tmp/</p>
<p>RUN yum install -y make <br>
gcc <br>
openssl-devel <br>
zlib-devel <br>
perl-devel <br>
pcre-devel &amp;&amp; <br>
cd /tmp/ &amp;&amp; tar zxf nginx-1.10.2.tar.gz &amp;&amp; <br>
cd /tmp/nginx-1.10.2 &amp;&amp; <br>
./configure --with-http_gzip_static_module --with-http_ssl_module &amp;&amp; <br>
make &amp;&amp; make install &amp;&amp; <br>
yum remove -y gcc openssl-devel zlib-devel perl-devel pcre-devel make &amp;&amp; <br>
yum clean all &amp;&amp; <br>
rm -rf /tmp/* &amp;&amp; <br>
ln -sf /dev/stdout /usr/local/nginx/logs/access.log &amp;&amp; <br>
ln -sf /dev/stderr /usr/local/nginx/logs/error.log</p>
<p>EXPOSE 80<br>
CMD [&quot;/usr/local/nginx/sbin/nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]</p>
<pre><code>上面的 `yum remove -y gcc openssl-devel zlib-devel perl-devel pcre-devel make` 和 `yum clean all` 就是移除不需要的文件，从而进一步减小体积，加上两条命令后，执行 `docker build` 后的镜像的体积为 413.8MB，相对上述的 535.2MB 减小了 121.4 MB。

&lt;div id=&quot;build-dependence-file-alpine&quot;&gt;&lt;/div&gt;
## Alpine 基础镜像的依赖
上述 centos 的移除依赖的后体积变化可能看着没有啥感觉，因为本身就很大，下面是 Alpine 的 Dockerfile

```Dockerfile
FROM alpine:3.4

COPY . /tmp/

RUN apk update &amp;&amp; \
    apk add gcc make openssl-dev zlib-dev perl-dev pcre-dev libc-dev &amp;&amp; \
    cd /tmp/ &amp;&amp; tar zxf nginx-1.10.2.tar.gz &amp;&amp; \
    cd /tmp/nginx-1.10.2 &amp;&amp; \
    ./configure --with-http_gzip_static_module --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install &amp;&amp; \
    apk del gcc make openssl-dev zlib-dev perl-dev pcre-dev libc-dev &amp;&amp; \
    rm -rf /var/cache/* &amp;&amp; \
    rm -rf /tmp/* &amp;&amp; \
    ln -sf /dev/stdout /usr/local/nginx/logs/access.log &amp;&amp; \
    ln -sf /dev/stderr /usr/local/nginx/logs/error.log

EXPOSE 80
CMD [&quot;/usr/local/nginx/sbin/nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]
</code></pre>
<p>这次 <code>docker build</code> 出来的镜像的体积为 11.74MB，直接从 155.8MB 减小了 144.04 MB，达到了数量级的变化。并且这个 11.74MB 的镜像也是可以正常运行的。</p>
<div id="reduce-image-level"></div>
# 减少镜像层级
由于 Docker 镜像的储存原理是分层的，其实 `docker build` 的过程就是 docker 运行了一个容器，然后执行 Dockerfile 里写的命令。并且每一个命令都会 commit 一下，每一次 commit 都是一层一层的叠加在原来的镜像上，也就是说在某一层里增加了一个文件，在下一层里删除这个文件，是没有任何效果的，镜像体积是不变的，可能反而会增加。所以减小镜像的体积除了替换基础镜像，还需要优化 Dockerfile，减少镜像的层级。
<p>那么对于一些需要通过 COPY 命令的方式拷贝到镜像里面的文件，可以使用 wget 的方式，下载到镜像里，然后使用完之后就删掉。</p>
<div id="level-centos"></div>
## centos
```Dockerfile
FROM centos:7.2.1511
<p>RUN yum install -y make <br>
gcc <br>
openssl-devel <br>
zlib-devel <br>
perl-devel <br>
pcre-devel <br>
wget &amp;&amp; <br>
wget --no-check-certificate -O /tmp/nginx-1.10.2.tar.gz <a href="http://nginx.org/download/nginx-1.10.2.tar.gz">http://nginx.org/download/nginx-1.10.2.tar.gz</a> &amp;&amp; <br>
cd /tmp/ &amp;&amp; tar zxf nginx-1.10.2.tar.gz &amp;&amp; <br>
cd /tmp/nginx-1.10.2 &amp;&amp; <br>
./configure --with-http_gzip_static_module --with-http_ssl_module &amp;&amp; <br>
make &amp;&amp; make install &amp;&amp; <br>
rm -rf /tmp/* &amp;&amp; <br>
ln -sf /dev/stdout /usr/local/nginx/logs/access.log &amp;&amp; <br>
ln -sf /dev/stderr /usr/local/nginx/logs/error.log</p>
<p>EXPOSE 80<br>
CMD [&quot;/usr/local/nginx/sbin/nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]</p>
<pre><code>上述通过 wget 的下载方式将 nginx 的源代码下载到了镜像里面，然后使用完之后，再删除掉，这样对于镜像来说，将不会存在 nginx 的源代码。通过此方法 `docker build` 出来的镜像是 534.8MB，相对于最原始的 535.2MB，少了一丢丢，这种方式随着文件的大小增大，效果显著。

&lt;div id=&quot;level-alpine&quot;&gt;&lt;/div&gt;
## Alpine
```Dockerfile
FROM alpine:3.4

RUN apk update &amp;&amp; \
    apk add gcc make openssl-dev zlib-dev perl-dev pcre-dev libc-dev wget &amp;&amp; \
    wget --no-check-certificate -O /tmp/nginx-1.10.2.tar.gz http://nginx.org/download/nginx-1.10.2.tar.gz &amp;&amp; \
    cd /tmp/ &amp;&amp; tar zxf nginx-1.10.2.tar.gz &amp;&amp; \
    cd /tmp/nginx-1.10.2 &amp;&amp; \
    ./configure --with-http_gzip_static_module --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install &amp;&amp; \
    rm -rf /tmp/* &amp;&amp; \
    ln -sf /dev/stdout /usr/local/nginx/logs/access.log &amp;&amp; \
    ln -sf /dev/stderr /usr/local/nginx/logs/error.log

EXPOSE 80
CMD [&quot;/usr/local/nginx/sbin/nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;]
</code></pre>
<p>通过 <code>docker build</code> 后，镜像大小为 155.4MB，因为 Alpine 的体积本身很小，所以效果不明显。</p>
<div id="bigfile-test"></div>
## 大文件测试
为了测试大文件的效果，我通过 dd 生成了一个 2.3GB 的文件，然后我分别通过 copy 的方式和 wget 的方式拷贝文件到镜像，并且移除文件。
<div id="bigfile-by-copy"></div>
### 通过 COPY
```Dockerfile
FROM alpine:3.4
<p>COPY . /code<br>
RUN rm -rf /code</p>
<p>CMD [&quot;tail&quot;, &quot;-f&quot;, &quot;/etc/hosts&quot;]</p>
<pre><code>基础镜像采用的是 Alpine，对于 2.3GB 来说，基础的镜像的体积可以忽略。`docker build` 出来的镜像体积是 2.463GB，可以看到，虽然通过 `rm -rf /code` 将文件删除了，但是由于 docker 镜像是分层的原因，所以镜像的依旧包含了 COPY 进去的文件的体积，虽然文件在 `docker run` 时是看不到的。

&lt;div id=&quot;bigfile-by-wget&quot;&gt;&lt;/div&gt;
### 通过 wget
```Dockerfile
FROM alpine:3.4

RUN apk update &amp;&amp; apk add wget &amp;&amp; \
    mkdir /code &amp;&amp; \
    wget --no-check-certificate -O /code/test.dbf http://172.24.4.188:8000/test.dbf &amp;&amp; \
    apk del wget &amp;&amp; \
    rm -rf /var/cache/* &amp;&amp; \
    rm -rf /code

CMD [&quot;tail&quot;, &quot;-f&quot;, &quot;/etc/hosts&quot;]
</code></pre>
<p>通过此方法 <code>docker build</code> 出来的镜像体积为 4.817MB，相对于 Alpine 的基础镜像 4.799MB，只增加了 0.018MB。相对于上述的 COPY 完全是质的变化。</p>
<div id="other"></div>
#其他
<div id="other-python"></div>
## Python
对于 Python 来说，在制作镜像时，往往会预先安装依赖的库，在安装的过程中，往往会产生大量的 `pyc` 和 `pyo` 文件，这些文件对于镜像来说完全是不需要的，完全可以在镜像成为容器运行时生成，当然，这可能会影响一丢丢启动速度，毕竟文件是需要生成的。所以以时间换空间的话，完全可以移除掉。
<p>通过 <code>find / -name &quot;*.py[co]&quot; -exec rm '{}' ';'</code> 就可以全都移除掉</p>
<div id="other-build-speed-up"></div>
## 通过镜像层级加速 build 过程
有时，在开发的时候，需要频繁的 build 镜像，如果全部在一个 RUN 里来执行，那么每次 build 时，所有的依赖都会安装一遍，这样极其浪费时间，可以通过 docker 镜像分层的原理，将安装依赖的过程放在一层里，然后代码放在最后 COPY 进去，因为一般情况下，都是代码频繁的变动，依赖的软件不会经常变动，这样可以利用 build 时的 cache，加速 build 的过程。</div>]]></content:encoded></item><item><title><![CDATA[使用 Nginx Upload Module 上传文件]]></title><description><![CDATA[<div class="kg-card-markdown"><p>从开始会写程序开始，一直都很讨厌处理上传文件，因为总觉得在自己写的 controller 里面从 HTTP Body 里读文件，然后再写到本地磁盘的过程是一件非常不（易）靠（出）谱（错）的事，总怕文件没有正确写入，而且常用的框架对于上传文件，都是直接将整个文件加载到内存中，当处理大文件上传时，内存岂不是要爆炸了。</p>
<p>后来知道了七牛和又拍云之类的云服务，顿时感觉对于文件上传的过程的瞬间简化了。类似与七牛之类的文件储存云服务一般都会提供<a href="http://qiniu-developer.u.qiniudn.com/docs/v6/api/reference/up/upload.html">文件直传</a>的接口，直接在前端就把文件上传到了七牛的服务器，而不经过业务服务器，然后当文件上传成功后，可以选择让七牛通知（回调）到业务服务器。</p>
<p>但是一般只有『互联网』企业才会选择七牛之类的云服务，可惜我就没怎么待过『互联网』企业，所以一直都没有正式的使用过文件直传这种方式，甚是伤心。如果你没有用过七牛，强烈推荐你试用一下，这里有我的邀请链接，你注册成功，我将收到 40G 的免费流量 <a href="https://portal.qiniu.com/signup?code=3lpwnduxidob5">https://portal.qiniu.com/</a></p></div>]]></description><link>https://hui.lu/upload-file-with-nginx-upload-file/</link><guid isPermaLink="false">59f426890ee5070001939278</guid><category><![CDATA[Nginx]]></category><category><![CDATA[Tornado]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Sat, 22 Oct 2016 10:15:14 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>从开始会写程序开始，一直都很讨厌处理上传文件，因为总觉得在自己写的 controller 里面从 HTTP Body 里读文件，然后再写到本地磁盘的过程是一件非常不（易）靠（出）谱（错）的事，总怕文件没有正确写入，而且常用的框架对于上传文件，都是直接将整个文件加载到内存中，当处理大文件上传时，内存岂不是要爆炸了。</p>
<p>后来知道了七牛和又拍云之类的云服务，顿时感觉对于文件上传的过程的瞬间简化了。类似与七牛之类的文件储存云服务一般都会提供<a href="http://qiniu-developer.u.qiniudn.com/docs/v6/api/reference/up/upload.html">文件直传</a>的接口，直接在前端就把文件上传到了七牛的服务器，而不经过业务服务器，然后当文件上传成功后，可以选择让七牛通知（回调）到业务服务器。</p>
<p>但是一般只有『互联网』企业才会选择七牛之类的云服务，可惜我就没怎么待过『互联网』企业，所以一直都没有正式的使用过文件直传这种方式，甚是伤心。如果你没有用过七牛，强烈推荐你试用一下，这里有我的邀请链接，你注册成功，我将收到 40G 的免费流量 <a href="https://portal.qiniu.com/signup?code=3lpwnduxidob5">https://portal.qiniu.com/signup?code=3lpwnduxidob5</a> ，然后这里是没有邀请码的链接 <a href="https://portal.qiniu.com/signup">https://portal.qiniu.com/signup</a> 。</p>
<p>然后最近做的项目有部分需要文件上传，于是就研究了下 <a href="https://www.nginx.com/resources/wiki/modules/upload/">Nginx Upload Module</a> 的使用方法，目测七牛也是用的类似的方法。</p>
<ul>
<li><a href="#nginx-upload-module">Nginx Upload Module</a>
<ul>
<li><a href="#nginx-upload-module-install">安装</a></li>
<li><a href="#nginx-upload-module-config">配置</a></li>
<li><a href="#nginx-upload-module-upload-form">上传参数</a></li>
</ul>
</li>
<li><a href="#upload-handler">业务程序处理文件</a>
<ul>
<li><a href="#upload-handler-tornado">使用 Tornado 来处理文件</a></li>
</ul>
</li>
<li><a href="#ref">参考</a></li>
</ul>
<div id="nginx-upload-module"></div>
<h1 id="nginxuploadmodule">Nginx Upload Module</h1>
<p>Nginx Upload Module 是 Nginx 的一个模块，一般 Linux 发行版里的 Nginx 包都没有预装这个模块，所以需要自己手动编译 Nginx，并且加入这个模块。</p>
<div id="nginx-upload-module-install"></div>
<h2 id="">安装</h2>
<p>手动编译 Nginx 需要一些依赖包，本着极简主义的原则，只安装必须的依赖包</p>
<ul>
<li><code>openssl</code> Nginx Upload Module 会用到</li>
<li><code>pcre</code> <a href="http://nginx.org/en/docs/http/ngx_http_rewrite_module.html">Nginx Rewrite</a> 会用到</li>
<li><code>zlib</code> gzip 压缩会用到</li>
</ul>
<pre><code class="language-shell">wget https://www.openssl.org/source/openssl-1.0.2j.tar.gz
wget ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.39.tar.gz
wget http://zlib.net/zlib-1.2.8.tar.gz
</code></pre>
<p>然后是下载最重要的 Nginx 了，这里我选择的 Nginx 是 1.10.2，目前的最新稳定版，然后 Nginx Upload Module，看 git 记录，已经至少有 6 年没更新过了，从 Nginx Upload Module 的 <a href="https://github.com/vkholodkov/nginx-upload-module">Github</a> 直接下载的最新版，在新版本（1.8.x）以上没法编译成功，需要用<code>2.2</code>这个分之的代码</p>
<pre><code class="language-shell">wget http://nginx.org/download/nginx-1.10.2.tar.gz
git clone -b 2.2 https://github.com/vkholodkov/nginx-upload-module
</code></pre>
<p>然后就是安装了</p>
<pre><code>tar zxf openssl-1.0.2j.tar.gz
tar zxf pcre-8.39.tar.gz
tar zxf zlib-1.2.8.tar.gz
tar zxf nginx-1.10.2.tar.gz
cd nginx-1.10.2

./configure --add-module=../nginx-upload-module --with-openssl=../openssl-OpenSSL_1_0_2j --with-http_gzip_static_module --with-http_ssl_module --with-pcre=../pcre-8.39/ --with-zlib=../zlib-1.2.8
make &amp;&amp; sudo make install
</code></pre>
<p>如果一切顺利的话，Nginx 已经被默认安装到了 <code>/usr/local/nginx/</code> 目录下了。</p>
<div id="nginx-upload-module-config"></div>
<h2 id="">配置</h2>
<p>上述完成后，通过 <code>sudo /usr/local/nginx/sbin/nginx</code> 就可以启动 Nginx 了。接下来是配置 Nginx Upload Module 了，具体的配置项，在<a href="https://www.nginx.com/resources/wiki/modules/upload/">官方文档</a> 已经说得很清楚了</p>
<pre><code class="language-nginx">server {
    client_max_body_size 100m;
    listen       80;

    # Upload form should be submitted to this location
    location /upload {
        # Pass altered request body to this location
        upload_pass   @test;

        # Store files to this directory
        # The directory is hashed, subdirectories 0 1 2 3 4 5 6 7 8 9 should exist
        upload_store /tmp 1;

        # Allow uploaded files to be read only by user
        upload_store_access user:r;

        # Set specified fields in request body
        upload_set_form_field $upload_field_name.name &quot;$upload_file_name&quot;;
        upload_set_form_field $upload_field_name.content_type &quot;$upload_content_type&quot;;
        upload_set_form_field $upload_field_name.path &quot;$upload_tmp_path&quot;;

        # Inform backend about hash and size of a file
        upload_aggregate_form_field &quot;$upload_field_name.md5&quot; &quot;$upload_file_md5&quot;;
        upload_aggregate_form_field &quot;$upload_field_name.size&quot; &quot;$upload_file_size&quot;;

        upload_pass_form_field &quot;^submit$|^description$&quot;;

        upload_cleanup 400 404 499 500-505;
    }

    # Pass altered request body to a backend
    location @test {
        proxy_pass   http://localhost:8080;
    }
}
</code></pre>
<p>Nginx 安装好后，默认的用户为 <code>nobody</code>，如果不修改，Nginx Upload Module 上传的文件所属是 <code>nobody</code>，其他用户没权限动它，要么是将 Nginx 的 <code>user</code> 配置修改，要么修改 Nginx Upload Module 的 <code>upload_store_access</code> 为 <code>all:rw</code>，不然没法读写。<br>
接下来是在 <code>upload_store</code> 设置的目录下创建对应的是个文件夹，从 0 到 9，注意确保 Nginx 有权限操作这 10 个目录</p>
<pre><code class="language-shell">mkdir -p /tmp/0 /tmp/1 /tmp/2 /tmp/3 /tmp/4 /tmp/5 /tmp/6 /tmp/7 /tmp/8 /tmp/9
</code></pre>
<p>其中 <code>upload_store</code> 后面跟随的数字表示 Nginx Upload Module 存的文件的目录的级数，如果为 2 则表示上述的 10 个目录下再来 10 个目录，同样要自己创建。</p>
<p>然后重新加载 Nginx 的配置文件</p>
<pre><code>sudo /usr/local/nginx/sbin/nginx -s reload
</code></pre>
<p>如果没意外 Nginx Upload Module 就正式安装好了。</p>
<div id="nginx-upload-module-upload-form"></div>
<h3 id="">上传参数</h3>
<p>前端的上传参数如下</p>
<pre><code class="language-html">&lt;form name=&quot;upload&quot; method=&quot;POST&quot; enctype=&quot;multipart/form-data&quot; action=&quot;/upload&quot;&gt;
      &lt;input type=&quot;file&quot; name=&quot;file&quot;&gt;&lt;br&gt;
      &lt;input type=&quot;file&quot; name=&quot;file&quot;&gt;&lt;br&gt;
      &lt;input type=&quot;submit&quot; name=&quot;submit&quot; value=&quot;Upload&quot;&gt;
    &lt;/form&gt;
</code></pre>
<div id="upload-handler"></div>
<h1 id="">业务程序处理文件</h1>
<p>Nginx Upload Module 只做一件事，那就是上传文件存到对应的地方，文件直接存起来是没啥用的，还需要跟真正的业务系统结合起来，所以 Nginx Upload Module 提供了一个 directive <code>upload_pass</code> 来将上传成功后的信息发送给业务程序，并且可以通过 <code>upload_set_form_field</code> 和 <code>upload_aggregate_form_field</code> 将前端的一些参数一起传递给后端去，这样真是太方便了。</p>
<div id="upload-handler-tornado"></div>
<h3 id="tornado">使用 Tornado 来处理文件</h3>
<pre><code class="language-python">class UploadHandler(tornado.web.RequestHandler):

    @tornado.gen.coroutine
    def post(self):
        keys = self.request.arguments.keys()
        if &quot;file.path&quot; not in keys:
            self.set_status(status_code=400, reason=&quot;file field not exist.&quot;)
            self.write(&quot;400&quot;)
            return
        if filter(lambda x: not x.startswith(&quot;file.&quot;), keys):
            self.set_status(status_code=400, reason=&quot;only allow file field upload&quot;)
            self.write(&quot;400&quot;)
            return
        files = list()
        file_path = self.request.arguments['file.path']
        for index in xrange(len(file_path)):
            file = {}
            file['name'] = self.request.arguments['file.name'][index]
            file['content_type'] = self.request.arguments['file.content_type'][index]
            file['path'] = self.request.arguments['file.path'][index]
            file['md5'] = self.request.arguments['file.md5'][index]
            file['size'] = self.request.arguments['file.size'][index]
            files.append(file)
        # mv tmp file to save store storage
        for file in files:
            # debug
            src_file = file['path']
            mime = magic.from_file(src_file, mime=True)
            ext = mimetypes.guess_extension(mime, False)
            dest_file = os.path.join(
                options.storage_path,
                file['md5'] + ext
            )
            if not os.path.exists(dest_file):
                yield LinuxUtils.mv(
                    src_file,
                    dest_file
                )
            else:
                yield LinuxUtils.rm(
                    src_file
                )
            file['path'] = file['md5'] + ext
        self.write({&quot;data&quot;: files})

</code></pre>
<p>上面是处理文件的示例，主要是做两件事</p>
<ul>
<li>配合 Nginx Upload Module 的 <code>upload_cleanup</code> directive 过滤掉不正确的文件上传参数</li>
<li>将文件从临时目录移到其他目录，并且使用 <code>libmagic</code> 来判断文件的类型，从而重命名文件</li>
</ul>
<p>完整的代码在这里 <a href="https://gist.github.com/cloverstd/deef6e9a4db76dab5c4d33ee68f63ec5">https://gist.github.com/cloverstd/deef6e9a4db76dab5c4d33ee68f63ec5</a></p>
<p>从此，虽然用不了七牛，但是文件上传也可以成为一件愉快的事了。</p>
<div id="ref"></div>
<h1 id="">参考</h1>
<ul>
<li><a href="https://www.nginx.com/resources/wiki/modules/upload/">https://www.nginx.com/resources/wiki/modules/upload/</a></li>
<li><a href="https://imququ.com/post/my-nginx-conf.html">https://imququ.com/post/my-nginx-conf.html</a></li>
<li><a href="http://agentzh.org/misc/code/nginx/core/ngx_file.c.html#L430">http://agentzh.org/misc/code/nginx/core/ngx_file.c.html#L430</a></li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[Docker Registry V2 Auth Server]]></title><description><![CDATA[Docker Registry V2 Auth Server；基于 Python Flask 的 Docker Registry 自定义认证服务的实现]]></description><link>https://hui.lu/docker-registry-v2-auth-server-with-python/</link><guid isPermaLink="false">59f426890ee5070001939277</guid><category><![CDATA[Docker]]></category><category><![CDATA[Python]]></category><category><![CDATA[Docker Registry]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Fri, 19 Aug 2016 18:43:45 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>由于工作原因，最近研究了下 <a href="https://docs.docker.com/registry/">Docker Registry V2</a> （以下内容如无特别说明，Registry 均指 Docker Registry V2）的私有化（这里所指的私有化并不是指的私有仓库，而是在私有云里面面向内部的共有仓库，并且对外隔离的）的定制化开发。</p>
<p>Registry 是 Docker 官方提供的镜像仓库，官方的 <a href="https://hub.docker.com">Docker Hub</a> 就是基于 Registry 的，Registry 分为 <a href="https://github.com/docker/docker-registry">V1</a> 和 V2，V1 是我大爱的 Python 的写的，V2 是我现在大爱的 Golang 开发的，由于 V1 已经被官方抛弃了，所以目前的定制化是基于 V2 版本的。</p>
<ul>
<li><a href="#auth-schema">认证结构</a></li>
<li><a href="#auth-flow">认证流程</a>
<ul>
<li><a href="#enable-registry-auth">开启 Registry 认证模式</a>
<ul>
<li><a href="#generate-key">生成密钥</a></li>
<li><a href="#start-registry">启动 Registry</a></li>
</ul>
</li>
<li><a href="#auth-server">Auth Server</a>
<ul>
<li><a href="#generate-jwt-token">生成 jwt token</a>
<ul>
<li><a href="#jwt-claim">Claim</a></li>
<li><a href="#jwt-headers-kid">Headers</a></li>
<li><a href="#jwt-token">token</a></li>
</ul>
</li>
<li><a href="#return-token-to-docker">响应 Docker 客户端</a></li>
</ul>
</li>
<li><a href="#code">具体实现代码</a></li>
</ul>
</li>
<li><a href="#ref">参考</a></li>
</ul>
<div id="auth-schema"></div>
#认证结构
Registry 提供了三种认证方式，本篇中的实现均是基于 token 模式下的。
<p>Registry 提供了松耦合的 Registry 系统，将 Auth Server 与 Registry 的 push/pull 给分离开来了，将 Auth Server 的实现交由用户来实现，个人觉得真是太赞了。<br>
<img src="//cdn.cloverstd.com/images/blog/2016/08/19/v2-registry-auth.png" alt="v2-registry-auth"><br>
上图是官方认证结构图，用户在启动 Registry 并且配置 <code>auth.token</code> 的认证方式时，当用户进行 <code>push/pull/login</code> 的操作时，会到 Auth Server 去认证，由 Auth Server 来判断是否授权通过。基于这一模式，就可以在私有化 Registry 中控制不同的用户（项目）有不同的权限。</p>
<div id="auth-flow"></div>
# 认证流程
<div id="enable-registry-auth"></div>
## 开启 Registry 认证模式
Registry 的认证模式必须在启动 Registry 时指定 `auth.token` 相关的配置才行，下面是配置示例，具体配置项，可以参考 [Registry Configuration](https://docs.docker.com/registry/configuration/)
```yaml
version: 0.1
log:
  level: debug
  fields:
    service: registry
storage:
    cache:
        layerinfo: inmemory
    filesystem:
        rootdirectory: /storage
    maintenance:
        uploadpurging:
            enabled: false
    delete:
        enabled: true
http:
    addr: :5000
    secret: placeholder
auth:
  token:
    issuer: registry-token-issuer
    realm: http://11.11.11.1:8080/service/token
    rootcertbundle: /etc/registry/root.crt
    service: token-service
```
上面的 `auth.token` 就指定了认证是启用 Token 模式来认证，下面是四个字段的说明
<ul>
<li><code>auth.token.issuer</code> : token 发行人，用来在 Auth Server 识别身份</li>
<li><code>auth.token.realm</code> : Auth Server 的 URI</li>
<li><code>auth.token.rootcertbundle</code> : 公钥的绝对路径</li>
<li><code>auth.token.service</code> : 认证的 service，当多个 Registry 公用一个 Auth Server 时，可以区分来自哪里</li>
</ul>
<p>上述的 <code>auth.token.rootcertbundle</code> 可以使用 <code>openssl</code> 来生成，在 Token 认证体系里是需要自己生成私钥和公钥来做 token 的认证的。</p>
<div id="generate-key"></div>
### 生成密钥
<pre><code class="language-shell"># 生成私钥
openssl genrsa -out ./private_key.pem 4096
# 生成公钥
openssl req -new -x509 -key ./private_key.pem -out ./root.crt -days 3650 -subj /C=CN/ST=state/L=CN/O=cloverstd/OU=cloverstd\ unit/CN=hui.lu/emailAddress=i@hui.lu
</code></pre>
<p>上述操作后，会在当前目录生成 <code>private_key.pem</code> 和 <code>root.crt</code></p>
<div id="start-registry"></div>
### 启动 Registry
假设上述的 Registry 的配置文件为 `registry.yaml` 也在当前目录，那么就可以以下列的方式启动 Registry 了
```shell
docker run -it --rm \
    -p 5000:5000 \
    -v `pwd`/root.crt:/etc/registry/root.crt:ro \
    -v `pwd`/registry.yaml:/etc/docker/registry/config.yml:ro \
    registry:2
```
这里并没有用 `-d`，主要是为了调试方便。
<p>在启动后可以试下 <code>docker pull 127.0.0.1:5000/nginx</code> 这个命令，Docker 会返回 <code>getsockopt: connection refused</code> 的错误，因为我们的 Auth Server 还没有启动起来。</p>
<div id="auth-server"></div>
## Auth Server
<p>当执行 <code>pull/login/push</code> 这三个需要认证的命令时，Auth Server 会收到一条 <code>GET</code> 请求，并且会有一些参数。</p>
<p>在上述配置中，我配置的 <code>auth.token.realm</code> 是 <code>http://11.11.11.1:8080/service/token</code>，当我执行 <code>docker pull 127.0.0.1:5000/nginx</code> 时，Auth Server 会收到下面这样的请求</p>
<pre><code class="language-http">GET /service/token?scope=repository:nginx:pull&amp;service=token-service HTTP/1.1
Content-Length:
User-Agent: docker/1.12.0 go/go1.6.3 git-commit/8eab29e kernel/4.4.15-moby os/linux arch/amd64 UpstreamClient(Docker-Client/1.12.0 (darwin))
Connection: close
Host: 11.11.11.1:8080
Content-Type:
Accept-Encoding: gzip

</code></pre>
<p>这是因为 Docker 首先会发一条请求过来确认是否需要认证，这里服务器要给正确的相应，告诉 Docker 是否需要认证，正确的要求 Docker 提供认证服务的请求应该如下</p>
<pre><code class="language-http">HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Docker-Distribution-Api-Version: registry/2.0
Www-Authenticate: Bearer realm=&quot;http://11.11.11.1:8080/service/token&quot;,service=&quot;token-service&quot;,scope=&quot;repository:nginx:pull,push&quot;
Date: Thu, 10 Sep 2015 19:32:31 GMT
Content-Length: 235
Strict-Transport-Security: max-age=31536000

{&quot;errors&quot;:[{&quot;code&quot;:&quot;UNAUTHORIZED&quot;,&quot;message&quot;:&quot;access to the requested resource is not authorized&quot;,&quot;detail&quot;:[{&quot;Type&quot;:&quot;repository&quot;,&quot;Name&quot;:&quot;samalba/my-app&quot;,&quot;Action&quot;:&quot;pull&quot;},{&quot;Type&quot;:&quot;repository&quot;,&quot;Name&quot;:&quot;samalba/my-app&quot;,&quot;Action&quot;:&quot;push&quot;}]}]}
</code></pre>
<p>这里有以下俩要注意</p>
<ul>
<li><code>Www-Authenticate</code> 这里要指出 Auth Server 的地址，还有支持的 scope 操作</li>
<li>在 body 里需要给出错误的原因</li>
<li>HTTP Status Code 必须为 401</li>
</ul>
<p>其实我测了下发现，并没那么严格的要求，只要 HTTP Status Code 为 401 并且 body 的格式相对正确就够了。</p>
<p>然后，Docker 会提示输入用户名和密码，这里我输入 test 和 test，然后服务器会收到跟上述类似的请求，只是多了一个 HTTP Basic Auth 的头 <code>Authorization</code>。通过 base64 解码一下就可以拿到用户名和密码，然后在这里就可以通过各种验证手段（数据库查询等）来验证用户的身份信息了，身份信息认证过了之后，就要认证用户是否有权操作他所要操作的 <code>scope</code> 了</p>
<p><code>scope</code> 根据官方的说明，可能会有多个 <code>scope</code> 作为 URL 参数传送过来，所以这里需要获取到的是一个 <code>scope</code> 的数组，在很多框架里都有实现获取同名参数，作为一个数组。</p>
<p><code>scope</code> 由 <code>Name</code>、<code>Type</code> 和 <code>Actions</code> 组成，并且由 <code>:</code> 分割开来，直接 <code>split(':')</code> 一下就可以得到解析过后的 <code>scope</code> 了，一般来说，<code>scope</code> 会是以下的样子</p>
<pre><code class="language-json">{
    &quot;scopes&quot;: [{
        &quot;type&quot;: &quot;repository&quot;,
        &quot;name&quot;: &quot;nginx&quot;,
        &quot;actions&quot;: [&quot;pull&quot;, &quot;push&quot;]
    }]
}
</code></pre>
<p>然后就可以根据 <code>name</code> 和 <code>actions</code> 来选择用户是否有权操作了，在 Registry 里并没有很明确的用户私有空间这样的概念，通常我们所指定 Registry，例如 <code>127.0.0.1:5000/test/nginx:1.9</code> 是指在 <code>127.0.0.1:5000</code> 这个 Registry 里面，test 用户下的 nginx 镜像的 1.9 版本，其实在 Registry 里，并没有 test 这个用户空间，这里的是 test 完全是 Auth Server 可以自定义的一种命名空间而已，在 Resgitry 里，<code>/test/nginx:1.9</code> 就相当于一段唯一的路由而已。</p>
<div id="generate-jwt-token"></div>
### 生成 jwt token
当权限也通过之后，就是要生成 token 告诉 Docker 一切准备就绪了，Registry 的 token 是使用 [jwt](https://jwt.io/) 生成的。上面我们生成的私钥，在这里就需要用到了，具体的 jwt 原理这里就不复述了，这里只说明生成 token 的流程。
<div id="jwt-claim"></div>
#### Claim
首先是 jwt 的 Claim 构成，如下所示
```python
claim = {
    "iss": self.issuer,
    "sub": self.account,
    "aud": self.service,
    "exp": now + self.token_expires,
    "nbf": now,
    "iat": now,
    "jti": base64.b64encode(os.urandom(1024)), # TODO
    "access": self.access
}
```
其中部分字段的说明如下
<ul>
<li><code>iss</code> 前期通过 <code>registry.yaml</code> 配置文件里的 <code>issuser</code></li>
<li><code>sub</code> 当前操作的帐号或者说用户名</li>
<li><code>aud</code> 前期通过 <code>registry.yaml</code> 配置文件里的 <code>service</code></li>
<li><code>exp</code>、<code>iat</code> 和 <code>nbf</code> 表示的是 token 时间相关的</li>
<li><code>jti</code> 是一段随机字符串</li>
<li><code>access</code> 就是上述解析过后的 <code>scope</code></li>
</ul>
<div id="jwt-headers-kid"></div>
#### Headers
然后就是 jwt 的 Headers 部分了，关键就是 `kid` 的生成，这里的 `kid` 是通过公钥的 `DER` 编码，然后截取  `DER` 编码的 `SHA256` 编码的 digest 的前 240bits，也就是 前 30 位的字符串，然后再通过 base32 编码，并且4个4个用 `:` 分割开来，具体如下
```python
private_key = open('./private_key.pem').read()
private_key = load_pem_private_key(
        private_key,
        password=None,
        backend=default_backend()
)
public_key = private_key.public_key()
<p>der_public_key = public_key.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)</p>
<p>sha256 = hashlib.sha256(der_public_key)<br>
base32_payload = base64.b32encode(sha256.digest()[:30]) # 240bits / 8<br>
res = &quot;:&quot;.join(<br>
[base32_payload[i:i+4] for i in xrange(0, 48, 4)]<br>
)</p>
<pre><code>最后的 `res` 也就是 `kid` 应该是这样的类型的 `PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6`。

&lt;div id=&quot;jwt-token&quot;&gt;&lt;/div&gt;
### token
拿到 `kid` 和 `claim` 后就可以来生成 token 了
```python
token = jwt.encode(claim, private_key, algorithm='RS256', headers=headers)
</code></pre>
<div id="return-token-to-docker"></div>
### 响应 Docker 客户端
然后将 token 返回给 Docker 即可，注意设置 `Content-Type` 为 `application/json`。
<div id="code"></div>
## 具体实现代码
下面是使用 Python 的 `Flask` 实现的
```python
from flask import Flask, request, jsonify, make_response, json
import base64
import jwt
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, load_pem_private_key
import time
import os
import hashlib
<p>app = Flask(<strong>name</strong>)</p>
<p>SERVICE = 'token-service'<br>
ISSUER = 'registry-token-issuer'</p>
<p>class DockerRegistryAuth(object):</p>
<pre><code>def __init__(self, account, scopes, service, issuer, private_key_path, token_expires=300):
    self.account = account
    self.issuer = issuer
    self.scopes = scopes
    self.access = self.get_access_by_scopes(scopes)
    self.service = service
    self.private_key_path = private_key_path
    self.token_expires = token_expires

@property
def private_key(self):
    if getattr(self, '_private_key_content', None):
        return self._private_key_content

    with open(self.private_key_path, 'r') as fp:
        setattr(self, '_private_key_content', fp.read())
        return self._private_key_content

@property
def public_key(self):
    private_key = load_pem_private_key(
            self.private_key,
            password=None,
            backend=default_backend()
    )
    _public_key = private_key.public_key()
    return _public_key

def check_service(self, service):
    return self.service == service

def get_token(self):
    now = int(time.time())

    claim = {
        'iss': self.issuer,
        'sub': self.account,
        'aud': self.service,
        'exp': now + self.token_expires,
        'nbf': now,
        'iat': now,
        'jti': base64.b64encode(os.urandom(1024)),
        'access': self.access
    }

    headers = {
        'kid': self.get_kid()
    }
    token = jwt.encode(claim, self.private_key, algorithm='RS256', headers=headers)
    return {
        'token': token,
        'issued_at': now,
        'expires_in': now + self.token_expires
    }

def get_kid(self):
    der_public_key = self.public_key.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo)

    sha256 = hashlib.sha256(der_public_key)
    base32_payload = base64.b32encode(sha256.digest()[:30]) # 240bits / 8
    return &quot;:&quot;.join(
        [base32_payload[i:i+4] for i in xrange(0, 48, 4)]
    )


def get_login_info(self, authorization):
    if not authorization:
        return None
    auth_info = authorization
    if authorization.startswith('Basic'):
        auth_info = authorization[5:]

    user_info = base64.b64decode(auth_info)
    self.username, self.password = user_info.split(':')
    return {
        'username': self.username,
        'password': self.password,
    }

def get_access_by_scopes(self, scopes):
    access = list()
    if not scopes:
        return access
    for scope in scopes:
        type_, name, actions = scope.split(':')
        access.append({
            'type': type_,
            'name': name,
            'actions': actions.split(',')
        })

    return access
</code></pre>
<p>def unauthorized401(access, message=None, code=None):<br>
detail = list()<br>
for scope in access:<br>
for action in scope['actions']:<br>
detail.append({<br>
&quot;Action&quot;: action,<br>
&quot;Name&quot;: scope['name'],<br>
&quot;Type&quot;: scope['type']<br>
})<br>
data = {<br>
&quot;errors&quot;: [<br>
{<br>
&quot;code&quot;: code or &quot;UNAUTHORIZED&quot;,<br>
&quot;detail&quot;: detail,<br>
&quot;message&quot;: message or &quot;access to the requested resource is not authorized&quot;<br>
}<br>
]<br>
}<br>
resp = make_response(json.dumps(data), 401)<br>
resp.headers['Content-Type'] = 'application/json; charset=utf-8'<br>
resp.headers['Docker-Distribution-Api-Version'] = 'registry/2.0'<br>
resp.headers['Www-Authenticate'] = 'Bearer realm=&quot;<a href="https://auth.docker.io/token%22,service=%22registry.docker.io%22,scope=%22repository:samalba/my-app:pull,push">https://auth.docker.io/token&quot;,service=&quot;registry.docker.io&quot;,scope=&quot;repository:samalba/my-app:pull,push</a>&quot;'<br>
return resp</p>
<p>@app.route('/service/token')<br>
def service_token():<br>
print request.headers<br>
scopes = request.args.getlist('scope')<br>
account = request.args.get('account')<br>
client_id = request.args.get('client_id')<br>
service = request.args.get('service')<br>
http_base_auth = request.headers.get('Authorization')</p>
<pre><code>registry_auth = DockerRegistryAuth(account, scopes, SERVICE, ISSUER, './private_key.pem')
if not registry_auth.check_service(service):
    return unauthorized401(registry_auth.access, 'service not be allowed.')
user = registry_auth.get_login_info(http_base_auth)

if user:
    if not (user['username'] == 'test' and user['password'] == 'test'):
        return unauthorized401(registry_auth.access, 'incorrect username or password')
    res = registry_auth.get_token()
    return jsonify(
        token=res['token'],
    )

return unauthorized401(registry_auth.access)
</code></pre>
<p>if <strong>name</strong> == '<strong>main</strong>':<br>
app.run(host='0.0.0.0', port=8080, debug=True)</p>
<pre><code>
&lt;div id=&quot;ref&quot;&gt;&lt;/div&gt;
# 参考
* https://github.com/vmware/harbor
* https://docs.docker.com/registry
* https://github.com/itrp/docker-token-auth
* https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/
* https://pyjwt.readthedocs.io/en/latest/</code></pre>
</div>]]></content:encoded></item><item><title><![CDATA[用 Docker 部署 Python Web Application]]></title><description><![CDATA[使用 Docker 部署 Flask + Nginx + MySQL + Redis 应用]]></description><link>https://hui.lu/develop-python-web-with-docker/</link><guid isPermaLink="false">59f426890ee5070001939276</guid><category><![CDATA[Docker]]></category><category><![CDATA[Python]]></category><category><![CDATA[Flask]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Tue, 09 Aug 2016 15:03:14 GMT</pubDate><media:content url="//obogic5ph.qnssl.com/images/blog/2016/08/11/docker-banner2.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="//obogic5ph.qnssl.com/images/blog/2016/08/11/docker-banner2.jpg" alt="用 Docker 部署 Python Web Application"><p><small><span style="color:red">Tips: 下面的部分链接中带有小尾巴，谨点</span></small></p>
<p>去年的时候从阿里云捻过一次羊毛（<a href="https://www.aliyun.com/act/aliyun/campus.html">云翼计划</a>，如果愿意，可以是用我的阿里云推荐码，<code>z9xy74</code>）,如今一年已过，这个服务器也过期了，但是上面跑了一个微信企业号的应用在，由于有一直在使用的需求，不能停止，但是阿里云的羊毛是青岛节点，延迟高，而且现在到期了，默认变成了按流量付费，顿时觉得随时会少一套房子了。于是重新买了一个阿里云的华东节点，准备迁移过来，并且也准备把 <a href="https://m.do.co/c/dbb75a208c1c">DigitalOcean</a> 上的部分应用迁移回国。</p>
<ul>
<li><a href="#docker-init">Docker 环境的准备</a></li>
<li><a href="#image-init">镜像的初始化</a>
<ul>
<li><a href="#mysql">MySQL</a></li>
<li><a href="#redis">Redis</a></li>
<li><a href="#web">Web Application</a></li>
<li><a href="#nginx">Nginx</a></li>
</ul>
</li>
<li><a href="#compare-with-before">和之前的比较</a></li>
<li><a href="#some-questions">一些问题</a></li>
<li><a href="#ref">参考</a></li>
</ul>
<div id="docker-init"></div>
# Docker 环境的准备
使用阿里云提供的 [docker-engine](http://mirrors.aliyun.com/help/docker-engine) 加速安装，然后替换 Docker Hub Registry 为阿里云提供的加速器。可参考 [Docker Cluster with Swarm](https://hui.lu/docker-cluster/#make-ubuntu-docker-base-box) 这里安装 Docker 和替换 Registry。
<p>Docker 装好后，然后创建一个 docker 用户来专门管理 Docker 和存放一些配置文件。</p>
<pre><code class="language-bash">sudo useradd -m -g docker -s /bin/bash docker
sudo su docker
mkdir -p ~/conf ~/logs ~/data ~/source
</code></pre>
<div id="image-init"></div>
# 镜像的初始化
我的应用是基于 Python 的，使用 Flask 框架，数据库是 MySQL，然后有用到 Redis 做微信 `Access Token` 的持久化，所以需要先安装 MySQL 和 Redis，最后有用到 Nginx 做代理，这里直接使用的是官方最新镜像
```bash
docker pull mysql
docker pull nginx
docker pull redis
```
因为有用到加速器，所以速度还是杠杠的。
<div id="mysql"></div>
## MySQL
然后是分别将镜像跑起来，为了更灵活的使用，我将 MySQl 的配置文件和数据文件通过 volume 挂载到了宿主机上，这样就算容器挂了，数据还是在的。
下面是 MySQL 的相关配置
```bash
docker run -d --restart=always \
    -v /home/docker/data/docker-mysql:/var/lib/mysql \
    -v /home/docker/conf/mysql:/etc/mysql/conf.d:ro \
    --name mysql \
    mysql
```
上述将 MySQL 的数据文件挂载到了宿主机的 `/home/docker/data/docker-mysql` 上，并且将 MySQL 的配置文件也挂载到了宿主机上，然后在 `/home/docker/conf/mysql` 里有设置 MySQL 的默认编码为 `utf8` 的配置文件 `charset.cnf`，具体内容如下
```ini
[client]
default-character-set = utf8
[mysqld]
character-set-server = utf8
[mysql]
default-character-set = utf8
```
<div id="redis"></div>
## Redis
Redis 的话，因为只是在内存中持久化，所以对数据的保存没有啥要求，对默认配置也没用做修改，所以直接用 Docker 跑起来就可以了
```bash
docker run -d --restart=always \
    --name redis \
    redis
```
<div id="web"></div>
## Web Application
由于用的是 Docker 提供的 `link` 连接容器的，所以在 Nginx 跑起来之前，还需要 Web 先运行起来。
<p>因为用到了 Docker，这一天然的环境隔离神奇，所以对于 Python 之类的各种库依赖来说，直接一个 Docker 镜像打好，然后就随处都可以正常运行了。我没有将我的镜像放到 Docker Hub 或者其他 Registry 上，而是直接写好 Dockerfile，然后上传到服务器上做镜像。下面是我的 Dockerfile</p>
<pre><code class="language-docker">FROM python:2
ADD . /code
WORKDIR /code
RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
CMD gunicorn -c gunicorn.conf wsgi:app
</code></pre>
<p>我的 Web 程序是基于 Flask 的，然后用 gunicorn 使之运行起来，所以我将 <code>gunicorn.conf</code> 也是放在宿主机上，然后通过 volume 挂载到容器内部的。</p>
<pre><code class="language-bash">docker run -d --restart=always \
    -e MODE=PRODUCTION \
    --name app_name
    -v /home/docker/conf/app_name/gunicorn.conf:/code/gunicorn.conf:ro \
    -v /home/docker/conf/focus/production.py:/code/config/production.py:ro \
    -v /home/docker/logs/focus/gunicorn:/var/log/gunicorn \
    --link redis:redis \
    --link mysql:mysql \
    app_name:tag
</code></pre>
<p>其中 <code>/var/log/gunicorn</code> 里有 gunicorn 里写的日志文件，所以也挂载到了宿主机上。然后我的 Web 程序里的配置文件是通过读取环境变量 <code>MODE</code> 来判断是用开发的配置还是正式的配置，其中用到了 Flask 的配置加载，关键代码如下</p>
<pre><code class="language-python">def load_config():
    &quot;&quot;&quot;加载配置类&quot;&quot;&quot;
    mode = os.environ.get('MODE')
    logger = logging.getLogger(&quot;app&quot;)
    try:
        if mode == 'PRODUCTION':
            from .production import ProductionConfig
            enable_pretty_logging({&quot;logging&quot;: &quot;info&quot;})
            logger.info(b'load from production')
            return ProductionConfig
        else:
            from .development import DevelopmentConfig
            enable_pretty_logging()
            logger.info(b'load from development')
            return DevelopmentConfig
    except ImportError, e:
        logger.error(e)
        sys.exit(1)
</code></pre>
<p>对于容器间的访问，我用的是 Docker 提供的 <code>link</code>，所以通过 <code>docker run</code> 提供的 <code>--link</code> 参数，可以让运行的容器通过一个别名访问另一个容器里的网络，其实是 Docker 写入了容器里的 <code>/etc/hosts</code> 文件里。</p>
<div id="nginx"></div>
## Nginx
当一切都运行起来了之后，就是 Nginx 了
```bash
docker run -d --restart=always \
    --name nginx \
    -v /home/docker/logs/nginx:/var/log/nginx \
    -v /home/docker/conf/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \
    -v /home/docker/conf/nginx/conf.d:/etc/nginx/conf.d:ro \
    -p 80:80 \
    -p 433:433 \
    --link app_name:app_name \
    nginx
```
Nginx 的配置文件和日志也通过 volume 挂载到了宿主机上，这样方便操作，如果配置有了更改，直接 `restart` 容器即刻，暂时没找到可以不重启容器而直接 `reload` 配置的方法。
<div id="compare-with-before"></div>
# 和之前的比较
之前部署 Python 的 Web 应用，我都是 Nginx + gunicorn + supervisor，然后加上 virtualenv 来隔离依赖，现在使用 Docker 部署应用，直接省略掉了 supervisor，因为直接使用 Docker 来保持应用的运行。隔离依赖的话，Docker 比 virtualenv 更好，起码是系统级别的隔离了。
<div id="some-questions"></div>
# 一些问题
用 Docker 提供的 `link` 会有一些逻辑上的问题，如果当前容器依赖的 `link` 停止了或者不存在，当前容器也不就不能启动，这样如果 Web 系统复杂起来了，里面存在相互依赖的话，就会出现问题，不过我这太简单，完全不用靠谱 :D。
<p>其实 Docker 有提供 <a href="https://docs.docker.com/compose/overview/">Docker Compose</a> 来管理应用，但是我选择了直接使用 <code>docker run</code>，因为我将 <code>docker run</code> 写到了一个脚本里，所以就不想使用 <a href="https://docs.docker.com/compose/overview/">Docker Compose</a> 来增加更多的东西了。</p>
<div id="ref"></div>
# 参考
* http://hub.docker.com/_/nginx/
* http://hub.docker.com/_/mysql/</div>]]></content:encoded></item><item><title><![CDATA[使用 Tornado 实现 Docker 容器的 exec]]></title><description><![CDATA[使用 Tornado 的 WebSocket 和 Docker API 提供的 exec 功能实现 WebSSH（WebTerminal），前端使用 xterm.js]]></description><link>https://hui.lu/shi-yong-tornado-cao-zuo-docker-api/</link><guid isPermaLink="false">59f426890ee5070001939275</guid><category><![CDATA[Tornado]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Fri, 05 Aug 2016 21:25:33 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>因为工作项目的需求，研究了一下 <a href="https://en.wikipedia.org/wiki/Web-based_SSH">WebSSH</a>。</p>
<ul>
<li><a href="#principle">实现原理</a></li>
<li><a href="#run-bash-with-docker-cli">通过 Docker cli 运行 bash</a></li>
<li><a href="#run-bash-with-docker-api">通过 Docker API 运行 bash</a>
<ul>
<li><a href="#exec-create-api">创建一个  exec 实例</a></li>
<li><a href="#exec-start-api">执行 exec 实例</a></li>
</ul>
</li>
<li><a href="#webssh-with-frontend">与前端结合实现 WebSSH</a></li>
<li><a href="#ref">参考</a></li>
</ul>
<div id="principle"></div>
# 实现原理
由于某些时候，需要通过某种方式进入到容器内部里面去执行一些操作，一般容器并没有提供 SSH 服务，但是有些容器提供了 `bash`，可以通过执行 `bash` 进入到一个交互式的 `shell` 中，执行一些操作。
<div id="run-bash-with-docker-cli"></div>
# 通过 Docker cli 运行 bash
首先，是可以通过 `docker run -it image-name bash` 或者 `docker exec -it container-id bash`的方式在容器里执行一个交互式的 bash，类似于 SSH，但是不是基于 SSH。这里有个问题就是有的镜像并没有提供 `bash` 或者其他 `shell` 的时候，是没有办法通过此方法进入到容器里面去执行一些操作。
<div id="run-bash-with-docker-api"></div>
#通过 Docker API 运行 bash
Docker 有提供 API 来实现 Docker cli 的 `exec` 命令
<ul>
<li>POST /containers/(id or name)/exec</li>
<li>POST /exec/(id)/start</li>
</ul>
<p>首先是在一个容器里创一个 <code>exec</code> 的实例，然后是执行这个 <code>exec</code> 的实例。</p>
<p>但是这里的 <code>/exec/(id)/start</code>，Docker 用到了一个 <code>tcp stream</code> 来交互 <code>stdin</code>、<code>stdout</code> 和 <code>stderr</code>。其实官方的 Python SDK 里是有这个的实现，但是我用的是 Tornado，并没有找到相关的库，于是乎又只能撸轮子了。</p>
<p>下面是一步一步分析，这里假设容器 ID 为 'XXXX'</p>
<div id="exec-create-api"></div>
## 创建一个  exec 实例
<p>这个就是一个简单的 <code>POST</code> 请求。</p>
<pre><code class="language-python">http_client = tornado.httpclient.AsyncHTTPClient()
res = yield http_client.fetch(
    'http://{host}:{port}/containers/{container}/exec'.format(host=host, port=port, container=container),
    method='POST',
    headers={
        'Content-Type': 'application/json',
    },
    body=json.dumps({
        'AttachStdin': True,
        'AttachStdout': True,
        'AttachStderr': True,
        'DetachKeys': 'ctrl-p,ctrl-q',
        'Tty': True,
        'Cmd': [
            '/bin/bash'
        ]
    })
)
data = res.body
print data
exec_id = json.loads(data)['Id']
</code></pre>
<p>通过 <code>/containers/(id or name)/exec</code> 请求，得到一个 <code>exec id</code>。</p>
<div id="exec-start-api"></div>
## 执行 exec 实例
拿到 `exec id` 后，就是执行这个 `exec` 的实例了。
<p>在 Docker 这个 API 实现这里，跟 <code>WebSocket</code> 的实现类似，首先是一个 <code>POST</code> 请求过来（<code>WebSocket</code> 是 <code>GET</code> 请求），然后服务器不会中断这个连接，而是返回给客户端下面这样的 <code>HTTP</code> 报文</p>
<pre><code class="language-HTTP">HTTP/1.1 200 OK
Content-Type: application/vnd.docker.raw-stream
</code></pre>
<p>如果客户端的请求头里有下面两个头的话（其实只需要一个 <code>Upgrade</code>）</p>
<pre><code class="language-HTTP">Upgrade: tcp
Connection: Upgrade
</code></pre>
<p>Docker API 服务器会返回下面这样的 <code>HTTP</code> 报文</p>
<pre><code class="language-HTTP">HTTP/1.1 200 OK
Content-Type: application/vnd.docker.raw-stream
Connection: Upgrade
Upgrade: tcp
</code></pre>
<p>然后 Docker API 服务器就会往这个 <code>tcp stream</code> 里写 <code>stdout</code> 和 <code>stderr</code>（如果请求的时候有这个的需求）的内容，然后也可以往这个 <code>tcp stream</code> 里发送数据到容器里执行的命令的 <code>stdin</code>（如果请求的时候有这个需求）里面。</p>
<p>下面是这个操作的代码</p>
<pre><code class="language-python">tcp_client = tornado.tcpclient.TCPClient()
conn = yield tcp_client.connect(host, port)
data = json.dumps({
    'Detach': False,
    'Tty': True
})
yield conn.write(b'POST /exec/{}/start HTTP/1.1\r\n'.format(exec_id))
yield conn.write(b'Host: 11.11.11.3:2375\r\n')
yield conn.write(b'Connection: Upgrade\r\n')
yield conn.write(b'Content-Type: application/json\r\n')
yield conn.write(b'Upgrade: tcp\r\n')
yield conn.write(b'Content-Length: {}\r\n'.format(len(data)))
yield conn.write(b&quot;\r\n&quot;)
yield conn.write(data.encode('utf-8'))
res = yield conn.read_until(b'\r\n\r\n')
</code></pre>
<p>这里为什么要手动构建 <code>HTTP</code> 请求呢？因为没找到 Tornado 的 <code>HTTPClient</code> 有这种协议的实现，反正我是没找到。</p>
<p>然后就可以通过 <code>conn</code> 来读写数据了，<code>conn</code> 是一个 <code>IOStream</code> 的实例。</p>
<div id="webssh-with-frontend"></div>
# 与前端结合实现 WebSSH
前端我选用的是 [xterm.js](https://github.com/sourcelair/xterm.js)，这个是一个前后端的实现都有，后端是用 node.js 实现的，这里我替换掉了使用 Tornado 提供服务。
<p>具体的实现原理是打开前端页面，与后端建立一个 <code>WebSocket</code> 的连接，然后后端在打开 <code>WebSocket</code> 的连接的同时，通过上述的<a href="#run-bash-with-docker-api">通过 Docker API 运行 bash</a> 建立一个 <code>tcp</code> 连接，并且把前端所有的输入都转发到 <code>tcp</code> 去，然后 <code>tcp</code> 所有的输出都转发到 <code>WebSocket</code> 去。</p>
<p>具体后端代码如下</p>
<pre><code class="language-python">class BashHandler(tornado.websocket.WebSocketHandler):

    @tornado.gen.coroutine
    def open(self):
        print &quot;WebSocket opened&quot;
        container = '0e9bfdfe4639'
        container = '4798e46ea3c'
        host = '11.11.11.3'
        port = 2375
        host = '172.24.6.171'
        port = 4000
        http_client = tornado.httpclient.AsyncHTTPClient()
        res = yield http_client.fetch(
            'http://{host}:{port}/containers/{container}/exec'.format(host=host, port=port, container=container),
            method='POST',
            headers={
                'Content-Type': 'application/json',
            },
            body=json.dumps({
                'AttachStdin': True,
                'AttachStdout': True,
                'AttachStderr': True,
                'DetachKeys': 'ctrl-p,ctrl-q',
                'Tty': True,
                'Cmd': [
                    '/bin/bash'
                ]
            })
        )
        data = res.body
        print data
        exec_id = json.loads(data)['Id']
        print exec_id
        tcp_client = tornado.tcpclient.TCPClient()
        conn = yield tcp_client.connect(host, port)
        data = json.dumps({
            'Detach': False,
            'Tty': True
        })
        yield conn.write(b'POST /exec/{}/start HTTP/1.1\r\n'.format(exec_id))
        yield conn.write(b'Host: 11.11.11.3:2375\r\n')
        yield conn.write(b'Connection: Upgrade\r\n')
        yield conn.write(b'Content-Type: application/json\r\n')
        yield conn.write(b'Upgrade: tcp\r\n')
        yield conn.write(b'Content-Length: {}\r\n'.format(len(data)))
        yield conn.write(b&quot;\r\n&quot;)
        yield conn.write(data.encode('utf-8'))
        res = yield conn.read_until(b'\r\n\r\n')
        print res

        self.termin_conn = conn

        @tornado.gen.coroutine
        def test():
            while True:
                try:
                    data = yield conn.read_bytes(1024, partial=True)
                    self.write_message(data)
                except tornado.iostream.StreamClosedError:
                    self.close()
                    break

        yield test()

    @tornado.gen.coroutine
    def on_message(self, message):
        try:
            yield self.termin_conn.write(message.encode('utf-8'))
        except tornado.iostream.StreamClosedError:
            self.write_message('Terminal has disconnected.')
            self.close()

    def on_close(self):
        try:
            # 这里可能有暗坑，比如进入 vim 后，页面被关闭（WebSocket 关闭）
            # 如果发送 exit 到容器里的话，是没法退出的
            self.termin_conn.write('\nexit\n') # exit bash when ws close
            self.termin_conn.close()
        except tornado.iostream.StreamClosedError:
            pass
        print &quot;close&quot;

    def check_origin(self, origin):
        # just for test
        return True
</code></pre>
<div id="ref"></div>
#参考
* https://github.com/docker/docker/blob/master/api/server/router/container/exec.go
* https://github.com/docker/docker-py/blob/master/docker/api/exec_api.py
* https://docs.docker.com/engine/reference/api/docker\_remote\_api</div>]]></content:encoded></item><item><title><![CDATA[Docker Cluster with Swarm]]></title><description><![CDATA[使用 Swarm 部署 Docker 集群，结合 consul 做发现服务]]></description><link>https://hui.lu/docker-cluster/</link><guid isPermaLink="false">59f426890ee5070001939274</guid><category><![CDATA[Docker]]></category><category><![CDATA[Cluster]]></category><category><![CDATA[Docker Swarm]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Fri, 05 Aug 2016 20:41:35 GMT</pubDate><media:content url="//obogic5ph.qnssl.com/images/blog/2016/08/11/docker-swarm.jpg" medium="image"/><content:encoded><![CDATA[<div class="kg-card-markdown"><img src="//obogic5ph.qnssl.com/images/blog/2016/08/11/docker-swarm.jpg" alt="Docker Cluster with Swarm"><p>最近因为工作的原因，需要用到 Docker，并且搭建 Docker Cluster（集群），然后首先学习了一下 Docker Swarm，这里做一下记录。</p>
<ul>
<li><a href="#prepare-vagrant">准备工作</a>
<ul>
<li><a href="#make-ubuntu-docker-base-box">制作基础镜像</a></li>
<li><a href="#vagrant-init-multiple">初始化多台虚拟机</a></li>
</ul>
</li>
<li><a href="#docker-swarm">Docker Swarm</a>
<ul>
<li><a href="#discovery">发现服务</a>
<ul>
<li><a href="#discovery-docker-hub">Docker Hub</a></li>
<li><a href="#discovery-consul">Consul</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#ref">参考</a></li>
</ul>
<div id="prepare-vagrant"></div>
#准备工作
由于是在本机模拟集群，所以我采用的是 Vagrant 和 VirtualBox 创建虚拟机来模拟集群。
假设本机已经装好了 Vagrant。
<div id="make-ubuntu-docker-base-box"></div>
##制作基础镜像
本次模拟我采用的虚拟机系统是 Ubuntu 14.04，本来是应该通过下面命令完成的
```bash
vagrant init ubuntu/trusty64; vagrant up --provider virtualbox
```
但是因为国内的这网络状况，从 hashicorp 基本是很难拉下 box，虽然一个 box 也就几百兆，所以选择手动从 hashicorp 下载 box，下载地址是 [ubuntu/trusty64 ](https://atlas.hashicorp.com/ubuntu/boxes/trusty64)，现在在页面就有 box 的真实下载地址了（以前是没有的）。
<p>下载好 box 后，然后就是把基础镜像初始化，并且在此基础上做一些准备工作，<br>
添加下载的 box 作为基础镜像，在此基础镜像上替换 ubuntu 源为国内镜像，并且安装 Docker，然后设置 Docker 的 registry mirror 为阿里云的加速。</p>
<pre><code class="language-bash">vagrant box add --name ubuntu-base ./yourbox.box
vagrant init ubuntu-base # 初始化 Vagrantfile
vagrant up # 启动虚拟机
</code></pre>
<p>启动虚拟机后，通过 <code>vagrant ssh</code> 登录到虚拟机里，替换 apt-get 镜像为<a href="https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/">清华源</a>，然后通过阿里云的 <a href="http://mirrors.aliyun.com/help/docker-engine">Docker-Engine</a> 加速来安装 Docker。</p>
<pre><code class="language-bash">curl -sSL http://acs-public-mirror.oss-cn-hangzhou.aliyuncs.com/docker-engine/internet | sh -
sudo usermod -aG docker vagrant # 添加 vagrant 用户到 docker 管理组里
</code></pre>
<p>安装好之后，在阿里云 <a href="https://cr.console.aliyun.com">Docker 控制台</a>找到自己的 Docker Hub 加速地址，类似于<code>https://XXXXXX.mirror.aliyuncs.com</code>，然后通过下面的命令替换 register-mirror</p>
<pre><code class="language-bash">echo &quot;DOCKER_OPTS=\&quot;\$DOCKER_OPTS --registry-mirror=https://ade5ipeh.mirror.aliyuncs.com\&quot;&quot; | sudo tee -a /etc/default/docker 
echo &quot;DOCKER_OPTS=\&quot;\$DOCKER_OPTS -H 0.0.0.0:2375 -H unix:///var/run/docker.sock\&quot;&quot; | sudo tee -a /etc/default/docker # 这里的作用是添加一个 2375 的端口作为 docker manager 的 socket 端口
sudo service docker restart
</code></pre>
<p>本来是打算制作一个基础镜像，然后在此基础上，复制4个出来，然而这里有个巨坑的地方在，复制的镜像，跟原镜像一模一样，这样就导致后面通过 Swarm 创建集群的时候，节点 join 的时候会出现错误，<code>level=error msg=&quot;ID duplicated&quot;</code>，而 Docker 有一个唯一 ID，这个 ID 是保存在 <code>/etc/docker/key.json</code> 里在。所以需要做以下操作</p>
<pre><code class="language-bash"># https://github.com/docker/docker/issues/13278
sudo rm /etc/docker/key.json
</code></pre>
<p>然后可以通过 <code>docker -H :2375 info</code> 测试一下 Docker 是否正常工作，然后运行一个容器测试 <code>docker -H :2375 run hello-world</code>。</p>
<div id="vagrant-init-multiple"></div>
##初始化多台虚拟机
然后是打包基础镜像
```bash
vagrant package --output ubuntu-docker.box
vagrant box add --name ubuntu-docker-base ./ubuntu-docker.box
```
然后创建一个 `Vagrantfile`，内容如下
```ruby
Vagrant.configure("2") do |config|
  config.vm.synced_folder ".", "/vagrant", disabled: true
  config.vm.define :cluster0 do| cluster0_config|
    cluster0_config.vm.box = 'ubuntu-docker-base'
    cluster0_config.vm.hostname = 'cluster0'
    cluster0_config.vm.network "private_network", ip: "11.11.11.100"
  end
<p>config.vm.define :cluster1 do| cluster1_config|<br>
cluster1_config.vm.box = 'ubuntu-docker-base'<br>
cluster1_config.vm.hostname = 'cluster1'<br>
cluster1_config.vm.network &quot;private_network&quot;, ip: &quot;11.11.11.101&quot;<br>
end</p>
<p>config.vm.define :cluster2 do| cluster2_config|<br>
cluster2_config.vm.box = 'ubuntu-docker-base'<br>
cluster2_config.vm.hostname = 'cluster2'<br>
cluster2_config.vm.network &quot;private_network&quot;, ip: &quot;11.11.11.102&quot;<br>
end</p>
<p>config.vm.define :cluster3 do| cluster3_config|<br>
cluster3_config.vm.box = 'ubuntu-docker-base'<br>
cluster3_config.vm.hostname = 'cluster3'<br>
cluster3_config.vm.network &quot;private_network&quot;, ip: &quot;11.11.11.103&quot;<br>
end</p>
<p>config.vm.define :cluster4 do| cluster4_config|<br>
cluster4_config.vm.box = 'ubuntu-docker-base'<br>
cluster4_config.vm.hostname = 'cluster4'<br>
cluster4_config.vm.network &quot;private_network&quot;, ip: &quot;11.11.11.104&quot;<br>
end<br>
end</p>
<pre><code>这个配置文件的作用是基于 `ubuntu-docker-base` 这个基础 box，创建了5个虚拟机，并且设置了 `hostname` 和独立的内网 IP（为了互相能访问）。
&lt;div id=&quot;docker-swarm&quot;&gt;&lt;/div&gt;
#Docker Swarm
&lt;div id=&quot;discovery&quot;&gt;&lt;/div&gt;
##发现服务
发现服务的作用是让 swarm manage 知道有新的节点加入进来了，Docker Swarm 有提供下列这几种发现服务的支持

* Docker Hub
* key/value store
    * Consul
    * Etcd
    * ZooKeeper
* A static file or list of nodes
&lt;div id=&quot;discovery-docker-hub&quot;&gt;&lt;/div&gt;
###基于 Docker Hub 的发现服务
**使用 Docker Hub 作为发现服务是不推荐作为生产环境使用的**

现在假设使用虚拟机中的其中三台，使用情况如下

&lt;table&gt;
&lt;tr&gt;
&lt;th&gt;hostname&lt;/th&gt;
&lt;th&gt;role&lt;/th&gt;
&lt;th&gt;IP&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cluster0&lt;/td&gt;
&lt;td&gt;manager&lt;/td&gt;
&lt;td&gt;11.11.11.100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cluster1&lt;/td&gt;
&lt;td&gt;node0&lt;/td&gt;
&lt;td&gt;11.11.11.101&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cluster2&lt;/td&gt;
&lt;td&gt;node1&lt;/td&gt;
&lt;td&gt;11.11.11.102&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

首先是通过 swarm 创建一个 token
```bash
docker -H 11.11.11.100 run --rm swarm create
</code></pre>
<p>因为我用的是虚拟机，我并不想每次都 ssh 到虚拟机里去操作，这样很麻烦，然后可以在本机（Macbook）上使用 docker 的 <code>-H</code> 参数指定 docker 使用的 dockerd 的服务。</p>
<p>通过上述命令，可以拿到一个 token，假设为 <code>cd26cb761cb03e2ed8c0a25effda322c</code>，然后分别执行下面的操作</p>
<pre><code class="language-bash"># cluster0
docker -H 11.11.11.100 run -d -p 4000:4000 swarm manage -H :4000 token://cd26cb761cb03e2ed8c0a25effda322c
# cluster1
docker -H 11.11.11.101 run -d swarm join --addr=11.11.11.101:2375 token://cd26cb761cb03e2ed8c0a25effda322c
# cluster2
docker -H 11.11.11.102 run -d swarm join --addr=11.11.11.102:2375 token://cd26cb761cb03e2ed8c0a25effda322c
</code></pre>
<p>通过上述操作后，理论上已经在 <code>cluster0</code> 上建立了一个 swarm manage 的服务，在 <code>cluster1</code> 和 <code>cluster2</code> 两个节点上分别建立了 swarm node，可以通过 <code>docker -H 11.11.11.100:4000 info</code> 查看 swarm 的信息，理论上可以看到下列的信息</p>
<pre><code>Containers: 4
 Running: 2
 Paused: 0
 Stopped: 2
Images: 4
Server Version: swarm/1.2.4
Role: primary
Strategy: spread
Filters: health, port, containerslots, dependency, affinity, constraint
Nodes: 2
 cluster1: 11.11.11.101:2375
  └ ID: JSLI:JY6O:6GZI:LXRT:RQJ5:NZXN:FBMU:XUEB:CTSR:PEPU:ORVH:2A4T
  └ Status: Healthy
  └ Containers: 2 (1 Running, 0 Paused, 1 Stopped)
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 514.5 MiB
  └ Labels: kernelversion=3.13.0-93-generic, operatingsystem=Ubuntu 14.04.5 LTS, storagedriver=aufs
  └ UpdatedAt: 2016-08-05T18:39:49Z
  └ ServerVersion: 1.12.0
 cluster2: 11.11.11.102:2375
  └ ID: 2HZ6:6ZKW:S7XZ:LLTW:BQTS:YHFO:ELXC:6ZKM:4HHL:C6UO:IHE5:IG34
  └ Status: Healthy
  └ Containers: 2 (1 Running, 0 Paused, 1 Stopped)
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 514.5 MiB
  └ Labels: kernelversion=3.13.0-93-generic, operatingsystem=Ubuntu 14.04.5 LTS, storagedriver=aufs
  └ UpdatedAt: 2016-08-05T18:40:01Z
  └ ServerVersion: 1.12.0
Plugins:
 Volume:
 Network:
Swarm:
 NodeID:
 Is Manager: false
 Node Address:
Security Options:
Kernel Version: 3.13.0-93-generic
Operating System: linux
Architecture: amd64
CPUs: 2
Total Memory: 1.005 GiB
Name: b7b4f78090ac
Docker Root Dir:
Debug Mode (client): false
Debug Mode (server): false
</code></pre>
<p>通过 <code>docker -H 11.11.11.100:4000 run -P -d nginx</code> 可以在集群里运行一个 <code>nginx</code> 的容器，然后通过 <code>docker -H 11.11.11.100:4000 ps</code> 可以看到集群里的正在运行的容器，里面可以看到 <code>nginx</code> 具体是跑在哪一个容器里面在。</p>
<div id="discovery-consul"></div>
###使用 Consul 做发现服务
<table>
<tr>
<th>hostname</th>
<th>role</th>
<th>IP</th>
</tr>
<tr>
<td>cluster0</td>
<td>consul</td>
<td>11.11.11.100</td>
</tr>
<tr>
<td>cluster1</td>
<td>manager0</td>
<td>11.11.11.101</td>
</tr>
<tr>
<td>cluster2</td>
<td>manager1</td>
<td>11.11.11.102</td>
</tr>
<tr>
<td>cluster3</td>
<td>node0</td>
<td>11.11.11.103</td>
</tr>
<tr>
<td>cluster4</td>
<td>node1</td>
<td>11.11.11.104</td>
</tr>
</table>
<p>可以通过 <code>vagrant destory</code> 销毁所有的虚拟机（本机就是可以这么任性，随便搞），然后再通过 <code>vagrant up</code> 来再次启动虚拟机。</p>
<p>然后顺序执行下列操作来创建集群</p>
<pre><code class="language-bash"># cluster0
docker -H 11.11.11.100 run -d -p 8500:8500 --name=consul progrium/consul -server -bootstrap
# cluster1
docker -H 11.11.11.101 run -d -p 4000:4000 swarm manage -H :4000 --replication --advertise 11.11.11.101:4000 consul://11.11.11.100:8500
# cluster2
docker -H 11.11.11.102 run -d -p 4000:4000 swarm manage -H :4000 --replication --advertise 11.11.11.102:4000 consul://11.11.11.100:8500
# cluster3
docker -H 11.11.11.103 run -d swarm join --advertise=11.11.11.103:2375 consul://11.11.11.100:8500
# cluster4
docker -H 11.11.11.104 run -d swarm join --advertise=11.11.11.104:2375 consul://11.11.11.100:8500
</code></pre>
<p>可以通过 <a href="http://11.11.11.100:8500">http://11.11.11.100:8500</a> 进入 consul 提供的一个 Web 界面来查看 consul 的一些信息，Swarm 是通过使用 consul 的 key/value 来做发现服务的，所以在 consul 的 key/value 里是可以看到 swarm manage 的 leader 和 swarm node 的信息。</p>
<p>通过上述多节点的 manager，实现了 swarm manage 的高可用，如果干掉 manage leader，会发现 leader 自动切换到了另一个 manager。</p>
<div id="ref"></div>
#参考
没有下列的这些文章或者服务的帮助，我是无法完成上述这些操作的，特此感谢
<ul>
<li><a href="https://docs.docker.com/swarm/install-w-machine/">https://docs.docker.com/swarm/install-w-machine/</a></li>
<li><a href="https://docs.docker.com/swarm/install-manual/">https://docs.docker.com/swarm/install-manual/</a></li>
<li><a href="http://mirrors.aliyun.com/help/docker-engine">http://mirrors.aliyun.com/help/docker-engine</a></li>
<li><a href="https://github.com/docker/docker/issues/13278">https://github.com/docker/docker/issues/13278</a></li>
<li><a href="https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/">https://mirrors.tuna.tsinghua.edu.cn/help/ubuntu/</a></li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[为 Tornado 增加 Session]]></title><description><![CDATA[<div class="kg-card-markdown"><p>Tornado 是我非常喜欢的一个框架，但是它缺失了很多功能模块，比如说 Session，正因为它啥都没有，所以我就爱上它了，这样可以方便自己撸轮子。:D</p>
<ul>
<li><a href="#session-principle">Session 原理</a></li>
<li><a href="#tornado-session">为 Tornado 添加 Session</a>
<ul>
<li><a href="#sessionmixin">SessionMixin</a></li>
<li><a href="#with-tornado">结合 Tornado</a></li>
<li><a href="#session-storage">Session Storage</a></li>
</ul>
</li>
<li><a href="#thanks">感谢</a></li>
</ul>
<div id="session-principle"></div>
# Session 原理
<p>Session 是由于 HTTP 协议是无状态的，所以需要一种机制来保持客户端和服务器之间的会话。</p>
<p>HTTP 协议的流程是</p>
<pre><code>Client -&gt; Send Request -&gt; Server -&gt; Return Response
</code></pre>
<p>上述过程，是一次性的，也就是说当客户端发送请求直到服务器返回响应之后，客户端和服务器之间就再也没有任何联系了。于是一般人都会想到，在每次请求时带上一个特定的标识符，然后服务器端记录下来，每次根据特定的标志符就可以知道客户端的身份了。</p></div>]]></description><link>https://hui.lu/tornado-session-extension/</link><guid isPermaLink="false">59f426890ee5070001939273</guid><category><![CDATA[Tornado]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Tue, 19 Jul 2016 11:29:21 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>Tornado 是我非常喜欢的一个框架，但是它缺失了很多功能模块，比如说 Session，正因为它啥都没有，所以我就爱上它了，这样可以方便自己撸轮子。:D</p>
<ul>
<li><a href="#session-principle">Session 原理</a></li>
<li><a href="#tornado-session">为 Tornado 添加 Session</a>
<ul>
<li><a href="#sessionmixin">SessionMixin</a></li>
<li><a href="#with-tornado">结合 Tornado</a></li>
<li><a href="#session-storage">Session Storage</a></li>
</ul>
</li>
<li><a href="#thanks">感谢</a></li>
</ul>
<div id="session-principle"></div>
# Session 原理
<p>Session 是由于 HTTP 协议是无状态的，所以需要一种机制来保持客户端和服务器之间的会话。</p>
<p>HTTP 协议的流程是</p>
<pre><code>Client -&gt; Send Request -&gt; Server -&gt; Return Response
</code></pre>
<p>上述过程，是一次性的，也就是说当客户端发送请求直到服务器返回响应之后，客户端和服务器之间就再也没有任何联系了。于是一般人都会想到，在每次请求时带上一个特定的标识符，然后服务器端记录下来，每次根据特定的标志符就可以知道客户端的身份了。那么，这个特定的标志符就是 session id。这个就是大致上 session 的原理了。</p>
<p>一般情况下，session id 是储存在 cookie 里，并且通过 HTTP Header 传递给服务器，当然，也可以作为 HTTP Query parameters，只是这样个人觉得不太优雅，当然也有一些不安全的因素存在。</p>
<div id="tornado-session"></div>
# 为 Tornado 添加 Session
知道了 Session 的原理后，就可以很方便的为 Tornado 添加 Session 机制了。
<div id="sessionmixin"></div>
## SessionMixin
下面是一个 SessionMixin 类，作用是开启 Session 和保存 Session，和 PHP 的 `session_start` 简直是一样一样的作用，其中 `self.session_storage` 是服务器的 Session 储存容器。
``` python
class SessionMixin(object):
<pre><code>def open_session(self):
    self._session_name = self.get_secure_cookie('tornado-session')
    self.session = self.session_storage.get(self._session_name) or {}
    self._session_age = 3600 * 24 * 31

def set_session_age(self, expires):
    self._session_age = expires

def save_session(self):
    if not getattr(self, '_session_name'):
        self._session_name = str(uuid.uuid4())
        self.set_secure_cookie('wing2-session', self._session_name)
    self.session_storage.set(self._session_name, self.session, self._session_age)
</code></pre>
<pre><code>&lt;div id=&quot;with-tornado&quot;&gt;&lt;/div&gt;
## 结合 Tornado
有了 `SessionMixin` 之后，就是结合 Tornado 的使用了。
``` python
class BaseHandler(tornado.web.RequestHandler, SessionMixin):
    session_storage = dict()

    def prepare(self):
        self.open_session()

        if self.session.get('user_id'):
            user_id = self.session['user_id']
            self.current_user = UserModel.get_by_user_id(user_id)

    def finish(self, chunk=None):

        self.save_session()
        super(BaseHandler, self).finish(chunk)
</code></pre>
<p>上面是初始化 Session，然后继承 <code>BaseHandler</code>，并且在 RequestHandler 里使用 <code>self.session</code> 就可以操作 Session 了。</p>
<div id="session-storage"></div>
## Session Storage
上述的 Session 机制是一个单机版，并且重启程序无效，而且如果通过 Nginx 反代了多个 Tornado Server 的话，Session 并不能公用，为了解决这个，我们可以使用 Redis 来作为 Session 的容器。
<p>首先是一个抽象的 Session Storage 的类</p>
<pre><code class="language-python">class SessionStorage(object):

    def get(self, key, default=None):
        raise NotImplemented

    def set(self, key, value, expires=None):
        raise  NotImplemented

    def delete(self, key):
        raise NotImplemented
</code></pre>
<p>基本实现上述 3 个方法，就可以成为一个 Session 的容器了。</p>
<p>以下是 Redis 作为容器的示例</p>
<pre><code class="language-python">class RedisStorage(SessionStorage):

    def __init__(self, redis, prefix=&quot;wing2&quot;):
        self._redis = redis
        self._prefix = prefix

    def _wrapper(self, key):
        return &quot;session:{prefix}:{key}&quot;.format(
            prefix=self._prefix,
            key=key
        )

    def get(self, key, default=None):
        key = self._wrapper(key)
        value = self._redis.get(key)
        if not value:
            return default
        value = pickle.loads(value)
        return value

    def set(self, key, value, expires=None):
        key = self._wrapper(key)
        value = pickle.dumps(value)

        return self._redis.setex(key, value, expires or 86400)  # default one day

    def delete(self, key):
        key = self._wrapper(key)
        return self._redis.delete(key)
</code></pre>
<p>以上就是为 Tornado 添加 Session 的示例。</p>
<div id="thanks"></div>
# 感谢
* Flask Session (https://github.com/pallets/flask/blob/master/flask/sessions.py)
<p>EOF</p>
</div>]]></content:encoded></item><item><title><![CDATA[Flask 用户权限划分]]></title><description><![CDATA[<div class="kg-card-markdown"><p>最近学习了下用户权限划分的数据库结构，并且结合到了 Flask 和 SQLAlchemy 中</p>
<ul>
<li><a href="#basic-table">基础表</a>
<ul>
<li><a href="#user-table">用户表</a></li>
<li><a href="#role-table">角色表</a></li>
<li><a href="#permission-table">权限表</a></li>
<li><a href="#menu-table">菜单表</a></li>
</ul>
</li>
<li><a href="#relationship-table">关联表</a>
<ul>
<li><a href="#user-role-table">用户角色表</a></li>
<li><a href="#role-permission-table">角色权限表</a></li>
<li><a href="#role-menu-table">角色菜单表</a></li>
</ul>
</li>
<li><a href="#sqlalchemy">SQLAlchemy</a></li>
<li><a href="#with-flask">与 Flask 结合</a></li>
</ul>
<p>首先是数据库的整体结构图（简化版）<br>
<img src="//cdn.cloverstd.com/images/blog/2017/05/21/qq20160423-1-2x.png" alt=""></p>
<div id="basic-table"></div>
### 基础表
<div id="user-table"></div>
#### 用户表
<pre><code class="language-python">class UserModel(db.Model):
    __tablename__ = 'user'
    username = db.Column(db.String(50))
    password = db.Column(db.String(128))
    email = db.Column(db.String(128))
    mobile = db.Column(</code></pre></div>]]></description><link>https://hui.lu/yong-hu-shu-ju-ku-biao-jie-gou-hua-fen/</link><guid isPermaLink="false">59f426890ee5070001939272</guid><category><![CDATA[Flask]]></category><category><![CDATA[SQLAlchemy]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Sat, 23 Apr 2016 08:25:11 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>最近学习了下用户权限划分的数据库结构，并且结合到了 Flask 和 SQLAlchemy 中</p>
<ul>
<li><a href="#basic-table">基础表</a>
<ul>
<li><a href="#user-table">用户表</a></li>
<li><a href="#role-table">角色表</a></li>
<li><a href="#permission-table">权限表</a></li>
<li><a href="#menu-table">菜单表</a></li>
</ul>
</li>
<li><a href="#relationship-table">关联表</a>
<ul>
<li><a href="#user-role-table">用户角色表</a></li>
<li><a href="#role-permission-table">角色权限表</a></li>
<li><a href="#role-menu-table">角色菜单表</a></li>
</ul>
</li>
<li><a href="#sqlalchemy">SQLAlchemy</a></li>
<li><a href="#with-flask">与 Flask 结合</a></li>
</ul>
<p>首先是数据库的整体结构图（简化版）<br>
<img src="//cdn.cloverstd.com/images/blog/2017/05/21/qq20160423-1-2x.png" alt=""></p>
<div id="basic-table"></div>
### 基础表
<div id="user-table"></div>
#### 用户表
<pre><code class="language-python">class UserModel(db.Model):
    __tablename__ = 'user'
    username = db.Column(db.String(50))
    password = db.Column(db.String(128))
    email = db.Column(db.String(128))
    mobile = db.Column(db.String(11))
    name = db.Column(db.String(50))
    gender = db.Column(db.SmallInteger)  # 0 未知， 1 男 2 女
</code></pre>
<div id="role-table"></div>
#### 角色表
<pre><code class="language-python">class RoleModel(db.Model):
    __tablename__ = 'role'
    name = db.Column(db.String(20))
</code></pre>
<div id="permission-table"></div>
#### 权限表
<pre><code class="language-python">class PermissionModel(db.Model):
    __tablename__ = 'permission'
    name = db.Column(db.String(50))
    action = db.Column(db.String(250), unique=True)
</code></pre>
<div id="menu-table"></div>
#### 菜单表
<pre><code class="language-python">class MenuModel(db.Model):
    __tablename__ = 'menu'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))
    icon = db.Column(db.String(50))
    url = db.Column(db.String(250))
    order = db.Column(db.SmallInteger, default=0)
    bg_color = db.Column(db.String(50))
</code></pre>
<div id="relationship-table"></div>
### 关联表
<p>基础表完了就是关联表了</p>
<div id="user-role-table"></div>
#### 用户角色表
<p>用户跟角色，肯定是多对多的关系，按照 Flask-SQLAlchemy 里的 <a href="http://flask-sqlalchemy.pocoo.org/2.1/models/#many-to-many-relationships">Many-to-Many Relationships</a></p>
<pre><code class="language-python">user_role = db.Table('user_role',  # 用户角色关联表
    db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
    db.Column('role_id', db.Integer, db.ForeignKey('role.id')),
    db.Column('created_at', db.DateTime, default=datetime.now),
)
</code></pre>
<div id="role-permission-table"></div>
#### 角色权限表
<p>这里把权限挂在了角色下面，其实也可以去掉角色，直接跟用户挂钩，但是如果后期在后台分配用户权限，估计会累死。这里角色跟权限也是多对多</p>
<pre><code class="language-python">role_permission = db.Table('role_permission',  # 角色权限关联表
    db.Column('permission_id', db.Integer, db.ForeignKey('permission.id')),
    db.Column('role_id', db.Integer, db.ForeignKey('role.id')),
    db.Column('created_at', db.DateTime, default=datetime.now),
)
</code></pre>
<div id="role-menu-table"></div>
#### 角色菜单表
<p>同上，也是多对多</p>
<pre><code class="language-python">role_menu = db.Table('role_menu',  # 用户菜单关联表
    db.Column('role_id', db.Integer, db.ForeignKey('role.id')),
    db.Column('menu_id', db.Integer, db.ForeignKey('menu.id')),
    db.Column('created_at', db.DateTime, default=datetime.now),
    db.Column('is_delete', db.Boolean, default=False),
)
</code></pre>
<h3 id="sqlalchemy">SQLAlchemy</h3>
<p>如果需要获取一个用户的角色，可以利用<code>relationship</code>，关联到角色表上</p>
<pre><code class="language-python">class UserModel(db.Model):
    # ...
    roles = db.relationship(
        'RoleModel',
        secondary=user_role,
        backref=db.backref(
            'users',
            lazy='dynamic'
        )
    )
</code></pre>
<p>获取用户的所有权限可以用<code>property</code></p>
<pre><code class="language-python">class UserModel(db.Model):
    # ...
    @property
    def permissions(self):
        permissions = PermissionModel.query.join(role_permission).join(RoleModel).join(user_role).join(UserModel).\
            filter(
            UserModel.id == self.id
        )
        return permissions
</code></pre>
<p>同理菜单</p>
<pre><code class="language-python">class UserModel(db.Model):
    # ...
    @property
    def menus(self):
        menus = MenuModel.query.join(role_menu).join(RoleModel).join(user_role).join(UserModel).\
            filter(
            UserModel.id == self.id
        ).order_by(MenuModel.type_, MenuModel.order).all()
        return menus
</code></pre>
<p>这样就可以用<code>user.permissions</code>和<code>user.menus</code>来获得用户的权限和菜单了</p>
<div id="with-flask"></div>
### 与 Flask 结合
<p>数据库表结构设计好了，下面就是跟 Flask 的结合了</p>
<p>在 Python 中，用 decorator 可以用来做用户验证，比如下面</p>
<pre><code class="language-python">def auth(method):
    @functools.wraps(method)
    def wrapper(*args, **kwargs):
        user_id = session.get('user_id')
        if not user_id:
            return abort(403)
        return method(*args, **kwargs)
    return wrapper

@app.router('/user/info')
@auth
def user_info():
    return render_template('user/info.html')
</code></pre>
<p>上面就是利用 Python 的 decorator 来认证用户，其实也是简单的权限划分</p>
<p>因为在 Flask 中，每个 view 就是一个函数，所以在权限表中，用<code>action</code>来表示每个 view 的函数名，那么每个 view 就是一个最小的权限单位，如果一个角色拥有这个权限，那么他就可以请求这个 view 的操作。所以可以这样验证权限</p>
<pre><code class="language-python">class UserModel(db.Model):
    # ...
    def check(self, action):
        permission = self.permissions.filter(PermissionModel.action == action).first()
        return bool(permissions)
</code></pre>
<p>然后把这个权限验证写到 decorator 里去</p>
<pre><code class="language-python">permissions = list()

class Permission(object):

    def __init__(self, module=None, action=None):
        self.module = module
        self.action = action

    def check(self, module, func):
        if not self.current_user:
            return False
        return self.current_user.check('{module}.{action}'.format(
            module=module,
            action=func
        ))

    def deny(self):
        return fail(4003, u'无权访问')

    def __call__(self, func):
        permissions.append({
            'action': '{}.{}'.format(func.__module__, func.__name__),
            'name': func.__doc__
        })
        @wraps(func)
        def decorator(*args, **kwargs):
            if not self.check(func.__module__, func.__name__):
                return self.deny()
            return func(*args, **kwargs)
        return decorator

    def __enter__(self):
        if not self.check(self.module, self.action):
            try:
                self.deny()
            except Exception as e:
                raise e
            else:
                raise PermissionDeniedException

    def __exit(self):
        pass

    @property
    def current_user(self):
        return g.user

permission = Permission()
</code></pre>
<p>这里参考了 hustazp 的 <a href="https://github.com/hustlzp/permission">permission</a></p>
<p>使用 <code>func.__module__</code> 和 <code>func.__name__</code> 结合作为权限中的 <code>action</code>，如果单独用 <code>func.__name</code>，肯定会出现相同的函数名，如果加上 <code>func.__module__</code> 就在一定程度上避免了重合，并且将 <code>func.__doc__</code> 来作为权限种的 <code>name</code>，还没想到更好的办法来自动加入 <code>name</code>。</p>
<p>那么上面的用户认证换成 permission 就是下面</p>
<pre><code class="language-python">@app.router('/user/info')
@permission
def user_info():
    &quot;&quot;&quot;用户信息&quot;&quot;&quot;
    return render_template('user/info.html')
</code></pre>
<p>在开发的过程中，如果写了一个权限就要加到数据库里该有多累，于是就加了一个 <code>permissions</code>，这里把所有的 view 都加到这里面来，然后通过下面的脚本来加入权限</p>
<pre><code class="language-python">from application.models.user import PermissionModel, RoleModel, role_permission
from application.utils.permissions import permissions
for permission in permissions:
    p = PermissionModel.query.filter_by(action=permission['action']).first()
    if not p:
        p = PermissionModel(
            name=permission['name'],
            action=permission['action']
        )
        db.session.add(p)
        db.session.commit()

role = RoleModel.query.first()  # 这里默认获取一个角色，并且赋予权限
for p in PermissionModel.query.filter_by(is_delete=False):
    r = db.session.query(role_permission).join(RoleModel).join(PermissionModel).\
        filter(
            RoleModel.id == role.id,
            RoleModel.is_delete == False,
            PermissionModel.id == p.id,
            PermissionModel.is_delete == False,
            role_permission.c.is_delete == False,
        ).first()
    if not r:
        role.permissions.append(p)

role.save()
</code></pre>
</div>]]></content:encoded></item><item><title><![CDATA[tornado.ioloop 学习（一）]]></title><description><![CDATA[<div class="kg-card-markdown"><p><a href="http://www.tornadoweb.org/">Tornado</a> 是一个 Python 的 Web 框架和一个异步网络库。</p>
<blockquote>
<p>Tornado is a Python web framework and asynchronous networking library</p>
</blockquote>
<p>单纯作 Python 的 Web 框架，<a href="http://www.tornadoweb.org/">Tornado</a> 并没有啥特色，和 <a href="http://webpy.org/">Web.py</a> 的接口类似。话说我学的第一个 Python Web 框架就是 <a href="http://webpy.org/">Web.py</a>，导致我对相似的 <a href="http://www.tornadoweb.org/">Tornado</a> 恋恋不忘。于是乎，我就开始阅读 <a href="http://www.tornadoweb.org/">Tornado</a> 的源代码了。</p>
<p>就像上面的介绍，<a href="http://www.tornadoweb.org/">Tornado</a> 除了是一个 Web 框架，还是一个异步的网络库。而这个库的核心和灵魂就是 torndo.ioloop 了。</p></div>]]></description><link>https://hui.lu/tornado-ioloop-learn-1/</link><guid isPermaLink="false">59f426890ee5070001939271</guid><category><![CDATA[Tornado]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Tue, 05 Apr 2016 15:13:18 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p><a href="http://www.tornadoweb.org/">Tornado</a> 是一个 Python 的 Web 框架和一个异步网络库。</p>
<blockquote>
<p>Tornado is a Python web framework and asynchronous networking library</p>
</blockquote>
<p>单纯作 Python 的 Web 框架，<a href="http://www.tornadoweb.org/">Tornado</a> 并没有啥特色，和 <a href="http://webpy.org/">Web.py</a> 的接口类似。话说我学的第一个 Python Web 框架就是 <a href="http://webpy.org/">Web.py</a>，导致我对相似的 <a href="http://www.tornadoweb.org/">Tornado</a> 恋恋不忘。于是乎，我就开始阅读 <a href="http://www.tornadoweb.org/">Tornado</a> 的源代码了。</p>
<p>就像上面的介绍，<a href="http://www.tornadoweb.org/">Tornado</a> 除了是一个 Web 框架，还是一个异步的网络库。而这个库的核心和灵魂就是 torndo.ioloop 了。</p>
<h2 id="content">Content</h2>
<ul>
<li><a href="#content">Content</a></li>
<li><a href="#socket">socket</a>
<ul>
<li><a href="#server">server</a></li>
<li><a href="#client">client</a></li>
<li><a href="#blockingnetwork">blocking network</a></li>
</ul>
</li>
<li><a href="#socketbasedonthreading">socket based on threading</a></li>
<li><a href="#select">select</a></li>
<li><a href="#epoll">epoll</a></li>
<li><a href="#tornadoioloop">tornado.ioloop</a></li>
<li><a href="#refs">refs</a></li>
</ul>
<h2 id="socket">socket</h2>
<p>作为网络库，就不得不从 socket 说起了。<br>
虽然也是读过大学的人，但是当时网络、操作系统完全等于没学过，只怪自己不努力，所以现在对于 socket，只能从应用的层面去学习了。</p>
<h3 id="server">server</h3>
<pre><code class="language-Python">import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.bind(('127.0.0.1', 5000))
s.listen(1)
print &quot;waiting for connect&quot;
conn, addr = s.accept()
print conn, addr

while True:
    data = conn.recv(1024)
    if len(data) == 1:
        if ord(data) == 4:
            conn.sendall('bye.\n')
            break
    conn.sendall('Hello\n')

s.close()
</code></pre>
<p>首先是通过<code>socket.socket</code>创建一个<code>socket</code>对象，第一参数有三个可选</p>
<ul>
<li><code>socket.AF_UNIX</code> UNIX socket file，但是要系统支持</li>
<li><code>socket.AF_INET</code> IPv4 地址</li>
<li><code>socket.AF_INET6</code> IPv6 地址</li>
</ul>
<p>第二个参数，指定 socket 类型，有很多类型可选，但是常用的是下面两个</p>
<ul>
<li><code>socket.SOCK_STREAM</code> TCP 协议</li>
<li><code>socket.SOCK_DGRAM</code> UDP 协议</li>
</ul>
<p>然后通过<code>s.bind</code>来绑定<code>socket</code>监听的地址和端口，如果需要让所有地址都能连接上，可以指定<code>0.0.0.0</code></p>
<p>最后通过<code>s.accept</code>开始等待客户端连接上，这里是阻塞的，意思就是如果没有客户端连接上，后面的代码是永远也不会执行的。</p>
<p>下面通过<code>telnet 127.0.0.1 5000</code>来测试上面的效果</p>
<pre><code class="language-shell">&gt; telnet 127.0.0.1 5000
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello
Hello
hi
Hello
bye.
Connection closed by foreign host.
</code></pre>
<p>当连接<code>telnet</code>连接上 server 时，代码就会进入循环中，等待客户端发送数据，当客户端发送了数据之后，server 就会向客户端发送<code>hello</code>，这里的<code>s.recv</code>也是阻塞的。</p>
<p>最后如果收到了客户端发来的<code>EOF</code>（C-D）的时候，就会退出循环，然后通过<code>s.close</code>关闭 socket 后退出程序。</p>
<h3 id="client">client</h3>
<p>上面是通过<code>telnet</code>作为客户端来测试，下面通过 Python 里的 socket 来建立链接</p>
<pre><code class="language-python">import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect(('127.0.0.1', 5000))
s.sendall('hello')
data = s.recv(1024)
s.sendall(chr(4))
s.close()
print data

</code></pre>
<p>跟 server 一样，首先是初始化<code>socket</code>对象，然后通过<code>s.connect</code>连接到 server，接下来通过<code>s.sendall</code>发送数据到 server，这里的<code>s.connect</code>和<code>s.recv</code>也是阻塞的。</p>
<h3 id="blockingnetwork">blocking network</h3>
<p>上面的 server 和 client 都是阻塞的，或者说 server 只能接受一个 client 的连接，client 也只能连接到一个 server 上去。</p>
<p>但是实际情况比如一个网站（web server）是可以同时多个人浏览，web server 是 HTTP 协议，而 HTTP  协议是基于 TCP（socket）的，并且不止是接受一个连接。</p>
<h2 id="socketbasedonthreading">socket based on threading</h2>
<p>下面是 server 的例子</p>
<pre><code class="language-python">import socket
import threading

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.bind(('127.0.0.1', 5000))
s.listen(5)
print &quot;waiting for connect&quot;


class Server(threading.Thread):

    def __init__(self, sock):
        self.sock = sock
        super(Server, self).__init__()

    def run(self):
        conn, addr = self.sock.accept()
        print &quot;client {} joined\n&quot;.format(addr)
        while True:
            data = conn.recv(1024)
            if len(data) == 1:
                if ord(data) == 4:
                    break
            print &quot;data {} from {}&quot;.format(data, addr)
            conn.sendall('Hello\n')
        self.sock.close()
        print &quot;client {} left\n&quot;.format(addr)

thread = list()
for i in xrange(5):
    t = Server(s)
    thread.append(t)
    t.start()

</code></pre>
<p>通过<code>threading</code>来启动了5个<code>socket.accept</code>来接收 client 的连接，通过<code>telnet</code>可以同时启动5个 client，并且可以同时工作，如果需要接受更多的 client，可以增加线程数，不过增加线程带来的结果就是内存飚升，会影响性能，据说 windows 下有限制子线程数，也就是限制了客户端的连接数。</p>
<h2 id="select">select</h2>
<p>为了解决阻塞的问题，牛逼的人类发明了<code>select</code>这个系统调用，多路 I/O 复用，可以在单线程里同时处理多个 socket 连接。在类 Unix 系统中，网络被抽象成了文件，可读可写，所以作为一个文件描述符，可以被<code>select</code>这个系统调用监视。</p>
<pre><code class="language-python">import socket
import select

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.bind(('127.0.0.1', 5000))
s.listen(5)
rlist = [s]
wlist = []
message = dict()

print &quot;waiting connect&quot;
while rlist:
    readable, writable, exceptional = select.select(rlist, wlist, rlist)
    for sock in readable:
        if sock is s:
            conn, addr = s.accept()
            print &quot;client {} joined.&quot;.format(addr)
            message[conn] = list()
            rlist.append(conn)
        else:  # conn
            data = sock.recv(1024)
            if len(data) == 1:
                if ord(data) == 4:
                    sock.close()
                    if sock in wlist:
                        wlist.remove(sock)
                    if sock in rlist:
                        rlist.remove(sock)
                    if sock in message:
                        del message[sock]
                    print &quot;client {} left.&quot;.format(addr)
                    continue
            message[sock].append('Hello\n')
            if sock not in wlist:
                wlist.append(sock)

    for sock in writable:
        if sock in message:
            if message[sock]:
                data = message[sock].pop()
                if data:
                    sock.sendall(data)

    for sock in exceptional:
        rlist.remove(sock)
        print &quot;error occur.&quot;
        sock.close()
        del message[sock]

</code></pre>
<p>上面代码看着变得复杂起来了，其实细细看来还是很好理解的，<code>select.select</code>接受三个参数，</p>
<ul>
<li><code>rlist</code> 可读的文件描述符</li>
<li><code>wlist</code>  可写的文件描述符</li>
<li><code>xlist</code>  异常的对象</li>
</ul>
<p>并且<code>select.select</code> 会阻塞，直到有可操作的对象（文件描述符），然后返回<code>readable, writable, exceptional</code>，然后通过循环遍历这三个对象（文件描述符数组）进行操作。</p>
<p>在<code>readable</code>循环里，如果可读的对象是当前<code>socket</code>对象，说明有新的连接进入了，通过<code>sock.accept</code>得到的连接对象，这个连接对象也是一个可读可写的文件描述符，所以要得到新的连接的数据，将这个对象也放入到<code>rlist</code>里面去。当<code>readable</code>循环里的对象不是当前<code>socket</code>对时，那肯定就是建立的连接有了客户端发来的数据了，通过<code>sock.recv</code>就可以得到数据了。同理<code>writable</code>。</p>
<p>使用<code>select</code>，其中需要监视的文件描述符随着连接的加入，会不断的变多，因为<code>select</code>是遍历的数组，数组的长度是有限制的，并且对于不活跃的文件描述符，<code>select</code>还是会不停的遍历，这样效率极低。</p>
<h2 id="epoll">epoll</h2>
<p>由于<code>select</code>会有各种限制，所有在 Linux 中，又出现了<code>epoll</code>。</p>
<pre><code class="language-python">import socket
import select

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.bind(('0.0.0.0', 5000))
s.listen(1)
s.setblocking(0)
print &quot;waiting for connect&quot;

epoll = select.epoll()
epoll.register(s.fileno(), select.EPOLLIN)


connections = {}
requests = {}
responses = {}
clients = {}
try:
    while True:
        events = epoll.poll(1)
        for fileno, event in events:
            if fileno == s.fileno():  # sock
                conn, addr = s.accept()
                conn.setblocking(0)
                epoll.register(conn.fileno(), select.EPOLLIN)  # add conn to read
                connections[conn.fileno()] = conn
                clients[conn.fileno()] = addr
                print &quot;client {} joined&quot;.format(addr)
            elif event &amp; select.EPOLLIN:
                data = connections[fileno].recv(1024)
                print &quot;recv data {} from client {}&quot;.format(data, clients[fileno])
                if len(data) == 1:
                    if ord(data) == 4:
                        responses[fileno] = &quot;bye\n&quot;
                else:
                    responses[fileno] = &quot;hello\n&quot;
                epoll.modify(fileno, select.EPOLLOUT)
            elif event &amp; select.EPOLLOUT:
                if fileno in responses:
                    try:
                        connections[fileno].sendall(responses[fileno])
                        if responses[fileno] == &quot;bye\n&quot;:
                            epoll.modify(fileno, 0)
                            connections[fileno].shutdown(socket.SHUT_RDWR)
                        else:
                            epoll.modify(fileno, select.EPOLLIN)
                        del responses[fileno]
                    except:
                        epoll.modify(fileno, 0)
            elif event &amp; select.EPOLLHUP:
                epoll.unregister(fileno)
                connections[fileno].close()
                del connections[fileno]
                print &quot;client {} left&quot;.format(clients[fileno])
                del clients[fileno]
finally:
    epoll.unregister(s.fileno())
    epoll.close()
    s.close()


</code></pre>
<p>上面无限循环之前，是常规的 socket 对象的创建，然后是<code>epoll</code>对象的创建和 socket 对象的文件描述符的<code>fileno</code>作为<code>EPOLLIN</code>事件加入到<code>epoll</code>里去监视。</p>
<p>然后就进入了无限次的循环中，然后通过<code>epoll.poll</code>系统调用，获取可操作的文件描述符，和对应的事件，因为只注册了一个<code>EPOLLIN</code>事件，所以当有 client 要连接的时候，就可以通过<code>socket.accpet</code>来建立连接，然后再把建立的连接（也是一个文件描述符）加入到<code>epoll</code>中去监视读数据。当有<code>EPOLLIN</code>进入时，就可以从可读连接里去读数据了，在这里我设定如果读到数据就给 client 发送<code>hello</code>，所以就把当前连接（文件描述符）修改为<code>EPOLLOUT</code>事件，然后在进入<code>EPOLLOUT</code>事件时，就把数据发送给客户端去。</p>
<p>那么 <code>epoll</code>不会不停的扫描所有的文件描述符，而是在有事件发生时，才会处理这个事件。</p>
<h2 id="tornadoioloop">tornado.ioloop</h2>
<p>为什么看<code>tornado.ioloop</code>会有上面这些，因为特么的直接看<code>tornado.ioloop</code>，我看不太懂，经过上面一步一步下来，对于理解<code>tornado.ioloop</code>里的代码有很大的帮助。</p>
<p>因为是以学习为目的，所以从 <a href="http://www.tornadoweb.org/">Tornado</a> 的<a href="https://github.com/tornadoweb/tornado/blob/branch1.2/tornado/ioloop.py">v1.2</a>这个版本的<code>tornado.ioloop</code>来看，因为少，才557行，所以荣誉理解。</p>
<p><code>tornado.ioloop</code>是以<code>epoll</code>为基础的，但是在不支持<code>epoll</code>的平台，会选择该平台对应的实现来做。</p>
<p>从<a href="https://github.com/tornadoweb/tornado/blob/branch1.2/tornado/ioloop.py#L541">这里</a>开始</p>
<pre><code class="language-python"># Choose a poll implementation. Use epoll if it is available, fall back to
# select() for non-Linux platforms
if hasattr(select, &quot;epoll&quot;):
    # Python 2.6+ on Linux
    _poll = select.epoll
elif hasattr(select, &quot;kqueue&quot;):
    # Python 2.6+ on BSD or Mac
    _poll = _KQueue
else:
    try:
        # Linux systems with our C module installed
        import epoll
        _poll = _EPoll
    except:
        # All other systems
        import sys
        if &quot;linux&quot; in sys.platform:
            logging.warning(&quot;epoll module not found; using select()&quot;)
        _poll = _Select
</code></pre>
<p>如果是 Python 2.6+ 在 Linux 上，选择<code>select</code>下的 <code>epoll</code>，如果是在 BSD 或者 Mac，则选择<code>kqueue</code>，如果没有则首先尝试 <a href="http://www.tornadoweb.org/">Tronado</a> 写的 C 扩展的<code>epoll</code>模块，最后实在不行，就使用<code>select</code>。</p>
<p><a href="http://www.tornadoweb.org/">Tronado</a> 将不同的平台不同的 poll 实现了相同的接口，<code>_Select</code>、<code>_KQueue</code>、<code>_EPoll</code>，方便在<code>IOLoop</code>里使用。</p>
<p>在<code>IOLoop</code></p>
<ul>
<li><code>add_handler</code>相当于 <code>epoll.register</code></li>
<li><code>update_handler</code>相当于<code>epoll.modify</code></li>
<li><code>remove_handler</code>相当于<code>epoll.unregister</code></li>
</ul>
<p>然后就是<code>IOLoop.start</code>了，也是一个无限次循环，然后抛弃其他的不看，直接进入<a href="https://github.com/tornadoweb/tornado/blob/branch1.2/tornado/ioloop.py#L243">243</a>行的<code>event_pairs = self._impl.poll(poll_timeout)</code>，然后是<a href="https://github.com/tornadoweb/tornado/blob/branch1.2/tornado/ioloop.py#L267">267</a>行这里，得到文件描述符<code>fd</code>和<code>events</code>，接下来就像上述的<code>epoll</code>操作一样了，只是<code>tornado.ioloop</code>增加了一些 helper ，方便了操作而已。简化下来就是下述的代码了</p>
<pre><code class="language-python">class IOLoop(object):

    def __init__(self, impl=None):
        self._impl = impl or _poll()

    def add_handler(self, fd, handler, events):
        self._impl.register(fd, events | self.ERROR)

    def update_handler(self, fd, events):
        self._impl.modify(fd, events | self.ERROR)

    def remove_handler(self, fd):
        self._impl.unregister(fd)

    def start(self):
        while True:
            event_pairs = self._impl.poll(poll_timeout)

            self._events.update(event_pairs)
            while self._events:
                fd, events = self._events.popitem()
                self._handlers[fd](fd, events)

</code></pre>
<p>上面就是极其简化后的<code>IOLoop</code>了，看起来是不是跟<code>epoll</code>很像啊。</p>
<p>然后结合<code>tornado.ioloop</code>的示例代码</p>
<pre><code class="language-python">import errno
import functools
import ioloop
import socket
def connection_ready(sock, fd, events):
    while True:
        try:
            connection, address = sock.accept()
        except socket.error, e:
            if e.args[0] not in (errno.EWOULDBLOCK, errno.EAGAIN):
                raise
            return
        connection.setblocking(0)
        handle_connection(connection, address)
        
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setblocking(0)
sock.bind((&quot;&quot;, port))
sock.listen(128)
io_loop = ioloop.IOLoop.instance()
callback = functools.partial(connection_ready, sock)
io_loop.add_handler(sock.fileno(), callback, io_loop.READ)
io_loop.start()
</code></pre>
<p>在之前，完全是不懂上面的示例代码的意思，经过层层剥茧，从最原始的 socket 开始理解，就很好的理解了<code>tornado.ioloop</code>对于网络的处理了。</p>
<h2 id="refs">refs</h2>
<ul>
<li><a href="https://docs.python.org/2/library/select.html">https://docs.python.org/2/library/select.html</a></li>
<li><a href="https://docs.python.org/2/library/threading.html">https://docs.python.org/2/library/threading.html</a></li>
<li><a href="https://pymotw.com/2/select/">https://pymotw.com/2/select/</a></li>
<li><a href="https://github.com/tornadoweb/tornado/blob/branch1.2/tornado/ioloop.py">https://github.com/tornadoweb/tornado/blob/branch1.2/tornado/ioloop.py</a></li>
</ul>
</div>]]></content:encoded></item><item><title><![CDATA[麻辣香锅自制记]]></title><description><![CDATA[<div class="kg-card-markdown"><p>自从在上海吃过麻辣香锅之后，从此爱上了这个，原谅我这土包子没吃过好的。</p>
<p>昨日在张大妈白菜党看到了二手东<a href="http://item.jd.com/858422.html">海底捞麻辣香锅</a>特价，19.04RMB，加上我二手东钻石会员，每个月有2张免运费券，用也用不完。果断下单入手，于是开始了下面的麻辣香锅之旅。</p>
<h3 id="">食材采购</h3>
<p>首先就是选购食材了，根据在<a href="http://zhihu.com/question/22691004/answer/28623809">知乎</a>和<a href="http://www.xiachufang.com/recipe/1052406/">下厨房</a>看到的教程，以及平时在外面店里吃的结合起来，列出了如下菜单（只有做这种事，才想到了印象笔记的好处，买一个勾一个）</p>
<ul>
<li>土豆</li>
<li>藕</li>
<li>笋尖</li>
<li>里脊肉</li>
<li>鸡胸肉</li>
<li>鸡翅</li>
<li><del>骨肉相连</del></li>
<li>红薯</li>
<li><del>牛肉</del></li>
<li>腐竹</li>
<li>黑木耳</li>
<li>茶树菇</li>
<li>香菇</li>
<li>金针菇</li>
<li>火腿</li>
<li><del>虾</del></li>
<li>葱</li>
<li><del>五花肉</del></li>
<li>培根</li>
<li>大葱</li>
<li><del>郫县豆瓣酱</del></li>
<li><del>牛蛙</del></li>
</ul>
<p>最终根据实际情况，删除掉的都是没有买（比如难得处理虾）</p></div>]]></description><link>https://hui.lu/ma-la-xiang-guo-zi-zhi-ji/</link><guid isPermaLink="false">59f426890ee5070001939270</guid><category><![CDATA[生活]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Sat, 12 Mar 2016 13:45:09 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>自从在上海吃过麻辣香锅之后，从此爱上了这个，原谅我这土包子没吃过好的。</p>
<p>昨日在张大妈白菜党看到了二手东<a href="http://item.jd.com/858422.html">海底捞麻辣香锅</a>特价，19.04RMB，加上我二手东钻石会员，每个月有2张免运费券，用也用不完。果断下单入手，于是开始了下面的麻辣香锅之旅。</p>
<h3 id="">食材采购</h3>
<p>首先就是选购食材了，根据在<a href="http://zhihu.com/question/22691004/answer/28623809">知乎</a>和<a href="http://www.xiachufang.com/recipe/1052406/">下厨房</a>看到的教程，以及平时在外面店里吃的结合起来，列出了如下菜单（只有做这种事，才想到了印象笔记的好处，买一个勾一个）</p>
<ul>
<li>土豆</li>
<li>藕</li>
<li>笋尖</li>
<li>里脊肉</li>
<li>鸡胸肉</li>
<li>鸡翅</li>
<li><del>骨肉相连</del></li>
<li>红薯</li>
<li><del>牛肉</del></li>
<li>腐竹</li>
<li>黑木耳</li>
<li>茶树菇</li>
<li>香菇</li>
<li>金针菇</li>
<li>火腿</li>
<li><del>虾</del></li>
<li>葱</li>
<li><del>五花肉</del></li>
<li>培根</li>
<li>大葱</li>
<li><del>郫县豆瓣酱</del></li>
<li><del>牛蛙</del></li>
</ul>
<p>最终根据实际情况，删除掉的都是没有买（比如难得处理虾）或者因为各种情况买不到（比如牛蛙）的。</p>
<h3 id="">食材准备</h3>
<p>对于素菜，切丁切片洗净准备，对于鸡翅、鸡胸肉等冷冻食品，放入热水解冻，然后用盐和料酒腌制（如果喜欢清淡，这里要少放盐），最后加入淀粉裹匀。</p>
<p>根据上述知乎与下厨房教程的结合，做出了如下的选择</p>
<ul>
<li>素菜类全部用水煮</li>
<li>荤菜一律油炸</li>
</ul>
<p>经过水煮、油炸之后，食物大概8、9、10成熟的样子。然后锅里放入油（这里的油可以用上述油炸荤菜剩的油）、葱、大葱爆炒，然后加入二手东买的佐料，『炒出香味后』（来自说明书），加入食材后，『加入少量水收汁』（也是来自说明书），然后不停翻炒，直至汁水木有了。</p>
<p>由于家中锅太小，盆太少，最终分了4次爆炒，最终出了一大锅。</p>
<h3 id="">最终效果图</h3>
<p>最终耗时2小时，终于完成了如下的美味（海底捞佐料真的很美味）。</p>
<div style="max-width:80%; margin: 0 auto">
<img src="//cdn.cloverstd.com/images/blog/2017/05/21/lg3elig5qknbvv88eim4cq0wp4el776.png">
</div></div>]]></content:encoded></item><item><title><![CDATA[内网穿透-SSH 反向代理]]></title><description><![CDATA[<div class="kg-card-markdown"><p>最近搬了新家，4个人合租，办了上海电信的 30MB 的光纤，说是 30MB，上行才 2MB，好在下行是实打实的 30MB，于是就准备把吃灰的树莓派找出来继续做下载机下电影美剧看。</p>
<p>之前在寝室住的时候，直接在路由器上拨号上网，有公网 IP，用的路由器可以做端口转发，配合 DDNS，可以很方便的从外面访问控制树莓派，但是现在这边搬的光纤，必须用电信『强制』租给我们一个光猫，押金200元，说好一年之后可以凭光猫退200块，但是之前来给我们办理网络的师傅说甭指望了，退不了的，我直接告诉他，退不了就去工信部投诉，合同上写好了可以退（其实我也没看合同上到底有没有写，不是我去办理的）。『强租』也就算了，拨号的账号密码都写在光猫里，不给我们，超级管理员密码也不知道，问电信的师傅，说他也不知道，导致我们不能用自己的光猫，这也引发了此片文章。</p>
<p>通过<code>curl ip.cn</code>看了下，电信还算良心给分配了公网 IP，然后电信『</p></div>]]></description><link>https://hui.lu/nei-wang-chuan-tou-ssh-fan-xiang-dai-li/</link><guid isPermaLink="false">59f426890ee507000193926e</guid><category><![CDATA[反向代理]]></category><category><![CDATA[ssh]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Fri, 09 Oct 2015 12:21:18 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>最近搬了新家，4个人合租，办了上海电信的 30MB 的光纤，说是 30MB，上行才 2MB，好在下行是实打实的 30MB，于是就准备把吃灰的树莓派找出来继续做下载机下电影美剧看。</p>
<p>之前在寝室住的时候，直接在路由器上拨号上网，有公网 IP，用的路由器可以做端口转发，配合 DDNS，可以很方便的从外面访问控制树莓派，但是现在这边搬的光纤，必须用电信『强制』租给我们一个光猫，押金200元，说好一年之后可以凭光猫退200块，但是之前来给我们办理网络的师傅说甭指望了，退不了的，我直接告诉他，退不了就去工信部投诉，合同上写好了可以退（其实我也没看合同上到底有没有写，不是我去办理的）。『强租』也就算了，拨号的账号密码都写在光猫里，不给我们，超级管理员密码也不知道，问电信的师傅，说他也不知道，导致我们不能用自己的光猫，这也引发了此片文章。</p>
<p>通过<code>curl ip.cn</code>看了下，电信还算良心给分配了公网 IP，然后电信『强租』的光猫还是一个路由器，有普通管理员账号密码，进去后发现有 DMZ 和虚拟主机配置（端口转发），然后尝试了配置 DMZ 和虚拟主机配置（端口转发），结果发现怎么都不生效，于是不得不放弃之。然后 Google 之，发现了关键字『SSH 反向代理』。</p>
<p>然后按照<a href="http://segmentfault.com/a/1190000002718360">这里</a>的配置进行配置，然而配置好了后，SSH 连接时，会给出下面错误</p>
<pre><code>channel 2: open failed: connect failed: Connection refused
</code></pre>
<p>又一次 Google 之，发现了<a href="http://serverfault.com/a/541776">这个</a>，然而虽然没有解决我的问题，但是通过这个回答，我试了下把端口统一下</p>
<pre><code>ssh -fCNR &lt;port_b1&gt;:localhost:22 usr_b@B.B.B.B
ssh -fCNL &quot;*:&lt;port_b1&gt;:localhost:&lt;port_b1&gt;' localhost
</code></pre>
<p>注意上述，按照<a href="http://segmentfault.com/a/1190000002718360">这里</a>的配置，应该为</p>
<pre><code>ssh -fCNR &lt;port_b1&gt;:localhost:22 usr_b@B.B.B.B
ssh -fCNL &quot;*:&lt;port_b2&gt;:localhost:&lt;port_b1&gt;' localhost
</code></pre>
<p>当我按照这个配置，得到了错误，我就试了下把端口统一下，然后竟然成功了。但是按照文档，<code>port_b2</code>可以与<code>port_b1</code>不同，但我就是不能成功。</p>
<p>上述配置成功了，就可以从外面登陆到树莓派了</p>
<pre><code>ssh -p &lt;portb1&gt; usra@B.B.B.B
</code></pre>
<p>然而，因为网络的原因，<code>ssh</code>会断开，并且不会重连，虽然可以通过修改<code>sshd_config</code>配置修改连接时间，但是还是会断开。于是我就用<code>supervisor</code>来是<code>ssh</code>一直连接着。</p>
<p>这里需要注意以下几点：</p>
<ul>
<li>我去掉了<code>-f</code>参数，如果加上<code>-f</code>，<code>supervisor</code>就不能正常的监控<code>ssh</code>了；</li>
<li><code>ssh</code>我双向配置了 key 登陆，所以就不用输入密码，<code>supervisor</code>可以自动启动<code>ssh</code>；</li>
<li>因为配置了 key 登陆，而我的<code>supervisor</code>是以<code>root</code>用户运行，所以需要在<code>supervisor</code>里指定<code>user</code>参数，使<code>ssh</code>可以正常读取到<code>id_rsa</code>，当然也可以用<code>-i</code>参数显示的指定<code>id_rsa</code>路径。</li>
</ul>
<h2 id="update">update</h2>
<blockquote>
<p>2016-06-02<br>
对于 ssh 的自动重连，可以使用 autossh 配合 supervisor</p>
</blockquote>
<p>并且 ssh 应该加上下列参数</p>
<pre><code class="language-shell">-o &quot;ServerAliveInterval 60&quot; -o &quot;ServerAliveCountMax 3&quot; -o &quot;StrictHostKeyChecking no&quot;
</code></pre>
<p>不然，autossh 会『假死』</p>
<p><code>ServerAliveInterval</code>的作用是当没有数据交互时，间隔一段时间给服务器发送一个<code>message</code>，并且接受服务器的响应</p>
<p><code>ServerAliveCountMax</code>的作用是<code>ServerAliveInterval</code>最大重试次数，当失败了就会中断连接，这样 autossh 又能自动重连了</p>
<p>完整的的命令是</p>
<pre><code>autossh -M 0 -q -o &quot;ServerAliveInterval 60&quot; -o &quot;ServerAliveCountMax 3&quot; -o &quot;StrictHostKeyChecking no&quot; -CNR 2222:127.0.0.1:22 -i .ssh/id_rsa user@host
</code></pre>
<p>本机连接到 <code>host</code>主机，并且在 <code>host</code> 上设置一个 <code>127.0.0.1:2222</code>的反向代理到本机</p>
</div>]]></content:encoded></item><item><title><![CDATA[扇贝打卡 RSS 源]]></title><description><![CDATA[<div class="kg-card-markdown"><p>扇贝打卡只有分享到微博、微信、QQ，然而还要手动分享，这样不利于监督。<br>
于是我就想利用 <a href="http://ifttt.com">IFTTT</a> 来实现自动化操作，因为 <a href="http://ifttt.com">IFTTT</a> 并没有提供扇贝的 channel，所以曲线救国，就考虑到了通过 RSS，然后扇贝也没有提供打卡的 RSS 源，于是就只能手动生成 RSS 源了。</p>
<p>在很久很久以前，我记得扇贝有提供一个公开的个人打卡记录的页面，貌似是这个 <a href="http://shanbay.com/checkin/user/4539052/">http://shanbay.com/checkin/user/4539052/</a> ，但是现在这个 URL 必须登录后才能访问，本来想着模拟登录的，但是发现打卡的公共排行榜可以访问，例如『<a href="http://shanbay.com/checkin/favorite/">最受欢迎打卡</a>』，那就说明还是有办法可以不登陆访问个人的打卡记录的嘛，于是辗转曲折的找到了『XX 最近打卡日记』，例如 <a href="http://shanbay.com/checkin/record/25683094549/">http://shanbay.com/checkin/record/25683094549/</a> 这个，通过这个页面，</p></div>]]></description><link>https://hui.lu/shanbay-checkin-to-rss/</link><guid isPermaLink="false">59f426890ee507000193926d</guid><category><![CDATA[Tornado]]></category><dc:creator><![CDATA[cloverstd]]></dc:creator><pubDate>Fri, 02 Oct 2015 14:08:29 GMT</pubDate><content:encoded><![CDATA[<div class="kg-card-markdown"><p>扇贝打卡只有分享到微博、微信、QQ，然而还要手动分享，这样不利于监督。<br>
于是我就想利用 <a href="http://ifttt.com">IFTTT</a> 来实现自动化操作，因为 <a href="http://ifttt.com">IFTTT</a> 并没有提供扇贝的 channel，所以曲线救国，就考虑到了通过 RSS，然后扇贝也没有提供打卡的 RSS 源，于是就只能手动生成 RSS 源了。</p>
<p>在很久很久以前，我记得扇贝有提供一个公开的个人打卡记录的页面，貌似是这个 <a href="http://shanbay.com/checkin/user/4539052/">http://shanbay.com/checkin/user/4539052/</a> ，但是现在这个 URL 必须登录后才能访问，本来想着模拟登录的，但是发现打卡的公共排行榜可以访问，例如『<a href="http://shanbay.com/checkin/favorite/">最受欢迎打卡</a>』，那就说明还是有办法可以不登陆访问个人的打卡记录的嘛，于是辗转曲折的找到了『XX 最近打卡日记』，例如 <a href="http://shanbay.com/checkin/record/25683094549/">http://shanbay.com/checkin/record/25683094549/</a> 这个，通过这个页面，有最近的7条打卡记录，如果是做自动同步，7条记录就够了，然后就有了下面的程序了。</p>
<script src="https://gist.github.com/cloverstd/67e5b4897f11694ff3c6.js"></script>
<p><code>http://shanbay2rss.hui.lu/shanbay2rss/&lt;your_record_id&gt;</code>，这是一个可用的扇贝打卡 RSS 源。</p>
</div>]]></content:encoded></item></channel></rss>