Skip to content

Instantly share code, notes, and snippets.

@meowoodie
Last active August 29, 2015 14:22
Show Gist options
  • Save meowoodie/4a5891fe24362a0f3e24 to your computer and use it in GitHub Desktop.
Save meowoodie/4a5891fe24362a0f3e24 to your computer and use it in GitHub Desktop.

Senz CI 帮助文档

@Authored by: Woodie

@Updated at: Thursday, June 11, 2015

工作环境

  • Jenkins 持续集成系统工具
  • LeanCloud 测试和生产部署环境
  • DaoCloud 测试和生产部署环境
  • Github 代码库以及版本控制工具

基本概念

Jenkins

一个持续集成环境需要包括三个方面要素:代码存储库、构建过程和持续集成服务器。这里代码存储库我们采用的Github,持续集成服务器是Senz项目下的AliYun 服务器,你可以登陆Senz项目[Jenkins管理端][]来查看和管理当前的Jenkins Job(每一个job都是一连串的交互操作,需要在管理端进行相应的配置,以实现项目自动化测试和部署),构建过程即为部署在持续集成服务器上的开源工具 Jenkins。 Jenkins提供了一种易于使用的持续集成系统,使开发者从繁杂的集成中解脱出来,专注于更为重要的业务逻辑实现上。同时 Jenkins 能实施监控集成中存在的错误,提供详细的日志文件和提醒功能,还能用图表的形式形象地展示项目构建的趋势和稳定性。

LeanCloud

[LeanCloud][]是国内针对移动应用的一站式BaaS云端服务。在可见的未来,我们大部分的NodeJS后端服务和部分独立的Python(django或者flask)后端服务都会部署在LeanCloud环境下。LeanCloud为我们的移动应用产品提供完整的数据存储、代码测试环境、生产环境(docker容器)等服务,并且提供了一键式的deploy和publish项目API。 [LeanCloud]: https://leancloud.cn/

DaoCloud

[DaoCloud][]是国内首个Docker Hub镜像服务,提供互联网应用的持续集成、镜像构建、发布管理、容器托管解决方案。 目前,我们所有大计算量的算法子服务均会部署在DaoCloud环境上。DaoCloud的优点是界面友好,操作简单,容易上手,因此即使是不懂docker的新手来操作也能分分钟搞定。 [DaoCloud]: https://www.daocloud.io/

整体流程

任何项目对应一个github repo,每个repo目前有两个branch,分别是dev和master,每个branch需要维护一个独立的App,分别用于开发环境(dev branch)和生产环境(master branch)。 注意 开发和生产环境是两个完全独立的App,各自拥有自己的environment、db以及不同的代码branch(属于同一个repo)。 在部署好了环境和CI流程后,一个项目从开发到上线至生产环境的大致流程如下:

  • 在dev branch上进行日常的项目开发,开发了一个新的feature后,拉取代码到___本地环境___进行单元测试
  • git push origin dev会触发开发环境下代码的单元测试环节;
  • 代码调试无误,通过单元测试后,发布到___开发环境___,在真实环境中进行线上测试
  • git tag dev会触发线上测试环节;
  • 当积累了一定量的feature,并且在开发环境上运行一段时间没有问题,可以发布到___生产环境___,进行生产部署
  • 你也可以在生产部署前进行一次单元测试,以确保代码无误;
  • git push origin master会触发生产环境下代码的单元测试环节;
  • git tag prod会触发生产部署环节;

NodeJS后端服务(数据流操作)

  • 使用LeanCloud提供的最新LeanEngine环境开发;
  • 在LeanCloud上[配置git部署][]
  • 项目中需要加入判断工作环境代码,利用环境变量来区分现在是生产环境开发环境还是本地环境,不同的工作环境使用不同的log系统(即不同的第三方log工具token),不同的数据库(详见下文“生产环境和开发环境”章节)。
  • 开发项目用github做代码库和版本控制(详见下文“Jenkins CI”章节),rollbar和logentries做错误处理和日志记录(详见下文“项目日志和异常处理”章节),express的supertest做单元测试(详见下文“单元测试”章节);
  • 在github上创建代码库,每当需要发布最新的release版本时,使用git tag + 版本号(版本号统一使用vX.X.X的形式);
  • 在[Jenkins管理端][]创建testJob,每当代码库git push到master分支时,自动触发该项目test事件,执行test脚本,报告测试结果(详见下文“Jenkins CI”章节);
  • 在[Jenkins管理端][]创建publishJob,每当git tag发布最新release版本时,自动触发该项目的publish事件,首先会执行所有的test脚本,测试通过后再请求LeanCloud的deploy和publish API,启动项目部署到开发环境中(详见下文“Jenkins CI”章节)。 [Jenkins管理端]: http://182.92.72.69:8080/ [配置git部署]: https://leancloud.cn/docs/leanengine_guide-node.html#部署

