创业公司技术进化之路

Sat 04 August 2018

TL;DR

4 年前我有幸加入到现在这家公司,成为了公司早期员工之一,后来在开发上慢慢的得到了一些主动权,然后我就开始凭借着自己的摸索慢慢的完善了一些技术层面相关的东西。 这篇文章主要分享一下这其中的过程,前面的一些文章也有涉及但不全面,这里进行一个总结。其实这篇文章也可以叫作《我在创业公司这 4 年》。

面向招聘的技术选型

开始公司的业务主要是车载物联网硬件,涉及大量 TCP 连接建立和数据传输,所以此时的技术选型主要是 Tornado,主要用于以下两个方面:

  • TCP Server 处理硬件 TCP 连接
  • Web 框架,实现客户端 API

中间也尝试使用 Flask 实现管理后台,但当时的我对 Tornado 有非常执着的爱,所以在后面的项目中技术选型依然采用 Tornado 作为主要框架。 所以当时的技术选型是:

  • Tornado 作为 TCP Server 和客户端 API Web 框架
  • 采用 Flask 作为后台管理的框架
  • 前端采用 Bootstrap 3 + jQuery

后面公司转型做互联网车险 SaaS 平台也一直沿用这些选型,直到后来业务开始增长需要招人的时候才发现市面上熟悉 Tornado 的人员比较难招, 这时候又面临公司业务拆分,同时当下前后端分离也比较火,后端模板渲染的情况下需要后端了解一些基本的前端知识,而且这种情况下同前端工程师并不太好协同, 所以这时候新的产品线开始采用如下技术选型

  • Django 作为 Web 框架
  • 前端使用 React

选择 React 完全是是出于对 React 社区和 JSX 的喜爱,但是做完一个项目之后发现了一个相同的问题,就是招人非常困难,特别对于小公司来说就更难了。 所以后续就把一些新项目改为采用 Vue,招人也从 React 调整为 Vue。所以就目前来讲最终采用的技术选型就是:

  • 后端使用 Django 作为 Web 框架
  • 前端使用 Vue 作为开发框架

从这些就能看出来小公司在技术选型时就不能完全靠喜好,需要结合市面上的人才占比来选择合适的技术。

采用前后端分离架构之后好处是便是对后端的前端相关知识不再是硬性要求,前后端开发可以并行进行,同时前端代码的质量也比较好把控。 我们就出现很多后端工程师连 Bootstrap 都用不好的情况。

但是后端框架使用 Django 好处就是对于人事来说可以更好的筛选简历,因为人事看来简历上出现 Tornado 关键字的实在是太少了, 所以我们当前年度的 3、4 月份招聘旺季算是基本完成招聘任务的。同时也对 Django 有一个全新的认识,纠正了我很多偏见也学到了很多东西。

基于 Git 的内部包管理

由于后期的业务转型的时候项目拆分比较细,所以从这时候起就开始有意识的提取一些公共逻辑放在单独的包里, 早期这个包是通过拷贝的方式放在不同的项目里,这样更新就比较麻烦,后续了解到 pip 是可以通过 git 来安装包的, 所以后来就将这个包放在单独的仓库,每个项目通过在 requirements.txt 增加类似下面的内容来引用:

git+ssh://git@github.com/Owner/foobar@v2.14

这样有好处也有一些坏处,好处就是更新迭代方便,坏处就是增加部署成本。

部署

Fabric

早期项目只有一个所以部署基本是手动,后期项目开始变得多起来,手动部署就变得麻烦起来,这时候开始使用 Fabric 写一些部署脚本。 后来就写了一套通用的规则,然后暴漏出一些配置接口,配置诸如仓库 URL,Supervisor 启动项等等,然后将之放在通用库里, 同时编写一套 Makefile 的规则放在通用库里,我们的通用库取名是 allspark

这样直接在项目中放一个 Makefile 添加类似下面的内容:

ALLSPARK_PTH=$(shell python -c 'import os; import allspark;print(os.path.dirname(allspark.__file__))')
include $(ALLSPARK_PTH)/mkrules.mk

