Jul 11

墙内三大云数据库测试对比 不指定

felix021 @ 2016-7-11 22:26 [IT » ] 评论(3) , 引用(0) , 阅读(4939) | Via 本站原创
  我司CTO和技术总监都是腾讯系的,所以我们一开始就选用腾讯云的服务。他们家的云数据库提供了可视化的运维操作页面和自动备份的能力,降低了DB运维的门槛。同时云数据库还支持高可用架构,对数据的安全性和服务的可靠性更有保障。另外有的云数据库厂商还提供了诸如数据库审计、慢查询分析、数据回档等能力,大大减轻了数据库运维和DBA的工作量。

  其实我们就没有专门的DBA,都是开发自己上去折腾,通过把数据库的可靠性外包给云端,确实极大地降低了我们的工作量,这一点还是挺爽的。但是在具体的使用过程中,发现还是有些地方不够满意,比如MySQL最高版只有5.6,没法用上5.7.8+新增的JSON字段;建立数据库自带的只读从库门槛较高(要最高版本);binlog的备份不方便;数据库授权上的坑(没有FILE和SHUTDOWN,不能grant all on *.*)等。

  记得以前对比过阿里云和UCloud的云主机磁盘IO(那时候腾讯云好像才刚起步呢),这么久过去了,再来比比看,他们的云数据库怎么样。墙内目前就只有这三家还算比较能入眼吧,网易和百度的就先跳过,都没听说谁家在用。另外那个不要脸的X云就算了,期权都能黑下来的公司,估计也活不了多久。

  这次除了对比性能,顺便再看看价格。

  在测试开始之前先打个预防针:以下的测试可能比较粗糙,并不是针对实际业务进行的,所以结果仅供参考;而且实际的业务往往并不是以性能为唯一考量标准,公司的一整套业务需要多项云服务的支撑,最基础的主机、数据库、NoSQL、对象存储、负载均衡等服务这几家都比较完善了,但是在增值、附加服务上各有优劣、亮点,实际选型还是应该根据业务特点仔细考量。


性能


  首先最重要的,是云数据库的读写性能。我在规划实例的配置时,主要考虑下面2点:首先,云数据库要使用SSD硬盘,这样能够保证数据库服务器的IO能够尽量的快。其次,云数据库的内存要尽可能大,这样有尽可能多的数据能够被缓存,提高读写速度。

  因此我选择的数据库配置如下:硬盘300GB SSD,内存8GB左右。由于每个云平台提供的配置都不相同(腾讯云的内存和磁盘比例是限定的,UCloud的内存没有8G等等),我在三个云平台上分别申请了如下配置的云数据库进行性能测试:

点击在新窗口中浏览此图片

  由于实际情况下,云数据库一般是通过云服务器进行访问的,因此我在这3个平台分别申请了配置差不多的云主机,在上面运行性能测试。我申请的云主机的操作系统都是64位CentOS 6.5,具体的配置如下:

点击在新窗口中浏览此图片

  现在比较流行的测试数据库工具是sysbench,为了和实际使用的情况吻合,我对sysbench做了参数上的修改。

  一般来说,读操作要远远高于写操作,并且有很多操作是需要范围查找和排序的,所以我在测试中提高了读操作的比例,特别是提高了范围查找和排序的比重。

  同时,对于写操作,稍微提高了update操作的比例。最终运行的测试中,每一个事务的读操作和写操作的比例是6:1左右。为了模拟项目启动之后的场景,我的sysbench测试集的数据量是总共20张表,每张表200万行数据,开启32个线程,并行向DB发送事务请求,共运行30分钟。下面是我观察到的结果:

点击在新窗口中浏览此图片

  把这个测试结果做成图表是这个结果:

点击在新窗口中浏览此图片

  这个性能测试结果大大出乎我的意料,虽然UCloud的DB界面看上去和阿里云和腾讯云相比比较朴实,产品介绍中也介绍的相对简单,但是,性能上的优势让我吃惊。说实话,自己测试之前,我没想到UCloud有如此大幅领先的性能。从测试结果分析,UCloud比阿里云高了422%,比腾讯云高了297%。而且,这还是在UCloud云数据库的内存不如其它2家大的情况下的结果(UCloud:6G内存,阿里云和腾讯云:8G内存)。
这个数据让我对UCloud的SSD云数据库性能十分动心,要知道,这个是我没有做过任何调优,开箱即用的配置,完全符合我对于云数据库“快速部署,性能满意”的期望。
 
  以下是测试结果的截图,从上到下分别是UCloud的UDB,阿里云RDS和腾讯CDB:

UCloud的UDB↓

点击在新窗口中浏览此图片

阿里云RDS↓

点击在新窗口中浏览此图片

腾讯云CDB↓

点击在新窗口中浏览此图片

价格


  看过了性能之后,我又顺便分析了一下价格,貌似阿里云和UCloud的价格是线性关系的。阿里云根据内存,CPU和磁盘定价,而UCloud根据内存和磁盘定价,CPU免费。腾讯云的配置只有几档,每一档根据内存和磁盘来定价,内存和磁盘的排列是固定的,不是简单的线性关系。因此,我计算出了阿里云和UCloud的价格因素的计算常数,然后以腾讯云的配置为基础,分别推算了3个厂商在同等条件下的价格,可以得到三个厂商的定价趋势图如下:

