关于软路由的一些配置

家里的软路由之前配置的网络坏掉,重新配置一下,并做一些记录

为什么是软路由?

粗略的算下来,家里需要连接 WIFI 的设备已经有 18 个,所以就让软路由承担 DNS Server + 透明代理 + DHCP Server 的职责,WNDR 4300 专心做 AP。

软路由配置

Intel N3151
Intel 60G SSD
镁光 8G DDR3
Ubuntu 18.04(有一段时间没关注 Ubuntu, 转眼就 18.04 。。。。)
千兆双网卡

架构

联通光猫 <-------> 软路由 <-------> NETGEAR WNDR 4300

涉及组件

DNS: Overture + Dnsmasq (原因是 Clash 自带的 DNS Server 始终起不来,但是又没有任何 Log,就改用 maemual 的方案, 如果你的 Clash DNS Server 能正常工作,可以忽略这部分)
代理: Clash + Dler Cloud

网卡,DNS,DHCP 配置

关于这部分,请参考「x86-软路由透明代理构建方案」, 这三部分已经讲的特别清晰详细

代理

安装 Clash,并将你的 config.yml 存入 ~/.config/clash 目录下,配置 redir-port7892


iptables

通过 iptables 将内网流量跳过转发,并将其他 tcp 流量转发给 clash

1
2
3
4
5
6
7
8
9
10
11
12
sudo iptables -t nat -N CLASH
sudo iptables -t nat -A CLASH -d 0.0.0.0/8 -j RETURN
sudo iptables -t nat -A CLASH -d 10.0.0.0/8 -j RETURN
sudo iptables -t nat -A CLASH -d 127.0.0.0/8 -j RETURN
sudo iptables -t nat -A CLASH -d 169.254.0.0/16 -j RETURN
sudo iptables -t nat -A CLASH -d 172.16.0.0/12 -j RETURN
sudo iptables -t nat -A CLASH -d 192.168.0.0/16 -j RETURN
sudo iptables -t nat -A CLASH -d 224.0.0.0/4 -j RETURN
sudo iptables -t nat -A CLASH -d 240.0.0.0/4 -j RETURN

sudo iptables -t nat -A CLASH -p tcp -j REDIRECT --to-ports 7892
sudo iptables -t nat -A PREROUTING -p tcp -j CLASH

此时应该已经 ok ,家里的设备都可以畅通的使用,不过记得用 sudo netfilter-persistent save 保存下当前的配置

before_unicode

打算写几篇 Blog 把之前学习的 Unicode 的一些东西整理下。

抽象字符表

这是一个大家了解比较少的概念。简单的说,抽象字符表是「所有抽象字符的集合」

[image:101F4772-FD64-4E5E-9A4D-ED3C583650A3-297-0000F245656331E5/6D3A3F9F-1349-4F83-B326-B8B952060BE2.png]

这个字符表可以是封闭的,比如上图的 ASCII,声明有限的 128 个字符,且不允许新字符添加进去。

字符表也可以是开放的,比如 Unicode 字符表,允许不断地添加字符来拓展字符表。

编码字符集

也许你已经被 Unicode, UTF-8, GBKGBK 扰乱了头脑,现在可以先抛开这些概念,了解一下什么是「编码字符集」

在上图中,每个「字符」都对应一个值,这个值叫做「码位值(Code Point Value)」,比如在「ASCII 编码字符集」中,字符「A」的码位值是「十进制数 65」

当我们构建了「抽象字符表的字符」与「码位值」的映射关系时,也就构建了一个编码字符集。

一些常见字符集

  • ASCII:美国信息互换标准编码字符集
  • GB 2312:中华人民共和国国家标准简体中文字符集
  • Big 5:台湾地区使用的中文字符集
  • UCS:通用字符集

其中 GB 2312 和 Big5 都属于 DBCS (两字节字符集),即这个字符集的空间大小是 2 字节。

编码