这样就可以通过 make 进行一键部署,这套机制用了很长时间,目前还有部分维护较少的项目还再使用。

Ansible

后来开始立项 Django 的项目,由于 fabric 相关的封装都在 Tornado 的通用库里,再继续使用就比较麻烦,这时候我就开始寻求新的解决方案, 所以我又开始研究 Ansible,经过简单的了解和尝试我发现只要定义几个简单 role 就可以满足需求,所以我新建了一个仓库存放这些角色, Ansible 提供了 ansible-galaxy 来安装依赖,所以再每个项目下都有如下 Ansible 配置文件:

  • ansible.cfg -- 指定 hosts 位置

  • deploy/ansible/hosts -- 配置主机

  • deploy/ansible/requirements.yml -- 指定依赖

    内容如下

    - src: git@gitlab.example.com:username/ansible-roles
      scm: git
      name: ansible-roles
    
  • deploy/ansible/dev-playbook.yml -- 测试环境部署规则,指定变量和角色实现部署

  • deploy/ansible/prod-playbook.yml -- 生产环境部署,指定变量和角色实现部署

现在部署相关的就都开始使用 Ansible,部署命令如下:

# 需要在控制机上安装依赖
$ ansible-galaxy install -r deploy/ansible/requirements.yml
$ ansible-playbook deploy/ansible/dev-playbook.yml

这里 ansible-galaxy 有个坑,就是不支持更新,要想更新已安装的 ansible 角色需要手动删除并重新安装:

$ rm ~/.ansible/roles/ansible-roles
$ ansible-galaxy install -r deploy/ansible/requirements.yml

Deploy Key 到 SSH agent forwarding

在部署的过程中需要在服务器上拉取代码,就涉及到仓库权限的问题我们一开始的解决办法是部署之前通过 Fabric 上传一个 Deploy Key 到目标服务器,在部署完成之后再将对应的 Deploy Key 删除。

一个 Deploy Key 只对应一个仓库的只读权限,这种模式在前期是没有问题的,但是到了后期我们把通用库拆分到独立的仓库通过 pip 进行安装时就遇到了问题, 这个时候我们开始抛弃 Deploy Key 改为使用 SSH agent forwarding,具体请参见 Using SSH agent forwarding

ORM 使用

手写 SQL

早期我们使用手写 SQL 的方式与数据库交互,慢慢的我们发现这种方式存在一些问题:

  • 大块 SQL 语句在代码中异常丑陋
  • 非常容易编写错误的 SQL 语句
  • 代码 Review 过程中要额外注意 SQL 注入相关问题
  • 为了防止注入,根据条件拼接 SQL 语句比较困难同时拼接代码看起来丑陋并且难以理解

SQLAlchemy Core

后来发现 SQLAlchemy 对外提供的接口是分为两层的:

  • Core -- 语句生成引擎
  • ORM -- 基于语句生成引擎的 ORM

发现只使用 Core 和手写 SQL 并无太大区别,但是解决了上面的所有问题,请看下面示例:

import sqlalchemy as sa

from . import db_engine
from . import Table


with db_engine.connect() as db:
    db.execute(
        Table.select()
        .where(
             (Table.c.id == 1)
             &
             (Table.c.sex == "male")
             &
             Table.c.is_valid
        )
        .order_by(sa.desc(Table.c.id))
    )

同时也可以支持复杂的 SQL 语句,具体请参见 文档

Django ORM

再后来的新项目都采用了 Django 并使用 Django 自带的 ORM。

本地开发

我们应用的服务依赖较少,目前只依赖 MySQL 和 Redis。一开始大家都通过统一连接内网的同一服务进行本地开发, 这种开发模式会带来一个问题:

  1. 假设其中一名开发人员删除了一个字段,并调整了对应的代码但是没有提交
  2. 由于我们基本上是 TDD 的开发模式,此时就会导致其他人的单元测试无法正常运行