点击在新窗口中浏览此图片

  其中,腾讯云的价格是最便宜的,只是磁盘和内存的比例是固定的;UCloud的价格比腾讯云略贵,而阿里云比其它两家要高出40%左右的价钱。这可能是因为阿里云主备架构的关系;不过,我发现UCloud的普通版UDB和高可用版UDB几乎是一个价钱,也就是说,如果考虑主备架构的高可用版云数据库,UCloud的高可用版实例的价格比阿里云要低40%。仔细看了下,貌似UCloud的高可用数据库最近在进行促销,所以才会这么划算,也就是说趁活动期间购买的话可以省一大笔钱。可惜我们用的是腾讯云……

总结


  阿里云的文档非常全,而且详细。很多信息都可以通过文档来获取。而且它的MySQL在线管理工具很强大,就是建立DB的时候比较麻烦,还是要去界面上申请建立DB,价格较贵。

  腾讯云虽然没有阿里云的RDS做的那么完善,也还算易用。他们使用PHPMyAdmin来管理数据库,对于熟练这套工具的开发人员可能比较容易上手。价格较便宜。

  UCloud云数据库测出的TPS和QPS性能远高于业内平均水平。而且产品易用性好,价格适中。

  综上所述,性价比最高的云数据库是UCloud的UDB。

  额外再提一句,对于名列在程序员最讨厌两件事之一的“写文档”,在查看3个厂商的DB产品介绍时,感觉阿里云和腾讯云的产品介绍做的很好,里面有各种解决方案的架构,非常贴心。而且云数据库和云主机自建DB的区别也讲得很直观;而UCloud的UDB介绍比较简单,还需要进一步提高。

  以上。本文仅代表个人观点,如有意见和建议,欢迎探讨。
Jul 5

WR1041N v2 安装openwrt 不指定

felix021 @ 2016-7-5 22:31 [IT » 硬件] 评论(0) , 引用(0) , 阅读(2265) | Via 本站原创
(懒得看细节的同学直接拉到末尾下载带有shadowsocks的镜像)


TP-Link很久没给我的路由器更新固件了,而且它运行一段时间以后就需要重启才能保证wifi正常,这对于家里有基于Wifi的智能设备来说是不能忍的,所以只能折腾了,上OpenWRT吧。

记得09年的时候折腾路由器,WL520GU,刷上了DD-WRT,然后再交叉编译弄了个锐捷的客户端,这样就可以把校园网分享出来用。心血来潮到珞珈山水去翻了一下,当年发的帖子竟然还在Linux版置顶……

回归正题,1041n的OpenWRT页面在这里: https://wiki.openwrt.org/toh/tp-link/tl-wr1041n

虽然可以从 这里 下载预编译好的镜像,但是不折腾一下感觉不带感,所以参考官方的教程,自定义镜像。

1. 下载 Image Generator

当年玩WL520gu的时候,好辛苦,先得用openwrt的buildroot脚本创建一个交叉编译环境,那个脚本特别不适合中国国情,用wget慢慢下载国外的源……等得我都快崩溃了,于是拉出它的下载连接直接用迅雷下好放进去再启动。现在可好,直接就有现成的环境直接下载,一分钟就搞定了。

具体参考这个页面:https://wiki.openwrt.org/doc/howto/obtain.firmware.generate

正好1041n是ar71xx架构的,照做就好了。

2. 生成image

可惜1041n只有4M的flash,否则我一定把php和python都打进去,这样还能跑起个shadowsocks server。

最后权衡了下,是这么生成的:

    make image PROFILE=TLWR1041 PACKAGES='luci luci-i18n-base-zh-cn luci-i18n-firewall-zh-cn libev libpolarssl curl'

之所以把 libev 也打进去,主要是想之后可以用shadowsocks-libev,但是编译了一下发现可执行文件竟然 1M+,strip以后也还有两三百KB,还是算了。

如果不用curl的话,可以把libpolarssl和curl去掉,不过好像也没什么其他特别有用的了(或者是pptp?)。curl我是用来请求DnsPod的,这样可以及时把家里的IP刷到某个域名上去。

如果没什么错的话,生成的 image 就是这个了:

    bin/ar71xx/openwrt-15.05.1-ar71xx-generic-tl-wr1041n-v2-squashfs-factory.bin

3. 升级配置

tp不校验固件,所以直接通过路由器管理页面升级就行了。

升级了以后  telnet 192.168.1.1 登上去,用 passwd 修改root密码,然后就可以ssh登录,或者访问luci  http://192.168.1.1

image里面带了中文语言包,登录以后可以在 System -> Language 里面修改成简体中文。

配置PPPoE:打开  网络->接口,点击 WAN 接口的修改,看着配就好了。就那么点东西。

剩下的就是瞎折腾了

4. 创建用户

没什么卵用,弄着玩,这样就可以用另一个用户登录路由器,而且还是通过publickey authentication。

引用

echo 'felix021:*:103:100::/overlay/felix021:/bin/ash' >> /etc/passwd
echo 'felix021:*:16831:0:99999:7:::' >> /etc/shadow
mkdir -p /overlay/felix021/.ssh
cd /overlay/felix021/.ssh
echo $PUBKEY >> authorized_keys
chmod 600 authorized_keys
chown -R felix021:users /overlay/felix021/




==== UPDATE ====


还是花了一个晚上折腾了shadowsocks

1 安装SDK

参考:https://wiki.openwrt.org/doc/howto/obtain.firmware.sdk

下载SDK: https://downloads.openwrt.org/chaos_calmer/15.05.1/ar71xx/generic/OpenWrt-SDK-15.05.1-ar71xx-generic_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-x86_64.tar.bz2