Python后端服务(算法模块)

生产环境和开发环境

我们需要区分代码的工作环境,主要是依靠识别环境变量,我们定义APP_ENV为识别工作环境的环境变量名。每次程序启动时都会读取本地名为“APP_ENV”的环境变量值,该环境变量有三种取值:“dev”、“prod”和“local”分别代表开发环境、生产环境和本地环境。

相关配置

不同的工作环境需要有不同的配置,主要包括:

  • 数据库:目前我们所有的数据库都是依托在LeanCloud平台上,而一个项目一般对应一个到多个LeanCloud数据库class。因此不同的工作环境我们需要使用不同的class,如果一个项目用到了N个class,那么实际应该建立2*N个class。对于生产环境的class如果命名为XXX,那么开发环境的class则为dXXX。
  • log系统:目前我们使用了logentries和rollbar两个第三方log服务,因此不同的工作环境对应不同的logentries和rollbar的token即可。
  • 不同的容器:对于在daocloud上的项目而言,需要为不同的工作环境配置不同的容器。(详见下文“DaoCloud CI”章节)

数据库备份

由于每个项目都需要维护两套同样的数据库,因此需要自己为自己的项目编写一个脚本,来定期同步两个数据库中的所有数据,日后我们可能会提供这样的脚本接口供你使用。

Example

我们有一个完整的项目以供参考。这是一个flask项目,用来提供poi概率计算服务,你可以参考该项目的[根据环境变量配置token][]例子来进一步理解。 [根据环境变量配置token]: https://github.com/petchat/senz.middleware.poi.poiprob/blob/master/flask_app/poi_analyser_lib/config.py

项目日志和异常处理

任何开发项目都需要对运行代码的重要输出和错误异常进行记录,我们采用logentries+rollbar组合方案来实现项目的日志记录和异常处理工作。

  • Rollbar 功能强大,可以用于记录代码输出,上传捕获和未捕获到的异常到云端。针对我们的需求,我们仅使用rollbar上传未捕获到的异常(Uncaught exception)的feature,本质上rollbar在项目框架上深度定制了一个middleware,以此来捕获哪些我们没有catch到的exception。可以查看[rollbar文档][]来深入了解。
  • Logentries 主要用于记录代码中的各种输出,并同样上传到服务器,提供统一友好的用户UI方便开发者浏览查找。其优势在于对输出信息进行细致的分级记录,分别包括info、warning、debug、error四个等级的日志记录类型,并且提供丰富的查询接口来方便开发者快速定位到希望查看的对应日志信息。可以查看[logentries文档][]来深入了解。
  • 目前在flask项目中rollbar和logentries之间有一定的兼容性问题,主要体现在二者都使用了python内建的logger模块来进行log消息,导致两个模块分别向自己的服务器发送log记录时出现冲突,目前解决冲突的唯一方法是请求flask项目的http header中不加content-type字段。 [rollbar文档]:https://rollbar.com/docs/ [logentries文档]:https://logentries.com/doc/

日志类型

对于项目代码的输出信息我们规定包括四个级别,分别是:INFO,DEBUG,WARNING,ERROR。每个级别对应的输出信息应该是:

  • INFO: 代码关键环节的信息,以[模块名] 输出信息的格式输出;
  • DEBUG:需要输出的重要参数信息,以[模块名] 参数名: 参数内容的格式输出;
  • WARNING:出现了不合法的代码环节处,但是并不影响代码的执行(可能原因例如:前后版本不一致,导致输入参数有所区别但是不会致错),以[模块名] 警告信息的格式输出;
  • ERROR:代码抛出异常,可能会造成执行失败,该错误一般为已知错误类型,或自己定义的错误,以[错误类型] 错误信息. TRACEBACK: traceback记录的格式输出。

自定义错误