基于这种模式我们本地开发引入 Docker,使用 Docker 在本地启动 MySQL 和 Redis 服务,在我们的通用库里提供以下两个文件:

  • docker-compose.yml

    version: '3'
    services:
        mysql:
            image: mysql:5.6
            restart: always
            ports:
                - "3306:3306"
            volumes:
                - ~/.botpy/etc/mysql/conf.d:/etc/mysql/conf.d
                - ~/.botpy/data/mysql:/var/lib/mysql
            environment:
                MYSQL_ROOT_PASSWORD: root-password-you-should-replace
    
        redis:
            image: redis
            restart: always
            ports:
                - "6379:6379"
    
  • init-db.py -- 包装 mysqldump 和 mysql 命令实现同步内网数据库的脚本

数据库迁移最终流程就变为:

  1. 在本地编写并调试,并先应用到本地
  2. 提交 MR 在内网的测试数据库进行验证
  3. 验证通过后合并 MR 触发部署
    1. 迁移应用到内网的开发数据库
    2. 部署到测试数据库

数据库迁移

SQLAlchemy Migrate

早期我们是通过手动收集变更的 SQL 语句到指定文件下,然后在上线之前手动在数据库进行执行。后来这块就导致了很多上线问题, 主要是忘记收集和忘记执行,后面在找这方面的解决方案时发现了 SQLAlchemy Migrate ,经过简单的改造之后用起来还算舒心。

我们在用的过程中遇到的主要问题中文编码问题,经过排查是由于 sqlparse 库导致的,由于没有暴漏相关接口加上 SQLAlchemy Migrate 是由 OpenStack 维护,想要贡献代码非常困难,所以就通过在 manage.py 增加如下代码来解决:

import functools
import sqlparse


# HACK!!: 替换原函数修复 sqlalchemy-migrate 的编码 bug
sqlparse.format = functools.partial(sqlparse.format, encoding="utf8")

Alembic

虽然 SQLAlchemy Migrate 已经满足需求,但是在用的过程中发现会有两个问题:

  1. 社区更新不积极,
  2. 由于 SQLAlchemy Migrate 仅记录最后一个版本号,不便于多人开发,考虑如下场景:
    1. A 增加了版本 001-foo
    2. B 没有拉取代码也进行迁移就会也增加一个 001-bar
    3. A 的代码合并并部署,001-foo 会被执行(数据库标记 001 已执行)
    4. B 的代码合并并部署,由于 001 被标记为已执行 001-bar 不会进行执行

这时候发现 SQLAlchemy 的作者推出了 Alembic,经过简单的尝试发现该工具更加强大支持类似 Git 的版本控制, 开发也比较活跃,解决了上面两个问题。所以在后续就使用 Alembic 替换了 SQLAlchemy Migrate

Django Migrate

后续 Django 的新项目就开始采用 Django 自带的数据库迁移方案,这里就不再细述。

单元测试

测试数据库

我觉的我们之所以能成功的推行了单元测试,并将之作为日常开发中衡量代码的标准之一,最大的功劳就是解决了测试数据库相关问题, 就像我之前的文章提到的:

很长一段时间以来写单元测试都类似写执行脚本,运行一下然后看一下结果。

很大的原因就是没有解决测试数据库相关的问题,比如我写了一个测试然后在我本地数据库填充了数据,测试通过了。然后后面数据再变动测试就失败了。 为了解决这个问题我自己首先实现了一个基于 unittest 的测试收集和运行器,然后在测试运行开始之前插入一段代码做如下事情:

  1. 连接配置文件中的数据库并读取表结构信息
  2. 根据一定规则生成创建一个新的数据库
  3. 将读取的表结构信息应用到新数据库
  4. 加载测试包下的一些 SQL 文件并在新的数据库中执行
  5. Patch 配置文件,将数据库名调整到新的数据库

后面我们切换到 pytest 作为 test runner 后将这一块逻辑封装成了一个 pytest 的插件。加上后面上的 mock 我们的单元测试才真正的完善。 这也造就了我们大部分项目都达到了 80% ~ 95% 的单元测试覆盖率,同时也为我们之后迁移 Python 3 打下了很好的基础。