$ tar jxf Openwrt-SDK*

$ mv Openwrt-SDK* sdk

$ cd sdk

2 编译

$ git clone https://github.com/shadowsocks/openwrt-feeds.git package/feeds

$ git clone https://github.com/shadowsocks/shadowsocks-libev.git package/shadowsocks-libev
# 注@20180524,最新版本会出现编译错误,和libsodium貌似不兼容,建议用 v2.4.7

$ make menuconfig #选中 network 下的 shadowsocks-libev-polarssl

$ make package/shadowsocks-libev/compile V=99

编译好的文件位于:./build_dir/target-mips_34kc_uClibc-0.9.33.2/shadowsocks-libev-polarssl/shadowsocks-libev-2.4.7/ipkg-install/usr/bin/

3 腾出空间(回到Image Builder)

对于4M flash的image来说,openwrt给的空间实在太小了,所以只好打起现有package的主意。翻了一下,ipv6似乎占了比较大的空间,考虑到国内确实用不上ipv6,所以就拿它开刀吧。

参考前面 https://wiki.openwrt.org/doc/howto/obtain.firmware.generate 的 Remove useless files from firmware

修改 Makefile,添加 FILES_REMOVE 选项,并在 files_remove 文件中添加:
引用

/etc/modules.d/20-ipv6
/etc/modules.d/42-ip6tables
/etc/modules.d/nf-conntrack6
/etc/modules.d/nf-ipt6
/lib/modules/3.18.23/ip6table_filter.ko
/lib/modules/3.18.23/ip6table_mangle.ko
/lib/modules/3.18.23/ip6table_raw.ko
/lib/modules/3.18.23/ip6_tables.ko
/lib/modules/3.18.23/ip6t_REJECT.ko
/lib/modules/3.18.23/ipv6.ko
/lib/modules/3.18.23/nf_conntrack_ipv6.ko
/lib/modules/3.18.23/nf_defrag_ipv6.ko
/lib/modules/3.18.23/nf_log_ipv6.ko
/lib/modules/3.18.23/nf_reject_ipv6.ko
/usr/lib/libip6tc.so
/usr/lib/libip6tc.so.0
/usr/lib/libip6tc.so.0.1.0
/usr/lib/opkg/info/ip6tables.control
/usr/lib/opkg/info/ip6tables.list
/usr/lib/opkg/info/ip6tables.prerm
/usr/lib/opkg/info/kmod-ip6tables.control
/usr/lib/opkg/info/kmod-ip6tables.list
/usr/lib/opkg/info/kmod-ip6tables.postinst-pkg
/usr/lib/opkg/info/kmod-ip6tables.prerm
/usr/lib/opkg/info/kmod-ipv6.control
/usr/lib/opkg/info/kmod-ipv6.list
/usr/lib/opkg/info/kmod-ipv6.postinst-pkg
/usr/lib/opkg/info/kmod-ipv6.prerm
/usr/lib/opkg/info/kmod-nf-ipt6.control
/usr/lib/opkg/info/kmod-nf-ipt6.list
/usr/lib/opkg/info/kmod-nf-ipt6.postinst-pkg
/usr/lib/opkg/info/kmod-nf-ipt6.prerm
/usr/lib/opkg/info/libip6tc.control
/usr/lib/opkg/info/libip6tc.list
/usr/lib/opkg/info/libip6tc.prerm
/usr/lib/opkg/info/luci-proto-ipv6.control
/usr/lib/opkg/info/luci-proto-ipv6.list
/usr/lib/opkg/info/luci-proto-ipv6.prerm
/usr/sbin/ip6tables
/usr/sbin/ip6tables-restore
/usr/sbin/ip6tables-save
/lib/netifd/dhcpv6.script
/usr/lib/libiptext6.so


4 加入shadowsocks

$ mkdir -p files/etc/init.d files/usr/bin

$ cp /path/to/ss-server /path/to/ss-local files/usr/bin

$ cp /path/to/shadowsocks-libev/openwrt/files/shadowsocks.json files/etc/ss-server

$ cp /path/to/shadowsocks-libev/openwrt/files/shadowsocks.init files/etc/init.d/ss-server #修改配置文件路径为 /etc/ss-server,去掉 -b 0.0.0.0

$ cp /path/to/shadowsocks-libev/openwrt/files/shadowsocks.json files/etc/ss-local

$ cp /path/to/shadowsocks-libev/openwrt/files/shadowsocks.init files/etc/init.d/ss-local #修改配置文件路径为 /etc/ss-local

