Recent Posts

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
alswl's avatar

怎么打日志

需要解决的问题:

  • 业务日志打太乱,没有位置和格式约束
  • 什么情况打日志,如何避免滥用,提高日志作用

打日志最佳实践

Dropwizard 列出的打日志原则:

Be human readable.

Be machine parsable.

Be easy for sleepy ops folks to figure out why things are pear-shaped at 3:30AM using standard UNIXy tools like tail and grep.

eg.:

TRACE [2010-04-06 06:42:35,271] com.example.dw.Thing: Contemplating doing a thing.
DEBUG [2010-04-06 06:42:35,274] com.example.dw.Thing: About to do a thing.
INFO  [2010-04-06 06:42:35,274] com.example.dw.Thing: Doing a thing
WARN  [2010-04-06 06:42:35,275] com.example.dw.Thing: Doing a thing
ERROR [2010-04-06 06:42:35,275] com.example.dw.Thing: This may get ugly.
! java.lang.RuntimeException: oh noes!
! at com.example.dw.Thing.run(Thing.java:16)
!

splunk 的最佳实践:

  • 使用清晰的键值对:key1=val1, key2=val2
  • 对开发者易读
  • 全部添加时间戳
  • 使用唯一标记,比如 user_id, transaction_id
  • 使用文本
  • 使用开发者易用格式,比如 JSON
  • 尽可能多加一下数据
  • 标记调用来源,比如方法名,类名
  • 将多行事件拆分

除了一些浅显易懂的原则,还是 dropwizard 的三条原则和解决方案靠谱。 但是两篇文章都没有告诉如果在复杂系统里面记录有用的日志,打日志生命周期是怎样的。

既然找不到,我就就自行想想如何打有意义的日志。

日志不怕多,而是怕繁杂难搜索,产出的数据无意义难追踪问题。所以最关键是找到一个合理通用的方式组织起来即可:

  • 按照模块名 com.duitang.service.module.aaa 打日志,比如 com.duitang.service.module.aaa.log
  • 一个模块一个日志,模块复杂之后,可以拆分,com.duitang.service.module.aaa.core.log / com.duitang.service.module.aaa.query.log,其实在这个时侯,这个模块本身由于复杂性也会面临拆分。
  • 不分离 error / info 日志,放到一个文件,通过 grep 或者日志工具分离
  • 日志文件一定要 rotate,磁盘存储固定时间 N 天,远程文件存储固定时间 M 天,TTL 删除。

补充一个日志常见使用场景:

  • 外部资源调用
  • 状态变化
  • 系统入口和出口
  • 业务异常
  • 非预期执行

参考文档:


原文链接: https://blog.alswl.com/2015/10/how-to-log/
欢迎关注我的微信公众号:窥豹

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.

alswl's avatar

Redis 集群扩容

几乎每一个网站都需要用户登录状态系统,其中核心是存储 Session 的用户登录状态存储系统。 主流的实现之一是使用 Redis 存储用户登录信息,Redis 特点是功能简单、无依赖、 存储结构丰富、有持久化功能。 我大堆糖的 Session 存储系统也正是基于 Redis。

可是 Redis 也存在一些问题,比如 Redis 自身没有 Sharding 功能,Replication 也是在逐步完善完善过程中 (2.4 支持 Replication,2.8 加入 Replication partial resynchronization 功能)。 纵观当下流行的 DB 系统,哪个不是自带这两个特性,这两个分布式特性应该成为新出产的 DB 系统的标配。 而且作者还经常发布延期,放烟雾弹,不知道 Redis 自带 Sharding 特性要等到何年马月。

随着业务规模的扩大,单台 Redis 实例不能满足需求。 考虑到 Redis 也是久经考验的战士,替换掉他成本比较高,那就对 Redis 进行扩容。

扩容的基本要求是:

  • 扩大系统容量,成为分布式系统,未来有横向扩展
  • 业务不中断
  • 保证原始数据的可用性