为了方便日后在数据量庞大的生产环境中快速定位到我们需要查找的错误,或者对错误信息进行统计分析。我们需要简单的定义一些自己的错误类型,一般是以一个exception.py或者exception.js的形式。

  • 定义错误类型原则:
    • 只对该项目中核心处理模块定义自己的错误,而例如输入参数不合法等数据整理、准备过程中出现的一些常见错误直接catch住并抛出即可;
    • 核心处理模块中出现的任何可能的错误都尽可能merge到自己定义的错误中,保证核心模块只会抛出自己定义的错误,这样一旦核心模块出问题我们就能很很清楚的知道错误类型的范围;
    • 自己定义的错误类型不宜太多太复杂,基本描述清楚核心模块可能出错的几个环节即可。
  • 如何反馈错误:
    • 对于我们已知的错误,无论是常见系统定义的错误还是我们自己定义的错误,都由logentries.error汇报到logentries服务端。
    • 对于我们未知的错误,即我们没有catch住得错误,都由rollbar汇报到rollbar服务端(In fact,rollbar只用来负责汇报哪些我们无法catch到的未知错误,具体实现原理见本章“概述”)
  • 输出错误信息的要求:
    • exception文件中需要定义一个exception基类(这个基类继承自系统内建的exception),其他所有其他的exception类型都继承自这个基类,该基类主要负责收取当前出错时的traceback。
    • 对于一个自定义exception,首先需要继承自己定义的exception基类,然后该exception需要有接口能返回当前错误收取到的一些基本信息,例如python项目中可以用
    def __str__(self):
        return "Exception info"
    来返回错误内容。

Example

我们有一个完整的项目以供参考。这是一个flask项目,用来提供poi概率计算服务,你可以参考该项目的[exception定义][],以及如何在[核心模块中抛出自定义异常][],如何在[主程序中抛出常见错误和自定义错误][]。 [exception定义]: https://github.com/petchat/senz.middleware.poi.poiprob/blob/master/flask_app/poi_analyser_lib/exception.py [核心模块中抛出自定义异常]: https://github.com/petchat/senz.middleware.poi.poiprob/blob/master/flask_app/poi_analyser_lib/predictor.py [主程序中抛出常见错误和自定义错误]: https://github.com/petchat/senz.middleware.poi.poiprob/blob/master/flask_app/app.py

单元测试

任何项目在上线进入生产环境前都需要进行不同程度的单元测试,保证代码在各个环节都正常运行后才能投入使用。目前python flask项目采用flask自带的unittest模块进行单元测试,而NodeJS LeanCloud项目下采用express框架下的supertest。

Jenkins CI

下面介绍一下Jenkins里的相关操作和概念,以及在代码管理上的一些建议。Enjoy it!

如何创建testJob

首先需要登录到我们的Senz Jenkins管理端,账号和密码见trello的Account Card

  • testJob本质上是一个Jenkins Job,登录后首先点击左上角的New Item,来创建一个新的Jenkins Job;

  • 输入Item name,以格式

    senz.xxx.xxx_Test 
    

    选择freestyle project

  • 进入configure页面,项目名即为刚刚设定的Item name;

  • 输入github对应代码库url,来指定项目代码库;

  • 勾选Restrict where this project can be run,以限定该job最终运行环境为我们指定的机器,因为是testJob,所以部署在我们的aliyun 1服务器上,Label Expression内填python_main_server。你可以在主页左下方上查看Senz项目的主机情况,每个主机都有一个对应的label;

  • Source Code Management选择github,并填写相应的信息,Branch Specifier内填写*/master,以指定只检查master branch下的变化;

  • Build Triggers选择Build when a change is pushed to GitHub,每次master branch上的代码发生变化时触发build下的操作;

  • Build下的Excute shell中填写执行测试用例的shell脚本,例如:

    pip install -r ./flask_app/requirements.txt
    nosetests ./flask_app/test.py 
  • 点击save和apply来保存和生效配置信息

  • 配置完成后如果你想立即运行,可以点击左侧的Build Now来立即执行一遍生效的Job,同时在console output查看服务器的运行输出

如何创建publishJob