$ chmod +x files/etc/init.d/*

5 重新生成image

make image PROFILE=TLWR1041 PACKAGES='luci luci-i18n-base-zh-cn luci-i18n-firewall-zh-cn libev libpolarssl curl libpthread -kmod-ip6tables -ip6tables -kmod-ipv6 -kmod-nf-ipt6 -luci-proto-ipv6 -libip6tc' FILES=files/ FILES_REMOVE="files_remove"

升级一下新的rom,然后就可以在路由器上启动shadowsocks的server/client了。

在路由器上启动server,配合surge,就变成一个简易VPN了,效果相当赞。


附1:带有shadowsocks的镜像下载地址为 http://pan.baidu.com/s/1bpl9jK7 密码: 7ugp (貌似不加密码很容易失效)

附2:编译好的shadowsocks
Apr 23

Linux的文件锁机制初探 不指定

felix021 @ 2016-4-23 17:10 [IT » 操作系统] 评论(1) , 引用(0) , 阅读(2553) | Via 本站原创
# Linux下的文件锁

Linux下有两种不同的文件锁机制,一种是通过 `flock` 系统调用锁定整个文件,另一种是通过 `fcntl` 系统调用完成更细粒度的记录锁(锁定文件中的一个区间)

## open 系统调用

Linux下,在最初打开一个文件的时候,最终都是落到 `open` 这个系统调用上。

open系统调用 (@./fs/open.c) 里头做了很多事情,跟文件锁有关的主要是分配了一个 `struct file` 对象,并且给该对象的 `struct inode *inode` 和 `struct file_operations *f_op` 成员赋值。

其中 inode 是对应文件系统分配的(比如 ext4 文件系统的分配函数是 fs/ext4/inode.c 中的 `ext4_iget`),其中也包含了一个 `struct file_operations *` 类型的成员 `i_fop`,即针对该文件系统文件操作的函数指针集合。

以上提到的两次 `struct file_operations` 对象包含 `flock` 和 `lock` 两个函数指针,分别用于上述的两种不同锁机制。

内核在内存中只会给一个文件分配一个 `struct inode` 对象,不同进程打开同一个文件时,都会单独分配一个 `struct file` 对象,且 `file->inode` 指向该文件的 inode 对象。其中 `file->f_op` 实际上就是在 open 系统调用过程中,从 `inode->i_fop` 拷贝过来的。(open -> do_sys_open -> do_filp_open -> path_openat -> do_last -> vfs_open -> do_dentry_open: `f->f_op = fops_get(inode->i_fop)`)

## flock 加锁

内核首先根据第一个参数 fd 找到内核中的 `struct file` 对象,判断 `file->f_op->flock` 指针是否有效,如果有效(说明是文件系统指定了加锁机制,例如 fs/fuse/file.c 中指定了 `fuse_file_flock` 函数,而ext2/3/4都没有指定),就调用该指针指向的函数来执行加锁操作;如果不存在,则执行`flock_lock_file_wait` -> `flock_lock_inode` (@ fs/locks.c)。

如果文件系统没有指定加锁机制,那么 `flock_lock_inode` 会尝试给该文件对应的 inode 加文件锁(加锁机制比较琐碎,我没看得太细,大致是获取 `inode->i_flctx->flc_lock` 这个spinlock,然后增删改查 `inode->i_flctx->flc_flock` 这个链表,但是可以看出全是内存操作)。

如果文件系统指定了加锁机制,那么就要具体考察对应的加锁机制是否是持久化在磁盘上了;不过从加锁这个需求本身去考察,大多数情况下应当是期望能够快速完成,除了特殊情况,可以认为只是在内存中操作,并没有持久化到磁盘。

## fcntl 加锁

与 `flock` 类似,区别在于判断的函数指针是 `file->f_op->lock` ,如果不存在的话,则调用 `posix_lock_file` 尝试对该 inode 加记录锁,也是在 `inode->i_flctx` 上面倒腾,不过用的是 `inode->i_flctx->flc_lock` 这个链表,具体操作更复杂一些,还要考虑记录锁区间的交叉等问题。

## 结论

具体情况取决于文件系统是否使用了指定的加锁机制,如果有,要单独分析。

如果没有,所有的锁都是在内存中维护的,如果断电,锁自动失效。这样也比较符合通常的理解。

另外,针对文件的加锁行为一般都是进程执行的,进程退出(包括意外退出)时,exit 系统调用会调用 `exit_files` 函数清理该进程打开的所有文件。
Mar 11

Hi, HTTPS 不指定

felix021 @ 2016-3-11 01:16 [IT » 网络] 评论(0) , 引用(0) , 阅读(1861) | Via 本站原创
Let's Encrypt项目进入Public Beta已经好久拉,不过因为使用dnspod作为域名的解析服务提供商,该项目官方刚开始的时候并不被支持(总是报错),所以拖了好久,今天才终于搞起来。

用起来还真是超简单:

1. 获取证书

$ git clone https://github.com/letsencrypt/letsencrypt
$ cd letsencrypt
$ ./letsencrypt-auto certonly -w /path/to/www-root felix021.com www.felix021.com

没有什么意外的话,获取到的证书就存在 /etc/letsencrypt/live/felix021.com/ 下面

2. 配置nginx

最简单的就是加上三行:
引用
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/felix021.com/cert.pem;
    ssl_certificate_key /etc/letsencrypt/live/felix021.com/privkey.pem;


但是比较推荐的做法是增加一个80端口redirect到https的配置:
引用
server {
    listen 80;
    server_name felix021.com www.felix021.com;
    return 301 https://www.felix021.com$request_uri;
}


然后执行 service nginx reload ,就生效了。

p.s. 对于我使用的bo-blog博客系统,还有一个坑,就是需要在blog设置里面将URL路径的http换成https,否则使用相对路径引用的css等静态文件资源还是会引用到http去(这是多么奇葩的一个特性啊...)

3. 定期更新

该项目提供的整数有效期只有90天,似乎短了点,但是实际上因为提供了命令行自动更新的方式,并不会造成多大困然,反而可以缩短因为证书泄漏而导致的风险期(貌似也提供了revoke功能,不过我没尝试)。因此官方宣称证书的有效期未来可能会进一步缩短。

想要renew证书,最直接的方式就是前面的certonly命令带上完整参数再跑一次。官方还提供了一个更简单的"letsencrypt renew"命令,会读取上次的配置来重新获取证书。

基于此可以写一个简单的renew脚本,放到root的crontab里,每个月跑一次就好拉。记得renew完要service nginx reload就好啦。
Jan 11

解决ssh延迟 不指定

felix021 @ 2016-1-11 17:29 [杂碎] 评论(0) , 引用(0) , 阅读(2374) | Via 本站原创
不知道为什么这么多年都没遇到啥问题,到了钱厂以后接连遇到3种不同的ssh延迟,觉得有必要记一下了:

1. GSSAPIAuthentication

ssh -vv someserver 可以看到如下类似字样的:
引用
debug1: Next authentication method: gssapi-with-mic
debug1: Unspecified GSS failure.  Minor code may provide more information
Credentials cache file '/tmp/krb5cc_1000' not found


在 ~/.ssh/config(也可以是 /etc/ssh/ssh_config)加上
引用
Host *
  GSSAPIAuthentication no
  GSSAPIKeyExchange no


2. AddressFamily

ssh -vv 在这里卡住的:
引用
debug2: ssh_connect: needpriv 0

加上
引用
Host *
  AddressFamily inet


3. UseDNS

在server端卡住的(忘了log是啥了),在server的 /etc/ssh/sshd_config 加上
引用
UseDNS no



ssh的坑真多,感觉应该抽空整理一个全集,包括之前记录过的 ServerAliveCountMax/ClientAliveCountMax,还有诸如GatewayPorts之类的。

参考:
Fixing SSH login long delay
debug2: ssh_connect: needpriv 0 stuck - Fix.

Dec 25
== 卸载OneDrive ==

C:\windows\syswow64\onedrivesetup.exe /uninstall



== 卸载 Cortana ==



== 禁用Win+S ==

下载文件 (已下载 130 次)


== 从“此电脑”删除垃圾链接(图片、视频等) ==



== 禁用锁屏界面 ==

组策略-》计算机配置-》管理模版-》控制面板-》个性化-》不显示锁屏

p.s. 周年更新以后这个方法失效了,得这么用:

本地安全策略 -> 软件限制策略 -> 新建路径规则, 路径填写“C:\Windows\SystemApps\Microsoft.LockApp_cw5n1h2txyewy”,安全级别“不允许”

== 禁止自动更新 ==

不让我设置禁止自动重启,那只好禁止自动更新了。

services.msc -> windows update -> 禁止

== windows图片查看器 ==

下载文件 (已下载 125 次)


== 图片查看器背景色发黄 ==

显示设置->高级显示设置->显示适配器属性->颜色管理

设备:选择显示器,勾上“使用我对此设备的设置”,“添加(A)...",选择 “sRGB IEC61966-2.1”,"设置为默认配置"

== 垃圾APP ==

有些右键就卸载掉了(但实际上文件还在),这样删比较干净点。

打开Powershell执行:

Get-AppxPackage -name Microsoft.ZuneMusic | remove-appxpackage
Get-AppxPackage -name Microsoft.XboxApp | remove-appxpackage
Get-AppxPackage -name Microsoft.WindowsMaps | remove-appxpackage
Get-AppxPackage -name Microsoft.BingWeather | remove-appxpackage
Get-AppxPackage -name Microsoft.ZuneVideo | remove-appxpackage
Get-AppxPackage -name Microsoft.BingSports | remove-appxpackage
Get-AppxPackage -name Microsoft.3DBuilder | remove-appxpackage


p.s. 打包下载
下载文件 (已下载 128 次)

Dec 17

usbip小记 不指定

felix021 @ 2015-12-17 09:55 [IT » 硬件] 评论(1) , 引用(0) , 阅读(3399) | Via 本站原创
好早以前看到过这个项目,不过当时没有测试成功,昨天晚上看了一下,有点眉目了,先记录一点东西。

1. 项目主页:http://usbip.sourceforge.net/ ,但是这里实际上已经没有再维护了,内核模块已经merge到linux kernel(drivers/usb/usbip)。这里有提供经过ReactOS(这个神奇的项目)签名的windows驱动程序(windows下只有client,没有server),但是不知道四年过去了还能不能用……

2. ubuntu默认安装没有相关模块,需要安装 linux-image-extras ,然后 cd /lib/modules/[KERNEL_VER]/kernel/drivers/usb/usbip && sudo insmod *.ko

3. ubuntu源里的usbip包已经是很老的版本,无法利用新的内核模块,会提示“usbipd requires usbip_common_mod.ko and usbip.ko kernel modules”,实际上userspace tool也已经挪到kernel source里了,位于 linux-source/tools/usb/usbip,编译方法可参考该目录下的README文件,类似于:
引用
./autogen.sh
./configure --with-usbids-dir=/usr/share/misc/
sudo make install

但是我是直接下载的kernel source,没有安装在系统目录,所以在编译的时候会提示 libsrc/usbip_common.h 找不到 linux/usbip.h ,解决方法很简单,把那一行改为 #include "usbip.h" ,然后把 linux-source/include/uapi/linux/usbip.h 复制到 libsrc 目录,再编译就行了(对,就是这么粗暴简单)。

4. update@2015-12-25:在linux下测试成功。windows的driver废弃太久,已经不能支持新版代码的协议了。

p.s. 还有一个收费的商业项目 usb-over-ethernet ,有free trial版本,但是没尝试过。
p.s. 2. 还有一个开源的项目SPICE,有一个 usbredir 模块 http://www.spice-space.org/page/UsbRedir ,不过好像还不完善。
Dec 6

Python: metaclass小记 不指定

felix021 @ 2015-12-6 17:59 [IT » Python] 评论(2) , 引用(0) , 阅读(1932) | Via 本站原创
友情提示:本文不一定适合阅读,如果执意要读,请备好晕车药。

== 题记 ==

"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't."

-- Tim Peters

== 起因 ==

这句话听起来就很诱人,曾经试图去理解它,但是因为没有实际的需求,就因为烧脑子而放弃了。不妨摘录一段Python document里关于metaclass的概述,简直就是绕口令:
引用
Terminology-wise, a metaclass is simply "the class of a class". Any class whose instances are themselves classes, is a metaclass. When we talk about an instance that's not a class, the instance's metaclass is the class of its class: by definition, x's metaclass is x.__class__.__class__. But when we talk about a class C, we often refer to its metaclass when we mean C.__class__ (not C.__class__.__class__, which would be a meta-metaclass; there's not much use for those although we don't rule them out).


昨天心血来潮想写一个带class initializer的class,发现绕不过metaclass了,于是又翻出来看。

== 概述 ==

其实是要理解metaclass的本质,无非是要时刻牢记两点:1. Python中一切皆对象; 2. class也是一个对象,它的class就是metaclass。

举例来说:
>>> class A(object): pass
...
>>> a = A()
>>> print (a, id(a), type(a))               
(<__main__.A object at 0xb183d0>, 11633616, <class '__main__.A'>)

>>> print (A, id(A), type(A))               
(<class '__main__.A'>, 11991040, <type 'type'>)

>>> print (type, id(type), type(type))
(<type 'type'>, 1891232, <type 'type'>)

其中第一个print很好理解:a是一个A的实例,有自己的id(其实就是内存地址)、a的class是A。

第二个print就有点烧脑子了:A是一个class,也有自己的id(因为A也是一个对象,虽然print出来的时候没有明确说),A的class是type。

而第三个就晕乎了:type是一个type,也有自己的id(因为type也是一个对象),type的class是type,也就是它自己。

再回想上面提到的两点:A是一个对象,它的class是metaclass。也就是说 type 是一个metaclass,而A类是type类的一个对象。

唉,本来想好好解释的,没想到还是说成绕口令了。算了,反正我懂了,继续。

== type ==

没有仔细了解type是什么的同学可能会以为type是一个函数:type(X)用于返回X的类对象。

然而并不完全是这样的:在python里,X(args)可能是调用一个函数,也可能是在实例化一个X的对象——而很不幸地,type(X)实际上是介于二者之间的一个调用:虽然type是一个class,但是它的__call__方法是存在的,于是python把它当成一个函数来调用,实际调用到了源码中的type_call;type_call调用了type.__new__试图初始化一个type类的实例,然而type.__new__(位于源码中的type_new函数)发现卧槽居然只有一个参数,于是就返回了这个参数的type(源码是这么写的:"return (PyObject *) Py_TYPE(x);",并没有生成新的对象)。也就是说,本来是个函数调用,里面却是要初始化一个对象,然而最后返回的却不是初始化的对象!尼玛那个特殊情况为毛不放到函数调用里面啊,开发者脑抽了吗!

感到脑抽的同学可以暂时忽略上面那段话,跟本文没太大关系。继续。

实际上type是在builtin模块中定义,指向源码中PyType_Type对象的一个引用:
//位于Python/bltinmodule.c
PyObject * _PyBuiltin_Init(void)
{
    ...
    SETBUILTIN("type",                  &PyType_Type);
    ...
}

这个PyType_Type又是个什么鬼?好吧,继续贴源码
//位于Objects/typeobject.c
PyTypeObject PyType_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "type",                                    /* tp_name */
    ...
    type_init,                                  /* tp_init */
    0,                                          /* tp_alloc */
    type_new,                                  /* tp_new */
    ...
};