在我们之前聊的字符集,更多的是逻辑上的映射(比如 A 对应的是 65),但是当我们想要在计算机使用时,我们要解决的一个问题就是如何「存储与传输一个字符」,这就引入了「编码/解码」这个概念

ASCII 的编解码很简单,就是占用一个 Byte 的定长空间,存储「码位值」的二进制数(比如 A 的码位值是 65,二进制数为 01000001)

GB 2312 的编码
[image:481859E9-52E0-41DD-8E51-2FFF4A4136C5-297-0000F64D7515AC9C/ECA8A5DD-AD0B-414D-8E67-5834239BAEF0.png]

  • 为了兼容 ASCII,如果一个字符是 ASCII 字符集中的字符,则采用 ASCII 字符集的编码形式,如字符「A」还是转换为「0x41」进行存储
  • 若是 GB2312 字符集中独有的字符,比如「哦」,对应的码位值为「0xC5B6」,则会存储为两个字节

GB2312 的解码

[image:75118E6C-F31D-481B-A90D-F4DFF0677B53-297-0000F6F5721461D1/E95A7FD6-9490-4319-8E4E-D8536E2B960E.png]

读取一个 Byte,如果值小于 128,在是 ASCII 中的字符。如果大于等于 128,就再读取一个字节,两个字节的值算出「码位值」。

为什么会有这么多字符集,我们又为什么需要 UCS

也许是因为我们的短视?

一开始大家觉得 ASCII 就够用,但是显然不是全世界都在说英语。

于是大家就开始搞自己的字符集。欧洲人搞了 ASCII 扩展字符集来支持拉丁字母,大陆搞了 GB 2312 支持很多汉字。台湾搞了「Big 5」来支持汉字。

不过这样搞完之后,我们遇到了新的问题。

[image:72C80279-B72D-4C2D-8789-BC647AAC4592-297-0000F7CEE660905C/061EE11E-334C-4879-96F0-1EFC2DCF06D3.png]

同样一个「鄙」字,台湾厂商会按照 Big5 的字符集与编码规则存成「0xBBC0」,大陆用户按照 GB2312 读出来会转换成「焕」。

对于「發」字,台湾按照 Big5 的字符集与编码规则存成「0xB560」,大陆这边读出来之后发现 GB2312 里这个值找不到对应的字,于是只好显示成「?」了。

这也是为什么早起很多 PC 用户装台湾的盗版游戏会普遍出现乱码,需要借助「金山快译内码转换器」这种工具的原因,这种工具本质就是帮你把「Big 5 的值转为对应的繁体字,再找到对应的简体字,再转为 GB2312 的值」

Python Decorator 的一些小细节/坑

Decorator 的本质是什么?

decorator 本质就是一个接收对象对象(对,是个对象,而不是大多数人认为的函数),更多的资料可以参照 理解Python的装饰器 | Darkof

1
2
3
4
5
final_func = decorator(wrapped_function) # 与注释部分的实质是一致的。

@decorator
def wrapped_func(*args, **kwargs):
pass

被装饰的函数与之前相比,改变了什么?

行为

这个是最显而易见的,装饰器可以在原函数执行之前或之后添加额外的行为

函数本身的属性

如果你简单的实现了下面的 decorator 会改变什么呢

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
def foo(func):
def wrapped(*args, **kwargs):
print "in decorator"
return func(*args, **kwargs)
return wrapped


class bar(object): # 原谅我用小写的 bar :)
def __init__(self, func):
self.func= func
def __call__(self, *args, **kwargs):
print "in decorator"
return self.func(*args, **kwargs)


@foo
def f1(a):
print a

print f1.__name__
#输出: 'wrapped'

@bar
def f2(a):
print a

print f2.__name__
# AttributeError Traceback (most recent call last)
# <ipython-input-9-dff5600c49e8> in <module>()
# ----> 1 f2.__name__
#
# AttributeError: 'bar' object has no attribute '__name__'

从上面我们看出来,函数的 __name__ 属性也发生了变化,这也是为什么我们推荐装饰器的时候使用 fucntools.wraps