publishJob本质上是两个JenkinsJob,第一个Job和testJob几乎一样,除了:

  • 项目名为:
    senz.xxx.xxx_Pretest 
    ```;
    
  • Branch Specifier内填写refs/tags/*,以指定一旦有新的tag产生则触发build操作;
  • 以及Post-build Actions的Build other projects中Projects to build填写第二个Job的项目名,并选择Trigger only if build is stable,以指定该Job完成后执行Publish Job。

第二个Job用来在生产环境部署代码,操作也很简单,主要区别如下:

  • 第二个项目名称为:
    senz.xxx.xxx_Publish
    
  • 指定项目的代码库,和master branch
  • Restrict where this project can be runBuild Triggers均不用选择。需要说明的是publishJob的build操作仅用向LeanCloud发送很轻量的HTTP请求即可,因此不用指定具体哪一台机器来执行这个Job,其次;而build的触发由上一个pretestJob来触发,因此不由其他触发源触发,因此也不用特殊指定。
  • Build下的Excute shell里填写请求LeanCloud后台的脚本,用curl即可,示例如下:
curl -X PUT -H "x-uluru-application-key:q37phyhnzh6k376oiae4stvbrklp2m9txh5yymnaxr4lr3zr" -H "x-uluru-application-id:knqxgvqzvz5qxlzy4xyu1s45kskq1x0allm6en72pi01ulw4" https://leancloud.cn/1/functions/

curl -X PUT -H "x-uluru-application-key:q37phyhnzh6k376oiae4stvbrklp2m9txh5yymnaxr4lr3zr" -H "x-uluru-application-id:knqxgvqzvz5qxlzy4xyu1s45kskq1x0allm6en72pi01ulw4" https://leancloud.cn/1/functions/publishFunctions

注意: + key和id需要改成自己项目对应的token。 + LeanCloud的部署命令默认从git源的master分支上拉取代码(这也是为什么一旦你push代码或者merge代码到master分支上后我们需要触发publishJob)

DaoCloud CI

DaoCloud主要分为两部分CI工作,分别是代码构建和持续集成

大致流程

  • 代码构建:
    • 由git tag触发
    • 从配置的git repo上拉取最新的master branch代码
    • 根据项目工程根目录下的Dockerfile指导build docker image
    • run docker container(启动容器)
  • 持续集成:
    • 由git push触发
    • 从配置的git repo上拉取最新的master branch代码
    • 根据项目工程根目录下的daocloud.yml中指定的image,来指导build docker image
    • run docker container(启动容器)
    • 配置daocloud.yml中env下的环境变量
    • run daocloud.yml中install下的命令(在这里配置所需环境)
    • run daocloud.yml中before_script下的命令(在这里输出环境变量值)
    • run daocloud.yml中script下的命令(在这里执行nosetests)

Github代码管理建议

我们推荐任何一个开发项目都持有两个branch,分别是masterdev

  • 日常的项目开发和bug调试都在dev下进行,只有当代码可以运行,需要进行测试的时候才merge到master branch上。
  • 同样的,每当项目阶段性的开发结束后,需要发布release版,要求(不是推荐)git tag vX.X.X,因为每次git tag都会触发Jenkins的publishJob执行。
@bboalimoe
Copy link

rest api 部署需要写个脚本否则无法缺人部署成功与否。

@bboalimoe
Copy link

dev branch的代码要先跟其他dev app跑几天后才可以merge到master。所以文档还缺少dev app的创建过程。

@lujiaying
Copy link

DaoCloud CI 部分,是否能明确写出 持续集成部分使用的image 并不是我们的代码image,而只是用于测试的环境image。。 我做的时候感觉还是蛮费解的

比如说,该项目如果是python项目,image就应该指定为daocloud/ci-python:2.7

@lujiaying
Copy link

可以参考该项目的根据环境变量配置token例子来进一步理解

不同环境的token怎么配? 是指分别在logentries, rollbar设置对应的三个环境的三个不同项目的token吗 ?

@lujiaying
Copy link

logentries 和 rollbar 搭配需要写一些额外的东西,是否需要提供一个模版呢?

是按照说明文档里说的 请求flask项目的http header中不加content-type字段。 还是?

比较推荐的Python的 logger写法如下:

import logging

logger = logging.getLogger(__name__)

但好像我们的项目中必须写成这样:

import logging

logger = logging.getLogger('rollbar')

@bboalimoe
Copy link

shixiang 这个项目放到一个github project的docs里把~ 好pull request

@bboalimoe
Copy link

然后 tyk 的api 命名规则我定一下。你给我整合到一块儿。

@bboalimoe
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment