threading vs async

前言

本文旨在探讨单一线程中线程模型和异步模型在完成类似的功能下的区别, 而由于进程管理(如gunicorn/pm2等)和架构上的优化(如读写分离等)属于进程外的范围, 对两都是适用的, 因此不在讨论之列. 请知悉.

多任务

我们常常会遇到一种情况: 我们开发的系统除了它的首要任务外,还常常伴随着有一些周期性的任务需要随之运行. 比如说, 到午夜的时候清空一下临时文件, 或者定时地发心跳包等等. 这些次要任务与主要任务之间相互不能阻塞, 如果它们之间互不相关独立运行的话, 解决起来也很简单:

  1. 跑一个独立的脚本或进程定时执行(不作深入讨论)
  2. 起一个相对独立的线程, 循环睡眠执行
  3. 异步事件定时执行

但有的时候, 情况可能会变得复杂. 比如说,需要在主任务里面控制这些定时任务具体是不是执行,或说动态地去修改执行间隔等. 对于独立的进程,可能就需要加一些机制进行去实现进程通信. 特别如果这些次要任务数量较多的话, 进程的通信和管理将变得很复杂. 这时,线程管理起来相对就方便一些, 通信也相对容易, 然而代价是需要考虑线程安全的问题. 线程安全又是一个不好处理的问题

共享计数器篇

请看以下基于线程模型的实现代码(c实现)

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
#include <stdio.h>
#include <pthread.h>
#include <time.h>

#define THREAD_TOTAL 100
#define NSEC_PER_SEC 1000000000.0

int counter = 0;

void *incr(void *args) {
for (int i = 0; i < 1000000; i++)
counter++;
}

pthread_mutex_t lock;
void *incr_safe(void *args) {
pthread_mutex_lock(&lock);
for (int i = 0; i < 1000000; i++)
counter++;
pthread_mutex_unlock(&lock);
}

void main() {
struct timespec start, end; // clock_t 计时不准,换timespec
clock_gettime(CLOCK_REALTIME, &start);
counter = 0;
pthread_t thread_ids[THREAD_TOTAL];
for (int i = 0; i < THREAD_TOTAL; i++) {
pthread_t thread_id;
#ifdef UNSAFE
pthread_create(&thread_id, NULL, incr, NULL);
#else
pthread_create(&thread_id, NULL, incr_safe, NULL);
#endif
thread_ids[i] = thread_id;
}
for (int i = 0; i < THREAD_TOTAL; i++) {
pthread_join(thread_ids[i], NULL);
}
clock_gettime(CLOCK_REALTIME, &end);
double elapsed = end.tv_sec-start.tv_sec + (end.tv_nsec-start.tv_nsec)/NSEC_PER_SEC;
printf("Counter: %d, Elapsed: %f\n", counter, elapsed);
}

编译得到不安全和安全的可执行版本:

1
2
$ gcc -o bin/counter-unsafe -lpthread -DUNSAFE 01-counter.c
$ gcc -o bin/counter-safe -lpthread 01-counter.c

输出参考:
counter-unsafe

1
Counter: 6296382  Elapsed: 0.323539

counter-safe

1
Counter: 100000000  Elapsed: 0.240659

以上的命令执行多次,大概可以看得出来:

  1. 不加锁的情况下,速度稍慢,数值不准确
  2. 加锁的情况下,速度稍快,数值准确

不安全版本的数值错误,是因为语句counter += 1会被拆成 读值,做加法,存回 三条指令(也可能不止),若操作系统在这几步中间切到别的线程上,必然会导致不符合预期的结果。

虽然加锁版本比不加锁还要快这一点很违反直觉,但这个是事实。事实上, 锁竞争的并没有相象中那么激烈(这个后面还会有例子说明),安全版本之所以会更快,是因为它在获得了锁之后,进行了全部的操作再释放锁给下一个线程。也即它保证了同一时间只有一个线程在执行,避免了很多线程的切换。在线程切换的过程中,操作系统需要先将当前上下文压栈,切回来时再弹栈。单一切换的损耗可能比锁竞争小, 但在数量差别具大的情况下切换带来的开销要比锁竞争明显得多。这个事实,是GIL的理论基础,python社区周期性地会出现 去除GIL 的动议和尝试,至今没人成功, 这里有一篇 python 的 wiki提到相关的问题(注意看Speed那一项)

来看看异步模型做类似的事情是怎么弄的(node实现):

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
PROMISE_TOTAL = 100

let counter = 0;

async function incr() {
for (let i = 0; i < 1000000; i++)
counter++;
}

async function count() {
counter = 0;
const ps = [];
for (let i = 0; i < PROMISE_TOTAL; i++) {
ps.push(incr());
}
await Promise.all(ps);
}

(async () => {
const start = process.hrtime();
await count();
const elapsed = process.hrtime(start);
const seconds = elapsed[0] + elapsed[1]/1000000000;
console.log('Counter: %d, Elapsed: %d', counter, seconds);
})();

输出参考:

1
Counter: 100000000,  Elapsed: 0.203944253

居然比c还快那么一点点。

附上基于 python 的线程和异步实现:

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
import asyncio
import time
import sys
from threading import Thread, Lock

THREAD_TOTAL = 100
counter = 0

def incr():
global counter
for i in range(1000000):
counter += 1

async def incr_async():
global counter
# 若没有这个for,在py3中是可以得到正确结果的
for i in range(1000000):
counter += 1