注意2点:

0. PyType_Type,也就是python里的type,是在源码中生成的一个对象;这个对象的类型是PyTypeObject,所以它恰好又是一个类,至于你信不信,反正我信了。后面我把它叫做类对象,注意:不是类的对象,而是类本身是一个对象。

1. PyVarObject_HEAD_INIT递归引用了自己(PyType_Type)作为它的type(在源码中,指定某个对象的type为X,就是指定了它在python环境中的class为X),所以前面第三个print中可以看到,type(type) == type(哈哈哈,写绕口令真好玩)

2. 在PyType_Type的定义指定了 tp_init = type_init 和 tp_new = type_new 这两个属性值。这是两个函数,也位于源码中的Object/typeobject.c。

关于第2点,在Python document中关于__new__方法的说明里有详细的介绍,这里简单总结一下:在new一个对象的时候,只会调用这个class的__new__方法,它需要生成一个对象、调用这个对象的__init__方法对它进行初始化,然后返回这个对象。

好吧,我发现不得不把简单总结展开,否则确实说不清楚。

== 实例化 ==

这是一个很有意思的设计:把实例化的流程暴露给码农,意味着码农可以在对象的生成前、生成后返回前两个环节对这个对象进行修改(【甚至】在__new__方法中生成并返回的对象并没有强制要求一定是该class的实例!不过在document里建议,如果要覆盖__new__方法,那么【应当】返回这个class的父类的__new__方法返回的对象)。这里还有一个非常tricky的地方:虽然没有明确指定,但是__new__方法被硬编码为一个staticmethod(有兴趣的话可以去翻type_new函数),它的第一个参数是需要被实例化的class,其余参数则是需要传给__init__的参数。

说起来非常枯燥,还是举一个例子吧,就用document里给出的Singleton:
class Singleton(object):
    def __new__(cls, *args, **kwargs):
        it = cls.__dict__.get("__it__")
        if it is not None:
            return it
        cls.__it__ = it = object.__new__(cls) #注意
        it.__init__(*args, **kwargs)
        return it

    def __init__(self, *args, **kwargs):
        pass

class DbConnection(Singleton):
    def __init__(self, db_config):
        self._connection = AnyHowToConnectBy(db_config)
       
conn = new DbConnection(db_config)


代码并不复杂,但是可能有点玄乎,需要理解一下那个cls参数,前面说了,它是需要被实例化的class,也就是说,最后一行实际执行的是:
DbConnection.__new__(DbConnection, db_config)

而DbConnection的__new__方法直接继承于Singleton, 所以实际调用的是
Singleton.__new__(DbConnection, db_config)

主要注意的地方,在上面这段代码的第六行,Singleton是继承于object(这里特指python中的那个object对象),因此调用了object.__new__(DbConnection)来生成一个对象,生成过程位于C源码中的object_new函数(Objects/typeobject.c),它会将新生成对象的type指定为DbConnection,然后直接返回。

Singleton.__new__在拿到了生成的DbConnection实例以后,将它保存在了DbConnection类的__it__属性中,然后对该实例进行初始化,最后返回。