1
2
3
4
5
6
7
8
import functools

def foo(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
print "in decorator"
return func(*args, **kwargs)
return wrapped

wraps 会把原函数的属性赋给新的 wrapped 这个函数(主要会同步的属性为 __name__, __module__, __doc__, __dict__, 当然,你也可以添加你希望同步的属性)

函数的参数

是的,很少有人会注意到被装饰过会,函数接收的参数也会产生变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
inspect.getargspec(f1)
# 输出 ArgSpec(args=[], varargs='args', keywords='kwargs', defaults=None)

inspect.getargspec(f2)
# TypeError Traceback (most recent call last)
# <ipython-input-16-bff760b02fba> in <module>()
# ----> 1 inspect.getargspec(f2)

# /System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/inspect.pyc in getargspec(func)
# 814 func = func.im_func
# 815 if not isfunction(func):
# --> 816 raise TypeError('{!r} is not a Python function'.format(func))
# 817 args, varargs, varkw = getargs(func.func_code)
# 818 return ArgSpec(args, varargs, varkw, func.func_defaults)

# TypeError: <__main__.bar object at 0x108729f50> is not a Python function

f1 接收的函数名从 a 变成了 args 和 kwargs, f2 干脆就拿不到了,这也意味着其实装饰器并不能做到 works anywhere(毕竟有很多装饰器会通过参数来判断这是不是一个 classmethod,然后两个装饰器混用可能会导致其中一个失效)

装饰 classmethod/staticmethod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import functools

def foo(func):
def wrapped(*args, **kwargs):
print "in decorator"
return func(*args, **kwargs)
return wrapped

class Bar(object):
@foo
@classmethod
def duck(cls):
print 'Yooooooooooo!'

Bar.duck()
# ---------------------------------------------------------------------------
# TypeError Traceback (most recent call last)
# <ipython-input-19-ddf54c241cc4> in <module>()
# ----> 1 Bar.duck()
#
# TypeError: unbound method wrapped() must be called with Bar instance as first argument (got nothing instead)

当我们尝试用之前实现的 decorator 来装饰 classmethod 的时候,会遇到 TypeError,原因是 classmethod/staticmethod 本质是一个 Descriptor 而非 function,我们在一开始提到这样一句话:

decorator 本质就是一个接收对象对象

在前面也聊到了,decorator 是个对象,可能是个 class 或是 function, 那么他接收的是什么呢,很多人认为 decorator 接受的是函数,然而严格来说,decorator 接收的是一个对象

当我们知道 classmethod/staticmehtod 是 Descriptor 之后就很容易的知道如何写一个装饰 Descriptor(当然,你需要先了解什么是 Descriptor)

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
class bar(object):  # 原谅我用小写的 bar :)
def __init__(self, func):
self.func= func
def __call__(self, *args, **kwargs):
print "in decorator"
return self.func(*args, **kwargs)

class foo(object):
def __init__(self, func):
self.func = func

def __get__(self, instance, owner):
func = self.func.__get__(instance, owner)
return bar(func)

def __call__(self, *args, **kwargs):
return bar(self.func)(*args, **kwargs)

class Duck(object):
@foo
@staticmethod
def fly():
print 'fly'

@foo
@classmethod
def run(cls):
print 'run'

@foo
def stop(self):
print 'stop'

Duck.fly()
# 输出
# in decorator
# fly

Duck.run()
# 输出
# in decorator
# run

Duck().stop()
# 输出
# in decorator
# stop

mysql offset 为什么这么慢。。。

之前从来没觉得 offset 有什么坑,也没有细想过 mysql 的 offset 的实现原理。

直到这周打算把 4000w+ 的数据热到 redis 中,写了一个脚本, 主要的代码大概如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from redis import Redis
from sqlalchemy import select

import table # Mysql Table Object

redis_cli = Redis(xxxxxxxxxxx)

CUR = 0
MAX = 40000000

while CUR <= MAX:
query = text("""SELECT id FROM example
LIMIT CUR, 1000
ORDER BY id DESC""")
result = table.execute(query).fetchall()
pipe = redis_cli.pipeline() # 使用 pipeline 来减少连接开销
for item in result:
pipe.set(item.id, 'foo')
pipe.execute()
CUR += 1000

开始执行大概下午 6 点左右,然后我就去吃饭逗猫写代码又睡了一觉。

上午 11 点左右来公司发现,才完成了 1000w 左右的数据,内心是崩溃的。。。。。

看了一眼 slow log,一次 Mysql 的查询需要 40s, 然后开始查一些资料找原因,发现 offset/limit 根本无法用到 index 机制,而是读整张表,然后数到需要便宜的位置,所以上面的代码到 1000w 时, mysql 会按照 id 的顺序逐条累加,一直找到第 1000w 的位置(至于为什么不通过 index 来直接找到 id 为 10000000 的数据,原因很简单,id 为 10000000 的数据并不已经代表是第 1000w 条数据,中间有可能会有数据被删除使得 id 非连续)。

找到了原因重写了一把脚本,把 offset 改成 where 就解决了这个问题,然后用了半个小时就跑完了数据- -。

thumbnail

Python 中的「全局变量」的小细节

前几天被同学问了一个问题,为什么自己修改修改了全局变量但是没有生效,例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# File: foo.py
l = 10

def bar():
global l
l = 20

#-----------------------------

# File: main.py
from foo import l, bar
if __name__ == '__main__':
print l
bar()
print l

输出:
10
10

所以我们还是要弄明白什么是全局变量。。。

Python 有全局变量么

有 - -|||。。。。。。
有两种情况是全局变量:

  • 在当前文件中的最外层作用于声明的变量为全局变量
  • 用 global 声明的变量为全局变量

Python 的「全局变量」的作用域是多大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# foo.py
l = 10


def bar():
global b
b = 1

--------------------------

# In ipython
In [1]: import foo

In [2]: 'l' in foo.__dict__
Out[2]: True

In [3]: 'b' in foo.__dict__
Out[3]: False

In [4]: foo.bar()

In [5]: 'b' in foo.__dict__
Out[5]: True

从上面的代码里我们得到以下几个结论

  • 全局变量的作用域被绑定到所在文件之下
  • 即使是先加载再声明全局变量,依然会绑定到该文件之中

为什么我不能跨文件修改全局变量

其实,正确的来说,这个问题问的并没有切中要点,在最初的代码中,我们无法修改 l 的主要原因是 import 的特性导致

The from form does not bind the module name: it goes through the list of identifiers, looks each one of them up in the module found in step (1), and binds the name in the local namespace to the object thus found.

所以当你执行这个语句

1
from foo import l

本质是在当前的 Namespace 中声明了变量 l, 并将 l 指向 foo 这个 module 中的 l 所指向的对象。

当你执行 foo.bar() 的时候,将 foo 中的 l 改为了 20, 但是 main.py 中 l 扔指向 10,所以并没有实现夸文件修改.
同理,在 main.py 中修改 l 也不会影响 foo.l

最后的最后

虽然前面举了例子,有分析了原理,但是其实就像是聊屠龙术的运行原理(更何况「全局变量」连屠龙书都算不上,顶多算是这个图里的 IE

除非是用来定义常量,否则不要在 Python 里用全局变量。

运营商缓存导致的奇葩问题

很久之前遇到的一个问题, 趁还记得, 记录下来, 避免日后忘记.

在公司上线 2014特别项目 之后, 有用户反馈出现了穿好问题. 这个这问题之前从来没有出现过.

通过检查服务器端未出现 Session 冲突, 用户在主站访问正常, 但是在进入 2014 项目之后, 发现信息出现串号现象.

我们的用户信息的 API 请求地址的格式类似为

1
/user_data.json

所有用户都会请求这个地址, 然后由服务器动态生成 Response 并返回, 通过在线上检查发现返回的数据也没有错误.

后来又发现, 遇到的串号用户基本上集中在相同的几个地区. 通过这个线索, 发现问题是出在 ISP服务商 的环节, 有些小的运营商会对你的静态数据做 cache 来加快用户访问速度, 即使你加了 no-cache 的设置, 但是仍然会有些运营商不按照规范强行缓存.

临时的解决方法就是讲请求地址改为

1
/user_data.json?_=[unix_timestamp]

利用 UNIX时间戳 来避开运营商缓存的问题, 最终问题得以解决.

作为总结, 在以后设计 API 的时候尽量要将每个用户的请求地址设计为独一无二的地址, 避免服务器对同一地址动态生成不同的数据, 最终避开这个问题.

PS:
想对运营商说….Fuck!

Amazon S3 的坑

之前就遇到过一次, 今天又有同事遇到, 总之 boto 是个很神奇的项目, 超多 issue 而且跟着文档走 S3 基本不可用, 经常会遇到 400 错误 T_T. 所以单独记录下.

正确的连接 boto:
首先在目录下创建 .boto 文件, 写入 access key 和 secret key:

1
2
3
[Credentials]
aws_access_key_id = YOURID
aws_secret_access_key = YOURKEY

然后连接的时候要注意设置 validate 为 False:

1
2
3
4
import boto.s3
from boto.s3.connection import Location
c = boto.s3.connect_to_region(Location.CNNorth1)
c.get_bucket(BUCKET_NAME, validate=False)

Python与浮点数

在周六参加 TDD Workshop 的时候, 遇到一个问题就是因为涉及到浮点数运算导致单元测试迟迟通不过- -. 回来就在这方面查了查

#简单的例子

1
2
3
4
5
In [1]: 0.1 + 0.2 == 0.3
Out[1]: False

In [4]: round(2.675, 2)
Out[4]: 2.67

简单说这个就是因为浮点数的问题引起的, 也导致我们浮点数的单元测试没有通过.

#关于浮点数
不管是什么数, 在计算机中最终都会被转化为 0 和 1 进行存储, 所以我们需要先弄明白以下几点问题

  • 一个小数如何转化为二进制
  • 浮点数的二进制如何存储

##浮点数的二进制表示
首先我们要了解浮点数二进制表示, 有以下两个原则:

  • 整数部分对 2 取余然后逆序排列
  • 小数部分乘 2 取整数部分, 然后顺序排列

2.25 的二进制表示是?

整数部分的二进制表示为 10, 小数部分我们逐步来算
0.25 * 2 = 0.5 整数部分取 0
0.5 * 2 = 1.0 整数部分取 1
所以 2.25 的二进制表示为 10.01

0.1 的表示是什么?

我们继续按照浮点数的二进制表示来计算
0.1 * 2 = 0.2 整数部分取 0
0.2 * 2 = 0.4 整数部分取 0
0.4 * 2 = 0.8 整数部分取 0
0.8 * 2 = 1.6 整数部分取 1
0.6 * 2 = 1.2 整数部分取 1
0.2 * 2 = 0.4 整数部分取 0

所以你会发现, 0.1 的二进制表示是 0.00011001100110011001100110011……0011
0011作为二进制小数的循环节不断的进行循环.

这就引出了一个问题, 你永远不能存下 0.1 的二进制, 即使你把全世界的硬盘都放在一起, 也存不下 0.1 的二进制小数.

##浮点数的二进制存储
Python 和 C 一样, 采用 IEEE 754 规范来存储浮点数. IEEE 754 对双精度浮点数的存储规范将 64 bit 分为 3 部分.

  • 第 1 bit 位用来存储 符号, 决定这个数是正数还是负数
  • 然后使用 11 bit 来存储指数部分
  • 剩下的 52 bit 用来存储尾数
    Double-precision_floating-point_format

而且可以指出的是, double 能存储的数的个数是有限的, double 能代表的数必然不超过 2^64 个, 那么现实世界上有多少个小数呢? 无限个. 计算机能做的只能是一个接近这个小数的值, 是这个值在一定精度下与逻辑认为的值相等. 换句话说, 每个小数的存储(但是不是所有的), 都会伴有精度的丢失.

#浮点数计算的问题

现在我们可以看一开始提到的例子

0.1 + 0.2 == 0.3


0.1 在 Python 中真正的数字是 0.1000000000000000055511151231257827021181583404541015625
0.2 在 Python 中真正的数字是 0.200000000000000011102230246251565404236316680908203125
0.3 在 Python 中真正的数字是 0.299999999999999988897769753748434595763683319091796875

这就是为什么 0.1 + 0.2 != 0.3 的原因

round(2.675, 2)

1
2
In [4]: round(2.675, 2)
Out[4]: 2.67

为什么 2.675 精确两位小数之后不是 2.68 呢, 因为 2.675 在计算机中真正的数字是 2.67499999999999982236431605997495353221893310546875

坑啊坑.

#我是如何遇到了这个问题
简单地说是因为我理解错了 decimal 这个模块的用法.
我一开始的使用方式是

1
2
In [14]: Decimal(2.675) * Decimal(1.2)
Out[14]: Decimal('3.209999999999999668043315637')

因为没有仔细看库手册导致的错误使用. 正确的用法是:

1
2
In [15]: Decimal('2.675') * Decimal('1.2')
Out[15]: Decimal('3.2100')

将字符串传入 Decimal, 而将数字直接传入, 它的效果是查看该数字在计算机中实际存储的数字.

#decimal是如何实现的计算精准
我粗略的过了一下 decimal 这个库的源代码, 这个根据 General Decimal Arithmetic Specification 来设计, 简单地说就是将传入的字符串记录符号, 记录一个大数(整数和小数部分直接拼接而成), 记录小数点位置, 然后重写这个类的 operation进行实现.

#参考
using-decimal-in-python
PEP327 Decimal Data Type
代码之谜(五)- 浮点数(谁偷了你的精度?)
Double-precision floating-point format
Floating Point Arithmetic: Issues and Limitations
IEEE 754
Decimal Code
General Decimal Arithmetic
Specification

字符串在 Python 2.x 和 3.x 下的适配

RQ 提交了一个 Pull Request 来解决 issue#437 .

最开始的提交 我做了以下的工作:

  • 找到问题所在
  • 写新的单元测试
  • 将相关串做 decode 操作
  • 跑单元测试通过, 提交 Pull Request

然后….就遇到问题了, Travis 跑完发现 Python2.x 下都没问题, 但是 Python3.x 都跑不过, 原因很简单, python3.x 的时候已经不区分 string 和 unicode, 统一采用 unicode, 因此也取消了 decode 方法. 那么我们如何来同时适配 2.x 和 3.x 版本呢. 我想过很多方法, 但是都觉得不够优雅, 后来还是在 RQ 这个库本身里找到了答案.

在 RQ 中, 对字符串的获取都会经过一个 as_text 的函数处理, 该函数位于 compat/__init__.py, 就是为了同时适配 2.x 和 3.x 版本, 函数如下:

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
if not PY2:
# Python 3.x and up
text_type = str
string_types = (str,)

def as_text(v):
if v is None:
return None
elif isinstance(v, bytes):
return v.decode('utf-8')
elif isinstance(v, str):
return v
else:
raise ValueError('Unknown type %r' % type(v))

def decode_redis_hash(h):
return dict((as_text(k), h[k]) for k in h)
else:
# Python 2.x
text_type = unicode
string_types = (str, unicode)

def as_text(v):
if v is None:
return None
return v.decode('utf-8')

def decode_redis_hash(h):
return h

首先通过 six 这个库来判断 Python 版本, 然后根据版本的不同, 声明 as_text 方法的具体实现, 这样在整个库在处理字符串时, 不需要考虑版本差异, 直接调用 as_text 进行处理即可.