pytest 作为 test runner

前面也提到了,一开始我们用的是基于 unittest 自己实现的测试发现、收集和运行的工具,这一块也是我受之前一家公司的影响, 后来发现 pytest 除了是一个强大的测试框架,同时也可以单独用来作为 test runner 使用,我不太喜欢 pytest 这种函数式方式 编写测试,很多 fixture markup 感觉太过隐式,所以我只拿 pytest 作为 test runner,单元测试还是使用 unittest 那一套。

pytest 作为 test runner 对比 unittest 的好处是:

  • 社区活跃
  • 生态好,插件多
  • 结合插件系统非常容易扩展和自定义

mock

参见之前的 博文

tox

开始每次运行测试构建测试相关的 Python 环境会比较麻烦,后面接触了 tox 可以很方便的构建测试环境, 同时支持多个 Python 版本环境构建。

CI/CD

结识 bors 和 homu

有一段时间我特别关注 Rust 社区,看到他们有一个机器人专门跑单元测试和合并 PR,我就开始思考能不能用到我们的项目中, 经过简单的观察我首先接触到了 bors ,初次使用后感觉真心酷炫,但是由于它是采用轮询会有如下两个问题:

  1. 时效性不好
  2. 容易达到 GitHub 接口请求次数上限
  3. 还有其他一些功能上的不全

这时候我就发现 Rust 社区早一不使用 bors 而是改为使用 homu 了,具体的信息可以参见之前的一篇 文章

GitHub

参见 Python github 私有项目通过 buildbot 进行 Review 。 后面也尝试迁移到 buildbot 0.9,具体方案参见 Add buildbot 0.9 support steps in README.md #119

GitLab

Pipelines

迁移到 GitLab 之后就弃用了 buildbot 改用 GitLab 自身的那一套 CI/CD。具体参见 GitLab CI/CD

我们 CI 构建用的自己构建的 Docker 镜像,里面集成了一些基础依赖包和部署需要的相关信息。

homu-gitlab

由于业务扩张 GitHub 上的私有仓库成本开始提升,所以就开始考虑迁移自建的 GitLab,这时面临一个问题就是怎么保持现有的工作流不变, 首先就是要找到一个 homu 的 GitLab 版实现,经过自己的搜寻后并没有发现合适的,所以我就尝试 Fork 了一份尝试自己迁移到 GitLab, 最终成功的迁移并应用到 GitLab 中来,参见 coldnight/homu-gitlab

一些不完美的地方

  1. 需要依赖 SSH 私玥

    由于 GitLab 的接口并没有 GitHub 那么完善,没有相关合并 MR 的接口,所以将之前主要依赖 GitHub 接口的部分都统一改成了通过 git 命令操作。

  2. 同步功能未实现,每次同步需要通过重启实现

持续进化

在上面完成之后最近还做了一些调整和优化:

  1. 启用 GitLab CI/CD 缓存来加速和优化 CI 构建
  2. 内网搭建 Docker Registry 统一托管和构建 CI Docker 镜像(这部分也是自动化的)
  3. 通过一个仓库托管 homu 的配置文件,每次调整提交后自动重启 homu 服务

pre-commit

我们一开始代码检测主要利用 Git 的 pre-commit 自己编写 shell 脚本来组合,后来发现 pre-commit 后开始统一替换为 pre-commitpre-commit 的作者也推出了其他很多代码检测相关的工具,我也应用到了我们自己的项目上。

Python

参见 迁移到 Python 3

总结

简单总结一下目前整个技术栈:

  • Python 3
    • Django/Tornado
    • SQLAlchemy
    • pytest
    • tox
    • Alembic
    • Celery
  • Ansible
  • GitLab
    • CI/CD
    • homu-gitlab
    • pre-commit
  • Docker
    • docker-compose
  • Vue
  • Sentry

Category: Python Tagged: Python 技术选型 CI/CD GitLab

comments


迁移到 Python 3

Thu 13 July 2017

前段时间(2017-06-07)我开始决定将公司现有的项目逐渐的迁移到 Python 3. 主要原因有一下几点:

促成我决定迁移到 Python 3 的主要原因是公司最大的项目的单元测试覆盖率经过一段时间的迭代终于达到了 80% 以上.

迁移方案

由于项目巨大任务艰巨无法短时间内就将项目迁移到 Python 3, 而且当前还有产品上的功能需要迭代. 所以迁移方案是同时兼容 Python 2 和 3, 并在迁移完成之后移除对 Python …

Category: Python Tagged: 2to3

comments

Read More

Python github 私有项目通过 buildbot 进行 Review

Sun 22 May 2016

背景

随着公司开发团队的壮大, 团队中每个人的水平参差不齐, 为了保证项目质量我们打算对 提交的代码进行 review, 但是苦于一直没有好的 review 机制. 前段时间我在逛 Rust 社区是发现了他们有一个 review 机器人 Homu 非常不错, 研究一下后我将其应用到我们当前 Python 项目中来配合 review, 我感觉非常棒, 今天抽空就分享给大家.

技术栈

本文涉及的项目和技术有:

0. 隔离 Github 部署 Key

Github 可以添加部署 Key 来实现部署, 但是每个项目必须是不同的部署 key. 这就给 多个私有项目的可持续集成带来一定的困难, 因为 buildbot 是通过轮询来获取 git 分支 变更的, 并且 buildbot 不支持指定私钥 …

Category: Python Tagged: Python github 私有 可持续集成 homu buildbot review

comments

Read More

Python mock 使用心得

Sun 03 April 2016

好久没有更新博客, 趁着清明节小长假和我儿子正在睡觉更新一篇刷刷存在感. 近来变化很多, 儿子也有了, 工作上也有很多收获. 这篇博客就分享一下关于 mock 的使用的心得体会.

很长一段时间以来写单元测试都类似写执行脚本, 运行一下然后看一下结果. 这里面有一部分原因是因为无法规避外部的依赖组件, 比如:

  • 数据库操作
  • 外部接口调用
  • 外部其他不可控因素

这样写测试只关心当前测试的结果, 而不去管其他测试是否 passed.

后面随着团队开始进新人, 由于团队里每个人的标准和水平不同, 开始不得不重视整体项目的质量, 发现没有好的测试就没有统一的标准来衡量提交代码的质量, 当然说到代码质量还有另外一个和测试放在一起的标准就是代码风格, 这不是本文的主题所里这里就暂且不提.

为了能写好测试就不得不面对现实项目的复杂性, 诸如外部接口数据库操作等. 这时开始将目光转向 mock, 因为之前有听过类似概念, 但是还是有误解, 以为把要测的东西都模拟掉了还测试什么呢? 但是真正的了解 mock 之后才完整的理解了单元测试.

单元测试应该只针对当前单元进行测试, 所有的外部依赖应该是稳定的, 在别处进行测试过的. 使用 mock 就可以对外部依赖组件实现进行模拟并且替换掉, 从而隐藏外部组件的实现, 使得单元测试将焦点只放在当前的逻辑(当前单元),

安装

mock 在 Python3 中是内置的, 直接 import …

Category: Python Tagged: Python mock unittest

comments

Read More

Python 内存泄露实战分析

Mon 30 March 2015

引子

之前一直盲目的认为 Python 不会存在内存泄露, 但是眼看着上线的项目随着运行时间的增长 而越来越大的内存占用, 我意识到我写的程序在发生内存泄露, 之前 debug 过 logging 模块导致的内存泄露.

目前看来, 还有别的地方引起的内存泄露. 经过一天的奋战, 终于找到了内存泄露的地方, 目前项目 跑了很长时间, 在业务量较小的时候内存还是能回到刚启动的时候的内存占用.

什么情况下不用这么麻烦

如果你的程序只是跑一下就退出大可不必大费周章的去查找是否有内存泄露, 因为 Python 在退出时 会释放它所分配的所有内存, 如果你的程序需要连续跑很长时间那么就要仔细的查找是否 产生了内存泄露.

场景

如何产生的内存泄露呢, 项目是一个 TCP server, 每当有连接过来时都会创建一个连接实例来进行 管理, 每次断开时连接实例还被占用并没有释放. 没有被释放的原因肯定是因为有某个地方对连接 实例的引用没有释放, 所以随着时间的推移, 连接创建分配内存, 连接断开并没有释放掉内存, 所以 就会产生内存泄露.

调试方法

由于不知道具体是哪里引起的内存泄露, 所以要耐心的一点点调试.

由于知道了断开连接时没有释放, 所以我就不停的模拟创建连接然后发送一些包后断开连接, 然后通过下面一行 shell 来观察内存占用情况 …

Category: Python Tagged: Python 内存 泄露 引用 回收 交叉

comments

Read More

logging 模块误用导致的内存泄露

Sat 31 January 2015

首先介绍下怎么发现的吧, 线上的项目日志是通过 logging 模块打到 syslog 里, 跑了一段时间后发现 syslog 的 UDP 连接超过了 8W, 没错是 8 W. 主要是 logging 模块用的不对

我们之前有这么一个需求, 就是针对每一个连接日志输出当前连接的信息, 所以每一个 连接就创建了一个日志实例, 并分配一个 Formatter, 创建日志实例为了区分其他连接 所以我就简单粗暴的用了当前对象的 id 来作为日志名称:

import logging


class Connection(object):
    def __init__(self):
        self._logger_name = "Connection.{}".format(id(self))
        self.logger = logging.getLogger(self._logger_name)

当然测试环境是开 DEBUG …

Category: Python Tagged: Python logging 内存泄露

comments

Read More

基于 Python 生成器的 Tornado 协程异步

Fri 19 December 2014

Tornado 4.0 已经发布了很长一段时间了, 新版本广泛的应用了协程(Future)特性. 我们目前已经将 Tornado 升级到最新版本, 而且也大量的使用协程特性.

很长时间没有更新博客, 今天就简单介绍下 Tornado 协程实现原理, Tornado 的协程是基于 Python 的生成器实现的, 所以首先来回顾下生成器.

生成器

Python 的生成器可以保存执行状态 并在下次调用的时候恢复, 通过在函数体内使用 yield 关键字 来创建一个生成器, 通过内置函数 next 或生成器的 next 方法来恢复生成器的状态.

def test():
    yield 1

我们调用 test 函数, 此时并不会返回结果, 而是会返回一个生成器

>>> test()
<generator object test at 0x100b3b320>

我们调用其 next …

Category: Python Tagged: Python generator coroutine 协程 生成器 Tornado

comments

Read More

Python 入门指南

Fri 23 May 2014

引子

经常能在 Python 群里看到很多新人在问一些非常基础的问题, 基本每天都在重复的问这些问题, 在这里就总结一下这些问题.

首先声明, 本文不打算教会你 Python, 本文力图陈列一些新手容易遇到的问题, 并企图教会你 如何学习 Python, 在遇到问题的时候如何提问.

关于版本

学习 Python 的第一步需要选择版本, Python 3.x 和 2.x 的断层较大, 3.x 不向后兼容 2.x. Python 现在主流应该还是 Python 2.7, Python 2.7 将会是 Python 2.x 的最后一个版本, 并且 会支持到 2020 年. 但是 Python 3 …

Category: Python Tagged: Python 入门 指南 新手 错误 书籍 工具

comments

Read More

Tornado 多进程实现分析

Fri 11 April 2014

引子

Tornado 是一个网络异步的的web开发框架, 并且可以利用多进程进行提高效率, 下面是创建一个多进程 tornado 程序的例子.

#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
import time

import tornado.web
import tornado.httpserver
import tornado.ioloop
import tornado.netutil
import tornado.process


class LongHandler(tornado.web.RequestHandler):

    def get(self):
        self.write(str(os.getpid()))
        time.sleep(10)


if __name__ …

Category: Python Tagged: Python fork_processes tornado 多进程 web 提升 效率

comments

Read More
Page 1 of 5

Next »

Fork me on GitHub