可以看到,任何继承于Singleton类的子类,只要不覆盖其__new__方法,每个类永远只会被实例化一次。

好了,第2点暂告一段落,接下来回归正题,尼玛我都快忘了要讲的是metaclass啊。

== metaclass ==

还记的上面可以暂时忽略的那段话吗?type(X)是试图实例化type对象,但是因为只有一个参数,所以源码中只是返回了X的类。而type的标准初始化参数应当有三个:class_name, bases, attributes。最前面那个"class A(object): pass",python解释器实际的流程是:

1. 解析这段代码,得知它需要创建一个【类对象】,这个类的名字叫做'A', 它的父类列表(用tuple表示)是 (object,),它的属性用一个dict来表示就是 {} 。

2. 查找用于生成这个类的metaclass。(终于讲到重点了有木有!)
    查找过程比较蛋疼,位于Python/ceval.c : build_class函数,按顺序优先采用以下几个:
    2.1 定义中使用 __metaclass__ 属性指定的(本例:没有)
    2.2 如果有父类,使用第一个父类的 __class__ 属性,也就是父类的metaclass(本例:object的class,也就是type)
    2.2.1 如果第一个父类没有 __class__ 属性,那就用父类的type(这是针对父类没有父类的情况)
    2.3 使用当前Globals()中的 __metaclass__ 指定的(本例:没有,不过2.2里已经找到了)
    2.4 使用PyClass_Type

    注:2.2.1和2.4中提到了没有父类,或者父类没有父类的情形,这就是python中的old-style class,在python2.2之前所有的对象都是这样的,而2.2之后可以继承于object类,就变成了new-style class。这种设计保持了向后兼容。

3. 使用metaclass来创建这个A类。由于A类的class就是metaclass,所以这个过程其实就是实例化metaclass的过程。本例中找到的metaclass是type,所以最终python执行的相当于这一句:
type('A', (object,), {})


再回想一下前面提到的实例化过程,实际上这一句分成两步: 1. 调用type.__new__(type, 'A', (object,), {})生成type的一个实例(也就是A类对象);2. 调用type.__init__(A, 'A', (object,), {}) 对A类对象进行初始化。注意:这里调用的是type.__init__,而不是A.__init__:因为A是type的一个实例。

流程终于解释完啦,不过我觉得还是举个栗子会比较好。就用我看到的那个有点二二的栗子吧:定义一个class,把它的所有属性都改成全大写的。我感觉这个栗子唯一的作用就是用来当栗子了。还好还有这个作用,否则连出生的机会都没有。

== 栗子 ==

直接上代码好了:
def upper_meta(name, bases, attrs):
    new_attrs = {}
    for name, value in attrs.items():
        if not name.startswith('__'):
            new_attrs[name.upper()] = value
        else:
            new_attrs[name] = value
    return type(name, bases, new_attrs)

class Foo(object):
    __metaclass__ = upper_meta
    hello = 'world'

print Foo.__dict__


请不要说“说好的metaclass呢!怎么变成了一个函数!我摔!”,回顾一下最最前面提到的一点:everything is an object in python。upper_meta作为一个函数,它也是一个对象啊。而metaclass也不过就是个对象,并没有本质上的差别——只要它被call的时候能接受name, bases, attrs这三个参数并返回一个类对象就行了。duck-typing的语言用起来就是有这样的一种不可言状的酸爽感。

理解了这一点,这段代码就能理解了,upper_meta返回了一个type类的实例——也就是Foo类,并且可以看到print出来的属性里头只有HELLO而没有hello。

考虑到可能有人不满意,想看使用class来作为metaclass的情形,我就勉为其难换个姿势再举一下这个栗子(真累)。
class upper_meta(type):
    def __new__(cls, name, bases, attrs):
        attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()])
        return type(name, bases, attrs)

写的太长了,换了一个短一点的oneliner,但是效果不变(其实我就是想炫一下,不服来咬我呀)。

这段代码虽然形式上跟前面的upper_meta函数不一样,但是本质是一样的:调用了upper_meta('Foo', (object,), {'hello': 'world'}),生成了一个新的名为Foo的类对象。

理论上,故事讲到这里应该结束了,然而我想说,压轴戏还没上呢。

== 压轴戏 ==