lock = Lock()
def incr_safe():
global counter, lock
lock.acquire()
for i in range(1000000):
counter += 1
lock.release()

def count(target):
threads = []
for i in range(THREAD_TOTAL):
thread = Thread(target=target)
threads.append(thread)

for thread in threads:
thread.start()

for thread in threads:
thread.join()

async def count_async():
coros = []
for _ in range(THREAD_TOTAL):
coros.append(incr_async())
await asyncio.gather(*coros);

start = time.time()
if len(sys.argv) > 1:
if sys.argv[1] == 'UNSAFE':
count(incr)
elif sys.argv[1] == 'ASYNC':
asyncio.run(count_async())
else:
count(incr_safe)
end = time.time()
print('Counter: %d, Elapsed: %f' % (counter, end-start))

通过执行以下三个命令,我们可以得到安全,安全非和异步的计数结果和耗时:

1
2
3
python 02-counter.py
python 02-counter.py UNSAFE
python 02-counter.py ASYNC

大致分别为:

1
2
3
Counter: 100000000,  Elapsed: 6.041980
Counter: 35023466, Elapsed: 6.367411
Counter: 100000000, Elapsed: 5.56256

ps:

  1. 这里要特别提一下在py3中, GIL的释放策略由原来py2的每N条”指令”释放一次, 变成了每隔一定时间(默认5ms)释放一次. 此时若累加操作完成太快会导致线程看起来似乎是安全的, 因此测试中特别地对累加操作连续执行百万次.
  2. python本身的执行效率不高, 不难推测出大量的时间花费在了循环累加上, 把这部时间去掉的话, 线程模型和异步模型的执行时间比例会大幅上升.

回到正题,以上几段代码。都是在模拟有多个并发请求,对同一共享变量的进行读写的情况。
相对于多线程, 异步模型完成同样的事情, 不但效率更好,写起来也更简练,需要操心的事情更少。对于共享变量,我们可以在任意的地方,毫无顾忌地任意读写

缓存加载篇

我们用一个例子来说明一下异步编程在功能上给我们带来了哪些便利. 假定我们的系统用户量挺大, 有些数据加载要花较长的时间, 很自然我们会使用到缓存. 缓存是一种看起来简单用起来其实挺麻烦的东西. 就比如说冷加载吧, 假定现在系统刚初始化或者缓存刚被清空. 这时候当用户访问到某个数据时, 我们就把它加载到缓存里再返回, 后续用户访问就直接从缓存读出返回. 但我们知道缓存加载需要一定时间, 在开始加载到加载完成的这一段时间内, 有其它用户也在请求呢? 或者更极端点, 这个时候是访问高峰, 缓存过期了. 此时有几百个用户同时在请求呢? 理想处理方式是, 保证整个过程中只有一个人去加载缓存, 其他人等到它加载完了直接使用缓存就行了. 对应到线程模型, 那即是保证只有一个线程在加载, 其它线程都在等待. 这是非常典型的线程同步问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <assert.h>
#include <time.h>

#define THREAD_TOTAL 1000
#define NSEC_PER_SEC 1000000000.0

short is_loading = 0;
int cache = 0;
int counter = 0;
pthread_mutex_t lock;


int load_from_db() {
sleep(1);
counter++;
return 123;
}

void* get_customer_detail_safe(void *args) {
if (cache) {
// good to go
assert(cache == 123);
} else if (is_loading) {
// loop until cache is ready
while (is_loading) {
usleep(100);
}
// good to go
assert(cache == 123);
} else {
printf("try to obtain the lock\n");
// lock
#ifdef BLOCKING
pthread_mutex_lock(&lock);
if (!cache) {
is_loading = 1;
cache = load_from_db();
is_loading = 0;
}
pthread_mutex_unlock(&lock);
#else
if (pthread_mutex_trylock(&lock) == 0) {
is_loading = 1;
cache = load_from_db();
is_loading = 0;
pthread_mutex_unlock(&lock);
} else {
// loop until cache is ready
while (is_loading) {
usleep(100);
}
}
#endif
// good to go
assert(cache == 123);
}
}

void* get_customer_detail_unsafe(void *args) {
if (cache) {
// good to go
} else {
cache = load_from_db();
// good to go
}
}

int main() {
struct timespec start, end;
clock_gettime(CLOCK_REALTIME, &start);
pthread_t thread_ids[THREAD_TOTAL];
for (int i = 0; i < THREAD_TOTAL; i++) {
pthread_t thread_id;
#ifdef UNSAFE
pthread_create(&thread_id, NULL, get_customer_detail_unsafe, NULL);
#else
pthread_create(&thread_id, NULL, get_customer_detail_safe, NULL);
#endif
thread_ids[i] = thread_id;
}
for (int i = 0; i < THREAD_TOTAL; i++) {
pthread_join(thread_ids[i], NULL);
}
clock_gettime(CLOCK_REALTIME, &end);
double elapsed = end.tv_sec-start.tv_sec + (end.tv_nsec-start.tv_nsec)/NSEC_PER_SEC;
printf("Counter: %d , Elapsed: %f\n", counter, elapsed);
}

通过以下命令编译分别得到非安全,安全(使用trylock)和安全(使用阻塞锁)三个版本:

1
2
3
gcc -o bin/cache-unsafe -lpthread -DUNSAFE 04-cache.c
gcc -o bin/cache-safe -lpthread 04-cache.c
gcc -o bin/cache-safe-blocking -lpthread -DBLOCKING 04-cache.c

