ichuan.net

自信打不死的心态活到老

招聘

目前我在币贝科技,一个小创业公司。公司大致情况是:2013 年成立;10 个人左右;产品有 BitBays.com比特湾钱包。现在想找几个志同道合的工程师一起工作,需求如下:

通用要求

  1. 掌握 Git
  2. 接受我们的代码风格
    1. Python: 4 空格缩进,PEP8 标准;字串用单引号;下划线连接命名
    2. JavaScript: 2 空格缩进;字串用单引号;驼峰命名
  3. 接受 shared repo 的协作方式

前端工程师

  • 精通 JavaScript / HTML / CSS
  • 掌握 Bootstrap / jQuery
  • 掌握 HTTP 协议
  • 了解 CoffeeScript / SASS / Jade 等预处理语言
  • 熟悉一些基本的前端安全问题(XSS / CSRF)及防范措施
  • 了解常用前端性能优化措施
  • 负责 BitBays.com 及 App 前端的实现及维护

后端工程师

  • JS 向:掌握 JavaScript(ES6) / Node.js / Express
  • Python 向:掌握 Python / Django
  • 掌握 MongoDB / MySQL,熟悉 Redis
  • 熟悉常用服务器管理命令
  • 熟悉常见 Web 安全问题及防范措施
  • 负责 BitBays.com 网站后端及数据库服务器的运维

安全工程师

  • 掌握常见安全攻击方法及应对措施
  • 熟悉各种社工攻击的防范
  • 能独立迅速进行攻击事件取证并及时防护
  • 跟踪比特币相关领域的安全问题,帮助我们及时应对
  • 负责排查解决产品业务逻辑漏洞及服务器安全

测试工程师

  • 有过全职的测试工作经验
  • 熟练使用各种自动化测试工具,搭建产品的测试体系
  • 负责网站和 App 产品的测试工作

如果有兴趣,欢迎电邮个人简历至 yc@bitbays.com

后会有期

上周回去办了手续,这两天收到了快递过来的离职证明,我算是正式从知道创宇离职了。

在创宇待了 4 年多了,唯一不舍的是那些与我并肩的小伙伴。希望大家各有所成,后会有期。

...
回头有一群朴素的少年
轻轻松松的走远
不知道哪一天才相见
...

简易安全 rsync 远程多备份方案

场景

web 主机 A,代码和数据库都在上面,需要异地备份数据至主机 B;主机 B 上需要保留最近 5 日的备份供追溯。

备份方案

使用 rsync 做远程备份,考虑安全性,使用 rsync over ssh。首先在 A 上生成 ssh 验证用的公私钥:

ssh-keygen -f ~/.ssh/rsync-key

把公钥追加到 B 的 ~/.ssh/authorized_keys 中。这个文件有种写法,在每行公钥前可以设置些参数,比如限定只能从某个地址登陆,只能执行某个命令:

from="1.2.3.4",command="/root/validate-rsync.sh",no-pty,no-port-forwarding ssh-dss AAAA....

参考这个文档/root/validate-rsync.sh 的内容为:

#!/bin/sh
case "$SSH_ORIGINAL_COMMAND" in
  *\&*)
    echo "Rejected" 
    ;;
  *\;*)
    echo "Rejected"
    ;;
  rsync\ --server*)
    $SSH_ORIGINAL_COMMAND
    ;;
  *)
    echo "Rejected"
    ;;
esac

这样就确保了即使 A 有 B 的 ssh 私钥,也只能用于 rsync 用途。

设 A 主机上代码目录在 /data/htdocs/,需备份至 B 的 /backups/web/,则在 A 上执行:

rsync -azh --stats --delete -e 'ssh -i /root/.ssh/rsync-key' /data/htdocs/ root@B:/backups/web/

对于数据库,可以考虑 dump 出来到网站目录,这样备份时同时把数据库备份了(放在 rsync 执行之前):

/usr/bin/mysqldump -uroot -proot webdb > /data/htdocs/db.bakcup

当然需要对 db.bakcup 设权限,不能让浏览器可以访问到。apache 下可以用 .htaccess 文件来做,nginx 下做法如:

location = /db.backup {
  return 404;
}

多备份方案

就像 /var/log/ 下那些日志文件一样,最初把 web 改为 web.1,下一次把 web.1 改为 web.2,同时把 web 改为 web.1,如此类推。我写了一个小脚本来做这个事:

/root/rsync-dir-rotate.sh /backups/web

这样每次执行就会 rotate 备份 /backups/web,最多保留 5 个(默认)。

合起来