我要把这栗子举得更高更远,也更符合实际开发的需求:继承。
class Bar(Foo):
    hi = 'there'

print Bar.__dict__

这段代码太简单了,但是埋在下面的逻辑却太复杂了。

它的输出并不是{'HI': 'there'}, 而是{'hi': 'there'}。你print Bar.HELLO, Bar.__metaclass__都能得到预期的输出,但是偏偏没有HI,只有hi。

为什么?这真是个烧脑细胞的事情。我已经把所有的逻辑都展现出来了,甚至还做了特别的标记。然而即便如此,想要把这个逻辑理顺,也是一件非常有挑战性的事情,幸好我已经想明白了:苦海无涯,回头是岸。啊呸,应该是——学海无涯苦作舟,不想明白不回头。

我想说“甚至还做了特别标记”这句话的意思是,我还给【甚至】这两个字做了特别标记:在__new__方法中生成并返回的对象并没有强制要求一定是该class的实例!

问题的关键就在这里:前面两个栗子中给出的upper_meta,返回的并不是upper_meta的实例,而是type的实例,而是type的实例,而是type的实例。重说三。

什么意思?再看看代码,最后return的是type(name, bases, attrs),也就是说,Foo类对象并不是upper_meta的实例,而是type的实例(也就是说:虽然指定并被使用的metaclass是upper_meta,但是最终创建出来的Foo类的metaclass是type)。不信你print type(Foo)试试,结果就是type,而不是upper_meta。

为什么这会导致继承于Foo类的Bar类不能由upper_meta来搭建?Bar.__metaclass__不还是upper_meta吗?

这个问题就没有那么困难了,有兴趣的同学可以自己试着分析一下,没兴趣的大概也不会有耐心看到这里吧。

Bar.__metaclass__并不是Bar的原生属性,而是继承于Foo的——所以在print Bar.__dict__的时候看不到__metaclass__。也就是说,在试图创建Bar时,attrs里并没有__metaclass__属性,所以并不会直接采用upper_meta。再回顾一下选择metaclass的顺序就可以发现,实际上在2.2里会选择Foo的metaclass——Foo的metaclass是type,而不是指定的upper_meta。

解决方法很简单:关键就是前面被特别标记了的【应当】返回这个class的父类的__new__方法返回的对象。具体到代码应当是这样:
class upper_meta(type):
    def __new__(cls, name, bases, attrs):
        attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()])
        return super(upper_meta, cls).__new__(cls, name, bases, attrs)

    def __init__(cls, name, bases, attrs):
        print >>sys.stderr, 'in upper_meta.__init__' #FOR TEST ONLY


新增的__init__方法并不是必须的,有兴趣的同学可以跟上面的栗子对比一下,由于前面返回的是type类的实例,调用到的是type.__init__;而这样正确的写法就会调用到upper_meta.__init__。(p.s. super也是烧脑细胞的东西,但用于解决钻石继承的问很有意思,有兴趣的同学可以看看Cooperative methods and "super")

果然很烧脑细胞吧。

关于metaclass的选择,还有另外一个坑:在metaclass 2.3提到了,找不到metaclass的情况下,会使用Globals()中定义的__metaclass__属性指定的元类来创建类,那么为什么下面的代码却没有生效呢?

def __metaclass__(name, bases, attrs):
    attrs = dict([(n if n.startswith('__') else n.upper(), v) for n, v in attrs.items()])
    return type(name, bases, attrs)

class Foo(object):
    hello = 'world'

print Foo.__dict__


== class initializer ==

回到我最初的需求:我需要创建带class initializer的类。为什么会有这样的需求?最常见的metaclass的应用场景是对数据库的封装。举例来说,我希望创建一个Table类,所有表都是继承于这个类,同时我还想给每一个表都设置一个缓存dict(使用主键作为key缓存查询结果)。一个很自然的想法是这样的:

class Table(object):
    _pk_cache = {}
   
    @classmethod
    def cache(cls, obj):
        cls._pk_cache[obj.pkey()] = obj;
       
    @classmethod
    def findByPk(cls, pkey):
        return cls._pk_cache[pkey]

    def __init__(self, pkey, args):
        self._pkey = pkey
        self._args = args
        type(self).cache(self)
       
    def pkey(self):
        return self._pkey

    def __repr__(self):
        return type(self).__name__ + ':' + repr(self._args)
       
class Student(Table):
    pass
   
class Grade(Table):
    pass
   
s1 = Student(1, 's1')
g1 = Grade(1, 'g1')

print Student.findByPk(1)

可惜这是错的。从输出结果就能看出来,返回的是一个Grade对象,而不是预期的Student对象。原因很简单:子类们并不直接拥有_pk_cache ,它们访问的是Table的_pk_cache ,而该dict只被初始化了一次。

当然,我可以在每一个继承于Table的class里新增一句 _pk_cache = {},但是这样的实现太丑了,而且一不注意就会漏掉导致出错。

所以我需要一个class initializer,在class被创建的时候,给它新增一个_pk_cache 。

在搞清楚了metaclass之后,解决方法特别简单:
class TableInitializer(type):
    def __new__(cls, name, bases, attrs):
        attrs['_pk_cache'] = {}
        return super(TableInitializer, cls).__new__(cls, name, bases, attrs)
       
class Table(object):
    __metaclass__ = TableInitializer

    ... #以下不变


完。(终于完结了,我写了一整个下午啊...)
分页: 4/96 第一页 上页 1 2 3 4 5 6 7 8 9 10 下页 最后页 [ 显示模式: 摘要 | 列表 ]