非安全的参考输出:

1
Counter: 996  , Elapsed: 1.018113

安全的trylock:

1
2
3
4
5
6
try to obtain the lock
try to obtain the lock
try to obtain the lock
try to obtain the lock
try to obtain the lock
Counter: 1 , Elapsed: 1.008818

安全的阻塞锁:

1
2
3
try to obtain the lock
try to obtain the lock
Counter: 1 , Elapsed: 1.008515

其中,安全的两个版本可以看出,锁竞争的情况并不严重。因此,使用trylock和lock不会有明显的差异

回到正题,在非安全的版本中,缓存被加载了996次!这若发现在生产环境,很容易由于某些节点瞬时负载过大,从而引发连锁反应。因此,保证某时耗时很长的操作在同一时间只被加载一次还是很有必要的。
我们来看看异步模型下可以怎么解决:

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
let counter = 0;

function loadFromDb() {
return new Promise(resolve => {
counter++;
setTimeout(() => resolve(123), 1000);
});
}

let loading = null;
let cache = null;
async function getCustomerDetailSafe() {
if (cache) {
// good to go
} else {
loading = loading || loadFromDb().then(() => {
loading = null;
});
cache = await loading;
// good to go
}
}

async function getCustomerDetailUnsafe() {
if (cache) {
// good to go
} else {
cache = await loadFromDb();
// good to go
}
}

(async() => {
const start = process.hrtime();
const requests = [];
for (let i = 0; i < 1000; i++) {
requests.push(getCustomerDetailSafe());
//requests.push(getCustomerDetailUnsafe());
}

await Promise.all(requests);
const elapsed = process.hrtime(start);
const seconds = elapsed[0] + elapsed[1]/1000000000;
console.log('Counter: %d, Elapsed: %d', counter, seconds);
})();

请允许我只贴安全版本的参考结果:

1
Counter: 1,  Elapsed: 1.005952907

经过N次重复实验,结果总是会比多线程的快一点点
注意,这个结果是很不得了的。因为c的执行速度比node快很多。有兴趣的同学可以试一下附带的三个指数级(n每加1,计算量翻一番) fib 实现,测一下它们间的速度差别。
异步模型能做到这一点,是因为所有你编写的代码,都是在一个线程里面执行的。也就不存在线程安全的问题,上面的loading会且只会被赋值一次。
反观线程模型,即使在这个简单的, 只有一个锁的情况, 在实现时也要小心翼翼. 若是情况再复杂些, 再多几个锁还需要操心死锁的问题. 更别提多人协作时情况会变得多么不可控了.
总的来说, 异步模型对于以往一些只能由线程来完成的功能非常地方便好用, 而且由于少了线程切换和锁竞争的开销, 并且速度往往有肉眼可见的提高

ps: 以上实现的是一种单一进程下响应式的缓存策略, 可以在进程内, 进程外, 任意的时间, 任意的方式清空缓存, 所有在缓存清空后访问的用户都能看到最新的数据. 我见过一些系统, 采用独立进程定时更新缓存的策略, 用户在使用的时候往往需要等待缓存更新, 当然也不能随便地通过管理界面去清缓存了. 这些都只是策略问题, 与编程模型无关.

线程的Flask 与 异步aiohttp

以下是参照 Flask 官方的代码:

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
import time
import redis
from flask import Flask
from threading import Lock
import logging
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)

app = Flask(__name__)

lock = Lock()
counter = 1

@app.route('/')
def index():
global counter
lock.acquire()
counter += 1
lock.release()
return str(counter)

@app.route('/slow')
def slow():
time.sleep(1)
return 'ok'

我们先来验证一下,Flask是多线程的,注意上面/slow,每个请求要花1秒。那我们并发的请求100个应该要等100s

1
$ ab -n 100 -c 100 http://localhost:5000/slow

截取部分结果:

1
Time taken for tests:   2.217 seconds

总共花了2.217秒,说明请求之间不会相互阻塞,可以确认Flask当前是运行在多线程的状态下。

aiohttp 也是依照官方文档给的例子稍微调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from aiohttp import web

counter = 0

async def index(request):
global counter
counter += 1
return web.Response(text=str(counter))


app = web.Application()
app.add_routes([web.get('/', index)])

web.run_app(app, port=5000)

好,功能一样,我们来看看输出结果
Flask

1
2
3
4
5
6
7
8
9
10
Concurrency Level:      300
Time taken for tests: 0.687 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 157000 bytes
HTML transferred: 4000 bytes
Requests per second: 1456.00 [#/sec] (mean)
Time per request: 206.044 [ms] (mean)
Time per request: 0.687 [ms] (mean, across all concurrent requests)
Transfer rate: 223.23 [Kbytes/sec] received

aiohttp

1
2
3
4
5
6
7
8
9
10
Concurrency Level:      300
Time taken for tests: 0.269 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 154000 bytes
HTML transferred: 4000 bytes
Requests per second: 3717.90 [#/sec] (mean)
Time per request: 80.691 [ms] (mean)
Time per request: 0.269 [ms] (mean, across all concurrent requests)
Transfer rate: 559.14 [Kbytes/sec] received

也许Flask不是线程模型的最佳实现,但aiohttp也一样不是最优的(比如sanic)。也许Flask还有许多优化的空间,在上生产环境还需要一阵敲打才能发挥出实力。但真这样话,那也只能是缺点吧? 要知道 aiohttp 上生产环境并不需要特别的配置,那么问题来了:

  1. 在单进程的情况下可能配置出一倍效率来吗?
  2. 开发环境与生产环境不一致管理起来麻烦吗?
  3. 多线程程序好调吗?

局限性

一件东西有它的长处, 必然有它的短处, 毕竟异步模型又不是马克思主义. 我们来讲讲它的局限性. 也许有的同学可能已经注意到, 我刚才讲的例子都是属于I/O密集性. 基本上都没有什么运算. 如果你的系统是CPU密集型的话, 异步模型就不是那么适用了. 我们来看一下指数级的斐波那契数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
start calculating
n = 44, calculation took 7.646799437
hello
hello
hello

/* 输出大概如下:
start calculating
hello
hello
hello
hello
hello
n = 44, calculation took: 4.572308
hello
*/

node 版

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
const fib = (n) => {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
};

setInterval(() => {
console.log('hello');
}, 1000);

(async() => {
const n = 44;
const start = process.hrtime();
console.log('start calculating');
fib(n);
const elapsed = process.hrtime(start);
const seconds = elapsed[0] + elapsed[1]/1000000000;
console.log('n = %d, calculation took %d', n, seconds);
})();


/* 输出大概如下:
start calculating
n = 44, calculation took 7.646799437
hello
hello
hello
...
*/

异步的事件循环机实际上相当于一个队列, 在当前代码段(请想像成以await关键字为分隔的分段)未执行完之前, 后面的代码段只能等待. 而线程由操作系统调度, 独立性是有保障的. 所以异步模型并不适合用来做有大量计算的事情. 当然这样的问题也好处理,只要将计算密集的逻辑分离出去(采用线程,进程,网络)把它变成I/O就行了.
异步还有另外一个问题是由于没有多线程,导致在多核机器上只能通过多进程来充分利用CPU。不过,新版的 node 已经开始支持 WorkerThread 了。至于python线程, 呵呵!

结论

通过以上的对比, 可以看出来对于并发任务来讲, 异步模型实现更为方便直观, 效率上也更优秀. 考虑到绝大多数系统都不是CPU密集型的, 掌握异步模型非常重要. 而在使用时也需要注意它的执行特点, 它更适合用于少量计算, 大量I/O, 大量吞吐的场景. 若中间有大量的计算, 将会阻塞整个进程, 需要注意将大量计算分离出去.

相关代码下载

https://github.com/klesh/threading-vs-async

Share Comments

How to make hexo-browsersync working by solving hexo-server rendering incomplete page

What is hexo-server and hexo-browsersync?

  • hexo-server 可以让你实时在本地预览你的hexo博客, 而不必编译整个网站, 对于编写博文和调试hexo本身的插件主题是很有用的
  • hexo-browsersync 只有hexo-server的话,每次改完都得手动刷新一下浏览器, 这很不科学. browsersync 可以自动在你保存文件的时候自动化地刷新浏览器

What is the problem?

实时预览/自动刷新, 这些都很普通. 所以我的期望也很普通, 装上能用就行. 奇怪的是, 有时候写着写着, 它突然间就不灵光了!
更神奇的是, 有时候删除掉一些文本后, 它又突然能 work 了. 追踪问题的过程太琐碎就不细说了, 总的来讲, 这玩意返回的 Content-Length 是错的, 比实际内容短,
导致浏览器将后面(也可能服务器也根本不输出了?)的内容被丢弃. 看了 hexo-server 的 github ,竟然没人提这个 issue? 好吧,若不是用的人少,那即是我太啰嗦写文太长了.

How to solve

当然, 关键还是怎么解这个问题. 我在 hexo/hexo-server 这几个 repos 翻了半天, 竟然都没有发现有 Content-Length 的设定.
有点神奇, 就在万般无奈的情况下, 看到 hexo-server 的 config 里面有一项叫做 compress . 一般服务器对内容压缩都会使用流
式压缩, 即不会使用 Content-Length 预设页面的长度. 经过测试, 确实可以解决代码被截断的问题:

_config.yml 加入:

1
2
3
4
5
6
7
8
9
server:
port: 5000
log: false
ip: 127.0.0.1
compress: true
header: true
serveStatic:
extensions:
- html

Share Comments

How to build wxWidget app bundle in Mac OS X

今天终于把 fu 写完了。于是便愉快地开始了打包工作,我的目标也很简单,像多数 mac app 一样,生成一个 dmg 文件,里面一个 fu.app 和一个 Applications 的链接就完了。却是不曾想,这么简单的事情费了整整一天的时间!

吐糟一下 Apple

本来想加多一个勾选框给用户可以选择是否自动启动,但 Apple Developer 的文档真是令人惊奇地差劲。全部都不提供 code sample ,光把那些长长的方法和字段名列一下就指望开发者能明白。我实现在不想去摆弄 objective c 这门如此罗索的语言,只好求助 google,结果

Share Comments

升级 Ubuntu Kernel 至 4.9 - 启用 BBR 提高梯子效率

前言

BBR 具体的原理可以参照 知乎上的文章 。对于技术工种的人们来讲,梯子这种日常使用频率极高的工具,自然是一分快十分好,果断要升级一下!

选择 kernel

官方的 apt 包提供的内核以稳定为主,必然不会是最新的。要升级到 4.9 的 Kernel,需要手动操作。

首先是挑选需要的版本,官方 upstream kernel 列表:http://kernel.ubuntu.com/~kernel-ppa/mainline/ , 拉到下面就可以看到 4.9 了,点进去。

我的是 64 位系统,对应就要下载这个节点的文件:

1
2
3
4
5
6
Build for amd64 succeeded (see BUILD.LOG.amd64):
linux-headers-4.9.0-040900_4.9.0-040900.201612111631_all.deb
linux-headers-4.9.0-040900-generic_4.9.0-040900.201612111631_amd64.deb
linux-headers-4.9.0-040900-lowlatency_4.9.0-040900.201612111631_amd64.deb
linux-image-4.9.0-040900-generic_4.9.0-040900.201612111631_amd64.deb
linux-image-4.9.0-040900-lowlatency_4.9.0-040900.201612111631_amd64.deb

内核一般由 2 部份组成,linux-headers 是内核的头文件,当你编译的程序需要引用内核时就靠它了; linux-image 开头的文件就是内核编译后的镜像,是实际可运行的部分。genericlowlatency 则是针对不同的使用场景进行调优的版本。

稍微查了一下 lowlatency 的信息。它比较适用实时性要求较高的场景,比如说录音之类的使用场景,所谓有得必说失,代价可能是稳定性和吞吐量(这个就跟 BBR 的原理差不多,牺牲带宽换取速度,当然这里的吞吐量会不会也影响到网络就不知道咯)。我这边由于网络出口本身也就不怎么样,估计就是有差别也很难测得出来。总之,若你不知道选哪个的话,就用 generic 版本的。

安装配置

上面有 3 个 headers 和 2 个 image ,其中 headers 中的 allgenericlowlatency 都需要的依赖。也就是一共要安装 3 个包。接下来,把 3 个文件都 wget 到本地:

1
2
3
4
wget \
http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.9/linux-headers-4.9.0-040900_4.9.0-040900.201612111631_all.deb \
http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.9/linux-headers-4.9.0-040900-generic_4.9.0-040900.201612111631_amd64.deb \
http://kernel.ubuntu.com/~kernel-ppa/mainline/v4.9/linux-image-4.9.0-040900-generic_4.9.0-040900.201612111631_amd64.deb

安装:

1
dpkg -i linux-*

配置使用新内核:

1
2
update-grub
reboot

正常启动,删除旧内核,通过以下的命令列出所有的 headersimage

1
2
dpkg -l | grep linux-headers | awk '{print $2}'
dpkg -l | grep linux-image | awk '{print $2}'

然后把旧的 headers 和 image 一个个删除掉就行了:

1
dpkg purge xxxx

开始配置 BBR

1
2
3
4
echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf
sysctl -p
reboot

测试 BBR 是否是已经开启(若有输出即证明 ok 了)

1
lsmod | grep bbr

个人使用感受

看油管确实流畅许多。今天看 Primitive Technology 1080p 时,只在开头缓冲了一下,后面就很流畅了。

Share Comments

在 Xcode 8 中创建 wxWidgets 的工程

wxWidgets 的文档太老了,相应的指引根本无法使用。经过一番摸索,终于找到了在 Xcode 中创建 wxWidgets 的工程。

前提条件

Homebrew

1
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

wxWidgets

1
$ brew install wxwidgets

开始创建工程

  1. 打开 Xcode -> “Create a new Xcode project”
    Create a new Xcode project

    注意要选择 “Cocoa Application”。我一开始选了 “Command Line Tool” ,虽然也能正常跑出来界面,但生成的只是一个可执行文件,而不是 Application Bundle 。这样一来就无法定制程序的图标和一些其它的行为。

  2. 语言选 Objective C ,其它的勾全部清空。
    Set up project

  1. Build Settings -> Other Linker Flags

    打开 Terminal 输入

    1
    wx-config --libs

    将输出的内容添加到:
    Other Linker Flags

  2. Build Settings -> Other C++ Flags

    打开 Terminal 输入

    1
    wx-config --cxxflags

    将输出的内容添加到 “Ohter C++ Flags”

  3. 把没用的文件删除:
    Delete useless files
    其中 Assets.xcassets 是用来放图标的,要留着。MainMenu.xib 是程序菜单,不需要可以删除掉。

  4. wxWidgets 的 Hello world 测试一下。
    new file
    新建一个 main.cpp ,粘贴:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    // wxWidgets "Hello world" Program
    // For compilers that support precompilation, includes "wx/wx.h".
    #include <wx/wxprec.h>
    #ifndef WX_PRECOMP
    #include <wx/wx.h>
    #endif
    class MyApp: public wxApp
    {
    public:
    virtual bool OnInit();
    };
    class MyFrame: public wxFrame
    {
    public:
    MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size);
    private:
    void OnHello(wxCommandEvent& event);
    void OnExit(wxCommandEvent& event);
    void OnAbout(wxCommandEvent& event);
    wxDECLARE_EVENT_TABLE();
    };
    enum
    {
    ID_Hello = 1
    };
    wxBEGIN_EVENT_TABLE(MyFrame, wxFrame)
    EVT_MENU(ID_Hello, MyFrame::OnHello)
    EVT_MENU(wxID_EXIT, MyFrame::OnExit)
    EVT_MENU(wxID_ABOUT, MyFrame::OnAbout)
    wxEND_EVENT_TABLE()
    wxIMPLEMENT_APP(MyApp);
    bool MyApp::OnInit()
    {
    MyFrame *frame = new MyFrame( "Hello World", wxPoint(50, 50), wxSize(450, 340) );
    frame->Show( true );
    return true;
    }
    MyFrame::MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size)
    : wxFrame(NULL, wxID_ANY, title, pos, size)
    {
    wxMenu *menuFile = new wxMenu;
    menuFile->Append(ID_Hello, "&Hello...\tCtrl-H",
    "Help string shown in status bar for this menu item");
    menuFile->AppendSeparator();
    menuFile->Append(wxID_EXIT);
    wxMenu *menuHelp = new wxMenu;
    menuHelp->Append(wxID_ABOUT);
    wxMenuBar *menuBar = new wxMenuBar;
    menuBar->Append( menuFile, "&File" );
    menuBar->Append( menuHelp, "&Help" );
    SetMenuBar( menuBar );
    CreateStatusBar();
    SetStatusText( "Welcome to wxWidgets!" );
    }
    void MyFrame::OnExit(wxCommandEvent& event)
    {
    Close( true );
    }
    void MyFrame::OnAbout(wxCommandEvent& event)
    {
    wxMessageBox( "This is a wxWidgets' Hello world sample",
    "About Hello World", wxOK | wxICON_INFORMATION );
    }
    void MyFrame::OnHello(wxCommandEvent& event)
    {
    wxLogMessage("Hello world from wxWidgets!");
    }

    7, 点击运行,一切 OK 就可以开始愉快地编程了。

Share Comments

违法停车时如何避免罚款?

如何正确地违法停车
太有才了!

9gag

Share Comments

Ubuntu 下配置基于 Nginx/Openresty 自动化 Let's encrypt 证书申请、更新

简述

趁着年底有点时间,把自己的服务器操作系统升级到了 16.04 。Let’s Encrypt 是早就听说过,只是一直没时间搞。前两天听朋友讲到在 nginx/openresty 下有自动化的 lua 脚本可以实现自动化的申请和证书更新,感觉非常有意思,顺便折腾了一把。

过程

Openresty

Openresty 是在 nginx core 的基础上集成了 LuaJIT 和许多第三方的 nginx 模块。除了 nginx 本身具备的功能外,还可以用来做 web application,web service。利用 lua 可以直接在 Openresty 里面构建动态服务。目前官方只提供 RPM 的预编译包,其它操作系统需要自行编译。 官方的安装说明 简明易懂,直接照猫画虎即可。

接下来要配置 systemd ,让 Openresty 可以自动启动。

1
$ sudo vim /etc/systemd/system/nginx.service

nginx.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=The nginx HTTP and reverse proxy server
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/usr/local/openresty/nginx/logs/nginx.pid
ExecStartPre=/usr/local/openresty/nginx/sbin/nginx -t
ExecStart=/usr/local/openresty/nginx/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

