Recent Posts

小莞's avatar

服务性能监控:USE方法(The USE Method)

本文首发在沪江技术学院公众号,小莞翻译,我做了校对。 由于微信公众号的封闭性,我担心未来文章不容易被发现。 为了避免沧海遗珠,特意转到这里。


USE 方法是一种能分析任何系统性能的方法论。 我们可以根据能帮助系统分析的结构化清单,来迅速的定位资源的瓶颈和错误所在。 它通常会先以列出问题为开始,然后再寻找适合的指标,而不是给你制定一些固定的指标, 然后让你按部就班的执行下去。

本页左侧下方,是我列出的,根据不同的操作系统(Linux、 Solaris 等) 衍生的 USE 方法列表。(译者注:可以参考原文链接)

我列出了为不同的操作系统而衍生的 USE 方法列表供大家参考, 你们可以根据你的环境来为你的站点服务,选择适合的附加监控指标。

通过这个工具,可以很方便的筛选出适合不同的系统的建议 metrics:USE Method: Rosetta Stone of Performance Checklists

Intro(Introduction)

如果你遇到一个很严重的性能问题升级的时候,并且你不能确定它是否由服务导致的, 这时候你该怎么办?

我们都说万事开头难。所以我开发出了 USE 方法,来帮助大家,如何去快速的解决常见的性能问题,而同时又不容易忽略重要的地方。

USE 方法在设计之初就定位了简洁、明了、完整、快速的特性, 就好像一本航天手册的紧急事项列表那样。 (译者注:航天手册,介绍包括不限于飞机的各种特性、指标、性能等, 用于帮助飞行学员学习驾驶飞机,或者是帮助那些希望提高他们的飞行潜能和航空知识的人了解的更全面)。

USE 方法已经在不同的企业、课堂(作为学习工具)以及最近的云计算等场景中,被成功应用了无数次。

USE 方法基于 3+1 模型(三种指标类型+一种策略),来切入一个复杂的系统。我发现它仅仅发挥了 5% 的力量,就解决了大概 80% 的服务器问题,并且正如我将证明的,它除了服务器以外,也同样适应于各种系统。

它应当被理解为一种工具,一种很大的方法工具箱里面的工具。不过,它目前仍然还有很多问题类型以待解决,还需要点其他方法和更多的时间。

Summary

USE 方法可以概括为:检查所有的资源的利用率,饱和度,和错误信息。

我们期望大家能尽早使用 USE 方法去做性能检查,或者是用它确定系统的瓶颈。

名词定义:

  • 资源: 服务器功能性的物理组成硬件(CPU, 硬盘, 总线)
  • 利用率: 资源执行某工作的平均时间
  • 饱和:衡量资源超载工作的程度,往往会被塞入队列
  • 错误: 错误事件的数量

分析软件资源,或者是软件的强制性限制(资源控制)也是很有用的,同时要关注哪些指标是处于正常的可接受范围之内的。这些指标通常用以下术语表示:

  • 利用率: 以一个时间段内的百分比来表示,例如:一个硬盘以 90% 的利用率运行
  • 饱和度: 一个队列的长度,例如:CPUs 平均的运行时队列长度是4
  • 错误(数): 可度量的数量,例如:这个网络接口有 50 次(超时?)

我们应该要调查那些错误,因为它们会降低系统的性能,并且当故障模型处于可回复模式的时候,它可能不会立刻被发现。

这包括了那些失败和重试等操作,以及那些来自无效设备池的失效设备。

低利用率是否意味着未饱和?

即使在很长一段时间内利用率很低,一个爆发增长的高利用率,也会导致饱和 and 性能问题,这点要理解起来可能有违三观!

我举个例子,有一位客户遇到的问题,即使他们的监控工具显示 CPU 使用率从来没有超出过 80% ,但是 CPU 饱和度依然有问题(延迟)监控工具报告了 5 分钟的平均值,而其中,CPU利用率曾在数秒内高达 100% 。

资源列表

下面来看如何开始使用。

准备工作时, 你需要一个资源列表来按步就班的去做。 下面是一个服务器的通用列表:

  • CPUs: sockets, cores, hardware threads (virtual CPUs)
  • 内存: 容量
  • 网络接口
  • 存储设备: I/O, 容量
  • 控制器: 存储, 网卡
  • 通道: CPUs, memory, I/O

有些组件分两种类型的资源:存储设备是服务请求资源(I / O) 以及容量资源(population), 两种类型都可能成为系统瓶颈。 请求资源可以定义为队列系统,可以将请求先存入排队然后再消化请求。

有些物理组件已被省略,例如硬件缓存(例如,MMU TLB / TSB,CPU)。

USE 方法对于在高利用率或高饱和度下,遭受性能退化、导致瓶颈的资源最有效,在高利用率下缓存可以提高性能。

在使用 USE 方法排除系统的瓶颈问题之后 ,你可以检查缓存利用率和其他的性能属性。

如果你不确认要不要监控某一个资源时,不要犹豫,监控它,然后你就能看到那些指标工作的有多么的棒。

功能模块示意图

另外一种迭代资源的方法,是找到或者绘制一张系统的功能模块示意图。

这些显示了模块关系的图,在你查找数据流的瓶颈的时候是非常有用的,这里有一张Sun Fire V480 Guide(page 82)的例图:

我喜欢这些图表,尽管制作出它很难。 不过,由硬件工程师来画这张图是最适合的-他们最善于做这类事。如果不信的话你可以自己试试。

在确定各种总线的利用率的同时,为每个总线的功能图表,注释好它的最大带宽。这样我们就能在进行单次测量之前,得到能将系统瓶颈识别出来的图表。

Interconnects

CPU,内存和I / O interconnects 往往被忽略。 幸运的是,它们并不会频繁地成为系统的瓶颈。 不幸的是,如果它们真的频繁的成为瓶颈,我们能做的很少(也许你可以升级主板,或减少 load:例如,"zero copy"项目减轻内存总线 load)。

使用 USE 方法,至少你会意识到你没有考虑过的内容:interconnect 性能。 有关使用 USE 方法确定的互连问题的示例,请参阅分析 Analyzing the HyperTransport。

Metrics

给定资源列表,识别指标类型:利用率,饱和度和错误指标。这里有几个示例。看下面的 table,思考下每个资源和指标类型,metric 列是一些通用的 Unix/Linux 的术语提示(你可以描述的更具体些):


resource type metric CPU utilization CPU utilization (either per-CPU or a system-wide average) CPU saturation run-queue length or scheduler latency(aka Memory capacity utilization available free memory (system-wide) Memory capacity saturation anonymous paging or thread swapping (maybe "page scanning" too) Network interface utilization RX/TX throughput / max bandwidth Storage device I/O utilization device busy percent Storage device I/O saturation wait queue length Storage device I/O errors device errors ("soft", "hard", ...)


这些指标是每段间隔或者计数的平均值,作为你的自定义清单,要包括使用的监控软件,以及要查看的统计信息。如果是不可用的指标,可以打个问号。最后,你会完成一个完事的、简单、易读的 metrics 清单.

Harder Metrics

再来看几个硬件指标的组合


resource type metric CPU errors eg, correctable CPU cache ECC events or faulted CPUs (if the OS+HW supports that) Memory capacity errors Network saturation Storage controller utilization CPU interconnect utilization Memory interconnect saturation I/O interconnect utilization


这些依赖于操作系统的指标一般会更难测量些, 而我通常要用自己写的软件去收集这些指标。

重复所有的组合,并附上获取每个指标的说明,你会完成一个大概有30项指标的列表,其中有些是不能被测量的,还有些是难以测量的。

幸运的是,最常见的问题往往是简单的(例如,CPU 饱和度,内存容量饱和度,网络接口利用率,磁盘利用率),这类问题往往第一时间就能被检查出来。

本文的顶部,pic-1中的 example checklists 可作为参考。

In Practice

读取系统的所有组合指标,是非常耗时的,特别是当你开始使用总线和 interconnect 指标的情况下。

现在我们可以稍微解放下了,USE 方法可以让你了解你没有检查的部分,你可以只有关注其中几项的时间例如:CPUs, 内存容量, 存储容易, 存储设备 I/O, 网络接口等。通过 USE 方法,那些以前未知的未知指标现在变成了已知的未知指标(我理解为,以前我们不知道有哪些指标会有什么样的数据,现在起码能知道我们应该要关注哪些指标)。

如果将来定位一个性能问题的根本原因,对你的公司至关重要的时候,你至少已经有一个明确的、经过验证的列表,来辅助你进行更彻底的分析,请完成适合你自己的 USE 方法,有备无患。

希望随着时间的推移,易于检查的指标能得以增长,因为被添加到系统的 metrics 越多,会使 USE 方法将更容易(发挥它的力量)。 性能监视软件也可以帮上忙,添加 USE 方法向导to do the work for you(do what work? )。

Software Resources

有些软件资源可以用类似的方式去分析。 这通常适用于软件的较小组件,而不是整个应用程序。 例如:

  • 互斥锁(mutex locks):利用率可以定义为锁等待耗时;饱和率定义为等待这把锁的线程个数。
  • 线程池:利用率可以定义为线程工作的时长;饱和率是等待线程池分配的请求数量。
  • 进程/线程 容量:系统是有进程或线程的上限的,它的实际使用情况被定义为利用率;等待数量定义为饱和度;错误即是(资源)分配失败的情况(比如无法 fork)。 (译注:fork 是一个现有进程,通过调用 fork 函数创建一个新进程的过程)
  • 文件描述符容量(file descriptor capacity):和上述类似,但是把资源替换成文件描述符。

如果这几个指标很管用就一直用,要不然软件问题会被遗留给其他方法了(例如,延迟,后文会提到其他方法:other methodologies )。

Suggested Interpretations

USE 方法帮助你定位要使用哪些指标。 在学习了如何从操作系统中读取到这些指标后,你的下一步工作就是诠释它们的值。对于有的人来说, 这些诠释可能是很清晰的(因为他们可能很早就学习过,或者是做过笔记)。而其他并不那么明了的人,可能取决于系统负载的要求或期望 。

下面是一些解释指标类型的通用建议:

  • Utilization: 利用率通常象征瓶颈(检查饱和度可以进一步确认)。高利用率可能开始导致若干问题:
  • 对利用率进行长期观察时(几秒或几分钟),通常来说 70% 的利用率会掩盖掉瞬时的 100% 利用率。
  • 某些系统资源,比如硬盘,就算是高优先级请求来了,也不会在操作进行中被中断。当他们的利用率到 70% 时候,队列系统中的等待已经非常频繁和明显。而 CPU 则不一样,它能在大部分情况下被中断。
  • Saturation:任何非 0 的饱和度都可能是问题。它们通常是队列中排队的时间或排队的长度。
  • Errors:只要有一条错误,就值得去检查,特别是当错误持续发生从而导致性能降低时候。

要说明负面情况很容易:利用率低,不饱和,没有错误。 这比听起来更有用 - 缩小调查范围可以快速定位问题区域。

Cloud Computing

在云计算环境中,软件资源控制可能是为了限制 使用共享计算服务的 tenants 的流量。在 Joyent 公司,我们主要使用操作系统虚拟化(SmartOS),它强加了内存限制, CPU 限制和存储I / O限制。 所有这些资源限制,都可以使用USE Method进行检查,类似于检查物理资源。

例如,在我们的环境中,"内存容量利用率"可以是 tenants 的内存使用率 vs 它的内存上限 。即使传统的 Unix 页面扫描程序可能处于空闲状态,也可以通过匿名页面活动看到"内存容量饱和度"。

Strategy

下面是用流程图 的方式画了 USE 方法的示意图。 请注意,错误检查优先于利用率和饱和度检查(因为通常错误更快的表现出来,并更容易解释)。

USE 方法定位到的问题,可能是系统瓶颈。 不幸的是,系统可能会遇到多个性能问题,因此您发现的第一个可能的问题最终却不是个问题。 发现的每个问题都可以用方法持续的挖掘,然后继续使用 USE 方法对更多资源进行反复排查。

进一步分析的策略包括工作量特征和 drill-down 分析。 完成这些后,你应该有依据据能判断,纠正措施是要调整应用的负载或调整资源本身。

Apollo

(译者注:Apollo 这一段我们可以不太关注,它主要是讲 USE 方法,与阿波罗登月计划相关的系统设计的一些渊源)

我之前有提到过,USE 方法可以被应用到除服务器之外。为了找到一个有趣的例子, 我想到了一个我没有完全不了解的系统,并且不知道从哪里开始:阿波罗月球模块指导系统。 USE 方法提供了一个简单的流程来尝试第一步是寻找一个资源列表,或者更理想的话,找到一个功能模块图表。我在 【Lunar Module - LM10 Through LM14 Familiarization Manual】中发现了以下内容:

这些组件中的一部分可能未表现出利用率或饱和度特性。在迭代后, 就可以重新绘制只包含相关组件的图表(还可以包括:"可擦除存储"部分的内存,"核心区域"和 "vac区域 "寄存器)。

我将从阿波罗主脑(AGC)本身开始。 对于每个指标,我浏览了各种 LM 文档,看看哪些是合理的(有意义的):

  • AGC utilization: This could be defined as the number of CPU cycles doing jobs (not the "DUMMY JOB") divided by the clock rate (2.048 MHz). This metric appears to have been well understood at the time.
  • AGC saturation: This could be defined as the number of jobs in the "core set area", which are seven sets of registers to store program state. These allow a job to be suspended (by the "EXECUTIVE" program - what we\'d call a "kernel" these days) if an interrupt for a higher priority job arrives. Once exhausted, this moves from a saturation state to an error state, and the AGC reports a 1202 "EXECUTIVE OVERFLOW-NO CORE SETS" alarm.
  • AGC errors: Many alarms are defined. Apart from 1202, there is also a 1203 alarm "WAITLIST OVERFLOW-TOO MANY TASKS", which is a performance issue of a different type: too many timed tasks are being processed before returning to normal job scheduling. As with 1202, it could be useful to define a saturation metric that was the length of the WAITLIST, so that saturation can be measured before the overflow and error occurs.

其中的一些细节,可能对于太空爱好者来说是非常熟悉的:在阿波罗 11 号降落的时候发生的著名的 1201("NO VAC AREAS")和 1202 警报。("VAC"是向量加速器的缩写, 用于处理 vector quantities 作业的额外存储; 我觉得 wikipadia 上将 "向量"描述为"空"可能是错误的)。

鉴于阿波罗 11 号的 1201 警报,可以继续使用其他方法分析,如工作负载表征。 工作负载很多可以在功能图中看到,大多数工作负载是通过中断来生效的。 包括用于跟踪命令模块的会合雷达,即使 LM 正在下降,该模块也仍然在执行中断 AGC(阿波罗主脑)的任务。 这是发现非必要工作的一个例子(或低优先级的工作; 雷达的一些更新可能是可取的,因此 LM AGC可以立即计算出中止路径)。

作为一个更深的例子,我将把会合雷达当作资源去检查. 错误最容易识别。 有三种信号类型: "DATA NO GOOD", "NO TRACK", and "SHAFT- AND TRUNNION-AXIS ERROR"。

在有某一小段时间里,我不知道能从哪里开始使用这个方法, 去寻找和研究具体的指标。

Other Methodologies

虽然 USE 方法可能会发现 80% 的服务器问题,但基于延迟的方法(例如Method R)可以找到所有的问题。 不过,如果你不熟悉软件内部结构,Method R 就有可能需要花费更多时间。 它们可能更适合已经熟悉它的数据库管理员或应用程序开发人员。

而 USE 方法的职责和专长包括操作系统(OS)和硬件,它更适合初级或高级系统管理员,当需要快速检查系统健康时,也可以由其他人员使用。

Tools Method

以下介绍一个基于工具的方法流程(我称它作"工具方法"),与 USE 方法作比较:

  1. 列出可用的性能工具(可以选择性安装或购买其他的)。
  2. 列出每个工具提供的有用的指标
  3. 列出每个工具可能的解释规则

按照这个方法做完后,将得到一个符合标准的清单,它告诉我们要运行的工具,要关注的指标以及如何解释它们。 虽然这相当有效,但有一个问题,它完全依赖于可用(或已知的)的,可以提供系统的不完整视图的工具。 用户也不知道他们得到的是一张不完整的视图 - 所以问题将仍然存在。

而如果使用 USE 方法,不同的是, USE 方法将通过迭代系统资源的方式,来创建一个完整的待确认问题列表,然后搜索工具来回答这些问题。这样构建了一张更完整的视图,未知的部分被记录下来,它们的存在被感知(这一句我理解成前文中提到的:未知 的未知变为已知的未知)。 基于 USE ,同样可以开发一个清单类似于工具方法(Tool-Method),显示要运行的工具(可用的位置),要关注的指标以及如何解释它。

另一个问题是,工具方法在遍历大量的工具时,将会使寻找瓶颈的任务性能得到分散。而 USE 方法提供了一种策略,即使是超多的可用工具和指标,也能有效地查找瓶颈和错误。

Conclusion

USE 方法是一个简单的,能执行完整的系统健康检查的策略,它可以识别常见的系统瓶颈和错误。它可以在调查的早期部署并快速定位问题范围,如果需要的话,还可以进一步通过其他方法进行更详细的研究。

我在这个篇幅上,解释了 USE 方法并且提供了通用的指标案例,请参阅左侧导航面板中对应操作系统的示例清单, 其建议了应用 USE 方法的工具和指标。另请参阅基于线程的补充方法,TSA Method。

Acknowledgments

  • 感谢 Cary Millsap and Jeff Holt (2003) 在"优化Oracle性能"一文中提到的 Method R 方法 (以及其他方法), 使我有了灵感,我应该要把这个方法论写出来。
  • 感谢 Sun Microsystems 的组织,包括 PAE 和 ISV, 他们将 USE 方法(那时还没命名)应用于他们的存储设备系列,绘制了标注指标和总线速度的 ASCII 功能块图表 - 这些都比您想象的要困难(我们应该早些时候询问硬件团队的帮助)。
  • 感谢我的学生们,多年前我授予他们这个方法论,谢谢他们提供给我的使用反馈。
  • 感谢 Virtual AGC 项目组(The Virtual AGC project),读他们的站点 ibiblio.org 上的文档库,就象是一种娱乐. 尤其是 LMA790-2 "Lunar Module LM-10 Through LM-14 Vehicle Familiarization Manual" ( 48 页有功能模块图表), 以及 "阿波罗指导和月球导航模块入门学习指南", 都很好的解释了执行程序和它的流程图 (These docs are 109 and 9 Mbytes in size.)
  • 感谢 Deirdré Straughan 编辑和提供反馈,这提高了我的认知。
  • 文章顶部的图片,是来自于波音 707 手册,1969 出版。它不是完整的,点击查看完整的版本(译注:为方便阅读,就是下面这张:)

Updates

USE Method updates:(略)

  • It was published in ACMQ as Thinking Methodically about Performance (2012).
  • It was also published in Communications of the ACM as Thinking Methodically about Performance (2013).
  • I presented it in the FISL13 talk The USE Method (2012).
  • I spoke about it at Oaktable World 2012: video, PDF.
  • I included it in the USENIX LISA `12 talk Performance Analysis Methodology.
  • It is covered in my book on Systems Performance, published by Prentice Hall (2013).

More updates (Apr 2014):

  • LuceraHQ are implementing USE Method metrics on SmartOS for performance monitoring of their high performance financial cloud.
  • LuceraHQ 正在 SmartOS 上,为他们高性能金融云的性能监测,实施 USE 方法指标
  • I spoke about the USE Method for OS X at MacIT 2014 (slides)。

原文链接: https://blog.alswl.com/2017/11/use-method/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
罗晟, alswl's avatar

Stack Overflow 的 HTTPS 化:漫漫长路的终点


今天,我们默认在 Stack Overflow 上部署了 HTTPS。目前所有的流量都将跳转到 https:// 上。与此同时,Google 链接也会在接下去的几周内更改。启用的过程本身只是举手之劳,但在此之前我们却花了好几年的时间。到目前为止,HTTPS 在我们所有的 Q&A 网站上都默认启用了。

在过去的两个月里,我们在 Stack Exchange 全网维持发布 HTTPS。Stack Overflow 是最后,也是迄今最大的的一个站点。这对我们来说是一个巨大里程碑,但决不意味着是终点。后文会提到,我们仍有很多需要做的事情。但现在我们总算能看得见终点了,耶!

友情提示:这篇文章讲述的是一个漫长的旅程。非常漫长。你可能已经注意到你的滚动条现在非常小。我们遇到的问题并不是只在 Stack Exchange/Overflow 才有,但这些问题的组合还挺罕见。我在文章中会讲到我们的一些尝试、折腾、错误、成功,也会包括一些开源项目——希望这些细节对你们有所帮助。由于它们的关系错综复杂,我难以用时间顺序来组织这篇文章,所以我会将文章拆解成架构、应用层、错误等几个主题。

首先,我们要提一下为什么我们的处境相对独特:

  • 我们有几百个域名(大量站点及服务)
  • 我们允许用户提交、嵌入内容(比如帖子中的图片和 YouTube 视频)
  • 我们仅有一个数据中心(造成单源的延时)
  • 我们有广告(及广告网络)
  • 我们用 websockets,任何时刻的活跃数都不少于 50 万个(连接数问题)
  • 我们会被 DDoSed 攻击(代理问题)
  • 我们有不少站点及应用还是通过 HTTP API 通信的(代理问题)
  • 我们热衷于性能(好像有点太过了)

由于这篇文章实在太长,我在这里先列出链接:

开篇

我们早在 2013 年就开始考虑在 Stack Overflow 上部署 HTTPS 了。是的,现在是 2017 年。所以,究竟是什么拖了我们四年?这个问题的答案放在任何一个 IT 项目上都适用:依赖和优先级。老实说,Stack Overflow 在信息安全性上的要求并不像别家那么高。我们不是银行,也不是医院,我们也不涉及信用卡支付,甚至于我们每个季度都会通过 HTTP 和 BT 种子的方式发布我们大部分的数据库。这意味着,从安全的角度来看,这件事情的紧急程度不像它在其他领域里那么高。而从依赖上来说,我们的复杂度比别人要高,在部署 HTTPS 时会在几大领域里踩坑,这些问题的组合是比较特殊的。后文中会看到,有一些域名的问题还是一直存在的。

容易踩坑的几个领域包括:

  • 用户内容(用户可以上传图片或者指定 URL)
  • 广告网络(合同及支持)
  • 单数据中心托管(延迟)
  • 不同层级下的几百个域名(证书)

那我们究竟是为什么需要 HTTPS 呢?因为数据并不是唯一需要安全性的东西。我们的用户中有操作员、开发者、还有各个级别的公司员工。我们希望他们到我们站点的通信是安全的。我们希望每一个用户的浏览历史是安全的。某些用户暗地里喜欢 monad 却又害怕被人发现。同时,Google 会提升 HTTPS 站点的搜索排名(虽然我们不知道能提升多少)。

哦,还有性能。我们热爱性能。我热爱性能。你热爱性能。我的狗热爱性能。让我给你一个性能的拥抱。很好。谢谢。你闻起来很香。

懒人包

很多人喜欢情人包,所以我们来一场快速问答(我们喜欢问答!):

  • 问:你们支持什么协议?
  • 问:你们支持 SSL v2 或者 v3 吗?
  • 问:你们支持哪些加密套件?
  • 问:Fastly 回源走的是 HTTPS 吗?
    • 答:是。如果到 CDN 的请求是 HTTPS,回源的请求也是 HTTPS。
  • 问:你们支持前向安全性吗?
    • 答:是。
  • 问:你们支持 HSTS 吗?
    • 答:支持。我们正在 Q&A 站点中逐步支持。一旦完成的话我们就会将其移至节点上。
  • 问:你们支持 HPKP 吗?
    • 答:不支持,应该也不会支持。
  • 问:你们支持 SNI 吗?
    • 答:不支持。出于 HTTP/2 性能考虑,我们使用是一个合并的通配符证书(详见后文)。
  • 问:你们的证书是哪来的?
    • 答:我们用的是 DigiCert,他们很棒。
  • 问:你们支持 IE 6 吗?
    • 答:这次之后终于不再支持了。IE 6 默认不支持 TLS(尽管你可以启用 1.0 的支持),而我们则不支持 SSL。当我们 301 跳转就绪的时候大部分 IE 6 用户就不能访问 Stack Overflow 了。一旦我们弃用 TLS 1.0,所有 IE 6 用户都不行了。
  • 问:你们负载均衡器用的什么?
  • 问:使用 HTTPS 的动机是什么?

证书

让我们先聊聊证书,因为这是最容易被误解的部分。不少朋友跟我说,他安装了 HTTPS 证书,因此他们已经完成 HTTPS 准备了。呵呵,麻烦你看一眼右侧那个小小的滚动条,这篇文章才刚刚开始,你觉得真的这么简单么?我有这个必要告诉你们一点人生的经验 :没这么容易的。

一个最常见的问题是:「为何不直接用 Let’s Encrypt?」

答案是:这个方案不适合我们。 Let’s Encrypt 的确是一个伟大的产品,我希望他们能够长期服务于大家。当你只有一个或少数几个域名时,它是非常出色的选择。但是很可惜,我们 Stack Exchange 有数百个站点,而 Let’s Encrypt 并不支持通配域名配置。这导致 Let’s Encrypt 无法满足我们的需求。要这么做,我们就不得不在每上一个新的 Q&A 站点的时候都部署一个(或两个)证书。这样会增加我们部署的复杂性,并且我们要么放弃不支持 SNI 的客户端(大约占 2% 的流量)要么提供超多的 IP——而我们目前没这么多的 IP。

我们之所以想控制证书,还有另外一个原因是我们想在本地负载均衡器以及 CDN / 代理提供商那边使用完成相同的证书。如果不做到这个,我们无法顺畅地做从代理那里做失效备援(failover)。支持 HTTP 公钥固定(HPKP)的客户端会报认证失败。虽然我们仍在评估是否使用 HPKP,但是如果有一天要用的话我们得提前做好准备。

很多朋友在看见我们的主证书时候会吓得目瞪口呆,因为它包含了我们的主域名和通配符子域名。它看上去长成这样:

为什么这么做?老实说,是我们让 DigiCert 替我们做的。这么做会导致每次发生变化的时候都需要手动合并证书,了 我们为什么要忍受这么麻烦的事呢?首先,我们期望能够尽可能让更多用户使用我们产品。这里面包括了那些还不支持 SNI 的用户(比如在我们项目启动的时候 Android 2.3 势头正猛)。另外,也包括 HTTP/2 与一些现实问题——我们过会儿会谈到这一块。

Meta 子域(meta.*.stackexcange.com

Stack Exchage 的一个设计理念是,针对每个 Q&A 站点,我们都有一个地方供讨论。我们称之为 “second place”。比如 meta.gaming.stackexchange.com 用来讨论 gaming.stackexchange.com。这个有什么特别之处呢?好吧,并没有,除了域名:这是一个 4 级域名。

我之前已经说过这个问题,但后来怎么样了呢?具体来说,现在面临的问题是 *.stackexchange.com 包含 gaming.stackexchange.com(及几百个其它站点),但它并不包含 meta.gaming.stackexchange.comRFC 6125 (第 6.4.3 节) 写道:

客户端 不应该 尝试匹配一个通配符在中间的域名(比如,不要匹配 bar.*.example.net

这意味着我们无法使用 meta.*.stackexchange.com,那怎么办呢?

  • 方案一:部署 SAN 证书(多域名证书)
    • 我们需要准备 3 个证书和 3 个 IP(每张证书支持域名上限是 100),并且会把新站上线复杂化(虽然这个机制已经改了)
    • 我们要在 CDN/代理层上部署三个自定义证书
    • 我们要给 meta.* 这种形式的域名配置额外的 DNS 词条
      • 根据 DNS 规则,我们必须给每个这样的站点配置一条 DNS,无法批量配置,从而提高了新站上线和维护代理的成本
  • 方案二:将所有域名迁移到 *.meta.stackexchange.com
    • 我们会有一次痛苦的迁移过程,但这是一次性的,并且未来维护证书成本较低
    • 我们需要部署一个全局登录系统(详情见此
    • 这个方案仍然不解决 HSTS 预加载下面的 includeSubDomains 问题(详情见此
  • 方案三:啥都不做,放弃
    • 这个方案最简单,然而这是假方案

我们部署了 全局登录系统,然后将子 meta 域名用 301 重定向到新地址,比如 gaming.meta.stackexchange.com。做完这个之后我们才意识到,因为这些域名曾经存在过,所以对于 HSTS 预加载来说是个很大的问题。这件事情还在进行中,我会在文章最后面讨论这个问题。这类问题对于 meta.pt.stackoverflow.com 等站点也存在,不过还好我们只有四个非英语版本的 Stack Overflow,所以问题没有被扩大。

对了,这个方案本身还存在另一个问题。由于将 cookies 移动到顶级目录,然后依赖于子域名对其的继承,我们必须调整一些其他域名。比如,在我们新系统中,我们使用 SendGrid 来发送邮件(进行中)。我们从 stackoverflow.email 这个域名发邮件,邮件内容里的链接域名是 sg-links.stackoverflow.email(使用 CNAME 管理),这样你的浏览器就不会将敏感的 cookie 发出去。如果这个域名是 links.stackoverflow.com,那么你的浏览器会将你在这个域名下的 cookie 发送出去。 我们有不少虽然使用我们的域名,但并不属于我们自己的服务。这些子域名都需要从我们受信的域名下移走,否则我们就会把你们的 cookie 发给非我们自有的服务器上。如果因为这种错误而导致 cookie 数据泄露,这将是件很丢人的事情。

我们有试过通过代理的方式来访问我们的 Hubspot CRM 网站,在传输过程中可以将 cookies 移除掉。但是很不幸 Hubspot 使用 Akamai,它会判定我们的 HAProxy 实例是机器人,并将其封掉。头三次的时候还挺有意思的……当然这也说明这个方式真的不管用。我们后来再也没试过了。

你是否好奇为什么 Stack Overflow 的博客地址是  https://stackoverflow.blog/?没错,这也是出于安全目的。我们把博客搭在一个外部服务上,这样市场部门和其他团队能够更便利地使用。正因为这样,我们不能把它放在有 cookie 的域名下面。

上面的方案会牵涉到子域名,引出 HSTS 预加载 和 includeSubDomains 命令问题,我们一会来谈这块内容。

性能:HTTP/2

很久之前,大家都认为 HTTPS 更慢。在那时候也确实是这样。但是时代在变化,我们说 HTTPS 的时候不再是单纯的 HTTPS,而是基于 HTTPS 的 HTTP/2。虽然 HTTP/2 不要求加密,但事实上却是加密的。主流浏览器都要求 HTTP/2 提供加密连接来启用其大部分特性。你可以来说 spec 或者规定上不是这么说的,但浏览器才是你要面对的现实。我诚挚地期望这个协议直接改名叫做 HTTPS/2,这样也能给大家省点时间。各浏览器厂商,你们听见了吗?

HTTP/2 有很多功能上的增强,特别是在用户请求之前可以主动推送资源这点。这里我就不展开了,Ilya Grigorik 已经写了一篇非常不错的文章。我这里简单罗列一下主要优点:

咦?怎么没提到证书呢?

一个很少人知道的特性是,你可以推送内容到不同的域名,只要满足以下的条件:

  1. 这两个域名需要解析到同一个 IP 上
  2. 这两个域名需要使用同一张 TLS 证书(看到没!)

让我们看一下我们当前 DNS 配置:

λ dig stackoverflow.com +noall +answer
; <<>> DiG 9.10.2-P3 <<>> stackoverflow.com +noall +answer
;; global options: +cmd
stackoverflow.com.      201     IN      A       151.101.1.69
stackoverflow.com.      201     IN      A       151.101.65.69
stackoverflow.com.      201     IN      A       151.101.129.69
stackoverflow.com.      201     IN      A       151.101.193.69

λ dig cdn.sstatic.net +noall +answer
; <<>> DiG 9.10.2-P3 <<>> cdn.sstatic.net +noall +answer
;; global options: +cmd
cdn.sstatic.net.        724     IN      A       151.101.193.69
cdn.sstatic.net.        724     IN      A       151.101.1.69
cdn.sstatic.net.        724     IN      A       151.101.65.69
cdn.sstatic.net.        724     IN      A       151.101.129.69

嘿,这些 IP 都是一致的,并且他们也拥有相同的证书!这意味着你可以直接使用 HTTP/2 的服务器推送功能,而无需影响 HTTP/1.1 用户。 HTTP/2 有推送的同时,HTTP/1.1 也有了域名共享(通过 sstatic.net)。我们暂未部署服务器推送功能,但一切都尽在掌握之中。

HTTPS 是我们实现性能目标的一个手段。可以这么说,我们的主要目标是性能,而非站点安全性。我们想要安全性,但光是安全性不足以让我们花那么多精力来在全网部署 HTTPS。当我们把所有因素都考虑在一起的时候,我们可以评估出要完成这件事情需要付出的巨大的时间和精力。在 2013 年,HTTP/2 还没有扮演那么重要的角色。而现在形势变了,对其的支持也多了,最终这成为了我们花时间调研 HTTPS 的催化剂。

值得注意的是 HTTP/2 标准在我们项目进展时还在持续发生变化。它从 SPDY 演化为 HTTP/2,从 NPN 演化为 ALPN。我们这里不会过多涉及到这部分细节,因为我们并没有为其做太多贡献。我们观望并从中获准,但整个互联网却在推进其向前发展。如果你感兴趣,可以看看 Cloudflare 是怎么讲述其演变的

HAProxy:支持 HTTPS

我们最早在 2013 年开始在 HAProxy 中使用 HTTPS。为什么是 HAProxy 呢?这是历史原因,我们已经在使用它了,而它在 2013 年 的 1.5 开发版中支持了 HTTPS,并在 2014 年发布了正式版。曾经有段时间,我们把 Nginx 放置在 HAProxy 之前(详情看这里)。但是简单些总是更好,我们总是想着要避免在链路、部署和其他问题上的复杂问题。

我不会探讨太多细节,因为也没什么好说的。HAProxy 在 1.5 之后使用 OpenSSL 支持 HTTPS,配置文件也是清晰易懂的。我们的配置方式如下:

  • 跑在 4 个进程上
    • 1 个用来做 HTTP/前端处理
    • 2-4 个用来处理 HTTPS 通讯
  • HTTPS 前端使用 socket 抽象命名空间来连接至 HTTP 后端,这样可以极大减少资源消耗
  • 每一个前端或者每一「层」都监听了 :433 端口(我们有主、二级、websockets 及开发环境)
  • 当请求进来的时候,我们在请求头上加入一些数据(也会移除掉一些你们发送过来的),再将其转发给 web 层
  • 我们使用  Mozilla 提供的加密套件。注意,这和我们 CDN 用的不是同样的套件。

HAProxy 比较简单,这是我们使用一个 SSL 证书来支持 :433 端口的第一步。事后看来,这也只是一小步。

这里是上面描述情况下的架构图,我们马上来说前面的那块云是怎么回事:

CDN/代理层:通过 Cloudflare 和 Fastly 优化延迟

我对 Stack Overflow 架构的效率一直很自豪。我们很厉害吧?仅用一个数据中心和几个服务器就撑起了一个大型网站。不过这次不一样了。尽管效率这件事情很好,但是在延迟上就成了个问题。我们不需要那么多服务器,我们也不需要多地扩展(不过我们有一个灾备节点)。这一次,这就成为了问题。由于光速,我们(暂时)无法解决延迟这个基础性问题。我们听说有人已经在处理这个问题了,不过他们造的时间机器好像有点问题。

让我们用数字来理解延迟。赤道长度是 40000 公里(光绕地球一圈的最坏情况)。光速在真空中是 299,792,458 米/秒。很多人用这个数字,但光纤并不是真空的。实际上光纤有 30-31% 损耗,所以我们的这个数字是:(40,075,000 m) / (299,792,458 m/s * .70) = 0.191s,也就是说最坏情况下绕地球一圈是 191ms,对吧?不对。这假设的是一条理想路径,而实际上两个网络节点的之间几乎不可能是直线。中间还有路由器、交换机、缓存、处理器队列等各种各样的延迟。累加起来的延迟相当可观。

这些和 Stack Overflow 有什么关系呢?云主机的优势出来了。如果你用一家云供应商,你访问到的就是相对较近的服务器。但对我们来说不是这样,你离服务部署在纽约或丹佛(主备模式)越远,延迟就越高。而使用 HTTPS,在协商连接的时候需要一个额外的往返。这还是最好的情况(使用 0-RTT 优化 TLS 1.3)。Ilya Grigorik 的 这个总结 讲的很好。

来说 Cloudflare 和 Fastly。HTTPS 并不是闭门造车的一个项目,你看下去就会知道,我们还有好几个项目在并行。在搭建一个靠近用户的 HTTPS 终端(以降低往返时间)时,我们主要考虑的是:

  • 终端 HTTPS 支持
  • DDoS 防护
  • CDN 功能
  • 与直连等同或更优的性能

优化代理层的准备:客户端性能测试

开始正式启用终端链路加速之前,我们需要有性能测试报告。我们在浏览器搭好了一整套覆盖全链路性能数据的测试。 浏览器里可以通过 JavaScript 从 window.performance 取性能耗时。打开你浏览器的审查器,你可以亲手试一下。我们希望这个过程透明,所以从第一天开始就把详细信息放在了 teststackoverflow.com 上。这上面并没有敏感信息,只有一些由页面直接载入的 URI 和资源,以及它们的耗时。每一张记录下来的页面大概长这样:

我们目前对 5% 的流量做性能监控。这个过程没有那么复杂,但是我们需要做的事情包括: 1. 把耗时转成 JSON 2. 页面加载后上传性能测试数据 3. 将性能测试上传给我们后台服务器 4. 在 SQL Server 中使用 clustered columnstore 存储数据 5. 使用 Bosun (具体是 BosunReporter.NET) 汇集数据

最终的结果是我们有了一份来自于全球真实用户的很好的实时汇总。这些数据可供我们分析、监控、报警,以及用于评估变化。它大概长这样:

幸好,我们有持续的流量来获取数据以供我们决策使用,目前的量级是 50 亿,并且还在增长中。这些数据概览如下:

OK,我们已经把基础工作准备好了,是时候来测试 CDN/代理层供应商了。

Cloudflare

我们评估了很多 CDN/DDoS 防护层供应商。最终选择了 Cloudflare,主要是考虑到他们的基础设施、快速响应、还有他们承诺的 Railgun。那么我们如何测试使用了 Cloudfalre 之后用户的真实效果?是否需要部署服务来获取用户数据?答案是不需要!

Stack Overflow 的数据量非常大:月 PV 过十亿。记得我们上面讲的客户端耗时纪录吗?我们每天都有几百万的访问了,所以不是直接可以问他们吗?我们是可以这么做,只需要在页面中嵌入 <iframe> 就行了。Cloudflare 已经是我们 cdn.sstatic.net(我们共用的无 cookie 的静态内容域)的托管商了。但是这是通过一条CNAME DNS 纪录来做的,我们把 DNS 指向他们的 DNS。所以要用 Cloudflare 来当代理服务的话,我们需要他们指向我们的 DNS。所以我们先需要测试他们 DNS 的性能。

实际上,要测试性能我们需要把二级域名给他们,而不是 something.stackoverflow.com,因为这样可能会有不一致的胶水记录而导致多次查询。明确一下,一级域名 (TLDs)指的是 .com.net.org.dance.duck.fail.gripe.here.horse.ing.kim.lol.ninja.pink.red.vodka. 和 .wtf。 注意,这些域名尾缀都是,我可没开玩笑。 二级域名 (SLDs) 就多了一级,比如 stackoverflow.comsuperuser.com 等等。我们需要测的就是这些域名的行为及表现。因此,我们就有了 teststackoverflow.com,通过这个新域名,我们在全球范围内测试 DNS 性能。对一部分比例的用户,通过嵌一个 <iframe>(在测试中开关),我们可以轻松地获取用户访问 DNS 的相关数据。

注意,测试过程最少需要 24 小时。在各个时区,互联网的表现会随着用户作息或者 Netflix 的使用情况等发生变化。所以要测试一个国家,需要完整的一天数据。最好是在工作日(而不要半天落在周六)。我们知道会有各种意外情况。互联网的性能并不是稳定的,我们要通过数据来证明这一点。

我们最初的假设是,多增加了的一个节点会带来额外的延时,我们会因此损失一部分页面加载性能。但是 DNS 性能上的增加其实弥补了这一块。比起我们只有一个数据中心来说,Cloudflare 的 DNS 服务器部署在离用户更近的地方,这一块性能要好得多得多。我希望我们能有空来放出这一块的数据,只不过这一块需要很多处理(以及托管),而我现在也没有足够多的时间。

接下来,我们开始将 teststackoverflow.com 放在 Cloudflare 的代理上做链路加速,同样也是放在 <iframe> 中。我们发现美国和加拿大的服务由于多余的节点而变慢,但是世界其他地方都是持平或者更好。这满足我们的期望。我们开始使用 Cloudflare 的网络对接我们的服务。期间发生了一些 DDos 的攻击,不过这是另外的事了。那么,为什么我们接受在美国和加拿大地区慢一点呢?因为每个页面加载需要的时间仅为 200-300ms,哪怕慢一点也还是飞快。当时我们认为 Railgun 可以将这些损耗弥补回来。

这些测试完成之后,我们为了预防 DDos 工作,做了一些其他工作。我们接入了额外的 ISP 服务商以供我们的 CDN/代理层对接。毕竟如果能绕过攻击的话,我们没必要在代理层做防护。现在每个机房都有 4 个 ISP 服务商(译者注:相当于电信、联通、移动、教育网),两组路由器,他们之间使用 BGP协议。我们还额外添置了两组负载均衡器专门用于处理 CDN/代理层的流量。

Cloudflare: Railgun

与此配套,我们启用了两组 Railgun。Railgun 的原理是在 Cloudflare 那边,使用 memcached 匹配 URL 进行缓存数据。当 Railgun 启用的时候,每个页面(有一个大小阈值)都会被缓存下来。那么在下一次请求时候,如果在这个 URL 在 Cloudflare 节点上和我们这里都缓存的话,我们仍然会问 web 服务器最新的数据。但是我们不需要传输完整的数据,只需要把传输和上次请求的差异数据传给 Cloudflure。他们把这个差异运用于他们的缓存上,然后再发回给客户端。这时候, gzip 压缩 的操作也从 Stack Overflow 的 9 台 Web Server 转移到了一个 Railgun 服务上,这台服务器得是 CPU 密集型的——我指出这点是因为,这项服务需要评估、购买,并且部署在我们这边。

举个例子,想象一下,两个用户打开同一个问题的页面。从浏览效果来看,他们的页面技术上长得几乎一样,仅仅有细微的差别。如果我们大部分的传输内容只是一个 diff 的话,这将是一个巨大的性能提升。

总而言之,Railgun 通过减少大量数据传输的方式提高性能。当它顺利工作的时候确实是这样。除此之外,还有一个额外的优点:请求不会重置连接。由于 TCP 慢启动,当连接环境较为复杂时候,可能导致连接被限流。而 Railgun 始终以固定的连接数连接到 Cloudflare 的终端,对用户请求采用了多路复用,从而其不会受慢启动影响。小的 diff 也减少了慢启动的开销。

很可惜,我们由于种种原因我们在使用 Railgun 过程中一直遇到问题。据我所知,我们拥有当时最大的 Railgun 部署规模,这把 Railgun 逼到了极限。尽管我们花了一年追踪各种问题,最终还是不得不放弃了。这种状况不仅没有给我们省钱,还耗费了更多的精力。现在几年过去了。如果你正在评估使用 Railgun,你最好看最新的版本,他们一直在做优化。我也建议你自己做决定是否使用 Railgun。

Fastly

我们最近才迁到 Fastly,因为我们在讲 CDN/代理层,我也会顺带一提。由于很多技术工作在 Cloudflare 那边已经完成,所以迁移本身并没有什么值得说的。大家会更感兴趣的是:为什么迁移?毕竟 Cloudflare 在各方面是不错的:丰富的数据中心、稳定的带宽价格、包含 DNS 服务。答案是:它不再是我们最佳的选择了。Flastly 提供了一些我们更为看中的特性:灵活的终端节点控制能力、配置快速分发、自动配置分发。并不是说 Cloudflare 不行,只是它不再适合 Stack Overflow 了。

事实胜于雄辩:如果我不认可 Cloudflare,我的私人博客不可能选择它,嘿,就是这个博客,你现在正在阅读的。

Fastly 吸引我们的主要功能是提供了 Varnish 和 VCL。这提供了高度的终端可定制性。有些功能吧,Cloudfalre 无法快速提供(因为他们是通用化的,会影响所有用户),在 Fastly 我们可以自己做。这是这两家架构上的差异,这种「代码级别高可配置」对于我们很适用。同时,我们也很喜欢他们在沟通、基础设施的开放性。

我来展示一个 VCL 好用在哪里的例子。最近我们遇到 .NET 4.6.2 的一个超恶心 bug,它会导致 max-age 有超过 2000 年的缓存时间。快速解决方法是在终端节点上有需要的时候去覆盖掉这个头部,当我写这篇文章的时候,这个 VCL 配置是这样的:

sub vcl_fetch {
  if (beresp.http.Cache-Control) {
      if (req.url.path ~ "^/users/flair/") {
          set beresp.http.Cache-Control = "public, max-age=180";
      } else {
          set beresp.http.Cache-Control = "private";
      }
  }

这将给用户能力展示页 3 分钟的缓存时间(数据量还好),其余页面都不设置。这是一个为解决紧急时间的非常便于部署的全局性解决方案。 我们很开心现在有能力在终端做一些事情。我们的 Jason Harvey 负责 VCL 配置,并写了一些自动化推送的功能。我们基于一个 Go 的开源库 fastlyctl 做了开发。

另一个 Fastly 的特点是可以使用我们自己的证书,Cloudflare 虽然也有这个服务,但是费用太高。如我上文提到的,我们现在已经具备使用 HTTP/2 推送的能力。但是,Fastly 就不支持 DNS,这个在 Cloudflare 那里是支持的。现在我们需要自己解决 DNS 的问题了。可能最有意思的就是这些来回的折腾吧?

全局 DNS

当我们从 Cloudflare 迁移到 Fastly 时候,我们必须评估并部署一个新的 DNS 供应商。这里有篇 Mark Henderson 写的 文章 。鉴于此,我们必须管理:

  • 我们自己的 DNS 服务器(备用)
  • Name.com 的服务器(为了那些不需要 HTTPS 的跳转服务)
  • Cloudflare DNS
  • Route 53 DNS
  • Google DNS
  • Azure DNS
  • 其他一些(测试时候使用)

这个本身就是另一个项目了。为了高效管理,我们开发了 DNSControl。这现在已经是开源项目了托管在 GiHub 上,使用 Go 语言编写。 简而言之,每当我们推送 JavaScript 的配置到 git,它都会马上在全球范围里面部署好 DNS 配置。这里有一个简单的例子,我们拿 askubuntu.com 做示范:

D('askubuntu.com', REG_NAMECOM,
    DnsProvider(R53,2),
    DnsProvider(GOOGLECLOUD,2),
    SPF,
    TXT('@', 'google-site-verification=PgJFv7ljJQmUa7wupnJgoim3Lx22fbQzyhES7-Q9cv8'), // webmasters
    A('@', ADDRESS24, FASTLY_ON),
    CNAME('www', '@'),
    CNAME('chat', 'chat.stackexchange.com.'),
    A('meta', ADDRESS24, FASTLY_ON),
END)

太棒了,接下来我们就可以使用客户端响应测试工具来测试啦!上面提到的工具可以实时告诉我们真实部署情况,而不是模拟数据。但是我们还需要测试所有部分都正常。

测试

客户端响应测试的追踪可以方便我们做性能测试,但这个并不适合用来做配置测试。客户端响应测试非常适合展现结果,但是配置有时候并没有界面,所以我们开发了 httpUnit (后来知道这个项目重名了 )。这也是一个使用 Go 语言的开源项目。以 teststackoverflow.com 举例,使用的配置如下:

[[plan]]
    label = "teststackoverflow_com"
    url = "http://teststackoverflow.com"
    ips = ["28i"]
    text = "<title>Test Stack Overflow Domain</title>"
    tags = ["so"]
[[plan]]
    label = "tls_teststackoverflow_com"
    url = "https://teststackoverflow.com"
    ips = ["28"]
    text = "<title>Test Stack Overflow Domain</title>"
    tags = ["so"]

每次我们更新一下防火墙、证书、绑定、跳转时都有必要测一下。我们必须保证我们的修改不会影响用户访问(先在预发布环境进行部署)。 httpUnit 就是我们来做集成测试的工具。

我们还有一个开发的内部工具(由亲爱的 Tom Limoncelli 开发),用来管理我们负载均衡上面的 VIP 地址 。我们先在一个备用负载均衡上面测试完成,然后将所有流量切过去,让之前的主负载均衡保持一个稳定状态。如果期间发生任何问题,我们可以轻易回滚。如果一切顺利,我们就把这个变更应用到那台负载均衡上。这个工具叫做 keepctl(keepalived control 的简称),时间允许的话很快就会整理开源出来。

应用层准备

上面提到的只是架构方面的工作。这通常是由 Stack Overflow 的几名网站可靠性工程师组成的团队完成的。而应用层也有很多需要完成的工作。这个列表会很长,先让我拿点咖啡和零食再慢慢说。

很重要的一点是,Stack Overflow 与 Stack Exchange 的架构 Q&A 采用了多租户技术。这意味着如果你访问 stackoverflow.com 或者 superuser.com 又或者 bicycles.stackexchange.com,你返回到的其实是同一台服务器上的同一个 w3wp.exe 进程。我们通过浏览器发送的 Host 请求头来改变请求的上下文。为了更好地理解我们下文中提到的一些概念,你需要知道我们代码中的 Current.Site 其实指的是 请求 中的站点。Current.Site.Url() 和 Current.Site.Paths.FaviconUrl 也是基于同样的概念。

换一句话说:我们的 Q&A 全站都是跑在同一个服务器上的同一个进程,而用户对此没有感知。我们在九台服务器上每一台跑一个进程,只是为了发布版本和冗余的问题。

全局登录

整个项目中有一些看起来可以独立出来(事实上也是),不过也同属于整个大 HTTPS 迁移中的一部分。登录就是其中一个项目。我首先来说说这个,因为这比别它变化都要早上线。

在 Stack Overflow(及 Stack Exchange)的头五六年里,你登录的是一个个的独立网站。比如,stackoverflow.comstackexchange.com 以及 gaming.stackexchange.com 都有它们自己的 cookies。值得注意的是:meta.gaming.stackexchange.com 的登录 cookie 是从 gaming.stackexchange.com 带过来的。这些是我们上面讨论证书时提到的 meta 站点。他们的登录信息是相关联的,你只能通过父站点登录。在技术上说并没有什么特别的,但考虑到用户体验就很糟糕了。你必须一个一个站登录。我们用「全局认证」的方法来「修复」了这个问题,方法是在页面上放一个 <iframe>,内面访问一下 stackauth.com。如果用户在别处登录过的话,它也会在这个站点上登录,至少会去试试。这个体验还行,但是会有弹出框问你是否点击重载以登录,这样就又不是太好。我们可以做得更好的。对了,你也可以去问问 Kevin Montrose 关于移动 Safari 的匿名模式,你会震惊的。

于是我们有了「通用登录」。为什么用「通用」这个名字?因为我们已经用过「全局」了。我们就是如此单纯。所幸 cookies 也很单纯的东西。父域名里的 cookie(如 stackexchange.com)在你的浏览器里被带到所有子域名里去(如 gaming.stackexchange.com)。如果我们只二级域名的话,其实我们的域名并不多:

是的,我们有一些域名是跳转到上面的列表中的,比如 askdifferent.com。但是这些只是跳转而已,它们没有 cookies 也无需登录。

这里有很多细节的后端工作我没有提(归功于 Geoff Dalgas 和 Adam Lear),但大体思路就是,当你登录的时候,我们把这些域名都写入一个 cookie。我们是通过第三方的 cookie 和随机数来做的。当你登录其中任意一个网站的时候,我们在页面上都会放 6 个 <img> 标签来往其它域名写入 cookie,本质上就完成了登录工作。这并不能在 所有情况 下都适用(尤其是移动 Safari 简直是要命了),但和之前比起来那是好得多了。

客户端的代码不复杂,基本上长这样:

$.post('/users/login/universal/request', function (data, text, req) {
    $.each(data, function (arrayId, group) {
        var url = '//' + group.Host + '/users/login/universal.gif?authToken=' + 
            encodeURIComponent(group.Token) + '&nonce=' + encodeURIComponent(group.Nonce);
        $(function ( ) { $('#footer').append('<img style="display:none" src="' + url + '"></img>'); });
    });
}, 'json');

但是要做到这点,我们必须上升到账号级别的认证(之前是用户级别)、改变读取 cookie 的方式、改变这些 meta 站的登录工作方式,同时还要将这一新的变动整合到其它应用中。比如说,Careers(现在拆成了 Talent 和 Jobs)用的是另一份代码库。我们需要让这些应用读取相应的 cookies,然后通过 API 调用 Q&A 应用来获取账户。我们部署了一个 NuGet 库来减少重复代码。底线是:你在一个地方登录,就在所有域名都登录。不弹框,不重载页面。

技术的层面上看,我们不用再关心 *.*.stackexchange.com 是什么了,只要它们是 stackexchange.com 下就行。这看起来和 HTTPS 没有关系,但这让我们可以把 meta.gaming.stackexchange.com 变成 gaming.meta.stackexchange.com 而不影响用户。

本地 HTTPS 开发

要想做得更好的话,本地环境应该尽量与开发和生产环境保持一致。幸好我们用的是 IIS,这件事情还简单的。我们使用一个工具来设置开发者环境,这个工具的名字叫「本地开发设置」——单纯吧?它可以安装工具(Visual Studio、git、SSMS 等)、服务(SQL Server、Redis、Elasticsearch)、仓库、数据库、网站以及一些其它东西。做好了基本的工具设置之后,我们要做的只是添加 SSL/TLS 证书。主要的思路如下:

Websites = @(
    @{
        Directory = "StackOverflow";
        Site = "local.mse.com";
        Aliases = "discuss.local.area51.lse.com", "local.sstatic.net";
        Databases = "Sites.Database", "Local.StackExchange.Meta", "Local.Area51", "Local.Area51.Meta";
        Certificate = $true;
    },
    @{
        Directory = "StackExchange.Website";
        Site = "local.lse.com";
        Databases = "Sites.Database", "Local.StackExchange", "Local.StackExchange.Meta", "Local.Area51.Meta";
        Certificate = $true;
    }
)

我把使用到的代码放在了一个 gist 上:Register-Websites.psm1。我们通过 host 头来设置网站(通过别名添加),如果直连的话就给它一个证书(嗯,现在应该把这个行为默认改为 $true 了),然后允许 AppPool 账号来访问数据库,于是我们本地也在使用 https:// 开发了。嗯,我知道我们应该把这个设置过程开源出来,不过我们仍需去掉一些专有的业务。会有这么一天的。

为什么这件事情很重要? 在此之前,我们从 /content 加载静态内容,而不是从另一个域名。这很方便,但也隐藏了类似于跨域请求(CORS)的问题。在同一个域名下用同一个协议能正常加载的资源,换到开发或者生产环境下就有可能出错。「在我这里是好的。」

当我们使用和生产环境中同样协议以及同样架构的 CDN 还有域名设置时,我们就可以在开发机器上找出并修复更多的问题。比如,你是否知道,从 https:// 跳转到 http:// 时,浏览器是不会发送 referer 的?这是一个安全上的问题,referer 头中可能带有以明文传输的敏感信息。

「Nick 你就扯吧,我们能拿到从 Google 拿到 referer 啊!」确实。但是这是因为他们主动选择这一行为。如果你看一下 Google 的搜索页面,你可以看到这样的 <meta> 指令:

<meta content="origin" id="mref" name="referrer">

这也就是为什么你可以取到 referer。

好的,我们已经设置好了,现在该做些什么呢?

混合内容:来自于你们

混合内容是个筐,什么都能往里装。我们这些年下来积累了哪些混合内容呢?不幸的是,有很多。这个列表里我们必须处理的用户提交内容:

上面的每一个都带有自己独有的问题,我仅仅会覆盖一下值得一提的部分。注意:我谈论的每一个解决方案都必须扩展到我们这个架构下的几百个站点和数据库上。

在上面的所有情况中(除了代码片段),要消除混合内容的第一步工作就是:你必须先消除的混合内容。否则,这个清理过程将会无穷无尽。要做到这一点,我们开始全网强制仅允许内嵌 https:// 图片。一旦这个完成之后,我们就可以开始清理了。

对于问题、答案以及其他帖子形式中,我们需要具体问题具体分析。我们先来搞定 90% 以上的情况:stack.imgur.com。在我来之前 Stack Overflow 就已经有自己托管的 Imgur 实例了。你在编辑器中上传的图片就会传到那里去。绝大部分的帖子都是用的这种方法,而他们几年前就为我们添加了 HTTPS 支持。所以这个就是一个很直接的查找替换(我们称为帖子 markdown 重处理)。

然后我们通过通过 Elasticsearch 对所有内容的索引来找出所有剩下的文件。我说的我们其实指的是 Samo。他在这里处理了大量的混合内容工作。当我们看到大部分的域名其实已经支持 HTTPS 了之后,我们决定:

  1. 对于每个 <img> 的源地址都尝试替换成 https://。如果能正常工作则替换帖子中的链接
  2. 如果源地址不支持 https://,将其转一个链接

当然,并没有那么顺利。我们发现用于匹配 URL 的正则表达式其实已经坏了好几年了,并且没有人发现……所以我们修复了正则,重新做了索引。

有人问我们:「为什么不做个代理呢?」呃,从法律和道德上来说,代理对我们的内容来说是个灰色地带。比如,我们 photo.stackexchange.com 上的摄像师会明确声明不用 Imgur 以保留他们的权利。我们充分理解。如果我们开始代理并缓存全图,这在法律上有点问题。我们后来发现在几百万张内嵌图片中,只有几千张即不支持 https:// 也没有 404 失效的。这个比例(低于 1%)不足于让我们去搭一个代理。

我们确实研究过搭一个代理相关的问题。费用有多少?需要多少存储?我们的带宽足够吗?我们有了一个大体上的估算,当然有点答案也不是很确定。比如我们是否要用 Fastly,还是直接走运营商?哪一种比较快?哪一种比较便宜?哪一种可以扩展?这个足够写另一篇博客了,如果你有具体问题的话可以在评论里提出,我会尽力回答。

所幸,在这个过程中,为了解决几个问题,balpha 更改了用 HTML5 嵌入 YouTube 的方式。我们也就顺便强制了一下 YouTube 的 https:// 嵌入。

剩下的几个内容领域的事情差不多:先阻止新的混合内容进来,再替换掉老的。这需要我们在下面几个领域进行更改:

  • 帖子
  • 个人资料
  • 开发故事
  • 帮助中心
  • 职场
  • 公司业务

声明:JavaScript 片段的问题仍然没有解决。这个有点难度的原因是:

  1. 资源有可能不以 https:// 的方式存在(比如一个库)
  2. 由于这个是 JavaScript,你可以自己构建出任意的 URL。这里我们就无力检查了。
    • 如果你有更好的方式来处理这个问题,请告诉我们。我们在可用性与安全性上不可兼得。

混合内容:来自我们

并不是处理完用户提交的内容就解决问题了。我们自己还是有不少 http:// 的地方需要处理。这些更改本身没什么特别的,但是这至少能解答「为什么花了那么长时间?」这个问题:

  • 广告服务(Calculon)
  • 广告服务(Adzerk)
  • 标签赞助商
  • JavaScript 假定
  • Area 51(这代码库也太老了)
  • 分析跟踪器(Quantcast, GA)
  • 每个站点引用的 JavaScript(社区插件)
  • /jobs 下的所有东西(这其实是个代理)
  • 用户能力
  • ……还有代码中所有出现 http:// 的地方

JavaScript 和链接比较令人痛苦,所以我在这里稍微提一下。

JavaScript 是一个不少人遗忘的角落,但这显然不能被无视。我们不少地方将主机域名传递给 JavaScript 时假定它是 http:// ,同时也有不少地方写死了 meta 站里的 meta. 前缀。很多,真的很多,救命。还好现在已经不这样了,我们现在用服务器渲染出一个站点,然后在页面顶部放入相应的选择:

StackExchange.init({
  "locale":"en",
  "stackAuthUrl":"https://stackauth.com",
  "site":{
    "name":"Stack Overflow"
    "childUrl":"https://meta.stackoverflow.com",
    "protocol":"http"
  },
  "user":{
    "gravatar":"<div class=\"gravatar-wrapper-32\"><img src=\"https://i.stack.imgur.com/nGCYr.jpg\"></div>",
    "profileUrl":"https://stackoverflow.com/users/13249/nick-craver"
  }
});

这几年来我们在代码里也用到了很多静态链接。比如,在页尾,在页脚,在帮助区域……到处都是。对每一个来说,解决方式都不复杂:把它们改成 <site>.Url("/path") 的形式就好了。不过要找出这些链接有点意思,因为你不能直接搜 "http://"。感谢 W3C 的丰功伟绩:

<svg xmlns="http://www.w3.org/2000/svg"...

是的,这些是标识符,是不能改的。所以我希望 Visual Studio 在查找文件框中增加一个「排除文件类型」的选项。Visual Studio 你听见了吗?VS Code 前段时间就加了这个功能。我这要求不过分。

这件事情很枯燥,就是在代码中找出一千个链接然后替换而已(包括注释、许可链接等)。但这就是人生,我们必须要做。把这些链接改成 .Url() 的形式之后,一旦站点支持 HTTPS 的时候,我们就可以让链接动态切换过去。比如我们得等到 meta.*.stackexchange.com 搬迁完成之后再进行切换。插播一下我们数据中心的密码是「煎饼馃子」拼音全称,应该没有人会读到这里吧,所以在这里存密码很安全。当站点迁完之后,.Url() 仍会正常工作,然后用 .Url() 来渲染默认为 HTTPS 的站点也会继续工作。这将静态链接变成了动态。

另一件重要的事情:这让我们的开发和本地环境都能正常工作,而不仅仅是链到生产环境上。这件事情虽然枯燥,但还是值得去做的。对了,因为我们的规范网址(canonical)也通过 .Url() 来做了,所以一旦用户开始用上 HTTPS,Google 也可以感知到。

一旦一个站点迁到 HTTPS 之后,我们会让爬虫来更新站点链接。我们把这个叫修正「Google 果汁」,同时这也可以让用户不再碰到 301。

跳转(301)

当你把站点移动到 HTTPS 之后,为了和 Google 配合,你有两件重要的事情要做:

  • 更新规范网址,比如 <link rel="canonical" href="https://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454" />
  • 把 http:// 链接通过 301 跳转至 https://

这个不复杂,也不是浩大的工程,但这非常非常重要。Stack Overflow 大部分的流量都是从 Google 搜索结果中过来的,所以我们得保证这个不产生负面影响。这个是我们的生计,如果我们因此丢了流量那我真是要失业了。还记得那些 .internal 的 API 调用吗?对,我们同样不能把所有东西都进行跳转。所以我们在处理跳转的时候需要一定的逻辑(比如我们也不能跳转 POST 请求,因为浏览器处理得不好),当然这个处理还是比较直接的。这里是实际上用到的代码:

public static void PerformHttpsRedirects()
{
    var https = Settings.HTTPS;
    // If we're on HTTPS, never redirect back
    if (Request.IsSecureConnection) return;

    // Not HTTPS-by-default? Abort.
    if (!https.IsDefault) return;
    // Not supposed to redirect anyone yet? Abort.
    if (https.RedirectFor == SiteSettings.RedirectAudience.NoOne) return;
    // Don't redirect .internal or any other direct connection
    // ...as this would break direct HOSTS to webserver as well
    if (RequestIPIsInternal()) return;

    // Only redirect GET/HEAD during the transition - we'll 301 and HSTS everything in Fastly later
    if (string.Equals(Request.HttpMethod, "GET", StringComparison.InvariantCultureIgnoreCase)
        || string.Equals(Request.HttpMethod, "HEAD", StringComparison.InvariantCultureIgnoreCase))
    {
        // Only redirect if we're redirecting everyone, or a crawler (if we're a crawler)
        if (https.RedirectFor == SiteSettings.RedirectAudience.Everyone
            || (https.RedirectFor == SiteSettings.RedirectAudience.Crawlers && Current.IsSearchEngine))
        {
            var resp = Context.InnerHttpContext.Response;
            // 301 when we're really sure (302 is the default)
            if (https.RedirectVia301)
            {
                resp.RedirectPermanent(Site.Url(Request.Url.PathAndQuery), false);
            }
            else
            {
                resp.Redirect(Site.Url(Request.Url.PathAndQuery), false);
            }
            Context.InnerHttpContext.ApplicationInstance.CompleteRequest();
        }
    }
}

注意我们并不是默认就跳 301(有一个 .RedirectVia301 设置),因为我们做一些会产生永久影响的事情之前必须仔细测试。我们会晚一点来讨论 HSTS 以及后续影响。

Websockets

这一块会过得快一点。Websocket 不难,从某种角度来说,这是我们做过的最简单的事情。我们用 websockets 来处理实时的用户影响力变化、收件箱通知、新问的问题、新增加的答案等等。这也就说基本上每开一个 Stack Overflow 的页面,我们都会有一个对应的 websocket 连接连到我们的负载均衡器上。

所以怎么改呢?其实很简单:安装一个证书,监听 :443 端口,然后用 wss://qa.sockets.stackexchange.com 来代替 ws:// 。后者其实早就做完了(我们用了一个专有的证书,但是这不重要)。从 ws://wss:// 只是配置一下的问题。一开始我们还用 ws:// 作为 wss:// 的备份方案,不过后来就变成仅用 wss:// 了。这么做有两个原因:

  1. 不用的话在 https:// 下面会有混合内容警告
  2. 可以支持更多用户。因为很多老的代理不能很好地处理 websockets。如果使用加密流量,大多数代理就只是透传而不会弄乱流量。对移动用户来说尤其是这样。

最大的问题就是:「我们能处理了这个负载吗?」我们全网处理了不少并发 websocket,在我写这估的时候我们有超过 600000 个并发的连接。这个是我们 HAProxy 的仪表盘在 Opserver 中的界面:

不管是在终端、抽象命名空间套接字还是前端来说都有很多连接。由于启用了 TLS 会话恢复,HAProxy 本身的负载也很重。要让用户下一次重新连接更快,第一次协商之后用户会拿到一个令牌,下一次会把这个令牌发送过来。如果我们的内存足够并且没有超时,我们会恢复上次的会话而不是再开一个。这个操作可以节省 CPU,对用户来说有性能提升,但会用到到更多内存。这个多因 key 大小而异(2048,4096 或是更多?)我们现在用的是 4096 位的 key。在开了 600000 个 websocket 的情况下,我们只用掉了负载均衡器 64GB 内存里的 19GB。这里面 12GB 是 HAProxy 在用,大多数为 TLS 会话缓存。所以结果来说还不错,如果我们不得不买内存的话,这也会是整个 HTTPS 迁移中最便宜的东西。

未知

我猜现在可能是我们来谈论一些未知问题的时候。有些问题是在我们尝试之前无法真正知道的:

  • Google Analytics 里的流量表现怎么样?(我们会失去 referer 吗?)
  • Google Webmasters 的转换是否平滑?(301 生效吗?规范域名呢?要多长时间?)
  • Google 搜索分析会怎么工作(我们会在搜索分析中看到 https:// 吗?)
  • 我们搜索排名会下降吗?(最恐怖的)

有很多人都谈过他们转化成 https:// 的心得,但对我们却有点不一样。我们不是一个站点。我们是多个域名下的多个站点。我们不知道 Google 会怎么对待我们的网络。它会知道 stackoverflow.comsuperuser.com 有关联吗?不知道。我们也不能指望 Google 来告诉我们这些。

所以我们就做测试。在我们全网发布 中,我们测试了几个域名:

对,这些是 Samo 和我会了仔细讨论出来的结果,花了有三分钟那么久吧。Meta 是因为这是我们最重要的反馈网站。Security 站上有很多专家可能会注意到相关的问题,特别是 HTTPS 方面。最后一个,Super User,我们需要知道搜索对我们内容的影响。比起 meta 和 security 来说法,Super User 的流量要大得多。最重要的是,它有来自 Google 的原生流量。

我们一直在观察并评估搜索的影响,所以 Super User 上了之后其他网站过了很久才跟上。到目前为止我们能说的是:基本上没影响。搜索、结果、点击还有排名的周变化都在正常范围内。我们公司依赖于这个流量,这对我们真的很重要。所幸,没有什么值得我们担心的点,我们可以继续发布。

错误

如果不提到我们搞砸的部分,这篇文章就还不够好。错误永远是个选择。让我们来总结一下这一路让我们后悔的事情:

错误:相对协议 URL

如果你的一个资源有一个 URL 的话,一般来说你会看到一些 http://example.com 或者 https://example.com 之类的东西,包括我们图片的路径等等。另一个选项就是你可以使用 //example.com。这被称为相对协议 URL。我们很早之前就在图片、JavaScript、CSS 等中这么用了(我们自有的资源,不是指用户提交)。几年后,我们发现这不是一个好主意,至少对我们来说不是。相对协议链接中的「相对」是对于页面而言。当你在 http://stackoverflow.com 时,//example.com 指的是 http://example.com;如果你在 https://stackoverflow.com 时,就和 https://example.com 等同。那么这个有什么问题呢?

问题在于,图片 URL 不仅是用在页面中,它们还用在邮件、API 还有移动应用中。当我们理了一下路径结构然后在到处都使用图片路径时我们发现不对了。虽然这个变化极大降低了代码冗余,并且简化了很多东西,结果却是我们在邮件中使用了相对 URL。绝大多数邮件客户端都不能处理相对协议 URL 的图片。因为它们不知道是什么协议。Email 不是 http:// 也不是 https://。只有你在浏览器里查看邮件,有可能是预期的效果。

那该怎么办?我们把所有的地方都换成了 https://。我把我们所有的路径代码统一到两个变量上:CDN 根路径,和对应特定站点的文件夹。例如 Stack Overflow 的样式表在 https://cdn.sstatic.net/Sites/stackoverflow/all.css 上(当然我们有缓存中断器),换成本地就是 https://local.sstatic.net/Sites/stackoverflow/all.css。你能看出其中的共同点。通过拼接路径,逻辑简单了不少。则 通过强制 https://,用户还可以在整站切换之前就享受 HTTP/2 的好处,因为所有静态资源都已经就位。都用 https:// 也表示我们可以在页面、邮件、移动还有 API 上使用同一个属性。这种统一也意味着我们有一个固定的地方来处理所有路径——我们到处都有缓存中断器。

注意:如果你像我们一样中断缓存,比如 https://cdn.sstatic.net/Sites/stackoverflow/all.css?v=070eac3e8cf4,请不要用构建号。我们的缓存中断使用的是文件的校验值,也就是说只有当文件真正变化的时候你才会下载一个新的文件。用构建号的话可能会稍微简单点,但同时也会对你的费用还有性能有所损伤。

能做这个当然很好,可我们为什么不从一开始就做呢?因为 HTTPS 在那个时候性能还不行。用户通过 https:// 访问会比 http://慢很多。举一个大一点的例子:我们上个月在 sstatic.net 上收到了四百万个请求,总共有 94TB。如果 HTTPS 性能不好的话,这里累积下来的延迟就很可观了。不过因为我们上了 HTTP/2,以及设置好 CDN/代理层,性能的问题已经好很多了。对于用户来说更快了,对我们来说则更简单,何乐不为呢!

错误:API 及 .internal

当我们把代理架起来开始测试的时候发现了什么?我们忘了一件很重要的事,准确地说,我忘了一件很重要的事。我们在内部 API 里大量地使用了 HTTP。当然这个是正常工作的,只是它们变得更慢、更复杂、也更容易出问题了。

比方说一个内部 API 需要访问 stackoverflow.com/some-internal-route,之前,节点是这些:

  • 原始 app
  • 网关/防火墙(暴露给公网)
  • 本地负载均衡器
  • 目标 web 服务器

这是因为我们是可以解析 stackoverflow.com 的,解析出来的 IP 就是我们的负载均衡器。当有代理的情况下,为了让用户能访问到最近的节点,他们访问到的是不同的 IP 和目标点。他们的 DNS 解析出来的 IP 是 CDN/代理层 (Fastly)。糟了,这意识着我们现在的路径是这样的:

  • 原始 app
  • 网关/防火墙(暴露给公网)
  • 我们的外部路由器
  • 运营商(多节点)
  • 代理(Cloudflare/Fastly)
  • 运营商(到我们的代理路)
  • 我们的外部路由器
  • 本地负载均衡器
  • 目标 web 服务器

嗯,这个看起来更糟了。为了实现一个从 A 调用一下 B,我们多了很多不必要的依赖,同时性能也下降了。我不是说我们的代理很慢,只是原本只需要 1ms 就可以连到我们数据中心……好吧,我们的代理很慢。

我们内部讨论了多次如何用最简单的方法解决这个问题。我们可以把请求改成 internal.stackoverflow.com,但是这会产生可观的修改(也许也会产生冲突)。我们也创建一个 DNS 来专门解析内部地址(但这样会产生通配符继承的问题)。我们也可以在内部把 stackoverflow.com 解析成不同的地址(这被称为水平分割 DNS),但是这一来不好调试,二来在多数据中心的场景下不知道该到哪一个。

最终,我们在所有暴露给外部 DNS 的域名后面都加了一个 .internal 后续。比如,在我们的网络中,stackoverflow.com.internal 会解析到我们的负载均衡器后面(DMZ)的一个内部子网内。我们这么做有几个原因:

  • 我们可以在内部的 DNS 服务器里覆盖且包含一个顶级域名服务器(活动目录)
  • 当请求从 HAProxy 传到 web 应用中时,我们可以把 .internalHost 头中移除(应用层无感知)
  • 如果我们需要内部到 DMZ 的 SSL,我们可以用一个类似的通配符组合
  • 客户端 API 的代码很简单(如果在域名列表中就加一个 .internal

我们客户端的 API 代码是大部分是由 Marc Gravell 写的一个 StackExchange.Network 的 NuGet 库。对于每一个要访问的 URL,我们都用静态的方法调用(所以也就只有通用的获取方法那几个地方)。如果存在的话就会返回一个「内部化」URL,否则保持不变。这意味着一次简单的 NuGet 更新就可以把这个逻辑变化部署到所有应用上。这个调用挺简单的:

# uri = SubstituteInternalUrl(uri);

这里是 stackoverflow.com DNS 行为的一个例子:

  • Fastly:151.101.193.69, 151.101.129.69, 151.101.65.69, 151.101.1.69
  • 直连(外部路由):198.252.206.16
  • 内部:10.7.3.16

记得我们之前提到的 dnscontrol 吗?我们可以用这个快速同步。归功于 JavaScript 的配置/定义,我们可以简单地共享、简化代码。我们匹配所有所有子网和所有数据中心中的所有 IP 的最后一个字节,所以用几个变量,所有 AD 和外部的 DNS 条目都对齐了。这也意味着我们的 HAProxy 配置更简单了,基本上就是这样:

stacklb::external::frontend_normal { 't1_http-in':
  section_name    => 'http-in',
  maxconn         => $t1_http_in_maxconn,
  inputs          => {
    "${external_ip_base}.16:80"  => [ 'name stackexchange' ],
    "${external_ip_base}.17:80"  => [ 'name careers' ],
    "${external_ip_base}.18:80"  => [ 'name openid' ],
    "${external_ip_base}.24:80"  => [ 'name misc' ],

综上,API 路径更快了,也更可靠了:

  • 原始 app
  • 本地负载均衡器(DMZ)
  • 目标 web 服务器

我们解决了几个问题,还剩下几百个等着我们。

错误:301 缓存

在从 http:// 301 跳到 https:// 时有一点我们没有意识的是,Fastly 缓存了我们的返回值。在 Fastly 中,默认的缓存键并不考虑协议。我个人不同意这个行为,因为在源站默认启用 301 跳转会导致无限循环。这个问题是这样造成的:

  1. 用户访问 http:// 上的一个网络
  2. 通过 301 跳转到了 https://
  3. Fastly 缓存了这个跳转
  4. 任意一个用户(包括 #1 中的那个)以 https:// 访问同一个页面
  5. Fastly 返回一个跳至 https:// 的 301,尽量你已经在这个页面上了

这就是为什么我们会有无限循环。要解决这个问题,我们得关掉 301,清掉 Fastly 缓存,然后开始调查。Fastly 建议我们在 vary 中加入 Fastly-SSL,像这样:

sub vcl_fetch {
  set beresp.http.Vary = if(beresp.http.Vary, beresp.http.Vary ",", "") "Fastly-SSL";

在我看来,这应该是默认行为。

错误:帮助中心的小插曲

记得我们必须修复的帮助文档吗?帮助文档都是按语言区分,只有极少数是按站点来分,所以本来它们是可以共享的。为了不产生大量重复代码及存储结构,我们做了一点小小的处理。我们把实际上的帖子对象(和问题、答案一样)存在了 meta.stackexchange.com 或者是这篇帖子关联的站点中。我们把生成的 HelpPost 存在中心的 Sites 数据库里,其实也就是生成的 HTML。在处理混合内容的时候,我们也处理了单个站里的帖子,简单吧!

当原始的帖子修复后,我们只需要为每个站点去再生成 HTML 然后填充回去就行了。但是这个时候我犯了个错误。回填的时候拿的是当前站点(调用回填的那个站点),而不是原始站。这导致 meta.stackexchange.com 里的 12345 帖子被 stackoverflow.com 里的 12345 帖子所替代。有的时候是答案、有的时候是问题,有的时候有一个 tag wiki。这也导致了一些很有意思的帮助文档。这里有一些相应的后果

我只能说,还好修复的过程挺简单的:

再一次将数据填充回去就能修复了。不过怎么说,这个当时算是在公共场合闹了个笑话。抱歉。

开源

这里有我们在这个过程中产出的项目,帮助我们改进了 HTTPS 部署的工作,希望有一天这些能拯救世界吧:

下一步

我们的工作并没有做完。接下去还有一此要做的:

  • 我们要修复我们聊天域名下的混合内容,如 chat.stackoverflow.com,这里有用户嵌入的图片等
  • 如果可能的话,我们把所有适用的域名加进 Chrome HSTS 预加载列表
  • 我们要评估 HPKP 以及我们是否想部署(这个很危险,目前我们倾向于不部署)
  • 我们需要把聊天移到 https://
  • 我们需要把所有的 cookies 迁移成安全模式
  • 我们在等能支持 HTTP/2 的 HAProxy 1.8(大概在九月出来)
  • 我们需要利用 HTTP/2 的推送(我会在六月与 Fastly 讨论这件事情——他们还现在不支持跨域名推送)
  • 我们需要把 301 行为从 CDN/代理移出以达到更好的性能(需要按站点发布)

HSTS 预加载

HSTS 指的是「HTTP 严格传输安全」。OWASP 在这里有一篇很好的总结。这个概念其实很简单:

  • 当你访问 https:// 页面的时候,我们给你发一个这样的头部:Strict-Transport-Security: max-age=31536000
  • 在这个时间内(秒),你的浏览器只会通过 https:// 访问这个域名

哪怕你是点击一个 http:// 的链接,你的浏览器也会直接跳到 https://。哪怕你有可能已经设置了一个 http:// 的跳转,但你的浏览器不会访问,它会直接访问 SSL/TLS。这也避免了用户访问不安全的 http:// 而遭到劫持。比如它可以把你劫持到一个 https://stack<长得很像o但实际是个圈的unicode>verflow.com 上,那个站点甚至有可能部好了 SSL/TLS 证书。只有不访问这个站点才是安全的。

但这需要我们至少访问一次站点,然后才能有这个头部,对吧?对。所以我们有 HSTS 预加载,这是一个域名列表,随着所有主流浏览器分发且由它们预加载。也就是说它们在第一次访问的时候就会跳到 https:// 去,所以永远不会有任何 http:// 通信。

很赞吧!所以要怎么才能上这个列表呢?这里是要求:

  1. 要有一个有效的证书
  2. 如果你监听 80 端口的话,HTTP 应该跳到同一个主机的 HTTPS 上
  3. 所有子域名都要支持 HTTPS
  4. 特别是如果有 DNS 纪录的话,www 子域名要支持 HTTPS
  5. 主域名的 HSTS 头必要满足如下条件:
  6. max-aget 至少得是十八周(10886400 秒)
  7. 必须有 includeSubDomains 指令
  8. 必须指定 preload 指令
  9. 如果你要跳转到 HTTPS 站点上,跳转也必须有 HSTS 头部(而不仅仅是跳过去的那个页面)

这听起来还行吧?我们所有的活跃域名都支持 HTTPS 并且有有效的证书了。不对,我们还有一个问题。记得我们有一个 meta.gaming.stackexchange.com 吧,虽然它跳到 gaming.meta.stackexchange.com,但这个跳转本身并没有有效证书。

以 meta 为例,如果我们在 HSTS 头里加入 includeSubDomains 指令,那么网上所有指向旧域名的链接都会踩坑。它们本该跳到一个 http:/// 站点上(现在是这样的),一旦改了就会变成一个非法证书错误。昨天我们看了一下流量日志,每天仍有 8 万次访问的是通过 301 跳到 meta 子域上的。这里有很多是爬虫,但还是有不少人为的流量是从博客或者收藏夹过来的……而有些爬虫真的很蠢,从来不根据 301 来更新他们的信息。嗯,你还在看这篇文章?我自己写着写着都已经睡着 3 次了。

我们该怎么办呢?我们是否要启用 SAN 证书,加入几百个域名,然后调整我们的基础架构使得 301 跳转也严格遵守 HTTPS 呢?如果要通过 Fastly 来做的话就会提升我们的成本(需要更多 IP、证书等等)。Let’s Encrypt 倒是真的能帮上点忙。获取证书的成本比较低,如果你不考虑设置及维护的人力成本的话(因为我们由于上文所述内容并没有在使用它).

还有一块是上古遗留问题:我们内部的域名是 ds.stackexchange.com。为什么是 ds.?我不确定。我猜可能是我们不知道怎么拼 data center 这个词。这意味着 includeSubDomains 会自动包含所有内部终端。虽然我们大部分都已经上了 https:// ,但是如果什么都走 HTTPS 会导致一些问题,也会带来一定延时。不是说我们不想在内部也用 https://,只不过这是一个整体的项目(大部分是证书分发和维护,还有多级证书),我们不想增加耦合。那为什么不改一下内部域名呢?主要还是时间问题,这一动迁需要大量的时间和协调。

目前,我们将 HSTS 的 max-age 设为两年,并且不包括 includeSubDomains。除非迫不得以,我不会从代码里移除这个设定,因为它太危险了。一旦我们把所有 Q&A 站点的 HSTS 时间都设置好之后,我们会和 Google 聊一下是不是能在不加 includeSubDomains 的情况下把我们加进 HSTS 列表中,至少我们会试试看。你可以看到,虽然很罕见,但目前的这份列表中还是出现了这种情况的。希望从加强 Stack Overflow 安全性的角度,他们能同意这一点。

聊天

为了尽快启用 安全 cookie(仅在 HTTPS 下发送),我们会将聊天(chat.stackoverflow.com、[chat.stackexchange.com及 chat.meta.stackexchange.com)跳转至 https://。 正如我们的通用登录所做的那样,聊天会依赖于二级域名下的 cookie。如果 cookie 仅在 https:// 下发送,你就只能在 https:// 下登录。

这一块有待斟酌,但其实在有混合内容的情况下将聊天迁至 https:// 是一件好事。我们的网络更加安全了,而我们也可以处理实时聊天中的混合内容。希望这个能在接下去的一两周之内实施,这在我的计划之中。

今天

不管怎么说,这就是我们今天到达的地步,也是我们过去四年中一直在做的事情。确实有很多更高优先级的事情阻挡了 HTTPS 的脚步——这也远远不是我们唯一在做的事情。但这就是生活。做这件事情的人们还在很多你们看不见的地方努力着,而涉及到的人也远不止我所提到的这些。在这篇文章中我只提到了一些花了我们很多时间的、比较复杂的话题(否则就会太长了),但是这一路上不管是 Stack Overflow 内部还是外部都有很多人帮助过我们。

我知道你们会有很多的疑问、顾虑、报怨、建议等等。我们非常欢迎这些内容。本周我们会关注底下的评论、我们的 meta 站、Reddit、Hacker News 以及 Twitter,并尽可能地回答/帮助你们。感谢阅读,能全文读下的来真是太棒了。(比心)


原文链接: https://blog.alswl.com/2017/09/https-on-stack-overflow/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
alswl's avatar

一个关于 nolock 的故事

加入沪江不久,我就被扔到一个将集团 SQL Sever 的数据库迁移到 MySQL 的项目里, 同时伴随进行的还有 .net 系统迁移到 Java 系统。 在这个过程中我发现了一个很有趣的现象:历史遗留的 .net 项目中, 几乎所有的 SQL 中都会使用一个关键字:nolock。 这让我很困惑,nolock 的字面意思是对当前技术不使用锁技术,为什么要这样用呢?

我找了一个范例如下:

SELECT [id] 
FROM   [dbo].[foos] WITH(nolock) 
WHERE  aField = 42 
       AND bField = 1 

作为横向支持工程师,开发工程师会问我:「数据库即将从 SQL Server 迁移到 MySQL,我们编码中还需要使用 nolock 么? MySQL 里面对应的写法是什么?」。 我并没有 SQL Server 的生产环境使用经验,一时间无法回答。 于是课后做相关知识学习,这里就是这次学习的一点成果。

这个问题将被拆解成三个小问题进行回答:

  • nolock 是什么?
  • 为什么会需要在每个 Query 语句使用 nolock
  • MySQL 的对应写法是什么?

让我们一个一个来看。

第一个问题:nolock 是什么?

nolock 是 SQL Server 的一个关键字,这类关键字官方将其称之为 Hints。 Hints 的设计目的是为了能够让 SQL 语句在运行时,动态修改查询优化器的行为。 在语法上,Hints 以 WITH 开头。除了 WITH(nolock), 还有 TABLOCK / INDEX / ROWLOCK 等常见的 Hints。

让我们仔细看看 MSDN 文档上的解释:

nolock 的作用等同于 READUNCOMMITTED

READUNCOMMITTED 这是一种 RDBMS 隔离级别。 使用 nolock 这个关键词,可以将当前查询语句隔离级别调整为 READ UNCOMMITTED

计算机基础好的同学,应该对 READUNCOMMITTED 这个关键词还有印象。 而基础不扎实的同学,也许只是觉得这个关键词眼熟,但是讲不清楚这是什么。 如果阅读这句话完全没有理解困难,那恭喜你,你可以直接跳到下一节了。 其他朋友就跟随我继续探索一下 RDMBS 的世界,复习一下隔离级别相关的知识。

隔离级别

SQL 92 定义了四个隔离级别 (Isolation (database systems) - Wikipedia), 其隔离程度由高到低是:

  • 可序列化(Serializable)
  • 可重复读(Repeatable reads)
  • 提交读(Read committed)
  • 未提交读(Read uncommitted)

单单将这几个技术名词简单地罗列出来并没有什么意义,还有这几个问题需要搞清楚:

  • 隔离级别解决什么问题?
  • 为什么存在多种隔离级别?
  • 我们所谓的隔离级别从高到低,是什么含义,如何逐层降低的?

首先是「隔离级别解决什么问题?」, 用通俗的语言描述就是:加一个针对数据资源的锁,从而保证数据操作过程中的一致性。

这是最简单的实现方式,过于粗暴的隔离性将大幅降低性能, 多种隔离级别就是是为了取得两者的平衡。

接下来我们来回答第二个问题「为什么存在多种粒度的隔离级别?」 这其实是一个需求和性能逐步平衡的过程,

我们逐层递进,将隔离级别由低到高逐层面临进行分析。

Read Uncommitted

Read Uncommitted 这个隔离级别是最低粒度的隔离级别, 如同它的名字一般,它允许在操作过程中不会锁,从而让当前事务读取到其他事务的数据。

如上图所示,在 Transaction 2 查询时候,Transaction 1 未提交的数据就已经对外暴露。 如果 Transaction 1 最后 Rollback 了,那么 Transaction 读取的数据就是错误的。

「读到了其他事务修改了但是未提交的数据」即是脏读

Read Committed

想要避免脏读,最简单的方式就是在事务更新操作上加一把写锁, 其他事务需要读取数据时候,需要等待这把写锁释放。

如上图所示,Transaction 1 在写操作时候,对数据 A 加了写锁, 那么 Transaction 2 想要读取 A,就必须等待这把锁释放。 这样就避免当前事务读取其他事务的未提交数据。

但是除了脏读,一致性的要求还需要「可重复读」,即 「在一个事务内,多次读取的特定数据都必须是一致的 (即便在这过程中该数据被其他事务修改)」。

上图就是没能保证「可重复度」,Transaction 2 第一次读取到了数据 A, 然后 Transaction 1 对数据 A 更新到 A',那么当 Tranction 2 再次读取 A 时候, 它本来期望读到 A,但是却读到了 A',这和它的预期不相符了。 解决这个问题,就需要提升隔离级别到「Repeatable Read」。

Repeatable Read

这个名字非常容易理解,即保障在一个事务内重复读取时, 始终能够读取到相同的内容。来看图:

如上所示,当 Transation 2 读取 A 时候,会同时加上一把 Read Lock, 这把锁会阻止 Transaction 1 将 A 更新为 A',Transaction 1 要么选择等待, 要么就选择结束。

当我们将隔离级别升到这里是,似乎已经完美无缺了。 不管是写入还是读取,我们都可以保证数据的一致性不被破坏。 但是其实还有漏洞:新增数据的一致性!

上述的三个隔离级别,都是对特定的一行数据进行加锁, 那假如将要更新的数据还没有写入数据库,如何进行加锁呢? 比如自增表的新键,或者现有数据内的空缺 Key?

如图所示,在上述操作中,Transaction 2 查询了一个范围 Range 之后,Transaction 1 在这个范围内插入了一条新的数据。此时 Transaction 2 再次进行范围查询时候, 会发现查询到的 Range 和上次已经不一样了,多了一个 newA。

这就是最高隔离级别才能解决的「幻影读」: 当两个完全相同的查询语句执行得到不同的结果集, 这常常在范围查询中出现。

Serializable

从字面意思看,该隔离级别需要将被操作的数据加锁加一把锁。 任何读写操作都需要先获得这把锁才能进行。如果操作中带 WHERE 条件, 还需要将 WHERE 条件相关的范围全部加锁。

如图所示,在 Transaction 2 操作过程中,会对 Range 进行加锁, 此时其他事务无法操作其中的数据,只能等待或者放弃。

DB 的默认隔离级别

现在我们已经理解了隔离级别,那么「SQL Server 默认使用的隔离级别是什么呢?」 根据 Customizing Transaction Isolation Level 这个文档描述,SQL Server 默认隔离级别是 READ COMMITTED。

MySQL InnoDB 的默认隔离级别可以在 MySQL :: MySQL 5.7 Reference Manual :: 14.5.2.1 Transaction Isolation Levels 查询到,是 Read-Repeatable。

隔离级别并没有最好之说,越高隔离级别会导致性能降低。 隔离级别的设定需要考虑业务场景。

第二个问题:为什么要使用 nolock?

我们已经知道 nolock 的作用是动态调整隔离级别。 那为什么在 SQL Server 的 Query 操作中,需要启用 nolock 呢? 我问了几个工程师,他们都语焉不详,或者是很泛泛地说:禁用读写锁,可以提升查询性能。

此时我产生了困惑:「那么此时的数据一致性就不需要考虑了么? 我们的数据库,已经到了需要禁用锁的程度来进行优化了么?」 我于是自己去探索,想知道为何广泛使用 nolock 会成为一个「最佳实践」?

由于时代久远,我只能追述到一些相关信息,比如 Top 10 SQL Server Integration Services Best Practices | SQL Server Customer Advisory Team 中提到 「Use the NOLOCK or TABLOCK hints to remove locking overhead.」 但这个是针对于 SSIS 查询器,并不是针对业务内部使用。 反而能找到一大堆的文档,在反对使用 nolock 这个关键字。

继续追查下去,还从蛛丝马迹中寻找到一个使用 nolock 的理由, SQL Server 默认是 Read Committed, 更新操作会产生排它锁,会 block 这个资源的查询操作, 已插入但未提交的数据主键也会产生一个共享锁, 而此时则会 block 这张表的全表查询和 Insert 操作。 为了避免 Insert 被 Block,就会推荐使用 nolock

为了验证这是原因,我做一些 nolock 测试。

nolock 测试

检查当前 SQL Server 隔离级别,确认隔离级别是默认的 Read Committed:

SELECT CASE transaction_isolation_level
       WHEN 0
         THEN 'Unspecified'
       WHEN 1
         THEN 'ReadUncommitted'
       WHEN 2
         THEN 'ReadCommitted'
       WHEN 3
         THEN 'Repeatable'
       WHEN 4
         THEN 'Serializable'
       WHEN 5
         THEN 'Snapshot' END AS TRANSACTION_ISOLATION_LEVEL
FROM sys.dm_exec_sessions
WHERE session_id = @@SPID

-- ReadCommitted

创建表,初始化数据:

CREATE TABLE foos (
  id    BIGINT    NOT NULL,
  value NCHAR(10) NULL,
  CONSTRAINT pk PRIMARY KEY clustered (id)
);
INSERT INTO foos (id, value) VALUES (1, '1'), (2, '2');

在 Transaction 1 中发起 Update 操作(INSERT / DELETE 同理),但是并不做 Commit 提交:

BEGIN TRANSACTION;
INSERT INTO foos (id, value) VALUES (3, '3');

开启一个新的 Session,发起全表查询和新增 PK 查询操作:

SELECT * FROM foos;
SELECT * FROM foos WHERE id = 4;

不出所料,此时查询果然会被 Block 住。

MVCC

并发控制的手段有这些:封锁、时间戳、乐观并发控制、悲观并发控制。 SQL Server 在 2015 后,引入了 MVCC(多版本控制)。 如果最终数据是一致,会允许数据写入,否则其他事务会被阻止写入。 那么 MVCC 引入是否可以解决 Insert 数据的锁问题? 同样,我做了以下测试:

查询 SQL Server 使用启用 MVCC ALLOW_SNAPSHOT_ISOLATION:

SELECT name, snapshot_isolation_state FROM sys.databases;

使用 T-SQL 启用测试表的 SNAPSHOT_ISOLATION:

ALTER DATABASE HJ_Test3D SET ALLOW_SNAPSHOT_ISOLATION ON;

接着重复上面里面的 Insert 试验,依然被 Block 住。 看来 MVCC 并不能解决 Insert 锁的问题。

SQL Server 2005 之后还需要使用 nolock 么?

从官方文档和上文测试可以看到,在 Insert 时候,由于排它锁的存在, 会导致 SELECT ALL 以及 SELECT 新插入数据的相关信息被锁住。 在这两种情景下面是需要使用 nolock 的。

除此之外,有这么几类场景可以使用 nolock

  • 在 SSIS 查询器中进行数据分析,不需要精准数据
  • 历史数据进行查询,没有数据更新操作,也不会产生脏数据

我们需要思考一下,性能和数据一致性上的权衡上, 我们是否愿意放弃数据一致性而为了提高一丝丝性能? 以及我们有多少场景,会频繁使用 SELECT ALL 操作而没有查询条件?

微软官方在 2015 的特性列表里面,明确地指出 nolock 特性未来会在某个版本被废除:

Specifying NOLOCK or READUNCOMMITTED in the FROM clause of an UPDATE or DELETE statement.

而改为推荐:

Remove the NOLOCK or READUNCOMMITTED table hints from the FROM clause.

事实上,我听过不少团队会禁止在生产环境使用不带 WHERE 条件的 SQL。 那在这种模式下,产生相关的问题的几率也就更小了。 如果有很高的并发需求,那需要考虑一下是否需要其他优化策略:比如使用主从分离、 Snapshot 导出、流式分析等技术。

第三个问题:MySQL 的对应写法是什么?

终于轮到 MySQL 的讨论了。MySQL,InnoDB 天生支持 MVCC, 并且支持 innodb_autoinc_lock_mode AUTO_INCREMENT Handling in InnoDB。 这样可以避免 Insert 操作锁住全局 Select 操作。 只有在同时 Insert 时候,才会被 Block 住。

innodb_autoinc_lock_mode 支持几种模式:

  • innodb_autoinc_lock_mode = 0 (“traditional” lock mode)
    • 涉及auto-increment列的插入语句加的表级AUTO-INC锁,只有插入执行结束后才会释放锁
  • innodb_autoinc_lock_mode = 1 (“consecutive” lock mode)
    • 可以事先确定插入行数的语句,分配连续的确定的 auto-increment 值
    • 对于插入行数不确定的插入语句,仍加表锁
    • 这种模式下,事务回滚,auto-increment 值不会回滚,换句话说,自增列内容会不连续
  • innodb_autoinc_lock_mode = 2 (“interleaved” lock mode)
    • 同一时刻多条 SQL 语句产生交错的 auto-increment 值

这里也做了相应的测试。首先检查数据库隔离级别和 innodb_autoinc_lock_mode 模式:

SELECT @@global.tx_isolation, @@session.tx_isolation, @@tx_isolation;
SHOW variables LIKE 'innodb_autoinc_lock_mode';

检查后发现都是 Repeatable Read,innodb_autoinc_lock_mode 模式是 1。 然后创建测试表:

CREATE TABLE `foos` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;

在 Transaction 1 中 Insert 数据:

START TRANSACTION;
INSERT INTO foos (name) VALUES ("a");

在 Transaction 2 中 Select 数据,可以正常查询:

SELECT * FROM   foos;

在 Transaction 2 中 Insert 数据,会被 Block 住:

START TRANSACTION;
INSERT INTO foos (name) VALUES ("a");

这个测试可以证明 MySQL 可以在 innodb_autoinc_lock_mode=1 下, Insert 同时 Query 不会被 Block, 但是在另外一个事务中 Insert 会被 Block。 结论是,由于 innodb_autoinc_lock_mode 的存在,MySQL 中可以不需要使用 nolock 关键词进行查询。

回顾一下

本文着重去回答这么几个问题:

  • 为什么要用 noloc
  • 为什么要改变隔离级别?
  • 为什么 MySQL 不需要做类似的事情?

虽然只凑足了三个 「为什么」 的排比, 但是聪明的读者仍然会发现,我是使用了著名的 五个为什么 方法思考问题。 通过使用这个方法,我们最后不但打破了老旧的最佳实践,还了解了本质原理, 并找到了新的最佳实践。

希望读者朋友在遇到困难时候,多问几个为什么,多抱着打破砂锅问到底的精神, 这样才能让每个困难成为我们成长的垫脚石。

相关资料


原文链接: https://blog.alswl.com/2017/09/sql-server-nolock/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
jaseywang's avatar

由 rc_mksid 引起 pppd 奔溃的一个 bug

最近手贱想把手头的几台玩具机器统一下标准,其中一个标准是将 kernel.pid_max 增加到了 512000,结果就在当天的凌晨,一台跑着 pptp 的 VPS 崩溃了: Jun 20 22:53:09 jaseywang vps pptpd: ======= Backtrace: ========= Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(__fortify_fail+0x37)[0x7fdba6416047] Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(+0x10d200)[0x7fdba6414200] Jun 20 22:53:09 jaseywang vps pptpd: /lib64/libc.so.6(+0x10c709)[0x7fdba6413709] Jun 20 … Continue reading
alswl's avatar

当我们在聊监控,我们在聊什么?

最近在团队中给大家做了一个分享,泛泛地聊了一些有关「监控」的话题。 其实做分享对分享者的作用往往大于参与者。 这是一次将自己知识的梳理的过程,于是我将这次分享整理成这篇文章。

目的 🎯

我们先来聊聊,什么是「监控」,以及我们期望通过「监控」完成哪些目的?

传统意义上的监控,是指:

通过一些手段和工具,关注运行中的硬件、软件、用户体验的关键数据,将其暴露出来。 当关键数据出现异常时候发出警告,进行人工或者自动的响应。

我们平时看到的最常见的监控系统,比如 Zabbix,提供了丰富的模板, 可以监控服务器的 Load / CPU Usage / Alive 这些常规指标。 并在出现问题时候,对其进行报警通知。 随后运维工程师们会上线进行应急操作,case by case 的处理故障。

我将上面的使用目的归纳为:

  • 故障发生时提供数据报警
  • 提供历史数据以供分析

故事到这里似乎可以结束了,可监控真的是这么简单的么? 当然没,随着时代的进步,用户对服务提出了更为严苛的要求, 同时我们也有能力进一步控制平均故障修复时间 (MTBF), 上述描述的做法已经不能满足我们了。

现在让我们切换一下视角,从传统的 OPS 的视角切换到 SRE (Site Reliability Engineering)的视角。 当我们在关注网站整体的可用性时,我们会发现: 故障警报处理当然很重要,但是我们根本上想减少甚至避免 MTBF。 我们有两种手段: 一种是去除单点故障,让问题自然发生,但是不对线上造成影响; 另一种是在问题出现的早期就发现并进行及时修复。 前者是高可用范畴,后者就是我们今天关注的「监控」了。

监控的目的是要将灾难消灭在襁褓里;在灾难即将出现或者发生问题时,
给大家展示直接的原因

那为了达成这两个目标,我们需要回到问题的本质,重新思考两个问题:

  1. 监控哪些对象?
  2. 如何识别故障?

对象 🐘🐘

我们说的监控对象,一般指的都是某个资源, 资源即持有某种其他方需要的某些属性的载体,包括硬件、软件。 除了资源这种类型,还有一种常见的监控对象是「体验」,即终端用户的访问感受, 这块内容我们暂时略去。

让我们来先看一下常见的资源:

  • 硬件
    • 服务器
    • 网络设备
  • 软件
    • Application
    • Infrastructure

这个分类是粗粒度的描述,为了落地地描述监控对象对象的健康状况, 我们还要进一步细化。以「服务器」为例,我们可以将其监控的内容细化为以下监控项:

  • CPU
  • Memory
  • Network interface
  • Storage devices
  • Controllers

如何评估这些监控项的健康状况?我们使用 SLI(Service Level Indicator)。 比如可用性就是一个最容易理解的 SLI。 这里我将资源归为两类,面向用户提供服务的资源和面向存储的资源, 以下是针对这两类资源的常见 SLI:

  • User-facing Service
    • Availability
    • Latency
    • Throughput
  • Storage System
    • Latency
    • Throughput
    • durability

基于 SLI 建立的数字关键指标,称之为 Service Level Objective。 SLO 往往是一组数字范围,比如 CPU 负载的 SLO 可以设置为 0.0-6.0(针对 8 核 CPU)。 不同的资源、不同的业务场景,会有不一样的 SLO 设计。

看到这里,我们已经聊了要监控哪些指标,那么接下来我们聊聊如何用量化的思想, 帮助指标更易于识别、分析和决策。

量化的思想 🔢

刚开始担任线上救火队成员时候,当有个系统出现问题时候,我经常听到这样的描述: 网站挂了、页面打不开了,CPU 出问题了,内存爆了,线程池炸了等等。 这样的表述虽然没错,但带来的可用价值太少,信息熵太低。 这样的说辞多了,就给人产生一种不靠谱,不科学的感觉。

那怎样才能成为科学的描述? 古希腊哲学家在思考宇宙的时候,提出了一种心智能力, 从而打开了科学的窗子,这就是 Reasonable,中文名叫理智,这成为了自然科学的基石。 使用 Reasonable 探讨意味着探讨要深入问题的本质,不停留在表象,挖掘出真正有价值的内容。

但是光有 Reasonable 还不够,B站粉丝建了一个微博,每天会检查 今天B站炸了吗, 他只能告诉我们炸没炸,不能给工程师带来实际的用处。 在科学的发展历史上,我们可以发现在亚里士多德的著作里没有任何数据公式。 他对现象只有描述,只是定性分析,通过描述性状来阐述定理。 这个定性的研究方式到了伽利略那里才出现了突破。 这里我们可以引入第二个关键词是 Quantifier,量化。 伽利略率先使用定量分析的方法,并将其运用到动力学和天文学,从而开创了近代科学。

如果我们以定量的方式来描述网站挂没挂,就会变成:网站的响应耗时在 30s,基本无法使用。 描述线程池出问题,就会变成:active 线程数量是 200,已经到达 maxCount 数量,无法进行分配。 你看,通过这样的描述,我们一下子就能发现问题出在哪里。

USE 💡

现在我们已经了解了「监控哪些对象?」,以及尝试用「量化」这个法宝来「识别故障」。 那有没有一些最佳实践帮助大家高效的识别故障呢?这里我推荐 Brend Gregg 大神的 USE 方法。 Brend Gregg 是 Netflix 的首席 SRE,著有 Systems Performance Book, 目前已经出版中文版 性能之巅:洞悉系统、企业与云计算

USE 分别是三个单词的首字母缩写:

  • Utilization:使用率,CPU running percent,硬盘的 IO
  • Saturation:饱和度,一般偏存储型资源,内存使用,硬盘使用
  • Error:错误数

我们可以为每个资源找到各自的 USE 度量指标,具体的 Check List 清单可以参考 USE Method: Rosetta Stone of Performance Checklists

这里举个例子,前段时间在设计 MySQL HA 方案时候,同时关注了 MySQL 的监控方案, 那么针对 MySQL,我们要做哪些监控呢?下面是使用 USE 方法设计出来的 SLI:

  • Business
    • Questions:语句计总,Throughput
    • Slow_queries:慢查询计总,Error
    • Com_select:查询语句计总,Throughput
    • Com_insert:插入语句计总,Throughput
    • Com_update:更新语句计总,Throughput
  • Threads & Connections
    • Threads_connected:当前连接数,Utilization
    • Threads_running:当前使用中连接数,Utilization
    • Aborted_connects:尝试连接失败数,Error
    • Connection_errors_max_connections:由于连接数超标从而失败的连接数,Error
  • Buffer
    • Innodb_buffer_pool_pages_total:内存使用页数,Utilization
    • Innodb_buffer_pool_read_requests:读请求数计总,Utilization

完 🏁

如果你对我上面描述的还意犹未尽,建议你可以看 Effective Monitoring and Alerting。 虽然本书没有中文版,但是关于监控、报警的原理解析很到位,值得一看。 另外还有一本 SRE: Google运维解密, 里面有不少篇幅在讲「SLA」,也是和监控、报警息息相关的。

这次讲了一些概念性的内容,期望对大家有帮助,下一次我再分享一篇文章,聊聊 Metrics。


原文链接: https://blog.alswl.com/2017/06/monitoring-introducing/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
Phoenix Nemo's avatar

NetFLOW / sFLOW 流量报告:FastNetMon + InfluxDB + Grafana

最近稍微有点时间折腾了下 Cisco 的三层交换,尝试搭建了一套数据中心用的流量统计/监控/报告系统。过程不是很复杂,但是也只算利用了一套高级软件组合的一点点功能。之后打算继续研究更多的功能实现,不过也要看有没有时间了…

准备工作

首先确认出口路由设备支持 netflow/sflow 的对应版本。一般 Cisco 的路由器或者三层交换都是支持的。

然后准备一个常见的 Linux 系统,虚拟机或者物理机都可以。

出口路由设备能够连通到该 Linux 系统,并且 flow collector 设置到该 Linux 系统的 IP 地址和对应端口。

FastNetMon

安装 fastnetmon,只需要一条简单的脚本命令。

然后将所有要监控的网段加入 /etc/networks_list。一行一个,例如:

1
2
3
10.1.0.0/16
192.168.254.0/24
8.8.0.0/16

按照安装文档打开两个终端,分别启动主进程和客户端

1
/opt/fastnetmon/fastnetmon
1
/opt/fastnetmon/fastnetmon_client

如果没有问题,应该在客户端上可以看到收到的 flow 数据。

先关闭 fastnetmon 进程,修改配置文件打开 Graphite 支持:

1
2
3
4
graphite = on
graphite_host = 127.0.0.1
graphite_port = 2003
graphite_prefix = fastnetmon

InfluxDB

安装 InfluxDB,官方提供了各种包管理器的安装方式。

配置文件一般位于 /etc/influxdb/influxdb.conf,需要根据环境做安全相关设置(侦听地址、端口、鉴权、etc)并打开 Graphite Simulation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[[graphite]]
enabled = true
bind-address = "127.0.0.1:2003"
database = "flow_dc1"
protocol = "tcp"
consistency-level = "one"
name-separator = "."

# batch-size / batch-timeout requires InfluxDB >= 0.9.3
batch-size = 5000 # will flush if this many points get buffered
batch-timeout = "1s" # will flush at least this often even if we haven't hit buffer limit

templates = [
"fastnetmon.hosts.* app.measurement.cidr.direction.function.resource",
"fastnetmon.networks.* app.measurement.cidr.direction.resource",
"fastnetmon.total.* app.measurement.direction.resource"
]

顺序重启 InfluxDB 和 fastnetmon。检查 flow 数据是否记录到 InfluxDB:

1
2
3
4
5
6
7
8
9
10
11
$ influx
Connected to http://localhost:8086 version 1.2.4
InfluxDB shell version: 1.2.4
> use flow_dc1
Using database flow_dc1
> select mean(value) from networks where direction = 'incoming' and resource = 'bps' group by *
name: networks
tags: app=fastnetmon, cidr=10_1_0_0_24, direction=incoming, resource=bps
time mean
---- ----
0 4735.049632696411

Grafana

Grafana 是一款非常强大且易用的数据可视化工具。安装 Grafana 然后修改配置文件的必要部分,配置文件一般位于 /etc/grafana/grafana.ini

完成后重启 Grafana,将浏览器指向 Grafana 的 HTTP 服务器地址即可看到登录界面。如果内部使用的话,建议关闭匿名访问和注册功能。

使用默认的 admin / admin 登录,按照引导完成配置、添加数据源(Data source),数据源即是 InfluxDB 的 HTTP API 地址。如果 Grafana 中限制了数据源白名单,需要将 InfluxDB 的 HTTP API 地址和端口加到白名单里。

添加面板、Graph,在 Graph 编辑模式里写入类似这样的查询语句:

1
SELECT mean("value") FROM "networks" WHERE "direction" = 'incoming' AND "resource" = 'bps' AND "cidr" =~ /^10_1_0_0_16/ AND $timeFilter GROUP BY time($interval) fill(previous)

即可看到有图表出现。根据需求完善查询语句和图表配置即可简单实现各种可视化效果。例如流量和数据包的实时报告:

总结

通过配合 FastNetMon,InfluxDB 和 Grafana 即可快速实现一套基于 NetFLOW / sFLOW 的流量统计报告系统。但是 FastNetMon 的功能远不止流量统计,Grafana 也有大量插件和灵活的用法可以满足更多需求。如果配置合理,此方案也可适用于 40Gbps+ 接入的中型数据中心且成本低廉。以及——

  1. InfluxDB 真的很快!
  2. Grafana 的图表真的很省资源!
  3. Chronograph 卡死了我的浏览器!(i7-7700K / Chrome)

以及一大早手工修好了 K812 的耳机线,省掉了 2 万日元的线材费用非常开心

alswl's avatar

XSS 攻击的处理

这是一年前写的项目笔记,一直在我的待办事项里等待做总结,今天偶然翻到,就整理成文章发出来。 谨以此文怀念 乌云


事情缘由

春节前的某一天,收到一封来自乌云(国内知名白帽子团队)的邮件, 告知我厂网站上出现一例 XSS 漏洞。 因为以前对 XSS 输入做过防御,还以为是某个前端 DOM 上的 XSS 漏洞, 后来仔细一看,不妙,是个影响甚大的存储型 XSS 漏洞。

这里简单科普一下 XSS 跨网站脚本 -维基百科,自由的百科全书 中介绍到:

跨网站脚本(Cross-site scripting,通常简称为XSS或跨站脚本或跨站脚本攻击)是一种网站应用程序的安全漏洞攻击,是代码注入的一种。 它允许恶意用户将代码注入到网页上,其他用户在观看网页时就会受到影响。这类攻击通常包含了HTML以及用户端脚本语言。

XSS 攻击可以分成两种,反射性 XSS / 存储型 XSS。前者是需要用户触发的 XSS, 针对当前用户的攻击行为。而后者存储型 XSS 则更为严重,一旦攻击代码被保存, 所有访问被攻击的页面,都会触发用户被攻击行为。

这次爆出的问题就是最严重的存储型 XSS,意味着每个访问到有问题页面的用户都会中招。 时间紧迫,问题必须被解决。

XSS 实现手段

在解决问题之前,需要对这个问题有必要的基础认识。 我们先看看 XSS 攻击是如何工作的,以及攻击者的目的是什么。

XSS 的原理是通过构造特殊的数据,并通过传递参数或者保存表单数据的方式, 让这些构建的数据破坏 DOM 结构,从而让自己预先构造数据中的 JS 脚本被执行。

检查存储型 XSS 漏洞的方法,可以在对应的 input field 里放入一些构造的数据,如果保存后可以被执行,就说明存在 XSS 漏洞。

常见的检测方法(来自 跨网站脚本 - 维基百科,自由的百科全书

><script>alert(document.cookie)</script>
='><script>alert(document.cookie)</script>
"><script>alert(document.cookie)</script>
<script>alert(document.cookie)</script>
<script>alert (vulnerable)</script>
%3Cscript%3Ealert('XSS')%3C/script%3E
<script>alert('XSS')</script>
<img src="javascript:alert('XSS')">
<img src="http://xxx.com/yyy.png" onerror="alert('XSS')">
(这个仅限IE有效)

攻击者通过 XSS 可以窃取用户的相关信息,如果用户是管理员,那么影响更大。 通过这些身份信息,攻击者可以进一步篡改信息或者进行诈骗,后果不堪设想。 PS:一个有效粗暴的方式,是将对公、对内系统的域名分离,对内部系统进行物理级别隔离。

我厂历史上的处理方案

XSS 问题又来已久,咱厂子开了这么久,历史上如何防御的呢? 答案是用了两个策略:第一个是使用 OWASP 提供的库进行内容过滤,第二个是在存储数据时,存储了转义后的数据。

在技术上处理 XSS,有两种态度可以选择:第一种是前置过滤,即将用户所有数据都进行转义, 在输出时候在前端(模板渲染)层面直接输出。 第二种是用户输入的数据不经过转义就直接存储起来,前端在使用时候保证对数据进行转义。

我厂历史上使用的方案的前者,优点是在于前端不需要在每个地方转义, 避免某个地方忘记了转义,从而导致漏洞。缺点则是在输出内容到非 Web 客户端时候,比如 APP,需要进行额外的数据处理过程, 否则 HTMLEncode 的内容,在 APP 上面无法正确输出。

这个处理方案是稳妥的,那么为什么最近又暴露出问题了? 排查之后发现,原来最近有若干个服务迁移到了一个新系统, 而新系统在安全上面没有全局处理,所以爆出了漏洞。

本次处理方案

知道了原因,那么可以快速解决问题了。在这次处理过程中,我们讨论了在当前移动平台增长迅速,Web 平台增长缓慢的大势下,能否直接存储用户原始数据? 而且由于规范制定不严格,目前系统内有些地方存储转码后数据,有些地方存储转码前数据。 导致在一些特殊的字符(颜文字)处理上不一致,从而导致在处理 br / < 这类特殊字符时,表现不同。

由于 DB 中有部分数据转义处理,部分数据原文存储,所以先处理输出后敏感信息,在模板层面启用全局 encode。 将有危险的数据转移为在 HTML 文本。

PS:现代 Web 框架的模板渲染引擎,一般会默认开启 HTMLEncode,而 Freemarker 居然在 2.3.24-rc01 才支持,现在都没有发布,唉……

处理方案:

  • 开启全局 HTML 输出 Encode,有一个 Default HTML-escape using Freemarker 方案,可以默认开启 Html Encode,在这个处理方案中,需要注意有些地方真的需要输出原始 html,需要 noescape 特殊处理
  • 检查所有前端操作,禁止字符串拼接,使用框架支持的模板进行渲染,拖小菊的福,新系统在这块工作完成度一直比较好
  • 将 OWASP 方案强制开启

其他 Tips

OWASP 有一个很长的 列表,教导如何避免 XSS,里面提到了:

  • 为何以及如何进行「积极防御」,对立面是仅仅输出时候转义内容本身
  • 几条对抗 XSS 的规则
    • 尽量不在特定地方输出不可信变量:script / comment / attribute / tag / style, 因为逃脱 HTMl 规则的字符串太多了。
    • 将不可信变量输出到 div / body / attribute / javascript tag / style 之前,对 & < > " ' / 进行转义
    • 将不可信变量输出 URL 参数之前,进行 URLEncode
    • 使用合适的 HTML 过滤库进行过滤
    • 预防 DOM-based XSS,见 DOM based XSS Prevention Cheat Sheet
    • 开启 HTTPOnly cookie,让浏览器接触不到 cookie

最后送上一个 XSS 攻击工具 http://webxss.net/,知己知彼,百战不殆。


原文链接: https://blog.alswl.com/2017/05/xss/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
Phoenix Nemo's avatar

不作死就不会死系列,TFTP 修复变砖的 Nighthawk X6

由于之前买的 AC87U 经常被 roommate 抱怨掉线(风评表示这货 5G 有问题,然而我连着 5G 毛事儿没有,隔壁用 2.4G 却一直掉线)…

新购入的路由器是 Netgear Nighthawk X6 R8000。

由于之前的 Security Advisory,所以到手第一件事就是配上网络更新固件啦。更新挺慢的于是点点点完事儿撸猫去了。过了一会儿回来一看怎么还没网络?得,砖了…

讲道理,Netgear 也算大厂了,这种 online update 干了不知道多少回,第一次遇到这都能变砖的(扶额。

现象就是电源橙色灯亮后一会儿变成白色灯闪烁,且网络服务没有启动。尝试过 factory reset 无效,官方提供的 TFTP 强刷工具也无效(刷不进…

解决方案反而是意想不到的简单。总之大概记录下修复的过程。

  1. 官方网站下载适用的新版固件并解压,应该得到一个 .chk 文件
  2. 关闭路由器电源等待 10 秒,网线插 LAN 口开机。
  3. 检查是否获得了正确的 IP。如果没有,可能 DHCP 服务没起来。手动设置一个正确的 IP 吧。然后能 ping 通路由器 IP 即可。
  4. 电源灯开始闪烁的时候,执行命令 tftp -i [router ip] put [path/to/firmware.chk]。例如 tftp -i 192.168.1.1 put ./R8000-V1.0.3.36_1.1.25.chk
  5. 等一会儿路由器自动重启,搞定。

配置都没丢…然后我依旧没有搞定 OCN 要怎么连 IPv6… 说好的 IPv6 PPPoE 呢…

Phoenix Nemo's avatar

Minecraft 服务器资源控制策略:AI 抑制而非数量限制

Minecraft 的 lag 问题已经司空见惯,各种控制资源消耗和卡顿的插件也层出不穷。但是它们几乎都非常用力地在一个点上:控制实体数量。

这并不无道理,因为 Minecraft 中最消耗资源的部分就是实体。但是暴力控制实体数量会导致刷怪塔无法正常工作、掉落物清理速度过快等问题,在生存服务器中可能引发玩家的强烈不满。

所以,喵窝开发组从另一个角度做出了一些尝试。

启发

生物实体的数量巨大,主要集中的地区显然不是野外的自然刷怪区,而是玩家聚集的刷怪场、村民工程、动物养殖场等。如果不限制生物的数量和密度同时降低资源消耗,那么只能从生物实体的特性入手了。

Minecraft 最近的版本中引用了 NoAI 的 NBT Tag,带有此标签的生物将不会进行 AI 计算。换句话说,除了占用服务器内存中的一点数据,几乎不会对这个生物实体有任何其他的 CPU 算力消耗。

也就是说,实体消耗的算力资源,绝大部分都是 AI 计算的消耗。

方案

抓上一票人做了一些测试,结果证实生物失去 AI 后大幅降低了 CPU 的算力消耗。这是个 positive 的信号,但是接下来的测试则遇到了问题。

对于养殖场,等生物数量变化不大(或者说只是定期来清理并重新养殖一次)的设施,生物失去 AI 的影响很小,只有在重新繁殖时需要恢复 AI。但是刷怪塔则因为生物没有 AI,同时也被强制不受重力影响而几乎无法使用,即便同时设置 NoGravityfalse 也无效。

开发组中 @Librazy 提到了 Spigot 的一个参数 nerf-spawner-mobs,开启时刷怪笼生成的生物将不会拥有 AI,但是会被外界影响(例如水流和火球等)而移动。这个选项是全局的,因此不需要开启,只需要反射 spigot 中设置该功能的方法即可。

于是整个方案的流程便是当服务器卡顿时抑制生物密集区的生物 AI 从而降低资源占用,同时最大程度上保证玩家对生物的需求。「服务器卡顿」的考量以服务器 TPS 而非实体数量为准,当服务器 TPS 高于一定值时即认为服务器没有超负荷,不会有任何操作,最大程度上利用硬件的性能。

实现

插件主要由开发组的 @Cylin@Librazy 编写,源代码以 MIT 协议发布在 GitHub 上。

插件每隔一段时间扫描服务器的 TPS 确认运行状况,如果 TPS 低于阈值则触发 AI 控制,TPS 高于一定值且持续一段时间即认为服务器已恢复正常运行状态,自动恢复被抑制的实体 AI 减少对生存体验的影响。

实现过程中额外添加了一些额外可能被生存服务器用到的功能:

  • per-world 控制,如果玩家需要建造以仇恨为基础的小黑塔,可以关闭对末地的控制。
  • 实体总量和单区块实体密度在 AI 抑制时纳入考虑,更加精准抑制资源消耗较高的区块。

测试

yasui 插件在 毛玉線圈物語 服务器中应用测试。由于近期玩家数量爆炸式增长(日常在线 5 人到 ~30 人甚至 50 人),各种实体控制插件均告无效。yasui 插件应用后被证实数次发挥作用,没有任何实体数量限制的前提下将服务器 TPS 稳定在 19 以上,服务器实体承载数量从 ~2500 提到至接近 5000,并且还有继续提高的可能(数次触发中最高一次单世界实体记录是 4808,其他世界中仍有大约 2000 实体未被计入)。

吐槽:你们贼能刷

jaseywang's avatar

全网统一账户实践

分享下目前我们全网的账号管理体系。

整体的账户管理思路是分而治之。主要分为下面三类账户:
1. 办公网账户,也就是大家熟悉的域账户。对于办公网账户,全网用户一人一账户,在 OpenLDAP 的基础上做了一些开发,这是进入公司内部的大门,所有新入职的员工都会分配一个该账号,不管是在办公室连接 Wi-Fi 还是在家连接 anyconnect VPN,访问 confluence/jira 等基础办公设置,都需要通过此账户进行登录认证。
2. 生产网账户,主要用来访问线上、线下机器资源。工程师访问线上生产、测试机器,登录线下自助机树莓派(raspberry) 均需要通过此账户认证,整个 user/group 的分配、HBAC/sudo 的控制、密码/公钥的管理均在 freeIPA/IDM 上实现,freeIPA 是 RedHat 支持的一整套集成安全信息管理解决方案系统,又称 IDM。
3. 数据库账户,这块比较小众,简单带过。
下面会针对上面两大块分别介绍。

办公网账户

该账号与所有人息息相关,从入职第一天起到你的 lastday,都需要登录该账户才可以访问办公资源,包括常见的 Wi-Fi/Gitlab/Jira/Confluence/Jenkins/Cisco Anyconnect VPN/Zabbix/Grafana/跳板机(intermediate host) 以及自己开发的各种内部系统。

这里面需要先简单描述下目前我们的多网分离的架构,多网是指办公网(包含国际线路)、生产网、测试网、专线网(到医院 HIS/LIS 等信息系统)、OOB(带外管理网),这五张大网是处于「部分」分离的状态的,比如,办公网到生产网/测试网/专线网/OOB 是完全分离的状态,除非通过跳板机(后面全部以 ih 代指)登录之后再访问;生产网跟测试网也是几乎分离的,除了极个别的公共服务。以办公网登录生产、测试网为例,在进入用户验证这步之前,会先通过 IP <-> MAC <-> 用户身份的一一映射绑定,先通过最基本 IP:PORT 的 ACL 方式来控制住大部分的请求。所以在你能通过网页打开 git,通过 ssh 协议 pull/push git 仓库的时候,说明已经通过 TCP/IP 层面的验证机制以及用户级别的验证机制,具体如何实现的会在后面的博客中说明。

默认情况下,OpenLDAP 支持的 schema 非常有限;另外不同应用接入方式都不大一样,需要逐一尝试。

比如 LDAP 跟 Anyconnect 的对接,官方提供的 schema 仅仅基本可用,离生产还有一段距离,默认只支持添加一组 IP/Netmask,以及一条 IP 层面的 ACL,为此需要自行扩展维护一套 cisco.ldif 文件,这个是我们自行维护的一个 schema 文件,根据我们业务的情况,新增了一组 IP/netmask 以及若干的 ACL。下面是一个普通用户的数据文件示例:

从 slapcat 导出的字段数据可以看到,为了实现 Wi-Fi 账号跟 OpenLDAP 的共享,加入了 samba 相关的 schema,具体的可以看这个文件

再比如,对于 anyconnect/ih 的认证,需要增加两步验证机制,这里引入 OTP(privacyIDEA) 以及 Radius 作为跟 OpenLDAP 的桥梁例,具体的交互图可以看这里:

最终实现了用户的 CRUD 在 OpenLDAP 层面控制。用户的两步验证,下图可以看到,privacyIDEA 支持十余种的 token,包括 hOTP/mOTP/sms/email/Yubikey 等等:

privacyIDEA 不仅仅支持我们使用的 ldapresolver,sql/passwd/scim 的 resolver 都有很好的支持。目前我们使用 Google Authenticator 是基于事件的 HOTP(RFC4226):

用来管理 OpenLDAP 的工具有不少,包括我们使用的经典的 phpldapadmin,除此之外,fusiondirectoryweb2ldap 都是不错的选择。同时为了支持用户的自助修改、重置密码,定期修改用户密码,我们引入了 LTB,一个非常神奇的 LDAP 工具箱集合,目前仅支持通过邮件找回密码的链接来重置密码:

除了自助机服务,LTB 还支持 LDAP 性能、用户使用数据的监控等若干非常实用的脚本。
这么重要的基础服务稳定性肯定是需要保障的,通过 syncprov 模块实现双 master 的写的高可用,通过多个 slave 同步 master,前端挂 Haproxy 实现读的高可用:

对用户来说,他们看到的是下面这个样子:

PACKT 出版的 Mastering OpenLDAP 是本通俗易懂的书,浏览一遍之后应该能轻松应对上面的内容。 

生产网账户

这里的生产网泛指包含线上生产测试服务器线下自助机树莓派等在内的所有 *nix 系统。所有需要登录如上机器的用户都需要获得该账户的权限之后,才能访问基于 HBAC 机制的主机以及颗粒度更细致的用户执行权限。

整个系统基于 freeIPA/IDM 实现,默认情况下,每台机器有一个专门用来运行进程(我们所有的 JVM 进程都跑在 vm 上,不会出现混跑的状态)的 worker 用户,所有的相关的 bin/, log/, lib/ 等目录文件都通过 Jenkins 统一打包到 ~/worker/ 下面,实现跟系统文件的隔离。/home/worker/ 的权限如下:

worker/ 下面所有的目录文件权限都如上所示。这样,只要用户在 bm_be_sre 这个组下面,就能实现 worker/ 下面也即 JVM 相关服务的读取权限:

下图可以看到,appCenter 这个服务是跑在 worker 下面的:

类似的,如果想实现 worker/ 下面的写操作,比如执行 bin/ 下面的脚本,将用户加到 30002(sre) 这个组即可,这里完全可以各自的需求自定义。所有的 sudo/su 相关权限设置全部在 IPA 的 Policy 操作即可,比如下面这个赋予的是 sre 这个组的用户可以执行 /bin/su – worker、/bin/su worker 这两个单独的比较危险的命令以及 sre_allow 命令行组里面基础命令(/bin/chmod, /bin/chown, /bin/cp 等等):

目前单台 8G/4core 的 IPA 支撑着 1000+ 的线上生产测试的物理虚拟机以及线下 100+ 的树莓派,应付起来绰绰有余,CPU 平均 1% 利用率、disk IO 平均 10%。

对我们的用户来说,在经过办公网/VPN 的 IP ACL 过滤之后,通过办公网账号结合 OTP 登录线上的任意一台跳板机(ih),然后通过 ih 再登录线上的机器,跟办公网账户类似的是,需要定期修改密码。对有权限看到的用户来说,他们看到的是下面这样

以上的两块账户体系算是把当年在阿里的基础核心账户体系复制了一遍,对最终用户来说,体验相对来说会更好些,当然我们没法也暂时没必要实现类似阿里郎那种监控每台入网设备所有操作的系统。

需要注意的是,在 IPA 上新增的用户,或者新增 sudo 权限,由于 sssd 的缓存,不会立即生效,可以通过 sss_cache 或者删除缓存文件可以让规则立即生效,这部分可以以类似 hook 的方式触发,每次修改用户权限的时候自动触发该操作。

freeIPA 结尾的 A 表示 audit,实际情况是目前还不支持 audit 功能,所以这块功能我们通过 snoopy 结合 rsyslog 方式进行收集审计

对于办公网账户,目前通过 python-ldap 实现了多个层次的自助以及服务的打通,freeIPA 还未进行这块的开发工作,最终的效果类比阿里内外,一个 portal 实现不同用户的自助服务。

数据库账户

第三块,或者说非常小众的一块账户是 DB 的账户,这里面主要涉及 MySQL 以及 NoSQL(Redis/ElasticSearch) 的账户划分管理审计。

MySQL 的账户管理我们通过 saltstack 的 mysql_user 进行管理,我们用的saltstack 是 2015.8.7,对 MySQL 5.7 的支持有不少 bug,2016 开头的版本对此都修复了,嫌麻烦的不妨直接把 salt 给升级了或者做 diff 给 2015.8.7 打 patch,主要原因是 5.7 之前存储用户表的密码字段是 password 而 5.7 之后变成了 authentication_string。

目前所有的 MySQL 账户用户通过 git 管理而密码通过 salt pillar 管理,审计则使用了 Percona 的 Audit Log Plugin 通过 rsyslog 打到中心服务器收集分析。

对于 Redis/ES 来说,由于上面存储的数据为非敏感数据,所有的都通过 IP 层面的 ACL 进行控制访问。

jaseywang's avatar

一张复杂网络的生长过程

虽然不是网络出身,但是对于我们全网架构的「生长」过程还是比较了解的,分几个重要的时间点讲讲里面有意思的事情。

15 年下旬

公司业务还没起飞,全网架构简单,应付一天 1k+ 的订单了(有效挂号单)绰绰有余,机房跟第三方的卡管机构通过一根 10M 的 SDH 专线打通,所有到院方的请求先经过第三方的卡管机构,由其转发再进行业务层面的处理,实际的带宽峰值不到 500kbps。

16 年初

正式开始批量接入三甲医院,同时跟医院数据打通的方式也由原先的经过第三方卡管机构的非直连方式变成了直连方式,所有接入的院区通过两根双活的专线跟我们机房打通,两根专线的意义很重大,一旦一根专线被挖掘机挖断,可以在 1s 之内切换到另外一根,上层业务通过重试机制可以做到几乎无感知。新闻上报道的某某地区由于光纤被挖掘机挖断造成网络中断的问题在后来专线数量增加的情况下时有发生,但是到目前为止对于这种双主的架构并无影响,考虑到成本等因素,暂时不会做 N + 2。

为了控制 IP 地址的分配以及部分安全性的考虑,跟院方接入全部采用双向 nat 的方式,双向 nat 的问题之前的博客有过介绍,只不过之前是通过 iptables 实现,本次通过专业的路由设备实现。经过双向的 IP 地址转后,我们对院方的地址全部隐藏,另外由于院内自助机的 IP 全部由我们自行分配,通过双向 nat 之后,我们有更大的自主权。下面这张图演示了 IP 地址转换的过程:

与此同时,为了快速实现新接入医院的稳定上线,保守采用了双防火墙双路由器的策略,通过 BGP/BFD 协议实现单条线路失败的快速收敛,总成本控制在 20K 左右:

医院的数据通过一根 2M 的专线,一根 10M 的专线汇聚到我们的核心机房,核心机房通过主流的三层结构互联。

对于办公网来说,为了实现访问 google 等国际联路,我们通过在办公网的路由器上设立 GRE 隧道的方式连接到国内的专线服务商,再由该服务商进行国际流量的转发,由于跨了国内的公网环境,会出现偶尔的延时增高以及中断的问题。

16 年中

随着接入院区的爆发式增长,成本成了很大的一个问题,原先所有院区接入同一个核心机房的方式不仅成本无法得到控制,单点也成为了一个迫在眉睫的问题。原先三层的简单架构也无法满足日后的水平横向扩展。为了解决上面的问题,花了大量时间跟北京具有运营商资质的公司进行了大量的商务沟通,实现了专线、带宽资源的大幅度下降。在保证稳定性的前提下,2M 的专线做到了平均 450RMB/m,10M 的 1500RMB/m,50M 的 4000RMB/m,100M 的 6000RMB,同时引入了阶梯价格的策略,量大会更加具有竞争力,当然三大运营商的价格会比鹏博士、天地祥云的高些,毕竟稳定性也高的多(这个后来验证确实如此)。最终跟北京三大运营商以及几家准运营商达成了友好的合作,实现了双方的共赢。上面的问题谈妥了,接下来自然引入了下面几个核心思想:
1. 链路类型,优先选用 SDH/MSTP 协议的,G.703 次之。
2. 运营商,成本跟稳定性下的权衡,优先选用电信/电信通,联通/天地祥云次之。
3. 专线接口类型,优先选用双绞线电口,光纤单模次之,方便排查问题,同时默认接入千兆全双工,百兆全双工作为备选。
4. 建立中立的中转机房,所有院区的专线先通过中转机房再进入核心机房,实现了跨区域专线的成本以及稳定性的大幅提升。
5. 在上条的基础上,全网引入骨干网 msr(metropolitan small router)、核心网 csr(core small router)、汇聚网 csw (core switch),专线 dll(dedicated leased line),边界路由器 edr(edge router),边界交换机 esw(edge switch)。优先选择中低端廉价设备,比如 msr 使用 Cisco2921/K9,csw 使用 WS-C3750X-24T-F,esw 使用 WS-2960X-24TS-L 等型号,通过 HSRP/BFD 等协议实现分而治之。

着重说下最后的两点,对于第 4 点,其架构图如下:

可以看到,这一阶段引入了 msr/csw 等角色,将运营商的专线接入诸如鹏博士、天地祥云等中立机房的进行中转,最终汇聚到核心的机房。每一台中转机房的 msr 承载 20 条左右的专线,通过上方的 csw 进行线性扩展,目前每条专线的带宽在 2~10M 左右,所以不会给机器造成很大的压力。

对于第 5 点,在核心骨干汇聚分成之后,一方面是实现了多网分离,即生产网、测试网、OOB 网,办公网以及到院方的专线网的分离,极大的方便了 tcp/ip 层面的访问控制,在这个阶段,所有对人的访问控制(人访问线上线下)全部通过在网络设备上加白名单的方式实现,算是实现了基础的访问控制,虽然比较繁琐。另外一方面全网由之前的树根结构扩展到了树根枝干末梢叶子的网状结构,扩展性以及冗余性都有了很大的提高,下面这张图是相对比较成型的网络拓扑:

这个阶段,除了院内、机房的大幅度改造之外,办公网尤其是国际网络也做了优化,从原先的 GRE 隧道方式一步到位成了专线直连方式,办公网通过专线直接到服务商节点,节点直接到第一线的香港节点,同时,为了避免所有流量都走国际网络,我们在内网通过 DNS view 的方式,将主要被墙的域名解析丢给 Google DNS,其余则解析丢给国内 DNS,DNS 层面解析结束之后,TCP/HTTP 的流量则通过在路由层面通过不同的 ACL 规则实现国内国际流量的划分。下图是日常访问 Google 的 ICMP 延时:

16 年底

为了进一步控制成本,响应国产化的号召,原先一家院区 4 台设备(2 台防火墙、2 台路由器)的架构,在保证稳定性不变的情况下,优化成了由 2 台带路由功能的防火墙(USG 6306)组成的专线网络:

在同样秒级切换的情况下,成本降低到了原先的一半不到。同时,考虑到后续接入非三甲院区的特殊性,目前正在调研舍弃大型物理硬件,通过 4G 网卡配套 VPN 的方式来进行网络层面的直连。

在优化成本以及性能的基础上,还进行了下面几点的优化:
1. 通过 PowerDNS 的 API 实现了上千条内网的 DNS 正反向解析,这对于使用 mtr 细颗粒度的排查定位问题提供了很大的帮助
2. QoS 的优化,优先走生产上的业务流量,其次生产上的非核心(日志、监控等),在 msr 上通过 CoPP 实现
3. 全网设备的配置备份问题,所有设备的配置每日自动备份到内网的 gitlab 上
4. 为了实现生产网访问被墙的资源,在办公网搞了两套 tinyproxy,生产的机器通过 http_proxy,访问前端的 Haproxy 实现了网络资源的自由访问
5. 办公网无线的 LDAP 认证,这块在全网统一账户实践有所涉及

从 15 年底到 16 年底的一年多时间,通过不断的演变,一个相对比较完善有很强冗余性的大网基本建设完成,该架构支撑目前每天的 100k 订单(有没有发现一年期间增长了多少倍),40k 的有效挂号量没有任何瓶颈。

在建设初期遇到不少跟院方通信的网络中断问题,起初做贼心虚以为是自身的问题,后来通过监控不断的完善,实现了分钟级别的问题发现与诊断。

下面列一些有趣的问题:
最常见的,某院区由于专线被挖断,单条专线中断,平均一个月一次。院方机房全部或者部分掉电,导致所有专线中断;院方机房制冷设备故障,导致专线设备罢工死机。院方线下自助机网络设备故障,导致该区域自助机全部脱网,类似的事情不一而足。再比如我们的 RPC 服务访问某院区 HIS,凡是下一跳为 10.222.0.45 均能够访问终点 IP,凡是下一跳为 10.222.0.41 则无法访问同样的的终点 IP,经过抓包发现由于院方的两台核心路由故障导致。最喜剧的性的事,某院区偷懒将我们跟 HIS 直连的 IP 划分到他们的核心网络,由于 arp-proxy 的问题导致全院 HIS 宕机数个小时。当然医院 HIS 宕机其实是件很频繁的事,之前仅仅因为处在一个封闭落后并且无知的环境,很多时候重启应用重启机器问题就算是绕过去了,现在一下子暴露在了一支「正规军」面前,一下子露馅了。

Phoenix Nemo's avatar

新轮子:Planet.js

一开始的选择(需要登陆)中纠结使用 Planet 还是博客的时候就已经在关注 Planet 这个东西。

非常奇特,本身并不存储什么数据,但是用这样一个简单的方式在极低的成本下将大量的用户和内容结合到了一起。

终于决定要建立一个社区星球的时候,我才发现它连主题模板都不支持响应式才不是我想写个好看点的主题结果折腾一天无果(ノ=Д=)ノ┻━┻

总之,使用的是 Ubuntu 仓库内的 planet-venus 包,遇到了各种问题…

  • 莫名其妙无法获取 feed,浏览器访问正常,用 curl 正常
  • 莫名其妙无法读取 feed 内容,其他 feedparser 均正常
  • 对文章内使用相对链接的内容无能为力
  • 模板语言落后,不要跟我说 old fashion
  • 输出路径一直是 cwd,不知道是不是 feature。但是这导致我的 home 下面到处都是 output 目录

强迫症犯了。遂决定自己实现一个因为逻辑很简单啊不过就是把 RSS 抓下来排个序再丢出去么

但是到具体的细节,还是有不少需要考量的东西。

压缩和编码

有些网站打开了 gzip 压缩,有些网站使用了非 UTF-8 编码…只是暴力读取的结果就是页面上一半正常一半乱码。

好在 feedparser 给出了使用 zlibiconv代码样例,这个问题就迎刃而解啦。

相对路径和内容安全

Planet Venus 似乎是会解析处理文章内不带有协议的链接和图片,以使这些资源能够在 planet 的页面中直接通过原地址访问。但是问题在于,planet venus 似乎只是暴力给所有非完整 URL 形式的资源地址都强行加上协议和主机名。于是就出现了这种情况——

  • 原文地址 http://www.example.com/2017/01/21/example.html
  • 原文中引用的资源 images/1234.jpg
  • 看起来没毛病,但是 Planet Venus 解析过来就是 http://www.example.com/images/1234.jpg
  • 这似乎看起来也没毛病… 然!而!正确的地址是 http://www.example.com/2017/01/21/images/1234.jpg

…呵呵哒。

先不管为什么会有这么奇怪的资源路径,但是 Feedparser 却可以正确解析到带有跟路径的地址。也就是上面的资源地址,Feedparser 解析完之后就是正确的 /2017/01/21/images/1234.jpg

但是只有正确的相对路径还不够,因为 planet 是单独的站点,直接把 HTML 往页面中插,结果就是浏览器会去请求 planet/2017/01/21/images/1234.jpg 然而 planet 的服务器上并不会有这个资源。

于是这个问题先放着,来看另一个问题。

聚合的一个特点是,几乎无法控制来源内容的安全性——因为其他的网站服务器都不是自己维护的,其他人能否保证自己的网站不被入侵、被跨站…都是未知数。如果有订阅的网站被入侵挂了马,阅读/订阅聚合 planet 的用户也会中招。

解决这个问题,常见的办法就是过滤掉不安全的 HTML tag。于是这里引入了 sanitize-html

其实我一开始打算用 regex 直接切掉不过还是不够 robust 所以乖乖去找包了… 但是惊喜的是这个包居然可以实现按规则替换!这就顺利解决了之前的问题,可以将 Feedparser 解析得到的路径加上正确的协议和主机地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
sanitizeHtml(html, {
transformTags: {
// restore origin links
'img': function (tagName, attribs) {
if (attribs && attribs.src) {
if (attribs.src.startsWith('/') && attribs.src.charAt(1) !== '/') {
// root-relative path, add corresponding host before root
return {
tagName: tagName,
attribs: {
src: host + attribs.src
}
};
} else if (attribs.src.startsWith('//') ||
attribs.src.startsWith('http://') ||
attribs.src.startsWith('https://')) {
// absolute path, do nothing
return {
tagName: tagName,
attribs: attribs
};
} else {
// Feedparser may not correctly recognize this path, try with host/path
return {
tagName: tagName,
attribs: {
src: host + '/' + attribs.src
}
};
}
}
// don't miss the default action or it throws exception.
return {
tagName: tagName,
attribs: attribs
};
}
}
});

解决了文章内容相对路径的问题,要过滤特定的标签或者标签属性则是这个包本来就要做的事情了,小菜。

处理完文章对象之后,用 Array.prototype.sort 带一个 compare function 通过更新日期排序,接下来就可以简单渲染页面和 RSS 文件啦。

其他

一些不太常见的功能,例如 http proxy 的支持(在特殊的网络环境下可能用到),长文章仅展示 summary 并提示继续阅读,avatar 的支持,模板和输出目录保持和配置文件的相对路径,等。

然后作为深有体会——

1
2
3
CSS
IS
AWESOME

因此打死不写前端的家伙选择直接套用了 YUI Library 的 purecss 框架做一个还算看得过去的模板。至少… 几番折腾之后把响应式和 pre 等难缠的宽度搞定了顺便玩了下 media query

于是代码 -> GitHub

以及已经在使用的 Planet NyaaCat

从开坑到基本完善大概花了 15 个小时… 果然长时间不写代码手生才不是冻得手打字都变慢了呢

Phoenix Nemo's avatar

玩了一下 NGINX RealIP 模块

最近要给网站上 CDN 于是折腾了下在 NGINX 部分获取客户端真实 IP 的方案。

嘛… 意想不到的简单就是…

安装 realip 模块

如果是 Debian/Ubuntu 系统,直接安装 nginx-extras 这个包即可。包含了很多有用的模块,不需要再自己编译。

如果是其他发行版,且没有提供额外模块的包的话,需要自己编译 NGINX。编译参数加 --with-http_realip_module 即可。

获得前端服务器地址

常见的 CDN 前端 IP 都可以从 CDN 提供商处获得,例如 CloudFlare 的 IP 地址段在这里

如果需要找到 Google Cloud Platform 的 IP 地址段,可以使用 Google 提供的 TXT 记录查询。

1
$ dig @8.8.8.8 _cloud-netblocks.googleusercontent.com TXT

获得记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; <<>> DiG 9.11.0-P2 <<>> @8.8.8.8 _cloud-netblocks.googleusercontent.com TXT
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 42732
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;_cloud-netblocks.googleusercontent.com. IN TXT

;; ANSWER SECTION:
_cloud-netblocks.googleusercontent.com. 3599 IN TXT "v=spf1 include:_cloud-netblocks1.googleusercontent.com include:_cloud-netblocks2.googleusercontent.com include:_cloud-netblocks3.googleusercontent.com include:_cloud-netblocks4.googleusercontent.com include:_cloud-netblocks5.googleusercontent.com ?all"

;; Query time: 51 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Wed Feb 08 20:31:29 JST 2017
;; MSG SIZE rcvd: 331

这里面的 _cloud-netblocks1.googleusercontent.com 等地址即是用于保存 GCP IP 段的地址。继续查询:

1
dig @8.8.8.8 _cloud-netblocks1.googleusercontent.com TXT

获得记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; <<>> DiG 9.11.0-P2 <<>> @8.8.8.8 _cloud-netblocks1.googleusercontent.com TXT
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 22867
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;_cloud-netblocks1.googleusercontent.com. IN TXT

;; ANSWER SECTION:
_cloud-netblocks1.googleusercontent.com. 3599 IN TXT "v=spf1 ip4:8.34.208.0/20 ip4:8.35.192.0/21 ip4:8.35.200.0/23 ip4:108.59.80.0/20 ip4:108.170.192.0/20 ip4:108.170.208.0/21 ip4:108.170.216.0/22 ip4:108.170.220.0/23 ip4:108.170.222.0/24 ?all"

;; Query time: 55 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Wed Feb 08 20:32:39 JST 2017
;; MSG SIZE rcvd: 270

于是得到了一堆 IP 地址。

设置 RealIP 模块

文件 /etc/nginx/snippets/realip.conf,请注意这个位置在其他发行版未必存在,放在 NGINX 配置目录下即可。

1
2
3
4
set_real_ip_from  192.168.1.0/24;
set_real_ip_from 192.168.2.1;
set_real_ip_from 2001:0db8::/32;
real_ip_header X-Forwarded-For;

然后在 vhost 配置文件中引用这个配置。

1
include snippets/realip.conf;

搞定,重启 NGINX 即可获得客户端真实 IP。

Note:

GCP 的 X-Forwarded-For 的客户端 IP 在第一个 , 的前面,所以一般需要 split(',')[0]

jaseywang's avatar

树莓派(raspberrypi)、saltstack 在线下自助机运维上的应用

目前每家院区都分布了从数台到近百台规模不等的自助机,覆盖了北京市属 22 家医院的三十多个院区,一千多台的日常变更、升级管理、甚至常人看来很简单的开关机成了摆在眼前的一大问题。下面这篇博客会抛出 4 个问题并且分享下我们线上的实战经验。

1. 开关机,一个看似很幼稚的问题
你去市面上问一个的有点经验的运维,「服务器怎么开关机」,他可能第一反应是,「服务器要关机吗?」如果你接着问,「如果现在就有这么一个需要,需要先关机过段时间再开机,怎么办?」,他很可能会告诉你,「远程卡啊,这不是标配吗,再不行,电话个驻场的帮忙开下呗」。 以上的情况对于机房的服务器是适用的,但是对于自助机,放在院内各个大厅角落,物理环境恶劣,随时断网断电,尘埃飞扬,更别谈恒温空调 UPS 之类的,要实现开关机,真不是一件容易的事情。我们在院内用的自助机,说白了就是一台组装的 PC,仅仅是通过串口并口连接了一些读卡器扫码器等外置设备。既然是消费级别的 PC,就不会有远程卡这么「高端」设备,当然,对于 PC 级别来说,市面上是有跟企业级别 iDRAC/iLO/iMana 类似技术的设备的:Intel 的 AMT,最终的实现效果跟远程卡几乎一样,另外一种能实现开机的方式就是通过 Wake-on-LAN 这种古老的协议。

有开就有关,AMT 能开能关,Wake-on-LAN 就没办法了。但是基本思想很简单,需要在每台机器上安装一个 agent,通过此 agent 来下发关机以及后续的变更操作。

有了上面的思路,我们来对比下 AMT 以及 Wake-on-LAN 这两种方案的优劣,如下图:

对于目前的业务来说,我们最看重的是经济成本以及可扩展性,很明显,Wake-on-LAN 完爆 AMT。

有了大方向,下面是具体的实现,首先是至少有一台常年开机的机器用来唤醒其他的机器,选自助机肯定不行,这些机器本身就会出现白天开机晚上关机的状态,后来想到了树莓派,设备小,可以完全放进一台自助机里面,只要该自助机的电源线不拔,就能保证树莓派的常年开机状态;管理简单,默认使用的 Raspbian 系统,源的同步,账户的管理,监控、变更等等跟我们线上完全打通;经济成本低,一台树莓派配上 SD 卡电源线插座传感器(这个后面会单独提)不到 400。有点需要注意的是,很多情况下院内会有多个 vlan,正常情况下 Wake-on-LAN 不支持跨网段的唤醒,一般来说,放在不同位置的自助机网段会不大一样,加上我们需要监控不同位置的温湿度烟雾的状态,所以最终决定出单家院区 4 台树莓派的标准配置。下图是一套树莓派的配置图:

搞明白了上面的核心问题,接下来要解决的是流程类的问题,每上一个新院区,我们会直接格式化 SD 卡灌装定制的 raspbian 系统,salt 初始化,zabbix/freeipa 注册,实施人员到现场之后插电接网即可。

2. 如何优雅的开关机
上面的话题仅仅覆盖了基本的原理,在我们上线的早期,每个院区仅仅上线了个位数的机器,同时运行的院区在三四个左右。这里需要提到一个背景,每个院区的部分自助机,同一个院区的不同自助机,开关机时间都不同,比如友谊位于门诊大厅的自助机是每天早上 7:00 准时开机对外使用,但是二三楼的自助机则是每天早上 07:10 启用,还有一台位于急诊大厅的机器则是全年 24 小时开机,而对于同仁医院来说,所有的机器都是早上 06:45 开机。

早期,我们通过 saltstack/jenkins/git 在树莓派上维护对应的院区的 crontab 列表来实现自助机的开关机。

另外一个频繁出现的问题是,院方会经常性的提出修改自助机开关机服务时间的需求,每次运营转达给我们再经过 git/jenkins 提交的效率异常的低效。为了彻底的将这部分重复低效的工作从运维手里解放出来,我们为院方开发了一套开放平台(open),除了日常的开发者文档更新、开发接口 API 调用,接口测试用例等之外,一块重要的功能就是为院方开放了自助机服务器时间的自主修改查看权限,这样将日常繁杂的自助机时间管理下移到了院方手里。

目前该平台支持全局、个别自助机的开关机时间:

bangkok 上面的 KioskScheduler 进程定期会向 open 平台同步各个院区所有自助机的开关机时间,将其写入到 MySQL,底层通过 APScheduler 这个任务框架来调度,使用 BackgroundScheduler 调度器,默认的 MemoryJobStore 作业存储。每家院区的规则都分为全局跟单独两种,全局规则针对该院区的所有自助机有效,优先级低;单台自助机规则仅针对单台自助机规则生效,优先级高。KioskScheduler 根据以上优先级的不同,优先执行高优先级的规则。这样「积水潭 2017 年春节期间,除了 10、11 号自助机在每天的 08:00 ~ 16:00 开机之外,其余所有的自助机均关机」的规则就能顺利实现了。

同时,为了确保 KioskScheduler 运行正常,应用层面通过 monit 实现进程的监控,业务层面的规则执行与否以及是否达到预计,我们通过 python-nmap 实现了一个批量扫描的脚本,每次开关机时间点触发后的 5min/10min/15min 三个阶段,对命中规则的自助机进行批量的存活性扫描,对于未达到期望的自助机会触发报警到我们的自助机运维同事那边进行人工处理。

对于开机的方式,部署在院内的 4 台树莓派通过 syndic 机制收到请求后同时向目标机器发起唤醒的请求,根据之前的经验,单台树莓派的唤醒偶尔会出现唤醒失败的情况,另外一点 4 台里面只有保证至少有 1 台存活,那么开机就能成功触发。

3. 如何低成本实现 OOB 的核心功能?
先来看下行业标杆的 Dell 远程卡(OOB)提供的一些核心功能,如下图:

根据我们日常使用频率的统计,80% 以上的操作花在了如下的操作上:

1. 服务器的开关机,尤其是机器 hang 住了/kernel panic 之后的开关机
2. 由于各种原因无法 ssh 之后需要通过虚拟终端进入查看当前机器屏幕的状态
3. 机器主要零部件的硬件状态,健康状况

对于一台自助机来说,其核心是简化后的服务器,没有 raid 等企业功能,除此之外,提供的服务跟服务器并无二异,仅仅是加上了 GUI 而已。对于第一、二两点,上面的话题已经阐述过,比较遗憾的是目前并不能对机器的死机、蓝屏实现脱离系统的开关机重启以及实时屏幕的查看,但是对于日常的开关机,VNC 远程操控,绰绰有余;对于第三点,目前诸如磁盘等底层硬件的监控是放在应用层来监控,对于我们来说,个别自助机硬件层面的宕机并不会影响整个院区的使用,我们及时报修返厂更换即可解决此类问题,众所周知的是,对于绝大多数的医院,其院内的物理环境,比如尘埃浓度(PM10)、温湿度等跟正规的机房比相距甚远,早期,我们发现某院区的部分的自助机经常性的宕机蓝屏,经过若干天系统级别应用级别的排查,都无所发现,后来现场的志愿者提醒我们是不是温度太高导致的,现场勘查后发现,西区的部分自助机直接放在了仅有一顶透明帐篷遮蔽的半室外空间内,6 月市内温度 30 度的情况下,这部分自助机箱体的温度已经接近 40 度了,蓝屏在所难免了。

为了及时发现这类问题,我们后续在所有树莓派上全部引进了温度、湿度以及烟雾的传感器,成本非常低,一个 DHT11 的温湿度传感器加上一个 MQ-2 的烟雾传感器成本在 20RMB 以内,加上现成的 Adafruit_DHT/RPi.GPIO 的库直接调用,完美解决此类问题。通过分散在不同地点的四台树莓派,我们能够推断出当前院内的物理环境,这对改进目前自助机的物理位置有非常重要的意义。

下图是某院区蓝屏故障增大期间周边温度的变化,观察可以发现具备一致性关系:

类似的,我们在院方机房放置的连接专线的路由设备,同样发现多次类似的问题,下图是某院区机房空调设备故障温度升高导致设备丢包增大中断服务的监控:

很多看似简单的问题,一旦场景转变了,需要应对的措施也就随之改变了。

4. 如何有效实现自助机的变更操作
自助机上的变更,主要分为两类,一种是最为频繁的版本升级,包括自助机上挂号缴费程序的版本、第三方依赖的程序升级,这类变更的特点是,每次的程序升级,其升级包都相对较大,小的几兆,大的有上百兆;第二种变更更多的集中在日常的系统变更,比如修改 hosts 以对应不同的 nat 影射、增删改查注册表等系,对于这类变更,最大的特点是占用的带宽小,但是操作频繁。

针对这两种变更,分别设计了两种不完全一样的变更方式,但是底层都是通过 saltstack master/syndic/minion 的架构实现文件的传输。中心端一台 master(存在单点的问题),每家院区 4 台部署了 syndic 的树莓派,同时跑 master/syndic,向中心端注册,底层的自助机部署有 minion 向其中一台 syndic 注册,目前 minion 到 syndic 同样存在单点问题,后续考虑将 syndic 升级成 MultiSyndic。

对于第一种变更方式,我们将要升级的版本库文件打包上传至 git,通过 jenkins 将数据从打包机 git pull 下来之后,在打包机通过 salt 将升级的文件分发到 salt syndic 上,syndic 上会起 http 服务,自助机每次开机的时候会自动的向 http 服务检查是否有最新的版本,如果有的话则会升级,对于回滚,我们将版本号递增内容回滚至上一版本重启机器即可。
对于第二种变更方式,如果是修改注册表之类的变更,我们使用 cmd.run 直接将需要跑的 key 执行结束,类似下面这样:
cmd.run 'reg add "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel" /v "{5399-XXX-CBA0}" /t reg_dword /d 1  /f'

如果是传输文件的变更,跟第一种方式类似,不同的是,文件落地到 syndic 之后,我们会直接通过 master/minion 的方式 push 到每台自助机,而不是主动 pull 的方式。同时为了确认文件的完整,每次从 master 到 syndic,从 syndic 到 minion 的两个关键步骤都会做一次 md5 校验,最终实现的效果可以参考早期的 saltpad 版本,下图是 bangkok 中变更的页面展示:

除了最基本的单次变更外,增加了多次执行的执行步骤以及执行步骤的模版功能,前者对于多合一的操作步骤很适用,后者对于经常使用的诸如修改 hosts 文件等很适用。

在这之前,我们尝试直接通过 salt master/minion 的方式进行日常的变更管理,这种方式对于线下的实施同事来说非常的不友好,其灵活性以及方便性远低于目前的方案,遂未在团队内推广使用。上面介绍的系统是目前在线的稳定系统,使用了一段时间整体反馈还不错,后续会优化版本控制以及更加自动的回滚等操作。

alswl's avatar

👁️ 预测未来?

拉普拉斯之妖

未来是可以被预测的么?

专家在预测股票趋势变化,天气预报员可以预测未来一周甚至更长时间的天气。 如果给他们更多的信息和参数,是否可以将未来预测的更准确? 如果精确的粒度可以达到基本粒子级别,同时给一个计算力超群的计算器,能否精确的推衍未来变化?

这些想法在我刚接触经典力学时浮现,学习了牛顿三定律之后,异常激动。 感觉人类可以凭借技术的进步,逐步对未来精确预测。彼时可以解决人类即将遇到的任何问题了,化问题于无形。

这想法其实在 200 年前就出现了。法国伟大的数学家拉普拉斯(Laplace)在他的著作「概率论」里面, 提出了这样的观点:

我们可以把宇宙现在的状态视为其过去的果以及未来的因。假若一位智者会知道在某一时刻所有促使自然运动的力和所有组构自然的物体的位置,假若他也能够对这些数据进行分析,则在宇宙里,从最大的物体到最小的粒子,它们的运动都包含在一条简单公式里。对于这位智者来说,没有任何事物会是含糊的,并且未来只会像过去般出现在他眼前。

「根据当前已知即可预测未来」,这种决定论,就是拉普拉斯之妖。

打破

除了拉普拉斯的决定论,这个世纪末还发生了另外一件事情。 开尔文男爵在 19 世纪的最后一天,发表了「物理大厦已经落成,所剩只是一些装饰性工作」的言论。 然而,在随后的岁月里,经典物理大厦却开始被量子力学逐步击破,轰然倒地。

在决定论这个方面,根据海森堡的不确定性理论,粒子的位置与动量不可同时被确定。 这个结论说明目前技术下面,无法精准测量粒子的状态。 既然无法准确测量粒子的状态,就失去了演算未来可能性的基础,更无法预测未来了。

那既然是无法预测,是否表示,预测未来这件事情是无稽之谈,未来是不成规律的?

混沌:被理解错误的「蝴蝶效应」

针对未来的预测,动力系统中,有相当多的研究和思考,其中比较注明的一项是:蝴蝶效应。

关于「蝴蝶效应」,不用过多解释。命名的来源据说是发现者 Edward Lorenz 觉得图形像是蝴蝶。 而有另一种另一说法是,Edward 的一个比喻「巴西蝴蝶煽动,引起德克萨斯的龙卷风」。

事实上这种说法是不精准的。Edward 在预测天气模型中,发现起始数据的微小差异, 会导致数日之后计算结果的巨大差异。 从点在于在预测模型上,忽略了一个蝴蝶煽动引起的力量,即起始条件的设定,会导致结果的巨大差异。 而不是表名龙卷风是蝴蝶煽动产生的。

如果蝴蝶煽动能够产生龙卷风。那么一个普通人的呼吸,也可以造成同样的效果。😂

分形和混沌

和混沌相关的,还有一个重要概念「分形」:

一个粗糙或零碎的几何形状,可以分成数个部分,且每一部分都(至少近似地)是整体缩小后的形状”[2],即具有自相似的性质。分形的核心是自相似。

随机从这种自相似的图形中提取样本,样本集表现出不可预测的特性。

混沌的定义:

非线性系统在一定参数条件下展现分岔(bifurcation)、周期运动与非周期运动相互纠缠,以至于通向某种非周期有序运动。这种运动是不可预测,呈现出失序的状态。三体问题,即是一例混沌的场景。

为什么混沌和分形是具有相关性的呢?这里有一个重要概念 IFS。 IFS(Iterated Function System)是迭代函数系统,即在数学上被认为一个完全向量空间上收缩映射的有限集。 分形的图案,部分可以抽象出 IFS 公式。

这个公式的奇妙之处是在于,在公式渐进推算过程之中,初始看到的结果是混沌无序的,而在逐步运算之后,可以看出分形的特征。

下图是根据一个 IFS 公式,逐步构造出一个分形图片的过程:

图片来自 Wikipedia https://en.wikipedia.org/wiki/Iterated_function_system

这种由初期混沌,逐渐表现为分形的情况,被学者总结为:「混沌在(生成)时间上是分形的;而分形的在空间(展示)上是混沌的。」 混沌中可以找到有序,在非线性中找到理性,混沌中可以演化出非规则,也可以演化出在规则中混沌。

哲学上的象征

感谢你迷迷糊糊的读到这里,混沌不仅仅在数学和物理上面,吸引着一代代人的探索。 在哲学生活指导方面,也有重大的意义。早在古人说「分久必合,合久必分」, 就是一种稳定线性的表现,具有预测性的表现。 而「一生二、二生三,三生万物」则体现了古人对分形的理解。 经典的「马蹄毁了一场战争」故事,讲的是混沌理论。

这些对未来预测能力的渴望,表现了对生活的掌控欲望,对未来可行性的探索, 在低谷期可以带来希望,大跃进时期可以带来警醒。


对于我个人而言,虽然失去了能够预测未来的理论基础而略感遗憾。 但是反过来想想,如果真的发明了一台机器,能够精确预言到未来, 那么在发明实现的当天,就能够推衍出未来所有出现的科学理论、文学、艺术创作, 那给后人创造了一个多么再无新事物的未来,这该是一个多么无趣的世界啊。


原文链接: https://blog.alswl.com/2017/01/prophecy/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
jaseywang's avatar

通过 noVNC 实现数千台自助机的实时可视化

背景很简单,目前我们运营维护着北京市属三甲医院数千台的自助机,这里面包含挂号取号机、检查报告机以及其他若干衍生出来的自助机种类,数量已经超过一千多台,不同的院区通过密密麻麻的专线跟我们的机房源源不断的进行着各种协议的数据传输与交互。

由于各方面技术以及非技术层面的限制,只能使用微软的系统,为了管理这部分的机器,一方面我们通过 saltstack 来进行日常的产品升级以及变更,对于桌面的可视,比如我们需要知道当前这台自助机前患者的操作,就需要 VNC 来帮助了。

一年前自助机刚上线时候,没有很详细的考虑过自助机的大规模运维问题,当时每台自助机都在 Clonezilla 打包阶段打包进了 realVNC,通过一台 Win 跳板机的 realVNC 客户端去连接,realVNC 有书签的功能,所以当时需要查看每台自助机界面的运行情况相对比较方便,随着数量的增加,这种先登录一台 win 的跳板机,在使用 realVNC 书签查看的传统 CS 架构就捉襟见肘了。迁移到 web 上势在必行,本着可小动不大动的原则,调研了 realVNC 的各种 SDK,遗憾的是都不支持,加上 realVNC
有版权问题,放弃。相比之下,TightVNC 则不存在这类问题。

后来我们组的同学调研了几个基于 web 的 VNC 客户端,peer-vncnoVNCguacamole ,peer-vnc 活跃量太低,底层用的是 noVNC,guacamole 还处在孵化期(功能相当强大)并且比较重,放弃。最终选择在 noVNC 的基础上做一些工作。

由于 TightVNC 默认不支持 WebSockets, noVNC 提供了 websockify 这个工具来做 TCP socket 的代理,接受 WebSockets 的握手,转化解析成 TCP socket 流量,然后在 CS 两端传递。由于我们涉及到较多的机器信息需要维护管理,我们将所有的机器信息,比如自助机的编码,机器名,IP/MAC 相关的信息预先存储到 MySQL 里面,接着按照 token: host:port 既定格式生成每家院区的连接信息配置文件,这一步可以在 jenkins/rundeck 上建个 job 方便每次增删改查。接着就可以启动了转发了。

在浏览器中打开本地的 6080 端口即可。下图是集成到我们内部管理平台上的截图,可以快速定位到自助机的健康状况并通过 VNC 进入确诊当前的状态:

noVNC 默认情况下会以交互式的方式连接,在这个过程中会做身份权限校验(账号连接、读写控制),是否是 true color 等,这个对于生产不是很适用,我们后来将授权这块做在 Django 上,结合 LDAP 做登录认证。考虑到专线带宽的限制,默认关闭了 true color 开启了压缩。VNC 对带宽的消耗还是比较厉害的,平均下来,每开一个新链接,会消耗 1Mbps 左右的带宽,所以如果需要做实时的展示大屏,需要考虑这块的瓶颈。

下图是 noVNC 实时图像,由此我们可以看到当前这台自助机的实时状态并对问题的自助机采取一定的操作:

最后将其挂在 haproxy/Nginx 后面,Nginx 1.3 之后支持 websocket,一个快速搭建并可投入使用的自助机可视化平台就算完成了。该平台我们内部命名为 bangkok。

以上思想可以移植到 *nix 平台上,目前不少主流的云 OpenStack/OpenNebula/Digital Ocean 也集成进了 noVNC。

alswl's avatar

👷如何做年前大扫除

今年过年特别早,离春节只剩下二十多天了。 为期 7 天的春节里,工程师们不上班,那万一线上业务出现了故障怎么办? 大公司的朋友们会安排专门的人进行值班(此处心疼一下那些需要大年三十还要值班保证高峰的工程师们), 而作为创业团队人少,难做到在线值守,就需要对线上进行一些整理盘点,找出潜在问题,为春节长假做一些准备。

我们称之为年前大扫除。

大扫除需要做些什么呢,且听我一一道来。

PS: 冷知识,大扫除英文是 spring cleaning,所以春节大扫除是 Spring Festival spring cleaning。

大扫除的内容

大扫除其实是一个查漏补缺+囤积粮草的事情。

查漏补缺,即找出潜在的问题。这些问题平时可能不会特意去查看, 借助大扫除这个运动,恰好进行盘点。 计算机的世界里,有一个方法论非常好使,在极多场景可以见到其身影:分层。 TCP 的七层模型,架构设计的 N 层 模型,都是对分层思想的使用。 查漏补缺也不例外,我们可以按照业务访问流程,将需要排查的问题拆分为:业务、应用、中间件、网络、物理、存储 etc。

通过分层,不仅仅完成了自上而下地遍历整个技术栈,也同时将不同模块的内容交给不同的责任方, 确保任务的分割。

分完模块,还要告知大家如何具体查找问题。 这里我介绍一个通用的方法:USE1

For every resource, check Utilization, Saturation, and Errors.

USE 方法是从 Brend Gregg 那里学来的。 在技术设施的领域里,Resource 即是指各种类型的资源,比如 CPU、磁盘、网络、内存, Utilization 指的是使用率,可以简单分为百分制和非百分制。 Saturation 是指饱和率,支持 queue 的资源,就会有这个指标。 Error 即错误,可以从错误统计和日志得知。2

业务领域里面,USE 也有相对应的含义。以审核系统举例, 对应的 USE 可以理解为「审核应用实例跑的 CPU 占用如何,任务队列是否塞满,业务日志是否有异常」。

除了 USE 里面提到的指标,还有几个指标特别重要: TPS 、Latency 和 Capacity。 这几个指标对性能敏感的尤为重要。 检查 USE 的同时,我们必须关注一下这三个指标, 确保 TPS / Latency 是否满足我们预期的 SLA。 哦?压根没有制定 SLA,不要慌,和历史数据对比,先制定一个粗糙的 SLA。 哦?连历史数据都没有?那只能找你 Leader 让他考量一下了。

负责每个子系统的同学,记得检查时候将这些收集到的数据列下来。 在 Metric 做的还不够完善时候,这些数据也是很宝贵的。

在我看来,检查 USE / TPS / Latency ,最大的作用是将抽象的可用性指标描述为几个易于理解的数值进行量化。 一旦能够量化,就可以对比、观测、监控,并且 Review 起来也异常轻松

应对方案

检查出问题之后,就要考虑应对了。时间急任务多,我们的应对方案是是囤积粮草 / 写救命笔记。

囤积粮草比较好理解,基于已有的容量预估,为容易出问题的系统提供一份冗余。 有些团队平时做基础设施就比较,做 Scale 就是小轻松。 那平时 Scalable 做的不好的朋友,就只能将应用实例多开一些,以避免临时出现的流量波动。

无状态的服务好搞,有状态的 DB 就很难在短时间内做 Scale。 检查这些服务的容量,如果重点资源临近阈值,比如 DB 的硬盘资源,缓存的内存容量。 核心服务的余量在检查中真的发现问题的话,那也只能短期内做扩容了。

对于小团队来说,春节长假的特殊性在于响应会变慢甚至是联系不上。 一旦线上有异常,可能找不到合适的人员来进行处理。 所以第二条写救命笔记则更为重要。 「Google SRE」里面有个小段子,一个绝对不能被按的按钮, 这个按钮会清空内存数据,在飞行过程中被宇航员按了。幸亏美女工程师(下图)写了相关的救命手册, 专门写了针对这种情况的操作,救了这些宇航员的命。

图片来自 「Google SRE」

从这个故事里面可以看到,一个紧急操作手册是多么重要。 所以在大扫除期间,我们还要补一补平时的文档,将一些常见问题 / 常规操作记录下来。 步骤需要细致到能让让每个远程值班的同学做到 step by step 操作。


啰嗦了这么多,相信大家对大扫除要做些什么已经有所印象了,祝大家过个好年,流量涨涨涨,还能平平安安的。


原文链接: https://blog.alswl.com/2017/01/spring-cleaning/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
Li Fanxi's avatar

2017新年好

2016年,我做了这些事:

– 写了8篇博客

博客空间总访问量61198 PageView(Google Analytics数据),比前一年稍有下降,但是实际的访问量应该还没有这么多,因为发现Google Analytics结果中出现了相当数量的Spam数据,暂时还没研究怎么能去过滤掉。跟去年一样,首页、在Linux下使用“360随身WiFi 2”calibre常见问题为Raspberry Pi 2编译内核模块这几个页面的PV占去了总PV的50%多。饭否发消息85条,包括照片17张。

– 自由软件相关

接手了一个网站:Linux Kernel Patch Statistics。这个网站的内容是按人、公司、国家等维度的指标去统计各Linux内核版本中Patch的数量。我很偶然地看到有人在LKML中吐槽说这个网站的域名过期了。这个网站的作者是我以前的同事,于是我联系他提醒他,没想到他表示说不打算继续维护这个网站了。我觉得就这么放弃一个在社区有一定影响力的网站有点可惜,所以在征得他同意的前提下,把这个网站接了过来,并且还请朋友帮忙把过期的域名给抢注了回来。

网站恢复运行的不到一个月时间中,我已经收到各种数据订正、添加功能和Bug报告的邮件,看来这个网站的的价值比我想象的还大一些。不过这个网站的后台代码确实是经久失修,所以目前数据统计的精准度非常糟糕(因为根据邮件地址把数据按公司、国别来归类,这里面的映射关系绝大部分是需要人肉来维护的,一旦没有及时维护,归类为Unknown的数据就会越来越多,也就失去了统计的意义),而且每天一次全量数据产出过程需要占用大量的CPU、IO和内存资源。所以后面需要优先先维护一下基础数据,保证统计数据质量,然后再考虑下整体的重构问题。

calibre贡献了一点点代码,改写了一下从Amazon获取书籍元信息的插件,使之可以支持中国亚马逊网站。给HBaseFlink的代码/文档各修正了一处Typo,其实只是为了实践一下给这两个项目Contribute的流程,不过后来由于工作内容的变化,没有再深入关注过这两个项目。给C++异步框架Seastar修正了一处Bug。train-graph合并了一些其它人贡献的代码和数据,发布了一个3.0版本。

– 几个IT产品

群晖DS916+ NAS:淘汰了原来用的DS214play,主要是出于盘位和性能的考虑。不过新的机器的性能依然很让人捉鸡。不过出于对DSM系统版权的尊重,我还是没有选择自己买机器组黑群晖的方案。我的群晖系统的评价依然是:轻度使用很不错,重度使用时细节缺失很多、问题也很多。但是市面上已经找不出更好的了。

Macbook Air:公司提供的工作电脑,我的第二台苹果设备(N年前得到过一个iPod Nano)。这样的电脑用来做开发机实在是性能捉鸡,尤其是为了编译Linux程序再启一个Docker的情况下。公认的优点就不说了,缺点就是有些Windows能做的事情它还是不能做,而有些Linux能做的事情它也不能做。对于我这种已经被Linux虐了十年的人来说,不能做Windows能做的事是很容易接受的,但现在不能做的事的又变多了,所以还有点不爽,于是有了下面的Dell 7040m。

Dell 7040m微型台式机:为了更有效的开发Linux C++程序,买了这个微型台式机当工作机。配置成i7 6700T的CPU,16G内存,SM951的SSD,装Arch Linux。实际用下来整体能满意,但是就编译大型C++程序来说,单核性能仍然不是非常出色。另有同事买了相似配置的Intel的Skull Canoyo,也是差不多的体验。我也知道我的应用场景下应该买个标准台式机才能配置更好性能的CPU,但是谁让我这个机器的外型的毒呢?

华硕AC68U路由器:其实去年买的AC66U完全够用了,不过还是因为一次特价剁手了更高端一点的AC68U。整体使用体验与AC66U相仿。不过从外观来说,我反而还更喜欢AC66U一点,AC66U给人的感觉是做工精致、用料实足。AC68U其实也一样,但给我的感觉却是:傻大笨粗。

Raspberry Pi 3:没啥说的,我是树莓派的脑残粉,出一个买一个。相对2来说,主要就是64位、内置蓝牙和Wi-Fi,性能稍有提高,别的就没有了。树莓派是吃灰神器是名不虚传的,这个现在已经吃灰。还买了一个Sense HAT传感器模块,做了一个贪吃蛇游戏后也吃灰了。有了3以后,我用以前吃灰的2和Camera Module做了一个网络摄像头,配合群晖做监控,效果勉强凑合。

Pebble 2:本来是在Kickstarter预定了Pebble Time 2,但因为正在用的Pebble花屏越来越严重,等不及就先收了一个Pebble 2,没想到次日Pebble就宣布被fitbit收购了(我觉得与其说是收购,不如就当是破产了更合适),Time 2也没有机会再问世了。Pebble 2的使用体验与Pebble高度一致,我很满意,只可惜这已经是绝唱了。希望在它坏掉以前,能有更出色的产品出现。

二手Kindle Paperwhite 2:跟以前用的Paperwhite比,差别并不大,只不过因为Paperwhite被老妈重度使用中,所以自己重买一个。没买3是因为性价比,毕竟我也不是重度使用。而且看书这个单一需求来说,我并不觉得Paperwhite 1/2/3/Kindle Voyage有多大的差别。

二手Intel Compute Stick:我需要一台常开的Windows机器来满足把NAS上的照片上传到 Google Photos的需求,这个东西很符合我的要求,功耗不到5W,直接由路由器USB口供电就可以了。性能对于我来说也完全够用,有了它不但解决了Google Photos上传的问题,甚至我的电脑上已经不再需要安装Windows虚拟机了,偶而需要用Windows的时候,直接远程桌面连上去用就可以了。

– 出行

南京*3、合肥、西安。对南京的感情依然不变、合肥真不是一个旅游城市、第二次去陕西省历史博物馆已找不回第一次去时惊艳的感觉。

展望2017年:

谈点工作,在用Java写了4年业务代码后,2016年,我终于又回归了技术开发。在短暂地用了一段时间Scala后,还回归到了C++开发。说是“回归”,其实还是更大的挑战,因为需要用C++ 14来写一些高性能的分布式程序,对于我来说也仍然是一个全新的课题。期待2017年可以在这个方面能有所收获。

Phoenix Nemo's avatar

重新迁移回 GCP

忍得住打 Call,忍不住折腾。看到黑科技就手痒。

虽然 AWS 用得挺安心不过越来越多的人向我抱怨博客打开速度很慢,想看文章都要等好久什么的。于是纠结了下,还是迁回了最贵但是最快的 Google Cloud Platform。这里记录一下调(zhe)教(teng)过程,毕竟 GCP 的 Cloud CDN 依然在 Alpha/Beta 阶段,可配置的选项实在太少而且很多 caveats。

前期准备

  • 域名
  • SSL 证书
  • 网站程序或内容

创建实例

我这样没什么访问量的静态网站用 f1.micro 就好啦~ 不过流量比较大的网站的话,还是建议选择高一些的配置和 SSD。

然后创建一个实例组(instance group),确保该实例组包含刚才创建的实例。

NGINX 配置

网站等会说。总之先配置好 SSL 和 Health Check。

1
2
# mkdir -p /var/www/hc
# touch /var/www/hc/index.html

默认站点配置 default.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 80;
listen [::]:80;
server_name hc; # health check
root /var/www/hc;
index index.html;
}

server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 301 https://$host$request_uri;
}

网站配置 example.com.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com;

ssl on;
ssl_certificate /etc/ssl/private/example_com.pem;
ssl_certificate_key /etc/ssl/private/example_com.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4";
keepalive_timeout 70;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_dhparam /etc/ssl/certs/dhparam.pem;

add_header Strict-Transport-Security max-age=63072000;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;

root /var/www/example.com;
location / {
# ...
}
}

(SSL 相关的很多配置步骤省略,请自行查阅其他文档)

创建负载均衡器

戳到 Networking 标签,选择 Load balancing 然后创建一个 HTTP(S) Load Balancing。首先为负载均衡器命名,因为要分别创建 2 个负载均衡所以这里可以写例如 website-http 之类作为区分。配置页面的三个标签:

  • backend
    • 创建一个新的 backend
    • backend service 选择 http
    • instance group 选择刚才创建的实例组
    • port numbers 80
    • health check 创建新的 health check 并设置 custom header,和之前 NGINX 里的配置一致,这里是 hc
    • 勾选 Enable Cloud CDN
  • Host and path rules
    • 添加域名对应的 //* 两个路径规则。如果有特殊需求则另外添加。
  • Frontend configuration
    • Protocol 选择 HTTP
    • Create IP Address 然后 reserve 一个 IP

第二个负载均衡器用于 HTTPS,名称可以是 website-https 这样。配置:

  • backend
    • 因为配置不一样,所以要再创建一个新的 backend
    • backend service 选择 https (要点 Edit)
    • instance group 选择刚才创建的同一个实例组
    • port numbers 443
    • health check 创建新的 health check 并设置协议为 https,custom header 为网站域名(因为要和 SSL 证书相符,示例里是 example.com
    • 勾选 Enable Cloud CDN
  • Host and path rules
    • 同上
  • Frontend configration
    • Protocol 选择 HTTPS
    • IP 选择刚才 reserve 的 IP
    • 创建证书,分别上传签发的证书、CA 证书链和私钥

更新解析&收尾

将域名的 DNS 解析到刚才 reserve 的 IP 即可。

至此就是基本的(当前版本的)Google Cloud CDN 配置步骤。还有很多可以自定义和扩展、优化的空间,但是这些需要根据特定的需求变化因此不再详细记录。

以及,不要忘记在虚拟机里把网站跑起来~

最后,南酱的 live 超棒(๑•̀ㅂ•́)و✧

alswl's avatar

🔑 也谈 HTTPS - 如何内测

(图片来自 茶杯中的可爱小白鼠 壁纸 - 2560x1920-堆糖,美好生活研究所)

在上篇文章 🔒 也谈 HTTPS - HTTPDNS + HTTPS 中, 我们谈了如何基于 HTTPDNS 来部署无坚不摧的 HTTPS 通信环境, 这次我们讨论另外一个比较头疼的问题:部署。

小站点部署 HTTPS 相对成本低,改改前端代码,就可以上线了。 但作为业务有一定复杂度的大网站,就没办法这么暴力上线了。

前端在基础库中调整 Scheme 之后,仍然可能存在很多边边角角没有覆盖到。 比如 JS 里面写死了 HTTP,那在 HTTPS 下请求 HTTP XHR 的话, 浏览器会将请求拦截掉。 一旦出现这种故障,用户就无法正常使用业务,小白用户往往也不懂得自己将 https:// 换成 http:// 使用。

解决的思路是足够的内测,找一群人帮我在 HTTPS 环境下使用足够长时间。 让他们当小白鼠,提前发现问题并解决。 于是,我把目光转向了身边的一大大群小白鼠,整个办公室的同事~😄

没错,我要强制所有同事使用 HTTPS 的公司网站,从而靠他们帮我发现问题。

靠发邮件、QQ 广播呼吁大家使用 HTTPS 站点的方法,估计是不行的。 没有利益驱动,推动力是不足的,我必须想点强制的手段让他们使用 HTTPS。

有三种方法来达到这个效果:

  1. 业务系统内入口判断用户身份,是雇员的话,切换到 HTTPS
  2. Nginx 入口系统判断 IP 来源,办公室 IP 则切换到 HTTPS
  3. 改造办公室网络,访问站点时候,自动切换到 HTTPS

为了避免对线上业务系统、基础设施造成影响,我采用了第三条方案。

说干就干,直接对公司网络出口设备是 ROSvia 动起刀子。

实现的原理如下:

  • A:办公室网络的 🐁 们请求站点 http://www.duitang.com
  • B:操作 RouterOS 的防火墙,将 dst 为 www.duitang.com IP 的 TCP 请求都 dst-nat 到新的一台 Nginx 服务器 proxy.duitang.com
  • C:这台 proxy.duitang.com 做过特别定制,将所有针对 *.duitang.com 请做一次 302 请求,将 http://www.duitang.com 请求都转发到 https://www.duitang.com
  • D:Client 收到 302 请求,重新请求 https://www.duitang.com
  • E:同 B
  • F:proxy.duitang.com 将请求转发到真正的 www.duitang.com 服务器

PS:这里要小心的是,需要配置 proxy.duitang.com 的 resolver 避免 Nginx 内部请求。

流程图:

这样操作之后,在办公室网络下,所有访问公司网站的 HTTP 流量都会跳转到 HTTPS。

PS:我原始方案想使用 ROS 的 L7 防火墙 直接抓 HTTP 包,match HTTP 头数据, 再修改返回的 TCP 包。 但测试下来发现 ROS L7 Firewall 不支持写 TCP 数据。 所以我最后只能使用中间跳转的方案。

如果不是使用 ROS 的朋友也不用担心,原理和流程已经讲清楚了, 无非是使用 Cisco / Huawei 网络设备的防火墙命令实现需要的功能。


上篇文章发完之后,好几个朋友问我 IP 证书供应商的事情。我就简单说一下我了解的情况。

国内 SSL 证书供应商们会给他们兜售的产品起各种各样花里胡哨的名字, 什么超真、超强、超安、超快,国外有些企业也会搞什么 Pro / Super / Premium / Essential, 其实 SSL 证书的区分,笼统来说就三种类型:DV / OV / EV, Domain Validation / Orgnization Validation / Extented Validation。 他们区别除了字面意思,就是所有权审核流程一个比一个麻烦。

想基于 IP 直接搞所有权审核,要看对应供应商的证书是否支持。 去年年底我做了一个调查,支持 IP 证书的厂家如下:

现在 Wosign 爆了丑闻,于是支持 IP SSL 又少了一家。 只剩下 GlobalSign 了,但是 GlobalSign OV 又贵审核又麻烦, 不知道看到此文的大神们有没有更好的推荐。


参考链接:


原文链接: https://blog.alswl.com/2016/12/https-2/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
alswl's avatar

教你在上海挑房子

点开的同学别失望,这并不是教你如何快速致富的 😂 ……


上海政府在 11-28 出了房屋新政 via,对房价进行进一步调控。 其核心思想是「认房又认贷」。3 月份的政策 via 是限制购房资格。 这次 11 月份调整则是提高二套房首付比例。 双管齐下,进一步给上海房市进行降温。

在这种严苛的政策下面,如何挑选一套自己满意,家人住得安心的房子就尤为关键了。 我对房市一直比较关注,也曾有几位朋友咨询我的一些经验。之前我是将 Evernote 笔记链接贴给别人,这次我就详细讲讲,如何在上海挑房子。

第一步,是建立有价值的 Indicator,并对具体房源进行计算。 Indicator 的含义是建立一系列评价指标,比如价格、户型、位置, 再根据这些 Indicator 给房源进行打分。 如果我是一个购房者,我挺希望我的置业顾问告诉我这套房有哪些优点、缺点,综合评价得几份。 (当然实际情况下这些置业顾问都是吹得天花乱坠,尽睁眼说瞎话。) 看房的过程可能长达数月,而人的记忆会随着时间逐步失真,只对最近看到的事物有深刻印象。 使用客观的数字评价房源,则可以进行精准的评价,避免产生主观臆断。

我使用的 Indicator 和评价细项罗列如下,供参考:

  • 价格
  • 面积
  • 地理位置:内环、中环、外环
  • 交通:离地铁距离,换乘便利性;多地铁线换乘;
  • 户型:南北通透;客厅朝南;全明;户型方正;无暗室
  • 装修:毛胚;简装;普通;精装;豪装
  • 商圈:购物中心;超市;菜场
  • 学区房
  • 产权:满五唯一
  • 楼层
  • 房龄:5 / 10 / 15 / 20
  • 政策发展前景
  • 停车

第二步,就是和家人达成一致的期望,避免潜在的纠纷。 每个人对未来的住房有自己的期望,即便是一家人, 各自看中的点也不会太一样。不管平时家庭决策是什么流程,我都建议坐下来一起讨论讨论。 将 Indicator 依次列出,大家讨论各自心中的优先级。

除了优先级的讨论,还要考虑一下各自对 Indicator 划分等级的理解。 比如对房型、装修这两项,评价标准就会很模糊,这一切都需要讨论清楚。

我个人最为看中价格、位置、交通,可以放弃的有学区房、停车、户型。

第三步,划定圈子,判断趋势。 众多 Indicator 里面,价格是固定的,户型、楼层等是和具体房源相关的, 我们能够撬动的最重要因素,其实就是位置。

作为上海这个拥有 2500w 人口的超大型城市,是不可能把各个位置的房源都扫一遍的, 所以必须重点规划自己准备实地考察的区域。 那么我们就在上海地图上面划几个圈了,数据可以通过链家、中原地产等多网站收集。 安居客有一个大颗粒度的圈子,展示了社区集中点 via, 下图是我筛选条件之后形成的几个簇:

(数据爬取分析这一块,我只服一个人,他专门写了一堆爬虫捞数据然后分析, 如果对杭州的房地产投资感兴趣,可以私信我,我可以帮忙问问。)

从城市级别来看,其实不同区域的变化趋势基本是一致的,但是局部地区可能受外部因素而出现较大波动。 比如上海闸北并入静安,价格就大涨;大宁板块产生一个地王,也会让这片区域价格大幅波动; 甚至新开业一个 Mall,都会让周边价格波动。 这些外部因素,必须提前关注政府市政规划方案、房产网站、公众号、房产微博。 政策相关推荐阅读:「上海市城市总体规划(2015-2040)纲要概要」 via 规划了整体发展格调。 「上海市商业网点布局规划(2013-2020)」via 规划了 14 个市级商圈,之前只有 10 个,还有若干区级商圈。

第四步,快速行动,快速决策

还记得捡西瓜的故事么,小猴子一直想找个大西瓜,但一路看过去最后什么都没捡到。 看房不仅仅是一个体力活,而且非常讲究时效性的,必须能够快速响应,快速决策。 在上海这中刚需强需求旺盛的城市,一套评价都不错的房源,半天内就会被别人拿下。 所以前期观察一段时间之后,必须摸清楚自己的需求和承受能力,遇到合适的房源, 就速战速决。

速战速决还依赖实地看房源的效率。 从我的经验来看,一天至多可以看 10 套左右的房源。 出发前一天约好中介、房东,列一个清单,时间、地点、联系方式全部罗列下来,第二天依照清单行动。 另外为了提高速度,全程应该打车,对于上海这样的一线城市,出租车费用和房产价格相比,真的是小钱。

在看中合适的房源之后,有些人可能面对这笔巨额交易摇摆不定,而错失了良机。 收益于 Indicator + 前期数据准备,我们在看房时候应该具有了客观的评价能力。 一旦各方面满意,不要犹豫,快速决策。 但也不要因为几次擦肩而过就冲动决策,要相信客观的 Indicator 数据。

我推荐上班族看房时间安排是,频繁看房两个月,每周末两天出动。 前期一个月看房,不管看中多中意的,都不做决策, 但要计算出一个自己心理预期。一个月后,看到合适的立刻上手。


OK 写完了。回顾其实这四个步骤,其实就是选一个开源软件的流程嘛。 设定特性需求 -> 设定期望 -> 调研候选者 -> 快速测试上线。 嚯嚯嚯。

最后,如果你恰好在看房,希望你看完本文有些收获,收获自己中意的住房。


原文链接: https://blog.alswl.com/2016/12/house/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
alswl's avatar

🔒 也谈 HTTPS - HTTPDNS + HTTPS

最近谈论 HTTPS 的文章很多,其原因之一是运营商作恶底线越来越低,动不动就插播广告, 前两天小米还联合几家公司发文 关于抵制流量劫持等违法行为的联合声明 痛斥某些运营商。 另一方面也是苹果 ATS 政策的大力推动,逼迫大家在 APP 中全部使用 HTTPS 通信。 上 HTTPS 的好处很多:保护用户的数据不外泄,避免中间人篡改数据, 对企业信息进行鉴权。

关于 HTTPS 如何购买证书,如何部署,网上的教程已经太多了,实践起来没有太大的难处。 我们在部署 HTTPS 的时候,遇到了一些新问题,首当其冲的就是 HTTPS 部分网络不可访问的问题:

尽管使用了 HTTPS 技术,部分邪恶的运营商,仍然使用 DNS 污染技术,让域名指向的他们自己服务器 而这些服务器并没有部署 SSL 服务(就算部署了,也会触发 SSL 证书 Common name 不一致报警), 导致 443 端口直接被拒绝。

这个问题不解决,强行上 HTTPS 的话,会导致一部分用户出现无法访问网站 一旦用户不爽了,轻则对产品不信任,重则直接导致用户流失。

运营商为了赚广告钱、省网间结算是不择手段的。 他们普遍使用的劫持手段是通过 ISP提供的 DNS 伪造域名。 那有没有什么方法可以解决 DNS 劫持呢? 业界有一套解决这类场景的方案,即 HTTPDNS。

HTTPDNS 的原理很简单,将 DNS 这种容易被劫持的协议,转为使用 HTTP 协议请求 Domain <-> IP 映射。 获得正确 IP 之后,Client 自己组装 HTTP 协议,从而避免 ISP 篡改数据。

有两篇文章很清晰的讲解了 HTTPDNS 的细节:

点击 https://dns.google.com/resolve?name=www.duitang.com / http://119.29.29.29/d?dn=www.duitang.com 感受一下 DNS-over-HTTPS / HTTPDNS。

单 IP 多域名支持

这个方案看似完美,但是在实际生产中,会遇到一个问题。

Android / iOS 在操作系统级别对 HTTPS 通信是提供了封装。 APP 无法在发起连接时候,也没有权限直接操作 socket。 所以尽管 APP 拿到了域名对应的 IP,却没有办法让这个 IP 在 HTTPS 里生效。

解决的思路很暴力:彻底放弃域名系统,完全使用基于 IP 系统的通讯。

原本请求 https://www.duitang.com 的 request, 调整为请求 https://221.228.82.181

OK,做到这一步,我们就可以跟运营商劫持说拜拜了。

不,还没结束。

完全搞定运营商之后,这 IP 方案给我们自己带来一个困扰: Nginx 服务器无法通过 Host 来识别不同域名下面的请求了!!! 在由于使用一个独立 IP,会导致所有域名请求混在一起,无法分别。 大公司可以 dedicated IP,小公司就玩不起了。

为了解决同一个 IP 下面多个域名的问题,我们引入了一个URL参数: __domain。 当请求 IP 域名时候,必须带着这个参数,服务器会将请求域名解析出来,再分发到对应的域名。

实现这个逻辑的 Nginx 核心代码:

set $query_domain $arg___domain;
if ($query_domain !~ '(www|a|b)\.example\.com') {
    rewrite ^ http://www.example.com/404/ redirect;
}
set $my_host $query_domain;
location / {
    proxy_set_header Host $my_host;
    proxy_set_header X-REAL-IP $remote_addr;
    proxy_pass $scheme://127.0.0.1;
}

最后一个注意事项是,记得调整 Nginx 配置的 remote_addr,否则都变成了 127.0.0.1, 也许会导致其他一些策略失效。

完美收工,效果如下:https://221.228.82.181/?__domain=www.duitang.com

恭喜你,已经掌握核心科技了,再也不怕运营商瞎折腾了,从此走上了业务蓬勃发展的金光大道……☀️

下一篇文章,我会再谈谈如何做 HTTPS 的「内测」,避免将线上业务一次性切到 HTTPS 导致不少边边角角业务无法正常使用。



原文链接: https://blog.alswl.com/2016/11/https-1/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
Li Fanxi's avatar

关注2017维也纳新年音乐会

曾经关注过的那些维也纳新年音乐会:关注维也纳新年音乐会,2017年将是我第22次收看维也纳新年音乐会的直播。

2017维也纳新年音乐会CD封面

  • 01 Franz Lehár – Nechledil Marsch aus der Operette Wiener Frauen – 内西雷迪尔进行曲(选自喜歌剧《维也纳的妇女》)
  • 02 Èmile Waldteufel – Les Patineurs; Walzer; op. 183 – 溜冰者圆舞曲
  • 03 Johann Strauss II – ‘s gibt nur Kaiserstadt, ‘s gibt nur Wien!; Polka; op. 291 – 只有帝国之都,只有维也纳(皇城)波尔卡 – 1997
  • 04 Josef Strauss – Winterlust; Polka schnell; op. 121 – 冬趣快速波尔卡 – 2005
  • 05 Johann Strauss II – Mephistos Hollenrufe; Walzer; op. 101 – 梅菲斯特的地狱圆舞曲 – 1995
  • 06 Johann Strauss II – So angstlich sind wir nicht!, op. 413 – 我们决不畏惧波尔卡 – 2009
  • 07 Franz von Suppé – Ouvertüre zu Pique Dame – 黑桃皇后序曲
  • 08 Carl Michael Ziehrer – Hereinspaziert!; Walzer; op. 518 – 闲庭信步圆舞曲 – 1979
  • 09 Otto Nicolai – Die lustigen Weiber von Windsor, Moon Choir – 月升小合唱(选自轻歌剧《愉快的温沙妇人》)
  • 10 Johann Strauss II – Pepita-Polka; op. 138 – 细花纹方格波尔卡
  • 11 Johann Strauss II – Rotunde-Quadrille; op. 360 – 圆形大厅四对舞
  • 12 Johann Strauss II – Die Extravaganten; Walzer; op. 205 – 奢靡圆舞曲
  • 13 Johann Strauss I – Indianer-Galopp; op. 111 – 印度人加洛普 – 2004
  • 14 Josef Strauss – Die Nasswalderin; Polka mazur; op. 267 – 纳斯瓦尔德的女孩玛祖卡波尔卡 – 1996
  • 15 Johann Strauss II – Auf zum Tanze!; Polka schnell; op. 436 – 跳舞吧快速波尔卡
  • 16 Johann Strauss II – Tausend und eine Nacht; Walzer; op. 346 – 一千零一夜圆舞曲 – 1992, 2005
  • 17 Johann Strauss II – Tik-Tak; Polka schnell; op. 365 – 提塔快速波尔卡 – 1979, 2002, 2012
  • 18 Eduard Strauss ? – ?
  • 19 Johann Strauss II – An der schönen blauen Donau, Walzer, op. 314 – 蓝色多瑙河圆舞
  • 20 Johann Strauss I – Radetzky-Marsch, op. 228 – 拉德茨基进行曲

2017年维也纳新年音乐会即将迎来首次登上该音乐盛事指挥台的指挥家古斯塔沃·杜达梅尔。做为当代最为杰出的指挥家之一,80后的杜达梅尔也将成为有史以来新年音乐会上最年轻的指挥。

年轻的指挥家为这传统古老的音乐会带来了新的气息,这次音乐会正式演出的17个曲目中,有8首是第一次在新年音乐会上与乐迷见面,剩下的9首中也有7首是只在新年乐会上演出过一次的“冷门”曲目。加演的第一个曲目目前还没有公布,如果“路边社”消息属实,加演的是爱德华的一首快速波尔卡,那么这次音乐会的曲目单中就将史无前例地出现九位作曲家的名字,新年音乐会在演绎施特劳斯家族音乐的传统之上,越来越多的融入了更多其他作曲家的作品。

维也纳音乐之友协会合唱团将第一次加入新年音乐会的演出行列,在下半场与爱乐乐团一同演绎“月升小合唱”。

中央电视台从1987年开始转播维也纳新年音乐会,到现在已经有30年。我从1996年开始收看新年音乐会,到现在已经超过了20年。经历了刚开始的陌生和新奇以及中间的狂热,新年音乐会如今已经成为了一种习惯。我用上面的文字列举完了音乐会曲目单的各项“技术参数”以后,惊讶地发现,对于每一个具体的曲目,我竟然写不出任何文字再去深入的点评它们。那些曾经演出过的曲目,在脑海中的印象似乎也变得越来越模糊。不过我也不打算像以前那样把曲目单上的曲子都找出来复习+预习一遍了,我相信在新年到来的时候,乐团的演绎会让我回忆起那些曾经熟悉旋律,那种与老朋友相见的感觉想必是非常美好的。

期待新年音乐会,也期待新的一年。

Phoenix Nemo's avatar

使用 Unbound 搭建更好用的 DNS 服务器

用了好久的 DNSMasq 方案终于在大半年前彻底炸掉了。

原因不光是 DNSMasq 性能和安全性完全不足以撑起公网缓存/递归 DNS 的任务,也有想要做反污染和加速的时候确实太蛋疼的问题。

现在使用的方案是 Unbound+DNSCrypt,外带一份加速列表。这段时间看来,不管在我本机还是在公网服务的两台,效果和反馈都很不错。

准备工作

需要的程序:

  • unbound
  • dnscrypt-proxy
  • makefile
  • git

makefilegit 用于处理 dnsmasq-china-list

Unbound 配置

修改文件 /etc/unbound/unbound.conf。没有这个文件的话,一般需要找一下软件包里提供的配置 example 文件复制过去。这里列出的仅包含需要修改的部分,其他的按照默认配置一般没有问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
num-threads: 2 # 线程数可以修改为物理核心数
interface: 0.0.0.0 # 侦听所有 IPv4 地址
interface: ::0 # 侦听所有 IPv6 地址
# 如果只需要本机使用,则一个 interface: 127.0.0.1 即可
so-rcvbuf: 4m
so-sndbuf: 4m # 本机使用的话,这俩 buf 可以取消注释
so-reuseport: yes # 如果开了多线程,就写 yes
msg-cache-size: 64m # 本机可以设置 4m 或者更小
rrset-cache-size: 128m # 本机可以设置 4m 或者更小
cache-max-ttl: 3600 # 建议设置一个不太大的值...专治各种运营商 DNS 缓存不服
outgoing-num-tcp: 256 # 限制每个线程向上级查询的 TCP 并发数
incoming-num-tcp: 1024 # 限制每个线程接受查询的 TCP 并发数
# 下面这四个不需要解释了吧,不想用那个就写 no
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
tcp-upstream: no # 默认是 no,隧道状态比较稳的话也不需要写 yes。一些情况下强制使用 tcp 连上游的话写 yes
access-control: 0.0.0.0/0 allow # 本机用的话建议设置 127.0.0.0/8 allow,局域网用适当调整
root-hints: "/etc/unbound/root.hints" # 没有的话在 ftp://FTP.INTERNIC.NET/domain/named.cache 下载一份
hide-identity: yes # 不返回对 id.server 和 hostname.bind 的查询。
hide-version: yes # 不返回对 version.server 和 version.bind 的查询。
# 不过下面有 identity 和 version 的自定义选项,不隐藏这些的话,修改下选项还可以卖个萌(´・ω・`)
harden-glue: yes # 建议打开
module-config: "iterator" # 禁用 DNSSEC 检查,如果上游不支持 DNSSEC 就关掉。注意这个选项有可能在其他 include 的文件里
unwanted-reply-threshold: 10000000 # 针对各种网络不服,数值为建议值,具体可以自己修改看看效果
do-not-query-localhost: no # 一般是为了防止扯皮丢包开着,不过等下要用 DNSCrypt 所以关掉
prefetch: yes # 蛮好用的,开着吧
minimal-responses: yes # 省带宽,开着吧。本机用可以关掉
# 关键部分来了,把默认查询全部丢给 DNSCrypt。使用 [地址]@[端口] 指定查询地址和端口,默认端口 53。
# 然后把国内的地址丢给国内的缓存服务器。这两个选项的顺序不能错哟。
# 如果使用隧道查询,把这个地址改为隧道对端的地址,或者一个国外的 DNS 服务器都可以,例如 8.8.8.8。
# 具体看是在对端开 DNS 还是直接用国外的服务器。后者的话,前面 outgoing-interface 可以直接设置隧道本地端的地址,不过要配合 dnsmasq-china-list 的话,还是写路由表比较合适,否则不够灵活。
include: "/etc/unbound/accelerated-domains.china.unbound.conf"
forward-zone:
name: "."
forward-addr: 127.0.0.1@5353

DNSCrypt 配置

修改文件 /etc/default/dnscrypt-proxy

1
2
DNSCRYPT_PROXY_LOCAL_ADDRESS=127.0.0.1:5353 # 设置侦听在 127.0.0.1 端口 5353
DNSCRYPT_PROXY_RESOLVER_NAME=cisco # cisco 其实蛮快的,但是慢的话就去用个别的吧。d0wn 的那堆服务器真的不稳定,和名字一个样...

如果使用 systemd 的话这个文件就没用辣,看这里看这里

修改文件 /usr/lib/systemd/system/dnscrypt-proxy.socket

要修改的部分:

1
2
ListenStream=127.0.0.1:5353
ListenDatagram=127.0.0.1:5353

127.0.0.1:5353 就是上面 unbound 配置里 DNSCrypt 的监听地址。

dnsmasq-china-list 来加速

首先 clone 这个仓库到本地。

执行 make unbound 来生成一份 unbound 配置,然后放在上面 unbound 配置里写的地址,也就是 /etc/unbound/accelerated-domains.china.unbound.conf。默认的 DNS 是 114DNS,最不太近的一段时间都挺残的所以不建议用。如果要改为其他的,例如 DNSPod PublicDNS 的话使用 make SERVER=119.29.29.29 unbound 即可。

如果还需要一些特定的缓存上游设置,要放在 include: "/etc/unbound/accelerated-domains.china.unbound.conf" 这句前面。来举一只栗子。

举一只果子的栗子。

AppStore 的加载和下载速度一直在国内饱受一些极客们的诟病,然而同时饱受诟病的运营商 DNS 解析 AppStore 的下载地址反而是国内的 CDN 地址,速度会非常快。

需要使用 dig 工具,并且要在国内的网络环境下测试。Linux 发行版里都有包可以装,OS X 则自带。

第一条命令:dig +trace a100.phobos.apple.com.

递归追踪解析结果,这个记录被 CNAME 到了 a100.phobos-apple.com.akadns.net.

第二条命令:dig +trace a100.phobos-apple.com.akadns.net.,得到这条记录被 CNAME 到了 a1-a200.itunes-apple.com.akadns.net.

第三条命令:dig +trace a1-a200.itunes-apple.com.akadns.net.,看到了 CDN 的地址,a1-a200.phobos.apple.chinacache.net.

所以如果有国内的地址直接去跟踪解析 akadns.net 的 DNS 的话,一般是可以正确解析到国内 CDN 的。先找一个 akadns.net 的权威 NS 地址,比如 a1-128.akadns.net 对应的 IP 是 193.108.88.128。同时还得有国内的 CDN 地址也从国内地址去解析,那么配置里这样写:

1
2
3
4
5
6
forward-zone:
name: "akadns.net."
forward-addr: 193.108.88.128
forward-zone:
name: "chinacache.net."
forward-addr: 119.29.29.29 # DNSPod PublicDNS 的服务器地址

多次尝试之后,发现还有一个 CDN 地址 0gq2p7eckbs26f.mwcname.com,那么也把这个地址交给 unbound 通过国内网络解析。

1
2
3
forward-zone:
name: "mwcname.com."
forward-addr: 119.29.29.29

一并加到上面的配置里,然后来测试下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Phoenix-X1-Carbon :: ~ » dig @127.0.0.1 a100.phobos.apple.com

; <<>> DiG 9.10.3-P4 <<>> @127.0.0.1 a100.phobos.apple.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 24454
;; flags: qr rd ra; QUERY: 1, ANSWER: 14, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;a100.phobos.apple.com. IN A

;; ANSWER SECTION:
a100.phobos.apple.com. 3596 IN CNAME a100.phobos-apple.com.akadns.net.
a100.phobos-apple.com.akadns.net. 117 IN CNAME a1-a200.itunes-apple.com.akadns.net.
a1-a200.itunes-apple.com.akadns.net. 297 IN CNAME a1-a200.phobos.apple.chinacache.net.
a1-a200.phobos.apple.chinacache.net. 1799 IN CNAME a1-a200.phobos.apple.cncssr.chinacache.net.
a1-a200.phobos.apple.cncssr.chinacache.net. 1799 IN CNAME cc00109.h.cncssr.chinacache.net.
cc00109.h.cncssr.chinacache.net. 120 IN A 223.99.228.87
cc00109.h.cncssr.chinacache.net. 120 IN A 113.207.33.15
cc00109.h.cncssr.chinacache.net. 120 IN A 113.207.33.12
cc00109.h.cncssr.chinacache.net. 120 IN A 202.110.80.14
cc00109.h.cncssr.chinacache.net. 120 IN A 61.179.105.154
cc00109.h.cncssr.chinacache.net. 120 IN A 61.179.105.7
cc00109.h.cncssr.chinacache.net. 120 IN A 119.188.138.172
cc00109.h.cncssr.chinacache.net. 120 IN A 120.192.248.195
cc00109.h.cncssr.chinacache.net. 120 IN A 221.181.39.76

;; Query time: 233 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Thu Apr 28 00:34:02 CST 2016
;; MSG SIZE rcvd: 387

走国内 CDN 了吧~

重启相关服务,更改自己所用的 DNS 地址,完事儿收工(「・ω・)「

=== 2016-04-30 14:00 更新 ===

如果需要 edns-client-subnet 支持的话,需要手动编译源码安装。命令

1
2
3
4
# 克隆源码
svn co http://unbound.nlnetlabs.nl/svn/branches/edns-subnet/
# 编译安装
./configure --enable-subnet --with-libevent && make && sudo make install

配置文件的格式

1
2
# 默认向所有服务器发送 edns-client-subnet
send-client-subnet: 0.0.0.0/0

如果只对特定权威 DNS 发送 edns-client-subnet 请求,则按照此格式写多行 IP。

Phoenix Nemo's avatar

关于 SSL 和 IPKVM/VNC Applet 的笔记两则

虽然是没啥关系的两个东西不过最近没啥可写的所以放在一块做个笔记。

COMODO SSL CA 正确的串联姿势

有客户买了 COMODO Positive SSL 之后问我证书链变了要怎么串联。嘛确实文件变多了不过不至于这就不会了吧…

SSL 证书串联用 cat 命令的话顺序是自下而上,也就是 自己的证书 -> 二级 Intermediate CA -> 一级 Intermediate CA -> Root CA

所以不管文件有多少这个顺序总是有的。以 COMODO Positive SSL 为例,假设域名为 example.comcat 的串联命令:

1
cat example_com.crt COMODORSADomainValidationSecureServerCA.crt COMODORSAAddTrustCA.crt AddTrustExternalCARoot.crt > example_com.signed.crt

于是就可以拿着 example_com.signed.crt 和生成 CSR 时一并生成的 example_com.key 去给 NGINX 咯。

附一份 NGINX 配置样例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
server {
listen 80;
listen [::]:80 ipv6only=on;
server_name example.com;
## redirect http to https ##
rewrite ^ https://example.com$request_uri? permanent;
}

server {
listen 443 ssl;
listen [::]:443 ssl ipv6only=on;
server_name example.com;

ssl on;
ssl_certificate /etc/nginx/ssl/example_com.signed.crt;
ssl_certificate_key /etc/nginx/ssl/example_com.key;
ssl_protocols SSLv3 TLSv1;
ssl_prefer_server_ciphers on;
ssl_ciphers ALL:!aNULL:!ADH:!eNULL:!LOW:!EXP:RC4+RSA:+HIGH:+MEDIUM;
keepalive_timeout 70;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

location / {
# ...
}
}

最后不忘打广告: [https://c.phoenixlzx.com]

=== 2014-10-15 更新 ===

SSLv3 再次爆出漏洞,在 Google 的新草案 TLS_FALLBACK_SCSV 还未明朗的情况下,目前禁用 SSLv3 是一个 不考虑 IE6 的 workaround。

1
2
3
4
5
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDHE-RSA-AES256-SHA384:AES256-SHA256:HIGH:!RC4:!MD5:!aNULL:!eNULL:!NULL:!DH:!EDH:!AESGCM;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

IPKVM/VNC Applet 的使用

从 7u45 版本之后的 Java Webstart 就不能再直接执行 Ubersmith/SolusVM/Proxmox 等等的 IPKVM/VNC Applet 了。虽然实际上是这些商业软件的错不过还是很想让人骂 Java。

解决方案:下载一个旧版的 Java,比如 jdk6。

但是直接安装也是不能运行的,因为实际上调用的 java executable 是最新版 Java 的,所以一样报错。

如果是在 Linux 下,jre6 的目录在 $HOME/jre6/jre1.6.0_38,那么下载到 Applet 之后用下面的命令启动:

1
2
applet_path=/path/to/your/applet.jnlp
~/jre6/jre1.6.0_38/bin/java -verbose -Xbootclasspath/a:~/jre6/jre1.6.0_38/lib/javaws.jar:~/jre6/jre1.6.0_38/lib/deploy.jar:~/jre6/jre1.6.0_38/lib/plugin.jar -classpath ~/jre6/jre1.6.0_38/lib/deploy.jar -Djava.security.policy=file:~/jre6/jre1.6.0_38/lib/security/javaws.policy -DtrustProxy=true -Xverify:remote -Djnlpx.home=~/jre6/jre1.6.0_38/bin -Dsun.awt.warmup=true -Djnlpx.origFilenameArg=$applet_path -Djnlpx.remove=false -esa -Xnoclassgc -Djnlpx.splashport=59999 -Djnlpx.jvm=~/jre6/jre1.6.0_38/bin/java -Djnlpx.vmargs="-esa -Xnoclassgc" com.sun.javaws.Main $applet_path
Phoenix Nemo's avatar

Sierra 4G LTE EM7345 with Arch Linux

X1 Carbon 2015 内置 Sierra EM7345 4G LTE 适配器,系统里看到是这样:

1
2
$ lsusb
Bus 001 Device 002: ID 1199:a001 Sierra Wireless, Inc.

根据 ThinkPad EM7345 4G LTE Mobile Broadband - Overview and Service Parts 这块 EM7345 基本完美支持联通的 3G/4G 制式。

插入 microSIM 卡,然后安装需要的包:

1
# pacman -S usbutils usb_modeswitch modemmanager mobile-broadband-provider-info

启用 modemmanager 服务

1
systemctl enable modemmanager.service

然后加载内核模块 cdc_mbim,文件 /etc/mkinitcpio.conf

1
MODULES="... cdc_mbim"

重新生成内核镜像

1
# mkinitcpio -p linux # or change to your kernel preset here

重启即可在 NetworkManager 中新建 GSM 设备的 broadband (移动宽带) 连接。

顺便上一张图,老家小城市还是郊区所以 4G 信号几乎没有,还是达到了这个速度,点个赞(๑•̀ㅂ•́)و✧

参考文档:

Phoenix Nemo's avatar

公共场合请使用指纹识别器

前几天带本本去导师办公室修改论文,虽然开机启动速度快,但是还是在老师的注视之下紧张敲错了三遍密码…得,本来没在意的也记住密码了(ry

所以为啥有指纹识别器的本子偏偏要输密码(:з」∠)

检查设备

总之先来看下指纹识别器是哪家生产的好了。

1
2
Phoenix-X1-Carbon :: ~ » lsusb | grep -i 'finger'
Bus 001 Device 003: ID 138a:0017 Validity Sensors, Inc. Fingerprint Reader

搜索一下就发现 X1 Carbon Gen3 这么受欢迎的本子早已上了 ArchWiki https://wiki.archlinux.org/index.php/Lenovo_ThinkPad_X1_Carbon_(Gen_3)

安装驱动

安装 fprintd 包,这个包会依赖 libfprint,但是官方仓库中的 libfprint 目前的版本在此指纹识别器上无法正常工作,表现为 enroll 只扫描一次,并且无法验证指纹。

Note that recent versions of fprint are broken for this model : One is able to enroll a finger but recognition always fails.

GitHub 上已经有了 vfs5011 相关的 issue 和 fix,但是还没有发布新的稳定版。所以暂时安装 aur/libfprint-git 包。

配置指纹验证

Fprint - ArchWiki 上已经给出了配置方法。将 auth sufficient pam_fprintd.so 加入 /etc/pam.d/system-local-login 中的第一位置即可。同样方法可以用于 /etc/pam.d/ 下的其他文件。例如 KDE 登陆使用的 sddm

扫描指纹,以普通用户身份命令:

1
$ fprint-enroll

默认是右手食指。可以使用 -f 选项指定手指。正常的表现为成功扫描五次即可保存指纹,并且在需要验证的时候自动启动指纹识别器。不过还有个小问题,如果保存多个指纹,似乎只能识别保存指纹列表中的第一个。

然而我懒得折腾了就这样吧

Phoenix Nemo's avatar

使用 IPSec 连接带有 chnroutes 的隧道网络配置小记

标题似乎不是很易懂,于是解释下先。

位于大陆的服务器 A 和位于海外的服务器 B,A 与 B 之间使用隧道互联成内网,服务器 A 配置 chnroutes 以在必要的时候通过海外服务器访问网络,并在服务器 A 上配置 IPSec 服务器,从而使终端用户能够在任何网络环境下安全接入内网。大致描述如下:

需要加速海外访问时:终端用户 <- IPSec VPN -> 服务器 A <- 隧道 -> 服务器 B <-> 互联网

需要访问大陆网域时:终端用户 <- IPSec VPN -> 服务器 A <-> 互联网

此场景适用于互联网公司为员工提供快速、安全、便捷的工作网络环境,对于个人用户来说负担较大,不建议使用。

配置隧道

最简单的方案在服务器 A 和 B 上配置 GRE 隧道即可,步骤简单不再赘述。需要注意的是为了能让客户端的内网地址能够访问到隧道的对端,也就是服务器 B 端,服务器 B 上配置隧道时 peer 的参数应当是包含服务器 A 和 VPN 客户端 IP 的 IP 段,例如 ip addr add 10.7.0.1 peer 10.7.0.2/24 dev gre0

隧道打通后,服务器 A 的路由配置为:

  • 到服务器 B 的公网 IP 路由经服务器 A 的公网网关出站
  • 默认出口路由为隧道对端
  • 注意设置内网之间的路由
  • chnroutes 配置到大陆的流量经由服务器 A 的公网网关出站

配置 IPSec

安装 strongswan

1
# apt-get install strongswan

修改 /etc/ipsec.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
config setup
charonstart=yes
nat_traversal=yes
uniqueids=never # 修改为 yes 可以限制为单个设备连接

conn ios
keyexchange=ikev1
authby=xauthpsk
xauth=server
left=%defaultroute
leftsubnet=0.0.0.0/0
leftfirewall=yes
right=%any
rightsubnet=10.7.0.64/26 # 在配置的隧道网段中选择一个小段以避免地址冲突
rightsourceip=10.7.0.65/26 # rightsubnet 和 rightsourceip 按情况
pfs=no
auto=add

修改 /etc/ipsec.secrets

1
2
3
: PSK "Your pre-shared key" # 预共享密钥,注意修改引号内部分

username : XAUTH "userP@ss" # 用户名和密码,按格式每行一个

配置 iptables 转发

1
# iptables -t nat -A POSTROUTING -o <公网网卡> -s <VPN 网段> -j SNAT --to-source <公网地址>

在本例中,iptables 的命令为

1
iptables -t nat -A POSTROUTING -o eth0 -s 10.7.0.64/26 -j SNAT --to-source xxx.xxx.xxx.xxx

DNS 加速优化

既然是服务器中转分流,那么需要在服务器上进行 DNS 解析方可获得最佳效果。

安装 dnsmasq 和 git

1
# apt-get install dnsmasq git

修改 /etc/dnsmasq.conf 部分:

1
2
server=8.8.8.8 # 默认上游服务器通过隧道交由 Google DNS 解析以获得最佳海外站点效果
conf-dir=/etc/dnsmasq.d # 启用配置文件目录

获取 dnsmasq-china-list,将其中的配置文件软连接到 /etc/dnsmasq.d 下即可使用 114DNS 直接解析大部分需要加速的国内站点。

屏蔽公网对本机 DNS 服务的直接访问:

1
2
# iptables -A INPUT -p udp -d <服务器 A 公网 IP> --dport 53 -j DROP
# iptables -A INPUT -p tcp -d <服务器 A 公网 IP> --dport 53 -j DROP

修改 /etc/strongswan.confcharon { ... } 块内添加:

1
dns1 = 10.7.0.2  # 此处可填服务器 A 上除公网 IP 和 localhost 之外的任意可绑定 IP 地址,例如隧道的本地端地址

测试

一切就绪后重启 strongswan 服务:

1
# service strongswan restart

客户端的 IPSec VPN 配置:

  • 服务器地址为服务器 A 的公网地址
  • 用户名、密码、预共享密钥为服务器 A 中配置文件中的值

连接上之后在客户端 ping 任意公网 IP、服务器 A 和服务器 B 的隧道端 IP 应该都是通的。至此,客户端连接 IPSec VPN 后所有流量都会加密经由服务器 A 中转,在必要时会经由海外服务器 B 转发,保证了访问工作必需站点的速度和在任何公共网络环境下的数据安全。

Phoenix Nemo's avatar

Kancolle Broker - 舰娘直连游戏!

废话:啊……我终于治好了自己的拖延症。

事情的起因,无非就是 2015 年的第一场大戏——舰娘国服。嘛。不多评论,大家心里都明白。之后用 nginx + SNI Proxy 配置了一个代理服务器方便大家在日本地区外玩舰娘。但是这种代理服务有一个很大的问题就是会引起恶意攻击,被用作 DDoS 攻击的肉鸡使用,因此日本法律也是禁止 VPN 服务器的。

这个 idea 出现的时候还在帝都,房间里没开空调没有暖气,4M带宽20人用还有人开迅雷,无奈只好烧朋友送的 4G 流量卡来维持网络需求。完全没有力气做事情的时候偏偏看到 DMM 那个变态到极点的登录验证……

于是一直拖到回家之后碰上一大堆喵窝的活动… bangumi.moe 那边也在一直催着干活,直到今天我才良心发现用力让自己头脑清醒一点,认真分析了下 DMM 的登录验证。

整个过程大概是这样:客户端请求登录页面,页面中包含两个 token,加载完毕后一个 token 作为 header 数据,一个作为 XHR POST 请求数据发送给服务器,获得另外两个 token。这两个 token 将作为用户登录 email 和 password 的 key。用户登录时 post 给服务器的数据大概是下面这样:

1
2
3
4
5
6
7
8
9
{
"token": "11dde99d39af2287fc6eed02632ccbee",
"login_id": "user@example.com",
"save_login_id": 0,
"password": "P@ssw0rd",
"use_auto_login": 0,
"dfdfa8466f24710893d99529acaaeef0": "user@example.com",
"2760cb37702b03d10d92caf9daaaf675": "P@ssw0rd"
}

… 好了,看晕了吧。咱就不吐槽日本人脑子里到底在想啥了。总之来看代码 -> Github

原理就是登录后用 cookie 拿到游戏 link 的 apptoken,而这个游戏的 link 是不会验证日本 IP 的。

想测试效果的话,那么先关闭本地的各种舰娘代理,移除所有相关的 hosts,然后访问这里。使用 DMM 帐号登录,成功的话会跳到舰娘的游戏页面。适用于所有在日本境外没有日本代理的情况下使用。

问:会不会有安全问题?

必须的。而且比 SNI 代理更不安全。但是反过来说,也是比 SNI 更安全的。因为 DMM 登录发送密码完全没有加密,只是靠 HTTPS。所以用这个程序的话:

  1. 密码都是可以在服务端记录的。虽然程序里完全没有记录密码相关的代码,但是要加也不是难事。
  2. 没有 HTTPS 的情况下,你发送的所有数据都是明文的。所以我做的这个 demo 用了 HTTPS 加密链接。

所以如果是有 HTTPS 加密且 deploy 这个程序的人比较靠谱的话,安全性还是有保障的。

至于大家是不是相信我……嗯这个看各位如何考量啦。

最后:我真的没玩过舰娘,我也不会去玩。

Phoenix Nemo's avatar

懒(烂)办法制作 Telegram Sticker Pack

Telegram 支持表情包 (Stickers Pack) 之后各个群里就开始出现各种奇怪的聊天表情。嘛,虽然有些做得不错但是终究是别人分享的,自己有很多好看的表情图片就不便使用了。

所以今天折腾了下,用现成的图片集来制作 stickers pack。本文使用的 sips 命令内置于 OS X 操作系统,Linux 则可以更方便地使用 ImageMagick 来操作。

Telegram 要求 Sticker 图片为 PNG 格式,并且要有透明层,至少一边为 512 像素,另一边则不超过 512 像素。最大文件大小为 350KB。透明层就算了,我不会玩 PS。那么直接偷懒(死)来批量把当前目录下 JPG 和 PNG 混杂的图片们统一转换为 PNG 好了。

命令:

1
2
3
4
5
6
# 创建 output 目录以存放输出图片...
mkdir -p output
# 把当前目录下的所有 JPG 文件转换为 PNG 丢进 output 目录里。
for i in *.jpg; do sips -s format png $i --out output/$i.png;done
# 把剩下的 PNG 图片丢进 output 准备下一步的批量操作以及简单清理... 没有的话可以跳过。
mv *.png output/ && rm *.jpg

接下来把所有图片缩放为宽或高最大为 512 像素,并且保持比例。

1
2
cd output
sips -Z 512 *.png

搞定。最后一步就是添加 Stickers Pack。需要敲这位机器人

几条命令发给 stickers bot:

  1. /newstickerpack
  2. 发送表情包的名字…
  3. 在内置的 emoji 中发送一个最符合你要发送图片的表情…
  4. 然后把对应的图片作为文件发送
  5. 如果还有其他图片的话重复 3-4
  6. 全部表情图片设置完毕,发送 /publish 命令
  7. 为你的 stickers pack 取一个短名字 (用于 URL)

KABOOM! 新的 Stickers Pack 制作完成~ 快拿链接去和大家分享吧~

最后贴我的芙兰酱的表情包
这些图片木有高清的所以如果谁能做一份高清的表情来请务必告诉我_(:3」

=== 2015.06.07 更新 ===

Mika 菊苣制作了一份芙兰酱的高清表情包

Phoenix Nemo's avatar

迁移到了 uWSGI

虽然 PHP FPM 一直用得还行… 唔,大概已经不能叫做「还行」了,因为它总是莫名其妙地一个请求不响应然后阻塞掉了其他所有的请求。然后就是无止境的 504 Gateway Timeout… 唯一的解决方案是重启 FPM。

然后负载一高就%@T$#$#%@#$%什么的不说了。

无缝切换 PHP FPM 到 uWSGI

安装必要的包

1
apt-get install uwsgi uwsgi-plugin-php supervisor

如果是全新安装的话,php 的各种库也要一起装上

1
apt-get install php5-cgi php5-mysql php5-curl php5-gd php5-idn php-pear php5-imap php5-mcrypt php5-mhash php5-pspell php5-recode php5-sqlite php5-tidy php5-xmlrpc php5-xsl

创建文件 /etc/uwsgi/apps-available/uwsgi-php.ini 并软连接到 /etc/uwsgi/apps-enabled/uwsgi-php.ini

1
ln -s /etc/uwsgi/apps-available/uwsgi-php.ini /etc/uwsgi/apps-enabled/uwsgi-php.ini

文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[uwsgi]
plugins = php

; leave the master running as root (to allows bind on unix socket or low ports)
master = true
master-as-root = true

; listening on unix socket
socket = /var/run/uwsgi-php.sock
chown-socket = www-data:www-data

; drop privileges
uid = www-data
gid = www-data

; our working dir
project_dir = /var/www

; load additional PHP module ini files
env = PHP_INI_SCAN_DIR=/etc/php5/embed/conf.d

; ; or if you need to specify another PHP.ini
; php-ini = /etc/php5/embed/php.ini
; ; then load additional php module ini files manually
; for-glob = /etc/php5/embed/conf.d/*.ini
; php-ini-append = %(_)
; endfor =

; allow .php and .inc extensions
php-allowed-ext = .php
php-allowed-ext = .inc

; set php timezone
php-set = date.timezone=UTC

; disable uWSGI request logging
disable-logging = true
; use a max of 10 processes
processes = 10
; ...but start with only 2 and spawn the others on demand
cheaper = 2

如果要指定 php.ini 的话,env 会失效所以单独用一个循环来读入所有的额外模块配置。envphp-ini/for-glob 二选一。不知道有没有更优雅一些的方法呢…

创建 Supervisor 配置文件 /etc/supervisor/conf.d/uwsgi-php.conf

1
2
3
4
5
6
7
[program:uwsgi-php]
command = uwsgi --ini /etc/uwsgi/apps-enabled/uwsgi-php.ini
autorestart = true
user = root
stderr_logfile = /var/log/supervisor/uwsgi-php.log
logfile_maxbytes = 1048576
logfile_backups = 10

user 一定是 root,因为需要权限创建 unix socket。之后 uWSGI 会自动降权(参见前面的配置文件)。

然后在 NGINX PHP 的部分修改为

1
2
3
4
5
location ~ \.php$ {
include uwsgi_params;
uwsgi_modifier1 14;
uwsgi_pass unix:/var/run/uwsgi-php.sock;
}

最后重启所有服务并清理——

1
2
3
service supervisor restart
service nginx reload
apt-get purge php5-fpm

搞定(๑•̀ㅂ•́)و✧

Phoenix Nemo's avatar

想要导出 Telegram 贴图

Telegram 上出现了越来越多的优质贴纸,想要把这些贴纸用到其他 IM 平台上的时候就会比较麻烦,所以一直想要一键导出一个贴纸包的功能。

可惜的是,Telegram bot API 的限制,并没有任何简单的办法通过贴纸消息获得贴纸包的信息。寻找另外的途径,例如 telegram.me 的贴纸链接会定向到 tg://addstickers?set=[StickerSet]。搜索了一下现成客户端的源码,都是交给 MTProto 的 API 处理,也没有明确的解析过程。而这些客户端所调用的 messages.getStickerSet 也没有在官方的文档中列出。(吐槽:Telegram 的协议、文档和代码真是糟糕,查阅的时候我的表情一直是 黑人问号.gif

由于最近状况不是很好,所以只好暂时放弃继续读 webogram 的源码。因为读 Angular 的东西实在是折磨…

所以依然是选择直接发 sticker 再转为图片发给用户的模式。这样的已经有了相关的 bot,于是改为多个 sticker 打包、支持多语言、支持 jpg 和 png 以及批量缩放功能的 bot。需要安装 Node.js v4.0 及以上版本和支持 webp 的 ImageMagick。

虽然实现效果看起来还可以,但是并未实现最初希望的功能,所以只能是练手用的轮子而已。不过,这个轮子稍微尝试了一些新的东西。例如超简陋的内存数据库,而且很多细节考量更加周到,例如任务锁虽然不是写过最麻烦的,不过应该算是相对完善的。当然也考虑了内存数据库的手动释放以防内存爆炸为此还特地在群里讨论了 object children 被 undefine 而 object 其他 children 还在被引用的状态下是否可以回收部分内存的问题

源码的实现非常简单,但是好久不写代码还是手生,折腾了一下午写功能加一晚上和朋友们 debug。读源码戳 GitHub

这里有一只 bot 跑在测试环境,所以可以尝试一下。如果没理你说明沙盒没开,那么就请自己去跑源码来使用辣ᕕ(ᐛ)ᕗ

有几点坑,比如这个 node-telegram-bot-apionText 方法无法正确匹配 Negative Lookahead 的正则表达式(不应该啊…然而没深究),adm-zip 非常非常不好用,jszip 文档表述不清 API 调用复杂然而用起来了就还不错。

但是最坑的是,只为实现这么一个简单功能的 bot,我的 node_modules 目录下居然有

1
2
Phoenix-X1-Carbon :: js/telegram-stickerimagebot/node_modules ‹master› » ll | wc -l                                                                                               1
104

WHAT??? 104 个依赖包!!!

真是可怕…明明我已经尽可能减少不必要的依赖了…

Phoenix Nemo's avatar

搭建一套权威 DNS 服务架构

萌 DNS 已经年久失修。尽管一直有计划完全重写出一套应用目前各种 DNS 特性和优化的完整平台,但是目前的精力不允许。所以为了先让萌 DNS 的用户们至少先有一个能支持 Let’s Encrypt 的 DNS 服务,决定暂时舍弃 GeoDNS 功能,使用一套更加成熟的解决方案提供服务。

搭配方案如下:

服务器部署:

  • 管理服务器 x1
    • MySQL Master
    • PowerDNS
    • PowerDNS-Admin, supervisor, virtualenv, gunicorn…
    • NGINX, Let’s Encrypt
  • DNS 服务器 x4
    • MySQL Slave
    • PowerDNS

在管理服务器上安装 PowerDNS 和 MySQL Master 的考量是由于 PowerDNS-Admin 使用 PowerDNS HTTP API,在管理服务器(或管理私网中)启动一个仅用于提供 API 和操作主数据库的 PowerDNS 实例能够减轻 Primary NS Server 的压力并提升安全性。整套架构使用 Ansible 进行自动化部署,不过好久没用了各种生疏,照着文档折腾好久的配置…

于是这里暂且记录下整个过程。有些坑只是作者一时疏忽或者有别的考量但没有明确记录,也许在未来的版本中会修复。

安装 PowerDNS

所有服务器均使用 Ubuntu 16.04,需要 PowerDNS 4.0 以上的版本。按照此页面的说明添加 PowerDNS 官方的仓库即可。

1
# apt install pdns-server pdns-backend-mysql mysql-server

由 dpkg 自动配置 PowerDNS 的数据库,然后删除 /etc/powerdns/pdns.d无关的配置文件。

1
2
# rm /etc/powerdns/pdns.d/pdns.local.conf
# rm /etc/powerdns/pdns.d/pdns.simplebind.conf

配置 MySQL Replication,管理服务器作为 Master,其他 DNS 服务器作为 Slave。细节不多讲,官方文档 或者 DigitalOcean Tutorial

管理服务器 (MySQL Master) PowerDNS 配置文件 /etc/powerdns/pdns.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
api=yes
api-key=yourapisecretkey
api-logfile=/var/log/pdns-api.log
config-dir=/etc/powerdns
guardian=yes
include-dir=/etc/powerdns/pdns.d
launch=
local-address=127.0.0.1 # 不对外提供服务
local-ipv6=::1
security-poll-suffix=
setgid=pdns
setuid=pdns
webserver=yes
webserver-address=127.0.0.1 # 仅向本机的 PowerDNS-Admin 调用。如果配置在内网,请使用内网 IP
webserver-allow-from=127.0.0.1/32 # 同上,如果使用内网则写 PowerDNS-Admin 在内网的 IP
webserver-port=8081
default-soa-name=ns1.example.com # 改为 Primary NS 的地址
default-soa-edit=INCEPTION-INCREMENT
default-soa-mail=hostmaster.example.com # 改为默认服务器管理员的邮箱地址,并将 '@' 替换为 '.'
default-ttl=3600

DNS 服务器 (MySQL Slaves) PowerDNS 配置文件 /etc/powerdns/pdns.conf

1
2
3
4
5
6
7
8
9
10
11
config-dir=/etc/powerdns
daemon=yes
disable-axfr=yes
guardian=yes
include-dir=/etc/powerdns/pdns.d
launch=
security-poll-suffix=
server-id=ns1.example.com # 改为当前服务器的 ID,ns1/ns2/ns3/etc...
setgid=pdns
setuid=pdns
version-string=anonymous # 可以写任意字符串恶搞_(:з」∠)_

安装 PowerDNS-Admin

作者有提供详细的教程但是还是有坑

安装依赖:

1
# apt install git python-pip supervisor virtualenv python-dev libmysqlclient-dev libsasl2-dev libldap2-dev libssl-dev letsencrypt

创建数据库,切换到普通用户权限,clone 仓库到本地,然后一步一步操作即可。

1
2
3
4
5
6
7
8
$ git clone https://github.com/ngoduykhanh/PowerDNS-Admin.git
$ cd PowerDNS-Admin
$ virtualenv flask
$ source ./flask/bin/activate
$ pip install -r requirements.txt
$ pip install mysql gunicorn
$ cp config_template.py config.py
$ vim config.py

配置文件 config.py 中需要更改的地方:

1
2
3
4
5
6
7
8
9
SECRET_KEY = 'yoursessionencryptkey'
SQLA_DB_USER = 'yourdbusername'
SQLA_DB_PASSWORD = 'yourdbpassword'
SQLA_DB_HOST = 'localhost'
SQLA_DB_NAME = 'yourdbname'
PDNS_STATS_URL = 'http://localhost:8081/'
PDNS_API_KEY = 'yourapisecretkey'
PDNS_VERSION = '4.0.0'
RECORDS_ALLOW_EDIT = ['A', 'AAAA', 'CNAME', 'SPF', 'PTR', 'MX', 'TXT', 'SRV', 'NS', 'SOA']

然后执行 ./create_db.py。如果没有报错说明数据库安装成功,执行 ./run.py 即可访问 http://127.0.0.1:9393 看到登陆页面了。

部署 Web 服务

直接跑 run.py 当然不科学。Supervisor 配置文件 /etc/supervisor/conf.d/pdnsadmin.conf

1
2
3
4
5
6
7
8
9
10
11
[program:pdnsadmin]
command=/home/pdns/PowerDNS-Admin/flask/bin/gunicorn run:app
directory=/home/pdns/PowerDNS-Admin/
user=pdns
autostart=true
stdout_logfile=/var/log/supervisor/pdns-stdout.log
stdout_logfile_maxbytes=1MB
stdout_logfile_backups=2
stderr_logfile=/var/log/supervisor/pdns-stderr.log
stderr_logfile_maxbytes=1MB
stderr_logfile_backups=2

创建 DHParam

1
2
# cd /etc/ssl/certs
# openssl dhparam -out dhparam.pem 4096 # 如果性能不够请使用 2048

NGINX 配置文件 /etc/nginx/site-enabled/pdnsadmin.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
server {
listen 80;
server_name dns.example.com;

location /.well-known {
default_type "text/plain";
root /var/www/html;
}

location / {
return 301 https://dns.example.com$request_uri;
}
}

server {
listen 443 ssl;
listen [::]:443 ssl;
server_name dns.example.com;

ssl on;
ssl_certificate /etc/letsencrypt/live/dns.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/dns.example.com/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4";
keepalive_timeout 70;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

ssl_dhparam /etc/ssl/certs/dhparam.pem;

add_header Strict-Transport-Security max-age=63072000;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;


access_log /var/log/nginx/dns.example.com.access.log;
error_log /var/log/nginx/dns.example.com.error.log;

location /.well-known {
default_type "text/plain";
root /var/www/html;
}

location /static {
alias /home/pdns/PowerDNS-Admin/app/static;
}

location / {
proxy_pass http://127.0.0.1:8000;
proxy_redirect default;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forward-IP $remote_addr;
port_in_redirect on;
server_name_in_redirect off;
proxy_connect_timeout 300;
}
}

记得把 dns.example.com 换成自己的域名。

签发 Let’s Encrypt。也不多讲。NGINX 配置中已经有了针对 Let’s Encrypt 的续期设置。

然后重启各项服务

1
2
# systemctl restart supervisor
# systemctl restart nginx

查看 PowerDNS-Admin 的运行状态,使用 supervisorctl status

添加 GLUE 记录

要使自己的 NS 生效,必须有保存在上级 NS 中的记录。很多域名注册商都提供了配置 GLUE 记录的功能,例如 Hexonet (1API):

简言之,需要把自己的 NS 服务器及对应的 IP 记录到上级 NS。完成之后,通过 PowerDNS-Admin 添加自己的域名,zone 类型为 NATIVE。然后添加所有 NS 服务器的 A/AAAA 以及所有的 NS 记录——你没听错,要自己写 NS 记录。其他域名也需要添加这些 NS 记录,否则不会托管。

收尾

全部完成之后就是一个完整功能的 DNS 服务了。如果希望启用 DNSSEC,需要在管理服务器中通过 pdnsutil 来添加 key。

由于目前 PowerDNS-Admin 没有限制不能添加提供的 NS 之外的名称服务器,所以其他域名按照添加 GLUE 记录的方法,也可以将这些 NS 服务器「变成」自己的 NS。

好了,不会说话了。讲效果——

一般来说,DNS 服务都会提供多台 NS 服务器域名,将域名的 DNS 改为这些 NS 服务器才能托管到该 DNS 服务上。但是现在只需要知道这套 DNS 的服务器 IP 地址,即可给自己的域名添加 GLUE 记录、NS 记录和 NS 对应的 A/AAAA 记录进而使用自己的域名作为 NS,而不需要用 DNS 服务的 NS 域名。当然一般就是看起来会比较厉害而已…

Phoenix Nemo's avatar

ThinkPad X1 Carbon 2015 与久违的 Arch Linux

对 X1 Carbon 2015 垂涎已久。然而中国市场不提供定制配置、没有 16G 内存的版本、512G PCIe SSD 的型号配置触控屏。这些都不能忍…于是折腾了好久,打扰了至少三位在美国的朋友,最终在黑色星期五当天下单,前天顺丰到手。

先晒一下单。这个配置在联想美国官网购买的原价是 2622.60 美元,黑色星期五的各种优惠后价格是 1630 美元。嘛… 多出来的预算败了一块 1TB SSD 当移动硬盘,真是麻烦帮忙带回国的朋友了(っ‘ω’c)

X1 Carbon 2015 20BSCTO1WW

  • Intel Core i7 5600U 2.6GHz / 3.6GHz
  • 16G (2x8G) DDR3 1600MHz On-board RAM
  • 512G PCIe SSD
  • 2560x1440 QWHD IPS
  • Fingerprint Reader
  • Sierra LTE EM7345

其中 SSD 是 SAMSUNG SM951,测试读写速度分别为 1546MB/s 和 1260MB/sdmidecode --type memory 显示有两条镁光制造的内存,这个倒是和网络上流传的单根内存不太一样。不过两根都是 on-board,所以是无法更换的。

屏幕颜色偏暖,大概是为了长时间工作不会太劳累设定的。ThinkScope 提供了一份 ICC 文件,效果有点偏蓝(紫红?)。不过屏幕型号并不是页面上写的 LG LP140GH1-SPA2,而是 LG LP140QH1-SPB1。总之雾面屏加 86% sRGB coverage 导致它的显示效果比旁边的 MacBook Pro Retina 差了一大截,而且 HiDPI 带来软件上的问题… 嘛这个后面再说。

X1 Carbon 2015 整个机身做工很好,拿着很有份量(不是沉!)给人一种很结实的感觉,完全没有其他 ThinkPad 的塑料感。唯一略心颤的是屏幕没办法像 MacBook Pro 一样稳,和其他笔记本一样调整屏幕角度还是会晃动几下,大概是铰链设计的差异导致。键盘仍然是那个熟悉的手感——哪怕是悬浮式键帽,仍然和老式 ThinkPad 键盘手感不相上下。换到这么舒服的键盘之后打字速度已经快到让 fcitx 分不清键序啦( ´∀ )σ)Д`)其实是 GTK 程序的 bug。ThinkLight 改到键盘下方,可以更好地看清键帽更高大上了,但是没办法当作光源看其他东西… 不过本来也不怎么亮还是爱护下眼睛吧。触控板很舒服,虽然大概还是不够和 MacBook Pro 相比,毕竟人家有底层驱动优化,系统层还支持很多手势。单从手感上来讲还是很不错的。

新本本送到后开机检查硬件,然后当然是要删掉预装的 Windows 10 然后换上 Arch Linux 啦。之前 T420 的 UEFI 安装 Arch Linux 总是启动不能,然而 X1 Carbon 就很顺利。当然是关掉了 Secure Boot 的。

续航感觉还不错,中午送到开箱检查的时候就还有 60% 的电量,然后从下午开始到晚上一直在安装配置系统,到半夜的时候报告还剩 15% 电量。中间有几次编译,所以顺便看了下散热。室内温度大概 10 摄氏度,sensors 检测高负载时最高温度 57 摄氏度,一般工作温度 35-40 摄氏度。这是有点出乎意料的,本来以为超极本的散热都会很悲剧。不过据说其他人也有到过 80 度的,所以要再用用看。

Sierra LTE EM7345 是 microSIM 槽,然而手头只有 nanoSIM 所以还没用上。回去学校把流量卡补成 microSIM 再玩玩看。

安装过程完全不折腾。硬件兼容性报告只有一句话:Everything works out of box.

如果一定要在兼容性上挑刺儿的话,大概是 GPS 只有在 Windows 下才有办法打开,打开之后就可以直接在 Linux 下用了。

回到 Arch Linux 上。一年没用,KDE 已经快不认识了。通过更改 DPI、字体大小和图标大小,Qt5 的程序算是很好的支持了 HiDPI。然而一些 GTK / Qt4 程序就没那么好说话了… 总之用各种奇怪的办法让这些跟不上时代的程序变得勉强能看。

刚开始用 MacBook Pro 不久的时候,除去再也无法接受 retina 之下的显示效果(retina 有毒啊)之外,由于键盘、输入法(!!!)、快捷键、工具链、UNIX 生态等等问题上,OS X 上的工作效率只有 Linux 的 1/3 左右(是真的,很早的时候解决一个问题大概花了一个多小时,后来在 OS X 上再次遇到却花了一个下午)。

总之用来这几天,可以说是非常满意的。没有任何生产商设置的门槛,OS-Specific 的东西通过各种 折腾 Google、折腾 IRC 频道里的各位、折腾 ArchWiki… 等基本都解决了。现在看到它就有想摸一摸、想用它工作的冲动(๑•̀ㅂ•́)و✧然后上个图~

最后的一点牢骚。

一年多没怎么碰 Linux 桌面,虽然看起来变化很大,但是实际上的提升基本没有感觉到。倒不如说,由于硬件技术一直在革新,Linux 桌面却没有了当年的势头。前段时间去 KDE 主页看的时候发现他们把捐赠放到了首页上,而且赞助页看起来好粗糙,完全不像是 KDE 一贯精致的设计风格。从对 HiDPI 的支持来看更是如此。KDE/High-dpi issues 页面从半年前到现在基本没有太大的变化…

嘛。当然了就算是钱多得没处花的某有点软的公司也没让自己的操作系统好好的支持 HiDPI ( ´∀ )σ)Д`)

开源界仍然有大量被广泛使用的应用保持着粗制滥造、works for me 等等的心态。虽然绝大多数不会有人为之付费,但是这并不是不认真的理由。而且这样下去也并不会有多少杀手级的应用出现。

虽然作为 DevOps 的话只要有好用的命令行工具链就万事大吉了。 更何况现在厨子整天瞎搞,果子就算还能继续赚无脑追捧的大众的钱,它还能制作多少 developer-friendly 的产品呢…
(´・ω・`)

Phoenix Nemo's avatar

用优雅的方式在 OS X 中为单个应用设置语言

买到 CLIP STUDIO PAINT Pro,激活之后发现不给启动,显示 Unsupported OS 并退出。搜索了下发现是因为语言设置的问题导致,需要将系统环境设置为应用所支持的语言才能运行。

嘛。日本人做事情也是让人无话可说,那么多应用都没有多语言支持的… 用能支持的语言来显示就好了嘛。

话说回来,我一开始设置的系统语言是简体中文,虽然后备语言加了 English 和日本語,不过有些不能自动变更语言的应用在更换系统语言为 English 之后变得很别扭。于是寻找可以单独设置应用语言的方法。

用惯了 Linux 再用 OS X 其实并没有那么容易的改变习惯… 当我准备尝试单独 export 一份 locale 再运行 app 的时候 OS X 直接告诉我不适用我的表情简直和伊莉雅一样。

睡了一觉起来继续 Google。找到了可以单独为应用设置语言并且 launch 的 app Language Switcher,看起来不错,但是每次启动 CLIP STUDIO PAINT 都要先打开这货,这不是我想要的效果。

于是最终找到一个合适的解决方案:用 defaults 命令。

原帖在这里

设置 CLIP STUDIO PAINT Pro 语言环境命令:

1
defaults write jp.co.celsys.CLIPSTUDIOPAINT.lip AppleLanguages '("en-US")'

其中 jp.co.celsys.CLIPSTUDIOPAINT.lip 可以在应用的显示包信息 -> Contents -> Info.plist 中找到。

执行后就可以直接双击启动啦~

最后献丑一张w

Phoenix Nemo's avatar

末字幕组时代,我们何去何从

日本のアニメやドラマを勝手に翻訳して字幕を付け、違法配信する中国のアマチュア集団「字幕組」。北京在住のメンバーを取材してその実態や動機、違法性を伝えたところ、中国のネットメディアが本紙記事を翻訳、「字幕組がなくなれば抗日ドラマに洗脳される」などと擁護の声が殺到した。

这是好久之前出现在日本各大新闻站上的文章。今天偶然和朋友闲聊时再次聊到了版权问题。

前些时间开始,射手网、人人影视、极影等字幕发布站相继被 takedown,相关律师函甚至恐吓信开始出现在站长们的邮箱中。在中国,「版权」第一次闹得这么凶。然而可悲的是,这并不是因为中国人开始有了版权意识,而仅仅是影响了一些商业集团的利益。

在近期某字幕组发布的作品中,发布者这样写到:

現今,版權的世代到來
觀眾也都紛紛的看版權去了
這對我們來說不是什麼問題,因為我們本來就不在意什麼載量
做字幕本來就是自爽的,與其說是做給別人看,不如說是做給自己看
對字幕界比較大的問題其實是新人減少這件事

從這幾天MANGLOBE(動畫公司)倒閉事件來看
看似盛況的MAG(漫画、アニメ、ゲーム)也明確的顯示出,其實是正在走下坡的
但是國外購買版權這情況,是有助於動畫公司轉虧為盈,不再只是依靠BD賺錢
我們理所當然的是要感到高興
這絕對絕對不是壞事,有愛支持正版是對的

但同時我也希望各位,如果真的很喜歡某片的話
在經濟允許的情況下,買個BD原盤支持一下吧,因為並不是每個片子都有版權購買(AMAZON用郵局的VISA卡就能買囉)
請把口中的支持化作實際的支持吧,良好的作品不該讓它沈入水中

另一方面
由於版權大量崛起,似乎可以望見字幕組的數量會越來越少
有所謂的版權可以看後,也會大幅降低新人願意加入字幕組的意願,因此製作字幕的人會越來越少
最後,支撐著字幕組的人只剩下老人
工作,結婚,後宮,家庭,最後連老人們都各奔東西(現在越來越坑就是因為現實的干擾越來越多
接著就化作了時代的眼淚

曾经在早些年,在大陆的视频站大部分仍然只是互联网上用于大家互相上传视频自娱自乐的时候,并没有某相关部门令人无力吐槽的审查和胡搅蛮缠,日本动画也并没有那么大规模的影响力和市场。字幕组的相继出现,让国内的观众们能够看到制作精良的日本动画,也促进了国内 ACG 圈子的发展。更重要的,字幕组将日本的二次元文化、包含其中的人的素养,人性的光辉和思考,带给了越来越多的人。

在这个时候应运而生的发布站,整合了多个字幕组的作品给观众挑选,也直接加剧了字幕组之间的竞争。经过一段时间的发展,字幕组的工作流程越来越优化,翻译和字幕的质量越来越高,甚至做到了日本电视台首播后两三个小时就可以制作出精美字幕的程度。

字幕组的百家争鸣慢慢引起了商界的注意。好像就在一夜之间,各大视频站都开始争相购买日本动画的版权。这本来是一件好事,但是相关部门的审查、过长的广告等等问题引发了观众们的不满,他们开始将发布站和视频站推向一个对立面,微博上甚至出现了「视频站不给看,我们去xxx下载!」类似激进的言论。然而与此同时,某圈内朋友表示:

国内所有视频网站我都是VIP
我觉得体验还不错
就翻译有待加强

字幕组的建立,有些是因为兴趣爱好,也有些只是随口说出的玩笑。但是共有的一点,都对二次元文化充满了热爱。很多组员看到了喜欢的作品会攒下生活费购买蓝光盘用于收藏,根本不会拆封,只是为了支持一下制作公司而已。但是从法律上来讲,字幕组的行为依然不能被接受。

我们推动了动画在国内的推广,但是并没有实力给制作公司分钱。
所以是我们退场的时候了,让专业的来吧。

所有人都已经看到了残酷的现实。曾经为了兴趣抑或只是一句玩笑建立起的小组,一路走来承载着众多组员们的羁绊,转瞬间已经到了并不华丽的谢幕。

也许会有字幕组和发布站继续存在下去,但是更多人会选择在线观看视频站购买的正版动画。虽然并不是中国人的版权意识提高了,但是至少,视频公司不管是通过广告还是销售会员获得的利润,可以用来继续购买更多的版权动画给大家看。对动画制作公司,对观众们,都不是坏事。

说到底,大部分字幕组,只是为了大家能在一起玩得开心罢了。字幕只是让大家聚到一起的载体,就算没有再继续做字幕,这些「字幕组」之名也可以让组员们继续走在一起吧。想必,这段可能仅仅存在于近几代人中的记忆,会被小心翼翼地保存一生。

R.I.P

Phoenix Nemo's avatar

尝试迁移到 AWS Cloud

发了个帖子抱怨服务器一大堆却没个放自己个人站的地儿。其实是服务器都是生产环境要跑各种业务,不能放自己的东西。其实个人站就一个博客一个知识库,还都是静态的,连买个 Linode 都觉得资源浪费。放在 GCE f1.micro 上吧,是便宜了不过网络抽死。如果不是因为要用 SSL 防运营商劫持和中间人攻击,真的直接扔 GitHub 了。

感谢 @sparanoid 的建议,花了一下午尝试将自己的静态网站部署在 AWS 云上。

AWS 准备工作

S3 文件桶

创建一个 S3 文件桶,并在「属性」 -> 「静态网站托管」页选择「启用网站托管」,然后索引文件填写 index.html

在此页面会得到一个网站终端节点,例如 example.com.s3-website-ap-northeast-1.amazonaws.com 这样。先复制备用。

上传 SSL 证书

其实之前签发了一张三年的 ShinoSaki ECC 证书,然而 AWS 不认(ノ=Д=)ノ┻━┻ 只好去签了 AlphaSSL 的泛域名。

需要安装 awscli,可以直接 sudo pip install awscli

上传证书命令如下(示例名称记得改成自己的哦)

1
aws iam upload-server-certificate --server-certificate-name cert-name --certificate-body file://example_com.pem --private-key file://example_com.key --certificate-chain file://ca.pem --path /cloudfront/yoursite/

前面的 file:// 也不能移去,否则会报错 A client error (MalformedCertificate) occurred when calling the UploadServerCertificate operation: Unable to parse certificate. Please ensure the certificate is in PEM format.

创建 CloudFront Distribution

如果是 Jekyll 一类会创建「文件名.html」的程序,origin 直接下拉菜单中选择 S3 的文件桶地址即可。但是 Hexo 和 MinoriWiki 都是目录名下的 index.html,所以只能使用之前得到的网站终端节点。填入地址后,下面的配置随自己的想法选择即可。Alternate Domain Names (CNAMEs) 填写自己的域名,然后下面的 SSL 选择 Custom SSL Certificate (example.com) 和一个对应的证书。Default Root Object 填写 index.html

为什么只能使用网站终端节点而不是 S3 地址的原因是 CloudFront 和 S3 的 Default Root Object 工作原理不同,如果是请求子目录,那么 CloudFront 不会给默认加上 index.html 导致 403。

创建之后 CloudFront 需要(很长一段)时间来部署。于是先把分配的 CDN 地址 xxxxxxxxxxxxx.cloudfront.net. 复制备用。

设置 Route 53

建立一个 Route53 Hosted Zone,然后导入以前的 zonefile 或者什么的。网站的地址 Type 选择 A - IPv4 address,然后 Alias 选择 Yes。Alias Target 是刚才复制的 CloudFront 的 CDN 地址。Routing Policy 选择 Simple——GeoDNS 将交由 CloudFront 完成。

网站程序准备

Hexo

Hexo 已经有了现成的 S3 deployer,不过主页上的那个不工作也不管别人给提交的 PR。所以推荐使用 hexo-deployer-aws-s3

不过貌似是会强制覆盖每个文件的,上传花了好久啊比 Git 慢多了… S3 啥时候支持 Git (ノ=Д=)ノ┻━┻

MinoriWiki

MinoriWiki 从一开始就是面向 UNIX 系使用者的一套超简单个人知识库,所以压根没考虑用 S3 啊(ノ=Д=)ノ┻━┻

不过自己写的东西好处就是可以各种改(死)所以半个下午发布了一个带有 S3 支持的新版。使用了 node-s3-client 库,可以只上传修改了的文件。

搞定

一路搞下来,CloudFront 也就差不多了,域名解析应该也更新了。试试看效果吧~

没啥访问量的个人站,用 Route53 + S3 + CloudFront 的开支一个月可以控制在 2 美元左右,但是性能、网络和可用性远远比一个月 2 美元的 VPS 好得多。

然而在国内… AWS 被滥用还是挺凶的。所以各种服务被干扰,于是成了现在这惨样。

其实还不如直接托管在 GitHub 然后上个支持自定义 SSL 和 PAYG 的 CDN

Li Fanxi's avatar

十年(中)

这文章已经快烂尾了,努努力,先补个中段出来。

TL;DR

其实我已经不太想写后面的内容了,写这篇文章时我已经仔细地想过我所希望在文章中表达出来的内容。总结到最后,我觉得这十年中让我自己受益最大的一点就是:突破自我,走出舒适区,积极、主动地多走一步。

对于我自己来说,即使到现在,我也常常很难主动去打破自己的舒适区,积极地多走一步。但是回首过去的十年,但凡我多走了一步,无论是主动还被迫,最终都得到了超越预期的结果。

这里说的多走一步,还不仅仅局限于自己的事。在跟别人合作的时候或者打交道时,多走一步,或者帮助别人多走一步,都是非常有益的。

2009年

产品维护的工作比想象中的更难做,尤其是换到了一个全新的产品线后,对于一个完全不了解的产品,需要同时去给客户解决多个平台上N个版本产品中出现的各种疑难杂症,实在是一件非常有挑战的事情。

有两个Case印象深刻:

  1. 有个Case我完全无从下手,只好开始“忽悠”,说了自己的一些怀疑点,让核心技术支持部门协助客户去收集更多的日志,趁收集日志的时间我就可以在后台更进一步地分析问题。有一天突然就发现,美国开发团队以前开发这个产品的一位Principal Engineer(首席工程师?反正级别很高,是个传奇人物)跳进了这个Case。他不但详细的分析了问题的原因,给出解决思路和方案,还“顺手”把代码改好、打好补丁包。后来我向他致谢的时候,他说:这个产品就像是我的孩子,我有责任把它照顾好。
  2. 有位客户发现产品界面上的CPU占用率曲线在产品连续运行了两个月左右后就会显示成持续占用100%,但实际CPU占用率并没有这么高。我从前台Java Script一直到后台程序及内核代码全部分析了一遍以后终于发现,是因为程序中用32位无符号整形数来表示开机后到当前时间的毫秒数,所以每过49.7天,这个整形数就会溢出,造成问题。我修掉了这个问题,并且定制了一个Linux内核,可以在启动时指定uptime的初始值用于快速重现这个Bug,把Patch和这个测试用内核一起提交给QA。QA后来表示非常感激,因为如果没有这个测试用内核,要Mock出一个很大的uptime值是一件很麻烦的事情。

这两个Case说的都是Ownership,说的都是怎么在尽职的前提下再多走一步。在一个人人都很优秀的环境下,“尽职”只能算是一个基本要求,想要有额外的收获,你必须有主动的额外的付出。

有了前两年“脱宅”的积累,2009年全年的每个休息日我基本上都在南京城里逛来逛去,与南京这座城市的感情也越发深厚。尤其是在经历了一些事情后,让我对人的情感有了更为深入的理解,这对我后面的生活产生了很多积极的影响。很多事情,即使不能多走一步,也应该多想一点。多想一点,也许就能让一个原本优秀的人变得更为卓越。

除了没事逛来逛去,我也把很多很多时间放在了南京图书馆。这一年看了很多闲书,包括铁路、艺术、建筑、地理、文学、数学、摄影、设计等领域。现在回想起来,真是怀念这段时间,可以肆无忌惮地把大块时间花在“不务正业”的事情上。看了这么多闲书,并不可能帮助我在这些领域中有所建树,但是对于我来说,人生的乐趣就在于不断地学习、不断地探索未知。有时候,时间就是用来浪费的。

2010年

我的第三位Manager是一个爱憎分明的人,我有幸成为他比较喜欢的下属之一,所以他给了我充分自主发挥的空间,也为我争取了很多很多的机会,甚至为了帮我争取一个去美国参加会议的机会,“强迫”另一位Manager退掉了去美国的机票,把参会的名额留给我。当然,更重要的是他在工作中给我创造的机会和对我的各种想法的重视和支持。那时公司也很鼓励微创新,我的各种微创新在他的帮助下都得以在团队内或产品中落地生根,这给了我极大的自信,也帮助我在部门内找到了属于我的位置。

机遇和挑战总是并存的,在这个开发团队,我负责了这个产品两个大版本中最重要的两个大Feature,从前期方案调研到最后开发实现,统统要搞定。还是得益于公司完善的流程和管理, 这两个版本软件做完以后,我完整了实践了两遍软件从需求到发布的完整流程,从中学到了很多的东西。同时,这两个Feature也涉及到很多跟美国开发团队以及国外一些第三方公司的打交道的事,所以又进一步提高了我跟老外们打交道的能力。这一段经历,不但让我在技术上有所收获,更重要的是让我学到了做事的方法,很多事情并不会自动朝着你想象的方向去发展,怎么才能推动事情往前迈步,既需要努力,也需要很多的技巧。其中最基本的技巧就是要合理计划、分解任务制定小的里程碑、踏实实践

这一年的业余时间花了很多在自由软件上,更多的参与到一些简单自由软件项目中去。再次提起Ownership一词,在自由软件世界里,你可以既是软件用户,也是软件的主人。当你以软件主人的心态去使用自由软件时,才会真正理解自由软件背后的理念,也才能从中收获更多的价值。

 

jaseywang's avatar

Big transaction and MySQL replication lag

This saturday afternoon I was on call, and got a alert, one of our production MySQL slave begin to have lag like this:

After some time reviewing the monitoring metrics, I got the big breakthrough, there existed large spike of lock structs and delete operations during the lag period:

Usually, this was caused by big transaction on master node. The verfication was quite straightfoward, just analyze the binlog, the script was copied from percona.

After running the script, I do really found something astonishing, during that time the master had one transaction per minute, and each was comparatively big, nearly 10k delete operations for one transaction:
Timestamp : #160904 13:26:54 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7559 row(s) affected
Timestamp : #160904 13:27:56 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7726 row(s) affected
Timestamp : #160904 13:28:57 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7864 row(s) affected
Timestamp : #160904 13:29:59 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 8039 row(s) affected
Timestamp : #160904 13:31:01 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7961 row(s) affected
….
Timestamp : #160904 13:33:05 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7985 row(s) affected
Timestamp : #160904 13:34:06 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7849 row(s) affected
Timestamp : #160904 13:35:08 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7927 row(s) affected
Timestamp : #160904 13:36:10 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 8014 row(s) affected
Timestamp : #160904 13:37:11 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 8031 row(s) affected
Timestamp : #160904 13:38:13 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 8104 row(s) affected
Timestamp : #160904 13:39:15 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7730 row(s) affected
Timestamp : #160904 13:40:16 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7668 row(s) affected
Timestamp : #160904 13:41:18 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7836 row(s) affected
Timestamp : #160904 13:42:20 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7757 row(s) affected

Timestamp : #160904 13:48:29 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7835 row(s) affected
Timestamp : #160904 13:49:31 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7839 row(s) affected
Timestamp : #160904 13:50:32 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 8028 row(s) affected
Timestamp : #160904 13:51:34 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 8223 row(s) affected
Timestamp : #160904 13:52:35 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 8119 row(s) affected
Timestamp : #160904 13:53:37 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7924 row(s) affected
Timestamp : #160904 13:54:38 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 2139 row(s) affected
Timestamp : #160904 14:17:10 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 5592 row(s) affected
Timestamp : #160904 14:18:12 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 8260 row(s) affected
Timestamp : #160904 14:19:13 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7876 row(s) affected
Timestamp : #160904 14:20:14 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7994 row(s) affected
Timestamp : #160904 14:21:16 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7994 row(s) affected
Timestamp : #160904 14:22:17 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 7813 row(s) affected

Timestamp : #160904 18:15:27 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 8191 row(s) affected
Timestamp : #160904 18:16:27 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 8180 row(s) affected
Timestamp : #160904 18:17:27 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 8304 row(s) affected
Timestamp : #160904 18:18:28 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 8128 row(s) affected

If we compare the transaction of other time period, we can get the result, below is the top 10 transaction of Sep 2 which is far less than 10000:
Timestamp : #160902 18:00:31 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 850 row(s) affected
Timestamp : #160902 10:00:33 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 813 row(s) affected
Timestamp : #160902 8:00:10 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 786 row(s) affected
Timestamp : #160902 10:55:42 Table : `qmq_backup`.`qmq_log_records` Query Type : DELETE 646 row(s) affected
Timestamp : #160902 12:30:01 Table : `qmq_backup`.`qmq_log_records` Query Type : INSERT 608 row(s) affected
Timestamp : #160902 12:30:01 Table : `qmq_backup`.`qmq_log_records` Query Type : INSERT 606 row(s) affected
Timestamp : #160902 19:07:37 Table : `qmq_backup`.`qmq_log_records` Query Type : INSERT 599 row(s) affected
Timestamp : #160902 19:10:09 Table : `qmq_backup`.`qmq_log_records` Query Type : INSERT 582 row(s) affected
Timestamp : #160902 19:07:23 Table : `qmq_backup`.`qmq_log_records` Query Type : INSERT 575 row(s) affected

That explains why io thread works well but still have big lag, since by default slave has only single thread to process all including big transaction from master, it can't handle that and break.

What's next? find which service cause that big transaction, back-flush from the DB log, there do really have a job that purge the old data every minute like this:
[2016-09-04 14:21:53  INFO com.benmu.rapidq.backup.backup.task.HistoryCleaner:?] clean history qmq_backup_messages data by condition create_time < '2016-08-20 14:21:53', total clean: 892, elapse: 60 ms

After the discussion with developer, we decided to stop the job. Besides that, we consider to increase the thread(slave_parallel_worker) of slave since we currently use MySQL 5.7 which is its big selling point.

jaseywang's avatar

Nginx 200 response with empty body by double slash

We setup a new cluster of Nginx to send requests to HIS using curl with domain name as the Host header, unfortunately, we get 200 return code but empty body like this:

When we use IP as Host, it works well, and return the expected result:

After some debug, nothing found and tcpdump comes:

From the tcpdump, the above red arrow uses domain name as Host header and the below one uses IP as Host header, we see there exists a double slash when using domain name.

After some review of Nginx config file, we found the misconfiguration location:

alswl's avatar

API 集成测试实践

为了提高测试,工程师需要对自己提交的产物进行测试,一般是单元测试、集成测试。 之后提交物流转到 QA 团队,QA 团队根据需求描述对提交物进行测试, 这个测试过程非常耗费人力。 尤其是当开发交付的质量不高时候,很可能自身没有经过测试,会遇到主干流程都无法进行的状况。

如果在 QA 人工介入测试之前,就进行一轮黑盒自动化集成测试,可以大大地提高 QA 团队的工作效率。 基于这样的判断,我们团队花了一些时间,将基于 API 的自动化测试系统搭建起来。 现在将这个系统的选型和运行状况拎出来,和大家分享。

确认测试范围、目标和意义

  • 范围
    • 后台输出的 API 级别 URL
    • 使用场景
      • 打包时候的冒烟
      • Dev / QA 手工添加添加新特性用例
  • 目标
    • 覆盖大部分的 URL,当期设计为 top 10 URL,仅包含 GET 接口
    • 选型时,需要考虑非幂等(POST / DELETE / PUT)等接口
  • 意义
    • 提高开发效率,一种自动化的 IT 测试方案
    • 提高测试效率,减少人工集成测试成本
    • 提高工程质量,通过覆盖率提升,保证工程质量逐步提升,放心开发新功能

特性需求

选型一个系统,不是看市面上有哪些可以供选择,而是看我需要什么样特性的一款产品。 如果自己的需求和市面上的现成产品差异过大,也可以考虑自己定制。

  • Required
    • 开源
    • 免费
    • 使用 DSL 或者简单代码描述测试用例
    • 支持细粒度的单 API 测试和构建带过程的测试用例
    • HTTP API
  • Optional
    • CI 集成
    • UI

挑选出来的选型和评价

这部分工作,是和团队的其他成员一起去看的,大家各自分头寻找一些产品,然后进行评测,给出结论。

经过讨论,我们将重点关注放在这么几款下面:

搭建 demo,进行试用

在确定选用那几款产品之后,就可以集中精力在几款候选者里面。搭建相应的环境,对他们进行实际测试。

supertest:

  • 功能太简单了,简单到几乎可以自己写掉,不算一个 test framework

pyresettest:

  • 哈哈哈,YAML based,dreamed feature
  • 支持 YAML / extractor / validator
  • 天生支持 host 为参数
  • create for me!!!

hippie-swagger:

  • 在使用上,和 supertest 差异不大
  • 仍然需要自己定义,在 swagger 描述文件不存在时候会抛错,描述文件不符合时会抛错

robotframework:

  • 较为复杂
  • 有 YAML 了,不用试了

使用感觉

经过一个季度的试用,我们基于 pyresttest 的项目 abao 运行较稳定。 尽量在工程师提交代码之后,运行一次,从而可以在早期发现问题。

由于是基于 Python 的源代码,我们还给 pyresttest 开发了几款插件:

  • cookie_extractor:用来解析特定的 cookie
  • file_choice_generator:从文件随机选择预设数据
  • file_seq_generator:从文件顺序选择预设数据

在和 CI 的配合方面,我们在 Jinkins 搭建了 abao / abao-master 项目, 前者响应每次 Push 请求,都会自动构建一遍,后者每天凌晨会将 master 运行一遍。

感谢项目贡献者:

project  : abao
 repo age : 5 months
 active   : 32 days
 commits  : 109
 authors  :
    39  Chery.Peng  35.8%
    33  3D          30.3%
    17  yanqi.chen  15.6%
    11  橙子        10.1%
     7  fiona66     6.4%
     2  雪糕        1.8%

参考文档


原文链接: https://blog.alswl.com/2016/08/api-integration-test/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
Li Fanxi's avatar

十年(上)

上周末是参加工作十周年,回南京与同一年进入公司的老同事们聚了聚,很开心,也有颇多感想。

对于我在Trend Micro的第一份工作,一直以来,我是心存感激的。从学校走向社会,从稚嫩变得慢慢成熟,回头去看,有很多人、很多事都值得感激。

总结一下过去的十年,顺便熬一锅心灵鸡汤。

2006年

离开学校,走进公司。趋势科技应聘经历里提到的那位在一面时给了我很多自信的面试官成了我的第一位Manager。放到现在,也许我不会说事无巨细、无微不至的Manager是一位足够Professional的Manager,但是作为职场新人,这样的Manager给了我足够的帮助。他对我的信任、给我提供的机会和帮助,都为我的成长提供了一个良好的环境。后来,我做过新人的Mentor、也带过小项目,践行了很多从他身上学到的东西。

公司提供的培训,帮助我理解了Development Process、Design Documents、Technical Writing、Internationalize & Localization、User Centered Design、Writing Secure Code相关的知识,并在短时间内都在工作中得到了实践,这些经验一直受用至今。虽然互联网公司并不适合照搬传统软件公司的开发流程,但是很多方法思路依然值得借鉴。

技术方面,虽然1999年我就装过玩过Linux,但一直到工作前对Linux都还处于基本不懂的状态。工作的前半年,我学习了Linux下的各种开发工具,从无到有独立完成了产品中一个GTK程序的开发,学会了修改了内核模块的代码和内核调试的基本工具。后面五年中用到的各种知识和技能中的一大半都是在这半年中积累起来的。学东西最快的途径就是看书加实践,学而不思则罔,学而不实践则很难真正把书上的东西变成自己的。

我参与开发的第一个产品ServerProtect for Linux 3.0(图片来自网络)

公司让大家考了托业(TOEIC),并开始上英语课,还推行了一段时间英文会议。于是原先只出现在邮件交流中的各种“洋泾浜”英语,开始出现在日常的会议中,挂在了每个人的嘴边。这些并不完美的英语,却真正的帮助到了我,从这以后,基本上我就摆脱了“哑巴英语”的困境 。

英语课上,老师推荐了一部电影《Dead Poet Society》,在这部电影里,我看到了自己的影子,这部电影让我改变了很多。以Carpe diem的理由,我开始努力摆脱曾经自己给自己定下的条条框框,去尝试更多的“不可能”。

2007年

年初,我的Mentor也许是因为偷懒,让我帮他去上原本属于他的Effective Presentation培训。没有想到,短短一天培训中学到的一些基础的演讲技能、对“紧张”的正确认识以及对演讲前充分准备的重要性的认识,就帮助我从一个不敢在公开场合发言的人,变成了一个不太惧怕公开演讲的人。培训导师通过理论和实践帮助我建立了自信。

为了开源产品中的内核模块,我深入理解了GNU工程及自由软件哲学,研究了各种自由软件/开源许可证,并开始积极参与到自由软件社区中。

为了实践Customer Insight,生平第一次出差、第一次坐飞机。我一直以为那时自己做的产品其实没有太多实用价值,当看到自己的产品在客户的环境中真正发挥着作用时,自豪感优然而生。

因为“Carpe diem”,这一年,我的行为举止发生了很多变化。发展了不少的小众爱好:暴走火车地铁无线电,并因此结交了几位挚友。自己去参加CSDN的技术大会,开始关注Web 2.0。通过开发“豆饭”,开始与互联网开发结下不解之缘。通过参与UCDChina的活动,与本地互联网公司的设计师们建立了良好的关系。我的社交圈子在这一年中,扩大了很多,并且很多时候都是我自己主动出击,这在以前,我是绝对做不到的。

2008年

由于参与开发的产品已经在前一年完成并发布,工作角色从产品开发转为产品维护。这段日子是我的工作经历中最为灰暗的日子。首先,产品维护需要在有限的时间内解决客户的问题,我在学会了“沟通技巧”和“做事方法”的同时,放弃了对技术精益求精的追求。为了降低Resolution Time,我学会了忽悠,而不是去寻找Root Cause。其次,新的Manager强调学习做事方法高于技术追求,他让我在各种挫折中自己去学会在职场中生存下来的技能,但是各种挫折不断地打击着我刚刚积累起来的一点点自信心。

然而,这种种不如意,却只是成长的代价。我这一年的技术积累没有前两年那么多,但依靠那些被逼迫着学到的职场技能,加上各种机缘巧合,反而倒奠定了我在团队中的地位。然而“捷径”只是加速达到某些目标的一种手段,真正起作用的,依然是踏踏实实的每一步。

公司开始强调文化建设,难听的说法叫“洗脑”。也许是那个时候还比较Simple和Naive,这次文化建设的培训让我受益非浅。直到现在,我依然认同那时被灌输的思想:你需要让你个人变得更加优秀,才能发挥出更大的价值。

参加了公司的Home Building Project,这是公司与一个公益组织Gawad Kalinga合作进行的公益项目,去菲律宾盖房子(这个翻译其实不对,这是Home Building,不是House Building,盖房子只是一种行为,目的是帮助那里的人们拥有更美好的家)。第一次走出国门、第一次与来自不同国家和地区的同事合作、第一次与这么多小朋友近距离接触、第一次与生活在贫穷与自然灾害面前的人们面对面交流。过去了这么多年,当初的震撼已经慢慢的淡去,但留在内心深处的东西,也许会影响一个人的一生。

Home Building Project的合影

那时公司组织各种活动很多,加上那段时间我自己也参与了不少线下的活动,从中自己积累和总结了很多组织线下活动的技能。3月底自告奋勇为当时的Unix-Center网站成立一周年组织了南京地区的线下活动,活动虽然规模不大,但很成功,甚至还吸引了上海、无锡等周边城市的自由软件爱好者。这次活动也为后来成立Nanjing Linux User Group埋下了种子。

Unix-Center周年庆

alswl's avatar

搞定暴涨的流量

2013 年左右,我司业务发展迅速,每天晚上都会面临服务器濒临崩溃情况。 我相信每个高速发展的互联网企业在某个阶段都会面临这样的情形,比如去年爆红的「足迹」。 过程往往是:线上出现故障,手机会收到报警,然后登录到服务器上去解决问题。 处理这种问题工种现在有一个时髦的名称,叫做「SRE(Site Reliability Engineer)」系统可用性工程师。

虽然我常常救火,但是我还是想尽可能避免线上发生故障。「最好的消息,就是没有消息。」 减少故障出现概率,增强系统可用性,降低故障处理时间是 SRE 的最大课题。 在这里有最常用的两个手段,一个是优化性能,一个是做好容量规划和扩展。 这里我着重讨论后者「容量规划」。

^ 看我的一堆报警消息

面临的问题

面对暴涨流量,一边是业务方的满心欢喜,一边就是工程师的烦恼和压力了。 也许是一个受欢迎的功能上线了,或者是某个社会活动导致流量爆发,系统开始出现高延迟,磁盘 IO 不够用了。 也许是 DB 第一个倒下,也许是 RPC 系统第一个倒下…… 呃,大神可能会说,我艹,RPC 系统第一个倒下还搞个屁啊,赶紧倒闭算了。

核心的问题就是,在现有性能下面,在面临可能的大流量冲击时候,如何做到不慌不忙,能够 handle 住突如其来的流量?

设定容量目标

在解决这个问题之前,我们得先考虑清楚,我们到底要多强的流量处理能力。 如果今天我们只是一个两三台服务器的小团队,却企图设计一个能够抗住 1 亿 pv 访问的系统, 显然是不现实的,至少是不经济的。

衡量系统容量的指标可以简化为在什么流量下面,提供什么样的可用性保证。 一个实际的样例是,在 1 亿 pv 下面,提供 99.99% 的可用性, 其可用性的评判标准是「服务器在 200ms 内返回正确的数据」。

这里有一个重要的概念,可用性保证,术语是服务等级协议(SLA)。 这个指标可以从大部分标准云供应商的标准条款里看到,比如我司机房供应商提供的可用性保证是 99.9%。 阿里云 ECS 的 SLA 是「99.95%」,统计周期是 1 个月 (如果故障时间低于 5 min,不计入故障时间,云供应商都这样,特别霸权)。

一个对 SLA 的直观认识是(具体数据来自 High availability - Wikipedia, the free encyclopedia):

  • 99.0% 意味着一年有 87 天不可用
  • 99.5% 意味着一年 1.83 天不可用
  • 99.9% 意味着一年 8.76 小时不可用
  • 99.99% 意味着一年 52.56 分钟不可用
  • 99.999% 意味着一年 5 分 15 秒不可用,这是高可用的一般标准

设定越高的 SLA 的成本越高,具体 SLA 的设定是成本、收益、期望的平衡。 不同的业务需要的 SLA 也不一样,一般认为 99.9% 基本可用,99.99% 可用性较高, 99.999% 为高可用。

有些云供应商号称 8 个 9,9 个 9,那往往都是对于存储服务里面的数据不丢失这个指标。 除了忽悠忽悠人,这个 SLA 没什么用的。

测量

做一件伟大事情时候,先有目标,下一步如果是迈出脚步出去闯荡,那么往往换来的是一个身心疲惫的自己。 更稳当的做法是,先摸摸清楚,自己有几把刷子,是不是还要再练练,有没有资格上战场。 没有 Profiling,就是瞎子,根本不用谈优化和容量规划。

对于一般的业务场景而言,常见的测量指标分为三类:

  • 服务器的硬件指标(CPU、内存、硬盘 IO、硬盘容量、网络)
  • 服务的软件指标(QPS / latency / pool)
  • 业务的数据指标(核心业务指标,比如注册数,核心动作次数)

我司的实践情况是这样的,我们使用 Zabbix 测量服务器,用自己设计的系统收集服务数据,使用 Grafana 呈现。 后者被设计到 RPC 系统内部,数据是全量收集。 我司在业务层面的数据监控做的还不足,这种不足不仅仅体现在数据的全面性上面,还体现在相关成员(比如产品汪)对数据的利用率上面。

除了测量线上的实施数据,了解某个设施的性能极限也是很重要,目前常见的测量方式是:

  • 模拟流量进行测试
  • 在线上进行测试,并实时跟踪进展情况,出现异常时候,停止流量切入
  • 从线上引入流量到测试环境进行测试

我发现,第一种方法往往不准,第三种方法对于小团队来说,成本太高。第二种方法是最粗暴和有效的。

预警和提醒

仅仅知道当前系统的性能表现是不足的,重要的如何将这些数据利用起来,对未来系统增长进行预估。 流量增长 vs 资源消耗,这个曲线大部分情况是线性的,有些情况确实指数增长的。

常见的做法是,给核心指标设置一个阈值(比如 80% 磁盘使用率,40% 磁盘 IO 利用率),当监控的数据到达这个阈值时候。 就必须进行容量扩充,进行负载均衡。

一个从运维同学身上学到的是,提前采购一些设备放到机房里面,比如硬盘、内存,别到时候供应商来不及供货。 必要库存可以降低 MTBF。

除了设定阈值报警,应当定期跑一些脚本获得数据。定期检查报警系统,避免报警系统失效。

必选项 - Scalable

上文写到,「必要时候进行容量扩充,进行负载均衡」。 这点的提出,意味这需要保证基础设施是可扩展的,支持负载均衡,支持硬件扩容

Web 系统比较容易做到横向扩容,使用 Nginx / LVS 等负载均衡即可。 中间件服务一般也是在设计时候就考虑了扩展。(什么?你们家 RPC 系统设计调用不支持扩展?什么脑残设计?!)

DB 级别的服务,往往就要花一些心思了,一些技术(比如 MySQL)想要做到横向扩展, 需要进行提前设计。而一些设施虽然容易进行扩展,比如 ES / Kafka 等现代化设施, 但在部署的时候仍然要进行一些提前准备。

除了提前做好 Scalable,还有几个和部署相关的 tips 可以供参考:

  • 使用工具:自动化部署,现在有太多工具可以供选择,比如 ansible 就是一个很好的工具
  • automatic everything:避免登录服务器操作才能保证未来自动化
  • 工程化:用最佳实践去维护部署系统,用工程化的态度去写部署代码
  • 保持同质,避免花样:避免使用 shell 级别的操作原语操作部署系统,使用预设的 module 去操作

End

好了,现在去预测一下当大流量来临之际,你的服务会在哪些环节失败。 想不出来的话,就一点点去测量各个环节性能,然后做一把容量规划吧。

调优和增加容量,这是两个手段,这两个手段互相作用,互相影响。使用时候需要根据成本和收益进行选择。

关于容量规划的更多细节,可以看看 Web容量规划的艺术 (豆瓣) 这里看看。只是这本书写在 2010 年,并且作者介绍的过于传统运维视角一些。


原文链接: https://blog.alswl.com/2016/06/capacity-planning/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
alswl's avatar

一次「分答」记录

尝试在团队内部发起一次类似「分答」的沟通方式,一对一面聊。 回答其他工程师的问题,这种沟通的方式暨在提供一个特定的场合,帮助加强双方了解, 解决团队中其他工程师的一些实际的问题。

在征得对方同意之后,我将他的问题和我的回答 PO 出来。

  1. 喜欢什么游戏,玩游戏的问题
    • 喜欢和人玩游戏,不喜欢和不认识网友玩游戏
    • 小学从红月开始玩,传奇、CS、魔兽、真三、DOTA,大学时候和舍友玩游戏,毕业后不玩
    • minecraft 尝试着玩过,没有玩下去,找不到人
    • 日活的桌面游戏,三国杀等蛮喜欢
    • 不喜欢浪费时间,怕没控制
    • 从玩游戏里面学习什么么?不,就是纯粹享受游戏,不会想这么多
  2. 喜欢什么语言
    • 看场合,小东西,小场景用 Python,生产环境用 Java,生产环境又有时间,考虑 Scala
    • 只允许选一门语言,就用 Python
    • 最吸引的特点,熟悉程度高,生产效率高,第三方库丰富,粘合性强
    • 为 Python 做一些功能扩展?不同时期答案会不一样,目前来说,希望有一个开发效率更高的 framework;有时间的话,会考虑如何绕过底层 GIL 问题
  3. 有什么事情投入精力,铩羽而归
    • 工具向重的人,推广 git 花了两年,推广 Restful 不利
    • 做业务开发 leader,和 tsu、小管一起做,项目管理、团队管理没做好,没做好,有挫败感。
  4. 职业迷茫期
    • 我作为一个学渣,并且长期以来没有人给予职业生涯规划,一直对自己未来发展有困惑
    • 如何在无领导的情况之下,探索未来道路,规划自己工作内容?跟优秀的人聊,跟外部的人聊,看博客,看书
    • 迷茫是客观存在的,并且可能持续存在很久,正视它
    • 现实中一定会遇到各种挑战和困难的,用挑战和困难填充自己,丰富自己的生活
    • 迷茫是未知,有恐惧,有兴奋。对当前的我来说,恐惧可能更多,但是要面对
    • 职业生涯里面,前期的同质度更高,资深工程师可以会给初级工程师规划清晰一些,但是越往后期,越难规划,需要自己探索
  5. 推荐书
  6. 学习来源
    • 博客,infoq 等专业信息来源
    • 书籍
    • 周围的人
    • 尽可能从每天时间挤出时间阅读
  7. 生活和工作平衡点
    • 不同的人,同一个人的不同时期所需要的平衡不一样
    • 对自己人生期望不一样的人,平衡点不一样
    • 对我来说,目前阶段不存在明显边界,尽可能投入工作
  8. 如何保持激情
    • 人总是有低谷期的,不可能一直保持亢奋,除非是甲亢
    • 短期情绪会有起伏,长期来看,保持梦想和对自己高要求,可以帮助保持激情
    • 我有低谷期,低谷期适合反思
  9. 如何擦屁股
    • 现实是残酷的,总有屁股要擦,自己也有脏屁股,今天要给昨天的自己擦屁股
    • 现实中存在脏屁股,尽可能了解它,看代码、文档,设计方案,了解的人,让自己变舒服
    • 责任心的体现,做好当下的事情;解决这样的问题,解决当前的问题;避免留下脏屁股,设计好,规划好
    • 责任心的话很虚。一个人做擦屁股、低产值的事情,从长远来看,是错的;短期需要撑下来;并且帮助团队避免这样的事情发生
  10. 最有成就感的事情
    • 顶着家庭压力,到上海来
    • 在堆糖改造环境,提升自己心智水平,不害怕问题,自信,自省

原文链接: https://blog.alswl.com/2016/06/q-and-a/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
Li Fanxi's avatar

Synology PhotoStation性能优化

本文的内容已经过时,6.7.0以上的Photo Station已经没有严重的性能问题了。

我折腾过不少家用NAS方案,包括最早的WD My Book World Edition,后来的Joggler,再到后来的Raspberry Pi,这些方案除了性能存在一些问题以外,最大的缺点就是易用性存在问题,不但非“专业”人士用起来存在困难,就连我自己也对土法泡制的照片管理功能感到不满。直到两年前入了群晖(Synology)的家用NAS DS214play,事情才变得安逸起来。

平心而论,Synology的系统虽然功能强大,体验也还不错,但细节上做得其实真是挺糙的。最近就发现了它的照片管理软件PhotoStation出现了严重的性能问题,在照片库里只存了8万余张照片的情况下,打开首页要花费的时间已经超过了20秒,每打开一个文件夹都需要等待10秒以上,几乎不可用了。

以下分析以DSM 6.0为例,PhotoStation版本为6.4-3166。PhotoStation的安装路径为/volume1/@appstore/PhotoStation。

Synology的DSM系统,后台使用PostgreSQL数据库,前端是PHP页面。简单推理一下就可以知道,PhotoStation的性能瓶颈主要是在对照片索引数据库的访问上。性能调优的第一步就是先要找到哪些SQL查询占用了太多的时间。打开PostgreSQL记录SQL查询的开关,并查看所有SQL执行情况:

$ sudo su postgres
$ vi ~/postgresql.conf
log_statement = 'all'
$ psql photo postgres
# SELECT pg_reload_conf();
# \q
$ exit
$ sudo tail -f /var/log/postgresql.log

通过查看SQL执行记录,很容易发现几个明显的慢查询:

1.

SELECT COUNT(*) as total FROM photo_image; 
SELECT COUNT(*) as total FROM video;

2.

SELECT COUNT(logid), MAX(logid) FROM photo_log;

3.

SELECT * FROM (
    SELECT path AS filename, timetaken AS takendate, 
        create_time AS createdate, 'photo' AS type
    FROM photo_image
    WHERE path LIKE '/volume1/photo/%'
        AND path NOT LIKE '/volume1/photo/%/%' AND disabled = 'f'
    UNION
    SELECT path AS filename, mdate AS takendate,
        date AS createdate, 'video' AS type
    FROM video
    WHERE path LIKE '/volume1/photo/%'
        AND path NOT LIKE '/volume1/photo/%/%'
        AND disabled = 'f'
    ) AS totalCount; 

4.

SELECT path, resolutionx, resolutiony, version FROM (
    SELECT path, resolutionx, resolutiony, version,
        create_time, privilege_shareid, disabled
    FROM photo_image WHERE privilege_shareid IN (
        SELECT shareid FROM photo_share WHERE ref_shareid = (
            SELECT shareid FROM photo_share WHERE sharename = '2016'))
    AND disabled = 'f'
    UNION ALL
    SELECT path, resolutionx, resolutiony, 0 AS version, 
        date AS create_time, privilege_shareid, disabled
    FROM video WHERE privilege_shareid IN (
        SELECT shareid FROM photo_share WHERE ref_shareid = (
            SELECT shareid FROM photo_share WHERE sharename = '2016'))
    AND disabled = 'f') temp
ORDER BY create_time DESC LIMIT 1; 

优化的思路很简单,由于PhotoStation在正常情况下访问数据库所需要的读性能是远远大于写性能的,所以就通过牺牲写性能来逐一击破上面这些慢查询:

1. 程序的目的就是想知道系统中有多少张照片和多少个视频(而且其实并不需要精确知道,差不多就行),可惜对于PostgreSQL来说,由于它采用MVCC来解决并发问题,SELECT COUNT(*)是一个需要进行全表扫描的慢操作。解决方案就是用另外用一张表来存这两个表的总记录条数,并在原表上添加触发器来更新记录数。

CREATE TABLE photo_count (table_oid Oid PRIMARY KEY, count int);
ALTER TABLE photo_count OWNER TO "PhotoStation";
CREATE FUNCTION count_increment() RETURNS TRIGGER AS $_$
BEGIN
  UPDATE photo_count SET count = count + 1 WHERE table_oid = TG_RELID;
  RETURN NEW;
END $_$ LANGUAGE 'plpgsql';
CREATE FUNCTION count_decrement() RETURNS TRIGGER AS $_$
BEGIN
  UPDATE photo_count SET count = count - 1  WHERE table_oid = TG_RELID;
  RETURN NEW;
END $_$ LANGUAGE 'plpgsql';
CREATE TRIGGER photo_image_increment_trig AFTER INSERT ON photo_image 
  FOR EACH ROW EXECUTE PROCEDURE count_increment();
CREATE TRIGGER photo_image_decrement_trig AFTER DELETE ON photo_image 
  FOR EACH ROW EXECUTE PROCEDURE count_decrement();
CREATE TRIGGER video_increment_trig AFTER INSERT ON video 
  FOR EACH ROW EXECUTE PROCEDURE count_increment();
CREATE TRIGGER video_decrement_trig AFTER DELETE ON video 
  FOR EACH ROW EXECUTE PROCEDURE count_decrement();
INSERT INTO photo_count VALUES 
  ('photo_image'::regclass, (SELECT COUNT(*) FROM photo_count));
INSERT INTO photo_count VALUES 
  ('video'::regclass, (SELECT COUNT(*) FROM video));

然后在PHP程序中修改需要统计表数记录数的逻辑,在这里可以看到似乎同一个Session中只会查一次,但即使就是这一次,也已经慢得让人不开心了:

diff --git a/photo/include/file.php b/photo/include/file.php
index 541c5cb..7caa5de 100755
--- a/photo/include/file.php
+++ b/photo/include/file.php
@@ -536,8 +536,11 @@ class File {
 		if ($key && isset($_SESSION[SYNOPHOTO_ADMIN_USER][$key])) {
 			return $_SESSION[SYNOPHOTO_ADMIN_USER][$key];
 		}
-		$query = "SELECT count(*) as total FROM $table";
-
+		if ('photo_image' == $table || 'video' == $table) {
+			$query = "SELECT count as total FROM photo_count where table_oid='$table'::regclass";
+		} else {
+		    $query = "SELECT count(*) as total FROM $table";
+		}
 		$result = PHOTO_DB_Query($query);
 		if (!$result) {
 			// db query fail, won't update session value

2. 其实可以用跟前一个问题类似的方法去解决。但是这个其实只是一个没太多用处的操作日志表,所以我用更为简单粗暴的方法去解决这个问题:减少在数据库中保留日志的条数。直接修改PHP程序:

diff --git a/photo/include/log.php b/photo/include/log.php
index 1c982af..56385db 100644
--- a/photo/include/log.php
+++ b/photo/include/log.php
@@ -1,8 +1,8 @@
 <?php
 
 class PhotoLog {
-	const LIMIT = 100000;
-	const PURGECOUNT = 10000;
+	const LIMIT = 1000;
+	const PURGECOUNT = 100;
 	public static $SupportFormat = array("html", "csv");
 
 	public static function Debug($msg)

3. 第三个问题主要体现在对照片路径的处理上,为了选出位于某个路径下(不含子目录)的照片,程序采用了path LIKE ‘/path/%’ AND path NOT LIKE ‘/path/%/%’这样的查询条件。其实PostgreSQL在一定程度上是可以利用path字段上的索引来很好的优化这个查询的,但是实际运行中发现(通过在PostgreSQL的客户端中用explain和explain analyze分析查询)在某些情况下索引会失效,造成非常差的查询性能。解决方案还是用写性能来换读性能,先在表上加一个dirname字段并建立索引,按path算好文件所在的目录名写入dirname,然后把查询条件改为对dirname的查询,避免使用通配符和LIKE运算即可:

ALTER TABLE photo_image ADD COLUMN dirname TEXT NOT NULL DEFAULT '';
UPDATE photo_image 
    SET dirname = LEFT(path,LENGTH(path)-STRPOS(REVERSE(path),'/')+1);
ALTER TABLE video ADD COLUMN dirname TEXT NOT NULL DEFAULT '';
UPDATE video SET dirname = LEFT(path,LENGTH(path)-STRPOS(REVERSE(path),'/')+1);
CREATE INDEX dirname_index ON photo_image USING btree(dirname);
CREATE INDEX dirname_index ON video USING btree(dirname);
CREATE OR REPLACE FUNCTION set_dirname()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
BEGIN
  NEW.dirname := LEFT(NEW.path,LENGTH(NEW.path)-STRPOS(REVERSE(NEW.path),'/')+1);
  RETURN NEW;
END $function$
CREATE TRIGGER set_dirname_trigger 
BEFORE INSERT OR UPDATE ON photo_image 
FOR EACH ROW 
  EXECUTE PROCEDURE set_dirname();
CREATE TRIGGER set_dirname_trigger 
BEFORE INSERT OR UPDATE ON video 
FOR EACH ROW 
  EXECUTE PROCEDURE set_dirname();

同时修改PHP程序:

diff --git a/photo/include/photo/synophoto_csPhotoDB.php b/photo/include/photo/synophoto_csPhotoDB.php
index ac8f932..43e58ee 100755
--- a/photo/include/photo/synophoto_csPhotoDB.php
+++ b/photo/include/photo/synophoto_csPhotoDB.php
@@ -1607,10 +1607,8 @@ class csSYNOPhotoDB {
 		} else {
 			$albumRealPath = self::EscapeLikeParam(SYNOPHOTO_SERVICE_REAL_DIR_PATH."{$albumName}/");
 		}
-		$cond = "path LIKE ? {$this->escapeStr} AND path NOT LIKE ? {$this->escapeStr} AND disabled = 'f' ";
-		array_push($pathSqlParam, "{$albumRealPath}%");
-		array_push($pathSqlParam, "{$albumRealPath}%/%");
-
+		$cond = "dirname = ? {$this->escapeStr} AND disabled = 'f' ";
+		array_push($pathSqlParam, "{$albumRealPath}");
 		if (!$removePhoto) {
 			$photoQuery = "SELECT path as filename, timetaken as takendate, create_time as createdate, 'photo' as type
 FROM photo_image WHERE $cond";
 			$sqlParam = array_merge($sqlParam, $pathSqlParam);

4. 这个查询只是为了查询一个目录及其所有子目录中最新一个照片或视频,用其缩略图来作为目录的封面图片。群晖的工程师自己也知道这个查询很慢,所以还在程序中加了个逻辑,当照片视频数量大于200000时,放弃按日期排序,直接随机选一张。然而,这个查询实际上是可以简单优化的,明明不需要把所有的照片视频UNION到一起后再找出最新的一个,可以直接分别找出最新的照片和最新的视频,然后再到这两个中去取一个相对更新的就可以了。直接修改PHP代码实现:

diff --git a/photo/include/photo/synophoto_csPhotoAlbum.php b/photo/include/photo/synophoto_csPhotoAlbum.php
index ca128f0..f0e57e7 100755
--- a/photo/include/photo/synophoto_csPhotoAlbum.php
+++ b/photo/include/photo/synophoto_csPhotoAlbum.php
@@ -145,9 +145,11 @@ class csSYNOPhotoAlbum {
 		$cond .= " AND disabled = 'f'";
 
 		$table = "(" .
-				"SELECT path, resolutionx, resolutiony, version, create_time, privilege_shareid, disabled FROM pho
to_image WHERE $cond " .
+				"(SELECT path, resolutionx, resolutiony, version, create_time, privilege_shareid, disabled FRO
M photo_image WHERE $cond " .
+				"ORDER BY create_time DESC LIMIT 1)" .
 				"UNION ALL " .
-				"SELECT path, resolutionx, resolutiony, 0 as version, date as create_time, privilege_shareid, disa
bled FROM video WHERE $cond " .
+				"(SELECT path, resolutionx, resolutiony, 0 as version, date as create_time, privilege_shareid,
 disabled FROM video WHERE $cond " .
+				"ORDER BY create_time DESC LIMIT 1)" .
 		") temp";
 
 		// this may cost lots of time, so it won't sort by create_time if the total count exceeds the threshold (200,000)

做完以上优化,我的PhotoStation已经基本可以做到点进文件夹秒开了,至少我自己已经比较满意了。声明一下,其实我并不太懂数据库相关理论和技术,以上“优化”只能说是在我自己的实验中起到了优化的效果,也许其中一些并不太科学,希望这篇文章能起到抛砖引玉的作用。

重新安装PhotoStation或升级DSM系统会造成我们对程序所作的修改丢失,所以在修改完成后,务必做好备份。

另外,适时对PostgreSQL数据库进行VACUUM操作似乎可以起到提高访问性能的目的,尤其是在做过大量照片更新后。

Li Fanxi's avatar

一个网络访问故障的排查

故障描述:

一个Python程序,在我的Macbook Air上使用Gearman库访问Gearman服务器时始终报错:
Found no valid connections in list:
[<GearmanConnection 111.111.111.111:80 connected=True>]

问题排查过程:

  1. 我的另一台电脑的Linux系统下运行这个程序是正常的,所以证明Gearman服务器本身是好的,程序也是对的。
  2. 别人的Macbook Air/Macbook Pro上运行这个程序也是正常的,系统中的Python版本是一致的,证明这个程序在Mac OS X下并不存在兼容性问题。
  3. 111.111.111.111:80这个是一个从开发环境访问生产环境服务器的虚拟IP/端口,把Gearman服务安装到开发环境网络中222.222.222.222:4730,程序运行正常。
  4. 由于访问开发环境正常、访问生产环境异常,所以怀疑是网络原因。使用ping/traceroute命令访问111.111.111.111,都正常。用telnet 111.111.111.111 80尝试TCP连接,也正常。
  5. 在无线/有线之间切换网络物理连接方式,测试结果一样:本机与目标服务器之间网络通畅,TCP连接正常,但Gearman连接无法正常建立。并且同处于同一个网络中的其它电脑上运行一样程序都访问正常。
  6. 怀疑Gearman的Python库在某些特殊情况下有Bug,在里面加了很多print来打印日志,发现客户端在与服务器建立完连接后,在真正要通信时,连接又变成了断开状态。但由于不太了解Gearman库的实现细节,再要进一步Debug,存在一定困难,暂时放弃这个思路。
  7. 为了排查Gearman连接断开的原因,用Wireshark进行抓包。抓包的结果非常令人惊讶:在程序运行的整个过程中,没有抓到任何客户端与服务器之间通信的数据包。但是程序打印出来的日志却明明显示出连接是先建立再断开的。
  8. 至此,问题已经快要查清楚了:本机有个程序劫持了本机发起的网络通信,实施中间人攻击,造成Gearman连接异常。
  9. 用Python写socket程序尝试向不同目标发起各种TCP连接(后来意识到其实用telnet就可以了),同时用Wireshark抓包,发现本机发起的所有目标为80端口的连接都会被劫持。劫持的效果是如果通过80端口进行HTTP通信,劫持的程序就会充当一个代理服务器的功能,正常完成通信过程。但是一开始出问题的程序是在80端口跑Gearman的通信协议,所以这个劫持程序无法正确处理,造成了通信异常。
  10. 最后就是要找出是哪个程序实施了劫持,用了一个很土的方法:下载一个大文件,同时用netstat -na查出与服务器80端口通信的本地端口号(谁叫Mac OS X的netstat命令没有-p参数直接看是哪个进程的连接呢?),然后用sudo lsof -i :<local port>命令查出这个本地端口号的使用者。

真相大白:

Cisco AnyConnect Secure Mobility Client中自带了一个进程名为acwebsecagent的Web安全模块,这个安全模块不管VPN是否在使用,都会劫持本机所有的发往80端口的通信,具体它做了什么好事坏事就不得而知了。

找到了问题所在,网上搜一搜就能查到很多吐槽这个Web Security模块的贴子了,它会随着AnyConnect默认安装到你的电脑上(我的电脑系统是IT预装的,不然我装AnyConnect时肯定会手工把这个勾勾去掉)。解决问题的方法也很简单 ,一行命令卸载它:
sudo /opt/cisco/anyconnect/bin/websecurity_uninstall.sh

alswl's avatar

海贼王和创业团队

一个同事在知乎提了一个问题 如果把草帽海贼团比作一个创业团队,这个Team的组织架构是怎样的?有哪些优势,又有哪些不足?

这个话题很有趣,作为追了多年的 fans ,又是身处互联网创业团队的我要来强答一记。

海贼王的世界很大,富有个性的角色和团队也很多,恰好可以和显示世界中的互联网创业团队进行对比。 柳传志讲的好,做大事,要「建班子,定战略,带队伍」,我们就着金句来看看草帽这个团队怎么样。

产品和战略

我先讲产品和战略。

由于海贼王世界的设定,以及作为一个全年段漫画动画的原因。 海贼王世界的整体战略是较为简单,海贼团的目标都较为单一,即「找到 哥尔 D 罗杰 留下的宝藏」。 海贼团们实现目标的路径也较为单一,打斗增强战斗力,不断寻找线索,去伟大航道寻找宝藏。

所以故事的开展反而是围绕另外一条隐秘的线索「历史的真相」,尾田大神埋坑很深。 「历史的真相」并不是作为大部分人努力和前进的目标,所以「海贼王」世界的故事虽然曲折跌宕, 但是产品模型极为单一,不具备太多可以讨论的点。

团队

接着讲团队,一个团队第一重要的是创始人,这必须要是一位领袖人物。

评价领袖优秀程度,从这么几个角度评价:

  • 意志力:打不死的小强,不达目标誓不罢休
  • 专业技能:能打、脑子灵光、心灵手巧、一技之长
  • 规划能力:除了体力值和专业智慧,也要讲讲战略和谋略
  • 管理能力:沟通协调、团队管理促进能力,能够带领团队实现目标,促进他人成长

以这个标准来看几个具有领袖气质的角色:

  • 路飞
    • 意志力:5
    • 专业技能:5
    • 规划能力:1(率性而为的大爷)
    • 管理能力:1(团队自由生长)
  • 白胡子
    • 意志力:5
    • 专业技能:5
    • 规划能力:4(顶上之战的过程证明)
    • 管理能力:5(队长们的成长和忠诚度证明)
  • 多弗朗明哥
    • 意志力:5
    • 专业技能:5
    • 规划能力:5
    • 管理能力:5
  • 艾斯
    • 意志力:5
    • 专业技能:4(以牺牲的时间点战斗力打分)
    • 规划能力:2(追踪黑胡子,一个人冒进,被黑胡子摆了一道)
    • 管理能力:2(也就混到一个队长,能够鼓励其他人,但是没发现黑胡子成长的问题)
  • 唐僧(乱入一个对比)
    • 意志力:5+
    • 专业技能:4(熟读经书算不算专业能力?)
    • 规划能力:0
    • 管理能力:0

从上面的判断可以看出,对于一个领袖而言,路飞仅仅是不错,但是谈不上多么优秀, 倒是白胡子,典范啊典范。

根据上面提到的能力模型,草帽海贼团的其他角色大家也可以心里评估出来了: 大部分人都是意志力、专业技能强悍(即便是乌索普,射击能力也是可以评上 3),但是同时也在规划能力和管理能力上面较弱。

唉,这么弱,很难继续支撑草帽海贼团继续走下去啊,怎么办呢?

带队伍

草帽海贼团,一个极为漂亮的「自组织团队」。

什么是「自组织团队」?来自 InfoQ 的一篇文章 什么是自组织团队? 里面讲到团队的特性:

  • 分散式的控制,也就是说与集中式的控制截然相反,
  • 不断适应改变的环境,
  • 在局部相互作用下自然浮现出来的结构,
  • 反馈,包括肯定的和否定的
  • 弹性,归结于系统修复和调整的能力。

用简单的关键词概括,其实是这么几个关键词:

  • 自行决策
  • 自己成长
  • 适应环境变化
  • 团队内部信任并且沟通顺畅

从这几个关键词来看,草帽海贼团和这种自组织模式极为匹配。路飞从来不会发号施令,统一调度,顶多喊几句口号振奋一下大家。 路飞自身的规划能力和帮助他人成长的能力也远不及格。团队里面其他的成员,索隆、香吉士等的成长,完全是靠自己, 甚至在「两年」跨度这么长的时间,尾田也是借外部力量帮助团队这些成员成长,而不是依靠领袖来培养成员。

路飞选择这种自组织模式,不仅仅是一种巧合,更多的是一种逼不得已。尾田大神写的就是青春热血漫画,要是换成一个有勇有谋, 那就成主旋律电视剧了,大家可能也是因为路飞这种优点和缺点喜欢他。

Anti-自组织模式

和「自组织模式」相反的情况,可以参照一下多弗朗明哥的团队。我们可以看到,在多弗朗明哥团里里面,有严密的等级关系(四大干部,家族干部,普通小兵), 有帮助人才成长的流程(培训罗的一系列流程)。 这种设定也是多弗朗明哥的战略能力和管理能力的体现。

还有一种更极致的组织模式「集权模式」,由核心层发号施令,协同作战。 海贼王的世界里,较少发现这样的模式,反派角色海军有一点点像。 倒是另外一部作品「星球大战」,其中的帝国、第一秩序,都是典型的 Manager 发号施令,底层士兵只要好好作战即可。 统一培养成行的克隆人,就可以支撑这样的团队发挥十足战力。

这种集权模式,缺少向上反馈能力,缺少自发的创新能力。在业务发展顺利规模扩张阶段,也许可以发展不错,但是长久以往, 核心层必然会发觉创新力和驱动力不足,花费大量的精力在管理、制度建设、奖惩机制上面。

路飞的缺点和优点一样鲜明,他很幸运的(可能也完全不知道)使用了「自组织团队」这种模式,完成了创业的起步阶段。 但是这种模式有也有自带的缺陷,需要目标一致性很高,团队成员自身有极高成长度。一旦遇到业务爆炸(顶上之战这种规模的战役), 这团队就完蛋了。期望路飞在未来的航道上,注意培养一下自己不善于的能力。好消息是,从最近的「庞克哈撒德篇」和「德雷斯罗撒篇」战况来看, 路飞已经开始学会制定「寻找友军结盟」、「树立竞争对手标杆」这样的初级战略方案了。

海贼王的世界,真的就是现实中「海盗」的正式写照吧。完成不可能的目标,成就团队,成就自己。

「世代传承的意志,时代的变迁,人们的梦,只要人们继续追求自由的答案,这一切的一切都将永不停止.这世界……没错!一个追求自由任凭选择的世界,就在我们的眼前无限地延伸,如果我们的梦想可以引导你的方向的话,就去追寻吧!在名为信念的旗帜下」


原文链接: https://blog.alswl.com/2016/04/onepiece-startup/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
alswl's avatar

Windows management for hacker

团队里的同学有时看见我键指如飞,可以用快捷键将 Mac 的窗口玩转于手心。他们表示酷炫非常, 心生羡慕的同时,希望掌握这门技艺,我就把使用的 Phoenix 介绍给大家。结果过了一段时间, 发现普及率并不高,本着好为人师的精神,今天我就来八一八这款优秀的桌面管理工具。

在介绍我使用的工具之前,我要先介绍一下我选择的原因和历史。

ps:配图是我长草多年的 Ergodox Infinity。(@夫人,看到这里的时候,请留步思考 5s)。

Alt+Tab = 苦难的历史

当我还年轻的时候,曾经对 Alt+Tab 这个快捷键愤慨无比,觉得这种设计虽然简单但是很蠢。

  • 命中率低。要在茫茫图标中查找自己需要的窗口,如果开了 20 个应用,切换到一个非常用窗口至多可能需要 20-1 次按键。 不要跟我说有 Alt+Shift+Tab 的反向循环操作,估计大部分用户都不知道。而且 正向/反向 切换伤脑子。
  • 交互方式耗时。由于 Alt+Tab 的操作需要用户进行反馈才能进行,是 眼-手-眼-手 的操作反馈方式, 这种交互模式费时费脑子。跟这种需要实时反馈对立的交互模式应当是类似「一键呼出」的操作。
  • 在大显示器、多显示器的环境下面,Alt-Tab 模式没有做任何优化
  • 频繁切出窗口进行切换,容易让人分神,比如切换时候看到某个播放器的标题,在放一首喜欢的歌,很可能吸引过去。

有人会问,难道不优化都是缺陷么? 我的答案是肯定的。Alt+Tab 是几乎除鼠标之外的唯一一种窗口操作方式。如果它不能跟上时代的步伐, 对大显示器、多显示器做优化,那就是不作为,不努力,不上进。跟不上变化是要被淘汰的!

平铺式窗口管理器

在被 Windows Alt-Tab 虐了多年之后,我长大了,开始接触 Linux,但是这种痛苦仍然时刻包围着我。 当我分期购买一个外置显示器之后,这种痛苦到达到了顶端。 恰好彼时我是 ArchLinux 的信徒,很快就发现了一片桃源: 自己安装 Window manager

我重新认识了桌面系统世界,除了介绍常规的 Gnome / KDE / xfce 之外,还有一类窗口管理系统,他们叫做「平铺式窗口管理器」。 (严格来说,Gnome / KDE / xfce 属于 Desktop environment,层级比「窗口管理系统」要高,我这里不做严格区分)。

什么是平铺式窗口管理器?来一个直观的认识:

官方的解释是……呵呵,自己点进去看解释吧。平铺式桌面管理器(包括同时支持平铺式和堆叠式的混合式桌面管理器)给我带来了新的认知, 原来桌面系统是可以进行接口编程的,我不那么 care 到底是哪种风格,我 care 的是, 能否通过编程定制来解决我的 Alt-Tab 问题。

我期望的窗口管理模式

  • 快速启动最常用的应用,同时也能将其快速呼出
  • 对大屏幕友好,现在显示器普遍是大屏幕,可以自由控制窗体的移动,方便多个窗口同时进行操作
  • 对多屏幕友好,多屏幕间的切换,要友好。可以快速屏幕间切换
  • 对键盘友好,对鼠标友好,全键盘操作模式,但同时要对鼠标友好,比如鼠标跟随焦点功能,毕竟一些操作还是鼠标方便
  • 帮助集中注意力,将操作界面隔离成多个目的区分的空间,比如写作时候,期望只有一个 Evernote + Chrome 在眼前

我在 Mac 下面的方案

呜呼,感谢伟大的 Jason Milkins 做了一堆尝试, 创造了一堆乱七八糟的桌面管理器。然后感谢 Kasper Hirvikoski 在 Jason 拍拍屁股走人之后,接过了 Jason 的棒子,将 Phoenix 这个项目快速推进,解决了一堆导致不可用的 bug, 并新增了很多特性。

回到要介绍的主角身上,Phoenix

A lightweight OS X window and app manager scriptable with JavaScript

基本特性是:

  • Javascript 作为配置文件,定制性超级强
  • 支持 App / Window / Space / Screen 等对象的操作

基于 Phoenix,我达成了我的窗口管理模式的目标:

  • 快速启动
    • 使用 Option + ` / 1 / 2 / 3 / 4 / 8 / 9 / e / a / s / z /, / . / / 启动
    • iTerm / Chrome / Safari / QQ / Bearychat / Wechat / Neteasy Music / MacVim / IntelliJ IDEA / Macdown / Mail / Evernote / Finder
  • 窗口操作
    • Option + - / =
      • 大小控制
    • Option + m
      • 窗口移动到屏幕中央
    • Option + Space
      • 鼠标找回到窗口中央
  • 屏幕内操作
    • Option + J / K
      • 焦点在同屏幕窗口切换
    • Ctrl + Option + J / K / H / L
      • 窗口移动
  • 屏幕间操作
    • Option + H / L
      • 焦点左右屏幕切换
  • Space 操作
    • Option + I / O
      • Space 左右切换
    • Option + Ctrl + I / O
      • 将当前窗口移动到相邻 Space
    • Option + Enter
      • 将当前窗口移动到 Work Space
    • Option + Delete
      • 将当前窗口移动到 Park Space
    • Work / Park Space 就是用来帮助我集中精力的

我的配置文件在 https://github.com/alswl/.oOo./blob/master/.phoenix.js 我的配置文件可以开箱即用,但这是我自己的工作模式,想要获得自己最舒适的效果,需要自己进行一些研究和定制。

其他一些替代方案


我曾经写过一篇 Linux 程序员的 Mac 安装记录, 告诉大家我在 Mac 上面常用的工具、包管理器。


原文链接: https://blog.alswl.com/2016/04/windows-management-for-hacker/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
's avatar

Firefox 插件:TooManyTabs

TooManyTabs 简直是标(tuo)签(yan)党(zheng)的福音啊!每每在一大堆待读标签里面翻找要看的网页简直要疯掉!包括我和我的内存 ▔f゚゚▔

但只是简单的插件介绍的话是不足以让我这个万年懒癌晚期开工写博的,主要是这货配合 Firefox 的帐户同步功能的时候有点儿坑:一开始没有给分组重命名的时候,几个电脑的 TooManyTabs 一同步就全乱套了;直到我把各个电脑的分组都分别命名,然后又费了老劲儿地整理一番之后才搞定。

让我觉得这个插件很神奇的是,它是用 Firefox 的书签功能来保存标签的,但又不是简单地保存网址,因为标签恢复之后其前进后退的历史也都一并恢复了。看来 Firefox 的书签还有很多的隐藏功能有待发掘啊。

最后的最后,既然是通过书签功能来保存和同步的,那如果 Firefox Android 的书签功能能再给点儿力就好了,让我可以直接把网址保存到指定的分组下面,这样就不用先发送到电脑上再(能用电脑时)TooManyTabs 了(再加上 Firefox Android 的发送标签功能有时还会丢标签)。

以上

Li Fanxi's avatar

最好的简谱编辑排版软件JP-Word

本文是一则硬广告。

上大学时跟同学一起办过一份音乐小报,名为《白桦林》,虽然只办了一期就夭折了,不过在这期小报上试图排版朴树那首《白桦林》的简谱(附带吉他和弦)的经历让人难以忘怀。我那时坚信自己用Word排版复杂版面的水平已经到了出神入化的程度,但还是被这个简谱排版工作打败了。Word虽然是“所见即所得”,但是那排版好的谱子打印出来时,总是会有那么些对不齐或者走样。那时学校打印店打印一页A4纸的价格是1.5元,不舍得反复打印试验,最终还是通过剪刀胶水才把这个排版任务搞定的。

《白桦林》小报第三版。由于Word版本和字体的原因,现在看到的跟当年排出来的效果有差别。

上大学时,经常玩一个叫HappyEO的电脑键盘模拟电子琴的软件——强势插入,再打个广告,HappyEO是用电脑键盘(也可以接MIDI键盘)模拟电子琴的最好的软件,没有之一。因为对这个软件的喜爱,我跟HappyEO的作者有着较多的交流,一路见证着HappyEO电子琴和后来的iDreamPiano模拟钢琴等软件的诞生。2003年时,他告诉我他做了一个简谱排版软件,叫JP-Word

实话说,1.0的JP-Word一点也不诱人,粗糙的界面、鸡肋的图片输出和打印功能、封闭专有的文件格式实在不能让我提起太大的兴趣。加上对于我来说,平时几乎没有简谱排版的需求,所以对这个软件并没有太高的热情。接下来两三年时间,JP-Word慢慢升级到了3.0,功能丰富了一些,但没有什么本质的变化。

一下过了十年,JP-Word的作者突然告诉我,JP-Word出了4.0的新版本。这个脱胎换骨的新版本,着实可以用“惊艳”来形容。

JP-Word 4.0界面预览

不像五线谱打谱有自由的Lilypond,还有Overture、Encore等强大的商业软件。市面上简谱音乐软件并不多,优秀打谱软件更是少之又少。之所以我敢在文章标题上把JP-Word说成是“最好的”,个人认为主要有以下几点:

  • 独创的的“切换音符时值组合”功能。常见音符的时值组合不用一个个手工调整,音符输入后按几下空格键就自动组合好了。
  • 强大的歌词编辑功能。支持歌词自动对位,谱打完,歌词一打上去就自动对齐到它该去的地方。
  • 支持矢量PDF的输出。而且排版结果不管是布局还是字体,都非常美观,放到专业环境下用也毫无压力。
  • 开放的JPW-ABC格式。一改以前用专有格式的方式,新版本用了开放的纯文本来描述乐谱,为二次开发提供了便利。

JP-Word简谱编辑软件不是完全免费的,免费的版本只提供了部分主要的功能集,但折腾党可以通过手工写JPW-ABC的方式实现几乎全部收费版本的功能。

缺点也是有的,比如目前不支持直接播放乐谱和MIDI导出。不过Thanks to开放的JPW-ABC格式,完全可以写个简单的脚本,把JPW-ABC文件转换为MIDI文件。我正在尝试写个简单Python脚本,把JPW-ABC转换为Lilypond格式,然后借助Lilypond,可以直接生成对应的五线谱和MIDI文件。有兴趣可以关注我的Github项目jpw2lilypond,不过目前上面还只有一个很垃圾的Prototype(对象建模建错了,虽然基本功能在,但已经没办法往下写了),而且现在每天的时间都不够用,开发进度一定不会很快。

JP-Word 4.0的功能介绍与使用说明:http://www.happyeo.com/intro_jpw.htm

下载JP-Word 4.0免费版的地址:http://www.happyeo.com/downloads_jpw.htm

注册JP-Word的方法:http://www.happyeo.com/register_jpw4.htm

Li Fanxi's avatar

实践个人网站迁移HTTPS与HTTP/2

赶个时髦,把自己的博客进行了全站HTTPS改造,并升级支持了HTTP/2,总结在此,当作备忘。

很惭愧,虽然曾经做过几年Web安全产品,其实我自己并没有非常深入的去理解和思考Web安全更多内在的东西,所以可能文中的部分描述并不完全准确。很多内容参考了Jerry Qu的博客上的内容,都以参考文献的方式列在文章中,我这里只写结论,技术细节可以参考他的原文。

动因

HTTPS改造的好处当然是更安全。虽然对于一个博客网站来说,“安全”似乎并不是一个非常重要的因素,但是以国内现实的情况来说,使用HTTPS提供网站服务有一个好处就是可以避免网络运营商篡改网页内容(比如插入弹出广告)——其实吧,HTTPS以后,Chrome浏览器地址栏显示的绿色小锁才是吸引我迁移的真正原因,挺好看的。

HTTP/2从协议层面消除了传统HTTP的一些不足和缺陷,对我来说,直接的好处就是可以大幅度提高网页载入的速度。有关HTTP/2的前世今生,可以参考以下文章[1]。

HTTPS改造

证书

HTTPS改造的一个基本要素就是证书,在传统上有很多认证机构(CA)可以收费签发证书,比如大名鼎鼎的Verisign。现在也有很多公司可以提供免费或者廉价的证书,比如有名的StartSSL,以及最近很火的Let’s Encrypt

我先是尝试了StartSSL的免费证书,但是它只能签发有效期一年的免费证书,每年都得手动去更新证书是一件很让人头痛的事情。所以后来选定了使用Let’s Encrypt,虽然Let’s Encrypt的证书有效期只有三个月,但是可以方便的通过脚本来实现自动更新。

使用Let’s Encrypt的证书有两种方式,一种是使用他的提供的工具脚本,另一种是使用ACME协议。我目前使用的是ACME协议方式,参考[2]。如果用Let’s Encrypt的工具,参考[3]。我个人比较喜欢ACME协议方式,因为很轻量级,Let’s Encrypt自己的工具太过“全家桶”了,不够简洁明了。

主要的步骤如下:

  1. 生成一个帐号私钥
    $ openssl genrsa 4096 > account.key
  2. 生成一个域名私钥
    $ openssl genrsa 4096 > domain.key
  3. 生成证书签名请求CSR文件,通常至少包含祼域名和带www主机名的两个域名。
    $ openssl req -new -sha256 -key domain.key -subj "/"
        -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf
        "[SAN]\nsubjectAltName=DNS:yoursite.com,DNS:www.yoursite.com")) >
        domain.csr
  4. 在自己网站上配置一个可以从外部访问的目录,用来完成challenge。Let’s Encrypt会生成一个文件,你把它放在这个目录里,然后Let’s Encrypt如果能访问到这个文件,就证明了这个域名是属于你的。nginx配置类似于如下,配在80端口的Server里面:
    location ^~ /.well-known/acme-challenge/
    {
        alias /home/xxx/www/challenges/;
        try_files $uri =404;
    }
  5. 下载acme_tiny脚本,并运行,里面用到了帐号私钥(account.key)、域名私钥(domain.key)、CSR文件(domain.csr)和ACME challenge的路径,生成签发的证书(signed.crt)。
    $ wget https://raw.githubusercontent.com/diafygi/acme-tiny/master/acme_tiny.py
    $ python acme_tiny.py --account-key ./account.key --csr ./domain.csr
        --acme-dir /home/xxx/www/challenges/ > ./signed.crt
  6. 最后合并Let’s Encrypt的中间证书和我们自己的证书:
    $ wget -O - https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem
        > intermediate.pem
    $ cat signed.crt intermediate.pem > chained.pem

Web服务器

我使用nginx作为Web服务器,启用HTTPS服务,只需要在原来的HTTP服务上加几行配置就可以了:

listen 443 ssl;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:
  EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:
  EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_certificate /home/user/path/to/your/chained.pem;
ssl_certificate_key /home/user/path/to/your/domain.key;
ssl_session_timeout 5m;

其中需要用到你自己网站的私钥domain.key,还有Let’s Encrypt给你签发并合并了中间证书的证书文件chained.pem。

SSL协议版本与加密算法

我上面的配置用的是CloudFlare推荐的配置,详细的讨论可以参考[4]。我这么配基本上放弃了对Windows XP + IE6的支持,但可以让Qualys SSL Labs给出的评分提高到A级。

WordPress

我的博客是用WordPress构建的,WordPress在很多地方都会把带协议的网站URL给存下来,造成无法透明的把HTTP改成HTTPS。暴力解决方法就是去它的数据库做全文替换[5]。

先把WordPress设置->常规中的两个URL设置改为https,然后去WordPress的数据库对已有的文章进行全局字符串替换:

UPDATE `<wordpress_prefix>_posts` SET post_content=
    (REPLACE (post_content, 'http://[domain name]',
    'https://[domain name]'));

其它页面嵌入内容

如果在HTTPS的页面上嵌入了非HTTPS的内容,比如跨站的HTTP图片,浏览器上的绿色小锁就会变成灰色了。如果是跨站的CSS或者JavaScript,在现代浏览器上可能会直接被禁止加载[6]。

解决的方案就是把src的协议去掉,直接写成//domain.com/path/to/image这样的形式,可以兼容HTTP和HTTPS。

但是有些源站根本就不支持HTTPS,或者虽然提供了HTTPS服务,但证书不合法,这么做就行不通了。比如我的页面右侧的饭否的图片,虽然饭否有HTTPS服务,但证书过期了,直接嵌入就会有问题。

我的解决方案也是简单粗暴的,直接在nginx里为这些网站相关URL做一个反向代理。比如,为了解决饭否图片的问题,我在我的nginx里加了以下配置,然后把饭否图片的域名换成我自己的域名:

location /u {
    proxy_pass http://b.fanfou.com;
    proxy_set_header Host b.fanfou.com;
    proxy_redirect off;
}

自动重定向HTTP请求

至此,HTTPS改造已经准备好了,重启nginx后就可以用HTTPS协议来访问网站,检查是否工作正常。主要检查证书是不是正常,另外还有看看有没有混杂非HTTPS资源造成页面加载不正常的。Chrome的Developer Tools可以帮助你排查这些问题。

如果一切正常,就可以考虑自动重定向所有的HTTP请求了,301跳转通常是最理想的方式。在nginx的80端口http服务器配置中添加以下的内容:

location / {
    return 301 https://$server_name$request_uri;
}

其它要考虑的问题

SNI:如果在nginx用server_name实现了单主机的多虚拟站点,那就会出现一个IP地址上对应多个域名的情况,这时服务器和客户端都需要支持SNI,才能在HTTPS的情况下正常工作。较新版本的nginx版本服务器都是支持SNI的,但IE6之类的老旧浏览器不支持。所以如果放弃老旧浏览器支持的话,SNI不是个问题。否则就只能保证在同一个IP上只启用一个域名的HTTPS网站,才能确保客户访问无障碍。

HSTS:虽然启用了HTTPS,但是用户在访问时如果没有显式输入https协议,现在的浏览器默认还是会先选用http协议,再由服务器进行301/302跳转,这时就有可能被劫持。目前的解决方案是在自己的网站上输出HSTS头,并把自己的域名加入HSTS Preload List列表里[6]。我没有启用HSTS,因为一但启用没办法撤销,万一以后不能提供HTTPS服务了,想降级为HTTP都没有机会了。

其它:对于个人小网站来说,也许前面的讨论基本够用。但对稍大的网站来说,要考虑的问题还有更多。比如:CDN支持、各子域名证书的管理模式、SHA1不安全证书与老版本浏览器兼容性、大量非HTTPS外部资源的处理、特殊客户端不支持302引流等等。

HTTP/2改造

新版的nginx已经内置了对HTTP/2协议的支持,所以完成HTTPS改造后,启用HTTP/2支持是一件相对来说比较简单的事情。

在nginx里HTTPS的listen配置中加入http2即可,当然不要忘记重新reload nginx:

listen 443 ssl http2;

在Chrome中可以安装一个HTTP/2 and SPDY indicator插件,这时打开HTTP/2网站时,地址栏右侧就会出现一个蓝色的闪电标记,证明这个网站已经支持了HTTP/2协议。

新版的curl也可以用来帮助检查HTTP/2是否工作正常,在curl网站URL时,加上–http2参数即可。别忘了,必须是用HTTPS协议才能支持HTTP/2哦。

说得这么轻巧,事实上很多系统中的nginx都不是可以支持HTTP/2的新版本,所以还需要手动编译新版的nginx。

nginx -V可以列出当前nginx编译时使用的configure参数,可以作为重新编译时的参考,在它列出的参数的基础上加上–with-http_v2_module参数,就可以启用HTTP/2功能了。

参考文献

[1] 凯文叔叔的网志 – HTTP/2

[2] Let’s Encrypt,免费好用的 HTTPS 证书

[3] Let’s Encrypt SSL证书配置

[4] 关于启用 HTTPS 的一些经验分享(二)

[5] 迁移 WordPress 博客至 HTTPS

[6] 关于启用 HTTPS 的一些经验分享(一)

alswl's avatar

技术之外

这是一天一本书的第二次执行,这次选了一本比较薄的书,上周的书看了一天,脑仁疼。


在我组织团队新兵训练营(入职之后一段时间内容集中的培训)时候, 经常和新同事聊到一个词:软实力。 我将其描述为专业技能之外的能力。每个人都这种能力的解读可能会不一样, 我将其拆解为:「对工作的热情;观察力和反思能力;做计划和决策是否周全」。

这种拆解是不全面的,「多年」以后,我意识到,硬实力考衡的是专业的能力, 而软实力则是考衡人的因素。这种晚来的意识让我在一段时间里面, 将自己的工作陷入困境,并且得不到解药。

Google 的两位工程师 Brian W. Fitzpatrick 和 Ben Collins-Sussman 写了一本书极客与团队,通过他们的视角, 告诉大家想要在团队中获得成功的另一面。不要被书名误解,我觉得「开发者和团队」是更好的名字, 虽然没那么酷。

要在团队中获得成功,你必须以谦虚尊重信任为核心原则。

要做的第一件事情,应该就是沟通了。让自己成为一个玻璃玲珑人, 其他人可以看到你的方向、目标和里程碑,同时可以看到你的进展和问题点。 这样不但可以获得工作中的肯定,当个人的目标设定和团队出现偏差, 又或是开发过程中在一个点停顿了太久,可以有其他人参与进来或直接伸出援手。

这种透明度对上对下都应该如此。团队的领导, 应当在开发周期内的早期就明确告知团队愿景、目标和设定的里程碑。 产生共鸣的愿景可以让人对目标有渴望,对自己工作有认同。 各位还记得中国中小学开学第一周里,大多都有一个开学典礼讲话。讲的好的领导, 会阐述自己的教学理念,去年取得的成绩,今年的教学着重点。 讲的差的领导就是泛泛而谈,每年都是一套话术,完全看不到长进。

缺失沟通,还会将个人陷于单打独斗的境地,一个篮球队需要 5 个人大, 一个人牛逼没屁用。

提高工程质量的一个有效手段就是 CI(持续集成),将开发过程中一点点的小进展都以一种机械的方式呈现出来, 并进行测试。另一个有效手段是 Code Review,不但推荐要 CR,更是要尽早、快速的 CR。 避免屎积压多了拉,太臭。

我突然想到一条实践:即便是做一个人的项目,在精简程度上也保持最小的一个阈值, 想象明天就要长假,工作要交给别人维护,如何在交付物里面有足够的信息让其他人知晓细节。 而不是丢给后继维护者一句冰冷的话:「看代码」。

沟通必须是有效的,我想任何人都不想听一个嘴碎的人在那边逼逼一下午。 有很多结构化、一部的沟通可以显著提高沟通效率: 项目看板、设计文档、Code Review、代码注释、数据字典等。

第二个重要的观点是,接受失败,承认自己不是无能的。你可能很聪明,但所做的事情不一定完全都是正确的, 连上帝都会犯错,何况是普通人。犯错不可怕,但是犯错还认识不到可怕。犯错并且认识到了, 但是拒绝承认错误的人,不是可怕,而是应该要被淘汰,这类人会极其难以合作。 如何你周围都是这样的人,或者你上司是这样的人,也许你可以考虑换一个地方,在拉钩搜索「堆糖」试试吧。

关于接受失败的另外一个隐含后续发展就是「成长」。意识到这个世界是动态发展的, 「要以发展的眼光看待事物」是一个非常非常有用的认知。 能自己意识到失败,并且会主动复盘,重新认知自己的人,往往会成长的极为迅速。 关于成长的话题可以讨论很深,以后可以单独拎出来讨论。

书中提到一个失败后回顾的清单:

  1. 简要
  2. 时间的时间线,从发现到调查,再到最终见过
  3. 事件发生的主因
  4. 影响和损失评估
  5. 立即修正问题的步骤
  6. 防止事件再次发生的步骤
  7. 得到的教训

我就哈哈哈了,这不就是我大堆糖的故障报告模板么?

第三点,如何成长?简单来说,去冒险,去承担自己能力之外的任务, 去挑战没有经历过的任务。有一条彼得定律:「在组织或企业的等级制度中, 人会因其某种特质或特殊技能,令他在被擢升到不能胜任的职位,相反变成组织的障碍物(冗员)及负资产。」。 前半段含义是,大部分情况下面,并不是具有了相应能力才去承担,而是试着去承担任务。 无论成功与否,对当前去挑战的人来说,都能够得到历练,从而能力得到提升。

第四点是:成为 Leader,而不是 Manager。 一个团队是一艘危机四伏的海面上一只船,如果没有一个船长,那么就前途叵测。 在职业生涯的某些阶段,你可能自然成为船长,也许是一个项目的船长,也许是一个小 Team 的船长。 那么切记,船长是 Leader,而不是 Manager,是能力综合,可守可攻,顺风时候会把舵, 缺人时候可以顶任何岗位的船长。而不是手持大鞭的 Manager。 我觉得新闻联播里面描述的人民公仆,就是一个很好的 Leader。

一年多前之前和铁柱聊过,一个 Leader 是否需要要以能力服众。 我仍然保持当初的观点:「是的」。在目标管理、方向把握上面, 强大的技术背景可以有魄力的开展工作,挖掘新技术,推动变化。 在遇到困难时候,可以决策、解决问题。 这是由这个行业特质决定的,互联网是依赖创造力的脑力劳动,而不是根据人数线性增加产出的体力劳动。

但毕竟不是每个人都一定拥有 Leader 特质,难道就要一辈子做技术得不到上升? Google 的一种做法,可以很好解决这个问题。分离 TL(techlead)和 TLM(techleadmanager), 前者更着重技术,后者不但关心技术,还关心手下工程师的成长。 用国内互联网的职责分工描述,大概就是有技术专家和团队负责人的区别。

关于这条,书中的几点最佳实践非常棒:

  1. 放下自负
  2. 做一个禅师(保持冷静和理性)
  3. 成为催化剂
  4. 当一个导师
  5. 设置明确的目标
  6. 坦诚(三明治赞美法)
  7. 记录快乐程度

最后聊一下对书本身的评价。黄易山在 Quora 写过一段非常有名的 为什么工程师管理这么难?。 这本书讨论的内容要比黄易山那篇回答范围更大,讲述的也更详细(废话,这是书)。 作者是典型的工程师,书目结构易读,第五章从反模式来思考问题非常赞。

我读过几本技术管理相关的书籍,印象深刻的有两本,一本是温伯格的成为技术领导者,另外一本是此书。温伯格的行文比较跳跃、比较抽象,不容易读。 而这本书不但通俗异动,也添加了非常具有可操作性的最佳实践。 从创造力驱动的角度出发,技术开发者都是管理者。因为他们需要设计方案,创造价值,而不是重复劳动, 所以我推荐每个开发者阅读。

好了,学习够了充分的理论,下面就是做起来了,「知行合一」。


开给自己的处方:

  • 上面提到的最佳实践
  • 谦逊:谦逊一些,低调一些,向他人学习
  • 坚毅:认准目标,稳步前行,不放弃
  • 信心:信念也许可以重建,但是对自己始终保有信心,也许会错,但是要相信自己的判断
  • 开会技巧:超过 5 人的会用单向宣讲更有效

原文链接: https://blog.alswl.com/2016/02/team-geek/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
jaseywang's avatar

OpenVPN Connectivity Issue in Public Network

We established a ovpn tunnel between 2 IDCs in September 2014, and we have monitored the availability and performance of two ends for a long time. The geographical distance between 2 IDCs are quite short, but with different telecom carries. mtr shows that there exits ahout 7 hops from one end to the other. The below screenshot shows the standard ping loss.


The result is quite interesting. At first we used UDP protocol, and we often experienced network disconnection issue, later we switched to TCP, and it improved a lot. From the digram, the average package is 1.11%.
Why 1.11%, what I can explain is the tunnel is often fully saturated during the peak hour, and this can't solved at the moment, so no matter what protocol, the package loss should exists. Another possible reason is the complexity of public network which I can't quantitate.
The current plan works during current background, but no "how many 9s" guarantee. Anyway, if we want to achieve more stable connectivity, a DLL(dedicated leased line) is a better choice.

alswl's avatar

一例 Timeout 故障

早晨刚到公司, HAProxy 报警,Trtornis(第三方云存储网关,用来统一管理阿里云和七牛云的对象存储) 全飘红。

检查日志,并没有 ERROR 信息,但是大量 WARN 报错。

WARN  [2015-12-09 11:01:02,730] org.eclipse.jetty.util.thread.QueuedThreadPool: dw{STARTED,8<=50<=50,i=0,q=1024} rejected org.eclipse.jetty.io.AbstractConne
ction$2@62c021c6
WARN  [2015-12-09 11:01:02,731] org.eclipse.jetty.io.SelectorManager: Could not process key for channel java.nio.channels.SocketChannel[connected local=/10.
1.1.78:8350 remote=/10.1.1.74:63290]
! java.util.concurrent.RejectedExecutionException: org.eclipse.jetty.io.AbstractConnection$2@62c021c6
! at org.eclipse.jetty.util.thread.QueuedThreadPool.execute(QueuedThreadPool.java:362) [tritonis-shaded.jar:0.0.1-SNAPSHOT]
! at org.eclipse.jetty.io.AbstractConnection$FillingState.onEnter(AbstractConnection.java:379) ~[tritonis-shaded.jar:0.0.1-SNAPSHOT]
! at org.eclipse.jetty.io.AbstractConnection.next(AbstractConnection.java:273) ~[tritonis-shaded.jar:0.0.1-SNAPSHOT]
! at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:563) ~[tritonis-shaded.jar:0.0.1-SNAPSHOT]
! at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:82) ~[tritonis-shaded.jar:0.0.1-SNAPSHOT]
! at org.eclipse.jetty.io.SelectChannelEndPoint.onSelected(SelectChannelEndPoint.java:109) ~[tritonis-shaded.jar:0.0.1-SNAPSHOT]
! at org.eclipse.jetty.io.SelectorManager$ManagedSelector.processKey(SelectorManager.java:636) [tritonis-shaded.jar:0.0.1-SNAPSHOT]
! at org.eclipse.jetty.io.SelectorManager$ManagedSelector.select(SelectorManager.java:607) [tritonis-shaded.jar:0.0.1-SNAPSHOT]
! at org.eclipse.jetty.io.SelectorManager$ManagedSelector.run(SelectorManager.java:545) [tritonis-shaded.jar:0.0.1-SNAPSHOT]
! at org.eclipse.jetty.util.thread.NonBlockingThread.run(NonBlockingThread.java:52) [tritonis-shaded.jar:0.0.1-SNAPSHOT]
! at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635) [tritonis-shaded.jar:0.0.1-SNAPSHOT]
! at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:555) [tritonis-shaded.jar:0.0.1-SNAPSHOT]
! at java.lang.Thread.run(Thread.java:745) [na:1.8.0_51]

这是七牛 API 调用出现问题,通过应用 Metrics API 检查 Threads:

http http://A-DOMAIN:8351/threads | sort | uniq -c | sort -gr G qiniu

正常的实例:

异常的实例:

确认是七牛服务慢导致现成爆掉,但是 46 这个值来自于哪里?

检查了 App 的配置文件,的确配置了 maxThreads 为 50:

server:
  gzip:
    enabled: false
  requestLog:
    appenders: []
  maxThreads: 50
  applicationConnectors:
    - type: http
      port: 8350
      acceptorThreads: 2
      selectorThreads: 2
  adminConnectors:
    - type: http
      port: 8351

一般对应服务不稳定,有个简单策略,超时,那么七牛服务超时是多少?找了一下相关代码,下巴都没合上:

# com.qiniu.http.Client
    this.httpClient.setConnectTimeout((long)Config.CONNECT_TIMEOUT, TimeUnit.SECONDS);
    this.httpClient.setReadTimeout((long)Config.RESPONSE_TIMEOUT, TimeUnit.SECONDS);

# com.qiniu.common.Config
public static int CONNECT_TIMEOUT = 10000;
public static int RESPONSE_TIMEOUT = 30000;

超时时间 8h,吓得我赶紧关掉了显示器。 赶紧将这个 Connection 改成了 2 分钟,Connect 改成了 5s。

咨询了七牛的工程师故障当天的状况:

sorry 忘了回复,问题已经定位,早上因为有个别用户list请求数量过大,导致了整个list接口出现大量满请求,影响返回是list接口的调用 和 portal上内容管理界面列取文件key的速度,目前已经恢复

大量慢请求,这个已经内部有告警和恢复机制,11点25恢复的

国内一线的云服务供应商也会出这样不可用故障,即便是有服务放在云上,还是要自己留一个心眼,多关注可用性啊。


原文链接: https://blog.alswl.com/2016/02/a-timeout-fault/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
alswl's avatar

一次艰难的 Wiki 升级

公司使用 Confluence 管理自己的知识库, 现在使用的版本还是 3.0.1,而最新的 Confluence 版本已经是 5.4+。 新版本加入的一些现代化 Web 系统的新特性很吸引人(拖拽上传,可见即所得编辑), 在群众的强烈呼声下,我着手开始升级。

官方的升级路线很扯,3.0.1 的升级路线是:

  • 3.0.1 -> 3.5.17
  • 5.0.3 -> 5.4.4

中间两次大版本升级,第一次原因不明,第二次是更新了 markup 渲染引擎, 改为 HTML 格式类型的渲染模式。

由于一些原因,我们系统还跑在 embedded 模式下(其实就是 HyperSQL),这种大版本升级, 需要先从内置库升级到外部数据库,比如 MySQL。

苦逼旅程就开始了。

From embedded to MySQL

更新内置库到外部库的操作流程:

  • 导出当前的数据备份,包括附件,我导出后 1G+
  • 使用当前同版本(3.0.1)安装一个全新的 wiki,注意下载 JDBC-connector
  • 安装之后,配置好 MySQL,开始导入之前准备好的备份
  • 悲剧上演

遇到了错误:

Import failed. Hibernate operation: could not insert: [com.atlassian.confluence.core.BodyContent#12028015]; SQL []; Duplicate entry '12028015' for key 'PRIMARY'; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '12028015' for key 'PRIMARY'

官方文档 https://confluence.atlassian.com/doc/troubleshooting-xml-backups-that-fail-on-restore-199034.html 让修改重复键数据,好吧,我改,搜索一把重复主键,将备份里面的 entities.xml 弄出来。

# 格式化
awk '{s=s $0}END{gsub(/></,">\n<",s);s=gensub(/>([^ \n>]*)</,">\n\\1\n<","g",s);print s}' entities.xml > entities.xml.format
# 找重复主键 cat entities.xml.format G 'content" class="Page' -A 2 G -E '[0-9]+' | sort | uniq -c | sort -gr L cat entities.xml.format | grep 'name="id"' -A 1 -B 1 | grep -E '[0-9]+' -B 2 L

操作过程中,发现有数据就一条数据(grep entities.xml),还是插入重复(13238835)。 官方文档解释是,内置数据库的锁有时候会不灵,插入重复键。 于是决定再试试去掉主键约束方案,大不了那个数据我就不要了。

ALTER TABLE BODYCONTENT DROP PRIMARY KEY;

结果还有其他 PK 约束,我于是一条一条解开,然后……还是不行,真是作了一手的好死。

结论是,这数据错误了太多,已经无法手工修复。

横插一刀的 Emoji 😊😢💗

导入时候报了这么一个错误:

Caused by: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x8C\x8D\xE5\x9B...' for column 'BODY' at row 1 org.xml.sax.SAXException: Error while parsing 2015-10-19 23:14:13,108 ERROR [Importing data task] [confluence.importexport.impl.ReverseDatabinder] fromXML Error processing backup: -- referer: http://10.1.2.155:8087/setup/setup-restore-start.action | url: /setup/setup-restore-local.action | userName: anonymous | action: setup-restore-local org.xml.sax.SAXException: Error while parsing net.sf.hibernate.exception.GenericJDBCException: could not insert: [com.atlassian.confluence.core.BodyContent#12028161]

这是 Emoji 编码的问题,理论上 MySQL 换到 5.6+,更新 encoding 就可以了。

但是……Confluence 的建表 SQL 爆出了 255 varchar 超过 1000 限制的错误 ,我尝试使用 innodb_large_prefix 似乎可以解决(因为重复键的问题,导致导入已经行不通)。

另外 innodb_large_prefix 是 5.6.3 才有的,只能升级 MySQL, 并且需要创建表时候使用 DYNAMIC 参数。

弄个 Emoji 这么绕,这导致我直接弃用了 MySQL。

如果是正常迁移,不遇到重复键,Emoji 的问题,可以参考官方的文档,完成平滑迁移:

  • https://confluence.atlassian.com/doc/migrating-to-another-database-148867.html
  • https://confluence.atlassian.com/doc/database-setup-for-mysql-128747.html
  • https://confluence.atlassian.com/doc/upgrading-confluence-4578.html
  • https://confluence.atlassian.com/doc/upgrading-confluence-manually-255363437.html
  • https://confluence.atlassian.com/conf56/confluence-user-s-guide/creating-content/using-the-editor/using-symbols-emoticons-and-special-characters

妈蛋,自己干

上面这么点东西,陆陆续续花了我两周的时间(晚上)。已经确认走不通平滑迁移,那就别怪我手段糙了。

使用 API 导出后直接导入,这种做法最大问题是不平滑,会丢掉 Wiki 修改的历史记录, 在和各个业务方沟通之后,最后达成了一致:可以暴力升级。

升级流程:

  • 准备最新 Confluence 新站点
  • 关停站点
  • 导出数据,包括 Page、评论、附件
  • 导入 Page,评论,附件
  • 启动旧站点,开启只读模式
  • 启用新站点

官方有一个 Universal Wiki Converter, 我在 Bitbucket 上面找到了源码,但是已经不可工作了。 虽然宣称「The UWC will however save you 1-2+ weeks of scripting development time, compared with starting from scratch, for many of the most common conversion cases.」 但并没有卵用。

不行就自己随便搞搞好了,看了一下开发需要的 Conflunce API, 和尤其贴心的新版本 RESTful API,就开始搞了。

写迁移代码,在这里 atlassian-confluence-xxoo,已经开源了,只使用过一次,成功的从 3.0.1 迁移到 5.4.4, 理论上,支持任意版本的 3.x/4.x Confluence 迁移到最新。

使用 python app.py -h 查看帮助,不行就看看代码。

希望有迁移需求的同学,搜索到这里能够获得一些帮助。


原文链接: https://blog.alswl.com/2016/01/confluence-upgrade/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
Li Fanxi's avatar

2016新年好

2015年,我做了这些事:

– 写了3篇博客

博客空间总访问量66949 PageView(Google Analytics数据),出乎意料地比前一年上涨14.7%,但还是没有达到2013年时的水平。首页、在Linux下使用“360随身WiFi 2”calibre常见问题为Raspberry Pi 2编译内核模块这几个页面的PV占总PV的50%,2015年新写的文章中有一篇能排进前50% PV,也算欣慰。饭否发消息118条,包括照片19张。

– 自由软件相关

Richard Stallman的传记《Free as in Freedom》第一版中文版翻译工作在经历了各种坎坷和挫折后终于完成了,中文版正式命名为《若为自由故——自由软件之父理查德·斯托曼传》。这本书是与GNU中文翻译小组协调人邓楠共同翻译完成的,我负责部分的翻译质量我个人并不十分满意,一方面因为英文水平的原因,一方面也是因为后期做得确实太过仓促。

图书出版后,出版社投入了很多资源进行推广,包括通过哲思社区邀请到Stallman本人来中国演讲并签售,不过总体效果还是差强人意。计划出版第二版的事情,目前也因为各种原因暂时搁浅。出乎意料的事情倒是第一版的Kindle版本很顺利的上架了,虽然Stallman对此也许不太高兴,因为他说过,Kindle是Malware

值得一提的是,这本书是国内少有的依据GFDL许可证出版的图书,我很用力地推动出版社编辑去完成了这件几乎不可能的事情。当然,因为众所可以理解的原因,这件事做得非常低调,但是如果你仔细看这本书的版权页,除了能看到GFDL的声明外,还能看到一些很微妙的变化。

– 几个IT产品

Myo手势控制手环:其实是2014年买的,只不过一直跳票,直到2015年才到货。到手一天就解毒了,直接放到闲鱼上去转让给别人了。这真的只是个实验品,要实现它的官方Demo上那些很炫酷的功能,还有万里长征要走。

Raspberry Pi 2:跟一代相比,性能提升了很多。很多人批判树莓派系列产品的性能、性价比、实用性,我觉得都用力用错了方向。这东西原本的定位就是个电脑学习机,它的优势在于丰富的资源和强大的社区。如果需要更低的价格、更好的性能,完全可以选择其它的嵌入式设备。

联想newifi路由器:在最低69元的成交价格上,买到一个802.11AC的路由器,并且还能刷成华硕固件或者OpenWrt固件,而且运行还非常稳定,很值了。虽然它只有百兆的LAN口,虽然它5G信号很弱。当然,它的原生固件实在不忍吐槽。

华硕AC-66U路由器:曾经的高端路由器,今年最低打折到了不到500元,买回来几乎没有太折腾就扔到角落让它勤勤恳恳的工作去了。这才是一个合格路由器应该具有素质!当然,实际上我还是刷了一次机,改了很多脚本,为了可以顺畅使用Google搜索,还为了实现电信、移动双线接入和策略路由。

华为荣耀4X(二手,两台)/5X/7手机:今年年内居然买了4台华为手机,除了7以外还都是最低端的机器。实际使用体验还算不错,第一次发现Android手机不root也可以用的好好的。当然,低端就是低端,屏幕色彩很差,外放音质也很差,做工/手感也比较一般,不过日常使用并无大碍,电池续航能力也不错。

– 旅游

2015年0出游,铁路运转里程0。

– 其它

还有一件很重要的事情,不放在博客上说。

展望2016年:

2016年是挺多事的周年纪念的,比如初中毕业20周年、研究生毕业暨工作10周年等等,年纪越大越喜欢怀念过去。当然,怀念是为了总结,为了前进。

Li Fanxi's avatar

关注2016维也纳新年音乐会

曾经关注过的那些维也纳新年音乐会:关注维也纳新年音乐会,2016年将是我第21次收看维也纳新年音乐会的直播。

2016年维也纳新年音乐会将由拉脱维亚指挥家马里斯·杨松斯(Mariss Jansons)执棒,他2006年第一次登上维也纳新年音乐会的指挥台,史无前例的在一场新年音乐会中演绎了23首乐曲。而他在2012年的再次登台,又打破了这个纪录,达到了24首。

2016年新年音乐会曲目的单的正式消息来得特别晚,直到上周才在维也纳爱乐乐团的官居网上放出。总共21首。其实一个月前就有小道消息放出了这个曲目单,不过在这个谣言满天飞的时代,我还是等正式消息再来写这篇文章。

2016维也纳新年音乐会CD封面

上半场:

  • 01 – Robert Stolz – UNO Marsch – 国际联合进行曲
  • 02 – Johann Strauss II – Schatzwalzer; op. 418 – 珍宝圆舞曲 (2003, 2009)
  • 03 – Johann Strauss II – Violetta-Polka française; op. 404 – 维奥列塔法兰西波尔卡
  • 04 – Johann Strauss II – Vergnügungszug; Polka schnell; op. 281 – 游览车快速波尔卡 (1970, 1982, 1987, 1992, 2001, 2005)
  • 05 – Carl Michael Ziehrer – Weana Madl’n; Walzer; op. 288 – 维也纳的少女圆舞曲
  • 06 – Eduard Strauss – Mit Extrapost; Polka schnell; op. 259 – 特快邮车快速波尔卡 (2000)

下半场:

  • 07 – Johann Strauss II – Ouverture zu Eine Nacht in Venedig – 威尼斯之夜序曲 (1994, 2001, 2009)
  • 08 – Eduard Strauss – Außer Rand und Band-Polka schnell; op.168 – 激动万分快速波尔卡
  • 09 – Josef Strauss – Sphärenklänge; Walzer; op. 235 – 天体乐声圆舞曲 (1954, 1964, 1979, 1980, 1983, 1987, 1992, 2004, 2009, 2013)
  • 10 – Johann Strauss II – Sängerlust-Polka française; op. 328 – 快乐的歌手法兰西波尔卡(维也纳童声合唱团)
  • 11 – Josef Strauss – Auf Ferienreisen; Polka; op. 133 – 假期旅行波尔卡(维也纳童声合唱团) (1988, 1995)
  • 12 – Johann Strauss II – Fürstin Ninetta – Entr’acte zwischen 2. und 3. Akt 轻歌剧《侯爵夫人尼奈塔》第三幕间奏曲
  • 13 – Emil Waldteufel – Valse-España; op. 236 – 西班牙圆舞曲
  • 14 – Joseph Hellmesberger/Vater – Ball-Szene 舞会场景
  • 15 – Johann Strauss I – Seufzer-Galopp; op. 9 – 叹息加洛普 (1991)
  • 16 – Josef Strauss – Die Libelle; Polka Mazur; op. 204 – 蜻蜓玛祖卡波尔卡 (1954, 1983, 1989, 2000, 2002, 2008)
  • 17 – Johann Strauss II – Kaiser Walzer, op.437 – 皇帝圆舞曲 (1975, 1982, 1987, 1991, 1996, 2003, 2008)
  • 18 – Johann Strauss II – Auf der Jagd; Polka schnell; op. 373 – 在猎场上快速波尔卡 (1954, 1979, 1988, 1993, 2005, 2010)

加演:

  • 19 – Johann Strauss II – Im Sturmschritt; Polka schnell; op. 348 – 飞奔快速波尔卡 (1990, 2004)
  • 20 – Johann Strauss II – An der schönen blauen Donau, Walzer, op. 314 – 蓝色多瑙河圆舞曲
  • 21 – Johann Strauss I – Radetzky-Marsch, op. 228 – 拉德茨基进行曲

每次看新一年的曲目单时,总会在脑中冒出两种想法:“又来”、“这是什么鬼”。这恰恰就是对每届新年音乐会不变的期待:期待看到经典曲目的全新演绎,也期待在这个舞台上见到更多新的作曲家、新的作品。

2016年新年音乐会引入了三位新的作曲家的作品:罗伯特·施托尔茨的《联合国进行曲》、埃米尔·瓦尔德退费尔的《西班牙圆舞曲》以及老约瑟夫·赫尔梅斯伯格的《维也纳的舞会场景》。约瑟夫·赫尔梅斯伯格的舞曲作品在历年的新年音乐会上已经多次出现,而他父亲老约瑟夫·赫尔梅斯伯格的作品还是第一次出现,不知道会带来何种耳目一新的感觉。除此之外,非施氏家族的曲目还选择了齐莱尔的《维也纳的少女圆舞曲》,这也是一个首次在新年音乐会上亮相的节目。

2016年是爱德华·施特劳斯逝世100周年,指挥和乐团特别选择了两首爱德华的波尔卡舞曲,纪念这位施特劳斯家族最小的成员。不过群众呼声很大希望能听到的爱德华的圆舞曲作品依然没有露面。

指挥杨松斯在2012年时请来了维也纳童声合唱团的小朋友们,这个拥有500多年历史的合唱团的小歌唱家们,在2016年的元旦将又一次用他们的天籁之声征服全世界的听众。

2016年的曲目中有几首耳熟能详的“又来”曲目,比如:《游览车》、《天体乐声》、《蜻蜓》、《皇帝》和《在猎场上》,当然这些就算是新年音乐会的经典作品了。看到名字与旋律一样优美的《天体乐声圆舞曲》,又不禁让我想起所谓“天体乐声大魔咒”了:在新年音乐会历史上,波斯科夫斯基、卡拉扬、克莱伯、穆蒂这几位大师在演完这个曲子以后就都没有再上过维也纳新年音乐会的指挥台了。只有马泽尔和巴伦博伊姆破解了这个“魔咒”,但是他们有一个共同点,就是在首次登台新年音乐会时就指挥演奏了这个曲目。扬松斯是我很喜欢的指挥家之一,我可真心希望他不要被这“魔咒”所困。

2016年是我收看维也纳新年音乐会直播的20周年,对1996年洛林·马泽尔在新年音乐会上第一次用中文“新年好”向全世界问好的场面仍然记忆尤新。十周年的2006年前后,我在QQ的“施特劳斯之声”群和一些论坛认识了Duckula、苏大米、JosefKitty、king_zhd、定定、蓝色多瑙河等很多有相同爱好的朋友,并且花了很多时间通过各种渠道尽可能收集了历年新年音乐会所有录音录像资料,估计是那时在网上可以找到的整理得最完整的一套录音资料了。2015年是维也纳新年音乐会创办的75周年纪念,两个月前SONY为此出版了一套维也纳新年音乐会75周年曲目全集CD, 收录所有在新年音乐会上演出过的曲目,还特地补录了所有早年没有录音时演出过的曲目。拿到这套CD,回想起以前一首首曲子的寻找的日子,颇为唏嘘。

alswl's avatar

SS with Haproxy

以前用自己的 SS,Linode 美国,后来 Linode 日本,但是始终拼不过上海电信的国际带宽。 经常不稳定,丢一半的包。

于是买了 SS 服务, 9 台服务器,自己挑觉得速度快的服务器。

但一直固定某台服务器也会偶尔出问题,导致邮件出不来,网页打不开。 需要手动切换一下服务器。 于是用 HA 做了一个本地代理,调整了一些参数,让 SS 总是有快速的服务器供选择。

结构:

+-----------------+                                                  +----------------+
|                 |                                                  |                |
|    Server 1     |>>>>v                                           >>|   Mail.app     |
|                 |    v                                           ^ |                |
+-----------------+    v                                           ^ +----------------+
                       v                                           ^
+-----------------+    v    |----------------+      +------------+ ^ +----------------+
|                 |    v    |                |      |            | ^ |                |
|    Server 2     |>>>>>>>>>|    HAProxy     |>>>>>>| SS-Client  |>>>|   Browser      |
|                 |    ^    |                |      |            | v |                |
+-----------------+    ^    +----------------+      +------------+ v +----------------+
                       ^                                           v   
+-----------------+    ^                                           v +----------------+
|                 |    ^                                           v |                |
|    Server 3     |>>>>^                                           v>|   Evernote...  |
|                 |                                                  |                |
+-----------------+                                                  +----------------+

配置:

global
    ulimit-n  4096


defaults
    log global
    mode    tcp
    timeout connect 1s
    timeout client 1s
    timeout server 1s


listen stats
    bind 127.0.0.1:12222
    mode http
    stats enable
    stats uri /
    stats refresh 8s


listen ss
    bind 127.0.0.1:1081
    mode tcp
    option tcpka
    #balance source
    balance roundrobin
    log global
    maxconn 1024

    timeout connect 200ms
    timeout client 600s
    timeout server 600s
    timeout check 80ms  # for office / home
    # timeout check 400ms  # for starbucks
    retries 1
    option redispatch
    option tcp-check

    server s1 host1:port maxconn 20 check inter 2s rise 30 fall 6 backup
    server s2 host2:port maxconn 20 check inter 2s rise 30 fall 6
    server s3 host2:port maxconn 20 check inter 1s rise 60 fall 6

挺稳定,很快速。

update: 2015-12-15,添加 backup 项,选一台最稳定的做 backup,避免所有连接都超时。 update: 2015-12-13,添加 redispatch / retries 项,换机器重试, 大幅提高可用性,注意,可能在非幂等状态下面产生未知错误。

在跑的 node,有些延迟高,被干掉了。

看 1080P 也挺顺畅。


原文链接: https://blog.alswl.com/2015/11/ss-with-haproxy/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
sunjw's avatar

fHash 2.0.0 for Mac OS X

fHash 2.0.0 for Mac OS X is available on Mac App Store now!

fHash is an open source files’ hash calculator for Windows and Mac OS X

  • MD5, SHA1, SHA256 and CRC32.
  • Drag & drop.
  • Integrated with context menu in Explorer/Finder.
  • Support multiple files.
  • Support 10.10 and later Mac OS X.
  • English and Simplified Chinese UI.

The icon “希” is from hash in Chinese “哈希”.
Thanks for Trend Micro Mobile Club’s support.

Project Site https://github.com/sunjw/fhash

LICENSE
GPL 2.0 for codes hosted on GitHub.
May use other licenses for binary package on other distribution sites.





sunjw's avatar

JSToolNpp 1.16.10 Released

What’s New in JSToolNpp:

  • 1.16.10
    • Fix configuration saving bug.
  • 1.16.8
    • Fix regex related bug.
  • 1.16.6
    • Fix negative number bug.
    • Fix some expression format bug.
  • 1.16.5
    • Fix regex detection.
    • Fix some crash bugs.

Project site:
http://www.sunjw.us/jstoolnpp/

Download links:
SourceForget.net

jaseywang's avatar

Can Venta Airwasher Effectively Reduce PM2.5 or PM10?

There is not official tests state that whether Venta airwasher is able to reduce PM2.5 or PM10 effectly, hence, I made some tests with the help of Dylos air particle counter during the good weather, PM2.5 index less than 100 means good or moderate, PM2.5 larger than 100 which means unhealthy or even hazardous. My bedroom's door or window is closed all the time and it's a confined space.

The answer is partially efficient. In centain conditions, it works, other times, no.

When the PM2.5 outside is less than 100, Venta can effectly keep the indoor PM2.5 around 30 or even less.

However, when outside is unhealthy, which means the index is above 100 and beyond, it really can't effectly reduce the particles inside, and the metrics I got from Dylos indoor have positive correlation with outside index, sometimes, Dylos even get 100 or even more which is totally unacceptable for people in the bedroom.

Now, the answer is quite clear, if the weather outside is good, just keep the Venta open and it can handle. When outside is unhealty, you really need to turn on your air filter, and don't rely on it, you need HEPA filter.

What about PM10? Venta seems work all the time, no matter good or bad outside, it can keep the index under 100 or lower, most of time, my bedroom PM10 is around 50, sometimes, after long time filter without opening the door, you can see single digit.

alswl's avatar

几步拥有一个安全密码

给团队非开发同学写的邮件,对其他人也有些意义,遂贴出来。


这个互联网越来越不安全 https://www.baidu.com/s?wd=%E5%AF%86%E7%A0%81%E6%B3%84%E9%9C%B2%E4%BA%8B%E4%BB%B6。密码数据库泄露,黑客暴库攻击,社会工程学攻击层出不穷。我给大家介绍几个小方法,轻松提高自己各类密码的安全等级。

先给个地址,大家可以测试一下自己常用密码的复杂度:https://howsecureismypassword.net/

认为自己电脑水平还可以的,请直接翻到文章最后。

如何管理密码

给普通用户的建议:

  • 密码设置复杂一些,不要使用生日、日期、姓名等有意义的信息
  • 使用一套合理的密码生成策略

重点来了,密码生成策略:

  • 选择自己喜欢的诗词或者某句话,比如「床前明月光,疑是地上霜」,取其拼音的第一个字母 cqmygysdss
  • 将每个句子第一个单词大写,cqmygysdss -> CqmygYsdss
  • 加上对应网站的信息,比如 163:CqmygYsdss163,qq: CqmygYsdssqq

大功告成,你的密码安全级别提升了。

----------------------------- 高手的分界线 -----------------------

给高水平选手推荐的方案:

  • 核心思想:密码分级,分离普通密码、重要密码(财务相关等)。
  • 普通密码使用大小写英文和数字混合
  • 重要密码和财务相关密码,独立生成密码,保存到 1Password / KeePass 等工具中,每个应用软件独立开来。
  • 推荐 KeePass,免费,参考 http://www.iplaysoft.com/keepass.html

常见弱密码和暴力破解需要时间

  • 123456 / 不需要
  • duitang / 2s
  • huhela1993 / 10天
  • huhela_1993 / 48年
  • CqmygYsdssqq / 3千年
  • ]W?852HCMHFUYzrz,F / 10000^5 年(Keepass 生成的密码)

最后

针对任何在邮箱中向大家索取:

密码、员工信息、通讯录、组织架构等的邮件,请大家注意确认对方的真实身份,不要轻易透露重要信息。


原文链接: https://blog.alswl.com/2015/10/a-security-password/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
jaseywang's avatar

Metrics Dashboard Comparision for Linux Desktop

I need to know how good/poorly my desktop(Loongson & Rasberry Pi 2) is running, so there should at least exits a solution that can see its realtime and history metrics of the the system like cpu, memory, network, disk io etc.

Also tools like Glances, nmon, sar(sysstat) or atop provide a nice realtime metircs, it can't get its history data easily which it more suitable for massive production use, also no web dashborard for a nicely view, so I just pass them. Systems like Icinga, Nagios, Graphite, Zabbix, etc. are so heavy and complicated which are also not convenient for a 2GB memory desktop use.

Below are some tools that may help you for setting up a tiny to small monitoring/metrics system. At the end, I'll pick up the most suitable one for my own.

psdash
A dashboard overview of the system using psutils and Flask.

The installation is quite straightforward with pip packager manager. Unfortunately, As my Debian jessie testing version, I happened to a "PROTOCOL_SSLv3 is not defined" bug that stop the system running, you can modify the python gevent lib to work around(1, 2).   

The web dashboard is qute lighweight and nice, you can get the realtime data from the every-3-second refresh web automatically, the fatal problem is it can't get the history data and charts.

Linux-dash 
Quite similar to psdash, but comes with more technical stack like Node.js, Go, and PHP. It's offical slogon is "A simple, low-overhead web dashboard for GNU / Linux. (~1MB)", besides that, not much eye-catching point.

ezservermonitor-web 
Besides all the feature psdash and Linux-dash, it also has a simple trigger configuration, Say, for the load average metric, it will be displayed with gauges, when is less than 50%, it's green, when is more than 76% util, it turns to red. Also, it has the built-in ping and servers check function, which is handy for your family internal use.

Ezservermonitor also has a console based tools called EZ SERVER MONITOR`SH, without web interface.

Web VMStat 
It’s a small application written in Java and HTML which displays live Linux system statistics. It just takes over vmstat command in a pretty web page with SmoothieCharts and websocketd in realtime.

munin 
The last is usually the best. Yes, it's the only one I want to recommend for you, no matter how many desktops you have, Munin can handle them easily and more importantly, it only taks you few minutes depending on your network quality if you use apt/yum to install it directly, the real out-of-box product. it used RRD as it's backend storage engine. by default, 300s interval which I think it's enough for most of desktop users, with at one year history. 

If you are unsatisfied with its hundreds of plugins, just write a new one or porting from any Unix platform by yourself by any scripts language. This is its killer feature.

After the above comparision, you could choose ones according to your demands.

Want near-realtime(5m or so) and history data with charts? Munin is a best option.

Want realtime data, without history data? psdash, Linux-dash, Web-VmStat are those you're looking for.

Still not satisfied? You may consider using Graphite, Zabbix if you have system admin experience, since both are enterprise level open source product.

Li Fanxi's avatar

为Raspberry Pi 2编译内核模块

2015-04-21更新:原始的rpi-source项目已经由PeterOGB 接手维护,所以无须再用我下文中提到的我改过的rpi-source脚本,直接用原始的就可以了。其它文中提到的背景知识都仍然有效。

即把第一个命令改为:
$ wget https://raw.githubusercontent.com/notro/rpi-source/master/rpi-source
&& chmod a+x rpi-source

2015-07-29更新:Raspbian的内核版本已经升级到4.x,rpi-source还不能正确处理,需要进行以下额外的工作:

1. rpi-source需要获取的/proc/config.gz默认不存在了,需要额外加载模块来实现:

$ sudo modprobe configs

2. rpi-source在4.x内核下无法正确检测gcc版本,运行rpi-source时请加–skip-gcc选项。


 

在Linux下使用“360随身WiFi 2”》一文的留言区中,曾经有人问过,为什么编译出来的模块insmod/modprobe时报“Exec format error”,我不假思索的回复,请他检查编译模块时用的内核头文件与实际运行的内核是否完全匹配。这个答案倒也不算错,不过其实并没有解决问题,因为遇到的这个问题的人一般都已经用了“正确”的方式去编译他的模块,就算再重新做几遍,还是会遇到一样的问题。

最近我给Raspberry Pi 2编译内核模块时,遇到了一样的问题,花了很多时间才真正解决,在这里总结一下。以下描述的方法和内容,对Raspberry Pi (A/A+/B/B+)和Raspberry Pi 2都适用。

准备编译模块需要的内核树的方法(适用于Raspbian):

1. 下载我改过的rpi-source脚本
$ wget https://raw.githubusercontent.com/lifanxi/rpi-source/master/rpi-source
&& chmod a+x rpi-source

2. 运行rpi-source
$ ./rpi-source

3. 好了,可以进入模块源代码的目录进行模块编译了。

疑难排解:

1. rpi-source报gcc版本不一致

截止2015-03-12,Raspbian最新的内核是用gcc 4.8编译的(可以查看/proc/version确认),而Raspbian中自带的gcc是4.6的,需要升级到4.8。因为4.8的gcc已经backport了,所以可以直接sudo apt-get install gcc-4.8 g++-4.8,然后用update-alternatives设置优先级即可[1]。

2. 如果用rpi-source –skip-gcc忽略gcc版本检查,并强行用4.6的gcc会编译模块怎么样?

我的试验结果是模块可以编译,但在加载模块时会造成kernel oops,然后再用insmod/modprobe/rmmod/lsmod等命令时会挂住,只能重启解决。如果你编的模块是会自动加载的,重启前先把它删掉,不然启动时就会挂住。

3. rpi-source无法正常下载内核代码或Modules.symvers文件

有可能是你的内核版本太老,rpi-source只支持Raspberry Pi 3.10.37以上的内核。对于Raspberry Pi 2,它只支持3.18.6以上的内核。解决办法是先运行sudo rpi-update更新内核和固件,更新后请重启系统,然后再重新运行rpi-source。

4. 编译模块时报找不到arch/armv6l或arch/armv7l目录

尝试在make命令前加ARCH=arm参数,或尝试把/lib/modules/`uname -r`/build/arch中的arm软链为armv6l或armv7l后再编译。

背景知识:

1. Raspbian的内核包

不要按照使用Debian的习惯去找什么linux-image、linux-source之类的包,Raspbian的内核包是raspberrypi-bootloader,里面包含了内核、模块和一些启动文件,但没有Module.symvers和头文件。

2. rpi-update是啥

rpi-update是Raspbian内置的更新内核和相关固件的脚本,它的逻辑是去https://github.com/Hexxeh/rpi-firmware这个仓库下载最新的内核和固件,替换现有的版本。更新完成后会更新/boot/.firmware_revision,记下最新版本对应的Git Hash,以后rpi-update或rpi-source都会根据这个Hash去GitHub找对应文件。

3. Raspberry Pi的官方内核去哪里找

http://github.com/raspberrypi,里面的linux对应内核源代码,firmware是编译好的内核和相关文件。而rpi-update用的https://github.com/Hexxeh/rpi-firmware其实是firmware中部分文件的一个镜像,分出一个镜像仓库可以让rpi-update脚本的实现变得比较简单[2]。

4. rpi-source做了些啥

根据rpi-update记录在/boot/.firmware_revision中的内核版本Git Hash(如果没有用rpi-update更新过内核,就从raspberrypi-bootloaderq包的changlog中解析出Hash),去raspberrypi/linux仓库中获取对应的源代码,把/lib/modules/`uname -r`/build和/lib/modules/`uname -r`/source对应的软链建好,从/proc/config.gz获取当前内核配置,去raspberrypi/firmware仓库中获取对应的Modules.symvers跟内核代码放在一起,然后make modules_prepare准备好编译模块所需要的内核树。

5. 你改的rpi-source改了些啥

rpi-source的作者已经宣布不再维护这个脚本,并且这个脚本不支持Raspberry Pi 2,所以我在GitHub上Fork了一份,做了以下改动:

  • 修改了脚本自动更新URL到我Fork出来的版本;
  • 检查/proc/cpuinfo,判断当前硬件是Raspberry Pi还是Raspberry Pi 2;
  • 可以通过-b参数强行指定Raspberry Pi的硬件版本;
  • 根据不同的硬件,下载不同版本的Modules.symvers;
  • 如果用参数指定了要求用默认配置来配置内核树,则对不同硬件版本的Raspberry Pi调用不同的命令[3]。

6. Raspberry Pi和Raspberry Pi 2的内核有啥区别

Raspberry Pi 2的SOC是BCM2709,基于ARM 7(armv7l),而一代是BCM2708,ARM 6(armv6l),所以二代的内核中用了一些armv7l中特有特性。目前在打包的时候两个版本内核文件是打包在一起的,只是用后缀7或v7来区别,启动的时候会按实际硬件选择。

7. Module.symvers是干嘛用的?

一句话讲不清,有兴趣请参考[4]。总之,没有Module.symvers或用错了Module.symvers都可能会造成你加载模块时报Exec format error。如果你遇到了这样的情况,请确认rpi-source的执行过程中有没有失败的步骤。armv7l和armv6l版本的内核用的Module.symvers是不通用的,在raspberrypi/firmware中分别命名为Module.symvers和Modules7.symvers,但放到内核树中使用时需要命名为Module.symvers,如果是你自己准备内核树,务必要小心,我自己在这个问题上犯了错误,浪费了很多时间。当然,如果用我改过的rpi-source,那它已经帮你搞定了这件事。

8. 我用了rpi-update和rpi-source后编出来的模块还是无法加载。

目前我用本文描述的方法编译了过天猫魔盘(rtl8192eu)、360随身WiFi 2(mt7601u)这两种无线网卡的驱动,都工作正常。如果你遇到了别的问题,不妨在这里留言,可以一起讨论一下。

另外,终级大法一定是重新完整的编译整个内核,不过如果你想在Raspberry Pi上完成这个工作,那必须等有充分的耐心。所以,最好是在PC上进行交叉编译[3]。

[1] https://github.com/notro/rpi-source/wiki

[2] https://github.com/Hexxeh/rpi-firmware/blob/master/README.md

[3] https://github.com/raspberrypi/documentation/blob/master/linux/kernel/building.md

[4] http://www.ibm.com/developerworks/cn/linux/l-cn-kernelmodules/

sunjw's avatar

Build CyanogenMod 12 for Nexus 5

工作之后好久不写博客了,这里都要长草了。。。

之前写过一篇 Build CyanogenMod 10.2 for Nexus 4,现在手机换成 Nexus 5 了,系统也升级到 CM 12 了,写一篇 How to 总结一下。

1. 你还是要准备一台机器(虚拟机也行),装上 Ubuntu,Ubuntu 14 LTS 是个不错的选择,LTS 会有很长时间的软件更新支持。硬盘分的大一些,100GB 起步吧,内存 3G 起,其他随意。

2. 系统装好之后,按照 CM 官方的指导 How To Build CyanogenMod Android for Google Nexus 5 (“hammerhead”) 安装必要的软件。需要注意的是,编译 CM 12 需要 OpenJDK 7,所以安装时,将 openjdk-6-jdk openjdk-6-jre 换成 openjdk-7-jdk openjdk-7-jre,另外 Ubuntu 14 已经没有 ia32-libs 这个包了,需要安装 lib32z1 lib32ncurses5 lib32bz2-1.0 作为 32 位兼容运行时。

3. 之后就可以按照手册准备 repo,使用 repo init -u https://github.com/CyanogenMod/android.git -b cm-12.0 初始化 repo。

4. repo sync

5. breakfast hammerhead

6. 这里建议在 breakfast 之后再 repo sync 一次

7. Nexus 5 的私有库在这里有 https://github.com/TheMuppets/proprietary_vendor_lge,可以将其添加到本地 repo,打开同步好的源码目录,进入 .repo/local_manifests 目录,在其中创建 lge.xml,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<manifest>
  <project name="TheMuppets/proprietary_vendor_lge.git" path="vendor/lge" remote="github" />
</manifest>

之后再 repo sync 一次。

8. 到这里源码同步完成,可以 brunch hammerhead 啦。

需要注意的是,请使用多种方法科学上网。

's avatar

我的 MBP SSD 优化过程

嗯,这次的标题终于没那么长了 ;-)


给我的 MBP 换 SSD 已经是很久之前的事了,当时就已经折腾过一次优化了,不过没有相应的记录。最近发现了新装的 Yosemite 的一些老是搞不定而且连原因都不知道的问题(其中一个可以参见我在 AskDifferent 上的提问),一怒之下重装之,自然对 SSD 的优化也要重新搞一遍。搞的过程中发现有的手段能用,有的却不行,特此记录。

优化手段的主要来源是朋友的一篇博文及其中的链接。

注意:操作有风险,动手须谨慎哟。因为我是不(lan)会(de)做太多解释的,所以你一定要在操作前搞清楚那些个命令的作用哟,特别是像 rmmv 啦之类的危险命令哟,可别说我没提醒你哟 -_-

Firefox 缓存设置

虽然 Firefox 不是我的主役浏览器,但有时还会用用,姑且改之。总之就是打开 about:config 页面,修改如下设置:

  • browser.cache.disk.enable 改为 false
  • browser.cache.memory.enable 改为 true

这样 Firefox 就只会把缓存的东西都放在内存而不是硬盘(也就是 SSD)上了(应该吧)。

关掉根文件系统的「上一次读取时间」特性

「上一次读取时间」指的虽然是文件上次被读取的时间,但这个信息是会写到硬盘上文件的元数据中的。想想系统文件还不是整天被读过来读过去的,但这个时间信息又没什么用,果断禁之!以 root 身份创建(如果之前没有的话)并编辑文件 /etc/fstab,增加如下一行(或者修改原有配置,增加 noatime 挂载属性):

/dev/disk2 / hfs rw,noatime

其中的 /dev/disk2 自然要换成你的 SSD 对应的磁盘文件路径啦。

禁用冬眠模式

执行如下命令即可:

$ sudo pmset -a hibernatemode 0
$ sudo rm /var/vm/sleepimage

PS. 冬眠模式是为了在电池耗尽时能保存机器的运行状态,防止意外丢失数据的。一般认为电池耗尽这种情况很少会发生,毕竟当你发现电池快用光时肯定就会到处找插座了嘛。结果我最近偏偏碰到了一次,而且还是在插着电源的时候!一开始还怀疑难道我的电源线又挂了?!结果后来试着把电脑端反过来接就好了。真是莫名,看来这根电源线也大限将至么……不管怎样,有问题的时候电脑端的 LED 指示灯虽然亮着但是很暗几乎看不出来,难道这表示电压不足?有知道的人么?(不过首先,得有能看到我这篇博的人(泪奔

减少临时文件的读写

RAMDisk

我在试了网上的方法之后,发现如果用了 RAMDisk,则系统很快就会卡死。据陈同学说是 OS X 的 RAMDisk 实现有问题,嗯,所以就要想别的招了。

将临时文件目录移到机械硬盘分区

因为我是将……好吧其实我也不记得主硬盘位放的是哪个硬盘了,Any way :-),SSD 和原机械硬盘现在都被我挂着用,于是就可以考虑将那些个临时文件目录移到机械硬盘上,减少对 SSD 的读写。

网上说可以将 /private/tmp/private/var/run 两个目录挂载到 RAMDisk 上,那想必也是可以挪到其他地方的吧;不过经实践,只有前者可以,后者(至少用这里的软链接方式)会导致系统启动出错,只好作罢。

/private/tmp 移到机械硬盘上的方法:

$ sudo ditto /private/tmp /Volumes/your_hdd_name/private/tmp
$ sudo rm -rf /private/tmp
$ sudo ln -s /Volumes/your_hdd_name/private/tmp /private/tmp

然后其实用户的家目录也可以这样挪到机械硬盘上,不过要注意数据的迁移,所以命令稍有不同:

sudo ditto /Users /Volumes/your_hdd_name/Users
sudo mv /Users /Users.bak
sudo ln -s /Volumes/your_hdd_name/Users /Users
sudo rm -rf /Users.bak

另外还有一个目录 /private/var/log,也就是系统日志目录,我觉得也有必要挪一下,但又怕像 /private/var/run 那样失败,所以一时没有折腾。

禁用 Safari 的 Webpage Previews 功能

我好像没做这条,因为我不怎么用 Safari 的说。

关闭 Spotlight 索引

因为我也不怎么用 Spotlight,所以这个我也(跟上一条连起来,感觉有点儿怪怪的哈)关了,记得就是去系统配置里面把所有的勾都给取消掉就行了,嗯。

关闭「时间机器」功能

反正我是没开过,反正我的重要数据(代码啥的)都在网上有仓库,图片有 Google+ Photos,电子书……你懂的。

据说是该功能在你没插备份盘时会往系统盘备份,要不要关就看你的实际需求啰。

禁用「自动休眠硬盘」选项

就是系统配置、节能器里的那个「Put the hard disk(s) to sleep when possible」。不过我开着这项,因为我还有个机械硬盘嘛。

Trim Enabler

这个是重头戏哟,所以放在最后(不过这样是不是就会被人忽略掉啊,嘛不管了)。

原文中的链接已经坏了,因为那个链接的作者更新了一篇新的,所以把老的给删了,新链接在此。建议好好阅读学习哟。不过对于最新的 Yosemite 来说,有一个更新的脚本

虽说是重头戏,但工作相当于全都丢给别人了嘛,不过这种细节你就不要在意啦。

打完收功!

Li Fanxi's avatar

2015新年好

2014年,我做了这些事:

– (令人发指地只)写了4篇博客

博客空间总访问量58350 PageView(Google Analytics数据),比前一年下降32%,是最近几年以来首次PV下降,不好好维护就是这样的后果。首页、在Linux下使用“360随身WiFi 2”calibre常见问题这几个页面的PV占总PV的50%。饭否发消息187条,包括照片50张。

– 自由软件相关

GDG  Hangzhou的活动依然很丰富,但我几乎没有参加什么线下的活动,越来越有变宅的趋势。

五月份Richard Stallman来中国时,有缘相见。他的传记《Free as in Freedom》第一版中文版翻译工作已经完成,中文版正式命名为《若为自由故——自由软件之父理查德·斯托曼传》,目前还在二校阶段,有望在农历春节前出版。由于RMS本人反对出版第一版,希望能出第二版,所以2015内可能会继续整理出版该书的第二版。

– 几个IT产品

Synology DS-214play:群晖的NAS最大的特色在于它的配套软件,为了“不折腾”,直接买了白群晖。买了就投入使用,没有太折腾。目前为止很满意。配套软件DS Photo+、DS Video等在深度使用后,感觉有点低于期望值,但依然堪用。

Pebble Watch:去美国时正好遇到Pebble Watch降价,就入了一个。其实先前已经关注过这个智能手表,感觉它有很多软肋,但其平台的开放性还是对我有一些吸引力的。Pebble日常使用没有问题,但在我的ZTE手机上时常会有蓝牙连接意外断开的情况,目测是手机的问题,稍有困扰。

GoPro Hero 3:朋友送给我的,我实在不理解这个东西为什么能这么火。作为一个运动相机,它的应用范围实在是太狭窄了,除了在一些极限运动(跳伞、潜水、攀岩)中它有很强的不可替代性外,一般的跑步、登山、骑行用它录下来的视频都没啥可观赏性,可能是我对画质的要求太高了吧。

元征golo 3/4:元征尝试在“车联网”领域发力之作,通过OBD,对车辆进行检测,同时提供3G Wi-Fi热点、轨迹记录、车辆定位等功能。概念上还是不错的,产品一般般,但其配套软件做得实在不敢让人恭维。

乐视X60s电视:渣画质、渣音质、屏幕严重漏光,但在没有比较的情况下,这几点一般人都不会关注。片源丰富是它的最大优点,总体性能来说跟电视+乐视盒子差不多,自带的本地高清播放能力一般,放高清还是得专业盒子才好。

BandwagonHost的VPS:年付$3.99的VPS,安装Shadowsocks后的访问速度比月付$20的Linode VPS快N倍(当然Linode VPS依然是一个很不错的VPS,从功能、服务、稳定性等角度来说),最快的时候可以跑满我的20M中国电信带宽,在路由器上直接配置了Shadowsocks+iptables,上网各种安逸。

– 旅游

美国十九日游桂林四日游

国内铁路运转里程约850公里。体验了纽约地铁、芝加哥地铁。

2012年时所计划的60km以上暴走杭州计划一直没有实施,不过今年完成了一次环西湖群山毅行,路线为老和山-北高峰-石人岭-天竺山-十里郞当-五云山-林海亭-贵人峰-虎跑-玉皇山-凤皇山-云居山-吴山广场,山路行程25.19千米,耗时9.5小时。总体感受比平地徒步50km还是要轻松一点。

– 其它

与其他同事合作翻译出版了《Raspberry Pi创意项目制作》一书。

展望2015年:

2014,很多人、很多事都发生了变化。2015会有更多的变化等待着我,每一天都有更多新的东西等待着发现,加油!

Li Fanxi's avatar

关注2015维也纳新年音乐会

曾经关注过的那些维也纳新年音乐会:关注维也纳新年音乐会,2015年将是我第20次收看维也纳新年音乐会的直播。

今年真是破天荒了,往年都是会在11月中下旬由唱片公司或广播公司透露出次年新年音乐会的曲目单,而这次则是在9月中旬就由维也纳爱乐乐团的官方网站公布了新年音乐会的曲目单

上半场

01 Franz von Suppé – Ein Morgen, ein Mittag, ein Abend in Wien; Ouvertüre – 维也纳的早中晚序曲 – 1990,2000

02 Johann Strauss II – Marchen aus dem Orient Walzer, op.444 – 东方童话圆舞曲 – 2009

03 Josef Strauss – Wiener Leben; Polka francaise; op. 218 – 维也纳的生活法兰西波尔卡

04 Eduard Strauss – Wo man lacht und lebt; Polka schnell; op. 108 – 我们的欢笑与生活快速波尔卡

05 Joseph Strauss – Dorfschwalben aus Österreich, Walzer op.164 – 奥地利村燕圆舞曲 – 1963,1992,2001,2008

06 Johann Strauss II – Zugabe: Vom Donaustrande; Polka schenll; op.356 – 自多瑙河之滨快速波尔卡 – 2000

下半场

07 Johann Strauss II – Perpetuum mobile; Polka; op. 257 – 无穷动波尔卡 – 1954,1978,1980,1987,1988,1993,1995,1999,2002,2010

08 Johann Strauss II – Accelerationen; Walzer; op. 234 – 加速圆舞曲 – 1981,1989,1994,2004

09 Johann Strauss II – Electro-Magnetische; Polka; op. 110 – 电磁波尔卡

10 Eduard Strauss – Mit Dampf; Polka schnell; op. 70 – 蒸汽快速波尔卡

11 Johann Strauss II – An der Elbe; Walzer; op. 477 – 易北河畔圆舞曲

12 Hans Christian Lumbye – Champagner Galopp – 香槟加洛普 – 2010

13 Johann Strauss II – Studenten-Polka; Polka francaise; op. 263 – 大学生法兰西波尔卡

14 Johann Strauss I – Freiheits-Marsch; op. 226 – 自由进行曲

15 Johann Strauss II – Wein, Weib und Gesang; Walzer; op. 333 – 美酒、女人和歌圆舞曲 – 1979,2000,2010

16 Eduard Strauss – Mit Chic; Polka schnell; op. 221 – 雅致快速波尔卡 – 1994

加演曲目尚未公布,按惯例应该是一首波尔卡加上下面这两首雷打不动的曲目(确实是雷打不动,不过2004年底的海啸吹走了2005年新年音乐会上的拉德茨基进行曲):

18 Johann Strauss II, An der schönen blauen Donau, Walzer, op. 314 – 蓝色多瑙河圆舞曲

19 Johann Strauss I, Radetzky-Marsch, op. 228 – 拉德茨基进行曲

担任2015年维也纳新年音乐会指挥的是印度指挥家祖宾·梅塔,他曾于1990年、1995年、1998年、2007年指挥过维也纳新年音乐会。在2007年新年音乐会上,梅塔在威尼斯狂欢节的梦幻回忆一曲中与乐手们的搞怪互动,给人留下了深刻的印象。

由于这次不是唱片公司放出的曲目单,所以CD封面还尚未剧透出来。放一些梅塔曾经指挥过的新年音乐会的CD封面吧。

祖宾·梅塔指挥过的维也纳新年音乐会

整理这个曲目单时给我留下的第一印象是:这次选择了好几首爱德华·施特劳斯的作品,细数一下,总共是3首,并且其中两首是第一次在新年音乐会上演奏的曲目(看资料也许在20世纪80年代以前演奏过,但是我不太了解),这应该是史无前例的。2015年是爱德华诞辰180周年,不知道与这个是不是有点关系。

今年的曲子不算多,19首,应该算是个比较合适的数字。在2012年的新年音乐会中,指挥杨松斯创记录的选了24首曲子,演下来真让人觉得他有点不从心。

总体来说,这个曲目单虽然颇有炒冷饭的嫌疑,而且大多还炒的是15年前的冷饭,但也算是中规中矩,不像最近几年有些很“出格”的安排。开场的序曲、奥地利村燕等乐曲都很有正统维也纳的气息,相信会很让人陶醉。倒是用无穷动作为下半场开场,让人觉得有点不太搭调。

2014年,克劳迪奥·阿巴多和洛林·马泽尔这两位曾经指挥过维也纳新年音乐会的指挥大师离开了人世。阿巴多曾经指挥过颇具争议的1988年和1991年维也纳新年音乐会,而马泽尔指挥过1980-1986、1994、1996、1999、2005年新年音乐会,是我最喜欢的指挥家之一。当我写起这篇文章,细数着过去20年多的新年音乐会,才又一次想起大师的离世,感受到遗憾和忧伤。2005年新年音乐会上,为了表达印度洋海啸的遇难者的哀思,取消了拉德茨基进行曲的演出,成了马泽尔一次像是没有画上句号的演出。那一年的演出中,在维也纳森林圆舞曲中小号手还罕见的出现了失误,多少有一些遗憾。虽然知道不太可能,但是我一直幻想着有一天马泽尔可以再次登上这个指挥台,为他在新年音乐会上的指挥生涯画上一个漂亮的句号。 整整10年过去了,幻想最终也破灭了,愿逝者安息,生者前行。

's avatar

Keepalived 实现双机热备并对关键进程进行监控

三连更有木有!(对不起我网文看多了,而且中间其实断过两天的,我不会告诉你其实是我把这事儿给忘了的)

另,这篇照例是工作需要。


使用 Keepalived 实现多机热备(无负载均衡)时,除了对网口状态的监控外,一般还要对系统关键进程(如 Web 服务器的话就是 nginx 或者 httpd 之类的)进行监控,这时就要用到 vrrp_script 配置。网上能找到的 vrrp_script 示例都使用了 weight 选项,以实现基于优先级机制的切换,我在使用中遇到了一点儿问题,总结一下。

一般在使用双机热备时,如果当前主机——假设为 A ——出了问题(关键进程挂掉,优先级降低,低于备机),这时会自动切换到备机——假设为 B ——继续完成工作,同时等待管理员对 A 的问题进行修复。当 A 的问题修复时,我们不希望 A 马上切换为主机,因为这样有可能断掉用户正在使用的连接,体验不好。在 keepalived 中,通过给 A 增加一条 nopreempt 配置,就可以防止 A 主动抢回主机状态。

这时问题就来了。当 A 的问题(关键进程恢复)修复之后,A 的优先级会恢复到原来的值,但由于 nopreempt 的存在,A 并不会立刻抢占。这时就相当于 A 作为备机,却有更高的优先级。如果 B 之后出了问题(关键进程挂掉,优先级降低),却不会导致切换,因为 B 在出问题之前,其优先级就已经比 A 低了。

网上有文章说,遇到这种情况,可以在 B 的进程监控脚本中把 B 的 keepalived 进程杀掉,来强制触发切换动作,这种作法显然并不优雅。有没有更好的方法呢?

注意到,当拔网线触发切换时,被拔网线的机器上,keepalived 并不是降低了自己的优先级,而是进入 FAULT 状态(除 MASTER 和 BACKUP 之外的另一种状态),这时是不会有上述由优先级引起的问题的。所以新的思路就是,当进程监控失败时,不要降低优先级,而是令 keepalived 进入 FAULT 状态,同时在主备两机都加上 nopreempt 选项。

至于怎么令 keepalived 进入 FAULT 状态,也很简单,直接去掉 vrrp_script 中的 weight 选项即可。虽然谁也没说 weight 是必选项,但因为网上能搜到的示例都是配了 weight 的,导致很多人(至少包括我)会主观认为这是必选项,从而无法意识到还有其他方法。当然这也跟 keepalived 的文档不太完善有关。

PS. 让我发现 weight 不是必选项的来源是……找不着了,囧。

's avatar

LoadRunner 中进行 HTTP 测试时使 Keep-Alive 生效的注意事项

工作需要,在使用 LoadRunner 进行 HTTP 测试时,为了使每个虚拟用户在不同的循环周期中都能保持长连接,则除了要打开 Keep-Alive 运行时配置(默认打开)之外,还有两个选项需要修改,这两个选项都在运行时配置的「Browser - Browser Simulation」中:

  • Simulate browser cache :取消勾选。禁用对浏览器缓存机制的模拟,令虚拟用户确实的每次都真正发起与 Web 服务器的对话,而不是只读取一下缓存。

  • Simulate a new user on each iteration :取消勾选。不要在每次执行 Action 之前重置虚拟用户的状态,不然会把原来的 TCP 长连接也重置掉,Keep-Alive 就没用了(更准确地说,是在不同循环周期之间就没用了)。

's avatar

VS2010 环境中调试 IE ActiveX 控制时断点不起作用的问题

又是惯例的长了好长时间的草,今天先来篇短的。

工作需要,在使用 VS2010 开发调试 IE ActiveX 插件时,默认条件下下的断点不起作用,这是因为选错了调试器,在工程属性 - Debugging 中的 Debugger to launch 项,选择 Web Browser Debugger ,然后在 HTTP URL 项填本地 HTML 文件的绝对路径,以 file:/// 打头就可以了。

Li Fanxi's avatar

Richard Stallman杭州行

我是来给Blog除草的,写个流水账。

5月初,终于磕磕碰碰地把Richard Stallman的传记《Free as in Freedom》翻译完了给出版社交稿了,想找他老人家写个序。他的回答还真是干脆:“没问题,我有空一定写。不过你们别等我的序啊,指不定什么时候能写好。还有,你们能不能把这书在两周内给印出300本来啊,我马上要来中国做6场演讲,我要用!”

虽然我清楚的知道在两周内把一本刚翻译完连错别字都没改完的书出版出来是不可能的,不过还是马上与徐继哲联系了一下,看看有没有别的变通的方法。徐继哲和哲思社区是RMS这次中国的行程的策划者。很意外的得知,RMS这次不但要来杭州和南京,还要来阿里巴巴。于是,我联系了公司负责接待RMS来访的同事,争取到了协助组织这次活动的机会。

所谓协助组织,主要也就是协助审阅活动宣传文案、挑选RMS演讲的提问环节的问题。不过,也别小看这点事情,其实很多人并不了解RMS的自由软件运动,大部分人更是把自由软件运动和“开源”混为一谈。事实上,我很早以前就说过,我其实不喜欢RMS带有宗教气息的自由软件运动。不过既然是邀请人家来做演讲,那还是投其所好吧。我努力的把所有文案和问题都重新组织了一遍,尽可能避免“开源”等会让RMS情绪激动的词语的出现。

19日一早接到RMS一行,按活动流程,先带他参观园区并给他简要介绍一下阿里巴巴集团。直接就碰了第一鼻子灰:“介绍要花多久?我不想听,我的时间很宝贵。”不过这倒也给我减轻了不少压力,于是这个参观介绍环节就简化到了不到十分钟时间。

合个影,RMS表示说,如果在合影中出现公司的名字或Logo,就不能把照片公开发布。我开玩笑说,我们可以把公司的Logo“Photoshop”掉再发布。这话显然激怒了他,“你在说什么?Photoshop?你应该说,GIMP,GIMP,GIMP!”

途径阿里云的办公区域,我介绍说这是“Alibaba Cloud Computing ”。RMS立马批评说“Cloud Computing”是一个很含糊其辞的词语,我们不能这么说,blah,blah。事后我回忆了一下,19日一天内,他给不同人总共讲了四遍为什么“Cloud Computing”是一个不好的词。

途经连接两幢楼的连接平台,RMS和他的女朋友被平台上的植物和鲜花所吸引,停下来拍照。我又嘴贱了,我说,过会儿吃完午饭离下午的演讲还有一点时间,可以在园区散散步,还有更多美丽的植物和花。“我有很多工作要做,有很多Email要回复,我没有时间散步!”RMS说。

早上的小范围圆桌会议后,公司活动组织者开始请RMS在一些T恤和图书上签名,作为下午的演讲活动的礼物。这个事他们先前没有跟我商量,所以我也没想到要提醒他们注意事项,结果就是我意料之中的:RMS拒绝在非自由的图书上签名,也拒绝在印有公司Logo的T恤上签名。

还好我自己倒是有备而来,事先印刷了几本《Free as in Freedom》中文版草稿。因为这本书使用GFDL许可证,RMS欣然在上面签名,不过他反复强调,请出版原书第二版的中文版,别出第一版,因为第一版中有很多错误。可惜这个事不是我和他可以说了算的,所以就只好先搪塞过去了。RMS的女朋友对这本书很感兴趣,同时她似乎对GNU网站的翻译工作也很关心和了解,希望我们能把GNU网站的翻译工作做得更好,翻译更多RMS的作品。不管怎么说,签名版《Free as in Freedom》到手了。难道我会告诉大家这就是我对这次活动这么积极的原因吗?

RMS的签名和他的Pleasure Card

午餐,RMS真是个吃货。嗯,吃饭时他还把他不离身的龙芯笔记本电脑垫在盘子下面,弄得全是油。

下午的演讲,参考我另外整理的演讲实录,没啥新意。不过拍卖环节气氛还挺热烈,RMS真会卖萌啊,于是一个Baby GNU公仔拍出了550元的价格。提问环节完全没有按流程来,现场提问时不时蹦出“开源”一词,惹得RMS很是生气。他大声说“I am for 自由软件!”,全场大笑。

与RMS同行的,还有一位日本朋友,Akira Urushibata。2008年我在上海复旦大学召开的哲思自由软件峰会上听过他有关汉字哲学的演讲,记忆犹新。所以,当大家的焦点都在RMS身上,完全无视Akira的存在时,我找到不少机会与他进行了交流。Akira虽然不太会说中文,但他对汉字以及中国文化还是有很深的认识。Akira下午没有进行演讲,而晚上我赶到浙江大学玉泉校区活动现场时,也已经错过了他的演讲,颇为遗憾。浙大的活动现场也很火爆,不过我已经没有兴趣再听一遍RMS的演讲了,所以RMS演讲时,我就在会场外面跟继续跟Akira聊天,他跟我讲了不少中国历史,很多我都不知道,非常汗颜。Akira还送了我一本他的书,签完名,他问我要不要在上面再给我写点什么。我一时语塞,他想了想说,“我给你写个‘庖丁解牛’”。

接待RMS一行花了我大半天时间,整理演讲内容摘要也花了不少时间,后果就是忙上加忙了。经验告诉我,我总是越忙越写Blog,所以本流水账也诞生了。嗯,这事儿应该就到此为止了。

Li Fanxi's avatar

Richard Stallman演讲实录

这里收录的是Richard Stallman 2014年5月19日在杭州阿里巴巴西溪园区演讲的内容概要。应活动组织方的原本的期望,是需要完整听译的,但那样实在是一个比较费劲的工作,所以最后决定摘录了这些内容概要。同时我提供了一些参考资料的链接,对于演讲中的大部分主题,GNU网站上都可以找到详细阐述相关内容的文章。

录音下载地址(ogg格式):http://pan.baidu.com/s/1bnaRpi3 密码:mnep

开场白

如果你拍摄了照片,请不要发布到类似于Facebook的社交媒体上。因为这些网站会监控你的行为。

如果你录制了演讲的视频,请以ogg或WebM等自由的格式来发布,不要使用任何MP打头的格式,不要以Flash的形式发布,不要以Windows Media或QuickTime的格式发布,也不要发到Youtube上。

请确保其它人可以使用自由软件而不用额外的私有软件就可以下载这些视频。

参考阅读:

主题演讲

1. 什么是自由软件?

自由软件尊重用户和社区的自由。Free Software中的Free的含义是“自由”,而不是“免费”。

自由软件关乎使用者运行、复制、发布、研究、修改和改进该软件的自由。 更精确地说,自由软件赋予软件使用者四种自由:

  • 不论目的为何,有运行该软件的自由(自由之零)。
  • 有研究该软件如何运行,以及按需改写该软件的自由(自由之一)。取得该软件源代码为达成此目的之前提。
  • 有重新发布拷贝的自由,这样你可以借此来敦亲睦邻(自由之二)。
  • 有改进该软件,以及向公众发布改进的自由,这样整个社群都可受惠(自由之三)。取得该软件源码为达成此目的之前提。

所以,自由软件总是一个整体,不会有“部分自由”的软件。你对自由软件所做的修改,也必须是自由的。自由软件不是一个技术问题,而是关乎用户的自由,是一个社交与政治的问题。

参考阅读:

2. 为什么私有软件是不好的

私有软件在道德和社交上都存在问题,人们应该避免使用。开发一个自由软件是对社区的贡献,贡献的大小取决于这个软件有多有用。但开发私有软件不是对社区的贡献,私有软件限制了人们的自由,是有害的。

自由软件的运动的目标就是让大家可以自由的分享软件。如果使用了私有软件,你会面临两难的境地。比如,如果有朋友向你要一份私有软件的副本,你就得违反软件使用协议,或者拒绝朋友的请求。这时你应该选择错得太不严重的那个方式:违反软件使用协议。因为开发私有软件本身就是罪恶,必面要犯错时,就先得罪那些犯错在先的人。

解决这种两难境地的最好办法是避免使用私有软件,你手头没有私有软件,你的朋友也就不会要向你要副本。

参考阅读:

3. 为什么我们需要自由软件

自由软件可以帮助用户完全掌控自己的电脑,包括电脑本身和上面运行的软件。作为软件开发者,你需要保证你的软件的用户充分享有上面提到的四个维度的自由。

私有软件开发商常常会在软件中植入一个不好的东西,比如DRM,后门或监控用户行为的功能。所以,私有软件就是恶意软件。比如:Windows是一个恶意软件,因为它内置了DRM、后门和监控用户的程序。尤其是移动设备上的Windows 8,它只允许用户安装指定的应用程序。微软还可以通过Windows在你的电脑上自动安装系统更新程序,就这是一个后门,只有恶意软件才会这么做。

同样的,苹果的所有“i”系列产品也都是恶意的。开发者试图把用户都关进“监狱”,控制他们的一切,所以才有“越狱”行为的出现。

Flash Player是恶意软件,因为它有监控用户行为和DRM的功能。

Android是恶意软件,因为它会把用户位置发送给某些公司。

Kindle是恶意软件,因为它剥夺了人们阅读图书的自由。它会把阅读进度发送到Amazon,它还限制了你与朋友分享图书的自由。Amazon还留了个后门,可以远程的删除你设备上的图书。

在乔治·奥威尔的《1984》中描述了人们在无时不刻的监视下生活的故事,非常值得一读。

请引导你的朋友从一开始就不要犯错,尽可能远离这些充满恶意的软件系统。

手机也是一种危险的产品,很多运营商可以远程改写手机上所安装的软件,甚至把它变成一个行动跟踪和窃听设备。有些手机甚至关机也是假的,只要电池还在,它就不断会发出信号,更有些手机甚至不允许拆卸、更换电池。运营商可以随时知道你所在的位置。我不用手机,虽然这有点不太方便,但在方便和自由之间,我更珍视自由。

使用私有软件就可能使用了恶意软件,要保证安全,就应该选择自由软件,因为你可以自己去研究它的实现、改进它,在这个过程中即便真的发现了有恶意的功能,也可以很方便的把这些功能去掉。事实上自由软件作者不会在自由软件中植入恶意的功能,因为自由软件让这样植入无处藏身。

Ubuntu虽然包含了很多自由软件,但它也存在一些间谍软件的特性。请参考文章:https://www.gnu.org/philosophy/ubuntu-spyware.html

参考阅读:

4. 自由软件运动

1983自由软件运动发起的时候,那时几乎所有软件都是私有的,所以我决定重新创造一个自由的操作系统,重写所有的软件。作为软件的作者,我可以保证这些软件都是自由的。要保证整个系统都是自由软件,这个工作量很大,所以需要集合各种现有的力量来完成这个任务。保证与Unix的兼容,这样可以用这个自由的系统来替换Unix。

GNU是一个递归名词,是GNU’s Not Unix!的缩写。创造这个词的时候有两个想法:1.它是一个递归词,并表达了GNU不是Unix这个意思。2.GNU本身就是一个单词,是角马这种动物的名词。不过在说GNU工程时,发音要发为g’noo,而不是new。因为这个工程已经有20多年历史了,它不再“new”了(幽默)。

参考阅读:

5. GNU/Linux

Linux这个词常常被错用,正确的用法是在讨论这个操作系统时把它叫作“GNU/Linux”,因为Linux只是操作系统的内核,加上各种GNU的程序后,才构成了完整的操作系统。这种错用从1992就开始了,那时GNU还在开发自己的Hurd内核,但是Hurd的野心太大,一时半会儿完成不了。1992年Linus把Linux以GPL发布后,Linux就成了自由软件,所以GNU选择Linux作为系统的内核。因此,在讨论整个系统时,需要同时提到GNU和Linux,它们缺一不可。

GNU必须的是自由的,Linux则是Linus创造的,Linus本人并不反对私有软件,但他不应该误导人们只看到Linux而忽视GNU工程。应该让人们清楚的知道,GNU是一个自由的操作系统,GNU/Linux并不是Linus一个人的成果。

GNU的未来取决于我们的价值观,所以我们需要向所有人宣传我们的价值观。我们必须强调自由的和重要性,不然我们就无法告诉人们为什么我们要为GNU工程而努力。

参考阅读:

6. 开源

“开源”是一个会误导人的词语,它是1998年时自由软件社区中一个不乐意宣传自由软件运动精神的人创造的(指的是Eric Raymond),它们不乐意传播自由的理念,把我们的作品与我们的价值观分离。开源不讲自由的理念,自由软件关乎人们的道德,开源只管利益。

我不是“开源”的支持者。我们必须珍视我们崇尚自由的理念,向人们宣传我们的理念。

参考阅读:

7. 我们应该怎么做

大部分GNU/Linux发行版都包含了一些非自由的软件,它们引导用户去使用非自由的软件,这是不对的。我们应该使用真正自由的GNU/Linux发行版。比如:gNewSense。虽然使用完全自由的发行版可能会带来一些使用上的不方便,但是我们在方便和自由之间应该选择自由。自由和公正比方便更重要,这是我们应有的价值观和选择。

作为一个自由的操作系统,就应该保证系统的每个组件都是自由的。很多时候仅仅“开源”是不够的。Linux内核其实也不是完全自由的,因为它里面带有一些二进制的BLOB,虽然它们也是以“源代码”的形式出现,但它们本质上并不是自由的,Linus在“自由”和“方便”之间错误的选择了“方便”。所以GNU也有一个自由的Linux分支,名为Linux Libre去掉了Linux中那些不自由的东西。

Mozilla是开源的,但也不是完全自由的。因为它支持一些非自由的组件,包括DRM。大家应该联合起来谴责这种行为。

很多网页中也有非自由的JavaScript代码,JavaScript总是“开源”的,但是开发者仍然应该在代码中包含软件许可证,明确表明它们是自由的。我们有一个名为LibreJS的浏览器插件,可以自动阻止非自由的JavaScript代码运行(其中可能包含很多恶意的JavaScript代码),同时这个插件还可以帮你向网站开发者发出抱怨邮件,促使他们把JavaScript换成是自由的代码。所以,当你开发JavaScript代码时,请带上一个自由软件许可证。不要使用非自由的JavaScript库,如果必须要用,你可以考虑自己重写一个自由的版本。

避免使用SAAS服务,因为你没办法控制你的程序在什么样的系统上运行。

虽然Linux使用了GPLv2许可证,但这只是因为Linus本人想对代码有更多的控制权。但是我们应该使用“either version 3 of the License, or (at your option) any later version”,这样当有新版本GPL许可证发布时,你的代码就可以和自动允许其它人使用更新的许可证。你应该对自由软件基金会有充分的信任,如果你不信任自由软件基金会会维护人们的自由的权利,那也没有别的什么机构更值得信任了。

我们支持反向工程,因为这样可以创造出更多的自由软件。

如果要了解更多相关的信息,访问以下网站:www.gnu.orgwww.fsf.org

如果想帮助GNU工程,请访问:www.gnu.org/help。可以先从以下小事做起:用GNU/Linux来称呼使用Linux内核的发行版;避免使用“开源”一词,使用“自由软件”。加入自由软件基金会,成为一名准会员。

参考阅读:

问答

Q: 在中国,阿里巴巴在开源和自由软件事业上做了很多工作。但其它一些公司并不重视这个。如何来更好的提升自由软件的影响力。

A: 可以关注一下GNU AGPL许可证。这个许可证保证了Copyleft许可证在Web上运行时的适用条款。通过它可以推动更多的厂商开放他们的代码,向自由软件做出贡献。

参考阅读:http://www.gnu.org/licenses/why-affero-gpl.html

Q: 有些软件用“抽象层”的方式来隔离自己与“开源”软件,怎么看这个问题?

A: 我不是“开源”的支持者。抽象层的做法并不奏效,只要你的程序与自由软件是一个整体,GPL就能对你的程序产生约束,整个 程序都得是自由的。除非你的程序跟另一个自由软件只是在系统中同时运行,相互之间没有联系,这才可以对你的程序使用不同的许可证。但这种情况下,也不需要“抽象层”这个东西了。

Q: 开源软件一样可以让我们拥有“自由之一”,有啥不好的。

A: 开源会带来误解,开源会限制你的自由。开源只是让你可以看到源代码,在具体的使用条款上可能会有各种限制。

Q: “云计算”是恶意软件吗?

A: 请不要使用“云计算”这个词,因为它很不准确,得具体情况具体分析。如果是说“虚拟机租用”,只要服务商不限制你在虚拟机运行自由软件,那就是没有问题的。如果是说“SAAS”,那是一件不好的事情,前面已经提过。如果是说“云存储”,那就是不好的,因为你不应该把自己的东西存到别人那里,他们可能会滥用你的私人文件。除此以外,云计算这个词可能还有更多的应用场景,请不要使用这么含糊的词语。

参考阅读:http://www.gnu.org/philosophy/who-does-that-server-really-serve.html

Q: 硬件很多不是自由的,怎么办?

A: 目前没什么办法。我们只能尽量支持按自由理念来设计的硬件,但毕竟硬件需要工厂去生产,我们对此没有足够的控制力。

Q: 如果微软和苹果也开源的话……

A: 我不再回答有关开源的问题,我们不应该宣传“开源”这个词汇。我是为“自由软件”而生。你可以重新组织一下你的问题吗?

Q: 如果微软和苹果能把他们的软件变为自由软件的话……

A: 那我们当然可以使用。

Q: 为什么自由软件与非自由软件不能和谐相处,人们各取所需呢?有些私有软件也做得非常出色。

A: 曾经,奴隶与农场主也是“和谐”相处的。我不用私有软件,我会躲开他们,如果我用私有软件,他们会夺去我的自由。自由软件做得再差,也比没有好。私有软件做得再好,也比没有不好。所以我选择没有。

自由软件运动的目标就是让软件都变成自由的,GNU不希望你使用任何私有软件。现在我们已经取得了很多的成就,要实现完全自由计算的愿景,我们需要更多的自由软件。

也许私有软件能做不少有用的工作,但是开发私有软件依然是不对的。

参考阅读:http://www.gnu.org/philosophy/when_free_software_isnt_practically_better.html

Q: 现在跟上世纪80年代相比,对于自由软件有何不同?

A: 1980年代时,我们没有自由的操作系统。但那时的开发者还很诚实,他们开发用户想要的软件,不会开发恶意的系统。

现在,非自由的操作系统中,常常包含很多恶意的东西,还促使用户去信任系统,失去对系统的控制。

总体来说,现在的环境比80年代更差。不过,80年代时你没办法找到一个自由操作系统来在你的电脑上运行,现在,我们完全可以在自己的电脑上运行自由操作系统,从这点来说,环境是好了很多。

's avatar

OpenSSL 与 WinSock2 配合使用时遇到的一个坑

今天才发现,这里已经两年多没有被照看过了,估计连树都要长出来了吧。


在用 Windows 的 WSAEventSelect 模式进行网络编程时,比较固定的一个模式是这样的:

...
WSAEventSelect(sock, events[0], FD_READ);
...
while (1)
{
    ret = WSAWaitForMultipleEvents(1, events, FALSE, timeout, FALSE);
    if (ret >= WSA_WAIT_EVENT_0 && ret < WSA_WAIT_EVENT_0 + 1)
    {
        eventIndex = ret - WSA_WAIT_EVENT_0;
        if (eventIndex == 0)
        {
            WSAEnumNetworkEvents(sock, events[0], &networkEvents);
            if (networkEvents.lNetworkEvents & FD_READ)
            {
                ...
                recv(sock, buf, sizeof(buf));
                ...
            }
            ...
        }
        ...
    }
    ...
}
...

当要处理的 sock 属于一个 OpenSSL 连接时,只要把当中的 recv 换成 SSL_read 就行了,当然前面还得加上些 readWaitonWrite 之类的标志位检查啥的,这里就不详细列举了,请参考《Network Security with OpenSSL》一书中的 5.2.2.3 小节。

但我在实际使用中(事实上是在压力测试中)发现,当程序运行一段时间之后,WSAWaitForMultipleEvents 就会忽然不再返回可读信号了,从而导致对该 socket 的接收操作完全停止。

经过各种调试手段,甚至是在 OpenSSL 中加入调试输出之后,终于发现问题出在 SSL_read 的实现机制上,貌似 OpenSSL 实现了某种程度的读写缓冲(具体没细看),使得对 SSL_read 的一次调用,并不一定会触发其对底层 socket 的读取操作。而如果没有对底层 socket 的读取操作,那么 windows 的对应 event 对象就不会被 reenable (参考 MSDN 中对 WSAEventSelect 接口的说明文档),从而导致 WSAWaitForMultipleEvents 不再对该事件作检查。

原因找到了,那么相应的解决方法也就不难发现了,对于我的使用场景来说(不完全是像上面的示例片断那么清晰),最简单的作法就是在 SSL_read 之前加一个空的 recv 调用,其传给 recv 的第二、三个参数的值分别是 NULL 和 0 ,这样就能强制触发 windows event 的 reenable ,同时又不会影响到 SSL 对象内部的读取状态了。

PS. 遇到这个问题时,最重要的一步其实是如何能够稳定而快速的复现问题。一开始做压力测试时,只有在连续跑个一天以上时才会不定时的出现,导致效率很低;后来偶然发现,通过虚拟机搭建的受限环境,反而能很快复现问题。想来这也是另一种形式的压力测试吧,正常的压力测试是保持环境不变,加大请求压力;这里则变成了保持请求不变,同时压缩可用环境。

sunjw's avatar

Notepad++ plugin local exploit

前段时间,在用 npp 分析一个 html 文件时,准备把其中的混合在一起的 js 分离出来,在 js 前后换行时 npp 崩溃了,感觉是哪里 bug 了,重试了几次都能很稳定的重现。本着张银奎老师的精神,上调试器。用 windbg 附加到 npp 进程,重现这个 crash 后,停到调试器内,看了下栈。

很明显栈溢出了,返回地址都是文本中的字符,溢出发生在 NotepadSharp.dll 也就是 Notepad# 插件中。Npp 上的插件基本都是开源的,那就把代码搞下来,用 VS 编译了个 debug 版,放进去再次触发崩溃,发现 PluginDefinition.cpp 的 void Newline() 函数中

char line[9999];

::SendMessage(curScintilla, SCI_GETLINE, line_number – 1 , (LPARAM)line);

将当前光标上一行的文本内容拷贝到 char line[9999] 这个 buffer 中,所以很自然,当文本行内容很长,就栈溢出了。

既然溢出在栈上,通过覆盖返回地址和栈指针,在 XP 上应该可以轻松控制程序执行流。不过这个插件的 release 版是在 VS2010 上编译的,打开了 /GS 参数,所以在函数中有 security cookie 检查,而且 security cookie 是随机值。

试图覆盖返回地址时,也会将 security cookie 覆盖,所以导致函数返回之前就出错。

在 Yuki 的指点下,加长文本行的内容,向后继续覆盖,试图覆盖到 exception handler。再次触发后,eip的确变成了覆盖的内容,不过并非覆盖了 exception handler ,而是覆盖了某个窗口消息循环中的回调函数指针。

这是原始的 0x000da8dc 开始的内容,后面会看到 0x000da8dc 存储的是 edi,0x000da8f4 存储的则是回调函数指针,原始的内容是 User32.dll 的 CallWindowProcW 函数指针。经测试,这里应该是 Explorer 插件的回调函数,其实没有Explorer 插件也可以在附近的位置覆盖到其它窗口回调。当按回车后,::SendMessage 不仅拷贝了字符串,还触发了多个插件注册的回调函数。所以在 NotepadSharp 的 Newline 函数还未返回前,其他插件,例如 Explorer 之前注册的窗口回调就被调用了。于是通过很长的溢出,覆盖了其他的栈空间,避开了 security cookie 检查,从而能控制 eip。通过覆盖,在 html 文件对应位置填写 0x7E47B533,也就是 Windows XP SP3 en 上的 jmp edi 指令。

并且在 0x000da8dc 填上 EB1C,短跳转 1C 个字节。之所以要跳转是因为 edi 在之前覆盖的函数指针前方,如果需要执行更多的代码,空间似乎有点不太够,通过跳转来到指针位置的后方。在跳转目标地址填上 int 3 也就是 CC。触发漏洞后,成功的停在 windbg里面。后面就可以用 shellcode 弹个计算器什么的了,不过查看寄存器,发现 ebp 指向的地址不可写,就把 esp 的值赋给 ebp,当作新的栈。最终,贴上一段 metasploit 的 shellcode,成功的在 npp 中按一下回车,导致 npp 崩溃,并弹出了计算器。

这个问题已经报告给 npp 的插件管理者,也报告给了 Notepad# 的作者,不过似乎该插件作者已经很久不更新该插件,也没有留下有效的联系地址,似乎一直对该问题没有响应。如果已经安装了 Notepad# 插件,最好将其卸载,虽然被攻击的可能性不大,但是崩溃什么的也不好。

sunjw's avatar

JSToolNpp 1.16 Released

What’s New in JSToolNpp 1.16:

  • Performance improvement.
  • Move download back to SourceForge.net.

Download links:
SourceForget.net Download
Google Code Download

sunjw's avatar

Build CyanogenMod 10.2 for Nexus 4

一直想尝试着自己编译 CM,毕竟如果想修改系统 Apk,就有可能需要编译整个系统。于是在开了一台 Ubuntu x86_64 虚拟机(这是一个大坑,等会儿说)作为编译机器,参考的主要是 CM 官方 wiki:http://wiki.cyanogenmod.org/w/Build_for_mako?setlang=en。根据 wiki 指示准备好基础环境后,开始 repo sync,总共下载了 6GB 多的工程代码。总的来说,一直到 wiki 的 “Start the build” 一节,都没有问题,照着做就好了。不过当执行 brunch mako 之后,坑就一个个冒出来了。

第一个坑是磁盘空间,一开始给虚拟机分配的磁盘小了些,明显不足以完成整个编译,于是又加了个 40GB 的虚拟磁盘,并且在该磁盘上编译。40GB 似乎够了,不过最后的事实证明其实 40GB 几乎不够,最终完成编译后只剩 400MB 了!所以编译 Android 还是分配一个大一点的磁盘吧。

第二个坑就是 x86_64,ARM 处理器目前还是 32 位的,包括编译使用的 repo 好像也是 32 位的,所以首先需要安装 32 位 c/c++ 运行库。之后真正的坑在于 JDK,wiki 中没有明确说明 JDK 应该安装 32 位还是 64 位的,我一开始想 java 部分应该和架构没什么关系,就装了个 64 位 JDK,结果开始使用 javadoc 编译 Droiddoc 时,make 出错了,显示错误 45,一开始在论坛上查了一下,说是应该使用 Oracle JDK,于是尝试安装了 64 位的 Oracle JDK,结果还是一样出错。后来仔细看了论坛,发现其实问题是 64 位 JDK。也不知道为什么,64 位 JDK 的 javadoc 就是会导致编译错误,但是之前的 jar 包都编译正常。通过 apt-get 安装了 32 位 Open JDK 后,问题解决了。

第三个坑是内存,一开始只给虚拟机分配了 1GB 内存,结果在编译 framework 时,malloc.c 中的 assert 出错了。。。关机,调整到 2GB,编译终于继续下去了。

第四个坑是 CM 加入的 Focal,在编译 external/Focal 中的链接库时总是出错,论坛上说 Focal 不太稳定,可以不编译。好吧,暴力一点,直接把 external/Focal 和 apps/Focal 删掉了,编译继续。

最终花了一个下午+半个晚上(不包括 repo sync 的时间,这个根据网速会有所不同,可以自己算算下载 6GB 需要多久),终于编译出来了。好了,能编译了,下一步就是准备尝试修改了。

 

sunjw's avatar

fHash 1.8 Released

增加 Shell Extension 实现的高级右键菜单支持
修正其他 bug

Use Shell Extension to implement context menu shortcut
Fix other bugs

Download:
https://code.google.com/p/fhash/downloads/list
http://sourceforge.net/projects/fhash/files/1.8/

sunjw's avatar

用 WinDbg 调试符号不全的程序

其实也就是记录一下用 WinDbg 调试 fHash 的一次过程,这个 bug 是由于没好好看文档造成的,我需要让 fHash 知道当前运行的操作系统是 32 位还是 64 位的,之后好去选择正确的 shell extension 文件。查文档看到了这个函数 IsWow64Process ,之后错误的以为 kernel32 中有这个函数就是 64 位操作系统,结果就已这个逻辑写下了错误的 64 位系统判断函数,和错误文件读取逻辑。


tstring tstrExeDirPath(pszExeFullPath);
tstring::size_type idx = tstrExeDirPath.rfind(_T("\\"));
tstrExeDirPath = tstrExeDirPath.substr(0, idx);

tstring tstrShlExtDll = tstrExeDirPath;
tstrShlExtDll.append(_T("\\fHashShlExt"));
if(IsWindows64())
    tstrShlExtDll.append(_T("64"));
tstrShlExtDll.append(_T(".dll"));

WIN32_FIND_DATA ffData;
HANDLE hFind = FindFirstFile(tstrShlExtDll.c_str(), &ffData);

bool bRet = (hFind != INVALID_HANDLE_VALUE);

if(bRet)
{
#if defined(UNICODE) || defined(_UNICODE)
    wcscpy_s(pszShlDllPath, MAX_PATH, tstrShlExtDll.c_str());
#else
    strcpy_s(pszShlDllPath, MAX_PATH, tstrShlExtDll.c_str());
#endif

   FindClose(hFind);
}

return bRet;

结果很自然,在 32 位的 Windows XP 上没有执行正确。但是测试环境没有 VisualStudio,而且用的是 Release 编译,也没有带着符号文件,这怎么调试呢。Windows XP 上装有 WinDbg,那么就用它来调试。

启动 fHash 和 WinDbg,附加到 fHash 进程,由于没有 fHash 的符号,于是考虑在 FindFirstFile 函数上下断点:

bp kernel32!FindFirstFileW

Go,开始运行,点击“添加右键菜单”,WinDbg 停在 FindFirstFile 断点上,回到 Call Stack 的上一层:

.frame 01

看到 call 之前第二次压栈 push eax,检查 eax 对应内存地址的数据:


r eax
db 003b7ed0

看到指针指向的字符串是 …/fHashShlExt64.dll,说明之前 IsWindows64() 返回的 true。也就是说 kernel32 中有 IsWow64Process 函数,我们来看一下。

在 GetProcAddress 函数上下断点:

bp kernel32!GetProcAddress

再次尝试添加右键菜单,WinDbg 停在 GetProcAddress 断点,我们让它 Step Out,之后停在 IsWindows64() 函数里面,看看返回值 eax:


r eax
u 7c81f2b9

可以看到反编译该内存地址就是 IsWow64Process。

sunjw's avatar

JSToolNpp 1.15 Released

What’s New in JSToolNpp 1.15:

  • Change name to JSTool.
  • Added simple search in Json Viewer.
  • Fixed Json Viewer and editor linkin bug.
  • Fixed Json Viewer utf-8 bug.
  • Other tweaks.

Download links:

http://jstoolnpp.googlecode.com/files/JSMinNPP.1.15.uni.zip

http://sourceforge.net/projects/jsminnpp/files/latest/download

sunjw's avatar

Apple WWDC 2013 Keynote 全场视频下载

刚刚 Apple 放出了下载地址,各位猛击即可:

http://itstreaming.apple.com/podcasts/apple_keynotes/wwdc2013_ipod.m4v

Wang Congming's avatar

2012:变革

没有哪一年,像 2012 这样出乎我的预料。这一年,我从被认为拥有完美纯粹初恋的好男人,成了一个没人要的老男人;这一年,我刚刚驾驭了中层的工作,又紧接着面对进入高管层的挑战;这一年,承受了很多、学会了很多、中庸了很多。

– – –

事情来得那般突然,以致于我要面临从零开始生活的挑战。这真的是一个挑战,最搞笑的例子是,买个床垫回来发现竟然小那么多,我以为天下的床都一样大呢!再举个例子,我那时在商场对着 “x件套” 久久思索:怎么这些被子都这么薄呢?呵呵。。。慢慢地,下班逛一趟超市,提一堆东西回来成了一个习惯。那两个多月逛的超市,比之前 20 多年还要多吧。我终于意识到了大部分人早就意识到的结论:楼底下有个大超市,生活是多么的方便啊!

寂寞,可以让一个男人腐朽,也可以磨炼他的情怀。现在这里的一切,就只有一个你了,活得有没有品质,再没有借口了!好在,咱自我进化的能力尚足以慰藉。生活逐渐朝着自己想要的方向前进,有什么不可以呢。

2012,一向对尺寸不敏感的我,开始时常去测量,因为买东西必须有所参考;一向对金钱不敏感的我,偶尔也会对一眼售货单,并感慨通货膨胀的厉害。2012,我研究起了如何更有效率地清洁玻璃、如何更好地吸尘;我研究起了空气净化系统、地热供暖系统。2012,我甚至花了些时间研究休闲娱乐,如果既有时间又有钱,可以去哪玩呢?我开始去寻找花鸟鱼虫市场,养起了小动物(起初,它们只是装饰的工具,我认为在这样一个屋子里需要一点动态。买回来才意识到,这是一条条鲜活的生命,它们也有自己的习性、也会饿、会生病、会⋯⋯必须对它们的生命负责)⋯⋯2012,我在生活,学到了很多;却也偶尔无奈于一个人吃饭、看不到温馨的笑容。

– – –

2012,团队有了极速的扩张。我们同时涉及云计算、移动互联网和物联网;同时涉及 Web、App、C/S 和三维仿真;同时涉及平台研发、通用产品、专业产品和各异的项目业务。对于不愿丧失细节掌控力的人,这意味着太多。意味着必须对团队建设、业务需求、专业理论、产品设计、技术架构都有足够的投入;意味着必须对战略规划与落地有热切的期盼、对商务压力有坚强的意志、对团队成长与波动有沉稳的心理;意味着在期盼、压力和问题面前保持激情,并向团队传递信心与决心;意味着必须时刻准备进入全新的领域,并保持勤奋。

职业是一个舞台。在这个舞台上,演绎了自己的青涩,也演绎着一段极速成长的青春。

– – –

回首,我看到了悲哀,在最重要的事情上如此失败(好在,仍然让大部分人记住了自己爽朗的笑声、让领导可以向自己不必顾忌地泄压、让团队相信我们能成)。

如果说有什么值得欣慰,一些困境和压力可以 HOLD 住、一些事情做起来似乎有些天赋、一些毛病可以快速地自我进化掉,这些也许算吧。至少,这样说可以让自己感到高兴 :)。

也有很多不太满意的地方。这一年,仍然做了很多无聊的事情,浪费了很多时间;一些好的做法,仍然没有形成稳固的习惯、仍然在波动;有些时候,仍然可以明显扑捉到自己很天真、很幼稚(虽然希望能够装不懂,保留某些幼稚);对父母、亲朋的关照仍然还很不够。。。

这一年,我这个所谓的 geek,好像对科技失去了敏感。真正的 geek,应该是像 Elon Musk 这样的家伙,IT 之外,对数学、物理、生化⋯理论和应用都有着前卫的理解和尝试。在这方面几无收获,甚至要看记者写的新闻才知道发生了什么事。反倒是,生活上有了些长进。这是一种可怕的中庸么?⋯⋯只能勉强找一句话安慰自己,“花一些时间深入理解生活,有助于激发更有价值的创新”(倒真的在做家务的时候,想到了一些很好的工作上的创意~)。

– – –

单身男人的好处,是他有很多寂寞的时间。
直面寂寞,成就了情怀;
静观内心,凝聚了真我;
来去无虑,释放了豪情。

展望,希望 2013 是升华的一年。

除此之外,以下几条要做到!

  • 驾照。时间不是借口!
  • 多读书。2012 读了一些书,工作有关的、无关的,但还远远不够,要继续!

    2012 感觉还是太“文”了一点,2013 要恢复一下“野性”~!

  • 健身。2012 开了个好头,2013 要常态化!
  • 滑雪。这个这个,这才是符合我口味的运动~!虽然现在还很菜鸟 :(   要多练!
  • 潜水执照。要拿到 OW 执照,AOW 就更好了~。向海洋深处进发,哇哈哈,向往~! 12

– – –

莫为浮尘遮望眼,风物长宜放眼量!
有太多激动人心的事了,Can’t keep waiting!

Wang Congming's avatar

一名程序员的中期答卷 — 序

今天,我过了一个有工作也不干的周六。悠闲的时光,除了轻松惬意,也给人思绪。我忽然觉得,应该停下来,写点什么。

回首来南京工作的经历,我手中的素材开始慢慢变厚,然后又由厚变薄。现在,又要开始变厚了⋯。所以,是时候对前面的东西做点记录吧,当着年轻的记忆 ;) 。否则凭我的毛病,过了这个阶段就会觉得不值一提,然后慢慢忘记了。。

 

如果用一个词形容这两三年的工作,我觉得是“丰厚”。从一个人,到几个人、到几十个人;从锋芒毕露,到内敛、到担当、到承受。而北漂的那几年,感受的是神奇、纯粹的快乐。大概,“浪漫”完了,该负责任了 ^_^ 。

 

作为所谓的序篇(哈哈~),还是回忆回忆自己是怎么成为程序员的吧。这个,说起来就比较悲催了。牛人们的经历都是这样的:

我有个 Geek 老爸,在我很小的时候、世界上第一代 PC 诞生的时候,他在车库里自己捣鼓出了一台。然后,他用汇编写了好几个游戏给我玩。从此,我爱上了他那台电脑,以及编程。。。

我第一次接触计算机已经到了高中一年级(还没有网络),对着屏幕不知道该干什么,然后看着个别已经打字打得噼里啪啦的同学表现出一副无辜的样子。等到我可以经常使用计算机,已是快 2000 年的时候了(还是在网吧)。当时,其他同学都在忙着聊 QQ、玩游戏,我却在好奇怎么可以让网页上的文字一闪一闪的。然后,我用记事本看 HTML 的样子吓到了网吧老板,他警告我不准黑他们网吧,我倒是想啊~。

 

刚上大学那会,已经可以假模假样搞网站了。比如当时常见的,资源搜集与下载站等等。不知从某个时机开始,才尝试步入“正途”,思考说所谓的“计算机科学与技术”是学啥的(因为我不是计算机专业的)。后来找啊找,找到了三个靠谱的来源,分别是清华大学、兰州大学和 MIT,它们对于这个专业该做什么、怎么做,开放的资料给了我很大的帮助。记忆最深刻的,是当时我老旷课,拿着本《数据结构》的书躲在学校无人的楼顶上,想啊想,到底什么是 TMD 的“抽象”。。。现在回想起来,因为自己的性格,我对这个学科从完全的无知到稍微有一些理解,都是自己躲在角落里通过网络和书搞懂的。如果那时候能有个人点拨,应该会快很多吧。

 

再然后,我爱上了一个叫着“人工智能”的东西,因为前面的缘故,对传说中的 MIT 充满着向往。毫无道理地,我一直认为自己是个天才,我要去伟大的 MIT、做出伟大的科研成果、造出伟大的智能机器、然后取得伟大的商业成功⋯⋯。其实从中学的时候开始,我就有伟大的天才、伟大的科研成果、伟大的商业成功的梦想。然而这一刻,梦想突然变得这么清晰,那就是 MIT、人工智能!仿佛触手可及。

 

最后,经过挣扎,我发现我去不了 MIT。人生第一次感到绝望。即使高考考得不好,我仍然相信自己是个天才,这还没完。然而这一次,我要彻底放弃所谓的人工智能、MIT 的执念了,我得去找工作了;我得承认,自己只是茫茫人海中的普通一员了。那一夜,我为自己,在绝望中痛哭。仅有的一次。

 

从此,我成了一名快乐的程序员 :)。

Wang Congming's avatar

如果可以穿越,我想对年少的自己说:

勿有比较之心

这个环境,早早地向你灌输“人中龙凤”、“人上之人”的观念,仿佛人生的唯一目的就是要攀援一个等级,而成功的唯一途径便是将他人踏在脚下。权势与财富是唯一的衡量。

 

有一天,你会问自己,为什么来到这世上,所求为何?人生百年,似有一段光景,然比日月星辰、沧海桑田,实如蝼蚁,白驹过隙。人生所向,不过死得其所,求得内心安宁。

 

然纵渺小,你定也不甘心来这世界白走一趟,你定然想见识见识这种种之美。

想见识星宙斗转的自然之力,在苍穹的一角,仅这一颗小小星球上的千奇壮美已令你痴然。你不会“人定胜天”,而会充溢朝圣的心情,想用脚丈量大地、用心聆听安宁。

想见识千万年来人类文明的足迹,这个孤独的、智慧的、高傲的,狂躁的、愚蠢的、野蛮的种族,它走过了怎样的一条道路。因为,那是你来的地方,也将是你去的方向。历史透过尘埃在你面前呈现,而你也将化作尘埃组成历史。

 

终于,你在自然与历史的洪流中洗去了纤尘,变得温和谦恭。你爱悯地看着这个世界,在美与绝望之间,不再想只当一个旁观者。跟随你的心,去做点什么,你就没有白过。也许,你所选之道,已不同于环境的熏陶,你知道,那已不再重要。在广袤的时空间,人类有多样的价值,却没有唯一的“成功”。多样与不确定正是生命美与尊严之所。

 

你将不会后悔,因为你不仅曾经来过,而且清醒地活着;你体验了美,而且努力创造了美。

 

 

优秀是一种习惯,珍惜时光、重在实作

你在时间的无涯的罅隙中寻找到了一个渺小的人类存在的价值,你在生命独特的尊严的鼓舞下树立了前行的方向。你定然想做到更好,那么,请将自己从过往的缅怀、未来的畅想和当下的忧虑中释放开来,重要的,是迈出你的步伐。把想法变为行动,把行动做到优雅,让优雅成为习惯。

 

 

任何煎熬,待你挺过去之后,都将成为一种荣耀

不断的挑战,使你的生命变得丰满。不要倒下,因为待你挺过去之后,任何煎熬都将成为一种荣耀。这锤炼的是你的胸怀,使你装得下更大的未来。

Wang Congming's avatar

RSS feed 迁址

曾几何时,FeedBurner 有一统天下之势,咱也没能免落俗套,热热闹闹地烧了一把火。。

 

承蒙有些朋友订阅了我这小破文章,给大家知会一声,您要不嫌我啰嗦,烦请订阅 http://wangcongming.info/feed/ ,老的 FeedBurner 地址可能会在不日废除。

 

谢谢! ^_^

 

Wang Congming's avatar

我是怎么管理密码的

应推友要求,介绍下我现在是怎么管理密码的 :) 。

这个方法不是我首创,我也是受别人启发,但可以简单论述下为什么这个方法好~。

 

这个问题的背景,是这个国家的互联网行业,开发者素养差、管理者道德低的普遍现状(当然,国外也有很多不负责任/不懂如何负责任的网站)。我们对于所有网络服务,普遍处于不信任的状态(特别是国内还多了官僚监管这一层),但是有的时候又不得不用。那么,作为用户,我们如何保护自己的信息安全(这里只谈密码)?

 

理想状况:

  1. 每个网站都使用不同的密码;
  2. 而且都很复杂。

2 保证了如果某个网站技术不太行,保护措施做得不够好,你的密码也不会轻易被破解;而 1 保证了即使在极端情况下,也只会被泄露这一个网站的密码,不影响你在其它网站上的账户。

 

然而,要用人脑来记忆不同网站的不同的复杂的密码,显然是不可行的。所以我们要借助一些辅助措施。有一类软件提供这样的功能,需要的时候,它可以帮你生成一个随机的很复杂的密码,同时把它存起来。你可以注明这是什么网站的密码,然后用一个主密码把这些随机密码都加密起来。所以,软件负责记住不同网站不同的复杂密码,而我们人脑只需要记住一个主密码。

 

这是个好主意,但是还不够好。问题在于那些不同网站的密码都是随机生成的,这也就注定了我们的焦虑(我们失去了把控,只能完全依赖于一个黑盒)。我们总会担心记录弄丢了、损坏了、出门在外忘带了(同步?没网络呢、别人的机器呢?),或者某种平台这个软件不支持等等。总之,我们被 lock-in 了!

 

我现在的方法是,不使用随机密码,而是用主密码+网站名,然后算 sha1。即:

$ sha1sum

password+csdn

^D

对,出来一串“乱码”,就用这串乱码做密码。这样做的好处,只要密码后面的网站名不同,生成的“乱码”就完全不同,即使有几个网站同时不靠谱、同时把你的密码(即刚刚生成的“乱码”)泄露了,你的主密码是绝对安全的(当然,要搞复杂一点)。另一方面,你知道这些“乱码”是怎么生成的,所以你不再依赖于某一个单一的软件,因为 sha1 算法是整个计算机系统的重要基础设施之一,哪里都有它的踪影、久经考验。

 

哦,你会说,这是程序员的玩意儿,非程序员怎么办?非也,算 sha1 的小软件,可是比密码管理软件分布广泛多了。Windows/Linux/Mac OS、手机、平板⋯哪个平台上没有?不但有,而且很简单、很好用,绿色环保!就算没有,照着公开的算法写一个也不是什么难事。

所以,我们与其小心翼翼地备一个密码管理系统,不如大大方方地随便哪放一个算 sha1 的小软件。而且你可以下一个在手机上,真是走到哪用到哪啊。不是有很多人说,非硬件级的方案不用么?手机,这是最好的硬件方案了。:)

还有很重要的,如果因为种种原因,你要用一个公用/别人的电脑,就会对配置 1Password + Dropbox 感觉不爽,毕竟有你重要/隐私的数据在里面,同步下来总不太好。算 sha1 的小软件就无所谓了,随便下一个、随便算一下,然后叉掉就行了(要找一个熟悉的,确保不会保存历史记录)。

 

当然,算法只是一方面,为了方便起见,我们不希望每次都算一遍。所以,可以配合浏览器原生的记住密码、自动填写、自动同步的功能一起使用,我觉得堪称完美了(特别是 Firefox 还可以设置主密码保护已记住的密码)。

我已经使用这个方法一小段时间了,个人感觉良好。唯一的不爽是,有些 SB 网站居然对密码长度有限制,要求不超过 16/20/25/… 位。遇到这种情况,只能一边骂 SB 的同时,一边截短。。。

 

关于 1Password:

1Password 做得太优雅了(只用过 Mac 版),以致于我很乐意帮它做个广告(虽然我没再用它了⋯)。1Password 几个很好的功能:

  • 自动生成随机复杂密码:跟上面主要的思路相冲突,这功能等于没用;
  • 自动记忆、自动填写:浏览器原生支持,相比之下没有优势,等于没用;
  • 密码整理/管理非常方便:这是它比浏览器原生的功能强很多的地方。但是密码之所以需要整理,是因为每一个都很独特、都很重要,我们需要搞得它井井有条。但是当我们用了上面 sha1 的算法之后,会突然觉得没必要整理了。我们让浏览器记住,只是为了它能帮我们自动填写,方便一点而已。实际上,那些密码存在数据库里到底是什么鬼样子,我们一点都不担心,反正需要的时候再算一遍好咯。所以,不是 1Password 做得不够好,而是这功能对我已不再重要。

呵呵,好像说得它一点用处没有的样子,其实我们也可以用 1Password 记上面 sha1 产生的密码,它的好处比如:

  • 虽然密码整理已不再重要,但是它毕竟满好用的,说不定我们什么时候想整理整理呢?
  • 1Password 是跨浏览器的。如果用浏览器原生的记忆密码的功能,并且 Firefox、Chrome 同时用的话,同一个网站的 sha1 可能需要算两遍呵呵。而 1Password 是跨浏览器的,算一次就可以。
  • 除了网站的密码,1Password 还可以用来记信用卡都资料。

另外,如果不太喜欢上面说的所谓的算 sha1 那么 geek 的做法,也是用 1Password 的一个很好的理由,它是非常用户友好的。与 1Password 类似的,可以用开源且恰好免费(好吧,我是想给你一个印象:开源不一定免费)的 KeePass。

(完)

Wang Congming's avatar

用户:如何判断某网站程序员是不是傻逼(关于密码)

Update:这篇日志用的戏谑的语气,严谨地说来,当然不一定是程序员的责任,比如历史遗留、领导的要求、业务需要、进度限制等等⋯。即使是程序员自己的问题,如果只是经验不足,而非主观故意,也应该是可以理解的,毕竟谁没有年轻的时候呢。日志中用了“傻逼”这个词,有些人看得比较严肃,颇有些不满,在此致歉了 ^_^ 。

——

虽然最近很忙,但我发现负荷高了大脑会罢工,正经事想不进去⋯。于是乎,写篇休闲科普文章吧。:)

 

那啥,CSDN 600 万用户密码刚泄完,天涯网又泄露了 4000 万用户的密码出来。看起来像是有组织、有预谋的,越来越热闹了~。前面一篇日志,我从开发/设计者的角度,记录了几条实践要求,怎样保护用户密码才靠谱

 

那么,对于普通用户来说,如何判断一个网站的技术靠不靠谱?有没有什么现象,是不需要懂任何技术,只要你发现了,就可以对这个网站露出鄙夷的神情,骂它傻逼的?试着总结几条(只谈跟密码有关的):

  • 如果一个网站,能够通过邮箱、手机、安全问题等,找回密码,那它就是傻逼。这里注意“找回密码”和“重置密码”的区别,“找回密码”是把你原来的密码原样告诉你;“重置密码”不告诉你原来的密码,但是经过可靠的验证后允许输入一个新密码。前者是傻逼,后者不是。
  • 如果一个网站,要求密码不能超过多少个字符,或者不能包含特殊字符,那它就是傻逼。密码字符个数,有下限要求的,是好人;有上限要求的,是傻逼。
  • 如果一个网站,密码不区分大小写,是傻逼。
  • 如果一个网站,改密码的时候不先验证老密码,是傻逼。
  • 如果一个网站,登陆时不使用 HTTPS,是傻逼。这个不一定,有可能程序员不傻逼,老板是傻逼。。。
  • 如果一个网站,改密码后不要求重新登陆,很可能是傻逼。这个非绝对,只对允许一个账号同时多处登陆的网站有效(即使这样也非绝对傻逼)。总之,对允许一个账号同时多处登陆的网站来说,改密码后要求重新登陆的,是一般人;不要求重新登陆的,有可能是 SB,也有可能是 NB(但是 SB 的比例很高,NB 的比例很低,所以可以默认是傻逼⋯)。

 

好吧,一时半会就想到这么几条。欢迎补充 :) 。

 

Wang Congming's avatar

怎样保护用户密码才靠谱

CSDN 密码泄露,把我的也给泄掉了⋯⋯好几年没用了,躺着也中枪啊。幸好只是一个低级别的密码。忙着改密码的同时,也在考虑要不要启用 sha1(master password + domain name) 的密码方案。。。

 

好吧,还是说点有趣的,怎样保护用户密码才靠谱?

1、数据库存储

原始:

save2db(username, password)

进化:

save2db(username, md5(password))

再进化:

random salt = GUID

save2db(username, salt, md5(password.salt))

靠谱方案:

const globalSalt = [a-z]+[A-Z]+[0-9]+[%$#@!~*…]+

random privateSalt = GUID

save2db(username, privateSalt, sha1(globalSalt.password.privateSalt))

可接受的简化方案:

save2db(username, sha1(domain.password.username)

 

为什么要搞个 globalSalt?

globalSalt 写在代码中,也就是说,万一你的数据库泄露了(代码还没泄露),仍然有一道安全防线在保护你的用户。只有一个 salt 的情况下,虽然使得字典攻击变得更难,但如果用户密码不够长、不够复杂,暴力攻击仍是很轻松的事(因为既然你的数据库已经泄露了,用户的 salt 自然也就露掉了)。而有了 globalSalt,只要代码仍然安全(攻击者不知道你 globalSalt 的值为多少的情况下),它就能保证跟用户的密码组合起来变得非常长、非常复杂,超出了暴力破解的实际可能。

 

可不可以只用一个 globalSalt?

不可以。否则,好事者就可能在知道你 globalSalt 的情况为你这个网站专门生成一个字典,一次性搞定所有用户。privateSalt 的存在,使各个用户的 salt 都不一样,因此需要为每个用户生成一个专用的字典,不具备实际可行性。简单地说,globalSalt 防暴力攻击、privateSalt 防字典攻击。

 

简化方案中,可不可以不要 domain,只用 username?

不可以。一、有的用户,即使是用户名 + 密码仍然不够长,抗不了暴力;二、我们不仅是要(尽可能)保证 salt 的全站唯一,而是要全球唯一。有些用户名如 admin, root, webmaster 等非常通用,起不到 salt 应有的作用。

 

2、修改密码

    • 改密码,必先验证老密码,而不是登录着就让改。否则,就给 Session Hijacking 变成进一步的灾难修了条大道。
    • 如果老密码输错了,启用验证码保护。否则,就是 Session Hijacking 后暴力破解的后门。
    • 清 session,使所有登录的(如果支持一个帐号同时多处登录)cookie 失效。改了密码,就应该要求该用户所有会话重新验证。因为用户密码已经改了,表示原密码不再是有效的,甚至可能已经泄露了。

     

    3、其它

      • 传输密码时用 HTTPS;
      • 最好全程 HTTPS,杜绝 Session Hijacking(WIFI 什么的,最容易中招了);
      • 避免 Session Fixation(永远重新生成,不通过 URL 传输 SID);
      • 除特别需要,httpd 进程对代码目录只应该有读权限,而不应有写权限。你懂的,后门啊什么的最讨厌了~;
      • 定期 $ git status(.git 目录为 root 专属),如果不是 working directory clean,那就该报警了喂!
      • Injection、XSS 什么的,呃⋯超纲了,打住~。

       

      虽然都是很简单的事,但如果一条条验证,估计国内绝大部分网站都能找出问题,包括那些人们觉得很牛逼的(CSDN,程序员的社区啊,“全球最大”啊…存储密码用的是最原始的方法)。关于保护用户信息安全,你还有什么想法?

      Wang Congming's avatar

      The so called “social software development”

      昨儿晚上,我居然因为这个想法失眠了!

      ⋯⋯此刻,当一天的繁碌谢幕,静坐窗前,我想,我必须得把它写出来。

       

      话从头说起,昨天我发了一条推

      想问下有没有这样的网站:比如我想要一个手机应用,市场上没有、又不能自己开发,我就上去登记一下,写明自己的需求与设想,并投一点钱。其他人看到后,也可以补充完善,并追加小笔金额。待感兴趣的人与累积的金额够多,就会有开发者接招,把它做出来。版权仍归开发者,但初始用户有终身使用权。有么?

      之所以在推上问,是因为我觉得这个想法很普通,这样的服务应该已经存在,只是我不知道而已。可令我意外的是,答案居然是——没有。

       

      好吧,在开始之前,我想先总结一下我所了解的相关服务

       

      Kickstarter

      如果你有特别的创意,但是缺乏一些启动资金,可以把你的想法及现有的条件登记在此。社区用户如果认同你和你的创意,就会给你捐助。如果筹到了足够的启动资金,你就可以拿着这笔资金正式开始你的计划。当然,你需要承诺,项目成功之后给予当初的捐助者一些报答

      还有一些特点,比如:

      • 你的项目必须具备技术、艺术等方面的创意,而不是纯粹的慈善项目。红十字会类似的组织被明文禁止在此募捐;个人如果是 “我没有饭吃了” 或者 “我要买房子,需要贷一笔款”等,也是被禁止的。
      • 确保用户是因为热爱你设想的创造性的事物本身,而非作为经济上的投资,才给你捐助的。因此,承诺回报捐助人股权的行为是被禁止的。鼓励的回报形式包括优先获得产品的测试版等与项目相关的特别的纪念品。
      • 既然你不能在此煽情乞讨、也不能以所谓的高回报盲目用户,冷静的用户社区很适合作需求调研的窗口,测试你的计划是否契合大众的口味。因为如果到了截止日期,筹集的款项达不到你启动资金的要求,它们将被全部退回。捐助人的钱是安全的,而你也不会有拿了钱不干活的道德困境。

      也许很多人看完后的第一反应是谁会投钱、遇到骗子怎么办⋯⋯。别说,还真有很多人在投。Kickstarter 已经达到了百万用户的规模,并成功为各类创意活动募集了近1亿美元的启动资金。除了服务本身要提供一定的保障机制,整个社会大的信用体系、高质量的创意人群和宽容友善的创新环境,都让我们不免有些羡慕。

      C2C 作品:

      点名时间:比较忠于 Kickstarter 的思路(虽然强调得不够),可惜的是上面项目的质量不高,一眼望去感觉都是明信片 =_=! 。另外,所谓创意活动,应该是项目还没有启动,需要一笔启动资金;捐助人需要评估并承担项目失败的风险,而在项目成功后取得一些特别的回报。在点名时间,感觉有的项目把它当淘宝用了,明明东西已经做出来了(或者不存在任何困难),只不过多一个销售渠道。

      积木网:比较贪大,想要多囊括一些应用,最终搞得有形而无神。或者说,它是另外一套思路,不评论。

      总的来说,Kickstarter 是一个好东西,但是跟我想要的不是一回事。在 Kickstarter,你有创意,在此募集资金,并亲自实现自己的想法;我想要的服务,用户有一个需求,但无法自己实现,因此他在此悬赏,很多用户一起悬赏,最终有人接受挑战。其出发点,一个是要实现自己的创意、一个是要满足自己的需求。

      开个玩笑呵呵,Kickstarter 像是明星在台上表演,下面一群观众在欢呼;我想要的,是一群民众遇到了困难议论纷纷,牛仔望着他们渴盼的神情,提枪策马而去,迎接挑战。

       

      威客类网站

      先说句废话呵呵,一般说国外服务的时候,都可以指名道姓;而说国内的服务,都需要说“xx类”,威客类、SNS 类、微博类⋯⋯。我们总是在一窝蜂,什么时候可以不这么功利、可以有自个独特的品味呢?敢不敢弄点赚钱以外的追求、弄点发财以外的梦想?⋯⋯

      话归正题,所谓“威客”这个看似舶来品实际上是国人发明的词汇,意思是你有什么特长(如设计、开发、文案),然后在网上提供服务,获取报酬。用猪八戒网的口号来说,你在淘宝上买商品、在威客上买服务。比如你们公司要做一个官方网站,就可以上去发布一个需求,可以模拟现实世界中的商业招标的流程。

      你可能会说,这不是很好么?Yeah, that makes sense, but it’s not awesome! 用自己的技能赚钱,自然是木有什么可说的,但是这太商业了,太商业了,太商业了!各种功利钻营,一点都不 Cool!(顺道说一下,猪八戒网专门搞了个发需求、返现金的功能,这是现实世界中拿回扣的数字版么?真够中国特色的呵呵。还大张旗鼓作为一个独特功能来宣传,中国人到底还要不要脸?还有没有底线?)

      那么,如何才算 Cool?首先,整个社区应该是友善的,用户欣赏开发者、开发者从用户获得灵感与支持;其次,开发者必须保留对作品的所有权,雕琢以求完美,而不是被商业买断,为了混饭敷衍了事。当然,还有其它措施,容后详述。我想要的,是一股不排斥金钱、可实际运作,但不会铜臭泛滥、没有尔虞我诈,友善、良性前进的力量。

      补充:CSDN 的项目交易频道、国外的 freelancer 都跟上面说的类似。TaskRabbit 稍微有趣一点,与其它主要要求在电脑前工作的网站不同,TaskRabbit 上发布的主要是需要跑腿的体力活。

       

      Quirky

      如果你有一个产品构想,但是无法自己实现,可以考虑把它写到 Quirky 上。它背后有一个专业的团队,会评估你所构想的产品的市场可行性;如果他们觉得可行,就会把产品生产出来,拿到零售市场上去卖;而你作为产品的“发明者”,可以得到一定的利润。

      这个东东有点意思,但它不是我想要的。一方面,它专注于现实的生活用品的设计制造,而非软件开发;另一方面,我想要的是一个社会协作的状态,而非以一家公司为核心甚至全部。

      C2C 作品:

      还没发现呵呵,这玩意儿重点是它的设计制造能力和零售渠道,编程技术反而没那么重要。Copy 有难度啊⋯。

       

      好吧,废话够多了,该回到我们正题了:

      How to be awesome?

      我的设想:

      • 开发者必须保留对作品的完全所有权,而非商业买断。只有这样,才可能斟酌以求完美,而非敷衍混饭吃。
      • 作品必须开源。只有开源,作品才能经过同行评审,迫使每一个开发者自觉提高对自己的要求;只有开源,作品才能成为我们公开的名片,才能激发我们的个人荣誉感,而不只是为生计奔波。只有开源,个人的努力才能汇聚成社会的财富,而非沉寂于遗忘的角落。
      • 用户悬赏的金钱必须收下。是的,这是骄傲,收钱不伤感情。
      • 作品完成后,鼓励拿到各种 market 推广,以使(悬赏人之外的)更多用户受益;鼓励收费,付出需要认可。
      • 鼓励每个用户悬赏小笔金额,而不是相反。多用户共同悬赏,而非单用户买断。
      • 用户可以悬赏开发一个全新的产品,也可以悬赏完善现有的开源项目(于是,这就是与前面的开源要求相呼应了,不开源,作者撒手后用户就再也得不到支持)。

      “Social” 体现在哪些方面?

      • 需求搜集:多用户共同协作、启发、完善。
      • 悬赏资金:许多用户各投一点小钱,而非一人独自承担。
      • 项目推介:不只是开发者自己推广,用户也会努力一起推广。因为更多人参与悬赏,每个人需要投入的金额就会更低、吸引开发者接招的可能性就会更大。
      • 开发资源:因为开源,开发者可以站在前人的肩膀上,在庞大的开源宝库的基础之上进行重用与创作。
      • 开发过程:因为开源,整个开发过程是透明的,社区可以适时了解产品的开发状态,甚至参与开发。Release early, release often? We release at the first step, release at every step!
      • 后期维护:因为开源,即使作者消失了,他的作品永远流传我们的世界,并可被后人继续完善。

      对用户的价值?

      • 一个人只需悬赏很少的金额,就可以找到有品味的程序员帮你实现需求。基本无疑会超越外包公司的质量,可说是物美价廉。
      • 有共同爱好的用户和你一起脑力激荡,很可能比你一个人提所谓的需求,更全面、有创意。
      • 开发过程全透明,你得到的不再是一个黑盒子。
      • 作品完成后,作者很可能会无条件长期完善;即使原作者不再完善,你也很容易能找到其他人。
      • 最后,这整个过程都很 Cool(是的,这很重要!)。

      对开发者的价值?

      • 你可以在这上面得到很多创意和启发。很多时候,我们不是没有时间,而是不知道可以做点什么有实际价值的东西(特别是对学生、freelancer 来说)。
      • 你可以了解有多少人关注这个项目,这相当于帮你把前期的需求调研做好了;而且,有一笔启动资金送到你的手里。还有比这更棒的么?
      • 在这里活动的用户,很可能是很有经验的程序员、设计师、产品经理,他们可能只是没有时间亲自实现自己的所有需求。因此,你面对的不再是无知却又把自己当爷的客户,而是一群很有素养和经验的人,他们尊重你的努力,甚至会夸赞你代码的优雅;他们不只是提需求,甚至还提供创意、设计、技术和管理的经验。这将是你和用户共同创造的值得骄傲的作品,而非相互埋怨和忽悠后的垃圾。
      • 你拥有作品的完全所有权。
      • 因为开源,这不再是一做就扔的项目,而会高高挂在你的 Profile 中成为骄傲的记录。甚至在你退役后,江湖仍会流传你的传说。:-)
      • 这整个过程都很 Cool(是的,这很重要!)。

      对社会的价值?

      • 用户享用了很棒的产品;
      • 开发者获得了金钱、经验和荣誉;
      • 产品开发尽可能继承了前人的成果、产品完成后尽可能惠及了更多的用户,不重复造轮子节约了社会资源;
      • 结果开源,促成人类公共财富量的增长;
      • 鼓励完善已有开源项目,促成开源库整体质的提升。

      Wow! 这将是一个颠覆吗?定位?

      No,这不是什么颠覆,它更多是对其它模式的补充,而非替代。实际上,这可能只会是一个小众服务,公益性质的、在软件开发及相关群体内部受推崇,但普通用户不太熟悉的服务。实现这个服务,应该能养活自己,甚至还能赚点小钱,但肯定不会让你发财。

      但这个想法是如此之酷,以致于我很想要把它做出来。程序员写了那么多软件,难道就不能为自己写一个么?⋯⋯好吧,其实我打这么多字,是想有人实现我这个愿望,因为我最近确实是没有时间精力⋯⋯。

      目标用户?

      所有用户都可参与需求与悬赏,但应该还是以软件开发相关的群体为主,就像上面说过的:程序员、设计师、产品经理、Geek、软件爱好者等。

      至于如何推广到其他用户群体,这是后话,不能太乐观呵呵。

      目标开发者?

      学生、自由职业者、部分有余力的在职程序员。

      需要注明的是,虽然程序员能在这里赚到钱,甚至有可能赚大钱,但是最好不要把这当成主要的养家糊口的来源。而应该是一种乐趣与荣誉。要强调这种感觉。

      另外,这也是有经验的程序员培养新生程序员的训练场,所以我们会要求掌握 Github 这样的工具作为 Best Practice、培养基本的职业素养,而非像皮包公司那样纯粹赶工。程序员的集体行为主义 Ah?

      市场?推广?

      Social, social, social…。国内的、国外的,可以连接的都连接上吧,用户和开发者都有很强的动机将项目推广到他们的圈子里去。不被吸引的程序员不是好程序员,不被吸引的 Geek 不是好 Geek。^_^

      下一步怎么办,怎么推广到普通用户中去?下一步再说,能把第一步做好,已经很 Cool 了~!

      商业模式?

      用户投的悬赏金,在交给开发者之前就是很好的现金流啊。另外,你会想要贪污其中的 10% 么~。

      还有什么模式?广告啊、社区啊、招聘啊⋯⋯。

      总之,你应该能赚点酒钱(哦,忘了说,还有荣誉),但别抱着发财的心态来。

       

      最后:

      我承认我受原教旨黑客精神影响很深,有些理想主义。但我还没决定要做,主要是业余时间精力实在有限。写出来,是希望有认同这个想法,正好又有机会的童鞋把它做出来。。。

      另外,@nan8_8 童鞋贡献想法说,可以用同样的思路,悬赏人把这服务本身做出来。什么,A compiler that compiles itself ? 太 Cool 了!难道我现在应该正正经经写个需求,然后投入第一笔悬赏金,宣布启动么?冷静,冷静,先看看反馈再说⋯⋯ ^_^ 。

       

      Credit to: @lidake007 @libuchao @Leaskh @BreeStealth @soulhacker @klx1204 @zxq3206 @nowBOK @clc4214 @hanfeiyu @likuku @MissevanNews @sankmaru79 @nan8_8 @kenny_yuan @xmdingsky @ursatong and more…

       

      —-

      P.S.: 推荐大家看一个公开课。在这个个人越来越独立的时代和潮流下,传统的庞大的公司组织显得臃肿而乏味,我们需要思考,如何创造一种模式适应工作越来越个性化的需要。这个课程,让我不自觉地关注了以上那些服务。

      's avatar

      修改 OpenVPN 实现加密算法的自动协商

      另一篇博中的分析可知,OpenVPN 中有两个加解密通道。一条是标准的 SSL 协议通道,被 OpenVPN 用于协商自己所用的密钥。这个通道的加密算法当然也是通过 SSL 协议来进行协商的,可以通过 --tls-cipher 选项来进行配置。另一条是 OpenVPN 自己的加解密通道,用于交换实际的数据,也就是虚拟网卡抓到的 IP 报文。这个通道的加密算法则是通过 --cipher--auth 两个选项,分别在通调两端指定的。

      对于第二条通道的加密算法,必须要同时在两端分别指定一致的选项,有时候不是很方便(当然,我研究的还是 2.1.1 版本的 OpenVPN ,不知道最新的版本还是不是这样)。比如说,我想通过在服务端修改配置,指定加密算法,然后让连接我的客户端自动用同一个算法。最简单的修改思路,就是借用第一条通道中的算法协商机制,从 SSL 对象中取得协商出来的算法。

      具体做法就是:

      首先,在函数 do_init_crypto_tls_c1() 中,去掉对函数 init_key_type() 的调用。这个调用就是根据 --cipher--auth 选项进行算法配置的地方,我们要动态协商,自然是不需要这个了。

      但同时,这会引起接下来一个步骤的错误。在函数 crypto_adjust_frame_parameters() 中,会根据之前配置的算法进行报文中密钥空间的分配。现在还不知道算法,怎么知道要分配多少空间呢?就只能改成最大值了。分别改为 MAX_CIPHER_KEY_LENGTHMAX_HMAC_KEY_LENGTH 就行了。当然,这样改不仅浪费空间,而且也不够严谨,因为 key length 和 IV length 不是一回事,却只能都用 MAX_CIPHER_KEY_LENGTH 来初始化。

      最后就是在 SSL 协商好之后,从里面取加密算法了。具体位置在 key_method_2_write()key_method_2_read() 两个函数中,对 generate_key_expansion() 函数的调用之处了。在调用之前,初始化一下 key_type 就行了:

      struct key_type *key_type = (struct key_type *) session->opt->key_type;
      key_type->cipher = ks->ssl->enc_read_ctx->cipher;
      key_type->cipher_length = EVP_CIPHER_key_length (session->opt->key_type->cipher);
      #if OPENSSL_VERSION_NUMBER >= 0x010000000L
      key_type->digest = ks->ssl->read_hash->digest;
      #else
      key_type->digest = ks->ssl->read_hash;
      #endif
      key_type->hmac_length = EVP_MD_size (session->opt->key_type->digest);
      

      这样就行了。

      's avatar

      编写自己的 Web 版 Google Reader 客户端

      最近花在路上的时间明显变多了,手机就成了我打发这些路上时间的利器。小说神马的看太多也会腻,正好 Google Reader 中也积累了大量的未读文章,因此用手机看看订阅文章就成了一个比较好的选择。

      但多次使用下来,却也发现了很多 Google 原版 Web 界面的不方便之处。Google 提供了两个版本的 Reader 界面给手机,一个是苹果风格的 /i/ 界面,设计得很漂亮,功能也很全,用 Nokia 手机也能看,但太费流量(未证实,只是感觉,大概是因为一次加载了很多的文章吧),而且在网络状况不好的时候,体验不太好。另一个是 /m/ 界面,很简洁,选项不多,但也总有些这样那样的细节另我不太满意,下面详述。然后又去找 Nokia 的 S60v3 客户端软件,总之是没一个喜欢啦。作为一个 Geek (至少是自称),在这种情况下,最佳选择自然就是打造一个自己专属的解决方案啦。

      正好蹭着同学的一个 PHP 空间(其实也有 Python 和 Ruby 可选啦,但总觉得会很折腾,还是 PHP 好点,虽然折腾也少不了,见下),于是花了几天时间,连复习 PHP (以前只学过没用过,主要还是看 w3schools 的教程和查 php.net 的函数文档)、研究 OAuth (参见这篇文章,不过 Web 应用稍有不同)、研究 Google Reader API (参见这系列文章)、编写代码,终于搞定了一个很阳春的 Google Reader Mobile 版,刚刚满足我的手机阅读需要。

      以下就是我针对 Google 原版所做的,针对我个人的改进:

      • 保存期限的问题:以前也有感觉,这次趁着做这个东东,又确认了一下:Google 只会保存 30 天内的未读文章,超过 30 天的就自动标记成已读了。我希望能保存下来慢慢看,所以就搞了一个简单的 SQLite 数据库,定时把曾经是未读文章的 id 号都给保存下来,直到我通过这个界面读过之后再删除。嗯,这个功能是最重要的。
      • 先看最老的文章:当然,通过这两天对 API 的研究,我已经知道怎么在 Google 的 /m/ 界面中实现这个功能了,不过为了上一个功能,打造自己的界面还是必要的。
      • 重复载入的问题:当我要给一篇文章加星的时候,我一般都希望能直接看到下一篇文章,而不是再看一次被加星的这篇。这也是一个很小的改进啦,不过用起来很舒服,既节省了流量,也节省了时间。
      • 加载文章的时候先不要标记为已读:由于手机的网络状况通常不太好,当我浏览完一篇,想加个星标的时候,结果页面却打不开了,这种情况经常发生。所以,我就把标记为已读的这个步骤,挪到了看一下篇之前,或者加星标之后。这样,就比再去已读文章里面找要来得方便。
      • 去掉 /m/ 的所有其他功能:当然,这不是一个改进啦,只是我不需要而已。既然是用手机来看,当然就没有那么多需要啦,只要能实现浏览与加星标就好了。需要细看的,就加个标,等有电脑的时候再慢慢看。本来, Google Reader 的星标功能,除了作为 ToRead 列表外,就没什么其他用处了(当然,以后也许会跟搜索整合,然后优先显示神马的,那就不管了)。不过话说回来,功能的简化其实也是一种改进啦,最近刚好看到 InstaPaper 的作者也这么说来着。
      • 功×夫×网:不过其实 Google Reader 并没有被封的很严,倒是我自己的实现招来了这个问题,见下。

      简单来说,就是按顺序,对所有文章,看,(可选地)加星标,然后下一篇。需求往往就这么简单啊。

      当然,在做的时候,还是遇到过各种稀奇古怪的问题( Neta :编程好难啊),这里也一并记录下。

      先是在自己机器上 OK 了,等部署到空间的时候却报错: an error occured while processing this directive 。同样的脚本,放到其他目录就没事,还以为是 Apache 的权限问题,也没找到有什么特别的配置。直到有机会查看 Apache 的错误日志,才发现原来就是因为新加的这个目录有组的写权限,而 Apache 不允许这样,真是吐血。话说加上组的写权限,也是为了 SQLite 来着,因为 SQLite 会在数据库所在目录中写事务日志文件。

      然后就是发现,我用的 SQLite3 的 PHP 接口,在空间的 PHP 5.2 版本中还没有呢。真是悲剧。然后又吭哧吭哧地用 PDO 接口重写了数据库的代码。

      等到好不容易改好啦,用电脑访问也一切正常,结果用手机访问的时候,看不了两个页面,就会出现“连接被远程服务器关闭”的错误。一开始还没有往功×夫×网的方向想,只以为又是服务器的权限问题(我为什么要加“又”?),来回地看日志,切换测试,不停地折腾。直到在电脑上偶然看到,该网页需要翻×墙的标志,再一看 URL 才恍然,原来是我传的一个 GET 参数,是该文章的订阅源 URL ,而有些文章是从 Feed×Burner 订阅的,这才触发了功×夫×网。真是躺着也中枪啊,赶急加一个 Base64 编码搞定。

      不管怎样,一个简陋的阅读器就这样被实现出来了。鉴于我近期还不想烧钱换手机,这个东东应该还是很有用武之地的吧。不知道 Google 出的 Android 客户端有没有 30 天的限制。

      最后的最后,该版本仅限我个人使用,因为我没有实现多用户的功能,所以大家(如果有的话)也就不要去尝试访问了。我之后会把代码给开源的,到时随你(如果有的话,嗯)折腾。


      Update: 代码已上传

      Update 2012-12-31: 现在有了 Android 手机,已经开始用 gReader 这个客户端了,除了“延迟标记已读”这个功能还没有之外,其他所需功能已经都有了,而且还是离线阅读哦(我自己的版本就没有实现离线阅读这个功能,因为太麻烦)。不过我的数据库里面已经存了 7000 多篇文章没看了……囧

      's avatar

      [转载] Compilers: what are you thinking about?

      最近在翻看以前加星的 Google Reader 文章,把有用的整理出来打标签,然后就看到了这篇。原文作者的博客现在不知道被丢到哪边去了,搜也搜不到,转到这里,权当保存一下吧。

      原文链接:Compilers: what are you thinking about?。当然,已经打不开了。


      Compilers: what are you thinking about?

      Author: Rotten Cotton

      My recent post Compiler bibliography, a motley list of compiler papers that had been sitting in a box in my attic, generated a surprising amount of traffic: over a thousand unique visitors. But no comments. For those of you interested in compiler design, I ask, what are you trying to understand? What are you trying to do?

      I imagine some of you are interested in understanding how compilers work and, more importantly, how to build them. The first step in learning to design compilers is to build one. (Tautologous, no?) To start, I recommend following the syllabus of a compiler design course. Googling “compiler syllabus” returns a massive list of syllabi for compiler for compiler classes. The first link, a compiler design class, at Portand State University, follows Louden’s book Compiler Construction to build a SPARC compiler for a toy programming language, PCAT (pdf). MIT’s compiler design class, 6.035, is on OCW. Following one of these classes will teach you the basic challenges involved in designing a compiler and the organizational principles that have emerged to solve these problems. If you’re ambitious, start with a free C front end and build a C compiler for your favorite architecture. There is ckit for ML, Language.C for Haskell or clang (part of the LLVM project) written in, I think, C++. There are others, no doubt. This way, you won’t have to write a front-end and you will have a huge source of potential examples on which to test your compiler and, when your start to design optimizations (the fun part), explore your compiler’s performance.

      For a few years after I first took 6.035, whenever the course was taught again and the project (toy language) for that semester was announced, I would spend a long caffeine-fueled weekend hacking out a new compiler. I must have build three or four compilers this way, targeting different architectures and exploring various design choices. This was a very valuable experience.

      Once you understand the basics of compiler design, what’s next? There are a number of directions you can go.

      You can study programming language features and their implementation in a compiler and runtime. For this, I recommend a book like Scott’s Programming Language Pragmatics or Turbak and Gifford’s Design Concepts in Programming Languages. I used a draft version of the latter in MIT’s Programming Languages class, 6.821. This will move beyond compiling the standard C- or Pascal-like toy languages standard in most first-year compiler courses. Learn about semantics: denotational, operational and axiomatic. This is important if you (a) want to build a correct compiler, and (b) as a theoretical foundation for program analysis.

      At the other end of the spectrum are computer architectures. Go learn about computer architecture: instruction set architectures and micro-architectures, their implementation. There are many interesting architectures out there to study. Write assembly language programs by hand. Learn to extract maximal performance form an architecture on small examples. Pay attention to the techniques used to extract performance when hand-coding. These will be the basis for compiler optimizations. The standard text for computer architecture is probably Hennessy and Patterson’s text, Computer Architecture: A Quantitative Approach.

      A deep understanding of both the semantics of your input programming language and your target (micro-)architectures is essential to building a compiler that generates high-performance code. Program analysis and optimization is the bridge between the input program and assembly language. Start with a book on advanced compiler design like Muchnick’s Advanced Compiler Design and Implementation (or maybe the new dragon book, I haven’t looked at it) or start diving into the literature or program analysis and optimization.

      What are you trying to understand about compilers?

      's avatar

      [翻译] 用 Ruby 写编译器之六:匿名函数 lambda

      原文链接:http://www.hokstad.com/writing-a-compiler-in-ruby-bottom-up-step-6.html


      既然上次已经提到说,我们其实是在从 Lisp、Scheme 还有类似的其他语言中借鉴各种要实现的功能(我没想过要把这个项目做成原创的⋯⋯或者至少也要等到以后再说吧),那么现在也是时候实现一些更加强大的功能了。

      那就来做延迟求值以及匿名函数吧

      Lambda ,又名匿名函数,可以像普通的数值或者字符串类型那样被当作函数参数来到处传递,也可以在需要的时候才调用(当然不调也可以)。同时,外层函数(也就是定义匿名函数的函数)作为它们的运行环境,在其中定义的局部变量可以被这些匿名函数所访问。这就形成了一个闭包。我们这次并不是要实现完整的闭包功能,只是开头的一小步而已,完整的实现要等到再后面了。

      其实说到底,正如编程语言中的其他很多功能一样,闭包也只是又一种语法糖而已。比如说,你可以认为这样其实是定义了一个类,这个类中有唯一一个需要被调用的方法,还有一些作为运行环境的成员变量(或者你也可以反过来用闭包来实现面向对象系统 – 这是由 Wouter van Oortmerssen 所提出来的观点。自从我发现 Amiga E 这个项目之后,作为作者的他就成了我的偶像。如果你是一个编程语言方面的极客的话,那你就一定要去看看 Wouter 所做过的东西) – 有很多功能其实都是相互正交的。

      ( blah blah blah ⋯⋯这个人在说自己很啰嗦之类的,就不翻了)

      那么,为了避免定义很多具名小函数的麻烦,同时降低函数重名的概率, lambda 允许你在任何需要它们的地方进行定义,并且返回一个表示所定义函数的值。

      我们现在所要增加的是像下面这样的东西:

      (lambda (args) body)
      (call f (args))
      

      第一个表达式会返回所定义的函数(目前来说,其实就是这个函数的起始地址),而不是去执行这个函数。而 call ,当然就是以指定的参数列表,来调用传给它的那个函数地址。

      那么,这跟“真正”的闭包又有什么区别呢?

      最重要的一点就是,当你在 lambda 中引用了外层作用域中的某个变量后,那么,当以后对同一个 lambda 进行调用时,这个变量还是可以访问的。这样的变量与 lambda 绑定在了一起。当然,只是得到一个函数的地址的话,肯定是实现不了这个功能的。让我们来看一种实现闭包的技术吧,这样你就能够了解大概所要做的工作了(工作量不大,但也不小):

      • 我们可以创建一个“环境”,一个存放那些被引用到的外部变量的地方,以使得当外层函数返回之后,它们也可以继续存在。这个环境必须是在堆中,而且每次对外层函数的调用,都需要创建一个新的环境。
      • 我们必须返回一个可以用来访问这个环境的东西。你可以返回一个对象,其中的成员变量就是被 lambda 引用到的那些变量。或者你也可以用一个 thunk (中文叫啥呢?),也就是自动生成出来的一个包含有指向这个对象指针的小函数,它会在调用我们的 lambda 之前将这个对象加载到预先指定的地方。或者你也可以用什么其他的办法。
      • 你必须决定有哪些变量需要放入这个环境中。可以是外层函数中的所有局部变量,当然也可以只是那些被引用到的变量。后一种作法可以节省一定的内存空间,但是需要作更多的工作。

      好吧,还是让我们先来把匿名函数本身给弄出来吧。就像以前一样,我会一步一步地说明所要做的修改,但我同时也会对之前的代码做一些整理。这些整理的部分我就不一一说明了。

      首先是对 lambda 表达式本身进行处理的方法:

        def compile_lambda args, body
          name = "lambda__#{@seq}"
          @seq += 1
          compile_defun(name, args,body)
          puts "\tmovl\t$#{name},%eax"
          return [:subexpr]
        end
      

      这个实现应该是很容易理解的吧。我们在这里做的,就是给要定义的匿名函数,生成一个形如 lambda__[number] 的函数名,然后就把它当作一个普通函数来处理了。虽然你也可以就把它生成到外层函数的函数体中,但我发现那样做的话,就会显得很乱的样子,所以我现在还是就把它作为单独的函数来处理了。然后我们调用 #compile_defun 方法来处理这个函数,这样的话,这个函数其实也就只有对用户来说,才是真正匿名的了。然后我们把这个函数的地址保存在寄存器 %eax 中,这里同时也是我们存放子表达式结果的地方。当然,这是一种很懒的作法啦,我们迟早需要为复杂的表达式,实现更加强大的处理机制的。但寄存器分配果然还是很麻烦的一件事情,所以现在就先这样吧(将所有的中间结果压入栈中也是一种可行的作法啦,不过那样比较慢)。

      最后,我们返回一个 [:subexpr] ,来告拆调用者到哪边可以得到这个 lambda 的值。

      之后是一些重构。你也许已经注意到了, #compile_exp 中的代码有点乱,因为要处理不同类型的参数。让我们把这部分代码给提取出来:

        def compile_eval_arg arg
          atype, aparam = get_arg(arg)
          return "$.LC#{aparam}" if atype == :strconst
          return "$#{aparam}" if atype == :int
          return aparam.to_s if atype == :atom
          return "%eax"
        end
      

      要注意的是,这里又出现了一个新的 :atom 类型。借助于此,我们就可以把一个 C 函数的地址传给 :call 指令了。反正实现起来也很简单。当然,我们还要在 #get_arg 方法中加上如下代码,以使其生效:

          return [:atom, a] if (a.is_a?(Symbol))
      

      然后,作为重构的一部分,对 :call 的处理被分离了出来,成为一个单独的方法:

        def compile_call func, args
          stack_adjustment = PTR_SIZE + (((args.length+0.5)*PTR_SIZE/(4.0*PTR_SIZE)).round) * (4*PTR_SIZE)
          puts "\tsubl\t$#{stack_adjustment}, %esp"
          args.each_with_index do |a,i|
            param = compile_eval_arg(a)
            puts "\tmovl\t#{param}, #{i>0 ? i*4 : ""}(%esp)"
          end
      
          res = compile_eval_arg(func)
          res = "*%eax" if res == "%eax" # Ugly. Would be nicer if retain some knowledge of what res contains.
          puts "\tcall\t#{res}"
          puts "\taddl\t#{stack_adjustment}, %esp"
          return [:subexpr]
        end
      

      看起来很熟悉对不对?因为它就是原来的 #compile_exp 方法,只不过是用 #compile_eval_arg 替换掉了其中的一些代码。另外有改动的地方,就是同时也用 #compile_eval_arg 方法来得到要调用的函数,并对可能得到的 %eax 做了些手脚,在前面加了个星号。

      如果你知道这是怎么回事的话,你也许已经开始寻思其他的点子了,而不管那是真正的好事,还只是开枪打自己的脚。上面的代码其实就相当于,你把任意一个表达式的值当作指向一段代码的指针,然后也不做任何的检查,就直接跳过去执行它。如果是往一个随机的地址进行跳转的话,你最有可能得到的就是一个段错误了。当然,你也可以很容易地通过这项技术,来实现面向对象系统中的虚函数跳转表,或者其他的什么东西。因此,安全性将会是以后必须要考虑的东西。还有就是,要实现对一个地址(而不是函数名)的间接调用,你必须要在这个地址前面加上星号。

      那么,现在的 #compile_exp 方法变成什么样子了呢?简单来说就是,变得整齐多了:

        def compile_do(*exp)
          exp.each { |e| compile_exp(e) }
          return [:subexpr]
        end
        def compile_exp(exp)
          return if !exp || exp.size == 0
          return compile_do(*exp[1..-1]) if exp[0] == :do
          return compile_defun(*exp[1..-1]) if exp[0] == :defun
          return compile_ifelse(*exp[1..-1]) if exp[0] == :if
          return compile_lambda(*exp[1..-1]) if exp[0] == :lambda
          return compile_call(exp[1], exp[2]) if exp[0] == :call
          return compile_call(exp[0], *exp[1..-1])
        end
      

      看起来很不错,不是吗?#compile_call 几乎跟之前的 #compile_exp 一模一样,只不过是把一些代码给提取出来,成为了辅助方法而已。

      那么就来简单地测试一下吧:

      prog = [:do,
        [:call, [:lambda, [], [:puts, "Test"]], [] ]
      ]
      

      (看起来也没那么糟不是吗?)

      编译运行之:

      $ ruby step6.rb >step6.s
      $ make step6
      cc      step6.s  -o step6
      $ ./step6
      Test
      $
      

      这篇的代码在这里

      之后的部分

      因为我合并了几篇文章,下面就列一下更新过后的,“已经写完但还需要整理的”文章列表。因为我肯定还会合并下面的某些部分的,所以我想我需要找时间开始写一些新的部分了(为了完成我所定下的 30 篇的计划)。

      • 步骤七:再看用匿名函数实现循环,以及对函数参数的访问
      • 步骤八:实现赋值语句以及简单的四则运算
      • 步骤九:一个更简洁的 while 循环语句
      • 步骤十:测试这个语言:实现一个简单的输入转换模块
      • 步骤十一:重构代码生成器,分离架构相关的部分
      • 步骤十二:对某些功能点的讨论,以及未来的前进方向
      • 步骤十三:实现数组
      • 步骤十四:局部变量以及多重作用域
      • 步骤十五:访问变长参数列表
      • 步骤十六:再看输入转换模块,重构以支持新功能,并用其解析它自己
      • 步骤十七:总结实现自举所需要的功能点
      • 步骤十八:开始实现真正的解析器
      's avatar

      [翻译] 用 Ruby 写编译器之五:整数常量,以及 if 语句

      原文链接:http://www.hokstad.com/writing-a-compiler-in-ruby-bottom-up-step-5.html


      上次我承诺会发布的更快一些,不过还是失败了⋯⋯作为补偿,这章的内容将会是原计划中的第 5,6,7 章内容的合并,因为这三章确实都很短。闲话少叙:

      处理数字常量

      到目前为止,我们只处理了一些实现所必须的数字常量,也就是当一个外部函数的返回值是数字的情况,而且没有做任何形式的类型检查。

      那么,就让我们来看一下 gcc 在 C 语言中是怎样处理各种类型(包括 long long 等)的整数的吧。当然,这次还是针对 32 位的 x86 架构:

      void foo1(unsigned short a) {}
      void foo2(signed short a) {}
      void foo3(unsigned int a) {}
      void foo4(signed int a) {}
      void foo5(unsigned long a) {}
      void foo6(signed long a) {}
      void foo7(unsigned long long a) {}
      void foo8(signed long long a) {}
      
      int main()
      {
        foo1(1);
        foo2(2);
        foo3(3);
        foo4(4);
        foo5(5);
        foo6(6);
        foo7(7);
      }
      

      我略掉了大部分 gcc 所生成的代码,如果愿意,你可以自己执行 gcc -S 命令来看。有趣的部分是对各个函数的调用,其生成的代码如下:

              movl    $1, (%esp)
              call    foo1
              movl    $2, (%esp)
              call    foo2
              movl    $3, (%esp)
              call    foo3
              movl    $4, (%esp)
              call    foo4
              movl    $5, (%esp)
              call    foo5
              movl    $6, (%esp)
              call    foo6
              movl    $7, (%esp)
              movl    $0, 4(%esp)
              call    foo7
      

      换句话说,至少在处理函数调用的时候,gcc 都会把各种整数类型统一作为 32 位的整型来处理,除了 long long 类型之外。因此,我们可以暂时忘掉 long long 类型,而只处理 32 位以内的整数值,这样我们就可以忽略类型处理相关的东西了。

      懒惰还真是一种病啊。

      我们同时也会略过浮点型。为什么呢?因为只要有整数运算,你就可以实现一个完整的编译器了,所以现在就加入对浮点型的支持完全只是浪费时间而已。当然这个东西以后总归是要做的。

      另外,当还年轻时,我们连 FPU 是啥都还不知道呢。尽管如此,我们依然可以用整数来模拟各种定点运算,一样可以完成很多事情。

      那么,我们真正要做的修改有哪些呢?

      在方法 #get_arg 中,在处理字符串常量之前,加入如下代码:

          return [:int, a] if (a.is_a?(Fixnum))
      

      在方法 #compile_exp 中,我们用如下代码来处理 #get_arg 的返回值:

          elsif atype == :int then param = "$#{aparam}"
      

      然后,就完事了。就这么简单。

      然后就是测试啦:

      prog = [:do,
        [:printf,"'hello world' takes %ld bytes\n",[:strlen, "hello world"]],
        [:printf,"The above should show _%ld_ bytes\n",11]
      ]
      

      插曲:针对原生数据类型的一些思考

      “纯”面向对象类型的语言很棒,但并不是对底层的代码生成器而言的,我想。不管你是否想要实现一个纯的面向对象语言,我仍然坚信,首先实现原生的数据类型和其操作,是非常有价值的。你可以在后面的阶段再来考虑是否要对用户隐藏它们,或者是让它们看起来像是对象,或者是透明地在它们和对象之间执行自动转换的操作,等等。

      要注意的是,Matz 的 Ruby 解释器(Matz Ruby Interpreter,简称MRI)就是这样实现的:里面的数字就跟“真正的”对象神马的完全不一样,但是解释器本身却尽其所能的对用户隐藏这一事实。不过我个人认为 MRI 做的还是不够。

      If ... then ... else

      如果没有某种形式的条件逻辑支持的话,我们的语言是没有太大用处的。几乎所有有用的语言都支持某种形式的 if .. then .. else 构造。现在,我们要实现的是像 [:if, condition, if-arm, else-arm] 这样的构造,而且是以 C 语言的形式来实现。也就是说, 0 和空指针都表示假,其他值则都为真。

      仍然是一个简单的例子:

      void foo() {}
      void bar() {}
      int main()
      {
        if (baz()) {
          foo();
        } else {
          bar();
        }
      }
      

      相关的汇编输出:

              call    baz
              testl   %eax, %eax
              je      .L6
              call    foo
              jmp     .L10
      .L6:
              call    bar
      .L10:
      

      对于大多数的语言和架构来说,这都是一个编译 if .. then .. else 时会采用的通用模板:

      • 计算条件表达式。
      • 对结果进行测试(这里用的是 testl 指令 – 在其他架构中比较通用的还有 cmp 指令,或者对寄存器进行自减)。 testl 指令比较它的左右两个操作数,并进行相应的标志位设置。
      • 然后,条件跳转到 else 语句处。这里,我们检查条件表达式的值是否不为真。在这种情况下我们用的是 je 指令,即“相等时跳转”( jump on equal ),也就是当结果相等时跳转(要注意的是,在大多数的 CPU 架构中,很多指令都会设置条件码,而不仅仅是显示的测试指令)。
      • 然后执行 then 子句。
      • 跳过 else 子句,继续执行整个 if 语句之后的部分。
      • 生成 else 子句的标号,以及其中的指令序列。
      • 生成 if 语句的结束标号。

      其他还有很多不同的变种,比如根据条件表达式取值的概率,或者某个架构中是否跳转的执行代价,进而调整两个子句的顺序等。不过就目前来说,上面的方法已经足够了。

      总之,编译的方法还是很简单的,应该说就是以上所述流程的直译:

        def ifelse cond, if_arg,else_arm
          compile_exp(cond)
          puts "\ttestl\t%eax, %eax"
          @seq += 2
          else_arm_seq = @seq - 1
          end_if_arm_seq = @seq
          puts "\tje\t.L#{else_arm_seq}"
          compile_exp(if_arm)
          puts "\tjmp\t.L#{end_if_arm_seq}"
          puts ".L#{else_arm_seq}:"
          compile_exp(else_arm)
          puts ".L#{end_if_arm_seq}:"
        end
      

      这段代码应该很易懂的,其实就是对所有子句 – 条件, then 子句,以及 else 子句 – 分别调用 #compile_exp 方法,并在其间插入所需的辅助指令,同时用 @seq 成员来生成所需的标号。

      为了使其生效,我们在 #compile_exp 方法中的 return defun ... 之后插入如下代码:

          return ifelse(*exp[1..-1]) if (exp[0] == :if)
      

      下面是一个简单的测试:

      prog = [:do,
        [:if, [:strlen,""],
          [:puts, "IF: The string was not empty"],
          [:puts, "ELSE: The string was empty"]
        ],
        [:if, [:strlen,"Test"],
          [:puts, "Second IF: The string was not empty"],
          [:puts, "Second IF: The string was empty"]
        ]
      ]
      

      这里是最终结果

      如往常般,执行的方法如下:

      $ ruby step5.rb >step5.s
      $ make step5
      cc   step5.s   -o step5
      $ ./step5
      ELSE: The string was empty
      Second IF: The string was not empty
      $
      

      有关循环的一些思考

      非条件循环是很容易实现的,不过我们需要实现它吗?显然不需要。我们已经可以用递归来实现循环了,又何必要乱加东西呢?

      不过,要令它工作良好,我们还需要实现尾递归优化,可是我现在还没有做好准备。尾递归优化,或者更一般的形式 – 尾调用优化 – 所说的情况是,在一个函数的末尾,调用了一个需要相同或者更少个数参数的函数,并返回它所返回的值。在这种情况下,你可以将当前函数的调用栈,复用给被调用的函数来使用,并且是通过 jmp 而不是 call 来调用这个函数。 jmp 指令不会在堆栈中压入一个新的返回地址,因此当这个被调用的函数返回,返回到的就是当前函数的调用者那里,而不是当前的这个函数。

      这就同时完成了几件事情:首先,也是最重要的,就是堆栈不再会随着调用而增长了。其次,我们能够省掉几个指令周期。有了尾调用优化,再配合其他几个优化之后,你就可以这样来写循环,而不用担心堆栈溢出的问题了:

      [:defun, :loop, [], [:do,
        [:puts, "I am all loopy"],
        [:loop]
      ],
      [:loop]
      

      换句话说,尾调用优化意味着,对任何形如 (defun foo () (do bar foo)) 的函数来说,堆栈的使用率都会从原来的成比例增长减少为定值了。

      当前的版本已经可以编译上面的代码了,不过它会很快用完堆栈并且崩溃掉的。不是很令人满意啊。

      果然(原文: I sense a disturbance in the force ),两位读到这篇文章的极客都指出了堆栈增长的问题。

      现在,让我们先忽略这个问题吧,同时注意下对堆栈空间的使用好了。之后,我们会实现一个专门的循环结构的。目前就这样吧 – 如果以后我实现了尾调用优化的话,我们还可以重新考虑在运行时库中实现一个循环构造的方案。

      不管怎样,我们现在可以写出一个无限循环了⋯⋯不是太有用,不是吗?

      当然,我们其实也已经可以写 while 循环了:

      (defun some-while-loop () (if condition (some-while-loop) ()))
      

      看起来不是太好,不过确实可以工作。但看起来总归还是太丑了,所以我们总归还是要实现一个正尔八经的 while 循环的。

      我不是一个 Lisp 程序员。我没办法处理那么多的括号⋯⋯不过 Lisp 的语法确实很适合于一门还没有语法的语言。到了一定的阶段之后,我会实现一个完整的解析器的。也许是出于偶然,我已经实现的和将要实现的很多东西在某种程度上来说都是取自于 Lisp 的,至少看起来是这样的。如果你能适应 Lisp 的语法的话,这个语言还是非常强大的。就算你不打算用它来开发你的程序,好好地学一下这门语言也是非常值得的。

      在我想来,很多看起来像是从 Lisp 中得来的点子,大概都是来自于我花在学习 Lisp 的有限经验。

      下一篇:匿名函数,也许不止。

      's avatar

      [翻译] 用 Ruby 写编译器之四:自定义函数,以及运行时支持

      原文链接:http://www.hokstad.com/writing-a-compiler-in-ruby-bottom-up-step-4.html


      抱歉,又拖了很长时间。要忙的事情实在很多。正如上一篇文章末尾提到的那样,这次要讲的是自定义函数,以及一个简单的“运行时库”。

      自定义函数

      一门编程语言如果连函数和方法都没有的话,那也就不能算是一门语言了。而且,实践表明,一门面向对象语言中的所有特性都可以通过过程式的语言要素来实现:一个方法也只不过是以一个对象为额外参数的函数而已。因此,增加对函数的支持就是实现一门语言的核心所在。

      其实,这个东东也是很简单的啦。跟以前一样,还是让我们来看一下 C 语言中的函数是怎么实现的吧:

      void foo()
      {
        puts("Hello world");
      }
      

      gcc 生成的汇编代码是这个样子滴:

      .globl foo
              .type   foo, @function
      foo:
              pushl   %ebp
              movl    %esp, %ebp
              subl    $8, %esp
              movl    $.LC0, (%esp)
              call    puts
              leave
              ret
              .size   foo, .-foo
      

      其中的函数调用现在应该很容易认了吧。剩下的就是简单的样板代码了:

      在函数的开头,首先是将寄存器 %ebp 压入堆栈,然后拷贝寄存器 %esp%ebp 。而在函数的最后, leave 指令就是前面两条指令的逆操作,而 ret 指令则是从堆栈中弹出要返回到的指令地址(也就是调用该函数的那条指令的下一条指令)并跳转。为什么在这里要将 %esp (也就是堆栈指针)拷到 %ebp 呢?嘛,一个很明显的好处就是你可以尽情的申请堆栈空间,然后在完事时简单地将 %ebp 拷回给 %esp 就行了。从上面就可以看到, GCC 已经充分利用了这一点,直接用 leave 指令来处理调用函数时对参数所申请的空间 – 反正手工释放也只是浪费时间而已。

      这么说来的话,要做的事情应该就很简单了啊。

      首先需要修改方法 Compiler#initialize ,创建一个用来保存所有函数定义的哈希:

        def initialize
          @global_functions = {}
      

      然后增加一个输出所有函数定义的方法:

        def output_functions
          @global_functions.each do |name,data|
            puts ".globl #{name}"
            puts ".type   #{name}, @function"
            puts "#{name}:"
            puts "\tpushl   %ebp"
            puts "\tmovl    %esp, %ebp"
            compile_exp(data[1])
            puts "\tleave"
            puts "\tret"
            puts "\t.size   #{name}, .-#{name}"
            puts
          end
        end
      

      可以看到,这里也同时包括了 .globl.type.size 之类的东西。 .globl 的意思就是你想让这个函数也能够从其他文件(也就是编译单元)中调用,这在链接多个目标文件的时候是很重要的。我想 .type.size 主要是用在调试的时候,分别用来表示一个符号对应的是一个函数,以及这个函数的大小。

      除了这些之外,这个方法就很简单啦 – 它会通过调用 #compile_exp 方法来完成实际的工作。

      我们再来增加一个用来定义函数的辅助方法:

        def defun name, args, body
          @global_functions[name] = [args, body]
        end
      

      然后在方法 #compile_exp 中增加如下的几行代码:

          return if !exp || exp.size == 0
          return defun(*exp[1..-1]) if (exp[0] == :defun)
      

      之所以要增加第一行代码,一方面是出于健壮性的考虑,同时这也允许我们用 nil 和空数组来表示“啥也不做”的意思,当你要定义一个空函数的时候就会用到这一点了。这样一来,第二行代码就不需要去检查将要定义的是不是一个空函数了。

      不知道你注意到了没有,我们其实已经实现了对函数的递归定义。像 [:defun,:foo,[:defun, :bar, []]] 这样的代码完全是合法的。同时你也许会注意到,这个实现会导致两个函数其实都是可以从别处调用的。好吧,现在是没关系的啦,我们以后会处理这个的(要么不允许编写这样的代码,要么就只允许外层函数来调用内层函数 – 我还没有决定到底要做哪个啦)。

      剩下的事情就是输出这些函数的定义了,因此我们在方法 #compile 中对 #output_constants 的调用之前增加如下的一行:

          output_functions
      

      增加对一个运行时库的支持

      首先,让我们将现在的 #compile 方法重命名为 #compile_main ,然后重新定义 #compile 方法如下:

        def compile(exp)
          compile_main([:do, DO_BEFORE, exp, DO_AFTER])
        end
      

      之后是对常量 DO_BEFOREDO_AFTER 的定义(如果愿意的话,你也可以把它们放在一个单独的文件中,我现在就直接把它们放在开头好了):

      DO_BEFORE= [:do,
        [:defun, :hello_world,[], [:puts, "Hello World"]]
      ]
      DO_AFTER= []
      

      你得承认,你想看到的应该是更加高级一些的东东,但那样就违背我们最初的目标了。上面的代码对于实现一个运行时库来说已经足够了。当然,你也可以用一些只能通过 C 或者汇编才能实现的东西,只要把包含那些函数实现的目标文件给链接进来就可以了,因为我们一直都是在按照 C 语言的调用规则来办事的嘛。

      让我们来测试一下吧。在 Compiler.new.compile(prog) 的前面加入下面的代码:

      prog = [:hello_world]
      

      然后编译运行:

      $ ruby step4.rb >step4.s
      $ make step4
      cc    step4.s   -o step4
      $ ./step4
      Hello World
      $
      

      你可以在这里找到今天的成果。

      对函数参数的访问吗?

      今天还遗留了一个任务:实现对函数参数的访问。这个的工作量可是不小的。放心,我不会忘了这个的,这将会是第八篇文章的主题。我也不会让你等太久的啦,这次一定 :-)

      's avatar

      [翻译] 用 Ruby 写编译器之三:语句序列,以及子表达式

      原文链接:http://www.hokstad.com/writing-a-compiler-in-ruby-bottom-up—step-3.html


      我本来是想要早点发表的,可是我这周又不行了 – 虽然整理一篇旧文只需要半个小时。不管怎样,这是第三章,而且我会在末尾大概列一下之后的大纲。由于我会试着把一些小的步骤组合成更有内容的章节(下面就有个这样的例子),因此原来的 30 篇文章已经被我给减到了 20 篇左右(当然,这只是我已经完成了的,后面还有新的呢)。

      用 do 语句将表达式给串起来

      到目前为止,上次的第二版程序只能编译一个单独的表达式。只是这样的话,并不是非常的有用啊。因此我决定实现一种支持顺序执行的结构,就像函数体那样的。当然,如你所想,这是很简单的。我会增加一个关键字 do ,而其作用就是顺序执行传给它的每一个(个数不限哦,或者说,只受限于内存的大小)参数表达式。看起来就像这样:

      prog = [:do,
        [:printf,"Hello"],
        [:printf," "],
        [:printf,"World\n"]
      ]
      

      要实现这个是非常简单的。我们只需要在函数 #compile_exp 的开头加入下列代码:

          if exp[0] == :do
            exp[1..-1].each { |e| compile_exp(e) }
            return
          end
      

      递归在这里的作用很重要哦 – 毕竟你是在处理一个树形结构,那也就需要在越来越深层的树结点之上调用实现编译的核心函数,而这当然也包括我们的下一个目标,即对子表达式的处理。

      子表达式,步骤一

      先来给出一个我们想要支持的用例:

      prog = [:printf,"‘hello world’ takes %ld bytes\n",[:strlen, “hello world"]]
      

      第一个需要改变的地方,在函数 #get_arg 中,我们在其开头加入如下的代码:

          # Handle strings or subexpressions
          if a.is_a?(Array)
            compile_exp(a)
            return nil # What should we return?
          end
      

      如果你这时已经试着用上面的代码来编译测试用例了的话,gcc 会报错给你的,因为我们现在只处理了 #get_arg 的返回值是一个字符串常量对应的序列号的情况,而这对子表达式来说显然是不适用的。

      子表达式,步骤二:返回值

      那么 gcc 是怎么处理这个的呢。让我们来看看下面这段代码:

      int main()
      {
        printf("'Hello world' takes %ld bytes\n",foo("Hello world"));
      }
      

      所产生的汇编吧(只截取 main 中相关的部分):

          subl    $20, %esp
          movl    $.LC0, (%esp)
          call    foo
          movl    %eax, 4(%esp)
          movl    $.LC1, (%esp)
          call    printf
          addl    $20, %esp
      

      应该说还是很直观的吧。gcc 首先会去调用子表达式( foo ),并且希望这个函数能够把它的返回值放入寄存器 %eax 中,然后就会把这个值作为参数拷到堆栈上,而不是什么字符串常量的地址。

      首先是要调整 #get_arg 函数:

        def get_arg(a)
          # Handle strings and subexpressions
          if a.is_a?(Array)
            compile_exp(a)
            return [:subexpr]
          end
          seq = @string_constants[a]
          return seq if seq
          seq = @seq
          @seq += 1
          @string_constants[a] = seq
          return [:strconst,seq]
        end
      

      唯一需要改动的地方就是返回值了,我们增加了一个表示返回值类型的标识 – 以后还会加入其他类型的。

      剩下的工作就是改写 #compile_exp 函数中的相关部分了。这时就不能直接收集 #get_arg 的返回值了,而是需要对每个参数都做相应的处理并直接输出(而这同时也是 stack_adjustment 需要修改的原因,因为已经没有 args 数组了):

          stack_adjustment = PTR_SIZE + (((exp.length-1+0.5)*PTR_SIZE/(4.0*PTR_SIZE)).round) * (4*PTR_SIZE)
          puts "\tsubl\t$#{stack_adjustment}, %esp" if exp[0] != :do
      
          exp[1..-1].each_with_index do |a,i|
            atype, aparam = get_arg(a)
            if exp[0] != :do
              if atype == :strconst
                param = "$.LC#{aparam}"
              else
                param = "%eax"
              end
              puts "\tmovl\t#{param},#{i>0 ? i*4 : ""}(%esp)"
            end
          end
      

      如你所见,并不是什么复杂的更改。我们只是检查了 #get_arg 所返回的类型信息,并相应的输出字符串常量或者寄存器 %eax 而已。随着我们加入更多要处理的情况,这个部分代码还会继续扩充的。

      你可以在这里找到最新版本的代码

      之后的计划

      这里只列出的基本完成的部分。我的计划是,当我开始着手写新的部分时,我会将重心放在一个简单的解析器上,以尽快实现编译器的自举(即,编译它自己)。

      • 步骤四:运行时,以及函数的定义
      • 步骤五:处理其他类型的常量值
      • 步骤六:条件表达式 if ... then ... else
      • 步骤七:循环语句
      • 步骤八:匿名函数( lambda
      • 步骤九:用匿名函数来实现循环,以及对函数参数的处理
      • 步骤十:赋值,以及简单的代数运算
      • 步骤十一:更简结的 while 循环
      • 步骤十二:测试我们的语言:开发一个简单的输入转换模块
      • 步骤十三:重构代码生成模块,并抽象出平台相关的部分
      • 步骤十四:对一些概念的讨论,以及今后的前进方向
      • 步骤十五:数组
      • 步骤十六:局部变量以及多种作用域
      • 步骤十七:可变长参数列表
      • 步骤十八:再看输入转换模块:测试新的功能点,以及自我转换
      • 步骤十九:确定自举所需要实现的功能
      • 步骤二十:开始实现真正的解析器
      's avatar

      [翻译] 用 Ruby 写编译器之二:函数调用,以及 Hello World

      原文地址:http://www.hokstad.com/writing-a-compiler-in-ruby-bottom-up-step-2.html


      我会选择 Ruby 来作为我的实现语言并没有什么特别的理由。在现阶段,语言的选择并不重要;不过,我确实很喜欢 Ruby。

      在这之后,我会采取一系列的步骤令所实现的语言向其实现语言靠拢。我的意思是,我想将编译器实现为可以自举的,即它应该能够编译自身。

      而这也就意味着,要么我的编译器需要至少支持 Ruby 语言的一个子集,要么就需要一个中间的翻译步骤,来将编译器中的实现翻译成它自己可以编译的语言。

      虽然这一点并没有限制你所用的实现语言,但那至少意味着你用来实现的语言跟你要实现的语言之间很相似,除非你对实现编译器的自举没有什么兴趣。

      这也同时意味着,如果你想实现编译器的自举,那你最好不要用实现语言中的什么复杂的特性。要记住,你得实现所有你用过的那些语言特性,不然,当你要开始做自举的时候,你就得对整个编译器的架构做大的调整了。那一点都不好玩。

      话说回来,使用 Ruby 来作为实现语言的一大优点(同样对于其他某些语言来说也是这样,比如说 Lisp ),就是你可以很容易地构建出一个树形的数据结构出来 – Ruby 的话就是用数组或者哈希, Lisp 的话就是用列表。

      这也就是说,我可以用数组来手工构建抽象语法树,从而避免了实现一个语法分析器的工作。耶!代价就是很丑,不过也可以接受的语法啦。

      Hello World

      Hello World 的话看起来会是这个样子的:

      [:puts, "Hello World"]
      

      这里,我们需要处理的东西非常简单:我们需要将一个参数压入堆栈,然后调用一个函数。

      那么就让我们来看一下怎样用 x86 汇编来做这件事吧。我用 gcc -S 编译了下面的这段 C 程序:

      int main()
      {
          puts("Hello World");
      }
      

      然后看看输出会是什么样子的。下面给出的是真正相关的部分,是与上一次的输出比较之后的结果:

              .section        .rodata
      .LC0:
              .string "Hello World"
              .text
      ...
              subl    $4, %esp
              movl    $.LC0, (%esp)
              call    puts
              addl    $4, %esp
      ...
      

      如果你懂一些汇编的话,就算以前没有写过 x86 的汇编程序,也应该可以很容易的看懂这段代码吧:

      • 这里首先定义了一个字符串常量。
      • 通过对堆栈指针的减 4 操作,在堆栈上申请了一段 4 个字节大小的空间。
      • 然后将之前定义的字符串常量的地址,放入刚刚申请的那 4 个字节的空间中。
      • 接着,我们调用了由 glibc 提供的 puts 函数(在这个系列中,我会假设你已经有了 gcc/gas + glibc ;Linux 的话这些东东应该已经有了)。
      • 最后,通过一个加 4 操作来释放堆栈空间。

      那么,我们要怎样在我们的编译器中实现这一切呢?首先,我们需要一种方法来处理那些字符串常量,通过在上次的 Compiler 类的实现中添加下面的代码(我的所有 Ruby 代码都在这里,这样你就知道该做什么了):

        def initialize
          @string_constants = {}
          @seq = 0
        end
        def get_arg(a)
          # For now we assume strings only
          seq = @string_constants[a]
          return seq if seq
          seq = @seq
          @seq += 1
          @string_constants[a] = seq
          return seq
        end
      

      这段代码就是简单地将一个字符串常量映射到一个整数上,而这个整数则对应着一个标号。相同的字符串常量会对应到相同的整数上,因此也只会被输出一次。用哈希而不是数组来保证这种唯一性是一种很常用的优化手段,不过也不一定非要这样做。

      下面这个函数是用来输出所有的字符串常量的:

        def output_constants
          puts "\t.section\t.rodata"
          @string_constants.each do |c,seq|
            puts ".LC#{seq}:"
            puts "\t.string \"#{c}\""
          end
        end
      

      最后剩下的就是编译函数调用的代码了:

        def compile_exp(exp)
          call = exp[0].to_s
          args = exp[1..-1].collect {|a| get_arg(a)}
      
          puts "\tsubl\t$4,%esp"
      
          args.each do |a|
            puts "\tmovl\t$.LC#{a},(%esp)"
          end
      
          puts "\tcall\t#{call}"
          puts "\taddl\t$4, %esp"
        end
      

      也许你已经注意到这里的不一致性了:上面的代码虽然好像是可以处理多参数调用的样子,但却只从堆栈中减掉了一个 4 ,而不是按照实际的参数个数而进行相应的调整,从而导致了不同参数间的相互覆盖。

      我们马上就会处理这个问题的。对于我们简单的 Hello World 程序来说,目前这样已经足够了。

      在这段代码中还有几点需要注意:

      • 我们甚至都还没有检查被调用的函数到底存不存在 – gcc/gas 会帮我们处理这个问题的,虽然这也意味着没啥帮助的错误信息。
      • 我们可以调用任何一个可以连接的函数,只要这个函数只需一个字符串作为参数。
      • 这段代码目前还有很多需要被抽像出去的地方,比如说得到被调函数地址的方法,还有所有那些硬编码进来的 x86 汇编等。相信我,我会(慢慢)解决这些问题的。

      现在我们可以来试着运行一下这个编译器了。你应该会得到下面这样的输出:

              .text
      .globl main
              .type   main, @function
      main:
              leal    4(%esp), %ecx
              andl    $-16, %esp
              pushl   -4(%ecx)
              pushl   %ebp
              movl    %esp, %ebp
              pushl   %ecx
              subl    $4,%esp
              movl    $.LC0,(%esp)
              call    puts
              addl    $4, %esp
              popl    %ecx
              popl    %ebp
              leal    -4(%ecx), %esp
              ret
              .size   main, .-main
              .section        .rodata
      .LC0:
              .string "Hello World"
      

      下面来测试一下:

      [vidarh@dev compiler]$ ruby step2.rb >hello.s
      [vidarh@dev compiler]$ gcc -o hello hello.s
      [vidarh@dev compiler]$ ./hello
      Hello World
      [vidarh@dev compiler]$
      

      那么,要怎么处理多个参数的情况呢?

      我不会再展示用来说明的 C 代码和对应的汇编代码了 – 进行不同参数个数的调用并查看其输出对你来说应该不难。相反,我就直接给出对 compile_exp 函数所做的修改了(完整的代码在这里):

        PTR_SIZE=4
        def compile_exp(exp)
          call = exp[0].to_s
      
          args = exp[1..-1].collect {|a| get_arg(a)}
      
          # gcc on i386 does 4 bytes regardless of arguments, and then
          # jumps up 16 at a time, We will blindly do the same.
          stack_adjustment = PTR_SIZE + (((args.length+0.5)*PTR_SIZE/(4.0*PTR_SIZE)).round) * (4*PTR_SIZE)
          puts "\tsubl\t$#{stack_adjustment}, %esp"
          args.each_with_index do |a,i|
            puts "\tmovl\t$.LC#{a},#{i>0 ? i*PTR_SIZE : ""}(%esp)"
          end
      
          puts "\tcall\t#{call}"
          puts "\taddl\t$#{stack_adjustment}, %esp"
        end
      

      这里做了什么呢?改动的地方没几个:

      • 这里不再是申请固定大小的堆栈空间了(上一个版本中是 4 个字节),而是根据实际参数的个数来相应的调整堆栈指针。我得承认,我不知道 gcc 为什么会做这样的调整 – 而且原因并不重要,虽然我猜这是为了堆栈的对齐。优化和清理以后再说,并且,当你不知道某事的运行机理时,那就不要去改变它。
      • 这之后,如你所见,参数被一个一个地放到堆栈上了。我们还是假定它们全都是相同大小的指针(因此在 x86 上就是 4 个字节)。
      • 同时你还可以看到,第一个参数是被放在堆栈中最靠下的位置的。如果你还没有写过汇编程序,并且无法想象出这是怎么回事的话,那就把它们画出来吧;还要记住,这里的堆栈是向下扩展的。当申请空间时,我们是将堆栈指针向下移动的,而拷贝参数时则是从下往上(用越来越大的索引来访问 %esp ,就像你访问数组时一样)。

      这个编译器现在已经可以编译下面这样的代码了:

      [:printf,"Hello %s\n","World"]
      

      至于以后嘛

      这就是我们踏出的第一步,而且我保证之后的步骤会越来越实际的,因为只要实现很少的几个功能点,我们就可以编译实际的程序了。而且我会努力令这些步骤更加精炼,更多的说明这样做的原因,而不是仅仅解释做了什么。

      下面我会处理多个参数的调用(译者:我们不是才处理过嘛),然后是语句序列、子表达式,以及对返回值的处理,等等。

      大约十二个这样难度的步骤之后,我们就会完成函数定义、参数传递、条件判断、运行时库,甚至是一个用来实现匿名函数的简单的 lambda (真正的闭包就要到后面了)。

      再之后,我们会实现一个简单的文本处理程序,来对一个比 Ruby 的数组和符号更好一点的语法提供支持(只是某种程度啦,真正的语法分析得再多等等)。

      's avatar

      [翻译] 用 Ruby 写编译器之一:一个简单的 main 函数模板

      原文链接:http://www.hokstad.com/writing-a-compiler-in-ruby-bottom-up-step-1.html

      [译者抱怨:翻译好麻烦啊。]


      我已经将这件事情搁置了很长时间了 – 这个系列中最早的文章甚至可以追溯到 2005 年的早期,而那时我还没有开始写这个博客呢。

      我灰常喜欢编译器技术,而且我也已经写过好几个小型的编译器了。不久前我开始用 Ruby 语言写一个简单的编译器,而且我也以发表为目的记下了大量的笔记。问题在于,当我一步步地完成这个系列文章的时候,我的博客却渐渐地,没落了(?原文: Problem was I was that by the time I was finishing up the steps I have so far, my blog was languishing, and so it’s been just gathering dust. 啊,我的英文好渣)。

      我已经写完了大概 30 篇文章,并形成了一个简单但是可以工作的编译器。我也许要花上一段时间来发表它们,因为这些文章均需要一定程度的整理,而且我能花在写作上的时间不是很多。不过根据具体的时间安排,我也有可能一周就发表好几篇。

      闲话少叙,第一篇参上:

      有关一些背景,以及一小段代码

      通常,在开始尝试编写一个编译器时,我会选择自顶向下的开发方式。换句话说就是,我会采取常见的策略,从设计一个语法分析器开始,而不管这个分析器是手写的,还是通过分析器生成器来生成。然后我会通过一系列的树形结构转换程序将 AST(即 Abstract Syntax Tree,抽象语法树)转化为符号表,同时进行错误检查,为树结点附加各类信息,并最终进行代码生成的步骤。在我转而使用 Linux 之后,我在这里就会选择生成 C 代码,前提是你的语言能够很好的契合 C 的语意。从这里也可以看出,C 是一门多么低层的语言了。

      不过,我的第一个编译器是用 M68000 汇编语言写的,而且其输出也是 M68000 的汇编程序。

      我是从允许内嵌的汇编代码片断开始自举这个编译器的,并在此基础上逐步地增加所支持的语法结构。

      首先我加入了对函数的定义以及调用的支持,并以此为基础构建我的整个编译器。然后我会增加基于寄存器的基本四则运算的支持,然后是局部变量,等等等等,但在同时也会解析汇编,以保证当遇到内嵌汇编代码片段中的寄存器时,还能够保持寄存器分配器的简洁(原文: but parse the assembly so that the register allocator would stay clear of registers used in the assembler that was interspersed. )。

      然后这个编译器就从编译汇编语言的语法,逐渐演变成编译我所设计的杂牌语言的语法,其中的汇编也越来越少。

      我想再自举一个编译器出来。不过这次我就不打算用汇编来实现它了。这次我会从底层 – 也就是代码生成器 – 开始。并且我会完成如下几个目标:

      • 实现的简洁性优先于其性能。要点在于,我希望能够将它重写为以其自身来实现。换句话说,我希望它能够实现自举( self hosted ,跟 bootstrap 有啥区别呢?)。由于开始的代码最终会被替换掉,那么也就没有必要浪费时间在使它可以运行的更快上面。性能问题大可以以后再考虑。
      • 不要做限制性能的决定。虽然性能并不是我们最初的目标,但也不能太大意,而导致今后不好对此进行改进。 Ruby ,说的就是你( Ruby 的运行时非常的动态)。
      • 实现的简洁性优先于惯例。不要仅仅因为一个特性很方便就去实现它,而仅实现那些可以帮助我们完成对编译器的自举的特性。而这就意味着,这些特性要能使我们的生活更加方便,而不仅仅是无谓地增加了实现编译器本身的难度。
      • 不要急着设计语言,我们要设计的是特性。我想首先实现一个灵活而强大的代码生成器,以及一个可以支持各种功能的简洁的 AST 。我的意思是像 Lisp 语言那样极富表达能力的语法结构,并尽量用 Ruby 来达到相似的目标(这段翻的太渣了,请参考原文)。
      • 我并不想深入学习 x86 的汇编语言,虽然我会以其作为我的目标语言。这样说有点夸张。我很熟悉 M68K 和 6510 的汇编语言,同时我也具备足够的知识来阅读 x86 的汇编程序。虽然我从来没有用它写过什么像样的代码,而且我也不打算这么做。我所需要知道的一切信息都来自于查看 gcc 产生的汇编输出。要善用 gcc -S 命令。虽然可能对 x86 的某些细节存在疑问,但我所具备的基础汇编知识已经足够我理解那些个汇编代码了。虽然在此过程中,我很可能会犯一些很愚蠢的错误,但我同时也可以从中学到很多知识(而且那些非常相信能够从我这里学到相关知识的人们也请注意了 - 我在这方面也只是个初学者哦)。

      听起来之后貌似会抄很多的小道,同时也会遇到很多痛苦的地方,不是吗?但这同时也会非常有趣哦!

      不管怎样,我们先从一段没什么实际用处的代码开始吧:

      #!/bin/env ruby
      class Compiler
      
        def compile(exp)
          # Taken from gcc -S output
          puts <<PROLOG
       .file "bootstrap.rb"
       .text
      .globl main
       .type main, @function
      main:
       leal 4(%esp), %ecx
       andl $-16, %esp
       pushl -4(%ecx)
       pushl %ebp
       movl %esp, %ebp
       pushl %ecx
      PROLOG
      
          puts <<EPILOG
       popl %ecx
       popl %ebp
       leal -4(%ecx), %esp
       ret
      EPILOG
      
          puts <<GLOBAL_EPILOG
       .size main, .-main
      GLOBAL_EPILOG
        end
      end
      
      prog = [:puts,"Hello World"]
      
      Compiler.new.compile(prog)
      

      说实话,这段代码嘛也没做。它只是把用 gcc -S 命令将下面这段代码编译出来的结果拆吧拆吧之后给打印出来了而已:

      int main()
      {
      }
      

      这可是一个完整可工作的编译器哟 – 某种程度上来说吧。不幸地是,不管用它编译什么程序,得到的都只是一段毫无用处的代码,因些可以说它本身也是同样毫无用处的。但我们总要踏出这最初的一步。

      你可以认为这个系列的文章是我的“意识流”。我做这件事仅仅是因为好玩而已。我并没打算坐下来,好好地完成一个多么赞的设计。我甚至会因为一时兴起而把一段代码给丢掉,或者之后又把它给找回来。

      你在这里将要看到的其实已经是我第二次的代码了:我所做的每一个 SVN 提交都对应于我的一篇文章,不过我会在文章中做很多额外的讲解。有时这些讲解会令我中途改变主意 – 但我不会对代码做太大的修改,除非我认为这个修改能令我的讲解更清晰,或者我一开始完全就想错掉了。就算是这样,我也多半不会做出修改,除了在那边标注一下我会在之后解决。有些时候,我也会把几个我做起来很麻烦,但解释起来又很简单的步骤合而为一。

      我在这里欢迎大家涌跃发言。不过要记住,虽然我会十分留意您的发言,但我已经完成了这个系列中的很大一部分,我不会因为某些意见而完全重写这些文章。不过我会留下一些笔记,并同时记住我之前都写了些什么内容。

      下次我会让这个编译器真正去处理它的输入的,我保证。


      [译者再次抱怨:翻译好麻烦啊。]

      Wang Congming's avatar

      工作日志:从突击成长,到稳定发挥

      我最近常想到“稳定发挥”这个词。

       

      政治课上有一句话,叫“螺旋式上升、波浪式前进”。拿到职场来说,刚工作的同事应该是这个样子。我们面前有很多可能,在哪个方向上突击一下都能形成一个波峰。

       

      不过,有波峰,即有波谷。一名成熟的职业人,不能随便进入波谷状态,更要为岗位和其他人负责。职责愈大,愈不能随便波动,发挥的稳定性愈显得重要。

      随着经验的积累,通过突击式的努力使自己的能力见识在短时间内有大的提高,变得困难起来。更多的,是平缓的、渐进式的增长。这个时候,稳定性和可预见性成为了核心价值。事情到了这样的人手里,你可以放心地忘掉,因为他总能在合适的时机给出最优的答案。

       

      要保持前进的步伐、保障稳定的发挥,心态和习性要跟上。

      平和的情绪、积极的态度、清醒的状态,始终如一。把神志不清 / 烦躁不安…从字典里划去。内部情绪、外部干扰都不再成为影响稳定发挥的因素。

      工作有序,形成适合自己的高效的工作方式与习惯。有再多的工作,都能井井有条。头脑清晰、合理安排,为重要的事情挤出时间,而非乱成一团糟。

       

      在心态与习性的保障之下,稳定地输出工作成果。同时,有目标、有规划、有大局观,能描绘出蓝图,并清楚每一步工作所处的阶段。享受一步步逼近目标,和一切尽在掌控的感觉,激发自己更大的潜能。

      Wang Congming's avatar

      豆瓣数据存疑

      Update:
      感谢 Ratooykjingleptoqwhiplus 等网友的提醒,之前提的两个问题都有了答案。:-)

      1、豆瓣用户量突然飙升,是因为 QQ 空间的“豆瓣读书” app;

      2、Wayback Machine 抓取网站的存档之后,不会立即显示,貌似要一年以上历史。

      另外,我后来看了下,其实豆瓣的用户都有个数字编号,从用户的编号和注册时间就可以得出准确的数据(第一位用户的编号是 1000001,所以用户数等于用户编号减掉 100w,不考虑被封杀和自杀的情况)。

      下面是完整的数据图形:

      可以看到,09 年 8 月开始的那一跳……。最高时,一个月新增了 900 多万,相当于它前 4 年总和的 2.5 倍多。

      不过我们也看到,一年之后,已经开始恢复正常了。这大概是 QQ 空间输血能力的上限了吧。

      最后,我随机测试了一些,QQ 空间给豆瓣输送的用户基本都是无效的,没有任何活动。

      (Update 完)


      今天统计了一下豆瓣的注册用户数。因为豆瓣有在首页显示相关数据的习惯,所以这个统计显得比较简单(假设数据是准确的)。

      翻开 Wayback Machine,可以看到,豆瓣最早在首页显示用户量的记录是 06 年 11 月 2 日

      以下即是我根据这上面的数据绘的图:

      很可惜,Wayback Machine 对豆瓣的最后一次存档是 09 年 6 月 30 日,之后就没有了。不知道是豆瓣的 robots.txt 禁止了抓取(看了一下,似乎没有),还是 Wayback Machine 自身的原因。

      上图的数据看起来中规中矩,没什么奇特。不过,我们看它的总注册用户量,最后一条记录显示,当时豆瓣的用户数刚刚达到 350 万。那是 09 年 6 月末,那么,今天豆瓣标称的用户数是多少?—— 4713 万。

      加上这条,图立马变形。

      05 年到 09 年,四年多,350万;

      然后 2010 年,一年半,4713 万,13 倍。

      虽然我没太关注,但似乎豆瓣近一年来没搞过什么大的营销推广;反倒是因为审查严格,很多用户自杀了帐号。13 倍的数据有点惊讶,不知道是不是我哪搞错了。

      via these people and places