JerryQu 的小站 https://imququ.com 专注 WEB 端开发,关注 WEB 性能。 zh-cn Thu, 28 Mar 2024 17:39:08 +0800 聊聊一些近况 https://imququ.com/post/my-recent-work-and-life.html <p>真的有很多年没更新博客了,写博客这件事真的不能停,一停下来就完全不知道该怎么开始了。</p> <p>北京的初春雪景格外美 ── 可惜我只能透过酒店的窗户看到一小部分 ── 因为去过新冠确诊病例所在楼层而被集中隔离。酒店条件、餐食都不错,我本人也很健康,只是 21 天不能出房间实在有点久,久到居然想到在博客上写点东西。</p> <p><img alt="beijing snow" src="https://st.imququ.com/static/uploads/2022/03/snow.jpg" itemprop="image" width="500" height="375"></p> <p>博客上次更新还在 2017 年,这五年来发生的事情太多太多,就连疫情都迎来了第三个年头。想写的东西有点多,今天先从自己近期工作和生活上的一些变化聊起。</p> <blockquote> <p>提示:本文纯属闲聊,没有任何干货。</p> </blockquote> <h3 id="-">聊聊工作</h3> <p>有些热心的读者已经发现,前些时我修改了“关于”页面上的工作经历部分。是的,21年5月,在 360 工龄还差三个月满十年之际,我选择了离开。因为一些原因,当时我没有在任何场合提及这件事,但离开一家工作十年的公司远没想象中的那么容易。</p> <p>我的第一份工作是在百度电商部门。三年时间,极具挑战的项目和业内顶尖的同事,让我具备了一名合格工程师所需技能,也让我见识到一个优秀团队需要具备的所有条件。感谢百度!</p> <p>而我人生中更多的重要时刻,则是与我工作过的第二家公司奇虎 360 交织在一起。感恩 360 能让我在北京站稳脚跟;感恩我的历任领导一直赋予我更大的 Scope,给予充足的成长空间,帮助我快速成长;更要感恩一起并肩作战过的伙伴们,让我有了永远珍藏的回忆和不虚此行的十年。祝愿 360 公司能再次突破,走上新的高度,也祝愿 360 小伙伴们拥有更加美好的未来。</p> <p><img alt="thanks to 360 sousuo" src="https://st.imququ.com/static/uploads/2022/03/360so.jpg" itemprop="image" width="500" height="666"></p> <p>现在我所负责的业务是<a href="https://juejin.cn">稀土掘金</a>,一个专注于开发者成长的技术社区,现在属于字节大家庭中的一员。相信技术圈子尤其是前端研发的小伙伴早已熟悉掘金。</p> <p>我的职业生涯缘起 51js 一个帖子。当时被大雪所困回不了家,我在寝室写了一个 <a href="https://imququ.com/post/qgywebim.html">WebIM</a> 发到 51js 社区,引发了广泛讨论,也得到嗷嗷、Winter 和月影三位版主的关注,从而让我走上现在的道路。交流和成长是我对技术社区的初心,相信掘金未来可以更好地服务好开发者,帮助各个阶段的开发者更好地成长,请多多支持我们。</p> <p>掘金除了写作平台,还有面向大学生的<a href="https://forum.juejin.cn/youthcamp/">青训营</a>,面向体系化学习的<a href="https://juejin.cn/course">掘金课程</a>,专注于拓宽技术和管理视野的<a href="https://juejin.cn/live">掘金直播</a>,每年还会举办面向行业和未来的稀土掘金开发者大会。欢迎大家来掘金写小册、开直播、担任大会演讲嘉宾,请直接联系我。</p> <blockquote> <p>下周四(2022/03/24),掘金将邀请尤雨溪和两位字节前端专家一起,聊聊 Vue 3.0 和前端新趋势,我是本次活动主持人,欢迎捧场!<a href="https://live.juejin.cn/4354/vue3">现在就可以预约直播</a>。</p> </blockquote> <p>近期工作变动广告完毕,再来谈谈我对工作的看法。我不擅投资,买房之外所有投资均以惨败告终;我也不能接受创业风险,要养娃还要还房贷,工资是我长期稳定获取收入的主要来源。从这个角度讲,公司发展得好,个人才会有更大的利益,这也是我一直以来信奉的理念。但在工作中,我见过一些总是与公司站在对立面的员工,每天带着对公司的怨恨来上班,甚至巴不得公司早日垮掉。这让我很不能理解,这么痛苦为什么不早点换家自己喜欢的公司,如果能力不足以支撑跳槽而只会整天抱怨,则是极其不成熟的做法。从个人角度,如何选择好的公司、如何与公司一起长期发展,有一些技巧和规律,后续有机会在写。</p> <p>再来说说咱们行业。前些时看完《沸腾新十年》<a href="https://book.douban.com/subject/35567300/">上</a>、<a href="https://book.douban.com/subject/35567301/">下</a>,感慨诸多,作为亲历者参与互联网这惊心动魄的十多年,与之前读《沸腾十五年》的感受完全不一样。最近算法伦理、平台垄断、公司层面 ESG、个人层面 WLB 等话题,成为新的讨论热点,越来越多的人唱衰互联网,身边不断有人通过退休/换行业/进体制内离开互联网,同时新的进入者也很多,简历和面试环节竞争越来越激烈。</p> <p>对于科技进步和行业发展,人们总是会短期高估而长期低估,我对互联网行业变化的看法也是如此。最近,不少人看到有公司裁员就会寝食难安,我觉得没必要恐慌,除了少数严重缺乏职场竞争力的老人和尚未进入职场的新人要早作打算之外,我们大部分人短期内还不会涉及到裁员。而且只要行业保持健康,重新找一份工作也不难。看长远一些,生产力要素的变化、互联网作为基础设施权责的变化、行业资本与估值的变化、软科技向硬科技的变化等因素对互联网行业产生的长远影响,才是更应该关注和思考的。</p> <p>在人口红利、技术红利消失之后,政策红利必然会消失,互联网行业已经步入成熟稳定的发展阶段。短期往长期发展的过程,是我们能进行调整的窗口期。我个人的做法是:1)努力工作,保证稳定的现金流入;2)多读书,尤其是历史和宏观经济,从更大尺度看问题有助于保持内心平静;3)保持心身健康,做好长线发展准备;4)保持与各行各业的交流,关注大的趋势,避免落入信息孤岛;5)控制负债,保证现金流正常,做一些投资规划。</p> <p>这部分是个人观点,仅供参考。</p> <h3 id="-">聊聊生活</h3> <p>生活上一个非常大的变化是有了小屈屈,如今他已经三岁,健康快乐可爱。</p> <p>另外一个变化是我重新走进了校园。20 年 7 月我在明明老师的强烈建议下,报考了北大光华 MBA 项目。在经历两轮在线资料提交、线下面试、研究生统一招生考试、政治考试、政审之后,我终于在 21 年 9 月成为一名登记在册的非全日制学生,这也给我的生活带来了巨大的改变。</p> <p>说几点最大的感受:</p> <p><strong>同学</strong>:来自各行各业,经历、背景、能力都非常优秀,会学也会玩,有趣且兼容并包。这一点对于长期混迹于互联网尤其是技术圈子的我,无疑是打开了新世界的大门。</p> <p><strong>课程</strong>:形式多样,种类丰富,课堂上同学们的补充发言是亮点。基础课涵盖了财务会计、数据分析、经济学、战略/营销/运营管理、组织行为学等商科基础,选修方向也非常多,例如金融与投资、创新创业等等。时间方面,前两个学期几乎所有周末时间都会用来上课,每学期两个 Quater,每个 Quater 安排四门主课,上课 + 个人作业 + 小组作业 + 考试,非常充实;第三学期开始课程内容集中在选修和写论文上,会好很多。</p> <p><strong>校园</strong>:北大校园特别美,一塔湖图,食堂饭菜好吃还便宜。感谢北大人性化的疫情防控政策,让我们始终可以线下上课,虽然周末早起有难度,但在校园内上课的体验是网课远不能比的。</p> <p><img alt="Guanghua School of Management" src="https://st.imququ.com/static/uploads/2022/03/gsm.jpg" itemprop="image" width="500" height="375"></p> <p>由于 MBA 并不是一个普适性话题,这里不做过多介绍。一些基本信息请自行了解(学制、学籍学历、录取条件、收费等)。如果对此感兴趣欢迎留言,我会根据问题情况,后续看看是否邀请几个同学做场直播。</p> <p>刚看了下周的日历,又将是充实的一周,擼起袖子加油干,我们都有美好的未来,共勉!</p> <p>本文链接:<a href="https://imququ.com/post/my-recent-work-and-life.html">https://imququ.com/post/my-recent-work-and-life.html</a>,<a href="https://imququ.com/post/my-recent-work-and-life.html#comments" target="_blank">参与讨论</a></p> Sat, 19 Mar 2022 13:47:45 +0800 https://imququ.com/post/my-recent-work-and-life.html 如何为 ThinkJS 3 网站优化 TTFB 时间 https://imququ.com/post/reduce-ttfb-on-thinkjs3-website.html <p>今年早些时候,奇舞团开源的 Node.js 框架 ── <a href="https://thinkjs.org/">ThinkJS</a> 迎来了她的 3.0 版本。尽管今年我很少更新博客,但「每次 ThinkJS 发布大版本,我都要更新博客程序」的老传统还是不能丢。所以,你现在看到的这个博客,已经是基于 ThinkJS 3 全面重构后的新版。</p> <p>ThinkJS 3 基于 <a href="http://koajs.com">Koa</a> 2.x 开发,内核实现得非常小巧,框架通过 Middleware(兼容 Koa)、Adapter、Extend 等机制来扩展出强大而丰富的功能。按照惯例,ThinkJS 大版本之间无法平滑进行,但这次升级带来的工作量不算太大,本站的升级工作花了一下午全部完成。</p> <p>基于 ThinkJS 开发的网站普遍都很快,这篇文章我打算聊聊如何为 ThinkJS 3 网站优化 TTFB 时间,使之变得更快。</p> <p>Time to first byte(简称 TTFB)时间,又称首字节时间,是 WEB 性能优化中非常重要的指标。它代表着从浏览器发起 HTTP 请求到收到 HTTP 响应第一个字节的这段时间,包含了 DNS 解析、建立 TCP 连接、建立 SSL 连接、发送 HTTP 请求、网络传输、服务端处理、30X 重定向等阶段。在影响 TTFB 所有因素中,服务端程序何时输出响应决定了服务端处理时间的长短,也是本文关注的优化目标。</p> <p>优化 WEB 页面的 TTFB 时间除了要尽可能优化业务逻辑之外,还有两个常用技巧:</p> <ul> <li>多个 HTTP 请求响应(动静拆分);</li> <li>一个 HTTP 请求多次响应(分块传输);</li> </ul> <p>前者无非就是先尽快输出一个无服务端复杂逻辑的空壳页面,再发起 ajax、jsonp 等异步请求填充内容。这种方案不利于 SEO,比较适用于单页应用。</p> <p>像本站这种以内容为主的 Web 页面,非常适合采用第二个技巧来优化 TTFB 时间。本文重点介绍它。</p> <p>分块传输响应需要用到我之前介绍过的 <a href="https://imququ.com/post/transfer-encoding-header-in-http.html">HTTP Transfer-Encoding: chunked</a> 机制。有了这个机制,服务端可以随时将已经完成的部分响应发送给给客户端,而不必等待全部完成后再一次发送。浏览器拿到部分响应,就能解析并执行其中的 HTML、CSS 和 JS 代码,还能加载其中引用的子资源,最终让用户更快看到部分页面内容。分块传输也是 Facebook 在 2009 年实现的 Bigpipe 方案的理论基础,这里不再赘述。</p> <p>再来说说 ThinkJS。</p> <p>在 ThinkJS 之前几个版本中,我们可以通过 <code>http.write(content)</code> 发送多个 chunk,再通过 <code>http.end(content)</code> 发送最后一个 chunk,非常方便。</p> <p>而 ThinkJS 3 使用的 Koa 2.x,只能通过 <code>ctx.body</code> 设置并结束响应,意味着通常情况下响应只能发送一次,还得放在整个 Controller 流程的最后。</p> <p>通过分析代码,我找到在 ThinkJS 3 中多次发送响应的两种方案:</p> <ul> <li><code>ctx.body</code> 支持传入 Stream,创建 Readable 流 <code>rs</code> 并多次调用 <code>rs.push(content)</code> 可以多次发送 chunk,调用 <code>rs.push(null)</code> 可以结束响应;</li> <li>Koa 代码层面上并没有禁止我们使用 <code>ctx.res</code>,通过 <code>res</code> 对象可以完全控制响应;</li> </ul> <p>方案一比较正统;方案二则危险得多,官方都说要后果自负:</p> <blockquote> <p>Bypassing Koa&#39;s response handling is not supported. Avoid using the following node properties: res.statusCode, res.writeHead(), res.write(), res.end(). <a href="http://koajs.com/#ctx-res">via</a></p> </blockquote> <p>所以,本站最终采用了方案二。Koa 总共就几个文件,出啥奇怪的问题都不怕。</p> <p>下面开始贴代码。</p> <p>1)创建 Controller 的 Extend 文件 <code>src/extend/controller.js</code>:</p> <pre><code class="lang-javascript"><span class="hljs-keyword">const</span> firstChunkMinLength = <span class="hljs-number">4096</span>; <span class="hljs-built_in">module</span>.exports = { <span class="hljs-keyword">async</span> renderAndFlush(tpl) { <span class="hljs-keyword">let</span> content = <span class="hljs-keyword">await</span> <span class="hljs-keyword">this</span>.render(tpl); <span class="hljs-comment">//first chunk</span> <span class="hljs-keyword">if</span>(!<span class="hljs-keyword">this</span>.ctx.headerSent) { <span class="hljs-keyword">this</span>.ctx.type = <span class="hljs-string">'html'</span>; <span class="hljs-keyword">this</span>.ctx.flushHeaders(); <span class="hljs-keyword">let</span> length = content.length; <span class="hljs-keyword">if</span>(length &lt; firstChunkMinLength) { content += <span class="hljs-string">`&lt;s&gt;<span class="hljs-subst">${' '.repeat(firstChunkLength - length)}</span>&lt;/s&gt;`</span>; } } <span class="hljs-keyword">this</span>.ctx.res.write(content); } }; </code></pre> <p>输出第一个 chunk 之前,需要通过 ctx 的 <code>type</code> setter 和 <code>flushHeaders</code> 方法来输出响应起始行和头部。</p> <p>第一个 chunk 不能太小,否则会被某些浏览器缓存起来,不会马上显示,达不到我们想要的效果。更多细节可以点开这个 <a href="https://stackoverflow.com/questions/16909227/using-transfer-encoding-chunked-how-much-data-must-be-sent-before-browsers-s">stackoverflow 的链接</a>自己看。另外,我在实际测试中发现,只补空格 iOS Safari 依然不会立刻渲染,把空格放在标签里就没问题。也可能是我的幻觉,欢迎大家测试并指正。</p> <p>2)在 Controller 里将原本渲染模板的逻辑根据实际情况拆分为多步:</p> <pre><code class="lang-javascript"><span class="hljs-keyword">async</span> indexAction() { <span class="hljs-keyword">let</span> pageName = <span class="hljs-string">'blog-home'</span>; <span class="hljs-keyword">let</span> title = <span class="hljs-string">'JerryQu 的小站'</span>; <span class="hljs-keyword">this</span>.assign({ pageName, title}); <span class="hljs-comment">//输出头部和边栏 HTML</span> <span class="hljs-keyword">await</span> <span class="hljs-keyword">this</span>.renderAndFlush(<span class="hljs-string">'home/inc/header'</span>); <span class="hljs-comment">//查询数据库(耗时操作)</span> <span class="hljs-keyword">let</span> pn = <span class="hljs-keyword">this</span>.get(<span class="hljs-string">'pn'</span>); <span class="hljs-keyword">let</span> data = <span class="hljs-keyword">await</span> <span class="hljs-keyword">this</span>.model(<span class="hljs-string">'post'</span>).getPostList(pn, <span class="hljs-number">10</span>); data.pagerPath = getPagerPath(<span class="hljs-keyword">this</span>.ctx, <span class="hljs-string">'pn'</span>); <span class="hljs-keyword">this</span>.assign(data); <span class="hljs-comment">//输出剩余 HTML</span> <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.display(<span class="hljs-string">'home/index_post_list'</span>); } </code></pre> <p>也就是说需要提前发送的模板通过 renderAndFlush 来渲染并发送,剩余模板还是走原有的 display 逻辑。</p> <p>至此,本文要介绍的优化工作已经完成,赶紧打开浏览器验证一下吧。</p> <p>但是如果你照着我的代码改造,肯定会遇到不少坑,下面列举几个:</p> <p>1)原本的异常逻辑重定向到错误页不好用,直接提示 <code>Can&#39;t set headers after they are sent.</code> 错误。</p> <p>这个错误信息已经把原因描述得很明白,HTTP/1 的响应需要严格按照起始行、头部和正文的顺序发送,已经发送了正文,就不能再通过 30X 状态码和 <code>Location</code> 头部来跳转页面。</p> <p>解决方案:有一些会产生跳转的逻辑例如参数合法性检查,可以挪到发送第一个分块之前来进行。另一些异常跳转逻辑则无法提前,例如查询数据库后发现不存在对应的文章,这种情况可以输出一段 JS 代码在浏览器中跳转,或者直接渲染错误页面。</p> <p>2)提前输出的模板中部分变量取不到值。</p> <p>例如本站第一个分块输出了左侧内容,这个分块对应的模板中,有很多字段原本来自数据库查询后的结果,提前渲染必然取不到值。</p> <p>解决方案:这些需要用到数据库字段的逻辑,有一些可以挪到后续分块中;有一些则不好后移,例如需要动态赋值的页面 <code>&lt;title&gt;</code>,只能放在 <code>&lt;head&gt;</code> 里。一种方案是继续使用万能的 JS,通过后续分块中的 <code>docuemnt.title</code> 来给页面 title 赋值。对于不支持 JS 的 Spider,可以禁用提前输出响应策略。</p> <p>我使用了另外一套方案:由于文章数量不多,我索性在程序启动时,把全部文章 ID 和标题对应关系从数据库取出来,存在配置中。这样,后续第一个分块获取标题时无需查询数据库。</p> <p>下面这段代码需要放在 <code>src/bootstrap/worker.js</code>:</p> <pre><code class="lang-javascript"><span class="hljs-comment">//HTTP 服务启动前执行</span> think.beforeStartServer(<span class="hljs-keyword">async</span> () =&gt; { <span class="hljs-keyword">let</span> postTitle = {}; (<span class="hljs-keyword">await</span> think.model(<span class="hljs-string">'post'</span>).field([<span class="hljs-string">'slug'</span>, <span class="hljs-string">'title'</span>]).select()).forEach(item =&gt; { postTitle[item.slug] = item.title; }); think.config(<span class="hljs-string">'postTitle'</span>, postTitle); }); </code></pre> <p>3)Middleware 中获取到的 <code>ctx.body</code> 不是完整页面。</p> <p>本方案只有最后一个分块内容才会赋值给 <code>ctx.body</code>,前面分块的输出则完全绕过了 Middleware,出现这种情况是正常的。如果你不能接受,还是老老实实用 Readable Stream 吧。</p> <p>最后,看完本文,相信你对如何优化 ThinkJS 3 网站的 TTFB 时间有了足够了解,赶紧动手试试吧。遇到任何问题,欢迎留言讨论。</p> <p>本文链接:<a href="https://imququ.com/post/reduce-ttfb-on-thinkjs3-website.html">https://imququ.com/post/reduce-ttfb-on-thinkjs3-website.html</a>,<a href="https://imququ.com/post/reduce-ttfb-on-thinkjs3-website.html#comments" target="_blank">参与讨论</a></p> Tue, 28 Nov 2017 13:40:19 +0800 https://imququ.com/post/reduce-ttfb-on-thinkjs3-website.html 本博客开始支持 TLS 1.3 https://imququ.com/post/enable-tls-1-3.html <blockquote> <p>更新:在做出「<a href="https://imququ.com/post/about.html">暂停更新</a>」的决定后,本站一些实验性配置已经去除,包括本文提到的 TLS 1.3。我将以最低维护成本保证本站可用。@ 2020/04/22</p> </blockquote> <p>几个月前,我在升级本博客所用 Nginx 时,顺手加上了对 TLS 1.3 的支持,本文贴出详细的步骤和注意事项。有关 TLS 1.3 的介绍可以看 CloudFlare 的这篇文章:<a href="https://blog.cloudflare.com/tls-1-3-overview-and-q-and-a/">An overview of TLS 1.3 and Q&amp;A</a>。需要注意目前 Chrome 和 Firefox 支持的是 TLS 1.3 draft 18,暂时不要用在生产环境。</p> <blockquote> <p>更新:目前 Chrome 70 已经支持 TLS 1.3 final,本文已更新。@ 2018/10/19</p> </blockquote> <h3 id="-">安装依赖</h3> <p>我的 VPS 系统是 Ubuntu 16.04.3 LTS,如果你使用其它发行版,与包管理有关的命令请自行调整。</p> <p>首先安装依赖库和编译要用到的工具:</p> <pre><code class="lang-bash">sudo apt-get install build-essential libpcre3 libpcre3-dev zlib1g-dev unzip git </code></pre> <h3 id="-">获取必要组件</h3> <p><code>nginx-ct</code> 和 <code>ngx-brotli</code> 与本文主题无关,不过都是常用的 Nginx 组件,一并记录在这里。</p> <h4 id="nginx-ct">nginx-ct</h4> <p><code>nginx-ct</code> 模块用于启用 <a href="https://imququ.com/post/certificate-transparency.html">Certificate Transparency</a> 功能。直接从 github 上获取源码:</p> <pre><code class="lang-bash">wget -O nginx-ct.zip -c https://github.com/grahamedgecombe/nginx-ct/archive/v1.3.2.zip unzip nginx-ct.zip </code></pre> <p>注:大家常用的 Let&#39;s Encrypt 证书已内置 SCTs,<code>nginx-ct</code> 模块基本退出历史舞台了。</p> <h4 id="ngx_brotli">ngx_brotli</h4> <p>本站支持 Google 开发的 <a href="https://github.com/google/brotli">Brotli</a> 压缩格式,它通过内置分析大量网页得出的字典,实现了更高的压缩比率,同时几乎不影响压缩 / 解压速度。</p> <p>以前要想支持 <code>ngx_brotli</code> 模块,需要先手动编译 <code>libbrotli</code>。经评论里的朋友提醒,现在已经不用了。直接获取源码即可:</p> <pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/google/ngx_brotli.git <span class="hljs-built_in">cd</span> ngx_brotli git submodule update --init <span class="hljs-built_in">cd</span> ../ </code></pre> <h4 id="openssl">OpenSSL</h4> <p>为了支持 TLS 1.3 final,需要使用 OpenSSL 1.1.1 正式版:</p> <pre><code class="lang-bash">wget -c https://github.com/openssl/openssl/archive/OpenSSL_1_1_1.tar.gz tar xzf OpenSSL_1_1_1.tar.gz mv openssl-OpenSSL_1_1_1 openssl </code></pre> <h3 id="-nginx">编译并安装 Nginx</h3> <p>接着就可以获取 Nginx 源码,编译并安装:</p> <pre><code class="lang-bash">wget -c http://nginx.org/download/nginx-1.15.2.tar.gz tar zxf nginx-1.15.2.tar.gz <span class="hljs-built_in">cd</span> nginx-1.15.2 ./configure --add-module=../ngx_brotli --with-openssl=../openssl --with-openssl-opt=<span class="hljs-string">'enable-tls1_3 enable-weak-ssl-ciphers'</span> --with-http_v2_module --with-http_ssl_module --with-http_gzip_static_module make sudo make install </code></pre> <p><code>enable-tls1_3</code> 是让 OpenSSL 支持 TLS 1.3 的关键选项;而 <code>enable-weak-ssl-ciphers</code> 的作用是让 OpenSSL 继续支持 3DES 等不安全的 Cipher Suite,如果你打算继续支持 IE8,才需要加上这个选项。</p> <p>除了 <code>http_v2</code> 和 <code>http_ssl</code> 这两个 HTTP/2 必备模块之外,我还额外启用了 <code>http_gzip_static</code>,需要启用哪些模块需要根据自己实际情况来决定。</p> <p>以上步骤会把 Nginx 装到 <code>/usr/local/nginx/</code> 目录,如需更改路径可以在 configure 时指定。</p> <h3 id="web-">WEB 站点配置</h3> <p>在 Nginx 的站点配置中,以下两个参数需要修改:</p> <pre><code>ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # 增加 TLSv1.3 ssl_ciphers TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-128-GCM-SHA256:TLS13-AES-128-CCM-8-SHA256:TLS13-AES-128-CCM-SHA256:EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5; </code></pre><p>包含 <code>TLS13</code> 是 TLS 1.3 新增的 Cipher Suite,加在最前面即可;如果你不打算继续支持 IE8,可以去掉包含 <code>3DES</code> 的 Cipher Suite。</p> <p>本博客完整的 Nginx 配置,请<a href="https://imququ.com/post/my-nginx-conf.html">点击这里</a>查看。</p> <h3 id="-tls-1-3">验证是否支持 TLS 1.3</h3> <p>目前最新版 Chrome 和 Firefox 都支持 TLS 1.3,但需要手动开启:</p> <ul> <li>Chrome,将 <code>chrome://flags/</code> 中的 <code>Maximum TLS version enabled</code> 改为 <code>TLS 1.3</code>(Chrome 62 中需要将 <code>TLS 1.3</code> 改为 <code>Enabled (Draft)</code>,感谢 @TsuranSonoda 指出);</li> <li>Firefox,将 <code>about:config</code> 中的 <code>security.tls.version.max</code> 改为 <code>4</code>;</li> </ul> <p>本博客多次推荐的 <a href="https://www.ssllabs.com/ssltest/index.html">Qualys SSL Labs&#39;s SSL Server Test</a> 也支持验证服务端是否支持 TLS 1.3,非常方便,继续推荐。</p> <p>本文链接:<a href="https://imququ.com/post/enable-tls-1-3.html">https://imququ.com/post/enable-tls-1-3.html</a>,<a href="https://imququ.com/post/enable-tls-1-3.html#comments" target="_blank">参与讨论</a></p> Sun, 06 Aug 2017 14:37:02 +0800 https://imququ.com/post/enable-tls-1-3.html HTTPS 常见部署问题及解决方案 https://imququ.com/post/troubleshooting-https.html <p>在最近几年里,我写了很多有关 HTTPS 和 HTTP/2 的文章,涵盖了证书申请、Nginx 编译及配置、性能优化等方方面面。在这些文章的评论中,不少读者提出了各种各样的问题,我的邮箱也经常收到类似的邮件。本文用来罗列其中有代表性、且我知道解决方案的问题。</p> <p>为了控制篇幅,本文尽量只给出结论和引用链接,不展开讨论,如有疑问或不同意见,欢迎留言讨论。本文会持续更新,欢迎大家贡献自己遇到的问题和解决方案。</p> <blockquote> <p>实际上,遇到任何有关部署 HTTPS 或 HTTP/2 的问题,都推荐先用 <a href="https://www.ssllabs.com/ssltest/index.html">Qualys SSL Labs&#39;s SSL Server Test</a> 跑个测试,大部分问题都能被诊断出来。</p> </blockquote> <h3 id="-let-s-encrypt-">申请 Let&#39;s Encrypt 证书时,一直无法验证通过</h3> <p>这类问题一般是因为 Let&#39;s Encrypt 无法访问你的服务器,推荐尝试 <a href="https://github.com/Neilpang/acme.sh">acme.sh</a> 的 <a href="https://github.com/Neilpang/acme.sh#7-use-dns-mode">DNS 验证模式</a>,一般都能解决。 </p> <h3 id="-err_certificate_transparency_required">网站无法访问,提示 ERR_CERTIFICATE_TRANSPARENCY_REQUIRED</h3> <p>使用 Chrome 53 访问使用 Symantec 证书的网站,很可能会出现这个错误提示。这个问题由 Chrome 的某个 Bug 引起,目前最好的解决方案是升级到 Chrome 最新版。相关链接:</p> <ul> <li><a href="https://bugs.chromium.org/p/chromium/issues/detail?id=664177">Out of date Chrome results in ERR_CERTIFICATE_TRANSPARENCY_REQUIRED for Symantec operated sites</a>;</li> <li><a href="https://knowledge.symantec.com/support/ssl-certificates-support/index?page=content&amp;id=ALERT2160">Warning | Certificate Transparency error with Chrome 53</a>;</li> </ul> <h3 id="-">浏览器提示证书有错误</h3> <h4 id="-">检查证书链是否完整</h4> <p>首先确保网站使用的是合法 CA 签发的有效证书,其次检查 Web Server 配置中证书的完整性(一定要包含站点证书及所有中间证书)。如果缺失了中间证书,部分浏览器能够自动获取但严重影响 TLS 握手性能;部分浏览器直接报证书错误。</p> <blockquote> <p><a href="https://whatsmychaincert.com/">What&#39;s My Chain Cert?</a> 这个网站可以用来检查证书链是否完整,它还可以用来生成正确的证书链。</p> </blockquote> <h4 id="-sni">检查浏览器是否支持 SNI</h4> <p>如果只有老旧浏览器(例如 IE8 on Windows XP)提示这个错误,多半是因为你的服务器同时部署了使用不同证书的多个 HTTPS 站点,这样,不支持 SNI(Server Name Indication)的浏览器通常会获得错误的证书,从而无法访问。</p> <p>要解决浏览器不支持 SNI 带来的问题,可以将使用不同证书的 HTTPS 站点部署在不同服务器上;还可以利用 SAN(Subject Alternative Name)机制将多个域名放入同一张证书;当然你也可以直接无视这些老旧浏览器。特别地,使用不支持 SNI 的浏览器访问商业 HTTPS CDN,基本都会因为证书错误而无法使用。</p> <blockquote> <p>有关 SNI 的更多说明,请看「<a href="https://imququ.com/post/sth-about-switch-to-https-2.html#toc-2">关于启用 HTTPS 的一些经验分享(二)</a>」。</p> </blockquote> <h4 id="-">检查系统时间</h4> <p>如果用户电脑时间不对,也会导致浏览器提示证书有问题,这时浏览器一般都会有明确的提示,例如 Chrome 的 ERR_CERT_DATE_INVALID。</p> <h3 id="-http-2-err_spdy_inadequate_transport_security">启用 HTTP/2 后网站无法访问,提示 ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY</h3> <p>这个问题一般是由于 CipherSuite 配置有误造成的。建议对照「<a href="https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_configurations">Mozilla 的推荐配置</a>、<a href="https://github.com/cloudflare/sslconfig/blob/master/conf">CloudFlare 使用的配置</a>」等权威配置修改 Nginx 的 <code>ssl_ciphers</code> 配置项。</p> <blockquote> <p>有关这个问题的具体原因,请看「<a href="https://imququ.com/post/why-tls-handshake-failed-with-http2-enabled.html">从启用 HTTP/2 导致网站无法访问说起</a>」。</p> </blockquote> <h3 id="-err_ssl_version_or_cipher_mismatch">网站无法访问,提示 ERR_SSL_VERSION_OR_CIPHER_MISMATCH</h3> <p>出现这种错误,通常都是配置了不安全的 SSL 版本或者 CipherSuite —— 例如服务器只支持 SSLv3,或者 CipherSuite 只配置了 RC4 系列,使用 Chrome 访问就会得到这个提示。解决方案跟上一节一样。</p> <p>还有一种情况会出现这种错误 —— 使用不支持 ECC 的浏览器访问只提供 ECC 证书的网站。例如在 Windows XP 中,使用 ECC 证书的网站只有 Firefox 能访问(Firefox 的 TLS 自己实现,不依赖操作系统);Android 平台中,也需要 Android 4+ 才支持 ECC 证书。</p> <blockquote> <p>针对不支持 ECC 证书的浏览器,有一个完美的解决方案,请看「<a href="https://imququ.com/post/ecc-certificate.html">开始使用 ECC 证书</a>」。</p> </blockquote> <h3 id="-nginx-http-2-http-1-1">在 Nginx 启用 HTTP/2 后,浏览器依然使用 HTTP/1.1</h3> <p>Chrome 51+ 移除了对 NPN 的支持,只支持 ALPN,而浏览器和服务端都支持 NPN 或 ALPN,是用上 HTTP/2 的大前提。换句话说,如果服务端不支持 ALPN,Chrome 51+ 无法使用 HTTP/2。</p> <p>OpenSSL 1.0.2 才开始支持 ALPN —— 很多主流服务器系统自带的 OpenSSL 都低于这个版本,所以推荐在编译 Web Server 时自己指定 OpenSSL 的位置。</p> <blockquote> <p>详见「<a href="https://imququ.com/post/enable-alpn-asap.html">为什么我们应该尽快支持 ALPN</a>」。</p> </blockquote> <h3 id="-https-">升级到 HTTPS 后,网站部分资源不加载或提示不安全</h3> <p>记住一个原则:HTTPS 网站的所有外链资源(CSS、JS、图片、音频、字体文件、异步接口、表单 action 地址等等)都需要升级为 HTTPS,就不会遇到这个问题了。</p> <blockquote> <p>详见「<a href="https://imququ.com/post/sth-about-switch-to-https-3.html">关于启用 HTTPS 的一些经验分享(三)</a>」。</p> </blockquote> <h3 id="-safari-ios-">仅 Safari、iOS 各种浏览器无法访问</h3> <p>如果你的 HTTPS 网站用 PC Chrome 和 Firefox 访问一切正常,但 macOS Safari 和 iOS 各种浏览器无法访问,有可能是 Certificate Transparency 配置有误。当然,如果你之前没有通过 TLS 扩展启用 Certificate Transparency,请跳过本小节。</p> <p>具体症状是:通过 Wireshark 抓包分析,通常能看到名为 Illegal Parameter 的 Alert 信息;通过 <code>curl -v</code> 排查,一般能看到 Unknown SSL protocol error in connection 错误提示。</p> <p>这时候,请进入 Nginx <code>ssl_ct_static_scts</code> 配置指定的目录,检查 SCT 文件大小是否正常,尤其要关注是否存在空文件。</p> <blockquote> <p>需要注意的是:根据<a href="https://groups.google.com/a/chromium.org/forum/#!msg/ct-policy/u87C79AY-E8/VM4K1v8qCgAJ">官方公告</a>,从 2016 年 12 月 1 日开始,Google 的 Aviator CT log 服务将不再接受新的证书请求。用 <a href="https://github.com/grahamedgecombe/ct-submit">ct-submit</a> 等工具手动获取 SCT 文件时,不要再使用 Aviator 服务,否则就会得到空文件。</p> <p>更新:nginx-ct 的作者已经发布了 <a href="https://github.com/grahamedgecombe/nginx-ct/releases">v1.3.2</a>,针对零字节的 SCT 文件做了处理,不再发送。</p> </blockquote> <h3 id="-openssl-1-1-0-ie8-">将 OpenSSL 升级到 1.1.0+,IE8 无法访问</h3> <p>造成这个问题的根本原因是 OpenSSL 1.1.0+ 默认禁用了 3DES 系列的 Cipher Suites:</p> <blockquote> <p>For the 1.1.0 release, which we expect to release tomorrow, we will treat triple-DES just like we are treating RC4. It is not compiled by default; you have to use “enable-weak-ssl-ciphers” as a config option. <a href="https://www.openssl.org/blog/blog/2016/08/24/sweet32/">via</a></p> </blockquote> <p>升级到 OpenSSL 1.1.0+ 之后,要么选择不支持 Windows XP + IE8;要么在编译时加上 <code>enable-weak-ssl-ciphers</code> 参数。例如这是我的 Nginx 编译参数:</p> <pre><code class="lang-shell">./configure --add-module=../ngx_brotli --add-module=../nginx-ct-1.3.2 --with-openssl=../openssl --with-openssl-opt=<span class="hljs-string">'enable-tls1_3 enable-weak-ssl-ciphers'</span> --with-http_v2_module --with-http_ssl_module --with-http_gzip_static_module </code></pre> <p>本文链接:<a href="https://imququ.com/post/troubleshooting-https.html">https://imququ.com/post/troubleshooting-https.html</a>,<a href="https://imququ.com/post/troubleshooting-https.html#comments" target="_blank">参与讨论</a></p> Mon, 12 Dec 2016 23:50:26 +0800 https://imququ.com/post/troubleshooting-https.html 开始使用 VeryNginx https://imququ.com/post/use-verynginx.html <p><a href="https://github.com/alexazhou/VeryNginx">VeryNginx</a> 是一个功能强大而对人类友好的 Nginx 扩展程序,这是作者的原话。很久之前我就看到过这个项目,直到最近我才在本站试用了一把,确实好用,于是想通过本文把它介绍给更多人。</p> <p>VeryNginx 主要由两部分组成:基于 lua-nginx-module 开发的 Lua 脚本,以及基于 HTML/CSS/JS 开发的 Web 控制面板 —— 用于生成和管理 Lua 脚本所需配置。</p> <p>lua-nginx-module 能让 Lua 脚本直接跑在 Nginx 内部,比用 C 语言开发 Nginx 模块更容易上手,同时还能充分利用 Nginx 的非阻塞 I/O 模型,非常适合开发功能复杂、性能优异的 Web 应用。它也是大家熟知的 OpenResty 套件中一个最核心的模块。</p> <p>VeryNginx 通过在请求的不同阶段(如 init_by_lua*/rewrite_by_lua*/access_by_lua*/log_by_lua*)执行不同 Lua 脚本,实现给请求打标签及对拥有不同标签的请求进行不同的处理的功能。除此之外,它还支持常见的统计报表展示。</p> <h3 id="-verynginx">安装 VeryNginx</h3> <p>VeryNginx 依赖以下三个 Nginx 模块:</p> <ul> <li>lua-nginx-module</li> <li>http_stub_status_module</li> <li>http_ssl_module</li> </ul> <p>如果对 Nginx 没有定制化需求,建议直接使用 VeryNginx 默认的安装脚本,它会同时装好 VeryNginx 自身和 OpenResty 套件,最为方便。具体步骤请查看<a href="https://github.com/alexazhou/VeryNginx/blob/master/readme_zh.md">官方文档</a>。</p> <p>对于我这样喜欢各种折腾 Nginx 的人来说,修改之前的 Nginx 编译步骤,把上面三个模块加进去,也不算复杂。具体步骤后面再介绍,先来搞定 VeryNginx 工具本身。</p> <p>这一步很简单:下载 VeryNginx 最新版代码并安装即可:</p> <pre><code class="lang-bash">wget https://github.com/alexazhou/VeryNginx/archive/v0.3.3.zip unzip v0.3.3.zip <span class="hljs-built_in">cd</span> VeryNginx-0.3.3/ sudo python install.py install verynginx <span class="hljs-built_in">cd</span> ../ </code></pre> <p>安装 VeryNginx 用到了 Python 脚本,但这个项目跟 Python 没有半毛钱关系,不信可以看下 <code>install.py</code> 中的 <code>install_verynginx</code> 方法,只做了拷贝文件和修改配置目录权限两件事。</p> <p>VeryNginx 默认会被装到 <code>/opt/verynginx/</code> 目录,本文使用默认配置。</p> <h3 id="-nginx">编译 Nginx</h3> <p>VeryNginx 依赖的 <code>http_stub_status_module</code> 和 <code>http_ssl_module</code> 只需要在 configure 时加上就可以。<code>lua-nginx-module</code> 稍微麻烦一点,它有以下依赖:</p> <ul> <li>LuaJIT 2.0 或 LuaJIT 2.1(推荐)或 Lua 5.1(5.2 目前不支持);</li> <li>ngx_devel_kit(NDK);</li> <li>ngx_lua 源码;</li> </ul> <p>下面分别来搞定它们。本文使用 Ubuntu 16.04.1 LTS,全部采用默认路径安装。如果你的环境跟我不一样,一些命令请自行调整。</p> <h4 id="luajit">LuaJIT</h4> <p>下载并安装 <a href="http://luajit.org/download.html">LuaJIT</a>:</p> <pre><code class="lang-bash">wget http://luajit.org/download/LuaJIT-2.1.0-beta2.zip unzip LuaJIT-2.1.0-beta2.zip <span class="hljs-built_in">cd</span> LuaJIT-2.1.0-beta2/ make sudo make install <span class="hljs-built_in">cd</span> ../ </code></pre> <p>设置环境变量:</p> <pre><code>export LUAJIT_LIB=/usr/local/lib export LUAJIT_INC=/usr/local/include/luajit-2.1/ </code></pre><h4 id="ngx_devel_kit">ngx_devel_kit</h4> <p>下载并解压 <a href="https://github.com/simpl/ngx_devel_kit">ngx_devel_kit</a>:</p> <pre><code class="lang-bash">wget https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.zip unzip v0.3.0.zip </code></pre> <h4 id="ngx_lua">ngx_lua</h4> <p>下载并解压 <a href="https://github.com/openresty/lua-nginx-module/releases">ngx_lua</a>:</p> <pre><code class="lang-bash">wget https://github.com/openresty/lua-nginx-module/archive/v0.10.7.zip unzip v0.10.7.zip </code></pre> <h4 id="nginx">Nginx</h4> <p>本站编译 Nginx 的详细步骤,都记录在<a href="https://imququ.com/post/my-nginx-conf.html">这篇文章</a>,可以照搬。只有 configure 要改一下:</p> <pre><code>./configure --with-ld-opt=&quot;-Wl,-rpath,/usr/local/lib/&quot; --add-module=../ngx_devel_kit-0.3.0 --add-module=../lua-nginx-module-0.10.7 --add-module=../ngx_brotli --add-module=../nginx-ct-1.3.2 --with-openssl=../openssl --with-http_v2_module --with-http_ssl_module --with-http_gzip_static_module --with-http_stub_status_module make #make install 前请务必停止已有 Nginx 服务,sudo /etc/init.d/nginx stop sudo make install </code></pre><p>编译并安装好 Nginx 之后,建议通过 <code>-V</code> 参数再次确认:</p> <pre><code class="lang-bash">/usr/<span class="hljs-built_in">local</span>/nginx/sbin/nginx -V nginx version: nginx/1.11.7 built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4) built with OpenSSL 1.0.2j 26 Sep 2016 TLS SNI support enabled configure arguments: --with-ld-opt=-Wl,-rpath,/usr/<span class="hljs-built_in">local</span>/lib/ --add-module=../ngx_devel_kit-0.3.0 --add-module=../lua-nginx-module-0.10.7 --add-module=../ngx_brotli --add-module=../nginx-ct-1.3.1 --with-openssl=../openssl --with-http_v2_module --with-http_ssl_module --with-http_gzip_static_module --with-http_stub_status_module </code></pre> <h3 id="-verynginx">配置 VeryNginx</h3> <p>在 Nginx 中引入 VeryNginx 的配置文件,就可以让 VeryNginx 工作起来。首先要修改的是 Nginx 的主配置,一般位于 <code>/usr/local/nginx/conf/nginx.conf</code>。</p> <p>在主配置文件的最外层,加入以下配置:</p> <pre><code class="lang-bash">include /opt/verynginx/verynginx/nginx_conf/in_external.conf; </code></pre> <p>在主配置的 <code>http</code> 段落中,加入以下配置:</p> <pre><code class="lang-bash">include /opt/verynginx/verynginx/nginx_conf/in_http_block.conf; </code></pre> <p>在具体站点配置的 <code>server</code> 段落中,加入以下配置:</p> <pre><code class="lang-bash">include /opt/verynginx/verynginx/nginx_conf/in_server_block.conf; </code></pre> <p>加完之后,建议通过 <code>-t</code> 参数确保配置无误:</p> <pre><code class="lang-bash">/usr/<span class="hljs-built_in">local</span>/nginx/sbin/nginx -t </code></pre> <p>如果提示 <code>test is successful</code>,说明配置无误,可以重启 Nginx 服务;否则请根据提示排查。</p> <p>如果一切顺利,访问 <code>http://yourdomain.com/verynginx/index.html</code> 就可以见到 VeryNginx 的 Web 控制面板。默认用户名和密码都是 <code>verynginx</code>,登录后请务必修改。</p> <h3 id="-">使用示例</h3> <p>VeryNginx 使用非常简便,基本上不需要做过多说明。只是有一点需要注意:在 Web 控制面板中对任何配置项进行增删改之后,在点击页面右下角「Save」按钮之前并不会生效;点击「Reload」可还原到上一次配置。</p> <p>VeryNginx 可以根据多种特征(Client IP、Host、UserAgent、URI、Referer、Request Args)来组合出不同的规则,用来给请求打上标记(Matcher);可以给拥有不同标记的请求指定不同的处理动作(Custom Action)。</p> <p>下面通过一个实际案例来演示 VeryNginx 的基本用法。</p> <p>最近我发现某搜索引擎对本站的索引中,有大量重复内容(一共索引了 5000 多条记录,其他搜索引擎都只有几百):</p> <p><img src="https://st.imququ.com/static/uploads/2016/12/site-imququ-com-in-sogou.png" width="680" alt="imququ.com in sogou" itemprop="image" height="497"></p> <p>一般来说,并不是说搜索引擎收录的页面越多越好,相反如果收录的不同 URL 都指向了同样的内容,很可能被判作弊,从而导致站点被降权。</p> <p>从访问日志中,可以看到这家搜索引擎在大量抓取本站首页,并带上了一个无意义的 p 参数:</p> <pre><code class="lang-bash">106.120.173.72 - - [10/Dec/2016:05:50:43 +0800] <span class="hljs-string">"GET /index.html?p=142&amp;pn=7 HTTP/1.1"</span> 200 6098 <span class="hljs-string">"-"</span> <span class="hljs-string">"Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)"</span> 0.007 0.007 106.120.173.72 - - [10/Dec/2016:05:50:53 +0800] <span class="hljs-string">"GET /index.html?p=134&amp;pn=1 HTTP/1.1"</span> 200 4793 <span class="hljs-string">"-"</span> <span class="hljs-string">"Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)"</span> 0.007 0.007 106.120.173.72 - - [10/Dec/2016:05:51:03 +0800] <span class="hljs-string">"GET /index.html?p=94&amp;pn=1 HTTP/1.1"</span> 200 4793 <span class="hljs-string">"-"</span> <span class="hljs-string">"Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)"</span> 0.006 0.006 </code></pre> <p>本站没有使用 p 参数,这样会导致 Spider 抓取的页面虽然 URL 不一样,但内容完全一样,从而导致大量重复索引。如果是 Google 出现这种情况,可以通过 Google Webmaster 告诉 Spider 忽略指定参数。但这家搜索引擎的站长平台我一直无法认证成功,所以这条路不通。</p> <p>有了 VeryNginx,这种情况就很好处理了。首先通过 UserAgent 是否包含关键字、请求中是否存在 p 参数两个条件,对流量进行标记:</p> <p><img src="https://st.imququ.com/static/uploads/2016/12/very-nginx-matcher.png" width="685" alt="verynginx matcher" itemprop="image" height="238"></p> <p>然后使用「Filter」这个 Custom Action,直接将拥有这个标记的流量响应为 404:</p> <p><img src="https://st.imququ.com/static/uploads/2016/12/very-nginx-action.png" width="685" alt="verynginx action" itemprop="image" height="236"></p> <p>在 Web 控制面板保存配置后,立即生效。马上来测试一下:</p> <pre><code class="lang-bash">curl -I -H<span class="hljs-string">'User-Agent: Sogou web spider/4.0(+http://www.sogou.com/docs/help/webmasters.htm#07)'</span> <span class="hljs-string">'https://imququ.com/?p=95&amp;pn=17'</span> HTTP/1.1 404 Not Found Server: nginx ... ... </code></pre> <p>是不是很棒!通常,如果搜索引擎发现某个网址多次无法访问,就会从将其从索引库及 Spider 抓取列表中移除。</p> <p>可以看到,使用 VeryNginx 对特定流量进行标记和干预,比直接修改 Nginx 配置方便得多,也强大得多。除了前面演示的「Filter」之外,VeryNginx 还提供了「Scheme Lock、Redirect、URI Rewrite、Browser Verify、Frequency Limit」这几个 Custom Action,其中「Browser Verify」可以用来验证发起请求的客户端是否支持 Cookie 或者 JavaScript,达到防 CC 攻击的目的。</p> <p>大家都知道,我特别关注本站的访问速度。经过这段时间的试用,VeryNginx 在请求处理和内存占用上的表现,都令人满意。</p> <p>本文就写到这里,如果想要了解 VeryNginx 更多细节,推荐查看<a href="https://github.com/alexazhou/VeryNginx/wiki/%E7%9B%AE%E5%BD%95">官方文档</a>。</p> <p>本文链接:<a href="https://imququ.com/post/use-verynginx.html">https://imququ.com/post/use-verynginx.html</a>,<a href="https://imququ.com/post/use-verynginx.html#comments" target="_blank">参与讨论</a></p> Sat, 10 Dec 2016 23:35:53 +0800 https://imququ.com/post/use-verynginx.html iOS 10 Safari 视频播放新政策 https://imququ.com/post/new-video-policies-for-ios10.html <p>随着 iOS 10 的正式发布,Safari 也迎来了大量更新,例如新增了对 ES6、CSP2.0、Shadow DOM 等功能和特性的支持。本文为大家介绍 iOS10 自带 Safari 浏览器在视频播放政策上的最新变化。首先划出重点:1)iOS 10 Safari 支持特定视频自动播放;2)iOS 10 Safari 支持视频内联播放。想了解更多细节的同学请接着往下看。</p> <h3 id="-">之前的政策?</h3> <p>在 iPhoneOS 3(没错,当时 iOS 还不叫 iOS)Safari 开始支持 <code>&lt;video&gt;</code> 标签时,苹果人为设置了很多限制,例如视频无法自动播放 —— 甚至连 Meta 信息预加载都被禁用。显然,这是为了避免给用户造成高额流量费用而作出的妥协,顺便还能节省用户手机电量。</p> <p>随着视频的普及,在 iOS 8 中,苹果放宽了对 Safari 视频播放的限制:允许使用 <code>preload=&quot;metadata&quot;</code> 来预加载视频 Meta 信息。但仅此而已,Safari 中的 <code>&lt;video&gt;</code> 元素仍然无法自动播放,也无法内联播放 —— 这意味着视频只能在用户主动操作后才能播放,且播放时必须全屏。</p> <p>至于「用户主动操作」具体指的是哪些行为,苹果官方有详细的说明:</p> <ul> <li>点击视频播放按钮;</li> <li>触发 <code>touchend</code>、<code>click</code>、<code>doubleclick</code> 或 <code>keydown</code> 事件,且在事件处理函数中直接调用 <code>video.play()</code> 方法。显然,<code>button.addEventListener(&#39;click&#39;, () =&gt; { video.play(); })</code> 满足要求;而 <code>video.addEventListener(&#39;canplaythrough&#39;, () =&gt; { video.play(); })</code> 不满足要求;</li> </ul> <p>值得注意的是,上面讨论的是 iOS 自带 Safari 的视频播放政策。对于 iOS APP 而言,开发者在给 webview 设置 <code>mediaPlaybackRequiresUserAction</code> 和 <code>allowsInlineMediaPlayback</code> 属性之后,页面中的 <code>&lt;video&gt;</code> 标签就可以通过 <code>autoplay</code> 和 <code>webkit-playsinline</code> 属性来启用自动播放和内联播放功能。</p> <h3 id="ios-10-">iOS 10 新政策</h3> <p>随着视频的进一步普及,在 iOS 10 中,苹果终于进一步放松了 Safari 视频播放政策。</p> <h4 id="-">自动播放</h4> <p>iOS 10 Safari 允许自动播放以下两种视频:</p> <ul> <li>无音轨视频;</li> <li>无声音视频(设置了 <code>muted</code> 属性);</li> </ul> <p>对于这两种类型的视频,可以通过 <code>&lt;video autoplay&gt;</code> 或 <code>video.play()</code> 两种方式来自动播放,无需用户主动操作。但是,如果它们在播放时变得有声音(获取了音轨,或者 <code>muted</code> 属性被取消),Safari 会暂停播放。</p> <p>通过 <code>&lt;video autoplay&gt;</code> 自动播放的视频元素还需要满足一个条件:在可视区域内。同样,如果它们在播放时因为页面滚动等原因导致不可见,Safari 也会暂停播放。</p> <p>通过 <code>video.play()</code> 自动播放的视频元素无需可见。<code>video.play()</code> 返回的是 <code>Promise</code>,如果不满足自动播放条件,会触发 <code>reject</code> 行为。</p> <h4 id="-">内联播放</h4> <p>在 iOS 10 Safari 中,通过 <code>&lt;video playsinline&gt;</code> 可以让视频内联播放。设置了 <code>playsinline</code> 属性的视频在播放时不会自动全屏,但用户可以点击全屏按钮来手动全屏;没有设置 <code>playsinline</code> 的视频会在播放时自动全屏。无论是否设置 <code>playsinline</code> 属性,退出全屏后视频都会继续播放。</p> <p><code>playsinline</code> 属性在 iOS 10 之前需要写成 <code>webkit-playsinline</code>,它的浏览器厂商前缀在 iOS 10 中被移除。但是目前 iOS 微信还不支持去掉前缀的写法,两个属性最好都加上。</p> <p>显然,<code>&lt;video autoplay&gt;</code> 必须和 <code>playsinline</code> 属性一起使用。也就是说,只有默认内联播放的视频才有可能自动播放,这一点很容易理解。</p> <h3 id="-">一些典型应用</h3> <p>根据苹果公司的文章:GIF 相比 H.264 编码的视频,带宽占用为十二倍,电池消耗为两倍。没有声音的 <code>&lt;video&gt;</code> 元素很适合用作网页背景,取代 GIF:</p> <pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">video</span> <span class="hljs-attr">autoplay</span> <span class="hljs-attr">loop</span> <span class="hljs-attr">muted</span> <span class="hljs-attr">playsinline</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">source</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"image.mp4"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">source</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"image.webm"</span> <span class="hljs-attr">onerror</span>=<span class="hljs-string">"fallback(parentNode)"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"image.gif"</span>&gt;</span> <span class="hljs-tag">&lt;/<span class="hljs-name">video</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="actionscript"> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fallback</span><span class="hljs-params">(video)</span> </span>{ <span class="hljs-keyword">var</span> img = video.querySelector(<span class="hljs-string">'img'</span>); <span class="hljs-keyword">if</span> (img) { video.parentNode.replaceChild(img, video); } } </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> </code></pre> <p>这段代码使用 <code>&lt;video&gt;</code> 来替代 GIF 动画,并考虑了降级。但在 iOS 10- Safari 中,由于视频无法自动并内联播放,体验很差。要解决这个问题,可以使用 CSS 的 Media Queries:</p> <pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css"> <span class="hljs-selector-id">#either-gif-or-video</span> <span class="hljs-selector-tag">video</span> { <span class="hljs-attribute">display</span>: none; } @<span class="hljs-keyword">media</span> (-webkit-video-playable-inline) { <span class="hljs-selector-id">#either-gif-or-video</span> <span class="hljs-selector-tag">img</span> { <span class="hljs-attribute">display</span>: none; } <span class="hljs-selector-id">#either-gif-or-video</span> <span class="hljs-selector-tag">video</span> { <span class="hljs-attribute">display</span>: initial; } } </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"either-gif-or-video"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">video</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"image.mp4"</span> <span class="hljs-attr">autoplay</span> <span class="hljs-attr">loop</span> <span class="hljs-attr">muted</span> <span class="hljs-attr">playsinline</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">video</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"image.gif"</span>&gt;</span> <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span> </code></pre> <p>由于没有声音的 <code>&lt;video&gt;</code> 元素可以通过调用 <code>video.play()</code> 来自动播放,并且 <code>&lt;video&gt;</code> 元素不需要插入到 DOM 中,我们还可以使用 <code>&lt;canvas&gt;</code> 来做为视频播放的容器,这样就可以方便地修改视频画面了。示意代码如下:</p> <pre><code class="lang-js"><span class="hljs-keyword">var</span> video; <span class="hljs-keyword">var</span> canvas; <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">startPlayback</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-keyword">if</span> (!video) { video = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'video'</span>); video.src = <span class="hljs-string">'image.mp4'</span>; video.loop = <span class="hljs-literal">true</span>; video.addEventListener(<span class="hljs-string">'playing'</span>, paintVideo); } video.play(); } <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">paintVideo</span>(<span class="hljs-params"></span>) </span>{ <span class="hljs-keyword">if</span> (!canvas) { canvas = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">'canvas'</span>); canvas.width = video.videoWidth; canvas.height = video.videoHeight; <span class="hljs-built_in">document</span>.body.appendChild(canvas); } canvas.getContext(<span class="hljs-string">'2d'</span>).drawImage(video, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, canvas.width, canvas.height); <span class="hljs-keyword">if</span> (!video.paused) { requestAnimationFrame(paintVideo); } } startPlayback(); </code></pre> <p>本文主要内容来自于 Webkit 官方博客:<a href="https://webkit.org/blog/6784/new-video-policies-for-ios/">New &lt;video&gt; Policies for iOS</a>。</p> <p>本文链接:<a href="https://imququ.com/post/new-video-policies-for-ios10.html">https://imququ.com/post/new-video-policies-for-ios10.html</a>,<a href="https://imququ.com/post/new-video-policies-for-ios10.html#comments" target="_blank">参与讨论</a></p> Fri, 07 Oct 2016 15:20:46 +0800 https://imququ.com/post/new-video-policies-for-ios10.html 开始使用 ECC 证书 https://imququ.com/post/ecc-certificate.html <p>我之前的文章多次提到 ECC 证书,但一直没有专门介绍 ECC 证书的文章,今天补上。本文包含三部分内容:1)简单介绍 ECC 证书是什么;2)介绍如何申请 ECC 证书;3)以 Nginx 为例介绍如何使用 ECC 证书。顺便说下,本站已经用上了 ECC 证书。要查看本站主要支持哪些技术特性,<a href="https://imququ.com/post/readme.html">可以点这里</a>。</p> <h3 id="-">简单介绍</h3> <p>HTTPS 通过 TLS 层和证书机制提供了内容加密、身份认证和数据完整性三大功能,可以有效防止数据被监听或篡改,还能抵御 MITM(中间人)攻击。TLS 在实施加密过程中,需要用到非对称密钥交换和对称内容加密两大算法。</p> <p>对称内容加密强度非常高,加解密速度也很快,只是无法安全地生成和保管密钥。在 TLS 协议中,应用数据都是经过对称加密后传输的,传输中所使用的对称密钥,则是在握手阶段通过非对称密钥交换而来。常见的 AES-GCM、ChaCha20-Poly1305,都是对称加密算法。</p> <p>非对称密钥交换能在不安全的数据通道中,产生只有通信双方才知道的对称加密密钥。目前最常用的密钥交换算法有 RSA 和 ECDHE:RSA 历史悠久,支持度好,但不支持 PFS(Perfect Forward Secrecy);而 ECDHE 是使用了 ECC(椭圆曲线)的 DH(Diffie-Hellman)算法,计算速度快,支持 PFS。要了解更多 RSA 和 ECDHE 密钥交换的细节,可以阅读 Cloudflare 的<a href="https://blog.cloudflare.com/keyless-ssl-the-nitty-gritty-technical-details/">这篇文章</a>。</p> <p>只有非对称密钥交换,依然无法抵御 MITM 攻击,还得引入身份认证机制。对于大部分 HTTPS 网站来说,服务端一般通过 HTTP 应用层的帐号体系就能完成客户端身份认证;而浏览器想要验证服务端身份,需要用到服务端提供的证书。</p> <p>浏览器会在两个步骤中用到证书:1)证书合法性校验。确保证书由合法 CA 签署,且适用于当前网站;2)使用证书提供的非对称加密公钥,完成密钥交换和服务端认证。</p> <p>证书合法性校验的原理,简单总结如下:</p> <ul> <li>根据版本号、序列号、签名算法标识、发行者名称、有效期、证书主体名、证书主体公钥信息、发行商唯一标识、主体唯一标识、扩展等信息,生成 TBSCertificate(To Be Signed Certificate)信息;</li> <li>签发数字签名:使用 HASH 函数对 TBSCertificate 计算得到消息摘要,再用 CA 的私钥进行加密,得到签名;</li> <li>校验数字签名:使用相同的 HASH 函数对 TBSCertificate 计算得到消息摘要,与使用 CA 公钥解密签名得到内容相比较;</li> </ul> <p>可以看到校验证书需要同时用到签名和非对称加密算法:目前必须使用 SHA-2 做为证书签名函数(没有打 XP SP3 补丁的 IE6 不支持);目前一般使用 RSA 算法对 TBSCertificate 进行非对称加密。可以通过 openssl 工具来查看证书签名算法:</p> <pre><code class="lang-shell">$ openssl x509 -in chained.pem -noout -text | grep <span class="hljs-string">'Signature Algorithm'</span> Signature Algorithm: sha256WithRSAEncryption </code></pre> <p>大部分 CA 都有证书链,浏览器对于收到的多级证书,需要从站点证书开始逐级验证,直至出现操作系统或浏览器内置的受信任 CA 根证书。</p> <p>浏览器还需要校验当前访问的域名是否存在于证书 TBSCertificate 的 <code>Common Name</code> 或 <code>Subject Alternative Name</code> 字段之中。</p> <p>在 RSA 密钥交换中,浏览器使用证书提供的 RSA 公钥加密相关信息,如果服务端能解密,意味着服务端拥有证书对应的私钥,同时也能算出对称加密所需密钥。密钥交换和服务端认证合并在一起。</p> <p>在 ECDHE 密钥交换中,服务端使用证书私钥对相关信息进行签名,如果浏览器能用证书公钥验证签名,就说明服务端确实拥有对应私钥,从而完成了服务端认证。密钥交换和服务端认证是完全分开的。</p> <p>可用于 ECDHE 数字签名的算法主要有 RSA 和 ECDSA,也就是目前密钥交换 + 签名有三种主流选择:</p> <ul> <li>RSA 密钥交换(无需签名);</li> <li>ECDHE 密钥交换、RSA 签名;</li> <li>ECDHE 密钥交换、ECDSA 签名;</li> </ul> <p>以下是 Chrome 中这三种密钥交换方式的截图(截图来自于早期 Chrome,新版 Chrome 查看位置有了变化):</p> <p><img alt="key exchange" src="https://st.imququ.com/static/uploads/2016/03/key_exchange.png" itemprop="image" width="790" height="356"></p> <p>内置 ECDSA 公钥的证书一般被称之为 ECC 证书,内置 RSA 公钥的证书就是 RSA 证书。由于 256 位 ECC Key 在安全性上等同于 3072 位 RSA Key,加上 ECC 运算速度更快,ECDHE 密钥交换 + ECDSA 数字签名无疑是最好的选择。由于同等安全条件下,ECC 算法所需的 Key 更短,所以 ECC 证书文件体积比 RSA 证书要小一些。以下是本站的对比,可以看到左侧的 ECC 证书要小 1/3:</p> <p><img src="https://st.imququ.com/static/uploads/2016/08/ecc-certificate-vs-rsa-certificate.png" width="550" alt="ecc certificate vs rsa certificate" itemprop="image" height="396"></p> <p>RSA 证书可以用于 RSA 密钥交换(RSA 非对称加密)或 ECDHE 密钥交换(RSA 非对称签名);而 ECC 证书只能用于 ECDHE 密钥交换(ECDSA 非对称签名)。</p> <p>并不是所有浏览器都支持 ECDHE 密钥交换,也就是说 ECC 证书的兼容性要差一些。例如在 Windows XP 中,使用 ECC 证书的网站只有 Firefox 能访问(Firefox 的 TLS 自己实现,不依赖操作系统);Android 平台中,也需要 Android 4+ 才支持 ECC 证书。</p> <p>好消息是,Nginx 1.11.0 开始提供了对 RSA/ECC 双证书的支持。它的实现原理是:分析在 TLS 握手中双方协商得到的 Cipher Suite,如果支持 ECDSA 就返回 ECC 证书,否则返回 RSA 证书。</p> <p>也就是说,配合最新的 Nginx,我们可以使用 ECC 证书为现代浏览器提供更好的体验,同时老旧浏览器依然会得到 RSA 证书,从而保证了兼容性。这一次,鱼与熊掌可以兼得。</p> <h3 id="-">如何申请</h3> <p>如果你的 CA 支持签发 ECC 证书,使用以下命令生成 CSR(Certificate Signing Request,证书签名请求)文件并提交给提供商,就可以获得 ECC 证书:</p> <pre><code class="lang-shell">openssl ecparam -genkey -name secp256r1 | openssl ec -out ecc.key openssl req -new -key ecc.key -out ecc.csr </code></pre> <p>以上命令中可供选择的算法有 secp256r1 和 secp384r1,secp521r1 已被 Chrome 和 Firefox 废弃。</p> <p>我目前在用的 Let&#39;s Encrypt,也支持签发 ECC 证书。我使用了 <a href="https://github.com/Neilpang/acme.sh">acme.sh</a> 这个小巧的工具来签发证书,指定 <code>-k ec-256</code> 就可以将证书类型改为 ECC:</p> <pre><code class="lang-shell"><span class="hljs-string">"/root/.acme.sh"</span>/acme.sh --issue --dns dns_cx <span class="hljs-_">-d</span> imququ.com <span class="hljs-_">-d</span> www.imququ.com -k ec-256 </code></pre> <p>目前 Let&#39;s Encrypt 只提供 RSA 中间证书,官方预计会在 2017 年 3 月底提供 ECC 中间证书(<a href="https://letsencrypt.org/upcoming-features/">via</a>)。</p> <h3 id="-">如何使用</h3> <p>有了 RSA/ECC 双证书之后,还需要安装 Nginx 1.11.x。这部分内容我之前详细写过,<a href="https://imququ.com/post/my-nginx-conf.html">请点击查看</a>。</p> <p>一切准备妥当后,将证书配置改为双份即可:</p> <pre><code class="lang-nginx"><span class="hljs-attribute">ssl_certificate</span> example.com.rsa.crt; <span class="hljs-attribute">ssl_certificate_key</span> example.com.rsa.key; <span class="hljs-attribute">ssl_certificate</span> example.com.ecdsa.crt; <span class="hljs-attribute">ssl_certificate_key</span> example.com.ecdsa.key; </code></pre> <p>问题来了!本站使用 Cloudflare 提供的 Cipher Suites 配置,在 Nginx 中配置了双证书并重启,用 Chrome 测试发现仍然没有采用 ECC 证书。这是为什么呢?</p> <pre><code class="lang-nginx"><span class="hljs-comment"># https://github.com/cloudflare/sslconfig/blob/master/conf</span> <span class="hljs-attribute">ssl_ciphers</span> EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; <span class="hljs-attribute">ssl_prefer_server_ciphers</span> <span class="hljs-literal">on</span>; </code></pre> <p>研究发现,Chrome 与服务端协商到的 Cipher Suites 是 <code>ECDHE-RSA-AES128-GCM-SHA256</code>,来自于 <code>ssl_ciphers</code> 配置中的 <code>EECDH+AES128</code> 这部分。我们通过 openssl 工具看一下 <code>EECDH+AES128</code> 具体包含哪些 Cipher Suites:</p> <pre><code class="lang-shell">openssl ciphers -V <span class="hljs-string">'EECDH+AES128'</span> | column -t 0xC0,0x2F - ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(128) Mac=AEAD 0xC0,0x2B - ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(128) Mac=AEAD 0xC0,0x27 - ECDHE-RSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA256 0xC0,0x23 - ECDHE-ECDSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA256 0xC0,0x13 - ECDHE-RSA-AES128-SHA SSLv3 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA1 0xC0,0x09 - ECDHE-ECDSA-AES128-SHA SSLv3 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA1 </code></pre> <p>可以看到,使用 RSA 做为签名认证算法(Au=RSA)的加密套件排到了前面,导致 Nginx 作出了错误判断。</p> <p>知道原因就好办了,将这段配置改为 <code>EECDH+ECDSA+AES128:EECDH+aRSA+AES128</code>,再看一下:</p> <pre><code class="lang-shell">openssl ciphers -V <span class="hljs-string">'EECDH+ECDSA+AES128:EECDH+aRSA+AES128'</span> | column -t 0xC0,0x2B - ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(128) Mac=AEAD 0xC0,0x23 - ECDHE-ECDSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA256 0xC0,0x09 - ECDHE-ECDSA-AES128-SHA SSLv3 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA1 0xC0,0x2F - ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(128) Mac=AEAD 0xC0,0x27 - ECDHE-RSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA256 0xC0,0x13 - ECDHE-RSA-AES128-SHA SSLv3 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA1 </code></pre> <p>这下就没问题了。</p> <p>并不是所有加密套件都需要把 ECDSA 和 aRSA 分开写,例如 <code>EECDH+CHACHA20</code> 就不需要,ECDSA 默认就在前面:</p> <pre><code class="lang-shell">openssl ciphers -V <span class="hljs-string">'EECDH+CHACHA20'</span> | column -t 0xCC,0xA9 - ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=ECDSA Enc=ChaCha20-Poly1305 Mac=AEAD 0xCC,0xA8 - ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=RSA Enc=ChaCha20-Poly1305 Mac=AEAD </code></pre> <p>最终,我的 Cipher Suites 配置如下,供参考:</p> <pre><code class="lang-nginx"><span class="hljs-attribute">ssl_ciphers</span> EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5; </code></pre> <p>本文链接:<a href="https://imququ.com/post/ecc-certificate.html">https://imququ.com/post/ecc-certificate.html</a>,<a href="https://imququ.com/post/ecc-certificate.html#comments" target="_blank">参与讨论</a></p> Sat, 27 Aug 2016 23:10:18 +0800 https://imququ.com/post/ecc-certificate.html HTTP Alternative Services 介绍 https://imququ.com/post/http-alt-svc.html <p><a href="https://tools.ietf.org/html/rfc7838">HTTP Alternative Services</a>(HTTP 替代服务)是今年上半年由 IESG 通过的一项与 HTTP 有关的新协议。估计很少有人能从名字上猜出它是用来干嘛的,本文从解决什么问题、如何使用以及真实场景下的应用三方面来介绍这份协议。</p> <p>顺便说一下,HTTP 各种协议除了可以在 <a href="https://tools.ietf.org/">tools.ietf.org</a> 找到,还可以前往 <a href="http://httpwg.org/specs/">httpwg.org</a> 查看。后者格式更丰富,阅读体验更好,例如本文介绍的 HTTP Alternative Services 协议也可以通过<a href="http://httpwg.org/specs/rfc7838.html">这个地址</a>查看。</p> <h3 id="-">解决什么问题</h3> <p>在 Web 系统中,我们经常有把用户导向不同服务器的需求,例如让不同地域的用户访问离自己最近的服务器。记得我刚上网那会儿,很多网站都会提供电信 / 网通等不同二级域名,供不同运营商用户选择,这无疑增加了使用成本。当前,一般网站都使用 DNS 服务来解决这个问题:按地域、运营商等条件,将用户 DNS 请求解析到最合适的 IP。这种方案需要配置合理的 TTL(Time To Live)时间,太短会造成客户端频繁发起 DNS 查询,影响访问速度;太长则无法让 DNS 修改及时生效。</p> <p>大型 Web 系统经常会出现某个机房流量过大,需要尽快分流给其它机房这种场景。这时候依靠修改 DNS 解析有点力不从心:一方面由于 DNS 缓存的存在,新的解析不能马上生效;另一方面由于 HTTP 的 keep-alive 机制,已连接的浏览器还会继续使用之前解析到的 IP。</p> <p>而 HTTP Alternative Services 可以很好地解决这个问题:服务端可以将自己的替代服务地址以协议规定的方式告诉浏览器,对于支持这个协议的浏览器来说,后续请求都会使用新地址。</p> <p>协议规定的替代服务地址由三部分组成:协议、主机名和端口。也就是说一个网站的替代服务,可以部署在不同 IP、不同端口,甚至使用不同协议。</p> <p>不同于使用 30x 状态码进行重定向分流,<strong>HTTP Alternative Services 只改变浏览器获取资源的网络方式,上层应用不会感觉到任何变化</strong>。以下是两个示例:</p> <p><img src="https://st.imququ.com/static/uploads/2016/08/http-alt-svr-firefox.png" width="293" alt="http alt server in firefox" itemprop="image" height="128">(截图一:Firefox 48.0.1)</p> <p>在截图一中,浏览器通过 TLS 加密通道发起了 HTTP/2 请求,但上层拿到的 Request URL 仍然是 <code>http://</code> 开头的地址,浏览器地址栏也仍然显示为 <code>http://</code>。</p> <p><img src="https://st.imququ.com/static/uploads/2016/08/http-alt-svr-chrome.png" width="336" alt="http alt server in chrome" itemprop="image" height="142">(截图二:Chrome 54.0.2835.0 canary)</p> <p>在截图二中,浏览器将请求发送给了服务器 444 端口,而上层拿到的 Request URL 没有任何变化。</p> <h3 id="-">如何使用</h3> <p>对于 HTTP/1,协议新增了一个响应头部 <code>Alt-Svc</code>,用来指定替代服务地址,它的基本格式如下:</p> <pre><code>Alt-Svc: h2=&quot;alt.example.com:8000&quot;, h2=&quot;:443&quot;; ma=2592000; persist=1 </code></pre><p><code>h2=&quot;alt.example.com:8000&quot;</code> 这部分内容定义了替代服务使用的协议、主机名和端口,其中主机名和端口可选。多个替代服务之间用英文逗号分隔。</p> <p><code>ma</code> 是 max-age 的缩写,单位为秒。显然,它表示浏览器在指定时间内,可以直接使用替代服务地址。</p> <p>协议还规定,当网络发生变化时(例如从 WIFI 切到 3G),浏览器必须弃用当前所有替代服务,除非定义了 <code>persist=1</code>。</p> <p>对于 HTTP/2,协议新增了一个 <code>ALTSVC</code> 帧,具体定义这里略过。</p> <p>可以看到,对于 HTTP/1 来说,<code>Alt-Svc</code> 头部必须依附于首次响应,只有从第二个请求开始浏览器才会使用替代服务地址;而在 HTTP/2 中,<code>ALTSVC</code> 帧可以独立发送,浏览器从首次请求开始就能用上新地址。</p> <p>目前只有 Firefox 完整支持了 HTTP Alternative Services 协议,以下是在 Firefox 中的测试。</p> <p>首次访问指定地址时,服务端返回了一个 <code>Alt-Svc</code> 头部,指定了替代服务地址:</p> <p><img src="https://st.imququ.com/static/uploads/2016/08/http-alt-svr-firefox-1.png" width="762" alt="http alt server in firefox" itemprop="image" height="611"></p> <p>再次访问时,浏览器就会使用替代服务地址中指定的协议、主机名和端口发起请求。这一切对上层应用透明,但发往替代服务的请求头部,会多出一个 <code>Alt-Used</code> 字段:</p> <p><img src="https://st.imququ.com/static/uploads/2016/08/http-alt-svr-firefox-2.png" width="762" alt="http alt server in firefox" itemprop="image" height="611"></p> <p>需要注意的是,尽管我使用 <code>test.qgy18.com</code> 做为替代服务主机名,但浏览器在向替代服务发起请求时,仍然会使用当前页面的主机名。以下是 Wireshark 抓到的信息,SNI 依然是 <code>qgy18.com</code>:</p> <p><img src="https://st.imququ.com/static/uploads/2016/08/http-alt-svr-wireshark.png" width="762" alt="http alt server in wireshark" itemprop="image" height="611"></p> <p>出于安全考虑,协议提出了两点要求:1)所有替代服务必须基于 TLS 部署;2)原网站为 HTTPS 的情况下,替代服务必须使用原网站证书部署。特别地,在 Chrome 中,只有 HTTPS 原网站才能使用替代服务。</p> <h3 id="-">真实案例</h3> <p>Firefox 37 基于 HTTP Alternative Services 协议提出了一个非常有意思的方案:<a href="http://bitsup.blogspot.tw/2015/03/opportunistic-encryption-for-firefox.html">Opportunistic Encryption</a>(不知道怎么翻译,请读者自己意会,后续简称为 OE),我们一起来看下。</p> <p>我们知道对于 HTTPS 而言,如果没有证书信任链校验机制,无法抵御 MITM(中间人)攻击。但 Firefox 认为,不校验证书的 TLS 网站怎么也比纯明文传输要强,对于那些短期内无法切换到 HTTPS 的网站来说,OE 提供了一种可以使用 TLS 进行传输加密,但不进行证书校验的折衷方案。</p> <p>实施这个方案只需以下两步:</p> <ul> <li>部署 HTTP/2 over TLS 服务,允许使用自签名证书;</li> <li>给网站响应头部加上:<code>Alt-Svc: h2=&quot;:443&quot;; ma=600</code>;</li> </ul> <p>当用户通过 <code>http://yourdomain.com</code> 访问网站时,对于能够识别 <code>Alt-Svc</code> 的浏览器来说,后续所有流量都会使用新协议(HTTP/2 over TLS)、新端口(443)。同时对于上层应用来说,URL 依然是 <code>http://yourdomain.com</code>,没有任何变化。也就是说,OE 在提升安全性和性能的同时,无需对 Web 应用代码作出任何修改。</p> <p>但在 OE 中,用户首次访问还是走的 HTTP,响应很容易就被篡改,从而去掉 <code>Alt-Svc</code> 头部;即便是后续使用基于 TLS 部署的替代服务地址,由于浏览器不校验证书,还是很容易被攻击。所以,它只能将 HTTP 网站从<strong>极不安全</strong>提升到<strong>不安全</strong>,作用有限。</p> <p>有趣的是,Firefox 37 推出 OE 之后,很快就被发现存在巨大的安全问题,官方不得不迅速推出 37.0.1 来禁用 OE。当然,产生问题的原因是代码实现上的疏忽,修复 Bug 后,Firefox 又重新启用了它。</p> <p>Chrome 并不打算支持 Opportunistic Encryption,考虑 Firefox 当前的市场占有率,我个人觉得对于 OE 方案,大家了解下就可以了。</p> <p>本文链接:<a href="https://imququ.com/post/http-alt-svc.html">https://imququ.com/post/http-alt-svc.html</a>,<a href="https://imququ.com/post/http-alt-svc.html#comments" target="_blank">参与讨论</a></p> Sun, 21 Aug 2016 22:27:29 +0800 https://imququ.com/post/http-alt-svc.html 谈谈 Nginx 的 HTTP/2 POST Bug https://imququ.com/post/nginx-http2-post-bug.html <p>几个月前,我发现在某些情况下,使用 Safari 无法登录我的博客后台。当时研究了一下,发现这是 Nginx 处理 HTTP/2 POST 请求的一个 Bug。由于随后发布的 Nginx 1.11.0 修复了这个 Bug,我没有再持续关注。直到今天看到 <a href="http://v2ex.com/t/300566">v2ex 这个帖子</a>,我才发现 Nginx <strong>并没有</strong>将修复代码合并到当前稳定版中。所以,<strong>如果你在使用 Nginx 1.9.15~1.10.x 部署 HTTP/2 服务,请务必看完本文</strong>。</p> <h3 id="bug-">Bug 复现</h3> <p>复现这个 Bug 需要同时满足以下几个条件:</p> <ul> <li>使用 Nginx 特定版本(1.9.15~1.10.x)部署 HTTP/2 服务;</li> <li>使用特定的 HTTP/2 客户端,例如 OSX/iOS Safari、iOS 客户端、OkHttp 等(Chrome 没问题,MS IE/Edge 据说也有问题,但我没测试);</li> <li>只有 POST 场景才有问题(也就是说必须存在 DATA 帧);</li> <li>需要在建立 HTTP/2 连接后立即 POST(例如打开表单页面,再断网重连或重启 Nginx,不刷新页面直接提交);</li> </ul> <p>我用 OSX 10.11.6 自带的 Safari 9.1.2 可以稳定复现这个 Bug。触发 Bug 后,Safari 会提示无法连接到服务器。如下图:</p> <p><img src="https://st.imququ.com/static/uploads/2016/08/nginx-http2-bug.gif" width="631" alt="nginx http/2 post bug" itemprop="image" height="471"></p> <p>如果事先开启了 Nginx 的 debug 日志,可以找到类似这样的记录:</p> <pre><code>client sent stream with data before settings were acknowledged while processing HTTP/2 connection </code></pre><h3 id="-">产生原因</h3> <p>为了减少网络时延,不少 HTTP/2 客户端会在建立 HTTP/2 连接时同时发送其它帧,包括用来 POST 数据的 DATA 帧。而 Nginx 在客户端接受到 SETTINGS 帧之前,一直将初始窗口大小(initial window size)设置为 0。也就是说,客户端收到 SETTINGS 帧之前发送的 DATA 帧,会被 Nginx 以 REFUSED_STREAM 帧拒绝。而部分客户端在收到 REFUSED_STREAM 帧之后,会提示连接失败,而不是发起重试,这就是产生 Bug 的原因。</p> <p>那么,Nginx 这个逻辑合理吗,客户端提前发送 DATA 帧符合 HTTP/2 协议规定吗?HTTP/2 协议中的「HTTP/2 Connection Preface」章节有以下描述:</p> <blockquote> <p>To avoid unnecessary latency, clients are permitted to send additional frames to the server immediately after sending the client connection preface, without waiting to receive the server connection preface. It is important to note, however, that the server connection preface SETTINGS frame might include parameters that necessarily alter how a client is expected to communicate with the server. Upon receiving the SETTINGS frame, the client is expected to honor any parameters established. In some configurations, it is possible for the server to transmit SETTINGS before the client sends additional frames, providing an opportunity to avoid this issue. <a href="https://http2.github.io/http2-spec/#ConnectionHeader">via</a></p> </blockquote> <p>出于减少时延的目的,HTTP/2 协议允许客户端在发送连接序言(connection preface)之后,立即发送其它帧,无需等待来自服务端的 SETTTINGS 帧。</p> <p>而 Nginx 能够正常处理客户端提前发送的其它帧,唯独 DATA 帧不行。因为客户端尚未收到 SETTINGS 帧之前,Nginx 将初始窗口大小设置为 0。</p> <p>那么 Nginx 的初始窗口大小应该设置为多少才合理呢?以下这段内容来自于 HTTP/2 协议的「Initial Flow-Control Window Size」章节:</p> <blockquote> <p>Prior to receiving a SETTINGS frame that sets a value for SETTINGS_INITIAL_WINDOW_SIZE, an endpoint can only use the default initial window size when sending flow-controlled frames. Similarly, the connection flow-control window is set to the default initial window size until a WINDOW_UPDATE frame is received. <a href="https://http2.github.io/http2-spec/#InitialWindowSize">via</a></p> </blockquote> <p>也就是说 Nginx 应该将默认的初始窗口大小设置为 64KB。</p> <h3 id="-">如何解决</h3> <p>我猜测 Nginx 这么做是为了减少被攻击的风险,但无论如何这不符合 HTTP/2 协议规定,也造成了特定场景下 POST 请求不可用。<strong>Nginx 在 1.11.0 中解决了这一问题</strong>,并增加了一个配置项:</p> <blockquote> <p>Syntax: http2_body_preread_size size; <br> Default: http2_body_preread_size 64k; <br> Context: http, server <br> This directive appeared in version 1.11.0. <br> <br> Sets the size of the buffer per each request in which the request body may be saved before it is started to be processed. <a href="https://nginx.org/en/docs/http/ngx_http_v2_module.html#http2_body_preread_size">via</a></p> </blockquote> <p><code>http2_body_preread_size</code> 用来定义 Nginx 在客户端收到 SETTINGS 帧之前可以接受多大的 DATA 帧,默认为 64KB。如果将这个值设置为 0,那就跟之前版本的 Nginx 变得一样。</p> <p>需要特别注意的是,<strong>这个 Bug 由 Nginx 1.9.15 引入,而官方表示修复方案不会被移植到当前稳定版中,也就是对于 Nginx 1.9.15~1.10.x,这个问题将始终存在</strong>。对此,Nginx 有如下解释:</p> <blockquote> <p>We don&#39;t backport features to the stable branch (that&#39;s what we call stable, no enhancements). It receives only critical bug fixes. If you use such new, very complicated and actively developing protocol as HTTP/2 then it&#39;s naturally that you have to stay with the mainline branch. <a href="https://trac.nginx.org/nginx/ticket/959#comment:25">via</a></p> </blockquote> <p>简而言之,Nginx 认为 HTTP/2 功能本身尚未稳定,要部署 HTTP/2 就应该使用 Nginx 主线版,而不是稳定版。从更新日志来看,Nginx 最近几个主线版本也一直在修复与 HTTP/2 有关的问题,印证了这一说法。</p> <p>HTTP/2 是一项年轻的技术,也是 HTTP 历史上最大的一次革新。在实践 HTTP/2 过程中,一定要有时刻踩坑的心理准备,更要时刻关注 HTTP/2 协议和实现者的最新动态。对于重要业务,一定要在充分测试和评估之后再推进 HTTP/2。</p> <blockquote> <p>Update @ 2016.10.21,Nginx 最终还是在 <a href="http://nginx.org/en/CHANGES-1.10">1.10.2</a> 也修复了这个 Bug,使用稳定版的同学可以考虑升级了。感谢 @ZE3kr 的反馈。</p> </blockquote> <p>本文链接:<a href="https://imququ.com/post/nginx-http2-post-bug.html">https://imququ.com/post/nginx-http2-post-bug.html</a>,<a href="https://imququ.com/post/nginx-http2-post-bug.html#comments" target="_blank">参与讨论</a></p> Sat, 20 Aug 2016 14:03:03 +0800 https://imququ.com/post/nginx-http2-post-bug.html 开始使用 Docker https://imququ.com/post/use-docker.html <p>一年前,我在《<a href="https://imququ.com/post/vagrantup.html">开始使用 Vagrant</a>》一文中写到:使用虚拟化软件安装 Linux,有着「统一线下线上环境、不受升级宿主系统的影响、容易备份和恢复」等几大优点,非常适用于搭建 WEB 开发环境。</p> <p>但 Vagrant 这种依赖 VirtualBox/VMWare/Parallels Desktop 等软件虚拟完整操作系统的方案有几个硬伤,例如占用大量系统资源、新建或启动虚拟机不够迅速等。Docker 是操作系统级虚拟化,它虚拟出来的环境一般被称为 Docker 容器,而不是虚拟机。Docker 容器直接运行在宿主系统的操作系统内核之上,启动一个新的 Docker 容器能在秒级完成。</p> <p>由于 Docker 轻量、快速和高效,除了用于搭建开发环境,Docker 容器也非常适合用来部署线上服务。最近我将本博客程序改用 Docker 部署,你现在看到的页面正是由 Docker 容器提供服务。本文介绍这一过程。</p> <h3 id="-docker">安装 Docker</h3> <p><a href="https://www.docker.com/products/overview#/install_the_platform">Docker 官方文档</a>详尽地列出了各个系统下的 Docker 安装说明,请直接点过去看,本文不做搬运。</p> <p>对于 Windows/Mac 用户而言,推荐安装 Docker for Window/Mac,而不是 Docker Toolbox。前者可以直接利用宿主系统的虚拟化机制,拥有更好的性能;后者需要借助 VirtualBox 运行的 Linux 虚拟机。</p> <h3 id="-">镜像和容器</h3> <p>Docker 基于 Docker 镜像运行容器,通常我们所需大部分镜像都可以在 <a href="https://hub.docker.com/">hub.docker.com</a> 找到。</p> <p>在装好 Docker 的终端中,运行以下命令就可以启动容器:</p> <pre><code class="lang-bash">docker run ubuntu uname <span class="hljs-_">-a</span> </code></pre> <p>不出意外,可以看到这样的输出:</p> <pre><code>Unable to find image &#39;ubuntu:latest&#39; locally latest: Pulling from library/ubuntu 2f0243478e1f: Pull complete d8909ae88469: Pull complete 820f09abed29: Pull complete 01193a8f3d88: Pull complete Digest: sha256:8e2324f2288c26e1393b63e680ee7844202391414dbd48497e9a4fd997cd3cbf Status: Downloaded newer image for ubuntu:latest Linux 99bebffc2678 4.4.16-moby #1 SMP Tue Aug 9 17:20:17 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux </code></pre><p><code>docker run</code> 命令用来从指定镜像启动容器。由于我本地没有 ubuntu 镜像,Docker 首先会从官方 Hub 下载它;然后启动容器并执行 <code>uname -a</code> 命令。这个命令是在 Docker 容器内执行,输出的是容器系统信息。</p> <p>查看和管理 Docker 镜像及容器,主要有这些命令:</p> <ul> <li><code>docker images</code>:查看本地已经存在的镜像,<code>-a</code> 列出所有(默认不包括中间镜像);</li> <li><code>docker rmi IMAGE</code>:删除指定的镜像,<code>-f</code> 强制删除;</li> <li><code>docker ps</code>:查看运行中的 Docker 容器,<code>-a</code> 列出所有(默认不包括未运行的容器);</li> <li><code>docker rm CONTAINER</code>:删除指定的容器,<code>-f</code> 强制删除;</li> </ul> <p>使用 Docker 的最佳实践是保持职责单一,一个容器只提供一个服务。我的博客主要有这些服务:</p> <ul> <li>Nginx(80/443);</li> <li>MySQL(3306);</li> <li>Memcached(11211);</li> <li>ElasticSearch(9200);</li> <li>ThinkJS(8085);</li> </ul> <p>考虑到我经常折腾 Nginx,我选择把它留在宿主系统,剩余四个服务则改用 Docker 容器来运行。</p> <h3 id="-">构建镜像</h3> <p>我需要的 Mysql、Memcache、ElasticSearch 容器都可以使用官方镜像来运行。但我的博客系统,使用官方 Node.js 镜像存在两个问题:1)官方镜像中的 npm 是 v2,我希望换成 v3;2)官方镜像没有 libvips 库,无法安装本博客程序所依赖的 sharp npm 包。</p> <p>遇到这种情况,可以在 Docker Hub 看看有无第三方 Docker 镜像能够满足需求,也可以构造自己的镜像。我选择后者。</p> <p>要构建自己的 Docker 镜像,一般都会选定一个已有的镜像做为基础,再在上面增加自己的修改。我的 DockerFile 如下:</p> <pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> marcbachmann/libvips <span class="hljs-keyword">MAINTAINER</span> quguangyu@gmail.com <span class="hljs-keyword">RUN</span> <span class="bash">apt-get update </span> <span class="hljs-comment"># 修改时区</span> <span class="hljs-keyword">RUN</span> <span class="bash">ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime </span> <span class="hljs-comment"># 安装依赖</span> <span class="hljs-keyword">RUN</span> <span class="bash">apt-get install -y \ python \ curl \ build-essential </span> <span class="hljs-comment"># 安装 Node.js v4.x.x LTS</span> <span class="hljs-keyword">RUN</span> <span class="bash">curl <span class="hljs-_">-s</span>L https://deb.nodesource.com/setup_4.x | bash - </span><span class="hljs-keyword">RUN</span> <span class="bash">apt-get install -y nodejs </span> <span class="hljs-comment"># 安装 npm v3 和 pm2</span> <span class="hljs-keyword">RUN</span> <span class="bash">npm install -g npm@3 pm2 </span> <span class="hljs-comment"># 解决 npm 在 docker 下经常 rename 失败的问题。详见:</span> <span class="hljs-comment"># https://forums.docker.com/t/npm-install-doesnt-complete-inside-docker-container/12640/3</span> <span class="hljs-keyword">RUN</span> <span class="bash"><span class="hljs-built_in">cd</span> $(npm root -g)/npm \ &amp;&amp; npm install fs-extra \ &amp;&amp; sed -i <span class="hljs-_">-e</span> s/graceful-fs/fs-extra/ <span class="hljs-_">-e</span> s/fs\.rename/fs.move/ ./lib/utils/rename.js</span> </code></pre> <p>这份 DockerFile 作用是在 <code>marcbachmann/libvips</code> 镜像上增加了我需要的 Node.js,将 npm 升级到了 v3,还安装了 pm2。</p> <p>在 DockerFile 所在目录,执行以下命令就可以构建镜像,并将其推送至 <a href="https://hub.docker.com/">Docker Hub</a>(这里略过注册和登录过程):</p> <pre><code class="lang-shell">docker build -t qgy18/node . docker push qgy18/node </code></pre> <h3 id="docker-compose">Docker Compose</h3> <p>Docker Compose 是一个小工具。我们可以在一个文件里定义多个容器,使用 <code>docker-compose</code> 命令让它们全部运行就绪。Docker Compose 非常适合用来部署 WEB 系统这种需要多个容器配合工作的服务。</p> <p>如果你使用的是 Docker for Windows/Mac,<code>docker-compose</code> 命令应该直接可用。对于 Linux 平台,请参考<a href="https://docs.docker.com/compose/install/">官方文档</a>安装 Docker Compose。</p> <p>当前,我的博客系统目录结构如下:</p> <pre><code>├── blog │ ├── app │ ├── node_modules │ ├── package.json │ ├── pm2.json │ ├── view │ └── www ├── db │ ├── ... │ ├── ququ_blog │ └── sys ├── docker-compose.yml ├── esroot │ ├── config │ ├── data │ └── plugins └── shell ├── backup_blog_database.sh └── install_blog_package.sh </code></pre><p>我将所有需要持久化存储的文件都放在了宿主系统,例如代码目录(blog),数据库文件(db),ElasticSearch 配置、插件及数据文件(esroot)。这样数据更加安全,也更易于管理。</p> <p>shell 目录下的 <code>install_blog_package.sh</code> 用来安装博客 npm 依赖,我的宿主系统没有安装 Node.js,运行 <code>npm install</code> 也需要借助 Docker 容器,一行命令搞定:</p> <pre><code class="lang-shell">docker run -it --rm -v <span class="hljs-string">"<span class="hljs-variable">$PWD</span>/../blog"</span>:/app -w <span class="hljs-string">"/app"</span> qgy18/node npm install --registry=http://registry.npm.taobao.org --production </code></pre> <p>这行命令首先基于前面构建好的镜像运行了一个拥有 Node.js 和 npm3 的容器;然后将宿主系统的 <code>blog</code> 目录映射为容器的 <code>/app</code> 目录;再将容器的工作目录设置为 <code>/app</code>;最后执行 <code>npm install</code> 安装依赖。最为神奇的是,由于指定了 <code>--rm</code> 参数,这个容器在完成工作之后就会被彻底销毁,不留任何痕迹。</p> <p>类似的,由于宿主系统不再需要安装 MySQL,备份数据库也需要在容器内完成,这时候可以使用 <code>docker exec</code> 命令在已经运行的容器内执行指令。以下是 <code>backup_blog_database.sh</code> 文件的内容:</p> <pre><code class="lang-shell">docker <span class="hljs-built_in">exec</span> imququ_db mysqldump -uroot -p****** ququ_blog | gzip &gt; ../backup/ququblog.`date +%H`.sql.gz </code></pre> <p><code>docker-compose.yml</code> 文件内容如下,它定义了每个容器基于什么镜像运行,映射哪些目录,开放哪些端口:</p> <pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'2'</span> <span class="hljs-attr">services:</span> <span class="hljs-attr"> es:</span> <span class="hljs-attr"> image:</span> elasticsearch:<span class="hljs-number">2.3</span><span class="hljs-number">.0</span> <span class="hljs-attr"> container_name:</span> imququ_es <span class="hljs-attr"> volumes:</span> <span class="hljs-bullet"> -</span> ./esroot/data/:/usr/share/elasticsearch/data <span class="hljs-bullet"> -</span> ./esroot/config/:/usr/share/elasticsearch/config <span class="hljs-bullet"> -</span> ./esroot/plugins/:/usr/share/elasticsearch/plugins <span class="hljs-attr"> restart:</span> always <span class="hljs-attr"> expose:</span> <span class="hljs-bullet"> -</span> <span class="hljs-string">"9200"</span> <span class="hljs-attr"> cache:</span> <span class="hljs-attr"> image:</span> memcached:<span class="hljs-number">1.4</span><span class="hljs-number">.29</span> <span class="hljs-attr"> container_name:</span> imququ_cache <span class="hljs-attr"> restart:</span> always <span class="hljs-attr"> expose:</span> <span class="hljs-bullet"> -</span> <span class="hljs-string">"11211"</span> <span class="hljs-attr"> db:</span> <span class="hljs-attr"> image:</span> mysql:<span class="hljs-number">5.7</span><span class="hljs-number">.14</span> <span class="hljs-attr"> container_name:</span> imququ_db <span class="hljs-attr"> volumes:</span> <span class="hljs-bullet"> -</span> <span class="hljs-string">"./db:/var/lib/mysql"</span> <span class="hljs-attr"> restart:</span> always <span class="hljs-attr"> environment:</span> <span class="hljs-attr"> MYSQL_ROOT_PASSWORD:</span> ****** <span class="hljs-attr"> expose:</span> <span class="hljs-bullet"> -</span> <span class="hljs-string">"3306"</span> <span class="hljs-attr"> ports:</span> <span class="hljs-bullet"> -</span> <span class="hljs-string">"127.0.0.1:3306:3306"</span> <span class="hljs-attr"> blog:</span> <span class="hljs-attr"> depends_on:</span> <span class="hljs-bullet"> -</span> es <span class="hljs-bullet"> -</span> cache <span class="hljs-bullet"> -</span> db <span class="hljs-attr"> image:</span> qgy18/node <span class="hljs-attr"> container_name:</span> imququ_blog <span class="hljs-attr"> volumes:</span> <span class="hljs-bullet"> -</span> ./blog:/app <span class="hljs-attr"> restart:</span> always <span class="hljs-attr"> working_dir:</span> /app <span class="hljs-attr"> entrypoint:</span> <span class="hljs-bullet"> -</span> pm2 <span class="hljs-bullet"> -</span> start <span class="hljs-bullet"> -</span> pm2.json <span class="hljs-bullet"> -</span> --<span class="hljs-literal">no</span>-daemon <span class="hljs-attr"> links:</span> <span class="hljs-attr"> - es:</span>es <span class="hljs-attr"> - cache:</span>cache <span class="hljs-attr"> - db:</span>db <span class="hljs-attr"> ports:</span> <span class="hljs-bullet"> -</span> <span class="hljs-string">"127.0.0.1:8085:8085"</span> </code></pre> <p>在 blog 容器中,我通过 <code>links</code> 配置连接了前面几个容器。这样在代码中,就可以使用 <code>es</code> 做为 HOST 连接到 Elasticsearch 容器,使用 <code>db</code> 做为 HOST 连接到 MySQL,依此类推。</p> <p>我定义了 db 容器的 <code>ports</code> 配置,将宿主系统的 3306 端口映射到了 db 容器内,这样我就可以在宿主系统管理 MySQL 服务。同样地,使用宿主系统的 8085 端口可以访问到 blog 容器提供的 WEB 服务。</p> <p>通过 <code>docker-compose up -d</code> 命令就可以在后台启动所有容器。<code>docker ps</code> 可以用来查看各个容器的运行状态:</p> <pre><code>IMAGE COMMAND PORTS NAMES qgy18/node &quot;pm2 start pm2.json -&quot; 127.0.0.1:8085-&gt;8085/tcp imququ_blog elasticsearch:2.3.0 &quot;/docker-entrypoint.s&quot; 9200/tcp, 9300/tcp imququ_es mysql:5.7.14 &quot;docker-entrypoint.sh&quot; 127.0.0.1:3306-&gt;3306/tcp imququ_db memcached:1.4.29 &quot;docker-entrypoint.sh&quot; 11211/tcp imququ_cache </code></pre><p>本博客基于 Docker 容器运行了将近一周,非常稳定。宿主系统整体资源占用跟之前相比,也没有明显变化。前几天我把宿主系统升级到了 Ubuntu 16.04.1 LTS,博客服务没受任何影响,这种体验实在是美妙。</p> <p>本文链接:<a href="https://imququ.com/post/use-docker.html">https://imququ.com/post/use-docker.html</a>,<a href="https://imququ.com/post/use-docker.html#comments" target="_blank">参与讨论</a></p> Sun, 14 Aug 2016 15:03:49 +0800 https://imququ.com/post/use-docker.html