Google 了一下,有两个项目可以参考: https://github.com/idning/redis-mgr 和豌豆荚的 Codis

研究了这两项目的代码之后,发现前者存在几个问题: 需要停机进行操作。 后者提供了完整一套解决方案,Server/Proxy/Config Manage,对我这次迁移来说,太重了, 而且项目比较新,风险高,只能用来参考实现方法。

最后我决定参考 redis-mgr 的方案,然后使用两种方式同步数据: 系统运行中打上 patch 完成数据的动态迁移;后台跑迁移数据脚本。

方案的关键词

dump / restore / pttl

核心的操作流程是: 使用 dump 命令导出数据,restore 命令恢复数据,pttl 命令获取设置 TTL。

Presharding

Redis 官方没有 sharding 方案,但提供一种策略 presharding。 Redis 作者写了一篇Redis Presharding。核心是: 提前做 2^n 个实例,避免扩容时候数据迁移,一般使用 2^n 个实例, 目的是为了能够自然地乘以 2 进行拆分。 这些实例可以分开放,也可以放在同一台机器上面。

我这次操作,将 OLD_CLUSTER 的一个实例拆分为了 NEW_CLUSTER 的 32 个实例, 跑在 4 台服务器上面。

Twemproxy

Redis 的 经典款 Proxy,用来实现对多个 Redis 实例 sharding, Twitter 出品,链接

一致性 Hash

传统的 Hash 方法是 hash(key) = value % nodes.length, 当加入、减少 Hash 节点时候,会导致 hash(key) 的全部失效。 典型的一致性 Hash 算法,Ketama 通过环状 node 分布,解决了这个问题。

Twemproxy 还提供 modula 和 random 两种分布式方案。

db 大小预估

Redis 只能查看整个实例内存尺寸。不能查看单个 db。 使用 ramdomkey 做抽样检查,取 1% key 抽样,估计单个 key 大小, 然后做 benchmark 估算操作性能,估算操作时间。

动态迁移数据

为了在迁移过程中保证服务可用,需要将数据兼容 Redis 集群 OLD_CLUSTER / NEW_CLUSTER, 业务代码必须同时支持访问两个集群,做法也很简单

  • 访问 NEW_CLUSTER
  • 有数据则继续操作,无数据则访问 OLD_CLUSTER,获取数据 DATA
  • 将数据和 TTL 保存到 NEW_CLUSTER

将这个逻辑封装成一个 client,替换掉原来 Redis Client 即可。

注意,这个点可能产生幻读,读取 key 和写入 key 有个时间差, 但是我处理的 session 是 immutable 数据,不会出现问题。 而如果将 Redis 用作 Persistence,就要评估对业务的影响了。

后台迁移数据

依赖用户访问来进行迁移,效率太低了,这个迁移时间和最长 TTL 时间相当, 需要主动将这个数据从 OLD_CLUSTER 迁移到 NEW_CLUSTER

方案也很简单,使用 randomkey 获取一批数据,然后按动态迁移数据方法进行迁移。

pipeline

如果在业务逻辑中使用了 PIPELINE,就会遇到问题,需要改写掉业务方案, 待迁移完成之后,再进行恢复。

multikey

mget / mset 等多键操作方法需要注意拆解 key,然后一一 dump / restore / ttl

正式操作

线上操作的数据:

# 0.483 g db0
12:05:19,444 - __main__ - INFO - v, dumps keys 1014/1375371/..., time: 974.183676004
# 4.552 g db1
17:38:36,422 - __main__ - INFO - v, dumps keys 1392/7711834/..., time: 3076.41647506

附上 Migration Script:https://blog.alswl.com/2015/07/redis-migration/ Redis Cluster Migration


原文链接: https://blog.alswl.com/2015/07/redis-migration/
欢迎关注我的微信公众号:窥豹

3a1ff193cee606bd1e2ea554a16353ee
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