@Lenciel

Goodbye Palm4fun, and the tech stack review Part I

goodbye

大概两年前,和Bergkamp、194一次计划外的聊天之后,出于保护直肠的目的,本座离开了基友密布的Myriad,作为Palm4fun的联合创始人之一,开始捣鼓着自己创业。

在具体的研发工作方面,我主要是负责服务器端的开发。但因为被冠名CTO,我的工作还包括:

  • 制定研发流程,管理运作研发团队(很幸运,团队都是气味相投的小伙伴并且平均水平很高)
  • 搭建和维护各种IT系统让大家的工作更加轻松
  • 对各种项目进行技术选型、风险评估和报价
  • 培养有palm4fun自己特色的团队文化
  • 甚至是,设计我们的logo和T-Shirt

别误会,并不是和写代码比,我更喜欢做这些事情:我做这些,主要是经过多年的折腾,已经对自己想在什么样的环境里进行软件开发有了自己的体会。所以,我当然愿意花时间和小伙伴们一起,把理想中的工作环境具体到实践。

经过这两年的时间,虽然我们有纯技术团队创业理应遭遇的各种捉襟见肘,但因为整个团队的坚持和付出,在活下来的同时,也完成了一定的技术积累。有一个可喜的现象是,我们自己参与开发孵化的项目,虽然有一些死掉了,但也有一些拿到了几百万的天使投资;而我们作为外包方参与研发的项目,客户都非常认可我们的项目质量和工作方式。很多客户不但和我们确定了长期合作的关系,还积极介绍自己朋友的项目给我们。

新年到来之际,随着我们被Testbird收编,Palm4fun大部分成员即将投入到新公司的各条战线,Palm4fun作为一个组织也就此消亡了。回首这两年,我想说,如果你没有和我一起经历那说了你也不懂我还是不说了……

跨年的时候,茕茕孑立的本座画了张思维导图,主要目的是把过去两年palm4fun的积累整理一下。画出来之后很多朋友希望我分享高清无码图:因为整个图非常大,不太适合在移动设备上看。

stack_all

其实在一开始选择这些的时候,基本上就是从运维支撑和测试部署工具、产品开发和数据管理、基础设施和功能模块以及商业工具四个维度出发,所以就拆成四个部分简单过一遍。特别声明:选择的依据和出发点主要是根据个人喜好,包括自己使用的体验以及眼缘,并没有特别的理由。比如我们用Reviewboard不用Phabricator,完全是因为团队中大多数人已经用习惯了。

Build/Test/Deploy

stack_devops_1

  • 我们没有用Gerrit或者Phabricator的原因是它们功能太多了
  • Ngrok是做微信接口调试时意外发现的好物

Monitoring

stack_devops_2

  • Sentry帮我们在用户找到我们之前找到了很多问题
  • 一开始我们用过Nagios,它的设计也很不错,就是界面太…
  • Zabbix帮我们远离主机因为硬盘满了或者内存不够驾崩的场面

Making fixture with factory boy and faker

我们在Django项目的开发和测试过程中经常需要mock一些数据作为fixture,比较常见的做法是:

  1. 进行一些操作创建测试数据
  2. 使用dumpdata命令导出json格式的数据
  3. 以导出的json为模板构造测试数据用loaddata命令导入到数据库

这样对于大多数场景也算够用了,但是你总会遇到某一天客户走来说:“我想看看那个报表生成出来啥样,能不能创建两千条记录?”

这种时候你大概你第一反应是把之前那个json搞来copy-paste出两千份数据。但很快你就会意识到那是不行的:要构建一个对象,你常常需要先构建它外键的对象,而实际上线的项目它的数据库结构是非常复杂的(数据库结构图的生成见这里),所以构建两千条记录的工作量会远远超过你的想象:

schemaSpy

最近本座试用了factory boyfaker的组合,感觉还比较好用。

Factory Boy

最开始找这类批量生成测试数据的库,主要考察的是Model MommyFactory Boy。看了一下文档感觉两者的差别并不算很大,但是Factory Girl里面的Sienna Miller实在是让人过目不忘所以有什么好犹豫的呢?

Factories的文档上说明了基本的用法,需要注意的主要是如何生成有一定依赖关系的一组测试对象。

数据构造

Factory Boy下的数据构造主要是通过SequenceFuzz两个包来完成。

Sequence故名思议是顺序生成的,比如你要让生成的数据有规律的用户名和电话号码,这样你看到电话13000000001就是是对应user0001

1
2
user = Sequence(lambda n: u'user%04d' % n)
phone = Sequence(lambda n: u'1300000%04d' % n)

Fuzz则是随机的,主要用来构造像学校、专业或者生日这样的数据:

1
2
3
4
5
6
7
8
9
10
11
card_bank = FuzzyChoice([u'中国银行', u'中国招商银行', u'中国工商银行',
                      u'中国建设银行', u'成都银行'])
major = FuzzyChoice([u'地球物理学', u'大气科学', u'海洋科学', u'力学',
                  u'农业工程', u'环境科学', u'心理学', u'统计学',
                  u'系统科学', u'地矿', u'机械', u'仪器仪表',
                  u'能源动力', u'电气信息', u'土建', u'测绘',
                  u'环境与安全', u'化工与制药', u'交通运输', u'海洋工程;',
                  u'航空航天', u'武器', u'工程力学', u'生物工程',
                  u'公安技术', u'材料科学', u'材料', u'水利',
                  u'林业工程', u'轻工纺织食品', u'电子信息科学', u'其他'])
birthday = FuzzyNaiveDateTime(dt.datetime(1992, 1, 1), dt.datetime(1996, 1, 1))