启用:

1
2
$ sudo systemctl enable nginx
$ sudo systemctl start nginx

附上几个调试技巧:

  • 通过 systemctl status nginx 可以看到 nginx 是不是正常启动了。若是失败这里也会输出用的日志信息,可以按左右键对界面进行横向滚动
  • 需要看到更多日志可以使用 journalctl -b _PID=上一步中输出的PID 查看更多的信息。
  • 通过 curl http://localhost 确认 nginx 已经可以正常工作。

LuaRocks

这个 lua 的包管理器,相当于 node.js 的 npm 。安装的方法 点这里 。 上面推荐安装的版本是 2.0.13 。但我装的是当时最新的 2.4.2 ,也许是这个原因导致我后面配置 lua-resty-auto-ssl 的时候踩了坑?

lua-resty-auto-ssl

装完 LuaRocks 之后,首先要配置一下 PATH,在 ~/.profile 中加入:

1
export PATH=/usr/local/openresty/luajit/bin:/usr/local/openresty/bin:/usr/local/openresty/nginx/sbin:$PATH


1
source ~/.profile

然后就可以使用它来安装 lua-resty-auto-ssl 了:

1
2
3
4
5
6
$ sudo luarocks install lua-resty-auto-ssl

# Create /etc/resty-auto-ssl and make sure it's writable by whichever user your
# nginx workers run as (in this example, "www-data").
$ sudo mkdir /etc/resty-auto-ssl
$ sudo chown www-data /etc/resty-auto-ssl

修正 lua-resty-auto-ssl 的脚本权限问题