把 A 和 B 上执行的脚本分别放到 crontab 中。由于 rsync over ssh 效率不太高(正常网络下可能 1MB/s 左右),需要把两个脚本的执行时间错开,可以一个半夜执行,一个中午执行。

参考

  1. http://archive09.linux.com/feature/113847

极路由极1S的 OpenWRT 固件

UPDATE@2014/10/01: 修复了拨号不成功的问题,同时更新到了 r42701 UPDATE@2014/10/10:

  • 更新到 r42781
  • 自带 pdnsd + dnsmasq + shadowsocks 翻墙配置(需自己修改 /etc/shadowsocks.json)
  • 去掉 transmission,新增 aria2 下载工具。UI 在 /aria2 下,默认密码 zL6lU7A7XvLu,默认下载目录 /mnt/sd/download,日志在 /mnt/sd/aria2.log。在 /root/.aria2/aria2.conf 中修改配置
  • 彩色 console,vim-tiny
  • 固件地址:openwrt-ramips-mt7620a-hiwifi-hc5661-squashfs-sysupgrade-r42781.bin

UPDATE@2014/10/16: 修复了最右侧 lan 口的问题,同时更新到 r42925,地址:squashfs-sysupgrade-r42925.bin

UPDATE@2014/10/19: 加入 ipset,移除 aria2(可以 opkg install 自行安装), 同时更新到 r42949,地址:squashfs-sysupgrade-r42949.bin

买了个极路由1S,刷过了如意云、潘多拉和一些别人定制的 OpenWRT,不是有问题就是不稳定。后来想要自己编译一个官方的,折腾了好久才编译成功,遂分享出来。

在 svn r42689(git: 9b65261d6c48a86)源码基础上编译的极1S固件:openwrt-ramips-mt7620a-hiwifi-hc5661-squashfs-sysupgrade.bin

编译过程主要参考了官方 wiki:http://wiki.openwrt.org/doc/howto/build

官方源码中还不支持极路由 1s,需要打个 patch:ichuan/hiwifi-hc5661 。这个 patch 是抄 Akagi201/hiwifi-hc5661的,只是更新了代码。

直接照这个编译出来的固件可以刷,只是没有无线驱动。这是个坑,参考了论坛后得知需要先 make kernel_menuconfig,在 Machine -> Device tree 中选中 MT7620A eval kit

再编译就没问题了。刷完后可以开启 extroot,这样可以就把系统放到了 SD 卡上,不用受限于 16M 了,可以随便安装软件了。我装了完整版的 vim,bash 等工具,可以装 Python,Mysql-Server,做个网站开发机也不是问题。

个人感觉:开启 extroot 后启动较慢,等个几分钟就好,系统没有挂。

这个固件带了 transmission 和 samba,可以离线下 bt、磁力,之后用 ipad 看片。还带了最新版的 shadowsocks,配合 pdnsd、dnsmasq 可以透明翻墙。

编译出的一些软件在这里: http://ichuan.net/static/upload/openwrt-packages/

enter image description here

enter image description here

enter image description here

django-mysql 中的金钱计算事务处理

末尾有更新:20140604 更新

问题

在类银行系统中,涉及金钱计算的地方,不能使用 float 类型,因为:

# python 中
>>> 0.1 + 0.2 - 0.3
5.551115123125783e-17
>>> 0.1 + 0.2 - 0.3 == 0.3
False

// js 中
> 1.13 * 10000
11299.999999999998
> 1.13 * 10000 == 11300
false

所以 python 中提供了 Decimal 类型,用以人类期望的方式处理此类计算。

django 中的 DecimalField

django 中提供了对应 Decimal 类型的 DecimalField 字段:

class DecimalField(max_digits=None, decimal_places=None[, **options])

max_digits 表示最大位数,decimal_places 表示小数点后位数。假如你的系统里最多存 9999 元人民币,人民币小数点后可以精确到 2 位,需要的 max_digits 就是 6,decimal_places 就是 2。

假如使用了 MySQl backend, MySQL 中也支持 DECIMAL 类型,django 自动处理类型转换。

示例

下面以一个 django app 为例演示。

models 定义:

from django.db import models

class MyModel(models.Model):
    price = models.DecimalField(max_digits=16, decimal_places=2, default=0)

先创建一个 object 试试:

from decimal import Decimal
obj = MyModel.objects.create(price=Decimal('123.45'))

然后更新。假设商品降价,我们需要把 price 减去 9.99:

obj.price -= Decimal('9.99')
obj.save()

来看看具体执行的 SQL 是什么:

from django.db import connection
print connection.queries[-1]

输出为:

UPDATE `hello_mymodel` SET `price` = '113.46' WHERE `id` = 1

这种 update 会有个问题。在并发很高时,会遇到类似多线程的问题,因为加减操作都在客户端,某个线程写入 price 时可能之前拿到的已经被别人更新过了,所以需要原子写入。SQL 表述为:

UPDATE `hello_mymodel` SET `price` = 'price' - '9.99' WHERE `id` = 1

这种方式在 django 中可以用 F() 表达式来实现:

obj.price = F('price') - Decimal('9.99')
obj.save(update_fields=['price'])

另外,在调用 .save() 时,可以用 update_fields 传入需要 update 的字段(如上)。不然 django 可能会把所有字段都放在 SQL 中。

貌似很完美。但假如减去的 Decimal 和字段的精确度不一致呢?

obj.price = F('price') - Decimal('9.999')
obj.save(update_fields=['price'])

price 字段精确到小数点后 2 位,给它减去了 3 位的一个数,会报异常:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  ...
  File "/home/yc/envs/hello/local/lib/python2.7/sitein _warning_check
    warn(w[-1], self.Warning, 3)
Warning: Data truncated for column 'price' at row 1

这是 MySQL 报了一个 warning,django 因此报了异常。更新操作未成功。

解决方法很简单,在入库前,我们手动转换下精确度:

def to_decimal(s, precision=8):
    '''
    to_decimal('1.2345', 2) => Decimal('1.23')
    to_decimal(1.2345, 2) => Decimal('1.23')
    to_decimal(Decimal('1.2345'), 2) => Decimal('1.23')
    '''
    r = pow(10, precision)
    v = s if type(s) is Decimal else Decimal(str(s))
    try:
        return Decimal(int(v * r)) / r
    except:
        return Decimal(s)

obj.price = F('price') - to_decimal('9.999', 2)
obj.save(update_fields=['price'])

OK. 但对于只有 MySQL 在计算时才知道精确度多少的呢?比如乘积:

obj.price = F('price') * Decimal('9.99')
obj.save(update_fields=['price'])

这种还是会报上面的异常,我们没法在 django 层面对结果做类型转换。这种类型怎么办?只有上 raw sql 了。

另外,事务处理在 django 中可以用 transaction.atomic() 来做。事务的意思是代码块中的数据库操作要么都成功执行,没有异常;要么都不执行。实际操作中,用事务+原子操作配合可实现正确的金钱操作逻辑。

raw sql 加上事务,上面的例子改为:

from django.db import transaction, connection
try:
    with transaction.atomic():
        cursor = connection.cursor()
        cursor.execute(
            'UPDATE `hello_mymodel` SET `price` = CAST((`price` * %s) AS DECIMAL(16, 2)) WHERE `id` = %s',
            [Decimal('9.999'), obj.id]
        )
except:
    print 'save failed'

我们在 MySQL 层面用 CAST(%s AS DECIMAL(16, 2)) 来把结果转为和 price 字段同样格式的 Decimal 类型。

这种做法应该很强健了。最后还有一点,可能会有多个操作同时进行,实际应用中,减法操作我们可能希望在事务中检验 price 被更新后要大于 0, 这个最好也能在 MySQL 层面做,把责任推给它。上面的例子改为:

try:
    with transaction.atomic():
        cursor = connection.cursor()
        ret = cursor.execute(
            'UPDATE `hello_mymodel` SET `price` = `price` - CAST(%s AS DECIMAL(16, 2)) WHERE `id` = %s AND `price` >= CAST(%s AS DECIMAL(16, 2))',
            [Decimal('9999.999'), obj.id, Decimal('9999.999')]
        )
        assert ret
except:
    print 'save failed'

ret 是更新的行数,假如正确更新了,ret 就是 1。

总结

金钱运算用 Decimal 类型;django 中字段间操作用 F();F() 配合 Decimal 计算时,结果类型和字段类型完全一致的没问题,不可预测的用 raw sql。

20140604 更新

Decimal 类型在 MySQL 中运算还是有问题,即便结果类型和字段类型完全一致还是可能出问题(见 http://stackoverflow.com/q/23925271/265989 )。

django 中有 select_for_update(), 所以金钱操作时最好不要用自增自减运算,而是用 select_for_update() 的行级锁来避免冲突。

这样最后一个例子可以改为:

try:
    with transaction.atomic():
        locked_obj = MyModel.objects.select_for_update().get(pk=obj.id)
        locked_obj.price -= to_decimal('9999.999', 2)
        assert locked_obj.price >= 0
        locked_obj.save(update_fields=['price'])
except:
    print 'save failed'

参考:

  1. https://code.djangoproject.com/ticket/13666