当然,有的字段,比如姓名、地址这类通过顺序或者是随机的从某个设定的集合抽取效果都不够理想,后面会看到怎么用faker来构造它们。

关联对象生成

关联对象的关系有很多种(1:1, 1:n, n:1, n:n),主要都是通过组合运用SubFactoryRelatedFactory两者来生成,但具体的构造方式和先构造谁都要以实际情况而定。比如我们有User和Tester这样的1:1的关系:

1
2
3
4
5
6
class Tester(TimeBaseModel):

    user = models.OneToOneField(User,
                                verbose_name=u'账号',
                                related_name='tester')
    ...

这里在考虑是在TesterFactory里面把User作为SubFactory来生成,还是在UserFactory里面把Tester作为RelatedFactory来生成,主要就是看先后关系。很显然,在这里我们应该先构造系统里的User:

1
2
3
class TestUserFactory(UserFactory):
    ...
    tester = RelatedFactory('apps.tester.factories.TesterFactory', 'user')

这段代码告诉系统,在每个TestUser被构造的时候,用构造出来的user来创建一个1:1的Tester。这个Tester的构造会在usersave之前完成。

然后在Tester的构造过程中你可以直接通过SelfAttribute使用传入的user:

1
2
3
4
5
class TesterFactory(DjangoModelFactory):
    ...
    phone = SelfAttribute('user.phone')
    nick_name = SelfAttribute('user.nick_name')
    creator = SelfAttribute('user')

再比如,我们的TesterPlatformTask都会关联到测试任务TesterTask,它们俩看起来都是ForeinKey

1
2
3
4
5
6
7
class TesterTask(TestingDeviceMixin, TimeBaseModel):
    owner = models.ForeignKey(Tester,
                              verbose_name=u'测试人', )

    platform_task = models.ForeignKey(PlatformTask,
                                      verbose_name=u'任务',
                                      related_name=u'tester_tasks')

但对生成数据而言,我们的目标会是每个Tester在被创建的时候,都给它创建一个以这个TesterownerTesterTask,并且给这个TesterTask创建一个关联的PlatformTask

于是我们的写法就会是,首先在TesterFactory里面使用RelatedFactory来创建TesterTask:

1
2
3
4
class TesterFactory(DjangoModelFactory):
    ...
    tester_task = RelatedFactory('apps.tester.factories.TesterTaskFactory', 'owner')
    ...

然后在TesterTaskFactory里面创建PlatformTask,并且在构造的时候使用传入的owner的参数:

1
2
3
4
5
6
class TesterTaskFactory(DjangoModelFactory):
    ...
    platform_task = SubFactory('apps.platformtask.factories.PlatformTaskFactory',
                               company=SelfAttribute('..owner.user.company'),
                               owner=SelfAttribute('..owner.user'))
    ...

faker

有很多字段,比如姓名、地址这些,纯粹用Fuzz的办法很难做到“贴近真实”。faker就是用来解决这类字段的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from faker import Factory
fake = Factory.create()

fake.name()
# 'Lucy Cechtelar'

fake.address()
# "426 Jordy Lodge
#  Cartwrightshire, SC 88120-6700"

fake.text()
# Sint velit eveniet. Rerum atque repellat voluptatem quia rerum. Numquam excepturi
# beatae sint laudantium consequatur. Magni occaecati itaque sint et sit tempore. Nesciunt
# amet quidem. Iusto deleniti cum autem ad quia aperiam.
# A consectetur quos aliquam. In iste aliquid et aut similique suscipit. Consequatur qui
# quaerat iste minus hic expedita. Consequuntur error magni et laboriosam. Aut aspernatur
# voluptatem sit aliquam. Dolores voluptatum est.
# Aut molestias et maxime. Fugit autem facilis quos vero. Eius quibusdam possimus est.
# Ea quaerat et quisquam. Deleniti sunt quam. Adipisci consequatur id in occaecati.
# Et sint et. Ut ducimus quod nemo ab voluptatum.

这个包最可爱的地方就是支持本地化,比如一个随机的中文姓名可以这么去构造:

1
2
faker = FakerFactory.create('zh_CN')
name = lazy_attribute(lambda x: faker.name())

生成fixture

因为factory boyfaker主要的作用是在测试里面去mock数据,所以要用它们生成fixture不是那么容易。这是因为Django的整个设计上就很注意避免你把测试的数据写到生产的数据库,所以测试都会在一个在Setup阶段被创建,在TearDown阶段被删除的临时数据库里面进行(我看了一下,在开发版本的Django上已经加了一个--keepdb的参数使得你可以保留你用来运行测试的数据库了)。

所以我们可以在一个测试的Setup阶段把数据生成后,直接调用dumpdata命令来把数据dump出去:

1
2
3
4
5
6
7
8
9
def setUp(self):
    company = CompanyFactory.create(id=3)
    TestUserFactory.create(company=company, id=3000)
    TestUserFactory.create_batch(company=company, size=1500)

    #for test_user in test_users:

    create_fixture('tester', 'tester.json')
    create_fixture('account', 'account.json')

注意,这里在创建的时候指定id主要是为了让初始的id比较大,避免和系统里面已经有的id撞车导致你构造的测试数据在loaddata的时候报错或者覆盖现有数据。

其中,create_fixture函数内容如下:

1
2
3
4
5
6
def create_fixture(app_name, filename):
    buf = StringIO()
    management.call_command('dumpdata', app_name, indent=4, stdout=buf)
    buf.seek(0)
    with open(filename, 'w') as f:
        f.write(buf.read().encode('utf-8'))