1
2
$ sudo chmod +x /usr/local/openresty/luajit/share/lua/5.1/resty/auto-ssl/shell/*
$ sudo chmod +x /usr/local/openresty/luajit/share/lua/5.1/resty/auto-ssl/vendor/*

这就是上面我提到的坑,也许是 LuaRocks 的问题,也许不是。如果你找到了原因,欢迎 comment 给我,谢谢哈。

在 Openresty 中配置 lua-resty-auto-ssl

nginx 的 ssl 站点需要先指定一个静态的 ssl_certificate, 否则会报错,因此需要生成一个自签的证书,骗过 nginx 让它顺利启动之后,再由 lua-resty-auto-ssl 返回动态的证书。

1
2
3
4
$ sudo openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \
-subj '/CN=sni-support-required-for-valid-ssl' \
-keyout /etc/ssl/resty-auto-ssl-fallback.key \
-out /etc/ssl/resty-auto-ssl-fallback.crt

我习惯于将全局配置就放到 nginx.conf 里面,每个虚拟主机、网站或说应用独立一个配置文件 example.com.vh.conf 这样。

nginx 的全局配置 /usr/local/openresty/nginx/conf/nginx.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
http {
# 配置 lua-resty-auto-ssl 的全局选项,并启动服务
lua_shared_dict auto_ssl 1m;
resolver 8.8.8.8;

init_by_lua_block {
auto_ssl = (require "resty.auto-ssl").new()

# 这里你可以限定只给那些域名启用 auto ssl
auto_ssl:set("allow_domain", function(domain)
return true
end)

auto_ssl:init()
}

init_worker_by_lua_block {
auto_ssl:init_worker()
}

server {
listen 127.0.0.1:8999;
location / {
content_by_lua_block {
auto_ssl:hook_server()
}
}
}

include /path/to/example.com.vh.conf;
}

example.com.vh.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 80;
server_name example.com;

# Let's Encrypt 需要验证你对域名的控制权,这个就是用来应答的.
location /.well-known/acme-challenge/ {
content_by_lua_block {
auto_ssl:challenge_server()
}
}
}

server {
listen 443 ssl;
server_name example.com;

# lua-resty-auto-ssl 的精华部份,若当前还没有证书或已过期则自动申请,然后返回,证书有效就直接返回
ssl_certificate_by_lua_block {
auto_ssl:ssl_certificate()
}

# 这里配上之前我们生成的自签名证书,否则会报错
ssl_certificate /etc/ssl/resty-auto-ssl-fallback.crt;
ssl_certificate_key /etc/ssl/resty-auto-ssl-fallback.key;
}

我主要是演示这些配置放在哪些地方,参数的意义可以在 lua-resty-auto-ssl 主页上查询。

以上配置完成重启 nginx ,接着就可以看看 auto ssl 是否能正常工作了:

1
2
$ sudo systemctl restart nginx
$ curl https://example.com

若有错误,可以查看 nginx 的 error log, lua-resty-auto-ssl 会将错误信息输出到这上面。

总结

虽然早有 CertBot 这样的自动化工具,但 lua-resty-auto-ssl 显然是更加地方便,在新建站点的时候多加几行即可实现全自动化的 https 。

Share Comments

Surfingkeys is awesome

一直在 Chrome 中使用 vimium 实现 vim 键绑定操作,这个插件只是实现了一些基本的功能,基本是没什么扩展性。所以当我看到小众软件介绍这个 Surfingkeys 时,第一时间就装起来试用。不得不说这个插件做得真是太棒太强大了!

Surfingkeys help

除了 vimium 有的功能它都有之外,还拥有许多实用的键绑定,随时在页面输入 ? 就可以看到帮助,一边用一边学,不用特意去记忆。下面说说我最喜欢的几个功能:

注意所有按键都是区分大小写的,如 E = Shift-e

按 E/R 切换标签

在 vimium 中,最大的问题是它只能针对当前的 tab 操作。换言之,不能在 vim 操作上下文进行 tab 切换。虽然说 Chrome 的快捷键也可达成类似的效果,但按2个键必须比按3个组合键方便。更何况我们还保留了重映射和自定义扩展的权力。没错, Surfingkeys 这货还实现了 map 功能。

勘误,viumium 是可以切换 tab 的,感谢 Qiao Guo 的指正

按 sg/sw 等进行搜索

选中关键词,按 sg 使用 google 搜索, sog 是站内搜索;按 sw 是 bing 搜索, sow 同理是只搜索本站。相当实用!

按 t/T 新建 tab 或者切换 tab

Switch Tab
随着你的 tab 越开越多,想找回之前某个 tab 就不容易了吧。按一下 T ,输入 tab 的关键字,实时过滤,装逼利器啊。

按 [[ 和 ]] 进行翻页

这也是 vimium 所缺失,实又很实用的功能。就是帮你找到页面上的上一页、下一页链接,然后自动地帮你点击。可惜的是,插件本身只认识『英文』。由于当前版本并不支持在 setttings 中指定自定义的正则表达式,要让它认识”上一页、下一页”这些中文,只能通过覆盖它的键绑定来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mapkey('[[', '#1Click on the previous link on current page', function() {
var prevLinks = $('a').regex(/((<<|prev(ious)?|上一页|上一封)+)/i);
if (prevLinks.length) {
clickOn(prevLinks);
} else {
walkPageUrl(-1);
}
});
mapkey(']]', '#1Click on the next link on current page', function() {
var nextLinks = $('a').regex(/((>>|next|下一页|下一封)+)/i);
if (nextLinks.length) {
clickOn(nextLinks);
} else {
walkPageUrl(1);
}
});

按 Q 进行翻译

Translation

这下可以把『划词翻译』这个插件也去掉了,一直觉得划完之后会出来的图标个人感觉挺干扰的,毕竟多数的划选操作并不是为了翻译。现在,你可以选中想要翻译的单词,按 Q 即会弹出一个半透明的窗口,显示其中文解释。为什么要强调这个窗口是半透明的呢?因为我在用划词翻译感觉最不方便的是很多单词是在 图片 里面的。这个时候划词是没毛线作用的,只能是新开一个窗口去有道查一把。而这个半透明窗口真是太棒了,我只要按 Q 激活它,然后输入我想查的单词,中间不用担心这个窗口会把我想看的内容给挡住了。

不过,自带的翻译界面太简陋了。完全不能满足到我的需求,最起码音标、真人发音的什么的要一起显示下吧!好在 Surferingkeys 的扩展性非常强悍,打开它的 Settings ,加入以下代码你就可以得到一个相对完善的翻译界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mapkey('Q', '#8Open omnibar for word translation', function() {
Front.openOmniquery({
url: "https://api.shanbay.com/bdc/search/?word=",
query: Visual.getWordUnderCursor(),
style: "opacity: 0.8;",
parseResult: function(res) {
var res = eval("a=" + res.text);
if (res.data.definition) {
var tmp = [];
for (var reg in res.data.pronunciations) {
tmp.push(`[${reg}] ${res.data.pronunciations[reg]}`);
tmp.push(`<audio src="${res.data[reg+'_audio']}" controls></audio>`);
}
tmp.push(res.data.definition);
return [ `<pre>${tmp.join('\n')}</pre>` ];
}
return [res.data.definition || res.msg];
}
});
});

总结

总体来讲 Surfingkeys 比 vimium 强的不要太多。唯独就是滚动的效果没有 vimium 那么干脆,而且距离实在太短了。你可以通过 settings.scrollStepSize 来调整滚动的幅度,默认值的70,我设的是150。

Share Comments

Send email with attachment by msmtp and mutty in cli/shell/bash

Intro

这几天摆弄了一下 Motion ,一个有动态侦测功能的视频监控软件。当画面变动超过预定的阀值,就会触发拍照和录像功能。这些相应的事件可以添加回调脚本,因此可以利用这个回调来发送邮件以达到警报的效果。当我们举家外游时就可以利用它来监控家里的安全。

邮件发送,既是一个简单,也是一个复杂的课题。我记得以前我还自己搭建过 postfix 的邮件服务器,各种 DNS 设置,域名校验,花了不少时间和精力终于是搞到 Gmail 可以正常接收而不地被标记为 Spam 。时至今日,各种反垃圾邮件机制更加复杂,自己搭建邮件服务器显然是个不划算的买卖,直接使用现有的免费邮箱服务这种多快好省的选择才是王道。

简单来讲,就是在命令行中实现 Foxmail/Outlook 这些邮件客户端的功能,这样我们就可以在任何可以执行 shell 脚本的地方随意的加入邮件发送功能。

以下是在 Arch Linux 下配置邮件发送的方法:

Guide

更新包信息顺便升级软件

1
pacman -Syu

安装配置 msmtp

1
pacman -S msmtp

配置文件在 /etc/msmtprc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
defaults
auth on
tls on
tls_starttls off
#tls_certcheck off
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /tmp/msmtp.log

account MY_SMTP
host smtp.qq.com
port 465
from xxx@qq.com
user xxx@qq.com
password xxx

account default : MY_SMTP

上面这个是 qq 邮箱的配置,注意需要在 qq 邮箱的设置中开启 smtp 服务。其它一些邮箱服务可能要把 tls_certcheck 设置为 off

安装使用 mutty

msmtp 专注于邮件发送,对于邮件本身内容编排控制功能就要简陋许多。我想在报警邮件中增加一张图片以便可以大致知道是不是小偷进屋了。用 mutty 就可以很方便地实现附件添加。

1
pacman -S mutty

使用方法很简单:

1
echo "Movement has been detected on: %d %m %Y. The time of the movement was: %H:%M (Hour:Minute)" | mutt -s "Motion Detection Alert" yyy@qq.com -a %f

  • echo 部份是邮件正文
  • -s 标题
  • -a 附件
  • 收件人邮箱

其它

另外如果你有一些程序利用了系统的 sendmail 功能,你也可以安装 msmtp-mta 这个包,会帮你创建 sendmail 链接。把系统的 sendmail 功能转移到 msmtp 上。很方便。

Share Comments

MacOS 下设置 Docker 代理

Docker 原生的只有 Linux 支持,其它系统都是通过在虚拟机里面跑一个 Linux 环境来构造 Docker 环境。当然其它也包括了 MacOS。
在正常的网络环境下,理论上也不会有太多的使用体验区别。但是很可惜,我们生活在一个网络环境并不正常的维度里!

先简单讲下我的环境:

  • Mac OS 10.12.1 Sierra
  • Docker 1.12.3 官网下载的图形界面版本(dmg格式安装的)
  • brew 1.1.5

Docker 支持 http(s)_proxy 环境变量。 我装的是 libuv 版本的 ss ,只有 sock5 代理。为此需要一个能把 sock5 代理
转化为 http(s)_proxy 的工具:

安装 polipo :

1
brew install polipo

配置 polipo:

1
vim ~/.polipo

内容:

1
2
3
socksParentProxy=localhot:1080
proxyAddress=0.0.0.0
allowedClients=127.0.0.1,::1,10.20.0.0/24

最后2行很重要!docker 实际上是跑在一个虚拟机中,这里需要允许外部的连接进来,最后一个网段便是我虚拟内部的网段。具体的地址可以
通过下面的命令找出来:

1
ifconfig | grep inet

启动 polipo:

1
polipo

看着哪个不是你的局域网网段的基本上就是了。

点击右上角的 Docker 图标,Preferences -> Advanced -> HTTPS PROXY
填上之前在 ifconfig 观察到的那个 ip 地址就行了。

若过程中遇到问题可以利用以下的命令测试 proxy server 是不是正常启动:

1
https_server=192.168.x.x curl https://www.goolge.com

注意要填你局域网的网址,确定是可以通过外部的方式联接。

Share Comments